Repository: tursodatabase/turso Branch: main Commit: 167093a327b5 Files: 2057 Total size: 28.0 MB Directory structure: gitextract_3njjk3d9/ ├── .cargo/ │ └── config.toml ├── .claude/ │ └── skills/ │ ├── async-io-model/ │ │ └── SKILL.md │ ├── cdc/ │ │ └── SKILL.md │ ├── code-quality/ │ │ └── SKILL.md │ ├── debugging/ │ │ └── SKILL.md │ ├── differential-fuzzer/ │ │ └── SKILL.md │ ├── index-knowledge/ │ │ └── SKILL.md │ ├── mvcc/ │ │ └── SKILL.md │ ├── pr-workflow/ │ │ └── SKILL.md │ ├── storage-format/ │ │ └── SKILL.md │ ├── testing/ │ │ └── SKILL.md │ └── transaction-correctness/ │ └── SKILL.md ├── .config/ │ └── nextest.toml ├── .devcontainer/ │ ├── Dockerfile │ ├── Dockerfile.squid │ ├── devcontainer.json │ ├── docker-compose.yml │ ├── init-firewall.sh │ └── squid.conf ├── .dockerignore ├── .github/ │ ├── labeler.yml │ ├── pull_request_template.md │ ├── shared/ │ │ ├── install_sqlite/ │ │ │ └── action.yml │ │ ├── setup-mold/ │ │ │ └── action.yml │ │ └── setup-sccache/ │ │ └── action.yml │ ├── turso-bot.yml │ └── workflows/ │ ├── antithesis-schedule.yml │ ├── antithesis.yml │ ├── build-sim.yml │ ├── build-sqlancer.yml │ ├── c-compat.yml │ ├── claude.yml │ ├── codspeed.yml │ ├── dotnet-publish.yml │ ├── dotnet-test.yml │ ├── elle.yml │ ├── fuzz.yml │ ├── go.yml │ ├── java-publish.yml │ ├── java.yml │ ├── labeler.yml │ ├── napi.yml │ ├── perf_nightly.yml │ ├── publish-crates.yml │ ├── python.yml │ ├── react-native.yml │ ├── release.yml │ ├── rust.yml │ ├── rust_perf.yml │ ├── sqltest.yml │ ├── stale.yml │ └── turso-serverless.yml ├── .github.json ├── .gitignore ├── .python-version ├── AGENTS.md ├── CHANGELOG.md ├── COMPAT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── Dockerfile.antithesis ├── Dockerfile.cli ├── LICENSE.md ├── Makefile ├── NOTICE.md ├── PERF.md ├── Pipfile ├── README.md ├── bindings/ │ ├── dotnet/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── Makefile │ │ ├── Readme.md │ │ ├── Turso.slnx │ │ ├── rs_src/ │ │ │ └── lib.rs │ │ └── src/ │ │ ├── Benchmarks/ │ │ │ ├── Benchmarks.cs │ │ │ ├── Benchmarks.csproj │ │ │ └── Program.cs │ │ ├── Turso/ │ │ │ ├── Turso.csproj │ │ │ ├── TursoCommand.cs │ │ │ ├── TursoConnection.cs │ │ │ ├── TursoConnectionOptions.cs │ │ │ ├── TursoDataReader.cs │ │ │ ├── TursoParameter.cs │ │ │ ├── TursoParameterCollection.cs │ │ │ └── TursoTransaction.cs │ │ ├── Turso.Raw/ │ │ │ ├── Data/ │ │ │ │ ├── TursoNativeArray.cs │ │ │ │ ├── TursoNativeRowValueUnion.cs │ │ │ │ └── TursoNativeValue.cs │ │ │ ├── Public/ │ │ │ │ ├── Handles/ │ │ │ │ │ ├── TursoDatabaseHandle.cs │ │ │ │ │ └── TursoStatementHandle.cs │ │ │ │ ├── TursoBindings.cs │ │ │ │ ├── TursoException.cs │ │ │ │ └── Value/ │ │ │ │ ├── TursoEncryptionCipher.cs │ │ │ │ ├── TursoValue.cs │ │ │ │ └── TursoValueType.cs │ │ │ ├── Turso.Raw.csproj │ │ │ └── TursoInterop.cs │ │ └── Turso.Tests/ │ │ ├── Turso.Tests.csproj │ │ └── TursoTests.cs │ ├── go/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── VERSION │ │ ├── bindings.go │ │ ├── bindings_db.go │ │ ├── bindings_db_test.go │ │ ├── bindings_sync.go │ │ ├── driver_db.go │ │ ├── driver_db_test.go │ │ ├── driver_sync.go │ │ ├── driver_sync_test.go │ │ ├── go-bindings-db-tests.mdx │ │ ├── go-bindings-db.mdx │ │ ├── go-bindings-sync.mdx │ │ ├── go-driver-db.mdx │ │ ├── go-driver-sync.mdx │ │ ├── go.mod │ │ └── go.sum │ ├── java/ │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── .sdkmanrc │ │ ├── Cargo.toml │ │ ├── Makefile │ │ ├── README.md │ │ ├── build.gradle.kts │ │ ├── example/ │ │ │ ├── .gitignore │ │ │ ├── build.gradle.kts │ │ │ ├── gradle/ │ │ │ │ └── wrapper/ │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ │ ├── gradlew │ │ │ ├── gradlew.bat │ │ │ ├── settings.gradle.kts │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── tech.turso/ │ │ │ └── Main.java │ │ ├── gradle/ │ │ │ ├── publish.gradle.kts │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── rs_src/ │ │ │ ├── errors.rs │ │ │ ├── lib.rs │ │ │ ├── turso_connection.rs │ │ │ ├── turso_db.rs │ │ │ ├── turso_statement.rs │ │ │ └── utils.rs │ │ ├── settings.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ ├── examples/ │ │ │ │ │ └── EncryptionExample.java │ │ │ │ └── tech/ │ │ │ │ └── turso/ │ │ │ │ ├── JDBC.java │ │ │ │ ├── TursoConfig.java │ │ │ │ ├── TursoDataSource.java │ │ │ │ ├── TursoErrorCode.java │ │ │ │ ├── annotations/ │ │ │ │ │ ├── NativeInvocation.java │ │ │ │ │ ├── Nullable.java │ │ │ │ │ ├── SkipNullableCheck.java │ │ │ │ │ └── VisibleForTesting.java │ │ │ │ ├── core/ │ │ │ │ │ ├── SqliteCode.java │ │ │ │ │ ├── TursoConnection.java │ │ │ │ │ ├── TursoDB.java │ │ │ │ │ ├── TursoEncryptionCipher.java │ │ │ │ │ ├── TursoPropertiesHolder.java │ │ │ │ │ ├── TursoResultSet.java │ │ │ │ │ ├── TursoStatement.java │ │ │ │ │ ├── TursoStepResult.java │ │ │ │ │ └── TursoTransactionMode.java │ │ │ │ ├── exceptions/ │ │ │ │ │ └── TursoException.java │ │ │ │ ├── jdbc4/ │ │ │ │ │ ├── JDBC4Connection.java │ │ │ │ │ ├── JDBC4DatabaseMetaData.java │ │ │ │ │ ├── JDBC4PreparedStatement.java │ │ │ │ │ ├── JDBC4ResultSet.java │ │ │ │ │ └── JDBC4Statement.java │ │ │ │ └── utils/ │ │ │ │ ├── ByteArrayUtils.java │ │ │ │ ├── Logger.java │ │ │ │ ├── LoggerFactory.java │ │ │ │ └── TursoExceptionUtils.java │ │ │ └── resources/ │ │ │ ├── META-INF/ │ │ │ │ └── services/ │ │ │ │ └── java.sql.Driver │ │ │ └── turso-jdbc.properties │ │ └── test/ │ │ ├── java/ │ │ │ └── tech/ │ │ │ └── turso/ │ │ │ ├── IntegrationTest.java │ │ │ ├── JDBCTest.java │ │ │ ├── TestUtils.java │ │ │ ├── core/ │ │ │ │ ├── TursoDBTest.java │ │ │ │ └── TursoStatementTest.java │ │ │ └── jdbc4/ │ │ │ ├── JDBC4ConnectionTest.java │ │ │ ├── JDBC4DatabaseMetaDataTest.java │ │ │ ├── JDBC4PreparedStatementTest.java │ │ │ ├── JDBC4ResultSetTest.java │ │ │ ├── JDBC4StatementTest.java │ │ │ └── TransactionTest.java │ │ └── resources/ │ │ └── turso/ │ │ └── CACHEDIR.TAG │ ├── javascript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── .yarn/ │ │ │ └── releases/ │ │ │ └── yarn-4.9.2.cjs │ │ ├── .yarnrc.yml │ │ ├── Cargo.toml │ │ ├── Makefile │ │ ├── README.md │ │ ├── build.rs │ │ ├── docs/ │ │ │ ├── API.md │ │ │ └── CONTRIBUTING.md │ │ ├── package.json │ │ ├── packages/ │ │ │ ├── common/ │ │ │ │ ├── README.md │ │ │ │ ├── async-lock.ts │ │ │ │ ├── bind.ts │ │ │ │ ├── compat.ts │ │ │ │ ├── index.ts │ │ │ │ ├── package.json │ │ │ │ ├── promise.test.ts │ │ │ │ ├── promise.ts │ │ │ │ ├── sqlite-error.ts │ │ │ │ ├── tsconfig.json │ │ │ │ └── types.ts │ │ │ ├── native/ │ │ │ │ ├── README.md │ │ │ │ ├── compat.test.ts │ │ │ │ ├── compat.ts │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── package.json │ │ │ │ ├── promise.test.ts │ │ │ │ ├── promise.ts │ │ │ │ ├── tsconfig.json │ │ │ │ └── turso-sql-runner-split.test.ts │ │ │ ├── wasm/ │ │ │ │ ├── README.md │ │ │ │ ├── index-bundle.ts │ │ │ │ ├── index-default.ts │ │ │ │ ├── index-turbopack-hack.ts │ │ │ │ ├── index-vite-dev-hack.ts │ │ │ │ ├── package.json │ │ │ │ ├── promise-bundle.ts │ │ │ │ ├── promise-default.ts │ │ │ │ ├── promise-turbopack-hack.ts │ │ │ │ ├── promise-vite-dev-hack.ts │ │ │ │ ├── promise.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vite │ │ │ │ ├── vite.config.js │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── wasm-inline.ts │ │ │ │ └── worker.ts │ │ │ └── wasm-common/ │ │ │ ├── README.md │ │ │ ├── index.ts │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── perf/ │ │ │ ├── package.json │ │ │ ├── perf-better-sqlite3.js │ │ │ └── perf-turso.js │ │ ├── replace.sh │ │ ├── scripts/ │ │ │ └── inline-wasm-base64.js │ │ ├── src/ │ │ │ ├── browser.rs │ │ │ └── lib.rs │ │ ├── sync/ │ │ │ ├── Cargo.toml │ │ │ ├── README.md │ │ │ ├── build.rs │ │ │ ├── packages/ │ │ │ │ ├── common/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── package.json │ │ │ │ │ ├── remote-write-statement.ts │ │ │ │ │ ├── remote-writer.ts │ │ │ │ │ ├── run.ts │ │ │ │ │ ├── tsconfig.json │ │ │ │ │ └── types.ts │ │ │ │ ├── native/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── index.js │ │ │ │ │ ├── package.json │ │ │ │ │ ├── promise.test.ts │ │ │ │ │ ├── promise.ts │ │ │ │ │ ├── remote-write.test.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ └── wasm/ │ │ │ │ ├── .promise.test.ts.swp │ │ │ │ ├── README.md │ │ │ │ ├── cp-entrypoint.sh │ │ │ │ ├── index-bundle.ts │ │ │ │ ├── index-default.ts │ │ │ │ ├── index-turbopack-hack.ts │ │ │ │ ├── index-vite-dev-hack.ts │ │ │ │ ├── package.json │ │ │ │ ├── promise-bundle.ts │ │ │ │ ├── promise-default.ts │ │ │ │ ├── promise-turbopack-hack.ts │ │ │ │ ├── promise-vite-dev-hack.ts │ │ │ │ ├── promise.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vite.config.js │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── wasm-inline.ts │ │ │ │ └── worker.ts │ │ │ └── src/ │ │ │ ├── generator.rs │ │ │ ├── js_protocol_io.rs │ │ │ └── lib.rs │ │ ├── turso-sql-runner.mjs │ │ └── turso-sql-split.mjs │ ├── python/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── SQLALCHEMY_DIALECT.md │ │ ├── build.rs │ │ ├── py-bindings-db-aio.mdx │ │ ├── py-bindings-db.mdx │ │ ├── py-bindings-sync-aio.mdx │ │ ├── py-bindings-sync.mdx │ │ ├── py-bindings-tests-aio.mdx │ │ ├── py-bindings-tests.mdx │ │ ├── pyproject.toml │ │ ├── src/ │ │ │ ├── lib.rs │ │ │ ├── turso.rs │ │ │ └── turso_sync.rs │ │ ├── tests/ │ │ │ ├── __init__.py │ │ │ ├── test_database.py │ │ │ ├── test_database_aio.py │ │ │ ├── test_database_sync.py │ │ │ ├── test_database_sync_aio.py │ │ │ ├── test_sqlalchemy.py │ │ │ └── utils.py │ │ └── turso/ │ │ ├── __init__.py │ │ ├── aio/ │ │ │ ├── __init__.py │ │ │ └── sync/ │ │ │ └── __init__.py │ │ ├── lib.py │ │ ├── lib_aio.py │ │ ├── lib_sync.py │ │ ├── lib_sync_aio.py │ │ ├── py.typed │ │ ├── sqlalchemy/ │ │ │ ├── __init__.py │ │ │ └── dialect.py │ │ ├── sync/ │ │ │ └── __init__.py │ │ └── worker.py │ ├── react-native/ │ │ ├── .gitignore │ │ ├── Makefile │ │ ├── README.md │ │ ├── android/ │ │ │ ├── CMakeLists.txt │ │ │ ├── build.gradle │ │ │ ├── cpp-adapter.cpp │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── turso/ │ │ │ └── sync/ │ │ │ └── reactnative/ │ │ │ ├── TursoBridge.java │ │ │ ├── TursoModule.java │ │ │ └── TursoPackage.java │ │ ├── cpp/ │ │ │ ├── TursoConnectionHostObject.cpp │ │ │ ├── TursoConnectionHostObject.h │ │ │ ├── TursoDatabaseHostObject.cpp │ │ │ ├── TursoDatabaseHostObject.h │ │ │ ├── TursoHostObject.cpp │ │ │ ├── TursoHostObject.h │ │ │ ├── TursoStatementHostObject.cpp │ │ │ ├── TursoStatementHostObject.h │ │ │ ├── TursoSyncChangesHostObject.cpp │ │ │ ├── TursoSyncChangesHostObject.h │ │ │ ├── TursoSyncDatabaseHostObject.cpp │ │ │ ├── TursoSyncDatabaseHostObject.h │ │ │ ├── TursoSyncIoItemHostObject.cpp │ │ │ ├── TursoSyncIoItemHostObject.h │ │ │ ├── TursoSyncOperationHostObject.cpp │ │ │ └── TursoSyncOperationHostObject.h │ │ ├── ios/ │ │ │ ├── TursoModule.h │ │ │ └── TursoModule.mm │ │ ├── package.json │ │ ├── src/ │ │ │ ├── AsyncLock.ts │ │ │ ├── Database.ts │ │ │ ├── Statement.ts │ │ │ ├── index.ts │ │ │ ├── internal/ │ │ │ │ ├── asyncOperation.ts │ │ │ │ └── ioProcessor.ts │ │ │ └── types.ts │ │ ├── templates/ │ │ │ └── turso-sync-sdk-kit.xcframework/ │ │ │ ├── Info.plist │ │ │ ├── ios-arm64/ │ │ │ │ └── turso-sync-sdk-kit.framework/ │ │ │ │ └── Info.plist │ │ │ └── ios-arm64-simulator/ │ │ │ └── turso-sync-sdk-kit.framework/ │ │ │ └── Info.plist │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ └── turso-sync-react-native.podspec │ ├── rust/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── examples/ │ │ │ ├── README.md │ │ │ ├── concurrent_writes.rs │ │ │ ├── example.rs │ │ │ ├── example_struct.rs │ │ │ └── sync_example.rs │ │ ├── rust-driver-sync.mdx │ │ ├── src/ │ │ │ ├── connection.rs │ │ │ ├── lib.rs │ │ │ ├── params.rs │ │ │ ├── rows.rs │ │ │ ├── sync.rs │ │ │ ├── transaction.rs │ │ │ └── value.rs │ │ └── tests/ │ │ ├── integration_tests.rs │ │ └── test_deadlock_join.rs │ └── tcl/ │ ├── Makefile │ ├── test_probes.tcl │ └── turso_tcl.c ├── cli/ │ ├── Cargo.toml │ ├── SQL.sublime-syntax │ ├── app.rs │ ├── build.rs │ ├── commands/ │ │ ├── args.rs │ │ ├── import.rs │ │ └── mod.rs │ ├── config/ │ │ ├── mod.rs │ │ ├── palette.rs │ │ └── terminal.rs │ ├── docs/ │ │ ├── config.md │ │ └── internal/ │ │ └── commands.md │ ├── helper.rs │ ├── input.rs │ ├── main.rs │ ├── manual.rs │ ├── manuals/ │ │ ├── cdc.md │ │ ├── custom-types.md │ │ ├── encryption.md │ │ ├── index.md │ │ ├── materialized-views.md │ │ ├── mcp.md │ │ └── vector.md │ ├── mcp_server.rs │ ├── mvcc_repl.rs │ ├── opcodes_dictionary.rs │ ├── read_state_machine.rs │ ├── sync_server.mdx │ ├── sync_server.rs │ └── tests/ │ ├── non_interactive_exit_code.rs │ └── parameter_bindings.rs ├── core/ │ ├── Cargo.toml │ ├── assert.rs │ ├── benches/ │ │ ├── benchmark.rs │ │ ├── fts_benchmark.rs │ │ ├── graph_queries_benchmark.rs │ │ ├── hash_spill_benchmark.rs │ │ ├── json_benchmark.rs │ │ ├── mvcc_benchmark.rs │ │ ├── sql_functions/ │ │ │ ├── datetime.rs │ │ │ ├── likeop.rs │ │ │ ├── main.rs │ │ │ ├── numeric.rs │ │ │ └── value.rs │ │ ├── tpc_h_benchmark.rs │ │ └── write_perf_benchmark.rs │ ├── btree_dump.rs │ ├── build.rs │ ├── busy.rs │ ├── connection.rs │ ├── dbpage.rs │ ├── error.rs │ ├── ext/ │ │ ├── dynamic.rs │ │ ├── mod.rs │ │ └── vtab_xconnect.rs │ ├── fast_lock.rs │ ├── function.rs │ ├── functions/ │ │ ├── datetime.rs │ │ ├── mod.rs │ │ └── printf.rs │ ├── incremental/ │ │ ├── aggregate_operator.rs │ │ ├── compiler.rs │ │ ├── cursor.rs │ │ ├── dbsp.rs │ │ ├── expr_compiler.rs │ │ ├── filter_operator.rs │ │ ├── input_operator.rs │ │ ├── join_operator.rs │ │ ├── merge_operator.rs │ │ ├── mod.rs │ │ ├── operator.rs │ │ ├── persistence.rs │ │ ├── project_operator.rs │ │ └── view.rs │ ├── index_method/ │ │ ├── backing_btree.rs │ │ ├── fts.rs │ │ ├── mod.rs │ │ └── toy_vector_sparse_ivf.rs │ ├── info.rs │ ├── io/ │ │ ├── clock.rs │ │ ├── common.rs │ │ ├── completions.rs │ │ ├── generic.rs │ │ ├── io_uring.rs │ │ ├── memory.rs │ │ ├── mod.rs │ │ ├── unix.rs │ │ ├── vfs.rs │ │ ├── win_iocp.rs │ │ └── windows.rs │ ├── json/ │ │ ├── cache.rs │ │ ├── error.rs │ │ ├── jsonb.rs │ │ ├── mod.rs │ │ ├── ops.rs │ │ ├── path.rs │ │ └── vtab.rs │ ├── lib.rs │ ├── mvcc/ │ │ ├── clock.rs │ │ ├── cursor.rs │ │ ├── database/ │ │ │ ├── checkpoint_state_machine.rs │ │ │ ├── hermitage_tests.rs │ │ │ ├── mod.rs │ │ │ └── tests.rs │ │ ├── mod.rs │ │ └── persistent_storage/ │ │ ├── logical_log.rs │ │ └── mod.rs │ ├── numeric/ │ │ ├── decimal.rs │ │ ├── mod.rs │ │ └── nonnan.rs │ ├── parameters.rs │ ├── pragma.rs │ ├── pseudo.rs │ ├── regexp.rs │ ├── schema.rs │ ├── series.rs │ ├── state_machine.rs │ ├── statement.rs │ ├── stats.rs │ ├── storage/ │ │ ├── btree.rs │ │ ├── buffer_pool.rs │ │ ├── checksum.rs │ │ ├── database.rs │ │ ├── encryption.rs │ │ ├── journal_mode.rs │ │ ├── mod.rs │ │ ├── page_cache.rs │ │ ├── pager.rs │ │ ├── slot_bitmap.rs │ │ ├── sqlite3_ondisk.rs │ │ ├── state_machines.rs │ │ ├── subjournal.rs │ │ └── wal.rs │ ├── sync.rs │ ├── thread.rs │ ├── time/ │ │ ├── internal.rs │ │ └── mod.rs │ ├── translate/ │ │ ├── aggregation.rs │ │ ├── alter.rs │ │ ├── analyze.rs │ │ ├── attach.rs │ │ ├── collate.rs │ │ ├── compound_select.rs │ │ ├── delete.rs │ │ ├── display.rs │ │ ├── emitter/ │ │ │ ├── delete.rs │ │ │ ├── mod.rs │ │ │ ├── select.rs │ │ │ └── update.rs │ │ ├── expr.rs │ │ ├── expression_index.rs │ │ ├── fkeys.rs │ │ ├── group_by.rs │ │ ├── index.rs │ │ ├── insert.rs │ │ ├── integrity_check.rs │ │ ├── logical.rs │ │ ├── main_loop/ │ │ │ ├── body.rs │ │ │ ├── close.rs │ │ │ ├── conditions.rs │ │ │ ├── hash.rs │ │ │ ├── in_seek.rs │ │ │ ├── init.rs │ │ │ ├── mod.rs │ │ │ ├── multi_index.rs │ │ │ ├── open.rs │ │ │ └── seek.rs │ │ ├── mod.rs │ │ ├── optimizer/ │ │ │ ├── OPTIMIZER.md │ │ │ ├── access_method.rs │ │ │ ├── constraints.rs │ │ │ ├── cost.rs │ │ │ ├── cost_params.rs │ │ │ ├── join.rs │ │ │ ├── lift_common_subexpressions.rs │ │ │ ├── mod.rs │ │ │ ├── multi_index.rs │ │ │ ├── order.rs │ │ │ └── unnest.rs │ │ ├── order_by.rs │ │ ├── plan.rs │ │ ├── planner.rs │ │ ├── pragma.rs │ │ ├── result_row.rs │ │ ├── rollback.rs │ │ ├── schema.rs │ │ ├── select.rs │ │ ├── stmt_journal.rs │ │ ├── subquery.rs │ │ ├── transaction.rs │ │ ├── trigger.rs │ │ ├── trigger_exec.rs │ │ ├── update.rs │ │ ├── upsert.rs │ │ ├── vacuum.rs │ │ ├── values.rs │ │ ├── view.rs │ │ └── window.rs │ ├── turso_types_vtab.rs │ ├── types.rs │ ├── util.rs │ ├── uuid.rs │ ├── vdbe/ │ │ ├── affinity.rs │ │ ├── array.rs │ │ ├── bloom_filter.rs │ │ ├── builder.rs │ │ ├── execute.rs │ │ ├── explain.rs │ │ ├── hash_table.rs │ │ ├── insn.rs │ │ ├── metrics.rs │ │ ├── mod.rs │ │ ├── rowset.rs │ │ ├── sorter.rs │ │ └── value.rs │ ├── vector/ │ │ ├── mod.rs │ │ ├── operations/ │ │ │ ├── concat.rs │ │ │ ├── convert.rs │ │ │ ├── distance_cos.rs │ │ │ ├── distance_dot.rs │ │ │ ├── distance_l2.rs │ │ │ ├── jaccard.rs │ │ │ ├── mod.rs │ │ │ ├── serialize.rs │ │ │ ├── slice.rs │ │ │ └── text.rs │ │ └── vector_types.rs │ └── vtab.rs ├── deny.toml ├── dist-workspace.toml ├── docs/ │ ├── CODEOWNERS │ ├── agent-guides/ │ │ ├── async-io-model.md │ │ ├── code-quality.md │ │ ├── debugging.md │ │ ├── mvcc.md │ │ ├── pr-workflow.md │ │ ├── storage-format.md │ │ ├── testing.md │ │ └── transaction-correctness.md │ ├── contributing/ │ │ └── contributing_functions.md │ ├── fts.md │ ├── internals/ │ │ └── mvcc/ │ │ ├── DESIGN.md │ │ ├── GC.md │ │ ├── RECOVERY_SEMANTICS.md │ │ └── figures/ │ │ └── transactions.excalidraw │ ├── javascript-api-reference.md │ ├── language-reference/ │ │ └── book/ │ │ ├── print.html │ │ └── turso/ │ │ └── custom-types.html │ ├── manual.md │ ├── sql-reference/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── book.toml │ │ ├── cli/ │ │ │ ├── command-line-options.mdx │ │ │ ├── getting-started.mdx │ │ │ └── shell-commands.mdx │ │ ├── compatibility.mdx │ │ ├── data-types.mdx │ │ ├── experimental-features.mdx │ │ ├── expressions.mdx │ │ ├── extensions.mdx │ │ ├── functions/ │ │ │ ├── aggregate.mdx │ │ │ ├── array.mdx │ │ │ ├── date-time.mdx │ │ │ ├── fts.mdx │ │ │ ├── json.mdx │ │ │ ├── math.mdx │ │ │ ├── scalar.mdx │ │ │ ├── vector.mdx │ │ │ └── window.mdx │ │ ├── pragmas.mdx │ │ ├── preview.sh │ │ └── statements/ │ │ ├── alter-table.mdx │ │ ├── analyze.mdx │ │ ├── attach-database.mdx │ │ ├── create-index.mdx │ │ ├── create-materialized-view.mdx │ │ ├── create-table.mdx │ │ ├── create-trigger.mdx │ │ ├── create-type.mdx │ │ ├── create-view.mdx │ │ ├── create-virtual-table.mdx │ │ ├── delete.mdx │ │ ├── detach-database.mdx │ │ ├── drop-index.mdx │ │ ├── drop-table.mdx │ │ ├── drop-trigger.mdx │ │ ├── drop-type.mdx │ │ ├── drop-view.mdx │ │ ├── explain.mdx │ │ ├── insert.mdx │ │ ├── replace.mdx │ │ ├── select.mdx │ │ ├── transactions.mdx │ │ ├── update.mdx │ │ └── upsert.mdx │ └── testing.md ├── examples/ │ ├── .gitignore │ ├── README.md │ ├── dotnet/ │ │ ├── Encryption.cs │ │ ├── EncryptionExample.csproj │ │ └── README.md │ ├── go/ │ │ ├── README.md │ │ ├── encryption.go │ │ ├── go.mod │ │ └── go.sum │ ├── java/ │ │ └── README.md │ ├── javascript/ │ │ ├── concurrent-writes/ │ │ │ ├── index.mjs │ │ │ └── package.json │ │ ├── database-node/ │ │ │ ├── README.md │ │ │ ├── index.mjs │ │ │ └── package.json │ │ ├── database-wasm-vite/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── server.mjs │ │ │ ├── vercel.json │ │ │ └── vite.config.ts │ │ ├── encryption/ │ │ │ ├── encryption.mjs │ │ │ └── package.json │ │ ├── sync-encryption/ │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ └── sync_example.mjs │ │ ├── sync-node/ │ │ │ ├── README.md │ │ │ ├── index.mjs │ │ │ └── package.json │ │ └── sync-wasm-vite/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── server.mjs │ │ ├── vercel.json │ │ └── vite.config.ts │ ├── python/ │ │ ├── README.md │ │ ├── basic.py │ │ ├── concurrent_writes.py │ │ ├── encryption.py │ │ └── sync_example.py │ └── react-native/ │ ├── .bundle/ │ │ └── config │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc.js │ ├── .watchmanconfig │ ├── Gemfile │ ├── README.md │ ├── android/ │ │ ├── app/ │ │ │ ├── build.gradle │ │ │ ├── debug.keystore │ │ │ ├── proguard-rules.pro │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── tursoexample/ │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainApplication.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── rn_edit_text_material.xml │ │ │ └── values/ │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ └── settings.gradle │ ├── app.json │ ├── babel.config.js │ ├── index.js │ ├── ios/ │ │ ├── .xcode.env │ │ ├── Podfile │ │ ├── TursoExample/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Images.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Info.plist │ │ │ ├── LaunchScreen.storyboard │ │ │ └── PrivacyInfo.xcprivacy │ │ ├── TursoExample.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── TursoExample.xcscheme │ │ └── TursoExample.xcworkspace/ │ │ └── contents.xcworkspacedata │ ├── jest.config.js │ ├── metro.config.js │ ├── package.json │ ├── react-native.config.js │ ├── src/ │ │ └── App.tsx │ └── tsconfig.json ├── extensions/ │ ├── completion/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ ├── keywords.rs │ │ └── lib.rs │ ├── core/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── build.rs │ │ └── src/ │ │ ├── functions.rs │ │ ├── lib.rs │ │ ├── types.rs │ │ ├── vfs_modules.rs │ │ └── vtabs.rs │ ├── crypto/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ ├── crypto.rs │ │ └── lib.rs │ ├── csv/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ └── lib.rs │ ├── fuzzy/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ ├── caver.rs │ │ ├── common.rs │ │ ├── editdist.rs │ │ ├── lib.rs │ │ ├── phonetic.rs │ │ ├── rsoundex.rs │ │ ├── soundex.rs │ │ └── translit.rs │ ├── ipaddr/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ └── lib.rs │ ├── percentile/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ └── lib.rs │ ├── regexp/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ └── lib.rs │ └── tests/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── flake.nix ├── fuzz/ │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── fuzz_targets/ │ ├── cast_real.rs │ ├── expression.rs │ ├── scalar_func.rs │ └── schema.rs ├── licenses/ │ ├── bindings/ │ │ ├── java/ │ │ │ ├── assertj-license.md │ │ │ ├── errorprone-license.md │ │ │ ├── logback-license.md │ │ │ └── spotless-license.md │ │ └── python/ │ │ └── sqlalchemy-mit-license.md │ ├── core/ │ │ ├── libm-mit-license.md │ │ ├── pastey-apache-license.md │ │ ├── pastey-mit-license.md │ │ ├── serde-apache-license.md │ │ ├── serde-mit-license.md │ │ ├── serde_json5-license.md │ │ ├── windows-apache.license.md │ │ └── windows-mit-license.md │ └── extensions/ │ ├── ipnetwork-apache-license.md │ └── ipnetwork-mit-license.md ├── macros/ │ ├── Cargo.toml │ └── src/ │ ├── assert.rs │ ├── atomic_enum.rs │ ├── ext/ │ │ ├── agg_derive.rs │ │ ├── match_ignore_ascii_case.rs │ │ ├── mod.rs │ │ ├── scalars.rs │ │ ├── vfs_derive.rs │ │ └── vtab_derive.rs │ ├── lib.rs │ └── test.rs ├── packages/ │ └── turso-serverless/ │ ├── AGENT.md │ ├── README.md │ ├── examples/ │ │ ├── cloud-encryption/ │ │ │ ├── README.md │ │ │ ├── index.mjs │ │ │ └── package.json │ │ ├── remote/ │ │ │ ├── README.md │ │ │ ├── index.mjs │ │ │ └── package.json │ │ └── remote-compat/ │ │ ├── README.md │ │ ├── index.mjs │ │ └── package.json │ ├── integration-tests/ │ │ ├── compat.test.mjs │ │ └── serverless.test.mjs │ ├── package.json │ ├── src/ │ │ ├── async-lock.ts │ │ ├── compat/ │ │ │ └── index.ts │ │ ├── compat.ts │ │ ├── connection.ts │ │ ├── error.ts │ │ ├── index.ts │ │ ├── protocol.ts │ │ ├── session.ts │ │ └── statement.ts │ └── tsconfig.json ├── parser/ │ ├── Cargo.toml │ ├── README.md │ ├── benches/ │ │ └── parser_benchmark.rs │ └── src/ │ ├── ast/ │ │ ├── check.rs │ │ └── fmt.rs │ ├── ast.rs │ ├── error.rs │ ├── lexer.rs │ ├── lib.rs │ ├── parser.rs │ └── token.rs ├── perf/ │ ├── clickbench/ │ │ ├── .gitignore │ │ ├── benchmark.sh │ │ ├── create.sql │ │ ├── queries.sql │ │ └── run.sh │ ├── connection/ │ │ ├── README.md │ │ ├── gen-database.py │ │ ├── gen-databases │ │ ├── limbo/ │ │ │ ├── Cargo.toml │ │ │ ├── run-benchmark.sh │ │ │ └── src/ │ │ │ └── main.rs │ │ ├── plot.py │ │ └── rusqlite/ │ │ ├── Cargo.toml │ │ ├── run-benchmark.sh │ │ └── src/ │ │ └── main.rs │ ├── encryption/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── main.rs │ ├── graph-queries/ │ │ ├── .gitignore │ │ ├── generate_seed.py │ │ └── queries/ │ │ ├── 3_aggregate_or_in.sql │ │ ├── a_cooccurrence.sql │ │ ├── b_or_join.sql │ │ ├── c_edge_counts.sql │ │ ├── d_inlist_union.sql │ │ ├── e_activity_agg.sql │ │ ├── f1_streak_current.sql │ │ └── f2_streak_longest.sql │ ├── latency/ │ │ ├── README.md │ │ ├── limbo/ │ │ │ ├── .gitignore │ │ │ ├── Cargo.toml │ │ │ ├── gen-database.py │ │ │ ├── gen-databases │ │ │ ├── plot.py │ │ │ ├── run-benchmark.sh │ │ │ ├── rust-toolchain │ │ │ └── src/ │ │ │ └── main.rs │ │ └── rusqlite/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── gen-database.py │ │ ├── gen-databases │ │ ├── plot.py │ │ ├── run-benchmark.sh │ │ └── src/ │ │ └── main.rs │ ├── mobibench/ │ │ ├── README.md │ │ ├── plot/ │ │ │ ├── plot.py │ │ │ └── pyproject.toml │ │ └── run-eval.sh │ ├── throughput/ │ │ ├── README.md │ │ ├── plot/ │ │ │ ├── plot-compute-impact.py │ │ │ └── plot-thread-scaling.py │ │ ├── rusqlite/ │ │ │ ├── Cargo.toml │ │ │ ├── scripts/ │ │ │ │ └── bench.sh │ │ │ └── src/ │ │ │ └── main.rs │ │ └── turso/ │ │ ├── Cargo.toml │ │ ├── bench.sh │ │ ├── scripts/ │ │ │ └── bench.sh │ │ └── src/ │ │ └── main.rs │ └── tpc-h/ │ ├── README.md │ ├── benchmark.sh │ ├── compare.sh │ ├── plot/ │ │ ├── .python-version │ │ ├── plot.py │ │ ├── pyproject.toml │ │ └── results2csv.sh │ ├── queries/ │ │ ├── 1.sql │ │ ├── 10.sql │ │ ├── 11.sql │ │ ├── 12.sql │ │ ├── 13.sql │ │ ├── 14.sql │ │ ├── 15.sql │ │ ├── 16.sql │ │ ├── 17.sql │ │ ├── 18.sql │ │ ├── 19.sql │ │ ├── 2.sql │ │ ├── 20.sql │ │ ├── 21.sql │ │ ├── 22.sql │ │ ├── 3.sql │ │ ├── 4.sql │ │ ├── 5.sql │ │ ├── 6.sql │ │ ├── 7.sql │ │ ├── 8.sql │ │ └── 9.sql │ └── run.sh ├── pyproject.toml ├── rust-toolchain.toml ├── scripts/ │ ├── antithesis/ │ │ ├── launch.sh │ │ ├── publish-config.sh │ │ ├── publish-docker.sh │ │ └── publish-workload.sh │ ├── clean_interactions.sh │ ├── clone_test_db.sh │ ├── corruption-debug-tools/ │ │ ├── README.md │ │ ├── find_corrupt_frame.py │ │ ├── lib/ │ │ │ ├── __init__.py │ │ │ ├── diff.py │ │ │ ├── page.py │ │ │ ├── record.py │ │ │ └── wal.py │ │ ├── page_diff.py │ │ ├── page_history.py │ │ ├── page_info.py │ │ ├── track_rowid.py │ │ ├── verify_stale.py │ │ ├── wal_commits.py │ │ └── wal_info.py │ ├── corruption_bisecter.py │ ├── diff.sh │ ├── gen-changelog.py │ ├── install-sqlite3.sh │ ├── limbo-sqlite3 │ ├── merge-pr.py │ ├── publish-crates.sh │ ├── pyproject.toml │ ├── release-status.py │ ├── run-sim │ ├── run-sim.ps1 │ ├── run-sqlancer.sh │ ├── run-until-fail.sh │ ├── turso-mvcc-sqlite3 │ └── update-version.py ├── sdk-kit/ │ ├── Cargo.toml │ ├── README.md │ ├── bindgen.sh │ ├── readme-sdk-kit.mdx │ ├── src/ │ │ ├── bindings.rs │ │ ├── capi.rs │ │ ├── lib.rs │ │ └── rsapi.rs │ └── turso.h ├── sdk-kit-macros/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── sql_generation/ │ ├── Cargo.toml │ ├── generation/ │ │ ├── expr.rs │ │ ├── mod.rs │ │ ├── opts.rs │ │ ├── predicate/ │ │ │ ├── binary.rs │ │ │ ├── mod.rs │ │ │ └── unary.rs │ │ ├── query.rs │ │ ├── table.rs │ │ └── value/ │ │ ├── cmp.rs │ │ ├── mod.rs │ │ └── pattern.rs │ ├── lib.rs │ └── model/ │ ├── mod.rs │ ├── query/ │ │ ├── alter_table.rs │ │ ├── create.rs │ │ ├── create_index.rs │ │ ├── delete.rs │ │ ├── drop.rs │ │ ├── drop_index.rs │ │ ├── insert.rs │ │ ├── mod.rs │ │ ├── pragma.rs │ │ ├── predicate.rs │ │ ├── select.rs │ │ ├── transaction.rs │ │ └── update.rs │ └── table.rs ├── sqlite3/ │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── cbindgen.toml │ ├── examples/ │ │ └── example.c │ ├── include/ │ │ └── sqlite3.h │ ├── src/ │ │ └── lib.rs │ └── tests/ │ ├── .gitignore │ ├── Makefile │ ├── compat/ │ │ └── mod.rs │ └── sqlite3_tests.c ├── sync/ │ ├── engine/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── database_replay_generator.rs │ │ ├── database_sync_engine.rs │ │ ├── database_sync_engine_io.rs │ │ ├── database_sync_lazy_storage.rs │ │ ├── database_sync_operations.rs │ │ ├── database_tape.rs │ │ ├── errors.rs │ │ ├── io_operations.rs │ │ ├── lib.rs │ │ ├── server_proto.rs │ │ ├── sparse_io.rs │ │ ├── types.rs │ │ └── wal_session.rs │ └── sdk-kit/ │ ├── Cargo.toml │ ├── bindgen.sh │ ├── src/ │ │ ├── bindings.rs │ │ ├── capi.rs │ │ ├── lib.rs │ │ ├── rsapi.rs │ │ ├── sync_engine_io.rs │ │ └── turso_async_operation.rs │ └── turso_sync.h ├── testing/ │ ├── README.md │ ├── antithesis/ │ │ ├── README.md │ │ ├── bank-test/ │ │ │ ├── anytime_validate.py │ │ │ ├── eventually_validate.py │ │ │ ├── finally_validate.py │ │ │ ├── first_setup.py │ │ │ └── parallel_driver_generate_transaction.py │ │ ├── pyproject.toml │ │ ├── stress/ │ │ │ └── singleton_driver_stress.sh │ │ ├── stress-composer/ │ │ │ ├── first_setup.py │ │ │ ├── helper_utils.py │ │ │ ├── parallel_driver_alter_table.py │ │ │ ├── parallel_driver_create_index.py │ │ │ ├── parallel_driver_create_table.py │ │ │ ├── parallel_driver_delete.py │ │ │ ├── parallel_driver_drop_index.py │ │ │ ├── parallel_driver_drop_table.py │ │ │ ├── parallel_driver_insert.py │ │ │ ├── parallel_driver_integritycheck.py │ │ │ ├── parallel_driver_rollback.py │ │ │ ├── parallel_driver_schema_rollback.py │ │ │ ├── parallel_driver_update.py │ │ │ ├── parallel_driver_wal_checkpoint.py │ │ │ └── shuffle-run.sh │ │ ├── stress-io_uring/ │ │ │ └── singleton_driver_stress.sh │ │ ├── stress-io_uring-mvcc/ │ │ │ └── singleton_driver_stress.sh │ │ ├── stress-mvcc/ │ │ │ └── singleton_driver_stress.sh │ │ └── stress-unreliable/ │ │ └── singleton_driver_stress.sh │ ├── cli_tests/ │ │ ├── cli_test_cases.py │ │ ├── collate.py │ │ ├── console.py │ │ ├── constraint.py │ │ ├── extensions.py │ │ ├── memory.py │ │ ├── mvcc.py │ │ ├── sqlite_bench.py │ │ ├── test_files/ │ │ │ ├── test.csv │ │ │ └── test_w_header.csv │ │ ├── test_turso_cli.py │ │ ├── update.py │ │ ├── vfs_bench.py │ │ └── write.py │ ├── concurrent-simulator/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── bin/ │ │ │ ├── explore │ │ │ ├── run │ │ │ └── run-elle │ │ ├── chaotic_elle.rs │ │ ├── elle.rs │ │ ├── io.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── operations.rs │ │ ├── properties.rs │ │ ├── regression_tests.rs │ │ └── workloads.rs │ ├── differential-oracle/ │ │ ├── fuzzer/ │ │ │ ├── Cargo.toml │ │ │ ├── custom_types_fuzzer.rs │ │ │ ├── docker-runner/ │ │ │ │ ├── Dockerfile │ │ │ │ ├── docker-entrypoint.fuzzer.ts │ │ │ │ ├── github.ts │ │ │ │ ├── levenshtein.ts │ │ │ │ ├── logParse.ts │ │ │ │ ├── package.json │ │ │ │ ├── random.ts │ │ │ │ ├── slack.ts │ │ │ │ └── tsconfig.json │ │ │ ├── generate.rs │ │ │ ├── lib.rs │ │ │ ├── main.rs │ │ │ ├── memory/ │ │ │ │ ├── file.rs │ │ │ │ ├── io.rs │ │ │ │ └── mod.rs │ │ │ ├── oracle.rs │ │ │ ├── printf_fuzzer.rs │ │ │ ├── printf_gen.rs │ │ │ ├── runner.rs │ │ │ └── schema.rs │ │ ├── sql_gen/ │ │ │ ├── Cargo.toml │ │ │ └── src/ │ │ │ ├── ast.rs │ │ │ ├── builder.rs │ │ │ ├── capabilities.rs │ │ │ ├── context.rs │ │ │ ├── error.rs │ │ │ ├── functions.rs │ │ │ ├── generate/ │ │ │ │ ├── expr.rs │ │ │ │ ├── literal.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── select.rs │ │ │ │ └── stmt.rs │ │ │ ├── lib.rs │ │ │ ├── policy.rs │ │ │ ├── schema.rs │ │ │ ├── strategy.rs │ │ │ └── trace.rs │ │ ├── sql_gen_macros/ │ │ │ ├── Cargo.toml │ │ │ └── src/ │ │ │ └── lib.rs │ │ └── sql_gen_prop/ │ │ ├── Cargo.toml │ │ ├── alter_table.rs │ │ ├── create_index.rs │ │ ├── create_table.rs │ │ ├── create_trigger.rs │ │ ├── cte.rs │ │ ├── delete.rs │ │ ├── drop_index.rs │ │ ├── drop_table.rs │ │ ├── drop_trigger.rs │ │ ├── expression.rs │ │ ├── function.rs │ │ ├── generator.rs │ │ ├── insert.rs │ │ ├── lib.rs │ │ ├── profile.rs │ │ ├── result.rs │ │ ├── schema.rs │ │ ├── select.rs │ │ ├── statement.rs │ │ ├── transaction.rs │ │ ├── update.rs │ │ ├── utility.rs │ │ ├── value.rs │ │ └── view.rs │ ├── javascript/ │ │ ├── __test__/ │ │ │ ├── async.test.js │ │ │ └── sync.test.js │ │ ├── artifacts/ │ │ │ └── basic-test.sql │ │ └── package.json │ ├── pyproject.toml │ ├── simulator/ │ │ ├── .gitignore │ │ ├── COVERAGE.md │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── ROADMAP.md │ │ ├── common/ │ │ │ └── mod.rs │ │ ├── generation/ │ │ │ ├── mod.rs │ │ │ ├── plan.rs │ │ │ ├── property.rs │ │ │ └── query.rs │ │ ├── main.rs │ │ ├── model/ │ │ │ ├── interactions.rs │ │ │ ├── metrics.rs │ │ │ ├── mod.rs │ │ │ └── property.rs │ │ ├── plan_to_test.rs │ │ ├── profiles/ │ │ │ ├── io.rs │ │ │ ├── mod.rs │ │ │ └── query.rs │ │ ├── run-miri.sh │ │ ├── runner/ │ │ │ ├── bugbase.rs │ │ │ ├── cli.rs │ │ │ ├── clock.rs │ │ │ ├── differential.rs │ │ │ ├── doublecheck.rs │ │ │ ├── env.rs │ │ │ ├── execution.rs │ │ │ ├── file.rs │ │ │ ├── io.rs │ │ │ ├── memory/ │ │ │ │ ├── file.rs │ │ │ │ ├── io.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── mvcc_recovery.rs │ │ │ │ └── statement_abandon.rs │ │ │ └── mod.rs │ │ ├── shrink/ │ │ │ ├── mod.rs │ │ │ └── plan.rs │ │ └── simulator-docker-runner/ │ │ ├── Dockerfile.simulator │ │ ├── README.MD │ │ ├── docker-entrypoint.simulator.ts │ │ ├── github.ts │ │ ├── levenshtein.test.ts │ │ ├── levenshtein.ts │ │ ├── logParse.ts │ │ ├── package.json │ │ ├── random.ts │ │ ├── slack.ts │ │ └── tsconfig.json │ ├── sqlancer/ │ │ ├── README.md │ │ ├── patches/ │ │ │ ├── LimboProvider.java │ │ │ ├── LimboSchema.java │ │ │ └── SQLite3Schema.patch │ │ └── sqlancer-runner/ │ │ ├── Dockerfile.sqlancer │ │ ├── README.md │ │ ├── corruptionAnalysis.ts │ │ ├── docker-entrypoint.sqlancer.ts │ │ ├── github.ts │ │ ├── levenshtein.ts │ │ ├── logParse.ts │ │ ├── package.json │ │ ├── slack.ts │ │ └── tsconfig.json │ ├── sqlite3/ │ │ ├── README.md │ │ ├── all.test │ │ ├── alter.test │ │ ├── alter2.test │ │ ├── alter3.test │ │ ├── alter4.test │ │ ├── func.test │ │ ├── func2.test │ │ ├── func3.test │ │ ├── func4.test │ │ ├── func5.test │ │ ├── func6.test │ │ ├── func7.test │ │ ├── func8.test │ │ ├── func9.test │ │ ├── insert.test │ │ ├── insert2.test │ │ ├── insert3.test │ │ ├── insert4.test │ │ ├── insert5.test │ │ ├── join.test │ │ ├── join2.test │ │ ├── join3.test │ │ ├── join4.test │ │ ├── join5.test │ │ ├── join6.test │ │ ├── join7.test │ │ ├── join8.test │ │ ├── join9.test │ │ ├── joinA.test │ │ ├── joinB.test │ │ ├── joinC.test │ │ ├── joinD.test │ │ ├── joinE.test │ │ ├── joinF.test │ │ ├── joinH.test │ │ ├── select1.test │ │ ├── select2.test │ │ ├── select3.test │ │ ├── select4.test │ │ ├── select5.test │ │ ├── select6.test │ │ ├── select7.test │ │ ├── select8.test │ │ ├── select9.test │ │ ├── selectA.test │ │ ├── selectB.test │ │ ├── selectC.test │ │ ├── selectD.test │ │ ├── selectE.test │ │ ├── selectF.test │ │ ├── selectG.test │ │ ├── selectH.test │ │ └── tester.tcl │ ├── sqlite_test_ext/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── include/ │ │ │ ├── sqlite3.h │ │ │ └── sqlite3ext.h │ │ └── src/ │ │ ├── kvstore.c │ │ └── lib.rs │ ├── sqlright/ │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── collect_coverage.sh │ │ ├── crash_reports/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── collect_crashes.py │ │ │ ├── lib/ │ │ │ │ ├── __init__.py │ │ │ │ ├── database.py │ │ │ │ ├── executor.py │ │ │ │ ├── parser.py │ │ │ │ └── scanner.py │ │ │ ├── query_crashes.py │ │ │ └── schema.sql │ │ ├── patches/ │ │ │ ├── 0001-turso-increase-map-size-and-fix-gcc13.patch │ │ │ ├── 0002-turso-fix-macos-bison-compatibility.patch │ │ │ └── 0003-turso-fix-macos-compilation-errors.patch │ │ ├── run.sh │ │ └── setup.sh │ ├── sqltests/ │ │ ├── Cargo.toml │ │ ├── Makefile │ │ ├── database/ │ │ │ └── .gitignore │ │ ├── docs/ │ │ │ ├── README.md │ │ │ ├── adding-backends.md │ │ │ ├── architecture.md │ │ │ ├── backends/ │ │ │ │ └── cli.md │ │ │ ├── cli-usage.md │ │ │ ├── dsl-spec.md │ │ │ ├── parallelism.md │ │ │ └── snapshot-testing.md │ │ ├── examples/ │ │ │ ├── basic.sqltest │ │ │ ├── joins.sqltest │ │ │ ├── snapshot_example.sqltest │ │ │ └── snapshots/ │ │ │ ├── snapshot_example__query-plan-by-id.snap │ │ │ └── snapshot_example__query-plan-by-name.snap │ │ ├── src/ │ │ │ ├── backends/ │ │ │ │ ├── cli.rs │ │ │ │ ├── js.rs │ │ │ │ ├── mod.rs │ │ │ │ └── rust.rs │ │ │ ├── comparison/ │ │ │ │ ├── exact.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── pattern.rs │ │ │ │ └── unordered.rs │ │ │ ├── generator/ │ │ │ │ └── mod.rs │ │ │ ├── lib.rs │ │ │ ├── main.rs │ │ │ ├── output/ │ │ │ │ ├── json.rs │ │ │ │ ├── mod.rs │ │ │ │ └── pretty.rs │ │ │ ├── parser/ │ │ │ │ ├── ast.rs │ │ │ │ ├── lexer.rs │ │ │ │ ├── mod.rs │ │ │ │ └── sql_complete.rs │ │ │ ├── runner/ │ │ │ │ └── mod.rs │ │ │ ├── snapshot/ │ │ │ │ └── mod.rs │ │ │ └── tcl_converter/ │ │ │ ├── mod.rs │ │ │ ├── parser.rs │ │ │ └── utils.rs │ │ ├── syntax-highlighter/ │ │ │ └── vscode/ │ │ │ ├── README.md │ │ │ ├── language-configuration.json │ │ │ ├── package.json │ │ │ └── syntaxes/ │ │ │ └── sqltest.tmLanguage.json │ │ ├── tests/ │ │ │ ├── affinity.sqltest │ │ │ ├── agg-functions/ │ │ │ │ ├── agg-extreme-exponent.sqltest │ │ │ │ ├── default.sqltest │ │ │ │ ├── group-concat-types.sqltest │ │ │ │ ├── is-true.sqltest │ │ │ │ ├── memory.sqltest │ │ │ │ ├── sum-blob-types.sqltest │ │ │ │ ├── sum-inf-cancel.sqltest │ │ │ │ ├── sum-large-float.sqltest │ │ │ │ └── sum-text-types.sqltest │ │ │ ├── alter_rename_column_partial_idx.sqltest │ │ │ ├── alter_table.sqltest │ │ │ ├── autoincr.sqltest │ │ │ ├── before-update-trigger-correlated-subquery.sqltest │ │ │ ├── big.ignored_for_now │ │ │ ├── boolean.sqltest │ │ │ ├── btree-backward-scan.sqltest │ │ │ ├── btree-large-page-overflow.sqltest │ │ │ ├── btree_dump.sqltest │ │ │ ├── changes.sqltest │ │ │ ├── char.sqltest │ │ │ ├── check_constraint.sqltest │ │ │ ├── cmdlineshell.sqltest │ │ │ ├── coalesce/ │ │ │ │ ├── default.sqltest │ │ │ │ └── memory.sqltest │ │ │ ├── collate.sqltest │ │ │ ├── column_name_case.sqltest │ │ │ ├── compare.sqltest │ │ │ ├── composite-index-sort-elim.sqltest │ │ │ ├── concat.sqltest │ │ │ ├── correlated-subquery-hash-join.sqltest │ │ │ ├── correlated-subquery-in-clause.sqltest │ │ │ ├── correlated-subquery-window.sqltest │ │ │ ├── create_index.sqltest │ │ │ ├── create_table.sqltest │ │ │ ├── cross_join.sqltest │ │ │ ├── cte-real-affinity-join.sqltest │ │ │ ├── cte-union-all-aggregate-literals.sqltest │ │ │ ├── cte.sqltest │ │ │ ├── cte_cardinality.sqltest │ │ │ ├── cte_expressions.sqltest │ │ │ ├── default_value.sqltest │ │ │ ├── delete-correlated-subquery-rowid.sqltest │ │ │ ├── delete-correlated-subquery.sqltest │ │ │ ├── delete-limit-offset.sqltest │ │ │ ├── delete.sqltest │ │ │ ├── distinct.sqltest │ │ │ ├── drop_index.sqltest │ │ │ ├── drop_table.sqltest │ │ │ ├── duplicate-trigger-names.sqltest │ │ │ ├── expr-index-correlated-subquery.sqltest │ │ │ ├── foreign_keys.sqltest │ │ │ ├── generate_series.sqltest │ │ │ ├── glob/ │ │ │ │ ├── default.sqltest │ │ │ │ └── memory.sqltest │ │ │ ├── graph_traversal_text_pk.sqltest │ │ │ ├── group-by-expression-index.sqltest │ │ │ ├── groupby/ │ │ │ │ ├── constant-expr.sqltest │ │ │ │ ├── default.sqltest │ │ │ │ ├── duplicate-order-by.sqltest │ │ │ │ └── memory.sqltest │ │ │ ├── hex-real-compare.sqltest │ │ │ ├── in-index-seek.sqltest │ │ │ ├── in-null-or.sqltest │ │ │ ├── in-subquery-ungrouped-aggregate.sqltest │ │ │ ├── indexed_by.sqltest │ │ │ ├── insert-cte-compound.sqltest │ │ │ ├── insert.sqltest │ │ │ ├── insert_autorowid_index.sqltest │ │ │ ├── insert_not_null_default_index.sqltest │ │ │ ├── insert_or_ignore_autoincrement.sqltest │ │ │ ├── int64-overflow-seek.sqltest │ │ │ ├── integrity_check/ │ │ │ │ ├── expression_index.sqltest │ │ │ │ ├── memory.sqltest │ │ │ │ ├── parity_check_constraint.sqltest │ │ │ │ ├── parity_corrupt_expression_index.sqltest │ │ │ │ ├── parity_corrupt_index.sqltest │ │ │ │ ├── parity_corrupt_partial_index.sqltest │ │ │ │ ├── parity_freelist_count_mismatch.sqltest │ │ │ │ ├── parity_freelist_trunk_corrupt.sqltest │ │ │ │ ├── parity_missing_unique_index.sqltest │ │ │ │ ├── parity_non_unique_index.sqltest │ │ │ │ ├── parity_not_null_violation.sqltest │ │ │ │ ├── parity_overflow_list_length_mismatch.sqltest │ │ │ │ ├── parity_quick_check_constraint.sqltest │ │ │ │ ├── snapshot_plans.sqltest │ │ │ │ └── snapshots/ │ │ │ │ ├── snapshot_plans__integrity-check-multi-table-vdbe.snap │ │ │ │ ├── snapshot_plans__integrity-check-vdbe.snap │ │ │ │ ├── snapshot_plans__quick-check-multi-table-vdbe.snap │ │ │ │ └── snapshot_plans__quick-check-vdbe.snap │ │ │ ├── issue_5116.sqltest │ │ │ ├── issue_5212.sqltest │ │ │ ├── join/ │ │ │ │ ├── default.sqltest │ │ │ │ ├── hash.sqltest │ │ │ │ ├── memory.sqltest │ │ │ │ ├── natural_join_no_common.sqltest │ │ │ │ └── outer_hash_join.sqltest │ │ │ ├── json/ │ │ │ │ ├── default.sqltest │ │ │ │ ├── json_subtype_strip.sqltest │ │ │ │ ├── json_tree.sqltest │ │ │ │ └── memory.sqltest │ │ │ ├── last_insert_rowid.sqltest │ │ │ ├── left-join-case-iif-null-masking.sqltest │ │ │ ├── left-join-ifnull-optimization.sqltest │ │ │ ├── like.sqltest │ │ │ ├── limit.sqltest │ │ │ ├── literal.sqltest │ │ │ ├── math/ │ │ │ │ ├── default.sqltest │ │ │ │ ├── degrees-radians-precision.sqltest │ │ │ │ └── memory.sqltest │ │ │ ├── matview-create-index.sqltest │ │ │ ├── multi_index_dml.sqltest │ │ │ ├── multi_index_intersection.sqltest │ │ │ ├── multi_index_or_adversarial.sqltest │ │ │ ├── multi_index_or_adversarial_extra.sqltest │ │ │ ├── multi_index_or_compound.sqltest │ │ │ ├── multi_index_or_join.sqltest │ │ │ ├── mvcc-update-noop.sqltest │ │ │ ├── negative_zero.sqltest │ │ │ ├── not_between.sqltest │ │ │ ├── null/ │ │ │ │ ├── default.sqltest │ │ │ │ └── memory.sqltest │ │ │ ├── offset/ │ │ │ │ ├── default.sqltest │ │ │ │ └── memory.sqltest │ │ │ ├── on_conflict.sqltest │ │ │ ├── on_conflict_constraint_def.sqltest │ │ │ ├── orderby/ │ │ │ │ ├── default.sqltest │ │ │ │ ├── memory.sqltest │ │ │ │ ├── orderby_plan.sqltest │ │ │ │ └── snapshots/ │ │ │ │ ├── orderby_plan__orderby_a_desc_uses_sorter.snap │ │ │ │ ├── orderby_plan__orderby_a_uses_sorter.snap │ │ │ │ ├── orderby_plan__orderby_rowid_desc_no_sorter.snap │ │ │ │ └── orderby_plan__orderby_rowid_no_sorter.snap │ │ │ ├── partial_idx.sqltest │ │ │ ├── placeholder.sqltest │ │ │ ├── pragma/ │ │ │ │ ├── default.sqltest │ │ │ │ ├── index_info.sqltest │ │ │ │ ├── index_list.sqltest │ │ │ │ ├── index_xinfo.sqltest │ │ │ │ ├── memory.sqltest │ │ │ │ ├── require_where.sqltest │ │ │ │ ├── table_list.sqltest │ │ │ │ └── user_version_10.sqltest │ │ │ ├── pragma_query_only.sqltest │ │ │ ├── returning-fk-constraint.sqltest │ │ │ ├── returning.sqltest │ │ │ ├── rollback.sqltest │ │ │ ├── row-value-in.sqltest │ │ │ ├── savepoint.sqltest │ │ │ ├── scalar-functions-datetime.sqltest │ │ │ ├── scalar-functions-format.sqltest │ │ │ ├── scalar-functions-printf.sqltest │ │ │ ├── scalar-functions.sqltest │ │ │ ├── select/ │ │ │ │ ├── default.sqltest │ │ │ │ ├── memory.sqltest │ │ │ │ └── simple_min_max.sqltest │ │ │ ├── simple-count-optimization.sqltest │ │ │ ├── snapshot_tests/ │ │ │ │ ├── aggregation/ │ │ │ │ │ ├── aggregation.sqltest │ │ │ │ │ └── snapshots/ │ │ │ │ │ ├── aggregation__aggregation-with-join.snap │ │ │ │ │ ├── aggregation__count-column.snap │ │ │ │ │ ├── aggregation__count-distinct.snap │ │ │ │ │ ├── aggregation__count-star-plus-expr.snap │ │ │ │ │ ├── aggregation__distinct-count-with-join.snap │ │ │ │ │ ├── aggregation__group-by-with-having.snap │ │ │ │ │ ├── aggregation__min-max-per-group.snap │ │ │ │ │ ├── aggregation__min-max-with-index.snap │ │ │ │ │ ├── aggregation__multi-column-group-by.snap │ │ │ │ │ ├── aggregation__multiple-aggregates.snap │ │ │ │ │ ├── aggregation__nested-aggregation.snap │ │ │ │ │ ├── aggregation__simple-count-mixed-case.snap │ │ │ │ │ ├── aggregation__simple-count-no-args.snap │ │ │ │ │ ├── aggregation__simple-count-star.snap │ │ │ │ │ ├── aggregation__simple-group-by-single-column.snap │ │ │ │ │ └── aggregation__whole-table-aggregates.snap │ │ │ │ ├── analyze/ │ │ │ │ │ ├── analyze.sqltest │ │ │ │ │ └── snapshots/ │ │ │ │ │ ├── analyze__aggregate-after-analyze.snap │ │ │ │ │ ├── analyze__analyze-all-stat1-exists.snap │ │ │ │ │ ├── analyze__analyze-all.snap │ │ │ │ │ ├── analyze__analyze-database.snap │ │ │ │ │ ├── analyze__analyze-empty-table.snap │ │ │ │ │ ├── analyze__analyze-index-stat1-exists.snap │ │ │ │ │ ├── analyze__analyze-specific-index.snap │ │ │ │ │ ├── analyze__analyze-stat1-exists.snap │ │ │ │ │ ├── analyze__join-after-analyze.snap │ │ │ │ │ ├── analyze__join-aggregate-after-analyze.snap │ │ │ │ │ ├── analyze__max-after-analyze.snap │ │ │ │ │ ├── analyze__min-after-analyze.snap │ │ │ │ │ ├── analyze__order-by-index-after-analyze.snap │ │ │ │ │ ├── analyze__query-after-analyze-category.snap │ │ │ │ │ ├── analyze__query-after-analyze-equality.snap │ │ │ │ │ ├── analyze__query-after-analyze-range.snap │ │ │ │ │ ├── analyze__query-composite-full-after-analyze.snap │ │ │ │ │ ├── analyze__query-composite-partial-after-analyze.snap │ │ │ │ │ ├── analyze__query-composite-range-after-analyze.snap │ │ │ │ │ ├── analyze__query-for-nulls-after-analyze.snap │ │ │ │ │ ├── analyze__query-identical-composite-after-analyze.snap │ │ │ │ │ ├── analyze__query-identical-values-after-analyze.snap │ │ │ │ │ ├── analyze__query-three-column-first-only-after-analyze.snap │ │ │ │ │ ├── analyze__query-three-column-full-after-analyze.snap │ │ │ │ │ ├── analyze__query-three-column-partial-after-analyze.snap │ │ │ │ │ └── analyze__query-with-nulls-after-analyze.snap │ │ │ │ ├── indexes/ │ │ │ │ │ ├── indexes.sqltest │ │ │ │ │ └── snapshots/ │ │ │ │ │ ├── indexes__aggregate-over-in-subquery-or-in-subquery.snap │ │ │ │ │ ├── indexes__composite-index-first-column-only.snap │ │ │ │ │ ├── indexes__composite-index-full-usage.snap │ │ │ │ │ ├── indexes__composite-index-order-by-match.snap │ │ │ │ │ ├── indexes__composite-index-range-on-second.snap │ │ │ │ │ ├── indexes__composite-index-second-column-only.snap │ │ │ │ │ ├── indexes__covering-index-all-columns.snap │ │ │ │ │ ├── indexes__covering-index-subset.snap │ │ │ │ │ ├── indexes__covering-index-with-aggregation.snap │ │ │ │ │ ├── indexes__full-scan-function-on-indexed.snap │ │ │ │ │ ├── indexes__full-scan-leading-wildcard.snap │ │ │ │ │ ├── indexes__full-scan-no-index.snap │ │ │ │ │ ├── indexes__full-scan-non-indexed-column.snap │ │ │ │ │ ├── indexes__full-scan-with-aggregation.snap │ │ │ │ │ ├── indexes__in-clause-multiple-values.snap │ │ │ │ │ ├── indexes__in-composite-collation-mismatch-larger-table.snap │ │ │ │ │ ├── indexes__in-composite-collation-mismatch.snap │ │ │ │ │ ├── indexes__in-list-collation-match-explicit-nocase.snap │ │ │ │ │ ├── indexes__in-list-collation-mismatch.snap │ │ │ │ │ ├── indexes__in-list-composite-prefix.snap │ │ │ │ │ ├── indexes__in-list-index-seek.snap │ │ │ │ │ ├── indexes__in-list-no-index-scan.snap │ │ │ │ │ ├── indexes__in-list-rowid-seek.snap │ │ │ │ │ ├── indexes__in-subquery-collation-mismatch.snap │ │ │ │ │ ├── indexes__in-subquery-or-in-list-left-join.snap │ │ │ │ │ ├── indexes__in-subquery-or-in-subquery-indexed.snap │ │ │ │ │ ├── indexes__index-for-correlated-max-materialized-cte-bytecode.snap │ │ │ │ │ ├── indexes__index-for-correlated-max-materialized-cte.snap │ │ │ │ │ ├── indexes__index-for-max-bytecode.snap │ │ │ │ │ ├── indexes__index-for-max-materialized-cte-bytecode.snap │ │ │ │ │ ├── indexes__index-for-max-materialized-cte-no-where-bytecode.snap │ │ │ │ │ ├── indexes__index-for-max-materialized-cte-no-where.snap │ │ │ │ │ ├── indexes__index-for-max-materialized-cte.snap │ │ │ │ │ ├── indexes__index-for-max.snap │ │ │ │ │ ├── indexes__index-for-min-bytecode.snap │ │ │ │ │ ├── indexes__index-for-min.snap │ │ │ │ │ ├── indexes__index-for-order-by.snap │ │ │ │ │ ├── indexes__index-not-equal.snap │ │ │ │ │ ├── indexes__indexed-by-delete-category.snap │ │ │ │ │ ├── indexes__indexed-by-delete-sku.snap │ │ │ │ │ ├── indexes__indexed-by-join-both.snap │ │ │ │ │ ├── indexes__indexed-by-join-one-side.snap │ │ │ │ │ ├── indexes__indexed-by-no-where-clause.snap │ │ │ │ │ ├── indexes__indexed-by-select-category.snap │ │ │ │ │ ├── indexes__indexed-by-select-price-range.snap │ │ │ │ │ ├── indexes__indexed-by-select-sku.snap │ │ │ │ │ ├── indexes__indexed-by-three-way-join.snap │ │ │ │ │ ├── indexes__indexed-by-update-price.snap │ │ │ │ │ ├── indexes__indexed-by-update-sku.snap │ │ │ │ │ ├── indexes__join-with-index.snap │ │ │ │ │ ├── indexes__max-with-equality-prefix-bytecode.snap │ │ │ │ │ ├── indexes__multiple-indexes-choice.snap │ │ │ │ │ ├── indexes__non-covering-needs-table-lookup.snap │ │ │ │ │ ├── indexes__not-indexed-delete.snap │ │ │ │ │ ├── indexes__not-indexed-select.snap │ │ │ │ │ ├── indexes__not-indexed-update.snap │ │ │ │ │ ├── indexes__or-different-columns.snap │ │ │ │ │ ├── indexes__or-indexed-and-non-indexed.snap │ │ │ │ │ ├── indexes__or-same-column.snap │ │ │ │ │ ├── indexes__or-with-and-combinations.snap │ │ │ │ │ ├── indexes__primary-key-lookup.snap │ │ │ │ │ ├── indexes__range-scan-between.snap │ │ │ │ │ ├── indexes__range-scan-greater-equal.snap │ │ │ │ │ ├── indexes__range-scan-greater-than.snap │ │ │ │ │ ├── indexes__range-scan-less-equal.snap │ │ │ │ │ ├── indexes__range-scan-less-than.snap │ │ │ │ │ ├── indexes__range-scan-timestamp.snap │ │ │ │ │ ├── indexes__range-scan-with-equality.snap │ │ │ │ │ ├── indexes__rowid-range-no-alias.snap │ │ │ │ │ ├── indexes__rowid-seek-no-alias.snap │ │ │ │ │ ├── indexes__simple-index-equality-lookup.snap │ │ │ │ │ ├── indexes__simple-index-with-filter.snap │ │ │ │ │ ├── indexes__subquery-with-index.snap │ │ │ │ │ ├── indexes__unordered-correlated-max-materialized-cte-bytecode.snap │ │ │ │ │ └── indexes__unordered-materialized-cte-max-bytecode.snap │ │ │ │ ├── joins/ │ │ │ │ │ ├── joins.sqltest │ │ │ │ │ └── snapshots/ │ │ │ │ │ ├── joins__bigass-three-table-join-rowid-seek.snap │ │ │ │ │ ├── joins__cross-join-explicit.snap │ │ │ │ │ ├── joins__cross-join-implicit.snap │ │ │ │ │ ├── joins__cross-join-limited.snap │ │ │ │ │ ├── joins__cross-join-with-filter.snap │ │ │ │ │ ├── joins__four-table-join-with-aggregation.snap │ │ │ │ │ ├── joins__hash-join-full-outer.snap │ │ │ │ │ ├── joins__hash-join-inner-basic.snap │ │ │ │ │ ├── joins__hash-join-left-outer-agg.snap │ │ │ │ │ ├── joins__hash-join-left-outer.snap │ │ │ │ │ ├── joins__hash-join-three-table.snap │ │ │ │ │ ├── joins__hash-join-with-aggregation.snap │ │ │ │ │ ├── joins__hash-join-with-order-by.snap │ │ │ │ │ ├── joins__index-assisted-join-order-items.snap │ │ │ │ │ ├── joins__index-assisted-join-product-lookup.snap │ │ │ │ │ ├── joins__index-assisted-join-range-scan.snap │ │ │ │ │ ├── joins__index-assisted-join-single-customer.snap │ │ │ │ │ ├── joins__join-multiple-conditions.snap │ │ │ │ │ ├── joins__join-with-exists.snap │ │ │ │ │ ├── joins__join-with-subquery.snap │ │ │ │ │ ├── joins__left-outer-join-basic.snap │ │ │ │ │ ├── joins__left-outer-join-chained.snap │ │ │ │ │ ├── joins__left-outer-join-find-nulls.snap │ │ │ │ │ ├── joins__left-outer-join-with-aggregation.snap │ │ │ │ │ ├── joins__mixed-join-types.snap │ │ │ │ │ ├── joins__multi-table-join-complex-filter.snap │ │ │ │ │ ├── joins__natural-join.snap │ │ │ │ │ ├── joins__prefer-constant-bound-index-over-join-dependent.snap │ │ │ │ │ ├── joins__self-join-category-hierarchy.snap │ │ │ │ │ ├── joins__self-join-employee-manager.snap │ │ │ │ │ ├── joins__self-join-find-subordinates.snap │ │ │ │ │ ├── joins__self-join-prefer-join-dependent-index.snap │ │ │ │ │ ├── joins__self-join-same-department.snap │ │ │ │ │ ├── joins__three-table-join-explicit.snap │ │ │ │ │ ├── joins__three-table-join-rowid-seek.snap │ │ │ │ │ ├── joins__three-table-join.snap │ │ │ │ │ ├── joins__two-table-inner-join-basic.snap │ │ │ │ │ ├── joins__two-table-inner-join-explicit.snap │ │ │ │ │ ├── joins__two-table-inner-join-with-filter.snap │ │ │ │ │ └── joins__using-clause-join.snap │ │ │ │ ├── modifications/ │ │ │ │ │ ├── modifications.sqltest │ │ │ │ │ └── snapshots/ │ │ │ │ │ ├── modifications__delete-with-subquery.snap │ │ │ │ │ ├── modifications__delete-with-where.snap │ │ │ │ │ ├── modifications__insert-multiple-rows.snap │ │ │ │ │ ├── modifications__insert-or-abort-multi-unique.snap │ │ │ │ │ ├── modifications__insert-or-fail-multi-unique.snap │ │ │ │ │ ├── modifications__insert-or-ignore-multi-unique.snap │ │ │ │ │ ├── modifications__insert-or-ignore.snap │ │ │ │ │ ├── modifications__insert-or-replace-multi-notnull-default-partial.snap │ │ │ │ │ ├── modifications__insert-or-replace-multi-notnull-default.snap │ │ │ │ │ ├── modifications__insert-or-replace-multi-unique.snap │ │ │ │ │ ├── modifications__insert-or-replace.snap │ │ │ │ │ ├── modifications__insert-or-rollback-multi-unique.snap │ │ │ │ │ ├── modifications__insert-select.snap │ │ │ │ │ ├── modifications__insert-single-row.snap │ │ │ │ │ └── modifications__update-simple.snap │ │ │ │ ├── returning/ │ │ │ │ │ ├── returning.sqltest │ │ │ │ │ └── snapshots/ │ │ │ │ │ ├── returning__delete-returning-expr.snap │ │ │ │ │ ├── returning__delete-returning-star.snap │ │ │ │ │ ├── returning__insert-returning-expr.snap │ │ │ │ │ ├── returning__insert-returning-star.snap │ │ │ │ │ ├── returning__update-returning-expr.snap │ │ │ │ │ ├── returning__update-returning-star.snap │ │ │ │ │ └── returning__upsert-returning.snap │ │ │ │ ├── setops/ │ │ │ │ │ ├── setops.sqltest │ │ │ │ │ └── snapshots/ │ │ │ │ │ ├── setops__compound-mixed-union.snap │ │ │ │ │ ├── setops__compound-union-all-three-way.snap │ │ │ │ │ ├── setops__compound-union-except.snap │ │ │ │ │ ├── setops__compound-union-intersect.snap │ │ │ │ │ ├── setops__except-basic.snap │ │ │ │ │ ├── setops__except-multiple-columns.snap │ │ │ │ │ ├── setops__intersect-basic.snap │ │ │ │ │ ├── setops__intersect-multiple-columns.snap │ │ │ │ │ ├── setops__subquery-with-union.snap │ │ │ │ │ ├── setops__union-all-basic.snap │ │ │ │ │ ├── setops__union-all-filtered.snap │ │ │ │ │ ├── setops__union-distinct.snap │ │ │ │ │ ├── setops__union-explicit-distinct.snap │ │ │ │ │ ├── setops__union-subquery-aggregation.snap │ │ │ │ │ └── setops__union-with-limit.snap │ │ │ │ ├── sorting/ │ │ │ │ │ ├── snapshots/ │ │ │ │ │ │ ├── sorting__large-offset-small-limit.snap │ │ │ │ │ │ ├── sorting__limit-with-offset.snap │ │ │ │ │ │ ├── sorting__limit-without-order.snap │ │ │ │ │ │ ├── sorting__order-by-desc.snap │ │ │ │ │ │ ├── sorting__order-by-expression.snap │ │ │ │ │ │ ├── sorting__order-by-index-with-range.snap │ │ │ │ │ │ ├── sorting__order-by-limit-one.snap │ │ │ │ │ │ ├── sorting__order-by-mixed-direction.snap │ │ │ │ │ │ ├── sorting__order-by-multiple-columns.snap │ │ │ │ │ │ ├── sorting__order-by-nullable-desc.snap │ │ │ │ │ │ ├── sorting__order-by-single-column.snap │ │ │ │ │ │ ├── sorting__order-by-with-index.snap │ │ │ │ │ │ ├── sorting__order-by-with-limit.snap │ │ │ │ │ │ ├── sorting__order-by-with-nulls.snap │ │ │ │ │ │ ├── sorting__order-by-with-where.snap │ │ │ │ │ │ ├── sorting__sort-elim-all-eq-order-non-index.snap │ │ │ │ │ │ ├── sorting__sort-elim-eq-col-desc-direction.snap │ │ │ │ │ │ ├── sorting__sort-elim-eq-prefix-order-suffix.snap │ │ │ │ │ │ ├── sorting__sort-elim-expr-index-eq-prefix-order-suffix.snap │ │ │ │ │ │ ├── sorting__sort-elim-expr-index-group-by-eq-prefix.snap │ │ │ │ │ │ ├── sorting__sort-elim-left-join-outer-rowid-suffix.snap │ │ │ │ │ │ ├── sorting__sort-elim-no-eq-order-non-leading.snap │ │ │ │ │ │ ├── sorting__sort-elim-range-prefix-order-suffix.snap │ │ │ │ │ │ └── sorting__sort-elim-two-eq-order-last.snap │ │ │ │ │ └── sorting.sqltest │ │ │ │ ├── subqueries/ │ │ │ │ │ ├── snapshots/ │ │ │ │ │ │ ├── subqueries__correlated-subquery-count.snap │ │ │ │ │ │ ├── subqueries__correlated-subquery-salary.snap │ │ │ │ │ │ ├── subqueries__correlated-subquery-where.snap │ │ │ │ │ │ ├── subqueries__cte-aggregation.snap │ │ │ │ │ │ ├── subqueries__cte-correlated-range-probe.snap │ │ │ │ │ │ ├── subqueries__cte-in-subquery-and-main.snap │ │ │ │ │ │ ├── subqueries__cte-materialized-order-aggregate-opposite-dir.snap │ │ │ │ │ │ ├── subqueries__cte-materialized-order-aggregate-same-dir.snap │ │ │ │ │ │ ├── subqueries__cte-materialized-order-multi-col-all-flipped.snap │ │ │ │ │ │ ├── subqueries__cte-materialized-order-multi-col-mixed-clean-flip.snap │ │ │ │ │ │ ├── subqueries__cte-materialized-order-multi-col-partial-mismatch.snap │ │ │ │ │ │ ├── subqueries__cte-materialized-order-multi-col-same-dir.snap │ │ │ │ │ │ ├── subqueries__cte-materialized-order-outer-beyond-cte.snap │ │ │ │ │ │ ├── subqueries__cte-materialized-order-prefix-match.snap │ │ │ │ │ │ ├── subqueries__cte-materialized-order-reused-opposite-dir.snap │ │ │ │ │ │ ├── subqueries__cte-materialized-order-reused-same-dir.snap │ │ │ │ │ │ ├── subqueries__cte-materialized-scalar-probe.snap │ │ │ │ │ │ ├── subqueries__cte-multiple-chained.snap │ │ │ │ │ │ ├── subqueries__cte-multiple-independent.snap │ │ │ │ │ │ ├── subqueries__cte-referenced-multiple-times.snap │ │ │ │ │ │ ├── subqueries__cte-referencing-previous.snap │ │ │ │ │ │ ├── subqueries__cte-self-join.snap │ │ │ │ │ │ ├── subqueries__cte-simple.snap │ │ │ │ │ │ ├── subqueries__cte-with-correlated-subquery.snap │ │ │ │ │ │ ├── subqueries__cte-with-exists.snap │ │ │ │ │ │ ├── subqueries__delete-returning-target-selfread.snap │ │ │ │ │ │ ├── subqueries__derived-table-join.snap │ │ │ │ │ │ ├── subqueries__derived-table-nested.snap │ │ │ │ │ │ ├── subqueries__derived-table-simple.snap │ │ │ │ │ │ ├── subqueries__exists-correlated.snap │ │ │ │ │ │ ├── subqueries__exists-multiple-conditions.snap │ │ │ │ │ │ ├── subqueries__grouped-correlated-subquery-having.snap │ │ │ │ │ │ ├── subqueries__grouped-correlated-subquery-order-by.snap │ │ │ │ │ │ ├── subqueries__in-subquery-aggregation.snap │ │ │ │ │ │ ├── subqueries__in-subquery-simple.snap │ │ │ │ │ │ ├── subqueries__not-exists-never-ordered.snap │ │ │ │ │ │ ├── subqueries__not-exists-no-orders.snap │ │ │ │ │ │ ├── subqueries__not-in-subquery.snap │ │ │ │ │ │ ├── subqueries__run-length-current-window.snap │ │ │ │ │ │ ├── subqueries__run-length-longest-window.snap │ │ │ │ │ │ ├── subqueries__scalar-subquery-cross-table.snap │ │ │ │ │ │ ├── subqueries__scalar-subquery-max-price.snap │ │ │ │ │ │ ├── subqueries__scalar-subquery-simple-count.snap │ │ │ │ │ │ ├── subqueries__subquery-within-cte.snap │ │ │ │ │ │ ├── subqueries__update-returning-target-selfread.snap │ │ │ │ │ │ ├── subquery-sort-elision__eqp-multi-hop-outer-orderby-elided.snap │ │ │ │ │ │ ├── subquery-sort-elision__eqp-outer-orderby-elided-by-cte.snap │ │ │ │ │ │ ├── subquery-sort-elision__eqp-outer-orderby-elided-by-group.snap │ │ │ │ │ │ ├── subquery-sort-elision__eqp-outer-orderby-not-elided-no-cte-order.snap │ │ │ │ │ │ ├── subquery-sort-elision__eqp-outer-orderby-not-elided-partial.snap │ │ │ │ │ │ ├── subquery-sort-elision__eqp-window-sort-elided-by-cte-order.snap │ │ │ │ │ │ ├── subquery-sort-elision__eqp-window-sort-elided-by-group.snap │ │ │ │ │ │ ├── subquery-sort-elision__eqp-window-sort-elided-multi-hop.snap │ │ │ │ │ │ ├── subquery-sort-elision__eqp-window-sort-not-elided-no-cte-order.snap │ │ │ │ │ │ └── subquery-sort-elision__eqp-window-sort-not-elided-partial.snap │ │ │ │ │ ├── subqueries.sqltest │ │ │ │ │ └── subquery-sort-elision.sqltest │ │ │ │ ├── tpch/ │ │ │ │ │ ├── snapshots/ │ │ │ │ │ │ ├── tpch__q1-pricing-summary.snap │ │ │ │ │ │ ├── tpch__q10-returned-item.snap │ │ │ │ │ │ ├── tpch__q11-important-stock.snap │ │ │ │ │ │ ├── tpch__q12-shipping-modes.snap │ │ │ │ │ │ ├── tpch__q13-customer-distribution.snap │ │ │ │ │ │ ├── tpch__q14-promotion-effect.snap │ │ │ │ │ │ ├── tpch__q15-top-supplier.snap │ │ │ │ │ │ ├── tpch__q16-parts-supplier.snap │ │ │ │ │ │ ├── tpch__q17-small-quantity-order.snap │ │ │ │ │ │ ├── tpch__q18-large-volume-customer.snap │ │ │ │ │ │ ├── tpch__q19-discounted-revenue.snap │ │ │ │ │ │ ├── tpch__q2-minimum-cost-supplier.snap │ │ │ │ │ │ ├── tpch__q20-potential-part-promotion.snap │ │ │ │ │ │ ├── tpch__q21-suppliers-kept-waiting.snap │ │ │ │ │ │ ├── tpch__q22-global-sales-opportunity.snap │ │ │ │ │ │ ├── tpch__q3-shipping-priority.snap │ │ │ │ │ │ ├── tpch__q4-order-priority.snap │ │ │ │ │ │ ├── tpch__q5-local-supplier-volume.snap │ │ │ │ │ │ ├── tpch__q6-forecasting-revenue.snap │ │ │ │ │ │ ├── tpch__q7-volume-shipping.snap │ │ │ │ │ │ ├── tpch__q8-national-market-share.snap │ │ │ │ │ │ └── tpch__q9-product-type-profit.snap │ │ │ │ │ └── tpch.sqltest │ │ │ │ └── windows/ │ │ │ │ ├── snapshots/ │ │ │ │ │ ├── windows__aggregate-windows.snap │ │ │ │ │ ├── windows__multiple-windows.snap │ │ │ │ │ ├── windows__percent-of-total.snap │ │ │ │ │ ├── windows__row-number-basic.snap │ │ │ │ │ ├── windows__running-total.snap │ │ │ │ │ ├── windows__window-order-by.snap │ │ │ │ │ ├── windows__window-over-grouped.snap │ │ │ │ │ └── windows__window-with-subquery.snap │ │ │ │ └── windows.sqltest │ │ │ ├── snapshots/ │ │ │ │ ├── cte_cardinality__cte-graph-traversal-multi-index-or.snap │ │ │ │ ├── cte_cardinality__small-cte-materialized-big-cte-outer.snap │ │ │ │ ├── cte_cardinality__small-cte-reordered-to-outer.snap │ │ │ │ ├── cte_cardinality__small-subquery-reordered-to-outer.snap │ │ │ │ ├── cte_cardinality__three-ctes-big-outer-others-materialized.snap │ │ │ │ ├── graph_traversal_text_pk__cte-neighbor-traversal-text-pk.snap │ │ │ │ ├── multi_index_dml__delete-multi-index-and-access-shape.snap │ │ │ │ ├── multi_index_dml__delete-multi-index-compound-or-access-shape.snap │ │ │ │ ├── multi_index_dml__delete-multi-index-or-access-shape.snap │ │ │ │ ├── multi_index_dml__delete-multi-index-or-one-row-subset-access-shape.snap │ │ │ │ ├── multi_index_dml__delete-multi-index-or-two-row-subset-access-shape.snap │ │ │ │ ├── multi_index_dml__multi-index-or-access-shape.snap │ │ │ │ ├── multi_index_dml__update-multi-index-and-access-shape.snap │ │ │ │ ├── multi_index_dml__update-multi-index-or-access-shape.snap │ │ │ │ ├── multi_index_intersection__composite-index-preferred-over-intersection-eqp.snap │ │ │ │ ├── multi_index_intersection__multi-index-and-2-way-eqp.snap │ │ │ │ ├── multi_index_or_compound__compound-or-benchmark-shape-eqp.snap │ │ │ │ ├── multi_index_or_compound__compound-or-compound-branch-seek-eqp.snap │ │ │ │ ├── multi_index_or_compound__compound-or-correlated-subquery-eqp.snap │ │ │ │ ├── multi_index_or_compound__compound-or-cross-table-eqp.snap │ │ │ │ ├── multi_index_or_compound__compound-or-eqp.snap │ │ │ │ ├── multi_index_or_compound__compound-or-mixed-residual-eqp.snap │ │ │ │ ├── multi_index_or_compound__compound-or-rowid-branch-eqp.snap │ │ │ │ ├── multi_index_or_join__cte-join-with-or-plan.snap │ │ │ │ ├── multi_index_or_join__multi-index-or-in-join-parenthesized-plan.snap │ │ │ │ └── multi_index_or_join__multi-index-or-in-join-plan.snap │ │ │ ├── strict.sqltest │ │ │ ├── subquery/ │ │ │ │ ├── cte_chain_regression.sqltest │ │ │ │ ├── cte_materialization_adversarial.sqltest │ │ │ │ ├── default.sqltest │ │ │ │ ├── expressions.sqltest │ │ │ │ ├── materialized_cte_seek.sqltest │ │ │ │ ├── memory.sqltest │ │ │ │ └── subquery_cte_equivalence_tests.sqltest │ │ │ ├── total-changes.sqltest │ │ │ ├── transactions.sqltest │ │ │ ├── trigger-before-insert-affinity.sqltest │ │ │ ├── trigger-last-insert-rowid.sqltest │ │ │ ├── trigger-quoted-identifiers.sqltest │ │ │ ├── trigger-virtual-table-innocuous.sqltest │ │ │ ├── trigger.sqltest │ │ │ ├── trigger_attach_regression.sqltest │ │ │ ├── trigger_fk_cascade.sqltest │ │ │ ├── trigger_on_conflict.sqltest │ │ │ ├── unnest-exists.sqltest │ │ │ ├── update-returning-correlated-subquery.sqltest │ │ │ ├── update.sqltest │ │ │ ├── update_attached_db_index.sqltest │ │ │ ├── update_expression_index.sqltest │ │ │ ├── update_expression_index_affinity.sqltest │ │ │ ├── update_index_affinity.sqltest │ │ │ ├── update_or_replace_rowid_secondary_index.sqltest │ │ │ ├── upsert-expr-index.sqltest │ │ │ ├── upsert.sqltest │ │ │ ├── vacuum_into.sqltest │ │ │ ├── values.sqltest │ │ │ ├── view-rowid.sqltest │ │ │ ├── views.sqltest │ │ │ ├── virtual-table-left-join.sqltest │ │ │ ├── where/ │ │ │ │ ├── default.sqltest │ │ │ │ ├── memory.sqltest │ │ │ │ └── small.sqltest │ │ │ ├── window/ │ │ │ │ ├── default.sqltest │ │ │ │ ├── group-by-order-by-partition.sqltest │ │ │ │ └── memory.sqltest │ │ │ ├── window-agg-row-value.sqltest │ │ │ └── window-selfjoin-reset-sorter.sqltest │ │ └── turso-tests/ │ │ ├── alter_column.sqltest │ │ ├── analyze.sqltest │ │ ├── array-bugs.sqltest │ │ ├── array-edge-cases.sqltest │ │ ├── array.sqltest │ │ ├── attach/ │ │ │ ├── cross_db_views_rejection.sqltest │ │ │ ├── default.sqltest │ │ │ ├── memory.sqltest │ │ │ ├── small.sqltest │ │ │ └── writes.sqltest │ │ ├── builtin_pg_types.sqltest │ │ ├── custom_type_alter_default.sqltest │ │ ├── custom_type_datetime_validation.sqltest │ │ ├── custom_type_default_values.sqltest │ │ ├── custom_type_deterministic.sqltest │ │ ├── custom_type_expr_index.sqltest │ │ ├── custom_type_notnull_decode.sqltest │ │ ├── custom_type_operators.sqltest │ │ ├── custom_type_ordering.sqltest │ │ ├── custom_type_upsert_trigger.sqltest │ │ ├── custom_types.sqltest │ │ ├── custom_types_fk_cascade.sqltest │ │ ├── custom_types_non_strict.sqltest │ │ ├── fts.sqltest │ │ ├── json_object_star.sqltest │ │ ├── materialized_view_text_arithmetic.sqltest │ │ ├── materialized_views.sqltest │ │ ├── matview_create_same_txn.sqltest │ │ ├── matview_insert_or_replace.sqltest │ │ ├── matview_integrity_check.sqltest │ │ ├── multi-column-subquery-comparison.sqltest │ │ ├── mvcc_feature_compat.sqltest │ │ ├── mvcc_left_join_null_row.sqltest │ │ ├── on_conflict_constraint.sqltest │ │ ├── placeholder.sqltest │ │ ├── raise.sqltest │ │ ├── regexp.sqltest │ │ ├── row-value-adversarial.sqltest │ │ ├── strict_check_types.sqltest │ │ ├── strict_custom_type_input_validation.sqltest │ │ ├── time.sqltest │ │ └── vector.sqltest │ ├── stress/ │ │ ├── Cargo.toml │ │ ├── Dockerfile.antithesis-config │ │ ├── docker-compose.yaml │ │ ├── docker-entrypoint.sh │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── opts.rs │ │ ├── run-miri.sh │ │ └── tests/ │ │ └── shuttle_mvcc.rs │ ├── stress-go/ │ │ ├── README.md │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── system/ │ │ ├── all.test │ │ ├── gen-bigass-database.py │ │ ├── gen-database.py │ │ ├── tester.tcl │ │ └── vtab.test │ └── unreliable-libc/ │ ├── Makefile │ └── file.c ├── tests/ │ ├── Cargo.toml │ ├── README.md │ ├── fuzz/ │ │ ├── cte.rs │ │ ├── custom_types.rs │ │ ├── expression_index.rs │ │ ├── grammar_generator.rs │ │ ├── helpers.rs │ │ ├── join.rs │ │ ├── journal_mode.rs │ │ ├── mod.rs │ │ ├── orderby_collation.rs │ │ ├── raise.rs │ │ ├── rowid_alias.rs │ │ ├── savepoint.rs │ │ ├── subjournal.rs │ │ ├── subquery.rs │ │ └── test_join_optimizer.rs │ ├── integration/ │ │ ├── assert_details.rs │ │ ├── common.rs │ │ ├── conflict_resolution.rs │ │ ├── custom_types.rs │ │ ├── functions/ │ │ │ ├── mod.rs │ │ │ ├── test_cdc.rs │ │ │ ├── test_function_rowid.rs │ │ │ ├── test_sum.rs │ │ │ ├── test_uuid.rs │ │ │ └── test_wal_api.rs │ │ ├── fuzz_transaction/ │ │ │ └── mod.rs │ │ ├── index_method/ │ │ │ └── mod.rs │ │ ├── integrity_check.rs │ │ ├── mod.rs │ │ ├── mvcc.rs │ │ ├── pragma.rs │ │ ├── query_processing/ │ │ │ ├── encryption.rs │ │ │ ├── mod.rs │ │ │ ├── test_alter_table_reopen.rs │ │ │ ├── test_btree.rs │ │ │ ├── test_ddl.rs │ │ │ ├── test_expr_index.rs │ │ │ ├── test_hash_join_materialization.rs │ │ │ ├── test_materialized_subquery.rs │ │ │ ├── test_multi_thread.rs │ │ │ ├── test_page1.rs │ │ │ ├── test_read_path.rs │ │ │ ├── test_schema_updated.rs │ │ │ ├── test_transactions.rs │ │ │ ├── test_type_affinity.rs │ │ │ ├── test_vacuum.rs │ │ │ └── test_write_path.rs │ │ ├── statement_reset.rs │ │ ├── stmt_journal.rs │ │ ├── storage/ │ │ │ ├── autovacuum.rs │ │ │ ├── checksum.rs │ │ │ ├── header_version.rs │ │ │ ├── mod.rs │ │ │ └── short_read.rs │ │ ├── trigger.rs │ │ └── wal/ │ │ ├── mod.rs │ │ └── test_wal.rs │ └── lib.rs ├── tlaplus/ │ └── sqlite-tx/ │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── SqliteTx.cfg │ └── SqliteTx.tla └── tools/ └── dbhash/ ├── Cargo.toml ├── README.md ├── src/ │ ├── encoder.rs │ ├── lib.rs │ └── main.rs └── tests/ ├── README.md └── sqlite_compat.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [env] LIBSQLITE3_FLAGS = "-DSQLITE_ENABLE_MATH_FUNCTIONS" # necessary for rusqlite dependency in order to bundle SQLite with math functions included [build] # turso-sync package uses tokio_unstable to seed LocalRuntime and make it deterministic # unfortunately, cargo commands invoked from workspace root didn't capture config.toml from dependent crate # so, we set this cfg globally for workspace (see relevant issue build build-target: https://github.com/rust-lang/cargo/issues/7004) rustflags = [ "--cfg=tokio_unstable", ] # without these rustflags, cargo test failing because we are using shared napi library (turso_node) # (these settings copied from napi-rs itself, see https://github.com/napi-rs/napi-rs/issues/2130#issuecomment-2239407809) [target.'cfg(target_vendor = "apple")'] rustflags = [ "--cfg=tokio_unstable", "-C", "link-args=-Wl,-undefined,dynamic_lookup,-no_fixup_chains", ] # To be able to run unit tests on Linux, support compilation to 'x86_64-unknown-linux-gnu'. # pthread_key_create() destructors and segfault after a DSO unloading https://sourceware.org/bugzilla/show_bug.cgi?id=21031 [target.'cfg(all(target_os = "linux", target_env = "gnu"))'] rustflags = [ "--cfg=tokio_unstable", "-C", "link-args=-Wl,--warn-unresolved-symbols", "-C", "link-args=-Wl,-z,nodelete", ] # To be able to run unit tests on Windows, support compilation to 'x86_64-pc-windows-msvc'. [target.'cfg(target_env = "msvc")'] rustflags = [ "--cfg=tokio_unstable", "-C", "link-args=/FORCE" ] # Android targets need 16KB page alignment for Android 15+ compatibility [target.aarch64-linux-android] rustflags = [ "--cfg=tokio_unstable", "-C", "link-args=-Wl,-z,max-page-size=16384", ] [target.armv7-linux-androideabi] rustflags = [ "--cfg=tokio_unstable", "-C", "link-args=-Wl,-z,max-page-size=16384", ] [target.x86_64-linux-android] rustflags = [ "--cfg=tokio_unstable", "-C", "link-args=-Wl,-z,max-page-size=16384", ] [target.i686-linux-android] rustflags = [ "--cfg=tokio_unstable", "-C", "link-args=-Wl,-z,max-page-size=16384", ] ================================================ FILE: .claude/skills/async-io-model/SKILL.md ================================================ --- name: async-io-model description: Explanations of common asynchronous patterns used in tursodb. Involves IOResult, state machines, re-entrancy pitfalls, CompletionGroup. Always use these patterns in `core` when doing anything IO --- # Async I/O Model Guide Turso uses cooperative yielding with explicit state machines instead of Rust async/await. ## Core Types ```rust pub enum IOCompletions { Single(Completion), } #[must_use] pub enum IOResult { Done(T), // Operation complete, here's the result IO(IOCompletions), // Need I/O, call me again after completions finish } ``` Functions returning `IOResult` must be called repeatedly until `Done`. ## Completion and CompletionGroup A `Completion` tracks a single I/O operation: ```rust pub struct Completion { /* ... */ } impl Completion { pub fn finished(&self) -> bool; pub fn succeeded(&self) -> bool; pub fn get_error(&self) -> Option; } ``` To wait for multiple I/O operations, use `CompletionGroup`: ```rust let mut group = CompletionGroup::new(|_| {}); // Add individual completions group.add(&completion1); group.add(&completion2); // Build into single completion that finishes when all complete let combined = group.build(); io_yield_one!(combined); ``` `CompletionGroup` features: - Aggregates multiple completions into one - Calls callback when all complete (or any errors) - Can nest groups (add a group's completion to another group) - Cancellable via `group.cancel()` ## Helper Macros ### `return_if_io!` Unwraps `IOResult`, propagates IO variant up the call stack: ```rust let result = return_if_io!(some_io_operation()); // Only reaches here if operation returned Done ``` ### `io_yield_one!` Yields a single completion: ```rust io_yield_one!(completion); // Returns Ok(IOResult::IO(Single(completion))) ``` ## State Machine Pattern Operations that may yield use explicit state enums: ```rust enum MyOperationState { Start, WaitingForRead { page: PageRef }, Processing { data: Vec }, Done, } ``` The function loops, matching on state and transitioning: ```rust fn my_operation(&mut self) -> Result> { loop { match &mut self.state { MyOperationState::Start => { let (page, completion) = start_read(); self.state = MyOperationState::WaitingForRead { page }; io_yield_one!(completion); } MyOperationState::WaitingForRead { page } => { let data = page.get_contents(); self.state = MyOperationState::Processing { data: data.to_vec() }; // No yield, continue loop } MyOperationState::Processing { data } => { let result = process(data); self.state = MyOperationState::Done; return Ok(IOResult::Done(result)); } MyOperationState::Done => unreachable!(), } } } ``` ## Re-Entrancy: The Critical Pitfall **State mutations before yield points cause bugs on re-entry.** ### Wrong ```rust fn bad_example(&mut self) -> Result> { self.counter += 1; // Mutates state return_if_io!(something_that_might_yield()); // If yields, re-entry will increment again! Ok(IOResult::Done(())) } ``` If `something_that_might_yield()` returns `IO`, caller waits for completion, then calls `bad_example()` again. `counter` gets incremented twice (or more). ### Correct: Mutate After Yield ```rust fn good_example(&mut self) -> Result> { return_if_io!(something_that_might_yield()); self.counter += 1; // Only reached once, after IO completes Ok(IOResult::Done(())) } ``` ### Correct: Use State Machine ```rust enum State { Start, AfterIO } fn good_example(&mut self) -> Result> { loop { match self.state { State::Start => { // Don't mutate shared state here self.state = State::AfterIO; return_if_io!(something_that_might_yield()); } State::AfterIO => { self.counter += 1; // Safe: only entered once return Ok(IOResult::Done(())); } } } } ``` ## Common Re-Entrancy Bugs | Pattern | Problem | |---------|---------| | `vec.push(x); return_if_io!(...)` | Vec grows on each re-entry | | `idx += 1; return_if_io!(...)` | Index advances multiple times | | `map.insert(k,v); return_if_io!(...)` | Duplicate inserts or overwrites | | `flag = true; return_if_io!(...)` | Usually ok, but check logic | ## State Enum Design Encode progress in state variants: ```rust // Good: index is part of state, preserved across yields enum ProcessState { Start, ProcessingItem { idx: usize, items: Vec }, Done, } // Loop advances idx only when transitioning states ProcessingItem { idx, items } => { return_if_io!(process_item(&items[idx])); if idx + 1 < items.len() { self.state = ProcessingItem { idx: idx + 1, items }; } else { self.state = Done; } } ``` ## Turso Implementation Key files: - `core/types.rs` - `IOResult`, `IOCompletions`, `return_if_io!`, `return_and_restore_if_io!` - `core/io/completions.rs` - `Completion`, `CompletionGroup` - `core/util.rs` - `io_yield_one!` macro - `core/state_machine.rs` - Generic `StateMachine` wrapper - `core/storage/btree.rs` - Many state machine examples - `core/storage/pager.rs` - `CompletionGroup` usage examples ## Testing Async Code Re-entrancy bugs often only manifest under specific IO timing. Use: - Deterministic simulation (`testing/simulator/`) - Whopper concurrent DST (`testing/concurrent-simulator/`) - Fault injection to force yields at different points ## References - `docs/manual.md` section on I/O ================================================ FILE: .claude/skills/cdc/SKILL.md ================================================ --- name: cdc description: Change Data Capture - architecture, entrypoints, bytecode emission, sync engine integration, tests --- # CDC (Change Data Capture) - Internal Feature Map ## Overview CDC tracks INSERT/UPDATE/DELETE changes on database tables by writing change records into a dedicated CDC table (`turso_cdc` by default). It is per-connection, enabled via PRAGMA, and operates at the bytecode generation (translate) layer. The sync engine consumes CDC records to push local changes to the remote. ## Architecture Diagram ``` User SQL (INSERT/UPDATE/DELETE/DDL) | v ┌─────────────────────────────────────────────────┐ │ Translate layer (core/translate/) │ │ ┌───────────────────────────────────────────┐ │ │ │ prepare_cdc_if_necessary() │ │ │ │ - checks CaptureDataChangesInfo │ │ │ │ - opens CDC table cursor (OpenWrite) │ │ │ │ - skips if target == CDC table itself │ │ │ └───────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────┐ │ │ │ emit_cdc_insns() │ │ │ │ - writes (change_id, change_time, │ │ │ │ change_type, table_name, id, │ │ │ │ before, after, updates) into CDC tbl │ │ │ └───────────────────────────────────────────┘ │ │ + emit_cdc_full_record() / emit_cdc_patch_record() │ └─────────────────────────────────────────────────┘ | v CDC table (turso_cdc or custom name) | v ┌─────────────────────────────────────────────────┐ │ Sync engine (sync/engine/) │ │ DatabaseTape reads CDC table → DatabaseChange │ │ → apply/revert → push to remote │ └─────────────────────────────────────────────────┘ ``` ## Core Data Types ### `CaptureDataChangesMode` + `CaptureDataChangesInfo` — `core/lib.rs` CDC behavior is controlled by two types: ```rust #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] #[repr(u8)] enum CdcVersion { V1 = 1, V2 = 2, } const CDC_VERSION_CURRENT: CdcVersion = CdcVersion::V2; enum CaptureDataChangesMode { Id, // capture only rowid Before, // capture before-image After, // capture after-image Full, // before + after + updates } struct CaptureDataChangesInfo { mode: CaptureDataChangesMode, table: String, // CDC table name version: Option, // schema version (V1 or V2) } ``` The connection stores `Option` — `None` means CDC is off. Key methods on `CdcVersion`: - `has_commit_record()` — `self >= V2`, gates COMMIT record emission - `Display`/`FromStr` — round-trips `"v1"` ↔ `V1`, `"v2"` ↔ `V2` Key methods on `CaptureDataChangesInfo`: - `parse(value: &str, version: Option)` — parses PRAGMA argument `"[,]"`, returns `None` for "off" - `cdc_version()` — returns `CdcVersion` (panics if version is None). Single accessor replacing old `is_v1()`/`is_v2()`/`version()` methods. - `has_before()` / `has_after()` / `has_updates()` — mode capability checks - `mode_name()` — returns mode as string Convenience trait `CaptureDataChangesExt` on `Option` provides: - `has_before()` / `has_after()` / `has_updates()` — delegates to inner, returns false for None - `table()` — returns `Option<&str>`, None when CDC is off ### CDC Table Schema v1 Default table name: `turso_cdc` (constant `TURSO_CDC_DEFAULT_TABLE_NAME`) ```sql CREATE TABLE turso_cdc ( change_id INTEGER PRIMARY KEY AUTOINCREMENT, change_time INTEGER, -- unixepoch() change_type INTEGER, -- 1=INSERT, 0=UPDATE, -1=DELETE table_name TEXT, id , -- rowid of changed row before BLOB, -- binary record (before-image) after BLOB, -- binary record (after-image) updates BLOB -- binary record of per-column changes ); ``` ### CDC Table Schema v2 (current) ```sql CREATE TABLE turso_cdc ( change_id INTEGER PRIMARY KEY AUTOINCREMENT, change_time INTEGER, -- unixepoch() change_type INTEGER, -- 1=INSERT, 0=UPDATE, -1=DELETE, 2=COMMIT table_name TEXT, id , -- rowid of changed row before BLOB, -- binary record (before-image) after BLOB, -- binary record (after-image) updates BLOB, -- binary record of per-column changes change_txn_id INTEGER -- transaction ID (groups rows into transactions) ); ``` v2 adds: - `change_txn_id` column — groups CDC rows by transaction. Assigned via `conn_txn_id(candidate)` opcode which get-or-sets a per-connection transaction ID. - `change_type=2` (COMMIT) records — mark transaction boundaries. Emitted once per statement in autocommit mode, or on explicit `COMMIT`. The CDC table is created at runtime by the `InitCdcVersion` opcode via `CREATE TABLE IF NOT EXISTS`. ### CDC Version Table When CDC is first enabled, a version tracking table is created: ```sql CREATE TABLE turso_cdc_version ( table_name TEXT PRIMARY KEY, version TEXT NOT NULL ); ``` Current version: `CDC_VERSION_CURRENT = CdcVersion::V2` (defined in `core/lib.rs`, re-exported from `core/translate/pragma.rs`) ### Version Detection in InitCdcVersion The `InitCdcVersion` opcode detects v1 vs v2 by checking whether the CDC table already exists before creating it: - If CDC table already exists but has no version row → v1 (pre-existing table from before version tracking) - If CDC table doesn't exist → create with current version (v2) - If version row already exists → use that version as-is ### `DatabaseChange` — `sync/engine/src/types.rs:229-249` Sync engine's Rust representation of a CDC row. Has `into_apply()` and `into_revert()` methods for forward/backward replay. ### `OperationMode` — `core/translate/emitter.rs` Used by `emit_cdc_insns()` to determine `change_type` value: - `INSERT` → 1 - `UPDATE` / `SELECT` → 0 - `DELETE` → -1 - `COMMIT` → 2 (v2 only, emitted by `emit_cdc_commit_insns`) ## Entry Points ### 1. PRAGMA — Enable/Disable CDC **Set:** `core/translate/pragma.rs` - Checks MVCC is not enabled (CDC and MVCC are mutually exclusive) - Parses mode string via `CaptureDataChangesInfo::parse()` with `CDC_VERSION_CURRENT` - Emits a single `InitCdcVersion` opcode — all CDC setup (table creation, version tracking, state change) happens at execution time **Get (read current mode):** `core/translate/pragma.rs` - Returns 3 columns: `mode`, `table`, `version` - When off: returns `("off", NULL, NULL)` - When active: returns `(mode_name, table, version)` **Pragma registration:** `core/pragma.rs` — `CaptureDataChangesConn` (and deprecated alias `UnstableCaptureDataChangesConn`) with columns `["mode", "table", "version"]` ### 2. Connection State **Field:** `core/connection.rs` — `capture_data_changes: RwLock>` **Getter:** `get_capture_data_changes_info()` — returns read guard **Setter:** `set_capture_data_changes_info(opts: Option)` **Default:** initialized as `None` (CDC off) ### 3. ProgramBuilder Integration **Field:** `core/vdbe/builder.rs` — `capture_data_changes_info: Option` **Accessor:** `capture_data_changes_info()` — returns `&Option` **Passed from:** `core/translate/mod.rs` — read from connection when creating builder ### 4. PrepareContext **Field:** `core/vdbe/mod.rs` — `capture_data_changes: Option` **Set from:** `PrepareContext::from_connection()` — clones from `connection.get_capture_data_changes_info()` ### 5. InitCdcVersion Opcode — `core/vdbe/execute.rs` Always emitted by PRAGMA SET. Handles all CDC setup at execution time: 1. For "off": stores `None` in `state.pending_cdc_info`, returns early 2. Checks if CDC table already exists (for v1 backward compatibility) 3. Creates CDC table (`CREATE TABLE IF NOT EXISTS ...`) — v2 schema with `change_txn_id` column 4. Creates version table (`CREATE TABLE IF NOT EXISTS turso_cdc_version ...`) 5. Inserts version row: if CDC table pre-existed → "v1", otherwise → current version ("v2"). Uses `INSERT OR IGNORE` to preserve existing version rows. 6. Reads back actual version from the table 7. Stores computed `CaptureDataChangesInfo` in `state.pending_cdc_info` The connection's CDC state is **not applied in the opcode**. Instead, `pending_cdc_info` is applied in `halt()` only after the transaction commits successfully. This ensures atomicity: if any step fails and the transaction rolls back, the connection's CDC state remains unchanged. All table creation is done via nested `conn.prepare()`/`run_ignore_rows()` calls rather than bytecode emission, because the PRAGMA plan can't contain DML against tables that don't exist yet in the schema. ## Bytecode Emission (core/translate/emitter.rs) These are the core CDC code generation functions: | Function | Purpose | |----------|---------| | `prepare_cdc_if_necessary()` | Opens CDC table cursor if CDC is active and target != CDC table | | `emit_cdc_full_record()` | Reads all columns from cursor into a MakeRecord (for before/after image) | | `emit_cdc_patch_record()` | Builds record from in-flight register values (for after-image of INSERT/UPDATE) | | `emit_cdc_insns()` | Writes a single CDC row per changed row (INSERT/UPDATE/DELETE). Called per-row inside DML loops. | | `emit_cdc_commit_insns()` | Writes a COMMIT record (change_type=2) into CDC table (v2 only). Raw emission, no autocommit check. | | `emit_cdc_autocommit_commit()` | End-of-statement COMMIT emission. Checks `is_autocommit()` at runtime — only emits COMMIT if in autocommit mode. v2 only. | ### COMMIT Emission Strategy (v2) Per-row call sites use `emit_cdc_insns()` (no COMMIT). End-of-statement sites call `emit_cdc_autocommit_commit()` which checks `is_autocommit()` at runtime: - **Autocommit mode:** emits a COMMIT record after the statement completes - **Explicit transaction (`BEGIN...COMMIT`):** skips per-statement COMMIT; the explicit `COMMIT` statement emits the COMMIT record via `emit_cdc_commit_insns()` This ensures multi-row statements like `INSERT INTO t VALUES (1),(2),(3)` produce one COMMIT at the end, not one per row. ## Integration Points — Where CDC Records Are Emitted ### INSERT — `core/translate/insert.rs` - **Per-row:** `emit_cdc_insns()` after insert, and before delete for REPLACE/conflict - **End-of-statement:** `emit_cdc_autocommit_commit()` in `emit_epilogue()` after the insert loop ### UPDATE — `core/translate/emitter.rs` - **Per-row:** captures before-image, after-image via patch record, emits `emit_cdc_insns()` - **End-of-statement:** `emit_cdc_autocommit_commit()` after the update loop ### DELETE — `core/translate/emitter.rs` - **Per-row:** captures before-image and emits `emit_cdc_insns()` - **End-of-statement:** `emit_cdc_autocommit_commit()` after the delete loop ### UPSERT (ON CONFLICT DO UPDATE) — `core/translate/upsert.rs` - **Per-row:** `emit_cdc_insns()` for all three cases: pure insert, update after conflict, replace - No end-of-statement COMMIT — upsert shares INSERT's epilogue ### Schema Changes (DDL) — `core/translate/schema.rs` - **CREATE TABLE:** `emit_cdc_insns()` (insert into `sqlite_schema`) + `emit_cdc_autocommit_commit()` - **DROP TABLE:** `emit_cdc_insns()` per-row in metadata loop + `emit_cdc_autocommit_commit()` after loop - **CREATE INDEX:** `emit_cdc_insns()` + `emit_cdc_autocommit_commit()` (`core/translate/schema.rs`) - **DROP INDEX:** `emit_cdc_insns()` per-row + `emit_cdc_autocommit_commit()` after loop (`core/translate/index.rs`) DDL in explicit transactions (`BEGIN; CREATE TABLE t(x); COMMIT`) does NOT emit per-statement COMMIT — the autocommit check prevents it. ### ALTER TABLE — `core/translate/update.rs` - Sets `cdc_update_alter_statement` on the update plan when CDC has updates mode ### Views/Triggers — Explicitly excluded - `core/translate/view.rs` — passes `None` for CDC cursor - `core/translate/trigger.rs` — passes `None` for CDC cursor ### Subqueries — No CDC - `core/translate/subquery.rs` — `cdc_cursor_id: None` ## Helper Functions (for reading CDC data) ### `table_columns_json_array(table_name)` — `core/function.rs`, `core/vdbe/execute.rs` Returns JSON array of column names for a table. Used to interpret binary records. ### `bin_record_json_object(columns_json, blob)` — `core/function.rs`, `core/vdbe/execute.rs` Decodes a binary record (from `before`/`after`/`updates` columns) into a JSON object using column names. ## Sync Engine Integration The sync engine is the primary consumer of CDC data. ### DatabaseTape — `sync/engine/src/database_tape.rs` - **CDC config:** `DEFAULT_CDC_TABLE_NAME = "turso_cdc"`, `DEFAULT_CDC_MODE = "full"` - **PRAGMA name:** `CDC_PRAGMA_NAME = "capture_data_changes_conn"` - **Initialization:** `connect()` sets CDC pragma and caches `cdc_version` from `turso_cdc_version` table. Must be called before `iterate_changes()`. - **Version caching:** `cdc_version: RwLock>` — set by `connect()`, read by `iterate_changes()`. Panics if not set. - **Iterator:** `DatabaseChangesIterator` reads CDC table in batches, emits `DatabaseTapeOperation`. For v2, real COMMIT records from the table are emitted. For v1, a synthetic Commit is appended at end of batch. `ignore_schema_changes: true` (default) filters out `sqlite_schema` row changes but not COMMIT records. ### Sync Operations — `sync/engine/src/database_sync_operations.rs` - **Change counting:** `SELECT COUNT(*) FROM turso_cdc WHERE change_id > ?` ### Sync Engine — `sync/engine/src/database_sync_engine.rs` - **Initialization:** `open_db()` calls `main_tape.connect(coro)` to ensure CDC is set up and version is cached before any `iterate_changes()` calls. - During `apply_changes`, checks if CDC table existed, re-creates it after sync ### Replay Generator — `sync/engine/src/database_replay_generator.rs` - Requires `updates` column to be populated (full mode) ## Bindings CDC Surface All bindings expose `cdc_operations` as part of sync stats: | Binding | File | |---------|------| | Python | `bindings/python/src/turso_sync.rs` | | JavaScript | `bindings/javascript/sync/src/lib.rs` | | JS (generator) | `bindings/javascript/sync/src/generator.rs` | | Go | `bindings/go/bindings_sync.go` | | React Native | `bindings/react-native/src/types.ts` | | SDK Kit (C header) | `sync/sdk-kit/turso_sync.h` | | SDK Kit (Rust) | `sync/sdk-kit/src/bindings.rs` | ## Tests - **Integration tests:** `tests/integration/functions/test_cdc.rs` — covers all modes, CRUD, transactions, schema changes, version table, backward compatibility. Registered in `tests/integration/functions/mod.rs`. - **Sync engine tests:** `sync/engine/src/database_tape.rs` — CDC table reads, tape iteration, replay of schema changes. - **JS binding tests:** `bindings/javascript/sync/packages/{wasm,native}/promise.test.ts` Run: `cargo test -- test_cdc` (integration) or `cargo test -p turso_sync_engine -- database_tape` (sync engine). ## User-facing Documentation - **CLI manual page:** `cli/manuals/cdc.md` — accessible via `.manual cdc` in the REPL - **Database manual:** `docs/manual.md` — CDC section linked in TOC ## Key Design Decisions 1. **Per-connection, not per-database.** Each connection has its own CDC mode and can target different tables. 2. **Bytecode-level implementation.** CDC instructions are emitted alongside the actual DML bytecode during translation — no runtime hooks or triggers. 3. **Self-exclusion.** Changes to the CDC table and `turso_cdc_version` table are never captured (checked in `prepare_cdc_if_necessary`). 4. **Schema changes tracked.** DDL operations are recorded as changes to `sqlite_schema` table. 5. **Binary record format.** Before/after/updates columns use SQLite's MakeRecord format (same as B-tree payload). 6. **Transaction-aware.** CDC writes happen within the same transaction as the DML, so rollback naturally discards CDC entries. 7. **Version tracking.** CDC schema version is recorded in `turso_cdc_version` table and carried in `CaptureDataChangesInfo.version` for future schema evolution. 8. **Atomic PRAGMA.** Connection CDC state is deferred via `pending_cdc_info` in `ProgramState` and applied only at Halt. If the PRAGMA's disk writes fail and the transaction rolls back, the connection state stays unchanged. 9. **Per-statement COMMIT (v2).** COMMIT records are emitted once per statement (not per row), using `emit_cdc_autocommit_commit()` which checks `is_autocommit()` at runtime. In explicit transactions, only the final `COMMIT` emits a COMMIT CDC record. 10. **Backward-compatible version detection.** Pre-existing v1 CDC tables (without `turso_cdc_version`) are detected by checking table existence before creation. Existing tables get `CdcVersion::V1` inserted into the version table. 11. **Typed version enum.** `CdcVersion` enum with `#[repr(u8)]` and `Ord`/`PartialOrd` enables feature gating via integer comparison (`has_commit_record()` = `self >= V2`). `Display`/`FromStr` handles database round-trip. 12. **CDC and MVCC mutual exclusion.** Enabling CDC when MVCC is active (or vice versa) returns an error. Checked at PRAGMA set time and journal mode switch time. ================================================ FILE: .claude/skills/code-quality/SKILL.md ================================================ --- name: code-quality description: General Correctness rules, Rust patterns, comments, avoiding over-engineering. When writing code always take these into account --- # Code Quality Guide ## Core Principle Production database. Correctness paramount. Crash > corrupt. ## Correctness Rules 1. **No workarounds or quick hacks.** Handle all errors, check invariants 2. **Assert often.** Never silently fail or swallow edge cases 3. **Crash on invalid state** if it risks data integrity. Don't continue in undefined state 4. **Consider edge cases.** On long enough timeline, all possible bugs will happen ## Rust Patterns - Make illegal states unrepresentable - Exhaustive pattern matching - Prefer enums over strings/sentinels - Minimize heap allocations - Write CPU-friendly code (microsecond = long time) ## If-Statements Wrong: ```rust if condition { // happy path } else { // "shouldn't happen" - silently ignored } ``` Right: ```rust // If only one branch should ever be hit: assert!(condition, "invariant violated: ..."); // OR return Err(LimboError::InternalError("unexpected state".into())); // OR unreachable!("impossible state: ..."); ``` Use if-statements only when both branches are expected paths. ## Comments **Do:** - Document WHY, not what - Document functions, structs, enums, variants - Focus on why something is necessary **Don't:** - Comments that repeat code - References to AI conversations ("This test should trigger the bug") - Temporal markers ("added", "existing code", "Phase 1") ## Avoid Over-Engineering - Only changes directly requested or clearly necessary - Don't add features beyond what's asked - Don't add docstrings/comments to unchanged code - Don't add error handling for impossible scenarios - Don't create abstractions for one-time operations - Three similar lines > premature abstraction ## Index Mutations When code involves index inserts, deletes, or conflict resolution, double-check the ordering against SQLite. Wrong ordering causes index inconsistencies. and easy to miss. ## Ensure understanding of IO model - [Async IO model](../async-io-model/SKILL.md) ## Cleanup - Delete unused code completely - No backwards-compat hacks (renamed `_vars`, re-exports, `// removed` comments) ================================================ FILE: .claude/skills/debugging/SKILL.md ================================================ --- name: debugging description: How to debug tursodb using Bytecode comparison, logging, ThreadSanitizer, deterministic simulation, and corruption analysis tools --- # Debugging Guide ## Bytecode Comparison Flow Turso aims for SQLite compatibility. When behavior differs: ``` 1. EXPLAIN query in sqlite3 2. EXPLAIN query in tursodb 3. Compare bytecode ├─ Different → bug in code generation └─ Same but results differ → bug in VM or storage layer ``` ### Example ```bash # SQLite sqlite3 :memory: "EXPLAIN SELECT 1 + 1;" # Turso cargo run --bin tursodb :memory: "EXPLAIN SELECT 1 + 1;" ``` ## Manual Query Inspection ```bash cargo run --bin tursodb :memory: 'SELECT * FROM foo;' cargo run --bin tursodb :memory: 'EXPLAIN SELECT * FROM foo;' ``` ## Logging ```bash # Trace core during tests RUST_LOG=none,turso_core=trace make test # Output goes to testing/test.log # Warning: can be megabytes per test run ``` ## Threading Issues Use stress tests with ThreadSanitizer: ```bash rustup toolchain install nightly rustup override set nightly cargo run -Zbuild-std --target x86_64-unknown-linux-gnu \ -p turso_stress -- --vfs syscall --nr-threads 4 --nr-iterations 1000 ``` ## Deterministic Simulation Reproduce bugs with seed. Note: simulator uses legacy "limbo" naming. ```bash # Simulator RUST_LOG=limbo_sim=debug cargo run --bin limbo_sim -- -s # Whopper (concurrent DST) SEED=1234 ./testing/concurrent-simulator/bin/run ``` ## Architecture Reference - **Parser** → AST from SQL strings - **Code generator** → bytecode from AST - **Virtual machine** → executes SQLite-compatible bytecode - **Storage layer** → B-tree operations, paging ## Corruption Debugging For WAL corruption and database integrity issues, use the corruption debug tools in [scripts](./scripts). See [references/CORRUPTION-TOOLS.md](./references/CORRUPTION-TOOLS.md) for detailed usage. ================================================ FILE: .claude/skills/differential-fuzzer/SKILL.md ================================================ --- name: differential-fuzzer description: Information about the differential fuzzer tool, how to run it and use it catch bugs in Turso. Always load this skill when running this tool --- # Differential Fuzzer Always load [Debugging skill for reference](../debugging/) The differential fuzzer compares Turso results against SQLite for generated SQL statements to find correctness bugs. ## Location `testing/differential-oracle/fuzzer/` ## Running the Fuzzer ### Single Run ```bash # Basic run (100 statements, random seed) cargo run --bin differential_fuzzer # With specific seed for reproducibility cargo run --bin differential_fuzzer -- --seed 12345 # More statements with verbose output cargo run --bin differential_fuzzer -- -n 1000 --verbose # Keep database files after run (for debugging) cargo run --bin differential_fuzzer -- --seed 12345 --keep-files # All options cargo run --bin differential_fuzzer -- \ --seed # Deterministic seed -n # Number of statements (default: 100) -t # Number of tables (default: 2) -c # Columns per table (default: 5) --verbose # Print each SQL statement --keep-files # Persist .db files to disk ``` ### Continuous Fuzzing (Loop Mode) ```bash # Run forever with random seeds cargo run --bin differential_fuzzer -- loop # Run 50 iterations cargo run --bin differential_fuzzer -- loop 50 ``` ### Docker Runner (CI/Production) ```bash # Build and run from repo root docker build -f testing/differential-oracle/fuzzer/docker-runner/Dockerfile -t fuzzer . docker run -e GITHUB_TOKEN=xxx -e SLACK_WEBHOOK_URL=xxx fuzzer ``` Environment variables for docker-runner: - `TIME_LIMIT_MINUTES` - Total runtime (default: 1440 = 24h) - `PER_RUN_TIMEOUT_SECONDS` - Per-run timeout (default: 1200 = 20min) - `NUM_STATEMENTS` - Statements per run (default: 1000) - `LOG_TO_STDOUT` - Print fuzzer output (default: false) - `GITHUB_TOKEN` - For auto-filing issues - `SLACK_WEBHOOK_URL` - For notifications ## Output Files All output goes to `simulator-output/` directory: | File | Description | |------|-------------| | `test.sql` | All executed SQL statements. Failed statements prefixed with `-- FAILED:`, errors with `-- ERROR:` | | `schema.json` | Database schema at end of run (or at failure) | | `test.db` | Turso database file (only with `--keep-files`) | | `test-sqlite.db` | SQLite database file (only with `--keep-files`) | ## Reproducing Errors Always follow these steps 1. **Find the seed** in the error output: ``` INFO: Starting differential_fuzzer with config: SimConfig { seed: 12345, ... } ``` 2. **Re-run with that seed**: ```bash cargo run --bin differential_fuzzer -- --seed 12345 --verbose --keep-files ``` 3. **Check output files**: - `simulator-output/test.sql` - Find the failing statement (look for `-- FAILED:`) - `simulator-output/schema.json` - Check table structure at failure time 4. **Create a minimal reproducer** - Create reproducer in `.sqltest` or in `.rs` always load [Debugging skill for reference](../debugging/) 5. **Compare behavior manually**: If needed try to compare the behaviour and produce a report in the end. Always write to a tmp file first with Edit tool to test the sql and then pass it to the binaries. ```bash # Run failing SQL against SQLite sqlite3 :memory: < simulator-output/test.sql # Run against tursodb CLI tursodb :memory: < simulator-output/test.sql ``` ## Understanding Failures ### Oracle Failure Types 1. **Row set mismatch** - Turso returned different rows than SQLite 2. **Turso errored but SQLite succeeded** - Turso rejected valid SQL 3. **SQLite errored but Turso succeeded** - Turso accepted invalid SQL 4. **Schema mismatch** - Tables/columns differ after DDL ### Warning (non-fatal) - **Unordered LIMIT mismatch** - LIMIT without ORDER BY may return different valid rows ## Key Source Files | File | Purpose | |------|---------| | `main.rs` | CLI parsing, entry point | | `runner.rs` | Main simulation loop, executes statements on both DBs | | `oracle.rs` | Compares Turso vs SQLite results | | `schema.rs` | Introspects schema from both databases | | `memory/` | In-memory IO for deterministic simulation | ## Tracing Set `RUST_LOG` for more detailed output: ```bash RUST_LOG=debug cargo run --bin differential_fuzzer -- --seed 12345 ``` ================================================ FILE: .claude/skills/index-knowledge/SKILL.md ================================================ --- name: index-knowledge description: Generate hierarchical AGENTS.md knowledge base for a codebase. Creates root + complexity-scored subdirectory documentation. --- # index-knowledge Generate hierarchical AGENTS.md files. Root + complexity-scored subdirectories. ## Usage ``` --create-new # Read existing → remove all → regenerate from scratch --max-depth=2 # Limit directory depth (default: 5) ``` Default: Update mode (modify existing + create new where warranted) --- ## Workflow (High-Level) 1. **Discovery + Analysis** (concurrent) - Launch parallel explore agents (multiple Task calls in one message) - Main session: bash structure + LSP codemap + read existing AGENTS.md 2. **Score & Decide** - Determine AGENTS.md locations from merged findings 3. **Generate** - Root first, then subdirs in parallel 4. **Review** - Deduplicate, trim, validate **TodoWrite ALL phases. Mark in_progress → completed in real-time.** ``` TodoWrite([ { id: "discovery", content: "Fire explore agents + LSP codemap + read existing", status: "pending", priority: "high" }, { id: "scoring", content: "Score directories, determine locations", status: "pending", priority: "high" }, { id: "generate", content: "Generate AGENTS.md files (root + subdirs)", status: "pending", priority: "high" }, { id: "review", content: "Deduplicate, validate, trim", status: "pending", priority: "medium" } ]) ``` --- ## Phase 1: Discovery + Analysis (Concurrent) **Mark "discovery" as in_progress.** ### Launch Parallel Explore Agents Multiple Task calls in a single message execute in parallel. Results return directly. ``` // All Task calls in ONE message = parallel execution Task( description="project structure", subagent_type="explore", prompt="Project structure: PREDICT standard patterns for detected language → REPORT deviations only" ) Task( description="entry points", subagent_type="explore", prompt="Entry points: FIND main files → REPORT non-standard organization" ) Task( description="conventions", subagent_type="explore", prompt="Conventions: FIND config files (.eslintrc, pyproject.toml, .editorconfig) → REPORT project-specific rules" ) Task( description="anti-patterns", subagent_type="explore", prompt="Anti-patterns: FIND 'DO NOT', 'NEVER', 'ALWAYS', 'DEPRECATED' comments → LIST forbidden patterns" ) Task( description="build/ci", subagent_type="explore", prompt="Build/CI: FIND .github/workflows, Makefile → REPORT non-standard patterns" ) Task( description="test patterns", subagent_type="explore", prompt="Test patterns: FIND test configs, test structure → REPORT unique conventions" ) ``` **DYNAMIC AGENT SPAWNING**: After bash analysis, spawn ADDITIONAL explore agents based on project scale: | Factor | Threshold | Additional Agents | |--------|-----------|-------------------| | **Total files** | >100 | +1 per 100 files | | **Total lines** | >10k | +1 per 10k lines | | **Directory depth** | ≥4 | +2 for deep exploration | | **Large files (>500 lines)** | >10 files | +1 for complexity hotspots | | **Monorepo** | detected | +1 per package/workspace | | **Multiple languages** | >1 | +1 per language | ```bash # Measure project scale first total_files=$(find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' | wc -l) total_lines=$(find . -type f \( -name "*.ts" -o -name "*.py" -o -name "*.go" \) -not -path '*/node_modules/*' -exec wc -l {} + 2>/dev/null | tail -1 | awk '{print $1}') large_files=$(find . -type f \( -name "*.ts" -o -name "*.py" \) -not -path '*/node_modules/*' -exec wc -l {} + 2>/dev/null | awk '$1 > 500 {count++} END {print count+0}') max_depth=$(find . -type d -not -path '*/node_modules/*' -not -path '*/.git/*' | awk -F/ '{print NF}' | sort -rn | head -1) ``` Example spawning (all in ONE message for parallel execution): ``` // 500 files, 50k lines, depth 6, 15 large files → spawn additional agents Task( description="large files", subagent_type="explore", prompt="Large file analysis: FIND files >500 lines, REPORT complexity hotspots" ) Task( description="deep modules", subagent_type="explore", prompt="Deep modules at depth 4+: FIND hidden patterns, internal conventions" ) Task( description="cross-cutting", subagent_type="explore", prompt="Cross-cutting concerns: FIND shared utilities across directories" ) // ... more based on calculation ``` ### Main Session: Concurrent Analysis **While Task agents execute**, main session does: #### 1. Bash Structural Analysis ```bash # Directory depth + file counts find . -type d -not -path '*/\.*' -not -path '*/node_modules/*' -not -path '*/venv/*' -not -path '*/dist/*' -not -path '*/build/*' | awk -F/ '{print NF-1}' | sort -n | uniq -c # Files per directory (top 30) find . -type f -not -path '*/\.*' -not -path '*/node_modules/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -30 # Code concentration by extension find . -type f \( -name "*.py" -o -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.go" -o -name "*.rs" \) -not -path '*/node_modules/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -20 # Existing AGENTS.md / CLAUDE.md find . -type f \( -name "AGENTS.md" -o -name "CLAUDE.md" \) -not -path '*/node_modules/*' 2>/dev/null ``` #### 2. Read Existing AGENTS.md ``` For each existing file found: Read(filePath=file) Extract: key insights, conventions, anti-patterns Store in EXISTING_AGENTS map ``` If `--create-new`: Read all existing first (preserve context) → then delete all → regenerate. #### 3. LSP Codemap (if available) ``` lsp_servers() # Check availability # Entry points (parallel) lsp_document_symbols(filePath="src/index.ts") lsp_document_symbols(filePath="main.py") # Key symbols (parallel) lsp_workspace_symbols(filePath=".", query="class") lsp_workspace_symbols(filePath=".", query="interface") lsp_workspace_symbols(filePath=".", query="function") # Centrality for top exports lsp_find_references(filePath="...", line=X, character=Y) ``` **LSP Fallback**: If unavailable, rely on explore agents + AST-grep. **Merge: bash + LSP + existing + Task agent results. Mark "discovery" as completed.** --- ## Phase 2: Scoring & Location Decision **Mark "scoring" as in_progress.** ### Scoring Matrix | Factor | Weight | High Threshold | Source | |--------|--------|----------------|--------| | File count | 3x | >20 | bash | | Subdir count | 2x | >5 | bash | | Code ratio | 2x | >70% | bash | | Unique patterns | 1x | Has own config | explore | | Module boundary | 2x | Has index.ts/__init__.py | bash | | Symbol density | 2x | >30 symbols | LSP | | Export count | 2x | >10 exports | LSP | | Reference centrality | 3x | >20 refs | LSP | ### Decision Rules | Score | Action | |-------|--------| | **Root (.)** | ALWAYS create | | **>15** | Create AGENTS.md | | **8-15** | Create if distinct domain | | **<8** | Skip (parent covers) | ### Output ``` AGENTS_LOCATIONS = [ { path: ".", type: "root" }, { path: "src/hooks", score: 18, reason: "high complexity" }, { path: "src/api", score: 12, reason: "distinct domain" } ] ``` **Mark "scoring" as completed.** --- ## Phase 3: Generate AGENTS.md **Mark "generate" as in_progress.** ### Root AGENTS.md (Full Treatment) ```markdown # PROJECT KNOWLEDGE BASE **Generated:** {TIMESTAMP} **Commit:** {SHORT_SHA} **Branch:** {BRANCH} ## OVERVIEW {1-2 sentences: what + core stack} ## STRUCTURE \`\`\` {root}/ ├── {dir}/ # {non-obvious purpose only} └── {entry} \`\`\` ## WHERE TO LOOK | Task | Location | Notes | |------|----------|-------| ## CODE MAP {From LSP - skip if unavailable or project <10 files} | Symbol | Type | Location | Refs | Role | ## CONVENTIONS {ONLY deviations from standard} ## ANTI-PATTERNS (THIS PROJECT) {Explicitly forbidden here} ## UNIQUE STYLES {Project-specific} ## COMMANDS \`\`\`bash {dev/test/build} \`\`\` ## NOTES {Gotchas} ``` **Quality gates**: 50-150 lines, no generic advice, no obvious info. ### Subdirectory AGENTS.md (Parallel) Launch general agents for each location in ONE message (parallel execution): ``` // All in single message = parallel Task( description="AGENTS.md for src/hooks", subagent_type="general", prompt="Generate AGENTS.md for: src/hooks - Reason: high complexity - 30-80 lines max - NEVER repeat parent content - Sections: OVERVIEW (1 line), STRUCTURE (if >5 subdirs), WHERE TO LOOK, CONVENTIONS (if different), ANTI-PATTERNS - Write directly to src/hooks/AGENTS.md" ) Task( description="AGENTS.md for src/api", subagent_type="general", prompt="Generate AGENTS.md for: src/api - Reason: distinct domain - 30-80 lines max - NEVER repeat parent content - Sections: OVERVIEW (1 line), STRUCTURE (if >5 subdirs), WHERE TO LOOK, CONVENTIONS (if different), ANTI-PATTERNS - Write directly to src/api/AGENTS.md" ) // ... one Task per AGENTS_LOCATIONS entry ``` **Results return directly. Mark "generate" as completed.** --- ## Phase 4: Review & Deduplicate **Mark "review" as in_progress.** For each generated file: - Remove generic advice - Remove parent duplicates - Trim to size limits - Verify telegraphic style **Mark "review" as completed.** --- ## Final Report ``` === index-knowledge Complete === Mode: {update | create-new} Files: ✓ ./AGENTS.md (root, {N} lines) ✓ ./src/hooks/AGENTS.md ({N} lines) Dirs Analyzed: {N} AGENTS.md Created: {N} AGENTS.md Updated: {N} Hierarchy: ./AGENTS.md └── src/hooks/AGENTS.md ``` --- ## Anti-Patterns - **Static agent count**: MUST vary agents based on project size/depth - **Sequential execution**: MUST parallel (multiple Task calls in one message) - **Ignoring existing**: ALWAYS read existing first, even with --create-new - **Over-documenting**: Not every dir needs AGENTS.md - **Redundancy**: Child never repeats parent - **Generic content**: Remove anything that applies to ALL projects - **Verbose style**: Telegraphic or die ================================================ FILE: .claude/skills/mvcc/SKILL.md ================================================ --- name: mvcc description: Overview of Experimental MVCC feature - snapshot isolation, versioning, limitations --- # MVCC Guide (Experimental) Multi-Version Concurrency Control. **Work in progress, not production-ready.** **CRITICAL**: Ignore MVCC when debugging unless the bug is MVCC-specific. ## Enabling MVCC ```sql PRAGMA journal_mode = 'mvcc'; ``` Runtime configuration, not a compile-time feature flag. Per-database setting. ## How It Works Standard WAL: single version per page, readers see snapshot at read mark time. MVCC: multiple row versions, snapshot isolation. Each transaction sees consistent snapshot at begin time. ### Key Differences from WAL | Aspect | WAL | MVCC | |--------|-----|------| | Write granularity | Every commit writes full pages | Affected rows only | Readers/Writers | Don't block each other | Don't block each other | | Persistence | `.db-wal` | `.db-log` (logical log) | | Isolation | Snapshot (page-level) | Snapshot (row-level) | ### Versioning Each row version tracks: - `begin` - timestamp when visible - `end` - timestamp when deleted/replaced - `btree_resident` - existed before MVCC enabled ## Architecture ``` Database └─ mv_store: MvStore ├─ rows: SkipMap> ├─ txs: SkipMap ├─ Storage (.db-log file) └─ CheckpointStateMachine ``` **Per-connection**: `mv_tx` tracks current MVCC transaction. **Shared**: `MvStore` with lock-free `crossbeam_skiplist` structures. ## Key Files - `core/mvcc/mod.rs` - Module overview - `core/mvcc/database/mod.rs` - Main implementation (~3000 lines) - `core/mvcc/cursor.rs` - Merged MVCC + B-tree cursor - `core/mvcc/persistent_storage/logical_log.rs` - Disk format - `core/mvcc/database/checkpoint_state_machine.rs` - Checkpoint logic ## Checkpointing Flushes row versions to B-tree periodically. ```sql PRAGMA mvcc_checkpoint_threshold = ; ``` Process: acquire lock → begin pager txn → write rows → commit → truncate log → fsync → release. ## Current Limitations **Not implemented:** - Garbage collection (old versions accumulate) - Recovery from logical log on restart **Known issues:** - Checkpoint blocks other transactions, even reads! - No spilling to disk; memory use concerns ## Testing ```bash # Run MVCC-specific tests cargo test mvcc # TCL tests with MVCC make test-mvcc ``` Use `#[turso_macros::test(mvcc)]` attribute for MVCC-enabled tests. ```rust #[turso_macros::test(mvcc)] fn test_something() { // runs with MVCC enabled } ``` ## References - `core/mvcc/mod.rs` documents data anomalies (dirty reads, lost updates, etc.) - Snapshot isolation vs serializability: MVCC provides the former, not the latter ================================================ FILE: .claude/skills/pr-workflow/SKILL.md ================================================ --- name: pr-workflow description: General guidelines for Commits, formatting, CI, dependencies, security --- # PR Workflow Guide ## Commit Practices - **Atomic commits.** Small, focused, single purpose - **Don't mix:** logic + formatting, logic + refactoring - **Good message** = easy to write short description of intent Learn `git rebase -i` for clean history. ## PR Guidelines - Keep PRs focused and small - Run relevant tests before submitting - Each commit tells part of the story ## CI Environment Notes If running as GitHub Action: - Max-turns limit in `.github/workflows/claude.yml` - OK to commit WIP state and push - OK to open WIP PR and continue in another action - Don't spiral into rabbit holes. Stay focused on key task ## Security Never commit: - `.env` files - Credentials - Secrets ## Third-Party Dependencies When adding: 1. Add license file under `licenses/` 2. Update `NOTICE.md` with dependency info ## External APIs/Tools - Never guess API params or CLI args - Search official docs first - Ask for clarification if ambiguous ================================================ FILE: .claude/skills/storage-format/SKILL.md ================================================ --- name: storage-format description: SQLite file format, B-trees, pages, cells, overflow, freelist that is used in tursodb --- # Storage Format Guide ## Database File Structure ``` ┌─────────────────────────────┐ │ Page 1: Header + Schema │ ← First 100 bytes = DB header ├─────────────────────────────┤ │ Page 2..N: B-tree pages │ ← Tables and indexes │ Overflow pages │ │ Freelist pages │ └─────────────────────────────┘ ``` Page size: power of 2, 512-65536 bytes. Default 4096. ## Database Header (First 100 Bytes) | Offset | Size | Field | |--------|------|-------| | 0 | 16 | Magic: `"SQLite format 3\0"` | | 16 | 2 | Page size (big-endian) | | 18 | 1 | Write format version (1=rollback, 2=WAL) | | 19 | 1 | Read format version | | 24 | 4 | Change counter | | 28 | 4 | Database size in pages | | 32 | 4 | First freelist trunk page | | 36 | 4 | Total freelist pages | | 40 | 4 | Schema cookie | | 56 | 4 | Text encoding (1=UTF8, 2=UTF16LE, 3=UTF16BE) | All multi-byte integers: **big-endian**. ## Page Types | Flag | Type | Purpose | |------|------|---------| | 0x02 | Interior index | Index B-tree internal node | | 0x05 | Interior table | Table B-tree internal node | | 0x0a | Leaf index | Index B-tree leaf | | 0x0d | Leaf table | Table B-tree leaf | | - | Overflow | Payload exceeding cell capacity | | - | Freelist | Unused pages (trunk or leaf) | ## B-tree Structure Two B-tree types: - **Table B-tree**: 64-bit rowid keys, stores row data - **Index B-tree**: Arbitrary keys (index columns + rowid) ``` Interior page: [ptr0] key1 [ptr1] key2 [ptr2] ... │ │ │ ▼ ▼ ▼ child child child pages pages pages Leaf page: key1:data key2:data key3:data ... ``` Page 1 always root of `sqlite_schema` table. ## Cell Format ### Table Leaf Cell ``` [payload_size: varint] [rowid: varint] [payload] [overflow_ptr: u32?] ``` ### Table Interior Cell ``` [left_child_page: u32] [rowid: varint] ``` ### Index Cells Similar but key is arbitrary (columns + rowid), not just rowid. ## Record Format (Payload) ``` [header_size: varint] [type1: varint] [type2: varint] ... [data1] [data2] ... ``` Serial types: | Type | Meaning | |------|---------| | 0 | NULL | | 1-4 | 1/2/3/4 byte signed int | | 5 | 6 byte signed int | | 6 | 8 byte signed int | | 7 | IEEE 754 float | | 8 | Integer 0 | | 9 | Integer 1 | | ≥12 even | BLOB, length=(N-12)/2 | | ≥13 odd | Text, length=(N-13)/2 | ## Overflow Pages When payload exceeds threshold, excess stored in overflow chain: ``` [next_page: u32] [data...] ``` Last page has next_page=0. ## Freelist Linked list of trunk pages, each containing leaf page numbers: ``` Trunk: [next_trunk: u32] [leaf_count: u32] [leaf_pages: u32...] ``` ## Turso Implementation Key files: - `core/storage/sqlite3_ondisk.rs` - On-disk format, `PageType` enum - `core/storage/btree.rs` - B-tree operations (large file) - `core/storage/pager.rs` - Page management - `core/storage/buffer_pool.rs` - Page caching ## Debugging Storage ```bash # Integrity check cargo run --bin tursodb test.db "PRAGMA integrity_check;" # Page count cargo run --bin tursodb test.db "PRAGMA page_count;" # Freelist info cargo run --bin tursodb test.db "PRAGMA freelist_count;" ``` ## References - [SQLite File Format](https://sqlite.org/fileformat.html) - [SQLite B-Tree Module](https://sqlite.org/btreemodule.html) - [SQLite Internals: Pages & B-trees](https://fly.io/blog/sqlite-internals-btree/) ================================================ FILE: .claude/skills/testing/SKILL.md ================================================ --- name: testing description: How to write tests, when to use each type of test, and how to run them. Contains information about conversion of `.test` to `.sqltest`, and how to write `.sqltest` and rust tests --- # Testing Guide ## Test Types & When to Use | Type | Location | Use Case | |------|----------|----------| | `.sqltest` | `testing/sqltests/tests/` | SQL compatibility. **Preferred for new tests** | | TCL `.test` | `testing/` | Legacy SQL compat (being phased out) | | Rust integration | `tests/integration/` | Regression tests, complex scenarios | | Fuzz | `tests/fuzz/` | Complex features, edge case discovery | **Note:** TCL tests are being phased out in favor of testing/sqltests. The `.sqltest` format allows the same test cases to run against multiple backends (CLI, Rust bindings, etc.). ## Running Tests ```bash # Main test suite (TCL compat, sqlite3 compat, Python wrappers) make test # Single TCL test make test-single TEST=select.test # SQL test runner make -C testing/sqltests run-cli # OR cargo run -p test-runner -- run # Rust unit/integration tests (full workspace) cargo test ``` ## Writing Tests ### .sqltest (Preferred) ``` @database :default: test example-addition { SELECT 1 + 1; } expect { 2 } test example-multiple-rows { SELECT id, name FROM users WHERE id < 3; } expect { 1|alice 2|bob } ``` Location: `testing/sqltests/tests/*.sqltest` You must start converting TCL tests with the `convert` command from the test runner (e.g `cargo run -- convert -o `). It is not always accurate, but it will convert most of the tests. If some conversion emits a warning you will have to write by hand whatever is missing from it (e.g unroll a for each loop by hand). Then you need to verify the tests work by running them with `make -C testing/sqltests run-rust`, and adjust their output if something was wrong with the conversion. Also, we use harcoded databases in TCL, but with `.sqltest` we generate the database with a different seed, so you will probably need to change the expected test result to match the new database query output. Avoid changing the SQL statements from the test, just change the expected result ### TCL ```tcl do_execsql_test_on_specific_db {:memory:} test-name { SELECT 1 + 1; } {2} ``` Location: `testing/*.test` ### Rust Integration ```rust // tests/integration/test_foo.rs #[test] fn test_something() { let conn = Connection::open_in_memory().unwrap(); // ... } ``` ## Key Rules - Every functional change needs a test - Test must fail without change, pass with it - Prefer in-memory DBs: `:memory:` (sqltest) or `{:memory:}` (TCL) - Don't invent new test formats. Follow existing patterns - Write tests first when possible ## Test Database Schema `testing/system/testing.db` has `users` and `products` tables. See [docs/testing.md](../../../docs/testing.md) for schema. ## Logging During Tests ```bash RUST_LOG=none,turso_core=trace make test ``` Output: `testing/system/test.log`. Warning: very verbose. ================================================ FILE: .claude/skills/transaction-correctness/SKILL.md ================================================ --- name: transaction-correctness description: How WAL mechanics, checkpointing, concurrency rules, recovery work in tursodb --- # Transaction Correctness Guide Turso uses WAL (Write-Ahead Logging) mode exclusively. Files: `.db`, `.db-wal` (no `.db-shm` - Turso uses in-memory WAL index) ## WAL Mechanics ### Write Path 1. Writer appends frames (page data) to WAL file (sequential I/O) 2. COMMIT = frame with non-zero db_size in header (marks transaction end) 3. Original DB unchanged until checkpoint ### Read Path 1. Reader acquires read mark (mxFrame = last valid commit frame) 2. For each page: check WAL up to mxFrame, fall back to main DB 3. Reader sees consistent snapshot at its read mark ### Checkpointing Transfers WAL content back to main DB. ``` WAL grows → checkpoint triggered (default: 1000 pages) → pages copied to DB → WAL reused ``` Checkpoint types: - **PASSIVE**: Non-blocking, stops at pages needed by active readers - **FULL**: Waits for readers, checkpoints everything - **RESTART**: Like FULL, also resets WAL to beginning - **TRUNCATE**: Like RESTART, also truncates WAL file to zero length ### WAL-Index SQLite uses a shared memory file (`-shm`) for WAL index. **Turso does not** - it uses in-memory data structures (`frame_cache` hashmap, atomic read marks) since multi-process access is not supported. ## Concurrency Rules - One writer at a time - Readers don't block writer, writer doesn't block readers - Checkpoint must stop at pages needed by active readers ## Recovery On crash: 1. First connection acquires exclusive lock 2. Replays valid commits from WAL 3. Releases lock, normal operation resumes ## Turso Implementation Key files: - [WAL implementation](../../../core/storage/wal.rs) - WAL implementation - [Page management, transactions](../../../core/storage/pager.rs) ### Connection-Private vs Shared **Per-Connection (private):** - `Pager` - page cache, dirty pages, savepoints, commit state - `WalFile` - connection's snapshot view: - `max_frame` / `min_frame` - frame range for this connection's snapshot - `max_frame_read_lock_index` - which read lock slot this connection holds - `last_checksum` - rolling checksum state **Shared across connections:** - `WalFileShared` - global WAL state: - `frame_cache` - page-to-frame index (replaces `.shm` file) - `max_frame` / `nbackfills` - global WAL progress - `read_locks[5]` - read mark slots (TursoRwLock with embedded frame values) - `write_lock` - exclusive writer lock - `checkpoint_lock` - checkpoint serialization - `file` - WAL file handle - `DatabaseStorage` - main `.db` file - `BufferPool` - shared memory allocation ## Correctness Invariants 1. **Durability**: COMMIT record must be fsynced before returning success 2. **Atomicity**: Partial transactions never visible to readers 3. **Isolation**: Each reader sees consistent snapshot 4. **No lost updates**: Checkpoint can't overwrite uncommitted changes ## References - [SQLite WAL](https://sqlite.org/wal.html) - [WAL File Format](https://sqlite.org/walformat.html) ================================================ FILE: .config/nextest.toml ================================================ [profile.loom] # Filter to only run tests with "loom_" in the name default-filter = 'test(/loom_/)' # Loom tests are slow - model checking explores all interleavings slow-timeout = { period = "120s", terminate-after = 2 } fail-fast = false failure-output = "final" test-threads = 1 [profile.shuttle] # Filter to only run tests containing "shuttle_" in the name default-filter = 'test(/shuttle_/)' slow-timeout = { period = "60s", terminate-after = 2 } fail-fast = false failure-output = "final" test-threads = 1 ================================================ FILE: .devcontainer/Dockerfile ================================================ FROM node:20 ARG TZ ARG CLAUDE_CODE_VERSION ARG GIT_DELTA_VERSION ARG ZSH_IN_DOCKER_VERSION ARG GO_VERSION ARG RUST_VERSION ARG PYTHON_VERSION ENV TZ="$TZ" # Install build dependencies needed for further installations RUN apt-get update && apt-get install -y --no-install-recommends \ wget \ curl \ ca-certificates \ build-essential \ pkg-config \ gnupg2 \ python${PYTHON_VERSION}-dev \ sudo \ && apt-get clean && rm -rf /var/lib/apt/lists/* # Ensure default node user has access to /usr/local/share RUN mkdir -p /usr/local/share/npm-global && \ chown -R node:node /usr/local/share ARG USERNAME=node # Persist bash history. RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \ && mkdir /commandhistory \ && touch /commandhistory/.bash_history \ && chown -R $USERNAME /commandhistory # Set `DEVCONTAINER` environment variable to help with orientation ENV DEVCONTAINER=true # Create workspace and config directories and set permissions RUN mkdir -p /workspace /home/node/.claude && \ chown -R node:node /workspace /home/node/.claude WORKDIR /workspace RUN ARCH=$(dpkg --print-architecture) && \ wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ sudo dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" # Install Go ENV GOROOT=/usr/local/go ENV GOPATH=/home/node/go ENV PATH=$PATH:/usr/local/go/bin:/home/node/go/bin RUN set -eux; \ OS="$(uname -s)"; \ ARCH="$(uname -m)"; \ \ case "$OS" in \ Linux) \ GOOS=linux ;; \ Darwin) \ GOOS=darwin ;; \ *) \ echo "Unsupported OS: $OS" && exit 1 ;; \ esac; \ \ case "$ARCH" in \ x86_64|amd64) \ GOARCH=amd64 ;; \ aarch64|arm64) \ GOARCH=arm64 ;; \ *) \ echo "Unsupported arch: $ARCH" && exit 1 ;; \ esac; \ \ curl -fsSL "https://go.dev/dl/go${GO_VERSION}.${GOOS}-${GOARCH}.tar.gz" \ | sudo tar -C /usr/local -xz RUN go install golang.org/x/tools/gopls@latest RUN go install honnef.co/go/tools/cmd/staticcheck@latest RUN chown -R node:node /home/node/go # Set up non-root user USER node # Install Rust ENV RUSTUP_HOME=/home/node/.rustup \ CARGO_HOME=/home/node/.cargo ENV PATH=$PATH:/home/node/.cargo/bin RUN curl https://sh.rustup.rs -sSf | sh -s -- \ -y \ --default-toolchain ${RUST_VERSION} \ --profile minimal \ && rustup component add rustfmt clippy # Install Python ENV UV_HOME=/home/node/.uv ENV PATH=$PATH:/home/node/.cargo/bin:/home/node/.local/bin RUN curl -Ls https://astral.sh/uv/install.sh | sh RUN uv python install ${PYTHON_VERSION} # Check installation RUN rustc --version \ && cargo --version \ && go version \ && uv --version # Install global packages ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global ENV PATH=$PATH:/usr/local/share/npm-global/bin # Set the default shell to zsh rather than sh ENV SHELL=/bin/zsh # Set the default editor and visual ENV EDITOR=vim ENV VISUAL=vim # Switch back to root to install runtime dependencies USER root # Install runtime dependencies for final image RUN apt-get update && apt-get install -y --no-install-recommends \ traceroute \ iputils-ping \ sqlite3 \ less \ git \ procps \ fzf \ zsh \ man-db \ unzip \ gh \ iptables \ ipset \ iproute2 \ dnsutils \ aggregate \ jq \ nano \ vim \ && apt-get clean && rm -rf /var/lib/apt/lists/* # Switch back to node user USER node # Default powerline10k theme RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \ -p git \ -p fzf \ -a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \ -a "source /usr/share/doc/fzf/examples/completion.zsh" \ -a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \ -x # Install Claude RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} ================================================ FILE: .devcontainer/Dockerfile.squid ================================================ FROM ubuntu/squid:latest RUN apt-get update && apt-get install -y netcat-openbsd && rm -rf /var/lib/apt/lists/* ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "tursodb sandbox", "dockerComposeFile": ["docker-compose.yml"], "service": "tursodb", "remoteUser": "node", "workspaceFolder": "/workspace", "waitFor": "postStartCommand", "customizations": { "vscode": { "extensions": [ "anthropic.claude-code", "eamodio.gitlens", // Rust "rust-lang.rust-analyzer", // Go "golang.Go", // Python "ms-python.python", "ms-python.vscode-pylance", // JavaScript / TypeScript "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "ms-vscode.vscode-typescript-next" ], "settings": { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "terminal.integrated.defaultProfile.linux": "zsh", "terminal.integrated.profiles.linux": { "bash": { "path": "bash", "icon": "terminal-bash" }, "zsh": { "path": "zsh" } } } } } } ================================================ FILE: .devcontainer/docker-compose.yml ================================================ version: "3.9" services: proxy: build: context: . dockerfile: Dockerfile.squid networks: [sandbox, egress] volumes: - ./squid.conf:/etc/squid/squid.conf:ro healthcheck: test: ["CMD", "nc", "-z", "127.0.0.1", "3128"] interval: 1s timeout: 5s retries: 10 start_period: 1s tursodb: depends_on: proxy: condition: service_healthy build: context: . dockerfile: Dockerfile args: TZ: ${TZ:-America/Los_Angeles} CLAUDE_CODE_VERSION: "latest" GIT_DELTA_VERSION: "0.18.2" ZSH_IN_DOCKER_VERSION: "1.2.0" GO_VERSION: "1.24.10" RUST_VERSION: "1.88.0" PYTHON_VERSION: "3.11" environment: NODE_OPTIONS: "--max-old-space-size=4096" CLAUDE_CONFIG_DIR: "/home/node/.claude" POWERLEVEL9K_DISABLE_GITSTATUS: "true" HTTP_PROXY: "http://proxy:3128" HTTPS_PROXY: "http://proxy:3128" ALL_PROXY: "http://proxy:3128" NO_PROXY: "localhost,127.0.0.1" networks: [sandbox] volumes: - ..:/workspace:delegated - claude-code-bashhistory:/commandhistory - claude-code-config:/home/node/.claude cap_add: - NET_ADMIN - NET_RAW init: true tty: true volumes: claude-code-bashhistory: claude-code-config: networks: sandbox: internal: true egress: driver: bridge ================================================ FILE: .devcontainer/init-firewall.sh ================================================ #!/bin/bash set -euo pipefail # Exit on error, undefined vars, and pipeline failures IFS=$'\n\t' # Stricter word splitting # 1. Extract Docker DNS info BEFORE any flushing DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true) # Flush existing rules and delete existing ipsets iptables -F iptables -X iptables -t nat -F iptables -t nat -X iptables -t mangle -F iptables -t mangle -X ipset destroy allowed-domains 2>/dev/null || true # 2. Selectively restore ONLY internal Docker DNS resolution if [ -n "$DOCKER_DNS_RULES" ]; then echo "Restoring Docker DNS rules..." iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat else echo "No Docker DNS rules to restore" fi # First allow DNS and localhost before any restrictions # Allow outbound DNS iptables -A OUTPUT -p udp --dport 53 -j ACCEPT # Allow inbound DNS responses iptables -A INPUT -p udp --sport 53 -j ACCEPT # Allow outbound SSH iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT # Allow inbound SSH responses iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT # Allow localhost iptables -A INPUT -i lo -j ACCEPT iptables -A OUTPUT -o lo -j ACCEPT # Create ipset with CIDR support ipset create allowed-domains hash:net # Fetch GitHub meta information and aggregate + add their IP ranges echo "Fetching GitHub IP ranges..." gh_ranges=$(curl -s https://api.github.com/meta) if [ -z "$gh_ranges" ]; then echo "ERROR: Failed to fetch GitHub IP ranges" exit 1 fi if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then echo "ERROR: GitHub API response missing required fields" exit 1 fi echo "Processing GitHub IPs..." while read -r cidr; do if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then echo "ERROR: Invalid CIDR range from GitHub meta: $cidr" exit 1 fi echo "Adding GitHub range $cidr" ipset add allowed-domains "$cidr" done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q) # Resolve and add other allowed domains for domain in \ "files.pythonhosted.org" \ "static.crates.io" \ "index.crates.io" \ "proxy.golang.org" \ "registry.npmjs.org" \ "api.anthropic.com" \ "sentry.io" \ "statsig.anthropic.com" \ "statsig.com" \ "marketplace.visualstudio.com" \ "vscode.blob.core.windows.net" \ "update.code.visualstudio.com"; do echo "Resolving $domain..." ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}') if [ -z "$ips" ]; then echo "ERROR: Failed to resolve $domain" exit 1 fi while read -r ip; do if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then echo "ERROR: Invalid IP from DNS for $domain: $ip" exit 1 fi echo "Adding $ip for $domain" ipset add allowed-domains "$ip" done < <(echo "$ips") done # Get host IP from default route HOST_IP=$(ip route | grep default | cut -d" " -f3) if [ -z "$HOST_IP" ]; then echo "ERROR: Failed to detect host IP" exit 1 fi HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/") echo "Host network detected as: $HOST_NETWORK" # Set up remaining iptables rules iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT # Set default policies to DROP first iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT DROP # First allow established connections for already approved traffic iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # Then allow only specific outbound traffic to allowed domains iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT # Explicitly REJECT all other outbound traffic for immediate feedback iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited echo "Firewall configuration complete" echo "Verifying firewall rules..." if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then echo "ERROR: Firewall verification failed - was able to reach https://example.com" exit 1 else echo "Firewall verification passed - unable to reach https://example.com as expected" fi # Verify GitHub API access if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then echo "ERROR: Firewall verification failed - unable to reach https://api.github.com" exit 1 else echo "Firewall verification passed - able to reach https://api.github.com as expected" fi ================================================ FILE: .devcontainer/squid.conf ================================================ http_port 3128 # DNS dns_v4_first on # Allowed domains acl allowed dstdomain \ .pythonhosted.org \ .pypi.org \ .crates.io \ .static.rust-lang.org \ .npmjs.org \ .github.com \ .githubusercontent.com \ .go.dev \ .golang.org \ .anthropic.com \ .openai.com \ .sentry.io \ .statsig.com \ .visualstudio.com \ .windows.net # Allow HTTPS CONNECT only to allowed domains acl SSL_ports port 443 acl Safe_ports port 80 443 acl CONNECT method CONNECT http_access deny !Safe_ports http_access deny CONNECT !SSL_ports http_access allow allowed http_access deny all # Logging access_log stdio:/var/log/squid/access.log ================================================ FILE: .dockerignore ================================================ *target .git turso-test-runner assets perf/clickbench perf/tpc-h testing/sqlright/build ================================================ FILE: .github/labeler.yml ================================================ simulator: - changed-files: - any-glob-to-any-file: "testing/simulator/**/*" docs: - changed-files: - any-glob-to-any-file: "docs/**/*" extensionlib: - changed-files: - any-glob-to-any-file: ["extensions/core/**/*", "macros/src/ext/*", "core/ext/*"] macros: - changed-files: - any-glob-to-any-file: ["macros/**/*"] extensions-other: - all: - changed-files: - any-glob-to-any-file: "extensions/**/*" - all-globs-to-all-files: "!extensions/core/**/*" fuzzing: - changed-files: - any-glob-to-any-file: "fuzz/**/*" perf/benchmarks: - changed-files: - any-glob-to-any-file: ["perf/**/*", "core/benches/*"] go-bindings: - changed-files: - any-glob-to-any-file: "bindings/go/**/*" python-bindings: - changed-files: - any-glob-to-any-file: "bindings/python/**/*" js-bindings: - changed-files: - any-glob-to-any-file: "bindings/javascript/**/*" rust-bindings: - changed-files: - any-glob-to-any-file: "bindings/rust/**/*" java-bindings: - changed-files: - any-glob-to-any-file: "bindings/java/**/*" parser: - changed-files: - any-glob-to-any-file: "vendored/sqlite3-parser/*" cli: - changed-files: - any-glob-to-any-file: "cli/**/*" sqlite3: - changed-files: - any-glob-to-any-file: "sqlite3/**/*" core: - changed-files: - any-glob-to-any-file: "core/**/*" optimizer: - changed-files: - any-glob-to-any-file: "core/translate/optimizer/*" translation/planning: - changed-files: - any-glob-to-any-file: "core/translate/*.rs" io: - changed-files: - any-glob-to-any-file: "core/io/*" mvcc: - changed-files: - any-glob-to-any-file: "core/mvcc/**/*" vdbe: - changed-files: - any-glob-to-any-file: "core/vdbe/*" json: - changed-files: - any-glob-to-any-file: "core/json/*" storage: - changed-files: - any-glob-to-any-file: "core/storage/*" vector: - changed-files: - any-glob-to-any-file: "core/vector/*" turso-serverless: - changed-files: - any-glob-to-any-file: 'packages/turso-serverless' turso-sync: - changed-files: - any-glob-to-any-file: 'packages/turso-sync' antithesis: - changed-files: - any-glob-to-any-file: ["testing/antithesis/**/*", "scripts/antithesis/*", "testing/stress/**/*"] ci-actions: - changed-files: - any-glob-to-any-file: ".github/workflows/*.yml" ================================================ FILE: .github/pull_request_template.md ================================================ # NOTICE: ## Description ## Motivation and context ## Description of AI Usage ================================================ FILE: .github/shared/install_sqlite/action.yml ================================================ name: "Install SQLite" description: "Installs sqlite3 CLI for compat tests that invoke it as a subprocess (make test-compat)" runs: using: "composite" steps: - name: Install SQLite env: SQLITE_VERSION: "3510100" YEAR: 2025 run: | curl -o /tmp/sqlite.zip https://sqlite.org/$YEAR/sqlite-tools-linux-x64-$SQLITE_VERSION.zip > /dev/null echo "y" | unzip -j /tmp/sqlite.zip sqlite3 -d /usr/local/bin/ sqlite3 --version shell: bash ================================================ FILE: .github/shared/setup-mold/action.yml ================================================ name: "Setup mold linker" description: "Installs mold linker with retry logic to handle transient GitHub download failures" inputs: mold-version: description: "Version of mold to install" required: false default: "2.40.4" runs: using: "composite" steps: - name: Install mold linker shell: bash run: | MOLD_VERSION="${{ inputs.mold-version }}" ARCH="$(uname -m)" URL="https://github.com/rui314/mold/releases/download/v${MOLD_VERSION}/mold-${MOLD_VERSION}-${ARCH}-linux.tar.gz" TMPFILE="/tmp/mold.tar.gz" MAX_RETRIES=5 for i in $(seq 1 $MAX_RETRIES); do echo "Downloading mold v${MOLD_VERSION} (attempt $i/$MAX_RETRIES)..." if curl -fsSL --retry 3 --retry-delay 5 -o "$TMPFILE" "$URL"; then echo "Download succeeded." break fi if [ "$i" -eq "$MAX_RETRIES" ]; then echo "::warning::Failed to download mold after $MAX_RETRIES attempts. Building without mold." exit 0 fi SLEEP=$((i * 5)) echo "Download failed. Retrying in ${SLEEP}s..." sleep "$SLEEP" done sudo tar -C /usr/local --strip-components=1 --no-overwrite-dir -xzf "$TMPFILE" rm -f "$TMPFILE" echo "mold $(mold --version) installed successfully." ================================================ FILE: .github/shared/setup-sccache/action.yml ================================================ name: "Setup sccache" description: "Installs sccache with graceful fallback if cache service is unavailable" runs: using: "composite" steps: - name: Install sccache uses: mozilla-actions/sccache-action@v0.0.9 - name: Enable sccache shell: bash run: | export SCCACHE_GHA_ENABLED=true if sccache -s >/dev/null 2>&1; then echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" echo "sccache is available and configured" else echo "::warning::sccache cache service is unavailable, building without cache" fi ================================================ FILE: .github/turso-bot.yml ================================================ # Turso Bot configuration # Place this file in your repository at .github/turso-bot.yml # Enable or disable the bot for this repository enabled: true # BetterStack schedule ID to fetch on-call users scheduleId: "335158" ================================================ FILE: .github/workflows/antithesis-schedule.yml ================================================ name: Antithesis nightly on: schedule: - cron: "0 0 * * 0-5" # Sun-Fri: 4 hour run - cron: "0 0 * * 6" # Saturday: 24 hour run jobs: test: uses: ./.github/workflows/antithesis.yml with: duration: ${{ github.event.schedule == '0 0 * * 6' && 1440 || 240 }} test_name: ${{ github.event.schedule == '0 0 * * 6' && 'weekend run' || 'scheduled run' }} secrets: inherit ================================================ FILE: .github/workflows/antithesis.yml ================================================ name: Antithesis experiment on: # Allows the workflow to be triggered manually workflow_dispatch: inputs: test_name: description: "Name to differentiate this run (optional)" required: false default: "" type: string duration: description: "Duration in minutes (min 15, default 4 hours)" required: false default: 240 type: number workflow_call: inputs: test_name: required: true type: string duration: required: true type: number env: ANTITHESIS_DOCKER_HOST: us-central1-docker.pkg.dev ANTITHESIS_DOCKER_REPO: ${{ secrets.ANTITHESIS_DOCKER_REPO }} ANTITHESIS_REGISTRY_KEY: ${{ secrets.ANTITHESIS_REGISTRY_KEY }} jobs: test: runs-on: blacksmith timeout-minutes: 30 steps: - uses: actions/checkout@v3 - name: Publish workload run: bash ./scripts/antithesis/publish-workload.sh - name: Publish config run: bash ./scripts/antithesis/publish-config.sh - name: Run Antithesis Tests uses: antithesishq/antithesis-trigger-action@v0.5 with: notebook_name: limbo tenant: ${{ secrets.ANTITHESIS_TENANT }} username: ${{ secrets.ANTITHESIS_USER }} password: ${{ secrets.ANTITHESIS_PASSWD }} github_token: ${{ secrets.ANTITHESIS_GITHUB_TOKEN }} config_image: ${{ secrets.ANTITHESIS_DOCKER_REPO }}/limbo-config:antithesis-latest images: ${{ secrets.ANTITHESIS_DOCKER_REPO }}/limbo-workload:antithesis-latest description: >- Turso - ${{ github.event_name }} on ${{ github.ref_name }} (${{ github.sha }}) by ${{ github.actor }}${{ inputs.test_name && format(' ({0})', inputs.test_name) || '' }} email_recipients: ${{ secrets.ANTITHESIS_EMAIL }} test_name: ${{ inputs.test_name }} additional_parameters: |- antithesis.duration=${{ inputs.duration }} antithesis.source=${{ github.ref_name }} ================================================ FILE: .github/workflows/build-sim.yml ================================================ name: Build and push limbo-sim image on: workflow_dispatch: push: branches: - main # Add permissions needed for OIDC authentication with AWS permissions: id-token: write # allow getting OIDC token contents: read # allow reading repository contents # Ensure only one build runs at a time. A new push to main will cancel any in-progress build. concurrency: group: "build-sim" cancel-in-progress: true env: AWS_REGION: ${{ secrets.LIMBO_SIM_AWS_REGION }} IAM_ROLE: ${{ secrets.LIMBO_SIM_DEPLOYER_IAM_ROLE }} ECR_URL: ${{ secrets.LIMBO_SIM_ECR_URL }} ECR_IMAGE_NAME: ${{ secrets.LIMBO_SIM_IMAGE_NAME }} GIT_HASH: ${{ github.sha }} jobs: deploy: runs-on: blacksmith timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@v4 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ env.IAM_ROLE }} aws-region: ${{ env.AWS_REGION }} - name: Login to Amazon ECR uses: aws-actions/amazon-ecr-login@v2 - name: Build and push limbo-sim docker image run: | docker build -f testing/simulator/simulator-docker-runner/Dockerfile.simulator -t ${{ env.ECR_URL }}/${{ env.ECR_IMAGE_NAME }} --build-arg GIT_HASH=${{ env.GIT_HASH }} . docker push ${{ env.ECR_URL }}/${{ env.ECR_IMAGE_NAME }} ================================================ FILE: .github/workflows/build-sqlancer.yml ================================================ name: Build and push sqlancer-runner image on: workflow_dispatch: push: branches: - main paths: - "bindings/java/**" - "scripts/corruption-debug-tools/**" - "testing/sqlancer/**" - ".github/workflows/build-sqlancer.yml" # Add permissions needed for OIDC authentication with AWS permissions: id-token: write # allow getting OIDC token contents: read # allow reading repository contents # Ensure only one build runs at a time. A new push to main will cancel any in-progress build. concurrency: group: "build-sqlancer" cancel-in-progress: true env: AWS_REGION: ${{ secrets.LIMBO_SIM_AWS_REGION }} IAM_ROLE: ${{ secrets.SQLANCER_DEPLOYER_IAM_ROLE }} ECR_URL: ${{ secrets.SQLANCER_ECR_URL }} GIT_HASH: ${{ github.sha }} jobs: deploy: runs-on: blacksmith timeout-minutes: 45 steps: - name: Checkout code uses: actions/checkout@v4 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ env.IAM_ROLE }} aws-region: ${{ env.AWS_REGION }} - name: Login to Amazon ECR uses: aws-actions/amazon-ecr-login@v2 - name: Build and push sqlancer-runner docker image run: | docker build -f testing/sqlancer/sqlancer-runner/Dockerfile.sqlancer -t ${{ env.ECR_URL }}:latest --build-arg GIT_HASH=${{ env.GIT_HASH }} . docker push ${{ env.ECR_URL }}:latest ================================================ FILE: .github/workflows/c-compat.yml ================================================ name: C compat Tests on: workflow_dispatch: push: branches: - main tags: - v* pull_request: branches: - main concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: CARGO_INCREMENTAL: "0" CARGO_NET_RETRY: 10 jobs: test: runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Rust(stable) uses: dtolnay/rust-toolchain@stable - name: Setup mold linker uses: rui314/setup-mold@v1 - name: Rust cache uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Build Turso C bindings run: cargo build -p turso_sqlite3 --features capi --locked - name: Run C compat tests from Rust working-directory: sqlite3 run: cargo test --locked - name: Run C compat C tests linking against real SQLite working-directory: sqlite3/tests run: | make clean make LIBS="-lsqlite3" ./sqlite3-tests - name: Run C compat C tests linking against Turso working-directory: sqlite3/tests run: | make clean make LIBS="-L../../target/debug -lturso_sqlite3" LD_LIBRARY_PATH=../../target/debug ./sqlite3-tests ================================================ FILE: .github/workflows/claude.yml ================================================ name: Claude Code on: issue_comment: types: [created] pull_request_review_comment: types: [created] pull_request_review: types: [submitted] issues: types: [assigned] jobs: claude: concurrency: group: claude-${{ github.event.issue.number || github.event.pull_request.number }} cancel-in-progress: false # Only run when: # 1. @claude is mentioned in a comment on an issue or PR by an authorized user # 2. @claude is mentioned in a PR review by an authorized user # 3. claude[bot] is assigned to an issue # # Authorized users: OWNER, MEMBER, COLLABORATOR (prevents abuse by external users) if: | ( contains(fromJSON('["issue_comment", "pull_request_review_comment"]'), github.event_name) && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) ) || ( github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association) ) || ( github.event_name == 'issues' && github.event.assignee.login == 'claude[bot]' ) runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: write issues: write pull-requests: write actions: read id-token: write steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: Set up Rust uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 - name: Run Claude uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} branch_prefix: "claude/" additional_permissions: | actions: read claude_args: | --model claude-opus-4-5-20251101 --max-turns 100 --allowedTools "Bash(cargo:*),Bash(git:*),Bash(make:*),Bash(rustfmt:*),Bash(python:*),Edit,Write,Read,Glob,Grep,TodoWrite,Task,WebSearch" --disallowedTools "Bash(rm -rf *),Bash(sudo *),Bash(curl *),Bash(wget *)" ================================================ FILE: .github/workflows/codspeed.yml ================================================ name: CodSpeed on: push: branches: - "main" pull_request: workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true permissions: contents: read id-token: write jobs: benchmarks: runs-on: ubuntu-24.04 timeout-minutes: 60 # Allow this job to fail without failing the entire CI run continue-on-error: true steps: - uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@1.88.0 - name: Setup mold linker uses: rui314/setup-mold@v1 - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust-codspeed" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Install cargo-codspeed uses: taiki-e/install-action@v2 with: tool: cargo-codspeed - name: Generate graph-queries DBs run: | python3 perf/graph-queries/generate_seed.py | sqlite3 perf/graph-queries/graph-queries.db cp perf/graph-queries/graph-queries.db perf/graph-queries/graph-queries-analyzed.db sqlite3 perf/graph-queries/graph-queries-analyzed.db "ANALYZE;" - name: Build benchmarks (excluding TPC-H) run: make codspeed-build-bench-exclude-tpc-h - name: Run benchmarks (excluding TPC-H) uses: CodSpeedHQ/action@v4.5.2 with: mode: simulation run: cargo codspeed run # tpc-h: # runs-on: ubuntu-latest # timeout-minutes: 60 # env: # DB_FILE: "perf/tpc-h/TPC-H.db" # steps: # - uses: actions/checkout@v4 # - name: Setup Rust toolchain # uses: dtolnay/rust-toolchain@stable # - name: Setup mold linker # uses: rui314/setup-mold@v1 # - uses: Swatinem/rust-cache@v2 # with: # prefix-key: "v1-rust-codspeed" # cache-on-failure: true # - name: Setup sccache # uses: mozilla-actions/sccache-action@v0.0.9 # - name: Install cargo-codspeed # uses: taiki-e/install-action@v2 # with: # tool: cargo-codspeed # - name: Cache TPC-H # id: cache-tpch # uses: actions/cache@v4 # with: # path: ${{ env.DB_FILE }} # key: tpc-h # - name: Download TPC-H # if: steps.cache-tpch.outputs.cache-hit != 'true' # env: # DB_URL: "https://github.com/lovasoa/TPCH-sqlite/releases/download/v1.0/TPC-H.db" # run: wget -O $DB_FILE --no-verbose $DB_URL # - name: Build TPC-H benchmark # env: # SCCACHE_GHA_ENABLED: "true" # RUSTC_WRAPPER: "sccache" # run: cargo codspeed build --features codspeed --bench tpc_h_benchmark # - name: Run TPC-H benchmark # uses: CodSpeedHQ/action@v4.5.2 # with: # mode: simulation # run: cargo codspeed run --bench tpc_h_benchmark ================================================ FILE: .github/workflows/dotnet-publish.yml ================================================ name: Dotnet Publish on: # Manually trigger the workflow workflow_dispatch: env: working-directory: bindings/dotnet jobs: # Build native libraries for each platform build-natives: strategy: matrix: include: - os: ubuntu-latest make-target: build-rust-linux64 artifact-name: linux64 target: x86_64-unknown-linux-gnu - os: macos-latest make-target: build-rust-macos64 artifact-name: macos64 target: x86_64-apple-darwin - os: macos-latest make-target: build-rust-macosarm64 target: aarch64-apple-darwin artifact-name: macosarm64 - os: windows-latest make-target: build-rust-windows64 target: x86_64-pc-windows-msvc artifact-name: windows64 runs-on: ${{ matrix.os }} timeout-minutes: 30 defaults: run: working-directory: ${{ env.working-directory }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Add target run: rustup target add ${{ matrix.target }} - name: Build rust native library run: make ${{ matrix.make-target }} RUST_RELEASE_OPT='--profile release-official' - name: Upload rust native library uses: actions/upload-artifact@v4 with: name: native-${{ matrix.artifact-name }} path: | ${{ env.working-directory }}/rs_compiled/**/turso_dotnet.dll ${{ env.working-directory }}/rs_compiled/**/libturso_dotnet.so ${{ env.working-directory }}/rs_compiled/**/libturso_dotnet.dylib retention-days: 1 # Create Turso.Raw and Turso nuget packages publish: needs: build-natives runs-on: ubuntu-latest timeout-minutes: 30 defaults: run: working-directory: ${{ env.working-directory }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Install dotnet sdk 9.0 uses: actions/setup-dotnet@v4 with: dotnet-version: '9.0.x' - name: Download all native libraries uses: actions/download-artifact@v4 with: pattern: native-* path: ${{ env.working-directory }}/rs_compiled merge-multiple: true - name: Build and pack run: make pack # https://learn.microsoft.com/en-us/nuget/nuget-org/publish-a-package - name: Publish to nuget if: false # TODO: Get nuget api key and publish packages run: | dotnet nuget push ${{ env.working-directory }}/src/Turso.Raw/bin/Release/Turso.Raw.*.nupkg --api-key ... --source https://api.nuget.org/v3/index.json dotnet nuget push ${{ env.working-directory }}/src/Turso/bin/Release/Turso.*.nupkg --api-key ... --source https://api.nuget.org/v3/index.json - name: Upload Turso.Raw to artifacts uses: actions/upload-artifact@v4 with: name: Turso.Raw path: ${{ env.working-directory }}/src/Turso.Raw/bin/Release/Turso.Raw.*.nupkg retention-days: 7 - name: Upload Turso to artifacts uses: actions/upload-artifact@v4 with: name: Turso path: ${{ env.working-directory }}/src/Turso/bin/Release/Turso.*.nupkg retention-days: 7 ================================================ FILE: .github/workflows/dotnet-test.yml ================================================ name: Dotnet Tests on: push: branches: - main tags: - v* pull_request: branches: - main concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: working-directory: bindings/dotnet CARGO_INCREMENTAL: "0" CARGO_NET_RETRY: 10 jobs: test: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 defaults: run: working-directory: ${{ env.working-directory }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Rust(stable) uses: dtolnay/rust-toolchain@stable - name: Setup mold linker uses: rui314/setup-mold@v1 - name: Rust cache uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Install dotnet sdk 9.0 uses: actions/setup-dotnet@v4 with: dotnet-version: '9.0.x' - name: Run tests run: make test ================================================ FILE: .github/workflows/elle.yml ================================================ name: Elle Consistency Check on: push: branches: - main pull_request: branches: - main workflow_dispatch: inputs: max_steps: description: "Maximum simulation steps" required: false default: "100000" seed: description: "Random seed (leave empty for random)" required: false default: "" schedule: # Run nightly at 3am UTC - cron: "0 3 * * *" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: CARGO_TERM_COLOR: always jobs: elle-check: name: Elle ${{ matrix.elle_model }} (${{ matrix.mvcc && 'MVCC' || 'default' }}) runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 60 strategy: fail-fast: false matrix: mvcc: [false, true] elle_model: [list-append, rw-register] steps: - uses: actions/checkout@v4 - uses: useblacksmith/rust-cache@v3 with: prefix-key: "v1-rust" - uses: "./.github/shared/setup-sccache" - name: Setup Java uses: actions/setup-java@v4 with: distribution: "temurin" java-version: "21" - name: Cache Leiningen uses: actions/cache@v4 with: path: | /tmp/lein ~/.lein key: lein-${{ runner.os }} - name: Cache elle-cli uses: actions/cache@v4 with: path: /tmp/elle-cli key: elle-cli-${{ runner.os }}-${{ hashFiles('.github/workflows/elle.yml') }} - name: Build simulator run: cargo build -p turso_whopper - name: Run simulation with Elle env: SEED: ${{ github.event.inputs.seed }} run: | SEED_ARG="" if [ -n "$SEED" ]; then export SEED="$SEED" fi MAX_STEPS="${{ github.event.inputs.max_steps }}" MAX_STEPS="${MAX_STEPS:-100000}" MVCC_FLAG="" if [ "${{ matrix.mvcc }}" = "true" ]; then MVCC_FLAG="--enable-mvcc" fi ./target/debug/turso_whopper \ --elle ${{ matrix.elle_model }} \ --elle-output elle-history.edn \ --max-steps "$MAX_STEPS" \ $MVCC_FLAG - name: Setup Leiningen run: | if [ ! -x /tmp/lein/lein ]; then mkdir -p /tmp/lein curl -sL https://raw.githubusercontent.com/technomancy/leiningen/2.11.2/bin/lein -o /tmp/lein/lein chmod +x /tmp/lein/lein /tmp/lein/lein version fi - name: Build elle-cli run: | if [ ! -d /tmp/elle-cli ]; then git clone --depth 1 https://github.com/ligurio/elle-cli.git /tmp/elle-cli fi cd /tmp/elle-cli if [ ! -f target/*-standalone.jar ]; then /tmp/lein/lein uberjar fi - name: Install Graphviz run: sudo apt-get update && sudo apt-get install -y graphviz - name: Run Elle analysis run: | ELLE_JAR=$(ls -t /tmp/elle-cli/target/*-standalone.jar | head -1) mkdir -p elle-results echo "Using JAR: $ELLE_JAR" echo "History file: $(pwd)/elle-history.edn" echo "History size: $(wc -l < elle-history.edn) events" echo "" CONSISTENCY_FLAG="--consistency-models serializable" if [ "${{ matrix.mvcc }}" = "true" ]; then CONSISTENCY_FLAG="--consistency-models snapshot-isolation" fi java -jar "$ELLE_JAR" --model ${{ matrix.elle_model }} $CONSISTENCY_FLAG --verbose --directory elle-results elle-history.edn - name: Upload Elle results uses: actions/upload-artifact@v4 if: always() with: name: elle-results-${{ matrix.elle_model }}-${{ matrix.mvcc && 'mvcc' || 'default' }} path: | elle-history.edn elle-results/ retention-days: 30 ================================================ FILE: .github/workflows/fuzz.yml ================================================ name: Run long fuzz tests and stress test on: workflow_dispatch: push: branches: - main tags: - v* pull_request: branches: - main concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: CARGO_INCREMENTAL: "0" CARGO_NET_RETRY: 10 jobs: run-fuzz-tests: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Setup mold linker uses: rui314/setup-mold@v1 - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Install cargo-nextest uses: taiki-e/install-action@nextest - name: Set up Python 3.10 uses: useblacksmith/setup-python@v6 with: python-version: "3.10" - name: Build run: cargo build --verbose --locked --all-features - name: Run fuzz tests env: RUST_BACKTRACE: 1 FUZZ_MULTIPLIER: "0.5" run: cargo nextest run --locked --no-fail-fast -E 'binary(fuzz_tests)' --failure-output=final run-long-fuzz-tests: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Setup mold linker uses: rui314/setup-mold@v1 - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Install cargo-nextest uses: taiki-e/install-action@nextest - name: Set up Python 3.10 uses: useblacksmith/setup-python@v6 with: python-version: "3.10" - name: Build run: cargo build --verbose --locked --all-features - name: Run ignored long fuzz tests env: RUST_BACKTRACE: 1 FUZZ_MULTIPLIER: "0.5" run: cargo nextest run --locked --no-fail-fast --run-ignored ignored-only -E 'test(/fuzz_long/)' --failure-output=final simple-stress-test: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Setup mold linker uses: rui314/setup-mold@v1 - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Set up Python 3.10 uses: useblacksmith/setup-python@v6 with: python-version: "3.10" - name: Build run: cargo build --verbose --locked --all-features - name: Run ignored long tests env: RUST_BACKTRACE: 1 run: RUSTFLAGS="--cfg shuttle" cargo run -p turso_stress --locked -- --nr-threads 3 --nr-iterations 300 shuttle-stress-test: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Setup mold linker uses: rui314/setup-mold@v1 - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust-shuttle" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Run shuttle mvcc tests env: RUST_BACKTRACE: "full" RUSTFLAGS: "--cfg=shuttle" run: cargo test -p turso_stress --locked --test shuttle_mvcc - name: Run shuttle stress test env: RUST_BACKTRACE: 1 RUSTFLAGS: "--cfg=shuttle" run: cargo run -p turso_stress --locked -- --nr-threads 2 --nr-iterations 1000 libfuzzer-scalar-func: # Allow this job to fail without failing the entire CI run continue-on-error: true runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@nightly - name: Setup mold linker uses: rui314/setup-mold@v1 - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust-fuzz" cache-on-failure: true - name: Install cargo-fuzz run: cargo install cargo-fuzz - name: Run scalar_func fuzzer run: cargo +nightly fuzz run scalar_func -- -max_total_time=300 ================================================ FILE: .github/workflows/go.yml ================================================ ## path=../../.github/workflows/go-publish.yml name: Build & Publish Go Driver on: workflow_dispatch: push: branches: ["main"] tags: - "v*" pull_request: branches: ["main"] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: contents: write env: # Token with repo access to tursodatabase/turso-go-platform-libs GH_TOKEN: ${{ secrets.TURSO_GO_PLATFORM_LIBS_TOKEN }} CARGO_INCREMENTAL: "0" CARGO_NET_RETRY: 10 jobs: test: name: Build Rust and run Go tests runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: matrix: os: - blacksmith-4vcpu-ubuntu-2404 - macos-latest - windows-latest env: CARGO_TERM_COLOR: always steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Setup mold linker if: runner.os == 'Linux' uses: rui314/setup-mold@v1 - name: Rust cache uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Setup Go uses: actions/setup-go@v5 with: go-version: "1.24.10" cache: true cache-dependency-path: bindings/go/go.sum - name: Build Rust (debug) run: cargo build --verbose --locked # Linux: use LD_LIBRARY_PATH to point to Rust target dir - name: Run Go tests (Linux) if: runner.os == 'Linux' working-directory: bindings/go env: LOCAL_SYNC_SERVER: ../../target/debug/tursodb LD_LIBRARY_PATH: ${{ github.workspace }}/target/debug run: go test ./... -v # macOS: use DYLD_LIBRARY_PATH to point to Rust target dir - name: Run Go tests (macOS) if: runner.os == 'macOS' working-directory: bindings/go env: LOCAL_SYNC_SERVER: ../../target/debug/tursodb DYLD_LIBRARY_PATH: ${{ github.workspace }}/target/debug run: go test ./... -v # Windows: prepend Rust target dir to PATH so .dll can be found - name: Run Go tests (Windows) if: runner.os == 'Windows' working-directory: bindings/go shell: pwsh run: | $env:PATH = "${{ github.workspace }}\target\debug;$env:PATH" $env:LOCAL_SYNC_SERVER = "..\..\target\debug\tursodb.exe" go test ./... -v publish: name: Publish Go driver needs: test runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Ensure tooling (jq, coreutils) run: | sudo apt-get update sudo apt-get install -y jq coreutils - name: Compute publish variables id: vars shell: bash run: | set -euo pipefail EVENT="${{ github.event_name }}" REF="${{ github.ref }}" # Determine whether to publish for real: # - Manual dispatch: publish # - Tag push (refs/tags/v*): publish # - Everything else (including PR): dry-run if [[ "$EVENT" == "workflow_dispatch" ]]; then DO_PUBLISH="true" elif [[ "$REF" == refs/tags/v* ]]; then DO_PUBLISH="true" else DO_PUBLISH="false" fi if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then # For tag events, github.ref_name is correct TURSO_REF="${GITHUB_REF_NAME}" elif [[ -n "${GITHUB_HEAD_REF}" ]]; then # For PRs, github.head_ref contains the real source branch TURSO_REF="${GITHUB_HEAD_REF}" else # For push events to branches TURSO_REF="${GITHUB_REF_NAME}" fi # Branch to push into this repository if [[ "$EVENT" == "workflow_dispatch" ]]; then TARGET_TAG="" TARGET_BRANCH="go-release-${TURSO_REF}" elif [[ "$REF" == refs/tags/* ]]; then TARGET_TAG="${TURSO_REF}" TARGET_BRANCH="go-release-${TURSO_REF}" else TARGET_TAG="" TARGET_BRANCH="go-release-${TURSO_REF}" fi # Generate a unique nonce NONCE="$(uuidgen 2>/dev/null || true)" if [[ -z "$NONCE" ]]; then NONCE="$(date +%s)-$RANDOM-$RANDOM" fi echo "do_publish=$DO_PUBLISH" >> "$GITHUB_OUTPUT" echo "turso_ref=$TURSO_REF" >> "$GITHUB_OUTPUT" echo "target_branch=$TARGET_BRANCH" >> "$GITHUB_OUTPUT" echo "target_tag=$TARGET_TAG" >> "$GITHUB_OUTPUT" echo "nonce=$NONCE" >> "$GITHUB_OUTPUT" echo "Event: $EVENT" echo "Ref: $REF" echo "DO_PUBLISH=$DO_PUBLISH" echo "TURSO_REF=$TURSO_REF" echo "TARGET_BRANCH=$TARGET_BRANCH" echo "TARGET_TAG=$TARGET_TAG" echo "NONCE=$NONCE" - name: Dry-run (PRs and non-tag non-manual runs) if: steps.vars.outputs.do_publish != 'true' run: | echo "Dry run: not triggering turso-go-platform-libs and not pushing any commits." echo "Would have used:" echo " - turso_ref=${{ steps.vars.outputs.turso_ref }}" echo " - target_branch=${{ steps.vars.outputs.target_branch }}" echo " - nonce=${{ steps.vars.outputs.nonce }}" echo "Exiting." - name: Check token available for external repo if: steps.vars.outputs.do_publish == 'true' shell: bash run: | if [[ -z "${GH_TOKEN:-}" ]]; then echo "TURSO_GO_PLATFORM_LIBS_TOKEN is not set; cannot publish." exit 1 fi # Trigger turso-go-platform-libs build+publish workflow - name: Trigger external prebuilt-libs workflow if: steps.vars.outputs.do_publish == 'true' shell: bash env: TURSO_REF: ${{ steps.vars.outputs.turso_ref }} NONCE: ${{ steps.vars.outputs.nonce }} run: | set -euo pipefail echo "Triggering tursodatabase/turso-go-platform-libs build.yml with:" echo " - turso_ref=$TURSO_REF" echo " - push_changes=true" echo " - nonce=$NONCE" # Use gh to dispatch workflow gh workflow run build.yml \ -R tursodatabase/turso-go-platform-libs \ -f turso_ref="$TURSO_REF" \ -f push_changes=true \ -f nonce="$NONCE" # Wait for the external workflow to complete by locating the run that has our nonce in job names - name: Wait for external workflow completion if: steps.vars.outputs.do_publish == 'true' id: wait_external shell: bash env: NONCE: ${{ steps.vars.outputs.nonce }} run: | set -euo pipefail REPO="tursodatabase/turso-go-platform-libs" WORKFLOW_FILE="build.yml" echo "Searching for workflow run in $REPO matching nonce=$NONCE" # Try to find the run that includes the nonce in job names. FOUND_RUN_ID="" ATTEMPTS=0 MAX_ATTEMPTS=60 # ~15 minutes (with 15s sleep) while [[ -z "$FOUND_RUN_ID" && $ATTEMPTS -lt $MAX_ATTEMPTS ]]; do ATTEMPTS=$((ATTEMPTS+1)) # Fetch recent runs of the workflow RUN_IDS=$(gh api -H "Accept: application/vnd.github+json" \ repos/$REPO/actions/workflows/$WORKFLOW_FILE/runs \ -q '.workflow_runs[0:30][] | .id' || true) for RID in $RUN_IDS; do # List jobs and search for nonce in any job name if gh api -H "Accept: application/vnd.github+json" repos/$REPO/actions/runs/$RID/jobs -q '.jobs[].name' | grep -q "nonce=${NONCE}"; then FOUND_RUN_ID="$RID" break fi done if [[ -z "$FOUND_RUN_ID" ]]; then echo "Nonce not found yet (attempt $ATTEMPTS/$MAX_ATTEMPTS). Sleeping 15s..." sleep 15 fi done if [[ -z "$FOUND_RUN_ID" ]]; then echo "Failed to locate external workflow run with nonce=$NONCE" exit 1 fi echo "Found external run id: $FOUND_RUN_ID" echo "run_id=$FOUND_RUN_ID" >> "$GITHUB_OUTPUT" echo "Waiting for external run to complete..." while true; do STATUS=$(gh api repos/$REPO/actions/runs/$FOUND_RUN_ID -q '.status') CONCLUSION=$(gh api repos/$REPO/actions/runs/$FOUND_RUN_ID -q '.conclusion') echo "Status: $STATUS, Conclusion: $CONCLUSION" if [[ "$STATUS" == "completed" ]]; then if [[ "$CONCLUSION" != "success" ]]; then echo "External workflow did not succeed. Conclusion: $CONCLUSION" exit 1 fi break fi sleep 15 done - name: Determine libs branch name from turso_ref if: steps.vars.outputs.do_publish == 'true' id: libs_branch shell: bash env: TURSO_REF: ${{ steps.vars.outputs.turso_ref }} run: | set -euo pipefail BRANCH="turso-branch-$TURSO_REF" echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" echo "Resolved libs branch: $BRANCH" - name: Setup Go (for dependency update) if: steps.vars.outputs.do_publish == 'true' uses: actions/setup-go@v5 with: go-version: "1.24.10" - name: Create branch if: steps.vars.outputs.do_publish == 'true' shell: bash working-directory: bindings/go env: TARGET_TAG: ${{ steps.vars.outputs.target_tag }} TARGET_BRANCH: ${{ steps.vars.outputs.target_branch }} NONCE: ${{ steps.vars.outputs.nonce }} RESOLVED_VERSION: ${{ steps.update_dep.outputs.resolved_version }} run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" # Create/update branch # If branch exists locally/remote, check it out; otherwise create if git ls-remote --exit-code --heads origin "$TARGET_BRANCH" >/dev/null 2>&1; then echo "Branch $TARGET_BRANCH exists on remote — checking it out." git fetch origin "$TARGET_BRANCH":"$TARGET_BRANCH" git checkout "$TARGET_BRANCH" else echo "Creating new branch $TARGET_BRANCH" git checkout -b "$TARGET_BRANCH" fi - name: Update dependency to freshly built libs if: steps.vars.outputs.do_publish == 'true' id: update_dep working-directory: bindings/go shell: bash env: LIBS_BRANCH: ${{ steps.libs_branch.outputs.branch }} run: | set -euo pipefail echo "Updating github.com/tursodatabase/turso-go-platform-libs to branch: $LIBS_BRANCH" # Update dependency and tidy go get "github.com/tursodatabase/turso-go-platform-libs@${LIBS_BRANCH}" go mod tidy # Determine the resolved pseudo-version RESOLVED_VERSION="$(go list -m -json github.com/tursodatabase/turso-go-platform-libs | jq -r .Version)" echo "resolved_version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT" echo "Resolved version: $RESOLVED_VERSION" echo "Changed files:" git status --porcelain - name: Push branch and tag with updated go.mod if: steps.vars.outputs.do_publish == 'true' shell: bash working-directory: bindings/go env: TARGET_TAG: ${{ steps.vars.outputs.target_tag }} TARGET_BRANCH: ${{ steps.vars.outputs.target_branch }} NONCE: ${{ steps.vars.outputs.nonce }} RESOLVED_VERSION: ${{ steps.update_dep.outputs.resolved_version }} run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" # Stage changes if any git add go.mod go.sum || true if git diff --cached --quiet; then echo "No changes in go.mod/go.sum to commit; skipping push." exit 0 fi COMMIT_MSG="go: update turso-go-platform-libs to ${RESOLVED_VERSION} (nonce: ${NONCE})" git commit -m "$COMMIT_MSG" # Push branch git push --set-upstream origin "$TARGET_BRANCH" --force-with-lease || git push --set-upstream origin "$TARGET_BRANCH" echo "Pushed branch: $TARGET_BRANCH" if [[ "$TARGET_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([\-+][0-9A-Za-z\.-]+)?$ ]]; then # module is in the subdirectory - prepend path to the version tag git tag "bindings/go/$TARGET_TAG" git push origin tag "bindings/go/$TARGET_TAG" fi ================================================ FILE: .github/workflows/java-publish.yml ================================================ name: Publish Java Bindings to Maven Central on: # Manually trigger the workflow workflow_dispatch: env: working-directory: bindings/java jobs: # Build native libraries for each platform build-natives: strategy: matrix: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu make-target: linux_x86 artifact-name: linux-x86_64 - os: macos-latest target: x86_64-apple-darwin make-target: macos_x86 artifact-name: macos-x86_64 - os: macos-latest target: aarch64-apple-darwin make-target: macos_arm64 artifact-name: macos-arm64 - os: ubuntu-latest target: x86_64-pc-windows-gnu make-target: windows artifact-name: windows-x86_64 runs-on: ${{ matrix.os }} timeout-minutes: 30 defaults: run: working-directory: ${{ env.working-directory }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Verify and install Rust target run: | echo "Installing target: ${{ matrix.target }}" rustup target add ${{ matrix.target }} echo "Installed targets:" rustup target list --installed echo "Rust version:" rustc --version - name: Install cross-compilation tools (Windows on Linux) if: matrix.target == 'x86_64-pc-windows-gnu' run: | sudo apt-get update sudo apt-get install -y mingw-w64 - name: Build native library run: make ${{ matrix.make-target }} - name: Verify build output run: | echo "Build completed for ${{ matrix.target }}" ls -lah libs/ find libs/ -type f - name: Upload native library uses: actions/upload-artifact@v4 with: name: native-${{ matrix.artifact-name }} path: ${{ env.working-directory }}/libs/ retention-days: 1 # Publish to Maven Central with all native libraries publish: needs: build-natives runs-on: ubuntu-latest timeout-minutes: 30 defaults: run: working-directory: ${{ env.working-directory }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '8' - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 - name: Install Rust (for test builds) uses: dtolnay/rust-toolchain@stable - name: Download all native libraries uses: actions/download-artifact@v4 with: pattern: native-* path: ${{ env.working-directory }}/libs-temp merge-multiple: true - name: Organize native libraries run: | # Move downloaded artifacts to libs directory rm -rf libs mv libs-temp libs echo "Native libraries collected:" ls -R libs/ - name: Build test natives run: make build_test - name: Run tests run: ./gradlew test - name: Publish to Maven Central env: MAVEN_UPLOAD_USERNAME: ${{ secrets.MAVEN_UPLOAD_USERNAME }} MAVEN_UPLOAD_PASSWORD: ${{ secrets.MAVEN_UPLOAD_PASSWORD }} MAVEN_SIGNING_KEY: ${{ secrets.MAVEN_SIGNING_KEY }} MAVEN_SIGNING_PASSPHRASE: ${{ secrets.MAVEN_SIGNING_PASSPHRASE }} run: | echo "Building, signing, and publishing to Maven Central..." ./gradlew clean publishToMavenCentral --no-daemon --stacktrace - name: Upload bundle artifact if: always() uses: actions/upload-artifact@v4 with: name: maven-central-bundle path: ${{ env.working-directory }}/build/maven-central/*.zip retention-days: 7 ================================================ FILE: .github/workflows/java.yml ================================================ name: Java Tests on: push: branches: - main tags: - v* pull_request: branches: - main concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: working-directory: bindings/java CARGO_INCREMENTAL: "0" CARGO_NET_RETRY: 10 jobs: test: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 defaults: run: working-directory: ${{ env.working-directory }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Rust(stable) uses: dtolnay/rust-toolchain@stable - name: Setup mold linker uses: rui314/setup-mold@v1 - name: Rust cache uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Set up JDK uses: useblacksmith/setup-java@v5 with: distribution: 'temurin' java-version: '8' - name: Run Java tests run: make test ================================================ FILE: .github/workflows/labeler.yml ================================================ name: "Pull Request Labeler" on: - pull_request_target jobs: labeler: timeout-minutes: 30 permissions: contents: read issues: write pull-requests: write runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - uses: actions/checkout@v4 - uses: actions/labeler@v5 ================================================ FILE: .github/workflows/napi.yml ================================================ name: Build & publish @tursodatabase/database on: workflow_dispatch: push: branches: - main tags: - v* pull_request: branches: - main env: DEBUG: napi:* APP_NAME: turso MACOSX_DEPLOYMENT_TARGET: "10.13" CARGO_INCREMENTAL: "0" CARGO_NET_RETRY: 10 defaults: run: working-directory: bindings/javascript concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: build: timeout-minutes: 20 strategy: fail-fast: false matrix: settings: - host: windows-latest target: x86_64-pc-windows-msvc artifact: db-bindings-x86_64-pc-windows-msvc build: yarn workspace @tursodatabase/database napi-build --target x86_64-pc-windows-msvc - host: windows-latest target: x86_64-pc-windows-msvc artifact: sync-bindings-x86_64-pc-windows-msvc build: yarn workspace @tursodatabase/sync napi-build --target x86_64-pc-windows-msvc - host: ubuntu-latest target: x86_64-unknown-linux-gnu artifact: db-bindings-x86_64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian build: yarn workspace @tursodatabase/database napi-build --target x86_64-unknown-linux-gnu - host: ubuntu-latest target: x86_64-unknown-linux-gnu artifact: sync-bindings-x86_64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian build: yarn workspace @tursodatabase/sync napi-build --target x86_64-unknown-linux-gnu - host: macos-latest target: aarch64-apple-darwin artifact: db-bindings-aarch64-apple-darwin build: yarn workspace @tursodatabase/database napi-build --target aarch64-apple-darwin - host: macos-latest target: aarch64-apple-darwin artifact: sync-bindings-aarch64-apple-darwin build: yarn workspace @tursodatabase/sync napi-build --target aarch64-apple-darwin - host: blacksmith-2vcpu-ubuntu-2404-arm target: aarch64-unknown-linux-gnu artifact: db-bindings-aarch64-unknown-linux-gnu build: yarn workspace @tursodatabase/database napi-build --target aarch64-unknown-linux-gnu - host: blacksmith-2vcpu-ubuntu-2404-arm target: aarch64-unknown-linux-gnu artifact: sync-bindings-aarch64-unknown-linux-gnu build: yarn workspace @tursodatabase/sync napi-build --target aarch64-unknown-linux-gnu - host: ubuntu-latest target: wasm32-wasip1-threads artifact: db-bindings-wasm32-wasip1-threads setup: | rustup target add wasm32-wasip1-threads wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz tar -xvf wasi-sdk-25.0-x86_64-linux.tar.gz build: | export WASI_SDK_PATH="$(pwd)/wasi-sdk-25.0-x86_64-linux" export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) export TARGET_CXXFLAGS="--target=wasm32-wasi-threads --sysroot=$(pwd)/wasi-sdk-25.0-x86_64-linux/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj -lsetjmp" export TARGET_CFLAGS="$TARGET_CXXFLAGS" yarn workspace @tursodatabase/database-common build yarn workspace @tursodatabase/database-wasm-common build yarn workspace @tursodatabase/database-wasm build - host: ubuntu-latest target: wasm32-wasip1-threads artifact: sync-bindings-wasm32-wasip1-threads setup: | rustup target add wasm32-wasip1-threads wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz tar -xvf wasi-sdk-25.0-x86_64-linux.tar.gz build: | export WASI_SDK_PATH="$(pwd)/wasi-sdk-25.0-x86_64-linux" export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) export TARGET_CXXFLAGS="--target=wasm32-wasi-threads --sysroot=$(pwd)/wasi-sdk-25.0-x86_64-linux/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj -lsetjmp" export TARGET_CFLAGS="$TARGET_CXXFLAGS" yarn workspace @tursodatabase/database-common build yarn workspace @tursodatabase/database-wasm-common build yarn workspace @tursodatabase/sync-common build yarn workspace @tursodatabase/sync-wasm build name: ${{ matrix.settings.artifact }} - node@20 runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v4 - name: Setup node uses: actions/setup-node@v4 if: ${{ !matrix.settings.docker }} with: node-version: 20 - name: Install uses: dtolnay/rust-toolchain@stable if: ${{ !matrix.settings.docker }} with: toolchain: stable targets: ${{ matrix.settings.target }} - name: Cache cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ .cargo-cache target/ key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}- save-always: true - uses: mlugg/setup-zig@v2 if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' || matrix.settings.target == 'armv7-unknown-linux-musleabihf' }} with: version: 0.13.0 - name: Setup toolchain run: ${{ matrix.settings.setup }} if: ${{ matrix.settings.setup }} shell: bash - name: Install dependencies run: yarn install - name: Build common run: yarn workspace @tursodatabase/database-common build - name: Setup node x86 uses: actions/setup-node@v4 if: matrix.settings.target == 'x86_64-pc-windows-msvc' with: node-version: 20 architecture: x64 - name: Build in docker if: ${{ matrix.settings.docker }} shell: bash working-directory: . run: | docker run --rm --user 0:0 \ -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db \ -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache \ -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index \ -v ${{ github.workspace }}:/build \ -w /build/bindings/javascript \ ${{ matrix.settings.docker }} \ bash -c "${{ matrix.settings.build }}" - name: Build run: ${{ matrix.settings.build }} if: ${{ !matrix.settings.docker }} shell: bash - name: Upload artifact uses: actions/upload-artifact@v4 with: name: ${{ matrix.settings.artifact }} path: | bindings/javascript/packages/native/turso.*.node bindings/javascript/packages/wasm/turso.*.wasm bindings/javascript/sync/packages/native/sync.*.node bindings/javascript/sync/packages/wasm/sync.*.wasm if-no-files-found: error test-db-linux-x64-gnu-binding: name: Test DB bindings on Linux-x64-gnu - node@${{ matrix.node }} timeout-minutes: 30 needs: - build strategy: fail-fast: false matrix: node: - "20" runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - uses: actions/checkout@v4 - name: Setup node uses: useblacksmith/setup-node@v5 with: node-version: ${{ matrix.node }} - name: Install dependencies run: yarn install - name: Build common run: yarn workspace @tursodatabase/database-common build - name: Download all DB artifacts uses: actions/download-artifact@v4 with: path: bindings/javascript merge-multiple: true pattern: 'db*' - name: List packages run: ls -R . shell: bash - name: Test bindings run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-slim yarn workspace @tursodatabase/database test test-db-wasm-binding: name: Test DB bindings on browser@${{ matrix.node }} timeout-minutes: 30 needs: - build strategy: fail-fast: false matrix: node: - "20" runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - uses: actions/checkout@v4 - name: Setup node uses: useblacksmith/setup-node@v5 with: node-version: ${{ matrix.node }} - name: Install dependencies run: yarn install - name: Build common run: yarn workspace @tursodatabase/database-common build - name: Build wasm-common run: yarn workspace @tursodatabase/database-wasm-common build - name: Install playwright with deps run: yarn workspace @tursodatabase/database-wasm playwright install --with-deps - name: Download all DB artifacts uses: actions/download-artifact@v4 with: path: bindings/javascript merge-multiple: true pattern: 'db*' - name: List packages run: ls -R . shell: bash - name: Test bindings run: yarn workspace @tursodatabase/database-wasm test publish: name: Publish runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read id-token: write needs: - test-db-linux-x64-gnu-binding - test-db-wasm-binding steps: - uses: actions/checkout@v4 - name: Setup node uses: actions/setup-node@v4 with: node-version: 20 - name: Update npm run: npm install -g npm@11 - name: Download all DB artifacts uses: actions/download-artifact@v4 with: path: bindings/javascript merge-multiple: true pattern: 'db*' - name: Download all sync artifacts uses: actions/download-artifact@v4 with: path: bindings/javascript merge-multiple: true pattern: 'sync*' - name: List packages run: ls -R . shell: bash - name: Install dependencies run: yarn install - name: Install dependencies run: yarn tsc-build - name: Publish if: "startsWith(github.ref, 'refs/tags/v')" run: | if git log -1 --pretty=%B | grep "^Turso [0-9]\+\.[0-9]\+\.[0-9]\+$"; then npm publish --workspaces --access public --provenance elif git log -1 --pretty=%B | grep "^Turso [0-9]\+\.[0-9]\+\.[0-9]\+"; then npm publish --workspaces --access public --provenance --tag next else echo "git log structure is unexpected, skip publishing" npm publish --workspaces --dry-run fi - name: Publish (dry-run) if: "!startsWith(github.ref, 'refs/tags/v')" run: | npm pack --workspaces ================================================ FILE: .github/workflows/perf_nightly.yml ================================================ name: Nightly Benchmarks on Nyrkiö Runners (stability) on: workflow_dispatch: branches: ["main", "notmain", "master"] schedule: - cron: '24 4 * * *' push: # branches: ["main", "notmain", "master"] branches: ["notmain"] pull_request: # branches: ["main", "notmain", "master"] branches: ["notmain"] env: CARGO_TERM_COLOR: never jobs: bench: runs-on: nyrkio_perf_server_4cpu_ubuntu2404 timeout-minutes: 45 # FIXME: make this run faster steps: - uses: actions/checkout@v3 - uses: useblacksmith/setup-node@v5 with: node-version: 20 # cache: 'npm' # - name: Install dependencies # run: npm install && npm run build - name: Generate graph-queries DB run: | python3 perf/graph-queries/generate_seed.py | sqlite3 perf/graph-queries/graph-queries.db cp perf/graph-queries/graph-queries.db perf/graph-queries/graph-queries-analyzed.db sqlite3 perf/graph-queries/graph-queries-analyzed.db "ANALYZE;" - name: Bench run: make bench-exclude-tpc-h 2>&1 | tee output.txt - name: Analyze benchmark result with Nyrkiö uses: nyrkio/change-detection@HEAD with: name: nightly/turso tool: criterion output-file-path: output.txt github-token: ${{ secrets.GITHUB_TOKEN }} # What to do if a change is immediately detected by Nyrkiö. # Note that smaller changes are only detected with delay, usually after a change # persisted over 2-7 commits. Go to nyrkiö.com to view those or configure alerts. # Note that Nyrkiö will find all changes, also improvements. This means fail-on-alert # on pull events isn't compatible with this workflow being required to pass branch protection. fail-on-alert: false comment-on-alert: false comment-always: false # Nyrkiö configuration # Get yours from https://nyrkio.com/docs/getting-started nyrkio-token: ${{ secrets.NYRKIO_JWT_TOKEN }} # HTTP requests will fail for all non-core contributors that don't have their own token. # Don't want that to spoil the build, so: never-fail: true # Make results and change points public, so that any oss contributor can see them nyrkio-public: true # parameters of the algorithm. Note: These are global, so we only set them once and for all. # Smaller p-value = less change points found. Larger p-value = more, but also more false positives. nyrkio-settings-pvalue: 0.00001 # Ignore changes smaller than this. nyrkio-settings-threshold: 0% clickbench: runs-on: nyrkio_perf_server_4cpu_ubuntu2404 timeout-minutes: 30 steps: - uses: actions/checkout@v3 - uses: useblacksmith/setup-node@v5 with: node-version: 20 - name: Clickbench run: make clickbench - name: Analyze TURSO result with Nyrkiö uses: nyrkio/change-detection@HEAD with: name: nightly/clickbench/turso tool: time output-file-path: clickbench-tursodb.txt github-token: ${{ secrets.GITHUB_TOKEN }} # What to do if a change is immediately detected by Nyrkiö. # Note that smaller changes are only detected with delay, usually after a change # persisted over 2-7 commits. Go to nyrkiö.com to view those or configure alerts. # Note that Nyrkiö will find all changes, also improvements. This means fail-on-alert # on pull events isn't compatible with this workflow being required to pass branch protection. fail-on-alert: false comment-on-alert: false comment-always: false # Nyrkiö configuration # Get yours from https://nyrkio.com/docs/getting-started nyrkio-token: ${{ secrets.NYRKIO_JWT_TOKEN }} # HTTP requests will fail for all non-core contributors that don't have their own token. # Don't want that to spoil the build, so: never-fail: true # Make results and change points public, so that any oss contributor can see them nyrkio-public: true nyrkio-settings-pvalue: 0.00001 nyrkio-settings-threshold: 0% - name: Analyze SQLITE3 result with Nyrkiö uses: nyrkio/change-detection@HEAD with: name: nightly/clickbench/sqlite3 tool: time output-file-path: clickbench-sqlite3.txt fail-on-alert: false github-token: ${{ secrets.GITHUB_TOKEN }} comment-on-alert: false comment-always: false nyrkio-token: ${{ secrets.NYRKIO_JWT_TOKEN }} never-fail: true nyrkio-public: true nyrkio-settings-pvalue: 0.00001 nyrkio-settings-threshold: 0% tpc-h-criterion: runs-on: nyrkio_perf_server_4cpu_ubuntu2404 timeout-minutes: 60 env: DB_FILE: "perf/tpc-h/TPC-H.db" steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - name: Cache TPC-H id: cache-primes uses: useblacksmith/cache@v5 with: path: ${{ env.DB_FILE }} key: tpc-h - name: Download TPC-H if: steps.cache-primes.outputs.cache-hit != 'true' env: DB_URL: "https://github.com/lovasoa/TPCH-sqlite/releases/download/v1.0/TPC-H.db" run: wget -O $DB_FILE --no-verbose $DB_URL - name: Bench run: cargo bench --bench tpc_h_benchmark 2>&1 | tee output.txt - name: Analyze benchmark result with Nyrkiö uses: nyrkio/change-detection@HEAD with: name: nightly/tpc-h tool: criterion output-file-path: output.txt github-token: ${{ secrets.GITHUB_TOKEN }} # What to do if a change is immediately detected by Nyrkiö. # Note that smaller changes are only detected with delay, usually after a change # persisted over 2-7 commits. Go to nyrkiö.com to view those or configure alerts. # Note that Nyrkiö will find all changes, also improvements. This means fail-on-alert # on pull events isn't compatible with this workflow being required to pass branch protection. fail-on-alert: false comment-on-alert: false comment-always: false # Nyrkiö configuration # Get yours from https://nyrkio.com/docs/getting-started nyrkio-token: ${{ secrets.NYRKIO_JWT_TOKEN }} # HTTP requests will fail for all non-core contributors that don't have their own token. # Don't want that to spoil the build, so: never-fail: true # Make results and change points public, so that any oss contributor can see them nyrkio-public: true # parameters of the algorithm. Note: These are global, so we only set them once and for all. # Smaller p-value = less change points found. Larger p-value = more, but also more false positives. nyrkio-settings-pvalue: 0.00001 nyrkio-settings-threshold: 0% ================================================ FILE: .github/workflows/publish-crates.yml ================================================ name: Publish Crates on: push: tags: - '**[0-9]+.[0-9]+.[0-9]+*' jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: dtolnay/rust-toolchain@stable - name: Publish crates env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: ./scripts/publish-crates.sh ================================================ FILE: .github/workflows/python.yml ================================================ name: Python on: workflow_dispatch: push: branches: - main tags: - v* pull_request: branches: - main concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: working-directory: bindings/python PIP_DISABLE_PIP_VERSION_CHECK: "true" CARGO_INCREMENTAL: "0" CARGO_NET_RETRY: 10 jobs: configure-strategy: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 outputs: python-versions: ${{ steps.gen-matrix.outputs.python-versions }} steps: - id: gen-matrix run: | echo "python-versions=[\"3.9\",\"3.13\"]" >> $GITHUB_OUTPUT test: needs: configure-strategy timeout-minutes: 30 strategy: matrix: os: - blacksmith-4vcpu-ubuntu-2404 - macos-latest - windows-latest python-version: ${{ fromJson(needs.configure-strategy.outputs.python-versions) }} runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Setup mold linker if: runner.os == 'Linux' uses: rui314/setup-mold@v1 - name: Rust cache uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Build Rust (debug) run: cargo build --verbose --locked - name: Set up Python ${{ matrix.python-version }} uses: useblacksmith/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install uv uses: useblacksmith/setup-uv@v4 with: enable-cache: true - name: Install the project working-directory: ${{ env.working-directory }} run: uv sync --all-extras --dev --all-packages - name: Run Pytest working-directory: ${{ env.working-directory }} run: uv run pytest tests/test_database.py - name: Run Pytest (async driver) working-directory: ${{ env.working-directory }} run: uv run pytest tests/test_database_aio.py - name: Run Pytest (sync) working-directory: ${{ env.working-directory }} if: runner.os != 'Windows' env: LOCAL_SYNC_SERVER: "../../target/debug/tursodb" run: uv run pytest tests/test_database_sync.py - name: Run Pytest (sync, async driver) working-directory: ${{ env.working-directory }} if: runner.os != 'Windows' env: LOCAL_SYNC_SERVER: "../../target/debug/tursodb" run: uv run pytest tests/test_database_sync_aio.py - name: Run Pytest (windows, sync) working-directory: ${{ env.working-directory }} if: runner.os == 'Windows' env: LOCAL_SYNC_SERVER: "..\\..\\target\\debug\\tursodb.exe" run: uv run pytest tests/test_database_sync.py - name: Run Pytest (windows, sync, async driver) working-directory: ${{ env.working-directory }} if: runner.os == 'Windows' env: LOCAL_SYNC_SERVER: "..\\..\\target\\debug\\tursodb.exe" run: uv run pytest tests/test_database_sync_aio.py lint: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: useblacksmith/setup-python@v6 - name: Install uv uses: useblacksmith/setup-uv@v4 with: enable-cache: true - name: Install the project run: uv sync --all-extras --dev --all-packages - name: Ruff lint run: uvx ruff check linux: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 defaults: run: working-directory: ${{ env.working-directory }} strategy: matrix: target: [x86_64] steps: - uses: actions/checkout@v3 - uses: useblacksmith/setup-python@v6 with: python-version: '3.10' - name: Build wheels uses: PyO3/maturin-action@v1 with: working-directory: ${{ env.working-directory }} target: ${{ matrix.target }} args: --profile release-official --out dist --find-interpreter sccache: 'true' manylinux: auto - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-linux path: bindings/python/dist macos-arm64: runs-on: macos-latest timeout-minutes: 30 defaults: run: working-directory: ${{ env.working-directory }} strategy: matrix: target: [aarch64] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 env: CXX: clang++ CC: clang with: python-version: '3.10' - name: Build wheels uses: PyO3/maturin-action@v1 with: working-directory: ${{ env.working-directory }} target: ${{ matrix.target }} args: --profile release-official --out dist --find-interpreter sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-macos-arm64 path: bindings/python/dist sdist: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 defaults: run: working-directory: ${{ env.working-directory }} steps: - uses: actions/checkout@v3 - name: Build sdist uses: PyO3/maturin-action@v1 with: working-directory: ${{ env.working-directory }} command: sdist args: --out dist - name: Upload sdist uses: actions/upload-artifact@v4 with: name: wheels-sdist path: bindings/python/dist release: name: Release runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 if: "startsWith(github.ref, 'refs/tags/')" needs: [linux, macos-arm64, sdist] steps: - uses: actions/download-artifact@v4 with: path: bindings/python/dist pattern: wheels-* merge-multiple: true - name: Publish to PyPI uses: PyO3/maturin-action@v1 env: MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} with: working-directory: ${{ env.working-directory }} command: upload args: --skip-existing dist/* ================================================ FILE: .github/workflows/react-native.yml ================================================ name: Build & publish @tursodatabase/sync-react-native on: workflow_dispatch: push: branches: - main tags: - v* pull_request: branches: - main env: CARGO_INCREMENTAL: "0" CARGO_NET_RETRY: 10 working-directory: bindings/react-native concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: build-ios: name: Build iOS libraries runs-on: macos-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: aarch64-apple-ios,aarch64-apple-ios-sim - name: Rust cache uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust-ios" cache-on-failure: true - name: Build iOS libraries run: make ios-build working-directory: ${{ env.working-directory }} - name: Package iOS xcframework run: make package-dylibs working-directory: ${{ env.working-directory }} - name: Upload iOS artifacts uses: actions/upload-artifact@v4 with: name: ios-libs path: | bindings/react-native/libs/ios/ if-no-files-found: error build-android: name: Build Android libraries runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,i686-linux-android - name: Rust cache uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust-android" cache-on-failure: true - name: Set up Android NDK uses: android-actions/setup-android@v3 - name: Install cargo-ndk run: cargo install cargo-ndk - name: Build Android libraries run: make android working-directory: ${{ env.working-directory }} - name: Upload Android artifacts uses: actions/upload-artifact@v4 with: name: android-libs path: | bindings/react-native/libs/android/ if-no-files-found: error build-typescript: name: Build TypeScript runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm install working-directory: ${{ env.working-directory }} - name: Build TypeScript run: npm run build working-directory: ${{ env.working-directory }} - name: Type check run: npm run typescript working-directory: ${{ env.working-directory }} - name: Upload TypeScript build uses: actions/upload-artifact@v4 with: name: typescript-build path: | bindings/react-native/lib/ if-no-files-found: error publish: name: Publish to npm runs-on: ubuntu-latest timeout-minutes: 15 needs: - build-ios - build-android - build-typescript permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Update npm run: npm install -g npm@11 - name: Download iOS artifacts uses: actions/download-artifact@v4 with: name: ios-libs path: bindings/react-native/libs/ios/ - name: Download Android artifacts uses: actions/download-artifact@v4 with: name: android-libs path: bindings/react-native/libs/android/ - name: Download TypeScript build uses: actions/download-artifact@v4 with: name: typescript-build path: bindings/react-native/lib/ - name: Install dependencies run: npm install working-directory: ${{ env.working-directory }} - name: List package contents run: | echo "=== Package contents ===" ls -la echo "=== libs/ios ===" ls -laR libs/ios/ || echo "No iOS libs" echo "=== libs/android ===" ls -laR libs/android/ || echo "No Android libs" echo "=== lib (TypeScript) ===" ls -laR lib/ || echo "No lib" working-directory: ${{ env.working-directory }} - name: Publish if: startsWith(github.ref, 'refs/tags/v') run: | if git log -1 --pretty=%B | grep "^Turso [0-9]\+\.[0-9]\+\.[0-9]\+$"; then npm publish --access public --provenance elif git log -1 --pretty=%B | grep "^Turso [0-9]\+\.[0-9]\+\.[0-9]\+"; then npm publish --access public --provenance --tag next else echo "git log structure is unexpected, skip publishing" npm publish --dry-run fi working-directory: ${{ env.working-directory }} - name: Publish (dry-run) if: ${{ !startsWith(github.ref, 'refs/tags/v') }} run: npm pack working-directory: ${{ env.working-directory }} - name: Upload package tarball if: ${{ !startsWith(github.ref, 'refs/tags/v') }} uses: actions/upload-artifact@v4 with: name: npm-package path: bindings/react-native/*.tgz ================================================ FILE: .github/workflows/release.yml ================================================ # This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist # # Copyright 2022-2024, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: # # * checks for a Git Tag that looks like a release # * builds artifacts with dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip # * on success, uploads the artifacts to a GitHub Release # # Note that the GitHub Release will be created with a generated # title/body based on your changelogs. name: Release permissions: "contents": "write" # This task will run whenever you push a git tag that looks like a version # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION # must be a Cargo-style SemVer Version (must have at least major.minor.patch). # # If PACKAGE_NAME is specified, then the announcement will be for that # package (erroring out if it doesn't have the given version or isn't dist-able). # # If PACKAGE_NAME isn't specified, then the announcement will be for all # (dist-able) packages in the workspace with that version (this mode is # intended for workspaces with only one dist-able package, or with all dist-able # packages versioned/released in lockstep). # # If you push multiple tags at once, separate instances of this workflow will # spin up, creating an independent announcement for each one. However, GitHub # will hard limit this to 3 tags per commit, as it will assume more tags is a # mistake. # # If there's a prerelease-style suffix to the version, then the release(s) # will be marked as a prerelease. on: pull_request: push: tags: - '**[0-9]+.[0-9]+.[0-9]+*' jobs: # Run 'dist plan' (or host) to determine what tasks we need to do plan: runs-on: "ubuntu-22.04" outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ !github.event.pull_request && github.ref_name || '' }} tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} publishing: ${{ !github.event.pull_request }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: persist-credentials: false submodules: recursive - name: Install dist # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.2/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@v4 with: name: cargo-dist-cache path: ~/.cargo/bin/dist # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* # but also really annoying to build CI around when it needs secrets to work right.) - id: plan run: | dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json echo "dist ran successfully" cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" uses: actions/upload-artifact@v4 with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json # Build and packages all the platform-specific things build-local-artifacts: name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) # Let the initial task tell us to not run (currently very blunt) needs: - plan if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} strategy: fail-fast: false # Target platforms/runners are computed by dist in create-release. # Each member of the matrix has the following arguments: # # - runner: the github runner # - dist-args: cli flags to pass to dist # - install-dist: expression to run to install dist on the runner # # Typically there will be: # - 1 "global" task that builds universal installers # - N "local" tasks that build each platform's binaries and platform-specific installers matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} runs-on: ${{ matrix.runner }} container: ${{ matrix.container && matrix.container.image || null }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json permissions: "attestations": "write" "contents": "read" "id-token": "write" steps: - name: enable windows longpaths run: | git config --global core.longpaths true - uses: actions/checkout@v4 with: persist-credentials: false submodules: recursive - name: Install Rust non-interactively if not already installed if: ${{ matrix.container }} run: | if ! command -v cargo > /dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y echo "$HOME/.cargo/bin" >> $GITHUB_PATH fi - name: Install dist run: ${{ matrix.install_dist.run }} # Get the dist-manifest - name: Fetch local artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true - name: Install dependencies run: | ${{ matrix.packages_install }} - name: Build artifacts run: | # Actually do builds and make zips and whatnot dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json echo "dist ran successfully" - name: Attest uses: actions/attest-build-provenance@v2 with: subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*" - id: cargo-dist name: Post-build # We force bash here just because github makes it really hard to get values up # to "real" actions without writing to env-vars, and writing to env-vars has # inconsistent syntax between shell and powershell. shell: bash run: | # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" uses: actions/upload-artifact@v4 with: name: artifacts-build-local-${{ join(matrix.targets, '_') }} path: | ${{ steps.cargo-dist.outputs.paths }} ${{ env.BUILD_MANIFEST_NAME }} # Build and package all the platform-agnostic(ish) things build-global-artifacts: needs: - plan - build-local-artifacts runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - uses: actions/checkout@v4 with: persist-credentials: false submodules: recursive - name: Install cached dist uses: actions/download-artifact@v4 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true - id: cargo-dist shell: bash run: | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json echo "dist ran successfully" # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" uses: actions/upload-artifact@v4 with: name: artifacts-build-global path: | ${{ steps.cargo-dist.outputs.paths }} ${{ env.BUILD_MANIFEST_NAME }} # Determines if we should publish/announce host: needs: - plan - build-local-artifacts - build-global-artifacts # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: "ubuntu-22.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: - uses: actions/checkout@v4 with: persist-credentials: false submodules: recursive - name: Install cached dist uses: actions/download-artifact@v4 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true - id: host shell: bash run: | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" uses: actions/upload-artifact@v4 with: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json # Create a GitHub Release while uploading all files to it - name: "Download GitHub Artifacts" uses: actions/download-artifact@v4 with: pattern: artifacts-* path: artifacts merge-multiple: true - name: Cleanup run: | # Remove the granular manifests rm -f artifacts/*-dist-manifest.json - name: Create GitHub Release env: PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" RELEASE_COMMIT: "${{ github.sha }}" run: | # Write and read notes from a file to avoid quoting breaking things echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* announce: needs: - plan - host # use "always() && ..." to allow us to wait for all publish jobs while # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' }} runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: persist-credentials: false submodules: recursive ================================================ FILE: .github/workflows/rust.yml ================================================ name: Rust on: workflow_dispatch: push: branches: ["main"] pull_request: branches: ["main"] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: "0" CARGO_NET_RETRY: 10 jobs: license-check: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@cargo-deny - run: cargo deny check licenses cargo-fmt-check: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Check formatting run: cargo fmt --check - name: Check formatting (fuzz) run: cd fuzz && cargo fmt --check check-bindings: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Install bindgen run: cargo install --version 0.71.1 bindgen-cli - name: Regenerate sdk-kit bindings run: cd sdk-kit && ./bindgen.sh - name: Regenerate sync/sdk-kit bindings run: cd sync/sdk-kit && ./bindgen.sh - name: Check bindings are up to date run: | if ! git diff --exit-code sdk-kit/src/bindings.rs sync/sdk-kit/src/bindings.rs; then echo "Bindings are out of date. Run ./bindgen.sh in sdk-kit/ and sync/sdk-kit/ and commit the changes." exit 1 fi build-native: timeout-minutes: ${{ matrix.os == 'windows-latest' && 120 || 30 }} strategy: matrix: os: [blacksmith-4vcpu-ubuntu-2404, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview - name: Setup mold linker if: runner.os == 'Linux' uses: "./.github/shared/setup-mold" - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Install cargo-nextest uses: taiki-e/install-action@nextest - name: Install cargo-llvm-cov if: runner.os == 'Linux' uses: taiki-e/install-action@cargo-llvm-cov - name: Set up Python 3.10 uses: useblacksmith/setup-python@v6 with: python-version: "3.10" - name: Build run: cargo build --verbose --locked --all-features - name: Test (nextest with coverage) if: runner.os == 'Linux' env: RUST_LOG: ${{ runner.debug && 'turso_core::storage=trace' || '' }} LOCAL_SYNC_SERVER: "../../target/debug/tursodb" # for some reason, test(test_busy_snapshot_immediate) hung with coverage - so let's exclude it from the run for now run: | cargo llvm-cov nextest --workspace --all-features --locked --no-fail-fast -E 'not binary(fuzz_tests) and not test(test_busy_snapshot_immediate)' --failure-output=final --html --output-dir coverage echo "::group::Coverage Summary" cargo llvm-cov report | tee coverage/summary.txt echo "::endgroup::" timeout-minutes: 20 - name: Test (nextest) if: runner.os != 'Linux' env: RUST_LOG: ${{ runner.debug && 'turso_core::storage=trace' || '' }} LOCAL_SYNC_SERVER: "../../target/debug/tursodb" run: cargo nextest run --workspace --all-features --locked --no-fail-fast -E 'not binary(fuzz_tests)' --failure-output=final timeout-minutes: 20 - name: Test integrity_check corruption and short reads (no checksum feature because it interferes with the test) run: cargo nextest run -p core_tester --locked --no-fail-fast -E 'test(integrity_check) | test(short_read)' --failure-output=final timeout-minutes: 5 - name: Test doctests env: LOCAL_SYNC_SERVER: "../../target/debug/tursodb" run: cargo test --workspace --all-features --doc --locked - name: Upload coverage report if: runner.os == 'Linux' uses: actions/upload-artifact@v4 with: name: coverage-report path: coverage/ retention-days: 14 shuttle-tests: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Setup mold linker uses: "./.github/shared/setup-mold" - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust-shuttle" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Install cargo-nextest uses: taiki-e/install-action@nextest - name: Run shuttle tests run: | make test-shuttle # stress-nondeterminism-check: # runs-on: blacksmith-4vcpu-ubuntu-2404 # timeout-minutes: 15 # steps: # - uses: actions/checkout@v4 # - uses: dtolnay/rust-toolchain@stable # - name: Setup mold linker # uses: "./.github/shared/setup-mold" # - uses: Swatinem/rust-cache@v2 # with: # prefix-key: "v1-rust-shuttle" # cache-on-failure: true # - uses: "./.github/shared/setup-sccache" # - name: Check for uncontrolled nondeterminism # env: # RUSTFLAGS: "--cfg tokio_unstable --cfg shuttle" # run: | # cargo run -p turso_stress -- --check-uncontrolled-nondeterminism clippy: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - name: Clippy run: | cargo clippy --workspace --all-features --all-targets --locked -- --deny=warnings simulator: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 35 strategy: matrix: profile: - name: default iterations: 10 args: "--maximum-tests 1000 --min-tick 10 --max-tick 50 --io-backend=memory" - name: InsertHeavy iterations: 5 args: "--maximum-tests 1000 --min-tick 10 --max-tick 50 --profile write_heavy --io-backend=memory" - name: InsertHeavySpill iterations: 5 args: "--maximum-tests 1000 --min-tick 10 --max-tick 50 --profile write_heavy_spill --io-backend=memory" - name: Faultless iterations: 10 args: "--maximum-tests 1000 --min-tick 10 --max-tick 50 --profile faultless --io-backend=memory" - name: IOUring InsertHeavySpill iterations: 5 args: "--io-backend=io-uring --maximum-tests 1000 --profile=write_heavy_spill" - name: IOUring Differential iterations: 10 args: "--io-backend=io-uring --maximum-tests 1000 --differential" - name: Differential iterations: 10 args: "--maximum-tests 1000 --differential --io-backend=memory" steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Setup mold linker uses: "./.github/shared/setup-mold" - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" # Run these with for loops (instead of --loop flag in sim) so that each iteration gets a separate seed -- this produces much easier-to-debug traces when there's a failure since we don't have to potentially # run 10 iterations of the same seed to get to the failure - name: Simulator ${{ matrix.profile.name }} run: | for i in $(seq 1 ${{ matrix.profile.iterations }}); do ./scripts/run-sim ${{ matrix.profile.args }} || exit 1 done # TODO: find out why this blows the stack so quickly every time.. # e.g. https://github.com/tursodatabase/turso/actions/runs/22370904813/job/64749257476?pr=5588 # simulator-windows: # runs-on: windows-latest # timeout-minutes: 120 # strategy: # matrix: # profile: # - name: default # iterations: 10 # args: "--maximum-tests 1000 --min-tick 10 --max-tick 50 --io-backend=memory" # - name: InsertHeavy # iterations: 5 # args: "--maximum-tests 1000 --min-tick 10 --max-tick 50 --profile write_heavy --io-backend=memory" # - name: InsertHeavySpill # iterations: 5 # args: "--maximum-tests 1000 --min-tick 10 --max-tick 50 --profile write_heavy_spill --io-backend=memory" # - name: Faultless # iterations: 10 # args: "--maximum-tests 1000 --min-tick 10 --max-tick 50 --profile faultless --io-backend=memory" # - name: WindowsIOCP InsertHeavySpill # iterations: 5 # args: "--io-backend=experimental_win_iocp --maximum-tests 1000 --profile=write_heavy_spill" # - name: WindowsIOCP Differential # iterations: 10 # args: "--io-backend=experimental_win_iocp --maximum-tests 1000 --differential" # - name: Differential # iterations: 10 # args: "--maximum-tests 5000 --differential --io-backend=memory" # steps: # - uses: actions/checkout@v4 # - uses: dtolnay/rust-toolchain@stable # - uses: Swatinem/rust-cache@v2 # with: # prefix-key: "v1-rust" # cache-on-failure: true # - uses: "./.github/shared/setup-sccache" # - name: Windows Simulator ${{ matrix.profile.name }} # shell: pwsh # run: | # for ($i = 1; $i -le ${{ matrix.profile.iterations }}; $i++) { # & ./scripts/run-sim.ps1 ${{ matrix.profile.args }} # if ($LASTEXITCODE -ne 0) { # exit $LASTEXITCODE # } # } # TODO: for now leave this commented out, as it always fails # differential-fuzzer: # runs-on: blacksmith-4vcpu-ubuntu-2404 # timeout-minutes: 30 # steps: # - uses: actions/checkout@v4 # - uses: dtolnay/rust-toolchain@stable # - name: Setup mold linker # uses: rui314/setup-mold@v1 # - uses: Swatinem/rust-cache@v2 # with: # prefix-key: "v1-rust" # cache-on-failure: true # - name: Setup sccache # uses: mozilla-actions/sccache-action@v0.0.9 # - name: Run differential_fuzzer # env: # SCCACHE_GHA_ENABLED: "true" # RUSTC_WRAPPER: "sccache" # run: | # cargo run -p differential-fuzzer -- -n 1000 loop 10 test-limbo: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Setup mold linker uses: "./.github/shared/setup-mold" - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Install uv uses: useblacksmith/setup-uv@v4 with: enable-cache: true - name: Set up Python run: uv python install - uses: "./.github/shared/install_sqlite" - name: Test run: make test timeout-minutes: 20 concurrent-simulator: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Setup mold linker uses: "./.github/shared/setup-mold" - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: "./.github/shared/setup-sccache" - name: Run turso_whopper regression tests run: cargo test -p turso_whopper --test regression_tests timeout-minutes: 5 - name: Run turso_whopper run: | cargo build -p turso_whopper for i in $(seq 1 100); do ./target/debug/turso_whopper --reopen-probability 0.001 || exit 1 done test-sqlite: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: "./.github/shared/install_sqlite" - name: build SQLite test extensions run: cargo build --package limbo_sqlite_test_ext --locked - name: Test run: SQLITE_EXEC="sqlite3" make test-compat ================================================ FILE: .github/workflows/rust_perf.yml ================================================ name: Rust Benchmarks+Nyrkiö on: workflow_dispatch: push: branches: ["main", "master", "notmain"] pull_request: branches: ["main", "notmain", "master"] env: CARGO_TERM_COLOR: never CARGO_INCREMENTAL: "0" CARGO_NET_RETRY: 10 jobs: bench: runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false matrix: include: - bench: benchmark features: "--features bench" - bench: mvcc_benchmark features: "--features bench" - bench: json_benchmark features: "--features bench" - bench: sql_functions features: "--features bench" - bench: hash_spill_benchmark features: "--features bench" - bench: write_perf_benchmark features: "--features bench" - bench: fts_benchmark features: "--features bench" - bench: parser_benchmark features: "" - bench: graph_queries_benchmark features: "--features bench" steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - name: Generate graph-queries DB if: matrix.bench == 'graph_queries_benchmark' run: | python3 perf/graph-queries/generate_seed.py | sqlite3 perf/graph-queries/graph-queries.db cp perf/graph-queries/graph-queries.db perf/graph-queries/graph-queries-analyzed.db sqlite3 perf/graph-queries/graph-queries-analyzed.db "ANALYZE;" - name: Bench ${{ matrix.bench }} run: cargo bench --bench ${{ matrix.bench }} ${{ matrix.features }} 2>&1 | tee output.txt - uses: actions/upload-artifact@v4 with: name: bench-output-${{ matrix.bench }} path: output.txt bench-report: runs-on: ubuntu-latest needs: bench if: always() && !cancelled() steps: - uses: actions/download-artifact@v4 with: pattern: bench-output-* - name: Merge outputs run: cat bench-output-*/output.txt > output.txt - name: Analyze benchmark result with Nyrkiö uses: nyrkio/change-detection@HEAD with: name: turso tool: criterion output-file-path: output.txt github-token: ${{ secrets.GITHUB_TOKEN }} # What to do if a change is immediately detected by Nyrkiö. # Note that smaller changes are only detected with delay, usually after a change # persisted over 2-7 commits. Go to nyrkiö.com to view those or configure alerts. # Note that Nyrkiö will find all changes, also improvements. This means fail-on-alert # on pull events isn't compatible with this workflow being required to pass branch protection. fail-on-alert: false comment-on-alert: false comment-always: false # Nyrkiö configuration # Get yours from https://nyrkio.com/docs/getting-started nyrkio-token: ${{ secrets.NYRKIO_JWT_TOKEN }} # HTTP requests will fail for all non-core contributors that don't have their own token. # Don't want that to spoil the build, so: never-fail: true # Make results and change points public, so that any oss contributor can see them nyrkio-public: true # parameters of the algorithm. Note: These are global, so we only set them once and for all. # Smaller p-value = less change points found. Larger p-value = more, but also more false positives. nyrkio-settings-pvalue: 0.00001 nyrkio-settings-threshold: 0% clickbench: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - uses: useblacksmith/setup-node@v5 with: node-version: 20 - name: Clickbench run: make clickbench - name: Analyze TURSO result with Nyrkiö uses: nyrkio/change-detection@HEAD with: name: clickbench/turso tool: time output-file-path: clickbench-tursodb.txt github-token: ${{ secrets.GITHUB_TOKEN }} # What to do if a change is immediately detected by Nyrkiö. # Note that smaller changes are only detected with delay, usually after a change # persisted over 2-7 commits. Go to nyrkiö.com to view those or configure alerts. # Note that Nyrkiö will find all changes, also improvements. This means fail-on-alert # on pull events isn't compatible with this workflow being required to pass branch protection. fail-on-alert: false comment-on-alert: false comment-always: false # Nyrkiö configuration # Get yours from https://nyrkio.com/docs/getting-started nyrkio-token: ${{ secrets.NYRKIO_JWT_TOKEN }} # HTTP requests will fail for all non-core contributors that don't have their own token. # Don't want that to spoil the build, so: never-fail: true # Make results and change points public, so that any oss contributor can see them nyrkio-public: true nyrkio-settings-pvalue: 0.00001 nyrkio-settings-threshold: 0% - name: Analyze SQLITE3 result with Nyrkiö uses: nyrkio/change-detection@HEAD with: name: clickbench/sqlite3 tool: time output-file-path: clickbench-sqlite3.txt github-token: ${{ secrets.GITHUB_TOKEN }} fail-on-alert: false comment-on-alert: false comment-always: false nyrkio-token: ${{ secrets.NYRKIO_JWT_TOKEN }} never-fail: true nyrkio-public: true nyrkio-settings-pvalue: 0.00001 nyrkio-settings-threshold: 0% tpc-h-criterion: runs-on: ubuntu-latest timeout-minutes: 60 env: DB_FILE: "perf/tpc-h/TPC-H.db" steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - name: Cache TPC-H id: cache-primes uses: useblacksmith/cache@v5 with: path: ${{ env.DB_FILE }} key: tpc-h - name: Download TPC-H if: steps.cache-primes.outputs.cache-hit != 'true' env: DB_URL: "https://github.com/lovasoa/TPCH-sqlite/releases/download/v1.0/TPC-H.db" run: wget -O $DB_FILE --no-verbose $DB_URL - name: Bench run: cargo bench --bench tpc_h_benchmark --locked 2>&1 | tee output.txt - name: Analyze benchmark result with Nyrkiö uses: nyrkio/change-detection@HEAD with: name: tpc-h tool: criterion output-file-path: output.txt github-token: ${{ secrets.GITHUB_TOKEN }} # What to do if a change is immediately detected by Nyrkiö. # Note that smaller changes are only detected with delay, usually after a change # persisted over 2-7 commits. Go to nyrkiö.com to view those or configure alerts. # Note that Nyrkiö will find all changes, also improvements. This means fail-on-alert # on pull events isn't compatible with this workflow being required to pass branch protection. fail-on-alert: false comment-on-alert: false comment-always: false # Nyrkiö configuration # Get yours from https://nyrkio.com/docs/getting-started nyrkio-token: ${{ secrets.NYRKIO_JWT_TOKEN }} # HTTP requests will fail for all non-core contributors that don't have their own token. # Don't want that to spoil the build, so: never-fail: true # Make results and change points public, so that any oss contributor can see them nyrkio-public: true # parameters of the algorithm. Note: These are global, so we only set them once and for all. # Smaller p-value = less change points found. Larger p-value = more, but also more false positives. nyrkio-settings-pvalue: 0.00001 nyrkio-settings-threshold: 0% tpc-h: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - name: TPC-H env: RUST_LOG: "off" run: ./perf/tpc-h/benchmark.sh vfs-bench-compile: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: prefix-key: "v1-rust" cache-on-failure: true - name: Build run: cargo build --release --verbose --locked - name: Install uv uses: useblacksmith/setup-uv@v4 with: enable-cache: true - name: Set up Python run: uv python install - name: Install the project run: uv sync --package turso_test - name: Run benchmark run: uv run bench-vfs "SELECT 1;" 100 ================================================ FILE: .github/workflows/sqltest.yml ================================================ name: SQL Tests on: workflow_dispatch: push: branches: ["main"] pull_request: branches: ["main"] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: CARGO_TERM_COLOR: always jobs: sqltest-check: name: Check .sqltest syntax runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 10 steps: - uses: actions/checkout@v3 - uses: useblacksmith/rust-cache@v3 with: prefix-key: "v1-rust" - name: Build test runner run: make -C testing/sqltests build-runner - name: Check test syntax run: make -C testing/sqltests check sqltest-run-cli: name: Run SQL tests (CLI backend) runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v3 - uses: useblacksmith/rust-cache@v3 with: prefix-key: "v1-rust" - uses: "./.github/shared/setup-sccache" - uses: "./.github/shared/install_sqlite" - name: Build tursodb run: make -C testing/sqltests build-tursodb - name: Build test runner run: make -C testing/sqltests build-runner - name: Run tests run: make -C testing/sqltests run-cli CROSS_CHECK_BINARY=sqlite3 - name: Run tests (MVCC) run: make -C testing/sqltests run-cli MVCC=1 sqltest-run-rust: name: Run SQL tests (Rust backend) runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v3 - uses: useblacksmith/rust-cache@v3 with: prefix-key: "v1-rust" - name: Build test runner run: make -C testing/sqltests build-runner - name: Run tests run: make -C testing/sqltests run-rust - name: Run tests (MVCC) run: make -C testing/sqltests run-rust MVCC=1 sqltest-run-js: name: Run SQL tests (JS backend) runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 defaults: run: working-directory: bindings/javascript steps: - uses: actions/checkout@v3 - uses: useblacksmith/rust-cache@v3 with: prefix-key: "v1-rust" - name: Setup node uses: useblacksmith/setup-node@v5 with: node-version: 20 - uses: "./.github/shared/setup-sccache" - name: Install JS dependencies run: yarn install - name: Build common package run: yarn workspace @tursodatabase/database-common build - name: Build native bindings run: yarn workspace @tursodatabase/database napi-build --target x86_64-unknown-linux-gnu - name: Build TypeScript run: yarn workspace @tursodatabase/database tsc-build - name: Build test runner working-directory: testing/sqltests run: cargo build --release - name: Run tests working-directory: testing/sqltests run: make run-js - name: Run tests (MVCC) working-directory: testing/sqltests run: make run-js MVCC=1 sqltest-sqlite: name: Run SQL tests (SQLite) runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - uses: actions/checkout@v3 - uses: useblacksmith/rust-cache@v3 with: prefix-key: "v1-rust" - uses: "./.github/shared/install_sqlite" - name: Build test runner run: make -C testing/sqltests build-runner - name: Run tests against SQLite run: make -C testing/sqltests test-sqlite ================================================ FILE: .github/workflows/stale.yml ================================================ name: Stale on: schedule: - cron: '0 0 * * *' # Runs every day at midnight UTC permissions: issues: read pull-requests: write jobs: stale: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 30 steps: - name: Close stale pull requests uses: actions/stale@v6 with: repo-token: ${{ secrets.GH_TOKEN }} operations-per-run: 1000 ascending: true stale-pr-message: 'This pull request has been marked as stale due to inactivity. It will be closed in 7 days if no further activity occurs.' close-pr-message: 'This pull request has been closed due to inactivity. Please feel free to reopen it if you have further updates.' days-before-issue-stale: 365 days-before-stale: 30 days-before-close: 7 ================================================ FILE: .github/workflows/turso-serverless.yml ================================================ name: turso-serverless on: workflow_dispatch: push: branches: [ main ] paths: - 'packages/turso-serverless/**' - '.github/workflows/turso-serverless.yml' pull_request: branches: [ main ] paths: - 'packages/turso-serverless/**' - '.github/workflows/turso-serverless.yml' env: working-directory: packages/turso-serverless jobs: build: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 20 defaults: run: working-directory: ${{ env.working-directory }} steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: useblacksmith/setup-node@v5 with: node-version: '20' cache: 'npm' cache-dependency-path: ${{ env.working-directory }}/package-lock.json - name: Install dependencies run: npm ci - name: Build run: npm run build ================================================ FILE: .github.json ================================================ { "penberg": { "name": "Pekka Enberg", "email": "penberg@iki.fi" }, "pereman2": { "name": "Pere Diaz Bou", "email": "pere-altea@homail.com" }, "jussisaurio": { "name": "Jussi Saurio", "email": "jussi.saurio@gmail.com" } } ================================================ FILE: .gitignore ================================================ /target /target.noindex /.idea /.vscode /.sqlite3 **/target *.so *.dylib *.ipynb *.o *.sqlite # Python .mypy_cache/ .pytest_cache/ .ruff_cache/ .venv*/ __pycache__/ .coverage venv env .env .venv dist/ .tmp/ *.db **/*.db-wal **/*.db-shm **/*-wal # perf **/*/Mobibench perf/mobibench/plot/results.csv perf/mobibench/plot/mobibench.pdf perf/mobibench/plot/uv.lock # OS .DS_Store # Javascript **/node_modules/ # testing testing/limbo_output.txt **/limbo_output.txt testing/*.log .bugbase limbostress.log simulator.log **/*.txt profile.json.gz simulator-output/ tests/*.sql stress-go/stress-go &1 bisected.sql *.log *.db-log settings.local.json .claude/agents/ .build-hash # Track empty wals for read only databases !testing/sqltests/database/* testing/sqltests/database/integrity_*.db # For simulator redo test.sql # for elle edn transaction history *.edn elle-history-results/ custom-types-fuzzer-output/ CLAUDE.md bindings/python/dev.py bindings/python/implementation-test/ ================================================ FILE: .python-version ================================================ 3.13 ================================================ FILE: AGENTS.md ================================================ # Turso Agent Guidelines SQLite rewrite in Rust. 40+ crate workspace. ## Quick Reference ```bash cargo build # build. never build with release. cargo test # rust unit/integration tests cargo fmt # format (required) cargo clippy --workspace --all-features --all-targets -- --deny=warnings # lint cargo run -q --bin tursodb -- -q # run the interactive cli make test # TCL compat + sqlite3 + extensions + MVCC make test-single TEST=foo.test # single TCL test make -C testing/sqltests run-rust # sqltest runner (preferred for new tests) scripts/diff.sh "SQL" [label] # compare sqlite3 vs tursodb output ``` ## Structure ``` limbo/ ├── core/ # Database engine (translate/, storage/, vdbe/, io/, mvcc/) ├── parser/ # SQL parser (lexer, AST, grammar) ├── cli/ # tursodb CLI (REPL, MCP server, sync server) ├── bindings/ # Python, JS, Java, .NET, Go, Rust ├── extensions/ # crypto, regexp, csv, fuzzy, ipaddr, percentile ├── testing/ # simulator/, concurrent-simulator/, differential-oracle/ ├── sync/ # engine/, sdk-kit/ (Turso Cloud sync) ├── sdk-kit/ # High-level SDK abstraction └── tools/ # dbhash utility ``` ## Where to Look | Task | Location | Notes | |------|----------|-------| | Query execution | `core/vdbe/execute.rs` | 12k LOC bytecode interpreter | | SQL compilation | `core/translate/` | AST → bytecode, optimizer in `optimizer/` | | B-tree/pages | `core/storage/btree.rs` | 10k LOC, SQLite-compatible format | | WAL/durability | `core/storage/wal.rs` | Write-ahead log, checkpointing | | SQL parsing | `parser/src/parser.rs` | 11k LOC recursive descent | | Add extension | `extensions/core/` | ExtensionApi, scalar/aggregate/vtab traits | | Add binding | `bindings/` | PyO3, NAPI, JNI, FRB, CGO patterns | | Deterministic tests | `testing/simulator/` | Fault injection, differential testing | | New SQL tests | `testing/sqltests/tests/` | `.sqltest` format preferred | | Quick sqlite3 diff | `scripts/diff.sh` | Compare sqlite3 vs tursodb output for a query | | MVCC testing REPL | `cli/mvcc_repl.rs` | Multi-conn concurrent txn testing REPL | ## Guides - **[Testing](docs/agent-guides/testing.md)** - test types, when to use, how to write - **[Code Quality](docs/agent-guides/code-quality.md)** - correctness rules, Rust patterns, comments - **[Debugging](docs/agent-guides/debugging.md)** - bytecode comparison, logging, sanitizers - **[PR Workflow](docs/agent-guides/pr-workflow.md)** - commits, CI, dependencies - **[Transaction Correctness](docs/agent-guides/transaction-correctness.md)** - WAL, checkpointing, concurrency - **[Storage Format](docs/agent-guides/storage-format.md)** - file format, B-trees, pages - **[Async I/O Model](docs/agent-guides/async-io-model.md)** - IOResult, state machines, re-entrancy - **[MVCC](docs/agent-guides/mvcc.md)** - experimental multi-version concurrency (WIP) ## Core Principles 1. **Correctness paramount.** Production DB, not a toy. Crash > corrupt 2. **SQLite compatibility.** Compare bytecode with `EXPLAIN` 3. **Every change needs a test.** Must fail without change, pass with it 4. **Assert invariants.** Don't silently fail. Don't hedge with if-statements 5. **Own your regressions.** If tests fail after your change, they are your regressions. Debug them directly. Never stash/revert to "check if they fail on main" — that wastes time and is categorically banned. 6. **Validate your hypotheses.**: If you suspect a given cause for a bug, validate it and provide incontrovertible evidence. NEVER make unearned assumptions. ## CI Note Running in GitHub Action? Max-turns limit in `.github/workflows/claude.yml`. OK to push WIP and continue in another action. Stay focused, avoid rabbit holes. ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## 0.5.0 -- 2026-03-04 ### Added * github/release: Add retry logic to network-dependent install steps (Pekka Enberg") * github/release: Add retry logic to network-dependent install steps (Pekka Enberg) * Add JS and Python concurrent write examples (Avinash Sajjanshetty) * perf/react-native: add c++ getAllRows() helper (Jussi Saurio) * perf: add compiler branching hints (Preston Thorpe) * Add concurrent writes example in Rust (Avinash Sajjanshetty) * Add SQLAlchemy dialect for Python bindings (Orthel Toralen) * mvcc: add test that asserts no ghost commits after Busy errors happen (Jussi Saurio) * core: Support views in MVCC mode (RJ Barman) * add return type checking for Function calls for `CHECK` constraints (Pedro Muniz) * Add mobibench to gitignore (Preston Thorpe) * Add native TCL extension for in-process SQLite test harness (Ahmad Alhour) * bindings/rust: Add statement and row column inspection APIs (Pekka Enberg) * cli/sync: add read timeout to prevent deadlock on Windows (Orthel Toralen) * add SQL language reference documentation (Glauber Costa) * add stable PRAGMA capture_data_changes_conn name and keep old one around for compatibility (Nikita Sivukhin) * Improve Elle, add rw-register model and chaotic workloads (Avinash Sajjanshetty) * Add Mobibench evaluation: SQLite vs Turso (Pekka Enberg) * core/io: Add POSIX fallback for io_uring ftruncate (Pekka Enberg) * temporarily remove new windows simulation runs (Preston Thorpe) * feat: support encryption keys for attached databases via URI params (Glauber Costa) * Finish implementing RAISE (Preston Thorpe) * autofix: implement FkCheck opcode to prevent RETURNING rows after FK violations (Jussi Saurio) * Support custom types (Glauber Costa) * tests/fuzz: add fuzz_iterations helper and cut CI fuzz iters to 50% (Jussi Saurio) * Add STRICT table support to differential fuzzer (Glauber Costa) * Add support for RAISE() function (Glauber Costa) * Add ATTACH support to simulator (Glauber Costa) * partially implement FULL OUTER + LEFT and RIGHT hash joins (Preston Thorpe) * EXPLAIN QUERY PLAN: fix incorrect join order, add missing annotations, convert some snapshot tests to EQP-only (Jussi Saurio) * bindings/rust: add old multithreaded bug reproducers as mvcc regression tests (Jussi Saurio) * core/mvcc: Implement Hekaton commit dependency tracking (Pere Diaz Bou) * tests: add CTE regression tests for issue #4637 (Jussi Saurio) * add serial test crate to make `fuzz_pending_byte_database` serial for mvcc and wal (Pedro Muniz) * Add more tests to foreign_keys.sqltest (Jussi Saurio) * tests: add regression tests for expression index UPDATE correctness (Pedro Muniz) * Implement named savepoints (Preston Thorpe) * feat: Add materialized views support to Rust bindings and test runner (Martin Mauch) * Fix STRICT ADD COLUMN default type validation (Kumar Ujjawal) * Fix freelist_count dirty leak and add regression test (Kumar Ujjawal) * core: reimplement PRAGMA integrity_check as VDBE and add sqlite parity corpus (Jussi Saurio) * Restore new antithesis assertions (Mikaël Francoeur) * Use new Antithesis assertions (Mikaël Francoeur") * fix: NEW.rowid_alias in UPDATE Trigger WHEN Clause Evaluates to NULL (Jussi Saurio) * Add MVCC Antithesis stress test configurations (Pekka Enberg) * add tests verifying check constraints are evaluated before ON CONFLICT clauses (Jussi Saurio) * Improve support for attach databases (Glauber Costa) * translate: support row-valued comparison expressions (Jussi Saurio) * fix: add `changes` support to MVCC and adjust code to always emit it in one place only (Pedro Muniz) * Add regression tests for correlated subqueries using MULTI-INDEX AND plans (Jussi Saurio) * add tests verifying INSERT RETURNING does not skip index updates (Jussi Saurio) * btree: add property tests & make a few functions not panic on corruption (Jussi Saurio) * Use new Antithesis assertions (Mikaël Francoeur") * add regression tests for correlated subqueries registers fix (Pedro Muniz) * Use new Antithesis assertions (Mikaël Francoeur) * Add regression tests for concurrent commit deadlock in VDBE yield-spin loop (Avinash Sajjanshetty) * Add TPC-H benchmark README and plotting scripts (Pekka Enberg) * fix: Panic on ALTER TABLE ADD COLUMN With Invalid Collation Name #5305 (Jussi Saurio) * ci: add license checker to disallow GPL dependencies (Srinivas A) * Add some makefile commands for test runner in repo root (Preston Thorpe) * add exp-index-method to rust backend and for testing (Pavan Nambi) * Add support for FROM-clause subquery materialization and CTE materialization (Jussi Saurio) * Add new antithesis assertions (Mikaël Francoeur) * concurrent-simulator: Add UPDATE and DELETE workloads (Pekka Enberg) * Implement PRAGMA function_list with enum-derived function probing (Glauber Costa) * Add hermitage tests (Avinash Sajjanshetty) * Fix corruption errors caused by stale cursor positioning + add upsert support to simulator (Pavan Nambi) * make transactions supported in compat.md (Glauber Costa) * Add REGEXP operator support (Glauber Costa) * Add test case for MVCC dual cursor transaction isolation (Jussi Saurio) * Implement CHECK constraints (Glauber Costa) * Replace panic/todo/unimplemented with proper errors (Mikaël Francoeur) * testing/runner: Add @skip-if sqlite directive (Preston Thorpe) * Implement multi-index scan access method (Preston Thorpe) * add tests for ORDER BY aggregate not in SELECT with GROUP BY (Mikaël Francoeur) * github: Add workflow to publish crates to crates.io on tags (Pekka Enberg) * javascript: Add support for Statement.reader property (Pekka Enberg) * javascript: Fix implicit connect support (Pekka Enberg) * Add busy timeout and Connector pattern for go bindings (Preston Thorpe) * core/mvcc: add to destroyed_tables when insert+delete in same checkpoint (Pere Diaz Bou) * core/vdbe: fix newrowid Start state change on IO (Pere Diaz Bou) * add duration and fix notebook name (Mikaël Francoeur) * Add support for PRAGMA synchronous=NORMAL (Jussi Saurio) * Use new Antithesis trigger action (Mikaël Francoeur) * core/mvcc: update schema root pages with new allocated btree pages (Pere Diaz Bou) * add --experimental-attach flag to test runner backends (Mikaël Francoeur) * add index knowledge skill (Pedro Muniz) * Implement `vacuum_into` op (Avinash Sajjanshetty) * fix: only initialize result registers for subqueries that have not been evaluated yet (Pedro Muniz) * fix: add bounds checking to `cell_interior_read_left_child_page` (Avinash Sajjanshetty) * Add Namespace field for go-sync package (Preston Thorpe) * Add "release-official" compilation profile (Jussi Saurio) * Add support for local encryption in SDKs (Avinash Sajjanshetty) * fix build.rs rebuilding every time due to always getting new timestamp (Jussi Saurio) * add turso test runner to workspace member (Pedro Muniz) * fix dockerfile to add dbhash tool (Pedro Muniz) * add ReadableName strategy for differential fuzzer so that we mostly generate readable identifiers (Pedro Muniz) * implement `PRAGMA temp_store` optimization (Preston Thorpe) * Add `dbhash` tool calculate hash of logical db content (Avinash Sajjanshetty) * Add local encryption support for Go (Avinash Sajjanshetty) * mac: introduce PRAGMA fullfsync (Jussi Saurio) * Reintroduce record header caching (Jussi Saurio") * Add new insert/write performance benchmarks (Preston Thorpe) * compat.md: add generated + md reformat (Pere Diaz Bou) * Add support for remote encryption in sync operations (Avinash Sajjanshetty) * core/mvcc: add savepoints to mvcc with `begin/end_statment` (Pere Diaz Bou) * Add benchmark for hash table/spilling performance (Preston Thorpe) * Reintroduce record header caching (Jussi Saurio"' from Jussi Saurio) * Reintroduce record header caching (Jussi Saurio") * Reintroduce record header caching (Jussi Saurio) * Implement remaining ON CONFLICT ResolveTypes for INSERT and UPDATE statements (Preston Thorpe) * feat(cli): add .read command to execute SQL from a file (i cook code) * add bench feature to run benchmarks in nyrkio (Pedro Muniz) * Implement partial ON CONFLICT support for UPDATE statements (Preston Thorpe) * Support CTE visibility in VALUES+RETURNING subqueries and allow explicit col names (Preston Thorpe) * Add ECS task to run sqlancer nightly along with simulator (Preston Thorpe) * implement simple strategy to restart WAL log if possible (Nikita Sivukhin) * Add VSCode syntax highlighter for .sqltest files (Jussi Saurio) * DEFAULT value handling of NON-NULL columns in ADD COLUMN (Preston Thorpe) * Implement Full Text Search feature (Preston Thorpe) * add codspeed (Pedro Muniz) * core/mvcc: Add support for synchronous off mode (Pekka Enberg) * Add support for subqueries in the WHERE position for DML statements (Preston Thorpe) * Add vibecoded stress-go testing tool (Jussi Saurio) * fix: implement IS TRUE/IS FALSE with correct SQLite semantics (Jussi Saurio) * Add shuttle tests for IO trait (Pedro Muniz) * bindings/rust: support `prepare_cached` (Pere Diaz Bou) * Add more comprehensive PRAGMA integrity_check, and add PRAGMA quick_check (Jussi Saurio) * tests: add some shuttle tests (Pedro Muniz) * benchmarks: add micro benchmarks with divan (Pedro Muniz) * AGENTS.MD: add some magic words about max-turns (Jussi Saurio) * Add more instructions to AGENTS.MD and use Opus model instead of Claude for github bot (Jussi Saurio) * bindings/dotnet: implement IsDBNull check (Alex) * Implement json_object(*) to create JSON object from all columns (Martin Mauch) * Add some more guidelines to AGENTS.MD (Jussi Saurio) * Add id-token: write permission to claude action for OIDC (Jussi Saurio) * Add claude code github action and CLAUDE.MD file (Jussi Saurio) * antithesis: Add test case for WAL checkpointing (Pekka Enberg) * implement .dbtotxt and dbpage vtab (Pavan Nambi) * bindings/java: Add transaction support (Kim Seon Woo) ### Updated * core/mvcc: disable autoincrement for MVCC (Pere Diaz Bou) * testing/stress: Allow --seed parameter for non-deterministic runs (Pekka Enberg) * Update authors in Cargo.toml (Preston Thorpe) * refactor: reorganize emitter.rs into its own module (Preston Thorpe) * core/vector: Avoid simsimd on Windows AArch64 (Marc-André Moreau) * core/storage: Reject non-UTF-8 encoded databases on open (Mikaël Francoeur) * core/mvcc: Atomic timestamps when preparing commit (Avinash Sajjanshetty) * Differential fuzzer: detect more false positive with limit and order by (Pedro Muniz) * core/mvcc: move `payload_size` to frame header (Avinash Sajjanshetty) * Fast path no-op update in vdbe (Preston Thorpe) * Rename experimental_mvcc to mvcc (Eren Türkoğlu) * Update or replace ordering (Preston Thorpe) * Hash joins: partition spilling improvements (Preston Thorpe) * core/translate: Apply explicit view column aliases (Karthik Koralla) * core/translate: cache GROUP BY compound expressions properly (Jussi Saurio) * core: per-database MVCC transactions for attached databases (Glauber Costa) * Logical log tweaks (Jussi Saurio) * Assert that no two transaction timestamps are ever same (Avinash Sajjanshetty) * Update sqlite_sequence on ALTER TABLE ... RENAME TO (Kumar Ujjawal) * core/mvcc: Increase op count and tx payload size in logical log tx record format (Jussi Saurio) * core/translate: Use usable constraint even if preceded by unusable constraint on same column (Jussi Saurio) * Preserve BLOB-vs-no-affinity semantics in comparison coercion (Kumar Ujjawal) * compat: defer CTE column count check until its used (Pedro Muniz) * Improve SQLite3 tests (Pekka Enberg) * Accept trailing named column constraints in column defs (Kumar Ujjawal) * Enable index method table access for DML stmts (Preston Thorpe) * update unique_sets when renaming a column to prevent schema corruption (Glauber Costa) * Pretty print elle status progress (Avinash Sajjanshetty) * Decorrelate simple EXISTS / NOT EXISTS subqueries as semi/antijoins (Jussi Saurio) * Disable index-method access for DML (Kumar Ujjawal) * Avoid panic in DecrJumpZero on non-integer register (Kumar Ujjawal) * core/io: Track write buffer in completion (Jussi Saurio) * core/schema: Emit table-level UNIQUE constraints in BTreeTable::to_sql() (Jussi Saurio) * Avoid modulo-by-zero in WAL frame API fuzz rollback (Kumar Ujjawal) * enable async io in rust bindings (Pedro Muniz) * Make strict mode non-experimental (Glauber Costa) * Update misleading comment in MVCC (Avinash Sajjanshetty) * core/mvcc: Monotonic lockfree rowid allocation (Jussi Saurio) * core/mvcc: Use hash set to track commit dependencies (Avinash Sajjanshetty) * core/mvcc: check conflict delete btree resident tombstone (Pere Diaz Bou) * Adjust React Native example app instructions (Jussi Saurio) * Update example for VFS extensions in extensions/core/README.md (Preston Thorpe) * Enable partial indexes in MVCC mode (Preston Thorpe) * Enable Expression indexes in MVCC mode (Preston Thorpe) * Gate sqlite_dbpage writes behind --unsafe-testing (Kumar Ujjawal) * Enable FTS for JavaScript SDK (Nikita Sivukhin) * Sqltest cross check integrity (Jussi Saurio) * properly split sql statements for JS runner backend + enable triggers (Pedro Muniz) * Differential fuzzer improvements (Pedro Muniz) * core/translate: Remove spurious Affinity instruction from integrity_check (Pekka Enberg) * Remove some clones from MVCC code (Preston Thorpe) * remove `Arc>` from `Insn::Program` (Pedro Muniz) * ci: split bench into multiple jobs (Jussi Saurio) * Refactor core_tester fuzz module (Preston Thorpe) * Replace panic! and assert! with turso_macros assertions (Mikaël Francoeur) * SQLite compat: restrict ANY affinity special-casing to STRICT tables (Hossam Khero) * Enable MVCC for a bunch of fuzz tests (Preston Thorpe) * JS SDK: remote writes experimental (Nikita Sivukhin) * Reject triggers on virtual tables (Kumar Ujjawal) * serverless: Export Session and SessionConfig types (Nikita Sivukhin) * prevent scalar blob into JSONB (김선우) * install-sqlite: dont fail at get_download_url if sqlite3 already installed (Jussi Saurio) * refactor: stop using `connection.with_schema` inside `translate` module (Pedro Muniz) * Reject duplicate CTE names in WITH clause (Kumar Ujjawal) * Comment out flaky vector jaccard test (Preston Thorpe) * mvcc: disable cross-incompatible features (Jussi Saurio) * Random code cleanups to lib.rs (Pekka Enberg) * Reject column-level multi-column FK refs in ALTER TABLE (Kumar Ujjawal) * core: Make version functions non-deterministic (ratnesh mishra) * Experimental SQLRight setup (Mikaël Francoeur) * Reject FILTER clauses in view definitions (Kumar Ujjawal) * test/mvcc: re-enable database reopen logic in MVCC transaction fuzzer (Jussi Saurio) * perf: optimizations for FTS (Preston Thorpe) * CDC v2: transaction IDs and per-statement COMMIT records (Nikita Sivukhin) * Enable using Search for WHERE rowid= predicate instead of full scan (Preston Thorpe) * Reject reserved JSONB element types (Kumar Ujjawal) * core/mvcc: reparse schema on get_index_info (Pere Diaz Bou) * refactor: change translate functions to accept `&mut ProgramBuilder` (Pedro Muniz) * testing/stress: Remove multi-threading warning (Pekka Enberg) * github: Improve license check (Pekka Enberg) * core/mvcc: check for unique conflicts (Pere Diaz Bou) * Btree dump vtab (Nikita Sivukhin) * Cdc version table (Nikita Sivukhin) * vector8 + vector1bit (Nikita Sivukhin) * stress: Use single Database object (Pekka Enberg) * continue execution if IO was completed immediately (Nikita Sivukhin) * Lower Windows CI test timeout from 60 to 20 minutes (Pekka Enberg) * Multi index operator constraint (Pedro Muniz) * bench: gate nanosecond-level tests behind nanosecond-bench feature (Jussi Saurio) * use full link in JS SDK README so that links will be correct at npm (Nikita Sivukhin) * adjust is_constant check for `IN` and `emit_no_constant_insn` to avoid panic in VDBE cursor (Pedro Muniz) * concurrent-simulator: Explore in fast mode (Pekka Enberg) * Rewrite printf for full SQLite compatibility (Glauber Costa) * Pragmas (Glauber Costa) * testing/stress: Re-enable reopen and reconnect in stress tester (Pekka Enberg) * make `Numeric` the source of truth for number comparisons (Pedro Muniz) * Mvcc garbage collection (Jussi Saurio) * Remove unmaintained dart bindings (Avinash Sajjanshetty) * Convert antithesis from feature to cfg flag (Mikaël Francoeur) * core/storage: Remove unused FxHashSet import in btree.rs (Pekka Enberg) * core/mvcc: check integer primary key conflicts (Pere Diaz Bou) * use blocking IO for local databases for now (Nikita Sivukhin) * Make Github actions even more robust (Pekka Enberg) * bindings/wasm: Switch to notification-based I/O completion (Pekka Enberg) * Clippy more rules (Nikita Sivukhin) * properly handle url callback in react native bindings (Nikita Sivukhin) * Reduce antithesis stdout logging and log sql/tracing to files (Mikaël Francoeur) * Make Github workflows more robust (Pekka Enberg) * Change crate types in Dockerfile to restore Antithesis instrumentation (Mikaël Francoeur) * Whopper elle (Pedro Muniz) * allow url as function in react native bindings (Nikita Sivukhin) * JavaScript serverless driver improvements (Pekka Enberg) * MVCC: enable autocheckpoint at 1000 * 4120 bytes logical log size (Jussi Saurio) * Validate header (Nikita Sivukhin) * avoid unnecessary slow worker tasks in JS (Nikita Sivukhin) * do not cache chunks for .tantivy-meta.lock file (Nikita Sivukhin) * Destroy any custom index methods while dropping parent table (Preston Thorpe) * Stress progress bars (Pedro Muniz) * More snapshot tests (Pedro Muniz) * pin compiler and runner versions for codspeed benchmark (Pedro Muniz) * Move test runner to `testing/runner` (Pedro Muniz) * remove flaky test (Preston Thorpe) * Updates to COMPAT.MD (Jussi Saurio) * mark attach as experimental (Jussi Saurio) * run test with llvm-cov at linux (Nikita Sivukhin) * sim: generate unique constraints and verify statement rolls back correctly on failure (Pavan Nambi) * Bump urllib3 from 2.5.0 to 2.6.3 (app/dependabot) * Bump crossbeam-channel from 0.5.14 to 0.5.15 (app/dependabot) * JIT query generation in turso_stress (Mikaël Francoeur) * update determinism to match SQLite (Mikaël Francoeur) * core: database open is now async (Nikita Sivukhin) * bump claude code max turns (Pedro Muniz) * return NULL when there is no record in `op_column` (Pedro Muniz) * `strftime` should accept any kind of input not only text (Pedro Muniz) * React native example more tests (Nikita Sivukhin) * Document Rust sync API (Mikaël Francoeur) * Tpch snapshot testing (Pedro Muniz) * test runner: Snapshot testing (Pedro Muniz) * agressively black box benchmarks (Pedro Muniz) * Go driver time (Nikita Sivukhin) * `CHAR` should return `\0` for `NULL` (Pedro Muniz) * core/mvcc: use dual_peek in exists (Pere Diaz Bou) * update npm for trusting publishing features (Nikita Sivukhin) * Separate Connection from a prepared statement to enable proper caching (Preston Thorpe) * substitute HashMap for FxHashMap in core (Pedro Muniz) * adjust agents.md to call tursodb cli correctly (Pedro Muniz) * remove NODE_AUTH_TOKEN (Nikita Sivukhin) * React native (Nikita Sivukhin) * sql_generation: reduce likelihood of index name collisions (Jussi Saurio) * retry on reader contention for `TursoRwLock::read` (Pedro Muniz) * differential fuzzer + general purpose sql generation (Pedro Muniz) * cleanup: centralize optimizer params and make them loadable from json (Jussi Saurio) * remove accesses_db flag (Nikita Sivukhin) * Migrate more tcl tests (Pedro Muniz) * testing/system: allow specifying rowcounts for gen-bigass-database.py (Jussi Saurio) * tests/fuzz/cte_fuzz: try to prevent pathological queries (Jussi Saurio) * improvements of the whopper (Nikita Sivukhin) * Some Insert performance improvements (Preston Thorpe) * Performance improvements for hash joins (Preston Thorpe) * WAL auto truncation: increase epoch to prevent stale pages reuse (Nikita Sivukhin) * core/mvcc: reparse schema after stream of schema changes (Pere Diaz Bou) * Reduce CLI release build time by ~60% (Jussi Saurio) * cleanup/vdbe: remove unused IndexKeyOwned and remove unused IndexKeyUnpacked::cursor_id (Jussi Saurio) * Refactor: remove encryption key field from Database struct (Avinash Sajjanshetty) * Migrate more TCL tests (Pedro Muniz) * convert agent docs to skills (Pedro Muniz) * Some optimizations for tpc-h queries (Pavan Nambi) * Refactor AggContext to use flat payload representation (Jussi Saurio) * more testing cleanups - move simulators (Pedro Muniz) * more testing cleanups (Pedro Muniz) * react-native: prepare (Nikita Sivukhin) * Move stress to `testing/stress` (Pedro Muniz) * Move tcl test to `testing/system` (Pedro Muniz) * ci/bench: increase timeout to 45mins as workaround (Jussi Saurio) * Remove fts/tantivy as default dependency in turso-core (Preston Thorpe) * Move antithesis test to `testing/antithesis` (Pedro Muniz) * feat/perf: use HashTable machinery for DISTINCT instead of ephemeral btree (Jussi Saurio) * ci: split simulator into separate jobs (Jussi Saurio) * CI: Reduce iterations in cte fuzzer (Preston Thorpe) * ci: longer timeout for cargo nextest on windows (Jussi Saurio) * fuzz/cte: reduce iterations (Jussi Saurio) * docs: split AGENTS.MD into separate agent-guides files (Jussi Saurio) * Js test runner (Jussi Saurio) * refactor/pager: small cleanups (Jussi Saurio) * sim: only run turso integrity check 10% of the time (Jussi Saurio) * Rewrite BETWEEN statements for partial indexes in DML paths (Preston Thorpe) * Create global directive for skipping mvcc in test runner (Preston Thorpe) * stress: run stress with shuttle (Pedro Muniz) * Validate Send + Sync for `ProgramState` + change registers to hold a `Box<[Register]>` (Pedro Muniz) * test-runner: rust backend (Pedro Muniz) * test-runner: start migrating TCL tests (Pedro Muniz) * Give Antithesis tests better names (Mikaël Francoeur) * core/vdbe: Allow leading + sign in numeric string parsing (Pere Diaz Bou) * core/vdbe: trim return text with integer and float (Pere Diaz Bou) * Reset ProgramState::once on statement reset (Mikaël Francoeur) * return blob if we concat blobs (Nikita Sivukhin) * Replace GPL-2.0 bloom crate with MIT-licensed fastbloom (Jussi Saurio) * Refactor core/lib.rs Connection impl out to its own file (Preston Thorpe) * Disallow WITHOUT ROWID tables (Jussi Saurio) * core: Use fast monotonic time when possible (Pekka Enberg) * core/vdbe: move mutable fields from Program to ProgramState (Pere Diaz Bou) * replace custom SmallVec with smallvec crate (Ankush) * Open autovacuum databases in readonly mode when experimental flag not set (app/copilot-swe-agent) * remove unused lints, variables and functions (Pedro Muniz) * separate Sync module (Pedro Muniz) * Btree: optimize defragment_page (Khashayar Fereidani) * avoid recursive read locks (Nikita Sivukhin) * Track WAL state in savepoints for proper rollback (Pavan Nambi) * vdbe: Resize deferred_seeks on program state reset (Pekka Enberg) * Reset checkpoint state when PRAGMA wal_checkpoint fails (Jussi Saurio) * Increase test_eval_param_only_once timeout threshold for CI reliability (app/copilot-swe-agent) * stress: switch to WAL mode before SQLite integrity check (Pekka Enberg) * planner: hash-join planning with safe build-side materialization (Preston Thorpe) * core/mvcc: rollback index rows (Pere Diaz Bou) * core/vdbe: check negative root pages with `integrity_check` (Pere Diaz Bou) * antithesis/stress: run in verbose mode (Jussi Saurio) * docs: change test build instructions (Mason Hall) * btree: `get_prev_record` and `get_next_record` automatically invalidate_record (Pedro Muniz) * bindings/dotnet: allow setting TursoParameter.Direction (Alex) * VDBE micro-optimizations for read path (Jussi Saurio) ### Fixed * concurrent-simulator: Fix DDL inside BEGIN CONCURRENT transactions (Pekka Enberg) * core/json: Fix stale cursor crash in json_tree correlated subqueries (Pekka Enberg) * Avoid panic on unresolved qualified column in DO UPDATE (Kumar Ujjawal) * Fix unsound schema access when loading extensions (Preston Thorpe) * Fix unresolved IfNot label in scalar count(*) subqueries (Kumar Ujjawal) * core/translate: Fix non-literal defaults in Column instruction (Jussi Saurio) * core/mvcc: Fix header-only write durability (Jussi Saurio) * fix: IN(subquery) data corruption in ungrouped aggregates with NOT NULL columns (Vimal Prakash) * temporarily remove big sqltest to fix all the CI errors (Preston Thorpe) * Fix regression improperly consuming hash join WHERE terms (Preston Thorpe) * fix: UPDATE OR REPLACE does not fire FK CASCADE on implicitly-deleted row (Pedro Muniz) * Fix wal truncate on shutdown even after restart (Kumar Ujjawal) * fix: UPDATE RETURNING correlated subquery uses pre-UPDATE values (Pedro Muniz) * Make MVCC Elle check errors fail CI build (Pekka Enberg) * Overflow fix (Nikita Sivukhin) * fix: Window function with GROUP BY and ORDER BY causes optimizer assertion failure #5552 (Pedro Muniz) * Fix sync replay generator (Nikita Sivukhin) * fix: always reset pager state for attached databases (Pedro Muniz) * Fix three FULL OUTER JOIN bugs (Jussi Saurio) * fix snapshot isolation in MVCC (Preston Thorpe) * fix: clean up db-log and fix WAL extension for attached DBs in simulator (Glauber Costa) * fix: Expression index with correlated EXISTS subquery causes panic #5551 (Pedro Muniz) * Fix --experimental-strict incorrectly enabling custom types (Glauber Costa) * autofix: Aggregate COLLATE leaks to subsequent aggregates in same query (Jussi Saurio) * fix/mvcc: prevent persistent "Database is busy" after failed IO during checkpoint (Jussi Saurio) * autofix: CTE with UNION ALL corrupts first-column literals when any branch has aggregates (Jussi Saurio) * autofix: Triggers during FK CASCADE see pre-delete state of parent table (Jussi Saurio) * autofix: Correlated subquery with explicit JOIN returns wrong results after first row (Jussi Saurio) * autofix: LEFT JOIN incorrectly converted to INNER JOIN when WHERE uses CASE/IIF (Jussi Saurio) * autofix: Row value IN expression crashes with register allocation error (Jussi Saurio) * fix: BEFORE UPDATE trigger corrupts outer cursor position for correlated subqueries (Jussi Saurio) * autofix: DELETE with LIMIT ignores OFFSET clause (Preston Thorpe) * fix: UPSERT ON CONFLICT fails to match expression indexes #5550 (Preston Thorpe) * fix/optimizer: enable multi-index OR scans more widely and fix bad CTE query plans (Jussi Saurio) * Fix REAL affinity to preserve non-numeric text (Kumar Ujjawal) * Fix rowid resolution for views/WITHOUT ROWID (Kumar Ujjawal) * fix: Use pure-rust for macos for aegis to prevent macos compilation issues (Amodh Dhakal) * fix RETURNING, Statement::reset() and Program::abort() (Jussi Saurio) * Fix DEFAULT affinity handling (Jussi Saurio) * Improve I/O error message (Pekka Enberg) * fix: Handle IVM old record capture when REQUIRE_SEEK is set (Martin Mauch) * fix: MVCC cursor ignored left join null flag (Jussi Saurio) * fix: adjust `jump_if_null` branch offset as incorrectly failed NULL in check constraints in integrity checks (Pedro Muniz) * fix: emit RealAffinity when reading subquery columns from ephemeral indexes (Jussi Saurio) * fix: default `generate_series` STOP to SQLite Default (4294967295) when omitted (Damian Melia) * fix: mvcc: DDL statement inside BEGIN CONCURRENT returns a confusing error #3380 (Pedro Muniz) * fix: do not do unchecked indexing to non-static page offsets #4736 (Pedro Muniz) * fix: use consistent M_PI constant in degrees/radians for test parity with SQLite (Pedro Muniz) * fix: Register DBSP state index in schema for integrity_check (Martin Mauch) * fix: Panic on correlated subquery in IN clause referencing outer table alias #5213 (Pedro Muniz) * fix(wal): release WAL read lock on rollback to prevent checkpoint busy (Ahmad Alhour) * sqlite3: Fix segfault in sqlite3_free_table() (Pekka Enberg) * fix: OR in Partial Index WHERE Clause Causes Unresolved Label Panic (Jussi Saurio) * fix: No-op UPDATE causes datatype mismatch #5410 (Jussi Saurio) * fix: Expression Index Key Computed From Pre-Affinity Value During UPDATE (Jussi Saurio) * fix: ALTER TABLE RENAME COLUMN Does Not Update Partial Index WHERE Clauses (Jussi Saurio) * fix: Create materialized view in same transaction as source (Martin Mauch) * fix: "Cursor not found" when delete has a correlated subquery #5434 (Jussi Saurio) * fix: INSERT INTO With CTE + Compound SELECT Produces Garbled Data #5425 (Jussi Saurio) * fix: DELETE with correlated subquery sees in-flight deletions #5426 (Jussi Saurio) * fix: Panic When CTE Is Used in UPDATE SET Clause on Keyed Table (Jussi Saurio) * Fix FTS LIMIT 0/-1 handling to avoid Tantivy overflow (Kumar Ujjawal) * fix/correctness: use ephemeral table for UPDATE when expression index refers to updated columns (Mikaël Francoeur) * fix: update changes() and total_changes() inside explicit transactions (Damian Melia) * Fix: Fully enable foreign keys for MVCC (Preston Thorpe) * fix(cli): prevent extra space in multiline readline input (Karthik Koralla) * Fix materialized view re-entrancy bug (Martin Mauch) * fix affinity handling in check constraint evaluation (Jussi Saurio) * fix affinity calculation for index seeks, subqueries and ctes (Jussi Saurio) * fix: COLLATE Specification Lost in Concatenation with CHECK #5171 (Jussi Saurio) * fix: INSERT OR IGNORE + CHECK Failure Does Not Update sqlite_sequence (Jussi Saurio) * fix: Subquery in Trigger Body UPDATE SET Creates Poison Trigger (Jussi Saurio) * fix: WHERE clause returns wrong result for INT64_MAX compared against overflowed float (Jussi Saurio) * Fix prepare panic on comment/whitespace-only SQL (Kumar Ujjawal) * fix: Panic on Scalar Subquery Referencing CTE by Original Name After Alias (Jussi Saurio) * Fix SUM() type for BLOB inputs to return REAL (Kumar Ujjawal) * Unify error codes in LibsqlError for serverless package (Pekka Enberg) * Fix panic with duplicate expressions in window PARTITION BY (Mikaël Francoeur) * Fix panic and wrong results for window function in EXISTS subquery (Mikaël Francoeur) * fix: handle NULL properly in composite index seek/termination (Jussi Saurio) * fix: panic in PRAGMA integrity_check with expression indexes #5117 (Sanjeev) * fix: avoid cursor panic in SUM(TRUE IS TRUE) (Jussi Saurio) * Fix CLI exit code on query failure (Ahmad Alhour) * Fix MVCC trigger recovery rootpage handling (Jussi Saurio) * Fix collation with reordered hash joins (Preston Thorpe) * fix/btree: return error instead of panic if parent is unexpectedly a leaf (Jussi Saurio) * Fix panic on duplicate ORDER BY expressions with GROUP BY (Jussi Saurio) * fix: Align REAL→INTEGER cast with SQLite clamp semantics (Kumar Ujjawal) * fix: INSERT with auto-assigned rowid skips explicit index on IPK column (Jussi Saurio) * Fix affinity conversion of nan/inf text in numeric columns (Kumar Ujjawal) * fix: Correlated Subquery + Window Function in WHERE Hoisted Before Scan Loop (Jussi Saurio) * fix: more recursive read locks detected by shuttle with mvcc (Pedro Muniz) * fix/mvcc: dropped_root_pages tracking must survive restart (Jussi Saurio) * core/mvcc: Fix MVCC checkpoint panic when deleting index keys in inte… (Pekka Enberg) * fix: backward table scan skips rows on deep B-trees (ORDER BY DESC) (Pedro Muniz) * Phantom row fix (Pedro Muniz) * Insert or fix (Pedro Muniz) * fix: Panic on SELECT with 32767+ Columns (No SQLITE_MAX_COLUMN Limit) (Jussi Saurio) * fix: GROUP BY With Constant Expression Causes Infinite Loop #5300 (Jussi Saurio) * fix: propagate affinity for `IN` Subquery (Pedro Muniz) * fix(optimizer): fix correctness bugs in LEFT->INNER join optimization (Jussi Saurio) * fix/mvcc: fix test_logical_log_header_reserved_bytes_rejected test (Jussi Saurio) * Fix livelock in MVCC commit when pager_commit_lock is contended (Jussi Saurio) * Fix: handle multiplication overflow in time_date (Zohaib) * Fix panic on ROLLBACK TO by replacing assert! with bail_parse_error (codingbot24-s) * fix: Panic on CREATE TABLE with UNIQUE ... ON CONFLICT IGNORE #5221 (Jussi Saurio) * mvcc: define crash recovery / correctness semantics, rewrite logical log (Jussi Saurio) * Fix UPDATE incorrectly changing last_insert_rowid() (Kumar Ujjawal) * Fix panic in sum() with large floating-point values (Jussi Saurio) * fix(core): Handle PRAGMA cache_size overflow (fixes #5250) (ratnesh mishra) * fix: allow CTEs to shadow schema objects (Jussi Saurio) * fix: Panic on INSERT INTO t(nonexistent) SELECT ... FROM t #5226 (Jussi Saurio) * fix: Panic on Unresolved Table Reference in UPSERT DO UPDATE SET #5281 (Jussi Saurio) * core/storage: Fix pager commit completion (Pekka Enberg) * Fix throughput plot colors and hatching to match TPC-H plot (Pekka Enberg) * fix: Panic on 3+ Way MULTI-INDEX AND Query (RowSet State Violation) #… (Jussi Saurio) * fix: u16 Overflow in page_free_array Panics on 65536-Byte Pages #5276 (Jussi Saurio) * fix: handle PRAGMA database_list with argument without panicking (creativcoder) * fix: INSERT ... RETURNING panics with multi-column scalar subquery #5243 (Jussi Saurio) * fix: Panic When IN Subquery Used in LIMIT Expression #5247 (Jussi Saurio) * fix: ALTER TABLE RENAME COLUMN Fails on Case Mismatch #5246 (Jussi Saurio) * fix: read non-aggregate columns during AggStep for mixed expressions (Glauber Costa) * fix: null coroutine registers in ungrouped aggregate empty-result path (Glauber Costa) * fix: json_valid() With Zero Arguments Panics Instead of Returning Error (Jussi Saurio) * fix: LIMIT With i64::MIN Panics Due to Subtract Overflow in DecrJumpZ… (Jussi Saurio) * Fix missing dot in NOT NULL constraint error (Eren Türkoğlu) * fix: Materialized View Panics on Arithmetic With Text Column Values #… (Jussi Saurio) * fix: Panic on Scalar Subquery Referencing Rowid of Derived Table #5249 (Jussi Saurio) * Fix MVCC savepoint rollback leaving stale write set entries (Pekka Enberg) * fix: varint_len returned 10 for values with 58-64 significant bits (Jussi Saurio) * fix: do not apply numeric affinity of converted float is a NaN (Pedro Muniz) * try to fix js ci (Nikita Sivukhin) * fix: QUOTE should emit float in scientific notation (Pedro Muniz) * fix: GROUP BY sort order for extra columns (Pedro Muniz) * fix(core): fix ColumnUsedMask::is_only for overflow indices beyond vector length (Pedro Muniz) * Clear bloomfilters on rewind + hash table fix (Preston Thorpe) * fix: correctly jump to second OR expression if first expression is NULL (Pedro Muniz) * bindings/go: Fix spurious test failures from port conflicts (Pekka Enberg) * Fix changes() value for DELETE (Preston Thorpe) * core/mvcc: Fix transaction end version visibility check (Pere Diaz Bou) * Rn locking fix (Nikita Sivukhin) * fix prevent AUTOINCREMENT on non-INTEGER PKs with mixed-case names (김선우) * fix: cannot use index seek if the constraining expression references the same table being scanned (Pedro Muniz) * fix: evaluate result rows on exists subquery with DISTINCT clause (Pedro Muniz) * Fix multiple index corruption bugs involving INSERT OR (Jussi Saurio) * scripts/release-status.py: Classify issues as bugs by default (Pekka Enberg) * Fix broken build (Pavan Nambi) * Fix segfault in uuid_str/uuid_ts with no arguments (Preston Thorpe) * fix build (Mikaël Francoeur) * Strict fixes (Glauber Costa) * fix: prevent overflow panic in JsonArrayLength with malformed JSONB (Jussi Saurio) * Fix LEFT JOIN with virtual tables producing wrong results (Glauber Costa) * Fix user-defined columns named oid/rowid/_rowid_ resolving to internal rowid (Glauber Costa) * Rn fixes 2 (Nikita Sivukhin) * fix: emit offset after Distinct deduplication (Pedro Muniz) * fix: increase tolerance in vector L2 distance test to match simsimd vs rust precision (Pedro Muniz) * fix/encryption: do not panic if accessing db with missing or incorrect keys (Avinash Sajjanshetty) * fix: overflow handling in `cast_real_to_integer` (Pedro Muniz) * fix: convert text and blobs into bytes for correct parsing of strings into numbers (Pedro Muniz) * fix: `handle_text_sum` treat certain text values as approximate (Pedro Muniz) * Fix ruff check errors (Avinash Sajjanshetty) * Fix panic when CTE with indexed subquery is referenced twice (Mikaël Francoeur) * Fix STRICT tables incorrectly rejecting NULL values (Glauber Costa) * fix: allow qualified column references in partial index WHERE clause functions (Jussi Saurio) * Fix deadlock in MVCC mode when loading extensions on file databases (Jussi Saurio) * fix: WHERE clause treats string literals with numeric prefixes correctly (Jussi Saurio) * fix(update): apply affinity before MakeRecord to prevent index corruption (Pedro Muniz) * fix: JSON path UTF-8 char boundary panic (Jussi Saurio) * MVCC: fix checkpoint consistency and durability bugs (Jussi Saurio) * Fix CLI ".open" command to preserve experimental flags (Jussi Saurio) * fix/pager: properly reuse last empty trunk page for page allocation (Jussi Saurio) * Fix Antithesis CI config (Mikaël Francoeur) * Fix missing STRICT keyword in BTreeTable::to_sql() (Glauber Costa) * fix/connection: release wal locks on connection drop to prevent DB from being permalocked (Jussi Saurio) * Fix JavaScript driver error message to match libSQL (Pekka Enberg) * MVCC fixes found using whopper (Jussi Saurio) * Fix bug when empty namespace is provided in go sync driver (Preston Thorpe) * Fix passing namespace in to config in Go sync driver (Preston Thorpe) * fix: -0.0 and 0.0 should have the same hash (Pedro Muniz) * core/mvcc: fix btree row verification (Pere Diaz Bou) * fix: group concat did not convert values to text on output (Pedro Muniz) * Fix datetime modifier UTF-8 slicing panic (Mikaël Francoeur) * fix `is_nonnull` check for primary key columns (Pedro Muniz) * page cache: fix aggressive spilling logic (Preston Thorpe) * Fix bug in hash join table mask calculations (Jussi Saurio) * Fix broken encryption SDK tests (Avinash Sajjanshetty) * Fix correlated subquery issues (Jussi Saurio) * fix: avoid exponential CTE re-planning during query compilation (Jussi Saurio) * ivm: Fix comparison of Integer <-> Float (Martin Mauch) * Fix cross-link in Claude skills (Ofek Lev) * mvcc: fix for schema conflict on COMMIT (Nikita Sivukhin) * Fix bugs in hash-join materialization subplans (Jussi Saurio) * fix: prevent integer overflow panic in JSONB serialization (Jussi Saurio) * sqlancer dockerfile fix (Jussi Saurio) * fix: infer subquery result list column types (Jussi Saurio) * fix/vdbe: fix QUOTE() - should convert ints&floats to text (Jussi Saurio) * fix/optimizer: fix output cardinality estimation for index seeks (Jussi Saurio) * fix: truncate ORDER BY after rowid when rowid is first (Jussi Saurio) * fix: PRINTF converts non-text format argument to string (Jussi Saurio) * fix: substr returns NULL when provided NULL length (Jussi Saurio) * fix docker builds (Pedro Muniz) * fix: handle EXISTS in expr_vector_size instead of panicing with todo!() (Jussi Saurio) * fix: move subqueries to ephemeral plan when WHERE clause is moved (Jussi Saurio) * fix: eagerly remove ORDER BY if single aggregate, no groups/windows (Jussi Saurio) * fix: IN/NOT IN subqueries return NULL when value not found and subquery contains NULLs (Jussi Saurio) * fix/optimizer/stats: fix incorrectly named distinct_per_prefix field (Jussi Saurio) * fix antithesis dockerfile: download libvoidstar.so in Dockerfile instead of COPY (Jussi Saurio) * Optimizer: fix bugs, improve cost model (Jussi Saurio) * Fix wal checkpoint (Nikita Sivukhin) * Busy snapshot bugfix (Nikita Sivukhin) * fix/translate: revert change that allowed index cursor with stale position to be read (Jussi Saurio) * try_for_float: use Dekker algorithm to avoid precision error (Jussi Saurio) * fix: type affinity comparison for TEXT columns with numeric literals (#4745) (Srinivas A) * fix sync-wasm package to properly handle case when no url is provided (Nikita Sivukhin) * Fix JSON panics and incompatibility issues (Jussi Saurio) * Fix misuse of encryption key in the database manager cache (Avinash Sajjanshetty) * Run cargo clippy --fix --all in turso-test-runner (Preston Thorpe) * Fix ON CONFLICT resolution propagation to trigger statements (Preston Thorpe) * Fix sqlancer-runner image name in GH action build (Preston Thorpe) * Fix misleading turso-test-runner comments regarding mvcc&readonly (Jussi Saurio) * turso-test-runner: fix flakiness with Rust bindings + MVCC combo (Jussi Saurio) * Fix datetime parse_modifier() panicing on multibyte utf-8 (Jussi Saurio) * Ci fixes (Jussi Saurio) * Scalarfunc fuzzer + fixes (Jussi Saurio) * bugfix: fix corruption by setting `reserved_bytes` if requested (Avinash Sajjanshetty) * fix(vdbe): CHAR() function should handle full Unicode range (Mikaël Francoeur) * do not call unlock_after_restart in case of error during wal truncation - because we already released these locks earlier (Nikita Sivukhin) * WAL: ignore error from auto-checkpoint and fix bug (Nikita Sivukhin) * fix: dont create nested aggregates in cte_fuzz (Jussi Saurio) * properly unlock WriteLock if restart failed and ignore Busy errors when attempt to restart WAL file failed (Nikita Sivukhin) * fix(parser): reject duplicate PRIMARY KEY clauses on a single column (Mikaël Francoeur) * Cte fixes (Jussi Saurio) * fix/connect: read page1 in transaction to prevent illegal WAL read (Jussi Saurio) * fix: dont panic if decryption or checksum verification fails (Jussi Saurio) * fix: fsync DB file before truncating WAL after checkpoint (Jussi Saurio) * Return error instead of panicing when 1. short read occurs 2. page type is invalid (Jussi Saurio) * Checkpoint restart fix (Nikita Sivukhin) * fix: nullif/instr/concat should alloc regs for both args upfront (Jussi Saurio) * core/vdbe/sorter: Propagate write errors instead of corrupting data (Preston Thorpe) * fix(parser): BETWEEN and LIKE operator precedence with IS NOT NULL (Jussi Saurio) * fix: allow CTEs to be referenced multiple times (Jussi Saurio) * fix(parser): reject ?0 parameter with proper error instead of panicking (Jussi Saurio) * more recursive read lock fixes (Pedro Muniz) * fix: allow float literals in ORDER BY clause (Jussi Saurio) * fix: correct operator precedence in replace() argument check (Jussi Saurio) * fix: reject literal-only index expressions and fix JNI panic (Jussi Saurio) * fix: handle CAST without type name for SQLite compatibility (Jussi Saurio) * fix/vdbe: convert BusySnapshot to Busy if conn rolled back (Jussi Saurio) * sdk-kit: Export busy snapshot error to callers (Pekka Enberg) * fix: return NULL instead of panicking in uuid7_timestamp_ms for invalid blobs (Jussi Saurio) * fix: handle parse errors gracefully in LIMIT clause (Jussi Saurio) * fix: prevent panic on multi-byte UTF-8 in datetime functions (Jussi Saurio) * fix: INSERT INTO ... SELECT ... UNION ... inserts all rows (Jussi Saurio) * Fix DEFAULT value handling in integrity check (Jussi Saurio) * Fix stale overflow page read bug and improper re-entrancy handling in integrity_check (Jussi Saurio) * Fix integrity_check NOT NULL validation false positive (Jussi Saurio) * Fix compilation after cross-pollution with unused vars PR and integrity check PR (Jussi Saurio) * fix: use `checked_cast_text_to_numeric` for `Numeric` cast in `exec_cast` (Pedro Muniz) * fix: resolve label in ungrouped aggregation with SELECT DISTINCT (Jussi Saurio) * fix/checkpoint: always read page1 from db file when truncating (Jussi Saurio) * whopper: Handle BusySnapshot error gracefully (Pekka Enberg) * Fix WAL truncate checkpoint discarding uncheckpointed frames (Jussi Saurio) * btree.rs: make #[instrument] conditional on debug_assertions (Jussi Saurio) * Fix path not fsyncing the DB file after truncate checkpoint (Preston Thorpe) * core: make datetime functions faster and fix bugs (Jussi Saurio) * Valueiterator with fixes (Jussi Saurio) * stress: handle I/O error gracefully instead of panicking (Pekka Enberg) * stress: dont panic if get BusySnapshot error (Jussi Saurio) * bindings/rust: return error on out of bounds Row::get access (Jussi Saurio) * vector: increase fuzz error tolerance on windows (Jussi Saurio) * sim: fix yet another apply_snapshot() edge case (Jussi Saurio) ## 0.4.0 -- 2026-01-05 ### Added * stress: Add support for BEGIN CONCURRENT transactions (Pekka Enberg) * stress: add semi colon to transaction statements when printing to log file (Pedro Muniz) * Add scripts/corruption-debug-tools (Jussi Saurio) * add more asserts in balance operation (Pedro Muniz) * bindings/java: implement JDBC4 CharacterStream binding methods (Orange banana) * add --db-ref optional arg to run turso-stress against "template" database (Nikita Sivukhin) * support `format()` function (Fahd Ashour) * feat(extensions): add stddev aggregate function to percentile module (Kelvin) * simulator: add proper handling for deffered transactions in shadow state (Pedro Muniz) * feat: Add support for HAVING without GROUP BY (Nuno Gonçalves) * Implement foreign key actions (Preston Thorpe) * implement pragma cache spill (Preston Thorpe) * Add multiverse debugging instructions (Mikaël Francoeur) * Implement busy handlers/callbacks (Preston Thorpe) * add readonly checks to ensure we do not change the header (Pedro Muniz) * implement state machine for `op_journal_mode` (Pedro Muniz) * Add io_uring option for IO backend to simulator (Preston Thorpe) * core/storage: implement Cache Spilling (Preston Thorpe) * Add dotnet bindings to Turso (Kopylov Dmitriy) * fix bug in the sync engine wasm implementation (Nikita Sivukhin) * feat(hash-join): add hash matching for equivalent integer and real values (Nuno Gonçalves) * implement state machine for parsing input in CLI (Pedro Muniz) * add rust-analyzer component to toolchain (Pedro Muniz) * Add script to run SQLancer against turso + fix some bugs found by doing so (Jussi Saurio) * Add greedy join ordering for large queries (>12 tables) (Jussi Saurio) * core/mvcc/cursor: add missing reset state in `next` (Pere Diaz Bou) * Add PR template (Preston Thorpe) * fix: JSON_INSERT now correctly inserts new keys in nested objects (Mikaël Francoeur) * Remove unused parameter in `limbo_exec_rows` and add ergonomic ExecRows trait for testing (Pedro Muniz) * translate/optimizer: Finish implementing ANALYZE (Preston Thorpe) * core/mvcc/cursor: implement count (Pere Diaz Bou) * support libsql:// protocol as a sync url in python driver (Nikita Sivukhin) * initialize global header on bootstrap (Pedro Muniz) * Rust binding add prepare to transaction (Dave Warnock) * add turso bot config (Pedro Muniz) * feat: adding check for unquoted literals in values() (Rohith Suresh) * Improved Python driver with opt-in asyncio support (Nikita Sivukhin) * tcl,makefile: add tcl test infraestructure for mvcc (Pere Diaz Bou) * core/mvcc: fix bounds check new rowid (Pere Diaz Bou) * Added dot product vector distance (Tejas) * sim: add binary tool that converts plan.sql to rust test file (Jussi Saurio) * Add explanation for concurrent transactions (Pekka Enberg) * testing/fuzz: Add new fuzzer for joins (Preston Thorpe) * add lib-release profile (Nikita Sivukhin) * planner/vdbe: implement Hash Joins as an alternative to Ephemeral Indexes (Preston Thorpe) * Add sync support to the SDK kit (Nikita Sivukhin) * fix/mvcc: always reinitialize index iterator on seek (Jussi Saurio) * translate/vdbe: add bloom filter (Preston Thorpe) * mvcc: implement logical log recovery for indexes + checkpointing of indexes (Jussi Saurio) * Docs: add table of contents to CONTRIBUTING.md (Fahd Ashour) * finish implementing "quote" scalar function for blob types (Preston Thorpe) * add alias to colnames explicitly as column-N (Pavan Nambi) * Add `#[turso_macros::test]` to automatically create tests that can run MVCC with minimal code changes (Pedro Muniz) * mvcc: introduce stateful "dual cursor" (Jussi Saurio) * introduce program execution state in order to run stmt to completion in case of finalize or reset (Nikita Sivukhin) * mvcc: add some plumbing for index support (Jussi Saurio) * core/mvcc/tests: add fuzz test for mvcc with checkpoint and with CRUD ops (Pere Diaz Bou) * translate/planner: Implement Index creation on arbitrary expressions (Preston Thorpe) * Support table xinfo (Nikita Sivukhin) * core/mvcc/cursor: implement prev and last (Pere Diaz Bou) * Trigger support (Jussi Saurio) * translate/insert: Implement INSERT OR REPLACE (Preston Thorpe) * Add ColDef struct to make schema::Column creation more ergonomic (Preston Thorpe) * Support DELETE ... RETURNING (Jussi Saurio) * Refactor RETURNING to support arbitrary expressions (Jussi Saurio) * bindings/java: implement JDBC4 InputStream binding methods (ASCII/Binary, no-length and long overloads) (Orange banana) * Add RowSet instructions and rowset implementation (Jussi Saurio) * bindings/java: implement stream binding methods (int, InputStream, int) in JDBC4PreparedStatement (Orange banana) * workflows: Add GITHUB_TOKEN to all Nyrkiö steps (Henrik Ingo) * extensions/vtabs: implement remaining opcodes (Preston Thorpe) * Throw an error when adding generated columns via an alter table (Rohith Suresh) * add some docs for index method (Nikita Sivukhin) * bindings/java: Implement setObject(int, Object) in JDBC4PreparedStatement (Orange banana) ### Updated * Minor cleanups in function.rs and refactoring allocations (Preston Thorpe) * WAL: Stop copying page buffers during checkpoint (Preston Thorpe) * test runner: Test Converter (Pedro Muniz) * core/storage: Eliminate buffer copy in begin_write_btree_page() (Pekka Enberg) * Test runner Foundation (Pedro Muniz) * show failure output in the end with cargo nextest (Pedro Muniz) * core/storage: Zero remaining buffer bytes in begin_write_btree_page() (Pekka Enberg) * core/storage: Make PagerInner::buffer use Arc (Pekka Enberg) * Adjust merge script to truncate the PR template (Preston Thorpe) * Improvements to turso_stress (Mikaël Francoeur) * Page management cleanups (Pekka Enberg) * btree: reset `AdvanceState` after last state transition (Pedro Muniz) * Make BTree and MVCC cursor `Send + Sync` (Pedro Muniz) * Raise log level polling frequency (Mikaël Francoeur) * btree/pager: performance tuning (Preston Thorpe) * chore/btree: remove unused code (Jussi Saurio) * Modify bench-profile to allow generating better flamegraphs (Jussi Saurio) * all-mvcc: uncomment working tests (Pere Diaz Bou) * perf/sorter: sort pointers instead of records, use arena allocation (Jussi Saurio) * Remove TursoDBFactory (Mikaël Francoeur) * Save sync configuration (Nikita Sivukhin) * Optimized RecordCursor, Remove read_varint_fast (Khashayar Fereidani) * Run statements to completion on reset (Martin Mauch) * adjust tpc-h bench script to more easily compare results (Preston Thorpe) * stress: use multithreaded runtime (Jussi Saurio) * Conflict end txn (Nikita Sivukhin) * Accept SQL query using `AsRef` instead of `&str` (Arto Bendiken) * core: remove mutex from ImmutableRecord::cursor (Jussi Saurio) * Enable tokio-unstable in Antithesis image (Mikaël Francoeur) * Prevent dropping columns that contain fk references (Preston Thorpe) * lint/perf: deny eager fallback function calls (ok_or, map_or, unwrap_or) (Jussi Saurio) * reset statement in query() (Mikaël Francoeur) * User rust-gdb instead of gdb (Mikaël Francoeur) * Antithesis observability improvements (Mikaël Francoeur) * Rust bindings sync (Nikita Sivukhin) * Refactor/improve performance of commit path (Preston Thorpe) * Sdk kit rust bindings (Nikita Sivukhin) * Yet another refactor of INSERT translation (Preston Thorpe) * stress: Make SQLite integrity check more explicit (Pekka Enberg) * Sdk kit refactoring (Nikita Sivukhin) * perf/vdbe: reuse&clear ephemeral cursor on repeat invocations (Jussi Saurio) * perf/prepare: various optimizations (Jussi Saurio) * Improve lexer performance by using SIMD (Khashayar Fereidani) * Remove unnecessary `Cell` and `RefCell` for better multithreaded safety (Pedro Muniz) * Partial sync experimental (Nikita Sivukhin) * remove unneeded Result in exec unixepoch (Juan V. García) * Lexer/Parser Optimization and refactoring (Khashayar Fereidani) * tcl: run PRAGMA journal_mode=experimental_mvcc with mvcc (Pere Diaz Bou) * SDK tweaks (Nikita Sivukhin) * core/mvcc: set_null_flag(false) when seek is called (Pere Diaz Bou) * Mark triggers as experimental (Jussi Saurio) * Set all testing dbs to WAL journal mode (Preston Thorpe) * Remove run once from `Statement` (Pedro Muniz) * Use u64::from instead of .into() (Elina) * remove the warning directive to allow environment filter to work (Pedro Muniz) * core/execute: use same code for generating rowid in mvcc as in btree (Pere Diaz Bou) * Improve MVCC DX by dropping `--experimental-mvcc` flag (Pekka Enberg) * aws/sim: disable io-uring (Jussi Saurio) * Local sync server (Nikita Sivukhin) * Simplify slot bitmap to remove complex unused optimizations (Preston Thorpe) * clean up core tester to use `conn.execute` and `conn.exec_rows` for parsing correctly the expected values from select queries (Pedro Muniz) * Connection small refactor (Dave Warnock) * Enable MVCC with `PRAGMA journal_mode` (Pedro Muniz) * propagate partial sync settings in the web (Nikita Sivukhin) * Consider Order by expressions collation when deciding candidate index for iteration (Pedro Muniz) * Checkpoint cleanup (Jussi Saurio) * use cmath from system libraries only in tests in order to be more portable (Nikita Sivukhin) * No tempfiles on wasm (Nikita Sivukhin) * core: Make Pager thread-safe (Pekka Enberg) * update go mod name as we will serve module through custom proxy (Nikita Sivukhin) * Install sqlite locally to run tests and other scripts (Pedro Muniz) * rename speculative load to prefetch (docs already uses this terminology) (Nikita Sivukhin) * docs: update clippy command in CONTRIBUTING.md to match CI job (Nuno Gonçalves) * stress: Make random seed configurable (Pekka Enberg) * Devcontainer setup (Nikita Sivukhin) * core/mvcc/cursor: return previous max id (Pere Diaz Bou) * Update wording of AI section of PR template (Jussi Saurio) * whopper: Simulate time (Pekka Enberg) * increase lantency check for flaky test in test_read_path.rs (Preston Thorpe) * run get(...) to completion - otherwise INSERT ... RETURNING will be executed incorrectly (Nikita Sivukhin) * ci: run TCL tests for MVCC under CI (Pere Diaz Bou) * Get mutable reference to table in Schema so we can modify it with `Arc::make_mut` (Pedro Muniz) * also check for None `checkpointed_txid_max_old` when determining if `RowVersion` exists in the Db (Pedro Muniz) * Go driver (Nikita Sivukhin) * Minor improvements and refactoring in btree.rs (Preston Thorpe) * revert change in index_scan_compound_key_fuzz (Pedro Muniz) * Make `checkpointed_txid_max_old` be an `Optional` (Pedro Muniz) * Remove some useless clones in pager.rs (Preston Thorpe) * upgrade cargo dist to 0.30.2 (Nikita Sivukhin) * Partial sync improvements (Nikita Sivukhin) * Prevent concurrent tx ctrl and write (Nikita Sivukhin) * core/mvcc/cursor: ignore non visible rows on "last" (Pere Diaz Bou) * core/mvcc/tests: un-ignore seek tests (Pere Diaz Bou) * CI: simulator tweaks (Jussi Saurio) * chore: remove experimental_indexes feature flags (Jussi Saurio) * do not propagate the MvStore to opcodes (Pedro Muniz) * Run BEFORE and AFTER update triggers on upserts (Mikaël Francoeur) * Ignore SQLITE_BUSY during auto-checkpoint (Mikaël Francoeur) * Allocate Page 1 in pager on open (Pedro Muniz) * Prevent creating index on rowid pseudo-column (Mikaël Francoeur) * Improve Android compatibility (Martin Mauch) * Simulator Roadmap (Alperen Keleş) * guard subjournal access within single connection (Nikita Sivukhin) * Turso sdk kit version (Nikita Sivukhin) * Automatically Propagate Encryption options (Pedro Muniz) * core/mvcc: state machines for `prev`, `next`, `exists`, `rewind`, `last` (Pere Diaz Bou) * btree/balance: assert that if multiple overflow cells, they are adjacent sequential (Jussi Saurio) * simulator: generate more INSERT INTO ... SELECT self-inserts (Mikaël Francoeur) * Arc swap MvStore + centralize MvStore acquisition (Pedro Muniz) * mvcc: do not store index data twice in Row (Jussi Saurio) * Col name in trigger subquery (Rohith Suresh) * Update AEGIS crate version (Avinash Sajjanshetty) * SDK kit (Nikita Sivukhin) * Ensure LIKE is case-sensitive for non-ASCII characters (Tejas) * sim/aws: memory IO 100% of the time, differential 50% of the time (Jussi Saurio) * mvcc: reconstruct index rows on logical log recovery (Jussi Saurio) * More mvcc index stuff (Jussi Saurio) * mvcc: make more MvccLazyCursor ops compatible with indexes (Jussi Saurio) * drop triggers if table drops (Pavan Nambi) * Kill unwrap() calls in MVCC module (Pekka Enberg) * Kill unwrap() in vector module (Pekka Enberg) * Kill unwrap() calls in VDBE module (Pekka Enberg) * Tidied imports in Rust binding example without unwrap (Dave Warnock) * Rust binding example without unwrap (Dave Warnock) * Kill unwrap() calls in JSON module (Pekka Enberg) * use i64 for registers p1,p2,p3,p5 in EXPLAIN output (Mikaël Francoeur) * Kill unwrap() in macros (Pekka Enberg) * Kill unwrap in incremental module (Pekka Enberg) * mvcc: refactor RowID.row_id to be either i64 or a record (Jussi Saurio) * Kill unwrap() calls in extensions (Pekka Enberg) * SQLite C API improvements (Nikita Sivukhin) * simulator: only check all tables if we have any tables to check (Pedro Muniz) * core: Switch to parking_lot::Mutex (Pekka Enberg) * Simulator: refactor and simplify `InteractionPlan` (Pedro Muniz) * Enable nested self-inserts in simulator (Mikaël Francoeur) * correct order in column creation in join tests (Pavan Nambi) * Nyrkiö: Set all comment-on to false (Henrik Ingo) * Partial sync basic (Nikita Sivukhin) * Use `AsValueRef` in more functions (Pedro Muniz) * treat parameters as "constant" within a query (Nikita Sivukhin) * Completion: make it Send + Sync (Nikita Sivukhin) * core/mvcc: use btree cursor to navigate rows (Pere Diaz Bou) * core: update aegis (Daeho Ro) * Refactor affinity conversions for reusability (Pedro Muniz) * Create `AsValueRef` trait to allow us to be agnostic over ownership of `Value` or `ValueRef` (Pedro Muniz) * Move value functions to separate file (Pedro Muniz) * Avoid heavy macro (Nikita Sivukhin) * Stop blob json parsing at null terminator (Duy Dang) * core/translate: Remove unused ParamState (Preston Thorpe) * Toy index improvements (Nikita Sivukhin) * use dyn DatabaseStorage instead of DatabaseFile (Nikita Sivukhin) * Prevent DROP TABLE when table is referenced by foreign keys (Joao Faria) * core/vdbe Handle renaming child FK definitions in rename table stmt (Preston Thorpe) * Prevent misuse of subqueries that return multiple columns (Jussi Saurio) * Optimize and refactor schema::Column type (Preston Thorpe) * Clean up Connection::from_uri() by using DatabaseOpts (Rohith Suresh) * Select correct collation sequence for compound select (Pedro Muniz) * core: Disable autovacuum by default (Pekka Enberg) * Make mimalloc dependency optional (Pekka Enberg) * Update Java package version in scripts/update-version.py (Pekka Enberg) ### Fixed * stress: Keep going on I/O errors instead of panicking (Pekka Enberg) * fix: lint warnings unused variable/import in release build (Khashayar Fereidani) * integrity check: do not throw errors if pending byte page is never used (Pedro Muniz) * fix(storage): improve error message for truncated database files (Srinivas A) * fix/pager&wal: ensure wal write lock held when rolling back frame_cache (Jussi Saurio) * Enable debug_assertions for antithesis profile (Pekka Enberg) * General improvements, micro-optimizations and bug fix for core/functions (Khashayar Fereidani) * fix dockerfiles (Jussi Saurio) * Fix DROP TABLE to properly handle FK actions and allow for orphaned/NULL FK refs (Preston Thorpe) * core/mvcc/logical_log: off by one error reading logical log encrypted (Pere Diaz Bou) * Fix squeue overflow issue in io_uring (Preston Thorpe) * Affinity fixes (Pedro Muniz) * Read only fixes (Pedro Muniz) * slightly adjust fixed unstable test (Nikita Sivukhin) * CI test setup fixes + fix GroupCompletion bug (Pedro Muniz) * pyturso: fix panic (Nikita Sivukhin) * fix(core/translate): apply affinity conversion to hash join build and probe keys (Nuno Gonçalves) * fix/core: fix transaction issues (Jussi Saurio) * sim: fix apply_snapshot for create table, create index, drop column (Jussi Saurio) * fix stack overflow in long unary expressions ("' from Jussi Saurio) * Fix RTRIM ignoring trailing tabs (Krishna Vishal) * Fix incorrect conversion from TEXT to INTEGER when text is a number followed by a trailing non-breaking space (Krishna Vishal) * fix stack overflow in long unary expressions (") * Sync fixes (Nikita Sivukhin) * fix(core): prevent ALTER COLUMN from resulting in tables with only generated columns (Nuno Gonçalves) * core/storage: fixes for the commit path and `io_uring` (Preston Thorpe) * aws/sim: fixes and tweaks (Jussi Saurio) * fix succeeded check (Pedro Muniz) * fix/sim: all alter table ops must be recorded and applied in order (Jussi Saurio) * fix coroutine panic: replace ended_coroutine Bitfield with vec (Jussi Saurio) * Fix: update schema if DDL commit succeeded but checkpoint failed (Jussi Saurio) * Fix race condition in WAL frame_cache update with io_uring (Mikaël Francoeur) * fix(json): properly serialize infinite values (Nuno Gonçalves) * fix(core/util): reject integer primary key underflow (Nuno Gonçalves) * fix/core: decouple autocheckpoint result from transaction durability (Jussi Saurio) * Fix StreamingWalReader behavior with checksums of uncommitted frames (Preston Thorpe) * Fix ignored completion in free_page (Preston Thorpe) * Fix more instances of marking pages dirty after modification (Jussi Saurio) * fix/pager: mark freelist trunk page dirty BEFORE modifying it (Jussi Saurio) * fix/sim: modify rows in ALTER TABLE properly (Jussi Saurio) * core: Fix integrity_check pragma code generation (Pekka Enberg) * antithesis: Fix unique constraint exception handling in stress-composer tests (Pekka Enberg) * Fix Github go workflow (Nikita Sivukhin) * sim: fix bug in apply_snapshot (Jussi Saurio) * Sync better error messages (Nikita Sivukhin) * Fix CTE scope propagation for compound SELECTs (Martin Mauch) * Sqlite3 compat fix (Nikita Sivukhin) * Sim transaction fixes (Jussi Saurio) * sim/aws: comment on existing issues instead of skipping duplicates (Jussi Saurio) * Fix complex unique sqlite3 compat (Nikita Sivukhin) * fix/btree: disable move_to_rightmost optimization with triggers (Jussi Saurio) * Fix descending index scan returning rows when seek key is NULL (Jussi Saurio) * Fix external sorter losing rows when chunks need async IO (Jussi Saurio) * sim: stop ignoring sql execution errors (Jussi Saurio) * Fix two bugs with compound selects (Jussi Saurio) * Fix IN operator translation logic (Nikita Sivukhin) * fix/mvcc: seek() must seek from both mv store and btree (Jussi Saurio) * Fix panic in optimizer when usable constraint refs is empty (Preston Thorpe) * optimizer: fix incorrect index_col_pos assigned when multiple constraints ref same join key (Preston Thorpe) * Bloom filter fixes (Preston Thorpe) * fix: escape backslashes in json_object string values (Martin Mauch) * fix/mvcc: use existing schema object in mvcc bootstrap (Jussi Saurio) * Mvcc bugfixes (Jussi Saurio) * Fix vtab memory leak (Nikita Sivukhin) * Fix comparison of large numbers (Mikaël Francoeur) * Fix to Google Books link in CONTRIBUTING (Juan V. García) * core/mvcc: fix `exists` to use `BTreeCursor` as fallback (Pere Diaz Bou) * core/io: Improve error handling (Pekka Enberg) * core/index_method: Improve error handling in toy_vector_spare_ivf.rs (Pekka Enberg) * Triggers: fix issues with ALTER TABLE (Jussi Saurio) * Return parse error if NULLS LAST used in ORDER BY (Jussi Saurio) * Fix: Drop internal DBSP table when dropping materialized view (Martin Mauch) * Fix seek not applying correct affinity to seek expr (Pedro Muniz) * Fix EXISTS on LEFT JOIN null rows (Duy Dang) * Fix error handling on provided insert column count mismatch (Jussi Saurio) * core/vdbe: Fix incorrect `unreachable` condition in op_seek_rowid (Preston Thorpe) * Update and fix nix build (Alexander Hirner) * Fix INSERT UNION ALL (Duy Dang) * Fix LEFT JOIN subqueries reusing stale right-side values (Duy Dang) * Throw an error in case duplicate CTE names are found (Rohith Suresh) * Fix self-insert SUM when table uses INTEGER PRIMARY KEY (Duy Dang) # Changelog ## 0.3.0 -- 2025-10-30 ### Added * Implement wasNull tracking in ResultSet getter methods (김민석) * Support subqueries in all positions of a SELECT statement (Jussi Saurio) * Initialize LIMIT after after ORDER BY / GROUP BY initialization (Jussi Saurio) * index_method: implement basic trait and simple toy index (Nikita Sivukhin) * Where clause subquery support (Jussi Saurio) * sqlite3: Add multi-statement support for sqlite3_exec() (Preston Thorpe) * Add DISTINCT support to aggregate operator (Glauber Costa) * docs: Add vector search section to database manual (Pekka Enberg) * Support statement-level rollback via anonymous savepoints (Jussi Saurio) * Add AtomicEnum proc macro to generate atomic wrappers to replace RwLocks (Preston Thorpe) * Fix git directory resolution in simulator to support worktrees (Jussi Saurio) * Add Miri support for turso_stress, with bash scripts to run (Bob Peterson) * tests: Add rowid alias fuzz test case (Pekka Enberg) * core/translate: throw parse error on unsupported GENERATED column constraints (Preston Thorpe) * translate/insert: more refactoring and support INSERT OR IGNORE (Preston Thorpe) * sql_generation: Fix implementation of LTValue and GTValue for Text types (Jussi Saurio) * core/mvcc: implement CursorTrait on MVCC cursor (Pere Diaz Bou) * Add test case for vector() format crash (Pedro Muniz) * Add correct unique constraint test for tcl (Pedro Muniz) * stress: Add busy timeout support with 5 second default (Pekka Enberg) * Add WINDOW functions to COMPAT.md (Jussi Saurio) * core/translate: Add if alias and allow iff to have more arguments (Pavan Nambi) * core/btree: try to introduce trait for cursors (Pere Diaz Bou) * bindings/java: Add support for publishing to Maven Central (Kim Seon Woo) * add Calendar-based timezone conversion support in JDBC4ResultSet (김민석) * Add Nightly versions of benchmarks that run on Nyrkiö runners (Henrik Ingo) * Add support for sqlite_version() star syntax (Glauber Costa) * core/translate: implement basic foreign key constraint support (Preston Thorpe) * Simulator: Add Drop and pave the way for Schema changes (Pedro Muniz) * core/io: remove new_dummy in place of new_yield (Pere Diaz Bou) * Add MVCC checkpoint threshold pragma (bit-aloo) * core/incremental: Implement "is null" and "is not null" tests for view filter (Glauber Costa) * core/mvcc: implement PartialOrd for RowId (Pere Diaz Bou) * core/io: Add completion group API for managing multiple I/O operations (Pekka Enberg) * Add short writes to unreliable-libc (FamHaggs) * add basic examples for database-wasm package (Nikita Sivukhin) * core/wal: introduce transaction_count, same as iChange in sqlite (Pere Diaz Bou) ### Updated * antithesis: Upload config image in GitHub Actions workflow (Pekka Enberg) * perf/throughput: Improve reproducibility (Pekka Enberg) * translate: disallow correlated subqueries in HAVING and ORDER BY (Jussi Saurio) * Stmt reset cursors (Nikita Sivukhin) * reset move_to_right_state cached state in case of quick balancing (Nikita Sivukhin) * index_method: fully integrate into query planner (Nikita Sivukhin) * core: Switch to FxHash to improve performance (Pekka Enberg) * bindings/rust: Enable mimalloc as global allocator (Pekka Enberg) * index method syntax extension (Nikita Sivukhin) * Tighten Nyrkio p-value to 0.00001 (Henrik Ingo) * Strict numeric cast for op_must_be_int (bit-aloo) * core/vdbe: Reuse cursor in op_open_write() (Pekka Enberg) * core: Switch RwLock> to ArcSwap (Pekka Enberg) * Always returns Floats for sum and avg on DBSP aggregations (Glauber Costa) * perf/throughput: Use connection per transaction in rusqlite benchmark (Pekka Enberg) * Return null terminated strings from sqlite3_column_text (Preston Thorpe) * Order by heap sort (Nikita Sivukhin) * core/storage: Cache schema cookie in Pager (Pekka Enberg) * github: Run fuzz tests in a separate workflow (Pekka Enberg) * tests: Separate integration and fuzz tests (Pekka Enberg) * Vector speedup (Nikita Sivukhin) * parser: translate boolean values to literal when parsing column constraints (Preston Thorpe) * core/io: Make random generation deterministically simulated (Pedro Muniz) * core: move BTreeCursor under MVCC cursor (Pere Diaz Bou) * Move completion code to separate file (Pedro Muniz) * avoid unnecessary allocations (Nikita Sivukhin) * Make sure explicit column aliases have binding precedence in orderby (Pavan Nambi) * tests/integration: Reduce collation fuzz test iterations (Pekka Enberg) * Switch random blob creation to `get_random` (Pedro Muniz) * Shared WAL lock scoping (Pedro Muniz) * Remove tests that alter testing.db from views.test (Preston Thorpe) * tests/integration: Disable rowid alias differential fuzz test case (Pekka Enberg) * core/storage: Reduce logging level (Pekka Enberg) * cli: Improve manual page display (Pavan Nambi) * stress: prevent thread from holding write lock and then stopping (Jussi Saurio) * translate/select: prevent multiple identical non-aliased table references (Preston Thorpe) * Prevent on conflict column definitions on CREATE TABLE or opening DB (Preston Thorpe) * cli: .tables and .indexes to show data from attached tables aswell (Konstantinos Artopoulos) * bindings/rust: propagate the DropBehavior of a dropped tx to next access of DB, instead of panicing (Jussi Saurio) * Replace io_yield_many with completion groups (Pekka Enberg) * core/bree: remove duplicated code in BTreeCursor (Pere Diaz Bou) * core: Unsafe Send and Sync pushdown (Pekka Enberg) * Run SQLite integrity check after stress test run (Pedro Muniz) * Document ThreadSanitizer in CONTRIBUTING.md (Pekka Enberg) * tests/fuzz: Accept SEED env var for all fuzz tests (Preston Thorpe) * Make Rust bindings actually async (Pedro Muniz) * perf/throughput: force sqlite to use fullfsync (Pedro Muniz) * relax check in the vector test (Nikita Sivukhin) * Allow using indexes to iterate rows in UPDATE statements (Jussi Saurio) * Refactor INSERT translation to a modular setup with emitter context (Preston Thorpe) * increment Changes() only once conditionally (Pavan Nambi) * make comparison case sensitive (Pavan Nambi) * bindings/rust: Bump version recommendation to 0.2 (Kyle Kelley) * Run simulator under Miri (Bob Peterson) * Import workspace crates by name and not path (Pedro Muniz) * names shall not be shared between tables,indexs,vtabs,views (Pavan Nambi) * Get aliases to where shall they be used (Pavan Nambi) * remove cfg for `MAP_ANONYMOUS` (Pedro Muniz) * Simulator: `Drop Index` (Pedro Muniz) * Restrict joins to max 63 tables and allow arbitrary number of table columns (Jussi Saurio) * Simulator: persist files in sim memory IO for integrity check (Pedro Muniz) * Simulator: `ALTER TABLE` (Pedro Muniz) * Move all checksum tests behind the feature flag (Avinash Sajjanshetty) * Nyrkiö nightly: Reduce frequency to 1 per 24h (Henrik Ingo) * Vector improvements (Nikita Sivukhin) * Optimize sorter (Jussi Saurio) * Make sqlite_version() compatible with SQLite (Glauber Costa) * optimizer: optimize range scans to use upper and lower bounds more efficiently (Jussi Saurio) * translate: make bind_and_rewrite_expr() reject unbound identifiers if no referenced tables exist (Jussi Saurio) * Simulator: ignore `Property::AllTableHaveExpectedContent` when counting stats (Pedro Muniz) * When pwritev fails, clear the dirty pages (Pedro Muniz) * mvcc: Disable automatic checkpointing by default (Pekka Enberg) * Integrity check enhancements (Jussi Saurio) * Remove unsafe pointers (`RawSlice`) from `RefValue` (Levy A.) * Make table name not repeat in simulator (bit-aloo) * perf/throughput: Delete database before benchmark run (Pekka Enberg) * emit proper column information for explain prepared statements (Nikita Sivukhin) * core/mvcc/logical-log: switch RwLock to parking_lot (Pere Diaz Bou) * Modify Interactions Generation to only generate possible queries (Pedro Muniz) * eliminate the need for another `Once` in Completion (Pedro Muniz) * Rename Completion methods (Pedro Muniz) * Top level examples (Nikita Sivukhin) * docs: Explain BEGIN CONCURRENT (Pekka Enberg) * MVCC: do checkpoint writes in ascending order of rowid (Jussi Saurio) * core/mvcc: filter out seek results where is not same table_id (Pere Diaz Bou) * Simulator diff print (Pedro Muniz) * Improve simulator cli (bit-aloo) * core/mvcc: automatic logical log checkpointing on commit (Pere Diaz Bou) * remove `dyn DatabaseStorage` replace it with `DatabaseFile` (Pedro Muniz) * Actually enforce uniqueness in create unique index (Jussi Saurio) * core/wal: check index header on begin_write_tx (Pere Diaz Bou) * Disallow unexpected interop between WAL mode and MVCC mode (Jussi Saurio) ### Fixed * Fix database state going back in time after sync (Nikita Sivukhin) * Fix foreign key constraint enforcement on UNIQUE indexes (Jussi Saurio) * bindings/javascript: Improve open error messages (Pekka Enberg) * core/storage: Fix WAL already enabled issue (Pekka Enberg) * Return better syntax error messages (Diego Reis) * core/vdbe: fix ALTER COLUMN to propagate constraints to other table references (Preston Thorpe) * core/translate: fix ALTER COLUMN to propagate other constraint references (Preston Thorpe") * core/translate: fix ALTER COLUMN to propagate other constraint references (Preston Thorpe) * Fix deferred FK violations check before committing to WAL (Jussi Saurio) * translate/select: Fix rewriting Rowid expression when no btree table exists in joined table refs (Preston Thorpe) * Throw parse error on CHECK constraint in create table (Preston Thorpe) * Fix: rolling back tx on Error should set autocommit to true (Jussi Saurio) * Fix: outer CTEs should be available in subqueries (Jussi Saurio) * Fix change counter incrementation (Jussi Saurio) * Fix another "should have been rewritten" translation panic (Jussi Saurio) * Simulator: fix alter table shadowing to modify index column name (Pedro Muniz) * fix backwards compatible rowid alias behaviour (Pedro Muniz) * Fix typo in manual.md (Yevhen Kostryka) * core/vdbe: Improve IdxDelete error messages with context (Pekka Enberg) * Fix disallow reserved prefixes in ALTER TABLE RENAME TO (xmchx) * Fix incorrectly using an equality constraint twice for index seek (Jussi Saurio) * Fix IN operator NULL handling (Diego Reis) * Cleanup Simulator + Fix Column constraints in sql generation (Pedro Muniz) * Fix rusqlite compatibility claim (Dave Warnock) * Fix re-entrancy of op_destroy (used by DROP TABLE) (Jussi Saurio) * Fix VDBE program abort (Nikita Sivukhin) * Fix attach I/O error with in-memory databases (Preston Thorpe) * core/incremental: Fix re-insertion of data with same key (Glauber Costa) * core/io/unix: Fix short writes in try_pwritev_raw() (FamHaggs) ## 0.2.0 -- 2025-10-03 ### Added * docs: Add section on MVCC limitations (Pekka Enberg) * sim: add Profile::SimpleMvcc (Jussi Saurio) * Add encryption internals docs (Avinash Sajjanshetty) * core/storage: Apple platforms support (Charly Delaroche) * Reject unsupported FROM clauses in UPDATE (Mikaël Francoeur) * Add Database::indexes_enabled() (Jussi Saurio) * Add Mold linker setup to CONTRIBUTING.md (Pekka Enberg) * support multiple conflict clauses in upsert (Nikita Sivukhin) * stress: add option to choose how many tables to generate (Pere Diaz Bou) * add manual page about materialized views (Glauber Costa) * support mixed integer and float expressions in the expr_compiler (Glauber Costa) * github: Add 30 minute timeout to all jobs (Pekka Enberg) * Add `CAST` to fuzzer (Levy A.) * Implement json_tree (Mikaël Francoeur) * translate: disable support for UPDATE ... ORDER BY (Jussi Saurio) * MVCC: support alter table (Jussi Saurio) * mvcc: add blocking checkpoint (Jussi Saurio) * Add built-in manual pages for Turso (Glauber Costa) * Support referring to rowid as _rowid_ or oid (Jussi Saurio) * translate: implement Sequence opcode and fix sort order (Preston Thorpe) * mvcc: add blocking checkpoint lock (Jussi Saurio) * translate/emitter: Implement partial indexes (Preston Thorpe) * Support UNION queries in DBSP-based Materialized Views (Glauber Costa) * Add encryption throughput test (Avinash Sajjanshetty) * Support JOINs in DBSP materialized views (Glauber Costa) * Fix C API compatibility tests and add a minimal CI (Andrea Peruffo) * Introduce instruction VTABLE (Lâm Hoàng Phúc) * core/mvcc: introduce with_header for MVCC header update tracking (Pere Diaz Bou) * Inital support for window functions (Piotr Rżysko) * Implement Min/Max aggregators (Glauber Costa) * Add quoted identifier test cases for `ALTER TABLE` (Levy A.) * add perf/throughput/rusqlite to workspace (Pedro Muniz) * add perf/throughput/turso to workspace (Pedro Muniz) * Add per page checksums (Avinash Sajjanshetty) * core/throughput: Add per transaction think time support (Pekka Enberg) * Fix simulator docker build chef by adding whopper directory (Preston Thorpe) * Implement the balance_quick algorithm (Jussi Saurio) * benchmark: introduce simple 1 thread concurrent benchmark for mvcc/sq… (Pere Diaz Bou) * perf: Add simple throughput benchmark (Pekka Enberg) * Add BEGIN CONCURRENT support for MVCC mode (Pekka Enberg) * add explicit usize type annotation to range iterator in test (Denizhan Dakılır) * whopper: A new DST with concurrency (Pekka Enberg) * serverless: Add Connection.reconnect() method (Mayank) * Return parse error for unsupported exprs (Jussi Saurio) * translate: return parse error for unsupported join types (Jussi Saurio) * Add scripts that help debug bugs from simulator (Jussi Saurio) * core: Support ceiling modifier in datetime (Ceferino Patino) ### Updated * CI: run long fuzz tests and stress on every PR, use 2 threads for stress (Jussi Saurio) * Disallow INDEXED BY / NOT INDEXED in select (Jussi Saurio) * core/mvcc: Rename "-lg" to "-log" (Pekka Enberg) * docs: Document more CLI command line options (Pekka Enberg) * Update man pages for encryption (Avinash Sajjanshetty) * core/mvcc: Return completions from logical log methods (Pedro Muniz) * Improve MCP configuration docs (Jamie Barton) * Enable encryption properly in Rust bindings, whopper, and throughput tests (Avinash Sajjanshetty) * Enable checksums only if its opted in via feature flag (Avinash Sajjanshetty) * Sync engine defered sync (Nikita Sivukhin) * core/vdbe: Avoid cloning Arc on every VDBE step (Pekka Enberg) * mvcc: dont try to end pager tx on connection close (Jussi Saurio) * Allow workflow_dispatch for all CI to allow for re-running jobs (Preston Thorpe) * printf should truncates floats (Pavan Nambi) * simulator: reopen database with mvcc and indexes when necessary (Pedro Muniz) * core/storage: Switch page cache queue to linked list (Pekka Enberg) * Improve throughput benchmarks (Pekka Enberg) * Beta (Pekka Enberg) * make connect() method optional and call it implicitly on first query execution (Nikita Sivukhin) * mvcc: dont use mv store for ephemeral tables (Jussi Saurio) * Simulator: Concurrent transactions (Pedro Muniz) * Measure read/write latencies in encryption benchmarks (Avinash Sajjanshetty) * simplify `exec_trim` code + only pattern match on whitespace char (Pedro Muniz) * MVCC: Handle table ID / rootpages properly for both checkpointed and non-checkpointed tables (Jussi Saurio) * Make encryption opt in via flag (Avinash Sajjanshetty) * core/mvcc: Optimize exclusive transaction check (Pekka Enberg) * core: change root_page to i64 (Pere Diaz Bou) * core/storage: Remove unused import from encryption.rs (Pekka Enberg) * small improvement of stress testing tool (Nikita Sivukhin) * sum() can throw integer overflow (Duy Dang) * correct span in ParseUnexpectedToken (Lâm Hoàng Phúc) * Remove double-quoted identifier assert (Diego Reis) * substr scalar should also work with non-text values (Diego Reis) * core: Disallow CREATE INDEX when MVCC is enabled (Pekka Enberg) * javascript: Rename "browser" packages to "wasm" (Pekka Enberg) * core/vdbe: Wrap Program::n_change with AtomicI64 (Pekka Enberg) * use explicit null if it set instead of column default value (Nikita Sivukhin) * Improve encryption module (Avinash Sajjanshetty) * Remove vendored parser now that we have our own (Preston Thorpe) * core/storage: Wrap Pager::commit_info with RwLock (Pekka Enberg) * core/storage: Wrap WalFile::{max,min}_frame with AtomicU64 (Pekka Enberg) * core/storage: Wrap WalFile::max_frame_read_lock_index with AtomicUsize (Pekka Enberg) * core/storage: Mark Page as Send and Sync (Pekka Enberg) * Move turso.png image to assets directory (Preston Thorpe) * core/translate: rewrite default column value from identifier to string literal (Preston Thorpe) * core/translate: Persist NOT NULL column constraint to schema table (Preston Thorpe) * Sqlean fuzzy string (Danawan Bimantoro) * Make Sorter Send and Sync (Pekka Enberg) * length shall not count when it sees nullc (Pavan Nambi) * core/storage: Wrap WalFile::syncing with AtomicBool (Pekka Enberg) * Make MVCC code Send and Sync (Pekka Enberg) * core: recover logical log on `Database::connect` (Pere Diaz Bou) * core/storage: Display page category for rowid integrity check failure (Pekka Enberg) * translate: refactor arguments and centralize parameter context (Preston Thorpe) * translate: disallow creating/dropping internal tables (Jussi Saurio) * Improve DBSP view serialization (Glauber Costa) * Disallow multiple primary keys in table definition (Jussi Saurio) * Display nothing for .schema command when table not found (Preston Thorpe) * Make Connection Send (Pekka Enberg) * core/mvcc/logical-log: refactor get log path in tests (Pere Diaz Bou) * Disallow ORDER BY and LIMIT in a non-compound VALUES() statement (Jussi Saurio) * core: Wrap Connection::mv_tx with RwLock (Pekka Enberg) * Autoincrement (Pavan Nambi) * core/mvcc/logical-log: load logical log from disk (Pere Diaz Bou) * Normalize target table name identifier on table or column rename (Iaroslav Zeigerman) * use a different seed for `gen_rng` (Pedro Muniz) * core: Wrap Connection::attached_databases with RwLock (Pekka Enberg) * core/mvcc/logical-log: on disk format for logical log (Pere Diaz Bou) * Wrap more Connection fields with atomics (Pekka Enberg) * core/mvcc: Wrap Transaction::database_header with RwLock (Pekka Enberg) * core: Wrap Connection::capture_data_changes in RwLock (Pekka Enberg) * antithesis-tests: Rename "utils.py" to "helper_utils.py" (Pekka Enberg) * Make some Connection fields atomic (Pekka Enberg) * Simulator Runtime generation (Pedro Muniz) * Use SQL over HTTP batch statements for sync push (Nikita Sivukhin) * JavaScript bindings browser tests (Nikita Sivukhin) * core: Wrap Connection::transaction_state with RwLock (Pekka Enberg) * core: Wrap Connection::autocommit in AtomicBool (Pekka Enberg) * use wasm-runtime from NPM instead of patched sources (Nikita Sivukhin) * core: Wrap Connection::database_schemas in RwLock (Pekka Enberg) * core: Wrap Connection::schema in RwLock (Pekka Enberg) * Stop incrementing n_changes for idx delete (Preston Thorpe) * core: Wrap Connection::pager in RwLock (Pekka Enberg) * Disable extension loading at runtime (Preston Thorpe) * mvcc: simplify StateMachine (Jussi Saurio) * core: Wrap Pager::io_ctx in RwLock (Pekka Enberg) * Enable checksum tests if checksum feature is on (Kacper Kołodziej) * Wrap Pager vacuum state in RwLock (Pekka Enberg) * Enhancement to Sim Snapshot isolation code (Pedro Muniz) * core/io: Ensure callbacks are invoked once (Pedro Muniz) * Upgrade dist to 0.30.0 (Pekka Enberg) * Sync improvements (Nikita Sivukhin) * Remove some unnecessary unsafe impls (Pedro Muniz) * Pragma busy timeout (Nikita Sivukhin) * translate/optimize: centralize AST/expr traversal (Preston Thorpe) * prevent alter table with materialized views (Glauber Costa) * core/mvcc: Wrap LogicalLog in RwLock (Pekka Enberg) * mvcc: remove unused code related to is_logical_log() (Jussi Saurio) * Put the unused variable behind a flag as intended (Avinash Sajjanshetty) * whopper: Gracefully handle file size limits in simulator (Avinash Sajjanshetty) * core/storage: Wrap Pager::header_ref_state in RwLock (Pekka Enberg) * core/mvcc: Kill noop storage (Pekka Enberg) * core/mvcc: LogicalLog simple append serializer (Pere Diaz Bou) * core/storage: Wrap Pager::free_page_state with RwLock (Pekka Enberg) * core: Rename Connection::_db to db (Pekka Enberg) * core/storage: Switch Pager::max_page_count to AtomicU32 (Pekka Enberg) * core/storage: Use AtomicU16 for Pager::reserved_space (Pekka Enberg) * remove io.blocks from btree balancing code (Nikita Sivukhin) * core: Use sequential consistency for atomics by default (Pekka Enberg) * core/storage: Use AtomicU32 for Pager::page_size (Pekka Enberg) * Convert more Pager fields towards being Send (Pekka Enberg) * More async (Nikita Sivukhin) * Enable encryption option in Whopper (Avinash Sajjanshetty) * Compat: Translate the 2nd argument of group_concat / string_agg (Iaroslav Zeigerman) * Reduce allocations needed for `break_predicate_at_and_boundaries` (Lâm Hoàng Phúc) * Simulator Multiple Connections (Pedro Muniz) * translation: rewrite expressions and properly handle quoted identifiers in UPSERT (Preston Thorpe) * Remove serialization of normal write/commit path (Preston Thorpe) * core/vtab: Wrap InternalVirtualTable with RwLock (Pekka Enberg) * Clean up encryption feature flag usage (Avinash Sajjanshetty) * core/storage: Wrap Pager::checkpoint_state in RwLock (Pekka Enberg) * core: Wrap Pager dirty_pages in RwLock (Pekka Enberg) * core: Wrap MvCursor in Arc> (Pekka Enberg) * core/incremental: Wrap ViewTransactionState in Arc (Pekka Enberg) * core/function: Wrap ExtFunc in Arc (Pekka Enberg) * core/vtab: Mark VTabModuleImpl as Send and Sync (Pekka Enberg) * core/vtab: Use AtomicPtr for table_ptr (Pekka Enberg) * Remove LimboResult enum and InsnFunctionStepResult::Busy variant (Jussi Saurio) * core: Wrap symbol table with RwLock (Pekka Enberg) * core/ext: Switch vtab_modules from Rc to Arc (Pekka Enberg) * core/storage: Clean up unused import warning in encryption.rs (Pekka Enberg) * core: Convert Rc to Arc (Pekka Enberg) * whopper: Handle write-write conflict (Pekka Enberg) * mvcc: handle properly the case where starting pager read tx fails with busy (Jussi Saurio) * core/mvcc: Specify level for tracing (Pekka Enberg) * Switch to GitHub runners for performance workflows (Diego Reis) * Move common dependencies to workspace (Pedro Muniz) * avoid unnecessary cloning when formatting Txn for Display (Avinash Sajjanshetty) * Busy handler (Pedro Muniz) * test/fuzz: improve maintainability/usability of tx isolation test (Jussi Saurio) * mvcc: Complete commit state machine early if write set is empty (Jussi Saurio) * make whopper run with checksums (Avinash Sajjanshetty) * Whopper + MVCC (Pekka Enberg) * Dont grab page cache write lock in a loop (Preston Thorpe) * perf/throughput/turso: Async transactions with concurrent mode (Pekka Enberg) * Handle partial writes in unix IO for pwrite and pwritev (Preston Thorpe) * remove Stmt clone (Lâm Hoàng Phúc) * core/storage: Remove unused import warning (Pekka Enberg) * Handle `EXPLAIN QUERY PLAN` like SQLite (Lâm Hoàng Phúc) * Update epoch on each checkpoint to prevent using stale pages for backfilling (Preston Thorpe) * MVCC: remove reliance on BTreeCursor::has_record() (Jussi Saurio) * Refactor UPSERT to use wal_expr_mut to walk AST. (Preston Thorpe) * Commit uncommitted whopper lockfile (Jussi Saurio) * core/schema: Optimize get_dependent_materialized_views() when no views (Pekka Enberg) * core/mvcc: Eliminate RwLock wrapping Transaction (Pekka Enberg) * bindings/java: PreparedStatement executeUpdate (zongkx) * handle `EXPLAIN` like sqlite (Lâm Hoàng Phúc) * Document DEFERRED and IMMEDIATE transaction modes (Pekka Enberg) * Refactor parseschema (Jussi Saurio) * Remove some traces in super hot paths in btree (Preston Thorpe) * Sync package opfs (Nikita Sivukhin) * Ensure that Connection::query() checks whether its schema is up to date (Jussi Saurio) * refactor cli: `readline` will write to `input_buf` (Lâm Hoàng Phúc) * check freelist count in integrity check (Jussi Saurio) * Enable the use of indexes in DELETE statements (Jussi Saurio) * core: Rename IO::run_once() to IO::step() (Pekka Enberg) * simulator: Clean up code to use extract_if() (Pavan Nambi) ### Fixed * fix sync-engine bug when auth token is provided as dynamic function (Nikita Sivukhin) * Fix COLLATE (Jussi Saurio) * core/translate: fix rowid affinity (Preston Thorpe) * Improve error handling for cyclic views (Duy Dang) * Fix MVCC drop table (Jussi Saurio) * Fix MVCC startup infinite loop when using existing DB (Jussi Saurio) * Fix: JOIN USING should pick columns from left table, not right (Jussi Saurio) * fix/vdbe: reset op_transaction state properly (Jussi Saurio) * Resolve appropriate column name for rowid alias/PK (Preston Thorpe) * fix/mvcc: deserialize table_id as i64 (Jussi Saurio) * Substr fix UTF-8 (Pedro Muniz) * fix/mvcc: set log offset to end of file after recovery finishes (Jussi Saurio) * Fix index bookkeeping in DROP COLUMN (Jussi Saurio) * Fix self-insert with nested subquery (Mikaël Francoeur) * Fix SQLite database file pending byte page (Pedro Muniz) * Index search fixes (Nikita Sivukhin) * Anonymous params fix (Nikita Sivukhin) * core/vdbe: Fix BEGIN after BEGIN CONCURRENT check (Pekka Enberg) * sum should identify if there is num in strings/prefix of strings (Pavan Nambi) * remove UnterminatedBlockComment error (Lâm Hoàng Phúc) * core/printf: Compatibility tests and fixes for printf() (Luiz Gustavo) * Fix materialized views with complex expressions (Glauber Costa) * Fix column fetch in joins (Glauber Costa) * quoting fix (Nikita Sivukhin) * translate/upsert: fix explicit conflict target of non-rowid primary key in UPSERT (Preston Thorpe) * Fix zero limit (Nikita Sivukhin) * Correct spelling issue in ForeignKey ast node (Preston Thorpe) * resolve column alias after rewritting column access in the expression in returning insert clause (Nikita Sivukhin) * Fix materialized views where clause issues (Glauber Costa) * Fix various ALTER TABLE bugs (Jussi Saurio) * Fix offset variable handling (Nikita Sivukhin) * fix encryption config in the sync-client (Nikita Sivukhin) * fix avg aggregation (Nikita Sivukhin) * Fix CREATE INDEX with quoted identifiers (Iaroslav Zeigerman) * Fix ungrouped aggregate with offset clause (Preston Thorpe) * Fix incorrect "column is ambiguous" error with USING clause (Jussi Saurio) * parser: fix incorrect LIMIT/OFFSET parsing of form LIMIT x,y (Jussi Saurio) * Fix .schema command for empty databases (Diego Reis) * Fix JavaScript bindings (Nikita Sivukhin) * Fix result columns binding precedence (Jussi Saurio) * Fix program counter update in sequence test op (Preston Thorpe) * Fix INSERT INTO t DEFAULT VALUES (Jussi Saurio) * fix: CTE alias resolution in planner (Mayank) * Differential testing fixes (Pedro Muniz) * Fix busy handler (Lâm Hoàng Phúc) * DBSP: Return a parse error for a non-equality join (Glauber Costa) * sqlite3: Fix compatibility test error by canonicalizing path (Samuel Marks) * bugfix: clear reserved space for a reused page (Avinash Sajjanshetty) * Fix MVCC concurrency bugs (Jussi Saurio) * Fix math functions compatibility issues (Levy A.) * simulator: Fix shrinking (Pedro Muniz) * Fix some Rust compilation warnings (Samuel Marks) * translate/insert: fix `program.result_columns` when inserting multiple rows (Preston Thorpe) * stress: Retry sync on error to avoid a panic, take 2 (Pekka Enberg) * translate: couple fixes from testing with Gorm (Preston Thorpe) * Fix is_nonnull returns true on 1 / 0 (Lâm Hoàng Phúc) * Fix 3 different MVCC bugs (Jussi Saurio) * fix re-entrancy issue in Pager::free_page (Jussi Saurio) * stress: Retry sync on error to avoid a panic (Pekka Enberg) * move `divider_cell_is_overflow_cell` to debug assertions (Pedro Muniz) * Fix SharedWalFile deadlock in multithreaded context (Jussi Saurio) * Fix MVCC update (Jussi Saurio) * Various fixes to sync (Nikita Sivukhin) * mvcc: fix hang when CONCURRENT tx tries to commit and non-CONCURRENT tx is active (Jussi Saurio) * mvcc: fix two sources of panic (Jussi Saurio) * Fix MVCC rollback (Jussi Saurio) * Random fixes for MVCC (Jussi Saurio) * core: Panic on fsync() error by default (Pekka Enberg) * fix(btree): advance cursor after interior node replacement in delete (Jussi Saurio) * core/vdbe: Fix BEGIN CONCURRENT transactions (Pekka Enberg) * Fix incompatible math functions (Levy A.) * fix wasm-runtime package.json (Nikita Sivukhin) * fix CI for apple builds (Nikita Sivukhin) * hack imports of wasm due to the issues in Vite and Next.js build systems (Nikita Sivukhin) * Fix tests for views (Preston Thorpe) * Fixes views (Glauber Costa) * core: Fix reprepare to properly reset statement cursors and registers (Pedro Muniz) * Fix automatic indexes (Jussi Saurio) * Fix tx isolation test semantics after #3023 (Jussi Saurio) * Fix: read transaction cannot be allowed to start with a stale max frame (Jussi Saurio) * Fix value conversion for function parameters (Levy A.) * IO: handle errors properly in io_uring (Preston Thorpe) * core: Fix integer/float comparison (Pavan Nambi) * pager: fix incorrect freelist page count bookkeeping (Jussi Saurio) ## 0.1.5 -- 2025-09-10 ### Added * add missing module type for browser package (Nikita Sivukhin) * Implement 2-args json_each (Mikaël Francoeur) * Add OPFS support to JavaScript bindings (Nikita Sivukhin) * test/fuzz: add UPDATE/DELETE fuzz test (Jussi Saurio) * add gen-bigass-database.py (Jussi Saurio) * Add assertion: we read a page with the correct id (Jussi Saurio) * support float without fractional part (Lâm Hoàng Phúc) * expr: use more efficient implementation for binary condition exprs (Jussi Saurio) * Add json_each table-valued function (1-arg only) (Mikaël Francoeur) * Add io_uring support to stress (Pekka Enberg) * Refactor LIMIT/OFFSET handling to support expressions (bit-aloo) * Encryption: add support for other AEGIS and AES-GCM cipher variants (Frank Denis) * introduce package.json for separate *-browser package (both database and sync) (Nikita Sivukhin) * introduce `eq/contains/starts_with/ends_with_ignore_ascii_case` macros (Lâm Hoàng Phúc) * introduce `match_ignore_ascii_case` macro (Lâm Hoàng Phúc) * core: Make strict schema support experimental (Pekka Enberg) * core/printf: support for more basic substitution types (Luiz Gustavo) * Return sqlite_version() without being initialized (Preston Thorpe) * Support encryption for raw WAL frames (Gaurav Sarma) * bindings/java: Implement date, time related methods under JDBC4PreparedStatement (Kim Seon Woo) * Support cipher and encryption key URI options (William Souza) * Implement UPSERT (Preston Thorpe) * CLI: implement `Line` output .mode (Andrey Oskin) * add sqlite integrity check back (Pedro Muniz) * core: Initial pass on synchronous pragma (Pekka Enberg) * Introduce and propagate `IOContext` as required (Avinash Sajjanshetty) * Add some docs on encryption (Avinash Sajjanshetty) * sqlite3: Implement sqlite3_malloc() and sqlite3_free() (Pekka Enberg) * sqlite3: Implement sqlite3_next_stmt() (Pekka Enberg) * core/translate: Add support (Pekka Enberg) * sqlite3: Implement sqlite3_db_filename() (Pekka Enberg) * flake.nix: add uv dependency to nativeBuildInputs (Ceferino Patino) * sqlite3: Implement sqlite3_bind_parameter_index() (Pekka Enberg) * sqlite3: Implement sqlite3_clear_bindings() (Pekka Enberg) * sqlite3: Implement sqlite3_get_autocommit() (Pekka Enberg) * Add support for AEGIS encryption algorithm (Avinash Sajjanshetty) * bindings/java: Implement batch operations for JDBC4Statement (Kim Seon Woo) * Add syntax highlighting for EXPLAIN and ANALYZE (Alex Miller) * Add basic support for ANALYZE statement (Alex Miller) * correctly implement offset() in parser (Lâm Hoàng Phúc) * Switch to new parser in core (Levy A.) * github: Remove Intel Mac support (Pekka Enberg) * add remove_file method to the IO (Nikita Sivukhin) * Add libc fault injection to Antithesis (Pekka Enberg) * core/mvcc: support for MVCC (Pere Diaz Bou) * SQLite C API improvements: add column type and column decltype (Danawan Bimantoro) * Initial pass to support per page encryption (Avinash Sajjanshetty) ### Updated * clean `print_query_result` (Lâm Hoàng Phúc) * update update-script to properly handle JS workspace (Nikita Sivukhin) * no need `QueryStatistics` if `self.opts.timer` is not set (Lâm Hoàng Phúc) * optimizer: convert outer join to inner join if possible (Jussi Saurio) * Handle case where null flag is set in op_column (Jussi Saurio) * remove &1 (Lâm Hoàng Phúc) * reduce cloning `Arc` (Lâm Hoàng Phúc) * Evaluate left join seek key condition again after null row (Jussi Saurio) * use mlugg/setup-zig instead of unmaintained action (Kingsword) * Prevent setting of encryption keys if already set (Gaurav Sarma) * Remove RefCell from Cursor (Pedro Muniz) * Page Cache: optimize and use sieve/Gclock hybird algorithm in place of LRU (Preston Thorpe) * core: handle edge cases for read_varint (Sonny) * Persistence for DBSP-based materialized views (Glauber Costa) * io_uring: prevent out of order operations that could interfere with durability (Preston Thorpe) * core: Simplify WalFileShared life cycle (Pekka Enberg) * prevent modification to system tables. (Glauber Costa) * mark completion as done only after callback will be executed (Nikita Sivukhin) * core/mvcc: make commit_txn return on I/O (Pere Diaz Bou) * windows iterator returns no values for shorter slice (Lâm Hoàng Phúc) * Unify resolution of aggregate functions (Piotr Rżysko) * replace some matches with `match_ignore_ascii_case` macro (Lâm Hoàng Phúc) * Make io_uring sound for connections on multiple threads (Preston Thorpe) * build native package for ARM64 (Nikita Sivukhin) * refactor parser fmt (Lâm Hoàng Phúc) * string sometimes used as identifier quoting (Lâm Hoàng Phúc) * CURRENT_TIMESTAMP can fallback TK_ID (Lâm Hoàng Phúc) * remove `turso_sqlite3_parser` from `turso_parser` (Lâm Hoàng Phúc) * Simulate I/O in memory (Pedro) * Simulate I/O in memory (Pedro Muniz) * Refactor encryption to manage authentication tag internally (bit-aloo) * Unify handling of grouped and ungrouped aggregations (Piotr Rżysko) * Evict page from cache if page is unlocked and unloaded (Pedro Muniz) * Use u64 for file offsets in I/O and calculate such offsets in u64 (Preston Thorpe) * Document how to use CDC (Pavan Nambi) * Upgrade Rust version in simulator build Dockerfile (Preston Thorpe) * Parse booleans to integer literals in expressions (Preston Thorpe) * Simulator Profiles (Pedro Muniz) * Change views to use DBSP circuits (Glauber Costa) * core/wal: cache file size (Pere Diaz Bou) * Remove some code duplication in the CLI (Preston Thorpe) * core/translate: parse_table remove unnecessary clone of table name (Pere Diaz Bou) * Update COMPAT.md to remove CREATE INDEX default disabled (Preston Thorpe) * core/translate: remove unneessary agg clones (Pere Diaz Bou) * core/vdbe: Micro-optimize "zero_or_null" opcode (Pekka Enberg) * translate: with_capacity insns (Pere Diaz Bou) * perf: avoid constructing PageType in helper methods (Jussi Saurio) * refactor/perf: remove BTreePageInner (Jussi Saurio) * Improve integrity check (Nikita Sivukhin) * translate/insert: Improve string format performance (Pere Diaz Bou) * core/schema: get_dependent_materialized_views_unnormalized (Pere Diaz Bou) * core/util: emit literal, cow instead of replace (Pere Diaz Bou) * core/translate: sanize_string fast path improvement (Pere Diaz Bou) * core/io: Switch Unix I/O to use libc::pwrite() (Pekka Enberg) * Update README.md for Go documentation (Preston Thorpe) * improve sync engine (Nikita Sivukhin) * Remove Go bindings (Preston Thorpe) * core/storage: Micro-optimize Pager::commit_dirty_pages() (Pekka Enberg) * Rename Go driver to `turso` to not conflict with sqlite3 (Preston Thorpe) * Refactor: `Cell` instead of `RefCell` to store `CipherMode` in connection (Avinash Sajjanshetty) * Improve documentation of page pinning (Jussi Saurio) * Remove double indirection in the Parser (Pedro Muniz") * Fail CI run if Turso output differs from SQLite in TPC-H queries (Jussi Saurio) * Decouple SQL generation from Simulator crate (Pedro Muniz) * Make fill_cell_payload() safe for async IO and cache spilling (Jussi Saurio) * Remove Windows IO in place of Generic IO (Preston Thorpe) * Improve encryption API (Avinash Sajjanshetty) * Remove double indirection in the Parser (Pedro Muniz) * Update TPC-H running instructions in PERF.md (Alex Miller) * Truncate the WAL on last connection close (Preston Thorpe) * DBSP projection (Pekka Enberg) * Use vectored I/O for appending WAL frames (Preston Thorpe) * Remove unnecessary argument from Pager::end_tx() (Nikita Sivukhin) * refactor/btree: rewrite the find_free_cell() function (Jussi Saurio) * refactor/btree: rewrite the free_cell_range() function (Jussi Saurio) * Remove Result from signature (Mikaël Francoeur) * Remove duplicated attribute in (bit-aloo) * reduce cloning Token in parser (Lâm Hoàng Phúc) * refactor encryption module and make it configurable (Avinash Sajjanshetty) * Replace a couple refcells for types that trivially impl Copy (Preston Thorpe) * wal-api: allow to mix frames insert with SQL execution (Nikita Sivukhin) * move check code into parser (Lâm Hoàng Phúc) * Serialize compat tests and use Mutex::lock() instead of Mutex::try_lock() in UnixIO (Jussi Saurio) * sim: remove "run_once faults" (Jussi Saurio) * should not return a Completion when there is a page cache hit (Pedro Muniz) * github: Reduce Python build matrix (Pekka Enberg) * Page cache truncate (Nikita Sivukhin) * Wal api checkpoint seq (Nikita Sivukhin) * Use more structured approach in translate_insert (Jussi Saurio) * Remove hardcoded flag usage in DBHeader for encryption (Avinash Sajjanshetty) * properly execute pragmas - they may require some IO (Nikita Sivukhin) * Wal checkpoint upper bound (Nikita Sivukhin) * Improve WAL checkpointing performance (Preston Thorpe) * core/mvcc: store txid in conn and reset transaction state on commit (Pere Diaz Bou) * core/mvcc: start first rowid at 1 (Pere Diaz Bou) * refactor/vdbe: move insert-related seeking to VDBE from BTreeCursor (Jussi Saurio) ### Fixed * Fix clear_page_cache method and rollback (Preston Thorpe) * Fix read_entire_wal_dumb: incrementally build the frame cache (Preston Thorpe) * Fix merge script to prompt if tests are still in progress (Preston Thorpe) * SQL generation fixes (Pekka Enberg) * Fix affinity handling in MakeRecord (Pekka Enberg) * Fix infinite loop when IO failure happens on allocating first page (Preston Thorpe) * Fix crash in Next opcode if cursor stack has no pages (Jussi Saurio) * cli: Fix dump compatibility in "PRAGMA foreign_keys" (Pekka Enberg) * Small fixes (Nikita Sivukhin) * Avoid allocating and then immediately fallbacking errors in affinity (Jussi Saurio) * Fix float formatting and comparison + Blob concat (Levy A.) * Fix infinite loop when query starts comment token ("--") (Lâm Hoàng Phúc) * Fix sqlite3 test cases (Pekka Enberg) * Fix non-determinism in simulator (Pedro Muniz) * Fix column count in ImmutableRow (Glauber Costa) * Fix memory leak in page cache during balancing (Preston Thorpe) * Fix `sim-schema` command (Pedro Muniz) * Propagate decryption error from the callback (Avinash Sajjanshetty) * Fix sorter column deduplication (Piotr Rżysko) * Fix missing functions after revert (Pedro Muniz) * ci: fix merge-pr issue to escape command-line backticks (Ceferino Patino) * Fix several issues with integrity_check (Jussi Saurio) * core/io: Fix build on Android and iOS (Pekka Enberg) * WAL txn: fix reads from DB file (Nikita Sivukhin) * Fix blob type handling in JavaScript (Pekka Enberg) * Fix: all indexes need to be updated if the rowid changes (Jussi Saurio) * Fix: in UPDATE, insert rowid into index instead of NULL (Jussi Saurio) * Fix: normalize table name in DELETE (Jussi Saurio) ## 0.1.4 -- 2025-08-20 ### Added * bindings/rust: Add method (Pekka Enberg) * Add helper to convert io::clock::Instant to useable format (Preston Thorpe) * bindings/javascript: Add TypeScript declarations to package (Pekka Enberg) * add missing closing tag (Glauber Costa) * add metrics and implement the .stats command (Glauber Costa) * Add bench-sqlite script and makefile command (Preston Thorpe) * Fix simulator docker build by adding new sync directory (Preston Thorpe) * core/mvcc: schema_did_change support and find last valid version (Pere Diaz Bou) * Add list databases and open database commands to the MCP server (Glauber Costa) * Add documentation and rename functions (Mikaël Francoeur) * Add io_yield macros to reduce boilerplate (Preston Thorpe) * Add --keep-files flag to allow for inspection of files for successful simulator runs (Preston Thorpe) * core/printf: support for the %i operand (Luiz Gustavo) * Add parser to Dockerfiles for cargo chef (Jussi Saurio) * Add framework for testing extensions in TCL (Piotr Rżysko) * Fix WAL initialization to last committed frame (Nikita Sivukhin) * sim: add Property::TableHasExpectedContent (Jussi Saurio) * Properly implement CLI command (Preston Thorpe) * docs: add Claude Code MCP integration guide (Braden Wong) * Add assertion for expected write amount in writev callback (Preston Thorpe) * sim: add Property::ReadYourUpdatesBack (Jussi Saurio) * Add support for unlikely(X) (bit-aloo) * Implement normal views (Glauber Costa) * turso-sync: support checkpoint (Nikita Sivukhin) * bindings/javascript: Add async connect() function (Pekka Enberg) * Direct schema mutation – add RenameColumn instruction (Levy A.) * Implement Aggregations for DBSP views (Glauber Costa) * stop silently ignoring unsupported features in incremental view where clauses (Jussi Saurio) * turso-sync: support updates and schema changes (Nikita Sivukhin) * turso-cdc: add updates column for cdc table (Nikita Sivukhin) * docs: improve README initialization section clarity (Braden Wong) * Add support for Full checkpoint mode in the WAL (Preston Thorpe) * Add support for PRAGMA freelist_count (bit-aloo) * Initial pass on incremental view maintenance with DBSP (Glauber Costa) * SQLite C API improvements: add bind_text and bind_blob (Danawan Bimantoro) * perf/clickbench: enable rest of queries since we support DISTINCT and REGEXP_REPLACE (Jussi Saurio) * Add table name to the delete bytecode (Glauber Costa) * Initial pass on incremental view maintenance with DBSP (Glauber Costa) * docs: fix CLI command and add homebrew install instructions for MacOS (Mattia) * Reimplement LimboRwLock in the WAL (Preston Thorpe) * BufferPool: add arena backed pool to support fixed opcodes and coalescing (Preston Thorpe) * translate: return parse errors for unsupported features instead of silently ignoring (Jussi Saurio) * Add query only pragma (bit-aloo) * Direct schema mutation – add instruction (Levy A.) * Add .clone CLI command to copy database files (Preston Thorpe) * PageContent: make read_x/write_x methods private and add dedicated methods (Jussi Saurio) * SQLite C API improvements: add basic bind and column functions (Danawan Bimantoro) * javascript: Implement Statement.iterate() (Pekka Enberg) * Fix panic on loading extension on brand new connection (Preston Thorpe) * implement the MaxPgCount opcode (Glauber Costa) * Direct schema mutation – add instruction (Levy A.) * Add regexp capture (bit-aloo) * Add load_insn macro for compiler hint in vdbe::execute hot path (Preston Thorpe) * test/fuzz: add ALTER TABLE column ops to tx isolation fuzz test (Jussi Saurio) * core/mvcc: implement exists (Pere Diaz Bou) * tests/fuzz_transactions: add tests for fuzzing transactions with MVCC (Pere Diaz Bou) * perf/btree: implement fast algorithm for defragment_page (Jussi Saurio) * bindings/rust: add with_mvcc option, open with path too! (Pere Diaz Bou) * core/mvcc: implement seeking operations with rowid (Pere Diaz Bou) * bindings/rust: add with_mvcc option (Pere Diaz Bou) * Add bitmap to track pages in arenas (Preston Thorpe) * Direct schema mutation – add instruction (Levy A.) * core/mvcc: fix new rowid on restart (Pere Diaz Bou) * Implement JavaScript bindings with minimal Rust core (Pekka Enberg) * test/fuzz/transactions: add "PRAGMA wal_checkpoint" to txn isolation fuzz test (Jussi Saurio) * Support the OFFSET clause for Compound select (meteorgan) * Introduce some state machines in preparation for IO Completions refactor (Pedro Muniz) * Add cli Dockerfile (Pere Diaz Bou) * Implement the Cast opcode (Glauber Costa) * Support VALUES clauses for compound select (meteorgan) * fix: add packages to sim/antithesis dockerfiles for cargo-chef (Jussi Saurio) * Add vector_concat and vector_slice support (bit-aloo) * bindings/rust: Add Connection::execute_batch() (Rohith Suresh) * bindings/java: Throw UnsupportedOperationException for unimplemented … (Pekka Enberg) * turso-sync package: initial commit (Nikita Sivukhin) ### Updated * JavaScript improvements (Pekka Enberg) * bindings/javascript: Rename to (Pekka Enberg) * Small pager cleanups (Jussi Saurio) * Do not begin or end transactions in nested statement (Jussi Saurio) * make the MCP server instructions more visible on the README (Glauber Costa) * FaultyQuery enabled by default (Pedro Muniz) * hide our age (Glauber Costa) * make sure our responses are compliant with MCP (Glauber Costa) * Move sync code to own directory (Pekka Enberg) * Refactor: use regular save/restore context mechanism for delete balancing (Jussi Saurio) * Improve handling of inserts with column names (Wallys Ferreira) * emit SetCookie when creating a view (Glauber Costa) * unify halts (Glauber Costa) * Update stale in memory wal header after restarting log (Preston Thorpe) * sync-engine: Use SQL over HTTP instead of WAL push (Nikita Sivukhin) * Convert SQLite parser in Rust by hand (Lâm Hoàng Phúc) * Ensure we fsync the db file in all paths that we checkpoint (Preston Thorpe) * Revive async io extension PR (Preston Thorpe) * sim: reduce frequency of compound selects and complex joins (Jussi Saurio) * Use BufferPool owned by Database instead of a static global (Jussi Saurio) * sync-engine: avoid unnecessary WAL push (Nikita Sivukhin) * use virtual root page for sqlite_schema (Mikaël Francoeur) * io_uring: Gracefully handle submission queue overflow (Preston Thorpe) * Disable unused variables in cargo clippy for CI (Pedro Muniz) * Refactor: atomic ordering (Preston Thorpe) * Document the I/O model (Pedro Muniz) * disable checkpoint: adjust semantic (Nikita Sivukhin) * SDK: enable indices everywhere (Nikita Sivukhin) * Manual updates (Pekka Enberg) * Wait for I/O completions (Pedro Muniz) * IO Cleanups to use and (Pedro Muniz) * move our dbsp-based views to materialized views (Glauber Costa) * More State machines (Pedro Muniz) * Rename page -> slot for arenas + buffer pool (Preston Thorpe) * Unify JavaScript package README files (Pekka Enberg) * simple README with warning (Nikita Sivukhin) * core/wal: Minor checkpointing cleanups and optimizations (Preston Thorpe) * perf/btree: optimize op_column (Jussi Saurio) * Update PERF.md with mobibench instructions (Preston Thorpe) * Handle single, double and unquoted strings in values clause (Mikaël Francoeur) * Use rusqlite 0.37 with bundled SQLite everywhere (Jussi Saurio) * Feat/pragma module list (Lucas Forato) * remove turso-sync as now we have turso-sync-engine (Nikita Sivukhin) * Sorter IO Completions (Pedro Muniz) * Simulator should delete files after a successful run (Pedro Muniz) * turso-sync: js package (Nikita Sivukhin) * global allocator should not be set for library, only for executables (Pedro Muniz) * Evaluate WHERE conditions after LEFT JOIN (Piotr Rżysko) * Btree cache usable space (Jussi Saurio) * Rename JavaScript package to (Pekka Enberg) * perf: a few small insert optimizations (Jussi Saurio) * javascript: Organize test cases better (Pekka Enberg) * only allow multiples of 64 for performance in arena bitmap (Preston Thorpe) * btree: Use correct byte offsets for page 1 in defragmentation (Jussi Saurio) * bench/insert: use PRAGMA synchronous=full (Jussi Saurio) * JavaScript improvements (Pekka Enberg) * turso-sync: rewrite (Nikita Sivukhin) * bench/insert: use locking_mode EXCLUSIVE and journal_mode=WAL for sqlite (Jussi Saurio) * IO More State Machine (Pedro Muniz) * refactor/btree: cleanup write/delete/balancing states (Jussi Saurio) * cdc: emit entries for schema changes (Nikita Sivukhin) * Coll seq (Glauber Costa) * Remove RefCell from Buffer in IO trait methods and PageContents (Preston Thorpe) * Remove Clone impl for Buffer and PageContent (Preston Thorpe) * More state machine + Return IO in places where completions are created (Pedro Muniz) * cleanup: remove unused page uptodate flag (Jussi Saurio) * Relax I/O configuration attribute to cover all Unixes (Pedro Muniz) * Update defragment page to defragment in-place (João Severo) * Integrate virtual tables with optimizer (Piotr Rżysko) * Reprepare Statements when Schema changes (Pedro Muniz) * More State Machines in preparation for tracking IO Completions (Pedro Muniz) * coalesce any adjacent buffers from writev calls into fewer iovecs (Preston Thorpe) * extend raw WAL API with few more methods (Nikita Sivukhin) * Use pwrite for single buffer pwritev call in unix IO (Preston Thorpe) * hide dangerous methods behind conn_raw_api feature (Nikita Sivukhin) * preserve files in IO memory backend (Nikita Sivukhin) * Improve SQLite3 TCL test suite (Pekka Enberg) * Make completions idempotent (Preston Thorpe) * perf/btree: skip seek in move_to_rightmost() if we are already on rightmost page (Jussi Saurio) * perf/pager: dont clear page cache on commit (Jussi Saurio) * Rename liblimbo_sqlite3 to libturso_sqlite3 (Pekka Enberg) * core: Fold HeaderRef to pager module (Pekka Enberg) * perf/vdbe: remove eager cloning in op_comparison (Jussi Saurio) * Update cargo-dist to the latest official version (Hiroaki Yutani) * bindings/rust: Enhance API by removing verbosity (Diego Reis) * use state machine for NoConflict opcode (Mikaël Francoeur) * state_machine: remove State associated type (Pere Diaz Bou) * Single quotes inside a string literal have to be doubled in (Diego Reis) * core/mvcc: Move commit_txn() to generic state machinery (Pere Diaz Bou) * bindings/javascript: Reduce VM/native crossing overhead (Pekka Enberg) * Enable indexes by default (Jussi Saurio) * perf/btree: improve performance of rowid() function (Jussi Saurio) * core/mvcc: Persist changes through pager on commit (Pere Diaz Bou) * Open a temporary on-disk file for ephemeral tables (Jussi Saurio) * Force Sqlite to parse schema on connection benchmark (Levy A.) * more compat police (Glauber Costa) * Bury limbo-wasm (Diego Reis) * IN queries (Glauber Costa) * IN queries (Glauber Costa) * vdbe: Disallow checkpointing in transaction (Jussi Saurio) * Serverless JavaScript driver improvements (Pekka Enberg) * Direct `DatabaseHeader` reads and writes – `with_header` and `with_header_mut` (Levy A.) * refactor/btree: simplify get_next_record()/get_prev_record() (Jussi Saurio) * Accumulate/batch vectored writes when backfilling during checkpoint (Preston Thorpe) * remove non-existent opcode (Glauber Costa) * skip invalid inputs in cosine distance prop test (bit-aloo) * core/mvcc: Switch to parking_lot RwLock (Pekka Enberg) * Rewrite the WAL (Preston Thorpe) * turso-sync: bidirectional sync for local db (Nikita Sivukhin) * Change more function signatures to return Completions (Pedro Muniz) * Clean up conversion between InsnFunctionStepResult and StepResult (Diego Reis) * core/mvcc: simplify mvcc cursor types (Pere Diaz Bou) * bindings/javascript: Run tests serially (Diego Reis) * Javascript testing cleanups (Pekka Enberg) * Javascript API improvements (Pekka Enberg) * Change function signatures to return IO Completions (Pedro Muniz) ### Fixed * Fix page locked panic (Pedro Muniz) * Remove assertions from Completion::complete() and Completion::error() (Jussi Saurio) * Completion Error (Pedro Muniz) * fix/sim: prevent sim from trying to create an existing table or index (Jussi Saurio) * Fail simulator on parse errors (Jussi Saurio) * fix pragma table_info for views (Glauber Costa) * Fix two issues in simulator (Jussi Saurio) * Fix distinct order by (Jussi Saurio) * Fix UPDATE: Do not use an index for iteration if that index is going to be updated (Jussi Saurio) * Fix non-4096 page sizes (Jussi Saurio) * fix: Handle fresh INSERTs in materialized view incremental maintenance (Glauber Costa) * Fix: do computations on usable_space as usize, not as u16 (Jussi Saurio) * Sync engine fixes (Nikita Sivukhin) * Fix tables renaming to existing index and rename indexed columns on (Levy A.) * Fix max_frame determination and comments in WAL checkpointing (Preston Thorpe) * turso-sync: fix schema bug (Nikita Sivukhin) * Fix: Rename clickbench output file limbo/turso (Henrik Ingo) * Reprepare fix on write statement (Pedro Muniz) * Fix view processing in the VDBE (Jussi Saurio) * Fix performance regression in prepare (Piotr Rżysko) * Fix JavaScript bindings packaging (Nikita Sivukhin) * bench/insert: fix expected return value from pragma (Jussi Saurio) * Fix segfault on schema update for virtual tables (Preston Thorpe) * core/btree: fix re-entrancy bug in insert_into_page() (Jussi Saurio) * Fix performance regression (Jussi Saurio) * fix/wal: remove start_pages_in_frames_hack to prevent checkpoint data loss (Jussi Saurio) * fix/core/translate: ALTER TABLE DROP COLUMN: ensure schema cookie is updated even when target table is empty (Jussi Saurio) * io_uring: setup plumbing for Fixed opcodes (Preston Thorpe) * JavaScript serverless driver fixes (Pekka Enberg) * Fix vector deserialization alignment and blob/text empty mismatch (bit-aloo) * fix/wal: reset ongoing checkpoint state when checkpoint fails (Jussi Saurio) * Fix parser error for repetition in row values (Diego Reis) * fix/wal: only rollback WAL if txn was write + fix start state for WalFile (Jussi Saurio) * fix/bindings/rust: return errors instead of swallowing them and returning None (Jussi Saurio) * fix/wal: make db_changed check detect cases where max frame happens to be the same (Jussi Saurio) * fix/wal: reset page cache when another connection checkpointed in between (Jussi Saurio) * Fix merge script to prevent incorrectly marking merged contributor PRs as closed (Preston Thorpe) * Fix concat_ws to match sqlite behavior (bit-aloo) * bindings/rust: return errors instead of vibecoded numbers (Jussi Saurio) ## 0.1.3 -- 2025-07-29 ### Added * bindings/rust: Add WAL API support (Nikita Sivukhin) * Implement the Returning statement for inserts and updates (Glauber Costa) * implement the pragma encoding (Glauber Costa) * support doubly qualified identifiers (Glauber Costa) * mark detach as supported (Glauber Costa) * Support ATTACH (read only) (Glauber Costa) * serverless: Add DatabasError type (Pekka Enberg) * btree/balance: support case where immediate parent page of unbalanced child page also overflows (Jussi Saurio) * serverless: Add Statement.run() method (Pekka Enberg) * make add dirty to change flag and also add page to the dirty list (Nikita Sivukhin) * types: less noisy Debug implementation for ImmutableRecord (Jussi Saurio) * Safe `AtomicUsize` wrapper for `db_state`: add `DbState` and `AtomicDbState` (Levy A.) * Add Github workflow for Turso serverless package (Pekka Enberg) * Add Rickrolling Turso blog post to contrib (Avinash Sajjanshetty) * Add `@tursodatabase/serverless` package (Pekka Enberg) * Implement pragma database_list (Glauber Costa) * fix/test: fix and unignore incorrectly implemented test (Jussi Saurio) * implement Debug for Database (Glauber Costa) * implement pragma application_id (Glauber Costa) * implement write side of pragma schema_version (Glauber Costa) * Simplify blocking operations – add `io.block(fn)` for IO trait implementors (Levy A.) * bindings/rust: Initial support for transactions API (Diego Reis) * claude sonnet forgot to run clippy when implementing mcp server (Jussi Saurio) * bindings/js: support iterator, and more kinds of params (Mikaël Francoeur) * Add a native MCP server (Glauber Costa) * sim: add order by to some queries (Jussi Saurio) * Core: Introduce external sorting (Iaroslav Zeigerman) * forgot to set the state to NewTrunk if we have more leaf pages than free entries (Pedro Muniz) * Implement IO latency correctly in simulator (Pedro Muniz) * page cache: temporarily increase default size until WAL spill is implemented (Jussi Saurio) * compat: add integrity_check (Pere Diaz Bou) * sim: provide additional context in assertion failures (Jussi Saurio) * btree: add some assertions related to #2106 (Jussi Saurio) * bench: add insert benchmark (batch sizes: 1,10,100) (Jussi Saurio) * Support page_size pragma setting (meteorgan) ### Updated * bindings/javascript: Generate native npm packages at publish (Pekka Enberg) * Replace custom wasm bindings with napi-rs (Diego Reis) * core: Enforce shared database object per database file (Pekka Enberg) * btree/pager: Improve update performance by reusing freelist pages in allocate_page() (Jussi Saurio) * VDBE/op_column: use references to cursor payload instead of cloning (Jussi Saurio) * io/unix: wrap file with Mutex (Pere Diaz Bou) * core: Clone everything in schema (Pere Diaz Bou) * core/translate: Handle Expr::Id in `CREATE INDEX` (Kristofer) * Thread-safe `WindowsFile` (Levy A.) * io_uring: use Arc pointer for user data of entries (Preston Thorpe) * Stop checkpointing the entire WAL after every write when wal frame size > threshold (Preston Thorpe) * compat police (Glauber Costa) * remove upsert statement from COMPAT.md (Glauber Costa) * Update limbo -> turso in manual.md (stano) * bindings/javascript: Switch to napi v3 (Diego Reis) * Simplify sum() aggregation logic (bit-aloo) * Ignore WAL frames after bad checksum (Pere Diaz Bou) * parser: Distinguish quoted identifiers and unify Id into Name enum (bit-aloo) * btree: clear overflow pages when insert overwrites a cell (= UPDATE) (Jussi Saurio) * Append WAL frames one by one (Pere Diaz Bou) * pager: Clear stale page cache if database changed (Jussi Saurio) * sim/aws: ignore child process exits with code 137 (Jussi Saurio) * WAL insert API: force schema re-parse if necessary after WAL sync session end (Nikita Sivukhin) * WAL insert: mark pages as dirty (Nikita Sivukhin) * emit SetCookie after DropTable (Glauber Costa) * Bail early for read-only virtual tables (Preston Thorpe) * measure only the time it takes to open the actual connection (Glauber Costa) * Explicit rowid insert (Nikita Sivukhin) * Deserialize keys only once when sorting immutable records (Iaroslav Zeigerman) * WAL insert API (Nikita Sivukhin) * Pager: clear overflow cells when freeing page (Jussi Saurio) * make readonly a property of the database (Glauber Costa) * use default hasher for the sake of determinism (Nikita Sivukhin) * Load static extensions once and store on Database instead of once per connection (Preston Thorpe) * wal: write txn fail in case max_frame change midway (Pere Diaz Bou) * Improve Antithesis tests (Pekka Enberg) * core/lib: init_pager lock shared wal until filled (Pere Diaz Bou) * test/stress&sim: enable indexes by default (Jussi Saurio) * Usable space unwrap (Pedro Muniz) * BTreeTable::to_sql: wrap special column names in brackets (Nils Koch) * Avoid redundant decoding of record headers when reading sorted chunk files (Iaroslav Zeigerman) * compat: change page_size pragma and RowData opcode to yes (meteorgan) * Explain the Turso challenge (Glauber Costa) * gh workflow for dart (test, precompile, publish), only test is activated (Andika Tanuwijaya) * bindings/rust: Return number of rows changed from Connection::execute() (Rohith Suresh) * bindings/java: Make TursoDB and TursoDB factory thread-safe (Mikaël Francoeur) * use `wasm32-wasip1` target instead of `wasm32-wasi` (Nils Koch) * improve handling of double quotes (Glauber Costa) * Remove `find_cell`, allow overwrite of index interior cell (Jussi Saurio) * sim: change --disable-create-index flag to --enable-create-index (default false) (Jussi Saurio) * Reanimate MVCC (Pekka Enberg) * Replace verbose IO Completion methods with helpers (Preston Thorpe) * Use pread and pwrite in run_once (Ihor Andrianov) * `make_from_btree` should wait for IO to complete (Pedro Muniz) * chore: update rust to version 1.88.0 (Nils Koch) * refactor/btree&vdbe: fold index key info (sort order, collations) into a single struct (Jussi Saurio) * core: Copy-on-write for in-memory schema (Levy A.) * simulator: Disable `INSERT INTO .. SELECT` for being slow (Pekka Enberg) * Async IO: registration of file descriptors (Preston Thorpe) * Clean up AST unparsing, remove `ToSqlString` (Levy A.) * rename operation_xxx to change_xxx to make naming more consistent (Nikita Sivukhin) * Separate user-callable cacheflush from internal cacheflush logic (Diego Reis) * make unixepoch to return i64 (Nikita Sivukhin) * sim: ignore fsync faults (Jussi Saurio) * Updates to the simulator (Alperen Keleş) * small refactor: rename "amount" to "extra_amount" (Nikita Sivukhin) * refactor: Changes CursorResult to IOResult (Diego Reis) * btree: unify table&index seek page boundary handling (Jussi Saurio) * Treat table-valued functions as tables (Piotr Rżysko) ### Fixed * perf: fix logic error in is_simple_count() (Jussi Saurio) * Fix writing wal header for async IO (Preston Thorpe) * bindings/javascript: Fix tracing and `SqliteError` message not populated (Levy A.) * Fix sum() to follow the SQLite semantics (FamHaggs) * Fix error handling when binding column references while translating the UPDATE statement (Iaroslav Zeigerman) * Fix schema reparse logic (Nikita Sivukhin) * Fix page_count pragma (meteorgan) * sqlite3: Improve SQLite error handling and fix C-string safety (Ceferino Patino) * fix: SUM returns correct float for mixed numeric/non-numeric types & return value on empty set (Axel Tobieson Rova) * silence clippy errors with features disabled (Glauber Costa) * fix raw read frame WAL API (Nikita Sivukhin) * Work around CREATE TABLE whitespace issue (Jussi Saurio) * Fix issues in alter table tests and fix a small error in BTreeTable::to_sql (Jussi Saurio) * Fix duplicate SET statement compatibility with SQLite (Ihor Andrianov) * test/clickbench: fix clickbench (Jussi Saurio) * fix/sim: actually enable indexes by default (Jussi Saurio) * fix/btree/balance: interior cell insertion can leave page overfull (Jussi Saurio) * Fix `error: Stable 1.88.0 is not available` in Nix flake (Levy A.) * Fix column order for multi-row insertion (Nikita Sivukhin) * doc: fix intra-repo links for contribution guide and license, and expand binding links (bit-aloo) * fix opcodes missing a database register (Glauber Costa) * sorter: fix sorter panic on SortedChunkIOState::WaitingForRead (Jussi Saurio) * Fix parent page stack location after interior node replacement (Jussi Saurio) * Fix rollback for TxErrors (Diego Reis) * make most instrumentation levels to be Debug or Trace instead (Pedro Muniz) * Property `FaultyQuery` should fail if we encounter an error that is not expected (Pedro Muniz) * translate/create index: fix wrong collations (Jussi Saurio) * bind/java: Fix Linux x86 build release (Diego Reis) * fix/btree: fix insert_into_cell() logic (Jussi Saurio) * cli: fix not being able to redirect traces to a file from inline query (Jussi Saurio) * bind/javascript: Fix presentation mode disabling logic (Diego Reis) * btree: fix post-balancing seek bug in delete path (Jussi Saurio) * btree: fix trying to go upwards when we are already at the end of the entire btree (Jussi Saurio) * btree: fix interior cell replacement in btrees with depth >=3 (Jussi Saurio) * extensions/vtab: fix i32 being passed as i64 across FFI boundary (Jussi Saurio) * Fix CSV import in the shell (Jussi Saurio) * fix record header size calculations and incorrect assumptions (Jussi Saurio) * bindings/js: fix more tests (Mikaël Francoeur) ## 0.1.2 -- 2025-07-15 ### Added * Add `fuzz` to CI checks (Levy A.) * Add async header accessor functionality (Zaid Humayun) * core/vector: Euclidean distance support for vector search (KarinaMilet) * Fix: OP_NewRowId to generate semi random rowid when largest rowid is `i64::MAX` (Krishna Vishal) * vdbe: fix some issues with min() and max() and add ignored fuzz test (Jussi Saurio) * github: Update to newest Nyrkiö Github action (Henrik Ingo) * Add Nyrkiö to partners section in README (Henrik Ingo) * Support except operator for Compound select (meteorgan) * btree: fix incorrect comparison implementation in key_exists_in_index() (Jussi Saurio) * bindings/java: Implement required methods to run on JetBrains Datagrip (Kim Seon Woo) * add interactive transaction to property insert-values-select (Pere Diaz Bou) * add pere to antithesis (Pere Diaz Bou) * cli: Add support for `.headers` command (Pekka Enberg) * stress: add a way to run stress with indexes enabled (Jussi Saurio) * sim: add feature flags (indexes,mvcc) to CLI args (Jussi Saurio) * Add multi select test in JDBC4StatementTest (Kim Seon Woo) * bindings/dart initial implementation (Andika Tanuwijaya) * bindings/javascript: Implement Database.open (Lucas Forato) * add `libsql_disable_wal_checkpoint` (Pedro Muniz) * Add a threshold for large page cache values (Krishna Vishal) * Rollback schema support (Pere Diaz Bou) * add a README for the rust bindings (Glauber Costa) * add a basic readme for the typescript binding (Glauber Costa) * add a benchmark for connection time versus number of tables (Glauber Costa) * Add opening new connection from a sqlite compatible URI, read-only connections (Preston Thorpe) * Simplify `PseudoCursor` implementation (Levy A.) * bind/js: add tests for expand (Mikaël Francoeur) ### Updated * Gopher is biologically closer to beavers than hamsters (David Shekunts) * build: Update cargo-dist to 0.28.6 (Pekka Enberg) * cli: Fail import command if table does not exists (Pekka Enberg) * Assert I/O read and write sizes (Pere Diaz Bou) * do not check rowid alias for null (Nikita Sivukhin) * CDC functions (Nikita Sivukhin) * Ignore double quotes around table names (Zaid Humayun) * Efficient Record Comparison and Incremental Record Parsing (Krishna Vishal) * `parse_schema_rows` optimizations (Levy A.) * Simulator - only output color on terminal (Mikaël Francoeur) * Btree: more balance docs (Jussi Saurio) * btree: Improve balance non root docs (Jussi Saurio) * properly set last_checksum after recovering wal (Pere Diaz Bou) * btree/chore: remove unnecessary parameters to .cell_get() (Jussi Saurio) * core/btree: Make cell field names consistent (Jussi Saurio) * Enforce TCL 8.6+ in compatibility tests (Mikaël Francoeur) * Minor refactoring of btree (meteorgan) * bindings/python: Start transaction implicitly in execute() (Pekka Enberg) * sqlite3_ondisk: generalize left-child-pointer reading function to both index/table btrees (Jussi Saurio) * sim: post summary to slack (Jussi Saurio) * stress clippy (Jussi Saurio) * Synchronize WAL checkpointing (Pere Diaz Bou) * Reachable assertions in Antithesis Python Test for better logging (Pedro Muniz") * bindings/python: close connection only when reference count is one (Pere Diaz Bou) * parser: use YYSTACKDEPTH (Lâm Hoàng Phúc) * CI: remove duplicate fuzz run (Jussi Saurio) * Use binary search in find_cell() (Ihor Andrianov) * Use `str_to_f64` on float conversion (Levy A.) * parser: replace KEYWORDS with matching (Lâm Hoàng Phúc) * Reachable assertions in Antithesis Python Test for better logging (Pedro Muniz) * treat ImmutableRecord as Value::Blob (Nikita Sivukhin) * remove experimental_flag from script + remove -q flag default flag from `TestTursoShell` (Pedro Muniz) * Change data capture (Nikita Sivukhin) * Import subset of SQLite TCL tests (Pekka Enberg) * bindings/java: Merge JavaScript test suites (Mikaël Francoeur) * Import JavaScript bindings test suite from libSQL (Mikaël Francoeur) * bindings/java: Rename to Turso (Diego Reis) * Antithesis schema rollback tests (Pekka Enberg) * Disable adaptive colors when output_mode is list (meteorgan) * core/storage: Switch to turso_assert in btree.rs (Pekka Enberg) * core: Disable `ROLLBACK` statement (Pekka Enberg") * Rust binding improvements (Pedro Muniz") * `from_uri` was not passing mvcc and indexes flag to database creation for memory path (Pedro Muniz) * Turso, not Limbo, in pyproject.toml (Simon Willison) * Rename Limbo -> Turso in python tests (Preston Thorpe) * clarify discord situation (Glauber Costa) * automatically select terminal colors for pretty mode (Glauber Costa) * remote query_mode from ProgramBuilderOpts and from function arguments (Nikita Sivukhin) * limbo -> turso (Glauber Costa) * Rust binding improvements (Pedro Muniz) ### Fixed * test/fuzz: fix rowid_seek_fuzz not being a proper fuzz test (Jussi Saurio) * b-tree: fix bug in case when no matching rows was found in seek in the leaf page (Nikita Sivukhin) * Fix clippy errors for Rust 1.88.0 (Nils Koch) * sim: return LimboError::Busy when busy, instead of looping forever (Jussi Saurio) * btree/balance/validation: fix divider cell insert validation (Jussi Saurio) * btree/balance/validation: fix use-after-free in rightmost ptr validation (Jussi Saurio) * core/translate: Fix "misuse of aggregate function" error message (Pekka Enberg) * core/translate: Return error if SELECT needs tables and there are none (Mikaël Francoeur) * antithesis: Fix transaction management (Pekka Enberg) * core: Fix resolve_function() error messages (Pekka Enberg) * VDBE: fix op_insert re-entrancy (Jussi Saurio) * VDBE: fix op_idx_insert re-entrancy (Jussi Saurio) * bindings/javascript: Improve error handling compatibility with `better-sqlite3` (Mikaël Francoeur) * uv run ruff format && uv run ruff check --fix (Jussi Saurio) * vdbe: fix compilation (Pere Diaz Bou) * core/translate: Fix aggregate star error handling in prepare_one_sele… (Pekka Enberg) * Fix infinite loops, rollback problems, and other bugs found by I/O fault injection (Pedro Muniz) * core/translate: Unify no such table error messages (Pekka Enberg) * Fix `ScalarFunc::Glob` to handle NULL and other value types (Krishna Vishal) * fix: buffer pool is not thread safe problem (KaguraMilet) * Fix index update when INTEGER PRIMARY KEY (rowid alias) (Adrian-Ryan Acala) * Fix Python test import naming (Pedro Muniz) * Fix boxed memory leaks (Ihor Andrianov) * bindings/javascript: Formatting and typos (Mikaël Francoeur) ## 0.1.1 -- 2025-06-30 ### Fixed * JavaScript packaging (Pekka Enberg) ### Updated * simulator: FsyncNoWait + Faulty Query (Pedro Muniz) ## 0.1.0 -- 2025-06-30 ### Added * bindings/rust: Add feature flag to enable indexes (Pekka Enberg) * core: Add Antithesis-aware `turso_assert` (Pekka Enberg) * Fix database header contents on initialization (Pere Diaz Bou) * Support insersect operator for compound select (meteorgan) * Simulator: add latency to File IO (Pedro Muniz) * write page1 on database initialization (Pere Diaz Bou) * `Rollback` simple support (Pere Diaz Bou) * core/db&pager: fix locking for initializing empty database (Jussi Saurio) * sim: add Fault::ReopenDatabase (Jussi Saurio) * Fix database header initialization (Diego Reis) * Add Pedro to email recipients for antithesis (Pedro Muniz) * bindings/rust: Implement Debug for Connection (Charlie) * Fix: add uv sync to all packages for pytest github action (Pedro Muniz) * Implement RowData opcode (meteorgan) * Support indent for Goto opcode when executing explain (meteorgan) ### Updated * core: Disable `ROLLBACK` statement (Pekka Enberg) * WAL record db_size frame on commit last frame (Pere Diaz Bou) * Eliminate core extension dependencies (Pekka Enberg) * Move completion extension dependency to CLI (Pekka Enberg) * Rename `limbo` crate to `turso` (Pekka Enberg) * Rename `limbo_sqlite3_parser` crate to `turso_sqlite3_parser` (Pekka Enberg) * Rename `limbo_ext` crate to `turso_ext` (Pekka Enberg) * Rename `limbo_macros` to `turso_macros` (Pekka Enberg) * stress: Log reopen and reconnect (Pekka Enberg) * Rename `limbo_core` crate to `turso_core` (Pekka Enberg) * github: Run simulator on pull requests (Pekka Enberg) * bindings/rust: Named params (Andika Tanuwijaya) * Rename Limbo to Turso in the README and other files (Glauber Costa) * Remove dependency on test extension pkg (Preston Thorpe) * Cache `reserved_space` and `page_size` values at Pager init to prevent doing redundant IO (Krishna Vishal) * cli: Rename CLI to Turso (Pekka Enberg) * bindings/javascript: Rename package to `@tursodatabase/turso` (Pekka Enberg) * bindings/python: Rename package to `pyturso` (Pekka Enberg) * Rename Limbo to Turso Database (Pekka Enberg) * Bring back TPC-H benchmarks (Pekka Enberg) * Switch to runtime flag for enabling indexes (Pekka Enberg) * stress: reopen db / reconnect to db every now and then (Jussi Saurio) * Bring back some merge conflicts code (Pedro Muniz) * simulator: integrity check per query (Pedro Muniz) * stress: Improve progress reporting (Pekka Enberg) * Improve extension compatibility testing (Piotr Rżysko) * Ephemeral Table in Update (Pedro Muniz) * Use UV more in python related scripts and actions (Pedro Muniz) * Copy instrumented image and symbols in Dockerfile.antithesis (eric-dinh-antithesis) * ` op_transaction` `end_read_tx` in case of `begin_write_tx` is busy (Pere Diaz Bou) * antithesis-tests: Make test drivers robust when database is locked (Pekka Enberg) ### Fixed * tests/integration: Fix write path test on Windows (Pekka Enberg) * Fix deleting previous rowid when rowid is in the Set Clause (Pedro Muniz) * Fix executing multiple statements (Pere Diaz Bou) * Fix evaluation of ISNULL/NOTNULL in OR expressions (Piotr Rżysko) * bindings/javascript: Fix StepResult:IO handling (Pekka Enberg) * fix: use uv run instead of uvx for Pytest (Pedro Muniz) * sim: when loading bug, dont panic if there are no runs (Jussi Saurio) * sim: fix singlequote escaping and unescaping (Jussi Saurio) * Fix btree balance and seek after overwritten cell overflows (Jussi Saurio) * chore: fix clippy warnings (Nils Koch) * Fix CI errors (Piotr Rżysko) * Fix infinite aggregation loop when sorting is not required (Piotr Rżysko) * Fix DELETE not emitting constant `WhereTerms` (Pedro Muniz) * Fix handling of non-aggregate expressions (Piotr Rżysko) * Fix fuzz issue #1763 by using the `log2` & `log10` functions where applicable (Luca Muscat) ## 0.0.22 -- 2025-06-19 ### Added * Implement pragma wal_checkpoint() (Pedro Muniz) * Add abbreviated alias for `.quit` and `.exit` (Krishna Vishal) * Add manual WAL sync before checkpoint in con.close, fix async bug in checkpoint (Preston Thorpe) * Complete ALTER TABLE implementation (Levy A.) * Add affinity-based type coercion for seek and comparison operation (Krishna Vishal) * bindings/javascript: Add pragma() support (Anton Harniakou) * Add sleep between write tests to avoid database locking issues (Pedro Muniz) * bindings/java: Implement JDBC4DatabaseMetadata getTables (Kim Seon Woo) * bindings/javascript: Add source property to Statement (Anton Harniakou) * Support `sqlite_master` schema table name alias (Anton Harniakou) * js-bindings/implement .name property (Anton Harniakou) * bindings/java: Add support for Linux build (Diego Reis) * bindings/javascript: Add database property to Statement (Anton Harniakou) * simulator: add CREATE INDEX to interactions (Jussi Saurio) * Add support for pragma table-valued functions (Piotr Rżysko) * bindings/javascript: Add proper exec() method and raw() mode (Diego Reis) * Add simulator-docker-runner for running limbo-sim in a loop on AWS (Jussi Saurio) * simulator: add option to disable BugBase (Jussi Saurio) * simulator: switch to tracing, run io.run_once and add update queries (Pere Diaz Bou) * Fix: aggregate regs must be initialized as NULL at the start (Jussi Saurio) * add stress test with 1 thread 10k iterations to ci (Pere Diaz Bou) ### Updated * antithesis: Build Python package from sources (Pekka Enberg) * overwrite sqlite3 in install_sqlite (Pere Diaz Bou) * stress: Run integrity check for every iteration (Pekka Enberg) * core: Clean up `integrity_check()` (Pekka Enberg) * Simple integrity check on btree (Pere Diaz Bou) * Make SQLite bindings thread-safe (Pekka Enberg) * Switch Connection to use Arc instead of Rc (Pekka Enberg) * Drop unused code in op_column (meteorgan) * NOT NULL constraint (Anton Harniakou) * Simulator Ast Generation + Simulator Unary Operator + Refactor to use `limbo_core::Value` in Simulator for massive code reuse (Pedro Muniz) * Refactor compound select (meteorgan) * Disable index usage in DELETE because it does not work safely (Jussi Saurio) * Simulator: Better Shrinking (Pedro Muniz) * Simulator integrity_check (Pedro Muniz) * Remove leftover info trace (Jussi Saurio) * Namespace functions that operate on `Value` (Pedro Muniz) * Remove plan.to_sql_string() from optimize_plan() as it panics on TODOs (Jussi Saurio) * Minor: use use_eq_ignore_ascii_case in some places (Anton Harniakou) * Remove the FromValue trait (Anton Harniakou) * bindings/javascript: Refactor presentation mode and enhance test suite (Diego Reis) * Beginnings of AUTOVACUUM (Zaid Humayun) * Reverse Parse Limbo `ast` and Plans (Pedro Muniz) * simulator: log the interaction about to be executed with INFO (Jussi Saurio) * stress: Use temporary file unless one explicitly specified (Jussi Saurio) * Write database header via normal pager route (meteorgan) * simulator: options to disable certain query types (Pedro Muniz) * Make cursor seek reentrant (Pedro Muniz) * Pass input string to `translate` function (Pedro Muniz) * Small tracing enhancement (Pedro Muniz) * Adjust write cursors for delete to avoid opening more than once. (Pedro Muniz) * Convert u64 rowid to i64 (Pere Diaz Bou) * Use tempfile in constraint test (Jussi Saurio) * Remove frame id from key (Pere Diaz Bou) * clear page cache on transaction failure (Pere Diaz Bou) ### Fixed * Fix incorrect lossy conversion of `Value::Blob` to a utf-8 `String` (Luca Muscat) * Fix update queries to set `n_changes` (Kim Seon Woo) * bindings/rust: Fix Rows::next() I/O dispatcher handling (Pekka Enberg) * cli: fix panic of queries with less than 7 chars (Nils Koch) * Return parse error instead of corrupt error for `no such table` (Pedro Muniz) * simulator: disable all ansi encodings for debug print log file (Pedro Muniz) * Fix large inserts to unique indexes hanging (Jussi Saurio) * betters instrumentation for btree related operations + cleaner debug for `RefValue` (Pedro Muniz) * sim/aws: fix vibecoding errors in logic (Jussi Saurio) * Fix incorrect handling of OR clauses in HAVING (Jussi Saurio) * fix: Incorrect placeholder label in where clause translation (Pedro Muniz) * Fix rowid to_sql_string (Pedro Muniz) * Fix incorrect usage of indexes with non-contiguous columns (Jussi Saurio) * BTree traversal refactor and bugfixes (Pere Diaz Bou) * `LimboRwLock` write and read lock fixes (Pere Diaz Bou) * fix: make keyword_token safe by validating UTF-8 input (ankit) * Fix UPDATE straight up not working on non-unique indexes (Jussi Saurio) * Fix: ensure `PRAGMA cache_size` changes persist only for current session (meteorgan) * sim/aws: fix sim timeout handling (Jussi Saurio) * Fix WAL frame checksum mismatch (Diego Reis) * Set maximum open simulator-created issues (Jussi Saurio) * Fix cursors not being opened for indexes in DELETE (Jussi Saurio) * Fix: allow DeferredSeek on more than one cursor per program (Jussi Saurio) * Fix stress test to ignore unique constraint violation (krishna sindhur) * Fix ProgramBuilder::cursor_ref not having unique keys (Jussi Saurio) * Fix `serialize()` unreachable panic (Krishna Vishal) * Btree: fix cursor record state not being updated in insert_into_page() (Jussi Saurio) ## 0.0.21 - 2025-05-28 ### Added * Add Schema reference to Resolver - needed for adhoc subquery planning (Jussi Saurio) * Use the SetCookie opcode to implement user_version pragma (meteorgan) * Add libsql_wal_get_frame() API (Pekka Enberg) * Fix bug: op_vopen should replace cursor slot, not add new one (Jussi Saurio) * bind/js: Add support for bind() method and reduce boilerplate (Diego Reis) * Add PThorpe92 to codeowners file for extensions + go bindings (Preston Thorpe) * Refactor: add stable internal_id property to TableReference (Jussi Saurio) * refactor: introduce walk_expr() and walk_expr_mut() to reduce repetitive pattern matching (Jussi Saurio) * Add some comments for values statement (meteorgan) * fix bindings/wasm wal file creation by implementing `generate_random_number` (오웬) * core/pragma: Add support for update user_version (Diego Reis) * Support values statement and values in select (meteorgan) * Initial Support for Nested Translation (Pedro Muniz) * bindings/rust: Add pragma methods (Diego Reis) * Add collation column to Index struct (Jussi Saurio) * Add support for DISTINCT aggregate functions (Jussi Saurio) * bindings/javascript: Add Statement.iterate() method (Diego Reis) * (btree): Implement support for handling offset-based payload access with overflow support (Krishna Vishal) * Add labeler workflow and reorganize macros (Preston Thorpe) * Update Nyrkiö change detection to newest version (Henrik Ingo) * perf/ci: add basic tpc-h benchmark (Jussi Saurio) * Add `libsql_wal_frame_count()` API (Pekka Enberg) * Restructure optimizer to support join reordering (Jussi Saurio) * Add `rustfmt` to rust-toolchain.toml (Pekka Enberg) ### Updated * Make WhereTerm::consumed a Cell (Jussi Saurio) * Use lifetimes in walk_expr() to guarantee that child expr has same lifetime as parent expr (Jussi Saurio) * Small VDBE insn tweaks (Jussi Saurio) * Reset idx delete state after successful finish (Pere Diaz Bou) * feature: `INSERT INTO SELECT` (Pedro Muniz) * Small cleanups to pager/wal/vdbe - mostly naming (Jussi Saurio) * bindings/javascript: API enhancements (Diego Reis) * github: Migrate workflows to Blacksmith runners (blacksmith-sh[bot]) * UNION (Jussi Saurio) * xConnect for virtual tables to query core db connection (Preston Thorpe) * Reconstruct WAL frame cache when WAL is opened (Jussi Saurio) * set non-shared cache by default (Pere Diaz Bou) * TPC-H with criterion and nyrkio (Pedro Muniz) * UNION ALL (Jussi Saurio) * Drop Table OpCodes Use Ephemeral Table As Scratch Table (Zaid Humayun) * sqlite3-parser: Remove scanner trace-logging (Pekka Enberg) * sqlite3: Switch to tracing logger (Pekka Enberg) * CSV virtual table extension (Piotr Rżysko) * remove detection of comments in the middle of query in cli (Pedro Muniz) * btree: Remove assumption that all btrees have a rowid (Jussi Saurio) * Output rust backtrace in python tests (Preston Thorpe) * Optimization: lift common subexpressions from OR terms (Jussi Saurio) * refactor: replace Operation::Subquery with Table::FromClauseSubquery (Jussi Saurio) * Feature: Collate (Pedro Muniz) * Update README.md (Yusheng Guo) * Mark WHERE terms as consumed instead of deleting them (Jussi Saurio) * Cli config 2 (Pedro Muniz) * pager: bump default page cache size from 10 to 2000 pages (Jussi Saurio) * long fuzz tests ci on btree changes (Pere Diaz Bou) * Document how to run `cargo test` on Ubuntu (Zaid Humayun) * test page_free_array (Pere Diaz Bou) * Rename OwnedValue -> Value (Pekka Enberg) * Improve SQLite3 C API tests (Pekka Enberg) * github: Disable setup-node yarn cache (Pekka Enberg) * Update Unique constraint for Primary Keys and Indexes (Pedro Muniz) ### Fixed * Fix LIMIT handling (Jussi Saurio) * Fix off-by-one error in max_frame after WAL load (Jussi Saurio) * btree: fix infinite looping in backwards iteration of btree table (Jussi Saurio) * Fix labeler labeling everything as Extensions-Other (Jussi Saurio) * Fix bug in op_decr_jump_zero() (Jussi Saurio) * Page cache fixes (Pere Diaz Bou) * cli/fix: Apply default config for app (Diego Reis) * Fix labeler (Jussi Saurio) * Improve debug build validation speed (Pere Diaz Bou) * optimizer: fix order by removal logic (Jussi Saurio) * Fix updating single value (Pedro Muniz) * Autoindex fix (Pedro Muniz) * use temporary db in sqlite3 wal tests to fix later tests failing (Preston Thorpe) * fix labeler correct file name extension use .yml instead of .yaml (Mohamed A. Salah) * Fix autoindex of primary key marked as unique (Pere Diaz Bou) * Fix: unique contraint in auto index creation (Pedro Muniz) ## 0.0.20 - 2025-05-14 ### Added * Support isnull and notnull expr (meteorgan) * Add drop index (Anton Harniakou) * bindings/wasm: add types property for typescript setting (오병진) * Implement transaction support in Go adapter (Jonathan Ness) * Initial implementation of `ALTER TABLE RENAME` (Levy A.) * Add time.Time and bool data types support in Go adapter (Jonathan Ness) * Add tests for INSERT with specified column-name list (Anton Harniakou) * GROUP BY: refactor logic to support cases where no sorting is needed (Jussi Saurio) * Add embedded library support to Go adapter (Jonathan Ness) * Add time.Time support to Go driver parameter binding (Jonathan Ness) * Show explanation for the NewRowid opcode (Anton Harniakou) * Add notion of join ordering to plan (Jussi Saurio) * Add static feature to Cargo.toml to support extensions written inside core (Pedro Muniz) * implement Clone for Arc types (Pete Hayman) * Add PRAGMA schema_version (Anton Harniakou) * Support literal-value current_time, current_date and current_timestamp (meteorgan) * Add state machine for op_idx_delete + DeleteState simplification (Pere Diaz Bou) * Add the .indexes command (Anton Harniakou) * Optimization: only initialize `Rustyline` if we are in a tty (Pedro Muniz) * Add Antithesis Tests (eric-dinh-antithesis) * core/types: remove duplicate serialtype implementation (Jussi Saurio) * bindings/rust: Add Statement.columns() support (Timo Kösters) * docs: add Rust to "Getting Started" section (Timo Kösters) * Support xBestIndex in vtab API (Preston Thorpe) * Feat: add support for descending indexes (Jussi Saurio) ### Updated * github: Ensure rustmft is installed (Pekka Enberg) * btree: Coalesce free blocks in `page_free_array()` (Mohamed Hossam) * Count optimization (Pedro Muniz) * bindings/java: Remove disabled annotation for UPDATE and DELETE (Kim Seon Woo) * Refactor numeric literal (meteorgan) * EXPLAIN should show a comment for the Insert opcode (Anton Harniakou) * bindings/javascript: Improve compatibility with better-sqlite (Diego Reis) * bindings/go: Upgrade ebitengine/purego to allow for use with go 1.23.9 (Preston Thorpe) * Adjust vtab schema creation to display the underlying columns (Preston Thorpe) * Read only mode (Pedro Muniz) * Test that DROP TABLE also deletes the related indices (Anton Harniakou) * reset statement before executing in rust binding (Pedro Muniz) * Bump assorted dependencies (Preston Thorpe) * Eliminate a superfluous read transaction when doing PRAGMA user_version (Anton Harniakou) * update index on updated indexed columns (Pere Diaz Bou) * Save history on exit (Piotr Rżysko) * btree/tablebtree_move_to: micro-optimizations (Jussi Saurio) * refactor database open_file and open (meteorgan) * Give name to hard-coded page_size values (Anton Harniakou) * Performance: hoist entire expressions out of hot loops if they are constant (Jussi Saurio) * Feature: Composite Primary key constraint (Pedro Muniz) * types: refactor serialtype again to make it faster (Jussi Saurio) * replace vec with array in btree balancing (Lâm Hoàng Phúc) * Pragma page size reading (Anton Harniakou) * perf/btree: use binary search for Index seek operations (Jussi Saurio) * expr.is_nonnull(): return true if col.primary_key || col.notnull (Jussi Saurio) * Numeric Types Overhaul (Levy A.) * Python script to compare vfs performance (Preston Thorpe) * Create an automatic ephemeral index when a nested table scan would otherwise be selected (Jussi Saurio) * Bump julian_day_converter to 0.4.5 (meteorgan) * btree: avoid reading entire cell when only rowid needed (Jussi Saurio) * btree: use binary search in seek/move_to for table btrees (Jussi Saurio) * Feat: Covering indexes (Jussi Saurio) * allow index entry delete (Pere Diaz Bou) ### Fixed * testing/py: rename debug_print() to run_debug() (Jussi Saurio) * Fix handling of empty strings in prepared statements (Diego Reis) * CREATE VIRTUAL TABLE fixes (Piotr Rżysko) * Bindings/Go: Fix symbols for FFI calls (Preston Thorpe) * Fix bound parameters on insert statements with out of order column indexes (Preston Thorpe) * Fix memory leak caused by unclosed virtual table cursors (Piotr Rżysko) * Fix panic on async io due to reading locked page (Preston Thorpe) * Fix bug: we cant remove order by terms from the head of the list (Jussi Saurio) * Fix setting default value for primary key on UPDATE (Pere Diaz Bou) * Fix: allow page_size=65536 (meteorgan) * Fix `page_count` pragma (meteorgan) * Fix broken fuzz target due to old name (Levy A.) * Emit `IdxDelete` instruction and some fixes on seek after deletion (Pere Diaz Bou) * Bugfix: Explain command should display syntax errors in CLI (Anton Harniakou) * Fix incorrect between expression documentation (Pedro Muniz) * Fix bug: left join null flag not being cleared (Jussi Saurio) * Fix out of bounds access on `parse_numeric_str` (Levy A.) * Fix post balance validation (Pere Diaz Bou) ## 0.0.19 - 2025-04-16 ### Added * Add `BeginSubrtn`, `NotFound` and `Affinity` bytecodes (Diego Reis) * Add Ansi Colors to tcl test runner (Pedro Muniz) * support modifiers for julianday() (meteorgan) * Implement Once and OpenAutoindex opcodes (Jussi Saurio) * Add support for OpenEphemeral bytecode (Diego Reis) * simulator: Add Bug Database(BugBase) (Alperen Keleş) * feat: Add timediff data and time function (Sachin Kumar Singh) * core/btree: Add PageContent::new() helper (Pekka Enberg) * Add support to load log file with stress test (Pere Diaz Bou) * Support UPDATE for virtual tables (Preston Thorpe) * Add `.timer` command to print SQL execution statistics (Pere Diaz Bou) * Strict table support (Ihor Andrianov) * Support backwards index scan and seeks + utilize indexes in removing ORDER BY (Jussi Saurio) * Add deterministic Clock (Avinash Sajjanshetty) * Support offset clause in Update queries (Preston Thorpe) * Support Create Index (Preston Thorpe) * Support insert default values syntax (Preston Thorpe) * Add support for default values in INSERT statements (Diego Reis) ### Updated * Test: write tests for file backed db (Pedro Muniz) * btree: move some blocks of code to more reasonable places (Jussi Saurio) * Parse hex integers 2 (Anton Harniakou) * More index utils (Jussi Saurio) * Index utils (Jussi Saurio) * Feature: VDestroy for Dropping Virtual Tables (Pedro Muniz) * Feat balance shallower (Lâm Hoàng Phúc) * Parse hexidecimal integers (Anton Harniakou) * Code clean-ups (Diego Reis) * Return null when parameter is unbound (Levy A.) * Enhance robusteness of optimization for Binary expressions (Diego Reis) * Check that index seek key members are not null (Jussi Saurio) * Better diagnostics (Pedro Muniz) * simulator: provide high level commands on top of a single runner (Alperen Keleş) * build(deps-dev): bump vite from 6.0.7 to 6.2.6 in /bindings/wasm/test-limbo-pkg (dependabot[bot]) * btree: remove IterationState (Jussi Saurio) * build(deps): bump pyo3 from 0.24.0 to 0.24.1 (dependabot[bot]) * Multi column indexes + index seek refactor (Jussi Saurio) * Emit ANSI codes only when tracing is outputting to terminal (Preston Thorpe) * B-Tree code cleanups (Pekka Enberg) * btree index selection on rightmost pointer in `balance_non_root` (Pere Diaz Bou) * io/linux: make syscallio the default (io_uring is really slow) (Jussi Saurio) * Stress improvements (Pekka Enberg) * VDBE code cleanups (Pekka Enberg) * Memory tests to track large blob insertions (Pedro Muniz) * Setup tracing to allow output during test runs (Preston Thorpe) * allow insertion of multiple overflow cells (Pere Diaz Bou) * Properly handle insertion of indexed columns (Preston Thorpe) * VTabs: Proper handling of re-opened db files without the relevant extensions loaded (Preston Thorpe) * Account divider cell in size while distributing cells (Pere Diaz Bou) * Format infinite float as "Inf"/"-Inf" (jachewz) * update sqlite download version to 2025 + remove www. (Pere Diaz Bou) * Improve validation of btree balancing (Pere Diaz Bou) * Aggregation without group by produces incorrect results for scalars (Ihor Andrianov) * Dot command completion (Pedro Muniz) * Allow reading altered tables by defaulting to null in Column insn (Preston Thorpe) * docs(readme): update discord link (Jamie Barton) * More VDBE cleanups (Pekka Enberg) * Request load page on `insert_into_page` (Pere Diaz Bou) * core/vdbe: Rename execute_insn_* to op_* (Pekka Enberg) * Remove RWLock from Shared wal state (Pere Diaz Bou) * VDBE with indirect function dispatch (Pere Diaz Bou) ### Fixed * Fix truncation of error output in tests (Pedro Muniz) * Fix Unary Negate Operation on Blobs (Pedro Muniz) * Fix incompatibility `AND` Operation (Pedro Muniz) * Fix: comment out incorrect assert in fuzz (Pedro Muniz) * Fix two issues with indexes (Jussi Saurio) * Fuzz fix some operations (Pedro Muniz) * simulator: updates to bug base, refactors (Alperen Keleş) * Fix overwrite cell with size less than cell size (Pere Diaz Bou) * Fix `EXPLAIN` to be case insensitive (Pedro Muniz) * core: Fix syscall VFS on Linux (Pekka Enberg) * Index insert fixes (Pere Diaz Bou) * Decrease page count on balancing fixes (Pere Diaz Bou) * Remainder fixes (jachewz) * Fix virtual table translation issues (Preston Thorpe) * Fix overflow position in write_page() (Lâm Hoàng Phúc) ## 0.0.18 - 2025-04-02 ### Added * Jsonb support update (Ihor Andrianov) * Add BTree balancing after `delete` (Krishna Vishal) * Introduce Register struct (Pere Diaz Bou) * Introduce immutable record (Pere Diaz Bou) * Introduce libFuzzer (Levy A.) * WAL frame checksum support (Daniel Boll) * Initial JavaScript bindings with napi-rs (Pekka Enberg) * Initial pass at `UPDATE` support (Preston Thorpe) * Add `commit()` and placeholding insert to Python binding (Diego Reis) ### Updated * Create plan for Update queries (Preston Thorpe) * Validate cells inside a page after each operation (Pere Diaz Bou) * Refactor Cli Repl Commands to use clap (Pedro Muniz) * Allow balance_root to balance with interior pages (Pere Diaz Bou) * Let remainder (%) accept textual arguments (Anton Harniakou) * JSON code cleanups (Pekka Enberg) * Allocation improvements with ImmutableRecord, OwnedRecord and read_record (Pere Diaz Bou) * JavaScript binding improvements (Pekka Enberg) * Kill test environment (Pekka Enberg) * Remove public unlock method from `SpinLock` to prevent unsafe aliasing (Krishna Vishal) * Handle limit zero case in query plan emitter (Preston Thorpe) * Reduce MVCC cursor memory consumption (Ihor Andrianov) * Unary `+` is a noop (Levy A.) * JSON cache (Ihor Andrianov) * Bump `rusqlite` to 0.34 (Pere Diaz Bou) * core: Rename FileStorage to DatabaseFile (Pekka Enberg) * Improve Python bindings (Diego Reis) * Schema translation cleanups (Pekka Enberg) * Various JSON improvements (Ihor Andrianov) * Enable pretty mode in shell by default (Pekka Enberg) * Improve CLI color scheme (Pekka Enberg) * Impl Copy on some types in the pager to prevent explicit clones (Preston Thorpe) * Syntax highlighting and hinting (Pedro Muniz) * chore: gitignore files with an extension *.db (Anton Harniakou) * Organize extension library and feature gate VFS (Preston Thorpe) * fragment bench functions (Pere Diaz Bou) ### Fixed * Remove unnecessary balance code that crashes (Pere Diaz Bou) * Fix propagation of divider cell balancing interior page (Pere Diaz Bou) * Fuzz test btree fix seeking. (Pere Diaz Bou) * Fix IdxCmp insn comparisons (Jussi Saurio) * Fixes probably all floating point math issues and floating point display issues. (Ihor Andrianov) * Make BTreeCell/read_payload not allocate any data + overflow fixes (Pere Diaz Bou) * Fix `compute_shl` negate with overflow (Krishna Vishal) * Fix a typo in README.md (Tshepang Mbambo) * Fix platform specific FFI C pointer type casts (Preston Thorpe) * core: Fix Destroy opcode root page handling (Pekka Enberg) * Fix `SELECT 0.0 = 0` returning false (lgualtieri75) * bindings/python: Fix flaky tests (Diego Reis) * Fix io_uring WAL write corruption by ensuring buffer lifetime (Daniel Boll) ## 0.0.17 - 2025-03-19 ### Added * `BEGIN DEFERRED` support (Diego Reis) * Experimental MVCC integration (Pekka Enberg) * `DROP TABLE` support (Zaid Humayun) * Initial pass on Virtual FileSystem extension module (Preston Thorpe) * JSONB support (Ihor Andrianov) * Shell command completion (Pedro Muniz) ### Updated ### Fixed * Fixes and improvements to Rust bindings (yirt grek and 南宫茜) * Transaction management fixes (Pere Diaz Bou and Diego Reis) * JSON function fixes (Ihor Andrianov) ## 0.0.16 - 2025-03-05 ### Added * Virtual table support (Preston Thorpe) * Improvements to Java bindings (Kim Seon Woo) * Improvements to Rust bindings (Pekka Enberg) * Add sqlean ipaddr extension (EmNudge) * Add "dump" and "load" to the help menu (EmNudge) * Initial Antithesis testing tool (Pekka Enberg) ### Fixed * SQLite B-Tree balancing algorithm (Pere Diaz Bou) * B-Tree improves and fixes (Pere Diaz Bou and Perston Thorpe) * Display blobs as blob literals in `.dump` (from Mohamed Hossam) * Fix wrong count() result if the column specified contains a NULL (lgualtieri75) * Fix casting text to integer to match SQLite' (Preston Thorpe) * Improve `SELECT 1` performance to be on par with SQLite (Pekka Enberg) * Fix offset_sec normalization in extensions/time (meteorgan) * Handle parsing URI according to SQLite specification (Preston Thorpe) * Escape character is ignored in LIKE function (lgualtieri75) * Fix cast_text_to_number compatibility (Pedro Muniz) * Modify the LIKE function to work with all types (Mohamed Hossam) ## 0.0.15 - 2025-02-18 ### Added **Core:** * Initial pass on virtual tables (Preston Thorpe) * Import MVCC code to the source tree -- not enabled (Pekka Enberg, Piotr Sarna, Avinash Sajjanshetty) * Implement `json_set` (Marcus Nilsson) * Initial support for WITH clauses (common table expressions) (Jussi Saurio) * `BEGIN IMMEDIATE` + `COMMIT` support (Pekka Enberg) * `BEGIN EXCLUSIVE` support (Pekka Enberg) * Add Printf Support (Zaid Humayun) * Add support for `delete` row (Krishna Vishal) * Implement json_quote (Pedro Muniz) * Add read implementation of user_version pragma with ReadCookie opcode (Jonathan Webb) * Json path refine (Ihor Andrianov) * cli: Basic dump support (Glauber Costa) * Support numeric column references in GROUP BY (Jussi Saurio) * Implement the legacy_file_format pragma (Glauber Costa) * Added IdxLE and IdxLT opcodes (Omolola Olamide) **Java bindings:* * Improve JDBC support with, for example, prepared statements (Kim Seon Woo) * Rename package name `tech.turso` (Kim Seon Woo) **Extensions:** * Sqlean Crypto extension (Diego Reis) * Sqlean Time extension (Pedro Muniz) * Add support for `regexp_replace()` (lgualtieri75) **Simulator:** * Add NoREC testing property (Alperen Keleş) * Add `--differential` mode against SQLite (Alperen Keleş) ### Fixed **Core:** * Fix 24/48 bit width serial types parsing (Nikita Sivukhin) * Fix substr (Nikita Sivukhin) * Fix math binary (Nikita Sivukhin) * Fix and predicate (Nikita Sivukhin) * Fix IdxGt, IdxGe, IdxLt, and IdxLe instructions (Jussi Saurio) * Fix not evaling constant conditions when no tables in query (Jussi Saurio) * Fix remainder panic on zero right-hand-side (Jussi Saurio) * Fix invalid text columns generated by dump (Kingsley Yung) * Fix incorrect CAST text->numeric if valid prefix is 1 char long (Jussi Saurio) * Improve SQL statement prepare performance (Jussi Saurio) * Fix VCC write conflict handling (Jussi Saurio) * Fix various bugs in B-Tree handling (Nikita Sivukhin) * Fix case and emit (Nikita Sivukhin) * Fix coalesce (Nikita Sivukhin) * Fix cast (Nikita Sivukhin) * Fix string funcs (Nikita Sivukhin) * Fix floating point truncation in JSON #877 (lgualtieri75) * Fix bug with `SELECT` referring to a mixed-case alias (Jussi Saurio) ## 0.0.14 - 2025-02-04 ### Added **Core:** * Improve changes() and total_changes() functions and add tests (Ben Li) * Add support for `json_object` function (Jorge Hermo) * Implemented json_valid function (Harin) * Implement Not (Vrishabh) * Initial support for wal_checkpoint pragma (Sonny) * Implement Or and And bytecodes (Diego Reis) * Implement strftime function (Pedro Muniz) * implement sqlite_source_id function (Glauber Costa) * json_patch() function implementation (Ihor Andrianov) * json_remove() function implementation (Ihor Andrianov) * Implement isnull / not null for filter expressions (Glauber Costa) * Add support for offset in select queries (Ben Li) * Support returning column names from prepared statement (Preston Thorpe) * Implement Concat opcode (Harin) * Table info (Glauber Costa) * Pragma list (Glauber Costa) * Implement Noop bytecode (Pedro Muniz) * implement is and is not where constraints (Glauber Costa) * Pagecount (Glauber Costa) * Support column aliases in GROUP BY, ORDER BY and HAVING (Jussi Saurio) * Implement json_pretty (Pedro Muniz) **Extensions:** * Initial pass on vector extension (Pekka Enberg) * Enable static linking for 'built-in' extensions (Preston Thorpe) **Go Bindings:** * Initial support for Go database/sql driver (Preston Thorpe) * Avoid potentially expensive operations on prepare' (Glauber Costa) **Java Bindings:** * Implement JDBC `ResultSet` (Kim Seon Woo) * Implement LimboConnection `close()` (Kim Seon Woo) * Implement close() for `LimboStatement` and `LimboResultSet` (Kim Seon Woo) * Implement methods in `JDBC4ResultSet` (Kim Seon Woo) * Load native library from Jar (Kim Seon Woo) * Change logger dependency (Kim Seon Woo) * Log driver loading error (Pekka Enberg) **Simulator:** * Implement `--load` and `--watch` flags (Alperen Keleş) **Build system and CI:** * Add Nyrkiö change point detection to 'cargo bench' workflow (Henrik Ingo) ### Fixed * Fix `select X'1';` causes limbo to go in infinite loop (Krishna Vishal) * Fix rowid search codegen (Nikita Sivukhin) * Fix logical codegen (Nikita Sivukhin) * Fix parser panic when duplicate column names are given to `CREATE TABLE` (Krishna Vishal) * Fix panic when double quoted strings are used for column names. (Krishna Vishal) * Fix `SELECT -9223372036854775808` result differs from SQLite (Krishna Vishal) * Fix `SELECT ABS(-9223372036854775808)` causes limbo to panic. (Krishna Vishal) * Fix memory leaks, make extension types more efficient (Preston Thorpe) * Fix table with single column PRIMARY KEY to not create extra btree (Krishna Vishal) * Fix null cmp codegen (Nikita Sivukhin) * Fix null expr codegen (Nikita Sivukhin) * Fix rowid generation (Nikita Sivukhin) * Fix shr instruction (Nikita Sivukhin) * Fix strftime function compatibility problems (Pedro Muniz) * Dont fsync the WAL on read queries (Jussi Saurio) ## 0.0.13 - 2025-01-19 ### Added * Initial support for native Limbo extensions (Preston Thorpe) * npm packaging for node and web (Elijah Morgan) * Add support for `rowid` keyword' (Kould) * Add support for shift left, shift right, is and is not operators (Vrishabh) * Add regexp extension (Vrishabh) * Add counterexample minimization to simulator (Alperen Keleş) * Initial support for binding values to prepared statements (Levy A.) ### Updated * Java binding improvements (Kim Seon Woo) * Reduce `liblimbo_sqlite3.a` size' (Pekka Enberg) ### Fixed * Fix panics on invalid aggregate function arguments (Krishna Vishal) * Fix null compare operations not giving null (Vrishabh) * Run all statements from SQL argument in CLI (Vrishabh) * Fix MustBeInt opcode semantics (Vrishabh) * Fix recursive binary operation logic (Jussi Saurio) * Fix SQL comment parsing in Limbo shell (Diego Reis and Clyde) ## 0.0.12 - 2025-01-14 ### Added **Core:** * Improve JSON function support (Kacper Madej, Peter Sooley) * Support nested parenthesized conditional expressions (Preston Thorpe) * Add support for changes() and total_changes() functions (Lemon-Peppermint) * Auto-create index in CREATE TABLE when necessary (Jussi Saurio) * Add partial support for datetime() function (Preston Thorpe) * SQL parser performance improvements (Jussi Saurio) **Shell:** * Show pretty parse errors in the shell (Samyak Sarnayak) * Add CSV import support to shell (Vrishabh) * Selectable IO backend with --io={syscall,io-uring} argument (Jorge López Tello) **Bindings:** * Initial version of Java bindings (Kim Seon Woo) * Initial version of Rust bindings (Pekka Enberg) * Add OPFS support to Wasm bindings (Elijah Morgan) * Support uncorrelated FROM clause subqueries (Jussi Saurio) * In-memory support to `sqlite3_open()` (Pekka Enberg) ### Fixed * Make iterate() lazy in JavaScript bindings (Diego Reis) * Fix integer overflow output to be same as sqlite3 (Vrishabh) * Fix 8-bit serial type to encoding (Preston Thorpe) * Query plan optimizer bug fixes (Jussi Saurio) * B-Tree balancing fixes (Pere Diaz Bou) * Fix index seek wrong on `SeekOp::LT`\`SeekOp::GT` (Kould) * Fix arithmetic operations for text values' from Vrishabh * Fix quote escape in SQL literals (Vrishabh) ## 0.0.11 - 2024-12-31 ### Added * Add in-memory mode to Python bindings (Jean Arhancet) * Add json_array_length function (Peter Sooley) * Add support for the UUID extension (Preston Thorpe) ### Changed * Enable sqpoll by default in io_uring (Preston Thorpe) * Simulator improvements (Alperen Keleş) ### Fixed * Fix escaping issues with like and glob functions (Vrishabh) * Fix `sqlite_version()` out of bound panics' (Diego Reis) * Fix on-disk file format bugs (Jussi Saurio) ## 0.0.10 - 2024-12-18 ### Added * In-memory mode (Preston Thorpe) * More CLI improvements (Preston Thorpe) * Add support for replace() function (Alperen Keleş) * Unary operator improvements (Jean Arhancet) * Add support for unex(x, y) function (Kacper Kołodziej) ### Fixed * Fix primary key handling when there's rowid and PK is not alias (Jussi Saurio) ## 0.0.9 - 2024-12-12 ### Added * Improve CLI (Preston Thorpe) * Add support for iif() function (Alex Miller) * Add support for last_insert_rowid() function (Krishna Vishal) * Add support JOIN USING and NATURAL JOIN (Jussi Saurio) * Add support for more scalar functions (Kacper Kołodziej) * Add support for `HAVING` clause (Jussi Saurio) * Add `get()` and `iterate()` to JavaScript/Wasm API (Jean Arhancet) ## 0.0.8 - 2024-11-20 ### Added * Python package build and example usage (Pekka Enberg) ## 0.0.7 - 2024-11-20 ### Added * Minor improvements to JavaScript API (Pekka Enberg) * `CAST` support (Jussi Saurio) ### Fixed * Fix issues found in-btree code with the DST (Pere Diaz Bou) ## 0.0.6 - 2024-11-18 ### Fixed - Fix database truncation caused by `limbo-wasm` opening file in wrong mode (Pere Diaz Bou) ## 0.0.5 - 2024-11-18 ### Added - `CREATE TABLE` support (Pere Diaz Bou) - Add Add Database.prepare() and Statement.all() to Wasm bindings (Pekka Enberg) - WAL improvements (Pere Diaz Bou) - Primary key index scans and single-column secondary index scans (Jussi Saurio) - `GROUP BY` support (Jussi Saurio) - Overflow page support (Pere Diaz Bou) - Improvements to Python bindings (Jean Arhancet and Lauri Virtanen) - Improve scalar function support (Lauri Virtanen) ### Fixed - Panic in codegen with `COUNT(*)` (Jussi Saurio) - Fix `LIKE` to be case insensitive (RJ Barman) ## 0.0.4 - 2024-08-22 - Query planner rewrite (Jussi Saurio) - Initial pass on Python bindings (Jean Arhancet) - Improve scalar function support (Kim Seon Woo and Jean Arhancet) ### Added - Partial support for `json()` function (Jean Arhancet) ## 0.0.3 - 2024-08-01 ### Added - Initial pass on the write path. Note that the write path is not transactional yet. (Pere Diaz Bou) - More scalar functions: `unicode()` (Ethan Niser) - Optimize point queries with integer keys (Jussi Saurio) ### Fixed - `ORDER BY` support for nullable sorting columns and qualified identifiers (Jussi Saurio) - Fix `.schema` command crash in the CLI ([#212](https://github.com/tursodatabase/limbo/issues/212) (Jussi Saurio) ## 0.0.2 - 2024-07-24 ### Added - Partial `LEFT JOIN` support. - Partial `ORDER BY` support. - Partial scalar function support. ### Fixed - Lock database file with POSIX filesystem advisory lock when database is opened to prevent concurrent processes from corrupting a file. Please note that the locking scheme differs from SQLite, which uses POSIX advisory locks for every transaction. We're defaulting to locking on open because it's faster. (Issue #94) ### Changed - Install to `~/.limbo/` instead of `CARGO_HOME`. ## 0.0.1 - 2024-07-17 ### Added - Partial `SELECT` statement support, including `WHERE`, `LIKE`, `LIMIT`, `CROSS JOIN`, and `INNER JOIN`. - Aggregate function support. - `EXPLAIN` statement support. - Partial `PRAGMA` statement support, including `cache_size`. - Asynchronous I/O support with Linux io_uring using direct I/O and Darwin kqueue. - Initial pass on command line shell with following commands: ================================================ FILE: COMPAT.md ================================================ # Turso compatibility with SQLite This document describes the compatibility of Turso with SQLite. ## Table of contents - [Turso compatibility with SQLite](#turso-compatibility-with-sqlite) - [Table of contents](#table-of-contents) - [Overview](#overview) - [Features](#features) - [Limitations](#limitations) - [SQLite query language](#sqlite-query-language) - [Statements](#statements) - [PRAGMA](#pragma) - [Expressions](#expressions) - [SQL functions](#sql-functions) - [Scalar functions](#scalar-functions) - [Mathematical functions](#mathematical-functions) - [Aggregate functions](#aggregate-functions) - [Date and time functions](#date-and-time-functions) - [JSON functions](#json-functions) - [SQLite C API](#sqlite-c-api) - [Database Connection](#database-connection) - [Prepared Statements](#prepared-statements) - [Binding Parameters](#binding-parameters) - [Result Columns](#result-columns) - [Result Values](#result-values-sqlite3_value) - [Error Handling](#error-handling) - [Changes and Row IDs](#changes-and-row-ids) - [Memory Management](#memory-management) - [Callback Functions](#callback-functions) - [User-Defined Functions](#user-defined-functions) - [Collation Functions](#collation-functions) - [Backup API](#backup-api) - [BLOB I/O](#blob-io) - [WAL Functions](#wal-functions) - [Utility Functions](#utility-functions) - [Table Metadata](#table-metadata) - [Virtual Tables](#virtual-tables) - [Loadable Extensions](#loadable-extensions) - [Serialization](#serialization) - [Miscellaneous](#miscellaneous) - [Turso-specific Extensions](#turso-specific-extensions) - [SQLite VDBE opcodes](#sqlite-vdbe-opcodes) - [SQLite journaling modes](#sqlite-journaling-modes) - [Extensions](#extensions) - [UUID](#uuid) - [regexp](#regexp) - [Vector](#vector) - [Time](#time) - [Full-Text Search (FTS)](#full-text-search-fts) - [CSV](#csv) - [Percentile](#percentile) - [Table-Valued Functions](#table-valued-functions) - [Internal Virtual Tables](#internal-virtual-tables) ## Overview Turso aims to be fully compatible with SQLite, with opt-in features not supported by SQLite. ### Features * ✅ SQLite file format is fully supported * 🚧 SQLite query language [[status](#sqlite-query-language)] is partially supported * 🚧 SQLite C API [[status](#sqlite-c-api)] is partially supported ### Limitations * ⛔️ Concurrent access from multiple processes is not supported. * ⛔️ Vacuum is not supported. ## SQLite query language ### Statements | Statement | Status | Comment | |---------------------------|---------|-----------------------------------------------------------------------------------| | ALTER TABLE | ✅ Yes | | | ANALYZE | ✅ Yes | | | ATTACH DATABASE | ✅ Yes | | | BEGIN TRANSACTION | ✅ Yes | | | COMMIT TRANSACTION | ✅ Yes | | | CHECK | ✅ Yes | | | CREATE INDEX | ✅ Yes | | | CREATE TABLE | ✅ Yes | | | CREATE TABLE ... STRICT | 🚧 Partial | Strict schema mode is experimental. | | CREATE TRIGGER | ✅ Yes | | | CREATE VIEW | ✅ Yes | | | CREATE VIRTUAL TABLE | ✅ Yes | | | DELETE | ✅ Yes | | | DETACH DATABASE | ✅ Yes | | | DROP INDEX | 🚧 Partial | Disabled by default. | | DROP TABLE | ✅ Yes | | | DROP TRIGGER | ✅ Yes | | | DROP VIEW | ✅ Yes | | | END TRANSACTION | ✅ Yes | | | EXPLAIN | ✅ Yes | | | INDEXED BY | ❌ No | | | INSERT | ✅ Yes | | | INSERT ... ON CONFLICT (UPSERT) | ✅ Yes | | | ON CONFLICT clause | ✅ Yes | | | REINDEX | ❌ No | | | RELEASE SAVEPOINT | ✅ No | | | REPLACE | ✅ Yes | | | RETURNING clause | ✅ Yes | | | ROLLBACK TRANSACTION | ✅ Yes | | | SAVEPOINT | ✅ No | | | SELECT | ✅ Yes | | | SELECT ... WHERE | ✅ Yes | | | SELECT ... WHERE ... LIKE | ✅ Yes | | | SELECT ... LIMIT | ✅ Yes | | | SELECT ... ORDER BY | ✅ Yes | | | SELECT ... GROUP BY | ✅ Yes | | | SELECT ... HAVING | ✅ Yes | | | SELECT ... JOIN | ✅ Yes | | | SELECT ... CROSS JOIN | ❌ No | SQLite CROSS JOIN means "do not reorder joins". | | SELECT ... INNER JOIN | ✅ Yes | | | SELECT ... OUTER JOIN | 🚧 Partial | no RIGHT JOIN | | SELECT ... JOIN USING | ✅ Yes | | | SELECT ... NATURAL JOIN | ✅ Yes | | | UPDATE | ✅ Yes | | | VACUUM | ❌ No | | | WITH clause | 🚧 Partial | ❌ No RECURSIVE, no MATERIALIZED, only SELECT supported in CTEs | | WINDOW functions | 🚧 Partial | only default frame definition, no window-specific functions (rank() etc) | | GENERATED | ❌ No | | #### [PRAGMA](https://www.sqlite.org/pragma.html) | Statement | Status | Comment | |----------------------------------|------------|----------------------------------------------| | PRAGMA analysis_limit | ❌ No | | | PRAGMA application_id | ✅ Yes | | | PRAGMA auto_vacuum | ❌ No | | | PRAGMA automatic_index | ❌ No | | | PRAGMA busy_timeout | ✅ Yes | | | PRAGMA cache_size | ✅ Yes | | | PRAGMA cache_spill | 🚧 Partial | Enabled/Disabled only | | PRAGMA case_sensitive_like | Not Needed | deprecated in SQLite | | PRAGMA cell_size_check | ❌ No | | | PRAGMA checkpoint_fullsync | ❌ No | | | PRAGMA collation_list | ❌ No | | | PRAGMA compile_options | ❌ No | | | PRAGMA count_changes | Not Needed | deprecated in SQLite | | PRAGMA data_store_directory | Not Needed | deprecated in SQLite | | PRAGMA data_version | ❌ No | | | PRAGMA database_list | ✅ Yes | | | PRAGMA default_cache_size | Not Needed | deprecated in SQLite | | PRAGMA defer_foreign_keys | ❌ No | | | PRAGMA empty_result_callbacks | Not Needed | deprecated in SQLite | | PRAGMA encoding | ✅ Yes | | | PRAGMA foreign_key_check | ❌ No | | | PRAGMA foreign_key_list | ❌ No | | | PRAGMA foreign_keys | ✅ Yes | | | PRAGMA freelist_count | ✅ Yes | | | PRAGMA full_column_names | Not Needed | deprecated in SQLite | | PRAGMA fullsync | ❌ No | | | PRAGMA function_list | ✅ Yes | | | PRAGMA hard_heap_limit | ❌ No | | | PRAGMA ignore_check_constraints | ✅ Yes | | | PRAGMA incremental_vacuum | ❌ No | | | PRAGMA index_info | ✅ Yes | | | PRAGMA index_list | ✅ Yes | | | PRAGMA index_xinfo | ✅ Yes | | | PRAGMA integrity_check | ✅ Yes | | | PRAGMA journal_mode | ✅ Yes | | | PRAGMA journal_size_limit | ❌ No | | | PRAGMA legacy_alter_table | ❌ No | | | PRAGMA legacy_file_format | ✅ Yes | | | PRAGMA locking_mode | ❌ No | | | PRAGMA max_page_count | ✅ Yes | | | PRAGMA mmap_size | ❌ No | | | PRAGMA module_list | ❌ No | | | PRAGMA optimize | ❌ No | | | PRAGMA page_count | ✅ Yes | | | PRAGMA page_size | ✅ Yes | | | PRAGMA parser_trace | ❌ No | | | PRAGMA pragma_list | ✅ Yes | | | PRAGMA query_only | ✅ Yes | | | PRAGMA quick_check | ✅ Yes | | | PRAGMA read_uncommitted | ❌ No | | | PRAGMA recursive_triggers | ❌ No | | | PRAGMA reverse_unordered_selects | ❌ No | | | PRAGMA schema_version | ✅ Yes | For writes, emulate defensive mode (always noop)| | PRAGMA secure_delete | ❌ No | | | PRAGMA short_column_names | Not Needed | deprecated in SQLite | | PRAGMA shrink_memory | ❌ No | | | PRAGMA soft_heap_limit | ❌ No | | | PRAGMA stats | ❌ No | Used for testing in SQLite | | PRAGMA synchronous | 🚧 Partial | `OFF` and `FULL` supported | | PRAGMA table_info | ✅ Yes | | | PRAGMA table_list | ✅ Yes | | | PRAGMA table_xinfo | ✅ Yes | | | PRAGMA temp_store | ✅ Yes | | | PRAGMA temp_store_directory | Not Needed | deprecated in SQLite | | PRAGMA threads | ❌ No | | | PRAGMA trusted_schema | ❌ No | | | PRAGMA user_version | ✅ Yes | | | PRAGMA vdbe_addoptrace | ❌ No | | | PRAGMA vdbe_debug | ❌ No | | | PRAGMA vdbe_listing | ❌ No | | | PRAGMA vdbe_trace | ❌ No | | | PRAGMA wal_autocheckpoint | ❌ No | | | PRAGMA wal_checkpoint | 🚧 Partial | Not Needed calling with param (pragma-value) | | PRAGMA writable_schema | ❌ No | | ### Expressions Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). | Syntax | Status | Comment | |---------------------------|---------|------------------------------------------| | literals | ✅ Yes | | | schema.table.column | 🚧 Partial | Schemas aren't supported | | unary operator | ✅ Yes | | | binary operator | 🚧 Partial | Only `%`, `!<`, and `!>` are unsupported | | agg() FILTER (WHERE ...) | ❌ No | Is incorrectly ignored | | ... OVER (...) | ❌ No | Is incorrectly ignored | | (expr) | ✅ Yes | | | CAST (expr AS type) | ✅ Yes | | | COLLATE | 🚧 Partial | Custom Collations not supported | | (NOT) LIKE | ✅ Yes | | | (NOT) GLOB | ✅ Yes | | | (NOT) REGEXP | ✅ Yes | | | (NOT) MATCH | ❌ No | | | IS (NOT) | ✅ Yes | | | IS (NOT) DISTINCT FROM | ✅ Yes | | | (NOT) BETWEEN ... AND ... | ✅ Yes | Expression is rewritten in the optimizer | | (NOT) IN (SELECT...) | ✅ Yes | | | (NOT) EXISTS (SELECT...) | ✅ Yes | | | x (SELECT...)) | 🚧 Partial | Only scalar subqueries supported, i.e. not (x,y) = (SELECT...) | CASE WHEN THEN ELSE END | ✅ Yes | | | RAISE | ✅ Yes | `RAISE('msg')` and `RAISE(ABORT, 'msg')` also work outside triggers. | ### SQL functions #### Scalar functions | Function | Status | Comment | |------------------------------|---------|------------------------------------------------------| | abs(X) | ✅ Yes | | | changes() | 🚧 Partial | Still need to support update statements and triggers | | char(X1,X2,...,XN) | ✅ Yes | | | coalesce(X,Y,...) | ✅ Yes | | | concat(X,...) | ✅ Yes | | | concat_ws(SEP,X,...) | ✅ Yes | | | format(FORMAT,...) | ✅ Yes | | | glob(X,Y) | ✅ Yes | | | hex(X) | ✅ Yes | | | ifnull(X,Y) | ✅ Yes | | | if(X,Y,Z) | ✅ Yes | Alias of iif | | iif(X,Y,Z) | ✅ Yes | | | instr(X,Y) | ✅ Yes | | | last_insert_rowid() | ✅ Yes | | | length(X) | ✅ Yes | | | like(X,Y) | ✅ Yes | | | like(X,Y,Z) | ✅ Yes | | | likelihood(X,Y) | ✅ Yes | | | likely(X) | ✅ Yes | | | load_extension(X) | 🚧 Partial | Only Turso-native extensions, not SQLite .so/.dll | | load_extension(X,Y) | ❌ No | | | lower(X) | ✅ Yes | | | ltrim(X) | ✅ Yes | | | ltrim(X,Y) | ✅ Yes | | | max(X,Y,...) | ✅ Yes | | | min(X,Y,...) | ✅ Yes | | | nullif(X,Y) | ✅ Yes | | | octet_length(X) | ✅ Yes | | | printf(FORMAT,...) | ✅ Yes | | | quote(X) | ✅ Yes | | | random() | ✅ Yes | | | randomblob(N) | ✅ Yes | | | replace(X,Y,Z) | ✅ Yes | | | round(X) | ✅ Yes | | | round(X,Y) | ✅ Yes | | | rtrim(X) | ✅ Yes | | | rtrim(X,Y) | ✅ Yes | | | sign(X) | ✅ Yes | | | soundex(X) | ✅ Yes | | | sqlite_compileoption_get(N) | ❌ No | | | sqlite_compileoption_used(X) | ❌ No | | | sqlite_offset(X) | ❌ No | | | sqlite_source_id() | ✅ Yes | | | sqlite_version() | ✅ Yes | | | substr(X,Y,Z) | ✅ Yes | | | substr(X,Y) | ✅ Yes | | | substring(X,Y,Z) | ✅ Yes | | | substring(X,Y) | ✅ Yes | | | total_changes() | 🚧 Partial | Still need to support update statements and triggers | | trim(X) | ✅ Yes | | | trim(X,Y) | ✅ Yes | | | typeof(X) | ✅ Yes | | | unhex(X) | ✅ Yes | | | unhex(X,Y) | ✅ Yes | | | unicode(X) | ✅ Yes | | | unlikely(X) | ✅ Yes | | | upper(X) | ✅ Yes | | | unistr(X) | ❌ No | | | zeroblob(N) | ✅ Yes | | #### Mathematical functions | Function | Status | Comment | |------------|--------|---------| | acos(X) | ✅ Yes | | | acosh(X) | ✅ Yes | | | asin(X) | ✅ Yes | | | asinh(X) | ✅ Yes | | | atan(X) | ✅ Yes | | | atan2(Y,X) | ✅ Yes | | | atanh(X) | ✅ Yes | | | ceil(X) | ✅ Yes | | | ceiling(X) | ✅ Yes | | | cos(X) | ✅ Yes | | | cosh(X) | ✅ Yes | | | degrees(X) | ✅ Yes | | | exp(X) | ✅ Yes | | | floor(X) | ✅ Yes | | | ln(X) | ✅ Yes | | | log(B,X) | ✅ Yes | | | log(X) | ✅ Yes | | | log10(X) | ✅ Yes | | | log2(X) | ✅ Yes | | | mod(X,Y) | ✅ Yes | | | pi() | ✅ Yes | | | pow(X,Y) | ✅ Yes | | | power(X,Y) | ✅ Yes | | | radians(X) | ✅ Yes | | | sin(X) | ✅ Yes | | | sinh(X) | ✅ Yes | | | sqrt(X) | ✅ Yes | | | tan(X) | ✅ Yes | | | tanh(X) | ✅ Yes | | | trunc(X) | ✅ Yes | | #### Aggregate functions | Function | Status | Comment | |------------------------------|---------|---------| | avg(X) | ✅ Yes | | | count(X) | ✅ Yes | | | count(*) | ✅ Yes | | | group_concat(X) | ✅ Yes | | | group_concat(X,Y) | ✅ Yes | | | string_agg(X,Y) | ✅ Yes | | | max(X) | ✅ Yes | | | min(X) | ✅ Yes | | | sum(X) | ✅ Yes | | | total(X) | ✅ Yes | | | median(X) | ✅ Yes | Requires percentile extension | | percentile(Y,P) | ✅ Yes | Requires percentile extension | | percentile_cont(Y,P) | ✅ Yes | Requires percentile extension | | percentile_disc(Y,P) | ✅ Yes | Requires percentile extension | | stddev(X) | ✅ Yes | Turso extension | #### Date and time functions | Function | Status | Comment | |-------------|---------|------------------------------| | date() | ✅ Yes | | | time() | ✅ Yes | | | datetime() | ✅ Yes | | | julianday() | ✅ Yes | | | unixepoch() | ✅ Yes | | | strftime() | ✅ Yes | | | timediff() | ✅ Yes | | Modifiers: | Modifier | Status| Comment | |----------------|-------|---------------------------------| | Days | ✅ Yes | | | Hours | ✅ Yes | | | Minutes | ✅ Yes | | | Seconds | ✅ Yes | | | Months | ✅ Yes | | | Years | ✅ Yes | | | TimeOffset | ✅ Yes | | | DateOffset | ✅ Yes | | | DateTimeOffset | ✅ Yes | | | Ceiling | ✅ Yes | | | Floor | ✅ Yes | | | StartOfMonth | ✅ Yes | | | StartOfYear | ✅ Yes | | | StartOfDay | ✅ Yes | | | Weekday(N) | ✅ Yes | | | Auto | ✅ Yes | | | UnixEpoch | ✅ Yes | | | JulianDay | ✅ Yes | | | Localtime | ✅ Yes | | | Utc | ✅ Yes | | | Subsec | ✅ Yes | | #### JSON functions | Function | Status | Comment | | ---------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | json(json) | ✅ Yes | | | jsonb(json) | ✅ Yes | | | json_array(value1,value2,...) | ✅ Yes | | | jsonb_array(value1,value2,...) | ✅ Yes | | | json_array_length(json) | ✅ Yes | | | json_array_length(json,path) | ✅ Yes | | | json_error_position(json) | ✅ Yes | | | json_extract(json,path,...) | ✅ Yes | | | jsonb_extract(json,path,...) | ✅ Yes | | | json -> path | ✅ Yes | | | json ->> path | ✅ Yes | | | json_insert(json,path,value,...) | ✅ Yes | | | jsonb_insert(json,path,value,...) | ✅ Yes | | | json_object(label1,value1,...) | ✅ Yes | | | jsonb_object(label1,value1,...) | ✅ Yes | | | json_patch(json1,json2) | ✅ Yes | | | jsonb_patch(json1,json2) | ✅ Yes | | | json_pretty(json) | ✅ Yes | | | json_remove(json,path,...) | ✅ Yes | | | jsonb_remove(json,path,...) | ✅ Yes | | | json_replace(json,path,value,...) | ✅ Yes | | | jsonb_replace(json,path,value,...) | ✅ Yes | | | json_set(json,path,value,...) | ✅ Yes | | | jsonb_set(json,path,value,...) | ✅ Yes | | | json_type(json) | ✅ Yes | | | json_type(json,path) | ✅ Yes | | | json_valid(json) | ✅ Yes | | | json_valid(json,flags) | ✅ Yes | | | json_quote(value) | ✅ Yes | | | json_group_array(value) | ✅ Yes | | | jsonb_group_array(value) | ✅ Yes | | | json_group_object(label,value) | ✅ Yes | | | jsonb_group_object(name,value) | ✅ Yes | | | json_each(json) | ✅ Yes | | | json_each(json,path) | ✅ Yes | | | json_tree(json) | 🚧 Partial | see commented-out tests in json.test | | json_tree(json,path) | 🚧 Partial | see commented-out tests in json.test | ## SQLite C API ### Database Connection | Interface | Status | Comment | |------------------------|---------|---------| | sqlite3_open | ✅ Yes | | | sqlite3_open_v2 | 🚧 Partial | Delegates to sqlite3_open, flags/VFS ignored | | sqlite3_open16 | ❌ No | | | sqlite3_close | ✅ Yes | | | sqlite3_close_v2 | ✅ Yes | Same as sqlite3_close | | sqlite3_db_filename | ✅ Yes | | | sqlite3_db_config | ❌ No | Stub | | sqlite3_db_handle | ✅ Yes | | | sqlite3_db_readonly | ❌ No | | | sqlite3_db_status | ❌ No | | | sqlite3_db_cacheflush | ❌ No | | | sqlite3_db_release_memory | ❌ No | | | sqlite3_db_name | ❌ No | | | sqlite3_db_mutex | ❌ No | | | sqlite3_get_autocommit | ✅ Yes | | | sqlite3_limit | ❌ No | Stub | | sqlite3_initialize | ✅ Yes | | | sqlite3_shutdown | ✅ Yes | | | sqlite3_config | ❌ No | | ### Prepared Statements | Interface | Status | Comment | |-----------------------------|---------|---------| | sqlite3_prepare | ❌ No | | | sqlite3_prepare_v2 | ✅ Yes | | | sqlite3_prepare_v3 | ✅ Yes | Delegates to prepare_v2, prepFlags ignored | | sqlite3_prepare16 | ❌ No | | | sqlite3_prepare16_v2 | ❌ No | | | sqlite3_finalize | ✅ Yes | | | sqlite3_step | ✅ Yes | | | sqlite3_reset | ✅ Yes | | | sqlite3_exec | ✅ Yes | | | sqlite3_stmt_readonly | ❌ No | Stub | | sqlite3_stmt_busy | ❌ No | Stub | | sqlite3_stmt_status | ❌ No | | | sqlite3_sql | ❌ No | | | sqlite3_expanded_sql | ❌ No | Stub | | sqlite3_normalized_sql | ❌ No | | | sqlite3_next_stmt | ✅ Yes | | ### Binding Parameters | Interface | Status | Comment | |------------------------------|---------|---------| | sqlite3_bind_parameter_count | ✅ Yes | | | sqlite3_bind_parameter_name | ✅ Yes | | | sqlite3_bind_parameter_index | ✅ Yes | | | sqlite3_bind_null | ✅ Yes | | | sqlite3_bind_int | ✅ Yes | | | sqlite3_bind_int64 | ✅ Yes | | | sqlite3_bind_double | ✅ Yes | | | sqlite3_bind_text | ✅ Yes | | | sqlite3_bind_text16 | ❌ No | | | sqlite3_bind_text64 | ❌ No | | | sqlite3_bind_blob | ✅ Yes | | | sqlite3_bind_blob64 | ❌ No | | | sqlite3_bind_value | ❌ No | | | sqlite3_bind_pointer | ❌ No | | | sqlite3_bind_zeroblob | ❌ No | | | sqlite3_bind_zeroblob64 | ❌ No | | | sqlite3_clear_bindings | ✅ Yes | | ### Result Columns | Interface | Status | Comment | |--------------------------|---------|---------| | sqlite3_column_count | ✅ Yes | | | sqlite3_column_name | ✅ Yes | | | sqlite3_column_name16 | ❌ No | | | sqlite3_column_decltype | ✅ Yes | | | sqlite3_column_decltype16| ❌ No | | | sqlite3_column_type | ✅ Yes | | | sqlite3_column_int | ✅ Yes | | | sqlite3_column_int64 | ✅ Yes | | | sqlite3_column_double | ✅ Yes | | | sqlite3_column_text | ✅ Yes | | | sqlite3_column_text16 | ❌ No | | | sqlite3_column_blob | ✅ Yes | | | sqlite3_column_bytes | ✅ Yes | | | sqlite3_column_bytes16 | ❌ No | | | sqlite3_column_value | ❌ No | | | sqlite3_column_table_name| ✅ Yes | | | sqlite3_column_database_name | ❌ No | | | sqlite3_column_origin_name | ❌ No | | | sqlite3_data_count | ✅ Yes | | ### Result Values (sqlite3_value) | Interface | Status | Comment | |------------------------|---------|---------| | sqlite3_value_type | ✅ Yes | | | sqlite3_value_int | ✅ Yes | | | sqlite3_value_int64 | ✅ Yes | | | sqlite3_value_double | ✅ Yes | | | sqlite3_value_text | ✅ Yes | | | sqlite3_value_text16 | ❌ No | | | sqlite3_value_blob | ✅ Yes | | | sqlite3_value_bytes | ✅ Yes | | | sqlite3_value_bytes16 | ❌ No | | | sqlite3_value_dup | ❌ No | | | sqlite3_value_free | ❌ No | | | sqlite3_value_nochange | ❌ No | | | sqlite3_value_frombind | ❌ No | | | sqlite3_value_subtype | ❌ No | | | sqlite3_value_pointer | ❌ No | | | sqlite3_value_encoding | ❌ No | | | sqlite3_value_numeric_type | ❌ No | | ### Error Handling | Interface | Status | Comment | |------------------------|---------|---------| | sqlite3_errcode | ✅ Yes | | | sqlite3_errmsg | ✅ Yes | | | sqlite3_errmsg16 | ❌ No | | | sqlite3_errstr | ✅ Yes | | | sqlite3_extended_errcode | ✅ Yes | | | sqlite3_extended_result_codes | ❌ No | | | sqlite3_error_offset | ❌ No | | | sqlite3_system_errno | ❌ No | | ### Changes and Row IDs | Interface | Status | Comment | |------------------------|---------|---------| | sqlite3_changes | ✅ Yes | | | sqlite3_changes64 | ✅ Yes | | | sqlite3_total_changes | ✅ Yes | | | sqlite3_total_changes64| ❌ No | | | sqlite3_last_insert_rowid | ✅ Yes | | | sqlite3_set_last_insert_rowid | ❌ No | | ### Memory Management | Interface | Status | Comment | |------------------------|---------|---------| | sqlite3_malloc | ✅ Yes | | | sqlite3_malloc64 | ✅ Yes | | | sqlite3_free | ✅ Yes | | | sqlite3_realloc | ❌ No | | | sqlite3_realloc64 | ❌ No | | | sqlite3_msize | ❌ No | | | sqlite3_memory_used | ❌ No | | | sqlite3_memory_highwater | ❌ No | | | sqlite3_soft_heap_limit64 | ❌ No | | | sqlite3_hard_heap_limit64 | ❌ No | | | sqlite3_release_memory | ❌ No | | ### Callback Functions | Interface | Status | Comment | |--------------------------|---------|---------| | sqlite3_busy_handler | ✅ Yes | | | sqlite3_busy_timeout | ✅ Yes | | | sqlite3_trace_v2 | ❌ No | Stub | | sqlite3_progress_handler | ❌ No | Stub | | sqlite3_set_authorizer | ❌ No | Stub | | sqlite3_commit_hook | ❌ No | | | sqlite3_rollback_hook | ❌ No | | | sqlite3_update_hook | ❌ No | | | sqlite3_preupdate_hook | ❌ No | | | sqlite3_unlock_notify | ❌ No | | | sqlite3_wal_hook | ❌ No | | ### User-Defined Functions | Interface | Status | Comment | |------------------------------|---------|---------| | sqlite3_create_function | ❌ No | | | sqlite3_create_function_v2 | ❌ No | Stub | | sqlite3_create_function16 | ❌ No | | | sqlite3_create_window_function | ❌ No | Stub | | sqlite3_aggregate_context | ❌ No | Stub | | sqlite3_user_data | ❌ No | Stub | | sqlite3_context_db_handle | ❌ No | Stub | | sqlite3_get_auxdata | ❌ No | | | sqlite3_set_auxdata | ❌ No | | | sqlite3_result_null | ❌ No | Stub | | sqlite3_result_int | ❌ No | | | sqlite3_result_int64 | ❌ No | Stub | | sqlite3_result_double | ❌ No | Stub | | sqlite3_result_text | ❌ No | Stub | | sqlite3_result_text16 | ❌ No | | | sqlite3_result_text64 | ❌ No | | | sqlite3_result_blob | ❌ No | Stub | | sqlite3_result_blob64 | ❌ No | | | sqlite3_result_value | ❌ No | | | sqlite3_result_pointer | ❌ No | | | sqlite3_result_zeroblob | ❌ No | | | sqlite3_result_zeroblob64 | ❌ No | | | sqlite3_result_error | ❌ No | Stub | | sqlite3_result_error16 | ❌ No | | | sqlite3_result_error_code | ❌ No | | | sqlite3_result_error_nomem | ❌ No | Stub | | sqlite3_result_error_toobig | ❌ No | Stub | | sqlite3_result_subtype | ❌ No | | ### Collation Functions | Interface | Status | Comment | |-----------------------------|---------|---------| | sqlite3_create_collation | ❌ No | | | sqlite3_create_collation_v2 | ❌ No | Stub | | sqlite3_create_collation16 | ❌ No | | | sqlite3_collation_needed | ❌ No | | | sqlite3_collation_needed16 | ❌ No | | | sqlite3_stricmp | ❌ No | Stub | | sqlite3_strnicmp | ❌ No | | ### Backup API | Interface | Status | Comment | |--------------------------|---------|---------| | sqlite3_backup_init | ❌ No | Stub | | sqlite3_backup_step | ❌ No | Stub | | sqlite3_backup_finish | ❌ No | Stub | | sqlite3_backup_remaining | ❌ No | Stub | | sqlite3_backup_pagecount | ❌ No | Stub | ### BLOB I/O | Interface | Status | Comment | |------------------------|---------|---------| | sqlite3_blob_open | ❌ No | Stub | | sqlite3_blob_close | ❌ No | Stub | | sqlite3_blob_bytes | ❌ No | Stub | | sqlite3_blob_read | ❌ No | Stub | | sqlite3_blob_write | ❌ No | Stub | | sqlite3_blob_reopen | ❌ No | | ### WAL Functions | Interface | Status | Comment | |----------------------------|---------|---------| | sqlite3_wal_checkpoint | ✅ Yes | | | sqlite3_wal_checkpoint_v2 | ✅ Yes | | | sqlite3_wal_autocheckpoint | ❌ No | | | sqlite3_wal_hook | ❌ No | | ### Utility Functions | Interface | Status | Comment | |------------------------|---------|---------| | sqlite3_libversion | ✅ Yes | Returns "3.42.0" | | sqlite3_libversion_number | ✅ Yes | Returns 3042000 | | sqlite3_sourceid | ❌ No | | | sqlite3_threadsafe | ✅ Yes | Returns 1 | | sqlite3_complete | ❌ No | Stub | | sqlite3_interrupt | ❌ No | Stub | | sqlite3_sleep | ❌ No | Stub | | sqlite3_randomness | ❌ No | | | sqlite3_get_table | ✅ Yes | | | sqlite3_free_table | ✅ Yes | | | sqlite3_mprintf | ❌ No | | | sqlite3_vmprintf | ❌ No | | | sqlite3_snprintf | ❌ No | | | sqlite3_vsnprintf | ❌ No | | | sqlite3_strglob | ❌ No | | | sqlite3_strlike | ❌ No | | ### Table Metadata | Interface | Status | Comment | |------------------------------|---------|---------| | sqlite3_table_column_metadata | ✅ Yes | | ### Virtual Tables | Interface | Status | Comment | |--------------------------|---------|---------| | sqlite3_create_module | ❌ No | | | sqlite3_create_module_v2 | ❌ No | | | sqlite3_drop_modules | ❌ No | | | sqlite3_declare_vtab | ❌ No | | | sqlite3_overload_function| ❌ No | | | sqlite3_vtab_config | ❌ No | | | sqlite3_vtab_on_conflict | ❌ No | | | sqlite3_vtab_nochange | ❌ No | | | sqlite3_vtab_collation | ❌ No | | | sqlite3_vtab_distinct | ❌ No | | | sqlite3_vtab_in | ❌ No | | | sqlite3_vtab_in_first | ❌ No | | | sqlite3_vtab_in_next | ❌ No | | | sqlite3_vtab_rhs_value | ❌ No | | ### Loadable Extensions | Interface | Status | Comment | |------------------------------|---------|---------| | sqlite3_load_extension | ❌ No | | | sqlite3_enable_load_extension| ❌ No | | | sqlite3_auto_extension | ❌ No | | | sqlite3_cancel_auto_extension| ❌ No | | | sqlite3_reset_auto_extension | ❌ No | | ### Serialization | Interface | Status | Comment | |------------------------|---------|---------| | sqlite3_serialize | ❌ No | Stub | | sqlite3_deserialize | ❌ No | Stub | ### Miscellaneous | Interface | Status | Comment | |--------------------------|---------|---------| | sqlite3_keyword_count | ❌ No | | | sqlite3_keyword_name | ❌ No | | | sqlite3_keyword_check | ❌ No | | | sqlite3_txn_state | ❌ No | | | sqlite3_file_control | ❌ No | | | sqlite3_status | ❌ No | | | sqlite3_status64 | ❌ No | | | sqlite3_test_control | ❌ No | Testing only | | sqlite3_log | ❌ No | | ### Turso-specific Extensions | Interface | Status | Comment | |--------------------------------|---------|---------| | libsql_wal_frame_count | ✅ Yes | Get WAL frame count | | libsql_wal_get_frame | ✅ Yes | Extract frame from WAL | | libsql_wal_insert_frame | ✅ Yes | Insert frame into WAL | | libsql_wal_disable_checkpoint | ✅ Yes | Disable checkpointing | ## SQLite VDBE opcodes | Opcode | Status | Comment | |----------------|--------|---------| | Add | ✅ Yes | | | AddImm | ✅ Yes | | | Affinity | ✅ Yes | | | AggFinal | ✅ Yes | | | AggStep | ✅ Yes | | | AggValue | ✅ Yes | | | And | ✅ Yes | | | AutoCommit | ✅ Yes | | | BitAnd | ✅ Yes | | | BitNot | ✅ Yes | | | BitOr | ✅ Yes | | | Blob | ✅ Yes | | | BeginSubrtn | ✅ Yes | | | Cast | ✅ Yes | | | Checkpoint | ✅ Yes | | | Clear | ❌ No | | | Close | ✅ Yes | | | CollSeq | ✅ Yes | | | Column | ✅ Yes | | | Compare | ✅ Yes | | | Concat | ✅ Yes | | | Copy | ✅ Yes | | | Count | ✅ Yes | | | CreateBTree | 🚧 Partial| no temp databases | | DecrJumpZero | ✅ Yes | | | Delete | ✅ Yes | | | Destroy | ✅ Yes | | | Divide | ✅ Yes | | | DropIndex | ✅ Yes | | | DropTable | ✅ Yes | | | DropTrigger | ✅ Yes | | | EndCoroutine | ✅ Yes | | | Eq | ✅ Yes | | | Expire | ❌ No | | | Explain | ❌ No | | | FkCheck | ✅ Yes | | | FkCounter | ✅ Yes | | | FkIfZero | ✅ Yes | | | Found | ✅ Yes | | | Filter | ✅ Yes | | | FilterAdd | ✅ Yes | | | Function | ✅ Yes | | | Ge | ✅ Yes | | | Gosub | ✅ Yes | | | Goto | ✅ Yes | | | Gt | ✅ Yes | | | Halt | ✅ Yes | | | HaltIfNull | ✅ Yes | | | IdxDelete | ✅ Yes | | | IdxGE | ✅ Yes | | | IdxInsert | ✅ Yes | | | IdxLE | ✅ Yes | | | IdxLT | ✅ Yes | | | IdxRowid | ✅ Yes | | | If | ✅ Yes | | | IfNeg | ✅ Yes | | | IfNot | ✅ Yes | | | IfPos | ✅ Yes | | | IfZero | ❌ No | | | IncrVacuum | ❌ No | | | Init | ✅ Yes | | | InitCoroutine | ✅ Yes | | | Insert | ✅ Yes | | | Int64 | ✅ Yes | | | Integer | ✅ Yes | | | IntegrityCk | ✅ Yes | | | IsNull | ✅ Yes | | | IsUnique | ❌ No | | | JournalMode | ✅ Yes | | | Jump | ✅ Yes | | | Last | ✅ Yes | | | Le | ✅ Yes | | | LoadAnalysis | ❌ No | | | Lt | ✅ Yes | | | MakeRecord | ✅ Yes | | | MaxPgcnt | ✅ Yes | | | MemMax | ✅ Yes | | | Move | ✅ Yes | | | Multiply | ✅ Yes | | | MustBeInt | ✅ Yes | | | Ne | ✅ Yes | | | NewRowid | ✅ Yes | | | Next | ✅ Yes | | | Noop | ✅ Yes | | | Not | ✅ Yes | | | NotExists | ✅ Yes | | | NotFound | ✅ Yes | | | NotNull | ✅ Yes | | | Null | ✅ Yes | | | NullRow | ✅ Yes | | | Once | ✅ Yes | | | OpenAutoindex | ✅ Yes | | | OpenDup | ✅ Yes | | | OpenEphemeral | ✅ Yes | | | OpenPseudo | ✅ Yes | | | OpenRead | ✅ Yes | | | OpenWrite | ✅ Yes | | | Or | ✅ Yes | | | Pagecount | 🚧 Partial| no temp databases | | Param | ❌ No | | | ParseSchema | ✅ Yes | | | Permutation | ❌ No | | | Prev | ✅ Yes | | | Program | ✅ Yes | | | ReadCookie | 🚧 Partial| no temp databases, only user_version supported | | Real | ✅ Yes | | | RealAffinity | ✅ Yes | | | Remainder | ✅ Yes | | | ResetCount | ❌ No | | | ResetSorter | 🚧 Partial| sorter cursors are not supported yet; only ephemeral tables are | | ResultRow | ✅ Yes | | | Return | ✅ Yes | | | Rewind | ✅ Yes | | | RowData | ✅ Yes | | | RowId | ✅ Yes | | | RowKey | ❌ No | | | RowSetAdd | ✅ Yes | | | RowSetRead | ✅ Yes | | | RowSetTest | ✅ Yes | | | Rowid | ✅ Yes | | | SCopy | ❌ No | | | Savepoint | ✅ No | | | Seek | ❌ No | | | SeekGe | ✅ Yes | | | SeekGt | ✅ Yes | | | SeekLe | ✅ Yes | | | SeekLt | ✅ Yes | | | SeekRowid | ✅ Yes | | | SeekEnd | ✅ Yes | | | Sequence | ✅ Yes | | | SequenceTest | ✅ Yes | | | SetCookie | ✅ Yes | | | ShiftLeft | ✅ Yes | | | ShiftRight | ✅ Yes | | | SoftNull | ✅ Yes | | | Sort | ❌ No | | | SorterCompare | ✅ Yes | | | SorterData | ✅ Yes | | | SorterInsert | ✅ Yes | | | SorterNext | ✅ Yes | | | SorterOpen | ✅ Yes | | | SorterSort | ✅ Yes | | | String | NotNeeded | SQLite uses String for sized strings and String8 for null-terminated. All our strings are sized | | String8 | ✅ Yes | | | Subtract | ✅ Yes | | | TableLock | ❌ No | | | Trace | ❌ No | | | Transaction | ✅ Yes | | | VBegin | ✅ Yes | | | VColumn | ✅ Yes | | | VCreate | ✅ Yes | | | VDestroy | ✅ Yes | | | VFilter | ✅ Yes | | | VNext | ✅ Yes | | | VOpen | ✅ Yes | | | VRename | ✅ Yes | | | VUpdate | ✅ Yes | | | Vacuum | ❌ No | | | Variable | ✅ Yes | | | Yield | ✅ Yes | | | ZeroOrNull | ✅ Yes | | ## [SQLite journaling modes](https://www.sqlite.org/pragma.html#pragma_journal_mode) We currently don't have plan to support the rollback journal mode as it locks the database file during writes. Therefore, all rollback-type modes (delete, truncate, persist, memory) are marked are `Not Needed` below. | Journal mode | Status | Comment | |--------------|------------|--------------------------------| | wal | ✅ Yes | | | wal2 | ❌ No | experimental feature in sqlite | | delete | Not Needed | | | truncate | Not Needed | | | persist | Not Needed | | | memory | Not Needed | | ## Extensions Turso has in-tree extensions. ### UUID UUID's in Turso are `blobs` by default. | Function | Status | Comment | |-----------------------|--------|---------------------------------------------------------------| | uuid4() | ✅ Yes | UUID version 4 | | uuid4_str() | ✅ Yes | UUID v4 string alias `gen_random_uuid()` for PG compatibility | | uuid7(X?) | ✅ Yes | UUID version 7 (optional parameter for seconds since epoch) | | uuid7_timestamp_ms(X) | ✅ Yes | Convert a UUID v7 to milliseconds since epoch | | uuid_str(X) | ✅ Yes | Convert a valid UUID to string | | uuid_blob(X) | ✅ Yes | Convert a valid UUID to blob | ### regexp The `regexp` extension is compatible with [sqlean-regexp](https://github.com/nalgeon/sqlean/blob/main/docs/regexp.md). | Function | Status | Comment | |------------------------------------------------|--------|---------| | regexp(pattern, source) | ✅ Yes | | | regexp_like(source, pattern) | ✅ Yes | | | regexp_substr(source, pattern) | ✅ Yes | | | regexp_capture(source, pattern[, n]) | ✅ Yes | | | regexp_replace(source, pattern, replacement) | ✅ Yes | | ### Vector The `vector` extension is compatible with libSQL native vector search. | Function | Status | Comment | |------------------------------------------------|--------|---------| | vector(x) | ✅ Yes | | | vector32(x) | ✅ Yes | | | vector64(x) | ✅ Yes | | | vector_extract(x) | ✅ Yes | | | vector_distance_cos(x, y) | ✅ Yes | | | vector_distance_l2(x, y) | ✅ Yes |Euclidean distance| | vector_concat(x, y) | ✅ Yes | | | vector_slice(x, start_index, end_index) | ✅ Yes | | ### Time The `time` extension is compatible with [sqlean-time](https://github.com/nalgeon/sqlean/blob/main/docs/time.md). | Function | Status | Comment | | ------------------------------------------------------------------- | ------ |---------| | time_now() | ✅ Yes | | | time_date(year, month, day[, hour, min, sec[, nsec[, offset_sec]]]) | ✅ Yes | | | time_get_year(t) | ✅ Yes | | | time_get_month(t) | ✅ Yes | | | time_get_day(t) | ✅ Yes | | | time_get_hour(t) | ✅ Yes | | | time_get_minute(t) | ✅ Yes | | | time_get_second(t) | ✅ Yes | | | time_get_nano(t) | ✅ Yes | | | time_get_weekday(t) | ✅ Yes | | | time_get_yearday(t) | ✅ Yes | | | time_get_isoyear(t) | ✅ Yes | | | time_get_isoweek(t) | ✅ Yes | | | time_get(t, field) | ✅ Yes | | | time_unix(sec[, nsec]) | ✅ Yes | | | time_milli(msec) | ✅ Yes | | | time_micro(usec) | ✅ Yes | | | time_nano(nsec) | ✅ Yes | | | time_to_unix(t) | ✅ Yes | | | time_to_milli(t) | ✅ Yes | | | time_to_micro(t) | ✅ Yes | | | time_to_nano(t) | ✅ Yes | | | time_after(t, u) | ✅ Yes | | | time_before(t, u) | ✅ Yes | | | time_compare(t, u) | ✅ Yes | | | time_equal(t, u) | ✅ Yes | | | time_add(t, d) | ✅ Yes | | | time_add_date(t, years[, months[, days]]) | ✅ Yes | | | time_sub(t, u) | ✅ Yes | | | time_since(t) | ✅ Yes | | | time_until(t) | ✅ Yes | | | time_trunc(t, field) | ✅ Yes | | | time_trunc(t, d) | ✅ Yes | | | time_round(t, d) | ✅ Yes | | | time_fmt_iso(t[, offset_sec]) | ✅ Yes | | | time_fmt_datetime(t[, offset_sec]) | ✅ Yes | | | time_fmt_date(t[, offset_sec]) | ✅ Yes | | | time_fmt_time(t[, offset_sec]) | ✅ Yes | | | time_parse(s) | ✅ Yes | | | dur_ns() | ✅ Yes | | | dur_us() | ✅ Yes | | | dur_ms() | ✅ Yes | | | dur_s() | ✅ Yes | | | dur_m() | ✅ Yes | | | dur_h() | ✅ Yes | | ### Full-Text Search (FTS) Turso implements FTS using Tantivy instead of SQLite's FTS3/FTS4/FTS5. | Feature | Status | Comment | |---------|--------|---------| | CREATE INDEX ... USING fts | ✅ Yes | Turso-specific syntax | | fts_match() | ✅ Yes | | | fts_score() | ✅ Yes | BM25 relevance scoring | | fts_highlight() | ✅ Yes | | | MATCH operator | ✅ Yes | | | SQLite FTS3/FTS4/FTS5 | ❌ No | Use Turso FTS instead | | snippet() | ❌ No | | ### CSV The CSV extension provides RFC 4180 compliant CSV file reading. | Feature | Status | Comment | |---------|--------|---------| | CSV virtual table | ✅ Yes | `CREATE VIRTUAL TABLE ... USING csv(...)` | ### Percentile Statistical aggregate functions. | Function | Status | Comment | |----------|--------|---------| | median(X) | ✅ Yes | | | percentile(Y,P) | ✅ Yes | | | percentile_cont(Y,P) | ✅ Yes | | | percentile_disc(Y,P) | ✅ Yes | | ### Table-Valued Functions | Function | Status | Comment | |----------|--------|---------| | generate_series(start, stop[, step]) | ✅ Yes | All parameters supported | | carray() | ❌ No | C-API specific | ### Internal Virtual Tables | Virtual Table | Status | Comment | |---------------|--------|---------| | sqlite_dbpage | 🚧 Partial | readonly, no attach support | ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Turso We'd love to have you contribute to Turso! This document is a quick helper to get you going. - [Contributing to Turso](#contributing-to-turso) - [Getting Started](#getting-started) - [Configuring `mold` Linker](#configuring-mold-linker) - [Running Tests On Linux](#running-tests-on-linux) - [Debugging bugs](#debugging-bugs) - [Query execution debugging](#query-execution-debugging) - [Stress testing with sanitizers](#stress-testing-with-sanitizers) - [Finding things to work on](#finding-things-to-work-on) - [Submitting your work](#submitting-your-work) - [Compatibility tests](#compatibility-tests) - [Prerequisites](#prerequisites) - [Running the tests](#running-the-tests) - [SQL Test Runner](#sql-test-runner) - [TPC-H](#tpc-h) - [Deterministic simulation tests](#deterministic-simulation-tests) - [Whopper](#whopper) - [Python Bindings](#python-bindings) - [Fault injection with unreliable libc](#fault-injection-with-unreliable-libc) - [Antithesis](#antithesis) - [Adding Third Party Dependencies](#adding-third-party-dependencies) ## Getting Started Turso is a rewrite of SQLite in Rust. If you are new to SQLite, the following articles and books are a good starting point: * [Architecture of SQLite](https://www.sqlite.org/arch.html) * Sibsankar Haldar. [SQLite Database System Design and Implementation (2nd Edition)](https://books.google.com/books/?id=yWzwCwAAQBAJ&redir_esc=y). 2016 * Jay Kreibich. [Using SQLite: Small. Fast. Reliable. Choose Any Three. 1st Edition](https://www.oreilly.com/library/view/using-sqlite/9781449394592/). 2010 If you are new to Rust, the following books are recommended reading: * Jim Blandy et al. [Programming Rust, 2nd Edition](https://www.oreilly.com/library/view/programming-rust-2nd/9781492052586/). 2021 * Steve Klabnik and Carol Nichols. [The Rust Programming Language](https://doc.rust-lang.org/book/#the-rust-programming-language). 2022 Examples of contributing * [How to contribute a SQL function implementation](docs/contributing/contributing_functions.md) * [Rickrolling Turso DB](https://avi.im/blag/2025/rickrolling-turso) To build and run `tursodb` CLI: ```shell cargo run --package turso_cli --bin tursodb database.db ``` Run tests: ```console cargo build -p turso_sqlite3 --features capi cargo test ``` ### Configuring `mold` Linker The `mold` linker can reduce your build time from a minute to just few seconds. First, install `mold`: ```console # Fedora/RHEL sudo dnf install mold # Ubuntu/Debian sudo apt install mold ``` Then configure Cargo to use mold by creating `.cargo/config.toml`: **For Linux:** ```toml [target.x86_64-unknown-linux-gnu] linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=mold"] ``` ### Running Tests On Linux > [!NOTE] > These steps have been tested on Ubuntu Noble 24.04.2 LTS Running tests on Linux and getting them pass requires a few additional steps 1. Install [SQLite](https://www.sqlite.org/index.html) headers ```console sudo apt install sqlite3 libsqlite3-dev ``` 2. Install Python3 dev files ```console sudo apt install python3.12 python3.12-dev ``` 3. Set env var for Maturin ```console export PYO3_PYTHON=$(which python3) ``` 4. Build Cargo ```console cargo build -p turso_sqlite3 --features capi ``` 5. Run tests ```console cargo test ``` Test coverage report: ``` cargo tarpaulin -o html ``` > [!NOTE] > Generation of coverage report requires [tarpaulin](https://github.com/xd009642/tarpaulin) binary to be installed. > You can install it with `cargo install cargo-tarpaulin` [//]: # (TODO remove the below tip when the bug is solved) > [!TIP] > If coverage fails with "Test failed during run" error and all of the tests passed it might be the result of tarpaulin [bug](https://github.com/xd009642/tarpaulin/issues/1642). You can temporarily set [dynamic libraries linking manually](https://doc.rust-lang.org/cargo/reference/environment-variables.html#dynamic-library-paths) as a workaround, e.g. for linux `LD_LIBRARY_PATH="$(rustc --print=target-libdir)" cargo tarpaulin -o html`. Run benchmarks: ```console cargo bench --profile bench-profile --bench benchmark ``` Run benchmarks and generate flamegraphs: ```console echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid cargo bench --profile bench-profile --bench benchmark -- --profile-time=5 ``` ## Debugging bugs ### Query execution debugging Turso aims towards SQLite compatibility. If you find a query that has different behavior than SQLite, the first step is to check what the generated bytecode looks like. To do that, first run the `EXPLAIN` command in `sqlite3` shell: ``` sqlite> EXPLAIN SELECT first_name FROM users; addr opcode p1 p2 p3 p4 p5 comment ---- ------------- ---- ---- ---- ------------- -- ------------- 0 Init 0 7 0 0 Start at 7 1 OpenRead 0 2 0 2 0 root=2 iDb=0; users 2 Rewind 0 6 0 0 3 Column 0 1 1 0 r[1]= cursor 0 column 1 4 ResultRow 1 1 0 0 output=r[1] 5 Next 0 3 0 1 6 Halt 0 0 0 0 7 Transaction 0 0 1 0 1 usesStmtJournal=0 8 Goto 0 1 0 0 ``` and then run the same command in Turso's shell. If the bytecode is different, that's the bug -- work towards fixing code generation. If the bytecode is the same, but query results are different, then the bug is somewhere in the virtual machine interpreter or storage layer. ### Stress testing with sanitizers If you suspect a multi-threading issue, you can run the stress test with ThreadSanitizer enabled as follows: ```console rustup toolchain install nightly rustup override set nightly cargo run -Zbuild-std --target x86_64-unknown-linux-gnu -p turso_stress -- --vfs syscall --nr-threads 4 --nr-iterations 1000 ``` ## Finding things to work on The issue tracker has issues tagged with [good first issue](https://github.com/tursodatabase/limbo/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22), which are considered to be things to work on to get going. If you're interested in working on one of them, comment on the issue tracker, and we're happy to help you get going. ## Submitting your work Fork the repository and open a pull request to submit your work. The CI checks for formatting, Clippy warnings, and test failures so remember to run the following before submitting your pull request: * `cargo fmt` and `cargo clippy --workspace --all-features --all-targets -- --deny=warnings` to keep the code formatting in check. * `make test` to run the test suite. **Keep your pull requests focused and as small as possible, but not smaller.** IOW, when preparing a pull request, ensure it focuses on a single thing and that your commits align with that. For example, a good pull request might fix a specific bug or a group of related bugs. Or a good pull request might add a new feature and test for it. Conversely, a bad pull request might fix a bug, add a new feature, and refactor some code. **The commits in your pull request tell the story of your change.** Break your pull request into multiple commits when needed to make it easier to review and ensure that future developers can also understand the change as they are in the middle of a `git bisect` run to debug a nasty bug. A developer should be able to reconstruct the intent of your change and how you got to the end-result by reading the commits. To keep a clean commit history, make sure the commits are _atomic_: * **Keep commits as small as possible**. The smaller the commit, the easier it is to review, but also easier `git revert` when things go bad. * **Don't mix logic and cleanups in same commit**. If you need to refactor the code, do it in a commit of its own. Mixing refactoring with logic changes makes it very hard to review a commit. * **Don't mix logic and formatting changes in same commit**. Resist the urge to fix random formatting issues in the same commit as your logic changes, because it only makes it harder to review the commit. * **Write a good commit message**. You know your commit is atomic when it's easy to write a short commit message that describes the intent of the change. To produce pull requests like this, you should learn how to use Git's interactive rebase (`git rebase -i`). For a longer discussion on good commits, see Al Tenhundfeld's [What makes a good git commit](https://www.simplethread.com/what-makes-a-good-git-commit/), for example. ## Compatibility tests The `testing/test.all` is a starting point for adding functional tests using a similar syntax to SQLite. The purpose of these tests is to verify behavior matches with SQLite and Turso. ### Prerequisites 1. [Cargo-c](https://github.com/lu-zero/cargo-c) is needed for building C-ABI compatible library. You can get it via: ```console cargo install cargo-c ``` 2. [SQLite](https://www.sqlite.org/index.html) is needed for compatibility checking. You can install it using `brew` on macOS/Linux: ```console brew install sqlite ``` Or using `choco` on Windows: ```console choco install sqlite ``` ### Running the tests To run the test suite with Turso, simply run: ``` make test ``` To run the test suite with SQLite, type: ``` SQLITE_EXEC=sqlite3 SQLITE_FLAGS="" make test ``` When working on a new feature, please consider adding a test case for it. ## SQL Test Runner The `test-runner` crate provides a dedicated test runner with a custom DSL for writing SQL tests. Tests should be added to `testing/sqltests/tests/` using the `.sqltest` format. To run tests: ```console make -C testing/sqltests run ``` For full documentation on the DSL syntax and CLI usage, see the [test-runner docs](testing/sqltests/docs/). ## TPC-H [TPC-H](https://www.tpc.org/tpch/) is a standard benchmark for testing database performance. To try out Turso's performance against a TPC-H compatible workload, you can generate or download a TPC-H compatible SQLite database e.g. [here](https://github.com/lovasoa/TPCH-sqlite). ## Deterministic simulation tests The `simulator` directory contains a deterministic simulator for testing. What this means is that the behavior of a test run is deterministic based on the seed value. If the simulator catches a bug, you can always reproduce the exact same sequence of events by passing the same seed. The simulator also performs fault injection to discover interesting bugs. ### Whopper Whopper is a DST that, unlike `simulator`, performs concurrent query execution. To run Whopper for your local changes, run: ```console ./testing/concurrent-simulator/bin/run ``` The output of the simulation run looks as follows: ``` mode = fast seed = 11621338508193870992 . I/U/D/C . 22/17/15/0 . 41/34/20/3 | 62/43/27/4 | 88/55/30/5 ╱|╲ 97/58/30/6 ╱╲|╱╲ 108/62/30/7 ╱╲╱|╲╱╲ 115/67/32/7 ╱╲╱╲|╱╲╱╲ 121/74/35/7 ╱╲╱╲╱|╲╱╲╱╲ 125/80/38/7 ╱╲╱╲╱╲|╱╲╱╲╱╲ 141/94/43/8 real 0m1.250s user 0m0.843s sys 0m0.043s ``` The simulator prints ten progress indication lines, regardless of how long a run takes. The progress indicator line shows the following stats: * `I` -- the number of `INSERT` statements executed * `U` -- the number of `UPDATE` statements executed * `D` -- the number of `DELETE` statements executed * `C` -- the number of `PRAGMA integrity_check` statements executed This will do a short sanity check run in using the `fast` mode. If you need to reproduce a run, just defined the `SEED` environment variable as follows: ```console SEED=1234 ./testing/concurrent-simulator/bin/run ``` You can also run Whopper in exploration mode to find more serious bugs: ```console ./testing/concurrent-simulator/bin/explore ``` Note that exploration uses the `chaos` mode so if you need to reproduce a run, use: ```console SEED=1234 ./testing/concurrent-simulator/bin/run --mode chaos ``` Both `explore` and `run` accept the `--enable-checksums` and `--enable-encryption` flags for per page checksums and encryption respectively. ## Python Bindings Turso provides Python bindings built on top of the [PyO3](https://pyo3.rs) project. To compile the Python bindings locally, you first need to create and activate a Python virtual environment (for example, with Python `3.12`): ```bash python3.12 -m venv venv source venv/bin/activate ``` Then, install [Maturin](https://pypi.org/project/maturin/): ```bash pip install maturin ``` Once Maturin is installed, you can build the crate and install it as a Python module directly into the current virtual environment by running: ```bash cd bindings/python && maturin develop ``` ## Fault injection with unreliable libc First, build the unreliable libc: ``` cd testing/unreliable-libc make ``` The run the stress testing tool with fault injection enabled: ``` RUST_BACKTRACE=1 LD_PRELOAD=./testing/unreliable-libc/unreliable-libc.so cargo run -p turso_stress -- --nr-iterations 10000 ``` ## Antithesis Antithesis is a testing platform for finding bugs with reproducibility. In Turso, we use Antithesis in addition to our own deterministic simulation testing (DST) tool for the following: - Discovering bugs that the DST did not catch (and improve the DST) - Discovering bugs that the DST does not cover (for example, non-simulated I/O) If you have an Antithesis account, you first need to configure some environment variables: ```bash export ANTITHESIS_USER= export ANTITHESIS_TENANT= export ANTITHESIS_PASSWD= export ANTITHESIS_DOCKER_HOST= export ANTITHESIS_DOCKER_REPO= export ANTITHESIS_EMAIL= ``` You can then publish a new Antithesis workflow with: ```bash scripts/antithesis/publish-workload.sh ``` And launch an Antithesis test run with: ```bash scripts/antithesis/launch.sh ``` ## Adding Third Party Dependencies When you want to add third party dependencies, please follow these steps: 1. Add Licenses: Place the appropriate licenses for the third-party dependencies under the licenses directory. Ensure that each license is in a separate file and named appropriately. 2. Update NOTICE.md: Specify the licenses for the third-party dependencies in the NOTICE.md file. Include the name of the dependency, the license file path, and the homepage of the dependency. By following these steps, you ensure that all third-party dependencies are properly documented and their licenses are included in the project. ================================================ FILE: Cargo.toml ================================================ # Copyright 2023-2026 the Turso authors. All rights reserved. MIT license. [workspace] resolver = "2" members = [ "bindings/java", "bindings/javascript", "bindings/javascript/sync", "bindings/python", "bindings/dotnet", "bindings/rust", "cli", "core", "extensions/completion", "extensions/core", "extensions/crypto", "extensions/csv", "extensions/ipaddr", "extensions/percentile", "extensions/regexp", "extensions/tests", "extensions/fuzzy", "macros", "testing/simulator", "sqlite3", "testing/stress", "testing/sqlite_test_ext", "tests", "parser", "sync/engine", "sync/sdk-kit", "sql_generation", "testing/concurrent-simulator", "perf/throughput/turso", "perf/throughput/rusqlite", "perf/encryption", "tools/dbhash", "sdk-kit", "sdk-kit-macros", "testing/differential-oracle/sql_gen_prop", "testing/differential-oracle/sql_gen", "testing/differential-oracle/fuzzer", "testing/sqltests" ] exclude = ["perf/latency/limbo"] [workspace.package] version = "0.6.0-pre.4" authors = ["the Turso authors"] edition = "2021" license = "MIT" repository = "https://github.com/tursodatabase/turso" [workspace.dependencies] turso = { path = "bindings/rust", version = "0.6.0-pre.4" } turso_node = { path = "bindings/javascript", version = "0.6.0-pre.4" } turso_sdk_kit = { path = "sdk-kit", version = "0.6.0-pre.4" } turso_sdk_kit_macros = { path = "sdk-kit-macros", version = "0.6.0-pre.4" } turso_sync_sdk_kit = { path = "sync/sdk-kit", version = "0.6.0-pre.4" } limbo_completion = { path = "extensions/completion", version = "0.6.0-pre.4" } turso_core = { path = "core", version = "0.6.0-pre.4" } turso_sync_engine = { path = "sync/engine", version = "0.6.0-pre.4" } turso_ext = { path = "extensions/core", version = "0.6.0-pre.4" } turso_macros = { path = "macros", version = "0.6.0-pre.4" } turso_parser = { path = "parser", version = "0.6.0-pre.4" } sql_generation = { path = "sql_generation" } turso-dbhash = { path = "tools/dbhash" } strum = { version = "0.26", features = ["derive"] } strum_macros = "0.26" serde = "1.0" serde_json = "1.0" anyhow = "1.0.98" mimalloc = { version = "0.1.47", default-features = false } rusqlite = { version = "0.37.0", features = ["bundled"] } itertools = "0.14.0" rand = "0.9.2" rand_chacha = "0.9.0" tracing = "0.1.41" schemars = "1.0.4" garde = "0.22" parking_lot = "0.12.4" tokio = { version = "1.0", default-features = false } tracing-subscriber = "0.3.20" futures = "0.3" clap = "4.5.47" thiserror = "2.0.16" tempfile = "3.20.0" indexmap = "2.11.1" indicatif = "0.17" miette = "7.6.0" bitflags = "2.9.4" fallible-iterator = "0.3.0" criterion = "0.5.0" codspeed-criterion-compat = "4.2.1" divan = { package = "codspeed-divan-compat", version = "4.2.1" } chrono = { version = "0.4.42", default-features = false } hex = "0.4" antithesis_sdk = { version = "0.2", default-features = false } cfg-if = "1.0.0" tracing-appender = "0.2.3" env_logger = { version = "0.11.6", default-features = false } regex = "1.11.1" regex-syntax = { version = "0.8.5", default-features = false } similar = { version = "2.7.0" } similar-asserts = { version = "1.7.0" } bitmaps = { version = "3.2.1", default-features = false } console-subscriber = { version = "0.4.1" } either = { version = "1.15" } # Multi threading testing loom = { version = "0.7" } shuttle = { version = "0.8.1" } [profile.dev.package.similar] opt-level = 3 [profile.release] debug = "line-tables-only" codegen-units = 4 panic = "abort" lto = "thin" # Official release profile - maximum optimization for published binaries # Use for CLI releases, PyPI, npm, Maven, NuGet, etc. [profile.release-official] inherits = "release" codegen-units = 1 lto = true # override settings for sdk-kit release profiles in order to reduce size of produced binaries # otherwise, some platforms will have enormous libs (150MB+) [profile.lib-release] inherits = "release" debug = false codegen-units = 16 lto = false [profile.fuzzing] inherits = "dev" opt-level = 3 debug = "line-tables-only" panic = "abort" [profile.antithesis] inherits = "release" debug = true codegen-units = 1 panic = "abort" lto = true [profile.bench-profile] inherits = "release" debug = true lto = false # LTO hides function boundaries codegen-units = 16 # Less cross-unit inlining [profile.dist] inherits = "release-official" [workspace.lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(loom)', 'cfg(shuttle)', 'cfg(antithesis)'] } [workspace.lints.clippy] or_fun_call = "deny" clear_with_drain = "deny" collection_is_never_read = "deny" naive_bytecount = "deny" stable_sort_primitive = "deny" large_stack_frames = "deny" large_types_passed_by_value = "deny" redundant_clone = "deny" assigning_clones = "deny" ================================================ FILE: Dockerfile.antithesis ================================================ FROM lukemathwalker/cargo-chef:0.1.72-rust-1.88.0-slim-bullseye AS chef RUN apt update \ && apt install -y git libssl-dev pkg-config\ && apt clean \ && rm -rf /var/lib/apt/lists/* WORKDIR /app # # Cache dependencies # FROM chef AS planner COPY ./Cargo.lock ./Cargo.lock COPY ./Cargo.toml ./Cargo.toml COPY ./bindings/dotnet ./bindings/dotnet/ COPY ./bindings/java ./bindings/java/ COPY ./bindings/javascript ./bindings/javascript/ COPY ./bindings/python ./bindings/python/ COPY ./bindings/rust ./bindings/rust/ COPY ./cli ./cli/ COPY ./core ./core/ COPY ./extensions ./extensions/ COPY ./macros ./macros/ COPY ./packages ./packages/ COPY ./parser ./parser/ COPY ./perf/encryption ./perf/encryption COPY ./perf/throughput/rusqlite ./perf/throughput/rusqlite/ COPY ./perf/throughput/turso ./perf/throughput/turso/ COPY ./testing/simulator ./testing/simulator/ COPY ./sql_generation ./sql_generation COPY ./sqlite3 ./sqlite3/ COPY ./testing/stress ./testing/stress/ COPY ./sync ./sync/ COPY ./testing/sqlite_test_ext ./testing/sqlite_test_ext/ COPY ./testing/unreliable-libc ./testing/unreliable-libc/ COPY ./tests ./tests/ COPY ./testing/sqltests ./testing/sqltests/ COPY ./testing/concurrent-simulator ./testing/concurrent-simulator/ COPY ./testing/differential-oracle ./testing/differential-oracle/ COPY ./sdk-kit ./sdk-kit/ COPY ./sdk-kit-macros ./sdk-kit-macros/ COPY ./tools ./tools/ RUN cargo chef prepare --bin turso_stress --recipe-path recipe.json # # Build the project. # FROM chef AS builder ARG antithesis=true RUN apt-get update && apt-get install -y pip && rm -rf /var/lib/apt/lists/* RUN pip install maturin # Antithesis instrumentation library ADD https://antithesis.com/assets/instrumentation/libvoidstar.so /opt/antithesis/libvoidstar.so COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --bin turso_stress --release --recipe-path recipe.json COPY --from=planner /app/Cargo.toml /app/Cargo.lock ./ COPY --from=planner /app/bindings/dotnet ./bindings/dotnet/ COPY --from=planner /app/bindings/java ./bindings/java/ COPY --from=planner /app/bindings/javascript ./bindings/javascript/ COPY --from=planner /app/bindings/python ./bindings/python/ COPY --from=planner /app/bindings/rust ./bindings/rust/ COPY --from=planner /app/cli ./cli/ COPY --from=planner /app/core ./core/ COPY --from=planner /app/extensions ./extensions/ COPY --from=planner /app/macros ./macros/ COPY --from=planner /app/packages ./packages/ COPY --from=planner /app/parser ./parser/ COPY --from=planner /app/perf/encryption ./perf/encryption COPY --from=planner /app/perf/throughput/rusqlite ./perf/throughput/rusqlite/ COPY --from=planner /app/perf/throughput/turso ./perf/throughput/turso/ COPY --from=planner /app/testing/simulator ./testing/simulator/ COPY --from=planner /app/sql_generation ./sql_generation COPY --from=planner /app/sqlite3 ./sqlite3/ COPY --from=planner /app/testing/stress ./testing/stress/ COPY --from=planner /app/sync ./sync/ COPY --from=planner /app/testing/sqlite_test_ext ./testing/sqlite_test_ext/ COPY --from=planner /app/testing/unreliable-libc ./testing/unreliable-libc/ COPY --from=planner /app/tests ./tests/ COPY --from=planner /app/testing/sqltests ./testing/sqltests/ COPY --from=planner /app/testing/concurrent-simulator ./testing/concurrent-simulator/ COPY --from=planner /app/testing/differential-oracle ./testing/differential-oracle COPY --from=planner /app/sdk-kit ./sdk-kit/ COPY --from=planner /app/sdk-kit-macros ./sdk-kit-macros/ COPY --from=planner /app/tools ./tools/ # Remove cdylib and staticlib from this line: `crate-type = ["lib", "cdylib", "staticlib"]` # This is because somehow we lose llvm sanitizer coverage if the crate is not just a lib. RUN for f in sdk-kit/Cargo.toml sync/sdk-kit/Cargo.toml; do \ grep -qF 'crate-type = ["lib", "cdylib", "staticlib"]' "$f" \ || { echo "ERROR: expected crate-type not found in $f"; exit 1; }; \ done \ && sed -i 's/crate-type = \["lib", "cdylib", "staticlib"\]/crate-type = ["lib"]/' \ sdk-kit/Cargo.toml sync/sdk-kit/Cargo.toml RUN if [ "$antithesis" = "true" ]; then \ cp /opt/antithesis/libvoidstar.so /usr/lib/libvoidstar.so && \ export RUSTFLAGS="--cfg=tokio_unstable --cfg=antithesis -Ccodegen-units=1 -Cpasses=sancov-module -Cllvm-args=-sanitizer-coverage-level=3 -Cllvm-args=-sanitizer-coverage-trace-pc-guard -Clink-args=-Wl,--build-id -L/usr/lib/ -lvoidstar" && \ cargo build --bin turso_stress --profile antithesis; \ else \ cargo build --bin turso_stress --release; \ fi WORKDIR /app/bindings/python RUN maturin build WORKDIR /app/testing/unreliable-libc RUN make # # The final image. # FROM debian:bullseye-slim AS runtime RUN apt-get update && apt-get install -y bash curl xz-utils python3 procps sqlite3 bc binutils pip rust-gdb && rm -rf /var/lib/apt/lists/* RUN pip install antithesis WORKDIR /app EXPOSE 8080 COPY --from=builder /usr/lib/libvoidstar.so* /usr/lib/ COPY --from=builder /app/testing/unreliable-libc/unreliable-libc.so /usr/lib/ COPY --from=builder /app/target/antithesis/turso_stress /bin/turso_stress COPY --from=builder /app/target/antithesis/turso_stress /symbols COPY testing/stress/docker-entrypoint.sh /bin RUN chmod +x /bin/docker-entrypoint.sh COPY --from=builder /app/target/wheels/* /tmp RUN pip install /tmp/*.whl WORKDIR /app COPY ./testing/antithesis/bank-test/*.py /opt/antithesis/test/v1/bank-test/ COPY ./testing/antithesis/stress-composer/*.py /opt/antithesis/test/v1/stress-composer/ COPY ./testing/antithesis/stress /opt/antithesis/test/v1/stress COPY ./testing/antithesis/stress-io_uring /opt/antithesis/test/v1/stress-io_uring COPY ./testing/antithesis/stress-mvcc /opt/antithesis/test/v1/stress-mvcc COPY ./testing/antithesis/stress-io_uring-mvcc /opt/antithesis/test/v1/stress-io_uring-mvcc COPY ./testing/antithesis/stress-unreliable /opt/antithesis/test/v1/stress-unreliable RUN chmod 777 -R /opt/antithesis/test/v1 RUN mkdir /opt/antithesis/catalog RUN ln -s /opt/antithesis/test/v1/bank-test/*.py /opt/antithesis/catalog ENV RUST_BACKTRACE=1 ENTRYPOINT ["/bin/docker-entrypoint.sh"] ================================================ FILE: Dockerfile.cli ================================================ FROM rust:1.88.0 as builder WORKDIR /app # Copy the actual source code COPY . . # Build the CLI binary RUN cargo build --release --package turso_cli # Runtime stage FROM rust:1.88.0-slim WORKDIR /app # Copy the built binary COPY --from=builder /app/target/release/tursodb /usr/local/bin/ # Set the entrypoint ENTRYPOINT ["tursodb"] ================================================ FILE: LICENSE.md ================================================ MIT License Copyright 2024 the Turso authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ MINIMUM_RUST_VERSION := 1.73.0 CURRENT_RUST_VERSION := $(shell rustc -V | sed -E 's/rustc ([0-9]+\.[0-9]+\.[0-9]+).*/\1/') CURRENT_RUST_TARGET := $(shell rustc -vV | grep host | cut -d ' ' -f 2) RUSTUP := $(shell command -v rustup 2> /dev/null) UNAME_S := $(shell uname -s) MINIMUM_TCL_VERSION := 8.6 # Executable used to execute the compatibility tests. SQLITE_EXEC ?= scripts/limbo-sqlite3 RUST_LOG := off all: check-rust-version build .PHONY: all install-sqlite: ./scripts/install-sqlite3.sh .PHONY: install-sqlite check-rust-version: @echo "Checking Rust version..." @if [ "$(shell printf '%s\n' "$(MINIMUM_RUST_VERSION)" "$(CURRENT_RUST_VERSION)" | sort -V | head -n1)" = "$(CURRENT_RUST_VERSION)" ]; then \ echo "Rust version greater than $(MINIMUM_RUST_VERSION) is required. Current version is $(CURRENT_RUST_VERSION)."; \ if [ -n "$(RUSTUP)" ]; then \ echo "Updating Rust..."; \ rustup update stable; \ else \ echo "Please update Rust manually to a version greater than $(MINIMUM_RUST_VERSION)."; \ exit 1; \ fi; \ else \ echo "Rust version $(CURRENT_RUST_VERSION) is acceptable."; \ fi .PHONY: check-rust-version check-tcl-version: @printf '%s\n' \ 'set need "$(MINIMUM_TCL_VERSION)"' \ 'set have [info patchlevel]' \ 'if {[package vcompare $$have $$need] < 0} {' \ ' puts stderr "tclsh $$have found — need $$need+"' \ ' exit 1' \ '}' \ | tclsh .PHONY: check-tcl-version build: check-rust-version cargo build .PHONY: build turso-c: cargo cbuild .PHONY: turso-c uv-sync: uv sync --all-packages .PHONE: uv-sync uv-sync-test: uv sync --all-extras --dev --package turso_test .PHONE: uv-sync test: build uv-sync-test test-compat test-sqlite3 test-shell test-memory test-write test-update test-constraint test-collate test-extensions test-runner test-runner-js test-runner-cli .PHONY: test test-runner: @make -C testing/sqltests run .PHONY: test-runner test-runner-js: @make -C testing/sqltests run-js .PHONY: test-runner-js test-runner-cli: @make -C testing/sqltests run-cli .PHONY: test-runner-cli test-extensions: build uv-sync-test RUST_LOG=$(RUST_LOG) uv run --project limbo_test test-extensions .PHONY: test-extensions test-shell: build uv-sync-test RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-shell .PHONY: test-shell test-compat: check-tcl-version RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) ./testing/system/all.test test-single: check-tcl-version @if [ -z "$(TEST)" ]; then \ echo "Usage: make test-single TEST=path/to/test.test"; \ exit 1; \ fi RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) ./testing/system/$(TEST) .PHONY: test-single .PHONY: test-compat reset-db: ./scripts/clone_test_db.sh .PHONY: reset-db test-fuzz: RUST_LOG=$(RUST_LOG) cargo test -p core_tester --release -- fuzz RUST_LOG=$(RUST_LOG) cargo run -p differential-fuzzer --bin custom_types_fuzzer -- --seed $(or $(SEED), $$(date +%s)) -n $(or $(N),200) -t $(or $(TABLES),2) .PHONY: test-fuzz test-sqlite3: reset-db cargo test -p turso_sqlite3 --test compat -- --test-threads=1 ./scripts/clone_test_db.sh cargo test -p turso_sqlite3 --test compat --features sqlite3 -- --test-threads=1 .PHONY: test-sqlite3 test-memory: build uv-sync-test RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-memory .PHONY: test-memory test-write: build uv-sync-test @if [ "$(SQLITE_EXEC)" != "scripts/limbo-sqlite3" ]; then \ RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-write; \ else \ echo "Skipping test-write: SQLITE_EXEC does not have indexes scripts/limbo-sqlite3"; \ fi .PHONY: test-write test-update: build uv-sync-test @if [ "$(SQLITE_EXEC)" != "scripts/limbo-sqlite3" ]; then \ RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-update; \ else \ echo "Skipping test-update: SQLITE_EXEC does not have indexes scripts/limbo-sqlite3"; \ fi .PHONY: test-update test-collate: build uv-sync-test @if [ "$(SQLITE_EXEC)" != "scripts/limbo-sqlite3" ]; then \ RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-collate; \ else \ echo "Skipping test-collate: SQLITE_EXEC does not have indexes scripts/limbo-sqlite3"; \ fi .PHONY: test-collate test-constraint: build uv-sync-test @if [ "$(SQLITE_EXEC)" != "scripts/limbo-sqlite3" ]; then \ RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-constraint; \ else \ echo "Skipping test-constraint: SQLITE_EXEC does not have indexes scripts/limbo-sqlite3"; \ fi .PHONY: test-constraint bench-vfs: uv-sync-test build-release RUST_LOG=$(RUST_LOG) uv run --project limbo_test bench-vfs "$(SQL)" "$(N)" bench-sqlite: uv-sync-test build-release RUST_LOG=$(RUST_LOG) uv run --project limbo_test bench-sqlite "$(VFS)" "$(SQL)" "$(N)" clickbench: ./perf/clickbench/benchmark.sh .PHONY: clickbench build-release: check-rust-version cargo build --bin tursodb --release --features=tracing_release bench-exclude-tpc-h: @benchmarks=$$(cargo bench --bench 2>&1 | grep -A 1000 '^Available bench targets:' | grep -v '^Available bench targets:' | grep -v '^ *$$' | grep -v 'tpc_h_benchmark' | xargs -I {} printf -- "--bench %s " {}); \ if [ -z "$$benchmarks" ]; then \ echo "No benchmarks found (excluding tpc_h_benchmark)."; \ exit 1; \ else \ cargo bench $$benchmarks --features bench; \ fi .PHONY: bench-exclude-tpc-h codspeed-build-bench-exclude-tpc-h: @benchmarks=$$(cargo bench --bench 2>&1 | grep -A 1000 '^Available bench targets:' | grep -v '^Available bench targets:' | grep -v '^ *$$' | grep -v 'tpc_h_benchmark' | xargs -I {} printf -- "--bench %s " {}); \ if [ -z "$$benchmarks" ]; then \ echo "No benchmarks found (excluding tpc_h_benchmark)."; \ exit 1; \ else \ cargo codspeed build $$benchmarks --features codspeed; \ fi .PHONY: codspeed-build-bench-exclude-tpc-h docker-cli-build: docker build -f Dockerfile.cli -t turso-cli . docker-cli-run: docker run -it -v ./:/app turso-cli merge-pr: ifndef PR $(error PR is required. Usage: make merge-pr PR=123) endif @echo "Setting up environment for PR merge..." @if [ -z "$(GITHUB_REPOSITORY)" ]; then \ REPO=$$(git remote get-url origin | sed -E 's|.*github\.com[:/]([^/]+/[^/]+?)(\.git)?$$|\1|'); \ if [ -z "$$REPO" ]; then \ echo "Error: Could not detect repository from git remote"; \ exit 1; \ fi; \ export GITHUB_REPOSITORY="$$REPO"; \ else \ export GITHUB_REPOSITORY="$(GITHUB_REPOSITORY)"; \ fi; \ echo "Repository: $$REPO"; \ AUTH=$$(gh auth status); \ if [ -z "$$AUTH" ]; then \ echo "auth: $$AUTH"; \ echo "GitHub CLI not authenticated. Starting login process..."; \ gh auth login --scopes repo,workflow; \ else \ if ! echo "$$AUTH" | grep -q "workflow"; then \ echo "Warning: 'workflow' scope not detected. You may need to re-authenticate if merging PRs with workflow changes."; \ echo "Run: gh auth refresh -s repo,workflow"; \ fi; \ fi; \ if [ "$(LOCAL)" = "1" ]; then \ echo "merging PR #$(PR) locally"; \ uv run scripts/merge-pr.py $(PR) --local; \ else \ echo "merging PR #$(PR) on GitHub"; \ uv run scripts/merge-pr.py $(PR); \ fi .PHONY: merge-pr sim-schema: mkdir -p simulator/configs/custom cargo run -p limbo_sim -- print-schema > simulator/configs/custom/profile-schema.json test-shuttle: RUSTFLAGS='--cfg tokio_unstable --cfg shuttle' cargo nextest run --profile shuttle --package turso_core test-loom: RUSTFLAGS='--cfg tokio_unstable --cfg loom' cargo nextest run --profile loom --package turso_core ================================================ FILE: NOTICE.md ================================================ Limbo ======= Please visit our GitHub for more information: * https://github.com/tursodatabase/turso Dependencies ============ This product depends on Error Prone, distributed by the Error Prone project: * License: licenses/bindings/java/assertj-license.md (Apache License v2.0) * Homepage: https://github.com/google/error-prone This product depends on AssertJ, distributed by the AssertJ authors: * License: licenses/bindings/java/errorprone-license.md (Apache License v2.0) * Homepage: https://joel-costigliola.github.io/assertj/ This product depends on logback, distributed by the logback authors: * License: licenses/bindings/java/logback-license.md (Apache License v2.0) * Homepage: https://github.com/qos-ch/logback?tab=License-1-ov-file This product depends on spotless, distributed by the diffplug authors: * License: licenses/bindings/java/spotless-license.md (Apache License v2.0) * Homepage: https://github.com/diffplug/spotless This project depends on ipnetwork, distributed by the ipnetwork project: * License: licenses/extensions/ipnetwork-apache-license.md (Apache License v2.0) * License: licenses/extensions/ipnetwork-mit-license.md (MIT License) * Homepage: https://github.com/achanda/ipnetwork This project depends on libm, distributed by the rust-lang project: * License: licenses/extensions/libm-apache-license.md (Apache License v2.0) * License: licenses/extensions/libm-mit-license.md (MIT License) * Homepage: https://github.com/rust-lang/libm This project depends on pastey, distributed by the pastey authors: * License: licenses/core/pastey-apache-license.md (Apache License v2.0) * License: licenses/core/pastey-mit-license.md (MIT License) * Homepage: https://github.com/AS1100K/pastey This project depends on windows-sys, distributed by the Microsoft: * License: licenses/core/windows-apache.license.md (Apache License v2.0) * License: licenses/core/windows-mit-license.md (MIT License) * Homepage: https://github.com/microsoft/windows-rs This project depends on SQLAlchemy, distributed by the SQLAlchemy authors: * License: licenses/bindings/python/sqlalchemy-mit-license.md (MIT License) * Homepage: https://github.com/sqlalchemy/sqlalchemy ================================================ FILE: PERF.md ================================================ # Performance Testing ## Mobibench 1. Clone the source repository of Mobibench fork for Turso: ```console git clone git@github.com:penberg/Mobibench.git ``` 2. Build Mobibench: ```console cd Mobibench/shell LIBS="../../target/release/libturso_sqlite3.a -lm" make mv mobibench mobibench-turso ``` 3. Run Mobibench: (easiest way is to `cd` into `target/release`) ```console # with strace, from target/release strace -f -c ../../Mobibench/shell/mobibench-turso -f 1024 -r 4 -a 0 -y 0 -t 1 -d 0 -n 10000 -j 3 -s 2 -T 3 -D 1 ./mobibench -p -n 1000 -d 0 -j 4 ``` ## Clickbench We have a modified version of the Clickbench benchmark script that can be run with: ```shell make clickbench ``` This will build Turso in release mode, create a database, and run the benchmarks with a small subset of the Clickbench dataset. It will run the queries for both Turso and SQLite, and print the results. ## Comparing VFS's/IO Back-ends (io_uring | syscall) ```shell make bench-vfs SQL="select * from users;" N=500 ``` The naive script will build and run limbo in release mode and execute the given SQL (against a copy of the `testing/testing.db` file) `N` times with each `vfs`. This is not meant to be a definitive or thorough performance benchmark but serves to compare the two. ## TPC-H on linux if you are using `tlp` to manage power settings, you may want to disable it while running the TPC-H benchmark as it can affect performance. consider changing swap to `swapoff -a` Run the benchmark script: ```shell ./perf/tpc-h/benchmark.sh ``` ================================================ FILE: Pipfile ================================================ [[source]] url = "https://pypi.org/simple" verify_ssl = true name = "pypi" [packages] faker = "26.0.0" [dev-packages] [requires] python_version = "3.11" ================================================ FILE: README.md ================================================

Turso Database

Turso Database

An in-process SQL database, compatible with SQLite.

Crate NPM PyPI Maven Central

Chat with the Core Developers on Discord

Chat with other users of Turso (and Turso Cloud) on Discord

--- ## About Turso Database is an in-process SQL database written in Rust, compatible with SQLite. > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ## Features and Roadmap * **SQLite compatibility** for SQL dialect, file formats, and the C API [see [document](COMPAT.md) for details] * **Change data capture (CDC)** for real-time tracking of database changes. * **Multi-language support** for * [Go](bindings/go) * [JavaScript](bindings/javascript) * [Java](bindings/java) * [Python](bindings/python) * [Rust](bindings/rust) * [WebAssembly](bindings/javascript) * **Asynchronous I/O** support on Linux with `io_uring` * **Cross-platform** support for Linux, macOS, Windows and browsers (through WebAssembly) * **Vector support** support including exact search and vector manipulation * **Improved schema management** including extended `ALTER` support and faster schema changes. The database has the following experimental features: * **`BEGIN CONCURRENT`** for improved write throughput using multi-version concurrency control (MVCC). * **Encryption at rest** for protecting the data locally. * **Incremental computation** using DBSP for incremental view maintenance and query subscriptions. * **Full-Text-Search** powered by the awesome [tantivy](https://github.com/quickwit-oss/tantivy) library The following features are on our current roadmap: * **Vector indexing** for fast approximate vector search, similar to [libSQL vector search](https://turso.tech/vector). ## Getting Started Please see the [Turso Database Manual](docs/manual.md) for more information.
💻 Command Line
You can install the latest `turso` release with: ```shell curl --proto '=https' --tlsv1.2 -LsSf \ https://github.com/tursodatabase/turso/releases/latest/download/turso_cli-installer.sh | sh ``` Then launch the interactive shell: ```shell $ tursodb ``` This will start the Turso interactive shell where you can execute SQL statements: ```console Turso Enter ".help" for usage hints. Connected to a transient in-memory database. Use ".open FILENAME" to reopen on a persistent database turso> CREATE TABLE users (id INT, username TEXT); turso> INSERT INTO users VALUES (1, 'alice'); turso> INSERT INTO users VALUES (2, 'bob'); turso> SELECT * FROM users; 1|alice 2|bob ``` You can also build and run the latest development version with: ```shell cargo run ``` If you like docker, we got you covered. Simply run this in the root folder: ```bash make docker-cli-build && \ make docker-cli-run ```
🦀 Rust
```console cargo add turso ``` Example usage: ```rust let db = Builder::new_local("sqlite.db").build().await?; let conn = db.connect()?; let res = conn.query("SELECT * FROM users", ()).await?; ```
✨ JavaScript
```console npm i @tursodatabase/database ``` Example usage: ```js import { connect } from '@tursodatabase/database'; const db = await connect('sqlite.db'); const stmt = db.prepare('SELECT * FROM users'); const users = stmt.all(); console.log(users); ```
🐍 Python
```console uv pip install pyturso ``` Example usage: ```python import turso con = turso.connect("sqlite.db") cur = con.cursor() res = cur.execute("SELECT * FROM users") print(res.fetchone()) ```
🦫 Go
```console go get turso.tech/database/tursogo go install turso.tech/database/tursogo ``` Example usage: ```go import ( "database/sql" _ "turso.tech/database/tursogo" ) conn, _ = sql.Open("turso", "sqlite.db") defer conn.Close() stmt, _ := conn.Prepare("select * from users") defer stmt.Close() rows, _ = stmt.Query() for rows.Next() { var id int var username string _ := rows.Scan(&id, &username) fmt.Printf("User: ID: %d, Username: %s\n", id, username) } ```
☕️ Java
We integrated Turso Database into JDBC. For detailed instructions on how to use Turso Database with java, please refer to the [README.md under bindings/java](bindings/java/README.md).
🤖 MCP Server Mode
The Turso CLI includes a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that allows AI assistants to interact with your databases. Start the MCP server with: ```shell tursodb your_database.db --mcp ``` ### Configuration Add Turso to your MCP client configuration: ```json { "mcpServers": { "turso": { "command": "/path/to/.turso/tursodb", "args": ["/path/to/your/database.db", "--mcp"] } } } ``` ### Available Tools The MCP server provides nine tools for database interaction: 1. **`open_database`** - Open a new database 2. **`current_database`** - Describe the current database 3. **`list_tables`** - List all tables in the database 4. **`describe_table`** - Describe the structure of a specific table 5. **`execute_query`** - Execute read-only SELECT queries 6. **`insert_data`** - Insert new data into tables 7. **`update_data`** - Update existing data in tables 8. **`delete_data`** - Delete data from tables 9. **`schema_change`** - Execute schema modification statements (CREATE TABLE, ALTER TABLE, DROP TABLE) Once connected, you can ask your AI assistant: - "Show me all tables in the database" - "What's the schema for the users table?" - "Find all posts with more than 100 upvotes" - "Insert a new user with name 'Alice' and email 'alice@example.com'" ### MCP Clients
Claude Code If you're using [Claude Code](https://claude.ai/code), you can easily connect to your Turso MCP server using the built-in MCP management commands: #### Quick Setup 1. **Add the MCP server** to Claude Code: ```bash claude mcp add my-database -- tursodb ./path/to/your/database.db --mcp ``` 2. **Restart Claude Code** to activate the connection 3. **Start querying** your database through natural language! #### Command Breakdown ```bash claude mcp add my-database -- tursodb ./path/to/your/database.db --mcp # ↑ ↑ ↑ ↑ # | | | | # Name | Database path MCP flag # Separator ``` - **`my-database`** - Choose any name for your MCP server - **`--`** - Required separator between Claude options and your command - **`tursodb`** - The Turso database CLI - **`./path/to/your/database.db`** - Path to your SQLite database file - **`--mcp`** - Enables MCP server mode #### Example Usage ```bash # For a local project database cd /your/project claude mcp add my-project-db -- tursodb ./data/app.db --mcp # For an absolute path claude mcp add analytics-db -- tursodb /Users/you/databases/analytics.db --mcp # For a specific project (local scope) claude mcp add project-db --local -- tursodb ./database.db --mcp ``` #### Managing MCP Servers ```bash # List all configured MCP servers claude mcp list # Get details about a specific server claude mcp get my-database # Remove an MCP server claude mcp remove my-database ```
Claude Desktop For Claude Desktop, add the configuration to your `claude_desktop_config.json` file: ```json { "mcpServers": { "turso": { "command": "/path/to/.turso/tursodb", "args": ["./path/to/your/database.db.db", "--mcp"] } } } ```
Cursor For Cursor, configure MCP in your settings: 1. Open Cursor settings 2. Navigate to Extensions → MCP 3. Add a new server with: - **Name**: `turso` - **Command**: `/path/to/.turso/tursodb` - **Args**: `["./path/to/your/database.db.db", "--mcp"]` Alternatively, you can add it to your Cursor configuration file directly.
### Direct JSON-RPC Usage The MCP server runs as a single process that handles multiple JSON-RPC requests over stdin/stdout. Here's how to interact with it directly: #### Example with In-Memory Database ```bash cat << 'EOF' | tursodb --mcp {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "client", "version": "1.0"}}} {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "schema_change", "arguments": {"query": "CREATE TABLE users (id INTEGER, name TEXT, email TEXT)"}}} {"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "list_tables", "arguments": {}}} {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "insert_data", "arguments": {"query": "INSERT INTO users VALUES (1, 'Alice', 'alice@example.com')"}}} {"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "execute_query", "arguments": {"query": "SELECT * FROM users"}}} EOF ``` #### Example with Existing Database ```bash # Working with an existing database file cat << 'EOF' | tursodb mydb.db --mcp {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "client", "version": "1.0"}}} {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "list_tables", "arguments": {}}} EOF ```
## Contributing We'd love to have you contribute to Turso Database! Please check out the [contribution guide] to get started. ### Found a data corruption bug? Get up to $1,000.00 SQLite is loved because it is the most reliable database in the world. The next evolution of SQLite has to match or surpass this level of reliability. Turso is built with [Deterministic Simulation Testing](testing/simulator/README.md/) from the ground up, and is also tested by [Antithesis](https://antithesis.com). Even during Alpha, if you find a bug that leads to a data corruption and demonstrate how our simulator failed to catch it, you can get up to $1,000.00. As the project matures we will increase the size of the prize, and the scope of the bugs. List of rewarded cases: * B-Tree interior cell replacement issue in btrees with depth >=3 ([#2106](https://github.com/tursodatabase/turso/issues/2106)) * Don't allow autovacuum to be flipped on non-empty databases ([#3830](https://github.com/tursodatabase/turso/pull/3830)) * Self-insert with nested subquery generates corrupt data ([#3436](https://github.com/tursodatabase/turso/pull/3436)) * Ptrmap data corruption with pre-initialized autovacuum database ([#3894](https://github.com/tursodatabase/turso/pull/3894)) * WAL corruption on statement rollback with constraint violation ([#4493](https://github.com/tursodatabase/turso/pull/4493)) More details [here](https://turso.algora.io). Turso core staff are not eligible. ## FAQ ### Is Turso Database ready for production use? Turso Database is currently under heavy development and is **not** ready for production use. ### How is Turso Database different from Turso's libSQL? Turso Database is a project to build the next evolution of SQLite in Rust, with a strong open contribution focus and features like native async support, vector search, and more. The libSQL project is also an attempt to evolve SQLite in a similar direction, but through a fork rather than a rewrite. Rewriting SQLite in Rust started as an unassuming experiment, and due to its incredible success, replaces libSQL as our intended direction. At this point, libSQL is production ready, Turso Database is not - although it is evolving rapidly. More details [here](https://turso.tech/blog/we-will-rewrite-sqlite-and-we-are-going-all-in). ## Publications * Pekka Enberg, Sasu Tarkoma, Jon Crowcroft Ashwin Rao (2024). Serverless Runtime / Database Co-Design With Asynchronous I/O. In _EdgeSys ‘24_. [[PDF]](https://penberg.org/papers/penberg-edgesys24.pdf) * Pekka Enberg, Sasu Tarkoma, and Ashwin Rao (2023). Towards Database and Serverless Runtime Co-Design. In _CoNEXT-SW ’23_. [[PDF](https://penberg.org/papers/penberg-conext-sw-23.pdf)] [[Slides](https://penberg.org/papers/penberg-conext-sw-23-slides.pdf)] ## License This project is licensed under the [MIT license]. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Turso Database by you, shall be licensed as MIT, without any additional terms or conditions. [contribution guide]: CONTRIBUTING.md [MIT license]: LICENSE.md ## Partners Thanks to all the partners of Turso! ## Contributors Thanks to all the contributors to Turso Database! ================================================ FILE: bindings/dotnet/.gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from `dotnet new gitignore` # dotenv files .env # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET project.lock.json project.fragment.lock.json artifacts/ # Tye .tye/ # ASP.NET Scaffolding ScaffoldingReadMe.txt # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.tlog *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Coverlet is a free, cross platform Code Coverage Tool coverage*.json coverage*.xml coverage*.info # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio 6 auto-generated project file (contains which files were open etc.) *.vbp # Visual Studio 6 workspace and project file (working project files containing files to include in project) *.dsw *.dsp # Visual Studio 6 technical files *.ncb *.aps # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # Visual Studio History (VSHistory) files .vshistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Fody - auto-generated XML schema FodyWeavers.xsd # VS Code files for those working on multiple tools .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json *.code-workspace # Local History for Visual Studio Code .history/ # Windows Installer files from build outputs *.cab *.msi *.msix *.msm *.msp # JetBrains Rider *.sln.iml .idea/ ## ## Visual studio for Mac ## # globs Makefile.in *.userprefs *.usertasks config.make config.status aclocal.m4 install-sh autom4te.cache/ *.tar.gz tarballs/ test-results/ # Mac bundle stuff *.dmg *.app # content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk # content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # Vim temporary swap files *.swp src/Turso.Native/* rs_compiled/* ================================================ FILE: bindings/dotnet/Cargo.toml ================================================ [package] name = "turso-dotnet" version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true publish = false [lints] workspace = true [lib] name = "turso_dotnet" crate-type = ["cdylib"] path = "rs_src/lib.rs" [dependencies] turso_core = { workspace = true } ================================================ FILE: bindings/dotnet/Makefile ================================================ ifeq ($(OS),Windows_NT) OS_TARGET = windows64 else UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Linux) OS_TARGET = linux64 endif ifeq ($(UNAME_S),Darwin) UNAME_P := $(shell uname -p) ifeq ($(UNAME_P),x86_64) OS_TARGET = macos64 else OS_TARGET = macosarm64 endif endif endif RUST_RELEASE_OPT ?= DOTNET_RELEASE_OPT ?= all: echo "make [build|test|benchmark|build-production]" build-rust-windows64: cargo build --target-dir ./rs_compiled --target x86_64-pc-windows-msvc $(RUST_RELEASE_OPT) build-rust-linux64: cargo build --target-dir ./rs_compiled --target x86_64-unknown-linux-gnu $(RUST_RELEASE_OPT) build-rust-macos64: cargo build --target-dir ./rs_compiled --target x86_64-apple-darwin $(RUST_RELEASE_OPT) build-rust-macosarm64: cargo build --target-dir ./rs_compiled --target aarch64-apple-darwin $(RUST_RELEASE_OPT) build-rust: echo "Building for $(OS_TARGET)" $(MAKE) build-rust-$(OS_TARGET) build-dotnet: dotnet build $(DOTNET_RELEASE_OPT) ./src/Turso/Turso.csproj build: build-rust build-dotnet build-production: $(MAKE) build RUST_RELEASE_OPT='--release' DOTNET_RELEASE_OPT='-c Release' test: build dotnet test ./src/Turso.Tests/Turso.Tests.csproj benchmark: build-production dotnet run -c Release --project ./src/Benchmarks/Benchmarks.csproj pack: dotnet build -c Release ./src/Turso.Raw/Turso.Raw.csproj dotnet build -c Release ./src/Turso.Raw/Turso.Raw.csproj dotnet pack -c Release ./src/Turso.Raw/Turso.Raw.csproj dotnet pack -c Release ./src/Turso/Turso.csproj ================================================ FILE: bindings/dotnet/Readme.md ================================================ # Turso_dotnet Dotnet binding for turso database. ## Getting Started ```C# using Turso; using var connection = new TursoConnection("Data Source=:memory:"); connection.Open(); connection.ExecuteNonQuery("CREATE TABLE t(a, b)"); var rowsAffected = connection.ExecuteNonQuery("INSERT INTO t(a, b) VALUES (1, 2), (3, 4)"); Console.WriteLine($"RowsAffected: {rowsAffected}"); using var command = connection.CreateCommand(); command.CommandText = "SELECT * FROM t"; using var reader = command.ExecuteReader(); while (reader.Read()) { var a = reader.GetInt32(0); var b = reader.GetInt32(1); Console.WriteLine($"Value1: {a}, Value2: {b}"); } ``` ================================================ FILE: bindings/dotnet/Turso.slnx ================================================ ================================================ FILE: bindings/dotnet/rs_src/lib.rs ================================================ use std::borrow::Cow; use std::ffi::CStr; use std::num::NonZero; use std::os::raw::c_char; use std::ptr::null; use std::slice; use std::sync::Arc; use turso_core::types::Text; use turso_core::{ self, Connection, DatabaseOpts, EncryptionOpts, LimboError, OpenFlags, Statement, Value, IO, }; type Error = *const std::ffi::c_char; #[repr(C)] pub struct Database { io: Arc, connection: Arc, } #[repr(C)] #[derive(Copy, Clone)] pub enum ValueType { Empty = 0, Null = 1, Integer = 2, Float = 3, Text = 4, Blob = 5, } #[repr(C)] #[derive(Clone, Copy)] pub struct Array { ptr: *const u8, len: usize, } #[repr(C)] #[derive(Copy, Clone)] pub union TursoValueUnion { int_val: i64, real_val: f64, text: Array, blob: Array, } #[repr(C)] #[derive(Copy, Clone)] pub struct TursoValue { value_type: ValueType, value: TursoValueUnion, } pub fn allocate(value: T) -> *const T { Box::into_raw(Box::new(value)) } pub fn allocate_string(str: &str) -> *const c_char { std::ffi::CString::new(str).unwrap().into_raw() } pub fn to_vec(array: Array) -> Vec { unsafe { let slice = slice::from_raw_parts(array.ptr, array.len); slice.to_vec() } } pub fn to_value(value: TursoValue) -> Value { match value.value_type { ValueType::Empty => Value::Null, ValueType::Null => Value::Null, ValueType::Integer => Value::from_i64(unsafe { value.value.int_val }), ValueType::Float => Value::from_f64(unsafe { value.value.real_val }), ValueType::Blob => Value::Blob(to_vec(unsafe { value.value.blob })), ValueType::Text => { let slice = unsafe { slice::from_raw_parts(value.value.text.ptr, value.value.text.len) }; match str::from_utf8(slice) { Ok(value) => Value::Text(Text::new(value)), Err(_) => Value::Null, } } } } /// Opens a database at the specified path and returns a pointer to the database. /// If an error occurred, returns null and writes a pointer to a null-terminated string into `error_ptr`. /// /// # Safety /// /// - The returned database pointer must be freed with `db_close`. /// - Any error string written to `error_ptr` must be freed with `free_string`. /// - `path_ptr` must not be null and must point to a valid null-terminated UTF-8 string. /// - `error_ptr` must not be null and must point to a valid writable location. #[no_mangle] pub unsafe extern "C" fn db_open( path_ptr: *const c_char, error_ptr: *mut Error, ) -> *const Database { let path_cstr: &CStr = unsafe { CStr::from_ptr(path_ptr) }; let path_str = path_cstr.to_str(); let connection_result = Connection::from_uri(path_str.unwrap(), DatabaseOpts::new()); match connection_result { Ok((io, val)) => allocate(Database { io, connection: val, }), Err(err) => { unsafe { *error_ptr = allocate_string(format!("Error while opening database: {err}").as_str()) } null() } } } /// Opens a database with encryption at the specified path. /// If cipher_ptr or hexkey_ptr is null, opens without encryption. /// If an error occurred, returns null and writes a pointer to a null-terminated string into `error_ptr`. /// /// # Safety /// /// - The returned database pointer must be freed with `db_close`. /// - Any error string written to `error_ptr` must be freed with `free_string`. /// - `path_ptr` must not be null and must point to a valid null-terminated UTF-8 string. /// - `cipher_ptr` and `hexkey_ptr` must either both be null or both point to valid null-terminated UTF-8 strings. /// - `error_ptr` must not be null and must point to a valid writable location. #[no_mangle] pub unsafe extern "C" fn db_open_with_encryption( path_ptr: *const c_char, cipher_ptr: *const c_char, hexkey_ptr: *const c_char, error_ptr: *mut Error, ) -> *const Database { let path_cstr: &CStr = unsafe { CStr::from_ptr(path_ptr) }; let path_str = match path_cstr.to_str() { Ok(s) => s, Err(err) => { unsafe { *error_ptr = allocate_string(format!("Invalid path encoding: {err}").as_str()) } return null(); } }; // Parse encryption options if both cipher and hexkey are provided let encryption_opts = if !cipher_ptr.is_null() && !hexkey_ptr.is_null() { let cipher_cstr: &CStr = unsafe { CStr::from_ptr(cipher_ptr) }; let hexkey_cstr: &CStr = unsafe { CStr::from_ptr(hexkey_ptr) }; let cipher_str = match cipher_cstr.to_str() { Ok(s) => s, Err(err) => { unsafe { *error_ptr = allocate_string(format!("Invalid cipher encoding: {err}").as_str()) } return null(); } }; let hexkey_str = match hexkey_cstr.to_str() { Ok(s) => s, Err(err) => { unsafe { *error_ptr = allocate_string(format!("Invalid hexkey encoding: {err}").as_str()) } return null(); } }; Some(EncryptionOpts { cipher: cipher_str.to_string(), hexkey: hexkey_str.to_string(), }) } else { None }; let db_opts = if encryption_opts.is_some() { DatabaseOpts::new().with_encryption(true) } else { DatabaseOpts::new() }; let io: Arc = match turso_core::PlatformIO::new() { Ok(io) => Arc::new(io), Err(err) => { unsafe { *error_ptr = allocate_string(format!("Failed to create IO: {err}").as_str()) } return null(); } }; // Parse encryption key before opening database let encryption_key = if let Some(ref opts) = encryption_opts { match turso_core::EncryptionKey::from_hex_string(&opts.hexkey) { Ok(key) => Some(key), Err(err) => { unsafe { *error_ptr = allocate_string(format!("Invalid encryption key: {err}").as_str()) } return null(); } } } else { None }; let db = match turso_core::Database::open_file_with_flags( io.clone(), path_str, OpenFlags::Create, db_opts, encryption_opts, ) { Ok(db) => db, Err(err) => { unsafe { *error_ptr = allocate_string(format!("Error while opening database: {err}").as_str()) } return null(); } }; // Use connect_with_encryption to properly set up encryption context before reading pages let connection = match db.connect_with_encryption(encryption_key) { Ok(conn) => conn, Err(err) => { unsafe { *error_ptr = allocate_string(format!("Error while connecting: {err}").as_str()) } return null(); } }; allocate(Database { io, connection }) } /// Disposes the database pointer. /// /// # Safety /// /// - `db_ptr` must be a pointer allocated by `db_open` or `db_open_with_encryption`. /// - Call `db_close` only once per `db_ptr`. #[no_mangle] pub unsafe extern "C" fn db_close(db_ptr: *mut Database) { let _ = unsafe { Box::from_raw(db_ptr) }; } /// Frees a null-terminated string previously allocated by this library. /// /// # Safety /// /// - `string_ptr` must be a pointer returned by this library (e.g., error messages, column names). /// - Call `free_string` only once per `string_ptr`. #[no_mangle] pub unsafe extern "C" fn free_string(string_ptr: *mut c_char) { unsafe { drop(std::ffi::CString::from_raw(string_ptr)) }; } /// Prepares an SQL statement and returns a pointer to the prepared statement. /// If an error occurred, returns null and writes a pointer to a null-terminated string into `error_ptr`. /// /// # Safety /// /// - `db_ptr` must not be null. /// - `sql_ptr` must not be null and must point to a valid null-terminated UTF-8 string. /// - `error_ptr` must not be null and must point to a valid writable location. /// - When not null, the statement pointer must be freed with `free_statement` and any error string with `free_string`. #[no_mangle] pub unsafe extern "C" fn db_prepare_statement( db_ptr: *mut Database, sql_ptr: *const c_char, error_ptr: *mut Error, ) -> *const Statement { let sql = unsafe { CStr::from_ptr(sql_ptr) }.to_str(); let db = unsafe { &mut (*db_ptr) }; let prepare_result = db.connection.prepare(sql.unwrap()); match prepare_result { Ok(statement) => allocate(statement), Err(e) => { unsafe { *error_ptr = allocate_string(format!("Unable to prepare statement: {e}").as_str()) } null() } } } /// Binds a parameter to the statement by index. /// /// # Safety /// /// - `statement_ptr` must be a pointer returned by `db_prepare_statement`. /// - `index` must be >= 1. /// - `parameter_value` must be a valid pointer to a `TursoValue`. #[no_mangle] pub unsafe extern "C" fn bind_parameter( statement_ptr: *mut Statement, index: i32, parameter_value: *const TursoValue, ) { let statement = unsafe { &mut (*statement_ptr) }; statement.bind_at( NonZero::new(index.try_into().unwrap()).unwrap(), to_value(*parameter_value), ); } /// Binds a parameter to the statement by name. /// /// # Safety /// /// - `statement_ptr` must be a pointer returned by `db_prepare_statement`. /// - `parameter_name` must not be null and must point to a valid null-terminated UTF-8 string. /// - `parameter_value` must be a valid pointer to a `TursoValue`. #[no_mangle] pub unsafe extern "C" fn bind_named_parameter( statement_ptr: *mut Statement, parameter_name: *const c_char, parameter_value: *const TursoValue, ) { let statement = unsafe { &mut (*statement_ptr) }; let parameter_name = unsafe { CStr::from_ptr(parameter_name) }.to_str().unwrap(); for idx in 1..statement.parameters_count() + 1 { let non_zero_idx = NonZero::new(idx).unwrap(); let param = statement.parameters().name(non_zero_idx); let Some(name) = param else { continue; }; if parameter_name == name { statement.bind_at(non_zero_idx, to_value(*parameter_value)); return; } } } /// Returns the number of rows changed by the statement. /// /// # Safety /// /// - `statement_ptr` must not be null. #[no_mangle] pub unsafe extern "C" fn db_statement_nchange(statement_ptr: *mut Statement) -> i64 { let statement = unsafe { &mut (*statement_ptr) }; statement.n_change() } /// Executes the statement, advancing it by one step. /// If an error occurred, sets `error_ptr` to a pointer to a null-terminated string. /// /// # Safety /// /// - `statement_ptr` must not be null. /// - `error_ptr` must not be null and must point to a location that is valid for writing. /// - If set, the error string must be freed with `free_string`. #[no_mangle] pub unsafe extern "C" fn db_statement_execute_step( statement_ptr: *mut Statement, error_ptr: *mut Error, ) -> bool { let statement = unsafe { &mut (*statement_ptr) }; let result = statement.run_one_step_blocking(|| Ok(()), || Ok(())); match result { Ok(Some(_)) => true, Ok(None) => { // Done false } Err(LimboError::Interrupt) => { unsafe { *error_ptr = allocate_string("Interrupted") }; false } Err(LimboError::Busy) => { unsafe { *error_ptr = allocate_string("Database is busy") }; false } Err(err) => { unsafe { *error_ptr = allocate_string(err.to_string().as_str()) }; false } } } /// Frees the statement pointer. /// /// # Safety /// /// - `statement_ptr` must not be null. /// - Call `free_statement` only once per `statement_ptr`. #[no_mangle] pub unsafe extern "C" fn free_statement(statement_ptr: *mut Statement) { let mut statement = unsafe { Box::from_raw(statement_ptr) }; statement.reset_best_effort(); } /// Gets the current value from the row at the specified column index. /// /// # Safety /// /// - `statement_ptr` must not be null. /// - `col_idx` must be >= 0. #[no_mangle] pub unsafe extern "C" fn db_statement_get_value( statement_ptr: *mut Statement, col_idx: i32, ) -> TursoValue { let statement = unsafe { &mut (*statement_ptr) }; if let Some(row) = statement.row() { let value = match row.get_value(col_idx.try_into().unwrap()) { Value::Null => TursoValue { value_type: ValueType::Null, value: TursoValueUnion { int_val: 0 }, }, Value::Numeric(turso_core::Numeric::Integer(int_val)) => TursoValue { value_type: ValueType::Integer, value: TursoValueUnion { int_val: *int_val }, }, Value::Numeric(turso_core::Numeric::Float(float_value)) => TursoValue { value_type: ValueType::Float, value: TursoValueUnion { real_val: f64::from(*float_value), }, }, Value::Text(text) => { let array = Array { ptr: text.value.as_ptr(), len: text.value.len(), }; TursoValue { value_type: ValueType::Text, value: TursoValueUnion { text: array }, } } Value::Blob(blob) => { let bytes = blob.as_ptr(); let array = Array { ptr: bytes, len: blob.len(), }; TursoValue { value_type: ValueType::Blob, value: TursoValueUnion { blob: array }, } } }; return value; } TursoValue { value_type: ValueType::Empty, value: TursoValueUnion { int_val: 0 }, } } /// Gets the number of columns in the current statement. /// /// # Safety /// /// - `statement_ptr` must not be null. #[no_mangle] pub unsafe extern "C" fn db_statement_num_columns(statement_ptr: *mut Statement) -> i32 { let statement = unsafe { &mut (*statement_ptr) }; statement.num_columns().try_into().unwrap() } /// Gets the column name for the specified index. /// The returned string is heap-allocated; free it with `free_string` when no longer needed. /// /// # Safety /// /// - `statement_ptr` must not be null. /// - `index` must be >= 0. #[no_mangle] pub unsafe extern "C" fn db_statement_column_name( statement_ptr: *mut Statement, index: i32, ) -> *const std::ffi::c_char { let statement = unsafe { &mut (*statement_ptr) }; let col_name = statement.get_column_name(index.try_into().unwrap()); match col_name { Cow::Borrowed(value) => allocate_string(value), Cow::Owned(value) => allocate_string(value.as_str()), } } /// Checks whether the statement currently points to a row. /// /// # Safety /// /// - `statement_ptr` must not be null. #[no_mangle] pub unsafe extern "C" fn db_statement_has_rows(statement_ptr: *mut Statement) -> bool { let statement = unsafe { &mut (*statement_ptr) }; match statement.row() { Some(_val) => true, None => false, } } ================================================ FILE: bindings/dotnet/src/Benchmarks/Benchmarks.cs ================================================ using System.Data; using System.Data.SQLite; using System.Runtime.CompilerServices; using BenchmarkDotNet.Attributes; using Microsoft.Data.Sqlite; using SQLitePCL; using Turso; namespace Benchmarks; [MemoryDiagnoser] public class Benchmarks { private SQLiteConnection _systemDataSqliteConnection; private SqliteConnection _microsoftDataSqliteConnection; private TursoConnection _tursoConnection; [GlobalSetup] public void Setup() { _systemDataSqliteConnection = new SQLiteConnection("Data Source=:memory:"); _systemDataSqliteConnection.Open(); _microsoftDataSqliteConnection = new SqliteConnection("Data Source=:memory:"); _microsoftDataSqliteConnection.Open(); _tursoConnection = new TursoConnection("Data Source=:memory:"); _tursoConnection.Open(); CreateTable(_systemDataSqliteConnection); CreateTable(_microsoftDataSqliteConnection); CreateTable(_tursoConnection); } [Benchmark] public void TursoSelect() => Select(_tursoConnection); [Benchmark] public void SystemSqliteSelect() => Select(_systemDataSqliteConnection); [Benchmark] public void MicrososftSqliteSelect() => Select(_microsoftDataSqliteConnection); private void CreateTable(IDbConnection connection) { using var createTableCommand = connection.CreateCommand(); createTableCommand.CommandText = "CREATE TABLE t(a, b)"; createTableCommand.ExecuteNonQuery(); using var insertCommand = connection.CreateCommand(); insertCommand.CommandText = @"INSERT INTO t(a, b) VALUES (1, 2), (3, 4);"; insertCommand.ExecuteNonQuery(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Select(IDbConnection connection) { using var command = connection.CreateCommand(); command.CommandText = "SELECT * FROM t;"; using var reader = command.ExecuteReader(); var sum = 0; while (reader.Read()) sum += reader.GetInt32(0); GC.KeepAlive(sum); } } ================================================ FILE: bindings/dotnet/src/Benchmarks/Benchmarks.csproj ================================================  Exe net9.0 enable enable ================================================ FILE: bindings/dotnet/src/Benchmarks/Program.cs ================================================  using BenchmarkDotNet.Running; BenchmarkRunner.Run(); ================================================ FILE: bindings/dotnet/src/Turso/Turso.csproj ================================================  Library net9.0 enable enable Turso Turso ADO.NET provider for turso bindings 0.0.1 ================================================ FILE: bindings/dotnet/src/Turso/TursoCommand.cs ================================================ using System.Data; using System.Data.Common; using Turso.Raw.Public; using Turso.Raw.Public.Handles; namespace Turso; public class TursoCommand : DbCommand { private TursoConnection _connection; private TursoParameterCollection _parameterCollection = new(); private TursoTransaction? _transaction; private TursoStatementHandle? _statement; public TursoCommand(TursoConnection connection, TursoTransaction? transaction = null) { _connection = connection; _transaction = transaction; } public TursoCommand(TursoConnection connection, string command) { _connection = connection; _transaction = null; CommandText = command; } public override string CommandText { get; set; } = ""; public override int CommandTimeout { get; set; } = 30; public override CommandType CommandType { get => CommandType.Text; set => throw new NotSupportedException(); } public override bool DesignTimeVisible { get; set; } public override UpdateRowSource UpdatedRowSource { get; set; } protected override DbConnection? DbConnection { get => _connection; set => _connection = value as TursoConnection ?? throw new ArgumentException(); } protected override DbParameterCollection DbParameterCollection => _parameterCollection; public new virtual TursoParameterCollection Parameters => _parameterCollection; protected override DbTransaction? DbTransaction { get => _transaction; set => _transaction = value as TursoTransaction ?? throw new ArgumentException(); } protected override void Dispose(bool disposing) { base.Dispose(disposing); _statement?.Dispose(); } public override void Cancel() { } public override int ExecuteNonQuery() { var reader = Execute(); reader.NextResult(); return reader.RecordsAffected; } public override object? ExecuteScalar() { using var reader = Execute(); return reader.Read() ? reader.GetValue(0) : null; } public override void Prepare() { _statement = TursoBindings.PrepareStatement(_connection.Turso, CommandText); for (var i = 0; i < _parameterCollection.Count; i++) { var parameter = _parameterCollection[i] as TursoParameter; if (parameter == null) throw new ArgumentException("Parameter must be of type TursoParameter"); if (!string.IsNullOrEmpty(parameter.ParameterName)) { TursoBindings.BindNamedParameter(_statement, parameter.ParameterName, parameter.ToValue()); } else { TursoBindings.BindParameter(_statement, i + 1, parameter.ToValue()); } } } protected override DbParameter CreateDbParameter() { return new TursoParameter(); } protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { return Execute(behavior); } private DbDataReader Execute(CommandBehavior behavior = CommandBehavior.Default) { if (_statement is null) Prepare(); var reader = new TursoDataReader(this, _statement); return reader; } } ================================================ FILE: bindings/dotnet/src/Turso/TursoConnection.cs ================================================ using System.Data; using System.Data.Common; using Turso.Raw.Public; using Turso.Raw.Public.Handles; namespace Turso; public class TursoConnection : DbConnection { private TursoDatabaseHandle? _turso = null; private TursoConnectionOptions _connectionOptions; public override string ConnectionString { get => _connectionOptions.GetConnectionString(); set => _connectionOptions = TursoConnectionOptions.Parse(value); } public override string Database => "main"; public override string DataSource => _connectionOptions["Data Source"] ?? ""; public override string ServerVersion => throw new NotImplementedException(); public override ConnectionState State => _turso is not null ? ConnectionState.Open : ConnectionState.Closed; public TursoConnection() : this("") { } public TursoConnection(string connectionString) { _connectionOptions = TursoConnectionOptions.Parse(connectionString); } public override void Open() { var filename = _connectionOptions["Data Source"] ?? ":memory:"; var cipher = _connectionOptions.GetEncryptionCipher(); var hexkey = _connectionOptions["Encryption Key"]; if (cipher.HasValue && hexkey is not null) { _turso = TursoBindings.OpenDatabaseWithEncryption(filename, cipher.Value, hexkey); } else { _turso = TursoBindings.OpenDatabase(filename); } } public override void Close() { _turso?.Dispose(); _turso = null; } protected override void Dispose(bool disposing) { base.Dispose(disposing); _turso?.Dispose(); } protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) { if (_turso is null) { throw new Exception("Turso database is closed"); } return new TursoTransaction(this, isolationLevel); } protected override DbCommand CreateDbCommand() { if (_turso is null) { throw new Exception("Turso database is closed"); } return new TursoCommand(this); } public int ExecuteNonQuery(string sql) { using var command = CreateCommand(); command.CommandText = sql; return command.ExecuteNonQuery(); } public override void ChangeDatabase(string databaseName) { throw new NotSupportedException(); } internal TursoDatabaseHandle Turso => _turso; } ================================================ FILE: bindings/dotnet/src/Turso/TursoConnectionOptions.cs ================================================ using Turso.Raw.Public.Value; namespace Turso; public class TursoConnectionOptions { private Dictionary _options = new(); private void AddOption(string keyword, string value) { if (!_valid_keywords.Contains(keyword)) { throw new InvalidOperationException($"Unsupported keyword: {keyword}"); } _options[keyword] = value; } public string GetConnectionString() { var parts = new List(); foreach (var keyword in _valid_keywords) { var option = GetOption(keyword); if (option is not null) { parts.Add($"{keyword}={option}"); } } return string.Join(";", parts); } private string? GetOption(string keyword) { return _options.GetValueOrDefault(keyword); } public string? this[string keyword] { get => GetOption(keyword); set => AddOption(keyword, value ?? ""); } /// /// Gets the encryption cipher from the connection options. /// /// The cipher enum value, or null if not specified or invalid. public TursoEncryptionCipher? GetEncryptionCipher() { var cipherStr = GetOption("Encryption Cipher"); if (cipherStr is null) return null; return cipherStr.ToLowerInvariant() switch { "aes128gcm" => TursoEncryptionCipher.Aes128Gcm, "aes256gcm" => TursoEncryptionCipher.Aes256Gcm, "aegis256" => TursoEncryptionCipher.Aegis256, "aegis256x2" => TursoEncryptionCipher.Aegis256x2, "aegis128l" => TursoEncryptionCipher.Aegis128l, "aegis128x2" => TursoEncryptionCipher.Aegis128x2, "aegis128x4" => TursoEncryptionCipher.Aegis128x4, _ => throw new InvalidOperationException($"Unknown encryption cipher: {cipherStr}") }; } private readonly string[] _valid_keywords = [ "Data Source", "Mode", "Cache", "Password", "Foreign Keys", "Recursive Triggers", "Default Timeout", "Pooling", "Vfs", "Encryption Cipher", "Encryption Key" ]; public static TursoConnectionOptions Parse(string connectionString) { var options = new TursoConnectionOptions(); foreach (var optionPart in connectionString.Split(";")) { var separatorIndex = optionPart.IndexOf('='); if (separatorIndex == -1) continue; var keyword = optionPart.Substring(0, separatorIndex); var value = optionPart.Substring(separatorIndex + 1); options.AddOption(keyword, value); } return options; } } ================================================ FILE: bindings/dotnet/src/Turso/TursoDataReader.cs ================================================ using System.Collections; using System.ComponentModel; using System.Data.Common; using System.Globalization; using System.Runtime.CompilerServices; using Turso.Raw.Public; using Turso.Raw.Public.Handles; using Turso.Raw.Public.Value; namespace Turso; public class TursoDataReader : DbDataReader { private readonly TursoCommand _command; private readonly TursoStatementHandle _statement; public TursoDataReader(TursoCommand command, TursoStatementHandle statement) { _command = command; _statement = statement; } public override bool GetBoolean(int ordinal) { return TursoBindings.GetValue(_statement, ordinal).IntValue != 0; } public override byte GetByte(int ordinal) { return (byte)TursoBindings.GetValue(_statement, ordinal).IntValue; } public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) { return GetArray(ordinal, dataOffset, buffer, bufferOffset, length); } public override char GetChar(int ordinal) { var value = TursoBindings.GetValue(_statement, ordinal); if (value.ValueType == TursoValueType.Text && value.StringValue.Length == 1) { return value.StringValue[0]; } return (char)TursoBindings.GetValue(_statement, ordinal).IntValue; } public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) { return GetArray(ordinal, dataOffset, buffer, bufferOffset, length); } public override string GetDataTypeName(int ordinal) { var value = TursoBindings.GetValue(_statement, ordinal); return GetTypeName(value.ValueType); } public override DateTime GetDateTime(int ordinal) { var value = TursoBindings.GetValue(_statement, ordinal); switch (value.ValueType) { case TursoValueType.Text: return DateTime.Parse(GetString(ordinal), CultureInfo.InvariantCulture); default: return DateTime.MinValue; } } public override decimal GetDecimal(int ordinal) { return (decimal)TursoBindings.GetValue(_statement, ordinal).RealValue; } public override double GetDouble(int ordinal) { return TursoBindings.GetValue(_statement, ordinal).RealValue; } public override Type GetFieldType(int ordinal) { var value = TursoBindings.GetValue(_statement, ordinal); return value.ValueType switch { TursoValueType.Integer => typeof(long), TursoValueType.Real => typeof(double), TursoValueType.Text => typeof(string), TursoValueType.Blob => typeof(byte[]), _ => typeof(object) }; } public override float GetFloat(int ordinal) { return (float)TursoBindings.GetValue(_statement, ordinal).RealValue; } public override Guid GetGuid(int ordinal) { return Guid.Parse(TursoBindings.GetValue(_statement, ordinal).StringValue); } public override short GetInt16(int ordinal) { return (short)TursoBindings.GetValue(_statement, ordinal).IntValue; } public override int GetInt32(int ordinal) { return (int)TursoBindings.GetValue(_statement, ordinal).IntValue; } public override long GetInt64(int ordinal) { return TursoBindings.GetValue(_statement, ordinal).IntValue; } public override string GetName(int ordinal) { return TursoBindings.GetName(_statement, ordinal); } public override int GetOrdinal(string name) { var fields = TursoBindings.GetFieldCount(_statement); for (var i = 0; i < fields; i++) { var columnName = TursoBindings.GetName(_statement, i); if (columnName == name) return i; } throw new IndexOutOfRangeException($"column {name} not found"); } public override string GetString(int ordinal) { return TursoBindings.GetValue(_statement, ordinal).StringValue; } public override object? GetValue(int ordinal) { var value = TursoBindings.GetValue(_statement, ordinal); return value.ValueType switch { TursoValueType.Null or TursoValueType.Empty => null, TursoValueType.Integer => value.IntValue, TursoValueType.Real => value.RealValue, TursoValueType.Text => value.StringValue, TursoValueType.Blob => value.BlobValue, _ => throw new ArgumentOutOfRangeException() }; } public override int GetValues(object[] values) { var i = 0; for (; i < FieldCount; i++) { values[i] = GetValue(i)!; } return i; } public override bool IsDBNull(int ordinal) { var valueType = TursoBindings.GetValue(_statement, ordinal).ValueType; return valueType == TursoValueType.Null; } public override int FieldCount => TursoBindings.GetFieldCount(_statement); public override object this[int ordinal] => GetValue(ordinal)!; public override object this[string name] { get { var ordinal = GetOrdinal(name); return GetValue(ordinal)!; } } public override int RecordsAffected => TursoBindings.RowsAffected(_statement); public override bool HasRows => TursoBindings.HasRows(_statement); public override bool IsClosed => _statement.IsInvalid; public override bool NextResult() { while (TursoBindings.Read(_statement)) ; return true; } protected override void Dispose(bool disposing) { base.Dispose(disposing); _command.Dispose(); } public override bool Read() { return TursoBindings.Read(_statement); } public override int Depth => 0; public override IEnumerator GetEnumerator() { return new DbEnumerator(this, closeReader: false); } private long GetArray(int ordinal, long dataOffset, T[]? buffer, int bufferOffset, int length) where T : struct { var bytes = TursoBindings.GetValue(_statement, ordinal).BlobValue; if (buffer is null) { return Math.Min(bytes.Length - dataOffset, length); } var position = 0; for (; position < length; position++) { if (bufferOffset + position >= buffer.Length || position + dataOffset >= bytes.Length) break; buffer[bufferOffset + position] = Unsafe.As(ref bytes[position + dataOffset]); } return position; } private static string GetTypeName(TursoValueType valueType) { return valueType switch { TursoValueType.Empty => "", TursoValueType.Null => "NULL", TursoValueType.Integer => "INTEGER", TursoValueType.Real => "REAL", TursoValueType.Text => "TEXT", TursoValueType.Blob => "BLOB", _ => throw new InvalidEnumArgumentException(nameof(valueType)) }; } } ================================================ FILE: bindings/dotnet/src/Turso/TursoParameter.cs ================================================ using System.Data; using System.Data.Common; using Turso.Raw.Public.Value; namespace Turso; public class TursoParameter : DbParameter { private static readonly Dictionary TursoTypeMapping = new() { { typeof(bool), TursoValueType.Integer }, { typeof(byte), TursoValueType.Integer }, { typeof(byte[]), TursoValueType.Blob }, { typeof(char), TursoValueType.Text }, { typeof(DateTime), TursoValueType.Text }, { typeof(DateTimeOffset), TursoValueType.Text }, { typeof(DateOnly), TursoValueType.Text }, { typeof(TimeOnly), TursoValueType.Text }, { typeof(DBNull), TursoValueType.Null }, { typeof(decimal), TursoValueType.Text }, { typeof(double), TursoValueType.Real }, { typeof(float), TursoValueType.Real }, { typeof(Guid), TursoValueType.Text }, { typeof(int), TursoValueType.Integer }, { typeof(long), TursoValueType.Integer }, { typeof(sbyte), TursoValueType.Integer }, { typeof(short), TursoValueType.Integer }, { typeof(string), TursoValueType.Text }, { typeof(TimeSpan), TursoValueType.Text }, { typeof(uint), TursoValueType.Integer }, { typeof(ulong), TursoValueType.Integer }, { typeof(ushort), TursoValueType.Integer } }; public TursoParameter() { } public TursoParameter(object value) { Value = value; } public TursoParameter(string parameterName, object value) { ParameterName = parameterName; Value = value; } public TursoParameter(string parameterName, DbType dbType, object value) { ParameterName = parameterName; DbType = dbType; Value = value; } public override void ResetDbType() { DbType = DbType.String; } public override DbType DbType { get; set; } = DbType.String; public override ParameterDirection Direction { get => ParameterDirection.Input; set { if (value != ParameterDirection.Input) { throw new ArgumentException("Only input parameters are supported"); } } } public override bool IsNullable { get; set; } public override string? ParameterName { get; set; } public override string? SourceColumn { get; set; } public override object? Value { get; set; } public override bool SourceColumnNullMapping { get; set; } public TursoValue ToValue() { if (Value is null) return new TursoValue { ValueType = TursoValueType.Null }; var valueType = Value.GetType(); if (!TursoTypeMapping.TryGetValue(valueType, out var tursoValueType)) { throw new ArgumentException($"Parameter type {valueType} is not supported"); } return GetTursoValue(Value, tursoValueType); } public override int Size { get => Value is string s ? s.Length : Value is byte[] bytes ? bytes.Length : 0; set => throw new NotImplementedException(); } private TursoValue GetTursoValue(object value, TursoValueType tursoValueType) { return tursoValueType switch { TursoValueType.Empty => new TursoValue() { ValueType = TursoValueType.Empty }, TursoValueType.Null => new TursoValue() { ValueType = TursoValueType.Null }, TursoValueType.Integer => new TursoValue() { ValueType = TursoValueType.Integer, IntValue = Convert.ToInt64(value) }, TursoValueType.Real => new TursoValue() { ValueType = TursoValueType.Real, RealValue = Convert.ToDouble(value) }, TursoValueType.Text => new TursoValue() { ValueType = TursoValueType.Text, StringValue = value.ToString()! }, TursoValueType.Blob => new TursoValue() { ValueType = TursoValueType.Blob, BlobValue = (byte[])value }, _ => throw new ArgumentOutOfRangeException(nameof(tursoValueType), tursoValueType, null) }; } } ================================================ FILE: bindings/dotnet/src/Turso/TursoParameterCollection.cs ================================================ using System.Collections; using System.Data.Common; namespace Turso; public class TursoParameterCollection : DbParameterCollection { private readonly List _parameters = new(); public override int Count => _parameters.Count; public override object SyncRoot => throw new NotImplementedException(); public override int Add(object value) { _parameters.Add(value as TursoParameter ?? new TursoParameter(value)); return _parameters.Count - 1; } public int AddWithValue(string parameterName, object value) { _parameters.Add(new TursoParameter(parameterName, value)); return _parameters.Count - 1; } public override void AddRange(Array values) { for (var i = 0; i < values.Length; i++) { var value = values.GetValue(i)!; var parameter = value as TursoParameter ?? new TursoParameter(value); _parameters.Add(parameter); } } public override void Clear() { _parameters.Clear(); } public override bool Contains(object value) { return _parameters.Any(p => value is TursoParameter ? p == value : p.Value == value); } public override bool Contains(string value) { return _parameters.Any(p => p.ParameterName == value); } public override void CopyTo(Array array, int index) { _parameters.CopyTo((TursoParameter[])array, index); } public override IEnumerator GetEnumerator() { return _parameters.GetEnumerator(); } public override int IndexOf(object value) { return _parameters.FindIndex(p => value is TursoParameter ? p == value : p.Value == value); } public override int IndexOf(string parameterName) { return _parameters.FindIndex(p => p.ParameterName == parameterName); } public override void Insert(int index, object value) { _parameters.Insert(index, value as TursoParameter ?? new TursoParameter(value)); } public override void Remove(object value) { var index = IndexOf(value); if (index == -1) throw new ArgumentException($"Parameter {value} not found"); _parameters.RemoveAt(index); } public override void RemoveAt(int index) { _parameters.RemoveAt(index); } public override void RemoveAt(string parameterName) { var index = IndexOf(parameterName); if (index == -1) throw new ArgumentException($"Parameter {parameterName} not found"); _parameters.RemoveAt(index); } protected override DbParameter GetParameter(int index) { return _parameters[index]; } protected override DbParameter GetParameter(string parameterName) { return _parameters.Find(p => p.ParameterName == parameterName) ?? throw new ArgumentException($"Parameter {parameterName} not found"); } protected override void SetParameter(int index, DbParameter value) { _parameters[index] = value as TursoParameter ?? throw new ArgumentException($"Parameter {value} is not a TursoParameter"); } protected override void SetParameter(string parameterName, DbParameter value) { var index = IndexOf(parameterName); if (index == -1) throw new ArgumentException($"Parameter {parameterName} not found"); _parameters[index] = value as TursoParameter ?? throw new ArgumentException($"Parameter {value} is not a TursoParameter"); } } ================================================ FILE: bindings/dotnet/src/Turso/TursoTransaction.cs ================================================ using System.Data.Common; using IsolationLevel = System.Data.IsolationLevel; namespace Turso; public class TursoTransaction : DbTransaction { private TursoConnection _connection; private IsolationLevel _isolationLevel; private bool _completed = false; public TursoTransaction(TursoConnection connection, IsolationLevel isolationLevel) { _connection = connection; _isolationLevel = isolationLevel; if (isolationLevel == IsolationLevel.ReadUncommitted) connection.ExecuteNonQuery("PRAGMA read_uncommitted = 1;"); connection.ExecuteNonQuery("BEGIN"); } protected override void Dispose(bool disposing) { if (!_completed) { Rollback(); } } public override IsolationLevel IsolationLevel => _isolationLevel; protected override DbConnection? DbConnection => _connection; public override void Commit() { _connection.ExecuteNonQuery("COMMIT;"); CompleteTransaction(); } public override void Rollback() { try { _connection.ExecuteNonQuery("ROLLBACK;"); } finally { CompleteTransaction(); } } private void CompleteTransaction() { if (_isolationLevel == IsolationLevel.ReadUncommitted) _connection.ExecuteNonQuery("PRAGMA read_uncommitted = 0;"); _completed = true; } } ================================================ FILE: bindings/dotnet/src/Turso.Raw/Data/TursoNativeArray.cs ================================================ using System.Runtime.InteropServices; namespace Turso.Raw.Data; [StructLayout(LayoutKind.Sequential)] internal struct TursoNativeArray { public IntPtr Data; public UInt64 Length; } ================================================ FILE: bindings/dotnet/src/Turso.Raw/Data/TursoNativeRowValueUnion.cs ================================================ using System.Runtime.InteropServices; namespace Turso.Raw.Data; [StructLayout(LayoutKind.Explicit)] internal struct TursoNativeRowValueUnion { [FieldOffset(0)] public Int64 IntValue; [FieldOffset(0)] public Double RealValue; [FieldOffset(0)] public TursoNativeArray StringValue; [FieldOffset(0)] public TursoNativeArray BlobValue; } ================================================ FILE: bindings/dotnet/src/Turso.Raw/Data/TursoNativeValue.cs ================================================ using System.Runtime.InteropServices; using Turso.Raw.Public.Value; namespace Turso.Raw.Data; [StructLayout(LayoutKind.Explicit)] internal ref struct TursoNativeValue { [FieldOffset(0)] public TursoValueType ValueType; [FieldOffset(8)] public TursoNativeRowValueUnion RowValueUnion; } ================================================ FILE: bindings/dotnet/src/Turso.Raw/Public/Handles/TursoDatabaseHandle.cs ================================================ using System.Runtime.InteropServices; namespace Turso.Raw.Public.Handles; public class TursoDatabaseHandle() : SafeHandle(IntPtr.Zero, true) { protected override bool ReleaseHandle() { TursoInterop.CloseDatabase(handle); return true; } public void ThrowIfInvalid() { if (IsInvalid) throw new NullReferenceException("database is invalid"); } public static TursoDatabaseHandle FromPtr(IntPtr ptr) { var handle = new TursoDatabaseHandle(); handle.SetHandle(ptr); return handle; } public override bool IsInvalid => handle == IntPtr.Zero; } ================================================ FILE: bindings/dotnet/src/Turso.Raw/Public/Handles/TursoStatementHandle.cs ================================================ using System.Runtime.InteropServices; namespace Turso.Raw.Public.Handles; public class TursoStatementHandle() : SafeHandle(IntPtr.Zero, true) { protected override bool ReleaseHandle() { TursoInterop.FreeStatement(handle); return true; } public void ThrowIfInvalid() { if (IsInvalid) throw new NullReferenceException("statement is invalid"); } public static TursoStatementHandle FromPtr(IntPtr ptr) { var handle = new TursoStatementHandle(); handle.SetHandle(ptr); return handle; } public override bool IsInvalid => handle == IntPtr.Zero; } ================================================ FILE: bindings/dotnet/src/Turso.Raw/Public/TursoBindings.cs ================================================ using System.Runtime.InteropServices; using System.Text; using Turso.Raw.Data; using Turso.Raw.Public.Handles; using Turso.Raw.Public.Value; namespace Turso.Raw.Public; public static class TursoBindings { public static TursoDatabaseHandle OpenDatabase(string path) { ArgumentNullException.ThrowIfNull(path); var dbPtr = TursoInterop.OpenDatabase(path, out var errorPtr); if (errorPtr != IntPtr.Zero) ThrowException(errorPtr); return TursoDatabaseHandle.FromPtr(dbPtr); } /// /// Opens a database with local encryption. /// /// The path to the database file. /// The encryption cipher to use. /// The hex-encoded encryption key. /// A handle to the opened database. public static TursoDatabaseHandle OpenDatabaseWithEncryption(string path, TursoEncryptionCipher cipher, string hexkey) { ArgumentNullException.ThrowIfNull(path); ArgumentNullException.ThrowIfNull(hexkey); var cipherStr = cipher.ToRustString(); var dbPtr = TursoInterop.OpenDatabaseWithEncryption(path, cipherStr, hexkey, out var errorPtr); if (errorPtr != IntPtr.Zero) ThrowException(errorPtr); return TursoDatabaseHandle.FromPtr(dbPtr); } public static TursoStatementHandle PrepareStatement(TursoDatabaseHandle db, string sql) { db.ThrowIfInvalid(); ArgumentNullException.ThrowIfNull(sql); var statementPtr = TursoInterop.PrepareStatement(db, sql, out var errorPtr); if (errorPtr != IntPtr.Zero) ThrowException(errorPtr); return TursoStatementHandle.FromPtr(statementPtr); } public static void BindParameter(TursoStatementHandle statement, int index, TursoValue parameter) { statement.ThrowIfInvalid(); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(index); var nativeValue = FromValue(parameter, out var handle); try { unsafe { var ptr = &nativeValue; TursoInterop.BindParameter(statement, index, (IntPtr)ptr); } } finally { if (handle.HasValue) handle.Value.Free(); } } public static void BindNamedParameter(TursoStatementHandle statement, string name, TursoValue parameter) { statement.ThrowIfInvalid(); ArgumentNullException.ThrowIfNull(name); var nativeValue = FromValue(parameter, out var handle); try { unsafe { var ptr = &nativeValue; TursoInterop.BindNamedParameter(statement, name, (IntPtr)ptr); } } finally { if (handle.HasValue) handle.Value.Free(); } } public static bool Read(TursoStatementHandle statement) { statement.ThrowIfInvalid(); var hasData = TursoInterop.StatementExecuteStep(statement, out var errorPtr); if (errorPtr != IntPtr.Zero) ThrowException(errorPtr); return hasData; } public static TursoValue GetValue(TursoStatementHandle statement, int columnIndex) { statement.ThrowIfInvalid(); ArgumentOutOfRangeException.ThrowIfNegative(columnIndex); var rowValue = TursoInterop.GetValueFromStatement(statement, columnIndex); return rowValue.ValueType switch { TursoValueType.Empty => TursoValue.Empty(), TursoValueType.Null => TursoValue.Null(), TursoValueType.Integer => TursoValue.Int(rowValue.RowValueUnion.IntValue), TursoValueType.Real => TursoValue.Real(rowValue.RowValueUnion.RealValue), TursoValueType.Text => TursoValue.String( Encoding.UTF8.GetString(ToArray(rowValue.RowValueUnion.StringValue))), TursoValueType.Blob => TursoValue.Blob(ToArray(rowValue.RowValueUnion.BlobValue)), _ => throw new ArgumentOutOfRangeException() }; } public static string GetName(TursoStatementHandle statement, int ordinal) { statement.ThrowIfInvalid(); ArgumentOutOfRangeException.ThrowIfNegative(ordinal); var cname = TursoInterop.StatementColumnName(statement, ordinal); try { return Marshal.PtrToStringUTF8(cname) ?? ""; } finally { TursoInterop.FreeString(cname); } } public static int GetFieldCount(TursoStatementHandle statement) { statement.ThrowIfInvalid(); return TursoInterop.StatementNumColumns(statement); } public static int RowsAffected(TursoStatementHandle statement) { statement.ThrowIfInvalid(); return (int)TursoInterop.StatementRowsAffected(statement); } public static bool HasRows(TursoStatementHandle statement) { statement.ThrowIfInvalid(); return TursoInterop.StatementHasRows(statement); } private static TursoNativeValue FromValue(TursoValue value, out GCHandle? handle) { handle = null; var union = new TursoNativeRowValueUnion(); if (value.ValueType == TursoValueType.Integer) union.IntValue = value.IntValue; if (value.ValueType == TursoValueType.Real) union.RealValue = value.RealValue; if (value.ValueType == TursoValueType.Text) { var bytes = Encoding.UTF8.GetBytes(value.StringValue); handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); union.StringValue = new TursoNativeArray { Data = handle.Value.AddrOfPinnedObject(), Length = (ulong)bytes.Length }; } if (value.ValueType == TursoValueType.Blob) { handle = GCHandle.Alloc(value.BlobValue, GCHandleType.Pinned); union.BlobValue = new TursoNativeArray { Data = handle.Value.AddrOfPinnedObject(), Length = (ulong)value.BlobValue.Length }; } return new TursoNativeValue { ValueType = value.ValueType, RowValueUnion = union, }; } private static byte[] ToArray(TursoNativeArray array) { unsafe { var data = new Span((void*)array.Data, (int)array.Length); return data.ToArray(); } } private static void ThrowException(IntPtr errorPtr) { var errorMessage = Marshal.PtrToStringUTF8(errorPtr); var exception = new TursoException(errorMessage ?? "Internal error"); TursoInterop.FreeString(errorPtr); throw exception; } } ================================================ FILE: bindings/dotnet/src/Turso.Raw/Public/TursoException.cs ================================================ namespace Turso.Raw.Public; public class TursoException(string message) : Exception(message); ================================================ FILE: bindings/dotnet/src/Turso.Raw/Public/Value/TursoEncryptionCipher.cs ================================================ namespace Turso.Raw.Public.Value; /// /// Supported encryption ciphers for local database encryption. /// public enum TursoEncryptionCipher { /// AES-128-GCM cipher Aes128Gcm, /// AES-256-GCM cipher Aes256Gcm, /// AEGIS-256 cipher Aegis256, /// AEGIS-256X2 cipher Aegis256x2, /// AEGIS-128L cipher Aegis128l, /// AEGIS-128X2 cipher Aegis128x2, /// AEGIS-128X4 cipher Aegis128x4, } internal static class TursoEncryptionCipherExtensions { public static string ToRustString(this TursoEncryptionCipher cipher) { return cipher switch { TursoEncryptionCipher.Aes128Gcm => "aes128gcm", TursoEncryptionCipher.Aes256Gcm => "aes256gcm", TursoEncryptionCipher.Aegis256 => "aegis256", TursoEncryptionCipher.Aegis256x2 => "aegis256x2", TursoEncryptionCipher.Aegis128l => "aegis128l", TursoEncryptionCipher.Aegis128x2 => "aegis128x2", TursoEncryptionCipher.Aegis128x4 => "aegis128x4", _ => throw new ArgumentOutOfRangeException(nameof(cipher), cipher, null) }; } } ================================================ FILE: bindings/dotnet/src/Turso.Raw/Public/Value/TursoValue.cs ================================================ namespace Turso.Raw.Public.Value; public struct TursoValue { public TursoValueType ValueType; public long IntValue; public double RealValue; public string StringValue; public byte[] BlobValue; public static TursoValue Empty() => new() { ValueType = TursoValueType.Empty }; public static TursoValue Null() => new() { ValueType = TursoValueType.Null }; public static TursoValue Int(Int64 value) => new() { ValueType = TursoValueType.Integer, IntValue = value }; public static TursoValue Real(Double value) => new() { ValueType = TursoValueType.Real, RealValue = value }; public static TursoValue String(string value) => new() { ValueType = TursoValueType.Text, StringValue = value }; public static TursoValue Blob(byte[] value) => new() { ValueType = TursoValueType.Blob, BlobValue = value }; } ================================================ FILE: bindings/dotnet/src/Turso.Raw/Public/Value/TursoValueType.cs ================================================ namespace Turso.Raw.Public.Value; public enum TursoValueType { Empty = 0, Null = 1, Integer = 2, Real = 3, Text = 4, Blob = 5, } ================================================ FILE: bindings/dotnet/src/Turso.Raw/Turso.Raw.csproj ================================================  net9.0 enable enable true Turso.Raw Dotnet bindings for turso database 0.0.1 Always runtimes\win-x64\native\turso_dotnet.dll Always runtimes\linux-x64\native\turso_dotnet.so Always runtimes\osx-x64\native\turso_dotnet.dylib Always runtimes\osx-arm\native\turso_dotnet.dylib Always runtimes\win-x64\native\turso_dotnet.dll true runtimes\win-x64\native\turso_dotnet.dll Always runtimes\linux-x64\native\turso_dotnet.so true runtimes\linux-x64\native\turso_dotnet.so Always runtimes\osx-x64\native\turso_dotnet.dylib true runtimes\osx-x64\native\turso_dotnet.dylib Always runtimes\osx-arm64\native\turso_dotnet.dylib true runtimes\osx-arm64\native\turso_dotnet.dylib ================================================ FILE: bindings/dotnet/src/Turso.Raw/TursoInterop.cs ================================================ using System.Runtime.InteropServices; using Turso.Raw.Data; using Turso.Raw.Public.Handles; namespace Turso.Raw; internal static class TursoInterop { private const string DllName = "turso_dotnet"; [DllImport(DllName, EntryPoint = "db_open", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr OpenDatabase(string path, out IntPtr errorPtr); [DllImport(DllName, EntryPoint = "db_open_with_encryption", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr OpenDatabaseWithEncryption(string path, string? cipher, string? hexkey, out IntPtr errorPtr); [DllImport(DllName, EntryPoint = "db_close", CallingConvention = CallingConvention.Cdecl)] public static extern void CloseDatabase(IntPtr db); [DllImport(DllName, EntryPoint = "free_string", CallingConvention = CallingConvention.Cdecl)] public static extern void FreeString(IntPtr stringPtr); [DllImport(DllName, EntryPoint = "db_prepare_statement", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr PrepareStatement(TursoDatabaseHandle db, string sql, out IntPtr errorPtr); [DllImport(DllName, EntryPoint = "free_statement", CallingConvention = CallingConvention.Cdecl)] public static extern void FreeStatement(IntPtr statement); [DllImport(DllName, EntryPoint = "bind_parameter", CallingConvention = CallingConvention.Cdecl)] public static extern void BindParameter(TursoStatementHandle statement, int index, IntPtr tursoValue); [DllImport(DllName, EntryPoint = "bind_named_parameter", CallingConvention = CallingConvention.Cdecl)] public static extern void BindNamedParameter(TursoStatementHandle statement, string parameterName, IntPtr tursoValue); [DllImport(DllName, EntryPoint = "db_statement_execute_step", CallingConvention = CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool StatementExecuteStep(TursoStatementHandle statement, out IntPtr errorPtr); [DllImport(DllName, EntryPoint = "db_statement_nchange", CallingConvention = CallingConvention.Cdecl)] public static extern long StatementRowsAffected(TursoStatementHandle statement); [DllImport(DllName, EntryPoint = "db_statement_get_value", CallingConvention = CallingConvention.Cdecl)] public static extern TursoNativeValue GetValueFromStatement(TursoStatementHandle statement, int columnIndex); [DllImport(DllName, EntryPoint = "db_statement_num_columns", CallingConvention = CallingConvention.Cdecl)] public static extern int StatementNumColumns(TursoStatementHandle statement); [DllImport(DllName, EntryPoint = "db_statement_column_name", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr StatementColumnName(TursoStatementHandle statement, int index); [DllImport(DllName, EntryPoint = "db_statement_has_rows", CallingConvention = CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool StatementHasRows(TursoStatementHandle statement); } ================================================ FILE: bindings/dotnet/src/Turso.Tests/Turso.Tests.csproj ================================================  net9.0 latest enable enable false ================================================ FILE: bindings/dotnet/src/Turso.Tests/TursoTests.cs ================================================ using System.Data.Common; using AwesomeAssertions; using Turso.Raw.Public; using Turso.Raw.Public.Value; namespace Turso.Tests; public class TursoTests { [Test] public void TestSimpleQuery() { using var connection = new TursoConnection(); connection.Open(); using var cmd = new TursoCommand(connection, "SELECT * FROM generate_series(1,2,1)"); using var reader = cmd.ExecuteReader(); reader.Read().Should().BeTrue(); reader.GetInt32(0).Should().Be(1); reader.Read().Should().BeTrue(); reader.GetInt32(0).Should().Be(2); reader.Read().Should().BeFalse(); } [Test] public void TestPrepareStatement() { using var connection = new TursoConnection(); connection.Open(); using var cmd = new TursoCommand(connection, "SELECT * FROM generate_series(?,?,?)"); cmd.Parameters.Add(1); cmd.Parameters.Add(2); cmd.Parameters.Add(1); cmd.Prepare(); using var reader = cmd.ExecuteReader(); reader.Read().Should().BeTrue(); reader.GetInt32(0).Should().Be(1); reader.Read().Should().BeTrue(); reader.GetInt32(0).Should().Be(2); reader.Read().Should().BeFalse(); } [TestCase("stringValue", TestName = "TestStringValue")] [TestCase(new byte[] { 1, 2, 3, 4, 5 }, TestName = "TestBlobValue")] [TestCase(1, TestName = "TestIntValue")] [TestCase(2.5, TestName = "TestRealValue")] public void TestDifferentTypes(object typedValue) { using var connection = new TursoConnection(); connection.Open(); using (var create = new TursoCommand(connection, "CREATE TABLE t(v)")) { create.ExecuteNonQuery().Should().Be(0); } using (var insert = new TursoCommand(connection, "INSERT INTO t VALUES (?)")) { insert.Parameters.Add(typedValue); insert.ExecuteNonQuery().Should().Be(1); } using var select = new TursoCommand(connection, "SELECT v FROM t"); using var reader = select.ExecuteReader(); reader.Read().Should().BeTrue(); switch (typedValue) { case string s: reader.GetString(0).Should().Be(s); break; case byte[] bytes: ((byte[])reader.GetValue(0)).SequenceEqual(bytes).Should().BeTrue(); break; case int i: reader.GetInt32(0).Should().Be(i); break; case double d: reader.GetDouble(0).Should().Be(d); break; default: throw new AssertionException($"Unsupported test type: {typedValue.GetType()}"); } reader.Read().Should().BeFalse(); } [Test] public void TestBindNamedParameter() { using var connection = new TursoConnection(); connection.Open(); using var cmd = new TursoCommand(connection, "SELECT * FROM generate_series(?,:stop,?1)"); cmd.Parameters.Add(1); cmd.Parameters.AddWithValue(":stop", 2); cmd.Prepare(); using var reader = cmd.ExecuteReader(); reader.Read().Should().BeTrue(); reader.GetInt32(0).Should().Be(1); reader.Read().Should().BeTrue(); reader.GetInt32(0).Should().Be(2); reader.Read().Should().BeFalse(); } [Test] public void TestInsertData() { using var connection = new TursoConnection(); connection.Open(); using var create = new TursoCommand(connection, "CREATE TABLE t(id INTEGER, name TEXT)"); create.ExecuteNonQuery().Should().Be(0); using var insert = new TursoCommand(connection, "INSERT INTO t(id, name) VALUES (1, 'alice'), (2, 'bob')"); insert.ExecuteNonQuery().Should().Be(2); using var countCmd = new TursoCommand(connection, "SELECT COUNT(*) FROM t"); using var reader = countCmd.ExecuteReader(); reader.Read().Should().BeTrue(); reader.GetInt32(0).Should().Be(2); reader.Read().Should().BeFalse(); } [Test] public void TestFetchSpecificColumns() { using var connection = new TursoConnection(); connection.Open(); using var create = new TursoCommand(connection, "CREATE TABLE t(id INTEGER, name TEXT, age INTEGER)"); create.ExecuteNonQuery().Should().Be(0); using var insert = new TursoCommand(connection, "INSERT INTO t VALUES (1,'alice',30),(2,'bob',40)"); insert.ExecuteNonQuery().Should().Be(2); using var select = new TursoCommand(connection, "SELECT name, age FROM t WHERE id = 2"); using var reader = select.ExecuteReader(); reader.Read().Should().BeTrue(); reader.GetString(0).Should().Be("bob"); reader.GetInt32(1).Should().Be(40); reader.Read().Should().BeFalse(); } [Test] public void TestQueryError() { using var connection = new TursoConnection(); connection.Open(); using var cmd = new TursoCommand(connection, "SELECT * FROM table_that_does_not_exist"); cmd.Invoking(x => x.ExecuteReader()).Should().Throw() .WithMessage("Unable to prepare statement: Parse error: no such table: table_that_does_not_exist"); } [Test] [Ignore("https://github.com/tursodatabase/turso/pull/3591")] public void TestCommitTransaction() { using var connection = new TursoConnection("Data Source=./turso.db"); connection.Open(); using var connection2 = new TursoConnection("Data Source=./turso.db"); connection2.Open(); connection.ExecuteNonQuery("CREATE TABLE IF NOT EXISTS t(id INTEGER)"); connection.ExecuteNonQuery("DELETE FROM t"); using var tx = connection.BeginTransaction(); using var insert = new TursoCommand(connection, "INSERT INTO t VALUES (1),(2)"); insert.ExecuteNonQuery().Should().Be(2); using var selectBefore = new TursoCommand(connection2, "SELECT COUNT(*) FROM t"); using (var readerBefore = selectBefore.ExecuteReader()) { readerBefore.Read().Should().BeTrue(); readerBefore.GetInt32(0).Should().Be(0); } tx.Commit(); using var selectAfter = new TursoCommand(connection2, "SELECT COUNT(*) FROM t"); using var readerAfter = selectAfter.ExecuteReader(); readerAfter.Read().Should().BeTrue(); readerAfter.GetInt32(0).Should().Be(2); } [Test] public void TestRollbackTransaction() { using var connection = new TursoConnection(); connection.Open(); using var create = new TursoCommand(connection, "CREATE TABLE t(id INTEGER)"); create.ExecuteNonQuery().Should().Be(0); using var tx = connection.BeginTransaction(); using var insert = new TursoCommand(connection, "INSERT INTO t VALUES (1),(2)"); insert.ExecuteNonQuery().Should().Be(2); using var select = new TursoCommand(connection, "SELECT COUNT(*) FROM t"); using var reader = select.ExecuteReader(); reader.Read().Should().BeTrue(); reader.GetInt32(0).Should().Be(2); tx.Rollback(); using var select2 = new TursoCommand(connection, "SELECT COUNT(*) FROM t"); using var reader2 = select2.ExecuteReader(); reader2.Read().Should().BeTrue(); reader2.GetInt32(0).Should().Be(0); } [Test] public void TestDataReaderEnumerable() { using var connection = new TursoConnection(); connection.Open(); using var create = new TursoCommand(connection, "CREATE TABLE t(id INTEGER, name TEXT, age INTEGER)"); create.ExecuteNonQuery().Should().Be(0); using var insert = new TursoCommand(connection, "INSERT INTO t VALUES (1,'alice',30),(2,'bob',40),(3,'charlie',50)"); insert.ExecuteNonQuery().Should().Be(3); using var select = new TursoCommand(connection, "SELECT id, name, age FROM t ORDER BY id"); using var reader = select.ExecuteReader(); var results = new List<(long id, string name, long age)>(); foreach (DbDataRecord record in reader) { var id = record.GetInt64(0); var name = record.GetString(1); var age = record.GetInt64(2); results.Add((id, name, age)); } results.Should().HaveCount(3); results[0].Should().Be((1, "alice", 30)); results[1].Should().Be((2, "bob", 40)); results[2].Should().Be((3, "charlie", 50)); } [Test] public void TestEncryption() { var tempPath = Path.Combine(Path.GetTempPath(), $"turso_test_encrypted_{Guid.NewGuid()}.db"); var hexkey = "b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327"; var wrongKey = "aaaaaaa4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327"; try { // Create encrypted database using (var connection = new TursoConnection($"Data Source={tempPath};Encryption Cipher=aegis256;Encryption Key={hexkey}")) { connection.Open(); using var create = new TursoCommand(connection, "CREATE TABLE t(x TEXT)"); create.ExecuteNonQuery(); using var insert = new TursoCommand(connection, "INSERT INTO t VALUES ('secret')"); insert.ExecuteNonQuery(); using var checkpoint = new TursoCommand(connection, "PRAGMA wal_checkpoint(truncate)"); checkpoint.ExecuteNonQuery(); } // Verify data is encrypted on disk var content = File.ReadAllBytes(tempPath); content.Length.Should().BeGreaterThan(1024); var contentStr = System.Text.Encoding.UTF8.GetString(content); contentStr.Should().NotContain("secret"); // Verify we can re-open with the same key using (var connection2 = new TursoConnection($"Data Source={tempPath};Encryption Cipher=aegis256;Encryption Key={hexkey}")) { connection2.Open(); using var select = new TursoCommand(connection2, "SELECT * FROM t"); using var reader = select.ExecuteReader(); reader.Read().Should().BeTrue(); reader.GetString(0).Should().Be("secret"); } // Verify opening with wrong key fails Action openWithWrongKey = () => { using var conn = new TursoConnection($"Data Source={tempPath};Encryption Cipher=aegis256;Encryption Key={wrongKey}"); conn.Open(); using var select = new TursoCommand(conn, "SELECT * FROM t"); using var reader = select.ExecuteReader(); reader.Read(); }; openWithWrongKey.Should().Throw(); // Verify opening without encryption fails Action openWithoutEncryption = () => { using var conn = new TursoConnection($"Data Source={tempPath}"); conn.Open(); using var select = new TursoCommand(conn, "SELECT * FROM t"); using var reader = select.ExecuteReader(); reader.Read(); }; openWithoutEncryption.Should().Throw(); } finally { if (File.Exists(tempPath)) File.Delete(tempPath); } } } ================================================ FILE: bindings/go/LICENSE.md ================================================ MIT License Copyright 2025 the Turso authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: bindings/go/README.md ================================================

Turso Database for Go

Go Reference

Chat with other users of Turso on Discord

--- ## About > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ## Features - **SQLite compatible:** SQLite query language and file format support ([status](https://github.com/tursodatabase/turso/blob/main/COMPAT.md)). - **In-process**: No network overhead, runs directly in your Go process - **Cross-platform**: Supports Linux, macOS, Windows - **Remote partial sync**: Bootstrap from a remote database, pull remote changes, and push local changes when online — all while enjoying a fully operational database offline. - **No CGO**: This driver uses the awesome [purego](https://github.com/ebitengine/purego) library to call C (in this case Rust with C ABI) functions from Go. ## Installation ```bash go get turso.tech/database/tursogo ``` ## Get Started ```go package main import ( "database/sql" "fmt" "os" _ "turso.tech/database/tursogo" ) func main() { conn, err := sql.Open("turso", ":memory:") if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } sql := "CREATE table go_turso (foo INTEGER, bar TEXT)" _, _ = conn.Exec(sql) sql = "INSERT INTO go_turso (foo, bar) values (?, ?)" stmt, _ := conn.Prepare(sql) defer stmt.Close() _, _ = stmt.Exec(42, "turso") rows, _ := conn.Query("SELECT * from go_turso") defer rows.Close() for rows.Next() { var a int var b string _ = rows.Scan(&a, &b) fmt.Printf("%d, %s\n", a, b) // 42, turso } } ``` ## Sync Driver Use a remote Turso database while working locally. You can bootstrap local state from the remote, pull remote changes, and push local commits. Note: You need a Turso remote URL. See the Turso docs for provisioning and authentication. ```go package main import ( "context" "fmt" "log" "os" turso "turso.tech/database/tursogo" ) func main() { ctx := context.Background() // Connect a local database to a remote Turso database db, err := turso.NewTursoSyncDb(ctx, turso.TursoSyncDbConfig{ Path: ":memory:", // local db path (or a file path) RemoteUrl: "https://..turso.io", AuthToken: "", }) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } conn, err := db.Connect(ctx) if err != nil { log.Fatal(err) } defer conn.Close() sql := "CREATE table go_turso (foo INTEGER, bar TEXT)" _, _ = conn.ExecContext(ctx, sql) sql = "INSERT INTO go_turso (foo, bar) values (?, ?)" stmt, _ := conn.PrepareContext(ctx, sql) defer stmt.Close() _, _ = stmt.ExecContext(ctx, 42, "turso") // Push local commits to remote _ = db.Push(ctx) // Pull new changes from remote into local _, _ = db.Pull(ctx) rows, _ := conn.QueryContext(ctx, "SELECT * from go_turso") defer rows.Close() for rows.Next() { var a int var b string _ = rows.Scan(&a, &b) fmt.Printf("%d, %s\n", a, b) // 42, turso } // Optional: inspect and manage sync state stats, err := db.Stats(ctx) if err != nil { log.Println("Stats unavailable:", err) } else { log.Println("Current revision:", stats.NetworkReceivedBytes) } _ = db.Checkpoint(ctx) // compact local WAL after many writes } ``` ## License This project is licensed under the [MIT license](../../LICENSE.md). ## Support - [GitHub Issues](https://github.com/tursodatabase/turso/issues) - [Documentation](https://docs.turso.tech) - [Discord Community](https://tur.so/discord) ================================================ FILE: bindings/go/VERSION ================================================ 6119c6f0 ================================================ FILE: bindings/go/bindings.go ================================================ package turso import ( "fmt" "sync" turso_libs "github.com/tursodatabase/turso-go-platform-libs" ) var initLibrary sync.Once func InitLibrary(strategy turso_libs.LoadTursoLibraryConfig) { initLibrary.Do(func() { library, err := turso_libs.LoadTursoLibrary(strategy) if err != nil { panic(fmt.Errorf("unable to load turso library: %w", err)) } registerTursoDb(library) registerTursoSync(library) }) } ================================================ FILE: bindings/go/bindings_db.go ================================================ package turso // import "github.com/tursodatabase/turso/go" import ( "errors" "fmt" "runtime" "unsafe" "github.com/ebitengine/purego" ) // package-level errors var ( ErrTursoBusy = errors.New("turso: database is busy") ErrTursoInterrupt = errors.New("turso: interrupted") ErrTursoGeneric = errors.New("turso: error") ErrTursoMisuse = errors.New("turso: API misuse") ErrTursoConstraint = errors.New("turso: constraint failed") ErrTursoReadOnly = errors.New("turso: database is readonly") ErrTursoDatabaseFull = errors.New("turso: database is full") ErrTursoNotADb = errors.New("turso: not a database") ErrTursoCorrupt = errors.New("turso: database is corrupt") ) // DefaultBusyTimeout is the default busy timeout in milliseconds (5 seconds). // This matches common SQLite production recommendations. Set _busy_timeout=-1 // in the DSN to disable the busy handler completely. const DefaultBusyTimeout = 5000 // define all necessary constants first type TursoStatusCode int32 const ( TURSO_OK TursoStatusCode = 0 TURSO_DONE TursoStatusCode = 1 TURSO_ROW TursoStatusCode = 2 TURSO_IO TursoStatusCode = 3 TURSO_BUSY TursoStatusCode = 4 TURSO_INTERRUPT TursoStatusCode = 5 TURSO_ERROR TursoStatusCode = 127 TURSO_MISUSE TursoStatusCode = 128 TURSO_CONSTRAINT TursoStatusCode = 129 TURSO_READONLY TursoStatusCode = 130 TURSO_DATABASE_FULL TursoStatusCode = 131 TURSO_NOTADB TursoStatusCode = 132 TURSO_CORRUPT TursoStatusCode = 133 ) type TursoType int32 const ( TURSO_TYPE_UNKNOWN TursoType = 0 TURSO_TYPE_INTEGER TursoType = 1 TURSO_TYPE_REAL TursoType = 2 TURSO_TYPE_TEXT TursoType = 3 TURSO_TYPE_BLOB TursoType = 4 TURSO_TYPE_NULL TursoType = 5 ) type TursoTracingLevel int32 const ( TURSO_TRACING_LEVEL_ERROR TursoTracingLevel = 1 TURSO_TRACING_LEVEL_WARN TursoTracingLevel = 2 TURSO_TRACING_LEVEL_INFO TursoTracingLevel = 3 TURSO_TRACING_LEVEL_DEBUG TursoTracingLevel = 4 TURSO_TRACING_LEVEL_TRACE TursoTracingLevel = 5 ) // define opaque pointers as-is and accept them as exact arguments type turso_database_t struct{} type turso_connection_t struct{} type turso_statement_t struct{} type TursoDatabase *turso_database_t type TursoConnection *turso_connection_t type TursoStatement *turso_statement_t // define all public binding types type TursoLog struct { Message string Target string File string Timestamp uint64 Line uint Level TursoTracingLevel } type TursoConfig struct { // Logger is an optional callback invoked by the library. Logger func(log TursoLog) LogLevel string // zero-terminated C string expected by C; wrapper converts } type TursoDatabaseEncryptionOpts struct { Cipher string Hexkey string } type TursoDatabaseConfig struct { // Path to the database file or ":memory:" Path string // Optional comma separated list of experimental features to enable ExperimentalFeatures string // Parameter which defines who drives the IO - callee or the caller AsyncIO bool // optional VFS parameter explicitly specifying FS backend for the database. // Available options are: // - "memory": in-memory backend // - "syscall": generic syscall backend // - "io_uring": IO uring (supported only on Linux) // - "experimental_win_iocp": Windows IOCP [experimental](supported only on Windows) Vfs string // optional encryption parameters // as encryption is experimental - ExperimentalFeatures must have "encryption" in the list Encryption TursoDatabaseEncryptionOpts // BusyTimeout in milliseconds (0 = no timeout, immediate SQLITE_BUSY) BusyTimeout int } // define all necessary private C structs type turso_slice_ref_t struct { ptr uintptr len uintptr } type turso_log_t struct { message uintptr // const char* target uintptr // const char* file uintptr // const char* timestamp uint64 line uint // size_t level int32 // turso_tracing_level_t } type turso_config_t struct { logger uintptr // void (*logger)(const turso_log_t *log) log_level uintptr } type turso_database_config_t struct { async_io uint64 // non-zero value interpreted as async IO path uintptr // const char* experimental_features uintptr // const char* or null vfs uintptr // const char* or null encryption_cipher uintptr // const char* or null encryption_hexkey uintptr // const char* or null } // C extern method types type turso_status_code_t = int32 // then, define C extern methods var ( c_turso_setup func(config *turso_config_t, error_opt_out **byte) turso_status_code_t c_turso_database_new func(config *turso_database_config_t, database **turso_database_t, error_opt_out **byte) turso_status_code_t c_turso_database_open func(database TursoDatabase, error_opt_out **byte) turso_status_code_t c_turso_database_connect func(self TursoDatabase, connection **turso_connection_t, error_opt_out **byte) turso_status_code_t c_turso_connection_get_autocommit func(self TursoConnection) bool c_turso_connection_set_busy_timeout_ms func(self TursoConnection, timeout_ms int64) c_turso_connection_last_insert_rowid func(self TursoConnection) int64 c_turso_connection_prepare_single func(self TursoConnection, sql string, statement **turso_statement_t, error_opt_out **byte) turso_status_code_t c_turso_connection_prepare_first func(self TursoConnection, sql string, statement **turso_statement_t, tail_idx *uintptr, error_opt_out **byte) turso_status_code_t c_turso_connection_close func(self TursoConnection, error_opt_out **byte) turso_status_code_t c_turso_statement_execute func(self TursoStatement, rows_changes *uint64, error_opt_out **byte) turso_status_code_t c_turso_statement_step func(self TursoStatement, error_opt_out **byte) turso_status_code_t c_turso_statement_run_io func(self TursoStatement, error_opt_out **byte) turso_status_code_t c_turso_statement_reset func(self TursoStatement, error_opt_out **byte) turso_status_code_t c_turso_statement_finalize func(self TursoStatement, error_opt_out **byte) turso_status_code_t c_turso_statement_n_change func(self TursoStatement) int64 c_turso_statement_column_count func(self TursoStatement) int64 c_turso_statement_column_name func(self TursoStatement, index uintptr) uintptr c_turso_statement_column_decltype func(self TursoStatement, index uintptr) uintptr c_turso_statement_row_value_kind func(self TursoStatement, index uintptr) int32 c_turso_statement_row_value_bytes_count func(self TursoStatement, index uintptr) int64 c_turso_statement_row_value_bytes_ptr func(self TursoStatement, index uintptr) uintptr c_turso_statement_row_value_int func(self TursoStatement, index uintptr) int64 c_turso_statement_row_value_double func(self TursoStatement, index uintptr) float64 c_turso_statement_named_position func(self TursoStatement, name string) int64 c_turso_statement_parameters_count func(self TursoStatement) int64 c_turso_statement_bind_positional_null func(self TursoStatement, position uintptr) turso_status_code_t c_turso_statement_bind_positional_int func(self TursoStatement, position uintptr, value int64) turso_status_code_t c_turso_statement_bind_positional_double func(self TursoStatement, position uintptr, value float64) turso_status_code_t c_turso_statement_bind_positional_blob func(self TursoStatement, position uintptr, ptr *byte, len uintptr) turso_status_code_t c_turso_statement_bind_positional_text func(self TursoStatement, position uintptr, ptr *byte, len uintptr) turso_status_code_t c_turso_str_deinit func(self uintptr) c_turso_database_deinit func(self TursoDatabase) c_turso_connection_deinit func(self TursoConnection) c_turso_statement_deinit func(self TursoStatement) ) // implement a function to register extern methods from loaded lib // DO NOT load lib - as it will be done externally func registerTursoDb(handle uintptr) error { purego.RegisterLibFunc(&c_turso_setup, handle, "turso_setup") purego.RegisterLibFunc(&c_turso_database_new, handle, "turso_database_new") purego.RegisterLibFunc(&c_turso_database_open, handle, "turso_database_open") purego.RegisterLibFunc(&c_turso_database_connect, handle, "turso_database_connect") purego.RegisterLibFunc(&c_turso_connection_get_autocommit, handle, "turso_connection_get_autocommit") purego.RegisterLibFunc(&c_turso_connection_set_busy_timeout_ms, handle, "turso_connection_set_busy_timeout_ms") purego.RegisterLibFunc(&c_turso_connection_last_insert_rowid, handle, "turso_connection_last_insert_rowid") purego.RegisterLibFunc(&c_turso_connection_prepare_single, handle, "turso_connection_prepare_single") purego.RegisterLibFunc(&c_turso_connection_prepare_first, handle, "turso_connection_prepare_first") purego.RegisterLibFunc(&c_turso_connection_close, handle, "turso_connection_close") purego.RegisterLibFunc(&c_turso_statement_execute, handle, "turso_statement_execute") purego.RegisterLibFunc(&c_turso_statement_step, handle, "turso_statement_step") purego.RegisterLibFunc(&c_turso_statement_run_io, handle, "turso_statement_run_io") purego.RegisterLibFunc(&c_turso_statement_reset, handle, "turso_statement_reset") purego.RegisterLibFunc(&c_turso_statement_finalize, handle, "turso_statement_finalize") purego.RegisterLibFunc(&c_turso_statement_n_change, handle, "turso_statement_n_change") purego.RegisterLibFunc(&c_turso_statement_column_count, handle, "turso_statement_column_count") purego.RegisterLibFunc(&c_turso_statement_column_name, handle, "turso_statement_column_name") purego.RegisterLibFunc(&c_turso_statement_column_decltype, handle, "turso_statement_column_decltype") purego.RegisterLibFunc(&c_turso_statement_row_value_kind, handle, "turso_statement_row_value_kind") purego.RegisterLibFunc(&c_turso_statement_row_value_bytes_count, handle, "turso_statement_row_value_bytes_count") purego.RegisterLibFunc(&c_turso_statement_row_value_bytes_ptr, handle, "turso_statement_row_value_bytes_ptr") purego.RegisterLibFunc(&c_turso_statement_row_value_int, handle, "turso_statement_row_value_int") purego.RegisterLibFunc(&c_turso_statement_row_value_double, handle, "turso_statement_row_value_double") purego.RegisterLibFunc(&c_turso_statement_named_position, handle, "turso_statement_named_position") purego.RegisterLibFunc(&c_turso_statement_parameters_count, handle, "turso_statement_parameters_count") purego.RegisterLibFunc(&c_turso_statement_bind_positional_null, handle, "turso_statement_bind_positional_null") purego.RegisterLibFunc(&c_turso_statement_bind_positional_int, handle, "turso_statement_bind_positional_int") purego.RegisterLibFunc(&c_turso_statement_bind_positional_double, handle, "turso_statement_bind_positional_double") purego.RegisterLibFunc(&c_turso_statement_bind_positional_blob, handle, "turso_statement_bind_positional_blob") purego.RegisterLibFunc(&c_turso_statement_bind_positional_text, handle, "turso_statement_bind_positional_text") purego.RegisterLibFunc(&c_turso_str_deinit, handle, "turso_str_deinit") purego.RegisterLibFunc(&c_turso_database_deinit, handle, "turso_database_deinit") purego.RegisterLibFunc(&c_turso_connection_deinit, handle, "turso_connection_deinit") purego.RegisterLibFunc(&c_turso_statement_deinit, handle, "turso_statement_deinit") return nil } // Helper: map status code to default error kind func statusToError(status TursoStatusCode, msg string) error { var base error switch status { case TURSO_BUSY: base = ErrTursoBusy case TURSO_INTERRUPT: base = ErrTursoInterrupt case TURSO_ERROR: base = ErrTursoGeneric case TURSO_MISUSE: base = ErrTursoMisuse case TURSO_CONSTRAINT: base = ErrTursoConstraint case TURSO_READONLY: base = ErrTursoReadOnly case TURSO_DATABASE_FULL: base = ErrTursoDatabaseFull case TURSO_NOTADB: base = ErrTursoNotADb case TURSO_CORRUPT: base = ErrTursoCorrupt default: // for unknown error codes, fallback to generic base = ErrTursoGeneric } if msg != "" { return fmt.Errorf("%w: %s", base, msg) } return base } func decodeAndFreeCString(p *byte) string { return decodeAndFreeCStringRaw(uintptr(unsafe.Pointer(p))) } func decodeAndFreeCStringRaw(p uintptr) string { if p == 0 { return "" } // determine length var n uintptr for { b := *(*byte)(unsafe.Pointer(p + n)) if b == 0 { break } n++ } s := string(unsafe.Slice((*byte)(unsafe.Pointer(p)), n)) // free C-allocated string c_turso_str_deinit(p) return s } func decodeCStringNoFree(p uintptr) string { if p == 0 { return "" } cur := (*byte)(unsafe.Pointer(p)) // determine length var n uintptr for { b := *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(cur)) + n)) if b == 0 { break } n++ } return string(unsafe.Slice(cur, n)) } func makeCStringBytes(s string) ([]byte, uintptr) { if s == "" { return nil, 0 } b := make([]byte, 0, len(s)+1) b = append(b, s...) b = append(b, 0) return b, uintptr(unsafe.Pointer(&b[0])) } // ------- Logger callback plumbing -------- var ( loggerCallback uintptr loggerHandler func(TursoLog) ) func init() { loggerCallback = purego.NewCallback(func(p uintptr) uintptr { if p == 0 || loggerHandler == nil { return 0 } cl := (*turso_log_t)(unsafe.Pointer(p)) log := TursoLog{ Message: decodeCStringNoFree(cl.message), Target: decodeCStringNoFree(cl.target), File: decodeCStringNoFree(cl.file), Timestamp: cl.timestamp, Line: uint(cl.line), Level: TursoTracingLevel(cl.level), } // SAFETY: strings are copied above; no freeing needed. loggerHandler(log) return 0 }) } // Go wrappers over imported C bindings // turso_setup sets up global database info. func turso_setup(config TursoConfig) error { var cconf turso_config_t // Set logger callback if config.Logger != nil { loggerHandler = config.Logger cconf.logger = loggerCallback } // Log level C-string pointer var levelBytes []byte levelBytes, cconf.log_level = makeCStringBytes(config.LogLevel) var errPtr *byte status := c_turso_setup(&cconf, &errPtr) // Keep Go memory alive during C call runtime.KeepAlive(levelBytes) if status == int32(TURSO_OK) { return nil } msg := decodeAndFreeCString(errPtr) return statusToError(TursoStatusCode(status), msg) } // turso_database_new creates database holder but do not open it. func turso_database_new(config TursoDatabaseConfig) (TursoDatabase, error) { var cconf turso_database_config_t var pathBytes []byte var expBytes []byte var vfsBytes []byte var encryptionCipherBytes []byte var encryptionHexkeyBytes []byte pathBytes, cconf.path = makeCStringBytes(config.Path) if config.ExperimentalFeatures != "" { expBytes, cconf.experimental_features = makeCStringBytes(config.ExperimentalFeatures) } if config.Vfs != "" { vfsBytes, cconf.vfs = makeCStringBytes(config.Vfs) } if config.Encryption.Cipher != "" { encryptionCipherBytes, cconf.encryption_cipher = makeCStringBytes(config.Encryption.Cipher) } if config.Encryption.Hexkey != "" { encryptionHexkeyBytes, cconf.encryption_hexkey = makeCStringBytes(config.Encryption.Hexkey) } cconf.async_io = 0 if config.AsyncIO { cconf.async_io = 1 } var db *turso_database_t var errPtr *byte status := c_turso_database_new(&cconf, &db, &errPtr) runtime.KeepAlive(pathBytes) runtime.KeepAlive(expBytes) runtime.KeepAlive(vfsBytes) runtime.KeepAlive(encryptionCipherBytes) runtime.KeepAlive(encryptionHexkeyBytes) if status == int32(TURSO_OK) { return TursoDatabase(db), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_database_open opens the database. func turso_database_open(database TursoDatabase) error { var errPtr *byte status := c_turso_database_open(database, &errPtr) if status == int32(TURSO_OK) { return nil } msg := decodeAndFreeCString(errPtr) return statusToError(TursoStatusCode(status), msg) } // turso_database_connect connects to the database and returns a connection. func turso_database_connect(self TursoDatabase) (TursoConnection, error) { var conn *turso_connection_t var errPtr *byte status := c_turso_database_connect(self, &conn, &errPtr) if status == int32(TURSO_OK) { return TursoConnection(conn), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_connection_get_autocommit returns the autocommit state of the connection. func turso_connection_get_autocommit(self TursoConnection) bool { return c_turso_connection_get_autocommit(self) } // turso_connection_set_busy_timeout_ms sets busy timeout for the connection func turso_connection_set_busy_timeout_ms(self TursoConnection, timeoutMs int64) { c_turso_connection_set_busy_timeout_ms(self, timeoutMs) } // turso_connection_last_insert_rowid returns last insert rowid. func turso_connection_last_insert_rowid(self TursoConnection) int64 { return c_turso_connection_last_insert_rowid(self) } // turso_connection_prepare_single prepares a single statement in a connection. func turso_connection_prepare_single(self TursoConnection, sql string) (TursoStatement, error) { var stmt *turso_statement_t var errPtr *byte status := c_turso_connection_prepare_single(self, sql, &stmt, &errPtr) if status == int32(TURSO_OK) { return TursoStatement(stmt), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_connection_prepare_first prepares the first statement from a string containing multiple statements. func turso_connection_prepare_first(self TursoConnection, sql string) (TursoStatement, int, error) { var stmt *turso_statement_t var tail uintptr var errPtr *byte status := c_turso_connection_prepare_first(self, sql, &stmt, &tail, &errPtr) if status == int32(TURSO_OK) { return TursoStatement(stmt), int(tail), nil } msg := decodeAndFreeCString(errPtr) return nil, 0, statusToError(TursoStatusCode(status), msg) } // turso_connection_close closes the connection preventing any further operations. func turso_connection_close(self TursoConnection) error { var errPtr *byte status := c_turso_connection_close(self, &errPtr) if status == int32(TURSO_OK) { return nil } msg := decodeAndFreeCString(errPtr) return statusToError(TursoStatusCode(status), msg) } // turso_statement_execute executes a single statement // * execute returns TURSO_DONE if execution completed // * execute returns TURSO_IO if async_io was set and execution needs IO in order to make progress func turso_statement_execute(self TursoStatement) (TursoStatusCode, uint64, error) { var changes uint64 var errPtr *byte status := c_turso_statement_execute(self, &changes, &errPtr) switch TursoStatusCode(status) { case TURSO_OK, TURSO_DONE, TURSO_ROW, TURSO_IO: return TursoStatusCode(status), changes, nil default: msg := decodeAndFreeCString(errPtr) return TursoStatusCode(status), 0, statusToError(TursoStatusCode(status), msg) } } // turso_statement_step steps statement execution once. Returns DONE, ROW, IO, or error. func turso_statement_step(self TursoStatement) (TursoStatusCode, error) { var errPtr *byte status := c_turso_statement_step(self, &errPtr) switch TursoStatusCode(status) { case TURSO_OK, TURSO_DONE, TURSO_ROW, TURSO_IO: return TursoStatusCode(status), nil default: msg := decodeAndFreeCString(errPtr) return TursoStatusCode(status), statusToError(TursoStatusCode(status), msg) } } // turso_statement_run_io executes one iteration of underlying IO backend after TURSO_IO. func turso_statement_run_io(self TursoStatement) error { var errPtr *byte status := c_turso_statement_run_io(self, &errPtr) if status == int32(TURSO_OK) { return nil } msg := decodeAndFreeCString(errPtr) return statusToError(TursoStatusCode(status), msg) } // turso_statement_reset resets a statement. // this method must be called in order to cleanup statement resources and prepare it for re-execution // any pending execution will be aborted - be careful and in certain cases ensure that turso_statement_finalize called before turso_statement_reset func turso_statement_reset(self TursoStatement) error { var errPtr *byte status := c_turso_statement_reset(self, &errPtr) if status == int32(TURSO_OK) { return nil } msg := decodeAndFreeCString(errPtr) return statusToError(TursoStatusCode(status), msg) } // turso_statement_finalize finalizes a statement. func turso_statement_finalize(self TursoStatement) error { var errPtr *byte status := c_turso_statement_finalize(self, &errPtr) if status == int32(TURSO_OK) { return nil } msg := decodeAndFreeCString(errPtr) return statusToError(TursoStatusCode(status), msg) } // turso_statement_n_change returns amount of row modifications (insert/delete operations) made by the most recent executed statement. func turso_statement_n_change(self TursoStatement) int64 { return c_turso_statement_n_change(self) } // turso_statement_column_count returns the number of columns. func turso_statement_column_count(self TursoStatement) int64 { return c_turso_statement_column_count(self) } // turso_statement_column_name returns the column name at the index. // The underlying C string is freed automatically. func turso_statement_column_name(self TursoStatement, index int) string { ptr := c_turso_statement_column_name(self, uintptr(index)) return decodeAndFreeCStringRaw(ptr) } // turso_statement_column_decltype returns the column declared type at the index // (e.g. "INTEGER", "TEXT", "DATETIME", etc.). Returns empty string if not available. // The underlying C string is freed automatically. func turso_statement_column_decltype(self TursoStatement, index int) string { ptr := c_turso_statement_column_decltype(self, uintptr(index)) if ptr == 0 { return "" } return decodeAndFreeCStringRaw(ptr) } // turso_statement_row_value_kind returns the row value kind at index. func turso_statement_row_value_kind(self TursoStatement, index int) TursoType { return TursoType(c_turso_statement_row_value_kind(self, uintptr(index))) } // turso_statement_row_value_bytes_count returns number of bytes for BLOB or TEXT, -1 otherwise. func turso_statement_row_value_bytes_count(self TursoStatement, index int) int64 { return c_turso_statement_row_value_bytes_count(self, uintptr(index)) } // turso_statement_row_value_bytes_ptr returns pointer to start of BLOB/TEXT slice, or nil otherwise. func turso_statement_row_value_bytes_ptr(self TursoStatement, index int) uintptr { return c_turso_statement_row_value_bytes_ptr(self, uintptr(index)) } // turso_statement_row_value_int returns INTEGER value at index, or 0 otherwise. func turso_statement_row_value_int(self TursoStatement, index int) int64 { return c_turso_statement_row_value_int(self, uintptr(index)) } // turso_statement_row_value_double returns REAL value at index, or 0 otherwise. func turso_statement_row_value_double(self TursoStatement, index int) float64 { return c_turso_statement_row_value_double(self, uintptr(index)) } // turso_statement_named_position returns named argument position in a statement. func turso_statement_named_position(self TursoStatement, name string) int64 { return c_turso_statement_named_position(self, name) } // turso_statement_parameters_count returns parameters count for the statement. func turso_statement_parameters_count(self TursoStatement) int64 { return c_turso_statement_parameters_count(self) } // turso_statement_bind_positional_null binds a positional argument as NULL. func turso_statement_bind_positional_null(self TursoStatement, position int) error { status := c_turso_statement_bind_positional_null(self, uintptr(position)) if status == int32(TURSO_OK) { return nil } return statusToError(TursoStatusCode(status), "") } // turso_statement_bind_positional_int binds a positional argument as INTEGER. func turso_statement_bind_positional_int(self TursoStatement, position int, value int64) error { status := c_turso_statement_bind_positional_int(self, uintptr(position), value) if status == int32(TURSO_OK) { return nil } return statusToError(TursoStatusCode(status), "") } // turso_statement_bind_positional_double binds a positional argument as REAL. func turso_statement_bind_positional_double(self TursoStatement, position int, value float64) error { status := c_turso_statement_bind_positional_double(self, uintptr(position), value) if status == int32(TURSO_OK) { return nil } return statusToError(TursoStatusCode(status), "") } // turso_statement_bind_positional_blob binds a positional argument as BLOB. func turso_statement_bind_positional_blob(self TursoStatement, position int, value []byte) error { var ptr *byte var length uintptr if len(value) > 0 { ptr = &value[0] length = uintptr(len(value)) } status := c_turso_statement_bind_positional_blob(self, uintptr(position), ptr, length) if status == int32(TURSO_OK) { return nil } return statusToError(TursoStatusCode(status), "") } // turso_statement_bind_positional_text binds a positional argument as TEXT. // Note: underlying C API expects a pointer and length, not a zero-terminated string. func turso_statement_bind_positional_text(self TursoStatement, position int, value string) error { var ptr *byte var length uintptr if value != "" { // Point directly to string data; valid for the duration of the call. ptr = (*byte)(unsafe.Pointer(unsafe.StringData(value))) length = uintptr(len(value)) } status := c_turso_statement_bind_positional_text(self, uintptr(position), ptr, length) if status == int32(TURSO_OK) { return nil } return statusToError(TursoStatusCode(status), "") } // turso_database_deinit deallocates and closes a database. // SAFETY: caller must ensure that no other code can concurrently or later call methods over deinited database. func turso_database_deinit(self TursoDatabase) { c_turso_database_deinit(self) } // turso_connection_deinit deallocates and closes a connection. // SAFETY: caller must ensure that no other code can concurrently or later call methods over deinited connection. func turso_connection_deinit(self TursoConnection) { c_turso_connection_deinit(self) } // turso_statement_deinit deallocates and closes a statement. // SAFETY: caller must ensure that no other code can concurrently or later call methods over deinited statement. func turso_statement_deinit(self TursoStatement) { c_turso_statement_deinit(self) } // Additional ergonomic helpers (the only non-direct translations): // turso_statement_row_value_bytes returns a copy of bytes for BLOB or TEXT values, nil otherwise. func turso_statement_row_value_bytes(self TursoStatement, index int) []byte { n := c_turso_statement_row_value_bytes_count(self, uintptr(index)) if n <= 0 { return nil } ptr := c_turso_statement_row_value_bytes_ptr(self, uintptr(index)) if ptr == 0 { return nil } src := unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n) dst := make([]byte, n) copy(dst, src) return dst } // turso_statement_row_value_text returns a copy of text for TEXT values, "" otherwise. func turso_statement_row_value_text(self TursoStatement, index int) string { n := c_turso_statement_row_value_bytes_count(self, uintptr(index)) if n <= 0 { return "" } ptr := c_turso_statement_row_value_bytes_ptr(self, uintptr(index)) if ptr == 0 { return "" } bs := unsafe.Slice((*byte)(unsafe.Pointer(ptr)), n) // converting []byte to string copies return string(bs) } ================================================ FILE: bindings/go/bindings_db_test.go ================================================ package turso import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type dbConn struct { db TursoDatabase conn TursoConnection } func openInMemory(t *testing.T) (*dbConn, func()) { t.Helper() db, err := turso_database_new(TursoDatabaseConfig{ Path: ":memory:", ExperimentalFeatures: "", AsyncIO: false, }) require.NoError(t, err) require.NotNil(t, db) require.NoError(t, turso_database_open(db)) conn, err := turso_database_connect(db) require.NoError(t, err) require.NotNil(t, conn) cleanup := func() { _ = turso_connection_close(conn) turso_connection_deinit(conn) turso_database_deinit(db) } return &dbConn{db: db, conn: conn}, cleanup } func prepExec(t *testing.T, conn TursoConnection, sql string) uint64 { t.Helper() stmt, err := turso_connection_prepare_single(conn, sql) require.NoError(t, err) defer func() { _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) }() _, changes, err := turso_statement_execute(stmt) require.NoError(t, err) return changes } func prepStmt(t *testing.T, conn TursoConnection, sql string) TursoStatement { t.Helper() stmt, err := turso_connection_prepare_single(conn, sql) require.NoError(t, err) return stmt } func stepRow(t *testing.T, stmt TursoStatement) bool { t.Helper() code, err := turso_statement_step(stmt) require.NoError(t, err) if code == TURSO_ROW { return true } require.Equal(t, TURSO_DONE, code) return false } func TestSetupAndOpenMemory(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() ac := turso_connection_get_autocommit(conn.conn) assert.True(t, ac, "autocommit should be true for new connection") // simple sanity DDL changes := prepExec(t, conn.conn, "CREATE TABLE t(id INTEGER PRIMARY KEY, a INTEGER)") assert.Equal(t, uint64(0), changes) } func TestPrepareFirstMultipleStatements(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() sql := "CREATE TABLE t(a INT); INSERT INTO t(a) VALUES(1); SELECT a FROM t;" start := 0 for { sub := sql[start:] stmt, tail, err := turso_connection_prepare_first(conn.conn, sub) require.NoError(t, err) if stmt == nil { break } if tail <= 0 { break } // Execute or step depending on statement type colCount := turso_statement_column_count(stmt) if colCount == 0 { _, _, err := turso_statement_execute(stmt) require.NoError(t, err) } else { require.True(t, stepRow(t, stmt)) v := turso_statement_row_value_int(stmt, 0) assert.Equal(t, int64(1), v) require.False(t, stepRow(t, stmt)) } _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) start += tail } // Verify result stmt := prepStmt(t, conn.conn, "SELECT a FROM t") defer func() { _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) }() require.True(t, stepRow(t, stmt)) assert.Equal(t, int64(1), turso_statement_row_value_int(stmt, 0)) require.False(t, stepRow(t, stmt)) } func TestInsertReturningMultiplePartialFetchCommits(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() prepExec(t, conn.conn, "CREATE TABLE t(a INT)") stmt := prepStmt(t, conn.conn, "INSERT INTO t(a) VALUES (1),(2) RETURNING a") require.True(t, stepRow(t, stmt)) first := turso_statement_row_value_int(stmt, 0) assert.Equal(t, int64(1), first) // Do not consume all rows; finalize early _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) // Ensure both rows were inserted stmt2 := prepStmt(t, conn.conn, "SELECT COUNT(*) FROM t") defer func() { _ = turso_statement_finalize(stmt2) turso_statement_deinit(stmt2) }() require.True(t, stepRow(t, stmt2)) cnt := turso_statement_row_value_int(stmt2, 0) assert.Equal(t, int64(2), cnt) require.False(t, stepRow(t, stmt2)) } func TestInsertReturningWithExplicitTransactionAndPartialFetch(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() prepExec(t, conn.conn, "CREATE TABLE t(a INT)") prepExec(t, conn.conn, "BEGIN") stmt := prepStmt(t, conn.conn, "INSERT INTO t(a) VALUES (10),(20) RETURNING a") require.True(t, stepRow(t, stmt)) v := turso_statement_row_value_int(stmt, 0) assert.Equal(t, int64(10), v) // finalize without consuming all rows _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) // Commit should still succeed prepExec(t, conn.conn, "COMMIT") // Verify data stmt2 := prepStmt(t, conn.conn, "SELECT COUNT(*) FROM t") defer func() { _ = turso_statement_finalize(stmt2) turso_statement_deinit(stmt2) }() require.True(t, stepRow(t, stmt2)) assert.Equal(t, int64(2), turso_statement_row_value_int(stmt2, 0)) } func TestOnConflictDoNothingReturning(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() prepExec(t, conn.conn, "CREATE TABLE t(a INT PRIMARY KEY)") prepExec(t, conn.conn, "INSERT INTO t(a) VALUES(1)") stmt := prepStmt(t, conn.conn, "INSERT INTO t(a) VALUES(1) ON CONFLICT(a) DO NOTHING RETURNING a") defer func() { _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) }() // Should produce no rows and be done code, err := turso_statement_step(stmt) require.NoError(t, err) assert.Equal(t, TURSO_DONE, code) // Ensure count unchanged stmt2 := prepStmt(t, conn.conn, "SELECT COUNT(*) FROM t") defer func() { _ = turso_statement_finalize(stmt2) turso_statement_deinit(stmt2) }() require.True(t, stepRow(t, stmt2)) assert.Equal(t, int64(1), turso_statement_row_value_int(stmt2, 0)) } func TestSubqueries(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() prepExec(t, conn.conn, "CREATE TABLE t(a INT)") prepExec(t, conn.conn, "INSERT INTO t(a) VALUES (1),(2),(3),(4)") stmt := prepStmt(t, conn.conn, "SELECT a FROM (SELECT a FROM t WHERE a > 1) WHERE a < 4 ORDER BY a") defer func() { _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) }() var got []int64 for { if !stepRow(t, stmt) { break } got = append(got, turso_statement_row_value_int(stmt, 0)) } assert.Equal(t, []int64{2, 3}, got) } func TestJoin(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() prepExec(t, conn.conn, "CREATE TABLE t1(id INT PRIMARY KEY, name TEXT)") prepExec(t, conn.conn, "CREATE TABLE t2(id INT PRIMARY KEY, age INT)") prepExec(t, conn.conn, "INSERT INTO t1(id, name) VALUES (1,'a'),(2,'b'),(3,'c')") prepExec(t, conn.conn, "INSERT INTO t2(id, age) VALUES (1,10),(3,30)") stmt := prepStmt(t, conn.conn, "SELECT t1.id, t1.name, t2.age FROM t1 JOIN t2 ON t1.id = t2.id ORDER BY t1.id") defer func() { _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) }() var rows [][3]int64 var names []string for { if !stepRow(t, stmt) { break } id := turso_statement_row_value_int(stmt, 0) // name as TEXT name := turso_statement_row_value_text(stmt, 1) age := turso_statement_row_value_int(stmt, 2) rows = append(rows, [3]int64{id, int64(len(name)), age}) names = append(names, name) } assert.Equal(t, [][3]int64{{1, 1, 10}, {3, 1, 30}}, rows) assert.Equal(t, []string{"a", "c"}, names) } func TestAlterTable(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() prepExec(t, conn.conn, "CREATE TABLE t(id INT PRIMARY KEY)") prepExec(t, conn.conn, "ALTER TABLE t ADD COLUMN name TEXT") // Insert with new column present prepExec(t, conn.conn, "INSERT INTO t(id, name) VALUES(1, 'hello')") stmt := prepStmt(t, conn.conn, "SELECT name FROM t WHERE id = 1") defer func() { _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) }() require.True(t, stepRow(t, stmt)) assert.Equal(t, "hello", turso_statement_row_value_text(stmt, 0)) require.False(t, stepRow(t, stmt)) } func TestGenerateSeries(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() stmt := prepStmt(t, conn.conn, "SELECT value FROM generate_series(1,5)") defer func() { _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) }() var got []int64 for { if !stepRow(t, stmt) { break } got = append(got, turso_statement_row_value_int(stmt, 0)) } assert.Equal(t, []int64{1, 2, 3, 4, 5}, got) } func TestJSONFunctionsBindings(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() stmt := prepStmt(t, conn.conn, "SELECT json_extract('{\"x\": [1,2,3]}', '$.x[1]'), json_array_length('[1,2,3]')") defer func() { _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) }() require.True(t, stepRow(t, stmt)) kind0 := turso_statement_row_value_kind(stmt, 0) kind1 := turso_statement_row_value_kind(stmt, 1) assert.Equal(t, TURSO_TYPE_INTEGER, kind0) assert.Equal(t, TURSO_TYPE_INTEGER, kind1) assert.Equal(t, int64(2), turso_statement_row_value_int(stmt, 0)) assert.Equal(t, int64(3), turso_statement_row_value_int(stmt, 1)) require.False(t, stepRow(t, stmt)) } func TestBindingsPositionalAndNamed(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() prepExec(t, conn.conn, "CREATE TABLE t(i INTEGER, r REAL, s TEXT, b BLOB, n NULL)") // Positional parameters: ?1..? stmt := prepStmt(t, conn.conn, "INSERT INTO t(i,r,s,b,n) VALUES (?1,?2,?3,?4,?5)") require.NoError(t, turso_statement_bind_positional_int(stmt, 1, 42)) require.NoError(t, turso_statement_bind_positional_double(stmt, 2, 3.14)) require.NoError(t, turso_statement_bind_positional_text(stmt, 3, "hello")) require.NoError(t, turso_statement_bind_positional_blob(stmt, 4, []byte{0xde, 0xad, 0xbe, 0xef})) require.NoError(t, turso_statement_bind_positional_null(stmt, 5)) _, _, err := turso_statement_execute(stmt) require.NoError(t, err) _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) // Named parameters mapped to positional via named_position stmt2 := prepStmt(t, conn.conn, "INSERT INTO t(i,r,s,b,n) VALUES (:i,:r,:s,:b,:n)") defer func() { _ = turso_statement_finalize(stmt2) turso_statement_deinit(stmt2) }() posI := turso_statement_named_position(stmt2, ":i") posR := turso_statement_named_position(stmt2, ":r") posS := turso_statement_named_position(stmt2, ":s") posB := turso_statement_named_position(stmt2, ":b") posN := turso_statement_named_position(stmt2, ":n") require.Equal(t, posI, int64(1)) require.Equal(t, posR, int64(2)) require.Equal(t, posS, int64(3)) require.Equal(t, posB, int64(4)) require.Equal(t, posN, int64(5)) require.NoError(t, turso_statement_bind_positional_int(stmt2, int(posI), 7)) require.NoError(t, turso_statement_bind_positional_double(stmt2, int(posR), -1.5)) require.NoError(t, turso_statement_bind_positional_text(stmt2, int(posS), "world")) require.NoError(t, turso_statement_bind_positional_blob(stmt2, int(posB), []byte{})) // empty blob require.NoError(t, turso_statement_bind_positional_null(stmt2, int(posN))) _, _, err = turso_statement_execute(stmt2) require.NoError(t, err) // Verify retrieved values using row value helpers stmt3 := prepStmt(t, conn.conn, "SELECT i,r,s,b,n FROM t") defer func() { _ = turso_statement_finalize(stmt3) turso_statement_deinit(stmt3) }() // first row require.True(t, stepRow(t, stmt3)) assert.Equal(t, TURSO_TYPE_INTEGER, turso_statement_row_value_kind(stmt3, 0)) assert.Equal(t, int64(42), turso_statement_row_value_int(stmt3, 0)) assert.Equal(t, TURSO_TYPE_REAL, turso_statement_row_value_kind(stmt3, 1)) assert.InDelta(t, 3.14, turso_statement_row_value_double(stmt3, 1), 1e-9) assert.Equal(t, TURSO_TYPE_TEXT, turso_statement_row_value_kind(stmt3, 2)) assert.Equal(t, "hello", turso_statement_row_value_text(stmt3, 2)) assert.Equal(t, TURSO_TYPE_BLOB, turso_statement_row_value_kind(stmt3, 3)) assert.Equal(t, []byte{0xde, 0xad, 0xbe, 0xef}, turso_statement_row_value_bytes(stmt3, 3)) assert.Equal(t, TURSO_TYPE_NULL, turso_statement_row_value_kind(stmt3, 4)) // second row require.True(t, stepRow(t, stmt3)) assert.Equal(t, int64(7), turso_statement_row_value_int(stmt3, 0)) assert.InDelta(t, -1.5, turso_statement_row_value_double(stmt3, 1), 1e-9) assert.Equal(t, "world", turso_statement_row_value_text(stmt3, 2)) // empty blob assert.Nil(t, turso_statement_row_value_bytes(stmt3, 3)) require.False(t, stepRow(t, stmt3)) } func TestColumnMetadata(t *testing.T) { conn, cleanup := openInMemory(t) defer cleanup() prepExec(t, conn.conn, "CREATE TABLE t(id INT, name TEXT)") prepExec(t, conn.conn, "INSERT INTO t(id, name) VALUES (1, 'alice')") stmt := prepStmt(t, conn.conn, "SELECT id, name FROM t") defer func() { _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) }() cc := turso_statement_column_count(stmt) require.Equal(t, int64(2), cc) n0 := turso_statement_column_name(stmt, 0) n1 := turso_statement_column_name(stmt, 1) assert.Equal(t, "id", n0) assert.Equal(t, "name", n1) require.True(t, stepRow(t, stmt)) assert.Equal(t, int64(1), turso_statement_row_value_int(stmt, 0)) assert.Equal(t, "alice", turso_statement_row_value_text(stmt, 1)) require.False(t, stepRow(t, stmt)) } ================================================ FILE: bindings/go/bindings_sync.go ================================================ package turso // import "github.com/tursodatabase/turso/go" import ( "runtime" "unsafe" "github.com/ebitengine/purego" ) // ------------- Opaque types for sync engine ------------- type turso_sync_database_t struct{} type turso_sync_operation_t struct{} type turso_sync_io_item_t struct{} type turso_sync_changes_t struct{} type TursoSyncDatabase *turso_sync_database_t type TursoSyncOperation *turso_sync_operation_t type TursoSyncIoItem *turso_sync_io_item_t type TursoSyncChanges *turso_sync_changes_t // ------------- Enums ------------- type TursoSyncIoRequestType int32 const ( TURSO_SYNC_IO_NONE TursoSyncIoRequestType = 0 TURSO_SYNC_IO_HTTP TursoSyncIoRequestType = 1 TURSO_SYNC_IO_FULL_READ TursoSyncIoRequestType = 2 TURSO_SYNC_IO_FULL_WRITE TursoSyncIoRequestType = 3 ) type TursoSyncOperationResultType int32 const ( TURSO_ASYNC_RESULT_NONE TursoSyncOperationResultType = 0 TURSO_ASYNC_RESULT_CONNECTION TursoSyncOperationResultType = 1 TURSO_ASYNC_RESULT_CHANGES TursoSyncOperationResultType = 2 TURSO_ASYNC_RESULT_STATS TursoSyncOperationResultType = 3 ) // ------------- Public binding types ------------- // TursoSyncDatabaseConfig describes database sync configuration. type TursoSyncDatabaseConfig struct { // Path to the main database file (auxiliary files will derive names from this path) Path string // optional remote url (libsql://..., https://... or http://...) // this URL will be saved in the database metadata file in order to be able to reuse it if later client will be constructed without explicit remote url RemoteUrl string // Namespace for remote host Namespace string // Arbitrary client name used as a prefix for unique client id ClientName string // Long poll timeout for pull method in milliseconds LongPollTimeoutMs int // Bootstrap db if empty; if set - client will be able to connect to fresh db only when network is online BootstrapIfEmpty bool // Reserved bytes set for the database - necessary if remote encryption is set for the db in cloud ReservedBytes int // Prefix bootstrap strategy enabling partial sync that lazily pulls pages on demand and bootstraps db with first N bytes PartialBootstrapStrategyPrefix int // Query bootstrap strategy enabling partial sync - bootstraps db with pages touched by the server with given SQL query PartialBootstrapStrategyQuery string // optional parameter which defines segment size for lazy loading from remote server // one of valid PartialBootstrapStrategy* values MUST be set in order for this setting to have some effect PartialBootstrapSegmentSize int // optional parameter which defines if pages prefetch must be enabled // one of valid PartialBootstrapStrategy* values MUST be set in order for this setting to have some effect PartialBootstrapPrefetch bool // optional base64-encoded encryption key for remote encrypted databases RemoteEncryptionKey string // optional encryption cipher name (e.g. "aes256gcm", "chacha20poly1305") RemoteEncryptionCipher string } // TursoSyncStats holds sync engine stats. type TursoSyncStats struct { CDcOperations int64 MainWalSize int64 RevertWalSize int64 LastPullUnixTime int64 LastPushUnixTime int64 NetworkSentBytes int64 NetworkReceivedBytes int64 Revision string } // HTTP request description used by IO layer. type TursoSyncIoHttpRequest struct { Url string Method string Path string Body []byte Headers int } // HTTP header key-value pair. type TursoSyncIoHttpHeader struct { Key string Value string } // Atomic read request description. type TursoSyncIoFullReadRequest struct { Path string } // Atomic write request description. type TursoSyncIoFullWriteRequest struct { Path string Content []byte } // ------------- Private C-compatible structs ------------- type turso_sync_database_config_t struct { path uintptr // const char* remote_url uintptr // const char* client_name uintptr // const char* long_poll_timeout_ms int32 bootstrap_if_empty bool reserved_bytes int32 partial_bootstrap_strategy_prefix int32 partial_bootstrap_strategy_query uintptr // const char* partial_bootstrap_segment_size uintptr partial_bootstrap_prefetch bool remote_encryption_key uintptr // const char* remote_encryption_cipher uintptr // const char* } type turso_sync_io_http_request_t struct { url turso_slice_ref_t method turso_slice_ref_t path turso_slice_ref_t body turso_slice_ref_t headers int32 } type turso_sync_io_http_header_t struct { key turso_slice_ref_t value turso_slice_ref_t } type turso_sync_io_full_read_request_t struct { path turso_slice_ref_t } type turso_sync_io_full_write_request_t struct { path turso_slice_ref_t content turso_slice_ref_t } type turso_sync_stats_t struct { cdc_operations int64 main_wal_size int64 revert_wal_size int64 last_pull_unix_time int64 last_push_unix_time int64 network_sent_bytes int64 network_received_bytes int64 revision turso_slice_ref_t } // ------------- C extern function vars ------------- var ( c_turso_sync_database_new func( dbConfig *turso_database_config_t, syncConfig *turso_sync_database_config_t, database **turso_sync_database_t, errorOptOut **byte, ) int32 c_turso_sync_database_open func( self TursoSyncDatabase, operation **turso_sync_operation_t, errorOptOut **byte, ) int32 c_turso_sync_database_create func( self TursoSyncDatabase, operation **turso_sync_operation_t, errorOptOut **byte, ) int32 c_turso_sync_database_connect func( self TursoSyncDatabase, operation **turso_sync_operation_t, errorOptOut **byte, ) int32 c_turso_sync_database_stats func( self TursoSyncDatabase, operation **turso_sync_operation_t, errorOptOut **byte, ) int32 c_turso_sync_database_checkpoint func( self TursoSyncDatabase, operation **turso_sync_operation_t, errorOptOut **byte, ) int32 c_turso_sync_database_push_changes func( self TursoSyncDatabase, operation **turso_sync_operation_t, errorOptOut **byte, ) int32 c_turso_sync_database_wait_changes func( self TursoSyncDatabase, operation **turso_sync_operation_t, errorOptOut **byte, ) int32 c_turso_sync_database_apply_changes func( self TursoSyncDatabase, changes TursoSyncChanges, operation **turso_sync_operation_t, errorOptOut **byte, ) int32 c_turso_sync_operation_resume func( self TursoSyncOperation, errorOptOut **byte, ) int32 c_turso_sync_operation_result_kind func( self TursoSyncOperation, ) int32 c_turso_sync_operation_result_extract_connection func( self TursoSyncOperation, connection **turso_connection_t, ) int32 c_turso_sync_operation_result_extract_changes func( self TursoSyncOperation, changes **turso_sync_changes_t, ) int32 c_turso_sync_operation_result_extract_stats func( self TursoSyncOperation, stats *turso_sync_stats_t, ) int32 c_turso_sync_database_io_take_item func( self TursoSyncDatabase, item **turso_sync_io_item_t, errorOptOut **byte, ) int32 c_turso_sync_database_io_step_callbacks func( self TursoSyncDatabase, errorOptOut **byte, ) int32 c_turso_sync_database_io_request_kind func( self TursoSyncIoItem, ) int32 c_turso_sync_database_io_request_http func( self TursoSyncIoItem, request *turso_sync_io_http_request_t, ) int32 c_turso_sync_database_io_request_http_header func( self TursoSyncIoItem, index uintptr, header *turso_sync_io_http_header_t, ) int32 c_turso_sync_database_io_request_full_read func( self TursoSyncIoItem, request *turso_sync_io_full_read_request_t, ) int32 c_turso_sync_database_io_request_full_write func( self TursoSyncIoItem, request *turso_sync_io_full_write_request_t, ) int32 c_turso_sync_database_io_poison func( self TursoSyncIoItem, err *turso_slice_ref_t, ) int32 c_turso_sync_database_io_status func( self TursoSyncIoItem, status int32, ) int32 c_turso_sync_database_io_push_buffer func( self TursoSyncIoItem, buffer *turso_slice_ref_t, ) int32 c_turso_sync_database_io_done func( self TursoSyncIoItem, ) int32 c_turso_sync_database_deinit func( self TursoSyncDatabase, ) c_turso_sync_operation_deinit func( self TursoSyncOperation, ) c_turso_sync_database_io_item_deinit func( self TursoSyncIoItem, ) c_turso_sync_changes_deinit func( self TursoSyncChanges, ) ) // ------------- Registration ------------- // registerTursoSync registers Turso Sync C API function pointers from the given library handle. // Do not load library here; it is done externally. func registerTursoSync(handle uintptr) error { purego.RegisterLibFunc(&c_turso_sync_database_new, handle, "turso_sync_database_new") purego.RegisterLibFunc(&c_turso_sync_database_open, handle, "turso_sync_database_open") purego.RegisterLibFunc(&c_turso_sync_database_create, handle, "turso_sync_database_create") purego.RegisterLibFunc(&c_turso_sync_database_connect, handle, "turso_sync_database_connect") purego.RegisterLibFunc(&c_turso_sync_database_stats, handle, "turso_sync_database_stats") purego.RegisterLibFunc(&c_turso_sync_database_checkpoint, handle, "turso_sync_database_checkpoint") purego.RegisterLibFunc(&c_turso_sync_database_push_changes, handle, "turso_sync_database_push_changes") purego.RegisterLibFunc(&c_turso_sync_database_wait_changes, handle, "turso_sync_database_wait_changes") purego.RegisterLibFunc(&c_turso_sync_database_apply_changes, handle, "turso_sync_database_apply_changes") purego.RegisterLibFunc(&c_turso_sync_operation_resume, handle, "turso_sync_operation_resume") purego.RegisterLibFunc(&c_turso_sync_operation_result_kind, handle, "turso_sync_operation_result_kind") purego.RegisterLibFunc(&c_turso_sync_operation_result_extract_connection, handle, "turso_sync_operation_result_extract_connection") purego.RegisterLibFunc(&c_turso_sync_operation_result_extract_changes, handle, "turso_sync_operation_result_extract_changes") purego.RegisterLibFunc(&c_turso_sync_operation_result_extract_stats, handle, "turso_sync_operation_result_extract_stats") purego.RegisterLibFunc(&c_turso_sync_database_io_take_item, handle, "turso_sync_database_io_take_item") purego.RegisterLibFunc(&c_turso_sync_database_io_step_callbacks, handle, "turso_sync_database_io_step_callbacks") purego.RegisterLibFunc(&c_turso_sync_database_io_request_kind, handle, "turso_sync_database_io_request_kind") purego.RegisterLibFunc(&c_turso_sync_database_io_request_http, handle, "turso_sync_database_io_request_http") purego.RegisterLibFunc(&c_turso_sync_database_io_request_http_header, handle, "turso_sync_database_io_request_http_header") purego.RegisterLibFunc(&c_turso_sync_database_io_request_full_read, handle, "turso_sync_database_io_request_full_read") purego.RegisterLibFunc(&c_turso_sync_database_io_request_full_write, handle, "turso_sync_database_io_request_full_write") purego.RegisterLibFunc(&c_turso_sync_database_io_poison, handle, "turso_sync_database_io_poison") purego.RegisterLibFunc(&c_turso_sync_database_io_status, handle, "turso_sync_database_io_status") purego.RegisterLibFunc(&c_turso_sync_database_io_push_buffer, handle, "turso_sync_database_io_push_buffer") purego.RegisterLibFunc(&c_turso_sync_database_io_done, handle, "turso_sync_database_io_done") purego.RegisterLibFunc(&c_turso_sync_database_deinit, handle, "turso_sync_database_deinit") purego.RegisterLibFunc(&c_turso_sync_operation_deinit, handle, "turso_sync_operation_deinit") purego.RegisterLibFunc(&c_turso_sync_database_io_item_deinit, handle, "turso_sync_database_io_item_deinit") purego.RegisterLibFunc(&c_turso_sync_changes_deinit, handle, "turso_sync_changes_deinit") return nil } // ------------- Helpers ------------- func sliceRefToBytesCopy(s turso_slice_ref_t) []byte { if s.ptr == 0 || s.len == 0 { return nil } p := (*byte)(unsafe.Pointer(s.ptr)) n := int(s.len) src := unsafe.Slice(p, n) dst := make([]byte, n) copy(dst, src) return dst } func sliceRefToStringCopy(s turso_slice_ref_t) string { b := sliceRefToBytesCopy(s) if len(b) == 0 { return "" } return string(b) } // ------------- Go wrappers over C API ------------- // turso_sync_database_new creates the database sync holder but does not open it. func turso_sync_database_new(dbConfig TursoDatabaseConfig, syncConfig TursoSyncDatabaseConfig) (TursoSyncDatabase, error) { // Build C database config var cdb turso_database_config_t var pathBytes, expBytes []byte pathBytes, cdb.path = makeCStringBytes(dbConfig.Path) if dbConfig.ExperimentalFeatures != "" { expBytes, cdb.experimental_features = makeCStringBytes(dbConfig.ExperimentalFeatures) } cdb.async_io = 0 if dbConfig.AsyncIO { cdb.async_io = 1 } // Build C sync config var csync turso_sync_database_config_t var syncPathBytes, remoteUrlBytes, clientNameBytes, queryBytes []byte var encryptionKeyBytes, encryptionCipherBytes []byte syncPathBytes, csync.path = makeCStringBytes(syncConfig.Path) remoteUrlBytes, csync.remote_url = makeCStringBytes(syncConfig.RemoteUrl) clientNameBytes, csync.client_name = makeCStringBytes(syncConfig.ClientName) csync.long_poll_timeout_ms = int32(syncConfig.LongPollTimeoutMs) csync.bootstrap_if_empty = syncConfig.BootstrapIfEmpty csync.reserved_bytes = int32(syncConfig.ReservedBytes) csync.partial_bootstrap_strategy_prefix = int32(syncConfig.PartialBootstrapStrategyPrefix) csync.partial_bootstrap_segment_size = uintptr(syncConfig.PartialBootstrapSegmentSize) csync.partial_bootstrap_prefetch = syncConfig.PartialBootstrapPrefetch if syncConfig.PartialBootstrapStrategyQuery != "" { queryBytes, csync.partial_bootstrap_strategy_query = makeCStringBytes(syncConfig.PartialBootstrapStrategyQuery) } if syncConfig.RemoteEncryptionKey != "" { encryptionKeyBytes, csync.remote_encryption_key = makeCStringBytes(syncConfig.RemoteEncryptionKey) } if syncConfig.RemoteEncryptionCipher != "" { encryptionCipherBytes, csync.remote_encryption_cipher = makeCStringBytes(syncConfig.RemoteEncryptionCipher) } var db *turso_sync_database_t var errPtr *byte status := c_turso_sync_database_new(&cdb, &csync, &db, &errPtr) // Keep Go memory alive during C call runtime.KeepAlive(pathBytes) runtime.KeepAlive(remoteUrlBytes) runtime.KeepAlive(expBytes) runtime.KeepAlive(syncPathBytes) runtime.KeepAlive(clientNameBytes) runtime.KeepAlive(queryBytes) runtime.KeepAlive(encryptionKeyBytes) runtime.KeepAlive(encryptionCipherBytes) if status == int32(TURSO_OK) { return TursoSyncDatabase(db), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_sync_database_open opens prepared synced database. Fails if no properly setup database exists. // AsyncOperation returns None. func turso_sync_database_open(self TursoSyncDatabase) (TursoSyncOperation, error) { var op *turso_sync_operation_t var errPtr *byte status := c_turso_sync_database_open(self, &op, &errPtr) if status == int32(TURSO_OK) { return TursoSyncOperation(op), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_sync_database_create opens or prepares synced database or creates it if needed. // AsyncOperation returns None. func turso_sync_database_create(self TursoSyncDatabase) (TursoSyncOperation, error) { var op *turso_sync_operation_t var errPtr *byte status := c_turso_sync_database_create(self, &op, &errPtr) if status == int32(TURSO_OK) { return TursoSyncOperation(op), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_sync_database_connect creates a turso database connection. // SAFETY: synced database must be opened before this operation. // AsyncOperation returns Connection. func turso_sync_database_connect(self TursoSyncDatabase) (TursoSyncOperation, error) { var op *turso_sync_operation_t var errPtr *byte status := c_turso_sync_database_connect(self, &op, &errPtr) if status == int32(TURSO_OK) { return TursoSyncOperation(op), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_sync_database_stats collects stats about synced database. // AsyncOperation returns Stats. func turso_sync_database_stats(self TursoSyncDatabase) (TursoSyncOperation, error) { var op *turso_sync_operation_t var errPtr *byte status := c_turso_sync_database_stats(self, &op, &errPtr) if status == int32(TURSO_OK) { return TursoSyncOperation(op), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_sync_database_checkpoint performs WAL checkpoint for the synced database. // AsyncOperation returns None. func turso_sync_database_checkpoint(self TursoSyncDatabase) (TursoSyncOperation, error) { var op *turso_sync_operation_t var errPtr *byte status := c_turso_sync_database_checkpoint(self, &op, &errPtr) if status == int32(TURSO_OK) { return TursoSyncOperation(op), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_sync_database_push_changes pushes local changes to remote. // AsyncOperation returns None. func turso_sync_database_push_changes(self TursoSyncDatabase) (TursoSyncOperation, error) { var op *turso_sync_operation_t var errPtr *byte status := c_turso_sync_database_push_changes(self, &op, &errPtr) if status == int32(TURSO_OK) { return TursoSyncOperation(op), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_sync_database_wait_changes waits for remote changes. // AsyncOperation returns Changes. func turso_sync_database_wait_changes(self TursoSyncDatabase) (TursoSyncOperation, error) { var op *turso_sync_operation_t var errPtr *byte status := c_turso_sync_database_wait_changes(self, &op, &errPtr) if status == int32(TURSO_OK) { return TursoSyncOperation(op), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_sync_database_apply_changes applies remote changes locally. // SAFETY: caller must ensure that no other methods are executing concurrently (push/wait/checkpoint). // // the method CONSUMES turso_sync_changes_t instance and caller no longer owns it after the call // So, the changes MUST NOT be explicitly deallocated after the method call (either successful or not) // // AsyncOperation returns None. func turso_sync_database_apply_changes(self TursoSyncDatabase, changes TursoSyncChanges) (TursoSyncOperation, error) { var op *turso_sync_operation_t var errPtr *byte status := c_turso_sync_database_apply_changes(self, changes, &op, &errPtr) if status == int32(TURSO_OK) { return TursoSyncOperation(op), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_sync_operation_resume resumes async operation. // Returns status code (OK/IO/DONE or error code) and error if any. func turso_sync_operation_resume(self TursoSyncOperation) (TursoStatusCode, error) { var errPtr *byte status := c_turso_sync_operation_resume(self, &errPtr) switch TursoStatusCode(status) { case TURSO_OK, TURSO_IO, TURSO_DONE: return TursoStatusCode(status), nil default: msg := decodeAndFreeCString(errPtr) return TursoStatusCode(status), statusToError(TursoStatusCode(status), msg) } } // turso_sync_operation_result_kind extracts operation result kind. func turso_sync_operation_result_kind(self TursoSyncOperation) TursoSyncOperationResultType { return TursoSyncOperationResultType(c_turso_sync_operation_result_kind(self)) } // turso_sync_operation_result_extract_connection extracts Connection result from finished operation. func turso_sync_operation_result_extract_connection(self TursoSyncOperation) (TursoConnection, error) { var conn *turso_connection_t status := c_turso_sync_operation_result_extract_connection(self, &conn) if status == int32(TURSO_OK) { return TursoConnection(conn), nil } return nil, statusToError(TursoStatusCode(status), "") } // turso_sync_operation_result_extract_changes extracts Changes result from finished operation. // If no changes were fetched - return TURSO_OK and set changes to null pointer func turso_sync_operation_result_extract_changes(self TursoSyncOperation) (TursoSyncChanges, error) { var ch *turso_sync_changes_t status := c_turso_sync_operation_result_extract_changes(self, &ch) if status == int32(TURSO_OK) { return TursoSyncChanges(ch), nil } return nil, statusToError(TursoStatusCode(status), "") } // turso_sync_operation_result_extract_stats extracts Stats result from finished operation. func turso_sync_operation_result_extract_stats(self TursoSyncOperation) (TursoSyncStats, error) { var cstats turso_sync_stats_t status := c_turso_sync_operation_result_extract_stats(self, &cstats) if status != int32(TURSO_OK) { return TursoSyncStats{}, statusToError(TursoStatusCode(status), "") } return TursoSyncStats{ CDcOperations: cstats.cdc_operations, MainWalSize: cstats.main_wal_size, RevertWalSize: cstats.revert_wal_size, LastPullUnixTime: cstats.last_pull_unix_time, LastPushUnixTime: cstats.last_push_unix_time, NetworkSentBytes: cstats.network_sent_bytes, NetworkReceivedBytes: cstats.network_received_bytes, Revision: sliceRefToStringCopy(cstats.revision), }, nil } // turso_sync_database_io_take_item tries to take IO request from the sync engine IO queue. // if queue is empty - returns nil pointer to the TursoSyncIoItem func turso_sync_database_io_take_item(self TursoSyncDatabase) (TursoSyncIoItem, error) { var item *turso_sync_io_item_t var errPtr *byte status := c_turso_sync_database_io_take_item(self, &item, &errPtr) if status == int32(TURSO_OK) { return TursoSyncIoItem(item), nil } msg := decodeAndFreeCString(errPtr) return nil, statusToError(TursoStatusCode(status), msg) } // turso_sync_database_io_step_callbacks runs extra database callbacks after IO execution. func turso_sync_database_io_step_callbacks(self TursoSyncDatabase) error { var errPtr *byte status := c_turso_sync_database_io_step_callbacks(self, &errPtr) if status == int32(TURSO_OK) { return nil } msg := decodeAndFreeCString(errPtr) return statusToError(TursoStatusCode(status), msg) } // turso_sync_database_io_request_kind returns the IO request kind. func turso_sync_database_io_request_kind(self TursoSyncIoItem) TursoSyncIoRequestType { return TursoSyncIoRequestType(c_turso_sync_database_io_request_kind(self)) } // turso_sync_database_io_request_http gets HTTP request fields for an IO item. func turso_sync_database_io_request_http(self TursoSyncIoItem) (TursoSyncIoHttpRequest, error) { var creq turso_sync_io_http_request_t status := c_turso_sync_database_io_request_http(self, &creq) if status != int32(TURSO_OK) { return TursoSyncIoHttpRequest{}, statusToError(TursoStatusCode(status), "") } return TursoSyncIoHttpRequest{ Url: sliceRefToStringCopy(creq.url), Method: sliceRefToStringCopy(creq.method), Path: sliceRefToStringCopy(creq.path), Body: sliceRefToBytesCopy(creq.body), Headers: int(creq.headers), }, nil } // turso_sync_database_io_request_http_header returns HTTP header key-value pair at index. func turso_sync_database_io_request_http_header(self TursoSyncIoItem, index int) (TursoSyncIoHttpHeader, error) { var ch turso_sync_io_http_header_t status := c_turso_sync_database_io_request_http_header(self, uintptr(index), &ch) if status != int32(TURSO_OK) { return TursoSyncIoHttpHeader{}, statusToError(TursoStatusCode(status), "") } return TursoSyncIoHttpHeader{ Key: sliceRefToStringCopy(ch.key), Value: sliceRefToStringCopy(ch.value), }, nil } // turso_sync_database_io_request_full_read returns atomic read request fields. func turso_sync_database_io_request_full_read(self TursoSyncIoItem) (TursoSyncIoFullReadRequest, error) { var r turso_sync_io_full_read_request_t status := c_turso_sync_database_io_request_full_read(self, &r) if status != int32(TURSO_OK) { return TursoSyncIoFullReadRequest{}, statusToError(TursoStatusCode(status), "") } return TursoSyncIoFullReadRequest{Path: sliceRefToStringCopy(r.path)}, nil } // turso_sync_database_io_request_full_write returns atomic write request fields. func turso_sync_database_io_request_full_write(self TursoSyncIoItem) (TursoSyncIoFullWriteRequest, error) { var r turso_sync_io_full_write_request_t status := c_turso_sync_database_io_request_full_write(self, &r) if status != int32(TURSO_OK) { return TursoSyncIoFullWriteRequest{}, statusToError(TursoStatusCode(status), "") } return TursoSyncIoFullWriteRequest{ Path: sliceRefToStringCopy(r.path), Content: sliceRefToBytesCopy(r.content), }, nil } // turso_sync_database_io_poison marks IO request completion with error. func turso_sync_database_io_poison(self TursoSyncIoItem, errMsg string) error { var ref turso_slice_ref_t var bytes []byte if errMsg != "" { bytes = []byte(errMsg) ref.ptr = uintptr(unsafe.Pointer(&bytes[0])) ref.len = uintptr(len(bytes)) } status := c_turso_sync_database_io_poison(self, &ref) // Keep Go memory alive during call runtime.KeepAlive(bytes) if status == int32(TURSO_OK) { return nil } return statusToError(TursoStatusCode(status), "") } // turso_sync_database_io_status sets IO request completion status (e.g. HTTP status). func turso_sync_database_io_status(self TursoSyncIoItem, statusCode int) error { status := c_turso_sync_database_io_status(self, int32(statusCode)) if status == int32(TURSO_OK) { return nil } return statusToError(TursoStatusCode(status), "") } // turso_sync_database_io_push_buffer pushes bytes to the IO completion buffer. func turso_sync_database_io_push_buffer(self TursoSyncIoItem, buffer []byte) error { var ref turso_slice_ref_t if len(buffer) > 0 { ref.ptr = uintptr(unsafe.Pointer(&buffer[0])) ref.len = uintptr(len(buffer)) } status := c_turso_sync_database_io_push_buffer(self, &ref) // Keep Go memory alive during call runtime.KeepAlive(buffer) if status == int32(TURSO_OK) { return nil } return statusToError(TursoStatusCode(status), "") } // turso_sync_database_io_done sets IO request completion as done. func turso_sync_database_io_done(self TursoSyncIoItem) error { status := c_turso_sync_database_io_done(self) if status == int32(TURSO_OK) { return nil } return statusToError(TursoStatusCode(status), "") } // turso_sync_database_deinit deallocates a TursoDatabaseSync. func turso_sync_database_deinit(self TursoSyncDatabase) { c_turso_sync_database_deinit(self) } // turso_sync_operation_deinit deallocates a TursoAsyncOperation. func turso_sync_operation_deinit(self TursoSyncOperation) { c_turso_sync_operation_deinit(self) } // turso_sync_database_io_item_deinit deallocates a SyncEngineIoQueueItem. func turso_sync_database_io_item_deinit(self TursoSyncIoItem) { c_turso_sync_database_io_item_deinit(self) } // turso_sync_changes_deinit deallocates a TursoDatabaseSyncChanges. func turso_sync_changes_deinit(self TursoSyncChanges) { c_turso_sync_changes_deinit(self) } ================================================ FILE: bindings/go/driver_db.go ================================================ package turso import ( "context" "database/sql" "database/sql/driver" "errors" "fmt" "io" "math" "net/url" "strings" "sync" "time" turso_libs "github.com/tursodatabase/turso-go-platform-libs" ) // define all package level errors here var ( ErrTursoStmtClosed = errors.New("turso: statement closed") ErrTursoConnClosed = errors.New("turso: connection closed") ErrTursoRowsClosed = errors.New("turso: rows closed") ErrTursoTxDone = errors.New("turso: transaction done") ) // define all package level structs here type tursoDbDriver struct{} type tursoDbConnection struct { db TursoDatabase conn TursoConnection extraIo func() error mu sync.Mutex closed bool busyTimeout int // current busy timeout in milliseconds // keep flags for configuration if needed async bool } type tursoDbStatement struct { conn *tursoDbConnection sql string numInputs int closed bool } type tursoDbRows struct { conn *tursoDbConnection stmt TursoStatement columns []string decltypes []string closed bool err error } type tursoDbResult struct { lastInsertId int64 rowsAffected int64 } type tursoDbTx struct { conn *tursoDbConnection done bool } // register driver func init() { sql.Register("turso", &tursoDbDriver{}) } // Extra constructor for *tursoDbConnection instance which can be used to intergrate with turso Db driver // extr_io parameter is the arbitrary IO function which will be executed together with turso_statement_run_io func NewConnection(conn TursoConnection, extraIo func() error) *tursoDbConnection { return &tursoDbConnection{ conn: conn, extraIo: extraIo, } } // Optional helper to run global setup (logger and log level). func Setup(config TursoConfig) error { InitLibrary(turso_libs.LoadTursoLibraryConfig{}) return turso_setup(config) } // Implement sql.Driver methods func (d *tursoDbDriver) Open(dsn string) (driver.Conn, error) { InitLibrary(turso_libs.LoadTursoLibraryConfig{}) config, err := parseDSN(dsn) if err != nil { return nil, err } db, err := turso_database_new(config) if err != nil { return nil, err } if err := turso_database_open(db); err != nil { turso_database_deinit(db) return nil, err } c, err := turso_database_connect(db) if err != nil { turso_database_deinit(db) return nil, err } // Apply busy timeout - use default if not explicitly set // A value of -1 in config means explicitly disabled (no timeout) // A value of 0 means use the default timeout // A positive value is used as-is timeout := config.BusyTimeout if timeout == 0 { timeout = DefaultBusyTimeout // Apply sensible default } else if timeout < 0 { timeout = 0 // -1 means explicitly disable } if timeout > 0 { turso_connection_set_busy_timeout_ms(c, int64(timeout)) } return &tursoDbConnection{ db: db, conn: c, busyTimeout: timeout, async: config.AsyncIO, }, nil } // --- driver.Conn and friends --- // Ensure tursoDbConnection implements required interfaces. var ( _ driver.Conn = (*tursoDbConnection)(nil) _ driver.ConnPrepareContext = (*tursoDbConnection)(nil) _ driver.ExecerContext = (*tursoDbConnection)(nil) _ driver.QueryerContext = (*tursoDbConnection)(nil) _ driver.Pinger = (*tursoDbConnection)(nil) _ driver.ConnBeginTx = (*tursoDbConnection)(nil) ) func (c *tursoDbConnection) Prepare(query string) (driver.Stmt, error) { return c.PrepareContext(context.Background(), query) } func (c *tursoDbConnection) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { if err := c.checkOpen(); err != nil { return nil, err } // PREPARE in Prepare - do not delay that c.mu.Lock() defer c.mu.Unlock() if ctx.Err() != nil { return nil, ctx.Err() } stmt, err := turso_connection_prepare_single(c.conn, query) if err != nil { return nil, err } // determine number of inputs and then finalize immediately to avoid keeping state num := int(turso_statement_parameters_count(stmt)) _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) return &tursoDbStatement{ conn: c, sql: query, numInputs: num, }, nil } func (c *tursoDbConnection) Close() error { c.mu.Lock() defer c.mu.Unlock() if c.closed { return nil } // Close connection and deinit resources if c.conn != nil { _ = turso_connection_close(c.conn) turso_connection_deinit(c.conn) c.conn = nil } if c.db != nil { turso_database_deinit(c.db) c.db = nil } c.closed = true return nil } func (c *tursoDbConnection) Begin() (driver.Tx, error) { return c.BeginTx(context.Background(), driver.TxOptions{}) } func (c *tursoDbConnection) BeginTx(ctx context.Context, _ driver.TxOptions) (driver.Tx, error) { if err := c.checkOpen(); err != nil { return nil, err } // Use BEGIN (snapshot isolation) _, err := c.ExecContext(ctx, "BEGIN", nil) if err != nil { return nil, err } return &tursoDbTx{conn: c}, nil } func (c *tursoDbConnection) Ping(ctx context.Context) error { if err := c.checkOpen(); err != nil { return err } // trivial ping: simple select constant _, err := c.QueryContext(ctx, "SELECT 1", nil) if err != nil { return err } return nil } func (c *tursoDbConnection) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { if err := c.checkOpen(); err != nil { return nil, err } // Multi-statement support for Exec-family var totalAffected int64 c.mu.Lock() defer c.mu.Unlock() offset := 0 first := true var lastInsert int64 = 0 for { if ctx.Err() != nil { return nil, ctx.Err() } rest := query[offset:] if strings.TrimSpace(rest) == "" { break } stmt, tail, err := turso_connection_prepare_first(c.conn, rest) if err != nil { return nil, err } // Calculate absolute offset advance offset += tail // Bind only for the first statement if first && len(args) > 0 { if err := bindArgs(stmt, args); err != nil { _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) return nil, err } } // Execute statement fully affected, err := c.executeFully(ctx, stmt) // finalize and deinit regardless of status _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) if err != nil { return nil, err } // rows affected is capped at MaxInt64 if affected > uint64(math.MaxInt64-totalAffected) { totalAffected = math.MaxInt64 } else { totalAffected += int64(affected) } lastInsert = turso_connection_last_insert_rowid(c.conn) first = false // continue with the rest of the query string } return &tursoDbResult{ lastInsertId: lastInsert, rowsAffected: totalAffected, }, nil } func (c *tursoDbConnection) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { if err := c.checkOpen(); err != nil { return nil, err } c.mu.Lock() defer c.mu.Unlock() if ctx.Err() != nil { return nil, ctx.Err() } // Only single-statement queries supported here stmt, err := turso_connection_prepare_single(c.conn, query) if err != nil { return nil, err } if len(args) > 0 { if err := bindArgs(stmt, args); err != nil { _ = turso_statement_finalize(stmt) turso_statement_deinit(stmt) return nil, err } } // Return rows wrapper; do not step yet, leave cursor before first row return &tursoDbRows{ conn: c, stmt: stmt, }, nil } func (c *tursoDbConnection) checkOpen() error { c.mu.Lock() defer c.mu.Unlock() if c.closed || c.conn == nil { return ErrTursoConnClosed } return nil } // SetBusyTimeout sets the busy timeout for this connection in milliseconds. // Pass 0 to disable the busy handler (immediate SQLITE_BUSY on contention). // This method is thread-safe. func (c *tursoDbConnection) SetBusyTimeout(timeoutMs int) error { if err := c.checkOpen(); err != nil { return err } c.mu.Lock() defer c.mu.Unlock() if timeoutMs < 0 { timeoutMs = 0 } turso_connection_set_busy_timeout_ms(c.conn, int64(timeoutMs)) c.busyTimeout = timeoutMs return nil } // GetBusyTimeout returns the current busy timeout in milliseconds. // Returns 0 if the busy handler is disabled. func (c *tursoDbConnection) GetBusyTimeout() int { c.mu.Lock() defer c.mu.Unlock() return c.busyTimeout } // --- Connector Pattern --- // ConnectorOption configures a TursoConnector. type ConnectorOption func(*TursoConnector) // WithBusyTimeout sets the busy timeout in milliseconds. // Use 0 to disable the busy handler, -1 to use the default (5000ms). func WithBusyTimeout(ms int) ConnectorOption { return func(c *TursoConnector) { c.busyTimeout = ms } } // TursoConnector implements driver.Connector for programmatic configuration. type TursoConnector struct { dsn string busyTimeout int // -1 = use default, 0 = disabled, >0 = custom } // NewConnector creates a new TursoConnector with the given DSN and options. // By default, uses the DefaultBusyTimeout (5000ms). func NewConnector(dsn string, opts ...ConnectorOption) (*TursoConnector, error) { c := &TursoConnector{ dsn: dsn, busyTimeout: -1, // -1 means use default } for _, opt := range opts { opt(c) } return c, nil } // Connect implements driver.Connector. func (c *TursoConnector) Connect(ctx context.Context) (driver.Conn, error) { InitLibrary(turso_libs.LoadTursoLibraryConfig{}) config, err := parseDSN(c.dsn) if err != nil { return nil, err } // Override busy timeout from connector if set if c.busyTimeout >= 0 { // If connector explicitly sets 0, that means disabled // We use -1 internally to signal "disabled" to Open logic if c.busyTimeout == 0 { config.BusyTimeout = -1 // Will be converted to 0 in Open } else { config.BusyTimeout = c.busyTimeout } } // If busyTimeout is -1 (use default) and DSN didn't set one, leave it as 0 // which will trigger the default in Open() db, err := turso_database_new(config) if err != nil { return nil, err } if err := turso_database_open(db); err != nil { turso_database_deinit(db) return nil, err } conn, err := turso_database_connect(db) if err != nil { turso_database_deinit(db) return nil, err } // Apply busy timeout - same logic as Open() timeout := config.BusyTimeout if timeout == 0 { timeout = DefaultBusyTimeout } else if timeout < 0 { timeout = 0 } if timeout > 0 { turso_connection_set_busy_timeout_ms(conn, int64(timeout)) } return &tursoDbConnection{ db: db, conn: conn, busyTimeout: timeout, async: config.AsyncIO, }, nil } // Driver implements driver.Connector. func (c *TursoConnector) Driver() driver.Driver { return &tursoDbDriver{} } // Ensure TursoConnector implements driver.Connector var _ driver.Connector = (*TursoConnector)(nil) // --- driver.Stmt and friends --- // Ensure tursoDbStatement implements required interfaces. var ( _ driver.Stmt = (*tursoDbStatement)(nil) _ driver.StmtExecContext = (*tursoDbStatement)(nil) _ driver.StmtQueryContext = (*tursoDbStatement)(nil) ) func (s *tursoDbStatement) Close() error { s.closed = true return nil } func (s *tursoDbStatement) NumInput() int { return s.numInputs } func (s *tursoDbStatement) Exec(args []driver.Value) (driver.Result, error) { named := make([]driver.NamedValue, len(args)) for i, v := range args { named[i] = driver.NamedValue{Ordinal: i + 1, Value: v} } return s.ExecContext(context.Background(), named) } func (s *tursoDbStatement) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { if s.closed { return nil, ErrTursoStmtClosed } return s.conn.ExecContext(ctx, s.sql, args) } func (s *tursoDbStatement) Query(args []driver.Value) (driver.Rows, error) { named := make([]driver.NamedValue, len(args)) for i, v := range args { named[i] = driver.NamedValue{Ordinal: i + 1, Value: v} } return s.QueryContext(context.Background(), named) } func (s *tursoDbStatement) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { if s.closed { return nil, ErrTursoStmtClosed } return s.conn.QueryContext(ctx, s.sql, args) } // --- driver.Rows --- // Ensure tursoDbRows implements the required interface. var _ driver.Rows = (*tursoDbRows)(nil) func (r *tursoDbRows) Columns() []string { if r.columns != nil { return r.columns } n := int(turso_statement_column_count(r.stmt)) names := make([]string, n) decltypes := make([]string, n) for i := 0; i < n; i++ { names[i] = turso_statement_column_name(r.stmt, i) decltypes[i] = turso_statement_column_decltype(r.stmt, i) } r.columns = names r.decltypes = decltypes return r.columns } func (r *tursoDbRows) Close() error { if r.closed { return nil } r.closed = true _ = turso_statement_finalize(r.stmt) turso_statement_deinit(r.stmt) return nil } func (r *tursoDbRows) Next(dest []driver.Value) error { if r.closed { return io.EOF } // Ensure decltypes are populated _ = r.Columns() for { status, err := turso_statement_step(r.stmt) if err != nil { r.err = err return err } switch status { case TURSO_ROW: // Fill destination n := int(turso_statement_column_count(r.stmt)) if len(dest) != n { return fmt.Errorf("turso: expected %d dests, got %d", n, len(dest)) } for i := 0; i < n; i++ { kind := turso_statement_row_value_kind(r.stmt, i) switch kind { case TURSO_TYPE_NULL: dest[i] = nil case TURSO_TYPE_INTEGER: dest[i] = turso_statement_row_value_int(r.stmt, i) case TURSO_TYPE_REAL: dest[i] = turso_statement_row_value_double(r.stmt, i) case TURSO_TYPE_TEXT: text := turso_statement_row_value_text(r.stmt, i) // Check if column type indicates a time value if i < len(r.decltypes) && isTimeColumn(r.decltypes[i]) { if t, err := parseTimeString(text); err == nil { dest[i] = t } else { dest[i] = text } } else { dest[i] = text } case TURSO_TYPE_BLOB: dest[i] = turso_statement_row_value_bytes(r.stmt, i) default: dest[i] = nil } } return nil case TURSO_DONE: return io.EOF case TURSO_IO: // Run IO iteration if r.conn.extraIo != nil { if err := r.conn.extraIo(); err != nil { r.err = err return err } } if err := turso_statement_run_io(r.stmt); err != nil { r.err = err return err } continue case TURSO_OK: // Continue stepping continue default: return ErrTursoGeneric } } } // --- driver.Result --- var _ driver.Result = (*tursoDbResult)(nil) func (r *tursoDbResult) LastInsertId() (int64, error) { return r.lastInsertId, nil } func (r *tursoDbResult) RowsAffected() (int64, error) { return r.rowsAffected, nil } // --- driver.Tx --- var _ driver.Tx = (*tursoDbTx)(nil) func (tx *tursoDbTx) Commit() error { if tx.done { return ErrTursoTxDone } _, err := tx.conn.ExecContext(context.Background(), "COMMIT", nil) tx.done = true return err } func (tx *tursoDbTx) Rollback() error { if tx.done { return ErrTursoTxDone } _, err := tx.conn.ExecContext(context.Background(), "ROLLBACK", nil) tx.done = true return err } // Helpers // parseDSN supports format: [?experimental=&async=0|1&vfs=&encryption_cipher=&encryption_hexkey=&_busy_timeout=] func parseDSN(dsn string) (TursoDatabaseConfig, error) { config := TursoDatabaseConfig{Path: dsn} qMark := strings.IndexByte(dsn, '?') if qMark >= 0 { config.Path = dsn[:qMark] rawQuery := dsn[qMark+1:] vals, err := url.ParseQuery(rawQuery) if err != nil { return TursoDatabaseConfig{}, err } if v := vals.Get("experimental"); v != "" { config.ExperimentalFeatures = v } if v := vals.Get("async"); v != "" { config.AsyncIO = v == "1" || strings.EqualFold(v, "true") || strings.EqualFold(v, "yes") } if v := vals.Get("vfs"); v != "" { config.Vfs = v } if v := vals.Get("encryption_cipher"); v != "" { config.Encryption.Cipher = v } if v := vals.Get("encryption_hexkey"); v != "" { config.Encryption.Hexkey = v } if v := vals.Get("_busy_timeout"); v != "" { var timeout int if _, err := fmt.Sscanf(v, "%d", &timeout); err == nil { config.BusyTimeout = timeout } } } return config, nil } func (c *tursoDbConnection) executeFully(ctx context.Context, stmt TursoStatement) (uint64, error) { var latest uint64 for { if ctx != nil && ctx.Err() != nil { return 0, ctx.Err() } status, changes, err := turso_statement_execute(stmt) if err != nil { return 0, err } latest = changes switch status { case TURSO_DONE: return latest, nil case TURSO_IO: // perform one IO iteration and retry if c.extraIo != nil { if err := c.extraIo(); err != nil { return 0, err } } if err := turso_statement_run_io(stmt); err != nil { return 0, err } continue case TURSO_ROW: // Exhaust rows until DONE for { if ctx != nil && ctx.Err() != nil { return 0, ctx.Err() } st, err := turso_statement_step(stmt) if err != nil { return 0, err } if st == TURSO_ROW { continue } if st == TURSO_DONE { return latest, nil } if st == TURSO_IO { if c.extraIo != nil { if err := c.extraIo(); err != nil { return 0, err } } if err := turso_statement_run_io(stmt); err != nil { return 0, err } continue } // Continue on OK or others } case TURSO_OK: // keep going; step to progress st, err := turso_statement_step(stmt) if err != nil { return 0, err } if st == TURSO_DONE { return latest, nil } if st == TURSO_IO { if c.extraIo != nil { if err := c.extraIo(); err != nil { return 0, err } } if err := turso_statement_run_io(stmt); err != nil { return 0, err } } // and loop again default: return 0, statusToError(status, "") } } } // bindArgs binds ordered and named values to a statement. // Named values are resolved via turso_statement_named_position, otherwise ordinal positions are used (1-based). func bindArgs(stmt TursoStatement, args []driver.NamedValue) error { // Validate number of inputs if no named args present if len(args) > 0 { hasNamed := false for _, nv := range args { if nv.Name != "" { hasNamed = true break } } if !hasNamed { paramCount := int(turso_statement_parameters_count(stmt)) if paramCount >= 0 && len(args) != paramCount { return fmt.Errorf("turso: got %d args, want %d", len(args), paramCount) } } } for idx, nv := range args { pos := idx + 1 if nv.Name != "" { np := int(turso_statement_named_position(stmt, nv.Name)) if np <= 0 { return fmt.Errorf("turso: unknown named parameter %q", nv.Name) } pos = np } else if nv.Ordinal > 0 { pos = nv.Ordinal } if err := bindOne(stmt, pos, nv.Value); err != nil { return err } } return nil } func bindOne(stmt TursoStatement, position int, v any) error { if v == nil { return turso_statement_bind_positional_null(stmt, position) } switch x := v.(type) { case int: return turso_statement_bind_positional_int(stmt, position, int64(x)) case int8: return turso_statement_bind_positional_int(stmt, position, int64(x)) case int16: return turso_statement_bind_positional_int(stmt, position, int64(x)) case int32: return turso_statement_bind_positional_int(stmt, position, int64(x)) case int64: return turso_statement_bind_positional_int(stmt, position, x) case uint: return turso_statement_bind_positional_int(stmt, position, int64(x)) case uint8: return turso_statement_bind_positional_int(stmt, position, int64(x)) case uint16: return turso_statement_bind_positional_int(stmt, position, int64(x)) case uint32: return turso_statement_bind_positional_int(stmt, position, int64(x)) case uint64: // cap at MaxInt64 to avoid overflow i := int64(0) if x > uint64(math.MaxInt64) { i = math.MaxInt64 } else { i = int64(x) } return turso_statement_bind_positional_int(stmt, position, i) case float32: return turso_statement_bind_positional_double(stmt, position, float64(x)) case float64: return turso_statement_bind_positional_double(stmt, position, x) case bool: if x { return turso_statement_bind_positional_int(stmt, position, 1) } return turso_statement_bind_positional_int(stmt, position, 0) case []byte: return turso_statement_bind_positional_blob(stmt, position, x) case string: return turso_statement_bind_positional_text(stmt, position, x) case time.Time: // encode as RFC3339Nano string return turso_statement_bind_positional_text(stmt, position, x.Format(time.RFC3339Nano)) default: // Fallback to fmt to string return turso_statement_bind_positional_text(stmt, position, fmt.Sprint(v)) } } // isTimeColumn checks if the column declared type indicates a time/date column. // This matches the behavior of github.com/mattn/go-sqlite3. func isTimeColumn(decltype string) bool { if decltype == "" { return false } // Case-insensitive exact match for TIMESTAMP, DATETIME, DATE // Matches go-sqlite3 behavior: https://github.com/mattn/go-sqlite3/blob/master/sqlite3_type.go upper := strings.ToUpper(decltype) return upper == "TIMESTAMP" || upper == "DATETIME" || upper == "DATE" } // SQLiteTimestampFormats are the timestamp formats supported by go-sqlite3. // https://github.com/mattn/go-sqlite3/blob/master/sqlite3.go var SQLiteTimestampFormats = []string{ "2006-01-02 15:04:05.999999999-07:00", "2006-01-02T15:04:05.999999999-07:00", "2006-01-02 15:04:05.999999999", "2006-01-02T15:04:05.999999999", "2006-01-02 15:04:05", "2006-01-02T15:04:05", "2006-01-02 15:04", "2006-01-02T15:04", "2006-01-02", } // parseTimeString attempts to parse a string as a time.Time value. // This matches the behavior of github.com/mattn/go-sqlite3. func parseTimeString(s string) (time.Time, error) { // Strip trailing "Z" suffix before parsing (go-sqlite3 behavior) s = strings.TrimSuffix(s, "Z") for _, format := range SQLiteTimestampFormats { if t, err := time.ParseInLocation(format, s, time.UTC); err == nil { return t, nil } } return time.Time{}, fmt.Errorf("cannot parse %q as time", s) } ================================================ FILE: bindings/go/driver_db_test.go ================================================ package turso import ( "bytes" "database/sql" "fmt" "log" "math" "os" "path" "runtime" "slices" "sync" "testing" "time" "github.com/stretchr/testify/require" turso_libs "github.com/tursodatabase/turso-go-platform-libs" ) var ( conn *sql.DB ) func openMem(t *testing.T) *sql.DB { t.Helper() db, err := sql.Open("turso", ":memory:") if err != nil { t.Fatalf("open: %v", err) } t.Cleanup(func() { _ = db.Close() }) return db } func TestMain(m *testing.M) { InitLibrary(turso_libs.LoadTursoLibraryConfig{LoadStrategy: "mixed"}) var err error conn, err = sql.Open("turso", ":memory:") if err != nil { log.Fatalf("Failed to create database: %v", err) } err = conn.Ping() if err != nil { log.Fatalf("Error pinging database: %v", err) } defer conn.Close() err = createTable(conn) if err != nil { log.Fatalf("Error creating table: %v", err) } m.Run() } func TestEncryption(t *testing.T) { hexkey := "b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327" wrongKey := "aaaaaaa4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327" t.Run("encryption=disabled", func(t *testing.T) { tmp := t.TempDir() dbPath := path.Join(tmp, "local.db") conn, err := sql.Open("turso", dbPath) require.Nil(t, err) require.Nil(t, conn.Ping()) _, err = conn.Exec("CREATE TABLE t(x)") require.Nil(t, err) _, err = conn.Exec("INSERT INTO t SELECT 'secret' FROM generate_series(1, 1024)") require.Nil(t, err) _, err = conn.Exec("PRAGMA wal_checkpoint(TRUNCATE)") require.Nil(t, err) content, err := os.ReadFile(dbPath) require.Nil(t, err) require.True(t, bytes.Contains(content, []byte("secret"))) }) t.Run("encryption=enabled", func(t *testing.T) { tmp := t.TempDir() dbPath := path.Join(tmp, "local.db") dsn := fmt.Sprintf("%v?experimental=encryption&encryption_cipher=aegis256&encryption_hexkey=%s", dbPath, hexkey) conn, err := sql.Open("turso", dsn) require.Nil(t, err) require.Nil(t, conn.Ping()) _, err = conn.Exec("CREATE TABLE t(x)") require.Nil(t, err) _, err = conn.Exec("INSERT INTO t SELECT 'secret' FROM generate_series(1, 1024)") require.Nil(t, err) _, err = conn.Exec("PRAGMA wal_checkpoint(TRUNCATE)") require.Nil(t, err) content, err := os.ReadFile(dbPath) require.Nil(t, err) require.False(t, bytes.Contains(content, []byte("secret"))) conn.Close() }) t.Run("encryption=full_test", func(t *testing.T) { tmp := t.TempDir() dbPath := path.Join(tmp, "encrypted.db") dsn := fmt.Sprintf("%v?experimental=encryption&encryption_cipher=aegis256&encryption_hexkey=%s", dbPath, hexkey) conn, err := sql.Open("turso", dsn) require.Nil(t, err) require.Nil(t, conn.Ping()) _, err = conn.Exec("CREATE TABLE t(x)") require.Nil(t, err) _, err = conn.Exec("INSERT INTO t SELECT 'secret' FROM generate_series(1, 1024)") require.Nil(t, err) _, err = conn.Exec("PRAGMA wal_checkpoint(TRUNCATE)") require.Nil(t, err) conn.Close() content, err := os.ReadFile(dbPath) require.Nil(t, err) require.Greater(t, len(content), 16*1024) require.False(t, bytes.Contains(content, []byte("secret"))) // verify we can re-open with the same key conn2, err := sql.Open("turso", dsn) require.Nil(t, err) var count int err = conn2.QueryRow("SELECT count(*) FROM t").Scan(&count) require.Nil(t, err) require.Equal(t, 1024, count) conn2.Close() // verify opening with wrong key fails wrongDsn := fmt.Sprintf("%v?experimental=encryption&encryption_cipher=aegis256&encryption_hexkey=%s", dbPath, wrongKey) conn3, err := sql.Open("turso", wrongDsn) require.Nil(t, err) // open succeeds but query should fail _, err = conn3.Exec("SELECT * FROM t") require.NotNil(t, err) conn3.Close() // verify opening without encryption fails conn4, err := sql.Open("turso", dbPath) require.Nil(t, err) // Open succeeds but query should fail _, err = conn4.Exec("SELECT * FROM t") require.NotNil(t, err) conn4.Close() }) } func TestInsertData(t *testing.T) { err := insertData(conn) if err != nil { t.Fatalf("Error inserting data: %v", err) } } func TestQuery(t *testing.T) { query := "SELECT * FROM test;" stmt, err := conn.Prepare(query) if err != nil { t.Fatalf("Error preparing query: %v", err) } defer stmt.Close() rows, err := stmt.Query() if err != nil { t.Fatalf("Error executing query: %v", err) } defer rows.Close() expectedCols := []string{"foo", "bar", "baz"} cols, err := rows.Columns() if err != nil { t.Fatalf("Error getting columns: %v", err) } if len(cols) != len(expectedCols) { t.Fatalf("Expected %d columns, got %d", len(expectedCols), len(cols)) } for i, col := range cols { if col != expectedCols[i] { t.Errorf("Expected column %d to be %s, got %s", i, expectedCols[i], col) } } i := 1 for rows.Next() { var a int var b string var c []byte err = rows.Scan(&a, &b, &c) if err != nil { t.Fatalf("Error scanning row: %v", err) } if a != i || b != rowsMap[i] || !slicesAreEq(c, []byte(rowsMap[i])) { t.Fatalf("Expected %d, %s, %s, got %d, %s, %s", i, rowsMap[i], rowsMap[i], a, b, string(c)) } fmt.Println("RESULTS: ", a, b, string(c)) i++ } if err = rows.Err(); err != nil { t.Fatalf("Row iteration error: %v", err) } } func TestFunctions(t *testing.T) { insert := "INSERT INTO test (foo, bar, baz) VALUES (?, ?, zeroblob(?));" stmt, err := conn.Prepare(insert) if err != nil { t.Fatalf("Error preparing statement: %v", err) } _, err = stmt.Exec(60, "TestFunction", 400) if err != nil { t.Fatalf("Error executing statement with arguments: %v", err) } stmt.Close() stmt, err = conn.Prepare("SELECT baz FROM test where foo = ?") if err != nil { t.Fatalf("Error preparing select stmt: %v", err) } defer stmt.Close() rows, err := stmt.Query(60) if err != nil { t.Fatalf("Error executing select stmt: %v", err) } defer rows.Close() for rows.Next() { var b []byte err = rows.Scan(&b) if err != nil { t.Fatalf("Error scanning row: %v", err) } if len(b) != 400 { t.Fatalf("Expected 100 bytes, got %d", len(b)) } } sql := "SELECT uuid4_str();" stmt, err = conn.Prepare(sql) if err != nil { t.Fatalf("Error preparing statement: %v", err) } defer stmt.Close() rows, err = stmt.Query() if err != nil { t.Fatalf("Error executing query: %v", err) } defer rows.Close() var i int for rows.Next() { var b string err = rows.Scan(&b) if err != nil { t.Fatalf("Error scanning row: %v", err) } if len(b) != 36 { t.Fatalf("Expected 36 bytes, got %d", len(b)) } i++ fmt.Printf("uuid: %s\n", b) } if i != 1 { t.Fatalf("Expected 1 row, got %d", i) } fmt.Println("zeroblob + uuid functions passed") } func TestDuplicateConnection(t *testing.T) { newConn := openMem(t) err := createTable(newConn) if err != nil { t.Fatalf("Error creating table: %v", err) } err = insertData(newConn) if err != nil { t.Fatalf("Error inserting data: %v", err) } query := "SELECT * FROM test;" rows, err := newConn.Query(query) if err != nil { t.Fatalf("Error executing query: %v", err) } defer rows.Close() for rows.Next() { var a int var b string var c []byte err = rows.Scan(&a, &b, &c) if err != nil { t.Fatalf("Error scanning row: %v", err) } fmt.Println("RESULTS: ", a, b, string(c)) } } func TestDuplicateConnection2(t *testing.T) { newConn := openMem(t) sql := "CREATE TABLE test (foo INTEGER, bar INTEGER, baz BLOB);" newConn.Exec(sql) sql = "INSERT INTO test (foo, bar, baz) VALUES (?, ?, uuid4());" stmt, err := newConn.Prepare(sql) require.Nil(t, err) stmt.Exec(242345, 2342434) defer stmt.Close() query := "SELECT * FROM test;" rows, err := newConn.Query(query) if err != nil { t.Fatalf("Error executing query: %v", err) } defer rows.Close() for rows.Next() { var a int var b int var c []byte err = rows.Scan(&a, &b, &c) if err != nil { t.Fatalf("Error scanning row: %v", err) } fmt.Println("RESULTS: ", a, b, string(c)) if len(c) != 16 { t.Fatalf("Expected 16 bytes, got %d", len(c)) } } } func TestConnectionError(t *testing.T) { newConn := openMem(t) sql := "CREATE TABLE test (foo INTEGER, bar INTEGER, baz BLOB);" newConn.Exec(sql) sql = "INSERT INTO test (foo, bar, baz) VALUES (?, ?, notafunction(?));" _, err := newConn.Prepare(sql) if err == nil { t.Fatalf("Expected error, got nil") } expectedErr := "turso: error: Parse error: unknown function notafunction" if err.Error() != expectedErr { t.Fatalf("Error test failed, expected: %s, found: %v", expectedErr, err) } fmt.Println("Connection error test passed") } func TestStatementError(t *testing.T) { newConn := openMem(t) sql := "CREATE TABLE test (foo INTEGER, bar INTEGER, baz BLOB);" newConn.Exec(sql) sql = "INSERT INTO test (foo, bar, baz) VALUES (?, ?, ?);" stmt, err := newConn.Prepare(sql) if err != nil { t.Fatalf("Error preparing statement: %v", err) } _, err = stmt.Exec(1, 2) if err == nil { t.Fatalf("Expected error, got nil") } if err.Error() != "sql: expected 3 arguments, got 2" { t.Fatalf("Unexpected : %v\n", err) } fmt.Println("Statement error test passed") } func TestDriverRowsErrorMessages(t *testing.T) { db := openMem(t) _, err := db.Exec("CREATE TABLE test (id INTEGER, name TEXT)") if err != nil { t.Fatalf("failed to create table: %v", err) } _, err = db.Exec("INSERT INTO test (id, name) VALUES (?, ?)", 1, "Alice") if err != nil { t.Fatalf("failed to insert row: %v", err) } rows, err := db.Query("SELECT id, name FROM test") if err != nil { t.Fatalf("failed to query table: %v", err) } if !rows.Next() { t.Fatalf("expected at least one row") } var id int var name string err = rows.Scan(&name, &id) if err == nil { t.Fatalf("expected error scanning wrong type: %v", err) } t.Log("Rows error behavior test passed") } func TestTransaction(t *testing.T) { // Open database connection db := openMem(t) // Create a test table _, err := db.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)") if err != nil { t.Fatalf("Error creating table: %v", err) } // Insert initial data _, err = db.Exec("INSERT INTO test (id, name) VALUES (1, 'Initial')") if err != nil { t.Fatalf("Error inserting initial data: %v", err) } // Begin a transaction tx, err := db.Begin() if err != nil { t.Fatalf("Error starting transaction: %v", err) } // Insert data within the transaction _, err = tx.Exec("INSERT INTO test (id, name) VALUES (2, 'Transaction')") if err != nil { t.Fatalf("Error inserting data in transaction: %v", err) } // Commit the transaction err = tx.Commit() if err != nil { t.Fatalf("Error committing transaction: %v", err) } // Verify both rows are visible after commit rows, err := db.Query("SELECT id, name FROM test ORDER BY id") if err != nil { t.Fatalf("Error querying data after commit: %v", err) } defer rows.Close() expected := []struct { id int name string }{ {1, "Initial"}, {2, "Transaction"}, } i := 0 for rows.Next() { var id int var name string if err := rows.Scan(&id, &name); err != nil { t.Fatalf("Error scanning row: %v", err) } if id != expected[i].id || name != expected[i].name { t.Errorf("Row %d: expected (%d, %s), got (%d, %s)", i, expected[i].id, expected[i].name, id, name) } i++ } if i != 2 { t.Fatalf("Expected 2 rows, got %d", i) } t.Log("Transaction test passed") } func TestVectorOperations(t *testing.T) { db := openMem(t) // Test creating table with vector columns _, err := db.Exec(`CREATE TABLE vector_test (id INTEGER PRIMARY KEY, embedding F32_BLOB(64))`) if err != nil { t.Fatalf("Error creating vector table: %v", err) } // Test vector insertion _, err = db.Exec(`INSERT INTO vector_test VALUES (1, vector('[0.1, 0.2, 0.3, 0.4, 0.5]'))`) if err != nil { t.Fatalf("Error inserting vector: %v", err) } // Test vector similarity calculation var similarity float64 err = db.QueryRow(`SELECT vector_distance_cos(embedding, vector('[0.2, 0.3, 0.4, 0.5, 0.6]')) FROM vector_test WHERE id = 1`).Scan(&similarity) if err != nil { t.Fatalf("Error calculating vector similarity: %v", err) } if similarity <= 0 || similarity > 1 { t.Fatalf("Expected similarity between 0 and 1, got %f", similarity) } // Test vector extraction var extracted string err = db.QueryRow(`SELECT vector_extract(embedding) FROM vector_test WHERE id = 1`).Scan(&extracted) if err != nil { t.Fatalf("Error extracting vector: %v", err) } fmt.Printf("Extracted vector: %s\n", extracted) } func TestSQLFeatures(t *testing.T) { db := openMem(t) // Create test tables _, err := db.Exec(` CREATE TABLE customers ( id INTEGER PRIMARY KEY, name TEXT, age INTEGER )`) if err != nil { t.Fatalf("Error creating customers table: %v", err) } _, err = db.Exec(` CREATE TABLE orders ( id INTEGER PRIMARY KEY, customer_id INTEGER, amount REAL, date TEXT )`) if err != nil { t.Fatalf("Error creating orders table: %v", err) } // Insert test data _, err = db.Exec(` INSERT INTO customers VALUES (1, 'Alice', 30), (2, 'Bob', 25), (3, 'Charlie', 40)`) if err != nil { t.Fatalf("Error inserting customers: %v", err) } _, err = db.Exec(` INSERT INTO orders VALUES (1, 1, 100.50, '2024-01-01'), (2, 1, 200.75, '2024-02-01'), (3, 2, 50.25, '2024-01-15'), (4, 3, 300.00, '2024-02-10')`) if err != nil { t.Fatalf("Error inserting orders: %v", err) } // Test JOIN rows, err := db.Query(` SELECT c.name, o.amount FROM customers c INNER JOIN orders o ON c.id = o.customer_id ORDER BY o.amount DESC`) if err != nil { t.Fatalf("Error executing JOIN: %v", err) } defer rows.Close() // Check JOIN results expectedResults := []struct { name string amount float64 }{ {"Charlie", 300.00}, {"Alice", 200.75}, {"Alice", 100.50}, {"Bob", 50.25}, } i := 0 for rows.Next() { var name string var amount float64 if err := rows.Scan(&name, &amount); err != nil { t.Fatalf("Error scanning JOIN result: %v", err) } if i >= len(expectedResults) { t.Fatalf("Too many rows returned from JOIN") } if name != expectedResults[i].name || amount != expectedResults[i].amount { t.Fatalf("Row %d: expected (%s, %.2f), got (%s, %.2f)", i, expectedResults[i].name, expectedResults[i].amount, name, amount) } i++ } // Test GROUP BY with aggregation var count int var total float64 err = db.QueryRow(` SELECT COUNT(*), SUM(amount) FROM orders WHERE customer_id = 1 GROUP BY customer_id`).Scan(&count, &total) if err != nil { t.Fatalf("Error executing GROUP BY: %v", err) } if count != 2 || total != 301.25 { t.Fatalf("GROUP BY gave wrong results: count=%d, total=%.2f", count, total) } } func TestDateTimeFunctions(t *testing.T) { db := openMem(t) // Test date() var dateStr string err := db.QueryRow(`SELECT date('now')`).Scan(&dateStr) if err != nil { t.Fatalf("Error with date() function: %v", err) } fmt.Printf("Current date: %s\n", dateStr) // Test date arithmetic err = db.QueryRow(`SELECT date('2024-01-01', '+1 month')`).Scan(&dateStr) if err != nil { t.Fatalf("Error with date arithmetic: %v", err) } if dateStr != "2024-02-01" { t.Fatalf("Expected '2024-02-01', got '%s'", dateStr) } // Test strftime var formatted string err = db.QueryRow(`SELECT strftime('%Y-%m-%d', '2024-01-01')`).Scan(&formatted) if err != nil { t.Fatalf("Error with strftime function: %v", err) } if formatted != "2024-01-01" { t.Fatalf("Expected '2024-01-01', got '%s'", formatted) } } func TestMathFunctions(t *testing.T) { db := openMem(t) // Test basic math functions var result float64 err := db.QueryRow(`SELECT abs(-15.5)`).Scan(&result) if err != nil { t.Fatalf("Error with abs function: %v", err) } if result != 15.5 { t.Fatalf("abs(-15.5) should be 15.5, got %f", result) } // Test trigonometric functions err = db.QueryRow(`SELECT round(sin(radians(30)), 4)`).Scan(&result) if err != nil { t.Fatalf("Error with sin function: %v", err) } if math.Abs(result-0.5) > 0.0001 { t.Fatalf("sin(30 degrees) should be about 0.5, got %f", result) } // Test power functions err = db.QueryRow(`SELECT pow(2, 3)`).Scan(&result) if err != nil { t.Fatalf("Error with pow function: %v", err) } if result != 8 { t.Fatalf("2^3 should be 8, got %f", result) } } func TestJSONFunctions(t *testing.T) { db := openMem(t) // Test json function var valid int err := db.QueryRow(`SELECT json_valid('{"name":"John","age":30}')`).Scan(&valid) if err != nil { t.Fatalf("Error with json_valid function: %v", err) } if valid != 1 { t.Fatalf("Expected valid JSON to return 1, got %d", valid) } // Test json_extract var name string err = db.QueryRow(`SELECT json_extract('{"name":"John","age":30}', '$.name')`).Scan(&name) if err != nil { t.Fatalf("Error with json_extract function: %v", err) } if name != "John" { t.Fatalf("Expected 'John', got '%s'", name) } // Test JSON shorthand var age int err = db.QueryRow(`SELECT '{"name":"John","age":30}' -> '$.age'`).Scan(&age) if err != nil { t.Fatalf("Error with JSON shorthand: %v", err) } if age != 30 { t.Fatalf("Expected 30, got %d", age) } } func TestParameterOrdering(t *testing.T) { newConn := openMem(t) sql := "CREATE TABLE test (a,b,c);" newConn.Exec(sql) // Test inserting with parameters in a different order than // the table definition. sql = "INSERT INTO test (b, c ,a) VALUES (?, ?, ?);" expectedValues := []int{1, 2, 3} stmt, err := newConn.Prepare(sql) require.Nil(t, err) _, err = stmt.Exec(expectedValues[1], expectedValues[2], expectedValues[0]) if err != nil { t.Fatalf("Error preparing statement: %v", err) } // check that the values are in the correct order query := "SELECT a,b,c FROM test;" rows, err := newConn.Query(query) if err != nil { t.Fatalf("Error executing query: %v", err) } for rows.Next() { var a, b, c int err := rows.Scan(&a, &b, &c) if err != nil { t.Fatal("Error scanning row: ", err) } result := []int{a, b, c} for i := range 3 { if result[i] != expectedValues[i] { fmt.Printf("RESULTS: %d, %d, %d\n", a, b, c) fmt.Printf("EXPECTED: %d, %d, %d\n", expectedValues[0], expectedValues[1], expectedValues[2]) } } } // -- part 2 -- // mixed parameters and regular values sql2 := "CREATE TABLE test2 (a,b,c);" newConn.Exec(sql2) expectedValues2 := []int{1, 2, 3} // Test inserting with parameters in a different order than // the table definition, with a mixed regular parameter included sql2 = "INSERT INTO test2 (a, b ,c) VALUES (1, ?, ?);" _, err = newConn.Exec(sql2, expectedValues2[1], expectedValues2[2]) if err != nil { t.Fatalf("Error preparing statement: %v", err) } // check that the values are in the correct order query2 := "SELECT a,b,c FROM test2;" rows2, err := newConn.Query(query2) if err != nil { t.Fatalf("Error executing query: %v", err) } for rows2.Next() { var a, b, c int err := rows2.Scan(&a, &b, &c) if err != nil { t.Fatal("Error scanning row: ", err) } result := []int{a, b, c} fmt.Printf("RESULTS: %d, %d, %d\n", a, b, c) fmt.Printf("EXPECTED: %d, %d, %d\n", expectedValues[0], expectedValues[1], expectedValues[2]) for i := range 3 { if result[i] != expectedValues[i] { t.Fatalf("Expected %d, got %d", expectedValues[i], result[i]) } } } } func TestLimitOffsetParameters(t *testing.T) { newConn := openMem(t) sql := "CREATE TABLE test (a, b);" _, err := newConn.Exec(sql) if err != nil { t.Fatal("Error creating table") } sql = "INSERT INTO test (a, b) VALUES (1, 'a'), (2,'b'), (3,'c'), (4,'c'), (5,'d');" _, err = newConn.Exec(sql) if err != nil { t.Fatal("Error inserting data") } sql = "SELECT a, b FROM test ORDER BY b DESC LIMIT ? OFFSET ?;" query, err := newConn.Prepare(sql) if err != nil { t.Fatalf("Error preparing statement: %v", err) } limit := 2 offset := 1 expected := []int{4, 3} rows, err := query.Query(limit, offset) if err != nil { t.Fatalf("Error executing query: %v", err) } var a int var b string for rows.Next() { rows.Scan(&a, &b) if a != expected[0] && a != expected[1] { t.Fatalf("Expected %d or %d, got %d", expected[0], expected[1], a) } } } func TestIndex(t *testing.T) { newConn := openMem(t) sql := "CREATE TABLE users (name TEXT PRIMARY KEY, email TEXT)" _, err := newConn.Exec(sql) if err != nil { t.Fatalf("Error creating table: %v", err) } sql = "CREATE INDEX email_idx ON users(email)" _, err = newConn.Exec(sql) if err != nil { t.Fatalf("Error creating index: %v", err) } // Test inserting with parameters in a different order than // the table definition. sql = "INSERT INTO users VALUES ('alice', 'a@b.c'), ('bob', 'b@d.e')" _, err = newConn.Exec(sql) if err != nil { t.Fatalf("Error inserting data: %v", err) } for filter, row := range map[string][]string{ "a@b.c": {"alice", "a@b.c"}, "b@d.e": {"bob", "b@d.e"}, } { query := "SELECT * FROM users WHERE email = ?" rows, err := newConn.Query(query, filter) if err != nil { t.Fatalf("Error executing query: %v", err) } for rows.Next() { var name, email string err := rows.Scan(&name, &email) t.Log("name,email:", name, email) if err != nil { t.Fatal("Error scanning row: ", err) } if !slices.Equal([]string{name, email}, row) { t.Fatal("Unexpected result", row, []string{name, email}) } } } } func slicesAreEq(a, b []byte) bool { if len(a) != len(b) { fmt.Printf("LENGTHS NOT EQUAL: %d != %d\n", len(a), len(b)) return false } for i := range a { if a[i] != b[i] { fmt.Printf("SLICES NOT EQUAL: %v != %v\n", a, b) return false } } return true } var rowsMap = map[int]string{1: "hello", 2: "world", 3: "foo", 4: "bar", 5: "baz"} func createTable(conn *sql.DB) error { insert := "CREATE TABLE test (foo INT, bar TEXT, baz BLOB);" stmt, err := conn.Prepare(insert) if err != nil { return err } defer stmt.Close() _, err = stmt.Exec() return err } func insertData(conn *sql.DB) error { for i := 1; i <= 5; i++ { insert := "INSERT INTO test (foo, bar, baz) VALUES (?, ?, ?);" stmt, err := conn.Prepare(insert) if err != nil { return err } defer stmt.Close() if _, err = stmt.Exec(i, rowsMap[i], []byte(rowsMap[i])); err != nil { return err } } return nil } func TestNullHandling(t *testing.T) { db := openMem(t) _, err := db.Exec(` CREATE TABLE null_test ( id INTEGER PRIMARY KEY, text_val TEXT, int_val INTEGER, real_val REAL, blob_val BLOB )`) if err != nil { t.Fatalf("Error creating table: %v", err) } testCases := []struct { name string query string args []any expected []any }{ {"all nulls", "INSERT INTO null_test (id) VALUES (?)", []any{1}, []any{1, nil, nil, nil, nil}}, {"mixed nulls", "INSERT INTO null_test VALUES (?, ?, ?, ?, ?)", []any{2, "text", nil, 3.14, nil}, []any{2, "text", nil, 3.14, nil}}, {"no nulls", "INSERT INTO null_test VALUES (?, ?, ?, ?, ?)", []any{3, "full", 42, 2.718, []byte("data")}, []any{3, "full", 42, 2.718, []byte("data")}}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { _, err := db.Exec(tc.query, tc.args...) if err != nil { t.Fatalf("Error inserting: %v", err) } }) } rows, err := db.Query("SELECT * FROM null_test ORDER BY id") if err != nil { t.Fatalf("Error querying: %v", err) } defer rows.Close() i := 0 for rows.Next() { var id sql.NullInt64 var textVal sql.NullString var intVal sql.NullInt64 var realVal sql.NullFloat64 var blobVal []byte err := rows.Scan(&id, &textVal, &intVal, &realVal, &blobVal) if err != nil { t.Fatalf("Error scanning: %v", err) } if !id.Valid { t.Errorf("ID should always be valid") } i++ } if i != 3 { t.Fatalf("Expected 3 rows, got %d", i) } } func mustExec(t *testing.T, db *sql.DB, q string, args ...any) sql.Result { t.Helper() res, err := db.Exec(q, args...) if err != nil { t.Fatalf("exec %q: %v", q, err) } return res } func TestLastInsertIDAndRowsAffected(t *testing.T) { db := openMem(t) mustExec(t, db, `CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT)`) res := mustExec(t, db, `INSERT INTO t(name) VALUES ('alice')`) id, err := res.LastInsertId() if err != nil { t.Fatalf("LastInsertId: %v", err) } if id == 0 { t.Fatalf("expected non-zero last insert id") } res = mustExec(t, db, `UPDATE t SET name='ALICE' WHERE id = ?`, id) ra, err := res.RowsAffected() if err != nil { t.Fatalf("RowsAffected: %v", err) } if ra != 1 { t.Fatalf("expected 1 row affected, got %d", ra) } } func TestDataTypes(t *testing.T) { db, err := sql.Open("turso", ":memory:") if err != nil { t.Fatalf("Error opening connection: %v", err) } defer db.Close() _, err = db.Exec(` CREATE TABLE types_test ( col_integer INTEGER, col_real REAL, col_text TEXT, col_blob BLOB, col_numeric NUMERIC, col_boolean BOOLEAN, col_date DATE, col_datetime DATETIME, col_timestamp TIMESTAMP )`) if err != nil { t.Fatalf("Error creating table: %v", err) } // Insert test data now := time.Now() _, err = db.Exec(` INSERT INTO types_test VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, 42, 3.14159, "Hello, 世界", []byte{0x01, 0x02, 0x03}, "123.456", true, now.Format("2006-01-02"), now.Format("2006-01-02 15:04:05"), now.Unix(), ) if err != nil { t.Fatalf("Error inserting: %v", err) } // Query and verify each type var ( colInt int colReal float64 colText string colBlob []byte colNumeric string colBool bool colDate string colDateTime string colTimestamp int64 ) err = db.QueryRow("SELECT * FROM types_test").Scan( &colInt, &colReal, &colText, &colBlob, &colNumeric, &colBool, &colDate, &colDateTime, &colTimestamp, ) if err != nil { t.Fatalf("Error scanning: %v", err) } // Verify values if colInt != 42 { t.Errorf("Integer mismatch: got %d", colInt) } if math.Abs(colReal-3.14159) > 0.00001 { t.Errorf("Real mismatch: got %f", colReal) } if colText != "Hello, 世界" { t.Errorf("Text mismatch: got %s", colText) } if !slices.Equal(colBlob, []byte{0x01, 0x02, 0x03}) { t.Errorf("Blob mismatch: got %v", colBlob) } } func createDatabasesTable(t *testing.T, db *sql.DB) { t.Helper() _, err := db.Exec(` CREATE TABLE IF NOT EXISTS databases ( id INTEGER PRIMARY KEY, created_at TEXT, updated_at TEXT, deleted_at TEXT, hostname TEXT UNIQUE, namespace TEXT, fly_app TEXT, address TEXT, primary_address TEXT, cloud_cluster_name TEXT, local BOOLEAN, allowed_ips TEXT )`) if err != nil { t.Fatalf("create table: %v", err) } } func TestUpsertReturning_databaseSQL_Prepared(t *testing.T) { db := openMem(t) createDatabasesTable(t, db) const stmtText = ` INSERT INTO databases (created_at,updated_at,deleted_at,hostname,namespace,fly_app,address,primary_address,cloud_cluster_name,local,allowed_ips) VALUES (?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT (hostname) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hostname=excluded.hostname, namespace=excluded.namespace, fly_app=excluded.fly_app, address=excluded.address, primary_address=excluded.primary_address, cloud_cluster_name=excluded.cloud_cluster_name, local=excluded.local, allowed_ips=excluded.allowed_ips RETURNING id` now := time.Now() args := []any{ now, // created_at (driver will send RFC3339 string) now, // updated_at nil, // deleted_at "host-1.local", // hostname (unique) "ns-123", // namespace "app-xyz", // fly_app "http://127.0.0.1:10000", // address "", // primary_address "local", // cloud_cluster_name false, // local (bool -> int 0/1 in your marshaler) nil, // allowed_ips (NULL) } stmt, err := db.Prepare(stmtText) if err != nil { t.Fatalf("prepare: %v", err) } defer stmt.Close() var returnedID int64 if err := stmt.QueryRow(args...).Scan(&returnedID); err != nil { t.Fatalf("queryrow/scan: %v", err) } t.Logf("returned id: %d", returnedID) cpy := returnedID // Re-run to trigger ON CONFLICT path and ensure still binds 12 args and returns id args[1] = time.Now() // updated_at if err := stmt.QueryRow(args...).Scan(&returnedID); err != nil { t.Fatalf("queryrow/scan (conflict): %v", err) } if returnedID != cpy { t.Fatalf("expected same id on conflict, got %d then %d", cpy, returnedID) } } func TestInsertReturning(t *testing.T) { db := openMem(t) _, err := db.Exec(`CREATE TABLE IF NOT EXISTS t (x)`) if err != nil { t.Fatalf("create table: %v", err) } var returnedID int64 if err := db.QueryRow("INSERT INTO t VALUES (1) RETURNING x").Scan(&returnedID); err != nil { t.Fatalf("queryrow/scan: %v", err) } if returnedID != 1 { t.Fatalf("unexpected returnedId: %v", err) } t.Log(returnedID) if err := db.QueryRow("SELECT * FROM t").Scan(&returnedID); err != nil { t.Fatalf("queryrow/scan (conflict): %v", err) } if returnedID != 1 { t.Fatalf("unexpected returnedId: %v", err) } t.Log(returnedID) } func TestUpsertReturning_databaseSQL_Prepared_ArgCountMismatch(t *testing.T) { db := openMem(t) createDatabasesTable(t, db) const stmtText = ` INSERT INTO databases (created_at,updated_at,deleted_at,hostname,namespace,fly_app,address,primary_address,cloud_cluster_name,local,allowed_ips,id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT (hostname) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hostname=excluded.hostname, namespace=excluded.namespace, fly_app=excluded.fly_app, address=excluded.address, primary_address=excluded.primary_address, cloud_cluster_name=excluded.cloud_cluster_name, local=excluded.local, allowed_ips=excluded.allowed_ips RETURNING id` stmt, err := db.Prepare(stmtText) if err != nil { t.Fatalf("prepare: %v", err) } defer stmt.Close() now := time.Now() args := []any{ now, now, nil, "host-2.local", "ns", "app", "addr", "", "local", false, nil, 22, } // Append a bogus 13th arg to force the exact database/sql error you saw args = append(args, 999) var id int64 if err := stmt.QueryRow(args...).Scan(&id); err == nil { t.Fatal("expected argument count error, got nil") } } func TestMultiStatementExecution(t *testing.T) { db := openMem(t) t.Run("BasicMultiStatement", func(t *testing.T) { _, err := db.Exec(` CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); INSERT INTO users (name) VALUES ('Alice'); INSERT INTO users (name) VALUES ('Bob'); `) if err != nil { t.Fatalf("Failed to execute multi-statement: %v", err) } var count int err = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count) if err != nil { t.Fatalf("Failed to query count: %v", err) } if count != 2 { t.Errorf("Expected 2 rows, got %d", count) } }) t.Run("StringsWithSemicolons", func(t *testing.T) { _, err := db.Exec(` CREATE TABLE messages (id INTEGER PRIMARY KEY, text TEXT); INSERT INTO messages (text) VALUES ('Hello; World'); INSERT INTO messages (text) VALUES ('Test; Message; Multiple'); `) if err != nil { t.Fatalf("Failed to execute with semicolons in strings: %v", err) } var count int err = db.QueryRow("SELECT COUNT(*) FROM messages").Scan(&count) if err != nil { t.Fatalf("Failed to query count: %v", err) } if count != 2 { t.Errorf("Expected 2 rows, got %d", count) } rows, err := db.Query("SELECT text FROM messages ORDER BY id") if err != nil { t.Fatalf("Failed to query messages: %v", err) } defer rows.Close() expected := []string{"Hello; World", "Test; Message; Multiple"} i := 0 for rows.Next() { var text string if err := rows.Scan(&text); err != nil { t.Fatalf("Failed to scan: %v", err) } if text != expected[i] { t.Errorf("Row %d: expected %q, got %q", i, expected[i], text) } i++ } }) t.Run("EscapedQuotes", func(t *testing.T) { _, err := db.Exec(` CREATE TABLE names (id INTEGER PRIMARY KEY, name TEXT); INSERT INTO names (name) VALUES ('O''Brien'); INSERT INTO names (name) VALUES ('It''s working'); `) if err != nil { t.Fatalf("Failed to execute with escaped quotes: %v", err) } var count int err = db.QueryRow("SELECT COUNT(*) FROM names").Scan(&count) if err != nil { t.Fatalf("Failed to query count: %v", err) } if count != 2 { t.Errorf("Expected 2 rows, got %d", count) } var name string err = db.QueryRow("SELECT name FROM names WHERE id = 1").Scan(&name) if err != nil { t.Fatalf("Failed to query name: %v", err) } if name != "O'Brien" { t.Errorf("Expected \"O'Brien\", got %q", name) } }) t.Run("EmptyStatements", func(t *testing.T) { _, err := db.Exec(` CREATE TABLE test_empty (id INTEGER);;; INSERT INTO test_empty (id) VALUES (1);; `) if err != nil { t.Fatalf("Failed to execute with empty statements: %v", err) } var count int err = db.QueryRow("SELECT COUNT(*) FROM test_empty").Scan(&count) if err != nil { t.Fatalf("Failed to query count: %v", err) } if count != 1 { t.Errorf("Expected 1 row, got %d", count) } }) t.Run("FailureInMiddle", func(t *testing.T) { _, err := db.Exec(` CREATE TABLE partial (id INTEGER PRIMARY KEY); INSERT INTO partial (id) VALUES (1); INSERT INTO partial (id) VALUES (1); `) if err == nil { t.Fatal("Expected error for duplicate key, got nil") } var count int err = db.QueryRow("SELECT COUNT(*) FROM partial").Scan(&count) if err != nil { t.Fatalf("Failed to query count: %v", err) } if count != 1 { t.Errorf("Expected 1 row (from first INSERT before failure), got %d", count) } }) t.Run("WithParameters", func(t *testing.T) { _, err := db.Exec(`CREATE TABLE param_test (id INTEGER, name TEXT);`) if err != nil { t.Fatalf("Failed to create table: %v", err) } _, err = db.Exec("INSERT INTO param_test (id, name) VALUES (?, ?)", 1, "Test") if err != nil { t.Fatalf("Failed to insert with parameters: %v", err) } var count int err = db.QueryRow("SELECT COUNT(*) FROM param_test").Scan(&count) if err != nil { t.Fatalf("Failed to query count: %v", err) } if count != 1 { t.Errorf("Expected 1 row, got %d", count) } }) } func TestTimeValueRoundtrip(t *testing.T) { db := openMem(t) _, err := db.Exec(`CREATE TABLE time_test ( id INTEGER PRIMARY KEY, created_at DATETIME, updated_at DATETIME, deleted_at TIMESTAMP )`) require.NoError(t, err) // Use a fixed time for deterministic testing // time.Time values are stored as RFC3339Nano strings originalTime := time.Date(2024, 6, 15, 14, 30, 45, 123456789, time.UTC) laterTime := originalTime.Add(24 * time.Hour) // Insert using time.Time values _, err = db.Exec( `INSERT INTO time_test (id, created_at, updated_at, deleted_at) VALUES (?, ?, ?, ?)`, 1, originalTime, laterTime, nil, ) require.NoError(t, err) t.Run("scan into time.Time", func(t *testing.T) { var id int var createdAt, updatedAt time.Time var deletedAt sql.NullTime err := db.QueryRow(`SELECT id, created_at, updated_at, deleted_at FROM time_test WHERE id = 1`). Scan(&id, &createdAt, &updatedAt, &deletedAt) require.NoError(t, err) require.Equal(t, 1, id) require.True(t, originalTime.Equal(createdAt), "createdAt mismatch: expected %v, got %v", originalTime, createdAt) require.True(t, laterTime.Equal(updatedAt), "updatedAt mismatch: expected %v, got %v", laterTime, updatedAt) require.False(t, deletedAt.Valid, "deletedAt should be NULL") }) t.Run("scan into string then parse", func(t *testing.T) { var createdAtStr string err := db.QueryRow(`SELECT created_at FROM time_test WHERE id = 1`).Scan(&createdAtStr) require.NoError(t, err) // Verify the stored format is RFC3339Nano parsed, err := time.Parse(time.RFC3339Nano, createdAtStr) require.NoError(t, err) require.True(t, originalTime.Equal(parsed), "parsed time mismatch") }) t.Run("update with time.Time", func(t *testing.T) { newTime := originalTime.Add(48 * time.Hour) _, err := db.Exec(`UPDATE time_test SET updated_at = ? WHERE id = ?`, newTime, 1) require.NoError(t, err) var updatedAt time.Time err = db.QueryRow(`SELECT updated_at FROM time_test WHERE id = 1`).Scan(&updatedAt) require.NoError(t, err) require.True(t, newTime.Equal(updatedAt), "updated time mismatch") }) t.Run("query with time.Time parameter", func(t *testing.T) { // Insert another row anotherTime := originalTime.Add(72 * time.Hour) _, err := db.Exec(`INSERT INTO time_test (id, created_at) VALUES (?, ?)`, 2, anotherTime) require.NoError(t, err) // Query using time as parameter var id int err = db.QueryRow(`SELECT id FROM time_test WHERE created_at = ?`, originalTime).Scan(&id) require.NoError(t, err) require.Equal(t, 1, id) }) t.Run("prepared statement with time.Time", func(t *testing.T) { stmt, err := db.Prepare(`SELECT id, created_at FROM time_test WHERE created_at < ?`) require.NoError(t, err) defer stmt.Close() cutoff := originalTime.Add(1 * time.Hour) var id int var createdAt time.Time err = stmt.QueryRow(cutoff).Scan(&id, &createdAt) require.NoError(t, err) require.Equal(t, 1, id) require.True(t, originalTime.Equal(createdAt)) }) // expected behaviour - similar to the sqlite3 go driver as it uses decltype t.Run("transform datetime column", func(t *testing.T) { stmt, err := db.Prepare(`SELECT concat(created_at || '') FROM time_test`) require.NoError(t, err) defer stmt.Close() var createdAt string err = stmt.QueryRow().Scan(&createdAt) require.NoError(t, err) require.Equal(t, createdAt, originalTime.Format(time.RFC3339Nano)) }) } // --- Busy Timeout Tests --- func TestBusyTimeoutDefault(t *testing.T) { // Open a database without specifying busy timeout - should use default (5000ms) db, err := sql.Open("turso", ":memory:") require.NoError(t, err) defer db.Close() // Get the underlying connection and verify the timeout conn, err := db.Conn(t.Context()) require.NoError(t, err) defer conn.Close() err = conn.Raw(func(driverConn any) error { tc, ok := driverConn.(*tursoDbConnection) require.True(t, ok, "expected *tursoDbConnection") require.Equal(t, DefaultBusyTimeout, tc.GetBusyTimeout(), "expected default busy timeout of %d, got %d", DefaultBusyTimeout, tc.GetBusyTimeout()) return nil }) require.NoError(t, err) fmt.Println("Default busy timeout test passed") } func TestBusyTimeoutDSN(t *testing.T) { // Test that _busy_timeout in DSN overrides the default db, err := sql.Open("turso", ":memory:?_busy_timeout=10000") require.NoError(t, err) defer db.Close() conn, err := db.Conn(t.Context()) require.NoError(t, err) defer conn.Close() err = conn.Raw(func(driverConn any) error { tc, ok := driverConn.(*tursoDbConnection) require.True(t, ok) require.Equal(t, 10000, tc.GetBusyTimeout(), "expected busy timeout of 10000, got %d", tc.GetBusyTimeout()) return nil }) require.NoError(t, err) fmt.Println("Busy timeout DSN test passed") } func TestBusyTimeoutDisabled(t *testing.T) { // Test that _busy_timeout=-1 disables the timeout db, err := sql.Open("turso", ":memory:?_busy_timeout=-1") require.NoError(t, err) defer db.Close() conn, err := db.Conn(t.Context()) require.NoError(t, err) defer conn.Close() err = conn.Raw(func(driverConn any) error { tc, ok := driverConn.(*tursoDbConnection) require.True(t, ok) require.Equal(t, 0, tc.GetBusyTimeout(), "expected busy timeout of 0 (disabled), got %d", tc.GetBusyTimeout()) return nil }) fmt.Println("Busy timeout disabled test passed") require.NoError(t, err) } func TestBusyTimeoutRuntimeChange(t *testing.T) { db, err := sql.Open("turso", ":memory:") require.NoError(t, err) defer db.Close() conn, err := db.Conn(t.Context()) require.NoError(t, err) defer conn.Close() err = conn.Raw(func(driverConn any) error { tc, ok := driverConn.(*tursoDbConnection) require.True(t, ok) // Check initial default require.Equal(t, DefaultBusyTimeout, tc.GetBusyTimeout()) // Change to custom value err := tc.SetBusyTimeout(15000) require.NoError(t, err) require.Equal(t, 15000, tc.GetBusyTimeout()) // Disable timeout err = tc.SetBusyTimeout(0) require.NoError(t, err) require.Equal(t, 0, tc.GetBusyTimeout()) return nil }) fmt.Println("Busy timeout runtime change test passed") require.NoError(t, err) } func TestBusyTimeoutConnector(t *testing.T) { t.Run("default timeout via connector", func(t *testing.T) { connector, err := NewConnector(":memory:") require.NoError(t, err) db := sql.OpenDB(connector) defer db.Close() conn, err := db.Conn(t.Context()) require.NoError(t, err) defer conn.Close() err = conn.Raw(func(driverConn any) error { tc, ok := driverConn.(*tursoDbConnection) require.True(t, ok) require.Equal(t, DefaultBusyTimeout, tc.GetBusyTimeout()) return nil }) require.NoError(t, err) }) t.Run("custom timeout via connector", func(t *testing.T) { connector, err := NewConnector(":memory:", WithBusyTimeout(20000)) require.NoError(t, err) db := sql.OpenDB(connector) defer db.Close() conn, err := db.Conn(t.Context()) require.NoError(t, err) defer conn.Close() err = conn.Raw(func(driverConn any) error { tc, ok := driverConn.(*tursoDbConnection) require.True(t, ok) require.Equal(t, 20000, tc.GetBusyTimeout()) return nil }) require.NoError(t, err) }) t.Run("disabled timeout via connector", func(t *testing.T) { connector, err := NewConnector(":memory:", WithBusyTimeout(0)) require.NoError(t, err) db := sql.OpenDB(connector) defer db.Close() conn, err := db.Conn(t.Context()) require.NoError(t, err) defer conn.Close() err = conn.Raw(func(driverConn any) error { tc, ok := driverConn.(*tursoDbConnection) require.True(t, ok) require.Equal(t, 0, tc.GetBusyTimeout()) return nil }) require.NoError(t, err) }) fmt.Println("Busy timeout connector test passed") } func TestBusyTimeoutConcurrentWrites(t *testing.T) { // This test verifies that with a busy timeout, concurrent writers succeed // instead of immediately failing with SQLITE_BUSY tmp := t.TempDir() dbPath := path.Join(tmp, "concurrent.db") // Open with default timeout db, err := sql.Open("turso", dbPath) require.NoError(t, err) defer db.Close() // Create table _, err = db.Exec("CREATE TABLE counter (id INTEGER PRIMARY KEY, value INTEGER)") require.NoError(t, err) _, err = db.Exec("INSERT INTO counter (id, value) VALUES (1, 0)") require.NoError(t, err) // Run concurrent updates const numGoroutines = 5 const numUpdates = 10 done := make(chan error, numGoroutines) for i := range numGoroutines { go func(workerID int) { for j := range numUpdates { _, err := db.Exec("UPDATE counter SET value = value + 1 WHERE id = 1") if err != nil { done <- fmt.Errorf("worker %d, update %d: %w", workerID, j, err) return } } done <- nil }(i) } // Collect results for range numGoroutines { err := <-done require.NoError(t, err, "concurrent update should succeed with busy timeout") } // Verify final count var finalValue int err = db.QueryRow("SELECT value FROM counter WHERE id = 1").Scan(&finalValue) require.NoError(t, err) require.Equal(t, numGoroutines*numUpdates, finalValue, "expected %d updates, got %d", numGoroutines*numUpdates, finalValue) fmt.Println("Busy timeout concurrent writes test passed") } func TestParallelSelectColumnsConcurrency(t *testing.T) { tmp := t.TempDir() dbPath := path.Join(tmp, "parallel_select_columns.db") db, err := sql.Open("turso", dbPath) require.NoError(t, err) defer db.Close() maxConns := runtime.GOMAXPROCS(0) if maxConns < 16 { maxConns = 16 } db.SetMaxOpenConns(maxConns) db.SetMaxIdleConns(maxConns) _, err = db.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)") require.NoError(t, err) _, err = db.Exec("INSERT INTO users (id, name, email) VALUES (1, 'alice', 'alice@example.com')") require.NoError(t, err) workers := runtime.GOMAXPROCS(0) * 4 if workers < 32 { workers = 32 } const roundsPerWorker = 12 const selectsPerRound = 300 start := make(chan struct{}) var wg sync.WaitGroup errCh := make(chan error, workers) for g := range workers { wg.Add(1) go func(workerID int) { defer wg.Done() <-start for round := 0; round < roundsPerWorker; round++ { for i := 0; i < selectsPerRound; i++ { rows, qerr := db.Query("SELECT name, email, name FROM users WHERE id = 1") if qerr != nil { errCh <- fmt.Errorf("worker %d query: %w", workerID, qerr) return } // Force column metadata fetch path on every query. cols, cerr := rows.Columns() if cerr != nil { _ = rows.Close() errCh <- fmt.Errorf("worker %d columns: %w", workerID, cerr) return } if len(cols) != 3 { _ = rows.Close() errCh <- fmt.Errorf("worker %d unexpected columns len=%d", workerID, len(cols)) return } for rows.Next() { var a, b, c string if serr := rows.Scan(&a, &b, &c); serr != nil { _ = rows.Close() errCh <- fmt.Errorf("worker %d scan: %w", workerID, serr) return } } if rerr := rows.Err(); rerr != nil { _ = rows.Close() errCh <- fmt.Errorf("worker %d rows err: %w", workerID, rerr) return } if closeErr := rows.Close(); closeErr != nil { errCh <- fmt.Errorf("worker %d close: %w", workerID, closeErr) return } // Perturb scheduling/memory often to surface pointer lifetime bugs faster. if i%128 == 0 { runtime.GC() runtime.Gosched() } } } }(g) } close(start) wg.Wait() close(errCh) for runErr := range errCh { require.NoError(t, runErr) } } ================================================ FILE: bindings/go/driver_sync.go ================================================ package turso import ( "bytes" "context" "database/sql" "database/sql/driver" "errors" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "strings" "sync" "time" turso_libs "github.com/tursodatabase/turso-go-platform-libs" ) // TursoPartialSyncConfig configures partial sync behavior. type TursoPartialSyncConfig struct { // if positive, prefix partial bootstrap strategy will be used BootstrapStrategyPrefix int // if not empty, query partial bootstrap strategy will be used BootstrapStrategyQuery string // optional parameter which defines segment size for lazy loading from remote server // one of valid BootstrapStrategy* values MUST be set in order for this setting to have some effect SegmentSize int // optional parameter which defines if pages prefetch must be enabled // one of valid BootstrapStrategy* values MUST be set in order for this setting to have some effect Prefetch bool } // Public configuration for a synced database. type TursoSyncDbConfig struct { // Path to the main database file locally. // Supports DSN-style options: "mydb.db?_busy_timeout=5000" // Supported options: // - _busy_timeout: busy timeout in milliseconds (default: 5000, use -1 to disable) Path string // remote url for the sync // remote_url MUST be used in all sync engine operations: during bootstrap and all further operations RemoteUrl string // remote namespace for the sync client (optional) Namespace string // token for remote authentication // auth token value WILL not have any prefix and must be used as "Authorization" header prepended with "Bearer " prefix AuthToken string // optional unique client name (library MUST use `turso-sync-go` if omitted) ClientName string // long polling timeout LongPollTimeoutMs int // if not set, initial bootstrap phase will be skipped and caller must call .pull(...) explicitly in order to get initial state from remote // default value is true BootstrapIfEmpty *bool // configuration for partial sync (disabled by default) // WARNING: This feature is EXPERIMENTAL PartialSyncExperimental TursoPartialSyncConfig // pass it as-is to the underlying connection ExperimentalFeatures string // BusyTimeout in milliseconds for database connections. // Default is 5000ms (5 seconds). Set to -1 to disable busy handler. // Can also be specified via Path DSN: "mydb.db?_busy_timeout=10000" // If both are set, this field takes precedence. BusyTimeout int } // statistics for the synced database. type TursoSyncDbStats struct { // amount of local operations written since last Pull(...) call CdcOperations int64 // size of the main WAL file MainWalSize int64 // size of the revert WAL file RevertWalSize int64 // last successful pull time LastPullUnixTime int64 // last successful push time LastPushUnixTime int64 // total amount of bytes sent over the network (both Push and Pull operations are tracked together) NetworkSentBytes int64 // total amount of bytes received over the network (both Push and Pull operations are tracked together) NetworkReceivedBytes int64 // opaque server revision - it MUST NOT be interpreted/parsed in any way Revision string } // define public structs here // TursoSyncDb is a high-level synced database wrapper built over driver_db.go and bindings_sync.go. // It provides push/pull sync operations and creates SQL connections backed by the sync engine. type TursoSyncDb struct { db TursoSyncDatabase baseURL string authToken string namespace string client *http.Client busyTimeout int // busy timeout in milliseconds (0 = disabled) mu sync.Mutex } // main constructor to create synced database func NewTursoSyncDb(ctx context.Context, config TursoSyncDbConfig) (*TursoSyncDb, error) { InitLibrary(turso_libs.LoadTursoLibraryConfig{}) if strings.TrimSpace(config.Path) == "" { return nil, errors.New("turso: empty Path in TursoSyncDbConfig") } clientName := config.ClientName if clientName == "" { clientName = "turso-sync-go" } bootstrap := true if config.BootstrapIfEmpty != nil { bootstrap = *config.BootstrapIfEmpty } // Parse DSN-style options from Path (e.g., "mydb.db?_busy_timeout=5000") dbPath, dsnOpts := parseSyncDSN(config.Path) // Determine busy timeout: explicit config field takes precedence over DSN busyTimeout := config.BusyTimeout if busyTimeout == 0 { // Not explicitly set in config, check DSN if dsnOpts.BusyTimeout != 0 { busyTimeout = dsnOpts.BusyTimeout } // If still 0, will use default when creating connections } remoteUrl := normalizeUrl(config.RemoteUrl) // Create sync database holder dbCfg := TursoDatabaseConfig{ Path: dbPath, ExperimentalFeatures: config.ExperimentalFeatures, AsyncIO: true, // MUST be true for external IO handling } syncCfg := TursoSyncDatabaseConfig{ Path: dbPath, RemoteUrl: remoteUrl, Namespace: config.Namespace, ClientName: clientName, LongPollTimeoutMs: config.LongPollTimeoutMs, BootstrapIfEmpty: bootstrap, ReservedBytes: 0, PartialBootstrapStrategyPrefix: config.PartialSyncExperimental.BootstrapStrategyPrefix, PartialBootstrapStrategyQuery: config.PartialSyncExperimental.BootstrapStrategyQuery, PartialBootstrapSegmentSize: config.PartialSyncExperimental.SegmentSize, PartialBootstrapPrefetch: config.PartialSyncExperimental.Prefetch, } sdb, err := turso_sync_database_new(dbCfg, syncCfg) if err != nil { return nil, err } d := &TursoSyncDb{ db: sdb, baseURL: strings.TrimRight(remoteUrl, "/"), authToken: strings.TrimSpace(config.AuthToken), namespace: config.Namespace, busyTimeout: busyTimeout, client: &http.Client{ // No global timeout to allow long-poll; rely on request context. Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, MaxIdleConns: 32, MaxIdleConnsPerHost: 32, IdleConnTimeout: 90 * time.Second, DisableCompression: false, }, }, } // Create/open database with bootstrap logic as needed. op, err := turso_sync_database_create(d.db) if err != nil { return nil, err } _, _, err = d.driveOpUntilDone(ctx, op) if err != nil { return nil, err } return d, nil } // create turso db local connnection // internal connector to integrate with database/sql pool type tursoSyncConnector struct{ db *TursoSyncDb } func (c *tursoSyncConnector) Connect(ctx context.Context) (driver.Conn, error) { c.db.mu.Lock() defer c.db.mu.Unlock() op, err := turso_sync_database_connect(c.db.db) if err != nil { return nil, err } kind, opFinal, err := c.db.driveOpUntilDone(ctx, op) if err != nil { return nil, err } defer turso_sync_operation_deinit(opFinal) if kind != TURSO_ASYNC_RESULT_CONNECTION { return nil, errors.New("turso: unexpected result kind when connecting") } conn, err := turso_sync_operation_result_extract_connection(opFinal) if err != nil { return nil, err } // Integrate with the base driver; provide extra IO hook to process one IO item per iteration. extra := func() error { return c.db.processOneIo() } dbConn := NewConnection(conn, extra) // Apply busy timeout - use default if not explicitly set timeout := c.db.busyTimeout if timeout == 0 { timeout = DefaultBusyTimeout // Apply sensible default } else if timeout < 0 { timeout = 0 // -1 means explicitly disable } if timeout > 0 { turso_connection_set_busy_timeout_ms(conn, int64(timeout)) } dbConn.busyTimeout = timeout return dbConn, nil } func (c *tursoSyncConnector) Driver() driver.Driver { return &tursoDbDriver{} } // create tursodb connection using NewConnection(...) from driver_db.go and tursoSyncConnector helper func (d *TursoSyncDb) Connect(ctx context.Context) (*sql.DB, error) { return sql.OpenDB(&tursoSyncConnector{db: d}), nil } // implement EXTRA sync methods // Pull fresh data from the remote // Pull DO NOT sent any local changes to the remote and instead "rebase" them on top of new changes from remote // Return true, if new changes were applied locally - otherwise return false func (d *TursoSyncDb) Pull(ctx context.Context) (bool, error) { d.mu.Lock() defer d.mu.Unlock() // 1) Wait for remote changes waitOp, err := turso_sync_database_wait_changes(d.db) if err != nil { return false, err } kind, waitFinal, err := d.driveOpUntilDone(ctx, waitOp) if err != nil { return false, err } defer turso_sync_operation_deinit(waitFinal) if kind != TURSO_ASYNC_RESULT_CHANGES { return false, errors.New("turso: unexpected result kind for wait_changes") } changes, err := turso_sync_operation_result_extract_changes(waitFinal) if err != nil { return false, err } // No changes available if changes == nil { return false, nil } // 2) Apply fetched changes locally applyOp, err := turso_sync_database_apply_changes(d.db, changes) if err != nil { // changes ownership is transferred to apply_changes even in case of error return false, err } _, applyFinal, err := d.driveOpUntilDone(ctx, applyOp) if err != nil { return false, err } turso_sync_operation_deinit(applyFinal) return true, nil } // Push local changes to the remote // Push DO NOT fetch any remote changes func (d *TursoSyncDb) Push(ctx context.Context) error { d.mu.Lock() defer d.mu.Unlock() op, err := turso_sync_database_push_changes(d.db) if err != nil { return err } _, opFinal, err := d.driveOpUntilDone(ctx, op) if err != nil { return err } turso_sync_operation_deinit(opFinal) return nil } // Get stats for the synced database func (d *TursoSyncDb) Stats(ctx context.Context) (TursoSyncDbStats, error) { d.mu.Lock() defer d.mu.Unlock() op, err := turso_sync_database_stats(d.db) if err != nil { return TursoSyncDbStats{}, err } kind, opFinal, err := d.driveOpUntilDone(ctx, op) if err != nil { return TursoSyncDbStats{}, err } defer turso_sync_operation_deinit(opFinal) if kind != TURSO_ASYNC_RESULT_STATS { return TursoSyncDbStats{}, errors.New("turso: unexpected result kind for stats") } stats, err := turso_sync_operation_result_extract_stats(opFinal) if err != nil { return TursoSyncDbStats{}, err } return TursoSyncDbStats{ CdcOperations: stats.CDcOperations, MainWalSize: stats.MainWalSize, RevertWalSize: stats.RevertWalSize, LastPullUnixTime: stats.LastPullUnixTime, LastPushUnixTime: stats.LastPushUnixTime, NetworkSentBytes: stats.NetworkSentBytes, NetworkReceivedBytes: stats.NetworkReceivedBytes, Revision: stats.Revision, }, nil } // Checkpoint local WAL of the database func (d *TursoSyncDb) Checkpoint(ctx context.Context) error { d.mu.Lock() defer d.mu.Unlock() op, err := turso_sync_database_checkpoint(d.db) if err != nil { return err } _, opFinal, err := d.driveOpUntilDone(ctx, op) if err != nil { return err } turso_sync_operation_deinit(opFinal) return nil } // driveOpUntilDone resumes an async operation until completion, serving IO requests as needed. // It returns the final result kind and the operation handle that must be deinitialized by the caller. func (d *TursoSyncDb) driveOpUntilDone(ctx context.Context, op TursoSyncOperation) (TursoSyncOperationResultType, TursoSyncOperation, error) { for { if ctx != nil && ctx.Err() != nil { return TURSO_ASYNC_RESULT_NONE, op, ctx.Err() } code, err := turso_sync_operation_resume(op) if err != nil { return TURSO_ASYNC_RESULT_NONE, op, err } switch code { case TURSO_DONE: return turso_sync_operation_result_kind(op), op, nil case TURSO_IO: if err := d.processIoQueue(ctx); err != nil { return TURSO_ASYNC_RESULT_NONE, op, err } continue case TURSO_OK: // Just continue continue default: return TURSO_ASYNC_RESULT_NONE, op, statusToError(code, "") } } } // processOneIo handles at most one IO item (used as extra IO iteration inside SQL driver). func (d *TursoSyncDb) processOneIo() error { item, err := turso_sync_database_io_take_item(d.db) if err != nil { return err } if item == nil { // Still run callbacks to allow engine to progress timers/state. return turso_sync_database_io_step_callbacks(d.db) } _ = d.handleIoItem(context.Background(), item) turso_sync_database_io_item_deinit(item) return turso_sync_database_io_step_callbacks(d.db) } // processIoQueue drains IO queue until it's empty. func (d *TursoSyncDb) processIoQueue(ctx context.Context) error { for { if ctx != nil && ctx.Err() != nil { return ctx.Err() } item, err := turso_sync_database_io_take_item(d.db) if err != nil { return err } if item == nil { break } _ = d.handleIoItem(ctx, item) turso_sync_database_io_item_deinit(item) } return turso_sync_database_io_step_callbacks(d.db) } func buildHostname(baseURL, namespace string) (string, error) { u, err := url.Parse(baseURL) if err != nil { return "", err } if namespace == "" { return u.Host, nil } return namespace + "." + u.Host, nil } // handleIoItem performs execution of a single IO item. // It streams data in chunks for HTTP and file operations to avoid loading whole payloads in memory. func (d *TursoSyncDb) handleIoItem(ctx context.Context, item TursoSyncIoItem) error { switch turso_sync_database_io_request_kind(item) { case TURSO_SYNC_IO_HTTP: req, err := turso_sync_database_io_request_http(item) if err != nil { _ = turso_sync_database_io_poison(item, err.Error()) _ = turso_sync_database_io_done(item) return err } // Build URL buildUrl := joinUrl(d.baseURL, req.Path) // Build headers hdr := make(http.Header, req.Headers+2) for i := 0; i < req.Headers; i++ { h, err := turso_sync_database_io_request_http_header(item, i) if err != nil { _ = turso_sync_database_io_poison(item, err.Error()) _ = turso_sync_database_io_done(item) return err } if h.Key != "" { hdr.Add(h.Key, h.Value) } } if d.authToken != "" { hdr.Set("Authorization", "Bearer "+d.authToken) } // Propagate sensible defaults if hdr.Get("User-Agent") == "" { hdr.Set("User-Agent", "turso-sync-go") } // Prepare request body reader var body io.Reader if len(req.Body) > 0 { body = bytes.NewReader(req.Body) } httpReq, err := http.NewRequestWithContext(ctx, req.Method, buildUrl, body) if err != nil { _ = turso_sync_database_io_poison(item, err.Error()) _ = turso_sync_database_io_done(item) return err } host, err := buildHostname(d.baseURL, d.namespace) if err != nil { return err } httpReq.Host = host httpReq.Header = hdr resp, err := d.client.Do(httpReq) if err != nil { _ = turso_sync_database_io_poison(item, err.Error()) _ = turso_sync_database_io_done(item) return err } defer resp.Body.Close() // Send status _ = turso_sync_database_io_status(item, resp.StatusCode) // Stream body buf := make([]byte, 64*1024) for { if ctx != nil && ctx.Err() != nil { _ = turso_sync_database_io_poison(item, ctx.Err().Error()) break } n, rerr := resp.Body.Read(buf) if n > 0 { // push the exact slice view; underlying call copies bytes synchronously _ = turso_sync_database_io_push_buffer(item, buf[:n]) } if rerr == io.EOF { break } if rerr != nil { _ = turso_sync_database_io_poison(item, rerr.Error()) break } } _ = turso_sync_database_io_done(item) return nil case TURSO_SYNC_IO_FULL_READ: r, err := turso_sync_database_io_request_full_read(item) if err != nil { _ = turso_sync_database_io_poison(item, err.Error()) _ = turso_sync_database_io_done(item) return err } f, ferr := os.Open(r.Path) if errors.Is(ferr, os.ErrNotExist) { _ = turso_sync_database_io_done(item) return nil } else if ferr != nil { _ = turso_sync_database_io_poison(item, ferr.Error()) _ = turso_sync_database_io_done(item) return ferr } defer f.Close() buf := make([]byte, 64*1024) for { if ctx != nil && ctx.Err() != nil { _ = turso_sync_database_io_poison(item, ctx.Err().Error()) break } n, rerr := f.Read(buf) if n > 0 { _ = turso_sync_database_io_push_buffer(item, buf[:n]) } if rerr == io.EOF { break } if rerr != nil { _ = turso_sync_database_io_poison(item, rerr.Error()) break } } _ = turso_sync_database_io_done(item) return nil case TURSO_SYNC_IO_FULL_WRITE: r, err := turso_sync_database_io_request_full_write(item) if err != nil { _ = turso_sync_database_io_poison(item, err.Error()) _ = turso_sync_database_io_done(item) return err } // Ensure directory exists if dir := filepath.Dir(r.Path); dir != "" && dir != "." { _ = os.MkdirAll(dir, 0o755) } // Write file atomically-ish by writing to a temp and renaming tmp := r.Path + ".tmp" if werr := os.WriteFile(tmp, r.Content, 0o644); werr != nil { _ = turso_sync_database_io_poison(item, werr.Error()) _ = turso_sync_database_io_done(item) return werr } if rerr := os.Rename(tmp, r.Path); rerr != nil { _ = turso_sync_database_io_poison(item, rerr.Error()) _ = turso_sync_database_io_done(item) return rerr } _ = turso_sync_database_io_done(item) return nil default: // Unknown or none; mark done _ = turso_sync_database_io_done(item) return nil } } func joinUrl(base, p string) string { if !strings.HasPrefix(p, "/") { p = "/" + p } return strings.TrimRight(base, "/") + p } func normalizeUrl(base string) string { if cut, ok := strings.CutPrefix(base, "libsql://"); ok { return "https://" + cut } return base } // syncDSNOptions holds options parsed from DSN-style path type syncDSNOptions struct { BusyTimeout int // 0 = not set, >0 = custom, <0 = disabled } // parseSyncDSN parses a DSN-style path like "mydb.db?_busy_timeout=5000" // and returns the clean path and parsed options. func parseSyncDSN(dsn string) (string, syncDSNOptions) { var opts syncDSNOptions qMark := strings.IndexByte(dsn, '?') if qMark < 0 { return dsn, opts } path := dsn[:qMark] rawQuery := dsn[qMark+1:] vals, err := url.ParseQuery(rawQuery) if err != nil { return dsn, opts // Return original on parse error } if v := vals.Get("_busy_timeout"); v != "" { var timeout int if _, err := fmt.Sscanf(v, "%d", &timeout); err == nil { opts.BusyTimeout = timeout } } return path, opts } ================================================ FILE: bindings/go/driver_sync_test.go ================================================ package turso import ( "bytes" "context" "encoding/json" "fmt" "io" "math/rand" "net" "net/http" "os" "path" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/require" ) // --- Busy Timeout DSN Parsing Tests (no server required) --- func TestSyncDSNParsing(t *testing.T) { tests := []struct { name string dsn string expectedPath string expectedTimeout int }{ { name: "simple path", dsn: "mydb.db", expectedPath: "mydb.db", expectedTimeout: 0, }, { name: "path with busy timeout", dsn: "mydb.db?_busy_timeout=10000", expectedPath: "mydb.db", expectedTimeout: 10000, }, { name: "path with negative busy timeout", dsn: "mydb.db?_busy_timeout=-1", expectedPath: "mydb.db", expectedTimeout: -1, }, { name: "memory db with timeout", dsn: ":memory:?_busy_timeout=5000", expectedPath: ":memory:", expectedTimeout: 5000, }, { name: "path with other options", dsn: "/path/to/db.db?other=value&_busy_timeout=3000", expectedPath: "/path/to/db.db", expectedTimeout: 3000, }, { name: "path with zero timeout", dsn: "test.db?_busy_timeout=0", expectedPath: "test.db", expectedTimeout: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { path, opts := parseSyncDSN(tt.dsn) require.Equal(t, tt.expectedPath, path) require.Equal(t, tt.expectedTimeout, opts.BusyTimeout) }) } } func TestSyncBusyTimeoutConfigPrecedence(t *testing.T) { // Test that explicit BusyTimeout in config takes precedence over DSN t.Run("config overrides DSN", func(t *testing.T) { // This test verifies the logic without actually creating a database config := TursoSyncDbConfig{ Path: "test.db?_busy_timeout=1000", BusyTimeout: 5000, // Explicit config should win } // Parse DSN like NewTursoSyncDb does _, dsnOpts := parseSyncDSN(config.Path) busyTimeout := config.BusyTimeout if busyTimeout == 0 { if dsnOpts.BusyTimeout != 0 { busyTimeout = dsnOpts.BusyTimeout } } // Explicit config should take precedence require.Equal(t, 5000, busyTimeout) }) t.Run("DSN used when config not set", func(t *testing.T) { config := TursoSyncDbConfig{ Path: "test.db?_busy_timeout=3000", BusyTimeout: 0, // Not set } _, dsnOpts := parseSyncDSN(config.Path) busyTimeout := config.BusyTimeout if busyTimeout == 0 { if dsnOpts.BusyTimeout != 0 { busyTimeout = dsnOpts.BusyTimeout } } // DSN timeout should be used require.Equal(t, 3000, busyTimeout) }) t.Run("default used when neither set", func(t *testing.T) { config := TursoSyncDbConfig{ Path: "test.db", BusyTimeout: 0, } _, dsnOpts := parseSyncDSN(config.Path) busyTimeout := config.BusyTimeout if busyTimeout == 0 { if dsnOpts.BusyTimeout != 0 { busyTimeout = dsnOpts.BusyTimeout } } // Should be 0 at this point, default applied in Connect() require.Equal(t, 0, busyTimeout) }) } func randomString() string { return fmt.Sprintf("r-%v", rand.Intn(1000_000_000)) } var ( AdminUrl = "http://localhost:8081" UserUrl = "http://localhost:8080" ) type TursoServer struct { DbUrl string userUrl string host string server *os.Process } // getFreePort asks the OS for a free port by binding to port 0. func getFreePort() (int, error) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return 0, err } port := l.Addr().(*net.TCPAddr).Port l.Close() return port, nil } func NewTursoServer() (*TursoServer, error) { if localSyncServer, ok := os.LookupEnv("LOCAL_SYNC_SERVER"); ok { const maxRetries = 5 var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { port, err := getFreePort() if err != nil { lastErr = err continue } turso, err := startLocalServer(localSyncServer, port) if err != nil { lastErr = err continue } return turso, nil } return nil, fmt.Errorf("failed to start server after %d attempts: %v", maxRetries, lastErr) } else { name := randomString() err := handleResponse(http.Post(fmt.Sprintf("%v/v1/tenants/%v", AdminUrl, name), "application/json", nil)) if err != nil { return nil, err } err = handleResponse(http.Post(fmt.Sprintf("%v/v1/tenants/%v/groups/%v", AdminUrl, name, name), "application/json", nil)) if err != nil { return nil, err } err = handleResponse(http.Post(fmt.Sprintf("%v/v1/tenants/%v/groups/%v/databases/%v", AdminUrl, name, name, name), "application/json", nil)) if err != nil { return nil, err } userUrl := strings.Split(UserUrl, "://") turso := &TursoServer{ userUrl: UserUrl, DbUrl: fmt.Sprintf("%v://%v--%v--%v.%v", userUrl[0], name, name, name, userUrl[1]), host: fmt.Sprintf("%v--%v--%v.localhost", name, name, name), } return turso, nil } } func startLocalServer(localSyncServer string, port int) (*TursoServer, error) { server, err := os.StartProcess( localSyncServer, []string{localSyncServer, "--sync-server", fmt.Sprintf("0.0.0.0:%v", port)}, &os.ProcAttr{Files: []*os.File{ os.Stdin, os.Stdout, os.Stderr, }}, ) if err != nil { return nil, err } turso := &TursoServer{ userUrl: fmt.Sprintf("http://localhost:%v", port), DbUrl: fmt.Sprintf("http://localhost:%v", port), host: "", server: server, } // Wait for server to become ready, with timeout and process health check. deadline := time.Now().Add(30 * time.Second) exitCh := make(chan error, 1) go func() { _, err := server.Wait() exitCh <- err }() for { select { case exitErr := <-exitCh: return nil, fmt.Errorf("server process exited before becoming ready: %v", exitErr) default: } if time.Now().After(deadline) { server.Kill() return nil, fmt.Errorf("timed out waiting for server to become ready on port %d", port) } resp, err := http.Get(turso.userUrl) if err == nil { resp.Body.Close() break } time.Sleep(10 * time.Millisecond) } return turso, nil } func (s *TursoServer) Close() { if s.server != nil { s.server.Kill() s.server.Wait() } } func handleResponse(response *http.Response, err error) error { if err != nil { return err } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { return err } text := string(body) if response.StatusCode == 200 || response.StatusCode == 400 && strings.Contains(text, "already exists") { return nil } return fmt.Errorf("http failed: %v %v", response.StatusCode, text) } func (s *TursoServer) DbSql(sql string) ([][]any, error) { data := map[string]any{ "requests": []map[string]any{ {"type": "execute", "stmt": map[string]any{"sql": sql}}, }, } payload, err := json.Marshal(data) if err != nil { return nil, err } r, err := http.NewRequest("POST", fmt.Sprintf("%v/v2/pipeline", s.userUrl), bytes.NewReader(payload)) if err != nil { return nil, err } r.Host = s.host response, err := http.DefaultClient.Do(r) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode != 200 { return nil, fmt.Errorf("bad response: %v", response.StatusCode) } body, err := io.ReadAll(response.Body) if err != nil { return nil, err } var result map[string]any err = json.Unmarshal(body, &result) if err != nil { return nil, err } if result["results"].([]any)[0].(map[string]any)["type"] != "ok" { return nil, fmt.Errorf("bad response: %+v", result) } inner := result["results"].([]any)[0].(map[string]any)["response"].(map[string]any)["result"].(map[string]any)["rows"].([]any) rows := make([][]any, 0) for _, innerRow := range inner { row := make([]any, 0) for _, cell := range innerRow.([]any) { row = append(row, cell.(map[string]any)["value"]) } rows = append(rows, row) } return rows, nil } var ( SYNC_TEST_RUN = os.Getenv("SYNC_TEST_RUN") == "true" ) func TestSyncBootstrap(t *testing.T) { server, err := NewTursoServer() require.Nil(t, err) t.Cleanup(func() { server.Close() }) _, err = server.DbSql("CREATE TABLE t(x)") require.Nil(t, err) _, err = server.DbSql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync-go')") require.Nil(t, err) db, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{ Path: ":memory:", ClientName: "turso-sync-go", RemoteUrl: server.DbUrl, }) require.Nil(t, err) conn, err := db.Connect(context.Background()) require.Nil(t, err) rows, err := conn.QueryContext(context.Background(), "SELECT * FROM t") require.Nil(t, err) values := make([]string, 0) for rows.Next() { var value string require.Nil(t, rows.Scan(&value)) values = append(values, value) } require.Equal(t, values, []string{"hello", "turso", "sync-go"}) } func TestSyncConfigPersistence(t *testing.T) { server, err := NewTursoServer() require.Nil(t, err) t.Cleanup(func() { server.Close() }) _, err = server.DbSql("CREATE TABLE t(x)") require.Nil(t, err) _, err = server.DbSql("INSERT INTO t VALUES (42)") require.Nil(t, err) dir := t.TempDir() local := path.Join(dir, "local.db") db1, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{ Path: local, ClientName: "turso-sync-go", RemoteUrl: server.DbUrl, }) require.Nil(t, err) conn, err := db1.Connect(context.Background()) require.Nil(t, err) rows, err := conn.QueryContext(context.Background(), "SELECT * FROM t") require.Nil(t, err) values := make([]int64, 0) for rows.Next() { var value int64 require.Nil(t, rows.Scan(&value)) values = append(values, value) } require.Equal(t, values, []int64{42}) rows.Close() conn.Close() _, err = server.DbSql("INSERT INTO t VALUES (41)") require.Nil(t, err) db2, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{ Path: local, RemoteUrl: server.DbUrl, }) require.Nil(t, err) _, err = db2.Pull(context.Background()) require.Nil(t, err) conn, err = db2.Connect(context.Background()) require.Nil(t, err) rows, err = conn.QueryContext(context.Background(), "SELECT * FROM t") require.Nil(t, err) values = make([]int64, 0) for rows.Next() { var value int64 require.Nil(t, rows.Scan(&value)) values = append(values, value) } require.Equal(t, values, []int64{42, 41}) } func TestSyncBootstrapPersistent(t *testing.T) { server, err := NewTursoServer() require.Nil(t, err) t.Cleanup(func() { server.Close() }) _, err = server.DbSql("CREATE TABLE t(x)") require.Nil(t, err) _, err = server.DbSql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync-go')") require.Nil(t, err) dir, err := os.MkdirTemp(".", "test-sync-") require.Nil(t, err) t.Cleanup(func() { os.RemoveAll(dir) }) db, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{ Path: filepath.Join(dir, "local.db"), RemoteUrl: server.DbUrl, }) require.Nil(t, err) conn, err := db.Connect(context.Background()) require.Nil(t, err) rows, err := conn.QueryContext(context.Background(), "SELECT * FROM t") require.Nil(t, err) values := make([]string, 0) for rows.Next() { var value string require.Nil(t, rows.Scan(&value)) values = append(values, value) } require.Equal(t, values, []string{"hello", "turso", "sync-go"}) } func TestSyncPull(t *testing.T) { server, err := NewTursoServer() require.Nil(t, err) t.Cleanup(func() { server.Close() }) _, err = server.DbSql("CREATE TABLE t(x)") require.Nil(t, err) _, err = server.DbSql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync-go')") require.Nil(t, err) db, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{ Path: ":memory:", ClientName: "turso-sync-go", RemoteUrl: server.DbUrl, }) require.Nil(t, err) conn, err := db.Connect(context.Background()) require.Nil(t, err) _, err = server.DbSql("INSERT INTO t VALUES ('pull-works')") require.Nil(t, err) rows, err := conn.QueryContext(context.Background(), "SELECT * FROM t") require.Nil(t, err) values := make([]string, 0) for rows.Next() { var value string require.Nil(t, rows.Scan(&value)) values = append(values, value) } require.Equal(t, values, []string{"hello", "turso", "sync-go"}) rows.Close() changes, err := db.Pull(context.Background()) require.Nil(t, err) require.True(t, changes) changes, err = db.Pull(context.Background()) require.Nil(t, err) require.False(t, changes) rows, err = conn.QueryContext(context.Background(), "SELECT * FROM t") require.Nil(t, err) values = make([]string, 0) for rows.Next() { var value string require.Nil(t, rows.Scan(&value)) values = append(values, value) } require.Equal(t, values, []string{"hello", "turso", "sync-go", "pull-works"}) rows.Close() } func TestSyncPullDoNotPush(t *testing.T) { server, err := NewTursoServer() require.Nil(t, err) t.Cleanup(func() { server.Close() }) _, err = server.DbSql("CREATE TABLE t(x)") require.Nil(t, err) _, err = server.DbSql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync-go')") require.Nil(t, err) db, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{ Path: ":memory:", ClientName: "turso-sync-go", RemoteUrl: server.DbUrl, }) require.Nil(t, err) conn, err := db.Connect(context.Background()) require.Nil(t, err) _, err = server.DbSql("INSERT INTO t VALUES ('pull-works')") require.Nil(t, err) rows, err := conn.QueryContext(context.Background(), "SELECT * FROM t") require.Nil(t, err) values := make([]string, 0) for rows.Next() { var value string require.Nil(t, rows.Scan(&value)) values = append(values, value) } require.Equal(t, values, []string{"hello", "turso", "sync-go"}) rows.Close() _, err = conn.Exec("INSERT INTO t VALUES ('push-is-local')") require.Nil(t, err) changes, err := db.Pull(context.Background()) require.Nil(t, err) require.True(t, changes) changes, err = db.Pull(context.Background()) require.Nil(t, err) require.False(t, changes) rows, err = conn.QueryContext(context.Background(), "SELECT * FROM t") require.Nil(t, err) values = make([]string, 0) for rows.Next() { var value string require.Nil(t, rows.Scan(&value)) values = append(values, value) } require.Equal(t, values, []string{"hello", "turso", "sync-go", "pull-works", "push-is-local"}) rows.Close() remote, err := server.DbSql("SELECT * FROM t") require.Nil(t, err) require.Equal(t, remote, [][]any{{"hello"}, {"turso"}, {"sync-go"}, {"pull-works"}}) } func TestSyncPush(t *testing.T) { server, err := NewTursoServer() require.Nil(t, err) t.Cleanup(func() { server.Close() }) _, err = server.DbSql("CREATE TABLE t(x)") require.Nil(t, err) _, err = server.DbSql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync-go')") require.Nil(t, err) db, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{ Path: ":memory:", ClientName: "turso-sync-go", RemoteUrl: server.DbUrl, }) require.Nil(t, err) conn, err := db.Connect(context.Background()) require.Nil(t, err) _, err = conn.Exec("INSERT INTO t VALUES ('push-works')") require.Nil(t, err) rows, err := server.DbSql("SELECT * FROM t") require.Nil(t, err) require.Equal(t, rows, [][]any{{"hello"}, {"turso"}, {"sync-go"}}) require.Nil(t, db.Push(context.Background())) rows, err = server.DbSql("SELECT * FROM t") require.Nil(t, err) require.Equal(t, rows, [][]any{{"hello"}, {"turso"}, {"sync-go"}, {"push-works"}}) } func TestSyncCheckpoint(t *testing.T) { server, err := NewTursoServer() require.Nil(t, err) t.Cleanup(func() { server.Close() }) db, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{ Path: ":memory:", ClientName: "turso-sync-go", RemoteUrl: server.DbUrl, }) require.Nil(t, err) conn, err := db.Connect(context.Background()) require.Nil(t, err) _, err = conn.Exec("CREATE TABLE t(x)") require.Nil(t, err) for i := 0; i < 1024; i++ { _, err = conn.Exec(fmt.Sprintf("INSERT INTO t VALUES (%v)", i)) require.Nil(t, err) } stats1, err := db.Stats(context.Background()) require.Nil(t, err) require.Nil(t, db.Checkpoint(context.Background())) stats2, err := db.Stats(context.Background()) require.Nil(t, err) require.Greater(t, stats1.MainWalSize, int64(1024*1024)) require.Equal(t, stats1.RevertWalSize, int64(0)) require.Equal(t, stats2.MainWalSize, int64(0)) require.Less(t, stats2.RevertWalSize, int64(8*1024)) require.Nil(t, db.Push(context.Background())) rows, err := server.DbSql("SELECT SUM(x) FROM t") require.Nil(t, err) require.Equal(t, rows, [][]any{{fmt.Sprintf("%v", 1024*1023/2)}}) } func TestSyncPartial(t *testing.T) { server, err := NewTursoServer() require.Nil(t, err) t.Cleanup(func() { server.Close() }) _, err = server.DbSql("CREATE TABLE t(x)") require.Nil(t, err) _, err = server.DbSql("INSERT INTO t SELECT randomblob(1024) FROM generate_series(1, 1024)") require.Nil(t, err) db, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{ Path: ":memory:", ClientName: "turso-sync-go", RemoteUrl: server.DbUrl, PartialSyncExperimental: TursoPartialSyncConfig{ BootstrapStrategyPrefix: 128 * 1024, }, }) require.Nil(t, err) conn, err := db.Connect(context.Background()) require.Nil(t, err) rows, err := conn.QueryContext(context.Background(), "SELECT LENGTH(x) FROM t LIMIT 1") require.Nil(t, err) values := make([]int, 0) for rows.Next() { var value int require.Nil(t, rows.Scan(&value)) values = append(values, value) } require.Equal(t, values, []int{1024}) rows.Close() stats1, err := db.Stats(context.Background()) require.Nil(t, err) require.Less(t, stats1.NetworkReceivedBytes, int64(256*1024)) rows, err = conn.QueryContext(context.Background(), "SELECT SUM(LENGTH(x)) FROM t") require.Nil(t, err) values = make([]int, 0) for rows.Next() { var value int require.Nil(t, rows.Scan(&value)) values = append(values, value) } require.Equal(t, values, []int{1024 * 1024}) rows.Close() stats2, err := db.Stats(context.Background()) require.Nil(t, err) require.Greater(t, stats2.NetworkReceivedBytes, int64(1024*1024)) } // TestSyncLargeSchema tests syncing a database where the schema table spans multiple pages. // This reproduces a bug where make_from_btree does blocking IO during database opening, // but the caller (sync engine) has no way to spin the external IO loop to fetch // overflow pages that aren't loaded yet. func TestSyncLargeSchema(t *testing.T) { server, err := NewTursoServer() require.Nil(t, err) t.Cleanup(func() { server.Close() }) // Create many tables with long column definitions to make sqlite_schema span multiple pages. // Each CREATE TABLE statement will be stored in the schema table. // SQLite page usable space is ~4000 bytes, so creating tables with ~500 byte definitions // means we need ~8+ tables to overflow to another page. numTables := 50 for i := 0; i < numTables; i++ { // Create a table with many columns to make a long CREATE statement (~1KB each) columns := "" for j := 0; j < 20; j++ { if j > 0 { columns += ", " } columns += fmt.Sprintf("column_%d_%d_with_a_very_long_name_to_increase_size INTEGER DEFAULT 0", i, j) } sql := fmt.Sprintf("CREATE TABLE large_schema_table_%d (%s)", i, columns) _, err = server.DbSql(sql) require.Nil(t, err) } // Insert some data into the first table _, err = server.DbSql("INSERT INTO large_schema_table_0 (column_0_0_with_a_very_long_name_to_increase_size) VALUES (42)") require.Nil(t, err) // Use partial sync with a small bootstrap prefix to ensure not all schema pages are fetched upfront. // The bug manifests when opening the database: make_from_btree needs to read the full schema, // but overflow pages for the schema table aren't loaded yet, and blocking IO can't be driven // by the sync engine's coroutine-based IO loop. db, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{ Path: ":memory:", ClientName: "turso-sync-go", RemoteUrl: server.DbUrl, PartialSyncExperimental: TursoPartialSyncConfig{ // Use a small prefix to ensure we don't fetch all schema pages upfront BootstrapStrategyPrefix: 8 * 1024, }, }) require.Nil(t, err) conn, err := db.Connect(context.Background()) require.Nil(t, err) // Verify we can query one of the tables created with the large schema rows, err := conn.QueryContext(context.Background(), "SELECT column_0_0_with_a_very_long_name_to_increase_size FROM large_schema_table_0") require.Nil(t, err) values := make([]int, 0) for rows.Next() { var value int require.Nil(t, rows.Scan(&value)) values = append(values, value) } require.Equal(t, values, []int{42}) rows.Close() // Also verify that all tables are accessible in the schema rows, err = conn.QueryContext(context.Background(), "SELECT COUNT(*) FROM sqlite_schema WHERE type='table' AND name LIKE 'large_schema_table_%'") require.Nil(t, err) var count int require.True(t, rows.Next()) require.Nil(t, rows.Scan(&count)) require.Equal(t, count, numTables) rows.Close() } ================================================ FILE: bindings/go/go-bindings-db-tests.mdx ================================================ --- name: 2025-11-26-go-bindings-test --- Generate tests for Go functions bindings for the Turso - SQLite-compatible embedded database written in Rust. # Bindings You must generate tests for the bindings translated from C interface defined in turso.h file: # Implemention - Assert conditions with "github.com/stretchr/testify" - Make tests short, concise and independent. Explicitly assert results - do not create any "mocks" - Prefer to use ":memory:" db in order to not manage files # Test cases Generate tests which will cover generic use of SQL. **Non exhaustive** list of things to check: - Subqueries - INSERT ... RETURNING ... * Make additional test case for scenario, where multiple values were inserted, but only one row were fetch * Make sure that in this case transaction will be properly commited even when not all rows were consumed - CONFLICT clauses (and how driver inform caller about conflict) - Basic DDL statements (CREATE/DELETE) - More complex DDL statements (ALTER TABLE) - Builtin virtual tables (generate_series) - JOIN - JSON functions ================================================ FILE: bindings/go/go-bindings-db.mdx ================================================ --- name: 2025-11-26-go-bindings --- Generate Go functions bindings for the Turso - SQLite-compatible embedded database written in Rust. You must use C API bridge implemented in Rust which expose ABI compatible functions to interact with the database. The main goal is to translate C code to Go-friendly bindings which will have no additional logic, but more ergonomic API: 1. Return (err error) when possible 2. Manipulate with strings accordingly # Bindings You must generate bindings for C interface defined in turso.h file: # Rules General rules for driver implementation you **MUST** follow and never go against these rules: - DO NOT USE cgo, USE purego instead - DO NOT introduce any new public methods - export turso.h content as-is - DO NOT register library - this will be done externally - AVOID unnecessary FFI calls as their cost is non zero - AVOID unnecessary strings transformations - replace them with more efficient alternatives if possible - DO NOT ever mix `turso_statement_execute` and `turso_statement_step` methods: every statement must be either "stepped" or "executed" * This is because `execute` ignores all rows - SQL query can be arbitrary, be very careful writing the code which relies on properties derived from the simple string analysis - FOCUS on code readability: if possible extract helper function but make sure that it will be used more than once and that it really contribute to the code readability - WATCH OUT for variables scopes and do not use variables which are no longer accessible - WATCH OUT for string representations: in some cases library expected zero terminated C-string and in some cases library operates with string/byte slices - BE AWARE, that purego marshal only explicit parameters of the function calls. If you have C struct passed by reference - you need to do marshalling by yourself (e.g. convert strings to zero-terminated C strings, etc) - STRUCTURE of the implementation * Declaration order of elements and semantic blocks MUST be exsactly the same * (details and full enumerations omited in the example for brevity but you must generate full code) ```go package turso // define all package level errors here // note, that OK, DONE, ROW, IO are statuses - so you don't need to create errors for them var ( TursoBusyErr = errors.New("turso: database is busy") // provide short but concise error message ... ) // define all necessary constants first type TursoStatusCode int32 // note, that the only real statuses are OK, DONE, ROW, IO - everything else is errors const ( TURSO_OK TursoStatusCode = 0 ... ) type TursoType int32 const ( TURSO_TYPE_INTEGER TursoType = 1 ... ) // define opaque pointers as-is and accept them as exact arguments (e.g. func turso_database_connect(self TursoDatabase) ... - DO NOT add extra indirection) type TursoDatabase *turso_database_t // define all public binding types // the public binding types MUST have fields with native safe go types type TursoConfig struct { ... } // define all necessary private C structs // private C structs MUST have fields with low level types (e.g. uintptr, numbers) type turso_config_t struct { ... } // then, define C extern methods var ( // always use c_ structs here - never mix them with exported public types c_turso_setup func(config turso_config_t) turso_status_code_t ... ) // implement a function to register extern methods from loaded lib // DO NOT load lib - as it will be done externally func register_turso_db(handle uintptr) error { purego.RegisterLibFunc(&c_turso_setup, handle, "turso_setup") ... } // Go wrappers over imported C bindings // always use exported public types here - never mix them with c_ structs func turso_setup(config TursoConig) error { ... } ``` # Implementation - The package name is "github.com/tursodatabase/turso/go" - Use exactly same names as in turso.h for Go method names and prepend `c_` prefix for C extern functions - Separate "public" types from "private" types: * public types MUST be used ONLY in Go bindings methods (`turso_.*`) and have fields with native safe go types (string, slice, etc) * public types MUST NOT use low level types explicitly (e.g. unintptr) - they must be either replaced with wrapper public type or native alternative must be used instead (e.g. int instead of uinptrt if this is size_t) * private types MUST be used ONLY in C wrapper functions (`c_turso_.*`) and have fields with low-level C-compatible go types (uintptr, numbers, etc) - Document generated methods with docstrings - Replace `turso_status_code_t` and out error parameter with proper native golang `error` * If function returns only OK or error code, do not return status explicitly from the Go binding - just return error (and any value if it was returned as out parameter) - Convert C-strings back and forth appropriately * Remember, that Golang strings are not null-terminated - so you will need to convert them to zero-terminated strings by copying data in some cases - Support callback in turso_setup with purego.NewCallback - Make one exception and implement `turso_statement_row_value_bytes` and `turso_statement_row_value_text` methods which will use `c_turso_statement_row_value_bytes_ptr` and `c_turso_statement_row_value_bytes_count` extern methods under the hood * This MUST be the only non-direct translation of C extern functions # Purego Inspect following docstring from the official purego repository in order to understand how marshalling works: ================================================ FILE: bindings/go/go-bindings-sync.mdx ================================================ --- name: 2025-11-26-go-bindings-sync --- Generate Go functions bindings for the sync engine built on top of Turso - SQLite-compatible embedded database written in Rust. You must use C API bridge implemented in Rust which expose ABI compatible functions to interact with the database. The main goal is to translate C code to Go-friendly bindings which will have no additional logic, but more ergonomic API: 1. Return (err error) when possible 2. Manipulate with strings accordingly # Bindings You must generate bindings for C interface defined in turso_sync.h file. Note, that this header file depends on the turso.h definitions for which Go bindings already exists. You MUST just reuse them. DO NOT copy or reimplement these bindings: # Rules General rules for driver implementation you **MUST** follow and never go against these rules: - DO NOT USE cgo, USE purego instead - DO NOT introduce any new public methods - export turso.h content as-is - DO NOT register library - this will be done externally - AVOID unnecessary FFI calls as their cost is non zero - AVOID unnecessary strings transformations - replace them with more efficient alternatives if possible - FOCUS on code readability: if possible extract helper function but make sure that it will be used more than once and that it really contribute to the code readability - WATCH OUT for variables scopes and do not use variables which are no longer accessible - WATCH OUT for string representations: in some cases library expected zero terminated C-string and in some cases library operates with string/byte slices - BE AWARE, that purego marshal only explicit parameters of the function calls. If you have C struct passed by reference - you need to do marshalling by yourself (e.g. convert strings to zero-terminated C strings, etc) - STRUCTURE of the implementation * Declaration order of elements and semantic blocks MUST be exsactly the same * (details and full enumerations omited in the example for brevity but you must generate full code) ```go package turso // define all package level errors here // define opaque pointers as-is and accept them as exact arguments (e.g. func turso_database_connect(self TursoDatabase) ... - DO NOT add extra indirection) type TursoSyncDatabase *turso_sync_database_t // define all public binding types // the public binding types MUST have fields with native safe go types type TursoSyncDatabaseConfig struct { ... } // define all necessary private C structs // private C structs MUST have fields with low level types (e.g. uintptr, numbers) type turso_sync_database_config_t struct { ... } // then, define C extern methods var ( // always use c_ structs here - never mix them with exported public types c_turso_sync_database_new func( dbConfig *c_turso_database_config_t, syncConfig *c_turso_sync_database_config_t, database unsafe.Pointer, errorOptOut unsafe.Pointer, ) c_turso_status_code_t ... ) // imiplement a function to register extern methods from loaded lib // DO NOT load lib - as it will be done externally func register_turso_sync(handle uintptr) error { purego.RegisterLibFunc(&c_turso_sync_database_new, handle, "turso_sync_database_new") ... } // Go wrappers over imported C bindings // always use exported public types here - never mix them with c_ structs func turso_sync_database_new(...) ... { ... } ``` # Implementation - The package name is "github.com/tursodatabase/turso/go" - Use exactly same names as in turso_sync.h for Go method names and prepend `c_` prefix for C extern functions - Separate "public" types from "private" types: * public types MUST be used ONLY in Go bindings methods (`turso_sync_.*`) and have fields with native safe go types (string, slice, etc) * public types MUST NOT use low level types explicitly (e.g. unintptr) - they must be either replaced with wrapper public type or native alternative must be used instead (e.g. int instead of uinptrt if this is size_t) * private types MUST be used ONLY in C wrapper functions (`c_turso_sync_.*`) and have fields with low-level C-compatible go types (uintptr, numbers, etc) - Document generated methods with docstrings - Replace `turso_status_code_t` and out error parameter with proper native golang `error` - Convert C-strings back and forth appropriately * Remember, that Golang strings are not null-terminated - so you will need to convert them to zero-terminated strings by copying data in some cases # Purego Inspect following docstring from the official purego repository in order to understand how marshalling works: ================================================ FILE: bindings/go/go-driver-db.mdx ================================================ --- name: 2025-11-26-go-bindings --- Generate Go SDK for the Turso - SQLite-compatible embedded database written in Rust. You must use bindings_db.go bridge from Go to C API. # Bindings # Rules - PREPARE statement in Prepare - do not delay that * Consequently, you MUST properly implement NumInputs() method for statement using `turso_statement_parameters_count` method - NEVER reset non-failed statement which wasn't finalized before that - this will abort all progress which is wrong semantic - USE `BEGIN` always as SQLite and Turso doesn't have other transaction modes except from SNAPSHOT ISOLATION - WATCH OUT for locks and BE CAREFUL with methods calling each other and locking same mutex - DO NOT USE `uint64(^int64(0))` - use `math.MaxInt64` instead - DO NOT USE `_ interface{ Done() <-chan struct{} }` - use `ctx context.Context` instead - USE simple control flow - carefully manage state and locks - but try to avoid complex wrapper high-order functions (withLock, etc) - STRUCTURE of the implementation * Declaration order of elements and semantic blocks MUST be exsactly the same * (details and full enumerations omited in the example for brevity but you must generate full code) ```go package turso // define all package level errors here var ( TursoStmtClosedErr = errors.New("turso: statement closed") // provide short but concise error message ... ) // define all package level structs here struct tursoDbDriver { ... } // register driver func init() { sql.Register("turso", &tursoDbDriver{}) } // Extra constructor for *tursoDbConnection instance which can be used to intergrate with turso Db driver // extr_io parameter is the arbitrary IO function which will be executed together with turso_statement_run_io func NewConnection(conn TursoConnection, extraIo func() error) *tursoDbConnection { } // Implement sql.Driver methods ... ``` # Implementation The SDK should has following components under the hood: * `type tursoDbConnection struct { }` - wrapper which holds connection to the turso and protect it against concurrent use * `type tursoDbStatement struct { }` - wrapper which holds prepared statement to the turso and protect it against concurrent use * `type tursoDbRows struct { }` - wrapper which holds prepared statement and provide sqld/database compatible methods to iterate over rows of the statement * `type tursoDbDriver struct { }` - type to register in the sql/database as driver * `type tursoDbResult struct { }` - type implementing `driver.Result` interface * `type tursoDbTx struct { }` - type implementing `driver.Tx` interface * Support following DSN format: `[?experimental=&async=0|1]` * Implement multi-statement support for `Exec*` family of methods. Use `turso_connection_prepare_first` method for that # Go driver API Inspect go sql driver documentation and follow it during implementation: ================================================ FILE: bindings/go/go-driver-sync.mdx ================================================ --- name: 2025-11-26-go-bindings-sync --- Turso - is the SQLite compatible database written in Rust. One of the important features of the Turso - is native ability to sync database with the Cloud in both directions (push local changes and pull remote changes). Your task is to generate EXTRA functionality on top of the existing Golang driver which will extend regular embedded with sync capability. Do not modify existing driver - its already implemented in the driver_db.go (based on bindings_db.go) Your task is to write extra code which will use abstractions driver_db.go + bindings_db.go and build sync support in the Gollang on top of it in the driver_sync.go file. # Rules General rules for driver implementation you **MUST** follow and never go against these rules: - USE already implemented driver - DO NOT copy it - SET async_io=True for the driver database configuration - because partial sync support requires TURSO_IO to handled externally from the bindings - ADD context.Context in the methods when this make sense and we have control over method API - STRUCTURE of the implementation * Declaration order of elements and semantic blocks MUST be exsactly the same * (details and full enumerations omited in the example for brevity but you must generate full code) ```go package turso struct TursoSyncDbConfig { // path to the main database file locally Path string // remote url for the sync // remote_url MUST be used in all sync engine operations: during bootstrap and all further operations RemoteUrl string // token for remote authentication // auth token value WILL not have any prefix and must be used as "Authorization" header prepended with "Bearer " prefix AuthToken string // optional unique client name (library MUST use `turso-sync-go` if omitted) ClientName string // long polling timeout LongPollTimeoutMs int // if not set, initial bootstrap phase will be skipped and caller must call .pull(...) explicitly in order to get initial state from remote // default value is true BootstrapIfEmpty *bool // if positive, prefix partial bootstrap strategy will be used PartialBootstrapStrategyPrefix int // if not empty, query partial bootstrap strategy will be used PartialBootstrapStrategyQuery string // pass it as-is to the underlying connection ExperimentalFeatures string } // statistics for the synced database. type TursoSyncDbStats struct { // amount of local operations written since last Pull(...) call CdcOperations int64 // size of the main WAL file MainWalSize int64 // size of the revert WAL file RevertWalSize int64 // last successful pull time LastPullUnixTime int64 // last successful push time LastPushUnixTime int64 // total amount of bytes sent over the network (both Push and Pull operations are tracked together) NetworkSentBytes int64 // total amount of bytes received over the network (both Push and Pull operations are tracked together) NetworkReceivedBytes int64 // opaque server revision - it MUST NOT be interpreted/parsed in any way Revision string } // define public structs here struct TursoSyncDb { ... } // main constructor to create synced database func NewTursoSyncDb(ctx context.Context, config TursoSyncDbConfig) (*TursoSyncDb, error) { ... } // create turso db local connnection // internal connector to integrate with database/sql pool type tursoSyncConnector struct { db *TursoSyncDb } func (c *tursoSyncConnector) Connect(ctx context.Context) (driver.Conn, error) { ... } func (c *tursoSyncConnector) Driver() driver.Driver { return &tursoDbDriver{} } // create tursodb connection using NewConnection(...) from driver_db.go and tursoSyncConnector helper func (d *TursoSyncDb) Connect(ctx context.Context) (*sql.DB, error) { ... } // implement EXTRA sync methods // Pull fresh data from the remote // Pull DO NOT sent any local changes to the remote and instead "rebase" them on top of new changes from remote // Return true, if new changes were applied locally - otherwise return false func (d *TursoSyncDb) Pull(ctx context.Context) (bool, error) { ... } // Push local changes to the remote // Push DO NOT fetch any remote changes func (d *TursoSyncDb) Push(ctx context.Context) error { ... } // Get stats for the synced database func (d *TursoSyncDb) Stats(ctx context.Context) (TursoSyncDbStats, error) { ... } // Checkpoint local WAL of the database func (d *TursoSyncDb) Checkpoint(ctx context.Context) error { ... } ``` - STREAM data from the http request to the completion in chunks and spin async operation in between in order to prevent loading whole response in memory - AVOID unnecessary FFI calls as their cost is non zero - AVOID unnecessary strings transformations - replace them with more efficient alternatives if possible - AVOID cryptic names - prefer short but concise names (wr is BAD, full_write is GOOD) - FOCUS on code readability: extract helper functions if it will contribute to the code readability (do not overoptimize - it's fine to have some logic inlined especially if it is not repeated anywhere else) - WATCH OUT for variables scopes and do not use variables which are no longer accessible # Implementation - Annotate public API with types - Add comments about public API fields/functions to clarify meaning and usage scenarios - Use `turso_sync_database_create()` method for creation of the synced database for now - DO NOT use init + open pair # Bindings You must use bindings in the `bindings_sync.go` and intergrate with `driver_db.go` code (use `NewConnection` function for that) Inspect `bindings_db.go` as you will reuse some abstractions from there. ================================================ FILE: bindings/go/go.mod ================================================ module turso.tech/database/tursogo go 1.24.0 toolchain go1.24.10 require ( github.com/ebitengine/purego v0.9.1 github.com/stretchr/testify v1.11.1 github.com/tursodatabase/turso-go-platform-libs v0.0.0-20251210190052-57d6c2f7db38 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.38.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: bindings/go/go.sum ================================================ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tursodatabase/turso-go-platform-libs v0.0.0-20251210075532-b81eb47907db h1:BwgDmz73+tIagiVlbbZTJVWehMVMnxmN0kHWgL0lvQ0= github.com/tursodatabase/turso-go-platform-libs v0.0.0-20251210075532-b81eb47907db/go.mod h1:bo+Lpv5OYOX1gRV9L5DLKMsYxmDs56SkZwnCOLEFcxU= github.com/tursodatabase/turso-go-platform-libs v0.0.0-20251210190052-57d6c2f7db38 h1:W+4IfZunKyz6NtCVovUsZ0ni/xsd9iworxbQs+3usF4= github.com/tursodatabase/turso-go-platform-libs v0.0.0-20251210190052-57d6c2f7db38/go.mod h1:bo+Lpv5OYOX1gRV9L5DLKMsYxmDs56SkZwnCOLEFcxU= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: bindings/java/.editorconfig ================================================ root = true [*.java] indent_size = 2 ij_continuation_indent_size = 4 ================================================ FILE: bindings/java/.gitignore ================================================ .gradle build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ ### IntelliJ IDEA ### .idea/ *.iws *.iml *.ipr out/ !**/src/main/**/out/ !**/src/test/**/out/ ### Eclipse ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache bin/ !**/src/main/**/bin/ !**/src/test/**/bin/ **/debug/** ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ ### VS Code ### .vscode/ ### Mac OS ### .DS_Store ### turso builds ### libs temp ### Rust build artifacts ### .rustc_info.json ================================================ FILE: bindings/java/.sdkmanrc ================================================ java=8.0.452-amzn ================================================ FILE: bindings/java/Cargo.toml ================================================ [package] name = "turso-java" version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true publish = false [lints] workspace = true [features] tracing_release = ["turso_core/tracing_release"] [lib] name = "_turso_java" crate-type = ["cdylib"] path = "rs_src/lib.rs" [dependencies] turso_core = { workspace = true, features = ["io_uring"] } jni = "0.21.1" thiserror = { workspace = true } ================================================ FILE: bindings/java/Makefile ================================================ RELEASE_DIR := libs TEMP_DIR := temp CARGO_BUILD := cargo build --profile release-official MACOS_X86_DIR := $(RELEASE_DIR)/macos_x86 MACOS_ARM64_DIR := $(RELEASE_DIR)/macos_arm64 WINDOWS_DIR := $(RELEASE_DIR)/windows LINUX_X86_DIR := $(RELEASE_DIR)/linux_x86 .PHONY: libs macos_x86 macos_arm64 windows lint lint_apply test build_test libs: macos_x86 macos_arm64 windows linux_x86 macos_x86: @echo "Building release version for macOS x86_64..." @mkdir -p $(TEMP_DIR) $(MACOS_X86_DIR) @CARGO_TARGET_DIR=$(TEMP_DIR) $(CARGO_BUILD) --target x86_64-apple-darwin @cp $(TEMP_DIR)/x86_64-apple-darwin/release-official/lib_turso_java.dylib $(MACOS_X86_DIR) @rm -rf $(TEMP_DIR) macos_arm64: @echo "Building release version for macOS ARM64..." @mkdir -p $(TEMP_DIR) $(MACOS_ARM64_DIR) @CARGO_TARGET_DIR=$(TEMP_DIR) $(CARGO_BUILD) --target aarch64-apple-darwin @cp $(TEMP_DIR)/aarch64-apple-darwin/release-official/lib_turso_java.dylib $(MACOS_ARM64_DIR) @rm -rf $(TEMP_DIR) # windows generates file with name `_turso_java.dll` unlike others, so we manually add prefix windows: @echo "Building release version for Windows..." @mkdir -p $(TEMP_DIR) $(WINDOWS_DIR) @CARGO_TARGET_DIR=$(TEMP_DIR) $(CARGO_BUILD) --target x86_64-pc-windows-gnu @cp $(TEMP_DIR)/x86_64-pc-windows-gnu/release-official/_turso_java.dll $(WINDOWS_DIR)/lib_turso_java.dll @rm -rf $(TEMP_DIR) linux_x86: @echo "Building release version for linux x86_64..." @mkdir -p $(TEMP_DIR) $(LINUX_X86_DIR) @CARGO_TARGET_DIR=$(TEMP_DIR) $(CARGO_BUILD) --target x86_64-unknown-linux-gnu @cp $(TEMP_DIR)/x86_64-unknown-linux-gnu/release-official/lib_turso_java.so $(LINUX_X86_DIR) @rm -rf $(TEMP_DIR) lint: ./gradlew spotlessCheck lint_apply: ./gradlew spotlessApply test: lint build_test ./gradlew test build_test: CARGO_TARGET_DIR=src/test/resources/turso cargo build publish_local: ./gradlew clean publishToMavenLocal ================================================ FILE: bindings/java/README.md ================================================ # Turso JDBC Driver The Turso JDBC driver is a library for accessing and creating Turso database files using Java. ## Project Status The project is actively developed. Feel free to open issues and contribute. To view related works, visit this [issue](https://github.com/tursodatabase/turso/issues/615). ## How to use Currently, we have not published to the maven central. Instead, you can locally build the jar and deploy it to maven local to use it. ### Build jar and publish to maven local ```shell $ cd bindings/java # Please select the appropriate target platform, currently supports `macos_x86`, `macos_arm64`, `windows` and `linux_x86` $ make macos_x86 # deploy to maven local $ make publish_local ``` Now you can use the dependency as follows: ```kotlin dependencies { implementation("tech.turso:turso:0.0.1-SNAPSHOT") } ``` ## Code style - Favor composition over inheritance. For example, `JDBC4Connection` doesn't implement `TursoConnection`. Instead, it includes `TursoConnection` as a field. This approach allows us to preserve the characteristics of Turso using `TursoConnection` easily while maintaining interoperability with the Java world using `JDBC4Connection`. ================================================ FILE: bindings/java/build.gradle.kts ================================================ import net.ltgt.gradle.errorprone.CheckSeverity import net.ltgt.gradle.errorprone.errorprone import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent plugins { java application `java-library` `maven-publish` signing id("net.ltgt.errorprone") version "3.1.0" // If you're stuck on JRE 8, use id 'com.diffplug.spotless' version '6.13.0' or older. id("com.diffplug.spotless") version "6.13.0" } // Apply publishing configuration apply(from = "gradle/publish.gradle.kts") // Helper function to read properties with defaults fun prop(key: String, default: String? = null): String? = findProperty(key)?.toString() ?: default group = prop("projectGroup") ?: error("projectGroup must be set in gradle.properties") version = prop("projectVersion") ?: error("projectVersion must be set in gradle.properties") java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 withJavadocJar() withSourcesJar() } // TODO: Add javadoc to required class and methods. After that, let's remove this settings tasks.withType { options { (this as StandardJavadocDocletOptions).apply { addStringOption("Xdoclint:none", "-quiet") } } } repositories { mavenCentral() } dependencies { compileOnly("org.slf4j:slf4j-api:1.7.32") errorprone("com.uber.nullaway:nullaway:0.10.26") // maximum version which supports java 8 errorprone("com.google.errorprone:error_prone_core:2.10.0") // maximum version which supports java 8 testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.assertj:assertj-core:3.27.0") } application { val tursoSystemLibraryPath = System.getenv("TURSO_LIBRARY_PATH") if (tursoSystemLibraryPath != null) { applicationDefaultJvmArgs = listOf( "-Djava.library.path=${System.getProperty("java.library.path")}:$tursoSystemLibraryPath" ) } } tasks.jar { from("libs") { into("libs") } } sourceSets { test { resources { file("src/main/resource/turso-jdbc.properties") } } } tasks.test { useJUnitPlatform() // In order to find rust built file under resources, we need to set it as system path systemProperty( "java.library.path", "${System.getProperty("java.library.path")}:$projectDir/src/test/resources/turso/debug" ) // For our fancy test logging testLogging { // set options for log level LIFECYCLE events( TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.STANDARD_OUT ) exceptionFormat = TestExceptionFormat.FULL showExceptions = true showCauses = true showStackTraces = true // set options for log level DEBUG and INFO debug { events( TestLogEvent.STARTED, TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.STANDARD_ERROR, TestLogEvent.STANDARD_OUT ) exceptionFormat = TestExceptionFormat.FULL } info.events = debug.events info.exceptionFormat = debug.exceptionFormat afterSuite(KotlinClosure2({ desc, result -> if (desc.parent == null) { // will match the outermost suite val output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" val startItem = "| " val endItem = " |" val repeatLength = startItem.length + output.length + endItem.length println("\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat(repeatLength)) } })) } } tasks.withType { options.errorprone { // Let's select which checks to perform. NullAway is enough for now. disableAllChecks = true check("NullAway", CheckSeverity.ERROR) option("NullAway:AnnotatedPackages", "tech.turso") option( "NullAway:CustomNullableAnnotations", "tech.turso.annotations.Nullable,tech.turso.annotations.SkipNullableCheck" ) } if (name.lowercase().contains("test")) { options.errorprone { disable("NullAway") } } } spotless { java { target("**/*.java") targetExclude(layout.buildDirectory.dir("**/*.java").get().asFile) targetExclude("example/**/*.java") targetExclude("src/main/java/examples/**/*.java") removeUnusedImports() googleJavaFormat("1.7") // or use eclipse().configFile("path/to/eclipse-format.xml") } } // Task to run the encryption example tasks.register("runEncryptionExample") { group = "examples" description = "Run the local database encryption example" classpath = sourceSets["main"].runtimeClasspath mainClass.set("examples.EncryptionExample") // Set the library path for native bindings // Check multiple possible locations for the native library val limboRoot = projectDir.parentFile.parentFile val tursoLibraryPath = System.getenv("TURSO_LIBRARY_PATH") ?: listOf( "${projectDir}/src/main/resources/libs/macos_arm64", "${projectDir}/src/main/resources/libs/linux_x64", "${projectDir}/build/resources/main/libs/macos_arm64", "${projectDir}/build/resources/main/libs/linux_x64", "${limboRoot}/target/debug", "${limboRoot}/target/release" ).joinToString(":") jvmArgs = listOf("-Djava.library.path=${System.getProperty("java.library.path")}:$tursoLibraryPath") } ================================================ FILE: bindings/java/example/.gitignore ================================================ .gradle build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ sample.db sample.db-wal ### IntelliJ IDEA ### .idea ================================================ FILE: bindings/java/example/build.gradle.kts ================================================ plugins { id("java") } group = "tech.turso" version = "1.0-SNAPSHOT" repositories { mavenLocal() mavenCentral() } dependencies { implementation("tech.turso:turso:0.0.1-SNAPSHOT") testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") } tasks.test { useJUnitPlatform() } tasks.register("run") { group = "application" classpath = sourceSets["main"].runtimeClasspath mainClass.set("tech.turso.Main") } ================================================ FILE: bindings/java/example/gradle/wrapper/gradle-wrapper.properties ================================================ #Sun Feb 02 20:06:51 KST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: bindings/java/example/gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and # * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: bindings/java/example/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: bindings/java/example/settings.gradle.kts ================================================ rootProject.name = "example" ================================================ FILE: bindings/java/example/src/main/java/tech.turso/Main.java ================================================ package tech.turso; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class Main { public static void main(String[] args) throws SQLException { try (Connection conn = DriverManager.getConnection("jdbc:turso:sample.db")) { try (Statement stmt = conn.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT)) { stmt.execute("CREATE TABLE users (id INT PRIMARY KEY, username TEXT);"); stmt.execute("INSERT INTO users VALUES (1, 'turso');"); stmt.execute("INSERT INTO users VALUES (2, 'turso');"); stmt.execute("INSERT INTO users VALUES (3, 'who knows');"); stmt.execute("SELECT * FROM users"); System.out.println( "result: " + stmt.getResultSet().getInt(1) + ", " + stmt.getResultSet().getString(2)); } } } } ================================================ FILE: bindings/java/gradle/publish.gradle.kts ================================================ import java.security.MessageDigest import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication import org.gradle.plugins.signing.SigningExtension // Helper function to read properties with defaults fun prop(key: String, default: String? = null): String? = project.findProperty(key)?.toString() ?: default // Maven Publishing Configuration configure { publications { create("mavenJava") { from(components["java"]) groupId = prop("projectGroup")!! artifactId = prop("projectArtifactId")!! version = prop("projectVersion")!! pom { name.set(prop("pomName")) description.set(prop("pomDescription")) url.set(prop("pomUrl")) licenses { license { name.set(prop("pomLicenseName")) url.set(prop("pomLicenseUrl")) } } developers { developer { id.set(prop("pomDeveloperId")) name.set(prop("pomDeveloperName")) email.set(prop("pomDeveloperEmail")) } } scm { connection.set(prop("pomScmConnection")) developerConnection.set(prop("pomScmDeveloperConnection")) url.set(prop("pomScmUrl")) } } } } } // GPG Signing Configuration configure { // Make signing required for publishing setRequired(true) // For CI/GitHub Actions: use in-memory keys val signingKey = providers.environmentVariable("MAVEN_SIGNING_KEY").orNull val signingPassword = providers.environmentVariable("MAVEN_SIGNING_PASSPHRASE").orNull if (signingKey != null && signingPassword != null) { // CI mode: use in-memory keys useInMemoryPgpKeys(signingKey, signingPassword) } else { // Local mode: use GPG command from system useGpgCmd() } sign(the().publications["mavenJava"]) } // Helper task to generate checksums val generateChecksums by tasks.registering { dependsOn("jar", "sourcesJar", "javadocJar", "generatePomFileForMavenJavaPublication") val checksumDir = layout.buildDirectory.dir("checksums") doLast { val files = listOf( tasks.named("jar").get().outputs.files.singleFile, tasks.named("sourcesJar").get().outputs.files.singleFile, tasks.named("javadocJar").get().outputs.files.singleFile, layout.buildDirectory.file("publications/mavenJava/pom-default.xml").get().asFile ) checksumDir.get().asFile.mkdirs() files.forEach { file -> if (file.exists()) { // MD5 val md5 = MessageDigest.getInstance("MD5") .digest(file.readBytes()) .joinToString("") { "%02x".format(it) } file("${file.absolutePath}.md5").writeText(md5) // SHA1 val sha1 = MessageDigest.getInstance("SHA-1") .digest(file.readBytes()) .joinToString("") { "%02x".format(it) } file("${file.absolutePath}.sha1").writeText(sha1) } } } } // Task to create a bundle zip for Maven Central Portal val createMavenCentralBundle by tasks.registering(Zip::class) { group = "publishing" description = "Creates a bundle zip for Maven Central Portal upload" dependsOn("generatePomFileForMavenJavaPublication", "jar", "sourcesJar", "javadocJar", "signMavenJavaPublication", generateChecksums) // Ensure signing happens before bundle creation mustRunAfter("signMavenJavaPublication") val groupId = prop("projectGroup")!!.replace(".", "/") val artifactId = prop("projectArtifactId")!! val projectVer = project.version.toString() // Validate version is not SNAPSHOT for Maven Central doFirst { if (projectVer.contains("SNAPSHOT")) { throw GradleException( "Cannot publish SNAPSHOT version to Maven Central. " + "Please change projectVersion in gradle.properties to a release version (e.g., 0.0.1)" ) } } archiveFileName.set("$artifactId-$projectVer-bundle.zip") destinationDirectory.set(layout.buildDirectory.dir("maven-central")) // Maven Central expects files in groupId/artifactId/version/ structure val basePath = "$groupId/$artifactId/$projectVer" // Main JAR + checksums + signature from(tasks.named("jar").get().outputs.files) { into(basePath) rename { "$artifactId-$projectVer.jar" } } from(tasks.named("jar").get().outputs.files.singleFile.absolutePath + ".md5") { into(basePath) rename { "$artifactId-$projectVer.jar.md5" } } from(tasks.named("jar").get().outputs.files.singleFile.absolutePath + ".sha1") { into(basePath) rename { "$artifactId-$projectVer.jar.sha1" } } // Sources JAR + checksums + signature from(tasks.named("sourcesJar").get().outputs.files) { into(basePath) rename { "$artifactId-$projectVer-sources.jar" } } from(tasks.named("sourcesJar").get().outputs.files.singleFile.absolutePath + ".md5") { into(basePath) rename { "$artifactId-$projectVer-sources.jar.md5" } } from(tasks.named("sourcesJar").get().outputs.files.singleFile.absolutePath + ".sha1") { into(basePath) rename { "$artifactId-$projectVer-sources.jar.sha1" } } // Javadoc JAR + checksums + signature from(tasks.named("javadocJar").get().outputs.files) { into(basePath) rename { "$artifactId-$projectVer-javadoc.jar" } } from(tasks.named("javadocJar").get().outputs.files.singleFile.absolutePath + ".md5") { into(basePath) rename { "$artifactId-$projectVer-javadoc.jar.md5" } } from(tasks.named("javadocJar").get().outputs.files.singleFile.absolutePath + ".sha1") { into(basePath) rename { "$artifactId-$projectVer-javadoc.jar.sha1" } } // POM + checksums + signature from(layout.buildDirectory.file("publications/mavenJava/pom-default.xml")) { into(basePath) rename { "$artifactId-$projectVer.pom" } } from(layout.buildDirectory.file("publications/mavenJava/pom-default.xml").get().asFile.absolutePath + ".md5") { into(basePath) rename { "$artifactId-$projectVer.pom.md5" } } from(layout.buildDirectory.file("publications/mavenJava/pom-default.xml").get().asFile.absolutePath + ".sha1") { into(basePath) rename { "$artifactId-$projectVer.pom.sha1" } } // Signature files - get them from the signing task outputs doFirst { val signingTask = tasks.named("signMavenJavaPublication").get() logger.lifecycle("Signing task outputs: ${signingTask.outputs.files.files}") } // Include signature files generated by the signing plugin from(tasks.named("signMavenJavaPublication").get().outputs.files) { into(basePath) include("*.jar.asc", "pom-default.xml.asc") exclude("module.json.asc") // Exclude gradle module metadata signature rename { name -> // Only rename the POM signature file // JAR signatures are already correctly named by the signing plugin if (name == "pom-default.xml.asc") { "$artifactId-$projectVer.pom.asc" } else { name // Keep original name (already correct) } } } } // Task to upload bundle to Maven Central Portal tasks.register("publishToMavenCentral") { group = "publishing" description = "Publishes artifacts to Maven Central Portal" // Run publish first to generate signatures, then create bundle dependsOn("publish") dependsOn(createMavenCentralBundle) // Make sure bundle creation happens after publish createMavenCentralBundle.get().mustRunAfter("publish") doLast { val username = providers.environmentVariable("MAVEN_UPLOAD_USERNAME").orNull val password = providers.environmentVariable("MAVEN_UPLOAD_PASSWORD").orNull val bundleFile = createMavenCentralBundle.get().archiveFile.get().asFile require(username != null) { "MAVEN_UPLOAD_USERNAME environment variable must be set" } require(password != null) { "MAVEN_UPLOAD_PASSWORD environment variable must be set" } require(bundleFile.exists()) { "Bundle file does not exist: ${bundleFile.absolutePath}" } logger.lifecycle("Uploading bundle to Maven Central Portal...") logger.lifecycle("Bundle: ${bundleFile.absolutePath}") logger.lifecycle("Size: ${bundleFile.length() / 1024} KB") // Use curl for uploading (simple and available on most systems) exec { commandLine( "curl", "-X", "POST", "-u", "$username:$password", "-F", "bundle=@${bundleFile.absolutePath}", "https://central.sonatype.com/api/v1/publisher/upload?name=${bundleFile.name}&publishingType=AUTOMATIC" ) } logger.lifecycle("Upload completed. Check https://central.sonatype.com/publishing for status.") } } ================================================ FILE: bindings/java/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: bindings/java/gradle.properties ================================================ projectGroup=tech.turso projectVersion=0.6.0-pre.4 projectArtifactId=turso # POM metadata pomName=Turso JDBC Driver pomDescription=Turso JDBC driver for Java applications pomUrl=https://github.com/tursodatabase/turso pomLicenseName=MIT License pomLicenseUrl=https://opensource.org/licenses/MIT pomDeveloperId=turso pomDeveloperName=Turso pomDeveloperEmail=penberg@iki.fi pomScmConnection=scm:git:git://github.com/tursodatabase/turso.git pomScmDeveloperConnection=scm:git:ssh://github.com:tursodatabase/turso.git pomScmUrl=https://github.com/tursodatabase/turso ================================================ FILE: bindings/java/gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s ' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: bindings/java/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: bindings/java/rs_src/errors.rs ================================================ use jni::errors::{Error, JniError}; use thiserror::Error; #[derive(Debug, Error)] pub enum TursoError { #[error("Custom error: `{0}`")] CustomError(String), #[error("Invalid database pointer")] InvalidDatabasePointer, #[error("Invalid connection pointer")] InvalidConnectionPointer, #[error("JNI Errors: `{0}`")] JNIErrors(Error), } impl From for TursoError { fn from(_value: turso_core::LimboError) -> Self { todo!() } } impl From for JniError { fn from(value: TursoError) -> Self { match value { TursoError::CustomError(_) | TursoError::InvalidDatabasePointer | TursoError::InvalidConnectionPointer | TursoError::JNIErrors(_) => { eprintln!("Error occurred: {value:?}"); JniError::Other(-1) } } } } impl From for TursoError { fn from(value: jni::errors::Error) -> Self { TursoError::JNIErrors(value) } } pub type Result = std::result::Result; pub const SQLITE_OK: i32 = 0; // Successful result pub const SQLITE_ERROR: i32 = 1; // Generic error #[allow(dead_code)] pub const SQLITE_INTERNAL: i32 = 2; // Internal logic error in SQLite #[allow(dead_code)] pub const SQLITE_PERM: i32 = 3; // Access permission denied #[allow(dead_code)] pub const SQLITE_ABORT: i32 = 4; // Callback routine requested an abort #[allow(dead_code)] pub const SQLITE_BUSY: i32 = 5; // The database file is locked #[allow(dead_code)] pub const SQLITE_LOCKED: i32 = 6; // A table in the database is locked #[allow(dead_code)] pub const SQLITE_NOMEM: i32 = 7; // A malloc() failed #[allow(dead_code)] pub const SQLITE_READONLY: i32 = 8; // Attempt to write a readonly database #[allow(dead_code)] pub const SQLITE_INTERRUPT: i32 = 9; // Operation terminated by sqlite3_interrupt() #[allow(dead_code)] pub const SQLITE_IOERR: i32 = 10; // Some kind of disk I/O error occurred #[allow(dead_code)] pub const SQLITE_CORRUPT: i32 = 11; // The database disk image is malformed #[allow(dead_code)] pub const SQLITE_NOTFOUND: i32 = 12; // Unknown opcode in sqlite3_file_control() #[allow(dead_code)] pub const SQLITE_FULL: i32 = 13; // Insertion failed because database is full #[allow(dead_code)] pub const SQLITE_CANTOPEN: i32 = 14; // Unable to open the database file #[allow(dead_code)] pub const SQLITE_PROTOCOL: i32 = 15; // Database lock protocol error #[allow(dead_code)] pub const SQLITE_EMPTY: i32 = 16; // Internal use only #[allow(dead_code)] pub const SQLITE_SCHEMA: i32 = 17; // The database schema changed #[allow(dead_code)] pub const SQLITE_TOOBIG: i32 = 18; // String or BLOB exceeds size limit #[allow(dead_code)] pub const SQLITE_CONSTRAINT: i32 = 19; // Abort due to constraint violation #[allow(dead_code)] pub const SQLITE_MISMATCH: i32 = 20; // Data type mismatch #[allow(dead_code)] pub const SQLITE_MISUSE: i32 = 21; // Library used incorrectly #[allow(dead_code)] pub const SQLITE_NOLFS: i32 = 22; // Uses OS features not supported on host #[allow(dead_code)] pub const SQLITE_AUTH: i32 = 23; // Authorization denied #[allow(dead_code)] pub const SQLITE_ROW: i32 = 100; // sqlite3_step() has another row ready #[allow(dead_code)] pub const SQLITE_DONE: i32 = 101; // sqlite3_step() has finished executing #[allow(dead_code)] pub const SQLITE_INTEGER: i32 = 1; #[allow(dead_code)] pub const SQLITE_FLOAT: i32 = 2; #[allow(dead_code)] pub const SQLITE_TEXT: i32 = 3; #[allow(dead_code)] pub const SQLITE_BLOB: i32 = 4; #[allow(dead_code)] pub const SQLITE_NULL: i32 = 5; pub const TURSO_FAILED_TO_PARSE_BYTE_ARRAY: i32 = 1100; pub const TURSO_FAILED_TO_PREPARE_STATEMENT: i32 = 1200; pub const TURSO_ETC: i32 = 9999; ================================================ FILE: bindings/java/rs_src/lib.rs ================================================ mod errors; mod turso_connection; mod turso_db; mod turso_statement; mod utils; ================================================ FILE: bindings/java/rs_src/turso_connection.rs ================================================ use crate::errors::{ Result, TursoError, TURSO_ETC, TURSO_FAILED_TO_PARSE_BYTE_ARRAY, TURSO_FAILED_TO_PREPARE_STATEMENT, }; use crate::turso_statement::TursoStatement; use crate::utils::{set_err_msg_and_throw_exception, utf8_byte_arr_to_str}; use jni::objects::{JByteArray, JObject}; use jni::sys::jlong; use jni::JNIEnv; use std::sync::Arc; use turso_core::Connection; #[derive(Clone)] pub struct TursoConnection { pub(crate) conn: Arc, pub(crate) _io: Arc, } impl TursoConnection { pub fn new(conn: Arc, io: Arc) -> Self { TursoConnection { conn, _io: io } } #[allow(clippy::wrong_self_convention)] pub fn to_ptr(self) -> jlong { Box::into_raw(Box::new(self)) as jlong } pub fn drop(ptr: jlong) { let _boxed = unsafe { Box::from_raw(ptr as *mut TursoConnection) }; } } pub fn to_turso_connection(ptr: jlong) -> Result<&'static mut TursoConnection> { if ptr == 0 { Err(TursoError::InvalidConnectionPointer) } else { unsafe { Ok(&mut *(ptr as *mut TursoConnection)) } } } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoConnection__1close<'local>( _env: JNIEnv<'local>, _obj: JObject<'local>, connection_ptr: jlong, ) { TursoConnection::drop(connection_ptr); } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoConnection_prepareUtf8<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, connection_ptr: jlong, sql_bytes: JByteArray<'local>, ) -> jlong { let connection = match to_turso_connection(connection_ptr) { Ok(conn) => conn, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return 0; } }; let sql = match utf8_byte_arr_to_str(&env, sql_bytes) { Ok(sql) => sql, Err(e) => { set_err_msg_and_throw_exception( &mut env, obj, TURSO_FAILED_TO_PARSE_BYTE_ARRAY, e.to_string(), ); return 0; } }; match connection.conn.prepare(sql) { Ok(stmt) => TursoStatement::new(stmt, connection.clone()).to_ptr(), Err(e) => { set_err_msg_and_throw_exception( &mut env, obj, TURSO_FAILED_TO_PREPARE_STATEMENT, e.to_string(), ); 0 } } } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoConnection__1getAutoCommit<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, connection_ptr: jlong, ) -> jni::sys::jboolean { let connection = match to_turso_connection(connection_ptr) { Ok(conn) => conn, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return 0; // equivalent to false, but exception is thrown } }; if connection.conn.get_auto_commit() { 1 } else { 0 } } ================================================ FILE: bindings/java/rs_src/turso_db.rs ================================================ use crate::errors::{Result, TursoError, TURSO_ETC}; use crate::turso_connection::TursoConnection; use crate::utils::set_err_msg_and_throw_exception; use jni::objects::{JByteArray, JObject, JString}; use jni::sys::{jint, jlong}; use jni::JNIEnv; use std::sync::Arc; use turso_core::{Database, DatabaseOpts, EncryptionKey, EncryptionOpts, OpenFlags}; struct TursoDB { db: Arc, io: Arc, /// Encryption info: (cipher, hexkey) - stored as strings for lazy parsing encryption_info: Option<(String, String)>, } impl TursoDB { pub fn new( db: Arc, io: Arc, encryption_info: Option<(String, String)>, ) -> Self { TursoDB { db, io, encryption_info, } } #[allow(clippy::wrong_self_convention)] pub fn to_ptr(self) -> jlong { Box::into_raw(Box::new(self)) as jlong } pub fn drop(ptr: jlong) { let _boxed = unsafe { Box::from_raw(ptr as *mut TursoDB) }; } } fn to_turso_db(ptr: jlong) -> Result<&'static mut TursoDB> { if ptr == 0 { Err(TursoError::InvalidDatabasePointer) } else { unsafe { Ok(&mut *(ptr as *mut TursoDB)) } } } #[no_mangle] #[allow(clippy::arc_with_non_send_sync)] pub extern "system" fn Java_tech_turso_core_TursoDB_openUtf8<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, file_path_byte_arr: JByteArray<'local>, _open_flags: jint, ) -> jlong { let io = match turso_core::PlatformIO::new() { Ok(io) => Arc::new(io), Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return -1; } }; let path = match env .convert_byte_array(file_path_byte_arr) .map_err(|e| e.to_string()) { Ok(bytes) => match String::from_utf8(bytes) { Ok(s) => s, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return -1; } }, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e); return -1; } }; let db = match Database::open_file(io.clone(), &path) { Ok(db) => db, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return -1; } }; TursoDB::new(db, io, None).to_ptr() } /// Opens a database with encryption support. /// cipher and hexkey can be null for unencrypted databases. #[no_mangle] #[allow(clippy::arc_with_non_send_sync)] pub extern "system" fn Java_tech_turso_core_TursoDB_openWithEncryptionUtf8<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, file_path_byte_arr: JByteArray<'local>, _open_flags: jint, cipher: JString<'local>, hexkey: JString<'local>, ) -> jlong { let io = match turso_core::PlatformIO::new() { Ok(io) => Arc::new(io), Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return -1; } }; let path = match env .convert_byte_array(file_path_byte_arr) .map_err(|e| e.to_string()) { Ok(bytes) => match String::from_utf8(bytes) { Ok(s) => s, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return -1; } }, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e); return -1; } }; // Parse encryption options if provided let encryption_opts = if !cipher.is_null() && !hexkey.is_null() { let cipher_str: String = match env.get_string(&cipher) { Ok(s) => s.into(), Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return -1; } }; let hexkey_str: String = match env.get_string(&hexkey) { Ok(s) => s.into(), Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return -1; } }; Some(EncryptionOpts { cipher: cipher_str, hexkey: hexkey_str, }) } else { None }; // Clone encryption info before encryption_opts is consumed let encryption_info = encryption_opts .as_ref() .map(|opts| (opts.cipher.clone(), opts.hexkey.clone())); let db_opts = if encryption_opts.is_some() { DatabaseOpts::new().with_encryption(true) } else { DatabaseOpts::new() }; let db = match Database::open_file_with_flags( io.clone(), &path, OpenFlags::Create, db_opts, encryption_opts, ) { Ok(db) => db, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return -1; } }; TursoDB::new(db, io, encryption_info).to_ptr() } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoDB_connect0<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, db_pointer: jlong, ) -> jlong { let db = match to_turso_db(db_pointer) { Ok(db) => db, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return 0; } }; // Parse encryption key if encryption info is present let encryption_key = if let Some((_cipher, hexkey)) = &db.encryption_info { match EncryptionKey::from_hex_string(hexkey) { Ok(key) => Some(key), Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return 0; } } } else { None }; // Use connect_with_encryption to properly set up encryption context // before the pager reads page 1. This is required for encrypted databases. let conn = match db.db.connect_with_encryption(encryption_key) { Ok(conn) => conn, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, e.to_string()); return 0; } }; TursoConnection::new(conn, db.io.clone()).to_ptr() } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoDB_close0<'local>( _env: JNIEnv<'local>, _obj: JObject<'local>, db_pointer: jlong, ) { TursoDB::drop(db_pointer); } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoDB_throwJavaException<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, error_code: jint, ) { set_err_msg_and_throw_exception( &mut env, obj, error_code, "throw java exception".to_string(), ); } ================================================ FILE: bindings/java/rs_src/turso_statement.rs ================================================ use crate::errors::{Result, SQLITE_ERROR, SQLITE_OK}; use crate::errors::{TursoError, TURSO_ETC}; use crate::turso_connection::TursoConnection; use crate::utils::set_err_msg_and_throw_exception; use jni::objects::{JByteArray, JObject, JObjectArray, JString, JValue}; use jni::sys::{jdouble, jint, jlong}; use jni::JNIEnv; use std::num::NonZero; use turso_core::{LimboError, Statement, Value}; pub const STEP_RESULT_ID_ROW: i32 = 10; #[allow(dead_code)] pub const STEP_RESULT_ID_IO: i32 = 20; pub const STEP_RESULT_ID_DONE: i32 = 30; pub const STEP_RESULT_ID_INTERRUPT: i32 = 40; pub const STEP_RESULT_ID_BUSY: i32 = 50; pub const STEP_RESULT_ID_ERROR: i32 = 60; pub struct TursoStatement { pub(crate) stmt: Statement, pub(crate) connection: TursoConnection, } impl TursoStatement { pub fn new(stmt: Statement, connection: TursoConnection) -> Self { TursoStatement { stmt, connection } } #[allow(clippy::wrong_self_convention)] pub fn to_ptr(self) -> jlong { Box::into_raw(Box::new(self)) as jlong } pub fn drop(ptr: jlong) { let _boxed = unsafe { Box::from_raw(ptr as *mut TursoStatement) }; } } pub fn to_turso_statement(ptr: jlong) -> Result<&'static mut TursoStatement> { if ptr == 0 { Err(TursoError::InvalidConnectionPointer) } else { unsafe { Ok(&mut *(ptr as *mut TursoStatement)) } } } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement_step<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, stmt_ptr: jlong, ) -> JObject<'local> { let stmt = match to_turso_statement(stmt_ptr) { Ok(stmt) => stmt, Err(e) => { let err_msg = e.to_string(); set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, err_msg.clone()); return to_turso_step_result_error(&mut env, &err_msg); } }; let result = stmt.stmt.run_one_step_blocking(|| Ok(()), || Ok(())); match result { Ok(Some(row)) => match row_to_obj_array(&mut env, row) { Ok(row) => to_turso_step_result(&mut env, STEP_RESULT_ID_ROW, Some(row)), Err(e) => { let err_msg = e.to_string(); set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, err_msg.clone()); to_turso_step_result_error(&mut env, &err_msg) } }, Ok(None) => { // Done to_turso_step_result(&mut env, STEP_RESULT_ID_DONE, None) } Err(LimboError::Interrupt) => { to_turso_step_result(&mut env, STEP_RESULT_ID_INTERRUPT, None) } Err(LimboError::Busy) => to_turso_step_result(&mut env, STEP_RESULT_ID_BUSY, None), Err(err) => { let err_msg = err.to_string(); set_err_msg_and_throw_exception(&mut env, obj, TURSO_ETC, err_msg.clone()); to_turso_step_result_error(&mut env, &err_msg) } } } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement__1close<'local>( _env: JNIEnv<'local>, _obj: JObject<'local>, stmt_ptr: jlong, ) { TursoStatement::drop(stmt_ptr); } fn row_to_obj_array<'local>( env: &mut JNIEnv<'local>, row: &turso_core::Row, ) -> Result> { let obj_array = env.new_object_array(row.len() as i32, "java/lang/Object", JObject::null())?; for (i, value) in row.get_values().enumerate() { let obj = match value { turso_core::Value::Null => JObject::null(), turso_core::Value::Numeric(turso_core::Numeric::Integer(i)) => { env.new_object("java/lang/Long", "(J)V", &[JValue::Long(*i)])? } turso_core::Value::Numeric(turso_core::Numeric::Float(f)) => { env.new_object("java/lang/Double", "(D)V", &[JValue::Double(f64::from(*f))])? } turso_core::Value::Text(s) => env.new_string(s.as_str())?.into(), turso_core::Value::Blob(b) => env.byte_array_from_slice(b.as_slice())?.into(), }; if let Err(e) = env.set_object_array_element(&obj_array, i as i32, obj) { eprintln!("Error on parsing row: {e:?}"); } } Ok(obj_array.into()) } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement_columns<'local>( mut env: JNIEnv<'local>, _obj: JObject<'local>, stmt_ptr: jlong, ) -> JObject<'local> { let stmt = to_turso_statement(stmt_ptr).unwrap(); let num_columns = stmt.stmt.num_columns(); let obj_arr: JObjectArray = env .new_object_array(num_columns as i32, "java/lang/String", JObject::null()) .unwrap(); for i in 0..num_columns { let column_name = stmt.stmt.get_column_name(i); let str = env.new_string(column_name.into_owned()).unwrap(); env.set_object_array_element(&obj_arr, i as i32, str) .unwrap(); } obj_arr.into() } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement_bindNull<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, stmt_ptr: jlong, position: jint, ) -> jint { let stmt = match to_turso_statement(stmt_ptr) { Ok(stmt) => stmt, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); return SQLITE_ERROR; } }; stmt.stmt .bind_at(NonZero::new(position as usize).unwrap(), Value::Null); SQLITE_OK } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement_bindLong<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, stmt_ptr: jlong, position: jint, value: jlong, ) -> jint { let stmt = match to_turso_statement(stmt_ptr) { Ok(stmt) => stmt, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); return SQLITE_ERROR; } }; stmt.stmt.bind_at( NonZero::new(position as usize).unwrap(), Value::from_i64(value), ); SQLITE_OK } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement_bindDouble<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, stmt_ptr: jlong, position: jint, value: jdouble, ) -> jint { let stmt = match to_turso_statement(stmt_ptr) { Ok(stmt) => stmt, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); return SQLITE_ERROR; } }; stmt.stmt.bind_at( NonZero::new(position as usize).unwrap(), Value::from_f64(value), ); SQLITE_OK } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement_bindText<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, stmt_ptr: jlong, position: jint, value: JString<'local>, ) -> jint { let stmt = match to_turso_statement(stmt_ptr) { Ok(stmt) => stmt, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); return SQLITE_ERROR; } }; let text: String = match env.get_string(&value) { Ok(s) => s.into(), Err(_) => return SQLITE_ERROR, }; stmt.stmt.bind_at( NonZero::new(position as usize).unwrap(), Value::build_text(text), ); SQLITE_OK } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement_bindBlob<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, stmt_ptr: jlong, position: jint, value: JByteArray<'local>, ) -> jint { let stmt = match to_turso_statement(stmt_ptr) { Ok(stmt) => stmt, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); return SQLITE_ERROR; } }; let blob: Vec = match env.convert_byte_array(value) { Ok(b) => b, Err(_) => return SQLITE_ERROR, }; stmt.stmt .bind_at(NonZero::new(position as usize).unwrap(), Value::Blob(blob)); SQLITE_OK } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement_totalChanges<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, stmt_ptr: jlong, ) -> jlong { let stmt = match to_turso_statement(stmt_ptr) { Ok(stmt) => stmt, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); return -1; } }; stmt.connection.conn.total_changes() } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement_changes<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, stmt_ptr: jlong, ) -> jlong { let stmt = match to_turso_statement(stmt_ptr) { Ok(stmt) => stmt, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); return -1; } }; stmt.connection.conn.changes() } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement_parameterCount<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, stmt_ptr: jlong, ) -> jint { let stmt = match to_turso_statement(stmt_ptr) { Ok(stmt) => stmt, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); return -1; } }; stmt.stmt.parameters_count() as jint } #[no_mangle] pub extern "system" fn Java_tech_turso_core_TursoStatement_reset<'local>( mut env: JNIEnv<'local>, obj: JObject<'local>, stmt_ptr: jlong, ) -> jint { let stmt = match to_turso_statement(stmt_ptr) { Ok(stmt) => stmt, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); return -1; } }; match stmt.stmt.reset() { Ok(_) => 0, Err(e) => { set_err_msg_and_throw_exception(&mut env, obj, SQLITE_ERROR, e.to_string()); -1 } } } /// Converts an optional `JObject` into Java's `TursoStepResult`. /// /// This function takes an optional `JObject` and converts it into a Java object /// of type `TursoStepResult`. The conversion is done by creating a new Java object with the /// appropriate constructor arguments. /// /// # Arguments /// /// * `env` - A mutable reference to the JNI environment. /// * `id` - An integer representing the type of `StepResult`. /// * `result` - An optional `JObject` that contains the result data. /// /// # Returns /// /// A `JObject` representing the `TursoStepResult` in Java. If the object creation fails, /// a null `JObject` is returned fn to_turso_step_result<'local>( env: &mut JNIEnv<'local>, id: i32, result: Option>, ) -> JObject<'local> { let mut ctor_args = vec![JValue::Int(id)]; if let Some(res) = result { ctor_args.push(JValue::Object(&res)); env.new_object( "tech/turso/core/TursoStepResult", "(I[Ljava/lang/Object;)V", &ctor_args, ) } else { env.new_object("tech/turso/core/TursoStepResult", "(I)V", &ctor_args) } .unwrap_or_else(|_| JObject::null()) } /// Creates an error `TursoStepResult` with an error message. fn to_turso_step_result_error<'local>( env: &mut JNIEnv<'local>, error_message: &str, ) -> JObject<'local> { // Clear any pending exception first, as JNI calls won't work with a pending exception. // This can happen if set_err_msg_and_throw_exception was called before this function. if env.exception_check().unwrap_or(false) { let _ = env.exception_clear(); } let error_str = match env.new_string(error_message) { Ok(s) => s, Err(_) => return JObject::null(), }; let error_obj: JObject = error_str.into(); let ctor_args = vec![ JValue::Int(STEP_RESULT_ID_ERROR), JValue::Object(&error_obj), ]; env.new_object( "tech/turso/core/TursoStepResult", "(ILjava/lang/String;)V", &ctor_args, ) .unwrap_or_else(|_| JObject::null()) } ================================================ FILE: bindings/java/rs_src/utils.rs ================================================ use crate::errors::TursoError; use jni::objects::{JByteArray, JObject}; use jni::JNIEnv; pub(crate) fn utf8_byte_arr_to_str( env: &JNIEnv, bytes: JByteArray, ) -> crate::errors::Result { let bytes = env .convert_byte_array(bytes) .map_err(|_| TursoError::CustomError("Failed to retrieve bytes".to_string()))?; let str = String::from_utf8(bytes).map_err(|_| { TursoError::CustomError("Failed to convert utf8 byte array into string".to_string()) })?; Ok(str) } /// Sets the error message and throws a Java exception. /// /// This function converts the provided error message to a byte array and calls the /// `throwTursoException` method on the provided Java object to throw an exception. /// /// # Parameters /// - `env`: The JNI environment. /// - `obj`: The Java object on which the exception will be thrown. /// - `err_code`: The error code corresponding to the exception. Refer to `tech.turso.core.Codes` for the list of error codes. /// - `err_msg`: The error message to be included in the exception. /// /// # Example /// ```rust /// set_err_msg_and_throw_exception(env, obj, Codes::SQLITE_ERROR, "An error occurred".to_string()); /// ``` pub fn set_err_msg_and_throw_exception<'local>( env: &mut JNIEnv<'local>, obj: JObject<'local>, err_code: i32, err_msg: String, ) { let error_message_bytes = env .byte_array_from_slice(err_msg.as_bytes()) .expect("Failed to convert to byte array"); match env.call_method( obj, "throwTursoException", "(I[B)V", &[err_code.into(), (&error_message_bytes).into()], ) { Ok(_) => { // do nothing because above method will always return Err } Err(_e) => { // do nothing because our java app will handle Err } } } ================================================ FILE: bindings/java/settings.gradle.kts ================================================ rootProject.name = "turso" ================================================ FILE: bindings/java/src/main/java/examples/EncryptionExample.java ================================================ package examples; import tech.turso.core.TursoConnection; import tech.turso.core.TursoDB; import tech.turso.core.TursoEncryptionCipher; import tech.turso.core.TursoResultSet; import tech.turso.core.TursoStatement; import java.io.File; import java.nio.file.Files; /** * Local Database Encryption Example * * This example demonstrates how to use local database encryption * with the Turso Java SDK. * * Supported ciphers: * - AES_128_GCM * - AES_256_GCM * - AEGIS_256 * - AEGIS_256X2 * - AEGIS_128L * - AEGIS_128X2 * - AEGIS_128X4 */ public class EncryptionExample { private static final String DB_PATH = "encrypted.db"; // 32-byte hex key for aegis256 (256 bits = 32 bytes = 64 hex chars) private static final String ENCRYPTION_KEY = "b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327"; public static void main(String[] args) throws Exception { System.out.println("=== Turso Local Encryption Example ===\n"); // Create an encrypted database System.out.println("1. Creating encrypted database..."); TursoDB db = TursoDB.createWithEncryption( "jdbc:turso:" + DB_PATH, DB_PATH, TursoEncryptionCipher.AEGIS_256, ENCRYPTION_KEY ); TursoConnection conn = new TursoConnection("jdbc:turso:" + DB_PATH, db); // Create a table and insert sensitive data System.out.println("2. Creating table and inserting data..."); TursoStatement stmt = conn.prepare("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, ssn TEXT)"); stmt.execute(); stmt.close(); stmt = conn.prepare("INSERT INTO users (name, ssn) VALUES ('Alice', '123-45-6789')"); stmt.execute(); stmt.close(); stmt = conn.prepare("INSERT INTO users (name, ssn) VALUES ('Bob', '987-65-4321')"); stmt.execute(); stmt.close(); // Checkpoint to flush data to disk stmt = conn.prepare("PRAGMA wal_checkpoint(truncate)"); stmt.execute(); stmt.close(); // Query the data System.out.println("3. Querying data..."); stmt = conn.prepare("SELECT * FROM users"); stmt.execute(); TursoResultSet rs = stmt.getResultSet(); while (rs.next()) { System.out.println(" User: id=" + rs.get(1) + ", name=" + rs.get(2) + ", ssn=" + rs.get(3)); } stmt.close(); conn.close(); db.close(); // Verify the data is encrypted on disk System.out.println("\n4. Verifying encryption..."); byte[] rawContent = Files.readAllBytes(new File(DB_PATH).toPath()); String contentStr = new String(rawContent); boolean containsPlaintext = contentStr.contains("Alice") || contentStr.contains("123-45-6789"); if (containsPlaintext) { System.out.println(" WARNING: Data appears to be unencrypted!"); } else { System.out.println(" Data is encrypted on disk (plaintext not found)"); } // Reopen with the same key System.out.println("\n5. Reopening database with correct key..."); TursoDB db2 = TursoDB.createWithEncryption( "jdbc:turso:" + DB_PATH, DB_PATH, TursoEncryptionCipher.AEGIS_256, ENCRYPTION_KEY ); TursoConnection conn2 = new TursoConnection("jdbc:turso:" + DB_PATH, db2); TursoStatement stmt2 = conn2.prepare("SELECT name FROM users"); stmt2.execute(); TursoResultSet rs2 = stmt2.getResultSet(); System.out.print(" Successfully read users: "); while (rs2.next()) { System.out.print(rs2.get(1) + " "); } System.out.println(); stmt2.close(); conn2.close(); db2.close(); // Demonstrate that wrong key fails System.out.println("\n6. Attempting to open with wrong key (should fail)..."); try { TursoDB db3 = TursoDB.createWithEncryption( "jdbc:turso:" + DB_PATH, DB_PATH, TursoEncryptionCipher.AEGIS_256, "aaaaaaa4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327" ); TursoConnection conn3 = new TursoConnection("jdbc:turso:" + DB_PATH, db3); TursoStatement stmt3 = conn3.prepare("SELECT * FROM users"); stmt3.execute(); System.out.println(" ERROR: Should have failed with wrong key!"); } catch (Exception e) { System.out.println(" Correctly failed: " + e.getMessage()); } // Cleanup new File(DB_PATH).delete(); System.out.println("\n=== Example completed successfully ==="); } } ================================================ FILE: bindings/java/src/main/java/tech/turso/JDBC.java ================================================ package tech.turso; import java.sql.*; import java.util.Locale; import java.util.Properties; import tech.turso.annotations.Nullable; import tech.turso.annotations.SkipNullableCheck; import tech.turso.core.TursoPropertiesHolder; import tech.turso.jdbc4.JDBC4Connection; import tech.turso.utils.Logger; import tech.turso.utils.LoggerFactory; /** Turso JDBC driver implementation. */ public final class JDBC implements Driver { private static final Logger logger = LoggerFactory.getLogger(JDBC.class); private static final String VALID_URL_PREFIX = "jdbc:turso:"; static { try { DriverManager.registerDriver(new JDBC()); } catch (Exception e) { logger.error("Failed to register driver", e); } } /** * Creates a new Turso JDBC connection. * * @param url the database URL * @param properties connection properties * @return a new connection instance, or null if the URL is not valid * @throws SQLException if a database access error occurs */ @Nullable public static JDBC4Connection createConnection(String url, Properties properties) throws SQLException { if (!isValidURL(url)) return null; url = url.trim(); return new JDBC4Connection(url, extractAddress(url), properties); } private static boolean isValidURL(String url) { return (url != null && url.toLowerCase(Locale.ROOT).startsWith(VALID_URL_PREFIX)); } private static String extractAddress(String url) { return url.substring(VALID_URL_PREFIX.length()); } @Nullable @Override public Connection connect(String url, Properties info) throws SQLException { return createConnection(url, info); } @Override public boolean acceptsURL(String url) throws SQLException { return isValidURL(url); } @Override public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { return TursoConfig.getDriverPropertyInfo(); } @Override public int getMajorVersion() { return Integer.parseInt(TursoPropertiesHolder.getDriverVersion().split("\\.")[0]); } @Override public int getMinorVersion() { return Integer.parseInt(TursoPropertiesHolder.getDriverVersion().split("\\.")[1]); } @Override public boolean jdbcCompliant() { return false; } @Override @SkipNullableCheck public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { // TODO return null; } } ================================================ FILE: bindings/java/src/main/java/tech/turso/TursoConfig.java ================================================ package tech.turso; import java.sql.DriverPropertyInfo; import java.util.Arrays; import java.util.Properties; /** Turso Configuration. */ public final class TursoConfig { private static final DriverPropertyInfo[] driverPropertyInfo = driverPropertyInfo(); private final Properties pragma; public TursoConfig(Properties properties) { this.pragma = properties; } public static DriverPropertyInfo[] getDriverPropertyInfo() { return driverPropertyInfo; } public Properties toProperties() { Properties copy = new Properties(); copy.putAll(pragma); return copy; } public enum Pragma { ; private final String pragmaName; private final String description; private final String[] choices; Pragma(String pragmaName, String description, String[] choices) { this.pragmaName = pragmaName; this.description = description; this.choices = choices; } public String getPragmaName() { return pragmaName; } } private static DriverPropertyInfo[] driverPropertyInfo() { return Arrays.stream(Pragma.values()) .map( p -> { DriverPropertyInfo info = new DriverPropertyInfo(p.pragmaName, null); info.description = p.description; info.choices = p.choices; info.required = false; return info; }) .toArray(DriverPropertyInfo[]::new); } } ================================================ FILE: bindings/java/src/main/java/tech/turso/TursoDataSource.java ================================================ package tech.turso; import java.io.PrintWriter; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.Properties; import java.util.logging.Logger; import javax.sql.DataSource; import tech.turso.annotations.Nullable; import tech.turso.annotations.SkipNullableCheck; /** Provides {@link DataSource} API for configuring Turso database connection. */ public final class TursoDataSource implements DataSource { private final TursoConfig tursoConfig; private final String url; /** * Creates a datasource based on the provided configuration. * * @param tursoConfig The configuration for the datasource. */ public TursoDataSource(TursoConfig tursoConfig, String url) { this.tursoConfig = tursoConfig; this.url = url; } @Override @Nullable public Connection getConnection() throws SQLException { return getConnection(null, null); } @Override @Nullable public Connection getConnection(@Nullable String username, @Nullable String password) throws SQLException { Properties properties = tursoConfig.toProperties(); if (username != null) properties.put("user", username); if (password != null) properties.put("pass", password); return JDBC.createConnection(url, properties); } @Override @SkipNullableCheck public PrintWriter getLogWriter() throws SQLException { // TODO return null; } @Override public void setLogWriter(PrintWriter out) throws SQLException { // TODO } @Override public void setLoginTimeout(int seconds) throws SQLException { // TODO } @Override public int getLoginTimeout() throws SQLException { // TODO return 0; } @Override @SkipNullableCheck public Logger getParentLogger() throws SQLFeatureNotSupportedException { // TODO return null; } @Override @SkipNullableCheck public T unwrap(Class iface) throws SQLException { // TODO return null; } @Override public boolean isWrapperFor(Class iface) throws SQLException { // TODO return false; } } ================================================ FILE: bindings/java/src/main/java/tech/turso/TursoErrorCode.java ================================================ package tech.turso; import tech.turso.core.SqliteCode; /** Turso error code. Superset of SQLite3 error code. */ public enum TursoErrorCode { SQLITE_OK(SqliteCode.SQLITE_OK, "Successful result"), SQLITE_ERROR(SqliteCode.SQLITE_ERROR, "SQL error or missing database"), SQLITE_INTERNAL(SqliteCode.SQLITE_INTERNAL, "An internal logic error in SQLite"), SQLITE_PERM(SqliteCode.SQLITE_PERM, "Access permission denied"), SQLITE_ABORT(SqliteCode.SQLITE_ABORT, "Callback routine requested an abort"), SQLITE_BUSY(SqliteCode.SQLITE_BUSY, "The database file is locked"), SQLITE_LOCKED(SqliteCode.SQLITE_LOCKED, "A table in the database is locked"), SQLITE_NOMEM(SqliteCode.SQLITE_NOMEM, "A malloc() failed"), SQLITE_READONLY(SqliteCode.SQLITE_READONLY, "Attempt to write a readonly database"), SQLITE_INTERRUPT(SqliteCode.SQLITE_INTERRUPT, "Operation terminated by sqlite_interrupt()"), SQLITE_IOERR(SqliteCode.SQLITE_IOERR, "Some kind of disk I/O error occurred"), SQLITE_CORRUPT(SqliteCode.SQLITE_CORRUPT, "The database disk image is malformed"), SQLITE_NOTFOUND(SqliteCode.SQLITE_NOTFOUND, "(Internal Only) Table or record not found"), SQLITE_FULL(SqliteCode.SQLITE_FULL, "Insertion failed because database is full"), SQLITE_CANTOPEN(SqliteCode.SQLITE_CANTOPEN, "Unable to open the database file"), SQLITE_PROTOCOL(SqliteCode.SQLITE_PROTOCOL, "Database lock protocol error"), SQLITE_EMPTY(SqliteCode.SQLITE_EMPTY, "(Internal Only) Database table is empty"), SQLITE_SCHEMA(SqliteCode.SQLITE_SCHEMA, "The database schema changed"), SQLITE_TOOBIG(SqliteCode.SQLITE_TOOBIG, "Too much data for one row of a table"), SQLITE_CONSTRAINT(SqliteCode.SQLITE_CONSTRAINT, "Abort due to constraint violation"), SQLITE_MISMATCH(SqliteCode.SQLITE_MISMATCH, "Data type mismatch"), SQLITE_MISUSE(SqliteCode.SQLITE_MISUSE, "Library used incorrectly"), SQLITE_NOLFS(SqliteCode.SQLITE_NOLFS, "Uses OS features not supported on host"), SQLITE_AUTH(SqliteCode.SQLITE_AUTH, "Authorization denied"), SQLITE_ROW(SqliteCode.SQLITE_ROW, "sqlite_step() has another row ready"), SQLITE_DONE(SqliteCode.SQLITE_DONE, "sqlite_step() has finished executing"), SQLITE_INTEGER(SqliteCode.SQLITE_INTEGER, "Integer type"), SQLITE_FLOAT(SqliteCode.SQLITE_FLOAT, "Float type"), SQLITE_TEXT(SqliteCode.SQLITE_TEXT, "Text type"), SQLITE_BLOB(SqliteCode.SQLITE_BLOB, "Blob type"), SQLITE_NULL(SqliteCode.SQLITE_NULL, "Null type"), UNKNOWN_ERROR(-1, "Unknown error"), TURSO_FAILED_TO_PARSE_BYTE_ARRAY(1100, "Failed to parse ut8 byte array"), TURSO_FAILED_TO_PREPARE_STATEMENT(1200, "Failed to prepare statement"), TURSO_ETC(9999, "Unclassified error"); public final int code; public final String message; /** * @param code Error code * @param message Message for the error. */ TursoErrorCode(int code, String message) { this.code = code; this.message = message; } public static TursoErrorCode getErrorCode(int errorCode) { for (TursoErrorCode tursoErrorCode : TursoErrorCode.values()) { if (errorCode == tursoErrorCode.code) return tursoErrorCode; } return UNKNOWN_ERROR; } @Override public String toString() { return ("tursoErrorCode{" + "code=" + code + ", message='" + message + '\'' + '}'); } } ================================================ FILE: bindings/java/src/main/java/tech/turso/annotations/NativeInvocation.java ================================================ package tech.turso.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Annotation to mark methods that are called by native functions. For example, throwing exceptions * or creating java objects. */ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) public @interface NativeInvocation { String invokedFrom() default ""; } ================================================ FILE: bindings/java/src/main/java/tech/turso/annotations/Nullable.java ================================================ package tech.turso.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Annotation to mark nullable types. * *

This annotation is used to indicate that a method, field, or parameter can be null. It helps * in identifying potential nullability issues and improving code quality. */ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) public @interface Nullable {} ================================================ FILE: bindings/java/src/main/java/tech/turso/annotations/SkipNullableCheck.java ================================================ package tech.turso.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Marker annotation to skip nullable checks. * *

This annotation is used to mark methods, fields, or parameters that should be excluded from * nullable checks. It is typically applied to code that is still under development or requires * special handling. */ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) public @interface SkipNullableCheck {} ================================================ FILE: bindings/java/src/main/java/tech/turso/annotations/VisibleForTesting.java ================================================ package tech.turso.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** Annotation to mark methods that use larger visibility for testing purposes. */ @Retention(RetentionPolicy.SOURCE) @Target(ElementType.METHOD) public @interface VisibleForTesting {} ================================================ FILE: bindings/java/src/main/java/tech/turso/core/SqliteCode.java ================================================ /* * Copyright (c) 2007 David Crawshaw * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package tech.turso.core; /** Sqlite error codes. */ public final class SqliteCode { /** Successful result */ public static final int SQLITE_OK = 0; /** SQL error or missing database */ public static final int SQLITE_ERROR = 1; /** An internal logic error in SQLite */ public static final int SQLITE_INTERNAL = 2; /** Access permission denied */ public static final int SQLITE_PERM = 3; /** Callback routine requested an abort */ public static final int SQLITE_ABORT = 4; /** The database file is locked */ public static final int SQLITE_BUSY = 5; /** A table in the database is locked */ public static final int SQLITE_LOCKED = 6; /** A malloc() failed */ public static final int SQLITE_NOMEM = 7; /** Attempt to write a readonly database */ public static final int SQLITE_READONLY = 8; /** Operation terminated by sqlite_interrupt() */ public static final int SQLITE_INTERRUPT = 9; /** Some kind of disk I/O error occurred */ public static final int SQLITE_IOERR = 10; /** The database disk image is malformed */ public static final int SQLITE_CORRUPT = 11; /** (Internal Only) Table or record not found */ public static final int SQLITE_NOTFOUND = 12; /** Insertion failed because database is full */ public static final int SQLITE_FULL = 13; /** Unable to open the database file */ public static final int SQLITE_CANTOPEN = 14; /** Database lock protocol error */ public static final int SQLITE_PROTOCOL = 15; /** (Internal Only) Database table is empty */ public static final int SQLITE_EMPTY = 16; /** The database schema changed */ public static final int SQLITE_SCHEMA = 17; /** Too much data for one row of a table */ public static final int SQLITE_TOOBIG = 18; /** Abort due to constraint violation */ public static final int SQLITE_CONSTRAINT = 19; /** Data type mismatch */ public static final int SQLITE_MISMATCH = 20; /** Library used incorrectly */ public static final int SQLITE_MISUSE = 21; /** Uses OS features not supported on host */ public static final int SQLITE_NOLFS = 22; /** Authorization denied */ public static final int SQLITE_AUTH = 23; /** sqlite_step() has another row ready */ public static final int SQLITE_ROW = 100; /** sqlite_step() has finished executing */ public static final int SQLITE_DONE = 101; // types returned by sqlite3_column_type() public static final int SQLITE_INTEGER = 1; public static final int SQLITE_FLOAT = 2; public static final int SQLITE_TEXT = 3; public static final int SQLITE_BLOB = 4; public static final int SQLITE_NULL = 5; } ================================================ FILE: bindings/java/src/main/java/tech/turso/core/TursoConnection.java ================================================ package tech.turso.core; import static tech.turso.utils.ByteArrayUtils.stringToUtf8ByteArray; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Properties; import tech.turso.annotations.NativeInvocation; import tech.turso.utils.Logger; import tech.turso.utils.LoggerFactory; import tech.turso.utils.TursoExceptionUtils; public final class TursoConnection { private static final Logger logger = LoggerFactory.getLogger(TursoConnection.class); private final String url; private final long connectionPtr; private final TursoDB database; private boolean closed; // Transaction state fields private boolean autoCommit = true; private int transactionIsolation = Connection.TRANSACTION_SERIALIZABLE; private boolean inTransaction = false; private final Object transactionLock = new Object(); public TursoConnection(String url, String filePath) throws SQLException { this(url, filePath, new Properties()); } /** * Creates a connection to turso database * * @param url e.g. "jdbc:turso:fileName" * @param filePath path to file */ public TursoConnection(String url, String filePath, Properties properties) throws SQLException { this.url = url; this.database = open(url, filePath, properties); this.connectionPtr = this.database.connect(); } /** * Creates a connection using an existing TursoDB instance. This is useful for encrypted databases * created with TursoDB.createWithEncryption(). * * @param url e.g. "jdbc:turso:fileName" * @param database an existing TursoDB instance */ public TursoConnection(String url, TursoDB database) throws SQLException { this.url = url; this.database = database; this.connectionPtr = this.database.connect(); } private static TursoDB open(String url, String filePath, Properties properties) throws SQLException { return TursoDB.create(url, filePath); } public void checkOpen() throws SQLException { if (isClosed()) throw new SQLException("database connection closed"); } public String getUrl() { return url; } public void close() throws SQLException { if (isClosed()) { return; } // Roll back any pending transaction before closing synchronized (transactionLock) { if (inTransaction) { try { executeInternal("ROLLBACK"); } catch (SQLException e) { // Log but don't throw - we're closing anyway logger.warn("Failed to rollback transaction during close", e); } finally { inTransaction = false; } } } this._close(this.connectionPtr); this.closed = true; } private native void _close(long connectionPtr); private native boolean _getAutoCommit(long connectionPtr); public boolean isClosed() throws SQLException { return closed; } public TursoDB getDatabase() { return database; } /** * Compiles an SQL statement. * * @param sql An SQL statement. * @return Pointer to statement. * @throws SQLException if a database access error occurs. */ public TursoStatement prepare(String sql) throws SQLException { return prepare(sql, true); } /** * Compiles an SQL statement with optional transaction start check. * * @param sql An SQL statement. * @param checkTransaction Whether to check and start transaction if needed. * @return Pointer to statement. * @throws SQLException if a database access error occurs. */ private TursoStatement prepare(String sql, boolean checkTransaction) throws SQLException { logger.trace("DriverManager [{}] [SQLite EXEC] {}", Thread.currentThread().getName(), sql); // Ensure transaction is started if needed (lazy transaction start) if (checkTransaction) { ensureTransactionStarted(sql); } byte[] sqlBytes = stringToUtf8ByteArray(sql); if (sqlBytes == null) { throw new SQLException("Failed to convert " + sql + " into bytes"); } return new TursoStatement(sql, prepareUtf8(connectionPtr, sqlBytes)); } private native long prepareUtf8(long connectionPtr, byte[] sqlUtf8) throws SQLException; // TODO: check whether this is still valid for turso /** * Checks whether the type, concurrency, and holdability settings for a {@link ResultSet} are * supported by the SQLite interface. Supported settings are: * *

    *
  • type: {@link ResultSet#TYPE_FORWARD_ONLY} *
  • concurrency: {@link ResultSet#CONCUR_READ_ONLY}) *
  • holdability: {@link ResultSet#CLOSE_CURSORS_AT_COMMIT} *
* * @param resultSetType the type setting. * @param resultSetConcurrency the concurrency setting. * @param resultSetHoldability the holdability setting. */ public void checkCursor(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { if (resultSetType != ResultSet.TYPE_FORWARD_ONLY) { throw new SQLException("SQLite only supports TYPE_FORWARD_ONLY cursors"); } if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY) { throw new SQLException("SQLite only supports CONCUR_READ_ONLY cursors"); } if (resultSetHoldability != ResultSet.CLOSE_CURSORS_AT_COMMIT) { throw new SQLException("SQLite only supports closing cursors at commit"); } } /** * Sets the auto-commit mode for this connection. * *

When auto-commit is enabled (the default), each SQL statement is committed automatically * upon completion. When auto-commit is disabled, statements are grouped into transactions that * must be explicitly committed or rolled back. * *

If this method is called to enable auto-commit while a transaction is active, the current * transaction is committed first. * * @param autoCommit true to enable auto-commit mode; false to disable it * @throws SQLException if a database access error occurs or the connection is closed */ public void setAutoCommit(boolean autoCommit) throws SQLException { synchronized (transactionLock) { checkOpen(); if (this.autoCommit == autoCommit) { return; // No-op if already in desired mode } // If enabling autocommit and there's a pending transaction, commit it if (autoCommit && inTransaction) { commit(); } this.autoCommit = autoCommit; } } /** * Gets the current auto-commit mode for this connection. * * @return true if auto-commit mode is enabled; false otherwise * @throws SQLException if a database access error occurs or the connection is closed */ public boolean getAutoCommit() throws SQLException { checkOpen(); return autoCommit; } /** * Commits the current transaction. * * @throws SQLException if in auto-commit mode, closed, or database error occurs. */ public void commit() throws SQLException { synchronized (transactionLock) { checkOpen(); if (autoCommit) { throw new SQLException("Cannot commit in autocommit mode."); } if (inTransaction) { executeInternal("COMMIT"); inTransaction = false; } } } /** * Rolls back the current transaction. * * @throws SQLException if in auto-commit mode, closed, or database error occurs. */ public void rollback() throws SQLException { synchronized (transactionLock) { checkOpen(); if (autoCommit) { throw new SQLException("Cannot rollback in autocommit mode."); } if (inTransaction) { executeInternal("ROLLBACK"); inTransaction = false; } } } /** * Lazy transaction starter. Starts a transaction if one isn't active, auto-commit is disabled, * and the statement isn't a control command. */ private void ensureTransactionStarted(String sql) throws SQLException { if (autoCommit || inTransaction) return; // Avoid recursive start for control statements String trimmed = sql.trim().toUpperCase(); if (trimmed.startsWith("BEGIN") || trimmed.startsWith("COMMIT") || trimmed.startsWith("ROLLBACK")) { return; } String beginMode = TursoTransactionMode.fromIsolationLevel(transactionIsolation).getSql(); executeInternal(beginMode); inTransaction = true; } /** * Internal helper to execute transaction control statements without triggering recursive * transaction checks. */ private void executeInternal(String sql) throws SQLException { try (TursoStatement stmt = prepare(sql, false)) { stmt.execute(); } } /** * Sets the transaction isolation level. * * @param level one of the following {@code Connection} constants: {@code * Connection.TRANSACTION_READ_UNCOMMITTED}, {@code Connection.TRANSACTION_READ_COMMITTED}, * {@code Connection.TRANSACTION_REPEATABLE_READ}, or {@code * Connection.TRANSACTION_SERIALIZABLE}. * @throws SQLException if a database access error occurs, this method is called on a closed * connection or the given parameter is not one of the {@code Connection} constants */ public void setTransactionIsolation(int level) throws SQLException { synchronized (transactionLock) { checkOpen(); if (inTransaction) { throw new SQLException("Cannot change isolation level while transaction is active."); } if (level != Connection.TRANSACTION_READ_UNCOMMITTED && level != Connection.TRANSACTION_READ_COMMITTED && level != Connection.TRANSACTION_REPEATABLE_READ && level != Connection.TRANSACTION_SERIALIZABLE) { throw new SQLException("Invalid transaction isolation level: " + level); } this.transactionIsolation = level; } } /** * Retrieves the current transaction isolation level. * * @return the current transaction isolation level * @throws SQLException if a database access error occurs or the connection is closed */ public int getTransactionIsolation() throws SQLException { checkOpen(); return transactionIsolation; } /** * Throws formatted SQLException with error code and message. * * @param errorCode Error code. * @param errorMessageBytes Error message. */ @NativeInvocation(invokedFrom = "turso_connection.rs") private void throwTursoException(int errorCode, byte[] errorMessageBytes) throws SQLException { TursoExceptionUtils.throwTursoException(errorCode, errorMessageBytes); } } ================================================ FILE: bindings/java/src/main/java/tech/turso/core/TursoDB.java ================================================ package tech.turso.core; import static tech.turso.utils.ByteArrayUtils.stringToUtf8ByteArray; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; import tech.turso.TursoErrorCode; import tech.turso.annotations.NativeInvocation; import tech.turso.annotations.Nullable; import tech.turso.annotations.VisibleForTesting; import tech.turso.utils.Logger; import tech.turso.utils.LoggerFactory; import tech.turso.utils.TursoExceptionUtils; /** This class provides a thin JNI layer over the SQLite3 C API. */ public final class TursoDB implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(TursoDB.class); // Pointer to database instance private long dbPointer; private boolean isOpen; private final String url; private final String filePath; static { if ("The Android Project".equals(System.getProperty("java.vm.vendor"))) { // TODO } else { // continue with non Android execution path } } /** * Enum representing different architectures and their corresponding library paths and file * extensions. */ private enum Architecture { MACOS_ARM64("libs/macos_arm64/lib_turso_java.dylib", ".dylib"), MACOS_X86("libs/macos_x86/lib_turso_java.dylib", ".dylib"), LINUX_X86("libs/linux_x86/lib_turso_java.so", ".so"), WINDOWS("libs/windows/lib_turso_java.dll", ".dll"), UNSUPPORTED("", ""); private final String libPath; private final String fileExtension; Architecture(String libPath, String fileExtension) { this.libPath = libPath; this.fileExtension = fileExtension; } public String getLibPath() { return libPath; } public String getFileExtension() { return fileExtension; } public static Architecture detect() { String osName = System.getProperty("os.name").toLowerCase(); String osArch = System.getProperty("os.arch").toLowerCase(); // TODO: add support for arm64 on Linux if (osName.contains("linux")) { if (osArch.contains("aarch64") || osArch.contains("arm64")) { throw new UnsupportedOperationException( "ARM64 architecture is not supported on Linux yet"); } else if (osArch.contains("x86_64") || osArch.contains("amd64")) { return LINUX_X86; } } if (osName.contains("mac")) { if (osArch.contains("aarch64") || osArch.contains("arm64")) { return MACOS_ARM64; } else if (osArch.contains("x86_64") || osArch.contains("amd64")) { return MACOS_X86; } } else if (osName.contains("win")) { return WINDOWS; } return UNSUPPORTED; } } /** * This method attempts to load the native library required for turso operations. It first tries * to load the library from the system's library path using {@link #loadFromSystemPath()}. If that * fails, it attempts to load the library from the JAR file using {@link #loadFromJar()}. If * either method succeeds, the `isLoaded` flag is set to true. If both methods fail, an {@link * InternalError} is thrown indicating that the necessary native library could not be loaded. * * @throws InternalError if the native library cannot be loaded from either the system path or the * JAR file. */ private static void load() { new SingletonHolder(); } // "lazy initialization holder class idiom" (Effective Java #83) private static class SingletonHolder { static { if (!loadFromSystemPath() && !loadFromJar()) { throw new InternalError("Unable to load necessary native library"); } } } /** * Load the native library from the system path. * *

This method attempts to load the native library named "_turso_java" from the system's * library path. If the library is successfully loaded, the `isLoaded` flag is set to true. * * @return true if the library was successfully loaded, false otherwise. */ private static boolean loadFromSystemPath() { try { System.loadLibrary("_turso_java"); return true; } catch (Throwable t) { logger.info("Unable to load from default path: {}", String.valueOf(t)); } return false; } /** * Load the native library from the JAR file. * *

By default, native libraries are packaged within the JAR file. This method extracts the * appropriate native library for the current operating system and architecture from the JAR and * loads it. * * @return true if the library was successfully loaded, false otherwise. */ private static boolean loadFromJar() { Architecture arch = Architecture.detect(); if (arch == Architecture.UNSUPPORTED) { logger.info("Unsupported OS or architecture"); return false; } try { InputStream is = TursoDB.class.getClassLoader().getResourceAsStream(arch.getLibPath()); assert is != null; File file = convertInputStreamToFile(is, arch); System.load(file.getPath()); return true; } catch (Throwable t) { logger.info("Unable to load from jar: {}", String.valueOf(t)); } return false; } private static File convertInputStreamToFile(InputStream is, Architecture arch) throws IOException { File tempFile = File.createTempFile("lib", arch.getFileExtension()); tempFile.deleteOnExit(); try (FileOutputStream os = new FileOutputStream(tempFile)) { int read; byte[] bytes = new byte[1024]; while ((read = is.read(bytes)) != -1) { os.write(bytes, 0, read); } } return tempFile; } /** * @param url eTurso.gTursoTurso. "jdbc:turso:fileName * @param filePath e.g. path to file */ public static TursoDB create(String url, String filePath) throws SQLException { return new TursoDB(url, filePath, null, null); } /** * Creates a TursoDB instance with encryption support. * * @param url e.g. "jdbc:turso:fileName" * @param filePath e.g. path to file * @param cipher the encryption cipher to use * @param hexkey the hex-encoded encryption key */ public static TursoDB createWithEncryption( String url, String filePath, TursoEncryptionCipher cipher, String hexkey) throws SQLException { return new TursoDB(url, filePath, cipher, hexkey); } // TODO: receive config as argument private TursoDB( String url, String filePath, @Nullable TursoEncryptionCipher cipher, @Nullable String hexkey) throws SQLException { this.url = url; this.filePath = filePath; load(); open(0, cipher != null ? cipher.getValue() : null, hexkey); } // TODO: add support for JNI public native void interrupt(); public boolean isClosed() { return !this.isOpen; } public boolean isOpen() { return this.isOpen; } private void open(int openFlags, @Nullable String cipher, @Nullable String hexkey) throws SQLException { open0(filePath, openFlags, cipher, hexkey); } private void open0( String filePath, int openFlags, @Nullable String cipher, @Nullable String hexkey) throws SQLException { byte[] filePathBytes = stringToUtf8ByteArray(filePath); if (filePathBytes == null) { throw TursoExceptionUtils.buildTursoException( TursoErrorCode.TURSO_ETC.code, "File path cannot be converted to byteArray. File name: " + filePath); } if (cipher != null && hexkey != null) { dbPointer = openWithEncryptionUtf8(filePathBytes, openFlags, cipher, hexkey); } else { dbPointer = openUtf8(filePathBytes, openFlags); } isOpen = true; } private native long openUtf8(byte[] file, int openFlags) throws SQLException; private native long openWithEncryptionUtf8( byte[] file, int openFlags, String cipher, String hexkey) throws SQLException; public long connect() throws SQLException { return connect0(dbPointer); } private native long connect0(long databasePtr) throws SQLException; @Override public void close() throws Exception { if (!isOpen) return; close0(dbPointer); isOpen = false; } private native void close0(long databasePtr) throws SQLException; @VisibleForTesting native void throwJavaException(int errorCode) throws SQLException; /** * Throws formatted SQLException with error code and message. * * @param errorCode Error code. * @param errorMessageBytes Error message. */ @NativeInvocation(invokedFrom = "turso_db.rs") private void throwTursoException(int errorCode, byte[] errorMessageBytes) throws SQLException { TursoExceptionUtils.throwTursoException(errorCode, errorMessageBytes); } } ================================================ FILE: bindings/java/src/main/java/tech/turso/core/TursoEncryptionCipher.java ================================================ package tech.turso.core; /** Supported encryption ciphers for local database encryption. */ public enum TursoEncryptionCipher { /** AES-128-GCM cipher */ AES_128_GCM("aes128gcm"), /** AES-256-GCM cipher */ AES_256_GCM("aes256gcm"), /** AEGIS-256 cipher */ AEGIS_256("aegis256"), /** AEGIS-256X2 cipher */ AEGIS_256X2("aegis256x2"), /** AEGIS-128L cipher */ AEGIS_128L("aegis128l"), /** AEGIS-128X2 cipher */ AEGIS_128X2("aegis128x2"), /** AEGIS-128X4 cipher */ AEGIS_128X4("aegis128x4"); private final String value; TursoEncryptionCipher(String value) { this.value = value; } /** Returns the string value of the cipher for internal use. */ public String getValue() { return value; } } ================================================ FILE: bindings/java/src/main/java/tech/turso/core/TursoPropertiesHolder.java ================================================ package tech.turso.core; import java.io.IOException; import java.io.InputStream; import java.util.Properties; import tech.turso.jdbc4.JDBC4DatabaseMetaData; import tech.turso.utils.Logger; import tech.turso.utils.LoggerFactory; public class TursoPropertiesHolder { private static final Logger logger = LoggerFactory.getLogger(TursoPropertiesHolder.class); private static String driverName = ""; private static String driverVersion = ""; static { try (InputStream tursoJdbcPropStream = JDBC4DatabaseMetaData.class .getClassLoader() .getResourceAsStream("turso-jdbc.properties"); ) { if (tursoJdbcPropStream == null) { throw new IOException("Cannot load turso-jdbc.properties from jar"); } final Properties properties = new Properties(); properties.load(tursoJdbcPropStream); driverName = properties.getProperty("driverName"); driverVersion = properties.getProperty("driverVersion"); } catch (IOException e) { logger.error("Failed to load driverName and driverVersion"); } } public static String getDriverName() { return driverName; } public static String getDriverVersion() { return driverVersion; } } ================================================ FILE: bindings/java/src/main/java/tech/turso/core/TursoResultSet.java ================================================ package tech.turso.core; import java.sql.ResultSet; import java.sql.SQLException; import tech.turso.annotations.Nullable; import tech.turso.utils.Logger; import tech.turso.utils.LoggerFactory; /** * A table of data representing turso database result set, which is generated by executing a * statement that queries the database. * *

A {@link TursoResultSet} object is automatically closed when the {@link TursoStatement} object * that generated it is closed or re-executed. */ public final class TursoResultSet { private static final Logger log = LoggerFactory.getLogger(TursoResultSet.class); private final TursoStatement statement; // Name of the columns private String[] columnNames = new String[0]; // Whether the result set does not have any rows. private boolean isEmptyResultSet = false; // If the result set is open. Doesn't mean it has results. private boolean open; // Maximum number of rows as set by the statement private long maxRows; // number of current row, starts at 1 (0 is used to represent loading data) private int row = 0; private boolean pastLastRow = false; @Nullable private TursoStepResult lastStepResult; public static TursoResultSet of(TursoStatement statement) { return new TursoResultSet(statement); } private TursoResultSet(TursoStatement statement) { this.open = true; this.statement = statement; } /** * Consumes all the rows in this {@link ResultSet} until the {@link #next()} method returns * `false`. * * @throws SQLException if the result set is not open or if an error occurs while iterating. */ public void consumeAll() throws SQLException { if (!open) { throw new SQLException("The result set is not open"); } while (next()) {} } /** * Moves the cursor forward one row from its current position. A {@link TursoResultSet} cursor is * initially positioned before the first fow; the first call to the method next makes * the first row the current row; the second call makes the second row the current row, and so on. * When a call to the next method returns false, the cursor is * positioned after the last row. * *

Note that turso only supports ResultSet.TYPE_FORWARD_ONLY, which means that the * cursor can only move forward. * * @return true if the new current row is valid; false if there are no more rows * @throws SQLException if a database access error occurs */ public boolean next() throws SQLException { if (!open) { throw new SQLException("The resultSet is not open"); } if (isEmptyResultSet || pastLastRow) { return false; // completed ResultSet } if (maxRows != 0 && row == maxRows) { return false; } lastStepResult = this.statement.step(); log.debug("lastStepResult: {}", lastStepResult); if (lastStepResult.isRow()) { row++; } if (lastStepResult.isInInvalidState()) { open = false; String errorMessage = lastStepResult.getErrorMessage(); if (errorMessage != null && !errorMessage.isEmpty()) { throw new SQLException("step() returned invalid result: " + errorMessage); } else { throw new SQLException("step() returned invalid result: " + lastStepResult); } } pastLastRow = lastStepResult.isDone(); if (pastLastRow && row == 0) { isEmptyResultSet = true; } return !pastLastRow; } /** Checks whether the last step result has returned row result. */ public boolean hasLastStepReturnedRow() { return lastStepResult != null && lastStepResult.isRow(); } /** Checks whether the cursor is positioned after the last row. */ public boolean isPastLastRow() { return pastLastRow; } /** Checks whether the result set is empty (has no rows). */ public boolean isEmpty() { return isEmptyResultSet; } /** Gets the current row number (0-based, 0 means before first row). */ public int getRow() { return row; } /** * Checks the status of the result set. * * @return true if it's ready to iterate over the result set; false otherwise. */ public boolean isOpen() { return open; } /** @throws SQLException if not {@link #open} */ public void checkOpen() throws SQLException { if (!open) { throw new SQLException("ResultSet closed"); } } public void close() throws SQLException { this.open = false; } public Object get(String columnName) throws SQLException { final int columnsLength = this.columnNames.length; for (int i = 0; i < columnsLength; i++) { if (this.columnNames[i].equals(columnName)) { return get(i + 1); } } throw new SQLException("column name " + columnName + " not found"); } public Object get(int columnIndex) throws SQLException { if (!this.isOpen()) { throw new SQLException("ResultSet is not open"); } if (this.lastStepResult == null || this.lastStepResult.getResult() == null) { throw new SQLException("ResultSet is null"); } final Object[] resultSet = this.lastStepResult.getResult(); if (columnIndex > resultSet.length || columnIndex < 0) { throw new SQLException("columnIndex out of bound"); } return resultSet[columnIndex - 1]; } public String[] getColumnNames() { return this.columnNames; } public void setColumnNames(String[] columnNames) { this.columnNames = columnNames; } @Override public String toString() { return ("tursoResultSet{" + "statement=" + statement + ", isEmptyResultSet=" + isEmptyResultSet + ", open=" + open + ", maxRows=" + maxRows + ", row=" + row + ", pastLastRow=" + pastLastRow + ", lastResult=" + lastStepResult + '}'); } } ================================================ FILE: bindings/java/src/main/java/tech/turso/core/TursoStatement.java ================================================ package tech.turso.core; import java.sql.SQLException; import tech.turso.annotations.NativeInvocation; import tech.turso.annotations.Nullable; import tech.turso.utils.Logger; import tech.turso.utils.LoggerFactory; import tech.turso.utils.TursoExceptionUtils; /** * By default, only one resultSet object per TursoStatement can be open at * the same time. Therefore, if the reading of one resultSet object is interleaved with * the reading of another, each must have been generated by different TursoStatement * objects. All execution method in the TursoStatement implicitly close the current * resultSet object of the statement if an open one exists. */ public final class TursoStatement implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(TursoStatement.class); private final String sql; private final long statementPointer; private TursoResultSet resultSet; private boolean closed; // TODO: what if the statement we ran was DDL, update queries and etc. Should we still create a // resultSet? public TursoStatement(String sql, long statementPointer) { this.sql = sql; this.statementPointer = statementPointer; this.resultSet = TursoResultSet.of(this); log.debug("Creating statement with sql: {}", this.sql); } public TursoResultSet getResultSet() { return resultSet; } /** * Expects a clean statement created right after prepare method is called. * * @return true if the ResultSet has at least one row; false otherwise. */ public boolean execute() throws SQLException { resultSet.next(); return resultSet.hasLastStepReturnedRow(); } TursoStepResult step() throws SQLException { final TursoStepResult result = step(this.statementPointer); if (result == null) { throw new SQLException("step() returned null, which is only returned when an error occurs"); } return result; } /** * Because turso supports async I/O, it is possible to return a {@link TursoStepResult} with * {@link TursoStepResult#STEP_RESULT_ID_ROW}. However, this is handled by the native side, so you * can expect that this method will not return a {@link TursoStepResult#STEP_RESULT_ID_ROW}. */ @Nullable private native TursoStepResult step(long stmtPointer) throws SQLException; /** * Throws formatted SQLException with error code and message. * * @param errorCode Error code. * @param errorMessageBytes Error message. */ @NativeInvocation(invokedFrom = "turso_statement.rs") private void throwTursoException(int errorCode, byte[] errorMessageBytes) throws SQLException { TursoExceptionUtils.throwTursoException(errorCode, errorMessageBytes); } /** * Closes the current statement and releases any resources associated with it. This method calls * the native `_close` method to perform the actual closing operation. */ public void close() throws SQLException { if (closed) { return; } this.resultSet.close(); _close(statementPointer); closed = true; } private native void _close(long statementPointer); /** * Initializes the column metadata, such as the names of the columns. Since {@link TursoStatement} * can only have a single {@link TursoResultSet}, it is appropriate to place the initialization of * column metadata here. * * @throws SQLException if a database access error occurs while retrieving column names */ public void initializeColumnMetadata() throws SQLException { final String[] columnNames = this.columns(statementPointer); if (columnNames != null) { this.resultSet.setColumnNames(columnNames); } } @Nullable private native String[] columns(long statementPointer) throws SQLException; /** * Binds a NULL value to the prepared statement at the specified position. * * @param position The index of the SQL parameter to be set to NULL. * @return Result Codes * @throws SQLException If a database access error occurs. */ public int bindNull(int position) throws SQLException { final int result = bindNull(statementPointer, position); if (result != 0) { throw new SQLException("Exception while binding NULL value at position " + position); } return result; } private native int bindNull(long statementPointer, int position) throws SQLException; /** * Binds an integer value to the prepared statement at the specified position. This function calls * bindLong because turso treats all integers as long (as well as SQLite). * *

According to SQLite documentation, the value is a signed integer, stored in 0, 1, 2, 3, 4, * 6, or 8 bytes depending on the magnitude of the value. * * @param position The index of the SQL parameter to be set. * @param value The integer value to bind to the parameter. * @return A result code indicating the success or failure of the operation. * @throws SQLException If a database access error occurs. */ public int bindInt(int position, int value) throws SQLException { return bindLong(position, value); } /** * Binds a long value to the prepared statement at the specified position. * * @param position The index of the SQL parameter to be set. * @param value The value to bind to the parameter. * @return Result Codes * @throws SQLException If a database access error occurs. */ public int bindLong(int position, long value) throws SQLException { final int result = bindLong(statementPointer, position, value); if (result != 0) { throw new SQLException("Exception while binding long value at position " + position); } return result; } private native int bindLong(long statementPointer, int position, long value) throws SQLException; /** * Binds a double value to the prepared statement at the specified position. * * @param position The index of the SQL parameter to be set. * @param value The value to bind to the parameter. * @return Result Codes * @throws SQLException If a database access error occurs. */ public int bindDouble(int position, double value) throws SQLException { final int result = bindDouble(statementPointer, position, value); if (result != 0) { throw new SQLException("Exception while binding double value at position " + position); } return result; } private native int bindDouble(long statementPointer, int position, double value) throws SQLException; /** * Binds a text value to the prepared statement at the specified position. * * @param position The index of the SQL parameter to be set. * @param value The value to bind to the parameter. * @return Result Codes * @throws SQLException If a database access error occurs. */ public int bindText(int position, String value) throws SQLException { final int result = bindText(statementPointer, position, value); if (result != 0) { throw new SQLException("Exception while binding text value at position " + position); } return result; } private native int bindText(long statementPointer, int position, String value) throws SQLException; /** * Binds a blob value to the prepared statement at the specified position. * * @param position The index of the SQL parameter to be set. * @param value The value to bind to the parameter. * @return Result Codes * @throws SQLException If a database access error occurs. */ public int bindBlob(int position, byte[] value) throws SQLException { final int result = bindBlob(statementPointer, position, value); if (result != 0) { throw new SQLException("Exception while binding blob value at position " + position); } return result; } private native int bindBlob(long statementPointer, int position, byte[] value) throws SQLException; public void bindObject(int parameterIndex, Object x) throws SQLException { if (x == null) { this.bindNull(parameterIndex); return; } if (x instanceof Byte) { this.bindInt(parameterIndex, (Byte) x); } else if (x instanceof Short) { this.bindInt(parameterIndex, (Short) x); } else if (x instanceof Integer) { this.bindInt(parameterIndex, (Integer) x); } else if (x instanceof Long) { this.bindLong(parameterIndex, (Long) x); } else if (x instanceof String) { bindText(parameterIndex, (String) x); } else if (x instanceof Float) { bindDouble(parameterIndex, (Float) x); } else if (x instanceof Double) { bindDouble(parameterIndex, (Double) x); } else if (x instanceof byte[]) { bindBlob(parameterIndex, (byte[]) x); } else { throw new SQLException("Unsupported object type in bindObject: " + x.getClass().getName()); } } /** * Returns total number of changes. * * @throws SQLException If a database access error occurs */ public long totalChanges() throws SQLException { final long result = totalChanges(statementPointer); if (result == -1) { throw new SQLException("Exception while retrieving total number of changes"); } return result; } private native long totalChanges(long statementPointer) throws SQLException; /** * Returns number of changes. * * @throws SQLException If a database access error occurs */ public long changes() throws SQLException { final long result = changes(statementPointer); if (result == -1) { throw new SQLException("Exception while retrieving number of changes"); } return result; } private native long changes(long statementPointer) throws SQLException; /** * Returns the number of parameters in this statement. Parameters are the `?`'s that get replaced * by the provided arguments. * * @throws SQLException If a database access error occurs */ public int parameterCount() throws SQLException { final int result = parameterCount(statementPointer); if (result == -1) { throw new SQLException("Exception while retrieving parameter count"); } return result; } private native int parameterCount(long statementPointer) throws SQLException; /** Resets this statement so it's ready for re-execution */ public void reset() throws SQLException { final int result = reset(statementPointer); if (result == -1) { throw new SQLException("Exception while resetting statement"); } this.resultSet = TursoResultSet.of(this); } private native int reset(long statementPointer) throws SQLException; /** * Checks if the statement is closed. * * @return true if the statement is closed, false otherwise. */ public boolean isClosed() { return closed; } @Override public String toString() { return ("tursoStatement{" + "statementPointer=" + statementPointer + ", sql='" + sql + '\'' + '}'); } } ================================================ FILE: bindings/java/src/main/java/tech/turso/core/TursoStepResult.java ================================================ package tech.turso.core; import java.util.Arrays; import tech.turso.annotations.NativeInvocation; import tech.turso.annotations.Nullable; /** Represents the step result of turso's statement's step function. */ public final class TursoStepResult { private static final int STEP_RESULT_ID_ROW = 10; private static final int STEP_RESULT_ID_IO = 20; private static final int STEP_RESULT_ID_DONE = 30; private static final int STEP_RESULT_ID_INTERRUPT = 40; // Indicates that the database file could not be written because of concurrent activity by some // other connection private static final int STEP_RESULT_ID_BUSY = 50; private static final int STEP_RESULT_ID_ERROR = 60; // Identifier for Turso's StepResult private final int stepResultId; @Nullable private final Object[] result; @Nullable private final String errorMessage; @NativeInvocation(invokedFrom = "turso_statement.rs") public TursoStepResult(int stepResultId) { this.stepResultId = stepResultId; this.result = null; this.errorMessage = null; } @NativeInvocation(invokedFrom = "turso_statement.rs") public TursoStepResult(int stepResultId, Object[] result) { this.stepResultId = stepResultId; this.result = result; this.errorMessage = null; } @NativeInvocation(invokedFrom = "turso_statement.rs") public TursoStepResult(int stepResultId, String errorMessage) { this.stepResultId = stepResultId; this.result = null; this.errorMessage = errorMessage; } public boolean isRow() { return stepResultId == STEP_RESULT_ID_ROW; } public boolean isDone() { return stepResultId == STEP_RESULT_ID_DONE; } public boolean isInInvalidState() { // current implementation doesn't allow STEP_RESULT_ID_IO to be returned return (stepResultId == STEP_RESULT_ID_IO || stepResultId == STEP_RESULT_ID_INTERRUPT || stepResultId == STEP_RESULT_ID_BUSY || stepResultId == STEP_RESULT_ID_ERROR); } @Nullable public Object[] getResult() { return result; } @Nullable public String getErrorMessage() { return errorMessage; } @Override public String toString() { return ("tursoStepResult{" + "stepResultName=" + getStepResultName() + ", result=" + Arrays.toString(result) + '}'); } private String getStepResultName() { switch (stepResultId) { case STEP_RESULT_ID_ROW: return "ROW"; case STEP_RESULT_ID_IO: return "IO"; case STEP_RESULT_ID_DONE: return "DONE"; case STEP_RESULT_ID_INTERRUPT: return "INTERRUPT"; case STEP_RESULT_ID_BUSY: return "BUSY"; case STEP_RESULT_ID_ERROR: return "ERROR"; default: return "UNKNOWN"; } } } ================================================ FILE: bindings/java/src/main/java/tech/turso/core/TursoTransactionMode.java ================================================ package tech.turso.core; import java.sql.Connection; /** * Defines the transaction modes supported by Turso (SQLite). * *

Transactions can be DEFERRED, IMMEDIATE, or EXCLUSIVE. The default behavior is DEFERRED. */ public enum TursoTransactionMode { /** * The transaction does not actually start until the database is first accessed. * *

Internally, the {@code BEGIN DEFERRED} statement merely sets a flag on the database * connection that turns off the automatic commit that would normally occur when the last * statement finishes. * *

If the first statement after {@code BEGIN DEFERRED} is a {@code SELECT}, then a read * transaction is started. Subsequent write statements will upgrade the transaction to a write * transaction if possible, or return {@code SQLITE_BUSY}. This maximizes concurrency for * read-heavy workloads. */ DEFERRED("BEGIN DEFERRED"), /** * Causes the database connection to start a new write immediately, without waiting for a write * statement. * *

The {@code BEGIN IMMEDIATE} might fail with {@code SQLITE_BUSY} if another write transaction * is already active on another database connection. * *

This prevents other connections from writing to the database, effectively serializing write * transactions and ensuring a stable snapshot for the duration of this transaction. */ IMMEDIATE("BEGIN IMMEDIATE"), /** * Causes the database connection to start a new write immediately and prevents other database * connections from reading out of the database while the transaction is underway. */ EXCLUSIVE("BEGIN EXCLUSIVE"); private final String sql; TursoTransactionMode(String sql) { this.sql = sql; } public String getSql() { return sql; } /** * Determine the transaction mode based on the JDBC transaction isolation level. * *

* *

    *
  • {@link Connection#TRANSACTION_READ_UNCOMMITTED} -> {@link #DEFERRED} *
  • {@link Connection#TRANSACTION_READ_COMMITTED} -> {@link #DEFERRED} *
  • {@link Connection#TRANSACTION_REPEATABLE_READ} -> {@link #IMMEDIATE} *
  • {@link Connection#TRANSACTION_SERIALIZABLE} -> {@link #IMMEDIATE} *
* * @param level the JDBC transaction isolation level * @return the corresponding TursoTransactionMode */ public static TursoTransactionMode fromIsolationLevel(int level) { if (level == Connection.TRANSACTION_READ_UNCOMMITTED || level == Connection.TRANSACTION_READ_COMMITTED) { return DEFERRED; } return IMMEDIATE; } } ================================================ FILE: bindings/java/src/main/java/tech/turso/exceptions/TursoException.java ================================================ package tech.turso.exceptions; import java.sql.SQLException; import tech.turso.TursoErrorCode; public final class TursoException extends SQLException { private TursoErrorCode resultCode; public TursoException(String message, TursoErrorCode resultCode) { super(message, null, resultCode.code & 0xff); this.resultCode = resultCode; } public TursoErrorCode getResultCode() { return resultCode; } } ================================================ FILE: bindings/java/src/main/java/tech/turso/jdbc4/JDBC4Connection.java ================================================ package tech.turso.jdbc4; import java.sql.*; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.concurrent.Executor; import tech.turso.annotations.SkipNullableCheck; import tech.turso.core.TursoConnection; import tech.turso.core.TursoStatement; /** JDBC 4 Connection implementation for Turso databases. */ public final class JDBC4Connection implements Connection { private final TursoConnection connection; private Map> typeMap = new HashMap<>(); /** * Creates a new JDBC4 connection. * * @param url the database URL * @param filePath the database file path * @throws SQLException if a database access error occurs */ public JDBC4Connection(String url, String filePath) throws SQLException { this.connection = new TursoConnection(url, filePath); } /** * Creates a new JDBC4 connection with properties. * * @param url the database URL * @param filePath the database file path * @param properties connection properties * @throws SQLException if a database access error occurs */ public JDBC4Connection(String url, String filePath, Properties properties) throws SQLException { this.connection = new TursoConnection(url, filePath, properties); } /** * Prepares a SQL statement for execution. * * @param sql the SQL statement to prepare * @return the prepared statement * @throws SQLException if a database access error occurs */ public TursoStatement prepare(String sql) throws SQLException { final TursoStatement statement = connection.prepare(sql); statement.initializeColumnMetadata(); return statement; } @Override public Statement createStatement() throws SQLException { return createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); } @Override public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { return createStatement(resultSetType, resultSetConcurrency, ResultSet.CLOSE_CURSORS_AT_COMMIT); } @Override public Statement createStatement( int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { connection.checkOpen(); connection.checkCursor(resultSetType, resultSetConcurrency, resultSetHoldability); return new JDBC4Statement(this); } @Override public String nativeSQL(String sql) throws SQLException { return sql; } @Override public void setAutoCommit(boolean autoCommit) throws SQLException { connection.setAutoCommit(autoCommit); } @Override public boolean getAutoCommit() throws SQLException { return connection.getAutoCommit(); } @Override public void commit() throws SQLException { connection.commit(); } @Override public void rollback() throws SQLException { connection.rollback(); } @Override public void close() throws SQLException { connection.close(); } @Override public boolean isClosed() throws SQLException { return connection.isClosed(); } @Override @SkipNullableCheck public DatabaseMetaData getMetaData() throws SQLException { return new JDBC4DatabaseMetaData(this); } @Override public void setReadOnly(boolean readOnly) throws SQLException { // TODO } @Override public boolean isReadOnly() throws SQLException { // TODO return false; } @Override public void setCatalog(String catalog) throws SQLException {} @Override public String getCatalog() throws SQLException { return ""; } @Override public void setTransactionIsolation(int level) throws SQLException { connection.setTransactionIsolation(level); } @Override public int getTransactionIsolation() throws SQLException { return connection.getTransactionIsolation(); } @Override @SkipNullableCheck public SQLWarning getWarnings() throws SQLException { // TODO return null; } @Override public void clearWarnings() throws SQLException { // TODO } @Override public Map> getTypeMap() throws SQLException { return this.typeMap; } @Override public void setTypeMap(Map> map) throws SQLException { synchronized (this) { this.typeMap = map; } } @Override public int getHoldability() throws SQLException { connection.checkOpen(); return ResultSet.CLOSE_CURSORS_AT_COMMIT; } @Override public void setHoldability(int holdability) throws SQLException { connection.checkOpen(); if (holdability != ResultSet.CLOSE_CURSORS_AT_COMMIT) { throw new SQLException("turso only supports CLOSE_CURSORS_AT_COMMIT"); } } @Override @SkipNullableCheck public Savepoint setSavepoint() throws SQLException { throw new SQLFeatureNotSupportedException("Savepoints are not supported by Turso"); } @Override @SkipNullableCheck public Savepoint setSavepoint(String name) throws SQLException { throw new SQLFeatureNotSupportedException("Savepoints are not supported by Turso"); } @Override public void rollback(Savepoint savepoint) throws SQLException { throw new SQLFeatureNotSupportedException("Savepoints are not supported by Turso"); } @Override public void releaseSavepoint(Savepoint savepoint) throws SQLException { throw new SQLFeatureNotSupportedException("Savepoints are not supported by Turso"); } @Override public CallableStatement prepareCall(String sql) throws SQLException { return prepareCall( sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); } @Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { return prepareCall(sql, resultSetType, resultSetConcurrency, ResultSet.CLOSE_CURSORS_AT_COMMIT); } @Override public CallableStatement prepareCall( String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { throw new SQLException("turso does not support stored procedures"); } @Override public PreparedStatement prepareStatement(String sql) throws SQLException { return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); } @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { return prepareStatement( sql, resultSetType, resultSetConcurrency, ResultSet.CLOSE_CURSORS_AT_COMMIT); } @Override public PreparedStatement prepareStatement( String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { connection.checkOpen(); connection.checkCursor(resultSetType, resultSetConcurrency, resultSetHoldability); return new JDBC4PreparedStatement(this, sql); } @Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { return prepareStatement(sql); } @Override public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { // TODO: maybe we can enhance this functionality by using columnIndexes return prepareStatement(sql); } @Override public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { // TODO: maybe we can enhance this functionality by using columnNames return prepareStatement(sql); } @Override public Clob createClob() throws SQLException { throw new SQLFeatureNotSupportedException("createClob not supported"); } @Override public Blob createBlob() throws SQLException { throw new SQLFeatureNotSupportedException("createBlob not supported"); } @Override public NClob createNClob() throws SQLException { throw new SQLFeatureNotSupportedException("createNClob not supported"); } @Override @SkipNullableCheck public SQLXML createSQLXML() throws SQLException { throw new SQLFeatureNotSupportedException("createSQLXML not supported"); } @Override public boolean isValid(int timeout) throws SQLException { if (isClosed()) { return false; } try (Statement statement = createStatement()) { return statement.execute("select 1;"); } } @Override public void setClientInfo(String name, String value) throws SQLClientInfoException { // TODO } @Override public void setClientInfo(Properties properties) throws SQLClientInfoException { // TODO } @Override public String getClientInfo(String name) throws SQLException { // TODO return ""; } @Override @SkipNullableCheck public Properties getClientInfo() throws SQLException { // TODO return null; } @Override @SkipNullableCheck public Array createArrayOf(String typeName, Object[] elements) throws SQLException { // TODO return null; } @Override @SkipNullableCheck public Struct createStruct(String typeName, Object[] attributes) throws SQLException { // TODO return null; } @Override public void setSchema(String schema) throws SQLException { // TODO } @Override @SkipNullableCheck public String getSchema() throws SQLException { // TODO return ""; } @Override public void abort(Executor executor) throws SQLException { if (isClosed()) { return; } close(); } @Override public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { // TODO } @Override public int getNetworkTimeout() throws SQLException { // TODO return 0; } @Override @SkipNullableCheck public T unwrap(Class iface) throws SQLException { return null; } @Override public boolean isWrapperFor(Class iface) throws SQLException { // TODO return false; } /** * Sets the busy timeout for the connection. * * @param busyTimeout the timeout in milliseconds */ public void setBusyTimeout(int busyTimeout) { // TODO: add support for busy timeout } /** @return busy timeout in milliseconds. */ public int getBusyTimeout() { // TODO: add support for busyTimeout return 0; } /** * Gets the database URL. * * @return the database URL */ public String getUrl() { return this.connection.getUrl(); } /** * Checks if the connection is open. * * @throws SQLException if the connection is closed */ public void checkOpen() throws SQLException { connection.checkOpen(); } } ================================================ FILE: bindings/java/src/main/java/tech/turso/jdbc4/JDBC4DatabaseMetaData.java ================================================ package tech.turso.jdbc4; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.RowIdLifetime; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import tech.turso.annotations.Nullable; import tech.turso.annotations.SkipNullableCheck; import tech.turso.core.TursoPropertiesHolder; import tech.turso.utils.Logger; import tech.turso.utils.LoggerFactory; /** JDBC 4 DatabaseMetaData implementation for Turso databases. */ public final class JDBC4DatabaseMetaData implements DatabaseMetaData { private static final Logger logger = LoggerFactory.getLogger(JDBC4DatabaseMetaData.class); private final JDBC4Connection connection; @Nullable private PreparedStatement getTables = null; @Nullable private PreparedStatement getTableTypes = null; @Nullable private PreparedStatement getTypeInfo = null; @Nullable private PreparedStatement getCatalogs = null; @Nullable private PreparedStatement getSchemas = null; @Nullable private PreparedStatement getUDTs = null; @Nullable private PreparedStatement getColumnsTblName = null; @Nullable private PreparedStatement getSuperTypes = null; @Nullable private PreparedStatement getSuperTables = null; @Nullable private PreparedStatement getTablePrivileges = null; @Nullable private PreparedStatement getIndexInfo = null; @Nullable private PreparedStatement getProcedures = null; @Nullable private PreparedStatement getAttributes = null; @Nullable private PreparedStatement getBestRowIdentifier = null; @Nullable private PreparedStatement getVersionColumns = null; @Nullable private PreparedStatement getColumnPrivileges = null; /** * Creates a new JDBC4DatabaseMetaData instance. * * @param connection the database connection */ public JDBC4DatabaseMetaData(JDBC4Connection connection) { this.connection = connection; } @Override public boolean allProceduresAreCallable() { return false; } @Override public boolean allTablesAreSelectable() { return true; } @Override public String getURL() { return connection.getUrl(); } @Override @Nullable public String getUserName() { return null; } @Override public boolean isReadOnly() throws SQLException { return connection.isReadOnly(); } @Override public boolean nullsAreSortedHigh() { return true; } @Override public boolean nullsAreSortedLow() { return !nullsAreSortedHigh(); } @Override public boolean nullsAreSortedAtStart() { return true; } @Override public boolean nullsAreSortedAtEnd() { return !nullsAreSortedAtStart(); } @Override public String getDatabaseProductName() { return "turso"; } @Override public String getDatabaseProductVersion() { // TODO return ""; } @Override public String getDriverName() { return TursoPropertiesHolder.getDriverName(); } @Override public String getDriverVersion() { return TursoPropertiesHolder.getDriverVersion(); } @Override public int getDriverMajorVersion() { return Integer.parseInt(getDriverVersion().split("\\.")[0]); } @Override public int getDriverMinorVersion() { return Integer.parseInt(getDriverVersion().split("\\.")[1]); } @Override public boolean usesLocalFiles() { return true; } @Override public boolean usesLocalFilePerTable() { return false; } @Override public boolean supportsMixedCaseIdentifiers() { return true; } @Override public boolean storesUpperCaseIdentifiers() { return false; } @Override public boolean storesLowerCaseIdentifiers() { return false; } @Override public boolean storesMixedCaseIdentifiers() { return true; } @Override public boolean supportsMixedCaseQuotedIdentifiers() { return false; } @Override public boolean storesUpperCaseQuotedIdentifiers() { return false; } @Override public boolean storesLowerCaseQuotedIdentifiers() { return false; } @Override public boolean storesMixedCaseQuotedIdentifiers() { return false; } @Override public String getIdentifierQuoteString() { return "\""; } @Override public String getSQLKeywords() { // TODO: add more turso supported keywords return ("ABORT,ACTION,AFTER,ANALYZE,ATTACH,AUTOINCREMENT,BEFORE," + "CASCADE,CONFLICT,DATABASE,DEFERRABLE,DEFERRED,DESC,DETACH," + "EXCLUSIVE,EXPLAIN,FAIL,GLOB,IGNORE,INDEX,INDEXED,INITIALLY,INSTEAD,ISNULL," + "KEY,LIMIT,NOTNULL,OFFSET,PLAN,PRAGMA,QUERY," + "RAISE,REGEXP,REINDEX,RENAME,REPLACE,RESTRICT," + "TEMP,TEMPORARY,TRANSACTION,VACUUM,VIEW,VIRTUAL"); } @Override public String getNumericFunctions() { // TODO return ""; } @Override public String getStringFunctions() { // TOOD return ""; } @Override public String getSystemFunctions() { // TODO return ""; } @Override public String getTimeDateFunctions() { // TODO return ""; } @Override public String getSearchStringEscape() { return "\\"; } @Override public String getExtraNameCharacters() { return ""; } @Override public boolean supportsAlterTableWithAddColumn() { return false; } @Override public boolean supportsAlterTableWithDropColumn() { return false; } @Override public boolean supportsColumnAliasing() { return true; } @Override public boolean nullPlusNonNullIsNull() { return true; } @Override public boolean supportsConvert() { return false; } @Override public boolean supportsConvert(int fromType, int toType) { return false; } @Override public boolean supportsTableCorrelationNames() { return false; } @Override public boolean supportsDifferentTableCorrelationNames() { return false; } @Override public boolean supportsExpressionsInOrderBy() { return true; } @Override public boolean supportsOrderByUnrelated() { return false; } @Override public boolean supportsGroupBy() { return true; } @Override public boolean supportsGroupByUnrelated() { return false; } @Override public boolean supportsGroupByBeyondSelect() { return false; } @Override public boolean supportsLikeEscapeClause() { return false; } @Override public boolean supportsMultipleResultSets() { return false; } @Override public boolean supportsMultipleTransactions() { return true; } @Override public boolean supportsNonNullableColumns() { return true; } @Override public boolean supportsMinimumSQLGrammar() { // ODBC minimum grammar should be supported, which are the followings: // SELECT // INSERT // UPDATE // DELETE // CREATE TABLE // DROP TABLE // GRANT // REVOKE // COMMIT // ROLLBACK // DECLARE CURSOR // FETCH // CLOSE CURSOR // TODO: Let's return true when turso supports them all return false; } @Override public boolean supportsCoreSQLGrammar() { // supportsMinimumSQLGrammar() and some additional such as: // Joins (INNER JOIN, OUTER JOIN, LEFT JOIN, RIGHT JOIN) // Set operations (UNION, INTERSECT, EXCEPT) // Subqueries (e.g., SELECT * FROM table WHERE column IN (SELECT column FROM another_table)) // Table expressions (SELECT column FROM (SELECT * FROM table) AS subquery) // Data type support (includes more SQL data types like FLOAT, NUMERIC, DECIMAL) // Basic string functions (e.g., LENGTH(), SUBSTRING(), CONCAT()) // TODO: Let's return true when turso supports them all return false; } @Override public boolean supportsExtendedSQLGrammar() { return false; } @Override public boolean supportsANSI92EntryLevelSQL() { return false; } @Override public boolean supportsANSI92IntermediateSQL() { return false; } @Override public boolean supportsANSI92FullSQL() { return false; } @Override public boolean supportsIntegrityEnhancementFacility() { return false; } @Override public boolean supportsOuterJoins() { return true; } @Override public boolean supportsFullOuterJoins() { return true; } @Override public boolean supportsLimitedOuterJoins() { return true; } @Override public String getSchemaTerm() { return "schema"; } @Override public String getProcedureTerm() { return "not_implemented"; } @Override public String getCatalogTerm() { return "catalog"; } @Override public boolean isCatalogAtStart() { // sqlite and turso doesn't use catalog return false; } @Override public String getCatalogSeparator() { return "."; } @Override public boolean supportsSchemasInDataManipulation() { return false; } @Override public boolean supportsSchemasInProcedureCalls() { return false; } @Override public boolean supportsSchemasInTableDefinitions() { return false; } @Override public boolean supportsSchemasInIndexDefinitions() { return false; } @Override public boolean supportsSchemasInPrivilegeDefinitions() { return false; } @Override public boolean supportsCatalogsInDataManipulation() { return false; } @Override public boolean supportsCatalogsInProcedureCalls() { return false; } @Override public boolean supportsCatalogsInTableDefinitions() { return false; } @Override public boolean supportsCatalogsInIndexDefinitions() { return false; } @Override public boolean supportsCatalogsInPrivilegeDefinitions() { return false; } @Override public boolean supportsPositionedDelete() { return false; } @Override public boolean supportsPositionedUpdate() { return false; } @Override public boolean supportsSelectForUpdate() { return false; } @Override public boolean supportsStoredProcedures() { return false; } @Override public boolean supportsSubqueriesInComparisons() { return false; } @Override public boolean supportsSubqueriesInExists() { return true; } @Override public boolean supportsSubqueriesInIns() { return true; } @Override public boolean supportsSubqueriesInQuantifieds() { return false; } @Override public boolean supportsCorrelatedSubqueries() { return false; } @Override public boolean supportsUnion() { // TODO: return true when turso supports return false; } @Override public boolean supportsUnionAll() { // TODO: return true when turso supports return false; } @Override public boolean supportsOpenCursorsAcrossCommit() { return false; } @Override public boolean supportsOpenCursorsAcrossRollback() { return false; } @Override public boolean supportsOpenStatementsAcrossCommit() { return false; } @Override public boolean supportsOpenStatementsAcrossRollback() { return false; } @Override public int getMaxBinaryLiteralLength() { return 0; } @Override public int getMaxCharLiteralLength() { return 0; } @Override public int getMaxColumnNameLength() { return 0; } @Override public int getMaxColumnsInGroupBy() { return 0; } @Override public int getMaxColumnsInIndex() { return 0; } @Override public int getMaxColumnsInOrderBy() { return 0; } @Override public int getMaxColumnsInSelect() { return 0; } @Override public int getMaxColumnsInTable() { return 0; } @Override public int getMaxConnections() { return 0; } @Override public int getMaxCursorNameLength() { return 0; } @Override public int getMaxIndexLength() { return 0; } @Override public int getMaxSchemaNameLength() { return 0; } @Override public int getMaxProcedureNameLength() { return 0; } @Override public int getMaxCatalogNameLength() { return 0; } @Override public int getMaxRowSize() { return 0; } @Override public boolean doesMaxRowSizeIncludeBlobs() { return false; } @Override public int getMaxStatementLength() { return 0; } @Override public int getMaxStatements() { return 0; } @Override public int getMaxTableNameLength() { return 0; } @Override public int getMaxTablesInSelect() { return 0; } @Override public int getMaxUserNameLength() { return 0; } @Override public int getDefaultTransactionIsolation() { // TODO: after turso introduces Hekaton MVCC, what should we return? return Connection.TRANSACTION_SERIALIZABLE; } @Override public boolean supportsTransactions() { // TODO: turso doesn't support transactions fully, let's return true when supported return false; } @Override public boolean supportsTransactionIsolationLevel(int level) { return Connection.TRANSACTION_SERIALIZABLE == level; } @Override public boolean supportsDataDefinitionAndDataManipulationTransactions() { // TODO: return true when supported return false; } @Override public boolean supportsDataManipulationTransactionsOnly() { return false; } @Override public boolean dataDefinitionCausesTransactionCommit() { return false; } @Override public boolean dataDefinitionIgnoredInTransactions() { return false; } @Override @SkipNullableCheck public ResultSet getProcedures( String catalog, String schemaPattern, String procedureNamePattern) { // TODO return null; } @Override @SkipNullableCheck public ResultSet getProcedureColumns( String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) { // TODO return null; } // TODO: make use of getSearchStringEscape @Override public ResultSet getTables( @Nullable String catalog, @Nullable String schemaPattern, String tableNamePattern, @Nullable String[] types) throws SQLException { // SQLite doesn't support catalogs or schemas — reject if non-empty values provided if (catalog != null && !catalog.isEmpty()) { return connection.prepareStatement("SELECT * FROM sqlite_schema WHERE 1=0").executeQuery(); } if (schemaPattern != null && !schemaPattern.isEmpty()) { return connection.prepareStatement("SELECT * FROM sqlite_schema WHERE 1=0").executeQuery(); } // Start building query StringBuilder sql = new StringBuilder( "SELECT " + "NULL AS TABLE_CAT, " + "NULL AS TABLE_SCHEM, " + "name AS TABLE_NAME, " + "CASE type " + " WHEN 'table' THEN 'TABLE' " + " WHEN 'view' THEN 'VIEW' " + " ELSE UPPER(type) " + "END AS TABLE_TYPE, " + "NULL AS REMARKS, " + "NULL AS TYPE_CAT, " + "NULL AS TYPE_SCHEM, " + "NULL AS TYPE_NAME, " + "NULL AS SELF_REFERENCING_COL_NAME, " + "NULL AS REF_GENERATION " + "FROM sqlite_schema " + "WHERE 1=1"); // Apply type filtering if needed if (types != null && types.length > 0) { sql.append(" AND type IN ("); for (int i = 0; i < types.length; i++) { if (i > 0) sql.append(", "); sql.append("?"); } sql.append(")"); } // Apply table name pattern filtering if (tableNamePattern != null) { sql.append(" AND name LIKE ?"); } // Comply with spec: sort by TABLE_TYPE, TABLE_CAT, TABLE_SCHEM, TABLE_NAME sql.append(" ORDER BY TABLE_TYPE, TABLE_CAT, TABLE_SCHEM, TABLE_NAME"); // Prepare and bind statement PreparedStatement stmt = connection.prepareStatement(sql.toString()); int paramIndex = 1; if (types != null && types.length > 0) { for (String type : types) { String sqliteType; if ("TABLE".equalsIgnoreCase(type)) { sqliteType = "table"; } else if ("VIEW".equalsIgnoreCase(type)) { sqliteType = "view"; } else { sqliteType = type.toLowerCase(); } stmt.setString(paramIndex++, sqliteType); } } if (tableNamePattern != null) { stmt.setString(paramIndex, tableNamePattern); } return stmt.executeQuery(); } @Override public ResultSet getSchemas() throws SQLException { if (getSchemas == null) { connection.checkOpen(); getSchemas = connection.prepareStatement("select null as TABLE_SCHEM, null as TABLE_CATALOG limit 0;"); } return getSchemas.executeQuery(); } @Override public ResultSet getCatalogs() throws SQLException { if (getCatalogs == null) { connection.checkOpen(); getCatalogs = connection.prepareStatement("select null as TABLE_CAT limit 0;"); } return getCatalogs.executeQuery(); } @Override @SkipNullableCheck public ResultSet getTableTypes() { // TODO return null; } @Override @SkipNullableCheck public ResultSet getColumns( String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) { // TODO - important return null; } @Override @SkipNullableCheck public ResultSet getColumnPrivileges( String catalog, String schema, String table, String columnNamePattern) throws SQLException { if (getColumnPrivileges == null) { connection.close(); getColumnPrivileges = connection.prepareStatement( "select null as TABLE_CAT, null as TABLE_SCHEM, " + "null as TABLE_NAME, null as COLUMN_NAME, null as GRANTOR, null as GRANTEE, " + "null as PRIVILEGE, null as IS_GRANTABLE limit 0;"); } return getColumnPrivileges.executeQuery(); } @Override @SkipNullableCheck public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { if (getTablePrivileges == null) { connection.checkOpen(); getTablePrivileges = connection.prepareStatement( "select null as TABLE_CAT, " + "null as TABLE_SCHEM, null as TABLE_NAME, null as GRANTOR, null " + "GRANTEE, null as PRIVILEGE, null as IS_GRANTABLE limit 0;"); } return getTablePrivileges.executeQuery(); } @Override @SkipNullableCheck public ResultSet getBestRowIdentifier( String catalog, String schema, String table, int scope, boolean nullable) throws SQLException { if (getBestRowIdentifier == null) { connection.checkOpen(); getBestRowIdentifier = connection.prepareStatement( "select null as SCOPE, null as COLUMN_NAME, " + "null as DATA_TYPE, null as TYPE_NAME, null as COLUMN_SIZE, " + "null as BUFFER_LENGTH, null as DECIMAL_DIGITS, null as PSEUDO_COLUMN limit 0;"); } return getBestRowIdentifier.executeQuery(); } @Override @SkipNullableCheck public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { if (getVersionColumns == null) { connection.close(); getVersionColumns = connection.prepareStatement( "select null as SCOPE, null as COLUMN_NAME, " + "null as DATA_TYPE, null as TYPE_NAME, null as COLUMN_SIZE, " + "null as BUFFER_LENGTH, null as DECIMAL_DIGITS, null as PSEUDO_COLUMN limit 0;"); } return getVersionColumns.executeQuery(); } @Override @SkipNullableCheck public ResultSet getPrimaryKeys(String catalog, String schema, String table) { // TODO - important return null; } @Override @SkipNullableCheck public ResultSet getImportedKeys(String catalog, String schema, String table) { // TODO return null; } @Override @SkipNullableCheck public ResultSet getExportedKeys(String catalog, String schema, String table) { return null; } @Override @SkipNullableCheck public ResultSet getCrossReference( String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) { // TODO return null; } @Override @SkipNullableCheck public ResultSet getTypeInfo() { // TODO return null; } @Override @SkipNullableCheck public ResultSet getIndexInfo( String catalog, String schema, String table, boolean unique, boolean approximate) { // TODO return null; } @Override public boolean supportsResultSetType(int type) { return type == ResultSet.TYPE_FORWARD_ONLY; } @Override public boolean supportsResultSetConcurrency(int type, int concurrency) { return (type == ResultSet.TYPE_FORWARD_ONLY && concurrency == ResultSet.CONCUR_READ_ONLY); } @Override public boolean ownUpdatesAreVisible(int type) { return false; } @Override public boolean ownDeletesAreVisible(int type) { return false; } @Override public boolean ownInsertsAreVisible(int type) { return false; } @Override public boolean othersUpdatesAreVisible(int type) { return false; } @Override public boolean othersDeletesAreVisible(int type) { return false; } @Override public boolean othersInsertsAreVisible(int type) { return false; } @Override public boolean updatesAreDetected(int type) { return false; } @Override public boolean deletesAreDetected(int type) { return false; } @Override public boolean insertsAreDetected(int type) { return false; } @Override public boolean supportsBatchUpdates() { // TODO - let's add support for batch updates in the future and let this method return true return false; } @Override public ResultSet getUDTs( String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { if (getUDTs == null) { connection.close(); getUDTs = connection.prepareStatement( "select null as TYPE_CAT, null as TYPE_SCHEM, " + "null as TYPE_NAME, null as CLASS_NAME, null as DATA_TYPE, null as REMARKS, " + "null as BASE_TYPE " + "limit 0;"); } getUDTs.clearParameters(); return getUDTs.executeQuery(); } @Override public Connection getConnection() { return connection; } @Override public boolean supportsSavepoints() { // TODO: return true when turso supports save points return false; } @Override public boolean supportsNamedParameters() { // TODO: return true when both turso and jdbc supports named parameters return false; } @Override public boolean supportsMultipleOpenResults() { return false; } @Override public boolean supportsGetGeneratedKeys() { // TODO: check return false; } @Override @SkipNullableCheck public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { if (getSuperTypes == null) { connection.checkOpen(); getSuperTypes = connection.prepareStatement( "select null as TYPE_CAT, null as TYPE_SCHEM, " + "null as TYPE_NAME, null as SUPERTYPE_CAT, null as SUPERTYPE_SCHEM, " + "null as SUPERTYPE_NAME limit 0;"); } return getSuperTypes.executeQuery(); } @Override @SkipNullableCheck public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { if (getSuperTables == null) { connection.checkOpen(); getSuperTables = connection.prepareStatement( "select null as TABLE_CAT, null as TABLE_SCHEM, " + "null as TABLE_NAME, null as SUPERTABLE_NAME limit 0;"); } return getSuperTables.executeQuery(); } @Override @SkipNullableCheck public ResultSet getAttributes( String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) throws SQLException { if (getAttributes == null) { connection.checkOpen(); getAttributes = connection.prepareStatement( "select null as TYPE_CAT, null as TYPE_SCHEM, " + "null as TYPE_NAME, null as ATTR_NAME, null as DATA_TYPE, " + "null as ATTR_TYPE_NAME, null as ATTR_SIZE, null as DECIMAL_DIGITS, " + "null as NUM_PREC_RADIX, null as NULLABLE, null as REMARKS, null as ATTR_DEF, " + "null as SQL_DATA_TYPE, null as SQL_DATETIME_SUB, null as CHAR_OCTET_LENGTH, " + "null as ORDINAL_POSITION, null as IS_NULLABLE, null as SCOPE_CATALOG, " + "null as SCOPE_SCHEMA, null as SCOPE_TABLE, null as SOURCE_DATA_TYPE limit 0;"); } return getAttributes.executeQuery(); } @Override public boolean supportsResultSetHoldability(int holdability) { return holdability == ResultSet.CLOSE_CURSORS_AT_COMMIT; } @Override public int getResultSetHoldability() { return ResultSet.CLOSE_CURSORS_AT_COMMIT; } @Override public int getDatabaseMajorVersion() { // TODO - important return 0; } @Override public int getDatabaseMinorVersion() { // TODO - important return 0; } @Override public int getJDBCMajorVersion() { return 4; } @Override public int getJDBCMinorVersion() { return 2; } @Override public int getSQLStateType() { return DatabaseMetaData.sqlStateSQL99; } @Override public boolean locatorsUpdateCopy() { return false; } @Override public boolean supportsStatementPooling() { return false; } @Override public RowIdLifetime getRowIdLifetime() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { if (getSchemas == null) { connection.checkOpen(); getSchemas = connection.prepareStatement("select null as TABLE_SCHEM, null as TABLE_CATALOG limit 0;"); } return getSchemas.executeQuery(); } @Override public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public boolean autoCommitFailureClosesAllResultSets() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public ResultSet getClientInfoProperties() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override @SkipNullableCheck public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override @SkipNullableCheck public ResultSet getFunctionColumns( String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) throws SQLException { throw new SQLFeatureNotSupportedException("Not yet implemented by SQLite JDBC driver"); } @Override @SkipNullableCheck public ResultSet getPseudoColumns( String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public boolean generatedKeyAlwaysReturned() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override @SkipNullableCheck public T unwrap(Class iface) throws SQLException { return null; } @Override public boolean isWrapperFor(Class iface) throws SQLException { return false; } } ================================================ FILE: bindings/java/src/main/java/tech/turso/jdbc4/JDBC4PreparedStatement.java ================================================ package tech.turso.jdbc4; import static java.util.Objects.requireNonNull; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.sql.*; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import tech.turso.annotations.Nullable; import tech.turso.annotations.SkipNullableCheck; import tech.turso.core.TursoResultSet; /** JDBC 4 PreparedStatement implementation for Turso databases. */ public final class JDBC4PreparedStatement extends JDBC4Statement implements PreparedStatement { private final String sql; private final JDBC4ResultSet resultSet; private final int paramCount; private Object[] currentBatchParams; private final ArrayList batchQueryParams = new ArrayList<>(); /** * Creates a new JDBC4PreparedStatement. * * @param connection the database connection * @param sql the SQL statement to prepare * @throws SQLException if a database access error occurs */ public JDBC4PreparedStatement(JDBC4Connection connection, String sql) throws SQLException { super(connection); this.sql = sql; this.statement = connection.prepare(sql); this.resultSet = new JDBC4ResultSet(this.statement.getResultSet(), this); this.paramCount = statement.parameterCount(); this.currentBatchParams = new Object[paramCount]; } @Override public ResultSet executeQuery() throws SQLException { // TODO: check bindings etc bindParams(currentBatchParams); return this.resultSet; } @Override public int executeUpdate() throws SQLException { requireNonNull(this.statement); bindParams(currentBatchParams); final TursoResultSet resultSet = statement.getResultSet(); resultSet.consumeAll(); return Math.toIntExact(statement.changes()); } /** * This helper method saves a parameter locally without binding it to the underlying native * statement. We have to do this so we are able to switch between different sets of parameters * when batching queries. */ private void setParam(int parameterIndex, @Nullable Object object) { requireNonNull(this.statement); currentBatchParams[parameterIndex - 1] = object; } @Override public void setNull(int parameterIndex, int sqlType) throws SQLException { requireNonNull(this.statement); setParam(parameterIndex, null); } @Override public void setBoolean(int parameterIndex, boolean x) throws SQLException { requireNonNull(this.statement); setParam(parameterIndex, x ? 1 : 0); } @Override public void setByte(int parameterIndex, byte x) throws SQLException { requireNonNull(this.statement); setParam(parameterIndex, x); } @Override public void setShort(int parameterIndex, short x) throws SQLException { requireNonNull(this.statement); setParam(parameterIndex, x); } @Override public void setInt(int parameterIndex, int x) throws SQLException { requireNonNull(this.statement); setParam(parameterIndex, x); } @Override public void setLong(int parameterIndex, long x) throws SQLException { requireNonNull(this.statement); setParam(parameterIndex, x); } @Override public void setFloat(int parameterIndex, float x) throws SQLException { requireNonNull(this.statement); setParam(parameterIndex, x); } @Override public void setDouble(int parameterIndex, double x) throws SQLException { requireNonNull(this.statement); setParam(parameterIndex, x); } @Override public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { requireNonNull(this.statement); setParam(parameterIndex, x.toString()); } @Override public void setString(int parameterIndex, String x) throws SQLException { requireNonNull(this.statement); setParam(parameterIndex, x); } @Override public void setBytes(int parameterIndex, byte[] x) throws SQLException { requireNonNull(this.statement); setParam(parameterIndex, x); } @Override public void setDate(int parameterIndex, Date x) throws SQLException { requireNonNull(this.statement); if (x == null) { setParam(parameterIndex, null); } else { long time = x.getTime(); setParam(parameterIndex, ByteBuffer.allocate(Long.BYTES).putLong(time).array()); } } @Override public void setTime(int parameterIndex, Time x) throws SQLException { requireNonNull(this.statement); if (x == null) { setParam(parameterIndex, null); } else { long time = x.getTime(); setParam(parameterIndex, ByteBuffer.allocate(Long.BYTES).putLong(time).array()); } } @Override public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { requireNonNull(this.statement); if (x == null) { setParam(parameterIndex, null); } else { long time = x.getTime(); setParam(parameterIndex, ByteBuffer.allocate(Long.BYTES).putLong(time).array()); } } @Override public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { requireNonNull(this.statement); if (x == null) { setParam(parameterIndex, null); return; } if (length < 0) { throw new SQLException("setAsciiStream length must be non-negative"); } if (length == 0) { setParam(parameterIndex, ""); return; } try { byte[] buffer = new byte[length]; int offset = 0; int read; while (offset < length && (read = x.read(buffer, offset, length - offset)) > 0) { offset += read; } String ascii = new String(buffer, 0, offset, StandardCharsets.US_ASCII); setParam(parameterIndex, ascii); } catch (IOException e) { throw new SQLException("Error reading ASCII stream", e); } } @Override public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { requireNonNull(this.statement); if (x == null) { setParam(parameterIndex, null); return; } if (length < 0) { throw new SQLException("setUnicodeStream length must be non-negative"); } if (length == 0) { setParam(parameterIndex, ""); return; } try { byte[] buffer = new byte[length]; int offset = 0; int read; while (offset < length && (read = x.read(buffer, offset, length - offset)) > 0) { offset += read; } String text = new String(buffer, 0, offset, StandardCharsets.UTF_8); setParam(parameterIndex, text); } catch (IOException e) { throw new SQLException("Error reading Unicode stream", e); } } @Override public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { requireNonNull(this.statement); if (x == null) { setParam(parameterIndex, null); return; } if (length < 0) { throw new SQLException("setBinaryStream length must be non-negative"); } if (length == 0) { setParam(parameterIndex, new byte[0]); return; } try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { byte[] buffer = new byte[8192]; int bytesRead; int totalRead = 0; while (totalRead < length && (bytesRead = x.read(buffer, 0, Math.min(buffer.length, length - totalRead))) > 0) { baos.write(buffer, 0, bytesRead); totalRead += bytesRead; } byte[] data = baos.toByteArray(); setParam(parameterIndex, data); } catch (IOException e) { throw new SQLException("Error reading binary stream", e); } } @Override public void clearParameters() { this.currentBatchParams = new Object[paramCount]; } @Override public void clearBatch() throws SQLException { this.batchQueryParams.clear(); this.currentBatchParams = new Object[paramCount]; } @Override public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { // TODO } @Override public void setObject(int parameterIndex, Object x) throws SQLException { requireNonNull(this.statement); if (x == null) { setParam(parameterIndex, null); return; } if (x instanceof String) { setString(parameterIndex, (String) x); } else if (x instanceof Integer) { setInt(parameterIndex, (Integer) x); } else if (x instanceof Long) { setLong(parameterIndex, (Long) x); } else if (x instanceof Boolean) { setBoolean(parameterIndex, (Boolean) x); } else if (x instanceof Double) { setDouble(parameterIndex, (Double) x); } else if (x instanceof Float) { setFloat(parameterIndex, (Float) x); } else if (x instanceof Byte) { setByte(parameterIndex, (Byte) x); } else if (x instanceof Short) { setShort(parameterIndex, (Short) x); } else if (x instanceof byte[]) { setBytes(parameterIndex, (byte[]) x); } else if (x instanceof Timestamp) { setTimestamp(parameterIndex, (Timestamp) x); } else if (x instanceof Date) { setDate(parameterIndex, (Date) x); } else if (x instanceof Time) { setTime(parameterIndex, (Time) x); } else if (x instanceof BigDecimal) { setBigDecimal(parameterIndex, (BigDecimal) x); } else if (x instanceof Blob || x instanceof Clob || x instanceof InputStream || x instanceof Reader) { throw new SQLException( "setObject does not yet support LOB or Stream types because the corresponding set methods are unimplemented. Type found: " + x.getClass().getName()); } else { throw new SQLException("Unsupported object type in setObject: " + x.getClass().getName()); } } @Override public boolean execute() throws SQLException { return execute(currentBatchParams); } /** This helper method runs the statement using the provided parameter values. */ private boolean execute(Object[] params) throws SQLException { // TODO: check whether this is sufficient requireNonNull(statement); bindParams(params); boolean result = statement.execute(); updateCount = statement.changes(); return result; } @Override public int[] executeBatch() throws SQLException { return Arrays.stream(executeLargeBatch()).mapToInt(l -> (int) l).toArray(); } @Override public long[] executeLargeBatch() throws SQLException { requireNonNull(this.statement); if (batchQueryParams.isEmpty()) { return new long[0]; } long[] updateCounts = new long[batchQueryParams.size()]; if (!isBatchCompatibleStatement(sql)) { updateCounts[0] = EXECUTE_FAILED; BatchUpdateException bue = new BatchUpdateException( "Batch commands cannot return result sets.", "HY000", // General error SQL state 0, Arrays.stream(updateCounts).mapToInt(l -> (int) l).toArray()); // Clear the batch after failure clearBatch(); throw bue; } for (int i = 0; i < batchQueryParams.size(); i++) { try { statement.reset(); execute(batchQueryParams.get(i)); updateCounts[i] = getUpdateCount(); } catch (SQLException e) { BatchUpdateException bue = new BatchUpdateException( "Batch entry " + i + " (" + sql + ") failed: " + e.getMessage(), e.getSQLState(), e.getErrorCode(), updateCounts, e.getCause()); // Clear the batch after failure clearBatch(); throw bue; } } clearBatch(); return updateCounts; } /** Takes the given set of parameters and binds it to the underlying statement. */ private void bindParams(Object[] params) throws SQLException { requireNonNull(statement); for (int paramIndex = 1; paramIndex <= params.length; paramIndex++) { statement.bindObject(paramIndex, params[paramIndex - 1]); } } @Override public void addBatch() { batchQueryParams.add(currentBatchParams); currentBatchParams = new Object[paramCount]; } @Override public void addBatch(String sql) throws SQLException { throw new SQLException("addBatch(String) cannot be called on a PreparedStatement"); } @Override public void setCharacterStream(int parameterIndex, @Nullable Reader reader, int length) throws SQLException { requireNonNull(this.statement); if (reader == null) { setParam(parameterIndex, null); return; } if (length < 0) { throw new SQLException("setCharacterStream length must be non-negative"); } if (length == 0) { setParam(parameterIndex, ""); return; } try { char[] buffer = new char[length]; int offset = 0; int read; while (offset < length && (read = reader.read(buffer, offset, length - offset)) > 0) { offset += read; } String value = new String(buffer, 0, offset); setParam(parameterIndex, value); } catch (IOException e) { throw new SQLException("Error reading character stream", e); } } @Override public void setRef(int parameterIndex, Ref x) throws SQLException { // TODO } @Override public void setBlob(int parameterIndex, Blob x) throws SQLException { // TODO } @Override public void setClob(int parameterIndex, Clob x) throws SQLException { // TODO } @Override public void setArray(int parameterIndex, Array x) throws SQLException { // TODO } @Override public ResultSetMetaData getMetaData() throws SQLException { return this.resultSet; } @Override public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { setDate(parameterIndex, x); } @Override public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { setTime(parameterIndex, x); } @Override public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { // TODO: Apply calendar timezone conversion setTimestamp(parameterIndex, x); } @Override public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { // TODO } @Override public void setURL(int parameterIndex, URL x) throws SQLException { // TODO } @Override @SkipNullableCheck public ParameterMetaData getParameterMetaData() throws SQLException { // TODO return null; } @Override public void setRowId(int parameterIndex, RowId x) throws SQLException { // TODO } @Override public void setNString(int parameterIndex, String value) throws SQLException { // TODO } @Override public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { // TODO } @Override public void setNClob(int parameterIndex, NClob value) throws SQLException { // TODO } @Override public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { // TODO } @Override public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { // TODO } @Override public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { // TODO } @Override public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { // TODO } @Override public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { // TODO } @Override public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { requireLengthIsPositiveInt(length); setAsciiStream(parameterIndex, x, (int) length); } @Override public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { requireLengthIsPositiveInt(length); setBinaryStream(parameterIndex, x, (int) length); } @Override public void setCharacterStream(int parameterIndex, @Nullable Reader reader, long length) throws SQLException { requireLengthIsPositiveInt(length); setCharacterStream(parameterIndex, reader, (int) length); } private void requireLengthIsPositiveInt(long length) throws SQLFeatureNotSupportedException { if (length > Integer.MAX_VALUE || length < 0) { throw new SQLFeatureNotSupportedException( "Data must have a length between 0 and Integer.MAX_VALUE"); } } @Override public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { requireNonNull(this.statement); if (x == null) { setParam(parameterIndex, null); return; } byte[] data = readBytes(x); String ascii = new String(data, StandardCharsets.US_ASCII); setParam(parameterIndex, ascii); } @Override public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { requireNonNull(this.statement); if (x == null) { setParam(parameterIndex, null); return; } byte[] data = readBytes(x); setParam(parameterIndex, data); } /** * Reads all bytes from the given input stream. * * @param x the input stream to read * @return a byte array containing the data * @throws SQLException if an I/O error occurs while reading */ private byte[] readBytes(InputStream x) throws SQLException { try { int firstByte = x.read(); if (firstByte == -1) { return new byte[0]; } ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(firstByte); byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = x.read(buffer)) > 0) { baos.write(buffer, 0, bytesRead); } return baos.toByteArray(); } catch (IOException e) { throw new SQLException("Error reading InputStream", e); } } @Override public void setCharacterStream(int parameterIndex, @Nullable Reader reader) throws SQLException { requireNonNull(this.statement); if (reader == null) { setParam(parameterIndex, null); return; } try { StringBuilder sb = new StringBuilder(); char[] buffer = new char[8192]; int read; while ((read = reader.read(buffer)) != -1) { sb.append(buffer, 0, read); } setParam(parameterIndex, sb.toString()); } catch (IOException e) { throw new SQLException("Error reading character stream", e); } } @Override public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { // TODO } @Override public void setClob(int parameterIndex, Reader reader) throws SQLException { // TODO } @Override public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { // TODO } @Override public void setNClob(int parameterIndex, Reader reader) throws SQLException { // TODO } } ================================================ FILE: bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSet.java ================================================ package tech.turso.jdbc4; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.URL; import java.nio.ByteBuffer; import java.sql.Array; import java.sql.Blob; import java.sql.Clob; import java.sql.Date; import java.sql.NClob; import java.sql.Ref; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.RowId; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.SQLXML; import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; import java.util.Calendar; import java.util.Map; import tech.turso.annotations.Nullable; import tech.turso.annotations.SkipNullableCheck; import tech.turso.core.TursoResultSet; /** JDBC 4 ResultSet implementation for Turso databases. */ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData { private final TursoResultSet resultSet; @Nullable private final Statement statement; private boolean wasNull = false; /** * Creates a new JDBC4ResultSet. * * @param resultSet the underlying Turso result set * @param statement the statement that created this result set */ public JDBC4ResultSet(TursoResultSet resultSet, @Nullable Statement statement) { this.resultSet = resultSet; this.statement = statement; } @Override public boolean next() throws SQLException { return resultSet.next(); } @Override public void close() throws SQLException { resultSet.close(); } @Override public boolean wasNull() throws SQLException { return wasNull; } @Override @Nullable public String getString(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return null; } return wrapTypeConversion(() -> (String) result); } @Override public boolean getBoolean(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return false; } return wrapTypeConversion(() -> (Long) result != 0); } @Override public byte getByte(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return 0; } return wrapTypeConversion(() -> ((Long) result).byteValue()); } @Override public short getShort(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return 0; } return wrapTypeConversion(() -> ((Long) result).shortValue()); } @Override public int getInt(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return 0; } return wrapTypeConversion(() -> ((Long) result).intValue()); } @Override public long getLong(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return 0; } return wrapTypeConversion(() -> (long) result); } @Override public float getFloat(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return 0; } return wrapTypeConversion(() -> ((Double) result).floatValue()); } @Override public double getDouble(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return 0; } return wrapTypeConversion(() -> (double) result); } // TODO: customize rounding mode? @Override @Nullable public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return null; } final double doubleResult = wrapTypeConversion(() -> (double) result); final BigDecimal bigDecimalResult = BigDecimal.valueOf(doubleResult); return bigDecimalResult.setScale(scale, RoundingMode.HALF_UP); } @Override @Nullable public byte[] getBytes(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return null; } return wrapTypeConversion(() -> (byte[]) result); } @Override @Nullable public Date getDate(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return null; } return wrapTypeConversion( () -> { if (result instanceof byte[]) { byte[] bytes = (byte[]) result; if (bytes.length == Long.BYTES) { long time = ByteBuffer.wrap(bytes).getLong(); return new Date(time); } } throw new SQLException("Cannot convert value to Date: " + result.getClass()); }); } @Override @SkipNullableCheck public Time getTime(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return null; } return wrapTypeConversion( () -> { if (result instanceof byte[]) { byte[] bytes = (byte[]) result; if (bytes.length == Long.BYTES) { long time = ByteBuffer.wrap(bytes).getLong(); return new Time(time); } } throw new SQLException("Cannot convert value to Date: " + result.getClass()); }); } @Override @SkipNullableCheck public Timestamp getTimestamp(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return null; } return wrapTypeConversion( () -> { if (result instanceof byte[]) { byte[] bytes = (byte[]) result; if (bytes.length == Long.BYTES) { long time = ByteBuffer.wrap(bytes).getLong(); return new Timestamp(time); } } throw new SQLException("Cannot convert value to Timestamp: " + result.getClass()); }); } @Override @SkipNullableCheck public InputStream getAsciiStream(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return null; } return wrapTypeConversion( () -> { if (result instanceof String) { return new ByteArrayInputStream(((String) result).getBytes("US-ASCII")); } else if (result instanceof byte[]) { return new ByteArrayInputStream((byte[]) result); } throw new SQLException("Cannot convert to ASCII stream: " + result.getClass()); }); } @Override @SkipNullableCheck public InputStream getUnicodeStream(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return null; } return wrapTypeConversion( () -> { if (result instanceof String) { return new ByteArrayInputStream(((String) result).getBytes("UTF-8")); } else if (result instanceof byte[]) { return new ByteArrayInputStream((byte[]) result); } throw new SQLException("Cannot convert to Unicode stream: " + result.getClass()); }); } @Override @SkipNullableCheck public InputStream getBinaryStream(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return null; } return wrapTypeConversion( () -> { if (result instanceof byte[]) { return new ByteArrayInputStream((byte[]) result); } throw new SQLException("Cannot convert to binary stream: " + result.getClass()); }); } @Override @Nullable public String getString(String columnLabel) throws SQLException { return getString(findColumn(columnLabel)); } @Override public boolean getBoolean(String columnLabel) throws SQLException { return getBoolean(findColumn(columnLabel)); } @Override public byte getByte(String columnLabel) throws SQLException { return getByte(findColumn(columnLabel)); } @Override public short getShort(String columnLabel) throws SQLException { return getShort(findColumn(columnLabel)); } @Override public int getInt(String columnLabel) throws SQLException { return getInt(findColumn(columnLabel)); } @Override public long getLong(String columnLabel) throws SQLException { return getLong(findColumn(columnLabel)); } @Override public float getFloat(String columnLabel) throws SQLException { return getFloat(findColumn(columnLabel)); } @Override public double getDouble(String columnLabel) throws SQLException { return getDouble(findColumn(columnLabel)); } @Override @SkipNullableCheck public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { return getBigDecimal(findColumn(columnLabel), scale); } @Override @Nullable public byte[] getBytes(String columnLabel) throws SQLException { return getBytes(findColumn(columnLabel)); } @Override @Nullable public Date getDate(String columnLabel) throws SQLException { final Object result = resultSet.get(columnLabel); wasNull = result == null; if (result == null) { return null; } return wrapTypeConversion( () -> { if (result instanceof byte[]) { byte[] bytes = (byte[]) result; if (bytes.length == Long.BYTES) { long time = ByteBuffer.wrap(bytes).getLong(); return new Date(time); } } // Try to parse as string if it's stored as TEXT if (result instanceof String) { return Date.valueOf((String) result); } throw new SQLException("Cannot convert value to Date: " + result.getClass()); }); } @Override @SkipNullableCheck public Time getTime(String columnLabel) throws SQLException { return getTime(findColumn(columnLabel)); } @Override @SkipNullableCheck public Timestamp getTimestamp(String columnLabel) throws SQLException { return getTimestamp(findColumn(columnLabel)); } @Override @SkipNullableCheck public InputStream getAsciiStream(String columnLabel) throws SQLException { return getAsciiStream(findColumn(columnLabel)); } @Override @SkipNullableCheck public InputStream getUnicodeStream(String columnLabel) throws SQLException { return getUnicodeStream(findColumn(columnLabel)); } @Override @SkipNullableCheck public InputStream getBinaryStream(String columnLabel) throws SQLException { return getBinaryStream(findColumn(columnLabel)); } @Override @SkipNullableCheck public SQLWarning getWarnings() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void clearWarnings() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public String getCursorName() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public ResultSetMetaData getMetaData() throws SQLException { return this; } @Override public Object getObject(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; return result; } @Override @SkipNullableCheck public Object getObject(String columnLabel) throws SQLException { return getObject(findColumn(columnLabel)); } @Override public int findColumn(String columnLabel) throws SQLException { if (columnLabel == null || columnLabel.isEmpty()) { throw new SQLException("column name not found"); } final String[] columnNames = resultSet.getColumnNames(); for (int i = 0; i < columnNames.length; i++) { if (columnNames[i].equals(columnLabel)) { return i + 1; } } throw new SQLException("column name " + columnLabel + " not found"); } @Override @SkipNullableCheck public Reader getCharacterStream(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return null; } return wrapTypeConversion(() -> new StringReader((String) result)); } @Override @Nullable public Reader getCharacterStream(String columnLabel) throws SQLException { return getCharacterStream(findColumn(columnLabel)); } @Override @Nullable public BigDecimal getBigDecimal(int columnIndex) throws SQLException { final Object result = resultSet.get(columnIndex); wasNull = result == null; if (result == null) { return null; } final double doubleResult = wrapTypeConversion(() -> (double) result); return BigDecimal.valueOf(doubleResult); } @Override @SkipNullableCheck public BigDecimal getBigDecimal(String columnLabel) throws SQLException { return getBigDecimal(findColumn(columnLabel)); } @Override public boolean isBeforeFirst() throws SQLException { // Empty ResultSet should return false per JDBC spec if (resultSet.isEmpty()) { return false; } return resultSet.isOpen() && resultSet.getRow() == 0 && !resultSet.isPastLastRow(); } @Override public boolean isAfterLast() throws SQLException { return resultSet.isOpen() && resultSet.isPastLastRow(); } @Override public boolean isFirst() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean isLast() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void beforeFirst() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void afterLast() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean first() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean last() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int getRow() throws SQLException { return resultSet.getRow(); } @Override public boolean absolute(int row) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean relative(int rows) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean previous() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void setFetchDirection(int direction) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int getFetchDirection() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void setFetchSize(int rows) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int getFetchSize() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int getType() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int getConcurrency() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean rowUpdated() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean rowInserted() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean rowDeleted() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNull(int columnIndex) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBoolean(int columnIndex, boolean x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateByte(int columnIndex, byte x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateShort(int columnIndex, short x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateInt(int columnIndex, int x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateLong(int columnIndex, long x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateFloat(int columnIndex, float x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateDouble(int columnIndex, double x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateString(int columnIndex, String x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBytes(int columnIndex, byte[] x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateDate(int columnIndex, Date x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateTime(int columnIndex, Time x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateObject(int columnIndex, Object x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNull(String columnLabel) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBoolean(String columnLabel, boolean x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateByte(String columnLabel, byte x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateShort(String columnLabel, short x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateInt(String columnLabel, int x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateLong(String columnLabel, long x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateFloat(String columnLabel, float x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateDouble(String columnLabel, double x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateString(String columnLabel, String x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBytes(String columnLabel, byte[] x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateDate(String columnLabel, Date x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateTime(String columnLabel, Time x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateObject(String columnLabel, Object x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void insertRow() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateRow() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void deleteRow() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void refreshRow() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void cancelRowUpdates() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void moveToInsertRow() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void moveToCurrentRow() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @Nullable public Statement getStatement() throws SQLException { return statement; } @Override @SkipNullableCheck public Object getObject(int columnIndex, Map> map) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public Ref getRef(int columnIndex) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public Blob getBlob(int columnIndex) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public Clob getClob(int columnIndex) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public Array getArray(int columnIndex) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public Object getObject(String columnLabel, Map> map) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public Ref getRef(String columnLabel) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public Blob getBlob(String columnLabel) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public Clob getClob(String columnLabel) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public Array getArray(String columnLabel) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @Nullable public Date getDate(int columnIndex, Calendar cal) throws SQLException { final Date date = getDate(columnIndex); if (date == null || cal == null) { return date; } return new Date(date.getTime() + calculateTimezoneOffset(date.getTime(), cal)); } @Override @Nullable public Date getDate(String columnLabel, Calendar cal) throws SQLException { return getDate(findColumn(columnLabel), cal); } @Override @Nullable public Time getTime(int columnIndex, Calendar cal) throws SQLException { final Time time = getTime(columnIndex); if (time == null || cal == null) { return time; } return new Time(time.getTime() + calculateTimezoneOffset(time.getTime(), cal)); } @Override @SkipNullableCheck public Time getTime(String columnLabel, Calendar cal) throws SQLException { return getTime(findColumn(columnLabel), cal); } @Override @SkipNullableCheck public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { final Timestamp timestamp = getTimestamp(columnIndex); if (timestamp == null || cal == null) { return timestamp; } return new Timestamp(timestamp.getTime() + calculateTimezoneOffset(timestamp.getTime(), cal)); } @Override @SkipNullableCheck public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { return getTimestamp(findColumn(columnLabel), cal); } @Override @SkipNullableCheck public URL getURL(int columnIndex) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public URL getURL(String columnLabel) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateRef(int columnIndex, Ref x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateRef(String columnLabel, Ref x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBlob(int columnIndex, Blob x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBlob(String columnLabel, Blob x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateClob(int columnIndex, Clob x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateClob(String columnLabel, Clob x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateArray(int columnIndex, Array x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateArray(String columnLabel, Array x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public RowId getRowId(int columnIndex) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public RowId getRowId(String columnLabel) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateRowId(int columnIndex, RowId x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateRowId(String columnLabel, RowId x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int getHoldability() throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean isClosed() throws SQLException { return !resultSet.isOpen(); } @Override public void updateNString(int columnIndex, String nString) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNString(String columnLabel, String nString) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNClob(int columnIndex, NClob nClob) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNClob(String columnLabel, NClob nClob) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public NClob getNClob(int columnIndex) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public NClob getNClob(String columnLabel) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public SQLXML getSQLXML(int columnIndex) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public SQLXML getSQLXML(String columnLabel) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public String getNString(int columnIndex) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public String getNString(String columnLabel) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public Reader getNCharacterStream(int columnIndex) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public Reader getNCharacterStream(String columnLabel) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateClob(int columnIndex, Reader reader) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateClob(String columnLabel, Reader reader) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNClob(int columnIndex, Reader reader) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public void updateNClob(String columnLabel, Reader reader) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public T getObject(int columnIndex, Class type) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public T getObject(String columnLabel, Class type) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override @SkipNullableCheck public T unwrap(Class iface) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean isWrapperFor(Class iface) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int getColumnCount() throws SQLException { return this.resultSet.getColumnNames().length; } @Override public boolean isAutoIncrement(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean isCaseSensitive(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean isSearchable(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean isCurrency(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int isNullable(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean isSigned(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int getColumnDisplaySize(int column) throws SQLException { return Integer.MAX_VALUE; } @Override public String getColumnLabel(int column) throws SQLException { // TODO: should consider "AS" keyword return getColumnName(column); } @Override public String getColumnName(int column) throws SQLException { if (column > 0 && column <= resultSet.getColumnNames().length) { return resultSet.getColumnNames()[column - 1]; } throw new SQLException("Index out of bound: " + column); } @Override public String getSchemaName(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int getPrecision(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int getScale(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public String getTableName(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public String getCatalogName(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public int getColumnType(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public String getColumnTypeName(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean isReadOnly(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean isWritable(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public boolean isDefinitelyWritable(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } @Override public String getColumnClassName(int column) throws SQLException { throw new UnsupportedOperationException("not implemented"); } private long calculateTimezoneOffset(long timeMillis, Calendar targetCal) { Calendar localCal = Calendar.getInstance(); return targetCal.getTimeZone().getOffset(timeMillis) - localCal.getTimeZone().getOffset(timeMillis); } /** * Functional interface for result set value suppliers. * * @param the type of value to supply */ @FunctionalInterface public interface ResultSetSupplier { /** * Gets a result from the result set. * * @return the result value * @throws Exception if an error occurs */ T get() throws Exception; } private T wrapTypeConversion(ResultSetSupplier supplier) throws SQLException { try { return supplier.get(); } catch (Exception e) { throw new SQLException("Type conversion failed: " + e); } } } ================================================ FILE: bindings/java/src/main/java/tech/turso/jdbc4/JDBC4Statement.java ================================================ package tech.turso.jdbc4; import static java.util.Objects.requireNonNull; import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Pattern; import tech.turso.annotations.Nullable; import tech.turso.annotations.SkipNullableCheck; import tech.turso.core.TursoResultSet; import tech.turso.core.TursoStatement; /** JDBC 4 Statement implementation for Turso databases. */ public class JDBC4Statement implements Statement { private static final Pattern BATCH_COMPATIBLE_PATTERN = Pattern.compile( "^\\s*" + // Leading whitespace "(?:/\\*.*?\\*/\\s*)*" + // Optional C-style comments "(?:--[^\\n]*\\n\\s*)*" + // Optional SQL line comments "(?:" + // Start of keywords group "INSERT|UPDATE|DELETE" + ")\\b", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); protected final JDBC4Connection connection; /** The underlying Turso statement. */ @Nullable protected TursoStatement statement = null; /** The number of rows affected by the last update operation. */ protected long updateCount; // Because JDBC4Statement has different life cycle in compared to tursoStatement, let's use this // field to manage JDBC4Statement lifecycle private boolean closed; private boolean closeOnCompletion; private final int resultSetType; private final int resultSetConcurrency; private final int resultSetHoldability; private int queryTimeoutSeconds; private ReentrantLock connectionLock = new ReentrantLock(); /** * List of SQL statements to be executed as a batch. Used for batch processing as per JDBC * specification. */ private List batchCommands = new ArrayList<>(); public JDBC4Statement(JDBC4Connection connection) { this( connection, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); } public JDBC4Statement( JDBC4Connection connection, int resultSetType, int resultSetConcurrency, int resultSetHoldability) { this.connection = connection; this.resultSetType = resultSetType; this.resultSetConcurrency = resultSetConcurrency; this.resultSetHoldability = resultSetHoldability; } // TODO: should executeQuery run execute right after preparing the statement? @Override public ResultSet executeQuery(String sql) throws SQLException { ensureOpen(); statement = this.withConnectionTimeout( () -> { try { // TODO: if sql is a readOnly query, do we still need the locks? connectionLock.lock(); return connection.prepare(sql); } finally { connectionLock.unlock(); } }); requireNonNull(statement, "statement should not be null after running execute method"); return new JDBC4ResultSet(statement.getResultSet(), this); } @Override public int executeUpdate(String sql) throws SQLException { final long previousTotalChanges = statement == null ? 0L : statement.totalChanges(); execute(sql); requireNonNull(statement, "statement should not be null after running execute method"); final TursoResultSet resultSet = statement.getResultSet(); resultSet.consumeAll(); return (int) (statement.totalChanges() - previousTotalChanges); } @Override public void close() throws SQLException { if (closed) { return; } if (this.statement != null) { this.statement.close(); } closed = true; } @Override public int getMaxFieldSize() throws SQLException { // TODO return 0; } @Override public void setMaxFieldSize(int max) throws SQLException { // TODO } @Override public int getMaxRows() throws SQLException { // TODO return 0; } @Override public void setMaxRows(int max) throws SQLException { // TODO } @Override public void setEscapeProcessing(boolean enable) throws SQLException { // TODO } @Override public int getQueryTimeout() throws SQLException { // TODO return 0; } @Override public void setQueryTimeout(int seconds) throws SQLException { if (seconds < 0) { throw new SQLException("Query timeout must be greater than 0"); } this.queryTimeoutSeconds = seconds; } @Override public void cancel() throws SQLException { // TODO } @Override @SkipNullableCheck public SQLWarning getWarnings() throws SQLException { // TODO return null; } @Override public void clearWarnings() throws SQLException { // TODO } @Override public void setCursorName(String name) throws SQLException { // TODO } /** * The execute method executes an SQL statement and indicates the form of the first * result. You must then use the methods getResultSet or getUpdateCount * to retrieve the result, and getMoreResults to move to any subsequent result(s). */ @Override public boolean execute(String sql) throws SQLException { ensureOpen(); return this.withConnectionTimeout( () -> { try { // TODO: if sql is a readOnly query, do we still need the locks? connectionLock.lock(); statement = connection.prepare(sql); final long previousChanges = statement.totalChanges(); final boolean result = statement.execute(); updateGeneratedKeys(); updateCount = statement.totalChanges() - previousChanges; return result; } finally { connectionLock.unlock(); } }); } @Override public ResultSet getResultSet() throws SQLException { requireNonNull(statement, "statement is null"); ensureOpen(); return new JDBC4ResultSet(statement.getResultSet(), this); } @Override public int getUpdateCount() throws SQLException { return (int) updateCount; } @Override public void setFetchDirection(int direction) throws SQLException { // TODO } @Override public int getFetchDirection() throws SQLException { // TODO return 0; } @Override public void setFetchSize(int rows) throws SQLException { // TODO } @Override public int getFetchSize() throws SQLException { // TODO return 0; } @Override public int getResultSetConcurrency() { return resultSetConcurrency; } @Override public int getResultSetType() { return resultSetType; } @Override public void addBatch(String sql) throws SQLException { ensureOpen(); if (sql == null) { throw new SQLException("SQL command cannot be null"); } batchCommands.add(sql); } @Override public void clearBatch() throws SQLException { ensureOpen(); batchCommands.clear(); } // TODO: let's make this batch operation atomic @Override public int[] executeBatch() throws SQLException { ensureOpen(); int[] updateCounts = new int[batchCommands.size()]; List failedCommands = new ArrayList<>(); // Execute each command in the batch for (int i = 0; i < batchCommands.size(); i++) { String sql = batchCommands.get(i); try { if (!isBatchCompatibleStatement(sql)) { failedCommands.add(sql); updateCounts[i] = EXECUTE_FAILED; BatchUpdateException bue = new BatchUpdateException( "Batch entry " + i + " (" + sql + ") was aborted. " + "Batch commands cannot return result sets.", "HY000", // General error SQL state 0, updateCounts); // Clear the batch after failure clearBatch(); throw bue; } execute(sql); // For DML statements, get the update count updateCounts[i] = getUpdateCount(); } catch (SQLException e) { failedCommands.add(sql); updateCounts[i] = EXECUTE_FAILED; // Create a BatchUpdateException with the partial results BatchUpdateException bue = new BatchUpdateException( "Batch entry " + i + " (" + sql + ") failed: " + e.getMessage(), e.getSQLState(), e.getErrorCode(), updateCounts, e.getCause()); // Clear the batch after failure clearBatch(); throw bue; } } // Clear the batch after successful execution clearBatch(); return updateCounts; } protected boolean isBatchCompatibleStatement(String sql) { if (sql == null || sql.trim().isEmpty()) { return false; } return BATCH_COMPATIBLE_PATTERN.matcher(sql).find(); } @Override public Connection getConnection() { return connection; } @Override public boolean getMoreResults() throws SQLException { return getMoreResults(Statement.CLOSE_CURRENT_RESULT); } @Override public boolean getMoreResults(int current) throws SQLException { requireNonNull(statement, "statement should not be null"); if (current != Statement.CLOSE_CURRENT_RESULT) { throw new SQLException("Invalid argument"); } statement.getResultSet().close(); updateCount = -1; return false; } @Override @SkipNullableCheck public ResultSet getGeneratedKeys() throws SQLException { // TODO return null; } @Override public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { // TODO: enhance return executeUpdate(sql); } @Override public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { // TODO: enhance return executeUpdate(sql); } @Override public int executeUpdate(String sql, String[] columnNames) throws SQLException { // TODO: enhance return executeUpdate(sql); } @Override public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { // TODO: enhance return execute(sql); } @Override public boolean execute(String sql, int[] columnIndexes) throws SQLException { // TODO: enhance return execute(sql); } @Override public boolean execute(String sql, String[] columnNames) throws SQLException { // TODO return false; } @Override public int getResultSetHoldability() { return resultSetHoldability; } @Override public boolean isClosed() throws SQLException { return this.closed; } @Override public void setPoolable(boolean poolable) throws SQLException { // TODO } @Override public boolean isPoolable() throws SQLException { // TODO return false; } @Override public void closeOnCompletion() throws SQLException { if (closed) { throw new SQLException("statement is closed"); } closeOnCompletion = true; } /** * Indicates whether the statement should be closed automatically when all its dependent result * sets are closed. */ @Override public boolean isCloseOnCompletion() throws SQLException { if (closed) { throw new SQLException("statement is closed"); } return closeOnCompletion; } @Override @SkipNullableCheck public T unwrap(Class iface) throws SQLException { // TODO return null; } @Override public boolean isWrapperFor(Class iface) throws SQLException { // TODO return false; } protected void updateGeneratedKeys() throws SQLException { // TODO } private T withConnectionTimeout(SQLCallable callable) throws SQLException { final int originalBusyTimeoutMillis = connection.getBusyTimeout(); if (queryTimeoutSeconds > 0) { // TODO: set busy timeout connection.setBusyTimeout(1000 * queryTimeoutSeconds); } try { return callable.call(); } finally { if (queryTimeoutSeconds > 0) { connection.setBusyTimeout(originalBusyTimeoutMillis); } } } /** * Functional interface for SQL callable operations. * * @param the return type */ @FunctionalInterface protected interface SQLCallable { /** * Executes the SQL operation. * * @return the result of the operation * @throws SQLException if a database access error occurs */ T call() throws SQLException; } private void ensureOpen() throws SQLException { if (closed) { throw new SQLException("Statement is closed"); } } } ================================================ FILE: bindings/java/src/main/java/tech/turso/utils/ByteArrayUtils.java ================================================ package tech.turso.utils; import java.nio.charset.StandardCharsets; import tech.turso.annotations.Nullable; /** Utility class for converting between byte arrays and strings using UTF-8 encoding. */ public final class ByteArrayUtils { /** * Converts a UTF-8 encoded byte array to a string. * * @param buffer the byte array to convert, may be null * @return the string representation, or null if the input is null */ @Nullable public static String utf8ByteBufferToString(@Nullable byte[] buffer) { if (buffer == null) { return null; } return new String(buffer, StandardCharsets.UTF_8); } /** * Converts a string to a UTF-8 encoded byte array. * * @param str the string to convert, may be null * @return the byte array representation, or null if the input is null */ @Nullable public static byte[] stringToUtf8ByteArray(@Nullable String str) { if (str == null) { return null; } return str.getBytes(StandardCharsets.UTF_8); } } ================================================ FILE: bindings/java/src/main/java/tech/turso/utils/Logger.java ================================================ package tech.turso.utils; /** A simple internal Logger interface. */ public interface Logger { void trace(String message, Object... params); void debug(String message, Object... params); void info(String message, Object... params); void warn(String message, Object... params); void error(String message, Object... params); } ================================================ FILE: bindings/java/src/main/java/tech/turso/utils/LoggerFactory.java ================================================ package tech.turso.utils; import java.util.logging.Level; /** * A factory for {@link Logger} instances that uses SLF4J if present, falling back on a * java.util.logging implementation otherwise. */ public final class LoggerFactory { static final boolean USE_SLF4J; static { boolean useSLF4J; try { Class.forName("org.slf4j.Logger"); useSLF4J = true; } catch (Exception e) { useSLF4J = false; } USE_SLF4J = useSLF4J; } /** * Get a {@link Logger} instance for the given host class. * * @param hostClass the host class from which log messages will be issued * @return a Logger */ public static Logger getLogger(Class hostClass) { if (USE_SLF4J) { return new SLF4JLogger(hostClass); } return new JDKLogger(hostClass); } private static class JDKLogger implements Logger { final java.util.logging.Logger logger; public JDKLogger(Class hostClass) { logger = java.util.logging.Logger.getLogger(hostClass.getCanonicalName()); } @Override public void trace(String message, Object... params) { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, String.format(message, params)); } } @Override public void debug(String message, Object... params) { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, String.format(message, params)); } } @Override public void info(String message, Object... params) { if (logger.isLoggable(Level.INFO)) { logger.log(Level.INFO, String.format(message, params)); } } @Override public void warn(String message, Object... params) { if (logger.isLoggable(Level.WARNING)) { logger.log(Level.WARNING, String.format(message, params)); } } @Override public void error(String message, Object... params) { if (logger.isLoggable(Level.SEVERE)) { logger.log(Level.SEVERE, String.format(message, params)); } } } private static class SLF4JLogger implements Logger { final org.slf4j.Logger logger; SLF4JLogger(Class hostClass) { logger = org.slf4j.LoggerFactory.getLogger(hostClass); } @Override public void trace(String message, Object... params) { if (logger.isTraceEnabled()) { logger.trace(message, String.format(message, params)); } } @Override public void debug(String message, Object... params) { if (logger.isDebugEnabled()) { logger.debug(message, String.format(message, params)); } } @Override public void info(String message, Object... params) { if (logger.isInfoEnabled()) { logger.info(message, String.format(message, params)); } } @Override public void warn(String message, Object... params) { if (logger.isWarnEnabled()) { logger.warn(message, String.format(message, params)); } } @Override public void error(String message, Object... params) { if (logger.isErrorEnabled()) { logger.error(message, String.format(message, params)); } } } } ================================================ FILE: bindings/java/src/main/java/tech/turso/utils/TursoExceptionUtils.java ================================================ package tech.turso.utils; import static tech.turso.utils.ByteArrayUtils.utf8ByteBufferToString; import java.sql.SQLException; import tech.turso.TursoErrorCode; import tech.turso.annotations.Nullable; import tech.turso.exceptions.TursoException; public final class TursoExceptionUtils { /** * Throws formatted SQLException with error code and message. * * @param errorCode Error code. * @param errorMessageBytes Error message. */ public static void throwTursoException(int errorCode, byte[] errorMessageBytes) throws SQLException { String errorMessage = utf8ByteBufferToString(errorMessageBytes); throw buildTursoException(errorCode, errorMessage); } /** * Throws formatted SQLException with error code and message. * * @param errorCode Error code. * @param errorMessage Error message. */ public static TursoException buildTursoException(int errorCode, @Nullable String errorMessage) throws SQLException { TursoErrorCode code = TursoErrorCode.getErrorCode(errorCode); String msg; if (code == TursoErrorCode.UNKNOWN_ERROR) { msg = String.format("%s:%s (%s)", code, errorCode, errorMessage); } else { msg = String.format("%s (%s)", code, errorMessage); } return new TursoException(msg, code); } } ================================================ FILE: bindings/java/src/main/resources/META-INF/services/java.sql.Driver ================================================ tech.turso.JDBC ================================================ FILE: bindings/java/src/main/resources/turso-jdbc.properties ================================================ driverName=turso-jdbc # find a way to relate driverVersion with project.version defined in gradle.properties driverVersion=0.0.1-SNAPSHOT ================================================ FILE: bindings/java/src/test/java/tech/turso/IntegrationTest.java ================================================ package tech.turso; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tech.turso.jdbc4.JDBC4Connection; class IntegrationTest { private JDBC4Connection connection; @BeforeEach void setUp() throws Exception { String filePath = TestUtils.createTempFile(); String url = "jdbc:turso:" + filePath; connection = new JDBC4Connection(url, filePath, new Properties()); } @Test void create_table_multi_inserts_select() throws Exception { try (Statement stmt = createDefaultStatement()) { stmt.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT);"); stmt.execute("INSERT INTO users VALUES (1, 'seonwoo');"); stmt.execute("INSERT INTO users VALUES (2, 'seonwoo');"); stmt.execute("INSERT INTO users VALUES (3, 'seonwoo');"); stmt.execute("SELECT * FROM users"); } } private Statement createDefaultStatement() throws SQLException { return connection.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); } } ================================================ FILE: bindings/java/src/test/java/tech/turso/JDBCTest.java ================================================ package tech.turso; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import java.sql.Connection; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; import org.junit.jupiter.api.Test; import tech.turso.jdbc4.JDBC4Connection; class JDBCTest { @Test void null_is_returned_when_invalid_url_is_passed() throws Exception { JDBC4Connection connection = JDBC.createConnection("jdbc:invalid:xxx", new Properties()); assertThat(connection).isNull(); } @Test void non_null_connection_is_returned_when_valid_url_is_passed() throws Exception { String fileUrl = TestUtils.createTempFile(); JDBC4Connection connection = JDBC.createConnection("jdbc:turso:" + fileUrl, new Properties()); assertThat(connection).isNotNull(); } @Test void connection_can_be_retrieved_from_DriverManager() throws SQLException { try (Connection connection = DriverManager.getConnection("jdbc:turso:sample.db")) { assertThat(connection).isNotNull(); } } @Test void retrieve_version() { assertDoesNotThrow(() -> DriverManager.getDriver("jdbc:turso:").getMajorVersion()); assertDoesNotThrow(() -> DriverManager.getDriver("jdbc:turso:").getMinorVersion()); } @Test void all_driver_property_info_should_have_a_description() throws Exception { Driver driver = DriverManager.getDriver("jdbc:turso:"); assertThat(driver.getPropertyInfo(null, null)) .allSatisfy((info) -> assertThat(info.description).isNotNull()); } @Test void return_null_when_protocol_can_not_be_handled() throws Exception { assertThat(JDBC.createConnection("jdbc:unknownprotocol:", null)).isNull(); } } ================================================ FILE: bindings/java/src/test/java/tech/turso/TestUtils.java ================================================ package tech.turso; import java.io.IOException; import java.nio.file.Files; public class TestUtils { /** Create temporary file and returns the path. */ public static String createTempFile() throws IOException { return Files.createTempFile("turso_test_db", null).toAbsolutePath().toString(); } } ================================================ FILE: bindings/java/src/test/java/tech/turso/core/TursoDBTest.java ================================================ package tech.turso.core; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.File; import java.nio.file.Files; import org.junit.jupiter.api.Test; import tech.turso.TestUtils; import tech.turso.TursoErrorCode; import tech.turso.exceptions.TursoException; public class TursoDBTest { @Test void db_should_open_and_close_normally() throws Exception { String dbPath = TestUtils.createTempFile(); TursoDB db = TursoDB.create("jdbc:turso" + dbPath, dbPath); db.close(); assertFalse(db.isOpen()); } @Test void throwJavaException_should_throw_appropriate_java_exception() throws Exception { String dbPath = TestUtils.createTempFile(); TursoDB db = TursoDB.create("jdbc:turso:" + dbPath, dbPath); final int tursoExceptionCode = TursoErrorCode.TURSO_ETC.code; try { db.throwJavaException(tursoExceptionCode); } catch (Exception e) { assertThat(e).isInstanceOf(TursoException.class); TursoException tursoException = (TursoException) e; assertThat(tursoException.getResultCode().code).isEqualTo(tursoExceptionCode); } } @Test void db_should_open_with_encryption() throws Exception { String dbPath = TestUtils.createTempFile(); String hexkey = "b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327"; String wrongKey = "aaaaaaa4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327"; // Create encrypted database TursoDB db = TursoDB.createWithEncryption( "jdbc:turso:" + dbPath, dbPath, TursoEncryptionCipher.AEGIS_256, hexkey); TursoConnection conn = new TursoConnection("jdbc:turso:" + dbPath, db); TursoStatement stmt = conn.prepare("CREATE TABLE t(x TEXT)"); stmt.execute(); stmt.close(); stmt = conn.prepare("INSERT INTO t VALUES ('secret')"); stmt.execute(); stmt.close(); stmt = conn.prepare("PRAGMA wal_checkpoint(truncate)"); stmt.execute(); stmt.close(); conn.close(); db.close(); // Verify data is encrypted on disk byte[] content = Files.readAllBytes(new File(dbPath).toPath()); assertThat(content.length).isGreaterThan(1024); String contentStr = new String(content); assertThat(contentStr).doesNotContain("secret"); // Verify we can re-open with the same key TursoDB db2 = TursoDB.createWithEncryption( "jdbc:turso:" + dbPath, dbPath, TursoEncryptionCipher.AEGIS_256, hexkey); TursoConnection conn2 = new TursoConnection("jdbc:turso:" + dbPath, db2); TursoStatement stmt2 = conn2.prepare("SELECT * FROM t"); boolean hasResult = stmt2.execute(); assertThat(hasResult).isTrue(); TursoResultSet rs = stmt2.getResultSet(); assertThat(rs.get(1)).isEqualTo("secret"); stmt2.close(); conn2.close(); db2.close(); // Verify opening with wrong key fails assertThrows( Exception.class, () -> { TursoDB dbWrongKey = TursoDB.createWithEncryption( "jdbc:turso:" + dbPath, dbPath, TursoEncryptionCipher.AEGIS_256, wrongKey); TursoConnection connWrongKey = new TursoConnection("jdbc:turso:" + dbPath, dbWrongKey); TursoStatement stmtWrongKey = connWrongKey.prepare("SELECT * FROM t"); stmtWrongKey.execute(); }); // Verify opening without encryption fails assertThrows( Exception.class, () -> { TursoDB dbNoEncryption = TursoDB.create("jdbc:turso:" + dbPath, dbPath); TursoConnection connNoEncryption = new TursoConnection("jdbc:turso:" + dbPath, dbNoEncryption); TursoStatement stmtNoEncryption = connNoEncryption.prepare("SELECT * FROM t"); stmtNoEncryption.execute(); }); } } ================================================ FILE: bindings/java/src/test/java/tech/turso/core/TursoStatementTest.java ================================================ package tech.turso.core; import static org.junit.jupiter.api.Assertions.*; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tech.turso.TestUtils; import tech.turso.jdbc4.JDBC4Connection; class TursoStatementTest { private JDBC4Connection connection; @BeforeEach void setUp() throws Exception { String filePath = TestUtils.createTempFile(); String url = "jdbc:turso:" + filePath; connection = new JDBC4Connection(url, filePath, new Properties()); } @Test void closing_statement_closes_related_resources() throws Exception { TursoStatement stmt = connection.prepare("SELECT 1;"); stmt.execute(); stmt.close(); assertTrue(stmt.isClosed()); assertFalse(stmt.getResultSet().isOpen()); } @Test void test_initializeColumnMetadata() throws Exception { runSql("CREATE TABLE users (name TEXT, age INT, country TEXT);"); runSql("INSERT INTO users VALUES ('seonwoo', 30, 'KR');"); final TursoStatement stmt = connection.prepare("SELECT * FROM users"); stmt.initializeColumnMetadata(); final TursoResultSet rs = stmt.getResultSet(); final String[] columnNames = rs.getColumnNames(); assertEquals("name", columnNames[0]); assertEquals("age", columnNames[1]); assertEquals("country", columnNames[2]); } @Test void test_bindNull() throws Exception { runSql("CREATE TABLE test (col1 TEXT);"); TursoStatement stmt = connection.prepare("INSERT INTO test (col1) VALUES (?);"); stmt.bindNull(1); stmt.execute(); stmt.close(); TursoStatement selectStmt = connection.prepare("SELECT col1 FROM test;"); selectStmt.execute(); assertNull(selectStmt.getResultSet().get(1)); selectStmt.close(); } @Test void test_bindLong() throws Exception { runSql("CREATE TABLE test (col1 BIGINT);"); TursoStatement stmt = connection.prepare("INSERT INTO test (col1) VALUES (?);"); stmt.bindLong(1, 123456789L); stmt.execute(); stmt.close(); TursoStatement selectStmt = connection.prepare("SELECT col1 FROM test;"); selectStmt.execute(); assertEquals(123456789L, selectStmt.getResultSet().get(1)); selectStmt.close(); } @Test void test_bindDouble() throws Exception { runSql("CREATE TABLE test (col1 DOUBLE);"); TursoStatement stmt = connection.prepare("INSERT INTO test (col1) VALUES (?);"); stmt.bindDouble(1, 3.14); stmt.execute(); stmt.close(); TursoStatement selectStmt = connection.prepare("SELECT col1 FROM test;"); selectStmt.execute(); assertEquals(3.14, selectStmt.getResultSet().get(1)); selectStmt.close(); } @Test void test_bindText() throws Exception { runSql("CREATE TABLE test (col1 TEXT);"); TursoStatement stmt = connection.prepare("INSERT INTO test (col1) VALUES (?);"); stmt.bindText(1, "hello"); stmt.execute(); stmt.close(); TursoStatement selectStmt = connection.prepare("SELECT col1 FROM test;"); selectStmt.execute(); assertEquals("hello", selectStmt.getResultSet().get(1)); selectStmt.close(); } @Test void test_bindBlob() throws Exception { runSql("CREATE TABLE test (col1 BLOB);"); TursoStatement stmt = connection.prepare("INSERT INTO test (col1) VALUES (?);"); byte[] blob = {1, 2, 3, 4, 5}; stmt.bindBlob(1, blob); stmt.execute(); stmt.close(); TursoStatement selectStmt = connection.prepare("SELECT col1 FROM test;"); selectStmt.execute(); assertArrayEquals(blob, (byte[]) selectStmt.getResultSet().get(1)); selectStmt.close(); } @Test void test_parameterCount() throws Exception { runSql("CREATE TABLE test (col1 INT);"); assertEquals(0, connection.prepare("INSERT INTO test VALUES (1)").parameterCount()); assertEquals(1, connection.prepare("INSERT INTO test VALUES (?)").parameterCount()); assertEquals(2, connection.prepare("INSERT INTO test VALUES (?), (?)").parameterCount()); } private void runSql(String sql) throws Exception { TursoStatement stmt = connection.prepare(sql); while (stmt.execute()) { stmt.execute(); } } } ================================================ FILE: bindings/java/src/test/java/tech/turso/jdbc4/JDBC4ConnectionTest.java ================================================ package tech.turso.jdbc4; import static org.junit.jupiter.api.Assertions.*; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tech.turso.TestUtils; class JDBC4ConnectionTest { private JDBC4Connection connection; @BeforeEach void setUp() throws Exception { String filePath = TestUtils.createTempFile(); String url = "jdbc:turso:" + filePath; connection = new JDBC4Connection(url, filePath, new Properties()); } @Test void test_create_statement_valid() throws SQLException { Statement stmt = connection.createStatement(); assertNotNull(stmt); assertEquals(ResultSet.TYPE_FORWARD_ONLY, stmt.getResultSetType()); assertEquals(ResultSet.CONCUR_READ_ONLY, stmt.getResultSetConcurrency()); assertEquals(ResultSet.CLOSE_CURSORS_AT_COMMIT, stmt.getResultSetHoldability()); } @Test void test_create_statement_with_type_and_concurrency_valid() throws SQLException { Statement stmt = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); assertNotNull(stmt); assertEquals(ResultSet.TYPE_FORWARD_ONLY, stmt.getResultSetType()); assertEquals(ResultSet.CONCUR_READ_ONLY, stmt.getResultSetConcurrency()); } @Test void test_create_statement_with_all_params_valid() throws SQLException { Statement stmt = connection.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); assertNotNull(stmt); assertEquals(ResultSet.TYPE_FORWARD_ONLY, stmt.getResultSetType()); assertEquals(ResultSet.CONCUR_READ_ONLY, stmt.getResultSetConcurrency()); assertEquals(ResultSet.CLOSE_CURSORS_AT_COMMIT, stmt.getResultSetHoldability()); } @Test void test_create_statement_invalid() { assertThrows( SQLException.class, () -> { connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, -1); }); } @Test void prepare_simple_create_table() throws Exception { connection.prepare("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT)"); } @Test void calling_close_multiple_times_throws_no_exception() throws Exception { assertFalse(connection.isClosed()); connection.close(); assertTrue(connection.isClosed()); connection.close(); } @Test void calling_methods_on_closed_connection_should_throw_exception() throws Exception { connection.close(); assertTrue(connection.isClosed()); assertThrows( SQLException.class, () -> connection.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, -1)); } @Test void isValid_should_return_true_on_open_connection() throws SQLException { assertTrue(connection.isValid(10)); } @Test void isValid_should_return_false_on_closed_connection() throws SQLException { connection.close(); assertFalse(connection.isValid(10)); } } ================================================ FILE: bindings/java/src/test/java/tech/turso/jdbc4/JDBC4DatabaseMetaDataTest.java ================================================ package tech.turso.jdbc4; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import tech.turso.TestUtils; class JDBC4DatabaseMetaDataTest { private JDBC4Connection connection; private JDBC4DatabaseMetaData metaData; @BeforeEach void set_up() throws Exception { String filePath = TestUtils.createTempFile(); String url = "jdbc:turso:" + filePath; connection = new JDBC4Connection(url, filePath, new Properties()); metaData = new JDBC4DatabaseMetaData(connection); } @Test void getURL_should_return_non_empty_string() { assertFalse(metaData.getURL().isEmpty()); } @Test void getDriverName_should_not_return_empty_string() { assertFalse(metaData.getDriverName().isEmpty()); } @Test void test_getDriverMajorVersion() { metaData.getDriverMajorVersion(); } @Test void test_getDriverMinorVersion() { metaData.getDriverMinorVersion(); } @Test void getDriverVersion_should_not_return_empty_string() { assertFalse(metaData.getDriverVersion().isEmpty()); } @Test void getTables_with_non_empty_catalog_should_return_empty() throws SQLException { ResultSet rs = metaData.getTables("nonexistent", null, null, null); assertNotNull(rs); assertFalse(rs.next()); rs.close(); } @Test void getTables_with_non_empty_schema_should_return_empty() throws SQLException { ResultSet rs = metaData.getTables(null, "schema", null, null); assertNotNull(rs); assertFalse(rs.next()); rs.close(); } @Test void getTables_should_return_correct_table_info() throws SQLException { try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE TABLE test_table (id INTEGER PRIMARY KEY)"); } ResultSet rs = metaData.getTables(null, null, null, null); assertNotNull(rs); assertTrue(rs.next()); assertEquals("test_table", rs.getString("TABLE_NAME")); assertEquals("TABLE", rs.getString("TABLE_TYPE")); assertFalse(rs.next()); rs.close(); } @Test void getTables_with_pattern_should_filter_results() throws SQLException { // Create test tables try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE TABLE test1 (id INTEGER PRIMARY KEY)"); stmt.execute("CREATE TABLE test2 (id INTEGER PRIMARY KEY)"); stmt.execute("CREATE TABLE other (id INTEGER PRIMARY KEY)"); } ResultSet rs = metaData.getTables(null, null, "test%", null); assertNotNull(rs); int tableCount = 0; while (rs.next()) { String tableName = rs.getString("TABLE_NAME"); assertTrue(tableName.startsWith("test")); tableCount++; } assertEquals(2, tableCount); rs.close(); } @Test @Disabled("CREATE VIEW not supported yet") void getTables_with_type_filter_should_return_only_views() throws SQLException { try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE TABLE my_table (id INTEGER PRIMARY KEY)"); stmt.execute("CREATE VIEW my_view AS SELECT * FROM my_table"); } ResultSet rs = metaData.getTables(null, null, null, new String[] {"VIEW"}); assertNotNull(rs); assertTrue(rs.next()); assertEquals("my_view", rs.getString("TABLE_NAME")); assertEquals("VIEW", rs.getString("TABLE_TYPE")); assertFalse(rs.next()); rs.close(); } @Test @Disabled("CREATE VIEW not supported yet") void getTables_with_pattern_and_type_filter_should_work_together() throws SQLException { try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE TABLE alpha (id INTEGER)"); stmt.execute("CREATE TABLE beta (id INTEGER)"); stmt.execute("CREATE VIEW alpha_view AS SELECT * FROM alpha"); } ResultSet rs = metaData.getTables(null, null, "alpha%", new String[] {"VIEW"}); assertNotNull(rs); assertTrue(rs.next()); assertEquals("alpha_view", rs.getString("TABLE_NAME")); assertEquals("VIEW", rs.getString("TABLE_TYPE")); assertFalse(rs.next()); rs.close(); } } ================================================ FILE: bindings/java/src/test/java/tech/turso/jdbc4/JDBC4PreparedStatementTest.java ================================================ package tech.turso.jdbc4; import static org.junit.jupiter.api.Assertions.*; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.sql.*; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tech.turso.TestUtils; class JDBC4PreparedStatementTest { private JDBC4Connection connection; @BeforeEach void setUp() throws Exception { String filePath = TestUtils.createTempFile(); String url = "jdbc:turso:" + filePath; connection = new JDBC4Connection(url, filePath, new Properties()); } @Test void testSetBoolean() throws SQLException { connection.prepareStatement("CREATE TABLE test (col INTEGER)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); stmt.setBoolean(1, true); stmt.setBoolean(2, false); stmt.setBoolean(3, true); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertTrue(rs.getBoolean(1)); assertTrue(rs.next()); assertFalse(rs.getBoolean(1)); assertTrue(rs.next()); assertTrue(rs.getBoolean(1)); } @Test void testSetByte() throws SQLException { connection.prepareStatement("CREATE TABLE test (col INTEGER)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); stmt.setByte(1, (byte) 1); stmt.setByte(2, (byte) 2); stmt.setByte(3, (byte) 3); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(1, rs.getByte(1)); assertTrue(rs.next()); assertEquals(2, rs.getByte(1)); assertTrue(rs.next()); assertEquals(3, rs.getByte(1)); } @Test void testSetShort() throws SQLException { connection.prepareStatement("CREATE TABLE test (col INTEGER)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); stmt.setShort(1, (short) 1); stmt.setShort(2, (short) 2); stmt.setShort(3, (short) 3); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(1, rs.getShort(1)); assertTrue(rs.next()); assertEquals(2, rs.getShort(1)); assertTrue(rs.next()); assertEquals(3, rs.getShort(1)); } @Test void testSetInt() throws SQLException { connection.prepareStatement("CREATE TABLE test (col INTEGER)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); stmt.setInt(1, 1); stmt.setInt(2, 2); stmt.setInt(3, 3); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(1, rs.getInt(1)); assertTrue(rs.next()); assertEquals(2, rs.getInt(1)); assertTrue(rs.next()); assertEquals(3, rs.getInt(1)); } @Test void testSetLong() throws SQLException { connection.prepareStatement("CREATE TABLE test (col INTEGER)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); stmt.setLong(1, 1L); stmt.setLong(2, 2L); stmt.setLong(3, 3L); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(1L, rs.getLong(1)); assertTrue(rs.next()); assertEquals(2L, rs.getLong(1)); assertTrue(rs.next()); assertEquals(3L, rs.getLong(1)); } @Test void testSetFloat() throws SQLException { connection.prepareStatement("CREATE TABLE test (col REAL)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); stmt.setFloat(1, 1.0f); stmt.setFloat(2, 2.0f); stmt.setFloat(3, 3.0f); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(1.0f, rs.getFloat(1)); assertTrue(rs.next()); assertEquals(2.0f, rs.getFloat(1)); assertTrue(rs.next()); assertEquals(3.0f, rs.getFloat(1)); } @Test void testSetDouble() throws SQLException { connection.prepareStatement("CREATE TABLE test (col REAL)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); stmt.setDouble(1, 1.0); stmt.setDouble(2, 2.0); stmt.setDouble(3, 3.0); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(1.0, rs.getDouble(1)); assertTrue(rs.next()); assertEquals(2.0, rs.getDouble(1)); assertTrue(rs.next()); assertEquals(3.0, rs.getDouble(1)); } @Test void testSetBigDecimal() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); stmt.setBigDecimal(1, new BigDecimal("1.0")); stmt.setBigDecimal(2, new BigDecimal("2.0")); stmt.setBigDecimal(3, new BigDecimal("3.0")); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals("1.0", rs.getString(1)); assertTrue(rs.next()); assertEquals("2.0", rs.getString(1)); assertTrue(rs.next()); assertEquals("3.0", rs.getString(1)); } @Test void testSetString() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); stmt.setString(1, "test1"); stmt.setString(2, "test2"); stmt.setString(3, "test3"); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals("test1", rs.getString(1)); assertTrue(rs.next()); assertEquals("test2", rs.getString(1)); assertTrue(rs.next()); assertEquals("test3", rs.getString(1)); } @Test void testSetBytes() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); stmt.setBytes(1, new byte[] {1, 2, 3}); stmt.setBytes(2, new byte[] {4, 5, 6}); stmt.setBytes(3, new byte[] {7, 8, 9}); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertArrayEquals(new byte[] {1, 2, 3}, rs.getBytes(1)); assertTrue(rs.next()); assertArrayEquals(new byte[] {4, 5, 6}, rs.getBytes(1)); assertTrue(rs.next()); assertArrayEquals(new byte[] {7, 8, 9}, rs.getBytes(1)); } @Test void testSetDate() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); Date date1 = new Date(1000000000000L); Date date2 = new Date(1500000000000L); Date date3 = new Date(2000000000000L); stmt.setDate(1, date1); stmt.setDate(2, date2); stmt.setDate(3, date3); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); JDBC4ResultSet rs = (JDBC4ResultSet) stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(date1, rs.getDate(1)); assertTrue(rs.next()); assertEquals(date2, rs.getDate(1)); assertTrue(rs.next()); assertEquals(date3, rs.getDate(1)); } @Test void testSetTime() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); Time time1 = new Time(1000000000000L); Time time2 = new Time(1500000000000L); Time time3 = new Time(2000000000000L); stmt.setTime(1, time1); stmt.setTime(2, time2); stmt.setTime(3, time3); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); JDBC4ResultSet rs = (JDBC4ResultSet) stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(time1, rs.getTime(1)); assertTrue(rs.next()); assertEquals(time2, rs.getTime(1)); assertTrue(rs.next()); assertEquals(time3, rs.getTime(1)); } @Test void testSetTimestamp() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); Timestamp timestamp1 = new Timestamp(1000000000000L); Timestamp timestamp2 = new Timestamp(1500000000000L); Timestamp timestamp3 = new Timestamp(2000000000000L); stmt.setTimestamp(1, timestamp1); stmt.setTimestamp(2, timestamp2); stmt.setTimestamp(3, timestamp3); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); JDBC4ResultSet rs = (JDBC4ResultSet) stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(timestamp1, rs.getTimestamp(1)); assertTrue(rs.next()); assertEquals(timestamp2, rs.getTimestamp(1)); assertTrue(rs.next()); assertEquals(timestamp3, rs.getTimestamp(1)); } @Test void testInsertMultipleTypes() throws SQLException { connection .prepareStatement("CREATE TABLE test (col1 INTEGER, col2 REAL, col3 TEXT, col4 BLOB)") .execute(); PreparedStatement stmt = connection.prepareStatement( "INSERT INTO test (col1, col2, col3, col4) VALUES (?, ?, ?, ?), (?, ?, ?, ?)"); stmt.setInt(1, 1); stmt.setFloat(2, 1.1f); stmt.setString(3, "row1"); stmt.setBytes(4, new byte[] {1, 2, 3}); stmt.setInt(5, 2); stmt.setFloat(6, 2.2f); stmt.setString(7, "row2"); stmt.setBytes(8, new byte[] {4, 5, 6}); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(1, rs.getInt(1)); assertEquals(1.1f, rs.getFloat(2)); assertEquals("row1", rs.getString(3)); assertArrayEquals(new byte[] {1, 2, 3}, rs.getBytes(4)); assertTrue(rs.next()); assertEquals(2, rs.getInt(1)); assertEquals(2.2f, rs.getFloat(2)); assertEquals("row2", rs.getString(3)); assertArrayEquals(new byte[] {4, 5, 6}, rs.getBytes(4)); } @Test void testSetObjectCoversAllSupportedTypes() throws SQLException { connection .prepareStatement( "CREATE TABLE test (" + "col1 INTEGER, " + "col2 REAL, " + "col3 TEXT, " + "col4 BLOB, " + "col5 INTEGER, " + "col6 TEXT, " + "col7 TEXT, " + "col8 TEXT, " + "col9 TEXT" + ")") .execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); stmt.setObject(1, 42); stmt.setObject(2, 3.141592d); stmt.setObject(3, "string_value"); stmt.setObject(4, new byte[] {1, 2, 3}); stmt.setObject(5, 1L); stmt.setObject(6, java.sql.Date.valueOf("2025-10-30")); stmt.setObject(7, java.sql.Time.valueOf("10:45:00")); stmt.setObject(8, java.sql.Timestamp.valueOf("2025-10-30 10:45:00")); stmt.setObject(9, new java.math.BigDecimal("12345.6789")); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT * FROM test;"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(42, rs.getInt(1)); assertEquals(3.141592d, rs.getDouble(2), 0.000001); assertEquals("string_value", rs.getString(3)); assertArrayEquals(new byte[] {1, 2, 3}, rs.getBytes(4)); assertTrue(rs.getBoolean(5)); assertEquals(java.sql.Date.valueOf("2025-10-30"), rs.getDate(6)); assertEquals(java.sql.Time.valueOf("10:45:00"), rs.getTime(7)); assertEquals(java.sql.Timestamp.valueOf("2025-10-30 10:45:00"), rs.getTimestamp(8)); String decimalText = rs.getString(9); assertEquals( new java.math.BigDecimal("12345.6789").stripTrailingZeros(), new java.math.BigDecimal(decimalText).stripTrailingZeros()); } @Test void testSetAsciiStream_intLength_insert_and_select() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); String text = "test"; byte[] bytes = text.getBytes(StandardCharsets.US_ASCII); InputStream stream = new ByteArrayInputStream(bytes); stmt.setAsciiStream(1, stream, bytes.length); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(text, rs.getString(1)); } @Test void testSetAsciiStream_intLength_nullStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); stmt.setAsciiStream(1, null, 0); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertNull(rs.getString(1)); } @Test void testSetAsciiStream_intLength_emptyStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); InputStream empty = new ByteArrayInputStream(new byte[0]); stmt.setAsciiStream(1, empty, 10); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals("", rs.getString(1)); } @Test void testSetAsciiStream_intLength_negativeLength() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); String text = "test"; byte[] bytes = text.getBytes(StandardCharsets.US_ASCII); InputStream stream = new ByteArrayInputStream(bytes); assertThrows(SQLException.class, () -> stmt.setAsciiStream(1, stream, -1)); } @Test void testSetBinaryStream_intLength_insert_and_select() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); byte[] data = {1, 2, 3, 4, 5}; InputStream stream = new ByteArrayInputStream(data); stmt.setBinaryStream(1, stream, data.length); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertArrayEquals(data, rs.getBytes(1)); } @Test void testSetBinaryStream_intLength_nullStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); stmt.setBinaryStream(1, null, 0); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertNull(rs.getBytes(1)); } @Test void testSetBinaryStream_intLength_emptyStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); InputStream empty = new ByteArrayInputStream(new byte[0]); stmt.setBinaryStream(1, empty, 10); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); byte[] result = rs.getBytes(1); assertNotNull(result); assertEquals(0, result.length); assertArrayEquals(new byte[0], result); } @Test void testSetBinaryStream_intLength_negativeLength() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); byte[] data = {1, 2, 3}; InputStream stream = new ByteArrayInputStream(data); assertThrows(SQLException.class, () -> stmt.setBinaryStream(1, stream, -1)); } @Test void testSetUnicodeStream_intLength_insert_and_select() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); String text = "안녕하세요😊 Hello🌏"; byte[] bytes = text.getBytes(StandardCharsets.UTF_8); InputStream stream = new ByteArrayInputStream(bytes); stmt.setUnicodeStream(1, stream, bytes.length); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(text, rs.getString(1)); } @Test void testSetUnicodeStream_intLength_nullStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); stmt.setUnicodeStream(1, null, 0); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertNull(rs.getString(1)); } @Test void testSetUnicodeStream_intLength_emptyStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); InputStream empty = new ByteArrayInputStream(new byte[0]); stmt.setUnicodeStream(1, empty, 10); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals("", rs.getString(1)); } @Test void testSetUnicodeStream_intLength_negativeLength() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); String text = "테스트"; byte[] bytes = text.getBytes(StandardCharsets.UTF_8); InputStream stream = new ByteArrayInputStream(bytes); assertThrows(SQLException.class, () -> stmt.setUnicodeStream(1, stream, -5)); } @Test void testSetAsciiStream_longLength_insert_and_select() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); String text = "test"; byte[] bytes = text.getBytes(StandardCharsets.US_ASCII); InputStream stream = new ByteArrayInputStream(bytes); stmt.setAsciiStream(1, stream, (long) bytes.length); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(text, rs.getString(1)); } @Test void testSetAsciiStream_longLength_nullStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); stmt.setAsciiStream(1, null, 0L); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertNull(rs.getString(1)); } @Test void testSetAsciiStream_longLength_emptyStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); InputStream empty = new ByteArrayInputStream(new byte[0]); stmt.setAsciiStream(1, empty, 0L); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals("", rs.getString(1)); } @Test void testSetAsciiStream_longLength_negative() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); InputStream stream = new ByteArrayInputStream("test".getBytes(StandardCharsets.US_ASCII)); assertThrows(SQLFeatureNotSupportedException.class, () -> stmt.setAsciiStream(1, stream, -1L)); } @Test void testSetAsciiStream_longLength_overflow() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); InputStream stream = new ByteArrayInputStream("test".getBytes(StandardCharsets.US_ASCII)); assertThrows( SQLFeatureNotSupportedException.class, () -> stmt.setAsciiStream(1, stream, (long) Integer.MAX_VALUE + 1)); } @Test void testSetBinaryStream_longLength_insert_and_select() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); byte[] data = {1, 2, 3, 4, 5}; InputStream stream = new ByteArrayInputStream(data); stmt.setBinaryStream(1, stream, (long) data.length); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertArrayEquals(data, rs.getBytes(1)); } @Test void testSetBinaryStream_longLength_nullStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); stmt.setBinaryStream(1, null, 0L); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertNull(rs.getBytes(1)); } @Test void testSetBinaryStream_longLength_emptyStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); InputStream empty = new ByteArrayInputStream(new byte[0]); stmt.setBinaryStream(1, empty, 0L); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); byte[] result = rs.getBytes(1); assertNotNull(result); assertEquals(0, result.length); assertArrayEquals(new byte[0], result); } @Test void testSetBinaryStream_longLength_negative() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); InputStream stream = new ByteArrayInputStream(new byte[] {1, 2, 3}); assertThrows(SQLFeatureNotSupportedException.class, () -> stmt.setBinaryStream(1, stream, -1L)); } @Test void testSetBinaryStream_longLength_overflow() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); InputStream stream = new ByteArrayInputStream(new byte[] {1, 2, 3}); assertThrows( SQLFeatureNotSupportedException.class, () -> stmt.setBinaryStream(1, stream, (long) Integer.MAX_VALUE + 1)); } @Test void testSetAsciiStream_noLength_insert_and_select() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); String text = "test"; byte[] bytes = text.getBytes(StandardCharsets.US_ASCII); InputStream stream = new ByteArrayInputStream(bytes); stmt.setAsciiStream(1, stream); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(text, rs.getString(1)); } @Test void testSetAsciiStream_noLength_nullStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); stmt.setAsciiStream(1, null); stmt.execute(); ResultSet rs = connection.prepareStatement("SELECT col FROM test").executeQuery(); assertTrue(rs.next()); assertNull(rs.getString(1)); } @Test void testSetAsciiStream_noLength_emptyStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); InputStream empty = new ByteArrayInputStream(new byte[0]); stmt.setAsciiStream(1, empty); stmt.execute(); ResultSet rs = connection.prepareStatement("SELECT col FROM test").executeQuery(); assertTrue(rs.next()); assertEquals("", rs.getString(1)); } @Test void testSetBinaryStream_noLength_insert_and_select() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); byte[] data = {1, 2, 3, 4, 5}; InputStream stream = new ByteArrayInputStream(data); stmt.setBinaryStream(1, stream); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertArrayEquals(data, rs.getBytes(1)); } @Test void testSetBinaryStream_noLength_nullStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); stmt.setBinaryStream(1, null); stmt.execute(); ResultSet rs = connection.prepareStatement("SELECT col FROM test").executeQuery(); assertTrue(rs.next()); assertNull(rs.getBytes(1)); } @Test void testSetBinaryStream_noLength_emptyStream() throws SQLException { connection.prepareStatement("CREATE TABLE test (col BLOB)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); InputStream empty = new ByteArrayInputStream(new byte[0]); stmt.setBinaryStream(1, empty); stmt.execute(); ResultSet rs = connection.prepareStatement("SELECT col FROM test").executeQuery(); assertTrue(rs.next()); byte[] result = rs.getBytes(1); assertNotNull(result); assertEquals(0, result.length); } @Test void testSetCharacterStream_intLength_insert_and_select() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); String text = "test"; Reader reader = new StringReader(text); stmt.setCharacterStream(1, reader, 4); stmt.execute(); ResultSet rs = connection.prepareStatement("SELECT col FROM test").executeQuery(); assertTrue(rs.next()); assertEquals("test", rs.getString(1)); } @Test void testSetCharacterStream_intLength_shorterThanLength() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); Reader reader = new StringReader("test"); stmt.setCharacterStream(1, reader, 5); stmt.execute(); ResultSet rs = connection.prepareStatement("SELECT col FROM test").executeQuery(); assertTrue(rs.next()); assertEquals("test", rs.getString(1)); } @Test void testSetCharacterStream_intLength_zero() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); Reader reader = new StringReader("test"); stmt.setCharacterStream(1, reader, 0); stmt.execute(); ResultSet rs = connection.prepareStatement("SELECT col FROM test").executeQuery(); assertTrue(rs.next()); assertEquals("", rs.getString(1)); } @Test void testSetCharacterStream_intLength_nullReader() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); stmt.setCharacterStream(1, null, 10); stmt.execute(); ResultSet rs = connection.prepareStatement("SELECT col FROM test").executeQuery(); assertTrue(rs.next()); assertNull(rs.getString(1)); } @Test void testSetCharacterStream_intLength_negativeLength() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); Reader reader = new StringReader("text"); assertThrows(SQLException.class, () -> stmt.setCharacterStream(1, reader, -1)); } @Test void testSetCharacterStream_noLength_insert_and_select() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); String text = "test"; Reader reader = new StringReader(text); stmt.setCharacterStream(1, reader); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(text, rs.getString(1)); } @Test void testSetCharacterStream_noLength_nullReader() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); stmt.setCharacterStream(1, null); stmt.execute(); ResultSet rs = connection.prepareStatement("SELECT col FROM test").executeQuery(); assertTrue(rs.next()); assertNull(rs.getString(1)); } @Test void testSetCharacterStream_noLength_emptyReader() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); Reader empty = new StringReader(""); stmt.setCharacterStream(1, empty); stmt.execute(); ResultSet rs = connection.prepareStatement("SELECT col FROM test").executeQuery(); assertTrue(rs.next()); assertEquals("", rs.getString(1)); } @Test void testSetCharacterStream_longLength_insert_and_select() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); String text = "test"; Reader reader = new StringReader(text); stmt.setCharacterStream(1, reader, (long) text.length()); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals(text, rs.getString(1)); } @Test void testSetCharacterStream_longLength_nullReader() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); stmt.setCharacterStream(1, null, 0L); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertNull(rs.getString(1)); } @Test void testSetCharacterStream_longLength_emptyReader() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); Reader empty = new StringReader(""); stmt.setCharacterStream(1, empty, 0L); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals("", rs.getString(1)); } @Test void testSetCharacterStream_longLength_shorterThanLength() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); Reader reader = new StringReader("test"); stmt.setCharacterStream(1, reader, 10L); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals("test", rs.getString(1)); } @Test void testSetCharacterStream_longLength_zero() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); Reader reader = new StringReader("test"); stmt.setCharacterStream(1, reader, 0L); stmt.execute(); PreparedStatement stmt2 = connection.prepareStatement("SELECT col FROM test"); ResultSet rs = stmt2.executeQuery(); assertTrue(rs.next()); assertEquals("", rs.getString(1)); } @Test void testSetCharacterStream_longLength_negative() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); Reader reader = new StringReader("test"); assertThrows( SQLFeatureNotSupportedException.class, () -> stmt.setCharacterStream(1, reader, -1L)); } @Test void testSetCharacterStream_longLength_overflow() throws Exception { connection.prepareStatement("CREATE TABLE test (col TEXT)").execute(); PreparedStatement stmt = connection.prepareStatement("INSERT INTO test (col) VALUES (?)"); Reader reader = new StringReader("test"); assertThrows( SQLFeatureNotSupportedException.class, () -> stmt.setCharacterStream(1, reader, (long) Integer.MAX_VALUE + 1)); } @Test void execute_insert_should_return_number_of_inserted_elements() throws Exception { connection.prepareStatement("CREATE TABLE test (col INTEGER)").execute(); PreparedStatement prepareStatement = connection.prepareStatement("INSERT INTO test (col) VALUES (?), (?), (?)"); prepareStatement.setInt(1, 1); prepareStatement.setInt(2, 2); prepareStatement.setInt(3, 3); assertEquals(prepareStatement.executeUpdate(), 3); } @Test void execute_update_should_return_number_of_updated_elements() throws Exception { connection.prepareStatement("CREATE TABLE test (col INTEGER)").execute(); connection.prepareStatement("INSERT INTO test (col) VALUES (1), (2), (3)").execute(); PreparedStatement preparedStatement = connection.prepareStatement("UPDATE test SET col = ? where col = 1 "); preparedStatement.setInt(1, 4); assertEquals(preparedStatement.executeUpdate(), 1); } @Test void execute_delete_should_return_number_of_deleted_elements() throws Exception { connection.prepareStatement("CREATE TABLE test (col INTEGER)").execute(); connection.prepareStatement("INSERT INTO test (col) VALUES (1), (2), (3)").execute(); PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM test where col = ? "); preparedStatement.setInt(1, 1); assertEquals(preparedStatement.executeUpdate(), 1); } @Test void testBatchInsert() throws Exception { connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO test (col1, col2) VALUES (?, ?)"); preparedStatement.setInt(1, 1); preparedStatement.setInt(2, 2); preparedStatement.addBatch(); preparedStatement.setInt(1, 3); preparedStatement.setInt(2, 4); preparedStatement.addBatch(); assertArrayEquals(new int[] {1, 1}, preparedStatement.executeBatch()); ResultSet rs = connection.prepareStatement("SELECT * FROM test").executeQuery(); assertTrue(rs.next()); assertEquals(1, rs.getInt(1)); assertEquals(2, rs.getInt(2)); assertTrue(rs.next()); assertEquals(3, rs.getInt(1)); assertEquals(4, rs.getInt(2)); assertFalse(rs.next()); } @Test void testBatchUpdate() throws Exception { connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); connection.prepareStatement("INSERT INTO test (col1, col2) VALUES (1, 1), (2, 2)").execute(); PreparedStatement preparedStatement = connection.prepareStatement("UPDATE test SET col2=? WHERE col1=?"); preparedStatement.setInt(1, 5); preparedStatement.setInt(2, 1); preparedStatement.addBatch(); preparedStatement.setInt(1, 6); preparedStatement.setInt(2, 2); preparedStatement.addBatch(); preparedStatement.setInt(1, 7); preparedStatement.setInt(2, 3); preparedStatement.addBatch(); assertArrayEquals(new int[] {1, 1, 0}, preparedStatement.executeBatch()); ResultSet rs = connection.prepareStatement("SELECT * FROM test").executeQuery(); assertTrue(rs.next()); assertEquals(1, rs.getInt(1)); assertEquals(5, rs.getInt(2)); assertTrue(rs.next()); assertEquals(2, rs.getInt(1)); assertEquals(6, rs.getInt(2)); assertFalse(rs.next()); } @Test void testBatchDelete() throws Exception { connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); connection.prepareStatement("INSERT INTO test (col1, col2) VALUES (1, 1), (2, 2)").execute(); PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM test WHERE col1=?"); preparedStatement.setInt(1, 1); preparedStatement.addBatch(); preparedStatement.setInt(1, 4); preparedStatement.addBatch(); assertArrayEquals(new int[] {1, 0}, preparedStatement.executeBatch()); ResultSet rs = connection.prepareStatement("SELECT * FROM test").executeQuery(); assertTrue(rs.next()); assertEquals(2, rs.getInt(1)); assertEquals(2, rs.getInt(2)); assertFalse(rs.next()); } @Test void testBatch_implicitAddBatch_shouldIgnore() throws Exception { connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO test (col1, col2) VALUES (?, ?)"); preparedStatement.setInt(1, 1); preparedStatement.setInt(2, 2); preparedStatement.addBatch(); // we set parameters but don't call addBatch afterward // we should only get a result for the first insert statement to match sqlite-jdbc behavior preparedStatement.setInt(1, 3); preparedStatement.setInt(2, 4); assertArrayEquals(new int[] {1}, preparedStatement.executeBatch()); ResultSet rs = connection.prepareStatement("SELECT * FROM test").executeQuery(); assertTrue(rs.next()); assertEquals(1, rs.getInt(1)); assertEquals(2, rs.getInt(2)); assertFalse(rs.next()); } @Test void testBatch_select_shouldFail() throws Exception { connection.prepareStatement("CREATE TABLE test (col1 INTEGER, col2 INTEGER)").execute(); PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM test WHERE col1=?"); preparedStatement.setInt(1, 1); preparedStatement.addBatch(); assertThrows(BatchUpdateException.class, preparedStatement::executeBatch); } } ================================================ FILE: bindings/java/src/test/java/tech/turso/jdbc4/JDBC4ResultSetTest.java ================================================ package tech.turso.jdbc4; import static org.junit.jupiter.api.Assertions.*; import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; import java.math.RoundingMode; import java.nio.ByteBuffer; import java.sql.*; import java.util.Calendar; import java.util.Date; import java.util.Properties; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import tech.turso.TestUtils; class JDBC4ResultSetTest { private Statement stmt; @BeforeEach void setUp() throws Exception { String filePath = TestUtils.createTempFile(); String url = "jdbc:turso:" + filePath; final JDBC4Connection connection = new JDBC4Connection(url, filePath, new Properties()); stmt = connection.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); } @Test void invoking_next_before_the_last_row_should_return_true() throws Exception { stmt.executeUpdate("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT);"); stmt.executeUpdate("INSERT INTO users VALUES (1, 'sinwoo');"); stmt.executeUpdate("INSERT INTO users VALUES (2, 'seonwoo');"); // first call to next occur internally stmt.executeQuery("SELECT * FROM users"); ResultSet resultSet = stmt.getResultSet(); assertTrue(resultSet.next()); } @Test void invoking_next_after_the_last_row_should_return_false() throws Exception { stmt.executeUpdate("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT);"); stmt.executeUpdate("INSERT INTO users VALUES (1, 'sinwoo');"); stmt.executeUpdate("INSERT INTO users VALUES (2, 'seonwoo');"); // first call to next occur internally stmt.executeQuery("SELECT * FROM users"); ResultSet resultSet = stmt.getResultSet(); while (resultSet.next()) { // run until next() returns false } // if the previous call to next() returned false, consecutive call to next() should return false // as well assertFalse(resultSet.next()); } @Test void close_resultSet_test() throws Exception { stmt.executeQuery("SELECT 1;"); ResultSet resultSet = stmt.getResultSet(); assertFalse(resultSet.isClosed()); resultSet.close(); assertTrue(resultSet.isClosed()); } @Test void calling_methods_on_closed_resultSet_should_throw_exception() throws Exception { stmt.executeQuery("SELECT 1;"); ResultSet resultSet = stmt.getResultSet(); resultSet.close(); assertTrue(resultSet.isClosed()); assertThrows(SQLException.class, resultSet::next); } @Test void test_getString() throws Exception { stmt.executeUpdate("CREATE TABLE test_string (string_col TEXT);"); stmt.executeUpdate("INSERT INTO test_string (string_col) VALUES ('test');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_string"); assertTrue(resultSet.next()); assertEquals("test", resultSet.getString(1)); } @Test void test_getString_returns_null_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (string_col TEXT);"); stmt.executeUpdate("INSERT INTO test_null (string_col) VALUES (NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); assertTrue(resultSet.next()); assertNull(resultSet.getString(1)); } @Test void test_getBoolean_true() throws Exception { stmt.executeUpdate("CREATE TABLE test_boolean (boolean_col INTEGER);"); stmt.executeUpdate("INSERT INTO test_boolean (boolean_col) VALUES (1);"); stmt.executeUpdate("INSERT INTO test_boolean (boolean_col) VALUES (2);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_boolean"); assertTrue(resultSet.next()); assertTrue(resultSet.getBoolean(1)); resultSet.next(); assertTrue(resultSet.getBoolean(1)); } @Test void test_getBoolean_false() throws Exception { stmt.executeUpdate("CREATE TABLE test_boolean (boolean_col INTEGER);"); stmt.executeUpdate("INSERT INTO test_boolean (boolean_col) VALUES (0);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_boolean"); assertTrue(resultSet.next()); assertFalse(resultSet.getBoolean(1)); } @Test void test_getBoolean_returns_false_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (boolean_col INTEGER);"); stmt.executeUpdate("INSERT INTO test_null (boolean_col) VALUES (NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); assertTrue(resultSet.next()); assertFalse(resultSet.getBoolean(1)); } @Test void test_getByte() throws Exception { stmt.executeUpdate("CREATE TABLE test_byte (byte_col INTEGER);"); stmt.executeUpdate("INSERT INTO test_byte (byte_col) VALUES (1);"); stmt.executeUpdate("INSERT INTO test_byte (byte_col) VALUES (128);"); // Exceeds byte size stmt.executeUpdate("INSERT INTO test_byte (byte_col) VALUES (-129);"); // Exceeds byte size ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_byte"); // Test value that fits within byte size assertTrue(resultSet.next()); assertEquals(1, resultSet.getByte(1)); // Test value that exceeds byte size (positive overflow) assertTrue(resultSet.next()); assertEquals(-128, resultSet.getByte(1)); // 128 overflows to -128 // Test value that exceeds byte size (negative overflow) assertTrue(resultSet.next()); assertEquals(127, resultSet.getByte(1)); // -129 overflows to 127 } @Test void test_getByte_returns_zero_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (byte_col INTEGER);"); stmt.executeUpdate("INSERT INTO test_null (byte_col) VALUES (NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); assertTrue(resultSet.next()); assertEquals(0, resultSet.getByte(1)); } @Test void test_getShort() throws Exception { stmt.executeUpdate("CREATE TABLE test_short (short_col INTEGER);"); stmt.executeUpdate("INSERT INTO test_short (short_col) VALUES (123);"); stmt.executeUpdate("INSERT INTO test_short (short_col) VALUES (32767);"); // Max short value stmt.executeUpdate("INSERT INTO test_short (short_col) VALUES (-32768);"); // Min short value ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_short"); // Test typical short value assertTrue(resultSet.next()); assertEquals(123, resultSet.getShort(1)); // Test maximum short value assertTrue(resultSet.next()); assertEquals(32767, resultSet.getShort(1)); // Test minimum short value assertTrue(resultSet.next()); assertEquals(-32768, resultSet.getShort(1)); } @Test void test_getShort_returns_zero_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (short_col INTEGER);"); stmt.executeUpdate("INSERT INTO test_null (short_col) VALUES (NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); assertTrue(resultSet.next()); assertEquals(0, resultSet.getShort(1)); } @Test void test_getInt() throws Exception { stmt.executeUpdate("CREATE TABLE test_int (int_col INT);"); stmt.executeUpdate("INSERT INTO test_int (int_col) VALUES (12345);"); stmt.executeUpdate("INSERT INTO test_int (int_col) VALUES (2147483647);"); // Max int value stmt.executeUpdate("INSERT INTO test_int (int_col) VALUES (-2147483648);"); // Min int value ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_int"); // Test typical int value assertTrue(resultSet.next()); assertEquals(12345, resultSet.getInt(1)); // Test maximum int value assertTrue(resultSet.next()); assertEquals(2147483647, resultSet.getInt(1)); // Test minimum int value assertTrue(resultSet.next()); assertEquals(-2147483648, resultSet.getInt(1)); } @Test void test_getInt_returns_zero_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (int_col INTEGER);"); stmt.executeUpdate("INSERT INTO test_null (int_col) VALUES (NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); assertTrue(resultSet.next()); assertEquals(0, resultSet.getInt(1)); } @Test void test_getLong() throws Exception { stmt.executeUpdate("CREATE TABLE test_long (long_col BIGINT);"); stmt.executeUpdate("INSERT INTO test_long (long_col) VALUES (1234567890);"); stmt.executeUpdate( "INSERT INTO test_long (long_col) VALUES (9223372036854775807);"); // Max long value stmt.executeUpdate( "INSERT INTO test_long (long_col) VALUES (-9223372036854775808);"); // Min long value ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_long"); // Test typical long value assertTrue(resultSet.next()); assertEquals(1234567890L, resultSet.getLong(1)); // Test maximum long value assertTrue(resultSet.next()); assertEquals(9223372036854775807L, resultSet.getLong(1)); // Test minimum long value assertTrue(resultSet.next()); assertEquals(-9223372036854775808L, resultSet.getLong(1)); } @Test void test_getLong_returns_zero_no_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (long_col INTEGER);"); stmt.executeUpdate("INSERT INTO test_null (long_col) VALUES (NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); assertTrue(resultSet.next()); assertEquals(0L, resultSet.getLong(1)); } @Test void test_getFloat() throws Exception { stmt.executeUpdate("CREATE TABLE test_float (float_col REAL);"); stmt.executeUpdate("INSERT INTO test_float (float_col) VALUES (1.23);"); stmt.executeUpdate( "INSERT INTO test_float (float_col) VALUES (3.4028235E38);"); // Max float value stmt.executeUpdate( "INSERT INTO test_float (float_col) VALUES (1.4E-45);"); // Min positive float value stmt.executeUpdate( "INSERT INTO test_float (float_col) VALUES (-3.4028235E38);"); // Min negative float value ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_float"); // Test typical float value assertTrue(resultSet.next()); assertEquals(1.23f, resultSet.getFloat(1), 0.0001); // Test maximum float value assertTrue(resultSet.next()); assertEquals(3.4028235E38f, resultSet.getFloat(1), 0.0001); // Test minimum positive float value assertTrue(resultSet.next()); assertEquals(1.4E-45f, resultSet.getFloat(1), 0.0001); // Test minimum negative float value assertTrue(resultSet.next()); assertEquals(-3.4028235E38f, resultSet.getFloat(1), 0.0001); } @Test void test_getFloat_returns_zero_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (float_col REAL);"); stmt.executeUpdate("INSERT INTO test_null (float_col) VALUES (NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); assertTrue(resultSet.next()); assertEquals(0.0f, resultSet.getFloat(1), 0.0001); } @Test void test_getDouble() throws Exception { stmt.executeUpdate("CREATE TABLE test_double (double_col REAL);"); stmt.executeUpdate("INSERT INTO test_double (double_col) VALUES (1.234567);"); stmt.executeUpdate( "INSERT INTO test_double (double_col) VALUES (1.7976931348623157E308);"); // Max double // value stmt.executeUpdate( "INSERT INTO test_double (double_col) VALUES (4.9E-324);"); // Min positive double value stmt.executeUpdate( "INSERT INTO test_double (double_col) VALUES (-1.7976931348623157E308);"); // Min negative // double value ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_double"); // Test typical double value assertTrue(resultSet.next()); assertEquals(1.234567, resultSet.getDouble(1), 0.0001); // Test maximum double value assertTrue(resultSet.next()); assertEquals(1.7976931348623157E308, resultSet.getDouble(1), 0.0001); // Test minimum positive double value assertTrue(resultSet.next()); assertEquals(4.9E-324, resultSet.getDouble(1), 0.0001); // Test minimum negative double value assertTrue(resultSet.next()); assertEquals(-1.7976931348623157E308, resultSet.getDouble(1), 0.0001); } @Test void test_getDouble_returns_zero_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (double_col REAL);"); stmt.executeUpdate("INSERT INTO test_null (double_col) VALUES (NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); assertTrue(resultSet.next()); assertEquals(0.0, resultSet.getDouble(1), 0.0001); } @Test void test_getBigDecimal() throws Exception { stmt.executeUpdate("CREATE TABLE test_bigdecimal (bigdecimal_col REAL);"); stmt.executeUpdate("INSERT INTO test_bigdecimal (bigdecimal_col) VALUES (12345.67);"); stmt.executeUpdate( "INSERT INTO test_bigdecimal (bigdecimal_col) VALUES (1.7976931348623157E308);"); // Max // double // value stmt.executeUpdate( "INSERT INTO test_bigdecimal (bigdecimal_col) VALUES (4.9E-324);"); // Min positive double // value stmt.executeUpdate( "INSERT INTO test_bigdecimal (bigdecimal_col) VALUES (-12345.67);"); // Negative value ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_bigdecimal"); // Test typical BigDecimal value assertTrue(resultSet.next()); assertEquals( new BigDecimal("12345.67").setScale(2, RoundingMode.HALF_UP), resultSet.getBigDecimal(1, 2)); // Test maximum double value assertTrue(resultSet.next()); assertEquals( new BigDecimal("1.7976931348623157E308").setScale(10, RoundingMode.HALF_UP), resultSet.getBigDecimal(1, 10)); // Test minimum positive double value assertTrue(resultSet.next()); assertEquals( new BigDecimal("4.9E-324").setScale(10, RoundingMode.HALF_UP), resultSet.getBigDecimal(1, 10)); // Test negative BigDecimal value assertTrue(resultSet.next()); assertEquals( new BigDecimal("-12345.67").setScale(2, RoundingMode.HALF_UP), resultSet.getBigDecimal(1, 2)); } @Test void test_getBigDecimal_returns_null_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (bigdecimal_col REAL);"); stmt.executeUpdate("INSERT INTO test_null (bigdecimal_col) VALUES (NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); assertTrue(resultSet.next()); assertNull(resultSet.getBigDecimal(1, 2)); } @ParameterizedTest @MethodSource("byteArrayProvider") void test_getBytes(byte[] data) throws Exception { stmt.executeUpdate("CREATE TABLE test_bytes (bytes_col BLOB);"); executeDMLAndAssert(data); } private static Stream byteArrayProvider() { return Stream.of( "Hello".getBytes(), "world".getBytes(), new byte[0], new byte[] {0x00, (byte) 0xFF}); } private void executeDMLAndAssert(byte[] data) throws SQLException { // Convert byte array to hexadecimal string StringBuilder hexString = new StringBuilder(); for (byte b : data) { hexString.append(String.format("%02X", b)); } // Execute DML statement stmt.executeUpdate("INSERT INTO test_bytes (bytes_col) VALUES (X'" + hexString + "');"); // Assert the inserted data ResultSet resultSet = stmt.executeQuery("SELECT bytes_col FROM test_bytes"); assertTrue(resultSet.next()); assertArrayEquals(data, resultSet.getBytes(1)); } @Test void test_getBytes_returns_null_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (bytes_col BLOB);"); stmt.executeUpdate("INSERT INTO test_null (bytes_col) VALUES (NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); assertTrue(resultSet.next()); assertNull(resultSet.getBytes(1)); } @Test void test_getXXX_methods_on_multiple_columns() throws Exception { stmt.executeUpdate( "CREATE TABLE test_integration (" + "string_col TEXT, " + "boolean_col INTEGER, " + "byte_col INTEGER, " + "short_col INTEGER, " + "int_col INTEGER, " + "long_col BIGINT, " + "float_col REAL, " + "double_col REAL, " + "bigdecimal_col REAL, " + "bytes_col BLOB);"); stmt.executeUpdate( "INSERT INTO test_integration VALUES (" + "'test', " + "1, " + "1, " + "123, " + "12345, " + "1234567890, " + "1.23, " + "1.234567, " + "12345.67, " + "X'48656C6C6F');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_integration"); assertTrue(resultSet.next()); // Verify each column assertEquals("test", resultSet.getString(1)); assertTrue(resultSet.getBoolean(2)); assertEquals(1, resultSet.getByte(3)); assertEquals(123, resultSet.getShort(4)); assertEquals(12345, resultSet.getInt(5)); assertEquals(1234567890L, resultSet.getLong(6)); assertEquals(1.23f, resultSet.getFloat(7), 0.0001); assertEquals(1.234567, resultSet.getDouble(8), 0.0001); assertEquals( new BigDecimal("12345.67").setScale(2, RoundingMode.HALF_UP), resultSet.getBigDecimal(9, 2)); assertArrayEquals("Hello".getBytes(), resultSet.getBytes(10)); } @Test void test_invalidColumnIndex_outOfBounds() throws Exception { stmt.executeUpdate("CREATE TABLE test_invalid (col INTEGER);"); stmt.executeUpdate("INSERT INTO test_invalid (col) VALUES (1);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_invalid"); assertTrue(resultSet.next()); // Test out-of-bounds column index assertThrows(SQLException.class, () -> resultSet.getInt(2)); } @Test void test_invalidColumnIndex_negative() throws Exception { stmt.executeUpdate("CREATE TABLE test_invalid (col INTEGER);"); stmt.executeUpdate("INSERT INTO test_invalid (col) VALUES (1);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_invalid"); assertTrue(resultSet.next()); // Test negative column index assertThrows(SQLException.class, () -> resultSet.getInt(-1)); } @Test void test_findColumn_with_exact_name() throws Exception { stmt.executeUpdate("CREATE TABLE users (id INTEGER, username TEXT, age INTEGER);"); stmt.executeUpdate("INSERT INTO users VALUES (1, 'minseok', 30);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM users"); assertTrue(resultSet.next()); assertEquals(1, resultSet.findColumn("id")); assertEquals(2, resultSet.findColumn("username")); assertEquals(3, resultSet.findColumn("age")); } @Test void test_findColumn_with_duplicate_names() throws Exception { // SQLite allows duplicate column names in SELECT stmt.executeUpdate("CREATE TABLE test (a INTEGER, b INTEGER);"); stmt.executeUpdate("INSERT INTO test VALUES (1, 2);"); ResultSet resultSet = stmt.executeQuery("SELECT a, a FROM test"); assertTrue(resultSet.next()); // Should return the FIRST occurrence assertEquals(1, resultSet.findColumn("a")); } @Test void test_findColumn_with_nonexistent_column() throws Exception { stmt.executeUpdate("CREATE TABLE users (id INTEGER);"); stmt.executeUpdate("INSERT INTO users VALUES (1);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM users"); assertTrue(resultSet.next()); SQLException exception = assertThrows(SQLException.class, () -> resultSet.findColumn("nonexistent")); assertEquals("column name nonexistent not found", exception.getMessage()); } @Test void test_findColumn_with_alias() throws Exception { stmt.executeUpdate("CREATE TABLE users (id INTEGER);"); stmt.executeUpdate("INSERT INTO users VALUES (1);"); ResultSet resultSet = stmt.executeQuery("SELECT id AS user_id FROM users"); assertTrue(resultSet.next()); // Should find by alias, not original column name assertEquals(1, resultSet.findColumn("user_id")); SQLException exception = assertThrows(SQLException.class, () -> resultSet.findColumn("id")); assertEquals("column name id not found", exception.getMessage()); } @Test void test_findColumn_with_empty_string() throws Exception { stmt.executeUpdate("CREATE TABLE test (col INTEGER);"); stmt.executeUpdate("INSERT INTO test VALUES (1);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test"); assertTrue(resultSet.next()); SQLException exception = assertThrows(SQLException.class, () -> resultSet.findColumn("")); assertEquals("column name not found", exception.getMessage()); } @Test void test_findColumn_with_special_characters() throws Exception { // SQLite allows spaces and special chars in column names if quoted stmt.executeUpdate("CREATE TABLE test ([user name] TEXT, [user-id] INTEGER);"); stmt.executeUpdate("INSERT INTO test VALUES ('minseok', 1);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test"); assertTrue(resultSet.next()); assertEquals(1, resultSet.findColumn("user name")); assertEquals(2, resultSet.findColumn("user-id")); } @Test void test_getCharacterStream() throws Exception { stmt.executeUpdate("CREATE TABLE test_char_stream (text_col TEXT);"); stmt.executeUpdate("INSERT INTO test_char_stream (text_col) VALUES ('Hello World');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_char_stream"); assertTrue(resultSet.next()); Reader reader = resultSet.getCharacterStream(1); char[] buffer = new char[11]; int charsRead = reader.read(buffer); assertEquals(11, charsRead); assertEquals("Hello World", new String(buffer)); } @Test void test_getCharacterStream_with_columnLabel() throws Exception { stmt.executeUpdate("CREATE TABLE test_char_stream (text_col TEXT);"); stmt.executeUpdate("INSERT INTO test_char_stream (text_col) VALUES ('Test Data');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_char_stream"); assertTrue(resultSet.next()); Reader reader = resultSet.getCharacterStream("text_col"); char[] buffer = new char[9]; reader.read(buffer); assertEquals("Test Data", new String(buffer)); } @Test void test_getCharacterStream_returns_null_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null (text_col TEXT);"); stmt.executeUpdate("INSERT INTO test_null (text_col) VALUES (NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null"); assertTrue(resultSet.next()); assertNull(resultSet.getCharacterStream(1)); } @Test void test_getBigDecimal_with_columnLabel() throws Exception { stmt.executeUpdate("CREATE TABLE test_bigdecimal (amount REAL);"); stmt.executeUpdate("INSERT INTO test_bigdecimal (amount) VALUES (12345.67);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_bigdecimal"); assertTrue(resultSet.next()); assertEquals(BigDecimal.valueOf(12345.67), resultSet.getBigDecimal("amount")); } @Test void test_isBeforeFirst_and_isAfterLast() throws Exception { stmt.executeUpdate("CREATE TABLE test_position (id INTEGER);"); stmt.executeUpdate("INSERT INTO test_position VALUES (1);"); stmt.executeUpdate("INSERT INTO test_position VALUES (2);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_position"); // Before first row assertTrue(resultSet.isBeforeFirst()); assertFalse(resultSet.isAfterLast()); // First row resultSet.next(); assertFalse(resultSet.isBeforeFirst()); assertFalse(resultSet.isAfterLast()); // Second row resultSet.next(); assertFalse(resultSet.isBeforeFirst()); assertFalse(resultSet.isAfterLast()); // After last row resultSet.next(); assertFalse(resultSet.isBeforeFirst()); assertTrue(resultSet.isAfterLast()); } @Test void test_isBeforeFirst_with_empty_resultSet() throws Exception { stmt.executeUpdate("CREATE TABLE test_empty (id INTEGER);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_empty"); // After calling next() on empty ResultSet assertFalse(resultSet.next()); assertFalse(resultSet.isBeforeFirst()); assertTrue(resultSet.isAfterLast()); } @Test void test_getRow() throws Exception { stmt.executeUpdate("CREATE TABLE test_row (id INTEGER);"); stmt.executeUpdate("INSERT INTO test_row VALUES (1);"); stmt.executeUpdate("INSERT INTO test_row VALUES (2);"); stmt.executeUpdate("INSERT INTO test_row VALUES (3);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_row"); // Before first row assertEquals(0, resultSet.getRow()); // First row resultSet.next(); assertEquals(1, resultSet.getRow()); // Second row resultSet.next(); assertEquals(2, resultSet.getRow()); // Third row resultSet.next(); assertEquals(3, resultSet.getRow()); // After last row resultSet.next(); assertEquals(3, resultSet.getRow()); } @Test void test_getDate_with_calendar() throws Exception { stmt.executeUpdate("CREATE TABLE test_date_cal (date_col BLOB);"); // 2025-10-07 03:00:00 UTC in milliseconds long utcTime = 1728270000000L; byte[] timeBytes = ByteBuffer.allocate(Long.BYTES).putLong(utcTime).array(); StringBuilder hexString = new StringBuilder(); for (byte b : timeBytes) { hexString.append(String.format("%02X", b)); } stmt.executeUpdate("INSERT INTO test_date_cal (date_col) VALUES (X'" + hexString + "');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_date_cal"); assertTrue(resultSet.next()); // Get date with UTC calendar Calendar utcCal = Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); Date utcDate = resultSet.getDate(1, utcCal); // Get date with Seoul calendar (UTC+9) Calendar seoulCal = Calendar.getInstance(java.util.TimeZone.getTimeZone("Asia/Seoul")); Date seoulDate = resultSet.getDate(1, seoulCal); // Seoul time should be 9 hours ahead long timeDiff = seoulDate.getTime() - utcDate.getTime(); assertEquals(9 * 60 * 60 * 1000, timeDiff); } @Test void test_getDate_with_calendar_columnLabel() throws Exception { stmt.executeUpdate("CREATE TABLE test_date_cal (created_at BLOB);"); // 2025-10-07 03:00:00 UTC in milliseconds long utcTime = 1728270000000L; byte[] timeBytes = ByteBuffer.allocate(Long.BYTES).putLong(utcTime).array(); StringBuilder hexString = new StringBuilder(); for (byte b : timeBytes) { hexString.append(String.format("%02X", b)); } stmt.executeUpdate("INSERT INTO test_date_cal (created_at) VALUES (X'" + hexString + "');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_date_cal"); assertTrue(resultSet.next()); Calendar utcCal = Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); Date date = resultSet.getDate("created_at", utcCal); assertNotNull(date); } @Test void test_getTime_with_calendar() throws Exception { stmt.executeUpdate("CREATE TABLE test_time_cal (time_col BLOB);"); // 2025-10-07 03:00:00 UTC in milliseconds long utcTime = 1728270000000L; byte[] timeBytes = ByteBuffer.allocate(Long.BYTES).putLong(utcTime).array(); StringBuilder hexString = new StringBuilder(); for (byte b : timeBytes) { hexString.append(String.format("%02X", b)); } stmt.executeUpdate("INSERT INTO test_time_cal (time_col) VALUES (X'" + hexString + "');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_time_cal"); assertTrue(resultSet.next()); // Get time with UTC calendar Calendar utcCal = Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); Time utcTime2 = resultSet.getTime(1, utcCal); // Get time with Seoul calendar (UTC+9) Calendar seoulCal = Calendar.getInstance(java.util.TimeZone.getTimeZone("Asia/Seoul")); Time seoulTime = resultSet.getTime(1, seoulCal); // Seoul time should be 9 hours ahead long timeDiff = seoulTime.getTime() - utcTime2.getTime(); assertEquals(9 * 60 * 60 * 1000, timeDiff); } @Test void test_getTime_with_calendar_columnLabel() throws Exception { stmt.executeUpdate("CREATE TABLE test_time_cal (created_at BLOB);"); // 2025-10-07 03:00:00 UTC in milliseconds long utcTime = 1728270000000L; byte[] timeBytes = ByteBuffer.allocate(Long.BYTES).putLong(utcTime).array(); StringBuilder hexString = new StringBuilder(); for (byte b : timeBytes) { hexString.append(String.format("%02X", b)); } stmt.executeUpdate("INSERT INTO test_time_cal (created_at) VALUES (X'" + hexString + "');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_time_cal"); assertTrue(resultSet.next()); Calendar utcCal = Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); Time time = resultSet.getTime("created_at", utcCal); assertNotNull(time); } @Test void test_getTimestamp_with_calendar() throws Exception { stmt.executeUpdate("CREATE TABLE test_timestamp_cal (timestamp_col BLOB);"); // 2025-10-07 03:00:00 UTC in milliseconds long utcTime = 1728270000000L; byte[] timeBytes = ByteBuffer.allocate(Long.BYTES).putLong(utcTime).array(); StringBuilder hexString = new StringBuilder(); for (byte b : timeBytes) { hexString.append(String.format("%02X", b)); } stmt.executeUpdate( "INSERT INTO test_timestamp_cal (timestamp_col) VALUES (X'" + hexString + "');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_timestamp_cal"); assertTrue(resultSet.next()); // Get timestamp with UTC calendar Calendar utcCal = Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); Timestamp utcTimestamp = resultSet.getTimestamp(1, utcCal); // Get timestamp with Seoul calendar (UTC+9) Calendar seoulCal = Calendar.getInstance(java.util.TimeZone.getTimeZone("Asia/Seoul")); Timestamp seoulTimestamp = resultSet.getTimestamp(1, seoulCal); // Seoul time should be 9 hours ahead long timeDiff = seoulTimestamp.getTime() - utcTimestamp.getTime(); assertEquals(9 * 60 * 60 * 1000, timeDiff); } @Test void test_getTimestamp_with_calendar_columnLabel() throws Exception { stmt.executeUpdate("CREATE TABLE test_timestamp_cal (created_at BLOB);"); // 2025-10-07 03:00:00 UTC in milliseconds long utcTime = 1728270000000L; byte[] timeBytes = ByteBuffer.allocate(Long.BYTES).putLong(utcTime).array(); StringBuilder hexString = new StringBuilder(); for (byte b : timeBytes) { hexString.append(String.format("%02X", b)); } stmt.executeUpdate( "INSERT INTO test_timestamp_cal (created_at) VALUES (X'" + hexString + "');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_timestamp_cal"); assertTrue(resultSet.next()); Calendar utcCal = Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); Timestamp timestamp = resultSet.getTimestamp("created_at", utcCal); assertNotNull(timestamp); } @Test void test_wasNull() throws Exception { stmt.executeUpdate("CREATE TABLE test_was_null (id INTEGER, name TEXT);"); stmt.executeUpdate("INSERT INTO test_was_null VALUES (1, 'test');"); stmt.executeUpdate("INSERT INTO test_was_null VALUES (NULL, NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_was_null"); // First row - non-null values assertTrue(resultSet.next()); int id = resultSet.getInt(1); assertFalse(resultSet.wasNull()); String name = resultSet.getString(2); assertFalse(resultSet.wasNull()); // Second row - null values assertTrue(resultSet.next()); int nullInt = resultSet.getInt(1); assertTrue(resultSet.wasNull()); assertEquals(0, nullInt); String nullString = resultSet.getString(2); assertTrue(resultSet.wasNull()); assertNull(nullString); } @Test void test_columnLabel_getters() throws Exception { stmt.executeUpdate( "CREATE TABLE test_column_label (" + "bool_col INTEGER, " + "byte_col INTEGER, " + "short_col INTEGER, " + "int_col INTEGER, " + "long_col BIGINT, " + "float_col REAL, " + "double_col REAL, " + "bytes_col BLOB);"); stmt.executeUpdate( "INSERT INTO test_column_label VALUES (" + "1, 127, 32767, 2147483647, 9223372036854775807, 3.14, 2.718281828, X'48656C6C6F');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_column_label"); assertTrue(resultSet.next()); // Test columnLabel-based getters assertTrue(resultSet.getBoolean("bool_col")); assertEquals(127, resultSet.getByte("byte_col")); assertEquals(32767, resultSet.getShort("short_col")); assertEquals(2147483647, resultSet.getInt("int_col")); assertEquals(9223372036854775807L, resultSet.getLong("long_col")); assertEquals(3.14f, resultSet.getFloat("float_col"), 0.001); assertEquals(2.718281828, resultSet.getDouble("double_col"), 0.000001); assertArrayEquals("Hello".getBytes(), resultSet.getBytes("bytes_col")); } @Test void test_getObject_with_columnLabel() throws Exception { stmt.executeUpdate("CREATE TABLE test_object (id INTEGER, name TEXT);"); stmt.executeUpdate("INSERT INTO test_object VALUES (42, 'test');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_object"); assertTrue(resultSet.next()); Object idObj = resultSet.getObject("id"); assertEquals(42L, idObj); assertFalse(resultSet.wasNull()); Object nameObj = resultSet.getObject("name"); assertEquals("test", nameObj); assertFalse(resultSet.wasNull()); } @Test void test_getBigDecimal_with_scale_columnLabel() throws Exception { stmt.executeUpdate("CREATE TABLE test_decimal (amount REAL);"); stmt.executeUpdate("INSERT INTO test_decimal VALUES (123.456789);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_decimal"); assertTrue(resultSet.next()); BigDecimal result = resultSet.getBigDecimal("amount", 2); assertEquals(new BigDecimal("123.46"), result); // Should be rounded to 2 decimal places } @Test void test_getTime_with_columnLabel() throws Exception { stmt.executeUpdate("CREATE TABLE test_time (time_col BLOB);"); long timeMillis = System.currentTimeMillis(); byte[] timeBytes = ByteBuffer.allocate(Long.BYTES).putLong(timeMillis).array(); StringBuilder hexString = new StringBuilder(); for (byte b : timeBytes) { hexString.append(String.format("%02X", b)); } stmt.executeUpdate("INSERT INTO test_time VALUES (X'" + hexString + "');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_time"); assertTrue(resultSet.next()); Time time = resultSet.getTime("time_col"); assertNotNull(time); assertEquals(timeMillis, time.getTime()); } @Test void test_getAsciiStream() throws Exception { stmt.executeUpdate("CREATE TABLE test_ascii (text_col TEXT);"); stmt.executeUpdate("INSERT INTO test_ascii VALUES ('Hello ASCII');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_ascii"); assertTrue(resultSet.next()); InputStream stream = resultSet.getAsciiStream(1); assertNotNull(stream); byte[] buffer = new byte[11]; int bytesRead = stream.read(buffer); assertEquals(11, bytesRead); assertEquals("Hello ASCII", new String(buffer, "US-ASCII")); assertFalse(resultSet.wasNull()); } @Test void test_getAsciiStream_with_columnLabel() throws Exception { stmt.executeUpdate("CREATE TABLE test_ascii (text_col TEXT);"); stmt.executeUpdate("INSERT INTO test_ascii VALUES ('Test');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_ascii"); assertTrue(resultSet.next()); InputStream stream = resultSet.getAsciiStream("text_col"); assertNotNull(stream); byte[] buffer = new byte[4]; stream.read(buffer); assertEquals("Test", new String(buffer, "US-ASCII")); } @Test void test_getBinaryStream() throws Exception { stmt.executeUpdate("CREATE TABLE test_binary (binary_col BLOB);"); byte[] data = {0x01, 0x02, 0x03, 0x04, 0x05}; StringBuilder hexString = new StringBuilder(); for (byte b : data) { hexString.append(String.format("%02X", b)); } stmt.executeUpdate("INSERT INTO test_binary VALUES (X'" + hexString + "');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_binary"); assertTrue(resultSet.next()); InputStream stream = resultSet.getBinaryStream(1); assertNotNull(stream); byte[] buffer = new byte[5]; int bytesRead = stream.read(buffer); assertEquals(5, bytesRead); assertArrayEquals(data, buffer); assertFalse(resultSet.wasNull()); } @Test void test_getBinaryStream_with_columnLabel() throws Exception { stmt.executeUpdate("CREATE TABLE test_binary (data BLOB);"); byte[] data = {0x0A, 0x0B, 0x0C}; StringBuilder hexString = new StringBuilder(); for (byte b : data) { hexString.append(String.format("%02X", b)); } stmt.executeUpdate("INSERT INTO test_binary VALUES (X'" + hexString + "');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_binary"); assertTrue(resultSet.next()); InputStream stream = resultSet.getBinaryStream("data"); assertNotNull(stream); byte[] buffer = new byte[3]; stream.read(buffer); assertArrayEquals(data, buffer); } @Test void test_getUnicodeStream() throws Exception { stmt.executeUpdate("CREATE TABLE test_unicode (text_col TEXT);"); stmt.executeUpdate("INSERT INTO test_unicode VALUES ('Hello minseok');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_unicode"); assertTrue(resultSet.next()); InputStream stream = resultSet.getUnicodeStream(1); assertNotNull(stream); byte[] buffer = new byte[1024]; int bytesRead = stream.read(buffer); String result = new String(buffer, 0, bytesRead, "UTF-8"); assertEquals("Hello minseok", result); assertFalse(resultSet.wasNull()); } @Test void test_getUnicodeStream_with_columnLabel() throws Exception { stmt.executeUpdate("CREATE TABLE test_unicode (text_col TEXT);"); stmt.executeUpdate("INSERT INTO test_unicode VALUES ('Unicode 테스트');"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_unicode"); assertTrue(resultSet.next()); InputStream stream = resultSet.getUnicodeStream("text_col"); assertNotNull(stream); byte[] buffer = new byte[1024]; int bytesRead = stream.read(buffer); String result = new String(buffer, 0, bytesRead, "UTF-8"); assertEquals("Unicode 테스트", result); } @Test void test_stream_methods_return_null_on_null() throws Exception { stmt.executeUpdate("CREATE TABLE test_null_stream (text_col TEXT, binary_col BLOB);"); stmt.executeUpdate("INSERT INTO test_null_stream VALUES (NULL, NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null_stream"); assertTrue(resultSet.next()); assertNull(resultSet.getAsciiStream(1)); assertTrue(resultSet.wasNull()); assertNull(resultSet.getUnicodeStream(1)); assertTrue(resultSet.wasNull()); assertNull(resultSet.getBinaryStream(2)); assertTrue(resultSet.wasNull()); } @Test void test_getMetaData_column_count() throws Exception { stmt.executeUpdate("CREATE TABLE test_meta (col1 INTEGER, col2 TEXT, col3 REAL);"); stmt.executeUpdate("INSERT INTO test_meta VALUES (1, 'test', 3.14);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_meta"); ResultSetMetaData metaData = resultSet.getMetaData(); assertEquals(3, metaData.getColumnCount()); assertEquals("col1", metaData.getColumnName(1)); assertEquals("col2", metaData.getColumnName(2)); assertEquals("col3", metaData.getColumnName(3)); assertEquals("col1", metaData.getColumnLabel(1)); assertEquals(Integer.MAX_VALUE, metaData.getColumnDisplaySize(1)); } @Test void test_wasNull_consistency_across_types() throws Exception { stmt.executeUpdate( "CREATE TABLE test_null_types (" + "int_col INTEGER, " + "text_col TEXT, " + "real_col REAL, " + "blob_col BLOB);"); stmt.executeUpdate("INSERT INTO test_null_types VALUES (NULL, NULL, NULL, NULL);"); ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_null_types"); assertTrue(resultSet.next()); // Test wasNull for various getter methods resultSet.getInt(1); assertTrue(resultSet.wasNull()); resultSet.getString(2); assertTrue(resultSet.wasNull()); resultSet.getDouble(3); assertTrue(resultSet.wasNull()); resultSet.getBytes(4); assertTrue(resultSet.wasNull()); resultSet.getObject(1); assertTrue(resultSet.wasNull()); resultSet.getBigDecimal(3); assertTrue(resultSet.wasNull()); } } ================================================ FILE: bindings/java/src/test/java/tech/turso/jdbc4/JDBC4StatementTest.java ================================================ package tech.turso.jdbc4; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.sql.BatchUpdateException; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tech.turso.TestUtils; class JDBC4StatementTest { private Statement stmt; @BeforeEach void setUp() throws Exception { String filePath = TestUtils.createTempFile(); String url = "jdbc:turso:" + filePath; final JDBC4Connection connection = new JDBC4Connection(url, filePath, new Properties()); stmt = connection.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); } @Test void execute_ddl_should_return_false() throws Exception { assertFalse(stmt.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT);")); } @Test void execute_insert_should_return_false() throws Exception { stmt.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT);"); assertFalse(stmt.execute("INSERT INTO users VALUES (1, 'turso');")); } @Test void execute_update_should_return_false() throws Exception { stmt.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT);"); stmt.execute("INSERT INTO users VALUES (1, 'turso');"); assertFalse(stmt.execute("UPDATE users SET username = 'seonwoo' WHERE id = 1;")); } @Test void execute_select_should_return_true() throws Exception { stmt.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT);"); stmt.execute("INSERT INTO users VALUES (1, 'turso');"); assertTrue(stmt.execute("SELECT * FROM users;")); } @Test void execute_select() throws Exception { stmt.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT);"); stmt.execute("INSERT INTO users VALUES (1, 'turso 1')"); stmt.execute("INSERT INTO users VALUES (2, 'turso 2')"); stmt.execute("INSERT INTO users VALUES (3, 'turso 3')"); ResultSet rs = stmt.executeQuery("SELECT * FROM users;"); rs.next(); int rowCount = 0; do { rowCount++; int id = rs.getInt(1); String username = rs.getString(2); assertEquals(id, rowCount); assertEquals(username, "turso " + rowCount); } while (rs.next()); assertEquals(rowCount, 3); assertFalse(rs.next()); } @Test void close_statement_test() throws Exception { stmt.close(); assertTrue(stmt.isClosed()); } @Test void double_close_is_no_op() throws SQLException { stmt.close(); assertDoesNotThrow(() -> stmt.close()); } @Test void operations_on_closed_statement_should_throw_exception() throws Exception { stmt.close(); assertThrows(SQLException.class, () -> stmt.execute("SELECT 1;")); } @Test void execute_update_should_return_number_of_inserted_elements() throws Exception { assertThat(stmt.executeUpdate("CREATE TABLE s1 (c1);")).isEqualTo(0); assertThat(stmt.executeUpdate("INSERT INTO s1 VALUES (0);")).isEqualTo(1); assertThat(stmt.executeUpdate("INSERT INTO s1 VALUES (1), (2);")).isEqualTo(2); assertThat(stmt.executeUpdate("INSERT INTO s1 VALUES (3), (4), (5);")).isEqualTo(3); } @Test void execute_update_should_return_number_of_updated_elements() throws Exception { assertThat(stmt.executeUpdate("CREATE TABLE s1 (c1 INT);")).isEqualTo(0); assertThat(stmt.executeUpdate("INSERT INTO s1 VALUES (1), (2), (3);")).isEqualTo(3); assertThat(stmt.executeUpdate("UPDATE s1 SET c1 = 0;")).isEqualTo(3); } @Test void execute_update_should_return_number_of_deleted_elements() throws Exception { assertThat(stmt.executeUpdate("CREATE TABLE s1 (c1);")).isEqualTo(0); assertThat(stmt.executeUpdate("INSERT INTO s1 VALUES (1), (2), (3);")).isEqualTo(3); assertThat(stmt.executeUpdate("DELETE FROM s1")).isEqualTo(3); } /** Tests for batch processing functionality */ @Test void testAddBatch_single_statement() throws SQLException { stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); int[] updateCounts = stmt.executeBatch(); assertThat(updateCounts).hasSize(1); assertThat(updateCounts[0]).isEqualTo(1); ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM batch_test;"); assertTrue(rs.next()); assertThat(rs.getInt(1)).isEqualTo(1); } @Test void testAddBatch_multiple_statements() throws SQLException { stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); stmt.addBatch("INSERT INTO batch_test VALUES (2, 'test2');"); stmt.addBatch("INSERT INTO batch_test VALUES (3, 'test3');"); int[] updateCounts = stmt.executeBatch(); assertThat(updateCounts).hasSize(3); assertThat(updateCounts[0]).isEqualTo(1); assertThat(updateCounts[1]).isEqualTo(1); assertThat(updateCounts[2]).isEqualTo(1); ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM batch_test;"); assertTrue(rs.next()); assertThat(rs.getInt(1)).isEqualTo(3); } @Test void testAddBatch_with_updates_and_deletes() throws SQLException { stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); stmt.execute( "INSERT INTO batch_test VALUES (1, 'initial1'), (2, 'initial2'), (3, 'initial3');"); stmt.addBatch("UPDATE batch_test SET value = 'updated';"); stmt.addBatch("DELETE FROM batch_test WHERE id = 2;"); stmt.addBatch("INSERT INTO batch_test VALUES (4, 'new');"); int[] updateCounts = stmt.executeBatch(); assertThat(updateCounts).hasSize(3); assertThat(updateCounts[0]).isEqualTo(3); // UPDATE affected 3 row assertThat(updateCounts[1]).isEqualTo(1); // DELETE affected 1 row assertThat(updateCounts[2]).isEqualTo(1); // INSERT affected 1 row // Verify final state ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM batch_test;"); assertTrue(rs.next()); assertThat(rs.getInt(1)).isEqualTo(3); // 3 initial - 1 deleted + 1 inserted = 3 } @Test void testClearBatch() throws SQLException { stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); stmt.addBatch("INSERT INTO batch_test VALUES (2, 'test2');"); stmt.clearBatch(); int[] updateCounts = stmt.executeBatch(); assertThat(updateCounts).isEmpty(); ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM batch_test;"); assertTrue(rs.next()); assertThat(rs.getInt(1)).isEqualTo(0); } @Test void testBatch_with_SELECT_should_throw_exception() throws SQLException { stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); stmt.execute("INSERT INTO batch_test VALUES (1, 'test1');"); stmt.addBatch("INSERT INTO batch_test VALUES (2, 'test2');"); stmt.addBatch("SELECT * FROM batch_test;"); // This should cause an exception stmt.addBatch("INSERT INTO batch_test VALUES (3, 'test3');"); BatchUpdateException exception = assertThrows(BatchUpdateException.class, () -> stmt.executeBatch()); assertTrue(exception.getMessage().contains("Batch commands cannot return result sets")); int[] updateCounts = exception.getUpdateCounts(); assertThat(updateCounts).hasSize(3); assertThat(updateCounts[0]).isEqualTo(1); // First INSERT succeeded assertThat(updateCounts[1]).isEqualTo(Statement.EXECUTE_FAILED); // SELECT failed } @Test void testBatch_with_null_command_should_throw_exception() { assertThrows(SQLException.class, () -> stmt.addBatch(null)); } @Test void testBatch_operations_on_closed_statement_should_throw_exception() throws SQLException { stmt.close(); assertThrows(SQLException.class, () -> stmt.addBatch("INSERT INTO test VALUES (1);")); assertThrows(SQLException.class, () -> stmt.clearBatch()); assertThrows(SQLException.class, () -> stmt.executeBatch()); } @Test void testBatch_with_syntax_error_should_throw_exception() throws SQLException { stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); stmt.addBatch("INVALID SQL SYNTAX;"); // This should cause an exception stmt.addBatch("INSERT INTO batch_test VALUES (3, 'test3');"); BatchUpdateException exception = assertThrows(BatchUpdateException.class, () -> stmt.executeBatch()); int[] updateCounts = exception.getUpdateCounts(); assertThat(updateCounts).hasSize(3); assertThat(updateCounts[0]).isEqualTo(1); // First INSERT succeeded assertThat(updateCounts[1]).isEqualTo(Statement.EXECUTE_FAILED); // Invalid SQL failed } @Test void testBatch_empty_batch_returns_empty_array() throws SQLException { int[] updateCounts = stmt.executeBatch(); assertThat(updateCounts).isEmpty(); } @Test void testBatch_clears_after_successful_execution() throws SQLException { stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); stmt.addBatch("INSERT INTO batch_test VALUES (1, 'test1');"); stmt.executeBatch(); int[] updateCounts = stmt.executeBatch(); assertThat(updateCounts).isEmpty(); } @Test void testBatch_clears_after_failed_execution() throws SQLException { stmt.execute("CREATE TABLE batch_test (id INTEGER PRIMARY KEY, value TEXT);"); stmt.addBatch("SELECT * FROM batch_test;"); assertThrows(BatchUpdateException.class, () -> stmt.executeBatch()); int[] updateCounts = stmt.executeBatch(); assertThat(updateCounts).isEmpty(); } /** Tests for isBatchCompatibleStatement method */ @Test void testIsBatchCompatibleStatement_compatible_statements() { JDBC4Statement jdbc4Stmt = (JDBC4Statement) stmt; assertTrue(jdbc4Stmt.isBatchCompatibleStatement("INSERT INTO table VALUES (1, 2);")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("insert into table values (1, 2);")); assertTrue( jdbc4Stmt.isBatchCompatibleStatement("INSERT INTO table (col1, col2) VALUES (1, 2);")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("INSERT OR REPLACE INTO table VALUES (1);")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("INSERT OR IGNORE INTO table VALUES (1);")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement(" INSERT INTO table VALUES (1);")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("\t\nINSERT INTO table VALUES (1);")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement(" \n\t INSERT INTO table VALUES (1);")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("/* comment */ INSERT INTO table VALUES (1);")); assertTrue( jdbc4Stmt.isBatchCompatibleStatement( "/* multi\nline\ncomment */ INSERT INTO table VALUES (1);")); assertTrue( jdbc4Stmt.isBatchCompatibleStatement("-- line comment\nINSERT INTO table VALUES (1);")); assertTrue( jdbc4Stmt.isBatchCompatibleStatement( "-- comment 1\n-- comment 2\nINSERT INTO table VALUES (1);")); assertTrue( jdbc4Stmt.isBatchCompatibleStatement( " /* comment */ -- another\n INSERT INTO table VALUES (1);")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("UPDATE table SET col = 1;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("update table set col = 1;")); assertTrue( jdbc4Stmt.isBatchCompatibleStatement("UPDATE table SET col1 = 1, col2 = 2 WHERE id = 3;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("UPDATE OR REPLACE table SET col = 1;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement(" UPDATE table SET col = 1;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("\t\nUPDATE table SET col = 1;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("/* comment */ UPDATE table SET col = 1;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("-- comment\nUPDATE table SET col = 1;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("DELETE FROM table;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("delete from table;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("DELETE FROM table WHERE id = 1;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement(" DELETE FROM table;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("\t\nDELETE FROM table;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("/* comment */ DELETE FROM table;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("-- comment\nDELETE FROM table;")); } @Test void testIsBatchCompatibleStatement_non_compatible_statements() { JDBC4Statement jdbc4Stmt = (JDBC4Statement) stmt; assertFalse(jdbc4Stmt.isBatchCompatibleStatement("SELECT * FROM table;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("select * from table;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement(" SELECT * FROM table;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("/* comment */ SELECT * FROM table;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("-- comment\nSELECT * FROM table;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("EXPLAIN SELECT * FROM table;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("EXPLAIN QUERY PLAN SELECT * FROM table;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("PRAGMA table_info(table);")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("PRAGMA foreign_keys = ON;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("ANALYZE;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("ANALYZE table;")); assertFalse( jdbc4Stmt.isBatchCompatibleStatement( "WITH cte AS (SELECT * FROM table) SELECT * FROM cte;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("VACUUM;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("VALUES (1, 2), (3, 4);")); } @Test void testIsBatchCompatibleStatement_edge_cases() { JDBC4Statement jdbc4Stmt = (JDBC4Statement) stmt; assertFalse(jdbc4Stmt.isBatchCompatibleStatement(null)); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement(" ")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("\t\n")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("/* comment only */")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("-- comment only")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("/* comment */ -- another comment")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("SELECT * FROM table WHERE name = 'INSERT';")); assertFalse( jdbc4Stmt.isBatchCompatibleStatement("SELECT * FROM table WHERE action = 'DELETE';")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("INSER INTO table VALUES (1);")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("UPDAT table SET col = 1;")); assertFalse(jdbc4Stmt.isBatchCompatibleStatement("DELET FROM table;")); } @Test void testIsBatchCompatibleStatement_case_insensitive() { JDBC4Statement jdbc4Stmt = (JDBC4Statement) stmt; assertTrue(jdbc4Stmt.isBatchCompatibleStatement("Insert INTO table VALUES (1);")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("InSeRt INTO table VALUES (1);")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("UPDATE table SET col = 1;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("UpDaTe table SET col = 1;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("Delete FROM table;")); assertTrue(jdbc4Stmt.isBatchCompatibleStatement("DeLeTe FROM table;")); } } ================================================ FILE: bindings/java/src/test/java/tech/turso/jdbc4/TransactionTest.java ================================================ package tech.turso.jdbc4; import static org.junit.jupiter.api.Assertions.*; import java.sql.*; import java.util.Properties; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tech.turso.TestUtils; public class TransactionTest { private Connection connection; private String url; @BeforeEach public void setUp() throws Exception { String filePath = TestUtils.createTempFile(); url = "jdbc:turso:" + filePath; connection = DriverManager.getConnection(url, new Properties()); } @AfterEach public void tearDown() throws Exception { if (connection != null && !connection.isClosed()) { connection.close(); } } @Test public void test_default_auto_commit() throws SQLException { assertTrue(connection.getAutoCommit()); } @Test public void test_commit_rollback_in_auto_commit_mode() { SQLException e1 = assertThrows(SQLException.class, () -> connection.commit()); assertTrue(e1.getMessage().contains("Cannot commit in autocommit mode")); SQLException e2 = assertThrows(SQLException.class, () -> connection.rollback()); assertTrue(e2.getMessage().contains("Cannot rollback in autocommit mode")); } @Test public void test_basic_commit() throws SQLException { connection.setAutoCommit(false); assertFalse(connection.getAutoCommit()); try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); stmt.execute("INSERT INTO test (name) VALUES ('Alice')"); connection.commit(); } // verify data persists in new connection or same connection try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT count(*) FROM test")) { assertTrue(rs.next()); assertEquals(1, rs.getInt(1)); } } @Test public void test_basic_rollback() throws SQLException { try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); } connection.setAutoCommit(false); try (Statement stmt = connection.createStatement()) { stmt.execute("INSERT INTO test (name) VALUES ('Bob')"); connection.rollback(); } // verify data is gone try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT count(*) FROM test")) { assertTrue(rs.next()); assertEquals(0, rs.getInt(1)); } } @Test public void test_auto_commit_toggle_commits() throws SQLException { try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); } connection.setAutoCommit(false); try (Statement stmt = connection.createStatement()) { stmt.execute("INSERT INTO test (name) VALUES ('Charlie')"); } // Setting autoCommit to true should commit the pending transaction connection.setAutoCommit(true); assertTrue(connection.getAutoCommit()); try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT count(*) FROM test")) { assertTrue(rs.next()); assertEquals(1, rs.getInt(1)); } } @Test public void test_isolation_level() throws SQLException { int[] levels = { Connection.TRANSACTION_SERIALIZABLE, Connection.TRANSACTION_READ_COMMITTED, Connection.TRANSACTION_READ_UNCOMMITTED, Connection.TRANSACTION_REPEATABLE_READ }; for (int level : levels) { connection.setTransactionIsolation(level); assertEquals(level, connection.getTransactionIsolation()); } // Should throw if changing during transaction connection.setAutoCommit(false); connection.setTransactionIsolation( Connection.TRANSACTION_SERIALIZABLE); // Valid (not started yet) try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"); // starts transaction } SQLException e = assertThrows( SQLException.class, () -> connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE)); assertTrue( e.getMessage().contains("Cannot change isolation level while transaction is active")); connection.rollback(); } @Test public void test_no_op_commit_rollback() throws SQLException { connection.setAutoCommit(false); // No transaction active yet (lazy start) // Should be no-op (no exception) connection.commit(); connection.rollback(); // Start transaction try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"); } // Now active, commit it connection.commit(); // Back to inactive connection.rollback(); // no-op } @Test public void test_savepoints_not_supported() throws SQLException { assertThrows(SQLException.class, () -> connection.setSavepoint()); assertThrows(SQLException.class, () -> connection.setSavepoint("foo")); assertThrows(SQLException.class, () -> connection.rollback(null)); assertThrows(SQLException.class, () -> connection.releaseSavepoint(null)); } @Test public void test_close_without_commit_rolls_back() throws SQLException { connection.setAutoCommit(false); // Create table and insert data try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE TABLE test_rollback (id INTEGER PRIMARY KEY, name TEXT)"); stmt.execute("INSERT INTO test_rollback (name) VALUES ('rollback_me')"); } // Close connection without commit connection.close(); // Reopen connection to check data try (Connection newConn = DriverManager.getConnection(url, new Properties()); Statement stmt = newConn.createStatement()) { // Let's expect the table might not exist. SQLException e = assertThrows(SQLException.class, () -> stmt.executeQuery("SELECT * FROM test_rollback")); assertTrue( e.getMessage().contains("no such table"), "Expected 'no such table' but got: " + e.getMessage()); } } @Test public void test_mixed_commit_rollback_scenario() throws SQLException { connection.setAutoCommit(false); // Initial schema setup - commit try (Statement stmt = connection.createStatement()) { stmt.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); } connection.commit(); // Insert generic data - commit try (Statement stmt = connection.createStatement()) { stmt.execute("INSERT INTO test (name) VALUES ('item_1')"); stmt.execute("INSERT INTO test (name) VALUES ('item_2')"); } connection.commit(); // Insert more data - rollback try (Statement stmt = connection.createStatement()) { stmt.execute("INSERT INTO test (name) VALUES ('item_to_rollback')"); } connection.rollback(); // Verify only committed data exists try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM test ORDER BY id")) { assertTrue(rs.next()); assertEquals(1, rs.getInt(1)); assertEquals("item_1", rs.getString(2)); assertTrue(rs.next()); assertEquals(2, rs.getInt(1)); assertEquals("item_2", rs.getString(2)); assertFalse(rs.next(), "Should only have 2 committed rows"); } } @Test public void test_double_close_is_safe() throws SQLException { connection.close(); // Second close should be no-op or safe assertDoesNotThrow(() -> connection.close()); assertTrue(connection.isClosed()); } @Test public void test_idempotent_set_auto_commit() throws SQLException { assertTrue(connection.getAutoCommit()); // Setting same value multiple times connection.setAutoCommit(true); assertTrue(connection.getAutoCommit()); connection.setAutoCommit(false); assertFalse(connection.getAutoCommit()); connection.setAutoCommit(false); // Second call assertFalse(connection.getAutoCommit()); } @Test public void test_manual_transaction_control_passthrough() throws SQLException { // Even if managed by driver, manual BEGIN/COMMIT should pass through // Note: Use with caution as it might desync abstract state try (Statement stmt = connection.createStatement()) { stmt.execute("BEGIN"); stmt.execute("CREATE TABLE manual_txn (id INT)"); stmt.execute("COMMIT"); } try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery( "SELECT name FROM sqlite_master WHERE type='table' AND name='manual_txn'")) { assertTrue(rs.next()); } } } ================================================ FILE: bindings/java/src/test/resources/turso/CACHEDIR.TAG ================================================ Signature: 8a477f597d28d172789f06886806bc55 # This file is a cache directory tag created by cargo. # For information about cache directory tags see https://bford.info/cachedir/ ================================================ FILE: bindings/javascript/.gitignore ================================================ # Created by https://www.toptal.com/developers/gitignore/api/node # Edit at https://www.toptal.com/developers/gitignore?templates=node ### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # Next.js build output .next # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # End of https://www.toptal.com/developers/gitignore/api/node # Created by https://www.toptal.com/developers/gitignore/api/macos # Edit at https://www.toptal.com/developers/gitignore?templates=macos ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### macOS Patch ### # iCloud generated files *.icloud # End of https://www.toptal.com/developers/gitignore/api/macos # Created by https://www.toptal.com/developers/gitignore/api/windows # Edit at https://www.toptal.com/developers/gitignore?templates=windows ### Windows ### # Windows thumbnail cache files Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # End of https://www.toptal.com/developers/gitignore/api/windows #Added by cargo /target Cargo.lock .pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions *.node *.wasm npm bundle *.db *.db-wal *.db-changes *.db-wal-revert *.db-info __screenshots__ ================================================ FILE: bindings/javascript/.npmignore ================================================ target Cargo.lock .cargo .github npm .eslintrc .prettierignore rustfmt.toml yarn.lock *.node .yarn __test__ renovate.json examples perf ================================================ FILE: bindings/javascript/.yarn/releases/yarn-4.9.2.cjs ================================================ #!/usr/bin/env node /* eslint-disable */ //prettier-ignore (()=>{var UVe=Object.create;var E_=Object.defineProperty;var HVe=Object.getOwnPropertyDescriptor;var jVe=Object.getOwnPropertyNames;var qVe=Object.getPrototypeOf,GVe=Object.prototype.hasOwnProperty;var Ie=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,r)=>(typeof require<"u"?require:e)[r]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var Ct=(t,e)=>()=>(t&&(e=t(t=0)),e);var L=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),Vt=(t,e)=>{for(var r in e)E_(t,r,{get:e[r],enumerable:!0})},WVe=(t,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of jVe(e))!GVe.call(t,a)&&a!==r&&E_(t,a,{get:()=>e[a],enumerable:!(s=HVe(e,a))||s.enumerable});return t};var et=(t,e,r)=>(r=t!=null?UVe(qVe(t)):{},WVe(e||!t||!t.__esModule?E_(r,"default",{value:t,enumerable:!0}):r,t));var fi={};Vt(fi,{SAFE_TIME:()=>d$,S_IFDIR:()=>rx,S_IFLNK:()=>nx,S_IFMT:()=>_f,S_IFREG:()=>N2});var _f,rx,N2,nx,d$,m$=Ct(()=>{_f=61440,rx=16384,N2=32768,nx=40960,d$=456789e3});var or={};Vt(or,{EBADF:()=>Uo,EBUSY:()=>YVe,EEXIST:()=>XVe,EINVAL:()=>KVe,EISDIR:()=>ZVe,ENOENT:()=>JVe,ENOSYS:()=>VVe,ENOTDIR:()=>zVe,ENOTEMPTY:()=>e7e,EOPNOTSUPP:()=>t7e,EROFS:()=>$Ve,ERR_DIR_CLOSED:()=>I_});function wc(t,e){return Object.assign(new Error(`${t}: ${e}`),{code:t})}function YVe(t){return wc("EBUSY",t)}function VVe(t,e){return wc("ENOSYS",`${t}, ${e}`)}function KVe(t){return wc("EINVAL",`invalid argument, ${t}`)}function Uo(t){return wc("EBADF",`bad file descriptor, ${t}`)}function JVe(t){return wc("ENOENT",`no such file or directory, ${t}`)}function zVe(t){return wc("ENOTDIR",`not a directory, ${t}`)}function ZVe(t){return wc("EISDIR",`illegal operation on a directory, ${t}`)}function XVe(t){return wc("EEXIST",`file already exists, ${t}`)}function $Ve(t){return wc("EROFS",`read-only filesystem, ${t}`)}function e7e(t){return wc("ENOTEMPTY",`directory not empty, ${t}`)}function t7e(t){return wc("EOPNOTSUPP",`operation not supported, ${t}`)}function I_(){return wc("ERR_DIR_CLOSED","Directory handle was closed")}var ix=Ct(()=>{});var el={};Vt(el,{BigIntStatsEntry:()=>rE,DEFAULT_MODE:()=>B_,DirEntry:()=>C_,StatEntry:()=>tE,areStatsEqual:()=>v_,clearStats:()=>sx,convertToBigIntStats:()=>n7e,makeDefaultStats:()=>y$,makeEmptyStats:()=>r7e});function y$(){return new tE}function r7e(){return sx(y$())}function sx(t){for(let e in t)if(Object.hasOwn(t,e)){let r=t[e];typeof r=="number"?t[e]=0:typeof r=="bigint"?t[e]=BigInt(0):w_.types.isDate(r)&&(t[e]=new Date(0))}return t}function n7e(t){let e=new rE;for(let r in t)if(Object.hasOwn(t,r)){let s=t[r];typeof s=="number"?e[r]=BigInt(s):w_.types.isDate(s)&&(e[r]=new Date(s))}return e.atimeNs=e.atimeMs*BigInt(1e6),e.mtimeNs=e.mtimeMs*BigInt(1e6),e.ctimeNs=e.ctimeMs*BigInt(1e6),e.birthtimeNs=e.birthtimeMs*BigInt(1e6),e}function v_(t,e){if(t.atimeMs!==e.atimeMs||t.birthtimeMs!==e.birthtimeMs||t.blksize!==e.blksize||t.blocks!==e.blocks||t.ctimeMs!==e.ctimeMs||t.dev!==e.dev||t.gid!==e.gid||t.ino!==e.ino||t.isBlockDevice()!==e.isBlockDevice()||t.isCharacterDevice()!==e.isCharacterDevice()||t.isDirectory()!==e.isDirectory()||t.isFIFO()!==e.isFIFO()||t.isFile()!==e.isFile()||t.isSocket()!==e.isSocket()||t.isSymbolicLink()!==e.isSymbolicLink()||t.mode!==e.mode||t.mtimeMs!==e.mtimeMs||t.nlink!==e.nlink||t.rdev!==e.rdev||t.size!==e.size||t.uid!==e.uid)return!1;let r=t,s=e;return!(r.atimeNs!==s.atimeNs||r.mtimeNs!==s.mtimeNs||r.ctimeNs!==s.ctimeNs||r.birthtimeNs!==s.birthtimeNs)}var w_,B_,C_,tE,rE,S_=Ct(()=>{w_=et(Ie("util")),B_=33188,C_=class{constructor(){this.name="";this.path="";this.mode=0}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&61440)===16384}isFIFO(){return!1}isFile(){return(this.mode&61440)===32768}isSocket(){return!1}isSymbolicLink(){return(this.mode&61440)===40960}},tE=class{constructor(){this.uid=0;this.gid=0;this.size=0;this.blksize=0;this.atimeMs=0;this.mtimeMs=0;this.ctimeMs=0;this.birthtimeMs=0;this.atime=new Date(0);this.mtime=new Date(0);this.ctime=new Date(0);this.birthtime=new Date(0);this.dev=0;this.ino=0;this.mode=B_;this.nlink=1;this.rdev=0;this.blocks=1}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&61440)===16384}isFIFO(){return!1}isFile(){return(this.mode&61440)===32768}isSocket(){return!1}isSymbolicLink(){return(this.mode&61440)===40960}},rE=class{constructor(){this.uid=BigInt(0);this.gid=BigInt(0);this.size=BigInt(0);this.blksize=BigInt(0);this.atimeMs=BigInt(0);this.mtimeMs=BigInt(0);this.ctimeMs=BigInt(0);this.birthtimeMs=BigInt(0);this.atimeNs=BigInt(0);this.mtimeNs=BigInt(0);this.ctimeNs=BigInt(0);this.birthtimeNs=BigInt(0);this.atime=new Date(0);this.mtime=new Date(0);this.ctime=new Date(0);this.birthtime=new Date(0);this.dev=BigInt(0);this.ino=BigInt(0);this.mode=BigInt(B_);this.nlink=BigInt(1);this.rdev=BigInt(0);this.blocks=BigInt(1)}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&BigInt(61440))===BigInt(16384)}isFIFO(){return!1}isFile(){return(this.mode&BigInt(61440))===BigInt(32768)}isSocket(){return!1}isSymbolicLink(){return(this.mode&BigInt(61440))===BigInt(40960)}}});function l7e(t){let e,r;if(e=t.match(o7e))t=e[1];else if(r=t.match(a7e))t=`\\\\${r[1]?".\\":""}${r[2]}`;else return t;return t.replace(/\//g,"\\")}function c7e(t){t=t.replace(/\\/g,"/");let e,r;return(e=t.match(i7e))?t=`/${e[1]}`:(r=t.match(s7e))&&(t=`/unc/${r[1]?".dot/":""}${r[2]}`),t}function ox(t,e){return t===ue?I$(e):D_(e)}var O2,vt,Er,ue,K,E$,i7e,s7e,o7e,a7e,D_,I$,tl=Ct(()=>{O2=et(Ie("path")),vt={root:"/",dot:".",parent:".."},Er={home:"~",nodeModules:"node_modules",manifest:"package.json",lockfile:"yarn.lock",virtual:"__virtual__",pnpJs:".pnp.js",pnpCjs:".pnp.cjs",pnpData:".pnp.data.json",pnpEsmLoader:".pnp.loader.mjs",rc:".yarnrc.yml",env:".env"},ue=Object.create(O2.default),K=Object.create(O2.default.posix);ue.cwd=()=>process.cwd();K.cwd=process.platform==="win32"?()=>D_(process.cwd()):process.cwd;process.platform==="win32"&&(K.resolve=(...t)=>t.length>0&&K.isAbsolute(t[0])?O2.default.posix.resolve(...t):O2.default.posix.resolve(K.cwd(),...t));E$=function(t,e,r){return e=t.normalize(e),r=t.normalize(r),e===r?".":(e.endsWith(t.sep)||(e=e+t.sep),r.startsWith(e)?r.slice(e.length):null)};ue.contains=(t,e)=>E$(ue,t,e);K.contains=(t,e)=>E$(K,t,e);i7e=/^([a-zA-Z]:.*)$/,s7e=/^\/\/(\.\/)?(.*)$/,o7e=/^\/([a-zA-Z]:.*)$/,a7e=/^\/unc\/(\.dot\/)?(.*)$/;D_=process.platform==="win32"?c7e:t=>t,I$=process.platform==="win32"?l7e:t=>t;ue.fromPortablePath=I$;ue.toPortablePath=D_});async function ax(t,e){let r="0123456789abcdef";await t.mkdirPromise(e.indexPath,{recursive:!0});let s=[];for(let a of r)for(let n of r)s.push(t.mkdirPromise(t.pathUtils.join(e.indexPath,`${a}${n}`),{recursive:!0}));return await Promise.all(s),e.indexPath}async function C$(t,e,r,s,a){let n=t.pathUtils.normalize(e),c=r.pathUtils.normalize(s),f=[],p=[],{atime:h,mtime:E}=a.stableTime?{atime:gd,mtime:gd}:await r.lstatPromise(c);await t.mkdirpPromise(t.pathUtils.dirname(e),{utimes:[h,E]}),await b_(f,p,t,n,r,c,{...a,didParentExist:!0});for(let C of f)await C();await Promise.all(p.map(C=>C()))}async function b_(t,e,r,s,a,n,c){let f=c.didParentExist?await w$(r,s):null,p=await a.lstatPromise(n),{atime:h,mtime:E}=c.stableTime?{atime:gd,mtime:gd}:p,C;switch(!0){case p.isDirectory():C=await f7e(t,e,r,s,f,a,n,p,c);break;case p.isFile():C=await h7e(t,e,r,s,f,a,n,p,c);break;case p.isSymbolicLink():C=await g7e(t,e,r,s,f,a,n,p,c);break;default:throw new Error(`Unsupported file type (${p.mode})`)}return(c.linkStrategy?.type!=="HardlinkFromIndex"||!p.isFile())&&((C||f?.mtime?.getTime()!==E.getTime()||f?.atime?.getTime()!==h.getTime())&&(e.push(()=>r.lutimesPromise(s,h,E)),C=!0),(f===null||(f.mode&511)!==(p.mode&511))&&(e.push(()=>r.chmodPromise(s,p.mode&511)),C=!0)),C}async function w$(t,e){try{return await t.lstatPromise(e)}catch{return null}}async function f7e(t,e,r,s,a,n,c,f,p){if(a!==null&&!a.isDirectory())if(p.overwrite)t.push(async()=>r.removePromise(s)),a=null;else return!1;let h=!1;a===null&&(t.push(async()=>{try{await r.mkdirPromise(s,{mode:f.mode})}catch(S){if(S.code!=="EEXIST")throw S}}),h=!0);let E=await n.readdirPromise(c),C=p.didParentExist&&!a?{...p,didParentExist:!1}:p;if(p.stableSort)for(let S of E.sort())await b_(t,e,r,r.pathUtils.join(s,S),n,n.pathUtils.join(c,S),C)&&(h=!0);else(await Promise.all(E.map(async P=>{await b_(t,e,r,r.pathUtils.join(s,P),n,n.pathUtils.join(c,P),C)}))).some(P=>P)&&(h=!0);return h}async function A7e(t,e,r,s,a,n,c,f,p,h){let E=await n.checksumFilePromise(c,{algorithm:"sha1"}),C=420,S=f.mode&511,P=`${E}${S!==C?S.toString(8):""}`,I=r.pathUtils.join(h.indexPath,E.slice(0,2),`${P}.dat`),R;(ce=>(ce[ce.Lock=0]="Lock",ce[ce.Rename=1]="Rename"))(R||={});let N=1,U=await w$(r,I);if(a){let ie=U&&a.dev===U.dev&&a.ino===U.ino,Ae=U?.mtimeMs!==u7e;if(ie&&Ae&&h.autoRepair&&(N=0,U=null),!ie)if(p.overwrite)t.push(async()=>r.removePromise(s)),a=null;else return!1}let W=!U&&N===1?`${I}.${Math.floor(Math.random()*4294967296).toString(16).padStart(8,"0")}`:null,te=!1;return t.push(async()=>{if(!U&&(N===0&&await r.lockPromise(I,async()=>{let ie=await n.readFilePromise(c);await r.writeFilePromise(I,ie)}),N===1&&W)){let ie=await n.readFilePromise(c);await r.writeFilePromise(W,ie);try{await r.linkPromise(W,I)}catch(Ae){if(Ae.code==="EEXIST")te=!0,await r.unlinkPromise(W);else throw Ae}}a||await r.linkPromise(I,s)}),e.push(async()=>{U||(await r.lutimesPromise(I,gd,gd),S!==C&&await r.chmodPromise(I,S)),W&&!te&&await r.unlinkPromise(W)}),!1}async function p7e(t,e,r,s,a,n,c,f,p){if(a!==null)if(p.overwrite)t.push(async()=>r.removePromise(s)),a=null;else return!1;return t.push(async()=>{let h=await n.readFilePromise(c);await r.writeFilePromise(s,h)}),!0}async function h7e(t,e,r,s,a,n,c,f,p){return p.linkStrategy?.type==="HardlinkFromIndex"?A7e(t,e,r,s,a,n,c,f,p,p.linkStrategy):p7e(t,e,r,s,a,n,c,f,p)}async function g7e(t,e,r,s,a,n,c,f,p){if(a!==null)if(p.overwrite)t.push(async()=>r.removePromise(s)),a=null;else return!1;return t.push(async()=>{await r.symlinkPromise(ox(r.pathUtils,await n.readlinkPromise(c)),s)}),!0}var gd,u7e,P_=Ct(()=>{tl();gd=new Date(456789e3*1e3),u7e=gd.getTime()});function lx(t,e,r,s){let a=()=>{let n=r.shift();if(typeof n>"u")return null;let c=t.pathUtils.join(e,n);return Object.assign(t.statSync(c),{name:n,path:void 0})};return new L2(e,a,s)}var L2,B$=Ct(()=>{ix();L2=class{constructor(e,r,s={}){this.path=e;this.nextDirent=r;this.opts=s;this.closed=!1}throwIfClosed(){if(this.closed)throw I_()}async*[Symbol.asyncIterator](){try{let e;for(;(e=await this.read())!==null;)yield e}finally{await this.close()}}read(e){let r=this.readSync();return typeof e<"u"?e(null,r):Promise.resolve(r)}readSync(){return this.throwIfClosed(),this.nextDirent()}close(e){return this.closeSync(),typeof e<"u"?e(null):Promise.resolve()}closeSync(){this.throwIfClosed(),this.opts.onClose?.(),this.closed=!0}}});function v$(t,e){if(t!==e)throw new Error(`Invalid StatWatcher status: expected '${e}', got '${t}'`)}var S$,cx,D$=Ct(()=>{S$=Ie("events");S_();cx=class t extends S$.EventEmitter{constructor(r,s,{bigint:a=!1}={}){super();this.status="ready";this.changeListeners=new Map;this.startTimeout=null;this.fakeFs=r,this.path=s,this.bigint=a,this.lastStats=this.stat()}static create(r,s,a){let n=new t(r,s,a);return n.start(),n}start(){v$(this.status,"ready"),this.status="running",this.startTimeout=setTimeout(()=>{this.startTimeout=null,this.fakeFs.existsSync(this.path)||this.emit("change",this.lastStats,this.lastStats)},3)}stop(){v$(this.status,"running"),this.status="stopped",this.startTimeout!==null&&(clearTimeout(this.startTimeout),this.startTimeout=null),this.emit("stop")}stat(){try{return this.fakeFs.statSync(this.path,{bigint:this.bigint})}catch{let r=this.bigint?new rE:new tE;return sx(r)}}makeInterval(r){let s=setInterval(()=>{let a=this.stat(),n=this.lastStats;v_(a,n)||(this.lastStats=a,this.emit("change",a,n))},r.interval);return r.persistent?s:s.unref()}registerChangeListener(r,s){this.addListener("change",r),this.changeListeners.set(r,this.makeInterval(s))}unregisterChangeListener(r){this.removeListener("change",r);let s=this.changeListeners.get(r);typeof s<"u"&&clearInterval(s),this.changeListeners.delete(r)}unregisterAllChangeListeners(){for(let r of this.changeListeners.keys())this.unregisterChangeListener(r)}hasChangeListeners(){return this.changeListeners.size>0}ref(){for(let r of this.changeListeners.values())r.ref();return this}unref(){for(let r of this.changeListeners.values())r.unref();return this}}});function nE(t,e,r,s){let a,n,c,f;switch(typeof r){case"function":a=!1,n=!0,c=5007,f=r;break;default:({bigint:a=!1,persistent:n=!0,interval:c=5007}=r),f=s;break}let p=ux.get(t);typeof p>"u"&&ux.set(t,p=new Map);let h=p.get(e);return typeof h>"u"&&(h=cx.create(t,e,{bigint:a}),p.set(e,h)),h.registerChangeListener(f,{persistent:n,interval:c}),h}function dd(t,e,r){let s=ux.get(t);if(typeof s>"u")return;let a=s.get(e);typeof a>"u"||(typeof r>"u"?a.unregisterAllChangeListeners():a.unregisterChangeListener(r),a.hasChangeListeners()||(a.stop(),s.delete(e)))}function md(t){let e=ux.get(t);if(!(typeof e>"u"))for(let r of e.keys())dd(t,r)}var ux,x_=Ct(()=>{D$();ux=new WeakMap});function d7e(t){let e=t.match(/\r?\n/g);if(e===null)return P$.EOL;let r=e.filter(a=>a===`\r `).length,s=e.length-r;return r>s?`\r `:` `}function yd(t,e){return e.replace(/\r?\n/g,d7e(t))}var b$,P$,Ep,Uf,Ed=Ct(()=>{b$=Ie("crypto"),P$=Ie("os");P_();tl();Ep=class{constructor(e){this.pathUtils=e}async*genTraversePromise(e,{stableSort:r=!1}={}){let s=[e];for(;s.length>0;){let a=s.shift();if((await this.lstatPromise(a)).isDirectory()){let c=await this.readdirPromise(a);if(r)for(let f of c.sort())s.push(this.pathUtils.join(a,f));else throw new Error("Not supported")}else yield a}}async checksumFilePromise(e,{algorithm:r="sha512"}={}){let s=await this.openPromise(e,"r");try{let n=Buffer.allocUnsafeSlow(65536),c=(0,b$.createHash)(r),f=0;for(;(f=await this.readPromise(s,n,0,65536))!==0;)c.update(f===65536?n:n.slice(0,f));return c.digest("hex")}finally{await this.closePromise(s)}}async removePromise(e,{recursive:r=!0,maxRetries:s=5}={}){let a;try{a=await this.lstatPromise(e)}catch(n){if(n.code==="ENOENT")return;throw n}if(a.isDirectory()){if(r){let n=await this.readdirPromise(e);await Promise.all(n.map(c=>this.removePromise(this.pathUtils.resolve(e,c))))}for(let n=0;n<=s;n++)try{await this.rmdirPromise(e);break}catch(c){if(c.code!=="EBUSY"&&c.code!=="ENOTEMPTY")throw c;nsetTimeout(f,n*100))}}else await this.unlinkPromise(e)}removeSync(e,{recursive:r=!0}={}){let s;try{s=this.lstatSync(e)}catch(a){if(a.code==="ENOENT")return;throw a}if(s.isDirectory()){if(r)for(let a of this.readdirSync(e))this.removeSync(this.pathUtils.resolve(e,a));this.rmdirSync(e)}else this.unlinkSync(e)}async mkdirpPromise(e,{chmod:r,utimes:s}={}){if(e=this.resolve(e),e===this.pathUtils.dirname(e))return;let a=e.split(this.pathUtils.sep),n;for(let c=2;c<=a.length;++c){let f=a.slice(0,c).join(this.pathUtils.sep);if(!this.existsSync(f)){try{await this.mkdirPromise(f)}catch(p){if(p.code==="EEXIST")continue;throw p}if(n??=f,r!=null&&await this.chmodPromise(f,r),s!=null)await this.utimesPromise(f,s[0],s[1]);else{let p=await this.statPromise(this.pathUtils.dirname(f));await this.utimesPromise(f,p.atime,p.mtime)}}}return n}mkdirpSync(e,{chmod:r,utimes:s}={}){if(e=this.resolve(e),e===this.pathUtils.dirname(e))return;let a=e.split(this.pathUtils.sep),n;for(let c=2;c<=a.length;++c){let f=a.slice(0,c).join(this.pathUtils.sep);if(!this.existsSync(f)){try{this.mkdirSync(f)}catch(p){if(p.code==="EEXIST")continue;throw p}if(n??=f,r!=null&&this.chmodSync(f,r),s!=null)this.utimesSync(f,s[0],s[1]);else{let p=this.statSync(this.pathUtils.dirname(f));this.utimesSync(f,p.atime,p.mtime)}}}return n}async copyPromise(e,r,{baseFs:s=this,overwrite:a=!0,stableSort:n=!1,stableTime:c=!1,linkStrategy:f=null}={}){return await C$(this,e,s,r,{overwrite:a,stableSort:n,stableTime:c,linkStrategy:f})}copySync(e,r,{baseFs:s=this,overwrite:a=!0}={}){let n=s.lstatSync(r),c=this.existsSync(e);if(n.isDirectory()){this.mkdirpSync(e);let p=s.readdirSync(r);for(let h of p)this.copySync(this.pathUtils.join(e,h),s.pathUtils.join(r,h),{baseFs:s,overwrite:a})}else if(n.isFile()){if(!c||a){c&&this.removeSync(e);let p=s.readFileSync(r);this.writeFileSync(e,p)}}else if(n.isSymbolicLink()){if(!c||a){c&&this.removeSync(e);let p=s.readlinkSync(r);this.symlinkSync(ox(this.pathUtils,p),e)}}else throw new Error(`Unsupported file type (file: ${r}, mode: 0o${n.mode.toString(8).padStart(6,"0")})`);let f=n.mode&511;this.chmodSync(e,f)}async changeFilePromise(e,r,s={}){return Buffer.isBuffer(r)?this.changeFileBufferPromise(e,r,s):this.changeFileTextPromise(e,r,s)}async changeFileBufferPromise(e,r,{mode:s}={}){let a=Buffer.alloc(0);try{a=await this.readFilePromise(e)}catch{}Buffer.compare(a,r)!==0&&await this.writeFilePromise(e,r,{mode:s})}async changeFileTextPromise(e,r,{automaticNewlines:s,mode:a}={}){let n="";try{n=await this.readFilePromise(e,"utf8")}catch{}let c=s?yd(n,r):r;n!==c&&await this.writeFilePromise(e,c,{mode:a})}changeFileSync(e,r,s={}){return Buffer.isBuffer(r)?this.changeFileBufferSync(e,r,s):this.changeFileTextSync(e,r,s)}changeFileBufferSync(e,r,{mode:s}={}){let a=Buffer.alloc(0);try{a=this.readFileSync(e)}catch{}Buffer.compare(a,r)!==0&&this.writeFileSync(e,r,{mode:s})}changeFileTextSync(e,r,{automaticNewlines:s=!1,mode:a}={}){let n="";try{n=this.readFileSync(e,"utf8")}catch{}let c=s?yd(n,r):r;n!==c&&this.writeFileSync(e,c,{mode:a})}async movePromise(e,r){try{await this.renamePromise(e,r)}catch(s){if(s.code==="EXDEV")await this.copyPromise(r,e),await this.removePromise(e);else throw s}}moveSync(e,r){try{this.renameSync(e,r)}catch(s){if(s.code==="EXDEV")this.copySync(r,e),this.removeSync(e);else throw s}}async lockPromise(e,r){let s=`${e}.flock`,a=1e3/60,n=Date.now(),c=null,f=async()=>{let p;try{[p]=await this.readJsonPromise(s)}catch{return Date.now()-n<500}try{return process.kill(p,0),!0}catch{return!1}};for(;c===null;)try{c=await this.openPromise(s,"wx")}catch(p){if(p.code==="EEXIST"){if(!await f())try{await this.unlinkPromise(s);continue}catch{}if(Date.now()-n<60*1e3)await new Promise(h=>setTimeout(h,a));else throw new Error(`Couldn't acquire a lock in a reasonable time (via ${s})`)}else throw p}await this.writePromise(c,JSON.stringify([process.pid]));try{return await r()}finally{try{await this.closePromise(c),await this.unlinkPromise(s)}catch{}}}async readJsonPromise(e){let r=await this.readFilePromise(e,"utf8");try{return JSON.parse(r)}catch(s){throw s.message+=` (in ${e})`,s}}readJsonSync(e){let r=this.readFileSync(e,"utf8");try{return JSON.parse(r)}catch(s){throw s.message+=` (in ${e})`,s}}async writeJsonPromise(e,r,{compact:s=!1}={}){let a=s?0:2;return await this.writeFilePromise(e,`${JSON.stringify(r,null,a)} `)}writeJsonSync(e,r,{compact:s=!1}={}){let a=s?0:2;return this.writeFileSync(e,`${JSON.stringify(r,null,a)} `)}async preserveTimePromise(e,r){let s=await this.lstatPromise(e),a=await r();typeof a<"u"&&(e=a),await this.lutimesPromise(e,s.atime,s.mtime)}async preserveTimeSync(e,r){let s=this.lstatSync(e),a=r();typeof a<"u"&&(e=a),this.lutimesSync(e,s.atime,s.mtime)}},Uf=class extends Ep{constructor(){super(K)}}});var js,Ip=Ct(()=>{Ed();js=class extends Ep{getExtractHint(e){return this.baseFs.getExtractHint(e)}resolve(e){return this.mapFromBase(this.baseFs.resolve(this.mapToBase(e)))}getRealPath(){return this.mapFromBase(this.baseFs.getRealPath())}async openPromise(e,r,s){return this.baseFs.openPromise(this.mapToBase(e),r,s)}openSync(e,r,s){return this.baseFs.openSync(this.mapToBase(e),r,s)}async opendirPromise(e,r){return Object.assign(await this.baseFs.opendirPromise(this.mapToBase(e),r),{path:e})}opendirSync(e,r){return Object.assign(this.baseFs.opendirSync(this.mapToBase(e),r),{path:e})}async readPromise(e,r,s,a,n){return await this.baseFs.readPromise(e,r,s,a,n)}readSync(e,r,s,a,n){return this.baseFs.readSync(e,r,s,a,n)}async writePromise(e,r,s,a,n){return typeof r=="string"?await this.baseFs.writePromise(e,r,s):await this.baseFs.writePromise(e,r,s,a,n)}writeSync(e,r,s,a,n){return typeof r=="string"?this.baseFs.writeSync(e,r,s):this.baseFs.writeSync(e,r,s,a,n)}async closePromise(e){return this.baseFs.closePromise(e)}closeSync(e){this.baseFs.closeSync(e)}createReadStream(e,r){return this.baseFs.createReadStream(e!==null?this.mapToBase(e):e,r)}createWriteStream(e,r){return this.baseFs.createWriteStream(e!==null?this.mapToBase(e):e,r)}async realpathPromise(e){return this.mapFromBase(await this.baseFs.realpathPromise(this.mapToBase(e)))}realpathSync(e){return this.mapFromBase(this.baseFs.realpathSync(this.mapToBase(e)))}async existsPromise(e){return this.baseFs.existsPromise(this.mapToBase(e))}existsSync(e){return this.baseFs.existsSync(this.mapToBase(e))}accessSync(e,r){return this.baseFs.accessSync(this.mapToBase(e),r)}async accessPromise(e,r){return this.baseFs.accessPromise(this.mapToBase(e),r)}async statPromise(e,r){return this.baseFs.statPromise(this.mapToBase(e),r)}statSync(e,r){return this.baseFs.statSync(this.mapToBase(e),r)}async fstatPromise(e,r){return this.baseFs.fstatPromise(e,r)}fstatSync(e,r){return this.baseFs.fstatSync(e,r)}lstatPromise(e,r){return this.baseFs.lstatPromise(this.mapToBase(e),r)}lstatSync(e,r){return this.baseFs.lstatSync(this.mapToBase(e),r)}async fchmodPromise(e,r){return this.baseFs.fchmodPromise(e,r)}fchmodSync(e,r){return this.baseFs.fchmodSync(e,r)}async chmodPromise(e,r){return this.baseFs.chmodPromise(this.mapToBase(e),r)}chmodSync(e,r){return this.baseFs.chmodSync(this.mapToBase(e),r)}async fchownPromise(e,r,s){return this.baseFs.fchownPromise(e,r,s)}fchownSync(e,r,s){return this.baseFs.fchownSync(e,r,s)}async chownPromise(e,r,s){return this.baseFs.chownPromise(this.mapToBase(e),r,s)}chownSync(e,r,s){return this.baseFs.chownSync(this.mapToBase(e),r,s)}async renamePromise(e,r){return this.baseFs.renamePromise(this.mapToBase(e),this.mapToBase(r))}renameSync(e,r){return this.baseFs.renameSync(this.mapToBase(e),this.mapToBase(r))}async copyFilePromise(e,r,s=0){return this.baseFs.copyFilePromise(this.mapToBase(e),this.mapToBase(r),s)}copyFileSync(e,r,s=0){return this.baseFs.copyFileSync(this.mapToBase(e),this.mapToBase(r),s)}async appendFilePromise(e,r,s){return this.baseFs.appendFilePromise(this.fsMapToBase(e),r,s)}appendFileSync(e,r,s){return this.baseFs.appendFileSync(this.fsMapToBase(e),r,s)}async writeFilePromise(e,r,s){return this.baseFs.writeFilePromise(this.fsMapToBase(e),r,s)}writeFileSync(e,r,s){return this.baseFs.writeFileSync(this.fsMapToBase(e),r,s)}async unlinkPromise(e){return this.baseFs.unlinkPromise(this.mapToBase(e))}unlinkSync(e){return this.baseFs.unlinkSync(this.mapToBase(e))}async utimesPromise(e,r,s){return this.baseFs.utimesPromise(this.mapToBase(e),r,s)}utimesSync(e,r,s){return this.baseFs.utimesSync(this.mapToBase(e),r,s)}async lutimesPromise(e,r,s){return this.baseFs.lutimesPromise(this.mapToBase(e),r,s)}lutimesSync(e,r,s){return this.baseFs.lutimesSync(this.mapToBase(e),r,s)}async mkdirPromise(e,r){return this.baseFs.mkdirPromise(this.mapToBase(e),r)}mkdirSync(e,r){return this.baseFs.mkdirSync(this.mapToBase(e),r)}async rmdirPromise(e,r){return this.baseFs.rmdirPromise(this.mapToBase(e),r)}rmdirSync(e,r){return this.baseFs.rmdirSync(this.mapToBase(e),r)}async rmPromise(e,r){return this.baseFs.rmPromise(this.mapToBase(e),r)}rmSync(e,r){return this.baseFs.rmSync(this.mapToBase(e),r)}async linkPromise(e,r){return this.baseFs.linkPromise(this.mapToBase(e),this.mapToBase(r))}linkSync(e,r){return this.baseFs.linkSync(this.mapToBase(e),this.mapToBase(r))}async symlinkPromise(e,r,s){let a=this.mapToBase(r);if(this.pathUtils.isAbsolute(e))return this.baseFs.symlinkPromise(this.mapToBase(e),a,s);let n=this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(r),e)),c=this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(a),n);return this.baseFs.symlinkPromise(c,a,s)}symlinkSync(e,r,s){let a=this.mapToBase(r);if(this.pathUtils.isAbsolute(e))return this.baseFs.symlinkSync(this.mapToBase(e),a,s);let n=this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(r),e)),c=this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(a),n);return this.baseFs.symlinkSync(c,a,s)}async readFilePromise(e,r){return this.baseFs.readFilePromise(this.fsMapToBase(e),r)}readFileSync(e,r){return this.baseFs.readFileSync(this.fsMapToBase(e),r)}readdirPromise(e,r){return this.baseFs.readdirPromise(this.mapToBase(e),r)}readdirSync(e,r){return this.baseFs.readdirSync(this.mapToBase(e),r)}async readlinkPromise(e){return this.mapFromBase(await this.baseFs.readlinkPromise(this.mapToBase(e)))}readlinkSync(e){return this.mapFromBase(this.baseFs.readlinkSync(this.mapToBase(e)))}async truncatePromise(e,r){return this.baseFs.truncatePromise(this.mapToBase(e),r)}truncateSync(e,r){return this.baseFs.truncateSync(this.mapToBase(e),r)}async ftruncatePromise(e,r){return this.baseFs.ftruncatePromise(e,r)}ftruncateSync(e,r){return this.baseFs.ftruncateSync(e,r)}watch(e,r,s){return this.baseFs.watch(this.mapToBase(e),r,s)}watchFile(e,r,s){return this.baseFs.watchFile(this.mapToBase(e),r,s)}unwatchFile(e,r){return this.baseFs.unwatchFile(this.mapToBase(e),r)}fsMapToBase(e){return typeof e=="number"?e:this.mapToBase(e)}}});var Hf,x$=Ct(()=>{Ip();Hf=class extends js{constructor(e,{baseFs:r,pathUtils:s}){super(s),this.target=e,this.baseFs=r}getRealPath(){return this.target}getBaseFs(){return this.baseFs}mapFromBase(e){return e}mapToBase(e){return e}}});function k$(t){let e=t;return typeof t.path=="string"&&(e.path=ue.toPortablePath(t.path)),e}var Q$,Yn,Id=Ct(()=>{Q$=et(Ie("fs"));Ed();tl();Yn=class extends Uf{constructor(e=Q$.default){super(),this.realFs=e}getExtractHint(){return!1}getRealPath(){return vt.root}resolve(e){return K.resolve(e)}async openPromise(e,r,s){return await new Promise((a,n)=>{this.realFs.open(ue.fromPortablePath(e),r,s,this.makeCallback(a,n))})}openSync(e,r,s){return this.realFs.openSync(ue.fromPortablePath(e),r,s)}async opendirPromise(e,r){return await new Promise((s,a)=>{typeof r<"u"?this.realFs.opendir(ue.fromPortablePath(e),r,this.makeCallback(s,a)):this.realFs.opendir(ue.fromPortablePath(e),this.makeCallback(s,a))}).then(s=>{let a=s;return Object.defineProperty(a,"path",{value:e,configurable:!0,writable:!0}),a})}opendirSync(e,r){let a=typeof r<"u"?this.realFs.opendirSync(ue.fromPortablePath(e),r):this.realFs.opendirSync(ue.fromPortablePath(e));return Object.defineProperty(a,"path",{value:e,configurable:!0,writable:!0}),a}async readPromise(e,r,s=0,a=0,n=-1){return await new Promise((c,f)=>{this.realFs.read(e,r,s,a,n,(p,h)=>{p?f(p):c(h)})})}readSync(e,r,s,a,n){return this.realFs.readSync(e,r,s,a,n)}async writePromise(e,r,s,a,n){return await new Promise((c,f)=>typeof r=="string"?this.realFs.write(e,r,s,this.makeCallback(c,f)):this.realFs.write(e,r,s,a,n,this.makeCallback(c,f)))}writeSync(e,r,s,a,n){return typeof r=="string"?this.realFs.writeSync(e,r,s):this.realFs.writeSync(e,r,s,a,n)}async closePromise(e){await new Promise((r,s)=>{this.realFs.close(e,this.makeCallback(r,s))})}closeSync(e){this.realFs.closeSync(e)}createReadStream(e,r){let s=e!==null?ue.fromPortablePath(e):e;return this.realFs.createReadStream(s,r)}createWriteStream(e,r){let s=e!==null?ue.fromPortablePath(e):e;return this.realFs.createWriteStream(s,r)}async realpathPromise(e){return await new Promise((r,s)=>{this.realFs.realpath(ue.fromPortablePath(e),{},this.makeCallback(r,s))}).then(r=>ue.toPortablePath(r))}realpathSync(e){return ue.toPortablePath(this.realFs.realpathSync(ue.fromPortablePath(e),{}))}async existsPromise(e){return await new Promise(r=>{this.realFs.exists(ue.fromPortablePath(e),r)})}accessSync(e,r){return this.realFs.accessSync(ue.fromPortablePath(e),r)}async accessPromise(e,r){return await new Promise((s,a)=>{this.realFs.access(ue.fromPortablePath(e),r,this.makeCallback(s,a))})}existsSync(e){return this.realFs.existsSync(ue.fromPortablePath(e))}async statPromise(e,r){return await new Promise((s,a)=>{r?this.realFs.stat(ue.fromPortablePath(e),r,this.makeCallback(s,a)):this.realFs.stat(ue.fromPortablePath(e),this.makeCallback(s,a))})}statSync(e,r){return r?this.realFs.statSync(ue.fromPortablePath(e),r):this.realFs.statSync(ue.fromPortablePath(e))}async fstatPromise(e,r){return await new Promise((s,a)=>{r?this.realFs.fstat(e,r,this.makeCallback(s,a)):this.realFs.fstat(e,this.makeCallback(s,a))})}fstatSync(e,r){return r?this.realFs.fstatSync(e,r):this.realFs.fstatSync(e)}async lstatPromise(e,r){return await new Promise((s,a)=>{r?this.realFs.lstat(ue.fromPortablePath(e),r,this.makeCallback(s,a)):this.realFs.lstat(ue.fromPortablePath(e),this.makeCallback(s,a))})}lstatSync(e,r){return r?this.realFs.lstatSync(ue.fromPortablePath(e),r):this.realFs.lstatSync(ue.fromPortablePath(e))}async fchmodPromise(e,r){return await new Promise((s,a)=>{this.realFs.fchmod(e,r,this.makeCallback(s,a))})}fchmodSync(e,r){return this.realFs.fchmodSync(e,r)}async chmodPromise(e,r){return await new Promise((s,a)=>{this.realFs.chmod(ue.fromPortablePath(e),r,this.makeCallback(s,a))})}chmodSync(e,r){return this.realFs.chmodSync(ue.fromPortablePath(e),r)}async fchownPromise(e,r,s){return await new Promise((a,n)=>{this.realFs.fchown(e,r,s,this.makeCallback(a,n))})}fchownSync(e,r,s){return this.realFs.fchownSync(e,r,s)}async chownPromise(e,r,s){return await new Promise((a,n)=>{this.realFs.chown(ue.fromPortablePath(e),r,s,this.makeCallback(a,n))})}chownSync(e,r,s){return this.realFs.chownSync(ue.fromPortablePath(e),r,s)}async renamePromise(e,r){return await new Promise((s,a)=>{this.realFs.rename(ue.fromPortablePath(e),ue.fromPortablePath(r),this.makeCallback(s,a))})}renameSync(e,r){return this.realFs.renameSync(ue.fromPortablePath(e),ue.fromPortablePath(r))}async copyFilePromise(e,r,s=0){return await new Promise((a,n)=>{this.realFs.copyFile(ue.fromPortablePath(e),ue.fromPortablePath(r),s,this.makeCallback(a,n))})}copyFileSync(e,r,s=0){return this.realFs.copyFileSync(ue.fromPortablePath(e),ue.fromPortablePath(r),s)}async appendFilePromise(e,r,s){return await new Promise((a,n)=>{let c=typeof e=="string"?ue.fromPortablePath(e):e;s?this.realFs.appendFile(c,r,s,this.makeCallback(a,n)):this.realFs.appendFile(c,r,this.makeCallback(a,n))})}appendFileSync(e,r,s){let a=typeof e=="string"?ue.fromPortablePath(e):e;s?this.realFs.appendFileSync(a,r,s):this.realFs.appendFileSync(a,r)}async writeFilePromise(e,r,s){return await new Promise((a,n)=>{let c=typeof e=="string"?ue.fromPortablePath(e):e;s?this.realFs.writeFile(c,r,s,this.makeCallback(a,n)):this.realFs.writeFile(c,r,this.makeCallback(a,n))})}writeFileSync(e,r,s){let a=typeof e=="string"?ue.fromPortablePath(e):e;s?this.realFs.writeFileSync(a,r,s):this.realFs.writeFileSync(a,r)}async unlinkPromise(e){return await new Promise((r,s)=>{this.realFs.unlink(ue.fromPortablePath(e),this.makeCallback(r,s))})}unlinkSync(e){return this.realFs.unlinkSync(ue.fromPortablePath(e))}async utimesPromise(e,r,s){return await new Promise((a,n)=>{this.realFs.utimes(ue.fromPortablePath(e),r,s,this.makeCallback(a,n))})}utimesSync(e,r,s){this.realFs.utimesSync(ue.fromPortablePath(e),r,s)}async lutimesPromise(e,r,s){return await new Promise((a,n)=>{this.realFs.lutimes(ue.fromPortablePath(e),r,s,this.makeCallback(a,n))})}lutimesSync(e,r,s){this.realFs.lutimesSync(ue.fromPortablePath(e),r,s)}async mkdirPromise(e,r){return await new Promise((s,a)=>{this.realFs.mkdir(ue.fromPortablePath(e),r,this.makeCallback(s,a))})}mkdirSync(e,r){return this.realFs.mkdirSync(ue.fromPortablePath(e),r)}async rmdirPromise(e,r){return await new Promise((s,a)=>{r?this.realFs.rmdir(ue.fromPortablePath(e),r,this.makeCallback(s,a)):this.realFs.rmdir(ue.fromPortablePath(e),this.makeCallback(s,a))})}rmdirSync(e,r){return this.realFs.rmdirSync(ue.fromPortablePath(e),r)}async rmPromise(e,r){return await new Promise((s,a)=>{r?this.realFs.rm(ue.fromPortablePath(e),r,this.makeCallback(s,a)):this.realFs.rm(ue.fromPortablePath(e),this.makeCallback(s,a))})}rmSync(e,r){return this.realFs.rmSync(ue.fromPortablePath(e),r)}async linkPromise(e,r){return await new Promise((s,a)=>{this.realFs.link(ue.fromPortablePath(e),ue.fromPortablePath(r),this.makeCallback(s,a))})}linkSync(e,r){return this.realFs.linkSync(ue.fromPortablePath(e),ue.fromPortablePath(r))}async symlinkPromise(e,r,s){return await new Promise((a,n)=>{this.realFs.symlink(ue.fromPortablePath(e.replace(/\/+$/,"")),ue.fromPortablePath(r),s,this.makeCallback(a,n))})}symlinkSync(e,r,s){return this.realFs.symlinkSync(ue.fromPortablePath(e.replace(/\/+$/,"")),ue.fromPortablePath(r),s)}async readFilePromise(e,r){return await new Promise((s,a)=>{let n=typeof e=="string"?ue.fromPortablePath(e):e;this.realFs.readFile(n,r,this.makeCallback(s,a))})}readFileSync(e,r){let s=typeof e=="string"?ue.fromPortablePath(e):e;return this.realFs.readFileSync(s,r)}async readdirPromise(e,r){return await new Promise((s,a)=>{r?r.recursive&&process.platform==="win32"?r.withFileTypes?this.realFs.readdir(ue.fromPortablePath(e),r,this.makeCallback(n=>s(n.map(k$)),a)):this.realFs.readdir(ue.fromPortablePath(e),r,this.makeCallback(n=>s(n.map(ue.toPortablePath)),a)):this.realFs.readdir(ue.fromPortablePath(e),r,this.makeCallback(s,a)):this.realFs.readdir(ue.fromPortablePath(e),this.makeCallback(s,a))})}readdirSync(e,r){return r?r.recursive&&process.platform==="win32"?r.withFileTypes?this.realFs.readdirSync(ue.fromPortablePath(e),r).map(k$):this.realFs.readdirSync(ue.fromPortablePath(e),r).map(ue.toPortablePath):this.realFs.readdirSync(ue.fromPortablePath(e),r):this.realFs.readdirSync(ue.fromPortablePath(e))}async readlinkPromise(e){return await new Promise((r,s)=>{this.realFs.readlink(ue.fromPortablePath(e),this.makeCallback(r,s))}).then(r=>ue.toPortablePath(r))}readlinkSync(e){return ue.toPortablePath(this.realFs.readlinkSync(ue.fromPortablePath(e)))}async truncatePromise(e,r){return await new Promise((s,a)=>{this.realFs.truncate(ue.fromPortablePath(e),r,this.makeCallback(s,a))})}truncateSync(e,r){return this.realFs.truncateSync(ue.fromPortablePath(e),r)}async ftruncatePromise(e,r){return await new Promise((s,a)=>{this.realFs.ftruncate(e,r,this.makeCallback(s,a))})}ftruncateSync(e,r){return this.realFs.ftruncateSync(e,r)}watch(e,r,s){return this.realFs.watch(ue.fromPortablePath(e),r,s)}watchFile(e,r,s){return this.realFs.watchFile(ue.fromPortablePath(e),r,s)}unwatchFile(e,r){return this.realFs.unwatchFile(ue.fromPortablePath(e),r)}makeCallback(e,r){return(s,a)=>{s?r(s):e(a)}}}});var Sn,T$=Ct(()=>{Id();Ip();tl();Sn=class extends js{constructor(e,{baseFs:r=new Yn}={}){super(K),this.target=this.pathUtils.normalize(e),this.baseFs=r}getRealPath(){return this.pathUtils.resolve(this.baseFs.getRealPath(),this.target)}resolve(e){return this.pathUtils.isAbsolute(e)?K.normalize(e):this.baseFs.resolve(K.join(this.target,e))}mapFromBase(e){return e}mapToBase(e){return this.pathUtils.isAbsolute(e)?e:this.pathUtils.join(this.target,e)}}});var R$,jf,F$=Ct(()=>{Id();Ip();tl();R$=vt.root,jf=class extends js{constructor(e,{baseFs:r=new Yn}={}){super(K),this.target=this.pathUtils.resolve(vt.root,e),this.baseFs=r}getRealPath(){return this.pathUtils.resolve(this.baseFs.getRealPath(),this.pathUtils.relative(vt.root,this.target))}getTarget(){return this.target}getBaseFs(){return this.baseFs}mapToBase(e){let r=this.pathUtils.normalize(e);if(this.pathUtils.isAbsolute(e))return this.pathUtils.resolve(this.target,this.pathUtils.relative(R$,e));if(r.match(/^\.\.\/?/))throw new Error(`Resolving this path (${e}) would escape the jail`);return this.pathUtils.resolve(this.target,e)}mapFromBase(e){return this.pathUtils.resolve(R$,this.pathUtils.relative(this.target,e))}}});var iE,N$=Ct(()=>{Ip();iE=class extends js{constructor(r,s){super(s);this.instance=null;this.factory=r}get baseFs(){return this.instance||(this.instance=this.factory()),this.instance}set baseFs(r){this.instance=r}mapFromBase(r){return r}mapToBase(r){return r}}});var Cd,rl,r0,O$=Ct(()=>{Cd=Ie("fs");Ed();Id();x_();ix();tl();rl=4278190080,r0=class extends Uf{constructor({baseFs:r=new Yn,filter:s=null,magicByte:a=42,maxOpenFiles:n=1/0,useCache:c=!0,maxAge:f=5e3,typeCheck:p=Cd.constants.S_IFREG,getMountPoint:h,factoryPromise:E,factorySync:C}){if(Math.floor(a)!==a||!(a>1&&a<=127))throw new Error("The magic byte must be set to a round value between 1 and 127 included");super();this.fdMap=new Map;this.nextFd=3;this.isMount=new Set;this.notMount=new Set;this.realPaths=new Map;this.limitOpenFilesTimeout=null;this.baseFs=r,this.mountInstances=c?new Map:null,this.factoryPromise=E,this.factorySync=C,this.filter=s,this.getMountPoint=h,this.magic=a<<24,this.maxAge=f,this.maxOpenFiles=n,this.typeCheck=p}getExtractHint(r){return this.baseFs.getExtractHint(r)}getRealPath(){return this.baseFs.getRealPath()}saveAndClose(){if(md(this),this.mountInstances)for(let[r,{childFs:s}]of this.mountInstances.entries())s.saveAndClose?.(),this.mountInstances.delete(r)}discardAndClose(){if(md(this),this.mountInstances)for(let[r,{childFs:s}]of this.mountInstances.entries())s.discardAndClose?.(),this.mountInstances.delete(r)}resolve(r){return this.baseFs.resolve(r)}remapFd(r,s){let a=this.nextFd++|this.magic;return this.fdMap.set(a,[r,s]),a}async openPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.openPromise(r,s,a),async(n,{subPath:c})=>this.remapFd(n,await n.openPromise(c,s,a)))}openSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.openSync(r,s,a),(n,{subPath:c})=>this.remapFd(n,n.openSync(c,s,a)))}async opendirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.opendirPromise(r,s),async(a,{subPath:n})=>await a.opendirPromise(n,s),{requireSubpath:!1})}opendirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.opendirSync(r,s),(a,{subPath:n})=>a.opendirSync(n,s),{requireSubpath:!1})}async readPromise(r,s,a,n,c){if((r&rl)!==this.magic)return await this.baseFs.readPromise(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>"u")throw Uo("read");let[p,h]=f;return await p.readPromise(h,s,a,n,c)}readSync(r,s,a,n,c){if((r&rl)!==this.magic)return this.baseFs.readSync(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>"u")throw Uo("readSync");let[p,h]=f;return p.readSync(h,s,a,n,c)}async writePromise(r,s,a,n,c){if((r&rl)!==this.magic)return typeof s=="string"?await this.baseFs.writePromise(r,s,a):await this.baseFs.writePromise(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>"u")throw Uo("write");let[p,h]=f;return typeof s=="string"?await p.writePromise(h,s,a):await p.writePromise(h,s,a,n,c)}writeSync(r,s,a,n,c){if((r&rl)!==this.magic)return typeof s=="string"?this.baseFs.writeSync(r,s,a):this.baseFs.writeSync(r,s,a,n,c);let f=this.fdMap.get(r);if(typeof f>"u")throw Uo("writeSync");let[p,h]=f;return typeof s=="string"?p.writeSync(h,s,a):p.writeSync(h,s,a,n,c)}async closePromise(r){if((r&rl)!==this.magic)return await this.baseFs.closePromise(r);let s=this.fdMap.get(r);if(typeof s>"u")throw Uo("close");this.fdMap.delete(r);let[a,n]=s;return await a.closePromise(n)}closeSync(r){if((r&rl)!==this.magic)return this.baseFs.closeSync(r);let s=this.fdMap.get(r);if(typeof s>"u")throw Uo("closeSync");this.fdMap.delete(r);let[a,n]=s;return a.closeSync(n)}createReadStream(r,s){return r===null?this.baseFs.createReadStream(r,s):this.makeCallSync(r,()=>this.baseFs.createReadStream(r,s),(a,{archivePath:n,subPath:c})=>{let f=a.createReadStream(c,s);return f.path=ue.fromPortablePath(this.pathUtils.join(n,c)),f})}createWriteStream(r,s){return r===null?this.baseFs.createWriteStream(r,s):this.makeCallSync(r,()=>this.baseFs.createWriteStream(r,s),(a,{subPath:n})=>a.createWriteStream(n,s))}async realpathPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.realpathPromise(r),async(s,{archivePath:a,subPath:n})=>{let c=this.realPaths.get(a);return typeof c>"u"&&(c=await this.baseFs.realpathPromise(a),this.realPaths.set(a,c)),this.pathUtils.join(c,this.pathUtils.relative(vt.root,await s.realpathPromise(n)))})}realpathSync(r){return this.makeCallSync(r,()=>this.baseFs.realpathSync(r),(s,{archivePath:a,subPath:n})=>{let c=this.realPaths.get(a);return typeof c>"u"&&(c=this.baseFs.realpathSync(a),this.realPaths.set(a,c)),this.pathUtils.join(c,this.pathUtils.relative(vt.root,s.realpathSync(n)))})}async existsPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.existsPromise(r),async(s,{subPath:a})=>await s.existsPromise(a))}existsSync(r){return this.makeCallSync(r,()=>this.baseFs.existsSync(r),(s,{subPath:a})=>s.existsSync(a))}async accessPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.accessPromise(r,s),async(a,{subPath:n})=>await a.accessPromise(n,s))}accessSync(r,s){return this.makeCallSync(r,()=>this.baseFs.accessSync(r,s),(a,{subPath:n})=>a.accessSync(n,s))}async statPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.statPromise(r,s),async(a,{subPath:n})=>await a.statPromise(n,s))}statSync(r,s){return this.makeCallSync(r,()=>this.baseFs.statSync(r,s),(a,{subPath:n})=>a.statSync(n,s))}async fstatPromise(r,s){if((r&rl)!==this.magic)return this.baseFs.fstatPromise(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw Uo("fstat");let[n,c]=a;return n.fstatPromise(c,s)}fstatSync(r,s){if((r&rl)!==this.magic)return this.baseFs.fstatSync(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw Uo("fstatSync");let[n,c]=a;return n.fstatSync(c,s)}async lstatPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.lstatPromise(r,s),async(a,{subPath:n})=>await a.lstatPromise(n,s))}lstatSync(r,s){return this.makeCallSync(r,()=>this.baseFs.lstatSync(r,s),(a,{subPath:n})=>a.lstatSync(n,s))}async fchmodPromise(r,s){if((r&rl)!==this.magic)return this.baseFs.fchmodPromise(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw Uo("fchmod");let[n,c]=a;return n.fchmodPromise(c,s)}fchmodSync(r,s){if((r&rl)!==this.magic)return this.baseFs.fchmodSync(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw Uo("fchmodSync");let[n,c]=a;return n.fchmodSync(c,s)}async chmodPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.chmodPromise(r,s),async(a,{subPath:n})=>await a.chmodPromise(n,s))}chmodSync(r,s){return this.makeCallSync(r,()=>this.baseFs.chmodSync(r,s),(a,{subPath:n})=>a.chmodSync(n,s))}async fchownPromise(r,s,a){if((r&rl)!==this.magic)return this.baseFs.fchownPromise(r,s,a);let n=this.fdMap.get(r);if(typeof n>"u")throw Uo("fchown");let[c,f]=n;return c.fchownPromise(f,s,a)}fchownSync(r,s,a){if((r&rl)!==this.magic)return this.baseFs.fchownSync(r,s,a);let n=this.fdMap.get(r);if(typeof n>"u")throw Uo("fchownSync");let[c,f]=n;return c.fchownSync(f,s,a)}async chownPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.chownPromise(r,s,a),async(n,{subPath:c})=>await n.chownPromise(c,s,a))}chownSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.chownSync(r,s,a),(n,{subPath:c})=>n.chownSync(c,s,a))}async renamePromise(r,s){return await this.makeCallPromise(r,async()=>await this.makeCallPromise(s,async()=>await this.baseFs.renamePromise(r,s),async()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})}),async(a,{subPath:n})=>await this.makeCallPromise(s,async()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})},async(c,{subPath:f})=>{if(a!==c)throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"});return await a.renamePromise(n,f)}))}renameSync(r,s){return this.makeCallSync(r,()=>this.makeCallSync(s,()=>this.baseFs.renameSync(r,s),()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})}),(a,{subPath:n})=>this.makeCallSync(s,()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})},(c,{subPath:f})=>{if(a!==c)throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"});return a.renameSync(n,f)}))}async copyFilePromise(r,s,a=0){let n=async(c,f,p,h)=>{if(a&Cd.constants.COPYFILE_FICLONE_FORCE)throw Object.assign(new Error(`EXDEV: cross-device clone not permitted, copyfile '${f}' -> ${h}'`),{code:"EXDEV"});if(a&Cd.constants.COPYFILE_EXCL&&await this.existsPromise(f))throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${f}' -> '${h}'`),{code:"EEXIST"});let E;try{E=await c.readFilePromise(f)}catch{throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${f}' -> '${h}'`),{code:"EINVAL"})}await p.writeFilePromise(h,E)};return await this.makeCallPromise(r,async()=>await this.makeCallPromise(s,async()=>await this.baseFs.copyFilePromise(r,s,a),async(c,{subPath:f})=>await n(this.baseFs,r,c,f)),async(c,{subPath:f})=>await this.makeCallPromise(s,async()=>await n(c,f,this.baseFs,s),async(p,{subPath:h})=>c!==p?await n(c,f,p,h):await c.copyFilePromise(f,h,a)))}copyFileSync(r,s,a=0){let n=(c,f,p,h)=>{if(a&Cd.constants.COPYFILE_FICLONE_FORCE)throw Object.assign(new Error(`EXDEV: cross-device clone not permitted, copyfile '${f}' -> ${h}'`),{code:"EXDEV"});if(a&Cd.constants.COPYFILE_EXCL&&this.existsSync(f))throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${f}' -> '${h}'`),{code:"EEXIST"});let E;try{E=c.readFileSync(f)}catch{throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${f}' -> '${h}'`),{code:"EINVAL"})}p.writeFileSync(h,E)};return this.makeCallSync(r,()=>this.makeCallSync(s,()=>this.baseFs.copyFileSync(r,s,a),(c,{subPath:f})=>n(this.baseFs,r,c,f)),(c,{subPath:f})=>this.makeCallSync(s,()=>n(c,f,this.baseFs,s),(p,{subPath:h})=>c!==p?n(c,f,p,h):c.copyFileSync(f,h,a)))}async appendFilePromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.appendFilePromise(r,s,a),async(n,{subPath:c})=>await n.appendFilePromise(c,s,a))}appendFileSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.appendFileSync(r,s,a),(n,{subPath:c})=>n.appendFileSync(c,s,a))}async writeFilePromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.writeFilePromise(r,s,a),async(n,{subPath:c})=>await n.writeFilePromise(c,s,a))}writeFileSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.writeFileSync(r,s,a),(n,{subPath:c})=>n.writeFileSync(c,s,a))}async unlinkPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.unlinkPromise(r),async(s,{subPath:a})=>await s.unlinkPromise(a))}unlinkSync(r){return this.makeCallSync(r,()=>this.baseFs.unlinkSync(r),(s,{subPath:a})=>s.unlinkSync(a))}async utimesPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.utimesPromise(r,s,a),async(n,{subPath:c})=>await n.utimesPromise(c,s,a))}utimesSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.utimesSync(r,s,a),(n,{subPath:c})=>n.utimesSync(c,s,a))}async lutimesPromise(r,s,a){return await this.makeCallPromise(r,async()=>await this.baseFs.lutimesPromise(r,s,a),async(n,{subPath:c})=>await n.lutimesPromise(c,s,a))}lutimesSync(r,s,a){return this.makeCallSync(r,()=>this.baseFs.lutimesSync(r,s,a),(n,{subPath:c})=>n.lutimesSync(c,s,a))}async mkdirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.mkdirPromise(r,s),async(a,{subPath:n})=>await a.mkdirPromise(n,s))}mkdirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.mkdirSync(r,s),(a,{subPath:n})=>a.mkdirSync(n,s))}async rmdirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.rmdirPromise(r,s),async(a,{subPath:n})=>await a.rmdirPromise(n,s))}rmdirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.rmdirSync(r,s),(a,{subPath:n})=>a.rmdirSync(n,s))}async rmPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.rmPromise(r,s),async(a,{subPath:n})=>await a.rmPromise(n,s))}rmSync(r,s){return this.makeCallSync(r,()=>this.baseFs.rmSync(r,s),(a,{subPath:n})=>a.rmSync(n,s))}async linkPromise(r,s){return await this.makeCallPromise(s,async()=>await this.baseFs.linkPromise(r,s),async(a,{subPath:n})=>await a.linkPromise(r,n))}linkSync(r,s){return this.makeCallSync(s,()=>this.baseFs.linkSync(r,s),(a,{subPath:n})=>a.linkSync(r,n))}async symlinkPromise(r,s,a){return await this.makeCallPromise(s,async()=>await this.baseFs.symlinkPromise(r,s,a),async(n,{subPath:c})=>await n.symlinkPromise(r,c))}symlinkSync(r,s,a){return this.makeCallSync(s,()=>this.baseFs.symlinkSync(r,s,a),(n,{subPath:c})=>n.symlinkSync(r,c))}async readFilePromise(r,s){return this.makeCallPromise(r,async()=>await this.baseFs.readFilePromise(r,s),async(a,{subPath:n})=>await a.readFilePromise(n,s))}readFileSync(r,s){return this.makeCallSync(r,()=>this.baseFs.readFileSync(r,s),(a,{subPath:n})=>a.readFileSync(n,s))}async readdirPromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.readdirPromise(r,s),async(a,{subPath:n})=>await a.readdirPromise(n,s),{requireSubpath:!1})}readdirSync(r,s){return this.makeCallSync(r,()=>this.baseFs.readdirSync(r,s),(a,{subPath:n})=>a.readdirSync(n,s),{requireSubpath:!1})}async readlinkPromise(r){return await this.makeCallPromise(r,async()=>await this.baseFs.readlinkPromise(r),async(s,{subPath:a})=>await s.readlinkPromise(a))}readlinkSync(r){return this.makeCallSync(r,()=>this.baseFs.readlinkSync(r),(s,{subPath:a})=>s.readlinkSync(a))}async truncatePromise(r,s){return await this.makeCallPromise(r,async()=>await this.baseFs.truncatePromise(r,s),async(a,{subPath:n})=>await a.truncatePromise(n,s))}truncateSync(r,s){return this.makeCallSync(r,()=>this.baseFs.truncateSync(r,s),(a,{subPath:n})=>a.truncateSync(n,s))}async ftruncatePromise(r,s){if((r&rl)!==this.magic)return this.baseFs.ftruncatePromise(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw Uo("ftruncate");let[n,c]=a;return n.ftruncatePromise(c,s)}ftruncateSync(r,s){if((r&rl)!==this.magic)return this.baseFs.ftruncateSync(r,s);let a=this.fdMap.get(r);if(typeof a>"u")throw Uo("ftruncateSync");let[n,c]=a;return n.ftruncateSync(c,s)}watch(r,s,a){return this.makeCallSync(r,()=>this.baseFs.watch(r,s,a),(n,{subPath:c})=>n.watch(c,s,a))}watchFile(r,s,a){return this.makeCallSync(r,()=>this.baseFs.watchFile(r,s,a),()=>nE(this,r,s,a))}unwatchFile(r,s){return this.makeCallSync(r,()=>this.baseFs.unwatchFile(r,s),()=>dd(this,r,s))}async makeCallPromise(r,s,a,{requireSubpath:n=!0}={}){if(typeof r!="string")return await s();let c=this.resolve(r),f=this.findMount(c);return f?n&&f.subPath==="/"?await s():await this.getMountPromise(f.archivePath,async p=>await a(p,f)):await s()}makeCallSync(r,s,a,{requireSubpath:n=!0}={}){if(typeof r!="string")return s();let c=this.resolve(r),f=this.findMount(c);return!f||n&&f.subPath==="/"?s():this.getMountSync(f.archivePath,p=>a(p,f))}findMount(r){if(this.filter&&!this.filter.test(r))return null;let s="";for(;;){let a=r.substring(s.length),n=this.getMountPoint(a,s);if(!n)return null;if(s=this.pathUtils.join(s,n),!this.isMount.has(s)){if(this.notMount.has(s))continue;try{if(this.typeCheck!==null&&(this.baseFs.statSync(s).mode&Cd.constants.S_IFMT)!==this.typeCheck){this.notMount.add(s);continue}}catch{return null}this.isMount.add(s)}return{archivePath:s,subPath:this.pathUtils.join(vt.root,r.substring(s.length))}}}limitOpenFiles(r){if(this.mountInstances===null)return;let s=Date.now(),a=s+this.maxAge,n=r===null?0:this.mountInstances.size-r;for(let[c,{childFs:f,expiresAt:p,refCount:h}]of this.mountInstances.entries())if(!(h!==0||f.hasOpenFileHandles?.())){if(s>=p){f.saveAndClose?.(),this.mountInstances.delete(c),n-=1;continue}else if(r===null||n<=0){a=p;break}f.saveAndClose?.(),this.mountInstances.delete(c),n-=1}this.limitOpenFilesTimeout===null&&(r===null&&this.mountInstances.size>0||r!==null)&&isFinite(a)&&(this.limitOpenFilesTimeout=setTimeout(()=>{this.limitOpenFilesTimeout=null,this.limitOpenFiles(null)},a-s).unref())}async getMountPromise(r,s){if(this.mountInstances){let a=this.mountInstances.get(r);if(!a){let n=await this.factoryPromise(this.baseFs,r);a=this.mountInstances.get(r),a||(a={childFs:n(),expiresAt:0,refCount:0})}this.mountInstances.delete(r),this.limitOpenFiles(this.maxOpenFiles-1),this.mountInstances.set(r,a),a.expiresAt=Date.now()+this.maxAge,a.refCount+=1;try{return await s(a.childFs)}finally{a.refCount-=1}}else{let a=(await this.factoryPromise(this.baseFs,r))();try{return await s(a)}finally{a.saveAndClose?.()}}}getMountSync(r,s){if(this.mountInstances){let a=this.mountInstances.get(r);return a||(a={childFs:this.factorySync(this.baseFs,r),expiresAt:0,refCount:0}),this.mountInstances.delete(r),this.limitOpenFiles(this.maxOpenFiles-1),this.mountInstances.set(r,a),a.expiresAt=Date.now()+this.maxAge,s(a.childFs)}else{let a=this.factorySync(this.baseFs,r);try{return s(a)}finally{a.saveAndClose?.()}}}}});var er,fx,L$=Ct(()=>{Ed();tl();er=()=>Object.assign(new Error("ENOSYS: unsupported filesystem access"),{code:"ENOSYS"}),fx=class t extends Ep{static{this.instance=new t}constructor(){super(K)}getExtractHint(){throw er()}getRealPath(){throw er()}resolve(){throw er()}async openPromise(){throw er()}openSync(){throw er()}async opendirPromise(){throw er()}opendirSync(){throw er()}async readPromise(){throw er()}readSync(){throw er()}async writePromise(){throw er()}writeSync(){throw er()}async closePromise(){throw er()}closeSync(){throw er()}createWriteStream(){throw er()}createReadStream(){throw er()}async realpathPromise(){throw er()}realpathSync(){throw er()}async readdirPromise(){throw er()}readdirSync(){throw er()}async existsPromise(e){throw er()}existsSync(e){throw er()}async accessPromise(){throw er()}accessSync(){throw er()}async statPromise(){throw er()}statSync(){throw er()}async fstatPromise(e){throw er()}fstatSync(e){throw er()}async lstatPromise(e){throw er()}lstatSync(e){throw er()}async fchmodPromise(){throw er()}fchmodSync(){throw er()}async chmodPromise(){throw er()}chmodSync(){throw er()}async fchownPromise(){throw er()}fchownSync(){throw er()}async chownPromise(){throw er()}chownSync(){throw er()}async mkdirPromise(){throw er()}mkdirSync(){throw er()}async rmdirPromise(){throw er()}rmdirSync(){throw er()}async rmPromise(){throw er()}rmSync(){throw er()}async linkPromise(){throw er()}linkSync(){throw er()}async symlinkPromise(){throw er()}symlinkSync(){throw er()}async renamePromise(){throw er()}renameSync(){throw er()}async copyFilePromise(){throw er()}copyFileSync(){throw er()}async appendFilePromise(){throw er()}appendFileSync(){throw er()}async writeFilePromise(){throw er()}writeFileSync(){throw er()}async unlinkPromise(){throw er()}unlinkSync(){throw er()}async utimesPromise(){throw er()}utimesSync(){throw er()}async lutimesPromise(){throw er()}lutimesSync(){throw er()}async readFilePromise(){throw er()}readFileSync(){throw er()}async readlinkPromise(){throw er()}readlinkSync(){throw er()}async truncatePromise(){throw er()}truncateSync(){throw er()}async ftruncatePromise(e,r){throw er()}ftruncateSync(e,r){throw er()}watch(){throw er()}watchFile(){throw er()}unwatchFile(){throw er()}}});var n0,M$=Ct(()=>{Ip();tl();n0=class extends js{constructor(e){super(ue),this.baseFs=e}mapFromBase(e){return ue.fromPortablePath(e)}mapToBase(e){return ue.toPortablePath(e)}}});var m7e,k_,y7e,Ao,_$=Ct(()=>{Id();Ip();tl();m7e=/^[0-9]+$/,k_=/^(\/(?:[^/]+\/)*?(?:\$\$virtual|__virtual__))((?:\/((?:[^/]+-)?[a-f0-9]+)(?:\/([^/]+))?)?((?:\/.*)?))$/,y7e=/^([^/]+-)?[a-f0-9]+$/,Ao=class t extends js{static makeVirtualPath(e,r,s){if(K.basename(e)!=="__virtual__")throw new Error('Assertion failed: Virtual folders must be named "__virtual__"');if(!K.basename(r).match(y7e))throw new Error("Assertion failed: Virtual components must be ended by an hexadecimal hash");let n=K.relative(K.dirname(e),s).split("/"),c=0;for(;c{Q_=et(Ie("buffer")),U$=Ie("url"),H$=Ie("util");Ip();tl();Ax=class extends js{constructor(e){super(ue),this.baseFs=e}mapFromBase(e){return e}mapToBase(e){if(typeof e=="string")return e;if(e instanceof URL)return(0,U$.fileURLToPath)(e);if(Buffer.isBuffer(e)){let r=e.toString();if(!E7e(e,r))throw new Error("Non-utf8 buffers are not supported at the moment. Please upvote the following issue if you encounter this error: https://github.com/yarnpkg/berry/issues/4942");return r}throw new Error(`Unsupported path type: ${(0,H$.inspect)(e)}`)}}});var V$,Ho,Cp,i0,px,hx,sE,Nu,Ou,q$,G$,W$,Y$,M2,K$=Ct(()=>{V$=Ie("readline"),Ho=Symbol("kBaseFs"),Cp=Symbol("kFd"),i0=Symbol("kClosePromise"),px=Symbol("kCloseResolve"),hx=Symbol("kCloseReject"),sE=Symbol("kRefs"),Nu=Symbol("kRef"),Ou=Symbol("kUnref"),M2=class{constructor(e,r){this[Y$]=1;this[W$]=void 0;this[G$]=void 0;this[q$]=void 0;this[Ho]=r,this[Cp]=e}get fd(){return this[Cp]}async appendFile(e,r){try{this[Nu](this.appendFile);let s=(typeof r=="string"?r:r?.encoding)??void 0;return await this[Ho].appendFilePromise(this.fd,e,s?{encoding:s}:void 0)}finally{this[Ou]()}}async chown(e,r){try{return this[Nu](this.chown),await this[Ho].fchownPromise(this.fd,e,r)}finally{this[Ou]()}}async chmod(e){try{return this[Nu](this.chmod),await this[Ho].fchmodPromise(this.fd,e)}finally{this[Ou]()}}createReadStream(e){return this[Ho].createReadStream(null,{...e,fd:this.fd})}createWriteStream(e){return this[Ho].createWriteStream(null,{...e,fd:this.fd})}datasync(){throw new Error("Method not implemented.")}sync(){throw new Error("Method not implemented.")}async read(e,r,s,a){try{this[Nu](this.read);let n;return Buffer.isBuffer(e)?n=e:(e??={},n=e.buffer??Buffer.alloc(16384),r=e.offset||0,s=e.length??n.byteLength,a=e.position??null),r??=0,s??=0,s===0?{bytesRead:s,buffer:n}:{bytesRead:await this[Ho].readPromise(this.fd,n,r,s,a),buffer:n}}finally{this[Ou]()}}async readFile(e){try{this[Nu](this.readFile);let r=(typeof e=="string"?e:e?.encoding)??void 0;return await this[Ho].readFilePromise(this.fd,r)}finally{this[Ou]()}}readLines(e){return(0,V$.createInterface)({input:this.createReadStream(e),crlfDelay:1/0})}async stat(e){try{return this[Nu](this.stat),await this[Ho].fstatPromise(this.fd,e)}finally{this[Ou]()}}async truncate(e){try{return this[Nu](this.truncate),await this[Ho].ftruncatePromise(this.fd,e)}finally{this[Ou]()}}utimes(e,r){throw new Error("Method not implemented.")}async writeFile(e,r){try{this[Nu](this.writeFile);let s=(typeof r=="string"?r:r?.encoding)??void 0;await this[Ho].writeFilePromise(this.fd,e,s)}finally{this[Ou]()}}async write(...e){try{if(this[Nu](this.write),ArrayBuffer.isView(e[0])){let[r,s,a,n]=e;return{bytesWritten:await this[Ho].writePromise(this.fd,r,s??void 0,a??void 0,n??void 0),buffer:r}}else{let[r,s,a]=e;return{bytesWritten:await this[Ho].writePromise(this.fd,r,s,a),buffer:r}}}finally{this[Ou]()}}async writev(e,r){try{this[Nu](this.writev);let s=0;if(typeof r<"u")for(let a of e){let n=await this.write(a,void 0,void 0,r);s+=n.bytesWritten,r+=n.bytesWritten}else for(let a of e){let n=await this.write(a);s+=n.bytesWritten}return{buffers:e,bytesWritten:s}}finally{this[Ou]()}}readv(e,r){throw new Error("Method not implemented.")}close(){if(this[Cp]===-1)return Promise.resolve();if(this[i0])return this[i0];if(this[sE]--,this[sE]===0){let e=this[Cp];this[Cp]=-1,this[i0]=this[Ho].closePromise(e).finally(()=>{this[i0]=void 0})}else this[i0]=new Promise((e,r)=>{this[px]=e,this[hx]=r}).finally(()=>{this[i0]=void 0,this[hx]=void 0,this[px]=void 0});return this[i0]}[(Ho,Cp,Y$=sE,W$=i0,G$=px,q$=hx,Nu)](e){if(this[Cp]===-1){let r=new Error("file closed");throw r.code="EBADF",r.syscall=e.name,r}this[sE]++}[Ou](){if(this[sE]--,this[sE]===0){let e=this[Cp];this[Cp]=-1,this[Ho].closePromise(e).then(this[px],this[hx])}}}});function _2(t,e){e=new Ax(e);let r=(s,a,n)=>{let c=s[a];s[a]=n,typeof c?.[oE.promisify.custom]<"u"&&(n[oE.promisify.custom]=c[oE.promisify.custom])};{r(t,"exists",(s,...a)=>{let c=typeof a[a.length-1]=="function"?a.pop():()=>{};process.nextTick(()=>{e.existsPromise(s).then(f=>{c(f)},()=>{c(!1)})})}),r(t,"read",(...s)=>{let[a,n,c,f,p,h]=s;if(s.length<=3){let E={};s.length<3?h=s[1]:(E=s[1],h=s[2]),{buffer:n=Buffer.alloc(16384),offset:c=0,length:f=n.byteLength,position:p}=E}if(c==null&&(c=0),f|=0,f===0){process.nextTick(()=>{h(null,0,n)});return}p==null&&(p=-1),process.nextTick(()=>{e.readPromise(a,n,c,f,p).then(E=>{h(null,E,n)},E=>{h(E,0,n)})})});for(let s of J$){let a=s.replace(/Promise$/,"");if(typeof t[a]>"u")continue;let n=e[s];if(typeof n>"u")continue;r(t,a,(...f)=>{let h=typeof f[f.length-1]=="function"?f.pop():()=>{};process.nextTick(()=>{n.apply(e,f).then(E=>{h(null,E)},E=>{h(E)})})})}t.realpath.native=t.realpath}{r(t,"existsSync",s=>{try{return e.existsSync(s)}catch{return!1}}),r(t,"readSync",(...s)=>{let[a,n,c,f,p]=s;return s.length<=3&&({offset:c=0,length:f=n.byteLength,position:p}=s[2]||{}),c==null&&(c=0),f|=0,f===0?0:(p==null&&(p=-1),e.readSync(a,n,c,f,p))});for(let s of I7e){let a=s;if(typeof t[a]>"u")continue;let n=e[s];typeof n>"u"||r(t,a,n.bind(e))}t.realpathSync.native=t.realpathSync}{let s=t.promises;for(let a of J$){let n=a.replace(/Promise$/,"");if(typeof s[n]>"u")continue;let c=e[a];typeof c>"u"||a!=="open"&&r(s,n,(f,...p)=>f instanceof M2?f[n].apply(f,p):c.call(e,f,...p))}r(s,"open",async(...a)=>{let n=await e.openPromise(...a);return new M2(n,e)})}t.read[oE.promisify.custom]=async(s,a,...n)=>({bytesRead:await e.readPromise(s,a,...n),buffer:a}),t.write[oE.promisify.custom]=async(s,a,...n)=>({bytesWritten:await e.writePromise(s,a,...n),buffer:a})}function gx(t,e){let r=Object.create(t);return _2(r,e),r}var oE,I7e,J$,z$=Ct(()=>{oE=Ie("util");j$();K$();I7e=new Set(["accessSync","appendFileSync","createReadStream","createWriteStream","chmodSync","fchmodSync","chownSync","fchownSync","closeSync","copyFileSync","linkSync","lstatSync","fstatSync","lutimesSync","mkdirSync","openSync","opendirSync","readlinkSync","readFileSync","readdirSync","readlinkSync","realpathSync","renameSync","rmdirSync","rmSync","statSync","symlinkSync","truncateSync","ftruncateSync","unlinkSync","unwatchFile","utimesSync","watch","watchFile","writeFileSync","writeSync"]),J$=new Set(["accessPromise","appendFilePromise","fchmodPromise","chmodPromise","fchownPromise","chownPromise","closePromise","copyFilePromise","linkPromise","fstatPromise","lstatPromise","lutimesPromise","mkdirPromise","openPromise","opendirPromise","readdirPromise","realpathPromise","readFilePromise","readdirPromise","readlinkPromise","renamePromise","rmdirPromise","rmPromise","statPromise","symlinkPromise","truncatePromise","ftruncatePromise","unlinkPromise","utimesPromise","writeFilePromise","writeSync"])});function Z$(t){let e=Math.ceil(Math.random()*4294967296).toString(16).padStart(8,"0");return`${t}${e}`}function X$(){if(T_)return T_;let t=ue.toPortablePath($$.default.tmpdir()),e=le.realpathSync(t);return process.once("exit",()=>{le.rmtempSync()}),T_={tmpdir:t,realTmpdir:e}}var $$,Lu,T_,le,eee=Ct(()=>{$$=et(Ie("os"));Id();tl();Lu=new Set,T_=null;le=Object.assign(new Yn,{detachTemp(t){Lu.delete(t)},mktempSync(t){let{tmpdir:e,realTmpdir:r}=X$();for(;;){let s=Z$("xfs-");try{this.mkdirSync(K.join(e,s))}catch(n){if(n.code==="EEXIST")continue;throw n}let a=K.join(r,s);if(Lu.add(a),typeof t>"u")return a;try{return t(a)}finally{if(Lu.has(a)){Lu.delete(a);try{this.removeSync(a)}catch{}}}}},async mktempPromise(t){let{tmpdir:e,realTmpdir:r}=X$();for(;;){let s=Z$("xfs-");try{await this.mkdirPromise(K.join(e,s))}catch(n){if(n.code==="EEXIST")continue;throw n}let a=K.join(r,s);if(Lu.add(a),typeof t>"u")return a;try{return await t(a)}finally{if(Lu.has(a)){Lu.delete(a);try{await this.removePromise(a)}catch{}}}}},async rmtempPromise(){await Promise.all(Array.from(Lu.values()).map(async t=>{try{await le.removePromise(t,{maxRetries:0}),Lu.delete(t)}catch{}}))},rmtempSync(){for(let t of Lu)try{le.removeSync(t),Lu.delete(t)}catch{}}})});var U2={};Vt(U2,{AliasFS:()=>Hf,BasePortableFakeFS:()=>Uf,CustomDir:()=>L2,CwdFS:()=>Sn,FakeFS:()=>Ep,Filename:()=>Er,JailFS:()=>jf,LazyFS:()=>iE,MountFS:()=>r0,NoFS:()=>fx,NodeFS:()=>Yn,PortablePath:()=>vt,PosixFS:()=>n0,ProxiedFS:()=>js,VirtualFS:()=>Ao,constants:()=>fi,errors:()=>or,extendFs:()=>gx,normalizeLineEndings:()=>yd,npath:()=>ue,opendir:()=>lx,patchFs:()=>_2,ppath:()=>K,setupCopyIndex:()=>ax,statUtils:()=>el,unwatchAllFiles:()=>md,unwatchFile:()=>dd,watchFile:()=>nE,xfs:()=>le});var bt=Ct(()=>{m$();ix();S_();P_();B$();x_();Ed();tl();tl();x$();Ed();T$();F$();N$();O$();L$();Id();M$();Ip();_$();z$();eee()});var see=L((wGt,iee)=>{iee.exports=nee;nee.sync=w7e;var tee=Ie("fs");function C7e(t,e){var r=e.pathExt!==void 0?e.pathExt:process.env.PATHEXT;if(!r||(r=r.split(";"),r.indexOf("")!==-1))return!0;for(var s=0;s{cee.exports=aee;aee.sync=B7e;var oee=Ie("fs");function aee(t,e,r){oee.stat(t,function(s,a){r(s,s?!1:lee(a,e))})}function B7e(t,e){return lee(oee.statSync(t),e)}function lee(t,e){return t.isFile()&&v7e(t,e)}function v7e(t,e){var r=t.mode,s=t.uid,a=t.gid,n=e.uid!==void 0?e.uid:process.getuid&&process.getuid(),c=e.gid!==void 0?e.gid:process.getgid&&process.getgid(),f=parseInt("100",8),p=parseInt("010",8),h=parseInt("001",8),E=f|p,C=r&h||r&p&&a===c||r&f&&s===n||r&E&&n===0;return C}});var Aee=L((SGt,fee)=>{var vGt=Ie("fs"),dx;process.platform==="win32"||global.TESTING_WINDOWS?dx=see():dx=uee();fee.exports=R_;R_.sync=S7e;function R_(t,e,r){if(typeof e=="function"&&(r=e,e={}),!r){if(typeof Promise!="function")throw new TypeError("callback not provided");return new Promise(function(s,a){R_(t,e||{},function(n,c){n?a(n):s(c)})})}dx(t,e||{},function(s,a){s&&(s.code==="EACCES"||e&&e.ignoreErrors)&&(s=null,a=!1),r(s,a)})}function S7e(t,e){try{return dx.sync(t,e||{})}catch(r){if(e&&e.ignoreErrors||r.code==="EACCES")return!1;throw r}}});var Eee=L((DGt,yee)=>{var aE=process.platform==="win32"||process.env.OSTYPE==="cygwin"||process.env.OSTYPE==="msys",pee=Ie("path"),D7e=aE?";":":",hee=Aee(),gee=t=>Object.assign(new Error(`not found: ${t}`),{code:"ENOENT"}),dee=(t,e)=>{let r=e.colon||D7e,s=t.match(/\//)||aE&&t.match(/\\/)?[""]:[...aE?[process.cwd()]:[],...(e.path||process.env.PATH||"").split(r)],a=aE?e.pathExt||process.env.PATHEXT||".EXE;.CMD;.BAT;.COM":"",n=aE?a.split(r):[""];return aE&&t.indexOf(".")!==-1&&n[0]!==""&&n.unshift(""),{pathEnv:s,pathExt:n,pathExtExe:a}},mee=(t,e,r)=>{typeof e=="function"&&(r=e,e={}),e||(e={});let{pathEnv:s,pathExt:a,pathExtExe:n}=dee(t,e),c=[],f=h=>new Promise((E,C)=>{if(h===s.length)return e.all&&c.length?E(c):C(gee(t));let S=s[h],P=/^".*"$/.test(S)?S.slice(1,-1):S,I=pee.join(P,t),R=!P&&/^\.[\\\/]/.test(t)?t.slice(0,2)+I:I;E(p(R,h,0))}),p=(h,E,C)=>new Promise((S,P)=>{if(C===a.length)return S(f(E+1));let I=a[C];hee(h+I,{pathExt:n},(R,N)=>{if(!R&&N)if(e.all)c.push(h+I);else return S(h+I);return S(p(h,E,C+1))})});return r?f(0).then(h=>r(null,h),r):f(0)},b7e=(t,e)=>{e=e||{};let{pathEnv:r,pathExt:s,pathExtExe:a}=dee(t,e),n=[];for(let c=0;c{"use strict";var Iee=(t={})=>{let e=t.env||process.env;return(t.platform||process.platform)!=="win32"?"PATH":Object.keys(e).reverse().find(s=>s.toUpperCase()==="PATH")||"Path"};F_.exports=Iee;F_.exports.default=Iee});var See=L((PGt,vee)=>{"use strict";var wee=Ie("path"),P7e=Eee(),x7e=Cee();function Bee(t,e){let r=t.options.env||process.env,s=process.cwd(),a=t.options.cwd!=null,n=a&&process.chdir!==void 0&&!process.chdir.disabled;if(n)try{process.chdir(t.options.cwd)}catch{}let c;try{c=P7e.sync(t.command,{path:r[x7e({env:r})],pathExt:e?wee.delimiter:void 0})}catch{}finally{n&&process.chdir(s)}return c&&(c=wee.resolve(a?t.options.cwd:"",c)),c}function k7e(t){return Bee(t)||Bee(t,!0)}vee.exports=k7e});var Dee=L((xGt,O_)=>{"use strict";var N_=/([()\][%!^"`<>&|;, *?])/g;function Q7e(t){return t=t.replace(N_,"^$1"),t}function T7e(t,e){return t=`${t}`,t=t.replace(/(?=(\\+?)?)\1"/g,'$1$1\\"'),t=t.replace(/(?=(\\+?)?)\1$/,"$1$1"),t=`"${t}"`,t=t.replace(N_,"^$1"),e&&(t=t.replace(N_,"^$1")),t}O_.exports.command=Q7e;O_.exports.argument=T7e});var Pee=L((kGt,bee)=>{"use strict";bee.exports=/^#!(.*)/});var kee=L((QGt,xee)=>{"use strict";var R7e=Pee();xee.exports=(t="")=>{let e=t.match(R7e);if(!e)return null;let[r,s]=e[0].replace(/#! ?/,"").split(" "),a=r.split("/").pop();return a==="env"?s:s?`${a} ${s}`:a}});var Tee=L((TGt,Qee)=>{"use strict";var L_=Ie("fs"),F7e=kee();function N7e(t){let r=Buffer.alloc(150),s;try{s=L_.openSync(t,"r"),L_.readSync(s,r,0,150,0),L_.closeSync(s)}catch{}return F7e(r.toString())}Qee.exports=N7e});var Oee=L((RGt,Nee)=>{"use strict";var O7e=Ie("path"),Ree=See(),Fee=Dee(),L7e=Tee(),M7e=process.platform==="win32",_7e=/\.(?:com|exe)$/i,U7e=/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;function H7e(t){t.file=Ree(t);let e=t.file&&L7e(t.file);return e?(t.args.unshift(t.file),t.command=e,Ree(t)):t.file}function j7e(t){if(!M7e)return t;let e=H7e(t),r=!_7e.test(e);if(t.options.forceShell||r){let s=U7e.test(e);t.command=O7e.normalize(t.command),t.command=Fee.command(t.command),t.args=t.args.map(n=>Fee.argument(n,s));let a=[t.command].concat(t.args).join(" ");t.args=["/d","/s","/c",`"${a}"`],t.command=process.env.comspec||"cmd.exe",t.options.windowsVerbatimArguments=!0}return t}function q7e(t,e,r){e&&!Array.isArray(e)&&(r=e,e=null),e=e?e.slice(0):[],r=Object.assign({},r);let s={command:t,args:e,options:r,file:void 0,original:{command:t,args:e}};return r.shell?s:j7e(s)}Nee.exports=q7e});var _ee=L((FGt,Mee)=>{"use strict";var M_=process.platform==="win32";function __(t,e){return Object.assign(new Error(`${e} ${t.command} ENOENT`),{code:"ENOENT",errno:"ENOENT",syscall:`${e} ${t.command}`,path:t.command,spawnargs:t.args})}function G7e(t,e){if(!M_)return;let r=t.emit;t.emit=function(s,a){if(s==="exit"){let n=Lee(a,e);if(n)return r.call(t,"error",n)}return r.apply(t,arguments)}}function Lee(t,e){return M_&&t===1&&!e.file?__(e.original,"spawn"):null}function W7e(t,e){return M_&&t===1&&!e.file?__(e.original,"spawnSync"):null}Mee.exports={hookChildProcess:G7e,verifyENOENT:Lee,verifyENOENTSync:W7e,notFoundError:__}});var j_=L((NGt,lE)=>{"use strict";var Uee=Ie("child_process"),U_=Oee(),H_=_ee();function Hee(t,e,r){let s=U_(t,e,r),a=Uee.spawn(s.command,s.args,s.options);return H_.hookChildProcess(a,s),a}function Y7e(t,e,r){let s=U_(t,e,r),a=Uee.spawnSync(s.command,s.args,s.options);return a.error=a.error||H_.verifyENOENTSync(a.status,s),a}lE.exports=Hee;lE.exports.spawn=Hee;lE.exports.sync=Y7e;lE.exports._parse=U_;lE.exports._enoent=H_});var qee=L((OGt,jee)=>{"use strict";function V7e(t,e){function r(){this.constructor=t}r.prototype=e.prototype,t.prototype=new r}function wd(t,e,r,s){this.message=t,this.expected=e,this.found=r,this.location=s,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,wd)}V7e(wd,Error);wd.buildMessage=function(t,e){var r={literal:function(h){return'"'+a(h.text)+'"'},class:function(h){var E="",C;for(C=0;C0){for(C=1,S=1;C>",b=ur(">>",!1),y=">&",F=ur(">&",!1),z=">",Z=ur(">",!1),$="<<<",oe=ur("<<<",!1),xe="<&",Te=ur("<&",!1),lt="<",It=ur("<",!1),qt=function(O){return{type:"argument",segments:[].concat(...O)}},ir=function(O){return O},Pt="$'",gn=ur("$'",!1),Pr="'",Ir=ur("'",!1),Nr=function(O){return[{type:"text",text:O}]},nn='""',ai=ur('""',!1),wo=function(){return{type:"text",text:""}},ns='"',to=ur('"',!1),Bo=function(O){return O},ji=function(O){return{type:"arithmetic",arithmetic:O,quoted:!0}},ro=function(O){return{type:"shell",shell:O,quoted:!0}},vo=function(O){return{type:"variable",...O,quoted:!0}},RA=function(O){return{type:"text",text:O}},pf=function(O){return{type:"arithmetic",arithmetic:O,quoted:!1}},yh=function(O){return{type:"shell",shell:O,quoted:!1}},Eh=function(O){return{type:"variable",...O,quoted:!1}},no=function(O){return{type:"glob",pattern:O}},jn=/^[^']/,Fs=Zi(["'"],!0,!1),io=function(O){return O.join("")},lu=/^[^$"]/,cu=Zi(["$",'"'],!0,!1),uu=`\\ `,FA=ur(`\\ `,!1),NA=function(){return""},aa="\\",la=ur("\\",!1),OA=/^[\\$"`]/,gr=Zi(["\\","$",'"',"`"],!1,!1),So=function(O){return O},Me="\\a",fu=ur("\\a",!1),Cr=function(){return"a"},hf="\\b",LA=ur("\\b",!1),MA=function(){return"\b"},Au=/^[Ee]/,pu=Zi(["E","e"],!1,!1),ac=function(){return"\x1B"},ve="\\f",Nt=ur("\\f",!1),lc=function(){return"\f"},Li="\\n",so=ur("\\n",!1),Rt=function(){return` `},xn="\\r",ca=ur("\\r",!1),qi=function(){return"\r"},Mi="\\t",Oa=ur("\\t",!1),dn=function(){return" "},Jn="\\v",hu=ur("\\v",!1),Ih=function(){return"\v"},La=/^[\\'"?]/,Ma=Zi(["\\","'",'"',"?"],!1,!1),Ua=function(O){return String.fromCharCode(parseInt(O,16))},Xe="\\x",Ha=ur("\\x",!1),gf="\\u",cc=ur("\\u",!1),wn="\\U",ua=ur("\\U",!1),_A=function(O){return String.fromCodePoint(parseInt(O,16))},UA=/^[0-7]/,fa=Zi([["0","7"]],!1,!1),vl=/^[0-9a-fA-f]/,Mt=Zi([["0","9"],["a","f"],["A","f"]],!1,!1),kn=Ef(),Aa="{}",ja=ur("{}",!1),is=function(){return"{}"},uc="-",gu=ur("-",!1),fc="+",qa=ur("+",!1),_i=".",ws=ur(".",!1),Sl=function(O,J,re){return{type:"number",value:(O==="-"?-1:1)*parseFloat(J.join("")+"."+re.join(""))}},df=function(O,J){return{type:"number",value:(O==="-"?-1:1)*parseInt(J.join(""))}},Ac=function(O){return{type:"variable",...O}},Bi=function(O){return{type:"variable",name:O}},Qn=function(O){return O},pc="*",Je=ur("*",!1),st="/",St=ur("/",!1),lr=function(O,J,re){return{type:J==="*"?"multiplication":"division",right:re}},ee=function(O,J){return J.reduce((re,de)=>({left:re,...de}),O)},Ee=function(O,J,re){return{type:J==="+"?"addition":"subtraction",right:re}},Oe="$((",gt=ur("$((",!1),yt="))",Dt=ur("))",!1),tr=function(O){return O},fn="$(",li=ur("$(",!1),Gi=function(O){return O},Tn="${",Ga=ur("${",!1),gy=":-",X1=ur(":-",!1),Do=function(O,J){return{name:O,defaultValue:J}},dy=":-}",Ch=ur(":-}",!1),$1=function(O){return{name:O,defaultValue:[]}},bo=":+",wh=ur(":+",!1),Bh=function(O,J){return{name:O,alternativeValue:J}},du=":+}",vh=ur(":+}",!1),Rg=function(O){return{name:O,alternativeValue:[]}},Fg=function(O){return{name:O}},Ng="$",my=ur("$",!1),mf=function(O){return e.isGlobPattern(O)},Po=function(O){return O},Dl=/^[a-zA-Z0-9_]/,Sh=Zi([["a","z"],["A","Z"],["0","9"],"_"],!1,!1),Og=function(){return Cy()},bl=/^[$@*?#a-zA-Z0-9_\-]/,Pl=Zi(["$","@","*","?","#",["a","z"],["A","Z"],["0","9"],"_","-"],!1,!1),yy=/^[()}<>$|&; \t"']/,HA=Zi(["(",")","}","<",">","$","|","&",";"," "," ",'"',"'"],!1,!1),Ey=/^[<>&; \t"']/,Iy=Zi(["<",">","&",";"," "," ",'"',"'"],!1,!1),jA=/^[ \t]/,qA=Zi([" "," "],!1,!1),Y=0,xt=0,GA=[{line:1,column:1}],xo=0,yf=[],mt=0,mu;if("startRule"in e){if(!(e.startRule in s))throw new Error(`Can't start parsing from rule "`+e.startRule+'".');a=s[e.startRule]}function Cy(){return t.substring(xt,Y)}function Lg(){return If(xt,Y)}function e2(O,J){throw J=J!==void 0?J:If(xt,Y),WA([Mg(O)],t.substring(xt,Y),J)}function Dh(O,J){throw J=J!==void 0?J:If(xt,Y),di(O,J)}function ur(O,J){return{type:"literal",text:O,ignoreCase:J}}function Zi(O,J,re){return{type:"class",parts:O,inverted:J,ignoreCase:re}}function Ef(){return{type:"any"}}function Wa(){return{type:"end"}}function Mg(O){return{type:"other",description:O}}function yu(O){var J=GA[O],re;if(J)return J;for(re=O-1;!GA[re];)re--;for(J=GA[re],J={line:J.line,column:J.column};rexo&&(xo=Y,yf=[]),yf.push(O))}function di(O,J){return new wd(O,null,null,J)}function WA(O,J,re){return new wd(wd.buildMessage(O,J),O,J,re)}function Ya(){var O,J,re;for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();return J!==r?(re=pa(),re===r&&(re=null),re!==r?(xt=O,J=n(re),O=J):(Y=O,O=r)):(Y=O,O=r),O}function pa(){var O,J,re,de,Ke;if(O=Y,J=bh(),J!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();re!==r?(de=_g(),de!==r?(Ke=Va(),Ke===r&&(Ke=null),Ke!==r?(xt=O,J=c(J,de,Ke),O=J):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r)}else Y=O,O=r;if(O===r)if(O=Y,J=bh(),J!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();re!==r?(de=_g(),de===r&&(de=null),de!==r?(xt=O,J=f(J,de),O=J):(Y=O,O=r)):(Y=O,O=r)}else Y=O,O=r;return O}function Va(){var O,J,re,de,Ke;for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();if(J!==r)if(re=pa(),re!==r){for(de=[],Ke=kt();Ke!==r;)de.push(Ke),Ke=kt();de!==r?(xt=O,J=p(re),O=J):(Y=O,O=r)}else Y=O,O=r;else Y=O,O=r;return O}function _g(){var O;return t.charCodeAt(Y)===59?(O=h,Y++):(O=r,mt===0&&wt(E)),O===r&&(t.charCodeAt(Y)===38?(O=C,Y++):(O=r,mt===0&&wt(S))),O}function bh(){var O,J,re;return O=Y,J=YA(),J!==r?(re=Ug(),re===r&&(re=null),re!==r?(xt=O,J=P(J,re),O=J):(Y=O,O=r)):(Y=O,O=r),O}function Ug(){var O,J,re,de,Ke,ft,dr;for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();if(J!==r)if(re=wy(),re!==r){for(de=[],Ke=kt();Ke!==r;)de.push(Ke),Ke=kt();if(de!==r)if(Ke=bh(),Ke!==r){for(ft=[],dr=kt();dr!==r;)ft.push(dr),dr=kt();ft!==r?(xt=O,J=I(re,Ke),O=J):(Y=O,O=r)}else Y=O,O=r;else Y=O,O=r}else Y=O,O=r;else Y=O,O=r;return O}function wy(){var O;return t.substr(Y,2)===R?(O=R,Y+=2):(O=r,mt===0&&wt(N)),O===r&&(t.substr(Y,2)===U?(O=U,Y+=2):(O=r,mt===0&&wt(W))),O}function YA(){var O,J,re;return O=Y,J=Cf(),J!==r?(re=Hg(),re===r&&(re=null),re!==r?(xt=O,J=te(J,re),O=J):(Y=O,O=r)):(Y=O,O=r),O}function Hg(){var O,J,re,de,Ke,ft,dr;for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();if(J!==r)if(re=Eu(),re!==r){for(de=[],Ke=kt();Ke!==r;)de.push(Ke),Ke=kt();if(de!==r)if(Ke=YA(),Ke!==r){for(ft=[],dr=kt();dr!==r;)ft.push(dr),dr=kt();ft!==r?(xt=O,J=ie(re,Ke),O=J):(Y=O,O=r)}else Y=O,O=r;else Y=O,O=r}else Y=O,O=r;else Y=O,O=r;return O}function Eu(){var O;return t.substr(Y,2)===Ae?(O=Ae,Y+=2):(O=r,mt===0&&wt(ce)),O===r&&(t.charCodeAt(Y)===124?(O=me,Y++):(O=r,mt===0&&wt(pe))),O}function Iu(){var O,J,re,de,Ke,ft;if(O=Y,J=kh(),J!==r)if(t.charCodeAt(Y)===61?(re=Be,Y++):(re=r,mt===0&&wt(Ce)),re!==r)if(de=VA(),de!==r){for(Ke=[],ft=kt();ft!==r;)Ke.push(ft),ft=kt();Ke!==r?(xt=O,J=g(J,de),O=J):(Y=O,O=r)}else Y=O,O=r;else Y=O,O=r;else Y=O,O=r;if(O===r)if(O=Y,J=kh(),J!==r)if(t.charCodeAt(Y)===61?(re=Be,Y++):(re=r,mt===0&&wt(Ce)),re!==r){for(de=[],Ke=kt();Ke!==r;)de.push(Ke),Ke=kt();de!==r?(xt=O,J=we(J),O=J):(Y=O,O=r)}else Y=O,O=r;else Y=O,O=r;return O}function Cf(){var O,J,re,de,Ke,ft,dr,Br,_n,mi,Bs;for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();if(J!==r)if(t.charCodeAt(Y)===40?(re=ye,Y++):(re=r,mt===0&&wt(fe)),re!==r){for(de=[],Ke=kt();Ke!==r;)de.push(Ke),Ke=kt();if(de!==r)if(Ke=pa(),Ke!==r){for(ft=[],dr=kt();dr!==r;)ft.push(dr),dr=kt();if(ft!==r)if(t.charCodeAt(Y)===41?(dr=se,Y++):(dr=r,mt===0&&wt(X)),dr!==r){for(Br=[],_n=kt();_n!==r;)Br.push(_n),_n=kt();if(Br!==r){for(_n=[],mi=qn();mi!==r;)_n.push(mi),mi=qn();if(_n!==r){for(mi=[],Bs=kt();Bs!==r;)mi.push(Bs),Bs=kt();mi!==r?(xt=O,J=De(Ke,_n),O=J):(Y=O,O=r)}else Y=O,O=r}else Y=O,O=r}else Y=O,O=r;else Y=O,O=r}else Y=O,O=r;else Y=O,O=r}else Y=O,O=r;else Y=O,O=r;if(O===r){for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();if(J!==r)if(t.charCodeAt(Y)===123?(re=Re,Y++):(re=r,mt===0&&wt(dt)),re!==r){for(de=[],Ke=kt();Ke!==r;)de.push(Ke),Ke=kt();if(de!==r)if(Ke=pa(),Ke!==r){for(ft=[],dr=kt();dr!==r;)ft.push(dr),dr=kt();if(ft!==r)if(t.charCodeAt(Y)===125?(dr=j,Y++):(dr=r,mt===0&&wt(rt)),dr!==r){for(Br=[],_n=kt();_n!==r;)Br.push(_n),_n=kt();if(Br!==r){for(_n=[],mi=qn();mi!==r;)_n.push(mi),mi=qn();if(_n!==r){for(mi=[],Bs=kt();Bs!==r;)mi.push(Bs),Bs=kt();mi!==r?(xt=O,J=Fe(Ke,_n),O=J):(Y=O,O=r)}else Y=O,O=r}else Y=O,O=r}else Y=O,O=r;else Y=O,O=r}else Y=O,O=r;else Y=O,O=r}else Y=O,O=r;else Y=O,O=r;if(O===r){for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();if(J!==r){for(re=[],de=Iu();de!==r;)re.push(de),de=Iu();if(re!==r){for(de=[],Ke=kt();Ke!==r;)de.push(Ke),Ke=kt();if(de!==r){if(Ke=[],ft=Cu(),ft!==r)for(;ft!==r;)Ke.push(ft),ft=Cu();else Ke=r;if(Ke!==r){for(ft=[],dr=kt();dr!==r;)ft.push(dr),dr=kt();ft!==r?(xt=O,J=Ne(re,Ke),O=J):(Y=O,O=r)}else Y=O,O=r}else Y=O,O=r}else Y=O,O=r}else Y=O,O=r;if(O===r){for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();if(J!==r){if(re=[],de=Iu(),de!==r)for(;de!==r;)re.push(de),de=Iu();else re=r;if(re!==r){for(de=[],Ke=kt();Ke!==r;)de.push(Ke),Ke=kt();de!==r?(xt=O,J=Pe(re),O=J):(Y=O,O=r)}else Y=O,O=r}else Y=O,O=r}}}return O}function Ns(){var O,J,re,de,Ke;for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();if(J!==r){if(re=[],de=ki(),de!==r)for(;de!==r;)re.push(de),de=ki();else re=r;if(re!==r){for(de=[],Ke=kt();Ke!==r;)de.push(Ke),Ke=kt();de!==r?(xt=O,J=Ye(re),O=J):(Y=O,O=r)}else Y=O,O=r}else Y=O,O=r;return O}function Cu(){var O,J,re;for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();if(J!==r?(re=qn(),re!==r?(xt=O,J=ke(re),O=J):(Y=O,O=r)):(Y=O,O=r),O===r){for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();J!==r?(re=ki(),re!==r?(xt=O,J=ke(re),O=J):(Y=O,O=r)):(Y=O,O=r)}return O}function qn(){var O,J,re,de,Ke;for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();return J!==r?(it.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(_e)),re===r&&(re=null),re!==r?(de=ss(),de!==r?(Ke=ki(),Ke!==r?(xt=O,J=x(re,de,Ke),O=J):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r),O}function ss(){var O;return t.substr(Y,2)===w?(O=w,Y+=2):(O=r,mt===0&&wt(b)),O===r&&(t.substr(Y,2)===y?(O=y,Y+=2):(O=r,mt===0&&wt(F)),O===r&&(t.charCodeAt(Y)===62?(O=z,Y++):(O=r,mt===0&&wt(Z)),O===r&&(t.substr(Y,3)===$?(O=$,Y+=3):(O=r,mt===0&&wt(oe)),O===r&&(t.substr(Y,2)===xe?(O=xe,Y+=2):(O=r,mt===0&&wt(Te)),O===r&&(t.charCodeAt(Y)===60?(O=lt,Y++):(O=r,mt===0&&wt(It))))))),O}function ki(){var O,J,re;for(O=Y,J=[],re=kt();re!==r;)J.push(re),re=kt();return J!==r?(re=VA(),re!==r?(xt=O,J=ke(re),O=J):(Y=O,O=r)):(Y=O,O=r),O}function VA(){var O,J,re;if(O=Y,J=[],re=wf(),re!==r)for(;re!==r;)J.push(re),re=wf();else J=r;return J!==r&&(xt=O,J=qt(J)),O=J,O}function wf(){var O,J;return O=Y,J=mn(),J!==r&&(xt=O,J=ir(J)),O=J,O===r&&(O=Y,J=jg(),J!==r&&(xt=O,J=ir(J)),O=J,O===r&&(O=Y,J=qg(),J!==r&&(xt=O,J=ir(J)),O=J,O===r&&(O=Y,J=os(),J!==r&&(xt=O,J=ir(J)),O=J))),O}function mn(){var O,J,re,de;return O=Y,t.substr(Y,2)===Pt?(J=Pt,Y+=2):(J=r,mt===0&&wt(gn)),J!==r?(re=yn(),re!==r?(t.charCodeAt(Y)===39?(de=Pr,Y++):(de=r,mt===0&&wt(Ir)),de!==r?(xt=O,J=Nr(re),O=J):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r),O}function jg(){var O,J,re,de;return O=Y,t.charCodeAt(Y)===39?(J=Pr,Y++):(J=r,mt===0&&wt(Ir)),J!==r?(re=Bf(),re!==r?(t.charCodeAt(Y)===39?(de=Pr,Y++):(de=r,mt===0&&wt(Ir)),de!==r?(xt=O,J=Nr(re),O=J):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r),O}function qg(){var O,J,re,de;if(O=Y,t.substr(Y,2)===nn?(J=nn,Y+=2):(J=r,mt===0&&wt(ai)),J!==r&&(xt=O,J=wo()),O=J,O===r)if(O=Y,t.charCodeAt(Y)===34?(J=ns,Y++):(J=r,mt===0&&wt(to)),J!==r){for(re=[],de=xl();de!==r;)re.push(de),de=xl();re!==r?(t.charCodeAt(Y)===34?(de=ns,Y++):(de=r,mt===0&&wt(to)),de!==r?(xt=O,J=Bo(re),O=J):(Y=O,O=r)):(Y=O,O=r)}else Y=O,O=r;return O}function os(){var O,J,re;if(O=Y,J=[],re=ko(),re!==r)for(;re!==r;)J.push(re),re=ko();else J=r;return J!==r&&(xt=O,J=Bo(J)),O=J,O}function xl(){var O,J;return O=Y,J=Xr(),J!==r&&(xt=O,J=ji(J)),O=J,O===r&&(O=Y,J=xh(),J!==r&&(xt=O,J=ro(J)),O=J,O===r&&(O=Y,J=JA(),J!==r&&(xt=O,J=vo(J)),O=J,O===r&&(O=Y,J=vf(),J!==r&&(xt=O,J=RA(J)),O=J))),O}function ko(){var O,J;return O=Y,J=Xr(),J!==r&&(xt=O,J=pf(J)),O=J,O===r&&(O=Y,J=xh(),J!==r&&(xt=O,J=yh(J)),O=J,O===r&&(O=Y,J=JA(),J!==r&&(xt=O,J=Eh(J)),O=J,O===r&&(O=Y,J=By(),J!==r&&(xt=O,J=no(J)),O=J,O===r&&(O=Y,J=Ph(),J!==r&&(xt=O,J=RA(J)),O=J)))),O}function Bf(){var O,J,re;for(O=Y,J=[],jn.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(Fs));re!==r;)J.push(re),jn.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(Fs));return J!==r&&(xt=O,J=io(J)),O=J,O}function vf(){var O,J,re;if(O=Y,J=[],re=kl(),re===r&&(lu.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(cu))),re!==r)for(;re!==r;)J.push(re),re=kl(),re===r&&(lu.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(cu)));else J=r;return J!==r&&(xt=O,J=io(J)),O=J,O}function kl(){var O,J,re;return O=Y,t.substr(Y,2)===uu?(J=uu,Y+=2):(J=r,mt===0&&wt(FA)),J!==r&&(xt=O,J=NA()),O=J,O===r&&(O=Y,t.charCodeAt(Y)===92?(J=aa,Y++):(J=r,mt===0&&wt(la)),J!==r?(OA.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(gr)),re!==r?(xt=O,J=So(re),O=J):(Y=O,O=r)):(Y=O,O=r)),O}function yn(){var O,J,re;for(O=Y,J=[],re=Qo(),re===r&&(jn.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(Fs)));re!==r;)J.push(re),re=Qo(),re===r&&(jn.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(Fs)));return J!==r&&(xt=O,J=io(J)),O=J,O}function Qo(){var O,J,re;return O=Y,t.substr(Y,2)===Me?(J=Me,Y+=2):(J=r,mt===0&&wt(fu)),J!==r&&(xt=O,J=Cr()),O=J,O===r&&(O=Y,t.substr(Y,2)===hf?(J=hf,Y+=2):(J=r,mt===0&&wt(LA)),J!==r&&(xt=O,J=MA()),O=J,O===r&&(O=Y,t.charCodeAt(Y)===92?(J=aa,Y++):(J=r,mt===0&&wt(la)),J!==r?(Au.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(pu)),re!==r?(xt=O,J=ac(),O=J):(Y=O,O=r)):(Y=O,O=r),O===r&&(O=Y,t.substr(Y,2)===ve?(J=ve,Y+=2):(J=r,mt===0&&wt(Nt)),J!==r&&(xt=O,J=lc()),O=J,O===r&&(O=Y,t.substr(Y,2)===Li?(J=Li,Y+=2):(J=r,mt===0&&wt(so)),J!==r&&(xt=O,J=Rt()),O=J,O===r&&(O=Y,t.substr(Y,2)===xn?(J=xn,Y+=2):(J=r,mt===0&&wt(ca)),J!==r&&(xt=O,J=qi()),O=J,O===r&&(O=Y,t.substr(Y,2)===Mi?(J=Mi,Y+=2):(J=r,mt===0&&wt(Oa)),J!==r&&(xt=O,J=dn()),O=J,O===r&&(O=Y,t.substr(Y,2)===Jn?(J=Jn,Y+=2):(J=r,mt===0&&wt(hu)),J!==r&&(xt=O,J=Ih()),O=J,O===r&&(O=Y,t.charCodeAt(Y)===92?(J=aa,Y++):(J=r,mt===0&&wt(la)),J!==r?(La.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(Ma)),re!==r?(xt=O,J=So(re),O=J):(Y=O,O=r)):(Y=O,O=r),O===r&&(O=wu()))))))))),O}function wu(){var O,J,re,de,Ke,ft,dr,Br,_n,mi,Bs,zA;return O=Y,t.charCodeAt(Y)===92?(J=aa,Y++):(J=r,mt===0&&wt(la)),J!==r?(re=ha(),re!==r?(xt=O,J=Ua(re),O=J):(Y=O,O=r)):(Y=O,O=r),O===r&&(O=Y,t.substr(Y,2)===Xe?(J=Xe,Y+=2):(J=r,mt===0&&wt(Ha)),J!==r?(re=Y,de=Y,Ke=ha(),Ke!==r?(ft=Os(),ft!==r?(Ke=[Ke,ft],de=Ke):(Y=de,de=r)):(Y=de,de=r),de===r&&(de=ha()),de!==r?re=t.substring(re,Y):re=de,re!==r?(xt=O,J=Ua(re),O=J):(Y=O,O=r)):(Y=O,O=r),O===r&&(O=Y,t.substr(Y,2)===gf?(J=gf,Y+=2):(J=r,mt===0&&wt(cc)),J!==r?(re=Y,de=Y,Ke=Os(),Ke!==r?(ft=Os(),ft!==r?(dr=Os(),dr!==r?(Br=Os(),Br!==r?(Ke=[Ke,ft,dr,Br],de=Ke):(Y=de,de=r)):(Y=de,de=r)):(Y=de,de=r)):(Y=de,de=r),de!==r?re=t.substring(re,Y):re=de,re!==r?(xt=O,J=Ua(re),O=J):(Y=O,O=r)):(Y=O,O=r),O===r&&(O=Y,t.substr(Y,2)===wn?(J=wn,Y+=2):(J=r,mt===0&&wt(ua)),J!==r?(re=Y,de=Y,Ke=Os(),Ke!==r?(ft=Os(),ft!==r?(dr=Os(),dr!==r?(Br=Os(),Br!==r?(_n=Os(),_n!==r?(mi=Os(),mi!==r?(Bs=Os(),Bs!==r?(zA=Os(),zA!==r?(Ke=[Ke,ft,dr,Br,_n,mi,Bs,zA],de=Ke):(Y=de,de=r)):(Y=de,de=r)):(Y=de,de=r)):(Y=de,de=r)):(Y=de,de=r)):(Y=de,de=r)):(Y=de,de=r)):(Y=de,de=r),de!==r?re=t.substring(re,Y):re=de,re!==r?(xt=O,J=_A(re),O=J):(Y=O,O=r)):(Y=O,O=r)))),O}function ha(){var O;return UA.test(t.charAt(Y))?(O=t.charAt(Y),Y++):(O=r,mt===0&&wt(fa)),O}function Os(){var O;return vl.test(t.charAt(Y))?(O=t.charAt(Y),Y++):(O=r,mt===0&&wt(Mt)),O}function Ph(){var O,J,re,de,Ke;if(O=Y,J=[],re=Y,t.charCodeAt(Y)===92?(de=aa,Y++):(de=r,mt===0&&wt(la)),de!==r?(t.length>Y?(Ke=t.charAt(Y),Y++):(Ke=r,mt===0&&wt(kn)),Ke!==r?(xt=re,de=So(Ke),re=de):(Y=re,re=r)):(Y=re,re=r),re===r&&(re=Y,t.substr(Y,2)===Aa?(de=Aa,Y+=2):(de=r,mt===0&&wt(ja)),de!==r&&(xt=re,de=is()),re=de,re===r&&(re=Y,de=Y,mt++,Ke=vy(),mt--,Ke===r?de=void 0:(Y=de,de=r),de!==r?(t.length>Y?(Ke=t.charAt(Y),Y++):(Ke=r,mt===0&&wt(kn)),Ke!==r?(xt=re,de=So(Ke),re=de):(Y=re,re=r)):(Y=re,re=r))),re!==r)for(;re!==r;)J.push(re),re=Y,t.charCodeAt(Y)===92?(de=aa,Y++):(de=r,mt===0&&wt(la)),de!==r?(t.length>Y?(Ke=t.charAt(Y),Y++):(Ke=r,mt===0&&wt(kn)),Ke!==r?(xt=re,de=So(Ke),re=de):(Y=re,re=r)):(Y=re,re=r),re===r&&(re=Y,t.substr(Y,2)===Aa?(de=Aa,Y+=2):(de=r,mt===0&&wt(ja)),de!==r&&(xt=re,de=is()),re=de,re===r&&(re=Y,de=Y,mt++,Ke=vy(),mt--,Ke===r?de=void 0:(Y=de,de=r),de!==r?(t.length>Y?(Ke=t.charAt(Y),Y++):(Ke=r,mt===0&&wt(kn)),Ke!==r?(xt=re,de=So(Ke),re=de):(Y=re,re=r)):(Y=re,re=r)));else J=r;return J!==r&&(xt=O,J=io(J)),O=J,O}function KA(){var O,J,re,de,Ke,ft;if(O=Y,t.charCodeAt(Y)===45?(J=uc,Y++):(J=r,mt===0&&wt(gu)),J===r&&(t.charCodeAt(Y)===43?(J=fc,Y++):(J=r,mt===0&&wt(qa))),J===r&&(J=null),J!==r){if(re=[],it.test(t.charAt(Y))?(de=t.charAt(Y),Y++):(de=r,mt===0&&wt(_e)),de!==r)for(;de!==r;)re.push(de),it.test(t.charAt(Y))?(de=t.charAt(Y),Y++):(de=r,mt===0&&wt(_e));else re=r;if(re!==r)if(t.charCodeAt(Y)===46?(de=_i,Y++):(de=r,mt===0&&wt(ws)),de!==r){if(Ke=[],it.test(t.charAt(Y))?(ft=t.charAt(Y),Y++):(ft=r,mt===0&&wt(_e)),ft!==r)for(;ft!==r;)Ke.push(ft),it.test(t.charAt(Y))?(ft=t.charAt(Y),Y++):(ft=r,mt===0&&wt(_e));else Ke=r;Ke!==r?(xt=O,J=Sl(J,re,Ke),O=J):(Y=O,O=r)}else Y=O,O=r;else Y=O,O=r}else Y=O,O=r;if(O===r){if(O=Y,t.charCodeAt(Y)===45?(J=uc,Y++):(J=r,mt===0&&wt(gu)),J===r&&(t.charCodeAt(Y)===43?(J=fc,Y++):(J=r,mt===0&&wt(qa))),J===r&&(J=null),J!==r){if(re=[],it.test(t.charAt(Y))?(de=t.charAt(Y),Y++):(de=r,mt===0&&wt(_e)),de!==r)for(;de!==r;)re.push(de),it.test(t.charAt(Y))?(de=t.charAt(Y),Y++):(de=r,mt===0&&wt(_e));else re=r;re!==r?(xt=O,J=df(J,re),O=J):(Y=O,O=r)}else Y=O,O=r;if(O===r&&(O=Y,J=JA(),J!==r&&(xt=O,J=Ac(J)),O=J,O===r&&(O=Y,J=hc(),J!==r&&(xt=O,J=Bi(J)),O=J,O===r)))if(O=Y,t.charCodeAt(Y)===40?(J=ye,Y++):(J=r,mt===0&&wt(fe)),J!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();if(re!==r)if(de=oo(),de!==r){for(Ke=[],ft=kt();ft!==r;)Ke.push(ft),ft=kt();Ke!==r?(t.charCodeAt(Y)===41?(ft=se,Y++):(ft=r,mt===0&&wt(X)),ft!==r?(xt=O,J=Qn(de),O=J):(Y=O,O=r)):(Y=O,O=r)}else Y=O,O=r;else Y=O,O=r}else Y=O,O=r}return O}function Sf(){var O,J,re,de,Ke,ft,dr,Br;if(O=Y,J=KA(),J!==r){for(re=[],de=Y,Ke=[],ft=kt();ft!==r;)Ke.push(ft),ft=kt();if(Ke!==r)if(t.charCodeAt(Y)===42?(ft=pc,Y++):(ft=r,mt===0&&wt(Je)),ft===r&&(t.charCodeAt(Y)===47?(ft=st,Y++):(ft=r,mt===0&&wt(St))),ft!==r){for(dr=[],Br=kt();Br!==r;)dr.push(Br),Br=kt();dr!==r?(Br=KA(),Br!==r?(xt=de,Ke=lr(J,ft,Br),de=Ke):(Y=de,de=r)):(Y=de,de=r)}else Y=de,de=r;else Y=de,de=r;for(;de!==r;){for(re.push(de),de=Y,Ke=[],ft=kt();ft!==r;)Ke.push(ft),ft=kt();if(Ke!==r)if(t.charCodeAt(Y)===42?(ft=pc,Y++):(ft=r,mt===0&&wt(Je)),ft===r&&(t.charCodeAt(Y)===47?(ft=st,Y++):(ft=r,mt===0&&wt(St))),ft!==r){for(dr=[],Br=kt();Br!==r;)dr.push(Br),Br=kt();dr!==r?(Br=KA(),Br!==r?(xt=de,Ke=lr(J,ft,Br),de=Ke):(Y=de,de=r)):(Y=de,de=r)}else Y=de,de=r;else Y=de,de=r}re!==r?(xt=O,J=ee(J,re),O=J):(Y=O,O=r)}else Y=O,O=r;return O}function oo(){var O,J,re,de,Ke,ft,dr,Br;if(O=Y,J=Sf(),J!==r){for(re=[],de=Y,Ke=[],ft=kt();ft!==r;)Ke.push(ft),ft=kt();if(Ke!==r)if(t.charCodeAt(Y)===43?(ft=fc,Y++):(ft=r,mt===0&&wt(qa)),ft===r&&(t.charCodeAt(Y)===45?(ft=uc,Y++):(ft=r,mt===0&&wt(gu))),ft!==r){for(dr=[],Br=kt();Br!==r;)dr.push(Br),Br=kt();dr!==r?(Br=Sf(),Br!==r?(xt=de,Ke=Ee(J,ft,Br),de=Ke):(Y=de,de=r)):(Y=de,de=r)}else Y=de,de=r;else Y=de,de=r;for(;de!==r;){for(re.push(de),de=Y,Ke=[],ft=kt();ft!==r;)Ke.push(ft),ft=kt();if(Ke!==r)if(t.charCodeAt(Y)===43?(ft=fc,Y++):(ft=r,mt===0&&wt(qa)),ft===r&&(t.charCodeAt(Y)===45?(ft=uc,Y++):(ft=r,mt===0&&wt(gu))),ft!==r){for(dr=[],Br=kt();Br!==r;)dr.push(Br),Br=kt();dr!==r?(Br=Sf(),Br!==r?(xt=de,Ke=Ee(J,ft,Br),de=Ke):(Y=de,de=r)):(Y=de,de=r)}else Y=de,de=r;else Y=de,de=r}re!==r?(xt=O,J=ee(J,re),O=J):(Y=O,O=r)}else Y=O,O=r;return O}function Xr(){var O,J,re,de,Ke,ft;if(O=Y,t.substr(Y,3)===Oe?(J=Oe,Y+=3):(J=r,mt===0&&wt(gt)),J!==r){for(re=[],de=kt();de!==r;)re.push(de),de=kt();if(re!==r)if(de=oo(),de!==r){for(Ke=[],ft=kt();ft!==r;)Ke.push(ft),ft=kt();Ke!==r?(t.substr(Y,2)===yt?(ft=yt,Y+=2):(ft=r,mt===0&&wt(Dt)),ft!==r?(xt=O,J=tr(de),O=J):(Y=O,O=r)):(Y=O,O=r)}else Y=O,O=r;else Y=O,O=r}else Y=O,O=r;return O}function xh(){var O,J,re,de;return O=Y,t.substr(Y,2)===fn?(J=fn,Y+=2):(J=r,mt===0&&wt(li)),J!==r?(re=pa(),re!==r?(t.charCodeAt(Y)===41?(de=se,Y++):(de=r,mt===0&&wt(X)),de!==r?(xt=O,J=Gi(re),O=J):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r),O}function JA(){var O,J,re,de,Ke,ft;return O=Y,t.substr(Y,2)===Tn?(J=Tn,Y+=2):(J=r,mt===0&&wt(Ga)),J!==r?(re=hc(),re!==r?(t.substr(Y,2)===gy?(de=gy,Y+=2):(de=r,mt===0&&wt(X1)),de!==r?(Ke=Ns(),Ke!==r?(t.charCodeAt(Y)===125?(ft=j,Y++):(ft=r,mt===0&&wt(rt)),ft!==r?(xt=O,J=Do(re,Ke),O=J):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r),O===r&&(O=Y,t.substr(Y,2)===Tn?(J=Tn,Y+=2):(J=r,mt===0&&wt(Ga)),J!==r?(re=hc(),re!==r?(t.substr(Y,3)===dy?(de=dy,Y+=3):(de=r,mt===0&&wt(Ch)),de!==r?(xt=O,J=$1(re),O=J):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r),O===r&&(O=Y,t.substr(Y,2)===Tn?(J=Tn,Y+=2):(J=r,mt===0&&wt(Ga)),J!==r?(re=hc(),re!==r?(t.substr(Y,2)===bo?(de=bo,Y+=2):(de=r,mt===0&&wt(wh)),de!==r?(Ke=Ns(),Ke!==r?(t.charCodeAt(Y)===125?(ft=j,Y++):(ft=r,mt===0&&wt(rt)),ft!==r?(xt=O,J=Bh(re,Ke),O=J):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r),O===r&&(O=Y,t.substr(Y,2)===Tn?(J=Tn,Y+=2):(J=r,mt===0&&wt(Ga)),J!==r?(re=hc(),re!==r?(t.substr(Y,3)===du?(de=du,Y+=3):(de=r,mt===0&&wt(vh)),de!==r?(xt=O,J=Rg(re),O=J):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r),O===r&&(O=Y,t.substr(Y,2)===Tn?(J=Tn,Y+=2):(J=r,mt===0&&wt(Ga)),J!==r?(re=hc(),re!==r?(t.charCodeAt(Y)===125?(de=j,Y++):(de=r,mt===0&&wt(rt)),de!==r?(xt=O,J=Fg(re),O=J):(Y=O,O=r)):(Y=O,O=r)):(Y=O,O=r),O===r&&(O=Y,t.charCodeAt(Y)===36?(J=Ng,Y++):(J=r,mt===0&&wt(my)),J!==r?(re=hc(),re!==r?(xt=O,J=Fg(re),O=J):(Y=O,O=r)):(Y=O,O=r)))))),O}function By(){var O,J,re;return O=Y,J=Gg(),J!==r?(xt=Y,re=mf(J),re?re=void 0:re=r,re!==r?(xt=O,J=Po(J),O=J):(Y=O,O=r)):(Y=O,O=r),O}function Gg(){var O,J,re,de,Ke;if(O=Y,J=[],re=Y,de=Y,mt++,Ke=Qh(),mt--,Ke===r?de=void 0:(Y=de,de=r),de!==r?(t.length>Y?(Ke=t.charAt(Y),Y++):(Ke=r,mt===0&&wt(kn)),Ke!==r?(xt=re,de=So(Ke),re=de):(Y=re,re=r)):(Y=re,re=r),re!==r)for(;re!==r;)J.push(re),re=Y,de=Y,mt++,Ke=Qh(),mt--,Ke===r?de=void 0:(Y=de,de=r),de!==r?(t.length>Y?(Ke=t.charAt(Y),Y++):(Ke=r,mt===0&&wt(kn)),Ke!==r?(xt=re,de=So(Ke),re=de):(Y=re,re=r)):(Y=re,re=r);else J=r;return J!==r&&(xt=O,J=io(J)),O=J,O}function kh(){var O,J,re;if(O=Y,J=[],Dl.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(Sh)),re!==r)for(;re!==r;)J.push(re),Dl.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(Sh));else J=r;return J!==r&&(xt=O,J=Og()),O=J,O}function hc(){var O,J,re;if(O=Y,J=[],bl.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(Pl)),re!==r)for(;re!==r;)J.push(re),bl.test(t.charAt(Y))?(re=t.charAt(Y),Y++):(re=r,mt===0&&wt(Pl));else J=r;return J!==r&&(xt=O,J=Og()),O=J,O}function vy(){var O;return yy.test(t.charAt(Y))?(O=t.charAt(Y),Y++):(O=r,mt===0&&wt(HA)),O}function Qh(){var O;return Ey.test(t.charAt(Y))?(O=t.charAt(Y),Y++):(O=r,mt===0&&wt(Iy)),O}function kt(){var O,J;if(O=[],jA.test(t.charAt(Y))?(J=t.charAt(Y),Y++):(J=r,mt===0&&wt(qA)),J!==r)for(;J!==r;)O.push(J),jA.test(t.charAt(Y))?(J=t.charAt(Y),Y++):(J=r,mt===0&&wt(qA));else O=r;return O}if(mu=a(),mu!==r&&Y===t.length)return mu;throw mu!==r&&Y!1}){try{return(0,Gee.parse)(t,e)}catch(r){throw r.location&&(r.message=r.message.replace(/(\.)?$/,` (line ${r.location.start.line}, column ${r.location.start.column})$1`)),r}}function cE(t,{endSemicolon:e=!1}={}){return t.map(({command:r,type:s},a)=>`${Ex(r)}${s===";"?a!==t.length-1||e?";":"":" &"}`).join(" ")}function Ex(t){return`${uE(t.chain)}${t.then?` ${q_(t.then)}`:""}`}function q_(t){return`${t.type} ${Ex(t.line)}`}function uE(t){return`${W_(t)}${t.then?` ${G_(t.then)}`:""}`}function G_(t){return`${t.type} ${uE(t.chain)}`}function W_(t){switch(t.type){case"command":return`${t.envs.length>0?`${t.envs.map(e=>mx(e)).join(" ")} `:""}${t.args.map(e=>Y_(e)).join(" ")}`;case"subshell":return`(${cE(t.subshell)})${t.args.length>0?` ${t.args.map(e=>H2(e)).join(" ")}`:""}`;case"group":return`{ ${cE(t.group,{endSemicolon:!0})} }${t.args.length>0?` ${t.args.map(e=>H2(e)).join(" ")}`:""}`;case"envs":return t.envs.map(e=>mx(e)).join(" ");default:throw new Error(`Unsupported command type: "${t.type}"`)}}function mx(t){return`${t.name}=${t.args[0]?Bd(t.args[0]):""}`}function Y_(t){switch(t.type){case"redirection":return H2(t);case"argument":return Bd(t);default:throw new Error(`Unsupported argument type: "${t.type}"`)}}function H2(t){return`${t.subtype} ${t.args.map(e=>Bd(e)).join(" ")}`}function Bd(t){return t.segments.map(e=>V_(e)).join("")}function V_(t){let e=(s,a)=>a?`"${s}"`:s,r=s=>s===""?"''":s.match(/[()}<>$|&;"'\n\t ]/)?s.match(/['\t\p{C}]/u)?s.match(/'/)?`"${s.replace(/["$\t\p{C}]/u,z7e)}"`:`$'${s.replace(/[\t\p{C}]/u,Yee)}'`:`'${s}'`:s;switch(t.type){case"text":return r(t.text);case"glob":return t.pattern;case"shell":return e(`$(${cE(t.shell)})`,t.quoted);case"variable":return e(typeof t.defaultValue>"u"?typeof t.alternativeValue>"u"?`\${${t.name}}`:t.alternativeValue.length===0?`\${${t.name}:+}`:`\${${t.name}:+${t.alternativeValue.map(s=>Bd(s)).join(" ")}}`:t.defaultValue.length===0?`\${${t.name}:-}`:`\${${t.name}:-${t.defaultValue.map(s=>Bd(s)).join(" ")}}`,t.quoted);case"arithmetic":return`$(( ${Ix(t.arithmetic)} ))`;default:throw new Error(`Unsupported argument segment type: "${t.type}"`)}}function Ix(t){let e=a=>{switch(a){case"addition":return"+";case"subtraction":return"-";case"multiplication":return"*";case"division":return"/";default:throw new Error(`Can't extract operator from arithmetic expression of type "${a}"`)}},r=(a,n)=>n?`( ${a} )`:a,s=a=>r(Ix(a),!["number","variable"].includes(a.type));switch(t.type){case"number":return String(t.value);case"variable":return t.name;default:return`${s(t.left)} ${e(t.type)} ${s(t.right)}`}}var Gee,Wee,J7e,Yee,z7e,Vee=Ct(()=>{Gee=et(qee());Wee=new Map([["\f","\\f"],[` `,"\\n"],["\r","\\r"],[" ","\\t"],["\v","\\v"],["\0","\\0"]]),J7e=new Map([["\\","\\\\"],["$","\\$"],['"','\\"'],...Array.from(Wee,([t,e])=>[t,`"$'${e}'"`])]),Yee=t=>Wee.get(t)??`\\x${t.charCodeAt(0).toString(16).padStart(2,"0")}`,z7e=t=>J7e.get(t)??`"$'${Yee(t)}'"`});var Jee=L((zGt,Kee)=>{"use strict";function Z7e(t,e){function r(){this.constructor=t}r.prototype=e.prototype,t.prototype=new r}function vd(t,e,r,s){this.message=t,this.expected=e,this.found=r,this.location=s,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,vd)}Z7e(vd,Error);vd.buildMessage=function(t,e){var r={literal:function(h){return'"'+a(h.text)+'"'},class:function(h){var E="",C;for(C=0;C0){for(C=1,S=1;CAe&&(Ae=W,ce=[]),ce.push(_e))}function rt(_e,x){return new vd(_e,null,null,x)}function Fe(_e,x,w){return new vd(vd.buildMessage(_e,x),_e,x,w)}function Ne(){var _e,x,w,b;return _e=W,x=Pe(),x!==r?(t.charCodeAt(W)===47?(w=n,W++):(w=r,me===0&&j(c)),w!==r?(b=Pe(),b!==r?(te=_e,x=f(x,b),_e=x):(W=_e,_e=r)):(W=_e,_e=r)):(W=_e,_e=r),_e===r&&(_e=W,x=Pe(),x!==r&&(te=_e,x=p(x)),_e=x),_e}function Pe(){var _e,x,w,b;return _e=W,x=Ye(),x!==r?(t.charCodeAt(W)===64?(w=h,W++):(w=r,me===0&&j(E)),w!==r?(b=it(),b!==r?(te=_e,x=C(x,b),_e=x):(W=_e,_e=r)):(W=_e,_e=r)):(W=_e,_e=r),_e===r&&(_e=W,x=Ye(),x!==r&&(te=_e,x=S(x)),_e=x),_e}function Ye(){var _e,x,w,b,y;return _e=W,t.charCodeAt(W)===64?(x=h,W++):(x=r,me===0&&j(E)),x!==r?(w=ke(),w!==r?(t.charCodeAt(W)===47?(b=n,W++):(b=r,me===0&&j(c)),b!==r?(y=ke(),y!==r?(te=_e,x=P(),_e=x):(W=_e,_e=r)):(W=_e,_e=r)):(W=_e,_e=r)):(W=_e,_e=r),_e===r&&(_e=W,x=ke(),x!==r&&(te=_e,x=P()),_e=x),_e}function ke(){var _e,x,w;if(_e=W,x=[],I.test(t.charAt(W))?(w=t.charAt(W),W++):(w=r,me===0&&j(R)),w!==r)for(;w!==r;)x.push(w),I.test(t.charAt(W))?(w=t.charAt(W),W++):(w=r,me===0&&j(R));else x=r;return x!==r&&(te=_e,x=P()),_e=x,_e}function it(){var _e,x,w;if(_e=W,x=[],N.test(t.charAt(W))?(w=t.charAt(W),W++):(w=r,me===0&&j(U)),w!==r)for(;w!==r;)x.push(w),N.test(t.charAt(W))?(w=t.charAt(W),W++):(w=r,me===0&&j(U));else x=r;return x!==r&&(te=_e,x=P()),_e=x,_e}if(pe=a(),pe!==r&&W===t.length)return pe;throw pe!==r&&W{zee=et(Jee())});var Dd=L((XGt,Sd)=>{"use strict";function Xee(t){return typeof t>"u"||t===null}function $7e(t){return typeof t=="object"&&t!==null}function eKe(t){return Array.isArray(t)?t:Xee(t)?[]:[t]}function tKe(t,e){var r,s,a,n;if(e)for(n=Object.keys(e),r=0,s=n.length;r{"use strict";function j2(t,e){Error.call(this),this.name="YAMLException",this.reason=t,this.mark=e,this.message=(this.reason||"(unknown reason)")+(this.mark?" "+this.mark.toString():""),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack||""}j2.prototype=Object.create(Error.prototype);j2.prototype.constructor=j2;j2.prototype.toString=function(e){var r=this.name+": ";return r+=this.reason||"(unknown reason)",!e&&this.mark&&(r+=" "+this.mark.toString()),r};$ee.exports=j2});var rte=L((e5t,tte)=>{"use strict";var ete=Dd();function K_(t,e,r,s,a){this.name=t,this.buffer=e,this.position=r,this.line=s,this.column=a}K_.prototype.getSnippet=function(e,r){var s,a,n,c,f;if(!this.buffer)return null;for(e=e||4,r=r||75,s="",a=this.position;a>0&&`\0\r \x85\u2028\u2029`.indexOf(this.buffer.charAt(a-1))===-1;)if(a-=1,this.position-a>r/2-1){s=" ... ",a+=5;break}for(n="",c=this.position;cr/2-1){n=" ... ",c-=5;break}return f=this.buffer.slice(a,c),ete.repeat(" ",e)+s+f+n+` `+ete.repeat(" ",e+this.position-a+s.length)+"^"};K_.prototype.toString=function(e){var r,s="";return this.name&&(s+='in "'+this.name+'" '),s+="at line "+(this.line+1)+", column "+(this.column+1),e||(r=this.getSnippet(),r&&(s+=`: `+r)),s};tte.exports=K_});var bs=L((t5t,ite)=>{"use strict";var nte=fE(),iKe=["kind","resolve","construct","instanceOf","predicate","represent","defaultStyle","styleAliases"],sKe=["scalar","sequence","mapping"];function oKe(t){var e={};return t!==null&&Object.keys(t).forEach(function(r){t[r].forEach(function(s){e[String(s)]=r})}),e}function aKe(t,e){if(e=e||{},Object.keys(e).forEach(function(r){if(iKe.indexOf(r)===-1)throw new nte('Unknown option "'+r+'" is met in definition of "'+t+'" YAML type.')}),this.tag=t,this.kind=e.kind||null,this.resolve=e.resolve||function(){return!0},this.construct=e.construct||function(r){return r},this.instanceOf=e.instanceOf||null,this.predicate=e.predicate||null,this.represent=e.represent||null,this.defaultStyle=e.defaultStyle||null,this.styleAliases=oKe(e.styleAliases||null),sKe.indexOf(this.kind)===-1)throw new nte('Unknown kind "'+this.kind+'" is specified for "'+t+'" YAML type.')}ite.exports=aKe});var bd=L((r5t,ote)=>{"use strict";var ste=Dd(),Bx=fE(),lKe=bs();function J_(t,e,r){var s=[];return t.include.forEach(function(a){r=J_(a,e,r)}),t[e].forEach(function(a){r.forEach(function(n,c){n.tag===a.tag&&n.kind===a.kind&&s.push(c)}),r.push(a)}),r.filter(function(a,n){return s.indexOf(n)===-1})}function cKe(){var t={scalar:{},sequence:{},mapping:{},fallback:{}},e,r;function s(a){t[a.kind][a.tag]=t.fallback[a.tag]=a}for(e=0,r=arguments.length;e{"use strict";var uKe=bs();ate.exports=new uKe("tag:yaml.org,2002:str",{kind:"scalar",construct:function(t){return t!==null?t:""}})});var ute=L((i5t,cte)=>{"use strict";var fKe=bs();cte.exports=new fKe("tag:yaml.org,2002:seq",{kind:"sequence",construct:function(t){return t!==null?t:[]}})});var Ate=L((s5t,fte)=>{"use strict";var AKe=bs();fte.exports=new AKe("tag:yaml.org,2002:map",{kind:"mapping",construct:function(t){return t!==null?t:{}}})});var vx=L((o5t,pte)=>{"use strict";var pKe=bd();pte.exports=new pKe({explicit:[lte(),ute(),Ate()]})});var gte=L((a5t,hte)=>{"use strict";var hKe=bs();function gKe(t){if(t===null)return!0;var e=t.length;return e===1&&t==="~"||e===4&&(t==="null"||t==="Null"||t==="NULL")}function dKe(){return null}function mKe(t){return t===null}hte.exports=new hKe("tag:yaml.org,2002:null",{kind:"scalar",resolve:gKe,construct:dKe,predicate:mKe,represent:{canonical:function(){return"~"},lowercase:function(){return"null"},uppercase:function(){return"NULL"},camelcase:function(){return"Null"}},defaultStyle:"lowercase"})});var mte=L((l5t,dte)=>{"use strict";var yKe=bs();function EKe(t){if(t===null)return!1;var e=t.length;return e===4&&(t==="true"||t==="True"||t==="TRUE")||e===5&&(t==="false"||t==="False"||t==="FALSE")}function IKe(t){return t==="true"||t==="True"||t==="TRUE"}function CKe(t){return Object.prototype.toString.call(t)==="[object Boolean]"}dte.exports=new yKe("tag:yaml.org,2002:bool",{kind:"scalar",resolve:EKe,construct:IKe,predicate:CKe,represent:{lowercase:function(t){return t?"true":"false"},uppercase:function(t){return t?"TRUE":"FALSE"},camelcase:function(t){return t?"True":"False"}},defaultStyle:"lowercase"})});var Ete=L((c5t,yte)=>{"use strict";var wKe=Dd(),BKe=bs();function vKe(t){return 48<=t&&t<=57||65<=t&&t<=70||97<=t&&t<=102}function SKe(t){return 48<=t&&t<=55}function DKe(t){return 48<=t&&t<=57}function bKe(t){if(t===null)return!1;var e=t.length,r=0,s=!1,a;if(!e)return!1;if(a=t[r],(a==="-"||a==="+")&&(a=t[++r]),a==="0"){if(r+1===e)return!0;if(a=t[++r],a==="b"){for(r++;r=0?"0b"+t.toString(2):"-0b"+t.toString(2).slice(1)},octal:function(t){return t>=0?"0"+t.toString(8):"-0"+t.toString(8).slice(1)},decimal:function(t){return t.toString(10)},hexadecimal:function(t){return t>=0?"0x"+t.toString(16).toUpperCase():"-0x"+t.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}})});var wte=L((u5t,Cte)=>{"use strict";var Ite=Dd(),kKe=bs(),QKe=new RegExp("^(?:[-+]?(?:0|[1-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");function TKe(t){return!(t===null||!QKe.test(t)||t[t.length-1]==="_")}function RKe(t){var e,r,s,a;return e=t.replace(/_/g,"").toLowerCase(),r=e[0]==="-"?-1:1,a=[],"+-".indexOf(e[0])>=0&&(e=e.slice(1)),e===".inf"?r===1?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:e===".nan"?NaN:e.indexOf(":")>=0?(e.split(":").forEach(function(n){a.unshift(parseFloat(n,10))}),e=0,s=1,a.forEach(function(n){e+=n*s,s*=60}),r*e):r*parseFloat(e,10)}var FKe=/^[-+]?[0-9]+e/;function NKe(t,e){var r;if(isNaN(t))switch(e){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===t)switch(e){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===t)switch(e){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(Ite.isNegativeZero(t))return"-0.0";return r=t.toString(10),FKe.test(r)?r.replace("e",".e"):r}function OKe(t){return Object.prototype.toString.call(t)==="[object Number]"&&(t%1!==0||Ite.isNegativeZero(t))}Cte.exports=new kKe("tag:yaml.org,2002:float",{kind:"scalar",resolve:TKe,construct:RKe,predicate:OKe,represent:NKe,defaultStyle:"lowercase"})});var z_=L((f5t,Bte)=>{"use strict";var LKe=bd();Bte.exports=new LKe({include:[vx()],implicit:[gte(),mte(),Ete(),wte()]})});var Z_=L((A5t,vte)=>{"use strict";var MKe=bd();vte.exports=new MKe({include:[z_()]})});var Pte=L((p5t,bte)=>{"use strict";var _Ke=bs(),Ste=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),Dte=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");function UKe(t){return t===null?!1:Ste.exec(t)!==null||Dte.exec(t)!==null}function HKe(t){var e,r,s,a,n,c,f,p=0,h=null,E,C,S;if(e=Ste.exec(t),e===null&&(e=Dte.exec(t)),e===null)throw new Error("Date resolve error");if(r=+e[1],s=+e[2]-1,a=+e[3],!e[4])return new Date(Date.UTC(r,s,a));if(n=+e[4],c=+e[5],f=+e[6],e[7]){for(p=e[7].slice(0,3);p.length<3;)p+="0";p=+p}return e[9]&&(E=+e[10],C=+(e[11]||0),h=(E*60+C)*6e4,e[9]==="-"&&(h=-h)),S=new Date(Date.UTC(r,s,a,n,c,f,p)),h&&S.setTime(S.getTime()-h),S}function jKe(t){return t.toISOString()}bte.exports=new _Ke("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:UKe,construct:HKe,instanceOf:Date,represent:jKe})});var kte=L((h5t,xte)=>{"use strict";var qKe=bs();function GKe(t){return t==="<<"||t===null}xte.exports=new qKe("tag:yaml.org,2002:merge",{kind:"scalar",resolve:GKe})});var Rte=L((g5t,Tte)=>{"use strict";var Pd;try{Qte=Ie,Pd=Qte("buffer").Buffer}catch{}var Qte,WKe=bs(),X_=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= \r`;function YKe(t){if(t===null)return!1;var e,r,s=0,a=t.length,n=X_;for(r=0;r64)){if(e<0)return!1;s+=6}return s%8===0}function VKe(t){var e,r,s=t.replace(/[\r\n=]/g,""),a=s.length,n=X_,c=0,f=[];for(e=0;e>16&255),f.push(c>>8&255),f.push(c&255)),c=c<<6|n.indexOf(s.charAt(e));return r=a%4*6,r===0?(f.push(c>>16&255),f.push(c>>8&255),f.push(c&255)):r===18?(f.push(c>>10&255),f.push(c>>2&255)):r===12&&f.push(c>>4&255),Pd?Pd.from?Pd.from(f):new Pd(f):f}function KKe(t){var e="",r=0,s,a,n=t.length,c=X_;for(s=0;s>18&63],e+=c[r>>12&63],e+=c[r>>6&63],e+=c[r&63]),r=(r<<8)+t[s];return a=n%3,a===0?(e+=c[r>>18&63],e+=c[r>>12&63],e+=c[r>>6&63],e+=c[r&63]):a===2?(e+=c[r>>10&63],e+=c[r>>4&63],e+=c[r<<2&63],e+=c[64]):a===1&&(e+=c[r>>2&63],e+=c[r<<4&63],e+=c[64],e+=c[64]),e}function JKe(t){return Pd&&Pd.isBuffer(t)}Tte.exports=new WKe("tag:yaml.org,2002:binary",{kind:"scalar",resolve:YKe,construct:VKe,predicate:JKe,represent:KKe})});var Nte=L((m5t,Fte)=>{"use strict";var zKe=bs(),ZKe=Object.prototype.hasOwnProperty,XKe=Object.prototype.toString;function $Ke(t){if(t===null)return!0;var e=[],r,s,a,n,c,f=t;for(r=0,s=f.length;r{"use strict";var tJe=bs(),rJe=Object.prototype.toString;function nJe(t){if(t===null)return!0;var e,r,s,a,n,c=t;for(n=new Array(c.length),e=0,r=c.length;e{"use strict";var sJe=bs(),oJe=Object.prototype.hasOwnProperty;function aJe(t){if(t===null)return!0;var e,r=t;for(e in r)if(oJe.call(r,e)&&r[e]!==null)return!1;return!0}function lJe(t){return t!==null?t:{}}Mte.exports=new sJe("tag:yaml.org,2002:set",{kind:"mapping",resolve:aJe,construct:lJe})});var pE=L((I5t,Ute)=>{"use strict";var cJe=bd();Ute.exports=new cJe({include:[Z_()],implicit:[Pte(),kte()],explicit:[Rte(),Nte(),Lte(),_te()]})});var jte=L((C5t,Hte)=>{"use strict";var uJe=bs();function fJe(){return!0}function AJe(){}function pJe(){return""}function hJe(t){return typeof t>"u"}Hte.exports=new uJe("tag:yaml.org,2002:js/undefined",{kind:"scalar",resolve:fJe,construct:AJe,predicate:hJe,represent:pJe})});var Gte=L((w5t,qte)=>{"use strict";var gJe=bs();function dJe(t){if(t===null||t.length===0)return!1;var e=t,r=/\/([gim]*)$/.exec(t),s="";return!(e[0]==="/"&&(r&&(s=r[1]),s.length>3||e[e.length-s.length-1]!=="/"))}function mJe(t){var e=t,r=/\/([gim]*)$/.exec(t),s="";return e[0]==="/"&&(r&&(s=r[1]),e=e.slice(1,e.length-s.length-1)),new RegExp(e,s)}function yJe(t){var e="/"+t.source+"/";return t.global&&(e+="g"),t.multiline&&(e+="m"),t.ignoreCase&&(e+="i"),e}function EJe(t){return Object.prototype.toString.call(t)==="[object RegExp]"}qte.exports=new gJe("tag:yaml.org,2002:js/regexp",{kind:"scalar",resolve:dJe,construct:mJe,predicate:EJe,represent:yJe})});var Vte=L((B5t,Yte)=>{"use strict";var Sx;try{Wte=Ie,Sx=Wte("esprima")}catch{typeof window<"u"&&(Sx=window.esprima)}var Wte,IJe=bs();function CJe(t){if(t===null)return!1;try{var e="("+t+")",r=Sx.parse(e,{range:!0});return!(r.type!=="Program"||r.body.length!==1||r.body[0].type!=="ExpressionStatement"||r.body[0].expression.type!=="ArrowFunctionExpression"&&r.body[0].expression.type!=="FunctionExpression")}catch{return!1}}function wJe(t){var e="("+t+")",r=Sx.parse(e,{range:!0}),s=[],a;if(r.type!=="Program"||r.body.length!==1||r.body[0].type!=="ExpressionStatement"||r.body[0].expression.type!=="ArrowFunctionExpression"&&r.body[0].expression.type!=="FunctionExpression")throw new Error("Failed to resolve function");return r.body[0].expression.params.forEach(function(n){s.push(n.name)}),a=r.body[0].expression.body.range,r.body[0].expression.body.type==="BlockStatement"?new Function(s,e.slice(a[0]+1,a[1]-1)):new Function(s,"return "+e.slice(a[0],a[1]))}function BJe(t){return t.toString()}function vJe(t){return Object.prototype.toString.call(t)==="[object Function]"}Yte.exports=new IJe("tag:yaml.org,2002:js/function",{kind:"scalar",resolve:CJe,construct:wJe,predicate:vJe,represent:BJe})});var q2=L((S5t,Jte)=>{"use strict";var Kte=bd();Jte.exports=Kte.DEFAULT=new Kte({include:[pE()],explicit:[jte(),Gte(),Vte()]})});var hre=L((D5t,G2)=>{"use strict";var wp=Dd(),rre=fE(),SJe=rte(),nre=pE(),DJe=q2(),o0=Object.prototype.hasOwnProperty,Dx=1,ire=2,sre=3,bx=4,$_=1,bJe=2,zte=3,PJe=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,xJe=/[\x85\u2028\u2029]/,kJe=/[,\[\]\{\}]/,ore=/^(?:!|!!|![a-z\-]+!)$/i,are=/^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i;function Zte(t){return Object.prototype.toString.call(t)}function qf(t){return t===10||t===13}function kd(t){return t===9||t===32}function nl(t){return t===9||t===32||t===10||t===13}function hE(t){return t===44||t===91||t===93||t===123||t===125}function QJe(t){var e;return 48<=t&&t<=57?t-48:(e=t|32,97<=e&&e<=102?e-97+10:-1)}function TJe(t){return t===120?2:t===117?4:t===85?8:0}function RJe(t){return 48<=t&&t<=57?t-48:-1}function Xte(t){return t===48?"\0":t===97?"\x07":t===98?"\b":t===116||t===9?" ":t===110?` `:t===118?"\v":t===102?"\f":t===114?"\r":t===101?"\x1B":t===32?" ":t===34?'"':t===47?"/":t===92?"\\":t===78?"\x85":t===95?"\xA0":t===76?"\u2028":t===80?"\u2029":""}function FJe(t){return t<=65535?String.fromCharCode(t):String.fromCharCode((t-65536>>10)+55296,(t-65536&1023)+56320)}var lre=new Array(256),cre=new Array(256);for(xd=0;xd<256;xd++)lre[xd]=Xte(xd)?1:0,cre[xd]=Xte(xd);var xd;function NJe(t,e){this.input=t,this.filename=e.filename||null,this.schema=e.schema||DJe,this.onWarning=e.onWarning||null,this.legacy=e.legacy||!1,this.json=e.json||!1,this.listener=e.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=t.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.documents=[]}function ure(t,e){return new rre(e,new SJe(t.filename,t.input,t.position,t.line,t.position-t.lineStart))}function Rr(t,e){throw ure(t,e)}function Px(t,e){t.onWarning&&t.onWarning.call(null,ure(t,e))}var $te={YAML:function(e,r,s){var a,n,c;e.version!==null&&Rr(e,"duplication of %YAML directive"),s.length!==1&&Rr(e,"YAML directive accepts exactly one argument"),a=/^([0-9]+)\.([0-9]+)$/.exec(s[0]),a===null&&Rr(e,"ill-formed argument of the YAML directive"),n=parseInt(a[1],10),c=parseInt(a[2],10),n!==1&&Rr(e,"unacceptable YAML version of the document"),e.version=s[0],e.checkLineBreaks=c<2,c!==1&&c!==2&&Px(e,"unsupported YAML version of the document")},TAG:function(e,r,s){var a,n;s.length!==2&&Rr(e,"TAG directive accepts exactly two arguments"),a=s[0],n=s[1],ore.test(a)||Rr(e,"ill-formed tag handle (first argument) of the TAG directive"),o0.call(e.tagMap,a)&&Rr(e,'there is a previously declared suffix for "'+a+'" tag handle'),are.test(n)||Rr(e,"ill-formed tag prefix (second argument) of the TAG directive"),e.tagMap[a]=n}};function s0(t,e,r,s){var a,n,c,f;if(e1&&(t.result+=wp.repeat(` `,e-1))}function OJe(t,e,r){var s,a,n,c,f,p,h,E,C=t.kind,S=t.result,P;if(P=t.input.charCodeAt(t.position),nl(P)||hE(P)||P===35||P===38||P===42||P===33||P===124||P===62||P===39||P===34||P===37||P===64||P===96||(P===63||P===45)&&(a=t.input.charCodeAt(t.position+1),nl(a)||r&&hE(a)))return!1;for(t.kind="scalar",t.result="",n=c=t.position,f=!1;P!==0;){if(P===58){if(a=t.input.charCodeAt(t.position+1),nl(a)||r&&hE(a))break}else if(P===35){if(s=t.input.charCodeAt(t.position-1),nl(s))break}else{if(t.position===t.lineStart&&xx(t)||r&&hE(P))break;if(qf(P))if(p=t.line,h=t.lineStart,E=t.lineIndent,ls(t,!1,-1),t.lineIndent>=e){f=!0,P=t.input.charCodeAt(t.position);continue}else{t.position=c,t.line=p,t.lineStart=h,t.lineIndent=E;break}}f&&(s0(t,n,c,!1),tU(t,t.line-p),n=c=t.position,f=!1),kd(P)||(c=t.position+1),P=t.input.charCodeAt(++t.position)}return s0(t,n,c,!1),t.result?!0:(t.kind=C,t.result=S,!1)}function LJe(t,e){var r,s,a;if(r=t.input.charCodeAt(t.position),r!==39)return!1;for(t.kind="scalar",t.result="",t.position++,s=a=t.position;(r=t.input.charCodeAt(t.position))!==0;)if(r===39)if(s0(t,s,t.position,!0),r=t.input.charCodeAt(++t.position),r===39)s=t.position,t.position++,a=t.position;else return!0;else qf(r)?(s0(t,s,a,!0),tU(t,ls(t,!1,e)),s=a=t.position):t.position===t.lineStart&&xx(t)?Rr(t,"unexpected end of the document within a single quoted scalar"):(t.position++,a=t.position);Rr(t,"unexpected end of the stream within a single quoted scalar")}function MJe(t,e){var r,s,a,n,c,f;if(f=t.input.charCodeAt(t.position),f!==34)return!1;for(t.kind="scalar",t.result="",t.position++,r=s=t.position;(f=t.input.charCodeAt(t.position))!==0;){if(f===34)return s0(t,r,t.position,!0),t.position++,!0;if(f===92){if(s0(t,r,t.position,!0),f=t.input.charCodeAt(++t.position),qf(f))ls(t,!1,e);else if(f<256&&lre[f])t.result+=cre[f],t.position++;else if((c=TJe(f))>0){for(a=c,n=0;a>0;a--)f=t.input.charCodeAt(++t.position),(c=QJe(f))>=0?n=(n<<4)+c:Rr(t,"expected hexadecimal character");t.result+=FJe(n),t.position++}else Rr(t,"unknown escape sequence");r=s=t.position}else qf(f)?(s0(t,r,s,!0),tU(t,ls(t,!1,e)),r=s=t.position):t.position===t.lineStart&&xx(t)?Rr(t,"unexpected end of the document within a double quoted scalar"):(t.position++,s=t.position)}Rr(t,"unexpected end of the stream within a double quoted scalar")}function _Je(t,e){var r=!0,s,a=t.tag,n,c=t.anchor,f,p,h,E,C,S={},P,I,R,N;if(N=t.input.charCodeAt(t.position),N===91)p=93,C=!1,n=[];else if(N===123)p=125,C=!0,n={};else return!1;for(t.anchor!==null&&(t.anchorMap[t.anchor]=n),N=t.input.charCodeAt(++t.position);N!==0;){if(ls(t,!0,e),N=t.input.charCodeAt(t.position),N===p)return t.position++,t.tag=a,t.anchor=c,t.kind=C?"mapping":"sequence",t.result=n,!0;r||Rr(t,"missed comma between flow collection entries"),I=P=R=null,h=E=!1,N===63&&(f=t.input.charCodeAt(t.position+1),nl(f)&&(h=E=!0,t.position++,ls(t,!0,e))),s=t.line,dE(t,e,Dx,!1,!0),I=t.tag,P=t.result,ls(t,!0,e),N=t.input.charCodeAt(t.position),(E||t.line===s)&&N===58&&(h=!0,N=t.input.charCodeAt(++t.position),ls(t,!0,e),dE(t,e,Dx,!1,!0),R=t.result),C?gE(t,n,S,I,P,R):h?n.push(gE(t,null,S,I,P,R)):n.push(P),ls(t,!0,e),N=t.input.charCodeAt(t.position),N===44?(r=!0,N=t.input.charCodeAt(++t.position)):r=!1}Rr(t,"unexpected end of the stream within a flow collection")}function UJe(t,e){var r,s,a=$_,n=!1,c=!1,f=e,p=0,h=!1,E,C;if(C=t.input.charCodeAt(t.position),C===124)s=!1;else if(C===62)s=!0;else return!1;for(t.kind="scalar",t.result="";C!==0;)if(C=t.input.charCodeAt(++t.position),C===43||C===45)$_===a?a=C===43?zte:bJe:Rr(t,"repeat of a chomping mode identifier");else if((E=RJe(C))>=0)E===0?Rr(t,"bad explicit indentation width of a block scalar; it cannot be less than one"):c?Rr(t,"repeat of an indentation width identifier"):(f=e+E-1,c=!0);else break;if(kd(C)){do C=t.input.charCodeAt(++t.position);while(kd(C));if(C===35)do C=t.input.charCodeAt(++t.position);while(!qf(C)&&C!==0)}for(;C!==0;){for(eU(t),t.lineIndent=0,C=t.input.charCodeAt(t.position);(!c||t.lineIndentf&&(f=t.lineIndent),qf(C)){p++;continue}if(t.lineIndente)&&p!==0)Rr(t,"bad indentation of a sequence entry");else if(t.lineIndente)&&(dE(t,e,bx,!0,a)&&(I?S=t.result:P=t.result),I||(gE(t,h,E,C,S,P,n,c),C=S=P=null),ls(t,!0,-1),N=t.input.charCodeAt(t.position)),t.lineIndent>e&&N!==0)Rr(t,"bad indentation of a mapping entry");else if(t.lineIndente?p=1:t.lineIndent===e?p=0:t.lineIndente?p=1:t.lineIndent===e?p=0:t.lineIndent tag; it should be "scalar", not "'+t.kind+'"'),C=0,S=t.implicitTypes.length;C tag; it should be "'+P.kind+'", not "'+t.kind+'"'),P.resolve(t.result)?(t.result=P.construct(t.result),t.anchor!==null&&(t.anchorMap[t.anchor]=t.result)):Rr(t,"cannot resolve a node with !<"+t.tag+"> explicit tag")):Rr(t,"unknown tag !<"+t.tag+">");return t.listener!==null&&t.listener("close",t),t.tag!==null||t.anchor!==null||E}function WJe(t){var e=t.position,r,s,a,n=!1,c;for(t.version=null,t.checkLineBreaks=t.legacy,t.tagMap={},t.anchorMap={};(c=t.input.charCodeAt(t.position))!==0&&(ls(t,!0,-1),c=t.input.charCodeAt(t.position),!(t.lineIndent>0||c!==37));){for(n=!0,c=t.input.charCodeAt(++t.position),r=t.position;c!==0&&!nl(c);)c=t.input.charCodeAt(++t.position);for(s=t.input.slice(r,t.position),a=[],s.length<1&&Rr(t,"directive name must not be less than one character in length");c!==0;){for(;kd(c);)c=t.input.charCodeAt(++t.position);if(c===35){do c=t.input.charCodeAt(++t.position);while(c!==0&&!qf(c));break}if(qf(c))break;for(r=t.position;c!==0&&!nl(c);)c=t.input.charCodeAt(++t.position);a.push(t.input.slice(r,t.position))}c!==0&&eU(t),o0.call($te,s)?$te[s](t,s,a):Px(t,'unknown document directive "'+s+'"')}if(ls(t,!0,-1),t.lineIndent===0&&t.input.charCodeAt(t.position)===45&&t.input.charCodeAt(t.position+1)===45&&t.input.charCodeAt(t.position+2)===45?(t.position+=3,ls(t,!0,-1)):n&&Rr(t,"directives end mark is expected"),dE(t,t.lineIndent-1,bx,!1,!0),ls(t,!0,-1),t.checkLineBreaks&&xJe.test(t.input.slice(e,t.position))&&Px(t,"non-ASCII line breaks are interpreted as content"),t.documents.push(t.result),t.position===t.lineStart&&xx(t)){t.input.charCodeAt(t.position)===46&&(t.position+=3,ls(t,!0,-1));return}if(t.position"u"&&(r=e,e=null);var s=fre(t,r);if(typeof e!="function")return s;for(var a=0,n=s.length;a"u"&&(r=e,e=null),Are(t,e,wp.extend({schema:nre},r))}function VJe(t,e){return pre(t,wp.extend({schema:nre},e))}G2.exports.loadAll=Are;G2.exports.load=pre;G2.exports.safeLoadAll=YJe;G2.exports.safeLoad=VJe});var Lre=L((b5t,sU)=>{"use strict";var Y2=Dd(),V2=fE(),KJe=q2(),JJe=pE(),wre=Object.prototype.toString,Bre=Object.prototype.hasOwnProperty,zJe=9,W2=10,ZJe=13,XJe=32,$Je=33,eze=34,vre=35,tze=37,rze=38,nze=39,ize=42,Sre=44,sze=45,Dre=58,oze=61,aze=62,lze=63,cze=64,bre=91,Pre=93,uze=96,xre=123,fze=124,kre=125,jo={};jo[0]="\\0";jo[7]="\\a";jo[8]="\\b";jo[9]="\\t";jo[10]="\\n";jo[11]="\\v";jo[12]="\\f";jo[13]="\\r";jo[27]="\\e";jo[34]='\\"';jo[92]="\\\\";jo[133]="\\N";jo[160]="\\_";jo[8232]="\\L";jo[8233]="\\P";var Aze=["y","Y","yes","Yes","YES","on","On","ON","n","N","no","No","NO","off","Off","OFF"];function pze(t,e){var r,s,a,n,c,f,p;if(e===null)return{};for(r={},s=Object.keys(e),a=0,n=s.length;a0?t.charCodeAt(n-1):null,S=S&&mre(c,f)}else{for(n=0;ns&&t[C+1]!==" ",C=n);else if(!mE(c))return kx;f=n>0?t.charCodeAt(n-1):null,S=S&&mre(c,f)}h=h||E&&n-C-1>s&&t[C+1]!==" "}return!p&&!h?S&&!a(t)?Tre:Rre:r>9&&Qre(t)?kx:h?Nre:Fre}function Eze(t,e,r,s){t.dump=function(){if(e.length===0)return"''";if(!t.noCompatMode&&Aze.indexOf(e)!==-1)return"'"+e+"'";var a=t.indent*Math.max(1,r),n=t.lineWidth===-1?-1:Math.max(Math.min(t.lineWidth,40),t.lineWidth-a),c=s||t.flowLevel>-1&&r>=t.flowLevel;function f(p){return gze(t,p)}switch(yze(e,c,t.indent,n,f)){case Tre:return e;case Rre:return"'"+e.replace(/'/g,"''")+"'";case Fre:return"|"+yre(e,t.indent)+Ere(dre(e,a));case Nre:return">"+yre(e,t.indent)+Ere(dre(Ize(e,n),a));case kx:return'"'+Cze(e,n)+'"';default:throw new V2("impossible error: invalid scalar style")}}()}function yre(t,e){var r=Qre(t)?String(e):"",s=t[t.length-1]===` `,a=s&&(t[t.length-2]===` `||t===` `),n=a?"+":s?"":"-";return r+n+` `}function Ere(t){return t[t.length-1]===` `?t.slice(0,-1):t}function Ize(t,e){for(var r=/(\n+)([^\n]*)/g,s=function(){var h=t.indexOf(` `);return h=h!==-1?h:t.length,r.lastIndex=h,Ire(t.slice(0,h),e)}(),a=t[0]===` `||t[0]===" ",n,c;c=r.exec(t);){var f=c[1],p=c[2];n=p[0]===" ",s+=f+(!a&&!n&&p!==""?` `:"")+Ire(p,e),a=n}return s}function Ire(t,e){if(t===""||t[0]===" ")return t;for(var r=/ [^ ]/g,s,a=0,n,c=0,f=0,p="";s=r.exec(t);)f=s.index,f-a>e&&(n=c>a?c:f,p+=` `+t.slice(a,n),a=n+1),c=f;return p+=` `,t.length-a>e&&c>a?p+=t.slice(a,c)+` `+t.slice(c+1):p+=t.slice(a),p.slice(1)}function Cze(t){for(var e="",r,s,a,n=0;n=55296&&r<=56319&&(s=t.charCodeAt(n+1),s>=56320&&s<=57343)){e+=gre((r-55296)*1024+s-56320+65536),n++;continue}a=jo[r],e+=!a&&mE(r)?t[n]:a||gre(r)}return e}function wze(t,e,r){var s="",a=t.tag,n,c;for(n=0,c=r.length;n1024&&(E+="? "),E+=t.dump+(t.condenseFlow?'"':"")+":"+(t.condenseFlow?"":" "),Qd(t,e,h,!1,!1)&&(E+=t.dump,s+=E));t.tag=a,t.dump="{"+s+"}"}function Sze(t,e,r,s){var a="",n=t.tag,c=Object.keys(r),f,p,h,E,C,S;if(t.sortKeys===!0)c.sort();else if(typeof t.sortKeys=="function")c.sort(t.sortKeys);else if(t.sortKeys)throw new V2("sortKeys must be a boolean or a function");for(f=0,p=c.length;f1024,C&&(t.dump&&W2===t.dump.charCodeAt(0)?S+="?":S+="? "),S+=t.dump,C&&(S+=rU(t,e)),Qd(t,e+1,E,!0,C)&&(t.dump&&W2===t.dump.charCodeAt(0)?S+=":":S+=": ",S+=t.dump,a+=S));t.tag=n,t.dump=a||"{}"}function Cre(t,e,r){var s,a,n,c,f,p;for(a=r?t.explicitTypes:t.implicitTypes,n=0,c=a.length;n tag resolver accepts not "'+p+'" style');t.dump=s}return!0}return!1}function Qd(t,e,r,s,a,n){t.tag=null,t.dump=r,Cre(t,r,!1)||Cre(t,r,!0);var c=wre.call(t.dump);s&&(s=t.flowLevel<0||t.flowLevel>e);var f=c==="[object Object]"||c==="[object Array]",p,h;if(f&&(p=t.duplicates.indexOf(r),h=p!==-1),(t.tag!==null&&t.tag!=="?"||h||t.indent!==2&&e>0)&&(a=!1),h&&t.usedDuplicates[p])t.dump="*ref_"+p;else{if(f&&h&&!t.usedDuplicates[p]&&(t.usedDuplicates[p]=!0),c==="[object Object]")s&&Object.keys(t.dump).length!==0?(Sze(t,e,t.dump,a),h&&(t.dump="&ref_"+p+t.dump)):(vze(t,e,t.dump),h&&(t.dump="&ref_"+p+" "+t.dump));else if(c==="[object Array]"){var E=t.noArrayIndent&&e>0?e-1:e;s&&t.dump.length!==0?(Bze(t,E,t.dump,a),h&&(t.dump="&ref_"+p+t.dump)):(wze(t,E,t.dump),h&&(t.dump="&ref_"+p+" "+t.dump))}else if(c==="[object String]")t.tag!=="?"&&Eze(t,t.dump,e,n);else{if(t.skipInvalid)return!1;throw new V2("unacceptable kind of an object to dump "+c)}t.tag!==null&&t.tag!=="?"&&(t.dump="!<"+t.tag+"> "+t.dump)}return!0}function Dze(t,e){var r=[],s=[],a,n;for(nU(t,r,s),a=0,n=s.length;a{"use strict";var Qx=hre(),Mre=Lre();function Tx(t){return function(){throw new Error("Function "+t+" is deprecated and cannot be used.")}}Wi.exports.Type=bs();Wi.exports.Schema=bd();Wi.exports.FAILSAFE_SCHEMA=vx();Wi.exports.JSON_SCHEMA=z_();Wi.exports.CORE_SCHEMA=Z_();Wi.exports.DEFAULT_SAFE_SCHEMA=pE();Wi.exports.DEFAULT_FULL_SCHEMA=q2();Wi.exports.load=Qx.load;Wi.exports.loadAll=Qx.loadAll;Wi.exports.safeLoad=Qx.safeLoad;Wi.exports.safeLoadAll=Qx.safeLoadAll;Wi.exports.dump=Mre.dump;Wi.exports.safeDump=Mre.safeDump;Wi.exports.YAMLException=fE();Wi.exports.MINIMAL_SCHEMA=vx();Wi.exports.SAFE_SCHEMA=pE();Wi.exports.DEFAULT_SCHEMA=q2();Wi.exports.scan=Tx("scan");Wi.exports.parse=Tx("parse");Wi.exports.compose=Tx("compose");Wi.exports.addConstructor=Tx("addConstructor")});var Hre=L((x5t,Ure)=>{"use strict";var Pze=_re();Ure.exports=Pze});var qre=L((k5t,jre)=>{"use strict";function xze(t,e){function r(){this.constructor=t}r.prototype=e.prototype,t.prototype=new r}function Td(t,e,r,s){this.message=t,this.expected=e,this.found=r,this.location=s,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,Td)}xze(Td,Error);Td.buildMessage=function(t,e){var r={literal:function(h){return'"'+a(h.text)+'"'},class:function(h){var E="",C;for(C=0;C0){for(C=1,S=1;C({[gt]:Oe})))},Ae=function(ee){return ee},ce=function(ee){return ee},me=La("correct indentation"),pe=" ",Be=dn(" ",!1),Ce=function(ee){return ee.length===lr*St},g=function(ee){return ee.length===(lr+1)*St},we=function(){return lr++,!0},ye=function(){return lr--,!0},fe=function(){return ca()},se=La("pseudostring"),X=/^[^\r\n\t ?:,\][{}#&*!|>'"%@`\-]/,De=Jn(["\r",` `," "," ","?",":",",","]","[","{","}","#","&","*","!","|",">","'",'"',"%","@","`","-"],!0,!1),Re=/^[^\r\n\t ,\][{}:#"']/,dt=Jn(["\r",` `," "," ",",","]","[","{","}",":","#",'"',"'"],!0,!1),j=function(){return ca().replace(/^ *| *$/g,"")},rt="--",Fe=dn("--",!1),Ne=/^[a-zA-Z\/0-9]/,Pe=Jn([["a","z"],["A","Z"],"/",["0","9"]],!1,!1),Ye=/^[^\r\n\t :,]/,ke=Jn(["\r",` `," "," ",":",","],!0,!1),it="null",_e=dn("null",!1),x=function(){return null},w="true",b=dn("true",!1),y=function(){return!0},F="false",z=dn("false",!1),Z=function(){return!1},$=La("string"),oe='"',xe=dn('"',!1),Te=function(){return""},lt=function(ee){return ee},It=function(ee){return ee.join("")},qt=/^[^"\\\0-\x1F\x7F]/,ir=Jn(['"',"\\",["\0",""],"\x7F"],!0,!1),Pt='\\"',gn=dn('\\"',!1),Pr=function(){return'"'},Ir="\\\\",Nr=dn("\\\\",!1),nn=function(){return"\\"},ai="\\/",wo=dn("\\/",!1),ns=function(){return"/"},to="\\b",Bo=dn("\\b",!1),ji=function(){return"\b"},ro="\\f",vo=dn("\\f",!1),RA=function(){return"\f"},pf="\\n",yh=dn("\\n",!1),Eh=function(){return` `},no="\\r",jn=dn("\\r",!1),Fs=function(){return"\r"},io="\\t",lu=dn("\\t",!1),cu=function(){return" "},uu="\\u",FA=dn("\\u",!1),NA=function(ee,Ee,Oe,gt){return String.fromCharCode(parseInt(`0x${ee}${Ee}${Oe}${gt}`))},aa=/^[0-9a-fA-F]/,la=Jn([["0","9"],["a","f"],["A","F"]],!1,!1),OA=La("blank space"),gr=/^[ \t]/,So=Jn([" "," "],!1,!1),Me=La("white space"),fu=/^[ \t\n\r]/,Cr=Jn([" "," ",` `,"\r"],!1,!1),hf=`\r `,LA=dn(`\r `,!1),MA=` `,Au=dn(` `,!1),pu="\r",ac=dn("\r",!1),ve=0,Nt=0,lc=[{line:1,column:1}],Li=0,so=[],Rt=0,xn;if("startRule"in e){if(!(e.startRule in s))throw new Error(`Can't start parsing from rule "`+e.startRule+'".');a=s[e.startRule]}function ca(){return t.substring(Nt,ve)}function qi(){return Ua(Nt,ve)}function Mi(ee,Ee){throw Ee=Ee!==void 0?Ee:Ua(Nt,ve),gf([La(ee)],t.substring(Nt,ve),Ee)}function Oa(ee,Ee){throw Ee=Ee!==void 0?Ee:Ua(Nt,ve),Ha(ee,Ee)}function dn(ee,Ee){return{type:"literal",text:ee,ignoreCase:Ee}}function Jn(ee,Ee,Oe){return{type:"class",parts:ee,inverted:Ee,ignoreCase:Oe}}function hu(){return{type:"any"}}function Ih(){return{type:"end"}}function La(ee){return{type:"other",description:ee}}function Ma(ee){var Ee=lc[ee],Oe;if(Ee)return Ee;for(Oe=ee-1;!lc[Oe];)Oe--;for(Ee=lc[Oe],Ee={line:Ee.line,column:Ee.column};OeLi&&(Li=ve,so=[]),so.push(ee))}function Ha(ee,Ee){return new Td(ee,null,null,Ee)}function gf(ee,Ee,Oe){return new Td(Td.buildMessage(ee,Ee),ee,Ee,Oe)}function cc(){var ee;return ee=_A(),ee}function wn(){var ee,Ee,Oe;for(ee=ve,Ee=[],Oe=ua();Oe!==r;)Ee.push(Oe),Oe=ua();return Ee!==r&&(Nt=ee,Ee=n(Ee)),ee=Ee,ee}function ua(){var ee,Ee,Oe,gt,yt;return ee=ve,Ee=vl(),Ee!==r?(t.charCodeAt(ve)===45?(Oe=c,ve++):(Oe=r,Rt===0&&Xe(f)),Oe!==r?(gt=Qn(),gt!==r?(yt=fa(),yt!==r?(Nt=ee,Ee=p(yt),ee=Ee):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r),ee}function _A(){var ee,Ee,Oe;for(ee=ve,Ee=[],Oe=UA();Oe!==r;)Ee.push(Oe),Oe=UA();return Ee!==r&&(Nt=ee,Ee=h(Ee)),ee=Ee,ee}function UA(){var ee,Ee,Oe,gt,yt,Dt,tr,fn,li;if(ee=ve,Ee=Qn(),Ee===r&&(Ee=null),Ee!==r){if(Oe=ve,t.charCodeAt(ve)===35?(gt=E,ve++):(gt=r,Rt===0&&Xe(C)),gt!==r){if(yt=[],Dt=ve,tr=ve,Rt++,fn=st(),Rt--,fn===r?tr=void 0:(ve=tr,tr=r),tr!==r?(t.length>ve?(fn=t.charAt(ve),ve++):(fn=r,Rt===0&&Xe(S)),fn!==r?(tr=[tr,fn],Dt=tr):(ve=Dt,Dt=r)):(ve=Dt,Dt=r),Dt!==r)for(;Dt!==r;)yt.push(Dt),Dt=ve,tr=ve,Rt++,fn=st(),Rt--,fn===r?tr=void 0:(ve=tr,tr=r),tr!==r?(t.length>ve?(fn=t.charAt(ve),ve++):(fn=r,Rt===0&&Xe(S)),fn!==r?(tr=[tr,fn],Dt=tr):(ve=Dt,Dt=r)):(ve=Dt,Dt=r);else yt=r;yt!==r?(gt=[gt,yt],Oe=gt):(ve=Oe,Oe=r)}else ve=Oe,Oe=r;if(Oe===r&&(Oe=null),Oe!==r){if(gt=[],yt=Je(),yt!==r)for(;yt!==r;)gt.push(yt),yt=Je();else gt=r;gt!==r?(Nt=ee,Ee=P(),ee=Ee):(ve=ee,ee=r)}else ve=ee,ee=r}else ve=ee,ee=r;if(ee===r&&(ee=ve,Ee=vl(),Ee!==r?(Oe=ja(),Oe!==r?(gt=Qn(),gt===r&&(gt=null),gt!==r?(t.charCodeAt(ve)===58?(yt=I,ve++):(yt=r,Rt===0&&Xe(R)),yt!==r?(Dt=Qn(),Dt===r&&(Dt=null),Dt!==r?(tr=fa(),tr!==r?(Nt=ee,Ee=N(Oe,tr),ee=Ee):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r),ee===r&&(ee=ve,Ee=vl(),Ee!==r?(Oe=is(),Oe!==r?(gt=Qn(),gt===r&&(gt=null),gt!==r?(t.charCodeAt(ve)===58?(yt=I,ve++):(yt=r,Rt===0&&Xe(R)),yt!==r?(Dt=Qn(),Dt===r&&(Dt=null),Dt!==r?(tr=fa(),tr!==r?(Nt=ee,Ee=N(Oe,tr),ee=Ee):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r),ee===r))){if(ee=ve,Ee=vl(),Ee!==r)if(Oe=is(),Oe!==r)if(gt=Qn(),gt!==r)if(yt=gu(),yt!==r){if(Dt=[],tr=Je(),tr!==r)for(;tr!==r;)Dt.push(tr),tr=Je();else Dt=r;Dt!==r?(Nt=ee,Ee=N(Oe,yt),ee=Ee):(ve=ee,ee=r)}else ve=ee,ee=r;else ve=ee,ee=r;else ve=ee,ee=r;else ve=ee,ee=r;if(ee===r)if(ee=ve,Ee=vl(),Ee!==r)if(Oe=is(),Oe!==r){if(gt=[],yt=ve,Dt=Qn(),Dt===r&&(Dt=null),Dt!==r?(t.charCodeAt(ve)===44?(tr=U,ve++):(tr=r,Rt===0&&Xe(W)),tr!==r?(fn=Qn(),fn===r&&(fn=null),fn!==r?(li=is(),li!==r?(Nt=yt,Dt=te(Oe,li),yt=Dt):(ve=yt,yt=r)):(ve=yt,yt=r)):(ve=yt,yt=r)):(ve=yt,yt=r),yt!==r)for(;yt!==r;)gt.push(yt),yt=ve,Dt=Qn(),Dt===r&&(Dt=null),Dt!==r?(t.charCodeAt(ve)===44?(tr=U,ve++):(tr=r,Rt===0&&Xe(W)),tr!==r?(fn=Qn(),fn===r&&(fn=null),fn!==r?(li=is(),li!==r?(Nt=yt,Dt=te(Oe,li),yt=Dt):(ve=yt,yt=r)):(ve=yt,yt=r)):(ve=yt,yt=r)):(ve=yt,yt=r);else gt=r;gt!==r?(yt=Qn(),yt===r&&(yt=null),yt!==r?(t.charCodeAt(ve)===58?(Dt=I,ve++):(Dt=r,Rt===0&&Xe(R)),Dt!==r?(tr=Qn(),tr===r&&(tr=null),tr!==r?(fn=fa(),fn!==r?(Nt=ee,Ee=ie(Oe,gt,fn),ee=Ee):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)}else ve=ee,ee=r;else ve=ee,ee=r}return ee}function fa(){var ee,Ee,Oe,gt,yt,Dt,tr;if(ee=ve,Ee=ve,Rt++,Oe=ve,gt=st(),gt!==r?(yt=Mt(),yt!==r?(t.charCodeAt(ve)===45?(Dt=c,ve++):(Dt=r,Rt===0&&Xe(f)),Dt!==r?(tr=Qn(),tr!==r?(gt=[gt,yt,Dt,tr],Oe=gt):(ve=Oe,Oe=r)):(ve=Oe,Oe=r)):(ve=Oe,Oe=r)):(ve=Oe,Oe=r),Rt--,Oe!==r?(ve=Ee,Ee=void 0):Ee=r,Ee!==r?(Oe=Je(),Oe!==r?(gt=kn(),gt!==r?(yt=wn(),yt!==r?(Dt=Aa(),Dt!==r?(Nt=ee,Ee=Ae(yt),ee=Ee):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r),ee===r&&(ee=ve,Ee=st(),Ee!==r?(Oe=kn(),Oe!==r?(gt=_A(),gt!==r?(yt=Aa(),yt!==r?(Nt=ee,Ee=Ae(gt),ee=Ee):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r),ee===r))if(ee=ve,Ee=uc(),Ee!==r){if(Oe=[],gt=Je(),gt!==r)for(;gt!==r;)Oe.push(gt),gt=Je();else Oe=r;Oe!==r?(Nt=ee,Ee=ce(Ee),ee=Ee):(ve=ee,ee=r)}else ve=ee,ee=r;return ee}function vl(){var ee,Ee,Oe;for(Rt++,ee=ve,Ee=[],t.charCodeAt(ve)===32?(Oe=pe,ve++):(Oe=r,Rt===0&&Xe(Be));Oe!==r;)Ee.push(Oe),t.charCodeAt(ve)===32?(Oe=pe,ve++):(Oe=r,Rt===0&&Xe(Be));return Ee!==r?(Nt=ve,Oe=Ce(Ee),Oe?Oe=void 0:Oe=r,Oe!==r?(Ee=[Ee,Oe],ee=Ee):(ve=ee,ee=r)):(ve=ee,ee=r),Rt--,ee===r&&(Ee=r,Rt===0&&Xe(me)),ee}function Mt(){var ee,Ee,Oe;for(ee=ve,Ee=[],t.charCodeAt(ve)===32?(Oe=pe,ve++):(Oe=r,Rt===0&&Xe(Be));Oe!==r;)Ee.push(Oe),t.charCodeAt(ve)===32?(Oe=pe,ve++):(Oe=r,Rt===0&&Xe(Be));return Ee!==r?(Nt=ve,Oe=g(Ee),Oe?Oe=void 0:Oe=r,Oe!==r?(Ee=[Ee,Oe],ee=Ee):(ve=ee,ee=r)):(ve=ee,ee=r),ee}function kn(){var ee;return Nt=ve,ee=we(),ee?ee=void 0:ee=r,ee}function Aa(){var ee;return Nt=ve,ee=ye(),ee?ee=void 0:ee=r,ee}function ja(){var ee;return ee=Sl(),ee===r&&(ee=fc()),ee}function is(){var ee,Ee,Oe;if(ee=Sl(),ee===r){if(ee=ve,Ee=[],Oe=qa(),Oe!==r)for(;Oe!==r;)Ee.push(Oe),Oe=qa();else Ee=r;Ee!==r&&(Nt=ee,Ee=fe()),ee=Ee}return ee}function uc(){var ee;return ee=_i(),ee===r&&(ee=ws(),ee===r&&(ee=Sl(),ee===r&&(ee=fc()))),ee}function gu(){var ee;return ee=_i(),ee===r&&(ee=Sl(),ee===r&&(ee=qa())),ee}function fc(){var ee,Ee,Oe,gt,yt,Dt;if(Rt++,ee=ve,X.test(t.charAt(ve))?(Ee=t.charAt(ve),ve++):(Ee=r,Rt===0&&Xe(De)),Ee!==r){for(Oe=[],gt=ve,yt=Qn(),yt===r&&(yt=null),yt!==r?(Re.test(t.charAt(ve))?(Dt=t.charAt(ve),ve++):(Dt=r,Rt===0&&Xe(dt)),Dt!==r?(yt=[yt,Dt],gt=yt):(ve=gt,gt=r)):(ve=gt,gt=r);gt!==r;)Oe.push(gt),gt=ve,yt=Qn(),yt===r&&(yt=null),yt!==r?(Re.test(t.charAt(ve))?(Dt=t.charAt(ve),ve++):(Dt=r,Rt===0&&Xe(dt)),Dt!==r?(yt=[yt,Dt],gt=yt):(ve=gt,gt=r)):(ve=gt,gt=r);Oe!==r?(Nt=ee,Ee=j(),ee=Ee):(ve=ee,ee=r)}else ve=ee,ee=r;return Rt--,ee===r&&(Ee=r,Rt===0&&Xe(se)),ee}function qa(){var ee,Ee,Oe,gt,yt;if(ee=ve,t.substr(ve,2)===rt?(Ee=rt,ve+=2):(Ee=r,Rt===0&&Xe(Fe)),Ee===r&&(Ee=null),Ee!==r)if(Ne.test(t.charAt(ve))?(Oe=t.charAt(ve),ve++):(Oe=r,Rt===0&&Xe(Pe)),Oe!==r){for(gt=[],Ye.test(t.charAt(ve))?(yt=t.charAt(ve),ve++):(yt=r,Rt===0&&Xe(ke));yt!==r;)gt.push(yt),Ye.test(t.charAt(ve))?(yt=t.charAt(ve),ve++):(yt=r,Rt===0&&Xe(ke));gt!==r?(Nt=ee,Ee=j(),ee=Ee):(ve=ee,ee=r)}else ve=ee,ee=r;else ve=ee,ee=r;return ee}function _i(){var ee,Ee;return ee=ve,t.substr(ve,4)===it?(Ee=it,ve+=4):(Ee=r,Rt===0&&Xe(_e)),Ee!==r&&(Nt=ee,Ee=x()),ee=Ee,ee}function ws(){var ee,Ee;return ee=ve,t.substr(ve,4)===w?(Ee=w,ve+=4):(Ee=r,Rt===0&&Xe(b)),Ee!==r&&(Nt=ee,Ee=y()),ee=Ee,ee===r&&(ee=ve,t.substr(ve,5)===F?(Ee=F,ve+=5):(Ee=r,Rt===0&&Xe(z)),Ee!==r&&(Nt=ee,Ee=Z()),ee=Ee),ee}function Sl(){var ee,Ee,Oe,gt;return Rt++,ee=ve,t.charCodeAt(ve)===34?(Ee=oe,ve++):(Ee=r,Rt===0&&Xe(xe)),Ee!==r?(t.charCodeAt(ve)===34?(Oe=oe,ve++):(Oe=r,Rt===0&&Xe(xe)),Oe!==r?(Nt=ee,Ee=Te(),ee=Ee):(ve=ee,ee=r)):(ve=ee,ee=r),ee===r&&(ee=ve,t.charCodeAt(ve)===34?(Ee=oe,ve++):(Ee=r,Rt===0&&Xe(xe)),Ee!==r?(Oe=df(),Oe!==r?(t.charCodeAt(ve)===34?(gt=oe,ve++):(gt=r,Rt===0&&Xe(xe)),gt!==r?(Nt=ee,Ee=lt(Oe),ee=Ee):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)),Rt--,ee===r&&(Ee=r,Rt===0&&Xe($)),ee}function df(){var ee,Ee,Oe;if(ee=ve,Ee=[],Oe=Ac(),Oe!==r)for(;Oe!==r;)Ee.push(Oe),Oe=Ac();else Ee=r;return Ee!==r&&(Nt=ee,Ee=It(Ee)),ee=Ee,ee}function Ac(){var ee,Ee,Oe,gt,yt,Dt;return qt.test(t.charAt(ve))?(ee=t.charAt(ve),ve++):(ee=r,Rt===0&&Xe(ir)),ee===r&&(ee=ve,t.substr(ve,2)===Pt?(Ee=Pt,ve+=2):(Ee=r,Rt===0&&Xe(gn)),Ee!==r&&(Nt=ee,Ee=Pr()),ee=Ee,ee===r&&(ee=ve,t.substr(ve,2)===Ir?(Ee=Ir,ve+=2):(Ee=r,Rt===0&&Xe(Nr)),Ee!==r&&(Nt=ee,Ee=nn()),ee=Ee,ee===r&&(ee=ve,t.substr(ve,2)===ai?(Ee=ai,ve+=2):(Ee=r,Rt===0&&Xe(wo)),Ee!==r&&(Nt=ee,Ee=ns()),ee=Ee,ee===r&&(ee=ve,t.substr(ve,2)===to?(Ee=to,ve+=2):(Ee=r,Rt===0&&Xe(Bo)),Ee!==r&&(Nt=ee,Ee=ji()),ee=Ee,ee===r&&(ee=ve,t.substr(ve,2)===ro?(Ee=ro,ve+=2):(Ee=r,Rt===0&&Xe(vo)),Ee!==r&&(Nt=ee,Ee=RA()),ee=Ee,ee===r&&(ee=ve,t.substr(ve,2)===pf?(Ee=pf,ve+=2):(Ee=r,Rt===0&&Xe(yh)),Ee!==r&&(Nt=ee,Ee=Eh()),ee=Ee,ee===r&&(ee=ve,t.substr(ve,2)===no?(Ee=no,ve+=2):(Ee=r,Rt===0&&Xe(jn)),Ee!==r&&(Nt=ee,Ee=Fs()),ee=Ee,ee===r&&(ee=ve,t.substr(ve,2)===io?(Ee=io,ve+=2):(Ee=r,Rt===0&&Xe(lu)),Ee!==r&&(Nt=ee,Ee=cu()),ee=Ee,ee===r&&(ee=ve,t.substr(ve,2)===uu?(Ee=uu,ve+=2):(Ee=r,Rt===0&&Xe(FA)),Ee!==r?(Oe=Bi(),Oe!==r?(gt=Bi(),gt!==r?(yt=Bi(),yt!==r?(Dt=Bi(),Dt!==r?(Nt=ee,Ee=NA(Oe,gt,yt,Dt),ee=Ee):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)):(ve=ee,ee=r)))))))))),ee}function Bi(){var ee;return aa.test(t.charAt(ve))?(ee=t.charAt(ve),ve++):(ee=r,Rt===0&&Xe(la)),ee}function Qn(){var ee,Ee;if(Rt++,ee=[],gr.test(t.charAt(ve))?(Ee=t.charAt(ve),ve++):(Ee=r,Rt===0&&Xe(So)),Ee!==r)for(;Ee!==r;)ee.push(Ee),gr.test(t.charAt(ve))?(Ee=t.charAt(ve),ve++):(Ee=r,Rt===0&&Xe(So));else ee=r;return Rt--,ee===r&&(Ee=r,Rt===0&&Xe(OA)),ee}function pc(){var ee,Ee;if(Rt++,ee=[],fu.test(t.charAt(ve))?(Ee=t.charAt(ve),ve++):(Ee=r,Rt===0&&Xe(Cr)),Ee!==r)for(;Ee!==r;)ee.push(Ee),fu.test(t.charAt(ve))?(Ee=t.charAt(ve),ve++):(Ee=r,Rt===0&&Xe(Cr));else ee=r;return Rt--,ee===r&&(Ee=r,Rt===0&&Xe(Me)),ee}function Je(){var ee,Ee,Oe,gt,yt,Dt;if(ee=ve,Ee=st(),Ee!==r){for(Oe=[],gt=ve,yt=Qn(),yt===r&&(yt=null),yt!==r?(Dt=st(),Dt!==r?(yt=[yt,Dt],gt=yt):(ve=gt,gt=r)):(ve=gt,gt=r);gt!==r;)Oe.push(gt),gt=ve,yt=Qn(),yt===r&&(yt=null),yt!==r?(Dt=st(),Dt!==r?(yt=[yt,Dt],gt=yt):(ve=gt,gt=r)):(ve=gt,gt=r);Oe!==r?(Ee=[Ee,Oe],ee=Ee):(ve=ee,ee=r)}else ve=ee,ee=r;return ee}function st(){var ee;return t.substr(ve,2)===hf?(ee=hf,ve+=2):(ee=r,Rt===0&&Xe(LA)),ee===r&&(t.charCodeAt(ve)===10?(ee=MA,ve++):(ee=r,Rt===0&&Xe(Au)),ee===r&&(t.charCodeAt(ve)===13?(ee=pu,ve++):(ee=r,Rt===0&&Xe(ac)))),ee}let St=2,lr=0;if(xn=a(),xn!==r&&ve===t.length)return xn;throw xn!==r&&ve"u"?!0:typeof t=="object"&&t!==null&&!Array.isArray(t)?Object.keys(t).every(e=>Vre(t[e])):!1}function oU(t,e,r){if(t===null)return`null `;if(typeof t=="number"||typeof t=="boolean")return`${t.toString()} `;if(typeof t=="string")return`${Wre(t)} `;if(Array.isArray(t)){if(t.length===0)return`[] `;let s=" ".repeat(e);return` ${t.map(n=>`${s}- ${oU(n,e+1,!1)}`).join("")}`}if(typeof t=="object"&&t){let[s,a]=t instanceof Rx?[t.data,!1]:[t,!0],n=" ".repeat(e),c=Object.keys(s);a&&c.sort((p,h)=>{let E=Gre.indexOf(p),C=Gre.indexOf(h);return E===-1&&C===-1?ph?1:0:E!==-1&&C===-1?-1:E===-1&&C!==-1?1:E-C});let f=c.filter(p=>!Vre(s[p])).map((p,h)=>{let E=s[p],C=Wre(p),S=oU(E,e+1,!0),P=h>0||r?n:"",I=C.length>1024?`? ${C} ${P}:`:`${C}:`,R=S.startsWith(` `)?S:` ${S}`;return`${P}${I}${R}`}).join(e===0?` `:"")||` `;return r?` ${f}`:`${f}`}throw new Error(`Unsupported value type (${t})`)}function il(t){try{let e=oU(t,0,!1);return e!==` `?e:""}catch(e){throw e.location&&(e.message=e.message.replace(/(\.)?$/,` (line ${e.location.start.line}, column ${e.location.start.column})$1`)),e}}function Tze(t){return t.endsWith(` `)||(t+=` `),(0,Yre.parse)(t)}function Fze(t){if(Rze.test(t))return Tze(t);let e=(0,Fx.safeLoad)(t,{schema:Fx.FAILSAFE_SCHEMA,json:!0});if(e==null)return{};if(typeof e!="object")throw new Error(`Expected an indexed object, got a ${typeof e} instead. Does your file follow Yaml's rules?`);if(Array.isArray(e))throw new Error("Expected an indexed object, got an array instead. Does your file follow Yaml's rules?");return e}function cs(t){return Fze(t)}var Fx,Yre,Qze,Gre,Rx,Rze,Kre=Ct(()=>{Fx=et(Hre()),Yre=et(qre()),Qze=/^(?![-?:,\][{}#&*!|>'"%@` \t\r\n]).([ \t]*(?![,\][{}:# \t\r\n]).)*$/,Gre=["__metadata","version","resolution","dependencies","peerDependencies","dependenciesMeta","peerDependenciesMeta","binaries"],Rx=class{constructor(e){this.data=e}};il.PreserveOrdering=Rx;Rze=/^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i});var K2={};Vt(K2,{parseResolution:()=>Cx,parseShell:()=>yx,parseSyml:()=>cs,stringifyArgument:()=>Y_,stringifyArgumentSegment:()=>V_,stringifyArithmeticExpression:()=>Ix,stringifyCommand:()=>W_,stringifyCommandChain:()=>uE,stringifyCommandChainThen:()=>G_,stringifyCommandLine:()=>Ex,stringifyCommandLineThen:()=>q_,stringifyEnvSegment:()=>mx,stringifyRedirectArgument:()=>H2,stringifyResolution:()=>wx,stringifyShell:()=>cE,stringifyShellLine:()=>cE,stringifySyml:()=>il,stringifyValueArgument:()=>Bd});var Bc=Ct(()=>{Vee();Zee();Kre()});var zre=L((N5t,aU)=>{"use strict";var Nze=t=>{let e=!1,r=!1,s=!1;for(let a=0;a{if(!(typeof t=="string"||Array.isArray(t)))throw new TypeError("Expected the input to be `string | string[]`");e=Object.assign({pascalCase:!1},e);let r=a=>e.pascalCase?a.charAt(0).toUpperCase()+a.slice(1):a;return Array.isArray(t)?t=t.map(a=>a.trim()).filter(a=>a.length).join("-"):t=t.trim(),t.length===0?"":t.length===1?e.pascalCase?t.toUpperCase():t.toLowerCase():(t!==t.toLowerCase()&&(t=Nze(t)),t=t.replace(/^[_.\- ]+/,"").toLowerCase().replace(/[_.\- ]+(\w|$)/g,(a,n)=>n.toUpperCase()).replace(/\d+(\w|$)/g,a=>a.toUpperCase()),r(t))};aU.exports=Jre;aU.exports.default=Jre});var Zre=L((O5t,Oze)=>{Oze.exports=[{name:"Agola CI",constant:"AGOLA",env:"AGOLA_GIT_REF",pr:"AGOLA_PULL_REQUEST_ID"},{name:"Appcircle",constant:"APPCIRCLE",env:"AC_APPCIRCLE"},{name:"AppVeyor",constant:"APPVEYOR",env:"APPVEYOR",pr:"APPVEYOR_PULL_REQUEST_NUMBER"},{name:"AWS CodeBuild",constant:"CODEBUILD",env:"CODEBUILD_BUILD_ARN"},{name:"Azure Pipelines",constant:"AZURE_PIPELINES",env:"TF_BUILD",pr:{BUILD_REASON:"PullRequest"}},{name:"Bamboo",constant:"BAMBOO",env:"bamboo_planKey"},{name:"Bitbucket Pipelines",constant:"BITBUCKET",env:"BITBUCKET_COMMIT",pr:"BITBUCKET_PR_ID"},{name:"Bitrise",constant:"BITRISE",env:"BITRISE_IO",pr:"BITRISE_PULL_REQUEST"},{name:"Buddy",constant:"BUDDY",env:"BUDDY_WORKSPACE_ID",pr:"BUDDY_EXECUTION_PULL_REQUEST_ID"},{name:"Buildkite",constant:"BUILDKITE",env:"BUILDKITE",pr:{env:"BUILDKITE_PULL_REQUEST",ne:"false"}},{name:"CircleCI",constant:"CIRCLE",env:"CIRCLECI",pr:"CIRCLE_PULL_REQUEST"},{name:"Cirrus CI",constant:"CIRRUS",env:"CIRRUS_CI",pr:"CIRRUS_PR"},{name:"Codefresh",constant:"CODEFRESH",env:"CF_BUILD_ID",pr:{any:["CF_PULL_REQUEST_NUMBER","CF_PULL_REQUEST_ID"]}},{name:"Codemagic",constant:"CODEMAGIC",env:"CM_BUILD_ID",pr:"CM_PULL_REQUEST"},{name:"Codeship",constant:"CODESHIP",env:{CI_NAME:"codeship"}},{name:"Drone",constant:"DRONE",env:"DRONE",pr:{DRONE_BUILD_EVENT:"pull_request"}},{name:"dsari",constant:"DSARI",env:"DSARI"},{name:"Earthly",constant:"EARTHLY",env:"EARTHLY_CI"},{name:"Expo Application Services",constant:"EAS",env:"EAS_BUILD"},{name:"Gerrit",constant:"GERRIT",env:"GERRIT_PROJECT"},{name:"Gitea Actions",constant:"GITEA_ACTIONS",env:"GITEA_ACTIONS"},{name:"GitHub Actions",constant:"GITHUB_ACTIONS",env:"GITHUB_ACTIONS",pr:{GITHUB_EVENT_NAME:"pull_request"}},{name:"GitLab CI",constant:"GITLAB",env:"GITLAB_CI",pr:"CI_MERGE_REQUEST_ID"},{name:"GoCD",constant:"GOCD",env:"GO_PIPELINE_LABEL"},{name:"Google Cloud Build",constant:"GOOGLE_CLOUD_BUILD",env:"BUILDER_OUTPUT"},{name:"Harness CI",constant:"HARNESS",env:"HARNESS_BUILD_ID"},{name:"Heroku",constant:"HEROKU",env:{env:"NODE",includes:"/app/.heroku/node/bin/node"}},{name:"Hudson",constant:"HUDSON",env:"HUDSON_URL"},{name:"Jenkins",constant:"JENKINS",env:["JENKINS_URL","BUILD_ID"],pr:{any:["ghprbPullId","CHANGE_ID"]}},{name:"LayerCI",constant:"LAYERCI",env:"LAYERCI",pr:"LAYERCI_PULL_REQUEST"},{name:"Magnum CI",constant:"MAGNUM",env:"MAGNUM"},{name:"Netlify CI",constant:"NETLIFY",env:"NETLIFY",pr:{env:"PULL_REQUEST",ne:"false"}},{name:"Nevercode",constant:"NEVERCODE",env:"NEVERCODE",pr:{env:"NEVERCODE_PULL_REQUEST",ne:"false"}},{name:"Prow",constant:"PROW",env:"PROW_JOB_ID"},{name:"ReleaseHub",constant:"RELEASEHUB",env:"RELEASE_BUILD_ID"},{name:"Render",constant:"RENDER",env:"RENDER",pr:{IS_PULL_REQUEST:"true"}},{name:"Sail CI",constant:"SAIL",env:"SAILCI",pr:"SAIL_PULL_REQUEST_NUMBER"},{name:"Screwdriver",constant:"SCREWDRIVER",env:"SCREWDRIVER",pr:{env:"SD_PULL_REQUEST",ne:"false"}},{name:"Semaphore",constant:"SEMAPHORE",env:"SEMAPHORE",pr:"PULL_REQUEST_NUMBER"},{name:"Sourcehut",constant:"SOURCEHUT",env:{CI_NAME:"sourcehut"}},{name:"Strider CD",constant:"STRIDER",env:"STRIDER"},{name:"TaskCluster",constant:"TASKCLUSTER",env:["TASK_ID","RUN_ID"]},{name:"TeamCity",constant:"TEAMCITY",env:"TEAMCITY_VERSION"},{name:"Travis CI",constant:"TRAVIS",env:"TRAVIS",pr:{env:"TRAVIS_PULL_REQUEST",ne:"false"}},{name:"Vela",constant:"VELA",env:"VELA",pr:{VELA_PULL_REQUEST:"1"}},{name:"Vercel",constant:"VERCEL",env:{any:["NOW_BUILDER","VERCEL"]},pr:"VERCEL_GIT_PULL_REQUEST_ID"},{name:"Visual Studio App Center",constant:"APPCENTER",env:"APPCENTER_BUILD_ID"},{name:"Woodpecker",constant:"WOODPECKER",env:{CI:"woodpecker"},pr:{CI_BUILD_EVENT:"pull_request"}},{name:"Xcode Cloud",constant:"XCODE_CLOUD",env:"CI_XCODE_PROJECT",pr:"CI_PULL_REQUEST_NUMBER"},{name:"Xcode Server",constant:"XCODE_SERVER",env:"XCS"}]});var Rd=L(_l=>{"use strict";var $re=Zre(),Ps=process.env;Object.defineProperty(_l,"_vendors",{value:$re.map(function(t){return t.constant})});_l.name=null;_l.isPR=null;$re.forEach(function(t){let r=(Array.isArray(t.env)?t.env:[t.env]).every(function(s){return Xre(s)});if(_l[t.constant]=r,!!r)switch(_l.name=t.name,typeof t.pr){case"string":_l.isPR=!!Ps[t.pr];break;case"object":"env"in t.pr?_l.isPR=t.pr.env in Ps&&Ps[t.pr.env]!==t.pr.ne:"any"in t.pr?_l.isPR=t.pr.any.some(function(s){return!!Ps[s]}):_l.isPR=Xre(t.pr);break;default:_l.isPR=null}});_l.isCI=!!(Ps.CI!=="false"&&(Ps.BUILD_ID||Ps.BUILD_NUMBER||Ps.CI||Ps.CI_APP_ID||Ps.CI_BUILD_ID||Ps.CI_BUILD_NUMBER||Ps.CI_NAME||Ps.CONTINUOUS_INTEGRATION||Ps.RUN_ID||_l.name));function Xre(t){return typeof t=="string"?!!Ps[t]:"env"in t?Ps[t.env]&&Ps[t.env].includes(t.includes):"any"in t?t.any.some(function(e){return!!Ps[e]}):Object.keys(t).every(function(e){return Ps[e]===t[e]})}});var ei,En,Fd,lU,Nx,ene,cU,uU,Ox=Ct(()=>{(function(t){t.StartOfInput="\0",t.EndOfInput="",t.EndOfPartialInput=""})(ei||(ei={}));(function(t){t[t.InitialNode=0]="InitialNode",t[t.SuccessNode=1]="SuccessNode",t[t.ErrorNode=2]="ErrorNode",t[t.CustomNode=3]="CustomNode"})(En||(En={}));Fd=-1,lU=/^(-h|--help)(?:=([0-9]+))?$/,Nx=/^(--[a-z]+(?:-[a-z]+)*|-[a-zA-Z]+)$/,ene=/^-[a-zA-Z]{2,}$/,cU=/^([^=]+)=([\s\S]*)$/,uU=process.env.DEBUG_CLI==="1"});var nt,yE,Lx,fU,Mx=Ct(()=>{Ox();nt=class extends Error{constructor(e){super(e),this.clipanion={type:"usage"},this.name="UsageError"}},yE=class extends Error{constructor(e,r){if(super(),this.input=e,this.candidates=r,this.clipanion={type:"none"},this.name="UnknownSyntaxError",this.candidates.length===0)this.message="Command not found, but we're not sure what's the alternative.";else if(this.candidates.every(s=>s.reason!==null&&s.reason===r[0].reason)){let[{reason:s}]=this.candidates;this.message=`${s} ${this.candidates.map(({usage:a})=>`$ ${a}`).join(` `)}`}else if(this.candidates.length===1){let[{usage:s}]=this.candidates;this.message=`Command not found; did you mean: $ ${s} ${fU(e)}`}else this.message=`Command not found; did you mean one of: ${this.candidates.map(({usage:s},a)=>`${`${a}.`.padStart(4)} ${s}`).join(` `)} ${fU(e)}`}},Lx=class extends Error{constructor(e,r){super(),this.input=e,this.usages=r,this.clipanion={type:"none"},this.name="AmbiguousSyntaxError",this.message=`Cannot find which to pick amongst the following alternatives: ${this.usages.map((s,a)=>`${`${a}.`.padStart(4)} ${s}`).join(` `)} ${fU(e)}`}},fU=t=>`While running ${t.filter(e=>e!==ei.EndOfInput&&e!==ei.EndOfPartialInput).map(e=>{let r=JSON.stringify(e);return e.match(/\s/)||e.length===0||r!==`"${e}"`?r:e}).join(" ")}`});function Lze(t){let e=t.split(` `),r=e.filter(a=>a.match(/\S/)),s=r.length>0?r.reduce((a,n)=>Math.min(a,n.length-n.trimStart().length),Number.MAX_VALUE):0;return e.map(a=>a.slice(s).trimRight()).join(` `)}function qo(t,{format:e,paragraphs:r}){return t=t.replace(/\r\n?/g,` `),t=Lze(t),t=t.replace(/^\n+|\n+$/g,""),t=t.replace(/^(\s*)-([^\n]*?)\n+/gm,`$1-$2 `),t=t.replace(/\n(\n)?\n*/g,(s,a)=>a||" "),r&&(t=t.split(/\n/).map(s=>{let a=s.match(/^\s*[*-][\t ]+(.*)/);if(!a)return s.match(/(.{1,80})(?: |$)/g).join(` `);let n=s.length-s.trimStart().length;return a[1].match(new RegExp(`(.{1,${78-n}})(?: |$)`,"g")).map((c,f)=>" ".repeat(n)+(f===0?"- ":" ")+c).join(` `)}).join(` `)),t=t.replace(/(`+)((?:.|[\n])*?)\1/g,(s,a,n)=>e.code(a+n+a)),t=t.replace(/(\*\*)((?:.|[\n])*?)\1/g,(s,a,n)=>e.bold(a+n+a)),t?`${t} `:""}var AU,tne,rne,pU=Ct(()=>{AU=Array(80).fill("\u2501");for(let t=0;t<=24;++t)AU[AU.length-t]=`\x1B[38;5;${232+t}m\u2501`;tne={header:t=>`\x1B[1m\u2501\u2501\u2501 ${t}${t.length<75?` ${AU.slice(t.length+5).join("")}`:":"}\x1B[0m`,bold:t=>`\x1B[1m${t}\x1B[22m`,error:t=>`\x1B[31m\x1B[1m${t}\x1B[22m\x1B[39m`,code:t=>`\x1B[36m${t}\x1B[39m`},rne={header:t=>t,bold:t=>t,error:t=>t,code:t=>t}});function Ea(t){return{...t,[J2]:!0}}function Gf(t,e){return typeof t>"u"?[t,e]:typeof t=="object"&&t!==null&&!Array.isArray(t)?[void 0,t]:[t,e]}function _x(t,{mergeName:e=!1}={}){let r=t.match(/^([^:]+): (.*)$/m);if(!r)return"validation failed";let[,s,a]=r;return e&&(a=a[0].toLowerCase()+a.slice(1)),a=s!=="."||!e?`${s.replace(/^\.(\[|$)/,"$1")}: ${a}`:`: ${a}`,a}function z2(t,e){return e.length===1?new nt(`${t}${_x(e[0],{mergeName:!0})}`):new nt(`${t}: ${e.map(r=>` - ${_x(r)}`).join("")}`)}function Nd(t,e,r){if(typeof r>"u")return e;let s=[],a=[],n=f=>{let p=e;return e=f,n.bind(null,p)};if(!r(e,{errors:s,coercions:a,coercion:n}))throw z2(`Invalid value for ${t}`,s);for(let[,f]of a)f();return e}var J2,Bp=Ct(()=>{Mx();J2=Symbol("clipanion/isOption")});var Ia={};Vt(Ia,{KeyRelationship:()=>Wf,TypeAssertionError:()=>l0,applyCascade:()=>$2,as:()=>rZe,assert:()=>$ze,assertWithErrors:()=>eZe,cascade:()=>qx,fn:()=>nZe,hasAtLeastOneKey:()=>IU,hasExactLength:()=>ane,hasForbiddenKeys:()=>wZe,hasKeyRelationship:()=>tB,hasMaxLength:()=>sZe,hasMinLength:()=>iZe,hasMutuallyExclusiveKeys:()=>BZe,hasRequiredKeys:()=>CZe,hasUniqueItems:()=>oZe,isArray:()=>Ux,isAtLeast:()=>yU,isAtMost:()=>cZe,isBase64:()=>mZe,isBoolean:()=>Wze,isDate:()=>Vze,isDict:()=>zze,isEnum:()=>po,isHexColor:()=>dZe,isISO8601:()=>gZe,isInExclusiveRange:()=>fZe,isInInclusiveRange:()=>uZe,isInstanceOf:()=>Xze,isInteger:()=>EU,isJSON:()=>yZe,isLiteral:()=>ine,isLowerCase:()=>AZe,isMap:()=>Jze,isNegative:()=>aZe,isNullable:()=>IZe,isNumber:()=>dU,isObject:()=>sne,isOneOf:()=>mU,isOptional:()=>EZe,isPartial:()=>Zze,isPayload:()=>Yze,isPositive:()=>lZe,isRecord:()=>jx,isSet:()=>Kze,isString:()=>IE,isTuple:()=>Hx,isUUID4:()=>hZe,isUnknown:()=>gU,isUpperCase:()=>pZe,makeTrait:()=>one,makeValidator:()=>Wr,matchesRegExp:()=>X2,softAssert:()=>tZe});function ti(t){return t===null?"null":t===void 0?"undefined":t===""?"an empty string":typeof t=="symbol"?`<${t.toString()}>`:Array.isArray(t)?"an array":JSON.stringify(t)}function EE(t,e){if(t.length===0)return"nothing";if(t.length===1)return ti(t[0]);let r=t.slice(0,-1),s=t[t.length-1],a=t.length>2?`, ${e} `:` ${e} `;return`${r.map(n=>ti(n)).join(", ")}${a}${ti(s)}`}function a0(t,e){var r,s,a;return typeof e=="number"?`${(r=t?.p)!==null&&r!==void 0?r:"."}[${e}]`:Mze.test(e)?`${(s=t?.p)!==null&&s!==void 0?s:""}.${e}`:`${(a=t?.p)!==null&&a!==void 0?a:"."}[${JSON.stringify(e)}]`}function hU(t,e,r){return t===1?e:r}function mr({errors:t,p:e}={},r){return t?.push(`${e??"."}: ${r}`),!1}function qze(t,e){return r=>{t[e]=r}}function Yf(t,e){return r=>{let s=t[e];return t[e]=r,Yf(t,e).bind(null,s)}}function Z2(t,e,r){let s=()=>(t(r()),a),a=()=>(t(e),s);return s}function gU(){return Wr({test:(t,e)=>!0})}function ine(t){return Wr({test:(e,r)=>e!==t?mr(r,`Expected ${ti(t)} (got ${ti(e)})`):!0})}function IE(){return Wr({test:(t,e)=>typeof t!="string"?mr(e,`Expected a string (got ${ti(t)})`):!0})}function po(t){let e=Array.isArray(t)?t:Object.values(t),r=e.every(a=>typeof a=="string"||typeof a=="number"),s=new Set(e);return s.size===1?ine([...s][0]):Wr({test:(a,n)=>s.has(a)?!0:r?mr(n,`Expected one of ${EE(e,"or")} (got ${ti(a)})`):mr(n,`Expected a valid enumeration value (got ${ti(a)})`)})}function Wze(){return Wr({test:(t,e)=>{var r;if(typeof t!="boolean"){if(typeof e?.coercions<"u"){if(typeof e?.coercion>"u")return mr(e,"Unbound coercion result");let s=Gze.get(t);if(typeof s<"u")return e.coercions.push([(r=e.p)!==null&&r!==void 0?r:".",e.coercion.bind(null,s)]),!0}return mr(e,`Expected a boolean (got ${ti(t)})`)}return!0}})}function dU(){return Wr({test:(t,e)=>{var r;if(typeof t!="number"){if(typeof e?.coercions<"u"){if(typeof e?.coercion>"u")return mr(e,"Unbound coercion result");let s;if(typeof t=="string"){let a;try{a=JSON.parse(t)}catch{}if(typeof a=="number")if(JSON.stringify(a)===t)s=a;else return mr(e,`Received a number that can't be safely represented by the runtime (${t})`)}if(typeof s<"u")return e.coercions.push([(r=e.p)!==null&&r!==void 0?r:".",e.coercion.bind(null,s)]),!0}return mr(e,`Expected a number (got ${ti(t)})`)}return!0}})}function Yze(t){return Wr({test:(e,r)=>{var s;if(typeof r?.coercions>"u")return mr(r,"The isPayload predicate can only be used with coercion enabled");if(typeof r.coercion>"u")return mr(r,"Unbound coercion result");if(typeof e!="string")return mr(r,`Expected a string (got ${ti(e)})`);let a;try{a=JSON.parse(e)}catch{return mr(r,`Expected a JSON string (got ${ti(e)})`)}let n={value:a};return t(a,Object.assign(Object.assign({},r),{coercion:Yf(n,"value")}))?(r.coercions.push([(s=r.p)!==null&&s!==void 0?s:".",r.coercion.bind(null,n.value)]),!0):!1}})}function Vze(){return Wr({test:(t,e)=>{var r;if(!(t instanceof Date)){if(typeof e?.coercions<"u"){if(typeof e?.coercion>"u")return mr(e,"Unbound coercion result");let s;if(typeof t=="string"&&nne.test(t))s=new Date(t);else{let a;if(typeof t=="string"){let n;try{n=JSON.parse(t)}catch{}typeof n=="number"&&(a=n)}else typeof t=="number"&&(a=t);if(typeof a<"u")if(Number.isSafeInteger(a)||!Number.isSafeInteger(a*1e3))s=new Date(a*1e3);else return mr(e,`Received a timestamp that can't be safely represented by the runtime (${t})`)}if(typeof s<"u")return e.coercions.push([(r=e.p)!==null&&r!==void 0?r:".",e.coercion.bind(null,s)]),!0}return mr(e,`Expected a date (got ${ti(t)})`)}return!0}})}function Ux(t,{delimiter:e}={}){return Wr({test:(r,s)=>{var a;let n=r;if(typeof r=="string"&&typeof e<"u"&&typeof s?.coercions<"u"){if(typeof s?.coercion>"u")return mr(s,"Unbound coercion result");r=r.split(e)}if(!Array.isArray(r))return mr(s,`Expected an array (got ${ti(r)})`);let c=!0;for(let f=0,p=r.length;f{var n,c;if(Object.getPrototypeOf(s).toString()==="[object Set]")if(typeof a?.coercions<"u"){if(typeof a?.coercion>"u")return mr(a,"Unbound coercion result");let f=[...s],p=[...s];if(!r(p,Object.assign(Object.assign({},a),{coercion:void 0})))return!1;let h=()=>p.some((E,C)=>E!==f[C])?new Set(p):s;return a.coercions.push([(n=a.p)!==null&&n!==void 0?n:".",Z2(a.coercion,s,h)]),!0}else{let f=!0;for(let p of s)if(f=t(p,Object.assign({},a))&&f,!f&&a?.errors==null)break;return f}if(typeof a?.coercions<"u"){if(typeof a?.coercion>"u")return mr(a,"Unbound coercion result");let f={value:s};return r(s,Object.assign(Object.assign({},a),{coercion:Yf(f,"value")}))?(a.coercions.push([(c=a.p)!==null&&c!==void 0?c:".",Z2(a.coercion,s,()=>new Set(f.value))]),!0):!1}return mr(a,`Expected a set (got ${ti(s)})`)}})}function Jze(t,e){let r=Ux(Hx([t,e])),s=jx(e,{keys:t});return Wr({test:(a,n)=>{var c,f,p;if(Object.getPrototypeOf(a).toString()==="[object Map]")if(typeof n?.coercions<"u"){if(typeof n?.coercion>"u")return mr(n,"Unbound coercion result");let h=[...a],E=[...a];if(!r(E,Object.assign(Object.assign({},n),{coercion:void 0})))return!1;let C=()=>E.some((S,P)=>S[0]!==h[P][0]||S[1]!==h[P][1])?new Map(E):a;return n.coercions.push([(c=n.p)!==null&&c!==void 0?c:".",Z2(n.coercion,a,C)]),!0}else{let h=!0;for(let[E,C]of a)if(h=t(E,Object.assign({},n))&&h,!h&&n?.errors==null||(h=e(C,Object.assign(Object.assign({},n),{p:a0(n,E)}))&&h,!h&&n?.errors==null))break;return h}if(typeof n?.coercions<"u"){if(typeof n?.coercion>"u")return mr(n,"Unbound coercion result");let h={value:a};return Array.isArray(a)?r(a,Object.assign(Object.assign({},n),{coercion:void 0}))?(n.coercions.push([(f=n.p)!==null&&f!==void 0?f:".",Z2(n.coercion,a,()=>new Map(h.value))]),!0):!1:s(a,Object.assign(Object.assign({},n),{coercion:Yf(h,"value")}))?(n.coercions.push([(p=n.p)!==null&&p!==void 0?p:".",Z2(n.coercion,a,()=>new Map(Object.entries(h.value)))]),!0):!1}return mr(n,`Expected a map (got ${ti(a)})`)}})}function Hx(t,{delimiter:e}={}){let r=ane(t.length);return Wr({test:(s,a)=>{var n;if(typeof s=="string"&&typeof e<"u"&&typeof a?.coercions<"u"){if(typeof a?.coercion>"u")return mr(a,"Unbound coercion result");s=s.split(e),a.coercions.push([(n=a.p)!==null&&n!==void 0?n:".",a.coercion.bind(null,s)])}if(!Array.isArray(s))return mr(a,`Expected a tuple (got ${ti(s)})`);let c=r(s,Object.assign({},a));for(let f=0,p=s.length;f{var n;if(Array.isArray(s)&&typeof a?.coercions<"u")return typeof a?.coercion>"u"?mr(a,"Unbound coercion result"):r(s,Object.assign(Object.assign({},a),{coercion:void 0}))?(s=Object.fromEntries(s),a.coercions.push([(n=a.p)!==null&&n!==void 0?n:".",a.coercion.bind(null,s)]),!0):!1;if(typeof s!="object"||s===null)return mr(a,`Expected an object (got ${ti(s)})`);let c=Object.keys(s),f=!0;for(let p=0,h=c.length;p{if(typeof a!="object"||a===null)return mr(n,`Expected an object (got ${ti(a)})`);let c=new Set([...r,...Object.keys(a)]),f={},p=!0;for(let h of c){if(h==="constructor"||h==="__proto__")p=mr(Object.assign(Object.assign({},n),{p:a0(n,h)}),"Unsafe property name");else{let E=Object.prototype.hasOwnProperty.call(t,h)?t[h]:void 0,C=Object.prototype.hasOwnProperty.call(a,h)?a[h]:void 0;typeof E<"u"?p=E(C,Object.assign(Object.assign({},n),{p:a0(n,h),coercion:Yf(a,h)}))&&p:e===null?p=mr(Object.assign(Object.assign({},n),{p:a0(n,h)}),`Extraneous property (got ${ti(C)})`):Object.defineProperty(f,h,{enumerable:!0,get:()=>C,set:qze(a,h)})}if(!p&&n?.errors==null)break}return e!==null&&(p||n?.errors!=null)&&(p=e(f,n)&&p),p}});return Object.assign(s,{properties:t})}function Zze(t){return sne(t,{extra:jx(gU())})}function one(t){return()=>t}function Wr({test:t}){return one(t)()}function $ze(t,e){if(!e(t))throw new l0}function eZe(t,e){let r=[];if(!e(t,{errors:r}))throw new l0({errors:r})}function tZe(t,e){}function rZe(t,e,{coerce:r=!1,errors:s,throw:a}={}){let n=s?[]:void 0;if(!r){if(e(t,{errors:n}))return a?t:{value:t,errors:void 0};if(a)throw new l0({errors:n});return{value:void 0,errors:n??!0}}let c={value:t},f=Yf(c,"value"),p=[];if(!e(t,{errors:n,coercion:f,coercions:p})){if(a)throw new l0({errors:n});return{value:void 0,errors:n??!0}}for(let[,h]of p)h();return a?c.value:{value:c.value,errors:void 0}}function nZe(t,e){let r=Hx(t);return(...s)=>{if(!r(s))throw new l0;return e(...s)}}function iZe(t){return Wr({test:(e,r)=>e.length>=t?!0:mr(r,`Expected to have a length of at least ${t} elements (got ${e.length})`)})}function sZe(t){return Wr({test:(e,r)=>e.length<=t?!0:mr(r,`Expected to have a length of at most ${t} elements (got ${e.length})`)})}function ane(t){return Wr({test:(e,r)=>e.length!==t?mr(r,`Expected to have a length of exactly ${t} elements (got ${e.length})`):!0})}function oZe({map:t}={}){return Wr({test:(e,r)=>{let s=new Set,a=new Set;for(let n=0,c=e.length;nt<=0?!0:mr(e,`Expected to be negative (got ${t})`)})}function lZe(){return Wr({test:(t,e)=>t>=0?!0:mr(e,`Expected to be positive (got ${t})`)})}function yU(t){return Wr({test:(e,r)=>e>=t?!0:mr(r,`Expected to be at least ${t} (got ${e})`)})}function cZe(t){return Wr({test:(e,r)=>e<=t?!0:mr(r,`Expected to be at most ${t} (got ${e})`)})}function uZe(t,e){return Wr({test:(r,s)=>r>=t&&r<=e?!0:mr(s,`Expected to be in the [${t}; ${e}] range (got ${r})`)})}function fZe(t,e){return Wr({test:(r,s)=>r>=t&&re!==Math.round(e)?mr(r,`Expected to be an integer (got ${e})`):!t&&!Number.isSafeInteger(e)?mr(r,`Expected to be a safe integer (got ${e})`):!0})}function X2(t){return Wr({test:(e,r)=>t.test(e)?!0:mr(r,`Expected to match the pattern ${t.toString()} (got ${ti(e)})`)})}function AZe(){return Wr({test:(t,e)=>t!==t.toLowerCase()?mr(e,`Expected to be all-lowercase (got ${t})`):!0})}function pZe(){return Wr({test:(t,e)=>t!==t.toUpperCase()?mr(e,`Expected to be all-uppercase (got ${t})`):!0})}function hZe(){return Wr({test:(t,e)=>jze.test(t)?!0:mr(e,`Expected to be a valid UUID v4 (got ${ti(t)})`)})}function gZe(){return Wr({test:(t,e)=>nne.test(t)?!0:mr(e,`Expected to be a valid ISO 8601 date string (got ${ti(t)})`)})}function dZe({alpha:t=!1}){return Wr({test:(e,r)=>(t?_ze.test(e):Uze.test(e))?!0:mr(r,`Expected to be a valid hexadecimal color string (got ${ti(e)})`)})}function mZe(){return Wr({test:(t,e)=>Hze.test(t)?!0:mr(e,`Expected to be a valid base 64 string (got ${ti(t)})`)})}function yZe(t=gU()){return Wr({test:(e,r)=>{let s;try{s=JSON.parse(e)}catch{return mr(r,`Expected to be a valid JSON string (got ${ti(e)})`)}return t(s,r)}})}function qx(t,...e){let r=Array.isArray(e[0])?e[0]:e;return Wr({test:(s,a)=>{var n,c;let f={value:s},p=typeof a?.coercions<"u"?Yf(f,"value"):void 0,h=typeof a?.coercions<"u"?[]:void 0;if(!t(s,Object.assign(Object.assign({},a),{coercion:p,coercions:h})))return!1;let E=[];if(typeof h<"u")for(let[,C]of h)E.push(C());try{if(typeof a?.coercions<"u"){if(f.value!==s){if(typeof a?.coercion>"u")return mr(a,"Unbound coercion result");a.coercions.push([(n=a.p)!==null&&n!==void 0?n:".",a.coercion.bind(null,f.value)])}(c=a?.coercions)===null||c===void 0||c.push(...h)}return r.every(C=>C(f.value,a))}finally{for(let C of E)C()}}})}function $2(t,...e){let r=Array.isArray(e[0])?e[0]:e;return qx(t,r)}function EZe(t){return Wr({test:(e,r)=>typeof e>"u"?!0:t(e,r)})}function IZe(t){return Wr({test:(e,r)=>e===null?!0:t(e,r)})}function CZe(t,e){var r;let s=new Set(t),a=eB[(r=e?.missingIf)!==null&&r!==void 0?r:"missing"];return Wr({test:(n,c)=>{let f=new Set(Object.keys(n)),p=[];for(let h of s)a(f,h,n)||p.push(h);return p.length>0?mr(c,`Missing required ${hU(p.length,"property","properties")} ${EE(p,"and")}`):!0}})}function IU(t,e){var r;let s=new Set(t),a=eB[(r=e?.missingIf)!==null&&r!==void 0?r:"missing"];return Wr({test:(n,c)=>Object.keys(n).some(h=>a(s,h,n))?!0:mr(c,`Missing at least one property from ${EE(Array.from(s),"or")}`)})}function wZe(t,e){var r;let s=new Set(t),a=eB[(r=e?.missingIf)!==null&&r!==void 0?r:"missing"];return Wr({test:(n,c)=>{let f=new Set(Object.keys(n)),p=[];for(let h of s)a(f,h,n)&&p.push(h);return p.length>0?mr(c,`Forbidden ${hU(p.length,"property","properties")} ${EE(p,"and")}`):!0}})}function BZe(t,e){var r;let s=new Set(t),a=eB[(r=e?.missingIf)!==null&&r!==void 0?r:"missing"];return Wr({test:(n,c)=>{let f=new Set(Object.keys(n)),p=[];for(let h of s)a(f,h,n)&&p.push(h);return p.length>1?mr(c,`Mutually exclusive properties ${EE(p,"and")}`):!0}})}function tB(t,e,r,s){var a,n;let c=new Set((a=s?.ignore)!==null&&a!==void 0?a:[]),f=eB[(n=s?.missingIf)!==null&&n!==void 0?n:"missing"],p=new Set(r),h=vZe[e],E=e===Wf.Forbids?"or":"and";return Wr({test:(C,S)=>{let P=new Set(Object.keys(C));if(!f(P,t,C)||c.has(C[t]))return!0;let I=[];for(let R of p)(f(P,R,C)&&!c.has(C[R]))!==h.expect&&I.push(R);return I.length>=1?mr(S,`Property "${t}" ${h.message} ${hU(I.length,"property","properties")} ${EE(I,E)}`):!0}})}var Mze,_ze,Uze,Hze,jze,nne,Gze,Xze,mU,l0,eB,Wf,vZe,Ul=Ct(()=>{Mze=/^[a-zA-Z_][a-zA-Z0-9_]*$/;_ze=/^#[0-9a-f]{6}$/i,Uze=/^#[0-9a-f]{6}([0-9a-f]{2})?$/i,Hze=/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/,jze=/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}$/i,nne=/^(?:[1-9]\d{3}(-?)(?:(?:0[1-9]|1[0-2])\1(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])\1(?:29|30)|(?:0[13578]|1[02])(?:\1)31|00[1-9]|0[1-9]\d|[12]\d{2}|3(?:[0-5]\d|6[0-5]))|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)(?:(-?)02(?:\2)29|-?366))T(?:[01]\d|2[0-3])(:?)[0-5]\d(?:\3[0-5]\d)?(?:Z|[+-][01]\d(?:\3[0-5]\d)?)$/;Gze=new Map([["true",!0],["True",!0],["1",!0],[1,!0],["false",!1],["False",!1],["0",!1],[0,!1]]);Xze=t=>Wr({test:(e,r)=>e instanceof t?!0:mr(r,`Expected an instance of ${t.name} (got ${ti(e)})`)}),mU=(t,{exclusive:e=!1}={})=>Wr({test:(r,s)=>{var a,n,c;let f=[],p=typeof s?.errors<"u"?[]:void 0;for(let h=0,E=t.length;h1?mr(s,`Expected to match exactly a single predicate (matched ${f.join(", ")})`):(c=s?.errors)===null||c===void 0||c.push(...p),!1}});l0=class extends Error{constructor({errors:e}={}){let r="Type mismatch";if(e&&e.length>0){r+=` `;for(let s of e)r+=` - ${s}`}super(r)}};eB={missing:(t,e)=>t.has(e),undefined:(t,e,r)=>t.has(e)&&typeof r[e]<"u",nil:(t,e,r)=>t.has(e)&&r[e]!=null,falsy:(t,e,r)=>t.has(e)&&!!r[e]};(function(t){t.Forbids="Forbids",t.Requires="Requires"})(Wf||(Wf={}));vZe={[Wf.Forbids]:{expect:!1,message:"forbids using"},[Wf.Requires]:{expect:!0,message:"requires using"}}});var ot,c0=Ct(()=>{Bp();ot=class{constructor(){this.help=!1}static Usage(e){return e}async catch(e){throw e}async validateAndExecute(){let r=this.constructor.schema;if(Array.isArray(r)){let{isDict:a,isUnknown:n,applyCascade:c}=await Promise.resolve().then(()=>(Ul(),Ia)),f=c(a(n()),r),p=[],h=[];if(!f(this,{errors:p,coercions:h}))throw z2("Invalid option schema",p);for(let[,C]of h)C()}else if(r!=null)throw new Error("Invalid command schema");let s=await this.execute();return typeof s<"u"?s:0}};ot.isOption=J2;ot.Default=[]});function sl(t){uU&&console.log(t)}function cne(){let t={nodes:[]};for(let e=0;e{if(e.has(s))return;e.add(s);let a=t.nodes[s];for(let c of Object.values(a.statics))for(let{to:f}of c)r(f);for(let[,{to:c}]of a.dynamics)r(c);for(let{to:c}of a.shortcuts)r(c);let n=new Set(a.shortcuts.map(({to:c})=>c));for(;a.shortcuts.length>0;){let{to:c}=a.shortcuts.shift(),f=t.nodes[c];for(let[p,h]of Object.entries(f.statics)){let E=Object.prototype.hasOwnProperty.call(a.statics,p)?a.statics[p]:a.statics[p]=[];for(let C of h)E.some(({to:S})=>C.to===S)||E.push(C)}for(let[p,h]of f.dynamics)a.dynamics.some(([E,{to:C}])=>p===E&&h.to===C)||a.dynamics.push([p,h]);for(let p of f.shortcuts)n.has(p.to)||(a.shortcuts.push(p),n.add(p.to))}};r(En.InitialNode)}function bZe(t,{prefix:e=""}={}){if(uU){sl(`${e}Nodes are:`);for(let r=0;rE!==En.ErrorNode).map(({state:E})=>({usage:E.candidateUsage,reason:null})));if(h.every(({node:E})=>E===En.ErrorNode))throw new yE(e,h.map(({state:E})=>({usage:E.candidateUsage,reason:E.errorMessage})));s=kZe(h)}if(s.length>0){sl(" Results:");for(let n of s)sl(` - ${n.node} -> ${JSON.stringify(n.state)}`)}else sl(" No results");return s}function xZe(t,e,{endToken:r=ei.EndOfInput}={}){let s=PZe(t,[...e,r]);return QZe(e,s.map(({state:a})=>a))}function kZe(t){let e=0;for(let{state:r}of t)r.path.length>e&&(e=r.path.length);return t.filter(({state:r})=>r.path.length===e)}function QZe(t,e){let r=e.filter(S=>S.selectedIndex!==null),s=r.filter(S=>!S.partial);if(s.length>0&&(r=s),r.length===0)throw new Error;let a=r.filter(S=>S.selectedIndex===Fd||S.requiredOptions.every(P=>P.some(I=>S.options.find(R=>R.name===I))));if(a.length===0)throw new yE(t,r.map(S=>({usage:S.candidateUsage,reason:null})));let n=0;for(let S of a)S.path.length>n&&(n=S.path.length);let c=a.filter(S=>S.path.length===n),f=S=>S.positionals.filter(({extra:P})=>!P).length+S.options.length,p=c.map(S=>({state:S,positionalCount:f(S)})),h=0;for(let{positionalCount:S}of p)S>h&&(h=S);let E=p.filter(({positionalCount:S})=>S===h).map(({state:S})=>S),C=TZe(E);if(C.length>1)throw new Lx(t,C.map(S=>S.candidateUsage));return C[0]}function TZe(t){let e=[],r=[];for(let s of t)s.selectedIndex===Fd?r.push(s):e.push(s);return r.length>0&&e.push({...lne,path:une(...r.map(s=>s.path)),options:r.reduce((s,a)=>s.concat(a.options),[])}),e}function une(t,e,...r){return e===void 0?Array.from(t):une(t.filter((s,a)=>s===e[a]),...r)}function Hl(){return{dynamics:[],shortcuts:[],statics:{}}}function fne(t){return t===En.SuccessNode||t===En.ErrorNode}function CU(t,e=0){return{to:fne(t.to)?t.to:t.to>=En.CustomNode?t.to+e-En.CustomNode+1:t.to+e,reducer:t.reducer}}function RZe(t,e=0){let r=Hl();for(let[s,a]of t.dynamics)r.dynamics.push([s,CU(a,e)]);for(let s of t.shortcuts)r.shortcuts.push(CU(s,e));for(let[s,a]of Object.entries(t.statics))r.statics[s]=a.map(n=>CU(n,e));return r}function qs(t,e,r,s,a){t.nodes[e].dynamics.push([r,{to:s,reducer:a}])}function CE(t,e,r,s){t.nodes[e].shortcuts.push({to:r,reducer:s})}function Ca(t,e,r,s,a){(Object.prototype.hasOwnProperty.call(t.nodes[e].statics,r)?t.nodes[e].statics[r]:t.nodes[e].statics[r]=[]).push({to:s,reducer:a})}function Gx(t,e,r,s,a){if(Array.isArray(e)){let[n,...c]=e;return t[n](r,s,a,...c)}else return t[e](r,s,a)}var lne,FZe,wU,jl,BU,Wx,Yx=Ct(()=>{Ox();Mx();lne={candidateUsage:null,requiredOptions:[],errorMessage:null,ignoreOptions:!1,path:[],positionals:[],options:[],remainder:null,selectedIndex:Fd,partial:!1,tokens:[]};FZe={always:()=>!0,isOptionLike:(t,e)=>!t.ignoreOptions&&e!=="-"&&e.startsWith("-"),isNotOptionLike:(t,e)=>t.ignoreOptions||e==="-"||!e.startsWith("-"),isOption:(t,e,r,s)=>!t.ignoreOptions&&e===s,isBatchOption:(t,e,r,s)=>!t.ignoreOptions&&ene.test(e)&&[...e.slice(1)].every(a=>s.has(`-${a}`)),isBoundOption:(t,e,r,s,a)=>{let n=e.match(cU);return!t.ignoreOptions&&!!n&&Nx.test(n[1])&&s.has(n[1])&&a.filter(c=>c.nameSet.includes(n[1])).every(c=>c.allowBinding)},isNegatedOption:(t,e,r,s)=>!t.ignoreOptions&&e===`--no-${s.slice(2)}`,isHelp:(t,e)=>!t.ignoreOptions&&lU.test(e),isUnsupportedOption:(t,e,r,s)=>!t.ignoreOptions&&e.startsWith("-")&&Nx.test(e)&&!s.has(e),isInvalidOption:(t,e)=>!t.ignoreOptions&&e.startsWith("-")&&!Nx.test(e)},wU={setCandidateState:(t,e,r,s)=>({...t,...s}),setSelectedIndex:(t,e,r,s)=>({...t,selectedIndex:s}),setPartialIndex:(t,e,r,s)=>({...t,selectedIndex:s,partial:!0}),pushBatch:(t,e,r,s)=>{let a=t.options.slice(),n=t.tokens.slice();for(let c=1;c{let[,s,a]=e.match(cU),n=t.options.concat({name:s,value:a}),c=t.tokens.concat([{segmentIndex:r,type:"option",slice:[0,s.length],option:s},{segmentIndex:r,type:"assign",slice:[s.length,s.length+1]},{segmentIndex:r,type:"value",slice:[s.length+1,s.length+a.length+1]}]);return{...t,options:n,tokens:c}},pushPath:(t,e,r)=>{let s=t.path.concat(e),a=t.tokens.concat({segmentIndex:r,type:"path"});return{...t,path:s,tokens:a}},pushPositional:(t,e,r)=>{let s=t.positionals.concat({value:e,extra:!1}),a=t.tokens.concat({segmentIndex:r,type:"positional"});return{...t,positionals:s,tokens:a}},pushExtra:(t,e,r)=>{let s=t.positionals.concat({value:e,extra:!0}),a=t.tokens.concat({segmentIndex:r,type:"positional"});return{...t,positionals:s,tokens:a}},pushExtraNoLimits:(t,e,r)=>{let s=t.positionals.concat({value:e,extra:jl}),a=t.tokens.concat({segmentIndex:r,type:"positional"});return{...t,positionals:s,tokens:a}},pushTrue:(t,e,r,s)=>{let a=t.options.concat({name:s,value:!0}),n=t.tokens.concat({segmentIndex:r,type:"option",option:s});return{...t,options:a,tokens:n}},pushFalse:(t,e,r,s)=>{let a=t.options.concat({name:s,value:!1}),n=t.tokens.concat({segmentIndex:r,type:"option",option:s});return{...t,options:a,tokens:n}},pushUndefined:(t,e,r,s)=>{let a=t.options.concat({name:e,value:void 0}),n=t.tokens.concat({segmentIndex:r,type:"option",option:e});return{...t,options:a,tokens:n}},pushStringValue:(t,e,r)=>{var s;let a=t.options[t.options.length-1],n=t.options.slice(),c=t.tokens.concat({segmentIndex:r,type:"value"});return a.value=((s=a.value)!==null&&s!==void 0?s:[]).concat([e]),{...t,options:n,tokens:c}},setStringValue:(t,e,r)=>{let s=t.options[t.options.length-1],a=t.options.slice(),n=t.tokens.concat({segmentIndex:r,type:"value"});return s.value=e,{...t,options:a,tokens:n}},inhibateOptions:t=>({...t,ignoreOptions:!0}),useHelp:(t,e,r,s)=>{let[,,a]=e.match(lU);return typeof a<"u"?{...t,options:[{name:"-c",value:String(s)},{name:"-i",value:a}]}:{...t,options:[{name:"-c",value:String(s)}]}},setError:(t,e,r,s)=>e===ei.EndOfInput||e===ei.EndOfPartialInput?{...t,errorMessage:`${s}.`}:{...t,errorMessage:`${s} ("${e}").`},setOptionArityError:(t,e)=>{let r=t.options[t.options.length-1];return{...t,errorMessage:`Not enough arguments to option ${r.name}.`}}},jl=Symbol(),BU=class{constructor(e,r){this.allOptionNames=new Map,this.arity={leading:[],trailing:[],extra:[],proxy:!1},this.options=[],this.paths=[],this.cliIndex=e,this.cliOpts=r}addPath(e){this.paths.push(e)}setArity({leading:e=this.arity.leading,trailing:r=this.arity.trailing,extra:s=this.arity.extra,proxy:a=this.arity.proxy}){Object.assign(this.arity,{leading:e,trailing:r,extra:s,proxy:a})}addPositional({name:e="arg",required:r=!0}={}){if(!r&&this.arity.extra===jl)throw new Error("Optional parameters cannot be declared when using .rest() or .proxy()");if(!r&&this.arity.trailing.length>0)throw new Error("Optional parameters cannot be declared after the required trailing positional arguments");!r&&this.arity.extra!==jl?this.arity.extra.push(e):this.arity.extra!==jl&&this.arity.extra.length===0?this.arity.leading.push(e):this.arity.trailing.push(e)}addRest({name:e="arg",required:r=0}={}){if(this.arity.extra===jl)throw new Error("Infinite lists cannot be declared multiple times in the same command");if(this.arity.trailing.length>0)throw new Error("Infinite lists cannot be declared after the required trailing positional arguments");for(let s=0;s1)throw new Error("The arity cannot be higher than 1 when the option only supports the --arg=value syntax");if(!Number.isInteger(s))throw new Error(`The arity must be an integer, got ${s}`);if(s<0)throw new Error(`The arity must be positive, got ${s}`);let f=e.reduce((p,h)=>h.length>p.length?h:p,"");for(let p of e)this.allOptionNames.set(p,f);this.options.push({preferredName:f,nameSet:e,description:r,arity:s,hidden:a,required:n,allowBinding:c})}setContext(e){this.context=e}usage({detailed:e=!0,inlineOptions:r=!0}={}){let s=[this.cliOpts.binaryName],a=[];if(this.paths.length>0&&s.push(...this.paths[0]),e){for(let{preferredName:c,nameSet:f,arity:p,hidden:h,description:E,required:C}of this.options){if(h)continue;let S=[];for(let I=0;I`:`[${P}]`)}s.push(...this.arity.leading.map(c=>`<${c}>`)),this.arity.extra===jl?s.push("..."):s.push(...this.arity.extra.map(c=>`[${c}]`)),s.push(...this.arity.trailing.map(c=>`<${c}>`))}return{usage:s.join(" "),options:a}}compile(){if(typeof this.context>"u")throw new Error("Assertion failed: No context attached");let e=cne(),r=En.InitialNode,s=this.usage().usage,a=this.options.filter(f=>f.required).map(f=>f.nameSet);r=Mu(e,Hl()),Ca(e,En.InitialNode,ei.StartOfInput,r,["setCandidateState",{candidateUsage:s,requiredOptions:a}]);let n=this.arity.proxy?"always":"isNotOptionLike",c=this.paths.length>0?this.paths:[[]];for(let f of c){let p=r;if(f.length>0){let S=Mu(e,Hl());CE(e,p,S),this.registerOptions(e,S),p=S}for(let S=0;S0||!this.arity.proxy){let S=Mu(e,Hl());qs(e,p,"isHelp",S,["useHelp",this.cliIndex]),qs(e,S,"always",S,"pushExtra"),Ca(e,S,ei.EndOfInput,En.SuccessNode,["setSelectedIndex",Fd]),this.registerOptions(e,p)}this.arity.leading.length>0&&(Ca(e,p,ei.EndOfInput,En.ErrorNode,["setError","Not enough positional arguments"]),Ca(e,p,ei.EndOfPartialInput,En.SuccessNode,["setPartialIndex",this.cliIndex]));let h=p;for(let S=0;S0||S+1!==this.arity.leading.length)&&(Ca(e,P,ei.EndOfInput,En.ErrorNode,["setError","Not enough positional arguments"]),Ca(e,P,ei.EndOfPartialInput,En.SuccessNode,["setPartialIndex",this.cliIndex])),qs(e,h,"isNotOptionLike",P,"pushPositional"),h=P}let E=h;if(this.arity.extra===jl||this.arity.extra.length>0){let S=Mu(e,Hl());if(CE(e,h,S),this.arity.extra===jl){let P=Mu(e,Hl());this.arity.proxy||this.registerOptions(e,P),qs(e,h,n,P,"pushExtraNoLimits"),qs(e,P,n,P,"pushExtraNoLimits"),CE(e,P,S)}else for(let P=0;P0)&&this.registerOptions(e,I),qs(e,E,n,I,"pushExtra"),CE(e,I,S),E=I}E=S}this.arity.trailing.length>0&&(Ca(e,E,ei.EndOfInput,En.ErrorNode,["setError","Not enough positional arguments"]),Ca(e,E,ei.EndOfPartialInput,En.SuccessNode,["setPartialIndex",this.cliIndex]));let C=E;for(let S=0;S=0&&e{let c=n?ei.EndOfPartialInput:ei.EndOfInput;return xZe(s,a,{endToken:c})}}}}});function pne(){return Vx.default&&"getColorDepth"in Vx.default.WriteStream.prototype?Vx.default.WriteStream.prototype.getColorDepth():process.env.FORCE_COLOR==="0"?1:process.env.FORCE_COLOR==="1"||typeof process.stdout<"u"&&process.stdout.isTTY?8:1}function hne(t){let e=Ane;if(typeof e>"u"){if(t.stdout===process.stdout&&t.stderr===process.stderr)return null;let{AsyncLocalStorage:r}=Ie("async_hooks");e=Ane=new r;let s=process.stdout._write;process.stdout._write=function(n,c,f){let p=e.getStore();return typeof p>"u"?s.call(this,n,c,f):p.stdout.write(n,c,f)};let a=process.stderr._write;process.stderr._write=function(n,c,f){let p=e.getStore();return typeof p>"u"?a.call(this,n,c,f):p.stderr.write(n,c,f)}}return r=>e.run(t,r)}var Vx,Ane,gne=Ct(()=>{Vx=et(Ie("tty"),1)});var Kx,dne=Ct(()=>{c0();Kx=class t extends ot{constructor(e){super(),this.contexts=e,this.commands=[]}static from(e,r){let s=new t(r);s.path=e.path;for(let a of e.options)switch(a.name){case"-c":s.commands.push(Number(a.value));break;case"-i":s.index=Number(a.value);break}return s}async execute(){let e=this.commands;if(typeof this.index<"u"&&this.index>=0&&this.index1){this.context.stdout.write(`Multiple commands match your selection: `),this.context.stdout.write(` `);let r=0;for(let s of this.commands)this.context.stdout.write(this.cli.usage(this.contexts[s].commandClass,{prefix:`${r++}. `.padStart(5)}));this.context.stdout.write(` `),this.context.stdout.write(`Run again with -h= to see the longer details of any of those commands. `)}}}});async function Ene(...t){let{resolvedOptions:e,resolvedCommandClasses:r,resolvedArgv:s,resolvedContext:a}=Cne(t);return wa.from(r,e).runExit(s,a)}async function Ine(...t){let{resolvedOptions:e,resolvedCommandClasses:r,resolvedArgv:s,resolvedContext:a}=Cne(t);return wa.from(r,e).run(s,a)}function Cne(t){let e,r,s,a;switch(typeof process<"u"&&typeof process.argv<"u"&&(s=process.argv.slice(2)),t.length){case 1:r=t[0];break;case 2:t[0]&&t[0].prototype instanceof ot||Array.isArray(t[0])?(r=t[0],Array.isArray(t[1])?s=t[1]:a=t[1]):(e=t[0],r=t[1]);break;case 3:Array.isArray(t[2])?(e=t[0],r=t[1],s=t[2]):t[0]&&t[0].prototype instanceof ot||Array.isArray(t[0])?(r=t[0],s=t[1],a=t[2]):(e=t[0],r=t[1],a=t[2]);break;default:e=t[0],r=t[1],s=t[2],a=t[3];break}if(typeof s>"u")throw new Error("The argv parameter must be provided when running Clipanion outside of a Node context");return{resolvedOptions:e,resolvedCommandClasses:r,resolvedArgv:s,resolvedContext:a}}function yne(t){return t()}var mne,wa,wne=Ct(()=>{Ox();Yx();pU();gne();c0();dne();mne=Symbol("clipanion/errorCommand");wa=class t{constructor({binaryLabel:e,binaryName:r="...",binaryVersion:s,enableCapture:a=!1,enableColors:n}={}){this.registrations=new Map,this.builder=new Wx({binaryName:r}),this.binaryLabel=e,this.binaryName=r,this.binaryVersion=s,this.enableCapture=a,this.enableColors=n}static from(e,r={}){let s=new t(r),a=Array.isArray(e)?e:[e];for(let n of a)s.register(n);return s}register(e){var r;let s=new Map,a=new e;for(let p in a){let h=a[p];typeof h=="object"&&h!==null&&h[ot.isOption]&&s.set(p,h)}let n=this.builder.command(),c=n.cliIndex,f=(r=e.paths)!==null&&r!==void 0?r:a.paths;if(typeof f<"u")for(let p of f)n.addPath(p);this.registrations.set(e,{specs:s,builder:n,index:c});for(let[p,{definition:h}]of s.entries())h(n,p);n.setContext({commandClass:e})}process(e,r){let{input:s,context:a,partial:n}=typeof e=="object"&&Array.isArray(e)?{input:e,context:r}:e,{contexts:c,process:f}=this.builder.compile(),p=f(s,{partial:n}),h={...t.defaultContext,...a};switch(p.selectedIndex){case Fd:{let E=Kx.from(p,c);return E.context=h,E.tokens=p.tokens,E}default:{let{commandClass:E}=c[p.selectedIndex],C=this.registrations.get(E);if(typeof C>"u")throw new Error("Assertion failed: Expected the command class to have been registered.");let S=new E;S.context=h,S.tokens=p.tokens,S.path=p.path;try{for(let[P,{transformer:I}]of C.specs.entries())S[P]=I(C.builder,P,p,h);return S}catch(P){throw P[mne]=S,P}}break}}async run(e,r){var s,a;let n,c={...t.defaultContext,...r},f=(s=this.enableColors)!==null&&s!==void 0?s:c.colorDepth>1;if(!Array.isArray(e))n=e;else try{n=this.process(e,c)}catch(E){return c.stdout.write(this.error(E,{colored:f})),1}if(n.help)return c.stdout.write(this.usage(n,{colored:f,detailed:!0})),0;n.context=c,n.cli={binaryLabel:this.binaryLabel,binaryName:this.binaryName,binaryVersion:this.binaryVersion,enableCapture:this.enableCapture,enableColors:this.enableColors,definitions:()=>this.definitions(),definition:E=>this.definition(E),error:(E,C)=>this.error(E,C),format:E=>this.format(E),process:(E,C)=>this.process(E,{...c,...C}),run:(E,C)=>this.run(E,{...c,...C}),usage:(E,C)=>this.usage(E,C)};let p=this.enableCapture&&(a=hne(c))!==null&&a!==void 0?a:yne,h;try{h=await p(()=>n.validateAndExecute().catch(E=>n.catch(E).then(()=>0)))}catch(E){return c.stdout.write(this.error(E,{colored:f,command:n})),1}return h}async runExit(e,r){process.exitCode=await this.run(e,r)}definition(e,{colored:r=!1}={}){if(!e.usage)return null;let{usage:s}=this.getUsageByRegistration(e,{detailed:!1}),{usage:a,options:n}=this.getUsageByRegistration(e,{detailed:!0,inlineOptions:!1}),c=typeof e.usage.category<"u"?qo(e.usage.category,{format:this.format(r),paragraphs:!1}):void 0,f=typeof e.usage.description<"u"?qo(e.usage.description,{format:this.format(r),paragraphs:!1}):void 0,p=typeof e.usage.details<"u"?qo(e.usage.details,{format:this.format(r),paragraphs:!0}):void 0,h=typeof e.usage.examples<"u"?e.usage.examples.map(([E,C])=>[qo(E,{format:this.format(r),paragraphs:!1}),C.replace(/\$0/g,this.binaryName)]):void 0;return{path:s,usage:a,category:c,description:f,details:p,examples:h,options:n}}definitions({colored:e=!1}={}){let r=[];for(let s of this.registrations.keys()){let a=this.definition(s,{colored:e});a&&r.push(a)}return r}usage(e=null,{colored:r,detailed:s=!1,prefix:a="$ "}={}){var n;if(e===null){for(let p of this.registrations.keys()){let h=p.paths,E=typeof p.usage<"u";if(!h||h.length===0||h.length===1&&h[0].length===0||((n=h?.some(P=>P.length===0))!==null&&n!==void 0?n:!1))if(e){e=null;break}else e=p;else if(E){e=null;continue}}e&&(s=!0)}let c=e!==null&&e instanceof ot?e.constructor:e,f="";if(c)if(s){let{description:p="",details:h="",examples:E=[]}=c.usage||{};p!==""&&(f+=qo(p,{format:this.format(r),paragraphs:!1}).replace(/^./,P=>P.toUpperCase()),f+=` `),(h!==""||E.length>0)&&(f+=`${this.format(r).header("Usage")} `,f+=` `);let{usage:C,options:S}=this.getUsageByRegistration(c,{inlineOptions:!1});if(f+=`${this.format(r).bold(a)}${C} `,S.length>0){f+=` `,f+=`${this.format(r).header("Options")} `;let P=S.reduce((I,R)=>Math.max(I,R.definition.length),0);f+=` `;for(let{definition:I,description:R}of S)f+=` ${this.format(r).bold(I.padEnd(P))} ${qo(R,{format:this.format(r),paragraphs:!1})}`}if(h!==""&&(f+=` `,f+=`${this.format(r).header("Details")} `,f+=` `,f+=qo(h,{format:this.format(r),paragraphs:!0})),E.length>0){f+=` `,f+=`${this.format(r).header("Examples")} `;for(let[P,I]of E)f+=` `,f+=qo(P,{format:this.format(r),paragraphs:!1}),f+=`${I.replace(/^/m,` ${this.format(r).bold(a)}`).replace(/\$0/g,this.binaryName)} `}}else{let{usage:p}=this.getUsageByRegistration(c);f+=`${this.format(r).bold(a)}${p} `}else{let p=new Map;for(let[S,{index:P}]of this.registrations.entries()){if(typeof S.usage>"u")continue;let I=typeof S.usage.category<"u"?qo(S.usage.category,{format:this.format(r),paragraphs:!1}):null,R=p.get(I);typeof R>"u"&&p.set(I,R=[]);let{usage:N}=this.getUsageByIndex(P);R.push({commandClass:S,usage:N})}let h=Array.from(p.keys()).sort((S,P)=>S===null?-1:P===null?1:S.localeCompare(P,"en",{usage:"sort",caseFirst:"upper"})),E=typeof this.binaryLabel<"u",C=typeof this.binaryVersion<"u";E||C?(E&&C?f+=`${this.format(r).header(`${this.binaryLabel} - ${this.binaryVersion}`)} `:E?f+=`${this.format(r).header(`${this.binaryLabel}`)} `:f+=`${this.format(r).header(`${this.binaryVersion}`)} `,f+=` ${this.format(r).bold(a)}${this.binaryName} `):f+=`${this.format(r).bold(a)}${this.binaryName} `;for(let S of h){let P=p.get(S).slice().sort((R,N)=>R.usage.localeCompare(N.usage,"en",{usage:"sort",caseFirst:"upper"})),I=S!==null?S.trim():"General commands";f+=` `,f+=`${this.format(r).header(`${I}`)} `;for(let{commandClass:R,usage:N}of P){let U=R.usage.description||"undocumented";f+=` `,f+=` ${this.format(r).bold(N)} `,f+=` ${qo(U,{format:this.format(r),paragraphs:!1})}`}}f+=` `,f+=qo("You can also print more details about any of these commands by calling them with the `-h,--help` flag right after the command name.",{format:this.format(r),paragraphs:!0})}return f}error(e,r){var s,{colored:a,command:n=(s=e[mne])!==null&&s!==void 0?s:null}=r===void 0?{}:r;(!e||typeof e!="object"||!("stack"in e))&&(e=new Error(`Execution failed with a non-error rejection (rejected value: ${JSON.stringify(e)})`));let c="",f=e.name.replace(/([a-z])([A-Z])/g,"$1 $2");f==="Error"&&(f="Internal Error"),c+=`${this.format(a).error(f)}: ${e.message} `;let p=e.clipanion;return typeof p<"u"?p.type==="usage"&&(c+=` `,c+=this.usage(n)):e.stack&&(c+=`${e.stack.replace(/^.*\n/,"")} `),c}format(e){var r;return((r=e??this.enableColors)!==null&&r!==void 0?r:t.defaultContext.colorDepth>1)?tne:rne}getUsageByRegistration(e,r){let s=this.registrations.get(e);if(typeof s>"u")throw new Error("Assertion failed: Unregistered command");return this.getUsageByIndex(s.index,r)}getUsageByIndex(e,r){return this.builder.getBuilderByIndex(e).usage(r)}};wa.defaultContext={env:process.env,stdin:process.stdin,stdout:process.stdout,stderr:process.stderr,colorDepth:pne()}});var rB,Bne=Ct(()=>{c0();rB=class extends ot{async execute(){this.context.stdout.write(`${JSON.stringify(this.cli.definitions(),null,2)} `)}};rB.paths=[["--clipanion=definitions"]]});var nB,vne=Ct(()=>{c0();nB=class extends ot{async execute(){this.context.stdout.write(this.cli.usage())}};nB.paths=[["-h"],["--help"]]});function Jx(t={}){return Ea({definition(e,r){var s;e.addProxy({name:(s=t.name)!==null&&s!==void 0?s:r,required:t.required})},transformer(e,r,s){return s.positionals.map(({value:a})=>a)}})}var vU=Ct(()=>{Bp()});var iB,Sne=Ct(()=>{c0();vU();iB=class extends ot{constructor(){super(...arguments),this.args=Jx()}async execute(){this.context.stdout.write(`${JSON.stringify(this.cli.process(this.args).tokens,null,2)} `)}};iB.paths=[["--clipanion=tokens"]]});var sB,Dne=Ct(()=>{c0();sB=class extends ot{async execute(){var e;this.context.stdout.write(`${(e=this.cli.binaryVersion)!==null&&e!==void 0?e:""} `)}};sB.paths=[["-v"],["--version"]]});var SU={};Vt(SU,{DefinitionsCommand:()=>rB,HelpCommand:()=>nB,TokensCommand:()=>iB,VersionCommand:()=>sB});var bne=Ct(()=>{Bne();vne();Sne();Dne()});function Pne(t,e,r){let[s,a]=Gf(e,r??{}),{arity:n=1}=a,c=t.split(","),f=new Set(c);return Ea({definition(p){p.addOption({names:c,arity:n,hidden:a?.hidden,description:a?.description,required:a.required})},transformer(p,h,E){let C,S=typeof s<"u"?[...s]:void 0;for(let{name:P,value:I}of E.options)f.has(P)&&(C=P,S=S??[],S.push(I));return typeof S<"u"?Nd(C??h,S,a.validator):S}})}var xne=Ct(()=>{Bp()});function kne(t,e,r){let[s,a]=Gf(e,r??{}),n=t.split(","),c=new Set(n);return Ea({definition(f){f.addOption({names:n,allowBinding:!1,arity:0,hidden:a.hidden,description:a.description,required:a.required})},transformer(f,p,h){let E=s;for(let{name:C,value:S}of h.options)c.has(C)&&(E=S);return E}})}var Qne=Ct(()=>{Bp()});function Tne(t,e,r){let[s,a]=Gf(e,r??{}),n=t.split(","),c=new Set(n);return Ea({definition(f){f.addOption({names:n,allowBinding:!1,arity:0,hidden:a.hidden,description:a.description,required:a.required})},transformer(f,p,h){let E=s;for(let{name:C,value:S}of h.options)c.has(C)&&(E??(E=0),S?E+=1:E=0);return E}})}var Rne=Ct(()=>{Bp()});function Fne(t={}){return Ea({definition(e,r){var s;e.addRest({name:(s=t.name)!==null&&s!==void 0?s:r,required:t.required})},transformer(e,r,s){let a=c=>{let f=s.positionals[c];return f.extra===jl||f.extra===!1&&cc)}})}var Nne=Ct(()=>{Yx();Bp()});function NZe(t,e,r){let[s,a]=Gf(e,r??{}),{arity:n=1}=a,c=t.split(","),f=new Set(c);return Ea({definition(p){p.addOption({names:c,arity:a.tolerateBoolean?0:n,hidden:a.hidden,description:a.description,required:a.required})},transformer(p,h,E,C){let S,P=s;typeof a.env<"u"&&C.env[a.env]&&(S=a.env,P=C.env[a.env]);for(let{name:I,value:R}of E.options)f.has(I)&&(S=I,P=R);return typeof P=="string"?Nd(S??h,P,a.validator):P}})}function OZe(t={}){let{required:e=!0}=t;return Ea({definition(r,s){var a;r.addPositional({name:(a=t.name)!==null&&a!==void 0?a:s,required:t.required})},transformer(r,s,a){var n;for(let c=0;c{Yx();Bp()});var ge={};Vt(ge,{Array:()=>Pne,Boolean:()=>kne,Counter:()=>Tne,Proxy:()=>Jx,Rest:()=>Fne,String:()=>One,applyValidator:()=>Nd,cleanValidationError:()=>_x,formatError:()=>z2,isOptionSymbol:()=>J2,makeCommandOption:()=>Ea,rerouteArguments:()=>Gf});var Mne=Ct(()=>{Bp();vU();xne();Qne();Rne();Nne();Lne()});var oB={};Vt(oB,{Builtins:()=>SU,Cli:()=>wa,Command:()=>ot,Option:()=>ge,UsageError:()=>nt,formatMarkdownish:()=>qo,run:()=>Ine,runExit:()=>Ene});var Wt=Ct(()=>{Mx();pU();c0();wne();bne();Mne()});var _ne=L((q9t,LZe)=>{LZe.exports={name:"dotenv",version:"16.3.1",description:"Loads environment variables from .env file",main:"lib/main.js",types:"lib/main.d.ts",exports:{".":{types:"./lib/main.d.ts",require:"./lib/main.js",default:"./lib/main.js"},"./config":"./config.js","./config.js":"./config.js","./lib/env-options":"./lib/env-options.js","./lib/env-options.js":"./lib/env-options.js","./lib/cli-options":"./lib/cli-options.js","./lib/cli-options.js":"./lib/cli-options.js","./package.json":"./package.json"},scripts:{"dts-check":"tsc --project tests/types/tsconfig.json",lint:"standard","lint-readme":"standard-markdown",pretest:"npm run lint && npm run dts-check",test:"tap tests/*.js --100 -Rspec",prerelease:"npm test",release:"standard-version"},repository:{type:"git",url:"git://github.com/motdotla/dotenv.git"},funding:"https://github.com/motdotla/dotenv?sponsor=1",keywords:["dotenv","env",".env","environment","variables","config","settings"],readmeFilename:"README.md",license:"BSD-2-Clause",devDependencies:{"@definitelytyped/dtslint":"^0.0.133","@types/node":"^18.11.3",decache:"^4.6.1",sinon:"^14.0.1",standard:"^17.0.0","standard-markdown":"^7.1.0","standard-version":"^9.5.0",tap:"^16.3.0",tar:"^6.1.11",typescript:"^4.8.4"},engines:{node:">=12"},browser:{fs:!1}}});var qne=L((G9t,vp)=>{var Une=Ie("fs"),bU=Ie("path"),MZe=Ie("os"),_Ze=Ie("crypto"),UZe=_ne(),PU=UZe.version,HZe=/(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;function jZe(t){let e={},r=t.toString();r=r.replace(/\r\n?/mg,` `);let s;for(;(s=HZe.exec(r))!=null;){let a=s[1],n=s[2]||"";n=n.trim();let c=n[0];n=n.replace(/^(['"`])([\s\S]*)\1$/mg,"$2"),c==='"'&&(n=n.replace(/\\n/g,` `),n=n.replace(/\\r/g,"\r")),e[a]=n}return e}function qZe(t){let e=jne(t),r=Gs.configDotenv({path:e});if(!r.parsed)throw new Error(`MISSING_DATA: Cannot parse ${e} for an unknown reason`);let s=Hne(t).split(","),a=s.length,n;for(let c=0;c=a)throw f}return Gs.parse(n)}function GZe(t){console.log(`[dotenv@${PU}][INFO] ${t}`)}function WZe(t){console.log(`[dotenv@${PU}][WARN] ${t}`)}function DU(t){console.log(`[dotenv@${PU}][DEBUG] ${t}`)}function Hne(t){return t&&t.DOTENV_KEY&&t.DOTENV_KEY.length>0?t.DOTENV_KEY:process.env.DOTENV_KEY&&process.env.DOTENV_KEY.length>0?process.env.DOTENV_KEY:""}function YZe(t,e){let r;try{r=new URL(e)}catch(f){throw f.code==="ERR_INVALID_URL"?new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=development"):f}let s=r.password;if(!s)throw new Error("INVALID_DOTENV_KEY: Missing key part");let a=r.searchParams.get("environment");if(!a)throw new Error("INVALID_DOTENV_KEY: Missing environment part");let n=`DOTENV_VAULT_${a.toUpperCase()}`,c=t.parsed[n];if(!c)throw new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${n} in your .env.vault file.`);return{ciphertext:c,key:s}}function jne(t){let e=bU.resolve(process.cwd(),".env");return t&&t.path&&t.path.length>0&&(e=t.path),e.endsWith(".vault")?e:`${e}.vault`}function VZe(t){return t[0]==="~"?bU.join(MZe.homedir(),t.slice(1)):t}function KZe(t){GZe("Loading env from encrypted .env.vault");let e=Gs._parseVault(t),r=process.env;return t&&t.processEnv!=null&&(r=t.processEnv),Gs.populate(r,e,t),{parsed:e}}function JZe(t){let e=bU.resolve(process.cwd(),".env"),r="utf8",s=!!(t&&t.debug);t&&(t.path!=null&&(e=VZe(t.path)),t.encoding!=null&&(r=t.encoding));try{let a=Gs.parse(Une.readFileSync(e,{encoding:r})),n=process.env;return t&&t.processEnv!=null&&(n=t.processEnv),Gs.populate(n,a,t),{parsed:a}}catch(a){return s&&DU(`Failed to load ${e} ${a.message}`),{error:a}}}function zZe(t){let e=jne(t);return Hne(t).length===0?Gs.configDotenv(t):Une.existsSync(e)?Gs._configVault(t):(WZe(`You set DOTENV_KEY but you are missing a .env.vault file at ${e}. Did you forget to build it?`),Gs.configDotenv(t))}function ZZe(t,e){let r=Buffer.from(e.slice(-64),"hex"),s=Buffer.from(t,"base64"),a=s.slice(0,12),n=s.slice(-16);s=s.slice(12,-16);try{let c=_Ze.createDecipheriv("aes-256-gcm",r,a);return c.setAuthTag(n),`${c.update(s)}${c.final()}`}catch(c){let f=c instanceof RangeError,p=c.message==="Invalid key length",h=c.message==="Unsupported state or unable to authenticate data";if(f||p){let E="INVALID_DOTENV_KEY: It must be 64 characters long (or more)";throw new Error(E)}else if(h){let E="DECRYPTION_FAILED: Please check your DOTENV_KEY";throw new Error(E)}else throw console.error("Error: ",c.code),console.error("Error: ",c.message),c}}function XZe(t,e,r={}){let s=!!(r&&r.debug),a=!!(r&&r.override);if(typeof e!="object")throw new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate");for(let n of Object.keys(e))Object.prototype.hasOwnProperty.call(t,n)?(a===!0&&(t[n]=e[n]),s&&DU(a===!0?`"${n}" is already defined and WAS overwritten`:`"${n}" is already defined and was NOT overwritten`)):t[n]=e[n]}var Gs={configDotenv:JZe,_configVault:KZe,_parseVault:qZe,config:zZe,decrypt:ZZe,parse:jZe,populate:XZe};vp.exports.configDotenv=Gs.configDotenv;vp.exports._configVault=Gs._configVault;vp.exports._parseVault=Gs._parseVault;vp.exports.config=Gs.config;vp.exports.decrypt=Gs.decrypt;vp.exports.parse=Gs.parse;vp.exports.populate=Gs.populate;vp.exports=Gs});var Wne=L((W9t,Gne)=>{"use strict";Gne.exports=(t,...e)=>new Promise(r=>{r(t(...e))})});var Od=L((Y9t,xU)=>{"use strict";var $Ze=Wne(),Yne=t=>{if(t<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let e=[],r=0,s=()=>{r--,e.length>0&&e.shift()()},a=(f,p,...h)=>{r++;let E=$Ze(f,...h);p(E),E.then(s,s)},n=(f,p,...h)=>{rnew Promise(h=>n(f,h,...p));return Object.defineProperties(c,{activeCount:{get:()=>r},pendingCount:{get:()=>e.length}}),c};xU.exports=Yne;xU.exports.default=Yne});function Vf(t){return`YN${t.toString(10).padStart(4,"0")}`}function zx(t){let e=Number(t.slice(2));if(typeof Dr[e]>"u")throw new Error(`Unknown message name: "${t}"`);return e}var Dr,Zx=Ct(()=>{Dr=(Me=>(Me[Me.UNNAMED=0]="UNNAMED",Me[Me.EXCEPTION=1]="EXCEPTION",Me[Me.MISSING_PEER_DEPENDENCY=2]="MISSING_PEER_DEPENDENCY",Me[Me.CYCLIC_DEPENDENCIES=3]="CYCLIC_DEPENDENCIES",Me[Me.DISABLED_BUILD_SCRIPTS=4]="DISABLED_BUILD_SCRIPTS",Me[Me.BUILD_DISABLED=5]="BUILD_DISABLED",Me[Me.SOFT_LINK_BUILD=6]="SOFT_LINK_BUILD",Me[Me.MUST_BUILD=7]="MUST_BUILD",Me[Me.MUST_REBUILD=8]="MUST_REBUILD",Me[Me.BUILD_FAILED=9]="BUILD_FAILED",Me[Me.RESOLVER_NOT_FOUND=10]="RESOLVER_NOT_FOUND",Me[Me.FETCHER_NOT_FOUND=11]="FETCHER_NOT_FOUND",Me[Me.LINKER_NOT_FOUND=12]="LINKER_NOT_FOUND",Me[Me.FETCH_NOT_CACHED=13]="FETCH_NOT_CACHED",Me[Me.YARN_IMPORT_FAILED=14]="YARN_IMPORT_FAILED",Me[Me.REMOTE_INVALID=15]="REMOTE_INVALID",Me[Me.REMOTE_NOT_FOUND=16]="REMOTE_NOT_FOUND",Me[Me.RESOLUTION_PACK=17]="RESOLUTION_PACK",Me[Me.CACHE_CHECKSUM_MISMATCH=18]="CACHE_CHECKSUM_MISMATCH",Me[Me.UNUSED_CACHE_ENTRY=19]="UNUSED_CACHE_ENTRY",Me[Me.MISSING_LOCKFILE_ENTRY=20]="MISSING_LOCKFILE_ENTRY",Me[Me.WORKSPACE_NOT_FOUND=21]="WORKSPACE_NOT_FOUND",Me[Me.TOO_MANY_MATCHING_WORKSPACES=22]="TOO_MANY_MATCHING_WORKSPACES",Me[Me.CONSTRAINTS_MISSING_DEPENDENCY=23]="CONSTRAINTS_MISSING_DEPENDENCY",Me[Me.CONSTRAINTS_INCOMPATIBLE_DEPENDENCY=24]="CONSTRAINTS_INCOMPATIBLE_DEPENDENCY",Me[Me.CONSTRAINTS_EXTRANEOUS_DEPENDENCY=25]="CONSTRAINTS_EXTRANEOUS_DEPENDENCY",Me[Me.CONSTRAINTS_INVALID_DEPENDENCY=26]="CONSTRAINTS_INVALID_DEPENDENCY",Me[Me.CANT_SUGGEST_RESOLUTIONS=27]="CANT_SUGGEST_RESOLUTIONS",Me[Me.FROZEN_LOCKFILE_EXCEPTION=28]="FROZEN_LOCKFILE_EXCEPTION",Me[Me.CROSS_DRIVE_VIRTUAL_LOCAL=29]="CROSS_DRIVE_VIRTUAL_LOCAL",Me[Me.FETCH_FAILED=30]="FETCH_FAILED",Me[Me.DANGEROUS_NODE_MODULES=31]="DANGEROUS_NODE_MODULES",Me[Me.NODE_GYP_INJECTED=32]="NODE_GYP_INJECTED",Me[Me.AUTHENTICATION_NOT_FOUND=33]="AUTHENTICATION_NOT_FOUND",Me[Me.INVALID_CONFIGURATION_KEY=34]="INVALID_CONFIGURATION_KEY",Me[Me.NETWORK_ERROR=35]="NETWORK_ERROR",Me[Me.LIFECYCLE_SCRIPT=36]="LIFECYCLE_SCRIPT",Me[Me.CONSTRAINTS_MISSING_FIELD=37]="CONSTRAINTS_MISSING_FIELD",Me[Me.CONSTRAINTS_INCOMPATIBLE_FIELD=38]="CONSTRAINTS_INCOMPATIBLE_FIELD",Me[Me.CONSTRAINTS_EXTRANEOUS_FIELD=39]="CONSTRAINTS_EXTRANEOUS_FIELD",Me[Me.CONSTRAINTS_INVALID_FIELD=40]="CONSTRAINTS_INVALID_FIELD",Me[Me.AUTHENTICATION_INVALID=41]="AUTHENTICATION_INVALID",Me[Me.PROLOG_UNKNOWN_ERROR=42]="PROLOG_UNKNOWN_ERROR",Me[Me.PROLOG_SYNTAX_ERROR=43]="PROLOG_SYNTAX_ERROR",Me[Me.PROLOG_EXISTENCE_ERROR=44]="PROLOG_EXISTENCE_ERROR",Me[Me.STACK_OVERFLOW_RESOLUTION=45]="STACK_OVERFLOW_RESOLUTION",Me[Me.AUTOMERGE_FAILED_TO_PARSE=46]="AUTOMERGE_FAILED_TO_PARSE",Me[Me.AUTOMERGE_IMMUTABLE=47]="AUTOMERGE_IMMUTABLE",Me[Me.AUTOMERGE_SUCCESS=48]="AUTOMERGE_SUCCESS",Me[Me.AUTOMERGE_REQUIRED=49]="AUTOMERGE_REQUIRED",Me[Me.DEPRECATED_CLI_SETTINGS=50]="DEPRECATED_CLI_SETTINGS",Me[Me.PLUGIN_NAME_NOT_FOUND=51]="PLUGIN_NAME_NOT_FOUND",Me[Me.INVALID_PLUGIN_REFERENCE=52]="INVALID_PLUGIN_REFERENCE",Me[Me.CONSTRAINTS_AMBIGUITY=53]="CONSTRAINTS_AMBIGUITY",Me[Me.CACHE_OUTSIDE_PROJECT=54]="CACHE_OUTSIDE_PROJECT",Me[Me.IMMUTABLE_INSTALL=55]="IMMUTABLE_INSTALL",Me[Me.IMMUTABLE_CACHE=56]="IMMUTABLE_CACHE",Me[Me.INVALID_MANIFEST=57]="INVALID_MANIFEST",Me[Me.PACKAGE_PREPARATION_FAILED=58]="PACKAGE_PREPARATION_FAILED",Me[Me.INVALID_RANGE_PEER_DEPENDENCY=59]="INVALID_RANGE_PEER_DEPENDENCY",Me[Me.INCOMPATIBLE_PEER_DEPENDENCY=60]="INCOMPATIBLE_PEER_DEPENDENCY",Me[Me.DEPRECATED_PACKAGE=61]="DEPRECATED_PACKAGE",Me[Me.INCOMPATIBLE_OS=62]="INCOMPATIBLE_OS",Me[Me.INCOMPATIBLE_CPU=63]="INCOMPATIBLE_CPU",Me[Me.FROZEN_ARTIFACT_EXCEPTION=64]="FROZEN_ARTIFACT_EXCEPTION",Me[Me.TELEMETRY_NOTICE=65]="TELEMETRY_NOTICE",Me[Me.PATCH_HUNK_FAILED=66]="PATCH_HUNK_FAILED",Me[Me.INVALID_CONFIGURATION_VALUE=67]="INVALID_CONFIGURATION_VALUE",Me[Me.UNUSED_PACKAGE_EXTENSION=68]="UNUSED_PACKAGE_EXTENSION",Me[Me.REDUNDANT_PACKAGE_EXTENSION=69]="REDUNDANT_PACKAGE_EXTENSION",Me[Me.AUTO_NM_SUCCESS=70]="AUTO_NM_SUCCESS",Me[Me.NM_CANT_INSTALL_EXTERNAL_SOFT_LINK=71]="NM_CANT_INSTALL_EXTERNAL_SOFT_LINK",Me[Me.NM_PRESERVE_SYMLINKS_REQUIRED=72]="NM_PRESERVE_SYMLINKS_REQUIRED",Me[Me.UPDATE_LOCKFILE_ONLY_SKIP_LINK=73]="UPDATE_LOCKFILE_ONLY_SKIP_LINK",Me[Me.NM_HARDLINKS_MODE_DOWNGRADED=74]="NM_HARDLINKS_MODE_DOWNGRADED",Me[Me.PROLOG_INSTANTIATION_ERROR=75]="PROLOG_INSTANTIATION_ERROR",Me[Me.INCOMPATIBLE_ARCHITECTURE=76]="INCOMPATIBLE_ARCHITECTURE",Me[Me.GHOST_ARCHITECTURE=77]="GHOST_ARCHITECTURE",Me[Me.RESOLUTION_MISMATCH=78]="RESOLUTION_MISMATCH",Me[Me.PROLOG_LIMIT_EXCEEDED=79]="PROLOG_LIMIT_EXCEEDED",Me[Me.NETWORK_DISABLED=80]="NETWORK_DISABLED",Me[Me.NETWORK_UNSAFE_HTTP=81]="NETWORK_UNSAFE_HTTP",Me[Me.RESOLUTION_FAILED=82]="RESOLUTION_FAILED",Me[Me.AUTOMERGE_GIT_ERROR=83]="AUTOMERGE_GIT_ERROR",Me[Me.CONSTRAINTS_CHECK_FAILED=84]="CONSTRAINTS_CHECK_FAILED",Me[Me.UPDATED_RESOLUTION_RECORD=85]="UPDATED_RESOLUTION_RECORD",Me[Me.EXPLAIN_PEER_DEPENDENCIES_CTA=86]="EXPLAIN_PEER_DEPENDENCIES_CTA",Me[Me.MIGRATION_SUCCESS=87]="MIGRATION_SUCCESS",Me[Me.VERSION_NOTICE=88]="VERSION_NOTICE",Me[Me.TIPS_NOTICE=89]="TIPS_NOTICE",Me[Me.OFFLINE_MODE_ENABLED=90]="OFFLINE_MODE_ENABLED",Me[Me.INVALID_PROVENANCE_ENVIRONMENT=91]="INVALID_PROVENANCE_ENVIRONMENT",Me))(Dr||{})});var aB=L((K9t,Vne)=>{var eXe="2.0.0",tXe=Number.MAX_SAFE_INTEGER||9007199254740991,rXe=16,nXe=250,iXe=["major","premajor","minor","preminor","patch","prepatch","prerelease"];Vne.exports={MAX_LENGTH:256,MAX_SAFE_COMPONENT_LENGTH:rXe,MAX_SAFE_BUILD_LENGTH:nXe,MAX_SAFE_INTEGER:tXe,RELEASE_TYPES:iXe,SEMVER_SPEC_VERSION:eXe,FLAG_INCLUDE_PRERELEASE:1,FLAG_LOOSE:2}});var lB=L((J9t,Kne)=>{var sXe=typeof process=="object"&&process.env&&process.env.NODE_DEBUG&&/\bsemver\b/i.test(process.env.NODE_DEBUG)?(...t)=>console.error("SEMVER",...t):()=>{};Kne.exports=sXe});var wE=L((Sp,Jne)=>{var{MAX_SAFE_COMPONENT_LENGTH:kU,MAX_SAFE_BUILD_LENGTH:oXe,MAX_LENGTH:aXe}=aB(),lXe=lB();Sp=Jne.exports={};var cXe=Sp.re=[],uXe=Sp.safeRe=[],rr=Sp.src=[],nr=Sp.t={},fXe=0,QU="[a-zA-Z0-9-]",AXe=[["\\s",1],["\\d",aXe],[QU,oXe]],pXe=t=>{for(let[e,r]of AXe)t=t.split(`${e}*`).join(`${e}{0,${r}}`).split(`${e}+`).join(`${e}{1,${r}}`);return t},Kr=(t,e,r)=>{let s=pXe(e),a=fXe++;lXe(t,a,e),nr[t]=a,rr[a]=e,cXe[a]=new RegExp(e,r?"g":void 0),uXe[a]=new RegExp(s,r?"g":void 0)};Kr("NUMERICIDENTIFIER","0|[1-9]\\d*");Kr("NUMERICIDENTIFIERLOOSE","\\d+");Kr("NONNUMERICIDENTIFIER",`\\d*[a-zA-Z-]${QU}*`);Kr("MAINVERSION",`(${rr[nr.NUMERICIDENTIFIER]})\\.(${rr[nr.NUMERICIDENTIFIER]})\\.(${rr[nr.NUMERICIDENTIFIER]})`);Kr("MAINVERSIONLOOSE",`(${rr[nr.NUMERICIDENTIFIERLOOSE]})\\.(${rr[nr.NUMERICIDENTIFIERLOOSE]})\\.(${rr[nr.NUMERICIDENTIFIERLOOSE]})`);Kr("PRERELEASEIDENTIFIER",`(?:${rr[nr.NUMERICIDENTIFIER]}|${rr[nr.NONNUMERICIDENTIFIER]})`);Kr("PRERELEASEIDENTIFIERLOOSE",`(?:${rr[nr.NUMERICIDENTIFIERLOOSE]}|${rr[nr.NONNUMERICIDENTIFIER]})`);Kr("PRERELEASE",`(?:-(${rr[nr.PRERELEASEIDENTIFIER]}(?:\\.${rr[nr.PRERELEASEIDENTIFIER]})*))`);Kr("PRERELEASELOOSE",`(?:-?(${rr[nr.PRERELEASEIDENTIFIERLOOSE]}(?:\\.${rr[nr.PRERELEASEIDENTIFIERLOOSE]})*))`);Kr("BUILDIDENTIFIER",`${QU}+`);Kr("BUILD",`(?:\\+(${rr[nr.BUILDIDENTIFIER]}(?:\\.${rr[nr.BUILDIDENTIFIER]})*))`);Kr("FULLPLAIN",`v?${rr[nr.MAINVERSION]}${rr[nr.PRERELEASE]}?${rr[nr.BUILD]}?`);Kr("FULL",`^${rr[nr.FULLPLAIN]}$`);Kr("LOOSEPLAIN",`[v=\\s]*${rr[nr.MAINVERSIONLOOSE]}${rr[nr.PRERELEASELOOSE]}?${rr[nr.BUILD]}?`);Kr("LOOSE",`^${rr[nr.LOOSEPLAIN]}$`);Kr("GTLT","((?:<|>)?=?)");Kr("XRANGEIDENTIFIERLOOSE",`${rr[nr.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`);Kr("XRANGEIDENTIFIER",`${rr[nr.NUMERICIDENTIFIER]}|x|X|\\*`);Kr("XRANGEPLAIN",`[v=\\s]*(${rr[nr.XRANGEIDENTIFIER]})(?:\\.(${rr[nr.XRANGEIDENTIFIER]})(?:\\.(${rr[nr.XRANGEIDENTIFIER]})(?:${rr[nr.PRERELEASE]})?${rr[nr.BUILD]}?)?)?`);Kr("XRANGEPLAINLOOSE",`[v=\\s]*(${rr[nr.XRANGEIDENTIFIERLOOSE]})(?:\\.(${rr[nr.XRANGEIDENTIFIERLOOSE]})(?:\\.(${rr[nr.XRANGEIDENTIFIERLOOSE]})(?:${rr[nr.PRERELEASELOOSE]})?${rr[nr.BUILD]}?)?)?`);Kr("XRANGE",`^${rr[nr.GTLT]}\\s*${rr[nr.XRANGEPLAIN]}$`);Kr("XRANGELOOSE",`^${rr[nr.GTLT]}\\s*${rr[nr.XRANGEPLAINLOOSE]}$`);Kr("COERCEPLAIN",`(^|[^\\d])(\\d{1,${kU}})(?:\\.(\\d{1,${kU}}))?(?:\\.(\\d{1,${kU}}))?`);Kr("COERCE",`${rr[nr.COERCEPLAIN]}(?:$|[^\\d])`);Kr("COERCEFULL",rr[nr.COERCEPLAIN]+`(?:${rr[nr.PRERELEASE]})?(?:${rr[nr.BUILD]})?(?:$|[^\\d])`);Kr("COERCERTL",rr[nr.COERCE],!0);Kr("COERCERTLFULL",rr[nr.COERCEFULL],!0);Kr("LONETILDE","(?:~>?)");Kr("TILDETRIM",`(\\s*)${rr[nr.LONETILDE]}\\s+`,!0);Sp.tildeTrimReplace="$1~";Kr("TILDE",`^${rr[nr.LONETILDE]}${rr[nr.XRANGEPLAIN]}$`);Kr("TILDELOOSE",`^${rr[nr.LONETILDE]}${rr[nr.XRANGEPLAINLOOSE]}$`);Kr("LONECARET","(?:\\^)");Kr("CARETTRIM",`(\\s*)${rr[nr.LONECARET]}\\s+`,!0);Sp.caretTrimReplace="$1^";Kr("CARET",`^${rr[nr.LONECARET]}${rr[nr.XRANGEPLAIN]}$`);Kr("CARETLOOSE",`^${rr[nr.LONECARET]}${rr[nr.XRANGEPLAINLOOSE]}$`);Kr("COMPARATORLOOSE",`^${rr[nr.GTLT]}\\s*(${rr[nr.LOOSEPLAIN]})$|^$`);Kr("COMPARATOR",`^${rr[nr.GTLT]}\\s*(${rr[nr.FULLPLAIN]})$|^$`);Kr("COMPARATORTRIM",`(\\s*)${rr[nr.GTLT]}\\s*(${rr[nr.LOOSEPLAIN]}|${rr[nr.XRANGEPLAIN]})`,!0);Sp.comparatorTrimReplace="$1$2$3";Kr("HYPHENRANGE",`^\\s*(${rr[nr.XRANGEPLAIN]})\\s+-\\s+(${rr[nr.XRANGEPLAIN]})\\s*$`);Kr("HYPHENRANGELOOSE",`^\\s*(${rr[nr.XRANGEPLAINLOOSE]})\\s+-\\s+(${rr[nr.XRANGEPLAINLOOSE]})\\s*$`);Kr("STAR","(<|>)?=?\\s*\\*");Kr("GTE0","^\\s*>=\\s*0\\.0\\.0\\s*$");Kr("GTE0PRE","^\\s*>=\\s*0\\.0\\.0-0\\s*$")});var Xx=L((z9t,zne)=>{var hXe=Object.freeze({loose:!0}),gXe=Object.freeze({}),dXe=t=>t?typeof t!="object"?hXe:t:gXe;zne.exports=dXe});var TU=L((Z9t,$ne)=>{var Zne=/^[0-9]+$/,Xne=(t,e)=>{let r=Zne.test(t),s=Zne.test(e);return r&&s&&(t=+t,e=+e),t===e?0:r&&!s?-1:s&&!r?1:tXne(e,t);$ne.exports={compareIdentifiers:Xne,rcompareIdentifiers:mXe}});var Go=L((X9t,nie)=>{var $x=lB(),{MAX_LENGTH:eie,MAX_SAFE_INTEGER:ek}=aB(),{safeRe:tie,t:rie}=wE(),yXe=Xx(),{compareIdentifiers:BE}=TU(),RU=class t{constructor(e,r){if(r=yXe(r),e instanceof t){if(e.loose===!!r.loose&&e.includePrerelease===!!r.includePrerelease)return e;e=e.version}else if(typeof e!="string")throw new TypeError(`Invalid version. Must be a string. Got type "${typeof e}".`);if(e.length>eie)throw new TypeError(`version is longer than ${eie} characters`);$x("SemVer",e,r),this.options=r,this.loose=!!r.loose,this.includePrerelease=!!r.includePrerelease;let s=e.trim().match(r.loose?tie[rie.LOOSE]:tie[rie.FULL]);if(!s)throw new TypeError(`Invalid Version: ${e}`);if(this.raw=e,this.major=+s[1],this.minor=+s[2],this.patch=+s[3],this.major>ek||this.major<0)throw new TypeError("Invalid major version");if(this.minor>ek||this.minor<0)throw new TypeError("Invalid minor version");if(this.patch>ek||this.patch<0)throw new TypeError("Invalid patch version");s[4]?this.prerelease=s[4].split(".").map(a=>{if(/^[0-9]+$/.test(a)){let n=+a;if(n>=0&&n=0;)typeof this.prerelease[n]=="number"&&(this.prerelease[n]++,n=-2);if(n===-1){if(r===this.prerelease.join(".")&&s===!1)throw new Error("invalid increment argument: identifier already exists");this.prerelease.push(a)}}if(r){let n=[r,a];s===!1&&(n=[r]),BE(this.prerelease[0],r)===0?isNaN(this.prerelease[1])&&(this.prerelease=n):this.prerelease=n}break}default:throw new Error(`invalid increment argument: ${e}`)}return this.raw=this.format(),this.build.length&&(this.raw+=`+${this.build.join(".")}`),this}};nie.exports=RU});var Ld=L(($9t,sie)=>{var iie=Go(),EXe=(t,e,r=!1)=>{if(t instanceof iie)return t;try{return new iie(t,e)}catch(s){if(!r)return null;throw s}};sie.exports=EXe});var aie=L((eWt,oie)=>{var IXe=Ld(),CXe=(t,e)=>{let r=IXe(t,e);return r?r.version:null};oie.exports=CXe});var cie=L((tWt,lie)=>{var wXe=Ld(),BXe=(t,e)=>{let r=wXe(t.trim().replace(/^[=v]+/,""),e);return r?r.version:null};lie.exports=BXe});var Aie=L((rWt,fie)=>{var uie=Go(),vXe=(t,e,r,s,a)=>{typeof r=="string"&&(a=s,s=r,r=void 0);try{return new uie(t instanceof uie?t.version:t,r).inc(e,s,a).version}catch{return null}};fie.exports=vXe});var gie=L((nWt,hie)=>{var pie=Ld(),SXe=(t,e)=>{let r=pie(t,null,!0),s=pie(e,null,!0),a=r.compare(s);if(a===0)return null;let n=a>0,c=n?r:s,f=n?s:r,p=!!c.prerelease.length;if(!!f.prerelease.length&&!p)return!f.patch&&!f.minor?"major":c.patch?"patch":c.minor?"minor":"major";let E=p?"pre":"";return r.major!==s.major?E+"major":r.minor!==s.minor?E+"minor":r.patch!==s.patch?E+"patch":"prerelease"};hie.exports=SXe});var mie=L((iWt,die)=>{var DXe=Go(),bXe=(t,e)=>new DXe(t,e).major;die.exports=bXe});var Eie=L((sWt,yie)=>{var PXe=Go(),xXe=(t,e)=>new PXe(t,e).minor;yie.exports=xXe});var Cie=L((oWt,Iie)=>{var kXe=Go(),QXe=(t,e)=>new kXe(t,e).patch;Iie.exports=QXe});var Bie=L((aWt,wie)=>{var TXe=Ld(),RXe=(t,e)=>{let r=TXe(t,e);return r&&r.prerelease.length?r.prerelease:null};wie.exports=RXe});var vc=L((lWt,Sie)=>{var vie=Go(),FXe=(t,e,r)=>new vie(t,r).compare(new vie(e,r));Sie.exports=FXe});var bie=L((cWt,Die)=>{var NXe=vc(),OXe=(t,e,r)=>NXe(e,t,r);Die.exports=OXe});var xie=L((uWt,Pie)=>{var LXe=vc(),MXe=(t,e)=>LXe(t,e,!0);Pie.exports=MXe});var tk=L((fWt,Qie)=>{var kie=Go(),_Xe=(t,e,r)=>{let s=new kie(t,r),a=new kie(e,r);return s.compare(a)||s.compareBuild(a)};Qie.exports=_Xe});var Rie=L((AWt,Tie)=>{var UXe=tk(),HXe=(t,e)=>t.sort((r,s)=>UXe(r,s,e));Tie.exports=HXe});var Nie=L((pWt,Fie)=>{var jXe=tk(),qXe=(t,e)=>t.sort((r,s)=>jXe(s,r,e));Fie.exports=qXe});var cB=L((hWt,Oie)=>{var GXe=vc(),WXe=(t,e,r)=>GXe(t,e,r)>0;Oie.exports=WXe});var rk=L((gWt,Lie)=>{var YXe=vc(),VXe=(t,e,r)=>YXe(t,e,r)<0;Lie.exports=VXe});var FU=L((dWt,Mie)=>{var KXe=vc(),JXe=(t,e,r)=>KXe(t,e,r)===0;Mie.exports=JXe});var NU=L((mWt,_ie)=>{var zXe=vc(),ZXe=(t,e,r)=>zXe(t,e,r)!==0;_ie.exports=ZXe});var nk=L((yWt,Uie)=>{var XXe=vc(),$Xe=(t,e,r)=>XXe(t,e,r)>=0;Uie.exports=$Xe});var ik=L((EWt,Hie)=>{var e$e=vc(),t$e=(t,e,r)=>e$e(t,e,r)<=0;Hie.exports=t$e});var OU=L((IWt,jie)=>{var r$e=FU(),n$e=NU(),i$e=cB(),s$e=nk(),o$e=rk(),a$e=ik(),l$e=(t,e,r,s)=>{switch(e){case"===":return typeof t=="object"&&(t=t.version),typeof r=="object"&&(r=r.version),t===r;case"!==":return typeof t=="object"&&(t=t.version),typeof r=="object"&&(r=r.version),t!==r;case"":case"=":case"==":return r$e(t,r,s);case"!=":return n$e(t,r,s);case">":return i$e(t,r,s);case">=":return s$e(t,r,s);case"<":return o$e(t,r,s);case"<=":return a$e(t,r,s);default:throw new TypeError(`Invalid operator: ${e}`)}};jie.exports=l$e});var Gie=L((CWt,qie)=>{var c$e=Go(),u$e=Ld(),{safeRe:sk,t:ok}=wE(),f$e=(t,e)=>{if(t instanceof c$e)return t;if(typeof t=="number"&&(t=String(t)),typeof t!="string")return null;e=e||{};let r=null;if(!e.rtl)r=t.match(e.includePrerelease?sk[ok.COERCEFULL]:sk[ok.COERCE]);else{let p=e.includePrerelease?sk[ok.COERCERTLFULL]:sk[ok.COERCERTL],h;for(;(h=p.exec(t))&&(!r||r.index+r[0].length!==t.length);)(!r||h.index+h[0].length!==r.index+r[0].length)&&(r=h),p.lastIndex=h.index+h[1].length+h[2].length;p.lastIndex=-1}if(r===null)return null;let s=r[2],a=r[3]||"0",n=r[4]||"0",c=e.includePrerelease&&r[5]?`-${r[5]}`:"",f=e.includePrerelease&&r[6]?`+${r[6]}`:"";return u$e(`${s}.${a}.${n}${c}${f}`,e)};qie.exports=f$e});var Yie=L((wWt,Wie)=>{"use strict";Wie.exports=function(t){t.prototype[Symbol.iterator]=function*(){for(let e=this.head;e;e=e.next)yield e.value}}});var ak=L((BWt,Vie)=>{"use strict";Vie.exports=Fn;Fn.Node=Md;Fn.create=Fn;function Fn(t){var e=this;if(e instanceof Fn||(e=new Fn),e.tail=null,e.head=null,e.length=0,t&&typeof t.forEach=="function")t.forEach(function(a){e.push(a)});else if(arguments.length>0)for(var r=0,s=arguments.length;r1)r=e;else if(this.head)s=this.head.next,r=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var a=0;s!==null;a++)r=t(r,s.value,a),s=s.next;return r};Fn.prototype.reduceReverse=function(t,e){var r,s=this.tail;if(arguments.length>1)r=e;else if(this.tail)s=this.tail.prev,r=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(var a=this.length-1;s!==null;a--)r=t(r,s.value,a),s=s.prev;return r};Fn.prototype.toArray=function(){for(var t=new Array(this.length),e=0,r=this.head;r!==null;e++)t[e]=r.value,r=r.next;return t};Fn.prototype.toArrayReverse=function(){for(var t=new Array(this.length),e=0,r=this.tail;r!==null;e++)t[e]=r.value,r=r.prev;return t};Fn.prototype.slice=function(t,e){e=e||this.length,e<0&&(e+=this.length),t=t||0,t<0&&(t+=this.length);var r=new Fn;if(ethis.length&&(e=this.length);for(var s=0,a=this.head;a!==null&&sthis.length&&(e=this.length);for(var s=this.length,a=this.tail;a!==null&&s>e;s--)a=a.prev;for(;a!==null&&s>t;s--,a=a.prev)r.push(a.value);return r};Fn.prototype.splice=function(t,e,...r){t>this.length&&(t=this.length-1),t<0&&(t=this.length+t);for(var s=0,a=this.head;a!==null&&s{"use strict";var g$e=ak(),_d=Symbol("max"),bp=Symbol("length"),vE=Symbol("lengthCalculator"),fB=Symbol("allowStale"),Ud=Symbol("maxAge"),Dp=Symbol("dispose"),Kie=Symbol("noDisposeOnSet"),Ws=Symbol("lruList"),_u=Symbol("cache"),zie=Symbol("updateAgeOnGet"),LU=()=>1,_U=class{constructor(e){if(typeof e=="number"&&(e={max:e}),e||(e={}),e.max&&(typeof e.max!="number"||e.max<0))throw new TypeError("max must be a non-negative number");let r=this[_d]=e.max||1/0,s=e.length||LU;if(this[vE]=typeof s!="function"?LU:s,this[fB]=e.stale||!1,e.maxAge&&typeof e.maxAge!="number")throw new TypeError("maxAge must be a number");this[Ud]=e.maxAge||0,this[Dp]=e.dispose,this[Kie]=e.noDisposeOnSet||!1,this[zie]=e.updateAgeOnGet||!1,this.reset()}set max(e){if(typeof e!="number"||e<0)throw new TypeError("max must be a non-negative number");this[_d]=e||1/0,uB(this)}get max(){return this[_d]}set allowStale(e){this[fB]=!!e}get allowStale(){return this[fB]}set maxAge(e){if(typeof e!="number")throw new TypeError("maxAge must be a non-negative number");this[Ud]=e,uB(this)}get maxAge(){return this[Ud]}set lengthCalculator(e){typeof e!="function"&&(e=LU),e!==this[vE]&&(this[vE]=e,this[bp]=0,this[Ws].forEach(r=>{r.length=this[vE](r.value,r.key),this[bp]+=r.length})),uB(this)}get lengthCalculator(){return this[vE]}get length(){return this[bp]}get itemCount(){return this[Ws].length}rforEach(e,r){r=r||this;for(let s=this[Ws].tail;s!==null;){let a=s.prev;Jie(this,e,s,r),s=a}}forEach(e,r){r=r||this;for(let s=this[Ws].head;s!==null;){let a=s.next;Jie(this,e,s,r),s=a}}keys(){return this[Ws].toArray().map(e=>e.key)}values(){return this[Ws].toArray().map(e=>e.value)}reset(){this[Dp]&&this[Ws]&&this[Ws].length&&this[Ws].forEach(e=>this[Dp](e.key,e.value)),this[_u]=new Map,this[Ws]=new g$e,this[bp]=0}dump(){return this[Ws].map(e=>lk(this,e)?!1:{k:e.key,v:e.value,e:e.now+(e.maxAge||0)}).toArray().filter(e=>e)}dumpLru(){return this[Ws]}set(e,r,s){if(s=s||this[Ud],s&&typeof s!="number")throw new TypeError("maxAge must be a number");let a=s?Date.now():0,n=this[vE](r,e);if(this[_u].has(e)){if(n>this[_d])return SE(this,this[_u].get(e)),!1;let p=this[_u].get(e).value;return this[Dp]&&(this[Kie]||this[Dp](e,p.value)),p.now=a,p.maxAge=s,p.value=r,this[bp]+=n-p.length,p.length=n,this.get(e),uB(this),!0}let c=new UU(e,r,n,a,s);return c.length>this[_d]?(this[Dp]&&this[Dp](e,r),!1):(this[bp]+=c.length,this[Ws].unshift(c),this[_u].set(e,this[Ws].head),uB(this),!0)}has(e){if(!this[_u].has(e))return!1;let r=this[_u].get(e).value;return!lk(this,r)}get(e){return MU(this,e,!0)}peek(e){return MU(this,e,!1)}pop(){let e=this[Ws].tail;return e?(SE(this,e),e.value):null}del(e){SE(this,this[_u].get(e))}load(e){this.reset();let r=Date.now();for(let s=e.length-1;s>=0;s--){let a=e[s],n=a.e||0;if(n===0)this.set(a.k,a.v);else{let c=n-r;c>0&&this.set(a.k,a.v,c)}}}prune(){this[_u].forEach((e,r)=>MU(this,r,!1))}},MU=(t,e,r)=>{let s=t[_u].get(e);if(s){let a=s.value;if(lk(t,a)){if(SE(t,s),!t[fB])return}else r&&(t[zie]&&(s.value.now=Date.now()),t[Ws].unshiftNode(s));return a.value}},lk=(t,e)=>{if(!e||!e.maxAge&&!t[Ud])return!1;let r=Date.now()-e.now;return e.maxAge?r>e.maxAge:t[Ud]&&r>t[Ud]},uB=t=>{if(t[bp]>t[_d])for(let e=t[Ws].tail;t[bp]>t[_d]&&e!==null;){let r=e.prev;SE(t,e),e=r}},SE=(t,e)=>{if(e){let r=e.value;t[Dp]&&t[Dp](r.key,r.value),t[bp]-=r.length,t[_u].delete(r.key),t[Ws].removeNode(e)}},UU=class{constructor(e,r,s,a,n){this.key=e,this.value=r,this.length=s,this.now=a,this.maxAge=n||0}},Jie=(t,e,r,s)=>{let a=r.value;lk(t,a)&&(SE(t,r),t[fB]||(a=void 0)),a&&e.call(s,a.value,a.key,t)};Zie.exports=_U});var Sc=L((SWt,rse)=>{var HU=class t{constructor(e,r){if(r=m$e(r),e instanceof t)return e.loose===!!r.loose&&e.includePrerelease===!!r.includePrerelease?e:new t(e.raw,r);if(e instanceof jU)return this.raw=e.value,this.set=[[e]],this.format(),this;if(this.options=r,this.loose=!!r.loose,this.includePrerelease=!!r.includePrerelease,this.raw=e.trim().split(/\s+/).join(" "),this.set=this.raw.split("||").map(s=>this.parseRange(s.trim())).filter(s=>s.length),!this.set.length)throw new TypeError(`Invalid SemVer Range: ${this.raw}`);if(this.set.length>1){let s=this.set[0];if(this.set=this.set.filter(a=>!ese(a[0])),this.set.length===0)this.set=[s];else if(this.set.length>1){for(let a of this.set)if(a.length===1&&v$e(a[0])){this.set=[a];break}}}this.format()}format(){return this.range=this.set.map(e=>e.join(" ").trim()).join("||").trim(),this.range}toString(){return this.range}parseRange(e){let s=((this.options.includePrerelease&&w$e)|(this.options.loose&&B$e))+":"+e,a=$ie.get(s);if(a)return a;let n=this.options.loose,c=n?ol[Ba.HYPHENRANGELOOSE]:ol[Ba.HYPHENRANGE];e=e.replace(c,F$e(this.options.includePrerelease)),Si("hyphen replace",e),e=e.replace(ol[Ba.COMPARATORTRIM],E$e),Si("comparator trim",e),e=e.replace(ol[Ba.TILDETRIM],I$e),Si("tilde trim",e),e=e.replace(ol[Ba.CARETTRIM],C$e),Si("caret trim",e);let f=e.split(" ").map(C=>S$e(C,this.options)).join(" ").split(/\s+/).map(C=>R$e(C,this.options));n&&(f=f.filter(C=>(Si("loose invalid filter",C,this.options),!!C.match(ol[Ba.COMPARATORLOOSE])))),Si("range list",f);let p=new Map,h=f.map(C=>new jU(C,this.options));for(let C of h){if(ese(C))return[C];p.set(C.value,C)}p.size>1&&p.has("")&&p.delete("");let E=[...p.values()];return $ie.set(s,E),E}intersects(e,r){if(!(e instanceof t))throw new TypeError("a Range is required");return this.set.some(s=>tse(s,r)&&e.set.some(a=>tse(a,r)&&s.every(n=>a.every(c=>n.intersects(c,r)))))}test(e){if(!e)return!1;if(typeof e=="string")try{e=new y$e(e,this.options)}catch{return!1}for(let r=0;rt.value==="<0.0.0-0",v$e=t=>t.value==="",tse=(t,e)=>{let r=!0,s=t.slice(),a=s.pop();for(;r&&s.length;)r=s.every(n=>a.intersects(n,e)),a=s.pop();return r},S$e=(t,e)=>(Si("comp",t,e),t=P$e(t,e),Si("caret",t),t=D$e(t,e),Si("tildes",t),t=k$e(t,e),Si("xrange",t),t=T$e(t,e),Si("stars",t),t),va=t=>!t||t.toLowerCase()==="x"||t==="*",D$e=(t,e)=>t.trim().split(/\s+/).map(r=>b$e(r,e)).join(" "),b$e=(t,e)=>{let r=e.loose?ol[Ba.TILDELOOSE]:ol[Ba.TILDE];return t.replace(r,(s,a,n,c,f)=>{Si("tilde",t,s,a,n,c,f);let p;return va(a)?p="":va(n)?p=`>=${a}.0.0 <${+a+1}.0.0-0`:va(c)?p=`>=${a}.${n}.0 <${a}.${+n+1}.0-0`:f?(Si("replaceTilde pr",f),p=`>=${a}.${n}.${c}-${f} <${a}.${+n+1}.0-0`):p=`>=${a}.${n}.${c} <${a}.${+n+1}.0-0`,Si("tilde return",p),p})},P$e=(t,e)=>t.trim().split(/\s+/).map(r=>x$e(r,e)).join(" "),x$e=(t,e)=>{Si("caret",t,e);let r=e.loose?ol[Ba.CARETLOOSE]:ol[Ba.CARET],s=e.includePrerelease?"-0":"";return t.replace(r,(a,n,c,f,p)=>{Si("caret",t,a,n,c,f,p);let h;return va(n)?h="":va(c)?h=`>=${n}.0.0${s} <${+n+1}.0.0-0`:va(f)?n==="0"?h=`>=${n}.${c}.0${s} <${n}.${+c+1}.0-0`:h=`>=${n}.${c}.0${s} <${+n+1}.0.0-0`:p?(Si("replaceCaret pr",p),n==="0"?c==="0"?h=`>=${n}.${c}.${f}-${p} <${n}.${c}.${+f+1}-0`:h=`>=${n}.${c}.${f}-${p} <${n}.${+c+1}.0-0`:h=`>=${n}.${c}.${f}-${p} <${+n+1}.0.0-0`):(Si("no pr"),n==="0"?c==="0"?h=`>=${n}.${c}.${f}${s} <${n}.${c}.${+f+1}-0`:h=`>=${n}.${c}.${f}${s} <${n}.${+c+1}.0-0`:h=`>=${n}.${c}.${f} <${+n+1}.0.0-0`),Si("caret return",h),h})},k$e=(t,e)=>(Si("replaceXRanges",t,e),t.split(/\s+/).map(r=>Q$e(r,e)).join(" ")),Q$e=(t,e)=>{t=t.trim();let r=e.loose?ol[Ba.XRANGELOOSE]:ol[Ba.XRANGE];return t.replace(r,(s,a,n,c,f,p)=>{Si("xRange",t,s,a,n,c,f,p);let h=va(n),E=h||va(c),C=E||va(f),S=C;return a==="="&&S&&(a=""),p=e.includePrerelease?"-0":"",h?a===">"||a==="<"?s="<0.0.0-0":s="*":a&&S?(E&&(c=0),f=0,a===">"?(a=">=",E?(n=+n+1,c=0,f=0):(c=+c+1,f=0)):a==="<="&&(a="<",E?n=+n+1:c=+c+1),a==="<"&&(p="-0"),s=`${a+n}.${c}.${f}${p}`):E?s=`>=${n}.0.0${p} <${+n+1}.0.0-0`:C&&(s=`>=${n}.${c}.0${p} <${n}.${+c+1}.0-0`),Si("xRange return",s),s})},T$e=(t,e)=>(Si("replaceStars",t,e),t.trim().replace(ol[Ba.STAR],"")),R$e=(t,e)=>(Si("replaceGTE0",t,e),t.trim().replace(ol[e.includePrerelease?Ba.GTE0PRE:Ba.GTE0],"")),F$e=t=>(e,r,s,a,n,c,f,p,h,E,C,S,P)=>(va(s)?r="":va(a)?r=`>=${s}.0.0${t?"-0":""}`:va(n)?r=`>=${s}.${a}.0${t?"-0":""}`:c?r=`>=${r}`:r=`>=${r}${t?"-0":""}`,va(h)?p="":va(E)?p=`<${+h+1}.0.0-0`:va(C)?p=`<${h}.${+E+1}.0-0`:S?p=`<=${h}.${E}.${C}-${S}`:t?p=`<${h}.${E}.${+C+1}-0`:p=`<=${p}`,`${r} ${p}`.trim()),N$e=(t,e,r)=>{for(let s=0;s0){let a=t[s].semver;if(a.major===e.major&&a.minor===e.minor&&a.patch===e.patch)return!0}return!1}return!0}});var AB=L((DWt,lse)=>{var pB=Symbol("SemVer ANY"),WU=class t{static get ANY(){return pB}constructor(e,r){if(r=nse(r),e instanceof t){if(e.loose===!!r.loose)return e;e=e.value}e=e.trim().split(/\s+/).join(" "),GU("comparator",e,r),this.options=r,this.loose=!!r.loose,this.parse(e),this.semver===pB?this.value="":this.value=this.operator+this.semver.version,GU("comp",this)}parse(e){let r=this.options.loose?ise[sse.COMPARATORLOOSE]:ise[sse.COMPARATOR],s=e.match(r);if(!s)throw new TypeError(`Invalid comparator: ${e}`);this.operator=s[1]!==void 0?s[1]:"",this.operator==="="&&(this.operator=""),s[2]?this.semver=new ose(s[2],this.options.loose):this.semver=pB}toString(){return this.value}test(e){if(GU("Comparator.test",e,this.options.loose),this.semver===pB||e===pB)return!0;if(typeof e=="string")try{e=new ose(e,this.options)}catch{return!1}return qU(e,this.operator,this.semver,this.options)}intersects(e,r){if(!(e instanceof t))throw new TypeError("a Comparator is required");return this.operator===""?this.value===""?!0:new ase(e.value,r).test(this.value):e.operator===""?e.value===""?!0:new ase(this.value,r).test(e.semver):(r=nse(r),r.includePrerelease&&(this.value==="<0.0.0-0"||e.value==="<0.0.0-0")||!r.includePrerelease&&(this.value.startsWith("<0.0.0")||e.value.startsWith("<0.0.0"))?!1:!!(this.operator.startsWith(">")&&e.operator.startsWith(">")||this.operator.startsWith("<")&&e.operator.startsWith("<")||this.semver.version===e.semver.version&&this.operator.includes("=")&&e.operator.includes("=")||qU(this.semver,"<",e.semver,r)&&this.operator.startsWith(">")&&e.operator.startsWith("<")||qU(this.semver,">",e.semver,r)&&this.operator.startsWith("<")&&e.operator.startsWith(">")))}};lse.exports=WU;var nse=Xx(),{safeRe:ise,t:sse}=wE(),qU=OU(),GU=lB(),ose=Go(),ase=Sc()});var hB=L((bWt,cse)=>{var O$e=Sc(),L$e=(t,e,r)=>{try{e=new O$e(e,r)}catch{return!1}return e.test(t)};cse.exports=L$e});var fse=L((PWt,use)=>{var M$e=Sc(),_$e=(t,e)=>new M$e(t,e).set.map(r=>r.map(s=>s.value).join(" ").trim().split(" "));use.exports=_$e});var pse=L((xWt,Ase)=>{var U$e=Go(),H$e=Sc(),j$e=(t,e,r)=>{let s=null,a=null,n=null;try{n=new H$e(e,r)}catch{return null}return t.forEach(c=>{n.test(c)&&(!s||a.compare(c)===-1)&&(s=c,a=new U$e(s,r))}),s};Ase.exports=j$e});var gse=L((kWt,hse)=>{var q$e=Go(),G$e=Sc(),W$e=(t,e,r)=>{let s=null,a=null,n=null;try{n=new G$e(e,r)}catch{return null}return t.forEach(c=>{n.test(c)&&(!s||a.compare(c)===1)&&(s=c,a=new q$e(s,r))}),s};hse.exports=W$e});var yse=L((QWt,mse)=>{var YU=Go(),Y$e=Sc(),dse=cB(),V$e=(t,e)=>{t=new Y$e(t,e);let r=new YU("0.0.0");if(t.test(r)||(r=new YU("0.0.0-0"),t.test(r)))return r;r=null;for(let s=0;s{let f=new YU(c.semver.version);switch(c.operator){case">":f.prerelease.length===0?f.patch++:f.prerelease.push(0),f.raw=f.format();case"":case">=":(!n||dse(f,n))&&(n=f);break;case"<":case"<=":break;default:throw new Error(`Unexpected operation: ${c.operator}`)}}),n&&(!r||dse(r,n))&&(r=n)}return r&&t.test(r)?r:null};mse.exports=V$e});var Ise=L((TWt,Ese)=>{var K$e=Sc(),J$e=(t,e)=>{try{return new K$e(t,e).range||"*"}catch{return null}};Ese.exports=J$e});var ck=L((RWt,vse)=>{var z$e=Go(),Bse=AB(),{ANY:Z$e}=Bse,X$e=Sc(),$$e=hB(),Cse=cB(),wse=rk(),eet=ik(),tet=nk(),ret=(t,e,r,s)=>{t=new z$e(t,s),e=new X$e(e,s);let a,n,c,f,p;switch(r){case">":a=Cse,n=eet,c=wse,f=">",p=">=";break;case"<":a=wse,n=tet,c=Cse,f="<",p="<=";break;default:throw new TypeError('Must provide a hilo val of "<" or ">"')}if($$e(t,e,s))return!1;for(let h=0;h{P.semver===Z$e&&(P=new Bse(">=0.0.0")),C=C||P,S=S||P,a(P.semver,C.semver,s)?C=P:c(P.semver,S.semver,s)&&(S=P)}),C.operator===f||C.operator===p||(!S.operator||S.operator===f)&&n(t,S.semver))return!1;if(S.operator===p&&c(t,S.semver))return!1}return!0};vse.exports=ret});var Dse=L((FWt,Sse)=>{var net=ck(),iet=(t,e,r)=>net(t,e,">",r);Sse.exports=iet});var Pse=L((NWt,bse)=>{var set=ck(),oet=(t,e,r)=>set(t,e,"<",r);bse.exports=oet});var Qse=L((OWt,kse)=>{var xse=Sc(),aet=(t,e,r)=>(t=new xse(t,r),e=new xse(e,r),t.intersects(e,r));kse.exports=aet});var Rse=L((LWt,Tse)=>{var cet=hB(),uet=vc();Tse.exports=(t,e,r)=>{let s=[],a=null,n=null,c=t.sort((E,C)=>uet(E,C,r));for(let E of c)cet(E,e,r)?(n=E,a||(a=E)):(n&&s.push([a,n]),n=null,a=null);a&&s.push([a,null]);let f=[];for(let[E,C]of s)E===C?f.push(E):!C&&E===c[0]?f.push("*"):C?E===c[0]?f.push(`<=${C}`):f.push(`${E} - ${C}`):f.push(`>=${E}`);let p=f.join(" || "),h=typeof e.raw=="string"?e.raw:String(e);return p.length{var Fse=Sc(),KU=AB(),{ANY:VU}=KU,gB=hB(),JU=vc(),fet=(t,e,r={})=>{if(t===e)return!0;t=new Fse(t,r),e=new Fse(e,r);let s=!1;e:for(let a of t.set){for(let n of e.set){let c=pet(a,n,r);if(s=s||c!==null,c)continue e}if(s)return!1}return!0},Aet=[new KU(">=0.0.0-0")],Nse=[new KU(">=0.0.0")],pet=(t,e,r)=>{if(t===e)return!0;if(t.length===1&&t[0].semver===VU){if(e.length===1&&e[0].semver===VU)return!0;r.includePrerelease?t=Aet:t=Nse}if(e.length===1&&e[0].semver===VU){if(r.includePrerelease)return!0;e=Nse}let s=new Set,a,n;for(let P of t)P.operator===">"||P.operator===">="?a=Ose(a,P,r):P.operator==="<"||P.operator==="<="?n=Lse(n,P,r):s.add(P.semver);if(s.size>1)return null;let c;if(a&&n){if(c=JU(a.semver,n.semver,r),c>0)return null;if(c===0&&(a.operator!==">="||n.operator!=="<="))return null}for(let P of s){if(a&&!gB(P,String(a),r)||n&&!gB(P,String(n),r))return null;for(let I of e)if(!gB(P,String(I),r))return!1;return!0}let f,p,h,E,C=n&&!r.includePrerelease&&n.semver.prerelease.length?n.semver:!1,S=a&&!r.includePrerelease&&a.semver.prerelease.length?a.semver:!1;C&&C.prerelease.length===1&&n.operator==="<"&&C.prerelease[0]===0&&(C=!1);for(let P of e){if(E=E||P.operator===">"||P.operator===">=",h=h||P.operator==="<"||P.operator==="<=",a){if(S&&P.semver.prerelease&&P.semver.prerelease.length&&P.semver.major===S.major&&P.semver.minor===S.minor&&P.semver.patch===S.patch&&(S=!1),P.operator===">"||P.operator===">="){if(f=Ose(a,P,r),f===P&&f!==a)return!1}else if(a.operator===">="&&!gB(a.semver,String(P),r))return!1}if(n){if(C&&P.semver.prerelease&&P.semver.prerelease.length&&P.semver.major===C.major&&P.semver.minor===C.minor&&P.semver.patch===C.patch&&(C=!1),P.operator==="<"||P.operator==="<="){if(p=Lse(n,P,r),p===P&&p!==n)return!1}else if(n.operator==="<="&&!gB(n.semver,String(P),r))return!1}if(!P.operator&&(n||a)&&c!==0)return!1}return!(a&&h&&!n&&c!==0||n&&E&&!a&&c!==0||S||C)},Ose=(t,e,r)=>{if(!t)return e;let s=JU(t.semver,e.semver,r);return s>0?t:s<0||e.operator===">"&&t.operator===">="?e:t},Lse=(t,e,r)=>{if(!t)return e;let s=JU(t.semver,e.semver,r);return s<0?t:s>0||e.operator==="<"&&t.operator==="<="?e:t};Mse.exports=fet});var Ai=L((_Wt,jse)=>{var zU=wE(),Use=aB(),het=Go(),Hse=TU(),get=Ld(),det=aie(),met=cie(),yet=Aie(),Eet=gie(),Iet=mie(),Cet=Eie(),wet=Cie(),Bet=Bie(),vet=vc(),Det=bie(),bet=xie(),Pet=tk(),xet=Rie(),ket=Nie(),Qet=cB(),Tet=rk(),Ret=FU(),Fet=NU(),Net=nk(),Oet=ik(),Let=OU(),Met=Gie(),_et=AB(),Uet=Sc(),Het=hB(),jet=fse(),qet=pse(),Get=gse(),Wet=yse(),Yet=Ise(),Vet=ck(),Ket=Dse(),Jet=Pse(),zet=Qse(),Zet=Rse(),Xet=_se();jse.exports={parse:get,valid:det,clean:met,inc:yet,diff:Eet,major:Iet,minor:Cet,patch:wet,prerelease:Bet,compare:vet,rcompare:Det,compareLoose:bet,compareBuild:Pet,sort:xet,rsort:ket,gt:Qet,lt:Tet,eq:Ret,neq:Fet,gte:Net,lte:Oet,cmp:Let,coerce:Met,Comparator:_et,Range:Uet,satisfies:Het,toComparators:jet,maxSatisfying:qet,minSatisfying:Get,minVersion:Wet,validRange:Yet,outside:Vet,gtr:Ket,ltr:Jet,intersects:zet,simplifyRange:Zet,subset:Xet,SemVer:het,re:zU.re,src:zU.src,tokens:zU.t,SEMVER_SPEC_VERSION:Use.SEMVER_SPEC_VERSION,RELEASE_TYPES:Use.RELEASE_TYPES,compareIdentifiers:Hse.compareIdentifiers,rcompareIdentifiers:Hse.rcompareIdentifiers}});var Gse=L((UWt,qse)=>{"use strict";function $et(t,e){function r(){this.constructor=t}r.prototype=e.prototype,t.prototype=new r}function Hd(t,e,r,s){this.message=t,this.expected=e,this.found=r,this.location=s,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,Hd)}$et(Hd,Error);Hd.buildMessage=function(t,e){var r={literal:function(h){return'"'+a(h.text)+'"'},class:function(h){var E="",C;for(C=0;C0){for(C=1,S=1;C{switch(Te[1]){case"|":return xe|Te[3];case"&":return xe&Te[3];case"^":return xe^Te[3]}},$)},S="!",P=Fe("!",!1),I=function($){return!$},R="(",N=Fe("(",!1),U=")",W=Fe(")",!1),te=function($){return $},ie=/^[^ \t\n\r()!|&\^]/,Ae=Ne([" "," ",` `,"\r","(",")","!","|","&","^"],!0,!1),ce=function($){return e.queryPattern.test($)},me=function($){return e.checkFn($)},pe=ke("whitespace"),Be=/^[ \t\n\r]/,Ce=Ne([" "," ",` `,"\r"],!1,!1),g=0,we=0,ye=[{line:1,column:1}],fe=0,se=[],X=0,De;if("startRule"in e){if(!(e.startRule in s))throw new Error(`Can't start parsing from rule "`+e.startRule+'".');a=s[e.startRule]}function Re(){return t.substring(we,g)}function dt(){return _e(we,g)}function j($,oe){throw oe=oe!==void 0?oe:_e(we,g),b([ke($)],t.substring(we,g),oe)}function rt($,oe){throw oe=oe!==void 0?oe:_e(we,g),w($,oe)}function Fe($,oe){return{type:"literal",text:$,ignoreCase:oe}}function Ne($,oe,xe){return{type:"class",parts:$,inverted:oe,ignoreCase:xe}}function Pe(){return{type:"any"}}function Ye(){return{type:"end"}}function ke($){return{type:"other",description:$}}function it($){var oe=ye[$],xe;if(oe)return oe;for(xe=$-1;!ye[xe];)xe--;for(oe=ye[xe],oe={line:oe.line,column:oe.column};xe<$;)t.charCodeAt(xe)===10?(oe.line++,oe.column=1):oe.column++,xe++;return ye[$]=oe,oe}function _e($,oe){var xe=it($),Te=it(oe);return{start:{offset:$,line:xe.line,column:xe.column},end:{offset:oe,line:Te.line,column:Te.column}}}function x($){gfe&&(fe=g,se=[]),se.push($))}function w($,oe){return new Hd($,null,null,oe)}function b($,oe,xe){return new Hd(Hd.buildMessage($,oe),$,oe,xe)}function y(){var $,oe,xe,Te,lt,It,qt,ir;if($=g,oe=F(),oe!==r){for(xe=[],Te=g,lt=Z(),lt!==r?(t.charCodeAt(g)===124?(It=n,g++):(It=r,X===0&&x(c)),It===r&&(t.charCodeAt(g)===38?(It=f,g++):(It=r,X===0&&x(p)),It===r&&(t.charCodeAt(g)===94?(It=h,g++):(It=r,X===0&&x(E)))),It!==r?(qt=Z(),qt!==r?(ir=F(),ir!==r?(lt=[lt,It,qt,ir],Te=lt):(g=Te,Te=r)):(g=Te,Te=r)):(g=Te,Te=r)):(g=Te,Te=r);Te!==r;)xe.push(Te),Te=g,lt=Z(),lt!==r?(t.charCodeAt(g)===124?(It=n,g++):(It=r,X===0&&x(c)),It===r&&(t.charCodeAt(g)===38?(It=f,g++):(It=r,X===0&&x(p)),It===r&&(t.charCodeAt(g)===94?(It=h,g++):(It=r,X===0&&x(E)))),It!==r?(qt=Z(),qt!==r?(ir=F(),ir!==r?(lt=[lt,It,qt,ir],Te=lt):(g=Te,Te=r)):(g=Te,Te=r)):(g=Te,Te=r)):(g=Te,Te=r);xe!==r?(we=$,oe=C(oe,xe),$=oe):(g=$,$=r)}else g=$,$=r;return $}function F(){var $,oe,xe,Te,lt,It;return $=g,t.charCodeAt(g)===33?(oe=S,g++):(oe=r,X===0&&x(P)),oe!==r?(xe=F(),xe!==r?(we=$,oe=I(xe),$=oe):(g=$,$=r)):(g=$,$=r),$===r&&($=g,t.charCodeAt(g)===40?(oe=R,g++):(oe=r,X===0&&x(N)),oe!==r?(xe=Z(),xe!==r?(Te=y(),Te!==r?(lt=Z(),lt!==r?(t.charCodeAt(g)===41?(It=U,g++):(It=r,X===0&&x(W)),It!==r?(we=$,oe=te(Te),$=oe):(g=$,$=r)):(g=$,$=r)):(g=$,$=r)):(g=$,$=r)):(g=$,$=r),$===r&&($=z())),$}function z(){var $,oe,xe,Te,lt;if($=g,oe=Z(),oe!==r){if(xe=g,Te=[],ie.test(t.charAt(g))?(lt=t.charAt(g),g++):(lt=r,X===0&&x(Ae)),lt!==r)for(;lt!==r;)Te.push(lt),ie.test(t.charAt(g))?(lt=t.charAt(g),g++):(lt=r,X===0&&x(Ae));else Te=r;Te!==r?xe=t.substring(xe,g):xe=Te,xe!==r?(we=g,Te=ce(xe),Te?Te=void 0:Te=r,Te!==r?(we=$,oe=me(xe),$=oe):(g=$,$=r)):(g=$,$=r)}else g=$,$=r;return $}function Z(){var $,oe;for(X++,$=[],Be.test(t.charAt(g))?(oe=t.charAt(g),g++):(oe=r,X===0&&x(Ce));oe!==r;)$.push(oe),Be.test(t.charAt(g))?(oe=t.charAt(g),g++):(oe=r,X===0&&x(Ce));return X--,$===r&&(oe=r,X===0&&x(pe)),$}if(De=a(),De!==r&&g===t.length)return De;throw De!==r&&g{var{parse:ttt}=Gse();uk.makeParser=(t=/[a-z]+/)=>(e,r)=>ttt(e,{queryPattern:t,checkFn:r});uk.parse=uk.makeParser()});var Vse=L((jWt,Yse)=>{"use strict";Yse.exports={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}});var ZU=L((qWt,Jse)=>{var dB=Vse(),Kse={};for(let t of Object.keys(dB))Kse[dB[t]]=t;var hr={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};Jse.exports=hr;for(let t of Object.keys(hr)){if(!("channels"in hr[t]))throw new Error("missing channels property: "+t);if(!("labels"in hr[t]))throw new Error("missing channel labels property: "+t);if(hr[t].labels.length!==hr[t].channels)throw new Error("channel and label counts mismatch: "+t);let{channels:e,labels:r}=hr[t];delete hr[t].channels,delete hr[t].labels,Object.defineProperty(hr[t],"channels",{value:e}),Object.defineProperty(hr[t],"labels",{value:r})}hr.rgb.hsl=function(t){let e=t[0]/255,r=t[1]/255,s=t[2]/255,a=Math.min(e,r,s),n=Math.max(e,r,s),c=n-a,f,p;n===a?f=0:e===n?f=(r-s)/c:r===n?f=2+(s-e)/c:s===n&&(f=4+(e-r)/c),f=Math.min(f*60,360),f<0&&(f+=360);let h=(a+n)/2;return n===a?p=0:h<=.5?p=c/(n+a):p=c/(2-n-a),[f,p*100,h*100]};hr.rgb.hsv=function(t){let e,r,s,a,n,c=t[0]/255,f=t[1]/255,p=t[2]/255,h=Math.max(c,f,p),E=h-Math.min(c,f,p),C=function(S){return(h-S)/6/E+1/2};return E===0?(a=0,n=0):(n=E/h,e=C(c),r=C(f),s=C(p),c===h?a=s-r:f===h?a=1/3+e-s:p===h&&(a=2/3+r-e),a<0?a+=1:a>1&&(a-=1)),[a*360,n*100,h*100]};hr.rgb.hwb=function(t){let e=t[0],r=t[1],s=t[2],a=hr.rgb.hsl(t)[0],n=1/255*Math.min(e,Math.min(r,s));return s=1-1/255*Math.max(e,Math.max(r,s)),[a,n*100,s*100]};hr.rgb.cmyk=function(t){let e=t[0]/255,r=t[1]/255,s=t[2]/255,a=Math.min(1-e,1-r,1-s),n=(1-e-a)/(1-a)||0,c=(1-r-a)/(1-a)||0,f=(1-s-a)/(1-a)||0;return[n*100,c*100,f*100,a*100]};function rtt(t,e){return(t[0]-e[0])**2+(t[1]-e[1])**2+(t[2]-e[2])**2}hr.rgb.keyword=function(t){let e=Kse[t];if(e)return e;let r=1/0,s;for(let a of Object.keys(dB)){let n=dB[a],c=rtt(t,n);c.04045?((e+.055)/1.055)**2.4:e/12.92,r=r>.04045?((r+.055)/1.055)**2.4:r/12.92,s=s>.04045?((s+.055)/1.055)**2.4:s/12.92;let a=e*.4124+r*.3576+s*.1805,n=e*.2126+r*.7152+s*.0722,c=e*.0193+r*.1192+s*.9505;return[a*100,n*100,c*100]};hr.rgb.lab=function(t){let e=hr.rgb.xyz(t),r=e[0],s=e[1],a=e[2];r/=95.047,s/=100,a/=108.883,r=r>.008856?r**(1/3):7.787*r+16/116,s=s>.008856?s**(1/3):7.787*s+16/116,a=a>.008856?a**(1/3):7.787*a+16/116;let n=116*s-16,c=500*(r-s),f=200*(s-a);return[n,c,f]};hr.hsl.rgb=function(t){let e=t[0]/360,r=t[1]/100,s=t[2]/100,a,n,c;if(r===0)return c=s*255,[c,c,c];s<.5?a=s*(1+r):a=s+r-s*r;let f=2*s-a,p=[0,0,0];for(let h=0;h<3;h++)n=e+1/3*-(h-1),n<0&&n++,n>1&&n--,6*n<1?c=f+(a-f)*6*n:2*n<1?c=a:3*n<2?c=f+(a-f)*(2/3-n)*6:c=f,p[h]=c*255;return p};hr.hsl.hsv=function(t){let e=t[0],r=t[1]/100,s=t[2]/100,a=r,n=Math.max(s,.01);s*=2,r*=s<=1?s:2-s,a*=n<=1?n:2-n;let c=(s+r)/2,f=s===0?2*a/(n+a):2*r/(s+r);return[e,f*100,c*100]};hr.hsv.rgb=function(t){let e=t[0]/60,r=t[1]/100,s=t[2]/100,a=Math.floor(e)%6,n=e-Math.floor(e),c=255*s*(1-r),f=255*s*(1-r*n),p=255*s*(1-r*(1-n));switch(s*=255,a){case 0:return[s,p,c];case 1:return[f,s,c];case 2:return[c,s,p];case 3:return[c,f,s];case 4:return[p,c,s];case 5:return[s,c,f]}};hr.hsv.hsl=function(t){let e=t[0],r=t[1]/100,s=t[2]/100,a=Math.max(s,.01),n,c;c=(2-r)*s;let f=(2-r)*a;return n=r*a,n/=f<=1?f:2-f,n=n||0,c/=2,[e,n*100,c*100]};hr.hwb.rgb=function(t){let e=t[0]/360,r=t[1]/100,s=t[2]/100,a=r+s,n;a>1&&(r/=a,s/=a);let c=Math.floor(6*e),f=1-s;n=6*e-c,c&1&&(n=1-n);let p=r+n*(f-r),h,E,C;switch(c){default:case 6:case 0:h=f,E=p,C=r;break;case 1:h=p,E=f,C=r;break;case 2:h=r,E=f,C=p;break;case 3:h=r,E=p,C=f;break;case 4:h=p,E=r,C=f;break;case 5:h=f,E=r,C=p;break}return[h*255,E*255,C*255]};hr.cmyk.rgb=function(t){let e=t[0]/100,r=t[1]/100,s=t[2]/100,a=t[3]/100,n=1-Math.min(1,e*(1-a)+a),c=1-Math.min(1,r*(1-a)+a),f=1-Math.min(1,s*(1-a)+a);return[n*255,c*255,f*255]};hr.xyz.rgb=function(t){let e=t[0]/100,r=t[1]/100,s=t[2]/100,a,n,c;return a=e*3.2406+r*-1.5372+s*-.4986,n=e*-.9689+r*1.8758+s*.0415,c=e*.0557+r*-.204+s*1.057,a=a>.0031308?1.055*a**(1/2.4)-.055:a*12.92,n=n>.0031308?1.055*n**(1/2.4)-.055:n*12.92,c=c>.0031308?1.055*c**(1/2.4)-.055:c*12.92,a=Math.min(Math.max(0,a),1),n=Math.min(Math.max(0,n),1),c=Math.min(Math.max(0,c),1),[a*255,n*255,c*255]};hr.xyz.lab=function(t){let e=t[0],r=t[1],s=t[2];e/=95.047,r/=100,s/=108.883,e=e>.008856?e**(1/3):7.787*e+16/116,r=r>.008856?r**(1/3):7.787*r+16/116,s=s>.008856?s**(1/3):7.787*s+16/116;let a=116*r-16,n=500*(e-r),c=200*(r-s);return[a,n,c]};hr.lab.xyz=function(t){let e=t[0],r=t[1],s=t[2],a,n,c;n=(e+16)/116,a=r/500+n,c=n-s/200;let f=n**3,p=a**3,h=c**3;return n=f>.008856?f:(n-16/116)/7.787,a=p>.008856?p:(a-16/116)/7.787,c=h>.008856?h:(c-16/116)/7.787,a*=95.047,n*=100,c*=108.883,[a,n,c]};hr.lab.lch=function(t){let e=t[0],r=t[1],s=t[2],a;a=Math.atan2(s,r)*360/2/Math.PI,a<0&&(a+=360);let c=Math.sqrt(r*r+s*s);return[e,c,a]};hr.lch.lab=function(t){let e=t[0],r=t[1],a=t[2]/360*2*Math.PI,n=r*Math.cos(a),c=r*Math.sin(a);return[e,n,c]};hr.rgb.ansi16=function(t,e=null){let[r,s,a]=t,n=e===null?hr.rgb.hsv(t)[2]:e;if(n=Math.round(n/50),n===0)return 30;let c=30+(Math.round(a/255)<<2|Math.round(s/255)<<1|Math.round(r/255));return n===2&&(c+=60),c};hr.hsv.ansi16=function(t){return hr.rgb.ansi16(hr.hsv.rgb(t),t[2])};hr.rgb.ansi256=function(t){let e=t[0],r=t[1],s=t[2];return e===r&&r===s?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(r/255*5)+Math.round(s/255*5)};hr.ansi16.rgb=function(t){let e=t%10;if(e===0||e===7)return t>50&&(e+=3.5),e=e/10.5*255,[e,e,e];let r=(~~(t>50)+1)*.5,s=(e&1)*r*255,a=(e>>1&1)*r*255,n=(e>>2&1)*r*255;return[s,a,n]};hr.ansi256.rgb=function(t){if(t>=232){let n=(t-232)*10+8;return[n,n,n]}t-=16;let e,r=Math.floor(t/36)/5*255,s=Math.floor((e=t%36)/6)/5*255,a=e%6/5*255;return[r,s,a]};hr.rgb.hex=function(t){let r=(((Math.round(t[0])&255)<<16)+((Math.round(t[1])&255)<<8)+(Math.round(t[2])&255)).toString(16).toUpperCase();return"000000".substring(r.length)+r};hr.hex.rgb=function(t){let e=t.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!e)return[0,0,0];let r=e[0];e[0].length===3&&(r=r.split("").map(f=>f+f).join(""));let s=parseInt(r,16),a=s>>16&255,n=s>>8&255,c=s&255;return[a,n,c]};hr.rgb.hcg=function(t){let e=t[0]/255,r=t[1]/255,s=t[2]/255,a=Math.max(Math.max(e,r),s),n=Math.min(Math.min(e,r),s),c=a-n,f,p;return c<1?f=n/(1-c):f=0,c<=0?p=0:a===e?p=(r-s)/c%6:a===r?p=2+(s-e)/c:p=4+(e-r)/c,p/=6,p%=1,[p*360,c*100,f*100]};hr.hsl.hcg=function(t){let e=t[1]/100,r=t[2]/100,s=r<.5?2*e*r:2*e*(1-r),a=0;return s<1&&(a=(r-.5*s)/(1-s)),[t[0],s*100,a*100]};hr.hsv.hcg=function(t){let e=t[1]/100,r=t[2]/100,s=e*r,a=0;return s<1&&(a=(r-s)/(1-s)),[t[0],s*100,a*100]};hr.hcg.rgb=function(t){let e=t[0]/360,r=t[1]/100,s=t[2]/100;if(r===0)return[s*255,s*255,s*255];let a=[0,0,0],n=e%1*6,c=n%1,f=1-c,p=0;switch(Math.floor(n)){case 0:a[0]=1,a[1]=c,a[2]=0;break;case 1:a[0]=f,a[1]=1,a[2]=0;break;case 2:a[0]=0,a[1]=1,a[2]=c;break;case 3:a[0]=0,a[1]=f,a[2]=1;break;case 4:a[0]=c,a[1]=0,a[2]=1;break;default:a[0]=1,a[1]=0,a[2]=f}return p=(1-r)*s,[(r*a[0]+p)*255,(r*a[1]+p)*255,(r*a[2]+p)*255]};hr.hcg.hsv=function(t){let e=t[1]/100,r=t[2]/100,s=e+r*(1-e),a=0;return s>0&&(a=e/s),[t[0],a*100,s*100]};hr.hcg.hsl=function(t){let e=t[1]/100,s=t[2]/100*(1-e)+.5*e,a=0;return s>0&&s<.5?a=e/(2*s):s>=.5&&s<1&&(a=e/(2*(1-s))),[t[0],a*100,s*100]};hr.hcg.hwb=function(t){let e=t[1]/100,r=t[2]/100,s=e+r*(1-e);return[t[0],(s-e)*100,(1-s)*100]};hr.hwb.hcg=function(t){let e=t[1]/100,s=1-t[2]/100,a=s-e,n=0;return a<1&&(n=(s-a)/(1-a)),[t[0],a*100,n*100]};hr.apple.rgb=function(t){return[t[0]/65535*255,t[1]/65535*255,t[2]/65535*255]};hr.rgb.apple=function(t){return[t[0]/255*65535,t[1]/255*65535,t[2]/255*65535]};hr.gray.rgb=function(t){return[t[0]/100*255,t[0]/100*255,t[0]/100*255]};hr.gray.hsl=function(t){return[0,0,t[0]]};hr.gray.hsv=hr.gray.hsl;hr.gray.hwb=function(t){return[0,100,t[0]]};hr.gray.cmyk=function(t){return[0,0,0,t[0]]};hr.gray.lab=function(t){return[t[0],0,0]};hr.gray.hex=function(t){let e=Math.round(t[0]/100*255)&255,s=((e<<16)+(e<<8)+e).toString(16).toUpperCase();return"000000".substring(s.length)+s};hr.rgb.gray=function(t){return[(t[0]+t[1]+t[2])/3/255*100]}});var Zse=L((GWt,zse)=>{var fk=ZU();function ntt(){let t={},e=Object.keys(fk);for(let r=e.length,s=0;s{var XU=ZU(),att=Zse(),DE={},ltt=Object.keys(XU);function ctt(t){let e=function(...r){let s=r[0];return s==null?s:(s.length>1&&(r=s),t(r))};return"conversion"in t&&(e.conversion=t.conversion),e}function utt(t){let e=function(...r){let s=r[0];if(s==null)return s;s.length>1&&(r=s);let a=t(r);if(typeof a=="object")for(let n=a.length,c=0;c{DE[t]={},Object.defineProperty(DE[t],"channels",{value:XU[t].channels}),Object.defineProperty(DE[t],"labels",{value:XU[t].labels});let e=att(t);Object.keys(e).forEach(s=>{let a=e[s];DE[t][s]=utt(a),DE[t][s].raw=ctt(a)})});Xse.exports=DE});var pk=L((YWt,ioe)=>{"use strict";var eoe=(t,e)=>(...r)=>`\x1B[${t(...r)+e}m`,toe=(t,e)=>(...r)=>{let s=t(...r);return`\x1B[${38+e};5;${s}m`},roe=(t,e)=>(...r)=>{let s=t(...r);return`\x1B[${38+e};2;${s[0]};${s[1]};${s[2]}m`},Ak=t=>t,noe=(t,e,r)=>[t,e,r],bE=(t,e,r)=>{Object.defineProperty(t,e,{get:()=>{let s=r();return Object.defineProperty(t,e,{value:s,enumerable:!0,configurable:!0}),s},enumerable:!0,configurable:!0})},$U,PE=(t,e,r,s)=>{$U===void 0&&($U=$se());let a=s?10:0,n={};for(let[c,f]of Object.entries($U)){let p=c==="ansi16"?"ansi":c;c===e?n[p]=t(r,a):typeof f=="object"&&(n[p]=t(f[e],a))}return n};function ftt(){let t=new Map,e={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};e.color.gray=e.color.blackBright,e.bgColor.bgGray=e.bgColor.bgBlackBright,e.color.grey=e.color.blackBright,e.bgColor.bgGrey=e.bgColor.bgBlackBright;for(let[r,s]of Object.entries(e)){for(let[a,n]of Object.entries(s))e[a]={open:`\x1B[${n[0]}m`,close:`\x1B[${n[1]}m`},s[a]=e[a],t.set(n[0],n[1]);Object.defineProperty(e,r,{value:s,enumerable:!1})}return Object.defineProperty(e,"codes",{value:t,enumerable:!1}),e.color.close="\x1B[39m",e.bgColor.close="\x1B[49m",bE(e.color,"ansi",()=>PE(eoe,"ansi16",Ak,!1)),bE(e.color,"ansi256",()=>PE(toe,"ansi256",Ak,!1)),bE(e.color,"ansi16m",()=>PE(roe,"rgb",noe,!1)),bE(e.bgColor,"ansi",()=>PE(eoe,"ansi16",Ak,!0)),bE(e.bgColor,"ansi256",()=>PE(toe,"ansi256",Ak,!0)),bE(e.bgColor,"ansi16m",()=>PE(roe,"rgb",noe,!0)),e}Object.defineProperty(ioe,"exports",{enumerable:!0,get:ftt})});var ooe=L((VWt,soe)=>{"use strict";soe.exports=(t,e=process.argv)=>{let r=t.startsWith("-")?"":t.length===1?"-":"--",s=e.indexOf(r+t),a=e.indexOf("--");return s!==-1&&(a===-1||s{"use strict";var Att=Ie("os"),aoe=Ie("tty"),Dc=ooe(),{env:xs}=process,u0;Dc("no-color")||Dc("no-colors")||Dc("color=false")||Dc("color=never")?u0=0:(Dc("color")||Dc("colors")||Dc("color=true")||Dc("color=always"))&&(u0=1);"FORCE_COLOR"in xs&&(xs.FORCE_COLOR==="true"?u0=1:xs.FORCE_COLOR==="false"?u0=0:u0=xs.FORCE_COLOR.length===0?1:Math.min(parseInt(xs.FORCE_COLOR,10),3));function e4(t){return t===0?!1:{level:t,hasBasic:!0,has256:t>=2,has16m:t>=3}}function t4(t,e){if(u0===0)return 0;if(Dc("color=16m")||Dc("color=full")||Dc("color=truecolor"))return 3;if(Dc("color=256"))return 2;if(t&&!e&&u0===void 0)return 0;let r=u0||0;if(xs.TERM==="dumb")return r;if(process.platform==="win32"){let s=Att.release().split(".");return Number(s[0])>=10&&Number(s[2])>=10586?Number(s[2])>=14931?3:2:1}if("CI"in xs)return["TRAVIS","CIRCLECI","APPVEYOR","GITLAB_CI"].some(s=>s in xs)||xs.CI_NAME==="codeship"?1:r;if("TEAMCITY_VERSION"in xs)return/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(xs.TEAMCITY_VERSION)?1:0;if("GITHUB_ACTIONS"in xs)return 1;if(xs.COLORTERM==="truecolor")return 3;if("TERM_PROGRAM"in xs){let s=parseInt((xs.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(xs.TERM_PROGRAM){case"iTerm.app":return s>=3?3:2;case"Apple_Terminal":return 2}}return/-256(color)?$/i.test(xs.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(xs.TERM)||"COLORTERM"in xs?1:r}function ptt(t){let e=t4(t,t&&t.isTTY);return e4(e)}loe.exports={supportsColor:ptt,stdout:e4(t4(!0,aoe.isatty(1))),stderr:e4(t4(!0,aoe.isatty(2)))}});var foe=L((JWt,uoe)=>{"use strict";var htt=(t,e,r)=>{let s=t.indexOf(e);if(s===-1)return t;let a=e.length,n=0,c="";do c+=t.substr(n,s-n)+e+r,n=s+a,s=t.indexOf(e,n);while(s!==-1);return c+=t.substr(n),c},gtt=(t,e,r,s)=>{let a=0,n="";do{let c=t[s-1]==="\r";n+=t.substr(a,(c?s-1:s)-a)+e+(c?`\r `:` `)+r,a=s+1,s=t.indexOf(` `,a)}while(s!==-1);return n+=t.substr(a),n};uoe.exports={stringReplaceAll:htt,stringEncaseCRLFWithFirstIndex:gtt}});var doe=L((zWt,goe)=>{"use strict";var dtt=/(?:\\(u(?:[a-f\d]{4}|\{[a-f\d]{1,6}\})|x[a-f\d]{2}|.))|(?:\{(~)?(\w+(?:\([^)]*\))?(?:\.\w+(?:\([^)]*\))?)*)(?:[ \t]|(?=\r?\n)))|(\})|((?:.|[\r\n\f])+?)/gi,Aoe=/(?:^|\.)(\w+)(?:\(([^)]*)\))?/g,mtt=/^(['"])((?:\\.|(?!\1)[^\\])*)\1$/,ytt=/\\(u(?:[a-f\d]{4}|{[a-f\d]{1,6}})|x[a-f\d]{2}|.)|([^\\])/gi,Ett=new Map([["n",` `],["r","\r"],["t"," "],["b","\b"],["f","\f"],["v","\v"],["0","\0"],["\\","\\"],["e","\x1B"],["a","\x07"]]);function hoe(t){let e=t[0]==="u",r=t[1]==="{";return e&&!r&&t.length===5||t[0]==="x"&&t.length===3?String.fromCharCode(parseInt(t.slice(1),16)):e&&r?String.fromCodePoint(parseInt(t.slice(2,-1),16)):Ett.get(t)||t}function Itt(t,e){let r=[],s=e.trim().split(/\s*,\s*/g),a;for(let n of s){let c=Number(n);if(!Number.isNaN(c))r.push(c);else if(a=n.match(mtt))r.push(a[2].replace(ytt,(f,p,h)=>p?hoe(p):h));else throw new Error(`Invalid Chalk template style argument: ${n} (in style '${t}')`)}return r}function Ctt(t){Aoe.lastIndex=0;let e=[],r;for(;(r=Aoe.exec(t))!==null;){let s=r[1];if(r[2]){let a=Itt(s,r[2]);e.push([s].concat(a))}else e.push([s])}return e}function poe(t,e){let r={};for(let a of e)for(let n of a.styles)r[n[0]]=a.inverse?null:n.slice(1);let s=t;for(let[a,n]of Object.entries(r))if(Array.isArray(n)){if(!(a in s))throw new Error(`Unknown Chalk style: ${a}`);s=n.length>0?s[a](...n):s[a]}return s}goe.exports=(t,e)=>{let r=[],s=[],a=[];if(e.replace(dtt,(n,c,f,p,h,E)=>{if(c)a.push(hoe(c));else if(p){let C=a.join("");a=[],s.push(r.length===0?C:poe(t,r)(C)),r.push({inverse:f,styles:Ctt(p)})}else if(h){if(r.length===0)throw new Error("Found extraneous } in Chalk template literal");s.push(poe(t,r)(a.join(""))),a=[],r.pop()}else a.push(E)}),s.push(a.join("")),r.length>0){let n=`Chalk template literal is missing ${r.length} closing bracket${r.length===1?"":"s"} (\`}\`)`;throw new Error(n)}return s.join("")}});var kE=L((ZWt,woe)=>{"use strict";var mB=pk(),{stdout:n4,stderr:i4}=coe(),{stringReplaceAll:wtt,stringEncaseCRLFWithFirstIndex:Btt}=foe(),{isArray:hk}=Array,yoe=["ansi","ansi","ansi256","ansi16m"],xE=Object.create(null),vtt=(t,e={})=>{if(e.level&&!(Number.isInteger(e.level)&&e.level>=0&&e.level<=3))throw new Error("The `level` option should be an integer from 0 to 3");let r=n4?n4.level:0;t.level=e.level===void 0?r:e.level},s4=class{constructor(e){return Eoe(e)}},Eoe=t=>{let e={};return vtt(e,t),e.template=(...r)=>Coe(e.template,...r),Object.setPrototypeOf(e,gk.prototype),Object.setPrototypeOf(e.template,e),e.template.constructor=()=>{throw new Error("`chalk.constructor()` is deprecated. Use `new chalk.Instance()` instead.")},e.template.Instance=s4,e.template};function gk(t){return Eoe(t)}for(let[t,e]of Object.entries(mB))xE[t]={get(){let r=dk(this,o4(e.open,e.close,this._styler),this._isEmpty);return Object.defineProperty(this,t,{value:r}),r}};xE.visible={get(){let t=dk(this,this._styler,!0);return Object.defineProperty(this,"visible",{value:t}),t}};var Ioe=["rgb","hex","keyword","hsl","hsv","hwb","ansi","ansi256"];for(let t of Ioe)xE[t]={get(){let{level:e}=this;return function(...r){let s=o4(mB.color[yoe[e]][t](...r),mB.color.close,this._styler);return dk(this,s,this._isEmpty)}}};for(let t of Ioe){let e="bg"+t[0].toUpperCase()+t.slice(1);xE[e]={get(){let{level:r}=this;return function(...s){let a=o4(mB.bgColor[yoe[r]][t](...s),mB.bgColor.close,this._styler);return dk(this,a,this._isEmpty)}}}}var Stt=Object.defineProperties(()=>{},{...xE,level:{enumerable:!0,get(){return this._generator.level},set(t){this._generator.level=t}}}),o4=(t,e,r)=>{let s,a;return r===void 0?(s=t,a=e):(s=r.openAll+t,a=e+r.closeAll),{open:t,close:e,openAll:s,closeAll:a,parent:r}},dk=(t,e,r)=>{let s=(...a)=>hk(a[0])&&hk(a[0].raw)?moe(s,Coe(s,...a)):moe(s,a.length===1?""+a[0]:a.join(" "));return Object.setPrototypeOf(s,Stt),s._generator=t,s._styler=e,s._isEmpty=r,s},moe=(t,e)=>{if(t.level<=0||!e)return t._isEmpty?"":e;let r=t._styler;if(r===void 0)return e;let{openAll:s,closeAll:a}=r;if(e.indexOf("\x1B")!==-1)for(;r!==void 0;)e=wtt(e,r.close,r.open),r=r.parent;let n=e.indexOf(` `);return n!==-1&&(e=Btt(e,a,s,n)),s+e+a},r4,Coe=(t,...e)=>{let[r]=e;if(!hk(r)||!hk(r.raw))return e.join(" ");let s=e.slice(1),a=[r.raw[0]];for(let n=1;n{"use strict";bc.isInteger=t=>typeof t=="number"?Number.isInteger(t):typeof t=="string"&&t.trim()!==""?Number.isInteger(Number(t)):!1;bc.find=(t,e)=>t.nodes.find(r=>r.type===e);bc.exceedsLimit=(t,e,r=1,s)=>s===!1||!bc.isInteger(t)||!bc.isInteger(e)?!1:(Number(e)-Number(t))/Number(r)>=s;bc.escapeNode=(t,e=0,r)=>{let s=t.nodes[e];s&&(r&&s.type===r||s.type==="open"||s.type==="close")&&s.escaped!==!0&&(s.value="\\"+s.value,s.escaped=!0)};bc.encloseBrace=t=>t.type!=="brace"||t.commas>>0+t.ranges>>0?!1:(t.invalid=!0,!0);bc.isInvalidBrace=t=>t.type!=="brace"?!1:t.invalid===!0||t.dollar?!0:!(t.commas>>0+t.ranges>>0)||t.open!==!0||t.close!==!0?(t.invalid=!0,!0):!1;bc.isOpenOrClose=t=>t.type==="open"||t.type==="close"?!0:t.open===!0||t.close===!0;bc.reduce=t=>t.reduce((e,r)=>(r.type==="text"&&e.push(r.value),r.type==="range"&&(r.type="text"),e),[]);bc.flatten=(...t)=>{let e=[],r=s=>{for(let a=0;a{"use strict";var Boe=yk();voe.exports=(t,e={})=>{let r=(s,a={})=>{let n=e.escapeInvalid&&Boe.isInvalidBrace(a),c=s.invalid===!0&&e.escapeInvalid===!0,f="";if(s.value)return(n||c)&&Boe.isOpenOrClose(s)?"\\"+s.value:s.value;if(s.value)return s.value;if(s.nodes)for(let p of s.nodes)f+=r(p);return f};return r(t)}});var Doe=L((eYt,Soe)=>{"use strict";Soe.exports=function(t){return typeof t=="number"?t-t===0:typeof t=="string"&&t.trim()!==""?Number.isFinite?Number.isFinite(+t):isFinite(+t):!1}});var Noe=L((tYt,Foe)=>{"use strict";var boe=Doe(),jd=(t,e,r)=>{if(boe(t)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(e===void 0||t===e)return String(t);if(boe(e)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let s={relaxZeros:!0,...r};typeof s.strictZeros=="boolean"&&(s.relaxZeros=s.strictZeros===!1);let a=String(s.relaxZeros),n=String(s.shorthand),c=String(s.capture),f=String(s.wrap),p=t+":"+e+"="+a+n+c+f;if(jd.cache.hasOwnProperty(p))return jd.cache[p].result;let h=Math.min(t,e),E=Math.max(t,e);if(Math.abs(h-E)===1){let R=t+"|"+e;return s.capture?`(${R})`:s.wrap===!1?R:`(?:${R})`}let C=Roe(t)||Roe(e),S={min:t,max:e,a:h,b:E},P=[],I=[];if(C&&(S.isPadded=C,S.maxLen=String(S.max).length),h<0){let R=E<0?Math.abs(E):1;I=Poe(R,Math.abs(h),S,s),h=S.a=0}return E>=0&&(P=Poe(h,E,S,s)),S.negatives=I,S.positives=P,S.result=Dtt(I,P,s),s.capture===!0?S.result=`(${S.result})`:s.wrap!==!1&&P.length+I.length>1&&(S.result=`(?:${S.result})`),jd.cache[p]=S,S.result};function Dtt(t,e,r){let s=a4(t,e,"-",!1,r)||[],a=a4(e,t,"",!1,r)||[],n=a4(t,e,"-?",!0,r)||[];return s.concat(n).concat(a).join("|")}function btt(t,e){let r=1,s=1,a=koe(t,r),n=new Set([e]);for(;t<=a&&a<=e;)n.add(a),r+=1,a=koe(t,r);for(a=Qoe(e+1,s)-1;t1&&f.count.pop(),f.count.push(E.count[0]),f.string=f.pattern+Toe(f.count),c=h+1;continue}r.isPadded&&(C=Ttt(h,r,s)),E.string=C+E.pattern+Toe(E.count),n.push(E),c=h+1,f=E}return n}function a4(t,e,r,s,a){let n=[];for(let c of t){let{string:f}=c;!s&&!xoe(e,"string",f)&&n.push(r+f),s&&xoe(e,"string",f)&&n.push(r+f)}return n}function xtt(t,e){let r=[];for(let s=0;se?1:e>t?-1:0}function xoe(t,e,r){return t.some(s=>s[e]===r)}function koe(t,e){return Number(String(t).slice(0,-e)+"9".repeat(e))}function Qoe(t,e){return t-t%Math.pow(10,e)}function Toe(t){let[e=0,r=""]=t;return r||e>1?`{${e+(r?","+r:"")}}`:""}function Qtt(t,e,r){return`[${t}${e-t===1?"":"-"}${e}]`}function Roe(t){return/^-?(0+)\d/.test(t)}function Ttt(t,e,r){if(!e.isPadded)return t;let s=Math.abs(e.maxLen-String(t).length),a=r.relaxZeros!==!1;switch(s){case 0:return"";case 1:return a?"0?":"0";case 2:return a?"0{0,2}":"00";default:return a?`0{0,${s}}`:`0{${s}}`}}jd.cache={};jd.clearCache=()=>jd.cache={};Foe.exports=jd});var u4=L((rYt,qoe)=>{"use strict";var Rtt=Ie("util"),Moe=Noe(),Ooe=t=>t!==null&&typeof t=="object"&&!Array.isArray(t),Ftt=t=>e=>t===!0?Number(e):String(e),l4=t=>typeof t=="number"||typeof t=="string"&&t!=="",yB=t=>Number.isInteger(+t),c4=t=>{let e=`${t}`,r=-1;if(e[0]==="-"&&(e=e.slice(1)),e==="0")return!1;for(;e[++r]==="0";);return r>0},Ntt=(t,e,r)=>typeof t=="string"||typeof e=="string"?!0:r.stringify===!0,Ott=(t,e,r)=>{if(e>0){let s=t[0]==="-"?"-":"";s&&(t=t.slice(1)),t=s+t.padStart(s?e-1:e,"0")}return r===!1?String(t):t},Loe=(t,e)=>{let r=t[0]==="-"?"-":"";for(r&&(t=t.slice(1),e--);t.length{t.negatives.sort((c,f)=>cf?1:0),t.positives.sort((c,f)=>cf?1:0);let r=e.capture?"":"?:",s="",a="",n;return t.positives.length&&(s=t.positives.join("|")),t.negatives.length&&(a=`-(${r}${t.negatives.join("|")})`),s&&a?n=`${s}|${a}`:n=s||a,e.wrap?`(${r}${n})`:n},_oe=(t,e,r,s)=>{if(r)return Moe(t,e,{wrap:!1,...s});let a=String.fromCharCode(t);if(t===e)return a;let n=String.fromCharCode(e);return`[${a}-${n}]`},Uoe=(t,e,r)=>{if(Array.isArray(t)){let s=r.wrap===!0,a=r.capture?"":"?:";return s?`(${a}${t.join("|")})`:t.join("|")}return Moe(t,e,r)},Hoe=(...t)=>new RangeError("Invalid range arguments: "+Rtt.inspect(...t)),joe=(t,e,r)=>{if(r.strictRanges===!0)throw Hoe([t,e]);return[]},Mtt=(t,e)=>{if(e.strictRanges===!0)throw new TypeError(`Expected step "${t}" to be a number`);return[]},_tt=(t,e,r=1,s={})=>{let a=Number(t),n=Number(e);if(!Number.isInteger(a)||!Number.isInteger(n)){if(s.strictRanges===!0)throw Hoe([t,e]);return[]}a===0&&(a=0),n===0&&(n=0);let c=a>n,f=String(t),p=String(e),h=String(r);r=Math.max(Math.abs(r),1);let E=c4(f)||c4(p)||c4(h),C=E?Math.max(f.length,p.length,h.length):0,S=E===!1&&Ntt(t,e,s)===!1,P=s.transform||Ftt(S);if(s.toRegex&&r===1)return _oe(Loe(t,C),Loe(e,C),!0,s);let I={negatives:[],positives:[]},R=W=>I[W<0?"negatives":"positives"].push(Math.abs(W)),N=[],U=0;for(;c?a>=n:a<=n;)s.toRegex===!0&&r>1?R(a):N.push(Ott(P(a,U),C,S)),a=c?a-r:a+r,U++;return s.toRegex===!0?r>1?Ltt(I,s):Uoe(N,null,{wrap:!1,...s}):N},Utt=(t,e,r=1,s={})=>{if(!yB(t)&&t.length>1||!yB(e)&&e.length>1)return joe(t,e,s);let a=s.transform||(S=>String.fromCharCode(S)),n=`${t}`.charCodeAt(0),c=`${e}`.charCodeAt(0),f=n>c,p=Math.min(n,c),h=Math.max(n,c);if(s.toRegex&&r===1)return _oe(p,h,!1,s);let E=[],C=0;for(;f?n>=c:n<=c;)E.push(a(n,C)),n=f?n-r:n+r,C++;return s.toRegex===!0?Uoe(E,null,{wrap:!1,options:s}):E},Ik=(t,e,r,s={})=>{if(e==null&&l4(t))return[t];if(!l4(t)||!l4(e))return joe(t,e,s);if(typeof r=="function")return Ik(t,e,1,{transform:r});if(Ooe(r))return Ik(t,e,0,r);let a={...s};return a.capture===!0&&(a.wrap=!0),r=r||a.step||1,yB(r)?yB(t)&&yB(e)?_tt(t,e,r,a):Utt(t,e,Math.max(Math.abs(r),1),a):r!=null&&!Ooe(r)?Mtt(r,a):Ik(t,e,1,r)};qoe.exports=Ik});var Yoe=L((nYt,Woe)=>{"use strict";var Htt=u4(),Goe=yk(),jtt=(t,e={})=>{let r=(s,a={})=>{let n=Goe.isInvalidBrace(a),c=s.invalid===!0&&e.escapeInvalid===!0,f=n===!0||c===!0,p=e.escapeInvalid===!0?"\\":"",h="";if(s.isOpen===!0||s.isClose===!0)return p+s.value;if(s.type==="open")return f?p+s.value:"(";if(s.type==="close")return f?p+s.value:")";if(s.type==="comma")return s.prev.type==="comma"?"":f?s.value:"|";if(s.value)return s.value;if(s.nodes&&s.ranges>0){let E=Goe.reduce(s.nodes),C=Htt(...E,{...e,wrap:!1,toRegex:!0});if(C.length!==0)return E.length>1&&C.length>1?`(${C})`:C}if(s.nodes)for(let E of s.nodes)h+=r(E,s);return h};return r(t)};Woe.exports=jtt});var Joe=L((iYt,Koe)=>{"use strict";var qtt=u4(),Voe=Ek(),QE=yk(),qd=(t="",e="",r=!1)=>{let s=[];if(t=[].concat(t),e=[].concat(e),!e.length)return t;if(!t.length)return r?QE.flatten(e).map(a=>`{${a}}`):e;for(let a of t)if(Array.isArray(a))for(let n of a)s.push(qd(n,e,r));else for(let n of e)r===!0&&typeof n=="string"&&(n=`{${n}}`),s.push(Array.isArray(n)?qd(a,n,r):a+n);return QE.flatten(s)},Gtt=(t,e={})=>{let r=e.rangeLimit===void 0?1e3:e.rangeLimit,s=(a,n={})=>{a.queue=[];let c=n,f=n.queue;for(;c.type!=="brace"&&c.type!=="root"&&c.parent;)c=c.parent,f=c.queue;if(a.invalid||a.dollar){f.push(qd(f.pop(),Voe(a,e)));return}if(a.type==="brace"&&a.invalid!==!0&&a.nodes.length===2){f.push(qd(f.pop(),["{}"]));return}if(a.nodes&&a.ranges>0){let C=QE.reduce(a.nodes);if(QE.exceedsLimit(...C,e.step,r))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let S=qtt(...C,e);S.length===0&&(S=Voe(a,e)),f.push(qd(f.pop(),S)),a.nodes=[];return}let p=QE.encloseBrace(a),h=a.queue,E=a;for(;E.type!=="brace"&&E.type!=="root"&&E.parent;)E=E.parent,h=E.queue;for(let C=0;C{"use strict";zoe.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` `,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var rae=L((oYt,tae)=>{"use strict";var Wtt=Ek(),{MAX_LENGTH:Xoe,CHAR_BACKSLASH:f4,CHAR_BACKTICK:Ytt,CHAR_COMMA:Vtt,CHAR_DOT:Ktt,CHAR_LEFT_PARENTHESES:Jtt,CHAR_RIGHT_PARENTHESES:ztt,CHAR_LEFT_CURLY_BRACE:Ztt,CHAR_RIGHT_CURLY_BRACE:Xtt,CHAR_LEFT_SQUARE_BRACKET:$oe,CHAR_RIGHT_SQUARE_BRACKET:eae,CHAR_DOUBLE_QUOTE:$tt,CHAR_SINGLE_QUOTE:ert,CHAR_NO_BREAK_SPACE:trt,CHAR_ZERO_WIDTH_NOBREAK_SPACE:rrt}=Zoe(),nrt=(t,e={})=>{if(typeof t!="string")throw new TypeError("Expected a string");let r=e||{},s=typeof r.maxLength=="number"?Math.min(Xoe,r.maxLength):Xoe;if(t.length>s)throw new SyntaxError(`Input length (${t.length}), exceeds max characters (${s})`);let a={type:"root",input:t,nodes:[]},n=[a],c=a,f=a,p=0,h=t.length,E=0,C=0,S,P={},I=()=>t[E++],R=N=>{if(N.type==="text"&&f.type==="dot"&&(f.type="text"),f&&f.type==="text"&&N.type==="text"){f.value+=N.value;return}return c.nodes.push(N),N.parent=c,N.prev=f,f=N,N};for(R({type:"bos"});E0){if(c.ranges>0){c.ranges=0;let N=c.nodes.shift();c.nodes=[N,{type:"text",value:Wtt(c)}]}R({type:"comma",value:S}),c.commas++;continue}if(S===Ktt&&C>0&&c.commas===0){let N=c.nodes;if(C===0||N.length===0){R({type:"text",value:S});continue}if(f.type==="dot"){if(c.range=[],f.value+=S,f.type="range",c.nodes.length!==3&&c.nodes.length!==5){c.invalid=!0,c.ranges=0,f.type="text";continue}c.ranges++,c.args=[];continue}if(f.type==="range"){N.pop();let U=N[N.length-1];U.value+=f.value+S,f=U,c.ranges--;continue}R({type:"dot",value:S});continue}R({type:"text",value:S})}do if(c=n.pop(),c.type!=="root"){c.nodes.forEach(W=>{W.nodes||(W.type==="open"&&(W.isOpen=!0),W.type==="close"&&(W.isClose=!0),W.nodes||(W.type="text"),W.invalid=!0)});let N=n[n.length-1],U=N.nodes.indexOf(c);N.nodes.splice(U,1,...c.nodes)}while(n.length>0);return R({type:"eos"}),a};tae.exports=nrt});var sae=L((aYt,iae)=>{"use strict";var nae=Ek(),irt=Yoe(),srt=Joe(),ort=rae(),ql=(t,e={})=>{let r=[];if(Array.isArray(t))for(let s of t){let a=ql.create(s,e);Array.isArray(a)?r.push(...a):r.push(a)}else r=[].concat(ql.create(t,e));return e&&e.expand===!0&&e.nodupes===!0&&(r=[...new Set(r)]),r};ql.parse=(t,e={})=>ort(t,e);ql.stringify=(t,e={})=>nae(typeof t=="string"?ql.parse(t,e):t,e);ql.compile=(t,e={})=>(typeof t=="string"&&(t=ql.parse(t,e)),irt(t,e));ql.expand=(t,e={})=>{typeof t=="string"&&(t=ql.parse(t,e));let r=srt(t,e);return e.noempty===!0&&(r=r.filter(Boolean)),e.nodupes===!0&&(r=[...new Set(r)]),r};ql.create=(t,e={})=>t===""||t.length<3?[t]:e.expand!==!0?ql.compile(t,e):ql.expand(t,e);iae.exports=ql});var EB=L((lYt,uae)=>{"use strict";var art=Ie("path"),Kf="\\\\/",oae=`[^${Kf}]`,Pp="\\.",lrt="\\+",crt="\\?",Ck="\\/",urt="(?=.)",aae="[^/]",A4=`(?:${Ck}|$)`,lae=`(?:^|${Ck})`,p4=`${Pp}{1,2}${A4}`,frt=`(?!${Pp})`,Art=`(?!${lae}${p4})`,prt=`(?!${Pp}{0,1}${A4})`,hrt=`(?!${p4})`,grt=`[^.${Ck}]`,drt=`${aae}*?`,cae={DOT_LITERAL:Pp,PLUS_LITERAL:lrt,QMARK_LITERAL:crt,SLASH_LITERAL:Ck,ONE_CHAR:urt,QMARK:aae,END_ANCHOR:A4,DOTS_SLASH:p4,NO_DOT:frt,NO_DOTS:Art,NO_DOT_SLASH:prt,NO_DOTS_SLASH:hrt,QMARK_NO_DOT:grt,STAR:drt,START_ANCHOR:lae},mrt={...cae,SLASH_LITERAL:`[${Kf}]`,QMARK:oae,STAR:`${oae}*?`,DOTS_SLASH:`${Pp}{1,2}(?:[${Kf}]|$)`,NO_DOT:`(?!${Pp})`,NO_DOTS:`(?!(?:^|[${Kf}])${Pp}{1,2}(?:[${Kf}]|$))`,NO_DOT_SLASH:`(?!${Pp}{0,1}(?:[${Kf}]|$))`,NO_DOTS_SLASH:`(?!${Pp}{1,2}(?:[${Kf}]|$))`,QMARK_NO_DOT:`[^.${Kf}]`,START_ANCHOR:`(?:^|[${Kf}])`,END_ANCHOR:`(?:[${Kf}]|$)`},yrt={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};uae.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:yrt,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:art.sep,extglobChars(t){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${t.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(t){return t===!0?mrt:cae}}});var IB=L(al=>{"use strict";var Ert=Ie("path"),Irt=process.platform==="win32",{REGEX_BACKSLASH:Crt,REGEX_REMOVE_BACKSLASH:wrt,REGEX_SPECIAL_CHARS:Brt,REGEX_SPECIAL_CHARS_GLOBAL:vrt}=EB();al.isObject=t=>t!==null&&typeof t=="object"&&!Array.isArray(t);al.hasRegexChars=t=>Brt.test(t);al.isRegexChar=t=>t.length===1&&al.hasRegexChars(t);al.escapeRegex=t=>t.replace(vrt,"\\$1");al.toPosixSlashes=t=>t.replace(Crt,"/");al.removeBackslashes=t=>t.replace(wrt,e=>e==="\\"?"":e);al.supportsLookbehinds=()=>{let t=process.version.slice(1).split(".").map(Number);return t.length===3&&t[0]>=9||t[0]===8&&t[1]>=10};al.isWindows=t=>t&&typeof t.windows=="boolean"?t.windows:Irt===!0||Ert.sep==="\\";al.escapeLast=(t,e,r)=>{let s=t.lastIndexOf(e,r);return s===-1?t:t[s-1]==="\\"?al.escapeLast(t,e,s-1):`${t.slice(0,s)}\\${t.slice(s)}`};al.removePrefix=(t,e={})=>{let r=t;return r.startsWith("./")&&(r=r.slice(2),e.prefix="./"),r};al.wrapOutput=(t,e={},r={})=>{let s=r.contains?"":"^",a=r.contains?"":"$",n=`${s}(?:${t})${a}`;return e.negated===!0&&(n=`(?:^(?!${n}).*$)`),n}});var yae=L((uYt,mae)=>{"use strict";var fae=IB(),{CHAR_ASTERISK:h4,CHAR_AT:Srt,CHAR_BACKWARD_SLASH:CB,CHAR_COMMA:Drt,CHAR_DOT:g4,CHAR_EXCLAMATION_MARK:d4,CHAR_FORWARD_SLASH:dae,CHAR_LEFT_CURLY_BRACE:m4,CHAR_LEFT_PARENTHESES:y4,CHAR_LEFT_SQUARE_BRACKET:brt,CHAR_PLUS:Prt,CHAR_QUESTION_MARK:Aae,CHAR_RIGHT_CURLY_BRACE:xrt,CHAR_RIGHT_PARENTHESES:pae,CHAR_RIGHT_SQUARE_BRACKET:krt}=EB(),hae=t=>t===dae||t===CB,gae=t=>{t.isPrefix!==!0&&(t.depth=t.isGlobstar?1/0:1)},Qrt=(t,e)=>{let r=e||{},s=t.length-1,a=r.parts===!0||r.scanToEnd===!0,n=[],c=[],f=[],p=t,h=-1,E=0,C=0,S=!1,P=!1,I=!1,R=!1,N=!1,U=!1,W=!1,te=!1,ie=!1,Ae=!1,ce=0,me,pe,Be={value:"",depth:0,isGlob:!1},Ce=()=>h>=s,g=()=>p.charCodeAt(h+1),we=()=>(me=pe,p.charCodeAt(++h));for(;h0&&(fe=p.slice(0,E),p=p.slice(E),C-=E),ye&&I===!0&&C>0?(ye=p.slice(0,C),se=p.slice(C)):I===!0?(ye="",se=p):ye=p,ye&&ye!==""&&ye!=="/"&&ye!==p&&hae(ye.charCodeAt(ye.length-1))&&(ye=ye.slice(0,-1)),r.unescape===!0&&(se&&(se=fae.removeBackslashes(se)),ye&&W===!0&&(ye=fae.removeBackslashes(ye)));let X={prefix:fe,input:t,start:E,base:ye,glob:se,isBrace:S,isBracket:P,isGlob:I,isExtglob:R,isGlobstar:N,negated:te,negatedExtglob:ie};if(r.tokens===!0&&(X.maxDepth=0,hae(pe)||c.push(Be),X.tokens=c),r.parts===!0||r.tokens===!0){let De;for(let Re=0;Re{"use strict";var wk=EB(),Gl=IB(),{MAX_LENGTH:Bk,POSIX_REGEX_SOURCE:Trt,REGEX_NON_SPECIAL_CHARS:Rrt,REGEX_SPECIAL_CHARS_BACKREF:Frt,REPLACEMENTS:Eae}=wk,Nrt=(t,e)=>{if(typeof e.expandRange=="function")return e.expandRange(...t,e);t.sort();let r=`[${t.join("-")}]`;try{new RegExp(r)}catch{return t.map(a=>Gl.escapeRegex(a)).join("..")}return r},TE=(t,e)=>`Missing ${t}: "${e}" - use "\\\\${e}" to match literal characters`,E4=(t,e)=>{if(typeof t!="string")throw new TypeError("Expected a string");t=Eae[t]||t;let r={...e},s=typeof r.maxLength=="number"?Math.min(Bk,r.maxLength):Bk,a=t.length;if(a>s)throw new SyntaxError(`Input length: ${a}, exceeds maximum allowed length: ${s}`);let n={type:"bos",value:"",output:r.prepend||""},c=[n],f=r.capture?"":"?:",p=Gl.isWindows(e),h=wk.globChars(p),E=wk.extglobChars(h),{DOT_LITERAL:C,PLUS_LITERAL:S,SLASH_LITERAL:P,ONE_CHAR:I,DOTS_SLASH:R,NO_DOT:N,NO_DOT_SLASH:U,NO_DOTS_SLASH:W,QMARK:te,QMARK_NO_DOT:ie,STAR:Ae,START_ANCHOR:ce}=h,me=x=>`(${f}(?:(?!${ce}${x.dot?R:C}).)*?)`,pe=r.dot?"":N,Be=r.dot?te:ie,Ce=r.bash===!0?me(r):Ae;r.capture&&(Ce=`(${Ce})`),typeof r.noext=="boolean"&&(r.noextglob=r.noext);let g={input:t,index:-1,start:0,dot:r.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:c};t=Gl.removePrefix(t,g),a=t.length;let we=[],ye=[],fe=[],se=n,X,De=()=>g.index===a-1,Re=g.peek=(x=1)=>t[g.index+x],dt=g.advance=()=>t[++g.index]||"",j=()=>t.slice(g.index+1),rt=(x="",w=0)=>{g.consumed+=x,g.index+=w},Fe=x=>{g.output+=x.output!=null?x.output:x.value,rt(x.value)},Ne=()=>{let x=1;for(;Re()==="!"&&(Re(2)!=="("||Re(3)==="?");)dt(),g.start++,x++;return x%2===0?!1:(g.negated=!0,g.start++,!0)},Pe=x=>{g[x]++,fe.push(x)},Ye=x=>{g[x]--,fe.pop()},ke=x=>{if(se.type==="globstar"){let w=g.braces>0&&(x.type==="comma"||x.type==="brace"),b=x.extglob===!0||we.length&&(x.type==="pipe"||x.type==="paren");x.type!=="slash"&&x.type!=="paren"&&!w&&!b&&(g.output=g.output.slice(0,-se.output.length),se.type="star",se.value="*",se.output=Ce,g.output+=se.output)}if(we.length&&x.type!=="paren"&&(we[we.length-1].inner+=x.value),(x.value||x.output)&&Fe(x),se&&se.type==="text"&&x.type==="text"){se.value+=x.value,se.output=(se.output||"")+x.value;return}x.prev=se,c.push(x),se=x},it=(x,w)=>{let b={...E[w],conditions:1,inner:""};b.prev=se,b.parens=g.parens,b.output=g.output;let y=(r.capture?"(":"")+b.open;Pe("parens"),ke({type:x,value:w,output:g.output?"":I}),ke({type:"paren",extglob:!0,value:dt(),output:y}),we.push(b)},_e=x=>{let w=x.close+(r.capture?")":""),b;if(x.type==="negate"){let y=Ce;if(x.inner&&x.inner.length>1&&x.inner.includes("/")&&(y=me(r)),(y!==Ce||De()||/^\)+$/.test(j()))&&(w=x.close=`)$))${y}`),x.inner.includes("*")&&(b=j())&&/^\.[^\\/.]+$/.test(b)){let F=E4(b,{...e,fastpaths:!1}).output;w=x.close=`)${F})${y})`}x.prev.type==="bos"&&(g.negatedExtglob=!0)}ke({type:"paren",extglob:!0,value:X,output:w}),Ye("parens")};if(r.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(t)){let x=!1,w=t.replace(Frt,(b,y,F,z,Z,$)=>z==="\\"?(x=!0,b):z==="?"?y?y+z+(Z?te.repeat(Z.length):""):$===0?Be+(Z?te.repeat(Z.length):""):te.repeat(F.length):z==="."?C.repeat(F.length):z==="*"?y?y+z+(Z?Ce:""):Ce:y?b:`\\${b}`);return x===!0&&(r.unescape===!0?w=w.replace(/\\/g,""):w=w.replace(/\\+/g,b=>b.length%2===0?"\\\\":b?"\\":"")),w===t&&r.contains===!0?(g.output=t,g):(g.output=Gl.wrapOutput(w,g,e),g)}for(;!De();){if(X=dt(),X==="\0")continue;if(X==="\\"){let b=Re();if(b==="/"&&r.bash!==!0||b==="."||b===";")continue;if(!b){X+="\\",ke({type:"text",value:X});continue}let y=/^\\+/.exec(j()),F=0;if(y&&y[0].length>2&&(F=y[0].length,g.index+=F,F%2!==0&&(X+="\\")),r.unescape===!0?X=dt():X+=dt(),g.brackets===0){ke({type:"text",value:X});continue}}if(g.brackets>0&&(X!=="]"||se.value==="["||se.value==="[^")){if(r.posix!==!1&&X===":"){let b=se.value.slice(1);if(b.includes("[")&&(se.posix=!0,b.includes(":"))){let y=se.value.lastIndexOf("["),F=se.value.slice(0,y),z=se.value.slice(y+2),Z=Trt[z];if(Z){se.value=F+Z,g.backtrack=!0,dt(),!n.output&&c.indexOf(se)===1&&(n.output=I);continue}}}(X==="["&&Re()!==":"||X==="-"&&Re()==="]")&&(X=`\\${X}`),X==="]"&&(se.value==="["||se.value==="[^")&&(X=`\\${X}`),r.posix===!0&&X==="!"&&se.value==="["&&(X="^"),se.value+=X,Fe({value:X});continue}if(g.quotes===1&&X!=='"'){X=Gl.escapeRegex(X),se.value+=X,Fe({value:X});continue}if(X==='"'){g.quotes=g.quotes===1?0:1,r.keepQuotes===!0&&ke({type:"text",value:X});continue}if(X==="("){Pe("parens"),ke({type:"paren",value:X});continue}if(X===")"){if(g.parens===0&&r.strictBrackets===!0)throw new SyntaxError(TE("opening","("));let b=we[we.length-1];if(b&&g.parens===b.parens+1){_e(we.pop());continue}ke({type:"paren",value:X,output:g.parens?")":"\\)"}),Ye("parens");continue}if(X==="["){if(r.nobracket===!0||!j().includes("]")){if(r.nobracket!==!0&&r.strictBrackets===!0)throw new SyntaxError(TE("closing","]"));X=`\\${X}`}else Pe("brackets");ke({type:"bracket",value:X});continue}if(X==="]"){if(r.nobracket===!0||se&&se.type==="bracket"&&se.value.length===1){ke({type:"text",value:X,output:`\\${X}`});continue}if(g.brackets===0){if(r.strictBrackets===!0)throw new SyntaxError(TE("opening","["));ke({type:"text",value:X,output:`\\${X}`});continue}Ye("brackets");let b=se.value.slice(1);if(se.posix!==!0&&b[0]==="^"&&!b.includes("/")&&(X=`/${X}`),se.value+=X,Fe({value:X}),r.literalBrackets===!1||Gl.hasRegexChars(b))continue;let y=Gl.escapeRegex(se.value);if(g.output=g.output.slice(0,-se.value.length),r.literalBrackets===!0){g.output+=y,se.value=y;continue}se.value=`(${f}${y}|${se.value})`,g.output+=se.value;continue}if(X==="{"&&r.nobrace!==!0){Pe("braces");let b={type:"brace",value:X,output:"(",outputIndex:g.output.length,tokensIndex:g.tokens.length};ye.push(b),ke(b);continue}if(X==="}"){let b=ye[ye.length-1];if(r.nobrace===!0||!b){ke({type:"text",value:X,output:X});continue}let y=")";if(b.dots===!0){let F=c.slice(),z=[];for(let Z=F.length-1;Z>=0&&(c.pop(),F[Z].type!=="brace");Z--)F[Z].type!=="dots"&&z.unshift(F[Z].value);y=Nrt(z,r),g.backtrack=!0}if(b.comma!==!0&&b.dots!==!0){let F=g.output.slice(0,b.outputIndex),z=g.tokens.slice(b.tokensIndex);b.value=b.output="\\{",X=y="\\}",g.output=F;for(let Z of z)g.output+=Z.output||Z.value}ke({type:"brace",value:X,output:y}),Ye("braces"),ye.pop();continue}if(X==="|"){we.length>0&&we[we.length-1].conditions++,ke({type:"text",value:X});continue}if(X===","){let b=X,y=ye[ye.length-1];y&&fe[fe.length-1]==="braces"&&(y.comma=!0,b="|"),ke({type:"comma",value:X,output:b});continue}if(X==="/"){if(se.type==="dot"&&g.index===g.start+1){g.start=g.index+1,g.consumed="",g.output="",c.pop(),se=n;continue}ke({type:"slash",value:X,output:P});continue}if(X==="."){if(g.braces>0&&se.type==="dot"){se.value==="."&&(se.output=C);let b=ye[ye.length-1];se.type="dots",se.output+=X,se.value+=X,b.dots=!0;continue}if(g.braces+g.parens===0&&se.type!=="bos"&&se.type!=="slash"){ke({type:"text",value:X,output:C});continue}ke({type:"dot",value:X,output:C});continue}if(X==="?"){if(!(se&&se.value==="(")&&r.noextglob!==!0&&Re()==="("&&Re(2)!=="?"){it("qmark",X);continue}if(se&&se.type==="paren"){let y=Re(),F=X;if(y==="<"&&!Gl.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(se.value==="("&&!/[!=<:]/.test(y)||y==="<"&&!/<([!=]|\w+>)/.test(j()))&&(F=`\\${X}`),ke({type:"text",value:X,output:F});continue}if(r.dot!==!0&&(se.type==="slash"||se.type==="bos")){ke({type:"qmark",value:X,output:ie});continue}ke({type:"qmark",value:X,output:te});continue}if(X==="!"){if(r.noextglob!==!0&&Re()==="("&&(Re(2)!=="?"||!/[!=<:]/.test(Re(3)))){it("negate",X);continue}if(r.nonegate!==!0&&g.index===0){Ne();continue}}if(X==="+"){if(r.noextglob!==!0&&Re()==="("&&Re(2)!=="?"){it("plus",X);continue}if(se&&se.value==="("||r.regex===!1){ke({type:"plus",value:X,output:S});continue}if(se&&(se.type==="bracket"||se.type==="paren"||se.type==="brace")||g.parens>0){ke({type:"plus",value:X});continue}ke({type:"plus",value:S});continue}if(X==="@"){if(r.noextglob!==!0&&Re()==="("&&Re(2)!=="?"){ke({type:"at",extglob:!0,value:X,output:""});continue}ke({type:"text",value:X});continue}if(X!=="*"){(X==="$"||X==="^")&&(X=`\\${X}`);let b=Rrt.exec(j());b&&(X+=b[0],g.index+=b[0].length),ke({type:"text",value:X});continue}if(se&&(se.type==="globstar"||se.star===!0)){se.type="star",se.star=!0,se.value+=X,se.output=Ce,g.backtrack=!0,g.globstar=!0,rt(X);continue}let x=j();if(r.noextglob!==!0&&/^\([^?]/.test(x)){it("star",X);continue}if(se.type==="star"){if(r.noglobstar===!0){rt(X);continue}let b=se.prev,y=b.prev,F=b.type==="slash"||b.type==="bos",z=y&&(y.type==="star"||y.type==="globstar");if(r.bash===!0&&(!F||x[0]&&x[0]!=="/")){ke({type:"star",value:X,output:""});continue}let Z=g.braces>0&&(b.type==="comma"||b.type==="brace"),$=we.length&&(b.type==="pipe"||b.type==="paren");if(!F&&b.type!=="paren"&&!Z&&!$){ke({type:"star",value:X,output:""});continue}for(;x.slice(0,3)==="/**";){let oe=t[g.index+4];if(oe&&oe!=="/")break;x=x.slice(3),rt("/**",3)}if(b.type==="bos"&&De()){se.type="globstar",se.value+=X,se.output=me(r),g.output=se.output,g.globstar=!0,rt(X);continue}if(b.type==="slash"&&b.prev.type!=="bos"&&!z&&De()){g.output=g.output.slice(0,-(b.output+se.output).length),b.output=`(?:${b.output}`,se.type="globstar",se.output=me(r)+(r.strictSlashes?")":"|$)"),se.value+=X,g.globstar=!0,g.output+=b.output+se.output,rt(X);continue}if(b.type==="slash"&&b.prev.type!=="bos"&&x[0]==="/"){let oe=x[1]!==void 0?"|$":"";g.output=g.output.slice(0,-(b.output+se.output).length),b.output=`(?:${b.output}`,se.type="globstar",se.output=`${me(r)}${P}|${P}${oe})`,se.value+=X,g.output+=b.output+se.output,g.globstar=!0,rt(X+dt()),ke({type:"slash",value:"/",output:""});continue}if(b.type==="bos"&&x[0]==="/"){se.type="globstar",se.value+=X,se.output=`(?:^|${P}|${me(r)}${P})`,g.output=se.output,g.globstar=!0,rt(X+dt()),ke({type:"slash",value:"/",output:""});continue}g.output=g.output.slice(0,-se.output.length),se.type="globstar",se.output=me(r),se.value+=X,g.output+=se.output,g.globstar=!0,rt(X);continue}let w={type:"star",value:X,output:Ce};if(r.bash===!0){w.output=".*?",(se.type==="bos"||se.type==="slash")&&(w.output=pe+w.output),ke(w);continue}if(se&&(se.type==="bracket"||se.type==="paren")&&r.regex===!0){w.output=X,ke(w);continue}(g.index===g.start||se.type==="slash"||se.type==="dot")&&(se.type==="dot"?(g.output+=U,se.output+=U):r.dot===!0?(g.output+=W,se.output+=W):(g.output+=pe,se.output+=pe),Re()!=="*"&&(g.output+=I,se.output+=I)),ke(w)}for(;g.brackets>0;){if(r.strictBrackets===!0)throw new SyntaxError(TE("closing","]"));g.output=Gl.escapeLast(g.output,"["),Ye("brackets")}for(;g.parens>0;){if(r.strictBrackets===!0)throw new SyntaxError(TE("closing",")"));g.output=Gl.escapeLast(g.output,"("),Ye("parens")}for(;g.braces>0;){if(r.strictBrackets===!0)throw new SyntaxError(TE("closing","}"));g.output=Gl.escapeLast(g.output,"{"),Ye("braces")}if(r.strictSlashes!==!0&&(se.type==="star"||se.type==="bracket")&&ke({type:"maybe_slash",value:"",output:`${P}?`}),g.backtrack===!0){g.output="";for(let x of g.tokens)g.output+=x.output!=null?x.output:x.value,x.suffix&&(g.output+=x.suffix)}return g};E4.fastpaths=(t,e)=>{let r={...e},s=typeof r.maxLength=="number"?Math.min(Bk,r.maxLength):Bk,a=t.length;if(a>s)throw new SyntaxError(`Input length: ${a}, exceeds maximum allowed length: ${s}`);t=Eae[t]||t;let n=Gl.isWindows(e),{DOT_LITERAL:c,SLASH_LITERAL:f,ONE_CHAR:p,DOTS_SLASH:h,NO_DOT:E,NO_DOTS:C,NO_DOTS_SLASH:S,STAR:P,START_ANCHOR:I}=wk.globChars(n),R=r.dot?C:E,N=r.dot?S:E,U=r.capture?"":"?:",W={negated:!1,prefix:""},te=r.bash===!0?".*?":P;r.capture&&(te=`(${te})`);let ie=pe=>pe.noglobstar===!0?te:`(${U}(?:(?!${I}${pe.dot?h:c}).)*?)`,Ae=pe=>{switch(pe){case"*":return`${R}${p}${te}`;case".*":return`${c}${p}${te}`;case"*.*":return`${R}${te}${c}${p}${te}`;case"*/*":return`${R}${te}${f}${p}${N}${te}`;case"**":return R+ie(r);case"**/*":return`(?:${R}${ie(r)}${f})?${N}${p}${te}`;case"**/*.*":return`(?:${R}${ie(r)}${f})?${N}${te}${c}${p}${te}`;case"**/.*":return`(?:${R}${ie(r)}${f})?${c}${p}${te}`;default:{let Be=/^(.*?)\.(\w+)$/.exec(pe);if(!Be)return;let Ce=Ae(Be[1]);return Ce?Ce+c+Be[2]:void 0}}},ce=Gl.removePrefix(t,W),me=Ae(ce);return me&&r.strictSlashes!==!0&&(me+=`${f}?`),me};Iae.exports=E4});var Bae=L((AYt,wae)=>{"use strict";var Ort=Ie("path"),Lrt=yae(),I4=Cae(),C4=IB(),Mrt=EB(),_rt=t=>t&&typeof t=="object"&&!Array.isArray(t),$i=(t,e,r=!1)=>{if(Array.isArray(t)){let E=t.map(S=>$i(S,e,r));return S=>{for(let P of E){let I=P(S);if(I)return I}return!1}}let s=_rt(t)&&t.tokens&&t.input;if(t===""||typeof t!="string"&&!s)throw new TypeError("Expected pattern to be a non-empty string");let a=e||{},n=C4.isWindows(e),c=s?$i.compileRe(t,e):$i.makeRe(t,e,!1,!0),f=c.state;delete c.state;let p=()=>!1;if(a.ignore){let E={...e,ignore:null,onMatch:null,onResult:null};p=$i(a.ignore,E,r)}let h=(E,C=!1)=>{let{isMatch:S,match:P,output:I}=$i.test(E,c,e,{glob:t,posix:n}),R={glob:t,state:f,regex:c,posix:n,input:E,output:I,match:P,isMatch:S};return typeof a.onResult=="function"&&a.onResult(R),S===!1?(R.isMatch=!1,C?R:!1):p(E)?(typeof a.onIgnore=="function"&&a.onIgnore(R),R.isMatch=!1,C?R:!1):(typeof a.onMatch=="function"&&a.onMatch(R),C?R:!0)};return r&&(h.state=f),h};$i.test=(t,e,r,{glob:s,posix:a}={})=>{if(typeof t!="string")throw new TypeError("Expected input to be a string");if(t==="")return{isMatch:!1,output:""};let n=r||{},c=n.format||(a?C4.toPosixSlashes:null),f=t===s,p=f&&c?c(t):t;return f===!1&&(p=c?c(t):t,f=p===s),(f===!1||n.capture===!0)&&(n.matchBase===!0||n.basename===!0?f=$i.matchBase(t,e,r,a):f=e.exec(p)),{isMatch:!!f,match:f,output:p}};$i.matchBase=(t,e,r,s=C4.isWindows(r))=>(e instanceof RegExp?e:$i.makeRe(e,r)).test(Ort.basename(t));$i.isMatch=(t,e,r)=>$i(e,r)(t);$i.parse=(t,e)=>Array.isArray(t)?t.map(r=>$i.parse(r,e)):I4(t,{...e,fastpaths:!1});$i.scan=(t,e)=>Lrt(t,e);$i.compileRe=(t,e,r=!1,s=!1)=>{if(r===!0)return t.output;let a=e||{},n=a.contains?"":"^",c=a.contains?"":"$",f=`${n}(?:${t.output})${c}`;t&&t.negated===!0&&(f=`^(?!${f}).*$`);let p=$i.toRegex(f,e);return s===!0&&(p.state=t),p};$i.makeRe=(t,e={},r=!1,s=!1)=>{if(!t||typeof t!="string")throw new TypeError("Expected a non-empty string");let a={negated:!1,fastpaths:!0};return e.fastpaths!==!1&&(t[0]==="."||t[0]==="*")&&(a.output=I4.fastpaths(t,e)),a.output||(a=I4(t,e)),$i.compileRe(a,e,r,s)};$i.toRegex=(t,e)=>{try{let r=e||{};return new RegExp(t,r.flags||(r.nocase?"i":""))}catch(r){if(e&&e.debug===!0)throw r;return/$^/}};$i.constants=Mrt;wae.exports=$i});var Sae=L((pYt,vae)=>{"use strict";vae.exports=Bae()});var Sa=L((hYt,xae)=>{"use strict";var bae=Ie("util"),Pae=sae(),Jf=Sae(),w4=IB(),Dae=t=>t===""||t==="./",Qi=(t,e,r)=>{e=[].concat(e),t=[].concat(t);let s=new Set,a=new Set,n=new Set,c=0,f=E=>{n.add(E.output),r&&r.onResult&&r.onResult(E)};for(let E=0;E!s.has(E));if(r&&h.length===0){if(r.failglob===!0)throw new Error(`No matches found for "${e.join(", ")}"`);if(r.nonull===!0||r.nullglob===!0)return r.unescape?e.map(E=>E.replace(/\\/g,"")):e}return h};Qi.match=Qi;Qi.matcher=(t,e)=>Jf(t,e);Qi.isMatch=(t,e,r)=>Jf(e,r)(t);Qi.any=Qi.isMatch;Qi.not=(t,e,r={})=>{e=[].concat(e).map(String);let s=new Set,a=[],n=f=>{r.onResult&&r.onResult(f),a.push(f.output)},c=new Set(Qi(t,e,{...r,onResult:n}));for(let f of a)c.has(f)||s.add(f);return[...s]};Qi.contains=(t,e,r)=>{if(typeof t!="string")throw new TypeError(`Expected a string: "${bae.inspect(t)}"`);if(Array.isArray(e))return e.some(s=>Qi.contains(t,s,r));if(typeof e=="string"){if(Dae(t)||Dae(e))return!1;if(t.includes(e)||t.startsWith("./")&&t.slice(2).includes(e))return!0}return Qi.isMatch(t,e,{...r,contains:!0})};Qi.matchKeys=(t,e,r)=>{if(!w4.isObject(t))throw new TypeError("Expected the first argument to be an object");let s=Qi(Object.keys(t),e,r),a={};for(let n of s)a[n]=t[n];return a};Qi.some=(t,e,r)=>{let s=[].concat(t);for(let a of[].concat(e)){let n=Jf(String(a),r);if(s.some(c=>n(c)))return!0}return!1};Qi.every=(t,e,r)=>{let s=[].concat(t);for(let a of[].concat(e)){let n=Jf(String(a),r);if(!s.every(c=>n(c)))return!1}return!0};Qi.all=(t,e,r)=>{if(typeof t!="string")throw new TypeError(`Expected a string: "${bae.inspect(t)}"`);return[].concat(e).every(s=>Jf(s,r)(t))};Qi.capture=(t,e,r)=>{let s=w4.isWindows(r),n=Jf.makeRe(String(t),{...r,capture:!0}).exec(s?w4.toPosixSlashes(e):e);if(n)return n.slice(1).map(c=>c===void 0?"":c)};Qi.makeRe=(...t)=>Jf.makeRe(...t);Qi.scan=(...t)=>Jf.scan(...t);Qi.parse=(t,e)=>{let r=[];for(let s of[].concat(t||[]))for(let a of Pae(String(s),e))r.push(Jf.parse(a,e));return r};Qi.braces=(t,e)=>{if(typeof t!="string")throw new TypeError("Expected a string");return e&&e.nobrace===!0||!/\{.*\}/.test(t)?[t]:Pae(t,e)};Qi.braceExpand=(t,e)=>{if(typeof t!="string")throw new TypeError("Expected a string");return Qi.braces(t,{...e,expand:!0})};xae.exports=Qi});var Qae=L((gYt,kae)=>{"use strict";kae.exports=({onlyFirst:t=!1}={})=>{let e=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|");return new RegExp(e,t?void 0:"g")}});var vk=L((dYt,Tae)=>{"use strict";var Urt=Qae();Tae.exports=t=>typeof t=="string"?t.replace(Urt(),""):t});var Fae=L((mYt,Rae)=>{function Hrt(){this.__data__=[],this.size=0}Rae.exports=Hrt});var RE=L((yYt,Nae)=>{function jrt(t,e){return t===e||t!==t&&e!==e}Nae.exports=jrt});var wB=L((EYt,Oae)=>{var qrt=RE();function Grt(t,e){for(var r=t.length;r--;)if(qrt(t[r][0],e))return r;return-1}Oae.exports=Grt});var Mae=L((IYt,Lae)=>{var Wrt=wB(),Yrt=Array.prototype,Vrt=Yrt.splice;function Krt(t){var e=this.__data__,r=Wrt(e,t);if(r<0)return!1;var s=e.length-1;return r==s?e.pop():Vrt.call(e,r,1),--this.size,!0}Lae.exports=Krt});var Uae=L((CYt,_ae)=>{var Jrt=wB();function zrt(t){var e=this.__data__,r=Jrt(e,t);return r<0?void 0:e[r][1]}_ae.exports=zrt});var jae=L((wYt,Hae)=>{var Zrt=wB();function Xrt(t){return Zrt(this.__data__,t)>-1}Hae.exports=Xrt});var Gae=L((BYt,qae)=>{var $rt=wB();function ent(t,e){var r=this.__data__,s=$rt(r,t);return s<0?(++this.size,r.push([t,e])):r[s][1]=e,this}qae.exports=ent});var BB=L((vYt,Wae)=>{var tnt=Fae(),rnt=Mae(),nnt=Uae(),int=jae(),snt=Gae();function FE(t){var e=-1,r=t==null?0:t.length;for(this.clear();++e{var ont=BB();function ant(){this.__data__=new ont,this.size=0}Yae.exports=ant});var Jae=L((DYt,Kae)=>{function lnt(t){var e=this.__data__,r=e.delete(t);return this.size=e.size,r}Kae.exports=lnt});var Zae=L((bYt,zae)=>{function cnt(t){return this.__data__.get(t)}zae.exports=cnt});var $ae=L((PYt,Xae)=>{function unt(t){return this.__data__.has(t)}Xae.exports=unt});var B4=L((xYt,ele)=>{var fnt=typeof global=="object"&&global&&global.Object===Object&&global;ele.exports=fnt});var Pc=L((kYt,tle)=>{var Ant=B4(),pnt=typeof self=="object"&&self&&self.Object===Object&&self,hnt=Ant||pnt||Function("return this")();tle.exports=hnt});var Gd=L((QYt,rle)=>{var gnt=Pc(),dnt=gnt.Symbol;rle.exports=dnt});var ole=L((TYt,sle)=>{var nle=Gd(),ile=Object.prototype,mnt=ile.hasOwnProperty,ynt=ile.toString,vB=nle?nle.toStringTag:void 0;function Ent(t){var e=mnt.call(t,vB),r=t[vB];try{t[vB]=void 0;var s=!0}catch{}var a=ynt.call(t);return s&&(e?t[vB]=r:delete t[vB]),a}sle.exports=Ent});var lle=L((RYt,ale)=>{var Int=Object.prototype,Cnt=Int.toString;function wnt(t){return Cnt.call(t)}ale.exports=wnt});var Wd=L((FYt,fle)=>{var cle=Gd(),Bnt=ole(),vnt=lle(),Snt="[object Null]",Dnt="[object Undefined]",ule=cle?cle.toStringTag:void 0;function bnt(t){return t==null?t===void 0?Dnt:Snt:ule&&ule in Object(t)?Bnt(t):vnt(t)}fle.exports=bnt});var Wl=L((NYt,Ale)=>{function Pnt(t){var e=typeof t;return t!=null&&(e=="object"||e=="function")}Ale.exports=Pnt});var Sk=L((OYt,ple)=>{var xnt=Wd(),knt=Wl(),Qnt="[object AsyncFunction]",Tnt="[object Function]",Rnt="[object GeneratorFunction]",Fnt="[object Proxy]";function Nnt(t){if(!knt(t))return!1;var e=xnt(t);return e==Tnt||e==Rnt||e==Qnt||e==Fnt}ple.exports=Nnt});var gle=L((LYt,hle)=>{var Ont=Pc(),Lnt=Ont["__core-js_shared__"];hle.exports=Lnt});var yle=L((MYt,mle)=>{var v4=gle(),dle=function(){var t=/[^.]+$/.exec(v4&&v4.keys&&v4.keys.IE_PROTO||"");return t?"Symbol(src)_1."+t:""}();function Mnt(t){return!!dle&&dle in t}mle.exports=Mnt});var S4=L((_Yt,Ele)=>{var _nt=Function.prototype,Unt=_nt.toString;function Hnt(t){if(t!=null){try{return Unt.call(t)}catch{}try{return t+""}catch{}}return""}Ele.exports=Hnt});var Cle=L((UYt,Ile)=>{var jnt=Sk(),qnt=yle(),Gnt=Wl(),Wnt=S4(),Ynt=/[\\^$.*+?()[\]{}|]/g,Vnt=/^\[object .+?Constructor\]$/,Knt=Function.prototype,Jnt=Object.prototype,znt=Knt.toString,Znt=Jnt.hasOwnProperty,Xnt=RegExp("^"+znt.call(Znt).replace(Ynt,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function $nt(t){if(!Gnt(t)||qnt(t))return!1;var e=jnt(t)?Xnt:Vnt;return e.test(Wnt(t))}Ile.exports=$nt});var Ble=L((HYt,wle)=>{function eit(t,e){return t?.[e]}wle.exports=eit});var f0=L((jYt,vle)=>{var tit=Cle(),rit=Ble();function nit(t,e){var r=rit(t,e);return tit(r)?r:void 0}vle.exports=nit});var Dk=L((qYt,Sle)=>{var iit=f0(),sit=Pc(),oit=iit(sit,"Map");Sle.exports=oit});var SB=L((GYt,Dle)=>{var ait=f0(),lit=ait(Object,"create");Dle.exports=lit});var xle=L((WYt,Ple)=>{var ble=SB();function cit(){this.__data__=ble?ble(null):{},this.size=0}Ple.exports=cit});var Qle=L((YYt,kle)=>{function uit(t){var e=this.has(t)&&delete this.__data__[t];return this.size-=e?1:0,e}kle.exports=uit});var Rle=L((VYt,Tle)=>{var fit=SB(),Ait="__lodash_hash_undefined__",pit=Object.prototype,hit=pit.hasOwnProperty;function git(t){var e=this.__data__;if(fit){var r=e[t];return r===Ait?void 0:r}return hit.call(e,t)?e[t]:void 0}Tle.exports=git});var Nle=L((KYt,Fle)=>{var dit=SB(),mit=Object.prototype,yit=mit.hasOwnProperty;function Eit(t){var e=this.__data__;return dit?e[t]!==void 0:yit.call(e,t)}Fle.exports=Eit});var Lle=L((JYt,Ole)=>{var Iit=SB(),Cit="__lodash_hash_undefined__";function wit(t,e){var r=this.__data__;return this.size+=this.has(t)?0:1,r[t]=Iit&&e===void 0?Cit:e,this}Ole.exports=wit});var _le=L((zYt,Mle)=>{var Bit=xle(),vit=Qle(),Sit=Rle(),Dit=Nle(),bit=Lle();function NE(t){var e=-1,r=t==null?0:t.length;for(this.clear();++e{var Ule=_le(),Pit=BB(),xit=Dk();function kit(){this.size=0,this.__data__={hash:new Ule,map:new(xit||Pit),string:new Ule}}Hle.exports=kit});var Gle=L((XYt,qle)=>{function Qit(t){var e=typeof t;return e=="string"||e=="number"||e=="symbol"||e=="boolean"?t!=="__proto__":t===null}qle.exports=Qit});var DB=L(($Yt,Wle)=>{var Tit=Gle();function Rit(t,e){var r=t.__data__;return Tit(e)?r[typeof e=="string"?"string":"hash"]:r.map}Wle.exports=Rit});var Vle=L((eVt,Yle)=>{var Fit=DB();function Nit(t){var e=Fit(this,t).delete(t);return this.size-=e?1:0,e}Yle.exports=Nit});var Jle=L((tVt,Kle)=>{var Oit=DB();function Lit(t){return Oit(this,t).get(t)}Kle.exports=Lit});var Zle=L((rVt,zle)=>{var Mit=DB();function _it(t){return Mit(this,t).has(t)}zle.exports=_it});var $le=L((nVt,Xle)=>{var Uit=DB();function Hit(t,e){var r=Uit(this,t),s=r.size;return r.set(t,e),this.size+=r.size==s?0:1,this}Xle.exports=Hit});var bk=L((iVt,ece)=>{var jit=jle(),qit=Vle(),Git=Jle(),Wit=Zle(),Yit=$le();function OE(t){var e=-1,r=t==null?0:t.length;for(this.clear();++e{var Vit=BB(),Kit=Dk(),Jit=bk(),zit=200;function Zit(t,e){var r=this.__data__;if(r instanceof Vit){var s=r.__data__;if(!Kit||s.length{var Xit=BB(),$it=Vae(),est=Jae(),tst=Zae(),rst=$ae(),nst=rce();function LE(t){var e=this.__data__=new Xit(t);this.size=e.size}LE.prototype.clear=$it;LE.prototype.delete=est;LE.prototype.get=tst;LE.prototype.has=rst;LE.prototype.set=nst;nce.exports=LE});var sce=L((aVt,ice)=>{var ist="__lodash_hash_undefined__";function sst(t){return this.__data__.set(t,ist),this}ice.exports=sst});var ace=L((lVt,oce)=>{function ost(t){return this.__data__.has(t)}oce.exports=ost});var cce=L((cVt,lce)=>{var ast=bk(),lst=sce(),cst=ace();function xk(t){var e=-1,r=t==null?0:t.length;for(this.__data__=new ast;++e{function ust(t,e){for(var r=-1,s=t==null?0:t.length;++r{function fst(t,e){return t.has(e)}Ace.exports=fst});var D4=L((AVt,hce)=>{var Ast=cce(),pst=fce(),hst=pce(),gst=1,dst=2;function mst(t,e,r,s,a,n){var c=r&gst,f=t.length,p=e.length;if(f!=p&&!(c&&p>f))return!1;var h=n.get(t),E=n.get(e);if(h&&E)return h==e&&E==t;var C=-1,S=!0,P=r&dst?new Ast:void 0;for(n.set(t,e),n.set(e,t);++C{var yst=Pc(),Est=yst.Uint8Array;gce.exports=Est});var mce=L((hVt,dce)=>{function Ist(t){var e=-1,r=Array(t.size);return t.forEach(function(s,a){r[++e]=[a,s]}),r}dce.exports=Ist});var Ece=L((gVt,yce)=>{function Cst(t){var e=-1,r=Array(t.size);return t.forEach(function(s){r[++e]=s}),r}yce.exports=Cst});var vce=L((dVt,Bce)=>{var Ice=Gd(),Cce=b4(),wst=RE(),Bst=D4(),vst=mce(),Sst=Ece(),Dst=1,bst=2,Pst="[object Boolean]",xst="[object Date]",kst="[object Error]",Qst="[object Map]",Tst="[object Number]",Rst="[object RegExp]",Fst="[object Set]",Nst="[object String]",Ost="[object Symbol]",Lst="[object ArrayBuffer]",Mst="[object DataView]",wce=Ice?Ice.prototype:void 0,P4=wce?wce.valueOf:void 0;function _st(t,e,r,s,a,n,c){switch(r){case Mst:if(t.byteLength!=e.byteLength||t.byteOffset!=e.byteOffset)return!1;t=t.buffer,e=e.buffer;case Lst:return!(t.byteLength!=e.byteLength||!n(new Cce(t),new Cce(e)));case Pst:case xst:case Tst:return wst(+t,+e);case kst:return t.name==e.name&&t.message==e.message;case Rst:case Nst:return t==e+"";case Qst:var f=vst;case Fst:var p=s&Dst;if(f||(f=Sst),t.size!=e.size&&!p)return!1;var h=c.get(t);if(h)return h==e;s|=bst,c.set(t,e);var E=Bst(f(t),f(e),s,a,n,c);return c.delete(t),E;case Ost:if(P4)return P4.call(t)==P4.call(e)}return!1}Bce.exports=_st});var kk=L((mVt,Sce)=>{function Ust(t,e){for(var r=-1,s=e.length,a=t.length;++r{var Hst=Array.isArray;Dce.exports=Hst});var x4=L((EVt,bce)=>{var jst=kk(),qst=xc();function Gst(t,e,r){var s=e(t);return qst(t)?s:jst(s,r(t))}bce.exports=Gst});var xce=L((IVt,Pce)=>{function Wst(t,e){for(var r=-1,s=t==null?0:t.length,a=0,n=[];++r{function Yst(){return[]}kce.exports=Yst});var Qk=L((wVt,Tce)=>{var Vst=xce(),Kst=k4(),Jst=Object.prototype,zst=Jst.propertyIsEnumerable,Qce=Object.getOwnPropertySymbols,Zst=Qce?function(t){return t==null?[]:(t=Object(t),Vst(Qce(t),function(e){return zst.call(t,e)}))}:Kst;Tce.exports=Zst});var Fce=L((BVt,Rce)=>{function Xst(t,e){for(var r=-1,s=Array(t);++r{function $st(t){return t!=null&&typeof t=="object"}Nce.exports=$st});var Lce=L((SVt,Oce)=>{var eot=Wd(),tot=zf(),rot="[object Arguments]";function not(t){return tot(t)&&eot(t)==rot}Oce.exports=not});var bB=L((DVt,Uce)=>{var Mce=Lce(),iot=zf(),_ce=Object.prototype,sot=_ce.hasOwnProperty,oot=_ce.propertyIsEnumerable,aot=Mce(function(){return arguments}())?Mce:function(t){return iot(t)&&sot.call(t,"callee")&&!oot.call(t,"callee")};Uce.exports=aot});var jce=L((bVt,Hce)=>{function lot(){return!1}Hce.exports=lot});var xB=L((PB,ME)=>{var cot=Pc(),uot=jce(),Wce=typeof PB=="object"&&PB&&!PB.nodeType&&PB,qce=Wce&&typeof ME=="object"&&ME&&!ME.nodeType&&ME,fot=qce&&qce.exports===Wce,Gce=fot?cot.Buffer:void 0,Aot=Gce?Gce.isBuffer:void 0,pot=Aot||uot;ME.exports=pot});var kB=L((PVt,Yce)=>{var hot=9007199254740991,got=/^(?:0|[1-9]\d*)$/;function dot(t,e){var r=typeof t;return e=e??hot,!!e&&(r=="number"||r!="symbol"&&got.test(t))&&t>-1&&t%1==0&&t{var mot=9007199254740991;function yot(t){return typeof t=="number"&&t>-1&&t%1==0&&t<=mot}Vce.exports=yot});var Jce=L((kVt,Kce)=>{var Eot=Wd(),Iot=Tk(),Cot=zf(),wot="[object Arguments]",Bot="[object Array]",vot="[object Boolean]",Sot="[object Date]",Dot="[object Error]",bot="[object Function]",Pot="[object Map]",xot="[object Number]",kot="[object Object]",Qot="[object RegExp]",Tot="[object Set]",Rot="[object String]",Fot="[object WeakMap]",Not="[object ArrayBuffer]",Oot="[object DataView]",Lot="[object Float32Array]",Mot="[object Float64Array]",_ot="[object Int8Array]",Uot="[object Int16Array]",Hot="[object Int32Array]",jot="[object Uint8Array]",qot="[object Uint8ClampedArray]",Got="[object Uint16Array]",Wot="[object Uint32Array]",Di={};Di[Lot]=Di[Mot]=Di[_ot]=Di[Uot]=Di[Hot]=Di[jot]=Di[qot]=Di[Got]=Di[Wot]=!0;Di[wot]=Di[Bot]=Di[Not]=Di[vot]=Di[Oot]=Di[Sot]=Di[Dot]=Di[bot]=Di[Pot]=Di[xot]=Di[kot]=Di[Qot]=Di[Tot]=Di[Rot]=Di[Fot]=!1;function Yot(t){return Cot(t)&&Iot(t.length)&&!!Di[Eot(t)]}Kce.exports=Yot});var Rk=L((QVt,zce)=>{function Vot(t){return function(e){return t(e)}}zce.exports=Vot});var Fk=L((QB,_E)=>{var Kot=B4(),Zce=typeof QB=="object"&&QB&&!QB.nodeType&&QB,TB=Zce&&typeof _E=="object"&&_E&&!_E.nodeType&&_E,Jot=TB&&TB.exports===Zce,Q4=Jot&&Kot.process,zot=function(){try{var t=TB&&TB.require&&TB.require("util").types;return t||Q4&&Q4.binding&&Q4.binding("util")}catch{}}();_E.exports=zot});var Nk=L((TVt,eue)=>{var Zot=Jce(),Xot=Rk(),Xce=Fk(),$ce=Xce&&Xce.isTypedArray,$ot=$ce?Xot($ce):Zot;eue.exports=$ot});var T4=L((RVt,tue)=>{var eat=Fce(),tat=bB(),rat=xc(),nat=xB(),iat=kB(),sat=Nk(),oat=Object.prototype,aat=oat.hasOwnProperty;function lat(t,e){var r=rat(t),s=!r&&tat(t),a=!r&&!s&&nat(t),n=!r&&!s&&!a&&sat(t),c=r||s||a||n,f=c?eat(t.length,String):[],p=f.length;for(var h in t)(e||aat.call(t,h))&&!(c&&(h=="length"||a&&(h=="offset"||h=="parent")||n&&(h=="buffer"||h=="byteLength"||h=="byteOffset")||iat(h,p)))&&f.push(h);return f}tue.exports=lat});var Ok=L((FVt,rue)=>{var cat=Object.prototype;function uat(t){var e=t&&t.constructor,r=typeof e=="function"&&e.prototype||cat;return t===r}rue.exports=uat});var R4=L((NVt,nue)=>{function fat(t,e){return function(r){return t(e(r))}}nue.exports=fat});var sue=L((OVt,iue)=>{var Aat=R4(),pat=Aat(Object.keys,Object);iue.exports=pat});var aue=L((LVt,oue)=>{var hat=Ok(),gat=sue(),dat=Object.prototype,mat=dat.hasOwnProperty;function yat(t){if(!hat(t))return gat(t);var e=[];for(var r in Object(t))mat.call(t,r)&&r!="constructor"&&e.push(r);return e}oue.exports=yat});var RB=L((MVt,lue)=>{var Eat=Sk(),Iat=Tk();function Cat(t){return t!=null&&Iat(t.length)&&!Eat(t)}lue.exports=Cat});var Lk=L((_Vt,cue)=>{var wat=T4(),Bat=aue(),vat=RB();function Sat(t){return vat(t)?wat(t):Bat(t)}cue.exports=Sat});var F4=L((UVt,uue)=>{var Dat=x4(),bat=Qk(),Pat=Lk();function xat(t){return Dat(t,Pat,bat)}uue.exports=xat});var pue=L((HVt,Aue)=>{var fue=F4(),kat=1,Qat=Object.prototype,Tat=Qat.hasOwnProperty;function Rat(t,e,r,s,a,n){var c=r&kat,f=fue(t),p=f.length,h=fue(e),E=h.length;if(p!=E&&!c)return!1;for(var C=p;C--;){var S=f[C];if(!(c?S in e:Tat.call(e,S)))return!1}var P=n.get(t),I=n.get(e);if(P&&I)return P==e&&I==t;var R=!0;n.set(t,e),n.set(e,t);for(var N=c;++C{var Fat=f0(),Nat=Pc(),Oat=Fat(Nat,"DataView");hue.exports=Oat});var mue=L((qVt,due)=>{var Lat=f0(),Mat=Pc(),_at=Lat(Mat,"Promise");due.exports=_at});var Eue=L((GVt,yue)=>{var Uat=f0(),Hat=Pc(),jat=Uat(Hat,"Set");yue.exports=jat});var Cue=L((WVt,Iue)=>{var qat=f0(),Gat=Pc(),Wat=qat(Gat,"WeakMap");Iue.exports=Wat});var FB=L((YVt,Pue)=>{var N4=gue(),O4=Dk(),L4=mue(),M4=Eue(),_4=Cue(),bue=Wd(),UE=S4(),wue="[object Map]",Yat="[object Object]",Bue="[object Promise]",vue="[object Set]",Sue="[object WeakMap]",Due="[object DataView]",Vat=UE(N4),Kat=UE(O4),Jat=UE(L4),zat=UE(M4),Zat=UE(_4),Yd=bue;(N4&&Yd(new N4(new ArrayBuffer(1)))!=Due||O4&&Yd(new O4)!=wue||L4&&Yd(L4.resolve())!=Bue||M4&&Yd(new M4)!=vue||_4&&Yd(new _4)!=Sue)&&(Yd=function(t){var e=bue(t),r=e==Yat?t.constructor:void 0,s=r?UE(r):"";if(s)switch(s){case Vat:return Due;case Kat:return wue;case Jat:return Bue;case zat:return vue;case Zat:return Sue}return e});Pue.exports=Yd});var Oue=L((VVt,Nue)=>{var U4=Pk(),Xat=D4(),$at=vce(),elt=pue(),xue=FB(),kue=xc(),Que=xB(),tlt=Nk(),rlt=1,Tue="[object Arguments]",Rue="[object Array]",Mk="[object Object]",nlt=Object.prototype,Fue=nlt.hasOwnProperty;function ilt(t,e,r,s,a,n){var c=kue(t),f=kue(e),p=c?Rue:xue(t),h=f?Rue:xue(e);p=p==Tue?Mk:p,h=h==Tue?Mk:h;var E=p==Mk,C=h==Mk,S=p==h;if(S&&Que(t)){if(!Que(e))return!1;c=!0,E=!1}if(S&&!E)return n||(n=new U4),c||tlt(t)?Xat(t,e,r,s,a,n):$at(t,e,p,r,s,a,n);if(!(r&rlt)){var P=E&&Fue.call(t,"__wrapped__"),I=C&&Fue.call(e,"__wrapped__");if(P||I){var R=P?t.value():t,N=I?e.value():e;return n||(n=new U4),a(R,N,r,s,n)}}return S?(n||(n=new U4),elt(t,e,r,s,a,n)):!1}Nue.exports=ilt});var Uue=L((KVt,_ue)=>{var slt=Oue(),Lue=zf();function Mue(t,e,r,s,a){return t===e?!0:t==null||e==null||!Lue(t)&&!Lue(e)?t!==t&&e!==e:slt(t,e,r,s,Mue,a)}_ue.exports=Mue});var jue=L((JVt,Hue)=>{var olt=Uue();function alt(t,e){return olt(t,e)}Hue.exports=alt});var H4=L((zVt,que)=>{var llt=f0(),clt=function(){try{var t=llt(Object,"defineProperty");return t({},"",{}),t}catch{}}();que.exports=clt});var _k=L((ZVt,Wue)=>{var Gue=H4();function ult(t,e,r){e=="__proto__"&&Gue?Gue(t,e,{configurable:!0,enumerable:!0,value:r,writable:!0}):t[e]=r}Wue.exports=ult});var j4=L((XVt,Yue)=>{var flt=_k(),Alt=RE();function plt(t,e,r){(r!==void 0&&!Alt(t[e],r)||r===void 0&&!(e in t))&&flt(t,e,r)}Yue.exports=plt});var Kue=L(($Vt,Vue)=>{function hlt(t){return function(e,r,s){for(var a=-1,n=Object(e),c=s(e),f=c.length;f--;){var p=c[t?f:++a];if(r(n[p],p,n)===!1)break}return e}}Vue.exports=hlt});var zue=L((e7t,Jue)=>{var glt=Kue(),dlt=glt();Jue.exports=dlt});var q4=L((NB,HE)=>{var mlt=Pc(),efe=typeof NB=="object"&&NB&&!NB.nodeType&&NB,Zue=efe&&typeof HE=="object"&&HE&&!HE.nodeType&&HE,ylt=Zue&&Zue.exports===efe,Xue=ylt?mlt.Buffer:void 0,$ue=Xue?Xue.allocUnsafe:void 0;function Elt(t,e){if(e)return t.slice();var r=t.length,s=$ue?$ue(r):new t.constructor(r);return t.copy(s),s}HE.exports=Elt});var Uk=L((t7t,rfe)=>{var tfe=b4();function Ilt(t){var e=new t.constructor(t.byteLength);return new tfe(e).set(new tfe(t)),e}rfe.exports=Ilt});var G4=L((r7t,nfe)=>{var Clt=Uk();function wlt(t,e){var r=e?Clt(t.buffer):t.buffer;return new t.constructor(r,t.byteOffset,t.length)}nfe.exports=wlt});var Hk=L((n7t,ife)=>{function Blt(t,e){var r=-1,s=t.length;for(e||(e=Array(s));++r{var vlt=Wl(),sfe=Object.create,Slt=function(){function t(){}return function(e){if(!vlt(e))return{};if(sfe)return sfe(e);t.prototype=e;var r=new t;return t.prototype=void 0,r}}();ofe.exports=Slt});var jk=L((s7t,lfe)=>{var Dlt=R4(),blt=Dlt(Object.getPrototypeOf,Object);lfe.exports=blt});var W4=L((o7t,cfe)=>{var Plt=afe(),xlt=jk(),klt=Ok();function Qlt(t){return typeof t.constructor=="function"&&!klt(t)?Plt(xlt(t)):{}}cfe.exports=Qlt});var ffe=L((a7t,ufe)=>{var Tlt=RB(),Rlt=zf();function Flt(t){return Rlt(t)&&Tlt(t)}ufe.exports=Flt});var Y4=L((l7t,pfe)=>{var Nlt=Wd(),Olt=jk(),Llt=zf(),Mlt="[object Object]",_lt=Function.prototype,Ult=Object.prototype,Afe=_lt.toString,Hlt=Ult.hasOwnProperty,jlt=Afe.call(Object);function qlt(t){if(!Llt(t)||Nlt(t)!=Mlt)return!1;var e=Olt(t);if(e===null)return!0;var r=Hlt.call(e,"constructor")&&e.constructor;return typeof r=="function"&&r instanceof r&&Afe.call(r)==jlt}pfe.exports=qlt});var V4=L((c7t,hfe)=>{function Glt(t,e){if(!(e==="constructor"&&typeof t[e]=="function")&&e!="__proto__")return t[e]}hfe.exports=Glt});var qk=L((u7t,gfe)=>{var Wlt=_k(),Ylt=RE(),Vlt=Object.prototype,Klt=Vlt.hasOwnProperty;function Jlt(t,e,r){var s=t[e];(!(Klt.call(t,e)&&Ylt(s,r))||r===void 0&&!(e in t))&&Wlt(t,e,r)}gfe.exports=Jlt});var Vd=L((f7t,dfe)=>{var zlt=qk(),Zlt=_k();function Xlt(t,e,r,s){var a=!r;r||(r={});for(var n=-1,c=e.length;++n{function $lt(t){var e=[];if(t!=null)for(var r in Object(t))e.push(r);return e}mfe.exports=$lt});var Ife=L((p7t,Efe)=>{var ect=Wl(),tct=Ok(),rct=yfe(),nct=Object.prototype,ict=nct.hasOwnProperty;function sct(t){if(!ect(t))return rct(t);var e=tct(t),r=[];for(var s in t)s=="constructor"&&(e||!ict.call(t,s))||r.push(s);return r}Efe.exports=sct});var jE=L((h7t,Cfe)=>{var oct=T4(),act=Ife(),lct=RB();function cct(t){return lct(t)?oct(t,!0):act(t)}Cfe.exports=cct});var Bfe=L((g7t,wfe)=>{var uct=Vd(),fct=jE();function Act(t){return uct(t,fct(t))}wfe.exports=Act});var xfe=L((d7t,Pfe)=>{var vfe=j4(),pct=q4(),hct=G4(),gct=Hk(),dct=W4(),Sfe=bB(),Dfe=xc(),mct=ffe(),yct=xB(),Ect=Sk(),Ict=Wl(),Cct=Y4(),wct=Nk(),bfe=V4(),Bct=Bfe();function vct(t,e,r,s,a,n,c){var f=bfe(t,r),p=bfe(e,r),h=c.get(p);if(h){vfe(t,r,h);return}var E=n?n(f,p,r+"",t,e,c):void 0,C=E===void 0;if(C){var S=Dfe(p),P=!S&&yct(p),I=!S&&!P&&wct(p);E=p,S||P||I?Dfe(f)?E=f:mct(f)?E=gct(f):P?(C=!1,E=pct(p,!0)):I?(C=!1,E=hct(p,!0)):E=[]:Cct(p)||Sfe(p)?(E=f,Sfe(f)?E=Bct(f):(!Ict(f)||Ect(f))&&(E=dct(p))):C=!1}C&&(c.set(p,E),a(E,p,s,n,c),c.delete(p)),vfe(t,r,E)}Pfe.exports=vct});var Tfe=L((m7t,Qfe)=>{var Sct=Pk(),Dct=j4(),bct=zue(),Pct=xfe(),xct=Wl(),kct=jE(),Qct=V4();function kfe(t,e,r,s,a){t!==e&&bct(e,function(n,c){if(a||(a=new Sct),xct(n))Pct(t,e,c,r,kfe,s,a);else{var f=s?s(Qct(t,c),n,c+"",t,e,a):void 0;f===void 0&&(f=n),Dct(t,c,f)}},kct)}Qfe.exports=kfe});var K4=L((y7t,Rfe)=>{function Tct(t){return t}Rfe.exports=Tct});var Nfe=L((E7t,Ffe)=>{function Rct(t,e,r){switch(r.length){case 0:return t.call(e);case 1:return t.call(e,r[0]);case 2:return t.call(e,r[0],r[1]);case 3:return t.call(e,r[0],r[1],r[2])}return t.apply(e,r)}Ffe.exports=Rct});var J4=L((I7t,Lfe)=>{var Fct=Nfe(),Ofe=Math.max;function Nct(t,e,r){return e=Ofe(e===void 0?t.length-1:e,0),function(){for(var s=arguments,a=-1,n=Ofe(s.length-e,0),c=Array(n);++a{function Oct(t){return function(){return t}}Mfe.exports=Oct});var jfe=L((w7t,Hfe)=>{var Lct=_fe(),Ufe=H4(),Mct=K4(),_ct=Ufe?function(t,e){return Ufe(t,"toString",{configurable:!0,enumerable:!1,value:Lct(e),writable:!0})}:Mct;Hfe.exports=_ct});var Gfe=L((B7t,qfe)=>{var Uct=800,Hct=16,jct=Date.now;function qct(t){var e=0,r=0;return function(){var s=jct(),a=Hct-(s-r);if(r=s,a>0){if(++e>=Uct)return arguments[0]}else e=0;return t.apply(void 0,arguments)}}qfe.exports=qct});var z4=L((v7t,Wfe)=>{var Gct=jfe(),Wct=Gfe(),Yct=Wct(Gct);Wfe.exports=Yct});var Vfe=L((S7t,Yfe)=>{var Vct=K4(),Kct=J4(),Jct=z4();function zct(t,e){return Jct(Kct(t,e,Vct),t+"")}Yfe.exports=zct});var Jfe=L((D7t,Kfe)=>{var Zct=RE(),Xct=RB(),$ct=kB(),eut=Wl();function tut(t,e,r){if(!eut(r))return!1;var s=typeof e;return(s=="number"?Xct(r)&&$ct(e,r.length):s=="string"&&e in r)?Zct(r[e],t):!1}Kfe.exports=tut});var Zfe=L((b7t,zfe)=>{var rut=Vfe(),nut=Jfe();function iut(t){return rut(function(e,r){var s=-1,a=r.length,n=a>1?r[a-1]:void 0,c=a>2?r[2]:void 0;for(n=t.length>3&&typeof n=="function"?(a--,n):void 0,c&&nut(r[0],r[1],c)&&(n=a<3?void 0:n,a=1),e=Object(e);++s{var sut=Tfe(),out=Zfe(),aut=out(function(t,e,r,s){sut(t,e,r,s)});Xfe.exports=aut});var je={};Vt(je,{AsyncActions:()=>$4,BufferStream:()=>X4,CachingStrategy:()=>fAe,DefaultStream:()=>e3,allSettledSafe:()=>Uu,assertNever:()=>r3,bufferStream:()=>GE,buildIgnorePattern:()=>hut,convertMapsToIndexableObjects:()=>Wk,dynamicRequire:()=>kp,escapeRegExp:()=>cut,getArrayWithDefault:()=>LB,getFactoryWithDefault:()=>Vl,getMapWithDefault:()=>n3,getSetWithDefault:()=>xp,groupBy:()=>mut,isIndexableObject:()=>Z4,isPathLike:()=>gut,isTaggedYarnVersion:()=>lut,makeDeferred:()=>lAe,mapAndFilter:()=>Yl,mapAndFind:()=>A0,mergeIntoTarget:()=>pAe,overrideType:()=>uut,parseBoolean:()=>MB,parseInt:()=>WE,parseOptionalBoolean:()=>AAe,plural:()=>Gk,prettifyAsyncErrors:()=>qE,prettifySyncErrors:()=>i3,releaseAfterUseAsync:()=>Aut,replaceEnvVariables:()=>Yk,sortMap:()=>Ys,toMerged:()=>dut,tryParseOptionalBoolean:()=>s3,validateEnum:()=>fut});function lut(t){return!!(sAe.default.valid(t)&&t.match(/^[^-]+(-rc\.[0-9]+)?$/))}function Gk(t,{one:e,more:r,zero:s=r}){return t===0?s:t===1?e:r}function cut(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function uut(t){}function r3(t){throw new Error(`Assertion failed: Unexpected object '${t}'`)}function fut(t,e){let r=Object.values(t);if(!r.includes(e))throw new nt(`Invalid value for enumeration: ${JSON.stringify(e)} (expected one of ${r.map(s=>JSON.stringify(s)).join(", ")})`);return e}function Yl(t,e){let r=[];for(let s of t){let a=e(s);a!==oAe&&r.push(a)}return r}function A0(t,e){for(let r of t){let s=e(r);if(s!==aAe)return s}}function Z4(t){return typeof t=="object"&&t!==null}async function Uu(t){let e=await Promise.allSettled(t),r=[];for(let s of e){if(s.status==="rejected")throw s.reason;r.push(s.value)}return r}function Wk(t){if(t instanceof Map&&(t=Object.fromEntries(t)),Z4(t))for(let e of Object.keys(t)){let r=t[e];Z4(r)&&(t[e]=Wk(r))}return t}function Vl(t,e,r){let s=t.get(e);return typeof s>"u"&&t.set(e,s=r()),s}function LB(t,e){let r=t.get(e);return typeof r>"u"&&t.set(e,r=[]),r}function xp(t,e){let r=t.get(e);return typeof r>"u"&&t.set(e,r=new Set),r}function n3(t,e){let r=t.get(e);return typeof r>"u"&&t.set(e,r=new Map),r}async function Aut(t,e){if(e==null)return await t();try{return await t()}finally{await e()}}async function qE(t,e){try{return await t()}catch(r){throw r.message=e(r.message),r}}function i3(t,e){try{return t()}catch(r){throw r.message=e(r.message),r}}async function GE(t){return await new Promise((e,r)=>{let s=[];t.on("error",a=>{r(a)}),t.on("data",a=>{s.push(a)}),t.on("end",()=>{e(Buffer.concat(s))})})}function lAe(){let t,e;return{promise:new Promise((s,a)=>{t=s,e=a}),resolve:t,reject:e}}function cAe(t){return OB(ue.fromPortablePath(t))}function uAe(path){let physicalPath=ue.fromPortablePath(path),currentCacheEntry=OB.cache[physicalPath];delete OB.cache[physicalPath];let result;try{result=cAe(physicalPath);let freshCacheEntry=OB.cache[physicalPath],dynamicModule=eval("module"),freshCacheIndex=dynamicModule.children.indexOf(freshCacheEntry);freshCacheIndex!==-1&&dynamicModule.children.splice(freshCacheIndex,1)}finally{OB.cache[physicalPath]=currentCacheEntry}return result}function put(t){let e=eAe.get(t),r=le.statSync(t);if(e?.mtime===r.mtimeMs)return e.instance;let s=uAe(t);return eAe.set(t,{mtime:r.mtimeMs,instance:s}),s}function kp(t,{cachingStrategy:e=2}={}){switch(e){case 0:return uAe(t);case 1:return put(t);case 2:return cAe(t);default:throw new Error("Unsupported caching strategy")}}function Ys(t,e){let r=Array.from(t);Array.isArray(e)||(e=[e]);let s=[];for(let n of e)s.push(r.map(c=>n(c)));let a=r.map((n,c)=>c);return a.sort((n,c)=>{for(let f of s){let p=f[n]f[c]?1:0;if(p!==0)return p}return 0}),a.map(n=>r[n])}function hut(t){return t.length===0?null:t.map(e=>`(${nAe.default.makeRe(e,{windows:!1,dot:!0}).source})`).join("|")}function Yk(t,{env:e}){let r=/\${(?[\d\w_]+)(?:)?(?:-(?[^}]*))?}/g;return t.replace(r,(...s)=>{let{variableName:a,colon:n,fallback:c}=s[s.length-1],f=Object.hasOwn(e,a),p=e[a];if(p||f&&!n)return p;if(c!=null)return c;throw new nt(`Environment variable not found (${a})`)})}function MB(t){switch(t){case"true":case"1":case 1:case!0:return!0;case"false":case"0":case 0:case!1:return!1;default:throw new Error(`Couldn't parse "${t}" as a boolean`)}}function AAe(t){return typeof t>"u"?t:MB(t)}function s3(t){try{return AAe(t)}catch{return null}}function gut(t){return!!(ue.isAbsolute(t)||t.match(/^(\.{1,2}|~)\//))}function pAe(t,...e){let r=c=>({value:c}),s=r(t),a=e.map(c=>r(c)),{value:n}=(0,rAe.default)(s,...a,(c,f)=>{if(Array.isArray(c)&&Array.isArray(f)){for(let p of f)c.find(h=>(0,tAe.default)(h,p))||c.push(p);return c}});return n}function dut(...t){return pAe({},...t)}function mut(t,e){let r=Object.create(null);for(let s of t){let a=s[e];r[a]??=[],r[a].push(s)}return r}function WE(t){return typeof t=="string"?Number.parseInt(t,10):t}var tAe,rAe,nAe,iAe,sAe,t3,oAe,aAe,X4,$4,e3,OB,eAe,fAe,kc=Ct(()=>{bt();Wt();tAe=et(jue()),rAe=et($fe()),nAe=et(Sa()),iAe=et(Od()),sAe=et(Ai()),t3=Ie("stream");oAe=Symbol();Yl.skip=oAe;aAe=Symbol();A0.skip=aAe;X4=class extends t3.Transform{constructor(){super(...arguments);this.chunks=[]}_transform(r,s,a){if(s!=="buffer"||!Buffer.isBuffer(r))throw new Error("Assertion failed: BufferStream only accept buffers");this.chunks.push(r),a(null,null)}_flush(r){r(null,Buffer.concat(this.chunks))}};$4=class{constructor(e){this.deferred=new Map;this.promises=new Map;this.limit=(0,iAe.default)(e)}set(e,r){let s=this.deferred.get(e);typeof s>"u"&&this.deferred.set(e,s=lAe());let a=this.limit(()=>r());return this.promises.set(e,a),a.then(()=>{this.promises.get(e)===a&&s.resolve()},n=>{this.promises.get(e)===a&&s.reject(n)}),s.promise}reduce(e,r){let s=this.promises.get(e)??Promise.resolve();this.set(e,()=>r(s))}async wait(){await Promise.all(this.promises.values())}},e3=class extends t3.Transform{constructor(r=Buffer.alloc(0)){super();this.active=!0;this.ifEmpty=r}_transform(r,s,a){if(s!=="buffer"||!Buffer.isBuffer(r))throw new Error("Assertion failed: DefaultStream only accept buffers");this.active=!1,a(null,r)}_flush(r){this.active&&this.ifEmpty.length>0?r(null,this.ifEmpty):r(null)}},OB=eval("require");eAe=new Map;fAe=(s=>(s[s.NoCache=0]="NoCache",s[s.FsTime=1]="FsTime",s[s.Node=2]="Node",s))(fAe||{})});var YE,o3,a3,hAe=Ct(()=>{YE=(r=>(r.HARD="HARD",r.SOFT="SOFT",r))(YE||{}),o3=(s=>(s.Dependency="Dependency",s.PeerDependency="PeerDependency",s.PeerDependencyMeta="PeerDependencyMeta",s))(o3||{}),a3=(s=>(s.Inactive="inactive",s.Redundant="redundant",s.Active="active",s))(a3||{})});var he={};Vt(he,{LogLevel:()=>Xk,Style:()=>Jk,Type:()=>pt,addLogFilterSupport:()=>HB,applyColor:()=>ri,applyHyperlink:()=>KE,applyStyle:()=>Kd,json:()=>Jd,jsonOrPretty:()=>Iut,mark:()=>A3,pretty:()=>Ut,prettyField:()=>Zf,prettyList:()=>f3,prettyTruncatedLocatorList:()=>Zk,stripAnsi:()=>VE.default,supportsColor:()=>zk,supportsHyperlinks:()=>u3,tuple:()=>Hu});function gAe(t){let e=["KiB","MiB","GiB","TiB"],r=e.length;for(;r>1&&t<1024**r;)r-=1;let s=1024**r;return`${Math.floor(t*100/s)/100} ${e[r-1]}`}function Vk(t,e){if(Array.isArray(e))return e.length===0?ri(t,"[]",pt.CODE):ri(t,"[ ",pt.CODE)+e.map(r=>Vk(t,r)).join(", ")+ri(t," ]",pt.CODE);if(typeof e=="string")return ri(t,JSON.stringify(e),pt.STRING);if(typeof e=="number")return ri(t,JSON.stringify(e),pt.NUMBER);if(typeof e=="boolean")return ri(t,JSON.stringify(e),pt.BOOLEAN);if(e===null)return ri(t,"null",pt.NULL);if(typeof e=="object"&&Object.getPrototypeOf(e)===Object.prototype){let r=Object.entries(e);return r.length===0?ri(t,"{}",pt.CODE):ri(t,"{ ",pt.CODE)+r.map(([s,a])=>`${Vk(t,s)}: ${Vk(t,a)}`).join(", ")+ri(t," }",pt.CODE)}if(typeof e>"u")return ri(t,"undefined",pt.NULL);throw new Error("Assertion failed: The value doesn't seem to be a valid JSON object")}function Hu(t,e){return[e,t]}function Kd(t,e,r){return t.get("enableColors")&&r&2&&(e=UB.default.bold(e)),e}function ri(t,e,r){if(!t.get("enableColors"))return e;let s=yut.get(r);if(s===null)return e;let a=typeof s>"u"?r:c3.level>=3?s[0]:s[1],n=typeof a=="number"?l3.ansi256(a):a.startsWith("#")?l3.hex(a):l3[a];if(typeof n!="function")throw new Error(`Invalid format type ${a}`);return n(e)}function KE(t,e,r){return t.get("enableHyperlinks")?Eut?`\x1B]8;;${r}\x1B\\${e}\x1B]8;;\x1B\\`:`\x1B]8;;${r}\x07${e}\x1B]8;;\x07`:e}function Ut(t,e,r){if(e===null)return ri(t,"null",pt.NULL);if(Object.hasOwn(Kk,r))return Kk[r].pretty(t,e);if(typeof e!="string")throw new Error(`Assertion failed: Expected the value to be a string, got ${typeof e}`);return ri(t,e,r)}function f3(t,e,r,{separator:s=", "}={}){return[...e].map(a=>Ut(t,a,r)).join(s)}function Jd(t,e){if(t===null)return null;if(Object.hasOwn(Kk,e))return Kk[e].json(t);if(typeof t!="string")throw new Error(`Assertion failed: Expected the value to be a string, got ${typeof t}`);return t}function Iut(t,e,[r,s]){return t?Jd(r,s):Ut(e,r,s)}function A3(t){return{Check:ri(t,"\u2713","green"),Cross:ri(t,"\u2718","red"),Question:ri(t,"?","cyan")}}function Zf(t,{label:e,value:[r,s]}){return`${Ut(t,e,pt.CODE)}: ${Ut(t,r,s)}`}function Zk(t,e,r){let s=[],a=[...e],n=r;for(;a.length>0;){let h=a[0],E=`${Yr(t,h)}, `,C=p3(h).length+2;if(s.length>0&&nh).join("").slice(0,-2);let c="X".repeat(a.length.toString().length),f=`and ${c} more.`,p=a.length;for(;s.length>1&&nh).join(""),f.replace(c,Ut(t,p,pt.NUMBER))].join("")}function HB(t,{configuration:e}){let r=e.get("logFilters"),s=new Map,a=new Map,n=[];for(let C of r){let S=C.get("level");if(typeof S>"u")continue;let P=C.get("code");typeof P<"u"&&s.set(P,S);let I=C.get("text");typeof I<"u"&&a.set(I,S);let R=C.get("pattern");typeof R<"u"&&n.push([dAe.default.matcher(R,{contains:!0}),S])}n.reverse();let c=(C,S,P)=>{if(C===null||C===0)return P;let I=a.size>0||n.length>0?(0,VE.default)(S):S;if(a.size>0){let R=a.get(I);if(typeof R<"u")return R??P}if(n.length>0){for(let[R,N]of n)if(R(I))return N??P}if(s.size>0){let R=s.get(Vf(C));if(typeof R<"u")return R??P}return P},f=t.reportInfo,p=t.reportWarning,h=t.reportError,E=function(C,S,P,I){switch(c(S,P,I)){case"info":f.call(C,S,P);break;case"warning":p.call(C,S??0,P);break;case"error":h.call(C,S??0,P);break}};t.reportInfo=function(...C){return E(this,...C,"info")},t.reportWarning=function(...C){return E(this,...C,"warning")},t.reportError=function(...C){return E(this,...C,"error")}}var UB,_B,dAe,VE,pt,Jk,c3,zk,u3,l3,yut,Wo,Kk,Eut,Xk,Qc=Ct(()=>{bt();UB=et(kE()),_B=et(Rd());Wt();dAe=et(Sa()),VE=et(vk());Zx();Yo();pt={NO_HINT:"NO_HINT",ID:"ID",NULL:"NULL",SCOPE:"SCOPE",NAME:"NAME",RANGE:"RANGE",REFERENCE:"REFERENCE",NUMBER:"NUMBER",STRING:"STRING",BOOLEAN:"BOOLEAN",PATH:"PATH",URL:"URL",ADDED:"ADDED",REMOVED:"REMOVED",CODE:"CODE",INSPECT:"INSPECT",DURATION:"DURATION",SIZE:"SIZE",SIZE_DIFF:"SIZE_DIFF",IDENT:"IDENT",DESCRIPTOR:"DESCRIPTOR",LOCATOR:"LOCATOR",RESOLUTION:"RESOLUTION",DEPENDENT:"DEPENDENT",PACKAGE_EXTENSION:"PACKAGE_EXTENSION",SETTING:"SETTING",MARKDOWN:"MARKDOWN",MARKDOWN_INLINE:"MARKDOWN_INLINE"},Jk=(e=>(e[e.BOLD=2]="BOLD",e))(Jk||{}),c3=_B.default.GITHUB_ACTIONS?{level:2}:UB.default.supportsColor?{level:UB.default.supportsColor.level}:{level:0},zk=c3.level!==0,u3=zk&&!_B.default.GITHUB_ACTIONS&&!_B.default.CIRCLE&&!_B.default.GITLAB,l3=new UB.default.Instance(c3),yut=new Map([[pt.NO_HINT,null],[pt.NULL,["#a853b5",129]],[pt.SCOPE,["#d75f00",166]],[pt.NAME,["#d7875f",173]],[pt.RANGE,["#00afaf",37]],[pt.REFERENCE,["#87afff",111]],[pt.NUMBER,["#ffd700",220]],[pt.STRING,["#b4bd68",32]],[pt.BOOLEAN,["#faa023",209]],[pt.PATH,["#d75fd7",170]],[pt.URL,["#d75fd7",170]],[pt.ADDED,["#5faf00",70]],[pt.REMOVED,["#ff3131",160]],[pt.CODE,["#87afff",111]],[pt.SIZE,["#ffd700",220]]]),Wo=t=>t;Kk={[pt.ID]:Wo({pretty:(t,e)=>typeof e=="number"?ri(t,`${e}`,pt.NUMBER):ri(t,e,pt.CODE),json:t=>t}),[pt.INSPECT]:Wo({pretty:(t,e)=>Vk(t,e),json:t=>t}),[pt.NUMBER]:Wo({pretty:(t,e)=>ri(t,`${e}`,pt.NUMBER),json:t=>t}),[pt.IDENT]:Wo({pretty:(t,e)=>es(t,e),json:t=>cn(t)}),[pt.LOCATOR]:Wo({pretty:(t,e)=>Yr(t,e),json:t=>cl(t)}),[pt.DESCRIPTOR]:Wo({pretty:(t,e)=>ni(t,e),json:t=>ll(t)}),[pt.RESOLUTION]:Wo({pretty:(t,{descriptor:e,locator:r})=>jB(t,e,r),json:({descriptor:t,locator:e})=>({descriptor:ll(t),locator:e!==null?cl(e):null})}),[pt.DEPENDENT]:Wo({pretty:(t,{locator:e,descriptor:r})=>h3(t,e,r),json:({locator:t,descriptor:e})=>({locator:cl(t),descriptor:ll(e)})}),[pt.PACKAGE_EXTENSION]:Wo({pretty:(t,e)=>{switch(e.type){case"Dependency":return`${es(t,e.parentDescriptor)} \u27A4 ${ri(t,"dependencies",pt.CODE)} \u27A4 ${es(t,e.descriptor)}`;case"PeerDependency":return`${es(t,e.parentDescriptor)} \u27A4 ${ri(t,"peerDependencies",pt.CODE)} \u27A4 ${es(t,e.descriptor)}`;case"PeerDependencyMeta":return`${es(t,e.parentDescriptor)} \u27A4 ${ri(t,"peerDependenciesMeta",pt.CODE)} \u27A4 ${es(t,Da(e.selector))} \u27A4 ${ri(t,e.key,pt.CODE)}`;default:throw new Error(`Assertion failed: Unsupported package extension type: ${e.type}`)}},json:t=>{switch(t.type){case"Dependency":return`${cn(t.parentDescriptor)} > ${cn(t.descriptor)}`;case"PeerDependency":return`${cn(t.parentDescriptor)} >> ${cn(t.descriptor)}`;case"PeerDependencyMeta":return`${cn(t.parentDescriptor)} >> ${t.selector} / ${t.key}`;default:throw new Error(`Assertion failed: Unsupported package extension type: ${t.type}`)}}}),[pt.SETTING]:Wo({pretty:(t,e)=>(t.get(e),KE(t,ri(t,e,pt.CODE),`https://yarnpkg.com/configuration/yarnrc#${e}`)),json:t=>t}),[pt.DURATION]:Wo({pretty:(t,e)=>{if(e>1e3*60){let r=Math.floor(e/1e3/60),s=Math.ceil((e-r*60*1e3)/1e3);return s===0?`${r}m`:`${r}m ${s}s`}else{let r=Math.floor(e/1e3),s=e-r*1e3;return s===0?`${r}s`:`${r}s ${s}ms`}},json:t=>t}),[pt.SIZE]:Wo({pretty:(t,e)=>ri(t,gAe(e),pt.NUMBER),json:t=>t}),[pt.SIZE_DIFF]:Wo({pretty:(t,e)=>{let r=e>=0?"+":"-",s=r==="+"?pt.REMOVED:pt.ADDED;return ri(t,`${r} ${gAe(Math.max(Math.abs(e),1))}`,s)},json:t=>t}),[pt.PATH]:Wo({pretty:(t,e)=>ri(t,ue.fromPortablePath(e),pt.PATH),json:t=>ue.fromPortablePath(t)}),[pt.MARKDOWN]:Wo({pretty:(t,{text:e,format:r,paragraphs:s})=>qo(e,{format:r,paragraphs:s}),json:({text:t})=>t}),[pt.MARKDOWN_INLINE]:Wo({pretty:(t,e)=>(e=e.replace(/(`+)((?:.|[\n])*?)\1/g,(r,s,a)=>Ut(t,s+a+s,pt.CODE)),e=e.replace(/(\*\*)((?:.|[\n])*?)\1/g,(r,s,a)=>Kd(t,a,2)),e),json:t=>t})};Eut=!!process.env.KONSOLE_VERSION;Xk=(a=>(a.Error="error",a.Warning="warning",a.Info="info",a.Discard="discard",a))(Xk||{})});var mAe=L(JE=>{"use strict";Object.defineProperty(JE,"__esModule",{value:!0});JE.splitWhen=JE.flatten=void 0;function Cut(t){return t.reduce((e,r)=>[].concat(e,r),[])}JE.flatten=Cut;function wut(t,e){let r=[[]],s=0;for(let a of t)e(a)?(s++,r[s]=[]):r[s].push(a);return r}JE.splitWhen=wut});var yAe=L($k=>{"use strict";Object.defineProperty($k,"__esModule",{value:!0});$k.isEnoentCodeError=void 0;function But(t){return t.code==="ENOENT"}$k.isEnoentCodeError=But});var EAe=L(eQ=>{"use strict";Object.defineProperty(eQ,"__esModule",{value:!0});eQ.createDirentFromStats=void 0;var g3=class{constructor(e,r){this.name=e,this.isBlockDevice=r.isBlockDevice.bind(r),this.isCharacterDevice=r.isCharacterDevice.bind(r),this.isDirectory=r.isDirectory.bind(r),this.isFIFO=r.isFIFO.bind(r),this.isFile=r.isFile.bind(r),this.isSocket=r.isSocket.bind(r),this.isSymbolicLink=r.isSymbolicLink.bind(r)}};function vut(t,e){return new g3(t,e)}eQ.createDirentFromStats=vut});var BAe=L(us=>{"use strict";Object.defineProperty(us,"__esModule",{value:!0});us.convertPosixPathToPattern=us.convertWindowsPathToPattern=us.convertPathToPattern=us.escapePosixPath=us.escapeWindowsPath=us.escape=us.removeLeadingDotSegment=us.makeAbsolute=us.unixify=void 0;var Sut=Ie("os"),Dut=Ie("path"),IAe=Sut.platform()==="win32",but=2,Put=/(\\?)([()*?[\]{|}]|^!|[!+@](?=\()|\\(?![!()*+?@[\]{|}]))/g,xut=/(\\?)([()[\]{}]|^!|[!+@](?=\())/g,kut=/^\\\\([.?])/,Qut=/\\(?![!()+@[\]{}])/g;function Tut(t){return t.replace(/\\/g,"/")}us.unixify=Tut;function Rut(t,e){return Dut.resolve(t,e)}us.makeAbsolute=Rut;function Fut(t){if(t.charAt(0)==="."){let e=t.charAt(1);if(e==="/"||e==="\\")return t.slice(but)}return t}us.removeLeadingDotSegment=Fut;us.escape=IAe?d3:m3;function d3(t){return t.replace(xut,"\\$2")}us.escapeWindowsPath=d3;function m3(t){return t.replace(Put,"\\$2")}us.escapePosixPath=m3;us.convertPathToPattern=IAe?CAe:wAe;function CAe(t){return d3(t).replace(kut,"//$1").replace(Qut,"/")}us.convertWindowsPathToPattern=CAe;function wAe(t){return m3(t)}us.convertPosixPathToPattern=wAe});var SAe=L((q7t,vAe)=>{vAe.exports=function(e){if(typeof e!="string"||e==="")return!1;for(var r;r=/(\\).|([@?!+*]\(.*\))/g.exec(e);){if(r[2])return!0;e=e.slice(r.index+r[0].length)}return!1}});var PAe=L((G7t,bAe)=>{var Nut=SAe(),DAe={"{":"}","(":")","[":"]"},Out=function(t){if(t[0]==="!")return!0;for(var e=0,r=-2,s=-2,a=-2,n=-2,c=-2;ee&&(c===-1||c>s||(c=t.indexOf("\\",e),c===-1||c>s)))||a!==-1&&t[e]==="{"&&t[e+1]!=="}"&&(a=t.indexOf("}",e),a>e&&(c=t.indexOf("\\",e),c===-1||c>a))||n!==-1&&t[e]==="("&&t[e+1]==="?"&&/[:!=]/.test(t[e+2])&&t[e+3]!==")"&&(n=t.indexOf(")",e),n>e&&(c=t.indexOf("\\",e),c===-1||c>n))||r!==-1&&t[e]==="("&&t[e+1]!=="|"&&(rr&&(c=t.indexOf("\\",r),c===-1||c>n))))return!0;if(t[e]==="\\"){var f=t[e+1];e+=2;var p=DAe[f];if(p){var h=t.indexOf(p,e);h!==-1&&(e=h+1)}if(t[e]==="!")return!0}else e++}return!1},Lut=function(t){if(t[0]==="!")return!0;for(var e=0;e{"use strict";var Mut=PAe(),_ut=Ie("path").posix.dirname,Uut=Ie("os").platform()==="win32",y3="/",Hut=/\\/g,jut=/[\{\[].*[\}\]]$/,qut=/(^|[^\\])([\{\[]|\([^\)]+$)/,Gut=/\\([\!\*\?\|\[\]\(\)\{\}])/g;xAe.exports=function(e,r){var s=Object.assign({flipBackslashes:!0},r);s.flipBackslashes&&Uut&&e.indexOf(y3)<0&&(e=e.replace(Hut,y3)),jut.test(e)&&(e+=y3),e+="a";do e=_ut(e);while(Mut(e)||qut.test(e));return e.replace(Gut,"$1")}});var MAe=L(jr=>{"use strict";Object.defineProperty(jr,"__esModule",{value:!0});jr.removeDuplicateSlashes=jr.matchAny=jr.convertPatternsToRe=jr.makeRe=jr.getPatternParts=jr.expandBraceExpansion=jr.expandPatternsWithBraceExpansion=jr.isAffectDepthOfReadingPattern=jr.endsWithSlashGlobStar=jr.hasGlobStar=jr.getBaseDirectory=jr.isPatternRelatedToParentDirectory=jr.getPatternsOutsideCurrentDirectory=jr.getPatternsInsideCurrentDirectory=jr.getPositivePatterns=jr.getNegativePatterns=jr.isPositivePattern=jr.isNegativePattern=jr.convertToNegativePattern=jr.convertToPositivePattern=jr.isDynamicPattern=jr.isStaticPattern=void 0;var Wut=Ie("path"),Yut=kAe(),E3=Sa(),QAe="**",Vut="\\",Kut=/[*?]|^!/,Jut=/\[[^[]*]/,zut=/(?:^|[^!*+?@])\([^(]*\|[^|]*\)/,Zut=/[!*+?@]\([^(]*\)/,Xut=/,|\.\./,$ut=/(?!^)\/{2,}/g;function TAe(t,e={}){return!RAe(t,e)}jr.isStaticPattern=TAe;function RAe(t,e={}){return t===""?!1:!!(e.caseSensitiveMatch===!1||t.includes(Vut)||Kut.test(t)||Jut.test(t)||zut.test(t)||e.extglob!==!1&&Zut.test(t)||e.braceExpansion!==!1&&eft(t))}jr.isDynamicPattern=RAe;function eft(t){let e=t.indexOf("{");if(e===-1)return!1;let r=t.indexOf("}",e+1);if(r===-1)return!1;let s=t.slice(e,r);return Xut.test(s)}function tft(t){return tQ(t)?t.slice(1):t}jr.convertToPositivePattern=tft;function rft(t){return"!"+t}jr.convertToNegativePattern=rft;function tQ(t){return t.startsWith("!")&&t[1]!=="("}jr.isNegativePattern=tQ;function FAe(t){return!tQ(t)}jr.isPositivePattern=FAe;function nft(t){return t.filter(tQ)}jr.getNegativePatterns=nft;function ift(t){return t.filter(FAe)}jr.getPositivePatterns=ift;function sft(t){return t.filter(e=>!I3(e))}jr.getPatternsInsideCurrentDirectory=sft;function oft(t){return t.filter(I3)}jr.getPatternsOutsideCurrentDirectory=oft;function I3(t){return t.startsWith("..")||t.startsWith("./..")}jr.isPatternRelatedToParentDirectory=I3;function aft(t){return Yut(t,{flipBackslashes:!1})}jr.getBaseDirectory=aft;function lft(t){return t.includes(QAe)}jr.hasGlobStar=lft;function NAe(t){return t.endsWith("/"+QAe)}jr.endsWithSlashGlobStar=NAe;function cft(t){let e=Wut.basename(t);return NAe(t)||TAe(e)}jr.isAffectDepthOfReadingPattern=cft;function uft(t){return t.reduce((e,r)=>e.concat(OAe(r)),[])}jr.expandPatternsWithBraceExpansion=uft;function OAe(t){let e=E3.braces(t,{expand:!0,nodupes:!0,keepEscaping:!0});return e.sort((r,s)=>r.length-s.length),e.filter(r=>r!=="")}jr.expandBraceExpansion=OAe;function fft(t,e){let{parts:r}=E3.scan(t,Object.assign(Object.assign({},e),{parts:!0}));return r.length===0&&(r=[t]),r[0].startsWith("/")&&(r[0]=r[0].slice(1),r.unshift("")),r}jr.getPatternParts=fft;function LAe(t,e){return E3.makeRe(t,e)}jr.makeRe=LAe;function Aft(t,e){return t.map(r=>LAe(r,e))}jr.convertPatternsToRe=Aft;function pft(t,e){return e.some(r=>r.test(t))}jr.matchAny=pft;function hft(t){return t.replace($ut,"/")}jr.removeDuplicateSlashes=hft});var jAe=L((V7t,HAe)=>{"use strict";var gft=Ie("stream"),_Ae=gft.PassThrough,dft=Array.prototype.slice;HAe.exports=mft;function mft(){let t=[],e=dft.call(arguments),r=!1,s=e[e.length-1];s&&!Array.isArray(s)&&s.pipe==null?e.pop():s={};let a=s.end!==!1,n=s.pipeError===!0;s.objectMode==null&&(s.objectMode=!0),s.highWaterMark==null&&(s.highWaterMark=64*1024);let c=_Ae(s);function f(){for(let E=0,C=arguments.length;E0||(r=!1,p())}function P(I){function R(){I.removeListener("merge2UnpipeEnd",R),I.removeListener("end",R),n&&I.removeListener("error",N),S()}function N(U){c.emit("error",U)}if(I._readableState.endEmitted)return S();I.on("merge2UnpipeEnd",R),I.on("end",R),n&&I.on("error",N),I.pipe(c,{end:!1}),I.resume()}for(let I=0;I{"use strict";Object.defineProperty(rQ,"__esModule",{value:!0});rQ.merge=void 0;var yft=jAe();function Eft(t){let e=yft(t);return t.forEach(r=>{r.once("error",s=>e.emit("error",s))}),e.once("close",()=>qAe(t)),e.once("end",()=>qAe(t)),e}rQ.merge=Eft;function qAe(t){t.forEach(e=>e.emit("close"))}});var WAe=L(zE=>{"use strict";Object.defineProperty(zE,"__esModule",{value:!0});zE.isEmpty=zE.isString=void 0;function Ift(t){return typeof t=="string"}zE.isString=Ift;function Cft(t){return t===""}zE.isEmpty=Cft});var Qp=L(Vo=>{"use strict";Object.defineProperty(Vo,"__esModule",{value:!0});Vo.string=Vo.stream=Vo.pattern=Vo.path=Vo.fs=Vo.errno=Vo.array=void 0;var wft=mAe();Vo.array=wft;var Bft=yAe();Vo.errno=Bft;var vft=EAe();Vo.fs=vft;var Sft=BAe();Vo.path=Sft;var Dft=MAe();Vo.pattern=Dft;var bft=GAe();Vo.stream=bft;var Pft=WAe();Vo.string=Pft});var JAe=L(Ko=>{"use strict";Object.defineProperty(Ko,"__esModule",{value:!0});Ko.convertPatternGroupToTask=Ko.convertPatternGroupsToTasks=Ko.groupPatternsByBaseDirectory=Ko.getNegativePatternsAsPositive=Ko.getPositivePatterns=Ko.convertPatternsToTasks=Ko.generate=void 0;var ju=Qp();function xft(t,e){let r=YAe(t,e),s=YAe(e.ignore,e),a=VAe(r),n=KAe(r,s),c=a.filter(E=>ju.pattern.isStaticPattern(E,e)),f=a.filter(E=>ju.pattern.isDynamicPattern(E,e)),p=C3(c,n,!1),h=C3(f,n,!0);return p.concat(h)}Ko.generate=xft;function YAe(t,e){let r=t;return e.braceExpansion&&(r=ju.pattern.expandPatternsWithBraceExpansion(r)),e.baseNameMatch&&(r=r.map(s=>s.includes("/")?s:`**/${s}`)),r.map(s=>ju.pattern.removeDuplicateSlashes(s))}function C3(t,e,r){let s=[],a=ju.pattern.getPatternsOutsideCurrentDirectory(t),n=ju.pattern.getPatternsInsideCurrentDirectory(t),c=w3(a),f=w3(n);return s.push(...B3(c,e,r)),"."in f?s.push(v3(".",n,e,r)):s.push(...B3(f,e,r)),s}Ko.convertPatternsToTasks=C3;function VAe(t){return ju.pattern.getPositivePatterns(t)}Ko.getPositivePatterns=VAe;function KAe(t,e){return ju.pattern.getNegativePatterns(t).concat(e).map(ju.pattern.convertToPositivePattern)}Ko.getNegativePatternsAsPositive=KAe;function w3(t){let e={};return t.reduce((r,s)=>{let a=ju.pattern.getBaseDirectory(s);return a in r?r[a].push(s):r[a]=[s],r},e)}Ko.groupPatternsByBaseDirectory=w3;function B3(t,e,r){return Object.keys(t).map(s=>v3(s,t[s],e,r))}Ko.convertPatternGroupsToTasks=B3;function v3(t,e,r,s){return{dynamic:s,positive:e,negative:r,base:t,patterns:[].concat(e,r.map(ju.pattern.convertToNegativePattern))}}Ko.convertPatternGroupToTask=v3});var ZAe=L(nQ=>{"use strict";Object.defineProperty(nQ,"__esModule",{value:!0});nQ.read=void 0;function kft(t,e,r){e.fs.lstat(t,(s,a)=>{if(s!==null){zAe(r,s);return}if(!a.isSymbolicLink()||!e.followSymbolicLink){S3(r,a);return}e.fs.stat(t,(n,c)=>{if(n!==null){if(e.throwErrorOnBrokenSymbolicLink){zAe(r,n);return}S3(r,a);return}e.markSymbolicLink&&(c.isSymbolicLink=()=>!0),S3(r,c)})})}nQ.read=kft;function zAe(t,e){t(e)}function S3(t,e){t(null,e)}});var XAe=L(iQ=>{"use strict";Object.defineProperty(iQ,"__esModule",{value:!0});iQ.read=void 0;function Qft(t,e){let r=e.fs.lstatSync(t);if(!r.isSymbolicLink()||!e.followSymbolicLink)return r;try{let s=e.fs.statSync(t);return e.markSymbolicLink&&(s.isSymbolicLink=()=>!0),s}catch(s){if(!e.throwErrorOnBrokenSymbolicLink)return r;throw s}}iQ.read=Qft});var $Ae=L(p0=>{"use strict";Object.defineProperty(p0,"__esModule",{value:!0});p0.createFileSystemAdapter=p0.FILE_SYSTEM_ADAPTER=void 0;var sQ=Ie("fs");p0.FILE_SYSTEM_ADAPTER={lstat:sQ.lstat,stat:sQ.stat,lstatSync:sQ.lstatSync,statSync:sQ.statSync};function Tft(t){return t===void 0?p0.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},p0.FILE_SYSTEM_ADAPTER),t)}p0.createFileSystemAdapter=Tft});var epe=L(b3=>{"use strict";Object.defineProperty(b3,"__esModule",{value:!0});var Rft=$Ae(),D3=class{constructor(e={}){this._options=e,this.followSymbolicLink=this._getValue(this._options.followSymbolicLink,!0),this.fs=Rft.createFileSystemAdapter(this._options.fs),this.markSymbolicLink=this._getValue(this._options.markSymbolicLink,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0)}_getValue(e,r){return e??r}};b3.default=D3});var zd=L(h0=>{"use strict";Object.defineProperty(h0,"__esModule",{value:!0});h0.statSync=h0.stat=h0.Settings=void 0;var tpe=ZAe(),Fft=XAe(),P3=epe();h0.Settings=P3.default;function Nft(t,e,r){if(typeof e=="function"){tpe.read(t,x3(),e);return}tpe.read(t,x3(e),r)}h0.stat=Nft;function Oft(t,e){let r=x3(e);return Fft.read(t,r)}h0.statSync=Oft;function x3(t={}){return t instanceof P3.default?t:new P3.default(t)}});var ipe=L((nKt,npe)=>{var rpe;npe.exports=typeof queueMicrotask=="function"?queueMicrotask.bind(typeof window<"u"?window:global):t=>(rpe||(rpe=Promise.resolve())).then(t).catch(e=>setTimeout(()=>{throw e},0))});var ope=L((iKt,spe)=>{spe.exports=Mft;var Lft=ipe();function Mft(t,e){let r,s,a,n=!0;Array.isArray(t)?(r=[],s=t.length):(a=Object.keys(t),r={},s=a.length);function c(p){function h(){e&&e(p,r),e=null}n?Lft(h):h()}function f(p,h,E){r[p]=E,(--s===0||h)&&c(h)}s?a?a.forEach(function(p){t[p](function(h,E){f(p,h,E)})}):t.forEach(function(p,h){p(function(E,C){f(h,E,C)})}):c(null),n=!1}});var k3=L(aQ=>{"use strict";Object.defineProperty(aQ,"__esModule",{value:!0});aQ.IS_SUPPORT_READDIR_WITH_FILE_TYPES=void 0;var oQ=process.versions.node.split(".");if(oQ[0]===void 0||oQ[1]===void 0)throw new Error(`Unexpected behavior. The 'process.versions.node' variable has invalid value: ${process.versions.node}`);var ape=Number.parseInt(oQ[0],10),_ft=Number.parseInt(oQ[1],10),lpe=10,Uft=10,Hft=ape>lpe,jft=ape===lpe&&_ft>=Uft;aQ.IS_SUPPORT_READDIR_WITH_FILE_TYPES=Hft||jft});var cpe=L(lQ=>{"use strict";Object.defineProperty(lQ,"__esModule",{value:!0});lQ.createDirentFromStats=void 0;var Q3=class{constructor(e,r){this.name=e,this.isBlockDevice=r.isBlockDevice.bind(r),this.isCharacterDevice=r.isCharacterDevice.bind(r),this.isDirectory=r.isDirectory.bind(r),this.isFIFO=r.isFIFO.bind(r),this.isFile=r.isFile.bind(r),this.isSocket=r.isSocket.bind(r),this.isSymbolicLink=r.isSymbolicLink.bind(r)}};function qft(t,e){return new Q3(t,e)}lQ.createDirentFromStats=qft});var T3=L(cQ=>{"use strict";Object.defineProperty(cQ,"__esModule",{value:!0});cQ.fs=void 0;var Gft=cpe();cQ.fs=Gft});var R3=L(uQ=>{"use strict";Object.defineProperty(uQ,"__esModule",{value:!0});uQ.joinPathSegments=void 0;function Wft(t,e,r){return t.endsWith(r)?t+e:t+r+e}uQ.joinPathSegments=Wft});var gpe=L(g0=>{"use strict";Object.defineProperty(g0,"__esModule",{value:!0});g0.readdir=g0.readdirWithFileTypes=g0.read=void 0;var Yft=zd(),upe=ope(),Vft=k3(),fpe=T3(),Ape=R3();function Kft(t,e,r){if(!e.stats&&Vft.IS_SUPPORT_READDIR_WITH_FILE_TYPES){ppe(t,e,r);return}hpe(t,e,r)}g0.read=Kft;function ppe(t,e,r){e.fs.readdir(t,{withFileTypes:!0},(s,a)=>{if(s!==null){fQ(r,s);return}let n=a.map(f=>({dirent:f,name:f.name,path:Ape.joinPathSegments(t,f.name,e.pathSegmentSeparator)}));if(!e.followSymbolicLinks){F3(r,n);return}let c=n.map(f=>Jft(f,e));upe(c,(f,p)=>{if(f!==null){fQ(r,f);return}F3(r,p)})})}g0.readdirWithFileTypes=ppe;function Jft(t,e){return r=>{if(!t.dirent.isSymbolicLink()){r(null,t);return}e.fs.stat(t.path,(s,a)=>{if(s!==null){if(e.throwErrorOnBrokenSymbolicLink){r(s);return}r(null,t);return}t.dirent=fpe.fs.createDirentFromStats(t.name,a),r(null,t)})}}function hpe(t,e,r){e.fs.readdir(t,(s,a)=>{if(s!==null){fQ(r,s);return}let n=a.map(c=>{let f=Ape.joinPathSegments(t,c,e.pathSegmentSeparator);return p=>{Yft.stat(f,e.fsStatSettings,(h,E)=>{if(h!==null){p(h);return}let C={name:c,path:f,dirent:fpe.fs.createDirentFromStats(c,E)};e.stats&&(C.stats=E),p(null,C)})}});upe(n,(c,f)=>{if(c!==null){fQ(r,c);return}F3(r,f)})})}g0.readdir=hpe;function fQ(t,e){t(e)}function F3(t,e){t(null,e)}});var Ipe=L(d0=>{"use strict";Object.defineProperty(d0,"__esModule",{value:!0});d0.readdir=d0.readdirWithFileTypes=d0.read=void 0;var zft=zd(),Zft=k3(),dpe=T3(),mpe=R3();function Xft(t,e){return!e.stats&&Zft.IS_SUPPORT_READDIR_WITH_FILE_TYPES?ype(t,e):Epe(t,e)}d0.read=Xft;function ype(t,e){return e.fs.readdirSync(t,{withFileTypes:!0}).map(s=>{let a={dirent:s,name:s.name,path:mpe.joinPathSegments(t,s.name,e.pathSegmentSeparator)};if(a.dirent.isSymbolicLink()&&e.followSymbolicLinks)try{let n=e.fs.statSync(a.path);a.dirent=dpe.fs.createDirentFromStats(a.name,n)}catch(n){if(e.throwErrorOnBrokenSymbolicLink)throw n}return a})}d0.readdirWithFileTypes=ype;function Epe(t,e){return e.fs.readdirSync(t).map(s=>{let a=mpe.joinPathSegments(t,s,e.pathSegmentSeparator),n=zft.statSync(a,e.fsStatSettings),c={name:s,path:a,dirent:dpe.fs.createDirentFromStats(s,n)};return e.stats&&(c.stats=n),c})}d0.readdir=Epe});var Cpe=L(m0=>{"use strict";Object.defineProperty(m0,"__esModule",{value:!0});m0.createFileSystemAdapter=m0.FILE_SYSTEM_ADAPTER=void 0;var ZE=Ie("fs");m0.FILE_SYSTEM_ADAPTER={lstat:ZE.lstat,stat:ZE.stat,lstatSync:ZE.lstatSync,statSync:ZE.statSync,readdir:ZE.readdir,readdirSync:ZE.readdirSync};function $ft(t){return t===void 0?m0.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},m0.FILE_SYSTEM_ADAPTER),t)}m0.createFileSystemAdapter=$ft});var wpe=L(O3=>{"use strict";Object.defineProperty(O3,"__esModule",{value:!0});var eAt=Ie("path"),tAt=zd(),rAt=Cpe(),N3=class{constructor(e={}){this._options=e,this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!1),this.fs=rAt.createFileSystemAdapter(this._options.fs),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,eAt.sep),this.stats=this._getValue(this._options.stats,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0),this.fsStatSettings=new tAt.Settings({followSymbolicLink:this.followSymbolicLinks,fs:this.fs,throwErrorOnBrokenSymbolicLink:this.throwErrorOnBrokenSymbolicLink})}_getValue(e,r){return e??r}};O3.default=N3});var AQ=L(y0=>{"use strict";Object.defineProperty(y0,"__esModule",{value:!0});y0.Settings=y0.scandirSync=y0.scandir=void 0;var Bpe=gpe(),nAt=Ipe(),L3=wpe();y0.Settings=L3.default;function iAt(t,e,r){if(typeof e=="function"){Bpe.read(t,M3(),e);return}Bpe.read(t,M3(e),r)}y0.scandir=iAt;function sAt(t,e){let r=M3(e);return nAt.read(t,r)}y0.scandirSync=sAt;function M3(t={}){return t instanceof L3.default?t:new L3.default(t)}});var Spe=L((hKt,vpe)=>{"use strict";function oAt(t){var e=new t,r=e;function s(){var n=e;return n.next?e=n.next:(e=new t,r=e),n.next=null,n}function a(n){r.next=n,r=n}return{get:s,release:a}}vpe.exports=oAt});var bpe=L((gKt,_3)=>{"use strict";var aAt=Spe();function Dpe(t,e,r){if(typeof t=="function"&&(r=e,e=t,t=null),!(r>=1))throw new Error("fastqueue concurrency must be equal to or greater than 1");var s=aAt(lAt),a=null,n=null,c=0,f=null,p={push:R,drain:Tc,saturated:Tc,pause:E,paused:!1,get concurrency(){return r},set concurrency(Ae){if(!(Ae>=1))throw new Error("fastqueue concurrency must be equal to or greater than 1");if(r=Ae,!p.paused)for(;a&&c=r||p.paused?n?(n.next=me,n=me):(a=me,n=me,p.saturated()):(c++,e.call(t,me.value,me.worked))}function N(Ae,ce){var me=s.get();me.context=t,me.release=U,me.value=Ae,me.callback=ce||Tc,me.errorHandler=f,c>=r||p.paused?a?(me.next=a,a=me):(a=me,n=me,p.saturated()):(c++,e.call(t,me.value,me.worked))}function U(Ae){Ae&&s.release(Ae);var ce=a;ce&&c<=r?p.paused?c--:(n===a&&(n=null),a=ce.next,ce.next=null,e.call(t,ce.value,ce.worked),n===null&&p.empty()):--c===0&&p.drain()}function W(){a=null,n=null,p.drain=Tc}function te(){a=null,n=null,p.drain(),p.drain=Tc}function ie(Ae){f=Ae}}function Tc(){}function lAt(){this.value=null,this.callback=Tc,this.next=null,this.release=Tc,this.context=null,this.errorHandler=null;var t=this;this.worked=function(r,s){var a=t.callback,n=t.errorHandler,c=t.value;t.value=null,t.callback=Tc,t.errorHandler&&n(r,c),a.call(t.context,r,s),t.release(t)}}function cAt(t,e,r){typeof t=="function"&&(r=e,e=t,t=null);function s(E,C){e.call(this,E).then(function(S){C(null,S)},C)}var a=Dpe(t,s,r),n=a.push,c=a.unshift;return a.push=f,a.unshift=p,a.drained=h,a;function f(E){var C=new Promise(function(S,P){n(E,function(I,R){if(I){P(I);return}S(R)})});return C.catch(Tc),C}function p(E){var C=new Promise(function(S,P){c(E,function(I,R){if(I){P(I);return}S(R)})});return C.catch(Tc),C}function h(){if(a.idle())return new Promise(function(S){S()});var E=a.drain,C=new Promise(function(S){a.drain=function(){E(),S()}});return C}}_3.exports=Dpe;_3.exports.promise=cAt});var pQ=L(Xf=>{"use strict";Object.defineProperty(Xf,"__esModule",{value:!0});Xf.joinPathSegments=Xf.replacePathSegmentSeparator=Xf.isAppliedFilter=Xf.isFatalError=void 0;function uAt(t,e){return t.errorFilter===null?!0:!t.errorFilter(e)}Xf.isFatalError=uAt;function fAt(t,e){return t===null||t(e)}Xf.isAppliedFilter=fAt;function AAt(t,e){return t.split(/[/\\]/).join(e)}Xf.replacePathSegmentSeparator=AAt;function pAt(t,e,r){return t===""?e:t.endsWith(r)?t+e:t+r+e}Xf.joinPathSegments=pAt});var j3=L(H3=>{"use strict";Object.defineProperty(H3,"__esModule",{value:!0});var hAt=pQ(),U3=class{constructor(e,r){this._root=e,this._settings=r,this._root=hAt.replacePathSegmentSeparator(e,r.pathSegmentSeparator)}};H3.default=U3});var W3=L(G3=>{"use strict";Object.defineProperty(G3,"__esModule",{value:!0});var gAt=Ie("events"),dAt=AQ(),mAt=bpe(),hQ=pQ(),yAt=j3(),q3=class extends yAt.default{constructor(e,r){super(e,r),this._settings=r,this._scandir=dAt.scandir,this._emitter=new gAt.EventEmitter,this._queue=mAt(this._worker.bind(this),this._settings.concurrency),this._isFatalError=!1,this._isDestroyed=!1,this._queue.drain=()=>{this._isFatalError||this._emitter.emit("end")}}read(){return this._isFatalError=!1,this._isDestroyed=!1,setImmediate(()=>{this._pushToQueue(this._root,this._settings.basePath)}),this._emitter}get isDestroyed(){return this._isDestroyed}destroy(){if(this._isDestroyed)throw new Error("The reader is already destroyed");this._isDestroyed=!0,this._queue.killAndDrain()}onEntry(e){this._emitter.on("entry",e)}onError(e){this._emitter.once("error",e)}onEnd(e){this._emitter.once("end",e)}_pushToQueue(e,r){let s={directory:e,base:r};this._queue.push(s,a=>{a!==null&&this._handleError(a)})}_worker(e,r){this._scandir(e.directory,this._settings.fsScandirSettings,(s,a)=>{if(s!==null){r(s,void 0);return}for(let n of a)this._handleEntry(n,e.base);r(null,void 0)})}_handleError(e){this._isDestroyed||!hQ.isFatalError(this._settings,e)||(this._isFatalError=!0,this._isDestroyed=!0,this._emitter.emit("error",e))}_handleEntry(e,r){if(this._isDestroyed||this._isFatalError)return;let s=e.path;r!==void 0&&(e.path=hQ.joinPathSegments(r,e.name,this._settings.pathSegmentSeparator)),hQ.isAppliedFilter(this._settings.entryFilter,e)&&this._emitEntry(e),e.dirent.isDirectory()&&hQ.isAppliedFilter(this._settings.deepFilter,e)&&this._pushToQueue(s,r===void 0?void 0:e.path)}_emitEntry(e){this._emitter.emit("entry",e)}};G3.default=q3});var Ppe=L(V3=>{"use strict";Object.defineProperty(V3,"__esModule",{value:!0});var EAt=W3(),Y3=class{constructor(e,r){this._root=e,this._settings=r,this._reader=new EAt.default(this._root,this._settings),this._storage=[]}read(e){this._reader.onError(r=>{IAt(e,r)}),this._reader.onEntry(r=>{this._storage.push(r)}),this._reader.onEnd(()=>{CAt(e,this._storage)}),this._reader.read()}};V3.default=Y3;function IAt(t,e){t(e)}function CAt(t,e){t(null,e)}});var xpe=L(J3=>{"use strict";Object.defineProperty(J3,"__esModule",{value:!0});var wAt=Ie("stream"),BAt=W3(),K3=class{constructor(e,r){this._root=e,this._settings=r,this._reader=new BAt.default(this._root,this._settings),this._stream=new wAt.Readable({objectMode:!0,read:()=>{},destroy:()=>{this._reader.isDestroyed||this._reader.destroy()}})}read(){return this._reader.onError(e=>{this._stream.emit("error",e)}),this._reader.onEntry(e=>{this._stream.push(e)}),this._reader.onEnd(()=>{this._stream.push(null)}),this._reader.read(),this._stream}};J3.default=K3});var kpe=L(Z3=>{"use strict";Object.defineProperty(Z3,"__esModule",{value:!0});var vAt=AQ(),gQ=pQ(),SAt=j3(),z3=class extends SAt.default{constructor(){super(...arguments),this._scandir=vAt.scandirSync,this._storage=[],this._queue=new Set}read(){return this._pushToQueue(this._root,this._settings.basePath),this._handleQueue(),this._storage}_pushToQueue(e,r){this._queue.add({directory:e,base:r})}_handleQueue(){for(let e of this._queue.values())this._handleDirectory(e.directory,e.base)}_handleDirectory(e,r){try{let s=this._scandir(e,this._settings.fsScandirSettings);for(let a of s)this._handleEntry(a,r)}catch(s){this._handleError(s)}}_handleError(e){if(gQ.isFatalError(this._settings,e))throw e}_handleEntry(e,r){let s=e.path;r!==void 0&&(e.path=gQ.joinPathSegments(r,e.name,this._settings.pathSegmentSeparator)),gQ.isAppliedFilter(this._settings.entryFilter,e)&&this._pushToStorage(e),e.dirent.isDirectory()&&gQ.isAppliedFilter(this._settings.deepFilter,e)&&this._pushToQueue(s,r===void 0?void 0:e.path)}_pushToStorage(e){this._storage.push(e)}};Z3.default=z3});var Qpe=L($3=>{"use strict";Object.defineProperty($3,"__esModule",{value:!0});var DAt=kpe(),X3=class{constructor(e,r){this._root=e,this._settings=r,this._reader=new DAt.default(this._root,this._settings)}read(){return this._reader.read()}};$3.default=X3});var Tpe=L(t8=>{"use strict";Object.defineProperty(t8,"__esModule",{value:!0});var bAt=Ie("path"),PAt=AQ(),e8=class{constructor(e={}){this._options=e,this.basePath=this._getValue(this._options.basePath,void 0),this.concurrency=this._getValue(this._options.concurrency,Number.POSITIVE_INFINITY),this.deepFilter=this._getValue(this._options.deepFilter,null),this.entryFilter=this._getValue(this._options.entryFilter,null),this.errorFilter=this._getValue(this._options.errorFilter,null),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,bAt.sep),this.fsScandirSettings=new PAt.Settings({followSymbolicLinks:this._options.followSymbolicLinks,fs:this._options.fs,pathSegmentSeparator:this._options.pathSegmentSeparator,stats:this._options.stats,throwErrorOnBrokenSymbolicLink:this._options.throwErrorOnBrokenSymbolicLink})}_getValue(e,r){return e??r}};t8.default=e8});var mQ=L($f=>{"use strict";Object.defineProperty($f,"__esModule",{value:!0});$f.Settings=$f.walkStream=$f.walkSync=$f.walk=void 0;var Rpe=Ppe(),xAt=xpe(),kAt=Qpe(),r8=Tpe();$f.Settings=r8.default;function QAt(t,e,r){if(typeof e=="function"){new Rpe.default(t,dQ()).read(e);return}new Rpe.default(t,dQ(e)).read(r)}$f.walk=QAt;function TAt(t,e){let r=dQ(e);return new kAt.default(t,r).read()}$f.walkSync=TAt;function RAt(t,e){let r=dQ(e);return new xAt.default(t,r).read()}$f.walkStream=RAt;function dQ(t={}){return t instanceof r8.default?t:new r8.default(t)}});var yQ=L(i8=>{"use strict";Object.defineProperty(i8,"__esModule",{value:!0});var FAt=Ie("path"),NAt=zd(),Fpe=Qp(),n8=class{constructor(e){this._settings=e,this._fsStatSettings=new NAt.Settings({followSymbolicLink:this._settings.followSymbolicLinks,fs:this._settings.fs,throwErrorOnBrokenSymbolicLink:this._settings.followSymbolicLinks})}_getFullEntryPath(e){return FAt.resolve(this._settings.cwd,e)}_makeEntry(e,r){let s={name:r,path:r,dirent:Fpe.fs.createDirentFromStats(r,e)};return this._settings.stats&&(s.stats=e),s}_isFatalError(e){return!Fpe.errno.isEnoentCodeError(e)&&!this._settings.suppressErrors}};i8.default=n8});var a8=L(o8=>{"use strict";Object.defineProperty(o8,"__esModule",{value:!0});var OAt=Ie("stream"),LAt=zd(),MAt=mQ(),_At=yQ(),s8=class extends _At.default{constructor(){super(...arguments),this._walkStream=MAt.walkStream,this._stat=LAt.stat}dynamic(e,r){return this._walkStream(e,r)}static(e,r){let s=e.map(this._getFullEntryPath,this),a=new OAt.PassThrough({objectMode:!0});a._write=(n,c,f)=>this._getEntry(s[n],e[n],r).then(p=>{p!==null&&r.entryFilter(p)&&a.push(p),n===s.length-1&&a.end(),f()}).catch(f);for(let n=0;nthis._makeEntry(a,r)).catch(a=>{if(s.errorFilter(a))return null;throw a})}_getStat(e){return new Promise((r,s)=>{this._stat(e,this._fsStatSettings,(a,n)=>a===null?r(n):s(a))})}};o8.default=s8});var Npe=L(c8=>{"use strict";Object.defineProperty(c8,"__esModule",{value:!0});var UAt=mQ(),HAt=yQ(),jAt=a8(),l8=class extends HAt.default{constructor(){super(...arguments),this._walkAsync=UAt.walk,this._readerStream=new jAt.default(this._settings)}dynamic(e,r){return new Promise((s,a)=>{this._walkAsync(e,r,(n,c)=>{n===null?s(c):a(n)})})}async static(e,r){let s=[],a=this._readerStream.static(e,r);return new Promise((n,c)=>{a.once("error",c),a.on("data",f=>s.push(f)),a.once("end",()=>n(s))})}};c8.default=l8});var Ope=L(f8=>{"use strict";Object.defineProperty(f8,"__esModule",{value:!0});var qB=Qp(),u8=class{constructor(e,r,s){this._patterns=e,this._settings=r,this._micromatchOptions=s,this._storage=[],this._fillStorage()}_fillStorage(){for(let e of this._patterns){let r=this._getPatternSegments(e),s=this._splitSegmentsIntoSections(r);this._storage.push({complete:s.length<=1,pattern:e,segments:r,sections:s})}}_getPatternSegments(e){return qB.pattern.getPatternParts(e,this._micromatchOptions).map(s=>qB.pattern.isDynamicPattern(s,this._settings)?{dynamic:!0,pattern:s,patternRe:qB.pattern.makeRe(s,this._micromatchOptions)}:{dynamic:!1,pattern:s})}_splitSegmentsIntoSections(e){return qB.array.splitWhen(e,r=>r.dynamic&&qB.pattern.hasGlobStar(r.pattern))}};f8.default=u8});var Lpe=L(p8=>{"use strict";Object.defineProperty(p8,"__esModule",{value:!0});var qAt=Ope(),A8=class extends qAt.default{match(e){let r=e.split("/"),s=r.length,a=this._storage.filter(n=>!n.complete||n.segments.length>s);for(let n of a){let c=n.sections[0];if(!n.complete&&s>c.length||r.every((p,h)=>{let E=n.segments[h];return!!(E.dynamic&&E.patternRe.test(p)||!E.dynamic&&E.pattern===p)}))return!0}return!1}};p8.default=A8});var Mpe=L(g8=>{"use strict";Object.defineProperty(g8,"__esModule",{value:!0});var EQ=Qp(),GAt=Lpe(),h8=class{constructor(e,r){this._settings=e,this._micromatchOptions=r}getFilter(e,r,s){let a=this._getMatcher(r),n=this._getNegativePatternsRe(s);return c=>this._filter(e,c,a,n)}_getMatcher(e){return new GAt.default(e,this._settings,this._micromatchOptions)}_getNegativePatternsRe(e){let r=e.filter(EQ.pattern.isAffectDepthOfReadingPattern);return EQ.pattern.convertPatternsToRe(r,this._micromatchOptions)}_filter(e,r,s,a){if(this._isSkippedByDeep(e,r.path)||this._isSkippedSymbolicLink(r))return!1;let n=EQ.path.removeLeadingDotSegment(r.path);return this._isSkippedByPositivePatterns(n,s)?!1:this._isSkippedByNegativePatterns(n,a)}_isSkippedByDeep(e,r){return this._settings.deep===1/0?!1:this._getEntryLevel(e,r)>=this._settings.deep}_getEntryLevel(e,r){let s=r.split("/").length;if(e==="")return s;let a=e.split("/").length;return s-a}_isSkippedSymbolicLink(e){return!this._settings.followSymbolicLinks&&e.dirent.isSymbolicLink()}_isSkippedByPositivePatterns(e,r){return!this._settings.baseNameMatch&&!r.match(e)}_isSkippedByNegativePatterns(e,r){return!EQ.pattern.matchAny(e,r)}};g8.default=h8});var _pe=L(m8=>{"use strict";Object.defineProperty(m8,"__esModule",{value:!0});var Zd=Qp(),d8=class{constructor(e,r){this._settings=e,this._micromatchOptions=r,this.index=new Map}getFilter(e,r){let s=Zd.pattern.convertPatternsToRe(e,this._micromatchOptions),a=Zd.pattern.convertPatternsToRe(r,Object.assign(Object.assign({},this._micromatchOptions),{dot:!0}));return n=>this._filter(n,s,a)}_filter(e,r,s){let a=Zd.path.removeLeadingDotSegment(e.path);if(this._settings.unique&&this._isDuplicateEntry(a)||this._onlyFileFilter(e)||this._onlyDirectoryFilter(e)||this._isSkippedByAbsoluteNegativePatterns(a,s))return!1;let n=e.dirent.isDirectory(),c=this._isMatchToPatterns(a,r,n)&&!this._isMatchToPatterns(a,s,n);return this._settings.unique&&c&&this._createIndexRecord(a),c}_isDuplicateEntry(e){return this.index.has(e)}_createIndexRecord(e){this.index.set(e,void 0)}_onlyFileFilter(e){return this._settings.onlyFiles&&!e.dirent.isFile()}_onlyDirectoryFilter(e){return this._settings.onlyDirectories&&!e.dirent.isDirectory()}_isSkippedByAbsoluteNegativePatterns(e,r){if(!this._settings.absolute)return!1;let s=Zd.path.makeAbsolute(this._settings.cwd,e);return Zd.pattern.matchAny(s,r)}_isMatchToPatterns(e,r,s){let a=Zd.pattern.matchAny(e,r);return!a&&s?Zd.pattern.matchAny(e+"/",r):a}};m8.default=d8});var Upe=L(E8=>{"use strict";Object.defineProperty(E8,"__esModule",{value:!0});var WAt=Qp(),y8=class{constructor(e){this._settings=e}getFilter(){return e=>this._isNonFatalError(e)}_isNonFatalError(e){return WAt.errno.isEnoentCodeError(e)||this._settings.suppressErrors}};E8.default=y8});var jpe=L(C8=>{"use strict";Object.defineProperty(C8,"__esModule",{value:!0});var Hpe=Qp(),I8=class{constructor(e){this._settings=e}getTransformer(){return e=>this._transform(e)}_transform(e){let r=e.path;return this._settings.absolute&&(r=Hpe.path.makeAbsolute(this._settings.cwd,r),r=Hpe.path.unixify(r)),this._settings.markDirectories&&e.dirent.isDirectory()&&(r+="/"),this._settings.objectMode?Object.assign(Object.assign({},e),{path:r}):r}};C8.default=I8});var IQ=L(B8=>{"use strict";Object.defineProperty(B8,"__esModule",{value:!0});var YAt=Ie("path"),VAt=Mpe(),KAt=_pe(),JAt=Upe(),zAt=jpe(),w8=class{constructor(e){this._settings=e,this.errorFilter=new JAt.default(this._settings),this.entryFilter=new KAt.default(this._settings,this._getMicromatchOptions()),this.deepFilter=new VAt.default(this._settings,this._getMicromatchOptions()),this.entryTransformer=new zAt.default(this._settings)}_getRootDirectory(e){return YAt.resolve(this._settings.cwd,e.base)}_getReaderOptions(e){let r=e.base==="."?"":e.base;return{basePath:r,pathSegmentSeparator:"/",concurrency:this._settings.concurrency,deepFilter:this.deepFilter.getFilter(r,e.positive,e.negative),entryFilter:this.entryFilter.getFilter(e.positive,e.negative),errorFilter:this.errorFilter.getFilter(),followSymbolicLinks:this._settings.followSymbolicLinks,fs:this._settings.fs,stats:this._settings.stats,throwErrorOnBrokenSymbolicLink:this._settings.throwErrorOnBrokenSymbolicLink,transform:this.entryTransformer.getTransformer()}}_getMicromatchOptions(){return{dot:this._settings.dot,matchBase:this._settings.baseNameMatch,nobrace:!this._settings.braceExpansion,nocase:!this._settings.caseSensitiveMatch,noext:!this._settings.extglob,noglobstar:!this._settings.globstar,posix:!0,strictSlashes:!1}}};B8.default=w8});var qpe=L(S8=>{"use strict";Object.defineProperty(S8,"__esModule",{value:!0});var ZAt=Npe(),XAt=IQ(),v8=class extends XAt.default{constructor(){super(...arguments),this._reader=new ZAt.default(this._settings)}async read(e){let r=this._getRootDirectory(e),s=this._getReaderOptions(e);return(await this.api(r,e,s)).map(n=>s.transform(n))}api(e,r,s){return r.dynamic?this._reader.dynamic(e,s):this._reader.static(r.patterns,s)}};S8.default=v8});var Gpe=L(b8=>{"use strict";Object.defineProperty(b8,"__esModule",{value:!0});var $At=Ie("stream"),ept=a8(),tpt=IQ(),D8=class extends tpt.default{constructor(){super(...arguments),this._reader=new ept.default(this._settings)}read(e){let r=this._getRootDirectory(e),s=this._getReaderOptions(e),a=this.api(r,e,s),n=new $At.Readable({objectMode:!0,read:()=>{}});return a.once("error",c=>n.emit("error",c)).on("data",c=>n.emit("data",s.transform(c))).once("end",()=>n.emit("end")),n.once("close",()=>a.destroy()),n}api(e,r,s){return r.dynamic?this._reader.dynamic(e,s):this._reader.static(r.patterns,s)}};b8.default=D8});var Wpe=L(x8=>{"use strict";Object.defineProperty(x8,"__esModule",{value:!0});var rpt=zd(),npt=mQ(),ipt=yQ(),P8=class extends ipt.default{constructor(){super(...arguments),this._walkSync=npt.walkSync,this._statSync=rpt.statSync}dynamic(e,r){return this._walkSync(e,r)}static(e,r){let s=[];for(let a of e){let n=this._getFullEntryPath(a),c=this._getEntry(n,a,r);c===null||!r.entryFilter(c)||s.push(c)}return s}_getEntry(e,r,s){try{let a=this._getStat(e);return this._makeEntry(a,r)}catch(a){if(s.errorFilter(a))return null;throw a}}_getStat(e){return this._statSync(e,this._fsStatSettings)}};x8.default=P8});var Ype=L(Q8=>{"use strict";Object.defineProperty(Q8,"__esModule",{value:!0});var spt=Wpe(),opt=IQ(),k8=class extends opt.default{constructor(){super(...arguments),this._reader=new spt.default(this._settings)}read(e){let r=this._getRootDirectory(e),s=this._getReaderOptions(e);return this.api(r,e,s).map(s.transform)}api(e,r,s){return r.dynamic?this._reader.dynamic(e,s):this._reader.static(r.patterns,s)}};Q8.default=k8});var Vpe=L($E=>{"use strict";Object.defineProperty($E,"__esModule",{value:!0});$E.DEFAULT_FILE_SYSTEM_ADAPTER=void 0;var XE=Ie("fs"),apt=Ie("os"),lpt=Math.max(apt.cpus().length,1);$E.DEFAULT_FILE_SYSTEM_ADAPTER={lstat:XE.lstat,lstatSync:XE.lstatSync,stat:XE.stat,statSync:XE.statSync,readdir:XE.readdir,readdirSync:XE.readdirSync};var T8=class{constructor(e={}){this._options=e,this.absolute=this._getValue(this._options.absolute,!1),this.baseNameMatch=this._getValue(this._options.baseNameMatch,!1),this.braceExpansion=this._getValue(this._options.braceExpansion,!0),this.caseSensitiveMatch=this._getValue(this._options.caseSensitiveMatch,!0),this.concurrency=this._getValue(this._options.concurrency,lpt),this.cwd=this._getValue(this._options.cwd,process.cwd()),this.deep=this._getValue(this._options.deep,1/0),this.dot=this._getValue(this._options.dot,!1),this.extglob=this._getValue(this._options.extglob,!0),this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!0),this.fs=this._getFileSystemMethods(this._options.fs),this.globstar=this._getValue(this._options.globstar,!0),this.ignore=this._getValue(this._options.ignore,[]),this.markDirectories=this._getValue(this._options.markDirectories,!1),this.objectMode=this._getValue(this._options.objectMode,!1),this.onlyDirectories=this._getValue(this._options.onlyDirectories,!1),this.onlyFiles=this._getValue(this._options.onlyFiles,!0),this.stats=this._getValue(this._options.stats,!1),this.suppressErrors=this._getValue(this._options.suppressErrors,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!1),this.unique=this._getValue(this._options.unique,!0),this.onlyDirectories&&(this.onlyFiles=!1),this.stats&&(this.objectMode=!0),this.ignore=[].concat(this.ignore)}_getValue(e,r){return e===void 0?r:e}_getFileSystemMethods(e={}){return Object.assign(Object.assign({},$E.DEFAULT_FILE_SYSTEM_ADAPTER),e)}};$E.default=T8});var CQ=L((UKt,Jpe)=>{"use strict";var Kpe=JAe(),cpt=qpe(),upt=Gpe(),fpt=Ype(),R8=Vpe(),Rc=Qp();async function F8(t,e){qu(t);let r=N8(t,cpt.default,e),s=await Promise.all(r);return Rc.array.flatten(s)}(function(t){t.glob=t,t.globSync=e,t.globStream=r,t.async=t;function e(h,E){qu(h);let C=N8(h,fpt.default,E);return Rc.array.flatten(C)}t.sync=e;function r(h,E){qu(h);let C=N8(h,upt.default,E);return Rc.stream.merge(C)}t.stream=r;function s(h,E){qu(h);let C=[].concat(h),S=new R8.default(E);return Kpe.generate(C,S)}t.generateTasks=s;function a(h,E){qu(h);let C=new R8.default(E);return Rc.pattern.isDynamicPattern(h,C)}t.isDynamicPattern=a;function n(h){return qu(h),Rc.path.escape(h)}t.escapePath=n;function c(h){return qu(h),Rc.path.convertPathToPattern(h)}t.convertPathToPattern=c;let f;(function(h){function E(S){return qu(S),Rc.path.escapePosixPath(S)}h.escapePath=E;function C(S){return qu(S),Rc.path.convertPosixPathToPattern(S)}h.convertPathToPattern=C})(f=t.posix||(t.posix={}));let p;(function(h){function E(S){return qu(S),Rc.path.escapeWindowsPath(S)}h.escapePath=E;function C(S){return qu(S),Rc.path.convertWindowsPathToPattern(S)}h.convertPathToPattern=C})(p=t.win32||(t.win32={}))})(F8||(F8={}));function N8(t,e,r){let s=[].concat(t),a=new R8.default(r),n=Kpe.generate(s,a),c=new e(a);return n.map(c.read,c)}function qu(t){if(![].concat(t).every(s=>Rc.string.isString(s)&&!Rc.string.isEmpty(s)))throw new TypeError("Patterns must be a string (non empty) or an array of strings")}Jpe.exports=F8});var Nn={};Vt(Nn,{checksumFile:()=>BQ,checksumPattern:()=>vQ,makeHash:()=>fs});function fs(...t){let e=(0,wQ.createHash)("sha512"),r="";for(let s of t)typeof s=="string"?r+=s:s&&(r&&(e.update(r),r=""),e.update(s));return r&&e.update(r),e.digest("hex")}async function BQ(t,{baseFs:e,algorithm:r}={baseFs:le,algorithm:"sha512"}){let s=await e.openPromise(t,"r");try{let n=Buffer.allocUnsafeSlow(65536),c=(0,wQ.createHash)(r),f=0;for(;(f=await e.readPromise(s,n,0,65536))!==0;)c.update(f===65536?n:n.slice(0,f));return c.digest("hex")}finally{await e.closePromise(s)}}async function vQ(t,{cwd:e}){let s=(await(0,O8.default)(t,{cwd:ue.fromPortablePath(e),onlyDirectories:!0})).map(f=>`${f}/**/*`),a=await(0,O8.default)([t,...s],{cwd:ue.fromPortablePath(e),onlyFiles:!1});a.sort();let n=await Promise.all(a.map(async f=>{let p=[Buffer.from(f)],h=K.join(e,ue.toPortablePath(f)),E=await le.lstatPromise(h);return E.isSymbolicLink()?p.push(Buffer.from(await le.readlinkPromise(h))):E.isFile()&&p.push(await le.readFilePromise(h)),p.join("\0")})),c=(0,wQ.createHash)("sha512");for(let f of n)c.update(f);return c.digest("hex")}var wQ,O8,E0=Ct(()=>{bt();wQ=Ie("crypto"),O8=et(CQ())});var q={};Vt(q,{allPeerRequests:()=>XB,areDescriptorsEqual:()=>ehe,areIdentsEqual:()=>VB,areLocatorsEqual:()=>KB,areVirtualPackagesEquivalent:()=>Ipt,bindDescriptor:()=>ypt,bindLocator:()=>Ept,convertDescriptorToLocator:()=>SQ,convertLocatorToDescriptor:()=>M8,convertPackageToLocator:()=>gpt,convertToIdent:()=>hpt,convertToManifestRange:()=>kpt,copyPackage:()=>WB,devirtualizeDescriptor:()=>YB,devirtualizeLocator:()=>tI,ensureDevirtualizedDescriptor:()=>dpt,ensureDevirtualizedLocator:()=>mpt,getIdentVendorPath:()=>j8,isPackageCompatible:()=>kQ,isVirtualDescriptor:()=>Tp,isVirtualLocator:()=>Gu,makeDescriptor:()=>On,makeIdent:()=>ba,makeLocator:()=>Vs,makeRange:()=>PQ,parseDescriptor:()=>I0,parseFileStyleRange:()=>Ppt,parseIdent:()=>Da,parseLocator:()=>Rp,parseRange:()=>Xd,prettyDependent:()=>h3,prettyDescriptor:()=>ni,prettyIdent:()=>es,prettyLocator:()=>Yr,prettyLocatorNoColors:()=>p3,prettyRange:()=>nI,prettyReference:()=>zB,prettyResolution:()=>jB,prettyWorkspace:()=>ZB,renamePackage:()=>_8,slugifyIdent:()=>L8,slugifyLocator:()=>rI,sortDescriptors:()=>iI,stringifyDescriptor:()=>ll,stringifyIdent:()=>cn,stringifyLocator:()=>cl,tryParseDescriptor:()=>JB,tryParseIdent:()=>the,tryParseLocator:()=>bQ,tryParseRange:()=>bpt,unwrapIdentFromScope:()=>Tpt,virtualizeDescriptor:()=>U8,virtualizePackage:()=>H8,wrapIdentIntoScope:()=>Qpt});function ba(t,e){if(t?.startsWith("@"))throw new Error("Invalid scope: don't prefix it with '@'");return{identHash:fs(t,e),scope:t,name:e}}function On(t,e){return{identHash:t.identHash,scope:t.scope,name:t.name,descriptorHash:fs(t.identHash,e),range:e}}function Vs(t,e){return{identHash:t.identHash,scope:t.scope,name:t.name,locatorHash:fs(t.identHash,e),reference:e}}function hpt(t){return{identHash:t.identHash,scope:t.scope,name:t.name}}function SQ(t){return{identHash:t.identHash,scope:t.scope,name:t.name,locatorHash:t.descriptorHash,reference:t.range}}function M8(t){return{identHash:t.identHash,scope:t.scope,name:t.name,descriptorHash:t.locatorHash,range:t.reference}}function gpt(t){return{identHash:t.identHash,scope:t.scope,name:t.name,locatorHash:t.locatorHash,reference:t.reference}}function _8(t,e){return{identHash:e.identHash,scope:e.scope,name:e.name,locatorHash:e.locatorHash,reference:e.reference,version:t.version,languageName:t.languageName,linkType:t.linkType,conditions:t.conditions,dependencies:new Map(t.dependencies),peerDependencies:new Map(t.peerDependencies),dependenciesMeta:new Map(t.dependenciesMeta),peerDependenciesMeta:new Map(t.peerDependenciesMeta),bin:new Map(t.bin)}}function WB(t){return _8(t,t)}function U8(t,e){if(e.includes("#"))throw new Error("Invalid entropy");return On(t,`virtual:${e}#${t.range}`)}function H8(t,e){if(e.includes("#"))throw new Error("Invalid entropy");return _8(t,Vs(t,`virtual:${e}#${t.reference}`))}function Tp(t){return t.range.startsWith(GB)}function Gu(t){return t.reference.startsWith(GB)}function YB(t){if(!Tp(t))throw new Error("Not a virtual descriptor");return On(t,t.range.replace(DQ,""))}function tI(t){if(!Gu(t))throw new Error("Not a virtual descriptor");return Vs(t,t.reference.replace(DQ,""))}function dpt(t){return Tp(t)?On(t,t.range.replace(DQ,"")):t}function mpt(t){return Gu(t)?Vs(t,t.reference.replace(DQ,"")):t}function ypt(t,e){return t.range.includes("::")?t:On(t,`${t.range}::${eI.default.stringify(e)}`)}function Ept(t,e){return t.reference.includes("::")?t:Vs(t,`${t.reference}::${eI.default.stringify(e)}`)}function VB(t,e){return t.identHash===e.identHash}function ehe(t,e){return t.descriptorHash===e.descriptorHash}function KB(t,e){return t.locatorHash===e.locatorHash}function Ipt(t,e){if(!Gu(t))throw new Error("Invalid package type");if(!Gu(e))throw new Error("Invalid package type");if(!VB(t,e)||t.dependencies.size!==e.dependencies.size)return!1;for(let r of t.dependencies.values()){let s=e.dependencies.get(r.identHash);if(!s||!ehe(r,s))return!1}return!0}function Da(t){let e=the(t);if(!e)throw new Error(`Invalid ident (${t})`);return e}function the(t){let e=t.match(Cpt);if(!e)return null;let[,r,s]=e;return ba(typeof r<"u"?r:null,s)}function I0(t,e=!1){let r=JB(t,e);if(!r)throw new Error(`Invalid descriptor (${t})`);return r}function JB(t,e=!1){let r=e?t.match(wpt):t.match(Bpt);if(!r)return null;let[,s,a,n]=r;if(n==="unknown")throw new Error(`Invalid range (${t})`);let c=typeof s<"u"?s:null,f=typeof n<"u"?n:"unknown";return On(ba(c,a),f)}function Rp(t,e=!1){let r=bQ(t,e);if(!r)throw new Error(`Invalid locator (${t})`);return r}function bQ(t,e=!1){let r=e?t.match(vpt):t.match(Spt);if(!r)return null;let[,s,a,n]=r;if(n==="unknown")throw new Error(`Invalid reference (${t})`);let c=typeof s<"u"?s:null,f=typeof n<"u"?n:"unknown";return Vs(ba(c,a),f)}function Xd(t,e){let r=t.match(Dpt);if(r===null)throw new Error(`Invalid range (${t})`);let s=typeof r[1]<"u"?r[1]:null;if(typeof e?.requireProtocol=="string"&&s!==e.requireProtocol)throw new Error(`Invalid protocol (${s})`);if(e?.requireProtocol&&s===null)throw new Error(`Missing protocol (${s})`);let a=typeof r[3]<"u"?decodeURIComponent(r[2]):null;if(e?.requireSource&&a===null)throw new Error(`Missing source (${t})`);let n=typeof r[3]<"u"?decodeURIComponent(r[3]):decodeURIComponent(r[2]),c=e?.parseSelector?eI.default.parse(n):n,f=typeof r[4]<"u"?eI.default.parse(r[4]):null;return{protocol:s,source:a,selector:c,params:f}}function bpt(t,e){try{return Xd(t,e)}catch{return null}}function Ppt(t,{protocol:e}){let{selector:r,params:s}=Xd(t,{requireProtocol:e,requireBindings:!0});if(typeof s.locator!="string")throw new Error(`Assertion failed: Invalid bindings for ${t}`);return{parentLocator:Rp(s.locator,!0),path:r}}function zpe(t){return t=t.replaceAll("%","%25"),t=t.replaceAll(":","%3A"),t=t.replaceAll("#","%23"),t}function xpt(t){return t===null?!1:Object.entries(t).length>0}function PQ({protocol:t,source:e,selector:r,params:s}){let a="";return t!==null&&(a+=`${t}`),e!==null&&(a+=`${zpe(e)}#`),a+=zpe(r),xpt(s)&&(a+=`::${eI.default.stringify(s)}`),a}function kpt(t){let{params:e,protocol:r,source:s,selector:a}=Xd(t);for(let n in e)n.startsWith("__")&&delete e[n];return PQ({protocol:r,source:s,params:e,selector:a})}function cn(t){return t.scope?`@${t.scope}/${t.name}`:`${t.name}`}function Qpt(t,e){return t.scope?ba(e,`${t.scope}__${t.name}`):ba(e,t.name)}function Tpt(t,e){if(t.scope!==e)return t;let r=t.name.indexOf("__");if(r===-1)return ba(null,t.name);let s=t.name.slice(0,r),a=t.name.slice(r+2);return ba(s,a)}function ll(t){return t.scope?`@${t.scope}/${t.name}@${t.range}`:`${t.name}@${t.range}`}function cl(t){return t.scope?`@${t.scope}/${t.name}@${t.reference}`:`${t.name}@${t.reference}`}function L8(t){return t.scope!==null?`@${t.scope}-${t.name}`:t.name}function rI(t){let{protocol:e,selector:r}=Xd(t.reference),s=e!==null?e.replace(Rpt,""):"exotic",a=Zpe.default.valid(r),n=a!==null?`${s}-${a}`:`${s}`,c=10;return t.scope?`${L8(t)}-${n}-${t.locatorHash.slice(0,c)}`:`${L8(t)}-${n}-${t.locatorHash.slice(0,c)}`}function es(t,e){return e.scope?`${Ut(t,`@${e.scope}/`,pt.SCOPE)}${Ut(t,e.name,pt.NAME)}`:`${Ut(t,e.name,pt.NAME)}`}function xQ(t){if(t.startsWith(GB)){let e=xQ(t.substring(t.indexOf("#")+1)),r=t.substring(GB.length,GB.length+Apt);return`${e} [${r}]`}else return t.replace(Fpt,"?[...]")}function nI(t,e){return`${Ut(t,xQ(e),pt.RANGE)}`}function ni(t,e){return`${es(t,e)}${Ut(t,"@",pt.RANGE)}${nI(t,e.range)}`}function zB(t,e){return`${Ut(t,xQ(e),pt.REFERENCE)}`}function Yr(t,e){return`${es(t,e)}${Ut(t,"@",pt.REFERENCE)}${zB(t,e.reference)}`}function p3(t){return`${cn(t)}@${xQ(t.reference)}`}function iI(t){return Ys(t,[e=>cn(e),e=>e.range])}function ZB(t,e){return es(t,e.anchoredLocator)}function jB(t,e,r){let s=Tp(e)?YB(e):e;return r===null?`${ni(t,s)} \u2192 ${A3(t).Cross}`:s.identHash===r.identHash?`${ni(t,s)} \u2192 ${zB(t,r.reference)}`:`${ni(t,s)} \u2192 ${Yr(t,r)}`}function h3(t,e,r){return r===null?`${Yr(t,e)}`:`${Yr(t,e)} (via ${nI(t,r.range)})`}function j8(t){return`node_modules/${cn(t)}`}function kQ(t,e){return t.conditions?ppt(t.conditions,r=>{let[,s,a]=r.match($pe),n=e[s];return n?n.includes(a):!0}):!0}function XB(t){let e=new Set;if("children"in t)e.add(t);else for(let r of t.requests.values())e.add(r);for(let r of e)for(let s of r.children.values())e.add(s);return e}var eI,Zpe,Xpe,GB,Apt,$pe,ppt,DQ,Cpt,wpt,Bpt,vpt,Spt,Dpt,Rpt,Fpt,Yo=Ct(()=>{eI=et(Ie("querystring")),Zpe=et(Ai()),Xpe=et(Wse());Qc();E0();kc();Yo();GB="virtual:",Apt=5,$pe=/(os|cpu|libc)=([a-z0-9_-]+)/,ppt=(0,Xpe.makeParser)($pe);DQ=/^[^#]*#/;Cpt=/^(?:@([^/]+?)\/)?([^@/]+)$/;wpt=/^(?:@([^/]+?)\/)?([^@/]+?)(?:@(.+))$/,Bpt=/^(?:@([^/]+?)\/)?([^@/]+?)(?:@(.+))?$/;vpt=/^(?:@([^/]+?)\/)?([^@/]+?)(?:@(.+))$/,Spt=/^(?:@([^/]+?)\/)?([^@/]+?)(?:@(.+))?$/;Dpt=/^([^#:]*:)?((?:(?!::)[^#])*)(?:#((?:(?!::).)*))?(?:::(.*))?$/;Rpt=/:$/;Fpt=/\?.*/});var rhe,nhe=Ct(()=>{Yo();rhe={hooks:{reduceDependency:(t,e,r,s,{resolver:a,resolveOptions:n})=>{for(let{pattern:c,reference:f}of e.topLevelWorkspace.manifest.resolutions){if(c.from&&(c.from.fullName!==cn(r)||e.configuration.normalizeLocator(Vs(Da(c.from.fullName),c.from.description??r.reference)).locatorHash!==r.locatorHash)||c.descriptor.fullName!==cn(t)||e.configuration.normalizeDependency(On(Rp(c.descriptor.fullName),c.descriptor.description??t.range)).descriptorHash!==t.descriptorHash)continue;return a.bindDescriptor(e.configuration.normalizeDependency(On(t,f)),e.topLevelWorkspace.anchoredLocator,n)}return t},validateProject:async(t,e)=>{for(let r of t.workspaces){let s=ZB(t.configuration,r);await t.configuration.triggerHook(a=>a.validateWorkspace,r,{reportWarning:(a,n)=>e.reportWarning(a,`${s}: ${n}`),reportError:(a,n)=>e.reportError(a,`${s}: ${n}`)})}},validateWorkspace:async(t,e)=>{let{manifest:r}=t;r.resolutions.length&&t.cwd!==t.project.cwd&&r.errors.push(new Error("Resolutions field will be ignored"));for(let s of r.errors)e.reportWarning(57,s.message)}}}});var Ei,$d=Ct(()=>{Ei=class t{static{this.protocol="workspace:"}supportsDescriptor(e,r){return!!(e.range.startsWith(t.protocol)||r.project.tryWorkspaceByDescriptor(e)!==null)}supportsLocator(e,r){return!!e.reference.startsWith(t.protocol)}shouldPersistResolution(e,r){return!1}bindDescriptor(e,r,s){return e}getResolutionDependencies(e,r){return{}}async getCandidates(e,r,s){return[s.project.getWorkspaceByDescriptor(e).anchoredLocator]}async getSatisfying(e,r,s,a){let[n]=await this.getCandidates(e,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(e,r){let s=r.project.getWorkspaceByCwd(e.reference.slice(t.protocol.length));return{...e,version:s.manifest.version||"0.0.0",languageName:"unknown",linkType:"SOFT",conditions:null,dependencies:r.project.configuration.normalizeDependencyMap(new Map([...s.manifest.dependencies,...s.manifest.devDependencies])),peerDependencies:new Map([...s.manifest.peerDependencies]),dependenciesMeta:s.manifest.dependenciesMeta,peerDependenciesMeta:s.manifest.peerDependenciesMeta,bin:s.manifest.bin}}}});var Or={};Vt(Or,{SemVer:()=>lhe.SemVer,clean:()=>Opt,getComparator:()=>ohe,mergeComparators:()=>q8,satisfiesWithPrereleases:()=>eA,simplifyRanges:()=>G8,stringifyComparator:()=>ahe,validRange:()=>ul});function eA(t,e,r=!1){if(!t)return!1;let s=`${e}${r}`,a=ihe.get(s);if(typeof a>"u")try{a=new Fp.default.Range(e,{includePrerelease:!0,loose:r})}catch{return!1}finally{ihe.set(s,a||null)}else if(a===null)return!1;let n;try{n=new Fp.default.SemVer(t,a)}catch{return!1}return a.test(n)?!0:(n.prerelease&&(n.prerelease=[]),a.set.some(c=>{for(let f of c)f.semver.prerelease&&(f.semver.prerelease=[]);return c.every(f=>f.test(n))}))}function ul(t){if(t.indexOf(":")!==-1)return null;let e=she.get(t);if(typeof e<"u")return e;try{e=new Fp.default.Range(t)}catch{e=null}return she.set(t,e),e}function Opt(t){let e=Npt.exec(t);return e?e[1]:null}function ohe(t){if(t.semver===Fp.default.Comparator.ANY)return{gt:null,lt:null};switch(t.operator){case"":return{gt:[">=",t.semver],lt:["<=",t.semver]};case">":case">=":return{gt:[t.operator,t.semver],lt:null};case"<":case"<=":return{gt:null,lt:[t.operator,t.semver]};default:throw new Error(`Assertion failed: Unexpected comparator operator (${t.operator})`)}}function q8(t){if(t.length===0)return null;let e=null,r=null;for(let s of t){if(s.gt){let a=e!==null?Fp.default.compare(s.gt[1],e[1]):null;(a===null||a>0||a===0&&s.gt[0]===">")&&(e=s.gt)}if(s.lt){let a=r!==null?Fp.default.compare(s.lt[1],r[1]):null;(a===null||a<0||a===0&&s.lt[0]==="<")&&(r=s.lt)}}if(e&&r){let s=Fp.default.compare(e[1],r[1]);if(s===0&&(e[0]===">"||r[0]==="<")||s>0)return null}return{gt:e,lt:r}}function ahe(t){if(t.gt&&t.lt){if(t.gt[0]===">="&&t.lt[0]==="<="&&t.gt[1].version===t.lt[1].version)return t.gt[1].version;if(t.gt[0]===">="&&t.lt[0]==="<"){if(t.lt[1].version===`${t.gt[1].major+1}.0.0-0`)return`^${t.gt[1].version}`;if(t.lt[1].version===`${t.gt[1].major}.${t.gt[1].minor+1}.0-0`)return`~${t.gt[1].version}`}}let e=[];return t.gt&&e.push(t.gt[0]+t.gt[1].version),t.lt&&e.push(t.lt[0]+t.lt[1].version),e.length?e.join(" "):"*"}function G8(t){let e=t.map(Lpt).map(s=>ul(s).set.map(a=>a.map(n=>ohe(n)))),r=e.shift().map(s=>q8(s)).filter(s=>s!==null);for(let s of e){let a=[];for(let n of r)for(let c of s){let f=q8([n,...c]);f!==null&&a.push(f)}r=a}return r.length===0?null:r.map(s=>ahe(s)).join(" || ")}function Lpt(t){let e=t.split("||");if(e.length>1){let r=new Set;for(let s of e)e.some(a=>a!==s&&Fp.default.subset(s,a))||r.add(s);if(r.size{Fp=et(Ai()),lhe=et(Ai()),ihe=new Map;she=new Map;Npt=/^(?:[\sv=]*?)((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\s*)$/});function che(t){let e=t.match(/^[ \t]+/m);return e?e[0]:" "}function uhe(t){return t.charCodeAt(0)===65279?t.slice(1):t}function Pa(t){return t.replace(/\\/g,"/")}function QQ(t,{yamlCompatibilityMode:e}){return e?s3(t):typeof t>"u"||typeof t=="boolean"?t:null}function fhe(t,e){let r=e.search(/[^!]/);if(r===-1)return"invalid";let s=r%2===0?"":"!",a=e.slice(r);return`${s}${t}=${a}`}function W8(t,e){return e.length===1?fhe(t,e[0]):`(${e.map(r=>fhe(t,r)).join(" | ")})`}var Ahe,Ht,sI=Ct(()=>{bt();Bc();Ahe=et(Ai());$d();kc();Np();Yo();Ht=class t{constructor(){this.indent=" ";this.name=null;this.version=null;this.os=null;this.cpu=null;this.libc=null;this.type=null;this.packageManager=null;this.private=!1;this.license=null;this.main=null;this.module=null;this.browser=null;this.languageName=null;this.bin=new Map;this.scripts=new Map;this.dependencies=new Map;this.devDependencies=new Map;this.peerDependencies=new Map;this.workspaceDefinitions=[];this.dependenciesMeta=new Map;this.peerDependenciesMeta=new Map;this.resolutions=[];this.files=null;this.publishConfig=null;this.installConfig=null;this.preferUnplugged=null;this.raw={};this.errors=[]}static{this.fileName="package.json"}static{this.allDependencies=["dependencies","devDependencies","peerDependencies"]}static{this.hardDependencies=["dependencies","devDependencies"]}static async tryFind(e,{baseFs:r=new Yn}={}){let s=K.join(e,"package.json");try{return await t.fromFile(s,{baseFs:r})}catch(a){if(a.code==="ENOENT")return null;throw a}}static async find(e,{baseFs:r}={}){let s=await t.tryFind(e,{baseFs:r});if(s===null)throw new Error("Manifest not found");return s}static async fromFile(e,{baseFs:r=new Yn}={}){let s=new t;return await s.loadFile(e,{baseFs:r}),s}static fromText(e){let r=new t;return r.loadFromText(e),r}loadFromText(e){let r;try{r=JSON.parse(uhe(e)||"{}")}catch(s){throw s.message+=` (when parsing ${e})`,s}this.load(r),this.indent=che(e)}async loadFile(e,{baseFs:r=new Yn}){let s=await r.readFilePromise(e,"utf8"),a;try{a=JSON.parse(uhe(s)||"{}")}catch(n){throw n.message+=` (when parsing ${e})`,n}this.load(a),this.indent=che(s)}load(e,{yamlCompatibilityMode:r=!1}={}){if(typeof e!="object"||e===null)throw new Error(`Utterly invalid manifest data (${e})`);this.raw=e;let s=[];if(this.name=null,typeof e.name=="string")try{this.name=Da(e.name)}catch{s.push(new Error("Parsing failed for the 'name' field"))}if(typeof e.version=="string"?this.version=e.version:this.version=null,Array.isArray(e.os)){let n=[];this.os=n;for(let c of e.os)typeof c!="string"?s.push(new Error("Parsing failed for the 'os' field")):n.push(c)}else this.os=null;if(Array.isArray(e.cpu)){let n=[];this.cpu=n;for(let c of e.cpu)typeof c!="string"?s.push(new Error("Parsing failed for the 'cpu' field")):n.push(c)}else this.cpu=null;if(Array.isArray(e.libc)){let n=[];this.libc=n;for(let c of e.libc)typeof c!="string"?s.push(new Error("Parsing failed for the 'libc' field")):n.push(c)}else this.libc=null;if(typeof e.type=="string"?this.type=e.type:this.type=null,typeof e.packageManager=="string"?this.packageManager=e.packageManager:this.packageManager=null,typeof e.private=="boolean"?this.private=e.private:this.private=!1,typeof e.license=="string"?this.license=e.license:this.license=null,typeof e.languageName=="string"?this.languageName=e.languageName:this.languageName=null,typeof e.main=="string"?this.main=Pa(e.main):this.main=null,typeof e.module=="string"?this.module=Pa(e.module):this.module=null,e.browser!=null)if(typeof e.browser=="string")this.browser=Pa(e.browser);else{this.browser=new Map;for(let[n,c]of Object.entries(e.browser))this.browser.set(Pa(n),typeof c=="string"?Pa(c):c)}else this.browser=null;if(this.bin=new Map,typeof e.bin=="string")e.bin.trim()===""?s.push(new Error("Invalid bin field")):this.name!==null?this.bin.set(this.name.name,Pa(e.bin)):s.push(new Error("String bin field, but no attached package name"));else if(typeof e.bin=="object"&&e.bin!==null)for(let[n,c]of Object.entries(e.bin)){if(typeof c!="string"||c.trim()===""){s.push(new Error(`Invalid bin definition for '${n}'`));continue}let f=Da(n);this.bin.set(f.name,Pa(c))}if(this.scripts=new Map,typeof e.scripts=="object"&&e.scripts!==null)for(let[n,c]of Object.entries(e.scripts)){if(typeof c!="string"){s.push(new Error(`Invalid script definition for '${n}'`));continue}this.scripts.set(n,c)}if(this.dependencies=new Map,typeof e.dependencies=="object"&&e.dependencies!==null)for(let[n,c]of Object.entries(e.dependencies)){if(typeof c!="string"){s.push(new Error(`Invalid dependency range for '${n}'`));continue}let f;try{f=Da(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}let p=On(f,c);this.dependencies.set(p.identHash,p)}if(this.devDependencies=new Map,typeof e.devDependencies=="object"&&e.devDependencies!==null)for(let[n,c]of Object.entries(e.devDependencies)){if(typeof c!="string"){s.push(new Error(`Invalid dependency range for '${n}'`));continue}let f;try{f=Da(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}let p=On(f,c);this.devDependencies.set(p.identHash,p)}if(this.peerDependencies=new Map,typeof e.peerDependencies=="object"&&e.peerDependencies!==null)for(let[n,c]of Object.entries(e.peerDependencies)){let f;try{f=Da(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}(typeof c!="string"||!c.startsWith(Ei.protocol)&&!ul(c))&&(s.push(new Error(`Invalid dependency range for '${n}'`)),c="*");let p=On(f,c);this.peerDependencies.set(p.identHash,p)}typeof e.workspaces=="object"&&e.workspaces!==null&&e.workspaces.nohoist&&s.push(new Error("'nohoist' is deprecated, please use 'installConfig.hoistingLimits' instead"));let a=Array.isArray(e.workspaces)?e.workspaces:typeof e.workspaces=="object"&&e.workspaces!==null&&Array.isArray(e.workspaces.packages)?e.workspaces.packages:[];this.workspaceDefinitions=[];for(let n of a){if(typeof n!="string"){s.push(new Error(`Invalid workspace definition for '${n}'`));continue}this.workspaceDefinitions.push({pattern:n})}if(this.dependenciesMeta=new Map,typeof e.dependenciesMeta=="object"&&e.dependenciesMeta!==null)for(let[n,c]of Object.entries(e.dependenciesMeta)){if(typeof c!="object"||c===null){s.push(new Error(`Invalid meta field for '${n}`));continue}let f=I0(n),p=this.ensureDependencyMeta(f),h=QQ(c.built,{yamlCompatibilityMode:r});if(h===null){s.push(new Error(`Invalid built meta field for '${n}'`));continue}let E=QQ(c.optional,{yamlCompatibilityMode:r});if(E===null){s.push(new Error(`Invalid optional meta field for '${n}'`));continue}let C=QQ(c.unplugged,{yamlCompatibilityMode:r});if(C===null){s.push(new Error(`Invalid unplugged meta field for '${n}'`));continue}Object.assign(p,{built:h,optional:E,unplugged:C})}if(this.peerDependenciesMeta=new Map,typeof e.peerDependenciesMeta=="object"&&e.peerDependenciesMeta!==null)for(let[n,c]of Object.entries(e.peerDependenciesMeta)){if(typeof c!="object"||c===null){s.push(new Error(`Invalid meta field for '${n}'`));continue}let f=I0(n),p=this.ensurePeerDependencyMeta(f),h=QQ(c.optional,{yamlCompatibilityMode:r});if(h===null){s.push(new Error(`Invalid optional meta field for '${n}'`));continue}Object.assign(p,{optional:h})}if(this.resolutions=[],typeof e.resolutions=="object"&&e.resolutions!==null)for(let[n,c]of Object.entries(e.resolutions)){if(typeof c!="string"){s.push(new Error(`Invalid resolution entry for '${n}'`));continue}try{this.resolutions.push({pattern:Cx(n),reference:c})}catch(f){s.push(f);continue}}if(Array.isArray(e.files)){this.files=new Set;for(let n of e.files){if(typeof n!="string"){s.push(new Error(`Invalid files entry for '${n}'`));continue}this.files.add(n)}}else this.files=null;if(typeof e.publishConfig=="object"&&e.publishConfig!==null){if(this.publishConfig={},typeof e.publishConfig.access=="string"&&(this.publishConfig.access=e.publishConfig.access),typeof e.publishConfig.main=="string"&&(this.publishConfig.main=Pa(e.publishConfig.main)),typeof e.publishConfig.module=="string"&&(this.publishConfig.module=Pa(e.publishConfig.module)),e.publishConfig.browser!=null)if(typeof e.publishConfig.browser=="string")this.publishConfig.browser=Pa(e.publishConfig.browser);else{this.publishConfig.browser=new Map;for(let[n,c]of Object.entries(e.publishConfig.browser))this.publishConfig.browser.set(Pa(n),typeof c=="string"?Pa(c):c)}if(typeof e.publishConfig.registry=="string"&&(this.publishConfig.registry=e.publishConfig.registry),typeof e.publishConfig.provenance=="boolean"&&(this.publishConfig.provenance=e.publishConfig.provenance),typeof e.publishConfig.bin=="string")this.name!==null?this.publishConfig.bin=new Map([[this.name.name,Pa(e.publishConfig.bin)]]):s.push(new Error("String bin field, but no attached package name"));else if(typeof e.publishConfig.bin=="object"&&e.publishConfig.bin!==null){this.publishConfig.bin=new Map;for(let[n,c]of Object.entries(e.publishConfig.bin)){if(typeof c!="string"){s.push(new Error(`Invalid bin definition for '${n}'`));continue}this.publishConfig.bin.set(n,Pa(c))}}if(Array.isArray(e.publishConfig.executableFiles)){this.publishConfig.executableFiles=new Set;for(let n of e.publishConfig.executableFiles){if(typeof n!="string"){s.push(new Error("Invalid executable file definition"));continue}this.publishConfig.executableFiles.add(Pa(n))}}}else this.publishConfig=null;if(typeof e.installConfig=="object"&&e.installConfig!==null){this.installConfig={};for(let n of Object.keys(e.installConfig))n==="hoistingLimits"?typeof e.installConfig.hoistingLimits=="string"?this.installConfig.hoistingLimits=e.installConfig.hoistingLimits:s.push(new Error("Invalid hoisting limits definition")):n=="selfReferences"?typeof e.installConfig.selfReferences=="boolean"?this.installConfig.selfReferences=e.installConfig.selfReferences:s.push(new Error("Invalid selfReferences definition, must be a boolean value")):s.push(new Error(`Unrecognized installConfig key: ${n}`))}else this.installConfig=null;if(typeof e.optionalDependencies=="object"&&e.optionalDependencies!==null)for(let[n,c]of Object.entries(e.optionalDependencies)){if(typeof c!="string"){s.push(new Error(`Invalid dependency range for '${n}'`));continue}let f;try{f=Da(n)}catch{s.push(new Error(`Parsing failed for the dependency name '${n}'`));continue}let p=On(f,c);this.dependencies.set(p.identHash,p);let h=On(f,"unknown"),E=this.ensureDependencyMeta(h);Object.assign(E,{optional:!0})}typeof e.preferUnplugged=="boolean"?this.preferUnplugged=e.preferUnplugged:this.preferUnplugged=null,this.errors=s}getForScope(e){switch(e){case"dependencies":return this.dependencies;case"devDependencies":return this.devDependencies;case"peerDependencies":return this.peerDependencies;default:throw new Error(`Unsupported value ("${e}")`)}}hasConsumerDependency(e){return!!(this.dependencies.has(e.identHash)||this.peerDependencies.has(e.identHash))}hasHardDependency(e){return!!(this.dependencies.has(e.identHash)||this.devDependencies.has(e.identHash))}hasSoftDependency(e){return!!this.peerDependencies.has(e.identHash)}hasDependency(e){return!!(this.hasHardDependency(e)||this.hasSoftDependency(e))}getConditions(){let e=[];return this.os&&this.os.length>0&&e.push(W8("os",this.os)),this.cpu&&this.cpu.length>0&&e.push(W8("cpu",this.cpu)),this.libc&&this.libc.length>0&&e.push(W8("libc",this.libc)),e.length>0?e.join(" & "):null}ensureDependencyMeta(e){if(e.range!=="unknown"&&!Ahe.default.valid(e.range))throw new Error(`Invalid meta field range for '${ll(e)}'`);let r=cn(e),s=e.range!=="unknown"?e.range:null,a=this.dependenciesMeta.get(r);a||this.dependenciesMeta.set(r,a=new Map);let n=a.get(s);return n||a.set(s,n={}),n}ensurePeerDependencyMeta(e){if(e.range!=="unknown")throw new Error(`Invalid meta field range for '${ll(e)}'`);let r=cn(e),s=this.peerDependenciesMeta.get(r);return s||this.peerDependenciesMeta.set(r,s={}),s}setRawField(e,r,{after:s=[]}={}){let a=new Set(s.filter(n=>Object.hasOwn(this.raw,n)));if(a.size===0||Object.hasOwn(this.raw,e))this.raw[e]=r;else{let n=this.raw,c=this.raw={},f=!1;for(let p of Object.keys(n))c[p]=n[p],f||(a.delete(p),a.size===0&&(c[e]=r,f=!0))}}exportTo(e,{compatibilityMode:r=!0}={}){if(Object.assign(e,this.raw),this.name!==null?e.name=cn(this.name):delete e.name,this.version!==null?e.version=this.version:delete e.version,this.os!==null?e.os=this.os:delete e.os,this.cpu!==null?e.cpu=this.cpu:delete e.cpu,this.type!==null?e.type=this.type:delete e.type,this.packageManager!==null?e.packageManager=this.packageManager:delete e.packageManager,this.private?e.private=!0:delete e.private,this.license!==null?e.license=this.license:delete e.license,this.languageName!==null?e.languageName=this.languageName:delete e.languageName,this.main!==null?e.main=this.main:delete e.main,this.module!==null?e.module=this.module:delete e.module,this.browser!==null){let n=this.browser;typeof n=="string"?e.browser=n:n instanceof Map&&(e.browser=Object.assign({},...Array.from(n.keys()).sort().map(c=>({[c]:n.get(c)}))))}else delete e.browser;this.bin.size===1&&this.name!==null&&this.bin.has(this.name.name)?e.bin=this.bin.get(this.name.name):this.bin.size>0?e.bin=Object.assign({},...Array.from(this.bin.keys()).sort().map(n=>({[n]:this.bin.get(n)}))):delete e.bin,this.workspaceDefinitions.length>0?this.raw.workspaces&&!Array.isArray(this.raw.workspaces)?e.workspaces={...this.raw.workspaces,packages:this.workspaceDefinitions.map(({pattern:n})=>n)}:e.workspaces=this.workspaceDefinitions.map(({pattern:n})=>n):this.raw.workspaces&&!Array.isArray(this.raw.workspaces)&&Object.keys(this.raw.workspaces).length>0?e.workspaces=this.raw.workspaces:delete e.workspaces;let s=[],a=[];for(let n of this.dependencies.values()){let c=this.dependenciesMeta.get(cn(n)),f=!1;if(r&&c){let p=c.get(null);p&&p.optional&&(f=!0)}f?a.push(n):s.push(n)}s.length>0?e.dependencies=Object.assign({},...iI(s).map(n=>({[cn(n)]:n.range}))):delete e.dependencies,a.length>0?e.optionalDependencies=Object.assign({},...iI(a).map(n=>({[cn(n)]:n.range}))):delete e.optionalDependencies,this.devDependencies.size>0?e.devDependencies=Object.assign({},...iI(this.devDependencies.values()).map(n=>({[cn(n)]:n.range}))):delete e.devDependencies,this.peerDependencies.size>0?e.peerDependencies=Object.assign({},...iI(this.peerDependencies.values()).map(n=>({[cn(n)]:n.range}))):delete e.peerDependencies,e.dependenciesMeta={};for(let[n,c]of Ys(this.dependenciesMeta.entries(),([f,p])=>f))for(let[f,p]of Ys(c.entries(),([h,E])=>h!==null?`0${h}`:"1")){let h=f!==null?ll(On(Da(n),f)):n,E={...p};r&&f===null&&delete E.optional,Object.keys(E).length!==0&&(e.dependenciesMeta[h]=E)}if(Object.keys(e.dependenciesMeta).length===0&&delete e.dependenciesMeta,this.peerDependenciesMeta.size>0?e.peerDependenciesMeta=Object.assign({},...Ys(this.peerDependenciesMeta.entries(),([n,c])=>n).map(([n,c])=>({[n]:c}))):delete e.peerDependenciesMeta,this.resolutions.length>0?e.resolutions=Object.assign({},...this.resolutions.map(({pattern:n,reference:c})=>({[wx(n)]:c}))):delete e.resolutions,this.files!==null?e.files=Array.from(this.files):delete e.files,this.preferUnplugged!==null?e.preferUnplugged=this.preferUnplugged:delete e.preferUnplugged,this.scripts!==null&&this.scripts.size>0){e.scripts??={};for(let n of Object.keys(e.scripts))this.scripts.has(n)||delete e.scripts[n];for(let[n,c]of this.scripts.entries())e.scripts[n]=c}else delete e.scripts;return e}}});var hhe=L((tJt,phe)=>{var Mpt=Pc(),_pt=function(){return Mpt.Date.now()};phe.exports=_pt});var dhe=L((rJt,ghe)=>{var Upt=/\s/;function Hpt(t){for(var e=t.length;e--&&Upt.test(t.charAt(e)););return e}ghe.exports=Hpt});var yhe=L((nJt,mhe)=>{var jpt=dhe(),qpt=/^\s+/;function Gpt(t){return t&&t.slice(0,jpt(t)+1).replace(qpt,"")}mhe.exports=Gpt});var oI=L((iJt,Ehe)=>{var Wpt=Wd(),Ypt=zf(),Vpt="[object Symbol]";function Kpt(t){return typeof t=="symbol"||Ypt(t)&&Wpt(t)==Vpt}Ehe.exports=Kpt});var Bhe=L((sJt,whe)=>{var Jpt=yhe(),Ihe=Wl(),zpt=oI(),Che=NaN,Zpt=/^[-+]0x[0-9a-f]+$/i,Xpt=/^0b[01]+$/i,$pt=/^0o[0-7]+$/i,eht=parseInt;function tht(t){if(typeof t=="number")return t;if(zpt(t))return Che;if(Ihe(t)){var e=typeof t.valueOf=="function"?t.valueOf():t;t=Ihe(e)?e+"":e}if(typeof t!="string")return t===0?t:+t;t=Jpt(t);var r=Xpt.test(t);return r||$pt.test(t)?eht(t.slice(2),r?2:8):Zpt.test(t)?Che:+t}whe.exports=tht});var Dhe=L((oJt,She)=>{var rht=Wl(),Y8=hhe(),vhe=Bhe(),nht="Expected a function",iht=Math.max,sht=Math.min;function oht(t,e,r){var s,a,n,c,f,p,h=0,E=!1,C=!1,S=!0;if(typeof t!="function")throw new TypeError(nht);e=vhe(e)||0,rht(r)&&(E=!!r.leading,C="maxWait"in r,n=C?iht(vhe(r.maxWait)||0,e):n,S="trailing"in r?!!r.trailing:S);function P(ce){var me=s,pe=a;return s=a=void 0,h=ce,c=t.apply(pe,me),c}function I(ce){return h=ce,f=setTimeout(U,e),E?P(ce):c}function R(ce){var me=ce-p,pe=ce-h,Be=e-me;return C?sht(Be,n-pe):Be}function N(ce){var me=ce-p,pe=ce-h;return p===void 0||me>=e||me<0||C&&pe>=n}function U(){var ce=Y8();if(N(ce))return W(ce);f=setTimeout(U,R(ce))}function W(ce){return f=void 0,S&&s?P(ce):(s=a=void 0,c)}function te(){f!==void 0&&clearTimeout(f),h=0,s=p=a=f=void 0}function ie(){return f===void 0?c:W(Y8())}function Ae(){var ce=Y8(),me=N(ce);if(s=arguments,a=this,p=ce,me){if(f===void 0)return I(p);if(C)return clearTimeout(f),f=setTimeout(U,e),P(p)}return f===void 0&&(f=setTimeout(U,e)),c}return Ae.cancel=te,Ae.flush=ie,Ae}She.exports=oht});var V8=L((aJt,bhe)=>{var aht=Dhe(),lht=Wl(),cht="Expected a function";function uht(t,e,r){var s=!0,a=!0;if(typeof t!="function")throw new TypeError(cht);return lht(r)&&(s="leading"in r?!!r.leading:s,a="trailing"in r?!!r.trailing:a),aht(t,e,{leading:s,maxWait:e,trailing:a})}bhe.exports=uht});function Aht(t){return typeof t.reportCode<"u"}var Phe,xhe,khe,fht,Yt,ho,Fc=Ct(()=>{Phe=et(V8()),xhe=Ie("stream"),khe=Ie("string_decoder"),fht=15,Yt=class extends Error{constructor(r,s,a){super(s);this.reportExtra=a;this.reportCode=r}};ho=class{constructor(){this.cacheHits=new Set;this.cacheMisses=new Set;this.reportedInfos=new Set;this.reportedWarnings=new Set;this.reportedErrors=new Set}getRecommendedLength(){return 180}reportCacheHit(e){this.cacheHits.add(e.locatorHash)}reportCacheMiss(e,r){this.cacheMisses.add(e.locatorHash)}static progressViaCounter(e){let r=0,s,a=new Promise(p=>{s=p}),n=p=>{let h=s;a=new Promise(E=>{s=E}),r=p,h()},c=(p=0)=>{n(r+1)},f=async function*(){for(;r{r=c}),a=(0,Phe.default)(c=>{let f=r;s=new Promise(p=>{r=p}),e=c,f()},1e3/fht),n=async function*(){for(;;)await s,yield{title:e}}();return{[Symbol.asyncIterator](){return n},hasProgress:!1,hasTitle:!0,setTitle:a}}async startProgressPromise(e,r){let s=this.reportProgress(e);try{return await r(e)}finally{s.stop()}}startProgressSync(e,r){let s=this.reportProgress(e);try{return r(e)}finally{s.stop()}}reportInfoOnce(e,r,s){let a=s&&s.key?s.key:r;this.reportedInfos.has(a)||(this.reportedInfos.add(a),this.reportInfo(e,r),s?.reportExtra?.(this))}reportWarningOnce(e,r,s){let a=s&&s.key?s.key:r;this.reportedWarnings.has(a)||(this.reportedWarnings.add(a),this.reportWarning(e,r),s?.reportExtra?.(this))}reportErrorOnce(e,r,s){let a=s&&s.key?s.key:r;this.reportedErrors.has(a)||(this.reportedErrors.add(a),this.reportError(e,r),s?.reportExtra?.(this))}reportExceptionOnce(e){Aht(e)?this.reportErrorOnce(e.reportCode,e.message,{key:e,reportExtra:e.reportExtra}):this.reportErrorOnce(1,e.stack||e.message,{key:e})}createStreamReporter(e=null){let r=new xhe.PassThrough,s=new khe.StringDecoder,a="";return r.on("data",n=>{let c=s.write(n),f;do if(f=c.indexOf(` `),f!==-1){let p=a+c.substring(0,f);c=c.substring(f+1),a="",e!==null?this.reportInfo(null,`${e} ${p}`):this.reportInfo(null,p)}while(f!==-1);a+=c}),r.on("end",()=>{let n=s.end();n!==""&&(e!==null?this.reportInfo(null,`${e} ${n}`):this.reportInfo(null,n))}),r}}});var aI,K8=Ct(()=>{Fc();Yo();aI=class{constructor(e){this.fetchers=e}supports(e,r){return!!this.tryFetcher(e,r)}getLocalPath(e,r){return this.getFetcher(e,r).getLocalPath(e,r)}async fetch(e,r){return await this.getFetcher(e,r).fetch(e,r)}tryFetcher(e,r){let s=this.fetchers.find(a=>a.supports(e,r));return s||null}getFetcher(e,r){let s=this.fetchers.find(a=>a.supports(e,r));if(!s)throw new Yt(11,`${Yr(r.project.configuration,e)} isn't supported by any available fetcher`);return s}}});var em,J8=Ct(()=>{Yo();em=class{constructor(e){this.resolvers=e.filter(r=>r)}supportsDescriptor(e,r){return!!this.tryResolverByDescriptor(e,r)}supportsLocator(e,r){return!!this.tryResolverByLocator(e,r)}shouldPersistResolution(e,r){return this.getResolverByLocator(e,r).shouldPersistResolution(e,r)}bindDescriptor(e,r,s){return this.getResolverByDescriptor(e,s).bindDescriptor(e,r,s)}getResolutionDependencies(e,r){return this.getResolverByDescriptor(e,r).getResolutionDependencies(e,r)}async getCandidates(e,r,s){return await this.getResolverByDescriptor(e,s).getCandidates(e,r,s)}async getSatisfying(e,r,s,a){return this.getResolverByDescriptor(e,a).getSatisfying(e,r,s,a)}async resolve(e,r){return await this.getResolverByLocator(e,r).resolve(e,r)}tryResolverByDescriptor(e,r){let s=this.resolvers.find(a=>a.supportsDescriptor(e,r));return s||null}getResolverByDescriptor(e,r){let s=this.resolvers.find(a=>a.supportsDescriptor(e,r));if(!s)throw new Error(`${ni(r.project.configuration,e)} isn't supported by any available resolver`);return s}tryResolverByLocator(e,r){let s=this.resolvers.find(a=>a.supportsLocator(e,r));return s||null}getResolverByLocator(e,r){let s=this.resolvers.find(a=>a.supportsLocator(e,r));if(!s)throw new Error(`${Yr(r.project.configuration,e)} isn't supported by any available resolver`);return s}}});var lI,z8=Ct(()=>{bt();Yo();lI=class{supports(e){return!!e.reference.startsWith("virtual:")}getLocalPath(e,r){let s=e.reference.indexOf("#");if(s===-1)throw new Error("Invalid virtual package reference");let a=e.reference.slice(s+1),n=Vs(e,a);return r.fetcher.getLocalPath(n,r)}async fetch(e,r){let s=e.reference.indexOf("#");if(s===-1)throw new Error("Invalid virtual package reference");let a=e.reference.slice(s+1),n=Vs(e,a),c=await r.fetcher.fetch(n,r);return await this.ensureVirtualLink(e,c,r)}getLocatorFilename(e){return rI(e)}async ensureVirtualLink(e,r,s){let a=r.packageFs.getRealPath(),n=s.project.configuration.get("virtualFolder"),c=this.getLocatorFilename(e),f=Ao.makeVirtualPath(n,c,a),p=new Hf(f,{baseFs:r.packageFs,pathUtils:K});return{...r,packageFs:p}}}});var TQ,Qhe=Ct(()=>{TQ=class t{static{this.protocol="virtual:"}static isVirtualDescriptor(e){return!!e.range.startsWith(t.protocol)}static isVirtualLocator(e){return!!e.reference.startsWith(t.protocol)}supportsDescriptor(e,r){return t.isVirtualDescriptor(e)}supportsLocator(e,r){return t.isVirtualLocator(e)}shouldPersistResolution(e,r){return!1}bindDescriptor(e,r,s){throw new Error('Assertion failed: calling "bindDescriptor" on a virtual descriptor is unsupported')}getResolutionDependencies(e,r){throw new Error('Assertion failed: calling "getResolutionDependencies" on a virtual descriptor is unsupported')}async getCandidates(e,r,s){throw new Error('Assertion failed: calling "getCandidates" on a virtual descriptor is unsupported')}async getSatisfying(e,r,s,a){throw new Error('Assertion failed: calling "getSatisfying" on a virtual descriptor is unsupported')}async resolve(e,r){throw new Error('Assertion failed: calling "resolve" on a virtual locator is unsupported')}}});var cI,Z8=Ct(()=>{bt();$d();cI=class{supports(e){return!!e.reference.startsWith(Ei.protocol)}getLocalPath(e,r){return this.getWorkspace(e,r).cwd}async fetch(e,r){let s=this.getWorkspace(e,r).cwd;return{packageFs:new Sn(s),prefixPath:vt.dot,localPath:s}}getWorkspace(e,r){return r.project.getWorkspaceByCwd(e.reference.slice(Ei.protocol.length))}}});function $B(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function The(t){return typeof t>"u"?3:$B(t)?0:Array.isArray(t)?1:2}function eH(t,e){return Object.hasOwn(t,e)}function hht(t){return $B(t)&&eH(t,"onConflict")&&typeof t.onConflict=="string"}function ght(t){if(typeof t>"u")return{onConflict:"default",value:t};if(!hht(t))return{onConflict:"default",value:t};if(eH(t,"value"))return t;let{onConflict:e,...r}=t;return{onConflict:e,value:r}}function Rhe(t,e){let r=$B(t)&&eH(t,e)?t[e]:void 0;return ght(r)}function uI(t,e){return[t,e,Fhe]}function tH(t){return Array.isArray(t)?t[2]===Fhe:!1}function X8(t,e){if($B(t)){let r={};for(let s of Object.keys(t))r[s]=X8(t[s],e);return uI(e,r)}return Array.isArray(t)?uI(e,t.map(r=>X8(r,e))):uI(e,t)}function $8(t,e,r,s,a){let n,c=[],f=a,p=0;for(let E=a-1;E>=s;--E){let[C,S]=t[E],{onConflict:P,value:I}=Rhe(S,r),R=The(I);if(R!==3){if(n??=R,R!==n||P==="hardReset"){p=f;break}if(R===2)return uI(C,I);if(c.unshift([C,I]),P==="reset"){p=E;break}P==="extend"&&E===s&&(s=0),f=E}}if(typeof n>"u")return null;let h=c.map(([E])=>E).join(", ");switch(n){case 1:return uI(h,new Array().concat(...c.map(([E,C])=>C.map(S=>X8(S,E)))));case 0:{let E=Object.assign({},...c.map(([,R])=>R)),C=Object.keys(E),S={},P=t.map(([R,N])=>[R,Rhe(N,r).value]),I=pht(P,([R,N])=>{let U=The(N);return U!==0&&U!==3});if(I!==-1){let R=P.slice(I+1);for(let N of C)S[N]=$8(R,e,N,0,R.length)}else for(let R of C)S[R]=$8(P,e,R,p,P.length);return uI(h,S)}default:throw new Error("Assertion failed: Non-extendable value type")}}function Nhe(t){return $8(t.map(([e,r])=>[e,{".":r}]),[],".",0,t.length)}function ev(t){return tH(t)?t[1]:t}function RQ(t){let e=tH(t)?t[1]:t;if(Array.isArray(e))return e.map(r=>RQ(r));if($B(e)){let r={};for(let[s,a]of Object.entries(e))r[s]=RQ(a);return r}return e}function rH(t){return tH(t)?t[0]:null}var pht,Fhe,Ohe=Ct(()=>{pht=(t,e,r)=>{let s=[...t];return s.reverse(),s.findIndex(e,r)};Fhe=Symbol()});var FQ={};Vt(FQ,{getDefaultGlobalFolder:()=>iH,getHomeFolder:()=>fI,isFolderInside:()=>sH});function iH(){if(process.platform==="win32"){let t=ue.toPortablePath(process.env.LOCALAPPDATA||ue.join((0,nH.homedir)(),"AppData","Local"));return K.resolve(t,"Yarn/Berry")}if(process.env.XDG_DATA_HOME){let t=ue.toPortablePath(process.env.XDG_DATA_HOME);return K.resolve(t,"yarn/berry")}return K.resolve(fI(),".yarn/berry")}function fI(){return ue.toPortablePath((0,nH.homedir)()||"/usr/local/share")}function sH(t,e){let r=K.relative(e,t);return r&&!r.startsWith("..")&&!K.isAbsolute(r)}var nH,NQ=Ct(()=>{bt();nH=Ie("os")});var _he=L((IJt,Mhe)=>{"use strict";var oH=Ie("https"),aH=Ie("http"),{URL:Lhe}=Ie("url"),lH=class extends aH.Agent{constructor(e){let{proxy:r,proxyRequestOptions:s,...a}=e;super(a),this.proxy=typeof r=="string"?new Lhe(r):r,this.proxyRequestOptions=s||{}}createConnection(e,r){let s={...this.proxyRequestOptions,method:"CONNECT",host:this.proxy.hostname,port:this.proxy.port,path:`${e.host}:${e.port}`,setHost:!1,headers:{...this.proxyRequestOptions.headers,connection:this.keepAlive?"keep-alive":"close",host:`${e.host}:${e.port}`},agent:!1,timeout:e.timeout||0};if(this.proxy.username||this.proxy.password){let n=Buffer.from(`${decodeURIComponent(this.proxy.username||"")}:${decodeURIComponent(this.proxy.password||"")}`).toString("base64");s.headers["proxy-authorization"]=`Basic ${n}`}this.proxy.protocol==="https:"&&(s.servername=this.proxy.hostname);let a=(this.proxy.protocol==="http:"?aH:oH).request(s);a.once("connect",(n,c,f)=>{a.removeAllListeners(),c.removeAllListeners(),n.statusCode===200?r(null,c):(c.destroy(),r(new Error(`Bad response: ${n.statusCode}`),null))}),a.once("timeout",()=>{a.destroy(new Error("Proxy timeout"))}),a.once("error",n=>{a.removeAllListeners(),r(n,null)}),a.end()}},cH=class extends oH.Agent{constructor(e){let{proxy:r,proxyRequestOptions:s,...a}=e;super(a),this.proxy=typeof r=="string"?new Lhe(r):r,this.proxyRequestOptions=s||{}}createConnection(e,r){let s={...this.proxyRequestOptions,method:"CONNECT",host:this.proxy.hostname,port:this.proxy.port,path:`${e.host}:${e.port}`,setHost:!1,headers:{...this.proxyRequestOptions.headers,connection:this.keepAlive?"keep-alive":"close",host:`${e.host}:${e.port}`},agent:!1,timeout:e.timeout||0};if(this.proxy.username||this.proxy.password){let n=Buffer.from(`${decodeURIComponent(this.proxy.username||"")}:${decodeURIComponent(this.proxy.password||"")}`).toString("base64");s.headers["proxy-authorization"]=`Basic ${n}`}this.proxy.protocol==="https:"&&(s.servername=this.proxy.hostname);let a=(this.proxy.protocol==="http:"?aH:oH).request(s);a.once("connect",(n,c,f)=>{if(a.removeAllListeners(),c.removeAllListeners(),n.statusCode===200){let p=super.createConnection({...e,socket:c});r(null,p)}else c.destroy(),r(new Error(`Bad response: ${n.statusCode}`),null)}),a.once("timeout",()=>{a.destroy(new Error("Proxy timeout"))}),a.once("error",n=>{a.removeAllListeners(),r(n,null)}),a.end()}};Mhe.exports={HttpProxyAgent:lH,HttpsProxyAgent:cH}});var uH,Uhe,Hhe,jhe=Ct(()=>{uH=et(_he(),1),Uhe=uH.default.HttpProxyAgent,Hhe=uH.default.HttpsProxyAgent});var Lp=L((Op,OQ)=>{"use strict";Object.defineProperty(Op,"__esModule",{value:!0});var qhe=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","BigInt64Array","BigUint64Array"];function mht(t){return qhe.includes(t)}var yht=["Function","Generator","AsyncGenerator","GeneratorFunction","AsyncGeneratorFunction","AsyncFunction","Observable","Array","Buffer","Blob","Object","RegExp","Date","Error","Map","Set","WeakMap","WeakSet","ArrayBuffer","SharedArrayBuffer","DataView","Promise","URL","FormData","URLSearchParams","HTMLElement",...qhe];function Eht(t){return yht.includes(t)}var Iht=["null","undefined","string","number","bigint","boolean","symbol"];function Cht(t){return Iht.includes(t)}function AI(t){return e=>typeof e===t}var{toString:Ghe}=Object.prototype,tv=t=>{let e=Ghe.call(t).slice(8,-1);if(/HTML\w+Element/.test(e)&&be.domElement(t))return"HTMLElement";if(Eht(e))return e},pi=t=>e=>tv(e)===t;function be(t){if(t===null)return"null";switch(typeof t){case"undefined":return"undefined";case"string":return"string";case"number":return"number";case"boolean":return"boolean";case"function":return"Function";case"bigint":return"bigint";case"symbol":return"symbol";default:}if(be.observable(t))return"Observable";if(be.array(t))return"Array";if(be.buffer(t))return"Buffer";let e=tv(t);if(e)return e;if(t instanceof String||t instanceof Boolean||t instanceof Number)throw new TypeError("Please don't use object wrappers for primitive types");return"Object"}be.undefined=AI("undefined");be.string=AI("string");var wht=AI("number");be.number=t=>wht(t)&&!be.nan(t);be.bigint=AI("bigint");be.function_=AI("function");be.null_=t=>t===null;be.class_=t=>be.function_(t)&&t.toString().startsWith("class ");be.boolean=t=>t===!0||t===!1;be.symbol=AI("symbol");be.numericString=t=>be.string(t)&&!be.emptyStringOrWhitespace(t)&&!Number.isNaN(Number(t));be.array=(t,e)=>Array.isArray(t)?be.function_(e)?t.every(e):!0:!1;be.buffer=t=>{var e,r,s,a;return(a=(s=(r=(e=t)===null||e===void 0?void 0:e.constructor)===null||r===void 0?void 0:r.isBuffer)===null||s===void 0?void 0:s.call(r,t))!==null&&a!==void 0?a:!1};be.blob=t=>pi("Blob")(t);be.nullOrUndefined=t=>be.null_(t)||be.undefined(t);be.object=t=>!be.null_(t)&&(typeof t=="object"||be.function_(t));be.iterable=t=>{var e;return be.function_((e=t)===null||e===void 0?void 0:e[Symbol.iterator])};be.asyncIterable=t=>{var e;return be.function_((e=t)===null||e===void 0?void 0:e[Symbol.asyncIterator])};be.generator=t=>{var e,r;return be.iterable(t)&&be.function_((e=t)===null||e===void 0?void 0:e.next)&&be.function_((r=t)===null||r===void 0?void 0:r.throw)};be.asyncGenerator=t=>be.asyncIterable(t)&&be.function_(t.next)&&be.function_(t.throw);be.nativePromise=t=>pi("Promise")(t);var Bht=t=>{var e,r;return be.function_((e=t)===null||e===void 0?void 0:e.then)&&be.function_((r=t)===null||r===void 0?void 0:r.catch)};be.promise=t=>be.nativePromise(t)||Bht(t);be.generatorFunction=pi("GeneratorFunction");be.asyncGeneratorFunction=t=>tv(t)==="AsyncGeneratorFunction";be.asyncFunction=t=>tv(t)==="AsyncFunction";be.boundFunction=t=>be.function_(t)&&!t.hasOwnProperty("prototype");be.regExp=pi("RegExp");be.date=pi("Date");be.error=pi("Error");be.map=t=>pi("Map")(t);be.set=t=>pi("Set")(t);be.weakMap=t=>pi("WeakMap")(t);be.weakSet=t=>pi("WeakSet")(t);be.int8Array=pi("Int8Array");be.uint8Array=pi("Uint8Array");be.uint8ClampedArray=pi("Uint8ClampedArray");be.int16Array=pi("Int16Array");be.uint16Array=pi("Uint16Array");be.int32Array=pi("Int32Array");be.uint32Array=pi("Uint32Array");be.float32Array=pi("Float32Array");be.float64Array=pi("Float64Array");be.bigInt64Array=pi("BigInt64Array");be.bigUint64Array=pi("BigUint64Array");be.arrayBuffer=pi("ArrayBuffer");be.sharedArrayBuffer=pi("SharedArrayBuffer");be.dataView=pi("DataView");be.enumCase=(t,e)=>Object.values(e).includes(t);be.directInstanceOf=(t,e)=>Object.getPrototypeOf(t)===e.prototype;be.urlInstance=t=>pi("URL")(t);be.urlString=t=>{if(!be.string(t))return!1;try{return new URL(t),!0}catch{return!1}};be.truthy=t=>!!t;be.falsy=t=>!t;be.nan=t=>Number.isNaN(t);be.primitive=t=>be.null_(t)||Cht(typeof t);be.integer=t=>Number.isInteger(t);be.safeInteger=t=>Number.isSafeInteger(t);be.plainObject=t=>{if(Ghe.call(t)!=="[object Object]")return!1;let e=Object.getPrototypeOf(t);return e===null||e===Object.getPrototypeOf({})};be.typedArray=t=>mht(tv(t));var vht=t=>be.safeInteger(t)&&t>=0;be.arrayLike=t=>!be.nullOrUndefined(t)&&!be.function_(t)&&vht(t.length);be.inRange=(t,e)=>{if(be.number(e))return t>=Math.min(0,e)&&t<=Math.max(e,0);if(be.array(e)&&e.length===2)return t>=Math.min(...e)&&t<=Math.max(...e);throw new TypeError(`Invalid range: ${JSON.stringify(e)}`)};var Sht=1,Dht=["innerHTML","ownerDocument","style","attributes","nodeValue"];be.domElement=t=>be.object(t)&&t.nodeType===Sht&&be.string(t.nodeName)&&!be.plainObject(t)&&Dht.every(e=>e in t);be.observable=t=>{var e,r,s,a;return t?t===((r=(e=t)[Symbol.observable])===null||r===void 0?void 0:r.call(e))||t===((a=(s=t)["@@observable"])===null||a===void 0?void 0:a.call(s)):!1};be.nodeStream=t=>be.object(t)&&be.function_(t.pipe)&&!be.observable(t);be.infinite=t=>t===1/0||t===-1/0;var Whe=t=>e=>be.integer(e)&&Math.abs(e%2)===t;be.evenInteger=Whe(0);be.oddInteger=Whe(1);be.emptyArray=t=>be.array(t)&&t.length===0;be.nonEmptyArray=t=>be.array(t)&&t.length>0;be.emptyString=t=>be.string(t)&&t.length===0;var bht=t=>be.string(t)&&!/\S/.test(t);be.emptyStringOrWhitespace=t=>be.emptyString(t)||bht(t);be.nonEmptyString=t=>be.string(t)&&t.length>0;be.nonEmptyStringAndNotWhitespace=t=>be.string(t)&&!be.emptyStringOrWhitespace(t);be.emptyObject=t=>be.object(t)&&!be.map(t)&&!be.set(t)&&Object.keys(t).length===0;be.nonEmptyObject=t=>be.object(t)&&!be.map(t)&&!be.set(t)&&Object.keys(t).length>0;be.emptySet=t=>be.set(t)&&t.size===0;be.nonEmptySet=t=>be.set(t)&&t.size>0;be.emptyMap=t=>be.map(t)&&t.size===0;be.nonEmptyMap=t=>be.map(t)&&t.size>0;be.propertyKey=t=>be.any([be.string,be.number,be.symbol],t);be.formData=t=>pi("FormData")(t);be.urlSearchParams=t=>pi("URLSearchParams")(t);var Yhe=(t,e,r)=>{if(!be.function_(e))throw new TypeError(`Invalid predicate: ${JSON.stringify(e)}`);if(r.length===0)throw new TypeError("Invalid number of values");return t.call(r,e)};be.any=(t,...e)=>(be.array(t)?t:[t]).some(s=>Yhe(Array.prototype.some,s,e));be.all=(t,...e)=>Yhe(Array.prototype.every,t,e);var _t=(t,e,r,s={})=>{if(!t){let{multipleValues:a}=s,n=a?`received values of types ${[...new Set(r.map(c=>`\`${be(c)}\``))].join(", ")}`:`received value of type \`${be(r)}\``;throw new TypeError(`Expected value which is \`${e}\`, ${n}.`)}};Op.assert={undefined:t=>_t(be.undefined(t),"undefined",t),string:t=>_t(be.string(t),"string",t),number:t=>_t(be.number(t),"number",t),bigint:t=>_t(be.bigint(t),"bigint",t),function_:t=>_t(be.function_(t),"Function",t),null_:t=>_t(be.null_(t),"null",t),class_:t=>_t(be.class_(t),"Class",t),boolean:t=>_t(be.boolean(t),"boolean",t),symbol:t=>_t(be.symbol(t),"symbol",t),numericString:t=>_t(be.numericString(t),"string with a number",t),array:(t,e)=>{_t(be.array(t),"Array",t),e&&t.forEach(e)},buffer:t=>_t(be.buffer(t),"Buffer",t),blob:t=>_t(be.blob(t),"Blob",t),nullOrUndefined:t=>_t(be.nullOrUndefined(t),"null or undefined",t),object:t=>_t(be.object(t),"Object",t),iterable:t=>_t(be.iterable(t),"Iterable",t),asyncIterable:t=>_t(be.asyncIterable(t),"AsyncIterable",t),generator:t=>_t(be.generator(t),"Generator",t),asyncGenerator:t=>_t(be.asyncGenerator(t),"AsyncGenerator",t),nativePromise:t=>_t(be.nativePromise(t),"native Promise",t),promise:t=>_t(be.promise(t),"Promise",t),generatorFunction:t=>_t(be.generatorFunction(t),"GeneratorFunction",t),asyncGeneratorFunction:t=>_t(be.asyncGeneratorFunction(t),"AsyncGeneratorFunction",t),asyncFunction:t=>_t(be.asyncFunction(t),"AsyncFunction",t),boundFunction:t=>_t(be.boundFunction(t),"Function",t),regExp:t=>_t(be.regExp(t),"RegExp",t),date:t=>_t(be.date(t),"Date",t),error:t=>_t(be.error(t),"Error",t),map:t=>_t(be.map(t),"Map",t),set:t=>_t(be.set(t),"Set",t),weakMap:t=>_t(be.weakMap(t),"WeakMap",t),weakSet:t=>_t(be.weakSet(t),"WeakSet",t),int8Array:t=>_t(be.int8Array(t),"Int8Array",t),uint8Array:t=>_t(be.uint8Array(t),"Uint8Array",t),uint8ClampedArray:t=>_t(be.uint8ClampedArray(t),"Uint8ClampedArray",t),int16Array:t=>_t(be.int16Array(t),"Int16Array",t),uint16Array:t=>_t(be.uint16Array(t),"Uint16Array",t),int32Array:t=>_t(be.int32Array(t),"Int32Array",t),uint32Array:t=>_t(be.uint32Array(t),"Uint32Array",t),float32Array:t=>_t(be.float32Array(t),"Float32Array",t),float64Array:t=>_t(be.float64Array(t),"Float64Array",t),bigInt64Array:t=>_t(be.bigInt64Array(t),"BigInt64Array",t),bigUint64Array:t=>_t(be.bigUint64Array(t),"BigUint64Array",t),arrayBuffer:t=>_t(be.arrayBuffer(t),"ArrayBuffer",t),sharedArrayBuffer:t=>_t(be.sharedArrayBuffer(t),"SharedArrayBuffer",t),dataView:t=>_t(be.dataView(t),"DataView",t),enumCase:(t,e)=>_t(be.enumCase(t,e),"EnumCase",t),urlInstance:t=>_t(be.urlInstance(t),"URL",t),urlString:t=>_t(be.urlString(t),"string with a URL",t),truthy:t=>_t(be.truthy(t),"truthy",t),falsy:t=>_t(be.falsy(t),"falsy",t),nan:t=>_t(be.nan(t),"NaN",t),primitive:t=>_t(be.primitive(t),"primitive",t),integer:t=>_t(be.integer(t),"integer",t),safeInteger:t=>_t(be.safeInteger(t),"integer",t),plainObject:t=>_t(be.plainObject(t),"plain object",t),typedArray:t=>_t(be.typedArray(t),"TypedArray",t),arrayLike:t=>_t(be.arrayLike(t),"array-like",t),domElement:t=>_t(be.domElement(t),"HTMLElement",t),observable:t=>_t(be.observable(t),"Observable",t),nodeStream:t=>_t(be.nodeStream(t),"Node.js Stream",t),infinite:t=>_t(be.infinite(t),"infinite number",t),emptyArray:t=>_t(be.emptyArray(t),"empty array",t),nonEmptyArray:t=>_t(be.nonEmptyArray(t),"non-empty array",t),emptyString:t=>_t(be.emptyString(t),"empty string",t),emptyStringOrWhitespace:t=>_t(be.emptyStringOrWhitespace(t),"empty string or whitespace",t),nonEmptyString:t=>_t(be.nonEmptyString(t),"non-empty string",t),nonEmptyStringAndNotWhitespace:t=>_t(be.nonEmptyStringAndNotWhitespace(t),"non-empty string and not whitespace",t),emptyObject:t=>_t(be.emptyObject(t),"empty object",t),nonEmptyObject:t=>_t(be.nonEmptyObject(t),"non-empty object",t),emptySet:t=>_t(be.emptySet(t),"empty set",t),nonEmptySet:t=>_t(be.nonEmptySet(t),"non-empty set",t),emptyMap:t=>_t(be.emptyMap(t),"empty map",t),nonEmptyMap:t=>_t(be.nonEmptyMap(t),"non-empty map",t),propertyKey:t=>_t(be.propertyKey(t),"PropertyKey",t),formData:t=>_t(be.formData(t),"FormData",t),urlSearchParams:t=>_t(be.urlSearchParams(t),"URLSearchParams",t),evenInteger:t=>_t(be.evenInteger(t),"even integer",t),oddInteger:t=>_t(be.oddInteger(t),"odd integer",t),directInstanceOf:(t,e)=>_t(be.directInstanceOf(t,e),"T",t),inRange:(t,e)=>_t(be.inRange(t,e),"in range",t),any:(t,...e)=>_t(be.any(t,...e),"predicate returns truthy for any value",e,{multipleValues:!0}),all:(t,...e)=>_t(be.all(t,...e),"predicate returns truthy for all values",e,{multipleValues:!0})};Object.defineProperties(be,{class:{value:be.class_},function:{value:be.function_},null:{value:be.null_}});Object.defineProperties(Op.assert,{class:{value:Op.assert.class_},function:{value:Op.assert.function_},null:{value:Op.assert.null_}});Op.default=be;OQ.exports=be;OQ.exports.default=be;OQ.exports.assert=Op.assert});var Vhe=L((wJt,fH)=>{"use strict";var LQ=class extends Error{constructor(e){super(e||"Promise was canceled"),this.name="CancelError"}get isCanceled(){return!0}},MQ=class t{static fn(e){return(...r)=>new t((s,a,n)=>{r.push(n),e(...r).then(s,a)})}constructor(e){this._cancelHandlers=[],this._isPending=!0,this._isCanceled=!1,this._rejectOnCancel=!0,this._promise=new Promise((r,s)=>{this._reject=s;let a=f=>{this._isPending=!1,r(f)},n=f=>{this._isPending=!1,s(f)},c=f=>{if(!this._isPending)throw new Error("The `onCancel` handler was attached after the promise settled.");this._cancelHandlers.push(f)};return Object.defineProperties(c,{shouldReject:{get:()=>this._rejectOnCancel,set:f=>{this._rejectOnCancel=f}}}),e(a,n,c)})}then(e,r){return this._promise.then(e,r)}catch(e){return this._promise.catch(e)}finally(e){return this._promise.finally(e)}cancel(e){if(!(!this._isPending||this._isCanceled)){if(this._cancelHandlers.length>0)try{for(let r of this._cancelHandlers)r()}catch(r){this._reject(r)}this._isCanceled=!0,this._rejectOnCancel&&this._reject(new LQ(e))}}get isCanceled(){return this._isCanceled}};Object.setPrototypeOf(MQ.prototype,Promise.prototype);fH.exports=MQ;fH.exports.CancelError=LQ});var Khe=L((pH,hH)=>{"use strict";Object.defineProperty(pH,"__esModule",{value:!0});function Pht(t){return t.encrypted}var AH=(t,e)=>{let r;typeof e=="function"?r={connect:e}:r=e;let s=typeof r.connect=="function",a=typeof r.secureConnect=="function",n=typeof r.close=="function",c=()=>{s&&r.connect(),Pht(t)&&a&&(t.authorized?r.secureConnect():t.authorizationError||t.once("secureConnect",r.secureConnect)),n&&t.once("close",r.close)};t.writable&&!t.connecting?c():t.connecting?t.once("connect",c):t.destroyed&&n&&r.close(t._hadError)};pH.default=AH;hH.exports=AH;hH.exports.default=AH});var Jhe=L((dH,mH)=>{"use strict";Object.defineProperty(dH,"__esModule",{value:!0});var xht=Khe(),kht=Number(process.versions.node.split(".")[0]),gH=t=>{let e={start:Date.now(),socket:void 0,lookup:void 0,connect:void 0,secureConnect:void 0,upload:void 0,response:void 0,end:void 0,error:void 0,abort:void 0,phases:{wait:void 0,dns:void 0,tcp:void 0,tls:void 0,request:void 0,firstByte:void 0,download:void 0,total:void 0}};t.timings=e;let r=c=>{let f=c.emit.bind(c);c.emit=(p,...h)=>(p==="error"&&(e.error=Date.now(),e.phases.total=e.error-e.start,c.emit=f),f(p,...h))};r(t),t.prependOnceListener("abort",()=>{e.abort=Date.now(),(!e.response||kht>=13)&&(e.phases.total=Date.now()-e.start)});let s=c=>{e.socket=Date.now(),e.phases.wait=e.socket-e.start;let f=()=>{e.lookup=Date.now(),e.phases.dns=e.lookup-e.socket};c.prependOnceListener("lookup",f),xht.default(c,{connect:()=>{e.connect=Date.now(),e.lookup===void 0&&(c.removeListener("lookup",f),e.lookup=e.connect,e.phases.dns=e.lookup-e.socket),e.phases.tcp=e.connect-e.lookup},secureConnect:()=>{e.secureConnect=Date.now(),e.phases.tls=e.secureConnect-e.connect}})};t.socket?s(t.socket):t.prependOnceListener("socket",s);let a=()=>{var c;e.upload=Date.now(),e.phases.request=e.upload-(c=e.secureConnect,c??e.connect)};return(typeof t.writableFinished=="boolean"?t.writableFinished:t.finished&&t.outputSize===0&&(!t.socket||t.socket.writableLength===0))?a():t.prependOnceListener("finish",a),t.prependOnceListener("response",c=>{e.response=Date.now(),e.phases.firstByte=e.response-e.upload,c.timings=e,r(c),c.prependOnceListener("end",()=>{e.end=Date.now(),e.phases.download=e.end-e.response,e.phases.total=e.end-e.start})}),e};dH.default=gH;mH.exports=gH;mH.exports.default=gH});var r0e=L((BJt,IH)=>{"use strict";var{V4MAPPED:Qht,ADDRCONFIG:Tht,ALL:t0e,promises:{Resolver:zhe},lookup:Rht}=Ie("dns"),{promisify:yH}=Ie("util"),Fht=Ie("os"),pI=Symbol("cacheableLookupCreateConnection"),EH=Symbol("cacheableLookupInstance"),Zhe=Symbol("expires"),Nht=typeof t0e=="number",Xhe=t=>{if(!(t&&typeof t.createConnection=="function"))throw new Error("Expected an Agent instance as the first argument")},Oht=t=>{for(let e of t)e.family!==6&&(e.address=`::ffff:${e.address}`,e.family=6)},$he=()=>{let t=!1,e=!1;for(let r of Object.values(Fht.networkInterfaces()))for(let s of r)if(!s.internal&&(s.family==="IPv6"?e=!0:t=!0,t&&e))return{has4:t,has6:e};return{has4:t,has6:e}},Lht=t=>Symbol.iterator in t,e0e={ttl:!0},Mht={all:!0},_Q=class{constructor({cache:e=new Map,maxTtl:r=1/0,fallbackDuration:s=3600,errorTtl:a=.15,resolver:n=new zhe,lookup:c=Rht}={}){if(this.maxTtl=r,this.errorTtl=a,this._cache=e,this._resolver=n,this._dnsLookup=yH(c),this._resolver instanceof zhe?(this._resolve4=this._resolver.resolve4.bind(this._resolver),this._resolve6=this._resolver.resolve6.bind(this._resolver)):(this._resolve4=yH(this._resolver.resolve4.bind(this._resolver)),this._resolve6=yH(this._resolver.resolve6.bind(this._resolver))),this._iface=$he(),this._pending={},this._nextRemovalTime=!1,this._hostnamesToFallback=new Set,s<1)this._fallback=!1;else{this._fallback=!0;let f=setInterval(()=>{this._hostnamesToFallback.clear()},s*1e3);f.unref&&f.unref()}this.lookup=this.lookup.bind(this),this.lookupAsync=this.lookupAsync.bind(this)}set servers(e){this.clear(),this._resolver.setServers(e)}get servers(){return this._resolver.getServers()}lookup(e,r,s){if(typeof r=="function"?(s=r,r={}):typeof r=="number"&&(r={family:r}),!s)throw new Error("Callback must be a function.");this.lookupAsync(e,r).then(a=>{r.all?s(null,a):s(null,a.address,a.family,a.expires,a.ttl)},s)}async lookupAsync(e,r={}){typeof r=="number"&&(r={family:r});let s=await this.query(e);if(r.family===6){let a=s.filter(n=>n.family===6);r.hints&Qht&&(Nht&&r.hints&t0e||a.length===0)?Oht(s):s=a}else r.family===4&&(s=s.filter(a=>a.family===4));if(r.hints&Tht){let{_iface:a}=this;s=s.filter(n=>n.family===6?a.has6:a.has4)}if(s.length===0){let a=new Error(`cacheableLookup ENOTFOUND ${e}`);throw a.code="ENOTFOUND",a.hostname=e,a}return r.all?s:s[0]}async query(e){let r=await this._cache.get(e);if(!r){let s=this._pending[e];if(s)r=await s;else{let a=this.queryAndCache(e);this._pending[e]=a,r=await a}}return r=r.map(s=>({...s})),r}async _resolve(e){let r=async h=>{try{return await h}catch(E){if(E.code==="ENODATA"||E.code==="ENOTFOUND")return[];throw E}},[s,a]=await Promise.all([this._resolve4(e,e0e),this._resolve6(e,e0e)].map(h=>r(h))),n=0,c=0,f=0,p=Date.now();for(let h of s)h.family=4,h.expires=p+h.ttl*1e3,n=Math.max(n,h.ttl);for(let h of a)h.family=6,h.expires=p+h.ttl*1e3,c=Math.max(c,h.ttl);return s.length>0?a.length>0?f=Math.min(n,c):f=n:f=c,{entries:[...s,...a],cacheTtl:f}}async _lookup(e){try{return{entries:await this._dnsLookup(e,{all:!0}),cacheTtl:0}}catch{return{entries:[],cacheTtl:0}}}async _set(e,r,s){if(this.maxTtl>0&&s>0){s=Math.min(s,this.maxTtl)*1e3,r[Zhe]=Date.now()+s;try{await this._cache.set(e,r,s)}catch(a){this.lookupAsync=async()=>{let n=new Error("Cache Error. Please recreate the CacheableLookup instance.");throw n.cause=a,n}}Lht(this._cache)&&this._tick(s)}}async queryAndCache(e){if(this._hostnamesToFallback.has(e))return this._dnsLookup(e,Mht);try{let r=await this._resolve(e);r.entries.length===0&&this._fallback&&(r=await this._lookup(e),r.entries.length!==0&&this._hostnamesToFallback.add(e));let s=r.entries.length===0?this.errorTtl:r.cacheTtl;return await this._set(e,r.entries,s),delete this._pending[e],r.entries}catch(r){throw delete this._pending[e],r}}_tick(e){let r=this._nextRemovalTime;(!r||e{this._nextRemovalTime=!1;let s=1/0,a=Date.now();for(let[n,c]of this._cache){let f=c[Zhe];a>=f?this._cache.delete(n):f("lookup"in r||(r.lookup=this.lookup),e[pI](r,s))}uninstall(e){if(Xhe(e),e[pI]){if(e[EH]!==this)throw new Error("The agent is not owned by this CacheableLookup instance");e.createConnection=e[pI],delete e[pI],delete e[EH]}}updateInterfaceInfo(){let{_iface:e}=this;this._iface=$he(),(e.has4&&!this._iface.has4||e.has6&&!this._iface.has6)&&this._cache.clear()}clear(e){if(e){this._cache.delete(e);return}this._cache.clear()}};IH.exports=_Q;IH.exports.default=_Q});var s0e=L((vJt,CH)=>{"use strict";var _ht=typeof URL>"u"?Ie("url").URL:URL,Uht="text/plain",Hht="us-ascii",n0e=(t,e)=>e.some(r=>r instanceof RegExp?r.test(t):r===t),jht=(t,{stripHash:e})=>{let r=t.match(/^data:([^,]*?),([^#]*?)(?:#(.*))?$/);if(!r)throw new Error(`Invalid URL: ${t}`);let s=r[1].split(";"),a=r[2],n=e?"":r[3],c=!1;s[s.length-1]==="base64"&&(s.pop(),c=!0);let f=(s.shift()||"").toLowerCase(),h=[...s.map(E=>{let[C,S=""]=E.split("=").map(P=>P.trim());return C==="charset"&&(S=S.toLowerCase(),S===Hht)?"":`${C}${S?`=${S}`:""}`}).filter(Boolean)];return c&&h.push("base64"),(h.length!==0||f&&f!==Uht)&&h.unshift(f),`data:${h.join(";")},${c?a.trim():a}${n?`#${n}`:""}`},i0e=(t,e)=>{if(e={defaultProtocol:"http:",normalizeProtocol:!0,forceHttp:!1,forceHttps:!1,stripAuthentication:!0,stripHash:!1,stripWWW:!0,removeQueryParameters:[/^utm_\w+/i],removeTrailingSlash:!0,removeDirectoryIndex:!1,sortQueryParameters:!0,...e},Reflect.has(e,"normalizeHttps"))throw new Error("options.normalizeHttps is renamed to options.forceHttp");if(Reflect.has(e,"normalizeHttp"))throw new Error("options.normalizeHttp is renamed to options.forceHttps");if(Reflect.has(e,"stripFragment"))throw new Error("options.stripFragment is renamed to options.stripHash");if(t=t.trim(),/^data:/i.test(t))return jht(t,e);let r=t.startsWith("//");!r&&/^\.*\//.test(t)||(t=t.replace(/^(?!(?:\w+:)?\/\/)|^\/\//,e.defaultProtocol));let a=new _ht(t);if(e.forceHttp&&e.forceHttps)throw new Error("The `forceHttp` and `forceHttps` options cannot be used together");if(e.forceHttp&&a.protocol==="https:"&&(a.protocol="http:"),e.forceHttps&&a.protocol==="http:"&&(a.protocol="https:"),e.stripAuthentication&&(a.username="",a.password=""),e.stripHash&&(a.hash=""),a.pathname&&(a.pathname=a.pathname.replace(/((?!:).|^)\/{2,}/g,(n,c)=>/^(?!\/)/g.test(c)?`${c}/`:"/")),a.pathname&&(a.pathname=decodeURI(a.pathname)),e.removeDirectoryIndex===!0&&(e.removeDirectoryIndex=[/^index\.[a-z]+$/]),Array.isArray(e.removeDirectoryIndex)&&e.removeDirectoryIndex.length>0){let n=a.pathname.split("/"),c=n[n.length-1];n0e(c,e.removeDirectoryIndex)&&(n=n.slice(0,n.length-1),a.pathname=n.slice(1).join("/")+"/")}if(a.hostname&&(a.hostname=a.hostname.replace(/\.$/,""),e.stripWWW&&/^www\.([a-z\-\d]{2,63})\.([a-z.]{2,5})$/.test(a.hostname)&&(a.hostname=a.hostname.replace(/^www\./,""))),Array.isArray(e.removeQueryParameters))for(let n of[...a.searchParams.keys()])n0e(n,e.removeQueryParameters)&&a.searchParams.delete(n);return e.sortQueryParameters&&a.searchParams.sort(),e.removeTrailingSlash&&(a.pathname=a.pathname.replace(/\/$/,"")),t=a.toString(),(e.removeTrailingSlash||a.pathname==="/")&&a.hash===""&&(t=t.replace(/\/$/,"")),r&&!e.normalizeProtocol&&(t=t.replace(/^http:\/\//,"//")),e.stripProtocol&&(t=t.replace(/^(?:https?:)?\/\//,"")),t};CH.exports=i0e;CH.exports.default=i0e});var l0e=L((SJt,a0e)=>{a0e.exports=o0e;function o0e(t,e){if(t&&e)return o0e(t)(e);if(typeof t!="function")throw new TypeError("need wrapper function");return Object.keys(t).forEach(function(s){r[s]=t[s]}),r;function r(){for(var s=new Array(arguments.length),a=0;a{var c0e=l0e();wH.exports=c0e(UQ);wH.exports.strict=c0e(u0e);UQ.proto=UQ(function(){Object.defineProperty(Function.prototype,"once",{value:function(){return UQ(this)},configurable:!0}),Object.defineProperty(Function.prototype,"onceStrict",{value:function(){return u0e(this)},configurable:!0})});function UQ(t){var e=function(){return e.called?e.value:(e.called=!0,e.value=t.apply(this,arguments))};return e.called=!1,e}function u0e(t){var e=function(){if(e.called)throw new Error(e.onceError);return e.called=!0,e.value=t.apply(this,arguments)},r=t.name||"Function wrapped with `once`";return e.onceError=r+" shouldn't be called more than once",e.called=!1,e}});var vH=L((bJt,A0e)=>{var qht=BH(),Ght=function(){},Wht=function(t){return t.setHeader&&typeof t.abort=="function"},Yht=function(t){return t.stdio&&Array.isArray(t.stdio)&&t.stdio.length===3},f0e=function(t,e,r){if(typeof e=="function")return f0e(t,null,e);e||(e={}),r=qht(r||Ght);var s=t._writableState,a=t._readableState,n=e.readable||e.readable!==!1&&t.readable,c=e.writable||e.writable!==!1&&t.writable,f=function(){t.writable||p()},p=function(){c=!1,n||r.call(t)},h=function(){n=!1,c||r.call(t)},E=function(I){r.call(t,I?new Error("exited with error code: "+I):null)},C=function(I){r.call(t,I)},S=function(){if(n&&!(a&&a.ended))return r.call(t,new Error("premature close"));if(c&&!(s&&s.ended))return r.call(t,new Error("premature close"))},P=function(){t.req.on("finish",p)};return Wht(t)?(t.on("complete",p),t.on("abort",S),t.req?P():t.on("request",P)):c&&!s&&(t.on("end",f),t.on("close",f)),Yht(t)&&t.on("exit",E),t.on("end",h),t.on("finish",p),e.error!==!1&&t.on("error",C),t.on("close",S),function(){t.removeListener("complete",p),t.removeListener("abort",S),t.removeListener("request",P),t.req&&t.req.removeListener("finish",p),t.removeListener("end",f),t.removeListener("close",f),t.removeListener("finish",p),t.removeListener("exit",E),t.removeListener("end",h),t.removeListener("error",C),t.removeListener("close",S)}};A0e.exports=f0e});var g0e=L((PJt,h0e)=>{var Vht=BH(),Kht=vH(),SH=Ie("fs"),rv=function(){},Jht=/^v?\.0/.test(process.version),HQ=function(t){return typeof t=="function"},zht=function(t){return!Jht||!SH?!1:(t instanceof(SH.ReadStream||rv)||t instanceof(SH.WriteStream||rv))&&HQ(t.close)},Zht=function(t){return t.setHeader&&HQ(t.abort)},Xht=function(t,e,r,s){s=Vht(s);var a=!1;t.on("close",function(){a=!0}),Kht(t,{readable:e,writable:r},function(c){if(c)return s(c);a=!0,s()});var n=!1;return function(c){if(!a&&!n){if(n=!0,zht(t))return t.close(rv);if(Zht(t))return t.abort();if(HQ(t.destroy))return t.destroy();s(c||new Error("stream was destroyed"))}}},p0e=function(t){t()},$ht=function(t,e){return t.pipe(e)},e0t=function(){var t=Array.prototype.slice.call(arguments),e=HQ(t[t.length-1]||rv)&&t.pop()||rv;if(Array.isArray(t[0])&&(t=t[0]),t.length<2)throw new Error("pump requires two streams per minimum");var r,s=t.map(function(a,n){var c=n0;return Xht(a,c,f,function(p){r||(r=p),p&&s.forEach(p0e),!c&&(s.forEach(p0e),e(r))})});return t.reduce($ht)};h0e.exports=e0t});var m0e=L((xJt,d0e)=>{"use strict";var{PassThrough:t0t}=Ie("stream");d0e.exports=t=>{t={...t};let{array:e}=t,{encoding:r}=t,s=r==="buffer",a=!1;e?a=!(r||s):r=r||"utf8",s&&(r=null);let n=new t0t({objectMode:a});r&&n.setEncoding(r);let c=0,f=[];return n.on("data",p=>{f.push(p),a?c=f.length:c+=p.length}),n.getBufferedValue=()=>e?f:s?Buffer.concat(f,c):f.join(""),n.getBufferedLength=()=>c,n}});var y0e=L((kJt,hI)=>{"use strict";var r0t=g0e(),n0t=m0e(),jQ=class extends Error{constructor(){super("maxBuffer exceeded"),this.name="MaxBufferError"}};async function qQ(t,e){if(!t)return Promise.reject(new Error("Expected a stream"));e={maxBuffer:1/0,...e};let{maxBuffer:r}=e,s;return await new Promise((a,n)=>{let c=f=>{f&&(f.bufferedData=s.getBufferedValue()),n(f)};s=r0t(t,n0t(e),f=>{if(f){c(f);return}a()}),s.on("data",()=>{s.getBufferedLength()>r&&c(new jQ)})}),s.getBufferedValue()}hI.exports=qQ;hI.exports.default=qQ;hI.exports.buffer=(t,e)=>qQ(t,{...e,encoding:"buffer"});hI.exports.array=(t,e)=>qQ(t,{...e,array:!0});hI.exports.MaxBufferError=jQ});var I0e=L((TJt,E0e)=>{"use strict";var i0t=new Set([200,203,204,206,300,301,308,404,405,410,414,501]),s0t=new Set([200,203,204,300,301,302,303,307,308,404,405,410,414,501]),o0t=new Set([500,502,503,504]),a0t={date:!0,connection:!0,"keep-alive":!0,"proxy-authenticate":!0,"proxy-authorization":!0,te:!0,trailer:!0,"transfer-encoding":!0,upgrade:!0},l0t={"content-length":!0,"content-encoding":!0,"transfer-encoding":!0,"content-range":!0};function tm(t){let e=parseInt(t,10);return isFinite(e)?e:0}function c0t(t){return t?o0t.has(t.status):!0}function DH(t){let e={};if(!t)return e;let r=t.trim().split(/,/);for(let s of r){let[a,n]=s.split(/=/,2);e[a.trim()]=n===void 0?!0:n.trim().replace(/^"|"$/g,"")}return e}function u0t(t){let e=[];for(let r in t){let s=t[r];e.push(s===!0?r:r+"="+s)}if(e.length)return e.join(", ")}E0e.exports=class{constructor(e,r,{shared:s,cacheHeuristic:a,immutableMinTimeToLive:n,ignoreCargoCult:c,_fromObject:f}={}){if(f){this._fromObject(f);return}if(!r||!r.headers)throw Error("Response headers missing");this._assertRequestHasHeaders(e),this._responseTime=this.now(),this._isShared=s!==!1,this._cacheHeuristic=a!==void 0?a:.1,this._immutableMinTtl=n!==void 0?n:24*3600*1e3,this._status="status"in r?r.status:200,this._resHeaders=r.headers,this._rescc=DH(r.headers["cache-control"]),this._method="method"in e?e.method:"GET",this._url=e.url,this._host=e.headers.host,this._noAuthorization=!e.headers.authorization,this._reqHeaders=r.headers.vary?e.headers:null,this._reqcc=DH(e.headers["cache-control"]),c&&"pre-check"in this._rescc&&"post-check"in this._rescc&&(delete this._rescc["pre-check"],delete this._rescc["post-check"],delete this._rescc["no-cache"],delete this._rescc["no-store"],delete this._rescc["must-revalidate"],this._resHeaders=Object.assign({},this._resHeaders,{"cache-control":u0t(this._rescc)}),delete this._resHeaders.expires,delete this._resHeaders.pragma),r.headers["cache-control"]==null&&/no-cache/.test(r.headers.pragma)&&(this._rescc["no-cache"]=!0)}now(){return Date.now()}storable(){return!!(!this._reqcc["no-store"]&&(this._method==="GET"||this._method==="HEAD"||this._method==="POST"&&this._hasExplicitExpiration())&&s0t.has(this._status)&&!this._rescc["no-store"]&&(!this._isShared||!this._rescc.private)&&(!this._isShared||this._noAuthorization||this._allowsStoringAuthenticated())&&(this._resHeaders.expires||this._rescc["max-age"]||this._isShared&&this._rescc["s-maxage"]||this._rescc.public||i0t.has(this._status)))}_hasExplicitExpiration(){return this._isShared&&this._rescc["s-maxage"]||this._rescc["max-age"]||this._resHeaders.expires}_assertRequestHasHeaders(e){if(!e||!e.headers)throw Error("Request headers missing")}satisfiesWithoutRevalidation(e){this._assertRequestHasHeaders(e);let r=DH(e.headers["cache-control"]);return r["no-cache"]||/no-cache/.test(e.headers.pragma)||r["max-age"]&&this.age()>r["max-age"]||r["min-fresh"]&&this.timeToLive()<1e3*r["min-fresh"]||this.stale()&&!(r["max-stale"]&&!this._rescc["must-revalidate"]&&(r["max-stale"]===!0||r["max-stale"]>this.age()-this.maxAge()))?!1:this._requestMatches(e,!1)}_requestMatches(e,r){return(!this._url||this._url===e.url)&&this._host===e.headers.host&&(!e.method||this._method===e.method||r&&e.method==="HEAD")&&this._varyMatches(e)}_allowsStoringAuthenticated(){return this._rescc["must-revalidate"]||this._rescc.public||this._rescc["s-maxage"]}_varyMatches(e){if(!this._resHeaders.vary)return!0;if(this._resHeaders.vary==="*")return!1;let r=this._resHeaders.vary.trim().toLowerCase().split(/\s*,\s*/);for(let s of r)if(e.headers[s]!==this._reqHeaders[s])return!1;return!0}_copyWithoutHopByHopHeaders(e){let r={};for(let s in e)a0t[s]||(r[s]=e[s]);if(e.connection){let s=e.connection.trim().split(/\s*,\s*/);for(let a of s)delete r[a]}if(r.warning){let s=r.warning.split(/,/).filter(a=>!/^\s*1[0-9][0-9]/.test(a));s.length?r.warning=s.join(",").trim():delete r.warning}return r}responseHeaders(){let e=this._copyWithoutHopByHopHeaders(this._resHeaders),r=this.age();return r>3600*24&&!this._hasExplicitExpiration()&&this.maxAge()>3600*24&&(e.warning=(e.warning?`${e.warning}, `:"")+'113 - "rfc7234 5.5.4"'),e.age=`${Math.round(r)}`,e.date=new Date(this.now()).toUTCString(),e}date(){let e=Date.parse(this._resHeaders.date);return isFinite(e)?e:this._responseTime}age(){let e=this._ageValue(),r=(this.now()-this._responseTime)/1e3;return e+r}_ageValue(){return tm(this._resHeaders.age)}maxAge(){if(!this.storable()||this._rescc["no-cache"]||this._isShared&&this._resHeaders["set-cookie"]&&!this._rescc.public&&!this._rescc.immutable||this._resHeaders.vary==="*")return 0;if(this._isShared){if(this._rescc["proxy-revalidate"])return 0;if(this._rescc["s-maxage"])return tm(this._rescc["s-maxage"])}if(this._rescc["max-age"])return tm(this._rescc["max-age"]);let e=this._rescc.immutable?this._immutableMinTtl:0,r=this.date();if(this._resHeaders.expires){let s=Date.parse(this._resHeaders.expires);return Number.isNaN(s)||ss)return Math.max(e,(r-s)/1e3*this._cacheHeuristic)}return e}timeToLive(){let e=this.maxAge()-this.age(),r=e+tm(this._rescc["stale-if-error"]),s=e+tm(this._rescc["stale-while-revalidate"]);return Math.max(0,e,r,s)*1e3}stale(){return this.maxAge()<=this.age()}_useStaleIfError(){return this.maxAge()+tm(this._rescc["stale-if-error"])>this.age()}useStaleWhileRevalidate(){return this.maxAge()+tm(this._rescc["stale-while-revalidate"])>this.age()}static fromObject(e){return new this(void 0,void 0,{_fromObject:e})}_fromObject(e){if(this._responseTime)throw Error("Reinitialized");if(!e||e.v!==1)throw Error("Invalid serialization");this._responseTime=e.t,this._isShared=e.sh,this._cacheHeuristic=e.ch,this._immutableMinTtl=e.imm!==void 0?e.imm:24*3600*1e3,this._status=e.st,this._resHeaders=e.resh,this._rescc=e.rescc,this._method=e.m,this._url=e.u,this._host=e.h,this._noAuthorization=e.a,this._reqHeaders=e.reqh,this._reqcc=e.reqcc}toObject(){return{v:1,t:this._responseTime,sh:this._isShared,ch:this._cacheHeuristic,imm:this._immutableMinTtl,st:this._status,resh:this._resHeaders,rescc:this._rescc,m:this._method,u:this._url,h:this._host,a:this._noAuthorization,reqh:this._reqHeaders,reqcc:this._reqcc}}revalidationHeaders(e){this._assertRequestHasHeaders(e);let r=this._copyWithoutHopByHopHeaders(e.headers);if(delete r["if-range"],!this._requestMatches(e,!0)||!this.storable())return delete r["if-none-match"],delete r["if-modified-since"],r;if(this._resHeaders.etag&&(r["if-none-match"]=r["if-none-match"]?`${r["if-none-match"]}, ${this._resHeaders.etag}`:this._resHeaders.etag),r["accept-ranges"]||r["if-match"]||r["if-unmodified-since"]||this._method&&this._method!="GET"){if(delete r["if-modified-since"],r["if-none-match"]){let a=r["if-none-match"].split(/,/).filter(n=>!/^\s*W\//.test(n));a.length?r["if-none-match"]=a.join(",").trim():delete r["if-none-match"]}}else this._resHeaders["last-modified"]&&!r["if-modified-since"]&&(r["if-modified-since"]=this._resHeaders["last-modified"]);return r}revalidatedPolicy(e,r){if(this._assertRequestHasHeaders(e),this._useStaleIfError()&&c0t(r))return{modified:!1,matches:!1,policy:this};if(!r||!r.headers)throw Error("Response headers missing");let s=!1;if(r.status!==void 0&&r.status!=304?s=!1:r.headers.etag&&!/^\s*W\//.test(r.headers.etag)?s=this._resHeaders.etag&&this._resHeaders.etag.replace(/^\s*W\//,"")===r.headers.etag:this._resHeaders.etag&&r.headers.etag?s=this._resHeaders.etag.replace(/^\s*W\//,"")===r.headers.etag.replace(/^\s*W\//,""):this._resHeaders["last-modified"]?s=this._resHeaders["last-modified"]===r.headers["last-modified"]:!this._resHeaders.etag&&!this._resHeaders["last-modified"]&&!r.headers.etag&&!r.headers["last-modified"]&&(s=!0),!s)return{policy:new this.constructor(e,r),modified:r.status!=304,matches:!1};let a={};for(let c in this._resHeaders)a[c]=c in r.headers&&!l0t[c]?r.headers[c]:this._resHeaders[c];let n=Object.assign({},r,{status:this._status,method:this._method,headers:a});return{policy:new this.constructor(e,n,{shared:this._isShared,cacheHeuristic:this._cacheHeuristic,immutableMinTimeToLive:this._immutableMinTtl}),modified:!1,matches:!0}}}});var GQ=L((RJt,C0e)=>{"use strict";C0e.exports=t=>{let e={};for(let[r,s]of Object.entries(t))e[r.toLowerCase()]=s;return e}});var B0e=L((FJt,w0e)=>{"use strict";var f0t=Ie("stream").Readable,A0t=GQ(),bH=class extends f0t{constructor(e,r,s,a){if(typeof e!="number")throw new TypeError("Argument `statusCode` should be a number");if(typeof r!="object")throw new TypeError("Argument `headers` should be an object");if(!(s instanceof Buffer))throw new TypeError("Argument `body` should be a buffer");if(typeof a!="string")throw new TypeError("Argument `url` should be a string");super(),this.statusCode=e,this.headers=A0t(r),this.body=s,this.url=a}_read(){this.push(this.body),this.push(null)}};w0e.exports=bH});var S0e=L((NJt,v0e)=>{"use strict";var p0t=["destroy","setTimeout","socket","headers","trailers","rawHeaders","statusCode","httpVersion","httpVersionMinor","httpVersionMajor","rawTrailers","statusMessage"];v0e.exports=(t,e)=>{let r=new Set(Object.keys(t).concat(p0t));for(let s of r)s in e||(e[s]=typeof t[s]=="function"?t[s].bind(t):t[s])}});var b0e=L((OJt,D0e)=>{"use strict";var h0t=Ie("stream").PassThrough,g0t=S0e(),d0t=t=>{if(!(t&&t.pipe))throw new TypeError("Parameter `response` must be a response stream.");let e=new h0t;return g0t(t,e),t.pipe(e)};D0e.exports=d0t});var P0e=L(PH=>{PH.stringify=function t(e){if(typeof e>"u")return e;if(e&&Buffer.isBuffer(e))return JSON.stringify(":base64:"+e.toString("base64"));if(e&&e.toJSON&&(e=e.toJSON()),e&&typeof e=="object"){var r="",s=Array.isArray(e);r=s?"[":"{";var a=!0;for(var n in e){var c=typeof e[n]=="function"||!s&&typeof e[n]>"u";Object.hasOwnProperty.call(e,n)&&!c&&(a||(r+=","),a=!1,s?e[n]==null?r+="null":r+=t(e[n]):e[n]!==void 0&&(r+=t(n)+":"+t(e[n])))}return r+=s?"]":"}",r}else return typeof e=="string"?JSON.stringify(/^:/.test(e)?":"+e:e):typeof e>"u"?"null":JSON.stringify(e)};PH.parse=function(t){return JSON.parse(t,function(e,r){return typeof r=="string"?/^:base64:/.test(r)?Buffer.from(r.substring(8),"base64"):/^:/.test(r)?r.substring(1):r:r})}});var T0e=L((MJt,Q0e)=>{"use strict";var m0t=Ie("events"),x0e=P0e(),y0t=t=>{let e={redis:"@keyv/redis",rediss:"@keyv/redis",mongodb:"@keyv/mongo",mongo:"@keyv/mongo",sqlite:"@keyv/sqlite",postgresql:"@keyv/postgres",postgres:"@keyv/postgres",mysql:"@keyv/mysql",etcd:"@keyv/etcd",offline:"@keyv/offline",tiered:"@keyv/tiered"};if(t.adapter||t.uri){let r=t.adapter||/^[^:+]*/.exec(t.uri)[0];return new(Ie(e[r]))(t)}return new Map},k0e=["sqlite","postgres","mysql","mongo","redis","tiered"],xH=class extends m0t{constructor(e,{emitErrors:r=!0,...s}={}){if(super(),this.opts={namespace:"keyv",serialize:x0e.stringify,deserialize:x0e.parse,...typeof e=="string"?{uri:e}:e,...s},!this.opts.store){let n={...this.opts};this.opts.store=y0t(n)}if(this.opts.compression){let n=this.opts.compression;this.opts.serialize=n.serialize.bind(n),this.opts.deserialize=n.deserialize.bind(n)}typeof this.opts.store.on=="function"&&r&&this.opts.store.on("error",n=>this.emit("error",n)),this.opts.store.namespace=this.opts.namespace;let a=n=>async function*(){for await(let[c,f]of typeof n=="function"?n(this.opts.store.namespace):n){let p=await this.opts.deserialize(f);if(!(this.opts.store.namespace&&!c.includes(this.opts.store.namespace))){if(typeof p.expires=="number"&&Date.now()>p.expires){this.delete(c);continue}yield[this._getKeyUnprefix(c),p.value]}}};typeof this.opts.store[Symbol.iterator]=="function"&&this.opts.store instanceof Map?this.iterator=a(this.opts.store):typeof this.opts.store.iterator=="function"&&this.opts.store.opts&&this._checkIterableAdaptar()&&(this.iterator=a(this.opts.store.iterator.bind(this.opts.store)))}_checkIterableAdaptar(){return k0e.includes(this.opts.store.opts.dialect)||k0e.findIndex(e=>this.opts.store.opts.url.includes(e))>=0}_getKeyPrefix(e){return`${this.opts.namespace}:${e}`}_getKeyPrefixArray(e){return e.map(r=>`${this.opts.namespace}:${r}`)}_getKeyUnprefix(e){return e.split(":").splice(1).join(":")}get(e,r){let{store:s}=this.opts,a=Array.isArray(e),n=a?this._getKeyPrefixArray(e):this._getKeyPrefix(e);if(a&&s.getMany===void 0){let c=[];for(let f of n)c.push(Promise.resolve().then(()=>s.get(f)).then(p=>typeof p=="string"?this.opts.deserialize(p):this.opts.compression?this.opts.deserialize(p):p).then(p=>{if(p!=null)return typeof p.expires=="number"&&Date.now()>p.expires?this.delete(f).then(()=>{}):r&&r.raw?p:p.value}));return Promise.allSettled(c).then(f=>{let p=[];for(let h of f)p.push(h.value);return p})}return Promise.resolve().then(()=>a?s.getMany(n):s.get(n)).then(c=>typeof c=="string"?this.opts.deserialize(c):this.opts.compression?this.opts.deserialize(c):c).then(c=>{if(c!=null)return a?c.map((f,p)=>{if(typeof f=="string"&&(f=this.opts.deserialize(f)),f!=null){if(typeof f.expires=="number"&&Date.now()>f.expires){this.delete(e[p]).then(()=>{});return}return r&&r.raw?f:f.value}}):typeof c.expires=="number"&&Date.now()>c.expires?this.delete(e).then(()=>{}):r&&r.raw?c:c.value})}set(e,r,s){let a=this._getKeyPrefix(e);typeof s>"u"&&(s=this.opts.ttl),s===0&&(s=void 0);let{store:n}=this.opts;return Promise.resolve().then(()=>{let c=typeof s=="number"?Date.now()+s:null;return typeof r=="symbol"&&this.emit("error","symbol cannot be serialized"),r={value:r,expires:c},this.opts.serialize(r)}).then(c=>n.set(a,c,s)).then(()=>!0)}delete(e){let{store:r}=this.opts;if(Array.isArray(e)){let a=this._getKeyPrefixArray(e);if(r.deleteMany===void 0){let n=[];for(let c of a)n.push(r.delete(c));return Promise.allSettled(n).then(c=>c.every(f=>f.value===!0))}return Promise.resolve().then(()=>r.deleteMany(a))}let s=this._getKeyPrefix(e);return Promise.resolve().then(()=>r.delete(s))}clear(){let{store:e}=this.opts;return Promise.resolve().then(()=>e.clear())}has(e){let r=this._getKeyPrefix(e),{store:s}=this.opts;return Promise.resolve().then(async()=>typeof s.has=="function"?s.has(r):await s.get(r)!==void 0)}disconnect(){let{store:e}=this.opts;if(typeof e.disconnect=="function")return e.disconnect()}};Q0e.exports=xH});var N0e=L((UJt,F0e)=>{"use strict";var E0t=Ie("events"),WQ=Ie("url"),I0t=s0e(),C0t=y0e(),kH=I0e(),R0e=B0e(),w0t=GQ(),B0t=b0e(),v0t=T0e(),nv=class t{constructor(e,r){if(typeof e!="function")throw new TypeError("Parameter `request` must be a function");return this.cache=new v0t({uri:typeof r=="string"&&r,store:typeof r!="string"&&r,namespace:"cacheable-request"}),this.createCacheableRequest(e)}createCacheableRequest(e){return(r,s)=>{let a;if(typeof r=="string")a=QH(WQ.parse(r)),r={};else if(r instanceof WQ.URL)a=QH(WQ.parse(r.toString())),r={};else{let[C,...S]=(r.path||"").split("?"),P=S.length>0?`?${S.join("?")}`:"";a=QH({...r,pathname:C,search:P})}r={headers:{},method:"GET",cache:!0,strictTtl:!1,automaticFailover:!1,...r,...S0t(a)},r.headers=w0t(r.headers);let n=new E0t,c=I0t(WQ.format(a),{stripWWW:!1,removeTrailingSlash:!1,stripAuthentication:!1}),f=`${r.method}:${c}`,p=!1,h=!1,E=C=>{h=!0;let S=!1,P,I=new Promise(N=>{P=()=>{S||(S=!0,N())}}),R=N=>{if(p&&!C.forceRefresh){N.status=N.statusCode;let W=kH.fromObject(p.cachePolicy).revalidatedPolicy(C,N);if(!W.modified){let te=W.policy.responseHeaders();N=new R0e(p.statusCode,te,p.body,p.url),N.cachePolicy=W.policy,N.fromCache=!0}}N.fromCache||(N.cachePolicy=new kH(C,N,C),N.fromCache=!1);let U;C.cache&&N.cachePolicy.storable()?(U=B0t(N),(async()=>{try{let W=C0t.buffer(N);if(await Promise.race([I,new Promise(ce=>N.once("end",ce))]),S)return;let te=await W,ie={cachePolicy:N.cachePolicy.toObject(),url:N.url,statusCode:N.fromCache?p.statusCode:N.statusCode,body:te},Ae=C.strictTtl?N.cachePolicy.timeToLive():void 0;C.maxTtl&&(Ae=Ae?Math.min(Ae,C.maxTtl):C.maxTtl),await this.cache.set(f,ie,Ae)}catch(W){n.emit("error",new t.CacheError(W))}})()):C.cache&&p&&(async()=>{try{await this.cache.delete(f)}catch(W){n.emit("error",new t.CacheError(W))}})(),n.emit("response",U||N),typeof s=="function"&&s(U||N)};try{let N=e(C,R);N.once("error",P),N.once("abort",P),n.emit("request",N)}catch(N){n.emit("error",new t.RequestError(N))}};return(async()=>{let C=async P=>{await Promise.resolve();let I=P.cache?await this.cache.get(f):void 0;if(typeof I>"u")return E(P);let R=kH.fromObject(I.cachePolicy);if(R.satisfiesWithoutRevalidation(P)&&!P.forceRefresh){let N=R.responseHeaders(),U=new R0e(I.statusCode,N,I.body,I.url);U.cachePolicy=R,U.fromCache=!0,n.emit("response",U),typeof s=="function"&&s(U)}else p=I,P.headers=R.revalidationHeaders(P),E(P)},S=P=>n.emit("error",new t.CacheError(P));this.cache.once("error",S),n.on("response",()=>this.cache.removeListener("error",S));try{await C(r)}catch(P){r.automaticFailover&&!h&&E(r),n.emit("error",new t.CacheError(P))}})(),n}}};function S0t(t){let e={...t};return e.path=`${t.pathname||"/"}${t.search||""}`,delete e.pathname,delete e.search,e}function QH(t){return{protocol:t.protocol,auth:t.auth,hostname:t.hostname||t.host||"localhost",port:t.port,pathname:t.pathname,search:t.search}}nv.RequestError=class extends Error{constructor(t){super(t.message),this.name="RequestError",Object.assign(this,t)}};nv.CacheError=class extends Error{constructor(t){super(t.message),this.name="CacheError",Object.assign(this,t)}};F0e.exports=nv});var L0e=L((qJt,O0e)=>{"use strict";var D0t=["aborted","complete","headers","httpVersion","httpVersionMinor","httpVersionMajor","method","rawHeaders","rawTrailers","setTimeout","socket","statusCode","statusMessage","trailers","url"];O0e.exports=(t,e)=>{if(e._readableState.autoDestroy)throw new Error("The second stream must have the `autoDestroy` option set to `false`");let r=new Set(Object.keys(t).concat(D0t)),s={};for(let a of r)a in e||(s[a]={get(){let n=t[a];return typeof n=="function"?n.bind(t):n},set(n){t[a]=n},enumerable:!0,configurable:!1});return Object.defineProperties(e,s),t.once("aborted",()=>{e.destroy(),e.emit("aborted")}),t.once("close",()=>{t.complete&&e.readable?e.once("end",()=>{e.emit("close")}):e.emit("close")}),e}});var _0e=L((GJt,M0e)=>{"use strict";var{Transform:b0t,PassThrough:P0t}=Ie("stream"),TH=Ie("zlib"),x0t=L0e();M0e.exports=t=>{let e=(t.headers["content-encoding"]||"").toLowerCase();if(!["gzip","deflate","br"].includes(e))return t;let r=e==="br";if(r&&typeof TH.createBrotliDecompress!="function")return t.destroy(new Error("Brotli is not supported on Node.js < 12")),t;let s=!0,a=new b0t({transform(f,p,h){s=!1,h(null,f)},flush(f){f()}}),n=new P0t({autoDestroy:!1,destroy(f,p){t.destroy(),p(f)}}),c=r?TH.createBrotliDecompress():TH.createUnzip();return c.once("error",f=>{if(s&&!t.readable){n.end();return}n.destroy(f)}),x0t(t,n),t.pipe(a).pipe(c).pipe(n),n}});var FH=L((WJt,U0e)=>{"use strict";var RH=class{constructor(e={}){if(!(e.maxSize&&e.maxSize>0))throw new TypeError("`maxSize` must be a number greater than 0");this.maxSize=e.maxSize,this.onEviction=e.onEviction,this.cache=new Map,this.oldCache=new Map,this._size=0}_set(e,r){if(this.cache.set(e,r),this._size++,this._size>=this.maxSize){if(this._size=0,typeof this.onEviction=="function")for(let[s,a]of this.oldCache.entries())this.onEviction(s,a);this.oldCache=this.cache,this.cache=new Map}}get(e){if(this.cache.has(e))return this.cache.get(e);if(this.oldCache.has(e)){let r=this.oldCache.get(e);return this.oldCache.delete(e),this._set(e,r),r}}set(e,r){return this.cache.has(e)?this.cache.set(e,r):this._set(e,r),this}has(e){return this.cache.has(e)||this.oldCache.has(e)}peek(e){if(this.cache.has(e))return this.cache.get(e);if(this.oldCache.has(e))return this.oldCache.get(e)}delete(e){let r=this.cache.delete(e);return r&&this._size--,this.oldCache.delete(e)||r}clear(){this.cache.clear(),this.oldCache.clear(),this._size=0}*keys(){for(let[e]of this)yield e}*values(){for(let[,e]of this)yield e}*[Symbol.iterator](){for(let e of this.cache)yield e;for(let e of this.oldCache){let[r]=e;this.cache.has(r)||(yield e)}}get size(){let e=0;for(let r of this.oldCache.keys())this.cache.has(r)||e++;return Math.min(this._size+e,this.maxSize)}};U0e.exports=RH});var OH=L((YJt,G0e)=>{"use strict";var k0t=Ie("events"),Q0t=Ie("tls"),T0t=Ie("http2"),R0t=FH(),xa=Symbol("currentStreamsCount"),H0e=Symbol("request"),Nc=Symbol("cachedOriginSet"),gI=Symbol("gracefullyClosing"),F0t=["maxDeflateDynamicTableSize","maxSessionMemory","maxHeaderListPairs","maxOutstandingPings","maxReservedRemoteStreams","maxSendHeaderBlockLength","paddingStrategy","localAddress","path","rejectUnauthorized","minDHSize","ca","cert","clientCertEngine","ciphers","key","pfx","servername","minVersion","maxVersion","secureProtocol","crl","honorCipherOrder","ecdhCurve","dhparam","secureOptions","sessionIdContext"],N0t=(t,e,r)=>{let s=0,a=t.length;for(;s>>1;r(t[n],e)?s=n+1:a=n}return s},O0t=(t,e)=>t.remoteSettings.maxConcurrentStreams>e.remoteSettings.maxConcurrentStreams,NH=(t,e)=>{for(let r of t)r[Nc].lengthe[Nc].includes(s))&&r[xa]+e[xa]<=e.remoteSettings.maxConcurrentStreams&&q0e(r)},L0t=(t,e)=>{for(let r of t)e[Nc].lengthr[Nc].includes(s))&&e[xa]+r[xa]<=r.remoteSettings.maxConcurrentStreams&&q0e(e)},j0e=({agent:t,isFree:e})=>{let r={};for(let s in t.sessions){let n=t.sessions[s].filter(c=>{let f=c[rm.kCurrentStreamsCount]{t[gI]=!0,t[xa]===0&&t.close()},rm=class t extends k0t{constructor({timeout:e=6e4,maxSessions:r=1/0,maxFreeSessions:s=10,maxCachedTlsSessions:a=100}={}){super(),this.sessions={},this.queue={},this.timeout=e,this.maxSessions=r,this.maxFreeSessions=s,this._freeSessionsCount=0,this._sessionsCount=0,this.settings={enablePush:!1},this.tlsSessionCache=new R0t({maxSize:a})}static normalizeOrigin(e,r){return typeof e=="string"&&(e=new URL(e)),r&&e.hostname!==r&&(e.hostname=r),e.origin}normalizeOptions(e){let r="";if(e)for(let s of F0t)e[s]&&(r+=`:${e[s]}`);return r}_tryToCreateNewSession(e,r){if(!(e in this.queue)||!(r in this.queue[e]))return;let s=this.queue[e][r];this._sessionsCount{Array.isArray(s)?(s=[...s],a()):s=[{resolve:a,reject:n}];let c=this.normalizeOptions(r),f=t.normalizeOrigin(e,r&&r.servername);if(f===void 0){for(let{reject:E}of s)E(new TypeError("The `origin` argument needs to be a string or an URL object"));return}if(c in this.sessions){let E=this.sessions[c],C=-1,S=-1,P;for(let I of E){let R=I.remoteSettings.maxConcurrentStreams;if(R=R||I[gI]||I.destroyed)continue;P||(C=R),N>S&&(P=I,S=N)}}if(P){if(s.length!==1){for(let{reject:I}of s){let R=new Error(`Expected the length of listeners to be 1, got ${s.length}. Please report this to https://github.com/szmarczak/http2-wrapper/`);I(R)}return}s[0].resolve(P);return}}if(c in this.queue){if(f in this.queue[c]){this.queue[c][f].listeners.push(...s),this._tryToCreateNewSession(c,f);return}}else this.queue[c]={};let p=()=>{c in this.queue&&this.queue[c][f]===h&&(delete this.queue[c][f],Object.keys(this.queue[c]).length===0&&delete this.queue[c])},h=()=>{let E=`${f}:${c}`,C=!1;try{let S=T0t.connect(e,{createConnection:this.createConnection,settings:this.settings,session:this.tlsSessionCache.get(E),...r});S[xa]=0,S[gI]=!1;let P=()=>S[xa]{this.tlsSessionCache.set(E,N)}),S.once("error",N=>{for(let{reject:U}of s)U(N);this.tlsSessionCache.delete(E)}),S.setTimeout(this.timeout,()=>{S.destroy()}),S.once("close",()=>{if(C){I&&this._freeSessionsCount--,this._sessionsCount--;let N=this.sessions[c];N.splice(N.indexOf(S),1),N.length===0&&delete this.sessions[c]}else{let N=new Error("Session closed without receiving a SETTINGS frame");N.code="HTTP2WRAPPER_NOSETTINGS";for(let{reject:U}of s)U(N);p()}this._tryToCreateNewSession(c,f)});let R=()=>{if(!(!(c in this.queue)||!P())){for(let N of S[Nc])if(N in this.queue[c]){let{listeners:U}=this.queue[c][N];for(;U.length!==0&&P();)U.shift().resolve(S);let W=this.queue[c];if(W[N].listeners.length===0&&(delete W[N],Object.keys(W).length===0)){delete this.queue[c];break}if(!P())break}}};S.on("origin",()=>{S[Nc]=S.originSet,P()&&(R(),NH(this.sessions[c],S))}),S.once("remoteSettings",()=>{if(S.ref(),S.unref(),this._sessionsCount++,h.destroyed){let N=new Error("Agent has been destroyed");for(let U of s)U.reject(N);S.destroy();return}S[Nc]=S.originSet;{let N=this.sessions;if(c in N){let U=N[c];U.splice(N0t(U,S,O0t),0,S)}else N[c]=[S]}this._freeSessionsCount+=1,C=!0,this.emit("session",S),R(),p(),S[xa]===0&&this._freeSessionsCount>this.maxFreeSessions&&S.close(),s.length!==0&&(this.getSession(f,r,s),s.length=0),S.on("remoteSettings",()=>{R(),NH(this.sessions[c],S)})}),S[H0e]=S.request,S.request=(N,U)=>{if(S[gI])throw new Error("The session is gracefully closing. No new streams are allowed.");let W=S[H0e](N,U);return S.ref(),++S[xa],S[xa]===S.remoteSettings.maxConcurrentStreams&&this._freeSessionsCount--,W.once("close",()=>{if(I=P(),--S[xa],!S.destroyed&&!S.closed&&(L0t(this.sessions[c],S),P()&&!S.closed)){I||(this._freeSessionsCount++,I=!0);let te=S[xa]===0;te&&S.unref(),te&&(this._freeSessionsCount>this.maxFreeSessions||S[gI])?S.close():(NH(this.sessions[c],S),R())}}),W}}catch(S){for(let P of s)P.reject(S);p()}};h.listeners=s,h.completed=!1,h.destroyed=!1,this.queue[c][f]=h,this._tryToCreateNewSession(c,f)})}request(e,r,s,a){return new Promise((n,c)=>{this.getSession(e,r,[{reject:c,resolve:f=>{try{n(f.request(s,a))}catch(p){c(p)}}}])})}createConnection(e,r){return t.connect(e,r)}static connect(e,r){r.ALPNProtocols=["h2"];let s=e.port||443,a=e.hostname||e.host;return typeof r.servername>"u"&&(r.servername=a),Q0t.connect(s,a,r)}closeFreeSessions(){for(let e of Object.values(this.sessions))for(let r of e)r[xa]===0&&r.close()}destroy(e){for(let r of Object.values(this.sessions))for(let s of r)s.destroy(e);for(let r of Object.values(this.queue))for(let s of Object.values(r))s.destroyed=!0;this.queue={}}get freeSessions(){return j0e({agent:this,isFree:!0})}get busySessions(){return j0e({agent:this,isFree:!1})}};rm.kCurrentStreamsCount=xa;rm.kGracefullyClosing=gI;G0e.exports={Agent:rm,globalAgent:new rm}});var MH=L((VJt,W0e)=>{"use strict";var{Readable:M0t}=Ie("stream"),LH=class extends M0t{constructor(e,r){super({highWaterMark:r,autoDestroy:!1}),this.statusCode=null,this.statusMessage="",this.httpVersion="2.0",this.httpVersionMajor=2,this.httpVersionMinor=0,this.headers={},this.trailers={},this.req=null,this.aborted=!1,this.complete=!1,this.upgrade=null,this.rawHeaders=[],this.rawTrailers=[],this.socket=e,this.connection=e,this._dumped=!1}_destroy(e){this.req._request.destroy(e)}setTimeout(e,r){return this.req.setTimeout(e,r),this}_dump(){this._dumped||(this._dumped=!0,this.removeAllListeners("data"),this.resume())}_read(){this.req&&this.req._request.resume()}};W0e.exports=LH});var _H=L((KJt,Y0e)=>{"use strict";Y0e.exports=t=>{let e={protocol:t.protocol,hostname:typeof t.hostname=="string"&&t.hostname.startsWith("[")?t.hostname.slice(1,-1):t.hostname,host:t.host,hash:t.hash,search:t.search,pathname:t.pathname,href:t.href,path:`${t.pathname||""}${t.search||""}`};return typeof t.port=="string"&&t.port.length!==0&&(e.port=Number(t.port)),(t.username||t.password)&&(e.auth=`${t.username||""}:${t.password||""}`),e}});var K0e=L((JJt,V0e)=>{"use strict";V0e.exports=(t,e,r)=>{for(let s of r)t.on(s,(...a)=>e.emit(s,...a))}});var z0e=L((zJt,J0e)=>{"use strict";J0e.exports=t=>{switch(t){case":method":case":scheme":case":authority":case":path":return!0;default:return!1}}});var X0e=L((XJt,Z0e)=>{"use strict";var dI=(t,e,r)=>{Z0e.exports[e]=class extends t{constructor(...a){super(typeof r=="string"?r:r(a)),this.name=`${super.name} [${e}]`,this.code=e}}};dI(TypeError,"ERR_INVALID_ARG_TYPE",t=>{let e=t[0].includes(".")?"property":"argument",r=t[1],s=Array.isArray(r);return s&&(r=`${r.slice(0,-1).join(", ")} or ${r.slice(-1)}`),`The "${t[0]}" ${e} must be ${s?"one of":"of"} type ${r}. Received ${typeof t[2]}`});dI(TypeError,"ERR_INVALID_PROTOCOL",t=>`Protocol "${t[0]}" not supported. Expected "${t[1]}"`);dI(Error,"ERR_HTTP_HEADERS_SENT",t=>`Cannot ${t[0]} headers after they are sent to the client`);dI(TypeError,"ERR_INVALID_HTTP_TOKEN",t=>`${t[0]} must be a valid HTTP token [${t[1]}]`);dI(TypeError,"ERR_HTTP_INVALID_HEADER_VALUE",t=>`Invalid value "${t[0]} for header "${t[1]}"`);dI(TypeError,"ERR_INVALID_CHAR",t=>`Invalid character in ${t[0]} [${t[1]}]`)});var GH=L(($Jt,sge)=>{"use strict";var _0t=Ie("http2"),{Writable:U0t}=Ie("stream"),{Agent:$0e,globalAgent:H0t}=OH(),j0t=MH(),q0t=_H(),G0t=K0e(),W0t=z0e(),{ERR_INVALID_ARG_TYPE:UH,ERR_INVALID_PROTOCOL:Y0t,ERR_HTTP_HEADERS_SENT:ege,ERR_INVALID_HTTP_TOKEN:V0t,ERR_HTTP_INVALID_HEADER_VALUE:K0t,ERR_INVALID_CHAR:J0t}=X0e(),{HTTP2_HEADER_STATUS:tge,HTTP2_HEADER_METHOD:rge,HTTP2_HEADER_PATH:nge,HTTP2_METHOD_CONNECT:z0t}=_0t.constants,Jo=Symbol("headers"),HH=Symbol("origin"),jH=Symbol("session"),ige=Symbol("options"),YQ=Symbol("flushedHeaders"),iv=Symbol("jobs"),Z0t=/^[\^`\-\w!#$%&*+.|~]+$/,X0t=/[^\t\u0020-\u007E\u0080-\u00FF]/,qH=class extends U0t{constructor(e,r,s){super({autoDestroy:!1});let a=typeof e=="string"||e instanceof URL;if(a&&(e=q0t(e instanceof URL?e:new URL(e))),typeof r=="function"||r===void 0?(s=r,r=a?e:{...e}):r={...e,...r},r.h2session)this[jH]=r.h2session;else if(r.agent===!1)this.agent=new $0e({maxFreeSessions:0});else if(typeof r.agent>"u"||r.agent===null)typeof r.createConnection=="function"?(this.agent=new $0e({maxFreeSessions:0}),this.agent.createConnection=r.createConnection):this.agent=H0t;else if(typeof r.agent.request=="function")this.agent=r.agent;else throw new UH("options.agent",["Agent-like Object","undefined","false"],r.agent);if(r.protocol&&r.protocol!=="https:")throw new Y0t(r.protocol,"https:");let n=r.port||r.defaultPort||this.agent&&this.agent.defaultPort||443,c=r.hostname||r.host||"localhost";delete r.hostname,delete r.host,delete r.port;let{timeout:f}=r;if(r.timeout=void 0,this[Jo]=Object.create(null),this[iv]=[],this.socket=null,this.connection=null,this.method=r.method||"GET",this.path=r.path,this.res=null,this.aborted=!1,this.reusedSocket=!1,r.headers)for(let[p,h]of Object.entries(r.headers))this.setHeader(p,h);r.auth&&!("authorization"in this[Jo])&&(this[Jo].authorization="Basic "+Buffer.from(r.auth).toString("base64")),r.session=r.tlsSession,r.path=r.socketPath,this[ige]=r,n===443?(this[HH]=`https://${c}`,":authority"in this[Jo]||(this[Jo][":authority"]=c)):(this[HH]=`https://${c}:${n}`,":authority"in this[Jo]||(this[Jo][":authority"]=`${c}:${n}`)),f&&this.setTimeout(f),s&&this.once("response",s),this[YQ]=!1}get method(){return this[Jo][rge]}set method(e){e&&(this[Jo][rge]=e.toUpperCase())}get path(){return this[Jo][nge]}set path(e){e&&(this[Jo][nge]=e)}get _mustNotHaveABody(){return this.method==="GET"||this.method==="HEAD"||this.method==="DELETE"}_write(e,r,s){if(this._mustNotHaveABody){s(new Error("The GET, HEAD and DELETE methods must NOT have a body"));return}this.flushHeaders();let a=()=>this._request.write(e,r,s);this._request?a():this[iv].push(a)}_final(e){if(this.destroyed)return;this.flushHeaders();let r=()=>{if(this._mustNotHaveABody){e();return}this._request.end(e)};this._request?r():this[iv].push(r)}abort(){this.res&&this.res.complete||(this.aborted||process.nextTick(()=>this.emit("abort")),this.aborted=!0,this.destroy())}_destroy(e,r){this.res&&this.res._dump(),this._request&&this._request.destroy(),r(e)}async flushHeaders(){if(this[YQ]||this.destroyed)return;this[YQ]=!0;let e=this.method===z0t,r=s=>{if(this._request=s,this.destroyed){s.destroy();return}e||G0t(s,this,["timeout","continue","close","error"]);let a=c=>(...f)=>{!this.writable&&!this.destroyed?c(...f):this.once("finish",()=>{c(...f)})};s.once("response",a((c,f,p)=>{let h=new j0t(this.socket,s.readableHighWaterMark);this.res=h,h.req=this,h.statusCode=c[tge],h.headers=c,h.rawHeaders=p,h.once("end",()=>{this.aborted?(h.aborted=!0,h.emit("aborted")):(h.complete=!0,h.socket=null,h.connection=null)}),e?(h.upgrade=!0,this.emit("connect",h,s,Buffer.alloc(0))?this.emit("close"):s.destroy()):(s.on("data",E=>{!h._dumped&&!h.push(E)&&s.pause()}),s.once("end",()=>{h.push(null)}),this.emit("response",h)||h._dump())})),s.once("headers",a(c=>this.emit("information",{statusCode:c[tge]}))),s.once("trailers",a((c,f,p)=>{let{res:h}=this;h.trailers=c,h.rawTrailers=p}));let{socket:n}=s.session;this.socket=n,this.connection=n;for(let c of this[iv])c();this.emit("socket",this.socket)};if(this[jH])try{r(this[jH].request(this[Jo]))}catch(s){this.emit("error",s)}else{this.reusedSocket=!0;try{r(await this.agent.request(this[HH],this[ige],this[Jo]))}catch(s){this.emit("error",s)}}}getHeader(e){if(typeof e!="string")throw new UH("name","string",e);return this[Jo][e.toLowerCase()]}get headersSent(){return this[YQ]}removeHeader(e){if(typeof e!="string")throw new UH("name","string",e);if(this.headersSent)throw new ege("remove");delete this[Jo][e.toLowerCase()]}setHeader(e,r){if(this.headersSent)throw new ege("set");if(typeof e!="string"||!Z0t.test(e)&&!W0t(e))throw new V0t("Header name",e);if(typeof r>"u")throw new K0t(r,e);if(X0t.test(r))throw new J0t("header content",e);this[Jo][e.toLowerCase()]=r}setNoDelay(){}setSocketKeepAlive(){}setTimeout(e,r){let s=()=>this._request.setTimeout(e,r);return this._request?s():this[iv].push(s),this}get maxHeadersCount(){if(!this.destroyed&&this._request)return this._request.session.localSettings.maxHeaderListSize}set maxHeadersCount(e){}};sge.exports=qH});var age=L((ezt,oge)=>{"use strict";var $0t=Ie("tls");oge.exports=(t={},e=$0t.connect)=>new Promise((r,s)=>{let a=!1,n,c=async()=>{await p,n.off("timeout",f),n.off("error",s),t.resolveSocket?(r({alpnProtocol:n.alpnProtocol,socket:n,timeout:a}),a&&(await Promise.resolve(),n.emit("timeout"))):(n.destroy(),r({alpnProtocol:n.alpnProtocol,timeout:a}))},f=async()=>{a=!0,c()},p=(async()=>{try{n=await e(t,c),n.on("error",s),n.once("timeout",f)}catch(h){s(h)}})()})});var cge=L((tzt,lge)=>{"use strict";var egt=Ie("net");lge.exports=t=>{let e=t.host,r=t.headers&&t.headers.host;return r&&(r.startsWith("[")?r.indexOf("]")===-1?e=r:e=r.slice(1,-1):e=r.split(":",1)[0]),egt.isIP(e)?"":e}});var Age=L((rzt,YH)=>{"use strict";var uge=Ie("http"),WH=Ie("https"),tgt=age(),rgt=FH(),ngt=GH(),igt=cge(),sgt=_H(),VQ=new rgt({maxSize:100}),sv=new Map,fge=(t,e,r)=>{e._httpMessage={shouldKeepAlive:!0};let s=()=>{t.emit("free",e,r)};e.on("free",s);let a=()=>{t.removeSocket(e,r)};e.on("close",a);let n=()=>{t.removeSocket(e,r),e.off("close",a),e.off("free",s),e.off("agentRemove",n)};e.on("agentRemove",n),t.emit("free",e,r)},ogt=async t=>{let e=`${t.host}:${t.port}:${t.ALPNProtocols.sort()}`;if(!VQ.has(e)){if(sv.has(e))return(await sv.get(e)).alpnProtocol;let{path:r,agent:s}=t;t.path=t.socketPath;let a=tgt(t);sv.set(e,a);try{let{socket:n,alpnProtocol:c}=await a;if(VQ.set(e,c),t.path=r,c==="h2")n.destroy();else{let{globalAgent:f}=WH,p=WH.Agent.prototype.createConnection;s?s.createConnection===p?fge(s,n,t):n.destroy():f.createConnection===p?fge(f,n,t):n.destroy()}return sv.delete(e),c}catch(n){throw sv.delete(e),n}}return VQ.get(e)};YH.exports=async(t,e,r)=>{if((typeof t=="string"||t instanceof URL)&&(t=sgt(new URL(t))),typeof e=="function"&&(r=e,e=void 0),e={ALPNProtocols:["h2","http/1.1"],...t,...e,resolveSocket:!0},!Array.isArray(e.ALPNProtocols)||e.ALPNProtocols.length===0)throw new Error("The `ALPNProtocols` option must be an Array with at least one entry");e.protocol=e.protocol||"https:";let s=e.protocol==="https:";e.host=e.hostname||e.host||"localhost",e.session=e.tlsSession,e.servername=e.servername||igt(e),e.port=e.port||(s?443:80),e._defaultAgent=s?WH.globalAgent:uge.globalAgent;let a=e.agent;if(a){if(a.addRequest)throw new Error("The `options.agent` object can contain only `http`, `https` or `http2` properties");e.agent=a[s?"https":"http"]}return s&&await ogt(e)==="h2"?(a&&(e.agent=a.http2),new ngt(e,r)):uge.request(e,r)};YH.exports.protocolCache=VQ});var hge=L((nzt,pge)=>{"use strict";var agt=Ie("http2"),lgt=OH(),VH=GH(),cgt=MH(),ugt=Age(),fgt=(t,e,r)=>new VH(t,e,r),Agt=(t,e,r)=>{let s=new VH(t,e,r);return s.end(),s};pge.exports={...agt,ClientRequest:VH,IncomingMessage:cgt,...lgt,request:fgt,get:Agt,auto:ugt}});var JH=L(KH=>{"use strict";Object.defineProperty(KH,"__esModule",{value:!0});var gge=Lp();KH.default=t=>gge.default.nodeStream(t)&&gge.default.function_(t.getBoundary)});var Ege=L(zH=>{"use strict";Object.defineProperty(zH,"__esModule",{value:!0});var mge=Ie("fs"),yge=Ie("util"),dge=Lp(),pgt=JH(),hgt=yge.promisify(mge.stat);zH.default=async(t,e)=>{if(e&&"content-length"in e)return Number(e["content-length"]);if(!t)return 0;if(dge.default.string(t))return Buffer.byteLength(t);if(dge.default.buffer(t))return t.length;if(pgt.default(t))return yge.promisify(t.getLength.bind(t))();if(t instanceof mge.ReadStream){let{size:r}=await hgt(t.path);return r===0?void 0:r}}});var XH=L(ZH=>{"use strict";Object.defineProperty(ZH,"__esModule",{value:!0});function ggt(t,e,r){let s={};for(let a of r)s[a]=(...n)=>{e.emit(a,...n)},t.on(a,s[a]);return()=>{for(let a of r)t.off(a,s[a])}}ZH.default=ggt});var Ige=L($H=>{"use strict";Object.defineProperty($H,"__esModule",{value:!0});$H.default=()=>{let t=[];return{once(e,r,s){e.once(r,s),t.push({origin:e,event:r,fn:s})},unhandleAll(){for(let e of t){let{origin:r,event:s,fn:a}=e;r.removeListener(s,a)}t.length=0}}}});var wge=L(ov=>{"use strict";Object.defineProperty(ov,"__esModule",{value:!0});ov.TimeoutError=void 0;var dgt=Ie("net"),mgt=Ige(),Cge=Symbol("reentry"),ygt=()=>{},KQ=class extends Error{constructor(e,r){super(`Timeout awaiting '${r}' for ${e}ms`),this.event=r,this.name="TimeoutError",this.code="ETIMEDOUT"}};ov.TimeoutError=KQ;ov.default=(t,e,r)=>{if(Cge in t)return ygt;t[Cge]=!0;let s=[],{once:a,unhandleAll:n}=mgt.default(),c=(C,S,P)=>{var I;let R=setTimeout(S,C,C,P);(I=R.unref)===null||I===void 0||I.call(R);let N=()=>{clearTimeout(R)};return s.push(N),N},{host:f,hostname:p}=r,h=(C,S)=>{t.destroy(new KQ(C,S))},E=()=>{for(let C of s)C();n()};if(t.once("error",C=>{if(E(),t.listenerCount("error")===0)throw C}),t.once("close",E),a(t,"response",C=>{a(C,"end",E)}),typeof e.request<"u"&&c(e.request,h,"request"),typeof e.socket<"u"){let C=()=>{h(e.socket,"socket")};t.setTimeout(e.socket,C),s.push(()=>{t.removeListener("timeout",C)})}return a(t,"socket",C=>{var S;let{socketPath:P}=t;if(C.connecting){let I=!!(P??dgt.isIP((S=p??f)!==null&&S!==void 0?S:"")!==0);if(typeof e.lookup<"u"&&!I&&typeof C.address().address>"u"){let R=c(e.lookup,h,"lookup");a(C,"lookup",R)}if(typeof e.connect<"u"){let R=()=>c(e.connect,h,"connect");I?a(C,"connect",R()):a(C,"lookup",N=>{N===null&&a(C,"connect",R())})}typeof e.secureConnect<"u"&&r.protocol==="https:"&&a(C,"connect",()=>{let R=c(e.secureConnect,h,"secureConnect");a(C,"secureConnect",R)})}if(typeof e.send<"u"){let I=()=>c(e.send,h,"send");C.connecting?a(C,"connect",()=>{a(t,"upload-complete",I())}):a(t,"upload-complete",I())}}),typeof e.response<"u"&&a(t,"upload-complete",()=>{let C=c(e.response,h,"response");a(t,"response",C)}),E}});var vge=L(ej=>{"use strict";Object.defineProperty(ej,"__esModule",{value:!0});var Bge=Lp();ej.default=t=>{t=t;let e={protocol:t.protocol,hostname:Bge.default.string(t.hostname)&&t.hostname.startsWith("[")?t.hostname.slice(1,-1):t.hostname,host:t.host,hash:t.hash,search:t.search,pathname:t.pathname,href:t.href,path:`${t.pathname||""}${t.search||""}`};return Bge.default.string(t.port)&&t.port.length>0&&(e.port=Number(t.port)),(t.username||t.password)&&(e.auth=`${t.username||""}:${t.password||""}`),e}});var Sge=L(tj=>{"use strict";Object.defineProperty(tj,"__esModule",{value:!0});var Egt=Ie("url"),Igt=["protocol","host","hostname","port","pathname","search"];tj.default=(t,e)=>{var r,s;if(e.path){if(e.pathname)throw new TypeError("Parameters `path` and `pathname` are mutually exclusive.");if(e.search)throw new TypeError("Parameters `path` and `search` are mutually exclusive.");if(e.searchParams)throw new TypeError("Parameters `path` and `searchParams` are mutually exclusive.")}if(e.search&&e.searchParams)throw new TypeError("Parameters `search` and `searchParams` are mutually exclusive.");if(!t){if(!e.protocol)throw new TypeError("No URL protocol specified");t=`${e.protocol}//${(s=(r=e.hostname)!==null&&r!==void 0?r:e.host)!==null&&s!==void 0?s:""}`}let a=new Egt.URL(t);if(e.path){let n=e.path.indexOf("?");n===-1?e.pathname=e.path:(e.pathname=e.path.slice(0,n),e.search=e.path.slice(n+1)),delete e.path}for(let n of Igt)e[n]&&(a[n]=e[n].toString());return a}});var Dge=L(nj=>{"use strict";Object.defineProperty(nj,"__esModule",{value:!0});var rj=class{constructor(){this.weakMap=new WeakMap,this.map=new Map}set(e,r){typeof e=="object"?this.weakMap.set(e,r):this.map.set(e,r)}get(e){return typeof e=="object"?this.weakMap.get(e):this.map.get(e)}has(e){return typeof e=="object"?this.weakMap.has(e):this.map.has(e)}};nj.default=rj});var sj=L(ij=>{"use strict";Object.defineProperty(ij,"__esModule",{value:!0});var Cgt=async t=>{let e=[],r=0;for await(let s of t)e.push(s),r+=Buffer.byteLength(s);return Buffer.isBuffer(e[0])?Buffer.concat(e,r):Buffer.from(e.join(""))};ij.default=Cgt});var Pge=L(nm=>{"use strict";Object.defineProperty(nm,"__esModule",{value:!0});nm.dnsLookupIpVersionToFamily=nm.isDnsLookupIpVersion=void 0;var bge={auto:0,ipv4:4,ipv6:6};nm.isDnsLookupIpVersion=t=>t in bge;nm.dnsLookupIpVersionToFamily=t=>{if(nm.isDnsLookupIpVersion(t))return bge[t];throw new Error("Invalid DNS lookup IP version")}});var oj=L(JQ=>{"use strict";Object.defineProperty(JQ,"__esModule",{value:!0});JQ.isResponseOk=void 0;JQ.isResponseOk=t=>{let{statusCode:e}=t,r=t.request.options.followRedirect?299:399;return e>=200&&e<=r||e===304}});var kge=L(aj=>{"use strict";Object.defineProperty(aj,"__esModule",{value:!0});var xge=new Set;aj.default=t=>{xge.has(t)||(xge.add(t),process.emitWarning(`Got: ${t}`,{type:"DeprecationWarning"}))}});var Qge=L(lj=>{"use strict";Object.defineProperty(lj,"__esModule",{value:!0});var bi=Lp(),wgt=(t,e)=>{if(bi.default.null_(t.encoding))throw new TypeError("To get a Buffer, set `options.responseType` to `buffer` instead");bi.assert.any([bi.default.string,bi.default.undefined],t.encoding),bi.assert.any([bi.default.boolean,bi.default.undefined],t.resolveBodyOnly),bi.assert.any([bi.default.boolean,bi.default.undefined],t.methodRewriting),bi.assert.any([bi.default.boolean,bi.default.undefined],t.isStream),bi.assert.any([bi.default.string,bi.default.undefined],t.responseType),t.responseType===void 0&&(t.responseType="text");let{retry:r}=t;if(e?t.retry={...e.retry}:t.retry={calculateDelay:s=>s.computedValue,limit:0,methods:[],statusCodes:[],errorCodes:[],maxRetryAfter:void 0},bi.default.object(r)?(t.retry={...t.retry,...r},t.retry.methods=[...new Set(t.retry.methods.map(s=>s.toUpperCase()))],t.retry.statusCodes=[...new Set(t.retry.statusCodes)],t.retry.errorCodes=[...new Set(t.retry.errorCodes)]):bi.default.number(r)&&(t.retry.limit=r),bi.default.undefined(t.retry.maxRetryAfter)&&(t.retry.maxRetryAfter=Math.min(...[t.timeout.request,t.timeout.connect].filter(bi.default.number))),bi.default.object(t.pagination)){e&&(t.pagination={...e.pagination,...t.pagination});let{pagination:s}=t;if(!bi.default.function_(s.transform))throw new Error("`options.pagination.transform` must be implemented");if(!bi.default.function_(s.shouldContinue))throw new Error("`options.pagination.shouldContinue` must be implemented");if(!bi.default.function_(s.filter))throw new TypeError("`options.pagination.filter` must be implemented");if(!bi.default.function_(s.paginate))throw new Error("`options.pagination.paginate` must be implemented")}return t.responseType==="json"&&t.headers.accept===void 0&&(t.headers.accept="application/json"),t};lj.default=wgt});var Tge=L(av=>{"use strict";Object.defineProperty(av,"__esModule",{value:!0});av.retryAfterStatusCodes=void 0;av.retryAfterStatusCodes=new Set([413,429,503]);var Bgt=({attemptCount:t,retryOptions:e,error:r,retryAfter:s})=>{if(t>e.limit)return 0;let a=e.methods.includes(r.options.method),n=e.errorCodes.includes(r.code),c=r.response&&e.statusCodes.includes(r.response.statusCode);if(!a||!n&&!c)return 0;if(r.response){if(s)return e.maxRetryAfter===void 0||s>e.maxRetryAfter?0:s;if(r.response.statusCode===413)return 0}let f=Math.random()*100;return 2**(t-1)*1e3+f};av.default=Bgt});var uv=L(Ln=>{"use strict";Object.defineProperty(Ln,"__esModule",{value:!0});Ln.UnsupportedProtocolError=Ln.ReadError=Ln.TimeoutError=Ln.UploadError=Ln.CacheError=Ln.HTTPError=Ln.MaxRedirectsError=Ln.RequestError=Ln.setNonEnumerableProperties=Ln.knownHookEvents=Ln.withoutBody=Ln.kIsNormalizedAlready=void 0;var Rge=Ie("util"),Fge=Ie("stream"),vgt=Ie("fs"),C0=Ie("url"),Nge=Ie("http"),cj=Ie("http"),Sgt=Ie("https"),Dgt=Jhe(),bgt=r0e(),Oge=N0e(),Pgt=_0e(),xgt=hge(),kgt=GQ(),at=Lp(),Qgt=Ege(),Lge=JH(),Tgt=XH(),Mge=wge(),Rgt=vge(),_ge=Sge(),Fgt=Dge(),Ngt=sj(),Uge=Pge(),Ogt=oj(),w0=kge(),Lgt=Qge(),Mgt=Tge(),uj,go=Symbol("request"),XQ=Symbol("response"),mI=Symbol("responseSize"),yI=Symbol("downloadedSize"),EI=Symbol("bodySize"),II=Symbol("uploadedSize"),zQ=Symbol("serverResponsesPiped"),Hge=Symbol("unproxyEvents"),jge=Symbol("isFromCache"),fj=Symbol("cancelTimeouts"),qge=Symbol("startedReading"),CI=Symbol("stopReading"),ZQ=Symbol("triggerRead"),B0=Symbol("body"),lv=Symbol("jobs"),Gge=Symbol("originalResponse"),Wge=Symbol("retryTimeout");Ln.kIsNormalizedAlready=Symbol("isNormalizedAlready");var _gt=at.default.string(process.versions.brotli);Ln.withoutBody=new Set(["GET","HEAD"]);Ln.knownHookEvents=["init","beforeRequest","beforeRedirect","beforeError","beforeRetry","afterResponse"];function Ugt(t){for(let e in t){let r=t[e];if(!at.default.string(r)&&!at.default.number(r)&&!at.default.boolean(r)&&!at.default.null_(r)&&!at.default.undefined(r))throw new TypeError(`The \`searchParams\` value '${String(r)}' must be a string, number, boolean or null`)}}function Hgt(t){return at.default.object(t)&&!("statusCode"in t)}var Aj=new Fgt.default,jgt=async t=>new Promise((e,r)=>{let s=a=>{r(a)};t.pending||e(),t.once("error",s),t.once("ready",()=>{t.off("error",s),e()})}),qgt=new Set([300,301,302,303,304,307,308]),Ggt=["context","body","json","form"];Ln.setNonEnumerableProperties=(t,e)=>{let r={};for(let s of t)if(s)for(let a of Ggt)a in s&&(r[a]={writable:!0,configurable:!0,enumerable:!1,value:s[a]});Object.defineProperties(e,r)};var As=class extends Error{constructor(e,r,s){var a;if(super(e),Error.captureStackTrace(this,this.constructor),this.name="RequestError",this.code=r.code,s instanceof sT?(Object.defineProperty(this,"request",{enumerable:!1,value:s}),Object.defineProperty(this,"response",{enumerable:!1,value:s[XQ]}),Object.defineProperty(this,"options",{enumerable:!1,value:s.options})):Object.defineProperty(this,"options",{enumerable:!1,value:s}),this.timings=(a=this.request)===null||a===void 0?void 0:a.timings,at.default.string(r.stack)&&at.default.string(this.stack)){let n=this.stack.indexOf(this.message)+this.message.length,c=this.stack.slice(n).split(` `).reverse(),f=r.stack.slice(r.stack.indexOf(r.message)+r.message.length).split(` `).reverse();for(;f.length!==0&&f[0]===c[0];)c.shift();this.stack=`${this.stack.slice(0,n)}${c.reverse().join(` `)}${f.reverse().join(` `)}`}}};Ln.RequestError=As;var $Q=class extends As{constructor(e){super(`Redirected ${e.options.maxRedirects} times. Aborting.`,{},e),this.name="MaxRedirectsError"}};Ln.MaxRedirectsError=$Q;var eT=class extends As{constructor(e){super(`Response code ${e.statusCode} (${e.statusMessage})`,{},e.request),this.name="HTTPError"}};Ln.HTTPError=eT;var tT=class extends As{constructor(e,r){super(e.message,e,r),this.name="CacheError"}};Ln.CacheError=tT;var rT=class extends As{constructor(e,r){super(e.message,e,r),this.name="UploadError"}};Ln.UploadError=rT;var nT=class extends As{constructor(e,r,s){super(e.message,e,s),this.name="TimeoutError",this.event=e.event,this.timings=r}};Ln.TimeoutError=nT;var cv=class extends As{constructor(e,r){super(e.message,e,r),this.name="ReadError"}};Ln.ReadError=cv;var iT=class extends As{constructor(e){super(`Unsupported protocol "${e.url.protocol}"`,{},e),this.name="UnsupportedProtocolError"}};Ln.UnsupportedProtocolError=iT;var Wgt=["socket","connect","continue","information","upgrade","timeout"],sT=class extends Fge.Duplex{constructor(e,r={},s){super({autoDestroy:!1,highWaterMark:0}),this[yI]=0,this[II]=0,this.requestInitialized=!1,this[zQ]=new Set,this.redirects=[],this[CI]=!1,this[ZQ]=!1,this[lv]=[],this.retryCount=0,this._progressCallbacks=[];let a=()=>this._unlockWrite(),n=()=>this._lockWrite();this.on("pipe",h=>{h.prependListener("data",a),h.on("data",n),h.prependListener("end",a),h.on("end",n)}),this.on("unpipe",h=>{h.off("data",a),h.off("data",n),h.off("end",a),h.off("end",n)}),this.on("pipe",h=>{h instanceof cj.IncomingMessage&&(this.options.headers={...h.headers,...this.options.headers})});let{json:c,body:f,form:p}=r;if((c||f||p)&&this._lockWrite(),Ln.kIsNormalizedAlready in r)this.options=r;else try{this.options=this.constructor.normalizeArguments(e,r,s)}catch(h){at.default.nodeStream(r.body)&&r.body.destroy(),this.destroy(h);return}(async()=>{var h;try{this.options.body instanceof vgt.ReadStream&&await jgt(this.options.body);let{url:E}=this.options;if(!E)throw new TypeError("Missing `url` property");if(this.requestUrl=E.toString(),decodeURI(this.requestUrl),await this._finalizeBody(),await this._makeRequest(),this.destroyed){(h=this[go])===null||h===void 0||h.destroy();return}for(let C of this[lv])C();this[lv].length=0,this.requestInitialized=!0}catch(E){if(E instanceof As){this._beforeError(E);return}this.destroyed||this.destroy(E)}})()}static normalizeArguments(e,r,s){var a,n,c,f,p;let h=r;if(at.default.object(e)&&!at.default.urlInstance(e))r={...s,...e,...r};else{if(e&&r&&r.url!==void 0)throw new TypeError("The `url` option is mutually exclusive with the `input` argument");r={...s,...r},e!==void 0&&(r.url=e),at.default.urlInstance(r.url)&&(r.url=new C0.URL(r.url.toString()))}if(r.cache===!1&&(r.cache=void 0),r.dnsCache===!1&&(r.dnsCache=void 0),at.assert.any([at.default.string,at.default.undefined],r.method),at.assert.any([at.default.object,at.default.undefined],r.headers),at.assert.any([at.default.string,at.default.urlInstance,at.default.undefined],r.prefixUrl),at.assert.any([at.default.object,at.default.undefined],r.cookieJar),at.assert.any([at.default.object,at.default.string,at.default.undefined],r.searchParams),at.assert.any([at.default.object,at.default.string,at.default.undefined],r.cache),at.assert.any([at.default.object,at.default.number,at.default.undefined],r.timeout),at.assert.any([at.default.object,at.default.undefined],r.context),at.assert.any([at.default.object,at.default.undefined],r.hooks),at.assert.any([at.default.boolean,at.default.undefined],r.decompress),at.assert.any([at.default.boolean,at.default.undefined],r.ignoreInvalidCookies),at.assert.any([at.default.boolean,at.default.undefined],r.followRedirect),at.assert.any([at.default.number,at.default.undefined],r.maxRedirects),at.assert.any([at.default.boolean,at.default.undefined],r.throwHttpErrors),at.assert.any([at.default.boolean,at.default.undefined],r.http2),at.assert.any([at.default.boolean,at.default.undefined],r.allowGetBody),at.assert.any([at.default.string,at.default.undefined],r.localAddress),at.assert.any([Uge.isDnsLookupIpVersion,at.default.undefined],r.dnsLookupIpVersion),at.assert.any([at.default.object,at.default.undefined],r.https),at.assert.any([at.default.boolean,at.default.undefined],r.rejectUnauthorized),r.https&&(at.assert.any([at.default.boolean,at.default.undefined],r.https.rejectUnauthorized),at.assert.any([at.default.function_,at.default.undefined],r.https.checkServerIdentity),at.assert.any([at.default.string,at.default.object,at.default.array,at.default.undefined],r.https.certificateAuthority),at.assert.any([at.default.string,at.default.object,at.default.array,at.default.undefined],r.https.key),at.assert.any([at.default.string,at.default.object,at.default.array,at.default.undefined],r.https.certificate),at.assert.any([at.default.string,at.default.undefined],r.https.passphrase),at.assert.any([at.default.string,at.default.buffer,at.default.array,at.default.undefined],r.https.pfx)),at.assert.any([at.default.object,at.default.undefined],r.cacheOptions),at.default.string(r.method)?r.method=r.method.toUpperCase():r.method="GET",r.headers===s?.headers?r.headers={...r.headers}:r.headers=kgt({...s?.headers,...r.headers}),"slashes"in r)throw new TypeError("The legacy `url.Url` has been deprecated. Use `URL` instead.");if("auth"in r)throw new TypeError("Parameter `auth` is deprecated. Use `username` / `password` instead.");if("searchParams"in r&&r.searchParams&&r.searchParams!==s?.searchParams){let P;if(at.default.string(r.searchParams)||r.searchParams instanceof C0.URLSearchParams)P=new C0.URLSearchParams(r.searchParams);else{Ugt(r.searchParams),P=new C0.URLSearchParams;for(let I in r.searchParams){let R=r.searchParams[I];R===null?P.append(I,""):R!==void 0&&P.append(I,R)}}(a=s?.searchParams)===null||a===void 0||a.forEach((I,R)=>{P.has(R)||P.append(R,I)}),r.searchParams=P}if(r.username=(n=r.username)!==null&&n!==void 0?n:"",r.password=(c=r.password)!==null&&c!==void 0?c:"",at.default.undefined(r.prefixUrl)?r.prefixUrl=(f=s?.prefixUrl)!==null&&f!==void 0?f:"":(r.prefixUrl=r.prefixUrl.toString(),r.prefixUrl!==""&&!r.prefixUrl.endsWith("/")&&(r.prefixUrl+="/")),at.default.string(r.url)){if(r.url.startsWith("/"))throw new Error("`input` must not start with a slash when using `prefixUrl`");r.url=_ge.default(r.prefixUrl+r.url,r)}else(at.default.undefined(r.url)&&r.prefixUrl!==""||r.protocol)&&(r.url=_ge.default(r.prefixUrl,r));if(r.url){"port"in r&&delete r.port;let{prefixUrl:P}=r;Object.defineProperty(r,"prefixUrl",{set:R=>{let N=r.url;if(!N.href.startsWith(R))throw new Error(`Cannot change \`prefixUrl\` from ${P} to ${R}: ${N.href}`);r.url=new C0.URL(R+N.href.slice(P.length)),P=R},get:()=>P});let{protocol:I}=r.url;if(I==="unix:"&&(I="http:",r.url=new C0.URL(`http://unix${r.url.pathname}${r.url.search}`)),r.searchParams&&(r.url.search=r.searchParams.toString()),I!=="http:"&&I!=="https:")throw new iT(r);r.username===""?r.username=r.url.username:r.url.username=r.username,r.password===""?r.password=r.url.password:r.url.password=r.password}let{cookieJar:E}=r;if(E){let{setCookie:P,getCookieString:I}=E;at.assert.function_(P),at.assert.function_(I),P.length===4&&I.length===0&&(P=Rge.promisify(P.bind(r.cookieJar)),I=Rge.promisify(I.bind(r.cookieJar)),r.cookieJar={setCookie:P,getCookieString:I})}let{cache:C}=r;if(C&&(Aj.has(C)||Aj.set(C,new Oge((P,I)=>{let R=P[go](P,I);return at.default.promise(R)&&(R.once=(N,U)=>{if(N==="error")R.catch(U);else if(N==="abort")(async()=>{try{(await R).once("abort",U)}catch{}})();else throw new Error(`Unknown HTTP2 promise event: ${N}`);return R}),R},C))),r.cacheOptions={...r.cacheOptions},r.dnsCache===!0)uj||(uj=new bgt.default),r.dnsCache=uj;else if(!at.default.undefined(r.dnsCache)&&!r.dnsCache.lookup)throw new TypeError(`Parameter \`dnsCache\` must be a CacheableLookup instance or a boolean, got ${at.default(r.dnsCache)}`);at.default.number(r.timeout)?r.timeout={request:r.timeout}:s&&r.timeout!==s.timeout?r.timeout={...s.timeout,...r.timeout}:r.timeout={...r.timeout},r.context||(r.context={});let S=r.hooks===s?.hooks;r.hooks={...r.hooks};for(let P of Ln.knownHookEvents)if(P in r.hooks)if(at.default.array(r.hooks[P]))r.hooks[P]=[...r.hooks[P]];else throw new TypeError(`Parameter \`${P}\` must be an Array, got ${at.default(r.hooks[P])}`);else r.hooks[P]=[];if(s&&!S)for(let P of Ln.knownHookEvents)s.hooks[P].length>0&&(r.hooks[P]=[...s.hooks[P],...r.hooks[P]]);if("family"in r&&w0.default('"options.family" was never documented, please use "options.dnsLookupIpVersion"'),s?.https&&(r.https={...s.https,...r.https}),"rejectUnauthorized"in r&&w0.default('"options.rejectUnauthorized" is now deprecated, please use "options.https.rejectUnauthorized"'),"checkServerIdentity"in r&&w0.default('"options.checkServerIdentity" was never documented, please use "options.https.checkServerIdentity"'),"ca"in r&&w0.default('"options.ca" was never documented, please use "options.https.certificateAuthority"'),"key"in r&&w0.default('"options.key" was never documented, please use "options.https.key"'),"cert"in r&&w0.default('"options.cert" was never documented, please use "options.https.certificate"'),"passphrase"in r&&w0.default('"options.passphrase" was never documented, please use "options.https.passphrase"'),"pfx"in r&&w0.default('"options.pfx" was never documented, please use "options.https.pfx"'),"followRedirects"in r)throw new TypeError("The `followRedirects` option does not exist. Use `followRedirect` instead.");if(r.agent){for(let P in r.agent)if(P!=="http"&&P!=="https"&&P!=="http2")throw new TypeError(`Expected the \`options.agent\` properties to be \`http\`, \`https\` or \`http2\`, got \`${P}\``)}return r.maxRedirects=(p=r.maxRedirects)!==null&&p!==void 0?p:0,Ln.setNonEnumerableProperties([s,h],r),Lgt.default(r,s)}_lockWrite(){let e=()=>{throw new TypeError("The payload has been already provided")};this.write=e,this.end=e}_unlockWrite(){this.write=super.write,this.end=super.end}async _finalizeBody(){let{options:e}=this,{headers:r}=e,s=!at.default.undefined(e.form),a=!at.default.undefined(e.json),n=!at.default.undefined(e.body),c=s||a||n,f=Ln.withoutBody.has(e.method)&&!(e.method==="GET"&&e.allowGetBody);if(this._cannotHaveBody=f,c){if(f)throw new TypeError(`The \`${e.method}\` method cannot be used with a body`);if([n,s,a].filter(p=>p).length>1)throw new TypeError("The `body`, `json` and `form` options are mutually exclusive");if(n&&!(e.body instanceof Fge.Readable)&&!at.default.string(e.body)&&!at.default.buffer(e.body)&&!Lge.default(e.body))throw new TypeError("The `body` option must be a stream.Readable, string or Buffer");if(s&&!at.default.object(e.form))throw new TypeError("The `form` option must be an Object");{let p=!at.default.string(r["content-type"]);n?(Lge.default(e.body)&&p&&(r["content-type"]=`multipart/form-data; boundary=${e.body.getBoundary()}`),this[B0]=e.body):s?(p&&(r["content-type"]="application/x-www-form-urlencoded"),this[B0]=new C0.URLSearchParams(e.form).toString()):(p&&(r["content-type"]="application/json"),this[B0]=e.stringifyJson(e.json));let h=await Qgt.default(this[B0],e.headers);at.default.undefined(r["content-length"])&&at.default.undefined(r["transfer-encoding"])&&!f&&!at.default.undefined(h)&&(r["content-length"]=String(h))}}else f?this._lockWrite():this._unlockWrite();this[EI]=Number(r["content-length"])||void 0}async _onResponseBase(e){let{options:r}=this,{url:s}=r;this[Gge]=e,r.decompress&&(e=Pgt(e));let a=e.statusCode,n=e;n.statusMessage=n.statusMessage?n.statusMessage:Nge.STATUS_CODES[a],n.url=r.url.toString(),n.requestUrl=this.requestUrl,n.redirectUrls=this.redirects,n.request=this,n.isFromCache=e.fromCache||!1,n.ip=this.ip,n.retryCount=this.retryCount,this[jge]=n.isFromCache,this[mI]=Number(e.headers["content-length"])||void 0,this[XQ]=e,e.once("end",()=>{this[mI]=this[yI],this.emit("downloadProgress",this.downloadProgress)}),e.once("error",f=>{e.destroy(),this._beforeError(new cv(f,this))}),e.once("aborted",()=>{this._beforeError(new cv({name:"Error",message:"The server aborted pending request",code:"ECONNRESET"},this))}),this.emit("downloadProgress",this.downloadProgress);let c=e.headers["set-cookie"];if(at.default.object(r.cookieJar)&&c){let f=c.map(async p=>r.cookieJar.setCookie(p,s.toString()));r.ignoreInvalidCookies&&(f=f.map(async p=>p.catch(()=>{})));try{await Promise.all(f)}catch(p){this._beforeError(p);return}}if(r.followRedirect&&e.headers.location&&qgt.has(a)){if(e.resume(),this[go]&&(this[fj](),delete this[go],this[Hge]()),(a===303&&r.method!=="GET"&&r.method!=="HEAD"||!r.methodRewriting)&&(r.method="GET","body"in r&&delete r.body,"json"in r&&delete r.json,"form"in r&&delete r.form,this[B0]=void 0,delete r.headers["content-length"]),this.redirects.length>=r.maxRedirects){this._beforeError(new $Q(this));return}try{let p=Buffer.from(e.headers.location,"binary").toString(),h=new C0.URL(p,s),E=h.toString();decodeURI(E),h.hostname!==s.hostname||h.port!==s.port?("host"in r.headers&&delete r.headers.host,"cookie"in r.headers&&delete r.headers.cookie,"authorization"in r.headers&&delete r.headers.authorization,(r.username||r.password)&&(r.username="",r.password="")):(h.username=r.username,h.password=r.password),this.redirects.push(E),r.url=h;for(let C of r.hooks.beforeRedirect)await C(r,n);this.emit("redirect",n,r),await this._makeRequest()}catch(p){this._beforeError(p);return}return}if(r.isStream&&r.throwHttpErrors&&!Ogt.isResponseOk(n)){this._beforeError(new eT(n));return}e.on("readable",()=>{this[ZQ]&&this._read()}),this.on("resume",()=>{e.resume()}),this.on("pause",()=>{e.pause()}),e.once("end",()=>{this.push(null)}),this.emit("response",e);for(let f of this[zQ])if(!f.headersSent){for(let p in e.headers){let h=r.decompress?p!=="content-encoding":!0,E=e.headers[p];h&&f.setHeader(p,E)}f.statusCode=a}}async _onResponse(e){try{await this._onResponseBase(e)}catch(r){this._beforeError(r)}}_onRequest(e){let{options:r}=this,{timeout:s,url:a}=r;Dgt.default(e),this[fj]=Mge.default(e,s,a);let n=r.cache?"cacheableResponse":"response";e.once(n,p=>{this._onResponse(p)}),e.once("error",p=>{var h;e.destroy(),(h=e.res)===null||h===void 0||h.removeAllListeners("end"),p=p instanceof Mge.TimeoutError?new nT(p,this.timings,this):new As(p.message,p,this),this._beforeError(p)}),this[Hge]=Tgt.default(e,this,Wgt),this[go]=e,this.emit("uploadProgress",this.uploadProgress);let c=this[B0],f=this.redirects.length===0?this:e;at.default.nodeStream(c)?(c.pipe(f),c.once("error",p=>{this._beforeError(new rT(p,this))})):(this._unlockWrite(),at.default.undefined(c)?(this._cannotHaveBody||this._noPipe)&&(f.end(),this._lockWrite()):(this._writeRequest(c,void 0,()=>{}),f.end(),this._lockWrite())),this.emit("request",e)}async _createCacheableRequest(e,r){return new Promise((s,a)=>{Object.assign(r,Rgt.default(e)),delete r.url;let n,c=Aj.get(r.cache)(r,async f=>{f._readableState.autoDestroy=!1,n&&(await n).emit("cacheableResponse",f),s(f)});r.url=e,c.once("error",a),c.once("request",async f=>{n=f,s(n)})})}async _makeRequest(){var e,r,s,a,n;let{options:c}=this,{headers:f}=c;for(let U in f)if(at.default.undefined(f[U]))delete f[U];else if(at.default.null_(f[U]))throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${U}\` header`);if(c.decompress&&at.default.undefined(f["accept-encoding"])&&(f["accept-encoding"]=_gt?"gzip, deflate, br":"gzip, deflate"),c.cookieJar){let U=await c.cookieJar.getCookieString(c.url.toString());at.default.nonEmptyString(U)&&(c.headers.cookie=U)}for(let U of c.hooks.beforeRequest){let W=await U(c);if(!at.default.undefined(W)){c.request=()=>W;break}}c.body&&this[B0]!==c.body&&(this[B0]=c.body);let{agent:p,request:h,timeout:E,url:C}=c;if(c.dnsCache&&!("lookup"in c)&&(c.lookup=c.dnsCache.lookup),C.hostname==="unix"){let U=/(?.+?):(?.+)/.exec(`${C.pathname}${C.search}`);if(U?.groups){let{socketPath:W,path:te}=U.groups;Object.assign(c,{socketPath:W,path:te,host:""})}}let S=C.protocol==="https:",P;c.http2?P=xgt.auto:P=S?Sgt.request:Nge.request;let I=(e=c.request)!==null&&e!==void 0?e:P,R=c.cache?this._createCacheableRequest:I;p&&!c.http2&&(c.agent=p[S?"https":"http"]),c[go]=I,delete c.request,delete c.timeout;let N=c;if(N.shared=(r=c.cacheOptions)===null||r===void 0?void 0:r.shared,N.cacheHeuristic=(s=c.cacheOptions)===null||s===void 0?void 0:s.cacheHeuristic,N.immutableMinTimeToLive=(a=c.cacheOptions)===null||a===void 0?void 0:a.immutableMinTimeToLive,N.ignoreCargoCult=(n=c.cacheOptions)===null||n===void 0?void 0:n.ignoreCargoCult,c.dnsLookupIpVersion!==void 0)try{N.family=Uge.dnsLookupIpVersionToFamily(c.dnsLookupIpVersion)}catch{throw new Error("Invalid `dnsLookupIpVersion` option value")}c.https&&("rejectUnauthorized"in c.https&&(N.rejectUnauthorized=c.https.rejectUnauthorized),c.https.checkServerIdentity&&(N.checkServerIdentity=c.https.checkServerIdentity),c.https.certificateAuthority&&(N.ca=c.https.certificateAuthority),c.https.certificate&&(N.cert=c.https.certificate),c.https.key&&(N.key=c.https.key),c.https.passphrase&&(N.passphrase=c.https.passphrase),c.https.pfx&&(N.pfx=c.https.pfx));try{let U=await R(C,N);at.default.undefined(U)&&(U=P(C,N)),c.request=h,c.timeout=E,c.agent=p,c.https&&("rejectUnauthorized"in c.https&&delete N.rejectUnauthorized,c.https.checkServerIdentity&&delete N.checkServerIdentity,c.https.certificateAuthority&&delete N.ca,c.https.certificate&&delete N.cert,c.https.key&&delete N.key,c.https.passphrase&&delete N.passphrase,c.https.pfx&&delete N.pfx),Hgt(U)?this._onRequest(U):this.writable?(this.once("finish",()=>{this._onResponse(U)}),this._unlockWrite(),this.end(),this._lockWrite()):this._onResponse(U)}catch(U){throw U instanceof Oge.CacheError?new tT(U,this):new As(U.message,U,this)}}async _error(e){try{for(let r of this.options.hooks.beforeError)e=await r(e)}catch(r){e=new As(r.message,r,this)}this.destroy(e)}_beforeError(e){if(this[CI])return;let{options:r}=this,s=this.retryCount+1;this[CI]=!0,e instanceof As||(e=new As(e.message,e,this));let a=e,{response:n}=a;(async()=>{if(n&&!n.body){n.setEncoding(this._readableState.encoding);try{n.rawBody=await Ngt.default(n),n.body=n.rawBody.toString()}catch{}}if(this.listenerCount("retry")!==0){let c;try{let f;n&&"retry-after"in n.headers&&(f=Number(n.headers["retry-after"]),Number.isNaN(f)?(f=Date.parse(n.headers["retry-after"])-Date.now(),f<=0&&(f=1)):f*=1e3),c=await r.retry.calculateDelay({attemptCount:s,retryOptions:r.retry,error:a,retryAfter:f,computedValue:Mgt.default({attemptCount:s,retryOptions:r.retry,error:a,retryAfter:f,computedValue:0})})}catch(f){this._error(new As(f.message,f,this));return}if(c){let f=async()=>{try{for(let p of this.options.hooks.beforeRetry)await p(this.options,a,s)}catch(p){this._error(new As(p.message,e,this));return}this.destroyed||(this.destroy(),this.emit("retry",s,e))};this[Wge]=setTimeout(f,c);return}}this._error(a)})()}_read(){this[ZQ]=!0;let e=this[XQ];if(e&&!this[CI]){e.readableLength&&(this[ZQ]=!1);let r;for(;(r=e.read())!==null;){this[yI]+=r.length,this[qge]=!0;let s=this.downloadProgress;s.percent<1&&this.emit("downloadProgress",s),this.push(r)}}}_write(e,r,s){let a=()=>{this._writeRequest(e,r,s)};this.requestInitialized?a():this[lv].push(a)}_writeRequest(e,r,s){this[go].destroyed||(this._progressCallbacks.push(()=>{this[II]+=Buffer.byteLength(e,r);let a=this.uploadProgress;a.percent<1&&this.emit("uploadProgress",a)}),this[go].write(e,r,a=>{!a&&this._progressCallbacks.length>0&&this._progressCallbacks.shift()(),s(a)}))}_final(e){let r=()=>{for(;this._progressCallbacks.length!==0;)this._progressCallbacks.shift()();if(!(go in this)){e();return}if(this[go].destroyed){e();return}this[go].end(s=>{s||(this[EI]=this[II],this.emit("uploadProgress",this.uploadProgress),this[go].emit("upload-complete")),e(s)})};this.requestInitialized?r():this[lv].push(r)}_destroy(e,r){var s;this[CI]=!0,clearTimeout(this[Wge]),go in this&&(this[fj](),!((s=this[XQ])===null||s===void 0)&&s.complete||this[go].destroy()),e!==null&&!at.default.undefined(e)&&!(e instanceof As)&&(e=new As(e.message,e,this)),r(e)}get _isAboutToError(){return this[CI]}get ip(){var e;return(e=this.socket)===null||e===void 0?void 0:e.remoteAddress}get aborted(){var e,r,s;return((r=(e=this[go])===null||e===void 0?void 0:e.destroyed)!==null&&r!==void 0?r:this.destroyed)&&!(!((s=this[Gge])===null||s===void 0)&&s.complete)}get socket(){var e,r;return(r=(e=this[go])===null||e===void 0?void 0:e.socket)!==null&&r!==void 0?r:void 0}get downloadProgress(){let e;return this[mI]?e=this[yI]/this[mI]:this[mI]===this[yI]?e=1:e=0,{percent:e,transferred:this[yI],total:this[mI]}}get uploadProgress(){let e;return this[EI]?e=this[II]/this[EI]:this[EI]===this[II]?e=1:e=0,{percent:e,transferred:this[II],total:this[EI]}}get timings(){var e;return(e=this[go])===null||e===void 0?void 0:e.timings}get isFromCache(){return this[jge]}pipe(e,r){if(this[qge])throw new Error("Failed to pipe. The response has been emitted already.");return e instanceof cj.ServerResponse&&this[zQ].add(e),super.pipe(e,r)}unpipe(e){return e instanceof cj.ServerResponse&&this[zQ].delete(e),super.unpipe(e),this}};Ln.default=sT});var fv=L(Wu=>{"use strict";var Ygt=Wu&&Wu.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r),Object.defineProperty(t,s,{enumerable:!0,get:function(){return e[r]}})}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),Vgt=Wu&&Wu.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&Ygt(e,t,r)};Object.defineProperty(Wu,"__esModule",{value:!0});Wu.CancelError=Wu.ParseError=void 0;var Yge=uv(),pj=class extends Yge.RequestError{constructor(e,r){let{options:s}=r.request;super(`${e.message} in "${s.url.toString()}"`,e,r.request),this.name="ParseError"}};Wu.ParseError=pj;var hj=class extends Yge.RequestError{constructor(e){super("Promise was canceled",{},e),this.name="CancelError"}get isCanceled(){return!0}};Wu.CancelError=hj;Vgt(uv(),Wu)});var Kge=L(gj=>{"use strict";Object.defineProperty(gj,"__esModule",{value:!0});var Vge=fv(),Kgt=(t,e,r,s)=>{let{rawBody:a}=t;try{if(e==="text")return a.toString(s);if(e==="json")return a.length===0?"":r(a.toString());if(e==="buffer")return a;throw new Vge.ParseError({message:`Unknown body type '${e}'`,name:"Error"},t)}catch(n){throw new Vge.ParseError(n,t)}};gj.default=Kgt});var dj=L(v0=>{"use strict";var Jgt=v0&&v0.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r),Object.defineProperty(t,s,{enumerable:!0,get:function(){return e[r]}})}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),zgt=v0&&v0.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&Jgt(e,t,r)};Object.defineProperty(v0,"__esModule",{value:!0});var Zgt=Ie("events"),Xgt=Lp(),$gt=Vhe(),oT=fv(),Jge=Kge(),zge=uv(),edt=XH(),tdt=sj(),Zge=oj(),rdt=["request","response","redirect","uploadProgress","downloadProgress"];function Xge(t){let e,r,s=new Zgt.EventEmitter,a=new $gt((c,f,p)=>{let h=E=>{let C=new zge.default(void 0,t);C.retryCount=E,C._noPipe=!0,p(()=>C.destroy()),p.shouldReject=!1,p(()=>f(new oT.CancelError(C))),e=C,C.once("response",async I=>{var R;if(I.retryCount=E,I.request.aborted)return;let N;try{N=await tdt.default(C),I.rawBody=N}catch{return}if(C._isAboutToError)return;let U=((R=I.headers["content-encoding"])!==null&&R!==void 0?R:"").toLowerCase(),W=["gzip","deflate","br"].includes(U),{options:te}=C;if(W&&!te.decompress)I.body=N;else try{I.body=Jge.default(I,te.responseType,te.parseJson,te.encoding)}catch(ie){if(I.body=N.toString(),Zge.isResponseOk(I)){C._beforeError(ie);return}}try{for(let[ie,Ae]of te.hooks.afterResponse.entries())I=await Ae(I,async ce=>{let me=zge.default.normalizeArguments(void 0,{...ce,retry:{calculateDelay:()=>0},throwHttpErrors:!1,resolveBodyOnly:!1},te);me.hooks.afterResponse=me.hooks.afterResponse.slice(0,ie);for(let Be of me.hooks.beforeRetry)await Be(me);let pe=Xge(me);return p(()=>{pe.catch(()=>{}),pe.cancel()}),pe})}catch(ie){C._beforeError(new oT.RequestError(ie.message,ie,C));return}if(!Zge.isResponseOk(I)){C._beforeError(new oT.HTTPError(I));return}r=I,c(C.options.resolveBodyOnly?I.body:I)});let S=I=>{if(a.isCanceled)return;let{options:R}=C;if(I instanceof oT.HTTPError&&!R.throwHttpErrors){let{response:N}=I;c(C.options.resolveBodyOnly?N.body:N);return}f(I)};C.once("error",S);let P=C.options.body;C.once("retry",(I,R)=>{var N,U;if(P===((N=R.request)===null||N===void 0?void 0:N.options.body)&&Xgt.default.nodeStream((U=R.request)===null||U===void 0?void 0:U.options.body)){S(R);return}h(I)}),edt.default(C,s,rdt)};h(0)});a.on=(c,f)=>(s.on(c,f),a);let n=c=>{let f=(async()=>{await a;let{options:p}=r.request;return Jge.default(r,c,p.parseJson,p.encoding)})();return Object.defineProperties(f,Object.getOwnPropertyDescriptors(a)),f};return a.json=()=>{let{headers:c}=e.options;return!e.writableFinished&&c.accept===void 0&&(c.accept="application/json"),n("json")},a.buffer=()=>n("buffer"),a.text=()=>n("text"),a}v0.default=Xge;zgt(fv(),v0)});var $ge=L(mj=>{"use strict";Object.defineProperty(mj,"__esModule",{value:!0});var ndt=fv();function idt(t,...e){let r=(async()=>{if(t instanceof ndt.RequestError)try{for(let a of e)if(a)for(let n of a)t=await n(t)}catch(a){t=a}throw t})(),s=()=>r;return r.json=s,r.text=s,r.buffer=s,r.on=s,r}mj.default=idt});var rde=L(yj=>{"use strict";Object.defineProperty(yj,"__esModule",{value:!0});var ede=Lp();function tde(t){for(let e of Object.values(t))(ede.default.plainObject(e)||ede.default.array(e))&&tde(e);return Object.freeze(t)}yj.default=tde});var ide=L(nde=>{"use strict";Object.defineProperty(nde,"__esModule",{value:!0})});var Ej=L(Lc=>{"use strict";var sdt=Lc&&Lc.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r),Object.defineProperty(t,s,{enumerable:!0,get:function(){return e[r]}})}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),odt=Lc&&Lc.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&sdt(e,t,r)};Object.defineProperty(Lc,"__esModule",{value:!0});Lc.defaultHandler=void 0;var sde=Lp(),Oc=dj(),adt=$ge(),lT=uv(),ldt=rde(),cdt={RequestError:Oc.RequestError,CacheError:Oc.CacheError,ReadError:Oc.ReadError,HTTPError:Oc.HTTPError,MaxRedirectsError:Oc.MaxRedirectsError,TimeoutError:Oc.TimeoutError,ParseError:Oc.ParseError,CancelError:Oc.CancelError,UnsupportedProtocolError:Oc.UnsupportedProtocolError,UploadError:Oc.UploadError},udt=async t=>new Promise(e=>{setTimeout(e,t)}),{normalizeArguments:aT}=lT.default,ode=(...t)=>{let e;for(let r of t)e=aT(void 0,r,e);return e},fdt=t=>t.isStream?new lT.default(void 0,t):Oc.default(t),Adt=t=>"defaults"in t&&"options"in t.defaults,pdt=["get","post","put","patch","head","delete"];Lc.defaultHandler=(t,e)=>e(t);var ade=(t,e)=>{if(t)for(let r of t)r(e)},lde=t=>{t._rawHandlers=t.handlers,t.handlers=t.handlers.map(s=>(a,n)=>{let c,f=s(a,p=>(c=n(p),c));if(f!==c&&!a.isStream&&c){let p=f,{then:h,catch:E,finally:C}=p;Object.setPrototypeOf(p,Object.getPrototypeOf(c)),Object.defineProperties(p,Object.getOwnPropertyDescriptors(c)),p.then=h,p.catch=E,p.finally=C}return f});let e=(s,a={},n)=>{var c,f;let p=0,h=E=>t.handlers[p++](E,p===t.handlers.length?fdt:h);if(sde.default.plainObject(s)){let E={...s,...a};lT.setNonEnumerableProperties([s,a],E),a=E,s=void 0}try{let E;try{ade(t.options.hooks.init,a),ade((c=a.hooks)===null||c===void 0?void 0:c.init,a)}catch(S){E=S}let C=aT(s,a,n??t.options);if(C[lT.kIsNormalizedAlready]=!0,E)throw new Oc.RequestError(E.message,E,C);return h(C)}catch(E){if(a.isStream)throw E;return adt.default(E,t.options.hooks.beforeError,(f=a.hooks)===null||f===void 0?void 0:f.beforeError)}};e.extend=(...s)=>{let a=[t.options],n=[...t._rawHandlers],c;for(let f of s)Adt(f)?(a.push(f.defaults.options),n.push(...f.defaults._rawHandlers),c=f.defaults.mutableDefaults):(a.push(f),"handlers"in f&&n.push(...f.handlers),c=f.mutableDefaults);return n=n.filter(f=>f!==Lc.defaultHandler),n.length===0&&n.push(Lc.defaultHandler),lde({options:ode(...a),handlers:n,mutableDefaults:!!c})};let r=async function*(s,a){let n=aT(s,a,t.options);n.resolveBodyOnly=!1;let c=n.pagination;if(!sde.default.object(c))throw new TypeError("`options.pagination` must be implemented");let f=[],{countLimit:p}=c,h=0;for(;h{let n=[];for await(let c of r(s,a))n.push(c);return n},e.paginate.each=r,e.stream=(s,a)=>e(s,{...a,isStream:!0});for(let s of pdt)e[s]=(a,n)=>e(a,{...n,method:s}),e.stream[s]=(a,n)=>e(a,{...n,method:s,isStream:!0});return Object.assign(e,cdt),Object.defineProperty(e,"defaults",{value:t.mutableDefaults?t:ldt.default(t),writable:t.mutableDefaults,configurable:t.mutableDefaults,enumerable:!0}),e.mergeOptions=ode,e};Lc.default=lde;odt(ide(),Lc)});var fde=L((Mp,cT)=>{"use strict";var hdt=Mp&&Mp.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r),Object.defineProperty(t,s,{enumerable:!0,get:function(){return e[r]}})}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),cde=Mp&&Mp.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&hdt(e,t,r)};Object.defineProperty(Mp,"__esModule",{value:!0});var gdt=Ie("url"),ude=Ej(),ddt={options:{method:"GET",retry:{limit:2,methods:["GET","PUT","HEAD","DELETE","OPTIONS","TRACE"],statusCodes:[408,413,429,500,502,503,504,521,522,524],errorCodes:["ETIMEDOUT","ECONNRESET","EADDRINUSE","ECONNREFUSED","EPIPE","ENOTFOUND","ENETUNREACH","EAI_AGAIN"],maxRetryAfter:void 0,calculateDelay:({computedValue:t})=>t},timeout:{},headers:{"user-agent":"got (https://github.com/sindresorhus/got)"},hooks:{init:[],beforeRequest:[],beforeRedirect:[],beforeRetry:[],beforeError:[],afterResponse:[]},cache:void 0,dnsCache:void 0,decompress:!0,throwHttpErrors:!0,followRedirect:!0,isStream:!1,responseType:"text",resolveBodyOnly:!1,maxRedirects:10,prefixUrl:"",methodRewriting:!0,ignoreInvalidCookies:!1,context:{},http2:!1,allowGetBody:!1,https:void 0,pagination:{transform:t=>t.request.options.responseType==="json"?t.body:JSON.parse(t.body),paginate:t=>{if(!Reflect.has(t.headers,"link"))return!1;let e=t.headers.link.split(","),r;for(let s of e){let a=s.split(";");if(a[1].includes("next")){r=a[0].trimStart().trim(),r=r.slice(1,-1);break}}return r?{url:new gdt.URL(r)}:!1},filter:()=>!0,shouldContinue:()=>!0,countLimit:1/0,backoff:0,requestLimit:1e4,stackAllItems:!0},parseJson:t=>JSON.parse(t),stringifyJson:t=>JSON.stringify(t),cacheOptions:{}},handlers:[ude.defaultHandler],mutableDefaults:!1},Ij=ude.default(ddt);Mp.default=Ij;cT.exports=Ij;cT.exports.default=Ij;cT.exports.__esModule=!0;cde(Ej(),Mp);cde(dj(),Mp)});var An={};Vt(An,{Method:()=>mde,del:()=>Cdt,get:()=>Bj,getNetworkSettings:()=>dde,post:()=>vj,put:()=>Idt,request:()=>Av});async function Cj(t){return Vl(pde,t,()=>le.readFilePromise(t).then(e=>(pde.set(t,e),e)))}function Edt({statusCode:t,statusMessage:e},r){let s=Ut(r,t,pt.NUMBER),a=`https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/${t}`;return KE(r,`${s}${e?` (${e})`:""}`,a)}async function uT(t,{configuration:e,customErrorMessage:r}){try{return await t}catch(s){if(s.name!=="HTTPError")throw s;let a=r?.(s,e)??s.response.body?.error;a==null&&(s.message.startsWith("Response code")?a="The remote server failed to provide the requested resource":a=s.message),s.code==="ETIMEDOUT"&&s.event==="socket"&&(a+=`(can be increased via ${Ut(e,"httpTimeout",pt.SETTING)})`);let n=new Yt(35,a,c=>{s.response&&c.reportError(35,` ${Zf(e,{label:"Response Code",value:Hu(pt.NO_HINT,Edt(s.response,e))})}`),s.request&&(c.reportError(35,` ${Zf(e,{label:"Request Method",value:Hu(pt.NO_HINT,s.request.options.method)})}`),c.reportError(35,` ${Zf(e,{label:"Request URL",value:Hu(pt.URL,s.request.requestUrl)})}`)),s.request.redirects.length>0&&c.reportError(35,` ${Zf(e,{label:"Request Redirects",value:Hu(pt.NO_HINT,f3(e,s.request.redirects,pt.URL))})}`),s.request.retryCount===s.request.options.retry.limit&&c.reportError(35,` ${Zf(e,{label:"Request Retry Count",value:Hu(pt.NO_HINT,`${Ut(e,s.request.retryCount,pt.NUMBER)} (can be increased via ${Ut(e,"httpRetry",pt.SETTING)})`)})}`)});throw n.originalError=s,n}}function dde(t,e){let r=[...e.configuration.get("networkSettings")].sort(([c],[f])=>f.length-c.length),s={enableNetwork:void 0,httpsCaFilePath:void 0,httpProxy:void 0,httpsProxy:void 0,httpsKeyFilePath:void 0,httpsCertFilePath:void 0},a=Object.keys(s),n=typeof t=="string"?new URL(t):t;for(let[c,f]of r)if(wj.default.isMatch(n.hostname,c))for(let p of a){let h=f.get(p);h!==null&&typeof s[p]>"u"&&(s[p]=h)}for(let c of a)typeof s[c]>"u"&&(s[c]=e.configuration.get(c));return s}async function Av(t,e,{configuration:r,headers:s,jsonRequest:a,jsonResponse:n,method:c="GET",wrapNetworkRequest:f}){let p={target:t,body:e,configuration:r,headers:s,jsonRequest:a,jsonResponse:n,method:c},h=async()=>await wdt(t,e,p),E=typeof f<"u"?await f(h,p):h;return await(await r.reduceHook(S=>S.wrapNetworkRequest,E,p))()}async function Bj(t,{configuration:e,jsonResponse:r,customErrorMessage:s,wrapNetworkRequest:a,...n}){let c=()=>uT(Av(t,null,{configuration:e,wrapNetworkRequest:a,...n}),{configuration:e,customErrorMessage:s}).then(p=>p.body),f=await(typeof a<"u"?c():Vl(Ade,t,()=>c().then(p=>(Ade.set(t,p),p))));return r?JSON.parse(f.toString()):f}async function Idt(t,e,{customErrorMessage:r,...s}){return(await uT(Av(t,e,{...s,method:"PUT"}),{customErrorMessage:r,configuration:s.configuration})).body}async function vj(t,e,{customErrorMessage:r,...s}){return(await uT(Av(t,e,{...s,method:"POST"}),{customErrorMessage:r,configuration:s.configuration})).body}async function Cdt(t,{customErrorMessage:e,...r}){return(await uT(Av(t,null,{...r,method:"DELETE"}),{customErrorMessage:e,configuration:r.configuration})).body}async function wdt(t,e,{configuration:r,headers:s,jsonRequest:a,jsonResponse:n,method:c="GET"}){let f=typeof t=="string"?new URL(t):t,p=dde(f,{configuration:r});if(p.enableNetwork===!1)throw new Yt(80,`Request to '${f.href}' has been blocked because of your configuration settings`);if(f.protocol==="http:"&&!wj.default.isMatch(f.hostname,r.get("unsafeHttpWhitelist")))throw new Yt(81,`Unsafe http requests must be explicitly whitelisted in your configuration (${f.hostname})`);let h={headers:s,method:c};h.responseType=n?"json":"buffer",e!==null&&(Buffer.isBuffer(e)||!a&&typeof e=="string"?h.body=e:h.json=e);let E=r.get("httpTimeout"),C=r.get("httpRetry"),S=r.get("enableStrictSsl"),P=p.httpsCaFilePath,I=p.httpsCertFilePath,R=p.httpsKeyFilePath,{default:N}=await Promise.resolve().then(()=>et(fde())),U=P?await Cj(P):void 0,W=I?await Cj(I):void 0,te=R?await Cj(R):void 0,ie={rejectUnauthorized:S,ca:U,cert:W,key:te},Ae={http:p.httpProxy?new Uhe({proxy:p.httpProxy,proxyRequestOptions:ie}):mdt,https:p.httpsProxy?new Hhe({proxy:p.httpsProxy,proxyRequestOptions:ie}):ydt},ce=N.extend({timeout:{socket:E},retry:C,agent:Ae,https:{rejectUnauthorized:S,certificateAuthority:U,certificate:W,key:te},...h});return r.getLimit("networkConcurrency")(()=>ce(f))}var hde,gde,wj,Ade,pde,mdt,ydt,mde,fT=Ct(()=>{bt();jhe();hde=Ie("https"),gde=Ie("http"),wj=et(Sa());Fc();Qc();kc();Ade=new Map,pde=new Map,mdt=new gde.Agent({keepAlive:!0}),ydt=new hde.Agent({keepAlive:!0});mde=(a=>(a.GET="GET",a.PUT="PUT",a.POST="POST",a.DELETE="DELETE",a))(mde||{})});var ps={};Vt(ps,{availableParallelism:()=>Dj,getArchitecture:()=>pv,getArchitectureName:()=>bdt,getArchitectureSet:()=>Sj,getCaller:()=>Qdt,major:()=>Bdt,openUrl:()=>vdt});function Ddt(){if(process.platform==="darwin"||process.platform==="win32")return null;let t;try{t=le.readFileSync(Sdt)}catch{}if(typeof t<"u"){if(t&&(t.includes("GLIBC")||t.includes("libc")))return"glibc";if(t&&t.includes("musl"))return"musl"}let r=(process.report?.getReport()??{}).sharedObjects??[],s=/\/(?:(ld-linux-|[^/]+-linux-gnu\/)|(libc.musl-|ld-musl-))/;return A0(r,a=>{let n=a.match(s);if(!n)return A0.skip;if(n[1])return"glibc";if(n[2])return"musl";throw new Error("Assertion failed: Expected the libc variant to have been detected")})??null}function pv(){return Ede=Ede??{os:process.platform,cpu:process.arch,libc:Ddt()}}function bdt(t=pv()){return t.libc?`${t.os}-${t.cpu}-${t.libc}`:`${t.os}-${t.cpu}`}function Sj(){let t=pv();return Ide=Ide??{os:[t.os],cpu:[t.cpu],libc:t.libc?[t.libc]:[]}}function kdt(t){let e=Pdt.exec(t);if(!e)return null;let r=e[2]&&e[2].indexOf("native")===0,s=e[2]&&e[2].indexOf("eval")===0,a=xdt.exec(e[2]);return s&&a!=null&&(e[2]=a[1],e[3]=a[2],e[4]=a[3]),{file:r?null:e[2],methodName:e[1]||"",arguments:r?[e[2]]:[],line:e[3]?+e[3]:null,column:e[4]?+e[4]:null}}function Qdt(){let e=new Error().stack.split(` `)[3];return kdt(e)}function Dj(){return typeof AT.default.availableParallelism<"u"?AT.default.availableParallelism():Math.max(1,AT.default.cpus().length)}var AT,Bdt,yde,vdt,Sdt,Ede,Ide,Pdt,xdt,pT=Ct(()=>{bt();AT=et(Ie("os"));hT();kc();Bdt=Number(process.versions.node.split(".")[0]),yde=new Map([["darwin","open"],["linux","xdg-open"],["win32","explorer.exe"]]).get(process.platform),vdt=typeof yde<"u"?async t=>{try{return await bj(yde,[t],{cwd:K.cwd()}),!0}catch{return!1}}:void 0,Sdt="/usr/bin/ldd";Pdt=/^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\/|[a-z]:\\|\\\\).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,xdt=/\((\S*)(?::(\d+))(?::(\d+))\)/});function Tj(t,e,r,s,a){let n=ev(r);if(s.isArray||s.type==="ANY"&&Array.isArray(n))return Array.isArray(n)?n.map((c,f)=>Pj(t,`${e}[${f}]`,c,s,a)):String(n).split(/,/).map(c=>Pj(t,e,c,s,a));if(Array.isArray(n))throw new Error(`Non-array configuration settings "${e}" cannot be an array`);return Pj(t,e,r,s,a)}function Pj(t,e,r,s,a){let n=ev(r);switch(s.type){case"ANY":return RQ(n);case"SHAPE":return Ndt(t,e,r,s,a);case"MAP":return Odt(t,e,r,s,a)}if(n===null&&!s.isNullable&&s.default!==null)throw new Error(`Non-nullable configuration settings "${e}" cannot be set to null`);if(s.values?.includes(n))return n;let f=(()=>{if(s.type==="BOOLEAN"&&typeof n!="string")return MB(n);if(typeof n!="string")throw new Error(`Expected configuration setting "${e}" to be a string, got ${typeof n}`);let p=Yk(n,{env:t.env});switch(s.type){case"ABSOLUTE_PATH":{let h=a,E=rH(r);return E&&E[0]!=="<"&&(h=K.dirname(E)),K.resolve(h,ue.toPortablePath(p))}case"LOCATOR_LOOSE":return Rp(p,!1);case"NUMBER":return parseInt(p);case"LOCATOR":return Rp(p);case"BOOLEAN":return MB(p);default:return p}})();if(s.values&&!s.values.includes(f))throw new Error(`Invalid value, expected one of ${s.values.join(", ")}`);return f}function Ndt(t,e,r,s,a){let n=ev(r);if(typeof n!="object"||Array.isArray(n))throw new nt(`Object configuration settings "${e}" must be an object`);let c=Rj(t,s,{ignoreArrays:!0});if(n===null)return c;for(let[f,p]of Object.entries(n)){let h=`${e}.${f}`;if(!s.properties[f])throw new nt(`Unrecognized configuration settings found: ${e}.${f} - run "yarn config -v" to see the list of settings supported in Yarn`);c.set(f,Tj(t,h,p,s.properties[f],a))}return c}function Odt(t,e,r,s,a){let n=ev(r),c=new Map;if(typeof n!="object"||Array.isArray(n))throw new nt(`Map configuration settings "${e}" must be an object`);if(n===null)return c;for(let[f,p]of Object.entries(n)){let h=s.normalizeKeys?s.normalizeKeys(f):f,E=`${e}['${h}']`,C=s.valueDefinition;c.set(h,Tj(t,E,p,C,a))}return c}function Rj(t,e,{ignoreArrays:r=!1}={}){switch(e.type){case"SHAPE":{if(e.isArray&&!r)return[];let s=new Map;for(let[a,n]of Object.entries(e.properties))s.set(a,Rj(t,n));return s}case"MAP":return e.isArray&&!r?[]:new Map;case"ABSOLUTE_PATH":return e.default===null?null:t.projectCwd===null?Array.isArray(e.default)?e.default.map(s=>K.normalize(s)):K.isAbsolute(e.default)?K.normalize(e.default):e.isNullable?null:void 0:Array.isArray(e.default)?e.default.map(s=>K.resolve(t.projectCwd,s)):K.resolve(t.projectCwd,e.default);default:return e.default}}function dT(t,e,r){if(e.type==="SECRET"&&typeof t=="string"&&r.hideSecrets)return Fdt;if(e.type==="ABSOLUTE_PATH"&&typeof t=="string"&&r.getNativePaths)return ue.fromPortablePath(t);if(e.isArray&&Array.isArray(t)){let s=[];for(let a of t)s.push(dT(a,e,r));return s}if(e.type==="MAP"&&t instanceof Map){if(t.size===0)return;let s=new Map;for(let[a,n]of t.entries()){let c=dT(n,e.valueDefinition,r);typeof c<"u"&&s.set(a,c)}return s}if(e.type==="SHAPE"&&t instanceof Map){if(t.size===0)return;let s=new Map;for(let[a,n]of t.entries()){let c=e.properties[a],f=dT(n,c,r);typeof f<"u"&&s.set(a,f)}return s}return t}function Ldt(){let t={};for(let[e,r]of Object.entries(process.env))e=e.toLowerCase(),e.startsWith(mT)&&(e=(0,wde.default)(e.slice(mT.length)),t[e]=r);return t}function kj(){let t=`${mT}rc_filename`;for(let[e,r]of Object.entries(process.env))if(e.toLowerCase()===t&&typeof r=="string")return r;return Qj}async function Cde(t){try{return await le.readFilePromise(t)}catch{return Buffer.of()}}async function Mdt(t,e){return Buffer.compare(...await Promise.all([Cde(t),Cde(e)]))===0}async function _dt(t,e){let[r,s]=await Promise.all([le.statPromise(t),le.statPromise(e)]);return r.dev===s.dev&&r.ino===s.ino}async function Hdt({configuration:t,selfPath:e}){let r=t.get("yarnPath");return t.get("ignorePath")||r===null||r===e||await Udt(r,e)?null:r}var wde,_p,Bde,vde,Sde,xj,Tdt,hv,Rdt,Up,mT,Qj,Fdt,gv,Dde,yT,gT,Udt,ze,dv=Ct(()=>{bt();Bc();wde=et(zre()),_p=et(Rd());Wt();Bde=et(qne()),vde=Ie("module"),Sde=et(Od()),xj=Ie("stream");nhe();sI();K8();J8();z8();Qhe();Z8();$d();Ohe();NQ();Qc();E0();fT();kc();pT();Np();Yo();Tdt=function(){if(!_p.GITHUB_ACTIONS||!process.env.GITHUB_EVENT_PATH)return!1;let t=ue.toPortablePath(process.env.GITHUB_EVENT_PATH),e;try{e=le.readJsonSync(t)}catch{return!1}return!(!("repository"in e)||!e.repository||(e.repository.private??!0))}(),hv=new Set(["@yarnpkg/plugin-constraints","@yarnpkg/plugin-exec","@yarnpkg/plugin-interactive-tools","@yarnpkg/plugin-stage","@yarnpkg/plugin-typescript","@yarnpkg/plugin-version","@yarnpkg/plugin-workspace-tools"]),Rdt=new Set(["isTestEnv","injectNpmUser","injectNpmPassword","injectNpm2FaToken","zipDataEpilogue","cacheCheckpointOverride","cacheVersionOverride","lockfileVersionOverride","binFolder","version","flags","profile","gpg","ignoreNode","wrapOutput","home","confDir","registry","ignoreCwd"]),Up=/^(?!v)[a-z0-9._-]+$/i,mT="yarn_",Qj=".yarnrc.yml",Fdt="********",gv=(E=>(E.ANY="ANY",E.BOOLEAN="BOOLEAN",E.ABSOLUTE_PATH="ABSOLUTE_PATH",E.LOCATOR="LOCATOR",E.LOCATOR_LOOSE="LOCATOR_LOOSE",E.NUMBER="NUMBER",E.STRING="STRING",E.SECRET="SECRET",E.SHAPE="SHAPE",E.MAP="MAP",E))(gv||{}),Dde=pt,yT=(r=>(r.JUNCTIONS="junctions",r.SYMLINKS="symlinks",r))(yT||{}),gT={lastUpdateCheck:{description:"Last timestamp we checked whether new Yarn versions were available",type:"STRING",default:null},yarnPath:{description:"Path to the local executable that must be used over the global one",type:"ABSOLUTE_PATH",default:null},ignorePath:{description:"If true, the local executable will be ignored when using the global one",type:"BOOLEAN",default:!1},globalFolder:{description:"Folder where all system-global files are stored",type:"ABSOLUTE_PATH",default:iH()},cacheFolder:{description:"Folder where the cache files must be written",type:"ABSOLUTE_PATH",default:"./.yarn/cache"},compressionLevel:{description:"Zip files compression level, from 0 to 9 or mixed (a variant of 9, which stores some files uncompressed, when compression doesn't yield good results)",type:"NUMBER",values:["mixed",0,1,2,3,4,5,6,7,8,9],default:0},virtualFolder:{description:"Folder where the virtual packages (cf doc) will be mapped on the disk (must be named __virtual__)",type:"ABSOLUTE_PATH",default:"./.yarn/__virtual__"},installStatePath:{description:"Path of the file where the install state will be persisted",type:"ABSOLUTE_PATH",default:"./.yarn/install-state.gz"},immutablePatterns:{description:"Array of glob patterns; files matching them won't be allowed to change during immutable installs",type:"STRING",default:[],isArray:!0},rcFilename:{description:"Name of the files where the configuration can be found",type:"STRING",default:kj()},enableGlobalCache:{description:"If true, the system-wide cache folder will be used regardless of `cache-folder`",type:"BOOLEAN",default:!0},cacheMigrationMode:{description:"Defines the conditions under which Yarn upgrades should cause the cache archives to be regenerated.",type:"STRING",values:["always","match-spec","required-only"],default:"always"},enableColors:{description:"If true, the CLI is allowed to use colors in its output",type:"BOOLEAN",default:zk,defaultText:""},enableHyperlinks:{description:"If true, the CLI is allowed to use hyperlinks in its output",type:"BOOLEAN",default:u3,defaultText:""},enableInlineBuilds:{description:"If true, the CLI will print the build output on the command line",type:"BOOLEAN",default:_p.isCI,defaultText:""},enableMessageNames:{description:"If true, the CLI will prefix most messages with codes suitable for search engines",type:"BOOLEAN",default:!0},enableProgressBars:{description:"If true, the CLI is allowed to show a progress bar for long-running events",type:"BOOLEAN",default:!_p.isCI,defaultText:""},enableTimers:{description:"If true, the CLI is allowed to print the time spent executing commands",type:"BOOLEAN",default:!0},enableTips:{description:"If true, installs will print a helpful message every day of the week",type:"BOOLEAN",default:!_p.isCI,defaultText:""},preferInteractive:{description:"If true, the CLI will automatically use the interactive mode when called from a TTY",type:"BOOLEAN",default:!1},preferTruncatedLines:{description:"If true, the CLI will truncate lines that would go beyond the size of the terminal",type:"BOOLEAN",default:!1},progressBarStyle:{description:"Which style of progress bar should be used (only when progress bars are enabled)",type:"STRING",default:void 0,defaultText:""},defaultLanguageName:{description:"Default language mode that should be used when a package doesn't offer any insight",type:"STRING",default:"node"},defaultProtocol:{description:"Default resolution protocol used when resolving pure semver and tag ranges",type:"STRING",default:"npm:"},enableTransparentWorkspaces:{description:"If false, Yarn won't automatically resolve workspace dependencies unless they use the `workspace:` protocol",type:"BOOLEAN",default:!0},supportedArchitectures:{description:"Architectures that Yarn will fetch and inject into the resolver",type:"SHAPE",properties:{os:{description:"Array of supported process.platform strings, or null to target them all",type:"STRING",isArray:!0,isNullable:!0,default:["current"]},cpu:{description:"Array of supported process.arch strings, or null to target them all",type:"STRING",isArray:!0,isNullable:!0,default:["current"]},libc:{description:"Array of supported libc libraries, or null to target them all",type:"STRING",isArray:!0,isNullable:!0,default:["current"]}}},enableMirror:{description:"If true, the downloaded packages will be retrieved and stored in both the local and global folders",type:"BOOLEAN",default:!0},enableNetwork:{description:"If false, Yarn will refuse to use the network if required to",type:"BOOLEAN",default:!0},enableOfflineMode:{description:"If true, Yarn will attempt to retrieve files and metadata from the global cache rather than the network",type:"BOOLEAN",default:!1},httpProxy:{description:"URL of the http proxy that must be used for outgoing http requests",type:"STRING",default:null},httpsProxy:{description:"URL of the http proxy that must be used for outgoing https requests",type:"STRING",default:null},unsafeHttpWhitelist:{description:"List of the hostnames for which http queries are allowed (glob patterns are supported)",type:"STRING",default:[],isArray:!0},httpTimeout:{description:"Timeout of each http request in milliseconds",type:"NUMBER",default:6e4},httpRetry:{description:"Retry times on http failure",type:"NUMBER",default:3},networkConcurrency:{description:"Maximal number of concurrent requests",type:"NUMBER",default:50},taskPoolConcurrency:{description:"Maximal amount of concurrent heavy task processing",type:"NUMBER",default:Dj()},taskPoolMode:{description:"Execution strategy for heavy tasks",type:"STRING",values:["async","workers"],default:"workers"},networkSettings:{description:"Network settings per hostname (glob patterns are supported)",type:"MAP",valueDefinition:{description:"",type:"SHAPE",properties:{httpsCaFilePath:{description:"Path to file containing one or multiple Certificate Authority signing certificates",type:"ABSOLUTE_PATH",default:null},enableNetwork:{description:"If false, the package manager will refuse to use the network if required to",type:"BOOLEAN",default:null},httpProxy:{description:"URL of the http proxy that must be used for outgoing http requests",type:"STRING",default:null},httpsProxy:{description:"URL of the http proxy that must be used for outgoing https requests",type:"STRING",default:null},httpsKeyFilePath:{description:"Path to file containing private key in PEM format",type:"ABSOLUTE_PATH",default:null},httpsCertFilePath:{description:"Path to file containing certificate chain in PEM format",type:"ABSOLUTE_PATH",default:null}}}},httpsCaFilePath:{description:"A path to a file containing one or multiple Certificate Authority signing certificates",type:"ABSOLUTE_PATH",default:null},httpsKeyFilePath:{description:"Path to file containing private key in PEM format",type:"ABSOLUTE_PATH",default:null},httpsCertFilePath:{description:"Path to file containing certificate chain in PEM format",type:"ABSOLUTE_PATH",default:null},enableStrictSsl:{description:"If false, SSL certificate errors will be ignored",type:"BOOLEAN",default:!0},logFilters:{description:"Overrides for log levels",type:"SHAPE",isArray:!0,concatenateValues:!0,properties:{code:{description:"Code of the messages covered by this override",type:"STRING",default:void 0},text:{description:"Code of the texts covered by this override",type:"STRING",default:void 0},pattern:{description:"Code of the patterns covered by this override",type:"STRING",default:void 0},level:{description:"Log level override, set to null to remove override",type:"STRING",values:Object.values(Xk),isNullable:!0,default:void 0}}},enableTelemetry:{description:"If true, telemetry will be periodically sent, following the rules in https://yarnpkg.com/advanced/telemetry",type:"BOOLEAN",default:!0},telemetryInterval:{description:"Minimal amount of time between two telemetry uploads, in days",type:"NUMBER",default:7},telemetryUserId:{description:"If you desire to tell us which project you are, you can set this field. Completely optional and opt-in.",type:"STRING",default:null},enableHardenedMode:{description:"If true, automatically enable --check-resolutions --refresh-lockfile on installs",type:"BOOLEAN",default:_p.isPR&&Tdt,defaultText:""},enableScripts:{description:"If true, packages are allowed to have install scripts by default",type:"BOOLEAN",default:!0},enableStrictSettings:{description:"If true, unknown settings will cause Yarn to abort",type:"BOOLEAN",default:!0},enableImmutableCache:{description:"If true, the cache is reputed immutable and actions that would modify it will throw",type:"BOOLEAN",default:!1},enableCacheClean:{description:"If false, disallows the `cache clean` command",type:"BOOLEAN",default:!0},checksumBehavior:{description:"Enumeration defining what to do when a checksum doesn't match expectations",type:"STRING",default:"throw"},injectEnvironmentFiles:{description:"List of all the environment files that Yarn should inject inside the process when it starts",type:"ABSOLUTE_PATH",default:[".env.yarn?"],isArray:!0},packageExtensions:{description:"Map of package corrections to apply on the dependency tree",type:"MAP",valueDefinition:{description:"The extension that will be applied to any package whose version matches the specified range",type:"SHAPE",properties:{dependencies:{description:"The set of dependencies that must be made available to the current package in order for it to work properly",type:"MAP",valueDefinition:{description:"A range",type:"STRING"}},peerDependencies:{description:"Inherited dependencies - the consumer of the package will be tasked to provide them",type:"MAP",valueDefinition:{description:"A semver range",type:"STRING"}},peerDependenciesMeta:{description:"Extra information related to the dependencies listed in the peerDependencies field",type:"MAP",valueDefinition:{description:"The peerDependency meta",type:"SHAPE",properties:{optional:{description:"If true, the selected peer dependency will be marked as optional by the package manager and the consumer omitting it won't be reported as an error",type:"BOOLEAN",default:!1}}}}}}}};Udt=process.platform==="win32"?Mdt:_dt;ze=class t{constructor(e){this.isCI=_p.isCI;this.projectCwd=null;this.plugins=new Map;this.settings=new Map;this.values=new Map;this.sources=new Map;this.invalid=new Map;this.env={};this.limits=new Map;this.packageExtensions=null;this.startingCwd=e}static{this.deleteProperty=Symbol()}static{this.telemetry=null}static create(e,r,s){let a=new t(e);typeof r<"u"&&!(r instanceof Map)&&(a.projectCwd=r),a.importSettings(gT);let n=typeof s<"u"?s:r instanceof Map?r:new Map;for(let[c,f]of n)a.activatePlugin(c,f);return a}static async find(e,r,{strict:s=!0,usePathCheck:a=null,useRc:n=!0}={}){let c=Ldt();delete c.rcFilename;let f=new t(e),p=await t.findRcFiles(e),h=await t.findFolderRcFile(fI());h&&(p.find(me=>me.path===h.path)||p.unshift(h));let E=Nhe(p.map(ce=>[ce.path,ce.data])),C=vt.dot,S=new Set(Object.keys(gT)),P=({yarnPath:ce,ignorePath:me,injectEnvironmentFiles:pe})=>({yarnPath:ce,ignorePath:me,injectEnvironmentFiles:pe}),I=({yarnPath:ce,ignorePath:me,injectEnvironmentFiles:pe,...Be})=>{let Ce={};for(let[g,we]of Object.entries(Be))S.has(g)&&(Ce[g]=we);return Ce},R=({yarnPath:ce,ignorePath:me,...pe})=>{let Be={};for(let[Ce,g]of Object.entries(pe))S.has(Ce)||(Be[Ce]=g);return Be};if(f.importSettings(P(gT)),f.useWithSource("",P(c),e,{strict:!1}),E){let[ce,me]=E;f.useWithSource(ce,P(me),C,{strict:!1})}if(a){if(await Hdt({configuration:f,selfPath:a})!==null)return f;f.useWithSource("",{ignorePath:!0},e,{strict:!1,overwrite:!0})}let N=await t.findProjectCwd(e);f.startingCwd=e,f.projectCwd=N;let U=Object.assign(Object.create(null),process.env);f.env=U;let W=await Promise.all(f.get("injectEnvironmentFiles").map(async ce=>{let me=ce.endsWith("?")?await le.readFilePromise(ce.slice(0,-1),"utf8").catch(()=>""):await le.readFilePromise(ce,"utf8");return(0,Bde.parse)(me)}));for(let ce of W)for(let[me,pe]of Object.entries(ce))f.env[me]=Yk(pe,{env:U});if(f.importSettings(I(gT)),f.useWithSource("",I(c),e,{strict:s}),E){let[ce,me]=E;f.useWithSource(ce,I(me),C,{strict:s})}let te=ce=>"default"in ce?ce.default:ce,ie=new Map([["@@core",rhe]]);if(r!==null)for(let ce of r.plugins.keys())ie.set(ce,te(r.modules.get(ce)));for(let[ce,me]of ie)f.activatePlugin(ce,me);let Ae=new Map([]);if(r!==null){let ce=new Map;for(let[Be,Ce]of r.modules)ce.set(Be,()=>Ce);let me=new Set,pe=async(Be,Ce)=>{let{factory:g,name:we}=kp(Be);if(!g||me.has(we))return;let ye=new Map(ce),fe=X=>{if((0,vde.isBuiltin)(X))return kp(X);if(ye.has(X))return ye.get(X)();throw new nt(`This plugin cannot access the package referenced via ${X} which is neither a builtin, nor an exposed entry`)},se=await qE(async()=>te(await g(fe)),X=>`${X} (when initializing ${we}, defined in ${Ce})`);ce.set(we,()=>se),me.add(we),Ae.set(we,se)};if(c.plugins)for(let Be of c.plugins.split(";")){let Ce=K.resolve(e,ue.toPortablePath(Be));await pe(Ce,"")}for(let{path:Be,cwd:Ce,data:g}of p)if(n&&Array.isArray(g.plugins))for(let we of g.plugins){let ye=typeof we!="string"?we.path:we,fe=we?.spec??"",se=we?.checksum??"";if(hv.has(fe))continue;let X=K.resolve(Ce,ue.toPortablePath(ye));if(!await le.existsPromise(X)){if(!fe){let dt=Ut(f,K.basename(X,".cjs"),pt.NAME),j=Ut(f,".gitignore",pt.NAME),rt=Ut(f,f.values.get("rcFilename"),pt.NAME),Fe=Ut(f,"https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored",pt.URL);throw new nt(`Missing source for the ${dt} plugin - please try to remove the plugin from ${rt} then reinstall it manually. This error usually occurs because ${j} is incorrect, check ${Fe} to make sure your plugin folder isn't gitignored.`)}if(!fe.match(/^https?:/)){let dt=Ut(f,K.basename(X,".cjs"),pt.NAME),j=Ut(f,f.values.get("rcFilename"),pt.NAME);throw new nt(`Failed to recognize the source for the ${dt} plugin - please try to delete the plugin from ${j} then reinstall it manually.`)}let De=await Bj(fe,{configuration:f}),Re=fs(De);if(se&&se!==Re){let dt=Ut(f,K.basename(X,".cjs"),pt.NAME),j=Ut(f,f.values.get("rcFilename"),pt.NAME),rt=Ut(f,`yarn plugin import ${fe}`,pt.CODE);throw new nt(`Failed to fetch the ${dt} plugin from its remote location: its checksum seems to have changed. If this is expected, please remove the plugin from ${j} then run ${rt} to reimport it.`)}await le.mkdirPromise(K.dirname(X),{recursive:!0}),await le.writeFilePromise(X,De)}await pe(X,Be)}}for(let[ce,me]of Ae)f.activatePlugin(ce,me);if(f.useWithSource("",R(c),e,{strict:s}),E){let[ce,me]=E;f.useWithSource(ce,R(me),C,{strict:s})}return f.get("enableGlobalCache")&&(f.values.set("cacheFolder",`${f.get("globalFolder")}/cache`),f.sources.set("cacheFolder","")),f}static async findRcFiles(e){let r=kj(),s=[],a=e,n=null;for(;a!==n;){n=a;let c=K.join(n,r);if(le.existsSync(c)){let f,p;try{p=await le.readFilePromise(c,"utf8"),f=cs(p)}catch{let h="";throw p?.match(/^\s+(?!-)[^:]+\s+\S+/m)&&(h=" (in particular, make sure you list the colons after each key name)"),new nt(`Parse error when loading ${c}; please check it's proper Yaml${h}`)}s.unshift({path:c,cwd:n,data:f})}a=K.dirname(n)}return s}static async findFolderRcFile(e){let r=K.join(e,Er.rc),s;try{s=await le.readFilePromise(r,"utf8")}catch(n){if(n.code==="ENOENT")return null;throw n}let a=cs(s);return{path:r,cwd:e,data:a}}static async findProjectCwd(e){let r=null,s=e,a=null;for(;s!==a;){if(a=s,le.existsSync(K.join(a,Er.lockfile)))return a;le.existsSync(K.join(a,Er.manifest))&&(r=a),s=K.dirname(a)}return r}static async updateConfiguration(e,r,s={}){let a=kj(),n=K.join(e,a),c=le.existsSync(n)?cs(await le.readFilePromise(n,"utf8")):{},f=!1,p;if(typeof r=="function"){try{p=r(c)}catch{p=r({})}if(p===c)return!1}else{p=c;for(let h of Object.keys(r)){let E=c[h],C=r[h],S;if(typeof C=="function")try{S=C(E)}catch{S=C(void 0)}else S=C;E!==S&&(S===t.deleteProperty?delete p[h]:p[h]=S,f=!0)}if(!f)return!1}return await le.changeFilePromise(n,il(p),{automaticNewlines:!0}),!0}static async addPlugin(e,r){r.length!==0&&await t.updateConfiguration(e,s=>{let a=s.plugins??[];if(a.length===0)return{...s,plugins:r};let n=[],c=[...r];for(let f of a){let p=typeof f!="string"?f.path:f,h=c.find(E=>E.path===p);h?(n.push(h),c=c.filter(E=>E!==h)):n.push(f)}return n.push(...c),{...s,plugins:n}})}static async updateHomeConfiguration(e){let r=fI();return await t.updateConfiguration(r,e)}activatePlugin(e,r){this.plugins.set(e,r),typeof r.configuration<"u"&&this.importSettings(r.configuration)}importSettings(e){for(let[r,s]of Object.entries(e))if(s!=null){if(this.settings.has(r))throw new Error(`Cannot redefine settings "${r}"`);this.settings.set(r,s),this.values.set(r,Rj(this,s))}}useWithSource(e,r,s,a){try{this.use(e,r,s,a)}catch(n){throw n.message+=` (in ${Ut(this,e,pt.PATH)})`,n}}use(e,r,s,{strict:a=!0,overwrite:n=!1}={}){a=a&&this.get("enableStrictSettings");for(let c of["enableStrictSettings",...Object.keys(r)]){let f=r[c],p=rH(f);if(p&&(e=p),typeof f>"u"||c==="plugins"||e===""&&Rdt.has(c))continue;if(c==="rcFilename")throw new nt(`The rcFilename settings can only be set via ${`${mT}RC_FILENAME`.toUpperCase()}, not via a rc file`);let h=this.settings.get(c);if(!h){let C=fI(),S=e[0]!=="<"?K.dirname(e):null;if(a&&!(S!==null?C===S:!1))throw new nt(`Unrecognized or legacy configuration settings found: ${c} - run "yarn config -v" to see the list of settings supported in Yarn`);this.invalid.set(c,e);continue}if(this.sources.has(c)&&!(n||h.type==="MAP"||h.isArray&&h.concatenateValues))continue;let E;try{E=Tj(this,c,f,h,s)}catch(C){throw C.message+=` in ${Ut(this,e,pt.PATH)}`,C}if(c==="enableStrictSettings"&&e!==""){a=E;continue}if(h.type==="MAP"){let C=this.values.get(c);this.values.set(c,new Map(n?[...C,...E]:[...E,...C])),this.sources.set(c,`${this.sources.get(c)}, ${e}`)}else if(h.isArray&&h.concatenateValues){let C=this.values.get(c);this.values.set(c,n?[...C,...E]:[...E,...C]),this.sources.set(c,`${this.sources.get(c)}, ${e}`)}else this.values.set(c,E),this.sources.set(c,e)}}get(e){if(!this.values.has(e))throw new Error(`Invalid configuration key "${e}"`);return this.values.get(e)}getSpecial(e,{hideSecrets:r=!1,getNativePaths:s=!1}){let a=this.get(e),n=this.settings.get(e);if(typeof n>"u")throw new nt(`Couldn't find a configuration settings named "${e}"`);return dT(a,n,{hideSecrets:r,getNativePaths:s})}getSubprocessStreams(e,{header:r,prefix:s,report:a}){let n,c,f=le.createWriteStream(e);if(this.get("enableInlineBuilds")){let p=a.createStreamReporter(`${s} ${Ut(this,"STDOUT","green")}`),h=a.createStreamReporter(`${s} ${Ut(this,"STDERR","red")}`);n=new xj.PassThrough,n.pipe(p),n.pipe(f),c=new xj.PassThrough,c.pipe(h),c.pipe(f)}else n=f,c=f,typeof r<"u"&&n.write(`${r} `);return{stdout:n,stderr:c}}makeResolver(){let e=[];for(let r of this.plugins.values())for(let s of r.resolvers||[])e.push(new s);return new em([new TQ,new Ei,...e])}makeFetcher(){let e=[];for(let r of this.plugins.values())for(let s of r.fetchers||[])e.push(new s);return new aI([new lI,new cI,...e])}getLinkers(){let e=[];for(let r of this.plugins.values())for(let s of r.linkers||[])e.push(new s);return e}getSupportedArchitectures(){let e=pv(),r=this.get("supportedArchitectures"),s=r.get("os");s!==null&&(s=s.map(c=>c==="current"?e.os:c));let a=r.get("cpu");a!==null&&(a=a.map(c=>c==="current"?e.cpu:c));let n=r.get("libc");return n!==null&&(n=Yl(n,c=>c==="current"?e.libc??Yl.skip:c)),{os:s,cpu:a,libc:n}}isInteractive({interactive:e,stdout:r}){return r.isTTY?e??this.get("preferInteractive"):!1}async getPackageExtensions(){if(this.packageExtensions!==null)return this.packageExtensions;this.packageExtensions=new Map;let e=this.packageExtensions,r=(s,a,{userProvided:n=!1}={})=>{if(!ul(s.range))throw new Error("Only semver ranges are allowed as keys for the packageExtensions setting");let c=new Ht;c.load(a,{yamlCompatibilityMode:!0});let f=LB(e,s.identHash),p=[];f.push([s.range,p]);let h={status:"inactive",userProvided:n,parentDescriptor:s};for(let E of c.dependencies.values())p.push({...h,type:"Dependency",descriptor:E});for(let E of c.peerDependencies.values())p.push({...h,type:"PeerDependency",descriptor:E});for(let[E,C]of c.peerDependenciesMeta)for(let[S,P]of Object.entries(C))p.push({...h,type:"PeerDependencyMeta",selector:E,key:S,value:P})};await this.triggerHook(s=>s.registerPackageExtensions,this,r);for(let[s,a]of this.get("packageExtensions"))r(I0(s,!0),Wk(a),{userProvided:!0});return e}normalizeLocator(e){return ul(e.reference)?Vs(e,`${this.get("defaultProtocol")}${e.reference}`):Up.test(e.reference)?Vs(e,`${this.get("defaultProtocol")}${e.reference}`):e}normalizeDependency(e){return ul(e.range)?On(e,`${this.get("defaultProtocol")}${e.range}`):Up.test(e.range)?On(e,`${this.get("defaultProtocol")}${e.range}`):e}normalizeDependencyMap(e){return new Map([...e].map(([r,s])=>[r,this.normalizeDependency(s)]))}normalizePackage(e,{packageExtensions:r}){let s=WB(e),a=r.get(e.identHash);if(typeof a<"u"){let c=e.version;if(c!==null){for(let[f,p]of a)if(eA(c,f))for(let h of p)switch(h.status==="inactive"&&(h.status="redundant"),h.type){case"Dependency":typeof s.dependencies.get(h.descriptor.identHash)>"u"&&(h.status="active",s.dependencies.set(h.descriptor.identHash,this.normalizeDependency(h.descriptor)));break;case"PeerDependency":typeof s.peerDependencies.get(h.descriptor.identHash)>"u"&&(h.status="active",s.peerDependencies.set(h.descriptor.identHash,h.descriptor));break;case"PeerDependencyMeta":{let E=s.peerDependenciesMeta.get(h.selector);(typeof E>"u"||!Object.hasOwn(E,h.key)||E[h.key]!==h.value)&&(h.status="active",Vl(s.peerDependenciesMeta,h.selector,()=>({}))[h.key]=h.value)}break;default:r3(h)}}}let n=c=>c.scope?`${c.scope}__${c.name}`:`${c.name}`;for(let c of s.peerDependenciesMeta.keys()){let f=Da(c);s.peerDependencies.has(f.identHash)||s.peerDependencies.set(f.identHash,On(f,"*"))}for(let c of s.peerDependencies.values()){if(c.scope==="types")continue;let f=n(c),p=ba("types",f),h=cn(p);s.peerDependencies.has(p.identHash)||s.peerDependenciesMeta.has(h)||s.dependencies.has(p.identHash)||(s.peerDependencies.set(p.identHash,On(p,"*")),s.peerDependenciesMeta.set(h,{optional:!0}))}return s.dependencies=new Map(Ys(s.dependencies,([,c])=>ll(c))),s.peerDependencies=new Map(Ys(s.peerDependencies,([,c])=>ll(c))),s}getLimit(e){return Vl(this.limits,e,()=>(0,Sde.default)(this.get(e)))}async triggerHook(e,...r){for(let s of this.plugins.values()){let a=s.hooks;if(!a)continue;let n=e(a);n&&await n(...r)}}async triggerMultipleHooks(e,r){for(let s of r)await this.triggerHook(e,...s)}async reduceHook(e,r,...s){let a=r;for(let n of this.plugins.values()){let c=n.hooks;if(!c)continue;let f=e(c);f&&(a=await f(a,...s))}return a}async firstHook(e,...r){for(let s of this.plugins.values()){let a=s.hooks;if(!a)continue;let n=e(a);if(!n)continue;let c=await n(...r);if(typeof c<"u")return c}return null}}});var Gr={};Vt(Gr,{EndStrategy:()=>Lj,ExecError:()=>ET,PipeError:()=>mv,execvp:()=>bj,pipevp:()=>Yu});function im(t){return t!==null&&typeof t.fd=="number"}function Fj(){}function Nj(){for(let t of sm)t.kill()}async function Yu(t,e,{cwd:r,env:s=process.env,strict:a=!1,stdin:n=null,stdout:c,stderr:f,end:p=2}){let h=["pipe","pipe","pipe"];n===null?h[0]="ignore":im(n)&&(h[0]=n),im(c)&&(h[1]=c),im(f)&&(h[2]=f);let E=(0,Oj.default)(t,e,{cwd:ue.fromPortablePath(r),env:{...s,PWD:ue.fromPortablePath(r)},stdio:h});sm.add(E),sm.size===1&&(process.on("SIGINT",Fj),process.on("SIGTERM",Nj)),!im(n)&&n!==null&&n.pipe(E.stdin),im(c)||E.stdout.pipe(c,{end:!1}),im(f)||E.stderr.pipe(f,{end:!1});let C=()=>{for(let S of new Set([c,f]))im(S)||S.end()};return new Promise((S,P)=>{E.on("error",I=>{sm.delete(E),sm.size===0&&(process.off("SIGINT",Fj),process.off("SIGTERM",Nj)),(p===2||p===1)&&C(),P(I)}),E.on("close",(I,R)=>{sm.delete(E),sm.size===0&&(process.off("SIGINT",Fj),process.off("SIGTERM",Nj)),(p===2||p===1&&I!==0)&&C(),I===0||!a?S({code:Mj(I,R)}):P(new mv({fileName:t,code:I,signal:R}))})})}async function bj(t,e,{cwd:r,env:s=process.env,encoding:a="utf8",strict:n=!1}){let c=["ignore","pipe","pipe"],f=[],p=[],h=ue.fromPortablePath(r);typeof s.PWD<"u"&&(s={...s,PWD:h});let E=(0,Oj.default)(t,e,{cwd:h,env:s,stdio:c});return E.stdout.on("data",C=>{f.push(C)}),E.stderr.on("data",C=>{p.push(C)}),await new Promise((C,S)=>{E.on("error",P=>{let I=ze.create(r),R=Ut(I,t,pt.PATH);S(new Yt(1,`Process ${R} failed to spawn`,N=>{N.reportError(1,` ${Zf(I,{label:"Thrown Error",value:Hu(pt.NO_HINT,P.message)})}`)}))}),E.on("close",(P,I)=>{let R=a==="buffer"?Buffer.concat(f):Buffer.concat(f).toString(a),N=a==="buffer"?Buffer.concat(p):Buffer.concat(p).toString(a);P===0||!n?C({code:Mj(P,I),stdout:R,stderr:N}):S(new ET({fileName:t,code:P,signal:I,stdout:R,stderr:N}))})})}function Mj(t,e){let r=jdt.get(e);return typeof r<"u"?128+r:t??1}function qdt(t,e,{configuration:r,report:s}){s.reportError(1,` ${Zf(r,t!==null?{label:"Exit Code",value:Hu(pt.NUMBER,t)}:{label:"Exit Signal",value:Hu(pt.CODE,e)})}`)}var Oj,Lj,mv,ET,sm,jdt,hT=Ct(()=>{bt();Oj=et(j_());dv();Fc();Qc();Lj=(s=>(s[s.Never=0]="Never",s[s.ErrorCode=1]="ErrorCode",s[s.Always=2]="Always",s))(Lj||{}),mv=class extends Yt{constructor({fileName:e,code:r,signal:s}){let a=ze.create(K.cwd()),n=Ut(a,e,pt.PATH);super(1,`Child ${n} reported an error`,c=>{qdt(r,s,{configuration:a,report:c})}),this.code=Mj(r,s)}},ET=class extends mv{constructor({fileName:e,code:r,signal:s,stdout:a,stderr:n}){super({fileName:e,code:r,signal:s}),this.stdout=a,this.stderr=n}};sm=new Set;jdt=new Map([["SIGINT",2],["SIGQUIT",3],["SIGKILL",9],["SIGTERM",15]])});function Pde(t){bde=t}function yv(){return typeof _j>"u"&&(_j=bde()),_j}var _j,bde,Uj=Ct(()=>{bde=()=>{throw new Error("Assertion failed: No libzip instance is available, and no factory was configured")}});var xde=L((IT,jj)=>{var Gdt=Object.assign({},Ie("fs")),Hj=function(){var t=typeof document<"u"&&document.currentScript?document.currentScript.src:void 0;return typeof __filename<"u"&&(t=t||__filename),function(e){e=e||{};var r=typeof e<"u"?e:{},s,a;r.ready=new Promise(function(Je,st){s=Je,a=st});var n={},c;for(c in r)r.hasOwnProperty(c)&&(n[c]=r[c]);var f=[],p="./this.program",h=function(Je,st){throw st},E=!1,C=!0,S="";function P(Je){return r.locateFile?r.locateFile(Je,S):S+Je}var I,R,N,U;C&&(E?S=Ie("path").dirname(S)+"/":S=__dirname+"/",I=function(st,St){var lr=Me(st);return lr?St?lr:lr.toString():(N||(N=Gdt),U||(U=Ie("path")),st=U.normalize(st),N.readFileSync(st,St?null:"utf8"))},R=function(st){var St=I(st,!0);return St.buffer||(St=new Uint8Array(St)),we(St.buffer),St},process.argv.length>1&&(p=process.argv[1].replace(/\\/g,"/")),f=process.argv.slice(2),h=function(Je){process.exit(Je)},r.inspect=function(){return"[Emscripten Module object]"});var W=r.print||console.log.bind(console),te=r.printErr||console.warn.bind(console);for(c in n)n.hasOwnProperty(c)&&(r[c]=n[c]);n=null,r.arguments&&(f=r.arguments),r.thisProgram&&(p=r.thisProgram),r.quit&&(h=r.quit);var ie=0,Ae=function(Je){ie=Je},ce;r.wasmBinary&&(ce=r.wasmBinary);var me=r.noExitRuntime||!0;typeof WebAssembly!="object"&&ns("no native wasm support detected");function pe(Je,st,St){switch(st=st||"i8",st.charAt(st.length-1)==="*"&&(st="i32"),st){case"i1":return Ye[Je>>0];case"i8":return Ye[Je>>0];case"i16":return Eh((Je>>1)*2);case"i32":return no((Je>>2)*4);case"i64":return no((Je>>2)*4);case"float":return pf((Je>>2)*4);case"double":return yh((Je>>3)*8);default:ns("invalid type for getValue: "+st)}return null}var Be,Ce=!1,g;function we(Je,st){Je||ns("Assertion failed: "+st)}function ye(Je){var st=r["_"+Je];return we(st,"Cannot call unknown function "+Je+", make sure it is exported"),st}function fe(Je,st,St,lr,ee){var Ee={string:function(Gi){var Tn=0;if(Gi!=null&&Gi!==0){var Ga=(Gi.length<<2)+1;Tn=Bi(Ga),dt(Gi,Tn,Ga)}return Tn},array:function(Gi){var Tn=Bi(Gi.length);return Fe(Gi,Tn),Tn}};function Oe(Gi){return st==="string"?De(Gi):st==="boolean"?!!Gi:Gi}var gt=ye(Je),yt=[],Dt=0;if(lr)for(var tr=0;tr=St)&&ke[lr];)++lr;return X.decode(ke.subarray(Je,lr))}function Re(Je,st,St,lr){if(!(lr>0))return 0;for(var ee=St,Ee=St+lr-1,Oe=0;Oe=55296&><=57343){var yt=Je.charCodeAt(++Oe);gt=65536+((gt&1023)<<10)|yt&1023}if(gt<=127){if(St>=Ee)break;st[St++]=gt}else if(gt<=2047){if(St+1>=Ee)break;st[St++]=192|gt>>6,st[St++]=128|gt&63}else if(gt<=65535){if(St+2>=Ee)break;st[St++]=224|gt>>12,st[St++]=128|gt>>6&63,st[St++]=128|gt&63}else{if(St+3>=Ee)break;st[St++]=240|gt>>18,st[St++]=128|gt>>12&63,st[St++]=128|gt>>6&63,st[St++]=128|gt&63}}return st[St]=0,St-ee}function dt(Je,st,St){return Re(Je,ke,st,St)}function j(Je){for(var st=0,St=0;St=55296&&lr<=57343&&(lr=65536+((lr&1023)<<10)|Je.charCodeAt(++St)&1023),lr<=127?++st:lr<=2047?st+=2:lr<=65535?st+=3:st+=4}return st}function rt(Je){var st=j(Je)+1,St=Ma(st);return St&&Re(Je,Ye,St,st),St}function Fe(Je,st){Ye.set(Je,st)}function Ne(Je,st){return Je%st>0&&(Je+=st-Je%st),Je}var Pe,Ye,ke,it,_e,x,w,b,y,F;function z(Je){Pe=Je,r.HEAP_DATA_VIEW=F=new DataView(Je),r.HEAP8=Ye=new Int8Array(Je),r.HEAP16=it=new Int16Array(Je),r.HEAP32=x=new Int32Array(Je),r.HEAPU8=ke=new Uint8Array(Je),r.HEAPU16=_e=new Uint16Array(Je),r.HEAPU32=w=new Uint32Array(Je),r.HEAPF32=b=new Float32Array(Je),r.HEAPF64=y=new Float64Array(Je)}var Z=r.INITIAL_MEMORY||16777216,$,oe=[],xe=[],Te=[],lt=!1;function It(){if(r.preRun)for(typeof r.preRun=="function"&&(r.preRun=[r.preRun]);r.preRun.length;)Pt(r.preRun.shift());Fs(oe)}function qt(){lt=!0,Fs(xe)}function ir(){if(r.postRun)for(typeof r.postRun=="function"&&(r.postRun=[r.postRun]);r.postRun.length;)Pr(r.postRun.shift());Fs(Te)}function Pt(Je){oe.unshift(Je)}function gn(Je){xe.unshift(Je)}function Pr(Je){Te.unshift(Je)}var Ir=0,Nr=null,nn=null;function ai(Je){Ir++,r.monitorRunDependencies&&r.monitorRunDependencies(Ir)}function wo(Je){if(Ir--,r.monitorRunDependencies&&r.monitorRunDependencies(Ir),Ir==0&&(Nr!==null&&(clearInterval(Nr),Nr=null),nn)){var st=nn;nn=null,st()}}r.preloadedImages={},r.preloadedAudios={};function ns(Je){r.onAbort&&r.onAbort(Je),Je+="",te(Je),Ce=!0,g=1,Je="abort("+Je+"). Build with -s ASSERTIONS=1 for more info.";var st=new WebAssembly.RuntimeError(Je);throw a(st),st}var to="data:application/octet-stream;base64,";function Bo(Je){return Je.startsWith(to)}var ji="data:application/octet-stream;base64,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QCAAKAJIIgEgASADaiADEAcaAkAgAyAAKAJsIgFNBEAgACABIANrNgJsDAELIABCADcCbAsgACAAKAJoIANrIgE2AmggACAAKAJYIANrNgJYIAEgACgChC5JBEAgACABNgKELgsgAEH8gAEoAgARAwAgAiADaiECCwJAIAAoAgAiASgCBCIERQ0AIAAoAjwhBSAAIAIgBCACIARJGyICBH8gACgCSCAAKAJoaiAFaiEFIAEgBCACazYCBAJAAkACQAJAIAEoAhwiBCgCFEEBaw4CAQACCyAEQaABaiAFIAEoAgAgAkHcgAEoAgARCAAMAgsgASABKAIwIAUgASgCACACQcSAASgCABEEADYCMAwBCyAFIAEoAgAgAhAHGgsgASABKAIAIAJqNgIAIAEgASgCCCACajYCCCAAKAI8BSAFCyACaiICNgI8AkAgACgChC4iASACakEDSQ0AIAAoAmggAWshAQJAIAAoAnRBgQhPBEAgACAAIAAoAkggAWoiAi0AACACLQABIAAoAnwRAAA2AlQMAQsgAUUNACAAIAFBAWsgACgChAERAgAaCyAAKAKELiAAKAI8IgJBAUZrIgRFDQAgACABIAQgACgCgAERBQAgACAAKAKELiAEazYChC4gACgCPCECCyACQYUCSw0AIAAoAgAoAgRFDQAgACgCMCEBDAELCwJAIAAoAkQiAiAAKAJAIgNNDQAgAAJ/IAAoAjwgACgCaGoiASADSwRAIAAoAkggAWpBACACIAFrIgNBggIgA0GCAkkbIgMQGSABIANqDAELIAFBggJqIgEgA00NASAAKAJIIANqQQAgAiADayICIAEgA2siAyACIANJGyIDEBkgACgCQCADags2AkALC50CAQF/AkAgAAJ/IAAoAqAuIgFBwABGBEAgACgCBCAAKAIQaiAAKQOYLjcAACAAQgA3A5guIAAgACgCEEEIajYCEEEADAELIAFBIE4EQCAAKAIEIAAoAhBqIAApA5guPgAAIAAgAEGcLmo1AgA3A5guIAAgACgCEEEEajYCECAAIAAoAqAuQSBrIgE2AqAuCyABQRBOBEAgACgCBCAAKAIQaiAAKQOYLj0AACAAIAAoAhBBAmo2AhAgACAAKQOYLkIQiDcDmC4gACAAKAKgLkEQayIBNgKgLgsgAUEISA0BIAAgACgCECIBQQFqNgIQ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KAJUIgEEfyABQQAgARAiEBkgACgCVAVBAAsQBiAAQQA2AlQLC9wMAgl/AX4jAEFAaiIGJAACQAJAAkACQAJAIAEoAjBBABAjIgVBAkZBACABKAI4QQAQIyIEQQFGGw0AIAVBAUZBACAEQQJGGw0AIAVBAkciAw0BIARBAkcNAQsgASABLwEMQYAQcjsBDEEAIQMMAQsgASABLwEMQf/vA3E7AQxBACEFIANFBEBB9eABIAEoAjAgAEEIahBpIgVFDQILIAJBgAJxBEAgBSEDDAELIARBAkcEQCAFIQMMAQtB9cYBIAEoAjggAEEIahBpIgNFBEAgBRAcDAILIAMgBTYCAAsgASABLwEMQf7/A3EgAS8BUiIFQQBHcjsBDAJAAkACQAJAAn8CQAJAIAEpAyhC/v///w9WDQAgASkDIEL+////D1YNACACQYAEcUUNASABKQNIQv////8PVA0BCyAFQYECa0H//wNxQQNJIQdBAQwBCyAFQYECa0H//wNxIQQgAkGACnFBgApHDQEgBEEDSSEHQQALIQkgBkIcEBciBEUEQCAAQQhqIgAEQCAAQQA2AgQgAEEONgIACyADEBwMBQsgAkGACHEhBQJAAkAgAkGAAnEEQAJAIAUNACABKQMgQv////8PVg0AIAEpAyhCgICAgBBUDQMLIAQgASkDKBAYIAEpAyAhDAwBCwJAAkACQCAFDQAgASkDIEL/////D1YNACABKQMoIgxC/////w9WDQEgASkDSEKAgICAEFQNBAsgASkDKCIMQv////8PVA0BCyAEIAwQGAsgASkDICIMQv////8PWgRAIAQgDBAYCyABKQNIIgxC/////w9UDQELIAQgDBAYCyAELQAARQRAIABBCGoiAARAIABBADYCBCAAQRQ2AgALIAQQCCADEBwMBQtBASEKQQEgBC0AAAR+IAQpAxAFQgALp0H//wNxIAYQRyEFIAQQCCAFIAM2AgAgBw0BDAILIAMhBSAEQQJLDQELIAZCBxAXIgRFBEAgAEEIaiIABEAgAEEANgIEIABBDjYCAAsgBRAcDAMLIARBAhANIARBhxJBAhAsIAQgAS0AUhBwIAQgAS8BEBANIAQtAABFBEAgAEEIaiIABEAgAEEANgIEIABBFDYCAAsgBBAIDAILQYGyAkEHIAYQRyEDIAQQCCADIAU2AgBBASEL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NgIEIABB7A42AhggGiEdDAMLIARB/wFxIgJBwABxRQRAIAggDSADLwECQQJ0aiAdp0F/IAJ0QX9zcUECdGoiAy0AASICayEIIB0gAq2IIR0gAy0AACIERQ0HDAELCyAEQSBxBEAgB0G//gA2AgQgASECDAgLIAdB0f4ANgIEIABB0A42AhggASECDAcLIB1BfyAGdEF/c62DIBt8IhunIQUgCCAEQQ9xIgNrIQggGiAErUIPg4ghHSABIBdrIgYgAjMBAiAaQX8gA3RBf3Otg3ynIgRPDQIgBCAGayIGIBlNDQEgBygCjEdFDQEgB0HR/gA2AgQgAEG5DDYCGAsgASECIAshBQwFCwJAIA5FBEAgDCARIAZraiEDDAELIAYgDk0EQCAMIA4gBmtqIQMMAQsgDCARIAYgDmsiBmtqIQMgBSAGTQ0AIAUgBmshBQJAAkAgASADTSABIA8gAWusIhogBq0iGyAaIBtUGyIapyIGaiICIANLcQ0AIAMgBmogAUsgASADT3ENACABIAMgBhAHGiACIQEMAQsgASADIAMgAWsiASABQR91IgFqIAFzIgIQByACaiEBIBogAq0iHn0iHFANACACIANqIQIDQAJAIBwgHiAcIB5UGyIbQiBUBEAgGyEaDAELIBsiGkIgfSIgQgWIQgF8QgODIh9QRQRAA0AgASACKQAANwAAIAEgAikAGDcAGCABIAIpABA3ABAgASACKQAINwAIIBpCIH0hGiACQSBqIQIgAUEgaiEBIB9CAX0iH0IAUg0ACwsgIELgAFQNAANAIAEgAikAADcAACABIAIpABg3ABggASACKQAQNwAQIAEgAikACDcACCABIAIpADg3ADggASACKQAwNwAwIAEgAikAKDcAKCABIAIpACA3ACAgASACKQBYNwBYIAEgAikAUDcAUCABIAIpAEg3AEggASACKQBANwBAIAEgAikAYDcAYCABIAIpAGg3AGggASACKQBwNwBwIAEgAikAeDcAeCACQYABaiECIAFBgAFqIQEgGkKAAX0iGkIfVg0ACwsgGkIQWgRAIAEgAikAADcAACABIAIpAAg3AAggGkIQfSEaIAJBEGohAiABQRBqIQELIBpCCFoEQCABIAIpAAA3AAAgGkIIfSEaIAJBCGohAiABQQhqIQELIBpCBFoEQCABIAIoAAA2AAAgGkIEfSEaIAJBBGohAiABQQRqIQELIBpCAloEQCABIAIvAAA7AAAgGkICfSEaIAJBAmohAiABQQJqIQELIBwgG30hHCAaUEUEQCABIAItAAA6AAAgAkEBaiECIAFBAWohAQsgHEIAUg0ACwsgDiEGIAwhAwsgBSAGSwRAAkACQCABIANNIAEgDyABa6wiGiAGrSIbIBogG1QbIhqnIglqIgIgA0txDQAgAyAJaiABSyABIANPcQ0AIAEgAyAJEAcaDAELIAEgAyADIAFrIgEgAUEfdSIBaiABcyIBEAcgAWohAiAaIAGtIh59IhxQDQAgASADaiEBA0ACQCAcIB4gHCAeVBsiG0IgVARAIBshGgwBCyAbIhpCIH0iIEIFiEIBfEIDgyIfUEUEQANAIAIgASkAADcAACACIAEpABg3ABggAiABKQAQNwAQIAIgASkACDcACCAaQiB9IRogAUEgaiEBIAJBIGohAiAfQgF9Ih9CAFINAAsLICBC4ABUDQADQCACIAEpAAA3AAAgAiABKQAYNwAYIAIgASkAEDcAECACIAEpAAg3AAggAiABKQA4NwA4IAIgASkAMDcAMCACIAEpACg3ACggAiABKQAgNwAgIAIgASkAWDcAWCACIAEpAFA3AFAgAiABKQBINwBIIAIgASkAQDcAQCACIAEpAGA3AGAgAiABKQBoNwBoIAIgASkAcDcAcCACIAEpAHg3AHggAUGAAWohASACQYABaiECIBpCgAF9IhpCH1YNAAsLIBpCEFoEQCACIAEpAAA3AAAgAiABKQAINwAIIBpCEH0hGiACQRBqIQIgAUEQaiEBCyAaQghaBEAgAiABKQAANwAAIBpCCH0hGiACQQhqIQIgAUEIaiEBCyAaQgRaBEAgAiABKAAANgAAIBpCBH0hGiACQQRqIQIgAUEEaiEBCyAaQgJaBEAgAiABLwAAOwAAIBpCAn0hGiACQQJqIQIgAUECaiEBCyAcIBt9IRwgGlBFBEAgAiABLQAAOgAAIAJBAWohAiABQQFqIQELIBxCAFINAAsLIAUgBmshAUEAIARrIQUCQCAEQQdLBEAgBCEDDAELIAEgBE0EQCAEIQMMAQsgAiAEayEFA0ACQCACIAUpAAA3AAAgBEEBdCEDIAEgBGshASACIARqIQIgBEEDSw0AIAMhBCABIANLDQELC0EAIANrIQULIAIgBWohBAJAIAUgDyACa6wiGiABrSIbIBogG1QbIhqnIgFIIAVBf0pxDQAgBUEBSCABIARqIAJLcQ0AIAIgBCABEAcgAWohAgwDCyACIAQgAyADQR91IgFqIAFzIgEQByABaiECIBogAa0iHn0iHFANAiABIARqIQEDQAJAIBwgHiAcIB5UGyIbQiBUBEAgGyEaDAELIBsiGkIgfSIgQgWIQgF8QgODIh9QRQRAA0AgAiABKQAANwAAIAIgASkAGDcAGCACIAEpABA3ABAgAiABKQAINwAIIBpCIH0hGiABQSBqIQEgAkEgaiECIB9CAX0iH0IAUg0ACwsgIELgAFQNAANAIAIgASkAADcAACACIAEpABg3ABggAiABKQAQNwAQIAIgASkACDcACCACIAEpADg3ADggAiABKQAwNwAwIAIgASkAKDcAKCACIAEpACA3ACAgAiABKQBYNwBYIAIgASkAUDcAUCACIAEpAEg3AEggAiABKQBANwBAIAIgASkAYDcAYCACIAEpAGg3AGggAiABKQBwNwBwIAIgASkAeDcAeCABQYABaiEBIAJBgAFqIQIgGkKAAX0iGkIfVg0ACwsgGkIQWgRAIAIgASkAADcAACACIAEpAAg3AAggGkIQfSEaIAJBEGohAiABQRBqIQELIBpCCFoEQCACIAEpAAA3AAAgGkIIfSEaIAJBCGohAiABQQhqIQELIBpCBFoEQCACIAEoAAA2AAAgGkIEfSEaIAJBBGohAiABQQRqIQELIBpCAloEQCACIAEvAAA7AAAgGkICfSEaIAJBAmohAiABQQJqIQELIBwgG30hHCAaUEUEQCACIAEtAAA6AAAgAkEBaiECIAFBAWohAQsgHFBFDQALDAILAkAgASADTSABIA8gAWusIhogBa0iGyAaIBtUGyIapyIEaiICIANLcQ0AIAMgBGogAUsgASADT3ENACABIAMgBBAHGgwCCyABIAMgAyABayIBIAFBH3UiAWogAXMiARAHIAFqIQIgGiABrSIefSIcUA0BIAEgA2ohAQNAAkAgHCAeIBwgHlQbIhtCIFQEQCAbIRoMAQsgGyIaQiB9IiBCBYhCAXxCA4MiH1BFBEADQCACIAEpAAA3AAAgAiABKQAYNwAYIAIgASkAEDcAECACIAEpAAg3AAggGkIgfSEaIAFBIGohASACQSBqIQIgH0IBfSIfQgBSDQALCyAgQuAAVA0AA0AgAiABKQAANwAAIAIgASkAGDcAGCACIAEpABA3ABAgAiABKQAINwAIIAIgASkAODcAOCACIAEpADA3ADAgAiABKQAoNwAoIAIgASkAIDcAICACIAEpAFg3AFggAiABKQBQNwBQIAIgASkASDcASCACIAEpAEA3AEAgAiABKQBgNwBgIAIgASkAaDcAaCACIAEpAHA3AHAgAiABKQB4NwB4IAFBgAFqIQEgAkGAAWohAiAaQoABfSIaQh9WDQALCyAaQhBaBEAgAiABKQAANwAAIAIgASkACDcACCAaQhB9IRogAkEQaiECIAFBEGohAQsgGkIIWgRAIAIgASkAADcAACAaQgh9IRogAkEIaiECIAFBCGohAQsgGkIEWgRAIAIgASgAADYAACAaQgR9IRogAkEEaiECIAFBBGohAQsgGkICWgRAIAIgAS8AADsAACAaQgJ9IRogAkECaiECIAFBAmohAQsgHCAbfSEcIBpQRQRAIAIgAS0AADoAACACQQFqIQIgAUEBaiEBCyAcUEUNAAsMAQsCQAJAIBYEQAJAIAQgBUkEQCAHKAKYRyAESw0BCyABIARrIQMCQEEAIARrIgVBf0ogDyABa6wiGiAbIBogG1QbIhqnIgIgBUpxDQAgBUEBSCACIANqIAFLcQ0AIAEgAyACEAcgAmohAgwFCyABIAMgBCAEQR91IgFqIAFzIgEQByABaiECIBogAa0iHn0iHFANBCABIANqIQEDQAJAIBwgHiAcIB5UGyIbQiBUBEAgGyEaDAELIBsiGkIgfSIgQgWIQgF8QgODIh9QRQRAA0AgAiABKQAANwAAIAIgASkAGDcAGCACIAEpABA3ABAgAiABKQAINwAIIBpCIH0hGiABQSBqIQEgAkEgaiECIB9CAX0iH0IAUg0ACwsgIELgAFQNAANAIAIgASkAADcAACACIAEpABg3ABggAiABKQAQNwAQIAIgASkACDcACCACIAEpADg3ADggAiABKQAwNwAwIAIgASkAKDcAKCACIAEpACA3ACAgAiABKQBYNwBYIAIgASkAUDcAUCACIAEpAEg3AEggAiABKQBANwBAIAIgASkAYDcAYCACIAEpAGg3AGggAiABKQBwNwBwIAIgASkAeDcAeCABQYABaiEBIAJBgAFqIQIgGkKAAX0iGkIfVg0ACwsgGkIQWgRAIAIgASkAADcAACACIAEpAAg3AAggGkIQfSEaIAJBEGohAiABQRBqIQELIBpCCFoEQCACIAEpAAA3AAAgGkIIfSEaIAJBCGohAiABQQhqIQELIBpCBFoEQCACIAEoAAA2AAAgGkIEfSEaIAJBBGohAiABQQRqIQELIBpCAloEQCACIAEvAAA7AAAgGkICfSEaIAJBAmohAiABQQJqIQELIBwgG30hHCAaUEUEQCACIAEtAAA6AAAgAkEBaiECIAFBAWohAQsgHFBFDQALDAQLIBAgAWsiCUEBaiIGIAUgBSAGSxshAyABIARrIQIgAUEHcUUNAiADRQ0CIAEgAi0AADoAACACQQFqIQIgAUEBaiIGQQdxQQAgA0EBayIFGw0BIAYhASAFIQMgCSEGDAILAkAgBCAFSQRAIAcoAphHIARLDQELIAEgASAEayIGKQAANwAAIAEgBUEBa0EHcUEBaiIDaiECIAUgA2siBEUNAyADIAZqIQEDQCACIAEpAAA3AAAgAUEIaiEBIAJBCGohAiAEQQhrIgQNAAsMAwsgASAEIAUQPyECDAILIAEgAi0AADoAASAJQQFrIQYgA0ECayEFIAJBAWohAgJAIAFBAmoiCkEHcUUNACAFRQ0AIAEgAi0AADoAAiAJQQJrIQYgA0EDayEFIAJBAWohAgJAIAFBA2oiCkEHcUUNACAFRQ0AIAEgAi0AADoAAyAJQQNrIQYgA0EEayEFIAJBAWohAgJAIAFBBGoiCkEHcUUNACAFRQ0AIAEgAi0AADoABCAJQQRrIQYgA0EFayEFIAJBAWohAgJAIAFBBWoiCkEHcUUNACAFRQ0AIAEgAi0AADoABSAJQQVrIQYgA0EGayEFIAJBAWohAgJAIAFBBmoiCkEHcUUNACAFRQ0AIAEgAi0AADoABiAJQQZrIQYgA0EHayEFIAJBAWohAgJAIAFBB2oiCkEHcUUNACAFRQ0AIAEgAi0AADoAByAJQQdrIQYgA0EIayEDIAFBCGohASACQQFqIQIMBgsgCiEBIAUhAwwFCyAKIQEgBSEDDAQLIAohASAFIQMMAwsgCiEBIAUhAwwCCyAKIQEgBSEDDAELIAohASAFIQMLAkACQCAGQRdNBEAgA0UNASADQQFrIQUgA0EHcSIEBEADQCABIAItAAA6AAAgA0EBayEDIAFBAWohASACQQFqIQIgBEEBayIEDQALCyAFQQdJDQEDQCABIAItAAA6AAAgASACLQABOgABIAEgAi0AAjoAAiABIAItAAM6AAMgASACLQAEOgAEIAEgAi0ABToABSABIAItAAY6AAYgASACLQAHOgAHIAFBCGohASACQQhqIQIgA0EIayIDDQALDAELIAMNAQsgASECDAELIAEgBCADED8hAgsgCyEFDAELIAEgAy0AAjoAACABQQFqIQILIAUgFE8NACACIBNJDQELCyAAIAI2AgwgACAFIAhBA3ZrIgE2AgAgACATIAJrQYMCajYCECAAIBQgAWtBDmo2AgQgByAIQQdxIgA2AogBIAcgHUJ/IACthkJ/hYM+AoQBC+cFAQR/IAMgAiACIANLGyEEIAAgAWshAgJAIABBB3FFDQAgBEUNACAAIAItAAA6AAAgA0EBayEGIAJBAWohAiAAQQFqIgdBB3FBACAEQQFrIgUbRQRAIAchACAFIQQgBiEDDAELIAAgAi0AADoAASADQQJrIQYgBEECayEFIAJBAWohAgJAIABBAmoiB0EHcUUNACAFRQ0AIAAgAi0AADoAAiADQQNrIQYgBEEDayEFIAJBAWohAgJAIABBA2oiB0EHcUUNACAFRQ0AIAAgAi0AADoAAyADQQRrIQYgBEEEayEFIAJBAWohAgJAIABBBGoiB0EHcUUNACAFRQ0AIAAgAi0AADoABCADQQVrIQYgBEEFayEFIAJBAWohAgJAIABBBWoiB0EHcUUNACAFRQ0AIAAgAi0AADoABSADQQZrIQYgBEEGayEFIAJBAWohAgJAIABBBmoiB0EHcUUNACAFRQ0AIAAgAi0AADoABiADQQdrIQYgBEEHayEFIAJBAWohAgJAIABBB2oiB0EHcUUNACAFRQ0AIAAgAi0AADoAByADQQhrIQMgBEEIayEEIABBCGohACACQQFqIQIMBgsgByEAIAUhBCAGIQMMBQsgByEAIAUhBCAGIQMMBAsgByEAIAUhBCAGIQMMAwsgByEAIAUhBCAGIQMMAgsgByEAIAUhBCAGIQMMAQsgByEAIAUhBCAGIQMLAkAgA0EXTQRAIARFDQEgBEEBayEBIARBB3EiAwRAA0AgACACLQAAOgAAIARBAWshBCAAQQFqIQAgAkEBaiECIANBAWsi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CyAAIAMtAAAEfiADKQMQBUIAC6dB//8DcSADKAIEEEcLIQUgAxAICyAEQRBqJAAgBQvgAQICfwF+QTAQCSICRQRAIAEEQCABQQA2AgQgAUEONgIAC0EADwsgAkIANwMIIAJBADYCACACQgA3AxAgAkIANwMYIAJCADcDICACQgA3ACUgAFAEQCACDwsCQCAAQv////8AVg0AIACnQQR0EAkiA0UNACACIAM2AgBBACEBQgEhBANAIAMgAUEEdGoiAUIANwIAIAFCADcABSAAIARSBEAgBKchASAEQgF8IQQMAQsLIAIgADcDCCACIAA3AxAgAg8LIAEEQCABQQA2AgQgAUEONgIAC0EAEBAgAhAGQQAL7gECA38BfiMAQRBrIgQkAAJAIARBDGpCBBAXIgNFBEBBfyECDAELAkAgAQRAIAJBgAZxIQUDQAJAIAUgASgCBHFFDQACQCADKQMIQgBUBEAgA0EAOgAADAELIANCADcDECADQQE6AAALIAMgAS8BCBANIAMgAS8BChANIAMtAABFBEAgAEEIaiIABEAgAEEANgIEIABBFDYCAAtBfyECDAQLQX8hAiAAIARBDGpCBBAbQQBIDQMgATMBCiIGUA0AIAAgASgCDCAGEBtBAEgNAwsgASgCACIBDQALC0EAIQILIAMQCAsgBEEQaiQAIAILPAEBfyAABEAgAUGABnEhAQNAIAEgACgCBHEEQCACIAAvAQpqQQRqIQILIAAoAgAiAA0ACwsgAkH//wNxC5wBAQN/IABFBEBBAA8LIAAhAwNAAn8CQAJAIAAvAQgiAUH04AFNBEAgAUEBRg0BIAFB9cYBRg0BDAILIAFBgbICRg0AIAFB9eABRw0BCyAAKAIAIQEgAEEANgIAIAAoAgwQBiAAEAYgASADIAAgA0YbIQMCQCACRQRAQQAhAgwBCyACIAE2AgALIAEMAQsgACICKAIACyIADQALIAMLsgQCBX8BfgJAAkACQCAAIAGtEBciAQRAIAEtAAANAUEAIQAMAgsgBARAIARBADYCBCAEQQ42AgALQQAPC0EAIQADQCABLQAABH4gASkDCCABKQMQfQVCAAtCBFQNASABEAwhByABIAEQDCIGrRATIghFBEBBACECIAQEQCAEQQA2AgQgBEEVNgIACyABEAggAEUNAwNAIAAoAgAhASAAKAIMEAYg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DCAAQRg2AggLDAMLIAAgBiAFLwFqQQAgBCABEQYAIQEgBhALIAFFDQILAkAgDUUEQCABIQMMAQsgACABIAUvAWgQgQEhAyABEAsgA0UNAQsCQCAJRSAMckUEQCADIQEMAQsgACADQQEQgAEhASADEAsgAUUNAQsgASEDDAELQQAhAwsgBUHwAGokACADC4UBAQF/IAFFBEAgAEEIaiIABEAgAEEANgIEIABBEjYCAAtBAA8LQTgQCSIDRQRAIABBCGoiAARAIABBADYCBCAAQQ42AgALQQAPCyADQQA2AhAgA0IANwIIIANCADcDKCADQQA2AgQgAyACNgIAIANCADcDGCADQQA2AjAgACABQTsgAxBCCw8AIAAgASACQQBBABCCAQusAgECfyABRQRAIABBCGoiAARAIABBADYCBCAAQRI2AgALQQAPCwJAIAJBfUsNACACQf//A3FBCEYNACAAQQhqIgAEQCAAQQA2AgQgAEEQNgIAC0EADwsCQEGwwAAQCSIFBEAgBUEANgIIIAVCADcCACAFQYiBAUGogQEgAxs2AqhAIAUgAjYCFCAFIAM6ABAgBUEAOgAPIAVBADsBDCAFIAMgAkF9SyIGcToADiAFQQggAiAGG0H//wNxIAQgBUGIgQFBqIEBIAMbKAIAEQAAIgI2AqxAIAINASAFEDEgBRAGCyAAQQhqIgAEQCAAQQA2AgQgAEEONgIAC0EADwsgACABQTogBRBCIgAEfyAABSAFKAKsQCAFKAKoQCgCBBEDACAFEDEgBRAGQQALC6ABAQF/IAIgACgCBCIDIAIgA0kbIgIEQCAAIAMgAms2AgQCQAJAAkACQCAAKAIcIgMoAhRBAWsOAgEAAgsgA0GgAWogASAAKAIAIAJB3IABKAIAEQgADAILIAAgACgCMCABIAAoAgAgAkHEgAEoAgARBAA2AjAMAQsgASAAKAIAIAIQBxoLIAAgACgCACACajYCACAAIAAoAgggAmo2AggLC7cCAQR/QX4hAgJAIABFDQAgACgCIEUNACAAKAIkIgRFDQAgACgCHCIBRQ0AIAEoAgAgAEcNAAJAAkAgASgCICIDQTlrDjkBAgICAgICAgICAgIBAgICAQICAgICAgICAgICAgICAgICAQICAgICAgICAgICAQICAgICAgICAgEACyADQZoFRg0AIANBKkcN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DCABajYCDCAGIAYoAgggAWo2AgggBSAFKAIUIAFqNgIUIAUgBSgCECABazYCECAGIAYoAhAgAWsiATYCECABDQAgBiAGKAIENgIIC0EAIQRBACEJIAMoAhBFDQAMCgsgAygCHCgCJCEGIAMgAygCGCIBQQFqNgIYIAEgBmotAAAhASADIARBAWo2AhAgAygCBCAEaiABOgAAIAEEQCADKAIQIQQMAQsLIAMoAhwoAixFDQAgAygCECIGIAlNDQAgBQJ/IAUoAjAhBEEAIAMoAgQgCWoiAUUNABogBCABIAYgCWtB1IABKAIAEQAACzYCMAsgA0HnADYCIAsCQCADKAIcKAIsBEAgAygCDCADKAIQIgFBAmpJBH8gBRAKIAMoAhANAkEABSABCyADKAIEaiAFKAIwOwAAIAMgAygCEEECajYCECADQaABakHkgAEoAgARAQAaCyADQfEANgIgIAUQCiADKAIQRQ0BDAcLDAYLIAUoAgQNAQsgAygCPA0AIApFDQEgAygCIEGaBUYNAQsCfyADKAKIASIBRQRAIAMgChCFAQwBCwJAAkACQCADKAKMAUECaw4CAAECCwJ/AkADQAJAAkAgAygCPA0AIAMQLyADKAI8DQAgCg0BQQAMBAsgAygCSCADKAJoai0AACEEIAMgAygC8C0iAUEBajYC8C0gASADKALsLWpBADoAACADIAMoAvAtIgFBAWo2AvAtIAEgAygC7C1qQQA6AAAgAyADKALwLSIBQQFqNgLwLSABIAMoAuwtaiAEOgAAIAMgBEECdGoiASABLwHkAUEBajsB5AEgAyADKAI8QQFrNgI8IAMgAygCaEEBaiIBNgJoIAMoAvAtIAMoAvQtRw0BQQAhBCADIAMoAlgiBkEATgR/IAMoAkggBmoFQQALIAEgBmtBABAPIAMgAygCaDYCWCADKAIAEAogAygCACgCEA0BDAILCyADQQA2AoQuIApBBEYEQCADIAMoAlgiAUEATgR/IAMoAkggAWoFQQALIAMoAmggAWtBARAPIAMgAygCaDYCWCADKAIAEApBA0ECIAMoAgAoAhAbDAILIAMoAvAtBEBBACEEIAMgAygCWCIBQQBOBH8gAygCSCABagVBAAsgAygCaCABa0EAEA8gAyADKAJoNgJYIAMoAgAQCiADKAIAKAIQRQ0BC0EBIQQLIAQL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bGl0ZXJhbC9sZW5ndGhzIHNldABpbnZhbGlkIGNvZGUgbGVuZ3RocyBzZXQAdW5rbm93biBoZWFkZXIgZmxhZ3Mgc2V0AGludmFsaWQgZGlzdGFuY2VzIHNldABpbnZhbGlkIGJpdCBsZW5ndGggcmVwZWF0AEZpbGUgYWxyZWFkeSBleGlzdHMAdG9vIG1hbnkgbGVuZ3RoIG9yIGRpc3RhbmNlIHN5bWJvbHMAaW52YWxpZCBzdG9yZWQgYmxvY2sgbGVuZ3RocwAlcyVzJXMAYnVmZmVyIGVycm9yAE5vIGVycm9yAHN0cmVhbSBlcnJvcgBUZWxsIGVycm9yAEludGVybmFsIGVycm9yAFNlZWsgZXJyb3IAV3JpdGUgZXJyb3IAZmlsZSBlcnJvcgBSZWFkIGVycm9yAFpsaWIgZXJyb3IAZGF0YSBlcnJvcgBDUkMgZXJyb3IAaW5jb21wYXRpYmxlIHZlcnNpb24AaW52YWxpZCBjb2RlIC0tIG1pc3NpbmcgZW5kLW9mLWJsb2NrAGluY29ycmVjdCBoZWFkZXIgY2hlY2sAaW5jb3JyZWN0IGxlbmd0aCBjaGVjawBpbmNvcnJlY3QgZGF0YSBjaGVjawBpbnZhbGlkIGRpc3RhbmNlIHRvbyBmYXIgYmFjawBoZWFkZXIgY3JjIG1pc21hdGNoADEuMi4xMy56bGliLW5nAGludmFsaWQgd2luZG93IHNpemUAUmVhZC1vbmx5IGFyY2hpdmUATm90IGEgemlwIGFyY2hpdmUAUmVzb3VyY2Ugc3RpbGwgaW4gdXNlAE1hbGxvYyBmYWlsdXJlAGludmFsaWQgYmxvY2sgdHlwZQBGYWlsdXJlIHRvIGNyZWF0ZSB0ZW1wb3JhcnkgZmlsZQBDYW4ndCBvcGVuIGZpbGUATm8gc3VjaCBmaWxlAFByZW1hdHVyZSBlbmQgb2YgZmlsZQBDYW4ndCByZW1vdmUgZmlsZQBpbnZhbGlkIGxpdGVyYWwvbGVuZ3RoIGNvZGUAaW52YWxpZCBkaXN0YW5jZSBjb2RlAHVua25vd24gY29tcHJlc3Npb24gbWV0aG9kAHN0cmVhbSBlbmQAQ29tcHJlc3NlZCBkYXRhIGludmFsaWQATXVsdGktZGlzayB6aXAgYXJjaGl2ZXMgbm90IHN1cHBvcnRl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UgAIANIACAAyAAgAsgAIAHIACADyAAgACgAIAIoACABKAAgAygAIACoACACqAAgAagAIAOoACAAaAAgAmgAIAFoACADaAAgAOgAIALoACAB6AAgA+gAIAAYACACGAAgARgAIAMYACAAmAAgApgAIAGYACADmAAgAFgAIAJYACABWAAgA1gAIADYACAC2AAgAdgAIAPYACAAOAAgAjgAIAE4ACADOAAgALgAIAK4ACABuAAgA7gAIAB4ACACeAAgAXgAIAN4ACAA+AAgAvgAIAH4ACAD+AAgAAQAIAIEACABBAAgAwQAIACEACAChAAgAYQAIAOEACAARAAgAkQAIAFEACADRAAgAMQAIALEACABxAAgA8QAIAAkACACJAAgASQAIAMkACAApAAgAqQAIAGkACADpAAgAGQAIAJkACABZAAgA2QAIADkACAC5AAgAeQAIAPkACAAFAAgAhQAIAEUACADFAAgAJQAIAKUACABlAAgA5QAIABUACACVAAgAVQAIANUACAA1AAgAtQAIAHUACAD1AAgADQAIAI0ACABNAAgAzQAIAC0ACACtAAgAbQAIAO0ACAAdAAgAnQAIAF0ACADdAAgAPQAIAL0ACAB9AAgA/QAIABMACQATAQkAkwAJAJMBCQBTAAkAUwEJANMACQDTAQkAMwAJADMBCQCzAAkAswEJAHMACQBzAQkA8wAJAPMBCQALAAkACwEJAIsACQCLAQkASwAJAEsBCQDLAAkAywEJACsACQArAQkAqwAJAKsBCQBrAAkAawEJAOsACQDrAQkAGwAJABsBCQCbAAkAmwEJAFsACQBbAQkA2wAJANsBCQA7AAkAOwEJALsACQC7AQkAewAJAHsBCQD7AAkA+wEJAAcACQAHAQkAhwAJAIcBCQBHAAkARwEJAMcACQDHAQkAJwAJACcBCQCnAAkApwEJAGcACQBnAQkA5wAJAOcBCQAXAAkAFwEJAJcACQCXAQkAVwAJAFcBCQDXAAkA1wEJADcACQA3AQkAtwAJALcBCQB3AAkAdwEJAPcACQD3AQkADwAJAA8BCQCPAAkAjwEJAE8ACQBPAQkAzwAJAM8BCQAvAAkALwEJAK8ACQCvAQkAbwAJAG8BCQDvAAkA7wEJAB8ACQAfAQkAnwAJAJ8BCQBfAAkAXwEJAN8ACQDfAQkAPwAJAD8BCQC/AAkA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SXMgYSBkaXJlY3RvcnkAVGV4dCBmaWxlIGJ1c3kARXhlYyBmb3JtYXQgZXJyb3IASW52YWxpZCBhcmd1bWVudABBcmd1bWVudCBsaXN0IHRvbyBsb25nAFN5bWJvbGljIGxpbmsgbG9vcABGaWxlbmFtZSB0b28gbG9uZwBUb28gbWFueSBvcGVuIGZpbGVzIGluIHN5c3RlbQBObyBmaWxlIGRlc2NyaXB0b3JzIGF2YWlsYWJsZQBCYWQgZmlsZSBkZXNjcmlwdG9yAE5vIGNoaWxkIHByb2Nlc3MAQmFkIGFkZHJlc3MARmlsZSB0b28gbGFyZ2UAVG9vIG1hbnkgbGlua3MATm8gbG9ja3MgYXZhaWxhYmxlAFJlc291cmNlIGRlYWRsb2NrIHdvdWxkIG9jY3VyAFN0YXRlIG5vdCByZWNvdmVyYWJsZQBQcmV2aW91cyBvd25lciBkaWVkAE9wZXJhdGlvbiBjYW5jZWxlZABGdW5jdGlvbiBub3QgaW1wbGVtZW50ZWQATm8gbWVzc2FnZSBvZiBkZXNpcmVkIHR5cGUASWRlbnRpZmllciByZW1vdmVkAERldmljZSBub3QgYSBzdHJlYW0ATm8gZGF0YSBhdmFpbGFibGUARGV2aWNlIHRpbWVvdXQAT3V0IG9mIHN0cmVhbXMgcmVzb3VyY2VzAExpbmsgaGFzIGJlZW4gc2V2ZXJlZABQcm90b2NvbCBlcnJvcgBCYWQgbWVzc2FnZQBGaWxlIGRlc2NyaXB0b3IgaW4gYmFkIHN0YXRlAE5vdCBhIHNvY2tldABEZXN0aW5hdGlvbiBhZGRyZXNzIHJlcXVpcmVkAE1lc3NhZ2UgdG9vIGxhcmdlAFByb3RvY29sIHdyb25nIHR5cGUgZm9yIHNvY2tldABQcm90b2NvbCBub3QgYXZhaWxhYmxlAFByb3RvY29sIG5vdCBzdXBwb3J0ZWQAU29ja2V0IHR5cGUgbm90IHN1cHBvcnRlZABOb3Qgc3VwcG9ydGVkAFByb3RvY29sIGZhbWlseSBub3Qgc3VwcG9ydGVkAEFkZHJlc3MgZmFtaWx5IG5vdCBzdXBwb3J0ZWQgYnkgcHJvdG9jb2wAQWRkcmVzcyBub3QgYXZhaWxhYmxlAE5ldHdvcmsgaXMgZG93bgBOZXR3b3JrIHVucmVhY2hhYmxlAENvbm5lY3Rpb24gcmVzZXQgYnkgbmV0d29yawBDb25uZWN0aW9uIGFib3J0ZWQATm8gYnVmZmVyIHNwYWNlIGF2YWlsYWJsZQBTb2NrZXQgaXMgY29ubmVjdGVkAFNvY2tldCBub3QgY29ubmVjdGVkAENhbm5vdCBzZW5kIGFmdGVyIHNvY2tldCBzaHV0ZG93bgBPcGVyYXRpb24gYWxyZWFkeSBpbiBwcm9ncmVzcwBPcGVyYXRpb24gaW4gcHJvZ3Jlc3MAU3RhbGUgZmlsZSBoYW5kbGUAUmVtb3RlIEkvTyBlcnJvcgBRdW90YSBleGNlZWRlZABObyBtZWRpdW0gZm91bmQAV3JvbmcgbWVkaXVtIHR5cGUATm8gZXJyb3IgaW5mb3JtYXRpb24AQcCAAQuFARMAAAAUAAAAFQAAABYAAAAXAAAAGAAAABkAAAAaAAAAGwAAABwAAAAdAAAAHgAAAB8AAAAgAAAAIQAAACIAAAAjAAAAgERQADEAAAAyAAAAMwAAADQAAAA1AAAANgAAADcAAAA4AAAAOQAAADIAAAAzAAAANAAAADUAAAA2AAAANwAAADgAQfSCAQsCXEQAQbCDAQsQ/////////////////////w==";Bo(ji)||(ji=P(ji));function ro(Je){try{if(Je==ji&&ce)return new Uint8Array(ce);var st=Me(Je);if(st)return st;if(R)return R(Je);throw"sync fetching of the wasm failed: you can preload it to Module['wasmBinary'] manually, or emcc.py will do that for you when generating HTML (but not JS)"}catch(St){ns(St)}}function vo(Je,st){var St,lr,ee;try{ee=ro(Je),lr=new WebAssembly.Module(ee),St=new WebAssembly.Instance(lr,st)}catch(Oe){var Ee=Oe.toString();throw te("failed to compile wasm module: "+Ee),(Ee.includes("imported Memory")||Ee.includes("memory import"))&&te("Memory size incompatibility issues may be due to changing INITIAL_MEMORY at runtime to something too large. Use ALLOW_MEMORY_GROWTH to allow any size memory (and also make sure not to set INITIAL_MEMORY at runtime to something smaller than it was at compile time)."),Oe}return[St,lr]}function RA(){var Je={a:fu};function st(ee,Ee){var Oe=ee.exports;r.asm=Oe,Be=r.asm.g,z(Be.buffer),$=r.asm.W,gn(r.asm.h),wo("wasm-instantiate")}if(ai("wasm-instantiate"),r.instantiateWasm)try{var St=r.instantiateWasm(Je,st);return St}catch(ee){return te("Module.instantiateWasm callback failed with error: "+ee),!1}var lr=vo(ji,Je);return st(lr[0]),r.asm}function pf(Je){return F.getFloat32(Je,!0)}function yh(Je){return F.getFloat64(Je,!0)}function Eh(Je){return F.getInt16(Je,!0)}function no(Je){return F.getInt32(Je,!0)}function jn(Je,st){F.setInt32(Je,st,!0)}function Fs(Je){for(;Je.length>0;){var st=Je.shift();if(typeof st=="function"){st(r);continue}var St=st.func;typeof St=="number"?st.arg===void 0?$.get(St)():$.get(St)(st.arg):St(st.arg===void 0?null:st.arg)}}function io(Je,st){var St=new Date(no((Je>>2)*4)*1e3);jn((st>>2)*4,St.getUTCSeconds()),jn((st+4>>2)*4,St.getUTCMinutes()),jn((st+8>>2)*4,St.getUTCHours()),jn((st+12>>2)*4,St.getUTCDate()),jn((st+16>>2)*4,St.getUTCMonth()),jn((st+20>>2)*4,St.getUTCFullYear()-1900),jn((st+24>>2)*4,St.getUTCDay()),jn((st+36>>2)*4,0),jn((st+32>>2)*4,0);var lr=Date.UTC(St.getUTCFullYear(),0,1,0,0,0,0),ee=(St.getTime()-lr)/(1e3*60*60*24)|0;return jn((st+28>>2)*4,ee),io.GMTString||(io.GMTString=rt("GMT")),jn((st+40>>2)*4,io.GMTString),st}function lu(Je,st){return io(Je,st)}function cu(Je,st,St){ke.copyWithin(Je,st,st+St)}function uu(Je){try{return Be.grow(Je-Pe.byteLength+65535>>>16),z(Be.buffer),1}catch{}}function FA(Je){var st=ke.length;Je=Je>>>0;var St=2147483648;if(Je>St)return!1;for(var lr=1;lr<=4;lr*=2){var ee=st*(1+.2/lr);ee=Math.min(ee,Je+100663296);var Ee=Math.min(St,Ne(Math.max(Je,ee),65536)),Oe=uu(Ee);if(Oe)return!0}return!1}function NA(Je){Ae(Je)}function aa(Je){var st=Date.now()/1e3|0;return Je&&jn((Je>>2)*4,st),st}function la(){if(la.called)return;la.called=!0;var Je=new Date().getFullYear(),st=new Date(Je,0,1),St=new Date(Je,6,1),lr=st.getTimezoneOffset(),ee=St.getTimezoneOffset(),Ee=Math.max(lr,ee);jn((Sl()>>2)*4,Ee*60),jn((ws()>>2)*4,+(lr!=ee));function Oe(fn){var li=fn.toTimeString().match(/\(([A-Za-z ]+)\)$/);return li?li[1]:"GMT"}var gt=Oe(st),yt=Oe(St),Dt=rt(gt),tr=rt(yt);ee>2)*4,Dt),jn((_i()+4>>2)*4,tr)):(jn((_i()>>2)*4,tr),jn((_i()+4>>2)*4,Dt))}function OA(Je){la();var st=Date.UTC(no((Je+20>>2)*4)+1900,no((Je+16>>2)*4),no((Je+12>>2)*4),no((Je+8>>2)*4),no((Je+4>>2)*4),no((Je>>2)*4),0),St=new Date(st);jn((Je+24>>2)*4,St.getUTCDay());var lr=Date.UTC(St.getUTCFullYear(),0,1,0,0,0,0),ee=(St.getTime()-lr)/(1e3*60*60*24)|0;return jn((Je+28>>2)*4,ee),St.getTime()/1e3|0}var gr=typeof atob=="function"?atob:function(Je){var st="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",St="",lr,ee,Ee,Oe,gt,yt,Dt,tr=0;Je=Je.replace(/[^A-Za-z0-9\+\/\=]/g,"");do Oe=st.indexOf(Je.charAt(tr++)),gt=st.indexOf(Je.charAt(tr++)),yt=st.indexOf(Je.charAt(tr++)),Dt=st.indexOf(Je.charAt(tr++)),lr=Oe<<2|gt>>4,ee=(gt&15)<<4|yt>>2,Ee=(yt&3)<<6|Dt,St=St+String.fromCharCode(lr),yt!==64&&(St=St+String.fromCharCode(ee)),Dt!==64&&(St=St+String.fromCharCode(Ee));while(tr0||(It(),Ir>0))return;function st(){Qn||(Qn=!0,r.calledRun=!0,!Ce&&(qt(),s(r),r.onRuntimeInitialized&&r.onRuntimeInitialized(),ir()))}r.setStatus?(r.setStatus("Running..."),setTimeout(function(){setTimeout(function(){r.setStatus("")},1),st()},1)):st()}if(r.run=pc,r.preInit)for(typeof r.preInit=="function"&&(r.preInit=[r.preInit]);r.preInit.length>0;)r.preInit.pop()();return pc(),e}}();typeof IT=="object"&&typeof jj=="object"?jj.exports=Hj:typeof define=="function"&&define.amd?define([],function(){return Hj}):typeof IT=="object"&&(IT.createModule=Hj)});var Hp,kde,Qde,Tde=Ct(()=>{Hp=["number","number"],kde=(X=>(X[X.ZIP_ER_OK=0]="ZIP_ER_OK",X[X.ZIP_ER_MULTIDISK=1]="ZIP_ER_MULTIDISK",X[X.ZIP_ER_RENAME=2]="ZIP_ER_RENAME",X[X.ZIP_ER_CLOSE=3]="ZIP_ER_CLOSE",X[X.ZIP_ER_SEEK=4]="ZIP_ER_SEEK",X[X.ZIP_ER_READ=5]="ZIP_ER_READ",X[X.ZIP_ER_WRITE=6]="ZIP_ER_WRITE",X[X.ZIP_ER_CRC=7]="ZIP_ER_CRC",X[X.ZIP_ER_ZIPCLOSED=8]="ZIP_ER_ZIPCLOSED",X[X.ZIP_ER_NOENT=9]="ZIP_ER_NOENT",X[X.ZIP_ER_EXISTS=10]="ZIP_ER_EXISTS",X[X.ZIP_ER_OPEN=11]="ZIP_ER_OPEN",X[X.ZIP_ER_TMPOPEN=12]="ZIP_ER_TMPOPEN",X[X.ZIP_ER_ZLIB=13]="ZIP_ER_ZLIB",X[X.ZIP_ER_MEMORY=14]="ZIP_ER_MEMORY",X[X.ZIP_ER_CHANGED=15]="ZIP_ER_CHANGED",X[X.ZIP_ER_COMPNOTSUPP=16]="ZIP_ER_COMPNOTSUPP",X[X.ZIP_ER_EOF=17]="ZIP_ER_EOF",X[X.ZIP_ER_INVAL=18]="ZIP_ER_INVAL",X[X.ZIP_ER_NOZIP=19]="ZIP_ER_NOZIP",X[X.ZIP_ER_INTERNAL=20]="ZIP_ER_INTERNAL",X[X.ZIP_ER_INCONS=21]="ZIP_ER_INCONS",X[X.ZIP_ER_REMOVE=22]="ZIP_ER_REMOVE",X[X.ZIP_ER_DELETED=23]="ZIP_ER_DELETED",X[X.ZIP_ER_ENCRNOTSUPP=24]="ZIP_ER_ENCRNOTSUPP",X[X.ZIP_ER_RDONLY=25]="ZIP_ER_RDONLY",X[X.ZIP_ER_NOPASSWD=26]="ZIP_ER_NOPASSWD",X[X.ZIP_ER_WRONGPASSWD=27]="ZIP_ER_WRONGPASSWD",X[X.ZIP_ER_OPNOTSUPP=28]="ZIP_ER_OPNOTSUPP",X[X.ZIP_ER_INUSE=29]="ZIP_ER_INUSE",X[X.ZIP_ER_TELL=30]="ZIP_ER_TELL",X[X.ZIP_ER_COMPRESSED_DATA=31]="ZIP_ER_COMPRESSED_DATA",X))(kde||{}),Qde=t=>({get HEAPU8(){return t.HEAPU8},errors:kde,SEEK_SET:0,SEEK_CUR:1,SEEK_END:2,ZIP_CHECKCONS:4,ZIP_EXCL:2,ZIP_RDONLY:16,ZIP_FL_OVERWRITE:8192,ZIP_FL_COMPRESSED:4,ZIP_OPSYS_DOS:0,ZIP_OPSYS_AMIGA:1,ZIP_OPSYS_OPENVMS:2,ZIP_OPSYS_UNIX:3,ZIP_OPSYS_VM_CMS:4,ZIP_OPSYS_ATARI_ST:5,ZIP_OPSYS_OS_2:6,ZIP_OPSYS_MACINTOSH:7,ZIP_OPSYS_Z_SYSTEM:8,ZIP_OPSYS_CPM:9,ZIP_OPSYS_WINDOWS_NTFS:10,ZIP_OPSYS_MVS:11,ZIP_OPSYS_VSE:12,ZIP_OPSYS_ACORN_RISC:13,ZIP_OPSYS_VFAT:14,ZIP_OPSYS_ALTERNATE_MVS:15,ZIP_OPSYS_BEOS:16,ZIP_OPSYS_TANDEM:17,ZIP_OPSYS_OS_400:18,ZIP_OPSYS_OS_X:19,ZIP_CM_DEFAULT:-1,ZIP_CM_STORE:0,ZIP_CM_DEFLATE:8,uint08S:t._malloc(1),uint32S:t._malloc(4),malloc:t._malloc,free:t._free,getValue:t.getValue,openFromSource:t.cwrap("zip_open_from_source","number",["number","number","number"]),close:t.cwrap("zip_close","number",["number"]),discard:t.cwrap("zip_discard",null,["number"]),getError:t.cwrap("zip_get_error","number",["number"]),getName:t.cwrap("zip_get_name","string",["number","number","number"]),getNumEntries:t.cwrap("zip_get_num_entries","number",["number","number"]),delete:t.cwrap("zip_delete","number",["number","number"]),statIndex:t.cwrap("zip_stat_index","number",["number",...Hp,"number","number"]),fopenIndex:t.cwrap("zip_fopen_index","number",["number",...Hp,"number"]),fread:t.cwrap("zip_fread","number",["number","number","number","number"]),fclose:t.cwrap("zip_fclose","number",["number"]),dir:{add:t.cwrap("zip_dir_add","number",["number","string"])},file:{add:t.cwrap("zip_file_add","number",["number","string","number","number"]),getError:t.cwrap("zip_file_get_error","number",["number"]),getExternalAttributes:t.cwrap("zip_file_get_external_attributes","number",["number",...Hp,"number","number","number"]),setExternalAttributes:t.cwrap("zip_file_set_external_attributes","number",["number",...Hp,"number","number","number"]),setMtime:t.cwrap("zip_file_set_mtime","number",["number",...Hp,"number","number"]),setCompression:t.cwrap("zip_set_file_compression","number",["number",...Hp,"number","number"])},ext:{countSymlinks:t.cwrap("zip_ext_count_symlinks","number",["number"])},error:{initWithCode:t.cwrap("zip_error_init_with_code",null,["number","number"]),strerror:t.cwrap("zip_error_strerror","string",["number"])},name:{locate:t.cwrap("zip_name_locate","number",["number","string","number"])},source:{fromUnattachedBuffer:t.cwrap("zip_source_buffer_create","number",["number",...Hp,"number","number"]),fromBuffer:t.cwrap("zip_source_buffer","number",["number","number",...Hp,"number"]),free:t.cwrap("zip_source_free",null,["number"]),keep:t.cwrap("zip_source_keep",null,["number"]),open:t.cwrap("zip_source_open","number",["number"]),close:t.cwrap("zip_source_close","number",["number"]),seek:t.cwrap("zip_source_seek","number",["number",...Hp,"number"]),tell:t.cwrap("zip_source_tell","number",["number"]),read:t.cwrap("zip_source_read","number",["number","number","number"]),error:t.cwrap("zip_source_error","number",["number"])},struct:{statS:t.cwrap("zipstruct_statS","number",[]),statSize:t.cwrap("zipstruct_stat_size","number",["number"]),statCompSize:t.cwrap("zipstruct_stat_comp_size","number",["number"]),statCompMethod:t.cwrap("zipstruct_stat_comp_method","number",["number"]),statMtime:t.cwrap("zipstruct_stat_mtime","number",["number"]),statCrc:t.cwrap("zipstruct_stat_crc","number",["number"]),errorS:t.cwrap("zipstruct_errorS","number",[]),errorCodeZip:t.cwrap("zipstruct_error_code_zip","number",["number"])}})});function qj(t,e){let r=t.indexOf(e);if(r<=0)return null;let s=r;for(;r>=0&&(s=r+e.length,t[s]!==K.sep);){if(t[r-1]===K.sep)return null;r=t.indexOf(e,s)}return t.length>s&&t[s]!==K.sep?null:t.slice(0,s)}var tA,Rde=Ct(()=>{bt();bt();rA();tA=class t extends r0{static async openPromise(e,r){let s=new t(r);try{return await e(s)}finally{s.saveAndClose()}}constructor(e={}){let r=e.fileExtensions,s=e.readOnlyArchives,a=typeof r>"u"?f=>qj(f,".zip"):f=>{for(let p of r){let h=qj(f,p);if(h)return h}return null},n=(f,p)=>new hs(p,{baseFs:f,readOnly:s,stats:f.statSync(p),customZipImplementation:e.customZipImplementation}),c=async(f,p)=>{let h={baseFs:f,readOnly:s,stats:await f.statPromise(p),customZipImplementation:e.customZipImplementation};return()=>new hs(p,h)};super({...e,factorySync:n,factoryPromise:c,getMountPoint:a})}}});var Gj,wI,Wj=Ct(()=>{Uj();Gj=class extends Error{constructor(e,r){super(e),this.name="Libzip Error",this.code=r}},wI=class{constructor(e){this.filesShouldBeCached=!0;let r="buffer"in e?e.buffer:e.baseFs.readFileSync(e.path);this.libzip=yv();let s=this.libzip.malloc(4);try{let c=0;e.readOnly&&(c|=this.libzip.ZIP_RDONLY);let f=this.allocateUnattachedSource(r);try{this.zip=this.libzip.openFromSource(f,c,s),this.lzSource=f}catch(p){throw this.libzip.source.free(f),p}if(this.zip===0){let p=this.libzip.struct.errorS();throw this.libzip.error.initWithCode(p,this.libzip.getValue(s,"i32")),this.makeLibzipError(p)}}finally{this.libzip.free(s)}let a=this.libzip.getNumEntries(this.zip,0),n=new Array(a);for(let c=0;c>>0,n=this.libzip.struct.statMtime(r)>>>0,c=this.libzip.struct.statCrc(r)>>>0;return{size:a,mtime:n,crc:c}}makeLibzipError(e){let r=this.libzip.struct.errorCodeZip(e),s=this.libzip.error.strerror(e),a=new Gj(s,this.libzip.errors[r]);if(r===this.libzip.errors.ZIP_ER_CHANGED)throw new Error(`Assertion failed: Unexpected libzip error: ${a.message}`);return a}setFileSource(e,r,s){let a=this.allocateSource(s);try{let n=this.libzip.file.add(this.zip,e,a,this.libzip.ZIP_FL_OVERWRITE);if(n===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));if(r!==null&&this.libzip.file.setCompression(this.zip,n,0,r[0],r[1])===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));return n}catch(n){throw this.libzip.source.free(a),n}}setMtime(e,r){if(this.libzip.file.setMtime(this.zip,e,0,r,0)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}getExternalAttributes(e){if(this.libzip.file.getExternalAttributes(this.zip,e,0,0,this.libzip.uint08S,this.libzip.uint32S)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));let s=this.libzip.getValue(this.libzip.uint08S,"i8")>>>0,a=this.libzip.getValue(this.libzip.uint32S,"i32")>>>0;return[s,a]}setExternalAttributes(e,r,s){if(this.libzip.file.setExternalAttributes(this.zip,e,0,0,r,s)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}locate(e){return this.libzip.name.locate(this.zip,e,0)}getFileSource(e){let r=this.libzip.struct.statS();if(this.libzip.statIndex(this.zip,e,0,0,r)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));let a=this.libzip.struct.statCompSize(r),n=this.libzip.struct.statCompMethod(r),c=this.libzip.malloc(a);try{let f=this.libzip.fopenIndex(this.zip,e,0,this.libzip.ZIP_FL_COMPRESSED);if(f===0)throw this.makeLibzipError(this.libzip.getError(this.zip));try{let p=this.libzip.fread(f,c,a,0);if(p===-1)throw this.makeLibzipError(this.libzip.file.getError(f));if(pa)throw new Error("Overread");let h=this.libzip.HEAPU8.subarray(c,c+a);return{data:Buffer.from(h),compressionMethod:n}}finally{this.libzip.fclose(f)}}finally{this.libzip.free(c)}}deleteEntry(e){if(this.libzip.delete(this.zip,e)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}addDirectory(e){let r=this.libzip.dir.add(this.zip,e);if(r===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));return r}getBufferAndClose(){try{if(this.libzip.source.keep(this.lzSource),this.libzip.close(this.zip)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));if(this.libzip.source.open(this.lzSource)===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));if(this.libzip.source.seek(this.lzSource,0,0,this.libzip.SEEK_END)===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));let e=this.libzip.source.tell(this.lzSource);if(e===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));if(this.libzip.source.seek(this.lzSource,0,0,this.libzip.SEEK_SET)===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));let r=this.libzip.malloc(e);if(!r)throw new Error("Couldn't allocate enough memory");try{let s=this.libzip.source.read(this.lzSource,r,e);if(s===-1)throw this.makeLibzipError(this.libzip.source.error(this.lzSource));if(se)throw new Error("Overread");let a=Buffer.from(this.libzip.HEAPU8.subarray(r,r+e));return process.env.YARN_IS_TEST_ENV&&process.env.YARN_ZIP_DATA_EPILOGUE&&(a=Buffer.concat([a,Buffer.from(process.env.YARN_ZIP_DATA_EPILOGUE)])),a}finally{this.libzip.free(r)}}finally{this.libzip.source.close(this.lzSource),this.libzip.source.free(this.lzSource)}}allocateBuffer(e){Buffer.isBuffer(e)||(e=Buffer.from(e));let r=this.libzip.malloc(e.byteLength);if(!r)throw new Error("Couldn't allocate enough memory");return new Uint8Array(this.libzip.HEAPU8.buffer,r,e.byteLength).set(e),{buffer:r,byteLength:e.byteLength}}allocateUnattachedSource(e){let r=this.libzip.struct.errorS(),{buffer:s,byteLength:a}=this.allocateBuffer(e),n=this.libzip.source.fromUnattachedBuffer(s,a,0,1,r);if(n===0)throw this.libzip.free(r),this.makeLibzipError(r);return n}allocateSource(e){let{buffer:r,byteLength:s}=this.allocateBuffer(e),a=this.libzip.source.fromBuffer(this.zip,r,s,0,1);if(a===0)throw this.libzip.free(r),this.makeLibzipError(this.libzip.getError(this.zip));return a}discard(){this.libzip.discard(this.zip)}}});function Wdt(t){if(typeof t=="string"&&String(+t)===t)return+t;if(typeof t=="number"&&Number.isFinite(t))return t<0?Date.now()/1e3:t;if(Fde.types.isDate(t))return t.getTime()/1e3;throw new Error("Invalid time")}function CT(){return Buffer.from([80,75,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0])}var ka,Yj,Fde,Vj,om,Kj,Jj,Nde,hs,wT=Ct(()=>{bt();bt();bt();bt();bt();bt();ka=Ie("fs"),Yj=Ie("stream"),Fde=Ie("util"),Vj=et(Ie("zlib"));Wj();om=3,Kj=0,Jj=8,Nde="mixed";hs=class extends Uf{constructor(r,s={}){super();this.listings=new Map;this.entries=new Map;this.fileSources=new Map;this.fds=new Map;this.nextFd=0;this.ready=!1;this.readOnly=!1;s.readOnly&&(this.readOnly=!0);let a=s;this.level=typeof a.level<"u"?a.level:Nde;let n=s.customZipImplementation??wI;if(typeof r=="string"){let{baseFs:f=new Yn}=a;this.baseFs=f,this.path=r}else this.path=null,this.baseFs=null;if(s.stats)this.stats=s.stats;else if(typeof r=="string")try{this.stats=this.baseFs.statSync(r)}catch(f){if(f.code==="ENOENT"&&a.create)this.stats=el.makeDefaultStats();else throw f}else this.stats=el.makeDefaultStats();typeof r=="string"?s.create?this.zipImpl=new n({buffer:CT(),readOnly:this.readOnly}):this.zipImpl=new n({path:r,baseFs:this.baseFs,readOnly:this.readOnly,size:this.stats.size}):this.zipImpl=new n({buffer:r??CT(),readOnly:this.readOnly}),this.listings.set(vt.root,new Set);let c=this.zipImpl.getListings();for(let f=0;f{this.closeSync(f)}})}async readPromise(r,s,a,n,c){return this.readSync(r,s,a,n,c)}readSync(r,s,a=0,n=s.byteLength,c=-1){let f=this.fds.get(r);if(typeof f>"u")throw or.EBADF("read");let p=c===-1||c===null?f.cursor:c,h=this.readFileSync(f.p);h.copy(s,a,p,p+n);let E=Math.max(0,Math.min(h.length-p,n));return(c===-1||c===null)&&(f.cursor+=E),E}async writePromise(r,s,a,n,c){return typeof s=="string"?this.writeSync(r,s,c):this.writeSync(r,s,a,n,c)}writeSync(r,s,a,n,c){throw typeof this.fds.get(r)>"u"?or.EBADF("read"):new Error("Unimplemented")}async closePromise(r){return this.closeSync(r)}closeSync(r){if(typeof this.fds.get(r)>"u")throw or.EBADF("read");this.fds.delete(r)}createReadStream(r,{encoding:s}={}){if(r===null)throw new Error("Unimplemented");let a=this.openSync(r,"r"),n=Object.assign(new Yj.PassThrough({emitClose:!0,autoDestroy:!0,destroy:(f,p)=>{clearImmediate(c),this.closeSync(a),p(f)}}),{close(){n.destroy()},bytesRead:0,path:r,pending:!1}),c=setImmediate(async()=>{try{let f=await this.readFilePromise(r,s);n.bytesRead=f.length,n.end(f)}catch(f){n.destroy(f)}});return n}createWriteStream(r,{encoding:s}={}){if(this.readOnly)throw or.EROFS(`open '${r}'`);if(r===null)throw new Error("Unimplemented");let a=[],n=this.openSync(r,"w"),c=Object.assign(new Yj.PassThrough({autoDestroy:!0,emitClose:!0,destroy:(f,p)=>{try{f?p(f):(this.writeFileSync(r,Buffer.concat(a),s),p(null))}catch(h){p(h)}finally{this.closeSync(n)}}}),{close(){c.destroy()},bytesWritten:0,path:r,pending:!1});return c.on("data",f=>{let p=Buffer.from(f);c.bytesWritten+=p.length,a.push(p)}),c}async realpathPromise(r){return this.realpathSync(r)}realpathSync(r){let s=this.resolveFilename(`lstat '${r}'`,r);if(!this.entries.has(s)&&!this.listings.has(s))throw or.ENOENT(`lstat '${r}'`);return s}async existsPromise(r){return this.existsSync(r)}existsSync(r){if(!this.ready)throw or.EBUSY(`archive closed, existsSync '${r}'`);if(this.symlinkCount===0){let a=K.resolve(vt.root,r);return this.entries.has(a)||this.listings.has(a)}let s;try{s=this.resolveFilename(`stat '${r}'`,r,void 0,!1)}catch{return!1}return s===void 0?!1:this.entries.has(s)||this.listings.has(s)}async accessPromise(r,s){return this.accessSync(r,s)}accessSync(r,s=ka.constants.F_OK){let a=this.resolveFilename(`access '${r}'`,r);if(!this.entries.has(a)&&!this.listings.has(a))throw or.ENOENT(`access '${r}'`);if(this.readOnly&&s&ka.constants.W_OK)throw or.EROFS(`access '${r}'`)}async statPromise(r,s={bigint:!1}){return s.bigint?this.statSync(r,{bigint:!0}):this.statSync(r)}statSync(r,s={bigint:!1,throwIfNoEntry:!0}){let a=this.resolveFilename(`stat '${r}'`,r,void 0,s.throwIfNoEntry);if(a!==void 0){if(!this.entries.has(a)&&!this.listings.has(a)){if(s.throwIfNoEntry===!1)return;throw or.ENOENT(`stat '${r}'`)}if(r[r.length-1]==="/"&&!this.listings.has(a))throw or.ENOTDIR(`stat '${r}'`);return this.statImpl(`stat '${r}'`,a,s)}}async fstatPromise(r,s){return this.fstatSync(r,s)}fstatSync(r,s){let a=this.fds.get(r);if(typeof a>"u")throw or.EBADF("fstatSync");let{p:n}=a,c=this.resolveFilename(`stat '${n}'`,n);if(!this.entries.has(c)&&!this.listings.has(c))throw or.ENOENT(`stat '${n}'`);if(n[n.length-1]==="/"&&!this.listings.has(c))throw or.ENOTDIR(`stat '${n}'`);return this.statImpl(`fstat '${n}'`,c,s)}async lstatPromise(r,s={bigint:!1}){return s.bigint?this.lstatSync(r,{bigint:!0}):this.lstatSync(r)}lstatSync(r,s={bigint:!1,throwIfNoEntry:!0}){let a=this.resolveFilename(`lstat '${r}'`,r,!1,s.throwIfNoEntry);if(a!==void 0){if(!this.entries.has(a)&&!this.listings.has(a)){if(s.throwIfNoEntry===!1)return;throw or.ENOENT(`lstat '${r}'`)}if(r[r.length-1]==="/"&&!this.listings.has(a))throw or.ENOTDIR(`lstat '${r}'`);return this.statImpl(`lstat '${r}'`,a,s)}}statImpl(r,s,a={}){let n=this.entries.get(s);if(typeof n<"u"){let c=this.zipImpl.stat(n),f=c.crc,p=c.size,h=c.mtime*1e3,E=this.stats.uid,C=this.stats.gid,S=512,P=Math.ceil(c.size/S),I=h,R=h,N=h,U=new Date(I),W=new Date(R),te=new Date(N),ie=new Date(h),Ae=this.listings.has(s)?ka.constants.S_IFDIR:this.isSymbolicLink(n)?ka.constants.S_IFLNK:ka.constants.S_IFREG,ce=Ae===ka.constants.S_IFDIR?493:420,me=Ae|this.getUnixMode(n,ce)&511,pe=Object.assign(new el.StatEntry,{uid:E,gid:C,size:p,blksize:S,blocks:P,atime:U,birthtime:W,ctime:te,mtime:ie,atimeMs:I,birthtimeMs:R,ctimeMs:N,mtimeMs:h,mode:me,crc:f});return a.bigint===!0?el.convertToBigIntStats(pe):pe}if(this.listings.has(s)){let c=this.stats.uid,f=this.stats.gid,p=0,h=512,E=0,C=this.stats.mtimeMs,S=this.stats.mtimeMs,P=this.stats.mtimeMs,I=this.stats.mtimeMs,R=new Date(C),N=new Date(S),U=new Date(P),W=new Date(I),te=ka.constants.S_IFDIR|493,Ae=Object.assign(new el.StatEntry,{uid:c,gid:f,size:p,blksize:h,blocks:E,atime:R,birthtime:N,ctime:U,mtime:W,atimeMs:C,birthtimeMs:S,ctimeMs:P,mtimeMs:I,mode:te,crc:0});return a.bigint===!0?el.convertToBigIntStats(Ae):Ae}throw new Error("Unreachable")}getUnixMode(r,s){let[a,n]=this.zipImpl.getExternalAttributes(r);return a!==om?s:n>>>16}registerListing(r){let s=this.listings.get(r);if(s)return s;this.registerListing(K.dirname(r)).add(K.basename(r));let n=new Set;return this.listings.set(r,n),n}registerEntry(r,s){this.registerListing(K.dirname(r)).add(K.basename(r)),this.entries.set(r,s)}unregisterListing(r){this.listings.delete(r),this.listings.get(K.dirname(r))?.delete(K.basename(r))}unregisterEntry(r){this.unregisterListing(r);let s=this.entries.get(r);this.entries.delete(r),!(typeof s>"u")&&(this.fileSources.delete(s),this.isSymbolicLink(s)&&this.symlinkCount--)}deleteEntry(r,s){this.unregisterEntry(r),this.zipImpl.deleteEntry(s)}resolveFilename(r,s,a=!0,n=!0){if(!this.ready)throw or.EBUSY(`archive closed, ${r}`);let c=K.resolve(vt.root,s);if(c==="/")return vt.root;let f=this.entries.get(c);if(a&&f!==void 0)if(this.symlinkCount!==0&&this.isSymbolicLink(f)){let p=this.getFileSource(f).toString();return this.resolveFilename(r,K.resolve(K.dirname(c),p),!0,n)}else return c;for(;;){let p=this.resolveFilename(r,K.dirname(c),!0,n);if(p===void 0)return p;let h=this.listings.has(p),E=this.entries.has(p);if(!h&&!E){if(n===!1)return;throw or.ENOENT(r)}if(!h)throw or.ENOTDIR(r);if(c=K.resolve(p,K.basename(c)),!a||this.symlinkCount===0)break;let C=this.zipImpl.locate(c.slice(1));if(C===-1)break;if(this.isSymbolicLink(C)){let S=this.getFileSource(C).toString();c=K.resolve(K.dirname(c),S)}else break}return c}setFileSource(r,s){let a=Buffer.isBuffer(s)?s:Buffer.from(s),n=K.relative(vt.root,r),c=null;this.level!=="mixed"&&(c=[this.level===0?Kj:Jj,this.level]);let f=this.zipImpl.setFileSource(n,c,a);return this.fileSources.set(f,a),f}isSymbolicLink(r){if(this.symlinkCount===0)return!1;let[s,a]=this.zipImpl.getExternalAttributes(r);return s!==om?!1:(a>>>16&ka.constants.S_IFMT)===ka.constants.S_IFLNK}getFileSource(r,s={asyncDecompress:!1}){let a=this.fileSources.get(r);if(typeof a<"u")return a;let{data:n,compressionMethod:c}=this.zipImpl.getFileSource(r);if(c===Kj)return this.zipImpl.filesShouldBeCached&&this.fileSources.set(r,n),n;if(c===Jj){if(s.asyncDecompress)return new Promise((f,p)=>{Vj.default.inflateRaw(n,(h,E)=>{h?p(h):(this.zipImpl.filesShouldBeCached&&this.fileSources.set(r,E),f(E))})});{let f=Vj.default.inflateRawSync(n);return this.zipImpl.filesShouldBeCached&&this.fileSources.set(r,f),f}}else throw new Error(`Unsupported compression method: ${c}`)}async fchmodPromise(r,s){return this.chmodPromise(this.fdToPath(r,"fchmod"),s)}fchmodSync(r,s){return this.chmodSync(this.fdToPath(r,"fchmodSync"),s)}async chmodPromise(r,s){return this.chmodSync(r,s)}chmodSync(r,s){if(this.readOnly)throw or.EROFS(`chmod '${r}'`);s&=493;let a=this.resolveFilename(`chmod '${r}'`,r,!1),n=this.entries.get(a);if(typeof n>"u")throw new Error(`Assertion failed: The entry should have been registered (${a})`);let f=this.getUnixMode(n,ka.constants.S_IFREG|0)&-512|s;this.zipImpl.setExternalAttributes(n,om,f<<16)}async fchownPromise(r,s,a){return this.chownPromise(this.fdToPath(r,"fchown"),s,a)}fchownSync(r,s,a){return this.chownSync(this.fdToPath(r,"fchownSync"),s,a)}async chownPromise(r,s,a){return this.chownSync(r,s,a)}chownSync(r,s,a){throw new Error("Unimplemented")}async renamePromise(r,s){return this.renameSync(r,s)}renameSync(r,s){throw new Error("Unimplemented")}async copyFilePromise(r,s,a){let{indexSource:n,indexDest:c,resolvedDestP:f}=this.prepareCopyFile(r,s,a),p=await this.getFileSource(n,{asyncDecompress:!0}),h=this.setFileSource(f,p);h!==c&&this.registerEntry(f,h)}copyFileSync(r,s,a=0){let{indexSource:n,indexDest:c,resolvedDestP:f}=this.prepareCopyFile(r,s,a),p=this.getFileSource(n),h=this.setFileSource(f,p);h!==c&&this.registerEntry(f,h)}prepareCopyFile(r,s,a=0){if(this.readOnly)throw or.EROFS(`copyfile '${r} -> '${s}'`);if(a&ka.constants.COPYFILE_FICLONE_FORCE)throw or.ENOSYS("unsupported clone operation",`copyfile '${r}' -> ${s}'`);let n=this.resolveFilename(`copyfile '${r} -> ${s}'`,r),c=this.entries.get(n);if(typeof c>"u")throw or.EINVAL(`copyfile '${r}' -> '${s}'`);let f=this.resolveFilename(`copyfile '${r}' -> ${s}'`,s),p=this.entries.get(f);if(a&(ka.constants.COPYFILE_EXCL|ka.constants.COPYFILE_FICLONE_FORCE)&&typeof p<"u")throw or.EEXIST(`copyfile '${r}' -> '${s}'`);return{indexSource:c,resolvedDestP:f,indexDest:p}}async appendFilePromise(r,s,a){if(this.readOnly)throw or.EROFS(`open '${r}'`);return typeof a>"u"?a={flag:"a"}:typeof a=="string"?a={flag:"a",encoding:a}:typeof a.flag>"u"&&(a={flag:"a",...a}),this.writeFilePromise(r,s,a)}appendFileSync(r,s,a={}){if(this.readOnly)throw or.EROFS(`open '${r}'`);return typeof a>"u"?a={flag:"a"}:typeof a=="string"?a={flag:"a",encoding:a}:typeof a.flag>"u"&&(a={flag:"a",...a}),this.writeFileSync(r,s,a)}fdToPath(r,s){let a=this.fds.get(r)?.p;if(typeof a>"u")throw or.EBADF(s);return a}async writeFilePromise(r,s,a){let{encoding:n,mode:c,index:f,resolvedP:p}=this.prepareWriteFile(r,a);f!==void 0&&typeof a=="object"&&a.flag&&a.flag.includes("a")&&(s=Buffer.concat([await this.getFileSource(f,{asyncDecompress:!0}),Buffer.from(s)])),n!==null&&(s=s.toString(n));let h=this.setFileSource(p,s);h!==f&&this.registerEntry(p,h),c!==null&&await this.chmodPromise(p,c)}writeFileSync(r,s,a){let{encoding:n,mode:c,index:f,resolvedP:p}=this.prepareWriteFile(r,a);f!==void 0&&typeof a=="object"&&a.flag&&a.flag.includes("a")&&(s=Buffer.concat([this.getFileSource(f),Buffer.from(s)])),n!==null&&(s=s.toString(n));let h=this.setFileSource(p,s);h!==f&&this.registerEntry(p,h),c!==null&&this.chmodSync(p,c)}prepareWriteFile(r,s){if(typeof r=="number"&&(r=this.fdToPath(r,"read")),this.readOnly)throw or.EROFS(`open '${r}'`);let a=this.resolveFilename(`open '${r}'`,r);if(this.listings.has(a))throw or.EISDIR(`open '${r}'`);let n=null,c=null;typeof s=="string"?n=s:typeof s=="object"&&({encoding:n=null,mode:c=null}=s);let f=this.entries.get(a);return{encoding:n,mode:c,resolvedP:a,index:f}}async unlinkPromise(r){return this.unlinkSync(r)}unlinkSync(r){if(this.readOnly)throw or.EROFS(`unlink '${r}'`);let s=this.resolveFilename(`unlink '${r}'`,r);if(this.listings.has(s))throw or.EISDIR(`unlink '${r}'`);let a=this.entries.get(s);if(typeof a>"u")throw or.EINVAL(`unlink '${r}'`);this.deleteEntry(s,a)}async utimesPromise(r,s,a){return this.utimesSync(r,s,a)}utimesSync(r,s,a){if(this.readOnly)throw or.EROFS(`utimes '${r}'`);let n=this.resolveFilename(`utimes '${r}'`,r);this.utimesImpl(n,a)}async lutimesPromise(r,s,a){return this.lutimesSync(r,s,a)}lutimesSync(r,s,a){if(this.readOnly)throw or.EROFS(`lutimes '${r}'`);let n=this.resolveFilename(`utimes '${r}'`,r,!1);this.utimesImpl(n,a)}utimesImpl(r,s){this.listings.has(r)&&(this.entries.has(r)||this.hydrateDirectory(r));let a=this.entries.get(r);if(a===void 0)throw new Error("Unreachable");this.zipImpl.setMtime(a,Wdt(s))}async mkdirPromise(r,s){return this.mkdirSync(r,s)}mkdirSync(r,{mode:s=493,recursive:a=!1}={}){if(a)return this.mkdirpSync(r,{chmod:s});if(this.readOnly)throw or.EROFS(`mkdir '${r}'`);let n=this.resolveFilename(`mkdir '${r}'`,r);if(this.entries.has(n)||this.listings.has(n))throw or.EEXIST(`mkdir '${r}'`);this.hydrateDirectory(n),this.chmodSync(n,s)}async rmdirPromise(r,s){return this.rmdirSync(r,s)}rmdirSync(r,{recursive:s=!1}={}){if(this.readOnly)throw or.EROFS(`rmdir '${r}'`);if(s){this.removeSync(r);return}let a=this.resolveFilename(`rmdir '${r}'`,r),n=this.listings.get(a);if(!n)throw or.ENOTDIR(`rmdir '${r}'`);if(n.size>0)throw or.ENOTEMPTY(`rmdir '${r}'`);let c=this.entries.get(a);if(typeof c>"u")throw or.EINVAL(`rmdir '${r}'`);this.deleteEntry(r,c)}async rmPromise(r,s){return this.rmSync(r,s)}rmSync(r,{recursive:s=!1}={}){if(this.readOnly)throw or.EROFS(`rm '${r}'`);if(s){this.removeSync(r);return}let a=this.resolveFilename(`rm '${r}'`,r),n=this.listings.get(a);if(!n)throw or.ENOTDIR(`rm '${r}'`);if(n.size>0)throw or.ENOTEMPTY(`rm '${r}'`);let c=this.entries.get(a);if(typeof c>"u")throw or.EINVAL(`rm '${r}'`);this.deleteEntry(r,c)}hydrateDirectory(r){let s=this.zipImpl.addDirectory(K.relative(vt.root,r));return this.registerListing(r),this.registerEntry(r,s),s}async linkPromise(r,s){return this.linkSync(r,s)}linkSync(r,s){throw or.EOPNOTSUPP(`link '${r}' -> '${s}'`)}async symlinkPromise(r,s){return this.symlinkSync(r,s)}symlinkSync(r,s){if(this.readOnly)throw or.EROFS(`symlink '${r}' -> '${s}'`);let a=this.resolveFilename(`symlink '${r}' -> '${s}'`,s);if(this.listings.has(a))throw or.EISDIR(`symlink '${r}' -> '${s}'`);if(this.entries.has(a))throw or.EEXIST(`symlink '${r}' -> '${s}'`);let n=this.setFileSource(a,r);this.registerEntry(a,n),this.zipImpl.setExternalAttributes(n,om,(ka.constants.S_IFLNK|511)<<16),this.symlinkCount+=1}async readFilePromise(r,s){typeof s=="object"&&(s=s?s.encoding:void 0);let a=await this.readFileBuffer(r,{asyncDecompress:!0});return s?a.toString(s):a}readFileSync(r,s){typeof s=="object"&&(s=s?s.encoding:void 0);let a=this.readFileBuffer(r);return s?a.toString(s):a}readFileBuffer(r,s={asyncDecompress:!1}){typeof r=="number"&&(r=this.fdToPath(r,"read"));let a=this.resolveFilename(`open '${r}'`,r);if(!this.entries.has(a)&&!this.listings.has(a))throw or.ENOENT(`open '${r}'`);if(r[r.length-1]==="/"&&!this.listings.has(a))throw or.ENOTDIR(`open '${r}'`);if(this.listings.has(a))throw or.EISDIR("read");let n=this.entries.get(a);if(n===void 0)throw new Error("Unreachable");return this.getFileSource(n,s)}async readdirPromise(r,s){return this.readdirSync(r,s)}readdirSync(r,s){let a=this.resolveFilename(`scandir '${r}'`,r);if(!this.entries.has(a)&&!this.listings.has(a))throw or.ENOENT(`scandir '${r}'`);let n=this.listings.get(a);if(!n)throw or.ENOTDIR(`scandir '${r}'`);if(s?.recursive)if(s?.withFileTypes){let c=Array.from(n,f=>Object.assign(this.statImpl("lstat",K.join(r,f)),{name:f,path:vt.dot}));for(let f of c){if(!f.isDirectory())continue;let p=K.join(f.path,f.name),h=this.listings.get(K.join(a,p));for(let E of h)c.push(Object.assign(this.statImpl("lstat",K.join(r,p,E)),{name:E,path:p}))}return c}else{let c=[...n];for(let f of c){let p=this.listings.get(K.join(a,f));if(!(typeof p>"u"))for(let h of p)c.push(K.join(f,h))}return c}else return s?.withFileTypes?Array.from(n,c=>Object.assign(this.statImpl("lstat",K.join(r,c)),{name:c,path:void 0})):[...n]}async readlinkPromise(r){let s=this.prepareReadlink(r);return(await this.getFileSource(s,{asyncDecompress:!0})).toString()}readlinkSync(r){let s=this.prepareReadlink(r);return this.getFileSource(s).toString()}prepareReadlink(r){let s=this.resolveFilename(`readlink '${r}'`,r,!1);if(!this.entries.has(s)&&!this.listings.has(s))throw or.ENOENT(`readlink '${r}'`);if(r[r.length-1]==="/"&&!this.listings.has(s))throw or.ENOTDIR(`open '${r}'`);if(this.listings.has(s))throw or.EINVAL(`readlink '${r}'`);let a=this.entries.get(s);if(a===void 0)throw new Error("Unreachable");if(!this.isSymbolicLink(a))throw or.EINVAL(`readlink '${r}'`);return a}async truncatePromise(r,s=0){let a=this.resolveFilename(`open '${r}'`,r),n=this.entries.get(a);if(typeof n>"u")throw or.EINVAL(`open '${r}'`);let c=await this.getFileSource(n,{asyncDecompress:!0}),f=Buffer.alloc(s,0);return c.copy(f),await this.writeFilePromise(r,f)}truncateSync(r,s=0){let a=this.resolveFilename(`open '${r}'`,r),n=this.entries.get(a);if(typeof n>"u")throw or.EINVAL(`open '${r}'`);let c=this.getFileSource(n),f=Buffer.alloc(s,0);return c.copy(f),this.writeFileSync(r,f)}async ftruncatePromise(r,s){return this.truncatePromise(this.fdToPath(r,"ftruncate"),s)}ftruncateSync(r,s){return this.truncateSync(this.fdToPath(r,"ftruncateSync"),s)}watch(r,s,a){let n;switch(typeof s){case"function":case"string":case"undefined":n=!0;break;default:({persistent:n=!0}=s);break}if(!n)return{on:()=>{},close:()=>{}};let c=setInterval(()=>{},24*60*60*1e3);return{on:()=>{},close:()=>{clearInterval(c)}}}watchFile(r,s,a){let n=K.resolve(vt.root,r);return nE(this,n,s,a)}unwatchFile(r,s){let a=K.resolve(vt.root,r);return dd(this,a,s)}}});function Lde(t,e,r=Buffer.alloc(0),s){let a=new hs(r),n=C=>C===e||C.startsWith(`${e}/`)?C.slice(0,e.length):null,c=async(C,S)=>()=>a,f=(C,S)=>a,p={...t},h=new Yn(p),E=new r0({baseFs:h,getMountPoint:n,factoryPromise:c,factorySync:f,magicByte:21,maxAge:1/0,typeCheck:s?.typeCheck});return _2(Ode.default,new n0(E)),a}var Ode,Mde=Ct(()=>{bt();Ode=et(Ie("fs"));wT()});var _de=Ct(()=>{Rde();wT();Mde()});var zj,Ev,BT,Ude=Ct(()=>{bt();wT();zj={CENTRAL_DIRECTORY:33639248,END_OF_CENTRAL_DIRECTORY:101010256},Ev=22,BT=class t{constructor(e){this.filesShouldBeCached=!1;if("buffer"in e)throw new Error("Buffer based zip archives are not supported");if(!e.readOnly)throw new Error("Writable zip archives are not supported");this.baseFs=e.baseFs,this.fd=this.baseFs.openSync(e.path,"r");try{this.entries=t.readZipSync(this.fd,this.baseFs,e.size)}catch(r){throw this.baseFs.closeSync(this.fd),this.fd="closed",r}}static readZipSync(e,r,s){if(s=0;N--)if(n.readUInt32LE(N)===zj.END_OF_CENTRAL_DIRECTORY){a=N;break}if(a===-1)throw new Error("Not a zip archive")}let c=n.readUInt16LE(a+10),f=n.readUInt32LE(a+12),p=n.readUInt32LE(a+16),h=n.readUInt16LE(a+20);if(a+h+Ev>n.length)throw new Error("Zip archive inconsistent");if(c==65535||f==4294967295||p==4294967295)throw new Error("Zip 64 is not supported");if(f>s)throw new Error("Zip archive inconsistent");if(c>f/46)throw new Error("Zip archive inconsistent");let E=Buffer.alloc(f);if(r.readSync(e,E,0,E.length,p)!==E.length)throw new Error("Zip archive inconsistent");let C=[],S=0,P=0,I=0;for(;PE.length)throw new Error("Zip archive inconsistent");if(E.readUInt32LE(S)!==zj.CENTRAL_DIRECTORY)throw new Error("Zip archive inconsistent");let N=E.readUInt16LE(S+4)>>>8;if(E.readUInt16LE(S+8)&1)throw new Error("Encrypted zip files are not supported");let W=E.readUInt16LE(S+10),te=E.readUInt32LE(S+16),ie=E.readUInt16LE(S+28),Ae=E.readUInt16LE(S+30),ce=E.readUInt16LE(S+32),me=E.readUInt32LE(S+42),pe=E.toString("utf8",S+46,S+46+ie).replaceAll("\0"," ");if(pe.includes("\0"))throw new Error("Invalid ZIP file");let Be=E.readUInt32LE(S+20),Ce=E.readUInt32LE(S+38);C.push({name:pe,os:N,mtime:fi.SAFE_TIME,crc:te,compressionMethod:W,isSymbolicLink:N===om&&(Ce>>>16&fi.S_IFMT)===fi.S_IFLNK,size:E.readUInt32LE(S+24),compressedSize:Be,externalAttributes:Ce,localHeaderOffset:me}),I+=Be,P+=1,S+=46+ie+Ae+ce}if(I>s)throw new Error("Zip archive inconsistent");if(S!==E.length)throw new Error("Zip archive inconsistent");return C}getExternalAttributes(e){let r=this.entries[e];return[r.os,r.externalAttributes]}getListings(){return this.entries.map(e=>e.name)}getSymlinkCount(){let e=0;for(let r of this.entries)r.isSymbolicLink&&(e+=1);return e}stat(e){let r=this.entries[e];return{crc:r.crc,mtime:r.mtime,size:r.size}}locate(e){for(let r=0;rNde,DEFLATE:()=>Jj,JsZipImpl:()=>BT,LibZipImpl:()=>wI,STORE:()=>Kj,ZIP_UNIX:()=>om,ZipFS:()=>hs,ZipOpenFS:()=>tA,getArchivePart:()=>qj,getLibzipPromise:()=>Vdt,getLibzipSync:()=>Ydt,makeEmptyArchive:()=>CT,mountMemoryDrive:()=>Lde});function Ydt(){return yv()}async function Vdt(){return yv()}var Hde,rA=Ct(()=>{Uj();Hde=et(xde());Tde();_de();Ude();Wj();Pde(()=>{let t=(0,Hde.default)();return Qde(t)})});var Cv,jde=Ct(()=>{bt();Wt();wv();Cv=class extends ot{constructor(){super(...arguments);this.cwd=ge.String("--cwd",process.cwd(),{description:"The directory to run the command in"});this.commandName=ge.String();this.args=ge.Proxy()}static{this.usage={description:"run a command using yarn's portable shell",details:` This command will run a command using Yarn's portable shell. Make sure to escape glob patterns, redirections, and other features that might be expanded by your own shell. Note: To escape something from Yarn's shell, you might have to escape it twice, the first time from your own shell. Note: Don't use this command in Yarn scripts, as Yarn's shell is automatically used. For a list of features, visit: https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-shell/README.md. `,examples:[["Run a simple command","$0 echo Hello"],["Run a command with a glob pattern","$0 echo '*.js'"],["Run a command with a redirection","$0 echo Hello World '>' hello.txt"],["Run a command with an escaped glob pattern (The double escape is needed in Unix shells)",`$0 echo '"*.js"'`],["Run a command with a variable (Double quotes are needed in Unix shells, to prevent them from expanding the variable)",'$0 "GREETING=Hello echo $GREETING World"']]}}async execute(){let r=this.args.length>0?`${this.commandName} ${this.args.join(" ")}`:this.commandName;return await BI(r,[],{cwd:ue.toPortablePath(this.cwd),stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr})}}});var Kl,qde=Ct(()=>{Kl=class extends Error{constructor(e){super(e),this.name="ShellError"}}});var DT={};Vt(DT,{fastGlobOptions:()=>Yde,isBraceExpansion:()=>Zj,isGlobPattern:()=>Kdt,match:()=>Jdt,micromatchOptions:()=>ST});function Kdt(t){if(!vT.default.scan(t,ST).isGlob)return!1;try{vT.default.parse(t,ST)}catch{return!1}return!0}function Jdt(t,{cwd:e,baseFs:r}){return(0,Gde.default)(t,{...Yde,cwd:ue.fromPortablePath(e),fs:gx(Wde.default,new n0(r))})}function Zj(t){return vT.default.scan(t,ST).isBrace}var Gde,Wde,vT,ST,Yde,Vde=Ct(()=>{bt();Gde=et(CQ()),Wde=et(Ie("fs")),vT=et(Sa()),ST={strictBrackets:!0},Yde={onlyDirectories:!1,onlyFiles:!1}});function Xj(){}function $j(){for(let t of am)t.kill()}function Zde(t,e,r,s){return a=>{let n=a[0]instanceof nA.Transform?"pipe":a[0],c=a[1]instanceof nA.Transform?"pipe":a[1],f=a[2]instanceof nA.Transform?"pipe":a[2],p=(0,Jde.default)(t,e,{...s,stdio:[n,c,f]});return am.add(p),am.size===1&&(process.on("SIGINT",Xj),process.on("SIGTERM",$j)),a[0]instanceof nA.Transform&&a[0].pipe(p.stdin),a[1]instanceof nA.Transform&&p.stdout.pipe(a[1],{end:!1}),a[2]instanceof nA.Transform&&p.stderr.pipe(a[2],{end:!1}),{stdin:p.stdin,promise:new Promise(h=>{p.on("error",E=>{switch(am.delete(p),am.size===0&&(process.off("SIGINT",Xj),process.off("SIGTERM",$j)),E.code){case"ENOENT":a[2].write(`command not found: ${t} `),h(127);break;case"EACCES":a[2].write(`permission denied: ${t} `),h(128);break;default:a[2].write(`uncaught error: ${E.message} `),h(1);break}}),p.on("close",E=>{am.delete(p),am.size===0&&(process.off("SIGINT",Xj),process.off("SIGTERM",$j)),h(E!==null?E:129)})})}}}function Xde(t){return e=>{let r=e[0]==="pipe"?new nA.PassThrough:e[0];return{stdin:r,promise:Promise.resolve().then(()=>t({stdin:r,stdout:e[1],stderr:e[2]}))}}}function bT(t,e){return t6.start(t,e)}function Kde(t,e=null){let r=new nA.PassThrough,s=new zde.StringDecoder,a="";return r.on("data",n=>{let c=s.write(n),f;do if(f=c.indexOf(` `),f!==-1){let p=a+c.substring(0,f);c=c.substring(f+1),a="",t(e!==null?`${e} ${p}`:p)}while(f!==-1);a+=c}),r.on("end",()=>{let n=s.end();n!==""&&t(e!==null?`${e} ${n}`:n)}),r}function $de(t,{prefix:e}){return{stdout:Kde(r=>t.stdout.write(`${r} `),t.stdout.isTTY?e:null),stderr:Kde(r=>t.stderr.write(`${r} `),t.stderr.isTTY?e:null)}}var Jde,nA,zde,am,Mc,e6,t6,r6=Ct(()=>{Jde=et(j_()),nA=Ie("stream"),zde=Ie("string_decoder"),am=new Set;Mc=class{constructor(e){this.stream=e}close(){}get(){return this.stream}},e6=class{constructor(){this.stream=null}close(){if(this.stream===null)throw new Error("Assertion failed: No stream attached");this.stream.end()}attach(e){this.stream=e}get(){if(this.stream===null)throw new Error("Assertion failed: No stream attached");return this.stream}},t6=class t{constructor(e,r){this.stdin=null;this.stdout=null;this.stderr=null;this.pipe=null;this.ancestor=e,this.implementation=r}static start(e,{stdin:r,stdout:s,stderr:a}){let n=new t(null,e);return n.stdin=r,n.stdout=s,n.stderr=a,n}pipeTo(e,r=1){let s=new t(this,e),a=new e6;return s.pipe=a,s.stdout=this.stdout,s.stderr=this.stderr,(r&1)===1?this.stdout=a:this.ancestor!==null&&(this.stderr=this.ancestor.stdout),(r&2)===2?this.stderr=a:this.ancestor!==null&&(this.stderr=this.ancestor.stderr),s}async exec(){let e=["ignore","ignore","ignore"];if(this.pipe)e[0]="pipe";else{if(this.stdin===null)throw new Error("Assertion failed: No input stream registered");e[0]=this.stdin.get()}let r;if(this.stdout===null)throw new Error("Assertion failed: No output stream registered");r=this.stdout,e[1]=r.get();let s;if(this.stderr===null)throw new Error("Assertion failed: No error stream registered");s=this.stderr,e[2]=s.get();let a=this.implementation(e);return this.pipe&&this.pipe.attach(a.stdin),await a.promise.then(n=>(r.close(),s.close(),n))}async run(){let e=[];for(let s=this;s;s=s.ancestor)e.push(s.exec());return(await Promise.all(e))[0]}}});var Dv={};Vt(Dv,{EntryCommand:()=>Cv,ShellError:()=>Kl,execute:()=>BI,globUtils:()=>DT});function eme(t,e,r){let s=new Jl.PassThrough({autoDestroy:!0});switch(t){case 0:(e&1)===1&&r.stdin.pipe(s,{end:!1}),(e&2)===2&&r.stdin instanceof Jl.Writable&&s.pipe(r.stdin,{end:!1});break;case 1:(e&1)===1&&r.stdout.pipe(s,{end:!1}),(e&2)===2&&s.pipe(r.stdout,{end:!1});break;case 2:(e&1)===1&&r.stderr.pipe(s,{end:!1}),(e&2)===2&&s.pipe(r.stderr,{end:!1});break;default:throw new Kl(`Bad file descriptor: "${t}"`)}return s}function xT(t,e={}){let r={...t,...e};return r.environment={...t.environment,...e.environment},r.variables={...t.variables,...e.variables},r}async function Zdt(t,e,r){let s=[],a=new Jl.PassThrough;return a.on("data",n=>s.push(n)),await kT(t,e,xT(r,{stdout:a})),Buffer.concat(s).toString().replace(/[\r\n]+$/,"")}async function tme(t,e,r){let s=t.map(async n=>{let c=await lm(n.args,e,r);return{name:n.name,value:c.join(" ")}});return(await Promise.all(s)).reduce((n,c)=>(n[c.name]=c.value,n),{})}function PT(t){return t.match(/[^ \r\n\t]+/g)||[]}async function ame(t,e,r,s,a=s){switch(t.name){case"$":s(String(process.pid));break;case"#":s(String(e.args.length));break;case"@":if(t.quoted)for(let n of e.args)a(n);else for(let n of e.args){let c=PT(n);for(let f=0;f=0&&n"u"&&(t.defaultValue?c=(await lm(t.defaultValue,e,r)).join(" "):t.alternativeValue&&(c="")),typeof c>"u")throw f?new Kl(`Unbound argument #${n}`):new Kl(`Unbound variable "${t.name}"`);if(t.quoted)s(c);else{let p=PT(c);for(let E=0;Es.push(n));let a=Number(s.join(" "));return Number.isNaN(a)?Bv({type:"variable",name:s.join(" ")},e,r):Bv({type:"number",value:a},e,r)}else return Xdt[t.type](await Bv(t.left,e,r),await Bv(t.right,e,r))}async function lm(t,e,r){let s=new Map,a=[],n=[],c=E=>{n.push(E)},f=()=>{n.length>0&&a.push(n.join("")),n=[]},p=E=>{c(E),f()},h=(E,C,S)=>{let P=JSON.stringify({type:E,fd:C}),I=s.get(P);typeof I>"u"&&s.set(P,I=[]),I.push(S)};for(let E of t){let C=!1;switch(E.type){case"redirection":{let S=await lm(E.args,e,r);for(let P of S)h(E.subtype,E.fd,P)}break;case"argument":for(let S of E.segments)switch(S.type){case"text":c(S.text);break;case"glob":c(S.pattern),C=!0;break;case"shell":{let P=await Zdt(S.shell,e,r);if(S.quoted)c(P);else{let I=PT(P);for(let R=0;R"u")throw new Error("Assertion failed: Expected a glob pattern to have been set");let P=await e.glob.match(S,{cwd:r.cwd,baseFs:e.baseFs});if(P.length===0){let I=Zj(S)?". Note: Brace expansion of arbitrary strings isn't currently supported. For more details, please read this issue: https://github.com/yarnpkg/berry/issues/22":"";throw new Kl(`No matches found: "${S}"${I}`)}for(let I of P.sort())p(I)}}if(s.size>0){let E=[];for(let[C,S]of s.entries())E.splice(E.length,0,C,String(S.length),...S);a.splice(0,0,"__ysh_set_redirects",...E,"--")}return a}function vv(t,e,r){e.builtins.has(t[0])||(t=["command",...t]);let s=ue.fromPortablePath(r.cwd),a=r.environment;typeof a.PWD<"u"&&(a={...a,PWD:s});let[n,...c]=t;if(n==="command")return Zde(c[0],c.slice(1),e,{cwd:s,env:a});let f=e.builtins.get(n);if(typeof f>"u")throw new Error(`Assertion failed: A builtin should exist for "${n}"`);return Xde(async({stdin:p,stdout:h,stderr:E})=>{let{stdin:C,stdout:S,stderr:P}=r;r.stdin=p,r.stdout=h,r.stderr=E;try{return await f(c,e,r)}finally{r.stdin=C,r.stdout=S,r.stderr=P}})}function $dt(t,e,r){return s=>{let a=new Jl.PassThrough,n=kT(t,e,xT(r,{stdin:a}));return{stdin:a,promise:n}}}function emt(t,e,r){return s=>{let a=new Jl.PassThrough,n=kT(t,e,r);return{stdin:a,promise:n}}}function rme(t,e,r,s){if(e.length===0)return t;{let a;do a=String(Math.random());while(Object.hasOwn(s.procedures,a));return s.procedures={...s.procedures},s.procedures[a]=t,vv([...e,"__ysh_run_procedure",a],r,s)}}async function nme(t,e,r){let s=t,a=null,n=null;for(;s;){let c=s.then?{...r}:r,f;switch(s.type){case"command":{let p=await lm(s.args,e,r),h=await tme(s.envs,e,r);f=s.envs.length?vv(p,e,xT(c,{environment:h})):vv(p,e,c)}break;case"subshell":{let p=await lm(s.args,e,r),h=$dt(s.subshell,e,c);f=rme(h,p,e,c)}break;case"group":{let p=await lm(s.args,e,r),h=emt(s.group,e,c);f=rme(h,p,e,c)}break;case"envs":{let p=await tme(s.envs,e,r);c.environment={...c.environment,...p},f=vv(["true"],e,c)}break}if(typeof f>"u")throw new Error("Assertion failed: An action should have been generated");if(a===null)n=bT(f,{stdin:new Mc(c.stdin),stdout:new Mc(c.stdout),stderr:new Mc(c.stderr)});else{if(n===null)throw new Error("Assertion failed: The execution pipeline should have been setup");switch(a){case"|":n=n.pipeTo(f,1);break;case"|&":n=n.pipeTo(f,3);break}}s.then?(a=s.then.type,s=s.then.chain):s=null}if(n===null)throw new Error("Assertion failed: The execution pipeline should have been setup");return await n.run()}async function tmt(t,e,r,{background:s=!1}={}){function a(n){let c=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],f=c[n%c.length];return ime.default.hex(f)}if(s){let n=r.nextBackgroundJobIndex++,c=a(n),f=`[${n}]`,p=c(f),{stdout:h,stderr:E}=$de(r,{prefix:p});return r.backgroundJobs.push(nme(t,e,xT(r,{stdout:h,stderr:E})).catch(C=>E.write(`${C.message} `)).finally(()=>{r.stdout.isTTY&&r.stdout.write(`Job ${p}, '${c(uE(t))}' has ended `)})),0}return await nme(t,e,r)}async function rmt(t,e,r,{background:s=!1}={}){let a,n=f=>{a=f,r.variables["?"]=String(f)},c=async f=>{try{return await tmt(f.chain,e,r,{background:s&&typeof f.then>"u"})}catch(p){if(!(p instanceof Kl))throw p;return r.stderr.write(`${p.message} `),1}};for(n(await c(t));t.then;){if(r.exitCode!==null)return r.exitCode;switch(t.then.type){case"&&":a===0&&n(await c(t.then.line));break;case"||":a!==0&&n(await c(t.then.line));break;default:throw new Error(`Assertion failed: Unsupported command type: "${t.then.type}"`)}t=t.then.line}return a}async function kT(t,e,r){let s=r.backgroundJobs;r.backgroundJobs=[];let a=0;for(let{command:n,type:c}of t){if(a=await rmt(n,e,r,{background:c==="&"}),r.exitCode!==null)return r.exitCode;r.variables["?"]=String(a)}return await Promise.all(r.backgroundJobs),r.backgroundJobs=s,a}function lme(t){switch(t.type){case"variable":return t.name==="@"||t.name==="#"||t.name==="*"||Number.isFinite(parseInt(t.name,10))||"defaultValue"in t&&!!t.defaultValue&&t.defaultValue.some(e=>Sv(e))||"alternativeValue"in t&&!!t.alternativeValue&&t.alternativeValue.some(e=>Sv(e));case"arithmetic":return n6(t.arithmetic);case"shell":return i6(t.shell);default:return!1}}function Sv(t){switch(t.type){case"redirection":return t.args.some(e=>Sv(e));case"argument":return t.segments.some(e=>lme(e));default:throw new Error(`Assertion failed: Unsupported argument type: "${t.type}"`)}}function n6(t){switch(t.type){case"variable":return lme(t);case"number":return!1;default:return n6(t.left)||n6(t.right)}}function i6(t){return t.some(({command:e})=>{for(;e;){let r=e.chain;for(;r;){let s;switch(r.type){case"subshell":s=i6(r.subshell);break;case"command":s=r.envs.some(a=>a.args.some(n=>Sv(n)))||r.args.some(a=>Sv(a));break}if(s)return!0;if(!r.then)break;r=r.then.chain}if(!e.then)break;e=e.then.line}return!1})}async function BI(t,e=[],{baseFs:r=new Yn,builtins:s={},cwd:a=ue.toPortablePath(process.cwd()),env:n=process.env,stdin:c=process.stdin,stdout:f=process.stdout,stderr:p=process.stderr,variables:h={},glob:E=DT}={}){let C={};for(let[I,R]of Object.entries(n))typeof R<"u"&&(C[I]=R);let S=new Map(zdt);for(let[I,R]of Object.entries(s))S.set(I,R);c===null&&(c=new Jl.PassThrough,c.end());let P=yx(t,E);if(!i6(P)&&P.length>0&&e.length>0){let{command:I}=P[P.length-1];for(;I.then;)I=I.then.line;let R=I.chain;for(;R.then;)R=R.then.chain;R.type==="command"&&(R.args=R.args.concat(e.map(N=>({type:"argument",segments:[{type:"text",text:N}]}))))}return await kT(P,{args:e,baseFs:r,builtins:S,initialStdin:c,initialStdout:f,initialStderr:p,glob:E},{cwd:a,environment:C,exitCode:null,procedures:{},stdin:c,stdout:f,stderr:p,variables:Object.assign({},h,{"?":0}),nextBackgroundJobIndex:1,backgroundJobs:[]})}var ime,sme,Jl,ome,zdt,Xdt,wv=Ct(()=>{bt();Bc();ime=et(kE()),sme=Ie("os"),Jl=Ie("stream"),ome=Ie("timers/promises");jde();qde();Vde();r6();r6();zdt=new Map([["cd",async([t=(0,sme.homedir)(),...e],r,s)=>{let a=K.resolve(s.cwd,ue.toPortablePath(t));if(!(await r.baseFs.statPromise(a).catch(c=>{throw c.code==="ENOENT"?new Kl(`cd: no such file or directory: ${t}`):c})).isDirectory())throw new Kl(`cd: not a directory: ${t}`);return s.cwd=a,0}],["pwd",async(t,e,r)=>(r.stdout.write(`${ue.fromPortablePath(r.cwd)} `),0)],[":",async(t,e,r)=>0],["true",async(t,e,r)=>0],["false",async(t,e,r)=>1],["exit",async([t,...e],r,s)=>s.exitCode=parseInt(t??s.variables["?"],10)],["echo",async(t,e,r)=>(r.stdout.write(`${t.join(" ")} `),0)],["sleep",async([t],e,r)=>{if(typeof t>"u")throw new Kl("sleep: missing operand");let s=Number(t);if(Number.isNaN(s))throw new Kl(`sleep: invalid time interval '${t}'`);return await(0,ome.setTimeout)(1e3*s,0)}],["unset",async(t,e,r)=>{for(let s of t)delete r.environment[s],delete r.variables[s];return 0}],["__ysh_run_procedure",async(t,e,r)=>{let s=r.procedures[t[0]];return await bT(s,{stdin:new Mc(r.stdin),stdout:new Mc(r.stdout),stderr:new Mc(r.stderr)}).run()}],["__ysh_set_redirects",async(t,e,r)=>{let s=r.stdin,a=r.stdout,n=r.stderr,c=[],f=[],p=[],h=0;for(;t[h]!=="--";){let C=t[h++],{type:S,fd:P}=JSON.parse(C),I=W=>{switch(P){case null:case 0:c.push(W);break;default:throw new Error(`Unsupported file descriptor: "${P}"`)}},R=W=>{switch(P){case null:case 1:f.push(W);break;case 2:p.push(W);break;default:throw new Error(`Unsupported file descriptor: "${P}"`)}},N=Number(t[h++]),U=h+N;for(let W=h;We.baseFs.createReadStream(K.resolve(r.cwd,ue.toPortablePath(t[W]))));break;case"<<<":I(()=>{let te=new Jl.PassThrough;return process.nextTick(()=>{te.write(`${t[W]} `),te.end()}),te});break;case"<&":I(()=>eme(Number(t[W]),1,r));break;case">":case">>":{let te=K.resolve(r.cwd,ue.toPortablePath(t[W]));R(te==="/dev/null"?new Jl.Writable({autoDestroy:!0,emitClose:!0,write(ie,Ae,ce){setImmediate(ce)}}):e.baseFs.createWriteStream(te,S===">>"?{flags:"a"}:void 0))}break;case">&":R(eme(Number(t[W]),2,r));break;default:throw new Error(`Assertion failed: Unsupported redirection type: "${S}"`)}}if(c.length>0){let C=new Jl.PassThrough;s=C;let S=P=>{if(P===c.length)C.end();else{let I=c[P]();I.pipe(C,{end:!1}),I.on("end",()=>{S(P+1)})}};S(0)}if(f.length>0){let C=new Jl.PassThrough;a=C;for(let S of f)C.pipe(S)}if(p.length>0){let C=new Jl.PassThrough;n=C;for(let S of p)C.pipe(S)}let E=await bT(vv(t.slice(h+1),e,r),{stdin:new Mc(s),stdout:new Mc(a),stderr:new Mc(n)}).run();return await Promise.all(f.map(C=>new Promise((S,P)=>{C.on("error",I=>{P(I)}),C.on("close",()=>{S()}),C.end()}))),await Promise.all(p.map(C=>new Promise((S,P)=>{C.on("error",I=>{P(I)}),C.on("close",()=>{S()}),C.end()}))),E}]]);Xdt={addition:(t,e)=>t+e,subtraction:(t,e)=>t-e,multiplication:(t,e)=>t*e,division:(t,e)=>Math.trunc(t/e)}});var QT=L((DXt,cme)=>{function nmt(t,e){for(var r=-1,s=t==null?0:t.length,a=Array(s);++r{var ume=Gd(),imt=QT(),smt=xc(),omt=oI(),amt=1/0,fme=ume?ume.prototype:void 0,Ame=fme?fme.toString:void 0;function pme(t){if(typeof t=="string")return t;if(smt(t))return imt(t,pme)+"";if(omt(t))return Ame?Ame.call(t):"";var e=t+"";return e=="0"&&1/t==-amt?"-0":e}hme.exports=pme});var bv=L((PXt,dme)=>{var lmt=gme();function cmt(t){return t==null?"":lmt(t)}dme.exports=cmt});var s6=L((xXt,mme)=>{function umt(t,e,r){var s=-1,a=t.length;e<0&&(e=-e>a?0:a+e),r=r>a?a:r,r<0&&(r+=a),a=e>r?0:r-e>>>0,e>>>=0;for(var n=Array(a);++s{var fmt=s6();function Amt(t,e,r){var s=t.length;return r=r===void 0?s:r,!e&&r>=s?t:fmt(t,e,r)}yme.exports=Amt});var o6=L((QXt,Ime)=>{var pmt="\\ud800-\\udfff",hmt="\\u0300-\\u036f",gmt="\\ufe20-\\ufe2f",dmt="\\u20d0-\\u20ff",mmt=hmt+gmt+dmt,ymt="\\ufe0e\\ufe0f",Emt="\\u200d",Imt=RegExp("["+Emt+pmt+mmt+ymt+"]");function Cmt(t){return Imt.test(t)}Ime.exports=Cmt});var wme=L((TXt,Cme)=>{function wmt(t){return t.split("")}Cme.exports=wmt});var kme=L((RXt,xme)=>{var Bme="\\ud800-\\udfff",Bmt="\\u0300-\\u036f",vmt="\\ufe20-\\ufe2f",Smt="\\u20d0-\\u20ff",Dmt=Bmt+vmt+Smt,bmt="\\ufe0e\\ufe0f",Pmt="["+Bme+"]",a6="["+Dmt+"]",l6="\\ud83c[\\udffb-\\udfff]",xmt="(?:"+a6+"|"+l6+")",vme="[^"+Bme+"]",Sme="(?:\\ud83c[\\udde6-\\uddff]){2}",Dme="[\\ud800-\\udbff][\\udc00-\\udfff]",kmt="\\u200d",bme=xmt+"?",Pme="["+bmt+"]?",Qmt="(?:"+kmt+"(?:"+[vme,Sme,Dme].join("|")+")"+Pme+bme+")*",Tmt=Pme+bme+Qmt,Rmt="(?:"+[vme+a6+"?",a6,Sme,Dme,Pmt].join("|")+")",Fmt=RegExp(l6+"(?="+l6+")|"+Rmt+Tmt,"g");function Nmt(t){return t.match(Fmt)||[]}xme.exports=Nmt});var Tme=L((FXt,Qme)=>{var Omt=wme(),Lmt=o6(),Mmt=kme();function _mt(t){return Lmt(t)?Mmt(t):Omt(t)}Qme.exports=_mt});var Fme=L((NXt,Rme)=>{var Umt=Eme(),Hmt=o6(),jmt=Tme(),qmt=bv();function Gmt(t){return function(e){e=qmt(e);var r=Hmt(e)?jmt(e):void 0,s=r?r[0]:e.charAt(0),a=r?Umt(r,1).join(""):e.slice(1);return s[t]()+a}}Rme.exports=Gmt});var Ome=L((OXt,Nme)=>{var Wmt=Fme(),Ymt=Wmt("toUpperCase");Nme.exports=Ymt});var c6=L((LXt,Lme)=>{var Vmt=bv(),Kmt=Ome();function Jmt(t){return Kmt(Vmt(t).toLowerCase())}Lme.exports=Jmt});var Mme=L((MXt,TT)=>{function zmt(){var t=0,e=1,r=2,s=3,a=4,n=5,c=6,f=7,p=8,h=9,E=10,C=11,S=12,P=13,I=14,R=15,N=16,U=17,W=0,te=1,ie=2,Ae=3,ce=4;function me(g,we){return 55296<=g.charCodeAt(we)&&g.charCodeAt(we)<=56319&&56320<=g.charCodeAt(we+1)&&g.charCodeAt(we+1)<=57343}function pe(g,we){we===void 0&&(we=0);var ye=g.charCodeAt(we);if(55296<=ye&&ye<=56319&&we=1){var fe=g.charCodeAt(we-1),se=ye;return 55296<=fe&&fe<=56319?(fe-55296)*1024+(se-56320)+65536:se}return ye}function Be(g,we,ye){var fe=[g].concat(we).concat([ye]),se=fe[fe.length-2],X=ye,De=fe.lastIndexOf(I);if(De>1&&fe.slice(1,De).every(function(j){return j==s})&&[s,P,U].indexOf(g)==-1)return ie;var Re=fe.lastIndexOf(a);if(Re>0&&fe.slice(1,Re).every(function(j){return j==a})&&[S,a].indexOf(se)==-1)return fe.filter(function(j){return j==a}).length%2==1?Ae:ce;if(se==t&&X==e)return W;if(se==r||se==t||se==e)return X==I&&we.every(function(j){return j==s})?ie:te;if(X==r||X==t||X==e)return te;if(se==c&&(X==c||X==f||X==h||X==E))return W;if((se==h||se==f)&&(X==f||X==p))return W;if((se==E||se==p)&&X==p)return W;if(X==s||X==R)return W;if(X==n)return W;if(se==S)return W;var dt=fe.indexOf(s)!=-1?fe.lastIndexOf(s)-1:fe.length-2;return[P,U].indexOf(fe[dt])!=-1&&fe.slice(dt+1,-1).every(function(j){return j==s})&&X==I||se==R&&[N,U].indexOf(X)!=-1?W:we.indexOf(a)!=-1?ie:se==a&&X==a?W:te}this.nextBreak=function(g,we){if(we===void 0&&(we=0),we<0)return 0;if(we>=g.length-1)return g.length;for(var ye=Ce(pe(g,we)),fe=[],se=we+1;se{var Zmt=/^(.*?)(\x1b\[[^m]+m|\x1b\]8;;.*?(\x1b\\|\u0007))/,RT;function Xmt(){if(RT)return RT;if(typeof Intl.Segmenter<"u"){let t=new Intl.Segmenter("en",{granularity:"grapheme"});return RT=e=>Array.from(t.segment(e),({segment:r})=>r)}else{let t=Mme(),e=new t;return RT=r=>e.splitGraphemes(r)}}_me.exports=(t,e=0,r=t.length)=>{if(e<0||r<0)throw new RangeError("Negative indices aren't supported by this implementation");let s=r-e,a="",n=0,c=0;for(;t.length>0;){let f=t.match(Zmt)||[t,t,void 0],p=Xmt()(f[1]),h=Math.min(e-n,p.length);p=p.slice(h);let E=Math.min(s-c,p.length);a+=p.slice(0,E).join(""),n+=h,c+=E,typeof f[2]<"u"&&(a+=f[2]),t=t.slice(f[0].length)}return a}});var un,Pv=Ct(()=>{un=process.env.YARN_IS_TEST_ENV?"0.0.0":"4.9.2"});function Yme(t,{configuration:e,json:r}){if(!e.get("enableMessageNames"))return"";let a=Vf(t===null?0:t);return!r&&t===null?Ut(e,a,"grey"):a}function u6(t,{configuration:e,json:r}){let s=Yme(t,{configuration:e,json:r});if(!s||t===null||t===0)return s;let a=Dr[t],n=`https://yarnpkg.com/advanced/error-codes#${s}---${a}`.toLowerCase();return KE(e,s,n)}async function vI({configuration:t,stdout:e,forceError:r},s){let a=await Ot.start({configuration:t,stdout:e,includeFooter:!1},async n=>{let c=!1,f=!1;for(let p of s)typeof p.option<"u"&&(p.error||r?(f=!0,n.reportError(50,p.message)):(c=!0,n.reportWarning(50,p.message)),p.callback?.());c&&!f&&n.reportSeparator()});return a.hasErrors()?a.exitCode():null}var Gme,FT,$mt,Hme,jme,S0,Wme,qme,eyt,tyt,NT,ryt,Ot,xv=Ct(()=>{Gme=et(Ume()),FT=et(Rd());Zx();Fc();Pv();Qc();$mt="\xB7",Hme=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],jme=80,S0=FT.default.GITHUB_ACTIONS?{start:t=>`::group::${t} `,end:t=>`::endgroup:: `}:FT.default.TRAVIS?{start:t=>`travis_fold:start:${t} `,end:t=>`travis_fold:end:${t} `}:FT.default.GITLAB?{start:t=>`section_start:${Math.floor(Date.now()/1e3)}:${t.toLowerCase().replace(/\W+/g,"_")}[collapsed=true]\r\x1B[0K${t} `,end:t=>`section_end:${Math.floor(Date.now()/1e3)}:${t.toLowerCase().replace(/\W+/g,"_")}\r\x1B[0K`}:null,Wme=S0!==null,qme=new Date,eyt=["iTerm.app","Apple_Terminal","WarpTerminal","vscode"].includes(process.env.TERM_PROGRAM)||!!process.env.WT_SESSION,tyt=t=>t,NT=tyt({patrick:{date:[17,3],chars:["\u{1F340}","\u{1F331}"],size:40},simba:{date:[19,7],chars:["\u{1F981}","\u{1F334}"],size:40},jack:{date:[31,10],chars:["\u{1F383}","\u{1F987}"],size:40},hogsfather:{date:[31,12],chars:["\u{1F389}","\u{1F384}"],size:40},default:{chars:["=","-"],size:80}}),ryt=eyt&&Object.keys(NT).find(t=>{let e=NT[t];return!(e.date&&(e.date[0]!==qme.getDate()||e.date[1]!==qme.getMonth()+1))})||"default";Ot=class extends ho{constructor({configuration:r,stdout:s,json:a=!1,forceSectionAlignment:n=!1,includeNames:c=!0,includePrefix:f=!0,includeFooter:p=!0,includeLogs:h=!a,includeInfos:E=h,includeWarnings:C=h}){super();this.uncommitted=new Set;this.warningCount=0;this.errorCount=0;this.timerFooter=[];this.startTime=Date.now();this.indent=0;this.level=0;this.progress=new Map;this.progressTime=0;this.progressFrame=0;this.progressTimeout=null;this.progressStyle=null;this.progressMaxScaledSize=null;if(HB(this,{configuration:r}),this.configuration=r,this.forceSectionAlignment=n,this.includeNames=c,this.includePrefix=f,this.includeFooter=p,this.includeInfos=E,this.includeWarnings=C,this.json=a,this.stdout=s,r.get("enableProgressBars")&&!a&&s.isTTY&&s.columns>22){let S=r.get("progressBarStyle")||ryt;if(!Object.hasOwn(NT,S))throw new Error("Assertion failed: Invalid progress bar style");this.progressStyle=NT[S];let P=Math.min(this.getRecommendedLength(),80);this.progressMaxScaledSize=Math.floor(this.progressStyle.size*P/80)}}static async start(r,s){let a=new this(r),n=process.emitWarning;process.emitWarning=(c,f)=>{if(typeof c!="string"){let h=c;c=h.message,f=f??h.name}let p=typeof f<"u"?`${f}: ${c}`:c;a.reportWarning(0,p)},r.includeVersion&&a.reportInfo(0,Kd(r.configuration,`Yarn ${un}`,2));try{await s(a)}catch(c){a.reportExceptionOnce(c)}finally{await a.finalize(),process.emitWarning=n}return a}hasErrors(){return this.errorCount>0}exitCode(){return this.hasErrors()?1:0}getRecommendedLength(){let s=this.progressStyle!==null?this.stdout.columns-1:super.getRecommendedLength();return Math.max(40,s-12-this.indent*2)}startSectionSync({reportHeader:r,reportFooter:s,skipIfEmpty:a},n){let c={committed:!1,action:()=>{r?.()}};a?this.uncommitted.add(c):(c.action(),c.committed=!0);let f=Date.now();try{return n()}catch(p){throw this.reportExceptionOnce(p),p}finally{let p=Date.now();this.uncommitted.delete(c),c.committed&&s?.(p-f)}}async startSectionPromise({reportHeader:r,reportFooter:s,skipIfEmpty:a},n){let c={committed:!1,action:()=>{r?.()}};a?this.uncommitted.add(c):(c.action(),c.committed=!0);let f=Date.now();try{return await n()}catch(p){throw this.reportExceptionOnce(p),p}finally{let p=Date.now();this.uncommitted.delete(c),c.committed&&s?.(p-f)}}startTimerImpl(r,s,a){return{cb:typeof s=="function"?s:a,reportHeader:()=>{this.level+=1,this.reportInfo(null,`\u250C ${r}`),this.indent+=1,S0!==null&&!this.json&&this.includeInfos&&this.stdout.write(S0.start(r))},reportFooter:f=>{if(this.indent-=1,S0!==null&&!this.json&&this.includeInfos){this.stdout.write(S0.end(r));for(let p of this.timerFooter)p()}this.configuration.get("enableTimers")&&f>200?this.reportInfo(null,`\u2514 Completed in ${Ut(this.configuration,f,pt.DURATION)}`):this.reportInfo(null,"\u2514 Completed"),this.level-=1},skipIfEmpty:(typeof s=="function"?{}:s).skipIfEmpty}}startTimerSync(r,s,a){let{cb:n,...c}=this.startTimerImpl(r,s,a);return this.startSectionSync(c,n)}async startTimerPromise(r,s,a){let{cb:n,...c}=this.startTimerImpl(r,s,a);return this.startSectionPromise(c,n)}reportSeparator(){this.indent===0?this.writeLine(""):this.reportInfo(null,"")}reportInfo(r,s){if(!this.includeInfos)return;this.commit();let a=this.formatNameWithHyperlink(r),n=a?`${a}: `:"",c=`${this.formatPrefix(n,"blueBright")}${s}`;this.json?this.reportJson({type:"info",name:r,displayName:this.formatName(r),indent:this.formatIndent(),data:s}):this.writeLine(c)}reportWarning(r,s){if(this.warningCount+=1,!this.includeWarnings)return;this.commit();let a=this.formatNameWithHyperlink(r),n=a?`${a}: `:"";this.json?this.reportJson({type:"warning",name:r,displayName:this.formatName(r),indent:this.formatIndent(),data:s}):this.writeLine(`${this.formatPrefix(n,"yellowBright")}${s}`)}reportError(r,s){this.errorCount+=1,this.timerFooter.push(()=>this.reportErrorImpl(r,s)),this.reportErrorImpl(r,s)}reportErrorImpl(r,s){this.commit();let a=this.formatNameWithHyperlink(r),n=a?`${a}: `:"";this.json?this.reportJson({type:"error",name:r,displayName:this.formatName(r),indent:this.formatIndent(),data:s}):this.writeLine(`${this.formatPrefix(n,"redBright")}${s}`,{truncate:!1})}reportFold(r,s){if(!S0)return;let a=`${S0.start(r)}${s}${S0.end(r)}`;this.timerFooter.push(()=>this.stdout.write(a))}reportProgress(r){if(this.progressStyle===null)return{...Promise.resolve(),stop:()=>{}};if(r.hasProgress&&r.hasTitle)throw new Error("Unimplemented: Progress bars can't have both progress and titles.");let s=!1,a=Promise.resolve().then(async()=>{let c={progress:r.hasProgress?0:void 0,title:r.hasTitle?"":void 0};this.progress.set(r,{definition:c,lastScaledSize:r.hasProgress?-1:void 0,lastTitle:void 0}),this.refreshProgress({delta:-1});for await(let{progress:f,title:p}of r)s||c.progress===f&&c.title===p||(c.progress=f,c.title=p,this.refreshProgress());n()}),n=()=>{s||(s=!0,this.progress.delete(r),this.refreshProgress({delta:1}))};return{...a,stop:n}}reportJson(r){this.json&&this.writeLine(`${JSON.stringify(r)}`)}async finalize(){if(!this.includeFooter)return;let r="";this.errorCount>0?r="Failed with errors":this.warningCount>0?r="Done with warnings":r="Done";let s=Ut(this.configuration,Date.now()-this.startTime,pt.DURATION),a=this.configuration.get("enableTimers")?`${r} in ${s}`:r;this.errorCount>0?this.reportError(0,a):this.warningCount>0?this.reportWarning(0,a):this.reportInfo(0,a)}writeLine(r,{truncate:s}={}){this.clearProgress({clear:!0}),this.stdout.write(`${this.truncate(r,{truncate:s})} `),this.writeProgress()}writeLines(r,{truncate:s}={}){this.clearProgress({delta:r.length});for(let a of r)this.stdout.write(`${this.truncate(a,{truncate:s})} `);this.writeProgress()}commit(){let r=this.uncommitted;this.uncommitted=new Set;for(let s of r)s.committed=!0,s.action()}clearProgress({delta:r=0,clear:s=!1}){this.progressStyle!==null&&this.progress.size+r>0&&(this.stdout.write(`\x1B[${this.progress.size+r}A`),(r>0||s)&&this.stdout.write("\x1B[0J"))}writeProgress(){if(this.progressStyle===null||(this.progressTimeout!==null&&clearTimeout(this.progressTimeout),this.progressTimeout=null,this.progress.size===0))return;let r=Date.now();r-this.progressTime>jme&&(this.progressFrame=(this.progressFrame+1)%Hme.length,this.progressTime=r);let s=Hme[this.progressFrame];for(let a of this.progress.values()){let n="";if(typeof a.lastScaledSize<"u"){let h=this.progressStyle.chars[0].repeat(a.lastScaledSize),E=this.progressStyle.chars[1].repeat(this.progressMaxScaledSize-a.lastScaledSize);n=` ${h}${E}`}let c=this.formatName(null),f=c?`${c}: `:"",p=a.definition.title?` ${a.definition.title}`:"";this.stdout.write(`${Ut(this.configuration,"\u27A4","blueBright")} ${f}${s}${n}${p} `)}this.progressTimeout=setTimeout(()=>{this.refreshProgress({force:!0})},jme)}refreshProgress({delta:r=0,force:s=!1}={}){let a=!1,n=!1;if(s||this.progress.size===0)a=!0;else for(let c of this.progress.values()){let f=typeof c.definition.progress<"u"?Math.trunc(this.progressMaxScaledSize*c.definition.progress):void 0,p=c.lastScaledSize;c.lastScaledSize=f;let h=c.lastTitle;if(c.lastTitle=c.definition.title,f!==p||(n=h!==c.definition.title)){a=!0;break}}a&&(this.clearProgress({delta:r,clear:n}),this.writeProgress())}truncate(r,{truncate:s}={}){return this.progressStyle===null&&(s=!1),typeof s>"u"&&(s=this.configuration.get("preferTruncatedLines")),s&&(r=(0,Gme.default)(r,0,this.stdout.columns-1)),r}formatName(r){return this.includeNames?Yme(r,{configuration:this.configuration,json:this.json}):""}formatPrefix(r,s){return this.includePrefix?`${Ut(this.configuration,"\u27A4",s)} ${r}${this.formatIndent()}`:""}formatNameWithHyperlink(r){return this.includeNames?u6(r,{configuration:this.configuration,json:this.json}):""}formatIndent(){return this.level>0||!this.forceSectionAlignment?"\u2502 ".repeat(this.indent):`${$mt} `}}});var In={};Vt(In,{PackageManager:()=>Jme,detectPackageManager:()=>zme,executePackageAccessibleBinary:()=>tye,executePackageScript:()=>OT,executePackageShellcode:()=>f6,executeWorkspaceAccessibleBinary:()=>cyt,executeWorkspaceLifecycleScript:()=>$me,executeWorkspaceScript:()=>Xme,getPackageAccessibleBinaries:()=>LT,getWorkspaceAccessibleBinaries:()=>eye,hasPackageScript:()=>oyt,hasWorkspaceScript:()=>A6,isNodeScript:()=>p6,makeScriptEnv:()=>kv,maybeExecuteWorkspaceLifecycleScript:()=>lyt,prepareExternalProject:()=>syt});async function D0(t,e,r,s=[]){if(process.platform==="win32"){let a=`@goto #_undefined_# 2>NUL || @title %COMSPEC% & @setlocal & @"${r}" ${s.map(n=>`"${n.replace('"','""')}"`).join(" ")} %*`;await le.writeFilePromise(K.format({dir:t,name:e,ext:".cmd"}),a)}await le.writeFilePromise(K.join(t,e),`#!/bin/sh exec "${r}" ${s.map(a=>`'${a.replace(/'/g,`'"'"'`)}'`).join(" ")} "$@" `,{mode:493})}async function zme(t){let e=await Ht.tryFind(t);if(e?.packageManager){let s=bQ(e.packageManager);if(s?.name){let a=`found ${JSON.stringify({packageManager:e.packageManager})} in manifest`,[n]=s.reference.split(".");switch(s.name){case"yarn":return{packageManagerField:!0,packageManager:Number(n)===1?"Yarn Classic":"Yarn",reason:a};case"npm":return{packageManagerField:!0,packageManager:"npm",reason:a};case"pnpm":return{packageManagerField:!0,packageManager:"pnpm",reason:a}}}}let r;try{r=await le.readFilePromise(K.join(t,Er.lockfile),"utf8")}catch{}return r!==void 0?r.match(/^__metadata:$/m)?{packageManager:"Yarn",reason:'"__metadata" key found in yarn.lock'}:{packageManager:"Yarn Classic",reason:'"__metadata" key not found in yarn.lock, must be a Yarn classic lockfile'}:le.existsSync(K.join(t,"package-lock.json"))?{packageManager:"npm",reason:`found npm's "package-lock.json" lockfile`}:le.existsSync(K.join(t,"pnpm-lock.yaml"))?{packageManager:"pnpm",reason:`found pnpm's "pnpm-lock.yaml" lockfile`}:null}async function kv({project:t,locator:e,binFolder:r,ignoreCorepack:s,lifecycleScript:a,baseEnv:n=t?.configuration.env??process.env}){let c={};for(let[E,C]of Object.entries(n))typeof C<"u"&&(c[E.toLowerCase()!=="path"?E:"PATH"]=C);let f=ue.fromPortablePath(r);c.BERRY_BIN_FOLDER=ue.fromPortablePath(f);let p=process.env.COREPACK_ROOT&&!s?ue.join(process.env.COREPACK_ROOT,"dist/yarn.js"):process.argv[1];if(await Promise.all([D0(r,"node",process.execPath),...un!==null?[D0(r,"run",process.execPath,[p,"run"]),D0(r,"yarn",process.execPath,[p]),D0(r,"yarnpkg",process.execPath,[p]),D0(r,"node-gyp",process.execPath,[p,"run","--top-level","node-gyp"])]:[]]),t&&(c.INIT_CWD=ue.fromPortablePath(t.configuration.startingCwd),c.PROJECT_CWD=ue.fromPortablePath(t.cwd)),c.PATH=c.PATH?`${f}${ue.delimiter}${c.PATH}`:`${f}`,c.npm_execpath=`${f}${ue.sep}yarn`,c.npm_node_execpath=`${f}${ue.sep}node`,e){if(!t)throw new Error("Assertion failed: Missing project");let E=t.tryWorkspaceByLocator(e),C=E?E.manifest.version??"":t.storedPackages.get(e.locatorHash).version??"";c.npm_package_name=cn(e),c.npm_package_version=C;let S;if(E)S=E.cwd;else{let P=t.storedPackages.get(e.locatorHash);if(!P)throw new Error(`Package for ${Yr(t.configuration,e)} not found in the project`);let I=t.configuration.getLinkers(),R={project:t,report:new Ot({stdout:new b0.PassThrough,configuration:t.configuration})},N=I.find(U=>U.supportsPackage(P,R));if(!N)throw new Error(`The package ${Yr(t.configuration,P)} isn't supported by any of the available linkers`);S=await N.findPackageLocation(P,R)}c.npm_package_json=ue.fromPortablePath(K.join(S,Er.manifest))}let h=un!==null?`yarn/${un}`:`yarn/${kp("@yarnpkg/core").version}-core`;return c.npm_config_user_agent=`${h} npm/? node/${process.version} ${process.platform} ${process.arch}`,a&&(c.npm_lifecycle_event=a),t&&await t.configuration.triggerHook(E=>E.setupScriptEnvironment,t,c,async(E,C,S)=>await D0(r,E,C,S)),c}async function syt(t,e,{configuration:r,report:s,workspace:a=null,locator:n=null}){await iyt(async()=>{await le.mktempPromise(async c=>{let f=K.join(c,"pack.log"),p=null,{stdout:h,stderr:E}=r.getSubprocessStreams(f,{prefix:ue.fromPortablePath(t),report:s}),C=n&&Gu(n)?tI(n):n,S=C?cl(C):"an external project";h.write(`Packing ${S} from sources `);let P=await zme(t),I;P!==null?(h.write(`Using ${P.packageManager} for bootstrap. Reason: ${P.reason} `),I=P.packageManager):(h.write(`No package manager configuration detected; defaulting to Yarn `),I="Yarn");let R=I==="Yarn"&&!P?.packageManagerField;await le.mktempPromise(async N=>{let U=await kv({binFolder:N,ignoreCorepack:R,baseEnv:{...process.env,COREPACK_ENABLE_AUTO_PIN:"0"}}),te=new Map([["Yarn Classic",async()=>{let Ae=a!==null?["workspace",a]:[],ce=K.join(t,Er.manifest),me=await le.readFilePromise(ce),pe=await Yu(process.execPath,[process.argv[1],"set","version","classic","--only-if-needed","--yarn-path"],{cwd:t,env:U,stdin:p,stdout:h,stderr:E,end:1});if(pe.code!==0)return pe.code;await le.writeFilePromise(ce,me),await le.appendFilePromise(K.join(t,".npmignore"),`/.yarn `),h.write(` `),delete U.NODE_ENV;let Be=await Yu("yarn",["install"],{cwd:t,env:U,stdin:p,stdout:h,stderr:E,end:1});if(Be.code!==0)return Be.code;h.write(` `);let Ce=await Yu("yarn",[...Ae,"pack","--filename",ue.fromPortablePath(e)],{cwd:t,env:U,stdin:p,stdout:h,stderr:E});return Ce.code!==0?Ce.code:0}],["Yarn",async()=>{let Ae=a!==null?["workspace",a]:[];U.YARN_ENABLE_INLINE_BUILDS="1";let ce=K.join(t,Er.lockfile);await le.existsPromise(ce)||await le.writeFilePromise(ce,"");let me=await Yu("yarn",[...Ae,"pack","--install-if-needed","--filename",ue.fromPortablePath(e)],{cwd:t,env:U,stdin:p,stdout:h,stderr:E});return me.code!==0?me.code:0}],["npm",async()=>{if(a!==null){let we=new b0.PassThrough,ye=GE(we);we.pipe(h,{end:!1});let fe=await Yu("npm",["--version"],{cwd:t,env:U,stdin:p,stdout:we,stderr:E,end:0});if(we.end(),fe.code!==0)return h.end(),E.end(),fe.code;let se=(await ye).toString().trim();if(!eA(se,">=7.x")){let X=ba(null,"npm"),De=On(X,se),Re=On(X,">=7.x");throw new Error(`Workspaces aren't supported by ${ni(r,De)}; please upgrade to ${ni(r,Re)} (npm has been detected as the primary package manager for ${Ut(r,t,pt.PATH)})`)}}let Ae=a!==null?["--workspace",a]:[];delete U.npm_config_user_agent,delete U.npm_config_production,delete U.NPM_CONFIG_PRODUCTION,delete U.NODE_ENV;let ce=await Yu("npm",["install","--legacy-peer-deps"],{cwd:t,env:U,stdin:p,stdout:h,stderr:E,end:1});if(ce.code!==0)return ce.code;let me=new b0.PassThrough,pe=GE(me);me.pipe(h);let Be=await Yu("npm",["pack","--silent",...Ae],{cwd:t,env:U,stdin:p,stdout:me,stderr:E});if(Be.code!==0)return Be.code;let Ce=(await pe).toString().trim().replace(/^.*\n/s,""),g=K.resolve(t,ue.toPortablePath(Ce));return await le.renamePromise(g,e),0}]]).get(I);if(typeof te>"u")throw new Error("Assertion failed: Unsupported workflow");let ie=await te();if(!(ie===0||typeof ie>"u"))throw le.detachTemp(c),new Yt(58,`Packing the package failed (exit code ${ie}, logs can be found here: ${Ut(r,f,pt.PATH)})`)})})})}async function oyt(t,e,{project:r}){let s=r.tryWorkspaceByLocator(t);if(s!==null)return A6(s,e);let a=r.storedPackages.get(t.locatorHash);if(!a)throw new Error(`Package for ${Yr(r.configuration,t)} not found in the project`);return await tA.openPromise(async n=>{let c=r.configuration,f=r.configuration.getLinkers(),p={project:r,report:new Ot({stdout:new b0.PassThrough,configuration:c})},h=f.find(P=>P.supportsPackage(a,p));if(!h)throw new Error(`The package ${Yr(r.configuration,a)} isn't supported by any of the available linkers`);let E=await h.findPackageLocation(a,p),C=new Sn(E,{baseFs:n});return(await Ht.find(vt.dot,{baseFs:C})).scripts.has(e)})}async function OT(t,e,r,{cwd:s,project:a,stdin:n,stdout:c,stderr:f}){return await le.mktempPromise(async p=>{let{manifest:h,env:E,cwd:C}=await Zme(t,{project:a,binFolder:p,cwd:s,lifecycleScript:e}),S=h.scripts.get(e);if(typeof S>"u")return 1;let P=async()=>await BI(S,r,{cwd:C,env:E,stdin:n,stdout:c,stderr:f});return await(await a.configuration.reduceHook(R=>R.wrapScriptExecution,P,a,t,e,{script:S,args:r,cwd:C,env:E,stdin:n,stdout:c,stderr:f}))()})}async function f6(t,e,r,{cwd:s,project:a,stdin:n,stdout:c,stderr:f}){return await le.mktempPromise(async p=>{let{env:h,cwd:E}=await Zme(t,{project:a,binFolder:p,cwd:s});return await BI(e,r,{cwd:E,env:h,stdin:n,stdout:c,stderr:f})})}async function ayt(t,{binFolder:e,cwd:r,lifecycleScript:s}){let a=await kv({project:t.project,locator:t.anchoredLocator,binFolder:e,lifecycleScript:s});return await h6(e,await eye(t)),typeof r>"u"&&(r=K.dirname(await le.realpathPromise(K.join(t.cwd,"package.json")))),{manifest:t.manifest,binFolder:e,env:a,cwd:r}}async function Zme(t,{project:e,binFolder:r,cwd:s,lifecycleScript:a}){let n=e.tryWorkspaceByLocator(t);if(n!==null)return ayt(n,{binFolder:r,cwd:s,lifecycleScript:a});let c=e.storedPackages.get(t.locatorHash);if(!c)throw new Error(`Package for ${Yr(e.configuration,t)} not found in the project`);return await tA.openPromise(async f=>{let p=e.configuration,h=e.configuration.getLinkers(),E={project:e,report:new Ot({stdout:new b0.PassThrough,configuration:p})},C=h.find(N=>N.supportsPackage(c,E));if(!C)throw new Error(`The package ${Yr(e.configuration,c)} isn't supported by any of the available linkers`);let S=await kv({project:e,locator:t,binFolder:r,lifecycleScript:a});await h6(r,await LT(t,{project:e}));let P=await C.findPackageLocation(c,E),I=new Sn(P,{baseFs:f}),R=await Ht.find(vt.dot,{baseFs:I});return typeof s>"u"&&(s=P),{manifest:R,binFolder:r,env:S,cwd:s}})}async function Xme(t,e,r,{cwd:s,stdin:a,stdout:n,stderr:c}){return await OT(t.anchoredLocator,e,r,{cwd:s,project:t.project,stdin:a,stdout:n,stderr:c})}function A6(t,e){return t.manifest.scripts.has(e)}async function $me(t,e,{cwd:r,report:s}){let{configuration:a}=t.project,n=null;await le.mktempPromise(async c=>{let f=K.join(c,`${e}.log`),p=`# This file contains the result of Yarn calling the "${e}" lifecycle script inside a workspace ("${ue.fromPortablePath(t.cwd)}") `,{stdout:h,stderr:E}=a.getSubprocessStreams(f,{report:s,prefix:Yr(a,t.anchoredLocator),header:p});s.reportInfo(36,`Calling the "${e}" lifecycle script`);let C=await Xme(t,e,[],{cwd:r,stdin:n,stdout:h,stderr:E});if(h.end(),E.end(),C!==0)throw le.detachTemp(c),new Yt(36,`${(0,Vme.default)(e)} script failed (exit code ${Ut(a,C,pt.NUMBER)}, logs can be found here: ${Ut(a,f,pt.PATH)}); run ${Ut(a,`yarn ${e}`,pt.CODE)} to investigate`)})}async function lyt(t,e,r){A6(t,e)&&await $me(t,e,r)}function p6(t){let e=K.extname(t);if(e.match(/\.[cm]?[jt]sx?$/))return!0;if(e===".exe"||e===".bin")return!1;let r=Buffer.alloc(4),s;try{s=le.openSync(t,"r")}catch{return!0}try{le.readSync(s,r,0,r.length,0)}finally{le.closeSync(s)}let a=r.readUint32BE();return!(a===3405691582||a===3489328638||a===2135247942||(a&4294901760)===1297743872)}async function LT(t,{project:e}){let r=e.configuration,s=new Map,a=e.storedPackages.get(t.locatorHash);if(!a)throw new Error(`Package for ${Yr(r,t)} not found in the project`);let n=new b0.Writable,c=r.getLinkers(),f={project:e,report:new Ot({configuration:r,stdout:n})},p=new Set([t.locatorHash]);for(let E of a.dependencies.values()){let C=e.storedResolutions.get(E.descriptorHash);if(!C)throw new Error(`Assertion failed: The resolution (${ni(r,E)}) should have been registered`);p.add(C)}let h=await Promise.all(Array.from(p,async E=>{let C=e.storedPackages.get(E);if(!C)throw new Error(`Assertion failed: The package (${E}) should have been registered`);if(C.bin.size===0)return Yl.skip;let S=c.find(I=>I.supportsPackage(C,f));if(!S)return Yl.skip;let P=null;try{P=await S.findPackageLocation(C,f)}catch(I){if(I.code==="LOCATOR_NOT_INSTALLED")return Yl.skip;throw I}return{dependency:C,packageLocation:P}}));for(let E of h){if(E===Yl.skip)continue;let{dependency:C,packageLocation:S}=E;for(let[P,I]of C.bin){let R=K.resolve(S,I);s.set(P,[C,ue.fromPortablePath(R),p6(R)])}}return s}async function eye(t){return await LT(t.anchoredLocator,{project:t.project})}async function h6(t,e){await Promise.all(Array.from(e,([r,[,s,a]])=>a?D0(t,r,process.execPath,[s]):D0(t,r,s,[])))}async function tye(t,e,r,{cwd:s,project:a,stdin:n,stdout:c,stderr:f,nodeArgs:p=[],packageAccessibleBinaries:h}){h??=await LT(t,{project:a});let E=h.get(e);if(!E)throw new Error(`Binary not found (${e}) for ${Yr(a.configuration,t)}`);return await le.mktempPromise(async C=>{let[,S]=E,P=await kv({project:a,locator:t,binFolder:C});await h6(P.BERRY_BIN_FOLDER,h);let I=p6(ue.toPortablePath(S))?Yu(process.execPath,[...p,S,...r],{cwd:s,env:P,stdin:n,stdout:c,stderr:f}):Yu(S,r,{cwd:s,env:P,stdin:n,stdout:c,stderr:f}),R;try{R=await I}finally{await le.removePromise(P.BERRY_BIN_FOLDER)}return R.code})}async function cyt(t,e,r,{cwd:s,stdin:a,stdout:n,stderr:c,packageAccessibleBinaries:f}){return await tye(t.anchoredLocator,e,r,{project:t.project,cwd:s,stdin:a,stdout:n,stderr:c,packageAccessibleBinaries:f})}var Vme,Kme,b0,Jme,nyt,iyt,g6=Ct(()=>{bt();bt();rA();wv();Vme=et(c6()),Kme=et(Od()),b0=Ie("stream");sI();Fc();xv();Pv();hT();Qc();kc();Np();Yo();Jme=(a=>(a.Yarn1="Yarn Classic",a.Yarn2="Yarn",a.Npm="npm",a.Pnpm="pnpm",a))(Jme||{});nyt=2,iyt=(0,Kme.default)(nyt)});var SI=L((o$t,nye)=>{"use strict";var rye=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"]]);nye.exports=t=>t?Object.keys(t).map(e=>[rye.has(e)?rye.get(e):e,t[e]]).reduce((e,r)=>(e[r[0]]=r[1],e),Object.create(null)):{}});var bI=L((a$t,Aye)=>{"use strict";var iye=typeof process=="object"&&process?process:{stdout:null,stderr:null},uyt=Ie("events"),sye=Ie("stream"),oye=Ie("string_decoder").StringDecoder,jp=Symbol("EOF"),qp=Symbol("maybeEmitEnd"),P0=Symbol("emittedEnd"),MT=Symbol("emittingEnd"),Qv=Symbol("emittedError"),_T=Symbol("closed"),aye=Symbol("read"),UT=Symbol("flush"),lye=Symbol("flushChunk"),fl=Symbol("encoding"),Gp=Symbol("decoder"),HT=Symbol("flowing"),Tv=Symbol("paused"),DI=Symbol("resume"),Ks=Symbol("bufferLength"),d6=Symbol("bufferPush"),m6=Symbol("bufferShift"),zo=Symbol("objectMode"),Zo=Symbol("destroyed"),y6=Symbol("emitData"),cye=Symbol("emitEnd"),E6=Symbol("emitEnd2"),Wp=Symbol("async"),Rv=t=>Promise.resolve().then(t),uye=global._MP_NO_ITERATOR_SYMBOLS_!=="1",fyt=uye&&Symbol.asyncIterator||Symbol("asyncIterator not implemented"),Ayt=uye&&Symbol.iterator||Symbol("iterator not implemented"),pyt=t=>t==="end"||t==="finish"||t==="prefinish",hyt=t=>t instanceof ArrayBuffer||typeof t=="object"&&t.constructor&&t.constructor.name==="ArrayBuffer"&&t.byteLength>=0,gyt=t=>!Buffer.isBuffer(t)&&ArrayBuffer.isView(t),jT=class{constructor(e,r,s){this.src=e,this.dest=r,this.opts=s,this.ondrain=()=>e[DI](),r.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},I6=class extends jT{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(e,r,s){super(e,r,s),this.proxyErrors=a=>r.emit("error",a),e.on("error",this.proxyErrors)}};Aye.exports=class fye extends sye{constructor(e){super(),this[HT]=!1,this[Tv]=!1,this.pipes=[],this.buffer=[],this[zo]=e&&e.objectMode||!1,this[zo]?this[fl]=null:this[fl]=e&&e.encoding||null,this[fl]==="buffer"&&(this[fl]=null),this[Wp]=e&&!!e.async||!1,this[Gp]=this[fl]?new oye(this[fl]):null,this[jp]=!1,this[P0]=!1,this[MT]=!1,this[_T]=!1,this[Qv]=null,this.writable=!0,this.readable=!0,this[Ks]=0,this[Zo]=!1}get bufferLength(){return this[Ks]}get encoding(){return this[fl]}set encoding(e){if(this[zo])throw new Error("cannot set encoding in objectMode");if(this[fl]&&e!==this[fl]&&(this[Gp]&&this[Gp].lastNeed||this[Ks]))throw new Error("cannot change encoding");this[fl]!==e&&(this[Gp]=e?new oye(e):null,this.buffer.length&&(this.buffer=this.buffer.map(r=>this[Gp].write(r)))),this[fl]=e}setEncoding(e){this.encoding=e}get objectMode(){return this[zo]}set objectMode(e){this[zo]=this[zo]||!!e}get async(){return this[Wp]}set async(e){this[Wp]=this[Wp]||!!e}write(e,r,s){if(this[jp])throw new Error("write after end");if(this[Zo])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof r=="function"&&(s=r,r="utf8"),r||(r="utf8");let a=this[Wp]?Rv:n=>n();return!this[zo]&&!Buffer.isBuffer(e)&&(gyt(e)?e=Buffer.from(e.buffer,e.byteOffset,e.byteLength):hyt(e)?e=Buffer.from(e):typeof e!="string"&&(this.objectMode=!0)),this[zo]?(this.flowing&&this[Ks]!==0&&this[UT](!0),this.flowing?this.emit("data",e):this[d6](e),this[Ks]!==0&&this.emit("readable"),s&&a(s),this.flowing):e.length?(typeof e=="string"&&!(r===this[fl]&&!this[Gp].lastNeed)&&(e=Buffer.from(e,r)),Buffer.isBuffer(e)&&this[fl]&&(e=this[Gp].write(e)),this.flowing&&this[Ks]!==0&&this[UT](!0),this.flowing?this.emit("data",e):this[d6](e),this[Ks]!==0&&this.emit("readable"),s&&a(s),this.flowing):(this[Ks]!==0&&this.emit("readable"),s&&a(s),this.flowing)}read(e){if(this[Zo])return null;if(this[Ks]===0||e===0||e>this[Ks])return this[qp](),null;this[zo]&&(e=null),this.buffer.length>1&&!this[zo]&&(this.encoding?this.buffer=[this.buffer.join("")]:this.buffer=[Buffer.concat(this.buffer,this[Ks])]);let r=this[aye](e||null,this.buffer[0]);return this[qp](),r}[aye](e,r){return e===r.length||e===null?this[m6]():(this.buffer[0]=r.slice(e),r=r.slice(0,e),this[Ks]-=e),this.emit("data",r),!this.buffer.length&&!this[jp]&&this.emit("drain"),r}end(e,r,s){return typeof e=="function"&&(s=e,e=null),typeof r=="function"&&(s=r,r="utf8"),e&&this.write(e,r),s&&this.once("end",s),this[jp]=!0,this.writable=!1,(this.flowing||!this[Tv])&&this[qp](),this}[DI](){this[Zo]||(this[Tv]=!1,this[HT]=!0,this.emit("resume"),this.buffer.length?this[UT]():this[jp]?this[qp]():this.emit("drain"))}resume(){return this[DI]()}pause(){this[HT]=!1,this[Tv]=!0}get destroyed(){return this[Zo]}get flowing(){return this[HT]}get paused(){return this[Tv]}[d6](e){this[zo]?this[Ks]+=1:this[Ks]+=e.length,this.buffer.push(e)}[m6](){return this.buffer.length&&(this[zo]?this[Ks]-=1:this[Ks]-=this.buffer[0].length),this.buffer.shift()}[UT](e){do;while(this[lye](this[m6]()));!e&&!this.buffer.length&&!this[jp]&&this.emit("drain")}[lye](e){return e?(this.emit("data",e),this.flowing):!1}pipe(e,r){if(this[Zo])return;let s=this[P0];return r=r||{},e===iye.stdout||e===iye.stderr?r.end=!1:r.end=r.end!==!1,r.proxyErrors=!!r.proxyErrors,s?r.end&&e.end():(this.pipes.push(r.proxyErrors?new I6(this,e,r):new jT(this,e,r)),this[Wp]?Rv(()=>this[DI]()):this[DI]()),e}unpipe(e){let r=this.pipes.find(s=>s.dest===e);r&&(this.pipes.splice(this.pipes.indexOf(r),1),r.unpipe())}addListener(e,r){return this.on(e,r)}on(e,r){let s=super.on(e,r);return e==="data"&&!this.pipes.length&&!this.flowing?this[DI]():e==="readable"&&this[Ks]!==0?super.emit("readable"):pyt(e)&&this[P0]?(super.emit(e),this.removeAllListeners(e)):e==="error"&&this[Qv]&&(this[Wp]?Rv(()=>r.call(this,this[Qv])):r.call(this,this[Qv])),s}get emittedEnd(){return this[P0]}[qp](){!this[MT]&&!this[P0]&&!this[Zo]&&this.buffer.length===0&&this[jp]&&(this[MT]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[_T]&&this.emit("close"),this[MT]=!1)}emit(e,r,...s){if(e!=="error"&&e!=="close"&&e!==Zo&&this[Zo])return;if(e==="data")return r?this[Wp]?Rv(()=>this[y6](r)):this[y6](r):!1;if(e==="end")return this[cye]();if(e==="close"){if(this[_T]=!0,!this[P0]&&!this[Zo])return;let n=super.emit("close");return this.removeAllListeners("close"),n}else if(e==="error"){this[Qv]=r;let n=super.emit("error",r);return this[qp](),n}else if(e==="resume"){let n=super.emit("resume");return this[qp](),n}else if(e==="finish"||e==="prefinish"){let n=super.emit(e);return this.removeAllListeners(e),n}let a=super.emit(e,r,...s);return this[qp](),a}[y6](e){for(let s of this.pipes)s.dest.write(e)===!1&&this.pause();let r=super.emit("data",e);return this[qp](),r}[cye](){this[P0]||(this[P0]=!0,this.readable=!1,this[Wp]?Rv(()=>this[E6]()):this[E6]())}[E6](){if(this[Gp]){let r=this[Gp].end();if(r){for(let s of this.pipes)s.dest.write(r);super.emit("data",r)}}for(let r of this.pipes)r.end();let e=super.emit("end");return this.removeAllListeners("end"),e}collect(){let e=[];this[zo]||(e.dataLength=0);let r=this.promise();return this.on("data",s=>{e.push(s),this[zo]||(e.dataLength+=s.length)}),r.then(()=>e)}concat(){return this[zo]?Promise.reject(new Error("cannot concat in objectMode")):this.collect().then(e=>this[zo]?Promise.reject(new Error("cannot concat in objectMode")):this[fl]?e.join(""):Buffer.concat(e,e.dataLength))}promise(){return new Promise((e,r)=>{this.on(Zo,()=>r(new Error("stream destroyed"))),this.on("error",s=>r(s)),this.on("end",()=>e())})}[fyt](){return{next:()=>{let r=this.read();if(r!==null)return Promise.resolve({done:!1,value:r});if(this[jp])return Promise.resolve({done:!0});let s=null,a=null,n=h=>{this.removeListener("data",c),this.removeListener("end",f),a(h)},c=h=>{this.removeListener("error",n),this.removeListener("end",f),this.pause(),s({value:h,done:!!this[jp]})},f=()=>{this.removeListener("error",n),this.removeListener("data",c),s({done:!0})},p=()=>n(new Error("stream destroyed"));return new Promise((h,E)=>{a=E,s=h,this.once(Zo,p),this.once("error",n),this.once("end",f),this.once("data",c)})}}}[Ayt](){return{next:()=>{let r=this.read();return{value:r,done:r===null}}}}destroy(e){return this[Zo]?(e?this.emit("error",e):this.emit(Zo),this):(this[Zo]=!0,this.buffer.length=0,this[Ks]=0,typeof this.close=="function"&&!this[_T]&&this.close(),e?this.emit("error",e):this.emit(Zo),this)}static isStream(e){return!!e&&(e instanceof fye||e instanceof sye||e instanceof uyt&&(typeof e.pipe=="function"||typeof e.write=="function"&&typeof e.end=="function"))}}});var hye=L((l$t,pye)=>{var dyt=Ie("zlib").constants||{ZLIB_VERNUM:4736};pye.exports=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:1/0,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},dyt))});var O6=L(zl=>{"use strict";var S6=Ie("assert"),x0=Ie("buffer").Buffer,mye=Ie("zlib"),cm=zl.constants=hye(),myt=bI(),gye=x0.concat,um=Symbol("_superWrite"),xI=class extends Error{constructor(e){super("zlib: "+e.message),this.code=e.code,this.errno=e.errno,this.code||(this.code="ZLIB_ERROR"),this.message="zlib: "+e.message,Error.captureStackTrace(this,this.constructor)}get name(){return"ZlibError"}},yyt=Symbol("opts"),Fv=Symbol("flushFlag"),dye=Symbol("finishFlushFlag"),N6=Symbol("fullFlushFlag"),Ii=Symbol("handle"),qT=Symbol("onError"),PI=Symbol("sawError"),C6=Symbol("level"),w6=Symbol("strategy"),B6=Symbol("ended"),c$t=Symbol("_defaultFullFlush"),GT=class extends myt{constructor(e,r){if(!e||typeof e!="object")throw new TypeError("invalid options for ZlibBase constructor");super(e),this[PI]=!1,this[B6]=!1,this[yyt]=e,this[Fv]=e.flush,this[dye]=e.finishFlush;try{this[Ii]=new mye[r](e)}catch(s){throw new xI(s)}this[qT]=s=>{this[PI]||(this[PI]=!0,this.close(),this.emit("error",s))},this[Ii].on("error",s=>this[qT](new xI(s))),this.once("end",()=>this.close)}close(){this[Ii]&&(this[Ii].close(),this[Ii]=null,this.emit("close"))}reset(){if(!this[PI])return S6(this[Ii],"zlib binding closed"),this[Ii].reset()}flush(e){this.ended||(typeof e!="number"&&(e=this[N6]),this.write(Object.assign(x0.alloc(0),{[Fv]:e})))}end(e,r,s){return e&&this.write(e,r),this.flush(this[dye]),this[B6]=!0,super.end(null,null,s)}get ended(){return this[B6]}write(e,r,s){if(typeof r=="function"&&(s=r,r="utf8"),typeof e=="string"&&(e=x0.from(e,r)),this[PI])return;S6(this[Ii],"zlib binding closed");let a=this[Ii]._handle,n=a.close;a.close=()=>{};let c=this[Ii].close;this[Ii].close=()=>{},x0.concat=h=>h;let f;try{let h=typeof e[Fv]=="number"?e[Fv]:this[Fv];f=this[Ii]._processChunk(e,h),x0.concat=gye}catch(h){x0.concat=gye,this[qT](new xI(h))}finally{this[Ii]&&(this[Ii]._handle=a,a.close=n,this[Ii].close=c,this[Ii].removeAllListeners("error"))}this[Ii]&&this[Ii].on("error",h=>this[qT](new xI(h)));let p;if(f)if(Array.isArray(f)&&f.length>0){p=this[um](x0.from(f[0]));for(let h=1;h{this.flush(a),n()};try{this[Ii].params(e,r)}finally{this[Ii].flush=s}this[Ii]&&(this[C6]=e,this[w6]=r)}}}},D6=class extends Yp{constructor(e){super(e,"Deflate")}},b6=class extends Yp{constructor(e){super(e,"Inflate")}},v6=Symbol("_portable"),P6=class extends Yp{constructor(e){super(e,"Gzip"),this[v6]=e&&!!e.portable}[um](e){return this[v6]?(this[v6]=!1,e[9]=255,super[um](e)):super[um](e)}},x6=class extends Yp{constructor(e){super(e,"Gunzip")}},k6=class extends Yp{constructor(e){super(e,"DeflateRaw")}},Q6=class extends Yp{constructor(e){super(e,"InflateRaw")}},T6=class extends Yp{constructor(e){super(e,"Unzip")}},WT=class extends GT{constructor(e,r){e=e||{},e.flush=e.flush||cm.BROTLI_OPERATION_PROCESS,e.finishFlush=e.finishFlush||cm.BROTLI_OPERATION_FINISH,super(e,r),this[N6]=cm.BROTLI_OPERATION_FLUSH}},R6=class extends WT{constructor(e){super(e,"BrotliCompress")}},F6=class extends WT{constructor(e){super(e,"BrotliDecompress")}};zl.Deflate=D6;zl.Inflate=b6;zl.Gzip=P6;zl.Gunzip=x6;zl.DeflateRaw=k6;zl.InflateRaw=Q6;zl.Unzip=T6;typeof mye.BrotliCompress=="function"?(zl.BrotliCompress=R6,zl.BrotliDecompress=F6):zl.BrotliCompress=zl.BrotliDecompress=class{constructor(){throw new Error("Brotli is not supported in this version of Node.js")}}});var kI=L((A$t,yye)=>{var Eyt=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform;yye.exports=Eyt!=="win32"?t=>t:t=>t&&t.replace(/\\/g,"/")});var YT=L((h$t,Eye)=>{"use strict";var Iyt=bI(),L6=kI(),M6=Symbol("slurp");Eye.exports=class extends Iyt{constructor(e,r,s){switch(super(),this.pause(),this.extended=r,this.globalExtended=s,this.header=e,this.startBlockSize=512*Math.ceil(e.size/512),this.blockRemain=this.startBlockSize,this.remain=e.size,this.type=e.type,this.meta=!1,this.ignore=!1,this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=!0;break;default:this.ignore=!0}this.path=L6(e.path),this.mode=e.mode,this.mode&&(this.mode=this.mode&4095),this.uid=e.uid,this.gid=e.gid,this.uname=e.uname,this.gname=e.gname,this.size=e.size,this.mtime=e.mtime,this.atime=e.atime,this.ctime=e.ctime,this.linkpath=L6(e.linkpath),this.uname=e.uname,this.gname=e.gname,r&&this[M6](r),s&&this[M6](s,!0)}write(e){let r=e.length;if(r>this.blockRemain)throw new Error("writing more to entry than is appropriate");let s=this.remain,a=this.blockRemain;return this.remain=Math.max(0,s-r),this.blockRemain=Math.max(0,a-r),this.ignore?!0:s>=r?super.write(e):super.write(e.slice(0,s))}[M6](e,r){for(let s in e)e[s]!==null&&e[s]!==void 0&&!(r&&s==="path")&&(this[s]=s==="path"||s==="linkpath"?L6(e[s]):e[s])}}});var _6=L(VT=>{"use strict";VT.name=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]);VT.code=new Map(Array.from(VT.name).map(t=>[t[1],t[0]]))});var Bye=L((d$t,wye)=>{"use strict";var Cyt=(t,e)=>{if(Number.isSafeInteger(t))t<0?Byt(t,e):wyt(t,e);else throw Error("cannot encode number outside of javascript safe integer range");return e},wyt=(t,e)=>{e[0]=128;for(var r=e.length;r>1;r--)e[r-1]=t&255,t=Math.floor(t/256)},Byt=(t,e)=>{e[0]=255;var r=!1;t=t*-1;for(var s=e.length;s>1;s--){var a=t&255;t=Math.floor(t/256),r?e[s-1]=Iye(a):a===0?e[s-1]=0:(r=!0,e[s-1]=Cye(a))}},vyt=t=>{let e=t[0],r=e===128?Dyt(t.slice(1,t.length)):e===255?Syt(t):null;if(r===null)throw Error("invalid base256 encoding");if(!Number.isSafeInteger(r))throw Error("parsed number outside of javascript safe integer range");return r},Syt=t=>{for(var e=t.length,r=0,s=!1,a=e-1;a>-1;a--){var n=t[a],c;s?c=Iye(n):n===0?c=n:(s=!0,c=Cye(n)),c!==0&&(r-=c*Math.pow(256,e-a-1))}return r},Dyt=t=>{for(var e=t.length,r=0,s=e-1;s>-1;s--){var a=t[s];a!==0&&(r+=a*Math.pow(256,e-s-1))}return r},Iye=t=>(255^t)&255,Cye=t=>(255^t)+1&255;wye.exports={encode:Cyt,parse:vyt}});var TI=L((m$t,Sye)=>{"use strict";var U6=_6(),QI=Ie("path").posix,vye=Bye(),H6=Symbol("slurp"),Zl=Symbol("type"),G6=class{constructor(e,r,s,a){this.cksumValid=!1,this.needPax=!1,this.nullBlock=!1,this.block=null,this.path=null,this.mode=null,this.uid=null,this.gid=null,this.size=null,this.mtime=null,this.cksum=null,this[Zl]="0",this.linkpath=null,this.uname=null,this.gname=null,this.devmaj=0,this.devmin=0,this.atime=null,this.ctime=null,Buffer.isBuffer(e)?this.decode(e,r||0,s,a):e&&this.set(e)}decode(e,r,s,a){if(r||(r=0),!e||!(e.length>=r+512))throw new Error("need 512 bytes for header");if(this.path=fm(e,r,100),this.mode=k0(e,r+100,8),this.uid=k0(e,r+108,8),this.gid=k0(e,r+116,8),this.size=k0(e,r+124,12),this.mtime=j6(e,r+136,12),this.cksum=k0(e,r+148,12),this[H6](s),this[H6](a,!0),this[Zl]=fm(e,r+156,1),this[Zl]===""&&(this[Zl]="0"),this[Zl]==="0"&&this.path.substr(-1)==="/"&&(this[Zl]="5"),this[Zl]==="5"&&(this.size=0),this.linkpath=fm(e,r+157,100),e.slice(r+257,r+265).toString()==="ustar\x0000")if(this.uname=fm(e,r+265,32),this.gname=fm(e,r+297,32),this.devmaj=k0(e,r+329,8),this.devmin=k0(e,r+337,8),e[r+475]!==0){let c=fm(e,r+345,155);this.path=c+"/"+this.path}else{let c=fm(e,r+345,130);c&&(this.path=c+"/"+this.path),this.atime=j6(e,r+476,12),this.ctime=j6(e,r+488,12)}let n=8*32;for(let c=r;c=r+512))throw new Error("need 512 bytes for header");let s=this.ctime||this.atime?130:155,a=byt(this.path||"",s),n=a[0],c=a[1];this.needPax=a[2],this.needPax=Am(e,r,100,n)||this.needPax,this.needPax=Q0(e,r+100,8,this.mode)||this.needPax,this.needPax=Q0(e,r+108,8,this.uid)||this.needPax,this.needPax=Q0(e,r+116,8,this.gid)||this.needPax,this.needPax=Q0(e,r+124,12,this.size)||this.needPax,this.needPax=q6(e,r+136,12,this.mtime)||this.needPax,e[r+156]=this[Zl].charCodeAt(0),this.needPax=Am(e,r+157,100,this.linkpath)||this.needPax,e.write("ustar\x0000",r+257,8),this.needPax=Am(e,r+265,32,this.uname)||this.needPax,this.needPax=Am(e,r+297,32,this.gname)||this.needPax,this.needPax=Q0(e,r+329,8,this.devmaj)||this.needPax,this.needPax=Q0(e,r+337,8,this.devmin)||this.needPax,this.needPax=Am(e,r+345,s,c)||this.needPax,e[r+475]!==0?this.needPax=Am(e,r+345,155,c)||this.needPax:(this.needPax=Am(e,r+345,130,c)||this.needPax,this.needPax=q6(e,r+476,12,this.atime)||this.needPax,this.needPax=q6(e,r+488,12,this.ctime)||this.needPax);let f=8*32;for(let p=r;p{let s=t,a="",n,c=QI.parse(t).root||".";if(Buffer.byteLength(s)<100)n=[s,a,!1];else{a=QI.dirname(s),s=QI.basename(s);do Buffer.byteLength(s)<=100&&Buffer.byteLength(a)<=e?n=[s,a,!1]:Buffer.byteLength(s)>100&&Buffer.byteLength(a)<=e?n=[s.substr(0,99),a,!0]:(s=QI.join(QI.basename(a),s),a=QI.dirname(a));while(a!==c&&!n);n||(n=[t.substr(0,99),"",!0])}return n},fm=(t,e,r)=>t.slice(e,e+r).toString("utf8").replace(/\0.*/,""),j6=(t,e,r)=>Pyt(k0(t,e,r)),Pyt=t=>t===null?null:new Date(t*1e3),k0=(t,e,r)=>t[e]&128?vye.parse(t.slice(e,e+r)):kyt(t,e,r),xyt=t=>isNaN(t)?null:t,kyt=(t,e,r)=>xyt(parseInt(t.slice(e,e+r).toString("utf8").replace(/\0.*$/,"").trim(),8)),Qyt={12:8589934591,8:2097151},Q0=(t,e,r,s)=>s===null?!1:s>Qyt[r]||s<0?(vye.encode(s,t.slice(e,e+r)),!0):(Tyt(t,e,r,s),!1),Tyt=(t,e,r,s)=>t.write(Ryt(s,r),e,r,"ascii"),Ryt=(t,e)=>Fyt(Math.floor(t).toString(8),e),Fyt=(t,e)=>(t.length===e-1?t:new Array(e-t.length-1).join("0")+t+" ")+"\0",q6=(t,e,r,s)=>s===null?!1:Q0(t,e,r,s.getTime()/1e3),Nyt=new Array(156).join("\0"),Am=(t,e,r,s)=>s===null?!1:(t.write(s+Nyt,e,r,"utf8"),s.length!==Buffer.byteLength(s)||s.length>r);Sye.exports=G6});var KT=L((y$t,Dye)=>{"use strict";var Oyt=TI(),Lyt=Ie("path"),Nv=class{constructor(e,r){this.atime=e.atime||null,this.charset=e.charset||null,this.comment=e.comment||null,this.ctime=e.ctime||null,this.gid=e.gid||null,this.gname=e.gname||null,this.linkpath=e.linkpath||null,this.mtime=e.mtime||null,this.path=e.path||null,this.size=e.size||null,this.uid=e.uid||null,this.uname=e.uname||null,this.dev=e.dev||null,this.ino=e.ino||null,this.nlink=e.nlink||null,this.global=r||!1}encode(){let e=this.encodeBody();if(e==="")return null;let r=Buffer.byteLength(e),s=512*Math.ceil(1+r/512),a=Buffer.allocUnsafe(s);for(let n=0;n<512;n++)a[n]=0;new Oyt({path:("PaxHeader/"+Lyt.basename(this.path)).slice(0,99),mode:this.mode||420,uid:this.uid||null,gid:this.gid||null,size:r,mtime:this.mtime||null,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime||null,ctime:this.ctime||null}).encode(a),a.write(e,512,r,"utf8");for(let n=r+512;n=Math.pow(10,n)&&(n+=1),n+a+s}};Nv.parse=(t,e,r)=>new Nv(Myt(_yt(t),e),r);var Myt=(t,e)=>e?Object.keys(t).reduce((r,s)=>(r[s]=t[s],r),e):t,_yt=t=>t.replace(/\n$/,"").split(` `).reduce(Uyt,Object.create(null)),Uyt=(t,e)=>{let r=parseInt(e,10);if(r!==Buffer.byteLength(e)+1)return t;e=e.substr((r+" ").length);let s=e.split("="),a=s.shift().replace(/^SCHILY\.(dev|ino|nlink)/,"$1");if(!a)return t;let n=s.join("=");return t[a]=/^([A-Z]+\.)?([mac]|birth|creation)time$/.test(a)?new Date(n*1e3):/^[0-9]+$/.test(n)?+n:n,t};Dye.exports=Nv});var RI=L((E$t,bye)=>{bye.exports=t=>{let e=t.length-1,r=-1;for(;e>-1&&t.charAt(e)==="/";)r=e,e--;return r===-1?t:t.slice(0,r)}});var JT=L((I$t,Pye)=>{"use strict";Pye.exports=t=>class extends t{warn(e,r,s={}){this.file&&(s.file=this.file),this.cwd&&(s.cwd=this.cwd),s.code=r instanceof Error&&r.code||e,s.tarCode=e,!this.strict&&s.recoverable!==!1?(r instanceof Error&&(s=Object.assign(r,s),r=r.message),this.emit("warn",s.tarCode,r,s)):r instanceof Error?this.emit("error",Object.assign(r,s)):this.emit("error",Object.assign(new Error(`${e}: ${r}`),s))}}});var Y6=L((w$t,xye)=>{"use strict";var zT=["|","<",">","?",":"],W6=zT.map(t=>String.fromCharCode(61440+t.charCodeAt(0))),Hyt=new Map(zT.map((t,e)=>[t,W6[e]])),jyt=new Map(W6.map((t,e)=>[t,zT[e]]));xye.exports={encode:t=>zT.reduce((e,r)=>e.split(r).join(Hyt.get(r)),t),decode:t=>W6.reduce((e,r)=>e.split(r).join(jyt.get(r)),t)}});var V6=L((B$t,Qye)=>{var{isAbsolute:qyt,parse:kye}=Ie("path").win32;Qye.exports=t=>{let e="",r=kye(t);for(;qyt(t)||r.root;){let s=t.charAt(0)==="/"&&t.slice(0,4)!=="//?/"?"/":r.root;t=t.substr(s.length),e+=s,r=kye(t)}return[e,t]}});var Rye=L((v$t,Tye)=>{"use strict";Tye.exports=(t,e,r)=>(t&=4095,r&&(t=(t|384)&-19),e&&(t&256&&(t|=64),t&32&&(t|=8),t&4&&(t|=1)),t)});var nq=L((b$t,Vye)=>{"use strict";var Uye=bI(),Hye=KT(),jye=TI(),sA=Ie("fs"),Fye=Ie("path"),iA=kI(),Gyt=RI(),qye=(t,e)=>e?(t=iA(t).replace(/^\.(\/|$)/,""),Gyt(e)+"/"+t):iA(t),Wyt=16*1024*1024,Nye=Symbol("process"),Oye=Symbol("file"),Lye=Symbol("directory"),J6=Symbol("symlink"),Mye=Symbol("hardlink"),Ov=Symbol("header"),ZT=Symbol("read"),z6=Symbol("lstat"),XT=Symbol("onlstat"),Z6=Symbol("onread"),X6=Symbol("onreadlink"),$6=Symbol("openfile"),eq=Symbol("onopenfile"),T0=Symbol("close"),$T=Symbol("mode"),tq=Symbol("awaitDrain"),K6=Symbol("ondrain"),oA=Symbol("prefix"),_ye=Symbol("hadError"),Gye=JT(),Yyt=Y6(),Wye=V6(),Yye=Rye(),eR=Gye(class extends Uye{constructor(e,r){if(r=r||{},super(r),typeof e!="string")throw new TypeError("path is required");this.path=iA(e),this.portable=!!r.portable,this.myuid=process.getuid&&process.getuid()||0,this.myuser=process.env.USER||"",this.maxReadSize=r.maxReadSize||Wyt,this.linkCache=r.linkCache||new Map,this.statCache=r.statCache||new Map,this.preservePaths=!!r.preservePaths,this.cwd=iA(r.cwd||process.cwd()),this.strict=!!r.strict,this.noPax=!!r.noPax,this.noMtime=!!r.noMtime,this.mtime=r.mtime||null,this.prefix=r.prefix?iA(r.prefix):null,this.fd=null,this.blockLen=null,this.blockRemain=null,this.buf=null,this.offset=null,this.length=null,this.pos=null,this.remain=null,typeof r.onwarn=="function"&&this.on("warn",r.onwarn);let s=!1;if(!this.preservePaths){let[a,n]=Wye(this.path);a&&(this.path=n,s=a)}this.win32=!!r.win32||process.platform==="win32",this.win32&&(this.path=Yyt.decode(this.path.replace(/\\/g,"/")),e=e.replace(/\\/g,"/")),this.absolute=iA(r.absolute||Fye.resolve(this.cwd,e)),this.path===""&&(this.path="./"),s&&this.warn("TAR_ENTRY_INFO",`stripping ${s} from absolute path`,{entry:this,path:s+this.path}),this.statCache.has(this.absolute)?this[XT](this.statCache.get(this.absolute)):this[z6]()}emit(e,...r){return e==="error"&&(this[_ye]=!0),super.emit(e,...r)}[z6](){sA.lstat(this.absolute,(e,r)=>{if(e)return this.emit("error",e);this[XT](r)})}[XT](e){this.statCache.set(this.absolute,e),this.stat=e,e.isFile()||(e.size=0),this.type=Kyt(e),this.emit("stat",e),this[Nye]()}[Nye](){switch(this.type){case"File":return this[Oye]();case"Directory":return this[Lye]();case"SymbolicLink":return this[J6]();default:return this.end()}}[$T](e){return Yye(e,this.type==="Directory",this.portable)}[oA](e){return qye(e,this.prefix)}[Ov](){this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.header=new jye({path:this[oA](this.path),linkpath:this.type==="Link"?this[oA](this.linkpath):this.linkpath,mode:this[$T](this.stat.mode),uid:this.portable?null:this.stat.uid,gid:this.portable?null:this.stat.gid,size:this.stat.size,mtime:this.noMtime?null:this.mtime||this.stat.mtime,type:this.type,uname:this.portable?null:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?null:this.stat.atime,ctime:this.portable?null:this.stat.ctime}),this.header.encode()&&!this.noPax&&super.write(new Hye({atime:this.portable?null:this.header.atime,ctime:this.portable?null:this.header.ctime,gid:this.portable?null:this.header.gid,mtime:this.noMtime?null:this.mtime||this.header.mtime,path:this[oA](this.path),linkpath:this.type==="Link"?this[oA](this.linkpath):this.linkpath,size:this.header.size,uid:this.portable?null:this.header.uid,uname:this.portable?null:this.header.uname,dev:this.portable?null:this.stat.dev,ino:this.portable?null:this.stat.ino,nlink:this.portable?null:this.stat.nlink}).encode()),super.write(this.header.block)}[Lye](){this.path.substr(-1)!=="/"&&(this.path+="/"),this.stat.size=0,this[Ov](),this.end()}[J6](){sA.readlink(this.absolute,(e,r)=>{if(e)return this.emit("error",e);this[X6](r)})}[X6](e){this.linkpath=iA(e),this[Ov](),this.end()}[Mye](e){this.type="Link",this.linkpath=iA(Fye.relative(this.cwd,e)),this.stat.size=0,this[Ov](),this.end()}[Oye](){if(this.stat.nlink>1){let e=this.stat.dev+":"+this.stat.ino;if(this.linkCache.has(e)){let r=this.linkCache.get(e);if(r.indexOf(this.cwd)===0)return this[Mye](r)}this.linkCache.set(e,this.absolute)}if(this[Ov](),this.stat.size===0)return this.end();this[$6]()}[$6](){sA.open(this.absolute,"r",(e,r)=>{if(e)return this.emit("error",e);this[eq](r)})}[eq](e){if(this.fd=e,this[_ye])return this[T0]();this.blockLen=512*Math.ceil(this.stat.size/512),this.blockRemain=this.blockLen;let r=Math.min(this.blockLen,this.maxReadSize);this.buf=Buffer.allocUnsafe(r),this.offset=0,this.pos=0,this.remain=this.stat.size,this.length=this.buf.length,this[ZT]()}[ZT](){let{fd:e,buf:r,offset:s,length:a,pos:n}=this;sA.read(e,r,s,a,n,(c,f)=>{if(c)return this[T0](()=>this.emit("error",c));this[Z6](f)})}[T0](e){sA.close(this.fd,e)}[Z6](e){if(e<=0&&this.remain>0){let a=new Error("encountered unexpected EOF");return a.path=this.absolute,a.syscall="read",a.code="EOF",this[T0](()=>this.emit("error",a))}if(e>this.remain){let a=new Error("did not encounter expected EOF");return a.path=this.absolute,a.syscall="read",a.code="EOF",this[T0](()=>this.emit("error",a))}if(e===this.remain)for(let a=e;athis[K6]())}[tq](e){this.once("drain",e)}write(e){if(this.blockRemaine?this.emit("error",e):this.end());this.offset>=this.length&&(this.buf=Buffer.allocUnsafe(Math.min(this.blockRemain,this.buf.length)),this.offset=0),this.length=this.buf.length-this.offset,this[ZT]()}}),rq=class extends eR{[z6](){this[XT](sA.lstatSync(this.absolute))}[J6](){this[X6](sA.readlinkSync(this.absolute))}[$6](){this[eq](sA.openSync(this.absolute,"r"))}[ZT](){let e=!0;try{let{fd:r,buf:s,offset:a,length:n,pos:c}=this,f=sA.readSync(r,s,a,n,c);this[Z6](f),e=!1}finally{if(e)try{this[T0](()=>{})}catch{}}}[tq](e){e()}[T0](e){sA.closeSync(this.fd),e()}},Vyt=Gye(class extends Uye{constructor(e,r){r=r||{},super(r),this.preservePaths=!!r.preservePaths,this.portable=!!r.portable,this.strict=!!r.strict,this.noPax=!!r.noPax,this.noMtime=!!r.noMtime,this.readEntry=e,this.type=e.type,this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.prefix=r.prefix||null,this.path=iA(e.path),this.mode=this[$T](e.mode),this.uid=this.portable?null:e.uid,this.gid=this.portable?null:e.gid,this.uname=this.portable?null:e.uname,this.gname=this.portable?null:e.gname,this.size=e.size,this.mtime=this.noMtime?null:r.mtime||e.mtime,this.atime=this.portable?null:e.atime,this.ctime=this.portable?null:e.ctime,this.linkpath=iA(e.linkpath),typeof r.onwarn=="function"&&this.on("warn",r.onwarn);let s=!1;if(!this.preservePaths){let[a,n]=Wye(this.path);a&&(this.path=n,s=a)}this.remain=e.size,this.blockRemain=e.startBlockSize,this.header=new jye({path:this[oA](this.path),linkpath:this.type==="Link"?this[oA](this.linkpath):this.linkpath,mode:this.mode,uid:this.portable?null:this.uid,gid:this.portable?null:this.gid,size:this.size,mtime:this.noMtime?null:this.mtime,type:this.type,uname:this.portable?null:this.uname,atime:this.portable?null:this.atime,ctime:this.portable?null:this.ctime}),s&&this.warn("TAR_ENTRY_INFO",`stripping ${s} from absolute path`,{entry:this,path:s+this.path}),this.header.encode()&&!this.noPax&&super.write(new Hye({atime:this.portable?null:this.atime,ctime:this.portable?null:this.ctime,gid:this.portable?null:this.gid,mtime:this.noMtime?null:this.mtime,path:this[oA](this.path),linkpath:this.type==="Link"?this[oA](this.linkpath):this.linkpath,size:this.size,uid:this.portable?null:this.uid,uname:this.portable?null:this.uname,dev:this.portable?null:this.readEntry.dev,ino:this.portable?null:this.readEntry.ino,nlink:this.portable?null:this.readEntry.nlink}).encode()),super.write(this.header.block),e.pipe(this)}[oA](e){return qye(e,this.prefix)}[$T](e){return Yye(e,this.type==="Directory",this.portable)}write(e){let r=e.length;if(r>this.blockRemain)throw new Error("writing more to entry than is appropriate");return this.blockRemain-=r,super.write(e)}end(){return this.blockRemain&&super.write(Buffer.alloc(this.blockRemain)),super.end()}});eR.Sync=rq;eR.Tar=Vyt;var Kyt=t=>t.isFile()?"File":t.isDirectory()?"Directory":t.isSymbolicLink()?"SymbolicLink":"Unsupported";Vye.exports=eR});var cR=L((x$t,eEe)=>{"use strict";var aR=class{constructor(e,r){this.path=e||"./",this.absolute=r,this.entry=null,this.stat=null,this.readdir=null,this.pending=!1,this.ignore=!1,this.piped=!1}},Jyt=bI(),zyt=O6(),Zyt=YT(),Aq=nq(),Xyt=Aq.Sync,$yt=Aq.Tar,eEt=ak(),Kye=Buffer.alloc(1024),nR=Symbol("onStat"),tR=Symbol("ended"),aA=Symbol("queue"),FI=Symbol("current"),pm=Symbol("process"),rR=Symbol("processing"),Jye=Symbol("processJob"),lA=Symbol("jobs"),iq=Symbol("jobDone"),iR=Symbol("addFSEntry"),zye=Symbol("addTarEntry"),lq=Symbol("stat"),cq=Symbol("readdir"),sR=Symbol("onreaddir"),oR=Symbol("pipe"),Zye=Symbol("entry"),sq=Symbol("entryOpt"),uq=Symbol("writeEntryClass"),$ye=Symbol("write"),oq=Symbol("ondrain"),lR=Ie("fs"),Xye=Ie("path"),tEt=JT(),aq=kI(),pq=tEt(class extends Jyt{constructor(e){super(e),e=e||Object.create(null),this.opt=e,this.file=e.file||"",this.cwd=e.cwd||process.cwd(),this.maxReadSize=e.maxReadSize,this.preservePaths=!!e.preservePaths,this.strict=!!e.strict,this.noPax=!!e.noPax,this.prefix=aq(e.prefix||""),this.linkCache=e.linkCache||new Map,this.statCache=e.statCache||new Map,this.readdirCache=e.readdirCache||new Map,this[uq]=Aq,typeof e.onwarn=="function"&&this.on("warn",e.onwarn),this.portable=!!e.portable,this.zip=null,e.gzip?(typeof e.gzip!="object"&&(e.gzip={}),this.portable&&(e.gzip.portable=!0),this.zip=new zyt.Gzip(e.gzip),this.zip.on("data",r=>super.write(r)),this.zip.on("end",r=>super.end()),this.zip.on("drain",r=>this[oq]()),this.on("resume",r=>this.zip.resume())):this.on("drain",this[oq]),this.noDirRecurse=!!e.noDirRecurse,this.follow=!!e.follow,this.noMtime=!!e.noMtime,this.mtime=e.mtime||null,this.filter=typeof e.filter=="function"?e.filter:r=>!0,this[aA]=new eEt,this[lA]=0,this.jobs=+e.jobs||4,this[rR]=!1,this[tR]=!1}[$ye](e){return super.write(e)}add(e){return this.write(e),this}end(e){return e&&this.write(e),this[tR]=!0,this[pm](),this}write(e){if(this[tR])throw new Error("write after end");return e instanceof Zyt?this[zye](e):this[iR](e),this.flowing}[zye](e){let r=aq(Xye.resolve(this.cwd,e.path));if(!this.filter(e.path,e))e.resume();else{let s=new aR(e.path,r,!1);s.entry=new $yt(e,this[sq](s)),s.entry.on("end",a=>this[iq](s)),this[lA]+=1,this[aA].push(s)}this[pm]()}[iR](e){let r=aq(Xye.resolve(this.cwd,e));this[aA].push(new aR(e,r)),this[pm]()}[lq](e){e.pending=!0,this[lA]+=1;let r=this.follow?"stat":"lstat";lR[r](e.absolute,(s,a)=>{e.pending=!1,this[lA]-=1,s?this.emit("error",s):this[nR](e,a)})}[nR](e,r){this.statCache.set(e.absolute,r),e.stat=r,this.filter(e.path,r)||(e.ignore=!0),this[pm]()}[cq](e){e.pending=!0,this[lA]+=1,lR.readdir(e.absolute,(r,s)=>{if(e.pending=!1,this[lA]-=1,r)return this.emit("error",r);this[sR](e,s)})}[sR](e,r){this.readdirCache.set(e.absolute,r),e.readdir=r,this[pm]()}[pm](){if(!this[rR]){this[rR]=!0;for(let e=this[aA].head;e!==null&&this[lA]this.warn(r,s,a),noPax:this.noPax,cwd:this.cwd,absolute:e.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime,prefix:this.prefix}}[Zye](e){this[lA]+=1;try{return new this[uq](e.path,this[sq](e)).on("end",()=>this[iq](e)).on("error",r=>this.emit("error",r))}catch(r){this.emit("error",r)}}[oq](){this[FI]&&this[FI].entry&&this[FI].entry.resume()}[oR](e){e.piped=!0,e.readdir&&e.readdir.forEach(a=>{let n=e.path,c=n==="./"?"":n.replace(/\/*$/,"/");this[iR](c+a)});let r=e.entry,s=this.zip;s?r.on("data",a=>{s.write(a)||r.pause()}):r.on("data",a=>{super.write(a)||r.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}}),fq=class extends pq{constructor(e){super(e),this[uq]=Xyt}pause(){}resume(){}[lq](e){let r=this.follow?"statSync":"lstatSync";this[nR](e,lR[r](e.absolute))}[cq](e,r){this[sR](e,lR.readdirSync(e.absolute))}[oR](e){let r=e.entry,s=this.zip;e.readdir&&e.readdir.forEach(a=>{let n=e.path,c=n==="./"?"":n.replace(/\/*$/,"/");this[iR](c+a)}),s?r.on("data",a=>{s.write(a)}):r.on("data",a=>{super[$ye](a)})}};pq.Sync=fq;eEe.exports=pq});var jI=L(Mv=>{"use strict";var rEt=bI(),nEt=Ie("events").EventEmitter,Al=Ie("fs"),dq=Al.writev;if(!dq){let t=process.binding("fs"),e=t.FSReqWrap||t.FSReqCallback;dq=(r,s,a,n)=>{let c=(p,h)=>n(p,h,s),f=new e;f.oncomplete=c,t.writeBuffers(r,s,a,f)}}var UI=Symbol("_autoClose"),Vu=Symbol("_close"),Lv=Symbol("_ended"),ii=Symbol("_fd"),tEe=Symbol("_finished"),F0=Symbol("_flags"),hq=Symbol("_flush"),mq=Symbol("_handleChunk"),yq=Symbol("_makeBuf"),hR=Symbol("_mode"),uR=Symbol("_needDrain"),MI=Symbol("_onerror"),HI=Symbol("_onopen"),gq=Symbol("_onread"),OI=Symbol("_onwrite"),N0=Symbol("_open"),Vp=Symbol("_path"),hm=Symbol("_pos"),cA=Symbol("_queue"),LI=Symbol("_read"),rEe=Symbol("_readSize"),R0=Symbol("_reading"),fR=Symbol("_remain"),nEe=Symbol("_size"),AR=Symbol("_write"),NI=Symbol("_writing"),pR=Symbol("_defaultFlag"),_I=Symbol("_errored"),gR=class extends rEt{constructor(e,r){if(r=r||{},super(r),this.readable=!0,this.writable=!1,typeof e!="string")throw new TypeError("path must be a string");this[_I]=!1,this[ii]=typeof r.fd=="number"?r.fd:null,this[Vp]=e,this[rEe]=r.readSize||16*1024*1024,this[R0]=!1,this[nEe]=typeof r.size=="number"?r.size:1/0,this[fR]=this[nEe],this[UI]=typeof r.autoClose=="boolean"?r.autoClose:!0,typeof this[ii]=="number"?this[LI]():this[N0]()}get fd(){return this[ii]}get path(){return this[Vp]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[N0](){Al.open(this[Vp],"r",(e,r)=>this[HI](e,r))}[HI](e,r){e?this[MI](e):(this[ii]=r,this.emit("open",r),this[LI]())}[yq](){return Buffer.allocUnsafe(Math.min(this[rEe],this[fR]))}[LI](){if(!this[R0]){this[R0]=!0;let e=this[yq]();if(e.length===0)return process.nextTick(()=>this[gq](null,0,e));Al.read(this[ii],e,0,e.length,null,(r,s,a)=>this[gq](r,s,a))}}[gq](e,r,s){this[R0]=!1,e?this[MI](e):this[mq](r,s)&&this[LI]()}[Vu](){if(this[UI]&&typeof this[ii]=="number"){let e=this[ii];this[ii]=null,Al.close(e,r=>r?this.emit("error",r):this.emit("close"))}}[MI](e){this[R0]=!0,this[Vu](),this.emit("error",e)}[mq](e,r){let s=!1;return this[fR]-=e,e>0&&(s=super.write(ethis[HI](e,r))}[HI](e,r){this[pR]&&this[F0]==="r+"&&e&&e.code==="ENOENT"?(this[F0]="w",this[N0]()):e?this[MI](e):(this[ii]=r,this.emit("open",r),this[hq]())}end(e,r){return e&&this.write(e,r),this[Lv]=!0,!this[NI]&&!this[cA].length&&typeof this[ii]=="number"&&this[OI](null,0),this}write(e,r){return typeof e=="string"&&(e=Buffer.from(e,r)),this[Lv]?(this.emit("error",new Error("write() after end()")),!1):this[ii]===null||this[NI]||this[cA].length?(this[cA].push(e),this[uR]=!0,!1):(this[NI]=!0,this[AR](e),!0)}[AR](e){Al.write(this[ii],e,0,e.length,this[hm],(r,s)=>this[OI](r,s))}[OI](e,r){e?this[MI](e):(this[hm]!==null&&(this[hm]+=r),this[cA].length?this[hq]():(this[NI]=!1,this[Lv]&&!this[tEe]?(this[tEe]=!0,this[Vu](),this.emit("finish")):this[uR]&&(this[uR]=!1,this.emit("drain"))))}[hq](){if(this[cA].length===0)this[Lv]&&this[OI](null,0);else if(this[cA].length===1)this[AR](this[cA].pop());else{let e=this[cA];this[cA]=[],dq(this[ii],e,this[hm],(r,s)=>this[OI](r,s))}}[Vu](){if(this[UI]&&typeof this[ii]=="number"){let e=this[ii];this[ii]=null,Al.close(e,r=>r?this.emit("error",r):this.emit("close"))}}},Iq=class extends dR{[N0](){let e;if(this[pR]&&this[F0]==="r+")try{e=Al.openSync(this[Vp],this[F0],this[hR])}catch(r){if(r.code==="ENOENT")return this[F0]="w",this[N0]();throw r}else e=Al.openSync(this[Vp],this[F0],this[hR]);this[HI](null,e)}[Vu](){if(this[UI]&&typeof this[ii]=="number"){let e=this[ii];this[ii]=null,Al.closeSync(e),this.emit("close")}}[AR](e){let r=!0;try{this[OI](null,Al.writeSync(this[ii],e,0,e.length,this[hm])),r=!1}finally{if(r)try{this[Vu]()}catch{}}}};Mv.ReadStream=gR;Mv.ReadStreamSync=Eq;Mv.WriteStream=dR;Mv.WriteStreamSync=Iq});var BR=L((T$t,uEe)=>{"use strict";var iEt=JT(),sEt=TI(),oEt=Ie("events"),aEt=ak(),lEt=1024*1024,cEt=YT(),iEe=KT(),uEt=O6(),Cq=Buffer.from([31,139]),_c=Symbol("state"),gm=Symbol("writeEntry"),Kp=Symbol("readEntry"),wq=Symbol("nextEntry"),sEe=Symbol("processEntry"),Uc=Symbol("extendedHeader"),_v=Symbol("globalExtendedHeader"),O0=Symbol("meta"),oEe=Symbol("emitMeta"),Pi=Symbol("buffer"),Jp=Symbol("queue"),dm=Symbol("ended"),aEe=Symbol("emittedEnd"),mm=Symbol("emit"),pl=Symbol("unzip"),mR=Symbol("consumeChunk"),yR=Symbol("consumeChunkSub"),Bq=Symbol("consumeBody"),lEe=Symbol("consumeMeta"),cEe=Symbol("consumeHeader"),ER=Symbol("consuming"),vq=Symbol("bufferConcat"),Sq=Symbol("maybeEnd"),Uv=Symbol("writing"),L0=Symbol("aborted"),IR=Symbol("onDone"),ym=Symbol("sawValidEntry"),CR=Symbol("sawNullBlock"),wR=Symbol("sawEOF"),fEt=t=>!0;uEe.exports=iEt(class extends oEt{constructor(e){e=e||{},super(e),this.file=e.file||"",this[ym]=null,this.on(IR,r=>{(this[_c]==="begin"||this[ym]===!1)&&this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}),e.ondone?this.on(IR,e.ondone):this.on(IR,r=>{this.emit("prefinish"),this.emit("finish"),this.emit("end"),this.emit("close")}),this.strict=!!e.strict,this.maxMetaEntrySize=e.maxMetaEntrySize||lEt,this.filter=typeof e.filter=="function"?e.filter:fEt,this.writable=!0,this.readable=!1,this[Jp]=new aEt,this[Pi]=null,this[Kp]=null,this[gm]=null,this[_c]="begin",this[O0]="",this[Uc]=null,this[_v]=null,this[dm]=!1,this[pl]=null,this[L0]=!1,this[CR]=!1,this[wR]=!1,typeof e.onwarn=="function"&&this.on("warn",e.onwarn),typeof e.onentry=="function"&&this.on("entry",e.onentry)}[cEe](e,r){this[ym]===null&&(this[ym]=!1);let s;try{s=new sEt(e,r,this[Uc],this[_v])}catch(a){return this.warn("TAR_ENTRY_INVALID",a)}if(s.nullBlock)this[CR]?(this[wR]=!0,this[_c]==="begin"&&(this[_c]="header"),this[mm]("eof")):(this[CR]=!0,this[mm]("nullBlock"));else if(this[CR]=!1,!s.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:s});else if(!s.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:s});else{let a=s.type;if(/^(Symbolic)?Link$/.test(a)&&!s.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:s});else if(!/^(Symbolic)?Link$/.test(a)&&s.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:s});else{let n=this[gm]=new cEt(s,this[Uc],this[_v]);if(!this[ym])if(n.remain){let c=()=>{n.invalid||(this[ym]=!0)};n.on("end",c)}else this[ym]=!0;n.meta?n.size>this.maxMetaEntrySize?(n.ignore=!0,this[mm]("ignoredEntry",n),this[_c]="ignore",n.resume()):n.size>0&&(this[O0]="",n.on("data",c=>this[O0]+=c),this[_c]="meta"):(this[Uc]=null,n.ignore=n.ignore||!this.filter(n.path,n),n.ignore?(this[mm]("ignoredEntry",n),this[_c]=n.remain?"ignore":"header",n.resume()):(n.remain?this[_c]="body":(this[_c]="header",n.end()),this[Kp]?this[Jp].push(n):(this[Jp].push(n),this[wq]())))}}}[sEe](e){let r=!0;return e?Array.isArray(e)?this.emit.apply(this,e):(this[Kp]=e,this.emit("entry",e),e.emittedEnd||(e.on("end",s=>this[wq]()),r=!1)):(this[Kp]=null,r=!1),r}[wq](){do;while(this[sEe](this[Jp].shift()));if(!this[Jp].length){let e=this[Kp];!e||e.flowing||e.size===e.remain?this[Uv]||this.emit("drain"):e.once("drain",s=>this.emit("drain"))}}[Bq](e,r){let s=this[gm],a=s.blockRemain,n=a>=e.length&&r===0?e:e.slice(r,r+a);return s.write(n),s.blockRemain||(this[_c]="header",this[gm]=null,s.end()),n.length}[lEe](e,r){let s=this[gm],a=this[Bq](e,r);return this[gm]||this[oEe](s),a}[mm](e,r,s){!this[Jp].length&&!this[Kp]?this.emit(e,r,s):this[Jp].push([e,r,s])}[oEe](e){switch(this[mm]("meta",this[O0]),e.type){case"ExtendedHeader":case"OldExtendedHeader":this[Uc]=iEe.parse(this[O0],this[Uc],!1);break;case"GlobalExtendedHeader":this[_v]=iEe.parse(this[O0],this[_v],!0);break;case"NextFileHasLongPath":case"OldGnuLongPath":this[Uc]=this[Uc]||Object.create(null),this[Uc].path=this[O0].replace(/\0.*/,"");break;case"NextFileHasLongLinkpath":this[Uc]=this[Uc]||Object.create(null),this[Uc].linkpath=this[O0].replace(/\0.*/,"");break;default:throw new Error("unknown meta: "+e.type)}}abort(e){this[L0]=!0,this.emit("abort",e),this.warn("TAR_ABORT",e,{recoverable:!1})}write(e){if(this[L0])return;if(this[pl]===null&&e){if(this[Pi]&&(e=Buffer.concat([this[Pi],e]),this[Pi]=null),e.lengththis[mR](n)),this[pl].on("error",n=>this.abort(n)),this[pl].on("end",n=>{this[dm]=!0,this[mR]()}),this[Uv]=!0;let a=this[pl][s?"end":"write"](e);return this[Uv]=!1,a}}this[Uv]=!0,this[pl]?this[pl].write(e):this[mR](e),this[Uv]=!1;let r=this[Jp].length?!1:this[Kp]?this[Kp].flowing:!0;return!r&&!this[Jp].length&&this[Kp].once("drain",s=>this.emit("drain")),r}[vq](e){e&&!this[L0]&&(this[Pi]=this[Pi]?Buffer.concat([this[Pi],e]):e)}[Sq](){if(this[dm]&&!this[aEe]&&!this[L0]&&!this[ER]){this[aEe]=!0;let e=this[gm];if(e&&e.blockRemain){let r=this[Pi]?this[Pi].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${e.blockRemain} more bytes, only ${r} available)`,{entry:e}),this[Pi]&&e.write(this[Pi]),e.end()}this[mm](IR)}}[mR](e){if(this[ER])this[vq](e);else if(!e&&!this[Pi])this[Sq]();else{if(this[ER]=!0,this[Pi]){this[vq](e);let r=this[Pi];this[Pi]=null,this[yR](r)}else this[yR](e);for(;this[Pi]&&this[Pi].length>=512&&!this[L0]&&!this[wR];){let r=this[Pi];this[Pi]=null,this[yR](r)}this[ER]=!1}(!this[Pi]||this[dm])&&this[Sq]()}[yR](e){let r=0,s=e.length;for(;r+512<=s&&!this[L0]&&!this[wR];)switch(this[_c]){case"begin":case"header":this[cEe](e,r),r+=512;break;case"ignore":case"body":r+=this[Bq](e,r);break;case"meta":r+=this[lEe](e,r);break;default:throw new Error("invalid state: "+this[_c])}r{"use strict";var AEt=SI(),AEe=BR(),qI=Ie("fs"),pEt=jI(),fEe=Ie("path"),Dq=RI();hEe.exports=(t,e,r)=>{typeof t=="function"?(r=t,e=null,t={}):Array.isArray(t)&&(e=t,t={}),typeof e=="function"&&(r=e,e=null),e?e=Array.from(e):e=[];let s=AEt(t);if(s.sync&&typeof r=="function")throw new TypeError("callback not supported for sync tar functions");if(!s.file&&typeof r=="function")throw new TypeError("callback only supported with file option");return e.length&&gEt(s,e),s.noResume||hEt(s),s.file&&s.sync?dEt(s):s.file?mEt(s,r):pEe(s)};var hEt=t=>{let e=t.onentry;t.onentry=e?r=>{e(r),r.resume()}:r=>r.resume()},gEt=(t,e)=>{let r=new Map(e.map(n=>[Dq(n),!0])),s=t.filter,a=(n,c)=>{let f=c||fEe.parse(n).root||".",p=n===f?!1:r.has(n)?r.get(n):a(fEe.dirname(n),f);return r.set(n,p),p};t.filter=s?(n,c)=>s(n,c)&&a(Dq(n)):n=>a(Dq(n))},dEt=t=>{let e=pEe(t),r=t.file,s=!0,a;try{let n=qI.statSync(r),c=t.maxReadSize||16*1024*1024;if(n.size{let r=new AEe(t),s=t.maxReadSize||16*1024*1024,a=t.file,n=new Promise((c,f)=>{r.on("error",f),r.on("end",c),qI.stat(a,(p,h)=>{if(p)f(p);else{let E=new pEt.ReadStream(a,{readSize:s,size:h.size});E.on("error",f),E.pipe(r)}})});return e?n.then(e,e):n},pEe=t=>new AEe(t)});var IEe=L((F$t,EEe)=>{"use strict";var yEt=SI(),SR=cR(),gEe=jI(),dEe=vR(),mEe=Ie("path");EEe.exports=(t,e,r)=>{if(typeof e=="function"&&(r=e),Array.isArray(t)&&(e=t,t={}),!e||!Array.isArray(e)||!e.length)throw new TypeError("no files or directories specified");e=Array.from(e);let s=yEt(t);if(s.sync&&typeof r=="function")throw new TypeError("callback not supported for sync tar functions");if(!s.file&&typeof r=="function")throw new TypeError("callback only supported with file option");return s.file&&s.sync?EEt(s,e):s.file?IEt(s,e,r):s.sync?CEt(s,e):wEt(s,e)};var EEt=(t,e)=>{let r=new SR.Sync(t),s=new gEe.WriteStreamSync(t.file,{mode:t.mode||438});r.pipe(s),yEe(r,e)},IEt=(t,e,r)=>{let s=new SR(t),a=new gEe.WriteStream(t.file,{mode:t.mode||438});s.pipe(a);let n=new Promise((c,f)=>{a.on("error",f),a.on("close",c),s.on("error",f)});return bq(s,e),r?n.then(r,r):n},yEe=(t,e)=>{e.forEach(r=>{r.charAt(0)==="@"?dEe({file:mEe.resolve(t.cwd,r.substr(1)),sync:!0,noResume:!0,onentry:s=>t.add(s)}):t.add(r)}),t.end()},bq=(t,e)=>{for(;e.length;){let r=e.shift();if(r.charAt(0)==="@")return dEe({file:mEe.resolve(t.cwd,r.substr(1)),noResume:!0,onentry:s=>t.add(s)}).then(s=>bq(t,e));t.add(r)}t.end()},CEt=(t,e)=>{let r=new SR.Sync(t);return yEe(r,e),r},wEt=(t,e)=>{let r=new SR(t);return bq(r,e),r}});var Pq=L((N$t,bEe)=>{"use strict";var BEt=SI(),CEe=cR(),Xl=Ie("fs"),wEe=jI(),BEe=vR(),vEe=Ie("path"),SEe=TI();bEe.exports=(t,e,r)=>{let s=BEt(t);if(!s.file)throw new TypeError("file is required");if(s.gzip)throw new TypeError("cannot append to compressed archives");if(!e||!Array.isArray(e)||!e.length)throw new TypeError("no files or directories specified");return e=Array.from(e),s.sync?vEt(s,e):DEt(s,e,r)};var vEt=(t,e)=>{let r=new CEe.Sync(t),s=!0,a,n;try{try{a=Xl.openSync(t.file,"r+")}catch(p){if(p.code==="ENOENT")a=Xl.openSync(t.file,"w+");else throw p}let c=Xl.fstatSync(a),f=Buffer.alloc(512);e:for(n=0;nc.size)break;n+=h,t.mtimeCache&&t.mtimeCache.set(p.path,p.mtime)}s=!1,SEt(t,r,n,a,e)}finally{if(s)try{Xl.closeSync(a)}catch{}}},SEt=(t,e,r,s,a)=>{let n=new wEe.WriteStreamSync(t.file,{fd:s,start:r});e.pipe(n),bEt(e,a)},DEt=(t,e,r)=>{e=Array.from(e);let s=new CEe(t),a=(c,f,p)=>{let h=(I,R)=>{I?Xl.close(c,N=>p(I)):p(null,R)},E=0;if(f===0)return h(null,0);let C=0,S=Buffer.alloc(512),P=(I,R)=>{if(I)return h(I);if(C+=R,C<512&&R)return Xl.read(c,S,C,S.length-C,E+C,P);if(E===0&&S[0]===31&&S[1]===139)return h(new Error("cannot append to compressed archives"));if(C<512)return h(null,E);let N=new SEe(S);if(!N.cksumValid)return h(null,E);let U=512*Math.ceil(N.size/512);if(E+U+512>f||(E+=U+512,E>=f))return h(null,E);t.mtimeCache&&t.mtimeCache.set(N.path,N.mtime),C=0,Xl.read(c,S,0,512,E,P)};Xl.read(c,S,0,512,E,P)},n=new Promise((c,f)=>{s.on("error",f);let p="r+",h=(E,C)=>{if(E&&E.code==="ENOENT"&&p==="r+")return p="w+",Xl.open(t.file,p,h);if(E)return f(E);Xl.fstat(C,(S,P)=>{if(S)return Xl.close(C,()=>f(S));a(C,P.size,(I,R)=>{if(I)return f(I);let N=new wEe.WriteStream(t.file,{fd:C,start:R});s.pipe(N),N.on("error",f),N.on("close",c),DEe(s,e)})})};Xl.open(t.file,p,h)});return r?n.then(r,r):n},bEt=(t,e)=>{e.forEach(r=>{r.charAt(0)==="@"?BEe({file:vEe.resolve(t.cwd,r.substr(1)),sync:!0,noResume:!0,onentry:s=>t.add(s)}):t.add(r)}),t.end()},DEe=(t,e)=>{for(;e.length;){let r=e.shift();if(r.charAt(0)==="@")return BEe({file:vEe.resolve(t.cwd,r.substr(1)),noResume:!0,onentry:s=>t.add(s)}).then(s=>DEe(t,e));t.add(r)}t.end()}});var xEe=L((O$t,PEe)=>{"use strict";var PEt=SI(),xEt=Pq();PEe.exports=(t,e,r)=>{let s=PEt(t);if(!s.file)throw new TypeError("file is required");if(s.gzip)throw new TypeError("cannot append to compressed archives");if(!e||!Array.isArray(e)||!e.length)throw new TypeError("no files or directories specified");return e=Array.from(e),kEt(s),xEt(s,e,r)};var kEt=t=>{let e=t.filter;t.mtimeCache||(t.mtimeCache=new Map),t.filter=e?(r,s)=>e(r,s)&&!(t.mtimeCache.get(r)>s.mtime):(r,s)=>!(t.mtimeCache.get(r)>s.mtime)}});var TEe=L((L$t,QEe)=>{var{promisify:kEe}=Ie("util"),M0=Ie("fs"),QEt=t=>{if(!t)t={mode:511,fs:M0};else if(typeof t=="object")t={mode:511,fs:M0,...t};else if(typeof t=="number")t={mode:t,fs:M0};else if(typeof t=="string")t={mode:parseInt(t,8),fs:M0};else throw new TypeError("invalid options argument");return t.mkdir=t.mkdir||t.fs.mkdir||M0.mkdir,t.mkdirAsync=kEe(t.mkdir),t.stat=t.stat||t.fs.stat||M0.stat,t.statAsync=kEe(t.stat),t.statSync=t.statSync||t.fs.statSync||M0.statSync,t.mkdirSync=t.mkdirSync||t.fs.mkdirSync||M0.mkdirSync,t};QEe.exports=QEt});var FEe=L((M$t,REe)=>{var TEt=process.platform,{resolve:REt,parse:FEt}=Ie("path"),NEt=t=>{if(/\0/.test(t))throw Object.assign(new TypeError("path must be a string without null bytes"),{path:t,code:"ERR_INVALID_ARG_VALUE"});if(t=REt(t),TEt==="win32"){let e=/[*|"<>?:]/,{root:r}=FEt(t);if(e.test(t.substr(r.length)))throw Object.assign(new Error("Illegal characters in path."),{path:t,code:"EINVAL"})}return t};REe.exports=NEt});var _Ee=L((_$t,MEe)=>{var{dirname:NEe}=Ie("path"),OEe=(t,e,r=void 0)=>r===e?Promise.resolve():t.statAsync(e).then(s=>s.isDirectory()?r:void 0,s=>s.code==="ENOENT"?OEe(t,NEe(e),e):void 0),LEe=(t,e,r=void 0)=>{if(r!==e)try{return t.statSync(e).isDirectory()?r:void 0}catch(s){return s.code==="ENOENT"?LEe(t,NEe(e),e):void 0}};MEe.exports={findMade:OEe,findMadeSync:LEe}});var Qq=L((U$t,HEe)=>{var{dirname:UEe}=Ie("path"),xq=(t,e,r)=>{e.recursive=!1;let s=UEe(t);return s===t?e.mkdirAsync(t,e).catch(a=>{if(a.code!=="EISDIR")throw a}):e.mkdirAsync(t,e).then(()=>r||t,a=>{if(a.code==="ENOENT")return xq(s,e).then(n=>xq(t,e,n));if(a.code!=="EEXIST"&&a.code!=="EROFS")throw a;return e.statAsync(t).then(n=>{if(n.isDirectory())return r;throw a},()=>{throw a})})},kq=(t,e,r)=>{let s=UEe(t);if(e.recursive=!1,s===t)try{return e.mkdirSync(t,e)}catch(a){if(a.code!=="EISDIR")throw a;return}try{return e.mkdirSync(t,e),r||t}catch(a){if(a.code==="ENOENT")return kq(t,e,kq(s,e,r));if(a.code!=="EEXIST"&&a.code!=="EROFS")throw a;try{if(!e.statSync(t).isDirectory())throw a}catch{throw a}}};HEe.exports={mkdirpManual:xq,mkdirpManualSync:kq}});var GEe=L((H$t,qEe)=>{var{dirname:jEe}=Ie("path"),{findMade:OEt,findMadeSync:LEt}=_Ee(),{mkdirpManual:MEt,mkdirpManualSync:_Et}=Qq(),UEt=(t,e)=>(e.recursive=!0,jEe(t)===t?e.mkdirAsync(t,e):OEt(e,t).then(s=>e.mkdirAsync(t,e).then(()=>s).catch(a=>{if(a.code==="ENOENT")return MEt(t,e);throw a}))),HEt=(t,e)=>{if(e.recursive=!0,jEe(t)===t)return e.mkdirSync(t,e);let s=LEt(e,t);try{return e.mkdirSync(t,e),s}catch(a){if(a.code==="ENOENT")return _Et(t,e);throw a}};qEe.exports={mkdirpNative:UEt,mkdirpNativeSync:HEt}});var KEe=L((j$t,VEe)=>{var WEe=Ie("fs"),jEt=process.version,Tq=jEt.replace(/^v/,"").split("."),YEe=+Tq[0]>10||+Tq[0]==10&&+Tq[1]>=12,qEt=YEe?t=>t.mkdir===WEe.mkdir:()=>!1,GEt=YEe?t=>t.mkdirSync===WEe.mkdirSync:()=>!1;VEe.exports={useNative:qEt,useNativeSync:GEt}});var eIe=L((q$t,$Ee)=>{var GI=TEe(),WI=FEe(),{mkdirpNative:JEe,mkdirpNativeSync:zEe}=GEe(),{mkdirpManual:ZEe,mkdirpManualSync:XEe}=Qq(),{useNative:WEt,useNativeSync:YEt}=KEe(),YI=(t,e)=>(t=WI(t),e=GI(e),WEt(e)?JEe(t,e):ZEe(t,e)),VEt=(t,e)=>(t=WI(t),e=GI(e),YEt(e)?zEe(t,e):XEe(t,e));YI.sync=VEt;YI.native=(t,e)=>JEe(WI(t),GI(e));YI.manual=(t,e)=>ZEe(WI(t),GI(e));YI.nativeSync=(t,e)=>zEe(WI(t),GI(e));YI.manualSync=(t,e)=>XEe(WI(t),GI(e));$Ee.exports=YI});var aIe=L((G$t,oIe)=>{"use strict";var Hc=Ie("fs"),Em=Ie("path"),KEt=Hc.lchown?"lchown":"chown",JEt=Hc.lchownSync?"lchownSync":"chownSync",rIe=Hc.lchown&&!process.version.match(/v1[1-9]+\./)&&!process.version.match(/v10\.[6-9]/),tIe=(t,e,r)=>{try{return Hc[JEt](t,e,r)}catch(s){if(s.code!=="ENOENT")throw s}},zEt=(t,e,r)=>{try{return Hc.chownSync(t,e,r)}catch(s){if(s.code!=="ENOENT")throw s}},ZEt=rIe?(t,e,r,s)=>a=>{!a||a.code!=="EISDIR"?s(a):Hc.chown(t,e,r,s)}:(t,e,r,s)=>s,Rq=rIe?(t,e,r)=>{try{return tIe(t,e,r)}catch(s){if(s.code!=="EISDIR")throw s;zEt(t,e,r)}}:(t,e,r)=>tIe(t,e,r),XEt=process.version,nIe=(t,e,r)=>Hc.readdir(t,e,r),$Et=(t,e)=>Hc.readdirSync(t,e);/^v4\./.test(XEt)&&(nIe=(t,e,r)=>Hc.readdir(t,r));var DR=(t,e,r,s)=>{Hc[KEt](t,e,r,ZEt(t,e,r,a=>{s(a&&a.code!=="ENOENT"?a:null)}))},iIe=(t,e,r,s,a)=>{if(typeof e=="string")return Hc.lstat(Em.resolve(t,e),(n,c)=>{if(n)return a(n.code!=="ENOENT"?n:null);c.name=e,iIe(t,c,r,s,a)});if(e.isDirectory())Fq(Em.resolve(t,e.name),r,s,n=>{if(n)return a(n);let c=Em.resolve(t,e.name);DR(c,r,s,a)});else{let n=Em.resolve(t,e.name);DR(n,r,s,a)}},Fq=(t,e,r,s)=>{nIe(t,{withFileTypes:!0},(a,n)=>{if(a){if(a.code==="ENOENT")return s();if(a.code!=="ENOTDIR"&&a.code!=="ENOTSUP")return s(a)}if(a||!n.length)return DR(t,e,r,s);let c=n.length,f=null,p=h=>{if(!f){if(h)return s(f=h);if(--c===0)return DR(t,e,r,s)}};n.forEach(h=>iIe(t,h,e,r,p))})},eIt=(t,e,r,s)=>{if(typeof e=="string")try{let a=Hc.lstatSync(Em.resolve(t,e));a.name=e,e=a}catch(a){if(a.code==="ENOENT")return;throw a}e.isDirectory()&&sIe(Em.resolve(t,e.name),r,s),Rq(Em.resolve(t,e.name),r,s)},sIe=(t,e,r)=>{let s;try{s=$Et(t,{withFileTypes:!0})}catch(a){if(a.code==="ENOENT")return;if(a.code==="ENOTDIR"||a.code==="ENOTSUP")return Rq(t,e,r);throw a}return s&&s.length&&s.forEach(a=>eIt(t,a,e,r)),Rq(t,e,r)};oIe.exports=Fq;Fq.sync=sIe});var fIe=L((W$t,Nq)=>{"use strict";var lIe=eIe(),jc=Ie("fs"),bR=Ie("path"),cIe=aIe(),Ku=kI(),PR=class extends Error{constructor(e,r){super("Cannot extract through symbolic link"),this.path=r,this.symlink=e}get name(){return"SylinkError"}},xR=class extends Error{constructor(e,r){super(r+": Cannot cd into '"+e+"'"),this.path=e,this.code=r}get name(){return"CwdError"}},kR=(t,e)=>t.get(Ku(e)),Hv=(t,e,r)=>t.set(Ku(e),r),tIt=(t,e)=>{jc.stat(t,(r,s)=>{(r||!s.isDirectory())&&(r=new xR(t,r&&r.code||"ENOTDIR")),e(r)})};Nq.exports=(t,e,r)=>{t=Ku(t);let s=e.umask,a=e.mode|448,n=(a&s)!==0,c=e.uid,f=e.gid,p=typeof c=="number"&&typeof f=="number"&&(c!==e.processUid||f!==e.processGid),h=e.preserve,E=e.unlink,C=e.cache,S=Ku(e.cwd),P=(N,U)=>{N?r(N):(Hv(C,t,!0),U&&p?cIe(U,c,f,W=>P(W)):n?jc.chmod(t,a,r):r())};if(C&&kR(C,t)===!0)return P();if(t===S)return tIt(t,P);if(h)return lIe(t,{mode:a}).then(N=>P(null,N),P);let R=Ku(bR.relative(S,t)).split("/");QR(S,R,a,C,E,S,null,P)};var QR=(t,e,r,s,a,n,c,f)=>{if(!e.length)return f(null,c);let p=e.shift(),h=Ku(bR.resolve(t+"/"+p));if(kR(s,h))return QR(h,e,r,s,a,n,c,f);jc.mkdir(h,r,uIe(h,e,r,s,a,n,c,f))},uIe=(t,e,r,s,a,n,c,f)=>p=>{p?jc.lstat(t,(h,E)=>{if(h)h.path=h.path&&Ku(h.path),f(h);else if(E.isDirectory())QR(t,e,r,s,a,n,c,f);else if(a)jc.unlink(t,C=>{if(C)return f(C);jc.mkdir(t,r,uIe(t,e,r,s,a,n,c,f))});else{if(E.isSymbolicLink())return f(new PR(t,t+"/"+e.join("/")));f(p)}}):(c=c||t,QR(t,e,r,s,a,n,c,f))},rIt=t=>{let e=!1,r="ENOTDIR";try{e=jc.statSync(t).isDirectory()}catch(s){r=s.code}finally{if(!e)throw new xR(t,r)}};Nq.exports.sync=(t,e)=>{t=Ku(t);let r=e.umask,s=e.mode|448,a=(s&r)!==0,n=e.uid,c=e.gid,f=typeof n=="number"&&typeof c=="number"&&(n!==e.processUid||c!==e.processGid),p=e.preserve,h=e.unlink,E=e.cache,C=Ku(e.cwd),S=N=>{Hv(E,t,!0),N&&f&&cIe.sync(N,n,c),a&&jc.chmodSync(t,s)};if(E&&kR(E,t)===!0)return S();if(t===C)return rIt(C),S();if(p)return S(lIe.sync(t,s));let I=Ku(bR.relative(C,t)).split("/"),R=null;for(let N=I.shift(),U=C;N&&(U+="/"+N);N=I.shift())if(U=Ku(bR.resolve(U)),!kR(E,U))try{jc.mkdirSync(U,s),R=R||U,Hv(E,U,!0)}catch{let te=jc.lstatSync(U);if(te.isDirectory()){Hv(E,U,!0);continue}else if(h){jc.unlinkSync(U),jc.mkdirSync(U,s),R=R||U,Hv(E,U,!0);continue}else if(te.isSymbolicLink())return new PR(U,U+"/"+I.join("/"))}return S(R)}});var Lq=L((Y$t,AIe)=>{var Oq=Object.create(null),{hasOwnProperty:nIt}=Object.prototype;AIe.exports=t=>(nIt.call(Oq,t)||(Oq[t]=t.normalize("NFKD")),Oq[t])});var dIe=L((V$t,gIe)=>{var pIe=Ie("assert"),iIt=Lq(),sIt=RI(),{join:hIe}=Ie("path"),oIt=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,aIt=oIt==="win32";gIe.exports=()=>{let t=new Map,e=new Map,r=h=>h.split("/").slice(0,-1).reduce((C,S)=>(C.length&&(S=hIe(C[C.length-1],S)),C.push(S||"/"),C),[]),s=new Set,a=h=>{let E=e.get(h);if(!E)throw new Error("function does not have any path reservations");return{paths:E.paths.map(C=>t.get(C)),dirs:[...E.dirs].map(C=>t.get(C))}},n=h=>{let{paths:E,dirs:C}=a(h);return E.every(S=>S[0]===h)&&C.every(S=>S[0]instanceof Set&&S[0].has(h))},c=h=>s.has(h)||!n(h)?!1:(s.add(h),h(()=>f(h)),!0),f=h=>{if(!s.has(h))return!1;let{paths:E,dirs:C}=e.get(h),S=new Set;return E.forEach(P=>{let I=t.get(P);pIe.equal(I[0],h),I.length===1?t.delete(P):(I.shift(),typeof I[0]=="function"?S.add(I[0]):I[0].forEach(R=>S.add(R)))}),C.forEach(P=>{let I=t.get(P);pIe(I[0]instanceof Set),I[0].size===1&&I.length===1?t.delete(P):I[0].size===1?(I.shift(),S.add(I[0])):I[0].delete(h)}),s.delete(h),S.forEach(P=>c(P)),!0};return{check:n,reserve:(h,E)=>{h=aIt?["win32 parallelization disabled"]:h.map(S=>iIt(sIt(hIe(S))).toLowerCase());let C=new Set(h.map(S=>r(S)).reduce((S,P)=>S.concat(P)));return e.set(E,{dirs:C,paths:h}),h.forEach(S=>{let P=t.get(S);P?P.push(E):t.set(S,[E])}),C.forEach(S=>{let P=t.get(S);P?P[P.length-1]instanceof Set?P[P.length-1].add(E):P.push(new Set([E])):t.set(S,[new Set([E])])}),c(E)}}}});var EIe=L((K$t,yIe)=>{var lIt=process.platform,cIt=lIt==="win32",uIt=global.__FAKE_TESTING_FS__||Ie("fs"),{O_CREAT:fIt,O_TRUNC:AIt,O_WRONLY:pIt,UV_FS_O_FILEMAP:mIe=0}=uIt.constants,hIt=cIt&&!!mIe,gIt=512*1024,dIt=mIe|AIt|fIt|pIt;yIe.exports=hIt?t=>t"w"});var Yq=L((J$t,RIe)=>{"use strict";var mIt=Ie("assert"),yIt=BR(),Mn=Ie("fs"),EIt=jI(),zp=Ie("path"),kIe=fIe(),IIe=Y6(),IIt=dIe(),CIt=V6(),$l=kI(),wIt=RI(),BIt=Lq(),CIe=Symbol("onEntry"),Uq=Symbol("checkFs"),wIe=Symbol("checkFs2"),FR=Symbol("pruneCache"),Hq=Symbol("isReusable"),qc=Symbol("makeFs"),jq=Symbol("file"),qq=Symbol("directory"),NR=Symbol("link"),BIe=Symbol("symlink"),vIe=Symbol("hardlink"),SIe=Symbol("unsupported"),DIe=Symbol("checkPath"),_0=Symbol("mkdir"),Xo=Symbol("onError"),TR=Symbol("pending"),bIe=Symbol("pend"),VI=Symbol("unpend"),Mq=Symbol("ended"),_q=Symbol("maybeClose"),Gq=Symbol("skip"),jv=Symbol("doChown"),qv=Symbol("uid"),Gv=Symbol("gid"),Wv=Symbol("checkedCwd"),QIe=Ie("crypto"),TIe=EIe(),vIt=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform,Yv=vIt==="win32",SIt=(t,e)=>{if(!Yv)return Mn.unlink(t,e);let r=t+".DELETE."+QIe.randomBytes(16).toString("hex");Mn.rename(t,r,s=>{if(s)return e(s);Mn.unlink(r,e)})},DIt=t=>{if(!Yv)return Mn.unlinkSync(t);let e=t+".DELETE."+QIe.randomBytes(16).toString("hex");Mn.renameSync(t,e),Mn.unlinkSync(e)},PIe=(t,e,r)=>t===t>>>0?t:e===e>>>0?e:r,xIe=t=>BIt(wIt($l(t))).toLowerCase(),bIt=(t,e)=>{e=xIe(e);for(let r of t.keys()){let s=xIe(r);(s===e||s.indexOf(e+"/")===0)&&t.delete(r)}},PIt=t=>{for(let e of t.keys())t.delete(e)},Vv=class extends yIt{constructor(e){if(e||(e={}),e.ondone=r=>{this[Mq]=!0,this[_q]()},super(e),this[Wv]=!1,this.reservations=IIt(),this.transform=typeof e.transform=="function"?e.transform:null,this.writable=!0,this.readable=!1,this[TR]=0,this[Mq]=!1,this.dirCache=e.dirCache||new Map,typeof e.uid=="number"||typeof e.gid=="number"){if(typeof e.uid!="number"||typeof e.gid!="number")throw new TypeError("cannot set owner without number uid and gid");if(e.preserveOwner)throw new TypeError("cannot preserve owner in archive and also set owner explicitly");this.uid=e.uid,this.gid=e.gid,this.setOwner=!0}else this.uid=null,this.gid=null,this.setOwner=!1;e.preserveOwner===void 0&&typeof e.uid!="number"?this.preserveOwner=process.getuid&&process.getuid()===0:this.preserveOwner=!!e.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():null,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():null,this.forceChown=e.forceChown===!0,this.win32=!!e.win32||Yv,this.newer=!!e.newer,this.keep=!!e.keep,this.noMtime=!!e.noMtime,this.preservePaths=!!e.preservePaths,this.unlink=!!e.unlink,this.cwd=$l(zp.resolve(e.cwd||process.cwd())),this.strip=+e.strip||0,this.processUmask=e.noChmod?0:process.umask(),this.umask=typeof e.umask=="number"?e.umask:this.processUmask,this.dmode=e.dmode||511&~this.umask,this.fmode=e.fmode||438&~this.umask,this.on("entry",r=>this[CIe](r))}warn(e,r,s={}){return(e==="TAR_BAD_ARCHIVE"||e==="TAR_ABORT")&&(s.recoverable=!1),super.warn(e,r,s)}[_q](){this[Mq]&&this[TR]===0&&(this.emit("prefinish"),this.emit("finish"),this.emit("end"),this.emit("close"))}[DIe](e){if(this.strip){let r=$l(e.path).split("/");if(r.length=this.strip)e.linkpath=s.slice(this.strip).join("/");else return!1}}if(!this.preservePaths){let r=$l(e.path),s=r.split("/");if(s.includes("..")||Yv&&/^[a-z]:\.\.$/i.test(s[0]))return this.warn("TAR_ENTRY_ERROR","path contains '..'",{entry:e,path:r}),!1;let[a,n]=CIt(r);a&&(e.path=n,this.warn("TAR_ENTRY_INFO",`stripping ${a} from absolute path`,{entry:e,path:r}))}if(zp.isAbsolute(e.path)?e.absolute=$l(zp.resolve(e.path)):e.absolute=$l(zp.resolve(this.cwd,e.path)),!this.preservePaths&&e.absolute.indexOf(this.cwd+"/")!==0&&e.absolute!==this.cwd)return this.warn("TAR_ENTRY_ERROR","path escaped extraction target",{entry:e,path:$l(e.path),resolvedPath:e.absolute,cwd:this.cwd}),!1;if(e.absolute===this.cwd&&e.type!=="Directory"&&e.type!=="GNUDumpDir")return!1;if(this.win32){let{root:r}=zp.win32.parse(e.absolute);e.absolute=r+IIe.encode(e.absolute.substr(r.length));let{root:s}=zp.win32.parse(e.path);e.path=s+IIe.encode(e.path.substr(s.length))}return!0}[CIe](e){if(!this[DIe](e))return e.resume();switch(mIt.equal(typeof e.absolute,"string"),e.type){case"Directory":case"GNUDumpDir":e.mode&&(e.mode=e.mode|448);case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[Uq](e);case"CharacterDevice":case"BlockDevice":case"FIFO":default:return this[SIe](e)}}[Xo](e,r){e.name==="CwdError"?this.emit("error",e):(this.warn("TAR_ENTRY_ERROR",e,{entry:r}),this[VI](),r.resume())}[_0](e,r,s){kIe($l(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cache:this.dirCache,cwd:this.cwd,mode:r,noChmod:this.noChmod},s)}[jv](e){return this.forceChown||this.preserveOwner&&(typeof e.uid=="number"&&e.uid!==this.processUid||typeof e.gid=="number"&&e.gid!==this.processGid)||typeof this.uid=="number"&&this.uid!==this.processUid||typeof this.gid=="number"&&this.gid!==this.processGid}[qv](e){return PIe(this.uid,e.uid,this.processUid)}[Gv](e){return PIe(this.gid,e.gid,this.processGid)}[jq](e,r){let s=e.mode&4095||this.fmode,a=new EIt.WriteStream(e.absolute,{flags:TIe(e.size),mode:s,autoClose:!1});a.on("error",p=>{a.fd&&Mn.close(a.fd,()=>{}),a.write=()=>!0,this[Xo](p,e),r()});let n=1,c=p=>{if(p){a.fd&&Mn.close(a.fd,()=>{}),this[Xo](p,e),r();return}--n===0&&Mn.close(a.fd,h=>{h?this[Xo](h,e):this[VI](),r()})};a.on("finish",p=>{let h=e.absolute,E=a.fd;if(e.mtime&&!this.noMtime){n++;let C=e.atime||new Date,S=e.mtime;Mn.futimes(E,C,S,P=>P?Mn.utimes(h,C,S,I=>c(I&&P)):c())}if(this[jv](e)){n++;let C=this[qv](e),S=this[Gv](e);Mn.fchown(E,C,S,P=>P?Mn.chown(h,C,S,I=>c(I&&P)):c())}c()});let f=this.transform&&this.transform(e)||e;f!==e&&(f.on("error",p=>{this[Xo](p,e),r()}),e.pipe(f)),f.pipe(a)}[qq](e,r){let s=e.mode&4095||this.dmode;this[_0](e.absolute,s,a=>{if(a){this[Xo](a,e),r();return}let n=1,c=f=>{--n===0&&(r(),this[VI](),e.resume())};e.mtime&&!this.noMtime&&(n++,Mn.utimes(e.absolute,e.atime||new Date,e.mtime,c)),this[jv](e)&&(n++,Mn.chown(e.absolute,this[qv](e),this[Gv](e),c)),c()})}[SIe](e){e.unsupported=!0,this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${e.type}`,{entry:e}),e.resume()}[BIe](e,r){this[NR](e,e.linkpath,"symlink",r)}[vIe](e,r){let s=$l(zp.resolve(this.cwd,e.linkpath));this[NR](e,s,"link",r)}[bIe](){this[TR]++}[VI](){this[TR]--,this[_q]()}[Gq](e){this[VI](),e.resume()}[Hq](e,r){return e.type==="File"&&!this.unlink&&r.isFile()&&r.nlink<=1&&!Yv}[Uq](e){this[bIe]();let r=[e.path];e.linkpath&&r.push(e.linkpath),this.reservations.reserve(r,s=>this[wIe](e,s))}[FR](e){e.type==="SymbolicLink"?PIt(this.dirCache):e.type!=="Directory"&&bIt(this.dirCache,e.absolute)}[wIe](e,r){this[FR](e);let s=f=>{this[FR](e),r(f)},a=()=>{this[_0](this.cwd,this.dmode,f=>{if(f){this[Xo](f,e),s();return}this[Wv]=!0,n()})},n=()=>{if(e.absolute!==this.cwd){let f=$l(zp.dirname(e.absolute));if(f!==this.cwd)return this[_0](f,this.dmode,p=>{if(p){this[Xo](p,e),s();return}c()})}c()},c=()=>{Mn.lstat(e.absolute,(f,p)=>{if(p&&(this.keep||this.newer&&p.mtime>e.mtime)){this[Gq](e),s();return}if(f||this[Hq](e,p))return this[qc](null,e,s);if(p.isDirectory()){if(e.type==="Directory"){let h=!this.noChmod&&e.mode&&(p.mode&4095)!==e.mode,E=C=>this[qc](C,e,s);return h?Mn.chmod(e.absolute,e.mode,E):E()}if(e.absolute!==this.cwd)return Mn.rmdir(e.absolute,h=>this[qc](h,e,s))}if(e.absolute===this.cwd)return this[qc](null,e,s);SIt(e.absolute,h=>this[qc](h,e,s))})};this[Wv]?n():a()}[qc](e,r,s){if(e){this[Xo](e,r),s();return}switch(r.type){case"File":case"OldFile":case"ContiguousFile":return this[jq](r,s);case"Link":return this[vIe](r,s);case"SymbolicLink":return this[BIe](r,s);case"Directory":case"GNUDumpDir":return this[qq](r,s)}}[NR](e,r,s,a){Mn[s](r,e.absolute,n=>{n?this[Xo](n,e):(this[VI](),e.resume()),a()})}},RR=t=>{try{return[null,t()]}catch(e){return[e,null]}},Wq=class extends Vv{[qc](e,r){return super[qc](e,r,()=>{})}[Uq](e){if(this[FR](e),!this[Wv]){let n=this[_0](this.cwd,this.dmode);if(n)return this[Xo](n,e);this[Wv]=!0}if(e.absolute!==this.cwd){let n=$l(zp.dirname(e.absolute));if(n!==this.cwd){let c=this[_0](n,this.dmode);if(c)return this[Xo](c,e)}}let[r,s]=RR(()=>Mn.lstatSync(e.absolute));if(s&&(this.keep||this.newer&&s.mtime>e.mtime))return this[Gq](e);if(r||this[Hq](e,s))return this[qc](null,e);if(s.isDirectory()){if(e.type==="Directory"){let c=!this.noChmod&&e.mode&&(s.mode&4095)!==e.mode,[f]=c?RR(()=>{Mn.chmodSync(e.absolute,e.mode)}):[];return this[qc](f,e)}let[n]=RR(()=>Mn.rmdirSync(e.absolute));this[qc](n,e)}let[a]=e.absolute===this.cwd?[]:RR(()=>DIt(e.absolute));this[qc](a,e)}[jq](e,r){let s=e.mode&4095||this.fmode,a=f=>{let p;try{Mn.closeSync(n)}catch(h){p=h}(f||p)&&this[Xo](f||p,e),r()},n;try{n=Mn.openSync(e.absolute,TIe(e.size),s)}catch(f){return a(f)}let c=this.transform&&this.transform(e)||e;c!==e&&(c.on("error",f=>this[Xo](f,e)),e.pipe(c)),c.on("data",f=>{try{Mn.writeSync(n,f,0,f.length)}catch(p){a(p)}}),c.on("end",f=>{let p=null;if(e.mtime&&!this.noMtime){let h=e.atime||new Date,E=e.mtime;try{Mn.futimesSync(n,h,E)}catch(C){try{Mn.utimesSync(e.absolute,h,E)}catch{p=C}}}if(this[jv](e)){let h=this[qv](e),E=this[Gv](e);try{Mn.fchownSync(n,h,E)}catch(C){try{Mn.chownSync(e.absolute,h,E)}catch{p=p||C}}}a(p)})}[qq](e,r){let s=e.mode&4095||this.dmode,a=this[_0](e.absolute,s);if(a){this[Xo](a,e),r();return}if(e.mtime&&!this.noMtime)try{Mn.utimesSync(e.absolute,e.atime||new Date,e.mtime)}catch{}if(this[jv](e))try{Mn.chownSync(e.absolute,this[qv](e),this[Gv](e))}catch{}r(),e.resume()}[_0](e,r){try{return kIe.sync($l(e),{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cache:this.dirCache,cwd:this.cwd,mode:r})}catch(s){return s}}[NR](e,r,s,a){try{Mn[s+"Sync"](r,e.absolute),a(),e.resume()}catch(n){return this[Xo](n,e)}}};Vv.Sync=Wq;RIe.exports=Vv});var MIe=L((z$t,LIe)=>{"use strict";var xIt=SI(),OR=Yq(),NIe=Ie("fs"),OIe=jI(),FIe=Ie("path"),Vq=RI();LIe.exports=(t,e,r)=>{typeof t=="function"?(r=t,e=null,t={}):Array.isArray(t)&&(e=t,t={}),typeof e=="function"&&(r=e,e=null),e?e=Array.from(e):e=[];let s=xIt(t);if(s.sync&&typeof r=="function")throw new TypeError("callback not supported for sync tar functions");if(!s.file&&typeof r=="function")throw new TypeError("callback only supported with file option");return e.length&&kIt(s,e),s.file&&s.sync?QIt(s):s.file?TIt(s,r):s.sync?RIt(s):FIt(s)};var kIt=(t,e)=>{let r=new Map(e.map(n=>[Vq(n),!0])),s=t.filter,a=(n,c)=>{let f=c||FIe.parse(n).root||".",p=n===f?!1:r.has(n)?r.get(n):a(FIe.dirname(n),f);return r.set(n,p),p};t.filter=s?(n,c)=>s(n,c)&&a(Vq(n)):n=>a(Vq(n))},QIt=t=>{let e=new OR.Sync(t),r=t.file,s=NIe.statSync(r),a=t.maxReadSize||16*1024*1024;new OIe.ReadStreamSync(r,{readSize:a,size:s.size}).pipe(e)},TIt=(t,e)=>{let r=new OR(t),s=t.maxReadSize||16*1024*1024,a=t.file,n=new Promise((c,f)=>{r.on("error",f),r.on("close",c),NIe.stat(a,(p,h)=>{if(p)f(p);else{let E=new OIe.ReadStream(a,{readSize:s,size:h.size});E.on("error",f),E.pipe(r)}})});return e?n.then(e,e):n},RIt=t=>new OR.Sync(t),FIt=t=>new OR(t)});var _Ie=L(ks=>{"use strict";ks.c=ks.create=IEe();ks.r=ks.replace=Pq();ks.t=ks.list=vR();ks.u=ks.update=xEe();ks.x=ks.extract=MIe();ks.Pack=cR();ks.Unpack=Yq();ks.Parse=BR();ks.ReadEntry=YT();ks.WriteEntry=nq();ks.Header=TI();ks.Pax=KT();ks.types=_6()});var Kq,UIe,U0,Kv,Jv,HIe=Ct(()=>{Kq=et(Od()),UIe=Ie("worker_threads"),U0=Symbol("kTaskInfo"),Kv=class{constructor(e,r){this.fn=e;this.limit=(0,Kq.default)(r.poolSize)}run(e){return this.limit(()=>this.fn(e))}},Jv=class{constructor(e,r){this.source=e;this.workers=[];this.limit=(0,Kq.default)(r.poolSize),this.cleanupInterval=setInterval(()=>{if(this.limit.pendingCount===0&&this.limit.activeCount===0){let s=this.workers.pop();s?s.terminate():clearInterval(this.cleanupInterval)}},5e3).unref()}createWorker(){this.cleanupInterval.refresh();let e=new UIe.Worker(this.source,{eval:!0,execArgv:[...process.execArgv,"--unhandled-rejections=strict"]});return e.on("message",r=>{if(!e[U0])throw new Error("Assertion failed: Worker sent a result without having a task assigned");e[U0].resolve(r),e[U0]=null,e.unref(),this.workers.push(e)}),e.on("error",r=>{e[U0]?.reject(r),e[U0]=null}),e.on("exit",r=>{r!==0&&e[U0]?.reject(new Error(`Worker exited with code ${r}`)),e[U0]=null}),e}run(e){return this.limit(()=>{let r=this.workers.pop()??this.createWorker();return r.ref(),new Promise((s,a)=>{r[U0]={resolve:s,reject:a},r.postMessage(e)})})}}});var qIe=L((eer,jIe)=>{var Jq;jIe.exports.getContent=()=>(typeof Jq>"u"&&(Jq=Ie("zlib").brotliDecompressSync(Buffer.from("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","base64")).toString()),Jq)});var gs={};Vt(gs,{convertToZip:()=>MIt,convertToZipWorker:()=>Xq,extractArchiveTo:()=>KIe,getDefaultTaskPool:()=>YIe,getTaskPoolForConfiguration:()=>VIe,makeArchiveFromDirectory:()=>LIt});function NIt(t,e){switch(t){case"async":return new Kv(Xq,{poolSize:e});case"workers":return new Jv((0,Zq.getContent)(),{poolSize:e});default:throw new Error(`Assertion failed: Unknown value ${t} for taskPoolMode`)}}function YIe(){return typeof zq>"u"&&(zq=NIt("workers",ps.availableParallelism())),zq}function VIe(t){return typeof t>"u"?YIe():Vl(OIt,t,()=>{let e=t.get("taskPoolMode"),r=t.get("taskPoolConcurrency");switch(e){case"async":return new Kv(Xq,{poolSize:r});case"workers":return new Jv((0,Zq.getContent)(),{poolSize:r});default:throw new Error(`Assertion failed: Unknown value ${e} for taskPoolMode`)}})}async function Xq(t){let{tmpFile:e,tgz:r,compressionLevel:s,extractBufferOpts:a}=t,n=new hs(e,{create:!0,level:s,stats:el.makeDefaultStats()}),c=Buffer.from(r.buffer,r.byteOffset,r.byteLength);return await KIe(c,n,a),n.saveAndClose(),e}async function LIt(t,{baseFs:e=new Yn,prefixPath:r=vt.root,compressionLevel:s,inMemory:a=!1}={}){let n;if(a)n=new hs(null,{level:s});else{let f=await le.mktempPromise(),p=K.join(f,"archive.zip");n=new hs(p,{create:!0,level:s})}let c=K.resolve(vt.root,r);return await n.copyPromise(c,t,{baseFs:e,stableTime:!0,stableSort:!0}),n}async function MIt(t,e={}){let r=await le.mktempPromise(),s=K.join(r,"archive.zip"),a=e.compressionLevel??e.configuration?.get("compressionLevel")??"mixed",n={prefixPath:e.prefixPath,stripComponents:e.stripComponents};return await(e.taskPool??VIe(e.configuration)).run({tmpFile:s,tgz:t,compressionLevel:a,extractBufferOpts:n}),new hs(s,{level:e.compressionLevel})}async function*_It(t){let e=new WIe.default.Parse,r=new GIe.PassThrough({objectMode:!0,autoDestroy:!0,emitClose:!0});e.on("entry",s=>{r.write(s)}),e.on("error",s=>{r.destroy(s)}),e.on("close",()=>{r.destroyed||r.end()}),e.end(t);for await(let s of r){let a=s;yield a,a.resume()}}async function KIe(t,e,{stripComponents:r=0,prefixPath:s=vt.dot}={}){function a(n){if(n.path[0]==="/")return!0;let c=n.path.split(/\//g);return!!(c.some(f=>f==="..")||c.length<=r)}for await(let n of _It(t)){if(a(n))continue;let c=K.normalize(ue.toPortablePath(n.path)).replace(/\/$/,"").split(/\//g);if(c.length<=r)continue;let f=c.slice(r).join("/"),p=K.join(s,f),h=420;switch((n.type==="Directory"||(n.mode??0)&73)&&(h|=73),n.type){case"Directory":e.mkdirpSync(K.dirname(p),{chmod:493,utimes:[fi.SAFE_TIME,fi.SAFE_TIME]}),e.mkdirSync(p,{mode:h}),e.utimesSync(p,fi.SAFE_TIME,fi.SAFE_TIME);break;case"OldFile":case"File":e.mkdirpSync(K.dirname(p),{chmod:493,utimes:[fi.SAFE_TIME,fi.SAFE_TIME]}),e.writeFileSync(p,await GE(n),{mode:h}),e.utimesSync(p,fi.SAFE_TIME,fi.SAFE_TIME);break;case"SymbolicLink":e.mkdirpSync(K.dirname(p),{chmod:493,utimes:[fi.SAFE_TIME,fi.SAFE_TIME]}),e.symlinkSync(n.linkpath,p),e.lutimesSync(p,fi.SAFE_TIME,fi.SAFE_TIME);break}}return e}var GIe,WIe,Zq,zq,OIt,JIe=Ct(()=>{Ve();bt();rA();GIe=Ie("stream"),WIe=et(_Ie());HIe();kc();Zq=et(qIe());OIt=new WeakMap});var ZIe=L(($q,zIe)=>{(function(t,e){typeof $q=="object"?zIe.exports=e():typeof define=="function"&&define.amd?define(e):t.treeify=e()})($q,function(){function t(a,n){var c=n?"\u2514":"\u251C";return a?c+="\u2500 ":c+="\u2500\u2500\u2510",c}function e(a,n){var c=[];for(var f in a)a.hasOwnProperty(f)&&(n&&typeof a[f]=="function"||c.push(f));return c}function r(a,n,c,f,p,h,E){var C="",S=0,P,I,R=f.slice(0);if(R.push([n,c])&&f.length>0&&(f.forEach(function(U,W){W>0&&(C+=(U[1]?" ":"\u2502")+" "),!I&&U[0]===n&&(I=!0)}),C+=t(a,c)+a,p&&(typeof n!="object"||n instanceof Date)&&(C+=": "+n),I&&(C+=" (circular ref.)"),E(C)),!I&&typeof n=="object"){var N=e(n,h);N.forEach(function(U){P=++S===N.length,r(U,n[U],P,R,p,h,E)})}}var s={};return s.asLines=function(a,n,c,f){var p=typeof c!="function"?c:!1;r(".",a,!1,[],n,p,f||c)},s.asTree=function(a,n,c){var f="";return r(".",a,!1,[],n,c,function(p){f+=p+` `}),f},s})});var Qs={};Vt(Qs,{emitList:()=>UIt,emitTree:()=>tCe,treeNodeToJson:()=>eCe,treeNodeToTreeify:()=>$Ie});function $Ie(t,{configuration:e}){let r={},s=0,a=(n,c)=>{let f=Array.isArray(n)?n.entries():Object.entries(n);for(let[p,h]of f){if(!h)continue;let{label:E,value:C,children:S}=h,P=[];typeof E<"u"&&P.push(Kd(e,E,2)),typeof C<"u"&&P.push(Ut(e,C[0],C[1])),P.length===0&&P.push(Kd(e,`${p}`,2));let I=P.join(": ").trim(),R=`\0${s++}\0`,N=c[`${R}${I}`]={};typeof S<"u"&&a(S,N)}};if(typeof t.children>"u")throw new Error("The root node must only contain children");return a(t.children,r),r}function eCe(t){let e=r=>{if(typeof r.children>"u"){if(typeof r.value>"u")throw new Error("Assertion failed: Expected a value to be set if the children are missing");return Jd(r.value[0],r.value[1])}let s=Array.isArray(r.children)?r.children.entries():Object.entries(r.children??{}),a=Array.isArray(r.children)?[]:{};for(let[n,c]of s)c&&(a[HIt(n)]=e(c));return typeof r.value>"u"?a:{value:Jd(r.value[0],r.value[1]),children:a}};return e(t)}function UIt(t,{configuration:e,stdout:r,json:s}){let a=t.map(n=>({value:n}));tCe({children:a},{configuration:e,stdout:r,json:s})}function tCe(t,{configuration:e,stdout:r,json:s,separators:a=0}){if(s){let c=Array.isArray(t.children)?t.children.values():Object.values(t.children??{});for(let f of c)f&&r.write(`${JSON.stringify(eCe(f))} `);return}let n=(0,XIe.asTree)($Ie(t,{configuration:e}),!1,!1);if(n=n.replace(/\0[0-9]+\0/g,""),a>=1&&(n=n.replace(/^([├└]─)/gm,`\u2502 $1`).replace(/^│\n/,"")),a>=2)for(let c=0;c<2;++c)n=n.replace(/^([│ ].{2}[├│ ].{2}[^\n]+\n)(([│ ]).{2}[├└].{2}[^\n]*\n[│ ].{2}[│ ].{2}[├└]─)/gm,`$1$3 \u2502 $2`).replace(/^│\n/,"");if(a>=3)throw new Error("Only the first two levels are accepted by treeUtils.emitTree");r.write(n)}function HIt(t){return typeof t=="string"?t.replace(/^\0[0-9]+\0/,""):t}var XIe,rCe=Ct(()=>{XIe=et(ZIe());Qc()});var LR,nCe=Ct(()=>{LR=class{constructor(e){this.releaseFunction=e;this.map=new Map}addOrCreate(e,r){let s=this.map.get(e);if(typeof s<"u"){if(s.refCount<=0)throw new Error(`Race condition in RefCountedMap. While adding a new key the refCount is: ${s.refCount} for ${JSON.stringify(e)}`);return s.refCount++,{value:s.value,release:()=>this.release(e)}}else{let a=r();return this.map.set(e,{refCount:1,value:a}),{value:a,release:()=>this.release(e)}}}release(e){let r=this.map.get(e);if(!r)throw new Error(`Unbalanced calls to release. No known instances of: ${JSON.stringify(e)}`);let s=r.refCount;if(s<=0)throw new Error(`Unbalanced calls to release. Too many release vs alloc refcount would become: ${s-1} of ${JSON.stringify(e)}`);s==1?(this.map.delete(e),this.releaseFunction(r.value)):r.refCount--}}});function zv(t){let e=t.match(jIt);if(!e?.groups)throw new Error("Assertion failed: Expected the checksum to match the requested pattern");let r=e.groups.cacheVersion?parseInt(e.groups.cacheVersion):null;return{cacheKey:e.groups.cacheKey??null,cacheVersion:r,cacheSpec:e.groups.cacheSpec??null,hash:e.groups.hash}}var iCe,eG,tG,MR,Jr,jIt,rG=Ct(()=>{Ve();bt();bt();rA();iCe=Ie("crypto"),eG=et(Ie("fs"));nCe();Fc();E0();kc();Yo();tG=WE(process.env.YARN_CACHE_CHECKPOINT_OVERRIDE??process.env.YARN_CACHE_VERSION_OVERRIDE??9),MR=WE(process.env.YARN_CACHE_VERSION_OVERRIDE??10),Jr=class t{constructor(e,{configuration:r,immutable:s=r.get("enableImmutableCache"),check:a=!1}){this.markedFiles=new Set;this.mutexes=new Map;this.refCountedZipFsCache=new LR(e=>{e.discardAndClose()});this.cacheId=`-${(0,iCe.randomBytes)(8).toString("hex")}.tmp`;this.configuration=r,this.cwd=e,this.immutable=s,this.check=a;let{cacheSpec:n,cacheKey:c}=t.getCacheKey(r);this.cacheSpec=n,this.cacheKey=c}static async find(e,{immutable:r,check:s}={}){let a=new t(e.get("cacheFolder"),{configuration:e,immutable:r,check:s});return await a.setup(),a}static getCacheKey(e){let r=e.get("compressionLevel"),s=r!=="mixed"?`c${r}`:"";return{cacheKey:[MR,s].join(""),cacheSpec:s}}get mirrorCwd(){if(!this.configuration.get("enableMirror"))return null;let e=`${this.configuration.get("globalFolder")}/cache`;return e!==this.cwd?e:null}getVersionFilename(e){return`${rI(e)}-${this.cacheKey}.zip`}getChecksumFilename(e,r){let a=zv(r).hash.slice(0,10);return`${rI(e)}-${a}.zip`}isChecksumCompatible(e){if(e===null)return!1;let{cacheVersion:r,cacheSpec:s}=zv(e);if(r===null||r{let pe=new hs,Be=K.join(vt.root,j8(e));return pe.mkdirSync(Be,{recursive:!0}),pe.writeJsonSync(K.join(Be,Er.manifest),{name:cn(e),mocked:!0}),pe},E=async(pe,{isColdHit:Be,controlPath:Ce=null})=>{if(Ce===null&&c.unstablePackages?.has(e.locatorHash))return{isValid:!0,hash:null};let g=r&&!Be?zv(r).cacheKey:this.cacheKey,we=!c.skipIntegrityCheck||!r?`${g}/${await BQ(pe)}`:r;if(Ce!==null){let fe=!c.skipIntegrityCheck||!r?`${this.cacheKey}/${await BQ(Ce)}`:r;if(we!==fe)throw new Yt(18,"The remote archive doesn't match the local checksum - has the local cache been corrupted?")}let ye=null;switch(r!==null&&we!==r&&(this.check?ye="throw":zv(r).cacheKey!==zv(we).cacheKey?ye="update":ye=this.configuration.get("checksumBehavior")),ye){case null:case"update":return{isValid:!0,hash:we};case"ignore":return{isValid:!0,hash:r};case"reset":return{isValid:!1,hash:r};default:case"throw":throw new Yt(18,"The remote archive doesn't match the expected checksum")}},C=async pe=>{if(!n)throw new Error(`Cache check required but no loader configured for ${Yr(this.configuration,e)}`);let Be=await n(),Ce=Be.getRealPath();Be.saveAndClose(),await le.chmodPromise(Ce,420);let g=await E(pe,{controlPath:Ce,isColdHit:!1});if(!g.isValid)throw new Error("Assertion failed: Expected a valid checksum");return g.hash},S=async()=>{if(f===null||!await le.existsPromise(f)){let pe=await n(),Be=pe.getRealPath();return pe.saveAndClose(),{source:"loader",path:Be}}return{source:"mirror",path:f}},P=async()=>{if(!n)throw new Error(`Cache entry required but missing for ${Yr(this.configuration,e)}`);if(this.immutable)throw new Yt(56,`Cache entry required but missing for ${Yr(this.configuration,e)}`);let{path:pe,source:Be}=await S(),{hash:Ce}=await E(pe,{isColdHit:!0}),g=this.getLocatorPath(e,Ce),we=[];Be!=="mirror"&&f!==null&&we.push(async()=>{let fe=`${f}${this.cacheId}`;await le.copyFilePromise(pe,fe,eG.default.constants.COPYFILE_FICLONE),await le.chmodPromise(fe,420),await le.renamePromise(fe,f)}),(!c.mirrorWriteOnly||f===null)&&we.push(async()=>{let fe=`${g}${this.cacheId}`;await le.copyFilePromise(pe,fe,eG.default.constants.COPYFILE_FICLONE),await le.chmodPromise(fe,420),await le.renamePromise(fe,g)});let ye=c.mirrorWriteOnly?f??g:g;return await Promise.all(we.map(fe=>fe())),[!1,ye,Ce]},I=async()=>{let Be=(async()=>{let Ce=c.unstablePackages?.has(e.locatorHash),g=Ce||!r||this.isChecksumCompatible(r)?this.getLocatorPath(e,r):null,we=g!==null?this.markedFiles.has(g)||await p.existsPromise(g):!1,ye=!!c.mockedPackages?.has(e.locatorHash)&&(!this.check||!we),fe=ye||we,se=fe?s:a;if(se&&se(),fe){let X=null,De=g;if(!ye)if(this.check)X=await C(De);else{let Re=await E(De,{isColdHit:!1});if(Re.isValid)X=Re.hash;else return P()}return[ye,De,X]}else{if(this.immutable&&Ce)throw new Yt(56,`Cache entry required but missing for ${Yr(this.configuration,e)}; consider defining ${he.pretty(this.configuration,"supportedArchitectures",he.Type.CODE)} to cache packages for multiple systems`);return P()}})();this.mutexes.set(e.locatorHash,Be);try{return await Be}finally{this.mutexes.delete(e.locatorHash)}};for(let pe;pe=this.mutexes.get(e.locatorHash);)await pe;let[R,N,U]=await I();R||this.markedFiles.add(N);let W=()=>this.refCountedZipFsCache.addOrCreate(N,()=>R?h():new hs(N,{baseFs:p,readOnly:!0})),te,ie=new iE(()=>i3(()=>(te=W(),te.value),pe=>`Failed to open the cache entry for ${Yr(this.configuration,e)}: ${pe}`),K),Ae=new Hf(N,{baseFs:ie,pathUtils:K}),ce=()=>{te?.release()},me=c.unstablePackages?.has(e.locatorHash)?null:U;return[Ae,ce,me]}},jIt=/^(?:(?(?[0-9]+)(?.*))\/)?(?.*)$/});var _R,sCe=Ct(()=>{_R=(r=>(r[r.SCRIPT=0]="SCRIPT",r[r.SHELLCODE=1]="SHELLCODE",r))(_R||{})});var qIt,KI,nG=Ct(()=>{bt();Bc();Np();Yo();qIt=[[/^(git(?:\+(?:https|ssh))?:\/\/.*(?:\.git)?)#(.*)$/,(t,e,r,s)=>`${r}#commit=${s}`],[/^https:\/\/((?:[^/]+?)@)?codeload\.github\.com\/([^/]+\/[^/]+)\/tar\.gz\/([0-9a-f]+)$/,(t,e,r="",s,a)=>`https://${r}github.com/${s}.git#commit=${a}`],[/^https:\/\/((?:[^/]+?)@)?github\.com\/([^/]+\/[^/]+?)(?:\.git)?#([0-9a-f]+)$/,(t,e,r="",s,a)=>`https://${r}github.com/${s}.git#commit=${a}`],[/^https?:\/\/[^/]+\/(?:[^/]+\/)*(?:@.+(?:\/|(?:%2f)))?([^/]+)\/(?:-|download)\/\1-[^/]+\.tgz(?:#|$)/,t=>`npm:${t}`],[/^https:\/\/npm\.pkg\.github\.com\/download\/(?:@[^/]+)\/(?:[^/]+)\/(?:[^/]+)\/(?:[0-9a-f]+)(?:#|$)/,t=>`npm:${t}`],[/^https:\/\/npm\.fontawesome\.com\/(?:@[^/]+)\/([^/]+)\/-\/([^/]+)\/\1-\2.tgz(?:#|$)/,t=>`npm:${t}`],[/^https?:\/\/[^/]+\/.*\/(@[^/]+)\/([^/]+)\/-\/\1\/\2-(?:[.\d\w-]+)\.tgz(?:#|$)/,(t,e)=>PQ({protocol:"npm:",source:null,selector:t,params:{__archiveUrl:e}})],[/^[^/]+\.tgz#[0-9a-f]+$/,t=>`npm:${t}`]],KI=class{constructor(e){this.resolver=e;this.resolutions=null}async setup(e,{report:r}){let s=K.join(e.cwd,Er.lockfile);if(!le.existsSync(s))return;let a=await le.readFilePromise(s,"utf8"),n=cs(a);if(Object.hasOwn(n,"__metadata"))return;let c=this.resolutions=new Map;for(let f of Object.keys(n)){let p=JB(f);if(!p){r.reportWarning(14,`Failed to parse the string "${f}" into a proper descriptor`);continue}let h=ul(p.range)?On(p,`npm:${p.range}`):p,{version:E,resolved:C}=n[f];if(!C)continue;let S;for(let[I,R]of qIt){let N=C.match(I);if(N){S=R(E,...N);break}}if(!S){r.reportWarning(14,`${ni(e.configuration,h)}: Only some patterns can be imported from legacy lockfiles (not "${C}")`);continue}let P=h;try{let I=Xd(h.range),R=JB(I.selector,!0);R&&(P=R)}catch{}c.set(h.descriptorHash,Vs(P,S))}}supportsDescriptor(e,r){return this.resolutions?this.resolutions.has(e.descriptorHash):!1}supportsLocator(e,r){return!1}shouldPersistResolution(e,r){throw new Error("Assertion failed: This resolver doesn't support resolving locators to packages")}bindDescriptor(e,r,s){return e}getResolutionDependencies(e,r){return{}}async getCandidates(e,r,s){if(!this.resolutions)throw new Error("Assertion failed: The resolution store should have been setup");let a=this.resolutions.get(e.descriptorHash);if(!a)throw new Error("Assertion failed: The resolution should have been registered");let n=M8(a),c=s.project.configuration.normalizeDependency(n);return await this.resolver.getCandidates(c,r,s)}async getSatisfying(e,r,s,a){let[n]=await this.getCandidates(e,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(e,r){throw new Error("Assertion failed: This resolver doesn't support resolving locators to packages")}}});var uA,oCe=Ct(()=>{Fc();xv();Qc();uA=class extends ho{constructor({configuration:r,stdout:s,suggestInstall:a=!0}){super();this.errorCount=0;HB(this,{configuration:r}),this.configuration=r,this.stdout=s,this.suggestInstall=a}static async start(r,s){let a=new this(r);try{await s(a)}catch(n){a.reportExceptionOnce(n)}finally{await a.finalize()}return a}hasErrors(){return this.errorCount>0}exitCode(){return this.hasErrors()?1:0}reportCacheHit(r){}reportCacheMiss(r){}startSectionSync(r,s){return s()}async startSectionPromise(r,s){return await s()}startTimerSync(r,s,a){return(typeof s=="function"?s:a)()}async startTimerPromise(r,s,a){return await(typeof s=="function"?s:a)()}reportSeparator(){}reportInfo(r,s){}reportWarning(r,s){}reportError(r,s){this.errorCount+=1,this.stdout.write(`${Ut(this.configuration,"\u27A4","redBright")} ${this.formatNameWithHyperlink(r)}: ${s} `)}reportProgress(r){return{...Promise.resolve().then(async()=>{for await(let{}of r);}),stop:()=>{}}}reportJson(r){}reportFold(r,s){}async finalize(){this.errorCount>0&&(this.stdout.write(` `),this.stdout.write(`${Ut(this.configuration,"\u27A4","redBright")} Errors happened when preparing the environment required to run this command. `),this.suggestInstall&&this.stdout.write(`${Ut(this.configuration,"\u27A4","redBright")} This might be caused by packages being missing from the lockfile, in which case running "yarn install" might help. `))}formatNameWithHyperlink(r){return u6(r,{configuration:this.configuration,json:!1})}}});var JI,iG=Ct(()=>{Yo();JI=class{constructor(e){this.resolver=e}supportsDescriptor(e,r){return!!(r.project.storedResolutions.get(e.descriptorHash)||r.project.originalPackages.has(SQ(e).locatorHash))}supportsLocator(e,r){return!!(r.project.originalPackages.has(e.locatorHash)&&!r.project.lockfileNeedsRefresh)}shouldPersistResolution(e,r){throw new Error("The shouldPersistResolution method shouldn't be called on the lockfile resolver, which would always answer yes")}bindDescriptor(e,r,s){return e}getResolutionDependencies(e,r){return this.resolver.getResolutionDependencies(e,r)}async getCandidates(e,r,s){let a=s.project.storedResolutions.get(e.descriptorHash);if(a){let c=s.project.originalPackages.get(a);if(c)return[c]}let n=s.project.originalPackages.get(SQ(e).locatorHash);if(n)return[n];throw new Error("Resolution expected from the lockfile data")}async getSatisfying(e,r,s,a){let[n]=await this.getCandidates(e,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(e,r){let s=r.project.originalPackages.get(e.locatorHash);if(!s)throw new Error("The lockfile resolver isn't meant to resolve packages - they should already have been stored into a cache");return s}}});function Zp(){}function GIt(t,e,r,s,a){for(var n=0,c=e.length,f=0,p=0;nP.length?R:P}),h.value=t.join(E)}else h.value=t.join(r.slice(f,f+h.count));f+=h.count,h.added||(p+=h.count)}}var S=e[c-1];return c>1&&typeof S.value=="string"&&(S.added||S.removed)&&t.equals("",S.value)&&(e[c-2].value+=S.value,e.pop()),e}function WIt(t){return{newPos:t.newPos,components:t.components.slice(0)}}function YIt(t,e){if(typeof t=="function")e.callback=t;else if(t)for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r]);return e}function cCe(t,e,r){return r=YIt(r,{ignoreWhitespace:!0}),cG.diff(t,e,r)}function VIt(t,e,r){return uG.diff(t,e,r)}function UR(t){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?UR=function(e){return typeof e}:UR=function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},UR(t)}function sG(t){return zIt(t)||ZIt(t)||XIt(t)||$It()}function zIt(t){if(Array.isArray(t))return oG(t)}function ZIt(t){if(typeof Symbol<"u"&&Symbol.iterator in Object(t))return Array.from(t)}function XIt(t,e){if(t){if(typeof t=="string")return oG(t,e);var r=Object.prototype.toString.call(t).slice(8,-1);if(r==="Object"&&t.constructor&&(r=t.constructor.name),r==="Map"||r==="Set")return Array.from(t);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return oG(t,e)}}function oG(t,e){(e==null||e>t.length)&&(e=t.length);for(var r=0,s=new Array(e);r"u"&&(c.context=4);var f=VIt(r,s,c);if(!f)return;f.push({value:"",lines:[]});function p(U){return U.map(function(W){return" "+W})}for(var h=[],E=0,C=0,S=[],P=1,I=1,R=function(W){var te=f[W],ie=te.lines||te.value.replace(/\n$/,"").split(` `);if(te.lines=ie,te.added||te.removed){var Ae;if(!E){var ce=f[W-1];E=P,C=I,ce&&(S=c.context>0?p(ce.lines.slice(-c.context)):[],E-=S.length,C-=S.length)}(Ae=S).push.apply(Ae,sG(ie.map(function(fe){return(te.added?"+":"-")+fe}))),te.added?I+=ie.length:P+=ie.length}else{if(E)if(ie.length<=c.context*2&&W=f.length-2&&ie.length<=c.context){var g=/\n$/.test(r),we=/\n$/.test(s),ye=ie.length==0&&S.length>Ce.oldLines;!g&&ye&&r.length>0&&S.splice(Ce.oldLines,0,"\\ No newline at end of file"),(!g&&!ye||!we)&&S.push("\\ No newline at end of file")}h.push(Ce),E=0,C=0,S=[]}P+=ie.length,I+=ie.length}},N=0;N{Zp.prototype={diff:function(e,r){var s=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{},a=s.callback;typeof s=="function"&&(a=s,s={}),this.options=s;var n=this;function c(R){return a?(setTimeout(function(){a(void 0,R)},0),!0):R}e=this.castInput(e),r=this.castInput(r),e=this.removeEmpty(this.tokenize(e)),r=this.removeEmpty(this.tokenize(r));var f=r.length,p=e.length,h=1,E=f+p;s.maxEditLength&&(E=Math.min(E,s.maxEditLength));var C=[{newPos:-1,components:[]}],S=this.extractCommon(C[0],r,e,0);if(C[0].newPos+1>=f&&S+1>=p)return c([{value:this.join(r),count:r.length}]);function P(){for(var R=-1*h;R<=h;R+=2){var N=void 0,U=C[R-1],W=C[R+1],te=(W?W.newPos:0)-R;U&&(C[R-1]=void 0);var ie=U&&U.newPos+1=f&&te+1>=p)return c(GIt(n,N.components,r,e,n.useLongestToken));C[R]=N}h++}if(a)(function R(){setTimeout(function(){if(h>E)return a();P()||R()},0)})();else for(;h<=E;){var I=P();if(I)return I}},pushComponent:function(e,r,s){var a=e[e.length-1];a&&a.added===r&&a.removed===s?e[e.length-1]={count:a.count+1,added:r,removed:s}:e.push({count:1,added:r,removed:s})},extractCommon:function(e,r,s,a){for(var n=r.length,c=s.length,f=e.newPos,p=f-a,h=0;f+1"u"?r:c}:s;return typeof t=="string"?t:JSON.stringify(aG(t,null,null,a),a," ")};Zv.equals=function(t,e){return Zp.prototype.equals.call(Zv,t.replace(/,([\r\n])/g,"$1"),e.replace(/,([\r\n])/g,"$1"))};lG=new Zp;lG.tokenize=function(t){return t.slice()};lG.join=lG.removeEmpty=function(t){return t}});var ACe=L((Ter,fCe)=>{var tCt=xc(),rCt=oI(),nCt=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,iCt=/^\w*$/;function sCt(t,e){if(tCt(t))return!1;var r=typeof t;return r=="number"||r=="symbol"||r=="boolean"||t==null||rCt(t)?!0:iCt.test(t)||!nCt.test(t)||e!=null&&t in Object(e)}fCe.exports=sCt});var gCe=L((Rer,hCe)=>{var pCe=bk(),oCt="Expected a function";function AG(t,e){if(typeof t!="function"||e!=null&&typeof e!="function")throw new TypeError(oCt);var r=function(){var s=arguments,a=e?e.apply(this,s):s[0],n=r.cache;if(n.has(a))return n.get(a);var c=t.apply(this,s);return r.cache=n.set(a,c)||n,c};return r.cache=new(AG.Cache||pCe),r}AG.Cache=pCe;hCe.exports=AG});var mCe=L((Fer,dCe)=>{var aCt=gCe(),lCt=500;function cCt(t){var e=aCt(t,function(s){return r.size===lCt&&r.clear(),s}),r=e.cache;return e}dCe.exports=cCt});var pG=L((Ner,yCe)=>{var uCt=mCe(),fCt=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,ACt=/\\(\\)?/g,pCt=uCt(function(t){var e=[];return t.charCodeAt(0)===46&&e.push(""),t.replace(fCt,function(r,s,a,n){e.push(a?n.replace(ACt,"$1"):s||r)}),e});yCe.exports=pCt});var Im=L((Oer,ECe)=>{var hCt=xc(),gCt=ACe(),dCt=pG(),mCt=bv();function yCt(t,e){return hCt(t)?t:gCt(t,e)?[t]:dCt(mCt(t))}ECe.exports=yCt});var zI=L((Ler,ICe)=>{var ECt=oI(),ICt=1/0;function CCt(t){if(typeof t=="string"||ECt(t))return t;var e=t+"";return e=="0"&&1/t==-ICt?"-0":e}ICe.exports=CCt});var HR=L((Mer,CCe)=>{var wCt=Im(),BCt=zI();function vCt(t,e){e=wCt(e,t);for(var r=0,s=e.length;t!=null&&r{var SCt=qk(),DCt=Im(),bCt=kB(),wCe=Wl(),PCt=zI();function xCt(t,e,r,s){if(!wCe(t))return t;e=DCt(e,t);for(var a=-1,n=e.length,c=n-1,f=t;f!=null&&++a{var kCt=HR(),QCt=hG(),TCt=Im();function RCt(t,e,r){for(var s=-1,a=e.length,n={};++s{function FCt(t,e){return t!=null&&e in Object(t)}DCe.exports=FCt});var gG=L((jer,PCe)=>{var NCt=Im(),OCt=bB(),LCt=xc(),MCt=kB(),_Ct=Tk(),UCt=zI();function HCt(t,e,r){e=NCt(e,t);for(var s=-1,a=e.length,n=!1;++s{var jCt=bCe(),qCt=gG();function GCt(t,e){return t!=null&&qCt(t,e,jCt)}xCe.exports=GCt});var TCe=L((Ger,QCe)=>{var WCt=SCe(),YCt=kCe();function VCt(t,e){return WCt(t,e,function(r,s){return YCt(t,s)})}QCe.exports=VCt});var OCe=L((Wer,NCe)=>{var RCe=Gd(),KCt=bB(),JCt=xc(),FCe=RCe?RCe.isConcatSpreadable:void 0;function zCt(t){return JCt(t)||KCt(t)||!!(FCe&&t&&t[FCe])}NCe.exports=zCt});var _Ce=L((Yer,MCe)=>{var ZCt=kk(),XCt=OCe();function LCe(t,e,r,s,a){var n=-1,c=t.length;for(r||(r=XCt),a||(a=[]);++n0&&r(f)?e>1?LCe(f,e-1,r,s,a):ZCt(a,f):s||(a[a.length]=f)}return a}MCe.exports=LCe});var HCe=L((Ver,UCe)=>{var $Ct=_Ce();function ewt(t){var e=t==null?0:t.length;return e?$Ct(t,1):[]}UCe.exports=ewt});var dG=L((Ker,jCe)=>{var twt=HCe(),rwt=J4(),nwt=z4();function iwt(t){return nwt(rwt(t,void 0,twt),t+"")}jCe.exports=iwt});var mG=L((Jer,qCe)=>{var swt=TCe(),owt=dG(),awt=owt(function(t,e){return t==null?{}:swt(t,e)});qCe.exports=awt});var jR,GCe=Ct(()=>{Fc();jR=class{constructor(e){this.resolver=e}supportsDescriptor(e,r){return this.resolver.supportsDescriptor(e,r)}supportsLocator(e,r){return this.resolver.supportsLocator(e,r)}shouldPersistResolution(e,r){return this.resolver.shouldPersistResolution(e,r)}bindDescriptor(e,r,s){return this.resolver.bindDescriptor(e,r,s)}getResolutionDependencies(e,r){return this.resolver.getResolutionDependencies(e,r)}async getCandidates(e,r,s){throw new Yt(20,`This package doesn't seem to be present in your lockfile; run "yarn install" to update the lockfile`)}async getSatisfying(e,r,s,a){throw new Yt(20,`This package doesn't seem to be present in your lockfile; run "yarn install" to update the lockfile`)}async resolve(e,r){throw new Yt(20,`This package doesn't seem to be present in your lockfile; run "yarn install" to update the lockfile`)}}});var Yi,yG=Ct(()=>{Fc();Yi=class extends ho{reportCacheHit(e){}reportCacheMiss(e){}startSectionSync(e,r){return r()}async startSectionPromise(e,r){return await r()}startTimerSync(e,r,s){return(typeof r=="function"?r:s)()}async startTimerPromise(e,r,s){return await(typeof r=="function"?r:s)()}reportSeparator(){}reportInfo(e,r){}reportWarning(e,r){}reportError(e,r){}reportProgress(e){return{...Promise.resolve().then(async()=>{for await(let{}of e);}),stop:()=>{}}}reportJson(e){}reportFold(e,r){}async finalize(){}}});var WCe,ZI,EG=Ct(()=>{bt();WCe=et(CQ());sI();$d();Qc();E0();Np();Yo();ZI=class{constructor(e,{project:r}){this.workspacesCwds=new Set;this.project=r,this.cwd=e}async setup(){this.manifest=await Ht.tryFind(this.cwd)??new Ht,this.relativeCwd=K.relative(this.project.cwd,this.cwd)||vt.dot;let e=this.manifest.name?this.manifest.name:ba(null,`${this.computeCandidateName()}-${fs(this.relativeCwd).substring(0,6)}`);this.anchoredDescriptor=On(e,`${Ei.protocol}${this.relativeCwd}`),this.anchoredLocator=Vs(e,`${Ei.protocol}${this.relativeCwd}`);let r=this.manifest.workspaceDefinitions.map(({pattern:a})=>a);if(r.length===0)return;let s=await(0,WCe.default)(r,{cwd:ue.fromPortablePath(this.cwd),onlyDirectories:!0,ignore:["**/node_modules","**/.git","**/.yarn"]});s.sort(),await s.reduce(async(a,n)=>{let c=K.resolve(this.cwd,ue.toPortablePath(n)),f=await le.existsPromise(K.join(c,"package.json"));await a,f&&this.workspacesCwds.add(c)},Promise.resolve())}get anchoredPackage(){let e=this.project.storedPackages.get(this.anchoredLocator.locatorHash);if(!e)throw new Error(`Assertion failed: Expected workspace ${ZB(this.project.configuration,this)} (${Ut(this.project.configuration,K.join(this.cwd,Er.manifest),pt.PATH)}) to have been resolved. Run "yarn install" to update the lockfile`);return e}accepts(e){let r=e.indexOf(":"),s=r!==-1?e.slice(0,r+1):null,a=r!==-1?e.slice(r+1):e;if(s===Ei.protocol&&K.normalize(a)===this.relativeCwd||s===Ei.protocol&&(a==="*"||a==="^"||a==="~"))return!0;let n=ul(a);return n?s===Ei.protocol?n.test(this.manifest.version??"0.0.0"):this.project.configuration.get("enableTransparentWorkspaces")&&this.manifest.version!==null?n.test(this.manifest.version):!1:!1}computeCandidateName(){return this.cwd===this.project.cwd?"root-workspace":`${K.basename(this.cwd)}`||"unnamed-workspace"}getRecursiveWorkspaceDependencies({dependencies:e=Ht.hardDependencies}={}){let r=new Set,s=a=>{for(let n of e)for(let c of a.manifest[n].values()){let f=this.project.tryWorkspaceByDescriptor(c);f===null||r.has(f)||(r.add(f),s(f))}};return s(this),r}getRecursiveWorkspaceDependents({dependencies:e=Ht.hardDependencies}={}){let r=new Set,s=a=>{for(let n of this.project.workspaces)e.some(f=>[...n.manifest[f].values()].some(p=>{let h=this.project.tryWorkspaceByDescriptor(p);return h!==null&&KB(h.anchoredLocator,a.anchoredLocator)}))&&!r.has(n)&&(r.add(n),s(n))};return s(this),r}getRecursiveWorkspaceChildren(){let e=new Set([this]);for(let r of e)for(let s of r.workspacesCwds){let a=this.project.workspacesByCwd.get(s);a&&e.add(a)}return e.delete(this),Array.from(e)}async persistManifest(){let e={};this.manifest.exportTo(e);let r=K.join(this.cwd,Ht.fileName),s=`${JSON.stringify(e,null,this.manifest.indent)} `;await le.changeFilePromise(r,s,{automaticNewlines:!0}),this.manifest.raw=e}}});function pwt({project:t,allDescriptors:e,allResolutions:r,allPackages:s,accessibleLocators:a=new Set,optionalBuilds:n=new Set,peerRequirements:c=new Map,peerWarnings:f=[],peerRequirementNodes:p=new Map,volatileDescriptors:h=new Set}){let E=new Map,C=[],S=new Map,P=new Map,I=new Map,R=new Map,N=new Map,U=new Map(t.workspaces.map(ce=>{let me=ce.anchoredLocator.locatorHash,pe=s.get(me);if(typeof pe>"u")throw new Error("Assertion failed: The workspace should have an associated package");return[me,WB(pe)]})),W=()=>{let ce=le.mktempSync(),me=K.join(ce,"stacktrace.log"),pe=String(C.length+1).length,Be=C.map((Ce,g)=>`${`${g+1}.`.padStart(pe," ")} ${cl(Ce)} `).join("");throw le.writeFileSync(me,Be),le.detachTemp(ce),new Yt(45,`Encountered a stack overflow when resolving peer dependencies; cf ${ue.fromPortablePath(me)}`)},te=ce=>{let me=r.get(ce.descriptorHash);if(typeof me>"u")throw new Error("Assertion failed: The resolution should have been registered");let pe=s.get(me);if(!pe)throw new Error("Assertion failed: The package could not be found");return pe},ie=(ce,me,pe,{top:Be,optional:Ce})=>{C.length>1e3&&W(),C.push(me);let g=Ae(ce,me,pe,{top:Be,optional:Ce});return C.pop(),g},Ae=(ce,me,pe,{top:Be,optional:Ce})=>{if(Ce||n.delete(me.locatorHash),a.has(me.locatorHash))return;a.add(me.locatorHash);let g=s.get(me.locatorHash);if(!g)throw new Error(`Assertion failed: The package (${Yr(t.configuration,me)}) should have been registered`);let we=new Set,ye=new Map,fe=[],se=[],X=[],De=[];for(let Re of Array.from(g.dependencies.values())){if(g.peerDependencies.has(Re.identHash)&&g.locatorHash!==Be)continue;if(Tp(Re))throw new Error("Assertion failed: Virtual packages shouldn't be encountered when virtualizing a branch");h.delete(Re.descriptorHash);let dt=Ce;if(!dt){let ke=g.dependenciesMeta.get(cn(Re));if(typeof ke<"u"){let it=ke.get(null);typeof it<"u"&&it.optional&&(dt=!0)}}let j=r.get(Re.descriptorHash);if(!j)throw new Error(`Assertion failed: The resolution (${ni(t.configuration,Re)}) should have been registered`);let rt=U.get(j)||s.get(j);if(!rt)throw new Error(`Assertion failed: The package (${j}, resolved from ${ni(t.configuration,Re)}) should have been registered`);if(rt.peerDependencies.size===0){ie(Re,rt,new Map,{top:Be,optional:dt});continue}let Fe,Ne,Pe=new Set,Ye=new Map;fe.push(()=>{Fe=U8(Re,me.locatorHash),Ne=H8(rt,me.locatorHash),g.dependencies.set(Re.identHash,Fe),r.set(Fe.descriptorHash,Ne.locatorHash),e.set(Fe.descriptorHash,Fe),s.set(Ne.locatorHash,Ne),xp(R,Ne.locatorHash).add(Fe.descriptorHash),we.add(Ne.locatorHash)}),se.push(()=>{N.set(Ne.locatorHash,Ye);for(let ke of Ne.peerDependencies.values()){let _e=Vl(ye,ke.identHash,()=>{let x=pe.get(ke.identHash)??null,w=g.dependencies.get(ke.identHash);return!w&&VB(me,ke)&&(ce.identHash===me.identHash?w=ce:(w=On(me,ce.range),e.set(w.descriptorHash,w),r.set(w.descriptorHash,me.locatorHash),h.delete(w.descriptorHash),x=null)),w||(w=On(ke,"missing:")),{subject:me,ident:ke,provided:w,root:!x,requests:new Map,hash:`p${fs(me.locatorHash,ke.identHash).slice(0,5)}`}}).provided;if(_e.range==="missing:"&&Ne.dependencies.has(ke.identHash)){Ne.peerDependencies.delete(ke.identHash);continue}if(Ye.set(ke.identHash,{requester:Ne,descriptor:ke,meta:Ne.peerDependenciesMeta.get(cn(ke)),children:new Map}),Ne.dependencies.set(ke.identHash,_e),Tp(_e)){let x=r.get(_e.descriptorHash);xp(I,x).add(Ne.locatorHash)}S.set(_e.identHash,_e),_e.range==="missing:"&&Pe.add(_e.identHash)}Ne.dependencies=new Map(Ys(Ne.dependencies,([ke,it])=>cn(it)))}),X.push(()=>{if(!s.has(Ne.locatorHash))return;let ke=E.get(rt.locatorHash);typeof ke=="number"&&ke>=2&&W();let it=E.get(rt.locatorHash),_e=typeof it<"u"?it+1:1;E.set(rt.locatorHash,_e),ie(Fe,Ne,Ye,{top:Be,optional:dt}),E.set(rt.locatorHash,_e-1)}),De.push(()=>{let ke=r.get(Fe.descriptorHash);if(typeof ke>"u")throw new Error("Assertion failed: Expected the descriptor to be registered");let it=N.get(ke);if(typeof it>"u")throw new Error("Assertion failed: Expected the peer requests to be registered");for(let _e of ye.values()){let x=it.get(_e.ident.identHash);x&&(_e.requests.set(Fe.descriptorHash,x),p.set(_e.hash,_e),_e.root||pe.get(_e.ident.identHash)?.children.set(Fe.descriptorHash,x))}if(s.has(Ne.locatorHash))for(let _e of Pe)Ne.dependencies.delete(_e)})}for(let Re of[...fe,...se])Re();for(let Re of we){we.delete(Re);let dt=s.get(Re),j=fs(tI(dt).locatorHash,...Array.from(dt.dependencies.values(),Pe=>{let Ye=Pe.range!=="missing:"?r.get(Pe.descriptorHash):"missing:";if(typeof Ye>"u")throw new Error(`Assertion failed: Expected the resolution for ${ni(t.configuration,Pe)} to have been registered`);return Ye===Be?`${Ye} (top)`:Ye})),rt=P.get(j);if(typeof rt>"u"){P.set(j,dt);continue}let Fe=xp(R,rt.locatorHash);for(let Pe of R.get(dt.locatorHash)??[])r.set(Pe,rt.locatorHash),Fe.add(Pe);s.delete(dt.locatorHash),a.delete(dt.locatorHash),we.delete(dt.locatorHash);let Ne=I.get(dt.locatorHash);if(Ne!==void 0){let Pe=xp(I,rt.locatorHash);for(let Ye of Ne)Pe.add(Ye),we.add(Ye)}}for(let Re of[...X,...De])Re()};for(let ce of t.workspaces){let me=ce.anchoredLocator;h.delete(ce.anchoredDescriptor.descriptorHash),ie(ce.anchoredDescriptor,me,new Map,{top:me.locatorHash,optional:!1})}for(let ce of p.values()){if(!ce.root)continue;let me=s.get(ce.subject.locatorHash);if(typeof me>"u")continue;for(let Be of ce.requests.values()){let Ce=`p${fs(ce.subject.locatorHash,cn(ce.ident),Be.requester.locatorHash).slice(0,5)}`;c.set(Ce,{subject:ce.subject.locatorHash,requested:ce.ident,rootRequester:Be.requester.locatorHash,allRequesters:Array.from(XB(Be),g=>g.requester.locatorHash)})}let pe=[...XB(ce)];if(ce.provided.range!=="missing:"){let Be=te(ce.provided),Ce=Be.version??"0.0.0",g=ye=>{if(ye.startsWith(Ei.protocol)){if(!t.tryWorkspaceByLocator(Be))return null;ye=ye.slice(Ei.protocol.length),(ye==="^"||ye==="~")&&(ye="*")}return ye},we=!0;for(let ye of pe){let fe=g(ye.descriptor.range);if(fe===null){we=!1;continue}if(!eA(Ce,fe)){we=!1;let se=`p${fs(ce.subject.locatorHash,cn(ce.ident),ye.requester.locatorHash).slice(0,5)}`;f.push({type:1,subject:me,requested:ce.ident,requester:ye.requester,version:Ce,hash:se,requirementCount:pe.length})}}if(!we){let ye=pe.map(fe=>g(fe.descriptor.range));f.push({type:3,node:ce,range:ye.includes(null)?null:G8(ye),hash:ce.hash})}}else{let Be=!0;for(let Ce of pe)if(!Ce.meta?.optional){Be=!1;let g=`p${fs(ce.subject.locatorHash,cn(ce.ident),Ce.requester.locatorHash).slice(0,5)}`;f.push({type:0,subject:me,requested:ce.ident,requester:Ce.requester,hash:g})}Be||f.push({type:2,node:ce,hash:ce.hash})}}}function*hwt(t){let e=new Map;if("children"in t)e.set(t,t);else for(let r of t.requests.values())e.set(r,r);for(let[r,s]of e){yield{request:r,root:s};for(let a of r.children.values())e.has(a)||e.set(a,s)}}function gwt(t,e){let r=[],s=[],a=!1;for(let n of t.peerWarnings)if(!(n.type===1||n.type===0)){if(!t.tryWorkspaceByLocator(n.node.subject)){a=!0;continue}if(n.type===3){let c=t.storedResolutions.get(n.node.provided.descriptorHash);if(typeof c>"u")throw new Error("Assertion failed: Expected the descriptor to be registered");let f=t.storedPackages.get(c);if(typeof f>"u")throw new Error("Assertion failed: Expected the package to be registered");let p=A0(hwt(n.node),({request:C,root:S})=>eA(f.version??"0.0.0",C.descriptor.range)?A0.skip:C===S?es(t.configuration,C.requester):`${es(t.configuration,C.requester)} (via ${es(t.configuration,S.requester)})`),h=[...XB(n.node)].length>1?"and other dependencies request":"requests",E=n.range?nI(t.configuration,n.range):Ut(t.configuration,"but they have non-overlapping ranges!","redBright");r.push(`${es(t.configuration,n.node.ident)} is listed by your project with version ${zB(t.configuration,f.version??"0.0.0")} (${Ut(t.configuration,n.hash,pt.CODE)}), which doesn't satisfy what ${p} ${h} (${E}).`)}if(n.type===2){let c=n.node.requests.size>1?" and other dependencies":"";s.push(`${Yr(t.configuration,n.node.subject)} doesn't provide ${es(t.configuration,n.node.ident)} (${Ut(t.configuration,n.hash,pt.CODE)}), requested by ${es(t.configuration,n.node.requests.values().next().value.requester)}${c}.`)}}e.startSectionSync({reportFooter:()=>{e.reportWarning(86,`Some peer dependencies are incorrectly met by your project; run ${Ut(t.configuration,"yarn explain peer-requirements ",pt.CODE)} for details, where ${Ut(t.configuration,"",pt.CODE)} is the six-letter p-prefixed code.`)},skipIfEmpty:!0},()=>{for(let n of Ys(r,c=>VE.default(c)))e.reportWarning(60,n);for(let n of Ys(s,c=>VE.default(c)))e.reportWarning(2,n)}),a&&e.reportWarning(86,`Some peer dependencies are incorrectly met by dependencies; run ${Ut(t.configuration,"yarn explain peer-requirements",pt.CODE)} for details.`)}var qR,GR,WR,KCe,wG,CG,BG,YR,lwt,cwt,YCe,uwt,fwt,Awt,ec,IG,VR,VCe,Tt,JCe=Ct(()=>{bt();bt();Bc();Wt();qR=Ie("crypto");fG();GR=et(mG()),WR=et(Od()),KCe=et(Ai()),wG=Ie("util"),CG=et(Ie("v8")),BG=et(Ie("zlib"));rG();dv();nG();iG();sI();J8();Fc();GCe();xv();yG();$d();EG();NQ();Qc();E0();kc();pT();g6();Np();Yo();YR=WE(process.env.YARN_LOCKFILE_VERSION_OVERRIDE??8),lwt=3,cwt=/ *, */g,YCe=/\/$/,uwt=32,fwt=(0,wG.promisify)(BG.default.gzip),Awt=(0,wG.promisify)(BG.default.gunzip),ec=(r=>(r.UpdateLockfile="update-lockfile",r.SkipBuild="skip-build",r))(ec||{}),IG={restoreLinkersCustomData:["linkersCustomData"],restoreResolutions:["accessibleLocators","conditionalLocators","disabledLocators","optionalBuilds","storedDescriptors","storedResolutions","storedPackages","lockFileChecksum"],restoreBuildState:["skippedBuilds","storedBuildState"]},VR=(a=>(a[a.NotProvided=0]="NotProvided",a[a.NotCompatible=1]="NotCompatible",a[a.NodeNotProvided=2]="NodeNotProvided",a[a.NodeNotCompatible=3]="NodeNotCompatible",a))(VR||{}),VCe=t=>fs(`${lwt}`,t),Tt=class t{constructor(e,{configuration:r}){this.resolutionAliases=new Map;this.workspaces=[];this.workspacesByCwd=new Map;this.workspacesByIdent=new Map;this.storedResolutions=new Map;this.storedDescriptors=new Map;this.storedPackages=new Map;this.storedChecksums=new Map;this.storedBuildState=new Map;this.accessibleLocators=new Set;this.conditionalLocators=new Set;this.disabledLocators=new Set;this.originalPackages=new Map;this.optionalBuilds=new Set;this.skippedBuilds=new Set;this.lockfileLastVersion=null;this.lockfileNeedsRefresh=!1;this.peerRequirements=new Map;this.peerWarnings=[];this.peerRequirementNodes=new Map;this.linkersCustomData=new Map;this.lockFileChecksum=null;this.installStateChecksum=null;this.configuration=r,this.cwd=e}static async find(e,r){if(!e.projectCwd)throw new nt(`No project found in ${r}`);let s=e.projectCwd,a=r,n=null;for(;n!==e.projectCwd;){if(n=a,le.existsSync(K.join(n,Er.manifest))){s=n;break}a=K.dirname(n)}let c=new t(e.projectCwd,{configuration:e});ze.telemetry?.reportProject(c.cwd),await c.setupResolutions(),await c.setupWorkspaces(),ze.telemetry?.reportWorkspaceCount(c.workspaces.length),ze.telemetry?.reportDependencyCount(c.workspaces.reduce((I,R)=>I+R.manifest.dependencies.size+R.manifest.devDependencies.size,0));let f=c.tryWorkspaceByCwd(s);if(f)return{project:c,workspace:f,locator:f.anchoredLocator};let p=await c.findLocatorForLocation(`${s}/`,{strict:!0});if(p)return{project:c,locator:p,workspace:null};let h=Ut(e,c.cwd,pt.PATH),E=Ut(e,K.relative(c.cwd,s),pt.PATH),C=`- If ${h} isn't intended to be a project, remove any yarn.lock and/or package.json file there.`,S=`- If ${h} is intended to be a project, it might be that you forgot to list ${E} in its workspace configuration.`,P=`- Finally, if ${h} is fine and you intend ${E} to be treated as a completely separate project (not even a workspace), create an empty yarn.lock file in it.`;throw new nt(`The nearest package directory (${Ut(e,s,pt.PATH)}) doesn't seem to be part of the project declared in ${Ut(e,c.cwd,pt.PATH)}. ${[C,S,P].join(` `)}`)}async setupResolutions(){this.storedResolutions=new Map,this.storedDescriptors=new Map,this.storedPackages=new Map,this.lockFileChecksum=null;let e=K.join(this.cwd,Er.lockfile),r=this.configuration.get("defaultLanguageName");if(le.existsSync(e)){let s=await le.readFilePromise(e,"utf8");this.lockFileChecksum=VCe(s);let a=cs(s);if(a.__metadata){let n=a.__metadata.version,c=a.__metadata.cacheKey;this.lockfileLastVersion=n,this.lockfileNeedsRefresh=n"u")throw new Error(`Assertion failed: Expected the lockfile entry to have a resolution field (${f})`);let h=Rp(p.resolution,!0),E=new Ht;E.load(p,{yamlCompatibilityMode:!0});let C=E.version,S=E.languageName||r,P=p.linkType.toUpperCase(),I=p.conditions??null,R=E.dependencies,N=E.peerDependencies,U=E.dependenciesMeta,W=E.peerDependenciesMeta,te=E.bin;if(p.checksum!=null){let Ae=typeof c<"u"&&!p.checksum.includes("/")?`${c}/${p.checksum}`:p.checksum;this.storedChecksums.set(h.locatorHash,Ae)}let ie={...h,version:C,languageName:S,linkType:P,conditions:I,dependencies:R,peerDependencies:N,dependenciesMeta:U,peerDependenciesMeta:W,bin:te};this.originalPackages.set(ie.locatorHash,ie);for(let Ae of f.split(cwt)){let ce=I0(Ae);n<=6&&(ce=this.configuration.normalizeDependency(ce),ce=On(ce,ce.range.replace(/^patch:[^@]+@(?!npm(:|%3A))/,"$1npm%3A"))),this.storedDescriptors.set(ce.descriptorHash,ce),this.storedResolutions.set(ce.descriptorHash,h.locatorHash)}}}else s.includes("yarn lockfile v1")&&(this.lockfileLastVersion=-1)}}async setupWorkspaces(){this.workspaces=[],this.workspacesByCwd=new Map,this.workspacesByIdent=new Map;let e=new Set,r=(0,WR.default)(4),s=async(a,n)=>{if(e.has(n))return a;e.add(n);let c=new ZI(n,{project:this});await r(()=>c.setup());let f=a.then(()=>{this.addWorkspace(c)});return Array.from(c.workspacesCwds).reduce(s,f)};await s(Promise.resolve(),this.cwd)}addWorkspace(e){let r=this.workspacesByIdent.get(e.anchoredLocator.identHash);if(typeof r<"u")throw new Error(`Duplicate workspace name ${es(this.configuration,e.anchoredLocator)}: ${ue.fromPortablePath(e.cwd)} conflicts with ${ue.fromPortablePath(r.cwd)}`);this.workspaces.push(e),this.workspacesByCwd.set(e.cwd,e),this.workspacesByIdent.set(e.anchoredLocator.identHash,e)}get topLevelWorkspace(){return this.getWorkspaceByCwd(this.cwd)}tryWorkspaceByCwd(e){K.isAbsolute(e)||(e=K.resolve(this.cwd,e)),e=K.normalize(e).replace(/\/+$/,"");let r=this.workspacesByCwd.get(e);return r||null}getWorkspaceByCwd(e){let r=this.tryWorkspaceByCwd(e);if(!r)throw new Error(`Workspace not found (${e})`);return r}tryWorkspaceByFilePath(e){let r=null;for(let s of this.workspaces)K.relative(s.cwd,e).startsWith("../")||r&&r.cwd.length>=s.cwd.length||(r=s);return r||null}getWorkspaceByFilePath(e){let r=this.tryWorkspaceByFilePath(e);if(!r)throw new Error(`Workspace not found (${e})`);return r}tryWorkspaceByIdent(e){let r=this.workspacesByIdent.get(e.identHash);return typeof r>"u"?null:r}getWorkspaceByIdent(e){let r=this.tryWorkspaceByIdent(e);if(!r)throw new Error(`Workspace not found (${es(this.configuration,e)})`);return r}tryWorkspaceByDescriptor(e){if(e.range.startsWith(Ei.protocol)){let s=e.range.slice(Ei.protocol.length);if(s!=="^"&&s!=="~"&&s!=="*"&&!ul(s))return this.tryWorkspaceByCwd(s)}let r=this.tryWorkspaceByIdent(e);return r===null||(Tp(e)&&(e=YB(e)),!r.accepts(e.range))?null:r}getWorkspaceByDescriptor(e){let r=this.tryWorkspaceByDescriptor(e);if(r===null)throw new Error(`Workspace not found (${ni(this.configuration,e)})`);return r}tryWorkspaceByLocator(e){let r=this.tryWorkspaceByIdent(e);return r===null||(Gu(e)&&(e=tI(e)),r.anchoredLocator.locatorHash!==e.locatorHash)?null:r}getWorkspaceByLocator(e){let r=this.tryWorkspaceByLocator(e);if(!r)throw new Error(`Workspace not found (${Yr(this.configuration,e)})`);return r}deleteDescriptor(e){this.storedResolutions.delete(e),this.storedDescriptors.delete(e)}deleteLocator(e){this.originalPackages.delete(e),this.storedPackages.delete(e),this.accessibleLocators.delete(e)}forgetResolution(e){if("descriptorHash"in e){let r=this.storedResolutions.get(e.descriptorHash);this.deleteDescriptor(e.descriptorHash);let s=new Set(this.storedResolutions.values());typeof r<"u"&&!s.has(r)&&this.deleteLocator(r)}if("locatorHash"in e){this.deleteLocator(e.locatorHash);for(let[r,s]of this.storedResolutions)s===e.locatorHash&&this.deleteDescriptor(r)}}forgetTransientResolutions(){let e=this.configuration.makeResolver(),r=new Map;for(let[s,a]of this.storedResolutions.entries()){let n=r.get(a);n||r.set(a,n=new Set),n.add(s)}for(let s of this.originalPackages.values()){let a;try{a=e.shouldPersistResolution(s,{project:this,resolver:e})}catch{a=!1}if(!a){this.deleteLocator(s.locatorHash);let n=r.get(s.locatorHash);if(n){r.delete(s.locatorHash);for(let c of n)this.deleteDescriptor(c)}}}}forgetVirtualResolutions(){for(let e of this.storedPackages.values())for(let[r,s]of e.dependencies)Tp(s)&&e.dependencies.set(r,YB(s))}getDependencyMeta(e,r){let s={},n=this.topLevelWorkspace.manifest.dependenciesMeta.get(cn(e));if(!n)return s;let c=n.get(null);if(c&&Object.assign(s,c),r===null||!KCe.default.valid(r))return s;for(let[f,p]of n)f!==null&&f===r&&Object.assign(s,p);return s}async findLocatorForLocation(e,{strict:r=!1}={}){let s=new Yi,a=this.configuration.getLinkers(),n={project:this,report:s};for(let c of a){let f=await c.findPackageLocator(e,n);if(f){if(r&&(await c.findPackageLocation(f,n)).replace(YCe,"")!==e.replace(YCe,""))continue;return f}}return null}async loadUserConfig(){let e=K.join(this.cwd,".pnp.cjs");await le.existsPromise(e)&&kp(e).setup();let r=K.join(this.cwd,"yarn.config.cjs");return await le.existsPromise(r)?kp(r):null}async preparePackage(e,{resolver:r,resolveOptions:s}){let a=await this.configuration.getPackageExtensions(),n=this.configuration.normalizePackage(e,{packageExtensions:a});for(let[c,f]of n.dependencies){let p=await this.configuration.reduceHook(E=>E.reduceDependency,f,this,n,f,{resolver:r,resolveOptions:s});if(!VB(f,p))throw new Error("Assertion failed: The descriptor ident cannot be changed through aliases");let h=r.bindDescriptor(p,n,s);n.dependencies.set(c,h)}return n}async resolveEverything(e){if(!this.workspacesByCwd||!this.workspacesByIdent)throw new Error("Workspaces must have been setup before calling this function");this.forgetVirtualResolutions();let r=new Map(this.originalPackages),s=[];e.lockfileOnly||this.forgetTransientResolutions();let a=e.resolver||this.configuration.makeResolver(),n=new KI(a);await n.setup(this,{report:e.report});let c=e.lockfileOnly?[new jR(a)]:[n,a],f=new em([new JI(a),...c]),p=new em([...c]),h=this.configuration.makeFetcher(),E=e.lockfileOnly?{project:this,report:e.report,resolver:f}:{project:this,report:e.report,resolver:f,fetchOptions:{project:this,cache:e.cache,checksums:this.storedChecksums,report:e.report,fetcher:h,cacheOptions:{mirrorWriteOnly:!0}}},C=new Map,S=new Map,P=new Map,I=new Map,R=new Map,N=new Map,U=this.topLevelWorkspace.anchoredLocator,W=new Set,te=[],ie=Sj(),Ae=this.configuration.getSupportedArchitectures();await e.report.startProgressPromise(ho.progressViaTitle(),async se=>{let X=async rt=>{let Fe=await qE(async()=>await f.resolve(rt,E),ke=>`${Yr(this.configuration,rt)}: ${ke}`);if(!KB(rt,Fe))throw new Error(`Assertion failed: The locator cannot be changed by the resolver (went from ${Yr(this.configuration,rt)} to ${Yr(this.configuration,Fe)})`);I.set(Fe.locatorHash,Fe),!r.delete(Fe.locatorHash)&&!this.tryWorkspaceByLocator(Fe)&&s.push(Fe);let Pe=await this.preparePackage(Fe,{resolver:f,resolveOptions:E}),Ye=Uu([...Pe.dependencies.values()].map(ke=>j(ke)));return te.push(Ye),Ye.catch(()=>{}),S.set(Pe.locatorHash,Pe),Pe},De=async rt=>{let Fe=R.get(rt.locatorHash);if(typeof Fe<"u")return Fe;let Ne=Promise.resolve().then(()=>X(rt));return R.set(rt.locatorHash,Ne),Ne},Re=async(rt,Fe)=>{let Ne=await j(Fe);return C.set(rt.descriptorHash,rt),P.set(rt.descriptorHash,Ne.locatorHash),Ne},dt=async rt=>{se.setTitle(ni(this.configuration,rt));let Fe=this.resolutionAliases.get(rt.descriptorHash);if(typeof Fe<"u")return Re(rt,this.storedDescriptors.get(Fe));let Ne=f.getResolutionDependencies(rt,E),Pe=Object.fromEntries(await Uu(Object.entries(Ne).map(async([it,_e])=>{let x=f.bindDescriptor(_e,U,E),w=await j(x);return W.add(w.locatorHash),[it,w]}))),ke=(await qE(async()=>await f.getCandidates(rt,Pe,E),it=>`${ni(this.configuration,rt)}: ${it}`))[0];if(typeof ke>"u")throw new Yt(82,`${ni(this.configuration,rt)}: No candidates found`);if(e.checkResolutions){let{locators:it}=await p.getSatisfying(rt,Pe,[ke],{...E,resolver:p});if(!it.find(_e=>_e.locatorHash===ke.locatorHash))throw new Yt(78,`Invalid resolution ${jB(this.configuration,rt,ke)}`)}return C.set(rt.descriptorHash,rt),P.set(rt.descriptorHash,ke.locatorHash),De(ke)},j=rt=>{let Fe=N.get(rt.descriptorHash);if(typeof Fe<"u")return Fe;C.set(rt.descriptorHash,rt);let Ne=Promise.resolve().then(()=>dt(rt));return N.set(rt.descriptorHash,Ne),Ne};for(let rt of this.workspaces){let Fe=rt.anchoredDescriptor;te.push(j(Fe))}for(;te.length>0;){let rt=[...te];te.length=0,await Uu(rt)}});let ce=Yl(r.values(),se=>this.tryWorkspaceByLocator(se)?Yl.skip:se);if(s.length>0||ce.length>0){let se=new Set(this.workspaces.flatMap(rt=>{let Fe=S.get(rt.anchoredLocator.locatorHash);if(!Fe)throw new Error("Assertion failed: The workspace should have been resolved");return Array.from(Fe.dependencies.values(),Ne=>{let Pe=P.get(Ne.descriptorHash);if(!Pe)throw new Error("Assertion failed: The resolution should have been registered");return Pe})})),X=rt=>se.has(rt.locatorHash)?"0":"1",De=rt=>cl(rt),Re=Ys(s,[X,De]),dt=Ys(ce,[X,De]),j=e.report.getRecommendedLength();Re.length>0&&e.report.reportInfo(85,`${Ut(this.configuration,"+",pt.ADDED)} ${Zk(this.configuration,Re,j)}`),dt.length>0&&e.report.reportInfo(85,`${Ut(this.configuration,"-",pt.REMOVED)} ${Zk(this.configuration,dt,j)}`)}let me=new Set(this.resolutionAliases.values()),pe=new Set(S.keys()),Be=new Set,Ce=new Map,g=[],we=new Map;pwt({project:this,accessibleLocators:Be,volatileDescriptors:me,optionalBuilds:pe,peerRequirements:Ce,peerWarnings:g,peerRequirementNodes:we,allDescriptors:C,allResolutions:P,allPackages:S});for(let se of W)pe.delete(se);for(let se of me)C.delete(se),P.delete(se);let ye=new Set,fe=new Set;for(let se of S.values())se.conditions!=null&&pe.has(se.locatorHash)&&(kQ(se,Ae)||(kQ(se,ie)&&e.report.reportWarningOnce(77,`${Yr(this.configuration,se)}: Your current architecture (${process.platform}-${process.arch}) is supported by this package, but is missing from the ${Ut(this.configuration,"supportedArchitectures",pt.SETTING)} setting`),fe.add(se.locatorHash)),ye.add(se.locatorHash));this.storedResolutions=P,this.storedDescriptors=C,this.storedPackages=S,this.accessibleLocators=Be,this.conditionalLocators=ye,this.disabledLocators=fe,this.originalPackages=I,this.optionalBuilds=pe,this.peerRequirements=Ce,this.peerWarnings=g,this.peerRequirementNodes=we}async fetchEverything({cache:e,report:r,fetcher:s,mode:a,persistProject:n=!0}){let c={mockedPackages:this.disabledLocators,unstablePackages:this.conditionalLocators},f=s||this.configuration.makeFetcher(),p={checksums:this.storedChecksums,project:this,cache:e,fetcher:f,report:r,cacheOptions:c},h=Array.from(new Set(Ys(this.storedResolutions.values(),[I=>{let R=this.storedPackages.get(I);if(!R)throw new Error("Assertion failed: The locator should have been registered");return cl(R)}])));a==="update-lockfile"&&(h=h.filter(I=>!this.storedChecksums.has(I)));let E=!1,C=ho.progressViaCounter(h.length);await r.reportProgress(C);let S=(0,WR.default)(uwt);if(await Uu(h.map(I=>S(async()=>{let R=this.storedPackages.get(I);if(!R)throw new Error("Assertion failed: The locator should have been registered");if(Gu(R))return;let N;try{N=await f.fetch(R,p)}catch(U){U.message=`${Yr(this.configuration,R)}: ${U.message}`,r.reportExceptionOnce(U),E=U;return}N.checksum!=null?this.storedChecksums.set(R.locatorHash,N.checksum):this.storedChecksums.delete(R.locatorHash),N.releaseFs&&N.releaseFs()}).finally(()=>{C.tick()}))),E)throw E;let P=n&&a!=="update-lockfile"?await this.cacheCleanup({cache:e,report:r}):null;if(r.cacheMisses.size>0||P){let R=(await Promise.all([...r.cacheMisses].map(async ce=>{let me=this.storedPackages.get(ce),pe=this.storedChecksums.get(ce)??null,Be=e.getLocatorPath(me,pe);return(await le.statPromise(Be)).size}))).reduce((ce,me)=>ce+me,0)-(P?.size??0),N=r.cacheMisses.size,U=P?.count??0,W=`${Gk(N,{zero:"No new packages",one:"A package was",more:`${Ut(this.configuration,N,pt.NUMBER)} packages were`})} added to the project`,te=`${Gk(U,{zero:"none were",one:"one was",more:`${Ut(this.configuration,U,pt.NUMBER)} were`})} removed`,ie=R!==0?` (${Ut(this.configuration,R,pt.SIZE_DIFF)})`:"",Ae=U>0?N>0?`${W}, and ${te}${ie}.`:`${W}, but ${te}${ie}.`:`${W}${ie}.`;r.reportInfo(13,Ae)}}async linkEverything({cache:e,report:r,fetcher:s,mode:a}){let n={mockedPackages:this.disabledLocators,unstablePackages:this.conditionalLocators,skipIntegrityCheck:!0},c=s||this.configuration.makeFetcher(),f={checksums:this.storedChecksums,project:this,cache:e,fetcher:c,report:r,cacheOptions:n},p=this.configuration.getLinkers(),h={project:this,report:r},E=new Map(p.map(ye=>{let fe=ye.makeInstaller(h),se=ye.getCustomDataKey(),X=this.linkersCustomData.get(se);return typeof X<"u"&&fe.attachCustomData(X),[ye,fe]})),C=new Map,S=new Map,P=new Map,I=new Map(await Uu([...this.accessibleLocators].map(async ye=>{let fe=this.storedPackages.get(ye);if(!fe)throw new Error("Assertion failed: The locator should have been registered");return[ye,await c.fetch(fe,f)]}))),R=[],N=new Set,U=[];for(let ye of this.accessibleLocators){let fe=this.storedPackages.get(ye);if(typeof fe>"u")throw new Error("Assertion failed: The locator should have been registered");let se=I.get(fe.locatorHash);if(typeof se>"u")throw new Error("Assertion failed: The fetch result should have been registered");let X=[],De=dt=>{X.push(dt)},Re=this.tryWorkspaceByLocator(fe);if(Re!==null){let dt=[],{scripts:j}=Re.manifest;for(let Fe of["preinstall","install","postinstall"])j.has(Fe)&&dt.push({type:0,script:Fe});try{for(let[Fe,Ne]of E)if(Fe.supportsPackage(fe,h)&&(await Ne.installPackage(fe,se,{holdFetchResult:De})).buildRequest!==null)throw new Error("Assertion failed: Linkers can't return build directives for workspaces; this responsibility befalls to the Yarn core")}finally{X.length===0?se.releaseFs?.():R.push(Uu(X).catch(()=>{}).then(()=>{se.releaseFs?.()}))}let rt=K.join(se.packageFs.getRealPath(),se.prefixPath);S.set(fe.locatorHash,rt),!Gu(fe)&&dt.length>0&&P.set(fe.locatorHash,{buildDirectives:dt,buildLocations:[rt]})}else{let dt=p.find(Fe=>Fe.supportsPackage(fe,h));if(!dt)throw new Yt(12,`${Yr(this.configuration,fe)} isn't supported by any available linker`);let j=E.get(dt);if(!j)throw new Error("Assertion failed: The installer should have been registered");let rt;try{rt=await j.installPackage(fe,se,{holdFetchResult:De})}finally{X.length===0?se.releaseFs?.():R.push(Uu(X).then(()=>{}).then(()=>{se.releaseFs?.()}))}C.set(fe.locatorHash,dt),S.set(fe.locatorHash,rt.packageLocation),rt.buildRequest&&rt.packageLocation&&(rt.buildRequest.skipped?(N.add(fe.locatorHash),this.skippedBuilds.has(fe.locatorHash)||U.push([fe,rt.buildRequest.explain])):P.set(fe.locatorHash,{buildDirectives:rt.buildRequest.directives,buildLocations:[rt.packageLocation]}))}}let W=new Map;for(let ye of this.accessibleLocators){let fe=this.storedPackages.get(ye);if(!fe)throw new Error("Assertion failed: The locator should have been registered");let se=this.tryWorkspaceByLocator(fe)!==null,X=async(De,Re)=>{let dt=S.get(fe.locatorHash);if(typeof dt>"u")throw new Error(`Assertion failed: The package (${Yr(this.configuration,fe)}) should have been registered`);let j=[];for(let rt of fe.dependencies.values()){let Fe=this.storedResolutions.get(rt.descriptorHash);if(typeof Fe>"u")throw new Error(`Assertion failed: The resolution (${ni(this.configuration,rt)}, from ${Yr(this.configuration,fe)})should have been registered`);let Ne=this.storedPackages.get(Fe);if(typeof Ne>"u")throw new Error(`Assertion failed: The package (${Fe}, resolved from ${ni(this.configuration,rt)}) should have been registered`);let Pe=this.tryWorkspaceByLocator(Ne)===null?C.get(Fe):null;if(typeof Pe>"u")throw new Error(`Assertion failed: The package (${Fe}, resolved from ${ni(this.configuration,rt)}) should have been registered`);Pe===De||Pe===null?S.get(Ne.locatorHash)!==null&&j.push([rt,Ne]):!se&&dt!==null&&LB(W,Fe).push(dt)}dt!==null&&await Re.attachInternalDependencies(fe,j)};if(se)for(let[De,Re]of E)De.supportsPackage(fe,h)&&await X(De,Re);else{let De=C.get(fe.locatorHash);if(!De)throw new Error("Assertion failed: The linker should have been found");let Re=E.get(De);if(!Re)throw new Error("Assertion failed: The installer should have been registered");await X(De,Re)}}for(let[ye,fe]of W){let se=this.storedPackages.get(ye);if(!se)throw new Error("Assertion failed: The package should have been registered");let X=C.get(se.locatorHash);if(!X)throw new Error("Assertion failed: The linker should have been found");let De=E.get(X);if(!De)throw new Error("Assertion failed: The installer should have been registered");await De.attachExternalDependents(se,fe)}let te=new Map;for(let[ye,fe]of E){let se=await fe.finalizeInstall();for(let X of se?.records??[])X.buildRequest.skipped?(N.add(X.locator.locatorHash),this.skippedBuilds.has(X.locator.locatorHash)||U.push([X.locator,X.buildRequest.explain])):P.set(X.locator.locatorHash,{buildDirectives:X.buildRequest.directives,buildLocations:X.buildLocations});typeof se?.customData<"u"&&te.set(ye.getCustomDataKey(),se.customData)}if(this.linkersCustomData=te,await Uu(R),a==="skip-build")return;for(let[,ye]of Ys(U,([fe])=>cl(fe)))ye(r);let ie=new Set(P.keys()),Ae=(0,qR.createHash)("sha512");Ae.update(process.versions.node),await this.configuration.triggerHook(ye=>ye.globalHashGeneration,this,ye=>{Ae.update("\0"),Ae.update(ye)});let ce=Ae.digest("hex"),me=new Map,pe=ye=>{let fe=me.get(ye.locatorHash);if(typeof fe<"u")return fe;let se=this.storedPackages.get(ye.locatorHash);if(typeof se>"u")throw new Error("Assertion failed: The package should have been registered");let X=(0,qR.createHash)("sha512");X.update(ye.locatorHash),me.set(ye.locatorHash,"");for(let De of se.dependencies.values()){let Re=this.storedResolutions.get(De.descriptorHash);if(typeof Re>"u")throw new Error(`Assertion failed: The resolution (${ni(this.configuration,De)}) should have been registered`);let dt=this.storedPackages.get(Re);if(typeof dt>"u")throw new Error("Assertion failed: The package should have been registered");X.update(pe(dt))}return fe=X.digest("hex"),me.set(ye.locatorHash,fe),fe},Be=(ye,fe)=>{let se=(0,qR.createHash)("sha512");se.update(ce),se.update(pe(ye));for(let X of fe)se.update(X);return se.digest("hex")},Ce=new Map,g=!1,we=ye=>{let fe=new Set([ye.locatorHash]);for(let se of fe){let X=this.storedPackages.get(se);if(!X)throw new Error("Assertion failed: The package should have been registered");for(let De of X.dependencies.values()){let Re=this.storedResolutions.get(De.descriptorHash);if(!Re)throw new Error(`Assertion failed: The resolution (${ni(this.configuration,De)}) should have been registered`);if(Re!==ye.locatorHash&&ie.has(Re))return!1;let dt=this.storedPackages.get(Re);if(!dt)throw new Error("Assertion failed: The package should have been registered");let j=this.tryWorkspaceByLocator(dt);if(j){if(j.anchoredLocator.locatorHash!==ye.locatorHash&&ie.has(j.anchoredLocator.locatorHash))return!1;fe.add(j.anchoredLocator.locatorHash)}fe.add(Re)}}return!0};for(;ie.size>0;){let ye=ie.size,fe=[];for(let se of ie){let X=this.storedPackages.get(se);if(!X)throw new Error("Assertion failed: The package should have been registered");if(!we(X))continue;let De=P.get(X.locatorHash);if(!De)throw new Error("Assertion failed: The build directive should have been registered");let Re=Be(X,De.buildLocations);if(this.storedBuildState.get(X.locatorHash)===Re){Ce.set(X.locatorHash,Re),ie.delete(se);continue}g||(await this.persistInstallStateFile(),g=!0),this.storedBuildState.has(X.locatorHash)?r.reportInfo(8,`${Yr(this.configuration,X)} must be rebuilt because its dependency tree changed`):r.reportInfo(7,`${Yr(this.configuration,X)} must be built because it never has been before or the last one failed`);let dt=De.buildLocations.map(async j=>{if(!K.isAbsolute(j))throw new Error(`Assertion failed: Expected the build location to be absolute (not ${j})`);for(let rt of De.buildDirectives){let Fe=`# This file contains the result of Yarn building a package (${cl(X)}) `;switch(rt.type){case 0:Fe+=`# Script name: ${rt.script} `;break;case 1:Fe+=`# Script code: ${rt.script} `;break}let Ne=null;if(!await le.mktempPromise(async Ye=>{let ke=K.join(Ye,"build.log"),{stdout:it,stderr:_e}=this.configuration.getSubprocessStreams(ke,{header:Fe,prefix:Yr(this.configuration,X),report:r}),x;try{switch(rt.type){case 0:x=await OT(X,rt.script,[],{cwd:j,project:this,stdin:Ne,stdout:it,stderr:_e});break;case 1:x=await f6(X,rt.script,[],{cwd:j,project:this,stdin:Ne,stdout:it,stderr:_e});break}}catch(y){_e.write(y.stack),x=1}if(it.end(),_e.end(),x===0)return!0;le.detachTemp(Ye);let w=`${Yr(this.configuration,X)} couldn't be built successfully (exit code ${Ut(this.configuration,x,pt.NUMBER)}, logs can be found here: ${Ut(this.configuration,ke,pt.PATH)})`,b=this.optionalBuilds.has(X.locatorHash);return b?r.reportInfo(9,w):r.reportError(9,w),Wme&&r.reportFold(ue.fromPortablePath(ke),le.readFileSync(ke,"utf8")),b}))return!1}return!0});fe.push(...dt,Promise.allSettled(dt).then(j=>{ie.delete(se),j.every(rt=>rt.status==="fulfilled"&&rt.value===!0)&&Ce.set(X.locatorHash,Re)}))}if(await Uu(fe),ye===ie.size){let se=Array.from(ie).map(X=>{let De=this.storedPackages.get(X);if(!De)throw new Error("Assertion failed: The package should have been registered");return Yr(this.configuration,De)}).join(", ");r.reportError(3,`Some packages have circular dependencies that make their build order unsatisfiable - as a result they won't be built (affected packages are: ${se})`);break}}this.storedBuildState=Ce,this.skippedBuilds=N}async installWithNewReport(e,r){return(await Ot.start({configuration:this.configuration,json:e.json,stdout:e.stdout,forceSectionAlignment:!0,includeLogs:!e.json&&!e.quiet,includeVersion:!0},async a=>{await this.install({...r,report:a})})).exitCode()}async install(e){let r=this.configuration.get("nodeLinker");ze.telemetry?.reportInstall(r);let s=!1;if(await e.report.startTimerPromise("Project validation",{skipIfEmpty:!0},async()=>{this.configuration.get("enableOfflineMode")&&e.report.reportWarning(90,"Offline work is enabled; Yarn won't fetch packages from the remote registry if it can avoid it"),await this.configuration.triggerHook(E=>E.validateProject,this,{reportWarning:(E,C)=>{e.report.reportWarning(E,C)},reportError:(E,C)=>{e.report.reportError(E,C),s=!0}})}),s)return;let a=await this.configuration.getPackageExtensions();for(let E of a.values())for(let[,C]of E)for(let S of C)S.status="inactive";let n=K.join(this.cwd,Er.lockfile),c=null;if(e.immutable)try{c=await le.readFilePromise(n,"utf8")}catch(E){throw E.code==="ENOENT"?new Yt(28,"The lockfile would have been created by this install, which is explicitly forbidden."):E}await e.report.startTimerPromise("Resolution step",async()=>{await this.resolveEverything(e)}),await e.report.startTimerPromise("Post-resolution validation",{skipIfEmpty:!0},async()=>{gwt(this,e.report);for(let[,E]of a)for(let[,C]of E)for(let S of C)if(S.userProvided){let P=Ut(this.configuration,S,pt.PACKAGE_EXTENSION);switch(S.status){case"inactive":e.report.reportWarning(68,`${P}: No matching package in the dependency tree; you may not need this rule anymore.`);break;case"redundant":e.report.reportWarning(69,`${P}: This rule seems redundant when applied on the original package; the extension may have been applied upstream.`);break}}if(c!==null){let E=yd(c,this.generateLockfile());if(E!==c){let C=uCe(n,n,c,E,void 0,void 0,{maxEditLength:100});if(C){e.report.reportSeparator();for(let S of C.hunks){e.report.reportInfo(null,`@@ -${S.oldStart},${S.oldLines} +${S.newStart},${S.newLines} @@`);for(let P of S.lines)P.startsWith("+")?e.report.reportError(28,Ut(this.configuration,P,pt.ADDED)):P.startsWith("-")?e.report.reportError(28,Ut(this.configuration,P,pt.REMOVED)):e.report.reportInfo(null,Ut(this.configuration,P,"grey"))}e.report.reportSeparator()}throw new Yt(28,"The lockfile would have been modified by this install, which is explicitly forbidden.")}}});for(let E of a.values())for(let[,C]of E)for(let S of C)S.userProvided&&S.status==="active"&&ze.telemetry?.reportPackageExtension(Jd(S,pt.PACKAGE_EXTENSION));await e.report.startTimerPromise("Fetch step",async()=>{await this.fetchEverything(e)});let f=e.immutable?[...new Set(this.configuration.get("immutablePatterns"))].sort():[],p=await Promise.all(f.map(async E=>vQ(E,{cwd:this.cwd})));(typeof e.persistProject>"u"||e.persistProject)&&await this.persist(),await e.report.startTimerPromise("Link step",async()=>{if(e.mode==="update-lockfile"){e.report.reportWarning(73,`Skipped due to ${Ut(this.configuration,"mode=update-lockfile",pt.CODE)}`);return}await this.linkEverything(e);let E=await Promise.all(f.map(async C=>vQ(C,{cwd:this.cwd})));for(let C=0;C{await this.configuration.triggerHook(E=>E.validateProjectAfterInstall,this,{reportWarning:(E,C)=>{e.report.reportWarning(E,C)},reportError:(E,C)=>{e.report.reportError(E,C),h=!0}})}),!h&&await this.configuration.triggerHook(E=>E.afterAllInstalled,this,e)}generateLockfile(){let e=new Map;for(let[n,c]of this.storedResolutions.entries()){let f=e.get(c);f||e.set(c,f=new Set),f.add(n)}let r={},{cacheKey:s}=Jr.getCacheKey(this.configuration);r.__metadata={version:YR,cacheKey:s};for(let[n,c]of e.entries()){let f=this.originalPackages.get(n);if(!f)continue;let p=[];for(let C of c){let S=this.storedDescriptors.get(C);if(!S)throw new Error("Assertion failed: The descriptor should have been registered");p.push(S)}let h=p.map(C=>ll(C)).sort().join(", "),E=new Ht;E.version=f.linkType==="HARD"?f.version:"0.0.0-use.local",E.languageName=f.languageName,E.dependencies=new Map(f.dependencies),E.peerDependencies=new Map(f.peerDependencies),E.dependenciesMeta=new Map(f.dependenciesMeta),E.peerDependenciesMeta=new Map(f.peerDependenciesMeta),E.bin=new Map(f.bin),r[h]={...E.exportTo({},{compatibilityMode:!1}),linkType:f.linkType.toLowerCase(),resolution:cl(f),checksum:this.storedChecksums.get(f.locatorHash),conditions:f.conditions||void 0}}return`${[`# This file is generated by running "yarn install" inside your project. `,`# Manual changes might be lost - proceed with caution! `].join("")} `+il(r)}async persistLockfile(){let e=K.join(this.cwd,Er.lockfile),r="";try{r=await le.readFilePromise(e,"utf8")}catch{}let s=this.generateLockfile(),a=yd(r,s);a!==r&&(await le.writeFilePromise(e,a),this.lockFileChecksum=VCe(a),this.lockfileNeedsRefresh=!1)}async persistInstallStateFile(){let e=[];for(let c of Object.values(IG))e.push(...c);let r=(0,GR.default)(this,e),s=CG.default.serialize(r),a=fs(s);if(this.installStateChecksum===a)return;let n=this.configuration.get("installStatePath");await le.mkdirPromise(K.dirname(n),{recursive:!0}),await le.writeFilePromise(n,await fwt(s)),this.installStateChecksum=a}async restoreInstallState({restoreLinkersCustomData:e=!0,restoreResolutions:r=!0,restoreBuildState:s=!0}={}){let a=this.configuration.get("installStatePath"),n;try{let c=await Awt(await le.readFilePromise(a));n=CG.default.deserialize(c),this.installStateChecksum=fs(c)}catch{r&&await this.applyLightResolution();return}e&&typeof n.linkersCustomData<"u"&&(this.linkersCustomData=n.linkersCustomData),s&&Object.assign(this,(0,GR.default)(n,IG.restoreBuildState)),r&&(n.lockFileChecksum===this.lockFileChecksum?Object.assign(this,(0,GR.default)(n,IG.restoreResolutions)):await this.applyLightResolution())}async applyLightResolution(){await this.resolveEverything({lockfileOnly:!0,report:new Yi}),await this.persistInstallStateFile()}async persist(){let e=(0,WR.default)(4);await Promise.all([this.persistLockfile(),...this.workspaces.map(r=>e(()=>r.persistManifest()))])}async cacheCleanup({cache:e,report:r}){if(this.configuration.get("enableGlobalCache"))return null;let s=new Set([".gitignore"]);if(!sH(e.cwd,this.cwd)||!await le.existsPromise(e.cwd))return null;let a=[];for(let c of await le.readdirPromise(e.cwd)){if(s.has(c))continue;let f=K.resolve(e.cwd,c);e.markedFiles.has(f)||(e.immutable?r.reportError(56,`${Ut(this.configuration,K.basename(f),"magenta")} appears to be unused and would be marked for deletion, but the cache is immutable`):a.push(le.lstatPromise(f).then(async p=>(await le.removePromise(f),p.size))))}if(a.length===0)return null;let n=await Promise.all(a);return{count:a.length,size:n.reduce((c,f)=>c+f,0)}}}});function dwt(t){let s=Math.floor(t.timeNow/864e5),a=t.updateInterval*864e5,n=t.state.lastUpdate??t.timeNow+a+Math.floor(a*t.randomInitialInterval),c=n+a,f=t.state.lastTips??s*864e5,p=f+864e5+8*36e5-t.timeZone,h=c<=t.timeNow,E=p<=t.timeNow,C=null;return(h||E||!t.state.lastUpdate||!t.state.lastTips)&&(C={},C.lastUpdate=h?t.timeNow:n,C.lastTips=f,C.blocks=h?{}:t.state.blocks,C.displayedTips=t.state.displayedTips),{nextState:C,triggerUpdate:h,triggerTips:E,nextTips:E?s*864e5:f}}var XI,zCe=Ct(()=>{bt();Pv();E0();fT();kc();Np();XI=class{constructor(e,r){this.values=new Map;this.hits=new Map;this.enumerators=new Map;this.nextTips=0;this.displayedTips=[];this.shouldCommitTips=!1;this.configuration=e;let s=this.getRegistryPath();this.isNew=!le.existsSync(s),this.shouldShowTips=!1,this.sendReport(r),this.startBuffer()}commitTips(){this.shouldShowTips&&(this.shouldCommitTips=!0)}selectTip(e){let r=new Set(this.displayedTips),s=f=>f&&un?eA(un,f):!1,a=e.map((f,p)=>p).filter(f=>e[f]&&s(e[f]?.selector));if(a.length===0)return null;let n=a.filter(f=>!r.has(f));if(n.length===0){let f=Math.floor(a.length*.2);this.displayedTips=f>0?this.displayedTips.slice(-f):[],n=a.filter(p=>!r.has(p))}let c=n[Math.floor(Math.random()*n.length)];return this.displayedTips.push(c),this.commitTips(),e[c]}reportVersion(e){this.reportValue("version",e.replace(/-git\..*/,"-git"))}reportCommandName(e){this.reportValue("commandName",e||"")}reportPluginName(e){this.reportValue("pluginName",e)}reportProject(e){this.reportEnumerator("projectCount",e)}reportInstall(e){this.reportHit("installCount",e)}reportPackageExtension(e){this.reportValue("packageExtension",e)}reportWorkspaceCount(e){this.reportValue("workspaceCount",String(e))}reportDependencyCount(e){this.reportValue("dependencyCount",String(e))}reportValue(e,r){xp(this.values,e).add(r)}reportEnumerator(e,r){xp(this.enumerators,e).add(fs(r))}reportHit(e,r="*"){let s=n3(this.hits,e),a=Vl(s,r,()=>0);s.set(r,a+1)}getRegistryPath(){let e=this.configuration.get("globalFolder");return K.join(e,"telemetry.json")}sendReport(e){let r=this.getRegistryPath(),s;try{s=le.readJsonSync(r)}catch{s={}}let{nextState:a,triggerUpdate:n,triggerTips:c,nextTips:f}=dwt({state:s,timeNow:Date.now(),timeZone:new Date().getTimezoneOffset()*60*1e3,randomInitialInterval:Math.random(),updateInterval:this.configuration.get("telemetryInterval")});if(this.nextTips=f,this.displayedTips=s.displayedTips??[],a!==null)try{le.mkdirSync(K.dirname(r),{recursive:!0}),le.writeJsonSync(r,a)}catch{return!1}if(c&&this.configuration.get("enableTips")&&(this.shouldShowTips=!0),n){let p=s.blocks??{};if(Object.keys(p).length===0){let h=`https://browser-http-intake.logs.datadoghq.eu/v1/input/${e}?ddsource=yarn`,E=C=>vj(h,C,{configuration:this.configuration}).catch(()=>{});for(let[C,S]of Object.entries(s.blocks??{})){if(Object.keys(S).length===0)continue;let P=S;P.userId=C,P.reportType="primary";for(let N of Object.keys(P.enumerators??{}))P.enumerators[N]=P.enumerators[N].length;E(P);let I=new Map,R=20;for(let[N,U]of Object.entries(P.values))U.length>0&&I.set(N,U.slice(0,R));for(;I.size>0;){let N={};N.userId=C,N.reportType="secondary",N.metrics={};for(let[U,W]of I)N.metrics[U]=W.shift(),W.length===0&&I.delete(U);E(N)}}}}return!0}applyChanges(){let e=this.getRegistryPath(),r;try{r=le.readJsonSync(e)}catch{r={}}let s=this.configuration.get("telemetryUserId")??"*",a=r.blocks=r.blocks??{},n=a[s]=a[s]??{};for(let c of this.hits.keys()){let f=n.hits=n.hits??{},p=f[c]=f[c]??{};for(let[h,E]of this.hits.get(c))p[h]=(p[h]??0)+E}for(let c of["values","enumerators"])for(let f of this[c].keys()){let p=n[c]=n[c]??{};p[f]=[...new Set([...p[f]??[],...this[c].get(f)??[]])]}this.shouldCommitTips&&(r.lastTips=this.nextTips,r.displayedTips=this.displayedTips),le.mkdirSync(K.dirname(e),{recursive:!0}),le.writeJsonSync(e,r)}startBuffer(){process.on("exit",()=>{try{this.applyChanges()}catch{}})}}});var Xv={};Vt(Xv,{BuildDirectiveType:()=>_R,CACHE_CHECKPOINT:()=>tG,CACHE_VERSION:()=>MR,Cache:()=>Jr,Configuration:()=>ze,DEFAULT_RC_FILENAME:()=>Qj,FormatType:()=>Dde,InstallMode:()=>ec,LEGACY_PLUGINS:()=>hv,LOCKFILE_VERSION:()=>YR,LegacyMigrationResolver:()=>KI,LightReport:()=>uA,LinkType:()=>YE,LockfileResolver:()=>JI,Manifest:()=>Ht,MessageName:()=>Dr,MultiFetcher:()=>aI,PackageExtensionStatus:()=>a3,PackageExtensionType:()=>o3,PeerWarningType:()=>VR,Project:()=>Tt,Report:()=>ho,ReportError:()=>Yt,SettingsType:()=>gv,StreamReport:()=>Ot,TAG_REGEXP:()=>Up,TelemetryManager:()=>XI,ThrowReport:()=>Yi,VirtualFetcher:()=>lI,WindowsLinkType:()=>yT,Workspace:()=>ZI,WorkspaceFetcher:()=>cI,WorkspaceResolver:()=>Ei,YarnVersion:()=>un,execUtils:()=>Gr,folderUtils:()=>FQ,formatUtils:()=>he,hashUtils:()=>Nn,httpUtils:()=>An,miscUtils:()=>je,nodeUtils:()=>ps,parseMessageName:()=>zx,reportOptionDeprecations:()=>vI,scriptUtils:()=>In,semverUtils:()=>Or,stringifyMessageName:()=>Vf,structUtils:()=>q,tgzUtils:()=>gs,treeUtils:()=>Qs});var Ve=Ct(()=>{hT();NQ();Qc();E0();fT();kc();pT();g6();Np();Yo();JIe();rCe();rG();dv();dv();sCe();nG();oCe();iG();sI();Zx();K8();JCe();Fc();xv();zCe();yG();z8();Z8();$d();EG();Pv();hAe()});var rwe=L((wrr,eS)=>{"use strict";var ywt=process.env.TERM_PROGRAM==="Hyper",Ewt=process.platform==="win32",$Ce=process.platform==="linux",vG={ballotDisabled:"\u2612",ballotOff:"\u2610",ballotOn:"\u2611",bullet:"\u2022",bulletWhite:"\u25E6",fullBlock:"\u2588",heart:"\u2764",identicalTo:"\u2261",line:"\u2500",mark:"\u203B",middot:"\xB7",minus:"\uFF0D",multiplication:"\xD7",obelus:"\xF7",pencilDownRight:"\u270E",pencilRight:"\u270F",pencilUpRight:"\u2710",percent:"%",pilcrow2:"\u2761",pilcrow:"\xB6",plusMinus:"\xB1",section:"\xA7",starsOff:"\u2606",starsOn:"\u2605",upDownArrow:"\u2195"},ewe=Object.assign({},vG,{check:"\u221A",cross:"\xD7",ellipsisLarge:"...",ellipsis:"...",info:"i",question:"?",questionSmall:"?",pointer:">",pointerSmall:"\xBB",radioOff:"( )",radioOn:"(*)",warning:"\u203C"}),twe=Object.assign({},vG,{ballotCross:"\u2718",check:"\u2714",cross:"\u2716",ellipsisLarge:"\u22EF",ellipsis:"\u2026",info:"\u2139",question:"?",questionFull:"\uFF1F",questionSmall:"\uFE56",pointer:$Ce?"\u25B8":"\u276F",pointerSmall:$Ce?"\u2023":"\u203A",radioOff:"\u25EF",radioOn:"\u25C9",warning:"\u26A0"});eS.exports=Ewt&&!ywt?ewe:twe;Reflect.defineProperty(eS.exports,"common",{enumerable:!1,value:vG});Reflect.defineProperty(eS.exports,"windows",{enumerable:!1,value:ewe});Reflect.defineProperty(eS.exports,"other",{enumerable:!1,value:twe})});var Ju=L((Brr,SG)=>{"use strict";var Iwt=t=>t!==null&&typeof t=="object"&&!Array.isArray(t),Cwt=/[\u001b\u009b][[\]#;?()]*(?:(?:(?:[^\W_]*;?[^\W_]*)\u0007)|(?:(?:[0-9]{1,4}(;[0-9]{0,4})*)?[~0-9=<>cf-nqrtyA-PRZ]))/g,nwe=()=>{let t={enabled:!0,visible:!0,styles:{},keys:{}};"FORCE_COLOR"in process.env&&(t.enabled=process.env.FORCE_COLOR!=="0");let e=n=>{let c=n.open=`\x1B[${n.codes[0]}m`,f=n.close=`\x1B[${n.codes[1]}m`,p=n.regex=new RegExp(`\\u001b\\[${n.codes[1]}m`,"g");return n.wrap=(h,E)=>{h.includes(f)&&(h=h.replace(p,f+c));let C=c+h+f;return E?C.replace(/\r*\n/g,`${f}$&${c}`):C},n},r=(n,c,f)=>typeof n=="function"?n(c):n.wrap(c,f),s=(n,c)=>{if(n===""||n==null)return"";if(t.enabled===!1)return n;if(t.visible===!1)return"";let f=""+n,p=f.includes(` `),h=c.length;for(h>0&&c.includes("unstyle")&&(c=[...new Set(["unstyle",...c])].reverse());h-- >0;)f=r(t.styles[c[h]],f,p);return f},a=(n,c,f)=>{t.styles[n]=e({name:n,codes:c}),(t.keys[f]||(t.keys[f]=[])).push(n),Reflect.defineProperty(t,n,{configurable:!0,enumerable:!0,set(h){t.alias(n,h)},get(){let h=E=>s(E,h.stack);return Reflect.setPrototypeOf(h,t),h.stack=this.stack?this.stack.concat(n):[n],h}})};return a("reset",[0,0],"modifier"),a("bold",[1,22],"modifier"),a("dim",[2,22],"modifier"),a("italic",[3,23],"modifier"),a("underline",[4,24],"modifier"),a("inverse",[7,27],"modifier"),a("hidden",[8,28],"modifier"),a("strikethrough",[9,29],"modifier"),a("black",[30,39],"color"),a("red",[31,39],"color"),a("green",[32,39],"color"),a("yellow",[33,39],"color"),a("blue",[34,39],"color"),a("magenta",[35,39],"color"),a("cyan",[36,39],"color"),a("white",[37,39],"color"),a("gray",[90,39],"color"),a("grey",[90,39],"color"),a("bgBlack",[40,49],"bg"),a("bgRed",[41,49],"bg"),a("bgGreen",[42,49],"bg"),a("bgYellow",[43,49],"bg"),a("bgBlue",[44,49],"bg"),a("bgMagenta",[45,49],"bg"),a("bgCyan",[46,49],"bg"),a("bgWhite",[47,49],"bg"),a("blackBright",[90,39],"bright"),a("redBright",[91,39],"bright"),a("greenBright",[92,39],"bright"),a("yellowBright",[93,39],"bright"),a("blueBright",[94,39],"bright"),a("magentaBright",[95,39],"bright"),a("cyanBright",[96,39],"bright"),a("whiteBright",[97,39],"bright"),a("bgBlackBright",[100,49],"bgBright"),a("bgRedBright",[101,49],"bgBright"),a("bgGreenBright",[102,49],"bgBright"),a("bgYellowBright",[103,49],"bgBright"),a("bgBlueBright",[104,49],"bgBright"),a("bgMagentaBright",[105,49],"bgBright"),a("bgCyanBright",[106,49],"bgBright"),a("bgWhiteBright",[107,49],"bgBright"),t.ansiRegex=Cwt,t.hasColor=t.hasAnsi=n=>(t.ansiRegex.lastIndex=0,typeof n=="string"&&n!==""&&t.ansiRegex.test(n)),t.alias=(n,c)=>{let f=typeof c=="string"?t[c]:c;if(typeof f!="function")throw new TypeError("Expected alias to be the name of an existing color (string) or a function");f.stack||(Reflect.defineProperty(f,"name",{value:n}),t.styles[n]=f,f.stack=[n]),Reflect.defineProperty(t,n,{configurable:!0,enumerable:!0,set(p){t.alias(n,p)},get(){let p=h=>s(h,p.stack);return Reflect.setPrototypeOf(p,t),p.stack=this.stack?this.stack.concat(f.stack):f.stack,p}})},t.theme=n=>{if(!Iwt(n))throw new TypeError("Expected theme to be an object");for(let c of Object.keys(n))t.alias(c,n[c]);return t},t.alias("unstyle",n=>typeof n=="string"&&n!==""?(t.ansiRegex.lastIndex=0,n.replace(t.ansiRegex,"")):""),t.alias("noop",n=>n),t.none=t.clear=t.noop,t.stripColor=t.unstyle,t.symbols=rwe(),t.define=a,t};SG.exports=nwe();SG.exports.create=nwe});var $o=L(pn=>{"use strict";var wwt=Object.prototype.toString,Gc=Ju(),iwe=!1,DG=[],swe={yellow:"blue",cyan:"red",green:"magenta",black:"white",blue:"yellow",red:"cyan",magenta:"green",white:"black"};pn.longest=(t,e)=>t.reduce((r,s)=>Math.max(r,e?s[e].length:s.length),0);pn.hasColor=t=>!!t&&Gc.hasColor(t);var JR=pn.isObject=t=>t!==null&&typeof t=="object"&&!Array.isArray(t);pn.nativeType=t=>wwt.call(t).slice(8,-1).toLowerCase().replace(/\s/g,"");pn.isAsyncFn=t=>pn.nativeType(t)==="asyncfunction";pn.isPrimitive=t=>t!=null&&typeof t!="object"&&typeof t!="function";pn.resolve=(t,e,...r)=>typeof e=="function"?e.call(t,...r):e;pn.scrollDown=(t=[])=>[...t.slice(1),t[0]];pn.scrollUp=(t=[])=>[t.pop(),...t];pn.reorder=(t=[])=>{let e=t.slice();return e.sort((r,s)=>r.index>s.index?1:r.index{let s=t.length,a=r===s?0:r<0?s-1:r,n=t[e];t[e]=t[a],t[a]=n};pn.width=(t,e=80)=>{let r=t&&t.columns?t.columns:e;return t&&typeof t.getWindowSize=="function"&&(r=t.getWindowSize()[0]),process.platform==="win32"?r-1:r};pn.height=(t,e=20)=>{let r=t&&t.rows?t.rows:e;return t&&typeof t.getWindowSize=="function"&&(r=t.getWindowSize()[1]),r};pn.wordWrap=(t,e={})=>{if(!t)return t;typeof e=="number"&&(e={width:e});let{indent:r="",newline:s=` `+r,width:a=80}=e,n=(s+r).match(/[^\S\n]/g)||[];a-=n.length;let c=`.{1,${a}}([\\s\\u200B]+|$)|[^\\s\\u200B]+?([\\s\\u200B]+|$)`,f=t.trim(),p=new RegExp(c,"g"),h=f.match(p)||[];return h=h.map(E=>E.replace(/\n$/,"")),e.padEnd&&(h=h.map(E=>E.padEnd(a," "))),e.padStart&&(h=h.map(E=>E.padStart(a," "))),r+h.join(s)};pn.unmute=t=>{let e=t.stack.find(s=>Gc.keys.color.includes(s));return e?Gc[e]:t.stack.find(s=>s.slice(2)==="bg")?Gc[e.slice(2)]:s=>s};pn.pascal=t=>t?t[0].toUpperCase()+t.slice(1):"";pn.inverse=t=>{if(!t||!t.stack)return t;let e=t.stack.find(s=>Gc.keys.color.includes(s));if(e){let s=Gc["bg"+pn.pascal(e)];return s?s.black:t}let r=t.stack.find(s=>s.slice(0,2)==="bg");return r?Gc[r.slice(2).toLowerCase()]||t:Gc.none};pn.complement=t=>{if(!t||!t.stack)return t;let e=t.stack.find(s=>Gc.keys.color.includes(s)),r=t.stack.find(s=>s.slice(0,2)==="bg");if(e&&!r)return Gc[swe[e]||e];if(r){let s=r.slice(2).toLowerCase(),a=swe[s];return a&&Gc["bg"+pn.pascal(a)]||t}return Gc.none};pn.meridiem=t=>{let e=t.getHours(),r=t.getMinutes(),s=e>=12?"pm":"am";e=e%12;let a=e===0?12:e,n=r<10?"0"+r:r;return a+":"+n+" "+s};pn.set=(t={},e="",r)=>e.split(".").reduce((s,a,n,c)=>{let f=c.length-1>n?s[a]||{}:r;return!pn.isObject(f)&&n{let s=t[e]==null?e.split(".").reduce((a,n)=>a&&a[n],t):t[e];return s??r};pn.mixin=(t,e)=>{if(!JR(t))return e;if(!JR(e))return t;for(let r of Object.keys(e)){let s=Object.getOwnPropertyDescriptor(e,r);if(s.hasOwnProperty("value"))if(t.hasOwnProperty(r)&&JR(s.value)){let a=Object.getOwnPropertyDescriptor(t,r);JR(a.value)?t[r]=pn.merge({},t[r],e[r]):Reflect.defineProperty(t,r,s)}else Reflect.defineProperty(t,r,s);else Reflect.defineProperty(t,r,s)}return t};pn.merge=(...t)=>{let e={};for(let r of t)pn.mixin(e,r);return e};pn.mixinEmitter=(t,e)=>{let r=e.constructor.prototype;for(let s of Object.keys(r)){let a=r[s];typeof a=="function"?pn.define(t,s,a.bind(e)):pn.define(t,s,a)}};pn.onExit=t=>{let e=(r,s)=>{iwe||(iwe=!0,DG.forEach(a=>a()),r===!0&&process.exit(128+s))};DG.length===0&&(process.once("SIGTERM",e.bind(null,!0,15)),process.once("SIGINT",e.bind(null,!0,2)),process.once("exit",e)),DG.push(t)};pn.define=(t,e,r)=>{Reflect.defineProperty(t,e,{value:r})};pn.defineExport=(t,e,r)=>{let s;Reflect.defineProperty(t,e,{enumerable:!0,configurable:!0,set(a){s=a},get(){return s?s():r()}})}});var owe=L(rC=>{"use strict";rC.ctrl={a:"first",b:"backward",c:"cancel",d:"deleteForward",e:"last",f:"forward",g:"reset",i:"tab",k:"cutForward",l:"reset",n:"newItem",m:"cancel",j:"submit",p:"search",r:"remove",s:"save",u:"undo",w:"cutLeft",x:"toggleCursor",v:"paste"};rC.shift={up:"shiftUp",down:"shiftDown",left:"shiftLeft",right:"shiftRight",tab:"prev"};rC.fn={up:"pageUp",down:"pageDown",left:"pageLeft",right:"pageRight",delete:"deleteForward"};rC.option={b:"backward",f:"forward",d:"cutRight",left:"cutLeft",up:"altUp",down:"altDown"};rC.keys={pageup:"pageUp",pagedown:"pageDown",home:"home",end:"end",cancel:"cancel",delete:"deleteForward",backspace:"delete",down:"down",enter:"submit",escape:"cancel",left:"left",space:"space",number:"number",return:"submit",right:"right",tab:"next",up:"up"}});var cwe=L((Drr,lwe)=>{"use strict";var awe=Ie("readline"),Bwt=owe(),vwt=/^(?:\x1b)([a-zA-Z0-9])$/,Swt=/^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/,Dwt={OP:"f1",OQ:"f2",OR:"f3",OS:"f4","[11~":"f1","[12~":"f2","[13~":"f3","[14~":"f4","[[A":"f1","[[B":"f2","[[C":"f3","[[D":"f4","[[E":"f5","[15~":"f5","[17~":"f6","[18~":"f7","[19~":"f8","[20~":"f9","[21~":"f10","[23~":"f11","[24~":"f12","[A":"up","[B":"down","[C":"right","[D":"left","[E":"clear","[F":"end","[H":"home",OA:"up",OB:"down",OC:"right",OD:"left",OE:"clear",OF:"end",OH:"home","[1~":"home","[2~":"insert","[3~":"delete","[4~":"end","[5~":"pageup","[6~":"pagedown","[[5~":"pageup","[[6~":"pagedown","[7~":"home","[8~":"end","[a":"up","[b":"down","[c":"right","[d":"left","[e":"clear","[2$":"insert","[3$":"delete","[5$":"pageup","[6$":"pagedown","[7$":"home","[8$":"end",Oa:"up",Ob:"down",Oc:"right",Od:"left",Oe:"clear","[2^":"insert","[3^":"delete","[5^":"pageup","[6^":"pagedown","[7^":"home","[8^":"end","[Z":"tab"};function bwt(t){return["[a","[b","[c","[d","[e","[2$","[3$","[5$","[6$","[7$","[8$","[Z"].includes(t)}function Pwt(t){return["Oa","Ob","Oc","Od","Oe","[2^","[3^","[5^","[6^","[7^","[8^"].includes(t)}var zR=(t="",e={})=>{let r,s={name:e.name,ctrl:!1,meta:!1,shift:!1,option:!1,sequence:t,raw:t,...e};if(Buffer.isBuffer(t)?t[0]>127&&t[1]===void 0?(t[0]-=128,t="\x1B"+String(t)):t=String(t):t!==void 0&&typeof t!="string"?t=String(t):t||(t=s.sequence||""),s.sequence=s.sequence||t||s.name,t==="\r")s.raw=void 0,s.name="return";else if(t===` `)s.name="enter";else if(t===" ")s.name="tab";else if(t==="\b"||t==="\x7F"||t==="\x1B\x7F"||t==="\x1B\b")s.name="backspace",s.meta=t.charAt(0)==="\x1B";else if(t==="\x1B"||t==="\x1B\x1B")s.name="escape",s.meta=t.length===2;else if(t===" "||t==="\x1B ")s.name="space",s.meta=t.length===2;else if(t<="")s.name=String.fromCharCode(t.charCodeAt(0)+97-1),s.ctrl=!0;else if(t.length===1&&t>="0"&&t<="9")s.name="number";else if(t.length===1&&t>="a"&&t<="z")s.name=t;else if(t.length===1&&t>="A"&&t<="Z")s.name=t.toLowerCase(),s.shift=!0;else if(r=vwt.exec(t))s.meta=!0,s.shift=/^[A-Z]$/.test(r[1]);else if(r=Swt.exec(t)){let a=[...t];a[0]==="\x1B"&&a[1]==="\x1B"&&(s.option=!0);let n=[r[1],r[2],r[4],r[6]].filter(Boolean).join(""),c=(r[3]||r[5]||1)-1;s.ctrl=!!(c&4),s.meta=!!(c&10),s.shift=!!(c&1),s.code=n,s.name=Dwt[n],s.shift=bwt(n)||s.shift,s.ctrl=Pwt(n)||s.ctrl}return s};zR.listen=(t={},e)=>{let{stdin:r}=t;if(!r||r!==process.stdin&&!r.isTTY)throw new Error("Invalid stream passed");let s=awe.createInterface({terminal:!0,input:r});awe.emitKeypressEvents(r,s);let a=(f,p)=>e(f,zR(f,p),s),n=r.isRaw;return r.isTTY&&r.setRawMode(!0),r.on("keypress",a),s.resume(),()=>{r.isTTY&&r.setRawMode(n),r.removeListener("keypress",a),s.pause(),s.close()}};zR.action=(t,e,r)=>{let s={...Bwt,...r};return e.ctrl?(e.action=s.ctrl[e.name],e):e.option&&s.option?(e.action=s.option[e.name],e):e.shift?(e.action=s.shift[e.name],e):(e.action=s.keys[e.name],e)};lwe.exports=zR});var fwe=L((brr,uwe)=>{"use strict";uwe.exports=t=>{t.timers=t.timers||{};let e=t.options.timers;if(e)for(let r of Object.keys(e)){let s=e[r];typeof s=="number"&&(s={interval:s}),xwt(t,r,s)}};function xwt(t,e,r={}){let s=t.timers[e]={name:e,start:Date.now(),ms:0,tick:0},a=r.interval||120;s.frames=r.frames||[],s.loading=!0;let n=setInterval(()=>{s.ms=Date.now()-s.start,s.tick++,t.render()},a);return s.stop=()=>{s.loading=!1,clearInterval(n)},Reflect.defineProperty(s,"interval",{value:n}),t.once("close",()=>s.stop()),s.stop}});var pwe=L((Prr,Awe)=>{"use strict";var{define:kwt,width:Qwt}=$o(),bG=class{constructor(e){let r=e.options;kwt(this,"_prompt",e),this.type=e.type,this.name=e.name,this.message="",this.header="",this.footer="",this.error="",this.hint="",this.input="",this.cursor=0,this.index=0,this.lines=0,this.tick=0,this.prompt="",this.buffer="",this.width=Qwt(r.stdout||process.stdout),Object.assign(this,r),this.name=this.name||this.message,this.message=this.message||this.name,this.symbols=e.symbols,this.styles=e.styles,this.required=new Set,this.cancelled=!1,this.submitted=!1}clone(){let e={...this};return e.status=this.status,e.buffer=Buffer.from(e.buffer),delete e.clone,e}set color(e){this._color=e}get color(){let e=this.prompt.styles;if(this.cancelled)return e.cancelled;if(this.submitted)return e.submitted;let r=this._color||e[this.status];return typeof r=="function"?r:e.pending}set loading(e){this._loading=e}get loading(){return typeof this._loading=="boolean"?this._loading:this.loadingChoices?"choices":!1}get status(){return this.cancelled?"cancelled":this.submitted?"submitted":"pending"}};Awe.exports=bG});var gwe=L((xrr,hwe)=>{"use strict";var PG=$o(),mo=Ju(),xG={default:mo.noop,noop:mo.noop,set inverse(t){this._inverse=t},get inverse(){return this._inverse||PG.inverse(this.primary)},set complement(t){this._complement=t},get complement(){return this._complement||PG.complement(this.primary)},primary:mo.cyan,success:mo.green,danger:mo.magenta,strong:mo.bold,warning:mo.yellow,muted:mo.dim,disabled:mo.gray,dark:mo.dim.gray,underline:mo.underline,set info(t){this._info=t},get info(){return this._info||this.primary},set em(t){this._em=t},get em(){return this._em||this.primary.underline},set heading(t){this._heading=t},get heading(){return this._heading||this.muted.underline},set pending(t){this._pending=t},get pending(){return this._pending||this.primary},set submitted(t){this._submitted=t},get submitted(){return this._submitted||this.success},set cancelled(t){this._cancelled=t},get cancelled(){return this._cancelled||this.danger},set typing(t){this._typing=t},get typing(){return this._typing||this.dim},set placeholder(t){this._placeholder=t},get placeholder(){return this._placeholder||this.primary.dim},set highlight(t){this._highlight=t},get highlight(){return this._highlight||this.inverse}};xG.merge=(t={})=>{t.styles&&typeof t.styles.enabled=="boolean"&&(mo.enabled=t.styles.enabled),t.styles&&typeof t.styles.visible=="boolean"&&(mo.visible=t.styles.visible);let e=PG.merge({},xG,t.styles);delete e.merge;for(let r of Object.keys(mo))e.hasOwnProperty(r)||Reflect.defineProperty(e,r,{get:()=>mo[r]});for(let r of Object.keys(mo.styles))e.hasOwnProperty(r)||Reflect.defineProperty(e,r,{get:()=>mo[r]});return e};hwe.exports=xG});var mwe=L((krr,dwe)=>{"use strict";var kG=process.platform==="win32",Xp=Ju(),Twt=$o(),QG={...Xp.symbols,upDownDoubleArrow:"\u21D5",upDownDoubleArrow2:"\u2B0D",upDownArrow:"\u2195",asterisk:"*",asterism:"\u2042",bulletWhite:"\u25E6",electricArrow:"\u2301",ellipsisLarge:"\u22EF",ellipsisSmall:"\u2026",fullBlock:"\u2588",identicalTo:"\u2261",indicator:Xp.symbols.check,leftAngle:"\u2039",mark:"\u203B",minus:"\u2212",multiplication:"\xD7",obelus:"\xF7",percent:"%",pilcrow:"\xB6",pilcrow2:"\u2761",pencilUpRight:"\u2710",pencilDownRight:"\u270E",pencilRight:"\u270F",plus:"+",plusMinus:"\xB1",pointRight:"\u261E",rightAngle:"\u203A",section:"\xA7",hexagon:{off:"\u2B21",on:"\u2B22",disabled:"\u2B22"},ballot:{on:"\u2611",off:"\u2610",disabled:"\u2612"},stars:{on:"\u2605",off:"\u2606",disabled:"\u2606"},folder:{on:"\u25BC",off:"\u25B6",disabled:"\u25B6"},prefix:{pending:Xp.symbols.question,submitted:Xp.symbols.check,cancelled:Xp.symbols.cross},separator:{pending:Xp.symbols.pointerSmall,submitted:Xp.symbols.middot,cancelled:Xp.symbols.middot},radio:{off:kG?"( )":"\u25EF",on:kG?"(*)":"\u25C9",disabled:kG?"(|)":"\u24BE"},numbers:["\u24EA","\u2460","\u2461","\u2462","\u2463","\u2464","\u2465","\u2466","\u2467","\u2468","\u2469","\u246A","\u246B","\u246C","\u246D","\u246E","\u246F","\u2470","\u2471","\u2472","\u2473","\u3251","\u3252","\u3253","\u3254","\u3255","\u3256","\u3257","\u3258","\u3259","\u325A","\u325B","\u325C","\u325D","\u325E","\u325F","\u32B1","\u32B2","\u32B3","\u32B4","\u32B5","\u32B6","\u32B7","\u32B8","\u32B9","\u32BA","\u32BB","\u32BC","\u32BD","\u32BE","\u32BF"]};QG.merge=t=>{let e=Twt.merge({},Xp.symbols,QG,t.symbols);return delete e.merge,e};dwe.exports=QG});var Ewe=L((Qrr,ywe)=>{"use strict";var Rwt=gwe(),Fwt=mwe(),Nwt=$o();ywe.exports=t=>{t.options=Nwt.merge({},t.options.theme,t.options),t.symbols=Fwt.merge(t.options),t.styles=Rwt.merge(t.options)}});var vwe=L((wwe,Bwe)=>{"use strict";var Iwe=process.env.TERM_PROGRAM==="Apple_Terminal",Owt=Ju(),TG=$o(),zu=Bwe.exports=wwe,Ui="\x1B[",Cwe="\x07",RG=!1,H0=zu.code={bell:Cwe,beep:Cwe,beginning:`${Ui}G`,down:`${Ui}J`,esc:Ui,getPosition:`${Ui}6n`,hide:`${Ui}?25l`,line:`${Ui}2K`,lineEnd:`${Ui}K`,lineStart:`${Ui}1K`,restorePosition:Ui+(Iwe?"8":"u"),savePosition:Ui+(Iwe?"7":"s"),screen:`${Ui}2J`,show:`${Ui}?25h`,up:`${Ui}1J`},Cm=zu.cursor={get hidden(){return RG},hide(){return RG=!0,H0.hide},show(){return RG=!1,H0.show},forward:(t=1)=>`${Ui}${t}C`,backward:(t=1)=>`${Ui}${t}D`,nextLine:(t=1)=>`${Ui}E`.repeat(t),prevLine:(t=1)=>`${Ui}F`.repeat(t),up:(t=1)=>t?`${Ui}${t}A`:"",down:(t=1)=>t?`${Ui}${t}B`:"",right:(t=1)=>t?`${Ui}${t}C`:"",left:(t=1)=>t?`${Ui}${t}D`:"",to(t,e){return e?`${Ui}${e+1};${t+1}H`:`${Ui}${t+1}G`},move(t=0,e=0){let r="";return r+=t<0?Cm.left(-t):t>0?Cm.right(t):"",r+=e<0?Cm.up(-e):e>0?Cm.down(e):"",r},restore(t={}){let{after:e,cursor:r,initial:s,input:a,prompt:n,size:c,value:f}=t;if(s=TG.isPrimitive(s)?String(s):"",a=TG.isPrimitive(a)?String(a):"",f=TG.isPrimitive(f)?String(f):"",c){let p=zu.cursor.up(c)+zu.cursor.to(n.length),h=a.length-r;return h>0&&(p+=zu.cursor.left(h)),p}if(f||e){let p=!a&&s?-s.length:-a.length+r;return e&&(p-=e.length),a===""&&s&&!n.includes(s)&&(p+=s.length),zu.cursor.move(p)}}},FG=zu.erase={screen:H0.screen,up:H0.up,down:H0.down,line:H0.line,lineEnd:H0.lineEnd,lineStart:H0.lineStart,lines(t){let e="";for(let r=0;r{if(!e)return FG.line+Cm.to(0);let r=n=>[...Owt.unstyle(n)].length,s=t.split(/\r?\n/),a=0;for(let n of s)a+=1+Math.floor(Math.max(r(n)-1,0)/e);return(FG.line+Cm.prevLine()).repeat(a-1)+FG.line+Cm.to(0)}});var nC=L((Trr,Dwe)=>{"use strict";var Lwt=Ie("events"),Swe=Ju(),NG=cwe(),Mwt=fwe(),_wt=pwe(),Uwt=Ewe(),hl=$o(),wm=vwe(),OG=class t extends Lwt{constructor(e={}){super(),this.name=e.name,this.type=e.type,this.options=e,Uwt(this),Mwt(this),this.state=new _wt(this),this.initial=[e.initial,e.default].find(r=>r!=null),this.stdout=e.stdout||process.stdout,this.stdin=e.stdin||process.stdin,this.scale=e.scale||1,this.term=this.options.term||process.env.TERM_PROGRAM,this.margin=jwt(this.options.margin),this.setMaxListeners(0),Hwt(this)}async keypress(e,r={}){this.keypressed=!0;let s=NG.action(e,NG(e,r),this.options.actions);this.state.keypress=s,this.emit("keypress",e,s),this.emit("state",this.state.clone());let a=this.options[s.action]||this[s.action]||this.dispatch;if(typeof a=="function")return await a.call(this,e,s);this.alert()}alert(){delete this.state.alert,this.options.show===!1?this.emit("alert"):this.stdout.write(wm.code.beep)}cursorHide(){this.stdout.write(wm.cursor.hide()),hl.onExit(()=>this.cursorShow())}cursorShow(){this.stdout.write(wm.cursor.show())}write(e){e&&(this.stdout&&this.state.show!==!1&&this.stdout.write(e),this.state.buffer+=e)}clear(e=0){let r=this.state.buffer;this.state.buffer="",!(!r&&!e||this.options.show===!1)&&this.stdout.write(wm.cursor.down(e)+wm.clear(r,this.width))}restore(){if(this.state.closed||this.options.show===!1)return;let{prompt:e,after:r,rest:s}=this.sections(),{cursor:a,initial:n="",input:c="",value:f=""}=this,p=this.state.size=s.length,h={after:r,cursor:a,initial:n,input:c,prompt:e,size:p,value:f},E=wm.cursor.restore(h);E&&this.stdout.write(E)}sections(){let{buffer:e,input:r,prompt:s}=this.state;s=Swe.unstyle(s);let a=Swe.unstyle(e),n=a.indexOf(s),c=a.slice(0,n),p=a.slice(n).split(` `),h=p[0],E=p[p.length-1],S=(s+(r?" "+r:"")).length,P=Se.call(this,this.value),this.result=()=>s.call(this,this.value),typeof r.initial=="function"&&(this.initial=await r.initial.call(this,this)),typeof r.onRun=="function"&&await r.onRun.call(this,this),typeof r.onSubmit=="function"){let a=r.onSubmit.bind(this),n=this.submit.bind(this);delete this.options.onSubmit,this.submit=async()=>(await a(this.name,this.value,this),n())}await this.start(),await this.render()}render(){throw new Error("expected prompt to have a custom render method")}run(){return new Promise(async(e,r)=>{if(this.once("submit",e),this.once("cancel",r),await this.skip())return this.render=()=>{},this.submit();await this.initialize(),this.emit("run")})}async element(e,r,s){let{options:a,state:n,symbols:c,timers:f}=this,p=f&&f[e];n.timer=p;let h=a[e]||n[e]||c[e],E=r&&r[e]!=null?r[e]:await h;if(E==="")return E;let C=await this.resolve(E,n,r,s);return!C&&r&&r[e]?this.resolve(h,n,r,s):C}async prefix(){let e=await this.element("prefix")||this.symbols,r=this.timers&&this.timers.prefix,s=this.state;return s.timer=r,hl.isObject(e)&&(e=e[s.status]||e.pending),hl.hasColor(e)?e:(this.styles[s.status]||this.styles.pending)(e)}async message(){let e=await this.element("message");return hl.hasColor(e)?e:this.styles.strong(e)}async separator(){let e=await this.element("separator")||this.symbols,r=this.timers&&this.timers.separator,s=this.state;s.timer=r;let a=e[s.status]||e.pending||s.separator,n=await this.resolve(a,s);return hl.isObject(n)&&(n=n[s.status]||n.pending),hl.hasColor(n)?n:this.styles.muted(n)}async pointer(e,r){let s=await this.element("pointer",e,r);if(typeof s=="string"&&hl.hasColor(s))return s;if(s){let a=this.styles,n=this.index===r,c=n?a.primary:h=>h,f=await this.resolve(s[n?"on":"off"]||s,this.state),p=hl.hasColor(f)?f:c(f);return n?p:" ".repeat(f.length)}}async indicator(e,r){let s=await this.element("indicator",e,r);if(typeof s=="string"&&hl.hasColor(s))return s;if(s){let a=this.styles,n=e.enabled===!0,c=n?a.success:a.dark,f=s[n?"on":"off"]||s;return hl.hasColor(f)?f:c(f)}return""}body(){return null}footer(){if(this.state.status==="pending")return this.element("footer")}header(){if(this.state.status==="pending")return this.element("header")}async hint(){if(this.state.status==="pending"&&!this.isValue(this.state.input)){let e=await this.element("hint");return hl.hasColor(e)?e:this.styles.muted(e)}}error(e){return this.state.submitted?"":e||this.state.error}format(e){return e}result(e){return e}validate(e){return this.options.required===!0?this.isValue(e):!0}isValue(e){return e!=null&&e!==""}resolve(e,...r){return hl.resolve(this,e,...r)}get base(){return t.prototype}get style(){return this.styles[this.state.status]}get height(){return this.options.rows||hl.height(this.stdout,25)}get width(){return this.options.columns||hl.width(this.stdout,80)}get size(){return{width:this.width,height:this.height}}set cursor(e){this.state.cursor=e}get cursor(){return this.state.cursor}set input(e){this.state.input=e}get input(){return this.state.input}set value(e){this.state.value=e}get value(){let{input:e,value:r}=this.state,s=[r,e].find(this.isValue.bind(this));return this.isValue(s)?s:this.initial}static get prompt(){return e=>new this(e).run()}};function Hwt(t){let e=a=>t[a]===void 0||typeof t[a]=="function",r=["actions","choices","initial","margin","roles","styles","symbols","theme","timers","value"],s=["body","footer","error","header","hint","indicator","message","prefix","separator","skip"];for(let a of Object.keys(t.options)){if(r.includes(a)||/^on[A-Z]/.test(a))continue;let n=t.options[a];typeof n=="function"&&e(a)?s.includes(a)||(t[a]=n.bind(t)):typeof t[a]!="function"&&(t[a]=n)}}function jwt(t){typeof t=="number"&&(t=[t,t,t,t]);let e=[].concat(t||[]),r=a=>a%2===0?` `:" ",s=[];for(let a=0;a<4;a++){let n=r(a);e[a]?s.push(n.repeat(e[a])):s.push("")}return s}Dwe.exports=OG});var xwe=L((Rrr,Pwe)=>{"use strict";var qwt=$o(),bwe={default(t,e){return e},checkbox(t,e){throw new Error("checkbox role is not implemented yet")},editable(t,e){throw new Error("editable role is not implemented yet")},expandable(t,e){throw new Error("expandable role is not implemented yet")},heading(t,e){return e.disabled="",e.indicator=[e.indicator," "].find(r=>r!=null),e.message=e.message||"",e},input(t,e){throw new Error("input role is not implemented yet")},option(t,e){return bwe.default(t,e)},radio(t,e){throw new Error("radio role is not implemented yet")},separator(t,e){return e.disabled="",e.indicator=[e.indicator," "].find(r=>r!=null),e.message=e.message||t.symbols.line.repeat(5),e},spacer(t,e){return e}};Pwe.exports=(t,e={})=>{let r=qwt.merge({},bwe,e.roles);return r[t]||r.default}});var tS=L((Frr,Twe)=>{"use strict";var Gwt=Ju(),Wwt=nC(),Ywt=xwe(),ZR=$o(),{reorder:LG,scrollUp:Vwt,scrollDown:Kwt,isObject:kwe,swap:Jwt}=ZR,MG=class extends Wwt{constructor(e){super(e),this.cursorHide(),this.maxSelected=e.maxSelected||1/0,this.multiple=e.multiple||!1,this.initial=e.initial||0,this.delay=e.delay||0,this.longest=0,this.num=""}async initialize(){typeof this.options.initial=="function"&&(this.initial=await this.options.initial.call(this)),await this.reset(!0),await super.initialize()}async reset(){let{choices:e,initial:r,autofocus:s,suggest:a}=this.options;if(this.state._choices=[],this.state.choices=[],this.choices=await Promise.all(await this.toChoices(e)),this.choices.forEach(n=>n.enabled=!1),typeof a!="function"&&this.selectable.length===0)throw new Error("At least one choice must be selectable");kwe(r)&&(r=Object.keys(r)),Array.isArray(r)?(s!=null&&(this.index=this.findIndex(s)),r.forEach(n=>this.enable(this.find(n))),await this.render()):(s!=null&&(r=s),typeof r=="string"&&(r=this.findIndex(r)),typeof r=="number"&&r>-1&&(this.index=Math.max(0,Math.min(r,this.choices.length)),this.enable(this.find(this.index)))),this.isDisabled(this.focused)&&await this.down()}async toChoices(e,r){this.state.loadingChoices=!0;let s=[],a=0,n=async(c,f)=>{typeof c=="function"&&(c=await c.call(this)),c instanceof Promise&&(c=await c);for(let p=0;p(this.state.loadingChoices=!1,c))}async toChoice(e,r,s){if(typeof e=="function"&&(e=await e.call(this,this)),e instanceof Promise&&(e=await e),typeof e=="string"&&(e={name:e}),e.normalized)return e;e.normalized=!0;let a=e.value;if(e=Ywt(e.role,this.options)(this,e),typeof e.disabled=="string"&&!e.hint&&(e.hint=e.disabled,e.disabled=!0),e.disabled===!0&&e.hint==null&&(e.hint="(disabled)"),e.index!=null)return e;e.name=e.name||e.key||e.title||e.value||e.message,e.message=e.message||e.name||"",e.value=[e.value,e.name].find(this.isValue.bind(this)),e.input="",e.index=r,e.cursor=0,ZR.define(e,"parent",s),e.level=s?s.level+1:1,e.indent==null&&(e.indent=s?s.indent+" ":e.indent||""),e.path=s?s.path+"."+e.name:e.name,e.enabled=!!(this.multiple&&!this.isDisabled(e)&&(e.enabled||this.isSelected(e))),this.isDisabled(e)||(this.longest=Math.max(this.longest,Gwt.unstyle(e.message).length));let c={...e};return e.reset=(f=c.input,p=c.value)=>{for(let h of Object.keys(c))e[h]=c[h];e.input=f,e.value=p},a==null&&typeof e.initial=="function"&&(e.input=await e.initial.call(this,this.state,e,r)),e}async onChoice(e,r){this.emit("choice",e,r,this),typeof e.onChoice=="function"&&await e.onChoice.call(this,this.state,e,r)}async addChoice(e,r,s){let a=await this.toChoice(e,r,s);return this.choices.push(a),this.index=this.choices.length-1,this.limit=this.choices.length,a}async newItem(e,r,s){let a={name:"New choice name?",editable:!0,newChoice:!0,...e},n=await this.addChoice(a,r,s);return n.updateChoice=()=>{delete n.newChoice,n.name=n.message=n.input,n.input="",n.cursor=0},this.render()}indent(e){return e.indent==null?e.level>1?" ".repeat(e.level-1):"":e.indent}dispatch(e,r){if(this.multiple&&this[r.name])return this[r.name]();this.alert()}focus(e,r){return typeof r!="boolean"&&(r=e.enabled),r&&!e.enabled&&this.selected.length>=this.maxSelected?this.alert():(this.index=e.index,e.enabled=r&&!this.isDisabled(e),e)}space(){return this.multiple?(this.toggle(this.focused),this.render()):this.alert()}a(){if(this.maxSelectedr.enabled);return this.choices.forEach(r=>r.enabled=!e),this.render()}i(){return this.choices.length-this.selected.length>this.maxSelected?this.alert():(this.choices.forEach(e=>e.enabled=!e.enabled),this.render())}g(e=this.focused){return this.choices.some(r=>!!r.parent)?(this.toggle(e.parent&&!e.choices?e.parent:e),this.render()):this.a()}toggle(e,r){if(!e.enabled&&this.selected.length>=this.maxSelected)return this.alert();typeof r!="boolean"&&(r=!e.enabled),e.enabled=r,e.choices&&e.choices.forEach(a=>this.toggle(a,r));let s=e.parent;for(;s;){let a=s.choices.filter(n=>this.isDisabled(n));s.enabled=a.every(n=>n.enabled===!0),s=s.parent}return Qwe(this,this.choices),this.emit("toggle",e,this),e}enable(e){return this.selected.length>=this.maxSelected?this.alert():(e.enabled=!this.isDisabled(e),e.choices&&e.choices.forEach(this.enable.bind(this)),e)}disable(e){return e.enabled=!1,e.choices&&e.choices.forEach(this.disable.bind(this)),e}number(e){this.num+=e;let r=s=>{let a=Number(s);if(a>this.choices.length-1)return this.alert();let n=this.focused,c=this.choices.find(f=>a===f.index);if(!c.enabled&&this.selected.length>=this.maxSelected)return this.alert();if(this.visible.indexOf(c)===-1){let f=LG(this.choices),p=f.indexOf(c);if(n.index>p){let h=f.slice(p,p+this.limit),E=f.filter(C=>!h.includes(C));this.choices=h.concat(E)}else{let h=p-this.limit+1;this.choices=f.slice(h).concat(f.slice(0,h))}}return this.index=this.choices.indexOf(c),this.toggle(this.focused),this.render()};return clearTimeout(this.numberTimeout),new Promise(s=>{let a=this.choices.length,n=this.num,c=(f=!1,p)=>{clearTimeout(this.numberTimeout),f&&(p=r(n)),this.num="",s(p)};if(n==="0"||n.length===1&&+(n+"0")>a)return c(!0);if(Number(n)>a)return c(!1,this.alert());this.numberTimeout=setTimeout(()=>c(!0),this.delay)})}home(){return this.choices=LG(this.choices),this.index=0,this.render()}end(){let e=this.choices.length-this.limit,r=LG(this.choices);return this.choices=r.slice(e).concat(r.slice(0,e)),this.index=this.limit-1,this.render()}first(){return this.index=0,this.render()}last(){return this.index=this.visible.length-1,this.render()}prev(){return this.visible.length<=1?this.alert():this.up()}next(){return this.visible.length<=1?this.alert():this.down()}right(){return this.cursor>=this.input.length?this.alert():(this.cursor++,this.render())}left(){return this.cursor<=0?this.alert():(this.cursor--,this.render())}up(){let e=this.choices.length,r=this.visible.length,s=this.index;return this.options.scroll===!1&&s===0?this.alert():e>r&&s===0?this.scrollUp():(this.index=(s-1%e+e)%e,this.isDisabled()?this.up():this.render())}down(){let e=this.choices.length,r=this.visible.length,s=this.index;return this.options.scroll===!1&&s===r-1?this.alert():e>r&&s===r-1?this.scrollDown():(this.index=(s+1)%e,this.isDisabled()?this.down():this.render())}scrollUp(e=0){return this.choices=Vwt(this.choices),this.index=e,this.isDisabled()?this.up():this.render()}scrollDown(e=this.visible.length-1){return this.choices=Kwt(this.choices),this.index=e,this.isDisabled()?this.down():this.render()}async shiftUp(){if(this.options.sort===!0){this.sorting=!0,this.swap(this.index-1),await this.up(),this.sorting=!1;return}return this.scrollUp(this.index)}async shiftDown(){if(this.options.sort===!0){this.sorting=!0,this.swap(this.index+1),await this.down(),this.sorting=!1;return}return this.scrollDown(this.index)}pageUp(){return this.visible.length<=1?this.alert():(this.limit=Math.max(this.limit-1,0),this.index=Math.min(this.limit-1,this.index),this._limit=this.limit,this.isDisabled()?this.up():this.render())}pageDown(){return this.visible.length>=this.choices.length?this.alert():(this.index=Math.max(0,this.index),this.limit=Math.min(this.limit+1,this.choices.length),this._limit=this.limit,this.isDisabled()?this.down():this.render())}swap(e){Jwt(this.choices,this.index,e)}isDisabled(e=this.focused){return e&&["disabled","collapsed","hidden","completing","readonly"].some(s=>e[s]===!0)?!0:e&&e.role==="heading"}isEnabled(e=this.focused){if(Array.isArray(e))return e.every(r=>this.isEnabled(r));if(e.choices){let r=e.choices.filter(s=>!this.isDisabled(s));return e.enabled&&r.every(s=>this.isEnabled(s))}return e.enabled&&!this.isDisabled(e)}isChoice(e,r){return e.name===r||e.index===Number(r)}isSelected(e){return Array.isArray(this.initial)?this.initial.some(r=>this.isChoice(e,r)):this.isChoice(e,this.initial)}map(e=[],r="value"){return[].concat(e||[]).reduce((s,a)=>(s[a]=this.find(a,r),s),{})}filter(e,r){let a=typeof e=="function"?e:(f,p)=>[f.name,p].includes(e),c=(this.options.multiple?this.state._choices:this.choices).filter(a);return r?c.map(f=>f[r]):c}find(e,r){if(kwe(e))return r?e[r]:e;let a=typeof e=="function"?e:(c,f)=>[c.name,f].includes(e),n=this.choices.find(a);if(n)return r?n[r]:n}findIndex(e){return this.choices.indexOf(this.find(e))}async submit(){let e=this.focused;if(!e)return this.alert();if(e.newChoice)return e.input?(e.updateChoice(),this.render()):this.alert();if(this.choices.some(c=>c.newChoice))return this.alert();let{reorder:r,sort:s}=this.options,a=this.multiple===!0,n=this.selected;return n===void 0?this.alert():(Array.isArray(n)&&r!==!1&&s!==!0&&(n=ZR.reorder(n)),this.value=a?n.map(c=>c.name):n.name,super.submit())}set choices(e=[]){this.state._choices=this.state._choices||[],this.state.choices=e;for(let r of e)this.state._choices.some(s=>s.name===r.name)||this.state._choices.push(r);if(!this._initial&&this.options.initial){this._initial=!0;let r=this.initial;if(typeof r=="string"||typeof r=="number"){let s=this.find(r);s&&(this.initial=s.index,this.focus(s,!0))}}}get choices(){return Qwe(this,this.state.choices||[])}set visible(e){this.state.visible=e}get visible(){return(this.state.visible||this.choices).slice(0,this.limit)}set limit(e){this.state.limit=e}get limit(){let{state:e,options:r,choices:s}=this,a=e.limit||this._limit||r.limit||s.length;return Math.min(a,this.height)}set value(e){super.value=e}get value(){return typeof super.value!="string"&&super.value===this.initial?this.input:super.value}set index(e){this.state.index=e}get index(){return Math.max(0,this.state?this.state.index:0)}get enabled(){return this.filter(this.isEnabled.bind(this))}get focused(){let e=this.choices[this.index];return e&&this.state.submitted&&this.multiple!==!0&&(e.enabled=!0),e}get selectable(){return this.choices.filter(e=>!this.isDisabled(e))}get selected(){return this.multiple?this.enabled:this.focused}};function Qwe(t,e){if(e instanceof Promise)return e;if(typeof e=="function"){if(ZR.isAsyncFn(e))return e;e=e.call(t,t)}for(let r of e){if(Array.isArray(r.choices)){let s=r.choices.filter(a=>!t.isDisabled(a));r.enabled=s.every(a=>a.enabled===!0)}t.isDisabled(r)===!0&&delete r.enabled}return e}Twe.exports=MG});var j0=L((Nrr,Rwe)=>{"use strict";var zwt=tS(),_G=$o(),UG=class extends zwt{constructor(e){super(e),this.emptyError=this.options.emptyError||"No items were selected"}async dispatch(e,r){if(this.multiple)return this[r.name]?await this[r.name](e,r):await super.dispatch(e,r);this.alert()}separator(){if(this.options.separator)return super.separator();let e=this.styles.muted(this.symbols.ellipsis);return this.state.submitted?super.separator():e}pointer(e,r){return!this.multiple||this.options.pointer?super.pointer(e,r):""}indicator(e,r){return this.multiple?super.indicator(e,r):""}choiceMessage(e,r){let s=this.resolve(e.message,this.state,e,r);return e.role==="heading"&&!_G.hasColor(s)&&(s=this.styles.strong(s)),this.resolve(s,this.state,e,r)}choiceSeparator(){return":"}async renderChoice(e,r){await this.onChoice(e,r);let s=this.index===r,a=await this.pointer(e,r),n=await this.indicator(e,r)+(e.pad||""),c=await this.resolve(e.hint,this.state,e,r);c&&!_G.hasColor(c)&&(c=this.styles.muted(c));let f=this.indent(e),p=await this.choiceMessage(e,r),h=()=>[this.margin[3],f+a+n,p,this.margin[1],c].filter(Boolean).join(" ");return e.role==="heading"?h():e.disabled?(_G.hasColor(p)||(p=this.styles.disabled(p)),h()):(s&&(p=this.styles.em(p)),h())}async renderChoices(){if(this.state.loading==="choices")return this.styles.warning("Loading choices");if(this.state.submitted)return"";let e=this.visible.map(async(n,c)=>await this.renderChoice(n,c)),r=await Promise.all(e);r.length||r.push(this.styles.danger("No matching choices"));let s=this.margin[0]+r.join(` `),a;return this.options.choicesHeader&&(a=await this.resolve(this.options.choicesHeader,this.state)),[a,s].filter(Boolean).join(` `)}format(){return!this.state.submitted||this.state.cancelled?"":Array.isArray(this.selected)?this.selected.map(e=>this.styles.primary(e.name)).join(", "):this.styles.primary(this.selected.name)}async render(){let{submitted:e,size:r}=this.state,s="",a=await this.header(),n=await this.prefix(),c=await this.separator(),f=await this.message();this.options.promptLine!==!1&&(s=[n,f,c,""].join(" "),this.state.prompt=s);let p=await this.format(),h=await this.error()||await this.hint(),E=await this.renderChoices(),C=await this.footer();p&&(s+=p),h&&!s.includes(h)&&(s+=" "+h),e&&!p&&!E.trim()&&this.multiple&&this.emptyError!=null&&(s+=this.styles.danger(this.emptyError)),this.clear(r),this.write([a,s,E,C].filter(Boolean).join(` `)),this.write(this.margin[2]),this.restore()}};Rwe.exports=UG});var Nwe=L((Orr,Fwe)=>{"use strict";var Zwt=j0(),Xwt=(t,e)=>{let r=t.toLowerCase();return s=>{let n=s.toLowerCase().indexOf(r),c=e(s.slice(n,n+r.length));return n>=0?s.slice(0,n)+c+s.slice(n+r.length):s}},HG=class extends Zwt{constructor(e){super(e),this.cursorShow()}moveCursor(e){this.state.cursor+=e}dispatch(e){return this.append(e)}space(e){return this.options.multiple?super.space(e):this.append(e)}append(e){let{cursor:r,input:s}=this.state;return this.input=s.slice(0,r)+e+s.slice(r),this.moveCursor(1),this.complete()}delete(){let{cursor:e,input:r}=this.state;return r?(this.input=r.slice(0,e-1)+r.slice(e),this.moveCursor(-1),this.complete()):this.alert()}deleteForward(){let{cursor:e,input:r}=this.state;return r[e]===void 0?this.alert():(this.input=`${r}`.slice(0,e)+`${r}`.slice(e+1),this.complete())}number(e){return this.append(e)}async complete(){this.completing=!0,this.choices=await this.suggest(this.input,this.state._choices),this.state.limit=void 0,this.index=Math.min(Math.max(this.visible.length-1,0),this.index),await this.render(),this.completing=!1}suggest(e=this.input,r=this.state._choices){if(typeof this.options.suggest=="function")return this.options.suggest.call(this,e,r);let s=e.toLowerCase();return r.filter(a=>a.message.toLowerCase().includes(s))}pointer(){return""}format(){if(!this.focused)return this.input;if(this.options.multiple&&this.state.submitted)return this.selected.map(e=>this.styles.primary(e.message)).join(", ");if(this.state.submitted){let e=this.value=this.input=this.focused.value;return this.styles.primary(e)}return this.input}async render(){if(this.state.status!=="pending")return super.render();let e=this.options.highlight?this.options.highlight.bind(this):this.styles.placeholder,r=Xwt(this.input,e),s=this.choices;this.choices=s.map(a=>({...a,message:r(a.message)})),await super.render(),this.choices=s}submit(){return this.options.multiple&&(this.value=this.selected.map(e=>e.name)),super.submit()}};Fwe.exports=HG});var qG=L((Lrr,Owe)=>{"use strict";var jG=$o();Owe.exports=(t,e={})=>{t.cursorHide();let{input:r="",initial:s="",pos:a,showCursor:n=!0,color:c}=e,f=c||t.styles.placeholder,p=jG.inverse(t.styles.primary),h=R=>p(t.styles.black(R)),E=r,C=" ",S=h(C);if(t.blink&&t.blink.off===!0&&(h=R=>R,S=""),n&&a===0&&s===""&&r==="")return h(C);if(n&&a===0&&(r===s||r===""))return h(s[0])+f(s.slice(1));s=jG.isPrimitive(s)?`${s}`:"",r=jG.isPrimitive(r)?`${r}`:"";let P=s&&s.startsWith(r)&&s!==r,I=P?h(s[r.length]):S;if(a!==r.length&&n===!0&&(E=r.slice(0,a)+h(r[a])+r.slice(a+1),I=""),n===!1&&(I=""),P){let R=t.styles.unstyle(E+I);return E+I+f(s.slice(R.length))}return E+I}});var XR=L((Mrr,Lwe)=>{"use strict";var $wt=Ju(),e1t=j0(),t1t=qG(),GG=class extends e1t{constructor(e){super({...e,multiple:!0}),this.type="form",this.initial=this.options.initial,this.align=[this.options.align,"right"].find(r=>r!=null),this.emptyError="",this.values={}}async reset(e){return await super.reset(),e===!0&&(this._index=this.index),this.index=this._index,this.values={},this.choices.forEach(r=>r.reset&&r.reset()),this.render()}dispatch(e){return!!e&&this.append(e)}append(e){let r=this.focused;if(!r)return this.alert();let{cursor:s,input:a}=r;return r.value=r.input=a.slice(0,s)+e+a.slice(s),r.cursor++,this.render()}delete(){let e=this.focused;if(!e||e.cursor<=0)return this.alert();let{cursor:r,input:s}=e;return e.value=e.input=s.slice(0,r-1)+s.slice(r),e.cursor--,this.render()}deleteForward(){let e=this.focused;if(!e)return this.alert();let{cursor:r,input:s}=e;if(s[r]===void 0)return this.alert();let a=`${s}`.slice(0,r)+`${s}`.slice(r+1);return e.value=e.input=a,this.render()}right(){let e=this.focused;return e?e.cursor>=e.input.length?this.alert():(e.cursor++,this.render()):this.alert()}left(){let e=this.focused;return e?e.cursor<=0?this.alert():(e.cursor--,this.render()):this.alert()}space(e,r){return this.dispatch(e,r)}number(e,r){return this.dispatch(e,r)}next(){let e=this.focused;if(!e)return this.alert();let{initial:r,input:s}=e;return r&&r.startsWith(s)&&s!==r?(e.value=e.input=r,e.cursor=e.value.length,this.render()):super.next()}prev(){let e=this.focused;return e?e.cursor===0?super.prev():(e.value=e.input="",e.cursor=0,this.render()):this.alert()}separator(){return""}format(e){return this.state.submitted?"":super.format(e)}pointer(){return""}indicator(e){return e.input?"\u29BF":"\u2299"}async choiceSeparator(e,r){let s=await this.resolve(e.separator,this.state,e,r)||":";return s?" "+this.styles.disabled(s):""}async renderChoice(e,r){await this.onChoice(e,r);let{state:s,styles:a}=this,{cursor:n,initial:c="",name:f,hint:p,input:h=""}=e,{muted:E,submitted:C,primary:S,danger:P}=a,I=p,R=this.index===r,N=e.validate||(()=>!0),U=await this.choiceSeparator(e,r),W=e.message;this.align==="right"&&(W=W.padStart(this.longest+1," ")),this.align==="left"&&(W=W.padEnd(this.longest+1," "));let te=this.values[f]=h||c,ie=h?"success":"dark";await N.call(e,te,this.state)!==!0&&(ie="danger");let Ae=a[ie],ce=Ae(await this.indicator(e,r))+(e.pad||""),me=this.indent(e),pe=()=>[me,ce,W+U,h,I].filter(Boolean).join(" ");if(s.submitted)return W=$wt.unstyle(W),h=C(h),I="",pe();if(e.format)h=await e.format.call(this,h,e,r);else{let Be=this.styles.muted;h=t1t(this,{input:h,initial:c,pos:n,showCursor:R,color:Be})}return this.isValue(h)||(h=this.styles.muted(this.symbols.ellipsis)),e.result&&(this.values[f]=await e.result.call(this,te,e,r)),R&&(W=S(W)),e.error?h+=(h?" ":"")+P(e.error.trim()):e.hint&&(h+=(h?" ":"")+E(e.hint.trim())),pe()}async submit(){return this.value=this.values,super.base.submit.call(this)}};Lwe.exports=GG});var WG=L((_rr,_we)=>{"use strict";var r1t=XR(),n1t=()=>{throw new Error("expected prompt to have a custom authenticate method")},Mwe=(t=n1t)=>{class e extends r1t{constructor(s){super(s)}async submit(){this.value=await t.call(this,this.values,this.state),super.base.submit.call(this)}static create(s){return Mwe(s)}}return e};_we.exports=Mwe()});var jwe=L((Urr,Hwe)=>{"use strict";var i1t=WG();function s1t(t,e){return t.username===this.options.username&&t.password===this.options.password}var Uwe=(t=s1t)=>{let e=[{name:"username",message:"username"},{name:"password",message:"password",format(s){return this.options.showPassword?s:(this.state.submitted?this.styles.primary:this.styles.muted)(this.symbols.asterisk.repeat(s.length))}}];class r extends i1t.create(t){constructor(a){super({...a,choices:e})}static create(a){return Uwe(a)}}return r};Hwe.exports=Uwe()});var $R=L((Hrr,qwe)=>{"use strict";var o1t=nC(),{isPrimitive:a1t,hasColor:l1t}=$o(),YG=class extends o1t{constructor(e){super(e),this.cursorHide()}async initialize(){let e=await this.resolve(this.initial,this.state);this.input=await this.cast(e),await super.initialize()}dispatch(e){return this.isValue(e)?(this.input=e,this.submit()):this.alert()}format(e){let{styles:r,state:s}=this;return s.submitted?r.success(e):r.primary(e)}cast(e){return this.isTrue(e)}isTrue(e){return/^[ty1]/i.test(e)}isFalse(e){return/^[fn0]/i.test(e)}isValue(e){return a1t(e)&&(this.isTrue(e)||this.isFalse(e))}async hint(){if(this.state.status==="pending"){let e=await this.element("hint");return l1t(e)?e:this.styles.muted(e)}}async render(){let{input:e,size:r}=this.state,s=await this.prefix(),a=await this.separator(),n=await this.message(),c=this.styles.muted(this.default),f=[s,n,c,a].filter(Boolean).join(" ");this.state.prompt=f;let p=await this.header(),h=this.value=this.cast(e),E=await this.format(h),C=await this.error()||await this.hint(),S=await this.footer();C&&!f.includes(C)&&(E+=" "+C),f+=" "+E,this.clear(r),this.write([p,f,S].filter(Boolean).join(` `)),this.restore()}set value(e){super.value=e}get value(){return this.cast(super.value)}};qwe.exports=YG});var Wwe=L((jrr,Gwe)=>{"use strict";var c1t=$R(),VG=class extends c1t{constructor(e){super(e),this.default=this.options.default||(this.initial?"(Y/n)":"(y/N)")}};Gwe.exports=VG});var Vwe=L((qrr,Ywe)=>{"use strict";var u1t=j0(),f1t=XR(),iC=f1t.prototype,KG=class extends u1t{constructor(e){super({...e,multiple:!0}),this.align=[this.options.align,"left"].find(r=>r!=null),this.emptyError="",this.values={}}dispatch(e,r){let s=this.focused,a=s.parent||{};return!s.editable&&!a.editable&&(e==="a"||e==="i")?super[e]():iC.dispatch.call(this,e,r)}append(e,r){return iC.append.call(this,e,r)}delete(e,r){return iC.delete.call(this,e,r)}space(e){return this.focused.editable?this.append(e):super.space()}number(e){return this.focused.editable?this.append(e):super.number(e)}next(){return this.focused.editable?iC.next.call(this):super.next()}prev(){return this.focused.editable?iC.prev.call(this):super.prev()}async indicator(e,r){let s=e.indicator||"",a=e.editable?s:super.indicator(e,r);return await this.resolve(a,this.state,e,r)||""}indent(e){return e.role==="heading"?"":e.editable?" ":" "}async renderChoice(e,r){return e.indent="",e.editable?iC.renderChoice.call(this,e,r):super.renderChoice(e,r)}error(){return""}footer(){return this.state.error}async validate(){let e=!0;for(let r of this.choices){if(typeof r.validate!="function"||r.role==="heading")continue;let s=r.parent?this.value[r.parent.name]:this.value;if(r.editable?s=r.value===r.name?r.initial||"":r.value:this.isDisabled(r)||(s=r.enabled===!0),e=await r.validate(s,this.state),e!==!0)break}return e!==!0&&(this.state.error=typeof e=="string"?e:"Invalid Input"),e}submit(){if(this.focused.newChoice===!0)return super.submit();if(this.choices.some(e=>e.newChoice))return this.alert();this.value={};for(let e of this.choices){let r=e.parent?this.value[e.parent.name]:this.value;if(e.role==="heading"){this.value[e.name]={};continue}e.editable?r[e.name]=e.value===e.name?e.initial||"":e.value:this.isDisabled(e)||(r[e.name]=e.enabled===!0)}return this.base.submit.call(this)}};Ywe.exports=KG});var Bm=L((Grr,Kwe)=>{"use strict";var A1t=nC(),p1t=qG(),{isPrimitive:h1t}=$o(),JG=class extends A1t{constructor(e){super(e),this.initial=h1t(this.initial)?String(this.initial):"",this.initial&&this.cursorHide(),this.state.prevCursor=0,this.state.clipboard=[]}async keypress(e,r={}){let s=this.state.prevKeypress;return this.state.prevKeypress=r,this.options.multiline===!0&&r.name==="return"&&(!s||s.name!=="return")?this.append(` `,r):super.keypress(e,r)}moveCursor(e){this.cursor+=e}reset(){return this.input=this.value="",this.cursor=0,this.render()}dispatch(e,r){if(!e||r.ctrl||r.code)return this.alert();this.append(e)}append(e){let{cursor:r,input:s}=this.state;this.input=`${s}`.slice(0,r)+e+`${s}`.slice(r),this.moveCursor(String(e).length),this.render()}insert(e){this.append(e)}delete(){let{cursor:e,input:r}=this.state;if(e<=0)return this.alert();this.input=`${r}`.slice(0,e-1)+`${r}`.slice(e),this.moveCursor(-1),this.render()}deleteForward(){let{cursor:e,input:r}=this.state;if(r[e]===void 0)return this.alert();this.input=`${r}`.slice(0,e)+`${r}`.slice(e+1),this.render()}cutForward(){let e=this.cursor;if(this.input.length<=e)return this.alert();this.state.clipboard.push(this.input.slice(e)),this.input=this.input.slice(0,e),this.render()}cutLeft(){let e=this.cursor;if(e===0)return this.alert();let r=this.input.slice(0,e),s=this.input.slice(e),a=r.split(" ");this.state.clipboard.push(a.pop()),this.input=a.join(" "),this.cursor=this.input.length,this.input+=s,this.render()}paste(){if(!this.state.clipboard.length)return this.alert();this.insert(this.state.clipboard.pop()),this.render()}toggleCursor(){this.state.prevCursor?(this.cursor=this.state.prevCursor,this.state.prevCursor=0):(this.state.prevCursor=this.cursor,this.cursor=0),this.render()}first(){this.cursor=0,this.render()}last(){this.cursor=this.input.length-1,this.render()}next(){let e=this.initial!=null?String(this.initial):"";if(!e||!e.startsWith(this.input))return this.alert();this.input=this.initial,this.cursor=this.initial.length,this.render()}prev(){if(!this.input)return this.alert();this.reset()}backward(){return this.left()}forward(){return this.right()}right(){return this.cursor>=this.input.length?this.alert():(this.moveCursor(1),this.render())}left(){return this.cursor<=0?this.alert():(this.moveCursor(-1),this.render())}isValue(e){return!!e}async format(e=this.value){let r=await this.resolve(this.initial,this.state);return this.state.submitted?this.styles.submitted(e||r):p1t(this,{input:e,initial:r,pos:this.cursor})}async render(){let e=this.state.size,r=await this.prefix(),s=await this.separator(),a=await this.message(),n=[r,a,s].filter(Boolean).join(" ");this.state.prompt=n;let c=await this.header(),f=await this.format(),p=await this.error()||await this.hint(),h=await this.footer();p&&!f.includes(p)&&(f+=" "+p),n+=" "+f,this.clear(e),this.write([c,n,h].filter(Boolean).join(` `)),this.restore()}};Kwe.exports=JG});var zwe=L((Wrr,Jwe)=>{"use strict";var g1t=t=>t.filter((e,r)=>t.lastIndexOf(e)===r),eF=t=>g1t(t).filter(Boolean);Jwe.exports=(t,e={},r="")=>{let{past:s=[],present:a=""}=e,n,c;switch(t){case"prev":case"undo":return n=s.slice(0,s.length-1),c=s[s.length-1]||"",{past:eF([r,...n]),present:c};case"next":case"redo":return n=s.slice(1),c=s[0]||"",{past:eF([...n,r]),present:c};case"save":return{past:eF([...s,r]),present:""};case"remove":return c=eF(s.filter(f=>f!==r)),a="",c.length&&(a=c.pop()),{past:c,present:a};default:throw new Error(`Invalid action: "${t}"`)}}});var ZG=L((Yrr,Xwe)=>{"use strict";var d1t=Bm(),Zwe=zwe(),zG=class extends d1t{constructor(e){super(e);let r=this.options.history;if(r&&r.store){let s=r.values||this.initial;this.autosave=!!r.autosave,this.store=r.store,this.data=this.store.get("values")||{past:[],present:s},this.initial=this.data.present||this.data.past[this.data.past.length-1]}}completion(e){return this.store?(this.data=Zwe(e,this.data,this.input),this.data.present?(this.input=this.data.present,this.cursor=this.input.length,this.render()):this.alert()):this.alert()}altUp(){return this.completion("prev")}altDown(){return this.completion("next")}prev(){return this.save(),super.prev()}save(){this.store&&(this.data=Zwe("save",this.data,this.input),this.store.set("values",this.data))}submit(){return this.store&&this.autosave===!0&&this.save(),super.submit()}};Xwe.exports=zG});var e1e=L((Vrr,$we)=>{"use strict";var m1t=Bm(),XG=class extends m1t{format(){return""}};$we.exports=XG});var r1e=L((Krr,t1e)=>{"use strict";var y1t=Bm(),$G=class extends y1t{constructor(e={}){super(e),this.sep=this.options.separator||/, */,this.initial=e.initial||""}split(e=this.value){return e?String(e).split(this.sep):[]}format(){let e=this.state.submitted?this.styles.primary:r=>r;return this.list.map(e).join(", ")}async submit(e){let r=this.state.error||await this.validate(this.list,this.state);return r!==!0?(this.state.error=r,super.submit()):(this.value=this.list,super.submit())}get list(){return this.split()}};t1e.exports=$G});var i1e=L((Jrr,n1e)=>{"use strict";var E1t=j0(),e5=class extends E1t{constructor(e){super({...e,multiple:!0})}};n1e.exports=e5});var r5=L((zrr,s1e)=>{"use strict";var I1t=Bm(),t5=class extends I1t{constructor(e={}){super({style:"number",...e}),this.min=this.isValue(e.min)?this.toNumber(e.min):-1/0,this.max=this.isValue(e.max)?this.toNumber(e.max):1/0,this.delay=e.delay!=null?e.delay:1e3,this.float=e.float!==!1,this.round=e.round===!0||e.float===!1,this.major=e.major||10,this.minor=e.minor||1,this.initial=e.initial!=null?e.initial:"",this.input=String(this.initial),this.cursor=this.input.length,this.cursorShow()}append(e){return!/[-+.]/.test(e)||e==="."&&this.input.includes(".")?this.alert("invalid number"):super.append(e)}number(e){return super.append(e)}next(){return this.input&&this.input!==this.initial?this.alert():this.isValue(this.initial)?(this.input=this.initial,this.cursor=String(this.initial).length,this.render()):this.alert()}up(e){let r=e||this.minor,s=this.toNumber(this.input);return s>this.max+r?this.alert():(this.input=`${s+r}`,this.render())}down(e){let r=e||this.minor,s=this.toNumber(this.input);return sthis.isValue(r));return this.value=this.toNumber(e||0),super.submit()}};s1e.exports=t5});var a1e=L((Zrr,o1e)=>{o1e.exports=r5()});var c1e=L((Xrr,l1e)=>{"use strict";var C1t=Bm(),n5=class extends C1t{constructor(e){super(e),this.cursorShow()}format(e=this.input){return this.keypressed?(this.state.submitted?this.styles.primary:this.styles.muted)(this.symbols.asterisk.repeat(e.length)):""}};l1e.exports=n5});var A1e=L(($rr,f1e)=>{"use strict";var w1t=Ju(),B1t=tS(),u1e=$o(),i5=class extends B1t{constructor(e={}){super(e),this.widths=[].concat(e.messageWidth||50),this.align=[].concat(e.align||"left"),this.linebreak=e.linebreak||!1,this.edgeLength=e.edgeLength||3,this.newline=e.newline||` `;let r=e.startNumber||1;typeof this.scale=="number"&&(this.scaleKey=!1,this.scale=Array(this.scale).fill(0).map((s,a)=>({name:a+r})))}async reset(){return this.tableized=!1,await super.reset(),this.render()}tableize(){if(this.tableized===!0)return;this.tableized=!0;let e=0;for(let r of this.choices){e=Math.max(e,r.message.length),r.scaleIndex=r.initial||2,r.scale=[];for(let s=0;s=this.scale.length-1?this.alert():(e.scaleIndex++,this.render())}left(){let e=this.focused;return e.scaleIndex<=0?this.alert():(e.scaleIndex--,this.render())}indent(){return""}format(){return this.state.submitted?this.choices.map(r=>this.styles.info(r.index)).join(", "):""}pointer(){return""}renderScaleKey(){return this.scaleKey===!1||this.state.submitted?"":["",...this.scale.map(s=>` ${s.name} - ${s.message}`)].map(s=>this.styles.muted(s)).join(` `)}renderScaleHeading(e){let r=this.scale.map(p=>p.name);typeof this.options.renderScaleHeading=="function"&&(r=this.options.renderScaleHeading.call(this,e));let s=this.scaleLength-r.join("").length,a=Math.round(s/(r.length-1)),c=r.map(p=>this.styles.strong(p)).join(" ".repeat(a)),f=" ".repeat(this.widths[0]);return this.margin[3]+f+this.margin[1]+c}scaleIndicator(e,r,s){if(typeof this.options.scaleIndicator=="function")return this.options.scaleIndicator.call(this,e,r,s);let a=e.scaleIndex===r.index;return r.disabled?this.styles.hint(this.symbols.radio.disabled):a?this.styles.success(this.symbols.radio.on):this.symbols.radio.off}renderScale(e,r){let s=e.scale.map(n=>this.scaleIndicator(e,n,r)),a=this.term==="Hyper"?"":" ";return s.join(a+this.symbols.line.repeat(this.edgeLength))}async renderChoice(e,r){await this.onChoice(e,r);let s=this.index===r,a=await this.pointer(e,r),n=await e.hint;n&&!u1e.hasColor(n)&&(n=this.styles.muted(n));let c=I=>this.margin[3]+I.replace(/\s+$/,"").padEnd(this.widths[0]," "),f=this.newline,p=this.indent(e),h=await this.resolve(e.message,this.state,e,r),E=await this.renderScale(e,r),C=this.margin[1]+this.margin[3];this.scaleLength=w1t.unstyle(E).length,this.widths[0]=Math.min(this.widths[0],this.width-this.scaleLength-C.length);let P=u1e.wordWrap(h,{width:this.widths[0],newline:f}).split(` `).map(I=>c(I)+this.margin[1]);return s&&(E=this.styles.info(E),P=P.map(I=>this.styles.info(I))),P[0]+=E,this.linebreak&&P.push(""),[p+a,P.join(` `)].filter(Boolean)}async renderChoices(){if(this.state.submitted)return"";this.tableize();let e=this.visible.map(async(a,n)=>await this.renderChoice(a,n)),r=await Promise.all(e),s=await this.renderScaleHeading();return this.margin[0]+[s,...r.map(a=>a.join(" "))].join(` `)}async render(){let{submitted:e,size:r}=this.state,s=await this.prefix(),a=await this.separator(),n=await this.message(),c="";this.options.promptLine!==!1&&(c=[s,n,a,""].join(" "),this.state.prompt=c);let f=await this.header(),p=await this.format(),h=await this.renderScaleKey(),E=await this.error()||await this.hint(),C=await this.renderChoices(),S=await this.footer(),P=this.emptyError;p&&(c+=p),E&&!c.includes(E)&&(c+=" "+E),e&&!p&&!C.trim()&&this.multiple&&P!=null&&(c+=this.styles.danger(P)),this.clear(r),this.write([f,c,h,C,S].filter(Boolean).join(` `)),this.state.submitted||this.write(this.margin[2]),this.restore()}submit(){this.value={};for(let e of this.choices)this.value[e.name]=e.scaleIndex;return this.base.submit.call(this)}};f1e.exports=i5});var g1e=L((enr,h1e)=>{"use strict";var p1e=Ju(),v1t=(t="")=>typeof t=="string"?t.replace(/^['"]|['"]$/g,""):"",o5=class{constructor(e){this.name=e.key,this.field=e.field||{},this.value=v1t(e.initial||this.field.initial||""),this.message=e.message||this.name,this.cursor=0,this.input="",this.lines=[]}},S1t=async(t={},e={},r=s=>s)=>{let s=new Set,a=t.fields||[],n=t.template,c=[],f=[],p=[],h=1;typeof n=="function"&&(n=await n());let E=-1,C=()=>n[++E],S=()=>n[E+1],P=I=>{I.line=h,c.push(I)};for(P({type:"bos",value:""});Eie.name===U.key);U.field=a.find(ie=>ie.name===U.key),te||(te=new o5(U),f.push(te)),te.lines.push(U.line-1);continue}let R=c[c.length-1];R.type==="text"&&R.line===h?R.value+=I:P({type:"text",value:I})}return P({type:"eos",value:""}),{input:n,tabstops:c,unique:s,keys:p,items:f}};h1e.exports=async t=>{let e=t.options,r=new Set(e.required===!0?[]:e.required||[]),s={...e.values,...e.initial},{tabstops:a,items:n,keys:c}=await S1t(e,s),f=s5("result",t,e),p=s5("format",t,e),h=s5("validate",t,e,!0),E=t.isValue.bind(t);return async(C={},S=!1)=>{let P=0;C.required=r,C.items=n,C.keys=c,C.output="";let I=async(W,te,ie,Ae)=>{let ce=await h(W,te,ie,Ae);return ce===!1?"Invalid field "+ie.name:ce};for(let W of a){let te=W.value,ie=W.key;if(W.type!=="template"){te&&(C.output+=te);continue}if(W.type==="template"){let Ae=n.find(Ce=>Ce.name===ie);e.required===!0&&C.required.add(Ae.name);let ce=[Ae.input,C.values[Ae.value],Ae.value,te].find(E),pe=(Ae.field||{}).message||W.inner;if(S){let Ce=await I(C.values[ie],C,Ae,P);if(Ce&&typeof Ce=="string"||Ce===!1){C.invalid.set(ie,Ce);continue}C.invalid.delete(ie);let g=await f(C.values[ie],C,Ae,P);C.output+=p1e.unstyle(g);continue}Ae.placeholder=!1;let Be=te;te=await p(te,C,Ae,P),ce!==te?(C.values[ie]=ce,te=t.styles.typing(ce),C.missing.delete(pe)):(C.values[ie]=void 0,ce=`<${pe}>`,te=t.styles.primary(ce),Ae.placeholder=!0,C.required.has(ie)&&C.missing.add(pe)),C.missing.has(pe)&&C.validating&&(te=t.styles.warning(ce)),C.invalid.has(ie)&&C.validating&&(te=t.styles.danger(ce)),P===C.index&&(Be!==te?te=t.styles.underline(te):te=t.styles.heading(p1e.unstyle(te))),P++}te&&(C.output+=te)}let R=C.output.split(` `).map(W=>" "+W),N=n.length,U=0;for(let W of n)C.invalid.has(W.name)&&W.lines.forEach(te=>{R[te][0]===" "&&(R[te]=C.styles.danger(C.symbols.bullet)+R[te].slice(1))}),t.isValue(C.values[W.name])&&U++;return C.completed=(U/N*100).toFixed(0),C.output=R.join(` `),C.output}};function s5(t,e,r,s){return(a,n,c,f)=>typeof c.field[t]=="function"?c.field[t].call(e,a,n,c,f):[s,a].find(p=>e.isValue(p))}});var m1e=L((tnr,d1e)=>{"use strict";var D1t=Ju(),b1t=g1e(),P1t=nC(),a5=class extends P1t{constructor(e){super(e),this.cursorHide(),this.reset(!0)}async initialize(){this.interpolate=await b1t(this),await super.initialize()}async reset(e){this.state.keys=[],this.state.invalid=new Map,this.state.missing=new Set,this.state.completed=0,this.state.values={},e!==!0&&(await this.initialize(),await this.render())}moveCursor(e){let r=this.getItem();this.cursor+=e,r.cursor+=e}dispatch(e,r){if(!r.code&&!r.ctrl&&e!=null&&this.getItem()){this.append(e,r);return}this.alert()}append(e,r){let s=this.getItem(),a=s.input.slice(0,this.cursor),n=s.input.slice(this.cursor);this.input=s.input=`${a}${e}${n}`,this.moveCursor(1),this.render()}delete(){let e=this.getItem();if(this.cursor<=0||!e.input)return this.alert();let r=e.input.slice(this.cursor),s=e.input.slice(0,this.cursor-1);this.input=e.input=`${s}${r}`,this.moveCursor(-1),this.render()}increment(e){return e>=this.state.keys.length-1?0:e+1}decrement(e){return e<=0?this.state.keys.length-1:e-1}first(){this.state.index=0,this.render()}last(){this.state.index=this.state.keys.length-1,this.render()}right(){if(this.cursor>=this.input.length)return this.alert();this.moveCursor(1),this.render()}left(){if(this.cursor<=0)return this.alert();this.moveCursor(-1),this.render()}prev(){this.state.index=this.decrement(this.state.index),this.getItem(),this.render()}next(){this.state.index=this.increment(this.state.index),this.getItem(),this.render()}up(){this.prev()}down(){this.next()}format(e){let r=this.state.completed<100?this.styles.warning:this.styles.success;return this.state.submitted===!0&&this.state.completed!==100&&(r=this.styles.danger),r(`${this.state.completed}% completed`)}async render(){let{index:e,keys:r=[],submitted:s,size:a}=this.state,n=[this.options.newline,` `].find(W=>W!=null),c=await this.prefix(),f=await this.separator(),p=await this.message(),h=[c,p,f].filter(Boolean).join(" ");this.state.prompt=h;let E=await this.header(),C=await this.error()||"",S=await this.hint()||"",P=s?"":await this.interpolate(this.state),I=this.state.key=r[e]||"",R=await this.format(I),N=await this.footer();R&&(h+=" "+R),S&&!R&&this.state.completed===0&&(h+=" "+S),this.clear(a);let U=[E,h,P,N,C.trim()];this.write(U.filter(Boolean).join(n)),this.restore()}getItem(e){let{items:r,keys:s,index:a}=this.state,n=r.find(c=>c.name===s[a]);return n&&n.input!=null&&(this.input=n.input,this.cursor=n.cursor),n}async submit(){typeof this.interpolate!="function"&&await this.initialize(),await this.interpolate(this.state,!0);let{invalid:e,missing:r,output:s,values:a}=this.state;if(e.size){let f="";for(let[p,h]of e)f+=`Invalid ${p}: ${h} `;return this.state.error=f,super.submit()}if(r.size)return this.state.error="Required: "+[...r.keys()].join(", "),super.submit();let c=D1t.unstyle(s).split(` `).map(f=>f.slice(1)).join(` `);return this.value={values:a,result:c},super.submit()}};d1e.exports=a5});var E1e=L((rnr,y1e)=>{"use strict";var x1t="(Use + to sort)",k1t=j0(),l5=class extends k1t{constructor(e){super({...e,reorder:!1,sort:!0,multiple:!0}),this.state.hint=[this.options.hint,x1t].find(this.isValue.bind(this))}indicator(){return""}async renderChoice(e,r){let s=await super.renderChoice(e,r),a=this.symbols.identicalTo+" ",n=this.index===r&&this.sorting?this.styles.muted(a):" ";return this.options.drag===!1&&(n=""),this.options.numbered===!0?n+`${r+1} - `+s:n+s}get selected(){return this.choices}submit(){return this.value=this.choices.map(e=>e.value),super.submit()}};y1e.exports=l5});var C1e=L((nnr,I1e)=>{"use strict";var Q1t=tS(),c5=class extends Q1t{constructor(e={}){if(super(e),this.emptyError=e.emptyError||"No items were selected",this.term=process.env.TERM_PROGRAM,!this.options.header){let r=["","4 - Strongly Agree","3 - Agree","2 - Neutral","1 - Disagree","0 - Strongly Disagree",""];r=r.map(s=>this.styles.muted(s)),this.state.header=r.join(` `)}}async toChoices(...e){if(this.createdScales)return!1;this.createdScales=!0;let r=await super.toChoices(...e);for(let s of r)s.scale=T1t(5,this.options),s.scaleIdx=2;return r}dispatch(){this.alert()}space(){let e=this.focused,r=e.scale[e.scaleIdx],s=r.selected;return e.scale.forEach(a=>a.selected=!1),r.selected=!s,this.render()}indicator(){return""}pointer(){return""}separator(){return this.styles.muted(this.symbols.ellipsis)}right(){let e=this.focused;return e.scaleIdx>=e.scale.length-1?this.alert():(e.scaleIdx++,this.render())}left(){let e=this.focused;return e.scaleIdx<=0?this.alert():(e.scaleIdx--,this.render())}indent(){return" "}async renderChoice(e,r){await this.onChoice(e,r);let s=this.index===r,a=this.term==="Hyper",n=a?9:8,c=a?"":" ",f=this.symbols.line.repeat(n),p=" ".repeat(n+(a?0:1)),h=te=>(te?this.styles.success("\u25C9"):"\u25EF")+c,E=r+1+".",C=s?this.styles.heading:this.styles.noop,S=await this.resolve(e.message,this.state,e,r),P=this.indent(e),I=P+e.scale.map((te,ie)=>h(ie===e.scaleIdx)).join(f),R=te=>te===e.scaleIdx?C(te):te,N=P+e.scale.map((te,ie)=>R(ie)).join(p),U=()=>[E,S].filter(Boolean).join(" "),W=()=>[U(),I,N," "].filter(Boolean).join(` `);return s&&(I=this.styles.cyan(I),N=this.styles.cyan(N)),W()}async renderChoices(){if(this.state.submitted)return"";let e=this.visible.map(async(s,a)=>await this.renderChoice(s,a)),r=await Promise.all(e);return r.length||r.push(this.styles.danger("No matching choices")),r.join(` `)}format(){return this.state.submitted?this.choices.map(r=>this.styles.info(r.scaleIdx)).join(", "):""}async render(){let{submitted:e,size:r}=this.state,s=await this.prefix(),a=await this.separator(),n=await this.message(),c=[s,n,a].filter(Boolean).join(" ");this.state.prompt=c;let f=await this.header(),p=await this.format(),h=await this.error()||await this.hint(),E=await this.renderChoices(),C=await this.footer();(p||!h)&&(c+=" "+p),h&&!c.includes(h)&&(c+=" "+h),e&&!p&&!E&&this.multiple&&this.type!=="form"&&(c+=this.styles.danger(this.emptyError)),this.clear(r),this.write([c,f,E,C].filter(Boolean).join(` `)),this.restore()}submit(){this.value={};for(let e of this.choices)this.value[e.name]=e.scaleIdx;return this.base.submit.call(this)}};function T1t(t,e={}){if(Array.isArray(e.scale))return e.scale.map(s=>({...s}));let r=[];for(let s=1;s{w1e.exports=ZG()});var S1e=L((snr,v1e)=>{"use strict";var R1t=$R(),u5=class extends R1t{async initialize(){await super.initialize(),this.value=this.initial=!!this.options.initial,this.disabled=this.options.disabled||"no",this.enabled=this.options.enabled||"yes",await this.render()}reset(){this.value=this.initial,this.render()}delete(){this.alert()}toggle(){this.value=!this.value,this.render()}enable(){if(this.value===!0)return this.alert();this.value=!0,this.render()}disable(){if(this.value===!1)return this.alert();this.value=!1,this.render()}up(){this.toggle()}down(){this.toggle()}right(){this.toggle()}left(){this.toggle()}next(){this.toggle()}prev(){this.toggle()}dispatch(e="",r){switch(e.toLowerCase()){case" ":return this.toggle();case"1":case"y":case"t":return this.enable();case"0":case"n":case"f":return this.disable();default:return this.alert()}}format(){let e=s=>this.styles.primary.underline(s);return[this.value?this.disabled:e(this.disabled),this.value?e(this.enabled):this.enabled].join(this.styles.muted(" / "))}async render(){let{size:e}=this.state,r=await this.header(),s=await this.prefix(),a=await this.separator(),n=await this.message(),c=await this.format(),f=await this.error()||await this.hint(),p=await this.footer(),h=[s,n,a,c].join(" ");this.state.prompt=h,f&&!h.includes(f)&&(h+=" "+f),this.clear(e),this.write([r,h,p].filter(Boolean).join(` `)),this.write(this.margin[2]),this.restore()}};v1e.exports=u5});var b1e=L((onr,D1e)=>{"use strict";var F1t=j0(),f5=class extends F1t{constructor(e){if(super(e),typeof this.options.correctChoice!="number"||this.options.correctChoice<0)throw new Error("Please specify the index of the correct answer from the list of choices")}async toChoices(e,r){let s=await super.toChoices(e,r);if(s.length<2)throw new Error("Please give at least two choices to the user");if(this.options.correctChoice>s.length)throw new Error("Please specify the index of the correct answer from the list of choices");return s}check(e){return e.index===this.options.correctChoice}async result(e){return{selectedAnswer:e,correctAnswer:this.options.choices[this.options.correctChoice].value,correct:await this.check(this.state)}}};D1e.exports=f5});var x1e=L(A5=>{"use strict";var P1e=$o(),Ts=(t,e)=>{P1e.defineExport(A5,t,e),P1e.defineExport(A5,t.toLowerCase(),e)};Ts("AutoComplete",()=>Nwe());Ts("BasicAuth",()=>jwe());Ts("Confirm",()=>Wwe());Ts("Editable",()=>Vwe());Ts("Form",()=>XR());Ts("Input",()=>ZG());Ts("Invisible",()=>e1e());Ts("List",()=>r1e());Ts("MultiSelect",()=>i1e());Ts("Numeral",()=>a1e());Ts("Password",()=>c1e());Ts("Scale",()=>A1e());Ts("Select",()=>j0());Ts("Snippet",()=>m1e());Ts("Sort",()=>E1e());Ts("Survey",()=>C1e());Ts("Text",()=>B1e());Ts("Toggle",()=>S1e());Ts("Quiz",()=>b1e())});var Q1e=L((lnr,k1e)=>{k1e.exports={ArrayPrompt:tS(),AuthPrompt:WG(),BooleanPrompt:$R(),NumberPrompt:r5(),StringPrompt:Bm()}});var nS=L((cnr,R1e)=>{"use strict";var T1e=Ie("assert"),h5=Ie("events"),q0=$o(),Zu=class extends h5{constructor(e,r){super(),this.options=q0.merge({},e),this.answers={...r}}register(e,r){if(q0.isObject(e)){for(let a of Object.keys(e))this.register(a,e[a]);return this}T1e.equal(typeof r,"function","expected a function");let s=e.toLowerCase();return r.prototype instanceof this.Prompt?this.prompts[s]=r:this.prompts[s]=r(this.Prompt,this),this}async prompt(e=[]){for(let r of[].concat(e))try{typeof r=="function"&&(r=await r.call(this)),await this.ask(q0.merge({},this.options,r))}catch(s){return Promise.reject(s)}return this.answers}async ask(e){typeof e=="function"&&(e=await e.call(this));let r=q0.merge({},this.options,e),{type:s,name:a}=e,{set:n,get:c}=q0;if(typeof s=="function"&&(s=await s.call(this,e,this.answers)),!s)return this.answers[a];T1e(this.prompts[s],`Prompt "${s}" is not registered`);let f=new this.prompts[s](r),p=c(this.answers,a);f.state.answers=this.answers,f.enquirer=this,a&&f.on("submit",E=>{this.emit("answer",a,E,f),n(this.answers,a,E)});let h=f.emit.bind(f);return f.emit=(...E)=>(this.emit.call(this,...E),h(...E)),this.emit("prompt",f,this),r.autofill&&p!=null?(f.value=f.input=p,r.autofill==="show"&&await f.submit()):p=f.value=await f.run(),p}use(e){return e.call(this,this),this}set Prompt(e){this._Prompt=e}get Prompt(){return this._Prompt||this.constructor.Prompt}get prompts(){return this.constructor.prompts}static set Prompt(e){this._Prompt=e}static get Prompt(){return this._Prompt||nC()}static get prompts(){return x1e()}static get types(){return Q1e()}static get prompt(){let e=(r,...s)=>{let a=new this(...s),n=a.emit.bind(a);return a.emit=(...c)=>(e.emit(...c),n(...c)),a.prompt(r)};return q0.mixinEmitter(e,new h5),e}};q0.mixinEmitter(Zu,new h5);var p5=Zu.prompts;for(let t of Object.keys(p5)){let e=t.toLowerCase(),r=s=>new p5[t](s).run();Zu.prompt[e]=r,Zu[e]=r,Zu[t]||Reflect.defineProperty(Zu,t,{get:()=>p5[t]})}var rS=t=>{q0.defineExport(Zu,t,()=>Zu.types[t])};rS("ArrayPrompt");rS("AuthPrompt");rS("BooleanPrompt");rS("NumberPrompt");rS("StringPrompt");R1e.exports=Zu});var aS=L((Wnr,U1e)=>{var H1t=HR();function j1t(t,e,r){var s=t==null?void 0:H1t(t,e);return s===void 0?r:s}U1e.exports=j1t});var q1e=L((Znr,j1e)=>{function q1t(t,e){for(var r=-1,s=t==null?0:t.length;++r{var G1t=Vd(),W1t=Lk();function Y1t(t,e){return t&&G1t(e,W1t(e),t)}G1e.exports=Y1t});var V1e=L(($nr,Y1e)=>{var V1t=Vd(),K1t=jE();function J1t(t,e){return t&&V1t(e,K1t(e),t)}Y1e.exports=J1t});var J1e=L((eir,K1e)=>{var z1t=Vd(),Z1t=Qk();function X1t(t,e){return z1t(t,Z1t(t),e)}K1e.exports=X1t});var I5=L((tir,z1e)=>{var $1t=kk(),e2t=jk(),t2t=Qk(),r2t=k4(),n2t=Object.getOwnPropertySymbols,i2t=n2t?function(t){for(var e=[];t;)$1t(e,t2t(t)),t=e2t(t);return e}:r2t;z1e.exports=i2t});var X1e=L((rir,Z1e)=>{var s2t=Vd(),o2t=I5();function a2t(t,e){return s2t(t,o2t(t),e)}Z1e.exports=a2t});var C5=L((nir,$1e)=>{var l2t=x4(),c2t=I5(),u2t=jE();function f2t(t){return l2t(t,u2t,c2t)}$1e.exports=f2t});var t2e=L((iir,e2e)=>{var A2t=Object.prototype,p2t=A2t.hasOwnProperty;function h2t(t){var e=t.length,r=new t.constructor(e);return e&&typeof t[0]=="string"&&p2t.call(t,"index")&&(r.index=t.index,r.input=t.input),r}e2e.exports=h2t});var n2e=L((sir,r2e)=>{var g2t=Uk();function d2t(t,e){var r=e?g2t(t.buffer):t.buffer;return new t.constructor(r,t.byteOffset,t.byteLength)}r2e.exports=d2t});var s2e=L((oir,i2e)=>{var m2t=/\w*$/;function y2t(t){var e=new t.constructor(t.source,m2t.exec(t));return e.lastIndex=t.lastIndex,e}i2e.exports=y2t});var u2e=L((air,c2e)=>{var o2e=Gd(),a2e=o2e?o2e.prototype:void 0,l2e=a2e?a2e.valueOf:void 0;function E2t(t){return l2e?Object(l2e.call(t)):{}}c2e.exports=E2t});var A2e=L((lir,f2e)=>{var I2t=Uk(),C2t=n2e(),w2t=s2e(),B2t=u2e(),v2t=G4(),S2t="[object Boolean]",D2t="[object Date]",b2t="[object Map]",P2t="[object Number]",x2t="[object RegExp]",k2t="[object Set]",Q2t="[object String]",T2t="[object Symbol]",R2t="[object ArrayBuffer]",F2t="[object DataView]",N2t="[object Float32Array]",O2t="[object Float64Array]",L2t="[object Int8Array]",M2t="[object Int16Array]",_2t="[object Int32Array]",U2t="[object Uint8Array]",H2t="[object Uint8ClampedArray]",j2t="[object Uint16Array]",q2t="[object Uint32Array]";function G2t(t,e,r){var s=t.constructor;switch(e){case R2t:return I2t(t);case S2t:case D2t:return new s(+t);case F2t:return C2t(t,r);case N2t:case O2t:case L2t:case M2t:case _2t:case U2t:case H2t:case j2t:case q2t:return v2t(t,r);case b2t:return new s;case P2t:case Q2t:return new s(t);case x2t:return w2t(t);case k2t:return new s;case T2t:return B2t(t)}}f2e.exports=G2t});var h2e=L((cir,p2e)=>{var W2t=FB(),Y2t=zf(),V2t="[object Map]";function K2t(t){return Y2t(t)&&W2t(t)==V2t}p2e.exports=K2t});var y2e=L((uir,m2e)=>{var J2t=h2e(),z2t=Rk(),g2e=Fk(),d2e=g2e&&g2e.isMap,Z2t=d2e?z2t(d2e):J2t;m2e.exports=Z2t});var I2e=L((fir,E2e)=>{var X2t=FB(),$2t=zf(),eBt="[object Set]";function tBt(t){return $2t(t)&&X2t(t)==eBt}E2e.exports=tBt});var v2e=L((Air,B2e)=>{var rBt=I2e(),nBt=Rk(),C2e=Fk(),w2e=C2e&&C2e.isSet,iBt=w2e?nBt(w2e):rBt;B2e.exports=iBt});var w5=L((pir,P2e)=>{var sBt=Pk(),oBt=q1e(),aBt=qk(),lBt=W1e(),cBt=V1e(),uBt=q4(),fBt=Hk(),ABt=J1e(),pBt=X1e(),hBt=F4(),gBt=C5(),dBt=FB(),mBt=t2e(),yBt=A2e(),EBt=W4(),IBt=xc(),CBt=xB(),wBt=y2e(),BBt=Wl(),vBt=v2e(),SBt=Lk(),DBt=jE(),bBt=1,PBt=2,xBt=4,S2e="[object Arguments]",kBt="[object Array]",QBt="[object Boolean]",TBt="[object Date]",RBt="[object Error]",D2e="[object Function]",FBt="[object GeneratorFunction]",NBt="[object Map]",OBt="[object Number]",b2e="[object Object]",LBt="[object RegExp]",MBt="[object Set]",_Bt="[object String]",UBt="[object Symbol]",HBt="[object WeakMap]",jBt="[object ArrayBuffer]",qBt="[object DataView]",GBt="[object Float32Array]",WBt="[object Float64Array]",YBt="[object Int8Array]",VBt="[object Int16Array]",KBt="[object Int32Array]",JBt="[object Uint8Array]",zBt="[object Uint8ClampedArray]",ZBt="[object Uint16Array]",XBt="[object Uint32Array]",Ci={};Ci[S2e]=Ci[kBt]=Ci[jBt]=Ci[qBt]=Ci[QBt]=Ci[TBt]=Ci[GBt]=Ci[WBt]=Ci[YBt]=Ci[VBt]=Ci[KBt]=Ci[NBt]=Ci[OBt]=Ci[b2e]=Ci[LBt]=Ci[MBt]=Ci[_Bt]=Ci[UBt]=Ci[JBt]=Ci[zBt]=Ci[ZBt]=Ci[XBt]=!0;Ci[RBt]=Ci[D2e]=Ci[HBt]=!1;function rF(t,e,r,s,a,n){var c,f=e&bBt,p=e&PBt,h=e&xBt;if(r&&(c=a?r(t,s,a,n):r(t)),c!==void 0)return c;if(!BBt(t))return t;var E=IBt(t);if(E){if(c=mBt(t),!f)return fBt(t,c)}else{var C=dBt(t),S=C==D2e||C==FBt;if(CBt(t))return uBt(t,f);if(C==b2e||C==S2e||S&&!a){if(c=p||S?{}:EBt(t),!f)return p?pBt(t,cBt(c,t)):ABt(t,lBt(c,t))}else{if(!Ci[C])return a?t:{};c=yBt(t,C,f)}}n||(n=new sBt);var P=n.get(t);if(P)return P;n.set(t,c),vBt(t)?t.forEach(function(N){c.add(rF(N,e,r,N,t,n))}):wBt(t)&&t.forEach(function(N,U){c.set(U,rF(N,e,r,U,t,n))});var I=h?p?gBt:hBt:p?DBt:SBt,R=E?void 0:I(t);return oBt(R||t,function(N,U){R&&(U=N,N=t[U]),aBt(c,U,rF(N,e,r,U,t,n))}),c}P2e.exports=rF});var B5=L((hir,x2e)=>{var $Bt=w5(),evt=1,tvt=4;function rvt(t){return $Bt(t,evt|tvt)}x2e.exports=rvt});var v5=L((gir,k2e)=>{var nvt=hG();function ivt(t,e,r){return t==null?t:nvt(t,e,r)}k2e.exports=ivt});var N2e=L((Cir,F2e)=>{var svt=Object.prototype,ovt=svt.hasOwnProperty;function avt(t,e){return t!=null&&ovt.call(t,e)}F2e.exports=avt});var L2e=L((wir,O2e)=>{var lvt=N2e(),cvt=gG();function uvt(t,e){return t!=null&&cvt(t,e,lvt)}O2e.exports=uvt});var _2e=L((Bir,M2e)=>{function fvt(t){var e=t==null?0:t.length;return e?t[e-1]:void 0}M2e.exports=fvt});var H2e=L((vir,U2e)=>{var Avt=HR(),pvt=s6();function hvt(t,e){return e.length<2?t:Avt(t,pvt(e,0,-1))}U2e.exports=hvt});var D5=L((Sir,j2e)=>{var gvt=Im(),dvt=_2e(),mvt=H2e(),yvt=zI();function Evt(t,e){return e=gvt(e,t),t=mvt(t,e),t==null||delete t[yvt(dvt(e))]}j2e.exports=Evt});var b5=L((Dir,q2e)=>{var Ivt=D5();function Cvt(t,e){return t==null?!0:Ivt(t,e)}q2e.exports=Cvt});var K2e=L((tsr,vvt)=>{vvt.exports={name:"@yarnpkg/cli",version:"4.9.2",license:"BSD-2-Clause",main:"./sources/index.ts",exports:{".":"./sources/index.ts","./polyfills":"./sources/polyfills.ts","./package.json":"./package.json"},dependencies:{"@yarnpkg/core":"workspace:^","@yarnpkg/fslib":"workspace:^","@yarnpkg/libzip":"workspace:^","@yarnpkg/parsers":"workspace:^","@yarnpkg/plugin-compat":"workspace:^","@yarnpkg/plugin-constraints":"workspace:^","@yarnpkg/plugin-dlx":"workspace:^","@yarnpkg/plugin-essentials":"workspace:^","@yarnpkg/plugin-exec":"workspace:^","@yarnpkg/plugin-file":"workspace:^","@yarnpkg/plugin-git":"workspace:^","@yarnpkg/plugin-github":"workspace:^","@yarnpkg/plugin-http":"workspace:^","@yarnpkg/plugin-init":"workspace:^","@yarnpkg/plugin-interactive-tools":"workspace:^","@yarnpkg/plugin-jsr":"workspace:^","@yarnpkg/plugin-link":"workspace:^","@yarnpkg/plugin-nm":"workspace:^","@yarnpkg/plugin-npm":"workspace:^","@yarnpkg/plugin-npm-cli":"workspace:^","@yarnpkg/plugin-pack":"workspace:^","@yarnpkg/plugin-patch":"workspace:^","@yarnpkg/plugin-pnp":"workspace:^","@yarnpkg/plugin-pnpm":"workspace:^","@yarnpkg/plugin-stage":"workspace:^","@yarnpkg/plugin-typescript":"workspace:^","@yarnpkg/plugin-version":"workspace:^","@yarnpkg/plugin-workspace-tools":"workspace:^","@yarnpkg/shell":"workspace:^","ci-info":"^4.0.0",clipanion:"^4.0.0-rc.2",semver:"^7.1.2",tslib:"^2.4.0",typanion:"^3.14.0"},devDependencies:{"@types/semver":"^7.1.0","@yarnpkg/builder":"workspace:^","@yarnpkg/monorepo":"workspace:^","@yarnpkg/pnpify":"workspace:^"},peerDependencies:{"@yarnpkg/core":"workspace:^"},scripts:{postpack:"rm -rf lib",prepack:'run build:compile "$(pwd)"',"build:cli+hook":"run build:pnp:hook && builder build bundle","build:cli":"builder build bundle","run:cli":"builder run","update-local":"run build:cli --no-git-hash && rsync -a --delete bundles/ bin/"},publishConfig:{main:"./lib/index.js",bin:null,exports:{".":"./lib/index.js","./package.json":"./package.json"}},files:["/lib/**/*","!/lib/pluginConfiguration.*","!/lib/cli.*"],"@yarnpkg/builder":{bundles:{standard:["@yarnpkg/plugin-essentials","@yarnpkg/plugin-compat","@yarnpkg/plugin-constraints","@yarnpkg/plugin-dlx","@yarnpkg/plugin-exec","@yarnpkg/plugin-file","@yarnpkg/plugin-git","@yarnpkg/plugin-github","@yarnpkg/plugin-http","@yarnpkg/plugin-init","@yarnpkg/plugin-interactive-tools","@yarnpkg/plugin-jsr","@yarnpkg/plugin-link","@yarnpkg/plugin-nm","@yarnpkg/plugin-npm","@yarnpkg/plugin-npm-cli","@yarnpkg/plugin-pack","@yarnpkg/plugin-patch","@yarnpkg/plugin-pnp","@yarnpkg/plugin-pnpm","@yarnpkg/plugin-stage","@yarnpkg/plugin-typescript","@yarnpkg/plugin-version","@yarnpkg/plugin-workspace-tools"]}},repository:{type:"git",url:"git+https://github.com/yarnpkg/berry.git",directory:"packages/yarnpkg-cli"},engines:{node:">=18.12.0"}}});var O5=L((Flr,oBe)=>{"use strict";oBe.exports=function(e,r){r===!0&&(r=0);var s="";if(typeof e=="string")try{s=new URL(e).protocol}catch{}else e&&e.constructor===URL&&(s=e.protocol);var a=s.split(/\:|\+/).filter(Boolean);return typeof r=="number"?a[r]:a}});var lBe=L((Nlr,aBe)=>{"use strict";var Gvt=O5();function Wvt(t){var e={protocols:[],protocol:null,port:null,resource:"",host:"",user:"",password:"",pathname:"",hash:"",search:"",href:t,query:{},parse_failed:!1};try{var r=new URL(t);e.protocols=Gvt(r),e.protocol=e.protocols[0],e.port=r.port,e.resource=r.hostname,e.host=r.host,e.user=r.username||"",e.password=r.password||"",e.pathname=r.pathname,e.hash=r.hash.slice(1),e.search=r.search.slice(1),e.href=r.href,e.query=Object.fromEntries(r.searchParams)}catch{e.protocols=["file"],e.protocol=e.protocols[0],e.port="",e.resource="",e.user="",e.pathname="",e.hash="",e.search="",e.href=t,e.query={},e.parse_failed=!0}return e}aBe.exports=Wvt});var fBe=L((Olr,uBe)=>{"use strict";var Yvt=lBe();function Vvt(t){return t&&typeof t=="object"&&"default"in t?t:{default:t}}var Kvt=Vvt(Yvt),Jvt="text/plain",zvt="us-ascii",cBe=(t,e)=>e.some(r=>r instanceof RegExp?r.test(t):r===t),Zvt=(t,{stripHash:e})=>{let r=/^data:(?[^,]*?),(?[^#]*?)(?:#(?.*))?$/.exec(t);if(!r)throw new Error(`Invalid URL: ${t}`);let{type:s,data:a,hash:n}=r.groups,c=s.split(";");n=e?"":n;let f=!1;c[c.length-1]==="base64"&&(c.pop(),f=!0);let p=(c.shift()||"").toLowerCase(),E=[...c.map(C=>{let[S,P=""]=C.split("=").map(I=>I.trim());return S==="charset"&&(P=P.toLowerCase(),P===zvt)?"":`${S}${P?`=${P}`:""}`}).filter(Boolean)];return f&&E.push("base64"),(E.length>0||p&&p!==Jvt)&&E.unshift(p),`data:${E.join(";")},${f?a.trim():a}${n?`#${n}`:""}`};function Xvt(t,e){if(e={defaultProtocol:"http:",normalizeProtocol:!0,forceHttp:!1,forceHttps:!1,stripAuthentication:!0,stripHash:!1,stripTextFragment:!0,stripWWW:!0,removeQueryParameters:[/^utm_\w+/i],removeTrailingSlash:!0,removeSingleSlash:!0,removeDirectoryIndex:!1,sortQueryParameters:!0,...e},t=t.trim(),/^data:/i.test(t))return Zvt(t,e);if(/^view-source:/i.test(t))throw new Error("`view-source:` is not supported as it is a non-standard protocol");let r=t.startsWith("//");!r&&/^\.*\//.test(t)||(t=t.replace(/^(?!(?:\w+:)?\/\/)|^\/\//,e.defaultProtocol));let a=new URL(t);if(e.forceHttp&&e.forceHttps)throw new Error("The `forceHttp` and `forceHttps` options cannot be used together");if(e.forceHttp&&a.protocol==="https:"&&(a.protocol="http:"),e.forceHttps&&a.protocol==="http:"&&(a.protocol="https:"),e.stripAuthentication&&(a.username="",a.password=""),e.stripHash?a.hash="":e.stripTextFragment&&(a.hash=a.hash.replace(/#?:~:text.*?$/i,"")),a.pathname){let c=/\b[a-z][a-z\d+\-.]{1,50}:\/\//g,f=0,p="";for(;;){let E=c.exec(a.pathname);if(!E)break;let C=E[0],S=E.index,P=a.pathname.slice(f,S);p+=P.replace(/\/{2,}/g,"/"),p+=C,f=S+C.length}let h=a.pathname.slice(f,a.pathname.length);p+=h.replace(/\/{2,}/g,"/"),a.pathname=p}if(a.pathname)try{a.pathname=decodeURI(a.pathname)}catch{}if(e.removeDirectoryIndex===!0&&(e.removeDirectoryIndex=[/^index\.[a-z]+$/]),Array.isArray(e.removeDirectoryIndex)&&e.removeDirectoryIndex.length>0){let c=a.pathname.split("/"),f=c[c.length-1];cBe(f,e.removeDirectoryIndex)&&(c=c.slice(0,-1),a.pathname=c.slice(1).join("/")+"/")}if(a.hostname&&(a.hostname=a.hostname.replace(/\.$/,""),e.stripWWW&&/^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(a.hostname)&&(a.hostname=a.hostname.replace(/^www\./,""))),Array.isArray(e.removeQueryParameters))for(let c of[...a.searchParams.keys()])cBe(c,e.removeQueryParameters)&&a.searchParams.delete(c);if(e.removeQueryParameters===!0&&(a.search=""),e.sortQueryParameters){a.searchParams.sort();try{a.search=decodeURIComponent(a.search)}catch{}}e.removeTrailingSlash&&(a.pathname=a.pathname.replace(/\/$/,""));let n=t;return t=a.toString(),!e.removeSingleSlash&&a.pathname==="/"&&!n.endsWith("/")&&a.hash===""&&(t=t.replace(/\/$/,"")),(e.removeTrailingSlash||a.pathname==="/")&&a.hash===""&&e.removeSingleSlash&&(t=t.replace(/\/$/,"")),r&&!e.normalizeProtocol&&(t=t.replace(/^http:\/\//,"//")),e.stripProtocol&&(t=t.replace(/^(?:https?:)?\/\//,"")),t}var L5=(t,e=!1)=>{let r=/^(?:([a-z_][a-z0-9_-]{0,31})@|https?:\/\/)([\w\.\-@]+)[\/:]([\~,\.\w,\-,\_,\/]+?(?:\.git|\/)?)$/,s=n=>{let c=new Error(n);throw c.subject_url=t,c};(typeof t!="string"||!t.trim())&&s("Invalid url."),t.length>L5.MAX_INPUT_LENGTH&&s("Input exceeds maximum length. If needed, change the value of parseUrl.MAX_INPUT_LENGTH."),e&&(typeof e!="object"&&(e={stripHash:!1}),t=Xvt(t,e));let a=Kvt.default(t);if(a.parse_failed){let n=a.href.match(r);n?(a.protocols=["ssh"],a.protocol="ssh",a.resource=n[2],a.host=n[2],a.user=n[1],a.pathname=`/${n[3]}`,a.parse_failed=!1):s("URL parsing failed.")}return a};L5.MAX_INPUT_LENGTH=2048;uBe.exports=L5});var hBe=L((Llr,pBe)=>{"use strict";var $vt=O5();function ABe(t){if(Array.isArray(t))return t.indexOf("ssh")!==-1||t.indexOf("rsync")!==-1;if(typeof t!="string")return!1;var e=$vt(t);if(t=t.substring(t.indexOf("://")+3),ABe(e))return!0;var r=new RegExp(".([a-zA-Z\\d]+):(\\d+)/");return!t.match(r)&&t.indexOf("@"){"use strict";var eSt=fBe(),gBe=hBe();function tSt(t){var e=eSt(t);return e.token="",e.password==="x-oauth-basic"?e.token=e.user:e.user==="x-token-auth"&&(e.token=e.password),gBe(e.protocols)||e.protocols.length===0&&gBe(t)?e.protocol="ssh":e.protocols.length?e.protocol=e.protocols[0]:(e.protocol="file",e.protocols=["file"]),e.href=e.href.replace(/\/$/,""),e}dBe.exports=tSt});var EBe=L((_lr,yBe)=>{"use strict";var rSt=mBe();function M5(t){if(typeof t!="string")throw new Error("The url must be a string.");var e=/^([a-z\d-]{1,39})\/([-\.\w]{1,100})$/i;e.test(t)&&(t="https://github.com/"+t);var r=rSt(t),s=r.resource.split("."),a=null;switch(r.toString=function(N){return M5.stringify(this,N)},r.source=s.length>2?s.slice(1-s.length).join("."):r.source=r.resource,r.git_suffix=/\.git$/.test(r.pathname),r.name=decodeURIComponent((r.pathname||r.href).replace(/(^\/)|(\/$)/g,"").replace(/\.git$/,"")),r.owner=decodeURIComponent(r.user),r.source){case"git.cloudforge.com":r.owner=r.user,r.organization=s[0],r.source="cloudforge.com";break;case"visualstudio.com":if(r.resource==="vs-ssh.visualstudio.com"){a=r.name.split("/"),a.length===4&&(r.organization=a[1],r.owner=a[2],r.name=a[3],r.full_name=a[2]+"/"+a[3]);break}else{a=r.name.split("/"),a.length===2?(r.owner=a[1],r.name=a[1],r.full_name="_git/"+r.name):a.length===3?(r.name=a[2],a[0]==="DefaultCollection"?(r.owner=a[2],r.organization=a[0],r.full_name=r.organization+"/_git/"+r.name):(r.owner=a[0],r.full_name=r.owner+"/_git/"+r.name)):a.length===4&&(r.organization=a[0],r.owner=a[1],r.name=a[3],r.full_name=r.organization+"/"+r.owner+"/_git/"+r.name);break}case"dev.azure.com":case"azure.com":if(r.resource==="ssh.dev.azure.com"){a=r.name.split("/"),a.length===4&&(r.organization=a[1],r.owner=a[2],r.name=a[3]);break}else{a=r.name.split("/"),a.length===5?(r.organization=a[0],r.owner=a[1],r.name=a[4],r.full_name="_git/"+r.name):a.length===3?(r.name=a[2],a[0]==="DefaultCollection"?(r.owner=a[2],r.organization=a[0],r.full_name=r.organization+"/_git/"+r.name):(r.owner=a[0],r.full_name=r.owner+"/_git/"+r.name)):a.length===4&&(r.organization=a[0],r.owner=a[1],r.name=a[3],r.full_name=r.organization+"/"+r.owner+"/_git/"+r.name),r.query&&r.query.path&&(r.filepath=r.query.path.replace(/^\/+/g,"")),r.query&&r.query.version&&(r.ref=r.query.version.replace(/^GB/,""));break}default:a=r.name.split("/");var n=a.length-1;if(a.length>=2){var c=a.indexOf("-",2),f=a.indexOf("blob",2),p=a.indexOf("tree",2),h=a.indexOf("commit",2),E=a.indexOf("src",2),C=a.indexOf("raw",2),S=a.indexOf("edit",2);n=c>0?c-1:f>0?f-1:p>0?p-1:h>0?h-1:E>0?E-1:C>0?C-1:S>0?S-1:n,r.owner=a.slice(0,n).join("/"),r.name=a[n],h&&(r.commit=a[n+2])}r.ref="",r.filepathtype="",r.filepath="";var P=a.length>n&&a[n+1]==="-"?n+1:n;a.length>P+2&&["raw","src","blob","tree","edit"].indexOf(a[P+1])>=0&&(r.filepathtype=a[P+1],r.ref=a[P+2],a.length>P+3&&(r.filepath=a.slice(P+3).join("/"))),r.organization=r.owner;break}r.full_name||(r.full_name=r.owner,r.name&&(r.full_name&&(r.full_name+="/"),r.full_name+=r.name)),r.owner.startsWith("scm/")&&(r.source="bitbucket-server",r.owner=r.owner.replace("scm/",""),r.organization=r.owner,r.full_name=r.owner+"/"+r.name);var I=/(projects|users)\/(.*?)\/repos\/(.*?)((\/.*$)|$)/,R=I.exec(r.pathname);return R!=null&&(r.source="bitbucket-server",R[1]==="users"?r.owner="~"+R[2]:r.owner=R[2],r.organization=r.owner,r.name=R[3],a=R[4].split("/"),a.length>1&&(["raw","browse"].indexOf(a[1])>=0?(r.filepathtype=a[1],a.length>2&&(r.filepath=a.slice(2).join("/"))):a[1]==="commits"&&a.length>2&&(r.commit=a[2])),r.full_name=r.owner+"/"+r.name,r.query.at?r.ref=r.query.at:r.ref=""),r}M5.stringify=function(t,e){e=e||(t.protocols&&t.protocols.length?t.protocols.join("+"):t.protocol);var r=t.port?":"+t.port:"",s=t.user||"git",a=t.git_suffix?".git":"";switch(e){case"ssh":return r?"ssh://"+s+"@"+t.resource+r+"/"+t.full_name+a:s+"@"+t.resource+":"+t.full_name+a;case"git+ssh":case"ssh+git":case"ftp":case"ftps":return e+"://"+s+"@"+t.resource+r+"/"+t.full_name+a;case"http":case"https":var n=t.token?nSt(t):t.user&&(t.protocols.includes("http")||t.protocols.includes("https"))?t.user+"@":"";return e+"://"+n+t.resource+r+"/"+iSt(t)+a;default:return t.href}};function nSt(t){switch(t.source){case"bitbucket.org":return"x-token-auth:"+t.token+"@";default:return t.token+"@"}}function iSt(t){switch(t.source){case"bitbucket-server":return"scm/"+t.full_name;default:return""+t.full_name}}yBe.exports=M5});var NBe=L((yur,FBe)=>{var gSt=QT(),dSt=Hk(),mSt=xc(),ySt=oI(),ESt=pG(),ISt=zI(),CSt=bv();function wSt(t){return mSt(t)?gSt(t,ISt):ySt(t)?[t]:dSt(ESt(CSt(t)))}FBe.exports=wSt});function DSt(t,e){return e===1&&SSt.has(t[0])}function hS(t){let e=Array.isArray(t)?t:(0,MBe.default)(t);return e.map((s,a)=>BSt.test(s)?`[${s}]`:vSt.test(s)&&!DSt(e,a)?`.${s}`:`[${JSON.stringify(s)}]`).join("").replace(/^\./,"")}function bSt(t,e){let r=[];if(e.methodName!==null&&r.push(he.pretty(t,e.methodName,he.Type.CODE)),e.file!==null){let s=[];s.push(he.pretty(t,e.file,he.Type.PATH)),e.line!==null&&(s.push(he.pretty(t,e.line,he.Type.NUMBER)),e.column!==null&&s.push(he.pretty(t,e.column,he.Type.NUMBER))),r.push(`(${s.join(he.pretty(t,":","grey"))})`)}return r.join(" ")}function oF(t,{manifestUpdates:e,reportedErrors:r},{fix:s}={}){let a=new Map,n=new Map,c=[...r.keys()].map(f=>[f,new Map]);for(let[f,p]of[...c,...e]){let h=r.get(f)?.map(P=>({text:P,fixable:!1}))??[],E=!1,C=t.getWorkspaceByCwd(f),S=C.manifest.exportTo({});for(let[P,I]of p){if(I.size>1){let R=[...I].map(([N,U])=>{let W=he.pretty(t.configuration,N,he.Type.INSPECT),te=U.size>0?bSt(t.configuration,U.values().next().value):null;return te!==null?` ${W} at ${te}`:` ${W}`}).join("");h.push({text:`Conflict detected in constraint targeting ${he.pretty(t.configuration,P,he.Type.CODE)}; conflicting values are:${R}`,fixable:!1})}else{let[[R]]=I,N=(0,OBe.default)(S,P);if(JSON.stringify(N)===JSON.stringify(R))continue;if(!s){let U=typeof N>"u"?`Missing field ${he.pretty(t.configuration,P,he.Type.CODE)}; expected ${he.pretty(t.configuration,R,he.Type.INSPECT)}`:typeof R>"u"?`Extraneous field ${he.pretty(t.configuration,P,he.Type.CODE)} currently set to ${he.pretty(t.configuration,N,he.Type.INSPECT)}`:`Invalid field ${he.pretty(t.configuration,P,he.Type.CODE)}; expected ${he.pretty(t.configuration,R,he.Type.INSPECT)}, found ${he.pretty(t.configuration,N,he.Type.INSPECT)}`;h.push({text:U,fixable:!0});continue}typeof R>"u"?(0,_Be.default)(S,P):(0,LBe.default)(S,P,R),E=!0}E&&a.set(C,S)}h.length>0&&n.set(C,h)}return{changedWorkspaces:a,remainingErrors:n}}function UBe(t,{configuration:e}){let r={children:[]};for(let[s,a]of t){let n=[];for(let f of a){let p=f.text.split(/\n/);f.fixable&&(p[0]=`${he.pretty(e,"\u2699","gray")} ${p[0]}`),n.push({value:he.tuple(he.Type.NO_HINT,p[0]),children:p.slice(1).map(h=>({value:he.tuple(he.Type.NO_HINT,h)}))})}let c={value:he.tuple(he.Type.LOCATOR,s.anchoredLocator),children:je.sortMap(n,f=>f.value[1])};r.children.push(c)}return r.children=je.sortMap(r.children,s=>s.value[1]),r}var OBe,LBe,MBe,_Be,WC,BSt,vSt,SSt,gS=Ct(()=>{Ve();OBe=et(aS()),LBe=et(v5()),MBe=et(NBe()),_Be=et(b5()),WC=class{constructor(e){this.indexedFields=e;this.items=[];this.indexes={};this.clear()}clear(){this.items=[];for(let e of this.indexedFields)this.indexes[e]=new Map}insert(e){this.items.push(e);for(let r of this.indexedFields){let s=Object.hasOwn(e,r)?e[r]:void 0;if(typeof s>"u")continue;je.getArrayWithDefault(this.indexes[r],s).push(e)}return e}find(e){if(typeof e>"u")return this.items;let r=Object.entries(e);if(r.length===0)return this.items;let s=[],a;for(let[c,f]of r){let p=c,h=Object.hasOwn(this.indexes,p)?this.indexes[p]:void 0;if(typeof h>"u"){s.push([p,f]);continue}let E=new Set(h.get(f)??[]);if(E.size===0)return[];if(typeof a>"u")a=E;else for(let C of a)E.has(C)||a.delete(C);if(a.size===0)break}let n=[...a??[]];return s.length>0&&(n=n.filter(c=>{for(let[f,p]of s)if(!(typeof p<"u"?Object.hasOwn(c,f)&&c[f]===p:Object.hasOwn(c,f)===!1))return!1;return!0})),n}},BSt=/^[0-9]+$/,vSt=/^[a-zA-Z0-9_]+$/,SSt=new Set(["scripts",...Ht.allDependencies])});var HBe=L((kur,X5)=>{var PSt;(function(t){var e=function(){return{"append/2":[new t.type.Rule(new t.type.Term("append",[new t.type.Var("X"),new t.type.Var("L")]),new t.type.Term("foldl",[new t.type.Term("append",[]),new t.type.Var("X"),new t.type.Term("[]",[]),new t.type.Var("L")]))],"append/3":[new t.type.Rule(new t.type.Term("append",[new t.type.Term("[]",[]),new t.type.Var("X"),new t.type.Var("X")]),null),new t.type.Rule(new t.type.Term("append",[new t.type.Term(".",[new t.type.Var("H"),new t.type.Var("T")]),new t.type.Var("X"),new t.type.Term(".",[new t.type.Var("H"),new t.type.Var("S")])]),new t.type.Term("append",[new t.type.Var("T"),new t.type.Var("X"),new t.type.Var("S")]))],"member/2":[new t.type.Rule(new t.type.Term("member",[new t.type.Var("X"),new t.type.Term(".",[new t.type.Var("X"),new t.type.Var("_")])]),null),new t.type.Rule(new t.type.Term("member",[new t.type.Var("X"),new t.type.Term(".",[new t.type.Var("_"),new t.type.Var("Xs")])]),new t.type.Term("member",[new t.type.Var("X"),new t.type.Var("Xs")]))],"permutation/2":[new t.type.Rule(new t.type.Term("permutation",[new t.type.Term("[]",[]),new t.type.Term("[]",[])]),null),new t.type.Rule(new t.type.Term("permutation",[new t.type.Term(".",[new t.type.Var("H"),new t.type.Var("T")]),new t.type.Var("S")]),new t.type.Term(",",[new t.type.Term("permutation",[new t.type.Var("T"),new t.type.Var("P")]),new t.type.Term(",",[new t.type.Term("append",[new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("P")]),new t.type.Term("append",[new t.type.Var("X"),new t.type.Term(".",[new t.type.Var("H"),new t.type.Var("Y")]),new t.type.Var("S")])])]))],"maplist/2":[new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("_"),new t.type.Term("[]",[])]),null),new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Term(".",[new t.type.Var("X"),new t.type.Var("Xs")])]),new t.type.Term(",",[new t.type.Term("call",[new t.type.Var("P"),new t.type.Var("X")]),new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Var("Xs")])]))],"maplist/3":[new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("_"),new t.type.Term("[]",[]),new t.type.Term("[]",[])]),null),new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Term(".",[new t.type.Var("A"),new t.type.Var("As")]),new t.type.Term(".",[new t.type.Var("B"),new t.type.Var("Bs")])]),new t.type.Term(",",[new t.type.Term("call",[new t.type.Var("P"),new t.type.Var("A"),new t.type.Var("B")]),new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Var("As"),new t.type.Var("Bs")])]))],"maplist/4":[new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("_"),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[])]),null),new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Term(".",[new t.type.Var("A"),new t.type.Var("As")]),new t.type.Term(".",[new t.type.Var("B"),new t.type.Var("Bs")]),new t.type.Term(".",[new t.type.Var("C"),new t.type.Var("Cs")])]),new t.type.Term(",",[new t.type.Term("call",[new t.type.Var("P"),new t.type.Var("A"),new t.type.Var("B"),new t.type.Var("C")]),new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Var("As"),new t.type.Var("Bs"),new t.type.Var("Cs")])]))],"maplist/5":[new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("_"),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[])]),null),new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Term(".",[new t.type.Var("A"),new t.type.Var("As")]),new t.type.Term(".",[new t.type.Var("B"),new t.type.Var("Bs")]),new t.type.Term(".",[new t.type.Var("C"),new t.type.Var("Cs")]),new t.type.Term(".",[new t.type.Var("D"),new t.type.Var("Ds")])]),new t.type.Term(",",[new t.type.Term("call",[new t.type.Var("P"),new t.type.Var("A"),new t.type.Var("B"),new t.type.Var("C"),new t.type.Var("D")]),new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Var("As"),new t.type.Var("Bs"),new t.type.Var("Cs"),new t.type.Var("Ds")])]))],"maplist/6":[new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("_"),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[])]),null),new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Term(".",[new t.type.Var("A"),new t.type.Var("As")]),new t.type.Term(".",[new t.type.Var("B"),new t.type.Var("Bs")]),new t.type.Term(".",[new t.type.Var("C"),new t.type.Var("Cs")]),new t.type.Term(".",[new t.type.Var("D"),new t.type.Var("Ds")]),new t.type.Term(".",[new t.type.Var("E"),new t.type.Var("Es")])]),new t.type.Term(",",[new t.type.Term("call",[new t.type.Var("P"),new t.type.Var("A"),new t.type.Var("B"),new t.type.Var("C"),new t.type.Var("D"),new t.type.Var("E")]),new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Var("As"),new t.type.Var("Bs"),new t.type.Var("Cs"),new t.type.Var("Ds"),new t.type.Var("Es")])]))],"maplist/7":[new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("_"),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[])]),null),new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Term(".",[new t.type.Var("A"),new t.type.Var("As")]),new t.type.Term(".",[new t.type.Var("B"),new t.type.Var("Bs")]),new t.type.Term(".",[new t.type.Var("C"),new t.type.Var("Cs")]),new t.type.Term(".",[new t.type.Var("D"),new t.type.Var("Ds")]),new t.type.Term(".",[new t.type.Var("E"),new t.type.Var("Es")]),new t.type.Term(".",[new t.type.Var("F"),new t.type.Var("Fs")])]),new t.type.Term(",",[new t.type.Term("call",[new t.type.Var("P"),new t.type.Var("A"),new t.type.Var("B"),new t.type.Var("C"),new t.type.Var("D"),new t.type.Var("E"),new t.type.Var("F")]),new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Var("As"),new t.type.Var("Bs"),new t.type.Var("Cs"),new t.type.Var("Ds"),new t.type.Var("Es"),new t.type.Var("Fs")])]))],"maplist/8":[new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("_"),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[]),new t.type.Term("[]",[])]),null),new t.type.Rule(new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Term(".",[new t.type.Var("A"),new t.type.Var("As")]),new t.type.Term(".",[new t.type.Var("B"),new t.type.Var("Bs")]),new t.type.Term(".",[new t.type.Var("C"),new t.type.Var("Cs")]),new t.type.Term(".",[new t.type.Var("D"),new t.type.Var("Ds")]),new t.type.Term(".",[new t.type.Var("E"),new t.type.Var("Es")]),new t.type.Term(".",[new t.type.Var("F"),new t.type.Var("Fs")]),new t.type.Term(".",[new t.type.Var("G"),new t.type.Var("Gs")])]),new t.type.Term(",",[new t.type.Term("call",[new t.type.Var("P"),new t.type.Var("A"),new t.type.Var("B"),new t.type.Var("C"),new t.type.Var("D"),new t.type.Var("E"),new t.type.Var("F"),new t.type.Var("G")]),new t.type.Term("maplist",[new t.type.Var("P"),new t.type.Var("As"),new t.type.Var("Bs"),new t.type.Var("Cs"),new t.type.Var("Ds"),new t.type.Var("Es"),new t.type.Var("Fs"),new t.type.Var("Gs")])]))],"include/3":[new t.type.Rule(new t.type.Term("include",[new t.type.Var("_"),new t.type.Term("[]",[]),new t.type.Term("[]",[])]),null),new t.type.Rule(new t.type.Term("include",[new t.type.Var("P"),new t.type.Term(".",[new t.type.Var("H"),new t.type.Var("T")]),new t.type.Var("L")]),new t.type.Term(",",[new t.type.Term("=..",[new t.type.Var("P"),new t.type.Var("A")]),new t.type.Term(",",[new t.type.Term("append",[new t.type.Var("A"),new t.type.Term(".",[new t.type.Var("H"),new t.type.Term("[]",[])]),new t.type.Var("B")]),new t.type.Term(",",[new t.type.Term("=..",[new t.type.Var("F"),new t.type.Var("B")]),new t.type.Term(",",[new t.type.Term(";",[new t.type.Term(",",[new t.type.Term("call",[new t.type.Var("F")]),new t.type.Term(",",[new t.type.Term("=",[new t.type.Var("L"),new t.type.Term(".",[new t.type.Var("H"),new t.type.Var("S")])]),new t.type.Term("!",[])])]),new t.type.Term("=",[new t.type.Var("L"),new t.type.Var("S")])]),new t.type.Term("include",[new t.type.Var("P"),new t.type.Var("T"),new t.type.Var("S")])])])])]))],"exclude/3":[new t.type.Rule(new t.type.Term("exclude",[new t.type.Var("_"),new t.type.Term("[]",[]),new t.type.Term("[]",[])]),null),new t.type.Rule(new t.type.Term("exclude",[new t.type.Var("P"),new t.type.Term(".",[new t.type.Var("H"),new t.type.Var("T")]),new t.type.Var("S")]),new t.type.Term(",",[new t.type.Term("exclude",[new t.type.Var("P"),new t.type.Var("T"),new t.type.Var("E")]),new t.type.Term(",",[new t.type.Term("=..",[new t.type.Var("P"),new t.type.Var("L")]),new t.type.Term(",",[new t.type.Term("append",[new t.type.Var("L"),new t.type.Term(".",[new t.type.Var("H"),new t.type.Term("[]",[])]),new t.type.Var("Q")]),new t.type.Term(",",[new t.type.Term("=..",[new t.type.Var("R"),new t.type.Var("Q")]),new t.type.Term(";",[new t.type.Term(",",[new t.type.Term("call",[new t.type.Var("R")]),new t.type.Term(",",[new t.type.Term("!",[]),new t.type.Term("=",[new t.type.Var("S"),new t.type.Var("E")])])]),new t.type.Term("=",[new t.type.Var("S"),new t.type.Term(".",[new t.type.Var("H"),new t.type.Var("E")])])])])])])]))],"foldl/4":[new t.type.Rule(new t.type.Term("foldl",[new t.type.Var("_"),new t.type.Term("[]",[]),new t.type.Var("I"),new t.type.Var("I")]),null),new t.type.Rule(new t.type.Term("foldl",[new t.type.Var("P"),new t.type.Term(".",[new t.type.Var("H"),new t.type.Var("T")]),new t.type.Var("I"),new t.type.Var("R")]),new t.type.Term(",",[new t.type.Term("=..",[new t.type.Var("P"),new t.type.Var("L")]),new t.type.Term(",",[new t.type.Term("append",[new t.type.Var("L"),new t.type.Term(".",[new t.type.Var("I"),new t.type.Term(".",[new t.type.Var("H"),new t.type.Term(".",[new t.type.Var("X"),new t.type.Term("[]",[])])])]),new t.type.Var("L2")]),new t.type.Term(",",[new t.type.Term("=..",[new t.type.Var("P2"),new t.type.Var("L2")]),new t.type.Term(",",[new t.type.Term("call",[new t.type.Var("P2")]),new t.type.Term("foldl",[new t.type.Var("P"),new t.type.Var("T"),new t.type.Var("X"),new t.type.Var("R")])])])])]))],"select/3":[new t.type.Rule(new t.type.Term("select",[new t.type.Var("E"),new t.type.Term(".",[new t.type.Var("E"),new t.type.Var("Xs")]),new t.type.Var("Xs")]),null),new t.type.Rule(new t.type.Term("select",[new t.type.Var("E"),new t.type.Term(".",[new t.type.Var("X"),new t.type.Var("Xs")]),new t.type.Term(".",[new t.type.Var("X"),new t.type.Var("Ys")])]),new t.type.Term("select",[new t.type.Var("E"),new t.type.Var("Xs"),new t.type.Var("Ys")]))],"sum_list/2":[new t.type.Rule(new t.type.Term("sum_list",[new t.type.Term("[]",[]),new t.type.Num(0,!1)]),null),new t.type.Rule(new t.type.Term("sum_list",[new t.type.Term(".",[new t.type.Var("X"),new t.type.Var("Xs")]),new t.type.Var("S")]),new t.type.Term(",",[new t.type.Term("sum_list",[new t.type.Var("Xs"),new t.type.Var("Y")]),new t.type.Term("is",[new t.type.Var("S"),new t.type.Term("+",[new t.type.Var("X"),new t.type.Var("Y")])])]))],"max_list/2":[new t.type.Rule(new t.type.Term("max_list",[new t.type.Term(".",[new t.type.Var("X"),new t.type.Term("[]",[])]),new t.type.Var("X")]),null),new t.type.Rule(new t.type.Term("max_list",[new t.type.Term(".",[new t.type.Var("X"),new t.type.Var("Xs")]),new t.type.Var("S")]),new t.type.Term(",",[new t.type.Term("max_list",[new t.type.Var("Xs"),new t.type.Var("Y")]),new t.type.Term(";",[new t.type.Term(",",[new t.type.Term(">=",[new t.type.Var("X"),new t.type.Var("Y")]),new t.type.Term(",",[new t.type.Term("=",[new t.type.Var("S"),new t.type.Var("X")]),new t.type.Term("!",[])])]),new t.type.Term("=",[new t.type.Var("S"),new t.type.Var("Y")])])]))],"min_list/2":[new t.type.Rule(new t.type.Term("min_list",[new t.type.Term(".",[new t.type.Var("X"),new t.type.Term("[]",[])]),new t.type.Var("X")]),null),new t.type.Rule(new t.type.Term("min_list",[new t.type.Term(".",[new t.type.Var("X"),new t.type.Var("Xs")]),new t.type.Var("S")]),new t.type.Term(",",[new t.type.Term("min_list",[new t.type.Var("Xs"),new t.type.Var("Y")]),new t.type.Term(";",[new t.type.Term(",",[new t.type.Term("=<",[new t.type.Var("X"),new t.type.Var("Y")]),new t.type.Term(",",[new t.type.Term("=",[new t.type.Var("S"),new t.type.Var("X")]),new t.type.Term("!",[])])]),new t.type.Term("=",[new t.type.Var("S"),new t.type.Var("Y")])])]))],"prod_list/2":[new t.type.Rule(new t.type.Term("prod_list",[new t.type.Term("[]",[]),new t.type.Num(1,!1)]),null),new t.type.Rule(new t.type.Term("prod_list",[new t.type.Term(".",[new t.type.Var("X"),new t.type.Var("Xs")]),new t.type.Var("S")]),new t.type.Term(",",[new t.type.Term("prod_list",[new t.type.Var("Xs"),new t.type.Var("Y")]),new t.type.Term("is",[new t.type.Var("S"),new t.type.Term("*",[new t.type.Var("X"),new t.type.Var("Y")])])]))],"last/2":[new t.type.Rule(new t.type.Term("last",[new t.type.Term(".",[new t.type.Var("X"),new t.type.Term("[]",[])]),new t.type.Var("X")]),null),new t.type.Rule(new t.type.Term("last",[new t.type.Term(".",[new t.type.Var("_"),new t.type.Var("Xs")]),new t.type.Var("X")]),new t.type.Term("last",[new t.type.Var("Xs"),new t.type.Var("X")]))],"prefix/2":[new t.type.Rule(new t.type.Term("prefix",[new t.type.Var("Part"),new t.type.Var("Whole")]),new t.type.Term("append",[new t.type.Var("Part"),new t.type.Var("_"),new t.type.Var("Whole")]))],"nth0/3":[new t.type.Rule(new t.type.Term("nth0",[new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z")]),new t.type.Term(";",[new t.type.Term("->",[new t.type.Term("var",[new t.type.Var("X")]),new t.type.Term("nth",[new t.type.Num(0,!1),new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z"),new t.type.Var("_")])]),new t.type.Term(",",[new t.type.Term(">=",[new t.type.Var("X"),new t.type.Num(0,!1)]),new t.type.Term(",",[new t.type.Term("nth",[new t.type.Num(0,!1),new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z"),new t.type.Var("_")]),new t.type.Term("!",[])])])]))],"nth1/3":[new t.type.Rule(new t.type.Term("nth1",[new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z")]),new t.type.Term(";",[new t.type.Term("->",[new t.type.Term("var",[new t.type.Var("X")]),new t.type.Term("nth",[new t.type.Num(1,!1),new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z"),new t.type.Var("_")])]),new t.type.Term(",",[new t.type.Term(">",[new t.type.Var("X"),new t.type.Num(0,!1)]),new t.type.Term(",",[new t.type.Term("nth",[new t.type.Num(1,!1),new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z"),new t.type.Var("_")]),new t.type.Term("!",[])])])]))],"nth0/4":[new t.type.Rule(new t.type.Term("nth0",[new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z"),new t.type.Var("W")]),new t.type.Term(";",[new t.type.Term("->",[new t.type.Term("var",[new t.type.Var("X")]),new t.type.Term("nth",[new t.type.Num(0,!1),new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z"),new t.type.Var("W")])]),new t.type.Term(",",[new t.type.Term(">=",[new t.type.Var("X"),new t.type.Num(0,!1)]),new t.type.Term(",",[new t.type.Term("nth",[new t.type.Num(0,!1),new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z"),new t.type.Var("W")]),new t.type.Term("!",[])])])]))],"nth1/4":[new t.type.Rule(new t.type.Term("nth1",[new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z"),new t.type.Var("W")]),new t.type.Term(";",[new t.type.Term("->",[new t.type.Term("var",[new t.type.Var("X")]),new t.type.Term("nth",[new t.type.Num(1,!1),new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z"),new t.type.Var("W")])]),new t.type.Term(",",[new t.type.Term(">",[new t.type.Var("X"),new t.type.Num(0,!1)]),new t.type.Term(",",[new t.type.Term("nth",[new t.type.Num(1,!1),new t.type.Var("X"),new t.type.Var("Y"),new t.type.Var("Z"),new t.type.Var("W")]),new t.type.Term("!",[])])])]))],"nth/5":[new t.type.Rule(new t.type.Term("nth",[new t.type.Var("N"),new t.type.Var("N"),new t.type.Term(".",[new t.type.Var("X"),new t.type.Var("Xs")]),new t.type.Var("X"),new t.type.Var("Xs")]),null),new t.type.Rule(new t.type.Term("nth",[new t.type.Var("N"),new t.type.Var("O"),new t.type.Term(".",[new t.type.Var("X"),new t.type.Var("Xs")]),new t.type.Var("Y"),new t.type.Term(".",[new t.type.Var("X"),new t.type.Var("Ys")])]),new t.type.Term(",",[new t.type.Term("is",[new t.type.Var("M"),new t.type.Term("+",[new t.type.Var("N"),new t.type.Num(1,!1)])]),new t.type.Term("nth",[new t.type.Var("M"),new t.type.Var("O"),new t.type.Var("Xs"),new t.type.Var("Y"),new t.type.Var("Ys")])]))],"length/2":function(s,a,n){var c=n.args[0],f=n.args[1];if(!t.type.is_variable(f)&&!t.type.is_integer(f))s.throw_error(t.error.type("integer",f,n.indicator));else if(t.type.is_integer(f)&&f.value<0)s.throw_error(t.error.domain("not_less_than_zero",f,n.indicator));else{var p=new t.type.Term("length",[c,new t.type.Num(0,!1),f]);t.type.is_integer(f)&&(p=new t.type.Term(",",[p,new t.type.Term("!",[])])),s.prepend([new t.type.State(a.goal.replace(p),a.substitution,a)])}},"length/3":[new t.type.Rule(new t.type.Term("length",[new t.type.Term("[]",[]),new t.type.Var("N"),new t.type.Var("N")]),null),new t.type.Rule(new t.type.Term("length",[new t.type.Term(".",[new t.type.Var("_"),new t.type.Var("X")]),new t.type.Var("A"),new t.type.Var("N")]),new t.type.Term(",",[new t.type.Term("succ",[new t.type.Var("A"),new t.type.Var("B")]),new t.type.Term("length",[new t.type.Var("X"),new t.type.Var("B"),new t.type.Var("N")])]))],"replicate/3":function(s,a,n){var c=n.args[0],f=n.args[1],p=n.args[2];if(t.type.is_variable(f))s.throw_error(t.error.instantiation(n.indicator));else if(!t.type.is_integer(f))s.throw_error(t.error.type("integer",f,n.indicator));else if(f.value<0)s.throw_error(t.error.domain("not_less_than_zero",f,n.indicator));else if(!t.type.is_variable(p)&&!t.type.is_list(p))s.throw_error(t.error.type("list",p,n.indicator));else{for(var h=new t.type.Term("[]"),E=0;E0;C--)E[C].equals(E[C-1])&&E.splice(C,1);for(var S=new t.type.Term("[]"),C=E.length-1;C>=0;C--)S=new t.type.Term(".",[E[C],S]);s.prepend([new t.type.State(a.goal.replace(new t.type.Term("=",[S,f])),a.substitution,a)])}}},"msort/2":function(s,a,n){var c=n.args[0],f=n.args[1];if(t.type.is_variable(c))s.throw_error(t.error.instantiation(n.indicator));else if(!t.type.is_variable(f)&&!t.type.is_fully_list(f))s.throw_error(t.error.type("list",f,n.indicator));else{for(var p=[],h=c;h.indicator==="./2";)p.push(h.args[0]),h=h.args[1];if(t.type.is_variable(h))s.throw_error(t.error.instantiation(n.indicator));else if(!t.type.is_empty_list(h))s.throw_error(t.error.type("list",c,n.indicator));else{for(var E=p.sort(t.compare),C=new t.type.Term("[]"),S=E.length-1;S>=0;S--)C=new t.type.Term(".",[E[S],C]);s.prepend([new t.type.State(a.goal.replace(new t.type.Term("=",[C,f])),a.substitution,a)])}}},"keysort/2":function(s,a,n){var c=n.args[0],f=n.args[1];if(t.type.is_variable(c))s.throw_error(t.error.instantiation(n.indicator));else if(!t.type.is_variable(f)&&!t.type.is_fully_list(f))s.throw_error(t.error.type("list",f,n.indicator));else{for(var p=[],h,E=c;E.indicator==="./2";){if(h=E.args[0],t.type.is_variable(h)){s.throw_error(t.error.instantiation(n.indicator));return}else if(!t.type.is_term(h)||h.indicator!=="-/2"){s.throw_error(t.error.type("pair",h,n.indicator));return}h.args[0].pair=h.args[1],p.push(h.args[0]),E=E.args[1]}if(t.type.is_variable(E))s.throw_error(t.error.instantiation(n.indicator));else if(!t.type.is_empty_list(E))s.throw_error(t.error.type("list",c,n.indicator));else{for(var C=p.sort(t.compare),S=new t.type.Term("[]"),P=C.length-1;P>=0;P--)S=new t.type.Term(".",[new t.type.Term("-",[C[P],C[P].pair]),S]),delete C[P].pair;s.prepend([new t.type.State(a.goal.replace(new t.type.Term("=",[S,f])),a.substitution,a)])}}},"take/3":function(s,a,n){var c=n.args[0],f=n.args[1],p=n.args[2];if(t.type.is_variable(f)||t.type.is_variable(c))s.throw_error(t.error.instantiation(n.indicator));else if(!t.type.is_list(f))s.throw_error(t.error.type("list",f,n.indicator));else if(!t.type.is_integer(c))s.throw_error(t.error.type("integer",c,n.indicator));else if(!t.type.is_variable(p)&&!t.type.is_list(p))s.throw_error(t.error.type("list",p,n.indicator));else{for(var h=c.value,E=[],C=f;h>0&&C.indicator==="./2";)E.push(C.args[0]),C=C.args[1],h--;if(h===0){for(var S=new t.type.Term("[]"),h=E.length-1;h>=0;h--)S=new t.type.Term(".",[E[h],S]);s.prepend([new t.type.State(a.goal.replace(new t.type.Term("=",[S,p])),a.substitution,a)])}}},"drop/3":function(s,a,n){var c=n.args[0],f=n.args[1],p=n.args[2];if(t.type.is_variable(f)||t.type.is_variable(c))s.throw_error(t.error.instantiation(n.indicator));else if(!t.type.is_list(f))s.throw_error(t.error.type("list",f,n.indicator));else if(!t.type.is_integer(c))s.throw_error(t.error.type("integer",c,n.indicator));else if(!t.type.is_variable(p)&&!t.type.is_list(p))s.throw_error(t.error.type("list",p,n.indicator));else{for(var h=c.value,E=[],C=f;h>0&&C.indicator==="./2";)E.push(C.args[0]),C=C.args[1],h--;h===0&&s.prepend([new t.type.State(a.goal.replace(new t.type.Term("=",[C,p])),a.substitution,a)])}},"reverse/2":function(s,a,n){var c=n.args[0],f=n.args[1],p=t.type.is_instantiated_list(c),h=t.type.is_instantiated_list(f);if(t.type.is_variable(c)&&t.type.is_variable(f))s.throw_error(t.error.instantiation(n.indicator));else if(!t.type.is_variable(c)&&!t.type.is_fully_list(c))s.throw_error(t.error.type("list",c,n.indicator));else if(!t.type.is_variable(f)&&!t.type.is_fully_list(f))s.throw_error(t.error.type("list",f,n.indicator));else if(!p&&!h)s.throw_error(t.error.instantiation(n.indicator));else{for(var E=p?c:f,C=new t.type.Term("[]",[]);E.indicator==="./2";)C=new t.type.Term(".",[E.args[0],C]),E=E.args[1];s.prepend([new t.type.State(a.goal.replace(new t.type.Term("=",[C,p?f:c])),a.substitution,a)])}},"list_to_set/2":function(s,a,n){var c=n.args[0],f=n.args[1];if(t.type.is_variable(c))s.throw_error(t.error.instantiation(n.indicator));else{for(var p=c,h=[];p.indicator==="./2";)h.push(p.args[0]),p=p.args[1];if(t.type.is_variable(p))s.throw_error(t.error.instantiation(n.indicator));else if(!t.type.is_term(p)||p.indicator!=="[]/0")s.throw_error(t.error.type("list",c,n.indicator));else{for(var E=[],C=new t.type.Term("[]",[]),S,P=0;P=0;P--)C=new t.type.Term(".",[E[P],C]);s.prepend([new t.type.State(a.goal.replace(new t.type.Term("=",[f,C])),a.substitution,a)])}}}}},r=["append/2","append/3","member/2","permutation/2","maplist/2","maplist/3","maplist/4","maplist/5","maplist/6","maplist/7","maplist/8","include/3","exclude/3","foldl/4","sum_list/2","max_list/2","min_list/2","prod_list/2","last/2","prefix/2","nth0/3","nth1/3","nth0/4","nth1/4","length/2","replicate/3","select/3","sort/2","msort/2","keysort/2","take/3","drop/3","reverse/2","list_to_set/2"];typeof X5<"u"?X5.exports=function(s){t=s,new t.type.Module("lists",e(),r)}:new t.type.Module("lists",e(),r)})(PSt)});var rve=L($r=>{"use strict";var Dm=process.platform==="win32",$5="aes-256-cbc",xSt="sha256",GBe="The current environment doesn't support interactive reading from TTY.",si=Ie("fs"),jBe=process.binding("tty_wrap").TTY,t9=Ie("child_process"),Y0=Ie("path"),r9={prompt:"> ",hideEchoBack:!1,mask:"*",limit:[],limitMessage:"Input another, please.$<( [)limit(])>",defaultInput:"",trueValue:[],falseValue:[],caseSensitive:!1,keepWhitespace:!1,encoding:"utf8",bufferSize:1024,print:void 0,history:!0,cd:!1,phContent:void 0,preCheck:void 0},$p="none",$u,VC,qBe=!1,W0,lF,e9,kSt=0,a9="",Sm=[],cF,WBe=!1,n9=!1,dS=!1;function YBe(t){function e(r){return r.replace(/[^\w\u0080-\uFFFF]/g,function(s){return"#"+s.charCodeAt(0)+";"})}return lF.concat(function(r){var s=[];return Object.keys(r).forEach(function(a){r[a]==="boolean"?t[a]&&s.push("--"+a):r[a]==="string"&&t[a]&&s.push("--"+a,e(t[a]))}),s}({display:"string",displayOnly:"boolean",keyIn:"boolean",hideEchoBack:"boolean",mask:"string",limit:"string",caseSensitive:"boolean"}))}function QSt(t,e){function r(U){var W,te="",ie;for(e9=e9||Ie("os").tmpdir();;){W=Y0.join(e9,U+te);try{ie=si.openSync(W,"wx")}catch(Ae){if(Ae.code==="EEXIST"){te++;continue}else throw Ae}si.closeSync(ie);break}return W}var s,a,n,c={},f,p,h=r("readline-sync.stdout"),E=r("readline-sync.stderr"),C=r("readline-sync.exit"),S=r("readline-sync.done"),P=Ie("crypto"),I,R,N;I=P.createHash(xSt),I.update(""+process.pid+kSt+++Math.random()),N=I.digest("hex"),R=P.createDecipher($5,N),s=YBe(t),Dm?(a=process.env.ComSpec||"cmd.exe",process.env.Q='"',n=["/V:ON","/S","/C","(%Q%"+a+"%Q% /V:ON /S /C %Q%%Q%"+W0+"%Q%"+s.map(function(U){return" %Q%"+U+"%Q%"}).join("")+" & (echo !ERRORLEVEL!)>%Q%"+C+"%Q%%Q%) 2>%Q%"+E+"%Q% |%Q%"+process.execPath+"%Q% %Q%"+__dirname+"\\encrypt.js%Q% %Q%"+$5+"%Q% %Q%"+N+"%Q% >%Q%"+h+"%Q% & (echo 1)>%Q%"+S+"%Q%"]):(a="/bin/sh",n=["-c",'("'+W0+'"'+s.map(function(U){return" '"+U.replace(/'/g,"'\\''")+"'"}).join("")+'; echo $?>"'+C+'") 2>"'+E+'" |"'+process.execPath+'" "'+__dirname+'/encrypt.js" "'+$5+'" "'+N+'" >"'+h+'"; echo 1 >"'+S+'"']),dS&&dS("_execFileSync",s);try{t9.spawn(a,n,e)}catch(U){c.error=new Error(U.message),c.error.method="_execFileSync - spawn",c.error.program=a,c.error.args=n}for(;si.readFileSync(S,{encoding:t.encoding}).trim()!=="1";);return(f=si.readFileSync(C,{encoding:t.encoding}).trim())==="0"?c.input=R.update(si.readFileSync(h,{encoding:"binary"}),"hex",t.encoding)+R.final(t.encoding):(p=si.readFileSync(E,{encoding:t.encoding}).trim(),c.error=new Error(GBe+(p?` `+p:"")),c.error.method="_execFileSync",c.error.program=a,c.error.args=n,c.error.extMessage=p,c.error.exitCode=+f),si.unlinkSync(h),si.unlinkSync(E),si.unlinkSync(C),si.unlinkSync(S),c}function TSt(t){var e,r={},s,a={env:process.env,encoding:t.encoding};if(W0||(Dm?process.env.PSModulePath?(W0="powershell.exe",lF=["-ExecutionPolicy","Bypass","-File",__dirname+"\\read.ps1"]):(W0="cscript.exe",lF=["//nologo",__dirname+"\\read.cs.js"]):(W0="/bin/sh",lF=[__dirname+"/read.sh"])),Dm&&!process.env.PSModulePath&&(a.stdio=[process.stdin]),t9.execFileSync){e=YBe(t),dS&&dS("execFileSync",e);try{r.input=t9.execFileSync(W0,e,a)}catch(n){s=n.stderr?(n.stderr+"").trim():"",r.error=new Error(GBe+(s?` `+s:"")),r.error.method="execFileSync",r.error.program=W0,r.error.args=e,r.error.extMessage=s,r.error.exitCode=n.status,r.error.code=n.code,r.error.signal=n.signal}}else r=QSt(t,a);return r.error||(r.input=r.input.replace(/^\s*'|'\s*$/g,""),t.display=""),r}function i9(t){var e="",r=t.display,s=!t.display&&t.keyIn&&t.hideEchoBack&&!t.mask;function a(){var n=TSt(t);if(n.error)throw n.error;return n.input}return n9&&n9(t),function(){var n,c,f;function p(){return n||(n=process.binding("fs"),c=process.binding("constants")),n}if(typeof $p=="string")if($p=null,Dm){if(f=function(h){var E=h.replace(/^\D+/,"").split("."),C=0;return(E[0]=+E[0])&&(C+=E[0]*1e4),(E[1]=+E[1])&&(C+=E[1]*100),(E[2]=+E[2])&&(C+=E[2]),C}(process.version),!(f>=20302&&f<40204||f>=5e4&&f<50100||f>=50600&&f<60200)&&process.stdin.isTTY)process.stdin.pause(),$p=process.stdin.fd,VC=process.stdin._handle;else try{$p=p().open("CONIN$",c.O_RDWR,parseInt("0666",8)),VC=new jBe($p,!0)}catch{}if(process.stdout.isTTY)$u=process.stdout.fd;else{try{$u=si.openSync("\\\\.\\CON","w")}catch{}if(typeof $u!="number")try{$u=p().open("CONOUT$",c.O_RDWR,parseInt("0666",8))}catch{}}}else{if(process.stdin.isTTY){process.stdin.pause();try{$p=si.openSync("/dev/tty","r"),VC=process.stdin._handle}catch{}}else try{$p=si.openSync("/dev/tty","r"),VC=new jBe($p,!1)}catch{}if(process.stdout.isTTY)$u=process.stdout.fd;else try{$u=si.openSync("/dev/tty","w")}catch{}}}(),function(){var n,c,f=!t.hideEchoBack&&!t.keyIn,p,h,E,C,S;cF="";function P(I){return I===qBe?!0:VC.setRawMode(I)!==0?!1:(qBe=I,!0)}if(WBe||!VC||typeof $u!="number"&&(t.display||!f)){e=a();return}if(t.display&&(si.writeSync($u,t.display),t.display=""),!t.displayOnly){if(!P(!f)){e=a();return}for(h=t.keyIn?1:t.bufferSize,p=Buffer.allocUnsafe&&Buffer.alloc?Buffer.alloc(h):new Buffer(h),t.keyIn&&t.limit&&(c=new RegExp("[^"+t.limit+"]","g"+(t.caseSensitive?"":"i")));;){E=0;try{E=si.readSync($p,p,0,h)}catch(I){if(I.code!=="EOF"){P(!1),e+=a();return}}if(E>0?(C=p.toString(t.encoding,0,E),cF+=C):(C=` `,cF+="\0"),C&&typeof(S=(C.match(/^(.*?)[\r\n]/)||[])[1])=="string"&&(C=S,n=!0),C&&(C=C.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g,"")),C&&c&&(C=C.replace(c,"")),C&&(f||(t.hideEchoBack?t.mask&&si.writeSync($u,new Array(C.length+1).join(t.mask)):si.writeSync($u,C)),e+=C),!t.keyIn&&n||t.keyIn&&e.length>=h)break}!f&&!s&&si.writeSync($u,` `),P(!1)}}(),t.print&&!s&&t.print(r+(t.displayOnly?"":(t.hideEchoBack?new Array(e.length+1).join(t.mask):e)+` `),t.encoding),t.displayOnly?"":a9=t.keepWhitespace||t.keyIn?e:e.trim()}function RSt(t,e){var r=[];function s(a){a!=null&&(Array.isArray(a)?a.forEach(s):(!e||e(a))&&r.push(a))}return s(t),r}function l9(t){return t.replace(/[\x00-\x7f]/g,function(e){return"\\x"+("00"+e.charCodeAt().toString(16)).substr(-2)})}function Js(){var t=Array.prototype.slice.call(arguments),e,r;return t.length&&typeof t[0]=="boolean"&&(r=t.shift(),r&&(e=Object.keys(r9),t.unshift(r9))),t.reduce(function(s,a){return a==null||(a.hasOwnProperty("noEchoBack")&&!a.hasOwnProperty("hideEchoBack")&&(a.hideEchoBack=a.noEchoBack,delete a.noEchoBack),a.hasOwnProperty("noTrim")&&!a.hasOwnProperty("keepWhitespace")&&(a.keepWhitespace=a.noTrim,delete a.noTrim),r||(e=Object.keys(a)),e.forEach(function(n){var c;if(a.hasOwnProperty(n))switch(c=a[n],n){case"mask":case"limitMessage":case"defaultInput":case"encoding":c=c!=null?c+"":"",c&&n!=="limitMessage"&&(c=c.replace(/[\r\n]/g,"")),s[n]=c;break;case"bufferSize":!isNaN(c=parseInt(c,10))&&typeof c=="number"&&(s[n]=c);break;case"displayOnly":case"keyIn":case"hideEchoBack":case"caseSensitive":case"keepWhitespace":case"history":case"cd":s[n]=!!c;break;case"limit":case"trueValue":case"falseValue":s[n]=RSt(c,function(f){var p=typeof f;return p==="string"||p==="number"||p==="function"||f instanceof RegExp}).map(function(f){return typeof f=="string"?f.replace(/[\r\n]/g,""):f});break;case"print":case"phContent":case"preCheck":s[n]=typeof c=="function"?c:void 0;break;case"prompt":case"display":s[n]=c??"";break}})),s},{})}function s9(t,e,r){return e.some(function(s){var a=typeof s;return a==="string"?r?t===s:t.toLowerCase()===s.toLowerCase():a==="number"?parseFloat(t)===s:a==="function"?s(t):s instanceof RegExp?s.test(t):!1})}function c9(t,e){var r=Y0.normalize(Dm?(process.env.HOMEDRIVE||"")+(process.env.HOMEPATH||""):process.env.HOME||"").replace(/[\/\\]+$/,"");return t=Y0.normalize(t),e?t.replace(/^~(?=\/|\\|$)/,r):t.replace(new RegExp("^"+l9(r)+"(?=\\/|\\\\|$)",Dm?"i":""),"~")}function KC(t,e){var r="(?:\\(([\\s\\S]*?)\\))?(\\w+|.-.)(?:\\(([\\s\\S]*?)\\))?",s=new RegExp("(\\$)?(\\$<"+r+">)","g"),a=new RegExp("(\\$)?(\\$\\{"+r+"\\})","g");function n(c,f,p,h,E,C){var S;return f||typeof(S=e(E))!="string"?p:S?(h||"")+S+(C||""):""}return t.replace(s,n).replace(a,n)}function VBe(t,e,r){var s,a=[],n=-1,c=0,f="",p;function h(E,C){return C.length>3?(E.push(C[0]+"..."+C[C.length-1]),p=!0):C.length&&(E=E.concat(C)),E}return s=t.reduce(function(E,C){return E.concat((C+"").split(""))},[]).reduce(function(E,C){var S,P;return e||(C=C.toLowerCase()),S=/^\d$/.test(C)?1:/^[A-Z]$/.test(C)?2:/^[a-z]$/.test(C)?3:0,r&&S===0?f+=C:(P=C.charCodeAt(0),S&&S===n&&P===c+1?a.push(C):(E=h(E,a),a=[C],n=S),c=P),E},[]),s=h(s,a),f&&(s.push(f),p=!0),{values:s,suppressed:p}}function KBe(t,e){return t.join(t.length>2?", ":e?" / ":"/")}function JBe(t,e){var r,s,a={},n;if(e.phContent&&(r=e.phContent(t,e)),typeof r!="string")switch(t){case"hideEchoBack":case"mask":case"defaultInput":case"caseSensitive":case"keepWhitespace":case"encoding":case"bufferSize":case"history":case"cd":r=e.hasOwnProperty(t)?typeof e[t]=="boolean"?e[t]?"on":"off":e[t]+"":"";break;case"limit":case"trueValue":case"falseValue":s=e[e.hasOwnProperty(t+"Src")?t+"Src":t],e.keyIn?(a=VBe(s,e.caseSensitive),s=a.values):s=s.filter(function(c){var f=typeof c;return f==="string"||f==="number"}),r=KBe(s,a.suppressed);break;case"limitCount":case"limitCountNotZero":r=e[e.hasOwnProperty("limitSrc")?"limitSrc":"limit"].length,r=r||t!=="limitCountNotZero"?r+"":"";break;case"lastInput":r=a9;break;case"cwd":case"CWD":case"cwdHome":r=process.cwd(),t==="CWD"?r=Y0.basename(r):t==="cwdHome"&&(r=c9(r));break;case"date":case"time":case"localeDate":case"localeTime":r=new Date()["to"+t.replace(/^./,function(c){return c.toUpperCase()})+"String"]();break;default:typeof(n=(t.match(/^history_m(\d+)$/)||[])[1])=="string"&&(r=Sm[Sm.length-n]||"")}return r}function zBe(t){var e=/^(.)-(.)$/.exec(t),r="",s,a,n,c;if(!e)return null;for(s=e[1].charCodeAt(0),a=e[2].charCodeAt(0),c=s And the length must be: $`,trueValue:null,falseValue:null,caseSensitive:!0},e,{history:!1,cd:!1,phContent:function(P){return P==="charlist"?r.text:P==="length"?s+"..."+a:null}}),c,f,p,h,E,C,S;for(e=e||{},c=KC(e.charlist?e.charlist+"":"$",zBe),(isNaN(s=parseInt(e.min,10))||typeof s!="number")&&(s=12),(isNaN(a=parseInt(e.max,10))||typeof a!="number")&&(a=24),h=new RegExp("^["+l9(c)+"]{"+s+","+a+"}$"),r=VBe([c],n.caseSensitive,!0),r.text=KBe(r.values,r.suppressed),f=e.confirmMessage!=null?e.confirmMessage:"Reinput a same one to confirm it: ",p=e.unmatchMessage!=null?e.unmatchMessage:"It differs from first one. Hit only the Enter key if you want to retry from first one.",t==null&&(t="Input new password: "),E=n.limitMessage;!S;)n.limit=h,n.limitMessage=E,C=$r.question(t,n),n.limit=[C,""],n.limitMessage=p,S=$r.question(f,n);return C};function $Be(t,e,r){var s;function a(n){return s=r(n),!isNaN(s)&&typeof s=="number"}return $r.question(t,Js({limitMessage:"Input valid number, please."},e,{limit:a,cd:!1})),s}$r.questionInt=function(t,e){return $Be(t,e,function(r){return parseInt(r,10)})};$r.questionFloat=function(t,e){return $Be(t,e,parseFloat)};$r.questionPath=function(t,e){var r,s="",a=Js({hideEchoBack:!1,limitMessage:`$Input valid path, please.$<( Min:)min>$<( Max:)max>`,history:!0,cd:!0},e,{keepWhitespace:!1,limit:function(n){var c,f,p;n=c9(n,!0),s="";function h(E){E.split(/\/|\\/).reduce(function(C,S){var P=Y0.resolve(C+=S+Y0.sep);if(!si.existsSync(P))si.mkdirSync(P);else if(!si.statSync(P).isDirectory())throw new Error("Non directory already exists: "+P);return C},"")}try{if(c=si.existsSync(n),r=c?si.realpathSync(n):Y0.resolve(n),!e.hasOwnProperty("exists")&&!c||typeof e.exists=="boolean"&&e.exists!==c)return s=(c?"Already exists":"No such file or directory")+": "+r,!1;if(!c&&e.create&&(e.isDirectory?h(r):(h(Y0.dirname(r)),si.closeSync(si.openSync(r,"w"))),r=si.realpathSync(r)),c&&(e.min||e.max||e.isFile||e.isDirectory)){if(f=si.statSync(r),e.isFile&&!f.isFile())return s="Not file: "+r,!1;if(e.isDirectory&&!f.isDirectory())return s="Not directory: "+r,!1;if(e.min&&f.size<+e.min||e.max&&f.size>+e.max)return s="Size "+f.size+" is out of range: "+r,!1}if(typeof e.validate=="function"&&(p=e.validate(r))!==!0)return typeof p=="string"&&(s=p),!1}catch(E){return s=E+"",!1}return!0},phContent:function(n){return n==="error"?s:n!=="min"&&n!=="max"?null:e.hasOwnProperty(n)?e[n]+"":""}});return e=e||{},t==null&&(t='Input path (you can "cd" and "pwd"): '),$r.question(t,a),r};function eve(t,e){var r={},s={};return typeof t=="object"?(Object.keys(t).forEach(function(a){typeof t[a]=="function"&&(s[e.caseSensitive?a:a.toLowerCase()]=t[a])}),r.preCheck=function(a){var n;return r.args=o9(a),n=r.args[0]||"",e.caseSensitive||(n=n.toLowerCase()),r.hRes=n!=="_"&&s.hasOwnProperty(n)?s[n].apply(a,r.args.slice(1)):s.hasOwnProperty("_")?s._.apply(a,r.args):null,{res:a,forceNext:!1}},s.hasOwnProperty("_")||(r.limit=function(){var a=r.args[0]||"";return e.caseSensitive||(a=a.toLowerCase()),s.hasOwnProperty(a)})):r.preCheck=function(a){return r.args=o9(a),r.hRes=typeof t=="function"?t.apply(a,r.args):!0,{res:a,forceNext:!1}},r}$r.promptCL=function(t,e){var r=Js({hideEchoBack:!1,limitMessage:"Requested command is not available.",caseSensitive:!1,history:!0},e),s=eve(t,r);return r.limit=s.limit,r.preCheck=s.preCheck,$r.prompt(r),s.args};$r.promptLoop=function(t,e){for(var r=Js({hideEchoBack:!1,trueValue:null,falseValue:null,caseSensitive:!1,history:!0},e);!t($r.prompt(r)););};$r.promptCLLoop=function(t,e){var r=Js({hideEchoBack:!1,limitMessage:"Requested command is not available.",caseSensitive:!1,history:!0},e),s=eve(t,r);for(r.limit=s.limit,r.preCheck=s.preCheck;$r.prompt(r),!s.hRes;);};$r.promptSimShell=function(t){return $r.prompt(Js({hideEchoBack:!1,history:!0},t,{prompt:function(){return Dm?"$>":(process.env.USER||"")+(process.env.HOSTNAME?"@"+process.env.HOSTNAME.replace(/\..*$/,""):"")+":$$ "}()}))};function tve(t,e,r){var s;return t==null&&(t="Are you sure? "),(!e||e.guide!==!1)&&(t+="")&&(t=t.replace(/\s*:?\s*$/,"")+" [y/n]: "),s=$r.keyIn(t,Js(e,{hideEchoBack:!1,limit:r,trueValue:"y",falseValue:"n",caseSensitive:!1})),typeof s=="boolean"?s:""}$r.keyInYN=function(t,e){return tve(t,e)};$r.keyInYNStrict=function(t,e){return tve(t,e,"yn")};$r.keyInPause=function(t,e){t==null&&(t="Continue..."),(!e||e.guide!==!1)&&(t+="")&&(t=t.replace(/\s+$/,"")+" (Hit any key)"),$r.keyIn(t,Js({limit:null},e,{hideEchoBack:!0,mask:""}))};$r.keyInSelect=function(t,e,r){var s=Js({hideEchoBack:!1},r,{trueValue:null,falseValue:null,caseSensitive:!1,phContent:function(p){return p==="itemsCount"?t.length+"":p==="firstItem"?(t[0]+"").trim():p==="lastItem"?(t[t.length-1]+"").trim():null}}),a="",n={},c=49,f=` `;if(!Array.isArray(t)||!t.length||t.length>35)throw"`items` must be Array (max length: 35).";return t.forEach(function(p,h){var E=String.fromCharCode(c);a+=E,n[E]=h,f+="["+E+"] "+(p+"").trim()+` `,c=c===57?97:c+1}),(!r||r.cancel!==!1)&&(a+="0",n[0]=-1,f+="[0] "+(r&&r.cancel!=null&&typeof r.cancel!="boolean"?(r.cancel+"").trim():"CANCEL")+` `),s.limit=a,f+=` `,e==null&&(e="Choose one from list: "),(e+="")&&((!r||r.guide!==!1)&&(e=e.replace(/\s*:?\s*$/,"")+" [$]: "),f+=e),n[$r.keyIn(f,s).toLowerCase()]};$r.getRawInput=function(){return cF};function mS(t,e){var r;return e.length&&(r={},r[t]=e[0]),$r.setDefaultOptions(r)[t]}$r.setPrint=function(){return mS("print",arguments)};$r.setPrompt=function(){return mS("prompt",arguments)};$r.setEncoding=function(){return mS("encoding",arguments)};$r.setMask=function(){return mS("mask",arguments)};$r.setBufferSize=function(){return mS("bufferSize",arguments)}});var u9=L((Tur,tc)=>{(function(){var t={major:0,minor:2,patch:66,status:"beta"};tau_file_system={files:{},open:function(w,b,y){var F=tau_file_system.files[w];if(!F){if(y==="read")return null;F={path:w,text:"",type:b,get:function(z,Z){return Z===this.text.length||Z>this.text.length?"end_of_file":this.text.substring(Z,Z+z)},put:function(z,Z){return Z==="end_of_file"?(this.text+=z,!0):Z==="past_end_of_file"?null:(this.text=this.text.substring(0,Z)+z+this.text.substring(Z+z.length),!0)},get_byte:function(z){if(z==="end_of_stream")return-1;var Z=Math.floor(z/2);if(this.text.length<=Z)return-1;var $=n(this.text[Math.floor(z/2)],0);return z%2===0?$&255:$/256>>>0},put_byte:function(z,Z){var $=Z==="end_of_stream"?this.text.length:Math.floor(Z/2);if(this.text.length<$)return null;var oe=this.text.length===$?-1:n(this.text[Math.floor(Z/2)],0);return Z%2===0?(oe=oe/256>>>0,oe=(oe&255)<<8|z&255):(oe=oe&255,oe=(z&255)<<8|oe&255),this.text.length===$?this.text+=c(oe):this.text=this.text.substring(0,$)+c(oe)+this.text.substring($+1),!0},flush:function(){return!0},close:function(){var z=tau_file_system.files[this.path];return z?!0:null}},tau_file_system.files[w]=F}return y==="write"&&(F.text=""),F}},tau_user_input={buffer:"",get:function(w,b){for(var y;tau_user_input.buffer.length\?\@\^\~\\]+|'(?:[^']*?(?:\\(?:x?\d+)?\\)*(?:'')*(?:\\')*)*')/,number:/^(?:0o[0-7]+|0x[0-9a-fA-F]+|0b[01]+|0'(?:''|\\[abfnrtv\\'"`]|\\x?\d+\\|[^\\])|\d+(?:\.\d+(?:[eE][+-]?\d+)?)?)/,string:/^(?:"([^"]|""|\\")*"|`([^`]|``|\\`)*`)/,l_brace:/^(?:\[)/,r_brace:/^(?:\])/,l_bracket:/^(?:\{)/,r_bracket:/^(?:\})/,bar:/^(?:\|)/,l_paren:/^(?:\()/,r_paren:/^(?:\))/};function N(w,b){return w.get_flag("char_conversion").id==="on"?b.replace(/./g,function(y){return w.get_char_conversion(y)}):b}function U(w){this.thread=w,this.text="",this.tokens=[]}U.prototype.set_last_tokens=function(w){return this.tokens=w},U.prototype.new_text=function(w){this.text=w,this.tokens=[]},U.prototype.get_tokens=function(w){var b,y=0,F=0,z=0,Z=[],$=!1;if(w){var oe=this.tokens[w-1];y=oe.len,b=N(this.thread,this.text.substr(oe.len)),F=oe.line,z=oe.start}else b=this.text;if(/^\s*$/.test(b))return null;for(;b!=="";){var xe=[],Te=!1;if(/^\n/.exec(b)!==null){F++,z=0,y++,b=b.replace(/\n/,""),$=!0;continue}for(var lt in R)if(R.hasOwnProperty(lt)){var It=R[lt].exec(b);It&&xe.push({value:It[0],name:lt,matches:It})}if(!xe.length)return this.set_last_tokens([{value:b,matches:[],name:"lexical",line:F,start:z}]);var oe=r(xe,function(Pr,Ir){return Pr.value.length>=Ir.value.length?Pr:Ir});switch(oe.start=z,oe.line=F,b=b.replace(oe.value,""),z+=oe.value.length,y+=oe.value.length,oe.name){case"atom":oe.raw=oe.value,oe.value.charAt(0)==="'"&&(oe.value=S(oe.value.substr(1,oe.value.length-2),"'"),oe.value===null&&(oe.name="lexical",oe.value="unknown escape sequence"));break;case"number":oe.float=oe.value.substring(0,2)!=="0x"&&oe.value.match(/[.eE]/)!==null&&oe.value!=="0'.",oe.value=I(oe.value),oe.blank=Te;break;case"string":var qt=oe.value.charAt(0);oe.value=S(oe.value.substr(1,oe.value.length-2),qt),oe.value===null&&(oe.name="lexical",oe.value="unknown escape sequence");break;case"whitespace":var ir=Z[Z.length-1];ir&&(ir.space=!0),Te=!0;continue;case"r_bracket":Z.length>0&&Z[Z.length-1].name==="l_bracket"&&(oe=Z.pop(),oe.name="atom",oe.value="{}",oe.raw="{}",oe.space=!1);break;case"r_brace":Z.length>0&&Z[Z.length-1].name==="l_brace"&&(oe=Z.pop(),oe.name="atom",oe.value="[]",oe.raw="[]",oe.space=!1);break}oe.len=y,Z.push(oe),Te=!1}var Pt=this.set_last_tokens(Z);return Pt.length===0?null:Pt};function W(w,b,y,F,z){if(!b[y])return{type:f,value:x.error.syntax(b[y-1],"expression expected",!0)};var Z;if(F==="0"){var $=b[y];switch($.name){case"number":return{type:p,len:y+1,value:new x.type.Num($.value,$.float)};case"variable":return{type:p,len:y+1,value:new x.type.Var($.value)};case"string":var oe;switch(w.get_flag("double_quotes").id){case"atom":oe=new j($.value,[]);break;case"codes":oe=new j("[]",[]);for(var xe=$.value.length-1;xe>=0;xe--)oe=new j(".",[new x.type.Num(n($.value,xe),!1),oe]);break;case"chars":oe=new j("[]",[]);for(var xe=$.value.length-1;xe>=0;xe--)oe=new j(".",[new x.type.Term($.value.charAt(xe),[]),oe]);break}return{type:p,len:y+1,value:oe};case"l_paren":var Pt=W(w,b,y+1,w.__get_max_priority(),!0);return Pt.type!==p?Pt:b[Pt.len]&&b[Pt.len].name==="r_paren"?(Pt.len++,Pt):{type:f,derived:!0,value:x.error.syntax(b[Pt.len]?b[Pt.len]:b[Pt.len-1],") or operator expected",!b[Pt.len])};case"l_bracket":var Pt=W(w,b,y+1,w.__get_max_priority(),!0);return Pt.type!==p?Pt:b[Pt.len]&&b[Pt.len].name==="r_bracket"?(Pt.len++,Pt.value=new j("{}",[Pt.value]),Pt):{type:f,derived:!0,value:x.error.syntax(b[Pt.len]?b[Pt.len]:b[Pt.len-1],"} or operator expected",!b[Pt.len])}}var Te=te(w,b,y,z);return Te.type===p||Te.derived||(Te=ie(w,b,y),Te.type===p||Te.derived)?Te:{type:f,derived:!1,value:x.error.syntax(b[y],"unexpected token")}}var lt=w.__get_max_priority(),It=w.__get_next_priority(F),qt=y;if(b[y].name==="atom"&&b[y+1]&&(b[y].space||b[y+1].name!=="l_paren")){var $=b[y++],ir=w.__lookup_operator_classes(F,$.value);if(ir&&ir.indexOf("fy")>-1){var Pt=W(w,b,y,F,z);if(Pt.type!==f)return $.value==="-"&&!$.space&&x.type.is_number(Pt.value)?{value:new x.type.Num(-Pt.value.value,Pt.value.is_float),len:Pt.len,type:p}:{value:new x.type.Term($.value,[Pt.value]),len:Pt.len,type:p};Z=Pt}else if(ir&&ir.indexOf("fx")>-1){var Pt=W(w,b,y,It,z);if(Pt.type!==f)return{value:new x.type.Term($.value,[Pt.value]),len:Pt.len,type:p};Z=Pt}}y=qt;var Pt=W(w,b,y,It,z);if(Pt.type===p){y=Pt.len;var $=b[y];if(b[y]&&(b[y].name==="atom"&&w.__lookup_operator_classes(F,$.value)||b[y].name==="bar"&&w.__lookup_operator_classes(F,"|"))){var gn=It,Pr=F,ir=w.__lookup_operator_classes(F,$.value);if(ir.indexOf("xf")>-1)return{value:new x.type.Term($.value,[Pt.value]),len:++Pt.len,type:p};if(ir.indexOf("xfx")>-1){var Ir=W(w,b,y+1,gn,z);return Ir.type===p?{value:new x.type.Term($.value,[Pt.value,Ir.value]),len:Ir.len,type:p}:(Ir.derived=!0,Ir)}else if(ir.indexOf("xfy")>-1){var Ir=W(w,b,y+1,Pr,z);return Ir.type===p?{value:new x.type.Term($.value,[Pt.value,Ir.value]),len:Ir.len,type:p}:(Ir.derived=!0,Ir)}else if(Pt.type!==f)for(;;){y=Pt.len;var $=b[y];if($&&$.name==="atom"&&w.__lookup_operator_classes(F,$.value)){var ir=w.__lookup_operator_classes(F,$.value);if(ir.indexOf("yf")>-1)Pt={value:new x.type.Term($.value,[Pt.value]),len:++y,type:p};else if(ir.indexOf("yfx")>-1){var Ir=W(w,b,++y,gn,z);if(Ir.type===f)return Ir.derived=!0,Ir;y=Ir.len,Pt={value:new x.type.Term($.value,[Pt.value,Ir.value]),len:y,type:p}}else break}else break}}else Z={type:f,value:x.error.syntax(b[Pt.len-1],"operator expected")};return Pt}return Pt}function te(w,b,y,F){if(!b[y]||b[y].name==="atom"&&b[y].raw==="."&&!F&&(b[y].space||!b[y+1]||b[y+1].name!=="l_paren"))return{type:f,derived:!1,value:x.error.syntax(b[y-1],"unfounded token")};var z=b[y],Z=[];if(b[y].name==="atom"&&b[y].raw!==","){if(y++,b[y-1].space)return{type:p,len:y,value:new x.type.Term(z.value,Z)};if(b[y]&&b[y].name==="l_paren"){if(b[y+1]&&b[y+1].name==="r_paren")return{type:f,derived:!0,value:x.error.syntax(b[y+1],"argument expected")};var $=W(w,b,++y,"999",!0);if($.type===f)return $.derived?$:{type:f,derived:!0,value:x.error.syntax(b[y]?b[y]:b[y-1],"argument expected",!b[y])};for(Z.push($.value),y=$.len;b[y]&&b[y].name==="atom"&&b[y].value===",";){if($=W(w,b,y+1,"999",!0),$.type===f)return $.derived?$:{type:f,derived:!0,value:x.error.syntax(b[y+1]?b[y+1]:b[y],"argument expected",!b[y+1])};Z.push($.value),y=$.len}if(b[y]&&b[y].name==="r_paren")y++;else return{type:f,derived:!0,value:x.error.syntax(b[y]?b[y]:b[y-1],", or ) expected",!b[y])}}return{type:p,len:y,value:new x.type.Term(z.value,Z)}}return{type:f,derived:!1,value:x.error.syntax(b[y],"term expected")}}function ie(w,b,y){if(!b[y])return{type:f,derived:!1,value:x.error.syntax(b[y-1],"[ expected")};if(b[y]&&b[y].name==="l_brace"){var F=W(w,b,++y,"999",!0),z=[F.value],Z=void 0;if(F.type===f)return b[y]&&b[y].name==="r_brace"?{type:p,len:y+1,value:new x.type.Term("[]",[])}:{type:f,derived:!0,value:x.error.syntax(b[y],"] expected")};for(y=F.len;b[y]&&b[y].name==="atom"&&b[y].value===",";){if(F=W(w,b,y+1,"999",!0),F.type===f)return F.derived?F:{type:f,derived:!0,value:x.error.syntax(b[y+1]?b[y+1]:b[y],"argument expected",!b[y+1])};z.push(F.value),y=F.len}var $=!1;if(b[y]&&b[y].name==="bar"){if($=!0,F=W(w,b,y+1,"999",!0),F.type===f)return F.derived?F:{type:f,derived:!0,value:x.error.syntax(b[y+1]?b[y+1]:b[y],"argument expected",!b[y+1])};Z=F.value,y=F.len}return b[y]&&b[y].name==="r_brace"?{type:p,len:y+1,value:g(z,Z)}:{type:f,derived:!0,value:x.error.syntax(b[y]?b[y]:b[y-1],$?"] expected":", or | or ] expected",!b[y])}}return{type:f,derived:!1,value:x.error.syntax(b[y],"list expected")}}function Ae(w,b,y){var F=b[y].line,z=W(w,b,y,w.__get_max_priority(),!1),Z=null,$;if(z.type!==f)if(y=z.len,b[y]&&b[y].name==="atom"&&b[y].raw===".")if(y++,x.type.is_term(z.value)){if(z.value.indicator===":-/2"?(Z=new x.type.Rule(z.value.args[0],Ce(z.value.args[1])),$={value:Z,len:y,type:p}):z.value.indicator==="-->/2"?(Z=pe(new x.type.Rule(z.value.args[0],z.value.args[1]),w),Z.body=Ce(Z.body),$={value:Z,len:y,type:x.type.is_rule(Z)?p:f}):(Z=new x.type.Rule(z.value,null),$={value:Z,len:y,type:p}),Z){var oe=Z.singleton_variables();oe.length>0&&w.throw_warning(x.warning.singleton(oe,Z.head.indicator,F))}return $}else return{type:f,value:x.error.syntax(b[y],"callable expected")};else return{type:f,value:x.error.syntax(b[y]?b[y]:b[y-1],". or operator expected")};return z}function ce(w,b,y){y=y||{},y.from=y.from?y.from:"$tau-js",y.reconsult=y.reconsult!==void 0?y.reconsult:!0;var F=new U(w),z={},Z;F.new_text(b);var $=0,oe=F.get_tokens($);do{if(oe===null||!oe[$])break;var xe=Ae(w,oe,$);if(xe.type===f)return new j("throw",[xe.value]);if(xe.value.body===null&&xe.value.head.indicator==="?-/1"){var Te=new it(w.session);Te.add_goal(xe.value.head.args[0]),Te.answer(function(It){x.type.is_error(It)?w.throw_warning(It.args[0]):(It===!1||It===null)&&w.throw_warning(x.warning.failed_goal(xe.value.head.args[0],xe.len))}),$=xe.len;var lt=!0}else if(xe.value.body===null&&xe.value.head.indicator===":-/1"){var lt=w.run_directive(xe.value.head.args[0]);$=xe.len,xe.value.head.args[0].indicator==="char_conversion/2"&&(oe=F.get_tokens($),$=0)}else{Z=xe.value.head.indicator,y.reconsult!==!1&&z[Z]!==!0&&!w.is_multifile_predicate(Z)&&(w.session.rules[Z]=a(w.session.rules[Z]||[],function(qt){return qt.dynamic}),z[Z]=!0);var lt=w.add_rule(xe.value,y);$=xe.len}if(!lt)return lt}while(!0);return!0}function me(w,b){var y=new U(w);y.new_text(b);var F=0;do{var z=y.get_tokens(F);if(z===null)break;var Z=W(w,z,0,w.__get_max_priority(),!1);if(Z.type!==f){var $=Z.len,oe=$;if(z[$]&&z[$].name==="atom"&&z[$].raw===".")w.add_goal(Ce(Z.value));else{var xe=z[$];return new j("throw",[x.error.syntax(xe||z[$-1],". or operator expected",!xe)])}F=Z.len+1}else return new j("throw",[Z.value])}while(!0);return!0}function pe(w,b){w=w.rename(b);var y=b.next_free_variable(),F=Be(w.body,y,b);return F.error?F.value:(w.body=F.value,w.head.args=w.head.args.concat([y,F.variable]),w.head=new j(w.head.id,w.head.args),w)}function Be(w,b,y){var F;if(x.type.is_term(w)&&w.indicator==="!/0")return{value:w,variable:b,error:!1};if(x.type.is_term(w)&&w.indicator===",/2"){var z=Be(w.args[0],b,y);if(z.error)return z;var Z=Be(w.args[1],z.variable,y);return Z.error?Z:{value:new j(",",[z.value,Z.value]),variable:Z.variable,error:!1}}else{if(x.type.is_term(w)&&w.indicator==="{}/1")return{value:w.args[0],variable:b,error:!1};if(x.type.is_empty_list(w))return{value:new j("true",[]),variable:b,error:!1};if(x.type.is_list(w)){F=y.next_free_variable();for(var $=w,oe;$.indicator==="./2";)oe=$,$=$.args[1];return x.type.is_variable($)?{value:x.error.instantiation("DCG"),variable:b,error:!0}:x.type.is_empty_list($)?(oe.args[1]=F,{value:new j("=",[b,w]),variable:F,error:!1}):{value:x.error.type("list",w,"DCG"),variable:b,error:!0}}else return x.type.is_callable(w)?(F=y.next_free_variable(),w.args=w.args.concat([b,F]),w=new j(w.id,w.args),{value:w,variable:F,error:!1}):{value:x.error.type("callable",w,"DCG"),variable:b,error:!0}}}function Ce(w){return x.type.is_variable(w)?new j("call",[w]):x.type.is_term(w)&&[",/2",";/2","->/2"].indexOf(w.indicator)!==-1?new j(w.id,[Ce(w.args[0]),Ce(w.args[1])]):w}function g(w,b){for(var y=b||new x.type.Term("[]",[]),F=w.length-1;F>=0;F--)y=new x.type.Term(".",[w[F],y]);return y}function we(w,b){for(var y=w.length-1;y>=0;y--)w[y]===b&&w.splice(y,1)}function ye(w){for(var b={},y=[],F=0;F=0;b--)if(w.charAt(b)==="/")return new j("/",[new j(w.substring(0,b)),new Re(parseInt(w.substring(b+1)),!1)])}function De(w){this.id=w}function Re(w,b){this.is_float=b!==void 0?b:parseInt(w)!==w,this.value=this.is_float?w:parseInt(w)}var dt=0;function j(w,b,y){this.ref=y||++dt,this.id=w,this.args=b||[],this.indicator=w+"/"+this.args.length}var rt=0;function Fe(w,b,y,F,z,Z){this.id=rt++,this.stream=w,this.mode=b,this.alias=y,this.type=F!==void 0?F:"text",this.reposition=z!==void 0?z:!0,this.eof_action=Z!==void 0?Z:"eof_code",this.position=this.mode==="append"?"end_of_stream":0,this.output=this.mode==="write"||this.mode==="append",this.input=this.mode==="read"}function Ne(w){w=w||{},this.links=w}function Pe(w,b,y){b=b||new Ne,y=y||null,this.goal=w,this.substitution=b,this.parent=y}function Ye(w,b,y){this.head=w,this.body=b,this.dynamic=y||!1}function ke(w){w=w===void 0||w<=0?1e3:w,this.rules={},this.src_predicates={},this.rename=0,this.modules=[],this.thread=new it(this),this.total_threads=1,this.renamed_variables={},this.public_predicates={},this.multifile_predicates={},this.limit=w,this.streams={user_input:new Fe(typeof tc<"u"&&tc.exports?nodejs_user_input:tau_user_input,"read","user_input","text",!1,"reset"),user_output:new Fe(typeof tc<"u"&&tc.exports?nodejs_user_output:tau_user_output,"write","user_output","text",!1,"eof_code")},this.file_system=typeof tc<"u"&&tc.exports?nodejs_file_system:tau_file_system,this.standard_input=this.streams.user_input,this.standard_output=this.streams.user_output,this.current_input=this.streams.user_input,this.current_output=this.streams.user_output,this.format_success=function(b){return b.substitution},this.format_error=function(b){return b.goal},this.flag={bounded:x.flag.bounded.value,max_integer:x.flag.max_integer.value,min_integer:x.flag.min_integer.value,integer_rounding_function:x.flag.integer_rounding_function.value,char_conversion:x.flag.char_conversion.value,debug:x.flag.debug.value,max_arity:x.flag.max_arity.value,unknown:x.flag.unknown.value,double_quotes:x.flag.double_quotes.value,occurs_check:x.flag.occurs_check.value,dialect:x.flag.dialect.value,version_data:x.flag.version_data.value,nodejs:x.flag.nodejs.value},this.__loaded_modules=[],this.__char_conversion={},this.__operators={1200:{":-":["fx","xfx"],"-->":["xfx"],"?-":["fx"]},1100:{";":["xfy"]},1050:{"->":["xfy"]},1e3:{",":["xfy"]},900:{"\\+":["fy"]},700:{"=":["xfx"],"\\=":["xfx"],"==":["xfx"],"\\==":["xfx"],"@<":["xfx"],"@=<":["xfx"],"@>":["xfx"],"@>=":["xfx"],"=..":["xfx"],is:["xfx"],"=:=":["xfx"],"=\\=":["xfx"],"<":["xfx"],"=<":["xfx"],">":["xfx"],">=":["xfx"]},600:{":":["xfy"]},500:{"+":["yfx"],"-":["yfx"],"/\\":["yfx"],"\\/":["yfx"]},400:{"*":["yfx"],"/":["yfx"],"//":["yfx"],rem:["yfx"],mod:["yfx"],"<<":["yfx"],">>":["yfx"]},200:{"**":["xfx"],"^":["xfy"],"-":["fy"],"+":["fy"],"\\":["fy"]}}}function it(w){this.epoch=Date.now(),this.session=w,this.session.total_threads++,this.total_steps=0,this.cpu_time=0,this.cpu_time_last=0,this.points=[],this.debugger=!1,this.debugger_states=[],this.level="top_level/0",this.__calls=[],this.current_limit=this.session.limit,this.warnings=[]}function _e(w,b,y){this.id=w,this.rules=b,this.exports=y,x.module[w]=this}_e.prototype.exports_predicate=function(w){return this.exports.indexOf(w)!==-1},De.prototype.unify=function(w,b){if(b&&e(w.variables(),this.id)!==-1&&!x.type.is_variable(w))return null;var y={};return y[this.id]=w,new Ne(y)},Re.prototype.unify=function(w,b){return x.type.is_number(w)&&this.value===w.value&&this.is_float===w.is_float?new Ne:null},j.prototype.unify=function(w,b){if(x.type.is_term(w)&&this.indicator===w.indicator){for(var y=new Ne,F=0;F=0){var F=this.args[0].value,z=Math.floor(F/26),Z=F%26;return"ABCDEFGHIJKLMNOPQRSTUVWXYZ"[Z]+(z!==0?z:"")}switch(this.indicator){case"[]/0":case"{}/0":case"!/0":return this.id;case"{}/1":return"{"+this.args[0].toString(w)+"}";case"./2":for(var $="["+this.args[0].toString(w),oe=this.args[1];oe.indicator==="./2";)$+=", "+oe.args[0].toString(w),oe=oe.args[1];return oe.indicator!=="[]/0"&&($+="|"+oe.toString(w)),$+="]",$;case",/2":return"("+this.args[0].toString(w)+", "+this.args[1].toString(w)+")";default:var xe=this.id,Te=w.session?w.session.lookup_operator(this.id,this.args.length):null;if(w.session===void 0||w.ignore_ops||Te===null)return w.quoted&&!/^(!|,|;|[a-z][0-9a-zA-Z_]*)$/.test(xe)&&xe!=="{}"&&xe!=="[]"&&(xe="'"+P(xe)+"'"),xe+(this.args.length?"("+s(this.args,function(ir){return ir.toString(w)}).join(", ")+")":"");var lt=Te.priority>b.priority||Te.priority===b.priority&&(Te.class==="xfy"&&this.indicator!==b.indicator||Te.class==="yfx"&&this.indicator!==b.indicator||this.indicator===b.indicator&&Te.class==="yfx"&&y==="right"||this.indicator===b.indicator&&Te.class==="xfy"&&y==="left");Te.indicator=this.indicator;var It=lt?"(":"",qt=lt?")":"";return this.args.length===0?"("+this.id+")":["fy","fx"].indexOf(Te.class)!==-1?It+xe+" "+this.args[0].toString(w,Te)+qt:["yf","xf"].indexOf(Te.class)!==-1?It+this.args[0].toString(w,Te)+" "+xe+qt:It+this.args[0].toString(w,Te,"left")+" "+this.id+" "+this.args[1].toString(w,Te,"right")+qt}},Fe.prototype.toString=function(w){return"("+this.id+")"},Ne.prototype.toString=function(w){var b="{";for(var y in this.links)this.links.hasOwnProperty(y)&&(b!=="{"&&(b+=", "),b+=y+"/"+this.links[y].toString(w));return b+="}",b},Pe.prototype.toString=function(w){return this.goal===null?"<"+this.substitution.toString(w)+">":"<"+this.goal.toString(w)+", "+this.substitution.toString(w)+">"},Ye.prototype.toString=function(w){return this.body?this.head.toString(w)+" :- "+this.body.toString(w)+".":this.head.toString(w)+"."},ke.prototype.toString=function(w){for(var b="",y=0;y=0;z--)F=new j(".",[b[z],F]);return F}return new j(this.id,s(this.args,function(Z){return Z.apply(w)}),this.ref)},Fe.prototype.apply=function(w){return this},Ye.prototype.apply=function(w){return new Ye(this.head.apply(w),this.body!==null?this.body.apply(w):null)},Ne.prototype.apply=function(w){var b,y={};for(b in this.links)this.links.hasOwnProperty(b)&&(y[b]=this.links[b].apply(w));return new Ne(y)},j.prototype.select=function(){for(var w=this;w.indicator===",/2";)w=w.args[0];return w},j.prototype.replace=function(w){return this.indicator===",/2"?this.args[0].indicator===",/2"?new j(",",[this.args[0].replace(w),this.args[1]]):w===null?this.args[1]:new j(",",[w,this.args[1]]):w},j.prototype.search=function(w){if(x.type.is_term(w)&&w.ref!==void 0&&this.ref===w.ref)return!0;for(var b=0;bb&&F0&&(b=this.head_point().substitution.domain());e(b,x.format_variable(this.session.rename))!==-1;)this.session.rename++;if(w.id==="_")return new De(x.format_variable(this.session.rename));this.session.renamed_variables[w.id]=x.format_variable(this.session.rename)}return new De(this.session.renamed_variables[w.id])},ke.prototype.next_free_variable=function(){return this.thread.next_free_variable()},it.prototype.next_free_variable=function(){this.session.rename++;var w=[];for(this.points.length>0&&(w=this.head_point().substitution.domain());e(w,x.format_variable(this.session.rename))!==-1;)this.session.rename++;return new De(x.format_variable(this.session.rename))},ke.prototype.is_public_predicate=function(w){return!this.public_predicates.hasOwnProperty(w)||this.public_predicates[w]===!0},it.prototype.is_public_predicate=function(w){return this.session.is_public_predicate(w)},ke.prototype.is_multifile_predicate=function(w){return this.multifile_predicates.hasOwnProperty(w)&&this.multifile_predicates[w]===!0},it.prototype.is_multifile_predicate=function(w){return this.session.is_multifile_predicate(w)},ke.prototype.prepend=function(w){return this.thread.prepend(w)},it.prototype.prepend=function(w){for(var b=w.length-1;b>=0;b--)this.points.push(w[b])},ke.prototype.success=function(w,b){return this.thread.success(w,b)},it.prototype.success=function(w,y){var y=typeof y>"u"?w:y;this.prepend([new Pe(w.goal.replace(null),w.substitution,y)])},ke.prototype.throw_error=function(w){return this.thread.throw_error(w)},it.prototype.throw_error=function(w){this.prepend([new Pe(new j("throw",[w]),new Ne,null,null)])},ke.prototype.step_rule=function(w,b){return this.thread.step_rule(w,b)},it.prototype.step_rule=function(w,b){var y=b.indicator;if(w==="user"&&(w=null),w===null&&this.session.rules.hasOwnProperty(y))return this.session.rules[y];for(var F=w===null?this.session.modules:e(this.session.modules,w)===-1?[]:[w],z=0;z1)&&this.again()},ke.prototype.answers=function(w,b,y){return this.thread.answers(w,b,y)},it.prototype.answers=function(w,b,y){var F=b||1e3,z=this;if(b<=0){y&&y();return}this.answer(function(Z){w(Z),Z!==!1?setTimeout(function(){z.answers(w,b-1,y)},1):y&&y()})},ke.prototype.again=function(w){return this.thread.again(w)},it.prototype.again=function(w){for(var b,y=Date.now();this.__calls.length>0;){for(this.warnings=[],w!==!1&&(this.current_limit=this.session.limit);this.current_limit>0&&this.points.length>0&&this.head_point().goal!==null&&!x.type.is_error(this.head_point().goal);)if(this.current_limit--,this.step()===!0)return;var F=Date.now();this.cpu_time_last=F-y,this.cpu_time+=this.cpu_time_last;var z=this.__calls.shift();this.current_limit<=0?z(null):this.points.length===0?z(!1):x.type.is_error(this.head_point().goal)?(b=this.session.format_error(this.points.pop()),this.points=[],z(b)):(this.debugger&&this.debugger_states.push(this.head_point()),b=this.session.format_success(this.points.pop()),z(b))}},ke.prototype.unfold=function(w){if(w.body===null)return!1;var b=w.head,y=w.body,F=y.select(),z=new it(this),Z=[];z.add_goal(F),z.step();for(var $=z.points.length-1;$>=0;$--){var oe=z.points[$],xe=b.apply(oe.substitution),Te=y.replace(oe.goal);Te!==null&&(Te=Te.apply(oe.substitution)),Z.push(new Ye(xe,Te))}var lt=this.rules[b.indicator],It=e(lt,w);return Z.length>0&&It!==-1?(lt.splice.apply(lt,[It,1].concat(Z)),!0):!1},it.prototype.unfold=function(w){return this.session.unfold(w)},De.prototype.interpret=function(w){return x.error.instantiation(w.level)},Re.prototype.interpret=function(w){return this},j.prototype.interpret=function(w){return x.type.is_unitary_list(this)?this.args[0].interpret(w):x.operate(w,this)},De.prototype.compare=function(w){return this.idw.id?1:0},Re.prototype.compare=function(w){if(this.value===w.value&&this.is_float===w.is_float)return 0;if(this.valuew.value)return 1},j.prototype.compare=function(w){if(this.args.lengthw.args.length||this.args.length===w.args.length&&this.id>w.id)return 1;for(var b=0;bF)return 1;if(w.constructor===Re){if(w.is_float&&b.is_float)return 0;if(w.is_float)return-1;if(b.is_float)return 1}return 0},is_substitution:function(w){return w instanceof Ne},is_state:function(w){return w instanceof Pe},is_rule:function(w){return w instanceof Ye},is_variable:function(w){return w instanceof De},is_stream:function(w){return w instanceof Fe},is_anonymous_var:function(w){return w instanceof De&&w.id==="_"},is_callable:function(w){return w instanceof j},is_number:function(w){return w instanceof Re},is_integer:function(w){return w instanceof Re&&!w.is_float},is_float:function(w){return w instanceof Re&&w.is_float},is_term:function(w){return w instanceof j},is_atom:function(w){return w instanceof j&&w.args.length===0},is_ground:function(w){if(w instanceof De)return!1;if(w instanceof j){for(var b=0;b0},is_list:function(w){return w instanceof j&&(w.indicator==="[]/0"||w.indicator==="./2")},is_empty_list:function(w){return w instanceof j&&w.indicator==="[]/0"},is_non_empty_list:function(w){return w instanceof j&&w.indicator==="./2"},is_fully_list:function(w){for(;w instanceof j&&w.indicator==="./2";)w=w.args[1];return w instanceof De||w instanceof j&&w.indicator==="[]/0"},is_instantiated_list:function(w){for(;w instanceof j&&w.indicator==="./2";)w=w.args[1];return w instanceof j&&w.indicator==="[]/0"},is_unitary_list:function(w){return w instanceof j&&w.indicator==="./2"&&w.args[1]instanceof j&&w.args[1].indicator==="[]/0"},is_character:function(w){return w instanceof j&&(w.id.length===1||w.id.length>0&&w.id.length<=2&&n(w.id,0)>=65536)},is_character_code:function(w){return w instanceof Re&&!w.is_float&&w.value>=0&&w.value<=1114111},is_byte:function(w){return w instanceof Re&&!w.is_float&&w.value>=0&&w.value<=255},is_operator:function(w){return w instanceof j&&x.arithmetic.evaluation[w.indicator]},is_directive:function(w){return w instanceof j&&x.directive[w.indicator]!==void 0},is_builtin:function(w){return w instanceof j&&x.predicate[w.indicator]!==void 0},is_error:function(w){return w instanceof j&&w.indicator==="throw/1"},is_predicate_indicator:function(w){return w instanceof j&&w.indicator==="//2"&&w.args[0]instanceof j&&w.args[0].args.length===0&&w.args[1]instanceof Re&&w.args[1].is_float===!1},is_flag:function(w){return w instanceof j&&w.args.length===0&&x.flag[w.id]!==void 0},is_value_flag:function(w,b){if(!x.type.is_flag(w))return!1;for(var y in x.flag[w.id].allowed)if(x.flag[w.id].allowed.hasOwnProperty(y)&&x.flag[w.id].allowed[y].equals(b))return!0;return!1},is_io_mode:function(w){return x.type.is_atom(w)&&["read","write","append"].indexOf(w.id)!==-1},is_stream_option:function(w){return x.type.is_term(w)&&(w.indicator==="alias/1"&&x.type.is_atom(w.args[0])||w.indicator==="reposition/1"&&x.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false")||w.indicator==="type/1"&&x.type.is_atom(w.args[0])&&(w.args[0].id==="text"||w.args[0].id==="binary")||w.indicator==="eof_action/1"&&x.type.is_atom(w.args[0])&&(w.args[0].id==="error"||w.args[0].id==="eof_code"||w.args[0].id==="reset"))},is_stream_position:function(w){return x.type.is_integer(w)&&w.value>=0||x.type.is_atom(w)&&(w.id==="end_of_stream"||w.id==="past_end_of_stream")},is_stream_property:function(w){return x.type.is_term(w)&&(w.indicator==="input/0"||w.indicator==="output/0"||w.indicator==="alias/1"&&(x.type.is_variable(w.args[0])||x.type.is_atom(w.args[0]))||w.indicator==="file_name/1"&&(x.type.is_variable(w.args[0])||x.type.is_atom(w.args[0]))||w.indicator==="position/1"&&(x.type.is_variable(w.args[0])||x.type.is_stream_position(w.args[0]))||w.indicator==="reposition/1"&&(x.type.is_variable(w.args[0])||x.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false"))||w.indicator==="type/1"&&(x.type.is_variable(w.args[0])||x.type.is_atom(w.args[0])&&(w.args[0].id==="text"||w.args[0].id==="binary"))||w.indicator==="mode/1"&&(x.type.is_variable(w.args[0])||x.type.is_atom(w.args[0])&&(w.args[0].id==="read"||w.args[0].id==="write"||w.args[0].id==="append"))||w.indicator==="eof_action/1"&&(x.type.is_variable(w.args[0])||x.type.is_atom(w.args[0])&&(w.args[0].id==="error"||w.args[0].id==="eof_code"||w.args[0].id==="reset"))||w.indicator==="end_of_stream/1"&&(x.type.is_variable(w.args[0])||x.type.is_atom(w.args[0])&&(w.args[0].id==="at"||w.args[0].id==="past"||w.args[0].id==="not")))},is_streamable:function(w){return w.__proto__.stream!==void 0},is_read_option:function(w){return x.type.is_term(w)&&["variables/1","variable_names/1","singletons/1"].indexOf(w.indicator)!==-1},is_write_option:function(w){return x.type.is_term(w)&&(w.indicator==="quoted/1"&&x.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false")||w.indicator==="ignore_ops/1"&&x.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false")||w.indicator==="numbervars/1"&&x.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false"))},is_close_option:function(w){return x.type.is_term(w)&&w.indicator==="force/1"&&x.type.is_atom(w.args[0])&&(w.args[0].id==="true"||w.args[0].id==="false")},is_modifiable_flag:function(w){return x.type.is_flag(w)&&x.flag[w.id].changeable},is_module:function(w){return w instanceof j&&w.indicator==="library/1"&&w.args[0]instanceof j&&w.args[0].args.length===0&&x.module[w.args[0].id]!==void 0}},arithmetic:{evaluation:{"e/0":{type_args:null,type_result:!0,fn:function(w){return Math.E}},"pi/0":{type_args:null,type_result:!0,fn:function(w){return Math.PI}},"tau/0":{type_args:null,type_result:!0,fn:function(w){return 2*Math.PI}},"epsilon/0":{type_args:null,type_result:!0,fn:function(w){return Number.EPSILON}},"+/1":{type_args:null,type_result:null,fn:function(w,b){return w}},"-/1":{type_args:null,type_result:null,fn:function(w,b){return-w}},"\\/1":{type_args:!1,type_result:!1,fn:function(w,b){return~w}},"abs/1":{type_args:null,type_result:null,fn:function(w,b){return Math.abs(w)}},"sign/1":{type_args:null,type_result:null,fn:function(w,b){return Math.sign(w)}},"float_integer_part/1":{type_args:!0,type_result:!1,fn:function(w,b){return parseInt(w)}},"float_fractional_part/1":{type_args:!0,type_result:!0,fn:function(w,b){return w-parseInt(w)}},"float/1":{type_args:null,type_result:!0,fn:function(w,b){return parseFloat(w)}},"floor/1":{type_args:!0,type_result:!1,fn:function(w,b){return Math.floor(w)}},"truncate/1":{type_args:!0,type_result:!1,fn:function(w,b){return parseInt(w)}},"round/1":{type_args:!0,type_result:!1,fn:function(w,b){return Math.round(w)}},"ceiling/1":{type_args:!0,type_result:!1,fn:function(w,b){return Math.ceil(w)}},"sin/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.sin(w)}},"cos/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.cos(w)}},"tan/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.tan(w)}},"asin/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.asin(w)}},"acos/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.acos(w)}},"atan/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.atan(w)}},"atan2/2":{type_args:null,type_result:!0,fn:function(w,b,y){return Math.atan2(w,b)}},"exp/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.exp(w)}},"sqrt/1":{type_args:null,type_result:!0,fn:function(w,b){return Math.sqrt(w)}},"log/1":{type_args:null,type_result:!0,fn:function(w,b){return w>0?Math.log(w):x.error.evaluation("undefined",b.__call_indicator)}},"+/2":{type_args:null,type_result:null,fn:function(w,b,y){return w+b}},"-/2":{type_args:null,type_result:null,fn:function(w,b,y){return w-b}},"*/2":{type_args:null,type_result:null,fn:function(w,b,y){return w*b}},"//2":{type_args:null,type_result:!0,fn:function(w,b,y){return b?w/b:x.error.evaluation("zero_division",y.__call_indicator)}},"///2":{type_args:!1,type_result:!1,fn:function(w,b,y){return b?parseInt(w/b):x.error.evaluation("zero_division",y.__call_indicator)}},"**/2":{type_args:null,type_result:!0,fn:function(w,b,y){return Math.pow(w,b)}},"^/2":{type_args:null,type_result:null,fn:function(w,b,y){return Math.pow(w,b)}},"<>/2":{type_args:!1,type_result:!1,fn:function(w,b,y){return w>>b}},"/\\/2":{type_args:!1,type_result:!1,fn:function(w,b,y){return w&b}},"\\//2":{type_args:!1,type_result:!1,fn:function(w,b,y){return w|b}},"xor/2":{type_args:!1,type_result:!1,fn:function(w,b,y){return w^b}},"rem/2":{type_args:!1,type_result:!1,fn:function(w,b,y){return b?w%b:x.error.evaluation("zero_division",y.__call_indicator)}},"mod/2":{type_args:!1,type_result:!1,fn:function(w,b,y){return b?w-parseInt(w/b)*b:x.error.evaluation("zero_division",y.__call_indicator)}},"max/2":{type_args:null,type_result:null,fn:function(w,b,y){return Math.max(w,b)}},"min/2":{type_args:null,type_result:null,fn:function(w,b,y){return Math.min(w,b)}}}},directive:{"dynamic/1":function(w,b){var y=b.args[0];if(x.type.is_variable(y))w.throw_error(x.error.instantiation(b.indicator));else if(!x.type.is_compound(y)||y.indicator!=="//2")w.throw_error(x.error.type("predicate_indicator",y,b.indicator));else if(x.type.is_variable(y.args[0])||x.type.is_variable(y.args[1]))w.throw_error(x.error.instantiation(b.indicator));else if(!x.type.is_atom(y.args[0]))w.throw_error(x.error.type("atom",y.args[0],b.indicator));else if(!x.type.is_integer(y.args[1]))w.throw_error(x.error.type("integer",y.args[1],b.indicator));else{var F=b.args[0].args[0].id+"/"+b.args[0].args[1].value;w.session.public_predicates[F]=!0,w.session.rules[F]||(w.session.rules[F]=[])}},"multifile/1":function(w,b){var y=b.args[0];x.type.is_variable(y)?w.throw_error(x.error.instantiation(b.indicator)):!x.type.is_compound(y)||y.indicator!=="//2"?w.throw_error(x.error.type("predicate_indicator",y,b.indicator)):x.type.is_variable(y.args[0])||x.type.is_variable(y.args[1])?w.throw_error(x.error.instantiation(b.indicator)):x.type.is_atom(y.args[0])?x.type.is_integer(y.args[1])?w.session.multifile_predicates[b.args[0].args[0].id+"/"+b.args[0].args[1].value]=!0:w.throw_error(x.error.type("integer",y.args[1],b.indicator)):w.throw_error(x.error.type("atom",y.args[0],b.indicator))},"set_prolog_flag/2":function(w,b){var y=b.args[0],F=b.args[1];x.type.is_variable(y)||x.type.is_variable(F)?w.throw_error(x.error.instantiation(b.indicator)):x.type.is_atom(y)?x.type.is_flag(y)?x.type.is_value_flag(y,F)?x.type.is_modifiable_flag(y)?w.session.flag[y.id]=F:w.throw_error(x.error.permission("modify","flag",y)):w.throw_error(x.error.domain("flag_value",new j("+",[y,F]),b.indicator)):w.throw_error(x.error.domain("prolog_flag",y,b.indicator)):w.throw_error(x.error.type("atom",y,b.indicator))},"use_module/1":function(w,b){var y=b.args[0];if(x.type.is_variable(y))w.throw_error(x.error.instantiation(b.indicator));else if(!x.type.is_term(y))w.throw_error(x.error.type("term",y,b.indicator));else if(x.type.is_module(y)){var F=y.args[0].id;e(w.session.modules,F)===-1&&w.session.modules.push(F)}},"char_conversion/2":function(w,b){var y=b.args[0],F=b.args[1];x.type.is_variable(y)||x.type.is_variable(F)?w.throw_error(x.error.instantiation(b.indicator)):x.type.is_character(y)?x.type.is_character(F)?y.id===F.id?delete w.session.__char_conversion[y.id]:w.session.__char_conversion[y.id]=F.id:w.throw_error(x.error.type("character",F,b.indicator)):w.throw_error(x.error.type("character",y,b.indicator))},"op/3":function(w,b){var y=b.args[0],F=b.args[1],z=b.args[2];if(x.type.is_variable(y)||x.type.is_variable(F)||x.type.is_variable(z))w.throw_error(x.error.instantiation(b.indicator));else if(!x.type.is_integer(y))w.throw_error(x.error.type("integer",y,b.indicator));else if(!x.type.is_atom(F))w.throw_error(x.error.type("atom",F,b.indicator));else if(!x.type.is_atom(z))w.throw_error(x.error.type("atom",z,b.indicator));else if(y.value<0||y.value>1200)w.throw_error(x.error.domain("operator_priority",y,b.indicator));else if(z.id===",")w.throw_error(x.error.permission("modify","operator",z,b.indicator));else if(z.id==="|"&&(y.value<1001||F.id.length!==3))w.throw_error(x.error.permission("modify","operator",z,b.indicator));else if(["fy","fx","yf","xf","xfx","yfx","xfy"].indexOf(F.id)===-1)w.throw_error(x.error.domain("operator_specifier",F,b.indicator));else{var Z={prefix:null,infix:null,postfix:null};for(var $ in w.session.__operators)if(w.session.__operators.hasOwnProperty($)){var oe=w.session.__operators[$][z.id];oe&&(e(oe,"fx")!==-1&&(Z.prefix={priority:$,type:"fx"}),e(oe,"fy")!==-1&&(Z.prefix={priority:$,type:"fy"}),e(oe,"xf")!==-1&&(Z.postfix={priority:$,type:"xf"}),e(oe,"yf")!==-1&&(Z.postfix={priority:$,type:"yf"}),e(oe,"xfx")!==-1&&(Z.infix={priority:$,type:"xfx"}),e(oe,"xfy")!==-1&&(Z.infix={priority:$,type:"xfy"}),e(oe,"yfx")!==-1&&(Z.infix={priority:$,type:"yfx"}))}var xe;switch(F.id){case"fy":case"fx":xe="prefix";break;case"yf":case"xf":xe="postfix";break;default:xe="infix";break}if(((Z.prefix&&xe==="prefix"||Z.postfix&&xe==="postfix"||Z.infix&&xe==="infix")&&Z[xe].type!==F.id||Z.infix&&xe==="postfix"||Z.postfix&&xe==="infix")&&y.value!==0)w.throw_error(x.error.permission("create","operator",z,b.indicator));else return Z[xe]&&(we(w.session.__operators[Z[xe].priority][z.id],F.id),w.session.__operators[Z[xe].priority][z.id].length===0&&delete w.session.__operators[Z[xe].priority][z.id]),y.value>0&&(w.session.__operators[y.value]||(w.session.__operators[y.value.toString()]={}),w.session.__operators[y.value][z.id]||(w.session.__operators[y.value][z.id]=[]),w.session.__operators[y.value][z.id].push(F.id)),!0}}},predicate:{"op/3":function(w,b,y){x.directive["op/3"](w,y)&&w.success(b)},"current_op/3":function(w,b,y){var F=y.args[0],z=y.args[1],Z=y.args[2],$=[];for(var oe in w.session.__operators)for(var xe in w.session.__operators[oe])for(var Te=0;Te/2"){var F=w.points,z=w.session.format_success,Z=w.session.format_error;w.session.format_success=function(Te){return Te.substitution},w.session.format_error=function(Te){return Te.goal},w.points=[new Pe(y.args[0].args[0],b.substitution,b)];var $=function(Te){w.points=F,w.session.format_success=z,w.session.format_error=Z,Te===!1?w.prepend([new Pe(b.goal.replace(y.args[1]),b.substitution,b)]):x.type.is_error(Te)?w.throw_error(Te.args[0]):Te===null?(w.prepend([b]),w.__calls.shift()(null)):w.prepend([new Pe(b.goal.replace(y.args[0].args[1]).apply(Te),b.substitution.apply(Te),b)])};w.__calls.unshift($)}else{var oe=new Pe(b.goal.replace(y.args[0]),b.substitution,b),xe=new Pe(b.goal.replace(y.args[1]),b.substitution,b);w.prepend([oe,xe])}},"!/0":function(w,b,y){var F,z,Z=[];for(F=b,z=null;F.parent!==null&&F.parent.goal.search(y);)if(z=F,F=F.parent,F.goal!==null){var $=F.goal.select();if($&&$.id==="call"&&$.search(y)){F=z;break}}for(var oe=w.points.length-1;oe>=0;oe--){for(var xe=w.points[oe],Te=xe.parent;Te!==null&&Te!==F.parent;)Te=Te.parent;Te===null&&Te!==F.parent&&Z.push(xe)}w.points=Z.reverse(),w.success(b)},"\\+/1":function(w,b,y){var F=y.args[0];x.type.is_variable(F)?w.throw_error(x.error.instantiation(w.level)):x.type.is_callable(F)?w.prepend([new Pe(b.goal.replace(new j(",",[new j(",",[new j("call",[F]),new j("!",[])]),new j("fail",[])])),b.substitution,b),new Pe(b.goal.replace(null),b.substitution,b)]):w.throw_error(x.error.type("callable",F,w.level))},"->/2":function(w,b,y){var F=b.goal.replace(new j(",",[y.args[0],new j(",",[new j("!"),y.args[1]])]));w.prepend([new Pe(F,b.substitution,b)])},"fail/0":function(w,b,y){},"false/0":function(w,b,y){},"true/0":function(w,b,y){w.success(b)},"call/1":se(1),"call/2":se(2),"call/3":se(3),"call/4":se(4),"call/5":se(5),"call/6":se(6),"call/7":se(7),"call/8":se(8),"once/1":function(w,b,y){var F=y.args[0];w.prepend([new Pe(b.goal.replace(new j(",",[new j("call",[F]),new j("!",[])])),b.substitution,b)])},"forall/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new Pe(b.goal.replace(new j("\\+",[new j(",",[new j("call",[F]),new j("\\+",[new j("call",[z])])])])),b.substitution,b)])},"repeat/0":function(w,b,y){w.prepend([new Pe(b.goal.replace(null),b.substitution,b),b])},"throw/1":function(w,b,y){x.type.is_variable(y.args[0])?w.throw_error(x.error.instantiation(w.level)):w.throw_error(y.args[0])},"catch/3":function(w,b,y){var F=w.points;w.points=[],w.prepend([new Pe(y.args[0],b.substitution,b)]);var z=w.session.format_success,Z=w.session.format_error;w.session.format_success=function(oe){return oe.substitution},w.session.format_error=function(oe){return oe.goal};var $=function(oe){var xe=w.points;if(w.points=F,w.session.format_success=z,w.session.format_error=Z,x.type.is_error(oe)){for(var Te=[],lt=w.points.length-1;lt>=0;lt--){for(var ir=w.points[lt],It=ir.parent;It!==null&&It!==b.parent;)It=It.parent;It===null&&It!==b.parent&&Te.push(ir)}w.points=Te;var qt=w.get_flag("occurs_check").indicator==="true/0",ir=new Pe,Pt=x.unify(oe.args[0],y.args[1],qt);Pt!==null?(ir.substitution=b.substitution.apply(Pt),ir.goal=b.goal.replace(y.args[2]).apply(Pt),ir.parent=b,w.prepend([ir])):w.throw_error(oe.args[0])}else if(oe!==!1){for(var gn=oe===null?[]:[new Pe(b.goal.apply(oe).replace(null),b.substitution.apply(oe),b)],Pr=[],lt=xe.length-1;lt>=0;lt--){Pr.push(xe[lt]);var Ir=xe[lt].goal!==null?xe[lt].goal.select():null;if(x.type.is_term(Ir)&&Ir.indicator==="!/0")break}var Nr=s(Pr,function(nn){return nn.goal===null&&(nn.goal=new j("true",[])),nn=new Pe(b.goal.replace(new j("catch",[nn.goal,y.args[1],y.args[2]])),b.substitution.apply(nn.substitution),nn.parent),nn.exclude=y.args[0].variables(),nn}).reverse();w.prepend(Nr),w.prepend(gn),oe===null&&(this.current_limit=0,w.__calls.shift()(null))}};w.__calls.unshift($)},"=/2":function(w,b,y){var F=w.get_flag("occurs_check").indicator==="true/0",z=new Pe,Z=x.unify(y.args[0],y.args[1],F);Z!==null&&(z.goal=b.goal.apply(Z).replace(null),z.substitution=b.substitution.apply(Z),z.parent=b,w.prepend([z]))},"unify_with_occurs_check/2":function(w,b,y){var F=new Pe,z=x.unify(y.args[0],y.args[1],!0);z!==null&&(F.goal=b.goal.apply(z).replace(null),F.substitution=b.substitution.apply(z),F.parent=b,w.prepend([F]))},"\\=/2":function(w,b,y){var F=w.get_flag("occurs_check").indicator==="true/0",z=x.unify(y.args[0],y.args[1],F);z===null&&w.success(b)},"subsumes_term/2":function(w,b,y){var F=w.get_flag("occurs_check").indicator==="true/0",z=x.unify(y.args[1],y.args[0],F);z!==null&&y.args[1].apply(z).equals(y.args[1])&&w.success(b)},"findall/3":function(w,b,y){var F=y.args[0],z=y.args[1],Z=y.args[2];if(x.type.is_variable(z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_callable(z))w.throw_error(x.error.type("callable",z,y.indicator));else if(!x.type.is_variable(Z)&&!x.type.is_list(Z))w.throw_error(x.error.type("list",Z,y.indicator));else{var $=w.next_free_variable(),oe=new j(",",[z,new j("=",[$,F])]),xe=w.points,Te=w.session.limit,lt=w.session.format_success;w.session.format_success=function(ir){return ir.substitution},w.add_goal(oe,!0,b);var It=[],qt=function(ir){if(ir!==!1&&ir!==null&&!x.type.is_error(ir))w.__calls.unshift(qt),It.push(ir.links[$.id]),w.session.limit=w.current_limit;else if(w.points=xe,w.session.limit=Te,w.session.format_success=lt,x.type.is_error(ir))w.throw_error(ir.args[0]);else if(w.current_limit>0){for(var Pt=new j("[]"),gn=It.length-1;gn>=0;gn--)Pt=new j(".",[It[gn],Pt]);w.prepend([new Pe(b.goal.replace(new j("=",[Z,Pt])),b.substitution,b)])}};w.__calls.unshift(qt)}},"bagof/3":function(w,b,y){var F,z=y.args[0],Z=y.args[1],$=y.args[2];if(x.type.is_variable(Z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_callable(Z))w.throw_error(x.error.type("callable",Z,y.indicator));else if(!x.type.is_variable($)&&!x.type.is_list($))w.throw_error(x.error.type("list",$,y.indicator));else{var oe=w.next_free_variable(),xe;Z.indicator==="^/2"?(xe=Z.args[0].variables(),Z=Z.args[1]):xe=[],xe=xe.concat(z.variables());for(var Te=Z.variables().filter(function(Nr){return e(xe,Nr)===-1}),lt=new j("[]"),It=Te.length-1;It>=0;It--)lt=new j(".",[new De(Te[It]),lt]);var qt=new j(",",[Z,new j("=",[oe,new j(",",[lt,z])])]),ir=w.points,Pt=w.session.limit,gn=w.session.format_success;w.session.format_success=function(Nr){return Nr.substitution},w.add_goal(qt,!0,b);var Pr=[],Ir=function(Nr){if(Nr!==!1&&Nr!==null&&!x.type.is_error(Nr)){w.__calls.unshift(Ir);var nn=!1,ai=Nr.links[oe.id].args[0],wo=Nr.links[oe.id].args[1];for(var ns in Pr)if(Pr.hasOwnProperty(ns)){var to=Pr[ns];if(to.variables.equals(ai)){to.answers.push(wo),nn=!0;break}}nn||Pr.push({variables:ai,answers:[wo]}),w.session.limit=w.current_limit}else if(w.points=ir,w.session.limit=Pt,w.session.format_success=gn,x.type.is_error(Nr))w.throw_error(Nr.args[0]);else if(w.current_limit>0){for(var Bo=[],ji=0;ji=0;vo--)ro=new j(".",[Nr[vo],ro]);Bo.push(new Pe(b.goal.replace(new j(",",[new j("=",[lt,Pr[ji].variables]),new j("=",[$,ro])])),b.substitution,b))}w.prepend(Bo)}};w.__calls.unshift(Ir)}},"setof/3":function(w,b,y){var F,z=y.args[0],Z=y.args[1],$=y.args[2];if(x.type.is_variable(Z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_callable(Z))w.throw_error(x.error.type("callable",Z,y.indicator));else if(!x.type.is_variable($)&&!x.type.is_list($))w.throw_error(x.error.type("list",$,y.indicator));else{var oe=w.next_free_variable(),xe;Z.indicator==="^/2"?(xe=Z.args[0].variables(),Z=Z.args[1]):xe=[],xe=xe.concat(z.variables());for(var Te=Z.variables().filter(function(Nr){return e(xe,Nr)===-1}),lt=new j("[]"),It=Te.length-1;It>=0;It--)lt=new j(".",[new De(Te[It]),lt]);var qt=new j(",",[Z,new j("=",[oe,new j(",",[lt,z])])]),ir=w.points,Pt=w.session.limit,gn=w.session.format_success;w.session.format_success=function(Nr){return Nr.substitution},w.add_goal(qt,!0,b);var Pr=[],Ir=function(Nr){if(Nr!==!1&&Nr!==null&&!x.type.is_error(Nr)){w.__calls.unshift(Ir);var nn=!1,ai=Nr.links[oe.id].args[0],wo=Nr.links[oe.id].args[1];for(var ns in Pr)if(Pr.hasOwnProperty(ns)){var to=Pr[ns];if(to.variables.equals(ai)){to.answers.push(wo),nn=!0;break}}nn||Pr.push({variables:ai,answers:[wo]}),w.session.limit=w.current_limit}else if(w.points=ir,w.session.limit=Pt,w.session.format_success=gn,x.type.is_error(Nr))w.throw_error(Nr.args[0]);else if(w.current_limit>0){for(var Bo=[],ji=0;ji=0;vo--)ro=new j(".",[Nr[vo],ro]);Bo.push(new Pe(b.goal.replace(new j(",",[new j("=",[lt,Pr[ji].variables]),new j("=",[$,ro])])),b.substitution,b))}w.prepend(Bo)}};w.__calls.unshift(Ir)}},"functor/3":function(w,b,y){var F,z=y.args[0],Z=y.args[1],$=y.args[2];if(x.type.is_variable(z)&&(x.type.is_variable(Z)||x.type.is_variable($)))w.throw_error(x.error.instantiation("functor/3"));else if(!x.type.is_variable($)&&!x.type.is_integer($))w.throw_error(x.error.type("integer",y.args[2],"functor/3"));else if(!x.type.is_variable(Z)&&!x.type.is_atomic(Z))w.throw_error(x.error.type("atomic",y.args[1],"functor/3"));else if(x.type.is_integer(Z)&&x.type.is_integer($)&&$.value!==0)w.throw_error(x.error.type("atom",y.args[1],"functor/3"));else if(x.type.is_variable(z)){if(y.args[2].value>=0){for(var oe=[],xe=0;xe<$.value;xe++)oe.push(w.next_free_variable());var Te=x.type.is_integer(Z)?Z:new j(Z.id,oe);w.prepend([new Pe(b.goal.replace(new j("=",[z,Te])),b.substitution,b)])}}else{var lt=x.type.is_integer(z)?z:new j(z.id,[]),It=x.type.is_integer(z)?new Re(0,!1):new Re(z.args.length,!1),qt=new j(",",[new j("=",[lt,Z]),new j("=",[It,$])]);w.prepend([new Pe(b.goal.replace(qt),b.substitution,b)])}},"arg/3":function(w,b,y){if(x.type.is_variable(y.args[0])||x.type.is_variable(y.args[1]))w.throw_error(x.error.instantiation(y.indicator));else if(y.args[0].value<0)w.throw_error(x.error.domain("not_less_than_zero",y.args[0],y.indicator));else if(!x.type.is_compound(y.args[1]))w.throw_error(x.error.type("compound",y.args[1],y.indicator));else{var F=y.args[0].value;if(F>0&&F<=y.args[1].args.length){var z=new j("=",[y.args[1].args[F-1],y.args[2]]);w.prepend([new Pe(b.goal.replace(z),b.substitution,b)])}}},"=../2":function(w,b,y){var F;if(x.type.is_variable(y.args[0])&&(x.type.is_variable(y.args[1])||x.type.is_non_empty_list(y.args[1])&&x.type.is_variable(y.args[1].args[0])))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_fully_list(y.args[1]))w.throw_error(x.error.type("list",y.args[1],y.indicator));else if(x.type.is_variable(y.args[0])){if(!x.type.is_variable(y.args[1])){var Z=[];for(F=y.args[1].args[1];F.indicator==="./2";)Z.push(F.args[0]),F=F.args[1];x.type.is_variable(y.args[0])&&x.type.is_variable(F)?w.throw_error(x.error.instantiation(y.indicator)):Z.length===0&&x.type.is_compound(y.args[1].args[0])?w.throw_error(x.error.type("atomic",y.args[1].args[0],y.indicator)):Z.length>0&&(x.type.is_compound(y.args[1].args[0])||x.type.is_number(y.args[1].args[0]))?w.throw_error(x.error.type("atom",y.args[1].args[0],y.indicator)):Z.length===0?w.prepend([new Pe(b.goal.replace(new j("=",[y.args[1].args[0],y.args[0]],b)),b.substitution,b)]):w.prepend([new Pe(b.goal.replace(new j("=",[new j(y.args[1].args[0].id,Z),y.args[0]])),b.substitution,b)])}}else{if(x.type.is_atomic(y.args[0]))F=new j(".",[y.args[0],new j("[]")]);else{F=new j("[]");for(var z=y.args[0].args.length-1;z>=0;z--)F=new j(".",[y.args[0].args[z],F]);F=new j(".",[new j(y.args[0].id),F])}w.prepend([new Pe(b.goal.replace(new j("=",[F,y.args[1]])),b.substitution,b)])}},"copy_term/2":function(w,b,y){var F=y.args[0].rename(w);w.prepend([new Pe(b.goal.replace(new j("=",[F,y.args[1]])),b.substitution,b.parent)])},"term_variables/2":function(w,b,y){var F=y.args[0],z=y.args[1];if(!x.type.is_fully_list(z))w.throw_error(x.error.type("list",z,y.indicator));else{var Z=g(s(ye(F.variables()),function($){return new De($)}));w.prepend([new Pe(b.goal.replace(new j("=",[z,Z])),b.substitution,b)])}},"clause/2":function(w,b,y){if(x.type.is_variable(y.args[0]))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_callable(y.args[0]))w.throw_error(x.error.type("callable",y.args[0],y.indicator));else if(!x.type.is_variable(y.args[1])&&!x.type.is_callable(y.args[1]))w.throw_error(x.error.type("callable",y.args[1],y.indicator));else if(w.session.rules[y.args[0].indicator]!==void 0)if(w.is_public_predicate(y.args[0].indicator)){var F=[];for(var z in w.session.rules[y.args[0].indicator])if(w.session.rules[y.args[0].indicator].hasOwnProperty(z)){var Z=w.session.rules[y.args[0].indicator][z];w.session.renamed_variables={},Z=Z.rename(w),Z.body===null&&(Z.body=new j("true"));var $=new j(",",[new j("=",[Z.head,y.args[0]]),new j("=",[Z.body,y.args[1]])]);F.push(new Pe(b.goal.replace($),b.substitution,b))}w.prepend(F)}else w.throw_error(x.error.permission("access","private_procedure",y.args[0].indicator,y.indicator))},"current_predicate/1":function(w,b,y){var F=y.args[0];if(!x.type.is_variable(F)&&(!x.type.is_compound(F)||F.indicator!=="//2"))w.throw_error(x.error.type("predicate_indicator",F,y.indicator));else if(!x.type.is_variable(F)&&!x.type.is_variable(F.args[0])&&!x.type.is_atom(F.args[0]))w.throw_error(x.error.type("atom",F.args[0],y.indicator));else if(!x.type.is_variable(F)&&!x.type.is_variable(F.args[1])&&!x.type.is_integer(F.args[1]))w.throw_error(x.error.type("integer",F.args[1],y.indicator));else{var z=[];for(var Z in w.session.rules)if(w.session.rules.hasOwnProperty(Z)){var $=Z.lastIndexOf("/"),oe=Z.substr(0,$),xe=parseInt(Z.substr($+1,Z.length-($+1))),Te=new j("/",[new j(oe),new Re(xe,!1)]),lt=new j("=",[Te,F]);z.push(new Pe(b.goal.replace(lt),b.substitution,b))}w.prepend(z)}},"asserta/1":function(w,b,y){if(x.type.is_variable(y.args[0]))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_callable(y.args[0]))w.throw_error(x.error.type("callable",y.args[0],y.indicator));else{var F,z;y.args[0].indicator===":-/2"?(F=y.args[0].args[0],z=Ce(y.args[0].args[1])):(F=y.args[0],z=null),x.type.is_callable(F)?z!==null&&!x.type.is_callable(z)?w.throw_error(x.error.type("callable",z,y.indicator)):w.is_public_predicate(F.indicator)?(w.session.rules[F.indicator]===void 0&&(w.session.rules[F.indicator]=[]),w.session.public_predicates[F.indicator]=!0,w.session.rules[F.indicator]=[new Ye(F,z,!0)].concat(w.session.rules[F.indicator]),w.success(b)):w.throw_error(x.error.permission("modify","static_procedure",F.indicator,y.indicator)):w.throw_error(x.error.type("callable",F,y.indicator))}},"assertz/1":function(w,b,y){if(x.type.is_variable(y.args[0]))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_callable(y.args[0]))w.throw_error(x.error.type("callable",y.args[0],y.indicator));else{var F,z;y.args[0].indicator===":-/2"?(F=y.args[0].args[0],z=Ce(y.args[0].args[1])):(F=y.args[0],z=null),x.type.is_callable(F)?z!==null&&!x.type.is_callable(z)?w.throw_error(x.error.type("callable",z,y.indicator)):w.is_public_predicate(F.indicator)?(w.session.rules[F.indicator]===void 0&&(w.session.rules[F.indicator]=[]),w.session.public_predicates[F.indicator]=!0,w.session.rules[F.indicator].push(new Ye(F,z,!0)),w.success(b)):w.throw_error(x.error.permission("modify","static_procedure",F.indicator,y.indicator)):w.throw_error(x.error.type("callable",F,y.indicator))}},"retract/1":function(w,b,y){if(x.type.is_variable(y.args[0]))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_callable(y.args[0]))w.throw_error(x.error.type("callable",y.args[0],y.indicator));else{var F,z;if(y.args[0].indicator===":-/2"?(F=y.args[0].args[0],z=y.args[0].args[1]):(F=y.args[0],z=new j("true")),typeof b.retract>"u")if(w.is_public_predicate(F.indicator)){if(w.session.rules[F.indicator]!==void 0){for(var Z=[],$=0;$w.get_flag("max_arity").value)w.throw_error(x.error.representation("max_arity",y.indicator));else{var F=y.args[0].args[0].id+"/"+y.args[0].args[1].value;w.is_public_predicate(F)?(delete w.session.rules[F],w.success(b)):w.throw_error(x.error.permission("modify","static_procedure",F,y.indicator))}},"atom_length/2":function(w,b,y){if(x.type.is_variable(y.args[0]))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_atom(y.args[0]))w.throw_error(x.error.type("atom",y.args[0],y.indicator));else if(!x.type.is_variable(y.args[1])&&!x.type.is_integer(y.args[1]))w.throw_error(x.error.type("integer",y.args[1],y.indicator));else if(x.type.is_integer(y.args[1])&&y.args[1].value<0)w.throw_error(x.error.domain("not_less_than_zero",y.args[1],y.indicator));else{var F=new Re(y.args[0].id.length,!1);w.prepend([new Pe(b.goal.replace(new j("=",[F,y.args[1]])),b.substitution,b)])}},"atom_concat/3":function(w,b,y){var F,z,Z=y.args[0],$=y.args[1],oe=y.args[2];if(x.type.is_variable(oe)&&(x.type.is_variable(Z)||x.type.is_variable($)))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_variable(Z)&&!x.type.is_atom(Z))w.throw_error(x.error.type("atom",Z,y.indicator));else if(!x.type.is_variable($)&&!x.type.is_atom($))w.throw_error(x.error.type("atom",$,y.indicator));else if(!x.type.is_variable(oe)&&!x.type.is_atom(oe))w.throw_error(x.error.type("atom",oe,y.indicator));else{var xe=x.type.is_variable(Z),Te=x.type.is_variable($);if(!xe&&!Te)z=new j("=",[oe,new j(Z.id+$.id)]),w.prepend([new Pe(b.goal.replace(z),b.substitution,b)]);else if(xe&&!Te)F=oe.id.substr(0,oe.id.length-$.id.length),F+$.id===oe.id&&(z=new j("=",[Z,new j(F)]),w.prepend([new Pe(b.goal.replace(z),b.substitution,b)]));else if(Te&&!xe)F=oe.id.substr(Z.id.length),Z.id+F===oe.id&&(z=new j("=",[$,new j(F)]),w.prepend([new Pe(b.goal.replace(z),b.substitution,b)]));else{for(var lt=[],It=0;It<=oe.id.length;It++){var qt=new j(oe.id.substr(0,It)),ir=new j(oe.id.substr(It));z=new j(",",[new j("=",[qt,Z]),new j("=",[ir,$])]),lt.push(new Pe(b.goal.replace(z),b.substitution,b))}w.prepend(lt)}}},"sub_atom/5":function(w,b,y){var F,z=y.args[0],Z=y.args[1],$=y.args[2],oe=y.args[3],xe=y.args[4];if(x.type.is_variable(z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_variable(Z)&&!x.type.is_integer(Z))w.throw_error(x.error.type("integer",Z,y.indicator));else if(!x.type.is_variable($)&&!x.type.is_integer($))w.throw_error(x.error.type("integer",$,y.indicator));else if(!x.type.is_variable(oe)&&!x.type.is_integer(oe))w.throw_error(x.error.type("integer",oe,y.indicator));else if(x.type.is_integer(Z)&&Z.value<0)w.throw_error(x.error.domain("not_less_than_zero",Z,y.indicator));else if(x.type.is_integer($)&&$.value<0)w.throw_error(x.error.domain("not_less_than_zero",$,y.indicator));else if(x.type.is_integer(oe)&&oe.value<0)w.throw_error(x.error.domain("not_less_than_zero",oe,y.indicator));else{var Te=[],lt=[],It=[];if(x.type.is_variable(Z))for(F=0;F<=z.id.length;F++)Te.push(F);else Te.push(Z.value);if(x.type.is_variable($))for(F=0;F<=z.id.length;F++)lt.push(F);else lt.push($.value);if(x.type.is_variable(oe))for(F=0;F<=z.id.length;F++)It.push(F);else It.push(oe.value);var qt=[];for(var ir in Te)if(Te.hasOwnProperty(ir)){F=Te[ir];for(var Pt in lt)if(lt.hasOwnProperty(Pt)){var gn=lt[Pt],Pr=z.id.length-F-gn;if(e(It,Pr)!==-1&&F+gn+Pr===z.id.length){var Ir=z.id.substr(F,gn);if(z.id===z.id.substr(0,F)+Ir+z.id.substr(F+gn,Pr)){var Nr=new j("=",[new j(Ir),xe]),nn=new j("=",[Z,new Re(F)]),ai=new j("=",[$,new Re(gn)]),wo=new j("=",[oe,new Re(Pr)]),ns=new j(",",[new j(",",[new j(",",[nn,ai]),wo]),Nr]);qt.push(new Pe(b.goal.replace(ns),b.substitution,b))}}}}w.prepend(qt)}},"atom_chars/2":function(w,b,y){var F=y.args[0],z=y.args[1];if(x.type.is_variable(F)&&x.type.is_variable(z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_variable(F)&&!x.type.is_atom(F))w.throw_error(x.error.type("atom",F,y.indicator));else if(x.type.is_variable(F)){for(var oe=z,xe=x.type.is_variable(F),Te="";oe.indicator==="./2";){if(x.type.is_character(oe.args[0]))Te+=oe.args[0].id;else if(x.type.is_variable(oe.args[0])&&xe){w.throw_error(x.error.instantiation(y.indicator));return}else if(!x.type.is_variable(oe.args[0])){w.throw_error(x.error.type("character",oe.args[0],y.indicator));return}oe=oe.args[1]}x.type.is_variable(oe)&&xe?w.throw_error(x.error.instantiation(y.indicator)):!x.type.is_empty_list(oe)&&!x.type.is_variable(oe)?w.throw_error(x.error.type("list",z,y.indicator)):w.prepend([new Pe(b.goal.replace(new j("=",[new j(Te),F])),b.substitution,b)])}else{for(var Z=new j("[]"),$=F.id.length-1;$>=0;$--)Z=new j(".",[new j(F.id.charAt($)),Z]);w.prepend([new Pe(b.goal.replace(new j("=",[z,Z])),b.substitution,b)])}},"atom_codes/2":function(w,b,y){var F=y.args[0],z=y.args[1];if(x.type.is_variable(F)&&x.type.is_variable(z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_variable(F)&&!x.type.is_atom(F))w.throw_error(x.error.type("atom",F,y.indicator));else if(x.type.is_variable(F)){for(var oe=z,xe=x.type.is_variable(F),Te="";oe.indicator==="./2";){if(x.type.is_character_code(oe.args[0]))Te+=c(oe.args[0].value);else if(x.type.is_variable(oe.args[0])&&xe){w.throw_error(x.error.instantiation(y.indicator));return}else if(!x.type.is_variable(oe.args[0])){w.throw_error(x.error.representation("character_code",y.indicator));return}oe=oe.args[1]}x.type.is_variable(oe)&&xe?w.throw_error(x.error.instantiation(y.indicator)):!x.type.is_empty_list(oe)&&!x.type.is_variable(oe)?w.throw_error(x.error.type("list",z,y.indicator)):w.prepend([new Pe(b.goal.replace(new j("=",[new j(Te),F])),b.substitution,b)])}else{for(var Z=new j("[]"),$=F.id.length-1;$>=0;$--)Z=new j(".",[new Re(n(F.id,$),!1),Z]);w.prepend([new Pe(b.goal.replace(new j("=",[z,Z])),b.substitution,b)])}},"char_code/2":function(w,b,y){var F=y.args[0],z=y.args[1];if(x.type.is_variable(F)&&x.type.is_variable(z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_variable(F)&&!x.type.is_character(F))w.throw_error(x.error.type("character",F,y.indicator));else if(!x.type.is_variable(z)&&!x.type.is_integer(z))w.throw_error(x.error.type("integer",z,y.indicator));else if(!x.type.is_variable(z)&&!x.type.is_character_code(z))w.throw_error(x.error.representation("character_code",y.indicator));else if(x.type.is_variable(z)){var Z=new Re(n(F.id,0),!1);w.prepend([new Pe(b.goal.replace(new j("=",[Z,z])),b.substitution,b)])}else{var $=new j(c(z.value));w.prepend([new Pe(b.goal.replace(new j("=",[$,F])),b.substitution,b)])}},"number_chars/2":function(w,b,y){var F,z=y.args[0],Z=y.args[1];if(x.type.is_variable(z)&&x.type.is_variable(Z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_variable(z)&&!x.type.is_number(z))w.throw_error(x.error.type("number",z,y.indicator));else if(!x.type.is_variable(Z)&&!x.type.is_list(Z))w.throw_error(x.error.type("list",Z,y.indicator));else{var $=x.type.is_variable(z);if(!x.type.is_variable(Z)){var oe=Z,xe=!0;for(F="";oe.indicator==="./2";){if(x.type.is_character(oe.args[0]))F+=oe.args[0].id;else if(x.type.is_variable(oe.args[0]))xe=!1;else if(!x.type.is_variable(oe.args[0])){w.throw_error(x.error.type("character",oe.args[0],y.indicator));return}oe=oe.args[1]}if(xe=xe&&x.type.is_empty_list(oe),!x.type.is_empty_list(oe)&&!x.type.is_variable(oe)){w.throw_error(x.error.type("list",Z,y.indicator));return}if(!xe&&$){w.throw_error(x.error.instantiation(y.indicator));return}else if(xe)if(x.type.is_variable(oe)&&$){w.throw_error(x.error.instantiation(y.indicator));return}else{var Te=w.parse(F),lt=Te.value;!x.type.is_number(lt)||Te.tokens[Te.tokens.length-1].space?w.throw_error(x.error.syntax_by_predicate("parseable_number",y.indicator)):w.prepend([new Pe(b.goal.replace(new j("=",[z,lt])),b.substitution,b)]);return}}if(!$){F=z.toString();for(var It=new j("[]"),qt=F.length-1;qt>=0;qt--)It=new j(".",[new j(F.charAt(qt)),It]);w.prepend([new Pe(b.goal.replace(new j("=",[Z,It])),b.substitution,b)])}}},"number_codes/2":function(w,b,y){var F,z=y.args[0],Z=y.args[1];if(x.type.is_variable(z)&&x.type.is_variable(Z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_variable(z)&&!x.type.is_number(z))w.throw_error(x.error.type("number",z,y.indicator));else if(!x.type.is_variable(Z)&&!x.type.is_list(Z))w.throw_error(x.error.type("list",Z,y.indicator));else{var $=x.type.is_variable(z);if(!x.type.is_variable(Z)){var oe=Z,xe=!0;for(F="";oe.indicator==="./2";){if(x.type.is_character_code(oe.args[0]))F+=c(oe.args[0].value);else if(x.type.is_variable(oe.args[0]))xe=!1;else if(!x.type.is_variable(oe.args[0])){w.throw_error(x.error.type("character_code",oe.args[0],y.indicator));return}oe=oe.args[1]}if(xe=xe&&x.type.is_empty_list(oe),!x.type.is_empty_list(oe)&&!x.type.is_variable(oe)){w.throw_error(x.error.type("list",Z,y.indicator));return}if(!xe&&$){w.throw_error(x.error.instantiation(y.indicator));return}else if(xe)if(x.type.is_variable(oe)&&$){w.throw_error(x.error.instantiation(y.indicator));return}else{var Te=w.parse(F),lt=Te.value;!x.type.is_number(lt)||Te.tokens[Te.tokens.length-1].space?w.throw_error(x.error.syntax_by_predicate("parseable_number",y.indicator)):w.prepend([new Pe(b.goal.replace(new j("=",[z,lt])),b.substitution,b)]);return}}if(!$){F=z.toString();for(var It=new j("[]"),qt=F.length-1;qt>=0;qt--)It=new j(".",[new Re(n(F,qt),!1),It]);w.prepend([new Pe(b.goal.replace(new j("=",[Z,It])),b.substitution,b)])}}},"upcase_atom/2":function(w,b,y){var F=y.args[0],z=y.args[1];x.type.is_variable(F)?w.throw_error(x.error.instantiation(y.indicator)):x.type.is_atom(F)?!x.type.is_variable(z)&&!x.type.is_atom(z)?w.throw_error(x.error.type("atom",z,y.indicator)):w.prepend([new Pe(b.goal.replace(new j("=",[z,new j(F.id.toUpperCase(),[])])),b.substitution,b)]):w.throw_error(x.error.type("atom",F,y.indicator))},"downcase_atom/2":function(w,b,y){var F=y.args[0],z=y.args[1];x.type.is_variable(F)?w.throw_error(x.error.instantiation(y.indicator)):x.type.is_atom(F)?!x.type.is_variable(z)&&!x.type.is_atom(z)?w.throw_error(x.error.type("atom",z,y.indicator)):w.prepend([new Pe(b.goal.replace(new j("=",[z,new j(F.id.toLowerCase(),[])])),b.substitution,b)]):w.throw_error(x.error.type("atom",F,y.indicator))},"atomic_list_concat/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new Pe(b.goal.replace(new j("atomic_list_concat",[F,new j("",[]),z])),b.substitution,b)])},"atomic_list_concat/3":function(w,b,y){var F=y.args[0],z=y.args[1],Z=y.args[2];if(x.type.is_variable(z)||x.type.is_variable(F)&&x.type.is_variable(Z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_variable(F)&&!x.type.is_list(F))w.throw_error(x.error.type("list",F,y.indicator));else if(!x.type.is_variable(Z)&&!x.type.is_atom(Z))w.throw_error(x.error.type("atom",Z,y.indicator));else if(x.type.is_variable(Z)){for(var oe="",xe=F;x.type.is_term(xe)&&xe.indicator==="./2";){if(!x.type.is_atom(xe.args[0])&&!x.type.is_number(xe.args[0])){w.throw_error(x.error.type("atomic",xe.args[0],y.indicator));return}oe!==""&&(oe+=z.id),x.type.is_atom(xe.args[0])?oe+=xe.args[0].id:oe+=""+xe.args[0].value,xe=xe.args[1]}oe=new j(oe,[]),x.type.is_variable(xe)?w.throw_error(x.error.instantiation(y.indicator)):!x.type.is_term(xe)||xe.indicator!=="[]/0"?w.throw_error(x.error.type("list",F,y.indicator)):w.prepend([new Pe(b.goal.replace(new j("=",[oe,Z])),b.substitution,b)])}else{var $=g(s(Z.id.split(z.id),function(Te){return new j(Te,[])}));w.prepend([new Pe(b.goal.replace(new j("=",[$,F])),b.substitution,b)])}},"@=/2":function(w,b,y){x.compare(y.args[0],y.args[1])>0&&w.success(b)},"@>=/2":function(w,b,y){x.compare(y.args[0],y.args[1])>=0&&w.success(b)},"compare/3":function(w,b,y){var F=y.args[0],z=y.args[1],Z=y.args[2];if(!x.type.is_variable(F)&&!x.type.is_atom(F))w.throw_error(x.error.type("atom",F,y.indicator));else if(x.type.is_atom(F)&&["<",">","="].indexOf(F.id)===-1)w.throw_error(x.type.domain("order",F,y.indicator));else{var $=x.compare(z,Z);$=$===0?"=":$===-1?"<":">",w.prepend([new Pe(b.goal.replace(new j("=",[F,new j($,[])])),b.substitution,b)])}},"is/2":function(w,b,y){var F=y.args[1].interpret(w);x.type.is_number(F)?w.prepend([new Pe(b.goal.replace(new j("=",[y.args[0],F],w.level)),b.substitution,b)]):w.throw_error(F)},"between/3":function(w,b,y){var F=y.args[0],z=y.args[1],Z=y.args[2];if(x.type.is_variable(F)||x.type.is_variable(z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_integer(F))w.throw_error(x.error.type("integer",F,y.indicator));else if(!x.type.is_integer(z))w.throw_error(x.error.type("integer",z,y.indicator));else if(!x.type.is_variable(Z)&&!x.type.is_integer(Z))w.throw_error(x.error.type("integer",Z,y.indicator));else if(x.type.is_variable(Z)){var $=[new Pe(b.goal.replace(new j("=",[Z,F])),b.substitution,b)];F.value=Z.value&&w.success(b)},"succ/2":function(w,b,y){var F=y.args[0],z=y.args[1];x.type.is_variable(F)&&x.type.is_variable(z)?w.throw_error(x.error.instantiation(y.indicator)):!x.type.is_variable(F)&&!x.type.is_integer(F)?w.throw_error(x.error.type("integer",F,y.indicator)):!x.type.is_variable(z)&&!x.type.is_integer(z)?w.throw_error(x.error.type("integer",z,y.indicator)):!x.type.is_variable(F)&&F.value<0?w.throw_error(x.error.domain("not_less_than_zero",F,y.indicator)):!x.type.is_variable(z)&&z.value<0?w.throw_error(x.error.domain("not_less_than_zero",z,y.indicator)):(x.type.is_variable(z)||z.value>0)&&(x.type.is_variable(F)?w.prepend([new Pe(b.goal.replace(new j("=",[F,new Re(z.value-1,!1)])),b.substitution,b)]):w.prepend([new Pe(b.goal.replace(new j("=",[z,new Re(F.value+1,!1)])),b.substitution,b)]))},"=:=/2":function(w,b,y){var F=x.arithmetic_compare(w,y.args[0],y.args[1]);x.type.is_term(F)?w.throw_error(F):F===0&&w.success(b)},"=\\=/2":function(w,b,y){var F=x.arithmetic_compare(w,y.args[0],y.args[1]);x.type.is_term(F)?w.throw_error(F):F!==0&&w.success(b)},"/2":function(w,b,y){var F=x.arithmetic_compare(w,y.args[0],y.args[1]);x.type.is_term(F)?w.throw_error(F):F>0&&w.success(b)},">=/2":function(w,b,y){var F=x.arithmetic_compare(w,y.args[0],y.args[1]);x.type.is_term(F)?w.throw_error(F):F>=0&&w.success(b)},"var/1":function(w,b,y){x.type.is_variable(y.args[0])&&w.success(b)},"atom/1":function(w,b,y){x.type.is_atom(y.args[0])&&w.success(b)},"atomic/1":function(w,b,y){x.type.is_atomic(y.args[0])&&w.success(b)},"compound/1":function(w,b,y){x.type.is_compound(y.args[0])&&w.success(b)},"integer/1":function(w,b,y){x.type.is_integer(y.args[0])&&w.success(b)},"float/1":function(w,b,y){x.type.is_float(y.args[0])&&w.success(b)},"number/1":function(w,b,y){x.type.is_number(y.args[0])&&w.success(b)},"nonvar/1":function(w,b,y){x.type.is_variable(y.args[0])||w.success(b)},"ground/1":function(w,b,y){y.variables().length===0&&w.success(b)},"acyclic_term/1":function(w,b,y){for(var F=b.substitution.apply(b.substitution),z=y.args[0].variables(),Z=0;Z0?Pt[Pt.length-1]:null,Pt!==null&&(qt=W(w,Pt,0,w.__get_max_priority(),!1))}if(qt.type===p&&qt.len===Pt.length-1&&gn.value==="."){qt=qt.value.rename(w);var Pr=new j("=",[z,qt]);if(oe.variables){var Ir=g(s(ye(qt.variables()),function(Nr){return new De(Nr)}));Pr=new j(",",[Pr,new j("=",[oe.variables,Ir])])}if(oe.variable_names){var Ir=g(s(ye(qt.variables()),function(nn){var ai;for(ai in w.session.renamed_variables)if(w.session.renamed_variables.hasOwnProperty(ai)&&w.session.renamed_variables[ai]===nn)break;return new j("=",[new j(ai,[]),new De(nn)])}));Pr=new j(",",[Pr,new j("=",[oe.variable_names,Ir])])}if(oe.singletons){var Ir=g(s(new Ye(qt,null).singleton_variables(),function(nn){var ai;for(ai in w.session.renamed_variables)if(w.session.renamed_variables.hasOwnProperty(ai)&&w.session.renamed_variables[ai]===nn)break;return new j("=",[new j(ai,[]),new De(nn)])}));Pr=new j(",",[Pr,new j("=",[oe.singletons,Ir])])}w.prepend([new Pe(b.goal.replace(Pr),b.substitution,b)])}else qt.type===p?w.throw_error(x.error.syntax(Pt[qt.len],"unexpected token",!1)):w.throw_error(qt.value)}}},"write/1":function(w,b,y){var F=y.args[0];w.prepend([new Pe(b.goal.replace(new j(",",[new j("current_output",[new De("S")]),new j("write",[new De("S"),F])])),b.substitution,b)])},"write/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new Pe(b.goal.replace(new j("write_term",[F,z,new j(".",[new j("quoted",[new j("false",[])]),new j(".",[new j("ignore_ops",[new j("false")]),new j(".",[new j("numbervars",[new j("true")]),new j("[]",[])])])])])),b.substitution,b)])},"writeq/1":function(w,b,y){var F=y.args[0];w.prepend([new Pe(b.goal.replace(new j(",",[new j("current_output",[new De("S")]),new j("writeq",[new De("S"),F])])),b.substitution,b)])},"writeq/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new Pe(b.goal.replace(new j("write_term",[F,z,new j(".",[new j("quoted",[new j("true",[])]),new j(".",[new j("ignore_ops",[new j("false")]),new j(".",[new j("numbervars",[new j("true")]),new j("[]",[])])])])])),b.substitution,b)])},"write_canonical/1":function(w,b,y){var F=y.args[0];w.prepend([new Pe(b.goal.replace(new j(",",[new j("current_output",[new De("S")]),new j("write_canonical",[new De("S"),F])])),b.substitution,b)])},"write_canonical/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new Pe(b.goal.replace(new j("write_term",[F,z,new j(".",[new j("quoted",[new j("true",[])]),new j(".",[new j("ignore_ops",[new j("true")]),new j(".",[new j("numbervars",[new j("false")]),new j("[]",[])])])])])),b.substitution,b)])},"write_term/2":function(w,b,y){var F=y.args[0],z=y.args[1];w.prepend([new Pe(b.goal.replace(new j(",",[new j("current_output",[new De("S")]),new j("write_term",[new De("S"),F,z])])),b.substitution,b)])},"write_term/3":function(w,b,y){var F=y.args[0],z=y.args[1],Z=y.args[2],$=x.type.is_stream(F)?F:w.get_stream_by_alias(F.id);if(x.type.is_variable(F)||x.type.is_variable(Z))w.throw_error(x.error.instantiation(y.indicator));else if(!x.type.is_list(Z))w.throw_error(x.error.type("list",Z,y.indicator));else if(!x.type.is_stream(F)&&!x.type.is_atom(F))w.throw_error(x.error.domain("stream_or_alias",F,y.indicator));else if(!x.type.is_stream($)||$.stream===null)w.throw_error(x.error.existence("stream",F,y.indicator));else if($.input)w.throw_error(x.error.permission("output","stream",F,y.indicator));else if($.type==="binary")w.throw_error(x.error.permission("output","binary_stream",F,y.indicator));else if($.position==="past_end_of_stream"&&$.eof_action==="error")w.throw_error(x.error.permission("output","past_end_of_stream",F,y.indicator));else{for(var oe={},xe=Z,Te;x.type.is_term(xe)&&xe.indicator==="./2";){if(Te=xe.args[0],x.type.is_variable(Te)){w.throw_error(x.error.instantiation(y.indicator));return}else if(!x.type.is_write_option(Te)){w.throw_error(x.error.domain("write_option",Te,y.indicator));return}oe[Te.id]=Te.args[0].id==="true",xe=xe.args[1]}if(xe.indicator!=="[]/0"){x.type.is_variable(xe)?w.throw_error(x.error.instantiation(y.indicator)):w.throw_error(x.error.type("list",Z,y.indicator));return}else{oe.session=w.session;var lt=z.toString(oe);$.stream.put(lt,$.position),typeof $.position=="number"&&($.position+=lt.length),w.success(b)}}},"halt/0":function(w,b,y){w.points=[]},"halt/1":function(w,b,y){var F=y.args[0];x.type.is_variable(F)?w.throw_error(x.error.instantiation(y.indicator)):x.type.is_integer(F)?w.points=[]:w.throw_error(x.error.type("integer",F,y.indicator))},"current_prolog_flag/2":function(w,b,y){var F=y.args[0],z=y.args[1];if(!x.type.is_variable(F)&&!x.type.is_atom(F))w.throw_error(x.error.type("atom",F,y.indicator));else if(!x.type.is_variable(F)&&!x.type.is_flag(F))w.throw_error(x.error.domain("prolog_flag",F,y.indicator));else{var Z=[];for(var $ in x.flag)if(x.flag.hasOwnProperty($)){var oe=new j(",",[new j("=",[new j($),F]),new j("=",[w.get_flag($),z])]);Z.push(new Pe(b.goal.replace(oe),b.substitution,b))}w.prepend(Z)}},"set_prolog_flag/2":function(w,b,y){var F=y.args[0],z=y.args[1];x.type.is_variable(F)||x.type.is_variable(z)?w.throw_error(x.error.instantiation(y.indicator)):x.type.is_atom(F)?x.type.is_flag(F)?x.type.is_value_flag(F,z)?x.type.is_modifiable_flag(F)?(w.session.flag[F.id]=z,w.success(b)):w.throw_error(x.error.permission("modify","flag",F)):w.throw_error(x.error.domain("flag_value",new j("+",[F,z]),y.indicator)):w.throw_error(x.error.domain("prolog_flag",F,y.indicator)):w.throw_error(x.error.type("atom",F,y.indicator))}},flag:{bounded:{allowed:[new j("true"),new j("false")],value:new j("true"),changeable:!1},max_integer:{allowed:[new Re(Number.MAX_SAFE_INTEGER)],value:new Re(Number.MAX_SAFE_INTEGER),changeable:!1},min_integer:{allowed:[new Re(Number.MIN_SAFE_INTEGER)],value:new Re(Number.MIN_SAFE_INTEGER),changeable:!1},integer_rounding_function:{allowed:[new j("down"),new j("toward_zero")],value:new j("toward_zero"),changeable:!1},char_conversion:{allowed:[new j("on"),new j("off")],value:new j("on"),changeable:!0},debug:{allowed:[new j("on"),new j("off")],value:new j("off"),changeable:!0},max_arity:{allowed:[new j("unbounded")],value:new j("unbounded"),changeable:!1},unknown:{allowed:[new j("error"),new j("fail"),new j("warning")],value:new j("error"),changeable:!0},double_quotes:{allowed:[new j("chars"),new j("codes"),new j("atom")],value:new j("codes"),changeable:!0},occurs_check:{allowed:[new j("false"),new j("true")],value:new j("false"),changeable:!0},dialect:{allowed:[new j("tau")],value:new j("tau"),changeable:!1},version_data:{allowed:[new j("tau",[new Re(t.major,!1),new Re(t.minor,!1),new Re(t.patch,!1),new j(t.status)])],value:new j("tau",[new Re(t.major,!1),new Re(t.minor,!1),new Re(t.patch,!1),new j(t.status)]),changeable:!1},nodejs:{allowed:[new j("yes"),new j("no")],value:new j(typeof tc<"u"&&tc.exports?"yes":"no"),changeable:!1}},unify:function(w,b,y){y=y===void 0?!1:y;for(var F=[{left:w,right:b}],z={};F.length!==0;){var Z=F.pop();if(w=Z.left,b=Z.right,x.type.is_term(w)&&x.type.is_term(b)){if(w.indicator!==b.indicator)return null;for(var $=0;$z.value?1:0:z}else return F},operate:function(w,b){if(x.type.is_operator(b)){for(var y=x.type.is_operator(b),F=[],z,Z=!1,$=0;$w.get_flag("max_integer").value||z0?w.start+w.matches[0].length:w.start,z=y?new j("token_not_found"):new j("found",[new j(w.value.toString())]),Z=new j(".",[new j("line",[new Re(w.line+1)]),new j(".",[new j("column",[new Re(F+1)]),new j(".",[z,new j("[]",[])])])]);return new j("error",[new j("syntax_error",[new j(b)]),Z])},syntax_by_predicate:function(w,b){return new j("error",[new j("syntax_error",[new j(w)]),X(b)])}},warning:{singleton:function(w,b,y){for(var F=new j("[]"),z=w.length-1;z>=0;z--)F=new j(".",[new De(w[z]),F]);return new j("warning",[new j("singleton_variables",[F,X(b)]),new j(".",[new j("line",[new Re(y,!1)]),new j("[]")])])},failed_goal:function(w,b){return new j("warning",[new j("failed_goal",[w]),new j(".",[new j("line",[new Re(b,!1)]),new j("[]")])])}},format_variable:function(w){return"_"+w},format_answer:function(w,b,F){b instanceof ke&&(b=b.thread);var F=F||{};if(F.session=b?b.session:void 0,x.type.is_error(w))return"uncaught exception: "+w.args[0].toString();if(w===!1)return"false.";if(w===null)return"limit exceeded ;";var z=0,Z="";if(x.type.is_substitution(w)){var $=w.domain(!0);w=w.filter(function(Te,lt){return!x.type.is_variable(lt)||$.indexOf(lt.id)!==-1&&Te!==lt.id})}for(var oe in w.links)w.links.hasOwnProperty(oe)&&(z++,Z!==""&&(Z+=", "),Z+=oe.toString(F)+" = "+w.links[oe].toString(F));var xe=typeof b>"u"||b.points.length>0?" ;":".";return z===0?"true"+xe:Z+xe},flatten_error:function(w){if(!x.type.is_error(w))return null;w=w.args[0];var b={};return b.type=w.args[0].id,b.thrown=b.type==="syntax_error"?null:w.args[1].id,b.expected=null,b.found=null,b.representation=null,b.existence=null,b.existence_type=null,b.line=null,b.column=null,b.permission_operation=null,b.permission_type=null,b.evaluation_type=null,b.type==="type_error"||b.type==="domain_error"?(b.expected=w.args[0].args[0].id,b.found=w.args[0].args[1].toString()):b.type==="syntax_error"?w.args[1].indicator==="./2"?(b.expected=w.args[0].args[0].id,b.found=w.args[1].args[1].args[1].args[0],b.found=b.found.id==="token_not_found"?b.found.id:b.found.args[0].id,b.line=w.args[1].args[0].args[0].value,b.column=w.args[1].args[1].args[0].args[0].value):b.thrown=w.args[1].id:b.type==="permission_error"?(b.found=w.args[0].args[2].toString(),b.permission_operation=w.args[0].args[0].id,b.permission_type=w.args[0].args[1].id):b.type==="evaluation_error"?b.evaluation_type=w.args[0].args[0].id:b.type==="representation_error"?b.representation=w.args[0].args[0].id:b.type==="existence_error"&&(b.existence=w.args[0].args[1].toString(),b.existence_type=w.args[0].args[0].id),b},create:function(w){return new x.type.Session(w)}};typeof tc<"u"?tc.exports=x:window.pl=x})()});function nve(t,e,r){t.prepend(r.map(s=>new gl.default.type.State(e.goal.replace(s),e.substitution,e)))}function f9(t){let e=sve.get(t.session);if(e==null)throw new Error("Assertion failed: A project should have been registered for the active session");return e}function ove(t,e){sve.set(t,e),t.consult(`:- use_module(library(${OSt.id})).`)}var A9,gl,ive,V0,FSt,NSt,sve,OSt,ave=Ct(()=>{Ve();A9=et(aS()),gl=et(u9()),ive=et(Ie("vm")),{is_atom:V0,is_variable:FSt,is_instantiated_list:NSt}=gl.default.type;sve=new WeakMap;OSt=new gl.default.type.Module("constraints",{"project_workspaces_by_descriptor/3":(t,e,r)=>{let[s,a,n]=r.args;if(!V0(s)||!V0(a)){t.throw_error(gl.default.error.instantiation(r.indicator));return}let c=q.parseIdent(s.id),f=q.makeDescriptor(c,a.id),h=f9(t).tryWorkspaceByDescriptor(f);FSt(n)&&h!==null&&nve(t,e,[new gl.default.type.Term("=",[n,new gl.default.type.Term(String(h.relativeCwd))])]),V0(n)&&h!==null&&h.relativeCwd===n.id&&t.success(e)},"workspace_field/3":(t,e,r)=>{let[s,a,n]=r.args;if(!V0(s)||!V0(a)){t.throw_error(gl.default.error.instantiation(r.indicator));return}let f=f9(t).tryWorkspaceByCwd(s.id);if(f==null)return;let p=(0,A9.default)(f.manifest.raw,a.id);typeof p>"u"||nve(t,e,[new gl.default.type.Term("=",[n,new gl.default.type.Term(typeof p=="object"?JSON.stringify(p):p)])])},"workspace_field_test/3":(t,e,r)=>{let[s,a,n]=r.args;t.prepend([new gl.default.type.State(e.goal.replace(new gl.default.type.Term("workspace_field_test",[s,a,n,new gl.default.type.Term("[]",[])])),e.substitution,e)])},"workspace_field_test/4":(t,e,r)=>{let[s,a,n,c]=r.args;if(!V0(s)||!V0(a)||!V0(n)||!NSt(c)){t.throw_error(gl.default.error.instantiation(r.indicator));return}let p=f9(t).tryWorkspaceByCwd(s.id);if(p==null)return;let h=(0,A9.default)(p.manifest.raw,a.id);if(typeof h>"u")return;let E={$$:h};for(let[S,P]of c.toJavaScript().entries())E[`$${S}`]=P;ive.default.runInNewContext(n.id,E)&&t.success(e)}},["project_workspaces_by_descriptor/3","workspace_field/3","workspace_field_test/3","workspace_field_test/4"])});var yS={};Vt(yS,{Constraints:()=>h9,DependencyType:()=>fve});function yo(t){if(t instanceof JC.default.type.Num)return t.value;if(t instanceof JC.default.type.Term)switch(t.indicator){case"throw/1":return yo(t.args[0]);case"error/1":return yo(t.args[0]);case"error/2":if(t.args[0]instanceof JC.default.type.Term&&t.args[0].indicator==="syntax_error/1")return Object.assign(yo(t.args[0]),...yo(t.args[1]));{let e=yo(t.args[0]);return e.message+=` (in ${yo(t.args[1])})`,e}case"syntax_error/1":return new Yt(43,`Syntax error: ${yo(t.args[0])}`);case"existence_error/2":return new Yt(44,`Existence error: ${yo(t.args[0])} ${yo(t.args[1])} not found`);case"instantiation_error/0":return new Yt(75,"Instantiation error: an argument is variable when an instantiated argument was expected");case"line/1":return{line:yo(t.args[0])};case"column/1":return{column:yo(t.args[0])};case"found/1":return{found:yo(t.args[0])};case"./2":return[yo(t.args[0])].concat(yo(t.args[1]));case"//2":return`${yo(t.args[0])}/${yo(t.args[1])}`;default:return t.id}throw`couldn't pretty print because of unsupported node ${t}`}function cve(t){let e;try{e=yo(t)}catch(r){throw typeof r=="string"?new Yt(42,`Unknown error: ${t} (note: ${r})`):r}return typeof e.line<"u"&&typeof e.column<"u"&&(e.message+=` at line ${e.line}, column ${e.column}`),e}function bm(t){return t.id==="null"?null:`${t.toJavaScript()}`}function LSt(t){if(t.id==="null")return null;{let e=t.toJavaScript();if(typeof e!="string")return JSON.stringify(e);try{return JSON.stringify(JSON.parse(e))}catch{return JSON.stringify(e)}}}function K0(t){return typeof t=="string"?`'${t}'`:"[]"}var uve,JC,fve,lve,p9,h9,ES=Ct(()=>{Ve();Ve();bt();uve=et(HBe()),JC=et(u9());gS();ave();(0,uve.default)(JC.default);fve=(s=>(s.Dependencies="dependencies",s.DevDependencies="devDependencies",s.PeerDependencies="peerDependencies",s))(fve||{}),lve=["dependencies","devDependencies","peerDependencies"];p9=class{constructor(e,r){let s=1e3*e.workspaces.length;this.session=JC.default.create(s),ove(this.session,e),this.session.consult(":- use_module(library(lists))."),this.session.consult(r)}fetchNextAnswer(){return new Promise(e=>{this.session.answer(r=>{e(r)})})}async*makeQuery(e){let r=this.session.query(e);if(r!==!0)throw cve(r);for(;;){let s=await this.fetchNextAnswer();if(s===null)throw new Yt(79,"Resolution limit exceeded");if(!s)break;if(s.id==="throw")throw cve(s);yield s}}};h9=class t{constructor(e){this.source="";this.project=e;let r=e.configuration.get("constraintsPath");le.existsSync(r)&&(this.source=le.readFileSync(r,"utf8"))}static async find(e){return new t(e)}getProjectDatabase(){let e="";for(let r of lve)e+=`dependency_type(${r}). `;for(let r of this.project.workspacesByCwd.values()){let s=r.relativeCwd;e+=`workspace(${K0(s)}). `,e+=`workspace_ident(${K0(s)}, ${K0(q.stringifyIdent(r.anchoredLocator))}). `,e+=`workspace_version(${K0(s)}, ${K0(r.manifest.version)}). `;for(let a of lve)for(let n of r.manifest[a].values())e+=`workspace_has_dependency(${K0(s)}, ${K0(q.stringifyIdent(n))}, ${K0(n.range)}, ${a}). `}return e+=`workspace(_) :- false. `,e+=`workspace_ident(_, _) :- false. `,e+=`workspace_version(_, _) :- false. `,e+=`workspace_has_dependency(_, _, _, _) :- false. `,e}getDeclarations(){let e="";return e+=`gen_enforced_dependency(_, _, _, _) :- false. `,e+=`gen_enforced_field(_, _, _) :- false. `,e}get fullSource(){return`${this.getProjectDatabase()} ${this.source} ${this.getDeclarations()}`}createSession(){return new p9(this.project,this.fullSource)}async processClassic(){let e=this.createSession();return{enforcedDependencies:await this.genEnforcedDependencies(e),enforcedFields:await this.genEnforcedFields(e)}}async process(){let{enforcedDependencies:e,enforcedFields:r}=await this.processClassic(),s=new Map;for(let{workspace:a,dependencyIdent:n,dependencyRange:c,dependencyType:f}of e){let p=hS([f,q.stringifyIdent(n)]),h=je.getMapWithDefault(s,a.cwd);je.getMapWithDefault(h,p).set(c??void 0,new Set)}for(let{workspace:a,fieldPath:n,fieldValue:c}of r){let f=hS(n),p=je.getMapWithDefault(s,a.cwd);je.getMapWithDefault(p,f).set(JSON.parse(c)??void 0,new Set)}return{manifestUpdates:s,reportedErrors:new Map}}async genEnforcedDependencies(e){let r=[];for await(let s of e.makeQuery("workspace(WorkspaceCwd), dependency_type(DependencyType), gen_enforced_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType).")){let a=K.resolve(this.project.cwd,bm(s.links.WorkspaceCwd)),n=bm(s.links.DependencyIdent),c=bm(s.links.DependencyRange),f=bm(s.links.DependencyType);if(a===null||n===null)throw new Error("Invalid rule");let p=this.project.getWorkspaceByCwd(a),h=q.parseIdent(n);r.push({workspace:p,dependencyIdent:h,dependencyRange:c,dependencyType:f})}return je.sortMap(r,[({dependencyRange:s})=>s!==null?"0":"1",({workspace:s})=>q.stringifyIdent(s.anchoredLocator),({dependencyIdent:s})=>q.stringifyIdent(s)])}async genEnforcedFields(e){let r=[];for await(let s of e.makeQuery("workspace(WorkspaceCwd), gen_enforced_field(WorkspaceCwd, FieldPath, FieldValue).")){let a=K.resolve(this.project.cwd,bm(s.links.WorkspaceCwd)),n=bm(s.links.FieldPath),c=LSt(s.links.FieldValue);if(a===null||n===null)throw new Error("Invalid rule");let f=this.project.getWorkspaceByCwd(a);r.push({workspace:f,fieldPath:n,fieldValue:c})}return je.sortMap(r,[({workspace:s})=>q.stringifyIdent(s.anchoredLocator),({fieldPath:s})=>s])}async*query(e){let r=this.createSession();for await(let s of r.makeQuery(e)){let a={};for(let[n,c]of Object.entries(s.links))n!=="_"&&(a[n]=bm(c));yield a}}}});var Ive=L(pF=>{"use strict";Object.defineProperty(pF,"__esModule",{value:!0});function NS(t){let e=[...t.caches],r=e.shift();return r===void 0?Eve():{get(s,a,n={miss:()=>Promise.resolve()}){return r.get(s,a,n).catch(()=>NS({caches:e}).get(s,a,n))},set(s,a){return r.set(s,a).catch(()=>NS({caches:e}).set(s,a))},delete(s){return r.delete(s).catch(()=>NS({caches:e}).delete(s))},clear(){return r.clear().catch(()=>NS({caches:e}).clear())}}}function Eve(){return{get(t,e,r={miss:()=>Promise.resolve()}){return e().then(a=>Promise.all([a,r.miss(a)])).then(([a])=>a)},set(t,e){return Promise.resolve(e)},delete(t){return Promise.resolve()},clear(){return Promise.resolve()}}}pF.createFallbackableCache=NS;pF.createNullCache=Eve});var wve=L((Apr,Cve)=>{Cve.exports=Ive()});var Bve=L(P9=>{"use strict";Object.defineProperty(P9,"__esModule",{value:!0});function rDt(t={serializable:!0}){let e={};return{get(r,s,a={miss:()=>Promise.resolve()}){let n=JSON.stringify(r);if(n in e)return Promise.resolve(t.serializable?JSON.parse(e[n]):e[n]);let c=s(),f=a&&a.miss||(()=>Promise.resolve());return c.then(p=>f(p)).then(()=>c)},set(r,s){return e[JSON.stringify(r)]=t.serializable?JSON.stringify(s):s,Promise.resolve(s)},delete(r){return delete e[JSON.stringify(r)],Promise.resolve()},clear(){return e={},Promise.resolve()}}}P9.createInMemoryCache=rDt});var Sve=L((hpr,vve)=>{vve.exports=Bve()});var bve=L(ef=>{"use strict";Object.defineProperty(ef,"__esModule",{value:!0});function nDt(t,e,r){let s={"x-algolia-api-key":r,"x-algolia-application-id":e};return{headers(){return t===x9.WithinHeaders?s:{}},queryParameters(){return t===x9.WithinQueryParameters?s:{}}}}function iDt(t){let e=0,r=()=>(e++,new Promise(s=>{setTimeout(()=>{s(t(r))},Math.min(100*e,1e3))}));return t(r)}function Dve(t,e=(r,s)=>Promise.resolve()){return Object.assign(t,{wait(r){return Dve(t.then(s=>Promise.all([e(s,r),s])).then(s=>s[1]))}})}function sDt(t){let e=t.length-1;for(e;e>0;e--){let r=Math.floor(Math.random()*(e+1)),s=t[e];t[e]=t[r],t[r]=s}return t}function oDt(t,e){return e&&Object.keys(e).forEach(r=>{t[r]=e[r](t)}),t}function aDt(t,...e){let r=0;return t.replace(/%s/g,()=>encodeURIComponent(e[r++]))}var lDt="4.22.1",cDt=t=>()=>t.transporter.requester.destroy(),x9={WithinQueryParameters:0,WithinHeaders:1};ef.AuthMode=x9;ef.addMethods=oDt;ef.createAuth=nDt;ef.createRetryablePromise=iDt;ef.createWaitablePromise=Dve;ef.destroy=cDt;ef.encode=aDt;ef.shuffle=sDt;ef.version=lDt});var OS=L((dpr,Pve)=>{Pve.exports=bve()});var xve=L(k9=>{"use strict";Object.defineProperty(k9,"__esModule",{value:!0});var uDt={Delete:"DELETE",Get:"GET",Post:"POST",Put:"PUT"};k9.MethodEnum=uDt});var LS=L((ypr,kve)=>{kve.exports=xve()});var Wve=L(Vi=>{"use strict";Object.defineProperty(Vi,"__esModule",{value:!0});var Tve=LS();function Q9(t,e){let r=t||{},s=r.data||{};return Object.keys(r).forEach(a=>{["timeout","headers","queryParameters","data","cacheable"].indexOf(a)===-1&&(s[a]=r[a])}),{data:Object.entries(s).length>0?s:void 0,timeout:r.timeout||e,headers:r.headers||{},queryParameters:r.queryParameters||{},cacheable:r.cacheable}}var MS={Read:1,Write:2,Any:3},sw={Up:1,Down:2,Timeouted:3},Rve=2*60*1e3;function R9(t,e=sw.Up){return{...t,status:e,lastUpdate:Date.now()}}function Fve(t){return t.status===sw.Up||Date.now()-t.lastUpdate>Rve}function Nve(t){return t.status===sw.Timeouted&&Date.now()-t.lastUpdate<=Rve}function F9(t){return typeof t=="string"?{protocol:"https",url:t,accept:MS.Any}:{protocol:t.protocol||"https",url:t.url,accept:t.accept||MS.Any}}function fDt(t,e){return Promise.all(e.map(r=>t.get(r,()=>Promise.resolve(R9(r))))).then(r=>{let s=r.filter(f=>Fve(f)),a=r.filter(f=>Nve(f)),n=[...s,...a],c=n.length>0?n.map(f=>F9(f)):e;return{getTimeout(f,p){return(a.length===0&&f===0?1:a.length+3+f)*p},statelessHosts:c}})}var ADt=({isTimedOut:t,status:e})=>!t&&~~e===0,pDt=t=>{let e=t.status;return t.isTimedOut||ADt(t)||~~(e/100)!==2&&~~(e/100)!==4},hDt=({status:t})=>~~(t/100)===2,gDt=(t,e)=>pDt(t)?e.onRetry(t):hDt(t)?e.onSuccess(t):e.onFail(t);function Qve(t,e,r,s){let a=[],n=Uve(r,s),c=Hve(t,s),f=r.method,p=r.method!==Tve.MethodEnum.Get?{}:{...r.data,...s.data},h={"x-algolia-agent":t.userAgent.value,...t.queryParameters,...p,...s.queryParameters},E=0,C=(S,P)=>{let I=S.pop();if(I===void 0)throw Gve(T9(a));let R={data:n,headers:c,method:f,url:Mve(I,r.path,h),connectTimeout:P(E,t.timeouts.connect),responseTimeout:P(E,s.timeout)},N=W=>{let te={request:R,response:W,host:I,triesLeft:S.length};return a.push(te),te},U={onSuccess:W=>Ove(W),onRetry(W){let te=N(W);return W.isTimedOut&&E++,Promise.all([t.logger.info("Retryable failure",N9(te)),t.hostsCache.set(I,R9(I,W.isTimedOut?sw.Timeouted:sw.Down))]).then(()=>C(S,P))},onFail(W){throw N(W),Lve(W,T9(a))}};return t.requester.send(R).then(W=>gDt(W,U))};return fDt(t.hostsCache,e).then(S=>C([...S.statelessHosts].reverse(),S.getTimeout))}function dDt(t){let{hostsCache:e,logger:r,requester:s,requestsCache:a,responsesCache:n,timeouts:c,userAgent:f,hosts:p,queryParameters:h,headers:E}=t,C={hostsCache:e,logger:r,requester:s,requestsCache:a,responsesCache:n,timeouts:c,userAgent:f,headers:E,queryParameters:h,hosts:p.map(S=>F9(S)),read(S,P){let I=Q9(P,C.timeouts.read),R=()=>Qve(C,C.hosts.filter(W=>(W.accept&MS.Read)!==0),S,I);if((I.cacheable!==void 0?I.cacheable:S.cacheable)!==!0)return R();let U={request:S,mappedRequestOptions:I,transporter:{queryParameters:C.queryParameters,headers:C.headers}};return C.responsesCache.get(U,()=>C.requestsCache.get(U,()=>C.requestsCache.set(U,R()).then(W=>Promise.all([C.requestsCache.delete(U),W]),W=>Promise.all([C.requestsCache.delete(U),Promise.reject(W)])).then(([W,te])=>te)),{miss:W=>C.responsesCache.set(U,W)})},write(S,P){return Qve(C,C.hosts.filter(I=>(I.accept&MS.Write)!==0),S,Q9(P,C.timeouts.write))}};return C}function mDt(t){let e={value:`Algolia for JavaScript (${t})`,add(r){let s=`; ${r.segment}${r.version!==void 0?` (${r.version})`:""}`;return e.value.indexOf(s)===-1&&(e.value=`${e.value}${s}`),e}};return e}function Ove(t){try{return JSON.parse(t.content)}catch(e){throw qve(e.message,t)}}function Lve({content:t,status:e},r){let s=t;try{s=JSON.parse(t).message}catch{}return jve(s,e,r)}function yDt(t,...e){let r=0;return t.replace(/%s/g,()=>encodeURIComponent(e[r++]))}function Mve(t,e,r){let s=_ve(r),a=`${t.protocol}://${t.url}/${e.charAt(0)==="/"?e.substr(1):e}`;return s.length&&(a+=`?${s}`),a}function _ve(t){let e=r=>Object.prototype.toString.call(r)==="[object Object]"||Object.prototype.toString.call(r)==="[object Array]";return Object.keys(t).map(r=>yDt("%s=%s",r,e(t[r])?JSON.stringify(t[r]):t[r])).join("&")}function Uve(t,e){if(t.method===Tve.MethodEnum.Get||t.data===void 0&&e.data===void 0)return;let r=Array.isArray(t.data)?t.data:{...t.data,...e.data};return JSON.stringify(r)}function Hve(t,e){let r={...t.headers,...e.headers},s={};return Object.keys(r).forEach(a=>{let n=r[a];s[a.toLowerCase()]=n}),s}function T9(t){return t.map(e=>N9(e))}function N9(t){let e=t.request.headers["x-algolia-api-key"]?{"x-algolia-api-key":"*****"}:{};return{...t,request:{...t.request,headers:{...t.request.headers,...e}}}}function jve(t,e,r){return{name:"ApiError",message:t,status:e,transporterStackTrace:r}}function qve(t,e){return{name:"DeserializationError",message:t,response:e}}function Gve(t){return{name:"RetryError",message:"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.",transporterStackTrace:t}}Vi.CallEnum=MS;Vi.HostStatusEnum=sw;Vi.createApiError=jve;Vi.createDeserializationError=qve;Vi.createMappedRequestOptions=Q9;Vi.createRetryError=Gve;Vi.createStatefulHost=R9;Vi.createStatelessHost=F9;Vi.createTransporter=dDt;Vi.createUserAgent=mDt;Vi.deserializeFailure=Lve;Vi.deserializeSuccess=Ove;Vi.isStatefulHostTimeouted=Nve;Vi.isStatefulHostUp=Fve;Vi.serializeData=Uve;Vi.serializeHeaders=Hve;Vi.serializeQueryParameters=_ve;Vi.serializeUrl=Mve;Vi.stackFrameWithoutCredentials=N9;Vi.stackTraceWithoutCredentials=T9});var _S=L((Ipr,Yve)=>{Yve.exports=Wve()});var Vve=L(z0=>{"use strict";Object.defineProperty(z0,"__esModule",{value:!0});var ow=OS(),EDt=_S(),US=LS(),IDt=t=>{let e=t.region||"us",r=ow.createAuth(ow.AuthMode.WithinHeaders,t.appId,t.apiKey),s=EDt.createTransporter({hosts:[{url:`analytics.${e}.algolia.com`}],...t,headers:{...r.headers(),"content-type":"application/json",...t.headers},queryParameters:{...r.queryParameters(),...t.queryParameters}}),a=t.appId;return ow.addMethods({appId:a,transporter:s},t.methods)},CDt=t=>(e,r)=>t.transporter.write({method:US.MethodEnum.Post,path:"2/abtests",data:e},r),wDt=t=>(e,r)=>t.transporter.write({method:US.MethodEnum.Delete,path:ow.encode("2/abtests/%s",e)},r),BDt=t=>(e,r)=>t.transporter.read({method:US.MethodEnum.Get,path:ow.encode("2/abtests/%s",e)},r),vDt=t=>e=>t.transporter.read({method:US.MethodEnum.Get,path:"2/abtests"},e),SDt=t=>(e,r)=>t.transporter.write({method:US.MethodEnum.Post,path:ow.encode("2/abtests/%s/stop",e)},r);z0.addABTest=CDt;z0.createAnalyticsClient=IDt;z0.deleteABTest=wDt;z0.getABTest=BDt;z0.getABTests=vDt;z0.stopABTest=SDt});var Jve=L((wpr,Kve)=>{Kve.exports=Vve()});var Zve=L(HS=>{"use strict";Object.defineProperty(HS,"__esModule",{value:!0});var O9=OS(),DDt=_S(),zve=LS(),bDt=t=>{let e=t.region||"us",r=O9.createAuth(O9.AuthMode.WithinHeaders,t.appId,t.apiKey),s=DDt.createTransporter({hosts:[{url:`personalization.${e}.algolia.com`}],...t,headers:{...r.headers(),"content-type":"application/json",...t.headers},queryParameters:{...r.queryParameters(),...t.queryParameters}});return O9.addMethods({appId:t.appId,transporter:s},t.methods)},PDt=t=>e=>t.transporter.read({method:zve.MethodEnum.Get,path:"1/strategies/personalization"},e),xDt=t=>(e,r)=>t.transporter.write({method:zve.MethodEnum.Post,path:"1/strategies/personalization",data:e},r);HS.createPersonalizationClient=bDt;HS.getPersonalizationStrategy=PDt;HS.setPersonalizationStrategy=xDt});var $ve=L((vpr,Xve)=>{Xve.exports=Zve()});var pSe=L(Ft=>{"use strict";Object.defineProperty(Ft,"__esModule",{value:!0});var Kt=OS(),dl=_S(),br=LS(),kDt=Ie("crypto");function hF(t){let e=r=>t.request(r).then(s=>{if(t.batch!==void 0&&t.batch(s.hits),!t.shouldStop(s))return s.cursor?e({cursor:s.cursor}):e({page:(r.page||0)+1})});return e({})}var QDt=t=>{let e=t.appId,r=Kt.createAuth(t.authMode!==void 0?t.authMode:Kt.AuthMode.WithinHeaders,e,t.apiKey),s=dl.createTransporter({hosts:[{url:`${e}-dsn.algolia.net`,accept:dl.CallEnum.Read},{url:`${e}.algolia.net`,accept:dl.CallEnum.Write}].concat(Kt.shuffle([{url:`${e}-1.algolianet.com`},{url:`${e}-2.algolianet.com`},{url:`${e}-3.algolianet.com`}])),...t,headers:{...r.headers(),"content-type":"application/x-www-form-urlencoded",...t.headers},queryParameters:{...r.queryParameters(),...t.queryParameters}}),a={transporter:s,appId:e,addAlgoliaAgent(n,c){s.userAgent.add({segment:n,version:c})},clearCache(){return Promise.all([s.requestsCache.clear(),s.responsesCache.clear()]).then(()=>{})}};return Kt.addMethods(a,t.methods)};function eSe(){return{name:"MissingObjectIDError",message:"All objects must have an unique objectID (like a primary key) to be valid. Algolia is also able to generate objectIDs automatically but *it's not recommended*. To do it, use the `{'autoGenerateObjectIDIfNotExist': true}` option."}}function tSe(){return{name:"ObjectNotFoundError",message:"Object not found."}}function rSe(){return{name:"ValidUntilNotFoundError",message:"ValidUntil not found in given secured api key."}}var TDt=t=>(e,r)=>{let{queryParameters:s,...a}=r||{},n={acl:e,...s!==void 0?{queryParameters:s}:{}},c=(f,p)=>Kt.createRetryablePromise(h=>jS(t)(f.key,p).catch(E=>{if(E.status!==404)throw E;return h()}));return Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:"1/keys",data:n},a),c)},RDt=t=>(e,r,s)=>{let a=dl.createMappedRequestOptions(s);return a.queryParameters["X-Algolia-User-ID"]=e,t.transporter.write({method:br.MethodEnum.Post,path:"1/clusters/mapping",data:{cluster:r}},a)},FDt=t=>(e,r,s)=>t.transporter.write({method:br.MethodEnum.Post,path:"1/clusters/mapping/batch",data:{users:e,cluster:r}},s),NDt=t=>(e,r)=>Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("/1/dictionaries/%s/batch",e),data:{clearExistingDictionaryEntries:!0,requests:{action:"addEntry",body:[]}}},r),(s,a)=>aw(t)(s.taskID,a)),gF=t=>(e,r,s)=>{let a=(n,c)=>qS(t)(e,{methods:{waitTask:ds}}).waitTask(n.taskID,c);return Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/operation",e),data:{operation:"copy",destination:r}},s),a)},ODt=t=>(e,r,s)=>gF(t)(e,r,{...s,scope:[mF.Rules]}),LDt=t=>(e,r,s)=>gF(t)(e,r,{...s,scope:[mF.Settings]}),MDt=t=>(e,r,s)=>gF(t)(e,r,{...s,scope:[mF.Synonyms]}),_Dt=t=>(e,r)=>e.method===br.MethodEnum.Get?t.transporter.read(e,r):t.transporter.write(e,r),UDt=t=>(e,r)=>{let s=(a,n)=>Kt.createRetryablePromise(c=>jS(t)(e,n).then(c).catch(f=>{if(f.status!==404)throw f}));return Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Delete,path:Kt.encode("1/keys/%s",e)},r),s)},HDt=t=>(e,r,s)=>{let a=r.map(n=>({action:"deleteEntry",body:{objectID:n}}));return Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("/1/dictionaries/%s/batch",e),data:{clearExistingDictionaryEntries:!1,requests:a}},s),(n,c)=>aw(t)(n.taskID,c))},jDt=()=>(t,e)=>{let r=dl.serializeQueryParameters(e),s=kDt.createHmac("sha256",t).update(r).digest("hex");return Buffer.from(s+r).toString("base64")},jS=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Get,path:Kt.encode("1/keys/%s",e)},r),nSe=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Get,path:Kt.encode("1/task/%s",e.toString())},r),qDt=t=>e=>t.transporter.read({method:br.MethodEnum.Get,path:"/1/dictionaries/*/settings"},e),GDt=t=>e=>t.transporter.read({method:br.MethodEnum.Get,path:"1/logs"},e),WDt=()=>t=>{let e=Buffer.from(t,"base64").toString("ascii"),r=/validUntil=(\d+)/,s=e.match(r);if(s===null)throw rSe();return parseInt(s[1],10)-Math.round(new Date().getTime()/1e3)},YDt=t=>e=>t.transporter.read({method:br.MethodEnum.Get,path:"1/clusters/mapping/top"},e),VDt=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Get,path:Kt.encode("1/clusters/mapping/%s",e)},r),KDt=t=>e=>{let{retrieveMappings:r,...s}=e||{};return r===!0&&(s.getClusters=!0),t.transporter.read({method:br.MethodEnum.Get,path:"1/clusters/mapping/pending"},s)},qS=t=>(e,r={})=>{let s={transporter:t.transporter,appId:t.appId,indexName:e};return Kt.addMethods(s,r.methods)},JDt=t=>e=>t.transporter.read({method:br.MethodEnum.Get,path:"1/keys"},e),zDt=t=>e=>t.transporter.read({method:br.MethodEnum.Get,path:"1/clusters"},e),ZDt=t=>e=>t.transporter.read({method:br.MethodEnum.Get,path:"1/indexes"},e),XDt=t=>e=>t.transporter.read({method:br.MethodEnum.Get,path:"1/clusters/mapping"},e),$Dt=t=>(e,r,s)=>{let a=(n,c)=>qS(t)(e,{methods:{waitTask:ds}}).waitTask(n.taskID,c);return Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/operation",e),data:{operation:"move",destination:r}},s),a)},ebt=t=>(e,r)=>{let s=(a,n)=>Promise.all(Object.keys(a.taskID).map(c=>qS(t)(c,{methods:{waitTask:ds}}).waitTask(a.taskID[c],n)));return Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:"1/indexes/*/batch",data:{requests:e}},r),s)},tbt=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Post,path:"1/indexes/*/objects",data:{requests:e}},r),rbt=t=>(e,r)=>{let s=e.map(a=>({...a,params:dl.serializeQueryParameters(a.params||{})}));return t.transporter.read({method:br.MethodEnum.Post,path:"1/indexes/*/queries",data:{requests:s},cacheable:!0},r)},nbt=t=>(e,r)=>Promise.all(e.map(s=>{let{facetName:a,facetQuery:n,...c}=s.params;return qS(t)(s.indexName,{methods:{searchForFacetValues:uSe}}).searchForFacetValues(a,n,{...r,...c})})),ibt=t=>(e,r)=>{let s=dl.createMappedRequestOptions(r);return s.queryParameters["X-Algolia-User-ID"]=e,t.transporter.write({method:br.MethodEnum.Delete,path:"1/clusters/mapping"},s)},sbt=t=>(e,r,s)=>{let a=r.map(n=>({action:"addEntry",body:n}));return Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("/1/dictionaries/%s/batch",e),data:{clearExistingDictionaryEntries:!0,requests:a}},s),(n,c)=>aw(t)(n.taskID,c))},obt=t=>(e,r)=>{let s=(a,n)=>Kt.createRetryablePromise(c=>jS(t)(e,n).catch(f=>{if(f.status!==404)throw f;return c()}));return Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("1/keys/%s/restore",e)},r),s)},abt=t=>(e,r,s)=>{let a=r.map(n=>({action:"addEntry",body:n}));return Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("/1/dictionaries/%s/batch",e),data:{clearExistingDictionaryEntries:!1,requests:a}},s),(n,c)=>aw(t)(n.taskID,c))},lbt=t=>(e,r,s)=>t.transporter.read({method:br.MethodEnum.Post,path:Kt.encode("/1/dictionaries/%s/search",e),data:{query:r},cacheable:!0},s),cbt=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Post,path:"1/clusters/mapping/search",data:{query:e}},r),ubt=t=>(e,r)=>Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Put,path:"/1/dictionaries/*/settings",data:e},r),(s,a)=>aw(t)(s.taskID,a)),fbt=t=>(e,r)=>{let s=Object.assign({},r),{queryParameters:a,...n}=r||{},c=a?{queryParameters:a}:{},f=["acl","indexes","referers","restrictSources","queryParameters","description","maxQueriesPerIPPerHour","maxHitsPerQuery"],p=E=>Object.keys(s).filter(C=>f.indexOf(C)!==-1).every(C=>{if(Array.isArray(E[C])&&Array.isArray(s[C])){let S=E[C];return S.length===s[C].length&&S.every((P,I)=>P===s[C][I])}else return E[C]===s[C]}),h=(E,C)=>Kt.createRetryablePromise(S=>jS(t)(e,C).then(P=>p(P)?Promise.resolve():S()));return Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Put,path:Kt.encode("1/keys/%s",e),data:c},n),h)},aw=t=>(e,r)=>Kt.createRetryablePromise(s=>nSe(t)(e,r).then(a=>a.status!=="published"?s():void 0)),iSe=t=>(e,r)=>{let s=(a,n)=>ds(t)(a.taskID,n);return Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/batch",t.indexName),data:{requests:e}},r),s)},Abt=t=>e=>hF({shouldStop:r=>r.cursor===void 0,...e,request:r=>t.transporter.read({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/browse",t.indexName),data:r},e)}),pbt=t=>e=>{let r={hitsPerPage:1e3,...e};return hF({shouldStop:s=>s.hits.length({...a,hits:a.hits.map(n=>(delete n._highlightResult,n))}))}})},hbt=t=>e=>{let r={hitsPerPage:1e3,...e};return hF({shouldStop:s=>s.hits.length({...a,hits:a.hits.map(n=>(delete n._highlightResult,n))}))}})},dF=t=>(e,r,s)=>{let{batchSize:a,...n}=s||{},c={taskIDs:[],objectIDs:[]},f=(p=0)=>{let h=[],E;for(E=p;E({action:r,body:C})),n).then(C=>(c.objectIDs=c.objectIDs.concat(C.objectIDs),c.taskIDs.push(C.taskID),E++,f(E)))};return Kt.createWaitablePromise(f(),(p,h)=>Promise.all(p.taskIDs.map(E=>ds(t)(E,h))))},gbt=t=>e=>Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/clear",t.indexName)},e),(r,s)=>ds(t)(r.taskID,s)),dbt=t=>e=>{let{forwardToReplicas:r,...s}=e||{},a=dl.createMappedRequestOptions(s);return r&&(a.queryParameters.forwardToReplicas=1),Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/rules/clear",t.indexName)},a),(n,c)=>ds(t)(n.taskID,c))},mbt=t=>e=>{let{forwardToReplicas:r,...s}=e||{},a=dl.createMappedRequestOptions(s);return r&&(a.queryParameters.forwardToReplicas=1),Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/synonyms/clear",t.indexName)},a),(n,c)=>ds(t)(n.taskID,c))},ybt=t=>(e,r)=>Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/deleteByQuery",t.indexName),data:e},r),(s,a)=>ds(t)(s.taskID,a)),Ebt=t=>e=>Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Delete,path:Kt.encode("1/indexes/%s",t.indexName)},e),(r,s)=>ds(t)(r.taskID,s)),Ibt=t=>(e,r)=>Kt.createWaitablePromise(sSe(t)([e],r).then(s=>({taskID:s.taskIDs[0]})),(s,a)=>ds(t)(s.taskID,a)),sSe=t=>(e,r)=>{let s=e.map(a=>({objectID:a}));return dF(t)(s,xm.DeleteObject,r)},Cbt=t=>(e,r)=>{let{forwardToReplicas:s,...a}=r||{},n=dl.createMappedRequestOptions(a);return s&&(n.queryParameters.forwardToReplicas=1),Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Delete,path:Kt.encode("1/indexes/%s/rules/%s",t.indexName,e)},n),(c,f)=>ds(t)(c.taskID,f))},wbt=t=>(e,r)=>{let{forwardToReplicas:s,...a}=r||{},n=dl.createMappedRequestOptions(a);return s&&(n.queryParameters.forwardToReplicas=1),Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Delete,path:Kt.encode("1/indexes/%s/synonyms/%s",t.indexName,e)},n),(c,f)=>ds(t)(c.taskID,f))},Bbt=t=>e=>oSe(t)(e).then(()=>!0).catch(r=>{if(r.status!==404)throw r;return!1}),vbt=t=>(e,r,s)=>t.transporter.read({method:br.MethodEnum.Post,path:Kt.encode("1/answers/%s/prediction",t.indexName),data:{query:e,queryLanguages:r},cacheable:!0},s),Sbt=t=>(e,r)=>{let{query:s,paginate:a,...n}=r||{},c=0,f=()=>cSe(t)(s||"",{...n,page:c}).then(p=>{for(let[h,E]of Object.entries(p.hits))if(e(E))return{object:E,position:parseInt(h,10),page:c};if(c++,a===!1||c>=p.nbPages)throw tSe();return f()});return f()},Dbt=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Get,path:Kt.encode("1/indexes/%s/%s",t.indexName,e)},r),bbt=()=>(t,e)=>{for(let[r,s]of Object.entries(t.hits))if(s.objectID===e)return parseInt(r,10);return-1},Pbt=t=>(e,r)=>{let{attributesToRetrieve:s,...a}=r||{},n=e.map(c=>({indexName:t.indexName,objectID:c,...s?{attributesToRetrieve:s}:{}}));return t.transporter.read({method:br.MethodEnum.Post,path:"1/indexes/*/objects",data:{requests:n}},a)},xbt=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Get,path:Kt.encode("1/indexes/%s/rules/%s",t.indexName,e)},r),oSe=t=>e=>t.transporter.read({method:br.MethodEnum.Get,path:Kt.encode("1/indexes/%s/settings",t.indexName),data:{getVersion:2}},e),kbt=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Get,path:Kt.encode("1/indexes/%s/synonyms/%s",t.indexName,e)},r),aSe=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Get,path:Kt.encode("1/indexes/%s/task/%s",t.indexName,e.toString())},r),Qbt=t=>(e,r)=>Kt.createWaitablePromise(lSe(t)([e],r).then(s=>({objectID:s.objectIDs[0],taskID:s.taskIDs[0]})),(s,a)=>ds(t)(s.taskID,a)),lSe=t=>(e,r)=>{let{createIfNotExists:s,...a}=r||{},n=s?xm.PartialUpdateObject:xm.PartialUpdateObjectNoCreate;return dF(t)(e,n,a)},Tbt=t=>(e,r)=>{let{safe:s,autoGenerateObjectIDIfNotExist:a,batchSize:n,...c}=r||{},f=(I,R,N,U)=>Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/operation",I),data:{operation:N,destination:R}},U),(W,te)=>ds(t)(W.taskID,te)),p=Math.random().toString(36).substring(7),h=`${t.indexName}_tmp_${p}`,E=L9({appId:t.appId,transporter:t.transporter,indexName:h}),C=[],S=f(t.indexName,h,"copy",{...c,scope:["settings","synonyms","rules"]});C.push(S);let P=(s?S.wait(c):S).then(()=>{let I=E(e,{...c,autoGenerateObjectIDIfNotExist:a,batchSize:n});return C.push(I),s?I.wait(c):I}).then(()=>{let I=f(h,t.indexName,"move",c);return C.push(I),s?I.wait(c):I}).then(()=>Promise.all(C)).then(([I,R,N])=>({objectIDs:R.objectIDs,taskIDs:[I.taskID,...R.taskIDs,N.taskID]}));return Kt.createWaitablePromise(P,(I,R)=>Promise.all(C.map(N=>N.wait(R))))},Rbt=t=>(e,r)=>M9(t)(e,{...r,clearExistingRules:!0}),Fbt=t=>(e,r)=>_9(t)(e,{...r,clearExistingSynonyms:!0}),Nbt=t=>(e,r)=>Kt.createWaitablePromise(L9(t)([e],r).then(s=>({objectID:s.objectIDs[0],taskID:s.taskIDs[0]})),(s,a)=>ds(t)(s.taskID,a)),L9=t=>(e,r)=>{let{autoGenerateObjectIDIfNotExist:s,...a}=r||{},n=s?xm.AddObject:xm.UpdateObject;if(n===xm.UpdateObject){for(let c of e)if(c.objectID===void 0)return Kt.createWaitablePromise(Promise.reject(eSe()))}return dF(t)(e,n,a)},Obt=t=>(e,r)=>M9(t)([e],r),M9=t=>(e,r)=>{let{forwardToReplicas:s,clearExistingRules:a,...n}=r||{},c=dl.createMappedRequestOptions(n);return s&&(c.queryParameters.forwardToReplicas=1),a&&(c.queryParameters.clearExistingRules=1),Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/rules/batch",t.indexName),data:e},c),(f,p)=>ds(t)(f.taskID,p))},Lbt=t=>(e,r)=>_9(t)([e],r),_9=t=>(e,r)=>{let{forwardToReplicas:s,clearExistingSynonyms:a,replaceExistingSynonyms:n,...c}=r||{},f=dl.createMappedRequestOptions(c);return s&&(f.queryParameters.forwardToReplicas=1),(n||a)&&(f.queryParameters.replaceExistingSynonyms=1),Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/synonyms/batch",t.indexName),data:e},f),(p,h)=>ds(t)(p.taskID,h))},cSe=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/query",t.indexName),data:{query:e},cacheable:!0},r),uSe=t=>(e,r,s)=>t.transporter.read({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/facets/%s/query",t.indexName,e),data:{facetQuery:r},cacheable:!0},s),fSe=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/rules/search",t.indexName),data:{query:e}},r),ASe=t=>(e,r)=>t.transporter.read({method:br.MethodEnum.Post,path:Kt.encode("1/indexes/%s/synonyms/search",t.indexName),data:{query:e}},r),Mbt=t=>(e,r)=>{let{forwardToReplicas:s,...a}=r||{},n=dl.createMappedRequestOptions(a);return s&&(n.queryParameters.forwardToReplicas=1),Kt.createWaitablePromise(t.transporter.write({method:br.MethodEnum.Put,path:Kt.encode("1/indexes/%s/settings",t.indexName),data:e},n),(c,f)=>ds(t)(c.taskID,f))},ds=t=>(e,r)=>Kt.createRetryablePromise(s=>aSe(t)(e,r).then(a=>a.status!=="published"?s():void 0)),_bt={AddObject:"addObject",Analytics:"analytics",Browser:"browse",DeleteIndex:"deleteIndex",DeleteObject:"deleteObject",EditSettings:"editSettings",Inference:"inference",ListIndexes:"listIndexes",Logs:"logs",Personalization:"personalization",Recommendation:"recommendation",Search:"search",SeeUnretrievableAttributes:"seeUnretrievableAttributes",Settings:"settings",Usage:"usage"},xm={AddObject:"addObject",UpdateObject:"updateObject",PartialUpdateObject:"partialUpdateObject",PartialUpdateObjectNoCreate:"partialUpdateObjectNoCreate",DeleteObject:"deleteObject",DeleteIndex:"delete",ClearIndex:"clear"},mF={Settings:"settings",Synonyms:"synonyms",Rules:"rules"},Ubt={None:"none",StopIfEnoughMatches:"stopIfEnoughMatches"},Hbt={Synonym:"synonym",OneWaySynonym:"oneWaySynonym",AltCorrection1:"altCorrection1",AltCorrection2:"altCorrection2",Placeholder:"placeholder"};Ft.ApiKeyACLEnum=_bt;Ft.BatchActionEnum=xm;Ft.ScopeEnum=mF;Ft.StrategyEnum=Ubt;Ft.SynonymEnum=Hbt;Ft.addApiKey=TDt;Ft.assignUserID=RDt;Ft.assignUserIDs=FDt;Ft.batch=iSe;Ft.browseObjects=Abt;Ft.browseRules=pbt;Ft.browseSynonyms=hbt;Ft.chunkedBatch=dF;Ft.clearDictionaryEntries=NDt;Ft.clearObjects=gbt;Ft.clearRules=dbt;Ft.clearSynonyms=mbt;Ft.copyIndex=gF;Ft.copyRules=ODt;Ft.copySettings=LDt;Ft.copySynonyms=MDt;Ft.createBrowsablePromise=hF;Ft.createMissingObjectIDError=eSe;Ft.createObjectNotFoundError=tSe;Ft.createSearchClient=QDt;Ft.createValidUntilNotFoundError=rSe;Ft.customRequest=_Dt;Ft.deleteApiKey=UDt;Ft.deleteBy=ybt;Ft.deleteDictionaryEntries=HDt;Ft.deleteIndex=Ebt;Ft.deleteObject=Ibt;Ft.deleteObjects=sSe;Ft.deleteRule=Cbt;Ft.deleteSynonym=wbt;Ft.exists=Bbt;Ft.findAnswers=vbt;Ft.findObject=Sbt;Ft.generateSecuredApiKey=jDt;Ft.getApiKey=jS;Ft.getAppTask=nSe;Ft.getDictionarySettings=qDt;Ft.getLogs=GDt;Ft.getObject=Dbt;Ft.getObjectPosition=bbt;Ft.getObjects=Pbt;Ft.getRule=xbt;Ft.getSecuredApiKeyRemainingValidity=WDt;Ft.getSettings=oSe;Ft.getSynonym=kbt;Ft.getTask=aSe;Ft.getTopUserIDs=YDt;Ft.getUserID=VDt;Ft.hasPendingMappings=KDt;Ft.initIndex=qS;Ft.listApiKeys=JDt;Ft.listClusters=zDt;Ft.listIndices=ZDt;Ft.listUserIDs=XDt;Ft.moveIndex=$Dt;Ft.multipleBatch=ebt;Ft.multipleGetObjects=tbt;Ft.multipleQueries=rbt;Ft.multipleSearchForFacetValues=nbt;Ft.partialUpdateObject=Qbt;Ft.partialUpdateObjects=lSe;Ft.removeUserID=ibt;Ft.replaceAllObjects=Tbt;Ft.replaceAllRules=Rbt;Ft.replaceAllSynonyms=Fbt;Ft.replaceDictionaryEntries=sbt;Ft.restoreApiKey=obt;Ft.saveDictionaryEntries=abt;Ft.saveObject=Nbt;Ft.saveObjects=L9;Ft.saveRule=Obt;Ft.saveRules=M9;Ft.saveSynonym=Lbt;Ft.saveSynonyms=_9;Ft.search=cSe;Ft.searchDictionaryEntries=lbt;Ft.searchForFacetValues=uSe;Ft.searchRules=fSe;Ft.searchSynonyms=ASe;Ft.searchUserIDs=cbt;Ft.setDictionarySettings=ubt;Ft.setSettings=Mbt;Ft.updateApiKey=fbt;Ft.waitAppTask=aw;Ft.waitTask=ds});var gSe=L((Dpr,hSe)=>{hSe.exports=pSe()});var dSe=L(yF=>{"use strict";Object.defineProperty(yF,"__esModule",{value:!0});function jbt(){return{debug(t,e){return Promise.resolve()},info(t,e){return Promise.resolve()},error(t,e){return Promise.resolve()}}}var qbt={Debug:1,Info:2,Error:3};yF.LogLevelEnum=qbt;yF.createNullLogger=jbt});var ySe=L((Ppr,mSe)=>{mSe.exports=dSe()});var wSe=L(U9=>{"use strict";Object.defineProperty(U9,"__esModule",{value:!0});var ESe=Ie("http"),ISe=Ie("https"),Gbt=Ie("url"),CSe={keepAlive:!0},Wbt=new ESe.Agent(CSe),Ybt=new ISe.Agent(CSe);function Vbt({agent:t,httpAgent:e,httpsAgent:r,requesterOptions:s={}}={}){let a=e||t||Wbt,n=r||t||Ybt;return{send(c){return new Promise(f=>{let p=Gbt.parse(c.url),h=p.query===null?p.pathname:`${p.pathname}?${p.query}`,E={...s,agent:p.protocol==="https:"?n:a,hostname:p.hostname,path:h,method:c.method,headers:{...s&&s.headers?s.headers:{},...c.headers},...p.port!==void 0?{port:p.port||""}:{}},C=(p.protocol==="https:"?ISe:ESe).request(E,R=>{let N=[];R.on("data",U=>{N=N.concat(U)}),R.on("end",()=>{clearTimeout(P),clearTimeout(I),f({status:R.statusCode||0,content:Buffer.concat(N).toString(),isTimedOut:!1})})}),S=(R,N)=>setTimeout(()=>{C.abort(),f({status:0,content:N,isTimedOut:!0})},R*1e3),P=S(c.connectTimeout,"Connection timeout"),I;C.on("error",R=>{clearTimeout(P),clearTimeout(I),f({status:0,content:R.message,isTimedOut:!1})}),C.once("response",()=>{clearTimeout(P),I=S(c.responseTimeout,"Socket timeout")}),c.data!==void 0&&C.write(c.data),C.end()})},destroy(){return a.destroy(),n.destroy(),Promise.resolve()}}}U9.createNodeHttpRequester=Vbt});var vSe=L((kpr,BSe)=>{BSe.exports=wSe()});var PSe=L((Qpr,bSe)=>{"use strict";var SSe=wve(),Kbt=Sve(),lw=Jve(),j9=OS(),H9=$ve(),jt=gSe(),Jbt=ySe(),zbt=vSe(),Zbt=_S();function DSe(t,e,r){let s={appId:t,apiKey:e,timeouts:{connect:2,read:5,write:30},requester:zbt.createNodeHttpRequester(),logger:Jbt.createNullLogger(),responsesCache:SSe.createNullCache(),requestsCache:SSe.createNullCache(),hostsCache:Kbt.createInMemoryCache(),userAgent:Zbt.createUserAgent(j9.version).add({segment:"Node.js",version:process.versions.node})},a={...s,...r},n=()=>c=>H9.createPersonalizationClient({...s,...c,methods:{getPersonalizationStrategy:H9.getPersonalizationStrategy,setPersonalizationStrategy:H9.setPersonalizationStrategy}});return jt.createSearchClient({...a,methods:{search:jt.multipleQueries,searchForFacetValues:jt.multipleSearchForFacetValues,multipleBatch:jt.multipleBatch,multipleGetObjects:jt.multipleGetObjects,multipleQueries:jt.multipleQueries,copyIndex:jt.copyIndex,copySettings:jt.copySettings,copyRules:jt.copyRules,copySynonyms:jt.copySynonyms,moveIndex:jt.moveIndex,listIndices:jt.listIndices,getLogs:jt.getLogs,listClusters:jt.listClusters,multipleSearchForFacetValues:jt.multipleSearchForFacetValues,getApiKey:jt.getApiKey,addApiKey:jt.addApiKey,listApiKeys:jt.listApiKeys,updateApiKey:jt.updateApiKey,deleteApiKey:jt.deleteApiKey,restoreApiKey:jt.restoreApiKey,assignUserID:jt.assignUserID,assignUserIDs:jt.assignUserIDs,getUserID:jt.getUserID,searchUserIDs:jt.searchUserIDs,listUserIDs:jt.listUserIDs,getTopUserIDs:jt.getTopUserIDs,removeUserID:jt.removeUserID,hasPendingMappings:jt.hasPendingMappings,generateSecuredApiKey:jt.generateSecuredApiKey,getSecuredApiKeyRemainingValidity:jt.getSecuredApiKeyRemainingValidity,destroy:j9.destroy,clearDictionaryEntries:jt.clearDictionaryEntries,deleteDictionaryEntries:jt.deleteDictionaryEntries,getDictionarySettings:jt.getDictionarySettings,getAppTask:jt.getAppTask,replaceDictionaryEntries:jt.replaceDictionaryEntries,saveDictionaryEntries:jt.saveDictionaryEntries,searchDictionaryEntries:jt.searchDictionaryEntries,setDictionarySettings:jt.setDictionarySettings,waitAppTask:jt.waitAppTask,customRequest:jt.customRequest,initIndex:c=>f=>jt.initIndex(c)(f,{methods:{batch:jt.batch,delete:jt.deleteIndex,findAnswers:jt.findAnswers,getObject:jt.getObject,getObjects:jt.getObjects,saveObject:jt.saveObject,saveObjects:jt.saveObjects,search:jt.search,searchForFacetValues:jt.searchForFacetValues,waitTask:jt.waitTask,setSettings:jt.setSettings,getSettings:jt.getSettings,partialUpdateObject:jt.partialUpdateObject,partialUpdateObjects:jt.partialUpdateObjects,deleteObject:jt.deleteObject,deleteObjects:jt.deleteObjects,deleteBy:jt.deleteBy,clearObjects:jt.clearObjects,browseObjects:jt.browseObjects,getObjectPosition:jt.getObjectPosition,findObject:jt.findObject,exists:jt.exists,saveSynonym:jt.saveSynonym,saveSynonyms:jt.saveSynonyms,getSynonym:jt.getSynonym,searchSynonyms:jt.searchSynonyms,browseSynonyms:jt.browseSynonyms,deleteSynonym:jt.deleteSynonym,clearSynonyms:jt.clearSynonyms,replaceAllObjects:jt.replaceAllObjects,replaceAllSynonyms:jt.replaceAllSynonyms,searchRules:jt.searchRules,getRule:jt.getRule,deleteRule:jt.deleteRule,saveRule:jt.saveRule,saveRules:jt.saveRules,replaceAllRules:jt.replaceAllRules,browseRules:jt.browseRules,clearRules:jt.clearRules}}),initAnalytics:()=>c=>lw.createAnalyticsClient({...s,...c,methods:{addABTest:lw.addABTest,getABTest:lw.getABTest,getABTests:lw.getABTests,stopABTest:lw.stopABTest,deleteABTest:lw.deleteABTest}}),initPersonalization:n,initRecommendation:()=>c=>(a.logger.info("The `initRecommendation` method is deprecated. Use `initPersonalization` instead."),n()(c))}})}DSe.version=j9.version;bSe.exports=DSe});var G9=L((Tpr,q9)=>{var xSe=PSe();q9.exports=xSe;q9.exports.default=xSe});var V9=L((Fpr,TSe)=>{"use strict";var QSe=Object.getOwnPropertySymbols,$bt=Object.prototype.hasOwnProperty,ePt=Object.prototype.propertyIsEnumerable;function tPt(t){if(t==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(t)}function rPt(){try{if(!Object.assign)return!1;var t=new String("abc");if(t[5]="de",Object.getOwnPropertyNames(t)[0]==="5")return!1;for(var e={},r=0;r<10;r++)e["_"+String.fromCharCode(r)]=r;var s=Object.getOwnPropertyNames(e).map(function(n){return e[n]});if(s.join("")!=="0123456789")return!1;var a={};return"abcdefghijklmnopqrst".split("").forEach(function(n){a[n]=n}),Object.keys(Object.assign({},a)).join("")==="abcdefghijklmnopqrst"}catch{return!1}}TSe.exports=rPt()?Object.assign:function(t,e){for(var r,s=tPt(t),a,n=1;n{"use strict";var J9=V9(),cw=60103,NSe=60106;Dn.Fragment=60107;Dn.StrictMode=60108;Dn.Profiler=60114;var OSe=60109,LSe=60110,MSe=60112;Dn.Suspense=60113;var _Se=60115,USe=60116;typeof Symbol=="function"&&Symbol.for&&(Wc=Symbol.for,cw=Wc("react.element"),NSe=Wc("react.portal"),Dn.Fragment=Wc("react.fragment"),Dn.StrictMode=Wc("react.strict_mode"),Dn.Profiler=Wc("react.profiler"),OSe=Wc("react.provider"),LSe=Wc("react.context"),MSe=Wc("react.forward_ref"),Dn.Suspense=Wc("react.suspense"),_Se=Wc("react.memo"),USe=Wc("react.lazy"));var Wc,RSe=typeof Symbol=="function"&&Symbol.iterator;function nPt(t){return t===null||typeof t!="object"?null:(t=RSe&&t[RSe]||t["@@iterator"],typeof t=="function"?t:null)}function GS(t){for(var e="https://reactjs.org/docs/error-decoder.html?invariant="+t,r=1;r{"use strict";JSe.exports=KSe()});var tW=L((Lpr,eW)=>{"use strict";var Cn=eW.exports;eW.exports.default=Cn;var Zn="\x1B[",WS="\x1B]",fw="\x07",CF=";",zSe=process.env.TERM_PROGRAM==="Apple_Terminal";Cn.cursorTo=(t,e)=>{if(typeof t!="number")throw new TypeError("The `x` argument is required");return typeof e!="number"?Zn+(t+1)+"G":Zn+(e+1)+";"+(t+1)+"H"};Cn.cursorMove=(t,e)=>{if(typeof t!="number")throw new TypeError("The `x` argument is required");let r="";return t<0?r+=Zn+-t+"D":t>0&&(r+=Zn+t+"C"),e<0?r+=Zn+-e+"A":e>0&&(r+=Zn+e+"B"),r};Cn.cursorUp=(t=1)=>Zn+t+"A";Cn.cursorDown=(t=1)=>Zn+t+"B";Cn.cursorForward=(t=1)=>Zn+t+"C";Cn.cursorBackward=(t=1)=>Zn+t+"D";Cn.cursorLeft=Zn+"G";Cn.cursorSavePosition=zSe?"\x1B7":Zn+"s";Cn.cursorRestorePosition=zSe?"\x1B8":Zn+"u";Cn.cursorGetPosition=Zn+"6n";Cn.cursorNextLine=Zn+"E";Cn.cursorPrevLine=Zn+"F";Cn.cursorHide=Zn+"?25l";Cn.cursorShow=Zn+"?25h";Cn.eraseLines=t=>{let e="";for(let r=0;r[WS,"8",CF,CF,e,fw,t,WS,"8",CF,CF,fw].join("");Cn.image=(t,e={})=>{let r=`${WS}1337;File=inline=1`;return e.width&&(r+=`;width=${e.width}`),e.height&&(r+=`;height=${e.height}`),e.preserveAspectRatio===!1&&(r+=";preserveAspectRatio=0"),r+":"+t.toString("base64")+fw};Cn.iTerm={setCwd:(t=process.cwd())=>`${WS}50;CurrentDir=${t}${fw}`,annotation:(t,e={})=>{let r=`${WS}1337;`,s=typeof e.x<"u",a=typeof e.y<"u";if((s||a)&&!(s&&a&&typeof e.length<"u"))throw new Error("`x`, `y` and `length` must be defined when `x` or `y` is defined");return t=t.replace(/\|/g,""),r+=e.isHidden?"AddHiddenAnnotation=":"AddAnnotation=",e.length>0?r+=(s?[t,e.length,e.x,e.y]:[e.length,t]).join("|"):r+=t,r+fw}}});var XSe=L((Mpr,rW)=>{"use strict";var ZSe=(t,e)=>{for(let r of Reflect.ownKeys(e))Object.defineProperty(t,r,Object.getOwnPropertyDescriptor(e,r));return t};rW.exports=ZSe;rW.exports.default=ZSe});var eDe=L((_pr,BF)=>{"use strict";var lPt=XSe(),wF=new WeakMap,$Se=(t,e={})=>{if(typeof t!="function")throw new TypeError("Expected a function");let r,s=0,a=t.displayName||t.name||"",n=function(...c){if(wF.set(n,++s),s===1)r=t.apply(this,c),t=null;else if(e.throw===!0)throw new Error(`Function \`${a}\` can only be called once`);return r};return lPt(n,t),wF.set(n,s),n};BF.exports=$Se;BF.exports.default=$Se;BF.exports.callCount=t=>{if(!wF.has(t))throw new Error(`The given function \`${t.name}\` is not wrapped by the \`onetime\` package`);return wF.get(t)}});var tDe=L((Upr,vF)=>{vF.exports=["SIGABRT","SIGALRM","SIGHUP","SIGINT","SIGTERM"];process.platform!=="win32"&&vF.exports.push("SIGVTALRM","SIGXCPU","SIGXFSZ","SIGUSR2","SIGTRAP","SIGSYS","SIGQUIT","SIGIOT");process.platform==="linux"&&vF.exports.push("SIGIO","SIGPOLL","SIGPWR","SIGSTKFLT","SIGUNUSED")});var sW=L((Hpr,hw)=>{var Ti=global.process,km=function(t){return t&&typeof t=="object"&&typeof t.removeListener=="function"&&typeof t.emit=="function"&&typeof t.reallyExit=="function"&&typeof t.listeners=="function"&&typeof t.kill=="function"&&typeof t.pid=="number"&&typeof t.on=="function"};km(Ti)?(rDe=Ie("assert"),Aw=tDe(),nDe=/^win/i.test(Ti.platform),YS=Ie("events"),typeof YS!="function"&&(YS=YS.EventEmitter),Ti.__signal_exit_emitter__?zs=Ti.__signal_exit_emitter__:(zs=Ti.__signal_exit_emitter__=new YS,zs.count=0,zs.emitted={}),zs.infinite||(zs.setMaxListeners(1/0),zs.infinite=!0),hw.exports=function(t,e){if(!km(global.process))return function(){};rDe.equal(typeof t,"function","a callback must be provided for exit handler"),pw===!1&&nW();var r="exit";e&&e.alwaysLast&&(r="afterexit");var s=function(){zs.removeListener(r,t),zs.listeners("exit").length===0&&zs.listeners("afterexit").length===0&&SF()};return zs.on(r,t),s},SF=function(){!pw||!km(global.process)||(pw=!1,Aw.forEach(function(e){try{Ti.removeListener(e,DF[e])}catch{}}),Ti.emit=bF,Ti.reallyExit=iW,zs.count-=1)},hw.exports.unload=SF,Qm=function(e,r,s){zs.emitted[e]||(zs.emitted[e]=!0,zs.emit(e,r,s))},DF={},Aw.forEach(function(t){DF[t]=function(){if(km(global.process)){var r=Ti.listeners(t);r.length===zs.count&&(SF(),Qm("exit",null,t),Qm("afterexit",null,t),nDe&&t==="SIGHUP"&&(t="SIGINT"),Ti.kill(Ti.pid,t))}}}),hw.exports.signals=function(){return Aw},pw=!1,nW=function(){pw||!km(global.process)||(pw=!0,zs.count+=1,Aw=Aw.filter(function(e){try{return Ti.on(e,DF[e]),!0}catch{return!1}}),Ti.emit=sDe,Ti.reallyExit=iDe)},hw.exports.load=nW,iW=Ti.reallyExit,iDe=function(e){km(global.process)&&(Ti.exitCode=e||0,Qm("exit",Ti.exitCode,null),Qm("afterexit",Ti.exitCode,null),iW.call(Ti,Ti.exitCode))},bF=Ti.emit,sDe=function(e,r){if(e==="exit"&&km(global.process)){r!==void 0&&(Ti.exitCode=r);var s=bF.apply(this,arguments);return Qm("exit",Ti.exitCode,null),Qm("afterexit",Ti.exitCode,null),s}else return bF.apply(this,arguments)}):hw.exports=function(){return function(){}};var rDe,Aw,nDe,YS,zs,SF,Qm,DF,pw,nW,iW,iDe,bF,sDe});var aDe=L((jpr,oDe)=>{"use strict";var cPt=eDe(),uPt=sW();oDe.exports=cPt(()=>{uPt(()=>{process.stderr.write("\x1B[?25h")},{alwaysLast:!0})})});var oW=L(gw=>{"use strict";var fPt=aDe(),PF=!1;gw.show=(t=process.stderr)=>{t.isTTY&&(PF=!1,t.write("\x1B[?25h"))};gw.hide=(t=process.stderr)=>{t.isTTY&&(fPt(),PF=!0,t.write("\x1B[?25l"))};gw.toggle=(t,e)=>{t!==void 0&&(PF=t),PF?gw.show(e):gw.hide(e)}});var fDe=L(VS=>{"use strict";var uDe=VS&&VS.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(VS,"__esModule",{value:!0});var lDe=uDe(tW()),cDe=uDe(oW()),APt=(t,{showCursor:e=!1}={})=>{let r=0,s="",a=!1,n=c=>{!e&&!a&&(cDe.default.hide(),a=!0);let f=c+` `;f!==s&&(s=f,t.write(lDe.default.eraseLines(r)+f),r=f.split(` `).length)};return n.clear=()=>{t.write(lDe.default.eraseLines(r)),s="",r=0},n.done=()=>{s="",r=0,e||(cDe.default.show(),a=!1)},n};VS.default={create:APt}});var ADe=L((Wpr,pPt)=>{pPt.exports=[{name:"AppVeyor",constant:"APPVEYOR",env:"APPVEYOR",pr:"APPVEYOR_PULL_REQUEST_NUMBER"},{name:"Azure Pipelines",constant:"AZURE_PIPELINES",env:"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI",pr:"SYSTEM_PULLREQUEST_PULLREQUESTID"},{name:"Bamboo",constant:"BAMBOO",env:"bamboo_planKey"},{name:"Bitbucket Pipelines",constant:"BITBUCKET",env:"BITBUCKET_COMMIT",pr:"BITBUCKET_PR_ID"},{name:"Bitrise",constant:"BITRISE",env:"BITRISE_IO",pr:"BITRISE_PULL_REQUEST"},{name:"Buddy",constant:"BUDDY",env:"BUDDY_WORKSPACE_ID",pr:"BUDDY_EXECUTION_PULL_REQUEST_ID"},{name:"Buildkite",constant:"BUILDKITE",env:"BUILDKITE",pr:{env:"BUILDKITE_PULL_REQUEST",ne:"false"}},{name:"CircleCI",constant:"CIRCLE",env:"CIRCLECI",pr:"CIRCLE_PULL_REQUEST"},{name:"Cirrus CI",constant:"CIRRUS",env:"CIRRUS_CI",pr:"CIRRUS_PR"},{name:"AWS CodeBuild",constant:"CODEBUILD",env:"CODEBUILD_BUILD_ARN"},{name:"Codeship",constant:"CODESHIP",env:{CI_NAME:"codeship"}},{name:"Drone",constant:"DRONE",env:"DRONE",pr:{DRONE_BUILD_EVENT:"pull_request"}},{name:"dsari",constant:"DSARI",env:"DSARI"},{name:"GitLab CI",constant:"GITLAB",env:"GITLAB_CI"},{name:"GoCD",constant:"GOCD",env:"GO_PIPELINE_LABEL"},{name:"Hudson",constant:"HUDSON",env:"HUDSON_URL"},{name:"Jenkins",constant:"JENKINS",env:["JENKINS_URL","BUILD_ID"],pr:{any:["ghprbPullId","CHANGE_ID"]}},{name:"Magnum CI",constant:"MAGNUM",env:"MAGNUM"},{name:"Netlify CI",constant:"NETLIFY",env:"NETLIFY_BUILD_BASE",pr:{env:"PULL_REQUEST",ne:"false"}},{name:"Sail CI",constant:"SAIL",env:"SAILCI",pr:"SAIL_PULL_REQUEST_NUMBER"},{name:"Semaphore",constant:"SEMAPHORE",env:"SEMAPHORE",pr:"PULL_REQUEST_NUMBER"},{name:"Shippable",constant:"SHIPPABLE",env:"SHIPPABLE",pr:{IS_PULL_REQUEST:"true"}},{name:"Solano CI",constant:"SOLANO",env:"TDDIUM",pr:"TDDIUM_PR_ID"},{name:"Strider CD",constant:"STRIDER",env:"STRIDER"},{name:"TaskCluster",constant:"TASKCLUSTER",env:["TASK_ID","RUN_ID"]},{name:"TeamCity",constant:"TEAMCITY",env:"TEAMCITY_VERSION"},{name:"Travis CI",constant:"TRAVIS",env:"TRAVIS",pr:{env:"TRAVIS_PULL_REQUEST",ne:"false"}}]});var gDe=L(rc=>{"use strict";var hDe=ADe(),AA=process.env;Object.defineProperty(rc,"_vendors",{value:hDe.map(function(t){return t.constant})});rc.name=null;rc.isPR=null;hDe.forEach(function(t){var e=Array.isArray(t.env)?t.env:[t.env],r=e.every(function(s){return pDe(s)});if(rc[t.constant]=r,r)switch(rc.name=t.name,typeof t.pr){case"string":rc.isPR=!!AA[t.pr];break;case"object":"env"in t.pr?rc.isPR=t.pr.env in AA&&AA[t.pr.env]!==t.pr.ne:"any"in t.pr?rc.isPR=t.pr.any.some(function(s){return!!AA[s]}):rc.isPR=pDe(t.pr);break;default:rc.isPR=null}});rc.isCI=!!(AA.CI||AA.CONTINUOUS_INTEGRATION||AA.BUILD_NUMBER||AA.RUN_ID||rc.name);function pDe(t){return typeof t=="string"?!!AA[t]:Object.keys(t).every(function(e){return AA[e]===t[e]})}});var mDe=L((Vpr,dDe)=>{"use strict";dDe.exports=gDe().isCI});var EDe=L((Kpr,yDe)=>{"use strict";var hPt=t=>{let e=new Set;do for(let r of Reflect.ownKeys(t))e.add([t,r]);while((t=Reflect.getPrototypeOf(t))&&t!==Object.prototype);return e};yDe.exports=(t,{include:e,exclude:r}={})=>{let s=a=>{let n=c=>typeof c=="string"?a===c:c.test(a);return e?e.some(n):r?!r.some(n):!0};for(let[a,n]of hPt(t.constructor.prototype)){if(n==="constructor"||!s(n))continue;let c=Reflect.getOwnPropertyDescriptor(a,n);c&&typeof c.value=="function"&&(t[n]=t[n].bind(t))}return t}});var SDe=L(Vn=>{"use strict";var mw,zS,TF,pW;typeof performance=="object"&&typeof performance.now=="function"?(IDe=performance,Vn.unstable_now=function(){return IDe.now()}):(aW=Date,CDe=aW.now(),Vn.unstable_now=function(){return aW.now()-CDe});var IDe,aW,CDe;typeof window>"u"||typeof MessageChannel!="function"?(dw=null,lW=null,cW=function(){if(dw!==null)try{var t=Vn.unstable_now();dw(!0,t),dw=null}catch(e){throw setTimeout(cW,0),e}},mw=function(t){dw!==null?setTimeout(mw,0,t):(dw=t,setTimeout(cW,0))},zS=function(t,e){lW=setTimeout(t,e)},TF=function(){clearTimeout(lW)},Vn.unstable_shouldYield=function(){return!1},pW=Vn.unstable_forceFrameRate=function(){}):(wDe=window.setTimeout,BDe=window.clearTimeout,typeof console<"u"&&(vDe=window.cancelAnimationFrame,typeof window.requestAnimationFrame!="function"&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),typeof vDe!="function"&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")),KS=!1,JS=null,xF=-1,uW=5,fW=0,Vn.unstable_shouldYield=function(){return Vn.unstable_now()>=fW},pW=function(){},Vn.unstable_forceFrameRate=function(t){0>t||125>>1,a=t[s];if(a!==void 0&&0QF(c,r))p!==void 0&&0>QF(p,c)?(t[s]=p,t[f]=r,s=f):(t[s]=c,t[n]=r,s=n);else if(p!==void 0&&0>QF(p,r))t[s]=p,t[f]=r,s=f;else break e}}return e}return null}function QF(t,e){var r=t.sortIndex-e.sortIndex;return r!==0?r:t.id-e.id}var pA=[],Z0=[],gPt=1,Yc=null,ea=3,FF=!1,Tm=!1,ZS=!1;function gW(t){for(var e=tf(Z0);e!==null;){if(e.callback===null)RF(Z0);else if(e.startTime<=t)RF(Z0),e.sortIndex=e.expirationTime,hW(pA,e);else break;e=tf(Z0)}}function dW(t){if(ZS=!1,gW(t),!Tm)if(tf(pA)!==null)Tm=!0,mw(mW);else{var e=tf(Z0);e!==null&&zS(dW,e.startTime-t)}}function mW(t,e){Tm=!1,ZS&&(ZS=!1,TF()),FF=!0;var r=ea;try{for(gW(e),Yc=tf(pA);Yc!==null&&(!(Yc.expirationTime>e)||t&&!Vn.unstable_shouldYield());){var s=Yc.callback;if(typeof s=="function"){Yc.callback=null,ea=Yc.priorityLevel;var a=s(Yc.expirationTime<=e);e=Vn.unstable_now(),typeof a=="function"?Yc.callback=a:Yc===tf(pA)&&RF(pA),gW(e)}else RF(pA);Yc=tf(pA)}if(Yc!==null)var n=!0;else{var c=tf(Z0);c!==null&&zS(dW,c.startTime-e),n=!1}return n}finally{Yc=null,ea=r,FF=!1}}var dPt=pW;Vn.unstable_IdlePriority=5;Vn.unstable_ImmediatePriority=1;Vn.unstable_LowPriority=4;Vn.unstable_NormalPriority=3;Vn.unstable_Profiling=null;Vn.unstable_UserBlockingPriority=2;Vn.unstable_cancelCallback=function(t){t.callback=null};Vn.unstable_continueExecution=function(){Tm||FF||(Tm=!0,mw(mW))};Vn.unstable_getCurrentPriorityLevel=function(){return ea};Vn.unstable_getFirstCallbackNode=function(){return tf(pA)};Vn.unstable_next=function(t){switch(ea){case 1:case 2:case 3:var e=3;break;default:e=ea}var r=ea;ea=e;try{return t()}finally{ea=r}};Vn.unstable_pauseExecution=function(){};Vn.unstable_requestPaint=dPt;Vn.unstable_runWithPriority=function(t,e){switch(t){case 1:case 2:case 3:case 4:case 5:break;default:t=3}var r=ea;ea=t;try{return e()}finally{ea=r}};Vn.unstable_scheduleCallback=function(t,e,r){var s=Vn.unstable_now();switch(typeof r=="object"&&r!==null?(r=r.delay,r=typeof r=="number"&&0s?(t.sortIndex=r,hW(Z0,t),tf(pA)===null&&t===tf(Z0)&&(ZS?TF():ZS=!0,zS(dW,r-s))):(t.sortIndex=a,hW(pA,t),Tm||FF||(Tm=!0,mw(mW))),t};Vn.unstable_wrapCallback=function(t){var e=ea;return function(){var r=ea;ea=e;try{return t.apply(this,arguments)}finally{ea=r}}}});var yW=L((zpr,DDe)=>{"use strict";DDe.exports=SDe()});var bDe=L((Zpr,XS)=>{XS.exports=function(e){var r={},s=V9(),a=hn(),n=yW();function c(v){for(var D="https://reactjs.org/docs/error-decoder.html?invariant="+v,Q=1;QUe||V[Se]!==ne[Ue])return` `+V[Se].replace(" at new "," at ");while(1<=Se&&0<=Ue);break}}}finally{ve=!1,Error.prepareStackTrace=Q}return(v=v?v.displayName||v.name:"")?ac(v):""}var lc=[],Li=-1;function so(v){return{current:v}}function Rt(v){0>Li||(v.current=lc[Li],lc[Li]=null,Li--)}function xn(v,D){Li++,lc[Li]=v.current,v.current=D}var ca={},qi=so(ca),Mi=so(!1),Oa=ca;function dn(v,D){var Q=v.type.contextTypes;if(!Q)return ca;var H=v.stateNode;if(H&&H.__reactInternalMemoizedUnmaskedChildContext===D)return H.__reactInternalMemoizedMaskedChildContext;var V={},ne;for(ne in Q)V[ne]=D[ne];return H&&(v=v.stateNode,v.__reactInternalMemoizedUnmaskedChildContext=D,v.__reactInternalMemoizedMaskedChildContext=V),V}function Jn(v){return v=v.childContextTypes,v!=null}function hu(){Rt(Mi),Rt(qi)}function Ih(v,D,Q){if(qi.current!==ca)throw Error(c(168));xn(qi,D),xn(Mi,Q)}function La(v,D,Q){var H=v.stateNode;if(v=D.childContextTypes,typeof H.getChildContext!="function")return Q;H=H.getChildContext();for(var V in H)if(!(V in v))throw Error(c(108,g(D)||"Unknown",V));return s({},Q,H)}function Ma(v){return v=(v=v.stateNode)&&v.__reactInternalMemoizedMergedChildContext||ca,Oa=qi.current,xn(qi,v),xn(Mi,Mi.current),!0}function Ua(v,D,Q){var H=v.stateNode;if(!H)throw Error(c(169));Q?(v=La(v,D,Oa),H.__reactInternalMemoizedMergedChildContext=v,Rt(Mi),Rt(qi),xn(qi,v)):Rt(Mi),xn(Mi,Q)}var Xe=null,Ha=null,gf=n.unstable_now;gf();var cc=0,wn=8;function ua(v){if(1&v)return wn=15,1;if(2&v)return wn=14,2;if(4&v)return wn=13,4;var D=24&v;return D!==0?(wn=12,D):v&32?(wn=11,32):(D=192&v,D!==0?(wn=10,D):v&256?(wn=9,256):(D=3584&v,D!==0?(wn=8,D):v&4096?(wn=7,4096):(D=4186112&v,D!==0?(wn=6,D):(D=62914560&v,D!==0?(wn=5,D):v&67108864?(wn=4,67108864):v&134217728?(wn=3,134217728):(D=805306368&v,D!==0?(wn=2,D):1073741824&v?(wn=1,1073741824):(wn=8,v))))))}function _A(v){switch(v){case 99:return 15;case 98:return 10;case 97:case 96:return 8;case 95:return 2;default:return 0}}function UA(v){switch(v){case 15:case 14:return 99;case 13:case 12:case 11:case 10:return 98;case 9:case 8:case 7:case 6:case 4:case 5:return 97;case 3:case 2:case 1:return 95;case 0:return 90;default:throw Error(c(358,v))}}function fa(v,D){var Q=v.pendingLanes;if(Q===0)return wn=0;var H=0,V=0,ne=v.expiredLanes,Se=v.suspendedLanes,Ue=v.pingedLanes;if(ne!==0)H=ne,V=wn=15;else if(ne=Q&134217727,ne!==0){var At=ne&~Se;At!==0?(H=ua(At),V=wn):(Ue&=ne,Ue!==0&&(H=ua(Ue),V=wn))}else ne=Q&~Se,ne!==0?(H=ua(ne),V=wn):Ue!==0&&(H=ua(Ue),V=wn);if(H===0)return 0;if(H=31-is(H),H=Q&((0>H?0:1<Q;Q++)D.push(v);return D}function ja(v,D,Q){v.pendingLanes|=D;var H=D-1;v.suspendedLanes&=H,v.pingedLanes&=H,v=v.eventTimes,D=31-is(D),v[D]=Q}var is=Math.clz32?Math.clz32:fc,uc=Math.log,gu=Math.LN2;function fc(v){return v===0?32:31-(uc(v)/gu|0)|0}var qa=n.unstable_runWithPriority,_i=n.unstable_scheduleCallback,ws=n.unstable_cancelCallback,Sl=n.unstable_shouldYield,df=n.unstable_requestPaint,Ac=n.unstable_now,Bi=n.unstable_getCurrentPriorityLevel,Qn=n.unstable_ImmediatePriority,pc=n.unstable_UserBlockingPriority,Je=n.unstable_NormalPriority,st=n.unstable_LowPriority,St=n.unstable_IdlePriority,lr={},ee=df!==void 0?df:function(){},Ee=null,Oe=null,gt=!1,yt=Ac(),Dt=1e4>yt?Ac:function(){return Ac()-yt};function tr(){switch(Bi()){case Qn:return 99;case pc:return 98;case Je:return 97;case st:return 96;case St:return 95;default:throw Error(c(332))}}function fn(v){switch(v){case 99:return Qn;case 98:return pc;case 97:return Je;case 96:return st;case 95:return St;default:throw Error(c(332))}}function li(v,D){return v=fn(v),qa(v,D)}function Gi(v,D,Q){return v=fn(v),_i(v,D,Q)}function Tn(){if(Oe!==null){var v=Oe;Oe=null,ws(v)}Ga()}function Ga(){if(!gt&&Ee!==null){gt=!0;var v=0;try{var D=Ee;li(99,function(){for(;vRn?(Un=kr,kr=null):Un=kr.sibling;var zr=Xt($e,kr,ht[Rn],Zt);if(zr===null){kr===null&&(kr=Un);break}v&&kr&&zr.alternate===null&&D($e,kr),qe=ne(zr,qe,Rn),Xn===null?Sr=zr:Xn.sibling=zr,Xn=zr,kr=Un}if(Rn===ht.length)return Q($e,kr),Sr;if(kr===null){for(;RnRn?(Un=kr,kr=null):Un=kr.sibling;var ci=Xt($e,kr,zr.value,Zt);if(ci===null){kr===null&&(kr=Un);break}v&&kr&&ci.alternate===null&&D($e,kr),qe=ne(ci,qe,Rn),Xn===null?Sr=ci:Xn.sibling=ci,Xn=ci,kr=Un}if(zr.done)return Q($e,kr),Sr;if(kr===null){for(;!zr.done;Rn++,zr=ht.next())zr=Lr($e,zr.value,Zt),zr!==null&&(qe=ne(zr,qe,Rn),Xn===null?Sr=zr:Xn.sibling=zr,Xn=zr);return Sr}for(kr=H($e,kr);!zr.done;Rn++,zr=ht.next())zr=zn(kr,$e,Rn,zr.value,Zt),zr!==null&&(v&&zr.alternate!==null&&kr.delete(zr.key===null?Rn:zr.key),qe=ne(zr,qe,Rn),Xn===null?Sr=zr:Xn.sibling=zr,Xn=zr);return v&&kr.forEach(function(Pu){return D($e,Pu)}),Sr}return function($e,qe,ht,Zt){var Sr=typeof ht=="object"&&ht!==null&&ht.type===E&&ht.key===null;Sr&&(ht=ht.props.children);var Xn=typeof ht=="object"&&ht!==null;if(Xn)switch(ht.$$typeof){case p:e:{for(Xn=ht.key,Sr=qe;Sr!==null;){if(Sr.key===Xn){switch(Sr.tag){case 7:if(ht.type===E){Q($e,Sr.sibling),qe=V(Sr,ht.props.children),qe.return=$e,$e=qe;break e}break;default:if(Sr.elementType===ht.type){Q($e,Sr.sibling),qe=V(Sr,ht.props),qe.ref=mt($e,Sr,ht),qe.return=$e,$e=qe;break e}}Q($e,Sr);break}else D($e,Sr);Sr=Sr.sibling}ht.type===E?(qe=Qf(ht.props.children,$e.mode,Zt,ht.key),qe.return=$e,$e=qe):(Zt=id(ht.type,ht.key,ht.props,null,$e.mode,Zt),Zt.ref=mt($e,qe,ht),Zt.return=$e,$e=Zt)}return Se($e);case h:e:{for(Sr=ht.key;qe!==null;){if(qe.key===Sr)if(qe.tag===4&&qe.stateNode.containerInfo===ht.containerInfo&&qe.stateNode.implementation===ht.implementation){Q($e,qe.sibling),qe=V(qe,ht.children||[]),qe.return=$e,$e=qe;break e}else{Q($e,qe);break}else D($e,qe);qe=qe.sibling}qe=Ro(ht,$e.mode,Zt),qe.return=$e,$e=qe}return Se($e)}if(typeof ht=="string"||typeof ht=="number")return ht=""+ht,qe!==null&&qe.tag===6?(Q($e,qe.sibling),qe=V(qe,ht),qe.return=$e,$e=qe):(Q($e,qe),qe=b2(ht,$e.mode,Zt),qe.return=$e,$e=qe),Se($e);if(yf(ht))return yi($e,qe,ht,Zt);if(Ce(ht))return Za($e,qe,ht,Zt);if(Xn&&mu($e,ht),typeof ht>"u"&&!Sr)switch($e.tag){case 1:case 22:case 0:case 11:case 15:throw Error(c(152,g($e.type)||"Component"))}return Q($e,qe)}}var Lg=Cy(!0),e2=Cy(!1),Dh={},ur=so(Dh),Zi=so(Dh),Ef=so(Dh);function Wa(v){if(v===Dh)throw Error(c(174));return v}function Mg(v,D){xn(Ef,D),xn(Zi,v),xn(ur,Dh),v=dt(D),Rt(ur),xn(ur,v)}function yu(){Rt(ur),Rt(Zi),Rt(Ef)}function If(v){var D=Wa(Ef.current),Q=Wa(ur.current);D=j(Q,v.type,D),Q!==D&&(xn(Zi,v),xn(ur,D))}function wt(v){Zi.current===v&&(Rt(ur),Rt(Zi))}var di=so(0);function WA(v){for(var D=v;D!==null;){if(D.tag===13){var Q=D.memoizedState;if(Q!==null&&(Q=Q.dehydrated,Q===null||gr(Q)||So(Q)))return D}else if(D.tag===19&&D.memoizedProps.revealOrder!==void 0){if(D.flags&64)return D}else if(D.child!==null){D.child.return=D,D=D.child;continue}if(D===v)break;for(;D.sibling===null;){if(D.return===null||D.return===v)return null;D=D.return}D.sibling.return=D.return,D=D.sibling}return null}var Ya=null,pa=null,Va=!1;function _g(v,D){var Q=za(5,null,null,0);Q.elementType="DELETED",Q.type="DELETED",Q.stateNode=D,Q.return=v,Q.flags=8,v.lastEffect!==null?(v.lastEffect.nextEffect=Q,v.lastEffect=Q):v.firstEffect=v.lastEffect=Q}function bh(v,D){switch(v.tag){case 5:return D=la(D,v.type,v.pendingProps),D!==null?(v.stateNode=D,!0):!1;case 6:return D=OA(D,v.pendingProps),D!==null?(v.stateNode=D,!0):!1;case 13:return!1;default:return!1}}function Ug(v){if(Va){var D=pa;if(D){var Q=D;if(!bh(v,D)){if(D=Me(Q),!D||!bh(v,D)){v.flags=v.flags&-1025|2,Va=!1,Ya=v;return}_g(Ya,Q)}Ya=v,pa=fu(D)}else v.flags=v.flags&-1025|2,Va=!1,Ya=v}}function wy(v){for(v=v.return;v!==null&&v.tag!==5&&v.tag!==3&&v.tag!==13;)v=v.return;Ya=v}function YA(v){if(!Z||v!==Ya)return!1;if(!Va)return wy(v),Va=!0,!1;var D=v.type;if(v.tag!==5||D!=="head"&&D!=="body"&&!it(D,v.memoizedProps))for(D=pa;D;)_g(v,D),D=Me(D);if(wy(v),v.tag===13){if(!Z)throw Error(c(316));if(v=v.memoizedState,v=v!==null?v.dehydrated:null,!v)throw Error(c(317));pa=LA(v)}else pa=Ya?Me(v.stateNode):null;return!0}function Hg(){Z&&(pa=Ya=null,Va=!1)}var Eu=[];function Iu(){for(var v=0;vne))throw Error(c(301));ne+=1,ki=ss=null,D.updateQueue=null,Cf.current=re,v=Q(H,V)}while(wf)}if(Cf.current=kt,D=ss!==null&&ss.next!==null,Cu=0,ki=ss=qn=null,VA=!1,D)throw Error(c(300));return v}function os(){var v={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return ki===null?qn.memoizedState=ki=v:ki=ki.next=v,ki}function xl(){if(ss===null){var v=qn.alternate;v=v!==null?v.memoizedState:null}else v=ss.next;var D=ki===null?qn.memoizedState:ki.next;if(D!==null)ki=D,ss=v;else{if(v===null)throw Error(c(310));ss=v,v={memoizedState:ss.memoizedState,baseState:ss.baseState,baseQueue:ss.baseQueue,queue:ss.queue,next:null},ki===null?qn.memoizedState=ki=v:ki=ki.next=v}return ki}function ko(v,D){return typeof D=="function"?D(v):D}function Bf(v){var D=xl(),Q=D.queue;if(Q===null)throw Error(c(311));Q.lastRenderedReducer=v;var H=ss,V=H.baseQueue,ne=Q.pending;if(ne!==null){if(V!==null){var Se=V.next;V.next=ne.next,ne.next=Se}H.baseQueue=V=ne,Q.pending=null}if(V!==null){V=V.next,H=H.baseState;var Ue=Se=ne=null,At=V;do{var Gt=At.lane;if((Cu&Gt)===Gt)Ue!==null&&(Ue=Ue.next={lane:0,action:At.action,eagerReducer:At.eagerReducer,eagerState:At.eagerState,next:null}),H=At.eagerReducer===v?At.eagerState:v(H,At.action);else{var vr={lane:Gt,action:At.action,eagerReducer:At.eagerReducer,eagerState:At.eagerState,next:null};Ue===null?(Se=Ue=vr,ne=H):Ue=Ue.next=vr,qn.lanes|=Gt,Zg|=Gt}At=At.next}while(At!==null&&At!==V);Ue===null?ne=H:Ue.next=Se,Do(H,D.memoizedState)||(Ke=!0),D.memoizedState=H,D.baseState=ne,D.baseQueue=Ue,Q.lastRenderedState=H}return[D.memoizedState,Q.dispatch]}function vf(v){var D=xl(),Q=D.queue;if(Q===null)throw Error(c(311));Q.lastRenderedReducer=v;var H=Q.dispatch,V=Q.pending,ne=D.memoizedState;if(V!==null){Q.pending=null;var Se=V=V.next;do ne=v(ne,Se.action),Se=Se.next;while(Se!==V);Do(ne,D.memoizedState)||(Ke=!0),D.memoizedState=ne,D.baseQueue===null&&(D.baseState=ne),Q.lastRenderedState=ne}return[ne,H]}function kl(v,D,Q){var H=D._getVersion;H=H(D._source);var V=y?D._workInProgressVersionPrimary:D._workInProgressVersionSecondary;if(V!==null?v=V===H:(v=v.mutableReadLanes,(v=(Cu&v)===v)&&(y?D._workInProgressVersionPrimary=H:D._workInProgressVersionSecondary=H,Eu.push(D))),v)return Q(D._source);throw Eu.push(D),Error(c(350))}function yn(v,D,Q,H){var V=ao;if(V===null)throw Error(c(349));var ne=D._getVersion,Se=ne(D._source),Ue=Cf.current,At=Ue.useState(function(){return kl(V,D,Q)}),Gt=At[1],vr=At[0];At=ki;var Lr=v.memoizedState,Xt=Lr.refs,zn=Xt.getSnapshot,yi=Lr.source;Lr=Lr.subscribe;var Za=qn;return v.memoizedState={refs:Xt,source:D,subscribe:H},Ue.useEffect(function(){Xt.getSnapshot=Q,Xt.setSnapshot=Gt;var $e=ne(D._source);if(!Do(Se,$e)){$e=Q(D._source),Do(vr,$e)||(Gt($e),$e=Ss(Za),V.mutableReadLanes|=$e&V.pendingLanes),$e=V.mutableReadLanes,V.entangledLanes|=$e;for(var qe=V.entanglements,ht=$e;0Q?98:Q,function(){v(!0)}),li(97m2&&(D.flags|=64,V=!0,$A(H,!1),D.lanes=33554432)}else{if(!V)if(v=WA(ne),v!==null){if(D.flags|=64,V=!0,v=v.updateQueue,v!==null&&(D.updateQueue=v,D.flags|=4),$A(H,!0),H.tail===null&&H.tailMode==="hidden"&&!ne.alternate&&!Va)return D=D.lastEffect=H.lastEffect,D!==null&&(D.nextEffect=null),null}else 2*Dt()-H.renderingStartTime>m2&&Q!==1073741824&&(D.flags|=64,V=!0,$A(H,!1),D.lanes=33554432);H.isBackwards?(ne.sibling=D.child,D.child=ne):(v=H.last,v!==null?v.sibling=ne:D.child=ne,H.last=ne)}return H.tail!==null?(v=H.tail,H.rendering=v,H.tail=v.sibling,H.lastEffect=D.lastEffect,H.renderingStartTime=Dt(),v.sibling=null,D=di.current,xn(di,V?D&1|2:D&1),v):null;case 23:case 24:return B2(),v!==null&&v.memoizedState!==null!=(D.memoizedState!==null)&&H.mode!=="unstable-defer-without-hiding"&&(D.flags|=4),null}throw Error(c(156,D.tag))}function YL(v){switch(v.tag){case 1:Jn(v.type)&&hu();var D=v.flags;return D&4096?(v.flags=D&-4097|64,v):null;case 3:if(yu(),Rt(Mi),Rt(qi),Iu(),D=v.flags,D&64)throw Error(c(285));return v.flags=D&-4097|64,v;case 5:return wt(v),null;case 13:return Rt(di),D=v.flags,D&4096?(v.flags=D&-4097|64,v):null;case 19:return Rt(di),null;case 4:return yu(),null;case 10:return Ng(v),null;case 23:case 24:return B2(),null;default:return null}}function Wg(v,D){try{var Q="",H=D;do Q+=$1(H),H=H.return;while(H);var V=Q}catch(ne){V=` Error generating stack: `+ne.message+` `+ne.stack}return{value:v,source:D,stack:V}}function Yg(v,D){try{console.error(D.value)}catch(Q){setTimeout(function(){throw Q})}}var VL=typeof WeakMap=="function"?WeakMap:Map;function i2(v,D,Q){Q=bl(-1,Q),Q.tag=3,Q.payload={element:null};var H=D.value;return Q.callback=function(){My||(My=!0,y2=H),Yg(v,D)},Q}function Vg(v,D,Q){Q=bl(-1,Q),Q.tag=3;var H=v.type.getDerivedStateFromError;if(typeof H=="function"){var V=D.value;Q.payload=function(){return Yg(v,D),H(V)}}var ne=v.stateNode;return ne!==null&&typeof ne.componentDidCatch=="function"&&(Q.callback=function(){typeof H!="function"&&(gc===null?gc=new Set([this]):gc.add(this),Yg(v,D));var Se=D.stack;this.componentDidCatch(D.value,{componentStack:Se!==null?Se:""})}),Q}var KL=typeof WeakSet=="function"?WeakSet:Set;function s2(v){var D=v.ref;if(D!==null)if(typeof D=="function")try{D(null)}catch(Q){kf(v,Q)}else D.current=null}function by(v,D){switch(D.tag){case 0:case 11:case 15:case 22:return;case 1:if(D.flags&256&&v!==null){var Q=v.memoizedProps,H=v.memoizedState;v=D.stateNode,D=v.getSnapshotBeforeUpdate(D.elementType===D.type?Q:bo(D.type,Q),H),v.__reactInternalSnapshotBeforeUpdate=D}return;case 3:F&&D.flags&256&&Fs(D.stateNode.containerInfo);return;case 5:case 6:case 4:case 17:return}throw Error(c(163))}function Fh(v,D){if(D=D.updateQueue,D=D!==null?D.lastEffect:null,D!==null){var Q=D=D.next;do{if((Q.tag&v)===v){var H=Q.destroy;Q.destroy=void 0,H!==void 0&&H()}Q=Q.next}while(Q!==D)}}function yP(v,D,Q){switch(Q.tag){case 0:case 11:case 15:case 22:if(D=Q.updateQueue,D=D!==null?D.lastEffect:null,D!==null){v=D=D.next;do{if((v.tag&3)===3){var H=v.create;v.destroy=H()}v=v.next}while(v!==D)}if(D=Q.updateQueue,D=D!==null?D.lastEffect:null,D!==null){v=D=D.next;do{var V=v;H=V.next,V=V.tag,V&4&&V&1&&(TP(Q,v),nM(Q,v)),v=H}while(v!==D)}return;case 1:v=Q.stateNode,Q.flags&4&&(D===null?v.componentDidMount():(H=Q.elementType===Q.type?D.memoizedProps:bo(Q.type,D.memoizedProps),v.componentDidUpdate(H,D.memoizedState,v.__reactInternalSnapshotBeforeUpdate))),D=Q.updateQueue,D!==null&&Ey(Q,D,v);return;case 3:if(D=Q.updateQueue,D!==null){if(v=null,Q.child!==null)switch(Q.child.tag){case 5:v=Re(Q.child.stateNode);break;case 1:v=Q.child.stateNode}Ey(Q,D,v)}return;case 5:v=Q.stateNode,D===null&&Q.flags&4&&to(v,Q.type,Q.memoizedProps,Q);return;case 6:return;case 4:return;case 12:return;case 13:Z&&Q.memoizedState===null&&(Q=Q.alternate,Q!==null&&(Q=Q.memoizedState,Q!==null&&(Q=Q.dehydrated,Q!==null&&Au(Q))));return;case 19:case 17:case 20:case 21:case 23:case 24:return}throw Error(c(163))}function EP(v,D){if(F)for(var Q=v;;){if(Q.tag===5){var H=Q.stateNode;D?yh(H):no(Q.stateNode,Q.memoizedProps)}else if(Q.tag===6)H=Q.stateNode,D?Eh(H):jn(H,Q.memoizedProps);else if((Q.tag!==23&&Q.tag!==24||Q.memoizedState===null||Q===v)&&Q.child!==null){Q.child.return=Q,Q=Q.child;continue}if(Q===v)break;for(;Q.sibling===null;){if(Q.return===null||Q.return===v)return;Q=Q.return}Q.sibling.return=Q.return,Q=Q.sibling}}function Py(v,D){if(Ha&&typeof Ha.onCommitFiberUnmount=="function")try{Ha.onCommitFiberUnmount(Xe,D)}catch{}switch(D.tag){case 0:case 11:case 14:case 15:case 22:if(v=D.updateQueue,v!==null&&(v=v.lastEffect,v!==null)){var Q=v=v.next;do{var H=Q,V=H.destroy;if(H=H.tag,V!==void 0)if(H&4)TP(D,Q);else{H=D;try{V()}catch(ne){kf(H,ne)}}Q=Q.next}while(Q!==v)}break;case 1:if(s2(D),v=D.stateNode,typeof v.componentWillUnmount=="function")try{v.props=D.memoizedProps,v.state=D.memoizedState,v.componentWillUnmount()}catch(ne){kf(D,ne)}break;case 5:s2(D);break;case 4:F?BP(v,D):z&&z&&(D=D.stateNode.containerInfo,v=lu(D),FA(D,v))}}function IP(v,D){for(var Q=D;;)if(Py(v,Q),Q.child===null||F&&Q.tag===4){if(Q===D)break;for(;Q.sibling===null;){if(Q.return===null||Q.return===D)return;Q=Q.return}Q.sibling.return=Q.return,Q=Q.sibling}else Q.child.return=Q,Q=Q.child}function xy(v){v.alternate=null,v.child=null,v.dependencies=null,v.firstEffect=null,v.lastEffect=null,v.memoizedProps=null,v.memoizedState=null,v.pendingProps=null,v.return=null,v.updateQueue=null}function CP(v){return v.tag===5||v.tag===3||v.tag===4}function wP(v){if(F){e:{for(var D=v.return;D!==null;){if(CP(D))break e;D=D.return}throw Error(c(160))}var Q=D;switch(D=Q.stateNode,Q.tag){case 5:var H=!1;break;case 3:D=D.containerInfo,H=!0;break;case 4:D=D.containerInfo,H=!0;break;default:throw Error(c(161))}Q.flags&16&&(pf(D),Q.flags&=-17);e:t:for(Q=v;;){for(;Q.sibling===null;){if(Q.return===null||CP(Q.return)){Q=null;break e}Q=Q.return}for(Q.sibling.return=Q.return,Q=Q.sibling;Q.tag!==5&&Q.tag!==6&&Q.tag!==18;){if(Q.flags&2||Q.child===null||Q.tag===4)continue t;Q.child.return=Q,Q=Q.child}if(!(Q.flags&2)){Q=Q.stateNode;break e}}H?o2(v,Q,D):a2(v,Q,D)}}function o2(v,D,Q){var H=v.tag,V=H===5||H===6;if(V)v=V?v.stateNode:v.stateNode.instance,D?ro(Q,v,D):wo(Q,v);else if(H!==4&&(v=v.child,v!==null))for(o2(v,D,Q),v=v.sibling;v!==null;)o2(v,D,Q),v=v.sibling}function a2(v,D,Q){var H=v.tag,V=H===5||H===6;if(V)v=V?v.stateNode:v.stateNode.instance,D?ji(Q,v,D):ai(Q,v);else if(H!==4&&(v=v.child,v!==null))for(a2(v,D,Q),v=v.sibling;v!==null;)a2(v,D,Q),v=v.sibling}function BP(v,D){for(var Q=D,H=!1,V,ne;;){if(!H){H=Q.return;e:for(;;){if(H===null)throw Error(c(160));switch(V=H.stateNode,H.tag){case 5:ne=!1;break e;case 3:V=V.containerInfo,ne=!0;break e;case 4:V=V.containerInfo,ne=!0;break e}H=H.return}H=!0}if(Q.tag===5||Q.tag===6)IP(v,Q),ne?RA(V,Q.stateNode):vo(V,Q.stateNode);else if(Q.tag===4){if(Q.child!==null){V=Q.stateNode.containerInfo,ne=!0,Q.child.return=Q,Q=Q.child;continue}}else if(Py(v,Q),Q.child!==null){Q.child.return=Q,Q=Q.child;continue}if(Q===D)break;for(;Q.sibling===null;){if(Q.return===null||Q.return===D)return;Q=Q.return,Q.tag===4&&(H=!1)}Q.sibling.return=Q.return,Q=Q.sibling}}function l2(v,D){if(F){switch(D.tag){case 0:case 11:case 14:case 15:case 22:Fh(3,D);return;case 1:return;case 5:var Q=D.stateNode;if(Q!=null){var H=D.memoizedProps;v=v!==null?v.memoizedProps:H;var V=D.type,ne=D.updateQueue;D.updateQueue=null,ne!==null&&Bo(Q,ne,V,v,H,D)}return;case 6:if(D.stateNode===null)throw Error(c(162));Q=D.memoizedProps,ns(D.stateNode,v!==null?v.memoizedProps:Q,Q);return;case 3:Z&&(D=D.stateNode,D.hydrate&&(D.hydrate=!1,MA(D.containerInfo)));return;case 12:return;case 13:vP(D),Kg(D);return;case 19:Kg(D);return;case 17:return;case 23:case 24:EP(D,D.memoizedState!==null);return}throw Error(c(163))}switch(D.tag){case 0:case 11:case 14:case 15:case 22:Fh(3,D);return;case 12:return;case 13:vP(D),Kg(D);return;case 19:Kg(D);return;case 3:Z&&(Q=D.stateNode,Q.hydrate&&(Q.hydrate=!1,MA(Q.containerInfo)));break;case 23:case 24:return}e:if(z){switch(D.tag){case 1:case 5:case 6:case 20:break e;case 3:case 4:D=D.stateNode,FA(D.containerInfo,D.pendingChildren);break e}throw Error(c(163))}}function vP(v){v.memoizedState!==null&&(d2=Dt(),F&&EP(v.child,!0))}function Kg(v){var D=v.updateQueue;if(D!==null){v.updateQueue=null;var Q=v.stateNode;Q===null&&(Q=v.stateNode=new KL),D.forEach(function(H){var V=sM.bind(null,v,H);Q.has(H)||(Q.add(H),H.then(V,V))})}}function JL(v,D){return v!==null&&(v=v.memoizedState,v===null||v.dehydrated!==null)?(D=D.memoizedState,D!==null&&D.dehydrated===null):!1}var ky=0,Qy=1,Ty=2,Jg=3,Ry=4;if(typeof Symbol=="function"&&Symbol.for){var zg=Symbol.for;ky=zg("selector.component"),Qy=zg("selector.has_pseudo_class"),Ty=zg("selector.role"),Jg=zg("selector.test_id"),Ry=zg("selector.text")}function Fy(v){var D=$(v);if(D!=null){if(typeof D.memoizedProps["data-testname"]!="string")throw Error(c(364));return D}if(v=ir(v),v===null)throw Error(c(362));return v.stateNode.current}function Df(v,D){switch(D.$$typeof){case ky:if(v.type===D.value)return!0;break;case Qy:e:{D=D.value,v=[v,0];for(var Q=0;Q";case Qy:return":has("+(bf(v)||"")+")";case Ty:return'[role="'+v.value+'"]';case Ry:return'"'+v.value+'"';case Jg:return'[data-testname="'+v.value+'"]';default:throw Error(c(365,v))}}function c2(v,D){var Q=[];v=[v,0];for(var H=0;HV&&(V=Se),Q&=~ne}if(Q=V,Q=Dt()-Q,Q=(120>Q?120:480>Q?480:1080>Q?1080:1920>Q?1920:3e3>Q?3e3:4320>Q?4320:1960*ZL(Q/1960))-Q,10 component higher in the tree to provide a loading indicator or placeholder to display.`)}vs!==5&&(vs=2),At=Wg(At,Ue),Xt=Se;do{switch(Xt.tag){case 3:ne=At,Xt.flags|=4096,D&=-D,Xt.lanes|=D;var Xn=i2(Xt,ne,D);yy(Xt,Xn);break e;case 1:ne=At;var kr=Xt.type,Rn=Xt.stateNode;if(!(Xt.flags&64)&&(typeof kr.getDerivedStateFromError=="function"||Rn!==null&&typeof Rn.componentDidCatch=="function"&&(gc===null||!gc.has(Rn)))){Xt.flags|=4096,D&=-D,Xt.lanes|=D;var Un=Vg(Xt,ne,D);yy(Xt,Un);break e}}Xt=Xt.return}while(Xt!==null)}QP(Q)}catch(zr){D=zr,Xi===Q&&Q!==null&&(Xi=Q=Q.return);continue}break}while(!0)}function xP(){var v=Oy.current;return Oy.current=kt,v===null?kt:v}function nd(v,D){var Q=xr;xr|=16;var H=xP();ao===v&&Ls===D||Mh(v,D);do try{$L();break}catch(V){PP(v,V)}while(!0);if(Rg(),xr=Q,Oy.current=H,Xi!==null)throw Error(c(261));return ao=null,Ls=0,vs}function $L(){for(;Xi!==null;)kP(Xi)}function eM(){for(;Xi!==null&&!Sl();)kP(Xi)}function kP(v){var D=NP(v.alternate,v,ep);v.memoizedProps=v.pendingProps,D===null?QP(v):Xi=D,f2.current=null}function QP(v){var D=v;do{var Q=D.alternate;if(v=D.return,D.flags&2048){if(Q=YL(D),Q!==null){Q.flags&=2047,Xi=Q;return}v!==null&&(v.firstEffect=v.lastEffect=null,v.flags|=2048)}else{if(Q=WL(Q,D,ep),Q!==null){Xi=Q;return}if(Q=D,Q.tag!==24&&Q.tag!==23||Q.memoizedState===null||ep&1073741824||!(Q.mode&4)){for(var H=0,V=Q.child;V!==null;)H|=V.lanes|V.childLanes,V=V.sibling;Q.childLanes=H}v!==null&&!(v.flags&2048)&&(v.firstEffect===null&&(v.firstEffect=D.firstEffect),D.lastEffect!==null&&(v.lastEffect!==null&&(v.lastEffect.nextEffect=D.firstEffect),v.lastEffect=D.lastEffect),1Dt()-d2?Mh(v,0):h2|=Q),da(v,D)}function sM(v,D){var Q=v.stateNode;Q!==null&&Q.delete(D),D=0,D===0&&(D=v.mode,D&2?D&4?(Su===0&&(Su=Nh),D=kn(62914560&~Su),D===0&&(D=4194304)):D=tr()===99?1:2:D=1),Q=To(),v=Hy(v,D),v!==null&&(ja(v,D,Q),da(v,Q))}var NP;NP=function(v,D,Q){var H=D.lanes;if(v!==null)if(v.memoizedProps!==D.pendingProps||Mi.current)Ke=!0;else if(Q&H)Ke=!!(v.flags&16384);else{switch(Ke=!1,D.tag){case 3:Sy(D),Hg();break;case 5:If(D);break;case 1:Jn(D.type)&&Ma(D);break;case 4:Mg(D,D.stateNode.containerInfo);break;case 10:Fg(D,D.memoizedProps.value);break;case 13:if(D.memoizedState!==null)return Q&D.child.childLanes?r2(v,D,Q):(xn(di,di.current&1),D=Gn(v,D,Q),D!==null?D.sibling:null);xn(di,di.current&1);break;case 19:if(H=(Q&D.childLanes)!==0,v.flags&64){if(H)return mP(v,D,Q);D.flags|=64}var V=D.memoizedState;if(V!==null&&(V.rendering=null,V.tail=null,V.lastEffect=null),xn(di,di.current),H)break;return null;case 23:case 24:return D.lanes=0,mi(v,D,Q)}return Gn(v,D,Q)}else Ke=!1;switch(D.lanes=0,D.tag){case 2:if(H=D.type,v!==null&&(v.alternate=null,D.alternate=null,D.flags|=2),v=D.pendingProps,V=dn(D,qi.current),mf(D,Q),V=qg(null,D,H,v,V,Q),D.flags|=1,typeof V=="object"&&V!==null&&typeof V.render=="function"&&V.$$typeof===void 0){if(D.tag=1,D.memoizedState=null,D.updateQueue=null,Jn(H)){var ne=!0;Ma(D)}else ne=!1;D.memoizedState=V.state!==null&&V.state!==void 0?V.state:null,Sh(D);var Se=H.getDerivedStateFromProps;typeof Se=="function"&&jA(D,H,Se,v),V.updater=qA,D.stateNode=V,V._reactInternals=D,xo(D,H,v,Q),D=t2(null,D,H,!0,ne,Q)}else D.tag=0,ft(null,D,V,Q),D=D.child;return D;case 16:V=D.elementType;e:{switch(v!==null&&(v.alternate=null,D.alternate=null,D.flags|=2),v=D.pendingProps,ne=V._init,V=ne(V._payload),D.type=V,ne=D.tag=aM(V),v=bo(V,v),ne){case 0:D=zA(null,D,V,v,Q);break e;case 1:D=dP(null,D,V,v,Q);break e;case 11:D=dr(null,D,V,v,Q);break e;case 14:D=Br(null,D,V,bo(V.type,v),H,Q);break e}throw Error(c(306,V,""))}return D;case 0:return H=D.type,V=D.pendingProps,V=D.elementType===H?V:bo(H,V),zA(v,D,H,V,Q);case 1:return H=D.type,V=D.pendingProps,V=D.elementType===H?V:bo(H,V),dP(v,D,H,V,Q);case 3:if(Sy(D),H=D.updateQueue,v===null||H===null)throw Error(c(282));if(H=D.pendingProps,V=D.memoizedState,V=V!==null?V.element:null,Og(v,D),HA(D,H,null,Q),H=D.memoizedState.element,H===V)Hg(),D=Gn(v,D,Q);else{if(V=D.stateNode,(ne=V.hydrate)&&(Z?(pa=fu(D.stateNode.containerInfo),Ya=D,ne=Va=!0):ne=!1),ne){if(Z&&(v=V.mutableSourceEagerHydrationData,v!=null))for(V=0;V=Gt&&ne>=Lr&&V<=vr&&Se<=Xt){v.splice(D,1);break}else if(H!==Gt||Q.width!==At.width||XtSe){if(!(ne!==Lr||Q.height!==At.height||vrV)){Gt>H&&(At.width+=Gt-H,At.x=H),vrne&&(At.height+=Lr-ne,At.y=ne),XtQ&&(Q=Se)),Se ")+` No matching component was found for: `)+v.join(" > ")}return null},r.getPublicRootInstance=function(v){if(v=v.current,!v.child)return null;switch(v.child.tag){case 5:return Re(v.child.stateNode);default:return v.child.stateNode}},r.injectIntoDevTools=function(v){if(v={bundleType:v.bundleType,version:v.version,rendererPackageName:v.rendererPackageName,rendererConfig:v.rendererConfig,overrideHookState:null,overrideHookStateDeletePath:null,overrideHookStateRenamePath:null,overrideProps:null,overridePropsDeletePath:null,overridePropsRenamePath:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:f.ReactCurrentDispatcher,findHostInstanceByFiber:cM,findFiberByHostInstance:v.findFiberByHostInstance||uM,findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null},typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u")v=!1;else{var D=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!D.isDisabled&&D.supportsFiber)try{Xe=D.inject(v),Ha=D}catch{}v=!0}return v},r.observeVisibleRects=function(v,D,Q,H){if(!qt)throw Error(c(363));v=u2(v,D);var V=nn(v,Q,H).disconnect;return{disconnect:function(){V()}}},r.registerMutableSourceForHydration=function(v,D){var Q=D._getVersion;Q=Q(D._source),v.mutableSourceEagerHydrationData==null?v.mutableSourceEagerHydrationData=[D,Q]:v.mutableSourceEagerHydrationData.push(D,Q)},r.runWithPriority=function(v,D){var Q=cc;try{return cc=v,D()}finally{cc=Q}},r.shouldSuspend=function(){return!1},r.unbatchedUpdates=function(v,D){var Q=xr;xr&=-2,xr|=8;try{return v(D)}finally{xr=Q,xr===0&&(Pf(),Tn())}},r.updateContainer=function(v,D,Q,H){var V=D.current,ne=To(),Se=Ss(V);e:if(Q){Q=Q._reactInternals;t:{if(we(Q)!==Q||Q.tag!==1)throw Error(c(170));var Ue=Q;do{switch(Ue.tag){case 3:Ue=Ue.stateNode.context;break t;case 1:if(Jn(Ue.type)){Ue=Ue.stateNode.__reactInternalMemoizedMergedChildContext;break t}}Ue=Ue.return}while(Ue!==null);throw Error(c(171))}if(Q.tag===1){var At=Q.type;if(Jn(At)){Q=La(Q,At,Ue);break e}}Q=Ue}else Q=ca;return D.context===null?D.context=Q:D.pendingContext=Q,D=bl(ne,Se),D.payload={element:v},H=H===void 0?null:H,H!==null&&(D.callback=H),Pl(V,D),Rl(V,Se,ne),Se},r}});var xDe=L((Xpr,PDe)=>{"use strict";PDe.exports=bDe()});var QDe=L(($pr,kDe)=>{"use strict";var mPt={ALIGN_COUNT:8,ALIGN_AUTO:0,ALIGN_FLEX_START:1,ALIGN_CENTER:2,ALIGN_FLEX_END:3,ALIGN_STRETCH:4,ALIGN_BASELINE:5,ALIGN_SPACE_BETWEEN:6,ALIGN_SPACE_AROUND:7,DIMENSION_COUNT:2,DIMENSION_WIDTH:0,DIMENSION_HEIGHT:1,DIRECTION_COUNT:3,DIRECTION_INHERIT:0,DIRECTION_LTR:1,DIRECTION_RTL:2,DISPLAY_COUNT:2,DISPLAY_FLEX:0,DISPLAY_NONE:1,EDGE_COUNT:9,EDGE_LEFT:0,EDGE_TOP:1,EDGE_RIGHT:2,EDGE_BOTTOM:3,EDGE_START:4,EDGE_END:5,EDGE_HORIZONTAL:6,EDGE_VERTICAL:7,EDGE_ALL:8,EXPERIMENTAL_FEATURE_COUNT:1,EXPERIMENTAL_FEATURE_WEB_FLEX_BASIS:0,FLEX_DIRECTION_COUNT:4,FLEX_DIRECTION_COLUMN:0,FLEX_DIRECTION_COLUMN_REVERSE:1,FLEX_DIRECTION_ROW:2,FLEX_DIRECTION_ROW_REVERSE:3,JUSTIFY_COUNT:6,JUSTIFY_FLEX_START:0,JUSTIFY_CENTER:1,JUSTIFY_FLEX_END:2,JUSTIFY_SPACE_BETWEEN:3,JUSTIFY_SPACE_AROUND:4,JUSTIFY_SPACE_EVENLY:5,LOG_LEVEL_COUNT:6,LOG_LEVEL_ERROR:0,LOG_LEVEL_WARN:1,LOG_LEVEL_INFO:2,LOG_LEVEL_DEBUG:3,LOG_LEVEL_VERBOSE:4,LOG_LEVEL_FATAL:5,MEASURE_MODE_COUNT:3,MEASURE_MODE_UNDEFINED:0,MEASURE_MODE_EXACTLY:1,MEASURE_MODE_AT_MOST:2,NODE_TYPE_COUNT:2,NODE_TYPE_DEFAULT:0,NODE_TYPE_TEXT:1,OVERFLOW_COUNT:3,OVERFLOW_VISIBLE:0,OVERFLOW_HIDDEN:1,OVERFLOW_SCROLL:2,POSITION_TYPE_COUNT:2,POSITION_TYPE_RELATIVE:0,POSITION_TYPE_ABSOLUTE:1,PRINT_OPTIONS_COUNT:3,PRINT_OPTIONS_LAYOUT:1,PRINT_OPTIONS_STYLE:2,PRINT_OPTIONS_CHILDREN:4,UNIT_COUNT:4,UNIT_UNDEFINED:0,UNIT_POINT:1,UNIT_PERCENT:2,UNIT_AUTO:3,WRAP_COUNT:3,WRAP_NO_WRAP:0,WRAP_WRAP:1,WRAP_WRAP_REVERSE:2};kDe.exports=mPt});var NDe=L((ehr,FDe)=>{"use strict";var yPt=Object.assign||function(t){for(var e=1;e"}}]),t}(),TDe=function(){NF(t,null,[{key:"fromJS",value:function(r){var s=r.width,a=r.height;return new t(s,a)}}]);function t(e,r){IW(this,t),this.width=e,this.height=r}return NF(t,[{key:"fromJS",value:function(r){r(this.width,this.height)}},{key:"toString",value:function(){return""}}]),t}(),RDe=function(){function t(e,r){IW(this,t),this.unit=e,this.value=r}return NF(t,[{key:"fromJS",value:function(r){r(this.unit,this.value)}},{key:"toString",value:function(){switch(this.unit){case rf.UNIT_POINT:return String(this.value);case rf.UNIT_PERCENT:return this.value+"%";case rf.UNIT_AUTO:return"auto";default:return this.value+"?"}}},{key:"valueOf",value:function(){return this.value}}]),t}();FDe.exports=function(t,e){function r(c,f,p){var h=c[f];c[f]=function(){for(var E=arguments.length,C=Array(E),S=0;S1?C-1:0),P=1;P1&&arguments[1]!==void 0?arguments[1]:NaN,p=arguments.length>2&&arguments[2]!==void 0?arguments[2]:NaN,h=arguments.length>3&&arguments[3]!==void 0?arguments[3]:rf.DIRECTION_LTR;return c.call(this,f,p,h)}),yPt({Config:e.Config,Node:e.Node,Layout:t("Layout",EPt),Size:t("Size",TDe),Value:t("Value",RDe),getInstanceCount:function(){return e.getInstanceCount.apply(e,arguments)}},rf)}});var ODe=L((exports,module)=>{(function(t,e){typeof define=="function"&&define.amd?define([],function(){return e}):typeof module=="object"&&module.exports?module.exports=e:(t.nbind=t.nbind||{}).init=e})(exports,function(Module,cb){typeof Module=="function"&&(cb=Module,Module={}),Module.onRuntimeInitialized=function(t,e){return function(){t&&t.apply(this,arguments);try{Module.ccall("nbind_init")}catch(r){e(r);return}e(null,{bind:Module._nbind_value,reflect:Module.NBind.reflect,queryType:Module.NBind.queryType,toggleLightGC:Module.toggleLightGC,lib:Module})}}(Module.onRuntimeInitialized,cb);var Module;Module||(Module=(typeof Module<"u"?Module:null)||{});var moduleOverrides={};for(var key in Module)Module.hasOwnProperty(key)&&(moduleOverrides[key]=Module[key]);var ENVIRONMENT_IS_WEB=!1,ENVIRONMENT_IS_WORKER=!1,ENVIRONMENT_IS_NODE=!1,ENVIRONMENT_IS_SHELL=!1;if(Module.ENVIRONMENT)if(Module.ENVIRONMENT==="WEB")ENVIRONMENT_IS_WEB=!0;else if(Module.ENVIRONMENT==="WORKER")ENVIRONMENT_IS_WORKER=!0;else if(Module.ENVIRONMENT==="NODE")ENVIRONMENT_IS_NODE=!0;else if(Module.ENVIRONMENT==="SHELL")ENVIRONMENT_IS_SHELL=!0;else throw new Error("The provided Module['ENVIRONMENT'] value is not valid. It must be one of: WEB|WORKER|NODE|SHELL.");else ENVIRONMENT_IS_WEB=typeof window=="object",ENVIRONMENT_IS_WORKER=typeof importScripts=="function",ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof Ie=="function"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER,ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;if(ENVIRONMENT_IS_NODE){Module.print||(Module.print=console.log),Module.printErr||(Module.printErr=console.warn);var nodeFS,nodePath;Module.read=function(e,r){nodeFS||(nodeFS={}("")),nodePath||(nodePath={}("")),e=nodePath.normalize(e);var s=nodeFS.readFileSync(e);return r?s:s.toString()},Module.readBinary=function(e){var r=Module.read(e,!0);return r.buffer||(r=new Uint8Array(r)),assert(r.buffer),r},Module.load=function(e){globalEval(read(e))},Module.thisProgram||(process.argv.length>1?Module.thisProgram=process.argv[1].replace(/\\/g,"/"):Module.thisProgram="unknown-program"),Module.arguments=process.argv.slice(2),typeof module<"u"&&(module.exports=Module),Module.inspect=function(){return"[Emscripten Module object]"}}else if(ENVIRONMENT_IS_SHELL)Module.print||(Module.print=print),typeof printErr<"u"&&(Module.printErr=printErr),typeof read<"u"?Module.read=read:Module.read=function(){throw"no read() available"},Module.readBinary=function(e){if(typeof readbuffer=="function")return new Uint8Array(readbuffer(e));var r=read(e,"binary");return assert(typeof r=="object"),r},typeof scriptArgs<"u"?Module.arguments=scriptArgs:typeof arguments<"u"&&(Module.arguments=arguments),typeof quit=="function"&&(Module.quit=function(t,e){quit(t)});else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(Module.read=function(e){var r=new XMLHttpRequest;return r.open("GET",e,!1),r.send(null),r.responseText},ENVIRONMENT_IS_WORKER&&(Module.readBinary=function(e){var r=new XMLHttpRequest;return r.open("GET",e,!1),r.responseType="arraybuffer",r.send(null),new Uint8Array(r.response)}),Module.readAsync=function(e,r,s){var a=new XMLHttpRequest;a.open("GET",e,!0),a.responseType="arraybuffer",a.onload=function(){a.status==200||a.status==0&&a.response?r(a.response):s()},a.onerror=s,a.send(null)},typeof arguments<"u"&&(Module.arguments=arguments),typeof console<"u")Module.print||(Module.print=function(e){console.log(e)}),Module.printErr||(Module.printErr=function(e){console.warn(e)});else{var TRY_USE_DUMP=!1;Module.print||(Module.print=TRY_USE_DUMP&&typeof dump<"u"?function(t){dump(t)}:function(t){})}ENVIRONMENT_IS_WORKER&&(Module.load=importScripts),typeof Module.setWindowTitle>"u"&&(Module.setWindowTitle=function(t){document.title=t})}else throw"Unknown runtime environment. Where are we?";function globalEval(t){eval.call(null,t)}!Module.load&&Module.read&&(Module.load=function(e){globalEval(Module.read(e))}),Module.print||(Module.print=function(){}),Module.printErr||(Module.printErr=Module.print),Module.arguments||(Module.arguments=[]),Module.thisProgram||(Module.thisProgram="./this.program"),Module.quit||(Module.quit=function(t,e){throw e}),Module.print=Module.print,Module.printErr=Module.printErr,Module.preRun=[],Module.postRun=[];for(var key in moduleOverrides)moduleOverrides.hasOwnProperty(key)&&(Module[key]=moduleOverrides[key]);moduleOverrides=void 0;var Runtime={setTempRet0:function(t){return tempRet0=t,t},getTempRet0:function(){return tempRet0},stackSave:function(){return STACKTOP},stackRestore:function(t){STACKTOP=t},getNativeTypeSize:function(t){switch(t){case"i1":case"i8":return 1;case"i16":return 2;case"i32":return 4;case"i64":return 8;case"float":return 4;case"double":return 8;default:{if(t[t.length-1]==="*")return Runtime.QUANTUM_SIZE;if(t[0]==="i"){var e=parseInt(t.substr(1));return assert(e%8===0),e/8}else return 0}}},getNativeFieldSize:function(t){return Math.max(Runtime.getNativeTypeSize(t),Runtime.QUANTUM_SIZE)},STACK_ALIGN:16,prepVararg:function(t,e){return e==="double"||e==="i64"?t&7&&(assert((t&7)===4),t+=4):assert((t&3)===0),t},getAlignSize:function(t,e,r){return!r&&(t=="i64"||t=="double")?8:t?Math.min(e||(t?Runtime.getNativeFieldSize(t):0),Runtime.QUANTUM_SIZE):Math.min(e,8)},dynCall:function(t,e,r){return r&&r.length?Module["dynCall_"+t].apply(null,[e].concat(r)):Module["dynCall_"+t].call(null,e)},functionPointers:[],addFunction:function(t){for(var e=0;e>2],r=(e+t+15|0)&-16;if(HEAP32[DYNAMICTOP_PTR>>2]=r,r>=TOTAL_MEMORY){var s=enlargeMemory();if(!s)return HEAP32[DYNAMICTOP_PTR>>2]=e,0}return e},alignMemory:function(t,e){var r=t=Math.ceil(t/(e||16))*(e||16);return r},makeBigInt:function(t,e,r){var s=r?+(t>>>0)+ +(e>>>0)*4294967296:+(t>>>0)+ +(e|0)*4294967296;return s},GLOBAL_BASE:8,QUANTUM_SIZE:4,__dummy__:0};Module.Runtime=Runtime;var ABORT=0,EXITSTATUS=0;function assert(t,e){t||abort("Assertion failed: "+e)}function getCFunc(ident){var func=Module["_"+ident];if(!func)try{func=eval("_"+ident)}catch(t){}return assert(func,"Cannot call unknown function "+ident+" (perhaps LLVM optimizations or closure removed it?)"),func}var cwrap,ccall;(function(){var JSfuncs={stackSave:function(){Runtime.stackSave()},stackRestore:function(){Runtime.stackRestore()},arrayToC:function(t){var e=Runtime.stackAlloc(t.length);return writeArrayToMemory(t,e),e},stringToC:function(t){var e=0;if(t!=null&&t!==0){var r=(t.length<<2)+1;e=Runtime.stackAlloc(r),stringToUTF8(t,e,r)}return e}},toC={string:JSfuncs.stringToC,array:JSfuncs.arrayToC};ccall=function(e,r,s,a,n){var c=getCFunc(e),f=[],p=0;if(a)for(var h=0;h>0]=e;break;case"i8":HEAP8[t>>0]=e;break;case"i16":HEAP16[t>>1]=e;break;case"i32":HEAP32[t>>2]=e;break;case"i64":tempI64=[e>>>0,(tempDouble=e,+Math_abs(tempDouble)>=1?tempDouble>0?(Math_min(+Math_floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math_ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[t>>2]=tempI64[0],HEAP32[t+4>>2]=tempI64[1];break;case"float":HEAPF32[t>>2]=e;break;case"double":HEAPF64[t>>3]=e;break;default:abort("invalid type for setValue: "+r)}}Module.setValue=setValue;function getValue(t,e,r){switch(e=e||"i8",e.charAt(e.length-1)==="*"&&(e="i32"),e){case"i1":return HEAP8[t>>0];case"i8":return HEAP8[t>>0];case"i16":return HEAP16[t>>1];case"i32":return HEAP32[t>>2];case"i64":return HEAP32[t>>2];case"float":return HEAPF32[t>>2];case"double":return HEAPF64[t>>3];default:abort("invalid type for setValue: "+e)}return null}Module.getValue=getValue;var ALLOC_NORMAL=0,ALLOC_STACK=1,ALLOC_STATIC=2,ALLOC_DYNAMIC=3,ALLOC_NONE=4;Module.ALLOC_NORMAL=ALLOC_NORMAL,Module.ALLOC_STACK=ALLOC_STACK,Module.ALLOC_STATIC=ALLOC_STATIC,Module.ALLOC_DYNAMIC=ALLOC_DYNAMIC,Module.ALLOC_NONE=ALLOC_NONE;function allocate(t,e,r,s){var a,n;typeof t=="number"?(a=!0,n=t):(a=!1,n=t.length);var c=typeof e=="string"?e:null,f;if(r==ALLOC_NONE?f=s:f=[typeof _malloc=="function"?_malloc:Runtime.staticAlloc,Runtime.stackAlloc,Runtime.staticAlloc,Runtime.dynamicAlloc][r===void 0?ALLOC_STATIC:r](Math.max(n,c?1:e.length)),a){var s=f,p;for(assert((f&3)==0),p=f+(n&-4);s>2]=0;for(p=f+n;s>0]=0;return f}if(c==="i8")return t.subarray||t.slice?HEAPU8.set(t,f):HEAPU8.set(new Uint8Array(t),f),f;for(var h=0,E,C,S;h>0],r|=s,!(s==0&&!e||(a++,e&&a==e)););e||(e=a);var n="";if(r<128){for(var c=1024,f;e>0;)f=String.fromCharCode.apply(String,HEAPU8.subarray(t,t+Math.min(e,c))),n=n?n+f:f,t+=c,e-=c;return n}return Module.UTF8ToString(t)}Module.Pointer_stringify=Pointer_stringify;function AsciiToString(t){for(var e="";;){var r=HEAP8[t++>>0];if(!r)return e;e+=String.fromCharCode(r)}}Module.AsciiToString=AsciiToString;function stringToAscii(t,e){return writeAsciiToMemory(t,e,!1)}Module.stringToAscii=stringToAscii;var UTF8Decoder=typeof TextDecoder<"u"?new TextDecoder("utf8"):void 0;function UTF8ArrayToString(t,e){for(var r=e;t[r];)++r;if(r-e>16&&t.subarray&&UTF8Decoder)return UTF8Decoder.decode(t.subarray(e,r));for(var s,a,n,c,f,p,h="";;){if(s=t[e++],!s)return h;if(!(s&128)){h+=String.fromCharCode(s);continue}if(a=t[e++]&63,(s&224)==192){h+=String.fromCharCode((s&31)<<6|a);continue}if(n=t[e++]&63,(s&240)==224?s=(s&15)<<12|a<<6|n:(c=t[e++]&63,(s&248)==240?s=(s&7)<<18|a<<12|n<<6|c:(f=t[e++]&63,(s&252)==248?s=(s&3)<<24|a<<18|n<<12|c<<6|f:(p=t[e++]&63,s=(s&1)<<30|a<<24|n<<18|c<<12|f<<6|p))),s<65536)h+=String.fromCharCode(s);else{var E=s-65536;h+=String.fromCharCode(55296|E>>10,56320|E&1023)}}}Module.UTF8ArrayToString=UTF8ArrayToString;function UTF8ToString(t){return UTF8ArrayToString(HEAPU8,t)}Module.UTF8ToString=UTF8ToString;function stringToUTF8Array(t,e,r,s){if(!(s>0))return 0;for(var a=r,n=r+s-1,c=0;c=55296&&f<=57343&&(f=65536+((f&1023)<<10)|t.charCodeAt(++c)&1023),f<=127){if(r>=n)break;e[r++]=f}else if(f<=2047){if(r+1>=n)break;e[r++]=192|f>>6,e[r++]=128|f&63}else if(f<=65535){if(r+2>=n)break;e[r++]=224|f>>12,e[r++]=128|f>>6&63,e[r++]=128|f&63}else if(f<=2097151){if(r+3>=n)break;e[r++]=240|f>>18,e[r++]=128|f>>12&63,e[r++]=128|f>>6&63,e[r++]=128|f&63}else if(f<=67108863){if(r+4>=n)break;e[r++]=248|f>>24,e[r++]=128|f>>18&63,e[r++]=128|f>>12&63,e[r++]=128|f>>6&63,e[r++]=128|f&63}else{if(r+5>=n)break;e[r++]=252|f>>30,e[r++]=128|f>>24&63,e[r++]=128|f>>18&63,e[r++]=128|f>>12&63,e[r++]=128|f>>6&63,e[r++]=128|f&63}}return e[r]=0,r-a}Module.stringToUTF8Array=stringToUTF8Array;function stringToUTF8(t,e,r){return stringToUTF8Array(t,HEAPU8,e,r)}Module.stringToUTF8=stringToUTF8;function lengthBytesUTF8(t){for(var e=0,r=0;r=55296&&s<=57343&&(s=65536+((s&1023)<<10)|t.charCodeAt(++r)&1023),s<=127?++e:s<=2047?e+=2:s<=65535?e+=3:s<=2097151?e+=4:s<=67108863?e+=5:e+=6}return e}Module.lengthBytesUTF8=lengthBytesUTF8;var UTF16Decoder=typeof TextDecoder<"u"?new TextDecoder("utf-16le"):void 0;function demangle(t){var e=Module.___cxa_demangle||Module.__cxa_demangle;if(e){try{var r=t.substr(1),s=lengthBytesUTF8(r)+1,a=_malloc(s);stringToUTF8(r,a,s);var n=_malloc(4),c=e(a,0,0,n);if(getValue(n,"i32")===0&&c)return Pointer_stringify(c)}catch{}finally{a&&_free(a),n&&_free(n),c&&_free(c)}return t}return Runtime.warnOnce("warning: build with -s DEMANGLE_SUPPORT=1 to link in libcxxabi demangling"),t}function demangleAll(t){var e=/__Z[\w\d_]+/g;return t.replace(e,function(r){var s=demangle(r);return r===s?r:r+" ["+s+"]"})}function jsStackTrace(){var t=new Error;if(!t.stack){try{throw new Error(0)}catch(e){t=e}if(!t.stack)return"(no stack trace available)"}return t.stack.toString()}function stackTrace(){var t=jsStackTrace();return Module.extraStackTrace&&(t+=` `+Module.extraStackTrace()),demangleAll(t)}Module.stackTrace=stackTrace;var HEAP,buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBufferViews(){Module.HEAP8=HEAP8=new Int8Array(buffer),Module.HEAP16=HEAP16=new Int16Array(buffer),Module.HEAP32=HEAP32=new Int32Array(buffer),Module.HEAPU8=HEAPU8=new Uint8Array(buffer),Module.HEAPU16=HEAPU16=new Uint16Array(buffer),Module.HEAPU32=HEAPU32=new Uint32Array(buffer),Module.HEAPF32=HEAPF32=new Float32Array(buffer),Module.HEAPF64=HEAPF64=new Float64Array(buffer)}var STATIC_BASE,STATICTOP,staticSealed,STACK_BASE,STACKTOP,STACK_MAX,DYNAMIC_BASE,DYNAMICTOP_PTR;STATIC_BASE=STATICTOP=STACK_BASE=STACKTOP=STACK_MAX=DYNAMIC_BASE=DYNAMICTOP_PTR=0,staticSealed=!1;function abortOnCannotGrowMemory(){abort("Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value "+TOTAL_MEMORY+", (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime but prevents some optimizations, (3) set Module.TOTAL_MEMORY to a higher value before the program runs, or (4) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ")}function enlargeMemory(){abortOnCannotGrowMemory()}var TOTAL_STACK=Module.TOTAL_STACK||5242880,TOTAL_MEMORY=Module.TOTAL_MEMORY||134217728;TOTAL_MEMORY0;){var e=t.shift();if(typeof e=="function"){e();continue}var r=e.func;typeof r=="number"?e.arg===void 0?Module.dynCall_v(r):Module.dynCall_vi(r,e.arg):r(e.arg===void 0?null:e.arg)}}var __ATPRERUN__=[],__ATINIT__=[],__ATMAIN__=[],__ATEXIT__=[],__ATPOSTRUN__=[],runtimeInitialized=!1,runtimeExited=!1;function preRun(){if(Module.preRun)for(typeof Module.preRun=="function"&&(Module.preRun=[Module.preRun]);Module.preRun.length;)addOnPreRun(Module.preRun.shift());callRuntimeCallbacks(__ATPRERUN__)}function ensureInitRuntime(){runtimeInitialized||(runtimeInitialized=!0,callRuntimeCallbacks(__ATINIT__))}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){callRuntimeCallbacks(__ATEXIT__),runtimeExited=!0}function postRun(){if(Module.postRun)for(typeof Module.postRun=="function"&&(Module.postRun=[Module.postRun]);Module.postRun.length;)addOnPostRun(Module.postRun.shift());callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(t){__ATPRERUN__.unshift(t)}Module.addOnPreRun=addOnPreRun;function addOnInit(t){__ATINIT__.unshift(t)}Module.addOnInit=addOnInit;function addOnPreMain(t){__ATMAIN__.unshift(t)}Module.addOnPreMain=addOnPreMain;function addOnExit(t){__ATEXIT__.unshift(t)}Module.addOnExit=addOnExit;function addOnPostRun(t){__ATPOSTRUN__.unshift(t)}Module.addOnPostRun=addOnPostRun;function intArrayFromString(t,e,r){var s=r>0?r:lengthBytesUTF8(t)+1,a=new Array(s),n=stringToUTF8Array(t,a,0,a.length);return e&&(a.length=n),a}Module.intArrayFromString=intArrayFromString;function intArrayToString(t){for(var e=[],r=0;r255&&(s&=255),e.push(String.fromCharCode(s))}return e.join("")}Module.intArrayToString=intArrayToString;function writeStringToMemory(t,e,r){Runtime.warnOnce("writeStringToMemory is deprecated and should not be called! Use stringToUTF8() instead!");var s,a;r&&(a=e+lengthBytesUTF8(t),s=HEAP8[a]),stringToUTF8(t,e,1/0),r&&(HEAP8[a]=s)}Module.writeStringToMemory=writeStringToMemory;function writeArrayToMemory(t,e){HEAP8.set(t,e)}Module.writeArrayToMemory=writeArrayToMemory;function writeAsciiToMemory(t,e,r){for(var s=0;s>0]=t.charCodeAt(s);r||(HEAP8[e>>0]=0)}if(Module.writeAsciiToMemory=writeAsciiToMemory,(!Math.imul||Math.imul(4294967295,5)!==-5)&&(Math.imul=function t(e,r){var s=e>>>16,a=e&65535,n=r>>>16,c=r&65535;return a*c+(s*c+a*n<<16)|0}),Math.imul=Math.imul,!Math.fround){var froundBuffer=new Float32Array(1);Math.fround=function(t){return froundBuffer[0]=t,froundBuffer[0]}}Math.fround=Math.fround,Math.clz32||(Math.clz32=function(t){t=t>>>0;for(var e=0;e<32;e++)if(t&1<<31-e)return e;return 32}),Math.clz32=Math.clz32,Math.trunc||(Math.trunc=function(t){return t<0?Math.ceil(t):Math.floor(t)}),Math.trunc=Math.trunc;var Math_abs=Math.abs,Math_cos=Math.cos,Math_sin=Math.sin,Math_tan=Math.tan,Math_acos=Math.acos,Math_asin=Math.asin,Math_atan=Math.atan,Math_atan2=Math.atan2,Math_exp=Math.exp,Math_log=Math.log,Math_sqrt=Math.sqrt,Math_ceil=Math.ceil,Math_floor=Math.floor,Math_pow=Math.pow,Math_imul=Math.imul,Math_fround=Math.fround,Math_round=Math.round,Math_min=Math.min,Math_clz32=Math.clz32,Math_trunc=Math.trunc,runDependencies=0,runDependencyWatcher=null,dependenciesFulfilled=null;function getUniqueRunDependency(t){return t}function addRunDependency(t){runDependencies++,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies)}Module.addRunDependency=addRunDependency;function removeRunDependency(t){if(runDependencies--,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies),runDependencies==0&&(runDependencyWatcher!==null&&(clearInterval(runDependencyWatcher),runDependencyWatcher=null),dependenciesFulfilled)){var e=dependenciesFulfilled;dependenciesFulfilled=null,e()}}Module.removeRunDependency=removeRunDependency,Module.preloadedImages={},Module.preloadedAudios={};var ASM_CONSTS=[function(t,e,r,s,a,n,c,f){return _nbind.callbackSignatureList[t].apply(this,arguments)}];function _emscripten_asm_const_iiiiiiii(t,e,r,s,a,n,c,f){return ASM_CONSTS[t](e,r,s,a,n,c,f)}function _emscripten_asm_const_iiiii(t,e,r,s,a){return ASM_CONSTS[t](e,r,s,a)}function _emscripten_asm_const_iiidddddd(t,e,r,s,a,n,c,f,p){return ASM_CONSTS[t](e,r,s,a,n,c,f,p)}function _emscripten_asm_const_iiididi(t,e,r,s,a,n,c){return ASM_CONSTS[t](e,r,s,a,n,c)}function _emscripten_asm_const_iiii(t,e,r,s){return ASM_CONSTS[t](e,r,s)}function _emscripten_asm_const_iiiid(t,e,r,s,a){return ASM_CONSTS[t](e,r,s,a)}function _emscripten_asm_const_iiiiii(t,e,r,s,a,n){return ASM_CONSTS[t](e,r,s,a,n)}STATIC_BASE=Runtime.GLOBAL_BASE,STATICTOP=STATIC_BASE+12800,__ATINIT__.push({func:function(){__GLOBAL__sub_I_Yoga_cpp()}},{func:function(){__GLOBAL__sub_I_nbind_cc()}},{func:function(){__GLOBAL__sub_I_common_cc()}},{func:function(){__GLOBAL__sub_I_Binding_cc()}}),allocate([0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,192,127,0,0,192,127,3,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,3,0,0,0,0,0,192,127,3,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,0,0,128,191,0,0,128,191,0,0,192,127,0,0,0,0,0,0,0,0,0,0,128,63,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,3,0,0,0,1,0,0,0,2,0,0,0,0,0,0,0,190,12,0,0,200,12,0,0,208,12,0,0,216,12,0,0,230,12,0,0,242,12,0,0,1,0,0,0,3,0,0,0,0,0,0,0,2,0,0,0,0,0,192,127,3,0,0,0,180,45,0,0,181,45,0,0,182,45,0,0,181,45,0,0,182,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,3,0,0,0,1,0,0,0,4,0,0,0,183,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,184,45,0,0,185,45,0,0,181,45,0,0,181,45,0,0,182,45,0,0,186,45,0,0,185,45,0,0,148,4,0,0,3,0,0,0,187,45,0,0,164,4,0,0,188,45,0,0,2,0,0,0,189,45,0,0,164,4,0,0,188,45,0,0,185,45,0,0,164,4,0,0,185,45,0,0,164,4,0,0,188,45,0,0,181,45,0,0,182,45,0,0,181,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,5,0,0,0,6,0,0,0,1,0,0,0,7,0,0,0,183,45,0,0,182,45,0,0,181,45,0,0,190,45,0,0,190,45,0,0,182,45,0,0,182,45,0,0,185,45,0,0,181,45,0,0,185,45,0,0,182,45,0,0,181,45,0,0,185,45,0,0,182,45,0,0,185,45,0,0,48,5,0,0,3,0,0,0,56,5,0,0,1,0,0,0,189,45,0,0,185,45,0,0,164,4,0,0,76,5,0,0,2,0,0,0,191,45,0,0,186,45,0,0,182,45,0,0,185,45,0,0,192,45,0,0,185,45,0,0,182,45,0,0,186,45,0,0,185,45,0,0,76,5,0,0,76,5,0,0,136,5,0,0,182,45,0,0,181,45,0,0,2,0,0,0,190,45,0,0,136,5,0,0,56,19,0,0,156,5,0,0,2,0,0,0,184,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,8,0,0,0,9,0,0,0,1,0,0,0,10,0,0,0,204,5,0,0,181,45,0,0,181,45,0,0,2,0,0,0,180,45,0,0,204,5,0,0,2,0,0,0,195,45,0,0,236,5,0,0,97,19,0,0,198,45,0,0,211,45,0,0,212,45,0,0,213,45,0,0,214,45,0,0,215,45,0,0,188,45,0,0,182,45,0,0,216,45,0,0,217,45,0,0,218,45,0,0,219,45,0,0,192,45,0,0,181,45,0,0,0,0,0,0,185,45,0,0,110,19,0,0,186,45,0,0,115,19,0,0,221,45,0,0,120,19,0,0,148,4,0,0,132,19,0,0,96,6,0,0,145,19,0,0,222,45,0,0,164,19,0,0,223,45,0,0,173,19,0,0,0,0,0,0,3,0,0,0,104,6,0,0,1,0,0,0,187,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,11,0,0,0,12,0,0,0,1,0,0,0,13,0,0,0,185,45,0,0,224,45,0,0,164,6,0,0,188,45,0,0,172,6,0,0,180,6,0,0,2,0,0,0,188,6,0,0,7,0,0,0,224,45,0,0,7,0,0,0,164,6,0,0,1,0,0,0,213,45,0,0,185,45,0,0,224,45,0,0,172,6,0,0,185,45,0,0,224,45,0,0,164,6,0,0,185,45,0,0,224,45,0,0,211,45,0,0,211,45,0,0,222,45,0,0,211,45,0,0,224,45,0,0,222,45,0,0,211,45,0,0,224,45,0,0,172,6,0,0,222,45,0,0,211,45,0,0,224,45,0,0,188,45,0,0,222,45,0,0,211,45,0,0,40,7,0,0,188,45,0,0,2,0,0,0,224,45,0,0,185,45,0,0,188,45,0,0,188,45,0,0,188,45,0,0,188,45,0,0,222,45,0,0,224,45,0,0,148,4,0,0,185,45,0,0,148,4,0,0,148,4,0,0,148,4,0,0,148,4,0,0,148,4,0,0,185,45,0,0,164,6,0,0,148,4,0,0,0,0,0,0,0,0,0,0,1,0,0,0,14,0,0,0,15,0,0,0,1,0,0,0,16,0,0,0,148,7,0,0,2,0,0,0,225,45,0,0,183,45,0,0,188,45,0,0,168,7,0,0,5,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,234,45,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,148,45,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,9,0,0,5,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,2,0,0,0,242,45,0,0,0,4,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,67,111,117,108,100,32,110,111,116,32,97,108,108,111,99,97,116,101,32,109,101,109,111,114,121,32,102,111,114,32,110,111,100,101,0,67,97,110,110,111,116,32,114,101,115,101,116,32,97,32,110,111,100,101,32,119,104,105,99,104,32,115,116,105,108,108,32,104,97,115,32,99,104,105,108,100,114,101,110,32,97,116,116,97,99,104,101,100,0,67,97,110,110,111,116,32,114,101,115,101,116,32,97,32,110,111,100,101,32,115,116,105,108,108,32,97,116,116,97,99,104,101,100,32,116,111,32,97,32,112,97,114,101,110,116,0,67,111,117,108,100,32,110,111,116,32,97,108,108,111,99,97,116,101,32,109,101,109,111,114,121,32,102,111,114,32,99,111,110,102,105,103,0,67,97,110,110,111,116,32,115,101,116,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,58,32,78,111,100,101,115,32,119,105,116,104,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,32,99,97,110,110,111,116,32,104,97,118,101,32,99,104,105,108,100,114,101,110,46,0,67,104,105,108,100,32,97,108,114,101,97,100,121,32,104,97,115,32,97,32,112,97,114,101,110,116,44,32,105,116,32,109,117,115,116,32,98,101,32,114,101,109,111,118,101,100,32,102,105,114,115,116,46,0,67,97,110,110,111,116,32,97,100,100,32,99,104,105,108,100,58,32,78,111,100,101,115,32,119,105,116,104,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,32,99,97,110,110,111,116,32,104,97,118,101,32,99,104,105,108,100,114,101,110,46,0,79,110,108,121,32,108,101,97,102,32,110,111,100,101,115,32,119,105,116,104,32,99,117,115,116,111,109,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,115,104,111,117,108,100,32,109,97,110,117,97,108,108,121,32,109,97,114,107,32,116,104,101,109,115,101,108,118,101,115,32,97,115,32,100,105,114,116,121,0,67,97,110,110,111,116,32,103,101,116,32,108,97,121,111,117,116,32,112,114,111,112,101,114,116,105,101,115,32,111,102,32,109,117,108,116,105,45,101,100,103,101,32,115,104,111,114,116,104,97,110,100,115,0,37,115,37,100,46,123,91,115,107,105,112,112,101,100,93,32,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,97,119,58,32,37,102,32,97,104,58,32,37,102,32,61,62,32,100,58,32,40,37,102,44,32,37,102,41,32,37,115,10,0,37,115,37,100,46,123,37,115,0,42,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,97,119,58,32,37,102,32,97,104,58,32,37,102,32,37,115,10,0,37,115,37,100,46,125,37,115,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,100,58,32,40,37,102,44,32,37,102,41,32,37,115,10,0,79,117,116,32,111,102,32,99,97,99,104,101,32,101,110,116,114,105,101,115,33,10,0,83,99,97,108,101,32,102,97,99,116,111,114,32,115,104,111,117,108,100,32,110,111,116,32,98,101,32,108,101,115,115,32,116,104,97,110,32,122,101,114,111,0,105,110,105,116,105,97,108,0,37,115,10,0,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,0,85,78,68,69,70,73,78,69,68,0,69,88,65,67,84,76,89,0,65,84,95,77,79,83,84,0,76,65,89,95,85,78,68,69,70,73,78,69,68,0,76,65,89,95,69,88,65,67,84,76,89,0,76,65,89,95,65,84,95,77,79,83,84,0,97,118,97,105,108,97,98,108,101,87,105,100,116,104,32,105,115,32,105,110,100,101,102,105,110,105,116,101,32,115,111,32,119,105,100,116,104,77,101,97,115,117,114,101,77,111,100,101,32,109,117,115,116,32,98,101,32,89,71,77,101,97,115,117,114,101,77,111,100,101,85,110,100,101,102,105,110,101,100,0,97,118,97,105,108,97,98,108,101,72,101,105,103,104,116,32,105,115,32,105,110,100,101,102,105,110,105,116,101,32,115,111,32,104,101,105,103,104,116,77,101,97,115,117,114,101,77,111,100,101,32,109,117,115,116,32,98,101,32,89,71,77,101,97,115,117,114,101,77,111,100,101,85,110,100,101,102,105,110,101,100,0,102,108,101,120,0,115,116,114,101,116,99,104,0,109,117,108,116,105,108,105,110,101,45,115,116,114,101,116,99,104,0,69,120,112,101,99,116,101,100,32,110,111,100,101,32,116,111,32,104,97,118,101,32,99,117,115,116,111,109,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,0,109,101,97,115,117,114,101,0,69,120,112,101,99,116,32,99,117,115,116,111,109,32,98,97,115,101,108,105,110,101,32,102,117,110,99,116,105,111,110,32,116,111,32,110,111,116,32,114,101,116,117,114,110,32,78,97,78,0,97,98,115,45,109,101,97,115,117,114,101,0,97,98,115,45,108,97,121,111,117,116,0,78,111,100,101,0,99,114,101,97,116,101,68,101,102,97,117,108,116,0,99,114,101,97,116,101,87,105,116,104,67,111,110,102,105,103,0,100,101,115,116,114,111,121,0,114,101,115,101,116,0,99,111,112,121,83,116,121,108,101,0,115,101,116,80,111,115,105,116,105,111,110,84,121,112,101,0,115,101,116,80,111,115,105,116,105,111,110,0,115,101,116,80,111,115,105,116,105,111,110,80,101,114,99,101,110,116,0,115,101,116,65,108,105,103,110,67,111,110,116,101,110,116,0,115,101,116,65,108,105,103,110,73,116,101,109,115,0,115,101,116,65,108,105,103,110,83,101,108,102,0,115,101,116,70,108,101,120,68,105,114,101,99,116,105,111,110,0,115,101,116,70,108,101,120,87,114,97,112,0,115,101,116,74,117,115,116,105,102,121,67,111,110,116,101,110,116,0,115,101,116,77,97,114,103,105,110,0,115,101,116,77,97,114,103,105,110,80,101,114,99,101,110,116,0,115,101,116,77,97,114,103,105,110,65,117,116,111,0,115,101,116,79,118,101,114,102,108,111,119,0,115,101,116,68,105,115,112,108,97,121,0,115,101,116,70,108,101,120,0,115,101,116,70,108,101,120,66,97,115,105,115,0,115,101,116,70,108,101,120,66,97,115,105,115,80,101,114,99,101,110,116,0,115,101,116,70,108,101,120,71,114,111,119,0,115,101,116,70,108,101,120,83,104,114,105,110,107,0,115,101,116,87,105,100,116,104,0,115,101,116,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,87,105,100,116,104,65,117,116,111,0,115,101,116,72,101,105,103,104,116,0,115,101,116,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,72,101,105,103,104,116,65,117,116,111,0,115,101,116,77,105,110,87,105,100,116,104,0,115,101,116,77,105,110,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,77,105,110,72,101,105,103,104,116,0,115,101,116,77,105,110,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,77,97,120,87,105,100,116,104,0,115,101,116,77,97,120,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,77,97,120,72,101,105,103,104,116,0,115,101,116,77,97,120,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,65,115,112,101,99,116,82,97,116,105,111,0,115,101,116,66,111,114,100,101,114,0,115,101,116,80,97,100,100,105,110,103,0,115,101,116,80,97,100,100,105,110,103,80,101,114,99,101,110,116,0,103,101,116,80,111,115,105,116,105,111,110,84,121,112,101,0,103,101,116,80,111,115,105,116,105,111,110,0,103,101,116,65,108,105,103,110,67,111,110,116,101,110,116,0,103,101,116,65,108,105,103,110,73,116,101,109,115,0,103,101,116,65,108,105,103,110,83,101,108,102,0,103,101,116,70,108,101,120,68,105,114,101,99,116,105,111,110,0,103,101,116,70,108,101,120,87,114,97,112,0,103,101,116,74,117,115,116,105,102,121,67,111,110,116,101,110,116,0,103,101,116,77,97,114,103,105,110,0,103,101,116,70,108,101,120,66,97,115,105,115,0,103,101,116,70,108,101,120,71,114,111,119,0,103,101,116,70,108,101,120,83,104,114,105,110,107,0,103,101,116,87,105,100,116,104,0,103,101,116,72,101,105,103,104,116,0,103,101,116,77,105,110,87,105,100,116,104,0,103,101,116,77,105,110,72,101,105,103,104,116,0,103,101,116,77,97,120,87,105,100,116,104,0,103,101,116,77,97,120,72,101,105,103,104,116,0,103,101,116,65,115,112,101,99,116,82,97,116,105,111,0,103,101,116,66,111,114,100,101,114,0,103,101,116,79,118,101,114,102,108,111,119,0,103,101,116,68,105,115,112,108,97,121,0,103,101,116,80,97,100,100,105,110,103,0,105,110,115,101,114,116,67,104,105,108,100,0,114,101,109,111,118,101,67,104,105,108,100,0,103,101,116,67,104,105,108,100,67,111,117,110,116,0,103,101,116,80,97,114,101,110,116,0,103,101,116,67,104,105,108,100,0,115,101,116,77,101,97,115,117,114,101,70,117,110,99,0,117,110,115,101,116,77,101,97,115,117,114,101,70,117,110,99,0,109,97,114,107,68,105,114,116,121,0,105,115,68,105,114,116,121,0,99,97,108,99,117,108,97,116,101,76,97,121,111,117,116,0,103,101,116,67,111,109,112,117,116,101,100,76,101,102,116,0,103,101,116,67,111,109,112,117,116,101,100,82,105,103,104,116,0,103,101,116,67,111,109,112,117,116,101,100,84,111,112,0,103,101,116,67,111,109,112,117,116,101,100,66,111,116,116,111,109,0,103,101,116,67,111,109,112,117,116,101,100,87,105,100,116,104,0,103,101,116,67,111,109,112,117,116,101,100,72,101,105,103,104,116,0,103,101,116,67,111,109,112,117,116,101,100,76,97,121,111,117,116,0,103,101,116,67,111,109,112,117,116,101,100,77,97,114,103,105,110,0,103,101,116,67,111,109,112,117,116,101,100,66,111,114,100,101,114,0,103,101,116,67,111,109,112,117,116,101,100,80,97,100,100,105,110,103,0,67,111,110,102,105,103,0,99,114,101,97,116,101,0,115,101,116,69,120,112,101,114,105,109,101,110,116,97,108,70,101,97,116,117,114,101,69,110,97,98,108,101,100,0,115,101,116,80,111,105,110,116,83,99,97,108,101,70,97,99,116,111,114,0,105,115,69,120,112,101,114,105,109,101,110,116,97,108,70,101,97,116,117,114,101,69,110,97,98,108,101,100,0,86,97,108,117,101,0,76,97,121,111,117,116,0,83,105,122,101,0,103,101,116,73,110,115,116,97,110,99,101,67,111,117,110,116,0,73,110,116,54,52,0,1,1,1,2,2,4,4,4,4,8,8,4,8,118,111,105,100,0,98,111,111,108,0,115,116,100,58,58,115,116,114,105,110,103,0,99,98,70,117,110,99,116,105,111,110,32,38,0,99,111,110,115,116,32,99,98,70,117,110,99,116,105,111,110,32,38,0,69,120,116,101,114,110,97,108,0,66,117,102,102,101,114,0,78,66,105,110,100,73,68,0,78,66,105,110,100,0,98,105,110,100,95,118,97,108,117,101,0,114,101,102,108,101,99,116,0,113,117,101,114,121,84,121,112,101,0,108,97,108,108,111,99,0,108,114,101,115,101,116,0,123,114,101,116,117,114,110,40,95,110,98,105,110,100,46,99,97,108,108,98,97,99,107,83,105,103,110,97,116,117,114,101,76,105,115,116,91,36,48,93,46,97,112,112,108,121,40,116,104,105,115,44,97,114,103,117,109,101,110,116,115,41,41,59,125,0,95,110,98,105,110,100,95,110,101,119,0,17,0,10,0,17,17,17,0,0,0,0,5,0,0,0,0,0,0,9,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,15,10,17,17,17,3,10,7,0,1,19,9,11,11,0,0,9,6,11,0,0,11,0,6,17,0,0,0,17,17,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,10,10,17,17,17,0,10,0,0,2,0,9,11,0,0,0,9,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,4,13,0,0,0,0,9,14,0,0,0,0,0,14,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,15,0,0,0,0,15,0,0,0,0,9,16,0,0,0,0,0,16,0,0,16,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,10,0,0,0,0,9,11,0,0,0,0,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,45,43,32,32,32,48,88,48,120,0,40,110,117,108,108,41,0,45,48,88,43,48,88,32,48,88,45,48,120,43,48,120,32,48,120,0,105,110,102,0,73,78,70,0,110,97,110,0,78,65,78,0,48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70,46,0,84,33,34,25,13,1,2,3,17,75,28,12,16,4,11,29,18,30,39,104,110,111,112,113,98,32,5,6,15,19,20,21,26,8,22,7,40,36,23,24,9,10,14,27,31,37,35,131,130,125,38,42,43,60,61,62,63,67,71,74,77,88,89,90,91,92,93,94,95,96,97,99,100,101,102,103,105,106,107,108,114,115,116,121,122,123,124,0,73,108,108,101,103,97,108,32,98,121,116,101,32,115,101,113,117,101,110,99,101,0,68,111,109,97,105,110,32,101,114,114,111,114,0,82,101,115,117,108,116,32,110,111,116,32,114,101,112,114,101,115,101,110,116,97,98,108,101,0,78,111,116,32,97,32,116,116,121,0,80,101,114,109,105,115,115,105,111,110,32,100,101,110,105,101,100,0,79,112,101,114,97,116,105,111,110,32,110,111,116,32,112,101,114,109,105,116,116,101,100,0,78,111,32,115,117,99,104,32,102,105,108,101,32,111,114,32,100,105,114,101,99,116,111,114,121,0,78,111,32,115,117,99,104,32,112,114,111,99,101,115,115,0,70,105,108,101,32,101,120,105,115,116,115,0,86,97,108,117,101,32,116,111,111,32,108,97,114,103,101,32,102,111,114,32,100,97,116,97,32,116,121,112,101,0,78,111,32,115,112,97,99,101,32,108,101,102,116,32,111,110,32,100,101,118,105,99,101,0,79,117,116,32,111,102,32,109,101,109,111,114,121,0,82,101,115,111,117,114,99,101,32,98,117,115,121,0,73,110,116,101,114,114,117,112,116,101,100,32,115,121,115,116,101,109,32,99,97,108,108,0,82,101,115,111,117,114,99,101,32,116,101,109,112,111,114,97,114,105,108,121,32,117,110,97,118,97,105,108,97,98,108,101,0,73,110,118,97,108,105,100,32,115,101,101,107,0,67,114,111,115,115,45,100,101,118,105,99,101,32,108,105,110,107,0,82,101,97,100,45,111,110,108,121,32,102,105,108,101,32,115,121,115,116,101,109,0,68,105,114,101,99,116,111,114,121,32,110,111,116,32,101,109,112,116,121,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,112,101,101,114,0,79,112,101,114,97,116,105,111,110,32,116,105,109,101,100,32,111,117,116,0,67,111,110,110,101,99,116,105,111,110,32,114,101,102,117,115,101,100,0,72,111,115,116,32,105,115,32,100,111,119,110,0,72,111,115,116,32,105,115,32,117,110,114,101,97,99,104,97,98,108,101,0,65,100,100,114,101,115,115,32,105,110,32,117,115,101,0,66,114,111,107,101,110,32,112,105,112,101,0,73,47,79,32,101,114,114,111,114,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,32,111,114,32,97,100,100,114,101,115,115,0,66,108,111,99,107,32,100,101,118,105,99,101,32,114,101,113,117,105,114,101,100,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,0,78,111,116,32,97,32,100,105,114,101,99,116,111,114,121,0,73,115,32,97,32,100,105,114,101,99,116,111,114,121,0,84,101,120,116,32,102,105,108,101,32,98,117,115,121,0,69,120,101,99,32,102,111,114,109,97,116,32,101,114,114,111,114,0,73,110,118,97,108,105,100,32,97,114,103,117,109,101,110,116,0,65,114,103,117,109,101,110,116,32,108,105,115,116,32,116,111,111,32,108,111,110,103,0,83,121,109,98,111,108,105,99,32,108,105,110,107,32,108,111,111,112,0,70,105,108,101,110,97,109,101,32,116,111,111,32,108,111,110,103,0,84,111,111,32,109,97,110,121,32,111,112,101,110,32,102,105,108,101,115,32,105,110,32,115,121,115,116,101,109,0,78,111,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,115,32,97,118,97,105,108,97,98,108,101,0,66,97,100,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,0,78,111,32,99,104,105,108,100,32,112,114,111,99,101,115,115,0,66,97,100,32,97,100,100,114,101,115,115,0,70,105,108,101,32,116,111,111,32,108,97,114,103,101,0,84,111,111,32,109,97,110,121,32,108,105,110,107,115,0,78,111,32,108,111,99,107,115,32,97,118,97,105,108,97,98,108,101,0,82,101,115,111,117,114,99,101,32,100,101,97,100,108,111,99,107,32,119,111,117,108,100,32,111,99,99,117,114,0,83,116,97,116,101,32,110,111,116,32,114,101,99,111,118,101,114,97,98,108,101,0,80,114,101,118,105,111,117,115,32,111,119,110,101,114,32,100,105,101,100,0,79,112,101,114,97,116,105,111,110,32,99,97,110,99,101,108,101,100,0,70,117,110,99,116,105,111,110,32,110,111,116,32,105,109,112,108,101,109,101,110,116,101,100,0,78,111,32,109,101,115,115,97,103,101,32,111,102,32,100,101,115,105,114,101,100,32,116,121,112,101,0,73,100,101,110,116,105,102,105,101,114,32,114,101,109,111,118,101,100,0,68,101,118,105,99,101,32,110,111,116,32,97,32,115,116,114,101,97,109,0,78,111,32,100,97,116,97,32,97,118,97,105,108,97,98,108,101,0,68,101,118,105,99,101,32,116,105,109,101,111,117,116,0,79,117,116,32,111,102,32,115,116,114,101,97,109,115,32,114,101,115,111,117,114,99,101,115,0,76,105,110,107,32,104,97,115,32,98,101,101,110,32,115,101,118,101,114,101,100,0,80,114,111,116,111,99,111,108,32,101,114,114,111,114,0,66,97,100,32,109,101,115,115,97,103,101,0,70,105,108,101,32,100,101,115,99,114,105,112,116,111,114,32,105,110,32,98,97,100,32,115,116,97,116,101,0,78,111,116,32,97,32,115,111,99,107,101,116,0,68,101,115,116,105,110,97,116,105,111,110,32,97,100,100,114,101,115,115,32,114,101,113,117,105,114,101,100,0,77,101,115,115,97,103,101,32,116,111,111,32,108,97,114,103,101,0,80,114,111,116,111,99,111,108,32,119,114,111,110,103,32,116,121,112,101,32,102,111,114,32,115,111,99,107,101,116,0,80,114,111,116,111,99,111,108,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,80,114,111,116,111,99,111,108,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,83,111,99,107,101,116,32,116,121,112,101,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,78,111,116,32,115,117,112,112,111,114,116,101,100,0,80,114,111,116,111,99,111,108,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,65,100,100,114,101,115,115,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,32,98,121,32,112,114,111,116,111,99,111,108,0,65,100,100,114,101,115,115,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,78,101,116,119,111,114,107,32,105,115,32,100,111,119,110,0,78,101,116,119,111,114,107,32,117,110,114,101,97,99,104,97,98,108,101,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,110,101,116,119,111,114,107,0,67,111,110,110,101,99,116,105,111,110,32,97,98,111,114,116,101,100,0,78,111,32,98,117,102,102,101,114,32,115,112,97,99,101,32,97,118,97,105,108,97,98,108,101,0,83,111,99,107,101,116,32,105,115,32,99,111,110,110,101,99,116,101,100,0,83,111,99,107,101,116,32,110,111,116,32,99,111,110,110,101,99,116,101,100,0,67,97,110,110,111,116,32,115,101,110,100,32,97,102,116,101,114,32,115,111,99,107,101,116,32,115,104,117,116,100,111,119,110,0,79,112,101,114,97,116,105,111,110,32,97,108,114,101,97,100,121,32,105,110,32,112,114,111,103,114,101,115,115,0,79,112,101,114,97,116,105,111,110,32,105,110,32,112,114,111,103,114,101,115,115,0,83,116,97,108,101,32,102,105,108,101,32,104,97,110,100,108,101,0,82,101,109,111,116,101,32,73,47,79,32,101,114,114,111,114,0,81,117,111,116,97,32,101,120,99,101,101,100,101,100,0,78,111,32,109,101,100,105,117,109,32,102,111,117,110,100,0,87,114,111,110,103,32,109,101,100,105,117,109,32,116,121,112,101,0,78,111,32,101,114,114,111,114,32,105,110,102,111,114,109,97,116,105,111,110,0,0],"i8",ALLOC_NONE,Runtime.GLOBAL_BASE);var tempDoublePtr=STATICTOP;STATICTOP+=16;function _atexit(t,e){__ATEXIT__.unshift({func:t,arg:e})}function ___cxa_atexit(){return _atexit.apply(null,arguments)}function _abort(){Module.abort()}function __ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj(){Module.printErr("missing function: _ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj"),abort(-1)}function __decorate(t,e,r,s){var a=arguments.length,n=a<3?e:s===null?s=Object.getOwnPropertyDescriptor(e,r):s,c;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")n=Reflect.decorate(t,e,r,s);else for(var f=t.length-1;f>=0;f--)(c=t[f])&&(n=(a<3?c(n):a>3?c(e,r,n):c(e,r))||n);return a>3&&n&&Object.defineProperty(e,r,n),n}function _defineHidden(t){return function(e,r){Object.defineProperty(e,r,{configurable:!1,enumerable:!1,value:t,writable:!0})}}var _nbind={};function __nbind_free_external(t){_nbind.externalList[t].dereference(t)}function __nbind_reference_external(t){_nbind.externalList[t].reference()}function _llvm_stackrestore(t){var e=_llvm_stacksave,r=e.LLVM_SAVEDSTACKS[t];e.LLVM_SAVEDSTACKS.splice(t,1),Runtime.stackRestore(r)}function __nbind_register_pool(t,e,r,s){_nbind.Pool.pageSize=t,_nbind.Pool.usedPtr=e/4,_nbind.Pool.rootPtr=r,_nbind.Pool.pagePtr=s/4,HEAP32[e/4]=16909060,HEAP8[e]==1&&(_nbind.bigEndian=!0),HEAP32[e/4]=0,_nbind.makeTypeKindTbl=(n={},n[1024]=_nbind.PrimitiveType,n[64]=_nbind.Int64Type,n[2048]=_nbind.BindClass,n[3072]=_nbind.BindClassPtr,n[4096]=_nbind.SharedClassPtr,n[5120]=_nbind.ArrayType,n[6144]=_nbind.ArrayType,n[7168]=_nbind.CStringType,n[9216]=_nbind.CallbackType,n[10240]=_nbind.BindType,n),_nbind.makeTypeNameTbl={Buffer:_nbind.BufferType,External:_nbind.ExternalType,Int64:_nbind.Int64Type,_nbind_new:_nbind.CreateValueType,bool:_nbind.BooleanType,"cbFunction &":_nbind.CallbackType,"const cbFunction &":_nbind.CallbackType,"const std::string &":_nbind.StringType,"std::string":_nbind.StringType},Module.toggleLightGC=_nbind.toggleLightGC,_nbind.callUpcast=Module.dynCall_ii;var a=_nbind.makeType(_nbind.constructType,{flags:2048,id:0,name:""});a.proto=Module,_nbind.BindClass.list.push(a);var n}function _emscripten_set_main_loop_timing(t,e){if(Browser.mainLoop.timingMode=t,Browser.mainLoop.timingValue=e,!Browser.mainLoop.func)return 1;if(t==0)Browser.mainLoop.scheduler=function(){var c=Math.max(0,Browser.mainLoop.tickStartTime+e-_emscripten_get_now())|0;setTimeout(Browser.mainLoop.runner,c)},Browser.mainLoop.method="timeout";else if(t==1)Browser.mainLoop.scheduler=function(){Browser.requestAnimationFrame(Browser.mainLoop.runner)},Browser.mainLoop.method="rAF";else if(t==2){if(!window.setImmediate){let n=function(c){c.source===window&&c.data===s&&(c.stopPropagation(),r.shift()())};var a=n,r=[],s="setimmediate";window.addEventListener("message",n,!0),window.setImmediate=function(f){r.push(f),ENVIRONMENT_IS_WORKER?(Module.setImmediates===void 0&&(Module.setImmediates=[]),Module.setImmediates.push(f),window.postMessage({target:s})):window.postMessage(s,"*")}}Browser.mainLoop.scheduler=function(){window.setImmediate(Browser.mainLoop.runner)},Browser.mainLoop.method="immediate"}return 0}function _emscripten_get_now(){abort()}function _emscripten_set_main_loop(t,e,r,s,a){Module.noExitRuntime=!0,assert(!Browser.mainLoop.func,"emscripten_set_main_loop: there can only be one main loop function at once: call emscripten_cancel_main_loop to cancel the previous one before setting a new one with different parameters."),Browser.mainLoop.func=t,Browser.mainLoop.arg=s;var n;typeof s<"u"?n=function(){Module.dynCall_vi(t,s)}:n=function(){Module.dynCall_v(t)};var c=Browser.mainLoop.currentlyRunningMainloop;if(Browser.mainLoop.runner=function(){if(!ABORT){if(Browser.mainLoop.queue.length>0){var p=Date.now(),h=Browser.mainLoop.queue.shift();if(h.func(h.arg),Browser.mainLoop.remainingBlockers){var E=Browser.mainLoop.remainingBlockers,C=E%1==0?E-1:Math.floor(E);h.counted?Browser.mainLoop.remainingBlockers=C:(C=C+.5,Browser.mainLoop.remainingBlockers=(8*E+C)/9)}if(console.log('main loop blocker "'+h.name+'" took '+(Date.now()-p)+" ms"),Browser.mainLoop.updateStatus(),c1&&Browser.mainLoop.currentFrameNumber%Browser.mainLoop.timingValue!=0){Browser.mainLoop.scheduler();return}else Browser.mainLoop.timingMode==0&&(Browser.mainLoop.tickStartTime=_emscripten_get_now());Browser.mainLoop.method==="timeout"&&Module.ctx&&(Module.printErr("Looks like you are rendering without using requestAnimationFrame for the main loop. You should use 0 for the frame rate in emscripten_set_main_loop in order to use requestAnimationFrame, as that can greatly improve your frame rates!"),Browser.mainLoop.method=""),Browser.mainLoop.runIter(n),!(c0?_emscripten_set_main_loop_timing(0,1e3/e):_emscripten_set_main_loop_timing(1,1),Browser.mainLoop.scheduler()),r)throw"SimulateInfiniteLoop"}var Browser={mainLoop:{scheduler:null,method:"",currentlyRunningMainloop:0,func:null,arg:0,timingMode:0,timingValue:0,currentFrameNumber:0,queue:[],pause:function(){Browser.mainLoop.scheduler=null,Browser.mainLoop.currentlyRunningMainloop++},resume:function(){Browser.mainLoop.currentlyRunningMainloop++;var t=Browser.mainLoop.timingMode,e=Browser.mainLoop.timingValue,r=Browser.mainLoop.func;Browser.mainLoop.func=null,_emscripten_set_main_loop(r,0,!1,Browser.mainLoop.arg,!0),_emscripten_set_main_loop_timing(t,e),Browser.mainLoop.scheduler()},updateStatus:function(){if(Module.setStatus){var t=Module.statusMessage||"Please wait...",e=Browser.mainLoop.remainingBlockers,r=Browser.mainLoop.expectedBlockers;e?e"u"&&(console.log("warning: Browser does not support creating object URLs. Built-in browser image decoding will not be available."),Module.noImageDecoding=!0);var t={};t.canHandle=function(n){return!Module.noImageDecoding&&/\.(jpg|jpeg|png|bmp)$/i.test(n)},t.handle=function(n,c,f,p){var h=null;if(Browser.hasBlobConstructor)try{h=new Blob([n],{type:Browser.getMimetype(c)}),h.size!==n.length&&(h=new Blob([new Uint8Array(n).buffer],{type:Browser.getMimetype(c)}))}catch(P){Runtime.warnOnce("Blob constructor present but fails: "+P+"; falling back to blob builder")}if(!h){var E=new Browser.BlobBuilder;E.append(new Uint8Array(n).buffer),h=E.getBlob()}var C=Browser.URLObject.createObjectURL(h),S=new Image;S.onload=function(){assert(S.complete,"Image "+c+" could not be decoded");var I=document.createElement("canvas");I.width=S.width,I.height=S.height;var R=I.getContext("2d");R.drawImage(S,0,0),Module.preloadedImages[c]=I,Browser.URLObject.revokeObjectURL(C),f&&f(n)},S.onerror=function(I){console.log("Image "+C+" could not be decoded"),p&&p()},S.src=C},Module.preloadPlugins.push(t);var e={};e.canHandle=function(n){return!Module.noAudioDecoding&&n.substr(-4)in{".ogg":1,".wav":1,".mp3":1}},e.handle=function(n,c,f,p){var h=!1;function E(R){h||(h=!0,Module.preloadedAudios[c]=R,f&&f(n))}function C(){h||(h=!0,Module.preloadedAudios[c]=new Audio,p&&p())}if(Browser.hasBlobConstructor){try{var S=new Blob([n],{type:Browser.getMimetype(c)})}catch{return C()}var P=Browser.URLObject.createObjectURL(S),I=new Audio;I.addEventListener("canplaythrough",function(){E(I)},!1),I.onerror=function(N){if(h)return;console.log("warning: browser could not fully decode audio "+c+", trying slower base64 approach");function U(W){for(var te="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",ie="=",Ae="",ce=0,me=0,pe=0;pe=6;){var Be=ce>>me-6&63;me-=6,Ae+=te[Be]}return me==2?(Ae+=te[(ce&3)<<4],Ae+=ie+ie):me==4&&(Ae+=te[(ce&15)<<2],Ae+=ie),Ae}I.src="data:audio/x-"+c.substr(-3)+";base64,"+U(n),E(I)},I.src=P,Browser.safeSetTimeout(function(){E(I)},1e4)}else return C()},Module.preloadPlugins.push(e);function r(){Browser.pointerLock=document.pointerLockElement===Module.canvas||document.mozPointerLockElement===Module.canvas||document.webkitPointerLockElement===Module.canvas||document.msPointerLockElement===Module.canvas}var s=Module.canvas;s&&(s.requestPointerLock=s.requestPointerLock||s.mozRequestPointerLock||s.webkitRequestPointerLock||s.msRequestPointerLock||function(){},s.exitPointerLock=document.exitPointerLock||document.mozExitPointerLock||document.webkitExitPointerLock||document.msExitPointerLock||function(){},s.exitPointerLock=s.exitPointerLock.bind(document),document.addEventListener("pointerlockchange",r,!1),document.addEventListener("mozpointerlockchange",r,!1),document.addEventListener("webkitpointerlockchange",r,!1),document.addEventListener("mspointerlockchange",r,!1),Module.elementPointerLock&&s.addEventListener("click",function(a){!Browser.pointerLock&&Module.canvas.requestPointerLock&&(Module.canvas.requestPointerLock(),a.preventDefault())},!1))},createContext:function(t,e,r,s){if(e&&Module.ctx&&t==Module.canvas)return Module.ctx;var a,n;if(e){var c={antialias:!1,alpha:!1};if(s)for(var f in s)c[f]=s[f];n=GL.createContext(t,c),n&&(a=GL.getContext(n).GLctx)}else a=t.getContext("2d");return a?(r&&(e||assert(typeof GLctx>"u","cannot set in module if GLctx is used, but we are a non-GL context that would replace it"),Module.ctx=a,e&&GL.makeContextCurrent(n),Module.useWebGL=e,Browser.moduleContextCreatedCallbacks.forEach(function(p){p()}),Browser.init()),a):null},destroyContext:function(t,e,r){},fullscreenHandlersInstalled:!1,lockPointer:void 0,resizeCanvas:void 0,requestFullscreen:function(t,e,r){Browser.lockPointer=t,Browser.resizeCanvas=e,Browser.vrDevice=r,typeof Browser.lockPointer>"u"&&(Browser.lockPointer=!0),typeof Browser.resizeCanvas>"u"&&(Browser.resizeCanvas=!1),typeof Browser.vrDevice>"u"&&(Browser.vrDevice=null);var s=Module.canvas;function a(){Browser.isFullscreen=!1;var c=s.parentNode;(document.fullscreenElement||document.mozFullScreenElement||document.msFullscreenElement||document.webkitFullscreenElement||document.webkitCurrentFullScreenElement)===c?(s.exitFullscreen=document.exitFullscreen||document.cancelFullScreen||document.mozCancelFullScreen||document.msExitFullscreen||document.webkitCancelFullScreen||function(){},s.exitFullscreen=s.exitFullscreen.bind(document),Browser.lockPointer&&s.requestPointerLock(),Browser.isFullscreen=!0,Browser.resizeCanvas&&Browser.setFullscreenCanvasSize()):(c.parentNode.insertBefore(s,c),c.parentNode.removeChild(c),Browser.resizeCanvas&&Browser.setWindowedCanvasSize()),Module.onFullScreen&&Module.onFullScreen(Browser.isFullscreen),Module.onFullscreen&&Module.onFullscreen(Browser.isFullscreen),Browser.updateCanvasDimensions(s)}Browser.fullscreenHandlersInstalled||(Browser.fullscreenHandlersInstalled=!0,document.addEventListener("fullscreenchange",a,!1),document.addEventListener("mozfullscreenchange",a,!1),document.addEventListener("webkitfullscreenchange",a,!1),document.addEventListener("MSFullscreenChange",a,!1));var n=document.createElement("div");s.parentNode.insertBefore(n,s),n.appendChild(s),n.requestFullscreen=n.requestFullscreen||n.mozRequestFullScreen||n.msRequestFullscreen||(n.webkitRequestFullscreen?function(){n.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT)}:null)||(n.webkitRequestFullScreen?function(){n.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT)}:null),r?n.requestFullscreen({vrDisplay:r}):n.requestFullscreen()},requestFullScreen:function(t,e,r){return Module.printErr("Browser.requestFullScreen() is deprecated. Please call Browser.requestFullscreen instead."),Browser.requestFullScreen=function(s,a,n){return Browser.requestFullscreen(s,a,n)},Browser.requestFullscreen(t,e,r)},nextRAF:0,fakeRequestAnimationFrame:function(t){var e=Date.now();if(Browser.nextRAF===0)Browser.nextRAF=e+1e3/60;else for(;e+2>=Browser.nextRAF;)Browser.nextRAF+=1e3/60;var r=Math.max(Browser.nextRAF-e,0);setTimeout(t,r)},requestAnimationFrame:function t(e){typeof window>"u"?Browser.fakeRequestAnimationFrame(e):(window.requestAnimationFrame||(window.requestAnimationFrame=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame||window.oRequestAnimationFrame||Browser.fakeRequestAnimationFrame),window.requestAnimationFrame(e))},safeCallback:function(t){return function(){if(!ABORT)return t.apply(null,arguments)}},allowAsyncCallbacks:!0,queuedAsyncCallbacks:[],pauseAsyncCallbacks:function(){Browser.allowAsyncCallbacks=!1},resumeAsyncCallbacks:function(){if(Browser.allowAsyncCallbacks=!0,Browser.queuedAsyncCallbacks.length>0){var t=Browser.queuedAsyncCallbacks;Browser.queuedAsyncCallbacks=[],t.forEach(function(e){e()})}},safeRequestAnimationFrame:function(t){return Browser.requestAnimationFrame(function(){ABORT||(Browser.allowAsyncCallbacks?t():Browser.queuedAsyncCallbacks.push(t))})},safeSetTimeout:function(t,e){return Module.noExitRuntime=!0,setTimeout(function(){ABORT||(Browser.allowAsyncCallbacks?t():Browser.queuedAsyncCallbacks.push(t))},e)},safeSetInterval:function(t,e){return Module.noExitRuntime=!0,setInterval(function(){ABORT||Browser.allowAsyncCallbacks&&t()},e)},getMimetype:function(t){return{jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",bmp:"image/bmp",ogg:"audio/ogg",wav:"audio/wav",mp3:"audio/mpeg"}[t.substr(t.lastIndexOf(".")+1)]},getUserMedia:function(t){window.getUserMedia||(window.getUserMedia=navigator.getUserMedia||navigator.mozGetUserMedia),window.getUserMedia(t)},getMovementX:function(t){return t.movementX||t.mozMovementX||t.webkitMovementX||0},getMovementY:function(t){return t.movementY||t.mozMovementY||t.webkitMovementY||0},getMouseWheelDelta:function(t){var e=0;switch(t.type){case"DOMMouseScroll":e=t.detail;break;case"mousewheel":e=t.wheelDelta;break;case"wheel":e=t.deltaY;break;default:throw"unrecognized mouse wheel event: "+t.type}return e},mouseX:0,mouseY:0,mouseMovementX:0,mouseMovementY:0,touches:{},lastTouches:{},calculateMouseEvent:function(t){if(Browser.pointerLock)t.type!="mousemove"&&"mozMovementX"in t?Browser.mouseMovementX=Browser.mouseMovementY=0:(Browser.mouseMovementX=Browser.getMovementX(t),Browser.mouseMovementY=Browser.getMovementY(t)),typeof SDL<"u"?(Browser.mouseX=SDL.mouseX+Browser.mouseMovementX,Browser.mouseY=SDL.mouseY+Browser.mouseMovementY):(Browser.mouseX+=Browser.mouseMovementX,Browser.mouseY+=Browser.mouseMovementY);else{var e=Module.canvas.getBoundingClientRect(),r=Module.canvas.width,s=Module.canvas.height,a=typeof window.scrollX<"u"?window.scrollX:window.pageXOffset,n=typeof window.scrollY<"u"?window.scrollY:window.pageYOffset;if(t.type==="touchstart"||t.type==="touchend"||t.type==="touchmove"){var c=t.touch;if(c===void 0)return;var f=c.pageX-(a+e.left),p=c.pageY-(n+e.top);f=f*(r/e.width),p=p*(s/e.height);var h={x:f,y:p};if(t.type==="touchstart")Browser.lastTouches[c.identifier]=h,Browser.touches[c.identifier]=h;else if(t.type==="touchend"||t.type==="touchmove"){var E=Browser.touches[c.identifier];E||(E=h),Browser.lastTouches[c.identifier]=E,Browser.touches[c.identifier]=h}return}var C=t.pageX-(a+e.left),S=t.pageY-(n+e.top);C=C*(r/e.width),S=S*(s/e.height),Browser.mouseMovementX=C-Browser.mouseX,Browser.mouseMovementY=S-Browser.mouseY,Browser.mouseX=C,Browser.mouseY=S}},asyncLoad:function(t,e,r,s){var a=s?"":"al "+t;Module.readAsync(t,function(n){assert(n,'Loading data file "'+t+'" failed (no arrayBuffer).'),e(new Uint8Array(n)),a&&removeRunDependency(a)},function(n){if(r)r();else throw'Loading data file "'+t+'" failed.'}),a&&addRunDependency(a)},resizeListeners:[],updateResizeListeners:function(){var t=Module.canvas;Browser.resizeListeners.forEach(function(e){e(t.width,t.height)})},setCanvasSize:function(t,e,r){var s=Module.canvas;Browser.updateCanvasDimensions(s,t,e),r||Browser.updateResizeListeners()},windowedWidth:0,windowedHeight:0,setFullscreenCanvasSize:function(){if(typeof SDL<"u"){var t=HEAPU32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2];t=t|8388608,HEAP32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2]=t}Browser.updateResizeListeners()},setWindowedCanvasSize:function(){if(typeof SDL<"u"){var t=HEAPU32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2];t=t&-8388609,HEAP32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2]=t}Browser.updateResizeListeners()},updateCanvasDimensions:function(t,e,r){e&&r?(t.widthNative=e,t.heightNative=r):(e=t.widthNative,r=t.heightNative);var s=e,a=r;if(Module.forcedAspectRatio&&Module.forcedAspectRatio>0&&(s/a>2];return e},getStr:function(){var t=Pointer_stringify(SYSCALLS.get());return t},get64:function(){var t=SYSCALLS.get(),e=SYSCALLS.get();return t>=0?assert(e===0):assert(e===-1),t},getZero:function(){assert(SYSCALLS.get()===0)}};function ___syscall6(t,e){SYSCALLS.varargs=e;try{var r=SYSCALLS.getStreamFromFD();return FS.close(r),0}catch(s){return(typeof FS>"u"||!(s instanceof FS.ErrnoError))&&abort(s),-s.errno}}function ___syscall54(t,e){SYSCALLS.varargs=e;try{return 0}catch(r){return(typeof FS>"u"||!(r instanceof FS.ErrnoError))&&abort(r),-r.errno}}function _typeModule(t){var e=[[0,1,"X"],[1,1,"const X"],[128,1,"X *"],[256,1,"X &"],[384,1,"X &&"],[512,1,"std::shared_ptr"],[640,1,"std::unique_ptr"],[5120,1,"std::vector"],[6144,2,"std::array"],[9216,-1,"std::function"]];function r(p,h,E,C,S,P){if(h==1){var I=C&896;(I==128||I==256||I==384)&&(p="X const")}var R;return P?R=E.replace("X",p).replace("Y",S):R=p.replace("X",E).replace("Y",S),R.replace(/([*&]) (?=[*&])/g,"$1")}function s(p,h,E,C,S){throw new Error(p+" type "+E.replace("X",h+"?")+(C?" with flag "+C:"")+" in "+S)}function a(p,h,E,C,S,P,I,R){P===void 0&&(P="X"),R===void 0&&(R=1);var N=E(p);if(N)return N;var U=C(p),W=U.placeholderFlag,te=e[W];I&&te&&(P=r(I[2],I[0],P,te[0],"?",!0));var ie;W==0&&(ie="Unbound"),W>=10&&(ie="Corrupt"),R>20&&(ie="Deeply nested"),ie&&s(ie,p,P,W,S||"?");var Ae=U.paramList[0],ce=a(Ae,h,E,C,S,P,te,R+1),me,pe={flags:te[0],id:p,name:"",paramList:[ce]},Be=[],Ce="?";switch(U.placeholderFlag){case 1:me=ce.spec;break;case 2:if((ce.flags&15360)==1024&&ce.spec.ptrSize==1){pe.flags=7168;break}case 3:case 6:case 5:me=ce.spec,ce.flags&15360;break;case 8:Ce=""+U.paramList[1],pe.paramList.push(U.paramList[1]);break;case 9:for(var g=0,we=U.paramList[1];g>2]=t),t}function _llvm_stacksave(){var t=_llvm_stacksave;return t.LLVM_SAVEDSTACKS||(t.LLVM_SAVEDSTACKS=[]),t.LLVM_SAVEDSTACKS.push(Runtime.stackSave()),t.LLVM_SAVEDSTACKS.length-1}function ___syscall140(t,e){SYSCALLS.varargs=e;try{var r=SYSCALLS.getStreamFromFD(),s=SYSCALLS.get(),a=SYSCALLS.get(),n=SYSCALLS.get(),c=SYSCALLS.get(),f=a;return FS.llseek(r,f,c),HEAP32[n>>2]=r.position,r.getdents&&f===0&&c===0&&(r.getdents=null),0}catch(p){return(typeof FS>"u"||!(p instanceof FS.ErrnoError))&&abort(p),-p.errno}}function ___syscall146(t,e){SYSCALLS.varargs=e;try{var r=SYSCALLS.get(),s=SYSCALLS.get(),a=SYSCALLS.get(),n=0;___syscall146.buffer||(___syscall146.buffers=[null,[],[]],___syscall146.printChar=function(E,C){var S=___syscall146.buffers[E];assert(S),C===0||C===10?((E===1?Module.print:Module.printErr)(UTF8ArrayToString(S,0)),S.length=0):S.push(C)});for(var c=0;c>2],p=HEAP32[s+(c*8+4)>>2],h=0;h"u"||!(E instanceof FS.ErrnoError))&&abort(E),-E.errno}}function __nbind_finish(){for(var t=0,e=_nbind.BindClass.list;tt.pageSize/2||e>t.pageSize-r){var s=_nbind.typeNameTbl.NBind.proto;return s.lalloc(e)}else return HEAPU32[t.usedPtr]=r+e,t.rootPtr+r},t.lreset=function(e,r){var s=HEAPU32[t.pagePtr];if(s){var a=_nbind.typeNameTbl.NBind.proto;a.lreset(e,r)}else HEAPU32[t.usedPtr]=e},t}();_nbind.Pool=Pool;function constructType(t,e){var r=t==10240?_nbind.makeTypeNameTbl[e.name]||_nbind.BindType:_nbind.makeTypeKindTbl[t],s=new r(e);return typeIdTbl[e.id]=s,_nbind.typeNameTbl[e.name]=s,s}_nbind.constructType=constructType;function getType(t){return typeIdTbl[t]}_nbind.getType=getType;function queryType(t){var e=HEAPU8[t],r=_nbind.structureList[e][1];t/=4,r<0&&(++t,r=HEAPU32[t]+1);var s=Array.prototype.slice.call(HEAPU32.subarray(t+1,t+1+r));return e==9&&(s=[s[0],s.slice(1)]),{paramList:s,placeholderFlag:e}}_nbind.queryType=queryType;function getTypes(t,e){return t.map(function(r){return typeof r=="number"?_nbind.getComplexType(r,constructType,getType,queryType,e):_nbind.typeNameTbl[r]})}_nbind.getTypes=getTypes;function readTypeIdList(t,e){return Array.prototype.slice.call(HEAPU32,t/4,t/4+e)}_nbind.readTypeIdList=readTypeIdList;function readAsciiString(t){for(var e=t;HEAPU8[e++];);return String.fromCharCode.apply("",HEAPU8.subarray(t,e-1))}_nbind.readAsciiString=readAsciiString;function readPolicyList(t){var e={};if(t)for(;;){var r=HEAPU32[t/4];if(!r)break;e[readAsciiString(r)]=!0,t+=4}return e}_nbind.readPolicyList=readPolicyList;function getDynCall(t,e){var r={float32_t:"d",float64_t:"d",int64_t:"d",uint64_t:"d",void:"v"},s=t.map(function(n){return r[n.name]||"i"}).join(""),a=Module["dynCall_"+s];if(!a)throw new Error("dynCall_"+s+" not found for "+e+"("+t.map(function(n){return n.name}).join(", ")+")");return a}_nbind.getDynCall=getDynCall;function addMethod(t,e,r,s){var a=t[e];t.hasOwnProperty(e)&&a?((a.arity||a.arity===0)&&(a=_nbind.makeOverloader(a,a.arity),t[e]=a),a.addMethod(r,s)):(r.arity=s,t[e]=r)}_nbind.addMethod=addMethod;function throwError(t){throw new Error(t)}_nbind.throwError=throwError,_nbind.bigEndian=!1,_a=_typeModule(_typeModule),_nbind.Type=_a.Type,_nbind.makeType=_a.makeType,_nbind.getComplexType=_a.getComplexType,_nbind.structureList=_a.structureList;var BindType=function(t){__extends(e,t);function e(){var r=t!==null&&t.apply(this,arguments)||this;return r.heap=HEAPU32,r.ptrSize=4,r}return e.prototype.needsWireRead=function(r){return!!this.wireRead||!!this.makeWireRead},e.prototype.needsWireWrite=function(r){return!!this.wireWrite||!!this.makeWireWrite},e}(_nbind.Type);_nbind.BindType=BindType;var PrimitiveType=function(t){__extends(e,t);function e(r){var s=t.call(this,r)||this,a=r.flags&32?{32:HEAPF32,64:HEAPF64}:r.flags&8?{8:HEAPU8,16:HEAPU16,32:HEAPU32}:{8:HEAP8,16:HEAP16,32:HEAP32};return s.heap=a[r.ptrSize*8],s.ptrSize=r.ptrSize,s}return e.prototype.needsWireWrite=function(r){return!!r&&!!r.Strict},e.prototype.makeWireWrite=function(r,s){return s&&s.Strict&&function(a){if(typeof a=="number")return a;throw new Error("Type mismatch")}},e}(BindType);_nbind.PrimitiveType=PrimitiveType;function pushCString(t,e){if(t==null){if(e&&e.Nullable)return 0;throw new Error("Type mismatch")}if(e&&e.Strict){if(typeof t!="string")throw new Error("Type mismatch")}else t=t.toString();var r=Module.lengthBytesUTF8(t)+1,s=_nbind.Pool.lalloc(r);return Module.stringToUTF8Array(t,HEAPU8,s,r),s}_nbind.pushCString=pushCString;function popCString(t){return t===0?null:Module.Pointer_stringify(t)}_nbind.popCString=popCString;var CStringType=function(t){__extends(e,t);function e(){var r=t!==null&&t.apply(this,arguments)||this;return r.wireRead=popCString,r.wireWrite=pushCString,r.readResources=[_nbind.resources.pool],r.writeResources=[_nbind.resources.pool],r}return e.prototype.makeWireWrite=function(r,s){return function(a){return pushCString(a,s)}},e}(BindType);_nbind.CStringType=CStringType;var BooleanType=function(t){__extends(e,t);function e(){var r=t!==null&&t.apply(this,arguments)||this;return r.wireRead=function(s){return!!s},r}return e.prototype.needsWireWrite=function(r){return!!r&&!!r.Strict},e.prototype.makeWireRead=function(r){return"!!("+r+")"},e.prototype.makeWireWrite=function(r,s){return s&&s.Strict&&function(a){if(typeof a=="boolean")return a;throw new Error("Type mismatch")}||r},e}(BindType);_nbind.BooleanType=BooleanType;var Wrapper=function(){function t(){}return t.prototype.persist=function(){this.__nbindState|=1},t}();_nbind.Wrapper=Wrapper;function makeBound(t,e){var r=function(s){__extends(a,s);function a(n,c,f,p){var h=s.call(this)||this;if(!(h instanceof a))return new(Function.prototype.bind.apply(a,Array.prototype.concat.apply([null],arguments)));var E=c,C=f,S=p;if(n!==_nbind.ptrMarker){var P=h.__nbindConstructor.apply(h,arguments);E=4608,S=HEAPU32[P/4],C=HEAPU32[P/4+1]}var I={configurable:!0,enumerable:!1,value:null,writable:!1},R={__nbindFlags:E,__nbindPtr:C};S&&(R.__nbindShared=S,_nbind.mark(h));for(var N=0,U=Object.keys(R);N>=1;var r=_nbind.valueList[t];return _nbind.valueList[t]=firstFreeValue,firstFreeValue=t,r}else{if(e)return _nbind.popShared(t,e);throw new Error("Invalid value slot "+t)}}_nbind.popValue=popValue;var valueBase=18446744073709552e3;function push64(t){return typeof t=="number"?t:pushValue(t)*4096+valueBase}function pop64(t){return t=3?c=Buffer.from(n):c=new Buffer(n),c.copy(s)}else getBuffer(s).set(n)}}_nbind.commitBuffer=commitBuffer;var dirtyList=[],gcTimer=0;function sweep(){for(var t=0,e=dirtyList;t>2]=DYNAMIC_BASE,staticSealed=!0;function invoke_viiiii(t,e,r,s,a,n){try{Module.dynCall_viiiii(t,e,r,s,a,n)}catch(c){if(typeof c!="number"&&c!=="longjmp")throw c;Module.setThrew(1,0)}}function invoke_vif(t,e,r){try{Module.dynCall_vif(t,e,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_vid(t,e,r){try{Module.dynCall_vid(t,e,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_fiff(t,e,r,s){try{return Module.dynCall_fiff(t,e,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_vi(t,e){try{Module.dynCall_vi(t,e)}catch(r){if(typeof r!="number"&&r!=="longjmp")throw r;Module.setThrew(1,0)}}function invoke_vii(t,e,r){try{Module.dynCall_vii(t,e,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_ii(t,e){try{return Module.dynCall_ii(t,e)}catch(r){if(typeof r!="number"&&r!=="longjmp")throw r;Module.setThrew(1,0)}}function invoke_viddi(t,e,r,s,a){try{Module.dynCall_viddi(t,e,r,s,a)}catch(n){if(typeof n!="number"&&n!=="longjmp")throw n;Module.setThrew(1,0)}}function invoke_vidd(t,e,r,s){try{Module.dynCall_vidd(t,e,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_iiii(t,e,r,s){try{return Module.dynCall_iiii(t,e,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_diii(t,e,r,s){try{return Module.dynCall_diii(t,e,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_di(t,e){try{return Module.dynCall_di(t,e)}catch(r){if(typeof r!="number"&&r!=="longjmp")throw r;Module.setThrew(1,0)}}function invoke_iid(t,e,r){try{return Module.dynCall_iid(t,e,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_iii(t,e,r){try{return Module.dynCall_iii(t,e,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_viiddi(t,e,r,s,a,n){try{Module.dynCall_viiddi(t,e,r,s,a,n)}catch(c){if(typeof c!="number"&&c!=="longjmp")throw c;Module.setThrew(1,0)}}function invoke_viiiiii(t,e,r,s,a,n,c){try{Module.dynCall_viiiiii(t,e,r,s,a,n,c)}catch(f){if(typeof f!="number"&&f!=="longjmp")throw f;Module.setThrew(1,0)}}function invoke_dii(t,e,r){try{return Module.dynCall_dii(t,e,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_i(t){try{return Module.dynCall_i(t)}catch(e){if(typeof e!="number"&&e!=="longjmp")throw e;Module.setThrew(1,0)}}function invoke_iiiiii(t,e,r,s,a,n){try{return Module.dynCall_iiiiii(t,e,r,s,a,n)}catch(c){if(typeof c!="number"&&c!=="longjmp")throw c;Module.setThrew(1,0)}}function invoke_viiid(t,e,r,s,a){try{Module.dynCall_viiid(t,e,r,s,a)}catch(n){if(typeof n!="number"&&n!=="longjmp")throw n;Module.setThrew(1,0)}}function invoke_viififi(t,e,r,s,a,n,c){try{Module.dynCall_viififi(t,e,r,s,a,n,c)}catch(f){if(typeof f!="number"&&f!=="longjmp")throw f;Module.setThrew(1,0)}}function invoke_viii(t,e,r,s){try{Module.dynCall_viii(t,e,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_v(t){try{Module.dynCall_v(t)}catch(e){if(typeof e!="number"&&e!=="longjmp")throw e;Module.setThrew(1,0)}}function invoke_viid(t,e,r,s){try{Module.dynCall_viid(t,e,r,s)}catch(a){if(typeof a!="number"&&a!=="longjmp")throw a;Module.setThrew(1,0)}}function invoke_idd(t,e,r){try{return Module.dynCall_idd(t,e,r)}catch(s){if(typeof s!="number"&&s!=="longjmp")throw s;Module.setThrew(1,0)}}function invoke_viiii(t,e,r,s,a){try{Module.dynCall_viiii(t,e,r,s,a)}catch(n){if(typeof n!="number"&&n!=="longjmp")throw n;Module.setThrew(1,0)}}Module.asmGlobalArg={Math,Int8Array,Int16Array,Int32Array,Uint8Array,Uint16Array,Uint32Array,Float32Array,Float64Array,NaN:NaN,Infinity:1/0},Module.asmLibraryArg={abort,assert,enlargeMemory,getTotalMemory,abortOnCannotGrowMemory,invoke_viiiii,invoke_vif,invoke_vid,invoke_fiff,invoke_vi,invoke_vii,invoke_ii,invoke_viddi,invoke_vidd,invoke_iiii,invoke_diii,invoke_di,invoke_iid,invoke_iii,invoke_viiddi,invoke_viiiiii,invoke_dii,invoke_i,invoke_iiiiii,invoke_viiid,invoke_viififi,invoke_viii,invoke_v,invoke_viid,invoke_idd,invoke_viiii,_emscripten_asm_const_iiiii,_emscripten_asm_const_iiidddddd,_emscripten_asm_const_iiiid,__nbind_reference_external,_emscripten_asm_const_iiiiiiii,_removeAccessorPrefix,_typeModule,__nbind_register_pool,__decorate,_llvm_stackrestore,___cxa_atexit,__extends,__nbind_get_value_object,__ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj,_emscripten_set_main_loop_timing,__nbind_register_primitive,__nbind_register_type,_emscripten_memcpy_big,__nbind_register_function,___setErrNo,__nbind_register_class,__nbind_finish,_abort,_nbind_value,_llvm_stacksave,___syscall54,_defineHidden,_emscripten_set_main_loop,_emscripten_get_now,__nbind_register_callback_signature,_emscripten_asm_const_iiiiii,__nbind_free_external,_emscripten_asm_const_iiii,_emscripten_asm_const_iiididi,___syscall6,_atexit,___syscall140,___syscall146,DYNAMICTOP_PTR,tempDoublePtr,ABORT,STACKTOP,STACK_MAX,cttz_i8,___dso_handle};var asm=function(t,e,r){var s=new t.Int8Array(r),a=new t.Int16Array(r),n=new t.Int32Array(r),c=new t.Uint8Array(r),f=new t.Uint16Array(r),p=new t.Uint32Array(r),h=new t.Float32Array(r),E=new t.Float64Array(r),C=e.DYNAMICTOP_PTR|0,S=e.tempDoublePtr|0,P=e.ABORT|0,I=e.STACKTOP|0,R=e.STACK_MAX|0,N=e.cttz_i8|0,U=e.___dso_handle|0,W=0,te=0,ie=0,Ae=0,ce=t.NaN,me=t.Infinity,pe=0,Be=0,Ce=0,g=0,we=0,ye=0,fe=t.Math.floor,se=t.Math.abs,X=t.Math.sqrt,De=t.Math.pow,Re=t.Math.cos,dt=t.Math.sin,j=t.Math.tan,rt=t.Math.acos,Fe=t.Math.asin,Ne=t.Math.atan,Pe=t.Math.atan2,Ye=t.Math.exp,ke=t.Math.log,it=t.Math.ceil,_e=t.Math.imul,x=t.Math.min,w=t.Math.max,b=t.Math.clz32,y=t.Math.fround,F=e.abort,z=e.assert,Z=e.enlargeMemory,$=e.getTotalMemory,oe=e.abortOnCannotGrowMemory,xe=e.invoke_viiiii,Te=e.invoke_vif,lt=e.invoke_vid,It=e.invoke_fiff,qt=e.invoke_vi,ir=e.invoke_vii,Pt=e.invoke_ii,gn=e.invoke_viddi,Pr=e.invoke_vidd,Ir=e.invoke_iiii,Nr=e.invoke_diii,nn=e.invoke_di,ai=e.invoke_iid,wo=e.invoke_iii,ns=e.invoke_viiddi,to=e.invoke_viiiiii,Bo=e.invoke_dii,ji=e.invoke_i,ro=e.invoke_iiiiii,vo=e.invoke_viiid,RA=e.invoke_viififi,pf=e.invoke_viii,yh=e.invoke_v,Eh=e.invoke_viid,no=e.invoke_idd,jn=e.invoke_viiii,Fs=e._emscripten_asm_const_iiiii,io=e._emscripten_asm_const_iiidddddd,lu=e._emscripten_asm_const_iiiid,cu=e.__nbind_reference_external,uu=e._emscripten_asm_const_iiiiiiii,FA=e._removeAccessorPrefix,NA=e._typeModule,aa=e.__nbind_register_pool,la=e.__decorate,OA=e._llvm_stackrestore,gr=e.___cxa_atexit,So=e.__extends,Me=e.__nbind_get_value_object,fu=e.__ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj,Cr=e._emscripten_set_main_loop_timing,hf=e.__nbind_register_primitive,LA=e.__nbind_register_type,MA=e._emscripten_memcpy_big,Au=e.__nbind_register_function,pu=e.___setErrNo,ac=e.__nbind_register_class,ve=e.__nbind_finish,Nt=e._abort,lc=e._nbind_value,Li=e._llvm_stacksave,so=e.___syscall54,Rt=e._defineHidden,xn=e._emscripten_set_main_loop,ca=e._emscripten_get_now,qi=e.__nbind_register_callback_signature,Mi=e._emscripten_asm_const_iiiiii,Oa=e.__nbind_free_external,dn=e._emscripten_asm_const_iiii,Jn=e._emscripten_asm_const_iiididi,hu=e.___syscall6,Ih=e._atexit,La=e.___syscall140,Ma=e.___syscall146,Ua=y(0);let Xe=y(0);function Ha(o){o=o|0;var l=0;return l=I,I=I+o|0,I=I+15&-16,l|0}function gf(){return I|0}function cc(o){o=o|0,I=o}function wn(o,l){o=o|0,l=l|0,I=o,R=l}function ua(o,l){o=o|0,l=l|0,W||(W=o,te=l)}function _A(o){o=o|0,ye=o}function UA(){return ye|0}function fa(){var o=0,l=0;Qr(8104,8,400)|0,Qr(8504,408,540)|0,o=9044,l=o+44|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));s[9088]=0,s[9089]=1,n[2273]=0,n[2274]=948,n[2275]=948,gr(17,8104,U|0)|0}function vl(o){o=o|0,gt(o+948|0)}function Mt(o){return o=y(o),((EP(o)|0)&2147483647)>>>0>2139095040|0}function kn(o,l,u){o=o|0,l=l|0,u=u|0;e:do if(n[o+(l<<3)+4>>2]|0)o=o+(l<<3)|0;else{if((l|2|0)==3&&n[o+60>>2]|0){o=o+56|0;break}switch(l|0){case 0:case 2:case 4:case 5:{if(n[o+52>>2]|0){o=o+48|0;break e}break}default:}if(n[o+68>>2]|0){o=o+64|0;break}else{o=(l|1|0)==5?948:u;break}}while(!1);return o|0}function Aa(o){o=o|0;var l=0;return l=KP(1e3)|0,ja(o,(l|0)!=0,2456),n[2276]=(n[2276]|0)+1,Qr(l|0,8104,1e3)|0,s[o+2>>0]|0&&(n[l+4>>2]=2,n[l+12>>2]=4),n[l+976>>2]=o,l|0}function ja(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0;d=I,I=I+16|0,A=d,l||(n[A>>2]=u,Gg(o,5,3197,A)),I=d}function is(){return Aa(956)|0}function uc(o){o=o|0;var l=0;return l=Jt(1e3)|0,gu(l,o),ja(n[o+976>>2]|0,1,2456),n[2276]=(n[2276]|0)+1,n[l+944>>2]=0,l|0}function gu(o,l){o=o|0,l=l|0;var u=0;Qr(o|0,l|0,948)|0,vy(o+948|0,l+948|0),u=o+960|0,o=l+960|0,l=u+40|0;do n[u>>2]=n[o>>2],u=u+4|0,o=o+4|0;while((u|0)<(l|0))}function fc(o){o=o|0;var l=0,u=0,A=0,d=0;if(l=o+944|0,u=n[l>>2]|0,u|0&&(qa(u+948|0,o)|0,n[l>>2]=0),u=_i(o)|0,u|0){l=0;do n[(ws(o,l)|0)+944>>2]=0,l=l+1|0;while((l|0)!=(u|0))}u=o+948|0,A=n[u>>2]|0,d=o+952|0,l=n[d>>2]|0,(l|0)!=(A|0)&&(n[d>>2]=l+(~((l+-4-A|0)>>>2)<<2)),Sl(u),JP(o),n[2276]=(n[2276]|0)+-1}function qa(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0;A=n[o>>2]|0,k=o+4|0,u=n[k>>2]|0,m=u;e:do if((A|0)==(u|0))d=A,B=4;else for(o=A;;){if((n[o>>2]|0)==(l|0)){d=o,B=4;break e}if(o=o+4|0,(o|0)==(u|0)){o=0;break}}while(!1);return(B|0)==4&&((d|0)!=(u|0)?(A=d+4|0,o=m-A|0,l=o>>2,l&&(Q2(d|0,A|0,o|0)|0,u=n[k>>2]|0),o=d+(l<<2)|0,(u|0)==(o|0)||(n[k>>2]=u+(~((u+-4-o|0)>>>2)<<2)),o=1):o=0),o|0}function _i(o){return o=o|0,(n[o+952>>2]|0)-(n[o+948>>2]|0)>>2|0}function ws(o,l){o=o|0,l=l|0;var u=0;return u=n[o+948>>2]|0,(n[o+952>>2]|0)-u>>2>>>0>l>>>0?o=n[u+(l<<2)>>2]|0:o=0,o|0}function Sl(o){o=o|0;var l=0,u=0,A=0,d=0;A=I,I=I+32|0,l=A,d=n[o>>2]|0,u=(n[o+4>>2]|0)-d|0,((n[o+8>>2]|0)-d|0)>>>0>u>>>0&&(d=u>>2,Py(l,d,d,o+8|0),IP(o,l),xy(l)),I=A}function df(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0;_=_i(o)|0;do if(_|0){if((n[(ws(o,0)|0)+944>>2]|0)==(o|0)){if(!(qa(o+948|0,l)|0))break;Qr(l+400|0,8504,540)|0,n[l+944>>2]=0,Oe(o);break}B=n[(n[o+976>>2]|0)+12>>2]|0,k=o+948|0,T=(B|0)==0,u=0,m=0;do A=n[(n[k>>2]|0)+(m<<2)>>2]|0,(A|0)==(l|0)?Oe(o):(d=uc(A)|0,n[(n[k>>2]|0)+(u<<2)>>2]=d,n[d+944>>2]=o,T||y_[B&15](A,d,o,u),u=u+1|0),m=m+1|0;while((m|0)!=(_|0));if(u>>>0<_>>>0){T=o+948|0,k=o+952|0,B=u,u=n[k>>2]|0;do m=(n[T>>2]|0)+(B<<2)|0,A=m+4|0,d=u-A|0,l=d>>2,l&&(Q2(m|0,A|0,d|0)|0,u=n[k>>2]|0),d=u,A=m+(l<<2)|0,(d|0)!=(A|0)&&(u=d+(~((d+-4-A|0)>>>2)<<2)|0,n[k>>2]=u),B=B+1|0;while((B|0)!=(_|0))}}while(!1)}function Ac(o){o=o|0;var l=0,u=0,A=0,d=0;Bi(o,(_i(o)|0)==0,2491),Bi(o,(n[o+944>>2]|0)==0,2545),l=o+948|0,u=n[l>>2]|0,A=o+952|0,d=n[A>>2]|0,(d|0)!=(u|0)&&(n[A>>2]=d+(~((d+-4-u|0)>>>2)<<2)),Sl(l),l=o+976|0,u=n[l>>2]|0,Qr(o|0,8104,1e3)|0,s[u+2>>0]|0&&(n[o+4>>2]=2,n[o+12>>2]=4),n[l>>2]=u}function Bi(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0;d=I,I=I+16|0,A=d,l||(n[A>>2]=u,Qo(o,5,3197,A)),I=d}function Qn(){return n[2276]|0}function pc(){var o=0;return o=KP(20)|0,Je((o|0)!=0,2592),n[2277]=(n[2277]|0)+1,n[o>>2]=n[239],n[o+4>>2]=n[240],n[o+8>>2]=n[241],n[o+12>>2]=n[242],n[o+16>>2]=n[243],o|0}function Je(o,l){o=o|0,l=l|0;var u=0,A=0;A=I,I=I+16|0,u=A,o||(n[u>>2]=l,Qo(0,5,3197,u)),I=A}function st(o){o=o|0,JP(o),n[2277]=(n[2277]|0)+-1}function St(o,l){o=o|0,l=l|0;var u=0;l?(Bi(o,(_i(o)|0)==0,2629),u=1):(u=0,l=0),n[o+964>>2]=l,n[o+988>>2]=u}function lr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;A=I,I=I+16|0,m=A+8|0,d=A+4|0,B=A,n[d>>2]=l,Bi(o,(n[l+944>>2]|0)==0,2709),Bi(o,(n[o+964>>2]|0)==0,2763),ee(o),l=o+948|0,n[B>>2]=(n[l>>2]|0)+(u<<2),n[m>>2]=n[B>>2],Ee(l,m,d)|0,n[(n[d>>2]|0)+944>>2]=o,Oe(o),I=A}function ee(o){o=o|0;var l=0,u=0,A=0,d=0,m=0,B=0,k=0;if(u=_i(o)|0,u|0&&(n[(ws(o,0)|0)+944>>2]|0)!=(o|0)){A=n[(n[o+976>>2]|0)+12>>2]|0,d=o+948|0,m=(A|0)==0,l=0;do B=n[(n[d>>2]|0)+(l<<2)>>2]|0,k=uc(B)|0,n[(n[d>>2]|0)+(l<<2)>>2]=k,n[k+944>>2]=o,m||y_[A&15](B,k,o,l),l=l+1|0;while((l|0)!=(u|0))}}function Ee(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0,Le=0,Qe=0,tt=0,Ze=0;tt=I,I=I+64|0,G=tt+52|0,k=tt+48|0,ae=tt+28|0,We=tt+24|0,Le=tt+20|0,Qe=tt,A=n[o>>2]|0,m=A,l=A+((n[l>>2]|0)-m>>2<<2)|0,A=o+4|0,d=n[A>>2]|0,B=o+8|0;do if(d>>>0<(n[B>>2]|0)>>>0){if((l|0)==(d|0)){n[l>>2]=n[u>>2],n[A>>2]=(n[A>>2]|0)+4;break}CP(o,l,d,l+4|0),l>>>0<=u>>>0&&(u=(n[A>>2]|0)>>>0>u>>>0?u+4|0:u),n[l>>2]=n[u>>2]}else{A=(d-m>>2)+1|0,d=O(o)|0,d>>>0>>0&&sn(o),M=n[o>>2]|0,_=(n[B>>2]|0)-M|0,m=_>>1,Py(Qe,_>>2>>>0>>1>>>0?m>>>0>>0?A:m:d,l-M>>2,o+8|0),M=Qe+8|0,A=n[M>>2]|0,m=Qe+12|0,_=n[m>>2]|0,B=_,T=A;do if((A|0)==(_|0)){if(_=Qe+4|0,A=n[_>>2]|0,Ze=n[Qe>>2]|0,d=Ze,A>>>0<=Ze>>>0){A=B-d>>1,A=A|0?A:1,Py(ae,A,A>>>2,n[Qe+16>>2]|0),n[We>>2]=n[_>>2],n[Le>>2]=n[M>>2],n[k>>2]=n[We>>2],n[G>>2]=n[Le>>2],o2(ae,k,G),A=n[Qe>>2]|0,n[Qe>>2]=n[ae>>2],n[ae>>2]=A,A=ae+4|0,Ze=n[_>>2]|0,n[_>>2]=n[A>>2],n[A>>2]=Ze,A=ae+8|0,Ze=n[M>>2]|0,n[M>>2]=n[A>>2],n[A>>2]=Ze,A=ae+12|0,Ze=n[m>>2]|0,n[m>>2]=n[A>>2],n[A>>2]=Ze,xy(ae),A=n[M>>2]|0;break}m=A,B=((m-d>>2)+1|0)/-2|0,k=A+(B<<2)|0,d=T-m|0,m=d>>2,m&&(Q2(k|0,A|0,d|0)|0,A=n[_>>2]|0),Ze=k+(m<<2)|0,n[M>>2]=Ze,n[_>>2]=A+(B<<2),A=Ze}while(!1);n[A>>2]=n[u>>2],n[M>>2]=(n[M>>2]|0)+4,l=wP(o,Qe,l)|0,xy(Qe)}while(!1);return I=tt,l|0}function Oe(o){o=o|0;var l=0;do{if(l=o+984|0,s[l>>0]|0)break;s[l>>0]=1,h[o+504>>2]=y(ce),o=n[o+944>>2]|0}while(o|0)}function gt(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-4-A|0)>>>2)<<2)),Et(u))}function yt(o){return o=o|0,n[o+944>>2]|0}function Dt(o){o=o|0,Bi(o,(n[o+964>>2]|0)!=0,2832),Oe(o)}function tr(o){return o=o|0,(s[o+984>>0]|0)!=0|0}function fn(o,l){o=o|0,l=l|0,EYe(o,l,400)|0&&(Qr(o|0,l|0,400)|0,Oe(o))}function li(o){o=o|0;var l=Xe;return l=y(h[o+44>>2]),o=Mt(l)|0,y(o?y(0):l)}function Gi(o){o=o|0;var l=Xe;return l=y(h[o+48>>2]),Mt(l)|0&&(l=s[(n[o+976>>2]|0)+2>>0]|0?y(1):y(0)),y(l)}function Tn(o,l){o=o|0,l=l|0,n[o+980>>2]=l}function Ga(o){return o=o|0,n[o+980>>2]|0}function gy(o,l){o=o|0,l=l|0;var u=0;u=o+4|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function X1(o){return o=o|0,n[o+4>>2]|0}function Do(o,l){o=o|0,l=l|0;var u=0;u=o+8|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function dy(o){return o=o|0,n[o+8>>2]|0}function Ch(o,l){o=o|0,l=l|0;var u=0;u=o+12|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function $1(o){return o=o|0,n[o+12>>2]|0}function bo(o,l){o=o|0,l=l|0;var u=0;u=o+16|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function wh(o){return o=o|0,n[o+16>>2]|0}function Bh(o,l){o=o|0,l=l|0;var u=0;u=o+20|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function du(o){return o=o|0,n[o+20>>2]|0}function vh(o,l){o=o|0,l=l|0;var u=0;u=o+24|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Rg(o){return o=o|0,n[o+24>>2]|0}function Fg(o,l){o=o|0,l=l|0;var u=0;u=o+28|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Ng(o){return o=o|0,n[o+28>>2]|0}function my(o,l){o=o|0,l=l|0;var u=0;u=o+32|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function mf(o){return o=o|0,n[o+32>>2]|0}function Po(o,l){o=o|0,l=l|0;var u=0;u=o+36|0,(n[u>>2]|0)!=(l|0)&&(n[u>>2]=l,Oe(o))}function Dl(o){return o=o|0,n[o+36>>2]|0}function Sh(o,l){o=o|0,l=y(l);var u=0;u=o+40|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function Og(o,l){o=o|0,l=y(l);var u=0;u=o+44|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function bl(o,l){o=o|0,l=y(l);var u=0;u=o+48|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function Pl(o,l){o=o|0,l=y(l);var u=0,A=0,d=0,m=0;m=Mt(l)|0,u=(m^1)&1,A=o+52|0,d=o+56|0,m|y(h[A>>2])==l&&(n[d>>2]|0)==(u|0)||(h[A>>2]=l,n[d>>2]=u,Oe(o))}function yy(o,l){o=o|0,l=y(l);var u=0,A=0;A=o+52|0,u=o+56|0,y(h[A>>2])==l&&(n[u>>2]|0)==2||(h[A>>2]=l,A=Mt(l)|0,n[u>>2]=A?3:2,Oe(o))}function HA(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+52|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function Ey(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,d=0,m=0;m=Mt(u)|0,A=(m^1)&1,d=o+132+(l<<3)|0,l=o+132+(l<<3)+4|0,m|y(h[d>>2])==u&&(n[l>>2]|0)==(A|0)||(h[d>>2]=u,n[l>>2]=A,Oe(o))}function Iy(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,d=0,m=0;m=Mt(u)|0,A=m?0:2,d=o+132+(l<<3)|0,l=o+132+(l<<3)+4|0,m|y(h[d>>2])==u&&(n[l>>2]|0)==(A|0)||(h[d>>2]=u,n[l>>2]=A,Oe(o))}function jA(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=l+132+(u<<3)|0,l=n[A+4>>2]|0,u=o,n[u>>2]=n[A>>2],n[u+4>>2]=l}function qA(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,d=0,m=0;m=Mt(u)|0,A=(m^1)&1,d=o+60+(l<<3)|0,l=o+60+(l<<3)+4|0,m|y(h[d>>2])==u&&(n[l>>2]|0)==(A|0)||(h[d>>2]=u,n[l>>2]=A,Oe(o))}function Y(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,d=0,m=0;m=Mt(u)|0,A=m?0:2,d=o+60+(l<<3)|0,l=o+60+(l<<3)+4|0,m|y(h[d>>2])==u&&(n[l>>2]|0)==(A|0)||(h[d>>2]=u,n[l>>2]=A,Oe(o))}function xt(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=l+60+(u<<3)|0,l=n[A+4>>2]|0,u=o,n[u>>2]=n[A>>2],n[u+4>>2]=l}function GA(o,l){o=o|0,l=l|0;var u=0;u=o+60+(l<<3)+4|0,(n[u>>2]|0)!=3&&(h[o+60+(l<<3)>>2]=y(ce),n[u>>2]=3,Oe(o))}function xo(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,d=0,m=0;m=Mt(u)|0,A=(m^1)&1,d=o+204+(l<<3)|0,l=o+204+(l<<3)+4|0,m|y(h[d>>2])==u&&(n[l>>2]|0)==(A|0)||(h[d>>2]=u,n[l>>2]=A,Oe(o))}function yf(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,d=0,m=0;m=Mt(u)|0,A=m?0:2,d=o+204+(l<<3)|0,l=o+204+(l<<3)+4|0,m|y(h[d>>2])==u&&(n[l>>2]|0)==(A|0)||(h[d>>2]=u,n[l>>2]=A,Oe(o))}function mt(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=l+204+(u<<3)|0,l=n[A+4>>2]|0,u=o,n[u>>2]=n[A>>2],n[u+4>>2]=l}function mu(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,d=0,m=0;m=Mt(u)|0,A=(m^1)&1,d=o+276+(l<<3)|0,l=o+276+(l<<3)+4|0,m|y(h[d>>2])==u&&(n[l>>2]|0)==(A|0)||(h[d>>2]=u,n[l>>2]=A,Oe(o))}function Cy(o,l){return o=o|0,l=l|0,y(h[o+276+(l<<3)>>2])}function Lg(o,l){o=o|0,l=y(l);var u=0,A=0,d=0,m=0;m=Mt(l)|0,u=(m^1)&1,A=o+348|0,d=o+352|0,m|y(h[A>>2])==l&&(n[d>>2]|0)==(u|0)||(h[A>>2]=l,n[d>>2]=u,Oe(o))}function e2(o,l){o=o|0,l=y(l);var u=0,A=0;A=o+348|0,u=o+352|0,y(h[A>>2])==l&&(n[u>>2]|0)==2||(h[A>>2]=l,A=Mt(l)|0,n[u>>2]=A?3:2,Oe(o))}function Dh(o){o=o|0;var l=0;l=o+352|0,(n[l>>2]|0)!=3&&(h[o+348>>2]=y(ce),n[l>>2]=3,Oe(o))}function ur(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+348|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function Zi(o,l){o=o|0,l=y(l);var u=0,A=0,d=0,m=0;m=Mt(l)|0,u=(m^1)&1,A=o+356|0,d=o+360|0,m|y(h[A>>2])==l&&(n[d>>2]|0)==(u|0)||(h[A>>2]=l,n[d>>2]=u,Oe(o))}function Ef(o,l){o=o|0,l=y(l);var u=0,A=0;A=o+356|0,u=o+360|0,y(h[A>>2])==l&&(n[u>>2]|0)==2||(h[A>>2]=l,A=Mt(l)|0,n[u>>2]=A?3:2,Oe(o))}function Wa(o){o=o|0;var l=0;l=o+360|0,(n[l>>2]|0)!=3&&(h[o+356>>2]=y(ce),n[l>>2]=3,Oe(o))}function Mg(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+356|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function yu(o,l){o=o|0,l=y(l);var u=0,A=0,d=0,m=0;m=Mt(l)|0,u=(m^1)&1,A=o+364|0,d=o+368|0,m|y(h[A>>2])==l&&(n[d>>2]|0)==(u|0)||(h[A>>2]=l,n[d>>2]=u,Oe(o))}function If(o,l){o=o|0,l=y(l);var u=0,A=0,d=0,m=0;m=Mt(l)|0,u=m?0:2,A=o+364|0,d=o+368|0,m|y(h[A>>2])==l&&(n[d>>2]|0)==(u|0)||(h[A>>2]=l,n[d>>2]=u,Oe(o))}function wt(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+364|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function di(o,l){o=o|0,l=y(l);var u=0,A=0,d=0,m=0;m=Mt(l)|0,u=(m^1)&1,A=o+372|0,d=o+376|0,m|y(h[A>>2])==l&&(n[d>>2]|0)==(u|0)||(h[A>>2]=l,n[d>>2]=u,Oe(o))}function WA(o,l){o=o|0,l=y(l);var u=0,A=0,d=0,m=0;m=Mt(l)|0,u=m?0:2,A=o+372|0,d=o+376|0,m|y(h[A>>2])==l&&(n[d>>2]|0)==(u|0)||(h[A>>2]=l,n[d>>2]=u,Oe(o))}function Ya(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+372|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function pa(o,l){o=o|0,l=y(l);var u=0,A=0,d=0,m=0;m=Mt(l)|0,u=(m^1)&1,A=o+380|0,d=o+384|0,m|y(h[A>>2])==l&&(n[d>>2]|0)==(u|0)||(h[A>>2]=l,n[d>>2]=u,Oe(o))}function Va(o,l){o=o|0,l=y(l);var u=0,A=0,d=0,m=0;m=Mt(l)|0,u=m?0:2,A=o+380|0,d=o+384|0,m|y(h[A>>2])==l&&(n[d>>2]|0)==(u|0)||(h[A>>2]=l,n[d>>2]=u,Oe(o))}function _g(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+380|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function bh(o,l){o=o|0,l=y(l);var u=0,A=0,d=0,m=0;m=Mt(l)|0,u=(m^1)&1,A=o+388|0,d=o+392|0,m|y(h[A>>2])==l&&(n[d>>2]|0)==(u|0)||(h[A>>2]=l,n[d>>2]=u,Oe(o))}function Ug(o,l){o=o|0,l=y(l);var u=0,A=0,d=0,m=0;m=Mt(l)|0,u=m?0:2,A=o+388|0,d=o+392|0,m|y(h[A>>2])==l&&(n[d>>2]|0)==(u|0)||(h[A>>2]=l,n[d>>2]=u,Oe(o))}function wy(o,l){o=o|0,l=l|0;var u=0,A=0;A=l+388|0,u=n[A+4>>2]|0,l=o,n[l>>2]=n[A>>2],n[l+4>>2]=u}function YA(o,l){o=o|0,l=y(l);var u=0;u=o+396|0,y(h[u>>2])!=l&&(h[u>>2]=l,Oe(o))}function Hg(o){return o=o|0,y(h[o+396>>2])}function Eu(o){return o=o|0,y(h[o+400>>2])}function Iu(o){return o=o|0,y(h[o+404>>2])}function Cf(o){return o=o|0,y(h[o+408>>2])}function Ns(o){return o=o|0,y(h[o+412>>2])}function Cu(o){return o=o|0,y(h[o+416>>2])}function qn(o){return o=o|0,y(h[o+420>>2])}function ss(o,l){switch(o=o|0,l=l|0,Bi(o,(l|0)<6,2918),l|0){case 0:{l=(n[o+496>>2]|0)==2?5:4;break}case 2:{l=(n[o+496>>2]|0)==2?4:5;break}default:}return y(h[o+424+(l<<2)>>2])}function ki(o,l){switch(o=o|0,l=l|0,Bi(o,(l|0)<6,2918),l|0){case 0:{l=(n[o+496>>2]|0)==2?5:4;break}case 2:{l=(n[o+496>>2]|0)==2?4:5;break}default:}return y(h[o+448+(l<<2)>>2])}function VA(o,l){switch(o=o|0,l=l|0,Bi(o,(l|0)<6,2918),l|0){case 0:{l=(n[o+496>>2]|0)==2?5:4;break}case 2:{l=(n[o+496>>2]|0)==2?4:5;break}default:}return y(h[o+472+(l<<2)>>2])}function wf(o,l){o=o|0,l=l|0;var u=0,A=Xe;return u=n[o+4>>2]|0,(u|0)==(n[l+4>>2]|0)?u?(A=y(h[o>>2]),o=y(se(y(A-y(h[l>>2]))))>2]=0,n[A+4>>2]=0,n[A+8>>2]=0,fu(A|0,o|0,l|0,0),Qo(o,3,(s[A+11>>0]|0)<0?n[A>>2]|0:A,u),jYe(A),I=u}function os(o,l,u,A){o=y(o),l=y(l),u=u|0,A=A|0;var d=Xe;o=y(o*l),d=y(A_(o,y(1)));do if(mn(d,y(0))|0)o=y(o-d);else{if(o=y(o-d),mn(d,y(1))|0){o=y(o+y(1));break}if(u){o=y(o+y(1));break}A||(d>y(.5)?d=y(1):(A=mn(d,y(.5))|0,d=y(A?1:0)),o=y(o+d))}while(!1);return y(o/l)}function xl(o,l,u,A,d,m,B,k,T,_,M,G,ae){o=o|0,l=y(l),u=u|0,A=y(A),d=d|0,m=y(m),B=B|0,k=y(k),T=y(T),_=y(_),M=y(M),G=y(G),ae=ae|0;var We=0,Le=Xe,Qe=Xe,tt=Xe,Ze=Xe,ct=Xe,He=Xe;return T>2]),Le!=y(0))?(tt=y(os(l,Le,0,0)),Ze=y(os(A,Le,0,0)),Qe=y(os(m,Le,0,0)),Le=y(os(k,Le,0,0))):(Qe=m,tt=l,Le=k,Ze=A),(d|0)==(o|0)?We=mn(Qe,tt)|0:We=0,(B|0)==(u|0)?ae=mn(Le,Ze)|0:ae=0,!We&&(ct=y(l-M),!(ko(o,ct,T)|0))&&!(Bf(o,ct,d,T)|0)?We=vf(o,ct,d,m,T)|0:We=1,!ae&&(He=y(A-G),!(ko(u,He,_)|0))&&!(Bf(u,He,B,_)|0)?ae=vf(u,He,B,k,_)|0:ae=1,ae=We&ae),ae|0}function ko(o,l,u){return o=o|0,l=y(l),u=y(u),(o|0)==1?o=mn(l,u)|0:o=0,o|0}function Bf(o,l,u,A){return o=o|0,l=y(l),u=u|0,A=y(A),(o|0)==2&(u|0)==0?l>=A?o=1:o=mn(l,A)|0:o=0,o|0}function vf(o,l,u,A,d){return o=o|0,l=y(l),u=u|0,A=y(A),d=y(d),(o|0)==2&(u|0)==2&A>l?d<=l?o=1:o=mn(l,d)|0:o=0,o|0}function kl(o,l,u,A,d,m,B,k,T,_,M){o=o|0,l=y(l),u=y(u),A=A|0,d=d|0,m=m|0,B=y(B),k=y(k),T=T|0,_=_|0,M=M|0;var G=0,ae=0,We=0,Le=0,Qe=Xe,tt=Xe,Ze=0,ct=0,He=0,Ge=0,Lt=0,qr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,Hn=Xe,Fo=Xe,No=Xe,Oo=0,$a=0;cr=I,I=I+160|0,$t=cr+152|0,fr=cr+120|0,qr=cr+104|0,He=cr+72|0,Le=cr+56|0,Lt=cr+8|0,ct=cr,Ge=(n[2279]|0)+1|0,n[2279]=Ge,Tr=o+984|0,s[Tr>>0]|0&&(n[o+512>>2]|0)!=(n[2278]|0)?Ze=4:(n[o+516>>2]|0)==(A|0)?Hr=0:Ze=4,(Ze|0)==4&&(n[o+520>>2]=0,n[o+924>>2]=-1,n[o+928>>2]=-1,h[o+932>>2]=y(-1),h[o+936>>2]=y(-1),Hr=1);e:do if(n[o+964>>2]|0)if(Qe=y(yn(o,2,B)),tt=y(yn(o,0,B)),G=o+916|0,No=y(h[G>>2]),Fo=y(h[o+920>>2]),Hn=y(h[o+932>>2]),xl(d,l,m,u,n[o+924>>2]|0,No,n[o+928>>2]|0,Fo,Hn,y(h[o+936>>2]),Qe,tt,M)|0)Ze=22;else if(We=n[o+520>>2]|0,!We)Ze=21;else for(ae=0;;){if(G=o+524+(ae*24|0)|0,Hn=y(h[G>>2]),Fo=y(h[o+524+(ae*24|0)+4>>2]),No=y(h[o+524+(ae*24|0)+16>>2]),xl(d,l,m,u,n[o+524+(ae*24|0)+8>>2]|0,Hn,n[o+524+(ae*24|0)+12>>2]|0,Fo,No,y(h[o+524+(ae*24|0)+20>>2]),Qe,tt,M)|0){Ze=22;break e}if(ae=ae+1|0,ae>>>0>=We>>>0){Ze=21;break}}else{if(T){if(G=o+916|0,!(mn(y(h[G>>2]),l)|0)){Ze=21;break}if(!(mn(y(h[o+920>>2]),u)|0)){Ze=21;break}if((n[o+924>>2]|0)!=(d|0)){Ze=21;break}G=(n[o+928>>2]|0)==(m|0)?G:0,Ze=22;break}if(We=n[o+520>>2]|0,!We)Ze=21;else for(ae=0;;){if(G=o+524+(ae*24|0)|0,mn(y(h[G>>2]),l)|0&&mn(y(h[o+524+(ae*24|0)+4>>2]),u)|0&&(n[o+524+(ae*24|0)+8>>2]|0)==(d|0)&&(n[o+524+(ae*24|0)+12>>2]|0)==(m|0)){Ze=22;break e}if(ae=ae+1|0,ae>>>0>=We>>>0){Ze=21;break}}}while(!1);do if((Ze|0)==21)s[11697]|0?(G=0,Ze=28):(G=0,Ze=31);else if((Ze|0)==22){if(ae=(s[11697]|0)!=0,!((G|0)!=0&(Hr^1)))if(ae){Ze=28;break}else{Ze=31;break}Le=G+16|0,n[o+908>>2]=n[Le>>2],We=G+20|0,n[o+912>>2]=n[We>>2],(s[11698]|0)==0|ae^1||(n[ct>>2]=wu(Ge)|0,n[ct+4>>2]=Ge,Qo(o,4,2972,ct),ae=n[o+972>>2]|0,ae|0&&op[ae&127](o),d=ha(d,T)|0,m=ha(m,T)|0,$a=+y(h[Le>>2]),Oo=+y(h[We>>2]),n[Lt>>2]=d,n[Lt+4>>2]=m,E[Lt+8>>3]=+l,E[Lt+16>>3]=+u,E[Lt+24>>3]=$a,E[Lt+32>>3]=Oo,n[Lt+40>>2]=_,Qo(o,4,2989,Lt))}while(!1);return(Ze|0)==28&&(ae=wu(Ge)|0,n[Le>>2]=ae,n[Le+4>>2]=Ge,n[Le+8>>2]=Hr?3047:11699,Qo(o,4,3038,Le),ae=n[o+972>>2]|0,ae|0&&op[ae&127](o),Lt=ha(d,T)|0,Ze=ha(m,T)|0,n[He>>2]=Lt,n[He+4>>2]=Ze,E[He+8>>3]=+l,E[He+16>>3]=+u,n[He+24>>2]=_,Qo(o,4,3049,He),Ze=31),(Ze|0)==31&&(Os(o,l,u,A,d,m,B,k,T,M),s[11697]|0&&(ae=n[2279]|0,Lt=wu(ae)|0,n[qr>>2]=Lt,n[qr+4>>2]=ae,n[qr+8>>2]=Hr?3047:11699,Qo(o,4,3083,qr),ae=n[o+972>>2]|0,ae|0&&op[ae&127](o),Lt=ha(d,T)|0,qr=ha(m,T)|0,Oo=+y(h[o+908>>2]),$a=+y(h[o+912>>2]),n[fr>>2]=Lt,n[fr+4>>2]=qr,E[fr+8>>3]=Oo,E[fr+16>>3]=$a,n[fr+24>>2]=_,Qo(o,4,3092,fr)),n[o+516>>2]=A,G||(ae=o+520|0,G=n[ae>>2]|0,(G|0)==16&&(s[11697]|0&&Qo(o,4,3124,$t),n[ae>>2]=0,G=0),T?G=o+916|0:(n[ae>>2]=G+1,G=o+524+(G*24|0)|0),h[G>>2]=l,h[G+4>>2]=u,n[G+8>>2]=d,n[G+12>>2]=m,n[G+16>>2]=n[o+908>>2],n[G+20>>2]=n[o+912>>2],G=0)),T&&(n[o+416>>2]=n[o+908>>2],n[o+420>>2]=n[o+912>>2],s[o+985>>0]=1,s[Tr>>0]=0),n[2279]=(n[2279]|0)+-1,n[o+512>>2]=n[2278],I=cr,Hr|(G|0)==0|0}function yn(o,l,u){o=o|0,l=l|0,u=y(u);var A=Xe;return A=y(J(o,l,u)),y(A+y(re(o,l,u)))}function Qo(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;m=I,I=I+16|0,d=m,n[d>>2]=A,o?A=n[o+976>>2]|0:A=0,kh(A,o,l,u,d),I=m}function wu(o){return o=o|0,(o>>>0>60?3201:3201+(60-o)|0)|0}function ha(o,l){o=o|0,l=l|0;var u=0,A=0,d=0;return d=I,I=I+32|0,u=d+12|0,A=d,n[u>>2]=n[254],n[u+4>>2]=n[255],n[u+8>>2]=n[256],n[A>>2]=n[257],n[A+4>>2]=n[258],n[A+8>>2]=n[259],(o|0)>2?o=11699:o=n[(l?A:u)+(o<<2)>>2]|0,I=d,o|0}function Os(o,l,u,A,d,m,B,k,T,_){o=o|0,l=y(l),u=y(u),A=A|0,d=d|0,m=m|0,B=y(B),k=y(k),T=T|0,_=_|0;var M=0,G=0,ae=0,We=0,Le=Xe,Qe=Xe,tt=Xe,Ze=Xe,ct=Xe,He=Xe,Ge=Xe,Lt=0,qr=0,fr=0,$t=Xe,Tr=Xe,Hr=0,cr=Xe,Hn=0,Fo=0,No=0,Oo=0,$a=0,Vh=0,Kh=0,dc=0,Jh=0,Ff=0,Nf=0,zh=0,Zh=0,Xh=0,on=0,mc=0,$h=0,ku=0,e0=Xe,t0=Xe,Of=Xe,Lf=Xe,Qu=Xe,lo=0,Ml=0,ya=0,yc=0,lp=0,cp=Xe,Mf=Xe,up=Xe,fp=Xe,co=Xe,Us=Xe,Ec=0,Wn=Xe,Ap=Xe,Lo=Xe,Tu=Xe,Mo=Xe,Ru=Xe,pp=0,hp=0,Fu=Xe,uo=Xe,Ic=0,gp=0,dp=0,mp=0,Fr=Xe,ui=0,Hs=0,_o=0,fo=0,Mr=0,Ar=0,Cc=0,zt=Xe,yp=0,vi=0;Cc=I,I=I+16|0,lo=Cc+12|0,Ml=Cc+8|0,ya=Cc+4|0,yc=Cc,Bi(o,(d|0)==0|(Mt(l)|0)^1,3326),Bi(o,(m|0)==0|(Mt(u)|0)^1,3406),Hs=ft(o,A)|0,n[o+496>>2]=Hs,Mr=dr(2,Hs)|0,Ar=dr(0,Hs)|0,h[o+440>>2]=y(J(o,Mr,B)),h[o+444>>2]=y(re(o,Mr,B)),h[o+428>>2]=y(J(o,Ar,B)),h[o+436>>2]=y(re(o,Ar,B)),h[o+464>>2]=y(Br(o,Mr)),h[o+468>>2]=y(_n(o,Mr)),h[o+452>>2]=y(Br(o,Ar)),h[o+460>>2]=y(_n(o,Ar)),h[o+488>>2]=y(mi(o,Mr,B)),h[o+492>>2]=y(Bs(o,Mr,B)),h[o+476>>2]=y(mi(o,Ar,B)),h[o+484>>2]=y(Bs(o,Ar,B));do if(n[o+964>>2]|0)zA(o,l,u,d,m,B,k);else{if(_o=o+948|0,fo=(n[o+952>>2]|0)-(n[_o>>2]|0)>>2,!fo){dP(o,l,u,d,m,B,k);break}if(!T&&t2(o,l,u,d,m,B,k)|0)break;ee(o),mc=o+508|0,s[mc>>0]=0,Mr=dr(n[o+4>>2]|0,Hs)|0,Ar=Sy(Mr,Hs)|0,ui=de(Mr)|0,$h=n[o+8>>2]|0,gp=o+28|0,ku=(n[gp>>2]|0)!=0,Mo=ui?B:k,Fu=ui?k:B,e0=y(Th(o,Mr,B)),t0=y(r2(o,Mr,B)),Le=y(Th(o,Ar,B)),Ru=y(Ka(o,Mr,B)),uo=y(Ka(o,Ar,B)),fr=ui?d:m,Ic=ui?m:d,Fr=ui?Ru:uo,ct=ui?uo:Ru,Tu=y(yn(o,2,B)),Ze=y(yn(o,0,B)),Qe=y(y(Xr(o+364|0,B))-Fr),tt=y(y(Xr(o+380|0,B))-Fr),He=y(y(Xr(o+372|0,k))-ct),Ge=y(y(Xr(o+388|0,k))-ct),Of=ui?Qe:He,Lf=ui?tt:Ge,Tu=y(l-Tu),l=y(Tu-Fr),Mt(l)|0?Fr=l:Fr=y($n(y(Ad(l,tt)),Qe)),Ap=y(u-Ze),l=y(Ap-ct),Mt(l)|0?Lo=l:Lo=y($n(y(Ad(l,Ge)),He)),Qe=ui?Fr:Lo,Wn=ui?Lo:Fr;e:do if((fr|0)==1)for(A=0,G=0;;){if(M=ws(o,G)|0,!A)y(ZA(M))>y(0)&&y(Rh(M))>y(0)?A=M:A=0;else if(n2(M)|0){We=0;break e}if(G=G+1|0,G>>>0>=fo>>>0){We=A;break}}else We=0;while(!1);Lt=We+500|0,qr=We+504|0,A=0,M=0,l=y(0),ae=0;do{if(G=n[(n[_o>>2]|0)+(ae<<2)>>2]|0,(n[G+36>>2]|0)==1)Dy(G),s[G+985>>0]=1,s[G+984>>0]=0;else{Sf(G),T&&xh(G,ft(G,Hs)|0,Qe,Wn,Fr);do if((n[G+24>>2]|0)!=1)if((G|0)==(We|0)){n[Lt>>2]=n[2278],h[qr>>2]=y(0);break}else{mP(o,G,Fr,d,Lo,Fr,Lo,m,Hs,_);break}else M|0&&(n[M+960>>2]=G),n[G+960>>2]=0,M=G,A=A|0?A:G;while(!1);Us=y(h[G+504>>2]),l=y(l+y(Us+y(yn(G,Mr,Fr))))}ae=ae+1|0}while((ae|0)!=(fo|0));for(No=l>Qe,Ec=ku&((fr|0)==2&No)?1:fr,Hn=(Ic|0)==1,$a=Hn&(T^1),Vh=(Ec|0)==1,Kh=(Ec|0)==2,dc=976+(Mr<<2)|0,Jh=(Ic|2|0)==2,Xh=Hn&(ku^1),Ff=1040+(Ar<<2)|0,Nf=1040+(Mr<<2)|0,zh=976+(Ar<<2)|0,Zh=(Ic|0)!=1,No=ku&((fr|0)!=0&No),Fo=o+976|0,Hn=Hn^1,l=Qe,Hr=0,Oo=0,Us=y(0),Qu=y(0);;){e:do if(Hr>>>0>>0)for(qr=n[_o>>2]|0,ae=0,Ge=y(0),He=y(0),tt=y(0),Qe=y(0),G=0,M=0,We=Hr;;){if(Lt=n[qr+(We<<2)>>2]|0,(n[Lt+36>>2]|0)!=1&&(n[Lt+940>>2]=Oo,(n[Lt+24>>2]|0)!=1)){if(Ze=y(yn(Lt,Mr,Fr)),on=n[dc>>2]|0,u=y(Xr(Lt+380+(on<<3)|0,Mo)),ct=y(h[Lt+504>>2]),u=y(Ad(u,ct)),u=y($n(y(Xr(Lt+364+(on<<3)|0,Mo)),u)),ku&(ae|0)!=0&y(Ze+y(He+u))>l){m=ae,Ze=Ge,fr=We;break e}Ze=y(Ze+u),u=y(He+Ze),Ze=y(Ge+Ze),n2(Lt)|0&&(tt=y(tt+y(ZA(Lt))),Qe=y(Qe-y(ct*y(Rh(Lt))))),M|0&&(n[M+960>>2]=Lt),n[Lt+960>>2]=0,ae=ae+1|0,M=Lt,G=G|0?G:Lt}else Ze=Ge,u=He;if(We=We+1|0,We>>>0>>0)Ge=Ze,He=u;else{m=ae,fr=We;break}}else m=0,Ze=y(0),tt=y(0),Qe=y(0),G=0,fr=Hr;while(!1);on=tt>y(0)&tty(0)&QeLf&((Mt(Lf)|0)^1))l=Lf,on=51;else if(s[(n[Fo>>2]|0)+3>>0]|0)on=51;else{if($t!=y(0)&&y(ZA(o))!=y(0)){on=53;break}l=Ze,on=53}while(!1);if((on|0)==51&&(on=0,Mt(l)|0?on=53:(Tr=y(l-Ze),cr=l)),(on|0)==53&&(on=0,Ze>2]|0,We=Try(0),He=y(Tr/$t),tt=y(0),Ze=y(0),l=y(0),M=G;do u=y(Xr(M+380+(ae<<3)|0,Mo)),Qe=y(Xr(M+364+(ae<<3)|0,Mo)),Qe=y(Ad(u,y($n(Qe,y(h[M+504>>2]))))),We?(u=y(Qe*y(Rh(M))),u!=y(-0)&&(zt=y(Qe-y(ct*u)),cp=y(Gn(M,Mr,zt,cr,Fr)),zt!=cp)&&(tt=y(tt-y(cp-Qe)),l=y(l+u))):Lt&&(Mf=y(ZA(M)),Mf!=y(0))&&(zt=y(Qe+y(He*Mf)),up=y(Gn(M,Mr,zt,cr,Fr)),zt!=up)&&(tt=y(tt-y(up-Qe)),Ze=y(Ze-Mf)),M=n[M+960>>2]|0;while(M|0);if(l=y(Ge+l),Qe=y(Tr+tt),lp)l=y(0);else{ct=y($t+Ze),We=n[dc>>2]|0,Lt=Qey(0),ct=y(Qe/ct),l=y(0);do{zt=y(Xr(G+380+(We<<3)|0,Mo)),tt=y(Xr(G+364+(We<<3)|0,Mo)),tt=y(Ad(zt,y($n(tt,y(h[G+504>>2]))))),Lt?(zt=y(tt*y(Rh(G))),Qe=y(-zt),zt!=y(-0)?(zt=y(He*Qe),Qe=y(Gn(G,Mr,y(tt+(qr?Qe:zt)),cr,Fr))):Qe=tt):ae&&(fp=y(ZA(G)),fp!=y(0))?Qe=y(Gn(G,Mr,y(tt+y(ct*fp)),cr,Fr)):Qe=tt,l=y(l-y(Qe-tt)),Ze=y(yn(G,Mr,Fr)),u=y(yn(G,Ar,Fr)),Qe=y(Qe+Ze),h[Ml>>2]=Qe,n[yc>>2]=1,tt=y(h[G+396>>2]);e:do if(Mt(tt)|0){M=Mt(Wn)|0;do if(!M){if(No|(oo(G,Ar,Wn)|0|Hn)||(as(o,G)|0)!=4||(n[(Ql(G,Ar)|0)+4>>2]|0)==3||(n[(Tl(G,Ar)|0)+4>>2]|0)==3)break;h[lo>>2]=Wn,n[ya>>2]=1;break e}while(!1);if(oo(G,Ar,Wn)|0){M=n[G+992+(n[zh>>2]<<2)>>2]|0,zt=y(u+y(Xr(M,Wn))),h[lo>>2]=zt,M=Zh&(n[M+4>>2]|0)==2,n[ya>>2]=((Mt(zt)|0|M)^1)&1;break}else{h[lo>>2]=Wn,n[ya>>2]=M?0:2;break}}else zt=y(Qe-Ze),$t=y(zt/tt),zt=y(tt*zt),n[ya>>2]=1,h[lo>>2]=y(u+(ui?$t:zt));while(!1);Bu(G,Mr,cr,Fr,yc,Ml),Bu(G,Ar,Wn,Fr,ya,lo);do if(!(oo(G,Ar,Wn)|0)&&(as(o,G)|0)==4){if((n[(Ql(G,Ar)|0)+4>>2]|0)==3){M=0;break}M=(n[(Tl(G,Ar)|0)+4>>2]|0)!=3}else M=0;while(!1);zt=y(h[Ml>>2]),$t=y(h[lo>>2]),yp=n[yc>>2]|0,vi=n[ya>>2]|0,kl(G,ui?zt:$t,ui?$t:zt,Hs,ui?yp:vi,ui?vi:yp,Fr,Lo,T&(M^1),3488,_)|0,s[mc>>0]=s[mc>>0]|s[G+508>>0],G=n[G+960>>2]|0}while(G|0)}}else l=y(0);if(l=y(Tr+l),vi=l>0]=vi|c[mc>>0],Kh&l>y(0)?(M=n[dc>>2]|0,n[o+364+(M<<3)+4>>2]|0&&(co=y(Xr(o+364+(M<<3)|0,Mo)),co>=y(0))?Qe=y($n(y(0),y(co-y(cr-l)))):Qe=y(0)):Qe=l,Lt=Hr>>>0>>0,Lt){We=n[_o>>2]|0,ae=Hr,M=0;do G=n[We+(ae<<2)>>2]|0,n[G+24>>2]|0||(M=((n[(Ql(G,Mr)|0)+4>>2]|0)==3&1)+M|0,M=M+((n[(Tl(G,Mr)|0)+4>>2]|0)==3&1)|0),ae=ae+1|0;while((ae|0)!=(fr|0));M?(Ze=y(0),u=y(0)):on=101}else on=101;e:do if((on|0)==101)switch(on=0,$h|0){case 1:{M=0,Ze=y(Qe*y(.5)),u=y(0);break e}case 2:{M=0,Ze=Qe,u=y(0);break e}case 3:{if(m>>>0<=1){M=0,Ze=y(0),u=y(0);break e}u=y((m+-1|0)>>>0),M=0,Ze=y(0),u=y(y($n(Qe,y(0)))/u);break e}case 5:{u=y(Qe/y((m+1|0)>>>0)),M=0,Ze=u;break e}case 4:{u=y(Qe/y(m>>>0)),M=0,Ze=y(u*y(.5));break e}default:{M=0,Ze=y(0),u=y(0);break e}}while(!1);if(l=y(e0+Ze),Lt){tt=y(Qe/y(M|0)),ae=n[_o>>2]|0,G=Hr,Qe=y(0);do{M=n[ae+(G<<2)>>2]|0;e:do if((n[M+36>>2]|0)!=1){switch(n[M+24>>2]|0){case 1:{if(ga(M,Mr)|0){if(!T)break e;zt=y(XA(M,Mr,cr)),zt=y(zt+y(Br(o,Mr))),zt=y(zt+y(J(M,Mr,Fr))),h[M+400+(n[Nf>>2]<<2)>>2]=zt;break e}break}case 0:if(vi=(n[(Ql(M,Mr)|0)+4>>2]|0)==3,zt=y(tt+l),l=vi?zt:l,T&&(vi=M+400+(n[Nf>>2]<<2)|0,h[vi>>2]=y(l+y(h[vi>>2]))),vi=(n[(Tl(M,Mr)|0)+4>>2]|0)==3,zt=y(tt+l),l=vi?zt:l,$a){zt=y(u+y(yn(M,Mr,Fr))),Qe=Wn,l=y(l+y(zt+y(h[M+504>>2])));break e}else{l=y(l+y(u+y($A(M,Mr,Fr)))),Qe=y($n(Qe,y($A(M,Ar,Fr))));break e}default:}T&&(zt=y(Ze+y(Br(o,Mr))),vi=M+400+(n[Nf>>2]<<2)|0,h[vi>>2]=y(zt+y(h[vi>>2])))}while(!1);G=G+1|0}while((G|0)!=(fr|0))}else Qe=y(0);if(u=y(t0+l),Jh?Ze=y(y(Gn(o,Ar,y(uo+Qe),Fu,B))-uo):Ze=Wn,tt=y(y(Gn(o,Ar,y(uo+(Xh?Wn:Qe)),Fu,B))-uo),Lt&T){G=Hr;do{ae=n[(n[_o>>2]|0)+(G<<2)>>2]|0;do if((n[ae+36>>2]|0)!=1){if((n[ae+24>>2]|0)==1){if(ga(ae,Ar)|0){if(zt=y(XA(ae,Ar,Wn)),zt=y(zt+y(Br(o,Ar))),zt=y(zt+y(J(ae,Ar,Fr))),M=n[Ff>>2]|0,h[ae+400+(M<<2)>>2]=zt,!(Mt(zt)|0))break}else M=n[Ff>>2]|0;zt=y(Br(o,Ar)),h[ae+400+(M<<2)>>2]=y(zt+y(J(ae,Ar,Fr)));break}M=as(o,ae)|0;do if((M|0)==4){if((n[(Ql(ae,Ar)|0)+4>>2]|0)==3){on=139;break}if((n[(Tl(ae,Ar)|0)+4>>2]|0)==3){on=139;break}if(oo(ae,Ar,Wn)|0){l=Le;break}yp=n[ae+908+(n[dc>>2]<<2)>>2]|0,n[lo>>2]=yp,l=y(h[ae+396>>2]),vi=Mt(l)|0,Qe=(n[S>>2]=yp,y(h[S>>2])),vi?l=tt:(Tr=y(yn(ae,Ar,Fr)),zt=y(Qe/l),l=y(l*Qe),l=y(Tr+(ui?zt:l))),h[Ml>>2]=l,h[lo>>2]=y(y(yn(ae,Mr,Fr))+Qe),n[ya>>2]=1,n[yc>>2]=1,Bu(ae,Mr,cr,Fr,ya,lo),Bu(ae,Ar,Wn,Fr,yc,Ml),l=y(h[lo>>2]),Tr=y(h[Ml>>2]),zt=ui?l:Tr,l=ui?Tr:l,vi=((Mt(zt)|0)^1)&1,kl(ae,zt,l,Hs,vi,((Mt(l)|0)^1)&1,Fr,Lo,1,3493,_)|0,l=Le}else on=139;while(!1);e:do if((on|0)==139){on=0,l=y(Ze-y($A(ae,Ar,Fr)));do if((n[(Ql(ae,Ar)|0)+4>>2]|0)==3){if((n[(Tl(ae,Ar)|0)+4>>2]|0)!=3)break;l=y(Le+y($n(y(0),y(l*y(.5)))));break e}while(!1);if((n[(Tl(ae,Ar)|0)+4>>2]|0)==3){l=Le;break}if((n[(Ql(ae,Ar)|0)+4>>2]|0)==3){l=y(Le+y($n(y(0),l)));break}switch(M|0){case 1:{l=Le;break e}case 2:{l=y(Le+y(l*y(.5)));break e}default:{l=y(Le+l);break e}}}while(!1);zt=y(Us+l),vi=ae+400+(n[Ff>>2]<<2)|0,h[vi>>2]=y(zt+y(h[vi>>2]))}while(!1);G=G+1|0}while((G|0)!=(fr|0))}if(Us=y(Us+tt),Qu=y($n(Qu,u)),m=Oo+1|0,fr>>>0>=fo>>>0)break;l=cr,Hr=fr,Oo=m}do if(T){if(M=m>>>0>1,!M&&!(WL(o)|0))break;if(!(Mt(Wn)|0)){l=y(Wn-Us);e:do switch(n[o+12>>2]|0){case 3:{Le=y(Le+l),He=y(0);break}case 2:{Le=y(Le+y(l*y(.5))),He=y(0);break}case 4:{Wn>Us?He=y(l/y(m>>>0)):He=y(0);break}case 7:if(Wn>Us){Le=y(Le+y(l/y(m<<1>>>0))),He=y(l/y(m>>>0)),He=M?He:y(0);break e}else{Le=y(Le+y(l*y(.5))),He=y(0);break e}case 6:{He=y(l/y(Oo>>>0)),He=Wn>Us&M?He:y(0);break}default:He=y(0)}while(!1);if(m|0)for(Lt=1040+(Ar<<2)|0,qr=976+(Ar<<2)|0,We=0,G=0;;){e:do if(G>>>0>>0)for(Qe=y(0),tt=y(0),l=y(0),ae=G;;){M=n[(n[_o>>2]|0)+(ae<<2)>>2]|0;do if((n[M+36>>2]|0)!=1&&!(n[M+24>>2]|0)){if((n[M+940>>2]|0)!=(We|0))break e;if(YL(M,Ar)|0&&(zt=y(h[M+908+(n[qr>>2]<<2)>>2]),l=y($n(l,y(zt+y(yn(M,Ar,Fr)))))),(as(o,M)|0)!=5)break;co=y(Wg(M)),co=y(co+y(J(M,0,Fr))),zt=y(h[M+912>>2]),zt=y(y(zt+y(yn(M,0,Fr)))-co),co=y($n(tt,co)),zt=y($n(Qe,zt)),Qe=zt,tt=co,l=y($n(l,y(co+zt)))}while(!1);if(M=ae+1|0,M>>>0>>0)ae=M;else{ae=M;break}}else tt=y(0),l=y(0),ae=G;while(!1);if(ct=y(He+l),u=Le,Le=y(Le+ct),G>>>0>>0){Ze=y(u+tt),M=G;do{G=n[(n[_o>>2]|0)+(M<<2)>>2]|0;e:do if((n[G+36>>2]|0)!=1&&!(n[G+24>>2]|0))switch(as(o,G)|0){case 1:{zt=y(u+y(J(G,Ar,Fr))),h[G+400+(n[Lt>>2]<<2)>>2]=zt;break e}case 3:{zt=y(y(Le-y(re(G,Ar,Fr)))-y(h[G+908+(n[qr>>2]<<2)>>2])),h[G+400+(n[Lt>>2]<<2)>>2]=zt;break e}case 2:{zt=y(u+y(y(ct-y(h[G+908+(n[qr>>2]<<2)>>2]))*y(.5))),h[G+400+(n[Lt>>2]<<2)>>2]=zt;break e}case 4:{if(zt=y(u+y(J(G,Ar,Fr))),h[G+400+(n[Lt>>2]<<2)>>2]=zt,oo(G,Ar,Wn)|0||(ui?(Qe=y(h[G+908>>2]),l=y(Qe+y(yn(G,Mr,Fr))),tt=ct):(tt=y(h[G+912>>2]),tt=y(tt+y(yn(G,Ar,Fr))),l=ct,Qe=y(h[G+908>>2])),mn(l,Qe)|0&&mn(tt,y(h[G+912>>2]))|0))break e;kl(G,l,tt,Hs,1,1,Fr,Lo,1,3501,_)|0;break e}case 5:{h[G+404>>2]=y(y(Ze-y(Wg(G)))+y(XA(G,0,Wn)));break e}default:break e}while(!1);M=M+1|0}while((M|0)!=(ae|0))}if(We=We+1|0,(We|0)==(m|0))break;G=ae}}}while(!1);if(h[o+908>>2]=y(Gn(o,2,Tu,B,B)),h[o+912>>2]=y(Gn(o,0,Ap,k,B)),Ec|0&&(pp=n[o+32>>2]|0,hp=(Ec|0)==2,!(hp&(pp|0)!=2))?hp&(pp|0)==2&&(l=y(Ru+cr),l=y($n(y(Ad(l,y(Yg(o,Mr,Qu,Mo)))),Ru)),on=198):(l=y(Gn(o,Mr,Qu,Mo,B)),on=198),(on|0)==198&&(h[o+908+(n[976+(Mr<<2)>>2]<<2)>>2]=l),Ic|0&&(dp=n[o+32>>2]|0,mp=(Ic|0)==2,!(mp&(dp|0)!=2))?mp&(dp|0)==2&&(l=y(uo+Wn),l=y($n(y(Ad(l,y(Yg(o,Ar,y(uo+Us),Fu)))),uo)),on=204):(l=y(Gn(o,Ar,y(uo+Us),Fu,B)),on=204),(on|0)==204&&(h[o+908+(n[976+(Ar<<2)>>2]<<2)>>2]=l),T){if((n[gp>>2]|0)==2){G=976+(Ar<<2)|0,ae=1040+(Ar<<2)|0,M=0;do We=ws(o,M)|0,n[We+24>>2]|0||(yp=n[G>>2]|0,zt=y(h[o+908+(yp<<2)>>2]),vi=We+400+(n[ae>>2]<<2)|0,zt=y(zt-y(h[vi>>2])),h[vi>>2]=y(zt-y(h[We+908+(yp<<2)>>2]))),M=M+1|0;while((M|0)!=(fo|0))}if(A|0){M=ui?Ec:d;do VL(o,A,Fr,M,Lo,Hs,_),A=n[A+960>>2]|0;while(A|0)}if(M=(Mr|2|0)==3,G=(Ar|2|0)==3,M|G){A=0;do ae=n[(n[_o>>2]|0)+(A<<2)>>2]|0,(n[ae+36>>2]|0)!=1&&(M&&i2(o,ae,Mr),G&&i2(o,ae,Ar)),A=A+1|0;while((A|0)!=(fo|0))}}}while(!1);I=Cc}function Ph(o,l){o=o|0,l=y(l);var u=0;ja(o,l>=y(0),3147),u=l==y(0),h[o+4>>2]=u?y(0):l}function KA(o,l,u,A){o=o|0,l=y(l),u=y(u),A=A|0;var d=Xe,m=Xe,B=0,k=0,T=0;n[2278]=(n[2278]|0)+1,Sf(o),oo(o,2,l)|0?(d=y(Xr(n[o+992>>2]|0,l)),T=1,d=y(d+y(yn(o,2,l)))):(d=y(Xr(o+380|0,l)),d>=y(0)?T=2:(T=((Mt(l)|0)^1)&1,d=l)),oo(o,0,u)|0?(m=y(Xr(n[o+996>>2]|0,u)),k=1,m=y(m+y(yn(o,0,l)))):(m=y(Xr(o+388|0,u)),m>=y(0)?k=2:(k=((Mt(u)|0)^1)&1,m=u)),B=o+976|0,kl(o,d,m,A,T,k,l,u,1,3189,n[B>>2]|0)|0&&(xh(o,n[o+496>>2]|0,l,u,l),JA(o,y(h[(n[B>>2]|0)+4>>2]),y(0),y(0)),s[11696]|0)&&jg(o,7)}function Sf(o){o=o|0;var l=0,u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;k=I,I=I+32|0,B=k+24|0,m=k+16|0,A=k+8|0,d=k,u=0;do l=o+380+(u<<3)|0,n[o+380+(u<<3)+4>>2]|0&&(T=l,_=n[T+4>>2]|0,M=A,n[M>>2]=n[T>>2],n[M+4>>2]=_,M=o+364+(u<<3)|0,_=n[M+4>>2]|0,T=d,n[T>>2]=n[M>>2],n[T+4>>2]=_,n[m>>2]=n[A>>2],n[m+4>>2]=n[A+4>>2],n[B>>2]=n[d>>2],n[B+4>>2]=n[d+4>>2],wf(m,B)|0)||(l=o+348+(u<<3)|0),n[o+992+(u<<2)>>2]=l,u=u+1|0;while((u|0)!=2);I=k}function oo(o,l,u){o=o|0,l=l|0,u=y(u);var A=0;switch(o=n[o+992+(n[976+(l<<2)>>2]<<2)>>2]|0,n[o+4>>2]|0){case 0:case 3:{o=0;break}case 1:{y(h[o>>2])>2])>2]|0){case 2:{l=y(y(y(h[o>>2])*l)/y(100));break}case 1:{l=y(h[o>>2]);break}default:l=y(ce)}return y(l)}function xh(o,l,u,A,d){o=o|0,l=l|0,u=y(u),A=y(A),d=y(d);var m=0,B=Xe;l=n[o+944>>2]|0?l:1,m=dr(n[o+4>>2]|0,l)|0,l=Sy(m,l)|0,u=y(yP(o,m,u)),A=y(yP(o,l,A)),B=y(u+y(J(o,m,d))),h[o+400+(n[1040+(m<<2)>>2]<<2)>>2]=B,u=y(u+y(re(o,m,d))),h[o+400+(n[1e3+(m<<2)>>2]<<2)>>2]=u,u=y(A+y(J(o,l,d))),h[o+400+(n[1040+(l<<2)>>2]<<2)>>2]=u,d=y(A+y(re(o,l,d))),h[o+400+(n[1e3+(l<<2)>>2]<<2)>>2]=d}function JA(o,l,u,A){o=o|0,l=y(l),u=y(u),A=y(A);var d=0,m=0,B=Xe,k=Xe,T=0,_=0,M=Xe,G=0,ae=Xe,We=Xe,Le=Xe,Qe=Xe;if(l!=y(0)&&(d=o+400|0,Qe=y(h[d>>2]),m=o+404|0,Le=y(h[m>>2]),G=o+416|0,We=y(h[G>>2]),_=o+420|0,B=y(h[_>>2]),ae=y(Qe+u),M=y(Le+A),A=y(ae+We),k=y(M+B),T=(n[o+988>>2]|0)==1,h[d>>2]=y(os(Qe,l,0,T)),h[m>>2]=y(os(Le,l,0,T)),u=y(A_(y(We*l),y(1))),mn(u,y(0))|0?m=0:m=(mn(u,y(1))|0)^1,u=y(A_(y(B*l),y(1))),mn(u,y(0))|0?d=0:d=(mn(u,y(1))|0)^1,Qe=y(os(A,l,T&m,T&(m^1))),h[G>>2]=y(Qe-y(os(ae,l,0,T))),Qe=y(os(k,l,T&d,T&(d^1))),h[_>>2]=y(Qe-y(os(M,l,0,T))),m=(n[o+952>>2]|0)-(n[o+948>>2]|0)>>2,m|0)){d=0;do JA(ws(o,d)|0,l,ae,M),d=d+1|0;while((d|0)!=(m|0))}}function By(o,l,u,A,d){switch(o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,u|0){case 5:case 0:{o=WX(n[489]|0,A,d)|0;break}default:o=MYe(A,d)|0}return o|0}function Gg(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;d=I,I=I+16|0,m=d,n[m>>2]=A,kh(o,0,l,u,m),I=d}function kh(o,l,u,A,d){if(o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,o=o|0?o:956,A$[n[o+8>>2]&1](o,l,u,A,d)|0,(u|0)==5)Nt();else return}function hc(o,l,u){o=o|0,l=l|0,u=u|0,s[o+l>>0]=u&1}function vy(o,l){o=o|0,l=l|0;var u=0,A=0;n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,u=l+4|0,A=(n[u>>2]|0)-(n[l>>2]|0)>>2,A|0&&(Qh(o,A),kt(o,n[l>>2]|0,n[u>>2]|0,A))}function Qh(o,l){o=o|0,l=l|0;var u=0;if((O(o)|0)>>>0>>0&&sn(o),l>>>0>1073741823)Nt();else{u=Jt(l<<2)|0,n[o+4>>2]=u,n[o>>2]=u,n[o+8>>2]=u+(l<<2);return}}function kt(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,A=o+4|0,o=u-l|0,(o|0)>0&&(Qr(n[A>>2]|0,l|0,o|0)|0,n[A>>2]=(n[A>>2]|0)+(o>>>2<<2))}function O(o){return o=o|0,1073741823}function J(o,l,u){return o=o|0,l=l|0,u=y(u),de(l)|0&&n[o+96>>2]|0?o=o+92|0:o=kn(o+60|0,n[1040+(l<<2)>>2]|0,992)|0,y(Ke(o,u))}function re(o,l,u){return o=o|0,l=l|0,u=y(u),de(l)|0&&n[o+104>>2]|0?o=o+100|0:o=kn(o+60|0,n[1e3+(l<<2)>>2]|0,992)|0,y(Ke(o,u))}function de(o){return o=o|0,(o|1|0)==3|0}function Ke(o,l){return o=o|0,l=y(l),(n[o+4>>2]|0)==3?l=y(0):l=y(Xr(o,l)),y(l)}function ft(o,l){return o=o|0,l=l|0,o=n[o>>2]|0,(o|0?o:(l|0)>1?l:1)|0}function dr(o,l){o=o|0,l=l|0;var u=0;e:do if((l|0)==2){switch(o|0){case 2:{o=3;break e}case 3:break;default:{u=4;break e}}o=2}else u=4;while(!1);return o|0}function Br(o,l){o=o|0,l=l|0;var u=Xe;return de(l)|0&&n[o+312>>2]|0&&(u=y(h[o+308>>2]),u>=y(0))||(u=y($n(y(h[(kn(o+276|0,n[1040+(l<<2)>>2]|0,992)|0)>>2]),y(0)))),y(u)}function _n(o,l){o=o|0,l=l|0;var u=Xe;return de(l)|0&&n[o+320>>2]|0&&(u=y(h[o+316>>2]),u>=y(0))||(u=y($n(y(h[(kn(o+276|0,n[1e3+(l<<2)>>2]|0,992)|0)>>2]),y(0)))),y(u)}function mi(o,l,u){o=o|0,l=l|0,u=y(u);var A=Xe;return de(l)|0&&n[o+240>>2]|0&&(A=y(Xr(o+236|0,u)),A>=y(0))||(A=y($n(y(Xr(kn(o+204|0,n[1040+(l<<2)>>2]|0,992)|0,u)),y(0)))),y(A)}function Bs(o,l,u){o=o|0,l=l|0,u=y(u);var A=Xe;return de(l)|0&&n[o+248>>2]|0&&(A=y(Xr(o+244|0,u)),A>=y(0))||(A=y($n(y(Xr(kn(o+204|0,n[1e3+(l<<2)>>2]|0,992)|0,u)),y(0)))),y(A)}function zA(o,l,u,A,d,m,B){o=o|0,l=y(l),u=y(u),A=A|0,d=d|0,m=y(m),B=y(B);var k=Xe,T=Xe,_=Xe,M=Xe,G=Xe,ae=Xe,We=0,Le=0,Qe=0;Qe=I,I=I+16|0,We=Qe,Le=o+964|0,Bi(o,(n[Le>>2]|0)!=0,3519),k=y(Ka(o,2,l)),T=y(Ka(o,0,l)),_=y(yn(o,2,l)),M=y(yn(o,0,l)),Mt(l)|0?G=l:G=y($n(y(0),y(y(l-_)-k))),Mt(u)|0?ae=u:ae=y($n(y(0),y(y(u-M)-T))),(A|0)==1&(d|0)==1?(h[o+908>>2]=y(Gn(o,2,y(l-_),m,m)),l=y(Gn(o,0,y(u-M),B,m))):(p$[n[Le>>2]&1](We,o,G,A,ae,d),G=y(k+y(h[We>>2])),ae=y(l-_),h[o+908>>2]=y(Gn(o,2,(A|2|0)==2?G:ae,m,m)),ae=y(T+y(h[We+4>>2])),l=y(u-M),l=y(Gn(o,0,(d|2|0)==2?ae:l,B,m))),h[o+912>>2]=l,I=Qe}function dP(o,l,u,A,d,m,B){o=o|0,l=y(l),u=y(u),A=A|0,d=d|0,m=y(m),B=y(B);var k=Xe,T=Xe,_=Xe,M=Xe;_=y(Ka(o,2,m)),k=y(Ka(o,0,m)),M=y(yn(o,2,m)),T=y(yn(o,0,m)),l=y(l-M),h[o+908>>2]=y(Gn(o,2,(A|2|0)==2?_:l,m,m)),u=y(u-T),h[o+912>>2]=y(Gn(o,0,(d|2|0)==2?k:u,B,m))}function t2(o,l,u,A,d,m,B){o=o|0,l=y(l),u=y(u),A=A|0,d=d|0,m=y(m),B=y(B);var k=0,T=Xe,_=Xe;return k=(A|0)==2,!(l<=y(0)&k)&&!(u<=y(0)&(d|0)==2)&&!((A|0)==1&(d|0)==1)?o=0:(T=y(yn(o,0,m)),_=y(yn(o,2,m)),k=l>2]=y(Gn(o,2,k?y(0):l,m,m)),l=y(u-T),k=u>2]=y(Gn(o,0,k?y(0):l,B,m)),o=1),o|0}function Sy(o,l){return o=o|0,l=l|0,Vg(o)|0?o=dr(2,l)|0:o=0,o|0}function Th(o,l,u){return o=o|0,l=l|0,u=y(u),u=y(mi(o,l,u)),y(u+y(Br(o,l)))}function r2(o,l,u){return o=o|0,l=l|0,u=y(u),u=y(Bs(o,l,u)),y(u+y(_n(o,l)))}function Ka(o,l,u){o=o|0,l=l|0,u=y(u);var A=Xe;return A=y(Th(o,l,u)),y(A+y(r2(o,l,u)))}function n2(o){return o=o|0,n[o+24>>2]|0?o=0:y(ZA(o))!=y(0)?o=1:o=y(Rh(o))!=y(0),o|0}function ZA(o){o=o|0;var l=Xe;if(n[o+944>>2]|0){if(l=y(h[o+44>>2]),Mt(l)|0)return l=y(h[o+40>>2]),o=l>y(0)&((Mt(l)|0)^1),y(o?l:y(0))}else l=y(0);return y(l)}function Rh(o){o=o|0;var l=Xe,u=0,A=Xe;do if(n[o+944>>2]|0){if(l=y(h[o+48>>2]),Mt(l)|0){if(u=s[(n[o+976>>2]|0)+2>>0]|0,!(u<<24>>24)&&(A=y(h[o+40>>2]),A>24?y(1):y(0)}}else l=y(0);while(!1);return y(l)}function Dy(o){o=o|0;var l=0,u=0;if(Xy(o+400|0,0,540)|0,s[o+985>>0]=1,ee(o),u=_i(o)|0,u|0){l=o+948|0,o=0;do Dy(n[(n[l>>2]|0)+(o<<2)>>2]|0),o=o+1|0;while((o|0)!=(u|0))}}function mP(o,l,u,A,d,m,B,k,T,_){o=o|0,l=l|0,u=y(u),A=A|0,d=y(d),m=y(m),B=y(B),k=k|0,T=T|0,_=_|0;var M=0,G=Xe,ae=0,We=0,Le=Xe,Qe=Xe,tt=0,Ze=Xe,ct=0,He=Xe,Ge=0,Lt=0,qr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,Hn=0,Fo=0;Hn=I,I=I+16|0,qr=Hn+12|0,fr=Hn+8|0,$t=Hn+4|0,Tr=Hn,cr=dr(n[o+4>>2]|0,T)|0,Ge=de(cr)|0,G=y(Xr(KL(l)|0,Ge?m:B)),Lt=oo(l,2,m)|0,Hr=oo(l,0,B)|0;do if(!(Mt(G)|0)&&!(Mt(Ge?u:d)|0)){if(M=l+504|0,!(Mt(y(h[M>>2]))|0)&&(!(s2(n[l+976>>2]|0,0)|0)||(n[l+500>>2]|0)==(n[2278]|0)))break;h[M>>2]=y($n(G,y(Ka(l,cr,m))))}else ae=7;while(!1);do if((ae|0)==7){if(ct=Ge^1,!(ct|Lt^1)){B=y(Xr(n[l+992>>2]|0,m)),h[l+504>>2]=y($n(B,y(Ka(l,2,m))));break}if(!(Ge|Hr^1)){B=y(Xr(n[l+996>>2]|0,B)),h[l+504>>2]=y($n(B,y(Ka(l,0,m))));break}h[qr>>2]=y(ce),h[fr>>2]=y(ce),n[$t>>2]=0,n[Tr>>2]=0,Ze=y(yn(l,2,m)),He=y(yn(l,0,m)),Lt?(Le=y(Ze+y(Xr(n[l+992>>2]|0,m))),h[qr>>2]=Le,n[$t>>2]=1,We=1):(We=0,Le=y(ce)),Hr?(G=y(He+y(Xr(n[l+996>>2]|0,B))),h[fr>>2]=G,n[Tr>>2]=1,M=1):(M=0,G=y(ce)),ae=n[o+32>>2]|0,Ge&(ae|0)==2?ae=2:Mt(Le)|0&&!(Mt(u)|0)&&(h[qr>>2]=u,n[$t>>2]=2,We=2,Le=u),!((ae|0)==2&ct)&&Mt(G)|0&&!(Mt(d)|0)&&(h[fr>>2]=d,n[Tr>>2]=2,M=2,G=d),Qe=y(h[l+396>>2]),tt=Mt(Qe)|0;do if(tt)ae=We;else{if((We|0)==1&ct){h[fr>>2]=y(y(Le-Ze)/Qe),n[Tr>>2]=1,M=1,ae=1;break}Ge&(M|0)==1?(h[qr>>2]=y(Qe*y(G-He)),n[$t>>2]=1,M=1,ae=1):ae=We}while(!1);Fo=Mt(u)|0,We=(as(o,l)|0)!=4,!(Ge|Lt|((A|0)!=1|Fo)|(We|(ae|0)==1))&&(h[qr>>2]=u,n[$t>>2]=1,!tt)&&(h[fr>>2]=y(y(u-Ze)/Qe),n[Tr>>2]=1,M=1),!(Hr|ct|((k|0)!=1|(Mt(d)|0))|(We|(M|0)==1))&&(h[fr>>2]=d,n[Tr>>2]=1,!tt)&&(h[qr>>2]=y(Qe*y(d-He)),n[$t>>2]=1),Bu(l,2,m,m,$t,qr),Bu(l,0,B,m,Tr,fr),u=y(h[qr>>2]),d=y(h[fr>>2]),kl(l,u,d,T,n[$t>>2]|0,n[Tr>>2]|0,m,B,0,3565,_)|0,B=y(h[l+908+(n[976+(cr<<2)>>2]<<2)>>2]),h[l+504>>2]=y($n(B,y(Ka(l,cr,m))))}while(!1);n[l+500>>2]=n[2278],I=Hn}function Gn(o,l,u,A,d){return o=o|0,l=l|0,u=y(u),A=y(A),d=y(d),A=y(Yg(o,l,u,A)),y($n(A,y(Ka(o,l,d))))}function as(o,l){return o=o|0,l=l|0,l=l+20|0,l=n[(n[l>>2]|0?l:o+16|0)>>2]|0,(l|0)==5&&Vg(n[o+4>>2]|0)|0&&(l=1),l|0}function Ql(o,l){return o=o|0,l=l|0,de(l)|0&&n[o+96>>2]|0?l=4:l=n[1040+(l<<2)>>2]|0,o+60+(l<<3)|0}function Tl(o,l){return o=o|0,l=l|0,de(l)|0&&n[o+104>>2]|0?l=5:l=n[1e3+(l<<2)>>2]|0,o+60+(l<<3)|0}function Bu(o,l,u,A,d,m){switch(o=o|0,l=l|0,u=y(u),A=y(A),d=d|0,m=m|0,u=y(Xr(o+380+(n[976+(l<<2)>>2]<<3)|0,u)),u=y(u+y(yn(o,l,A))),n[d>>2]|0){case 2:case 1:{d=Mt(u)|0,A=y(h[m>>2]),h[m>>2]=d|A>2]=2,h[m>>2]=u);break}default:}}function ga(o,l){return o=o|0,l=l|0,o=o+132|0,de(l)|0&&n[(kn(o,4,948)|0)+4>>2]|0?o=1:o=(n[(kn(o,n[1040+(l<<2)>>2]|0,948)|0)+4>>2]|0)!=0,o|0}function XA(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,d=0;return o=o+132|0,de(l)|0&&(A=kn(o,4,948)|0,(n[A+4>>2]|0)!=0)?d=4:(A=kn(o,n[1040+(l<<2)>>2]|0,948)|0,n[A+4>>2]|0?d=4:u=y(0)),(d|0)==4&&(u=y(Xr(A,u))),y(u)}function $A(o,l,u){o=o|0,l=l|0,u=y(u);var A=Xe;return A=y(h[o+908+(n[976+(l<<2)>>2]<<2)>>2]),A=y(A+y(J(o,l,u))),y(A+y(re(o,l,u)))}function WL(o){o=o|0;var l=0,u=0,A=0;e:do if(Vg(n[o+4>>2]|0)|0)l=0;else if((n[o+16>>2]|0)!=5)if(u=_i(o)|0,!u)l=0;else for(l=0;;){if(A=ws(o,l)|0,!(n[A+24>>2]|0)&&(n[A+20>>2]|0)==5){l=1;break e}if(l=l+1|0,l>>>0>=u>>>0){l=0;break}}else l=1;while(!1);return l|0}function YL(o,l){o=o|0,l=l|0;var u=Xe;return u=y(h[o+908+(n[976+(l<<2)>>2]<<2)>>2]),u>=y(0)&((Mt(u)|0)^1)|0}function Wg(o){o=o|0;var l=Xe,u=0,A=0,d=0,m=0,B=0,k=0,T=Xe;if(u=n[o+968>>2]|0,u)T=y(h[o+908>>2]),l=y(h[o+912>>2]),l=y(l$[u&0](o,T,l)),Bi(o,(Mt(l)|0)^1,3573);else{m=_i(o)|0;do if(m|0){for(u=0,d=0;;){if(A=ws(o,d)|0,n[A+940>>2]|0){B=8;break}if((n[A+24>>2]|0)!=1)if(k=(as(o,A)|0)==5,k){u=A;break}else u=u|0?u:A;if(d=d+1|0,d>>>0>=m>>>0){B=8;break}}if((B|0)==8&&!u)break;return l=y(Wg(u)),y(l+y(h[u+404>>2]))}while(!1);l=y(h[o+912>>2])}return y(l)}function Yg(o,l,u,A){o=o|0,l=l|0,u=y(u),A=y(A);var d=Xe,m=0;return Vg(l)|0?(l=1,m=3):de(l)|0?(l=0,m=3):(A=y(ce),d=y(ce)),(m|0)==3&&(d=y(Xr(o+364+(l<<3)|0,A)),A=y(Xr(o+380+(l<<3)|0,A))),m=A=y(0)&((Mt(A)|0)^1)),u=m?A:u,m=d>=y(0)&((Mt(d)|0)^1)&u>2]|0,m)|0,Le=Sy(tt,m)|0,Qe=de(tt)|0,G=y(yn(l,2,u)),ae=y(yn(l,0,u)),oo(l,2,u)|0?k=y(G+y(Xr(n[l+992>>2]|0,u))):ga(l,2)|0&&by(l,2)|0?(k=y(h[o+908>>2]),T=y(Br(o,2)),T=y(k-y(T+y(_n(o,2)))),k=y(XA(l,2,u)),k=y(Gn(l,2,y(T-y(k+y(Fh(l,2,u)))),u,u))):k=y(ce),oo(l,0,d)|0?T=y(ae+y(Xr(n[l+996>>2]|0,d))):ga(l,0)|0&&by(l,0)|0?(T=y(h[o+912>>2]),ct=y(Br(o,0)),ct=y(T-y(ct+y(_n(o,0)))),T=y(XA(l,0,d)),T=y(Gn(l,0,y(ct-y(T+y(Fh(l,0,d)))),d,u))):T=y(ce),_=Mt(k)|0,M=Mt(T)|0;do if(_^M&&(We=y(h[l+396>>2]),!(Mt(We)|0)))if(_){k=y(G+y(y(T-ae)*We));break}else{ct=y(ae+y(y(k-G)/We)),T=M?ct:T;break}while(!1);M=Mt(k)|0,_=Mt(T)|0,M|_&&(He=(M^1)&1,A=u>y(0)&((A|0)!=0&M),k=Qe?k:A?u:k,kl(l,k,T,m,Qe?He:A?2:He,M&(_^1)&1,k,T,0,3623,B)|0,k=y(h[l+908>>2]),k=y(k+y(yn(l,2,u))),T=y(h[l+912>>2]),T=y(T+y(yn(l,0,u)))),kl(l,k,T,m,1,1,k,T,1,3635,B)|0,by(l,tt)|0&&!(ga(l,tt)|0)?(He=n[976+(tt<<2)>>2]|0,ct=y(h[o+908+(He<<2)>>2]),ct=y(ct-y(h[l+908+(He<<2)>>2])),ct=y(ct-y(_n(o,tt))),ct=y(ct-y(re(l,tt,u))),ct=y(ct-y(Fh(l,tt,Qe?u:d))),h[l+400+(n[1040+(tt<<2)>>2]<<2)>>2]=ct):Ze=21;do if((Ze|0)==21){if(!(ga(l,tt)|0)&&(n[o+8>>2]|0)==1){He=n[976+(tt<<2)>>2]|0,ct=y(h[o+908+(He<<2)>>2]),ct=y(y(ct-y(h[l+908+(He<<2)>>2]))*y(.5)),h[l+400+(n[1040+(tt<<2)>>2]<<2)>>2]=ct;break}!(ga(l,tt)|0)&&(n[o+8>>2]|0)==2&&(He=n[976+(tt<<2)>>2]|0,ct=y(h[o+908+(He<<2)>>2]),ct=y(ct-y(h[l+908+(He<<2)>>2])),h[l+400+(n[1040+(tt<<2)>>2]<<2)>>2]=ct)}while(!1);by(l,Le)|0&&!(ga(l,Le)|0)?(He=n[976+(Le<<2)>>2]|0,ct=y(h[o+908+(He<<2)>>2]),ct=y(ct-y(h[l+908+(He<<2)>>2])),ct=y(ct-y(_n(o,Le))),ct=y(ct-y(re(l,Le,u))),ct=y(ct-y(Fh(l,Le,Qe?d:u))),h[l+400+(n[1040+(Le<<2)>>2]<<2)>>2]=ct):Ze=30;do if((Ze|0)==30&&!(ga(l,Le)|0)){if((as(o,l)|0)==2){He=n[976+(Le<<2)>>2]|0,ct=y(h[o+908+(He<<2)>>2]),ct=y(y(ct-y(h[l+908+(He<<2)>>2]))*y(.5)),h[l+400+(n[1040+(Le<<2)>>2]<<2)>>2]=ct;break}He=(as(o,l)|0)==3,He^(n[o+28>>2]|0)==2&&(He=n[976+(Le<<2)>>2]|0,ct=y(h[o+908+(He<<2)>>2]),ct=y(ct-y(h[l+908+(He<<2)>>2])),h[l+400+(n[1040+(Le<<2)>>2]<<2)>>2]=ct)}while(!1)}function i2(o,l,u){o=o|0,l=l|0,u=u|0;var A=Xe,d=0;d=n[976+(u<<2)>>2]|0,A=y(h[l+908+(d<<2)>>2]),A=y(y(h[o+908+(d<<2)>>2])-A),A=y(A-y(h[l+400+(n[1040+(u<<2)>>2]<<2)>>2])),h[l+400+(n[1e3+(u<<2)>>2]<<2)>>2]=A}function Vg(o){return o=o|0,(o|1|0)==1|0}function KL(o){o=o|0;var l=Xe;switch(n[o+56>>2]|0){case 0:case 3:{l=y(h[o+40>>2]),l>y(0)&((Mt(l)|0)^1)?o=s[(n[o+976>>2]|0)+2>>0]|0?1056:992:o=1056;break}default:o=o+52|0}return o|0}function s2(o,l){return o=o|0,l=l|0,(s[o+l>>0]|0)!=0|0}function by(o,l){return o=o|0,l=l|0,o=o+132|0,de(l)|0&&n[(kn(o,5,948)|0)+4>>2]|0?o=1:o=(n[(kn(o,n[1e3+(l<<2)>>2]|0,948)|0)+4>>2]|0)!=0,o|0}function Fh(o,l,u){o=o|0,l=l|0,u=y(u);var A=0,d=0;return o=o+132|0,de(l)|0&&(A=kn(o,5,948)|0,(n[A+4>>2]|0)!=0)?d=4:(A=kn(o,n[1e3+(l<<2)>>2]|0,948)|0,n[A+4>>2]|0?d=4:u=y(0)),(d|0)==4&&(u=y(Xr(A,u))),y(u)}function yP(o,l,u){return o=o|0,l=l|0,u=y(u),ga(o,l)|0?u=y(XA(o,l,u)):u=y(-y(Fh(o,l,u))),y(u)}function EP(o){return o=y(o),h[S>>2]=o,n[S>>2]|0|0}function Py(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>1073741823)Nt();else{d=Jt(l<<2)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<2)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<2)}function IP(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>2)<<2)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function xy(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-4-l|0)>>>2)<<2)),o=n[o>>2]|0,o|0&&Et(o)}function CP(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0;if(B=o+4|0,k=n[B>>2]|0,d=k-A|0,m=d>>2,o=l+(m<<2)|0,o>>>0>>0){A=k;do n[A>>2]=n[o>>2],o=o+4|0,A=(n[B>>2]|0)+4|0,n[B>>2]=A;while(o>>>0>>0)}m|0&&Q2(k+(0-m<<2)|0,l|0,d|0)|0}function wP(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0;return k=l+4|0,T=n[k>>2]|0,d=n[o>>2]|0,B=u,m=B-d|0,A=T+(0-(m>>2)<<2)|0,n[k>>2]=A,(m|0)>0&&Qr(A|0,d|0,m|0)|0,d=o+4|0,m=l+8|0,A=(n[d>>2]|0)-B|0,(A|0)>0&&(Qr(n[m>>2]|0,u|0,A|0)|0,n[m>>2]=(n[m>>2]|0)+(A>>>2<<2)),B=n[o>>2]|0,n[o>>2]=n[k>>2],n[k>>2]=B,B=n[d>>2]|0,n[d>>2]=n[m>>2],n[m>>2]=B,B=o+8|0,u=l+12|0,o=n[B>>2]|0,n[B>>2]=n[u>>2],n[u>>2]=o,n[l>>2]=n[k>>2],T|0}function o2(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;if(B=n[l>>2]|0,m=n[u>>2]|0,(B|0)!=(m|0)){d=o+8|0,u=((m+-4-B|0)>>>2)+1|0,o=B,A=n[d>>2]|0;do n[A>>2]=n[o>>2],A=(n[d>>2]|0)+4|0,n[d>>2]=A,o=o+4|0;while((o|0)!=(m|0));n[l>>2]=B+(u<<2)}}function a2(){fa()}function BP(){var o=0;return o=Jt(4)|0,l2(o),o|0}function l2(o){o=o|0,n[o>>2]=pc()|0}function vP(o){o=o|0,o|0&&(Kg(o),Et(o))}function Kg(o){o=o|0,st(n[o>>2]|0)}function JL(o,l,u){o=o|0,l=l|0,u=u|0,hc(n[o>>2]|0,l,u)}function ky(o,l){o=o|0,l=y(l),Ph(n[o>>2]|0,l)}function Qy(o,l){return o=o|0,l=l|0,s2(n[o>>2]|0,l)|0}function Ty(){var o=0;return o=Jt(8)|0,Jg(o,0),o|0}function Jg(o,l){o=o|0,l=l|0,l?l=Aa(n[l>>2]|0)|0:l=is()|0,n[o>>2]=l,n[o+4>>2]=0,Tn(l,o)}function Ry(o){o=o|0;var l=0;return l=Jt(8)|0,Jg(l,o),l|0}function zg(o){o=o|0,o|0&&(Fy(o),Et(o))}function Fy(o){o=o|0;var l=0;fc(n[o>>2]|0),l=o+4|0,o=n[l>>2]|0,n[l>>2]=0,o|0&&(Df(o),Et(o))}function Df(o){o=o|0,bf(o)}function bf(o){o=o|0,o=n[o>>2]|0,o|0&&Oa(o|0)}function c2(o){return o=o|0,Ga(o)|0}function u2(o){o=o|0;var l=0,u=0;u=o+4|0,l=n[u>>2]|0,n[u>>2]=0,l|0&&(Df(l),Et(l)),Ac(n[o>>2]|0)}function Ny(o,l){o=o|0,l=l|0,fn(n[o>>2]|0,n[l>>2]|0)}function zL(o,l){o=o|0,l=l|0,vh(n[o>>2]|0,l)}function ZL(o,l,u){o=o|0,l=l|0,u=+u,Ey(n[o>>2]|0,l,y(u))}function Oy(o,l,u){o=o|0,l=l|0,u=+u,Iy(n[o>>2]|0,l,y(u))}function f2(o,l){o=o|0,l=l|0,Ch(n[o>>2]|0,l)}function A2(o,l){o=o|0,l=l|0,bo(n[o>>2]|0,l)}function xr(o,l){o=o|0,l=l|0,Bh(n[o>>2]|0,l)}function ao(o,l){o=o|0,l=l|0,gy(n[o>>2]|0,l)}function Xi(o,l){o=o|0,l=l|0,Fg(n[o>>2]|0,l)}function Ls(o,l){o=o|0,l=l|0,Do(n[o>>2]|0,l)}function ep(o,l,u){o=o|0,l=l|0,u=+u,qA(n[o>>2]|0,l,y(u))}function p2(o,l,u){o=o|0,l=l|0,u=+u,Y(n[o>>2]|0,l,y(u))}function vs(o,l){o=o|0,l=l|0,GA(n[o>>2]|0,l)}function Ly(o,l){o=o|0,l=l|0,my(n[o>>2]|0,l)}function Nh(o,l){o=o|0,l=l|0,Po(n[o>>2]|0,l)}function Zg(o,l){o=o|0,l=+l,Sh(n[o>>2]|0,y(l))}function Oh(o,l){o=o|0,l=+l,Pl(n[o>>2]|0,y(l))}function h2(o,l){o=o|0,l=+l,yy(n[o>>2]|0,y(l))}function g2(o,l){o=o|0,l=+l,Og(n[o>>2]|0,y(l))}function d2(o,l){o=o|0,l=+l,bl(n[o>>2]|0,y(l))}function m2(o,l){o=o|0,l=+l,Lg(n[o>>2]|0,y(l))}function Pf(o,l){o=o|0,l=+l,e2(n[o>>2]|0,y(l))}function sr(o){o=o|0,Dh(n[o>>2]|0)}function My(o,l){o=o|0,l=+l,Zi(n[o>>2]|0,y(l))}function y2(o,l){o=o|0,l=+l,Ef(n[o>>2]|0,y(l))}function gc(o){o=o|0,Wa(n[o>>2]|0)}function xf(o,l){o=o|0,l=+l,yu(n[o>>2]|0,y(l))}function Xg(o,l){o=o|0,l=+l,If(n[o>>2]|0,y(l))}function $g(o,l){o=o|0,l=+l,di(n[o>>2]|0,y(l))}function E2(o,l){o=o|0,l=+l,WA(n[o>>2]|0,y(l))}function I2(o,l){o=o|0,l=+l,pa(n[o>>2]|0,y(l))}function vu(o,l){o=o|0,l=+l,Va(n[o>>2]|0,y(l))}function ed(o,l){o=o|0,l=+l,bh(n[o>>2]|0,y(l))}function C2(o,l){o=o|0,l=+l,Ug(n[o>>2]|0,y(l))}function _y(o,l){o=o|0,l=+l,YA(n[o>>2]|0,y(l))}function Su(o,l,u){o=o|0,l=l|0,u=+u,mu(n[o>>2]|0,l,y(u))}function Uy(o,l,u){o=o|0,l=l|0,u=+u,xo(n[o>>2]|0,l,y(u))}function td(o,l,u){o=o|0,l=l|0,u=+u,yf(n[o>>2]|0,l,y(u))}function rd(o){return o=o|0,Rg(n[o>>2]|0)|0}function To(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0;A=I,I=I+16|0,d=A,jA(d,n[l>>2]|0,u),Ss(o,d),I=A}function Ss(o,l){o=o|0,l=l|0,Rl(o,n[l+4>>2]|0,+y(h[l>>2]))}function Rl(o,l,u){o=o|0,l=l|0,u=+u,n[o>>2]=l,E[o+8>>3]=u}function Hy(o){return o=o|0,$1(n[o>>2]|0)|0}function da(o){return o=o|0,wh(n[o>>2]|0)|0}function SP(o){return o=o|0,du(n[o>>2]|0)|0}function Lh(o){return o=o|0,X1(n[o>>2]|0)|0}function w2(o){return o=o|0,Ng(n[o>>2]|0)|0}function XL(o){return o=o|0,dy(n[o>>2]|0)|0}function DP(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0;A=I,I=I+16|0,d=A,xt(d,n[l>>2]|0,u),Ss(o,d),I=A}function bP(o){return o=o|0,mf(n[o>>2]|0)|0}function jy(o){return o=o|0,Dl(n[o>>2]|0)|0}function B2(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,HA(A,n[l>>2]|0),Ss(o,A),I=u}function Mh(o){return o=o|0,+ +y(li(n[o>>2]|0))}function PP(o){return o=o|0,+ +y(Gi(n[o>>2]|0))}function xP(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,ur(A,n[l>>2]|0),Ss(o,A),I=u}function nd(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,Mg(A,n[l>>2]|0),Ss(o,A),I=u}function $L(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,wt(A,n[l>>2]|0),Ss(o,A),I=u}function eM(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,Ya(A,n[l>>2]|0),Ss(o,A),I=u}function kP(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,_g(A,n[l>>2]|0),Ss(o,A),I=u}function QP(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,wy(A,n[l>>2]|0),Ss(o,A),I=u}function tp(o){return o=o|0,+ +y(Hg(n[o>>2]|0))}function tM(o,l){return o=o|0,l=l|0,+ +y(Cy(n[o>>2]|0,l))}function rM(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0;A=I,I=I+16|0,d=A,mt(d,n[l>>2]|0,u),Ss(o,d),I=A}function Du(o,l,u){o=o|0,l=l|0,u=u|0,lr(n[o>>2]|0,n[l>>2]|0,u)}function nM(o,l){o=o|0,l=l|0,df(n[o>>2]|0,n[l>>2]|0)}function TP(o){return o=o|0,_i(n[o>>2]|0)|0}function iM(o){return o=o|0,o=yt(n[o>>2]|0)|0,o?o=c2(o)|0:o=0,o|0}function RP(o,l){return o=o|0,l=l|0,o=ws(n[o>>2]|0,l)|0,o?o=c2(o)|0:o=0,o|0}function kf(o,l){o=o|0,l=l|0;var u=0,A=0;A=Jt(4)|0,FP(A,l),u=o+4|0,l=n[u>>2]|0,n[u>>2]=A,l|0&&(Df(l),Et(l)),St(n[o>>2]|0,1)}function FP(o,l){o=o|0,l=l|0,lM(o,l)}function sM(o,l,u,A,d,m){o=o|0,l=l|0,u=y(u),A=A|0,d=y(d),m=m|0;var B=0,k=0;B=I,I=I+16|0,k=B,NP(k,Ga(l)|0,+u,A,+d,m),h[o>>2]=y(+E[k>>3]),h[o+4>>2]=y(+E[k+8>>3]),I=B}function NP(o,l,u,A,d,m){o=o|0,l=l|0,u=+u,A=A|0,d=+d,m=m|0;var B=0,k=0,T=0,_=0,M=0;B=I,I=I+32|0,M=B+8|0,_=B+20|0,T=B,k=B+16|0,E[M>>3]=u,n[_>>2]=A,E[T>>3]=d,n[k>>2]=m,qy(o,n[l+4>>2]|0,M,_,T,k),I=B}function qy(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0;var B=0,k=0;B=I,I=I+16|0,k=B,Nl(k),l=Ms(l)|0,OP(o,l,+E[u>>3],n[A>>2]|0,+E[d>>3],n[m>>2]|0),Ol(k),I=B}function Ms(o){return o=o|0,n[o>>2]|0}function OP(o,l,u,A,d,m){o=o|0,l=l|0,u=+u,A=A|0,d=+d,m=m|0;var B=0;B=ma(v2()|0)|0,u=+Ja(u),A=Gy(A)|0,d=+Ja(d),oM(o,Jn(0,B|0,l|0,+u,A|0,+d,Gy(m)|0)|0)}function v2(){var o=0;return s[7608]|0||(D2(9120),o=7608,n[o>>2]=1,n[o+4>>2]=0),9120}function ma(o){return o=o|0,n[o+8>>2]|0}function Ja(o){return o=+o,+ +Qf(o)}function Gy(o){return o=o|0,id(o)|0}function oM(o,l){o=o|0,l=l|0;var u=0,A=0,d=0;d=I,I=I+32|0,u=d,A=l,A&1?(za(u,0),Me(A|0,u|0)|0,S2(o,u),aM(u)):(n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=n[l+8>>2],n[o+12>>2]=n[l+12>>2]),I=d}function za(o,l){o=o|0,l=l|0,bu(o,l),n[o+8>>2]=0,s[o+24>>0]=0}function S2(o,l){o=o|0,l=l|0,l=l+8|0,n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=n[l+8>>2],n[o+12>>2]=n[l+12>>2]}function aM(o){o=o|0,s[o+24>>0]=0}function bu(o,l){o=o|0,l=l|0,n[o>>2]=l}function id(o){return o=o|0,o|0}function Qf(o){return o=+o,+o}function D2(o){o=o|0,Ro(o,b2()|0,4)}function b2(){return 1064}function Ro(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u,n[o+8>>2]=qi(l|0,u+1|0)|0}function lM(o,l){o=o|0,l=l|0,l=n[l>>2]|0,n[o>>2]=l,cu(l|0)}function LP(o){o=o|0;var l=0,u=0;u=o+4|0,l=n[u>>2]|0,n[u>>2]=0,l|0&&(Df(l),Et(l)),St(n[o>>2]|0,0)}function MP(o){o=o|0,Dt(n[o>>2]|0)}function Wy(o){return o=o|0,tr(n[o>>2]|0)|0}function cM(o,l,u,A){o=o|0,l=+l,u=+u,A=A|0,KA(n[o>>2]|0,y(l),y(u),A)}function uM(o){return o=o|0,+ +y(Eu(n[o>>2]|0))}function v(o){return o=o|0,+ +y(Cf(n[o>>2]|0))}function D(o){return o=o|0,+ +y(Iu(n[o>>2]|0))}function Q(o){return o=o|0,+ +y(Ns(n[o>>2]|0))}function H(o){return o=o|0,+ +y(Cu(n[o>>2]|0))}function V(o){return o=o|0,+ +y(qn(n[o>>2]|0))}function ne(o,l){o=o|0,l=l|0,E[o>>3]=+y(Eu(n[l>>2]|0)),E[o+8>>3]=+y(Cf(n[l>>2]|0)),E[o+16>>3]=+y(Iu(n[l>>2]|0)),E[o+24>>3]=+y(Ns(n[l>>2]|0)),E[o+32>>3]=+y(Cu(n[l>>2]|0)),E[o+40>>3]=+y(qn(n[l>>2]|0))}function Se(o,l){return o=o|0,l=l|0,+ +y(ss(n[o>>2]|0,l))}function Ue(o,l){return o=o|0,l=l|0,+ +y(ki(n[o>>2]|0,l))}function At(o,l){return o=o|0,l=l|0,+ +y(VA(n[o>>2]|0,l))}function Gt(){return Qn()|0}function vr(){Lr(),Xt(),zn(),yi(),Za(),$e()}function Lr(){vqe(11713,4938,1)}function Xt(){q6e(10448)}function zn(){v6e(10408)}function yi(){Vje(10324)}function Za(){tHe(10096)}function $e(){qe(9132)}function qe(o){o=o|0;var l=0,u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0,Le=0,Qe=0,tt=0,Ze=0,ct=0,He=0,Ge=0,Lt=0,qr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,Hn=0,Fo=0,No=0,Oo=0,$a=0,Vh=0,Kh=0,dc=0,Jh=0,Ff=0,Nf=0,zh=0,Zh=0,Xh=0,on=0,mc=0,$h=0,ku=0,e0=0,t0=0,Of=0,Lf=0,Qu=0,lo=0,Ml=0,ya=0,yc=0,lp=0,cp=0,Mf=0,up=0,fp=0,co=0,Us=0,Ec=0,Wn=0,Ap=0,Lo=0,Tu=0,Mo=0,Ru=0,pp=0,hp=0,Fu=0,uo=0,Ic=0,gp=0,dp=0,mp=0,Fr=0,ui=0,Hs=0,_o=0,fo=0,Mr=0,Ar=0,Cc=0;l=I,I=I+672|0,u=l+656|0,Cc=l+648|0,Ar=l+640|0,Mr=l+632|0,fo=l+624|0,_o=l+616|0,Hs=l+608|0,ui=l+600|0,Fr=l+592|0,mp=l+584|0,dp=l+576|0,gp=l+568|0,Ic=l+560|0,uo=l+552|0,Fu=l+544|0,hp=l+536|0,pp=l+528|0,Ru=l+520|0,Mo=l+512|0,Tu=l+504|0,Lo=l+496|0,Ap=l+488|0,Wn=l+480|0,Ec=l+472|0,Us=l+464|0,co=l+456|0,fp=l+448|0,up=l+440|0,Mf=l+432|0,cp=l+424|0,lp=l+416|0,yc=l+408|0,ya=l+400|0,Ml=l+392|0,lo=l+384|0,Qu=l+376|0,Lf=l+368|0,Of=l+360|0,t0=l+352|0,e0=l+344|0,ku=l+336|0,$h=l+328|0,mc=l+320|0,on=l+312|0,Xh=l+304|0,Zh=l+296|0,zh=l+288|0,Nf=l+280|0,Ff=l+272|0,Jh=l+264|0,dc=l+256|0,Kh=l+248|0,Vh=l+240|0,$a=l+232|0,Oo=l+224|0,No=l+216|0,Fo=l+208|0,Hn=l+200|0,cr=l+192|0,Hr=l+184|0,Tr=l+176|0,$t=l+168|0,fr=l+160|0,qr=l+152|0,Lt=l+144|0,Ge=l+136|0,He=l+128|0,ct=l+120|0,Ze=l+112|0,tt=l+104|0,Qe=l+96|0,Le=l+88|0,We=l+80|0,ae=l+72|0,G=l+64|0,M=l+56|0,_=l+48|0,T=l+40|0,k=l+32|0,B=l+24|0,m=l+16|0,d=l+8|0,A=l,ht(o,3646),Zt(o,3651,2)|0,Sr(o,3665,2)|0,Xn(o,3682,18)|0,n[Cc>>2]=19,n[Cc+4>>2]=0,n[u>>2]=n[Cc>>2],n[u+4>>2]=n[Cc+4>>2],kr(o,3690,u)|0,n[Ar>>2]=1,n[Ar+4>>2]=0,n[u>>2]=n[Ar>>2],n[u+4>>2]=n[Ar+4>>2],Rn(o,3696,u)|0,n[Mr>>2]=2,n[Mr+4>>2]=0,n[u>>2]=n[Mr>>2],n[u+4>>2]=n[Mr+4>>2],Un(o,3706,u)|0,n[fo>>2]=1,n[fo+4>>2]=0,n[u>>2]=n[fo>>2],n[u+4>>2]=n[fo+4>>2],zr(o,3722,u)|0,n[_o>>2]=2,n[_o+4>>2]=0,n[u>>2]=n[_o>>2],n[u+4>>2]=n[_o+4>>2],zr(o,3734,u)|0,n[Hs>>2]=3,n[Hs+4>>2]=0,n[u>>2]=n[Hs>>2],n[u+4>>2]=n[Hs+4>>2],Un(o,3753,u)|0,n[ui>>2]=4,n[ui+4>>2]=0,n[u>>2]=n[ui>>2],n[u+4>>2]=n[ui+4>>2],Un(o,3769,u)|0,n[Fr>>2]=5,n[Fr+4>>2]=0,n[u>>2]=n[Fr>>2],n[u+4>>2]=n[Fr+4>>2],Un(o,3783,u)|0,n[mp>>2]=6,n[mp+4>>2]=0,n[u>>2]=n[mp>>2],n[u+4>>2]=n[mp+4>>2],Un(o,3796,u)|0,n[dp>>2]=7,n[dp+4>>2]=0,n[u>>2]=n[dp>>2],n[u+4>>2]=n[dp+4>>2],Un(o,3813,u)|0,n[gp>>2]=8,n[gp+4>>2]=0,n[u>>2]=n[gp>>2],n[u+4>>2]=n[gp+4>>2],Un(o,3825,u)|0,n[Ic>>2]=3,n[Ic+4>>2]=0,n[u>>2]=n[Ic>>2],n[u+4>>2]=n[Ic+4>>2],zr(o,3843,u)|0,n[uo>>2]=4,n[uo+4>>2]=0,n[u>>2]=n[uo>>2],n[u+4>>2]=n[uo+4>>2],zr(o,3853,u)|0,n[Fu>>2]=9,n[Fu+4>>2]=0,n[u>>2]=n[Fu>>2],n[u+4>>2]=n[Fu+4>>2],Un(o,3870,u)|0,n[hp>>2]=10,n[hp+4>>2]=0,n[u>>2]=n[hp>>2],n[u+4>>2]=n[hp+4>>2],Un(o,3884,u)|0,n[pp>>2]=11,n[pp+4>>2]=0,n[u>>2]=n[pp>>2],n[u+4>>2]=n[pp+4>>2],Un(o,3896,u)|0,n[Ru>>2]=1,n[Ru+4>>2]=0,n[u>>2]=n[Ru>>2],n[u+4>>2]=n[Ru+4>>2],ci(o,3907,u)|0,n[Mo>>2]=2,n[Mo+4>>2]=0,n[u>>2]=n[Mo>>2],n[u+4>>2]=n[Mo+4>>2],ci(o,3915,u)|0,n[Tu>>2]=3,n[Tu+4>>2]=0,n[u>>2]=n[Tu>>2],n[u+4>>2]=n[Tu+4>>2],ci(o,3928,u)|0,n[Lo>>2]=4,n[Lo+4>>2]=0,n[u>>2]=n[Lo>>2],n[u+4>>2]=n[Lo+4>>2],ci(o,3948,u)|0,n[Ap>>2]=5,n[Ap+4>>2]=0,n[u>>2]=n[Ap>>2],n[u+4>>2]=n[Ap+4>>2],ci(o,3960,u)|0,n[Wn>>2]=6,n[Wn+4>>2]=0,n[u>>2]=n[Wn>>2],n[u+4>>2]=n[Wn+4>>2],ci(o,3974,u)|0,n[Ec>>2]=7,n[Ec+4>>2]=0,n[u>>2]=n[Ec>>2],n[u+4>>2]=n[Ec+4>>2],ci(o,3983,u)|0,n[Us>>2]=20,n[Us+4>>2]=0,n[u>>2]=n[Us>>2],n[u+4>>2]=n[Us+4>>2],kr(o,3999,u)|0,n[co>>2]=8,n[co+4>>2]=0,n[u>>2]=n[co>>2],n[u+4>>2]=n[co+4>>2],ci(o,4012,u)|0,n[fp>>2]=9,n[fp+4>>2]=0,n[u>>2]=n[fp>>2],n[u+4>>2]=n[fp+4>>2],ci(o,4022,u)|0,n[up>>2]=21,n[up+4>>2]=0,n[u>>2]=n[up>>2],n[u+4>>2]=n[up+4>>2],kr(o,4039,u)|0,n[Mf>>2]=10,n[Mf+4>>2]=0,n[u>>2]=n[Mf>>2],n[u+4>>2]=n[Mf+4>>2],ci(o,4053,u)|0,n[cp>>2]=11,n[cp+4>>2]=0,n[u>>2]=n[cp>>2],n[u+4>>2]=n[cp+4>>2],ci(o,4065,u)|0,n[lp>>2]=12,n[lp+4>>2]=0,n[u>>2]=n[lp>>2],n[u+4>>2]=n[lp+4>>2],ci(o,4084,u)|0,n[yc>>2]=13,n[yc+4>>2]=0,n[u>>2]=n[yc>>2],n[u+4>>2]=n[yc+4>>2],ci(o,4097,u)|0,n[ya>>2]=14,n[ya+4>>2]=0,n[u>>2]=n[ya>>2],n[u+4>>2]=n[ya+4>>2],ci(o,4117,u)|0,n[Ml>>2]=15,n[Ml+4>>2]=0,n[u>>2]=n[Ml>>2],n[u+4>>2]=n[Ml+4>>2],ci(o,4129,u)|0,n[lo>>2]=16,n[lo+4>>2]=0,n[u>>2]=n[lo>>2],n[u+4>>2]=n[lo+4>>2],ci(o,4148,u)|0,n[Qu>>2]=17,n[Qu+4>>2]=0,n[u>>2]=n[Qu>>2],n[u+4>>2]=n[Qu+4>>2],ci(o,4161,u)|0,n[Lf>>2]=18,n[Lf+4>>2]=0,n[u>>2]=n[Lf>>2],n[u+4>>2]=n[Lf+4>>2],ci(o,4181,u)|0,n[Of>>2]=5,n[Of+4>>2]=0,n[u>>2]=n[Of>>2],n[u+4>>2]=n[Of+4>>2],zr(o,4196,u)|0,n[t0>>2]=6,n[t0+4>>2]=0,n[u>>2]=n[t0>>2],n[u+4>>2]=n[t0+4>>2],zr(o,4206,u)|0,n[e0>>2]=7,n[e0+4>>2]=0,n[u>>2]=n[e0>>2],n[u+4>>2]=n[e0+4>>2],zr(o,4217,u)|0,n[ku>>2]=3,n[ku+4>>2]=0,n[u>>2]=n[ku>>2],n[u+4>>2]=n[ku+4>>2],Pu(o,4235,u)|0,n[$h>>2]=1,n[$h+4>>2]=0,n[u>>2]=n[$h>>2],n[u+4>>2]=n[$h+4>>2],fM(o,4251,u)|0,n[mc>>2]=4,n[mc+4>>2]=0,n[u>>2]=n[mc>>2],n[u+4>>2]=n[mc+4>>2],Pu(o,4263,u)|0,n[on>>2]=5,n[on+4>>2]=0,n[u>>2]=n[on>>2],n[u+4>>2]=n[on+4>>2],Pu(o,4279,u)|0,n[Xh>>2]=6,n[Xh+4>>2]=0,n[u>>2]=n[Xh>>2],n[u+4>>2]=n[Xh+4>>2],Pu(o,4293,u)|0,n[Zh>>2]=7,n[Zh+4>>2]=0,n[u>>2]=n[Zh>>2],n[u+4>>2]=n[Zh+4>>2],Pu(o,4306,u)|0,n[zh>>2]=8,n[zh+4>>2]=0,n[u>>2]=n[zh>>2],n[u+4>>2]=n[zh+4>>2],Pu(o,4323,u)|0,n[Nf>>2]=9,n[Nf+4>>2]=0,n[u>>2]=n[Nf>>2],n[u+4>>2]=n[Nf+4>>2],Pu(o,4335,u)|0,n[Ff>>2]=2,n[Ff+4>>2]=0,n[u>>2]=n[Ff>>2],n[u+4>>2]=n[Ff+4>>2],fM(o,4353,u)|0,n[Jh>>2]=12,n[Jh+4>>2]=0,n[u>>2]=n[Jh>>2],n[u+4>>2]=n[Jh+4>>2],sd(o,4363,u)|0,n[dc>>2]=1,n[dc+4>>2]=0,n[u>>2]=n[dc>>2],n[u+4>>2]=n[dc+4>>2],rp(o,4376,u)|0,n[Kh>>2]=2,n[Kh+4>>2]=0,n[u>>2]=n[Kh>>2],n[u+4>>2]=n[Kh+4>>2],rp(o,4388,u)|0,n[Vh>>2]=13,n[Vh+4>>2]=0,n[u>>2]=n[Vh>>2],n[u+4>>2]=n[Vh+4>>2],sd(o,4402,u)|0,n[$a>>2]=14,n[$a+4>>2]=0,n[u>>2]=n[$a>>2],n[u+4>>2]=n[$a+4>>2],sd(o,4411,u)|0,n[Oo>>2]=15,n[Oo+4>>2]=0,n[u>>2]=n[Oo>>2],n[u+4>>2]=n[Oo+4>>2],sd(o,4421,u)|0,n[No>>2]=16,n[No+4>>2]=0,n[u>>2]=n[No>>2],n[u+4>>2]=n[No+4>>2],sd(o,4433,u)|0,n[Fo>>2]=17,n[Fo+4>>2]=0,n[u>>2]=n[Fo>>2],n[u+4>>2]=n[Fo+4>>2],sd(o,4446,u)|0,n[Hn>>2]=18,n[Hn+4>>2]=0,n[u>>2]=n[Hn>>2],n[u+4>>2]=n[Hn+4>>2],sd(o,4458,u)|0,n[cr>>2]=3,n[cr+4>>2]=0,n[u>>2]=n[cr>>2],n[u+4>>2]=n[cr+4>>2],rp(o,4471,u)|0,n[Hr>>2]=1,n[Hr+4>>2]=0,n[u>>2]=n[Hr>>2],n[u+4>>2]=n[Hr+4>>2],_P(o,4486,u)|0,n[Tr>>2]=10,n[Tr+4>>2]=0,n[u>>2]=n[Tr>>2],n[u+4>>2]=n[Tr+4>>2],Pu(o,4496,u)|0,n[$t>>2]=11,n[$t+4>>2]=0,n[u>>2]=n[$t>>2],n[u+4>>2]=n[$t+4>>2],Pu(o,4508,u)|0,n[fr>>2]=3,n[fr+4>>2]=0,n[u>>2]=n[fr>>2],n[u+4>>2]=n[fr+4>>2],fM(o,4519,u)|0,n[qr>>2]=4,n[qr+4>>2]=0,n[u>>2]=n[qr>>2],n[u+4>>2]=n[qr+4>>2],TOe(o,4530,u)|0,n[Lt>>2]=19,n[Lt+4>>2]=0,n[u>>2]=n[Lt>>2],n[u+4>>2]=n[Lt+4>>2],ROe(o,4542,u)|0,n[Ge>>2]=12,n[Ge+4>>2]=0,n[u>>2]=n[Ge>>2],n[u+4>>2]=n[Ge+4>>2],FOe(o,4554,u)|0,n[He>>2]=13,n[He+4>>2]=0,n[u>>2]=n[He>>2],n[u+4>>2]=n[He+4>>2],NOe(o,4568,u)|0,n[ct>>2]=2,n[ct+4>>2]=0,n[u>>2]=n[ct>>2],n[u+4>>2]=n[ct+4>>2],OOe(o,4578,u)|0,n[Ze>>2]=20,n[Ze+4>>2]=0,n[u>>2]=n[Ze>>2],n[u+4>>2]=n[Ze+4>>2],LOe(o,4587,u)|0,n[tt>>2]=22,n[tt+4>>2]=0,n[u>>2]=n[tt>>2],n[u+4>>2]=n[tt+4>>2],kr(o,4602,u)|0,n[Qe>>2]=23,n[Qe+4>>2]=0,n[u>>2]=n[Qe>>2],n[u+4>>2]=n[Qe+4>>2],kr(o,4619,u)|0,n[Le>>2]=14,n[Le+4>>2]=0,n[u>>2]=n[Le>>2],n[u+4>>2]=n[Le+4>>2],MOe(o,4629,u)|0,n[We>>2]=1,n[We+4>>2]=0,n[u>>2]=n[We>>2],n[u+4>>2]=n[We+4>>2],_Oe(o,4637,u)|0,n[ae>>2]=4,n[ae+4>>2]=0,n[u>>2]=n[ae>>2],n[u+4>>2]=n[ae+4>>2],rp(o,4653,u)|0,n[G>>2]=5,n[G+4>>2]=0,n[u>>2]=n[G>>2],n[u+4>>2]=n[G+4>>2],rp(o,4669,u)|0,n[M>>2]=6,n[M+4>>2]=0,n[u>>2]=n[M>>2],n[u+4>>2]=n[M+4>>2],rp(o,4686,u)|0,n[_>>2]=7,n[_+4>>2]=0,n[u>>2]=n[_>>2],n[u+4>>2]=n[_+4>>2],rp(o,4701,u)|0,n[T>>2]=8,n[T+4>>2]=0,n[u>>2]=n[T>>2],n[u+4>>2]=n[T+4>>2],rp(o,4719,u)|0,n[k>>2]=9,n[k+4>>2]=0,n[u>>2]=n[k>>2],n[u+4>>2]=n[k+4>>2],rp(o,4736,u)|0,n[B>>2]=21,n[B+4>>2]=0,n[u>>2]=n[B>>2],n[u+4>>2]=n[B+4>>2],UOe(o,4754,u)|0,n[m>>2]=2,n[m+4>>2]=0,n[u>>2]=n[m>>2],n[u+4>>2]=n[m+4>>2],_P(o,4772,u)|0,n[d>>2]=3,n[d+4>>2]=0,n[u>>2]=n[d>>2],n[u+4>>2]=n[d+4>>2],_P(o,4790,u)|0,n[A>>2]=4,n[A+4>>2]=0,n[u>>2]=n[A>>2],n[u+4>>2]=n[A+4>>2],_P(o,4808,u)|0,I=l}function ht(o,l){o=o|0,l=l|0;var u=0;u=Y8e()|0,n[o>>2]=u,V8e(u,l),Gh(n[o>>2]|0)}function Zt(o,l,u){return o=o|0,l=l|0,u=u|0,T8e(o,Bn(l)|0,u,0),o|0}function Sr(o,l,u){return o=o|0,l=l|0,u=u|0,d8e(o,Bn(l)|0,u,0),o|0}function Xn(o,l,u){return o=o|0,l=l|0,u=u|0,r8e(o,Bn(l)|0,u,0),o|0}function kr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],U3e(o,l,d),I=A,o|0}function Rn(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],C3e(o,l,d),I=A,o|0}function Un(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],s3e(o,l,d),I=A,o|0}function zr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],q4e(o,l,d),I=A,o|0}function ci(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],b4e(o,l,d),I=A,o|0}function Pu(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],f4e(o,l,d),I=A,o|0}function fM(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],JUe(o,l,d),I=A,o|0}function sd(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],CUe(o,l,d),I=A,o|0}function rp(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],sUe(o,l,d),I=A,o|0}function _P(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],q_e(o,l,d),I=A,o|0}function TOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],b_e(o,l,d),I=A,o|0}function ROe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],f_e(o,l,d),I=A,o|0}function FOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],zMe(o,l,d),I=A,o|0}function NOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],NMe(o,l,d),I=A,o|0}function OOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],yMe(o,l,d),I=A,o|0}function LOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],tMe(o,l,d),I=A,o|0}function MOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],_Le(o,l,d),I=A,o|0}function _Oe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],CLe(o,l,d),I=A,o|0}function UOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],HOe(o,l,d),I=A,o|0}function HOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],jOe(o,u,d,1),I=A}function Bn(o){return o=o|0,o|0}function jOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=AM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=qOe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,GOe(m,A)|0,A),I=d}function AM(){var o=0,l=0;if(s[7616]|0||(jz(9136),gr(24,9136,U|0)|0,l=7616,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9136)|0)){o=9136,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));jz(9136)}return 9136}function qOe(o){return o=o|0,0}function GOe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=AM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],Hz(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(VOe(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function vn(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0;var B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0;B=I,I=I+32|0,ae=B+24|0,G=B+20|0,T=B+16|0,M=B+12|0,_=B+8|0,k=B+4|0,We=B,n[G>>2]=l,n[T>>2]=u,n[M>>2]=A,n[_>>2]=d,n[k>>2]=m,m=o+28|0,n[We>>2]=n[m>>2],n[ae>>2]=n[We>>2],WOe(o+24|0,ae,G,M,_,T,k)|0,n[m>>2]=n[n[m>>2]>>2],I=B}function WOe(o,l,u,A,d,m,B){return o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0,B=B|0,o=YOe(l)|0,l=Jt(24)|0,Uz(l+4|0,n[u>>2]|0,n[A>>2]|0,n[d>>2]|0,n[m>>2]|0,n[B>>2]|0),n[l>>2]=n[o>>2],n[o>>2]=l,l|0}function YOe(o){return o=o|0,n[o>>2]|0}function Uz(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0,n[o>>2]=l,n[o+4>>2]=u,n[o+8>>2]=A,n[o+12>>2]=d,n[o+16>>2]=m}function yr(o,l){return o=o|0,l=l|0,l|o|0}function Hz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function VOe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=KOe(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,JOe(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Hz(m,A,u),n[T>>2]=(n[T>>2]|0)+12,zOe(o,k),ZOe(k),I=_;return}}function KOe(o){return o=o|0,357913941}function JOe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function zOe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function ZOe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function jz(o){o=o|0,eLe(o)}function XOe(o){o=o|0,$Oe(o+24|0)}function Ur(o){return o=o|0,n[o>>2]|0}function $Oe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function eLe(o){o=o|0;var l=0;l=en()|0,tn(o,2,3,l,tLe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function en(){return 9228}function tLe(){return 1140}function rLe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0;return u=I,I=I+16|0,A=u+8|0,d=u,m=nLe(o)|0,o=n[m+4>>2]|0,n[d>>2]=n[m>>2],n[d+4>>2]=o,n[A>>2]=n[d>>2],n[A+4>>2]=n[d+4>>2],l=iLe(l,A)|0,I=u,l|0}function tn(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0,n[o>>2]=l,n[o+4>>2]=u,n[o+8>>2]=A,n[o+12>>2]=d,n[o+16>>2]=m}function nLe(o){return o=o|0,(n[(AM()|0)+24>>2]|0)+(o*12|0)|0}function iLe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0;return d=I,I=I+48|0,A=d,u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),ap[u&31](A,o),A=sLe(A)|0,I=d,A|0}function sLe(o){o=o|0;var l=0,u=0,A=0,d=0;return d=I,I=I+32|0,l=d+12|0,u=d,A=pM(qz()|0)|0,A?(hM(l,A),gM(u,l),oLe(o,u),o=dM(l)|0):o=aLe(o)|0,I=d,o|0}function qz(){var o=0;return s[7632]|0||(mLe(9184),gr(25,9184,U|0)|0,o=7632,n[o>>2]=1,n[o+4>>2]=0),9184}function pM(o){return o=o|0,n[o+36>>2]|0}function hM(o,l){o=o|0,l=l|0,n[o>>2]=l,n[o+4>>2]=o,n[o+8>>2]=0}function gM(o,l){o=o|0,l=l|0,n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=0}function oLe(o,l){o=o|0,l=l|0,fLe(l,o,o+8|0,o+16|0,o+24|0,o+32|0,o+40|0)|0}function dM(o){return o=o|0,n[(n[o+4>>2]|0)+8>>2]|0}function aLe(o){o=o|0;var l=0,u=0,A=0,d=0,m=0,B=0,k=0,T=0;T=I,I=I+16|0,u=T+4|0,A=T,d=Fl(8)|0,m=d,B=Jt(48)|0,k=B,l=k+48|0;do n[k>>2]=n[o>>2],k=k+4|0,o=o+4|0;while((k|0)<(l|0));return l=m+4|0,n[l>>2]=B,k=Jt(8)|0,B=n[l>>2]|0,n[A>>2]=0,n[u>>2]=n[A>>2],Gz(k,B,u),n[d>>2]=k,I=T,m|0}function Gz(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Jt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1092,n[u+12>>2]=l,n[o+4>>2]=u}function lLe(o){o=o|0,Zy(o),Et(o)}function cLe(o){o=o|0,o=n[o+12>>2]|0,o|0&&Et(o)}function uLe(o){o=o|0,Et(o)}function fLe(o,l,u,A,d,m,B){return o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0,B=B|0,m=ALe(n[o>>2]|0,l,u,A,d,m,B)|0,B=o+4|0,n[(n[B>>2]|0)+8>>2]=m,n[(n[B>>2]|0)+8>>2]|0}function ALe(o,l,u,A,d,m,B){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0,B=B|0;var k=0,T=0;return k=I,I=I+16|0,T=k,Nl(T),o=Ms(o)|0,B=pLe(o,+E[l>>3],+E[u>>3],+E[A>>3],+E[d>>3],+E[m>>3],+E[B>>3])|0,Ol(T),I=k,B|0}function pLe(o,l,u,A,d,m,B){o=o|0,l=+l,u=+u,A=+A,d=+d,m=+m,B=+B;var k=0;return k=ma(hLe()|0)|0,l=+Ja(l),u=+Ja(u),A=+Ja(A),d=+Ja(d),m=+Ja(m),io(0,k|0,o|0,+l,+u,+A,+d,+m,+ +Ja(B))|0}function hLe(){var o=0;return s[7624]|0||(gLe(9172),o=7624,n[o>>2]=1,n[o+4>>2]=0),9172}function gLe(o){o=o|0,Ro(o,dLe()|0,6)}function dLe(){return 1112}function mLe(o){o=o|0,_h(o)}function yLe(o){o=o|0,Wz(o+24|0),Yz(o+16|0)}function Wz(o){o=o|0,ILe(o)}function Yz(o){o=o|0,ELe(o)}function ELe(o){o=o|0;var l=0,u=0;if(l=n[o>>2]|0,l|0)do u=l,l=n[l>>2]|0,Et(u);while(l|0);n[o>>2]=0}function ILe(o){o=o|0;var l=0,u=0;if(l=n[o>>2]|0,l|0)do u=l,l=n[l>>2]|0,Et(u);while(l|0);n[o>>2]=0}function _h(o){o=o|0;var l=0;n[o+16>>2]=0,n[o+20>>2]=0,l=o+24|0,n[l>>2]=0,n[o+28>>2]=l,n[o+36>>2]=0,s[o+40>>0]=0,s[o+41>>0]=0}function CLe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],wLe(o,u,d,0),I=A}function wLe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=mM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=BLe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,vLe(m,A)|0,A),I=d}function mM(){var o=0,l=0;if(s[7640]|0||(Kz(9232),gr(26,9232,U|0)|0,l=7640,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9232)|0)){o=9232,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));Kz(9232)}return 9232}function BLe(o){return o=o|0,0}function vLe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=mM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],Vz(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(SLe(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function Vz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function SLe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=DLe(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,bLe(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Vz(m,A,u),n[T>>2]=(n[T>>2]|0)+12,PLe(o,k),xLe(k),I=_;return}}function DLe(o){return o=o|0,357913941}function bLe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function PLe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function xLe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function Kz(o){o=o|0,TLe(o)}function kLe(o){o=o|0,QLe(o+24|0)}function QLe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function TLe(o){o=o|0;var l=0;l=en()|0,tn(o,2,1,l,RLe()|0,3),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function RLe(){return 1144}function FLe(o,l,u,A,d){o=o|0,l=l|0,u=+u,A=+A,d=d|0;var m=0,B=0,k=0,T=0;m=I,I=I+16|0,B=m+8|0,k=m,T=NLe(o)|0,o=n[T+4>>2]|0,n[k>>2]=n[T>>2],n[k+4>>2]=o,n[B>>2]=n[k>>2],n[B+4>>2]=n[k+4>>2],OLe(l,B,u,A,d),I=m}function NLe(o){return o=o|0,(n[(mM()|0)+24>>2]|0)+(o*12|0)|0}function OLe(o,l,u,A,d){o=o|0,l=l|0,u=+u,A=+A,d=d|0;var m=0,B=0,k=0,T=0,_=0;_=I,I=I+16|0,B=_+2|0,k=_+1|0,T=_,m=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(m=n[(n[o>>2]|0)+m>>2]|0),Tf(B,u),u=+Rf(B,u),Tf(k,A),A=+Rf(k,A),np(T,d),T=ip(T,d)|0,c$[m&1](o,u,A,T),I=_}function Tf(o,l){o=o|0,l=+l}function Rf(o,l){return o=o|0,l=+l,+ +MLe(l)}function np(o,l){o=o|0,l=l|0}function ip(o,l){return o=o|0,l=l|0,LLe(l)|0}function LLe(o){return o=o|0,o|0}function MLe(o){return o=+o,+o}function _Le(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],ULe(o,u,d,1),I=A}function ULe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=yM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=HLe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,jLe(m,A)|0,A),I=d}function yM(){var o=0,l=0;if(s[7648]|0||(zz(9268),gr(27,9268,U|0)|0,l=7648,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9268)|0)){o=9268,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));zz(9268)}return 9268}function HLe(o){return o=o|0,0}function jLe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=yM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],Jz(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(qLe(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function Jz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function qLe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=GLe(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,WLe(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Jz(m,A,u),n[T>>2]=(n[T>>2]|0)+12,YLe(o,k),VLe(k),I=_;return}}function GLe(o){return o=o|0,357913941}function WLe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function YLe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function VLe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function zz(o){o=o|0,zLe(o)}function KLe(o){o=o|0,JLe(o+24|0)}function JLe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function zLe(o){o=o|0;var l=0;l=en()|0,tn(o,2,4,l,ZLe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function ZLe(){return 1160}function XLe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0;return u=I,I=I+16|0,A=u+8|0,d=u,m=$Le(o)|0,o=n[m+4>>2]|0,n[d>>2]=n[m>>2],n[d+4>>2]=o,n[A>>2]=n[d>>2],n[A+4>>2]=n[d+4>>2],l=eMe(l,A)|0,I=u,l|0}function $Le(o){return o=o|0,(n[(yM()|0)+24>>2]|0)+(o*12|0)|0}function eMe(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),Zz(hd[u&31](o)|0)|0}function Zz(o){return o=o|0,o&1|0}function tMe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],rMe(o,u,d,0),I=A}function rMe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=EM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=nMe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,iMe(m,A)|0,A),I=d}function EM(){var o=0,l=0;if(s[7656]|0||($z(9304),gr(28,9304,U|0)|0,l=7656,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9304)|0)){o=9304,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));$z(9304)}return 9304}function nMe(o){return o=o|0,0}function iMe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=EM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],Xz(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(sMe(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function Xz(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function sMe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=oMe(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,aMe(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],Xz(m,A,u),n[T>>2]=(n[T>>2]|0)+12,lMe(o,k),cMe(k),I=_;return}}function oMe(o){return o=o|0,357913941}function aMe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function lMe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function cMe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function $z(o){o=o|0,AMe(o)}function uMe(o){o=o|0,fMe(o+24|0)}function fMe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function AMe(o){o=o|0;var l=0;l=en()|0,tn(o,2,5,l,pMe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function pMe(){return 1164}function hMe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;A=I,I=I+16|0,d=A+8|0,m=A,B=gMe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],dMe(l,d,u),I=A}function gMe(o){return o=o|0,(n[(EM()|0)+24>>2]|0)+(o*12|0)|0}function dMe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0;m=I,I=I+16|0,d=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),Uh(d,u),u=Hh(d,u)|0,ap[A&31](o,u),jh(d),I=m}function Uh(o,l){o=o|0,l=l|0,mMe(o,l)}function Hh(o,l){return o=o|0,l=l|0,o|0}function jh(o){o=o|0,Df(o)}function mMe(o,l){o=o|0,l=l|0,IM(o,l)}function IM(o,l){o=o|0,l=l|0,n[o>>2]=l}function yMe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],EMe(o,u,d,0),I=A}function EMe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=CM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=IMe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,CMe(m,A)|0,A),I=d}function CM(){var o=0,l=0;if(s[7664]|0||(tZ(9340),gr(29,9340,U|0)|0,l=7664,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9340)|0)){o=9340,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));tZ(9340)}return 9340}function IMe(o){return o=o|0,0}function CMe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=CM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],eZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(wMe(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function eZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function wMe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=BMe(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,vMe(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],eZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,SMe(o,k),DMe(k),I=_;return}}function BMe(o){return o=o|0,357913941}function vMe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function SMe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function DMe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function tZ(o){o=o|0,xMe(o)}function bMe(o){o=o|0,PMe(o+24|0)}function PMe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function xMe(o){o=o|0;var l=0;l=en()|0,tn(o,2,4,l,kMe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function kMe(){return 1180}function QMe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=TMe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],u=RMe(l,d,u)|0,I=A,u|0}function TMe(o){return o=o|0,(n[(CM()|0)+24>>2]|0)+(o*12|0)|0}function RMe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0;return m=I,I=I+16|0,d=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),od(d,u),d=ad(d,u)|0,d=UP(m_[A&15](o,d)|0)|0,I=m,d|0}function od(o,l){o=o|0,l=l|0}function ad(o,l){return o=o|0,l=l|0,FMe(l)|0}function UP(o){return o=o|0,o|0}function FMe(o){return o=o|0,o|0}function NMe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],OMe(o,u,d,0),I=A}function OMe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=wM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=LMe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,MMe(m,A)|0,A),I=d}function wM(){var o=0,l=0;if(s[7672]|0||(nZ(9376),gr(30,9376,U|0)|0,l=7672,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9376)|0)){o=9376,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));nZ(9376)}return 9376}function LMe(o){return o=o|0,0}function MMe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=wM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],rZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(_Me(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function rZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function _Me(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=UMe(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,HMe(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],rZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,jMe(o,k),qMe(k),I=_;return}}function UMe(o){return o=o|0,357913941}function HMe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function jMe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function qMe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function nZ(o){o=o|0,YMe(o)}function GMe(o){o=o|0,WMe(o+24|0)}function WMe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function YMe(o){o=o|0;var l=0;l=en()|0,tn(o,2,5,l,iZ()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function iZ(){return 1196}function VMe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0;return u=I,I=I+16|0,A=u+8|0,d=u,m=KMe(o)|0,o=n[m+4>>2]|0,n[d>>2]=n[m>>2],n[d+4>>2]=o,n[A>>2]=n[d>>2],n[A+4>>2]=n[d+4>>2],l=JMe(l,A)|0,I=u,l|0}function KMe(o){return o=o|0,(n[(wM()|0)+24>>2]|0)+(o*12|0)|0}function JMe(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),UP(hd[u&31](o)|0)|0}function zMe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],ZMe(o,u,d,1),I=A}function ZMe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=BM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=XMe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,$Me(m,A)|0,A),I=d}function BM(){var o=0,l=0;if(s[7680]|0||(oZ(9412),gr(31,9412,U|0)|0,l=7680,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9412)|0)){o=9412,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));oZ(9412)}return 9412}function XMe(o){return o=o|0,0}function $Me(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=BM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],sZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(e_e(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function sZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function e_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=t_e(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,r_e(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],sZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,n_e(o,k),i_e(k),I=_;return}}function t_e(o){return o=o|0,357913941}function r_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function n_e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function i_e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function oZ(o){o=o|0,a_e(o)}function s_e(o){o=o|0,o_e(o+24|0)}function o_e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function a_e(o){o=o|0;var l=0;l=en()|0,tn(o,2,6,l,aZ()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function aZ(){return 1200}function l_e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0;return u=I,I=I+16|0,A=u+8|0,d=u,m=c_e(o)|0,o=n[m+4>>2]|0,n[d>>2]=n[m>>2],n[d+4>>2]=o,n[A>>2]=n[d>>2],n[A+4>>2]=n[d+4>>2],l=u_e(l,A)|0,I=u,l|0}function c_e(o){return o=o|0,(n[(BM()|0)+24>>2]|0)+(o*12|0)|0}function u_e(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),HP(hd[u&31](o)|0)|0}function HP(o){return o=o|0,o|0}function f_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],A_e(o,u,d,0),I=A}function A_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=vM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=p_e(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,h_e(m,A)|0,A),I=d}function vM(){var o=0,l=0;if(s[7688]|0||(cZ(9448),gr(32,9448,U|0)|0,l=7688,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9448)|0)){o=9448,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));cZ(9448)}return 9448}function p_e(o){return o=o|0,0}function h_e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=vM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],lZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(g_e(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function lZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function g_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=d_e(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,m_e(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],lZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,y_e(o,k),E_e(k),I=_;return}}function d_e(o){return o=o|0,357913941}function m_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function y_e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function E_e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function cZ(o){o=o|0,w_e(o)}function I_e(o){o=o|0,C_e(o+24|0)}function C_e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function w_e(o){o=o|0;var l=0;l=en()|0,tn(o,2,6,l,uZ()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function uZ(){return 1204}function B_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;A=I,I=I+16|0,d=A+8|0,m=A,B=v_e(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],S_e(l,d,u),I=A}function v_e(o){return o=o|0,(n[(vM()|0)+24>>2]|0)+(o*12|0)|0}function S_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0;m=I,I=I+16|0,d=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),SM(d,u),d=DM(d,u)|0,ap[A&31](o,d),I=m}function SM(o,l){o=o|0,l=l|0}function DM(o,l){return o=o|0,l=l|0,D_e(l)|0}function D_e(o){return o=o|0,o|0}function b_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],P_e(o,u,d,0),I=A}function P_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=bM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=x_e(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,k_e(m,A)|0,A),I=d}function bM(){var o=0,l=0;if(s[7696]|0||(AZ(9484),gr(33,9484,U|0)|0,l=7696,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9484)|0)){o=9484,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));AZ(9484)}return 9484}function x_e(o){return o=o|0,0}function k_e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=bM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],fZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(Q_e(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function fZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function Q_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=T_e(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,R_e(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],fZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,F_e(o,k),N_e(k),I=_;return}}function T_e(o){return o=o|0,357913941}function R_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function F_e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function N_e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function AZ(o){o=o|0,M_e(o)}function O_e(o){o=o|0,L_e(o+24|0)}function L_e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function M_e(o){o=o|0;var l=0;l=en()|0,tn(o,2,1,l,__e()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function __e(){return 1212}function U_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0;d=I,I=I+16|0,m=d+8|0,B=d,k=H_e(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],j_e(l,m,u,A),I=d}function H_e(o){return o=o|0,(n[(bM()|0)+24>>2]|0)+(o*12|0)|0}function j_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0;k=I,I=I+16|0,m=k+1|0,B=k,d=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(d=n[(n[o>>2]|0)+d>>2]|0),SM(m,u),m=DM(m,u)|0,od(B,A),B=ad(B,A)|0,F2[d&15](o,m,B),I=k}function q_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],G_e(o,u,d,1),I=A}function G_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=PM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=W_e(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,Y_e(m,A)|0,A),I=d}function PM(){var o=0,l=0;if(s[7704]|0||(hZ(9520),gr(34,9520,U|0)|0,l=7704,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9520)|0)){o=9520,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));hZ(9520)}return 9520}function W_e(o){return o=o|0,0}function Y_e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=PM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],pZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(V_e(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function pZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function V_e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=K_e(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,J_e(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],pZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,z_e(o,k),Z_e(k),I=_;return}}function K_e(o){return o=o|0,357913941}function J_e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function z_e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Z_e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function hZ(o){o=o|0,eUe(o)}function X_e(o){o=o|0,$_e(o+24|0)}function $_e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function eUe(o){o=o|0;var l=0;l=en()|0,tn(o,2,1,l,tUe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function tUe(){return 1224}function rUe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;return d=I,I=I+16|0,m=d+8|0,B=d,k=nUe(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],A=+iUe(l,m,u),I=d,+A}function nUe(o){return o=o|0,(n[(PM()|0)+24>>2]|0)+(o*12|0)|0}function iUe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return m=I,I=I+16|0,d=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(d,u),d=ip(d,u)|0,B=+Qf(+f$[A&7](o,d)),I=m,+B}function sUe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],oUe(o,u,d,1),I=A}function oUe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=xM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=aUe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,lUe(m,A)|0,A),I=d}function xM(){var o=0,l=0;if(s[7712]|0||(dZ(9556),gr(35,9556,U|0)|0,l=7712,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9556)|0)){o=9556,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));dZ(9556)}return 9556}function aUe(o){return o=o|0,0}function lUe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=xM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],gZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(cUe(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function gZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function cUe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=uUe(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,fUe(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],gZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,AUe(o,k),pUe(k),I=_;return}}function uUe(o){return o=o|0,357913941}function fUe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function AUe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function pUe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function dZ(o){o=o|0,dUe(o)}function hUe(o){o=o|0,gUe(o+24|0)}function gUe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function dUe(o){o=o|0;var l=0;l=en()|0,tn(o,2,5,l,mUe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function mUe(){return 1232}function yUe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=EUe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],u=+IUe(l,d),I=A,+u}function EUe(o){return o=o|0,(n[(xM()|0)+24>>2]|0)+(o*12|0)|0}function IUe(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),+ +Qf(+u$[u&15](o))}function CUe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],wUe(o,u,d,1),I=A}function wUe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=kM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=BUe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,vUe(m,A)|0,A),I=d}function kM(){var o=0,l=0;if(s[7720]|0||(yZ(9592),gr(36,9592,U|0)|0,l=7720,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9592)|0)){o=9592,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));yZ(9592)}return 9592}function BUe(o){return o=o|0,0}function vUe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=kM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],mZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(SUe(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function mZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function SUe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=DUe(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,bUe(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],mZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,PUe(o,k),xUe(k),I=_;return}}function DUe(o){return o=o|0,357913941}function bUe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function PUe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function xUe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function yZ(o){o=o|0,TUe(o)}function kUe(o){o=o|0,QUe(o+24|0)}function QUe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function TUe(o){o=o|0;var l=0;l=en()|0,tn(o,2,7,l,RUe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function RUe(){return 1276}function FUe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0;return u=I,I=I+16|0,A=u+8|0,d=u,m=NUe(o)|0,o=n[m+4>>2]|0,n[d>>2]=n[m>>2],n[d+4>>2]=o,n[A>>2]=n[d>>2],n[A+4>>2]=n[d+4>>2],l=OUe(l,A)|0,I=u,l|0}function NUe(o){return o=o|0,(n[(kM()|0)+24>>2]|0)+(o*12|0)|0}function OUe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0;return d=I,I=I+16|0,A=d,u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),ap[u&31](A,o),A=EZ(A)|0,I=d,A|0}function EZ(o){o=o|0;var l=0,u=0,A=0,d=0;return d=I,I=I+32|0,l=d+12|0,u=d,A=pM(IZ()|0)|0,A?(hM(l,A),gM(u,l),LUe(o,u),o=dM(l)|0):o=MUe(o)|0,I=d,o|0}function IZ(){var o=0;return s[7736]|0||(KUe(9640),gr(25,9640,U|0)|0,o=7736,n[o>>2]=1,n[o+4>>2]=0),9640}function LUe(o,l){o=o|0,l=l|0,jUe(l,o,o+8|0)|0}function MUe(o){o=o|0;var l=0,u=0,A=0,d=0,m=0,B=0,k=0;return u=I,I=I+16|0,d=u+4|0,B=u,A=Fl(8)|0,l=A,k=Jt(16)|0,n[k>>2]=n[o>>2],n[k+4>>2]=n[o+4>>2],n[k+8>>2]=n[o+8>>2],n[k+12>>2]=n[o+12>>2],m=l+4|0,n[m>>2]=k,o=Jt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[d>>2]=n[B>>2],QM(o,m,d),n[A>>2]=o,I=u,l|0}function QM(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Jt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1244,n[u+12>>2]=l,n[o+4>>2]=u}function _Ue(o){o=o|0,Zy(o),Et(o)}function UUe(o){o=o|0,o=n[o+12>>2]|0,o|0&&Et(o)}function HUe(o){o=o|0,Et(o)}function jUe(o,l,u){return o=o|0,l=l|0,u=u|0,l=qUe(n[o>>2]|0,l,u)|0,u=o+4|0,n[(n[u>>2]|0)+8>>2]=l,n[(n[u>>2]|0)+8>>2]|0}function qUe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0;return A=I,I=I+16|0,d=A,Nl(d),o=Ms(o)|0,u=GUe(o,n[l>>2]|0,+E[u>>3])|0,Ol(d),I=A,u|0}function GUe(o,l,u){o=o|0,l=l|0,u=+u;var A=0;return A=ma(WUe()|0)|0,l=Gy(l)|0,lu(0,A|0,o|0,l|0,+ +Ja(u))|0}function WUe(){var o=0;return s[7728]|0||(YUe(9628),o=7728,n[o>>2]=1,n[o+4>>2]=0),9628}function YUe(o){o=o|0,Ro(o,VUe()|0,2)}function VUe(){return 1264}function KUe(o){o=o|0,_h(o)}function JUe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],zUe(o,u,d,1),I=A}function zUe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=TM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=ZUe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,XUe(m,A)|0,A),I=d}function TM(){var o=0,l=0;if(s[7744]|0||(wZ(9684),gr(37,9684,U|0)|0,l=7744,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9684)|0)){o=9684,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));wZ(9684)}return 9684}function ZUe(o){return o=o|0,0}function XUe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=TM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],CZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):($Ue(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function CZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function $Ue(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=e4e(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,t4e(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],CZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,r4e(o,k),n4e(k),I=_;return}}function e4e(o){return o=o|0,357913941}function t4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function r4e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function n4e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function wZ(o){o=o|0,o4e(o)}function i4e(o){o=o|0,s4e(o+24|0)}function s4e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function o4e(o){o=o|0;var l=0;l=en()|0,tn(o,2,5,l,a4e()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function a4e(){return 1280}function l4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=c4e(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],u=u4e(l,d,u)|0,I=A,u|0}function c4e(o){return o=o|0,(n[(TM()|0)+24>>2]|0)+(o*12|0)|0}function u4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return B=I,I=I+32|0,d=B,m=B+16|0,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(m,u),m=ip(m,u)|0,F2[A&15](d,o,m),m=EZ(d)|0,I=B,m|0}function f4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],A4e(o,u,d,1),I=A}function A4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=RM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=p4e(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,h4e(m,A)|0,A),I=d}function RM(){var o=0,l=0;if(s[7752]|0||(vZ(9720),gr(38,9720,U|0)|0,l=7752,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9720)|0)){o=9720,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));vZ(9720)}return 9720}function p4e(o){return o=o|0,0}function h4e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=RM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],BZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(g4e(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function BZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function g4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=d4e(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,m4e(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],BZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,y4e(o,k),E4e(k),I=_;return}}function d4e(o){return o=o|0,357913941}function m4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function y4e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function E4e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function vZ(o){o=o|0,w4e(o)}function I4e(o){o=o|0,C4e(o+24|0)}function C4e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function w4e(o){o=o|0;var l=0;l=en()|0,tn(o,2,8,l,B4e()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function B4e(){return 1288}function v4e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0;return u=I,I=I+16|0,A=u+8|0,d=u,m=S4e(o)|0,o=n[m+4>>2]|0,n[d>>2]=n[m>>2],n[d+4>>2]=o,n[A>>2]=n[d>>2],n[A+4>>2]=n[d+4>>2],l=D4e(l,A)|0,I=u,l|0}function S4e(o){return o=o|0,(n[(RM()|0)+24>>2]|0)+(o*12|0)|0}function D4e(o,l){o=o|0,l=l|0;var u=0;return u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),id(hd[u&31](o)|0)|0}function b4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],P4e(o,u,d,0),I=A}function P4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=FM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=x4e(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,k4e(m,A)|0,A),I=d}function FM(){var o=0,l=0;if(s[7760]|0||(DZ(9756),gr(39,9756,U|0)|0,l=7760,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9756)|0)){o=9756,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));DZ(9756)}return 9756}function x4e(o){return o=o|0,0}function k4e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=FM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],SZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(Q4e(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function SZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function Q4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=T4e(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,R4e(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],SZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,F4e(o,k),N4e(k),I=_;return}}function T4e(o){return o=o|0,357913941}function R4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function F4e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function N4e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function DZ(o){o=o|0,M4e(o)}function O4e(o){o=o|0,L4e(o+24|0)}function L4e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function M4e(o){o=o|0;var l=0;l=en()|0,tn(o,2,8,l,_4e()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function _4e(){return 1292}function U4e(o,l,u){o=o|0,l=l|0,u=+u;var A=0,d=0,m=0,B=0;A=I,I=I+16|0,d=A+8|0,m=A,B=H4e(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],j4e(l,d,u),I=A}function H4e(o){return o=o|0,(n[(FM()|0)+24>>2]|0)+(o*12|0)|0}function j4e(o,l,u){o=o|0,l=l|0,u=+u;var A=0,d=0,m=0;m=I,I=I+16|0,d=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),Tf(d,u),u=+Rf(d,u),a$[A&31](o,u),I=m}function q4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],G4e(o,u,d,0),I=A}function G4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=NM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=W4e(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,Y4e(m,A)|0,A),I=d}function NM(){var o=0,l=0;if(s[7768]|0||(PZ(9792),gr(40,9792,U|0)|0,l=7768,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9792)|0)){o=9792,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));PZ(9792)}return 9792}function W4e(o){return o=o|0,0}function Y4e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=NM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],bZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(V4e(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function bZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function V4e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=K4e(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,J4e(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],bZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,z4e(o,k),Z4e(k),I=_;return}}function K4e(o){return o=o|0,357913941}function J4e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function z4e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Z4e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function PZ(o){o=o|0,e3e(o)}function X4e(o){o=o|0,$4e(o+24|0)}function $4e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function e3e(o){o=o|0;var l=0;l=en()|0,tn(o,2,1,l,t3e()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function t3e(){return 1300}function r3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A;var d=0,m=0,B=0,k=0;d=I,I=I+16|0,m=d+8|0,B=d,k=n3e(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],i3e(l,m,u,A),I=d}function n3e(o){return o=o|0,(n[(NM()|0)+24>>2]|0)+(o*12|0)|0}function i3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A;var d=0,m=0,B=0,k=0;k=I,I=I+16|0,m=k+1|0,B=k,d=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(d=n[(n[o>>2]|0)+d>>2]|0),np(m,u),m=ip(m,u)|0,Tf(B,A),A=+Rf(B,A),g$[d&15](o,m,A),I=k}function s3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],o3e(o,u,d,0),I=A}function o3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=OM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=a3e(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,l3e(m,A)|0,A),I=d}function OM(){var o=0,l=0;if(s[7776]|0||(kZ(9828),gr(41,9828,U|0)|0,l=7776,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9828)|0)){o=9828,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));kZ(9828)}return 9828}function a3e(o){return o=o|0,0}function l3e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=OM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],xZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(c3e(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function xZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function c3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=u3e(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,f3e(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],xZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,A3e(o,k),p3e(k),I=_;return}}function u3e(o){return o=o|0,357913941}function f3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function A3e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function p3e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function kZ(o){o=o|0,d3e(o)}function h3e(o){o=o|0,g3e(o+24|0)}function g3e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function d3e(o){o=o|0;var l=0;l=en()|0,tn(o,2,7,l,m3e()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function m3e(){return 1312}function y3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;A=I,I=I+16|0,d=A+8|0,m=A,B=E3e(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],I3e(l,d,u),I=A}function E3e(o){return o=o|0,(n[(OM()|0)+24>>2]|0)+(o*12|0)|0}function I3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0;m=I,I=I+16|0,d=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(d,u),d=ip(d,u)|0,ap[A&31](o,d),I=m}function C3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],w3e(o,u,d,0),I=A}function w3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=LM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=B3e(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,v3e(m,A)|0,A),I=d}function LM(){var o=0,l=0;if(s[7784]|0||(TZ(9864),gr(42,9864,U|0)|0,l=7784,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9864)|0)){o=9864,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));TZ(9864)}return 9864}function B3e(o){return o=o|0,0}function v3e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=LM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],QZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(S3e(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function QZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function S3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=D3e(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,b3e(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],QZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,P3e(o,k),x3e(k),I=_;return}}function D3e(o){return o=o|0,357913941}function b3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function P3e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function x3e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function TZ(o){o=o|0,T3e(o)}function k3e(o){o=o|0,Q3e(o+24|0)}function Q3e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function T3e(o){o=o|0;var l=0;l=en()|0,tn(o,2,8,l,R3e()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function R3e(){return 1320}function F3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;A=I,I=I+16|0,d=A+8|0,m=A,B=N3e(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],O3e(l,d,u),I=A}function N3e(o){return o=o|0,(n[(LM()|0)+24>>2]|0)+(o*12|0)|0}function O3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0;m=I,I=I+16|0,d=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),L3e(d,u),d=M3e(d,u)|0,ap[A&31](o,d),I=m}function L3e(o,l){o=o|0,l=l|0}function M3e(o,l){return o=o|0,l=l|0,_3e(l)|0}function _3e(o){return o=o|0,o|0}function U3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],H3e(o,u,d,0),I=A}function H3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=MM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=j3e(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,q3e(m,A)|0,A),I=d}function MM(){var o=0,l=0;if(s[7792]|0||(FZ(9900),gr(43,9900,U|0)|0,l=7792,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9900)|0)){o=9900,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));FZ(9900)}return 9900}function j3e(o){return o=o|0,0}function q3e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=MM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],RZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(G3e(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function RZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function G3e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=W3e(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,Y3e(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],RZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,V3e(o,k),K3e(k),I=_;return}}function W3e(o){return o=o|0,357913941}function Y3e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function V3e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function K3e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function FZ(o){o=o|0,Z3e(o)}function J3e(o){o=o|0,z3e(o+24|0)}function z3e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function Z3e(o){o=o|0;var l=0;l=en()|0,tn(o,2,22,l,X3e()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function X3e(){return 1344}function $3e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0;u=I,I=I+16|0,A=u+8|0,d=u,m=e8e(o)|0,o=n[m+4>>2]|0,n[d>>2]=n[m>>2],n[d+4>>2]=o,n[A>>2]=n[d>>2],n[A+4>>2]=n[d+4>>2],t8e(l,A),I=u}function e8e(o){return o=o|0,(n[(MM()|0)+24>>2]|0)+(o*12|0)|0}function t8e(o,l){o=o|0,l=l|0;var u=0;u=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(u=n[(n[o>>2]|0)+u>>2]|0),op[u&127](o)}function r8e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;m=n[o>>2]|0,d=_M()|0,o=n8e(u)|0,vn(m,l,d,o,i8e(u,A)|0,A)}function _M(){var o=0,l=0;if(s[7800]|0||(OZ(9936),gr(44,9936,U|0)|0,l=7800,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9936)|0)){o=9936,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));OZ(9936)}return 9936}function n8e(o){return o=o|0,o|0}function i8e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return k=I,I=I+16|0,d=k,m=k+4|0,n[d>>2]=o,T=_M()|0,B=T+24|0,l=yr(l,4)|0,n[m>>2]=l,u=T+28|0,A=n[u>>2]|0,A>>>0<(n[T+32>>2]|0)>>>0?(NZ(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(s8e(B,d,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function NZ(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function s8e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;if(k=I,I=I+32|0,d=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=o8e(o)|0,A>>>0>>0)sn(o);else{T=n[o>>2]|0,M=(n[o+8>>2]|0)-T|0,_=M>>2,a8e(d,M>>3>>>0>>1>>>0?_>>>0>>0?B:_:A,(n[m>>2]|0)-T>>3,o+8|0),B=d+8|0,NZ(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,l8e(o,d),c8e(d),I=k;return}}function o8e(o){return o=o|0,536870911}function a8e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{d=Jt(l<<3)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<3)}function l8e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>3)<<3)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function c8e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&Et(o)}function OZ(o){o=o|0,A8e(o)}function u8e(o){o=o|0,f8e(o+24|0)}function f8e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function A8e(o){o=o|0;var l=0;l=en()|0,tn(o,1,23,l,uZ()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function p8e(o,l){o=o|0,l=l|0,g8e(n[(h8e(o)|0)>>2]|0,l)}function h8e(o){return o=o|0,(n[(_M()|0)+24>>2]|0)+(o<<3)|0}function g8e(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,SM(A,l),l=DM(A,l)|0,op[o&127](l),I=u}function d8e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;m=n[o>>2]|0,d=UM()|0,o=m8e(u)|0,vn(m,l,d,o,y8e(u,A)|0,A)}function UM(){var o=0,l=0;if(s[7808]|0||(MZ(9972),gr(45,9972,U|0)|0,l=7808,n[l>>2]=1,n[l+4>>2]=0),!(Ur(9972)|0)){o=9972,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));MZ(9972)}return 9972}function m8e(o){return o=o|0,o|0}function y8e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return k=I,I=I+16|0,d=k,m=k+4|0,n[d>>2]=o,T=UM()|0,B=T+24|0,l=yr(l,4)|0,n[m>>2]=l,u=T+28|0,A=n[u>>2]|0,A>>>0<(n[T+32>>2]|0)>>>0?(LZ(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(E8e(B,d,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function LZ(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function E8e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;if(k=I,I=I+32|0,d=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=I8e(o)|0,A>>>0>>0)sn(o);else{T=n[o>>2]|0,M=(n[o+8>>2]|0)-T|0,_=M>>2,C8e(d,M>>3>>>0>>1>>>0?_>>>0>>0?B:_:A,(n[m>>2]|0)-T>>3,o+8|0),B=d+8|0,LZ(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,w8e(o,d),B8e(d),I=k;return}}function I8e(o){return o=o|0,536870911}function C8e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{d=Jt(l<<3)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<3)}function w8e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>3)<<3)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function B8e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&Et(o)}function MZ(o){o=o|0,D8e(o)}function v8e(o){o=o|0,S8e(o+24|0)}function S8e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function D8e(o){o=o|0;var l=0;l=en()|0,tn(o,1,9,l,b8e()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function b8e(){return 1348}function P8e(o,l){return o=o|0,l=l|0,k8e(n[(x8e(o)|0)>>2]|0,l)|0}function x8e(o){return o=o|0,(n[(UM()|0)+24>>2]|0)+(o<<3)|0}function k8e(o,l){o=o|0,l=l|0;var u=0,A=0;return u=I,I=I+16|0,A=u,_Z(A,l),l=UZ(A,l)|0,l=UP(hd[o&31](l)|0)|0,I=u,l|0}function _Z(o,l){o=o|0,l=l|0}function UZ(o,l){return o=o|0,l=l|0,Q8e(l)|0}function Q8e(o){return o=o|0,o|0}function T8e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;m=n[o>>2]|0,d=HM()|0,o=R8e(u)|0,vn(m,l,d,o,F8e(u,A)|0,A)}function HM(){var o=0,l=0;if(s[7816]|0||(jZ(10008),gr(46,10008,U|0)|0,l=7816,n[l>>2]=1,n[l+4>>2]=0),!(Ur(10008)|0)){o=10008,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));jZ(10008)}return 10008}function R8e(o){return o=o|0,o|0}function F8e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return k=I,I=I+16|0,d=k,m=k+4|0,n[d>>2]=o,T=HM()|0,B=T+24|0,l=yr(l,4)|0,n[m>>2]=l,u=T+28|0,A=n[u>>2]|0,A>>>0<(n[T+32>>2]|0)>>>0?(HZ(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(N8e(B,d,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function HZ(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function N8e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;if(k=I,I=I+32|0,d=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=O8e(o)|0,A>>>0>>0)sn(o);else{T=n[o>>2]|0,M=(n[o+8>>2]|0)-T|0,_=M>>2,L8e(d,M>>3>>>0>>1>>>0?_>>>0>>0?B:_:A,(n[m>>2]|0)-T>>3,o+8|0),B=d+8|0,HZ(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,M8e(o,d),_8e(d),I=k;return}}function O8e(o){return o=o|0,536870911}function L8e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{d=Jt(l<<3)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<3)}function M8e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>3)<<3)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function _8e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&Et(o)}function jZ(o){o=o|0,j8e(o)}function U8e(o){o=o|0,H8e(o+24|0)}function H8e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function j8e(o){o=o|0;var l=0;l=en()|0,tn(o,1,15,l,iZ()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function q8e(o){return o=o|0,W8e(n[(G8e(o)|0)>>2]|0)|0}function G8e(o){return o=o|0,(n[(HM()|0)+24>>2]|0)+(o<<3)|0}function W8e(o){return o=o|0,UP(tx[o&7]()|0)|0}function Y8e(){var o=0;return s[7832]|0||(eHe(10052),gr(25,10052,U|0)|0,o=7832,n[o>>2]=1,n[o+4>>2]=0),10052}function V8e(o,l){o=o|0,l=l|0,n[o>>2]=K8e()|0,n[o+4>>2]=J8e()|0,n[o+12>>2]=l,n[o+8>>2]=z8e()|0,n[o+32>>2]=2}function K8e(){return 11709}function J8e(){return 1188}function z8e(){return jP()|0}function Z8e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(qh(A,896)|0)==512?u|0&&(X8e(u),Et(u)):l|0&&(Fy(l),Et(l))}function qh(o,l){return o=o|0,l=l|0,l&o|0}function X8e(o){o=o|0,o=n[o+4>>2]|0,o|0&&Wh(o)}function jP(){var o=0;return s[7824]|0||(n[2511]=$8e()|0,n[2512]=0,o=7824,n[o>>2]=1,n[o+4>>2]=0),10044}function $8e(){return 0}function eHe(o){o=o|0,_h(o)}function tHe(o){o=o|0;var l=0,u=0,A=0,d=0,m=0;l=I,I=I+32|0,u=l+24|0,m=l+16|0,d=l+8|0,A=l,rHe(o,4827),nHe(o,4834,3)|0,iHe(o,3682,47)|0,n[m>>2]=9,n[m+4>>2]=0,n[u>>2]=n[m>>2],n[u+4>>2]=n[m+4>>2],sHe(o,4841,u)|0,n[d>>2]=1,n[d+4>>2]=0,n[u>>2]=n[d>>2],n[u+4>>2]=n[d+4>>2],oHe(o,4871,u)|0,n[A>>2]=10,n[A+4>>2]=0,n[u>>2]=n[A>>2],n[u+4>>2]=n[A+4>>2],aHe(o,4891,u)|0,I=l}function rHe(o,l){o=o|0,l=l|0;var u=0;u=_je()|0,n[o>>2]=u,Uje(u,l),Gh(n[o>>2]|0)}function nHe(o,l,u){return o=o|0,l=l|0,u=u|0,Bje(o,Bn(l)|0,u,0),o|0}function iHe(o,l,u){return o=o|0,l=l|0,u=u|0,lje(o,Bn(l)|0,u,0),o|0}function sHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],qHe(o,l,d),I=A,o|0}function oHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],vHe(o,l,d),I=A,o|0}function aHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=n[u+4>>2]|0,n[m>>2]=n[u>>2],n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],lHe(o,l,d),I=A,o|0}function lHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],cHe(o,u,d,1),I=A}function cHe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=jM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=uHe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,fHe(m,A)|0,A),I=d}function jM(){var o=0,l=0;if(s[7840]|0||(GZ(10100),gr(48,10100,U|0)|0,l=7840,n[l>>2]=1,n[l+4>>2]=0),!(Ur(10100)|0)){o=10100,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));GZ(10100)}return 10100}function uHe(o){return o=o|0,0}function fHe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=jM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],qZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(AHe(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function qZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function AHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=pHe(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,hHe(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],qZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,gHe(o,k),dHe(k),I=_;return}}function pHe(o){return o=o|0,357913941}function hHe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function gHe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function dHe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function GZ(o){o=o|0,EHe(o)}function mHe(o){o=o|0,yHe(o+24|0)}function yHe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function EHe(o){o=o|0;var l=0;l=en()|0,tn(o,2,6,l,IHe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function IHe(){return 1364}function CHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;return A=I,I=I+16|0,d=A+8|0,m=A,B=wHe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],u=BHe(l,d,u)|0,I=A,u|0}function wHe(o){return o=o|0,(n[(jM()|0)+24>>2]|0)+(o*12|0)|0}function BHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0;return m=I,I=I+16|0,d=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),np(d,u),d=ip(d,u)|0,d=Zz(m_[A&15](o,d)|0)|0,I=m,d|0}function vHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],SHe(o,u,d,0),I=A}function SHe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=qM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=DHe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,bHe(m,A)|0,A),I=d}function qM(){var o=0,l=0;if(s[7848]|0||(YZ(10136),gr(49,10136,U|0)|0,l=7848,n[l>>2]=1,n[l+4>>2]=0),!(Ur(10136)|0)){o=10136,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));YZ(10136)}return 10136}function DHe(o){return o=o|0,0}function bHe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=qM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],WZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(PHe(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function WZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function PHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=xHe(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,kHe(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],WZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,QHe(o,k),THe(k),I=_;return}}function xHe(o){return o=o|0,357913941}function kHe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function QHe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function THe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function YZ(o){o=o|0,NHe(o)}function RHe(o){o=o|0,FHe(o+24|0)}function FHe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function NHe(o){o=o|0;var l=0;l=en()|0,tn(o,2,9,l,OHe()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function OHe(){return 1372}function LHe(o,l,u){o=o|0,l=l|0,u=+u;var A=0,d=0,m=0,B=0;A=I,I=I+16|0,d=A+8|0,m=A,B=MHe(o)|0,o=n[B+4>>2]|0,n[m>>2]=n[B>>2],n[m+4>>2]=o,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],_He(l,d,u),I=A}function MHe(o){return o=o|0,(n[(qM()|0)+24>>2]|0)+(o*12|0)|0}function _He(o,l,u){o=o|0,l=l|0,u=+u;var A=0,d=0,m=0,B=Xe;m=I,I=I+16|0,d=m,A=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(A=n[(n[o>>2]|0)+A>>2]|0),UHe(d,u),B=y(HHe(d,u)),o$[A&1](o,B),I=m}function UHe(o,l){o=o|0,l=+l}function HHe(o,l){return o=o|0,l=+l,y(jHe(l))}function jHe(o){return o=+o,y(o)}function qHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,d=A+8|0,m=A,k=n[u>>2]|0,B=n[u+4>>2]|0,u=Bn(l)|0,n[m>>2]=k,n[m+4>>2]=B,n[d>>2]=n[m>>2],n[d+4>>2]=n[m+4>>2],GHe(o,u,d,0),I=A}function GHe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0,T=0,_=0,M=0;d=I,I=I+32|0,m=d+16|0,M=d+8|0,k=d,_=n[u>>2]|0,T=n[u+4>>2]|0,B=n[o>>2]|0,o=GM()|0,n[M>>2]=_,n[M+4>>2]=T,n[m>>2]=n[M>>2],n[m+4>>2]=n[M+4>>2],u=WHe(m)|0,n[k>>2]=_,n[k+4>>2]=T,n[m>>2]=n[k>>2],n[m+4>>2]=n[k+4>>2],vn(B,l,o,u,YHe(m,A)|0,A),I=d}function GM(){var o=0,l=0;if(s[7856]|0||(KZ(10172),gr(50,10172,U|0)|0,l=7856,n[l>>2]=1,n[l+4>>2]=0),!(Ur(10172)|0)){o=10172,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));KZ(10172)}return 10172}function WHe(o){return o=o|0,0}function YHe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0;return M=I,I=I+32|0,d=M+24|0,B=M+16|0,k=M,T=M+8|0,m=n[o>>2]|0,A=n[o+4>>2]|0,n[k>>2]=m,n[k+4>>2]=A,G=GM()|0,_=G+24|0,o=yr(l,4)|0,n[T>>2]=o,l=G+28|0,u=n[l>>2]|0,u>>>0<(n[G+32>>2]|0)>>>0?(n[B>>2]=m,n[B+4>>2]=A,n[d>>2]=n[B>>2],n[d+4>>2]=n[B+4>>2],VZ(u,d,o),o=(n[l>>2]|0)+12|0,n[l>>2]=o):(VHe(_,k,T),o=n[l>>2]|0),I=M,((o-(n[_>>2]|0)|0)/12|0)+-1|0}function VZ(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=n[l+4>>2]|0,n[o>>2]=n[l>>2],n[o+4>>2]=A,n[o+8>>2]=u}function VHe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;if(_=I,I=I+48|0,A=_+32|0,B=_+24|0,k=_,T=o+4|0,d=(((n[T>>2]|0)-(n[o>>2]|0)|0)/12|0)+1|0,m=KHe(o)|0,m>>>0>>0)sn(o);else{M=n[o>>2]|0,ae=((n[o+8>>2]|0)-M|0)/12|0,G=ae<<1,JHe(k,ae>>>0>>1>>>0?G>>>0>>0?d:G:m,((n[T>>2]|0)-M|0)/12|0,o+8|0),T=k+8|0,m=n[T>>2]|0,d=n[l+4>>2]|0,u=n[u>>2]|0,n[B>>2]=n[l>>2],n[B+4>>2]=d,n[A>>2]=n[B>>2],n[A+4>>2]=n[B+4>>2],VZ(m,A,u),n[T>>2]=(n[T>>2]|0)+12,zHe(o,k),ZHe(k),I=_;return}}function KHe(o){return o=o|0,357913941}function JHe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>357913941)Nt();else{d=Jt(l*12|0)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u*12|0)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l*12|0)}function zHe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(((d|0)/-12|0)*12|0)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function ZHe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~(((A+-12-l|0)>>>0)/12|0)*12|0)),o=n[o>>2]|0,o|0&&Et(o)}function KZ(o){o=o|0,eje(o)}function XHe(o){o=o|0,$He(o+24|0)}function $He(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~(((l+-12-A|0)>>>0)/12|0)*12|0)),Et(u))}function eje(o){o=o|0;var l=0;l=en()|0,tn(o,2,3,l,tje()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function tje(){return 1380}function rje(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0;d=I,I=I+16|0,m=d+8|0,B=d,k=nje(o)|0,o=n[k+4>>2]|0,n[B>>2]=n[k>>2],n[B+4>>2]=o,n[m>>2]=n[B>>2],n[m+4>>2]=n[B+4>>2],ije(l,m,u,A),I=d}function nje(o){return o=o|0,(n[(GM()|0)+24>>2]|0)+(o*12|0)|0}function ije(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0;k=I,I=I+16|0,m=k+1|0,B=k,d=n[l>>2]|0,l=n[l+4>>2]|0,o=o+(l>>1)|0,l&1&&(d=n[(n[o>>2]|0)+d>>2]|0),np(m,u),m=ip(m,u)|0,sje(B,A),B=oje(B,A)|0,F2[d&15](o,m,B),I=k}function sje(o,l){o=o|0,l=l|0}function oje(o,l){return o=o|0,l=l|0,aje(l)|0}function aje(o){return o=o|0,(o|0)!=0|0}function lje(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;m=n[o>>2]|0,d=WM()|0,o=cje(u)|0,vn(m,l,d,o,uje(u,A)|0,A)}function WM(){var o=0,l=0;if(s[7864]|0||(zZ(10208),gr(51,10208,U|0)|0,l=7864,n[l>>2]=1,n[l+4>>2]=0),!(Ur(10208)|0)){o=10208,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));zZ(10208)}return 10208}function cje(o){return o=o|0,o|0}function uje(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return k=I,I=I+16|0,d=k,m=k+4|0,n[d>>2]=o,T=WM()|0,B=T+24|0,l=yr(l,4)|0,n[m>>2]=l,u=T+28|0,A=n[u>>2]|0,A>>>0<(n[T+32>>2]|0)>>>0?(JZ(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(fje(B,d,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function JZ(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function fje(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;if(k=I,I=I+32|0,d=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=Aje(o)|0,A>>>0>>0)sn(o);else{T=n[o>>2]|0,M=(n[o+8>>2]|0)-T|0,_=M>>2,pje(d,M>>3>>>0>>1>>>0?_>>>0>>0?B:_:A,(n[m>>2]|0)-T>>3,o+8|0),B=d+8|0,JZ(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,hje(o,d),gje(d),I=k;return}}function Aje(o){return o=o|0,536870911}function pje(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{d=Jt(l<<3)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<3)}function hje(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>3)<<3)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function gje(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&Et(o)}function zZ(o){o=o|0,yje(o)}function dje(o){o=o|0,mje(o+24|0)}function mje(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function yje(o){o=o|0;var l=0;l=en()|0,tn(o,1,24,l,Eje()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function Eje(){return 1392}function Ije(o,l){o=o|0,l=l|0,wje(n[(Cje(o)|0)>>2]|0,l)}function Cje(o){return o=o|0,(n[(WM()|0)+24>>2]|0)+(o<<3)|0}function wje(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,_Z(A,l),l=UZ(A,l)|0,op[o&127](l),I=u}function Bje(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;m=n[o>>2]|0,d=YM()|0,o=vje(u)|0,vn(m,l,d,o,Sje(u,A)|0,A)}function YM(){var o=0,l=0;if(s[7872]|0||(XZ(10244),gr(52,10244,U|0)|0,l=7872,n[l>>2]=1,n[l+4>>2]=0),!(Ur(10244)|0)){o=10244,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));XZ(10244)}return 10244}function vje(o){return o=o|0,o|0}function Sje(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return k=I,I=I+16|0,d=k,m=k+4|0,n[d>>2]=o,T=YM()|0,B=T+24|0,l=yr(l,4)|0,n[m>>2]=l,u=T+28|0,A=n[u>>2]|0,A>>>0<(n[T+32>>2]|0)>>>0?(ZZ(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(Dje(B,d,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function ZZ(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function Dje(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;if(k=I,I=I+32|0,d=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=bje(o)|0,A>>>0>>0)sn(o);else{T=n[o>>2]|0,M=(n[o+8>>2]|0)-T|0,_=M>>2,Pje(d,M>>3>>>0>>1>>>0?_>>>0>>0?B:_:A,(n[m>>2]|0)-T>>3,o+8|0),B=d+8|0,ZZ(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,xje(o,d),kje(d),I=k;return}}function bje(o){return o=o|0,536870911}function Pje(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{d=Jt(l<<3)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<3)}function xje(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>3)<<3)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function kje(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&Et(o)}function XZ(o){o=o|0,Rje(o)}function Qje(o){o=o|0,Tje(o+24|0)}function Tje(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function Rje(o){o=o|0;var l=0;l=en()|0,tn(o,1,16,l,Fje()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function Fje(){return 1400}function Nje(o){return o=o|0,Lje(n[(Oje(o)|0)>>2]|0)|0}function Oje(o){return o=o|0,(n[(YM()|0)+24>>2]|0)+(o<<3)|0}function Lje(o){return o=o|0,Mje(tx[o&7]()|0)|0}function Mje(o){return o=o|0,o|0}function _je(){var o=0;return s[7880]|0||(Yje(10280),gr(25,10280,U|0)|0,o=7880,n[o>>2]=1,n[o+4>>2]=0),10280}function Uje(o,l){o=o|0,l=l|0,n[o>>2]=Hje()|0,n[o+4>>2]=jje()|0,n[o+12>>2]=l,n[o+8>>2]=qje()|0,n[o+32>>2]=4}function Hje(){return 11711}function jje(){return 1356}function qje(){return jP()|0}function Gje(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(qh(A,896)|0)==512?u|0&&(Wje(u),Et(u)):l|0&&(Kg(l),Et(l))}function Wje(o){o=o|0,o=n[o+4>>2]|0,o|0&&Wh(o)}function Yje(o){o=o|0,_h(o)}function Vje(o){o=o|0,Kje(o,4920),Jje(o)|0,zje(o)|0}function Kje(o,l){o=o|0,l=l|0;var u=0;u=IZ()|0,n[o>>2]=u,m6e(u,l),Gh(n[o>>2]|0)}function Jje(o){o=o|0;var l=0;return l=n[o>>2]|0,ld(l,a6e()|0),o|0}function zje(o){o=o|0;var l=0;return l=n[o>>2]|0,ld(l,Zje()|0),o|0}function Zje(){var o=0;return s[7888]|0||($Z(10328),gr(53,10328,U|0)|0,o=7888,n[o>>2]=1,n[o+4>>2]=0),Ur(10328)|0||$Z(10328),10328}function ld(o,l){o=o|0,l=l|0,vn(o,0,l,0,0,0)}function $Z(o){o=o|0,e6e(o),cd(o,10)}function Xje(o){o=o|0,$je(o+24|0)}function $je(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function e6e(o){o=o|0;var l=0;l=en()|0,tn(o,5,1,l,i6e()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function t6e(o,l,u){o=o|0,l=l|0,u=+u,r6e(o,l,u)}function cd(o,l){o=o|0,l=l|0,n[o+20>>2]=l}function r6e(o,l,u){o=o|0,l=l|0,u=+u;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+16|0,m=A+8|0,k=A+13|0,d=A,B=A+12|0,np(k,l),n[m>>2]=ip(k,l)|0,Tf(B,u),E[d>>3]=+Rf(B,u),n6e(o,m,d),I=A}function n6e(o,l,u){o=o|0,l=l|0,u=u|0,Rl(o+8|0,n[l>>2]|0,+E[u>>3]),s[o+24>>0]=1}function i6e(){return 1404}function s6e(o,l){return o=o|0,l=+l,o6e(o,l)|0}function o6e(o,l){o=o|0,l=+l;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return A=I,I=I+16|0,m=A+4|0,B=A+8|0,k=A,d=Fl(8)|0,u=d,T=Jt(16)|0,np(m,o),o=ip(m,o)|0,Tf(B,l),Rl(T,o,+Rf(B,l)),B=u+4|0,n[B>>2]=T,o=Jt(8)|0,B=n[B>>2]|0,n[k>>2]=0,n[m>>2]=n[k>>2],QM(o,B,m),n[d>>2]=o,I=A,u|0}function a6e(){var o=0;return s[7896]|0||(eX(10364),gr(54,10364,U|0)|0,o=7896,n[o>>2]=1,n[o+4>>2]=0),Ur(10364)|0||eX(10364),10364}function eX(o){o=o|0,u6e(o),cd(o,55)}function l6e(o){o=o|0,c6e(o+24|0)}function c6e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function u6e(o){o=o|0;var l=0;l=en()|0,tn(o,5,4,l,h6e()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function f6e(o){o=o|0,A6e(o)}function A6e(o){o=o|0,p6e(o)}function p6e(o){o=o|0,tX(o+8|0),s[o+24>>0]=1}function tX(o){o=o|0,n[o>>2]=0,E[o+8>>3]=0}function h6e(){return 1424}function g6e(){return d6e()|0}function d6e(){var o=0,l=0,u=0,A=0,d=0,m=0,B=0;return l=I,I=I+16|0,d=l+4|0,B=l,u=Fl(8)|0,o=u,A=Jt(16)|0,tX(A),m=o+4|0,n[m>>2]=A,A=Jt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[d>>2]=n[B>>2],QM(A,m,d),n[u>>2]=A,I=l,o|0}function m6e(o,l){o=o|0,l=l|0,n[o>>2]=y6e()|0,n[o+4>>2]=E6e()|0,n[o+12>>2]=l,n[o+8>>2]=I6e()|0,n[o+32>>2]=5}function y6e(){return 11710}function E6e(){return 1416}function I6e(){return qP()|0}function C6e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(qh(A,896)|0)==512?u|0&&(w6e(u),Et(u)):l|0&&Et(l)}function w6e(o){o=o|0,o=n[o+4>>2]|0,o|0&&Wh(o)}function qP(){var o=0;return s[7904]|0||(n[2600]=B6e()|0,n[2601]=0,o=7904,n[o>>2]=1,n[o+4>>2]=0),10400}function B6e(){return n[357]|0}function v6e(o){o=o|0,S6e(o,4926),D6e(o)|0}function S6e(o,l){o=o|0,l=l|0;var u=0;u=qz()|0,n[o>>2]=u,L6e(u,l),Gh(n[o>>2]|0)}function D6e(o){o=o|0;var l=0;return l=n[o>>2]|0,ld(l,b6e()|0),o|0}function b6e(){var o=0;return s[7912]|0||(rX(10412),gr(56,10412,U|0)|0,o=7912,n[o>>2]=1,n[o+4>>2]=0),Ur(10412)|0||rX(10412),10412}function rX(o){o=o|0,k6e(o),cd(o,57)}function P6e(o){o=o|0,x6e(o+24|0)}function x6e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function k6e(o){o=o|0;var l=0;l=en()|0,tn(o,5,5,l,F6e()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function Q6e(o){o=o|0,T6e(o)}function T6e(o){o=o|0,R6e(o)}function R6e(o){o=o|0;var l=0,u=0;l=o+8|0,u=l+48|0;do n[l>>2]=0,l=l+4|0;while((l|0)<(u|0));s[o+56>>0]=1}function F6e(){return 1432}function N6e(){return O6e()|0}function O6e(){var o=0,l=0,u=0,A=0,d=0,m=0,B=0,k=0;B=I,I=I+16|0,o=B+4|0,l=B,u=Fl(8)|0,A=u,d=Jt(48)|0,m=d,k=m+48|0;do n[m>>2]=0,m=m+4|0;while((m|0)<(k|0));return m=A+4|0,n[m>>2]=d,k=Jt(8)|0,m=n[m>>2]|0,n[l>>2]=0,n[o>>2]=n[l>>2],Gz(k,m,o),n[u>>2]=k,I=B,A|0}function L6e(o,l){o=o|0,l=l|0,n[o>>2]=M6e()|0,n[o+4>>2]=_6e()|0,n[o+12>>2]=l,n[o+8>>2]=U6e()|0,n[o+32>>2]=6}function M6e(){return 11704}function _6e(){return 1436}function U6e(){return qP()|0}function H6e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(qh(A,896)|0)==512?u|0&&(j6e(u),Et(u)):l|0&&Et(l)}function j6e(o){o=o|0,o=n[o+4>>2]|0,o|0&&Wh(o)}function q6e(o){o=o|0,G6e(o,4933),W6e(o)|0,Y6e(o)|0}function G6e(o,l){o=o|0,l=l|0;var u=0;u=dqe()|0,n[o>>2]=u,mqe(u,l),Gh(n[o>>2]|0)}function W6e(o){o=o|0;var l=0;return l=n[o>>2]|0,ld(l,oqe()|0),o|0}function Y6e(o){o=o|0;var l=0;return l=n[o>>2]|0,ld(l,V6e()|0),o|0}function V6e(){var o=0;return s[7920]|0||(nX(10452),gr(58,10452,U|0)|0,o=7920,n[o>>2]=1,n[o+4>>2]=0),Ur(10452)|0||nX(10452),10452}function nX(o){o=o|0,z6e(o),cd(o,1)}function K6e(o){o=o|0,J6e(o+24|0)}function J6e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function z6e(o){o=o|0;var l=0;l=en()|0,tn(o,5,1,l,eqe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function Z6e(o,l,u){o=o|0,l=+l,u=+u,X6e(o,l,u)}function X6e(o,l,u){o=o|0,l=+l,u=+u;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+32|0,m=A+8|0,k=A+17|0,d=A,B=A+16|0,Tf(k,l),E[m>>3]=+Rf(k,l),Tf(B,u),E[d>>3]=+Rf(B,u),$6e(o,m,d),I=A}function $6e(o,l,u){o=o|0,l=l|0,u=u|0,iX(o+8|0,+E[l>>3],+E[u>>3]),s[o+24>>0]=1}function iX(o,l,u){o=o|0,l=+l,u=+u,E[o>>3]=l,E[o+8>>3]=u}function eqe(){return 1472}function tqe(o,l){return o=+o,l=+l,rqe(o,l)|0}function rqe(o,l){o=+o,l=+l;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return A=I,I=I+16|0,B=A+4|0,k=A+8|0,T=A,d=Fl(8)|0,u=d,m=Jt(16)|0,Tf(B,o),o=+Rf(B,o),Tf(k,l),iX(m,o,+Rf(k,l)),k=u+4|0,n[k>>2]=m,m=Jt(8)|0,k=n[k>>2]|0,n[T>>2]=0,n[B>>2]=n[T>>2],sX(m,k,B),n[d>>2]=m,I=A,u|0}function sX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Jt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1452,n[u+12>>2]=l,n[o+4>>2]=u}function nqe(o){o=o|0,Zy(o),Et(o)}function iqe(o){o=o|0,o=n[o+12>>2]|0,o|0&&Et(o)}function sqe(o){o=o|0,Et(o)}function oqe(){var o=0;return s[7928]|0||(oX(10488),gr(59,10488,U|0)|0,o=7928,n[o>>2]=1,n[o+4>>2]=0),Ur(10488)|0||oX(10488),10488}function oX(o){o=o|0,cqe(o),cd(o,60)}function aqe(o){o=o|0,lqe(o+24|0)}function lqe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function cqe(o){o=o|0;var l=0;l=en()|0,tn(o,5,6,l,pqe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function uqe(o){o=o|0,fqe(o)}function fqe(o){o=o|0,Aqe(o)}function Aqe(o){o=o|0,aX(o+8|0),s[o+24>>0]=1}function aX(o){o=o|0,n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,n[o+12>>2]=0}function pqe(){return 1492}function hqe(){return gqe()|0}function gqe(){var o=0,l=0,u=0,A=0,d=0,m=0,B=0;return l=I,I=I+16|0,d=l+4|0,B=l,u=Fl(8)|0,o=u,A=Jt(16)|0,aX(A),m=o+4|0,n[m>>2]=A,A=Jt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[d>>2]=n[B>>2],sX(A,m,d),n[u>>2]=A,I=l,o|0}function dqe(){var o=0;return s[7936]|0||(Bqe(10524),gr(25,10524,U|0)|0,o=7936,n[o>>2]=1,n[o+4>>2]=0),10524}function mqe(o,l){o=o|0,l=l|0,n[o>>2]=yqe()|0,n[o+4>>2]=Eqe()|0,n[o+12>>2]=l,n[o+8>>2]=Iqe()|0,n[o+32>>2]=7}function yqe(){return 11700}function Eqe(){return 1484}function Iqe(){return qP()|0}function Cqe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(qh(A,896)|0)==512?u|0&&(wqe(u),Et(u)):l|0&&Et(l)}function wqe(o){o=o|0,o=n[o+4>>2]|0,o|0&&Wh(o)}function Bqe(o){o=o|0,_h(o)}function vqe(o,l,u){o=o|0,l=l|0,u=u|0,o=Bn(l)|0,l=Sqe(u)|0,u=Dqe(u,0)|0,rGe(o,l,u,VM()|0,0)}function Sqe(o){return o=o|0,o|0}function Dqe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return k=I,I=I+16|0,d=k,m=k+4|0,n[d>>2]=o,T=VM()|0,B=T+24|0,l=yr(l,4)|0,n[m>>2]=l,u=T+28|0,A=n[u>>2]|0,A>>>0<(n[T+32>>2]|0)>>>0?(cX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(Rqe(B,d,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function VM(){var o=0,l=0;if(s[7944]|0||(lX(10568),gr(61,10568,U|0)|0,l=7944,n[l>>2]=1,n[l+4>>2]=0),!(Ur(10568)|0)){o=10568,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));lX(10568)}return 10568}function lX(o){o=o|0,xqe(o)}function bqe(o){o=o|0,Pqe(o+24|0)}function Pqe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function xqe(o){o=o|0;var l=0;l=en()|0,tn(o,1,17,l,aZ()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function kqe(o){return o=o|0,Tqe(n[(Qqe(o)|0)>>2]|0)|0}function Qqe(o){return o=o|0,(n[(VM()|0)+24>>2]|0)+(o<<3)|0}function Tqe(o){return o=o|0,HP(tx[o&7]()|0)|0}function cX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function Rqe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;if(k=I,I=I+32|0,d=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=Fqe(o)|0,A>>>0>>0)sn(o);else{T=n[o>>2]|0,M=(n[o+8>>2]|0)-T|0,_=M>>2,Nqe(d,M>>3>>>0>>1>>>0?_>>>0>>0?B:_:A,(n[m>>2]|0)-T>>3,o+8|0),B=d+8|0,cX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,Oqe(o,d),Lqe(d),I=k;return}}function Fqe(o){return o=o|0,536870911}function Nqe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{d=Jt(l<<3)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<3)}function Oqe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>3)<<3)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function Lqe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&Et(o)}function Mqe(){_qe()}function _qe(){Uqe(10604)}function Uqe(o){o=o|0,Hqe(o,4955)}function Hqe(o,l){o=o|0,l=l|0;var u=0;u=jqe()|0,n[o>>2]=u,qqe(u,l),Gh(n[o>>2]|0)}function jqe(){var o=0;return s[7952]|0||(Xqe(10612),gr(25,10612,U|0)|0,o=7952,n[o>>2]=1,n[o+4>>2]=0),10612}function qqe(o,l){o=o|0,l=l|0,n[o>>2]=Vqe()|0,n[o+4>>2]=Kqe()|0,n[o+12>>2]=l,n[o+8>>2]=Jqe()|0,n[o+32>>2]=8}function Gh(o){o=o|0;var l=0,u=0;l=I,I=I+16|0,u=l,Yy()|0,n[u>>2]=o,Gqe(10608,u),I=l}function Yy(){return s[11714]|0||(n[2652]=0,gr(62,10608,U|0)|0,s[11714]=1),10608}function Gqe(o,l){o=o|0,l=l|0;var u=0;u=Jt(8)|0,n[u+4>>2]=n[l>>2],n[u>>2]=n[o>>2],n[o>>2]=u}function Wqe(o){o=o|0,Yqe(o)}function Yqe(o){o=o|0;var l=0,u=0;if(l=n[o>>2]|0,l|0)do u=l,l=n[l>>2]|0,Et(u);while(l|0);n[o>>2]=0}function Vqe(){return 11715}function Kqe(){return 1496}function Jqe(){return jP()|0}function zqe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(qh(A,896)|0)==512?u|0&&(Zqe(u),Et(u)):l|0&&Et(l)}function Zqe(o){o=o|0,o=n[o+4>>2]|0,o|0&&Wh(o)}function Xqe(o){o=o|0,_h(o)}function $qe(o,l){o=o|0,l=l|0;var u=0,A=0;Yy()|0,u=n[2652]|0;e:do if(u|0){for(;A=n[u+4>>2]|0,!(A|0&&!(GX(KM(A)|0,o)|0));)if(u=n[u>>2]|0,!u)break e;eGe(A,l)}while(!1)}function KM(o){return o=o|0,n[o+12>>2]|0}function eGe(o,l){o=o|0,l=l|0;var u=0;o=o+36|0,u=n[o>>2]|0,u|0&&(Df(u),Et(u)),u=Jt(4)|0,FP(u,l),n[o>>2]=u}function JM(){return s[11716]|0||(n[2664]=0,gr(63,10656,U|0)|0,s[11716]=1),10656}function uX(){var o=0;return s[11717]|0?o=n[2665]|0:(tGe(),n[2665]=1504,s[11717]=1,o=1504),o|0}function tGe(){s[11740]|0||(s[11718]=yr(yr(8,0)|0,0)|0,s[11719]=yr(yr(0,0)|0,0)|0,s[11720]=yr(yr(0,16)|0,0)|0,s[11721]=yr(yr(8,0)|0,0)|0,s[11722]=yr(yr(0,0)|0,0)|0,s[11723]=yr(yr(8,0)|0,0)|0,s[11724]=yr(yr(0,0)|0,0)|0,s[11725]=yr(yr(8,0)|0,0)|0,s[11726]=yr(yr(0,0)|0,0)|0,s[11727]=yr(yr(8,0)|0,0)|0,s[11728]=yr(yr(0,0)|0,0)|0,s[11729]=yr(yr(0,0)|0,32)|0,s[11730]=yr(yr(0,0)|0,32)|0,s[11740]=1)}function fX(){return 1572}function rGe(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0;var m=0,B=0,k=0,T=0,_=0,M=0;m=I,I=I+32|0,M=m+16|0,_=m+12|0,T=m+8|0,k=m+4|0,B=m,n[M>>2]=o,n[_>>2]=l,n[T>>2]=u,n[k>>2]=A,n[B>>2]=d,JM()|0,nGe(10656,M,_,T,k,B),I=m}function nGe(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0;var B=0;B=Jt(24)|0,Uz(B+4|0,n[l>>2]|0,n[u>>2]|0,n[A>>2]|0,n[d>>2]|0,n[m>>2]|0),n[B>>2]=n[o>>2],n[o>>2]=B}function AX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0,Le=0,Qe=0,tt=0,Ze=0,ct=0;if(ct=I,I=I+32|0,Le=ct+20|0,Qe=ct+8|0,tt=ct+4|0,Ze=ct,l=n[l>>2]|0,l|0){We=Le+4|0,T=Le+8|0,_=Qe+4|0,M=Qe+8|0,G=Qe+8|0,ae=Le+8|0;do{if(B=l+4|0,k=zM(B)|0,k|0){if(d=P2(k)|0,n[Le>>2]=0,n[We>>2]=0,n[T>>2]=0,A=(x2(k)|0)+1|0,iGe(Le,A),A|0)for(;A=A+-1|0,xu(Qe,n[d>>2]|0),m=n[We>>2]|0,m>>>0<(n[ae>>2]|0)>>>0?(n[m>>2]=n[Qe>>2],n[We>>2]=(n[We>>2]|0)+4):ZM(Le,Qe),A;)d=d+4|0;A=k2(k)|0,n[Qe>>2]=0,n[_>>2]=0,n[M>>2]=0;e:do if(n[A>>2]|0)for(d=0,m=0;;){if((d|0)==(m|0)?sGe(Qe,A):(n[d>>2]=n[A>>2],n[_>>2]=(n[_>>2]|0)+4),A=A+4|0,!(n[A>>2]|0))break e;d=n[_>>2]|0,m=n[G>>2]|0}while(!1);n[tt>>2]=GP(B)|0,n[Ze>>2]=Ur(k)|0,oGe(u,o,tt,Ze,Le,Qe),XM(Qe),sp(Le)}l=n[l>>2]|0}while(l|0)}I=ct}function zM(o){return o=o|0,n[o+12>>2]|0}function P2(o){return o=o|0,n[o+12>>2]|0}function x2(o){return o=o|0,n[o+16>>2]|0}function iGe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0;d=I,I=I+32|0,u=d,A=n[o>>2]|0,(n[o+8>>2]|0)-A>>2>>>0>>0&&(IX(u,l,(n[o+4>>2]|0)-A>>2,o+8|0),CX(o,u),wX(u)),I=d}function ZM(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0;if(B=I,I=I+32|0,u=B,A=o+4|0,d=((n[A>>2]|0)-(n[o>>2]|0)>>2)+1|0,m=EX(o)|0,m>>>0>>0)sn(o);else{k=n[o>>2]|0,_=(n[o+8>>2]|0)-k|0,T=_>>1,IX(u,_>>2>>>0>>1>>>0?T>>>0>>0?d:T:m,(n[A>>2]|0)-k>>2,o+8|0),m=u+8|0,n[n[m>>2]>>2]=n[l>>2],n[m>>2]=(n[m>>2]|0)+4,CX(o,u),wX(u),I=B;return}}function k2(o){return o=o|0,n[o+8>>2]|0}function sGe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0;if(B=I,I=I+32|0,u=B,A=o+4|0,d=((n[A>>2]|0)-(n[o>>2]|0)>>2)+1|0,m=yX(o)|0,m>>>0>>0)sn(o);else{k=n[o>>2]|0,_=(n[o+8>>2]|0)-k|0,T=_>>1,SGe(u,_>>2>>>0>>1>>>0?T>>>0>>0?d:T:m,(n[A>>2]|0)-k>>2,o+8|0),m=u+8|0,n[n[m>>2]>>2]=n[l>>2],n[m>>2]=(n[m>>2]|0)+4,DGe(o,u),bGe(u),I=B;return}}function GP(o){return o=o|0,n[o>>2]|0}function oGe(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0,aGe(o,l,u,A,d,m)}function XM(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-4-A|0)>>>2)<<2)),Et(u))}function sp(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-4-A|0)>>>2)<<2)),Et(u))}function aGe(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0;var B=0,k=0,T=0,_=0,M=0,G=0;B=I,I=I+48|0,M=B+40|0,k=B+32|0,G=B+24|0,T=B+12|0,_=B,Nl(k),o=Ms(o)|0,n[G>>2]=n[l>>2],u=n[u>>2]|0,A=n[A>>2]|0,$M(T,d),lGe(_,m),n[M>>2]=n[G>>2],cGe(o,M,u,A,T,_),XM(_),sp(T),Ol(k),I=B}function $M(o,l){o=o|0,l=l|0;var u=0,A=0;n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,u=l+4|0,A=(n[u>>2]|0)-(n[l>>2]|0)>>2,A|0&&(BGe(o,A),vGe(o,n[l>>2]|0,n[u>>2]|0,A))}function lGe(o,l){o=o|0,l=l|0;var u=0,A=0;n[o>>2]=0,n[o+4>>2]=0,n[o+8>>2]=0,u=l+4|0,A=(n[u>>2]|0)-(n[l>>2]|0)>>2,A|0&&(CGe(o,A),wGe(o,n[l>>2]|0,n[u>>2]|0,A))}function cGe(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0;var B=0,k=0,T=0,_=0,M=0,G=0;B=I,I=I+32|0,M=B+28|0,G=B+24|0,k=B+12|0,T=B,_=ma(uGe()|0)|0,n[G>>2]=n[l>>2],n[M>>2]=n[G>>2],l=ud(M)|0,u=pX(u)|0,A=e_(A)|0,n[k>>2]=n[d>>2],M=d+4|0,n[k+4>>2]=n[M>>2],G=d+8|0,n[k+8>>2]=n[G>>2],n[G>>2]=0,n[M>>2]=0,n[d>>2]=0,d=t_(k)|0,n[T>>2]=n[m>>2],M=m+4|0,n[T+4>>2]=n[M>>2],G=m+8|0,n[T+8>>2]=n[G>>2],n[G>>2]=0,n[M>>2]=0,n[m>>2]=0,uu(0,_|0,o|0,l|0,u|0,A|0,d|0,fGe(T)|0)|0,XM(T),sp(k),I=B}function uGe(){var o=0;return s[7968]|0||(EGe(10708),o=7968,n[o>>2]=1,n[o+4>>2]=0),10708}function ud(o){return o=o|0,gX(o)|0}function pX(o){return o=o|0,hX(o)|0}function e_(o){return o=o|0,HP(o)|0}function t_(o){return o=o|0,pGe(o)|0}function fGe(o){return o=o|0,AGe(o)|0}function AGe(o){o=o|0;var l=0,u=0,A=0;if(A=(n[o+4>>2]|0)-(n[o>>2]|0)|0,u=A>>2,A=Fl(A+4|0)|0,n[A>>2]=u,u|0){l=0;do n[A+4+(l<<2)>>2]=hX(n[(n[o>>2]|0)+(l<<2)>>2]|0)|0,l=l+1|0;while((l|0)!=(u|0))}return A|0}function hX(o){return o=o|0,o|0}function pGe(o){o=o|0;var l=0,u=0,A=0;if(A=(n[o+4>>2]|0)-(n[o>>2]|0)|0,u=A>>2,A=Fl(A+4|0)|0,n[A>>2]=u,u|0){l=0;do n[A+4+(l<<2)>>2]=gX((n[o>>2]|0)+(l<<2)|0)|0,l=l+1|0;while((l|0)!=(u|0))}return A|0}function gX(o){o=o|0;var l=0,u=0,A=0,d=0;return d=I,I=I+32|0,l=d+12|0,u=d,A=pM(dX()|0)|0,A?(hM(l,A),gM(u,l),JWe(o,u),o=dM(l)|0):o=hGe(o)|0,I=d,o|0}function dX(){var o=0;return s[7960]|0||(yGe(10664),gr(25,10664,U|0)|0,o=7960,n[o>>2]=1,n[o+4>>2]=0),10664}function hGe(o){o=o|0;var l=0,u=0,A=0,d=0,m=0,B=0,k=0;return u=I,I=I+16|0,d=u+4|0,B=u,A=Fl(8)|0,l=A,k=Jt(4)|0,n[k>>2]=n[o>>2],m=l+4|0,n[m>>2]=k,o=Jt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[d>>2]=n[B>>2],mX(o,m,d),n[A>>2]=o,I=u,l|0}function mX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Jt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1656,n[u+12>>2]=l,n[o+4>>2]=u}function gGe(o){o=o|0,Zy(o),Et(o)}function dGe(o){o=o|0,o=n[o+12>>2]|0,o|0&&Et(o)}function mGe(o){o=o|0,Et(o)}function yGe(o){o=o|0,_h(o)}function EGe(o){o=o|0,Ro(o,IGe()|0,5)}function IGe(){return 1676}function CGe(o,l){o=o|0,l=l|0;var u=0;if((yX(o)|0)>>>0>>0&&sn(o),l>>>0>1073741823)Nt();else{u=Jt(l<<2)|0,n[o+4>>2]=u,n[o>>2]=u,n[o+8>>2]=u+(l<<2);return}}function wGe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,A=o+4|0,o=u-l|0,(o|0)>0&&(Qr(n[A>>2]|0,l|0,o|0)|0,n[A>>2]=(n[A>>2]|0)+(o>>>2<<2))}function yX(o){return o=o|0,1073741823}function BGe(o,l){o=o|0,l=l|0;var u=0;if((EX(o)|0)>>>0>>0&&sn(o),l>>>0>1073741823)Nt();else{u=Jt(l<<2)|0,n[o+4>>2]=u,n[o>>2]=u,n[o+8>>2]=u+(l<<2);return}}function vGe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,A=o+4|0,o=u-l|0,(o|0)>0&&(Qr(n[A>>2]|0,l|0,o|0)|0,n[A>>2]=(n[A>>2]|0)+(o>>>2<<2))}function EX(o){return o=o|0,1073741823}function SGe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>1073741823)Nt();else{d=Jt(l<<2)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<2)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<2)}function DGe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>2)<<2)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function bGe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-4-l|0)>>>2)<<2)),o=n[o>>2]|0,o|0&&Et(o)}function IX(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>1073741823)Nt();else{d=Jt(l<<2)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<2)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<2)}function CX(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>2)<<2)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function wX(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-4-l|0)>>>2)<<2)),o=n[o>>2]|0,o|0&&Et(o)}function PGe(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0;var m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0,Le=0,Qe=0;if(Qe=I,I=I+32|0,M=Qe+20|0,G=Qe+12|0,_=Qe+16|0,ae=Qe+4|0,We=Qe,Le=Qe+8|0,k=uX()|0,m=n[k>>2]|0,B=n[m>>2]|0,B|0)for(T=n[k+8>>2]|0,k=n[k+4>>2]|0;xu(M,B),xGe(o,M,k,T),m=m+4|0,B=n[m>>2]|0,B;)T=T+1|0,k=k+1|0;if(m=fX()|0,B=n[m>>2]|0,B|0)do xu(M,B),n[G>>2]=n[m+4>>2],kGe(l,M,G),m=m+8|0,B=n[m>>2]|0;while(B|0);if(m=n[(Yy()|0)>>2]|0,m|0)do l=n[m+4>>2]|0,xu(M,n[(Vy(l)|0)>>2]|0),n[G>>2]=KM(l)|0,QGe(u,M,G),m=n[m>>2]|0;while(m|0);if(xu(_,0),m=JM()|0,n[M>>2]=n[_>>2],AX(M,m,d),m=n[(Yy()|0)>>2]|0,m|0){o=M+4|0,l=M+8|0,u=M+8|0;do{if(T=n[m+4>>2]|0,xu(G,n[(Vy(T)|0)>>2]|0),TGe(ae,BX(T)|0),B=n[ae>>2]|0,B|0){n[M>>2]=0,n[o>>2]=0,n[l>>2]=0;do xu(We,n[(Vy(n[B+4>>2]|0)|0)>>2]|0),k=n[o>>2]|0,k>>>0<(n[u>>2]|0)>>>0?(n[k>>2]=n[We>>2],n[o>>2]=(n[o>>2]|0)+4):ZM(M,We),B=n[B>>2]|0;while(B|0);RGe(A,G,M),sp(M)}n[Le>>2]=n[G>>2],_=vX(T)|0,n[M>>2]=n[Le>>2],AX(M,_,d),Yz(ae),m=n[m>>2]|0}while(m|0)}I=Qe}function xGe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,WGe(o,l,u,A)}function kGe(o,l,u){o=o|0,l=l|0,u=u|0,GGe(o,l,u)}function Vy(o){return o=o|0,o|0}function QGe(o,l,u){o=o|0,l=l|0,u=u|0,UGe(o,l,u)}function BX(o){return o=o|0,o+16|0}function TGe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;if(m=I,I=I+16|0,d=m+8|0,u=m,n[o>>2]=0,A=n[l>>2]|0,n[d>>2]=A,n[u>>2]=o,u=_Ge(u)|0,A|0){if(A=Jt(12)|0,B=(SX(d)|0)+4|0,o=n[B+4>>2]|0,l=A+4|0,n[l>>2]=n[B>>2],n[l+4>>2]=o,l=n[n[d>>2]>>2]|0,n[d>>2]=l,!l)o=A;else for(l=A;o=Jt(12)|0,T=(SX(d)|0)+4|0,k=n[T+4>>2]|0,B=o+4|0,n[B>>2]=n[T>>2],n[B+4>>2]=k,n[l>>2]=o,B=n[n[d>>2]>>2]|0,n[d>>2]=B,B;)l=o;n[o>>2]=n[u>>2],n[u>>2]=A}I=m}function RGe(o,l,u){o=o|0,l=l|0,u=u|0,FGe(o,l,u)}function vX(o){return o=o|0,o+24|0}function FGe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+32|0,B=A+24|0,d=A+16|0,k=A+12|0,m=A,Nl(d),o=Ms(o)|0,n[k>>2]=n[l>>2],$M(m,u),n[B>>2]=n[k>>2],NGe(o,B,m),sp(m),Ol(d),I=A}function NGe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=I,I=I+32|0,B=A+16|0,k=A+12|0,d=A,m=ma(OGe()|0)|0,n[k>>2]=n[l>>2],n[B>>2]=n[k>>2],l=ud(B)|0,n[d>>2]=n[u>>2],B=u+4|0,n[d+4>>2]=n[B>>2],k=u+8|0,n[d+8>>2]=n[k>>2],n[k>>2]=0,n[B>>2]=0,n[u>>2]=0,Fs(0,m|0,o|0,l|0,t_(d)|0)|0,sp(d),I=A}function OGe(){var o=0;return s[7976]|0||(LGe(10720),o=7976,n[o>>2]=1,n[o+4>>2]=0),10720}function LGe(o){o=o|0,Ro(o,MGe()|0,2)}function MGe(){return 1732}function _Ge(o){return o=o|0,n[o>>2]|0}function SX(o){return o=o|0,n[o>>2]|0}function UGe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;A=I,I=I+32|0,m=A+16|0,d=A+8|0,B=A,Nl(d),o=Ms(o)|0,n[B>>2]=n[l>>2],u=n[u>>2]|0,n[m>>2]=n[B>>2],DX(o,m,u),Ol(d),I=A}function DX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;A=I,I=I+16|0,m=A+4|0,B=A,d=ma(HGe()|0)|0,n[B>>2]=n[l>>2],n[m>>2]=n[B>>2],l=ud(m)|0,Fs(0,d|0,o|0,l|0,pX(u)|0)|0,I=A}function HGe(){var o=0;return s[7984]|0||(jGe(10732),o=7984,n[o>>2]=1,n[o+4>>2]=0),10732}function jGe(o){o=o|0,Ro(o,qGe()|0,2)}function qGe(){return 1744}function GGe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;A=I,I=I+32|0,m=A+16|0,d=A+8|0,B=A,Nl(d),o=Ms(o)|0,n[B>>2]=n[l>>2],u=n[u>>2]|0,n[m>>2]=n[B>>2],DX(o,m,u),Ol(d),I=A}function WGe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0;d=I,I=I+32|0,B=d+16|0,m=d+8|0,k=d,Nl(m),o=Ms(o)|0,n[k>>2]=n[l>>2],u=s[u>>0]|0,A=s[A>>0]|0,n[B>>2]=n[k>>2],YGe(o,B,u,A),Ol(m),I=d}function YGe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0;d=I,I=I+16|0,B=d+4|0,k=d,m=ma(VGe()|0)|0,n[k>>2]=n[l>>2],n[B>>2]=n[k>>2],l=ud(B)|0,u=Ky(u)|0,Mi(0,m|0,o|0,l|0,u|0,Ky(A)|0)|0,I=d}function VGe(){var o=0;return s[7992]|0||(JGe(10744),o=7992,n[o>>2]=1,n[o+4>>2]=0),10744}function Ky(o){return o=o|0,KGe(o)|0}function KGe(o){return o=o|0,o&255|0}function JGe(o){o=o|0,Ro(o,zGe()|0,3)}function zGe(){return 1756}function ZGe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;switch(ae=I,I=I+32|0,k=ae+8|0,T=ae+4|0,_=ae+20|0,M=ae,IM(o,0),A=KWe(l)|0,n[k>>2]=0,G=k+4|0,n[G>>2]=0,n[k+8>>2]=0,A<<24>>24){case 0:{s[_>>0]=0,XGe(T,u,_),WP(o,T)|0,bf(T);break}case 8:{G=a_(l)|0,s[_>>0]=8,xu(M,n[G+4>>2]|0),$Ge(T,u,_,M,G+8|0),WP(o,T)|0,bf(T);break}case 9:{if(m=a_(l)|0,l=n[m+4>>2]|0,l|0)for(B=k+8|0,d=m+12|0;l=l+-1|0,xu(T,n[d>>2]|0),A=n[G>>2]|0,A>>>0<(n[B>>2]|0)>>>0?(n[A>>2]=n[T>>2],n[G>>2]=(n[G>>2]|0)+4):ZM(k,T),l;)d=d+4|0;s[_>>0]=9,xu(M,n[m+8>>2]|0),e5e(T,u,_,M,k),WP(o,T)|0,bf(T);break}default:G=a_(l)|0,s[_>>0]=A,xu(M,n[G+4>>2]|0),t5e(T,u,_,M),WP(o,T)|0,bf(T)}sp(k),I=ae}function XGe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0;A=I,I=I+16|0,d=A,Nl(d),l=Ms(l)|0,h5e(o,l,s[u>>0]|0),Ol(d),I=A}function WP(o,l){o=o|0,l=l|0;var u=0;return u=n[o>>2]|0,u|0&&Oa(u|0),n[o>>2]=n[l>>2],n[l>>2]=0,o|0}function $Ge(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0;var m=0,B=0,k=0,T=0;m=I,I=I+32|0,k=m+16|0,B=m+8|0,T=m,Nl(B),l=Ms(l)|0,u=s[u>>0]|0,n[T>>2]=n[A>>2],d=n[d>>2]|0,n[k>>2]=n[T>>2],u5e(o,l,u,k,d),Ol(B),I=m}function e5e(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0;var m=0,B=0,k=0,T=0,_=0;m=I,I=I+32|0,T=m+24|0,B=m+16|0,_=m+12|0,k=m,Nl(B),l=Ms(l)|0,u=s[u>>0]|0,n[_>>2]=n[A>>2],$M(k,d),n[T>>2]=n[_>>2],o5e(o,l,u,T,k),sp(k),Ol(B),I=m}function t5e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0;d=I,I=I+32|0,B=d+16|0,m=d+8|0,k=d,Nl(m),l=Ms(l)|0,u=s[u>>0]|0,n[k>>2]=n[A>>2],n[B>>2]=n[k>>2],r5e(o,l,u,B),Ol(m),I=d}function r5e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0,B=0,k=0;d=I,I=I+16|0,m=d+4|0,k=d,B=ma(n5e()|0)|0,u=Ky(u)|0,n[k>>2]=n[A>>2],n[m>>2]=n[k>>2],YP(o,Fs(0,B|0,l|0,u|0,ud(m)|0)|0),I=d}function n5e(){var o=0;return s[8e3]|0||(i5e(10756),o=8e3,n[o>>2]=1,n[o+4>>2]=0),10756}function YP(o,l){o=o|0,l=l|0,IM(o,l)}function i5e(o){o=o|0,Ro(o,s5e()|0,2)}function s5e(){return 1772}function o5e(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0;var m=0,B=0,k=0,T=0,_=0;m=I,I=I+32|0,T=m+16|0,_=m+12|0,B=m,k=ma(a5e()|0)|0,u=Ky(u)|0,n[_>>2]=n[A>>2],n[T>>2]=n[_>>2],A=ud(T)|0,n[B>>2]=n[d>>2],T=d+4|0,n[B+4>>2]=n[T>>2],_=d+8|0,n[B+8>>2]=n[_>>2],n[_>>2]=0,n[T>>2]=0,n[d>>2]=0,YP(o,Mi(0,k|0,l|0,u|0,A|0,t_(B)|0)|0),sp(B),I=m}function a5e(){var o=0;return s[8008]|0||(l5e(10768),o=8008,n[o>>2]=1,n[o+4>>2]=0),10768}function l5e(o){o=o|0,Ro(o,c5e()|0,3)}function c5e(){return 1784}function u5e(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0;var m=0,B=0,k=0,T=0;m=I,I=I+16|0,k=m+4|0,T=m,B=ma(f5e()|0)|0,u=Ky(u)|0,n[T>>2]=n[A>>2],n[k>>2]=n[T>>2],A=ud(k)|0,YP(o,Mi(0,B|0,l|0,u|0,A|0,e_(d)|0)|0),I=m}function f5e(){var o=0;return s[8016]|0||(A5e(10780),o=8016,n[o>>2]=1,n[o+4>>2]=0),10780}function A5e(o){o=o|0,Ro(o,p5e()|0,3)}function p5e(){return 1800}function h5e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;A=ma(g5e()|0)|0,YP(o,dn(0,A|0,l|0,Ky(u)|0)|0)}function g5e(){var o=0;return s[8024]|0||(d5e(10792),o=8024,n[o>>2]=1,n[o+4>>2]=0),10792}function d5e(o){o=o|0,Ro(o,m5e()|0,1)}function m5e(){return 1816}function y5e(){E5e(),I5e(),C5e()}function E5e(){n[2702]=e$(65536)|0}function I5e(){H5e(10856)}function C5e(){w5e(10816)}function w5e(o){o=o|0,B5e(o,5044),v5e(o)|0}function B5e(o,l){o=o|0,l=l|0;var u=0;u=dX()|0,n[o>>2]=u,N5e(u,l),Gh(n[o>>2]|0)}function v5e(o){o=o|0;var l=0;return l=n[o>>2]|0,ld(l,S5e()|0),o|0}function S5e(){var o=0;return s[8032]|0||(bX(10820),gr(64,10820,U|0)|0,o=8032,n[o>>2]=1,n[o+4>>2]=0),Ur(10820)|0||bX(10820),10820}function bX(o){o=o|0,P5e(o),cd(o,25)}function D5e(o){o=o|0,b5e(o+24|0)}function b5e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function P5e(o){o=o|0;var l=0;l=en()|0,tn(o,5,18,l,T5e()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function x5e(o,l){o=o|0,l=l|0,k5e(o,l)}function k5e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0;u=I,I=I+16|0,A=u,d=u+4|0,od(d,l),n[A>>2]=ad(d,l)|0,Q5e(o,A),I=u}function Q5e(o,l){o=o|0,l=l|0,PX(o+4|0,n[l>>2]|0),s[o+8>>0]=1}function PX(o,l){o=o|0,l=l|0,n[o>>2]=l}function T5e(){return 1824}function R5e(o){return o=o|0,F5e(o)|0}function F5e(o){o=o|0;var l=0,u=0,A=0,d=0,m=0,B=0,k=0;return u=I,I=I+16|0,d=u+4|0,B=u,A=Fl(8)|0,l=A,k=Jt(4)|0,od(d,o),PX(k,ad(d,o)|0),m=l+4|0,n[m>>2]=k,o=Jt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[d>>2]=n[B>>2],mX(o,m,d),n[A>>2]=o,I=u,l|0}function Fl(o){o=o|0;var l=0,u=0;return o=o+7&-8,o>>>0<=32768&&(l=n[2701]|0,o>>>0<=(65536-l|0)>>>0)?(u=(n[2702]|0)+l|0,n[2701]=l+o,o=u):(o=e$(o+8|0)|0,n[o>>2]=n[2703],n[2703]=o,o=o+8|0),o|0}function N5e(o,l){o=o|0,l=l|0,n[o>>2]=O5e()|0,n[o+4>>2]=L5e()|0,n[o+12>>2]=l,n[o+8>>2]=M5e()|0,n[o+32>>2]=9}function O5e(){return 11744}function L5e(){return 1832}function M5e(){return qP()|0}function _5e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(qh(A,896)|0)==512?u|0&&(U5e(u),Et(u)):l|0&&Et(l)}function U5e(o){o=o|0,o=n[o+4>>2]|0,o|0&&Wh(o)}function H5e(o){o=o|0,j5e(o,5052),q5e(o)|0,G5e(o,5058,26)|0,W5e(o,5069,1)|0,Y5e(o,5077,10)|0,V5e(o,5087,19)|0,K5e(o,5094,27)|0}function j5e(o,l){o=o|0,l=l|0;var u=0;u=UWe()|0,n[o>>2]=u,HWe(u,l),Gh(n[o>>2]|0)}function q5e(o){o=o|0;var l=0;return l=n[o>>2]|0,ld(l,DWe()|0),o|0}function G5e(o,l,u){return o=o|0,l=l|0,u=u|0,lWe(o,Bn(l)|0,u,0),o|0}function W5e(o,l,u){return o=o|0,l=l|0,u=u|0,V9e(o,Bn(l)|0,u,0),o|0}function Y5e(o,l,u){return o=o|0,l=l|0,u=u|0,S9e(o,Bn(l)|0,u,0),o|0}function V5e(o,l,u){return o=o|0,l=l|0,u=u|0,u9e(o,Bn(l)|0,u,0),o|0}function xX(o,l){o=o|0,l=l|0;var u=0,A=0;e:for(;;){for(u=n[2703]|0;;){if((u|0)==(l|0))break e;if(A=n[u>>2]|0,n[2703]=A,!u)u=A;else break}Et(u)}n[2701]=o}function K5e(o,l,u){return o=o|0,l=l|0,u=u|0,J5e(o,Bn(l)|0,u,0),o|0}function J5e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;m=n[o>>2]|0,d=r_()|0,o=z5e(u)|0,vn(m,l,d,o,Z5e(u,A)|0,A)}function r_(){var o=0,l=0;if(s[8040]|0||(QX(10860),gr(65,10860,U|0)|0,l=8040,n[l>>2]=1,n[l+4>>2]=0),!(Ur(10860)|0)){o=10860,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));QX(10860)}return 10860}function z5e(o){return o=o|0,o|0}function Z5e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return k=I,I=I+16|0,d=k,m=k+4|0,n[d>>2]=o,T=r_()|0,B=T+24|0,l=yr(l,4)|0,n[m>>2]=l,u=T+28|0,A=n[u>>2]|0,A>>>0<(n[T+32>>2]|0)>>>0?(kX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(X5e(B,d,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function kX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function X5e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;if(k=I,I=I+32|0,d=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=$5e(o)|0,A>>>0>>0)sn(o);else{T=n[o>>2]|0,M=(n[o+8>>2]|0)-T|0,_=M>>2,e9e(d,M>>3>>>0>>1>>>0?_>>>0>>0?B:_:A,(n[m>>2]|0)-T>>3,o+8|0),B=d+8|0,kX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,t9e(o,d),r9e(d),I=k;return}}function $5e(o){return o=o|0,536870911}function e9e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{d=Jt(l<<3)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<3)}function t9e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>3)<<3)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function r9e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&Et(o)}function QX(o){o=o|0,s9e(o)}function n9e(o){o=o|0,i9e(o+24|0)}function i9e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function s9e(o){o=o|0;var l=0;l=en()|0,tn(o,1,11,l,o9e()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function o9e(){return 1840}function a9e(o,l,u){o=o|0,l=l|0,u=u|0,c9e(n[(l9e(o)|0)>>2]|0,l,u)}function l9e(o){return o=o|0,(n[(r_()|0)+24>>2]|0)+(o<<3)|0}function c9e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0;A=I,I=I+16|0,m=A+1|0,d=A,od(m,l),l=ad(m,l)|0,od(d,u),u=ad(d,u)|0,ap[o&31](l,u),I=A}function u9e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;m=n[o>>2]|0,d=n_()|0,o=f9e(u)|0,vn(m,l,d,o,A9e(u,A)|0,A)}function n_(){var o=0,l=0;if(s[8048]|0||(RX(10896),gr(66,10896,U|0)|0,l=8048,n[l>>2]=1,n[l+4>>2]=0),!(Ur(10896)|0)){o=10896,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));RX(10896)}return 10896}function f9e(o){return o=o|0,o|0}function A9e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return k=I,I=I+16|0,d=k,m=k+4|0,n[d>>2]=o,T=n_()|0,B=T+24|0,l=yr(l,4)|0,n[m>>2]=l,u=T+28|0,A=n[u>>2]|0,A>>>0<(n[T+32>>2]|0)>>>0?(TX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(p9e(B,d,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function TX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function p9e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;if(k=I,I=I+32|0,d=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=h9e(o)|0,A>>>0>>0)sn(o);else{T=n[o>>2]|0,M=(n[o+8>>2]|0)-T|0,_=M>>2,g9e(d,M>>3>>>0>>1>>>0?_>>>0>>0?B:_:A,(n[m>>2]|0)-T>>3,o+8|0),B=d+8|0,TX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,d9e(o,d),m9e(d),I=k;return}}function h9e(o){return o=o|0,536870911}function g9e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{d=Jt(l<<3)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<3)}function d9e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>3)<<3)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function m9e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&Et(o)}function RX(o){o=o|0,I9e(o)}function y9e(o){o=o|0,E9e(o+24|0)}function E9e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function I9e(o){o=o|0;var l=0;l=en()|0,tn(o,1,11,l,C9e()|0,1),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function C9e(){return 1852}function w9e(o,l){return o=o|0,l=l|0,v9e(n[(B9e(o)|0)>>2]|0,l)|0}function B9e(o){return o=o|0,(n[(n_()|0)+24>>2]|0)+(o<<3)|0}function v9e(o,l){o=o|0,l=l|0;var u=0,A=0;return u=I,I=I+16|0,A=u,od(A,l),l=ad(A,l)|0,l=HP(hd[o&31](l)|0)|0,I=u,l|0}function S9e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;m=n[o>>2]|0,d=i_()|0,o=D9e(u)|0,vn(m,l,d,o,b9e(u,A)|0,A)}function i_(){var o=0,l=0;if(s[8056]|0||(NX(10932),gr(67,10932,U|0)|0,l=8056,n[l>>2]=1,n[l+4>>2]=0),!(Ur(10932)|0)){o=10932,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));NX(10932)}return 10932}function D9e(o){return o=o|0,o|0}function b9e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return k=I,I=I+16|0,d=k,m=k+4|0,n[d>>2]=o,T=i_()|0,B=T+24|0,l=yr(l,4)|0,n[m>>2]=l,u=T+28|0,A=n[u>>2]|0,A>>>0<(n[T+32>>2]|0)>>>0?(FX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(P9e(B,d,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function FX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function P9e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;if(k=I,I=I+32|0,d=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=x9e(o)|0,A>>>0>>0)sn(o);else{T=n[o>>2]|0,M=(n[o+8>>2]|0)-T|0,_=M>>2,k9e(d,M>>3>>>0>>1>>>0?_>>>0>>0?B:_:A,(n[m>>2]|0)-T>>3,o+8|0),B=d+8|0,FX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,Q9e(o,d),T9e(d),I=k;return}}function x9e(o){return o=o|0,536870911}function k9e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{d=Jt(l<<3)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<3)}function Q9e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>3)<<3)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function T9e(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&Et(o)}function NX(o){o=o|0,N9e(o)}function R9e(o){o=o|0,F9e(o+24|0)}function F9e(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function N9e(o){o=o|0;var l=0;l=en()|0,tn(o,1,7,l,O9e()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function O9e(){return 1860}function L9e(o,l,u){return o=o|0,l=l|0,u=u|0,_9e(n[(M9e(o)|0)>>2]|0,l,u)|0}function M9e(o){return o=o|0,(n[(i_()|0)+24>>2]|0)+(o<<3)|0}function _9e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0;return A=I,I=I+32|0,B=A+12|0,m=A+8|0,k=A,T=A+16|0,d=A+4|0,U9e(T,l),H9e(k,T,l),Uh(d,u),u=Hh(d,u)|0,n[B>>2]=n[k>>2],F2[o&15](m,B,u),u=j9e(m)|0,bf(m),jh(d),I=A,u|0}function U9e(o,l){o=o|0,l=l|0}function H9e(o,l,u){o=o|0,l=l|0,u=u|0,q9e(o,u)}function j9e(o){return o=o|0,Ms(o)|0}function q9e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0;d=I,I=I+16|0,u=d,A=l,A&1?(G9e(u,0),Me(A|0,u|0)|0,W9e(o,u),Y9e(u)):n[o>>2]=n[l>>2],I=d}function G9e(o,l){o=o|0,l=l|0,bu(o,l),n[o+4>>2]=0,s[o+8>>0]=0}function W9e(o,l){o=o|0,l=l|0,n[o>>2]=n[l+4>>2]}function Y9e(o){o=o|0,s[o+8>>0]=0}function V9e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;m=n[o>>2]|0,d=s_()|0,o=K9e(u)|0,vn(m,l,d,o,J9e(u,A)|0,A)}function s_(){var o=0,l=0;if(s[8064]|0||(LX(10968),gr(68,10968,U|0)|0,l=8064,n[l>>2]=1,n[l+4>>2]=0),!(Ur(10968)|0)){o=10968,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));LX(10968)}return 10968}function K9e(o){return o=o|0,o|0}function J9e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return k=I,I=I+16|0,d=k,m=k+4|0,n[d>>2]=o,T=s_()|0,B=T+24|0,l=yr(l,4)|0,n[m>>2]=l,u=T+28|0,A=n[u>>2]|0,A>>>0<(n[T+32>>2]|0)>>>0?(OX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(z9e(B,d,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function OX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function z9e(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;if(k=I,I=I+32|0,d=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=Z9e(o)|0,A>>>0>>0)sn(o);else{T=n[o>>2]|0,M=(n[o+8>>2]|0)-T|0,_=M>>2,X9e(d,M>>3>>>0>>1>>>0?_>>>0>>0?B:_:A,(n[m>>2]|0)-T>>3,o+8|0),B=d+8|0,OX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,$9e(o,d),eWe(d),I=k;return}}function Z9e(o){return o=o|0,536870911}function X9e(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{d=Jt(l<<3)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<3)}function $9e(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>3)<<3)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function eWe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&Et(o)}function LX(o){o=o|0,nWe(o)}function tWe(o){o=o|0,rWe(o+24|0)}function rWe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function nWe(o){o=o|0;var l=0;l=en()|0,tn(o,1,1,l,iWe()|0,5),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function iWe(){return 1872}function sWe(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0,aWe(n[(oWe(o)|0)>>2]|0,l,u,A,d,m)}function oWe(o){return o=o|0,(n[(s_()|0)+24>>2]|0)+(o<<3)|0}function aWe(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0;var B=0,k=0,T=0,_=0,M=0,G=0;B=I,I=I+32|0,k=B+16|0,T=B+12|0,_=B+8|0,M=B+4|0,G=B,Uh(k,l),l=Hh(k,l)|0,Uh(T,u),u=Hh(T,u)|0,Uh(_,A),A=Hh(_,A)|0,Uh(M,d),d=Hh(M,d)|0,Uh(G,m),m=Hh(G,m)|0,s$[o&1](l,u,A,d,m),jh(G),jh(M),jh(_),jh(T),jh(k),I=B}function lWe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;m=n[o>>2]|0,d=o_()|0,o=cWe(u)|0,vn(m,l,d,o,uWe(u,A)|0,A)}function o_(){var o=0,l=0;if(s[8072]|0||(_X(11004),gr(69,11004,U|0)|0,l=8072,n[l>>2]=1,n[l+4>>2]=0),!(Ur(11004)|0)){o=11004,l=o+36|0;do n[o>>2]=0,o=o+4|0;while((o|0)<(l|0));_X(11004)}return 11004}function cWe(o){return o=o|0,o|0}function uWe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0,k=0,T=0;return k=I,I=I+16|0,d=k,m=k+4|0,n[d>>2]=o,T=o_()|0,B=T+24|0,l=yr(l,4)|0,n[m>>2]=l,u=T+28|0,A=n[u>>2]|0,A>>>0<(n[T+32>>2]|0)>>>0?(MX(A,o,l),l=(n[u>>2]|0)+8|0,n[u>>2]=l):(fWe(B,d,m),l=n[u>>2]|0),I=k,(l-(n[B>>2]|0)>>3)+-1|0}function MX(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,n[o+4>>2]=u}function fWe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0;if(k=I,I=I+32|0,d=k,m=o+4|0,B=((n[m>>2]|0)-(n[o>>2]|0)>>3)+1|0,A=AWe(o)|0,A>>>0>>0)sn(o);else{T=n[o>>2]|0,M=(n[o+8>>2]|0)-T|0,_=M>>2,pWe(d,M>>3>>>0>>1>>>0?_>>>0>>0?B:_:A,(n[m>>2]|0)-T>>3,o+8|0),B=d+8|0,MX(n[B>>2]|0,n[l>>2]|0,n[u>>2]|0),n[B>>2]=(n[B>>2]|0)+8,hWe(o,d),gWe(d),I=k;return}}function AWe(o){return o=o|0,536870911}function pWe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0;n[o+12>>2]=0,n[o+16>>2]=A;do if(l)if(l>>>0>536870911)Nt();else{d=Jt(l<<3)|0;break}else d=0;while(!1);n[o>>2]=d,A=d+(u<<3)|0,n[o+8>>2]=A,n[o+4>>2]=A,n[o+12>>2]=d+(l<<3)}function hWe(o,l){o=o|0,l=l|0;var u=0,A=0,d=0,m=0,B=0;A=n[o>>2]|0,B=o+4|0,m=l+4|0,d=(n[B>>2]|0)-A|0,u=(n[m>>2]|0)+(0-(d>>3)<<3)|0,n[m>>2]=u,(d|0)>0?(Qr(u|0,A|0,d|0)|0,A=m,u=n[m>>2]|0):A=m,m=n[o>>2]|0,n[o>>2]=u,n[A>>2]=m,m=l+8|0,d=n[B>>2]|0,n[B>>2]=n[m>>2],n[m>>2]=d,m=o+8|0,B=l+12|0,o=n[m>>2]|0,n[m>>2]=n[B>>2],n[B>>2]=o,n[l>>2]=n[A>>2]}function gWe(o){o=o|0;var l=0,u=0,A=0;l=n[o+4>>2]|0,u=o+8|0,A=n[u>>2]|0,(A|0)!=(l|0)&&(n[u>>2]=A+(~((A+-8-l|0)>>>3)<<3)),o=n[o>>2]|0,o|0&&Et(o)}function _X(o){o=o|0,yWe(o)}function dWe(o){o=o|0,mWe(o+24|0)}function mWe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function yWe(o){o=o|0;var l=0;l=en()|0,tn(o,1,12,l,EWe()|0,2),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function EWe(){return 1896}function IWe(o,l,u){o=o|0,l=l|0,u=u|0,wWe(n[(CWe(o)|0)>>2]|0,l,u)}function CWe(o){return o=o|0,(n[(o_()|0)+24>>2]|0)+(o<<3)|0}function wWe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0;A=I,I=I+16|0,m=A+4|0,d=A,BWe(m,l),l=vWe(m,l)|0,Uh(d,u),u=Hh(d,u)|0,ap[o&31](l,u),jh(d),I=A}function BWe(o,l){o=o|0,l=l|0}function vWe(o,l){return o=o|0,l=l|0,SWe(l)|0}function SWe(o){return o=o|0,o|0}function DWe(){var o=0;return s[8080]|0||(UX(11040),gr(70,11040,U|0)|0,o=8080,n[o>>2]=1,n[o+4>>2]=0),Ur(11040)|0||UX(11040),11040}function UX(o){o=o|0,xWe(o),cd(o,71)}function bWe(o){o=o|0,PWe(o+24|0)}function PWe(o){o=o|0;var l=0,u=0,A=0;u=n[o>>2]|0,A=u,u|0&&(o=o+4|0,l=n[o>>2]|0,(l|0)!=(u|0)&&(n[o>>2]=l+(~((l+-8-A|0)>>>3)<<3)),Et(u))}function xWe(o){o=o|0;var l=0;l=en()|0,tn(o,5,7,l,RWe()|0,0),n[o+24>>2]=0,n[o+28>>2]=0,n[o+32>>2]=0}function kWe(o){o=o|0,QWe(o)}function QWe(o){o=o|0,TWe(o)}function TWe(o){o=o|0,s[o+8>>0]=1}function RWe(){return 1936}function FWe(){return NWe()|0}function NWe(){var o=0,l=0,u=0,A=0,d=0,m=0,B=0;return l=I,I=I+16|0,d=l+4|0,B=l,u=Fl(8)|0,o=u,m=o+4|0,n[m>>2]=Jt(1)|0,A=Jt(8)|0,m=n[m>>2]|0,n[B>>2]=0,n[d>>2]=n[B>>2],OWe(A,m,d),n[u>>2]=A,I=l,o|0}function OWe(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]=l,u=Jt(16)|0,n[u+4>>2]=0,n[u+8>>2]=0,n[u>>2]=1916,n[u+12>>2]=l,n[o+4>>2]=u}function LWe(o){o=o|0,Zy(o),Et(o)}function MWe(o){o=o|0,o=n[o+12>>2]|0,o|0&&Et(o)}function _We(o){o=o|0,Et(o)}function UWe(){var o=0;return s[8088]|0||(VWe(11076),gr(25,11076,U|0)|0,o=8088,n[o>>2]=1,n[o+4>>2]=0),11076}function HWe(o,l){o=o|0,l=l|0,n[o>>2]=jWe()|0,n[o+4>>2]=qWe()|0,n[o+12>>2]=l,n[o+8>>2]=GWe()|0,n[o+32>>2]=10}function jWe(){return 11745}function qWe(){return 1940}function GWe(){return jP()|0}function WWe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,(qh(A,896)|0)==512?u|0&&(YWe(u),Et(u)):l|0&&Et(l)}function YWe(o){o=o|0,o=n[o+4>>2]|0,o|0&&Wh(o)}function VWe(o){o=o|0,_h(o)}function xu(o,l){o=o|0,l=l|0,n[o>>2]=l}function a_(o){return o=o|0,n[o>>2]|0}function KWe(o){return o=o|0,s[n[o>>2]>>0]|0}function JWe(o,l){o=o|0,l=l|0;var u=0,A=0;u=I,I=I+16|0,A=u,n[A>>2]=n[o>>2],zWe(l,A)|0,I=u}function zWe(o,l){o=o|0,l=l|0;var u=0;return u=ZWe(n[o>>2]|0,l)|0,l=o+4|0,n[(n[l>>2]|0)+8>>2]=u,n[(n[l>>2]|0)+8>>2]|0}function ZWe(o,l){o=o|0,l=l|0;var u=0,A=0;return u=I,I=I+16|0,A=u,Nl(A),o=Ms(o)|0,l=XWe(o,n[l>>2]|0)|0,Ol(A),I=u,l|0}function Nl(o){o=o|0,n[o>>2]=n[2701],n[o+4>>2]=n[2703]}function XWe(o,l){o=o|0,l=l|0;var u=0;return u=ma($We()|0)|0,dn(0,u|0,o|0,e_(l)|0)|0}function Ol(o){o=o|0,xX(n[o>>2]|0,n[o+4>>2]|0)}function $We(){var o=0;return s[8096]|0||(eYe(11120),o=8096,n[o>>2]=1,n[o+4>>2]=0),11120}function eYe(o){o=o|0,Ro(o,tYe()|0,1)}function tYe(){return 1948}function rYe(){nYe()}function nYe(){var o=0,l=0,u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0,Le=0,Qe=0;if(Le=I,I=I+16|0,M=Le+4|0,G=Le,aa(65536,10804,n[2702]|0,10812),u=uX()|0,l=n[u>>2]|0,o=n[l>>2]|0,o|0)for(A=n[u+8>>2]|0,u=n[u+4>>2]|0;hf(o|0,c[u>>0]|0|0,s[A>>0]|0),l=l+4|0,o=n[l>>2]|0,o;)A=A+1|0,u=u+1|0;if(o=fX()|0,l=n[o>>2]|0,l|0)do LA(l|0,n[o+4>>2]|0),o=o+8|0,l=n[o>>2]|0;while(l|0);LA(iYe()|0,5167),_=Yy()|0,o=n[_>>2]|0;e:do if(o|0){do sYe(n[o+4>>2]|0),o=n[o>>2]|0;while(o|0);if(o=n[_>>2]|0,o|0){T=_;do{for(;d=o,o=n[o>>2]|0,d=n[d+4>>2]|0,!!(oYe(d)|0);)if(n[G>>2]=T,n[M>>2]=n[G>>2],aYe(_,M)|0,!o)break e;if(lYe(d),T=n[T>>2]|0,l=HX(d)|0,m=Li()|0,B=I,I=I+((1*(l<<2)|0)+15&-16)|0,k=I,I=I+((1*(l<<2)|0)+15&-16)|0,l=n[(BX(d)|0)>>2]|0,l|0)for(u=B,A=k;n[u>>2]=n[(Vy(n[l+4>>2]|0)|0)>>2],n[A>>2]=n[l+8>>2],l=n[l>>2]|0,l;)u=u+4|0,A=A+4|0;Qe=Vy(d)|0,l=cYe(d)|0,u=HX(d)|0,A=uYe(d)|0,ac(Qe|0,l|0,B|0,k|0,u|0,A|0,KM(d)|0),OA(m|0)}while(o|0)}}while(!1);if(o=n[(JM()|0)>>2]|0,o|0)do Qe=o+4|0,_=zM(Qe)|0,d=k2(_)|0,m=P2(_)|0,B=(x2(_)|0)+1|0,k=VP(_)|0,T=jX(Qe)|0,_=Ur(_)|0,M=GP(Qe)|0,G=l_(Qe)|0,Au(0,d|0,m|0,B|0,k|0,T|0,_|0,M|0,G|0,c_(Qe)|0),o=n[o>>2]|0;while(o|0);o=n[(Yy()|0)>>2]|0;e:do if(o|0){t:for(;;){if(l=n[o+4>>2]|0,l|0&&(ae=n[(Vy(l)|0)>>2]|0,We=n[(vX(l)|0)>>2]|0,We|0)){u=We;do{l=u+4|0,A=zM(l)|0;r:do if(A|0)switch(Ur(A)|0){case 0:break t;case 4:case 3:case 2:{k=k2(A)|0,T=P2(A)|0,_=(x2(A)|0)+1|0,M=VP(A)|0,G=Ur(A)|0,Qe=GP(l)|0,Au(ae|0,k|0,T|0,_|0,M|0,0,G|0,Qe|0,l_(l)|0,c_(l)|0);break r}case 1:{B=k2(A)|0,k=P2(A)|0,T=(x2(A)|0)+1|0,_=VP(A)|0,M=jX(l)|0,G=Ur(A)|0,Qe=GP(l)|0,Au(ae|0,B|0,k|0,T|0,_|0,M|0,G|0,Qe|0,l_(l)|0,c_(l)|0);break r}case 5:{_=k2(A)|0,M=P2(A)|0,G=(x2(A)|0)+1|0,Qe=VP(A)|0,Au(ae|0,_|0,M|0,G|0,Qe|0,fYe(A)|0,Ur(A)|0,0,0,0);break r}default:break r}while(!1);u=n[u>>2]|0}while(u|0)}if(o=n[o>>2]|0,!o)break e}Nt()}while(!1);ve(),I=Le}function iYe(){return 11703}function sYe(o){o=o|0,s[o+40>>0]=0}function oYe(o){return o=o|0,(s[o+40>>0]|0)!=0|0}function aYe(o,l){return o=o|0,l=l|0,l=AYe(l)|0,o=n[l>>2]|0,n[l>>2]=n[o>>2],Et(o),n[l>>2]|0}function lYe(o){o=o|0,s[o+40>>0]=1}function HX(o){return o=o|0,n[o+20>>2]|0}function cYe(o){return o=o|0,n[o+8>>2]|0}function uYe(o){return o=o|0,n[o+32>>2]|0}function VP(o){return o=o|0,n[o+4>>2]|0}function jX(o){return o=o|0,n[o+4>>2]|0}function l_(o){return o=o|0,n[o+8>>2]|0}function c_(o){return o=o|0,n[o+16>>2]|0}function fYe(o){return o=o|0,n[o+20>>2]|0}function AYe(o){return o=o|0,n[o>>2]|0}function KP(o){o=o|0;var l=0,u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0,Le=0,Qe=0,tt=0,Ze=0,ct=0,He=0,Ge=0,Lt=0;Lt=I,I=I+16|0,ae=Lt;do if(o>>>0<245){if(_=o>>>0<11?16:o+11&-8,o=_>>>3,G=n[2783]|0,u=G>>>o,u&3|0)return l=(u&1^1)+o|0,o=11172+(l<<1<<2)|0,u=o+8|0,A=n[u>>2]|0,d=A+8|0,m=n[d>>2]|0,(o|0)==(m|0)?n[2783]=G&~(1<>2]=o,n[u>>2]=m),Ge=l<<3,n[A+4>>2]=Ge|3,Ge=A+Ge+4|0,n[Ge>>2]=n[Ge>>2]|1,Ge=d,I=Lt,Ge|0;if(M=n[2785]|0,_>>>0>M>>>0){if(u|0)return l=2<>>12&16,l=l>>>B,u=l>>>5&8,l=l>>>u,d=l>>>2&4,l=l>>>d,o=l>>>1&2,l=l>>>o,A=l>>>1&1,A=(u|B|d|o|A)+(l>>>A)|0,l=11172+(A<<1<<2)|0,o=l+8|0,d=n[o>>2]|0,B=d+8|0,u=n[B>>2]|0,(l|0)==(u|0)?(o=G&~(1<>2]=l,n[o>>2]=u,o=G),m=(A<<3)-_|0,n[d+4>>2]=_|3,A=d+_|0,n[A+4>>2]=m|1,n[A+m>>2]=m,M|0&&(d=n[2788]|0,l=M>>>3,u=11172+(l<<1<<2)|0,l=1<>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=d,n[l+12>>2]=d,n[d+8>>2]=l,n[d+12>>2]=u),n[2785]=m,n[2788]=A,Ge=B,I=Lt,Ge|0;if(k=n[2784]|0,k){if(u=(k&0-k)+-1|0,B=u>>>12&16,u=u>>>B,m=u>>>5&8,u=u>>>m,T=u>>>2&4,u=u>>>T,A=u>>>1&2,u=u>>>A,o=u>>>1&1,o=n[11436+((m|B|T|A|o)+(u>>>o)<<2)>>2]|0,u=(n[o+4>>2]&-8)-_|0,A=n[o+16+(((n[o+16>>2]|0)==0&1)<<2)>>2]|0,!A)T=o,m=u;else{do B=(n[A+4>>2]&-8)-_|0,T=B>>>0>>0,u=T?B:u,o=T?A:o,A=n[A+16+(((n[A+16>>2]|0)==0&1)<<2)>>2]|0;while(A|0);T=o,m=u}if(B=T+_|0,T>>>0>>0){d=n[T+24>>2]|0,l=n[T+12>>2]|0;do if((l|0)==(T|0)){if(o=T+20|0,l=n[o>>2]|0,!l&&(o=T+16|0,l=n[o>>2]|0,!l)){u=0;break}for(;;){if(u=l+20|0,A=n[u>>2]|0,A|0){l=A,o=u;continue}if(u=l+16|0,A=n[u>>2]|0,A)l=A,o=u;else break}n[o>>2]=0,u=l}else u=n[T+8>>2]|0,n[u+12>>2]=l,n[l+8>>2]=u,u=l;while(!1);do if(d|0){if(l=n[T+28>>2]|0,o=11436+(l<<2)|0,(T|0)==(n[o>>2]|0)){if(n[o>>2]=u,!u){n[2784]=k&~(1<>2]|0)!=(T|0)&1)<<2)>>2]=u,!u)break;n[u+24>>2]=d,l=n[T+16>>2]|0,l|0&&(n[u+16>>2]=l,n[l+24>>2]=u),l=n[T+20>>2]|0,l|0&&(n[u+20>>2]=l,n[l+24>>2]=u)}while(!1);return m>>>0<16?(Ge=m+_|0,n[T+4>>2]=Ge|3,Ge=T+Ge+4|0,n[Ge>>2]=n[Ge>>2]|1):(n[T+4>>2]=_|3,n[B+4>>2]=m|1,n[B+m>>2]=m,M|0&&(A=n[2788]|0,l=M>>>3,u=11172+(l<<1<<2)|0,l=1<>2]|0):(n[2783]=G|l,l=u,o=u+8|0),n[o>>2]=A,n[l+12>>2]=A,n[A+8>>2]=l,n[A+12>>2]=u),n[2785]=m,n[2788]=B),Ge=T+8|0,I=Lt,Ge|0}else G=_}else G=_}else G=_}else if(o>>>0<=4294967231)if(o=o+11|0,_=o&-8,T=n[2784]|0,T){A=0-_|0,o=o>>>8,o?_>>>0>16777215?k=31:(G=(o+1048320|0)>>>16&8,He=o<>>16&4,He=He<>>16&2,k=14-(M|G|k)+(He<>>15)|0,k=_>>>(k+7|0)&1|k<<1):k=0,u=n[11436+(k<<2)>>2]|0;e:do if(!u)u=0,o=0,He=57;else for(o=0,B=_<<((k|0)==31?0:25-(k>>>1)|0),m=0;;){if(d=(n[u+4>>2]&-8)-_|0,d>>>0>>0)if(d)o=u,A=d;else{o=u,A=0,d=u,He=61;break e}if(d=n[u+20>>2]|0,u=n[u+16+(B>>>31<<2)>>2]|0,m=(d|0)==0|(d|0)==(u|0)?m:d,d=(u|0)==0,d){u=m,He=57;break}else B=B<<((d^1)&1)}while(!1);if((He|0)==57){if((u|0)==0&(o|0)==0){if(o=2<>>12&16,G=G>>>B,m=G>>>5&8,G=G>>>m,k=G>>>2&4,G=G>>>k,M=G>>>1&2,G=G>>>M,u=G>>>1&1,o=0,u=n[11436+((m|B|k|M|u)+(G>>>u)<<2)>>2]|0}u?(d=u,He=61):(k=o,B=A)}if((He|0)==61)for(;;)if(He=0,u=(n[d+4>>2]&-8)-_|0,G=u>>>0>>0,u=G?u:A,o=G?d:o,d=n[d+16+(((n[d+16>>2]|0)==0&1)<<2)>>2]|0,d)A=u,He=61;else{k=o,B=u;break}if(k|0&&B>>>0<((n[2785]|0)-_|0)>>>0){if(m=k+_|0,k>>>0>=m>>>0)return Ge=0,I=Lt,Ge|0;d=n[k+24>>2]|0,l=n[k+12>>2]|0;do if((l|0)==(k|0)){if(o=k+20|0,l=n[o>>2]|0,!l&&(o=k+16|0,l=n[o>>2]|0,!l)){l=0;break}for(;;){if(u=l+20|0,A=n[u>>2]|0,A|0){l=A,o=u;continue}if(u=l+16|0,A=n[u>>2]|0,A)l=A,o=u;else break}n[o>>2]=0}else Ge=n[k+8>>2]|0,n[Ge+12>>2]=l,n[l+8>>2]=Ge;while(!1);do if(d){if(o=n[k+28>>2]|0,u=11436+(o<<2)|0,(k|0)==(n[u>>2]|0)){if(n[u>>2]=l,!l){A=T&~(1<>2]|0)!=(k|0)&1)<<2)>>2]=l,!l){A=T;break}n[l+24>>2]=d,o=n[k+16>>2]|0,o|0&&(n[l+16>>2]=o,n[o+24>>2]=l),o=n[k+20>>2]|0,o&&(n[l+20>>2]=o,n[o+24>>2]=l),A=T}else A=T;while(!1);do if(B>>>0>=16){if(n[k+4>>2]=_|3,n[m+4>>2]=B|1,n[m+B>>2]=B,l=B>>>3,B>>>0<256){u=11172+(l<<1<<2)|0,o=n[2783]|0,l=1<>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=m,n[l+12>>2]=m,n[m+8>>2]=l,n[m+12>>2]=u;break}if(l=B>>>8,l?B>>>0>16777215?l=31:(He=(l+1048320|0)>>>16&8,Ge=l<>>16&4,Ge=Ge<>>16&2,l=14-(ct|He|l)+(Ge<>>15)|0,l=B>>>(l+7|0)&1|l<<1):l=0,u=11436+(l<<2)|0,n[m+28>>2]=l,o=m+16|0,n[o+4>>2]=0,n[o>>2]=0,o=1<>2]=m,n[m+24>>2]=u,n[m+12>>2]=m,n[m+8>>2]=m;break}for(o=B<<((l|0)==31?0:25-(l>>>1)|0),u=n[u>>2]|0;;){if((n[u+4>>2]&-8|0)==(B|0)){He=97;break}if(A=u+16+(o>>>31<<2)|0,l=n[A>>2]|0,l)o=o<<1,u=l;else{He=96;break}}if((He|0)==96){n[A>>2]=m,n[m+24>>2]=u,n[m+12>>2]=m,n[m+8>>2]=m;break}else if((He|0)==97){He=u+8|0,Ge=n[He>>2]|0,n[Ge+12>>2]=m,n[He>>2]=m,n[m+8>>2]=Ge,n[m+12>>2]=u,n[m+24>>2]=0;break}}else Ge=B+_|0,n[k+4>>2]=Ge|3,Ge=k+Ge+4|0,n[Ge>>2]=n[Ge>>2]|1;while(!1);return Ge=k+8|0,I=Lt,Ge|0}else G=_}else G=_;else G=-1;while(!1);if(u=n[2785]|0,u>>>0>=G>>>0)return l=u-G|0,o=n[2788]|0,l>>>0>15?(Ge=o+G|0,n[2788]=Ge,n[2785]=l,n[Ge+4>>2]=l|1,n[Ge+l>>2]=l,n[o+4>>2]=G|3):(n[2785]=0,n[2788]=0,n[o+4>>2]=u|3,Ge=o+u+4|0,n[Ge>>2]=n[Ge>>2]|1),Ge=o+8|0,I=Lt,Ge|0;if(B=n[2786]|0,B>>>0>G>>>0)return ct=B-G|0,n[2786]=ct,Ge=n[2789]|0,He=Ge+G|0,n[2789]=He,n[He+4>>2]=ct|1,n[Ge+4>>2]=G|3,Ge=Ge+8|0,I=Lt,Ge|0;if(n[2901]|0?o=n[2903]|0:(n[2903]=4096,n[2902]=4096,n[2904]=-1,n[2905]=-1,n[2906]=0,n[2894]=0,o=ae&-16^1431655768,n[ae>>2]=o,n[2901]=o,o=4096),k=G+48|0,T=G+47|0,m=o+T|0,d=0-o|0,_=m&d,_>>>0<=G>>>0||(o=n[2893]|0,o|0&&(M=n[2891]|0,ae=M+_|0,ae>>>0<=M>>>0|ae>>>0>o>>>0)))return Ge=0,I=Lt,Ge|0;e:do if(n[2894]&4)l=0,He=133;else{u=n[2789]|0;t:do if(u){for(A=11580;o=n[A>>2]|0,!(o>>>0<=u>>>0&&(Qe=A+4|0,(o+(n[Qe>>2]|0)|0)>>>0>u>>>0));)if(o=n[A+8>>2]|0,o)A=o;else{He=118;break t}if(l=m-B&d,l>>>0<2147483647)if(o=Yh(l|0)|0,(o|0)==((n[A>>2]|0)+(n[Qe>>2]|0)|0)){if((o|0)!=-1){B=l,m=o,He=135;break e}}else A=o,He=126;else l=0}else He=118;while(!1);do if((He|0)==118)if(u=Yh(0)|0,(u|0)!=-1&&(l=u,We=n[2902]|0,Le=We+-1|0,l=(Le&l|0?(Le+l&0-We)-l|0:0)+_|0,We=n[2891]|0,Le=l+We|0,l>>>0>G>>>0&l>>>0<2147483647)){if(Qe=n[2893]|0,Qe|0&&Le>>>0<=We>>>0|Le>>>0>Qe>>>0){l=0;break}if(o=Yh(l|0)|0,(o|0)==(u|0)){B=l,m=u,He=135;break e}else A=o,He=126}else l=0;while(!1);do if((He|0)==126){if(u=0-l|0,!(k>>>0>l>>>0&(l>>>0<2147483647&(A|0)!=-1)))if((A|0)==-1){l=0;break}else{B=l,m=A,He=135;break e}if(o=n[2903]|0,o=T-l+o&0-o,o>>>0>=2147483647){B=l,m=A,He=135;break e}if((Yh(o|0)|0)==-1){Yh(u|0)|0,l=0;break}else{B=o+l|0,m=A,He=135;break e}}while(!1);n[2894]=n[2894]|4,He=133}while(!1);if((He|0)==133&&_>>>0<2147483647&&(ct=Yh(_|0)|0,Qe=Yh(0)|0,tt=Qe-ct|0,Ze=tt>>>0>(G+40|0)>>>0,!((ct|0)==-1|Ze^1|ct>>>0>>0&((ct|0)!=-1&(Qe|0)!=-1)^1))&&(B=Ze?tt:l,m=ct,He=135),(He|0)==135){l=(n[2891]|0)+B|0,n[2891]=l,l>>>0>(n[2892]|0)>>>0&&(n[2892]=l),T=n[2789]|0;do if(T){for(l=11580;;){if(o=n[l>>2]|0,u=l+4|0,A=n[u>>2]|0,(m|0)==(o+A|0)){He=145;break}if(d=n[l+8>>2]|0,d)l=d;else break}if((He|0)==145&&!(n[l+12>>2]&8|0)&&T>>>0>>0&T>>>0>=o>>>0){n[u>>2]=A+B,Ge=T+8|0,Ge=Ge&7|0?0-Ge&7:0,He=T+Ge|0,Ge=(n[2786]|0)+(B-Ge)|0,n[2789]=He,n[2786]=Ge,n[He+4>>2]=Ge|1,n[He+Ge+4>>2]=40,n[2790]=n[2905];break}for(m>>>0<(n[2787]|0)>>>0&&(n[2787]=m),u=m+B|0,l=11580;;){if((n[l>>2]|0)==(u|0)){He=153;break}if(o=n[l+8>>2]|0,o)l=o;else break}if((He|0)==153&&!(n[l+12>>2]&8|0)){n[l>>2]=m,M=l+4|0,n[M>>2]=(n[M>>2]|0)+B,M=m+8|0,M=m+(M&7|0?0-M&7:0)|0,l=u+8|0,l=u+(l&7|0?0-l&7:0)|0,_=M+G|0,k=l-M-G|0,n[M+4>>2]=G|3;do if((l|0)!=(T|0)){if((l|0)==(n[2788]|0)){Ge=(n[2785]|0)+k|0,n[2785]=Ge,n[2788]=_,n[_+4>>2]=Ge|1,n[_+Ge>>2]=Ge;break}if(o=n[l+4>>2]|0,(o&3|0)==1){B=o&-8,A=o>>>3;e:do if(o>>>0<256)if(o=n[l+8>>2]|0,u=n[l+12>>2]|0,(u|0)==(o|0)){n[2783]=n[2783]&~(1<>2]=u,n[u+8>>2]=o;break}else{m=n[l+24>>2]|0,o=n[l+12>>2]|0;do if((o|0)==(l|0)){if(A=l+16|0,u=A+4|0,o=n[u>>2]|0,!o)if(o=n[A>>2]|0,o)u=A;else{o=0;break}for(;;){if(A=o+20|0,d=n[A>>2]|0,d|0){o=d,u=A;continue}if(A=o+16|0,d=n[A>>2]|0,d)o=d,u=A;else break}n[u>>2]=0}else Ge=n[l+8>>2]|0,n[Ge+12>>2]=o,n[o+8>>2]=Ge;while(!1);if(!m)break;u=n[l+28>>2]|0,A=11436+(u<<2)|0;do if((l|0)!=(n[A>>2]|0)){if(n[m+16+(((n[m+16>>2]|0)!=(l|0)&1)<<2)>>2]=o,!o)break e}else{if(n[A>>2]=o,o|0)break;n[2784]=n[2784]&~(1<>2]=m,u=l+16|0,A=n[u>>2]|0,A|0&&(n[o+16>>2]=A,n[A+24>>2]=o),u=n[u+4>>2]|0,!u)break;n[o+20>>2]=u,n[u+24>>2]=o}while(!1);l=l+B|0,d=B+k|0}else d=k;if(l=l+4|0,n[l>>2]=n[l>>2]&-2,n[_+4>>2]=d|1,n[_+d>>2]=d,l=d>>>3,d>>>0<256){u=11172+(l<<1<<2)|0,o=n[2783]|0,l=1<>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=_,n[l+12>>2]=_,n[_+8>>2]=l,n[_+12>>2]=u;break}l=d>>>8;do if(!l)l=0;else{if(d>>>0>16777215){l=31;break}He=(l+1048320|0)>>>16&8,Ge=l<>>16&4,Ge=Ge<>>16&2,l=14-(ct|He|l)+(Ge<>>15)|0,l=d>>>(l+7|0)&1|l<<1}while(!1);if(A=11436+(l<<2)|0,n[_+28>>2]=l,o=_+16|0,n[o+4>>2]=0,n[o>>2]=0,o=n[2784]|0,u=1<>2]=_,n[_+24>>2]=A,n[_+12>>2]=_,n[_+8>>2]=_;break}for(o=d<<((l|0)==31?0:25-(l>>>1)|0),u=n[A>>2]|0;;){if((n[u+4>>2]&-8|0)==(d|0)){He=194;break}if(A=u+16+(o>>>31<<2)|0,l=n[A>>2]|0,l)o=o<<1,u=l;else{He=193;break}}if((He|0)==193){n[A>>2]=_,n[_+24>>2]=u,n[_+12>>2]=_,n[_+8>>2]=_;break}else if((He|0)==194){He=u+8|0,Ge=n[He>>2]|0,n[Ge+12>>2]=_,n[He>>2]=_,n[_+8>>2]=Ge,n[_+12>>2]=u,n[_+24>>2]=0;break}}else Ge=(n[2786]|0)+k|0,n[2786]=Ge,n[2789]=_,n[_+4>>2]=Ge|1;while(!1);return Ge=M+8|0,I=Lt,Ge|0}for(l=11580;o=n[l>>2]|0,!(o>>>0<=T>>>0&&(Ge=o+(n[l+4>>2]|0)|0,Ge>>>0>T>>>0));)l=n[l+8>>2]|0;d=Ge+-47|0,o=d+8|0,o=d+(o&7|0?0-o&7:0)|0,d=T+16|0,o=o>>>0>>0?T:o,l=o+8|0,u=m+8|0,u=u&7|0?0-u&7:0,He=m+u|0,u=B+-40-u|0,n[2789]=He,n[2786]=u,n[He+4>>2]=u|1,n[He+u+4>>2]=40,n[2790]=n[2905],u=o+4|0,n[u>>2]=27,n[l>>2]=n[2895],n[l+4>>2]=n[2896],n[l+8>>2]=n[2897],n[l+12>>2]=n[2898],n[2895]=m,n[2896]=B,n[2898]=0,n[2897]=l,l=o+24|0;do He=l,l=l+4|0,n[l>>2]=7;while((He+8|0)>>>0>>0);if((o|0)!=(T|0)){if(m=o-T|0,n[u>>2]=n[u>>2]&-2,n[T+4>>2]=m|1,n[o>>2]=m,l=m>>>3,m>>>0<256){u=11172+(l<<1<<2)|0,o=n[2783]|0,l=1<>2]|0):(n[2783]=o|l,l=u,o=u+8|0),n[o>>2]=T,n[l+12>>2]=T,n[T+8>>2]=l,n[T+12>>2]=u;break}if(l=m>>>8,l?m>>>0>16777215?u=31:(He=(l+1048320|0)>>>16&8,Ge=l<>>16&4,Ge=Ge<>>16&2,u=14-(ct|He|u)+(Ge<>>15)|0,u=m>>>(u+7|0)&1|u<<1):u=0,A=11436+(u<<2)|0,n[T+28>>2]=u,n[T+20>>2]=0,n[d>>2]=0,l=n[2784]|0,o=1<>2]=T,n[T+24>>2]=A,n[T+12>>2]=T,n[T+8>>2]=T;break}for(o=m<<((u|0)==31?0:25-(u>>>1)|0),u=n[A>>2]|0;;){if((n[u+4>>2]&-8|0)==(m|0)){He=216;break}if(A=u+16+(o>>>31<<2)|0,l=n[A>>2]|0,l)o=o<<1,u=l;else{He=215;break}}if((He|0)==215){n[A>>2]=T,n[T+24>>2]=u,n[T+12>>2]=T,n[T+8>>2]=T;break}else if((He|0)==216){He=u+8|0,Ge=n[He>>2]|0,n[Ge+12>>2]=T,n[He>>2]=T,n[T+8>>2]=Ge,n[T+12>>2]=u,n[T+24>>2]=0;break}}}else{Ge=n[2787]|0,(Ge|0)==0|m>>>0>>0&&(n[2787]=m),n[2895]=m,n[2896]=B,n[2898]=0,n[2792]=n[2901],n[2791]=-1,l=0;do Ge=11172+(l<<1<<2)|0,n[Ge+12>>2]=Ge,n[Ge+8>>2]=Ge,l=l+1|0;while((l|0)!=32);Ge=m+8|0,Ge=Ge&7|0?0-Ge&7:0,He=m+Ge|0,Ge=B+-40-Ge|0,n[2789]=He,n[2786]=Ge,n[He+4>>2]=Ge|1,n[He+Ge+4>>2]=40,n[2790]=n[2905]}while(!1);if(l=n[2786]|0,l>>>0>G>>>0)return ct=l-G|0,n[2786]=ct,Ge=n[2789]|0,He=Ge+G|0,n[2789]=He,n[He+4>>2]=ct|1,n[Ge+4>>2]=G|3,Ge=Ge+8|0,I=Lt,Ge|0}return n[(Jy()|0)>>2]=12,Ge=0,I=Lt,Ge|0}function JP(o){o=o|0;var l=0,u=0,A=0,d=0,m=0,B=0,k=0,T=0;if(o){u=o+-8|0,d=n[2787]|0,o=n[o+-4>>2]|0,l=o&-8,T=u+l|0;do if(o&1)k=u,B=u;else{if(A=n[u>>2]|0,!(o&3)||(B=u+(0-A)|0,m=A+l|0,B>>>0>>0))return;if((B|0)==(n[2788]|0)){if(o=T+4|0,l=n[o>>2]|0,(l&3|0)!=3){k=B,l=m;break}n[2785]=m,n[o>>2]=l&-2,n[B+4>>2]=m|1,n[B+m>>2]=m;return}if(u=A>>>3,A>>>0<256)if(o=n[B+8>>2]|0,l=n[B+12>>2]|0,(l|0)==(o|0)){n[2783]=n[2783]&~(1<>2]=l,n[l+8>>2]=o,k=B,l=m;break}d=n[B+24>>2]|0,o=n[B+12>>2]|0;do if((o|0)==(B|0)){if(u=B+16|0,l=u+4|0,o=n[l>>2]|0,!o)if(o=n[u>>2]|0,o)l=u;else{o=0;break}for(;;){if(u=o+20|0,A=n[u>>2]|0,A|0){o=A,l=u;continue}if(u=o+16|0,A=n[u>>2]|0,A)o=A,l=u;else break}n[l>>2]=0}else k=n[B+8>>2]|0,n[k+12>>2]=o,n[o+8>>2]=k;while(!1);if(d){if(l=n[B+28>>2]|0,u=11436+(l<<2)|0,(B|0)==(n[u>>2]|0)){if(n[u>>2]=o,!o){n[2784]=n[2784]&~(1<>2]|0)!=(B|0)&1)<<2)>>2]=o,!o){k=B,l=m;break}n[o+24>>2]=d,l=B+16|0,u=n[l>>2]|0,u|0&&(n[o+16>>2]=u,n[u+24>>2]=o),l=n[l+4>>2]|0,l?(n[o+20>>2]=l,n[l+24>>2]=o,k=B,l=m):(k=B,l=m)}else k=B,l=m}while(!1);if(!(B>>>0>=T>>>0)&&(o=T+4|0,A=n[o>>2]|0,!!(A&1))){if(A&2)n[o>>2]=A&-2,n[k+4>>2]=l|1,n[B+l>>2]=l,d=l;else{if(o=n[2788]|0,(T|0)==(n[2789]|0)){if(T=(n[2786]|0)+l|0,n[2786]=T,n[2789]=k,n[k+4>>2]=T|1,(k|0)!=(o|0))return;n[2788]=0,n[2785]=0;return}if((T|0)==(o|0)){T=(n[2785]|0)+l|0,n[2785]=T,n[2788]=B,n[k+4>>2]=T|1,n[B+T>>2]=T;return}d=(A&-8)+l|0,u=A>>>3;do if(A>>>0<256)if(l=n[T+8>>2]|0,o=n[T+12>>2]|0,(o|0)==(l|0)){n[2783]=n[2783]&~(1<>2]=o,n[o+8>>2]=l;break}else{m=n[T+24>>2]|0,o=n[T+12>>2]|0;do if((o|0)==(T|0)){if(u=T+16|0,l=u+4|0,o=n[l>>2]|0,!o)if(o=n[u>>2]|0,o)l=u;else{u=0;break}for(;;){if(u=o+20|0,A=n[u>>2]|0,A|0){o=A,l=u;continue}if(u=o+16|0,A=n[u>>2]|0,A)o=A,l=u;else break}n[l>>2]=0,u=o}else u=n[T+8>>2]|0,n[u+12>>2]=o,n[o+8>>2]=u,u=o;while(!1);if(m|0){if(o=n[T+28>>2]|0,l=11436+(o<<2)|0,(T|0)==(n[l>>2]|0)){if(n[l>>2]=u,!u){n[2784]=n[2784]&~(1<>2]|0)!=(T|0)&1)<<2)>>2]=u,!u)break;n[u+24>>2]=m,o=T+16|0,l=n[o>>2]|0,l|0&&(n[u+16>>2]=l,n[l+24>>2]=u),o=n[o+4>>2]|0,o|0&&(n[u+20>>2]=o,n[o+24>>2]=u)}}while(!1);if(n[k+4>>2]=d|1,n[B+d>>2]=d,(k|0)==(n[2788]|0)){n[2785]=d;return}}if(o=d>>>3,d>>>0<256){u=11172+(o<<1<<2)|0,l=n[2783]|0,o=1<>2]|0):(n[2783]=l|o,o=u,l=u+8|0),n[l>>2]=k,n[o+12>>2]=k,n[k+8>>2]=o,n[k+12>>2]=u;return}o=d>>>8,o?d>>>0>16777215?o=31:(B=(o+1048320|0)>>>16&8,T=o<>>16&4,T=T<>>16&2,o=14-(m|B|o)+(T<>>15)|0,o=d>>>(o+7|0)&1|o<<1):o=0,A=11436+(o<<2)|0,n[k+28>>2]=o,n[k+20>>2]=0,n[k+16>>2]=0,l=n[2784]|0,u=1<>>1)|0),u=n[A>>2]|0;;){if((n[u+4>>2]&-8|0)==(d|0)){o=73;break}if(A=u+16+(l>>>31<<2)|0,o=n[A>>2]|0,o)l=l<<1,u=o;else{o=72;break}}if((o|0)==72){n[A>>2]=k,n[k+24>>2]=u,n[k+12>>2]=k,n[k+8>>2]=k;break}else if((o|0)==73){B=u+8|0,T=n[B>>2]|0,n[T+12>>2]=k,n[B>>2]=k,n[k+8>>2]=T,n[k+12>>2]=u,n[k+24>>2]=0;break}}else n[2784]=l|u,n[A>>2]=k,n[k+24>>2]=A,n[k+12>>2]=k,n[k+8>>2]=k;while(!1);if(T=(n[2791]|0)+-1|0,n[2791]=T,!T)o=11588;else return;for(;o=n[o>>2]|0,o;)o=o+8|0;n[2791]=-1}}}function pYe(){return 11628}function hYe(o){o=o|0;var l=0,u=0;return l=I,I=I+16|0,u=l,n[u>>2]=mYe(n[o+60>>2]|0)|0,o=zP(hu(6,u|0)|0)|0,I=l,o|0}function qX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0;G=I,I=I+48|0,_=G+16|0,m=G,d=G+32|0,k=o+28|0,A=n[k>>2]|0,n[d>>2]=A,T=o+20|0,A=(n[T>>2]|0)-A|0,n[d+4>>2]=A,n[d+8>>2]=l,n[d+12>>2]=u,A=A+u|0,B=o+60|0,n[m>>2]=n[B>>2],n[m+4>>2]=d,n[m+8>>2]=2,m=zP(Ma(146,m|0)|0)|0;e:do if((A|0)!=(m|0)){for(l=2;!((m|0)<0);)if(A=A-m|0,We=n[d+4>>2]|0,ae=m>>>0>We>>>0,d=ae?d+8|0:d,l=(ae<<31>>31)+l|0,We=m-(ae?We:0)|0,n[d>>2]=(n[d>>2]|0)+We,ae=d+4|0,n[ae>>2]=(n[ae>>2]|0)-We,n[_>>2]=n[B>>2],n[_+4>>2]=d,n[_+8>>2]=l,m=zP(Ma(146,_|0)|0)|0,(A|0)==(m|0)){M=3;break e}n[o+16>>2]=0,n[k>>2]=0,n[T>>2]=0,n[o>>2]=n[o>>2]|32,(l|0)==2?u=0:u=u-(n[d+4>>2]|0)|0}else M=3;while(!1);return(M|0)==3&&(We=n[o+44>>2]|0,n[o+16>>2]=We+(n[o+48>>2]|0),n[k>>2]=We,n[T>>2]=We),I=G,u|0}function gYe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0;return d=I,I=I+32|0,m=d,A=d+20|0,n[m>>2]=n[o+60>>2],n[m+4>>2]=0,n[m+8>>2]=l,n[m+12>>2]=A,n[m+16>>2]=u,(zP(La(140,m|0)|0)|0)<0?(n[A>>2]=-1,o=-1):o=n[A>>2]|0,I=d,o|0}function zP(o){return o=o|0,o>>>0>4294963200&&(n[(Jy()|0)>>2]=0-o,o=-1),o|0}function Jy(){return(dYe()|0)+64|0}function dYe(){return u_()|0}function u_(){return 2084}function mYe(o){return o=o|0,o|0}function yYe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0;return d=I,I=I+32|0,A=d,n[o+36>>2]=1,!(n[o>>2]&64|0)&&(n[A>>2]=n[o+60>>2],n[A+4>>2]=21523,n[A+8>>2]=d+16,so(54,A|0)|0)&&(s[o+75>>0]=-1),A=qX(o,l,u)|0,I=d,A|0}function GX(o,l){o=o|0,l=l|0;var u=0,A=0;if(u=s[o>>0]|0,A=s[l>>0]|0,!(u<<24>>24)||u<<24>>24!=A<<24>>24)o=A;else{do o=o+1|0,l=l+1|0,u=s[o>>0]|0,A=s[l>>0]|0;while(!(!(u<<24>>24)||u<<24>>24!=A<<24>>24));o=A}return(u&255)-(o&255)|0}function EYe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0;e:do if(!u)o=0;else{for(;A=s[o>>0]|0,d=s[l>>0]|0,A<<24>>24==d<<24>>24;)if(u=u+-1|0,u)o=o+1|0,l=l+1|0;else{o=0;break e}o=(A&255)-(d&255)|0}while(!1);return o|0}function WX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0,Le=0,Qe=0;Qe=I,I=I+224|0,M=Qe+120|0,G=Qe+80|0,We=Qe,Le=Qe+136|0,A=G,d=A+40|0;do n[A>>2]=0,A=A+4|0;while((A|0)<(d|0));return n[M>>2]=n[u>>2],(f_(0,l,M,We,G)|0)<0?u=-1:((n[o+76>>2]|0)>-1?ae=IYe(o)|0:ae=0,u=n[o>>2]|0,_=u&32,(s[o+74>>0]|0)<1&&(n[o>>2]=u&-33),A=o+48|0,n[A>>2]|0?u=f_(o,l,M,We,G)|0:(d=o+44|0,m=n[d>>2]|0,n[d>>2]=Le,B=o+28|0,n[B>>2]=Le,k=o+20|0,n[k>>2]=Le,n[A>>2]=80,T=o+16|0,n[T>>2]=Le+80,u=f_(o,l,M,We,G)|0,m&&(ex[n[o+36>>2]&7](o,0,0)|0,u=n[k>>2]|0?u:-1,n[d>>2]=m,n[A>>2]=0,n[T>>2]=0,n[B>>2]=0,n[k>>2]=0)),A=n[o>>2]|0,n[o>>2]=A|_,ae|0&&CYe(o),u=A&32|0?-1:u),I=Qe,u|0}function f_(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0;var m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0,Le=0,Qe=0,tt=0,Ze=0,ct=0,He=0,Ge=0,Lt=0,qr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0;cr=I,I=I+64|0,fr=cr+16|0,$t=cr,Lt=cr+24|0,Tr=cr+8|0,Hr=cr+20|0,n[fr>>2]=l,ct=(o|0)!=0,He=Lt+40|0,Ge=He,Lt=Lt+39|0,qr=Tr+4|0,B=0,m=0,M=0;e:for(;;){do if((m|0)>-1)if((B|0)>(2147483647-m|0)){n[(Jy()|0)>>2]=75,m=-1;break}else{m=B+m|0;break}while(!1);if(B=s[l>>0]|0,B<<24>>24)k=l;else{Ze=87;break}t:for(;;){switch(B<<24>>24){case 37:{B=k,Ze=9;break t}case 0:{B=k;break t}default:}tt=k+1|0,n[fr>>2]=tt,B=s[tt>>0]|0,k=tt}t:do if((Ze|0)==9)for(;;){if(Ze=0,(s[k+1>>0]|0)!=37)break t;if(B=B+1|0,k=k+2|0,n[fr>>2]=k,(s[k>>0]|0)==37)Ze=9;else break}while(!1);if(B=B-l|0,ct&&Ds(o,l,B),B|0){l=k;continue}T=k+1|0,B=(s[T>>0]|0)+-48|0,B>>>0<10?(tt=(s[k+2>>0]|0)==36,Qe=tt?B:-1,M=tt?1:M,T=tt?k+3|0:T):Qe=-1,n[fr>>2]=T,B=s[T>>0]|0,k=(B<<24>>24)+-32|0;t:do if(k>>>0<32)for(_=0,G=B;;){if(B=1<>2]=T,B=s[T>>0]|0,k=(B<<24>>24)+-32|0,k>>>0>=32)break;G=B}else _=0;while(!1);if(B<<24>>24==42){if(k=T+1|0,B=(s[k>>0]|0)+-48|0,B>>>0<10&&(s[T+2>>0]|0)==36)n[d+(B<<2)>>2]=10,B=n[A+((s[k>>0]|0)+-48<<3)>>2]|0,M=1,T=T+3|0;else{if(M|0){m=-1;break}ct?(M=(n[u>>2]|0)+3&-4,B=n[M>>2]|0,n[u>>2]=M+4,M=0,T=k):(B=0,M=0,T=k)}n[fr>>2]=T,tt=(B|0)<0,B=tt?0-B|0:B,_=tt?_|8192:_}else{if(B=YX(fr)|0,(B|0)<0){m=-1;break}T=n[fr>>2]|0}do if((s[T>>0]|0)==46){if((s[T+1>>0]|0)!=42){n[fr>>2]=T+1,k=YX(fr)|0,T=n[fr>>2]|0;break}if(G=T+2|0,k=(s[G>>0]|0)+-48|0,k>>>0<10&&(s[T+3>>0]|0)==36){n[d+(k<<2)>>2]=10,k=n[A+((s[G>>0]|0)+-48<<3)>>2]|0,T=T+4|0,n[fr>>2]=T;break}if(M|0){m=-1;break e}ct?(tt=(n[u>>2]|0)+3&-4,k=n[tt>>2]|0,n[u>>2]=tt+4):k=0,n[fr>>2]=G,T=G}else k=-1;while(!1);for(Le=0;;){if(((s[T>>0]|0)+-65|0)>>>0>57){m=-1;break e}if(tt=T+1|0,n[fr>>2]=tt,G=s[(s[T>>0]|0)+-65+(5178+(Le*58|0))>>0]|0,ae=G&255,(ae+-1|0)>>>0<8)Le=ae,T=tt;else break}if(!(G<<24>>24)){m=-1;break}We=(Qe|0)>-1;do if(G<<24>>24==19)if(We){m=-1;break e}else Ze=49;else{if(We){n[d+(Qe<<2)>>2]=ae,We=A+(Qe<<3)|0,Qe=n[We+4>>2]|0,Ze=$t,n[Ze>>2]=n[We>>2],n[Ze+4>>2]=Qe,Ze=49;break}if(!ct){m=0;break e}VX($t,ae,u)}while(!1);if((Ze|0)==49&&(Ze=0,!ct)){B=0,l=tt;continue}T=s[T>>0]|0,T=(Le|0)!=0&(T&15|0)==3?T&-33:T,We=_&-65537,Qe=_&8192|0?We:_;t:do switch(T|0){case 110:switch((Le&255)<<24>>24){case 0:{n[n[$t>>2]>>2]=m,B=0,l=tt;continue e}case 1:{n[n[$t>>2]>>2]=m,B=0,l=tt;continue e}case 2:{B=n[$t>>2]|0,n[B>>2]=m,n[B+4>>2]=((m|0)<0)<<31>>31,B=0,l=tt;continue e}case 3:{a[n[$t>>2]>>1]=m,B=0,l=tt;continue e}case 4:{s[n[$t>>2]>>0]=m,B=0,l=tt;continue e}case 6:{n[n[$t>>2]>>2]=m,B=0,l=tt;continue e}case 7:{B=n[$t>>2]|0,n[B>>2]=m,n[B+4>>2]=((m|0)<0)<<31>>31,B=0,l=tt;continue e}default:{B=0,l=tt;continue e}}case 112:{T=120,k=k>>>0>8?k:8,l=Qe|8,Ze=61;break}case 88:case 120:{l=Qe,Ze=61;break}case 111:{T=$t,l=n[T>>2]|0,T=n[T+4>>2]|0,ae=BYe(l,T,He)|0,We=Ge-ae|0,_=0,G=5642,k=(Qe&8|0)==0|(k|0)>(We|0)?k:We+1|0,We=Qe,Ze=67;break}case 105:case 100:if(T=$t,l=n[T>>2]|0,T=n[T+4>>2]|0,(T|0)<0){l=ZP(0,0,l|0,T|0)|0,T=ye,_=$t,n[_>>2]=l,n[_+4>>2]=T,_=1,G=5642,Ze=66;break t}else{_=(Qe&2049|0)!=0&1,G=Qe&2048|0?5643:Qe&1|0?5644:5642,Ze=66;break t}case 117:{T=$t,_=0,G=5642,l=n[T>>2]|0,T=n[T+4>>2]|0,Ze=66;break}case 99:{s[Lt>>0]=n[$t>>2],l=Lt,_=0,G=5642,ae=He,T=1,k=We;break}case 109:{T=vYe(n[(Jy()|0)>>2]|0)|0,Ze=71;break}case 115:{T=n[$t>>2]|0,T=T|0?T:5652,Ze=71;break}case 67:{n[Tr>>2]=n[$t>>2],n[qr>>2]=0,n[$t>>2]=Tr,ae=-1,T=Tr,Ze=75;break}case 83:{l=n[$t>>2]|0,k?(ae=k,T=l,Ze=75):(_s(o,32,B,0,Qe),l=0,Ze=84);break}case 65:case 71:case 70:case 69:case 97:case 103:case 102:case 101:{B=DYe(o,+E[$t>>3],B,k,Qe,T)|0,l=tt;continue e}default:_=0,G=5642,ae=He,T=k,k=Qe}while(!1);t:do if((Ze|0)==61)Qe=$t,Le=n[Qe>>2]|0,Qe=n[Qe+4>>2]|0,ae=wYe(Le,Qe,He,T&32)|0,G=(l&8|0)==0|(Le|0)==0&(Qe|0)==0,_=G?0:2,G=G?5642:5642+(T>>4)|0,We=l,l=Le,T=Qe,Ze=67;else if((Ze|0)==66)ae=zy(l,T,He)|0,We=Qe,Ze=67;else if((Ze|0)==71)Ze=0,Qe=SYe(T,0,k)|0,Le=(Qe|0)==0,l=T,_=0,G=5642,ae=Le?T+k|0:Qe,T=Le?k:Qe-T|0,k=We;else if((Ze|0)==75){for(Ze=0,G=T,l=0,k=0;_=n[G>>2]|0,!(!_||(k=KX(Hr,_)|0,(k|0)<0|k>>>0>(ae-l|0)>>>0));)if(l=k+l|0,ae>>>0>l>>>0)G=G+4|0;else break;if((k|0)<0){m=-1;break e}if(_s(o,32,B,l,Qe),!l)l=0,Ze=84;else for(_=0;;){if(k=n[T>>2]|0,!k){Ze=84;break t}if(k=KX(Hr,k)|0,_=k+_|0,(_|0)>(l|0)){Ze=84;break t}if(Ds(o,Hr,k),_>>>0>=l>>>0){Ze=84;break}else T=T+4|0}}while(!1);if((Ze|0)==67)Ze=0,T=(l|0)!=0|(T|0)!=0,Qe=(k|0)!=0|T,T=((T^1)&1)+(Ge-ae)|0,l=Qe?ae:He,ae=He,T=Qe?(k|0)>(T|0)?k:T:k,k=(k|0)>-1?We&-65537:We;else if((Ze|0)==84){Ze=0,_s(o,32,B,l,Qe^8192),B=(B|0)>(l|0)?B:l,l=tt;continue}Le=ae-l|0,We=(T|0)<(Le|0)?Le:T,Qe=We+_|0,B=(B|0)<(Qe|0)?Qe:B,_s(o,32,B,Qe,k),Ds(o,G,_),_s(o,48,B,Qe,k^65536),_s(o,48,We,Le,0),Ds(o,l,Le),_s(o,32,B,Qe,k^8192),l=tt}e:do if((Ze|0)==87&&!o)if(!M)m=0;else{for(m=1;l=n[d+(m<<2)>>2]|0,!!l;)if(VX(A+(m<<3)|0,l,u),m=m+1|0,(m|0)>=10){m=1;break e}for(;;){if(n[d+(m<<2)>>2]|0){m=-1;break e}if(m=m+1|0,(m|0)>=10){m=1;break}}}while(!1);return I=cr,m|0}function IYe(o){return o=o|0,0}function CYe(o){o=o|0}function Ds(o,l,u){o=o|0,l=l|0,u=u|0,n[o>>2]&32||NYe(l,u,o)|0}function YX(o){o=o|0;var l=0,u=0,A=0;if(u=n[o>>2]|0,A=(s[u>>0]|0)+-48|0,A>>>0<10){l=0;do l=A+(l*10|0)|0,u=u+1|0,n[o>>2]=u,A=(s[u>>0]|0)+-48|0;while(A>>>0<10)}else l=0;return l|0}function VX(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0;e:do if(l>>>0<=20)do switch(l|0){case 9:{A=(n[u>>2]|0)+3&-4,l=n[A>>2]|0,n[u>>2]=A+4,n[o>>2]=l;break e}case 10:{A=(n[u>>2]|0)+3&-4,l=n[A>>2]|0,n[u>>2]=A+4,A=o,n[A>>2]=l,n[A+4>>2]=((l|0)<0)<<31>>31;break e}case 11:{A=(n[u>>2]|0)+3&-4,l=n[A>>2]|0,n[u>>2]=A+4,A=o,n[A>>2]=l,n[A+4>>2]=0;break e}case 12:{A=(n[u>>2]|0)+7&-8,l=A,d=n[l>>2]|0,l=n[l+4>>2]|0,n[u>>2]=A+8,A=o,n[A>>2]=d,n[A+4>>2]=l;break e}case 13:{d=(n[u>>2]|0)+3&-4,A=n[d>>2]|0,n[u>>2]=d+4,A=(A&65535)<<16>>16,d=o,n[d>>2]=A,n[d+4>>2]=((A|0)<0)<<31>>31;break e}case 14:{d=(n[u>>2]|0)+3&-4,A=n[d>>2]|0,n[u>>2]=d+4,d=o,n[d>>2]=A&65535,n[d+4>>2]=0;break e}case 15:{d=(n[u>>2]|0)+3&-4,A=n[d>>2]|0,n[u>>2]=d+4,A=(A&255)<<24>>24,d=o,n[d>>2]=A,n[d+4>>2]=((A|0)<0)<<31>>31;break e}case 16:{d=(n[u>>2]|0)+3&-4,A=n[d>>2]|0,n[u>>2]=d+4,d=o,n[d>>2]=A&255,n[d+4>>2]=0;break e}case 17:{d=(n[u>>2]|0)+7&-8,m=+E[d>>3],n[u>>2]=d+8,E[o>>3]=m;break e}case 18:{d=(n[u>>2]|0)+7&-8,m=+E[d>>3],n[u>>2]=d+8,E[o>>3]=m;break e}default:break e}while(!1);while(!1)}function wYe(o,l,u,A){if(o=o|0,l=l|0,u=u|0,A=A|0,!((o|0)==0&(l|0)==0))do u=u+-1|0,s[u>>0]=c[5694+(o&15)>>0]|0|A,o=XP(o|0,l|0,4)|0,l=ye;while(!((o|0)==0&(l|0)==0));return u|0}function BYe(o,l,u){if(o=o|0,l=l|0,u=u|0,!((o|0)==0&(l|0)==0))do u=u+-1|0,s[u>>0]=o&7|48,o=XP(o|0,l|0,3)|0,l=ye;while(!((o|0)==0&(l|0)==0));return u|0}function zy(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;if(l>>>0>0|(l|0)==0&o>>>0>4294967295){for(;A=g_(o|0,l|0,10,0)|0,u=u+-1|0,s[u>>0]=A&255|48,A=o,o=h_(o|0,l|0,10,0)|0,l>>>0>9|(l|0)==9&A>>>0>4294967295;)l=ye;l=o}else l=o;if(l)for(;u=u+-1|0,s[u>>0]=(l>>>0)%10|0|48,!(l>>>0<10);)l=(l>>>0)/10|0;return u|0}function vYe(o){return o=o|0,QYe(o,n[(kYe()|0)+188>>2]|0)|0}function SYe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;m=l&255,A=(u|0)!=0;e:do if(A&(o&3|0)!=0)for(d=l&255;;){if((s[o>>0]|0)==d<<24>>24){B=6;break e}if(o=o+1|0,u=u+-1|0,A=(u|0)!=0,!(A&(o&3|0)!=0)){B=5;break}}else B=5;while(!1);(B|0)==5&&(A?B=6:u=0);e:do if((B|0)==6&&(d=l&255,(s[o>>0]|0)!=d<<24>>24)){A=_e(m,16843009)|0;t:do if(u>>>0>3){for(;m=n[o>>2]^A,!((m&-2139062144^-2139062144)&m+-16843009|0);)if(o=o+4|0,u=u+-4|0,u>>>0<=3){B=11;break t}}else B=11;while(!1);if((B|0)==11&&!u){u=0;break}for(;;){if((s[o>>0]|0)==d<<24>>24)break e;if(o=o+1|0,u=u+-1|0,!u){u=0;break}}}while(!1);return(u|0?o:0)|0}function _s(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0;var m=0,B=0;if(B=I,I=I+256|0,m=B,(u|0)>(A|0)&(d&73728|0)==0){if(d=u-A|0,Xy(m|0,l|0,(d>>>0<256?d:256)|0)|0,d>>>0>255){l=u-A|0;do Ds(o,m,256),d=d+-256|0;while(d>>>0>255);d=l&255}Ds(o,m,d)}I=B}function KX(o,l){return o=o|0,l=l|0,o?o=PYe(o,l,0)|0:o=0,o|0}function DYe(o,l,u,A,d,m){o=o|0,l=+l,u=u|0,A=A|0,d=d|0,m=m|0;var B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0,Le=0,Qe=0,tt=0,Ze=0,ct=0,He=0,Ge=0,Lt=0,qr=0,fr=0,$t=0,Tr=0,Hr=0,cr=0,Hn=0;Hn=I,I=I+560|0,T=Hn+8|0,tt=Hn,cr=Hn+524|0,Hr=cr,_=Hn+512|0,n[tt>>2]=0,Tr=_+12|0,JX(l)|0,(ye|0)<0?(l=-l,fr=1,qr=5659):(fr=(d&2049|0)!=0&1,qr=d&2048|0?5662:d&1|0?5665:5660),JX(l)|0,$t=ye&2146435072;do if($t>>>0<2146435072|($t|0)==2146435072&!1){if(We=+bYe(l,tt)*2,B=We!=0,B&&(n[tt>>2]=(n[tt>>2]|0)+-1),ct=m|32,(ct|0)==97){Le=m&32,ae=Le|0?qr+9|0:qr,G=fr|2,B=12-A|0;do if(A>>>0>11|(B|0)==0)l=We;else{l=8;do B=B+-1|0,l=l*16;while(B|0);if((s[ae>>0]|0)==45){l=-(l+(-We-l));break}else{l=We+l-l;break}}while(!1);k=n[tt>>2]|0,B=(k|0)<0?0-k|0:k,B=zy(B,((B|0)<0)<<31>>31,Tr)|0,(B|0)==(Tr|0)&&(B=_+11|0,s[B>>0]=48),s[B+-1>>0]=(k>>31&2)+43,M=B+-2|0,s[M>>0]=m+15,_=(A|0)<1,T=(d&8|0)==0,B=cr;do $t=~~l,k=B+1|0,s[B>>0]=c[5694+$t>>0]|Le,l=(l-+($t|0))*16,(k-Hr|0)==1&&!(T&(_&l==0))?(s[k>>0]=46,B=B+2|0):B=k;while(l!=0);$t=B-Hr|0,Hr=Tr-M|0,Tr=(A|0)!=0&($t+-2|0)<(A|0)?A+2|0:$t,B=Hr+G+Tr|0,_s(o,32,u,B,d),Ds(o,ae,G),_s(o,48,u,B,d^65536),Ds(o,cr,$t),_s(o,48,Tr-$t|0,0,0),Ds(o,M,Hr),_s(o,32,u,B,d^8192);break}k=(A|0)<0?6:A,B?(B=(n[tt>>2]|0)+-28|0,n[tt>>2]=B,l=We*268435456):(l=We,B=n[tt>>2]|0),$t=(B|0)<0?T:T+288|0,T=$t;do Ge=~~l>>>0,n[T>>2]=Ge,T=T+4|0,l=(l-+(Ge>>>0))*1e9;while(l!=0);if((B|0)>0)for(_=$t,G=T;;){if(M=(B|0)<29?B:29,B=G+-4|0,B>>>0>=_>>>0){T=0;do He=t$(n[B>>2]|0,0,M|0)|0,He=p_(He|0,ye|0,T|0,0)|0,Ge=ye,Ze=g_(He|0,Ge|0,1e9,0)|0,n[B>>2]=Ze,T=h_(He|0,Ge|0,1e9,0)|0,B=B+-4|0;while(B>>>0>=_>>>0);T&&(_=_+-4|0,n[_>>2]=T)}for(T=G;!(T>>>0<=_>>>0);)if(B=T+-4|0,!(n[B>>2]|0))T=B;else break;if(B=(n[tt>>2]|0)-M|0,n[tt>>2]=B,(B|0)>0)G=T;else break}else _=$t;if((B|0)<0){A=((k+25|0)/9|0)+1|0,Qe=(ct|0)==102;do{if(Le=0-B|0,Le=(Le|0)<9?Le:9,_>>>0>>0){M=(1<>>Le,ae=0,B=_;do Ge=n[B>>2]|0,n[B>>2]=(Ge>>>Le)+ae,ae=_e(Ge&M,G)|0,B=B+4|0;while(B>>>0>>0);B=n[_>>2]|0?_:_+4|0,ae?(n[T>>2]=ae,_=B,B=T+4|0):(_=B,B=T)}else _=n[_>>2]|0?_:_+4|0,B=T;T=Qe?$t:_,T=(B-T>>2|0)>(A|0)?T+(A<<2)|0:B,B=(n[tt>>2]|0)+Le|0,n[tt>>2]=B}while((B|0)<0);B=_,A=T}else B=_,A=T;if(Ge=$t,B>>>0>>0){if(T=(Ge-B>>2)*9|0,M=n[B>>2]|0,M>>>0>=10){_=10;do _=_*10|0,T=T+1|0;while(M>>>0>=_>>>0)}}else T=0;if(Qe=(ct|0)==103,Ze=(k|0)!=0,_=k-((ct|0)!=102?T:0)+((Ze&Qe)<<31>>31)|0,(_|0)<(((A-Ge>>2)*9|0)+-9|0)){if(_=_+9216|0,Le=$t+4+(((_|0)/9|0)+-1024<<2)|0,_=((_|0)%9|0)+1|0,(_|0)<9){M=10;do M=M*10|0,_=_+1|0;while((_|0)!=9)}else M=10;if(G=n[Le>>2]|0,ae=(G>>>0)%(M>>>0)|0,_=(Le+4|0)==(A|0),_&(ae|0)==0)_=Le;else if(We=((G>>>0)/(M>>>0)|0)&1|0?9007199254740994:9007199254740992,He=(M|0)/2|0,l=ae>>>0>>0?.5:_&(ae|0)==(He|0)?1:1.5,fr&&(He=(s[qr>>0]|0)==45,l=He?-l:l,We=He?-We:We),_=G-ae|0,n[Le>>2]=_,We+l!=We){if(He=_+M|0,n[Le>>2]=He,He>>>0>999999999)for(T=Le;_=T+-4|0,n[T>>2]=0,_>>>0>>0&&(B=B+-4|0,n[B>>2]=0),He=(n[_>>2]|0)+1|0,n[_>>2]=He,He>>>0>999999999;)T=_;else _=Le;if(T=(Ge-B>>2)*9|0,G=n[B>>2]|0,G>>>0>=10){M=10;do M=M*10|0,T=T+1|0;while(G>>>0>=M>>>0)}}else _=Le;_=_+4|0,_=A>>>0>_>>>0?_:A,He=B}else _=A,He=B;for(ct=_;;){if(ct>>>0<=He>>>0){tt=0;break}if(B=ct+-4|0,!(n[B>>2]|0))ct=B;else{tt=1;break}}A=0-T|0;do if(Qe)if(B=((Ze^1)&1)+k|0,(B|0)>(T|0)&(T|0)>-5?(M=m+-1|0,k=B+-1-T|0):(M=m+-2|0,k=B+-1|0),B=d&8,B)Le=B;else{if(tt&&(Lt=n[ct+-4>>2]|0,(Lt|0)!=0))if((Lt>>>0)%10|0)_=0;else{_=0,B=10;do B=B*10|0,_=_+1|0;while(!((Lt>>>0)%(B>>>0)|0|0))}else _=9;if(B=((ct-Ge>>2)*9|0)+-9|0,(M|32|0)==102){Le=B-_|0,Le=(Le|0)>0?Le:0,k=(k|0)<(Le|0)?k:Le,Le=0;break}else{Le=B+T-_|0,Le=(Le|0)>0?Le:0,k=(k|0)<(Le|0)?k:Le,Le=0;break}}else M=m,Le=d&8;while(!1);if(Qe=k|Le,G=(Qe|0)!=0&1,ae=(M|32|0)==102,ae)Ze=0,B=(T|0)>0?T:0;else{if(B=(T|0)<0?A:T,B=zy(B,((B|0)<0)<<31>>31,Tr)|0,_=Tr,(_-B|0)<2)do B=B+-1|0,s[B>>0]=48;while((_-B|0)<2);s[B+-1>>0]=(T>>31&2)+43,B=B+-2|0,s[B>>0]=M,Ze=B,B=_-B|0}if(B=fr+1+k+G+B|0,_s(o,32,u,B,d),Ds(o,qr,fr),_s(o,48,u,B,d^65536),ae){M=He>>>0>$t>>>0?$t:He,Le=cr+9|0,G=Le,ae=cr+8|0,_=M;do{if(T=zy(n[_>>2]|0,0,Le)|0,(_|0)==(M|0))(T|0)==(Le|0)&&(s[ae>>0]=48,T=ae);else if(T>>>0>cr>>>0){Xy(cr|0,48,T-Hr|0)|0;do T=T+-1|0;while(T>>>0>cr>>>0)}Ds(o,T,G-T|0),_=_+4|0}while(_>>>0<=$t>>>0);if(Qe|0&&Ds(o,5710,1),_>>>0>>0&(k|0)>0)for(;;){if(T=zy(n[_>>2]|0,0,Le)|0,T>>>0>cr>>>0){Xy(cr|0,48,T-Hr|0)|0;do T=T+-1|0;while(T>>>0>cr>>>0)}if(Ds(o,T,(k|0)<9?k:9),_=_+4|0,T=k+-9|0,_>>>0>>0&(k|0)>9)k=T;else{k=T;break}}_s(o,48,k+9|0,9,0)}else{if(Qe=tt?ct:He+4|0,(k|0)>-1){tt=cr+9|0,Le=(Le|0)==0,A=tt,G=0-Hr|0,ae=cr+8|0,M=He;do{T=zy(n[M>>2]|0,0,tt)|0,(T|0)==(tt|0)&&(s[ae>>0]=48,T=ae);do if((M|0)==(He|0)){if(_=T+1|0,Ds(o,T,1),Le&(k|0)<1){T=_;break}Ds(o,5710,1),T=_}else{if(T>>>0<=cr>>>0)break;Xy(cr|0,48,T+G|0)|0;do T=T+-1|0;while(T>>>0>cr>>>0)}while(!1);Hr=A-T|0,Ds(o,T,(k|0)>(Hr|0)?Hr:k),k=k-Hr|0,M=M+4|0}while(M>>>0>>0&(k|0)>-1)}_s(o,48,k+18|0,18,0),Ds(o,Ze,Tr-Ze|0)}_s(o,32,u,B,d^8192)}else cr=(m&32|0)!=0,B=fr+3|0,_s(o,32,u,B,d&-65537),Ds(o,qr,fr),Ds(o,l!=l|!1?cr?5686:5690:cr?5678:5682,3),_s(o,32,u,B,d^8192);while(!1);return I=Hn,((B|0)<(u|0)?u:B)|0}function JX(o){o=+o;var l=0;return E[S>>3]=o,l=n[S>>2]|0,ye=n[S+4>>2]|0,l|0}function bYe(o,l){return o=+o,l=l|0,+ +zX(o,l)}function zX(o,l){o=+o,l=l|0;var u=0,A=0,d=0;switch(E[S>>3]=o,u=n[S>>2]|0,A=n[S+4>>2]|0,d=XP(u|0,A|0,52)|0,d&2047){case 0:{o!=0?(o=+zX(o*18446744073709552e3,l),u=(n[l>>2]|0)+-64|0):u=0,n[l>>2]=u;break}case 2047:break;default:n[l>>2]=(d&2047)+-1022,n[S>>2]=u,n[S+4>>2]=A&-2146435073|1071644672,o=+E[S>>3]}return+o}function PYe(o,l,u){o=o|0,l=l|0,u=u|0;do if(o){if(l>>>0<128){s[o>>0]=l,o=1;break}if(!(n[n[(xYe()|0)+188>>2]>>2]|0))if((l&-128|0)==57216){s[o>>0]=l,o=1;break}else{n[(Jy()|0)>>2]=84,o=-1;break}if(l>>>0<2048){s[o>>0]=l>>>6|192,s[o+1>>0]=l&63|128,o=2;break}if(l>>>0<55296|(l&-8192|0)==57344){s[o>>0]=l>>>12|224,s[o+1>>0]=l>>>6&63|128,s[o+2>>0]=l&63|128,o=3;break}if((l+-65536|0)>>>0<1048576){s[o>>0]=l>>>18|240,s[o+1>>0]=l>>>12&63|128,s[o+2>>0]=l>>>6&63|128,s[o+3>>0]=l&63|128,o=4;break}else{n[(Jy()|0)>>2]=84,o=-1;break}}else o=1;while(!1);return o|0}function xYe(){return u_()|0}function kYe(){return u_()|0}function QYe(o,l){o=o|0,l=l|0;var u=0,A=0;for(A=0;;){if((c[5712+A>>0]|0)==(o|0)){o=2;break}if(u=A+1|0,(u|0)==87){u=5800,A=87,o=5;break}else A=u}if((o|0)==2&&(A?(u=5800,o=5):u=5800),(o|0)==5)for(;;){do o=u,u=u+1|0;while(s[o>>0]|0);if(A=A+-1|0,A)o=5;else break}return TYe(u,n[l+20>>2]|0)|0}function TYe(o,l){return o=o|0,l=l|0,RYe(o,l)|0}function RYe(o,l){return o=o|0,l=l|0,l?l=FYe(n[l>>2]|0,n[l+4>>2]|0,o)|0:l=0,(l|0?l:o)|0}function FYe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0;ae=(n[o>>2]|0)+1794895138|0,m=fd(n[o+8>>2]|0,ae)|0,A=fd(n[o+12>>2]|0,ae)|0,d=fd(n[o+16>>2]|0,ae)|0;e:do if(m>>>0>>2>>>0&&(G=l-(m<<2)|0,A>>>0>>0&d>>>0>>0)&&!((d|A)&3|0)){for(G=A>>>2,M=d>>>2,_=0;;){if(k=m>>>1,T=_+k|0,B=T<<1,d=B+G|0,A=fd(n[o+(d<<2)>>2]|0,ae)|0,d=fd(n[o+(d+1<<2)>>2]|0,ae)|0,!(d>>>0>>0&A>>>0<(l-d|0)>>>0)){A=0;break e}if(s[o+(d+A)>>0]|0){A=0;break e}if(A=GX(u,o+d|0)|0,!A)break;if(A=(A|0)<0,(m|0)==1){A=0;break e}else _=A?_:T,m=A?k:m-k|0}A=B+M|0,d=fd(n[o+(A<<2)>>2]|0,ae)|0,A=fd(n[o+(A+1<<2)>>2]|0,ae)|0,A>>>0>>0&d>>>0<(l-A|0)>>>0?A=s[o+(A+d)>>0]|0?0:o+A|0:A=0}else A=0;while(!1);return A|0}function fd(o,l){o=o|0,l=l|0;var u=0;return u=i$(o|0)|0,(l|0?u:o)|0}function NYe(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0,k=0;A=u+16|0,d=n[A>>2]|0,d?m=5:OYe(u)|0?A=0:(d=n[A>>2]|0,m=5);e:do if((m|0)==5){if(k=u+20|0,B=n[k>>2]|0,A=B,(d-B|0)>>>0>>0){A=ex[n[u+36>>2]&7](u,o,l)|0;break}t:do if((s[u+75>>0]|0)>-1){for(B=l;;){if(!B){m=0,d=o;break t}if(d=B+-1|0,(s[o+d>>0]|0)==10)break;B=d}if(A=ex[n[u+36>>2]&7](u,o,B)|0,A>>>0>>0)break e;m=B,d=o+B|0,l=l-B|0,A=n[k>>2]|0}else m=0,d=o;while(!1);Qr(A|0,d|0,l|0)|0,n[k>>2]=(n[k>>2]|0)+l,A=m+l|0}while(!1);return A|0}function OYe(o){o=o|0;var l=0,u=0;return l=o+74|0,u=s[l>>0]|0,s[l>>0]=u+255|u,l=n[o>>2]|0,l&8?(n[o>>2]=l|32,o=-1):(n[o+8>>2]=0,n[o+4>>2]=0,u=n[o+44>>2]|0,n[o+28>>2]=u,n[o+20>>2]=u,n[o+16>>2]=u+(n[o+48>>2]|0),o=0),o|0}function $n(o,l){o=y(o),l=y(l);var u=0,A=0;u=ZX(o)|0;do if((u&2147483647)>>>0<=2139095040){if(A=ZX(l)|0,(A&2147483647)>>>0<=2139095040)if((A^u|0)<0){o=(u|0)<0?l:o;break}else{o=o>2]=o,n[S>>2]|0|0}function Ad(o,l){o=y(o),l=y(l);var u=0,A=0;u=XX(o)|0;do if((u&2147483647)>>>0<=2139095040){if(A=XX(l)|0,(A&2147483647)>>>0<=2139095040)if((A^u|0)<0){o=(u|0)<0?o:l;break}else{o=o>2]=o,n[S>>2]|0|0}function A_(o,l){o=y(o),l=y(l);var u=0,A=0,d=0,m=0,B=0,k=0,T=0,_=0;m=(h[S>>2]=o,n[S>>2]|0),k=(h[S>>2]=l,n[S>>2]|0),u=m>>>23&255,B=k>>>23&255,T=m&-2147483648,d=k<<1;e:do if(d|0&&!((u|0)==255|((LYe(l)|0)&2147483647)>>>0>2139095040)){if(A=m<<1,A>>>0<=d>>>0)return l=y(o*y(0)),y((A|0)==(d|0)?l:o);if(u)A=m&8388607|8388608;else{if(u=m<<9,(u|0)>-1){A=u,u=0;do u=u+-1|0,A=A<<1;while((A|0)>-1)}else u=0;A=m<<1-u}if(B)k=k&8388607|8388608;else{if(m=k<<9,(m|0)>-1){d=0;do d=d+-1|0,m=m<<1;while((m|0)>-1)}else d=0;B=d,k=k<<1-d}d=A-k|0,m=(d|0)>-1;t:do if((u|0)>(B|0)){for(;;){if(m)if(d)A=d;else break;if(A=A<<1,u=u+-1|0,d=A-k|0,m=(d|0)>-1,(u|0)<=(B|0))break t}l=y(o*y(0));break e}while(!1);if(m)if(d)A=d;else{l=y(o*y(0));break}if(A>>>0<8388608)do A=A<<1,u=u+-1|0;while(A>>>0<8388608);(u|0)>0?u=A+-8388608|u<<23:u=A>>>(1-u|0),l=(n[S>>2]=u|T,y(h[S>>2]))}else _=3;while(!1);return(_|0)==3&&(l=y(o*l),l=y(l/l)),y(l)}function LYe(o){return o=y(o),h[S>>2]=o,n[S>>2]|0|0}function MYe(o,l){return o=o|0,l=l|0,WX(n[582]|0,o,l)|0}function sn(o){o=o|0,Nt()}function Zy(o){o=o|0}function _Ye(o,l){return o=o|0,l=l|0,0}function UYe(o){return o=o|0,($X(o+4|0)|0)==-1?(op[n[(n[o>>2]|0)+8>>2]&127](o),o=1):o=0,o|0}function $X(o){o=o|0;var l=0;return l=n[o>>2]|0,n[o>>2]=l+-1,l+-1|0}function Wh(o){o=o|0,UYe(o)|0&&HYe(o)}function HYe(o){o=o|0;var l=0;l=o+8|0,n[l>>2]|0&&($X(l)|0)!=-1||op[n[(n[o>>2]|0)+16>>2]&127](o)}function Jt(o){o=o|0;var l=0;for(l=o|0?o:1;o=KP(l)|0,!(o|0);){if(o=qYe()|0,!o){o=0;break}h$[o&0]()}return o|0}function e$(o){return o=o|0,Jt(o)|0}function Et(o){o=o|0,JP(o)}function jYe(o){o=o|0,(s[o+11>>0]|0)<0&&Et(n[o>>2]|0)}function qYe(){var o=0;return o=n[2923]|0,n[2923]=o+0,o|0}function GYe(){}function ZP(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,A=l-A-(u>>>0>o>>>0|0)>>>0,ye=A,o-u>>>0|0|0}function p_(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,u=o+u>>>0,ye=l+A+(u>>>0>>0|0)>>>0,u|0|0}function Xy(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0,B=0;if(m=o+u|0,l=l&255,(u|0)>=67){for(;o&3;)s[o>>0]=l,o=o+1|0;for(A=m&-4|0,d=A-64|0,B=l|l<<8|l<<16|l<<24;(o|0)<=(d|0);)n[o>>2]=B,n[o+4>>2]=B,n[o+8>>2]=B,n[o+12>>2]=B,n[o+16>>2]=B,n[o+20>>2]=B,n[o+24>>2]=B,n[o+28>>2]=B,n[o+32>>2]=B,n[o+36>>2]=B,n[o+40>>2]=B,n[o+44>>2]=B,n[o+48>>2]=B,n[o+52>>2]=B,n[o+56>>2]=B,n[o+60>>2]=B,o=o+64|0;for(;(o|0)<(A|0);)n[o>>2]=B,o=o+4|0}for(;(o|0)<(m|0);)s[o>>0]=l,o=o+1|0;return m-u|0}function t$(o,l,u){return o=o|0,l=l|0,u=u|0,(u|0)<32?(ye=l<>>32-u,o<>>u,o>>>u|(l&(1<>>u-32|0)}function Qr(o,l,u){o=o|0,l=l|0,u=u|0;var A=0,d=0,m=0;if((u|0)>=8192)return MA(o|0,l|0,u|0)|0;if(m=o|0,d=o+u|0,(o&3)==(l&3)){for(;o&3;){if(!u)return m|0;s[o>>0]=s[l>>0]|0,o=o+1|0,l=l+1|0,u=u-1|0}for(u=d&-4|0,A=u-64|0;(o|0)<=(A|0);)n[o>>2]=n[l>>2],n[o+4>>2]=n[l+4>>2],n[o+8>>2]=n[l+8>>2],n[o+12>>2]=n[l+12>>2],n[o+16>>2]=n[l+16>>2],n[o+20>>2]=n[l+20>>2],n[o+24>>2]=n[l+24>>2],n[o+28>>2]=n[l+28>>2],n[o+32>>2]=n[l+32>>2],n[o+36>>2]=n[l+36>>2],n[o+40>>2]=n[l+40>>2],n[o+44>>2]=n[l+44>>2],n[o+48>>2]=n[l+48>>2],n[o+52>>2]=n[l+52>>2],n[o+56>>2]=n[l+56>>2],n[o+60>>2]=n[l+60>>2],o=o+64|0,l=l+64|0;for(;(o|0)<(u|0);)n[o>>2]=n[l>>2],o=o+4|0,l=l+4|0}else for(u=d-4|0;(o|0)<(u|0);)s[o>>0]=s[l>>0]|0,s[o+1>>0]=s[l+1>>0]|0,s[o+2>>0]=s[l+2>>0]|0,s[o+3>>0]=s[l+3>>0]|0,o=o+4|0,l=l+4|0;for(;(o|0)<(d|0);)s[o>>0]=s[l>>0]|0,o=o+1|0,l=l+1|0;return m|0}function r$(o){o=o|0;var l=0;return l=s[N+(o&255)>>0]|0,(l|0)<8?l|0:(l=s[N+(o>>8&255)>>0]|0,(l|0)<8?l+8|0:(l=s[N+(o>>16&255)>>0]|0,(l|0)<8?l+16|0:(s[N+(o>>>24)>>0]|0)+24|0))}function n$(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0;var m=0,B=0,k=0,T=0,_=0,M=0,G=0,ae=0,We=0,Le=0;if(M=o,T=l,_=T,B=u,ae=A,k=ae,!_)return m=(d|0)!=0,k?m?(n[d>>2]=o|0,n[d+4>>2]=l&0,ae=0,d=0,ye=ae,d|0):(ae=0,d=0,ye=ae,d|0):(m&&(n[d>>2]=(M>>>0)%(B>>>0),n[d+4>>2]=0),ae=0,d=(M>>>0)/(B>>>0)>>>0,ye=ae,d|0);m=(k|0)==0;do if(B){if(!m){if(m=(b(k|0)|0)-(b(_|0)|0)|0,m>>>0<=31){G=m+1|0,k=31-m|0,l=m-31>>31,B=G,o=M>>>(G>>>0)&l|_<>>(G>>>0)&l,m=0,k=M<>2]=o|0,n[d+4>>2]=T|l&0,ae=0,d=0,ye=ae,d|0):(ae=0,d=0,ye=ae,d|0)}if(m=B-1|0,m&B|0){k=(b(B|0)|0)+33-(b(_|0)|0)|0,Le=64-k|0,G=32-k|0,T=G>>31,We=k-32|0,l=We>>31,B=k,o=G-1>>31&_>>>(We>>>0)|(_<>>(k>>>0))&l,l=l&_>>>(k>>>0),m=M<>>(We>>>0))&T|M<>31;break}return d|0&&(n[d>>2]=m&M,n[d+4>>2]=0),(B|0)==1?(We=T|l&0,Le=o|0|0,ye=We,Le|0):(Le=r$(B|0)|0,We=_>>>(Le>>>0)|0,Le=_<<32-Le|M>>>(Le>>>0)|0,ye=We,Le|0)}else{if(m)return d|0&&(n[d>>2]=(_>>>0)%(B>>>0),n[d+4>>2]=0),We=0,Le=(_>>>0)/(B>>>0)>>>0,ye=We,Le|0;if(!M)return d|0&&(n[d>>2]=0,n[d+4>>2]=(_>>>0)%(k>>>0)),We=0,Le=(_>>>0)/(k>>>0)>>>0,ye=We,Le|0;if(m=k-1|0,!(m&k))return d|0&&(n[d>>2]=o|0,n[d+4>>2]=m&_|l&0),We=0,Le=_>>>((r$(k|0)|0)>>>0),ye=We,Le|0;if(m=(b(k|0)|0)-(b(_|0)|0)|0,m>>>0<=30){l=m+1|0,k=31-m|0,B=l,o=_<>>(l>>>0),l=_>>>(l>>>0),m=0,k=M<>2]=o|0,n[d+4>>2]=T|l&0,We=0,Le=0,ye=We,Le|0):(We=0,Le=0,ye=We,Le|0)}while(!1);if(!B)_=k,T=0,k=0;else{G=u|0|0,M=ae|A&0,_=p_(G|0,M|0,-1,-1)|0,u=ye,T=k,k=0;do A=T,T=m>>>31|T<<1,m=k|m<<1,A=o<<1|A>>>31|0,ae=o>>>31|l<<1|0,ZP(_|0,u|0,A|0,ae|0)|0,Le=ye,We=Le>>31|((Le|0)<0?-1:0)<<1,k=We&1,o=ZP(A|0,ae|0,We&G|0,(((Le|0)<0?-1:0)>>31|((Le|0)<0?-1:0)<<1)&M|0)|0,l=ye,B=B-1|0;while(B|0);_=T,T=0}return B=0,d|0&&(n[d>>2]=o,n[d+4>>2]=l),We=(m|0)>>>31|(_|B)<<1|(B<<1|m>>>31)&0|T,Le=(m<<1|0)&-2|k,ye=We,Le|0}function h_(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,n$(o,l,u,A,0)|0}function Yh(o){o=o|0;var l=0,u=0;return u=o+15&-16|0,l=n[C>>2]|0,o=l+u|0,(u|0)>0&(o|0)<(l|0)|(o|0)<0?(oe()|0,pu(12),-1):(n[C>>2]=o,(o|0)>($()|0)&&!(Z()|0)?(n[C>>2]=l,pu(12),-1):l|0)}function Q2(o,l,u){o=o|0,l=l|0,u=u|0;var A=0;if((l|0)<(o|0)&(o|0)<(l+u|0)){for(A=o,l=l+u|0,o=o+u|0;(u|0)>0;)o=o-1|0,l=l-1|0,u=u-1|0,s[o>>0]=s[l>>0]|0;o=A}else Qr(o,l,u)|0;return o|0}function g_(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0;var d=0,m=0;return m=I,I=I+16|0,d=m|0,n$(o,l,u,A,d)|0,I=m,ye=n[d+4>>2]|0,n[d>>2]|0|0}function i$(o){return o=o|0,(o&255)<<24|(o>>8&255)<<16|(o>>16&255)<<8|o>>>24|0}function WYe(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0,s$[o&1](l|0,u|0,A|0,d|0,m|0)}function YYe(o,l,u){o=o|0,l=l|0,u=y(u),o$[o&1](l|0,y(u))}function VYe(o,l,u){o=o|0,l=l|0,u=+u,a$[o&31](l|0,+u)}function KYe(o,l,u,A){return o=o|0,l=l|0,u=y(u),A=y(A),y(l$[o&0](l|0,y(u),y(A)))}function JYe(o,l){o=o|0,l=l|0,op[o&127](l|0)}function zYe(o,l,u){o=o|0,l=l|0,u=u|0,ap[o&31](l|0,u|0)}function ZYe(o,l){return o=o|0,l=l|0,hd[o&31](l|0)|0}function XYe(o,l,u,A,d){o=o|0,l=l|0,u=+u,A=+A,d=d|0,c$[o&1](l|0,+u,+A,d|0)}function $Ye(o,l,u,A){o=o|0,l=l|0,u=+u,A=+A,RVe[o&1](l|0,+u,+A)}function eVe(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,ex[o&7](l|0,u|0,A|0)|0}function tVe(o,l,u,A){return o=o|0,l=l|0,u=u|0,A=A|0,+FVe[o&1](l|0,u|0,A|0)}function rVe(o,l){return o=o|0,l=l|0,+u$[o&15](l|0)}function nVe(o,l,u){return o=o|0,l=l|0,u=+u,NVe[o&1](l|0,+u)|0}function iVe(o,l,u){return o=o|0,l=l|0,u=u|0,m_[o&15](l|0,u|0)|0}function sVe(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=+A,d=+d,m=m|0,OVe[o&1](l|0,u|0,+A,+d,m|0)}function oVe(o,l,u,A,d,m,B){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0,B=B|0,LVe[o&1](l|0,u|0,A|0,d|0,m|0,B|0)}function aVe(o,l,u){return o=o|0,l=l|0,u=u|0,+f$[o&7](l|0,u|0)}function lVe(o){return o=o|0,tx[o&7]()|0}function cVe(o,l,u,A,d,m){return o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0,A$[o&1](l|0,u|0,A|0,d|0,m|0)|0}function uVe(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=+d,MVe[o&1](l|0,u|0,A|0,+d)}function fVe(o,l,u,A,d,m,B){o=o|0,l=l|0,u=u|0,A=y(A),d=d|0,m=y(m),B=B|0,p$[o&1](l|0,u|0,y(A),d|0,y(m),B|0)}function AVe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,F2[o&15](l|0,u|0,A|0)}function pVe(o){o=o|0,h$[o&0]()}function hVe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A,g$[o&15](l|0,u|0,+A)}function gVe(o,l,u){return o=o|0,l=+l,u=+u,_Ve[o&1](+l,+u)|0}function dVe(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,y_[o&15](l|0,u|0,A|0,d|0)}function mVe(o,l,u,A,d){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,F(0)}function yVe(o,l){o=o|0,l=y(l),F(1)}function Xa(o,l){o=o|0,l=+l,F(2)}function EVe(o,l,u){return o=o|0,l=y(l),u=y(u),F(3),Xe}function wr(o){o=o|0,F(4)}function T2(o,l){o=o|0,l=l|0,F(5)}function Ll(o){return o=o|0,F(6),0}function IVe(o,l,u,A){o=o|0,l=+l,u=+u,A=A|0,F(7)}function CVe(o,l,u){o=o|0,l=+l,u=+u,F(8)}function wVe(o,l,u){return o=o|0,l=l|0,u=u|0,F(9),0}function BVe(o,l,u){return o=o|0,l=l|0,u=u|0,F(10),0}function pd(o){return o=o|0,F(11),0}function vVe(o,l){return o=o|0,l=+l,F(12),0}function R2(o,l){return o=o|0,l=l|0,F(13),0}function SVe(o,l,u,A,d){o=o|0,l=l|0,u=+u,A=+A,d=d|0,F(14)}function DVe(o,l,u,A,d,m){o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,m=m|0,F(15)}function d_(o,l){return o=o|0,l=l|0,F(16),0}function bVe(){return F(17),0}function PVe(o,l,u,A,d){return o=o|0,l=l|0,u=u|0,A=A|0,d=d|0,F(18),0}function xVe(o,l,u,A){o=o|0,l=l|0,u=u|0,A=+A,F(19)}function kVe(o,l,u,A,d,m){o=o|0,l=l|0,u=y(u),A=A|0,d=y(d),m=m|0,F(20)}function $P(o,l,u){o=o|0,l=l|0,u=u|0,F(21)}function QVe(){F(22)}function $y(o,l,u){o=o|0,l=l|0,u=+u,F(23)}function TVe(o,l){return o=+o,l=+l,F(24),0}function eE(o,l,u,A){o=o|0,l=l|0,u=u|0,A=A|0,F(25)}var s$=[mVe,PGe],o$=[yVe,ky],a$=[Xa,Zg,Oh,h2,g2,d2,m2,Pf,My,y2,xf,Xg,$g,E2,I2,vu,ed,C2,_y,Xa,Xa,Xa,Xa,Xa,Xa,Xa,Xa,Xa,Xa,Xa,Xa,Xa],l$=[EVe],op=[wr,Zy,lLe,cLe,uLe,_Ue,UUe,HUe,nqe,iqe,sqe,gGe,dGe,mGe,LWe,MWe,_We,vl,zg,u2,sr,gc,LP,MP,XOe,yLe,kLe,KLe,uMe,bMe,GMe,s_e,I_e,O_e,X_e,hUe,kUe,i4e,I4e,O4e,X4e,h3e,k3e,J3e,u8e,v8e,U8e,vP,mHe,RHe,XHe,dje,Qje,Xje,l6e,f6e,P6e,Q6e,K6e,aqe,uqe,bqe,Wqe,Wz,D5e,n9e,y9e,R9e,tWe,dWe,bWe,kWe,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr,wr],ap=[T2,Ny,zL,f2,A2,xr,ao,Xi,Ls,vs,Ly,Nh,B2,xP,nd,$L,eM,kP,QP,nM,kf,ne,$3e,p8e,Ije,x5e,$qe,xX,T2,T2,T2,T2],hd=[Ll,hYe,Ry,rd,Hy,da,SP,Lh,w2,XL,bP,jy,TP,iM,Wy,q8e,Nje,kqe,R5e,Fl,Ll,Ll,Ll,Ll,Ll,Ll,Ll,Ll,Ll,Ll,Ll,Ll],c$=[IVe,cM],RVe=[CVe,Z6e],ex=[wVe,qX,gYe,yYe,QMe,l4e,CHe,L9e],FVe=[BVe,rUe],u$=[pd,Mh,PP,tp,uM,v,D,Q,H,V,pd,pd,pd,pd,pd,pd],NVe=[vVe,s6e],m_=[R2,_Ye,RP,rLe,XLe,VMe,l_e,FUe,v4e,P8e,Qy,w9e,R2,R2,R2,R2],OVe=[SVe,FLe],LVe=[DVe,sWe],f$=[d_,tM,Se,Ue,At,yUe,d_,d_],tx=[bVe,Gt,Ty,BP,g6e,N6e,hqe,FWe],A$=[PVe,By],MVe=[xVe,r3e],p$=[kVe,sM],F2=[$P,To,DP,rM,Du,hMe,B_e,y3e,F3e,JL,ZGe,a9e,IWe,$P,$P,$P],h$=[QVe],g$=[$y,ZL,Oy,ep,p2,Su,Uy,td,U4e,LHe,t6e,$y,$y,$y,$y,$y],_Ve=[TVe,tqe],y_=[eE,U_e,Z8e,rje,Gje,C6e,H6e,Cqe,zqe,_5e,WWe,eE,eE,eE,eE,eE];return{_llvm_bswap_i32:i$,dynCall_idd:gVe,dynCall_i:lVe,_i64Subtract:ZP,___udivdi3:h_,dynCall_vif:YYe,setThrew:ua,dynCall_viii:AVe,_bitshift64Lshr:XP,_bitshift64Shl:t$,dynCall_vi:JYe,dynCall_viiddi:sVe,dynCall_diii:tVe,dynCall_iii:iVe,_memset:Xy,_sbrk:Yh,_memcpy:Qr,__GLOBAL__sub_I_Yoga_cpp:a2,dynCall_vii:zYe,___uremdi3:g_,dynCall_vid:VYe,stackAlloc:Ha,_nbind_init:rYe,getTempRet0:UA,dynCall_di:rVe,dynCall_iid:nVe,setTempRet0:_A,_i64Add:p_,dynCall_fiff:KYe,dynCall_iiii:eVe,_emscripten_get_global_libc:pYe,dynCall_viid:hVe,dynCall_viiid:uVe,dynCall_viififi:fVe,dynCall_ii:ZYe,__GLOBAL__sub_I_Binding_cc:y5e,dynCall_viiii:dVe,dynCall_iiiiii:cVe,stackSave:gf,dynCall_viiiii:WYe,__GLOBAL__sub_I_nbind_cc:vr,dynCall_vidd:$Ye,_free:JP,runPostSets:GYe,dynCall_viiiiii:oVe,establishStackSpace:wn,_memmove:Q2,stackRestore:cc,_malloc:KP,__GLOBAL__sub_I_common_cc:Mqe,dynCall_viddi:XYe,dynCall_dii:aVe,dynCall_v:pVe}}(Module.asmGlobalArg,Module.asmLibraryArg,buffer),_llvm_bswap_i32=Module._llvm_bswap_i32=asm._llvm_bswap_i32,getTempRet0=Module.getTempRet0=asm.getTempRet0,___udivdi3=Module.___udivdi3=asm.___udivdi3,setThrew=Module.setThrew=asm.setThrew,_bitshift64Lshr=Module._bitshift64Lshr=asm._bitshift64Lshr,_bitshift64Shl=Module._bitshift64Shl=asm._bitshift64Shl,_memset=Module._memset=asm._memset,_sbrk=Module._sbrk=asm._sbrk,_memcpy=Module._memcpy=asm._memcpy,stackAlloc=Module.stackAlloc=asm.stackAlloc,___uremdi3=Module.___uremdi3=asm.___uremdi3,_nbind_init=Module._nbind_init=asm._nbind_init,_i64Subtract=Module._i64Subtract=asm._i64Subtract,setTempRet0=Module.setTempRet0=asm.setTempRet0,_i64Add=Module._i64Add=asm._i64Add,_emscripten_get_global_libc=Module._emscripten_get_global_libc=asm._emscripten_get_global_libc,__GLOBAL__sub_I_Yoga_cpp=Module.__GLOBAL__sub_I_Yoga_cpp=asm.__GLOBAL__sub_I_Yoga_cpp,__GLOBAL__sub_I_Binding_cc=Module.__GLOBAL__sub_I_Binding_cc=asm.__GLOBAL__sub_I_Binding_cc,stackSave=Module.stackSave=asm.stackSave,__GLOBAL__sub_I_nbind_cc=Module.__GLOBAL__sub_I_nbind_cc=asm.__GLOBAL__sub_I_nbind_cc,_free=Module._free=asm._free,runPostSets=Module.runPostSets=asm.runPostSets,establishStackSpace=Module.establishStackSpace=asm.establishStackSpace,_memmove=Module._memmove=asm._memmove,stackRestore=Module.stackRestore=asm.stackRestore,_malloc=Module._malloc=asm._malloc,__GLOBAL__sub_I_common_cc=Module.__GLOBAL__sub_I_common_cc=asm.__GLOBAL__sub_I_common_cc,dynCall_viiiii=Module.dynCall_viiiii=asm.dynCall_viiiii,dynCall_vif=Module.dynCall_vif=asm.dynCall_vif,dynCall_vid=Module.dynCall_vid=asm.dynCall_vid,dynCall_fiff=Module.dynCall_fiff=asm.dynCall_fiff,dynCall_vi=Module.dynCall_vi=asm.dynCall_vi,dynCall_vii=Module.dynCall_vii=asm.dynCall_vii,dynCall_ii=Module.dynCall_ii=asm.dynCall_ii,dynCall_viddi=Module.dynCall_viddi=asm.dynCall_viddi,dynCall_vidd=Module.dynCall_vidd=asm.dynCall_vidd,dynCall_iiii=Module.dynCall_iiii=asm.dynCall_iiii,dynCall_diii=Module.dynCall_diii=asm.dynCall_diii,dynCall_di=Module.dynCall_di=asm.dynCall_di,dynCall_iid=Module.dynCall_iid=asm.dynCall_iid,dynCall_iii=Module.dynCall_iii=asm.dynCall_iii,dynCall_viiddi=Module.dynCall_viiddi=asm.dynCall_viiddi,dynCall_viiiiii=Module.dynCall_viiiiii=asm.dynCall_viiiiii,dynCall_dii=Module.dynCall_dii=asm.dynCall_dii,dynCall_i=Module.dynCall_i=asm.dynCall_i,dynCall_iiiiii=Module.dynCall_iiiiii=asm.dynCall_iiiiii,dynCall_viiid=Module.dynCall_viiid=asm.dynCall_viiid,dynCall_viififi=Module.dynCall_viififi=asm.dynCall_viififi,dynCall_viii=Module.dynCall_viii=asm.dynCall_viii,dynCall_v=Module.dynCall_v=asm.dynCall_v,dynCall_viid=Module.dynCall_viid=asm.dynCall_viid,dynCall_idd=Module.dynCall_idd=asm.dynCall_idd,dynCall_viiii=Module.dynCall_viiii=asm.dynCall_viiii;Runtime.stackAlloc=Module.stackAlloc,Runtime.stackSave=Module.stackSave,Runtime.stackRestore=Module.stackRestore,Runtime.establishStackSpace=Module.establishStackSpace,Runtime.setTempRet0=Module.setTempRet0,Runtime.getTempRet0=Module.getTempRet0,Module.asm=asm;function ExitStatus(t){this.name="ExitStatus",this.message="Program terminated with exit("+t+")",this.status=t}ExitStatus.prototype=new Error,ExitStatus.prototype.constructor=ExitStatus;var initialStackTop,preloadStartTime=null,calledMain=!1;dependenciesFulfilled=function t(){Module.calledRun||run(),Module.calledRun||(dependenciesFulfilled=t)},Module.callMain=Module.callMain=function t(e){e=e||[],ensureInitRuntime();var r=e.length+1;function s(){for(var p=0;p<3;p++)a.push(0)}var a=[allocate(intArrayFromString(Module.thisProgram),"i8",ALLOC_NORMAL)];s();for(var n=0;n0||(preRun(),runDependencies>0)||Module.calledRun)return;function e(){Module.calledRun||(Module.calledRun=!0,!ABORT&&(ensureInitRuntime(),preMain(),Module.onRuntimeInitialized&&Module.onRuntimeInitialized(),Module._main&&shouldRunNow&&Module.callMain(t),postRun()))}Module.setStatus?(Module.setStatus("Running..."),setTimeout(function(){setTimeout(function(){Module.setStatus("")},1),e()},1)):e()}Module.run=Module.run=run;function exit(t,e){e&&Module.noExitRuntime||(Module.noExitRuntime||(ABORT=!0,EXITSTATUS=t,STACKTOP=initialStackTop,exitRuntime(),Module.onExit&&Module.onExit(t)),ENVIRONMENT_IS_NODE&&process.exit(t),Module.quit(t,new ExitStatus(t)))}Module.exit=Module.exit=exit;var abortDecorators=[];function abort(t){Module.onAbort&&Module.onAbort(t),t!==void 0?(Module.print(t),Module.printErr(t),t=JSON.stringify(t)):t="",ABORT=!0,EXITSTATUS=1;var e=` If this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.`,r="abort("+t+") at "+stackTrace()+e;throw abortDecorators&&abortDecorators.forEach(function(s){r=s(r,t)}),r}if(Module.abort=Module.abort=abort,Module.preInit)for(typeof Module.preInit=="function"&&(Module.preInit=[Module.preInit]);Module.preInit.length>0;)Module.preInit.pop()();var shouldRunNow=!0;Module.noInitialRun&&(shouldRunNow=!1),run()})});var Rm=L((rhr,LDe)=>{"use strict";var IPt=NDe(),CPt=ODe(),CW=!1,wW=null;CPt({},function(t,e){if(!CW){if(CW=!0,t)throw t;wW=e}});if(!CW)throw new Error("Failed to load the yoga module - it needed to be loaded synchronously, but didn't");LDe.exports=IPt(wW.bind,wW.lib)});var vW=L((nhr,BW)=>{"use strict";var MDe=t=>Number.isNaN(t)?!1:t>=4352&&(t<=4447||t===9001||t===9002||11904<=t&&t<=12871&&t!==12351||12880<=t&&t<=19903||19968<=t&&t<=42182||43360<=t&&t<=43388||44032<=t&&t<=55203||63744<=t&&t<=64255||65040<=t&&t<=65049||65072<=t&&t<=65131||65281<=t&&t<=65376||65504<=t&&t<=65510||110592<=t&&t<=110593||127488<=t&&t<=127569||131072<=t&&t<=262141);BW.exports=MDe;BW.exports.default=MDe});var UDe=L((ihr,_De)=>{"use strict";_De.exports=function(){return/\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F|\uD83D\uDC68(?:\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68\uD83C\uDFFB|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|[\u2695\u2696\u2708]\uFE0F|\uD83D[\uDC66\uDC67]|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708])\uFE0F|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C[\uDFFB-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)\uD83C\uDFFB|\uD83E\uDDD1(?:\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])|\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)(?:\uD83C[\uDFFB\uDFFC])|\uD83D\uDC69(?:\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)(?:\uD83C[\uDFFB-\uDFFD])|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83D\uDC69(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|(?:(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)\uFE0F|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD6-\uDDDD])(?:(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\u200D[\u2640\u2642])|\uD83C\uDFF4\u200D\u2620)\uFE0F|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC15\u200D\uD83E\uDDBA|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF4\uD83C\uDDF2|\uD83C\uDDF6\uD83C\uDDE6|[#\*0-9]\uFE0F\u20E3|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270A-\u270D]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC70\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDCAA\uDD74\uDD7A\uDD90\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD36\uDDB5\uDDB6\uDDBB\uDDD2-\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5\uDEEB\uDEEC\uDEF4-\uDEFA\uDFE0-\uDFEB]|\uD83E[\uDD0D-\uDD3A\uDD3C-\uDD45\uDD47-\uDD71\uDD73-\uDD76\uDD7A-\uDDA2\uDDA5-\uDDAA\uDDAE-\uDDCA\uDDCD-\uDDFF\uDE70-\uDE73\uDE78-\uDE7A\uDE80-\uDE82\uDE90-\uDE95])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFA\uDFE0-\uDFEB]|\uD83E[\uDD0D-\uDD3A\uDD3C-\uDD45\uDD47-\uDD71\uDD73-\uDD76\uDD7A-\uDDA2\uDDA5-\uDDAA\uDDAE-\uDDCA\uDDCD-\uDDFF\uDE70-\uDE73\uDE78-\uDE7A\uDE80-\uDE82\uDE90-\uDE95])\uFE0F|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDC8F\uDC91\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD0F\uDD18-\uDD1F\uDD26\uDD30-\uDD39\uDD3C-\uDD3E\uDDB5\uDDB6\uDDB8\uDDB9\uDDBB\uDDCD-\uDDCF\uDDD1-\uDDDD])/g}});var $S=L((shr,SW)=>{"use strict";var wPt=vk(),BPt=vW(),vPt=UDe(),HDe=t=>{if(typeof t!="string"||t.length===0||(t=wPt(t),t.length===0))return 0;t=t.replace(vPt()," ");let e=0;for(let r=0;r=127&&s<=159||s>=768&&s<=879||(s>65535&&r++,e+=BPt(s)?2:1)}return e};SW.exports=HDe;SW.exports.default=HDe});var bW=L((ohr,DW)=>{"use strict";var SPt=$S(),jDe=t=>{let e=0;for(let r of t.split(` `))e=Math.max(e,SPt(r));return e};DW.exports=jDe;DW.exports.default=jDe});var qDe=L(eD=>{"use strict";var DPt=eD&&eD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(eD,"__esModule",{value:!0});var bPt=DPt(bW()),PW={};eD.default=t=>{if(t.length===0)return{width:0,height:0};if(PW[t])return PW[t];let e=bPt.default(t),r=t.split(` `).length;return PW[t]={width:e,height:r},{width:e,height:r}}});var GDe=L(tD=>{"use strict";var PPt=tD&&tD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(tD,"__esModule",{value:!0});var bn=PPt(Rm()),xPt=(t,e)=>{"position"in e&&t.setPositionType(e.position==="absolute"?bn.default.POSITION_TYPE_ABSOLUTE:bn.default.POSITION_TYPE_RELATIVE)},kPt=(t,e)=>{"marginLeft"in e&&t.setMargin(bn.default.EDGE_START,e.marginLeft||0),"marginRight"in e&&t.setMargin(bn.default.EDGE_END,e.marginRight||0),"marginTop"in e&&t.setMargin(bn.default.EDGE_TOP,e.marginTop||0),"marginBottom"in e&&t.setMargin(bn.default.EDGE_BOTTOM,e.marginBottom||0)},QPt=(t,e)=>{"paddingLeft"in e&&t.setPadding(bn.default.EDGE_LEFT,e.paddingLeft||0),"paddingRight"in e&&t.setPadding(bn.default.EDGE_RIGHT,e.paddingRight||0),"paddingTop"in e&&t.setPadding(bn.default.EDGE_TOP,e.paddingTop||0),"paddingBottom"in e&&t.setPadding(bn.default.EDGE_BOTTOM,e.paddingBottom||0)},TPt=(t,e)=>{var r;"flexGrow"in e&&t.setFlexGrow((r=e.flexGrow)!==null&&r!==void 0?r:0),"flexShrink"in e&&t.setFlexShrink(typeof e.flexShrink=="number"?e.flexShrink:1),"flexDirection"in e&&(e.flexDirection==="row"&&t.setFlexDirection(bn.default.FLEX_DIRECTION_ROW),e.flexDirection==="row-reverse"&&t.setFlexDirection(bn.default.FLEX_DIRECTION_ROW_REVERSE),e.flexDirection==="column"&&t.setFlexDirection(bn.default.FLEX_DIRECTION_COLUMN),e.flexDirection==="column-reverse"&&t.setFlexDirection(bn.default.FLEX_DIRECTION_COLUMN_REVERSE)),"flexBasis"in e&&(typeof e.flexBasis=="number"?t.setFlexBasis(e.flexBasis):typeof e.flexBasis=="string"?t.setFlexBasisPercent(Number.parseInt(e.flexBasis,10)):t.setFlexBasis(NaN)),"alignItems"in e&&((e.alignItems==="stretch"||!e.alignItems)&&t.setAlignItems(bn.default.ALIGN_STRETCH),e.alignItems==="flex-start"&&t.setAlignItems(bn.default.ALIGN_FLEX_START),e.alignItems==="center"&&t.setAlignItems(bn.default.ALIGN_CENTER),e.alignItems==="flex-end"&&t.setAlignItems(bn.default.ALIGN_FLEX_END)),"alignSelf"in e&&((e.alignSelf==="auto"||!e.alignSelf)&&t.setAlignSelf(bn.default.ALIGN_AUTO),e.alignSelf==="flex-start"&&t.setAlignSelf(bn.default.ALIGN_FLEX_START),e.alignSelf==="center"&&t.setAlignSelf(bn.default.ALIGN_CENTER),e.alignSelf==="flex-end"&&t.setAlignSelf(bn.default.ALIGN_FLEX_END)),"justifyContent"in e&&((e.justifyContent==="flex-start"||!e.justifyContent)&&t.setJustifyContent(bn.default.JUSTIFY_FLEX_START),e.justifyContent==="center"&&t.setJustifyContent(bn.default.JUSTIFY_CENTER),e.justifyContent==="flex-end"&&t.setJustifyContent(bn.default.JUSTIFY_FLEX_END),e.justifyContent==="space-between"&&t.setJustifyContent(bn.default.JUSTIFY_SPACE_BETWEEN),e.justifyContent==="space-around"&&t.setJustifyContent(bn.default.JUSTIFY_SPACE_AROUND))},RPt=(t,e)=>{var r,s;"width"in e&&(typeof e.width=="number"?t.setWidth(e.width):typeof e.width=="string"?t.setWidthPercent(Number.parseInt(e.width,10)):t.setWidthAuto()),"height"in e&&(typeof e.height=="number"?t.setHeight(e.height):typeof e.height=="string"?t.setHeightPercent(Number.parseInt(e.height,10)):t.setHeightAuto()),"minWidth"in e&&(typeof e.minWidth=="string"?t.setMinWidthPercent(Number.parseInt(e.minWidth,10)):t.setMinWidth((r=e.minWidth)!==null&&r!==void 0?r:0)),"minHeight"in e&&(typeof e.minHeight=="string"?t.setMinHeightPercent(Number.parseInt(e.minHeight,10)):t.setMinHeight((s=e.minHeight)!==null&&s!==void 0?s:0))},FPt=(t,e)=>{"display"in e&&t.setDisplay(e.display==="flex"?bn.default.DISPLAY_FLEX:bn.default.DISPLAY_NONE)},NPt=(t,e)=>{if("borderStyle"in e){let r=typeof e.borderStyle=="string"?1:0;t.setBorder(bn.default.EDGE_TOP,r),t.setBorder(bn.default.EDGE_BOTTOM,r),t.setBorder(bn.default.EDGE_LEFT,r),t.setBorder(bn.default.EDGE_RIGHT,r)}};tD.default=(t,e={})=>{xPt(t,e),kPt(t,e),QPt(t,e),TPt(t,e),RPt(t,e),FPt(t,e),NPt(t,e)}});var VDe=L((chr,YDe)=>{"use strict";var rD=$S(),OPt=vk(),LPt=pk(),kW=new Set(["\x1B","\x9B"]),MPt=39,WDe=t=>`${kW.values().next().value}[${t}m`,_Pt=t=>t.split(" ").map(e=>rD(e)),xW=(t,e,r)=>{let s=[...e],a=!1,n=rD(OPt(t[t.length-1]));for(let[c,f]of s.entries()){let p=rD(f);if(n+p<=r?t[t.length-1]+=f:(t.push(f),n=0),kW.has(f))a=!0;else if(a&&f==="m"){a=!1;continue}a||(n+=p,n===r&&c0&&t.length>1&&(t[t.length-2]+=t.pop())},UPt=t=>{let e=t.split(" "),r=e.length;for(;r>0&&!(rD(e[r-1])>0);)r--;return r===e.length?t:e.slice(0,r).join(" ")+e.slice(r).join("")},HPt=(t,e,r={})=>{if(r.trim!==!1&&t.trim()==="")return"";let s="",a="",n,c=_Pt(t),f=[""];for(let[p,h]of t.split(" ").entries()){r.trim!==!1&&(f[f.length-1]=f[f.length-1].trimLeft());let E=rD(f[f.length-1]);if(p!==0&&(E>=e&&(r.wordWrap===!1||r.trim===!1)&&(f.push(""),E=0),(E>0||r.trim===!1)&&(f[f.length-1]+=" ",E++)),r.hard&&c[p]>e){let C=e-E,S=1+Math.floor((c[p]-C-1)/e);Math.floor((c[p]-1)/e)e&&E>0&&c[p]>0){if(r.wordWrap===!1&&Ee&&r.wordWrap===!1){xW(f,h,e);continue}f[f.length-1]+=h}r.trim!==!1&&(f=f.map(UPt)),s=f.join(` `);for(let[p,h]of[...s].entries()){if(a+=h,kW.has(h)){let C=parseFloat(/\d[^m]*/.exec(s.slice(p,p+4)));n=C===MPt?null:C}let E=LPt.codes.get(Number(n));n&&E&&(s[p+1]===` `?a+=WDe(E):h===` `&&(a+=WDe(n)))}return a};YDe.exports=(t,e,r)=>String(t).normalize().replace(/\r\n/g,` `).split(` `).map(s=>HPt(s,e,r)).join(` `)});var zDe=L((uhr,JDe)=>{"use strict";var KDe="[\uD800-\uDBFF][\uDC00-\uDFFF]",jPt=t=>t&&t.exact?new RegExp(`^${KDe}$`):new RegExp(KDe,"g");JDe.exports=jPt});var QW=L((fhr,ebe)=>{"use strict";var qPt=vW(),GPt=zDe(),ZDe=pk(),$De=["\x1B","\x9B"],OF=t=>`${$De[0]}[${t}m`,XDe=(t,e,r)=>{let s=[];t=[...t];for(let a of t){let n=a;a.match(";")&&(a=a.split(";")[0][0]+"0");let c=ZDe.codes.get(parseInt(a,10));if(c){let f=t.indexOf(c.toString());f>=0?t.splice(f,1):s.push(OF(e?c:n))}else if(e){s.push(OF(0));break}else s.push(OF(n))}if(e&&(s=s.filter((a,n)=>s.indexOf(a)===n),r!==void 0)){let a=OF(ZDe.codes.get(parseInt(r,10)));s=s.reduce((n,c)=>c===a?[c,...n]:[...n,c],[])}return s.join("")};ebe.exports=(t,e,r)=>{let s=[...t.normalize()],a=[];r=typeof r=="number"?r:s.length;let n=!1,c,f=0,p="";for(let[h,E]of s.entries()){let C=!1;if($De.includes(E)){let S=/\d[^m]*/.exec(t.slice(h,h+18));c=S&&S.length>0?S[0]:void 0,fe&&f<=r)p+=E;else if(f===e&&!n&&c!==void 0)p=XDe(a);else if(f>=r){p+=XDe(a,!0,c);break}}return p}});var rbe=L((Ahr,tbe)=>{"use strict";var X0=QW(),WPt=$S();function LF(t,e,r){if(t.charAt(e)===" ")return e;for(let s=1;s<=3;s++)if(r){if(t.charAt(e+s)===" ")return e+s}else if(t.charAt(e-s)===" ")return e-s;return e}tbe.exports=(t,e,r)=>{r={position:"end",preferTruncationOnSpace:!1,...r};let{position:s,space:a,preferTruncationOnSpace:n}=r,c="\u2026",f=1;if(typeof t!="string")throw new TypeError(`Expected \`input\` to be a string, got ${typeof t}`);if(typeof e!="number")throw new TypeError(`Expected \`columns\` to be a number, got ${typeof e}`);if(e<1)return"";if(e===1)return c;let p=WPt(t);if(p<=e)return t;if(s==="start"){if(n){let h=LF(t,p-e+1,!0);return c+X0(t,h,p).trim()}return a===!0&&(c+=" ",f=2),c+X0(t,p-e+f,p)}if(s==="middle"){a===!0&&(c=" "+c+" ",f=3);let h=Math.floor(e/2);if(n){let E=LF(t,h),C=LF(t,p-(e-h)+1,!0);return X0(t,0,E)+c+X0(t,C,p).trim()}return X0(t,0,h)+c+X0(t,p-(e-h)+f,p)}if(s==="end"){if(n){let h=LF(t,e-1);return X0(t,0,h)+c}return a===!0&&(c=" "+c,f=2),X0(t,0,e-f)+c}throw new Error(`Expected \`options.position\` to be either \`start\`, \`middle\` or \`end\`, got ${s}`)}});var RW=L(nD=>{"use strict";var nbe=nD&&nD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(nD,"__esModule",{value:!0});var YPt=nbe(VDe()),VPt=nbe(rbe()),TW={};nD.default=(t,e,r)=>{let s=t+String(e)+String(r);if(TW[s])return TW[s];let a=t;if(r==="wrap"&&(a=YPt.default(t,e,{trim:!1,hard:!0})),r.startsWith("truncate")){let n="end";r==="truncate-middle"&&(n="middle"),r==="truncate-start"&&(n="start"),a=VPt.default(t,e,{position:n})}return TW[s]=a,a}});var NW=L(FW=>{"use strict";Object.defineProperty(FW,"__esModule",{value:!0});var ibe=t=>{let e="";if(t.childNodes.length>0)for(let r of t.childNodes){let s="";r.nodeName==="#text"?s=r.nodeValue:((r.nodeName==="ink-text"||r.nodeName==="ink-virtual-text")&&(s=ibe(r)),s.length>0&&typeof r.internal_transform=="function"&&(s=r.internal_transform(s))),e+=s}return e};FW.default=ibe});var OW=L(xi=>{"use strict";var iD=xi&&xi.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(xi,"__esModule",{value:!0});xi.setTextNodeValue=xi.createTextNode=xi.setStyle=xi.setAttribute=xi.removeChildNode=xi.insertBeforeNode=xi.appendChildNode=xi.createNode=xi.TEXT_NAME=void 0;var KPt=iD(Rm()),sbe=iD(qDe()),JPt=iD(GDe()),zPt=iD(RW()),ZPt=iD(NW());xi.TEXT_NAME="#text";xi.createNode=t=>{var e;let r={nodeName:t,style:{},attributes:{},childNodes:[],parentNode:null,yogaNode:t==="ink-virtual-text"?void 0:KPt.default.Node.create()};return t==="ink-text"&&((e=r.yogaNode)===null||e===void 0||e.setMeasureFunc(XPt.bind(null,r))),r};xi.appendChildNode=(t,e)=>{var r;e.parentNode&&xi.removeChildNode(e.parentNode,e),e.parentNode=t,t.childNodes.push(e),e.yogaNode&&((r=t.yogaNode)===null||r===void 0||r.insertChild(e.yogaNode,t.yogaNode.getChildCount())),(t.nodeName==="ink-text"||t.nodeName==="ink-virtual-text")&&MF(t)};xi.insertBeforeNode=(t,e,r)=>{var s,a;e.parentNode&&xi.removeChildNode(e.parentNode,e),e.parentNode=t;let n=t.childNodes.indexOf(r);if(n>=0){t.childNodes.splice(n,0,e),e.yogaNode&&((s=t.yogaNode)===null||s===void 0||s.insertChild(e.yogaNode,n));return}t.childNodes.push(e),e.yogaNode&&((a=t.yogaNode)===null||a===void 0||a.insertChild(e.yogaNode,t.yogaNode.getChildCount())),(t.nodeName==="ink-text"||t.nodeName==="ink-virtual-text")&&MF(t)};xi.removeChildNode=(t,e)=>{var r,s;e.yogaNode&&((s=(r=e.parentNode)===null||r===void 0?void 0:r.yogaNode)===null||s===void 0||s.removeChild(e.yogaNode)),e.parentNode=null;let a=t.childNodes.indexOf(e);a>=0&&t.childNodes.splice(a,1),(t.nodeName==="ink-text"||t.nodeName==="ink-virtual-text")&&MF(t)};xi.setAttribute=(t,e,r)=>{t.attributes[e]=r};xi.setStyle=(t,e)=>{t.style=e,t.yogaNode&&JPt.default(t.yogaNode,e)};xi.createTextNode=t=>{let e={nodeName:"#text",nodeValue:t,yogaNode:void 0,parentNode:null,style:{}};return xi.setTextNodeValue(e,t),e};var XPt=function(t,e){var r,s;let a=t.nodeName==="#text"?t.nodeValue:ZPt.default(t),n=sbe.default(a);if(n.width<=e||n.width>=1&&e>0&&e<1)return n;let c=(s=(r=t.style)===null||r===void 0?void 0:r.textWrap)!==null&&s!==void 0?s:"wrap",f=zPt.default(a,e,c);return sbe.default(f)},obe=t=>{var e;if(!(!t||!t.parentNode))return(e=t.yogaNode)!==null&&e!==void 0?e:obe(t.parentNode)},MF=t=>{let e=obe(t);e?.markDirty()};xi.setTextNodeValue=(t,e)=>{typeof e!="string"&&(e=String(e)),t.nodeValue=e,MF(t)}});var fbe=L(sD=>{"use strict";var ube=sD&&sD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(sD,"__esModule",{value:!0});var abe=yW(),$Pt=ube(xDe()),lbe=ube(Rm()),ta=OW(),cbe=t=>{t?.unsetMeasureFunc(),t?.freeRecursive()};sD.default=$Pt.default({schedulePassiveEffects:abe.unstable_scheduleCallback,cancelPassiveEffects:abe.unstable_cancelCallback,now:Date.now,getRootHostContext:()=>({isInsideText:!1}),prepareForCommit:()=>null,preparePortalMount:()=>null,clearContainer:()=>!1,shouldDeprioritizeSubtree:()=>!1,resetAfterCommit:t=>{if(t.isStaticDirty){t.isStaticDirty=!1,typeof t.onImmediateRender=="function"&&t.onImmediateRender();return}typeof t.onRender=="function"&&t.onRender()},getChildHostContext:(t,e)=>{let r=t.isInsideText,s=e==="ink-text"||e==="ink-virtual-text";return r===s?t:{isInsideText:s}},shouldSetTextContent:()=>!1,createInstance:(t,e,r,s)=>{if(s.isInsideText&&t==="ink-box")throw new Error(" can\u2019t be nested inside component");let a=t==="ink-text"&&s.isInsideText?"ink-virtual-text":t,n=ta.createNode(a);for(let[c,f]of Object.entries(e))c!=="children"&&(c==="style"?ta.setStyle(n,f):c==="internal_transform"?n.internal_transform=f:c==="internal_static"?n.internal_static=!0:ta.setAttribute(n,c,f));return n},createTextInstance:(t,e,r)=>{if(!r.isInsideText)throw new Error(`Text string "${t}" must be rendered inside component`);return ta.createTextNode(t)},resetTextContent:()=>{},hideTextInstance:t=>{ta.setTextNodeValue(t,"")},unhideTextInstance:(t,e)=>{ta.setTextNodeValue(t,e)},getPublicInstance:t=>t,hideInstance:t=>{var e;(e=t.yogaNode)===null||e===void 0||e.setDisplay(lbe.default.DISPLAY_NONE)},unhideInstance:t=>{var e;(e=t.yogaNode)===null||e===void 0||e.setDisplay(lbe.default.DISPLAY_FLEX)},appendInitialChild:ta.appendChildNode,appendChild:ta.appendChildNode,insertBefore:ta.insertBeforeNode,finalizeInitialChildren:(t,e,r,s)=>(t.internal_static&&(s.isStaticDirty=!0,s.staticNode=t),!1),supportsMutation:!0,appendChildToContainer:ta.appendChildNode,insertInContainerBefore:ta.insertBeforeNode,removeChildFromContainer:(t,e)=>{ta.removeChildNode(t,e),cbe(e.yogaNode)},prepareUpdate:(t,e,r,s,a)=>{t.internal_static&&(a.isStaticDirty=!0);let n={},c=Object.keys(s);for(let f of c)if(s[f]!==r[f]){if(f==="style"&&typeof s.style=="object"&&typeof r.style=="object"){let h=s.style,E=r.style,C=Object.keys(h);for(let S of C){if(S==="borderStyle"||S==="borderColor"){if(typeof n.style!="object"){let P={};n.style=P}n.style.borderStyle=h.borderStyle,n.style.borderColor=h.borderColor}if(h[S]!==E[S]){if(typeof n.style!="object"){let P={};n.style=P}n.style[S]=h[S]}}continue}n[f]=s[f]}return n},commitUpdate:(t,e)=>{for(let[r,s]of Object.entries(e))r!=="children"&&(r==="style"?ta.setStyle(t,s):r==="internal_transform"?t.internal_transform=s:r==="internal_static"?t.internal_static=!0:ta.setAttribute(t,r,s))},commitTextUpdate:(t,e,r)=>{ta.setTextNodeValue(t,r)},removeChild:(t,e)=>{ta.removeChildNode(t,e),cbe(e.yogaNode)}})});var pbe=L((mhr,Abe)=>{"use strict";Abe.exports=(t,e=1,r)=>{if(r={indent:" ",includeEmptyLines:!1,...r},typeof t!="string")throw new TypeError(`Expected \`input\` to be a \`string\`, got \`${typeof t}\``);if(typeof e!="number")throw new TypeError(`Expected \`count\` to be a \`number\`, got \`${typeof e}\``);if(typeof r.indent!="string")throw new TypeError(`Expected \`options.indent\` to be a \`string\`, got \`${typeof r.indent}\``);if(e===0)return t;let s=r.includeEmptyLines?/^/gm:/^(?!\s*$)/gm;return t.replace(s,r.indent.repeat(e))}});var hbe=L(oD=>{"use strict";var ext=oD&&oD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(oD,"__esModule",{value:!0});var _F=ext(Rm());oD.default=t=>t.getComputedWidth()-t.getComputedPadding(_F.default.EDGE_LEFT)-t.getComputedPadding(_F.default.EDGE_RIGHT)-t.getComputedBorder(_F.default.EDGE_LEFT)-t.getComputedBorder(_F.default.EDGE_RIGHT)});var gbe=L((Ehr,txt)=>{txt.exports={single:{topLeft:"\u250C",topRight:"\u2510",bottomRight:"\u2518",bottomLeft:"\u2514",vertical:"\u2502",horizontal:"\u2500"},double:{topLeft:"\u2554",topRight:"\u2557",bottomRight:"\u255D",bottomLeft:"\u255A",vertical:"\u2551",horizontal:"\u2550"},round:{topLeft:"\u256D",topRight:"\u256E",bottomRight:"\u256F",bottomLeft:"\u2570",vertical:"\u2502",horizontal:"\u2500"},bold:{topLeft:"\u250F",topRight:"\u2513",bottomRight:"\u251B",bottomLeft:"\u2517",vertical:"\u2503",horizontal:"\u2501"},singleDouble:{topLeft:"\u2553",topRight:"\u2556",bottomRight:"\u255C",bottomLeft:"\u2559",vertical:"\u2551",horizontal:"\u2500"},doubleSingle:{topLeft:"\u2552",topRight:"\u2555",bottomRight:"\u255B",bottomLeft:"\u2558",vertical:"\u2502",horizontal:"\u2550"},classic:{topLeft:"+",topRight:"+",bottomRight:"+",bottomLeft:"+",vertical:"|",horizontal:"-"}}});var mbe=L((Ihr,LW)=>{"use strict";var dbe=gbe();LW.exports=dbe;LW.exports.default=dbe});var MW=L(lD=>{"use strict";var rxt=lD&&lD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(lD,"__esModule",{value:!0});var aD=rxt(kE()),nxt=/^(rgb|hsl|hsv|hwb)\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/,ixt=/^(ansi|ansi256)\(\s?(\d+)\s?\)$/,UF=(t,e)=>e==="foreground"?t:"bg"+t[0].toUpperCase()+t.slice(1);lD.default=(t,e,r)=>{if(!e)return t;if(e in aD.default){let a=UF(e,r);return aD.default[a](t)}if(e.startsWith("#")){let a=UF("hex",r);return aD.default[a](e)(t)}if(e.startsWith("ansi")){let a=ixt.exec(e);if(!a)return t;let n=UF(a[1],r),c=Number(a[2]);return aD.default[n](c)(t)}if(e.startsWith("rgb")||e.startsWith("hsl")||e.startsWith("hsv")||e.startsWith("hwb")){let a=nxt.exec(e);if(!a)return t;let n=UF(a[1],r),c=Number(a[2]),f=Number(a[3]),p=Number(a[4]);return aD.default[n](c,f,p)(t)}return t}});var Ebe=L(cD=>{"use strict";var ybe=cD&&cD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(cD,"__esModule",{value:!0});var sxt=ybe(mbe()),_W=ybe(MW());cD.default=(t,e,r,s)=>{if(typeof r.style.borderStyle=="string"){let a=r.yogaNode.getComputedWidth(),n=r.yogaNode.getComputedHeight(),c=r.style.borderColor,f=sxt.default[r.style.borderStyle],p=_W.default(f.topLeft+f.horizontal.repeat(a-2)+f.topRight,c,"foreground"),h=(_W.default(f.vertical,c,"foreground")+` `).repeat(n-2),E=_W.default(f.bottomLeft+f.horizontal.repeat(a-2)+f.bottomRight,c,"foreground");s.write(t,e,p,{transformers:[]}),s.write(t,e+1,h,{transformers:[]}),s.write(t+a-1,e+1,h,{transformers:[]}),s.write(t,e+n-1,E,{transformers:[]})}}});var Cbe=L(uD=>{"use strict";var Fm=uD&&uD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(uD,"__esModule",{value:!0});var oxt=Fm(Rm()),axt=Fm(bW()),lxt=Fm(pbe()),cxt=Fm(RW()),uxt=Fm(hbe()),fxt=Fm(NW()),Axt=Fm(Ebe()),pxt=(t,e)=>{var r;let s=(r=t.childNodes[0])===null||r===void 0?void 0:r.yogaNode;if(s){let a=s.getComputedLeft(),n=s.getComputedTop();e=` `.repeat(n)+lxt.default(e,a)}return e},Ibe=(t,e,r)=>{var s;let{offsetX:a=0,offsetY:n=0,transformers:c=[],skipStaticElements:f}=r;if(f&&t.internal_static)return;let{yogaNode:p}=t;if(p){if(p.getDisplay()===oxt.default.DISPLAY_NONE)return;let h=a+p.getComputedLeft(),E=n+p.getComputedTop(),C=c;if(typeof t.internal_transform=="function"&&(C=[t.internal_transform,...c]),t.nodeName==="ink-text"){let S=fxt.default(t);if(S.length>0){let P=axt.default(S),I=uxt.default(p);if(P>I){let R=(s=t.style.textWrap)!==null&&s!==void 0?s:"wrap";S=cxt.default(S,I,R)}S=pxt(t,S),e.write(h,E,S,{transformers:C})}return}if(t.nodeName==="ink-box"&&Axt.default(h,E,t,e),t.nodeName==="ink-root"||t.nodeName==="ink-box")for(let S of t.childNodes)Ibe(S,e,{offsetX:h,offsetY:E,transformers:C,skipStaticElements:f})}};uD.default=Ibe});var vbe=L(fD=>{"use strict";var Bbe=fD&&fD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(fD,"__esModule",{value:!0});var wbe=Bbe(QW()),hxt=Bbe($S()),UW=class{constructor(e){this.writes=[];let{width:r,height:s}=e;this.width=r,this.height=s}write(e,r,s,a){let{transformers:n}=a;s&&this.writes.push({x:e,y:r,text:s,transformers:n})}get(){let e=[];for(let s=0;ss.trimRight()).join(` `),height:e.length}}};fD.default=UW});var bbe=L(AD=>{"use strict";var HW=AD&&AD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(AD,"__esModule",{value:!0});var gxt=HW(Rm()),Sbe=HW(Cbe()),Dbe=HW(vbe());AD.default=(t,e)=>{var r;if(t.yogaNode.setWidth(e),t.yogaNode){t.yogaNode.calculateLayout(void 0,void 0,gxt.default.DIRECTION_LTR);let s=new Dbe.default({width:t.yogaNode.getComputedWidth(),height:t.yogaNode.getComputedHeight()});Sbe.default(t,s,{skipStaticElements:!0});let a;!((r=t.staticNode)===null||r===void 0)&&r.yogaNode&&(a=new Dbe.default({width:t.staticNode.yogaNode.getComputedWidth(),height:t.staticNode.yogaNode.getComputedHeight()}),Sbe.default(t.staticNode,a,{skipStaticElements:!1}));let{output:n,height:c}=s.get();return{output:n,outputHeight:c,staticOutput:a?`${a.get().output} `:""}}return{output:"",outputHeight:0,staticOutput:""}}});var Qbe=L((Dhr,kbe)=>{"use strict";var Pbe=Ie("stream"),xbe=["assert","count","countReset","debug","dir","dirxml","error","group","groupCollapsed","groupEnd","info","log","table","time","timeEnd","timeLog","trace","warn"],jW={},dxt=t=>{let e=new Pbe.PassThrough,r=new Pbe.PassThrough;e.write=a=>t("stdout",a),r.write=a=>t("stderr",a);let s=new console.Console(e,r);for(let a of xbe)jW[a]=console[a],console[a]=s[a];return()=>{for(let a of xbe)console[a]=jW[a];jW={}}};kbe.exports=dxt});var GW=L(qW=>{"use strict";Object.defineProperty(qW,"__esModule",{value:!0});qW.default=new WeakMap});var YW=L(WW=>{"use strict";Object.defineProperty(WW,"__esModule",{value:!0});var mxt=hn(),Tbe=mxt.createContext({exit:()=>{}});Tbe.displayName="InternalAppContext";WW.default=Tbe});var KW=L(VW=>{"use strict";Object.defineProperty(VW,"__esModule",{value:!0});var yxt=hn(),Rbe=yxt.createContext({stdin:void 0,setRawMode:()=>{},isRawModeSupported:!1,internal_exitOnCtrlC:!0});Rbe.displayName="InternalStdinContext";VW.default=Rbe});var zW=L(JW=>{"use strict";Object.defineProperty(JW,"__esModule",{value:!0});var Ext=hn(),Fbe=Ext.createContext({stdout:void 0,write:()=>{}});Fbe.displayName="InternalStdoutContext";JW.default=Fbe});var XW=L(ZW=>{"use strict";Object.defineProperty(ZW,"__esModule",{value:!0});var Ixt=hn(),Nbe=Ixt.createContext({stderr:void 0,write:()=>{}});Nbe.displayName="InternalStderrContext";ZW.default=Nbe});var HF=L($W=>{"use strict";Object.defineProperty($W,"__esModule",{value:!0});var Cxt=hn(),Obe=Cxt.createContext({activeId:void 0,add:()=>{},remove:()=>{},activate:()=>{},deactivate:()=>{},enableFocus:()=>{},disableFocus:()=>{},focusNext:()=>{},focusPrevious:()=>{},focus:()=>{}});Obe.displayName="InternalFocusContext";$W.default=Obe});var Mbe=L((Rhr,Lbe)=>{"use strict";var wxt=/[|\\{}()[\]^$+*?.-]/g;Lbe.exports=t=>{if(typeof t!="string")throw new TypeError("Expected a string");return t.replace(wxt,"\\$&")}});var jbe=L((Fhr,Hbe)=>{"use strict";var Bxt=Mbe(),vxt=typeof process=="object"&&process&&typeof process.cwd=="function"?process.cwd():".",Ube=[].concat(Ie("module").builtinModules,"bootstrap_node","node").map(t=>new RegExp(`(?:\\((?:node:)?${t}(?:\\.js)?:\\d+:\\d+\\)$|^\\s*at (?:node:)?${t}(?:\\.js)?:\\d+:\\d+$)`));Ube.push(/\((?:node:)?internal\/[^:]+:\d+:\d+\)$/,/\s*at (?:node:)?internal\/[^:]+:\d+:\d+$/,/\/\.node-spawn-wrap-\w+-\w+\/node:\d+:\d+\)?$/);var eY=class t{constructor(e){e={ignoredPackages:[],...e},"internals"in e||(e.internals=t.nodeInternals()),"cwd"in e||(e.cwd=vxt),this._cwd=e.cwd.replace(/\\/g,"/"),this._internals=[].concat(e.internals,Sxt(e.ignoredPackages)),this._wrapCallSite=e.wrapCallSite||!1}static nodeInternals(){return[...Ube]}clean(e,r=0){r=" ".repeat(r),Array.isArray(e)||(e=e.split(` `)),!/^\s*at /.test(e[0])&&/^\s*at /.test(e[1])&&(e=e.slice(1));let s=!1,a=null,n=[];return e.forEach(c=>{if(c=c.replace(/\\/g,"/"),this._internals.some(p=>p.test(c)))return;let f=/^\s*at /.test(c);s?c=c.trimEnd().replace(/^(\s+)at /,"$1"):(c=c.trim(),f&&(c=c.slice(3))),c=c.replace(`${this._cwd}/`,""),c&&(f?(a&&(n.push(a),a=null),n.push(c)):(s=!0,a=c))}),n.map(c=>`${r}${c} `).join("")}captureString(e,r=this.captureString){typeof e=="function"&&(r=e,e=1/0);let{stackTraceLimit:s}=Error;e&&(Error.stackTraceLimit=e);let a={};Error.captureStackTrace(a,r);let{stack:n}=a;return Error.stackTraceLimit=s,this.clean(n)}capture(e,r=this.capture){typeof e=="function"&&(r=e,e=1/0);let{prepareStackTrace:s,stackTraceLimit:a}=Error;Error.prepareStackTrace=(f,p)=>this._wrapCallSite?p.map(this._wrapCallSite):p,e&&(Error.stackTraceLimit=e);let n={};Error.captureStackTrace(n,r);let{stack:c}=n;return Object.assign(Error,{prepareStackTrace:s,stackTraceLimit:a}),c}at(e=this.at){let[r]=this.capture(1,e);if(!r)return{};let s={line:r.getLineNumber(),column:r.getColumnNumber()};_be(s,r.getFileName(),this._cwd),r.isConstructor()&&(s.constructor=!0),r.isEval()&&(s.evalOrigin=r.getEvalOrigin()),r.isNative()&&(s.native=!0);let a;try{a=r.getTypeName()}catch{}a&&a!=="Object"&&a!=="[object Object]"&&(s.type=a);let n=r.getFunctionName();n&&(s.function=n);let c=r.getMethodName();return c&&n!==c&&(s.method=c),s}parseLine(e){let r=e&&e.match(Dxt);if(!r)return null;let s=r[1]==="new",a=r[2],n=r[3],c=r[4],f=Number(r[5]),p=Number(r[6]),h=r[7],E=r[8],C=r[9],S=r[10]==="native",P=r[11]===")",I,R={};if(E&&(R.line=Number(E)),C&&(R.column=Number(C)),P&&h){let N=0;for(let U=h.length-1;U>0;U--)if(h.charAt(U)===")")N++;else if(h.charAt(U)==="("&&h.charAt(U-1)===" "&&(N--,N===-1&&h.charAt(U-1)===" ")){let W=h.slice(0,U-1);h=h.slice(U+1),a+=` (${W}`;break}}if(a){let N=a.match(bxt);N&&(a=N[1],I=N[2])}return _be(R,h,this._cwd),s&&(R.constructor=!0),n&&(R.evalOrigin=n,R.evalLine=f,R.evalColumn=p,R.evalFile=c&&c.replace(/\\/g,"/")),S&&(R.native=!0),a&&(R.function=a),I&&a!==I&&(R.method=I),R}};function _be(t,e,r){e&&(e=e.replace(/\\/g,"/"),e.startsWith(`${r}/`)&&(e=e.slice(r.length+1)),t.file=e)}function Sxt(t){if(t.length===0)return[];let e=t.map(r=>Bxt(r));return new RegExp(`[/\\\\]node_modules[/\\\\](?:${e.join("|")})[/\\\\][^:]+:\\d+:\\d+`)}var Dxt=new RegExp("^(?:\\s*at )?(?:(new) )?(?:(.*?) \\()?(?:eval at ([^ ]+) \\((.+?):(\\d+):(\\d+)\\), )?(?:(.+?):(\\d+):(\\d+)|(native))(\\)?)$"),bxt=/^(.*?) \[as (.*?)\]$/;Hbe.exports=eY});var Gbe=L((Nhr,qbe)=>{"use strict";qbe.exports=(t,e)=>t.replace(/^\t+/gm,r=>" ".repeat(r.length*(e||2)))});var Ybe=L((Ohr,Wbe)=>{"use strict";var Pxt=Gbe(),xxt=(t,e)=>{let r=[],s=t-e,a=t+e;for(let n=s;n<=a;n++)r.push(n);return r};Wbe.exports=(t,e,r)=>{if(typeof t!="string")throw new TypeError("Source code is missing.");if(!e||e<1)throw new TypeError("Line number must start from `1`.");if(t=Pxt(t).split(/\r?\n/),!(e>t.length))return r={around:3,...r},xxt(e,r.around).filter(s=>t[s-1]!==void 0).map(s=>({line:s,value:t[s-1]}))}});var jF=L(nf=>{"use strict";var kxt=nf&&nf.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r),Object.defineProperty(t,s,{enumerable:!0,get:function(){return e[r]}})}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),Qxt=nf&&nf.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),Txt=nf&&nf.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.hasOwnProperty.call(t,r)&&kxt(e,t,r);return Qxt(e,t),e},Rxt=nf&&nf.__rest||function(t,e){var r={};for(var s in t)Object.prototype.hasOwnProperty.call(t,s)&&e.indexOf(s)<0&&(r[s]=t[s]);if(t!=null&&typeof Object.getOwnPropertySymbols=="function")for(var a=0,s=Object.getOwnPropertySymbols(t);a{var{children:r}=t,s=Rxt(t,["children"]);let a=Object.assign(Object.assign({},s),{marginLeft:s.marginLeft||s.marginX||s.margin||0,marginRight:s.marginRight||s.marginX||s.margin||0,marginTop:s.marginTop||s.marginY||s.margin||0,marginBottom:s.marginBottom||s.marginY||s.margin||0,paddingLeft:s.paddingLeft||s.paddingX||s.padding||0,paddingRight:s.paddingRight||s.paddingX||s.padding||0,paddingTop:s.paddingTop||s.paddingY||s.padding||0,paddingBottom:s.paddingBottom||s.paddingY||s.padding||0});return Vbe.default.createElement("ink-box",{ref:e,style:a},r)});tY.displayName="Box";tY.defaultProps={flexDirection:"row",flexGrow:0,flexShrink:1};nf.default=tY});var iY=L(pD=>{"use strict";var rY=pD&&pD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(pD,"__esModule",{value:!0});var Fxt=rY(hn()),yw=rY(kE()),Kbe=rY(MW()),nY=({color:t,backgroundColor:e,dimColor:r,bold:s,italic:a,underline:n,strikethrough:c,inverse:f,wrap:p,children:h})=>{if(h==null)return null;let E=C=>(r&&(C=yw.default.dim(C)),t&&(C=Kbe.default(C,t,"foreground")),e&&(C=Kbe.default(C,e,"background")),s&&(C=yw.default.bold(C)),a&&(C=yw.default.italic(C)),n&&(C=yw.default.underline(C)),c&&(C=yw.default.strikethrough(C)),f&&(C=yw.default.inverse(C)),C);return Fxt.default.createElement("ink-text",{style:{flexGrow:0,flexShrink:1,flexDirection:"row",textWrap:p},internal_transform:E},h)};nY.displayName="Text";nY.defaultProps={dimColor:!1,bold:!1,italic:!1,underline:!1,strikethrough:!1,wrap:"wrap"};pD.default=nY});var Xbe=L(sf=>{"use strict";var Nxt=sf&&sf.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r),Object.defineProperty(t,s,{enumerable:!0,get:function(){return e[r]}})}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),Oxt=sf&&sf.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),Lxt=sf&&sf.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.hasOwnProperty.call(t,r)&&Nxt(e,t,r);return Oxt(e,t),e},hD=sf&&sf.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(sf,"__esModule",{value:!0});var Jbe=Lxt(Ie("fs")),Rs=hD(hn()),zbe=hD(jbe()),Mxt=hD(Ybe()),th=hD(jF()),hA=hD(iY()),Zbe=new zbe.default({cwd:process.cwd(),internals:zbe.default.nodeInternals()}),_xt=({error:t})=>{let e=t.stack?t.stack.split(` `).slice(1):void 0,r=e?Zbe.parseLine(e[0]):void 0,s,a=0;if(r?.file&&r?.line&&Jbe.existsSync(r.file)){let n=Jbe.readFileSync(r.file,"utf8");if(s=Mxt.default(n,r.line),s)for(let{line:c}of s)a=Math.max(a,String(c).length)}return Rs.default.createElement(th.default,{flexDirection:"column",padding:1},Rs.default.createElement(th.default,null,Rs.default.createElement(hA.default,{backgroundColor:"red",color:"white"}," ","ERROR"," "),Rs.default.createElement(hA.default,null," ",t.message)),r&&Rs.default.createElement(th.default,{marginTop:1},Rs.default.createElement(hA.default,{dimColor:!0},r.file,":",r.line,":",r.column)),r&&s&&Rs.default.createElement(th.default,{marginTop:1,flexDirection:"column"},s.map(({line:n,value:c})=>Rs.default.createElement(th.default,{key:n},Rs.default.createElement(th.default,{width:a+1},Rs.default.createElement(hA.default,{dimColor:n!==r.line,backgroundColor:n===r.line?"red":void 0,color:n===r.line?"white":void 0},String(n).padStart(a," "),":")),Rs.default.createElement(hA.default,{key:n,backgroundColor:n===r.line?"red":void 0,color:n===r.line?"white":void 0}," "+c)))),t.stack&&Rs.default.createElement(th.default,{marginTop:1,flexDirection:"column"},t.stack.split(` `).slice(1).map(n=>{let c=Zbe.parseLine(n);return c?Rs.default.createElement(th.default,{key:n},Rs.default.createElement(hA.default,{dimColor:!0},"- "),Rs.default.createElement(hA.default,{dimColor:!0,bold:!0},c.function),Rs.default.createElement(hA.default,{dimColor:!0,color:"gray"}," ","(",c.file,":",c.line,":",c.column,")")):Rs.default.createElement(th.default,{key:n},Rs.default.createElement(hA.default,{dimColor:!0},"- "),Rs.default.createElement(hA.default,{dimColor:!0,bold:!0},n))})))};sf.default=_xt});var ePe=L(of=>{"use strict";var Uxt=of&&of.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r),Object.defineProperty(t,s,{enumerable:!0,get:function(){return e[r]}})}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),Hxt=of&&of.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),jxt=of&&of.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.hasOwnProperty.call(t,r)&&Uxt(e,t,r);return Hxt(e,t),e},Om=of&&of.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(of,"__esModule",{value:!0});var Nm=jxt(hn()),$be=Om(oW()),qxt=Om(YW()),Gxt=Om(KW()),Wxt=Om(zW()),Yxt=Om(XW()),Vxt=Om(HF()),Kxt=Om(Xbe()),Jxt=" ",zxt="\x1B[Z",Zxt="\x1B",qF=class extends Nm.PureComponent{constructor(){super(...arguments),this.state={isFocusEnabled:!0,activeFocusId:void 0,focusables:[],error:void 0},this.rawModeEnabledCount=0,this.handleSetRawMode=e=>{let{stdin:r}=this.props;if(!this.isRawModeSupported())throw r===process.stdin?new Error(`Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default. Read about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported`):new Error(`Raw mode is not supported on the stdin provided to Ink. Read about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported`);if(r.setEncoding("utf8"),e){this.rawModeEnabledCount===0&&(r.addListener("data",this.handleInput),r.resume(),r.setRawMode(!0)),this.rawModeEnabledCount++;return}--this.rawModeEnabledCount===0&&(r.setRawMode(!1),r.removeListener("data",this.handleInput),r.pause())},this.handleInput=e=>{e===""&&this.props.exitOnCtrlC&&this.handleExit(),e===Zxt&&this.state.activeFocusId&&this.setState({activeFocusId:void 0}),this.state.isFocusEnabled&&this.state.focusables.length>0&&(e===Jxt&&this.focusNext(),e===zxt&&this.focusPrevious())},this.handleExit=e=>{this.isRawModeSupported()&&this.handleSetRawMode(!1),this.props.onExit(e)},this.enableFocus=()=>{this.setState({isFocusEnabled:!0})},this.disableFocus=()=>{this.setState({isFocusEnabled:!1})},this.focus=e=>{this.setState(r=>r.focusables.some(a=>a?.id===e)?{activeFocusId:e}:r)},this.focusNext=()=>{this.setState(e=>{var r;let s=(r=e.focusables[0])===null||r===void 0?void 0:r.id;return{activeFocusId:this.findNextFocusable(e)||s}})},this.focusPrevious=()=>{this.setState(e=>{var r;let s=(r=e.focusables[e.focusables.length-1])===null||r===void 0?void 0:r.id;return{activeFocusId:this.findPreviousFocusable(e)||s}})},this.addFocusable=(e,{autoFocus:r})=>{this.setState(s=>{let a=s.activeFocusId;return!a&&r&&(a=e),{activeFocusId:a,focusables:[...s.focusables,{id:e,isActive:!0}]}})},this.removeFocusable=e=>{this.setState(r=>({activeFocusId:r.activeFocusId===e?void 0:r.activeFocusId,focusables:r.focusables.filter(s=>s.id!==e)}))},this.activateFocusable=e=>{this.setState(r=>({focusables:r.focusables.map(s=>s.id!==e?s:{id:e,isActive:!0})}))},this.deactivateFocusable=e=>{this.setState(r=>({activeFocusId:r.activeFocusId===e?void 0:r.activeFocusId,focusables:r.focusables.map(s=>s.id!==e?s:{id:e,isActive:!1})}))},this.findNextFocusable=e=>{var r;let s=e.focusables.findIndex(a=>a.id===e.activeFocusId);for(let a=s+1;a{var r;let s=e.focusables.findIndex(a=>a.id===e.activeFocusId);for(let a=s-1;a>=0;a--)if(!((r=e.focusables[a])===null||r===void 0)&&r.isActive)return e.focusables[a].id}}static getDerivedStateFromError(e){return{error:e}}isRawModeSupported(){return this.props.stdin.isTTY}render(){return Nm.default.createElement(qxt.default.Provider,{value:{exit:this.handleExit}},Nm.default.createElement(Gxt.default.Provider,{value:{stdin:this.props.stdin,setRawMode:this.handleSetRawMode,isRawModeSupported:this.isRawModeSupported(),internal_exitOnCtrlC:this.props.exitOnCtrlC}},Nm.default.createElement(Wxt.default.Provider,{value:{stdout:this.props.stdout,write:this.props.writeToStdout}},Nm.default.createElement(Yxt.default.Provider,{value:{stderr:this.props.stderr,write:this.props.writeToStderr}},Nm.default.createElement(Vxt.default.Provider,{value:{activeId:this.state.activeFocusId,add:this.addFocusable,remove:this.removeFocusable,activate:this.activateFocusable,deactivate:this.deactivateFocusable,enableFocus:this.enableFocus,disableFocus:this.disableFocus,focusNext:this.focusNext,focusPrevious:this.focusPrevious,focus:this.focus}},this.state.error?Nm.default.createElement(Kxt.default,{error:this.state.error}):this.props.children)))))}componentDidMount(){$be.default.hide(this.props.stdout)}componentWillUnmount(){$be.default.show(this.props.stdout),this.isRawModeSupported()&&this.handleSetRawMode(!1)}componentDidCatch(e){this.handleExit(e)}};of.default=qF;qF.displayName="InternalApp"});var nPe=L(af=>{"use strict";var Xxt=af&&af.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r),Object.defineProperty(t,s,{enumerable:!0,get:function(){return e[r]}})}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),$xt=af&&af.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),ekt=af&&af.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.hasOwnProperty.call(t,r)&&Xxt(e,t,r);return $xt(e,t),e},lf=af&&af.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(af,"__esModule",{value:!0});var tkt=lf(hn()),tPe=V8(),rkt=lf(fDe()),nkt=lf(tW()),ikt=lf(mDe()),skt=lf(EDe()),sY=lf(fbe()),okt=lf(bbe()),akt=lf(sW()),lkt=lf(Qbe()),ckt=ekt(OW()),ukt=lf(GW()),fkt=lf(ePe()),Ew=process.env.CI==="false"?!1:ikt.default,rPe=()=>{},oY=class{constructor(e){this.resolveExitPromise=()=>{},this.rejectExitPromise=()=>{},this.unsubscribeExit=()=>{},this.onRender=()=>{if(this.isUnmounted)return;let{output:r,outputHeight:s,staticOutput:a}=okt.default(this.rootNode,this.options.stdout.columns||80),n=a&&a!==` `;if(this.options.debug){n&&(this.fullStaticOutput+=a),this.options.stdout.write(this.fullStaticOutput+r);return}if(Ew){n&&this.options.stdout.write(a),this.lastOutput=r;return}if(n&&(this.fullStaticOutput+=a),s>=this.options.stdout.rows){this.options.stdout.write(nkt.default.clearTerminal+this.fullStaticOutput+r),this.lastOutput=r;return}n&&(this.log.clear(),this.options.stdout.write(a),this.log(r)),!n&&r!==this.lastOutput&&this.throttledLog(r),this.lastOutput=r},skt.default(this),this.options=e,this.rootNode=ckt.createNode("ink-root"),this.rootNode.onRender=e.debug?this.onRender:tPe(this.onRender,32,{leading:!0,trailing:!0}),this.rootNode.onImmediateRender=this.onRender,this.log=rkt.default.create(e.stdout),this.throttledLog=e.debug?this.log:tPe(this.log,void 0,{leading:!0,trailing:!0}),this.isUnmounted=!1,this.lastOutput="",this.fullStaticOutput="",this.container=sY.default.createContainer(this.rootNode,0,!1,null),this.unsubscribeExit=akt.default(this.unmount,{alwaysLast:!1}),e.patchConsole&&this.patchConsole(),Ew||(e.stdout.on("resize",this.onRender),this.unsubscribeResize=()=>{e.stdout.off("resize",this.onRender)})}render(e){let r=tkt.default.createElement(fkt.default,{stdin:this.options.stdin,stdout:this.options.stdout,stderr:this.options.stderr,writeToStdout:this.writeToStdout,writeToStderr:this.writeToStderr,exitOnCtrlC:this.options.exitOnCtrlC,onExit:this.unmount},e);sY.default.updateContainer(r,this.container,null,rPe)}writeToStdout(e){if(!this.isUnmounted){if(this.options.debug){this.options.stdout.write(e+this.fullStaticOutput+this.lastOutput);return}if(Ew){this.options.stdout.write(e);return}this.log.clear(),this.options.stdout.write(e),this.log(this.lastOutput)}}writeToStderr(e){if(!this.isUnmounted){if(this.options.debug){this.options.stderr.write(e),this.options.stdout.write(this.fullStaticOutput+this.lastOutput);return}if(Ew){this.options.stderr.write(e);return}this.log.clear(),this.options.stderr.write(e),this.log(this.lastOutput)}}unmount(e){this.isUnmounted||(this.onRender(),this.unsubscribeExit(),typeof this.restoreConsole=="function"&&this.restoreConsole(),typeof this.unsubscribeResize=="function"&&this.unsubscribeResize(),Ew?this.options.stdout.write(this.lastOutput+` `):this.options.debug||this.log.done(),this.isUnmounted=!0,sY.default.updateContainer(null,this.container,null,rPe),ukt.default.delete(this.options.stdout),e instanceof Error?this.rejectExitPromise(e):this.resolveExitPromise())}waitUntilExit(){return this.exitPromise||(this.exitPromise=new Promise((e,r)=>{this.resolveExitPromise=e,this.rejectExitPromise=r})),this.exitPromise}clear(){!Ew&&!this.options.debug&&this.log.clear()}patchConsole(){this.options.debug||(this.restoreConsole=lkt.default((e,r)=>{e==="stdout"&&this.writeToStdout(r),e==="stderr"&&(r.startsWith("The above error occurred")||this.writeToStderr(r))}))}};af.default=oY});var sPe=L(gD=>{"use strict";var iPe=gD&&gD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(gD,"__esModule",{value:!0});var Akt=iPe(nPe()),GF=iPe(GW()),pkt=Ie("stream"),hkt=(t,e)=>{let r=Object.assign({stdout:process.stdout,stdin:process.stdin,stderr:process.stderr,debug:!1,exitOnCtrlC:!0,patchConsole:!0},gkt(e)),s=dkt(r.stdout,()=>new Akt.default(r));return s.render(t),{rerender:s.render,unmount:()=>s.unmount(),waitUntilExit:s.waitUntilExit,cleanup:()=>GF.default.delete(r.stdout),clear:s.clear}};gD.default=hkt;var gkt=(t={})=>t instanceof pkt.Stream?{stdout:t,stdin:process.stdin}:t,dkt=(t,e)=>{let r;return GF.default.has(t)?r=GF.default.get(t):(r=e(),GF.default.set(t,r)),r}});var aPe=L(rh=>{"use strict";var mkt=rh&&rh.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r),Object.defineProperty(t,s,{enumerable:!0,get:function(){return e[r]}})}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),ykt=rh&&rh.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),Ekt=rh&&rh.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.hasOwnProperty.call(t,r)&&mkt(e,t,r);return ykt(e,t),e};Object.defineProperty(rh,"__esModule",{value:!0});var dD=Ekt(hn()),oPe=t=>{let{items:e,children:r,style:s}=t,[a,n]=dD.useState(0),c=dD.useMemo(()=>e.slice(a),[e,a]);dD.useLayoutEffect(()=>{n(e.length)},[e.length]);let f=c.map((h,E)=>r(h,a+E)),p=dD.useMemo(()=>Object.assign({position:"absolute",flexDirection:"column"},s),[s]);return dD.default.createElement("ink-box",{internal_static:!0,style:p},f)};oPe.displayName="Static";rh.default=oPe});var cPe=L(mD=>{"use strict";var Ikt=mD&&mD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(mD,"__esModule",{value:!0});var Ckt=Ikt(hn()),lPe=({children:t,transform:e})=>t==null?null:Ckt.default.createElement("ink-text",{style:{flexGrow:0,flexShrink:1,flexDirection:"row"},internal_transform:e},t);lPe.displayName="Transform";mD.default=lPe});var fPe=L(yD=>{"use strict";var wkt=yD&&yD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(yD,"__esModule",{value:!0});var Bkt=wkt(hn()),uPe=({count:t=1})=>Bkt.default.createElement("ink-text",null,` `.repeat(t));uPe.displayName="Newline";yD.default=uPe});var hPe=L(ED=>{"use strict";var APe=ED&&ED.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(ED,"__esModule",{value:!0});var vkt=APe(hn()),Skt=APe(jF()),pPe=()=>vkt.default.createElement(Skt.default,{flexGrow:1});pPe.displayName="Spacer";ED.default=pPe});var WF=L(ID=>{"use strict";var Dkt=ID&&ID.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(ID,"__esModule",{value:!0});var bkt=hn(),Pkt=Dkt(KW()),xkt=()=>bkt.useContext(Pkt.default);ID.default=xkt});var dPe=L(CD=>{"use strict";var kkt=CD&&CD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(CD,"__esModule",{value:!0});var gPe=hn(),Qkt=kkt(WF()),Tkt=(t,e={})=>{let{stdin:r,setRawMode:s,internal_exitOnCtrlC:a}=Qkt.default();gPe.useEffect(()=>{if(e.isActive!==!1)return s(!0),()=>{s(!1)}},[e.isActive,s]),gPe.useEffect(()=>{if(e.isActive===!1)return;let n=c=>{let f=String(c),p={upArrow:f==="\x1B[A",downArrow:f==="\x1B[B",leftArrow:f==="\x1B[D",rightArrow:f==="\x1B[C",pageDown:f==="\x1B[6~",pageUp:f==="\x1B[5~",return:f==="\r",escape:f==="\x1B",ctrl:!1,shift:!1,tab:f===" "||f==="\x1B[Z",backspace:f==="\b",delete:f==="\x7F"||f==="\x1B[3~",meta:!1};f<=""&&!p.return&&(f=String.fromCharCode(f.charCodeAt(0)+97-1),p.ctrl=!0),f.startsWith("\x1B")&&(f=f.slice(1),p.meta=!0);let h=f>="A"&&f<="Z",E=f>="\u0410"&&f<="\u042F";f.length===1&&(h||E)&&(p.shift=!0),p.tab&&f==="[Z"&&(p.shift=!0),(p.tab||p.backspace||p.delete)&&(f=""),(!(f==="c"&&p.ctrl)||!a)&&t(f,p)};return r?.on("data",n),()=>{r?.off("data",n)}},[e.isActive,r,a,t])};CD.default=Tkt});var mPe=L(wD=>{"use strict";var Rkt=wD&&wD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(wD,"__esModule",{value:!0});var Fkt=hn(),Nkt=Rkt(YW()),Okt=()=>Fkt.useContext(Nkt.default);wD.default=Okt});var yPe=L(BD=>{"use strict";var Lkt=BD&&BD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(BD,"__esModule",{value:!0});var Mkt=hn(),_kt=Lkt(zW()),Ukt=()=>Mkt.useContext(_kt.default);BD.default=Ukt});var EPe=L(vD=>{"use strict";var Hkt=vD&&vD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(vD,"__esModule",{value:!0});var jkt=hn(),qkt=Hkt(XW()),Gkt=()=>jkt.useContext(qkt.default);vD.default=Gkt});var CPe=L(DD=>{"use strict";var IPe=DD&&DD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(DD,"__esModule",{value:!0});var SD=hn(),Wkt=IPe(HF()),Ykt=IPe(WF()),Vkt=({isActive:t=!0,autoFocus:e=!1,id:r}={})=>{let{isRawModeSupported:s,setRawMode:a}=Ykt.default(),{activeId:n,add:c,remove:f,activate:p,deactivate:h,focus:E}=SD.useContext(Wkt.default),C=SD.useMemo(()=>r??Math.random().toString().slice(2,7),[r]);return SD.useEffect(()=>(c(C,{autoFocus:e}),()=>{f(C)}),[C,e]),SD.useEffect(()=>{t?p(C):h(C)},[t,C]),SD.useEffect(()=>{if(!(!s||!t))return a(!0),()=>{a(!1)}},[t]),{isFocused:!!C&&n===C,focus:E}};DD.default=Vkt});var wPe=L(bD=>{"use strict";var Kkt=bD&&bD.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(bD,"__esModule",{value:!0});var Jkt=hn(),zkt=Kkt(HF()),Zkt=()=>{let t=Jkt.useContext(zkt.default);return{enableFocus:t.enableFocus,disableFocus:t.disableFocus,focusNext:t.focusNext,focusPrevious:t.focusPrevious,focus:t.focus}};bD.default=Zkt});var BPe=L(aY=>{"use strict";Object.defineProperty(aY,"__esModule",{value:!0});aY.default=t=>{var e,r,s,a;return{width:(r=(e=t.yogaNode)===null||e===void 0?void 0:e.getComputedWidth())!==null&&r!==void 0?r:0,height:(a=(s=t.yogaNode)===null||s===void 0?void 0:s.getComputedHeight())!==null&&a!==void 0?a:0}}});var Vc=L(Eo=>{"use strict";Object.defineProperty(Eo,"__esModule",{value:!0});var Xkt=sPe();Object.defineProperty(Eo,"render",{enumerable:!0,get:function(){return Xkt.default}});var $kt=jF();Object.defineProperty(Eo,"Box",{enumerable:!0,get:function(){return $kt.default}});var eQt=iY();Object.defineProperty(Eo,"Text",{enumerable:!0,get:function(){return eQt.default}});var tQt=aPe();Object.defineProperty(Eo,"Static",{enumerable:!0,get:function(){return tQt.default}});var rQt=cPe();Object.defineProperty(Eo,"Transform",{enumerable:!0,get:function(){return rQt.default}});var nQt=fPe();Object.defineProperty(Eo,"Newline",{enumerable:!0,get:function(){return nQt.default}});var iQt=hPe();Object.defineProperty(Eo,"Spacer",{enumerable:!0,get:function(){return iQt.default}});var sQt=dPe();Object.defineProperty(Eo,"useInput",{enumerable:!0,get:function(){return sQt.default}});var oQt=mPe();Object.defineProperty(Eo,"useApp",{enumerable:!0,get:function(){return oQt.default}});var aQt=WF();Object.defineProperty(Eo,"useStdin",{enumerable:!0,get:function(){return aQt.default}});var lQt=yPe();Object.defineProperty(Eo,"useStdout",{enumerable:!0,get:function(){return lQt.default}});var cQt=EPe();Object.defineProperty(Eo,"useStderr",{enumerable:!0,get:function(){return cQt.default}});var uQt=CPe();Object.defineProperty(Eo,"useFocus",{enumerable:!0,get:function(){return uQt.default}});var fQt=wPe();Object.defineProperty(Eo,"useFocusManager",{enumerable:!0,get:function(){return fQt.default}});var AQt=BPe();Object.defineProperty(Eo,"measureElement",{enumerable:!0,get:function(){return AQt.default}})});var cY={};Vt(cY,{Gem:()=>lY});var vPe,Lm,lY,YF=Ct(()=>{vPe=et(Vc()),Lm=et(hn()),lY=(0,Lm.memo)(({active:t})=>{let e=(0,Lm.useMemo)(()=>t?"\u25C9":"\u25EF",[t]),r=(0,Lm.useMemo)(()=>t?"green":"yellow",[t]);return Lm.default.createElement(vPe.Text,{color:r},e)})});var DPe={};Vt(DPe,{useKeypress:()=>Mm});function Mm({active:t},e,r){let{stdin:s}=(0,SPe.useStdin)(),a=(0,VF.useCallback)((n,c)=>e(n,c),r);(0,VF.useEffect)(()=>{if(!(!t||!s))return s.on("keypress",a),()=>{s.off("keypress",a)}},[t,a,s])}var SPe,VF,PD=Ct(()=>{SPe=et(Vc()),VF=et(hn())});var PPe={};Vt(PPe,{FocusRequest:()=>bPe,useFocusRequest:()=>uY});var bPe,uY,fY=Ct(()=>{PD();bPe=(r=>(r.BEFORE="before",r.AFTER="after",r))(bPe||{}),uY=function({active:t},e,r){Mm({active:t},(s,a)=>{a.name==="tab"&&(a.shift?e("before"):e("after"))},r)}});var xPe={};Vt(xPe,{useListInput:()=>xD});var xD,KF=Ct(()=>{PD();xD=function(t,e,{active:r,minus:s,plus:a,set:n,loop:c=!0}){Mm({active:r},(f,p)=>{let h=e.indexOf(t);switch(p.name){case s:{let E=h-1;if(c){n(e[(e.length+E)%e.length]);return}if(E<0)return;n(e[E])}break;case a:{let E=h+1;if(c){n(e[E%e.length]);return}if(E>=e.length)return;n(e[E])}break}},[e,t,a,n,c])}});var JF={};Vt(JF,{ScrollableItems:()=>pQt});var $0,ml,pQt,zF=Ct(()=>{$0=et(Vc()),ml=et(hn());fY();KF();pQt=({active:t=!0,children:e=[],radius:r=10,size:s=1,loop:a=!0,onFocusRequest:n,willReachEnd:c})=>{let f=N=>{if(N.key===null)throw new Error("Expected all children to have a key");return N.key},p=ml.default.Children.map(e,N=>f(N)),h=p[0],[E,C]=(0,ml.useState)(h),S=p.indexOf(E);(0,ml.useEffect)(()=>{p.includes(E)||C(h)},[e]),(0,ml.useEffect)(()=>{c&&S>=p.length-2&&c()},[S]),uY({active:t&&!!n},N=>{n?.(N)},[n]),xD(E,p,{active:t,minus:"up",plus:"down",set:C,loop:a});let P=S-r,I=S+r;I>p.length&&(P-=I-p.length,I=p.length),P<0&&(I+=-P,P=0),I>=p.length&&(I=p.length-1);let R=[];for(let N=P;N<=I;++N){let U=p[N],W=t&&U===E;R.push(ml.default.createElement($0.Box,{key:U,height:s},ml.default.createElement($0.Box,{marginLeft:1,marginRight:1},ml.default.createElement($0.Text,null,W?ml.default.createElement($0.Text,{color:"cyan",bold:!0},">"):" ")),ml.default.createElement($0.Box,null,ml.default.cloneElement(e[N],{active:W}))))}return ml.default.createElement($0.Box,{flexDirection:"column",width:"100%"},R)}});var kPe,nh,QPe,AY,TPe,pY=Ct(()=>{kPe=et(Vc()),nh=et(hn()),QPe=Ie("readline"),AY=nh.default.createContext(null),TPe=({children:t})=>{let{stdin:e,setRawMode:r}=(0,kPe.useStdin)();(0,nh.useEffect)(()=>{r&&r(!0),e&&(0,QPe.emitKeypressEvents)(e)},[e,r]);let[s,a]=(0,nh.useState)(new Map),n=(0,nh.useMemo)(()=>({getAll:()=>s,get:c=>s.get(c),set:(c,f)=>a(new Map([...s,[c,f]]))}),[s,a]);return nh.default.createElement(AY.Provider,{value:n,children:t})}});var hY={};Vt(hY,{useMinistore:()=>hQt});function hQt(t,e){let r=(0,ZF.useContext)(AY);if(r===null)throw new Error("Expected this hook to run with a ministore context attached");if(typeof t>"u")return r.getAll();let s=(0,ZF.useCallback)(n=>{r.set(t,n)},[t,r.set]),a=r.get(t);return typeof a>"u"&&(a=e),[a,s]}var ZF,gY=Ct(()=>{ZF=et(hn());pY()});var $F={};Vt($F,{renderForm:()=>gQt});async function gQt(t,e,{stdin:r,stdout:s,stderr:a}){let n,c=p=>{let{exit:h}=(0,XF.useApp)();Mm({active:!0},(E,C)=>{C.name==="return"&&(n=p,h())},[h,p])},{waitUntilExit:f}=(0,XF.render)(dY.default.createElement(TPe,null,dY.default.createElement(t,{...e,useSubmit:c})),{stdin:r,stdout:s,stderr:a});return await f(),n}var XF,dY,eN=Ct(()=>{XF=et(Vc()),dY=et(hn());pY();PD()});var OPe=L(kD=>{"use strict";Object.defineProperty(kD,"__esModule",{value:!0});kD.UncontrolledTextInput=void 0;var FPe=hn(),mY=hn(),RPe=Vc(),_m=kE(),NPe=({value:t,placeholder:e="",focus:r=!0,mask:s,highlightPastedText:a=!1,showCursor:n=!0,onChange:c,onSubmit:f})=>{let[{cursorOffset:p,cursorWidth:h},E]=mY.useState({cursorOffset:(t||"").length,cursorWidth:0});mY.useEffect(()=>{E(R=>{if(!r||!n)return R;let N=t||"";return R.cursorOffset>N.length-1?{cursorOffset:N.length,cursorWidth:0}:R})},[t,r,n]);let C=a?h:0,S=s?s.repeat(t.length):t,P=S,I=e?_m.grey(e):void 0;if(n&&r){I=e.length>0?_m.inverse(e[0])+_m.grey(e.slice(1)):_m.inverse(" "),P=S.length>0?"":_m.inverse(" ");let R=0;for(let N of S)R>=p-C&&R<=p?P+=_m.inverse(N):P+=N,R++;S.length>0&&p===S.length&&(P+=_m.inverse(" "))}return RPe.useInput((R,N)=>{if(N.upArrow||N.downArrow||N.ctrl&&R==="c"||N.tab||N.shift&&N.tab)return;if(N.return){f&&f(t);return}let U=p,W=t,te=0;N.leftArrow?n&&U--:N.rightArrow?n&&U++:N.backspace||N.delete?p>0&&(W=t.slice(0,p-1)+t.slice(p,t.length),U--):(W=t.slice(0,p)+R+t.slice(p,t.length),U+=R.length,R.length>1&&(te=R.length)),p<0&&(U=0),p>t.length&&(U=t.length),E({cursorOffset:U,cursorWidth:te}),W!==t&&c(W)},{isActive:r}),FPe.createElement(RPe.Text,null,e?S.length>0?P:I:P)};kD.default=NPe;kD.UncontrolledTextInput=({initialValue:t="",...e})=>{let[r,s]=mY.useState(t);return FPe.createElement(NPe,Object.assign({},e,{value:r,onChange:s}))}});var _Pe={};Vt(_Pe,{Pad:()=>yY});var LPe,MPe,yY,EY=Ct(()=>{LPe=et(Vc()),MPe=et(hn()),yY=({length:t,active:e})=>{if(t===0)return null;let r=t>1?` ${"-".repeat(t-1)}`:" ";return MPe.default.createElement(LPe.Text,{dimColor:!e},r)}});var UPe={};Vt(UPe,{ItemOptions:()=>dQt});var TD,eg,dQt,HPe=Ct(()=>{TD=et(Vc()),eg=et(hn());KF();YF();EY();dQt=function({active:t,skewer:e,options:r,value:s,onChange:a,sizes:n=[]}){let c=r.filter(({label:p})=>!!p).map(({value:p})=>p),f=r.findIndex(p=>p.value===s&&p.label!="");return xD(s,c,{active:t,minus:"left",plus:"right",set:a}),eg.default.createElement(eg.default.Fragment,null,r.map(({label:p},h)=>{let E=h===f,C=n[h]-1||0,S=p.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,""),P=Math.max(0,C-S.length-2);return p?eg.default.createElement(TD.Box,{key:p,width:C,marginLeft:1},eg.default.createElement(TD.Text,{wrap:"truncate"},eg.default.createElement(lY,{active:E})," ",p),e?eg.default.createElement(yY,{active:t,length:P}):null):eg.default.createElement(TD.Box,{key:`spacer-${h}`,width:C,marginLeft:1})}))}});var rxe=L((Ugr,txe)=>{var kY;txe.exports=()=>(typeof kY>"u"&&(kY=Ie("zlib").brotliDecompressSync(Buffer.from("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","base64")).toString()),kY)});var Bxe=L((fdr,wxe)=>{var _Y=Symbol("arg flag"),Kc=class t extends Error{constructor(e,r){super(e),this.name="ArgError",this.code=r,Object.setPrototypeOf(this,t.prototype)}};function JD(t,{argv:e=process.argv.slice(2),permissive:r=!1,stopAtPositional:s=!1}={}){if(!t)throw new Kc("argument specification object is required","ARG_CONFIG_NO_SPEC");let a={_:[]},n={},c={};for(let f of Object.keys(t)){if(!f)throw new Kc("argument key cannot be an empty string","ARG_CONFIG_EMPTY_KEY");if(f[0]!=="-")throw new Kc(`argument key must start with '-' but found: '${f}'`,"ARG_CONFIG_NONOPT_KEY");if(f.length===1)throw new Kc(`argument key must have a name; singular '-' keys are not allowed: ${f}`,"ARG_CONFIG_NONAME_KEY");if(typeof t[f]=="string"){n[f]=t[f];continue}let p=t[f],h=!1;if(Array.isArray(p)&&p.length===1&&typeof p[0]=="function"){let[E]=p;p=(C,S,P=[])=>(P.push(E(C,S,P[P.length-1])),P),h=E===Boolean||E[_Y]===!0}else if(typeof p=="function")h=p===Boolean||p[_Y]===!0;else throw new Kc(`type missing or not a function or valid array type: ${f}`,"ARG_CONFIG_VAD_TYPE");if(f[1]!=="-"&&f.length>2)throw new Kc(`short argument keys (with a single hyphen) must have only one character: ${f}`,"ARG_CONFIG_SHORTOPT_TOOLONG");c[f]=[p,h]}for(let f=0,p=e.length;f0){a._=a._.concat(e.slice(f));break}if(h==="--"){a._=a._.concat(e.slice(f+1));break}if(h.length>1&&h[0]==="-"){let E=h[1]==="-"||h.length===2?[h]:h.slice(1).split("").map(C=>`-${C}`);for(let C=0;C1&&e[f+1][0]==="-"&&!(e[f+1].match(/^-?\d*(\.(?=\d))?\d*$/)&&(N===Number||typeof BigInt<"u"&&N===BigInt))){let W=P===R?"":` (alias for ${R})`;throw new Kc(`option requires argument: ${P}${W}`,"ARG_MISSING_REQUIRED_LONGARG")}a[R]=N(e[f+1],R,a[R]),++f}else a[R]=N(I,R,a[R])}}else a._.push(h)}return a}JD.flag=t=>(t[_Y]=!0,t);JD.COUNT=JD.flag((t,e,r)=>(r||0)+1);JD.ArgError=Kc;wxe.exports=JD});var Qxe=L((Hdr,kxe)=>{var qY;kxe.exports=()=>(typeof qY>"u"&&(qY=Ie("zlib").brotliDecompressSync(Buffer.from("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","base64")).toString()),qY)});var Oxe=L((JY,zY)=>{(function(t){JY&&typeof JY=="object"&&typeof zY<"u"?zY.exports=t():typeof define=="function"&&define.amd?define([],t):typeof window<"u"?window.isWindows=t():typeof global<"u"?global.isWindows=t():typeof self<"u"?self.isWindows=t():this.isWindows=t()})(function(){"use strict";return function(){return process&&(process.platform==="win32"||/^(msys|cygwin)$/.test(process.env.OSTYPE))}})});var Uxe=L((_mr,_xe)=>{"use strict";ZY.ifExists=mTt;var Dw=Ie("util"),Jc=Ie("path"),Lxe=Oxe(),hTt=/^#!\s*(?:\/usr\/bin\/env)?\s*([^ \t]+)(.*)$/,gTt={createPwshFile:!0,createCmdFile:Lxe(),fs:Ie("fs")},dTt=new Map([[".js","node"],[".cjs","node"],[".mjs","node"],[".cmd","cmd"],[".bat","cmd"],[".ps1","pwsh"],[".sh","sh"]]);function Mxe(t){let e={...gTt,...t},r=e.fs;return e.fs_={chmod:r.chmod?Dw.promisify(r.chmod):async()=>{},mkdir:Dw.promisify(r.mkdir),readFile:Dw.promisify(r.readFile),stat:Dw.promisify(r.stat),unlink:Dw.promisify(r.unlink),writeFile:Dw.promisify(r.writeFile)},e}async function ZY(t,e,r){let s=Mxe(r);await s.fs_.stat(t),await ETt(t,e,s)}function mTt(t,e,r){return ZY(t,e,r).catch(()=>{})}function yTt(t,e){return e.fs_.unlink(t).catch(()=>{})}async function ETt(t,e,r){let s=await vTt(t,r);return await ITt(e,r),CTt(t,e,s,r)}function ITt(t,e){return e.fs_.mkdir(Jc.dirname(t),{recursive:!0})}function CTt(t,e,r,s){let a=Mxe(s),n=[{generator:bTt,extension:""}];return a.createCmdFile&&n.push({generator:DTt,extension:".cmd"}),a.createPwshFile&&n.push({generator:PTt,extension:".ps1"}),Promise.all(n.map(c=>STt(t,e+c.extension,r,c.generator,a)))}function wTt(t,e){return yTt(t,e)}function BTt(t,e){return xTt(t,e)}async function vTt(t,e){let a=(await e.fs_.readFile(t,"utf8")).trim().split(/\r*\n/)[0].match(hTt);if(!a){let n=Jc.extname(t).toLowerCase();return{program:dTt.get(n)||null,additionalArgs:""}}return{program:a[1],additionalArgs:a[2]}}async function STt(t,e,r,s,a){let n=a.preserveSymlinks?"--preserve-symlinks":"",c=[r.additionalArgs,n].filter(f=>f).join(" ");return a=Object.assign({},a,{prog:r.program,args:c}),await wTt(e,a),await a.fs_.writeFile(e,s(t,e,a),"utf8"),BTt(e,a)}function DTt(t,e,r){let a=Jc.relative(Jc.dirname(e),t).split("/").join("\\"),n=Jc.isAbsolute(a)?`"${a}"`:`"%~dp0\\${a}"`,c,f=r.prog,p=r.args||"",h=XY(r.nodePath).win32;f?(c=`"%~dp0\\${f}.exe"`,a=n):(f=n,p="",a="");let E=r.progArgs?`${r.progArgs.join(" ")} `:"",C=h?`@SET NODE_PATH=${h}\r `:"";return c?C+=`@IF EXIST ${c} (\r ${c} ${p} ${a} ${E}%*\r ) ELSE (\r @SETLOCAL\r @SET PATHEXT=%PATHEXT:;.JS;=;%\r ${f} ${p} ${a} ${E}%*\r )\r `:C+=`@${f} ${p} ${a} ${E}%*\r `,C}function bTt(t,e,r){let s=Jc.relative(Jc.dirname(e),t),a=r.prog&&r.prog.split("\\").join("/"),n;s=s.split("\\").join("/");let c=Jc.isAbsolute(s)?`"${s}"`:`"$basedir/${s}"`,f=r.args||"",p=XY(r.nodePath).posix;a?(n=`"$basedir/${r.prog}"`,s=c):(a=c,f="",s="");let h=r.progArgs?`${r.progArgs.join(" ")} `:"",E=`#!/bin/sh basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") case \`uname\` in *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; esac `,C=r.nodePath?`export NODE_PATH="${p}" `:"";return n?E+=`${C}if [ -x ${n} ]; then exec ${n} ${f} ${s} ${h}"$@" else exec ${a} ${f} ${s} ${h}"$@" fi `:E+=`${C}${a} ${f} ${s} ${h}"$@" exit $? `,E}function PTt(t,e,r){let s=Jc.relative(Jc.dirname(e),t),a=r.prog&&r.prog.split("\\").join("/"),n=a&&`"${a}$exe"`,c;s=s.split("\\").join("/");let f=Jc.isAbsolute(s)?`"${s}"`:`"$basedir/${s}"`,p=r.args||"",h=XY(r.nodePath),E=h.win32,C=h.posix;n?(c=`"$basedir/${r.prog}$exe"`,s=f):(n=f,p="",s="");let S=r.progArgs?`${r.progArgs.join(" ")} `:"",P=`#!/usr/bin/env pwsh $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent $exe="" ${r.nodePath?`$env_node_path=$env:NODE_PATH $env:NODE_PATH="${E}" `:""}if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { # Fix case when both the Windows and Linux builds of Node # are installed in the same directory $exe=".exe" }`;return r.nodePath&&(P+=` else { $env:NODE_PATH="${C}" }`),c?P+=` $ret=0 if (Test-Path ${c}) { # Support pipeline input if ($MyInvocation.ExpectingInput) { $input | & ${c} ${p} ${s} ${S}$args } else { & ${c} ${p} ${s} ${S}$args } $ret=$LASTEXITCODE } else { # Support pipeline input if ($MyInvocation.ExpectingInput) { $input | & ${n} ${p} ${s} ${S}$args } else { & ${n} ${p} ${s} ${S}$args } $ret=$LASTEXITCODE } ${r.nodePath?`$env:NODE_PATH=$env_node_path `:""}exit $ret `:P+=` # Support pipeline input if ($MyInvocation.ExpectingInput) { $input | & ${n} ${p} ${s} ${S}$args } else { & ${n} ${p} ${s} ${S}$args } ${r.nodePath?`$env:NODE_PATH=$env_node_path `:""}exit $LASTEXITCODE `,P}function xTt(t,e){return e.fs_.chmod(t,493)}function XY(t){if(!t)return{win32:"",posix:""};let e=typeof t=="string"?t.split(Jc.delimiter):Array.from(t),r={};for(let s=0;s`/mnt/${f.toLowerCase()}`):e[s];r.win32=r.win32?`${r.win32};${a}`:a,r.posix=r.posix?`${r.posix}:${n}`:n,r[s]={win32:a,posix:n}}return r}_xe.exports=ZY});var AV=L((oEr,oke)=>{oke.exports=Ie("stream")});var uke=L((aEr,cke)=>{"use strict";function ake(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);e&&(s=s.filter(function(a){return Object.getOwnPropertyDescriptor(t,a).enumerable})),r.push.apply(r,s)}return r}function sRt(t){for(var e=1;e0?this.tail.next=s:this.head=s,this.tail=s,++this.length}},{key:"unshift",value:function(r){var s={data:r,next:this.head};this.length===0&&(this.tail=s),this.head=s,++this.length}},{key:"shift",value:function(){if(this.length!==0){var r=this.head.data;return this.length===1?this.head=this.tail=null:this.head=this.head.next,--this.length,r}}},{key:"clear",value:function(){this.head=this.tail=null,this.length=0}},{key:"join",value:function(r){if(this.length===0)return"";for(var s=this.head,a=""+s.data;s=s.next;)a+=r+s.data;return a}},{key:"concat",value:function(r){if(this.length===0)return hN.alloc(0);for(var s=hN.allocUnsafe(r>>>0),a=this.head,n=0;a;)ARt(a.data,s,n),n+=a.data.length,a=a.next;return s}},{key:"consume",value:function(r,s){var a;return rc.length?c.length:r;if(f===c.length?n+=c:n+=c.slice(0,r),r-=f,r===0){f===c.length?(++a,s.next?this.head=s.next:this.head=this.tail=null):(this.head=s,s.data=c.slice(f));break}++a}return this.length-=a,n}},{key:"_getBuffer",value:function(r){var s=hN.allocUnsafe(r),a=this.head,n=1;for(a.data.copy(s),r-=a.data.length;a=a.next;){var c=a.data,f=r>c.length?c.length:r;if(c.copy(s,s.length-r,0,f),r-=f,r===0){f===c.length?(++n,a.next?this.head=a.next:this.head=this.tail=null):(this.head=a,a.data=c.slice(f));break}++n}return this.length-=n,s}},{key:fRt,value:function(r,s){return pV(this,sRt({},s,{depth:0,customInspect:!1}))}}]),t}()});var gV=L((lEr,Ake)=>{"use strict";function pRt(t,e){var r=this,s=this._readableState&&this._readableState.destroyed,a=this._writableState&&this._writableState.destroyed;return s||a?(e?e(t):t&&(this._writableState?this._writableState.errorEmitted||(this._writableState.errorEmitted=!0,process.nextTick(hV,this,t)):process.nextTick(hV,this,t)),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(t||null,function(n){!e&&n?r._writableState?r._writableState.errorEmitted?process.nextTick(gN,r):(r._writableState.errorEmitted=!0,process.nextTick(fke,r,n)):process.nextTick(fke,r,n):e?(process.nextTick(gN,r),e(n)):process.nextTick(gN,r)}),this)}function fke(t,e){hV(t,e),gN(t)}function gN(t){t._writableState&&!t._writableState.emitClose||t._readableState&&!t._readableState.emitClose||t.emit("close")}function hRt(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finalCalled=!1,this._writableState.prefinished=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)}function hV(t,e){t.emit("error",e)}function gRt(t,e){var r=t._readableState,s=t._writableState;r&&r.autoDestroy||s&&s.autoDestroy?t.destroy(e):t.emit("error",e)}Ake.exports={destroy:pRt,undestroy:hRt,errorOrDestroy:gRt}});var ag=L((cEr,gke)=>{"use strict";var hke={};function Zc(t,e,r){r||(r=Error);function s(n,c,f){return typeof e=="string"?e:e(n,c,f)}class a extends r{constructor(c,f,p){super(s(c,f,p))}}a.prototype.name=r.name,a.prototype.code=t,hke[t]=a}function pke(t,e){if(Array.isArray(t)){let r=t.length;return t=t.map(s=>String(s)),r>2?`one of ${e} ${t.slice(0,r-1).join(", ")}, or `+t[r-1]:r===2?`one of ${e} ${t[0]} or ${t[1]}`:`of ${e} ${t[0]}`}else return`of ${e} ${String(t)}`}function dRt(t,e,r){return t.substr(!r||r<0?0:+r,e.length)===e}function mRt(t,e,r){return(r===void 0||r>t.length)&&(r=t.length),t.substring(r-e.length,r)===e}function yRt(t,e,r){return typeof r!="number"&&(r=0),r+e.length>t.length?!1:t.indexOf(e,r)!==-1}Zc("ERR_INVALID_OPT_VALUE",function(t,e){return'The value "'+e+'" is invalid for option "'+t+'"'},TypeError);Zc("ERR_INVALID_ARG_TYPE",function(t,e,r){let s;typeof e=="string"&&dRt(e,"not ")?(s="must not be",e=e.replace(/^not /,"")):s="must be";let a;if(mRt(t," argument"))a=`The ${t} ${s} ${pke(e,"type")}`;else{let n=yRt(t,".")?"property":"argument";a=`The "${t}" ${n} ${s} ${pke(e,"type")}`}return a+=`. Received type ${typeof r}`,a},TypeError);Zc("ERR_STREAM_PUSH_AFTER_EOF","stream.push() after EOF");Zc("ERR_METHOD_NOT_IMPLEMENTED",function(t){return"The "+t+" method is not implemented"});Zc("ERR_STREAM_PREMATURE_CLOSE","Premature close");Zc("ERR_STREAM_DESTROYED",function(t){return"Cannot call "+t+" after a stream was destroyed"});Zc("ERR_MULTIPLE_CALLBACK","Callback called multiple times");Zc("ERR_STREAM_CANNOT_PIPE","Cannot pipe, not readable");Zc("ERR_STREAM_WRITE_AFTER_END","write after end");Zc("ERR_STREAM_NULL_VALUES","May not write null values to stream",TypeError);Zc("ERR_UNKNOWN_ENCODING",function(t){return"Unknown encoding: "+t},TypeError);Zc("ERR_STREAM_UNSHIFT_AFTER_END_EVENT","stream.unshift() after end event");gke.exports.codes=hke});var dV=L((uEr,dke)=>{"use strict";var ERt=ag().codes.ERR_INVALID_OPT_VALUE;function IRt(t,e,r){return t.highWaterMark!=null?t.highWaterMark:e?t[r]:null}function CRt(t,e,r,s){var a=IRt(e,s,r);if(a!=null){if(!(isFinite(a)&&Math.floor(a)===a)||a<0){var n=s?r:"highWaterMark";throw new ERt(n,a)}return Math.floor(a)}return t.objectMode?16:16*1024}dke.exports={getHighWaterMark:CRt}});var mke=L((fEr,mV)=>{typeof Object.create=="function"?mV.exports=function(e,r){r&&(e.super_=r,e.prototype=Object.create(r.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}))}:mV.exports=function(e,r){if(r){e.super_=r;var s=function(){};s.prototype=r.prototype,e.prototype=new s,e.prototype.constructor=e}}});var lg=L((AEr,EV)=>{try{if(yV=Ie("util"),typeof yV.inherits!="function")throw"";EV.exports=yV.inherits}catch{EV.exports=mke()}var yV});var Eke=L((pEr,yke)=>{yke.exports=Ie("util").deprecate});var wV=L((hEr,Ske)=>{"use strict";Ske.exports=Ki;function Cke(t){var e=this;this.next=null,this.entry=null,this.finish=function(){KRt(e,t)}}var Qw;Ki.WritableState=ab;var wRt={deprecate:Eke()},wke=AV(),mN=Ie("buffer").Buffer,BRt=global.Uint8Array||function(){};function vRt(t){return mN.from(t)}function SRt(t){return mN.isBuffer(t)||t instanceof BRt}var CV=gV(),DRt=dV(),bRt=DRt.getHighWaterMark,cg=ag().codes,PRt=cg.ERR_INVALID_ARG_TYPE,xRt=cg.ERR_METHOD_NOT_IMPLEMENTED,kRt=cg.ERR_MULTIPLE_CALLBACK,QRt=cg.ERR_STREAM_CANNOT_PIPE,TRt=cg.ERR_STREAM_DESTROYED,RRt=cg.ERR_STREAM_NULL_VALUES,FRt=cg.ERR_STREAM_WRITE_AFTER_END,NRt=cg.ERR_UNKNOWN_ENCODING,Tw=CV.errorOrDestroy;lg()(Ki,wke);function ORt(){}function ab(t,e,r){Qw=Qw||Wm(),t=t||{},typeof r!="boolean"&&(r=e instanceof Qw),this.objectMode=!!t.objectMode,r&&(this.objectMode=this.objectMode||!!t.writableObjectMode),this.highWaterMark=bRt(this,t,"writableHighWaterMark",r),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var s=t.decodeStrings===!1;this.decodeStrings=!s,this.defaultEncoding=t.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(a){qRt(e,a)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.emitClose=t.emitClose!==!1,this.autoDestroy=!!t.autoDestroy,this.bufferedRequestCount=0,this.corkedRequestsFree=new Cke(this)}ab.prototype.getBuffer=function(){for(var e=this.bufferedRequest,r=[];e;)r.push(e),e=e.next;return r};(function(){try{Object.defineProperty(ab.prototype,"buffer",{get:wRt.deprecate(function(){return this.getBuffer()},"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch{}})();var dN;typeof Symbol=="function"&&Symbol.hasInstance&&typeof Function.prototype[Symbol.hasInstance]=="function"?(dN=Function.prototype[Symbol.hasInstance],Object.defineProperty(Ki,Symbol.hasInstance,{value:function(e){return dN.call(this,e)?!0:this!==Ki?!1:e&&e._writableState instanceof ab}})):dN=function(e){return e instanceof this};function Ki(t){Qw=Qw||Wm();var e=this instanceof Qw;if(!e&&!dN.call(Ki,this))return new Ki(t);this._writableState=new ab(t,this,e),this.writable=!0,t&&(typeof t.write=="function"&&(this._write=t.write),typeof t.writev=="function"&&(this._writev=t.writev),typeof t.destroy=="function"&&(this._destroy=t.destroy),typeof t.final=="function"&&(this._final=t.final)),wke.call(this)}Ki.prototype.pipe=function(){Tw(this,new QRt)};function LRt(t,e){var r=new FRt;Tw(t,r),process.nextTick(e,r)}function MRt(t,e,r,s){var a;return r===null?a=new RRt:typeof r!="string"&&!e.objectMode&&(a=new PRt("chunk",["string","Buffer"],r)),a?(Tw(t,a),process.nextTick(s,a),!1):!0}Ki.prototype.write=function(t,e,r){var s=this._writableState,a=!1,n=!s.objectMode&&SRt(t);return n&&!mN.isBuffer(t)&&(t=vRt(t)),typeof e=="function"&&(r=e,e=null),n?e="buffer":e||(e=s.defaultEncoding),typeof r!="function"&&(r=ORt),s.ending?LRt(this,r):(n||MRt(this,s,t,r))&&(s.pendingcb++,a=URt(this,s,n,t,e,r)),a};Ki.prototype.cork=function(){this._writableState.corked++};Ki.prototype.uncork=function(){var t=this._writableState;t.corked&&(t.corked--,!t.writing&&!t.corked&&!t.bufferProcessing&&t.bufferedRequest&&Bke(this,t))};Ki.prototype.setDefaultEncoding=function(e){if(typeof e=="string"&&(e=e.toLowerCase()),!(["hex","utf8","utf-8","ascii","binary","base64","ucs2","ucs-2","utf16le","utf-16le","raw"].indexOf((e+"").toLowerCase())>-1))throw new NRt(e);return this._writableState.defaultEncoding=e,this};Object.defineProperty(Ki.prototype,"writableBuffer",{enumerable:!1,get:function(){return this._writableState&&this._writableState.getBuffer()}});function _Rt(t,e,r){return!t.objectMode&&t.decodeStrings!==!1&&typeof e=="string"&&(e=mN.from(e,r)),e}Object.defineProperty(Ki.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}});function URt(t,e,r,s,a,n){if(!r){var c=_Rt(e,s,a);s!==c&&(r=!0,a="buffer",s=c)}var f=e.objectMode?1:s.length;e.length+=f;var p=e.length{"use strict";var JRt=Object.keys||function(t){var e=[];for(var r in t)e.push(r);return e};bke.exports=yA;var Dke=SV(),vV=wV();lg()(yA,Dke);for(BV=JRt(vV.prototype),yN=0;yN{var IN=Ie("buffer"),ch=IN.Buffer;function Pke(t,e){for(var r in t)e[r]=t[r]}ch.from&&ch.alloc&&ch.allocUnsafe&&ch.allocUnsafeSlow?xke.exports=IN:(Pke(IN,DV),DV.Buffer=Rw);function Rw(t,e,r){return ch(t,e,r)}Pke(ch,Rw);Rw.from=function(t,e,r){if(typeof t=="number")throw new TypeError("Argument must not be a number");return ch(t,e,r)};Rw.alloc=function(t,e,r){if(typeof t!="number")throw new TypeError("Argument must be a number");var s=ch(t);return e!==void 0?typeof r=="string"?s.fill(e,r):s.fill(e):s.fill(0),s};Rw.allocUnsafe=function(t){if(typeof t!="number")throw new TypeError("Argument must be a number");return ch(t)};Rw.allocUnsafeSlow=function(t){if(typeof t!="number")throw new TypeError("Argument must be a number");return IN.SlowBuffer(t)}});var xV=L(Tke=>{"use strict";var PV=kke().Buffer,Qke=PV.isEncoding||function(t){switch(t=""+t,t&&t.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function XRt(t){if(!t)return"utf8";for(var e;;)switch(t){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return t;default:if(e)return;t=(""+t).toLowerCase(),e=!0}}function $Rt(t){var e=XRt(t);if(typeof e!="string"&&(PV.isEncoding===Qke||!Qke(t)))throw new Error("Unknown encoding: "+t);return e||t}Tke.StringDecoder=lb;function lb(t){this.encoding=$Rt(t);var e;switch(this.encoding){case"utf16le":this.text=sFt,this.end=oFt,e=4;break;case"utf8":this.fillLast=rFt,e=4;break;case"base64":this.text=aFt,this.end=lFt,e=3;break;default:this.write=cFt,this.end=uFt;return}this.lastNeed=0,this.lastTotal=0,this.lastChar=PV.allocUnsafe(e)}lb.prototype.write=function(t){if(t.length===0)return"";var e,r;if(this.lastNeed){if(e=this.fillLast(t),e===void 0)return"";r=this.lastNeed,this.lastNeed=0}else r=0;return r>5===6?2:t>>4===14?3:t>>3===30?4:t>>6===2?-1:-2}function eFt(t,e,r){var s=e.length-1;if(s=0?(a>0&&(t.lastNeed=a-1),a):--s=0?(a>0&&(t.lastNeed=a-2),a):--s=0?(a>0&&(a===2?a=0:t.lastNeed=a-3),a):0))}function tFt(t,e,r){if((e[0]&192)!==128)return t.lastNeed=0,"\uFFFD";if(t.lastNeed>1&&e.length>1){if((e[1]&192)!==128)return t.lastNeed=1,"\uFFFD";if(t.lastNeed>2&&e.length>2&&(e[2]&192)!==128)return t.lastNeed=2,"\uFFFD"}}function rFt(t){var e=this.lastTotal-this.lastNeed,r=tFt(this,t,e);if(r!==void 0)return r;if(this.lastNeed<=t.length)return t.copy(this.lastChar,e,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);t.copy(this.lastChar,e,0,t.length),this.lastNeed-=t.length}function nFt(t,e){var r=eFt(this,t,e);if(!this.lastNeed)return t.toString("utf8",e);this.lastTotal=r;var s=t.length-(r-this.lastNeed);return t.copy(this.lastChar,0,s),t.toString("utf8",e,s)}function iFt(t){var e=t&&t.length?this.write(t):"";return this.lastNeed?e+"\uFFFD":e}function sFt(t,e){if((t.length-e)%2===0){var r=t.toString("utf16le",e);if(r){var s=r.charCodeAt(r.length-1);if(s>=55296&&s<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=t[t.length-2],this.lastChar[1]=t[t.length-1],r.slice(0,-1)}return r}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=t[t.length-1],t.toString("utf16le",e,t.length-1)}function oFt(t){var e=t&&t.length?this.write(t):"";if(this.lastNeed){var r=this.lastTotal-this.lastNeed;return e+this.lastChar.toString("utf16le",0,r)}return e}function aFt(t,e){var r=(t.length-e)%3;return r===0?t.toString("base64",e):(this.lastNeed=3-r,this.lastTotal=3,r===1?this.lastChar[0]=t[t.length-1]:(this.lastChar[0]=t[t.length-2],this.lastChar[1]=t[t.length-1]),t.toString("base64",e,t.length-r))}function lFt(t){var e=t&&t.length?this.write(t):"";return this.lastNeed?e+this.lastChar.toString("base64",0,3-this.lastNeed):e}function cFt(t){return t.toString(this.encoding)}function uFt(t){return t&&t.length?this.write(t):""}});var CN=L((mEr,Nke)=>{"use strict";var Rke=ag().codes.ERR_STREAM_PREMATURE_CLOSE;function fFt(t){var e=!1;return function(){if(!e){e=!0;for(var r=arguments.length,s=new Array(r),a=0;a{"use strict";var wN;function ug(t,e,r){return e in t?Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[e]=r,t}var hFt=CN(),fg=Symbol("lastResolve"),Ym=Symbol("lastReject"),ub=Symbol("error"),BN=Symbol("ended"),Vm=Symbol("lastPromise"),kV=Symbol("handlePromise"),Km=Symbol("stream");function Ag(t,e){return{value:t,done:e}}function gFt(t){var e=t[fg];if(e!==null){var r=t[Km].read();r!==null&&(t[Vm]=null,t[fg]=null,t[Ym]=null,e(Ag(r,!1)))}}function dFt(t){process.nextTick(gFt,t)}function mFt(t,e){return function(r,s){t.then(function(){if(e[BN]){r(Ag(void 0,!0));return}e[kV](r,s)},s)}}var yFt=Object.getPrototypeOf(function(){}),EFt=Object.setPrototypeOf((wN={get stream(){return this[Km]},next:function(){var e=this,r=this[ub];if(r!==null)return Promise.reject(r);if(this[BN])return Promise.resolve(Ag(void 0,!0));if(this[Km].destroyed)return new Promise(function(c,f){process.nextTick(function(){e[ub]?f(e[ub]):c(Ag(void 0,!0))})});var s=this[Vm],a;if(s)a=new Promise(mFt(s,this));else{var n=this[Km].read();if(n!==null)return Promise.resolve(Ag(n,!1));a=new Promise(this[kV])}return this[Vm]=a,a}},ug(wN,Symbol.asyncIterator,function(){return this}),ug(wN,"return",function(){var e=this;return new Promise(function(r,s){e[Km].destroy(null,function(a){if(a){s(a);return}r(Ag(void 0,!0))})})}),wN),yFt),IFt=function(e){var r,s=Object.create(EFt,(r={},ug(r,Km,{value:e,writable:!0}),ug(r,fg,{value:null,writable:!0}),ug(r,Ym,{value:null,writable:!0}),ug(r,ub,{value:null,writable:!0}),ug(r,BN,{value:e._readableState.endEmitted,writable:!0}),ug(r,kV,{value:function(n,c){var f=s[Km].read();f?(s[Vm]=null,s[fg]=null,s[Ym]=null,n(Ag(f,!1))):(s[fg]=n,s[Ym]=c)},writable:!0}),r));return s[Vm]=null,hFt(e,function(a){if(a&&a.code!=="ERR_STREAM_PREMATURE_CLOSE"){var n=s[Ym];n!==null&&(s[Vm]=null,s[fg]=null,s[Ym]=null,n(a)),s[ub]=a;return}var c=s[fg];c!==null&&(s[Vm]=null,s[fg]=null,s[Ym]=null,c(Ag(void 0,!0))),s[BN]=!0}),e.on("readable",dFt.bind(null,s)),s};Oke.exports=IFt});var Hke=L((EEr,Uke)=>{"use strict";function Mke(t,e,r,s,a,n,c){try{var f=t[n](c),p=f.value}catch(h){r(h);return}f.done?e(p):Promise.resolve(p).then(s,a)}function CFt(t){return function(){var e=this,r=arguments;return new Promise(function(s,a){var n=t.apply(e,r);function c(p){Mke(n,s,a,c,f,"next",p)}function f(p){Mke(n,s,a,c,f,"throw",p)}c(void 0)})}}function _ke(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);e&&(s=s.filter(function(a){return Object.getOwnPropertyDescriptor(t,a).enumerable})),r.push.apply(r,s)}return r}function wFt(t){for(var e=1;e{"use strict";Zke.exports=Pn;var Fw;Pn.ReadableState=Wke;var IEr=Ie("events").EventEmitter,Gke=function(e,r){return e.listeners(r).length},Ab=AV(),vN=Ie("buffer").Buffer,DFt=global.Uint8Array||function(){};function bFt(t){return vN.from(t)}function PFt(t){return vN.isBuffer(t)||t instanceof DFt}var QV=Ie("util"),ln;QV&&QV.debuglog?ln=QV.debuglog("stream"):ln=function(){};var xFt=uke(),MV=gV(),kFt=dV(),QFt=kFt.getHighWaterMark,SN=ag().codes,TFt=SN.ERR_INVALID_ARG_TYPE,RFt=SN.ERR_STREAM_PUSH_AFTER_EOF,FFt=SN.ERR_METHOD_NOT_IMPLEMENTED,NFt=SN.ERR_STREAM_UNSHIFT_AFTER_END_EVENT,Nw,TV,RV;lg()(Pn,Ab);var fb=MV.errorOrDestroy,FV=["error","close","destroy","pause","resume"];function OFt(t,e,r){if(typeof t.prependListener=="function")return t.prependListener(e,r);!t._events||!t._events[e]?t.on(e,r):Array.isArray(t._events[e])?t._events[e].unshift(r):t._events[e]=[r,t._events[e]]}function Wke(t,e,r){Fw=Fw||Wm(),t=t||{},typeof r!="boolean"&&(r=e instanceof Fw),this.objectMode=!!t.objectMode,r&&(this.objectMode=this.objectMode||!!t.readableObjectMode),this.highWaterMark=QFt(this,t,"readableHighWaterMark",r),this.buffer=new xFt,this.length=0,this.pipes=null,this.pipesCount=0,this.flowing=null,this.ended=!1,this.endEmitted=!1,this.reading=!1,this.sync=!0,this.needReadable=!1,this.emittedReadable=!1,this.readableListening=!1,this.resumeScheduled=!1,this.paused=!0,this.emitClose=t.emitClose!==!1,this.autoDestroy=!!t.autoDestroy,this.destroyed=!1,this.defaultEncoding=t.defaultEncoding||"utf8",this.awaitDrain=0,this.readingMore=!1,this.decoder=null,this.encoding=null,t.encoding&&(Nw||(Nw=xV().StringDecoder),this.decoder=new Nw(t.encoding),this.encoding=t.encoding)}function Pn(t){if(Fw=Fw||Wm(),!(this instanceof Pn))return new Pn(t);var e=this instanceof Fw;this._readableState=new Wke(t,this,e),this.readable=!0,t&&(typeof t.read=="function"&&(this._read=t.read),typeof t.destroy=="function"&&(this._destroy=t.destroy)),Ab.call(this)}Object.defineProperty(Pn.prototype,"destroyed",{enumerable:!1,get:function(){return this._readableState===void 0?!1:this._readableState.destroyed},set:function(e){this._readableState&&(this._readableState.destroyed=e)}});Pn.prototype.destroy=MV.destroy;Pn.prototype._undestroy=MV.undestroy;Pn.prototype._destroy=function(t,e){e(t)};Pn.prototype.push=function(t,e){var r=this._readableState,s;return r.objectMode?s=!0:typeof t=="string"&&(e=e||r.defaultEncoding,e!==r.encoding&&(t=vN.from(t,e),e=""),s=!0),Yke(this,t,e,!1,s)};Pn.prototype.unshift=function(t){return Yke(this,t,null,!0,!1)};function Yke(t,e,r,s,a){ln("readableAddChunk",e);var n=t._readableState;if(e===null)n.reading=!1,_Ft(t,n);else{var c;if(a||(c=LFt(n,e)),c)fb(t,c);else if(n.objectMode||e&&e.length>0)if(typeof e!="string"&&!n.objectMode&&Object.getPrototypeOf(e)!==vN.prototype&&(e=bFt(e)),s)n.endEmitted?fb(t,new NFt):NV(t,n,e,!0);else if(n.ended)fb(t,new RFt);else{if(n.destroyed)return!1;n.reading=!1,n.decoder&&!r?(e=n.decoder.write(e),n.objectMode||e.length!==0?NV(t,n,e,!1):LV(t,n)):NV(t,n,e,!1)}else s||(n.reading=!1,LV(t,n))}return!n.ended&&(n.length=jke?t=jke:(t--,t|=t>>>1,t|=t>>>2,t|=t>>>4,t|=t>>>8,t|=t>>>16,t++),t}function qke(t,e){return t<=0||e.length===0&&e.ended?0:e.objectMode?1:t!==t?e.flowing&&e.length?e.buffer.head.data.length:e.length:(t>e.highWaterMark&&(e.highWaterMark=MFt(t)),t<=e.length?t:e.ended?e.length:(e.needReadable=!0,0))}Pn.prototype.read=function(t){ln("read",t),t=parseInt(t,10);var e=this._readableState,r=t;if(t!==0&&(e.emittedReadable=!1),t===0&&e.needReadable&&((e.highWaterMark!==0?e.length>=e.highWaterMark:e.length>0)||e.ended))return ln("read: emitReadable",e.length,e.ended),e.length===0&&e.ended?OV(this):DN(this),null;if(t=qke(t,e),t===0&&e.ended)return e.length===0&&OV(this),null;var s=e.needReadable;ln("need readable",s),(e.length===0||e.length-t0?a=Jke(t,e):a=null,a===null?(e.needReadable=e.length<=e.highWaterMark,t=0):(e.length-=t,e.awaitDrain=0),e.length===0&&(e.ended||(e.needReadable=!0),r!==t&&e.ended&&OV(this)),a!==null&&this.emit("data",a),a};function _Ft(t,e){if(ln("onEofChunk"),!e.ended){if(e.decoder){var r=e.decoder.end();r&&r.length&&(e.buffer.push(r),e.length+=e.objectMode?1:r.length)}e.ended=!0,e.sync?DN(t):(e.needReadable=!1,e.emittedReadable||(e.emittedReadable=!0,Vke(t)))}}function DN(t){var e=t._readableState;ln("emitReadable",e.needReadable,e.emittedReadable),e.needReadable=!1,e.emittedReadable||(ln("emitReadable",e.flowing),e.emittedReadable=!0,process.nextTick(Vke,t))}function Vke(t){var e=t._readableState;ln("emitReadable_",e.destroyed,e.length,e.ended),!e.destroyed&&(e.length||e.ended)&&(t.emit("readable"),e.emittedReadable=!1),e.needReadable=!e.flowing&&!e.ended&&e.length<=e.highWaterMark,_V(t)}function LV(t,e){e.readingMore||(e.readingMore=!0,process.nextTick(UFt,t,e))}function UFt(t,e){for(;!e.reading&&!e.ended&&(e.length1&&zke(s.pipes,t)!==-1)&&!h&&(ln("false write response, pause",s.awaitDrain),s.awaitDrain++),r.pause())}function S(N){ln("onerror",N),R(),t.removeListener("error",S),Gke(t,"error")===0&&fb(t,N)}OFt(t,"error",S);function P(){t.removeListener("finish",I),R()}t.once("close",P);function I(){ln("onfinish"),t.removeListener("close",P),R()}t.once("finish",I);function R(){ln("unpipe"),r.unpipe(t)}return t.emit("pipe",r),s.flowing||(ln("pipe resume"),r.resume()),t};function HFt(t){return function(){var r=t._readableState;ln("pipeOnDrain",r.awaitDrain),r.awaitDrain&&r.awaitDrain--,r.awaitDrain===0&&Gke(t,"data")&&(r.flowing=!0,_V(t))}}Pn.prototype.unpipe=function(t){var e=this._readableState,r={hasUnpiped:!1};if(e.pipesCount===0)return this;if(e.pipesCount===1)return t&&t!==e.pipes?this:(t||(t=e.pipes),e.pipes=null,e.pipesCount=0,e.flowing=!1,t&&t.emit("unpipe",this,r),this);if(!t){var s=e.pipes,a=e.pipesCount;e.pipes=null,e.pipesCount=0,e.flowing=!1;for(var n=0;n0,s.flowing!==!1&&this.resume()):t==="readable"&&!s.endEmitted&&!s.readableListening&&(s.readableListening=s.needReadable=!0,s.flowing=!1,s.emittedReadable=!1,ln("on readable",s.length,s.reading),s.length?DN(this):s.reading||process.nextTick(jFt,this)),r};Pn.prototype.addListener=Pn.prototype.on;Pn.prototype.removeListener=function(t,e){var r=Ab.prototype.removeListener.call(this,t,e);return t==="readable"&&process.nextTick(Kke,this),r};Pn.prototype.removeAllListeners=function(t){var e=Ab.prototype.removeAllListeners.apply(this,arguments);return(t==="readable"||t===void 0)&&process.nextTick(Kke,this),e};function Kke(t){var e=t._readableState;e.readableListening=t.listenerCount("readable")>0,e.resumeScheduled&&!e.paused?e.flowing=!0:t.listenerCount("data")>0&&t.resume()}function jFt(t){ln("readable nexttick read 0"),t.read(0)}Pn.prototype.resume=function(){var t=this._readableState;return t.flowing||(ln("resume"),t.flowing=!t.readableListening,qFt(this,t)),t.paused=!1,this};function qFt(t,e){e.resumeScheduled||(e.resumeScheduled=!0,process.nextTick(GFt,t,e))}function GFt(t,e){ln("resume",e.reading),e.reading||t.read(0),e.resumeScheduled=!1,t.emit("resume"),_V(t),e.flowing&&!e.reading&&t.read(0)}Pn.prototype.pause=function(){return ln("call pause flowing=%j",this._readableState.flowing),this._readableState.flowing!==!1&&(ln("pause"),this._readableState.flowing=!1,this.emit("pause")),this._readableState.paused=!0,this};function _V(t){var e=t._readableState;for(ln("flow",e.flowing);e.flowing&&t.read()!==null;);}Pn.prototype.wrap=function(t){var e=this,r=this._readableState,s=!1;t.on("end",function(){if(ln("wrapped end"),r.decoder&&!r.ended){var c=r.decoder.end();c&&c.length&&e.push(c)}e.push(null)}),t.on("data",function(c){if(ln("wrapped data"),r.decoder&&(c=r.decoder.write(c)),!(r.objectMode&&c==null)&&!(!r.objectMode&&(!c||!c.length))){var f=e.push(c);f||(s=!0,t.pause())}});for(var a in t)this[a]===void 0&&typeof t[a]=="function"&&(this[a]=function(f){return function(){return t[f].apply(t,arguments)}}(a));for(var n=0;n=e.length?(e.decoder?r=e.buffer.join(""):e.buffer.length===1?r=e.buffer.first():r=e.buffer.concat(e.length),e.buffer.clear()):r=e.buffer.consume(t,e.decoder),r}function OV(t){var e=t._readableState;ln("endReadable",e.endEmitted),e.endEmitted||(e.ended=!0,process.nextTick(WFt,e,t))}function WFt(t,e){if(ln("endReadableNT",t.endEmitted,t.length),!t.endEmitted&&t.length===0&&(t.endEmitted=!0,e.readable=!1,e.emit("end"),t.autoDestroy)){var r=e._writableState;(!r||r.autoDestroy&&r.finished)&&e.destroy()}}typeof Symbol=="function"&&(Pn.from=function(t,e){return RV===void 0&&(RV=Hke()),RV(Pn,t,e)});function zke(t,e){for(var r=0,s=t.length;r{"use strict";$ke.exports=uh;var bN=ag().codes,YFt=bN.ERR_METHOD_NOT_IMPLEMENTED,VFt=bN.ERR_MULTIPLE_CALLBACK,KFt=bN.ERR_TRANSFORM_ALREADY_TRANSFORMING,JFt=bN.ERR_TRANSFORM_WITH_LENGTH_0,PN=Wm();lg()(uh,PN);function zFt(t,e){var r=this._transformState;r.transforming=!1;var s=r.writecb;if(s===null)return this.emit("error",new VFt);r.writechunk=null,r.writecb=null,e!=null&&this.push(e),s(t);var a=this._readableState;a.reading=!1,(a.needReadable||a.length{"use strict";tQe.exports=pb;var eQe=UV();lg()(pb,eQe);function pb(t){if(!(this instanceof pb))return new pb(t);eQe.call(this,t)}pb.prototype._transform=function(t,e,r){r(null,t)}});var aQe=L((vEr,oQe)=>{"use strict";var HV;function XFt(t){var e=!1;return function(){e||(e=!0,t.apply(void 0,arguments))}}var sQe=ag().codes,$Ft=sQe.ERR_MISSING_ARGS,eNt=sQe.ERR_STREAM_DESTROYED;function nQe(t){if(t)throw t}function tNt(t){return t.setHeader&&typeof t.abort=="function"}function rNt(t,e,r,s){s=XFt(s);var a=!1;t.on("close",function(){a=!0}),HV===void 0&&(HV=CN()),HV(t,{readable:e,writable:r},function(c){if(c)return s(c);a=!0,s()});var n=!1;return function(c){if(!a&&!n){if(n=!0,tNt(t))return t.abort();if(typeof t.destroy=="function")return t.destroy();s(c||new eNt("pipe"))}}}function iQe(t){t()}function nNt(t,e){return t.pipe(e)}function iNt(t){return!t.length||typeof t[t.length-1]!="function"?nQe:t.pop()}function sNt(){for(var t=arguments.length,e=new Array(t),r=0;r0;return rNt(c,p,h,function(E){a||(a=E),E&&n.forEach(iQe),!p&&(n.forEach(iQe),s(a))})});return e.reduce(nNt)}oQe.exports=sNt});var Ow=L((Xc,gb)=>{var hb=Ie("stream");process.env.READABLE_STREAM==="disable"&&hb?(gb.exports=hb.Readable,Object.assign(gb.exports,hb),gb.exports.Stream=hb):(Xc=gb.exports=SV(),Xc.Stream=hb||Xc,Xc.Readable=Xc,Xc.Writable=wV(),Xc.Duplex=Wm(),Xc.Transform=UV(),Xc.PassThrough=rQe(),Xc.finished=CN(),Xc.pipeline=aQe())});var uQe=L((SEr,cQe)=>{"use strict";var{Buffer:uf}=Ie("buffer"),lQe=Symbol.for("BufferList");function wi(t){if(!(this instanceof wi))return new wi(t);wi._init.call(this,t)}wi._init=function(e){Object.defineProperty(this,lQe,{value:!0}),this._bufs=[],this.length=0,e&&this.append(e)};wi.prototype._new=function(e){return new wi(e)};wi.prototype._offset=function(e){if(e===0)return[0,0];let r=0;for(let s=0;sthis.length||e<0)return;let r=this._offset(e);return this._bufs[r[0]][r[1]]};wi.prototype.slice=function(e,r){return typeof e=="number"&&e<0&&(e+=this.length),typeof r=="number"&&r<0&&(r+=this.length),this.copy(null,0,e,r)};wi.prototype.copy=function(e,r,s,a){if((typeof s!="number"||s<0)&&(s=0),(typeof a!="number"||a>this.length)&&(a=this.length),s>=this.length||a<=0)return e||uf.alloc(0);let n=!!e,c=this._offset(s),f=a-s,p=f,h=n&&r||0,E=c[1];if(s===0&&a===this.length){if(!n)return this._bufs.length===1?this._bufs[0]:uf.concat(this._bufs,this.length);for(let C=0;CS)this._bufs[C].copy(e,h,E),h+=S;else{this._bufs[C].copy(e,h,E,E+p),h+=S;break}p-=S,E&&(E=0)}return e.length>h?e.slice(0,h):e};wi.prototype.shallowSlice=function(e,r){if(e=e||0,r=typeof r!="number"?this.length:r,e<0&&(e+=this.length),r<0&&(r+=this.length),e===r)return this._new();let s=this._offset(e),a=this._offset(r),n=this._bufs.slice(s[0],a[0]+1);return a[1]===0?n.pop():n[n.length-1]=n[n.length-1].slice(0,a[1]),s[1]!==0&&(n[0]=n[0].slice(s[1])),this._new(n)};wi.prototype.toString=function(e,r,s){return this.slice(r,s).toString(e)};wi.prototype.consume=function(e){if(e=Math.trunc(e),Number.isNaN(e)||e<=0)return this;for(;this._bufs.length;)if(e>=this._bufs[0].length)e-=this._bufs[0].length,this.length-=this._bufs[0].length,this._bufs.shift();else{this._bufs[0]=this._bufs[0].slice(e),this.length-=e;break}return this};wi.prototype.duplicate=function(){let e=this._new();for(let r=0;rthis.length?this.length:e;let s=this._offset(e),a=s[0],n=s[1];for(;a=t.length){let p=c.indexOf(t,n);if(p!==-1)return this._reverseOffset([a,p]);n=c.length-t.length+1}else{let p=this._reverseOffset([a,n]);if(this._match(p,t))return p;n++}n=0}return-1};wi.prototype._match=function(t,e){if(this.length-t{"use strict";var jV=Ow().Duplex,oNt=lg(),db=uQe();function na(t){if(!(this instanceof na))return new na(t);if(typeof t=="function"){this._callback=t;let e=function(s){this._callback&&(this._callback(s),this._callback=null)}.bind(this);this.on("pipe",function(s){s.on("error",e)}),this.on("unpipe",function(s){s.removeListener("error",e)}),t=null}db._init.call(this,t),jV.call(this)}oNt(na,jV);Object.assign(na.prototype,db.prototype);na.prototype._new=function(e){return new na(e)};na.prototype._write=function(e,r,s){this._appendBuffer(e),typeof s=="function"&&s()};na.prototype._read=function(e){if(!this.length)return this.push(null);e=Math.min(e,this.length),this.push(this.slice(0,e)),this.consume(e)};na.prototype.end=function(e){jV.prototype.end.call(this,e),this._callback&&(this._callback(null,this.slice()),this._callback=null)};na.prototype._destroy=function(e,r){this._bufs.length=0,this.length=0,r(e)};na.prototype._isBufferList=function(e){return e instanceof na||e instanceof db||na.isBufferList(e)};na.isBufferList=db.isBufferList;xN.exports=na;xN.exports.BufferListStream=na;xN.exports.BufferList=db});var WV=L(Mw=>{var aNt=Buffer.alloc,lNt="0000000000000000000",cNt="7777777777777777777",AQe=48,pQe=Buffer.from("ustar\0","binary"),uNt=Buffer.from("00","binary"),fNt=Buffer.from("ustar ","binary"),ANt=Buffer.from(" \0","binary"),pNt=parseInt("7777",8),mb=257,GV=263,hNt=function(t,e,r){return typeof t!="number"?r:(t=~~t,t>=e?e:t>=0||(t+=e,t>=0)?t:0)},gNt=function(t){switch(t){case 0:return"file";case 1:return"link";case 2:return"symlink";case 3:return"character-device";case 4:return"block-device";case 5:return"directory";case 6:return"fifo";case 7:return"contiguous-file";case 72:return"pax-header";case 55:return"pax-global-header";case 27:return"gnu-long-link-path";case 28:case 30:return"gnu-long-path"}return null},dNt=function(t){switch(t){case"file":return 0;case"link":return 1;case"symlink":return 2;case"character-device":return 3;case"block-device":return 4;case"directory":return 5;case"fifo":return 6;case"contiguous-file":return 7;case"pax-header":return 72}return 0},hQe=function(t,e,r,s){for(;re?cNt.slice(0,e)+" ":lNt.slice(0,e-t.length)+t+" "};function mNt(t){var e;if(t[0]===128)e=!0;else if(t[0]===255)e=!1;else return null;for(var r=[],s=t.length-1;s>0;s--){var a=t[s];e?r.push(a):r.push(255-a)}var n=0,c=r.length;for(s=0;s=Math.pow(10,r)&&r++,e+r+t};Mw.decodeLongPath=function(t,e){return Lw(t,0,t.length,e)};Mw.encodePax=function(t){var e="";t.name&&(e+=qV(" path="+t.name+` `)),t.linkname&&(e+=qV(" linkpath="+t.linkname+` `));var r=t.pax;if(r)for(var s in r)e+=qV(" "+s+"="+r[s]+` `);return Buffer.from(e)};Mw.decodePax=function(t){for(var e={};t.length;){for(var r=0;r100;){var a=r.indexOf("/");if(a===-1)return null;s+=s?"/"+r.slice(0,a):r.slice(0,a),r=r.slice(a+1)}return Buffer.byteLength(r)>100||Buffer.byteLength(s)>155||t.linkname&&Buffer.byteLength(t.linkname)>100?null:(e.write(r),e.write(pg(t.mode&pNt,6),100),e.write(pg(t.uid,6),108),e.write(pg(t.gid,6),116),e.write(pg(t.size,11),124),e.write(pg(t.mtime.getTime()/1e3|0,11),136),e[156]=AQe+dNt(t.type),t.linkname&&e.write(t.linkname,157),pQe.copy(e,mb),uNt.copy(e,GV),t.uname&&e.write(t.uname,265),t.gname&&e.write(t.gname,297),e.write(pg(t.devmajor||0,6),329),e.write(pg(t.devminor||0,6),337),s&&e.write(s,345),e.write(pg(gQe(e),6),148),e)};Mw.decode=function(t,e,r){var s=t[156]===0?0:t[156]-AQe,a=Lw(t,0,100,e),n=hg(t,100,8),c=hg(t,108,8),f=hg(t,116,8),p=hg(t,124,12),h=hg(t,136,12),E=gNt(s),C=t[157]===0?null:Lw(t,157,100,e),S=Lw(t,265,32),P=Lw(t,297,32),I=hg(t,329,8),R=hg(t,337,8),N=gQe(t);if(N===8*32)return null;if(N!==hg(t,148,8))throw new Error("Invalid tar header. Maybe the tar is corrupted or it needs to be gunzipped?");if(pQe.compare(t,mb,mb+6)===0)t[345]&&(a=Lw(t,345,155,e)+"/"+a);else if(!(fNt.compare(t,mb,mb+6)===0&&ANt.compare(t,GV,GV+2)===0)){if(!r)throw new Error("Invalid tar header: unknown format.")}return s===0&&a&&a[a.length-1]==="/"&&(s=5),{name:a,mode:n,uid:c,gid:f,size:p,mtime:new Date(1e3*h),type:E,linkname:C,uname:S,gname:P,devmajor:I,devminor:R}}});var wQe=L((PEr,CQe)=>{var mQe=Ie("util"),yNt=fQe(),yb=WV(),yQe=Ow().Writable,EQe=Ow().PassThrough,IQe=function(){},dQe=function(t){return t&=511,t&&512-t},ENt=function(t,e){var r=new kN(t,e);return r.end(),r},INt=function(t,e){return e.path&&(t.name=e.path),e.linkpath&&(t.linkname=e.linkpath),e.size&&(t.size=parseInt(e.size,10)),t.pax=e,t},kN=function(t,e){this._parent=t,this.offset=e,EQe.call(this,{autoDestroy:!1})};mQe.inherits(kN,EQe);kN.prototype.destroy=function(t){this._parent.destroy(t)};var fh=function(t){if(!(this instanceof fh))return new fh(t);yQe.call(this,t),t=t||{},this._offset=0,this._buffer=yNt(),this._missing=0,this._partial=!1,this._onparse=IQe,this._header=null,this._stream=null,this._overflow=null,this._cb=null,this._locked=!1,this._destroyed=!1,this._pax=null,this._paxGlobal=null,this._gnuLongPath=null,this._gnuLongLinkPath=null;var e=this,r=e._buffer,s=function(){e._continue()},a=function(S){if(e._locked=!1,S)return e.destroy(S);e._stream||s()},n=function(){e._stream=null;var S=dQe(e._header.size);S?e._parse(S,c):e._parse(512,C),e._locked||s()},c=function(){e._buffer.consume(dQe(e._header.size)),e._parse(512,C),s()},f=function(){var S=e._header.size;e._paxGlobal=yb.decodePax(r.slice(0,S)),r.consume(S),n()},p=function(){var S=e._header.size;e._pax=yb.decodePax(r.slice(0,S)),e._paxGlobal&&(e._pax=Object.assign({},e._paxGlobal,e._pax)),r.consume(S),n()},h=function(){var S=e._header.size;this._gnuLongPath=yb.decodeLongPath(r.slice(0,S),t.filenameEncoding),r.consume(S),n()},E=function(){var S=e._header.size;this._gnuLongLinkPath=yb.decodeLongPath(r.slice(0,S),t.filenameEncoding),r.consume(S),n()},C=function(){var S=e._offset,P;try{P=e._header=yb.decode(r.slice(0,512),t.filenameEncoding,t.allowUnknownFormat)}catch(I){e.emit("error",I)}if(r.consume(512),!P){e._parse(512,C),s();return}if(P.type==="gnu-long-path"){e._parse(P.size,h),s();return}if(P.type==="gnu-long-link-path"){e._parse(P.size,E),s();return}if(P.type==="pax-global-header"){e._parse(P.size,f),s();return}if(P.type==="pax-header"){e._parse(P.size,p),s();return}if(e._gnuLongPath&&(P.name=e._gnuLongPath,e._gnuLongPath=null),e._gnuLongLinkPath&&(P.linkname=e._gnuLongLinkPath,e._gnuLongLinkPath=null),e._pax&&(e._header=P=INt(P,e._pax),e._pax=null),e._locked=!0,!P.size||P.type==="directory"){e._parse(512,C),e.emit("entry",P,ENt(e,S),a);return}e._stream=new kN(e,S),e.emit("entry",P,e._stream,a),e._parse(P.size,n),s()};this._onheader=C,this._parse(512,C)};mQe.inherits(fh,yQe);fh.prototype.destroy=function(t){this._destroyed||(this._destroyed=!0,t&&this.emit("error",t),this.emit("close"),this._stream&&this._stream.emit("close"))};fh.prototype._parse=function(t,e){this._destroyed||(this._offset+=t,this._missing=t,e===this._onheader&&(this._partial=!1),this._onparse=e)};fh.prototype._continue=function(){if(!this._destroyed){var t=this._cb;this._cb=IQe,this._overflow?this._write(this._overflow,void 0,t):t()}};fh.prototype._write=function(t,e,r){if(!this._destroyed){var s=this._stream,a=this._buffer,n=this._missing;if(t.length&&(this._partial=!0),t.lengthn&&(c=t.slice(n),t=t.slice(0,n)),s?s.end(t):a.append(t),this._overflow=c,this._onparse()}};fh.prototype._final=function(t){if(this._partial)return this.destroy(new Error("Unexpected end of data"));t()};CQe.exports=fh});var vQe=L((xEr,BQe)=>{BQe.exports=Ie("fs").constants||Ie("constants")});var xQe=L((kEr,PQe)=>{var _w=vQe(),SQe=vH(),TN=lg(),CNt=Buffer.alloc,DQe=Ow().Readable,Uw=Ow().Writable,wNt=Ie("string_decoder").StringDecoder,QN=WV(),BNt=parseInt("755",8),vNt=parseInt("644",8),bQe=CNt(1024),VV=function(){},YV=function(t,e){e&=511,e&&t.push(bQe.slice(0,512-e))};function SNt(t){switch(t&_w.S_IFMT){case _w.S_IFBLK:return"block-device";case _w.S_IFCHR:return"character-device";case _w.S_IFDIR:return"directory";case _w.S_IFIFO:return"fifo";case _w.S_IFLNK:return"symlink"}return"file"}var RN=function(t){Uw.call(this),this.written=0,this._to=t,this._destroyed=!1};TN(RN,Uw);RN.prototype._write=function(t,e,r){if(this.written+=t.length,this._to.push(t))return r();this._to._drain=r};RN.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit("close"))};var FN=function(){Uw.call(this),this.linkname="",this._decoder=new wNt("utf-8"),this._destroyed=!1};TN(FN,Uw);FN.prototype._write=function(t,e,r){this.linkname+=this._decoder.write(t),r()};FN.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit("close"))};var Eb=function(){Uw.call(this),this._destroyed=!1};TN(Eb,Uw);Eb.prototype._write=function(t,e,r){r(new Error("No body allowed for this entry"))};Eb.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit("close"))};var EA=function(t){if(!(this instanceof EA))return new EA(t);DQe.call(this,t),this._drain=VV,this._finalized=!1,this._finalizing=!1,this._destroyed=!1,this._stream=null};TN(EA,DQe);EA.prototype.entry=function(t,e,r){if(this._stream)throw new Error("already piping an entry");if(!(this._finalized||this._destroyed)){typeof e=="function"&&(r=e,e=null),r||(r=VV);var s=this;if((!t.size||t.type==="symlink")&&(t.size=0),t.type||(t.type=SNt(t.mode)),t.mode||(t.mode=t.type==="directory"?BNt:vNt),t.uid||(t.uid=0),t.gid||(t.gid=0),t.mtime||(t.mtime=new Date),typeof e=="string"&&(e=Buffer.from(e)),Buffer.isBuffer(e)){t.size=e.length,this._encode(t);var a=this.push(e);return YV(s,t.size),a?process.nextTick(r):this._drain=r,new Eb}if(t.type==="symlink"&&!t.linkname){var n=new FN;return SQe(n,function(f){if(f)return s.destroy(),r(f);t.linkname=n.linkname,s._encode(t),r()}),n}if(this._encode(t),t.type!=="file"&&t.type!=="contiguous-file")return process.nextTick(r),new Eb;var c=new RN(this);return this._stream=c,SQe(c,function(f){if(s._stream=null,f)return s.destroy(),r(f);if(c.written!==t.size)return s.destroy(),r(new Error("size mismatch"));YV(s,t.size),s._finalizing&&s.finalize(),r()}),c}};EA.prototype.finalize=function(){if(this._stream){this._finalizing=!0;return}this._finalized||(this._finalized=!0,this.push(bQe),this.push(null))};EA.prototype.destroy=function(t){this._destroyed||(this._destroyed=!0,t&&this.emit("error",t),this.emit("close"),this._stream&&this._stream.destroy&&this._stream.destroy())};EA.prototype._encode=function(t){if(!t.pax){var e=QN.encode(t);if(e){this.push(e);return}}this._encodePax(t)};EA.prototype._encodePax=function(t){var e=QN.encodePax({name:t.name,linkname:t.linkname,pax:t.pax}),r={name:"PaxHeader",mode:t.mode,uid:t.uid,gid:t.gid,size:e.length,mtime:t.mtime,type:"pax-header",linkname:t.linkname&&"PaxHeader",uname:t.uname,gname:t.gname,devmajor:t.devmajor,devminor:t.devminor};this.push(QN.encode(r)),this.push(e),YV(this,e.length),r.size=t.size,r.type=t.type,this.push(QN.encode(r))};EA.prototype._read=function(t){var e=this._drain;this._drain=VV,e()};PQe.exports=EA});var kQe=L(KV=>{KV.extract=wQe();KV.pack=xQe()});var qQe=L(Ra=>{"use strict";var MNt=Ra&&Ra.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(Ra,"__esModule",{value:!0});Ra.Minipass=Ra.isWritable=Ra.isReadable=Ra.isStream=void 0;var MQe=typeof process=="object"&&process?process:{stdout:null,stderr:null},o7=Ie("node:events"),jQe=MNt(Ie("node:stream")),_Nt=Ie("node:string_decoder"),UNt=t=>!!t&&typeof t=="object"&&(t instanceof qN||t instanceof jQe.default||(0,Ra.isReadable)(t)||(0,Ra.isWritable)(t));Ra.isStream=UNt;var HNt=t=>!!t&&typeof t=="object"&&t instanceof o7.EventEmitter&&typeof t.pipe=="function"&&t.pipe!==jQe.default.Writable.prototype.pipe;Ra.isReadable=HNt;var jNt=t=>!!t&&typeof t=="object"&&t instanceof o7.EventEmitter&&typeof t.write=="function"&&typeof t.end=="function";Ra.isWritable=jNt;var Ah=Symbol("EOF"),ph=Symbol("maybeEmitEnd"),gg=Symbol("emittedEnd"),LN=Symbol("emittingEnd"),Ib=Symbol("emittedError"),MN=Symbol("closed"),_Qe=Symbol("read"),_N=Symbol("flush"),UQe=Symbol("flushChunk"),ff=Symbol("encoding"),jw=Symbol("decoder"),Zs=Symbol("flowing"),Cb=Symbol("paused"),qw=Symbol("resume"),Xs=Symbol("buffer"),Ta=Symbol("pipes"),$s=Symbol("bufferLength"),e7=Symbol("bufferPush"),UN=Symbol("bufferShift"),ia=Symbol("objectMode"),rs=Symbol("destroyed"),t7=Symbol("error"),r7=Symbol("emitData"),HQe=Symbol("emitEnd"),n7=Symbol("emitEnd2"),CA=Symbol("async"),i7=Symbol("abort"),HN=Symbol("aborted"),wb=Symbol("signal"),Jm=Symbol("dataListeners"),nc=Symbol("discarded"),Bb=t=>Promise.resolve().then(t),qNt=t=>t(),GNt=t=>t==="end"||t==="finish"||t==="prefinish",WNt=t=>t instanceof ArrayBuffer||!!t&&typeof t=="object"&&t.constructor&&t.constructor.name==="ArrayBuffer"&&t.byteLength>=0,YNt=t=>!Buffer.isBuffer(t)&&ArrayBuffer.isView(t),jN=class{src;dest;opts;ondrain;constructor(e,r,s){this.src=e,this.dest=r,this.opts=s,this.ondrain=()=>e[qw](),this.dest.on("drain",this.ondrain)}unpipe(){this.dest.removeListener("drain",this.ondrain)}proxyErrors(e){}end(){this.unpipe(),this.opts.end&&this.dest.end()}},s7=class extends jN{unpipe(){this.src.removeListener("error",this.proxyErrors),super.unpipe()}constructor(e,r,s){super(e,r,s),this.proxyErrors=a=>r.emit("error",a),e.on("error",this.proxyErrors)}},VNt=t=>!!t.objectMode,KNt=t=>!t.objectMode&&!!t.encoding&&t.encoding!=="buffer",qN=class extends o7.EventEmitter{[Zs]=!1;[Cb]=!1;[Ta]=[];[Xs]=[];[ia];[ff];[CA];[jw];[Ah]=!1;[gg]=!1;[LN]=!1;[MN]=!1;[Ib]=null;[$s]=0;[rs]=!1;[wb];[HN]=!1;[Jm]=0;[nc]=!1;writable=!0;readable=!0;constructor(...e){let r=e[0]||{};if(super(),r.objectMode&&typeof r.encoding=="string")throw new TypeError("Encoding and objectMode may not be used together");VNt(r)?(this[ia]=!0,this[ff]=null):KNt(r)?(this[ff]=r.encoding,this[ia]=!1):(this[ia]=!1,this[ff]=null),this[CA]=!!r.async,this[jw]=this[ff]?new _Nt.StringDecoder(this[ff]):null,r&&r.debugExposeBuffer===!0&&Object.defineProperty(this,"buffer",{get:()=>this[Xs]}),r&&r.debugExposePipes===!0&&Object.defineProperty(this,"pipes",{get:()=>this[Ta]});let{signal:s}=r;s&&(this[wb]=s,s.aborted?this[i7]():s.addEventListener("abort",()=>this[i7]()))}get bufferLength(){return this[$s]}get encoding(){return this[ff]}set encoding(e){throw new Error("Encoding must be set at instantiation time")}setEncoding(e){throw new Error("Encoding must be set at instantiation time")}get objectMode(){return this[ia]}set objectMode(e){throw new Error("objectMode must be set at instantiation time")}get async(){return this[CA]}set async(e){this[CA]=this[CA]||!!e}[i7](){this[HN]=!0,this.emit("abort",this[wb]?.reason),this.destroy(this[wb]?.reason)}get aborted(){return this[HN]}set aborted(e){}write(e,r,s){if(this[HN])return!1;if(this[Ah])throw new Error("write after end");if(this[rs])return this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0;typeof r=="function"&&(s=r,r="utf8"),r||(r="utf8");let a=this[CA]?Bb:qNt;if(!this[ia]&&!Buffer.isBuffer(e)){if(YNt(e))e=Buffer.from(e.buffer,e.byteOffset,e.byteLength);else if(WNt(e))e=Buffer.from(e);else if(typeof e!="string")throw new Error("Non-contiguous data written to non-objectMode stream")}return this[ia]?(this[Zs]&&this[$s]!==0&&this[_N](!0),this[Zs]?this.emit("data",e):this[e7](e),this[$s]!==0&&this.emit("readable"),s&&a(s),this[Zs]):e.length?(typeof e=="string"&&!(r===this[ff]&&!this[jw]?.lastNeed)&&(e=Buffer.from(e,r)),Buffer.isBuffer(e)&&this[ff]&&(e=this[jw].write(e)),this[Zs]&&this[$s]!==0&&this[_N](!0),this[Zs]?this.emit("data",e):this[e7](e),this[$s]!==0&&this.emit("readable"),s&&a(s),this[Zs]):(this[$s]!==0&&this.emit("readable"),s&&a(s),this[Zs])}read(e){if(this[rs])return null;if(this[nc]=!1,this[$s]===0||e===0||e&&e>this[$s])return this[ph](),null;this[ia]&&(e=null),this[Xs].length>1&&!this[ia]&&(this[Xs]=[this[ff]?this[Xs].join(""):Buffer.concat(this[Xs],this[$s])]);let r=this[_Qe](e||null,this[Xs][0]);return this[ph](),r}[_Qe](e,r){if(this[ia])this[UN]();else{let s=r;e===s.length||e===null?this[UN]():typeof s=="string"?(this[Xs][0]=s.slice(e),r=s.slice(0,e),this[$s]-=e):(this[Xs][0]=s.subarray(e),r=s.subarray(0,e),this[$s]-=e)}return this.emit("data",r),!this[Xs].length&&!this[Ah]&&this.emit("drain"),r}end(e,r,s){return typeof e=="function"&&(s=e,e=void 0),typeof r=="function"&&(s=r,r="utf8"),e!==void 0&&this.write(e,r),s&&this.once("end",s),this[Ah]=!0,this.writable=!1,(this[Zs]||!this[Cb])&&this[ph](),this}[qw](){this[rs]||(!this[Jm]&&!this[Ta].length&&(this[nc]=!0),this[Cb]=!1,this[Zs]=!0,this.emit("resume"),this[Xs].length?this[_N]():this[Ah]?this[ph]():this.emit("drain"))}resume(){return this[qw]()}pause(){this[Zs]=!1,this[Cb]=!0,this[nc]=!1}get destroyed(){return this[rs]}get flowing(){return this[Zs]}get paused(){return this[Cb]}[e7](e){this[ia]?this[$s]+=1:this[$s]+=e.length,this[Xs].push(e)}[UN](){return this[ia]?this[$s]-=1:this[$s]-=this[Xs][0].length,this[Xs].shift()}[_N](e=!1){do;while(this[UQe](this[UN]())&&this[Xs].length);!e&&!this[Xs].length&&!this[Ah]&&this.emit("drain")}[UQe](e){return this.emit("data",e),this[Zs]}pipe(e,r){if(this[rs])return e;this[nc]=!1;let s=this[gg];return r=r||{},e===MQe.stdout||e===MQe.stderr?r.end=!1:r.end=r.end!==!1,r.proxyErrors=!!r.proxyErrors,s?r.end&&e.end():(this[Ta].push(r.proxyErrors?new s7(this,e,r):new jN(this,e,r)),this[CA]?Bb(()=>this[qw]()):this[qw]()),e}unpipe(e){let r=this[Ta].find(s=>s.dest===e);r&&(this[Ta].length===1?(this[Zs]&&this[Jm]===0&&(this[Zs]=!1),this[Ta]=[]):this[Ta].splice(this[Ta].indexOf(r),1),r.unpipe())}addListener(e,r){return this.on(e,r)}on(e,r){let s=super.on(e,r);if(e==="data")this[nc]=!1,this[Jm]++,!this[Ta].length&&!this[Zs]&&this[qw]();else if(e==="readable"&&this[$s]!==0)super.emit("readable");else if(GNt(e)&&this[gg])super.emit(e),this.removeAllListeners(e);else if(e==="error"&&this[Ib]){let a=r;this[CA]?Bb(()=>a.call(this,this[Ib])):a.call(this,this[Ib])}return s}removeListener(e,r){return this.off(e,r)}off(e,r){let s=super.off(e,r);return e==="data"&&(this[Jm]=this.listeners("data").length,this[Jm]===0&&!this[nc]&&!this[Ta].length&&(this[Zs]=!1)),s}removeAllListeners(e){let r=super.removeAllListeners(e);return(e==="data"||e===void 0)&&(this[Jm]=0,!this[nc]&&!this[Ta].length&&(this[Zs]=!1)),r}get emittedEnd(){return this[gg]}[ph](){!this[LN]&&!this[gg]&&!this[rs]&&this[Xs].length===0&&this[Ah]&&(this[LN]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[MN]&&this.emit("close"),this[LN]=!1)}emit(e,...r){let s=r[0];if(e!=="error"&&e!=="close"&&e!==rs&&this[rs])return!1;if(e==="data")return!this[ia]&&!s?!1:this[CA]?(Bb(()=>this[r7](s)),!0):this[r7](s);if(e==="end")return this[HQe]();if(e==="close"){if(this[MN]=!0,!this[gg]&&!this[rs])return!1;let n=super.emit("close");return this.removeAllListeners("close"),n}else if(e==="error"){this[Ib]=s,super.emit(t7,s);let n=!this[wb]||this.listeners("error").length?super.emit("error",s):!1;return this[ph](),n}else if(e==="resume"){let n=super.emit("resume");return this[ph](),n}else if(e==="finish"||e==="prefinish"){let n=super.emit(e);return this.removeAllListeners(e),n}let a=super.emit(e,...r);return this[ph](),a}[r7](e){for(let s of this[Ta])s.dest.write(e)===!1&&this.pause();let r=this[nc]?!1:super.emit("data",e);return this[ph](),r}[HQe](){return this[gg]?!1:(this[gg]=!0,this.readable=!1,this[CA]?(Bb(()=>this[n7]()),!0):this[n7]())}[n7](){if(this[jw]){let r=this[jw].end();if(r){for(let s of this[Ta])s.dest.write(r);this[nc]||super.emit("data",r)}}for(let r of this[Ta])r.end();let e=super.emit("end");return this.removeAllListeners("end"),e}async collect(){let e=Object.assign([],{dataLength:0});this[ia]||(e.dataLength=0);let r=this.promise();return this.on("data",s=>{e.push(s),this[ia]||(e.dataLength+=s.length)}),await r,e}async concat(){if(this[ia])throw new Error("cannot concat in objectMode");let e=await this.collect();return this[ff]?e.join(""):Buffer.concat(e,e.dataLength)}async promise(){return new Promise((e,r)=>{this.on(rs,()=>r(new Error("stream destroyed"))),this.on("error",s=>r(s)),this.on("end",()=>e())})}[Symbol.asyncIterator](){this[nc]=!1;let e=!1,r=async()=>(this.pause(),e=!0,{value:void 0,done:!0});return{next:()=>{if(e)return r();let a=this.read();if(a!==null)return Promise.resolve({done:!1,value:a});if(this[Ah])return r();let n,c,f=C=>{this.off("data",p),this.off("end",h),this.off(rs,E),r(),c(C)},p=C=>{this.off("error",f),this.off("end",h),this.off(rs,E),this.pause(),n({value:C,done:!!this[Ah]})},h=()=>{this.off("error",f),this.off("data",p),this.off(rs,E),r(),n({done:!0,value:void 0})},E=()=>f(new Error("stream destroyed"));return new Promise((C,S)=>{c=S,n=C,this.once(rs,E),this.once("error",f),this.once("end",h),this.once("data",p)})},throw:r,return:r,[Symbol.asyncIterator](){return this}}}[Symbol.iterator](){this[nc]=!1;let e=!1,r=()=>(this.pause(),this.off(t7,r),this.off(rs,r),this.off("end",r),e=!0,{done:!0,value:void 0}),s=()=>{if(e)return r();let a=this.read();return a===null?r():{done:!1,value:a}};return this.once("end",r),this.once(t7,r),this.once(rs,r),{next:s,throw:r,return:r,[Symbol.iterator](){return this}}}destroy(e){if(this[rs])return e?this.emit("error",e):this.emit(rs),this;this[rs]=!0,this[nc]=!0,this[Xs].length=0,this[$s]=0;let r=this;return typeof r.close=="function"&&!this[MN]&&r.close(),e?this.emit("error",e):this.emit(rs),this}static get isStream(){return Ra.isStream}};Ra.Minipass=qN});var YQe=L((ZEr,wA)=>{"use strict";var Sb=Ie("crypto"),{Minipass:JNt}=qQe(),l7=["sha512","sha384","sha256"],u7=["sha512"],zNt=/^[a-z0-9+/]+(?:=?=?)$/i,ZNt=/^([a-z0-9]+)-([^?]+)([?\S*]*)$/,XNt=/^([a-z0-9]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)?$/,$Nt=/^[\x21-\x7E]+$/,Db=t=>t?.length?`?${t.join("?")}`:"",c7=class extends JNt{#t;#r;#i;constructor(e){super(),this.size=0,this.opts=e,this.#e(),e?.algorithms?this.algorithms=[...e.algorithms]:this.algorithms=[...u7],this.algorithm!==null&&!this.algorithms.includes(this.algorithm)&&this.algorithms.push(this.algorithm),this.hashes=this.algorithms.map(Sb.createHash)}#e(){this.sri=this.opts?.integrity?ic(this.opts?.integrity,this.opts):null,this.expectedSize=this.opts?.size,this.sri?this.sri.isHash?(this.goodSri=!0,this.algorithm=this.sri.algorithm):(this.goodSri=!this.sri.isEmpty(),this.algorithm=this.sri.pickAlgorithm(this.opts)):this.algorithm=null,this.digests=this.goodSri?this.sri[this.algorithm]:null,this.optString=Db(this.opts?.options)}on(e,r){return e==="size"&&this.#r?r(this.#r):e==="integrity"&&this.#t?r(this.#t):e==="verified"&&this.#i?r(this.#i):super.on(e,r)}emit(e,r){return e==="end"&&this.#n(),super.emit(e,r)}write(e){return this.size+=e.length,this.hashes.forEach(r=>r.update(e)),super.write(e)}#n(){this.goodSri||this.#e();let e=ic(this.hashes.map((s,a)=>`${this.algorithms[a]}-${s.digest("base64")}${this.optString}`).join(" "),this.opts),r=this.goodSri&&e.match(this.sri,this.opts);if(typeof this.expectedSize=="number"&&this.size!==this.expectedSize){let s=new Error(`stream size mismatch when checking ${this.sri}. Wanted: ${this.expectedSize} Found: ${this.size}`);s.code="EBADSIZE",s.found=this.size,s.expected=this.expectedSize,s.sri=this.sri,this.emit("error",s)}else if(this.sri&&!r){let s=new Error(`${this.sri} integrity checksum failed when using ${this.algorithm}: wanted ${this.digests} but got ${e}. (${this.size} bytes)`);s.code="EINTEGRITY",s.found=e,s.expected=this.digests,s.algorithm=this.algorithm,s.sri=this.sri,this.emit("error",s)}else this.#r=this.size,this.emit("size",this.size),this.#t=e,this.emit("integrity",e),r&&(this.#i=r,this.emit("verified",r))}},hh=class{get isHash(){return!0}constructor(e,r){let s=r?.strict;this.source=e.trim(),this.digest="",this.algorithm="",this.options=[];let a=this.source.match(s?XNt:ZNt);if(!a||s&&!l7.includes(a[1]))return;this.algorithm=a[1],this.digest=a[2];let n=a[3];n&&(this.options=n.slice(1).split("?"))}hexDigest(){return this.digest&&Buffer.from(this.digest,"base64").toString("hex")}toJSON(){return this.toString()}match(e,r){let s=ic(e,r);if(!s)return!1;if(s.isIntegrity){let a=s.pickAlgorithm(r,[this.algorithm]);if(!a)return!1;let n=s[a].find(c=>c.digest===this.digest);return n||!1}return s.digest===this.digest?s:!1}toString(e){return e?.strict&&!(l7.includes(this.algorithm)&&this.digest.match(zNt)&&this.options.every(r=>r.match($Nt)))?"":`${this.algorithm}-${this.digest}${Db(this.options)}`}};function GQe(t,e,r,s){let a=t!=="",n=!1,c="",f=s.length-1;for(let h=0;hs[a].find(c=>n.digest===c.digest)))throw new Error("hashes do not match, cannot update integrity")}else this[a]=s[a]}match(e,r){let s=ic(e,r);if(!s)return!1;let a=s.pickAlgorithm(r,Object.keys(this));return!!a&&this[a]&&s[a]&&this[a].find(n=>s[a].find(c=>n.digest===c.digest))||!1}pickAlgorithm(e,r){let s=e?.pickAlgorithm||aOt,a=Object.keys(this).filter(n=>r?.length?r.includes(n):!0);return a.length?a.reduce((n,c)=>s(n,c)||n):null}};wA.exports.parse=ic;function ic(t,e){if(!t)return null;if(typeof t=="string")return a7(t,e);if(t.algorithm&&t.digest){let r=new zm;return r[t.algorithm]=[t],a7(vb(r,e),e)}else return a7(vb(t,e),e)}function a7(t,e){if(e?.single)return new hh(t,e);let r=t.trim().split(/\s+/).reduce((s,a)=>{let n=new hh(a,e);if(n.algorithm&&n.digest){let c=n.algorithm;s[c]||(s[c]=[]),s[c].push(n)}return s},new zm);return r.isEmpty()?null:r}wA.exports.stringify=vb;function vb(t,e){return t.algorithm&&t.digest?hh.prototype.toString.call(t,e):typeof t=="string"?vb(ic(t,e),e):zm.prototype.toString.call(t,e)}wA.exports.fromHex=eOt;function eOt(t,e,r){let s=Db(r?.options);return ic(`${e}-${Buffer.from(t,"hex").toString("base64")}${s}`,r)}wA.exports.fromData=tOt;function tOt(t,e){let r=e?.algorithms||[...u7],s=Db(e?.options);return r.reduce((a,n)=>{let c=Sb.createHash(n).update(t).digest("base64"),f=new hh(`${n}-${c}${s}`,e);if(f.algorithm&&f.digest){let p=f.algorithm;a[p]||(a[p]=[]),a[p].push(f)}return a},new zm)}wA.exports.fromStream=rOt;function rOt(t,e){let r=f7(e);return new Promise((s,a)=>{t.pipe(r),t.on("error",a),r.on("error",a);let n;r.on("integrity",c=>{n=c}),r.on("end",()=>s(n)),r.resume()})}wA.exports.checkData=nOt;function nOt(t,e,r){if(e=ic(e,r),!e||!Object.keys(e).length){if(r?.error)throw Object.assign(new Error("No valid integrity hashes to check against"),{code:"EINTEGRITY"});return!1}let s=e.pickAlgorithm(r),a=Sb.createHash(s).update(t).digest("base64"),n=ic({algorithm:s,digest:a}),c=n.match(e,r);if(r=r||{},c||!r.error)return c;if(typeof r.size=="number"&&t.length!==r.size){let f=new Error(`data size mismatch when checking ${e}. Wanted: ${r.size} Found: ${t.length}`);throw f.code="EBADSIZE",f.found=t.length,f.expected=r.size,f.sri=e,f}else{let f=new Error(`Integrity checksum failed when using ${s}: Wanted ${e}, but got ${n}. (${t.length} bytes)`);throw f.code="EINTEGRITY",f.found=n,f.expected=e,f.algorithm=s,f.sri=e,f}}wA.exports.checkStream=iOt;function iOt(t,e,r){if(r=r||Object.create(null),r.integrity=e,e=ic(e,r),!e||!Object.keys(e).length)return Promise.reject(Object.assign(new Error("No valid integrity hashes to check against"),{code:"EINTEGRITY"}));let s=f7(r);return new Promise((a,n)=>{t.pipe(s),t.on("error",n),s.on("error",n);let c;s.on("verified",f=>{c=f}),s.on("end",()=>a(c)),s.resume()})}wA.exports.integrityStream=f7;function f7(t=Object.create(null)){return new c7(t)}wA.exports.create=sOt;function sOt(t){let e=t?.algorithms||[...u7],r=Db(t?.options),s=e.map(Sb.createHash);return{update:function(a,n){return s.forEach(c=>c.update(a,n)),this},digest:function(){return e.reduce((n,c)=>{let f=s.shift().digest("base64"),p=new hh(`${c}-${f}${r}`,t);if(p.algorithm&&p.digest){let h=p.algorithm;n[h]||(n[h]=[]),n[h].push(p)}return n},new zm)}}}var oOt=Sb.getHashes(),WQe=["md5","whirlpool","sha1","sha224","sha256","sha384","sha512","sha3","sha3-256","sha3-384","sha3-512","sha3_256","sha3_384","sha3_512"].filter(t=>oOt.includes(t));function aOt(t,e){return WQe.indexOf(t.toLowerCase())>=WQe.indexOf(e.toLowerCase())?t:e}});var A7=L(dg=>{"use strict";Object.defineProperty(dg,"__esModule",{value:!0});dg.Signature=dg.Envelope=void 0;dg.Envelope={fromJSON(t){return{payload:GN(t.payload)?Buffer.from(VQe(t.payload)):Buffer.alloc(0),payloadType:GN(t.payloadType)?globalThis.String(t.payloadType):"",signatures:globalThis.Array.isArray(t?.signatures)?t.signatures.map(e=>dg.Signature.fromJSON(e)):[]}},toJSON(t){let e={};return t.payload.length!==0&&(e.payload=KQe(t.payload)),t.payloadType!==""&&(e.payloadType=t.payloadType),t.signatures?.length&&(e.signatures=t.signatures.map(r=>dg.Signature.toJSON(r))),e}};dg.Signature={fromJSON(t){return{sig:GN(t.sig)?Buffer.from(VQe(t.sig)):Buffer.alloc(0),keyid:GN(t.keyid)?globalThis.String(t.keyid):""}},toJSON(t){let e={};return t.sig.length!==0&&(e.sig=KQe(t.sig)),t.keyid!==""&&(e.keyid=t.keyid),e}};function VQe(t){return Uint8Array.from(globalThis.Buffer.from(t,"base64"))}function KQe(t){return globalThis.Buffer.from(t).toString("base64")}function GN(t){return t!=null}});var zQe=L(WN=>{"use strict";Object.defineProperty(WN,"__esModule",{value:!0});WN.Timestamp=void 0;WN.Timestamp={fromJSON(t){return{seconds:JQe(t.seconds)?globalThis.String(t.seconds):"0",nanos:JQe(t.nanos)?globalThis.Number(t.nanos):0}},toJSON(t){let e={};return t.seconds!=="0"&&(e.seconds=t.seconds),t.nanos!==0&&(e.nanos=Math.round(t.nanos)),e}};function JQe(t){return t!=null}});var Gw=L(_r=>{"use strict";Object.defineProperty(_r,"__esModule",{value:!0});_r.TimeRange=_r.X509CertificateChain=_r.SubjectAlternativeName=_r.X509Certificate=_r.DistinguishedName=_r.ObjectIdentifierValuePair=_r.ObjectIdentifier=_r.PublicKeyIdentifier=_r.PublicKey=_r.RFC3161SignedTimestamp=_r.LogId=_r.MessageSignature=_r.HashOutput=_r.SubjectAlternativeNameType=_r.PublicKeyDetails=_r.HashAlgorithm=void 0;_r.hashAlgorithmFromJSON=XQe;_r.hashAlgorithmToJSON=$Qe;_r.publicKeyDetailsFromJSON=eTe;_r.publicKeyDetailsToJSON=tTe;_r.subjectAlternativeNameTypeFromJSON=rTe;_r.subjectAlternativeNameTypeToJSON=nTe;var lOt=zQe(),El;(function(t){t[t.HASH_ALGORITHM_UNSPECIFIED=0]="HASH_ALGORITHM_UNSPECIFIED",t[t.SHA2_256=1]="SHA2_256",t[t.SHA2_384=2]="SHA2_384",t[t.SHA2_512=3]="SHA2_512",t[t.SHA3_256=4]="SHA3_256",t[t.SHA3_384=5]="SHA3_384"})(El||(_r.HashAlgorithm=El={}));function XQe(t){switch(t){case 0:case"HASH_ALGORITHM_UNSPECIFIED":return El.HASH_ALGORITHM_UNSPECIFIED;case 1:case"SHA2_256":return El.SHA2_256;case 2:case"SHA2_384":return El.SHA2_384;case 3:case"SHA2_512":return El.SHA2_512;case 4:case"SHA3_256":return El.SHA3_256;case 5:case"SHA3_384":return El.SHA3_384;default:throw new globalThis.Error("Unrecognized enum value "+t+" for enum HashAlgorithm")}}function $Qe(t){switch(t){case El.HASH_ALGORITHM_UNSPECIFIED:return"HASH_ALGORITHM_UNSPECIFIED";case El.SHA2_256:return"SHA2_256";case El.SHA2_384:return"SHA2_384";case El.SHA2_512:return"SHA2_512";case El.SHA3_256:return"SHA3_256";case El.SHA3_384:return"SHA3_384";default:throw new globalThis.Error("Unrecognized enum value "+t+" for enum HashAlgorithm")}}var rn;(function(t){t[t.PUBLIC_KEY_DETAILS_UNSPECIFIED=0]="PUBLIC_KEY_DETAILS_UNSPECIFIED",t[t.PKCS1_RSA_PKCS1V5=1]="PKCS1_RSA_PKCS1V5",t[t.PKCS1_RSA_PSS=2]="PKCS1_RSA_PSS",t[t.PKIX_RSA_PKCS1V5=3]="PKIX_RSA_PKCS1V5",t[t.PKIX_RSA_PSS=4]="PKIX_RSA_PSS",t[t.PKIX_RSA_PKCS1V15_2048_SHA256=9]="PKIX_RSA_PKCS1V15_2048_SHA256",t[t.PKIX_RSA_PKCS1V15_3072_SHA256=10]="PKIX_RSA_PKCS1V15_3072_SHA256",t[t.PKIX_RSA_PKCS1V15_4096_SHA256=11]="PKIX_RSA_PKCS1V15_4096_SHA256",t[t.PKIX_RSA_PSS_2048_SHA256=16]="PKIX_RSA_PSS_2048_SHA256",t[t.PKIX_RSA_PSS_3072_SHA256=17]="PKIX_RSA_PSS_3072_SHA256",t[t.PKIX_RSA_PSS_4096_SHA256=18]="PKIX_RSA_PSS_4096_SHA256",t[t.PKIX_ECDSA_P256_HMAC_SHA_256=6]="PKIX_ECDSA_P256_HMAC_SHA_256",t[t.PKIX_ECDSA_P256_SHA_256=5]="PKIX_ECDSA_P256_SHA_256",t[t.PKIX_ECDSA_P384_SHA_384=12]="PKIX_ECDSA_P384_SHA_384",t[t.PKIX_ECDSA_P521_SHA_512=13]="PKIX_ECDSA_P521_SHA_512",t[t.PKIX_ED25519=7]="PKIX_ED25519",t[t.PKIX_ED25519_PH=8]="PKIX_ED25519_PH",t[t.LMS_SHA256=14]="LMS_SHA256",t[t.LMOTS_SHA256=15]="LMOTS_SHA256"})(rn||(_r.PublicKeyDetails=rn={}));function eTe(t){switch(t){case 0:case"PUBLIC_KEY_DETAILS_UNSPECIFIED":return rn.PUBLIC_KEY_DETAILS_UNSPECIFIED;case 1:case"PKCS1_RSA_PKCS1V5":return rn.PKCS1_RSA_PKCS1V5;case 2:case"PKCS1_RSA_PSS":return rn.PKCS1_RSA_PSS;case 3:case"PKIX_RSA_PKCS1V5":return rn.PKIX_RSA_PKCS1V5;case 4:case"PKIX_RSA_PSS":return rn.PKIX_RSA_PSS;case 9:case"PKIX_RSA_PKCS1V15_2048_SHA256":return rn.PKIX_RSA_PKCS1V15_2048_SHA256;case 10:case"PKIX_RSA_PKCS1V15_3072_SHA256":return rn.PKIX_RSA_PKCS1V15_3072_SHA256;case 11:case"PKIX_RSA_PKCS1V15_4096_SHA256":return rn.PKIX_RSA_PKCS1V15_4096_SHA256;case 16:case"PKIX_RSA_PSS_2048_SHA256":return rn.PKIX_RSA_PSS_2048_SHA256;case 17:case"PKIX_RSA_PSS_3072_SHA256":return rn.PKIX_RSA_PSS_3072_SHA256;case 18:case"PKIX_RSA_PSS_4096_SHA256":return rn.PKIX_RSA_PSS_4096_SHA256;case 6:case"PKIX_ECDSA_P256_HMAC_SHA_256":return rn.PKIX_ECDSA_P256_HMAC_SHA_256;case 5:case"PKIX_ECDSA_P256_SHA_256":return rn.PKIX_ECDSA_P256_SHA_256;case 12:case"PKIX_ECDSA_P384_SHA_384":return rn.PKIX_ECDSA_P384_SHA_384;case 13:case"PKIX_ECDSA_P521_SHA_512":return rn.PKIX_ECDSA_P521_SHA_512;case 7:case"PKIX_ED25519":return rn.PKIX_ED25519;case 8:case"PKIX_ED25519_PH":return rn.PKIX_ED25519_PH;case 14:case"LMS_SHA256":return rn.LMS_SHA256;case 15:case"LMOTS_SHA256":return rn.LMOTS_SHA256;default:throw new globalThis.Error("Unrecognized enum value "+t+" for enum PublicKeyDetails")}}function tTe(t){switch(t){case rn.PUBLIC_KEY_DETAILS_UNSPECIFIED:return"PUBLIC_KEY_DETAILS_UNSPECIFIED";case rn.PKCS1_RSA_PKCS1V5:return"PKCS1_RSA_PKCS1V5";case rn.PKCS1_RSA_PSS:return"PKCS1_RSA_PSS";case rn.PKIX_RSA_PKCS1V5:return"PKIX_RSA_PKCS1V5";case rn.PKIX_RSA_PSS:return"PKIX_RSA_PSS";case rn.PKIX_RSA_PKCS1V15_2048_SHA256:return"PKIX_RSA_PKCS1V15_2048_SHA256";case rn.PKIX_RSA_PKCS1V15_3072_SHA256:return"PKIX_RSA_PKCS1V15_3072_SHA256";case rn.PKIX_RSA_PKCS1V15_4096_SHA256:return"PKIX_RSA_PKCS1V15_4096_SHA256";case rn.PKIX_RSA_PSS_2048_SHA256:return"PKIX_RSA_PSS_2048_SHA256";case rn.PKIX_RSA_PSS_3072_SHA256:return"PKIX_RSA_PSS_3072_SHA256";case rn.PKIX_RSA_PSS_4096_SHA256:return"PKIX_RSA_PSS_4096_SHA256";case rn.PKIX_ECDSA_P256_HMAC_SHA_256:return"PKIX_ECDSA_P256_HMAC_SHA_256";case rn.PKIX_ECDSA_P256_SHA_256:return"PKIX_ECDSA_P256_SHA_256";case rn.PKIX_ECDSA_P384_SHA_384:return"PKIX_ECDSA_P384_SHA_384";case rn.PKIX_ECDSA_P521_SHA_512:return"PKIX_ECDSA_P521_SHA_512";case rn.PKIX_ED25519:return"PKIX_ED25519";case rn.PKIX_ED25519_PH:return"PKIX_ED25519_PH";case rn.LMS_SHA256:return"LMS_SHA256";case rn.LMOTS_SHA256:return"LMOTS_SHA256";default:throw new globalThis.Error("Unrecognized enum value "+t+" for enum PublicKeyDetails")}}var BA;(function(t){t[t.SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED=0]="SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED",t[t.EMAIL=1]="EMAIL",t[t.URI=2]="URI",t[t.OTHER_NAME=3]="OTHER_NAME"})(BA||(_r.SubjectAlternativeNameType=BA={}));function rTe(t){switch(t){case 0:case"SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED":return BA.SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED;case 1:case"EMAIL":return BA.EMAIL;case 2:case"URI":return BA.URI;case 3:case"OTHER_NAME":return BA.OTHER_NAME;default:throw new globalThis.Error("Unrecognized enum value "+t+" for enum SubjectAlternativeNameType")}}function nTe(t){switch(t){case BA.SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED:return"SUBJECT_ALTERNATIVE_NAME_TYPE_UNSPECIFIED";case BA.EMAIL:return"EMAIL";case BA.URI:return"URI";case BA.OTHER_NAME:return"OTHER_NAME";default:throw new globalThis.Error("Unrecognized enum value "+t+" for enum SubjectAlternativeNameType")}}_r.HashOutput={fromJSON(t){return{algorithm:ys(t.algorithm)?XQe(t.algorithm):0,digest:ys(t.digest)?Buffer.from(Zm(t.digest)):Buffer.alloc(0)}},toJSON(t){let e={};return t.algorithm!==0&&(e.algorithm=$Qe(t.algorithm)),t.digest.length!==0&&(e.digest=Xm(t.digest)),e}};_r.MessageSignature={fromJSON(t){return{messageDigest:ys(t.messageDigest)?_r.HashOutput.fromJSON(t.messageDigest):void 0,signature:ys(t.signature)?Buffer.from(Zm(t.signature)):Buffer.alloc(0)}},toJSON(t){let e={};return t.messageDigest!==void 0&&(e.messageDigest=_r.HashOutput.toJSON(t.messageDigest)),t.signature.length!==0&&(e.signature=Xm(t.signature)),e}};_r.LogId={fromJSON(t){return{keyId:ys(t.keyId)?Buffer.from(Zm(t.keyId)):Buffer.alloc(0)}},toJSON(t){let e={};return t.keyId.length!==0&&(e.keyId=Xm(t.keyId)),e}};_r.RFC3161SignedTimestamp={fromJSON(t){return{signedTimestamp:ys(t.signedTimestamp)?Buffer.from(Zm(t.signedTimestamp)):Buffer.alloc(0)}},toJSON(t){let e={};return t.signedTimestamp.length!==0&&(e.signedTimestamp=Xm(t.signedTimestamp)),e}};_r.PublicKey={fromJSON(t){return{rawBytes:ys(t.rawBytes)?Buffer.from(Zm(t.rawBytes)):void 0,keyDetails:ys(t.keyDetails)?eTe(t.keyDetails):0,validFor:ys(t.validFor)?_r.TimeRange.fromJSON(t.validFor):void 0}},toJSON(t){let e={};return t.rawBytes!==void 0&&(e.rawBytes=Xm(t.rawBytes)),t.keyDetails!==0&&(e.keyDetails=tTe(t.keyDetails)),t.validFor!==void 0&&(e.validFor=_r.TimeRange.toJSON(t.validFor)),e}};_r.PublicKeyIdentifier={fromJSON(t){return{hint:ys(t.hint)?globalThis.String(t.hint):""}},toJSON(t){let e={};return t.hint!==""&&(e.hint=t.hint),e}};_r.ObjectIdentifier={fromJSON(t){return{id:globalThis.Array.isArray(t?.id)?t.id.map(e=>globalThis.Number(e)):[]}},toJSON(t){let e={};return t.id?.length&&(e.id=t.id.map(r=>Math.round(r))),e}};_r.ObjectIdentifierValuePair={fromJSON(t){return{oid:ys(t.oid)?_r.ObjectIdentifier.fromJSON(t.oid):void 0,value:ys(t.value)?Buffer.from(Zm(t.value)):Buffer.alloc(0)}},toJSON(t){let e={};return t.oid!==void 0&&(e.oid=_r.ObjectIdentifier.toJSON(t.oid)),t.value.length!==0&&(e.value=Xm(t.value)),e}};_r.DistinguishedName={fromJSON(t){return{organization:ys(t.organization)?globalThis.String(t.organization):"",commonName:ys(t.commonName)?globalThis.String(t.commonName):""}},toJSON(t){let e={};return t.organization!==""&&(e.organization=t.organization),t.commonName!==""&&(e.commonName=t.commonName),e}};_r.X509Certificate={fromJSON(t){return{rawBytes:ys(t.rawBytes)?Buffer.from(Zm(t.rawBytes)):Buffer.alloc(0)}},toJSON(t){let e={};return t.rawBytes.length!==0&&(e.rawBytes=Xm(t.rawBytes)),e}};_r.SubjectAlternativeName={fromJSON(t){return{type:ys(t.type)?rTe(t.type):0,identity:ys(t.regexp)?{$case:"regexp",regexp:globalThis.String(t.regexp)}:ys(t.value)?{$case:"value",value:globalThis.String(t.value)}:void 0}},toJSON(t){let e={};return t.type!==0&&(e.type=nTe(t.type)),t.identity?.$case==="regexp"?e.regexp=t.identity.regexp:t.identity?.$case==="value"&&(e.value=t.identity.value),e}};_r.X509CertificateChain={fromJSON(t){return{certificates:globalThis.Array.isArray(t?.certificates)?t.certificates.map(e=>_r.X509Certificate.fromJSON(e)):[]}},toJSON(t){let e={};return t.certificates?.length&&(e.certificates=t.certificates.map(r=>_r.X509Certificate.toJSON(r))),e}};_r.TimeRange={fromJSON(t){return{start:ys(t.start)?ZQe(t.start):void 0,end:ys(t.end)?ZQe(t.end):void 0}},toJSON(t){let e={};return t.start!==void 0&&(e.start=t.start.toISOString()),t.end!==void 0&&(e.end=t.end.toISOString()),e}};function Zm(t){return Uint8Array.from(globalThis.Buffer.from(t,"base64"))}function Xm(t){return globalThis.Buffer.from(t).toString("base64")}function cOt(t){let e=(globalThis.Number(t.seconds)||0)*1e3;return e+=(t.nanos||0)/1e6,new globalThis.Date(e)}function ZQe(t){return t instanceof globalThis.Date?t:typeof t=="string"?new globalThis.Date(t):cOt(lOt.Timestamp.fromJSON(t))}function ys(t){return t!=null}});var p7=L(Es=>{"use strict";Object.defineProperty(Es,"__esModule",{value:!0});Es.TransparencyLogEntry=Es.InclusionPromise=Es.InclusionProof=Es.Checkpoint=Es.KindVersion=void 0;var iTe=Gw();Es.KindVersion={fromJSON(t){return{kind:Fa(t.kind)?globalThis.String(t.kind):"",version:Fa(t.version)?globalThis.String(t.version):""}},toJSON(t){let e={};return t.kind!==""&&(e.kind=t.kind),t.version!==""&&(e.version=t.version),e}};Es.Checkpoint={fromJSON(t){return{envelope:Fa(t.envelope)?globalThis.String(t.envelope):""}},toJSON(t){let e={};return t.envelope!==""&&(e.envelope=t.envelope),e}};Es.InclusionProof={fromJSON(t){return{logIndex:Fa(t.logIndex)?globalThis.String(t.logIndex):"0",rootHash:Fa(t.rootHash)?Buffer.from(YN(t.rootHash)):Buffer.alloc(0),treeSize:Fa(t.treeSize)?globalThis.String(t.treeSize):"0",hashes:globalThis.Array.isArray(t?.hashes)?t.hashes.map(e=>Buffer.from(YN(e))):[],checkpoint:Fa(t.checkpoint)?Es.Checkpoint.fromJSON(t.checkpoint):void 0}},toJSON(t){let e={};return t.logIndex!=="0"&&(e.logIndex=t.logIndex),t.rootHash.length!==0&&(e.rootHash=VN(t.rootHash)),t.treeSize!=="0"&&(e.treeSize=t.treeSize),t.hashes?.length&&(e.hashes=t.hashes.map(r=>VN(r))),t.checkpoint!==void 0&&(e.checkpoint=Es.Checkpoint.toJSON(t.checkpoint)),e}};Es.InclusionPromise={fromJSON(t){return{signedEntryTimestamp:Fa(t.signedEntryTimestamp)?Buffer.from(YN(t.signedEntryTimestamp)):Buffer.alloc(0)}},toJSON(t){let e={};return t.signedEntryTimestamp.length!==0&&(e.signedEntryTimestamp=VN(t.signedEntryTimestamp)),e}};Es.TransparencyLogEntry={fromJSON(t){return{logIndex:Fa(t.logIndex)?globalThis.String(t.logIndex):"0",logId:Fa(t.logId)?iTe.LogId.fromJSON(t.logId):void 0,kindVersion:Fa(t.kindVersion)?Es.KindVersion.fromJSON(t.kindVersion):void 0,integratedTime:Fa(t.integratedTime)?globalThis.String(t.integratedTime):"0",inclusionPromise:Fa(t.inclusionPromise)?Es.InclusionPromise.fromJSON(t.inclusionPromise):void 0,inclusionProof:Fa(t.inclusionProof)?Es.InclusionProof.fromJSON(t.inclusionProof):void 0,canonicalizedBody:Fa(t.canonicalizedBody)?Buffer.from(YN(t.canonicalizedBody)):Buffer.alloc(0)}},toJSON(t){let e={};return t.logIndex!=="0"&&(e.logIndex=t.logIndex),t.logId!==void 0&&(e.logId=iTe.LogId.toJSON(t.logId)),t.kindVersion!==void 0&&(e.kindVersion=Es.KindVersion.toJSON(t.kindVersion)),t.integratedTime!=="0"&&(e.integratedTime=t.integratedTime),t.inclusionPromise!==void 0&&(e.inclusionPromise=Es.InclusionPromise.toJSON(t.inclusionPromise)),t.inclusionProof!==void 0&&(e.inclusionProof=Es.InclusionProof.toJSON(t.inclusionProof)),t.canonicalizedBody.length!==0&&(e.canonicalizedBody=VN(t.canonicalizedBody)),e}};function YN(t){return Uint8Array.from(globalThis.Buffer.from(t,"base64"))}function VN(t){return globalThis.Buffer.from(t).toString("base64")}function Fa(t){return t!=null}});var h7=L($c=>{"use strict";Object.defineProperty($c,"__esModule",{value:!0});$c.Bundle=$c.VerificationMaterial=$c.TimestampVerificationData=void 0;var sTe=A7(),vA=Gw(),oTe=p7();$c.TimestampVerificationData={fromJSON(t){return{rfc3161Timestamps:globalThis.Array.isArray(t?.rfc3161Timestamps)?t.rfc3161Timestamps.map(e=>vA.RFC3161SignedTimestamp.fromJSON(e)):[]}},toJSON(t){let e={};return t.rfc3161Timestamps?.length&&(e.rfc3161Timestamps=t.rfc3161Timestamps.map(r=>vA.RFC3161SignedTimestamp.toJSON(r))),e}};$c.VerificationMaterial={fromJSON(t){return{content:mg(t.publicKey)?{$case:"publicKey",publicKey:vA.PublicKeyIdentifier.fromJSON(t.publicKey)}:mg(t.x509CertificateChain)?{$case:"x509CertificateChain",x509CertificateChain:vA.X509CertificateChain.fromJSON(t.x509CertificateChain)}:mg(t.certificate)?{$case:"certificate",certificate:vA.X509Certificate.fromJSON(t.certificate)}:void 0,tlogEntries:globalThis.Array.isArray(t?.tlogEntries)?t.tlogEntries.map(e=>oTe.TransparencyLogEntry.fromJSON(e)):[],timestampVerificationData:mg(t.timestampVerificationData)?$c.TimestampVerificationData.fromJSON(t.timestampVerificationData):void 0}},toJSON(t){let e={};return t.content?.$case==="publicKey"?e.publicKey=vA.PublicKeyIdentifier.toJSON(t.content.publicKey):t.content?.$case==="x509CertificateChain"?e.x509CertificateChain=vA.X509CertificateChain.toJSON(t.content.x509CertificateChain):t.content?.$case==="certificate"&&(e.certificate=vA.X509Certificate.toJSON(t.content.certificate)),t.tlogEntries?.length&&(e.tlogEntries=t.tlogEntries.map(r=>oTe.TransparencyLogEntry.toJSON(r))),t.timestampVerificationData!==void 0&&(e.timestampVerificationData=$c.TimestampVerificationData.toJSON(t.timestampVerificationData)),e}};$c.Bundle={fromJSON(t){return{mediaType:mg(t.mediaType)?globalThis.String(t.mediaType):"",verificationMaterial:mg(t.verificationMaterial)?$c.VerificationMaterial.fromJSON(t.verificationMaterial):void 0,content:mg(t.messageSignature)?{$case:"messageSignature",messageSignature:vA.MessageSignature.fromJSON(t.messageSignature)}:mg(t.dsseEnvelope)?{$case:"dsseEnvelope",dsseEnvelope:sTe.Envelope.fromJSON(t.dsseEnvelope)}:void 0}},toJSON(t){let e={};return t.mediaType!==""&&(e.mediaType=t.mediaType),t.verificationMaterial!==void 0&&(e.verificationMaterial=$c.VerificationMaterial.toJSON(t.verificationMaterial)),t.content?.$case==="messageSignature"?e.messageSignature=vA.MessageSignature.toJSON(t.content.messageSignature):t.content?.$case==="dsseEnvelope"&&(e.dsseEnvelope=sTe.Envelope.toJSON(t.content.dsseEnvelope)),e}};function mg(t){return t!=null}});var g7=L(Fi=>{"use strict";Object.defineProperty(Fi,"__esModule",{value:!0});Fi.ClientTrustConfig=Fi.SigningConfig=Fi.TrustedRoot=Fi.CertificateAuthority=Fi.TransparencyLogInstance=void 0;var Il=Gw();Fi.TransparencyLogInstance={fromJSON(t){return{baseUrl:sa(t.baseUrl)?globalThis.String(t.baseUrl):"",hashAlgorithm:sa(t.hashAlgorithm)?(0,Il.hashAlgorithmFromJSON)(t.hashAlgorithm):0,publicKey:sa(t.publicKey)?Il.PublicKey.fromJSON(t.publicKey):void 0,logId:sa(t.logId)?Il.LogId.fromJSON(t.logId):void 0,checkpointKeyId:sa(t.checkpointKeyId)?Il.LogId.fromJSON(t.checkpointKeyId):void 0}},toJSON(t){let e={};return t.baseUrl!==""&&(e.baseUrl=t.baseUrl),t.hashAlgorithm!==0&&(e.hashAlgorithm=(0,Il.hashAlgorithmToJSON)(t.hashAlgorithm)),t.publicKey!==void 0&&(e.publicKey=Il.PublicKey.toJSON(t.publicKey)),t.logId!==void 0&&(e.logId=Il.LogId.toJSON(t.logId)),t.checkpointKeyId!==void 0&&(e.checkpointKeyId=Il.LogId.toJSON(t.checkpointKeyId)),e}};Fi.CertificateAuthority={fromJSON(t){return{subject:sa(t.subject)?Il.DistinguishedName.fromJSON(t.subject):void 0,uri:sa(t.uri)?globalThis.String(t.uri):"",certChain:sa(t.certChain)?Il.X509CertificateChain.fromJSON(t.certChain):void 0,validFor:sa(t.validFor)?Il.TimeRange.fromJSON(t.validFor):void 0}},toJSON(t){let e={};return t.subject!==void 0&&(e.subject=Il.DistinguishedName.toJSON(t.subject)),t.uri!==""&&(e.uri=t.uri),t.certChain!==void 0&&(e.certChain=Il.X509CertificateChain.toJSON(t.certChain)),t.validFor!==void 0&&(e.validFor=Il.TimeRange.toJSON(t.validFor)),e}};Fi.TrustedRoot={fromJSON(t){return{mediaType:sa(t.mediaType)?globalThis.String(t.mediaType):"",tlogs:globalThis.Array.isArray(t?.tlogs)?t.tlogs.map(e=>Fi.TransparencyLogInstance.fromJSON(e)):[],certificateAuthorities:globalThis.Array.isArray(t?.certificateAuthorities)?t.certificateAuthorities.map(e=>Fi.CertificateAuthority.fromJSON(e)):[],ctlogs:globalThis.Array.isArray(t?.ctlogs)?t.ctlogs.map(e=>Fi.TransparencyLogInstance.fromJSON(e)):[],timestampAuthorities:globalThis.Array.isArray(t?.timestampAuthorities)?t.timestampAuthorities.map(e=>Fi.CertificateAuthority.fromJSON(e)):[]}},toJSON(t){let e={};return t.mediaType!==""&&(e.mediaType=t.mediaType),t.tlogs?.length&&(e.tlogs=t.tlogs.map(r=>Fi.TransparencyLogInstance.toJSON(r))),t.certificateAuthorities?.length&&(e.certificateAuthorities=t.certificateAuthorities.map(r=>Fi.CertificateAuthority.toJSON(r))),t.ctlogs?.length&&(e.ctlogs=t.ctlogs.map(r=>Fi.TransparencyLogInstance.toJSON(r))),t.timestampAuthorities?.length&&(e.timestampAuthorities=t.timestampAuthorities.map(r=>Fi.CertificateAuthority.toJSON(r))),e}};Fi.SigningConfig={fromJSON(t){return{mediaType:sa(t.mediaType)?globalThis.String(t.mediaType):"",caUrl:sa(t.caUrl)?globalThis.String(t.caUrl):"",oidcUrl:sa(t.oidcUrl)?globalThis.String(t.oidcUrl):"",tlogUrls:globalThis.Array.isArray(t?.tlogUrls)?t.tlogUrls.map(e=>globalThis.String(e)):[],tsaUrls:globalThis.Array.isArray(t?.tsaUrls)?t.tsaUrls.map(e=>globalThis.String(e)):[]}},toJSON(t){let e={};return t.mediaType!==""&&(e.mediaType=t.mediaType),t.caUrl!==""&&(e.caUrl=t.caUrl),t.oidcUrl!==""&&(e.oidcUrl=t.oidcUrl),t.tlogUrls?.length&&(e.tlogUrls=t.tlogUrls),t.tsaUrls?.length&&(e.tsaUrls=t.tsaUrls),e}};Fi.ClientTrustConfig={fromJSON(t){return{mediaType:sa(t.mediaType)?globalThis.String(t.mediaType):"",trustedRoot:sa(t.trustedRoot)?Fi.TrustedRoot.fromJSON(t.trustedRoot):void 0,signingConfig:sa(t.signingConfig)?Fi.SigningConfig.fromJSON(t.signingConfig):void 0}},toJSON(t){let e={};return t.mediaType!==""&&(e.mediaType=t.mediaType),t.trustedRoot!==void 0&&(e.trustedRoot=Fi.TrustedRoot.toJSON(t.trustedRoot)),t.signingConfig!==void 0&&(e.signingConfig=Fi.SigningConfig.toJSON(t.signingConfig)),e}};function sa(t){return t!=null}});var cTe=L(Vr=>{"use strict";Object.defineProperty(Vr,"__esModule",{value:!0});Vr.Input=Vr.Artifact=Vr.ArtifactVerificationOptions_ObserverTimestampOptions=Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions=Vr.ArtifactVerificationOptions_TimestampAuthorityOptions=Vr.ArtifactVerificationOptions_CtlogOptions=Vr.ArtifactVerificationOptions_TlogOptions=Vr.ArtifactVerificationOptions=Vr.PublicKeyIdentities=Vr.CertificateIdentities=Vr.CertificateIdentity=void 0;var aTe=h7(),yg=Gw(),lTe=g7();Vr.CertificateIdentity={fromJSON(t){return{issuer:gi(t.issuer)?globalThis.String(t.issuer):"",san:gi(t.san)?yg.SubjectAlternativeName.fromJSON(t.san):void 0,oids:globalThis.Array.isArray(t?.oids)?t.oids.map(e=>yg.ObjectIdentifierValuePair.fromJSON(e)):[]}},toJSON(t){let e={};return t.issuer!==""&&(e.issuer=t.issuer),t.san!==void 0&&(e.san=yg.SubjectAlternativeName.toJSON(t.san)),t.oids?.length&&(e.oids=t.oids.map(r=>yg.ObjectIdentifierValuePair.toJSON(r))),e}};Vr.CertificateIdentities={fromJSON(t){return{identities:globalThis.Array.isArray(t?.identities)?t.identities.map(e=>Vr.CertificateIdentity.fromJSON(e)):[]}},toJSON(t){let e={};return t.identities?.length&&(e.identities=t.identities.map(r=>Vr.CertificateIdentity.toJSON(r))),e}};Vr.PublicKeyIdentities={fromJSON(t){return{publicKeys:globalThis.Array.isArray(t?.publicKeys)?t.publicKeys.map(e=>yg.PublicKey.fromJSON(e)):[]}},toJSON(t){let e={};return t.publicKeys?.length&&(e.publicKeys=t.publicKeys.map(r=>yg.PublicKey.toJSON(r))),e}};Vr.ArtifactVerificationOptions={fromJSON(t){return{signers:gi(t.certificateIdentities)?{$case:"certificateIdentities",certificateIdentities:Vr.CertificateIdentities.fromJSON(t.certificateIdentities)}:gi(t.publicKeys)?{$case:"publicKeys",publicKeys:Vr.PublicKeyIdentities.fromJSON(t.publicKeys)}:void 0,tlogOptions:gi(t.tlogOptions)?Vr.ArtifactVerificationOptions_TlogOptions.fromJSON(t.tlogOptions):void 0,ctlogOptions:gi(t.ctlogOptions)?Vr.ArtifactVerificationOptions_CtlogOptions.fromJSON(t.ctlogOptions):void 0,tsaOptions:gi(t.tsaOptions)?Vr.ArtifactVerificationOptions_TimestampAuthorityOptions.fromJSON(t.tsaOptions):void 0,integratedTsOptions:gi(t.integratedTsOptions)?Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions.fromJSON(t.integratedTsOptions):void 0,observerOptions:gi(t.observerOptions)?Vr.ArtifactVerificationOptions_ObserverTimestampOptions.fromJSON(t.observerOptions):void 0}},toJSON(t){let e={};return t.signers?.$case==="certificateIdentities"?e.certificateIdentities=Vr.CertificateIdentities.toJSON(t.signers.certificateIdentities):t.signers?.$case==="publicKeys"&&(e.publicKeys=Vr.PublicKeyIdentities.toJSON(t.signers.publicKeys)),t.tlogOptions!==void 0&&(e.tlogOptions=Vr.ArtifactVerificationOptions_TlogOptions.toJSON(t.tlogOptions)),t.ctlogOptions!==void 0&&(e.ctlogOptions=Vr.ArtifactVerificationOptions_CtlogOptions.toJSON(t.ctlogOptions)),t.tsaOptions!==void 0&&(e.tsaOptions=Vr.ArtifactVerificationOptions_TimestampAuthorityOptions.toJSON(t.tsaOptions)),t.integratedTsOptions!==void 0&&(e.integratedTsOptions=Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions.toJSON(t.integratedTsOptions)),t.observerOptions!==void 0&&(e.observerOptions=Vr.ArtifactVerificationOptions_ObserverTimestampOptions.toJSON(t.observerOptions)),e}};Vr.ArtifactVerificationOptions_TlogOptions={fromJSON(t){return{threshold:gi(t.threshold)?globalThis.Number(t.threshold):0,performOnlineVerification:gi(t.performOnlineVerification)?globalThis.Boolean(t.performOnlineVerification):!1,disable:gi(t.disable)?globalThis.Boolean(t.disable):!1}},toJSON(t){let e={};return t.threshold!==0&&(e.threshold=Math.round(t.threshold)),t.performOnlineVerification!==!1&&(e.performOnlineVerification=t.performOnlineVerification),t.disable!==!1&&(e.disable=t.disable),e}};Vr.ArtifactVerificationOptions_CtlogOptions={fromJSON(t){return{threshold:gi(t.threshold)?globalThis.Number(t.threshold):0,disable:gi(t.disable)?globalThis.Boolean(t.disable):!1}},toJSON(t){let e={};return t.threshold!==0&&(e.threshold=Math.round(t.threshold)),t.disable!==!1&&(e.disable=t.disable),e}};Vr.ArtifactVerificationOptions_TimestampAuthorityOptions={fromJSON(t){return{threshold:gi(t.threshold)?globalThis.Number(t.threshold):0,disable:gi(t.disable)?globalThis.Boolean(t.disable):!1}},toJSON(t){let e={};return t.threshold!==0&&(e.threshold=Math.round(t.threshold)),t.disable!==!1&&(e.disable=t.disable),e}};Vr.ArtifactVerificationOptions_TlogIntegratedTimestampOptions={fromJSON(t){return{threshold:gi(t.threshold)?globalThis.Number(t.threshold):0,disable:gi(t.disable)?globalThis.Boolean(t.disable):!1}},toJSON(t){let e={};return t.threshold!==0&&(e.threshold=Math.round(t.threshold)),t.disable!==!1&&(e.disable=t.disable),e}};Vr.ArtifactVerificationOptions_ObserverTimestampOptions={fromJSON(t){return{threshold:gi(t.threshold)?globalThis.Number(t.threshold):0,disable:gi(t.disable)?globalThis.Boolean(t.disable):!1}},toJSON(t){let e={};return t.threshold!==0&&(e.threshold=Math.round(t.threshold)),t.disable!==!1&&(e.disable=t.disable),e}};Vr.Artifact={fromJSON(t){return{data:gi(t.artifactUri)?{$case:"artifactUri",artifactUri:globalThis.String(t.artifactUri)}:gi(t.artifact)?{$case:"artifact",artifact:Buffer.from(uOt(t.artifact))}:gi(t.artifactDigest)?{$case:"artifactDigest",artifactDigest:yg.HashOutput.fromJSON(t.artifactDigest)}:void 0}},toJSON(t){let e={};return t.data?.$case==="artifactUri"?e.artifactUri=t.data.artifactUri:t.data?.$case==="artifact"?e.artifact=fOt(t.data.artifact):t.data?.$case==="artifactDigest"&&(e.artifactDigest=yg.HashOutput.toJSON(t.data.artifactDigest)),e}};Vr.Input={fromJSON(t){return{artifactTrustRoot:gi(t.artifactTrustRoot)?lTe.TrustedRoot.fromJSON(t.artifactTrustRoot):void 0,artifactVerificationOptions:gi(t.artifactVerificationOptions)?Vr.ArtifactVerificationOptions.fromJSON(t.artifactVerificationOptions):void 0,bundle:gi(t.bundle)?aTe.Bundle.fromJSON(t.bundle):void 0,artifact:gi(t.artifact)?Vr.Artifact.fromJSON(t.artifact):void 0}},toJSON(t){let e={};return t.artifactTrustRoot!==void 0&&(e.artifactTrustRoot=lTe.TrustedRoot.toJSON(t.artifactTrustRoot)),t.artifactVerificationOptions!==void 0&&(e.artifactVerificationOptions=Vr.ArtifactVerificationOptions.toJSON(t.artifactVerificationOptions)),t.bundle!==void 0&&(e.bundle=aTe.Bundle.toJSON(t.bundle)),t.artifact!==void 0&&(e.artifact=Vr.Artifact.toJSON(t.artifact)),e}};function uOt(t){return Uint8Array.from(globalThis.Buffer.from(t,"base64"))}function fOt(t){return globalThis.Buffer.from(t).toString("base64")}function gi(t){return t!=null}});var bb=L(eu=>{"use strict";var AOt=eu&&eu.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(e,r);(!a||("get"in a?!e.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,s,a)}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),Ww=eu&&eu.__exportStar||function(t,e){for(var r in t)r!=="default"&&!Object.prototype.hasOwnProperty.call(e,r)&&AOt(e,t,r)};Object.defineProperty(eu,"__esModule",{value:!0});Ww(A7(),eu);Ww(h7(),eu);Ww(Gw(),eu);Ww(p7(),eu);Ww(g7(),eu);Ww(cTe(),eu)});var KN=L(Cl=>{"use strict";Object.defineProperty(Cl,"__esModule",{value:!0});Cl.BUNDLE_V03_MEDIA_TYPE=Cl.BUNDLE_V03_LEGACY_MEDIA_TYPE=Cl.BUNDLE_V02_MEDIA_TYPE=Cl.BUNDLE_V01_MEDIA_TYPE=void 0;Cl.isBundleWithCertificateChain=pOt;Cl.isBundleWithPublicKey=hOt;Cl.isBundleWithMessageSignature=gOt;Cl.isBundleWithDsseEnvelope=dOt;Cl.BUNDLE_V01_MEDIA_TYPE="application/vnd.dev.sigstore.bundle+json;version=0.1";Cl.BUNDLE_V02_MEDIA_TYPE="application/vnd.dev.sigstore.bundle+json;version=0.2";Cl.BUNDLE_V03_LEGACY_MEDIA_TYPE="application/vnd.dev.sigstore.bundle+json;version=0.3";Cl.BUNDLE_V03_MEDIA_TYPE="application/vnd.dev.sigstore.bundle.v0.3+json";function pOt(t){return t.verificationMaterial.content.$case==="x509CertificateChain"}function hOt(t){return t.verificationMaterial.content.$case==="publicKey"}function gOt(t){return t.content.$case==="messageSignature"}function dOt(t){return t.content.$case==="dsseEnvelope"}});var fTe=L(zN=>{"use strict";Object.defineProperty(zN,"__esModule",{value:!0});zN.toMessageSignatureBundle=yOt;zN.toDSSEBundle=EOt;var mOt=bb(),JN=KN();function yOt(t){return{mediaType:t.certificateChain?JN.BUNDLE_V02_MEDIA_TYPE:JN.BUNDLE_V03_MEDIA_TYPE,content:{$case:"messageSignature",messageSignature:{messageDigest:{algorithm:mOt.HashAlgorithm.SHA2_256,digest:t.digest},signature:t.signature}},verificationMaterial:uTe(t)}}function EOt(t){return{mediaType:t.certificateChain?JN.BUNDLE_V02_MEDIA_TYPE:JN.BUNDLE_V03_MEDIA_TYPE,content:{$case:"dsseEnvelope",dsseEnvelope:IOt(t)},verificationMaterial:uTe(t)}}function IOt(t){return{payloadType:t.artifactType,payload:t.artifact,signatures:[COt(t)]}}function COt(t){return{keyid:t.keyHint||"",sig:t.signature}}function uTe(t){return{content:wOt(t),tlogEntries:[],timestampVerificationData:{rfc3161Timestamps:[]}}}function wOt(t){return t.certificate?t.certificateChain?{$case:"x509CertificateChain",x509CertificateChain:{certificates:[{rawBytes:t.certificate}]}}:{$case:"certificate",certificate:{rawBytes:t.certificate}}:{$case:"publicKey",publicKey:{hint:t.keyHint||""}}}});var m7=L(ZN=>{"use strict";Object.defineProperty(ZN,"__esModule",{value:!0});ZN.ValidationError=void 0;var d7=class extends Error{constructor(e,r){super(e),this.fields=r}};ZN.ValidationError=d7});var y7=L($m=>{"use strict";Object.defineProperty($m,"__esModule",{value:!0});$m.assertBundle=BOt;$m.assertBundleV01=ATe;$m.isBundleV01=vOt;$m.assertBundleV02=SOt;$m.assertBundleLatest=DOt;var XN=m7();function BOt(t){let e=$N(t);if(e.length>0)throw new XN.ValidationError("invalid bundle",e)}function ATe(t){let e=[];if(e.push(...$N(t)),e.push(...bOt(t)),e.length>0)throw new XN.ValidationError("invalid v0.1 bundle",e)}function vOt(t){try{return ATe(t),!0}catch{return!1}}function SOt(t){let e=[];if(e.push(...$N(t)),e.push(...pTe(t)),e.length>0)throw new XN.ValidationError("invalid v0.2 bundle",e)}function DOt(t){let e=[];if(e.push(...$N(t)),e.push(...pTe(t)),e.push(...POt(t)),e.length>0)throw new XN.ValidationError("invalid bundle",e)}function $N(t){let e=[];if((t.mediaType===void 0||!t.mediaType.match(/^application\/vnd\.dev\.sigstore\.bundle\+json;version=\d\.\d/)&&!t.mediaType.match(/^application\/vnd\.dev\.sigstore\.bundle\.v\d\.\d\+json/))&&e.push("mediaType"),t.content===void 0)e.push("content");else switch(t.content.$case){case"messageSignature":t.content.messageSignature.messageDigest===void 0?e.push("content.messageSignature.messageDigest"):t.content.messageSignature.messageDigest.digest.length===0&&e.push("content.messageSignature.messageDigest.digest"),t.content.messageSignature.signature.length===0&&e.push("content.messageSignature.signature");break;case"dsseEnvelope":t.content.dsseEnvelope.payload.length===0&&e.push("content.dsseEnvelope.payload"),t.content.dsseEnvelope.signatures.length!==1?e.push("content.dsseEnvelope.signatures"):t.content.dsseEnvelope.signatures[0].sig.length===0&&e.push("content.dsseEnvelope.signatures[0].sig");break}if(t.verificationMaterial===void 0)e.push("verificationMaterial");else{if(t.verificationMaterial.content===void 0)e.push("verificationMaterial.content");else switch(t.verificationMaterial.content.$case){case"x509CertificateChain":t.verificationMaterial.content.x509CertificateChain.certificates.length===0&&e.push("verificationMaterial.content.x509CertificateChain.certificates"),t.verificationMaterial.content.x509CertificateChain.certificates.forEach((r,s)=>{r.rawBytes.length===0&&e.push(`verificationMaterial.content.x509CertificateChain.certificates[${s}].rawBytes`)});break;case"certificate":t.verificationMaterial.content.certificate.rawBytes.length===0&&e.push("verificationMaterial.content.certificate.rawBytes");break}t.verificationMaterial.tlogEntries===void 0?e.push("verificationMaterial.tlogEntries"):t.verificationMaterial.tlogEntries.length>0&&t.verificationMaterial.tlogEntries.forEach((r,s)=>{r.logId===void 0&&e.push(`verificationMaterial.tlogEntries[${s}].logId`),r.kindVersion===void 0&&e.push(`verificationMaterial.tlogEntries[${s}].kindVersion`)})}return e}function bOt(t){let e=[];return t.verificationMaterial&&t.verificationMaterial.tlogEntries?.length>0&&t.verificationMaterial.tlogEntries.forEach((r,s)=>{r.inclusionPromise===void 0&&e.push(`verificationMaterial.tlogEntries[${s}].inclusionPromise`)}),e}function pTe(t){let e=[];return t.verificationMaterial&&t.verificationMaterial.tlogEntries?.length>0&&t.verificationMaterial.tlogEntries.forEach((r,s)=>{r.inclusionProof===void 0?e.push(`verificationMaterial.tlogEntries[${s}].inclusionProof`):r.inclusionProof.checkpoint===void 0&&e.push(`verificationMaterial.tlogEntries[${s}].inclusionProof.checkpoint`)}),e}function POt(t){let e=[];return t.verificationMaterial?.content?.$case==="x509CertificateChain"&&e.push("verificationMaterial.content.$case"),e}});var gTe=L(SA=>{"use strict";Object.defineProperty(SA,"__esModule",{value:!0});SA.envelopeToJSON=SA.envelopeFromJSON=SA.bundleToJSON=SA.bundleFromJSON=void 0;var eO=bb(),hTe=KN(),E7=y7(),xOt=t=>{let e=eO.Bundle.fromJSON(t);switch(e.mediaType){case hTe.BUNDLE_V01_MEDIA_TYPE:(0,E7.assertBundleV01)(e);break;case hTe.BUNDLE_V02_MEDIA_TYPE:(0,E7.assertBundleV02)(e);break;default:(0,E7.assertBundleLatest)(e);break}return e};SA.bundleFromJSON=xOt;var kOt=t=>eO.Bundle.toJSON(t);SA.bundleToJSON=kOt;var QOt=t=>eO.Envelope.fromJSON(t);SA.envelopeFromJSON=QOt;var TOt=t=>eO.Envelope.toJSON(t);SA.envelopeToJSON=TOt});var xb=L(Zr=>{"use strict";Object.defineProperty(Zr,"__esModule",{value:!0});Zr.isBundleV01=Zr.assertBundleV02=Zr.assertBundleV01=Zr.assertBundleLatest=Zr.assertBundle=Zr.envelopeToJSON=Zr.envelopeFromJSON=Zr.bundleToJSON=Zr.bundleFromJSON=Zr.ValidationError=Zr.isBundleWithPublicKey=Zr.isBundleWithMessageSignature=Zr.isBundleWithDsseEnvelope=Zr.isBundleWithCertificateChain=Zr.BUNDLE_V03_MEDIA_TYPE=Zr.BUNDLE_V03_LEGACY_MEDIA_TYPE=Zr.BUNDLE_V02_MEDIA_TYPE=Zr.BUNDLE_V01_MEDIA_TYPE=Zr.toMessageSignatureBundle=Zr.toDSSEBundle=void 0;var dTe=fTe();Object.defineProperty(Zr,"toDSSEBundle",{enumerable:!0,get:function(){return dTe.toDSSEBundle}});Object.defineProperty(Zr,"toMessageSignatureBundle",{enumerable:!0,get:function(){return dTe.toMessageSignatureBundle}});var Eg=KN();Object.defineProperty(Zr,"BUNDLE_V01_MEDIA_TYPE",{enumerable:!0,get:function(){return Eg.BUNDLE_V01_MEDIA_TYPE}});Object.defineProperty(Zr,"BUNDLE_V02_MEDIA_TYPE",{enumerable:!0,get:function(){return Eg.BUNDLE_V02_MEDIA_TYPE}});Object.defineProperty(Zr,"BUNDLE_V03_LEGACY_MEDIA_TYPE",{enumerable:!0,get:function(){return Eg.BUNDLE_V03_LEGACY_MEDIA_TYPE}});Object.defineProperty(Zr,"BUNDLE_V03_MEDIA_TYPE",{enumerable:!0,get:function(){return Eg.BUNDLE_V03_MEDIA_TYPE}});Object.defineProperty(Zr,"isBundleWithCertificateChain",{enumerable:!0,get:function(){return Eg.isBundleWithCertificateChain}});Object.defineProperty(Zr,"isBundleWithDsseEnvelope",{enumerable:!0,get:function(){return Eg.isBundleWithDsseEnvelope}});Object.defineProperty(Zr,"isBundleWithMessageSignature",{enumerable:!0,get:function(){return Eg.isBundleWithMessageSignature}});Object.defineProperty(Zr,"isBundleWithPublicKey",{enumerable:!0,get:function(){return Eg.isBundleWithPublicKey}});var ROt=m7();Object.defineProperty(Zr,"ValidationError",{enumerable:!0,get:function(){return ROt.ValidationError}});var tO=gTe();Object.defineProperty(Zr,"bundleFromJSON",{enumerable:!0,get:function(){return tO.bundleFromJSON}});Object.defineProperty(Zr,"bundleToJSON",{enumerable:!0,get:function(){return tO.bundleToJSON}});Object.defineProperty(Zr,"envelopeFromJSON",{enumerable:!0,get:function(){return tO.envelopeFromJSON}});Object.defineProperty(Zr,"envelopeToJSON",{enumerable:!0,get:function(){return tO.envelopeToJSON}});var Pb=y7();Object.defineProperty(Zr,"assertBundle",{enumerable:!0,get:function(){return Pb.assertBundle}});Object.defineProperty(Zr,"assertBundleLatest",{enumerable:!0,get:function(){return Pb.assertBundleLatest}});Object.defineProperty(Zr,"assertBundleV01",{enumerable:!0,get:function(){return Pb.assertBundleV01}});Object.defineProperty(Zr,"assertBundleV02",{enumerable:!0,get:function(){return Pb.assertBundleV02}});Object.defineProperty(Zr,"isBundleV01",{enumerable:!0,get:function(){return Pb.isBundleV01}})});var kb=L(nO=>{"use strict";Object.defineProperty(nO,"__esModule",{value:!0});nO.ByteStream=void 0;var I7=class extends Error{},rO=class t{constructor(e){this.start=0,e?(this.buf=e,this.view=Buffer.from(e)):(this.buf=new ArrayBuffer(0),this.view=Buffer.from(this.buf))}get buffer(){return this.view.subarray(0,this.start)}get length(){return this.view.byteLength}get position(){return this.start}seek(e){this.start=e}slice(e,r){let s=e+r;if(s>this.length)throw new I7("request past end of buffer");return this.view.subarray(e,s)}appendChar(e){this.ensureCapacity(1),this.view[this.start]=e,this.start+=1}appendUint16(e){this.ensureCapacity(2);let r=new Uint16Array([e]),s=new Uint8Array(r.buffer);this.view[this.start]=s[1],this.view[this.start+1]=s[0],this.start+=2}appendUint24(e){this.ensureCapacity(3);let r=new Uint32Array([e]),s=new Uint8Array(r.buffer);this.view[this.start]=s[2],this.view[this.start+1]=s[1],this.view[this.start+2]=s[0],this.start+=3}appendView(e){this.ensureCapacity(e.length),this.view.set(e,this.start),this.start+=e.length}getBlock(e){if(e<=0)return Buffer.alloc(0);if(this.start+e>this.view.length)throw new Error("request past end of buffer");let r=this.view.subarray(this.start,this.start+e);return this.start+=e,r}getUint8(){return this.getBlock(1)[0]}getUint16(){let e=this.getBlock(2);return e[0]<<8|e[1]}ensureCapacity(e){if(this.start+e>this.view.byteLength){let r=t.BLOCK_SIZE+(e>t.BLOCK_SIZE?e:0);this.realloc(this.view.byteLength+r)}}realloc(e){let r=new ArrayBuffer(e),s=Buffer.from(r);s.set(this.view),this.buf=r,this.view=s}};nO.ByteStream=rO;rO.BLOCK_SIZE=1024});var iO=L(Yw=>{"use strict";Object.defineProperty(Yw,"__esModule",{value:!0});Yw.ASN1TypeError=Yw.ASN1ParseError=void 0;var C7=class extends Error{};Yw.ASN1ParseError=C7;var w7=class extends Error{};Yw.ASN1TypeError=w7});var yTe=L(sO=>{"use strict";Object.defineProperty(sO,"__esModule",{value:!0});sO.decodeLength=FOt;sO.encodeLength=NOt;var mTe=iO();function FOt(t){let e=t.getUint8();if(!(e&128))return e;let r=e&127;if(r>6)throw new mTe.ASN1ParseError("length exceeds 6 byte limit");let s=0;for(let a=0;a0n;)r.unshift(Number(e&255n)),e=e>>8n;return Buffer.from([128|r.length,...r])}});var ITe=L(Ig=>{"use strict";Object.defineProperty(Ig,"__esModule",{value:!0});Ig.parseInteger=MOt;Ig.parseStringASCII=ETe;Ig.parseTime=_Ot;Ig.parseOID=UOt;Ig.parseBoolean=HOt;Ig.parseBitString=jOt;var OOt=/^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\.\d{3})?Z$/,LOt=/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\.\d{3})?Z$/;function MOt(t){let e=0,r=t.length,s=t[e],a=s>127,n=a?255:0;for(;s==n&&++e=50?1900:2e3,s[1]=a.toString()}return new Date(`${s[1]}-${s[2]}-${s[3]}T${s[4]}:${s[5]}:${s[6]}Z`)}function UOt(t){let e=0,r=t.length,s=t[e++],a=Math.floor(s/40),n=s%40,c=`${a}.${n}`,f=0;for(;e=f;--p)a.push(c>>p&1)}return a}});var wTe=L(oO=>{"use strict";Object.defineProperty(oO,"__esModule",{value:!0});oO.ASN1Tag=void 0;var CTe=iO(),ey={BOOLEAN:1,INTEGER:2,BIT_STRING:3,OCTET_STRING:4,OBJECT_IDENTIFIER:6,SEQUENCE:16,SET:17,PRINTABLE_STRING:19,UTC_TIME:23,GENERALIZED_TIME:24},B7={UNIVERSAL:0,APPLICATION:1,CONTEXT_SPECIFIC:2,PRIVATE:3},v7=class{constructor(e){if(this.number=e&31,this.constructed=(e&32)===32,this.class=e>>6,this.number===31)throw new CTe.ASN1ParseError("long form tags not supported");if(this.class===B7.UNIVERSAL&&this.number===0)throw new CTe.ASN1ParseError("unsupported tag 0x00")}isUniversal(){return this.class===B7.UNIVERSAL}isContextSpecific(e){let r=this.class===B7.CONTEXT_SPECIFIC;return e!==void 0?r&&this.number===e:r}isBoolean(){return this.isUniversal()&&this.number===ey.BOOLEAN}isInteger(){return this.isUniversal()&&this.number===ey.INTEGER}isBitString(){return this.isUniversal()&&this.number===ey.BIT_STRING}isOctetString(){return this.isUniversal()&&this.number===ey.OCTET_STRING}isOID(){return this.isUniversal()&&this.number===ey.OBJECT_IDENTIFIER}isUTCTime(){return this.isUniversal()&&this.number===ey.UTC_TIME}isGeneralizedTime(){return this.isUniversal()&&this.number===ey.GENERALIZED_TIME}toDER(){return this.number|(this.constructed?32:0)|this.class<<6}};oO.ASN1Tag=v7});var DTe=L(lO=>{"use strict";Object.defineProperty(lO,"__esModule",{value:!0});lO.ASN1Obj=void 0;var S7=kb(),ty=iO(),vTe=yTe(),Vw=ITe(),qOt=wTe(),aO=class{constructor(e,r,s){this.tag=e,this.value=r,this.subs=s}static parseBuffer(e){return STe(new S7.ByteStream(e))}toDER(){let e=new S7.ByteStream;if(this.subs.length>0)for(let a of this.subs)e.appendView(a.toDER());else e.appendView(this.value);let r=e.buffer,s=new S7.ByteStream;return s.appendChar(this.tag.toDER()),s.appendView((0,vTe.encodeLength)(r.length)),s.appendView(r),s.buffer}toBoolean(){if(!this.tag.isBoolean())throw new ty.ASN1TypeError("not a boolean");return(0,Vw.parseBoolean)(this.value)}toInteger(){if(!this.tag.isInteger())throw new ty.ASN1TypeError("not an integer");return(0,Vw.parseInteger)(this.value)}toOID(){if(!this.tag.isOID())throw new ty.ASN1TypeError("not an OID");return(0,Vw.parseOID)(this.value)}toDate(){switch(!0){case this.tag.isUTCTime():return(0,Vw.parseTime)(this.value,!0);case this.tag.isGeneralizedTime():return(0,Vw.parseTime)(this.value,!1);default:throw new ty.ASN1TypeError("not a date")}}toBitString(){if(!this.tag.isBitString())throw new ty.ASN1TypeError("not a bit string");return(0,Vw.parseBitString)(this.value)}};lO.ASN1Obj=aO;function STe(t){let e=new qOt.ASN1Tag(t.getUint8()),r=(0,vTe.decodeLength)(t),s=t.slice(t.position,r),a=t.position,n=[];if(e.constructed)n=BTe(t,r);else if(e.isOctetString())try{n=BTe(t,r)}catch{}return n.length===0&&t.seek(a+r),new aO(e,s,n)}function BTe(t,e){let r=t.position+e;if(r>t.length)throw new ty.ASN1ParseError("invalid length");let s=[];for(;t.position{"use strict";Object.defineProperty(cO,"__esModule",{value:!0});cO.ASN1Obj=void 0;var GOt=DTe();Object.defineProperty(cO,"ASN1Obj",{enumerable:!0,get:function(){return GOt.ASN1Obj}})});var Kw=L(Cg=>{"use strict";var WOt=Cg&&Cg.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(Cg,"__esModule",{value:!0});Cg.createPublicKey=YOt;Cg.digest=VOt;Cg.verify=KOt;Cg.bufferEqual=JOt;var Qb=WOt(Ie("crypto"));function YOt(t,e="spki"){return typeof t=="string"?Qb.default.createPublicKey(t):Qb.default.createPublicKey({key:t,format:"der",type:e})}function VOt(t,...e){let r=Qb.default.createHash(t);for(let s of e)r.update(s);return r.digest()}function KOt(t,e,r,s){try{return Qb.default.verify(s,t,e,r)}catch{return!1}}function JOt(t,e){try{return Qb.default.timingSafeEqual(t,e)}catch{return!1}}});var bTe=L(D7=>{"use strict";Object.defineProperty(D7,"__esModule",{value:!0});D7.preAuthEncoding=ZOt;var zOt="DSSEv1";function ZOt(t,e){let r=[zOt,t.length,t,e.length,""].join(" ");return Buffer.concat([Buffer.from(r,"ascii"),e])}});var kTe=L(fO=>{"use strict";Object.defineProperty(fO,"__esModule",{value:!0});fO.base64Encode=XOt;fO.base64Decode=$Ot;var PTe="base64",xTe="utf-8";function XOt(t){return Buffer.from(t,xTe).toString(PTe)}function $Ot(t){return Buffer.from(t,PTe).toString(xTe)}});var QTe=L(P7=>{"use strict";Object.defineProperty(P7,"__esModule",{value:!0});P7.canonicalize=b7;function b7(t){let e="";if(t===null||typeof t!="object"||t.toJSON!=null)e+=JSON.stringify(t);else if(Array.isArray(t)){e+="[";let r=!0;t.forEach(s=>{r||(e+=","),r=!1,e+=b7(s)}),e+="]"}else{e+="{";let r=!0;Object.keys(t).sort().forEach(s=>{r||(e+=","),r=!1,e+=JSON.stringify(s),e+=":",e+=b7(t[s])}),e+="}"}return e}});var x7=L(AO=>{"use strict";Object.defineProperty(AO,"__esModule",{value:!0});AO.toDER=rLt;AO.fromDER=nLt;var eLt=/-----BEGIN (.*)-----/,tLt=/-----END (.*)-----/;function rLt(t){let e="";return t.split(` `).forEach(r=>{r.match(eLt)||r.match(tLt)||(e+=r)}),Buffer.from(e,"base64")}function nLt(t,e="CERTIFICATE"){let s=t.toString("base64").match(/.{1,64}/g)||"";return[`-----BEGIN ${e}-----`,...s,`-----END ${e}-----`].join(` `).concat(` `)}});var pO=L(Jw=>{"use strict";Object.defineProperty(Jw,"__esModule",{value:!0});Jw.SHA2_HASH_ALGOS=Jw.ECDSA_SIGNATURE_ALGOS=void 0;Jw.ECDSA_SIGNATURE_ALGOS={"1.2.840.10045.4.3.1":"sha224","1.2.840.10045.4.3.2":"sha256","1.2.840.10045.4.3.3":"sha384","1.2.840.10045.4.3.4":"sha512"};Jw.SHA2_HASH_ALGOS={"2.16.840.1.101.3.4.2.1":"sha256","2.16.840.1.101.3.4.2.2":"sha384","2.16.840.1.101.3.4.2.3":"sha512"}});var Q7=L(hO=>{"use strict";Object.defineProperty(hO,"__esModule",{value:!0});hO.RFC3161TimestampVerificationError=void 0;var k7=class extends Error{};hO.RFC3161TimestampVerificationError=k7});var RTe=L(DA=>{"use strict";var iLt=DA&&DA.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(e,r);(!a||("get"in a?!e.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,s,a)}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),sLt=DA&&DA.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),oLt=DA&&DA.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.prototype.hasOwnProperty.call(t,r)&&iLt(e,t,r);return sLt(e,t),e};Object.defineProperty(DA,"__esModule",{value:!0});DA.TSTInfo=void 0;var TTe=oLt(Kw()),aLt=pO(),lLt=Q7(),T7=class{constructor(e){this.root=e}get version(){return this.root.subs[0].toInteger()}get genTime(){return this.root.subs[4].toDate()}get messageImprintHashAlgorithm(){let e=this.messageImprintObj.subs[0].subs[0].toOID();return aLt.SHA2_HASH_ALGOS[e]}get messageImprintHashedMessage(){return this.messageImprintObj.subs[1].value}get raw(){return this.root.toDER()}verify(e){let r=TTe.digest(this.messageImprintHashAlgorithm,e);if(!TTe.bufferEqual(r,this.messageImprintHashedMessage))throw new lLt.RFC3161TimestampVerificationError("message imprint does not match artifact")}get messageImprintObj(){return this.root.subs[2]}};DA.TSTInfo=T7});var NTe=L(bA=>{"use strict";var cLt=bA&&bA.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(e,r);(!a||("get"in a?!e.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,s,a)}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),uLt=bA&&bA.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),fLt=bA&&bA.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.prototype.hasOwnProperty.call(t,r)&&cLt(e,t,r);return uLt(e,t),e};Object.defineProperty(bA,"__esModule",{value:!0});bA.RFC3161Timestamp=void 0;var ALt=uO(),R7=fLt(Kw()),FTe=pO(),Tb=Q7(),pLt=RTe(),hLt="1.2.840.113549.1.7.2",gLt="1.2.840.113549.1.9.16.1.4",dLt="1.2.840.113549.1.9.4",F7=class t{constructor(e){this.root=e}static parse(e){let r=ALt.ASN1Obj.parseBuffer(e);return new t(r)}get status(){return this.pkiStatusInfoObj.subs[0].toInteger()}get contentType(){return this.contentTypeObj.toOID()}get eContentType(){return this.eContentTypeObj.toOID()}get signingTime(){return this.tstInfo.genTime}get signerIssuer(){return this.signerSidObj.subs[0].value}get signerSerialNumber(){return this.signerSidObj.subs[1].value}get signerDigestAlgorithm(){let e=this.signerDigestAlgorithmObj.subs[0].toOID();return FTe.SHA2_HASH_ALGOS[e]}get signatureAlgorithm(){let e=this.signatureAlgorithmObj.subs[0].toOID();return FTe.ECDSA_SIGNATURE_ALGOS[e]}get signatureValue(){return this.signatureValueObj.value}get tstInfo(){return new pLt.TSTInfo(this.eContentObj.subs[0].subs[0])}verify(e,r){if(!this.timeStampTokenObj)throw new Tb.RFC3161TimestampVerificationError("timeStampToken is missing");if(this.contentType!==hLt)throw new Tb.RFC3161TimestampVerificationError(`incorrect content type: ${this.contentType}`);if(this.eContentType!==gLt)throw new Tb.RFC3161TimestampVerificationError(`incorrect encapsulated content type: ${this.eContentType}`);this.tstInfo.verify(e),this.verifyMessageDigest(),this.verifySignature(r)}verifyMessageDigest(){let e=R7.digest(this.signerDigestAlgorithm,this.tstInfo.raw),r=this.messageDigestAttributeObj.subs[1].subs[0].value;if(!R7.bufferEqual(e,r))throw new Tb.RFC3161TimestampVerificationError("signed data does not match tstInfo")}verifySignature(e){let r=this.signedAttrsObj.toDER();if(r[0]=49,!R7.verify(r,e,this.signatureValue,this.signatureAlgorithm))throw new Tb.RFC3161TimestampVerificationError("signature verification failed")}get pkiStatusInfoObj(){return this.root.subs[0]}get timeStampTokenObj(){return this.root.subs[1]}get contentTypeObj(){return this.timeStampTokenObj.subs[0]}get signedDataObj(){return this.timeStampTokenObj.subs.find(r=>r.tag.isContextSpecific(0)).subs[0]}get encapContentInfoObj(){return this.signedDataObj.subs[2]}get signerInfosObj(){let e=this.signedDataObj;return e.subs[e.subs.length-1]}get signerInfoObj(){return this.signerInfosObj.subs[0]}get eContentTypeObj(){return this.encapContentInfoObj.subs[0]}get eContentObj(){return this.encapContentInfoObj.subs[1]}get signedAttrsObj(){return this.signerInfoObj.subs.find(r=>r.tag.isContextSpecific(0))}get messageDigestAttributeObj(){return this.signedAttrsObj.subs.find(r=>r.subs[0].tag.isOID()&&r.subs[0].toOID()===dLt)}get signerSidObj(){return this.signerInfoObj.subs[1]}get signerDigestAlgorithmObj(){return this.signerInfoObj.subs[2]}get signatureAlgorithmObj(){return this.signerInfoObj.subs[4]}get signatureValueObj(){return this.signerInfoObj.subs[5]}};bA.RFC3161Timestamp=F7});var OTe=L(gO=>{"use strict";Object.defineProperty(gO,"__esModule",{value:!0});gO.RFC3161Timestamp=void 0;var mLt=NTe();Object.defineProperty(gO,"RFC3161Timestamp",{enumerable:!0,get:function(){return mLt.RFC3161Timestamp}})});var MTe=L(PA=>{"use strict";var yLt=PA&&PA.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(e,r);(!a||("get"in a?!e.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,s,a)}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),ELt=PA&&PA.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),ILt=PA&&PA.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.prototype.hasOwnProperty.call(t,r)&&yLt(e,t,r);return ELt(e,t),e};Object.defineProperty(PA,"__esModule",{value:!0});PA.SignedCertificateTimestamp=void 0;var CLt=ILt(Kw()),LTe=kb(),N7=class t{constructor(e){this.version=e.version,this.logID=e.logID,this.timestamp=e.timestamp,this.extensions=e.extensions,this.hashAlgorithm=e.hashAlgorithm,this.signatureAlgorithm=e.signatureAlgorithm,this.signature=e.signature}get datetime(){return new Date(Number(this.timestamp.readBigInt64BE()))}get algorithm(){switch(this.hashAlgorithm){case 0:return"none";case 1:return"md5";case 2:return"sha1";case 3:return"sha224";case 4:return"sha256";case 5:return"sha384";case 6:return"sha512";default:return"unknown"}}verify(e,r){let s=new LTe.ByteStream;return s.appendChar(this.version),s.appendChar(0),s.appendView(this.timestamp),s.appendUint16(1),s.appendView(e),s.appendUint16(this.extensions.byteLength),this.extensions.byteLength>0&&s.appendView(this.extensions),CLt.verify(s.buffer,r,this.signature,this.algorithm)}static parse(e){let r=new LTe.ByteStream(e),s=r.getUint8(),a=r.getBlock(32),n=r.getBlock(8),c=r.getUint16(),f=r.getBlock(c),p=r.getUint8(),h=r.getUint8(),E=r.getUint16(),C=r.getBlock(E);if(r.position!==e.length)throw new Error("SCT buffer length mismatch");return new t({version:s,logID:a,timestamp:n,extensions:f,hashAlgorithm:p,signatureAlgorithm:h,signature:C})}};PA.SignedCertificateTimestamp=N7});var j7=L(oa=>{"use strict";Object.defineProperty(oa,"__esModule",{value:!0});oa.X509SCTExtension=oa.X509SubjectKeyIDExtension=oa.X509AuthorityKeyIDExtension=oa.X509SubjectAlternativeNameExtension=oa.X509KeyUsageExtension=oa.X509BasicConstraintsExtension=oa.X509Extension=void 0;var wLt=kb(),BLt=MTe(),gh=class{constructor(e){this.root=e}get oid(){return this.root.subs[0].toOID()}get critical(){return this.root.subs.length===3?this.root.subs[1].toBoolean():!1}get value(){return this.extnValueObj.value}get valueObj(){return this.extnValueObj}get extnValueObj(){return this.root.subs[this.root.subs.length-1]}};oa.X509Extension=gh;var O7=class extends gh{get isCA(){return this.sequence.subs[0]?.toBoolean()??!1}get pathLenConstraint(){return this.sequence.subs.length>1?this.sequence.subs[1].toInteger():void 0}get sequence(){return this.extnValueObj.subs[0]}};oa.X509BasicConstraintsExtension=O7;var L7=class extends gh{get digitalSignature(){return this.bitString[0]===1}get keyCertSign(){return this.bitString[5]===1}get crlSign(){return this.bitString[6]===1}get bitString(){return this.extnValueObj.subs[0].toBitString()}};oa.X509KeyUsageExtension=L7;var M7=class extends gh{get rfc822Name(){return this.findGeneralName(1)?.value.toString("ascii")}get uri(){return this.findGeneralName(6)?.value.toString("ascii")}otherName(e){let r=this.findGeneralName(0);return r===void 0||r.subs[0].toOID()!==e?void 0:r.subs[1].subs[0].value.toString("ascii")}findGeneralName(e){return this.generalNames.find(r=>r.tag.isContextSpecific(e))}get generalNames(){return this.extnValueObj.subs[0].subs}};oa.X509SubjectAlternativeNameExtension=M7;var _7=class extends gh{get keyIdentifier(){return this.findSequenceMember(0)?.value}findSequenceMember(e){return this.sequence.subs.find(r=>r.tag.isContextSpecific(e))}get sequence(){return this.extnValueObj.subs[0]}};oa.X509AuthorityKeyIDExtension=_7;var U7=class extends gh{get keyIdentifier(){return this.extnValueObj.subs[0].value}};oa.X509SubjectKeyIDExtension=U7;var H7=class extends gh{constructor(e){super(e)}get signedCertificateTimestamps(){let e=this.extnValueObj.subs[0].value,r=new wLt.ByteStream(e),s=r.getUint16()+2,a=[];for(;r.position{"use strict";var vLt=sc&&sc.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(e,r);(!a||("get"in a?!e.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,s,a)}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),SLt=sc&&sc.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),UTe=sc&&sc.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.prototype.hasOwnProperty.call(t,r)&&vLt(e,t,r);return SLt(e,t),e};Object.defineProperty(sc,"__esModule",{value:!0});sc.X509Certificate=sc.EXTENSION_OID_SCT=void 0;var DLt=uO(),_Te=UTe(Kw()),bLt=pO(),PLt=UTe(x7()),ry=j7(),xLt="2.5.29.14",kLt="2.5.29.15",QLt="2.5.29.17",TLt="2.5.29.19",RLt="2.5.29.35";sc.EXTENSION_OID_SCT="1.3.6.1.4.1.11129.2.4.2";var q7=class t{constructor(e){this.root=e}static parse(e){let r=typeof e=="string"?PLt.toDER(e):e,s=DLt.ASN1Obj.parseBuffer(r);return new t(s)}get tbsCertificate(){return this.tbsCertificateObj}get version(){return`v${(this.versionObj.subs[0].toInteger()+BigInt(1)).toString()}`}get serialNumber(){return this.serialNumberObj.value}get notBefore(){return this.validityObj.subs[0].toDate()}get notAfter(){return this.validityObj.subs[1].toDate()}get issuer(){return this.issuerObj.value}get subject(){return this.subjectObj.value}get publicKey(){return this.subjectPublicKeyInfoObj.toDER()}get signatureAlgorithm(){let e=this.signatureAlgorithmObj.subs[0].toOID();return bLt.ECDSA_SIGNATURE_ALGOS[e]}get signatureValue(){return this.signatureValueObj.value.subarray(1)}get subjectAltName(){let e=this.extSubjectAltName;return e?.uri||e?.rfc822Name}get extensions(){return this.extensionsObj?.subs[0]?.subs||[]}get extKeyUsage(){let e=this.findExtension(kLt);return e?new ry.X509KeyUsageExtension(e):void 0}get extBasicConstraints(){let e=this.findExtension(TLt);return e?new ry.X509BasicConstraintsExtension(e):void 0}get extSubjectAltName(){let e=this.findExtension(QLt);return e?new ry.X509SubjectAlternativeNameExtension(e):void 0}get extAuthorityKeyID(){let e=this.findExtension(RLt);return e?new ry.X509AuthorityKeyIDExtension(e):void 0}get extSubjectKeyID(){let e=this.findExtension(xLt);return e?new ry.X509SubjectKeyIDExtension(e):void 0}get extSCT(){let e=this.findExtension(sc.EXTENSION_OID_SCT);return e?new ry.X509SCTExtension(e):void 0}get isCA(){let e=this.extBasicConstraints?.isCA||!1;return this.extKeyUsage?e&&this.extKeyUsage.keyCertSign:e}extension(e){let r=this.findExtension(e);return r?new ry.X509Extension(r):void 0}verify(e){let r=e?.publicKey||this.publicKey,s=_Te.createPublicKey(r);return _Te.verify(this.tbsCertificate.toDER(),s,this.signatureValue,this.signatureAlgorithm)}validForDate(e){return this.notBefore<=e&&e<=this.notAfter}equals(e){return this.root.toDER().equals(e.root.toDER())}clone(){let e=this.root.toDER(),r=Buffer.alloc(e.length);return e.copy(r),t.parse(r)}findExtension(e){return this.extensions.find(r=>r.subs[0].toOID()===e)}get tbsCertificateObj(){return this.root.subs[0]}get signatureAlgorithmObj(){return this.root.subs[1]}get signatureValueObj(){return this.root.subs[2]}get versionObj(){return this.tbsCertificateObj.subs[0]}get serialNumberObj(){return this.tbsCertificateObj.subs[1]}get issuerObj(){return this.tbsCertificateObj.subs[3]}get validityObj(){return this.tbsCertificateObj.subs[4]}get subjectObj(){return this.tbsCertificateObj.subs[5]}get subjectPublicKeyInfoObj(){return this.tbsCertificateObj.subs[6]}get extensionsObj(){return this.tbsCertificateObj.subs.find(e=>e.tag.isContextSpecific(3))}};sc.X509Certificate=q7});var qTe=L(wg=>{"use strict";Object.defineProperty(wg,"__esModule",{value:!0});wg.X509SCTExtension=wg.X509Certificate=wg.EXTENSION_OID_SCT=void 0;var jTe=HTe();Object.defineProperty(wg,"EXTENSION_OID_SCT",{enumerable:!0,get:function(){return jTe.EXTENSION_OID_SCT}});Object.defineProperty(wg,"X509Certificate",{enumerable:!0,get:function(){return jTe.X509Certificate}});var FLt=j7();Object.defineProperty(wg,"X509SCTExtension",{enumerable:!0,get:function(){return FLt.X509SCTExtension}})});var wl=L(Kn=>{"use strict";var NLt=Kn&&Kn.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(e,r);(!a||("get"in a?!e.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,s,a)}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),OLt=Kn&&Kn.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),Rb=Kn&&Kn.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.prototype.hasOwnProperty.call(t,r)&&NLt(e,t,r);return OLt(e,t),e};Object.defineProperty(Kn,"__esModule",{value:!0});Kn.X509SCTExtension=Kn.X509Certificate=Kn.EXTENSION_OID_SCT=Kn.ByteStream=Kn.RFC3161Timestamp=Kn.pem=Kn.json=Kn.encoding=Kn.dsse=Kn.crypto=Kn.ASN1Obj=void 0;var LLt=uO();Object.defineProperty(Kn,"ASN1Obj",{enumerable:!0,get:function(){return LLt.ASN1Obj}});Kn.crypto=Rb(Kw());Kn.dsse=Rb(bTe());Kn.encoding=Rb(kTe());Kn.json=Rb(QTe());Kn.pem=Rb(x7());var MLt=OTe();Object.defineProperty(Kn,"RFC3161Timestamp",{enumerable:!0,get:function(){return MLt.RFC3161Timestamp}});var _Lt=kb();Object.defineProperty(Kn,"ByteStream",{enumerable:!0,get:function(){return _Lt.ByteStream}});var G7=qTe();Object.defineProperty(Kn,"EXTENSION_OID_SCT",{enumerable:!0,get:function(){return G7.EXTENSION_OID_SCT}});Object.defineProperty(Kn,"X509Certificate",{enumerable:!0,get:function(){return G7.X509Certificate}});Object.defineProperty(Kn,"X509SCTExtension",{enumerable:!0,get:function(){return G7.X509SCTExtension}})});var GTe=L(W7=>{"use strict";Object.defineProperty(W7,"__esModule",{value:!0});W7.extractJWTSubject=HLt;var ULt=wl();function HLt(t){let e=t.split(".",3),r=JSON.parse(ULt.encoding.base64Decode(e[1]));switch(r.iss){case"https://accounts.google.com":case"https://oauth2.sigstore.dev/auth":return r.email;default:return r.sub}}});var WTe=L((NIr,jLt)=>{jLt.exports={name:"@sigstore/sign",version:"3.1.0",description:"Sigstore signing library",main:"dist/index.js",types:"dist/index.d.ts",scripts:{clean:"shx rm -rf dist *.tsbuildinfo",build:"tsc --build",test:"jest"},files:["dist"],author:"bdehamer@github.com",license:"Apache-2.0",repository:{type:"git",url:"git+https://github.com/sigstore/sigstore-js.git"},bugs:{url:"https://github.com/sigstore/sigstore-js/issues"},homepage:"https://github.com/sigstore/sigstore-js/tree/main/packages/sign#readme",publishConfig:{provenance:!0},devDependencies:{"@sigstore/jest":"^0.0.0","@sigstore/mock":"^0.10.0","@sigstore/rekor-types":"^3.0.0","@types/make-fetch-happen":"^10.0.4","@types/promise-retry":"^1.1.6"},dependencies:{"@sigstore/bundle":"^3.1.0","@sigstore/core":"^2.0.0","@sigstore/protobuf-specs":"^0.4.0","make-fetch-happen":"^14.0.2","proc-log":"^5.0.0","promise-retry":"^2.0.1"},engines:{node:"^18.17.0 || >=20.5.0"}}});var VTe=L(zw=>{"use strict";var qLt=zw&&zw.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(zw,"__esModule",{value:!0});zw.getUserAgent=void 0;var YTe=qLt(Ie("os")),GLt=()=>{let t=WTe().version,e=process.version,r=YTe.default.platform(),s=YTe.default.arch();return`sigstore-js/${t} (Node ${e}) (${r}/${s})`};zw.getUserAgent=GLt});var Bg=L(Ji=>{"use strict";var WLt=Ji&&Ji.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(e,r);(!a||("get"in a?!e.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,s,a)}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),YLt=Ji&&Ji.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),KTe=Ji&&Ji.__importStar||function(){var t=function(e){return t=Object.getOwnPropertyNames||function(r){var s=[];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(s[s.length]=a);return s},t(e)};return function(e){if(e&&e.__esModule)return e;var r={};if(e!=null)for(var s=t(e),a=0;a{"use strict";Object.defineProperty(dO,"__esModule",{value:!0});dO.BaseBundleBuilder=void 0;var Y7=class{constructor(e){this.signer=e.signer,this.witnesses=e.witnesses}async create(e){let r=await this.prepare(e).then(f=>this.signer.sign(f)),s=await this.package(e,r),a=await Promise.all(this.witnesses.map(f=>f.testify(s.content,VLt(r.key)))),n=[],c=[];return a.forEach(({tlogEntries:f,rfc3161Timestamps:p})=>{n.push(...f??[]),c.push(...p??[])}),s.verificationMaterial.tlogEntries=n,s.verificationMaterial.timestampVerificationData={rfc3161Timestamps:c},s}async prepare(e){return e.data}};dO.BaseBundleBuilder=Y7;function VLt(t){switch(t.$case){case"publicKey":return t.publicKey;case"x509Certificate":return t.certificate}}});var J7=L(xA=>{"use strict";var KLt=xA&&xA.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(e,r);(!a||("get"in a?!e.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,s,a)}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),JLt=xA&&xA.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),zLt=xA&&xA.__importStar||function(){var t=function(e){return t=Object.getOwnPropertyNames||function(r){var s=[];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(s[s.length]=a);return s},t(e)};return function(e){if(e&&e.__esModule)return e;var r={};if(e!=null)for(var s=t(e),a=0;a{"use strict";Object.defineProperty(mO,"__esModule",{value:!0});mO.DSSEBundleBuilder=void 0;var $Lt=Bg(),eMt=V7(),tMt=J7(),z7=class extends eMt.BaseBundleBuilder{constructor(e){super(e),this.certificateChain=e.certificateChain??!1}async prepare(e){let r=zTe(e);return $Lt.dsse.preAuthEncoding(r.type,r.data)}async package(e,r){return(0,tMt.toDSSEBundle)(zTe(e),r,this.certificateChain)}};mO.DSSEBundleBuilder=z7;function zTe(t){return{...t,type:t.type??""}}});var XTe=L(yO=>{"use strict";Object.defineProperty(yO,"__esModule",{value:!0});yO.MessageSignatureBundleBuilder=void 0;var rMt=V7(),nMt=J7(),Z7=class extends rMt.BaseBundleBuilder{constructor(e){super(e)}async package(e,r){return(0,nMt.toMessageSignatureBundle)(e,r)}};yO.MessageSignatureBundleBuilder=Z7});var $Te=L(Zw=>{"use strict";Object.defineProperty(Zw,"__esModule",{value:!0});Zw.MessageSignatureBundleBuilder=Zw.DSSEBundleBuilder=void 0;var iMt=ZTe();Object.defineProperty(Zw,"DSSEBundleBuilder",{enumerable:!0,get:function(){return iMt.DSSEBundleBuilder}});var sMt=XTe();Object.defineProperty(Zw,"MessageSignatureBundleBuilder",{enumerable:!0,get:function(){return sMt.MessageSignatureBundleBuilder}})});var IO=L(EO=>{"use strict";Object.defineProperty(EO,"__esModule",{value:!0});EO.HTTPError=void 0;var X7=class extends Error{constructor({status:e,message:r,location:s}){super(`(${e}) ${r}`),this.statusCode=e,this.location=s}};EO.HTTPError=X7});var Xw=L(Nb=>{"use strict";Object.defineProperty(Nb,"__esModule",{value:!0});Nb.InternalError=void 0;Nb.internalError=aMt;var oMt=IO(),CO=class extends Error{constructor({code:e,message:r,cause:s}){super(r),this.name=this.constructor.name,this.cause=s,this.code=e}};Nb.InternalError=CO;function aMt(t,e,r){throw t instanceof oMt.HTTPError&&(r+=` - ${t.message}`),new CO({code:e,message:r,cause:t})}});var wO=L((WIr,eRe)=>{eRe.exports=fetch});var tRe=L($w=>{"use strict";var lMt=$w&&$w.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty($w,"__esModule",{value:!0});$w.CIContextProvider=void 0;var cMt=lMt(wO()),uMt=[fMt,AMt],$7=class{constructor(e="sigstore"){this.audience=e}async getToken(){return Promise.any(uMt.map(e=>e(this.audience))).catch(()=>Promise.reject("CI: no tokens available"))}};$w.CIContextProvider=$7;async function fMt(t){if(!process.env.ACTIONS_ID_TOKEN_REQUEST_URL||!process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN)return Promise.reject("no token available");let e=new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);return e.searchParams.append("audience",t),(await(0,cMt.default)(e.href,{retry:2,headers:{Accept:"application/json",Authorization:`Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`}})).json().then(s=>s.value)}async function AMt(){return process.env.SIGSTORE_ID_TOKEN?process.env.SIGSTORE_ID_TOKEN:Promise.reject("no token available")}});var rRe=L(BO=>{"use strict";Object.defineProperty(BO,"__esModule",{value:!0});BO.CIContextProvider=void 0;var pMt=tRe();Object.defineProperty(BO,"CIContextProvider",{enumerable:!0,get:function(){return pMt.CIContextProvider}})});var iRe=L((KIr,nRe)=>{var hMt=Symbol("proc-log.meta");nRe.exports={META:hMt,output:{LEVELS:["standard","error","buffer","flush"],KEYS:{standard:"standard",error:"error",buffer:"buffer",flush:"flush"},standard:function(...t){return process.emit("output","standard",...t)},error:function(...t){return process.emit("output","error",...t)},buffer:function(...t){return process.emit("output","buffer",...t)},flush:function(...t){return process.emit("output","flush",...t)}},log:{LEVELS:["notice","error","warn","info","verbose","http","silly","timing","pause","resume"],KEYS:{notice:"notice",error:"error",warn:"warn",info:"info",verbose:"verbose",http:"http",silly:"silly",timing:"timing",pause:"pause",resume:"resume"},error:function(...t){return process.emit("log","error",...t)},notice:function(...t){return process.emit("log","notice",...t)},warn:function(...t){return process.emit("log","warn",...t)},info:function(...t){return process.emit("log","info",...t)},verbose:function(...t){return process.emit("log","verbose",...t)},http:function(...t){return process.emit("log","http",...t)},silly:function(...t){return process.emit("log","silly",...t)},timing:function(...t){return process.emit("log","timing",...t)},pause:function(){return process.emit("log","pause")},resume:function(){return process.emit("log","resume")}},time:{LEVELS:["start","end"],KEYS:{start:"start",end:"end"},start:function(t,e){process.emit("time","start",t);function r(){return process.emit("time","end",t)}if(typeof e=="function"){let s=e();return s&&s.finally?s.finally(r):(r(),s)}return r},end:function(t){return process.emit("time","end",t)}},input:{LEVELS:["start","end","read"],KEYS:{start:"start",end:"end",read:"read"},start:function(t){process.emit("input","start");function e(){return process.emit("input","end")}if(typeof t=="function"){let r=t();return r&&r.finally?r.finally(e):(e(),r)}return e},end:function(){return process.emit("input","end")},read:function(...t){let e,r,s=new Promise((a,n)=>{e=a,r=n});return process.emit("input","read",e,r,...t),s}}}});var aRe=L((JIr,oRe)=>{"use strict";function sRe(t,e){for(let r in e)Object.defineProperty(t,r,{value:e[r],enumerable:!0,configurable:!0});return t}function gMt(t,e,r){if(!t||typeof t=="string")throw new TypeError("Please pass an Error to err-code");r||(r={}),typeof e=="object"&&(r=e,e=void 0),e!=null&&(r.code=e);try{return sRe(t,r)}catch{r.message=t.message,r.stack=t.stack;let a=function(){};return a.prototype=Object.create(Object.getPrototypeOf(t)),sRe(new a,r)}}oRe.exports=gMt});var cRe=L((zIr,lRe)=>{function tu(t,e){typeof e=="boolean"&&(e={forever:e}),this._originalTimeouts=JSON.parse(JSON.stringify(t)),this._timeouts=t,this._options=e||{},this._maxRetryTime=e&&e.maxRetryTime||1/0,this._fn=null,this._errors=[],this._attempts=1,this._operationTimeout=null,this._operationTimeoutCb=null,this._timeout=null,this._operationStart=null,this._options.forever&&(this._cachedTimeouts=this._timeouts.slice(0))}lRe.exports=tu;tu.prototype.reset=function(){this._attempts=1,this._timeouts=this._originalTimeouts};tu.prototype.stop=function(){this._timeout&&clearTimeout(this._timeout),this._timeouts=[],this._cachedTimeouts=null};tu.prototype.retry=function(t){if(this._timeout&&clearTimeout(this._timeout),!t)return!1;var e=new Date().getTime();if(t&&e-this._operationStart>=this._maxRetryTime)return this._errors.unshift(new Error("RetryOperation timeout occurred")),!1;this._errors.push(t);var r=this._timeouts.shift();if(r===void 0)if(this._cachedTimeouts)this._errors.splice(this._errors.length-1,this._errors.length),this._timeouts=this._cachedTimeouts.slice(0),r=this._timeouts.shift();else return!1;var s=this,a=setTimeout(function(){s._attempts++,s._operationTimeoutCb&&(s._timeout=setTimeout(function(){s._operationTimeoutCb(s._attempts)},s._operationTimeout),s._options.unref&&s._timeout.unref()),s._fn(s._attempts)},r);return this._options.unref&&a.unref(),!0};tu.prototype.attempt=function(t,e){this._fn=t,e&&(e.timeout&&(this._operationTimeout=e.timeout),e.cb&&(this._operationTimeoutCb=e.cb));var r=this;this._operationTimeoutCb&&(this._timeout=setTimeout(function(){r._operationTimeoutCb()},r._operationTimeout)),this._operationStart=new Date().getTime(),this._fn(this._attempts)};tu.prototype.try=function(t){console.log("Using RetryOperation.try() is deprecated"),this.attempt(t)};tu.prototype.start=function(t){console.log("Using RetryOperation.start() is deprecated"),this.attempt(t)};tu.prototype.start=tu.prototype.try;tu.prototype.errors=function(){return this._errors};tu.prototype.attempts=function(){return this._attempts};tu.prototype.mainError=function(){if(this._errors.length===0)return null;for(var t={},e=null,r=0,s=0;s=r&&(e=a,r=c)}return e}});var uRe=L(ny=>{var dMt=cRe();ny.operation=function(t){var e=ny.timeouts(t);return new dMt(e,{forever:t&&t.forever,unref:t&&t.unref,maxRetryTime:t&&t.maxRetryTime})};ny.timeouts=function(t){if(t instanceof Array)return[].concat(t);var e={retries:10,factor:2,minTimeout:1*1e3,maxTimeout:1/0,randomize:!1};for(var r in t)e[r]=t[r];if(e.minTimeout>e.maxTimeout)throw new Error("minTimeout is greater than maxTimeout");for(var s=[],a=0;a{fRe.exports=uRe()});var gRe=L(($Ir,hRe)=>{"use strict";var mMt=aRe(),yMt=ARe(),EMt=Object.prototype.hasOwnProperty;function pRe(t){return t&&t.code==="EPROMISERETRY"&&EMt.call(t,"retried")}function IMt(t,e){var r,s;return typeof t=="object"&&typeof e=="function"&&(r=e,e=t,t=r),s=yMt.operation(e),new Promise(function(a,n){s.attempt(function(c){Promise.resolve().then(function(){return t(function(f){throw pRe(f)&&(f=f.retried),mMt(new Error("Retrying"),"EPROMISERETRY",{retried:f})},c)}).then(a,function(f){pRe(f)&&(f=f.retried,s.retry(f||new Error))||n(f)})})})}hRe.exports=IMt});var vO=L(Ob=>{"use strict";var mRe=Ob&&Ob.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(Ob,"__esModule",{value:!0});Ob.fetchWithRetry=TMt;var CMt=Ie("http2"),wMt=mRe(wO()),dRe=iRe(),BMt=mRe(gRe()),vMt=Bg(),SMt=IO(),{HTTP2_HEADER_LOCATION:DMt,HTTP2_HEADER_CONTENT_TYPE:bMt,HTTP2_HEADER_USER_AGENT:PMt,HTTP_STATUS_INTERNAL_SERVER_ERROR:xMt,HTTP_STATUS_TOO_MANY_REQUESTS:kMt,HTTP_STATUS_REQUEST_TIMEOUT:QMt}=CMt.constants;async function TMt(t,e){return(0,BMt.default)(async(r,s)=>{let a=e.method||"POST",n={[PMt]:vMt.ua.getUserAgent(),...e.headers},c=await(0,wMt.default)(t,{method:a,headers:n,body:e.body,timeout:e.timeout,retry:!1}).catch(f=>(dRe.log.http("fetch",`${a} ${t} attempt ${s} failed with ${f}`),r(f)));if(c.ok)return c;{let f=await RMt(c);if(dRe.log.http("fetch",`${a} ${t} attempt ${s} failed with ${c.status}`),FMt(c.status))return r(f);throw f}},NMt(e.retry))}var RMt=async t=>{let e=t.statusText,r=t.headers.get(DMt)||void 0;if(t.headers.get(bMt)?.includes("application/json"))try{e=(await t.json()).message||e}catch{}return new SMt.HTTPError({status:t.status,message:e,location:r})},FMt=t=>[QMt,kMt].includes(t)||t>=xMt,NMt=t=>typeof t=="boolean"?{retries:t?1:0}:typeof t=="number"?{retries:t}:{retries:0,...t}});var yRe=L(SO=>{"use strict";Object.defineProperty(SO,"__esModule",{value:!0});SO.Fulcio=void 0;var OMt=vO(),eK=class{constructor(e){this.options=e}async createSigningCertificate(e){let{baseURL:r,retry:s,timeout:a}=this.options,n=`${r}/api/v2/signingCert`;return(await(0,OMt.fetchWithRetry)(n,{headers:{"Content-Type":"application/json"},body:JSON.stringify(e),timeout:a,retry:s})).json()}};SO.Fulcio=eK});var ERe=L(DO=>{"use strict";Object.defineProperty(DO,"__esModule",{value:!0});DO.CAClient=void 0;var LMt=Xw(),MMt=yRe(),tK=class{constructor(e){this.fulcio=new MMt.Fulcio({baseURL:e.fulcioBaseURL,retry:e.retry,timeout:e.timeout})}async createSigningCertificate(e,r,s){let a=_Mt(e,r,s);try{let n=await this.fulcio.createSigningCertificate(a);return(n.signedCertificateEmbeddedSct?n.signedCertificateEmbeddedSct:n.signedCertificateDetachedSct).chain.certificates}catch(n){(0,LMt.internalError)(n,"CA_CREATE_SIGNING_CERTIFICATE_ERROR","error creating signing certificate")}}};DO.CAClient=tK;function _Mt(t,e,r){return{credentials:{oidcIdentityToken:t},publicKeyRequest:{publicKey:{algorithm:"ECDSA",content:e},proofOfPossession:r.toString("base64")}}}});var CRe=L(e1=>{"use strict";var UMt=e1&&e1.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e1,"__esModule",{value:!0});e1.EphemeralSigner=void 0;var IRe=UMt(Ie("crypto")),HMt="ec",jMt="P-256",rK=class{constructor(){this.keypair=IRe.default.generateKeyPairSync(HMt,{namedCurve:jMt})}async sign(e){let r=IRe.default.sign(null,e,this.keypair.privateKey),s=this.keypair.publicKey.export({format:"pem",type:"spki"}).toString("ascii");return{signature:r,key:{$case:"publicKey",publicKey:s}}}};e1.EphemeralSigner=rK});var wRe=L(iy=>{"use strict";Object.defineProperty(iy,"__esModule",{value:!0});iy.FulcioSigner=iy.DEFAULT_FULCIO_URL=void 0;var nK=Xw(),qMt=Bg(),GMt=ERe(),WMt=CRe();iy.DEFAULT_FULCIO_URL="https://fulcio.sigstore.dev";var iK=class{constructor(e){this.ca=new GMt.CAClient({...e,fulcioBaseURL:e.fulcioBaseURL||iy.DEFAULT_FULCIO_URL}),this.identityProvider=e.identityProvider,this.keyHolder=e.keyHolder||new WMt.EphemeralSigner}async sign(e){let r=await this.getIdentityToken(),s;try{s=qMt.oidc.extractJWTSubject(r)}catch(f){throw new nK.InternalError({code:"IDENTITY_TOKEN_PARSE_ERROR",message:`invalid identity token: ${r}`,cause:f})}let a=await this.keyHolder.sign(Buffer.from(s));if(a.key.$case!=="publicKey")throw new nK.InternalError({code:"CA_CREATE_SIGNING_CERTIFICATE_ERROR",message:"unexpected format for signing key"});let n=await this.ca.createSigningCertificate(r,a.key.publicKey,a.signature);return{signature:(await this.keyHolder.sign(e)).signature,key:{$case:"x509Certificate",certificate:n[0]}}}async getIdentityToken(){try{return await this.identityProvider.getToken()}catch(e){throw new nK.InternalError({code:"IDENTITY_TOKEN_READ_ERROR",message:"error retrieving identity token",cause:e})}}};iy.FulcioSigner=iK});var vRe=L(t1=>{"use strict";Object.defineProperty(t1,"__esModule",{value:!0});t1.FulcioSigner=t1.DEFAULT_FULCIO_URL=void 0;var BRe=wRe();Object.defineProperty(t1,"DEFAULT_FULCIO_URL",{enumerable:!0,get:function(){return BRe.DEFAULT_FULCIO_URL}});Object.defineProperty(t1,"FulcioSigner",{enumerable:!0,get:function(){return BRe.FulcioSigner}})});var bRe=L(bO=>{"use strict";Object.defineProperty(bO,"__esModule",{value:!0});bO.Rekor=void 0;var SRe=vO(),sK=class{constructor(e){this.options=e}async createEntry(e){let{baseURL:r,timeout:s,retry:a}=this.options,n=`${r}/api/v1/log/entries`,f=await(await(0,SRe.fetchWithRetry)(n,{headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(e),timeout:s,retry:a})).json();return DRe(f)}async getEntry(e){let{baseURL:r,timeout:s,retry:a}=this.options,n=`${r}/api/v1/log/entries/${e}`,f=await(await(0,SRe.fetchWithRetry)(n,{method:"GET",headers:{Accept:"application/json"},timeout:s,retry:a})).json();return DRe(f)}};bO.Rekor=sK;function DRe(t){let e=Object.entries(t);if(e.length!=1)throw new Error("Received multiple entries in Rekor response");let[r,s]=e[0];return{...s,uuid:r}}});var xRe=L(PO=>{"use strict";Object.defineProperty(PO,"__esModule",{value:!0});PO.TLogClient=void 0;var PRe=Xw(),YMt=IO(),VMt=bRe(),oK=class{constructor(e){this.fetchOnConflict=e.fetchOnConflict??!1,this.rekor=new VMt.Rekor({baseURL:e.rekorBaseURL,retry:e.retry,timeout:e.timeout})}async createEntry(e){let r;try{r=await this.rekor.createEntry(e)}catch(s){if(KMt(s)&&this.fetchOnConflict){let a=s.location.split("/").pop()||"";try{r=await this.rekor.getEntry(a)}catch(n){(0,PRe.internalError)(n,"TLOG_FETCH_ENTRY_ERROR","error fetching tlog entry")}}else(0,PRe.internalError)(s,"TLOG_CREATE_ENTRY_ERROR","error creating tlog entry")}return r}};PO.TLogClient=oK;function KMt(t){return t instanceof YMt.HTTPError&&t.statusCode===409&&t.location!==void 0}});var kRe=L(aK=>{"use strict";Object.defineProperty(aK,"__esModule",{value:!0});aK.toProposedEntry=zMt;var JMt=xb(),vg=Bg(),Lb="sha256";function zMt(t,e,r="dsse"){switch(t.$case){case"dsseEnvelope":return r==="intoto"?$Mt(t.dsseEnvelope,e):XMt(t.dsseEnvelope,e);case"messageSignature":return ZMt(t.messageSignature,e)}}function ZMt(t,e){let r=t.messageDigest.digest.toString("hex"),s=t.signature.toString("base64"),a=vg.encoding.base64Encode(e);return{apiVersion:"0.0.1",kind:"hashedrekord",spec:{data:{hash:{algorithm:Lb,value:r}},signature:{content:s,publicKey:{content:a}}}}}function XMt(t,e){let r=JSON.stringify((0,JMt.envelopeToJSON)(t)),s=vg.encoding.base64Encode(e);return{apiVersion:"0.0.1",kind:"dsse",spec:{proposedContent:{envelope:r,verifiers:[s]}}}}function $Mt(t,e){let r=vg.crypto.digest(Lb,t.payload).toString("hex"),s=e_t(t,e),a=vg.encoding.base64Encode(t.payload.toString("base64")),n=vg.encoding.base64Encode(t.signatures[0].sig.toString("base64")),c=t.signatures[0].keyid,f=vg.encoding.base64Encode(e),p={payloadType:t.payloadType,payload:a,signatures:[{sig:n,publicKey:f}]};return c.length>0&&(p.signatures[0].keyid=c),{apiVersion:"0.0.2",kind:"intoto",spec:{content:{envelope:p,hash:{algorithm:Lb,value:s},payloadHash:{algorithm:Lb,value:r}}}}}function e_t(t,e){let r={payloadType:t.payloadType,payload:t.payload.toString("base64"),signatures:[{sig:t.signatures[0].sig.toString("base64"),publicKey:e}]};return t.signatures[0].keyid.length>0&&(r.signatures[0].keyid=t.signatures[0].keyid),vg.crypto.digest(Lb,vg.json.canonicalize(r)).toString("hex")}});var QRe=L(sy=>{"use strict";Object.defineProperty(sy,"__esModule",{value:!0});sy.RekorWitness=sy.DEFAULT_REKOR_URL=void 0;var t_t=Bg(),r_t=xRe(),n_t=kRe();sy.DEFAULT_REKOR_URL="https://rekor.sigstore.dev";var lK=class{constructor(e){this.entryType=e.entryType,this.tlog=new r_t.TLogClient({...e,rekorBaseURL:e.rekorBaseURL||sy.DEFAULT_REKOR_URL})}async testify(e,r){let s=(0,n_t.toProposedEntry)(e,r,this.entryType),a=await this.tlog.createEntry(s);return i_t(a)}};sy.RekorWitness=lK;function i_t(t){let e=Buffer.from(t.logID,"hex"),r=t_t.encoding.base64Decode(t.body),s=JSON.parse(r),a=t?.verification?.signedEntryTimestamp?s_t(t.verification.signedEntryTimestamp):void 0,n=t?.verification?.inclusionProof?o_t(t.verification.inclusionProof):void 0;return{tlogEntries:[{logIndex:t.logIndex.toString(),logId:{keyId:e},integratedTime:t.integratedTime.toString(),kindVersion:{kind:s.kind,version:s.apiVersion},inclusionPromise:a,inclusionProof:n,canonicalizedBody:Buffer.from(t.body,"base64")}]}}function s_t(t){return{signedEntryTimestamp:Buffer.from(t,"base64")}}function o_t(t){return{logIndex:t.logIndex.toString(),treeSize:t.treeSize.toString(),rootHash:Buffer.from(t.rootHash,"hex"),hashes:t.hashes.map(e=>Buffer.from(e,"hex")),checkpoint:{envelope:t.checkpoint}}}});var TRe=L(xO=>{"use strict";Object.defineProperty(xO,"__esModule",{value:!0});xO.TimestampAuthority=void 0;var a_t=vO(),cK=class{constructor(e){this.options=e}async createTimestamp(e){let{baseURL:r,timeout:s,retry:a}=this.options,n=`${r}/api/v1/timestamp`;return(await(0,a_t.fetchWithRetry)(n,{headers:{"Content-Type":"application/json"},body:JSON.stringify(e),timeout:s,retry:a})).buffer()}};xO.TimestampAuthority=cK});var FRe=L(kO=>{"use strict";Object.defineProperty(kO,"__esModule",{value:!0});kO.TSAClient=void 0;var l_t=Xw(),c_t=TRe(),u_t=Bg(),RRe="sha256",uK=class{constructor(e){this.tsa=new c_t.TimestampAuthority({baseURL:e.tsaBaseURL,retry:e.retry,timeout:e.timeout})}async createTimestamp(e){let r={artifactHash:u_t.crypto.digest(RRe,e).toString("base64"),hashAlgorithm:RRe};try{return await this.tsa.createTimestamp(r)}catch(s){(0,l_t.internalError)(s,"TSA_CREATE_TIMESTAMP_ERROR","error creating timestamp")}}};kO.TSAClient=uK});var NRe=L(QO=>{"use strict";Object.defineProperty(QO,"__esModule",{value:!0});QO.TSAWitness=void 0;var f_t=FRe(),fK=class{constructor(e){this.tsa=new f_t.TSAClient({tsaBaseURL:e.tsaBaseURL,retry:e.retry,timeout:e.timeout})}async testify(e){let r=A_t(e);return{rfc3161Timestamps:[{signedTimestamp:await this.tsa.createTimestamp(r)}]}}};QO.TSAWitness=fK;function A_t(t){switch(t.$case){case"dsseEnvelope":return t.dsseEnvelope.signatures[0].sig;case"messageSignature":return t.messageSignature.signature}}});var LRe=L(Sg=>{"use strict";Object.defineProperty(Sg,"__esModule",{value:!0});Sg.TSAWitness=Sg.RekorWitness=Sg.DEFAULT_REKOR_URL=void 0;var ORe=QRe();Object.defineProperty(Sg,"DEFAULT_REKOR_URL",{enumerable:!0,get:function(){return ORe.DEFAULT_REKOR_URL}});Object.defineProperty(Sg,"RekorWitness",{enumerable:!0,get:function(){return ORe.RekorWitness}});var p_t=NRe();Object.defineProperty(Sg,"TSAWitness",{enumerable:!0,get:function(){return p_t.TSAWitness}})});var pK=L(Is=>{"use strict";Object.defineProperty(Is,"__esModule",{value:!0});Is.TSAWitness=Is.RekorWitness=Is.DEFAULT_REKOR_URL=Is.FulcioSigner=Is.DEFAULT_FULCIO_URL=Is.CIContextProvider=Is.InternalError=Is.MessageSignatureBundleBuilder=Is.DSSEBundleBuilder=void 0;var MRe=$Te();Object.defineProperty(Is,"DSSEBundleBuilder",{enumerable:!0,get:function(){return MRe.DSSEBundleBuilder}});Object.defineProperty(Is,"MessageSignatureBundleBuilder",{enumerable:!0,get:function(){return MRe.MessageSignatureBundleBuilder}});var h_t=Xw();Object.defineProperty(Is,"InternalError",{enumerable:!0,get:function(){return h_t.InternalError}});var g_t=rRe();Object.defineProperty(Is,"CIContextProvider",{enumerable:!0,get:function(){return g_t.CIContextProvider}});var _Re=vRe();Object.defineProperty(Is,"DEFAULT_FULCIO_URL",{enumerable:!0,get:function(){return _Re.DEFAULT_FULCIO_URL}});Object.defineProperty(Is,"FulcioSigner",{enumerable:!0,get:function(){return _Re.FulcioSigner}});var AK=LRe();Object.defineProperty(Is,"DEFAULT_REKOR_URL",{enumerable:!0,get:function(){return AK.DEFAULT_REKOR_URL}});Object.defineProperty(Is,"RekorWitness",{enumerable:!0,get:function(){return AK.RekorWitness}});Object.defineProperty(Is,"TSAWitness",{enumerable:!0,get:function(){return AK.TSAWitness}})});var HRe=L(Mb=>{"use strict";var URe=Mb&&Mb.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(Mb,"__esModule",{value:!0});Mb.appDataPath=m_t;var d_t=URe(Ie("os")),r1=URe(Ie("path"));function m_t(t){let e=d_t.default.homedir();switch(process.platform){case"darwin":{let r=r1.default.join(e,"Library","Application Support");return r1.default.join(r,t)}case"win32":{let r=process.env.LOCALAPPDATA||r1.default.join(e,"AppData","Local");return r1.default.join(r,t,"Data")}default:{let r=process.env.XDG_DATA_HOME||r1.default.join(e,".local","share");return r1.default.join(r,t)}}}});var kA=L(Bl=>{"use strict";Object.defineProperty(Bl,"__esModule",{value:!0});Bl.UnsupportedAlgorithmError=Bl.CryptoError=Bl.LengthOrHashMismatchError=Bl.UnsignedMetadataError=Bl.RepositoryError=Bl.ValueError=void 0;var hK=class extends Error{};Bl.ValueError=hK;var _b=class extends Error{};Bl.RepositoryError=_b;var gK=class extends _b{};Bl.UnsignedMetadataError=gK;var dK=class extends _b{};Bl.LengthOrHashMismatchError=dK;var TO=class extends Error{};Bl.CryptoError=TO;var mK=class extends TO{};Bl.UnsupportedAlgorithmError=mK});var qRe=L(Dg=>{"use strict";Object.defineProperty(Dg,"__esModule",{value:!0});Dg.isDefined=y_t;Dg.isObject=jRe;Dg.isStringArray=E_t;Dg.isObjectArray=I_t;Dg.isStringRecord=C_t;Dg.isObjectRecord=w_t;function y_t(t){return t!==void 0}function jRe(t){return typeof t=="object"&&t!==null}function E_t(t){return Array.isArray(t)&&t.every(e=>typeof e=="string")}function I_t(t){return Array.isArray(t)&&t.every(jRe)}function C_t(t){return typeof t=="object"&&t!==null&&Object.keys(t).every(e=>typeof e=="string")&&Object.values(t).every(e=>typeof e=="string")}function w_t(t){return typeof t=="object"&&t!==null&&Object.keys(t).every(e=>typeof e=="string")&&Object.values(t).every(e=>typeof e=="object"&&e!==null)}});var EK=L((yCr,YRe)=>{var GRe=",",B_t=":",v_t="[",S_t="]",D_t="{",b_t="}";function yK(t){let e=[];if(typeof t=="string")e.push(WRe(t));else if(typeof t=="boolean")e.push(JSON.stringify(t));else if(Number.isInteger(t))e.push(JSON.stringify(t));else if(t===null)e.push(JSON.stringify(t));else if(Array.isArray(t)){e.push(v_t);let r=!0;t.forEach(s=>{r||e.push(GRe),r=!1,e.push(yK(s))}),e.push(S_t)}else if(typeof t=="object"){e.push(D_t);let r=!0;Object.keys(t).sort().forEach(s=>{r||e.push(GRe),r=!1,e.push(WRe(s)),e.push(B_t),e.push(yK(t[s]))}),e.push(b_t)}else throw new TypeError("cannot encode "+t.toString());return e.join("")}function WRe(t){return'"'+t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'}YRe.exports={canonicalize:yK}});var VRe=L(n1=>{"use strict";var P_t=n1&&n1.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(n1,"__esModule",{value:!0});n1.verifySignature=void 0;var x_t=EK(),k_t=P_t(Ie("crypto")),Q_t=(t,e,r)=>{let s=Buffer.from((0,x_t.canonicalize)(t));return k_t.default.verify(void 0,s,e,Buffer.from(r,"hex"))};n1.verifySignature=Q_t});var Af=L(ru=>{"use strict";var T_t=ru&&ru.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(e,r);(!a||("get"in a?!e.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,s,a)}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),R_t=ru&&ru.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),KRe=ru&&ru.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.prototype.hasOwnProperty.call(t,r)&&T_t(e,t,r);return R_t(e,t),e};Object.defineProperty(ru,"__esModule",{value:!0});ru.crypto=ru.guard=void 0;ru.guard=KRe(qRe());ru.crypto=KRe(VRe())});var oy=L(dh=>{"use strict";var F_t=dh&&dh.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(dh,"__esModule",{value:!0});dh.Signed=dh.MetadataKind=void 0;dh.isMetadataKind=O_t;var N_t=F_t(Ie("util")),Ub=kA(),IK=Af(),JRe=["1","0","31"],CK;(function(t){t.Root="root",t.Timestamp="timestamp",t.Snapshot="snapshot",t.Targets="targets"})(CK||(dh.MetadataKind=CK={}));function O_t(t){return typeof t=="string"&&Object.values(CK).includes(t)}var wK=class t{constructor(e){this.specVersion=e.specVersion||JRe.join(".");let r=this.specVersion.split(".");if(!(r.length===2||r.length===3)||!r.every(s=>L_t(s)))throw new Ub.ValueError("Failed to parse specVersion");if(r[0]!=JRe[0])throw new Ub.ValueError("Unsupported specVersion");this.expires=e.expires,this.version=e.version,this.unrecognizedFields=e.unrecognizedFields||{}}equals(e){return e instanceof t?this.specVersion===e.specVersion&&this.expires===e.expires&&this.version===e.version&&N_t.default.isDeepStrictEqual(this.unrecognizedFields,e.unrecognizedFields):!1}isExpired(e){return e||(e=new Date),e>=new Date(this.expires)}static commonFieldsFromJSON(e){let{spec_version:r,expires:s,version:a,...n}=e;if(IK.guard.isDefined(r)){if(typeof r!="string")throw new TypeError("spec_version must be a string")}else throw new Ub.ValueError("spec_version is not defined");if(IK.guard.isDefined(s)){if(typeof s!="string")throw new TypeError("expires must be a string")}else throw new Ub.ValueError("expires is not defined");if(IK.guard.isDefined(a)){if(typeof a!="number")throw new TypeError("version must be a number")}else throw new Ub.ValueError("version is not defined");return{specVersion:r,expires:s,version:a,unrecognizedFields:n}}};dh.Signed=wK;function L_t(t){return!isNaN(Number(t))}});var Hb=L(Pg=>{"use strict";var zRe=Pg&&Pg.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(Pg,"__esModule",{value:!0});Pg.TargetFile=Pg.MetaFile=void 0;var ZRe=zRe(Ie("crypto")),FO=zRe(Ie("util")),bg=kA(),RO=Af(),BK=class t{constructor(e){if(e.version<=0)throw new bg.ValueError("Metafile version must be at least 1");e.length!==void 0&&XRe(e.length),this.version=e.version,this.length=e.length,this.hashes=e.hashes,this.unrecognizedFields=e.unrecognizedFields||{}}equals(e){return e instanceof t?this.version===e.version&&this.length===e.length&&FO.default.isDeepStrictEqual(this.hashes,e.hashes)&&FO.default.isDeepStrictEqual(this.unrecognizedFields,e.unrecognizedFields):!1}verify(e){if(this.length!==void 0&&e.length!==this.length)throw new bg.LengthOrHashMismatchError(`Expected length ${this.length} but got ${e.length}`);this.hashes&&Object.entries(this.hashes).forEach(([r,s])=>{let a;try{a=ZRe.default.createHash(r)}catch{throw new bg.LengthOrHashMismatchError(`Hash algorithm ${r} not supported`)}let n=a.update(e).digest("hex");if(n!==s)throw new bg.LengthOrHashMismatchError(`Expected hash ${s} but got ${n}`)})}toJSON(){let e={version:this.version,...this.unrecognizedFields};return this.length!==void 0&&(e.length=this.length),this.hashes&&(e.hashes=this.hashes),e}static fromJSON(e){let{version:r,length:s,hashes:a,...n}=e;if(typeof r!="number")throw new TypeError("version must be a number");if(RO.guard.isDefined(s)&&typeof s!="number")throw new TypeError("length must be a number");if(RO.guard.isDefined(a)&&!RO.guard.isStringRecord(a))throw new TypeError("hashes must be string keys and values");return new t({version:r,length:s,hashes:a,unrecognizedFields:n})}};Pg.MetaFile=BK;var vK=class t{constructor(e){XRe(e.length),this.length=e.length,this.path=e.path,this.hashes=e.hashes,this.unrecognizedFields=e.unrecognizedFields||{}}get custom(){let e=this.unrecognizedFields.custom;return!e||Array.isArray(e)||typeof e!="object"?{}:e}equals(e){return e instanceof t?this.length===e.length&&this.path===e.path&&FO.default.isDeepStrictEqual(this.hashes,e.hashes)&&FO.default.isDeepStrictEqual(this.unrecognizedFields,e.unrecognizedFields):!1}async verify(e){let r=0,s=Object.keys(this.hashes).reduce((a,n)=>{try{a[n]=ZRe.default.createHash(n)}catch{throw new bg.LengthOrHashMismatchError(`Hash algorithm ${n} not supported`)}return a},{});for await(let a of e)r+=a.length,Object.values(s).forEach(n=>{n.update(a)});if(r!==this.length)throw new bg.LengthOrHashMismatchError(`Expected length ${this.length} but got ${r}`);Object.entries(s).forEach(([a,n])=>{let c=this.hashes[a],f=n.digest("hex");if(f!==c)throw new bg.LengthOrHashMismatchError(`Expected hash ${c} but got ${f}`)})}toJSON(){return{length:this.length,hashes:this.hashes,...this.unrecognizedFields}}static fromJSON(e,r){let{length:s,hashes:a,...n}=r;if(typeof s!="number")throw new TypeError("length must be a number");if(!RO.guard.isStringRecord(a))throw new TypeError("hashes must have string keys and values");return new t({length:s,path:e,hashes:a,unrecognizedFields:n})}};Pg.TargetFile=vK;function XRe(t){if(t<0)throw new bg.ValueError("Length must be at least 0")}});var $Re=L(SK=>{"use strict";Object.defineProperty(SK,"__esModule",{value:!0});SK.encodeOIDString=__t;var M_t=6;function __t(t){let e=t.split("."),r=parseInt(e[0],10)*40+parseInt(e[1],10),s=[];e.slice(2).forEach(n=>{let c=U_t(parseInt(n,10));s.push(...c)});let a=Buffer.from([r,...s]);return Buffer.from([M_t,a.length,...a])}function U_t(t){let e=[],r=0;for(;t>0;)e.unshift(t&127|r),t>>=7,r=128;return e}});var nFe=L(qb=>{"use strict";var H_t=qb&&qb.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(qb,"__esModule",{value:!0});qb.getPublicKey=W_t;var i1=H_t(Ie("crypto")),jb=kA(),DK=$Re(),NO=48,eFe=3,tFe=0,j_t="1.3.101.112",q_t="1.2.840.10045.2.1",G_t="1.2.840.10045.3.1.7",bK="-----BEGIN PUBLIC KEY-----";function W_t(t){switch(t.keyType){case"rsa":return Y_t(t);case"ed25519":return V_t(t);case"ecdsa":case"ecdsa-sha2-nistp256":case"ecdsa-sha2-nistp384":return K_t(t);default:throw new jb.UnsupportedAlgorithmError(`Unsupported key type: ${t.keyType}`)}}function Y_t(t){if(!t.keyVal.startsWith(bK))throw new jb.CryptoError("Invalid key format");let e=i1.default.createPublicKey(t.keyVal);switch(t.scheme){case"rsassa-pss-sha256":return{key:e,padding:i1.default.constants.RSA_PKCS1_PSS_PADDING};default:throw new jb.UnsupportedAlgorithmError(`Unsupported RSA scheme: ${t.scheme}`)}}function V_t(t){let e;if(t.keyVal.startsWith(bK))e=i1.default.createPublicKey(t.keyVal);else{if(!rFe(t.keyVal))throw new jb.CryptoError("Invalid key format");e=i1.default.createPublicKey({key:J_t.hexToDER(t.keyVal),format:"der",type:"spki"})}return{key:e}}function K_t(t){let e;if(t.keyVal.startsWith(bK))e=i1.default.createPublicKey(t.keyVal);else{if(!rFe(t.keyVal))throw new jb.CryptoError("Invalid key format");e=i1.default.createPublicKey({key:z_t.hexToDER(t.keyVal),format:"der",type:"spki"})}return{key:e}}var J_t={hexToDER:t=>{let e=Buffer.from(t,"hex"),r=(0,DK.encodeOIDString)(j_t),s=Buffer.concat([Buffer.concat([Buffer.from([NO]),Buffer.from([r.length]),r]),Buffer.concat([Buffer.from([eFe]),Buffer.from([e.length+1]),Buffer.from([tFe]),e])]);return Buffer.concat([Buffer.from([NO]),Buffer.from([s.length]),s])}},z_t={hexToDER:t=>{let e=Buffer.from(t,"hex"),r=Buffer.concat([Buffer.from([eFe]),Buffer.from([e.length+1]),Buffer.from([tFe]),e]),s=Buffer.concat([(0,DK.encodeOIDString)(q_t),(0,DK.encodeOIDString)(G_t)]),a=Buffer.concat([Buffer.from([NO]),Buffer.from([s.length]),s]);return Buffer.concat([Buffer.from([NO]),Buffer.from([a.length+r.length]),a,r])}},rFe=t=>/^[0-9a-fA-F]+$/.test(t)});var OO=L(s1=>{"use strict";var Z_t=s1&&s1.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(s1,"__esModule",{value:!0});s1.Key=void 0;var iFe=Z_t(Ie("util")),Gb=kA(),sFe=Af(),X_t=nFe(),PK=class t{constructor(e){let{keyID:r,keyType:s,scheme:a,keyVal:n,unrecognizedFields:c}=e;this.keyID=r,this.keyType=s,this.scheme=a,this.keyVal=n,this.unrecognizedFields=c||{}}verifySignature(e){let r=e.signatures[this.keyID];if(!r)throw new Gb.UnsignedMetadataError("no signature for key found in metadata");if(!this.keyVal.public)throw new Gb.UnsignedMetadataError("no public key found");let s=(0,X_t.getPublicKey)({keyType:this.keyType,scheme:this.scheme,keyVal:this.keyVal.public}),a=e.signed.toJSON();try{if(!sFe.crypto.verifySignature(a,s,r.sig))throw new Gb.UnsignedMetadataError(`failed to verify ${this.keyID} signature`)}catch(n){throw n instanceof Gb.UnsignedMetadataError?n:new Gb.UnsignedMetadataError(`failed to verify ${this.keyID} signature`)}}equals(e){return e instanceof t?this.keyID===e.keyID&&this.keyType===e.keyType&&this.scheme===e.scheme&&iFe.default.isDeepStrictEqual(this.keyVal,e.keyVal)&&iFe.default.isDeepStrictEqual(this.unrecognizedFields,e.unrecognizedFields):!1}toJSON(){return{keytype:this.keyType,scheme:this.scheme,keyval:this.keyVal,...this.unrecognizedFields}}static fromJSON(e,r){let{keytype:s,scheme:a,keyval:n,...c}=r;if(typeof s!="string")throw new TypeError("keytype must be a string");if(typeof a!="string")throw new TypeError("scheme must be a string");if(!sFe.guard.isStringRecord(n))throw new TypeError("keyval must be a string record");return new t({keyID:e,keyType:s,scheme:a,keyVal:n,unrecognizedFields:c})}};s1.Key=PK});var uFe=L((DCr,cFe)=>{"use strict";cFe.exports=aFe;function aFe(t,e,r){t instanceof RegExp&&(t=oFe(t,r)),e instanceof RegExp&&(e=oFe(e,r));var s=lFe(t,e,r);return s&&{start:s[0],end:s[1],pre:r.slice(0,s[0]),body:r.slice(s[0]+t.length,s[1]),post:r.slice(s[1]+e.length)}}function oFe(t,e){var r=e.match(t);return r?r[0]:null}aFe.range=lFe;function lFe(t,e,r){var s,a,n,c,f,p=r.indexOf(t),h=r.indexOf(e,p+1),E=p;if(p>=0&&h>0){for(s=[],n=r.length;E>=0&&!f;)E==p?(s.push(E),p=r.indexOf(t,E+1)):s.length==1?f=[s.pop(),h]:(a=s.pop(),a=0?p:h;s.length&&(f=[n,c])}return f}});var yFe=L((bCr,mFe)=>{var fFe=uFe();mFe.exports=tUt;var AFe="\0SLASH"+Math.random()+"\0",pFe="\0OPEN"+Math.random()+"\0",kK="\0CLOSE"+Math.random()+"\0",hFe="\0COMMA"+Math.random()+"\0",gFe="\0PERIOD"+Math.random()+"\0";function xK(t){return parseInt(t,10)==t?parseInt(t,10):t.charCodeAt(0)}function $_t(t){return t.split("\\\\").join(AFe).split("\\{").join(pFe).split("\\}").join(kK).split("\\,").join(hFe).split("\\.").join(gFe)}function eUt(t){return t.split(AFe).join("\\").split(pFe).join("{").split(kK).join("}").split(hFe).join(",").split(gFe).join(".")}function dFe(t){if(!t)return[""];var e=[],r=fFe("{","}",t);if(!r)return t.split(",");var s=r.pre,a=r.body,n=r.post,c=s.split(",");c[c.length-1]+="{"+a+"}";var f=dFe(n);return n.length&&(c[c.length-1]+=f.shift(),c.push.apply(c,f)),e.push.apply(e,c),e}function tUt(t){return t?(t.substr(0,2)==="{}"&&(t="\\{\\}"+t.substr(2)),Wb($_t(t),!0).map(eUt)):[]}function rUt(t){return"{"+t+"}"}function nUt(t){return/^-?0\d/.test(t)}function iUt(t,e){return t<=e}function sUt(t,e){return t>=e}function Wb(t,e){var r=[],s=fFe("{","}",t);if(!s)return[t];var a=s.pre,n=s.post.length?Wb(s.post,!1):[""];if(/\$$/.test(s.pre))for(var c=0;c=0;if(!E&&!C)return s.post.match(/,.*\}/)?(t=s.pre+"{"+s.body+kK+s.post,Wb(t)):[t];var S;if(E)S=s.body.split(/\.\./);else if(S=dFe(s.body),S.length===1&&(S=Wb(S[0],!1).map(rUt),S.length===1))return n.map(function(Ce){return s.pre+S[0]+Ce});var P;if(E){var I=xK(S[0]),R=xK(S[1]),N=Math.max(S[0].length,S[1].length),U=S.length==3?Math.abs(xK(S[2])):1,W=iUt,te=R0){var pe=new Array(me+1).join("0");Ae<0?ce="-"+pe+ce.slice(1):ce=pe+ce}}P.push(ce)}}else{P=[];for(var Be=0;Be{"use strict";Object.defineProperty(LO,"__esModule",{value:!0});LO.assertValidPattern=void 0;var oUt=1024*64,aUt=t=>{if(typeof t!="string")throw new TypeError("invalid pattern");if(t.length>oUt)throw new TypeError("pattern is too long")};LO.assertValidPattern=aUt});var CFe=L(MO=>{"use strict";Object.defineProperty(MO,"__esModule",{value:!0});MO.parseClass=void 0;var lUt={"[:alnum:]":["\\p{L}\\p{Nl}\\p{Nd}",!0],"[:alpha:]":["\\p{L}\\p{Nl}",!0],"[:ascii:]":["\\x00-\\x7f",!1],"[:blank:]":["\\p{Zs}\\t",!0],"[:cntrl:]":["\\p{Cc}",!0],"[:digit:]":["\\p{Nd}",!0],"[:graph:]":["\\p{Z}\\p{C}",!0,!0],"[:lower:]":["\\p{Ll}",!0],"[:print:]":["\\p{C}",!0],"[:punct:]":["\\p{P}",!0],"[:space:]":["\\p{Z}\\t\\r\\n\\v\\f",!0],"[:upper:]":["\\p{Lu}",!0],"[:word:]":["\\p{L}\\p{Nl}\\p{Nd}\\p{Pc}",!0],"[:xdigit:]":["A-Fa-f0-9",!1]},Yb=t=>t.replace(/[[\]\\-]/g,"\\$&"),cUt=t=>t.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),IFe=t=>t.join(""),uUt=(t,e)=>{let r=e;if(t.charAt(r)!=="[")throw new Error("not in a brace expression");let s=[],a=[],n=r+1,c=!1,f=!1,p=!1,h=!1,E=r,C="";e:for(;nC?s.push(Yb(C)+"-"+Yb(R)):R===C&&s.push(Yb(R)),C="",n++;continue}if(t.startsWith("-]",n+1)){s.push(Yb(R+"-")),n+=2;continue}if(t.startsWith("-",n+1)){C=R,n+=2;continue}s.push(Yb(R)),n++}if(E{"use strict";Object.defineProperty(_O,"__esModule",{value:!0});_O.unescape=void 0;var fUt=(t,{windowsPathsNoEscape:e=!1}={})=>e?t.replace(/\[([^\/\\])\]/g,"$1"):t.replace(/((?!\\).|^)\[([^\/\\])\]/g,"$1$2").replace(/\\([^\/])/g,"$1");_O.unescape=fUt});var RK=L(qO=>{"use strict";Object.defineProperty(qO,"__esModule",{value:!0});qO.AST=void 0;var AUt=CFe(),HO=UO(),pUt=new Set(["!","?","+","*","@"]),wFe=t=>pUt.has(t),hUt="(?!(?:^|/)\\.\\.?(?:$|/))",jO="(?!\\.)",gUt=new Set(["[","."]),dUt=new Set(["..","."]),mUt=new Set("().*{}+?[]^$\\!"),yUt=t=>t.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),TK="[^/]",BFe=TK+"*?",vFe=TK+"+?",QK=class t{type;#t;#r;#i=!1;#e=[];#n;#o;#l;#a=!1;#s;#c;#f=!1;constructor(e,r,s={}){this.type=e,e&&(this.#r=!0),this.#n=r,this.#t=this.#n?this.#n.#t:this,this.#s=this.#t===this?s:this.#t.#s,this.#l=this.#t===this?[]:this.#t.#l,e==="!"&&!this.#t.#a&&this.#l.push(this),this.#o=this.#n?this.#n.#e.length:0}get hasMagic(){if(this.#r!==void 0)return this.#r;for(let e of this.#e)if(typeof e!="string"&&(e.type||e.hasMagic))return this.#r=!0;return this.#r}toString(){return this.#c!==void 0?this.#c:this.type?this.#c=this.type+"("+this.#e.map(e=>String(e)).join("|")+")":this.#c=this.#e.map(e=>String(e)).join("")}#p(){if(this!==this.#t)throw new Error("should only call on root");if(this.#a)return this;this.toString(),this.#a=!0;let e;for(;e=this.#l.pop();){if(e.type!=="!")continue;let r=e,s=r.#n;for(;s;){for(let a=r.#o+1;!s.type&&atypeof r=="string"?r:r.toJSON()):[this.type,...this.#e.map(r=>r.toJSON())];return this.isStart()&&!this.type&&e.unshift([]),this.isEnd()&&(this===this.#t||this.#t.#a&&this.#n?.type==="!")&&e.push({}),e}isStart(){if(this.#t===this)return!0;if(!this.#n?.isStart())return!1;if(this.#o===0)return!0;let e=this.#n;for(let r=0;r{let[I,R,N,U]=typeof P=="string"?t.#h(P,this.#r,p):P.toRegExpSource(e);return this.#r=this.#r||N,this.#i=this.#i||U,I}).join(""),E="";if(this.isStart()&&typeof this.#e[0]=="string"&&!(this.#e.length===1&&dUt.has(this.#e[0]))){let I=gUt,R=r&&I.has(h.charAt(0))||h.startsWith("\\.")&&I.has(h.charAt(2))||h.startsWith("\\.\\.")&&I.has(h.charAt(4)),N=!r&&!e&&I.has(h.charAt(0));E=R?hUt:N?jO:""}let C="";return this.isEnd()&&this.#t.#a&&this.#n?.type==="!"&&(C="(?:$|\\/)"),[E+h+C,(0,HO.unescape)(h),this.#r=!!this.#r,this.#i]}let s=this.type==="*"||this.type==="+",a=this.type==="!"?"(?:(?!(?:":"(?:",n=this.#A(r);if(this.isStart()&&this.isEnd()&&!n&&this.type!=="!"){let p=this.toString();return this.#e=[p],this.type=null,this.#r=void 0,[p,(0,HO.unescape)(this.toString()),!1,!1]}let c=!s||e||r||!jO?"":this.#A(!0);c===n&&(c=""),c&&(n=`(?:${n})(?:${c})*?`);let f="";if(this.type==="!"&&this.#f)f=(this.isStart()&&!r?jO:"")+vFe;else{let p=this.type==="!"?"))"+(this.isStart()&&!r&&!e?jO:"")+BFe+")":this.type==="@"?")":this.type==="?"?")?":this.type==="+"&&c?")":this.type==="*"&&c?")?":`)${this.type}`;f=a+n+p}return[f,(0,HO.unescape)(n),this.#r=!!this.#r,this.#i]}#A(e){return this.#e.map(r=>{if(typeof r=="string")throw new Error("string type in extglob ast??");let[s,a,n,c]=r.toRegExpSource(e);return this.#i=this.#i||c,s}).filter(r=>!(this.isStart()&&this.isEnd())||!!r).join("|")}static#h(e,r,s=!1){let a=!1,n="",c=!1;for(let f=0;f{"use strict";Object.defineProperty(GO,"__esModule",{value:!0});GO.escape=void 0;var EUt=(t,{windowsPathsNoEscape:e=!1}={})=>e?t.replace(/[?*()[\]]/g,"[$&]"):t.replace(/[?*()[\]\\]/g,"\\$&");GO.escape=EUt});var QFe=L(pr=>{"use strict";var IUt=pr&&pr.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(pr,"__esModule",{value:!0});pr.unescape=pr.escape=pr.AST=pr.Minimatch=pr.match=pr.makeRe=pr.braceExpand=pr.defaults=pr.filter=pr.GLOBSTAR=pr.sep=pr.minimatch=void 0;var CUt=IUt(yFe()),WO=EFe(),bFe=RK(),wUt=FK(),BUt=UO(),vUt=(t,e,r={})=>((0,WO.assertValidPattern)(e),!r.nocomment&&e.charAt(0)==="#"?!1:new ay(e,r).match(t));pr.minimatch=vUt;var SUt=/^\*+([^+@!?\*\[\(]*)$/,DUt=t=>e=>!e.startsWith(".")&&e.endsWith(t),bUt=t=>e=>e.endsWith(t),PUt=t=>(t=t.toLowerCase(),e=>!e.startsWith(".")&&e.toLowerCase().endsWith(t)),xUt=t=>(t=t.toLowerCase(),e=>e.toLowerCase().endsWith(t)),kUt=/^\*+\.\*+$/,QUt=t=>!t.startsWith(".")&&t.includes("."),TUt=t=>t!=="."&&t!==".."&&t.includes("."),RUt=/^\.\*+$/,FUt=t=>t!=="."&&t!==".."&&t.startsWith("."),NUt=/^\*+$/,OUt=t=>t.length!==0&&!t.startsWith("."),LUt=t=>t.length!==0&&t!=="."&&t!=="..",MUt=/^\?+([^+@!?\*\[\(]*)?$/,_Ut=([t,e=""])=>{let r=PFe([t]);return e?(e=e.toLowerCase(),s=>r(s)&&s.toLowerCase().endsWith(e)):r},UUt=([t,e=""])=>{let r=xFe([t]);return e?(e=e.toLowerCase(),s=>r(s)&&s.toLowerCase().endsWith(e)):r},HUt=([t,e=""])=>{let r=xFe([t]);return e?s=>r(s)&&s.endsWith(e):r},jUt=([t,e=""])=>{let r=PFe([t]);return e?s=>r(s)&&s.endsWith(e):r},PFe=([t])=>{let e=t.length;return r=>r.length===e&&!r.startsWith(".")},xFe=([t])=>{let e=t.length;return r=>r.length===e&&r!=="."&&r!==".."},kFe=typeof process=="object"&&process?typeof process.env=="object"&&process.env&&process.env.__MINIMATCH_TESTING_PLATFORM__||process.platform:"posix",SFe={win32:{sep:"\\"},posix:{sep:"/"}};pr.sep=kFe==="win32"?SFe.win32.sep:SFe.posix.sep;pr.minimatch.sep=pr.sep;pr.GLOBSTAR=Symbol("globstar **");pr.minimatch.GLOBSTAR=pr.GLOBSTAR;var qUt="[^/]",GUt=qUt+"*?",WUt="(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?",YUt="(?:(?!(?:\\/|^)\\.).)*?",VUt=(t,e={})=>r=>(0,pr.minimatch)(r,t,e);pr.filter=VUt;pr.minimatch.filter=pr.filter;var nu=(t,e={})=>Object.assign({},t,e),KUt=t=>{if(!t||typeof t!="object"||!Object.keys(t).length)return pr.minimatch;let e=pr.minimatch;return Object.assign((s,a,n={})=>e(s,a,nu(t,n)),{Minimatch:class extends e.Minimatch{constructor(a,n={}){super(a,nu(t,n))}static defaults(a){return e.defaults(nu(t,a)).Minimatch}},AST:class extends e.AST{constructor(a,n,c={}){super(a,n,nu(t,c))}static fromGlob(a,n={}){return e.AST.fromGlob(a,nu(t,n))}},unescape:(s,a={})=>e.unescape(s,nu(t,a)),escape:(s,a={})=>e.escape(s,nu(t,a)),filter:(s,a={})=>e.filter(s,nu(t,a)),defaults:s=>e.defaults(nu(t,s)),makeRe:(s,a={})=>e.makeRe(s,nu(t,a)),braceExpand:(s,a={})=>e.braceExpand(s,nu(t,a)),match:(s,a,n={})=>e.match(s,a,nu(t,n)),sep:e.sep,GLOBSTAR:pr.GLOBSTAR})};pr.defaults=KUt;pr.minimatch.defaults=pr.defaults;var JUt=(t,e={})=>((0,WO.assertValidPattern)(t),e.nobrace||!/\{(?:(?!\{).)*\}/.test(t)?[t]:(0,CUt.default)(t));pr.braceExpand=JUt;pr.minimatch.braceExpand=pr.braceExpand;var zUt=(t,e={})=>new ay(t,e).makeRe();pr.makeRe=zUt;pr.minimatch.makeRe=pr.makeRe;var ZUt=(t,e,r={})=>{let s=new ay(e,r);return t=t.filter(a=>s.match(a)),s.options.nonull&&!t.length&&t.push(e),t};pr.match=ZUt;pr.minimatch.match=pr.match;var DFe=/[?*]|[+@!]\(.*?\)|\[|\]/,XUt=t=>t.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),ay=class{options;set;pattern;windowsPathsNoEscape;nonegate;negate;comment;empty;preserveMultipleSlashes;partial;globSet;globParts;nocase;isWindows;platform;windowsNoMagicRoot;regexp;constructor(e,r={}){(0,WO.assertValidPattern)(e),r=r||{},this.options=r,this.pattern=e,this.platform=r.platform||kFe,this.isWindows=this.platform==="win32",this.windowsPathsNoEscape=!!r.windowsPathsNoEscape||r.allowWindowsEscape===!1,this.windowsPathsNoEscape&&(this.pattern=this.pattern.replace(/\\/g,"/")),this.preserveMultipleSlashes=!!r.preserveMultipleSlashes,this.regexp=null,this.negate=!1,this.nonegate=!!r.nonegate,this.comment=!1,this.empty=!1,this.partial=!!r.partial,this.nocase=!!this.options.nocase,this.windowsNoMagicRoot=r.windowsNoMagicRoot!==void 0?r.windowsNoMagicRoot:!!(this.isWindows&&this.nocase),this.globSet=[],this.globParts=[],this.set=[],this.make()}hasMagic(){if(this.options.magicalBraces&&this.set.length>1)return!0;for(let e of this.set)for(let r of e)if(typeof r!="string")return!0;return!1}debug(...e){}make(){let e=this.pattern,r=this.options;if(!r.nocomment&&e.charAt(0)==="#"){this.comment=!0;return}if(!e){this.empty=!0;return}this.parseNegate(),this.globSet=[...new Set(this.braceExpand())],r.debug&&(this.debug=(...n)=>console.error(...n)),this.debug(this.pattern,this.globSet);let s=this.globSet.map(n=>this.slashSplit(n));this.globParts=this.preprocess(s),this.debug(this.pattern,this.globParts);let a=this.globParts.map((n,c,f)=>{if(this.isWindows&&this.windowsNoMagicRoot){let p=n[0]===""&&n[1]===""&&(n[2]==="?"||!DFe.test(n[2]))&&!DFe.test(n[3]),h=/^[a-z]:/i.test(n[0]);if(p)return[...n.slice(0,4),...n.slice(4).map(E=>this.parse(E))];if(h)return[n[0],...n.slice(1).map(E=>this.parse(E))]}return n.map(p=>this.parse(p))});if(this.debug(this.pattern,a),this.set=a.filter(n=>n.indexOf(!1)===-1),this.isWindows)for(let n=0;n=2?(e=this.firstPhasePreProcess(e),e=this.secondPhasePreProcess(e)):r>=1?e=this.levelOneOptimize(e):e=this.adjascentGlobstarOptimize(e),e}adjascentGlobstarOptimize(e){return e.map(r=>{let s=-1;for(;(s=r.indexOf("**",s+1))!==-1;){let a=s;for(;r[a+1]==="**";)a++;a!==s&&r.splice(s,a-s)}return r})}levelOneOptimize(e){return e.map(r=>(r=r.reduce((s,a)=>{let n=s[s.length-1];return a==="**"&&n==="**"?s:a===".."&&n&&n!==".."&&n!=="."&&n!=="**"?(s.pop(),s):(s.push(a),s)},[]),r.length===0?[""]:r))}levelTwoFileOptimize(e){Array.isArray(e)||(e=this.slashSplit(e));let r=!1;do{if(r=!1,!this.preserveMultipleSlashes){for(let a=1;aa&&s.splice(a+1,c-a);let f=s[a+1],p=s[a+2],h=s[a+3];if(f!==".."||!p||p==="."||p===".."||!h||h==="."||h==="..")continue;r=!0,s.splice(a,1);let E=s.slice(0);E[a]="**",e.push(E),a--}if(!this.preserveMultipleSlashes){for(let c=1;cr.length)}partsMatch(e,r,s=!1){let a=0,n=0,c=[],f="";for(;ate?r=r.slice(ie):te>ie&&(e=e.slice(te)))}}let{optimizationLevel:n=1}=this.options;n>=2&&(e=this.levelTwoFileOptimize(e)),this.debug("matchOne",this,{file:e,pattern:r}),this.debug("matchOne",e.length,r.length);for(var c=0,f=0,p=e.length,h=r.length;c>> no match, partial?`,e,S,r,P),S===p))}let R;if(typeof E=="string"?(R=C===E,this.debug("string match",E,C,R)):(R=E.test(C),this.debug("pattern match",E,C,R)),!R)return!1}if(c===p&&f===h)return!0;if(c===p)return s;if(f===h)return c===p-1&&e[c]==="";throw new Error("wtf?")}braceExpand(){return(0,pr.braceExpand)(this.pattern,this.options)}parse(e){(0,WO.assertValidPattern)(e);let r=this.options;if(e==="**")return pr.GLOBSTAR;if(e==="")return"";let s,a=null;(s=e.match(NUt))?a=r.dot?LUt:OUt:(s=e.match(SUt))?a=(r.nocase?r.dot?xUt:PUt:r.dot?bUt:DUt)(s[1]):(s=e.match(MUt))?a=(r.nocase?r.dot?UUt:_Ut:r.dot?HUt:jUt)(s):(s=e.match(kUt))?a=r.dot?TUt:QUt:(s=e.match(RUt))&&(a=FUt);let n=bFe.AST.fromGlob(e,this.options).toMMPattern();return a&&typeof n=="object"&&Reflect.defineProperty(n,"test",{value:a}),n}makeRe(){if(this.regexp||this.regexp===!1)return this.regexp;let e=this.set;if(!e.length)return this.regexp=!1,this.regexp;let r=this.options,s=r.noglobstar?GUt:r.dot?WUt:YUt,a=new Set(r.nocase?["i"]:[]),n=e.map(p=>{let h=p.map(E=>{if(E instanceof RegExp)for(let C of E.flags.split(""))a.add(C);return typeof E=="string"?XUt(E):E===pr.GLOBSTAR?pr.GLOBSTAR:E._src});return h.forEach((E,C)=>{let S=h[C+1],P=h[C-1];E!==pr.GLOBSTAR||P===pr.GLOBSTAR||(P===void 0?S!==void 0&&S!==pr.GLOBSTAR?h[C+1]="(?:\\/|"+s+"\\/)?"+S:h[C]=s:S===void 0?h[C-1]=P+"(?:\\/|"+s+")?":S!==pr.GLOBSTAR&&(h[C-1]=P+"(?:\\/|\\/"+s+"\\/)"+S,h[C+1]=pr.GLOBSTAR))}),h.filter(E=>E!==pr.GLOBSTAR).join("/")}).join("|"),[c,f]=e.length>1?["(?:",")"]:["",""];n="^"+c+n+f+"$",this.negate&&(n="^(?!"+n+").+$");try{this.regexp=new RegExp(n,[...a].join(""))}catch{this.regexp=!1}return this.regexp}slashSplit(e){return this.preserveMultipleSlashes?e.split("/"):this.isWindows&&/^\/\/[^\/]+/.test(e)?["",...e.split(/\/+/)]:e.split(/\/+/)}match(e,r=this.partial){if(this.debug("match",e,this.pattern),this.comment)return!1;if(this.empty)return e==="";if(e==="/"&&r)return!0;let s=this.options;this.isWindows&&(e=e.split("\\").join("/"));let a=this.slashSplit(e);this.debug(this.pattern,"split",a);let n=this.set;this.debug(this.pattern,"set",n);let c=a[a.length-1];if(!c)for(let f=a.length-2;!c&&f>=0;f--)c=a[f];for(let f=0;f{"use strict";var TFe=iu&&iu.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(iu,"__esModule",{value:!0});iu.SuccinctRoles=iu.DelegatedRole=iu.Role=iu.TOP_LEVEL_ROLE_NAMES=void 0;var RFe=TFe(Ie("crypto")),r4t=QFe(),YO=TFe(Ie("util")),VO=kA(),ly=Af();iu.TOP_LEVEL_ROLE_NAMES=["root","targets","snapshot","timestamp"];var Vb=class t{constructor(e){let{keyIDs:r,threshold:s,unrecognizedFields:a}=e;if(n4t(r))throw new VO.ValueError("duplicate key IDs found");if(s<1)throw new VO.ValueError("threshold must be at least 1");this.keyIDs=r,this.threshold=s,this.unrecognizedFields=a||{}}equals(e){return e instanceof t?this.threshold===e.threshold&&YO.default.isDeepStrictEqual(this.keyIDs,e.keyIDs)&&YO.default.isDeepStrictEqual(this.unrecognizedFields,e.unrecognizedFields):!1}toJSON(){return{keyids:this.keyIDs,threshold:this.threshold,...this.unrecognizedFields}}static fromJSON(e){let{keyids:r,threshold:s,...a}=e;if(!ly.guard.isStringArray(r))throw new TypeError("keyids must be an array");if(typeof s!="number")throw new TypeError("threshold must be a number");return new t({keyIDs:r,threshold:s,unrecognizedFields:a})}};iu.Role=Vb;function n4t(t){return new Set(t).size!==t.length}var NK=class t extends Vb{constructor(e){super(e);let{name:r,terminating:s,paths:a,pathHashPrefixes:n}=e;if(this.name=r,this.terminating=s,e.paths&&e.pathHashPrefixes)throw new VO.ValueError("paths and pathHashPrefixes are mutually exclusive");this.paths=a,this.pathHashPrefixes=n}equals(e){return e instanceof t?super.equals(e)&&this.name===e.name&&this.terminating===e.terminating&&YO.default.isDeepStrictEqual(this.paths,e.paths)&&YO.default.isDeepStrictEqual(this.pathHashPrefixes,e.pathHashPrefixes):!1}isDelegatedPath(e){if(this.paths)return this.paths.some(r=>s4t(e,r));if(this.pathHashPrefixes){let s=RFe.default.createHash("sha256").update(e).digest("hex");return this.pathHashPrefixes.some(a=>s.startsWith(a))}return!1}toJSON(){let e={...super.toJSON(),name:this.name,terminating:this.terminating};return this.paths&&(e.paths=this.paths),this.pathHashPrefixes&&(e.path_hash_prefixes=this.pathHashPrefixes),e}static fromJSON(e){let{keyids:r,threshold:s,name:a,terminating:n,paths:c,path_hash_prefixes:f,...p}=e;if(!ly.guard.isStringArray(r))throw new TypeError("keyids must be an array of strings");if(typeof s!="number")throw new TypeError("threshold must be a number");if(typeof a!="string")throw new TypeError("name must be a string");if(typeof n!="boolean")throw new TypeError("terminating must be a boolean");if(ly.guard.isDefined(c)&&!ly.guard.isStringArray(c))throw new TypeError("paths must be an array of strings");if(ly.guard.isDefined(f)&&!ly.guard.isStringArray(f))throw new TypeError("path_hash_prefixes must be an array of strings");return new t({keyIDs:r,threshold:s,name:a,terminating:n,paths:c,pathHashPrefixes:f,unrecognizedFields:p})}};iu.DelegatedRole=NK;var i4t=(t,e)=>t.map((r,s)=>[r,e[s]]);function s4t(t,e){let r=t.split("/"),s=e.split("/");return s.length!=r.length?!1:i4t(r,s).every(([a,n])=>(0,r4t.minimatch)(a,n))}var OK=class t extends Vb{constructor(e){super(e);let{bitLength:r,namePrefix:s}=e;if(r<=0||r>32)throw new VO.ValueError("bitLength must be between 1 and 32");this.bitLength=r,this.namePrefix=s,this.numberOfBins=Math.pow(2,r),this.suffixLen=(this.numberOfBins-1).toString(16).length}equals(e){return e instanceof t?super.equals(e)&&this.bitLength===e.bitLength&&this.namePrefix===e.namePrefix:!1}getRoleForTarget(e){let a=RFe.default.createHash("sha256").update(e).digest().subarray(0,4),n=32-this.bitLength,f=(a.readUInt32BE()>>>n).toString(16).padStart(this.suffixLen,"0");return`${this.namePrefix}-${f}`}*getRoles(){for(let e=0;e{"use strict";var o4t=o1&&o1.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(o1,"__esModule",{value:!0});o1.Root=void 0;var FFe=o4t(Ie("util")),MK=oy(),NFe=kA(),a4t=OO(),KO=LK(),JO=Af(),_K=class t extends MK.Signed{constructor(e){if(super(e),this.type=MK.MetadataKind.Root,this.keys=e.keys||{},this.consistentSnapshot=e.consistentSnapshot??!0,!e.roles)this.roles=KO.TOP_LEVEL_ROLE_NAMES.reduce((r,s)=>({...r,[s]:new KO.Role({keyIDs:[],threshold:1})}),{});else{let r=new Set(Object.keys(e.roles));if(!KO.TOP_LEVEL_ROLE_NAMES.every(s=>r.has(s)))throw new NFe.ValueError("missing top-level role");this.roles=e.roles}}addKey(e,r){if(!this.roles[r])throw new NFe.ValueError(`role ${r} does not exist`);this.roles[r].keyIDs.includes(e.keyID)||this.roles[r].keyIDs.push(e.keyID),this.keys[e.keyID]=e}equals(e){return e instanceof t?super.equals(e)&&this.consistentSnapshot===e.consistentSnapshot&&FFe.default.isDeepStrictEqual(this.keys,e.keys)&&FFe.default.isDeepStrictEqual(this.roles,e.roles):!1}toJSON(){return{_type:this.type,spec_version:this.specVersion,version:this.version,expires:this.expires,keys:l4t(this.keys),roles:c4t(this.roles),consistent_snapshot:this.consistentSnapshot,...this.unrecognizedFields}}static fromJSON(e){let{unrecognizedFields:r,...s}=MK.Signed.commonFieldsFromJSON(e),{keys:a,roles:n,consistent_snapshot:c,...f}=r;if(typeof c!="boolean")throw new TypeError("consistent_snapshot must be a boolean");return new t({...s,keys:u4t(a),roles:f4t(n),consistentSnapshot:c,unrecognizedFields:f})}};o1.Root=_K;function l4t(t){return Object.entries(t).reduce((e,[r,s])=>({...e,[r]:s.toJSON()}),{})}function c4t(t){return Object.entries(t).reduce((e,[r,s])=>({...e,[r]:s.toJSON()}),{})}function u4t(t){let e;if(JO.guard.isDefined(t)){if(!JO.guard.isObjectRecord(t))throw new TypeError("keys must be an object");e=Object.entries(t).reduce((r,[s,a])=>({...r,[s]:a4t.Key.fromJSON(s,a)}),{})}return e}function f4t(t){let e;if(JO.guard.isDefined(t)){if(!JO.guard.isObjectRecord(t))throw new TypeError("roles must be an object");e=Object.entries(t).reduce((r,[s,a])=>({...r,[s]:KO.Role.fromJSON(a)}),{})}return e}});var jK=L(zO=>{"use strict";Object.defineProperty(zO,"__esModule",{value:!0});zO.Signature=void 0;var HK=class t{constructor(e){let{keyID:r,sig:s}=e;this.keyID=r,this.sig=s}toJSON(){return{keyid:this.keyID,sig:this.sig}}static fromJSON(e){let{keyid:r,sig:s}=e;if(typeof r!="string")throw new TypeError("keyid must be a string");if(typeof s!="string")throw new TypeError("sig must be a string");return new t({keyID:r,sig:s})}};zO.Signature=HK});var WK=L(a1=>{"use strict";var A4t=a1&&a1.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(a1,"__esModule",{value:!0});a1.Snapshot=void 0;var p4t=A4t(Ie("util")),qK=oy(),LFe=Hb(),OFe=Af(),GK=class t extends qK.Signed{constructor(e){super(e),this.type=qK.MetadataKind.Snapshot,this.meta=e.meta||{"targets.json":new LFe.MetaFile({version:1})}}equals(e){return e instanceof t?super.equals(e)&&p4t.default.isDeepStrictEqual(this.meta,e.meta):!1}toJSON(){return{_type:this.type,meta:h4t(this.meta),spec_version:this.specVersion,version:this.version,expires:this.expires,...this.unrecognizedFields}}static fromJSON(e){let{unrecognizedFields:r,...s}=qK.Signed.commonFieldsFromJSON(e),{meta:a,...n}=r;return new t({...s,meta:g4t(a),unrecognizedFields:n})}};a1.Snapshot=GK;function h4t(t){return Object.entries(t).reduce((e,[r,s])=>({...e,[r]:s.toJSON()}),{})}function g4t(t){let e;if(OFe.guard.isDefined(t))if(OFe.guard.isObjectRecord(t))e=Object.entries(t).reduce((r,[s,a])=>({...r,[s]:LFe.MetaFile.fromJSON(a)}),{});else throw new TypeError("meta field is malformed");return e}});var MFe=L(l1=>{"use strict";var d4t=l1&&l1.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(l1,"__esModule",{value:!0});l1.Delegations=void 0;var ZO=d4t(Ie("util")),m4t=kA(),y4t=OO(),YK=LK(),XO=Af(),VK=class t{constructor(e){if(this.keys=e.keys,this.unrecognizedFields=e.unrecognizedFields||{},e.roles&&Object.keys(e.roles).some(r=>YK.TOP_LEVEL_ROLE_NAMES.includes(r)))throw new m4t.ValueError("Delegated role name conflicts with top-level role name");this.succinctRoles=e.succinctRoles,this.roles=e.roles}equals(e){return e instanceof t?ZO.default.isDeepStrictEqual(this.keys,e.keys)&&ZO.default.isDeepStrictEqual(this.roles,e.roles)&&ZO.default.isDeepStrictEqual(this.unrecognizedFields,e.unrecognizedFields)&&ZO.default.isDeepStrictEqual(this.succinctRoles,e.succinctRoles):!1}*rolesForTarget(e){if(this.roles)for(let r of Object.values(this.roles))r.isDelegatedPath(e)&&(yield{role:r.name,terminating:r.terminating});else this.succinctRoles&&(yield{role:this.succinctRoles.getRoleForTarget(e),terminating:!0})}toJSON(){let e={keys:E4t(this.keys),...this.unrecognizedFields};return this.roles?e.roles=I4t(this.roles):this.succinctRoles&&(e.succinct_roles=this.succinctRoles.toJSON()),e}static fromJSON(e){let{keys:r,roles:s,succinct_roles:a,...n}=e,c;return XO.guard.isObject(a)&&(c=YK.SuccinctRoles.fromJSON(a)),new t({keys:C4t(r),roles:w4t(s),unrecognizedFields:n,succinctRoles:c})}};l1.Delegations=VK;function E4t(t){return Object.entries(t).reduce((e,[r,s])=>({...e,[r]:s.toJSON()}),{})}function I4t(t){return Object.values(t).map(e=>e.toJSON())}function C4t(t){if(!XO.guard.isObjectRecord(t))throw new TypeError("keys is malformed");return Object.entries(t).reduce((e,[r,s])=>({...e,[r]:y4t.Key.fromJSON(r,s)}),{})}function w4t(t){let e;if(XO.guard.isDefined(t)){if(!XO.guard.isObjectArray(t))throw new TypeError("roles is malformed");e=t.reduce((r,s)=>{let a=YK.DelegatedRole.fromJSON(s);return{...r,[a.name]:a}},{})}return e}});var zK=L(c1=>{"use strict";var B4t=c1&&c1.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(c1,"__esModule",{value:!0});c1.Targets=void 0;var _Fe=B4t(Ie("util")),KK=oy(),v4t=MFe(),S4t=Hb(),$O=Af(),JK=class t extends KK.Signed{constructor(e){super(e),this.type=KK.MetadataKind.Targets,this.targets=e.targets||{},this.delegations=e.delegations}addTarget(e){this.targets[e.path]=e}equals(e){return e instanceof t?super.equals(e)&&_Fe.default.isDeepStrictEqual(this.targets,e.targets)&&_Fe.default.isDeepStrictEqual(this.delegations,e.delegations):!1}toJSON(){let e={_type:this.type,spec_version:this.specVersion,version:this.version,expires:this.expires,targets:D4t(this.targets),...this.unrecognizedFields};return this.delegations&&(e.delegations=this.delegations.toJSON()),e}static fromJSON(e){let{unrecognizedFields:r,...s}=KK.Signed.commonFieldsFromJSON(e),{targets:a,delegations:n,...c}=r;return new t({...s,targets:b4t(a),delegations:P4t(n),unrecognizedFields:c})}};c1.Targets=JK;function D4t(t){return Object.entries(t).reduce((e,[r,s])=>({...e,[r]:s.toJSON()}),{})}function b4t(t){let e;if($O.guard.isDefined(t))if($O.guard.isObjectRecord(t))e=Object.entries(t).reduce((r,[s,a])=>({...r,[s]:S4t.TargetFile.fromJSON(s,a)}),{});else throw new TypeError("targets must be an object");return e}function P4t(t){let e;if($O.guard.isDefined(t))if($O.guard.isObject(t))e=v4t.Delegations.fromJSON(t);else throw new TypeError("delegations must be an object");return e}});var eJ=L(eL=>{"use strict";Object.defineProperty(eL,"__esModule",{value:!0});eL.Timestamp=void 0;var ZK=oy(),UFe=Hb(),XK=Af(),$K=class t extends ZK.Signed{constructor(e){super(e),this.type=ZK.MetadataKind.Timestamp,this.snapshotMeta=e.snapshotMeta||new UFe.MetaFile({version:1})}equals(e){return e instanceof t?super.equals(e)&&this.snapshotMeta.equals(e.snapshotMeta):!1}toJSON(){return{_type:this.type,spec_version:this.specVersion,version:this.version,expires:this.expires,meta:{"snapshot.json":this.snapshotMeta.toJSON()},...this.unrecognizedFields}}static fromJSON(e){let{unrecognizedFields:r,...s}=ZK.Signed.commonFieldsFromJSON(e),{meta:a,...n}=r;return new t({...s,snapshotMeta:x4t(a),unrecognizedFields:n})}};eL.Timestamp=$K;function x4t(t){let e;if(XK.guard.isDefined(t)){let r=t["snapshot.json"];if(!XK.guard.isDefined(r)||!XK.guard.isObject(r))throw new TypeError("missing snapshot.json in meta");e=UFe.MetaFile.fromJSON(r)}return e}});var jFe=L(f1=>{"use strict";var k4t=f1&&f1.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(f1,"__esModule",{value:!0});f1.Metadata=void 0;var Q4t=EK(),HFe=k4t(Ie("util")),u1=oy(),Kb=kA(),T4t=UK(),R4t=jK(),F4t=WK(),N4t=zK(),O4t=eJ(),tJ=Af(),rJ=class t{constructor(e,r,s){this.signed=e,this.signatures=r||{},this.unrecognizedFields=s||{}}sign(e,r=!0){let s=Buffer.from((0,Q4t.canonicalize)(this.signed.toJSON())),a=e(s);r||(this.signatures={}),this.signatures[a.keyID]=a}verifyDelegate(e,r){let s,a={};switch(this.signed.type){case u1.MetadataKind.Root:a=this.signed.keys,s=this.signed.roles[e];break;case u1.MetadataKind.Targets:if(!this.signed.delegations)throw new Kb.ValueError(`No delegations found for ${e}`);a=this.signed.delegations.keys,this.signed.delegations.roles?s=this.signed.delegations.roles[e]:this.signed.delegations.succinctRoles&&this.signed.delegations.succinctRoles.isDelegatedRole(e)&&(s=this.signed.delegations.succinctRoles);break;default:throw new TypeError("invalid metadata type")}if(!s)throw new Kb.ValueError(`no delegation found for ${e}`);let n=new Set;if(s.keyIDs.forEach(c=>{let f=a[c];if(f)try{f.verifySignature(r),n.add(f.keyID)}catch{}}),n.sizer.toJSON()),signed:this.signed.toJSON(),...this.unrecognizedFields}}static fromJSON(e,r){let{signed:s,signatures:a,...n}=r;if(!tJ.guard.isDefined(s)||!tJ.guard.isObject(s))throw new TypeError("signed is not defined");if(e!==s._type)throw new Kb.ValueError(`expected '${e}', got ${s._type}`);if(!tJ.guard.isObjectArray(a))throw new TypeError("signatures is not an array");let c;switch(e){case u1.MetadataKind.Root:c=T4t.Root.fromJSON(s);break;case u1.MetadataKind.Timestamp:c=O4t.Timestamp.fromJSON(s);break;case u1.MetadataKind.Snapshot:c=F4t.Snapshot.fromJSON(s);break;case u1.MetadataKind.Targets:c=N4t.Targets.fromJSON(s);break;default:throw new TypeError("invalid metadata type")}let f={};return a.forEach(p=>{let h=R4t.Signature.fromJSON(p);if(f[h.keyID])throw new Kb.ValueError(`multiple signatures found for keyid: ${h.keyID}`);f[h.keyID]=h}),new t(c,f,n)}};f1.Metadata=rJ});var tL=L(Ni=>{"use strict";Object.defineProperty(Ni,"__esModule",{value:!0});Ni.Timestamp=Ni.Targets=Ni.Snapshot=Ni.Signature=Ni.Root=Ni.Metadata=Ni.Key=Ni.TargetFile=Ni.MetaFile=Ni.ValueError=Ni.MetadataKind=void 0;var L4t=oy();Object.defineProperty(Ni,"MetadataKind",{enumerable:!0,get:function(){return L4t.MetadataKind}});var M4t=kA();Object.defineProperty(Ni,"ValueError",{enumerable:!0,get:function(){return M4t.ValueError}});var qFe=Hb();Object.defineProperty(Ni,"MetaFile",{enumerable:!0,get:function(){return qFe.MetaFile}});Object.defineProperty(Ni,"TargetFile",{enumerable:!0,get:function(){return qFe.TargetFile}});var _4t=OO();Object.defineProperty(Ni,"Key",{enumerable:!0,get:function(){return _4t.Key}});var U4t=jFe();Object.defineProperty(Ni,"Metadata",{enumerable:!0,get:function(){return U4t.Metadata}});var H4t=UK();Object.defineProperty(Ni,"Root",{enumerable:!0,get:function(){return H4t.Root}});var j4t=jK();Object.defineProperty(Ni,"Signature",{enumerable:!0,get:function(){return j4t.Signature}});var q4t=WK();Object.defineProperty(Ni,"Snapshot",{enumerable:!0,get:function(){return q4t.Snapshot}});var G4t=zK();Object.defineProperty(Ni,"Targets",{enumerable:!0,get:function(){return G4t.Targets}});var W4t=eJ();Object.defineProperty(Ni,"Timestamp",{enumerable:!0,get:function(){return W4t.Timestamp}})});var WFe=L((WCr,GFe)=>{var A1=1e3,p1=A1*60,h1=p1*60,cy=h1*24,Y4t=cy*7,V4t=cy*365.25;GFe.exports=function(t,e){e=e||{};var r=typeof t;if(r==="string"&&t.length>0)return K4t(t);if(r==="number"&&isFinite(t))return e.long?z4t(t):J4t(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))};function K4t(t){if(t=String(t),!(t.length>100)){var e=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(t);if(e){var r=parseFloat(e[1]),s=(e[2]||"ms").toLowerCase();switch(s){case"years":case"year":case"yrs":case"yr":case"y":return r*V4t;case"weeks":case"week":case"w":return r*Y4t;case"days":case"day":case"d":return r*cy;case"hours":case"hour":case"hrs":case"hr":case"h":return r*h1;case"minutes":case"minute":case"mins":case"min":case"m":return r*p1;case"seconds":case"second":case"secs":case"sec":case"s":return r*A1;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return r;default:return}}}}function J4t(t){var e=Math.abs(t);return e>=cy?Math.round(t/cy)+"d":e>=h1?Math.round(t/h1)+"h":e>=p1?Math.round(t/p1)+"m":e>=A1?Math.round(t/A1)+"s":t+"ms"}function z4t(t){var e=Math.abs(t);return e>=cy?rL(t,e,cy,"day"):e>=h1?rL(t,e,h1,"hour"):e>=p1?rL(t,e,p1,"minute"):e>=A1?rL(t,e,A1,"second"):t+" ms"}function rL(t,e,r,s){var a=e>=r*1.5;return Math.round(t/r)+" "+s+(a?"s":"")}});var nJ=L((YCr,YFe)=>{function Z4t(t){r.debug=r,r.default=r,r.coerce=p,r.disable=c,r.enable=a,r.enabled=f,r.humanize=WFe(),r.destroy=h,Object.keys(t).forEach(E=>{r[E]=t[E]}),r.names=[],r.skips=[],r.formatters={};function e(E){let C=0;for(let S=0;S{if(ce==="%%")return"%";ie++;let pe=r.formatters[me];if(typeof pe=="function"){let Be=N[ie];ce=pe.call(U,Be),N.splice(ie,1),ie--}return ce}),r.formatArgs.call(U,N),(U.log||r.log).apply(U,N)}return R.namespace=E,R.useColors=r.useColors(),R.color=r.selectColor(E),R.extend=s,R.destroy=r.destroy,Object.defineProperty(R,"enabled",{enumerable:!0,configurable:!1,get:()=>S!==null?S:(P!==r.namespaces&&(P=r.namespaces,I=r.enabled(E)),I),set:N=>{S=N}}),typeof r.init=="function"&&r.init(R),R}function s(E,C){let S=r(this.namespace+(typeof C>"u"?":":C)+E);return S.log=this.log,S}function a(E){r.save(E),r.namespaces=E,r.names=[],r.skips=[];let C=(typeof E=="string"?E:"").trim().replace(" ",",").split(",").filter(Boolean);for(let S of C)S[0]==="-"?r.skips.push(S.slice(1)):r.names.push(S)}function n(E,C){let S=0,P=0,I=-1,R=0;for(;S"-"+C)].join(",");return r.enable(""),E}function f(E){for(let C of r.skips)if(n(E,C))return!1;for(let C of r.names)if(n(E,C))return!0;return!1}function p(E){return E instanceof Error?E.stack||E.message:E}function h(){console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.")}return r.enable(r.load()),r}YFe.exports=Z4t});var VFe=L((oc,nL)=>{oc.formatArgs=$4t;oc.save=e3t;oc.load=t3t;oc.useColors=X4t;oc.storage=r3t();oc.destroy=(()=>{let t=!1;return()=>{t||(t=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})();oc.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"];function X4t(){if(typeof window<"u"&&window.process&&(window.process.type==="renderer"||window.process.__nwjs))return!0;if(typeof navigator<"u"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;let t;return typeof document<"u"&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||typeof window<"u"&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||typeof navigator<"u"&&navigator.userAgent&&(t=navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/))&&parseInt(t[1],10)>=31||typeof navigator<"u"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)}function $4t(t){if(t[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+t[0]+(this.useColors?"%c ":" ")+"+"+nL.exports.humanize(this.diff),!this.useColors)return;let e="color: "+this.color;t.splice(1,0,e,"color: inherit");let r=0,s=0;t[0].replace(/%[a-zA-Z%]/g,a=>{a!=="%%"&&(r++,a==="%c"&&(s=r))}),t.splice(s,0,e)}oc.log=console.debug||console.log||(()=>{});function e3t(t){try{t?oc.storage.setItem("debug",t):oc.storage.removeItem("debug")}catch{}}function t3t(){let t;try{t=oc.storage.getItem("debug")}catch{}return!t&&typeof process<"u"&&"env"in process&&(t=process.env.DEBUG),t}function r3t(){try{return localStorage}catch{}}nL.exports=nJ()(oc);var{formatters:n3t}=nL.exports;n3t.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}});var JFe=L((eo,sL)=>{var i3t=Ie("tty"),iL=Ie("util");eo.init=f3t;eo.log=l3t;eo.formatArgs=o3t;eo.save=c3t;eo.load=u3t;eo.useColors=s3t;eo.destroy=iL.deprecate(()=>{},"Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.");eo.colors=[6,2,3,4,5,1];try{let t=Ie("supports-color");t&&(t.stderr||t).level>=2&&(eo.colors=[20,21,26,27,32,33,38,39,40,41,42,43,44,45,56,57,62,63,68,69,74,75,76,77,78,79,80,81,92,93,98,99,112,113,128,129,134,135,148,149,160,161,162,163,164,165,166,167,168,169,170,171,172,173,178,179,184,185,196,197,198,199,200,201,202,203,204,205,206,207,208,209,214,215,220,221])}catch{}eo.inspectOpts=Object.keys(process.env).filter(t=>/^debug_/i.test(t)).reduce((t,e)=>{let r=e.substring(6).toLowerCase().replace(/_([a-z])/g,(a,n)=>n.toUpperCase()),s=process.env[e];return/^(yes|on|true|enabled)$/i.test(s)?s=!0:/^(no|off|false|disabled)$/i.test(s)?s=!1:s==="null"?s=null:s=Number(s),t[r]=s,t},{});function s3t(){return"colors"in eo.inspectOpts?!!eo.inspectOpts.colors:i3t.isatty(process.stderr.fd)}function o3t(t){let{namespace:e,useColors:r}=this;if(r){let s=this.color,a="\x1B[3"+(s<8?s:"8;5;"+s),n=` ${a};1m${e} \x1B[0m`;t[0]=n+t[0].split(` `).join(` `+n),t.push(a+"m+"+sL.exports.humanize(this.diff)+"\x1B[0m")}else t[0]=a3t()+e+" "+t[0]}function a3t(){return eo.inspectOpts.hideDate?"":new Date().toISOString()+" "}function l3t(...t){return process.stderr.write(iL.formatWithOptions(eo.inspectOpts,...t)+` `)}function c3t(t){t?process.env.DEBUG=t:delete process.env.DEBUG}function u3t(){return process.env.DEBUG}function f3t(t){t.inspectOpts={};let e=Object.keys(eo.inspectOpts);for(let r=0;re.trim()).join(" ")};KFe.O=function(t){return this.inspectOpts.colors=this.useColors,iL.inspect(t,this.inspectOpts)}});var sJ=L((VCr,iJ)=>{typeof process>"u"||process.type==="renderer"||process.browser===!0||process.__nwjs?iJ.exports=VFe():iJ.exports=JFe()});var aL=L(zi=>{"use strict";Object.defineProperty(zi,"__esModule",{value:!0});zi.DownloadHTTPError=zi.DownloadLengthMismatchError=zi.DownloadError=zi.ExpiredMetadataError=zi.EqualVersionError=zi.BadVersionError=zi.RepositoryError=zi.PersistError=zi.RuntimeError=zi.ValueError=void 0;var oJ=class extends Error{};zi.ValueError=oJ;var aJ=class extends Error{};zi.RuntimeError=aJ;var lJ=class extends Error{};zi.PersistError=lJ;var Jb=class extends Error{};zi.RepositoryError=Jb;var oL=class extends Jb{};zi.BadVersionError=oL;var cJ=class extends oL{};zi.EqualVersionError=cJ;var uJ=class extends Jb{};zi.ExpiredMetadataError=uJ;var zb=class extends Error{};zi.DownloadError=zb;var fJ=class extends zb{};zi.DownloadLengthMismatchError=fJ;var AJ=class extends zb{constructor(e,r){super(e),this.statusCode=r}};zi.DownloadHTTPError=AJ});var ZFe=L(g1=>{"use strict";var hJ=g1&&g1.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(g1,"__esModule",{value:!0});g1.withTempFile=void 0;var pJ=hJ(Ie("fs/promises")),A3t=hJ(Ie("os")),zFe=hJ(Ie("path")),p3t=async t=>h3t(async e=>t(zFe.default.join(e,"tempfile")));g1.withTempFile=p3t;var h3t=async t=>{let e=await pJ.default.realpath(A3t.default.tmpdir()),r=await pJ.default.mkdtemp(e+zFe.default.sep);try{return await t(r)}finally{await pJ.default.rm(r,{force:!0,recursive:!0,maxRetries:3})}}});var dJ=L(xg=>{"use strict";var cL=xg&&xg.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(xg,"__esModule",{value:!0});xg.DefaultFetcher=xg.BaseFetcher=void 0;var g3t=cL(sJ()),XFe=cL(Ie("fs")),d3t=cL(wO()),m3t=cL(Ie("util")),$Fe=aL(),y3t=ZFe(),E3t=(0,g3t.default)("tuf:fetch"),lL=class{async downloadFile(e,r,s){return(0,y3t.withTempFile)(async a=>{let n=await this.fetch(e),c=0,f=XFe.default.createWriteStream(a);try{for await(let p of n){let h=Buffer.from(p);if(c+=h.length,c>r)throw new $Fe.DownloadLengthMismatchError("Max length reached");await I3t(f,h)}}finally{await m3t.default.promisify(f.close).bind(f)()}return s(a)})}async downloadBytes(e,r){return this.downloadFile(e,r,async s=>{let a=XFe.default.createReadStream(s),n=[];for await(let c of a)n.push(c);return Buffer.concat(n)})}};xg.BaseFetcher=lL;var gJ=class extends lL{constructor(e={}){super(),this.timeout=e.timeout,this.retry=e.retry}async fetch(e){E3t("GET %s",e);let r=await(0,d3t.default)(e,{timeout:this.timeout,retry:this.retry});if(!r.ok||!r?.body)throw new $Fe.DownloadHTTPError("Failed to download",r.status);return r.body}};xg.DefaultFetcher=gJ;var I3t=async(t,e)=>new Promise((r,s)=>{t.write(e,a=>{a&&s(a),r(!0)})})});var eNe=L(uL=>{"use strict";Object.defineProperty(uL,"__esModule",{value:!0});uL.defaultConfig=void 0;uL.defaultConfig={maxRootRotations:256,maxDelegations:32,rootMaxLength:512e3,timestampMaxLength:16384,snapshotMaxLength:2e6,targetsMaxLength:5e6,prefixTargetsWithHash:!0,fetchTimeout:1e5,fetchRetries:void 0,fetchRetry:2}});var tNe=L(fL=>{"use strict";Object.defineProperty(fL,"__esModule",{value:!0});fL.TrustedMetadataStore=void 0;var Cs=tL(),Hi=aL(),mJ=class{constructor(e){this.trustedSet={},this.referenceTime=new Date,this.loadTrustedRoot(e)}get root(){if(!this.trustedSet.root)throw new ReferenceError("No trusted root metadata");return this.trustedSet.root}get timestamp(){return this.trustedSet.timestamp}get snapshot(){return this.trustedSet.snapshot}get targets(){return this.trustedSet.targets}getRole(e){return this.trustedSet[e]}updateRoot(e){let r=JSON.parse(e.toString("utf8")),s=Cs.Metadata.fromJSON(Cs.MetadataKind.Root,r);if(s.signed.type!=Cs.MetadataKind.Root)throw new Hi.RepositoryError(`Expected 'root', got ${s.signed.type}`);if(this.root.verifyDelegate(Cs.MetadataKind.Root,s),s.signed.version!=this.root.signed.version+1)throw new Hi.BadVersionError(`Expected version ${this.root.signed.version+1}, got ${s.signed.version}`);return s.verifyDelegate(Cs.MetadataKind.Root,s),this.trustedSet.root=s,s}updateTimestamp(e){if(this.snapshot)throw new Hi.RuntimeError("Cannot update timestamp after snapshot");if(this.root.signed.isExpired(this.referenceTime))throw new Hi.ExpiredMetadataError("Final root.json is expired");let r=JSON.parse(e.toString("utf8")),s=Cs.Metadata.fromJSON(Cs.MetadataKind.Timestamp,r);if(s.signed.type!=Cs.MetadataKind.Timestamp)throw new Hi.RepositoryError(`Expected 'timestamp', got ${s.signed.type}`);if(this.root.verifyDelegate(Cs.MetadataKind.Timestamp,s),this.timestamp){if(s.signed.version{let p=n.signed.meta[c];if(!p)throw new Hi.RepositoryError(`Missing file ${c} in new snapshot`);if(p.version{"use strict";Object.defineProperty(yJ,"__esModule",{value:!0});yJ.join=w3t;var C3t=Ie("url");function w3t(t,e){return new C3t.URL(B3t(t)+v3t(e)).toString()}function B3t(t){return t.endsWith("/")?t:t+"/"}function v3t(t){return t.startsWith("/")?t.slice(1):t}});var nNe=L(su=>{"use strict";var S3t=su&&su.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(e,r);(!a||("get"in a?!e.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,s,a)}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),D3t=su&&su.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),CJ=su&&su.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(t!=null)for(var r in t)r!=="default"&&Object.prototype.hasOwnProperty.call(t,r)&&S3t(e,t,r);return D3t(e,t),e},b3t=su&&su.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(su,"__esModule",{value:!0});su.Updater=void 0;var QA=tL(),P3t=b3t(sJ()),d1=CJ(Ie("fs")),AL=CJ(Ie("path")),x3t=eNe(),uy=aL(),k3t=dJ(),Q3t=tNe(),Zb=CJ(rNe()),EJ=(0,P3t.default)("tuf:cache"),IJ=class{constructor(e){let{metadataDir:r,metadataBaseUrl:s,targetDir:a,targetBaseUrl:n,fetcher:c,config:f}=e;this.dir=r,this.metadataBaseUrl=s,this.targetDir=a,this.targetBaseUrl=n,this.forceCache=e.forceCache??!1;let p=this.loadLocalMetadata(QA.MetadataKind.Root);this.trustedSet=new Q3t.TrustedMetadataStore(p),this.config={...x3t.defaultConfig,...f},this.fetcher=c||new k3t.DefaultFetcher({timeout:this.config.fetchTimeout,retry:this.config.fetchRetries??this.config.fetchRetry})}async refresh(){if(this.forceCache)try{await this.loadTimestamp({checkRemote:!1})}catch{await this.loadRoot(),await this.loadTimestamp()}else await this.loadRoot(),await this.loadTimestamp();await this.loadSnapshot(),await this.loadTargets(QA.MetadataKind.Targets,QA.MetadataKind.Root)}async getTargetInfo(e){return this.trustedSet.targets||await this.refresh(),this.preorderDepthFirstWalk(e)}async downloadTarget(e,r,s){let a=r||this.generateTargetPath(e);if(!s){if(!this.targetBaseUrl)throw new uy.ValueError("Target base URL not set");s=this.targetBaseUrl}let n=e.path;if(this.trustedSet.root.signed.consistentSnapshot&&this.config.prefixTargetsWithHash){let p=Object.values(e.hashes),{dir:h,base:E}=AL.parse(n),C=`${p[0]}.${E}`;n=h?`${h}/${C}`:C}let f=Zb.join(s,n);return await this.fetcher.downloadFile(f,e.length,async p=>{await e.verify(d1.createReadStream(p)),EJ("WRITE %s",a),d1.copyFileSync(p,a)}),a}async findCachedTarget(e,r){r||(r=this.generateTargetPath(e));try{if(d1.existsSync(r))return await e.verify(d1.createReadStream(r)),r}catch{return}}loadLocalMetadata(e){let r=AL.join(this.dir,`${e}.json`);return EJ("READ %s",r),d1.readFileSync(r)}async loadRoot(){let r=this.trustedSet.root.signed.version+1,s=r+this.config.maxRootRotations;for(let a=r;a0;){let{roleName:a,parentRoleName:n}=r.pop();if(s.has(a))continue;let c=(await this.loadTargets(a,n))?.signed;if(!c)continue;let f=c.targets?.[e];if(f)return f;if(s.add(a),c.delegations){let p=[],h=c.delegations.rolesForTarget(e);for(let{role:E,terminating:C}of h)if(p.push({roleName:E,parentRoleName:a}),C){r.splice(0);break}p.reverse(),r.push(...p)}}}generateTargetPath(e){if(!this.targetDir)throw new uy.ValueError("Target directory not set");let r=encodeURIComponent(e.path);return AL.join(this.targetDir,r)}persistMetadata(e,r){let s=encodeURIComponent(e);try{let a=AL.join(this.dir,`${s}.json`);EJ("WRITE %s",a),d1.writeFileSync(a,r.toString("utf8"))}catch(a){throw new uy.PersistError(`Failed to persist metadata ${s} error: ${a}`)}}};su.Updater=IJ});var iNe=L(kg=>{"use strict";Object.defineProperty(kg,"__esModule",{value:!0});kg.Updater=kg.BaseFetcher=kg.TargetFile=void 0;var T3t=tL();Object.defineProperty(kg,"TargetFile",{enumerable:!0,get:function(){return T3t.TargetFile}});var R3t=dJ();Object.defineProperty(kg,"BaseFetcher",{enumerable:!0,get:function(){return R3t.BaseFetcher}});var F3t=nNe();Object.defineProperty(kg,"Updater",{enumerable:!0,get:function(){return F3t.Updater}})});var BJ=L(pL=>{"use strict";Object.defineProperty(pL,"__esModule",{value:!0});pL.TUFError=void 0;var wJ=class extends Error{constructor({code:e,message:r,cause:s}){super(r),this.code=e,this.cause=s,this.name=this.constructor.name}};pL.TUFError=wJ});var sNe=L(Xb=>{"use strict";var N3t=Xb&&Xb.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(Xb,"__esModule",{value:!0});Xb.readTarget=L3t;var O3t=N3t(Ie("fs")),hL=BJ();async function L3t(t,e){let r=await M3t(t,e);return new Promise((s,a)=>{O3t.default.readFile(r,"utf-8",(n,c)=>{n?a(new hL.TUFError({code:"TUF_READ_TARGET_ERROR",message:`error reading target ${r}`,cause:n})):s(c)})})}async function M3t(t,e){let r;try{r=await t.getTargetInfo(e)}catch(a){throw new hL.TUFError({code:"TUF_REFRESH_METADATA_ERROR",message:"error refreshing TUF metadata",cause:a})}if(!r)throw new hL.TUFError({code:"TUF_FIND_TARGET_ERROR",message:`target ${e} not found`});let s=await t.findCachedTarget(r);if(!s)try{s=await t.downloadTarget(r)}catch(a){throw new hL.TUFError({code:"TUF_DOWNLOAD_TARGET_ERROR",message:`error downloading target ${s}`,cause:a})}return s}});var oNe=L((iwr,_3t)=>{_3t.exports={"https://tuf-repo-cdn.sigstore.dev":{"root.json":"ewogInNpZ25hdHVyZXMiOiBbCiAgewogICAia2V5aWQiOiAiNmYyNjAwODlkNTkyM2RhZjIwMTY2Y2E2NTdjNTQzYWY2MTgzNDZhYjk3MTg4NGE5OTk2MmIwMTk4OGJiZTBjMyIsCiAgICJzaWciOiAiMzA0NjAyMjEwMDhhYjFmNmYxN2Q0ZjllNmQ3ZGNmMWM4ODkxMmI2YjUzY2MxMDM4ODY0NGFlMWYwOWJjMzdhMDgyY2QwNjAwM2UwMjIxMDBlMTQ1ZWY0YzdiNzgyZDRlODEwN2I1MzQzN2U2NjlkMDQ3Njg5MmNlOTk5OTAzYWUzM2QxNDQ0ODM2Njk5NmU3IgogIH0sCiAgewogICAia2V5aWQiOiAiZTcxYTU0ZDU0MzgzNWJhODZhZGFkOTQ2MDM3OWM3NjQxZmI4NzI2ZDE2NGVhNzY2ODAxYTFjNTIyYWJhN2VhMiIsCiAgICJzaWciOiAiMzA0NTAyMjEwMGM3NjhiMmY4NmRhOTk1NjkwMTljMTYwYTA4MWRhNTRhZTM2YzM0YzBhMzEyMGQzY2I2OWI1M2I3ZDExMzc1OGUwMjIwNGY2NzE1MThmNjE3YjIwZDQ2NTM3ZmFlNmMzYjYzYmFlODkxM2Y0ZjE5NjIxNTYxMDVjYzRmMDE5YWMzNWM2YSIKICB9LAogIHsKICAgImtleWlkIjogIjIyZjRjYWVjNmQ4ZTZmOTU1NWFmNjZiM2Q0YzNjYjA2YTNiYjIzZmRjN2UzOWM5MTZjNjFmNDYyZTZmNTJiMDYiLAogICAic2lnIjogIjMwNDUwMjIxMDBiNDQzNGU2OTk1ZDM2OGQyM2U3NDc1OWFjZDBjYjkwMTNjODNhNWQzNTExZjBmOTk3ZWM1NGM0NTZhZTQzNTBhMDIyMDE1YjBlMjY1ZDE4MmQyYjYxZGM3NGUxNTVkOThiM2MzZmJlNTY0YmEwNTI4NmFhMTRjOGRmMDJjOWI3NTY1MTYiCiAgfSwKICB7CiAgICJrZXlpZCI6ICI2MTY0MzgzODEyNWI0NDBiNDBkYjY5NDJmNWNiNWEzMWMwZGMwNDM2ODMxNmViMmFhYTU4Yjk1OTA0YTU4MjIyIiwKICAgInNpZyI6ICIzMDQ1MDIyMTAwODJjNTg0MTFkOTg5ZWI5Zjg2MTQxMDg1N2Q0MjM4MTU5MGVjOTQyNGRiZGFhNTFlNzhlZDEzNTE1NDMxOTA0ZTAyMjAxMTgxODVkYTZhNmMyOTQ3MTMxYzE3Nzk3ZTJiYjc2MjBjZTI2ZTVmMzAxZDFjZWFjNWYyYTdlNThmOWRjZjJlIgogIH0sCiAgewogICAia2V5aWQiOiAiYTY4N2U1YmY0ZmFiODJiMGVlNThkNDZlMDVjOTUzNTE0NWEyYzlhZmI0NThmNDNkNDJiNDVjYTBmZGNlMmE3MCIsCiAgICJzaWciOiAiMzA0NjAyMjEwMGM3ODUxMzg1NGNhZTljMzJlYWE2Yjg4ZTE4OTEyZjQ4MDA2YzI3NTdhMjU4ZjkxNzMxMmNhYmE3NTk0OGViOWUwMjIxMDBkOWUxYjRjZTBhZGZlOWZkMmUyMTQ4ZDdmYTI3YTJmNDBiYTExMjJiZDY5ZGE3NjEyZDhkMTc3NmIwMTNjOTFkIgogIH0sCiAgewogICAia2V5aWQiOiAiZmRmYTgzYTA3YjVhODM1ODliODdkZWQ0MWY3N2YzOWQyMzJhZDkxZjdjY2U1Mjg2OGRhY2QwNmJhMDg5ODQ5ZiIsCiAgICJzaWciOiAiMzA0NTAyMjA1NjQ4M2EyZDVkOWVhOWNlYzZlMTFlYWRmYjMzYzQ4NGI2MTQyOThmYWNhMTVhY2YxYzQzMWIxMWVkN2Y3MzRjMDIyMTAwZDBjMWQ3MjZhZjkyYTg3ZTRlNjY0NTljYTVhZGYzOGEwNWI0NGUxZjk0MzE4NDIzZjk1NGJhZThiY2E1YmIyZSIKICB9LAogIHsKICAgImtleWlkIjogImUyZjU5YWNiOTQ4ODUxOTQwN2UxOGNiZmM5MzI5NTEwYmUwM2MwNGFjYTk5MjlkMmYwMzAxMzQzZmVjODU1MjMiLAogICAic2lnIjogIjMwNDYwMjIxMDBkMDA0ZGU4ODAyNGMzMmRjNTY1M2E5ZjQ4NDNjZmM1MjE1NDI3MDQ4YWQ5NjAwZDJjZjljOTY5ZTZlZGZmM2QyMDIyMTAwZDllYmI3OThmNWZjNjZhZjEwODk5ZGVjZTAxNGE4NjI4Y2NmM2M1NDAyY2Q0YTQyNzAyMDc0NzJmOGY2ZTcxMiIKICB9LAogIHsKICAgImtleWlkIjogIjNjMzQ0YWEwNjhmZDRjYzRlODdkYzUwYjYxMmMwMjQzMWZiYzc3MWU5NTAwMzk5MzY4M2EyYjBiZjI2MGNmMGUiLAogICAic2lnIjogIjMwNDYwMjIxMDBiN2IwOTk5NmM0NWNhMmQ0YjA1NjAzZTU2YmFlZmEyOTcxOGEwYjcxMTQ3Y2Y4YzZlNjYzNDliYWE2MTQ3N2RmMDIyMTAwYzRkYTgwYzcxN2I0ZmE3YmJhMGZkNWM3MmRhOGEwNDk5MzU4YjAxMzU4YjIzMDlmNDFkMTQ1NmVhMWU3ZTFkOSIKICB9LAogIHsKICAgImtleWlkIjogImVjODE2Njk3MzRlMDE3OTk2YzViODVmM2QwMmMzZGUxZGQ0NjM3YTE1MjAxOWZlMWFmMTI1ZDJmOTM2OGI5NWUiLAogICAic2lnIjogIjMwNDYwMjIxMDBiZTk3ODJjMzA3NDRlNDExYTgyZmE4NWI1MTM4ZDYwMWNlMTQ4YmMxOTI1OGFlYzY0ZTdlYzI0NDc4ZjM4ODEyMDIyMTAwY2FlZjYzZGNhZjFhNGI5YTUwMGQzYmQwZTNmMTY0ZWMxOGYxYjYzZDdhOTQ2MGQ5YWNhYjEwNjZkYjBmMDE2ZCIKICB9LAogIHsKICAgImtleWlkIjogIjFlMWQ2NWNlOThiMTBhZGRhZDQ3NjRmZWJmN2RkYTJkMDQzNmIzZDNhMzg5MzU3OWMwZGRkYWVhMjBlNTQ4NDkiLAogICAic2lnIjogIjMwNDUwMjIwNzQ2ZWMzZjg1MzRjZTU1NTMxZDBkMDFmZjY0OTY0ZWY0NDBkMWU3ZDJjNGMxNDI0MDliOGU5NzY5ZjFhZGE2ZjAyMjEwMGUzYjkyOWZjZDkzZWExOGZlYWEwODI1ODg3YTcyMTA0ODk4NzlhNjY3ODBjMDdhODNmNGJkNDZlMmYwOWFiM2IiCiAgfQogXSwKICJzaWduZWQiOiB7CiAgIl90eXBlIjogInJvb3QiLAogICJjb25zaXN0ZW50X3NuYXBzaG90IjogdHJ1ZSwKICAiZXhwaXJlcyI6ICIyMDI1LTAyLTE5VDA4OjA0OjMyWiIsCiAgImtleXMiOiB7CiAgICIyMmY0Y2FlYzZkOGU2Zjk1NTVhZjY2YjNkNGMzY2IwNmEzYmIyM2ZkYzdlMzljOTE2YzYxZjQ2MmU2ZjUyYjA2IjogewogICAgImtleWlkX2hhc2hfYWxnb3JpdGhtcyI6IFsKICAgICAic2hhMjU2IiwKICAgICAic2hhNTEyIgogICAgXSwKICAgICJrZXl0eXBlIjogImVjZHNhIiwKICAgICJrZXl2YWwiOiB7CiAgICAgInB1YmxpYyI6ICItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFekJ6Vk9tSENQb2pNVkxTSTM2NFdpaVY4TlByRFxuNklnUnhWbGlza3ovdit5M0pFUjVtY1ZHY09ObGlEY1dNQzVKMmxmSG1qUE5QaGI0SDd4bThMemZTQT09XG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS1cbiIKICAgIH0sCiAgICAic2NoZW1lIjogImVjZHNhLXNoYTItbmlzdHAyNTYiLAogICAgIngtdHVmLW9uLWNpLWtleW93bmVyIjogIkBzYW50aWFnb3RvcnJlcyIKICAgfSwKICAgIjYxNjQzODM4MTI1YjQ0MGI0MGRiNjk0MmY1Y2I1YTMxYzBkYzA0MzY4MzE2ZWIyYWFhNThiOTU5MDRhNTgyMjIiOiB7CiAgICAia2V5aWRfaGFzaF9hbGdvcml0aG1zIjogWwogICAgICJzaGEyNTYiLAogICAgICJzaGE1MTIiCiAgICBdLAogICAgImtleXR5cGUiOiAiZWNkc2EiLAogICAgImtleXZhbCI6IHsKICAgICAicHVibGljIjogIi0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tXG5NRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVpbmlrU3NBUW1Za05lSDVlWXEvQ25JekxhYWNPXG54bFNhYXdRRE93cUt5L3RDcXhxNXh4UFNKYzIxSzRXSWhzOUd5T2tLZnp1ZVkzR0lMemNNSlo0Y1d3PT1cbi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLVxuIgogICAgfSwKICAgICJzY2hlbWUiOiAiZWNkc2Etc2hhMi1uaXN0cDI1NiIsCiAgICAieC10dWYtb24tY2kta2V5b3duZXIiOiAiQGJvYmNhbGxhd2F5IgogICB9LAogICAiNmYyNjAwODlkNTkyM2RhZjIwMTY2Y2E2NTdjNTQzYWY2MTgzNDZhYjk3MTg4NGE5OTk2MmIwMTk4OGJiZTBjMyI6IHsKICAgICJrZXlpZF9oYXNoX2FsZ29yaXRobXMiOiBbCiAgICAgInNoYTI1NiIsCiAgICAgInNoYTUxMiIKICAgIF0sCiAgICAia2V5dHlwZSI6ICJlY2RzYSIsCiAgICAia2V5dmFsIjogewogICAgICJwdWJsaWMiOiAiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRXk4WEtzbWhCWURJOEpjMEd3ekJ4ZUtheDBjbTVcblNUS0VVNjVIUEZ1blVuNDFzVDhwaTBGak00SWtIei9ZVW13bUxVTzBXdDdseGhqNkJrTElLNHFZQXc9PVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iCiAgICB9LAogICAgInNjaGVtZSI6ICJlY2RzYS1zaGEyLW5pc3RwMjU2IiwKICAgICJ4LXR1Zi1vbi1jaS1rZXlvd25lciI6ICJAZGxvcmVuYyIKICAgfSwKICAgIjcyNDdmMGRiYWQ4NWIxNDdlMTg2M2JhZGU3NjEyNDNjYzc4NWRjYjdhYTQxMGU3MTA1ZGQzZDJiNjFhMzZkMmMiOiB7CiAgICAia2V5aWRfaGFzaF9hbGdvcml0aG1zIjogWwogICAgICJzaGEyNTYiLAogICAgICJzaGE1MTIiCiAgICBdLAogICAgImtleXR5cGUiOiAiZWNkc2EiLAogICAgImtleXZhbCI6IHsKICAgICAicHVibGljIjogIi0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tXG5NRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVXUmlHcjUraiszSjVTc0grWnRyNW5FMkgyd083XG5CVituTzNzOTNnTGNhMThxVE96SFkxb1d5QUdEeWtNU3NHVFVCU3Q5RCtBbjBLZktzRDJtZlNNNDJRPT1cbi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLVxuIgogICAgfSwKICAgICJzY2hlbWUiOiAiZWNkc2Etc2hhMi1uaXN0cDI1NiIsCiAgICAieC10dWYtb24tY2ktb25saW5lLXVyaSI6ICJnY3BrbXM6Ly9wcm9qZWN0cy9zaWdzdG9yZS1yb290LXNpZ25pbmcvbG9jYXRpb25zL2dsb2JhbC9rZXlSaW5ncy9yb290L2NyeXB0b0tleXMvdGltZXN0YW1wIgogICB9LAogICAiYTY4N2U1YmY0ZmFiODJiMGVlNThkNDZlMDVjOTUzNTE0NWEyYzlhZmI0NThmNDNkNDJiNDVjYTBmZGNlMmE3MCI6IHsKICAgICJrZXlpZF9oYXNoX2FsZ29yaXRobXMiOiBbCiAgICAgInNoYTI1NiIsCiAgICAgInNoYTUxMiIKICAgIF0sCiAgICAia2V5dHlwZSI6ICJlY2RzYSIsCiAgICAia2V5dmFsIjogewogICAgICJwdWJsaWMiOiAiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTBnaHJoOTJMdzFZcjNpZEdWNVdxQ3RNREI4Q3hcbitEOGhkQzR3MlpMTklwbFZSb1ZHTHNrWWEzZ2hlTXlPamlKOGtQaTE1YVEyLy83UCtvajdVdkpQR3c9PVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iCiAgICB9LAogICAgInNjaGVtZSI6ICJlY2RzYS1zaGEyLW5pc3RwMjU2IiwKICAgICJ4LXR1Zi1vbi1jaS1rZXlvd25lciI6ICJAam9zaHVhZ2wiCiAgIH0sCiAgICJlNzFhNTRkNTQzODM1YmE4NmFkYWQ5NDYwMzc5Yzc2NDFmYjg3MjZkMTY0ZWE3NjY4MDFhMWM1MjJhYmE3ZWEyIjogewogICAgImtleWlkX2hhc2hfYWxnb3JpdGhtcyI6IFsKICAgICAic2hhMjU2IiwKICAgICAic2hhNTEyIgogICAgXSwKICAgICJrZXl0eXBlIjogImVjZHNhIiwKICAgICJrZXl2YWwiOiB7CiAgICAgInB1YmxpYyI6ICItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFRVhzejNTWlhGYjhqTVY0Mmo2cEpseWpialI4S1xuTjNCd29jZXhxNkxNSWI1cXNXS09RdkxOMTZOVWVmTGM0SHN3T291bVJzVlZhYWpTcFFTNmZvYmtSdz09XG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS1cbiIKICAgIH0sCiAgICAic2NoZW1lIjogImVjZHNhLXNoYTItbmlzdHAyNTYiLAogICAgIngtdHVmLW9uLWNpLWtleW93bmVyIjogIkBtbm02NzgiCiAgIH0KICB9LAogICJyb2xlcyI6IHsKICAgInJvb3QiOiB7CiAgICAia2V5aWRzIjogWwogICAgICI2ZjI2MDA4OWQ1OTIzZGFmMjAxNjZjYTY1N2M1NDNhZjYxODM0NmFiOTcxODg0YTk5OTYyYjAxOTg4YmJlMGMzIiwKICAgICAiZTcxYTU0ZDU0MzgzNWJhODZhZGFkOTQ2MDM3OWM3NjQxZmI4NzI2ZDE2NGVhNzY2ODAxYTFjNTIyYWJhN2VhMiIsCiAgICAgIjIyZjRjYWVjNmQ4ZTZmOTU1NWFmNjZiM2Q0YzNjYjA2YTNiYjIzZmRjN2UzOWM5MTZjNjFmNDYyZTZmNTJiMDYiLAogICAgICI2MTY0MzgzODEyNWI0NDBiNDBkYjY5NDJmNWNiNWEzMWMwZGMwNDM2ODMxNmViMmFhYTU4Yjk1OTA0YTU4MjIyIiwKICAgICAiYTY4N2U1YmY0ZmFiODJiMGVlNThkNDZlMDVjOTUzNTE0NWEyYzlhZmI0NThmNDNkNDJiNDVjYTBmZGNlMmE3MCIKICAgIF0sCiAgICAidGhyZXNob2xkIjogMwogICB9LAogICAic25hcHNob3QiOiB7CiAgICAia2V5aWRzIjogWwogICAgICI3MjQ3ZjBkYmFkODViMTQ3ZTE4NjNiYWRlNzYxMjQzY2M3ODVkY2I3YWE0MTBlNzEwNWRkM2QyYjYxYTM2ZDJjIgogICAgXSwKICAgICJ0aHJlc2hvbGQiOiAxLAogICAgIngtdHVmLW9uLWNpLWV4cGlyeS1wZXJpb2QiOiAzNjUwLAogICAgIngtdHVmLW9uLWNpLXNpZ25pbmctcGVyaW9kIjogMzY1CiAgIH0sCiAgICJ0YXJnZXRzIjogewogICAgImtleWlkcyI6IFsKICAgICAiNmYyNjAwODlkNTkyM2RhZjIwMTY2Y2E2NTdjNTQzYWY2MTgzNDZhYjk3MTg4NGE5OTk2MmIwMTk4OGJiZTBjMyIsCiAgICAgImU3MWE1NGQ1NDM4MzViYTg2YWRhZDk0NjAzNzljNzY0MWZiODcyNmQxNjRlYTc2NjgwMWExYzUyMmFiYTdlYTIiLAogICAgICIyMmY0Y2FlYzZkOGU2Zjk1NTVhZjY2YjNkNGMzY2IwNmEzYmIyM2ZkYzdlMzljOTE2YzYxZjQ2MmU2ZjUyYjA2IiwKICAgICAiNjE2NDM4MzgxMjViNDQwYjQwZGI2OTQyZjVjYjVhMzFjMGRjMDQzNjgzMTZlYjJhYWE1OGI5NTkwNGE1ODIyMiIsCiAgICAgImE2ODdlNWJmNGZhYjgyYjBlZTU4ZDQ2ZTA1Yzk1MzUxNDVhMmM5YWZiNDU4ZjQzZDQyYjQ1Y2EwZmRjZTJhNzAiCiAgICBdLAogICAgInRocmVzaG9sZCI6IDMKICAgfSwKICAgInRpbWVzdGFtcCI6IHsKICAgICJrZXlpZHMiOiBbCiAgICAgIjcyNDdmMGRiYWQ4NWIxNDdlMTg2M2JhZGU3NjEyNDNjYzc4NWRjYjdhYTQxMGU3MTA1ZGQzZDJiNjFhMzZkMmMiCiAgICBdLAogICAgInRocmVzaG9sZCI6IDEsCiAgICAieC10dWYtb24tY2ktZXhwaXJ5LXBlcmlvZCI6IDcsCiAgICAieC10dWYtb24tY2ktc2lnbmluZy1wZXJpb2QiOiA0CiAgIH0KICB9LAogICJzcGVjX3ZlcnNpb24iOiAiMS4wIiwKICAidmVyc2lvbiI6IDEwLAogICJ4LXR1Zi1vbi1jaS1leHBpcnktcGVyaW9kIjogMTgyLAogICJ4LXR1Zi1vbi1jaS1zaWduaW5nLXBlcmlvZCI6IDMxCiB9Cn0=",targets:{"trusted_root.json":"ewogICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLmRldi5zaWdzdG9yZS50cnVzdGVkcm9vdCtqc29uO3ZlcnNpb249MC4xIiwKICAidGxvZ3MiOiBbCiAgICB7CiAgICAgICJiYXNlVXJsIjogImh0dHBzOi8vcmVrb3Iuc2lnc3RvcmUuZGV2IiwKICAgICAgImhhc2hBbGdvcml0aG0iOiAiU0hBMl8yNTYiLAogICAgICAicHVibGljS2V5IjogewogICAgICAgICJyYXdCeXRlcyI6ICJNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUyRzJZKzJ0YWJkVFY1QmNHaUJJeDBhOWZBRndya0JibUxTR3RrczRMM3FYNnlZWTB6dWZCbmhDOFVyL2l5NTVHaFdQLzlBL2JZMkxoQzMwTTkrUll0dz09IiwKICAgICAgICAia2V5RGV0YWlscyI6ICJQS0lYX0VDRFNBX1AyNTZfU0hBXzI1NiIsCiAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgInN0YXJ0IjogIjIwMjEtMDEtMTJUMTE6NTM6MjcuMDAwWiIKICAgICAgICB9CiAgICAgIH0sCiAgICAgICJsb2dJZCI6IHsKICAgICAgICAia2V5SWQiOiAid05JOWF0UUdseitWV2ZPNkxSeWdINFFVZlkvOFc0UkZ3aVQ1aTVXUmdCMD0iCiAgICAgIH0KICAgIH0KICBdLAogICJjZXJ0aWZpY2F0ZUF1dGhvcml0aWVzIjogWwogICAgewogICAgICAic3ViamVjdCI6IHsKICAgICAgICAib3JnYW5pemF0aW9uIjogInNpZ3N0b3JlLmRldiIsCiAgICAgICAgImNvbW1vbk5hbWUiOiAic2lnc3RvcmUiCiAgICAgIH0sCiAgICAgICJ1cmkiOiAiaHR0cHM6Ly9mdWxjaW8uc2lnc3RvcmUuZGV2IiwKICAgICAgImNlcnRDaGFpbiI6IHsKICAgICAgICAiY2VydGlmaWNhdGVzIjogWwogICAgICAgICAgewogICAgICAgICAgICAicmF3Qnl0ZXMiOiAiTUlJQitEQ0NBWDZnQXdJQkFnSVROVmtEWm9DaW9mUERzeTdkZm02Z2VMYnVoekFLQmdncWhrak9QUVFEQXpBcU1SVXdFd1lEVlFRS0V3eHphV2R6ZEc5eVpTNWtaWFl4RVRBUEJnTlZCQU1UQ0hOcFozTjBiM0psTUI0WERUSXhNRE13TnpBek1qQXlPVm9YRFRNeE1ESXlNekF6TWpBeU9Wb3dLakVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SRXdEd1lEVlFRREV3aHphV2R6ZEc5eVpUQjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkxTeUE3SWk1aytwTk84WkVXWTB5bGVtV0Rvd09rTmEza0wrR1pFNVo1R1dlaEw5L0E5YlJOQTNSYnJzWjVpMEpjYXN0YVJMN1NwNWZwL2pENWR4cWMvVWRUVm5sdlMxNmFuKzJZZnN3ZS9RdUxvbFJVQ3JjT0UyKzJpQTUrdHpkNk5tTUdRd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFFd0hRWURWUjBPQkJZRUZNakZIUUJCbWlRcE1sRWs2dzJ1U3UxS0J0UHNNQjhHQTFVZEl3UVlNQmFBRk1qRkhRQkJtaVFwTWxFazZ3MnVTdTFLQnRQc01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01IOGxpV0pmTXVpNnZYWEJoakRnWTRNd3NsbU4vVEp4VmUvODNXckZvbXdtTmYwNTZ5MVg0OEY5YzRtM2Ezb3pYQUl4QUtqUmF5NS9hai9qc0tLR0lrbVFhdGpJOHV1cEhyLytDeEZ2YUpXbXBZcU5rTERHUlUrOW9yemg1aEkyUnJjdWFRPT0iCiAgICAgICAgICB9CiAgICAgICAgXQogICAgICB9LAogICAgICAidmFsaWRGb3IiOiB7CiAgICAgICAgInN0YXJ0IjogIjIwMjEtMDMtMDdUMDM6MjA6MjkuMDAwWiIsCiAgICAgICAgImVuZCI6ICIyMDIyLTEyLTMxVDIzOjU5OjU5Ljk5OVoiCiAgICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJzdWJqZWN0IjogewogICAgICAgICJvcmdhbml6YXRpb24iOiAic2lnc3RvcmUuZGV2IiwKICAgICAgICAiY29tbW9uTmFtZSI6ICJzaWdzdG9yZSIKICAgICAgfSwKICAgICAgInVyaSI6ICJodHRwczovL2Z1bGNpby5zaWdzdG9yZS5kZXYiLAogICAgICAiY2VydENoYWluIjogewogICAgICAgICJjZXJ0aWZpY2F0ZXMiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJyYXdCeXRlcyI6ICJNSUlDR2pDQ0FhR2dBd0lCQWdJVUFMblZpVmZuVTBickphc21Sa0hybi9VbmZhUXdDZ1lJS29aSXpqMEVBd013S2pFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUkV3RHdZRFZRUURFd2h6YVdkemRHOXlaVEFlRncweU1qQTBNVE15TURBMk1UVmFGdzB6TVRFd01EVXhNelUyTlRoYU1EY3hGVEFUQmdOVkJBb1RESE5wWjNOMGIzSmxMbVJsZGpFZU1Cd0dBMVVFQXhNVmMybG5jM1J2Y21VdGFXNTBaWEp0WldScFlYUmxNSFl3RUFZSEtvWkl6ajBDQVFZRks0RUVBQ0lEWWdBRThSVlMveXNIK05PdnVEWnlQSVp0aWxnVUY5TmxhcllwQWQ5SFAxdkJCSDFVNUNWNzdMU1M3czBaaUg0bkU3SHY3cHRTNkx2dlIvU1RrNzk4TFZnTXpMbEo0SGVJZkYzdEhTYWV4TGNZcFNBU3Ixa1MwTi9SZ0JKei85aldDaVhubzNzd2VUQU9CZ05WSFE4QkFmOEVCQU1DQVFZd0V3WURWUjBsQkF3d0NnWUlLd1lCQlFVSEF3TXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBREFkQmdOVkhRNEVGZ1FVMzlQcHoxWWtFWmI1cU5qcEtGV2l4aTRZWkQ4d0h3WURWUjBqQkJnd0ZvQVVXTUFlWDVGRnBXYXBlc3lRb1pNaTBDckZ4Zm93Q2dZSUtvWkl6ajBFQXdNRFp3QXdaQUl3UENzUUs0RFlpWllEUElhRGk1SEZLbmZ4WHg2QVNTVm1FUmZzeW5ZQmlYMlg2U0pSblpVODQvOURaZG5GdnZ4bUFqQk90NlFwQmxjNEovMER4dmtUQ3FwY2x2emlMNkJDQ1BuamRsSUIzUHUzQnhzUG15Z1VZN0lpMnpiZENkbGlpb3c9IgogICAgICAgICAgfSwKICAgICAgICAgIHsKICAgICAgICAgICAgInJhd0J5dGVzIjogIk1JSUI5ekNDQVh5Z0F3SUJBZ0lVQUxaTkFQRmR4SFB3amVEbG9Ed3lZQ2hBTy80d0NnWUlLb1pJemowRUF3TXdLakVWTUJNR0ExVUVDaE1NYzJsbmMzUnZjbVV1WkdWMk1SRXdEd1lEVlFRREV3aHphV2R6ZEc5eVpUQWVGdzB5TVRFd01EY3hNelUyTlRsYUZ3MHpNVEV3TURVeE16VTJOVGhhTUNveEZUQVRCZ05WQkFvVERITnBaM04wYjNKbExtUmxkakVSTUE4R0ExVUVBeE1JYzJsbmMzUnZjbVV3ZGpBUUJnY3Foa2pPUFFJQkJnVXJnUVFBSWdOaUFBVDdYZUZUNHJiM1BRR3dTNElhanRMazMvT2xucGdhbmdhQmNsWXBzWUJyNWkrNHluQjA3Y2ViM0xQME9JT1pkeGV4WDY5YzVpVnV5SlJRK0h6MDV5aStVRjN1QldBbEhwaVM1c2gwK0gyR0hFN1NYcmsxRUM1bTFUcjE5TDlnZzkyall6QmhNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCUll3QjVma1VXbFpxbDZ6SkNoa3lMUUtzWEYrakFmQmdOVkhTTUVHREFXZ0JSWXdCNWZrVVdsWnFsNnpKQ2hreUxRS3NYRitqQUtCZ2dxaGtqT1BRUURBd05wQURCbUFqRUFqMW5IZVhacCsxM05XQk5hK0VEc0RQOEcxV1dnMXRDTVdQL1dIUHFwYVZvMGpoc3dlTkZaZ1NzMGVFN3dZSTRxQWpFQTJXQjlvdDk4c0lrb0YzdlpZZGQzL1Z0V0I1YjlUTk1lYTdJeC9zdEo1VGZjTExlQUJMRTRCTkpPc1E0dm5CSEoiCiAgICAgICAgICB9CiAgICAgICAgXQogICAgICB9LAogICAgICAidmFsaWRGb3IiOiB7CiAgICAgICAgInN0YXJ0IjogIjIwMjItMDQtMTNUMjA6MDY6MTUuMDAwWiIKICAgICAgfQogICAgfQogIF0sCiAgImN0bG9ncyI6IFsKICAgIHsKICAgICAgImJhc2VVcmwiOiAiaHR0cHM6Ly9jdGZlLnNpZ3N0b3JlLmRldi90ZXN0IiwKICAgICAgImhhc2hBbGdvcml0aG0iOiAiU0hBMl8yNTYiLAogICAgICAicHVibGljS2V5IjogewogICAgICAgICJyYXdCeXRlcyI6ICJNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUViZndSK1JKdWRYc2NnUkJScEtYMVhGRHkzUHl1ZER4ei9TZm5SaTFmVDhla3BmQmQyTzF1b3o3anIzWjhuS3p4QTY5RVVRK2VGQ0ZJM3pldWJQV1U3dz09IiwKICAgICAgICAia2V5RGV0YWlscyI6ICJQS0lYX0VDRFNBX1AyNTZfU0hBXzI1NiIsCiAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgInN0YXJ0IjogIjIwMjEtMDMtMTRUMDA6MDA6MDAuMDAwWiIsCiAgICAgICAgICAiZW5kIjogIjIwMjItMTAtMzFUMjM6NTk6NTkuOTk5WiIKICAgICAgICB9CiAgICAgIH0sCiAgICAgICJsb2dJZCI6IHsKICAgICAgICAia2V5SWQiOiAiQ0dDUzhDaFMvMmhGMGRGcko0U2NSV2NZckJZOXd6alNiZWE4SWdZMmIzST0iCiAgICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJiYXNlVXJsIjogImh0dHBzOi8vY3RmZS5zaWdzdG9yZS5kZXYvMjAyMiIsCiAgICAgICJoYXNoQWxnb3JpdGhtIjogIlNIQTJfMjU2IiwKICAgICAgInB1YmxpY0tleSI6IHsKICAgICAgICAicmF3Qnl0ZXMiOiAiTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFaVBTbEZpMENtRlRmRWpDVXFGOUh1Q0VjWVhOS0FhWWFsSUptQlo4eXllelBqVHFoeHJLQnBNbmFvY1Z0TEpCSTFlTTN1WG5RelFHQUpkSjRnczlGeXc9PSIsCiAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICJ2YWxpZEZvciI6IHsKICAgICAgICAgICJzdGFydCI6ICIyMDIyLTEwLTIwVDAwOjAwOjAwLjAwMFoiCiAgICAgICAgfQogICAgICB9LAogICAgICAibG9nSWQiOiB7CiAgICAgICAgImtleUlkIjogIjNUMHdhc2JIRVRKakdSNGNtV2MzQXFKS1hyamVQSzMvaDRweWdDOHA3bzQ9IgogICAgICB9CiAgICB9CiAgXSwKICAidGltZXN0YW1wQXV0aG9yaXRpZXMiOiBbCiAgICB7CiAgICAgICJzdWJqZWN0IjogewogICAgICAgICJvcmdhbml6YXRpb24iOiAiR2l0SHViLCBJbmMuIiwKICAgICAgICAiY29tbW9uTmFtZSI6ICJJbnRlcm5hbCBTZXJ2aWNlcyBSb290IgogICAgICB9LAogICAgICAiY2VydENoYWluIjogewogICAgICAgICJjZXJ0aWZpY2F0ZXMiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJyYXdCeXRlcyI6ICJNSUlCM0RDQ0FXS2dBd0lCQWdJVWNoa05zSDM2WGEwNGIxTHFJYytxcjlEVmVjTXdDZ1lJS29aSXpqMEVBd013TWpFVk1CTUdBMVVFQ2hNTVIybDBTSFZpTENCSmJtTXVNUmt3RndZRFZRUURFeEJVVTBFZ2FXNTBaWEp0WldScFlYUmxNQjRYRFRJek1EUXhOREF3TURBd01Gb1hEVEkwTURReE16QXdNREF3TUZvd01qRVZNQk1HQTFVRUNoTU1SMmwwU0hWaUxDQkpibU11TVJrd0Z3WURWUVFERXhCVVUwRWdWR2x0WlhOMFlXMXdhVzVuTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFVUQ1Wk5iU3FZTWQ2cjhxcE9PRVg5aWJHblpUOUdzdVhPaHIvZjhVOUZKdWdCR0V4S1lwNDBPVUxTMGVyalpXN3hWOXhWNTJObkpmNU9lRHE0ZTVaS3FOV01GUXdEZ1lEVlIwUEFRSC9CQVFEQWdlQU1CTUdBMVVkSlFRTU1Bb0dDQ3NHQVFVRkJ3TUlNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVhVzFSdWRPZ1Z0MGxlcVkwV0tZYnVQcjQ3d0F3Q2dZSUtvWkl6ajBFQXdNRGFBQXdaUUl3YlVIOUh2RDRlakNaSk9XUW5xQWxrcVVSbGx2dTlNOCtWcUxiaVJLK3pTZlpDWndzaWxqUm44TVFRUlNrWEVFNUFqRUFnK1Z4cXRvamZWZnU4RGh6emhDeDlHS0VUYkpIYjE5aVY3Mm1NS1ViREFGbXpaNmJROGI1NFpiOHRpZHk1YVdlIgogICAgICAgICAgfSwKICAgICAgICAgIHsKICAgICAgICAgICAgInJhd0J5dGVzIjogIk1JSUNFRENDQVpXZ0F3SUJBZ0lVWDhaTzVRWFA3dk40ZE1RNWU5c1UzbnViOE9nd0NnWUlLb1pJemowRUF3TXdPREVWTUJNR0ExVUVDaE1NUjJsMFNIVmlMQ0JKYm1NdU1SOHdIUVlEVlFRREV4WkpiblJsY201aGJDQlRaWEoyYVdObGN5QlNiMjkwTUI0WERUSXpNRFF4TkRBd01EQXdNRm9YRFRJNE1EUXhNakF3TURBd01Gb3dNakVWTUJNR0ExVUVDaE1NUjJsMFNIVmlMQ0JKYm1NdU1Sa3dGd1lEVlFRREV4QlVVMEVnYVc1MFpYSnRaV1JwWVhSbE1IWXdFQVlIS29aSXpqMENBUVlGSzRFRUFDSURZZ0FFdk1MWS9kVFZidklKWUFOQXVzekV3Sm5RRTFsbGZ0eW55TUtJTWhoNDhIbXFiVnI1eWd5YnpzTFJMVktiQldPZFoyMWFlSnorZ1ppeXRaZXRxY3lGOVdsRVI1TkVNZjZKVjdaTm9qUXB4SHE0UkhHb0dTY2VRdi9xdlRpWnhFREtvMll3WkRBT0JnTlZIUThCQWY4RUJBTUNBUVl3RWdZRFZSMFRBUUgvQkFnd0JnRUIvd0lCQURBZEJnTlZIUTRFRmdRVWFXMVJ1ZE9nVnQwbGVxWTBXS1lidVByNDd3QXdId1lEVlIwakJCZ3dGb0FVOU5ZWWxvYm5BRzRjMC9xanh5SC9scS93eitRd0NnWUlLb1pJemowRUF3TURhUUF3WmdJeEFLMUIxODV5Z0NySVlGbElzM0dqc3dqbndTTUc2TFk4d29MVmRha0tEWnhWYThmOGNxTXMxRGhjeEowKzA5dzk1UUl4QU8rdEJ6Wms3dmpVSjlpSmdENFI2WldUeFFXS3FObTc0ak85OW8rbzlzdjRGSS9TWlRaVEZ5TW4wSUpFSGRObXlBPT0iCiAgICAgICAgICB9LAogICAgICAgICAgewogICAgICAgICAgICAicmF3Qnl0ZXMiOiAiTUlJQjlEQ0NBWHFnQXdJQkFnSVVhL0pBa2RVaks0SlV3c3F0YWlSSkdXaHFMU293Q2dZSUtvWkl6ajBFQXdNd09ERVZNQk1HQTFVRUNoTU1SMmwwU0hWaUxDQkpibU11TVI4d0hRWURWUVFERXhaSmJuUmxjbTVoYkNCVFpYSjJhV05sY3lCU2IyOTBNQjRYRFRJek1EUXhOREF3TURBd01Gb1hEVE16TURReE1UQXdNREF3TUZvd09ERVZNQk1HQTFVRUNoTU1SMmwwU0hWaUxDQkpibU11TVI4d0hRWURWUVFERXhaSmJuUmxjbTVoYkNCVFpYSjJhV05sY3lCU2IyOTBNSFl3RUFZSEtvWkl6ajBDQVFZRks0RUVBQ0lEWWdBRWY5akZBWHh6NGt4NjhBSFJNT2tGQmhmbERjTVR2emFYejR4L0ZDY1hqSi8xcUVLb24vcVBJR25hVVJza0R0eU5iTkRPcGVKVERERnF0NDhpTVBybnpweDZJWndxZW1mVUpONHhCRVpmemErcFl0L2l5b2QrOXRacjIwUlJXU3YvbzBVd1F6QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0VnWURWUjBUQVFIL0JBZ3dCZ0VCL3dJQkFqQWRCZ05WSFE0RUZnUVU5TllZbG9ibkFHNGMwL3FqeHlIL2xxL3d6K1F3Q2dZSUtvWkl6ajBFQXdNRGFBQXdaUUl4QUxaTFo4QmdSWHpLeExNTU45VklsTytlNGhyQm5OQmdGN3R6N0hucm93djJOZXRaRXJJQUNLRnltQmx2V0R2dE1BSXdaTytraTZzc1ExYnNabzk4TzhtRUFmMk5aN2lpQ2dERFUwVndqZWNvNnp5ZWgwekJUczkvN2dWNkFITlE1M3hEIgogICAgICAgICAgfQogICAgICAgIF0KICAgICAgfSwKICAgICAgInZhbGlkRm9yIjogewogICAgICAgICJzdGFydCI6ICIyMDIzLTA0LTE0VDAwOjAwOjAwLjAwMFoiCiAgICAgIH0KICAgIH0KICBdCn0K","registry.npmjs.org%2Fkeys.json":"ewogICAgImtleXMiOiBbCiAgICAgICAgewogICAgICAgICAgICAia2V5SWQiOiAiU0hBMjU2OmpsM2J3c3d1ODBQampva0NnaDBvMnc1YzJVNExoUUFFNTdnajljejFrekEiLAogICAgICAgICAgICAia2V5VXNhZ2UiOiAibnBtOnNpZ25hdHVyZXMiLAogICAgICAgICAgICAicHVibGljS2V5IjogewogICAgICAgICAgICAgICAgInJhd0J5dGVzIjogIk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTFPbGIzek1BRkZ4WEtIaUlrUU81Y0ozWWhsNWk2VVBwK0lodXRlQkpidUhjQTVVb2dLbzBFV3RsV3dXNktTYUtvVE5FWUw3SmxDUWlWbmtoQmt0VWdnPT0iLAogICAgICAgICAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgICAgICAgICAgICJzdGFydCI6ICIxOTk5LTAxLTAxVDAwOjAwOjAwLjAwMFoiLAogICAgICAgICAgICAgICAgICAgICJlbmQiOiAiMjAyNS0wMS0yOVQwMDowMDowMC4wMDBaIgogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJrZXlJZCI6ICJTSEEyNTY6amwzYndzd3U4MFBqam9rQ2doMG8ydzVjMlU0TGhRQUU1N2dqOWN6MWt6QSIsCiAgICAgICAgICAgICJrZXlVc2FnZSI6ICJucG06YXR0ZXN0YXRpb25zIiwKICAgICAgICAgICAgInB1YmxpY0tleSI6IHsKICAgICAgICAgICAgICAgICJyYXdCeXRlcyI6ICJNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUxT2xiM3pNQUZGeFhLSGlJa1FPNWNKM1lobDVpNlVQcCtJaHV0ZUJKYnVIY0E1VW9nS28wRVd0bFd3VzZLU2FLb1RORVlMN0psQ1FpVm5raEJrdFVnZz09IiwKICAgICAgICAgICAgICAgICJrZXlEZXRhaWxzIjogIlBLSVhfRUNEU0FfUDI1Nl9TSEFfMjU2IiwKICAgICAgICAgICAgICAgICJ2YWxpZEZvciI6IHsKICAgICAgICAgICAgICAgICAgICAic3RhcnQiOiAiMjAyMi0xMi0wMVQwMDowMDowMC4wMDBaIiwKICAgICAgICAgICAgICAgICAgICAiZW5kIjogIjIwMjUtMDEtMjlUMDA6MDA6MDAuMDAwWiIKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAia2V5SWQiOiAiU0hBMjU2OkRoUTh3UjVBUEJ2RkhMRi8rVGMrQVl2UE9kVHBjSURxT2h4c0JIUndDN1UiLAogICAgICAgICAgICAia2V5VXNhZ2UiOiAibnBtOnNpZ25hdHVyZXMiLAogICAgICAgICAgICAicHVibGljS2V5IjogewogICAgICAgICAgICAgICAgInJhd0J5dGVzIjogIk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRVk2WWE3VysrN2FVUHp2TVRyZXpINlljeDNjK0hPS1lDY05HeWJKWlNDSnEvZmQ3UWE4dXVBS3RkSWtVUXRRaUVLRVJoQW1FNWxNTUpoUDhPa0RPYTJnPT0iLAogICAgICAgICAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgICAgICAgICAgICJzdGFydCI6ICIyMDI1LTAxLTEzVDAwOjAwOjAwLjAwMFoiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgImtleUlkIjogIlNIQTI1NjpEaFE4d1I1QVBCdkZITEYvK1RjK0FZdlBPZFRwY0lEcU9oeHNCSFJ3QzdVIiwKICAgICAgICAgICAgImtleVVzYWdlIjogIm5wbTphdHRlc3RhdGlvbnMiLAogICAgICAgICAgICAicHVibGljS2V5IjogewogICAgICAgICAgICAgICAgInJhd0J5dGVzIjogIk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRVk2WWE3VysrN2FVUHp2TVRyZXpINlljeDNjK0hPS1lDY05HeWJKWlNDSnEvZmQ3UWE4dXVBS3RkSWtVUXRRaUVLRVJoQW1FNWxNTUpoUDhPa0RPYTJnPT0iLAogICAgICAgICAgICAgICAgImtleURldGFpbHMiOiAiUEtJWF9FQ0RTQV9QMjU2X1NIQV8yNTYiLAogICAgICAgICAgICAgICAgInZhbGlkRm9yIjogewogICAgICAgICAgICAgICAgICAgICJzdGFydCI6ICIyMDI1LTAxLTEzVDAwOjAwOjAwLjAwMFoiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICBdCn0K"}}}});var lNe=L(m1=>{"use strict";var aNe=m1&&m1.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(m1,"__esModule",{value:!0});m1.TUFClient=void 0;var Qg=aNe(Ie("fs")),$b=aNe(Ie("path")),U3t=iNe(),H3t=gL(),j3t=sNe(),SJ="targets",vJ=class{constructor(e){let r=new URL(e.mirrorURL),s=encodeURIComponent(r.host+r.pathname.replace(/\/$/,"")),a=$b.default.join(e.cachePath,s);q3t(a),G3t({cachePath:a,mirrorURL:e.mirrorURL,tufRootPath:e.rootPath,forceInit:e.forceInit}),this.updater=W3t({mirrorURL:e.mirrorURL,cachePath:a,forceCache:e.forceCache,retry:e.retry,timeout:e.timeout})}async refresh(){return this.updater.refresh()}getTarget(e){return(0,j3t.readTarget)(this.updater,e)}};m1.TUFClient=vJ;function q3t(t){let e=$b.default.join(t,SJ);Qg.default.existsSync(t)||Qg.default.mkdirSync(t,{recursive:!0}),Qg.default.existsSync(e)||Qg.default.mkdirSync(e)}function G3t({cachePath:t,mirrorURL:e,tufRootPath:r,forceInit:s}){let a=$b.default.join(t,"root.json");if(!Qg.default.existsSync(a)||s)if(r)Qg.default.copyFileSync(r,a);else{let c=oNe()[e];if(!c)throw new H3t.TUFError({code:"TUF_INIT_CACHE_ERROR",message:`No root.json found for mirror: ${e}`});Qg.default.writeFileSync(a,Buffer.from(c["root.json"],"base64")),Object.entries(c.targets).forEach(([f,p])=>{Qg.default.writeFileSync($b.default.join(t,SJ,f),Buffer.from(p,"base64"))})}}function W3t(t){let e={fetchTimeout:t.timeout,fetchRetry:t.retry};return new U3t.Updater({metadataBaseUrl:t.mirrorURL,targetBaseUrl:`${t.mirrorURL}/targets`,metadataDir:t.cachePath,targetDir:$b.default.join(t.cachePath,SJ),forceCache:t.forceCache,config:e})}});var gL=L(mh=>{"use strict";Object.defineProperty(mh,"__esModule",{value:!0});mh.TUFError=mh.DEFAULT_MIRROR_URL=void 0;mh.getTrustedRoot=$3t;mh.initTUF=e8t;var Y3t=bb(),V3t=HRe(),K3t=lNe();mh.DEFAULT_MIRROR_URL="https://tuf-repo-cdn.sigstore.dev";var J3t="sigstore-js",z3t={retries:2},Z3t=5e3,X3t="trusted_root.json";async function $3t(t={}){let r=await cNe(t).getTarget(X3t);return Y3t.TrustedRoot.fromJSON(JSON.parse(r))}async function e8t(t={}){let e=cNe(t);return e.refresh().then(()=>e)}function cNe(t){return new K3t.TUFClient({cachePath:t.cachePath||(0,V3t.appDataPath)(J3t),rootPath:t.rootPath,mirrorURL:t.mirrorURL||mh.DEFAULT_MIRROR_URL,retry:t.retry??z3t,timeout:t.timeout??Z3t,forceCache:t.forceCache??!1,forceInit:t.forceInit??t.force??!1})}var t8t=BJ();Object.defineProperty(mh,"TUFError",{enumerable:!0,get:function(){return t8t.TUFError}})});var uNe=L(dL=>{"use strict";Object.defineProperty(dL,"__esModule",{value:!0});dL.DSSESignatureContent=void 0;var eP=wl(),DJ=class{constructor(e){this.env=e}compareDigest(e){return eP.crypto.bufferEqual(e,eP.crypto.digest("sha256",this.env.payload))}compareSignature(e){return eP.crypto.bufferEqual(e,this.signature)}verifySignature(e){return eP.crypto.verify(this.preAuthEncoding,e,this.signature)}get signature(){return this.env.signatures.length>0?this.env.signatures[0].sig:Buffer.from("")}get preAuthEncoding(){return eP.dsse.preAuthEncoding(this.env.payloadType,this.env.payload)}};dL.DSSESignatureContent=DJ});var fNe=L(mL=>{"use strict";Object.defineProperty(mL,"__esModule",{value:!0});mL.MessageSignatureContent=void 0;var bJ=wl(),PJ=class{constructor(e,r){this.signature=e.signature,this.messageDigest=e.messageDigest.digest,this.artifact=r}compareSignature(e){return bJ.crypto.bufferEqual(e,this.signature)}compareDigest(e){return bJ.crypto.bufferEqual(e,this.messageDigest)}verifySignature(e){return bJ.crypto.verify(this.artifact,e,this.signature)}};mL.MessageSignatureContent=PJ});var pNe=L(yL=>{"use strict";Object.defineProperty(yL,"__esModule",{value:!0});yL.toSignedEntity=i8t;yL.signatureContent=ANe;var xJ=wl(),r8t=uNe(),n8t=fNe();function i8t(t,e){let{tlogEntries:r,timestampVerificationData:s}=t.verificationMaterial,a=[];for(let n of r)a.push({$case:"transparency-log",tlogEntry:n});for(let n of s?.rfc3161Timestamps??[])a.push({$case:"timestamp-authority",timestamp:xJ.RFC3161Timestamp.parse(n.signedTimestamp)});return{signature:ANe(t,e),key:s8t(t),tlogEntries:r,timestamps:a}}function ANe(t,e){switch(t.content.$case){case"dsseEnvelope":return new r8t.DSSESignatureContent(t.content.dsseEnvelope);case"messageSignature":return new n8t.MessageSignatureContent(t.content.messageSignature,e)}}function s8t(t){switch(t.verificationMaterial.content.$case){case"publicKey":return{$case:"public-key",hint:t.verificationMaterial.content.publicKey.hint};case"x509CertificateChain":return{$case:"certificate",certificate:xJ.X509Certificate.parse(t.verificationMaterial.content.x509CertificateChain.certificates[0].rawBytes)};case"certificate":return{$case:"certificate",certificate:xJ.X509Certificate.parse(t.verificationMaterial.content.certificate.rawBytes)}}}});var Co=L(y1=>{"use strict";Object.defineProperty(y1,"__esModule",{value:!0});y1.PolicyError=y1.VerificationError=void 0;var EL=class extends Error{constructor({code:e,message:r,cause:s}){super(r),this.code=e,this.cause=s,this.name=this.constructor.name}},kJ=class extends EL{};y1.VerificationError=kJ;var QJ=class extends EL{};y1.PolicyError=QJ});var hNe=L(IL=>{"use strict";Object.defineProperty(IL,"__esModule",{value:!0});IL.filterCertAuthorities=o8t;IL.filterTLogAuthorities=a8t;function o8t(t,e){return t.filter(r=>r.validFor.start<=e.start&&r.validFor.end>=e.end)}function a8t(t,e){return t.filter(r=>e.logID&&!r.logID.equals(e.logID)?!1:r.validFor.start<=e.targetDate&&e.targetDate<=r.validFor.end)}});var Ay=L(fy=>{"use strict";Object.defineProperty(fy,"__esModule",{value:!0});fy.filterTLogAuthorities=fy.filterCertAuthorities=void 0;fy.toTrustMaterial=c8t;var TJ=wl(),tP=bb(),l8t=Co(),RJ=new Date(0),FJ=new Date(864e13),mNe=hNe();Object.defineProperty(fy,"filterCertAuthorities",{enumerable:!0,get:function(){return mNe.filterCertAuthorities}});Object.defineProperty(fy,"filterTLogAuthorities",{enumerable:!0,get:function(){return mNe.filterTLogAuthorities}});function c8t(t,e){let r=typeof e=="function"?e:u8t(e);return{certificateAuthorities:t.certificateAuthorities.map(dNe),timestampAuthorities:t.timestampAuthorities.map(dNe),tlogs:t.tlogs.map(gNe),ctlogs:t.ctlogs.map(gNe),publicKey:r}}function gNe(t){let e=t.publicKey.keyDetails,r=e===tP.PublicKeyDetails.PKCS1_RSA_PKCS1V5||e===tP.PublicKeyDetails.PKIX_RSA_PKCS1V5||e===tP.PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256||e===tP.PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256||e===tP.PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256?"pkcs1":"spki";return{logID:t.logId.keyId,publicKey:TJ.crypto.createPublicKey(t.publicKey.rawBytes,r),validFor:{start:t.publicKey.validFor?.start||RJ,end:t.publicKey.validFor?.end||FJ}}}function dNe(t){return{certChain:t.certChain.certificates.map(e=>TJ.X509Certificate.parse(e.rawBytes)),validFor:{start:t.validFor?.start||RJ,end:t.validFor?.end||FJ}}}function u8t(t){return e=>{let r=(t||{})[e];if(!r)throw new l8t.VerificationError({code:"PUBLIC_KEY_ERROR",message:`key not found: ${e}`});return{publicKey:TJ.crypto.createPublicKey(r.rawBytes),validFor:s=>(r.validFor?.start||RJ)<=s&&(r.validFor?.end||FJ)>=s}}}});var NJ=L(rP=>{"use strict";Object.defineProperty(rP,"__esModule",{value:!0});rP.CertificateChainVerifier=void 0;rP.verifyCertificateChain=A8t;var py=Co(),f8t=Ay();function A8t(t,e){let r=(0,f8t.filterCertAuthorities)(e,{start:t.notBefore,end:t.notAfter}),s;for(let a of r)try{return new CL({trustedCerts:a.certChain,untrustedCert:t}).verify()}catch(n){s=n}throw new py.VerificationError({code:"CERTIFICATE_ERROR",message:"Failed to verify certificate chain",cause:s})}var CL=class{constructor(e){this.untrustedCert=e.untrustedCert,this.trustedCerts=e.trustedCerts,this.localCerts=p8t([...e.trustedCerts,e.untrustedCert])}verify(){let e=this.sort();return this.checkPath(e),e}sort(){let e=this.untrustedCert,r=this.buildPaths(e);if(r=r.filter(a=>a.some(n=>this.trustedCerts.includes(n))),r.length===0)throw new py.VerificationError({code:"CERTIFICATE_ERROR",message:"no trusted certificate path found"});let s=r.reduce((a,n)=>a.length{if(s&&a.extSubjectKeyID){a.extSubjectKeyID.keyIdentifier.equals(s)&&r.push(a);return}a.subject.equals(e.issuer)&&r.push(a)}),r=r.filter(a=>{try{return e.verify(a)}catch{return!1}}),r)}checkPath(e){if(e.length<1)throw new py.VerificationError({code:"CERTIFICATE_ERROR",message:"certificate chain must contain at least one certificate"});if(!e.slice(1).every(s=>s.isCA))throw new py.VerificationError({code:"CERTIFICATE_ERROR",message:"intermediate certificate is not a CA"});for(let s=e.length-2;s>=0;s--)if(!e[s].issuer.equals(e[s+1].subject))throw new py.VerificationError({code:"CERTIFICATE_ERROR",message:"incorrect certificate name chaining"});for(let s=0;s{"use strict";Object.defineProperty(OJ,"__esModule",{value:!0});OJ.verifySCTs=d8t;var wL=wl(),h8t=Co(),g8t=Ay();function d8t(t,e,r){let s,a=t.clone();for(let p=0;p{if(!(0,g8t.filterTLogAuthorities)(r,{logID:p.logID,targetDate:p.datetime}).some(C=>p.verify(n.buffer,C.publicKey)))throw new h8t.VerificationError({code:"CERTIFICATE_ERROR",message:"SCT verification failed"});return p.logID})}});var INe=L(BL=>{"use strict";Object.defineProperty(BL,"__esModule",{value:!0});BL.verifyPublicKey=w8t;BL.verifyCertificate=B8t;var m8t=wl(),ENe=Co(),y8t=NJ(),E8t=yNe(),I8t="1.3.6.1.4.1.57264.1.1",C8t="1.3.6.1.4.1.57264.1.8";function w8t(t,e,r){let s=r.publicKey(t);return e.forEach(a=>{if(!s.validFor(a))throw new ENe.VerificationError({code:"PUBLIC_KEY_ERROR",message:`Public key is not valid for timestamp: ${a.toISOString()}`})}),{key:s.publicKey}}function B8t(t,e,r){let s=(0,y8t.verifyCertificateChain)(t,r.certificateAuthorities);if(!e.every(n=>s.every(c=>c.validForDate(n))))throw new ENe.VerificationError({code:"CERTIFICATE_ERROR",message:"certificate is not valid or expired at the specified date"});return{scts:(0,E8t.verifySCTs)(s[0],s[1],r.ctlogs),signer:v8t(s[0])}}function v8t(t){let e,r=t.extension(C8t);r?e=r.valueObj.subs?.[0]?.value.toString("ascii"):e=t.extension(I8t)?.value.toString("ascii");let s={extensions:{issuer:e},subjectAlternativeName:t.subjectAltName};return{key:m8t.crypto.createPublicKey(t.publicKey),identity:s}}});var wNe=L(vL=>{"use strict";Object.defineProperty(vL,"__esModule",{value:!0});vL.verifySubjectAlternativeName=S8t;vL.verifyExtensions=D8t;var CNe=Co();function S8t(t,e){if(e===void 0||!e.match(t))throw new CNe.PolicyError({code:"UNTRUSTED_SIGNER_ERROR",message:`certificate identity error - expected ${t}, got ${e}`})}function D8t(t,e={}){let r;for(r in t)if(e[r]!==t[r])throw new CNe.PolicyError({code:"UNTRUSTED_SIGNER_ERROR",message:`invalid certificate extension - expected ${r}=${t[r]}, got ${r}=${e[r]}`})}});var BNe=L(HJ=>{"use strict";Object.defineProperty(HJ,"__esModule",{value:!0});HJ.verifyCheckpoint=x8t;var MJ=wl(),E1=Co(),b8t=Ay(),LJ=` `,P8t=/\u2014 (\S+) (\S+)\n/g;function x8t(t,e){let r=(0,b8t.filterTLogAuthorities)(e,{targetDate:new Date(Number(t.integratedTime)*1e3)}),s=t.inclusionProof,a=_J.fromString(s.checkpoint.envelope),n=UJ.fromString(a.note);if(!k8t(a,r))throw new E1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"invalid checkpoint signature"});if(!MJ.crypto.bufferEqual(n.logHash,s.rootHash))throw new E1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"root hash mismatch"})}function k8t(t,e){let r=Buffer.from(t.note,"utf-8");return t.signatures.every(s=>{let a=e.find(n=>MJ.crypto.bufferEqual(n.logID.subarray(0,4),s.keyHint));return a?MJ.crypto.verify(r,a.publicKey,s.signature):!1})}var _J=class t{constructor(e,r){this.note=e,this.signatures=r}static fromString(e){if(!e.includes(LJ))throw new E1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"missing checkpoint separator"});let r=e.indexOf(LJ),s=e.slice(0,r+1),n=e.slice(r+LJ.length).matchAll(P8t),c=Array.from(n,f=>{let[,p,h]=f,E=Buffer.from(h,"base64");if(E.length<5)throw new E1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"malformed checkpoint signature"});return{name:p,keyHint:E.subarray(0,4),signature:E.subarray(4)}});if(c.length===0)throw new E1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"no signatures found in checkpoint"});return new t(s,c)}},UJ=class t{constructor(e,r,s,a){this.origin=e,this.logSize=r,this.logHash=s,this.rest=a}static fromString(e){let r=e.trimEnd().split(` `);if(r.length<3)throw new E1.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"too few lines in checkpoint header"});let s=r[0],a=BigInt(r[1]),n=Buffer.from(r[2],"base64"),c=r.slice(3);return new t(s,a,n,c)}}});var vNe=L(WJ=>{"use strict";Object.defineProperty(WJ,"__esModule",{value:!0});WJ.verifyMerkleInclusion=R8t;var GJ=wl(),jJ=Co(),Q8t=Buffer.from([0]),T8t=Buffer.from([1]);function R8t(t){let e=t.inclusionProof,r=BigInt(e.logIndex),s=BigInt(e.treeSize);if(r<0n||r>=s)throw new jJ.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:`invalid index: ${r}`});let{inner:a,border:n}=F8t(r,s);if(e.hashes.length!==a+n)throw new jJ.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"invalid hash count"});let c=e.hashes.slice(0,a),f=e.hashes.slice(a),p=U8t(t.canonicalizedBody),h=O8t(N8t(p,c,r),f);if(!GJ.crypto.bufferEqual(h,e.rootHash))throw new jJ.VerificationError({code:"TLOG_INCLUSION_PROOF_ERROR",message:"calculated root hash does not match inclusion proof"})}function F8t(t,e){let r=L8t(t,e),s=M8t(t>>BigInt(r));return{inner:r,border:s}}function N8t(t,e,r){return e.reduce((s,a,n)=>r>>BigInt(n)&BigInt(1)?qJ(a,s):qJ(s,a),t)}function O8t(t,e){return e.reduce((r,s)=>qJ(s,r),t)}function L8t(t,e){return _8t(t^e-BigInt(1))}function M8t(t){return t.toString(2).split("1").length-1}function _8t(t){return t===0n?0:t.toString(2).length}function qJ(t,e){return GJ.crypto.digest("sha256",T8t,t,e)}function U8t(t){return GJ.crypto.digest("sha256",Q8t,t)}});var DNe=L(YJ=>{"use strict";Object.defineProperty(YJ,"__esModule",{value:!0});YJ.verifyTLogSET=q8t;var SNe=wl(),H8t=Co(),j8t=Ay();function q8t(t,e){if(!(0,j8t.filterTLogAuthorities)(e,{logID:t.logId.keyId,targetDate:new Date(Number(t.integratedTime)*1e3)}).some(a=>{let n=G8t(t),c=Buffer.from(SNe.json.canonicalize(n),"utf8"),f=t.inclusionPromise.signedEntryTimestamp;return SNe.crypto.verify(c,a.publicKey,f)}))throw new H8t.VerificationError({code:"TLOG_INCLUSION_PROMISE_ERROR",message:"inclusion promise could not be verified"})}function G8t(t){let{integratedTime:e,logIndex:r,logId:s,canonicalizedBody:a}=t;return{body:a.toString("base64"),integratedTime:Number(e),logIndex:Number(r),logID:s.keyId.toString("hex")}}});var bNe=L(JJ=>{"use strict";Object.defineProperty(JJ,"__esModule",{value:!0});JJ.verifyRFC3161Timestamp=V8t;var VJ=wl(),KJ=Co(),W8t=NJ(),Y8t=Ay();function V8t(t,e,r){let s=t.signingTime;if(r=(0,Y8t.filterCertAuthorities)(r,{start:s,end:s}),r=J8t(r,{serialNumber:t.signerSerialNumber,issuer:t.signerIssuer}),!r.some(n=>{try{return K8t(t,e,n),!0}catch{return!1}}))throw new KJ.VerificationError({code:"TIMESTAMP_ERROR",message:"timestamp could not be verified"})}function K8t(t,e,r){let[s,...a]=r.certChain,n=VJ.crypto.createPublicKey(s.publicKey),c=t.signingTime;try{new W8t.CertificateChainVerifier({untrustedCert:s,trustedCerts:a}).verify()}catch{throw new KJ.VerificationError({code:"TIMESTAMP_ERROR",message:"invalid certificate chain"})}if(!r.certChain.every(p=>p.validForDate(c)))throw new KJ.VerificationError({code:"TIMESTAMP_ERROR",message:"timestamp was signed with an expired certificate"});t.verify(e,n)}function J8t(t,e){return t.filter(r=>r.certChain.length>0&&VJ.crypto.bufferEqual(r.certChain[0].serialNumber,e.serialNumber)&&VJ.crypto.bufferEqual(r.certChain[0].issuer,e.issuer))}});var PNe=L(SL=>{"use strict";Object.defineProperty(SL,"__esModule",{value:!0});SL.verifyTSATimestamp=tHt;SL.verifyTLogTimestamp=rHt;var z8t=Co(),Z8t=BNe(),X8t=vNe(),$8t=DNe(),eHt=bNe();function tHt(t,e,r){return(0,eHt.verifyRFC3161Timestamp)(t,e,r),{type:"timestamp-authority",logID:t.signerSerialNumber,timestamp:t.signingTime}}function rHt(t,e){let r=!1;if(nHt(t)&&((0,$8t.verifyTLogSET)(t,e),r=!0),iHt(t)&&((0,X8t.verifyMerkleInclusion)(t),(0,Z8t.verifyCheckpoint)(t,e),r=!0),!r)throw new z8t.VerificationError({code:"TLOG_MISSING_INCLUSION_ERROR",message:"inclusion could not be verified"});return{type:"transparency-log",logID:t.logId.keyId,timestamp:new Date(Number(t.integratedTime)*1e3)}}function nHt(t){return t.inclusionPromise!==void 0}function iHt(t){return t.inclusionProof!==void 0}});var xNe=L(zJ=>{"use strict";Object.defineProperty(zJ,"__esModule",{value:!0});zJ.verifyDSSETLogBody=sHt;var DL=Co();function sHt(t,e){switch(t.apiVersion){case"0.0.1":return oHt(t,e);default:throw new DL.VerificationError({code:"TLOG_BODY_ERROR",message:`unsupported dsse version: ${t.apiVersion}`})}}function oHt(t,e){if(t.spec.signatures?.length!==1)throw new DL.VerificationError({code:"TLOG_BODY_ERROR",message:"signature count mismatch"});let r=t.spec.signatures[0].signature;if(!e.compareSignature(Buffer.from(r,"base64")))throw new DL.VerificationError({code:"TLOG_BODY_ERROR",message:"tlog entry signature mismatch"});let s=t.spec.payloadHash?.value||"";if(!e.compareDigest(Buffer.from(s,"hex")))throw new DL.VerificationError({code:"TLOG_BODY_ERROR",message:"DSSE payload hash mismatch"})}});var kNe=L(XJ=>{"use strict";Object.defineProperty(XJ,"__esModule",{value:!0});XJ.verifyHashedRekordTLogBody=aHt;var ZJ=Co();function aHt(t,e){switch(t.apiVersion){case"0.0.1":return lHt(t,e);default:throw new ZJ.VerificationError({code:"TLOG_BODY_ERROR",message:`unsupported hashedrekord version: ${t.apiVersion}`})}}function lHt(t,e){let r=t.spec.signature.content||"";if(!e.compareSignature(Buffer.from(r,"base64")))throw new ZJ.VerificationError({code:"TLOG_BODY_ERROR",message:"signature mismatch"});let s=t.spec.data.hash?.value||"";if(!e.compareDigest(Buffer.from(s,"hex")))throw new ZJ.VerificationError({code:"TLOG_BODY_ERROR",message:"digest mismatch"})}});var QNe=L($J=>{"use strict";Object.defineProperty($J,"__esModule",{value:!0});$J.verifyIntotoTLogBody=cHt;var bL=Co();function cHt(t,e){switch(t.apiVersion){case"0.0.2":return uHt(t,e);default:throw new bL.VerificationError({code:"TLOG_BODY_ERROR",message:`unsupported intoto version: ${t.apiVersion}`})}}function uHt(t,e){if(t.spec.content.envelope.signatures?.length!==1)throw new bL.VerificationError({code:"TLOG_BODY_ERROR",message:"signature count mismatch"});let r=fHt(t.spec.content.envelope.signatures[0].sig);if(!e.compareSignature(Buffer.from(r,"base64")))throw new bL.VerificationError({code:"TLOG_BODY_ERROR",message:"tlog entry signature mismatch"});let s=t.spec.content.payloadHash?.value||"";if(!e.compareDigest(Buffer.from(s,"hex")))throw new bL.VerificationError({code:"TLOG_BODY_ERROR",message:"DSSE payload hash mismatch"})}function fHt(t){return Buffer.from(t,"base64").toString("utf-8")}});var RNe=L(ez=>{"use strict";Object.defineProperty(ez,"__esModule",{value:!0});ez.verifyTLogBody=gHt;var TNe=Co(),AHt=xNe(),pHt=kNe(),hHt=QNe();function gHt(t,e){let{kind:r,version:s}=t.kindVersion,a=JSON.parse(t.canonicalizedBody.toString("utf8"));if(r!==a.kind||s!==a.apiVersion)throw new TNe.VerificationError({code:"TLOG_BODY_ERROR",message:`kind/version mismatch - expected: ${r}/${s}, received: ${a.kind}/${a.apiVersion}`});switch(a.kind){case"dsse":return(0,AHt.verifyDSSETLogBody)(a,e);case"intoto":return(0,hHt.verifyIntotoTLogBody)(a,e);case"hashedrekord":return(0,pHt.verifyHashedRekordTLogBody)(a,e);default:throw new TNe.VerificationError({code:"TLOG_BODY_ERROR",message:`unsupported kind: ${r}`})}}});var MNe=L(PL=>{"use strict";Object.defineProperty(PL,"__esModule",{value:!0});PL.Verifier=void 0;var dHt=Ie("util"),I1=Co(),FNe=INe(),NNe=wNe(),ONe=PNe(),mHt=RNe(),tz=class{constructor(e,r={}){this.trustMaterial=e,this.options={ctlogThreshold:r.ctlogThreshold??1,tlogThreshold:r.tlogThreshold??1,tsaThreshold:r.tsaThreshold??0}}verify(e,r){let s=this.verifyTimestamps(e),a=this.verifySigningKey(e,s);return this.verifyTLogs(e),this.verifySignature(e,a),r&&this.verifyPolicy(r,a.identity||{}),a}verifyTimestamps(e){let r=0,s=0,a=e.timestamps.map(n=>{switch(n.$case){case"timestamp-authority":return s++,(0,ONe.verifyTSATimestamp)(n.timestamp,e.signature.signature,this.trustMaterial.timestampAuthorities);case"transparency-log":return r++,(0,ONe.verifyTLogTimestamp)(n.tlogEntry,this.trustMaterial.tlogs)}});if(LNe(a))throw new I1.VerificationError({code:"TIMESTAMP_ERROR",message:"duplicate timestamp"});if(rn.timestamp)}verifySigningKey({key:e},r){switch(e.$case){case"public-key":return(0,FNe.verifyPublicKey)(e.hint,r,this.trustMaterial);case"certificate":{let s=(0,FNe.verifyCertificate)(e.certificate,r,this.trustMaterial);if(LNe(s.scts))throw new I1.VerificationError({code:"CERTIFICATE_ERROR",message:"duplicate SCT"});if(s.scts.length(0,mHt.verifyTLogBody)(s,e))}verifySignature(e,r){if(!e.signature.verifySignature(r.key))throw new I1.VerificationError({code:"SIGNATURE_ERROR",message:"signature verification failed"})}verifyPolicy(e,r){e.subjectAlternativeName&&(0,NNe.verifySubjectAlternativeName)(e.subjectAlternativeName,r.subjectAlternativeName),e.extensions&&(0,NNe.verifyExtensions)(e.extensions,r.extensions)}};PL.Verifier=tz;function LNe(t){for(let e=0;e{"use strict";Object.defineProperty(ou,"__esModule",{value:!0});ou.Verifier=ou.toTrustMaterial=ou.VerificationError=ou.PolicyError=ou.toSignedEntity=void 0;var yHt=pNe();Object.defineProperty(ou,"toSignedEntity",{enumerable:!0,get:function(){return yHt.toSignedEntity}});var _Ne=Co();Object.defineProperty(ou,"PolicyError",{enumerable:!0,get:function(){return _Ne.PolicyError}});Object.defineProperty(ou,"VerificationError",{enumerable:!0,get:function(){return _Ne.VerificationError}});var EHt=Ay();Object.defineProperty(ou,"toTrustMaterial",{enumerable:!0,get:function(){return EHt.toTrustMaterial}});var IHt=MNe();Object.defineProperty(ou,"Verifier",{enumerable:!0,get:function(){return IHt.Verifier}})});var UNe=L(Na=>{"use strict";Object.defineProperty(Na,"__esModule",{value:!0});Na.DEFAULT_TIMEOUT=Na.DEFAULT_RETRY=void 0;Na.createBundleBuilder=BHt;Na.createKeyFinder=vHt;Na.createVerificationPolicy=SHt;var CHt=wl(),C1=pK(),wHt=xL();Na.DEFAULT_RETRY={retries:2};Na.DEFAULT_TIMEOUT=5e3;function BHt(t,e){let r={signer:DHt(e),witnesses:PHt(e)};switch(t){case"messageSignature":return new C1.MessageSignatureBundleBuilder(r);case"dsseEnvelope":return new C1.DSSEBundleBuilder({...r,certificateChain:e.legacyCompatibility})}}function vHt(t){return e=>{let r=t(e);if(!r)throw new wHt.VerificationError({code:"PUBLIC_KEY_ERROR",message:`key not found: ${e}`});return{publicKey:CHt.crypto.createPublicKey(r),validFor:()=>!0}}}function SHt(t){let e={},r=t.certificateIdentityEmail||t.certificateIdentityURI;return r&&(e.subjectAlternativeName=r),t.certificateIssuer&&(e.extensions={issuer:t.certificateIssuer}),e}function DHt(t){return new C1.FulcioSigner({fulcioBaseURL:t.fulcioURL,identityProvider:t.identityProvider||bHt(t),retry:t.retry??Na.DEFAULT_RETRY,timeout:t.timeout??Na.DEFAULT_TIMEOUT})}function bHt(t){let e=t.identityToken;return e?{getToken:()=>Promise.resolve(e)}:new C1.CIContextProvider("sigstore")}function PHt(t){let e=[];return xHt(t)&&e.push(new C1.RekorWitness({rekorBaseURL:t.rekorURL,entryType:t.legacyCompatibility?"intoto":"dsse",fetchOnConflict:!1,retry:t.retry??Na.DEFAULT_RETRY,timeout:t.timeout??Na.DEFAULT_TIMEOUT})),kHt(t)&&e.push(new C1.TSAWitness({tsaBaseURL:t.tsaServerURL,retry:t.retry??Na.DEFAULT_RETRY,timeout:t.timeout??Na.DEFAULT_TIMEOUT})),e}function xHt(t){return t.tlogUpload!==!1}function kHt(t){return t.tsaServerURL!==void 0}});var qNe=L(au=>{"use strict";var QHt=au&&au.__createBinding||(Object.create?function(t,e,r,s){s===void 0&&(s=r);var a=Object.getOwnPropertyDescriptor(e,r);(!a||("get"in a?!e.__esModule:a.writable||a.configurable))&&(a={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,s,a)}:function(t,e,r,s){s===void 0&&(s=r),t[s]=e[r]}),THt=au&&au.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),HNe=au&&au.__importStar||function(){var t=function(e){return t=Object.getOwnPropertyNames||function(r){var s=[];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(s[s.length]=a);return s},t(e)};return function(e){if(e&&e.__esModule)return e;var r={};if(e!=null)for(var s=t(e),a=0;aa.verify(t,s))}async function jNe(t={}){let e=await RHt.getTrustedRoot({mirrorURL:t.tufMirrorURL,rootPath:t.tufRootPath,cachePath:t.tufCachePath,forceCache:t.tufForceCache,retry:t.retry??w1.DEFAULT_RETRY,timeout:t.timeout??w1.DEFAULT_TIMEOUT}),r=t.keySelector?w1.createKeyFinder(t.keySelector):void 0,s=(0,rz.toTrustMaterial)(e,r),a={ctlogThreshold:t.ctLogThreshold,tlogThreshold:t.tlogThreshold},n=new rz.Verifier(s,a),c=w1.createVerificationPolicy(t);return{verify:(f,p)=>{let h=(0,nz.bundleFromJSON)(f),E=(0,rz.toSignedEntity)(h,p);n.verify(E,c)}}}});var WNe=L(Oi=>{"use strict";Object.defineProperty(Oi,"__esModule",{value:!0});Oi.verify=Oi.sign=Oi.createVerifier=Oi.attest=Oi.VerificationError=Oi.PolicyError=Oi.TUFError=Oi.InternalError=Oi.DEFAULT_REKOR_URL=Oi.DEFAULT_FULCIO_URL=Oi.ValidationError=void 0;var LHt=xb();Object.defineProperty(Oi,"ValidationError",{enumerable:!0,get:function(){return LHt.ValidationError}});var iz=pK();Object.defineProperty(Oi,"DEFAULT_FULCIO_URL",{enumerable:!0,get:function(){return iz.DEFAULT_FULCIO_URL}});Object.defineProperty(Oi,"DEFAULT_REKOR_URL",{enumerable:!0,get:function(){return iz.DEFAULT_REKOR_URL}});Object.defineProperty(Oi,"InternalError",{enumerable:!0,get:function(){return iz.InternalError}});var MHt=gL();Object.defineProperty(Oi,"TUFError",{enumerable:!0,get:function(){return MHt.TUFError}});var GNe=xL();Object.defineProperty(Oi,"PolicyError",{enumerable:!0,get:function(){return GNe.PolicyError}});Object.defineProperty(Oi,"VerificationError",{enumerable:!0,get:function(){return GNe.VerificationError}});var kL=qNe();Object.defineProperty(Oi,"attest",{enumerable:!0,get:function(){return kL.attest}});Object.defineProperty(Oi,"createVerifier",{enumerable:!0,get:function(){return kL.createVerifier}});Object.defineProperty(Oi,"sign",{enumerable:!0,get:function(){return kL.sign}});Object.defineProperty(Oi,"verify",{enumerable:!0,get:function(){return kL.verify}})});var IOe=L((Fvr,EOe)=>{var Kjt=Y4();function Jjt(t){return Kjt(t)?void 0:t}EOe.exports=Jjt});var wOe=L((Nvr,COe)=>{var zjt=QT(),Zjt=w5(),Xjt=D5(),$jt=Im(),e6t=Vd(),t6t=IOe(),r6t=dG(),n6t=C5(),i6t=1,s6t=2,o6t=4,a6t=r6t(function(t,e){var r={};if(t==null)return r;var s=!1;e=zjt(e,function(n){return n=$jt(n,t),s||(s=n.length>1),n}),e6t(t,n6t(t),r),s&&(r=Zjt(r,i6t|s6t|o6t,t6t));for(var a=e.length;a--;)Xjt(r,e[a]);return r});COe.exports=a6t});bt();Ve();bt();var bOe=Ie("child_process"),POe=et(Rd());Wt();var $I=new Map([]);var $v={};Vt($v,{BaseCommand:()=>ut,WorkspaceRequiredError:()=>ar,getCli:()=>XCe,getDynamicLibs:()=>ZCe,getPluginConfiguration:()=>tC,openWorkspace:()=>eC,pluginCommands:()=>$I,runExit:()=>KR});Wt();var ut=class extends ot{constructor(){super(...arguments);this.cwd=ge.String("--cwd",{hidden:!0})}validateAndExecute(){if(typeof this.cwd<"u")throw new nt("The --cwd option is ambiguous when used anywhere else than the very first parameter provided in the command line, before even the command path");return super.validateAndExecute()}};Ve();bt();Wt();var ar=class extends nt{constructor(e,r){let s=K.relative(e,r),a=K.join(e,Ht.fileName);super(`This command can only be run from within a workspace of your project (${s} isn't a workspace of ${a}).`)}};Ve();bt();rA();Bc();wv();Wt();var mwt=et(Ai());Ul();var ZCe=()=>new Map([["@yarnpkg/cli",$v],["@yarnpkg/core",Xv],["@yarnpkg/fslib",U2],["@yarnpkg/libzip",Iv],["@yarnpkg/parsers",K2],["@yarnpkg/shell",Dv],["clipanion",oB],["semver",mwt],["typanion",Ia]]);Ve();async function eC(t,e){let{project:r,workspace:s}=await Tt.find(t,e);if(!s)throw new ar(r.cwd,e);return s}Ve();bt();rA();Bc();wv();Wt();var d6t=et(Ai());Ul();var Y5={};Vt(Y5,{AddCommand:()=>sC,BinCommand:()=>oC,CacheCleanCommand:()=>aC,ClipanionCommand:()=>pC,ConfigCommand:()=>fC,ConfigGetCommand:()=>lC,ConfigSetCommand:()=>cC,ConfigUnsetCommand:()=>uC,DedupeCommand:()=>AC,EntryCommand:()=>gC,ExecCommand:()=>mC,ExplainCommand:()=>IC,ExplainPeerRequirementsCommand:()=>yC,HelpCommand:()=>hC,InfoCommand:()=>CC,LinkCommand:()=>BC,NodeCommand:()=>vC,PluginCheckCommand:()=>SC,PluginImportCommand:()=>PC,PluginImportSourcesCommand:()=>xC,PluginListCommand:()=>DC,PluginRemoveCommand:()=>kC,PluginRuntimeCommand:()=>QC,RebuildCommand:()=>TC,RemoveCommand:()=>RC,RunCommand:()=>NC,RunIndexCommand:()=>FC,SetResolutionCommand:()=>OC,SetVersionCommand:()=>EC,SetVersionSourcesCommand:()=>bC,UnlinkCommand:()=>LC,UpCommand:()=>MC,VersionCommand:()=>dC,WhyCommand:()=>_C,WorkspaceCommand:()=>GC,WorkspacesListCommand:()=>qC,YarnCommand:()=>wC,dedupeUtils:()=>iF,default:()=>ASt,suggestUtils:()=>Xu});var xBe=et(Rd());Ve();Ve();Ve();Wt();var _1e=et(nS());Ul();var Xu={};Vt(Xu,{Modifier:()=>d5,Strategy:()=>tF,Target:()=>iS,WorkspaceModifier:()=>F1e,applyModifier:()=>L1t,extractDescriptorFromPath:()=>m5,extractRangeModifier:()=>N1e,fetchDescriptorFrom:()=>y5,findProjectDescriptors:()=>M1e,getModifier:()=>sS,getSuggestedDescriptors:()=>oS,makeWorkspaceDescriptor:()=>L1e,toWorkspaceModifier:()=>O1e});Ve();Ve();bt();var g5=et(Ai()),N1t="workspace:",iS=(s=>(s.REGULAR="dependencies",s.DEVELOPMENT="devDependencies",s.PEER="peerDependencies",s))(iS||{}),d5=(s=>(s.CARET="^",s.TILDE="~",s.EXACT="",s))(d5||{}),F1e=(s=>(s.CARET="^",s.TILDE="~",s.EXACT="*",s))(F1e||{}),tF=(n=>(n.KEEP="keep",n.REUSE="reuse",n.PROJECT="project",n.LATEST="latest",n.CACHE="cache",n))(tF||{});function sS(t,e){return t.exact?"":t.caret?"^":t.tilde?"~":e.configuration.get("defaultSemverRangePrefix")}var O1t=/^([\^~]?)[0-9]+(?:\.[0-9]+){0,2}(?:-\S+)?$/;function N1e(t,{project:e}){let r=t.match(O1t);return r?r[1]:e.configuration.get("defaultSemverRangePrefix")}function L1t(t,e){let{protocol:r,source:s,params:a,selector:n}=q.parseRange(t.range);return g5.default.valid(n)&&(n=`${e}${t.range}`),q.makeDescriptor(t,q.makeRange({protocol:r,source:s,params:a,selector:n}))}function O1e(t){switch(t){case"^":return"^";case"~":return"~";case"":return"*";default:throw new Error(`Assertion failed: Unknown modifier: "${t}"`)}}function L1e(t,e){return q.makeDescriptor(t.anchoredDescriptor,`${N1t}${O1e(e)}`)}async function M1e(t,{project:e,target:r}){let s=new Map,a=n=>{let c=s.get(n.descriptorHash);return c||s.set(n.descriptorHash,c={descriptor:n,locators:[]}),c};for(let n of e.workspaces)if(r==="peerDependencies"){let c=n.manifest.peerDependencies.get(t.identHash);c!==void 0&&a(c).locators.push(n.anchoredLocator)}else{let c=n.manifest.dependencies.get(t.identHash),f=n.manifest.devDependencies.get(t.identHash);r==="devDependencies"?f!==void 0?a(f).locators.push(n.anchoredLocator):c!==void 0&&a(c).locators.push(n.anchoredLocator):c!==void 0?a(c).locators.push(n.anchoredLocator):f!==void 0&&a(f).locators.push(n.anchoredLocator)}return s}async function m5(t,{cwd:e,workspace:r}){return await _1t(async s=>{K.isAbsolute(t)||(t=K.relative(r.cwd,K.resolve(e,t)),t.match(/^\.{0,2}\//)||(t=`./${t}`));let{project:a}=r,n=await y5(q.makeIdent(null,"archive"),t,{project:r.project,cache:s,workspace:r});if(!n)throw new Error("Assertion failed: The descriptor should have been found");let c=new Yi,f=a.configuration.makeResolver(),p=a.configuration.makeFetcher(),h={checksums:a.storedChecksums,project:a,cache:s,fetcher:p,report:c,resolver:f},E=f.bindDescriptor(n,r.anchoredLocator,h),C=q.convertDescriptorToLocator(E),S=await p.fetch(C,h),P=await Ht.find(S.prefixPath,{baseFs:S.packageFs});if(!P.name)throw new Error("Target path doesn't have a name");return q.makeDescriptor(P.name,t)})}function M1t(t){if(t.range==="unknown")return{type:"resolve",range:"latest"};if(Or.validRange(t.range))return{type:"fixed",range:t.range};if(Up.test(t.range))return{type:"resolve",range:t.range};let e=t.range.match(/^(?:jsr:|npm:)(.*)/);if(!e)return{type:"fixed",range:t.range};let[,r]=e,s=`${q.stringifyIdent(t)}@`;return r.startsWith(s)&&(r=r.slice(s.length)),Or.validRange(r)?{type:"fixed",range:t.range}:Up.test(r)?{type:"resolve",range:t.range}:{type:"fixed",range:t.range}}async function oS(t,{project:e,workspace:r,cache:s,target:a,fixed:n,modifier:c,strategies:f,maxResults:p=1/0}){if(!(p>=0))throw new Error(`Invalid maxResults (${p})`);let h=!n||t.range==="unknown"?M1t(t):{type:"fixed",range:t.range};if(h.type==="fixed")return{suggestions:[{descriptor:t,name:`Use ${q.prettyDescriptor(e.configuration,t)}`,reason:"(unambiguous explicit request)"}],rejections:[]};let E=typeof r<"u"&&r!==null&&r.manifest[a].get(t.identHash)||null,C=[],S=[],P=async I=>{try{await I()}catch(R){S.push(R)}};for(let I of f){if(C.length>=p)break;switch(I){case"keep":await P(async()=>{E&&C.push({descriptor:E,name:`Keep ${q.prettyDescriptor(e.configuration,E)}`,reason:"(no changes)"})});break;case"reuse":await P(async()=>{for(let{descriptor:R,locators:N}of(await M1e(t,{project:e,target:a})).values()){if(N.length===1&&N[0].locatorHash===r.anchoredLocator.locatorHash&&f.includes("keep"))continue;let U=`(originally used by ${q.prettyLocator(e.configuration,N[0])}`;U+=N.length>1?` and ${N.length-1} other${N.length>2?"s":""})`:")",C.push({descriptor:R,name:`Reuse ${q.prettyDescriptor(e.configuration,R)}`,reason:U})}});break;case"cache":await P(async()=>{for(let R of e.storedDescriptors.values())R.identHash===t.identHash&&C.push({descriptor:R,name:`Reuse ${q.prettyDescriptor(e.configuration,R)}`,reason:"(already used somewhere in the lockfile)"})});break;case"project":await P(async()=>{if(r.manifest.name!==null&&t.identHash===r.manifest.name.identHash)return;let R=e.tryWorkspaceByIdent(t);if(R===null)return;let N=L1e(R,c);C.push({descriptor:N,name:`Attach ${q.prettyDescriptor(e.configuration,N)}`,reason:`(local workspace at ${he.pretty(e.configuration,R.relativeCwd,he.Type.PATH)})`})});break;case"latest":{let R=e.configuration.get("enableNetwork"),N=e.configuration.get("enableOfflineMode");await P(async()=>{if(a==="peerDependencies")C.push({descriptor:q.makeDescriptor(t,"*"),name:"Use *",reason:"(catch-all peer dependency pattern)"});else if(!R&&!N)C.push({descriptor:null,name:"Resolve from latest",reason:he.pretty(e.configuration,"(unavailable because enableNetwork is toggled off)","grey")});else{let U=await y5(t,h.range,{project:e,cache:s,workspace:r,modifier:c});U&&C.push({descriptor:U,name:`Use ${q.prettyDescriptor(e.configuration,U)}`,reason:`(resolved from ${N?"the cache":"latest"})`})}})}break}}return{suggestions:C.slice(0,p),rejections:S.slice(0,p)}}async function y5(t,e,{project:r,cache:s,workspace:a,preserveModifier:n=!0,modifier:c}){let f=r.configuration.normalizeDependency(q.makeDescriptor(t,e)),p=new Yi,h=r.configuration.makeFetcher(),E=r.configuration.makeResolver(),C={project:r,fetcher:h,cache:s,checksums:r.storedChecksums,report:p,cacheOptions:{skipIntegrityCheck:!0}},S={...C,resolver:E,fetchOptions:C},P=E.bindDescriptor(f,a.anchoredLocator,S),I=await E.getCandidates(P,{},S);if(I.length===0)return null;let R=I[0],{protocol:N,source:U,params:W,selector:te}=q.parseRange(q.convertToManifestRange(R.reference));if(N===r.configuration.get("defaultProtocol")&&(N=null),g5.default.valid(te)){let ie=te;if(typeof c<"u")te=c+te;else if(n!==!1){let me=typeof n=="string"?n:f.range;te=N1e(me,{project:r})+te}let Ae=q.makeDescriptor(R,q.makeRange({protocol:N,source:U,params:W,selector:te}));(await E.getCandidates(r.configuration.normalizeDependency(Ae),{},S)).length!==1&&(te=ie)}return q.makeDescriptor(R,q.makeRange({protocol:N,source:U,params:W,selector:te}))}async function _1t(t){return await le.mktempPromise(async e=>{let r=ze.create(e);return r.useWithSource(e,{enableMirror:!1,compressionLevel:0},e,{overwrite:!0}),await t(new Jr(e,{configuration:r,check:!1,immutable:!1}))})}var sC=class extends ut{constructor(){super(...arguments);this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.fixed=ge.Boolean("-F,--fixed",!1,{description:"Store dependency tags as-is instead of resolving them"});this.exact=ge.Boolean("-E,--exact",!1,{description:"Don't use any semver modifier on the resolved range"});this.tilde=ge.Boolean("-T,--tilde",!1,{description:"Use the `~` semver modifier on the resolved range"});this.caret=ge.Boolean("-C,--caret",!1,{description:"Use the `^` semver modifier on the resolved range"});this.dev=ge.Boolean("-D,--dev",!1,{description:"Add a package as a dev dependency"});this.peer=ge.Boolean("-P,--peer",!1,{description:"Add a package as a peer dependency"});this.optional=ge.Boolean("-O,--optional",!1,{description:"Add / upgrade a package to an optional regular / peer dependency"});this.preferDev=ge.Boolean("--prefer-dev",!1,{description:"Add / upgrade a package to a dev dependency"});this.interactive=ge.Boolean("-i,--interactive",{description:"Reuse the specified package from other workspaces in the project"});this.cached=ge.Boolean("--cached",!1,{description:"Reuse the highest version already used somewhere within the project"});this.mode=ge.String("--mode",{description:"Change what artifacts installs generate",validator:po(ec)});this.silent=ge.Boolean("--silent",{hidden:!0});this.packages=ge.Rest()}static{this.paths=[["add"]]}static{this.usage=ot.Usage({description:"add dependencies to the project",details:"\n This command adds a package to the package.json for the nearest workspace.\n\n - If it didn't exist before, the package will by default be added to the regular `dependencies` field, but this behavior can be overriden thanks to the `-D,--dev` flag (which will cause the dependency to be added to the `devDependencies` field instead) and the `-P,--peer` flag (which will do the same but for `peerDependencies`).\n\n - If the package was already listed in your dependencies, it will by default be upgraded whether it's part of your `dependencies` or `devDependencies` (it won't ever update `peerDependencies`, though).\n\n - If set, the `--prefer-dev` flag will operate as a more flexible `-D,--dev` in that it will add the package to your `devDependencies` if it isn't already listed in either `dependencies` or `devDependencies`, but it will also happily upgrade your `dependencies` if that's what you already use (whereas `-D,--dev` would throw an exception).\n\n - If set, the `-O,--optional` flag will add the package to the `optionalDependencies` field and, in combination with the `-P,--peer` flag, it will add the package as an optional peer dependency. If the package was already listed in your `dependencies`, it will be upgraded to `optionalDependencies`. If the package was already listed in your `peerDependencies`, in combination with the `-P,--peer` flag, it will be upgraded to an optional peer dependency: `\"peerDependenciesMeta\": { \"\": { \"optional\": true } }`\n\n - If the added package doesn't specify a range at all its `latest` tag will be resolved and the returned version will be used to generate a new semver range (using the `^` modifier by default unless otherwise configured via the `defaultSemverRangePrefix` configuration, or the `~` modifier if `-T,--tilde` is specified, or no modifier at all if `-E,--exact` is specified). Two exceptions to this rule: the first one is that if the package is a workspace then its local version will be used, and the second one is that if you use `-P,--peer` the default range will be `*` and won't be resolved at all.\n\n - If the added package specifies a range (such as `^1.0.0`, `latest`, or `rc`), Yarn will add this range as-is in the resulting package.json entry (in particular, tags such as `rc` will be encoded as-is rather than being converted into a semver range).\n\n If the `--cached` option is used, Yarn will preferably reuse the highest version already used somewhere within the project, even if through a transitive dependency.\n\n If the `-i,--interactive` option is used (or if the `preferInteractive` settings is toggled on) the command will first try to check whether other workspaces in the project use the specified package and, if so, will offer to reuse them.\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n For a compilation of all the supported protocols, please consult the dedicated page from our website: https://yarnpkg.com/protocols.\n ",examples:[["Add a regular package to the current workspace","$0 add lodash"],["Add a specific version for a package to the current workspace","$0 add lodash@1.2.3"],["Add a package from a GitHub repository (the master branch) to the current workspace using a URL","$0 add lodash@https://github.com/lodash/lodash"],["Add a package from a GitHub repository (the master branch) to the current workspace using the GitHub protocol","$0 add lodash@github:lodash/lodash"],["Add a package from a GitHub repository (the master branch) to the current workspace using the GitHub protocol (shorthand)","$0 add lodash@lodash/lodash"],["Add a package from a specific branch of a GitHub repository to the current workspace using the GitHub protocol (shorthand)","$0 add lodash-es@lodash/lodash#es"],["Add a local package (gzipped tarball format) to the current workspace","$0 add local-package-name@file:../path/to/local-package-name-v0.1.2.tgz"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=this.fixed,f=r.isInteractive({interactive:this.interactive,stdout:this.context.stdout}),p=f||r.get("preferReuse"),h=sS(this,s),E=[p?"reuse":void 0,"project",this.cached?"cache":void 0,"latest"].filter(W=>typeof W<"u"),C=f?1/0:1,S=W=>{let te=q.tryParseDescriptor(W.slice(4));return te?te.range==="unknown"?q.makeDescriptor(te,`jsr:${q.stringifyIdent(te)}@latest`):q.makeDescriptor(te,`jsr:${te.range}`):null},P=await Promise.all(this.packages.map(async W=>{let te=W.match(/^\.{0,2}\//)?await m5(W,{cwd:this.context.cwd,workspace:a}):W.startsWith("jsr:")?S(W):q.tryParseDescriptor(W),ie=W.match(/^(https?:|git@github)/);if(ie)throw new nt(`It seems you are trying to add a package using a ${he.pretty(r,`${ie[0]}...`,he.Type.RANGE)} url; we now require package names to be explicitly specified. Try running the command again with the package name prefixed: ${he.pretty(r,"yarn add",he.Type.CODE)} ${he.pretty(r,q.makeDescriptor(q.makeIdent(null,"my-package"),`${ie[0]}...`),he.Type.DESCRIPTOR)}`);if(!te)throw new nt(`The ${he.pretty(r,W,he.Type.CODE)} string didn't match the required format (package-name@range). Did you perhaps forget to explicitly reference the package name?`);let Ae=U1t(a,te,{dev:this.dev,peer:this.peer,preferDev:this.preferDev,optional:this.optional});return await Promise.all(Ae.map(async me=>{let pe=await oS(te,{project:s,workspace:a,cache:n,fixed:c,target:me,modifier:h,strategies:E,maxResults:C});return{request:te,suggestedDescriptors:pe,target:me}}))})).then(W=>W.flat()),I=await uA.start({configuration:r,stdout:this.context.stdout,suggestInstall:!1},async W=>{for(let{request:te,suggestedDescriptors:{suggestions:ie,rejections:Ae}}of P)if(ie.filter(me=>me.descriptor!==null).length===0){let[me]=Ae;if(typeof me>"u")throw new Error("Assertion failed: Expected an error to have been set");s.configuration.get("enableNetwork")?W.reportError(27,`${q.prettyDescriptor(r,te)} can't be resolved to a satisfying range`):W.reportError(27,`${q.prettyDescriptor(r,te)} can't be resolved to a satisfying range (note: network resolution has been disabled)`),W.reportSeparator(),W.reportExceptionOnce(me)}});if(I.hasErrors())return I.exitCode();let R=!1,N=[],U=[];for(let{suggestedDescriptors:{suggestions:W},target:te}of P){let ie,Ae=W.filter(Be=>Be.descriptor!==null),ce=Ae[0].descriptor,me=Ae.every(Be=>q.areDescriptorsEqual(Be.descriptor,ce));Ae.length===1||me?ie=ce:(R=!0,{answer:ie}=await(0,_1e.prompt)({type:"select",name:"answer",message:"Which range do you want to use?",choices:W.map(({descriptor:Be,name:Ce,reason:g})=>Be?{name:Ce,hint:g,descriptor:Be}:{name:Ce,hint:g,disabled:!0}),onCancel:()=>process.exit(130),result(Be){return this.find(Be,"descriptor")},stdin:this.context.stdin,stdout:this.context.stdout}));let pe=a.manifest[te].get(ie.identHash);(typeof pe>"u"||pe.descriptorHash!==ie.descriptorHash)&&(a.manifest[te].set(ie.identHash,ie),this.optional&&(te==="dependencies"?a.manifest.ensureDependencyMeta({...ie,range:"unknown"}).optional=!0:te==="peerDependencies"&&(a.manifest.ensurePeerDependencyMeta({...ie,range:"unknown"}).optional=!0)),typeof pe>"u"?N.push([a,te,ie,E]):U.push([a,te,pe,ie]))}return await r.triggerMultipleHooks(W=>W.afterWorkspaceDependencyAddition,N),await r.triggerMultipleHooks(W=>W.afterWorkspaceDependencyReplacement,U),R&&this.context.stdout.write(` `),await s.installWithNewReport({json:this.json,stdout:this.context.stdout,quiet:this.context.quiet},{cache:n,mode:this.mode})}};function U1t(t,e,{dev:r,peer:s,preferDev:a,optional:n}){let c=t.manifest.dependencies.has(e.identHash),f=t.manifest.devDependencies.has(e.identHash),p=t.manifest.peerDependencies.has(e.identHash);if((r||s)&&c)throw new nt(`Package "${q.prettyIdent(t.project.configuration,e)}" is already listed as a regular dependency - remove the -D,-P flags or remove it from your dependencies first`);if(!r&&!s&&p)throw new nt(`Package "${q.prettyIdent(t.project.configuration,e)}" is already listed as a peer dependency - use either of -D or -P, or remove it from your peer dependencies first`);if(n&&f)throw new nt(`Package "${q.prettyIdent(t.project.configuration,e)}" is already listed as a dev dependency - remove the -O flag or remove it from your dev dependencies first`);if(n&&!s&&p)throw new nt(`Package "${q.prettyIdent(t.project.configuration,e)}" is already listed as a peer dependency - remove the -O flag or add the -P flag or remove it from your peer dependencies first`);if((r||a)&&n)throw new nt(`Package "${q.prettyIdent(t.project.configuration,e)}" cannot simultaneously be a dev dependency and an optional dependency`);let h=[];return s&&h.push("peerDependencies"),(r||a)&&h.push("devDependencies"),n&&h.push("dependencies"),h.length>0?h:f?["devDependencies"]:p?["peerDependencies"]:["dependencies"]}Ve();Ve();Wt();var oC=class extends ut{constructor(){super(...arguments);this.verbose=ge.Boolean("-v,--verbose",!1,{description:"Print both the binary name and the locator of the package that provides the binary"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.name=ge.String({required:!1})}static{this.paths=[["bin"]]}static{this.usage=ot.Usage({description:"get the path to a binary script",details:` When used without arguments, this command will print the list of all the binaries available in the current workspace. Adding the \`-v,--verbose\` flag will cause the output to contain both the binary name and the locator of the package that provides the binary. When an argument is specified, this command will just print the path to the binary on the standard output and exit. Note that the reported path may be stored within a zip archive. `,examples:[["List all the available binaries","$0 bin"],["Print the path to a specific binary","$0 bin eslint"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,locator:a}=await Tt.find(r,this.context.cwd);if(await s.restoreInstallState(),this.name){let f=(await In.getPackageAccessibleBinaries(a,{project:s})).get(this.name);if(!f)throw new nt(`Couldn't find a binary named "${this.name}" for package "${q.prettyLocator(r,a)}"`);let[,p]=f;return this.context.stdout.write(`${p} `),0}return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async c=>{let f=await In.getPackageAccessibleBinaries(a,{project:s}),h=Array.from(f.keys()).reduce((E,C)=>Math.max(E,C.length),0);for(let[E,[C,S]]of f)c.reportJson({name:E,source:q.stringifyIdent(C),path:S});if(this.verbose)for(let[E,[C]]of f)c.reportInfo(null,`${E.padEnd(h," ")} ${q.prettyLocator(r,C)}`);else for(let E of f.keys())c.reportInfo(null,E)})).exitCode()}};Ve();bt();Wt();var aC=class extends ut{constructor(){super(...arguments);this.mirror=ge.Boolean("--mirror",!1,{description:"Remove the global cache files instead of the local cache files"});this.all=ge.Boolean("--all",!1,{description:"Remove both the global cache files and the local cache files of the current project"})}static{this.paths=[["cache","clean"],["cache","clear"]]}static{this.usage=ot.Usage({description:"remove the shared cache files",details:` This command will remove all the files from the cache. `,examples:[["Remove all the local archives","$0 cache clean"],["Remove all the archives stored in the ~/.yarn directory","$0 cache clean --mirror"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);if(!r.get("enableCacheClean"))throw new nt("Cache cleaning is currently disabled. To enable it, set `enableCacheClean: true` in your configuration file. Note: Cache cleaning is typically not required and should be avoided when using Zero-Installs.");let s=await Jr.find(r);return(await Ot.start({configuration:r,stdout:this.context.stdout},async()=>{let n=(this.all||this.mirror)&&s.mirrorCwd!==null,c=!this.mirror;n&&(await le.removePromise(s.mirrorCwd),await r.triggerHook(f=>f.cleanGlobalArtifacts,r)),c&&await le.removePromise(s.cwd)})).exitCode()}};Ve();Wt();var H1e=et(aS()),E5=Ie("util"),lC=class extends ut{constructor(){super(...arguments);this.why=ge.Boolean("--why",!1,{description:"Print the explanation for why a setting has its value"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.unsafe=ge.Boolean("--no-redacted",!1,{description:"Don't redact secrets (such as tokens) from the output"});this.name=ge.String()}static{this.paths=[["config","get"]]}static{this.usage=ot.Usage({description:"read a configuration settings",details:` This command will print a configuration setting. Secrets (such as tokens) will be redacted from the output by default. If this behavior isn't desired, set the \`--no-redacted\` to get the untransformed value. `,examples:[["Print a simple configuration setting","yarn config get yarnPath"],["Print a complex configuration setting","yarn config get packageExtensions"],["Print a nested field from the configuration",`yarn config get 'npmScopes["my-company"].npmRegistryServer'`],["Print a token from the configuration","yarn config get npmAuthToken --no-redacted"],["Print a configuration setting as JSON","yarn config get packageExtensions --json"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=this.name.replace(/[.[].*$/,""),a=this.name.replace(/^[^.[]*/,"");if(typeof r.settings.get(s)>"u")throw new nt(`Couldn't find a configuration settings named "${s}"`);let c=r.getSpecial(s,{hideSecrets:!this.unsafe,getNativePaths:!0}),f=je.convertMapsToIndexableObjects(c),p=a?(0,H1e.default)(f,a):f,h=await Ot.start({configuration:r,includeFooter:!1,json:this.json,stdout:this.context.stdout},async E=>{E.reportJson(p)});if(!this.json){if(typeof p=="string")return this.context.stdout.write(`${p} `),h.exitCode();E5.inspect.styles.name="cyan",this.context.stdout.write(`${(0,E5.inspect)(p,{depth:1/0,colors:r.get("enableColors"),compact:!1})} `)}return h.exitCode()}};Ve();Wt();var Q2e=et(B5()),T2e=et(aS()),R2e=et(v5()),S5=Ie("util"),cC=class extends ut{constructor(){super(...arguments);this.json=ge.Boolean("--json",!1,{description:"Set complex configuration settings to JSON values"});this.home=ge.Boolean("-H,--home",!1,{description:"Update the home configuration instead of the project configuration"});this.name=ge.String();this.value=ge.String()}static{this.paths=[["config","set"]]}static{this.usage=ot.Usage({description:"change a configuration settings",details:` This command will set a configuration setting. When used without the \`--json\` flag, it can only set a simple configuration setting (a string, a number, or a boolean). When used with the \`--json\` flag, it can set both simple and complex configuration settings, including Arrays and Objects. `,examples:[["Set a simple configuration setting (a string, a number, or a boolean)","yarn config set initScope myScope"],["Set a simple configuration setting (a string, a number, or a boolean) using the `--json` flag",'yarn config set initScope --json \\"myScope\\"'],["Set a complex configuration setting (an Array) using the `--json` flag",`yarn config set unsafeHttpWhitelist --json '["*.example.com", "example.com"]'`],["Set a complex configuration setting (an Object) using the `--json` flag",`yarn config set packageExtensions --json '{ "@babel/parser@*": { "dependencies": { "@babel/types": "*" } } }'`],["Set a nested configuration setting",'yarn config set npmScopes.company.npmRegistryServer "https://npm.example.com"'],["Set a nested configuration setting using indexed access for non-simple keys",`yarn config set 'npmRegistries["//npm.example.com"].npmAuthToken' "ffffffff-ffff-ffff-ffff-ffffffffffff"`]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=()=>{if(!r.projectCwd)throw new nt("This command must be run from within a project folder");return r.projectCwd},a=this.name.replace(/[.[].*$/,""),n=this.name.replace(/^[^.[]*\.?/,"");if(typeof r.settings.get(a)>"u")throw new nt(`Couldn't find a configuration settings named "${a}"`);if(a==="enableStrictSettings")throw new nt("This setting only affects the file it's in, and thus cannot be set from the CLI");let f=this.json?JSON.parse(this.value):this.value;await(this.home?I=>ze.updateHomeConfiguration(I):I=>ze.updateConfiguration(s(),I))(I=>{if(n){let R=(0,Q2e.default)(I);return(0,R2e.default)(R,this.name,f),R}else return{...I,[a]:f}});let E=(await ze.find(this.context.cwd,this.context.plugins)).getSpecial(a,{hideSecrets:!0,getNativePaths:!0}),C=je.convertMapsToIndexableObjects(E),S=n?(0,T2e.default)(C,n):C;return(await Ot.start({configuration:r,includeFooter:!1,stdout:this.context.stdout},async I=>{S5.inspect.styles.name="cyan",I.reportInfo(0,`Successfully set ${this.name} to ${(0,S5.inspect)(S,{depth:1/0,colors:r.get("enableColors"),compact:!1})}`)})).exitCode()}};Ve();Wt();var G2e=et(B5()),W2e=et(L2e()),Y2e=et(b5()),uC=class extends ut{constructor(){super(...arguments);this.home=ge.Boolean("-H,--home",!1,{description:"Update the home configuration instead of the project configuration"});this.name=ge.String()}static{this.paths=[["config","unset"]]}static{this.usage=ot.Usage({description:"unset a configuration setting",details:` This command will unset a configuration setting. `,examples:[["Unset a simple configuration setting","yarn config unset initScope"],["Unset a complex configuration setting","yarn config unset packageExtensions"],["Unset a nested configuration setting","yarn config unset npmScopes.company.npmRegistryServer"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=()=>{if(!r.projectCwd)throw new nt("This command must be run from within a project folder");return r.projectCwd},a=this.name.replace(/[.[].*$/,""),n=this.name.replace(/^[^.[]*\.?/,"");if(typeof r.settings.get(a)>"u")throw new nt(`Couldn't find a configuration settings named "${a}"`);let f=this.home?h=>ze.updateHomeConfiguration(h):h=>ze.updateConfiguration(s(),h);return(await Ot.start({configuration:r,includeFooter:!1,stdout:this.context.stdout},async h=>{let E=!1;await f(C=>{if(!(0,W2e.default)(C,this.name))return h.reportWarning(0,`Configuration doesn't contain setting ${this.name}; there is nothing to unset`),E=!0,C;let S=n?(0,G2e.default)(C):{...C};return(0,Y2e.default)(S,this.name),S}),E||h.reportInfo(0,`Successfully unset ${this.name}`)})).exitCode()}};Ve();bt();Wt();var nF=Ie("util"),fC=class extends ut{constructor(){super(...arguments);this.noDefaults=ge.Boolean("--no-defaults",!1,{description:"Omit the default values from the display"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.verbose=ge.Boolean("-v,--verbose",{hidden:!0});this.why=ge.Boolean("--why",{hidden:!0});this.names=ge.Rest()}static{this.paths=[["config"]]}static{this.usage=ot.Usage({description:"display the current configuration",details:` This command prints the current active configuration settings. `,examples:[["Print the active configuration settings","$0 config"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins,{strict:!1}),s=await vI({configuration:r,stdout:this.context.stdout,forceError:this.json},[{option:this.verbose,message:"The --verbose option is deprecated, the settings' descriptions are now always displayed"},{option:this.why,message:"The --why option is deprecated, the settings' sources are now always displayed"}]);if(s!==null)return s;let a=this.names.length>0?[...new Set(this.names)].sort():[...r.settings.keys()].sort(),n,c=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async f=>{if(r.invalid.size>0&&!this.json){for(let[p,h]of r.invalid)f.reportError(34,`Invalid configuration key "${p}" in ${h}`);f.reportSeparator()}if(this.json)for(let p of a){if(this.noDefaults&&!r.sources.has(p))continue;let h=r.settings.get(p);typeof h>"u"&&f.reportError(34,`No configuration key named "${p}"`);let E=r.getSpecial(p,{hideSecrets:!0,getNativePaths:!0}),C=r.sources.get(p)??"",S=C&&C[0]!=="<"?ue.fromPortablePath(C):C;f.reportJson({key:p,effective:E,source:S,...h})}else{let p={breakLength:1/0,colors:r.get("enableColors"),maxArrayLength:2},h={},E={children:h};for(let C of a){if(this.noDefaults&&!r.sources.has(C))continue;let S=r.settings.get(C),P=r.sources.get(C)??"",I=r.getSpecial(C,{hideSecrets:!0,getNativePaths:!0}),R={Description:{label:"Description",value:he.tuple(he.Type.MARKDOWN,{text:S.description,format:this.cli.format(),paragraphs:!1})},Source:{label:"Source",value:he.tuple(P[0]==="<"?he.Type.CODE:he.Type.PATH,P)}};h[C]={value:he.tuple(he.Type.CODE,C),children:R};let N=(U,W)=>{for(let[te,ie]of W)if(ie instanceof Map){let Ae={};U[te]={children:Ae},N(Ae,ie)}else U[te]={label:te,value:he.tuple(he.Type.NO_HINT,(0,nF.inspect)(ie,p))}};I instanceof Map?N(R,I):R.Value={label:"Value",value:he.tuple(he.Type.NO_HINT,(0,nF.inspect)(I,p))}}a.length!==1&&(n=void 0),Qs.emitTree(E,{configuration:r,json:this.json,stdout:this.context.stdout,separators:2})}});if(!this.json&&typeof n<"u"){let f=a[0],p=(0,nF.inspect)(r.getSpecial(f,{hideSecrets:!0,getNativePaths:!0}),{colors:r.get("enableColors")});this.context.stdout.write(` `),this.context.stdout.write(`${p} `)}return c.exitCode()}};Ve();Wt();Ul();var iF={};Vt(iF,{Strategy:()=>lS,acceptedStrategies:()=>wvt,dedupe:()=>P5});Ve();Ve();var V2e=et(Sa()),lS=(e=>(e.HIGHEST="highest",e))(lS||{}),wvt=new Set(Object.values(lS)),Bvt={highest:async(t,e,{resolver:r,fetcher:s,resolveOptions:a,fetchOptions:n})=>{let c=new Map;for(let[p,h]of t.storedResolutions){let E=t.storedDescriptors.get(p);if(typeof E>"u")throw new Error(`Assertion failed: The descriptor (${p}) should have been registered`);je.getSetWithDefault(c,E.identHash).add(h)}let f=new Map(je.mapAndFilter(t.storedDescriptors.values(),p=>q.isVirtualDescriptor(p)?je.mapAndFilter.skip:[p.descriptorHash,je.makeDeferred()]));for(let p of t.storedDescriptors.values()){let h=f.get(p.descriptorHash);if(typeof h>"u")throw new Error(`Assertion failed: The descriptor (${p.descriptorHash}) should have been registered`);let E=t.storedResolutions.get(p.descriptorHash);if(typeof E>"u")throw new Error(`Assertion failed: The resolution (${p.descriptorHash}) should have been registered`);let C=t.originalPackages.get(E);if(typeof C>"u")throw new Error(`Assertion failed: The package (${E}) should have been registered`);Promise.resolve().then(async()=>{let S=r.getResolutionDependencies(p,a),P=Object.fromEntries(await je.allSettledSafe(Object.entries(S).map(async([te,ie])=>{let Ae=f.get(ie.descriptorHash);if(typeof Ae>"u")throw new Error(`Assertion failed: The descriptor (${ie.descriptorHash}) should have been registered`);let ce=await Ae.promise;if(!ce)throw new Error("Assertion failed: Expected the dependency to have been through the dedupe process itself");return[te,ce.updatedPackage]})));if(e.length&&!V2e.default.isMatch(q.stringifyIdent(p),e)||!r.shouldPersistResolution(C,a))return C;let I=c.get(p.identHash);if(typeof I>"u")throw new Error(`Assertion failed: The resolutions (${p.identHash}) should have been registered`);if(I.size===1)return C;let R=[...I].map(te=>{let ie=t.originalPackages.get(te);if(typeof ie>"u")throw new Error(`Assertion failed: The package (${te}) should have been registered`);return ie}),N=await r.getSatisfying(p,P,R,a),U=N.locators?.[0];if(typeof U>"u"||!N.sorted)return C;let W=t.originalPackages.get(U.locatorHash);if(typeof W>"u")throw new Error(`Assertion failed: The package (${U.locatorHash}) should have been registered`);return W}).then(async S=>{let P=await t.preparePackage(S,{resolver:r,resolveOptions:a});h.resolve({descriptor:p,currentPackage:C,updatedPackage:S,resolvedPackage:P})}).catch(S=>{h.reject(S)})}return[...f.values()].map(p=>p.promise)}};async function P5(t,{strategy:e,patterns:r,cache:s,report:a}){let{configuration:n}=t,c=new Yi,f=n.makeResolver(),p=n.makeFetcher(),h={cache:s,checksums:t.storedChecksums,fetcher:p,project:t,report:c,cacheOptions:{skipIntegrityCheck:!0}},E={project:t,resolver:f,report:c,fetchOptions:h};return await a.startTimerPromise("Deduplication step",async()=>{let C=Bvt[e],S=await C(t,r,{resolver:f,resolveOptions:E,fetcher:p,fetchOptions:h}),P=ho.progressViaCounter(S.length);await a.reportProgress(P);let I=0;await Promise.all(S.map(U=>U.then(W=>{if(W===null||W.currentPackage.locatorHash===W.updatedPackage.locatorHash)return;I++;let{descriptor:te,currentPackage:ie,updatedPackage:Ae}=W;a.reportInfo(0,`${q.prettyDescriptor(n,te)} can be deduped from ${q.prettyLocator(n,ie)} to ${q.prettyLocator(n,Ae)}`),a.reportJson({descriptor:q.stringifyDescriptor(te),currentResolution:q.stringifyLocator(ie),updatedResolution:q.stringifyLocator(Ae)}),t.storedResolutions.set(te.descriptorHash,Ae.locatorHash)}).finally(()=>P.tick())));let R;switch(I){case 0:R="No packages";break;case 1:R="One package";break;default:R=`${I} packages`}let N=he.pretty(n,e,he.Type.CODE);return a.reportInfo(0,`${R} can be deduped using the ${N} strategy`),I})}var AC=class extends ut{constructor(){super(...arguments);this.strategy=ge.String("-s,--strategy","highest",{description:"The strategy to use when deduping dependencies",validator:po(lS)});this.check=ge.Boolean("-c,--check",!1,{description:"Exit with exit code 1 when duplicates are found, without persisting the dependency tree"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.mode=ge.String("--mode",{description:"Change what artifacts installs generate",validator:po(ec)});this.patterns=ge.Rest()}static{this.paths=[["dedupe"]]}static{this.usage=ot.Usage({description:"deduplicate dependencies with overlapping ranges",details:"\n Duplicates are defined as descriptors with overlapping ranges being resolved and locked to different locators. They are a natural consequence of Yarn's deterministic installs, but they can sometimes pile up and unnecessarily increase the size of your project.\n\n This command dedupes dependencies in the current project using different strategies (only one is implemented at the moment):\n\n - `highest`: Reuses (where possible) the locators with the highest versions. This means that dependencies can only be upgraded, never downgraded. It's also guaranteed that it never takes more than a single pass to dedupe the entire dependency tree.\n\n **Note:** Even though it never produces a wrong dependency tree, this command should be used with caution, as it modifies the dependency tree, which can sometimes cause problems when packages don't strictly follow semver recommendations. Because of this, it is recommended to also review the changes manually.\n\n If set, the `-c,--check` flag will only report the found duplicates, without persisting the modified dependency tree. If changes are found, the command will exit with a non-zero exit code, making it suitable for CI purposes.\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n ### In-depth explanation:\n\n Yarn doesn't deduplicate dependencies by default, otherwise installs wouldn't be deterministic and the lockfile would be useless. What it actually does is that it tries to not duplicate dependencies in the first place.\n\n **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@*`will cause Yarn to reuse `foo@2.3.4`, even if the latest `foo` is actually `foo@2.10.14`, thus preventing unnecessary duplication.\n\n Duplication happens when Yarn can't unlock dependencies that have already been locked inside the lockfile.\n\n **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@2.10.14` will cause Yarn to install `foo@2.10.14` because the existing resolution doesn't satisfy the range `2.10.14`. This behavior can lead to (sometimes) unwanted duplication, since now the lockfile contains 2 separate resolutions for the 2 `foo` descriptors, even though they have overlapping ranges, which means that the lockfile can be simplified so that both descriptors resolve to `foo@2.10.14`.\n ",examples:[["Dedupe all packages","$0 dedupe"],["Dedupe all packages using a specific strategy","$0 dedupe --strategy highest"],["Dedupe a specific package","$0 dedupe lodash"],["Dedupe all packages with the `@babel/*` scope","$0 dedupe '@babel/*'"],["Check for duplicates (can be used as a CI step)","$0 dedupe --check"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Tt.find(r,this.context.cwd),a=await Jr.find(r);await s.restoreInstallState({restoreResolutions:!1});let n=0,c=await Ot.start({configuration:r,includeFooter:!1,stdout:this.context.stdout,json:this.json},async f=>{n=await P5(s,{strategy:this.strategy,patterns:this.patterns,cache:a,report:f})});return c.hasErrors()?c.exitCode():this.check?n?1:0:await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:a,mode:this.mode})}};Ve();Wt();var pC=class extends ut{static{this.paths=[["--clipanion=definitions"]]}async execute(){let{plugins:e}=await ze.find(this.context.cwd,this.context.plugins),r=[];for(let c of e){let{commands:f}=c[1];if(f){let h=wa.from(f).definitions();r.push([c[0],h])}}let s=this.cli.definitions(),a=(c,f)=>c.split(" ").slice(1).join()===f.split(" ").slice(1).join(),n=K2e()["@yarnpkg/builder"].bundles.standard;for(let c of r){let f=c[1];for(let p of f)s.find(h=>a(h.path,p.path)).plugin={name:c[0],isDefault:n.includes(c[0])}}this.context.stdout.write(`${JSON.stringify(s,null,2)} `)}};var hC=class extends ut{static{this.paths=[["help"],["--help"],["-h"]]}async execute(){this.context.stdout.write(this.cli.usage(null))}};Ve();bt();Wt();var gC=class extends ut{constructor(){super(...arguments);this.leadingArgument=ge.String();this.args=ge.Proxy()}async execute(){if(this.leadingArgument.match(/[\\/]/)&&!q.tryParseIdent(this.leadingArgument)){let r=K.resolve(this.context.cwd,ue.toPortablePath(this.leadingArgument));return await this.cli.run(this.args,{cwd:r})}else return await this.cli.run(["run",this.leadingArgument,...this.args])}};Ve();var dC=class extends ut{static{this.paths=[["-v"],["--version"]]}async execute(){this.context.stdout.write(`${un||""} `)}};Ve();Ve();Wt();var mC=class extends ut{constructor(){super(...arguments);this.commandName=ge.String();this.args=ge.Proxy()}static{this.paths=[["exec"]]}static{this.usage=ot.Usage({description:"execute a shell script",details:` This command simply executes a shell script within the context of the root directory of the active workspace using the portable shell. It also makes sure to call it in a way that's compatible with the current project (for example, on PnP projects the environment will be setup in such a way that PnP will be correctly injected into the environment). `,examples:[["Execute a single shell command","$0 exec echo Hello World"],["Execute a shell script",'$0 exec "tsc & babel src --out-dir lib"']]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,locator:a}=await Tt.find(r,this.context.cwd);return await s.restoreInstallState(),await In.executePackageShellcode(a,this.commandName,this.args,{cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,project:s})}};Ve();Wt();Ul();var yC=class extends ut{constructor(){super(...arguments);this.hash=ge.String({required:!1,validator:qx(IE(),[X2(/^p[0-9a-f]{5}$/)])})}static{this.paths=[["explain","peer-requirements"]]}static{this.usage=ot.Usage({description:"explain a set of peer requirements",details:` A peer requirement represents all peer requests that a subject must satisfy when providing a requested package to requesters. When the hash argument is specified, this command prints a detailed explanation of the peer requirement corresponding to the hash and whether it is satisfied or not. When used without arguments, this command lists all peer requirements and the corresponding hash that can be used to get detailed information about a given requirement. **Note:** A hash is a six-letter p-prefixed code that can be obtained from peer dependency warnings or from the list of all peer requirements (\`yarn explain peer-requirements\`). `,examples:[["Explain the corresponding peer requirement for a hash","$0 explain peer-requirements p1a4ed"],["List all peer requirements","$0 explain peer-requirements"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Tt.find(r,this.context.cwd);return await s.restoreInstallState({restoreResolutions:!1}),await s.applyLightResolution(),typeof this.hash<"u"?await Svt(this.hash,s,{stdout:this.context.stdout}):await Dvt(s,{stdout:this.context.stdout})}};async function Svt(t,e,r){let s=e.peerRequirementNodes.get(t);if(typeof s>"u")throw new Error(`No peerDependency requirements found for hash: "${t}"`);let a=new Set,n=p=>a.has(p.requester.locatorHash)?{value:he.tuple(he.Type.DEPENDENT,{locator:p.requester,descriptor:p.descriptor}),children:p.children.size>0?[{value:he.tuple(he.Type.NO_HINT,"...")}]:[]}:(a.add(p.requester.locatorHash),{value:he.tuple(he.Type.DEPENDENT,{locator:p.requester,descriptor:p.descriptor}),children:Object.fromEntries(Array.from(p.children.values(),h=>[q.stringifyLocator(h.requester),n(h)]))}),c=e.peerWarnings.find(p=>p.hash===t);return(await Ot.start({configuration:e.configuration,stdout:r.stdout,includeFooter:!1,includePrefix:!1},async p=>{let h=he.mark(e.configuration),E=c?h.Cross:h.Check;if(p.reportInfo(0,`Package ${he.pretty(e.configuration,s.subject,he.Type.LOCATOR)} is requested to provide ${he.pretty(e.configuration,s.ident,he.Type.IDENT)} by its descendants`),p.reportSeparator(),p.reportInfo(0,he.pretty(e.configuration,s.subject,he.Type.LOCATOR)),Qs.emitTree({children:Object.fromEntries(Array.from(s.requests.values(),C=>[q.stringifyLocator(C.requester),n(C)]))},{configuration:e.configuration,stdout:r.stdout,json:!1}),p.reportSeparator(),s.provided.range==="missing:"){let C=c?"":" , but all peer requests are optional";p.reportInfo(0,`${E} Package ${he.pretty(e.configuration,s.subject,he.Type.LOCATOR)} does not provide ${he.pretty(e.configuration,s.ident,he.Type.IDENT)}${C}.`)}else{let C=e.storedResolutions.get(s.provided.descriptorHash);if(!C)throw new Error("Assertion failed: Expected the descriptor to be registered");let S=e.storedPackages.get(C);if(!S)throw new Error("Assertion failed: Expected the package to be registered");p.reportInfo(0,`${E} Package ${he.pretty(e.configuration,s.subject,he.Type.LOCATOR)} provides ${he.pretty(e.configuration,s.ident,he.Type.IDENT)} with version ${q.prettyReference(e.configuration,S.version??"0.0.0")}, ${c?"which does not satisfy all requests.":"which satisfies all requests"}`),c?.type===3&&(c.range?p.reportInfo(0,` The combined requested range is ${he.pretty(e.configuration,c.range,he.Type.RANGE)}`):p.reportInfo(0," Unfortunately, the requested ranges have no overlap"))}})).exitCode()}async function Dvt(t,e){return(await Ot.start({configuration:t.configuration,stdout:e.stdout,includeFooter:!1,includePrefix:!1},async s=>{let a=he.mark(t.configuration),n=je.sortMap(t.peerRequirementNodes,[([,c])=>q.stringifyLocator(c.subject),([,c])=>q.stringifyIdent(c.ident)]);for(let[,c]of n.values()){if(!c.root)continue;let f=t.peerWarnings.find(E=>E.hash===c.hash),p=[...q.allPeerRequests(c)],h;if(p.length>2?h=` and ${p.length-1} other dependencies`:p.length===2?h=" and 1 other dependency":h="",c.provided.range!=="missing:"){let E=t.storedResolutions.get(c.provided.descriptorHash);if(!E)throw new Error("Assertion failed: Expected the resolution to have been registered");let C=t.storedPackages.get(E);if(!C)throw new Error("Assertion failed: Expected the provided package to have been registered");let S=`${he.pretty(t.configuration,c.hash,he.Type.CODE)} \u2192 ${f?a.Cross:a.Check} ${q.prettyLocator(t.configuration,c.subject)} provides ${q.prettyLocator(t.configuration,C)} to ${q.prettyLocator(t.configuration,p[0].requester)}${h}`;f?s.reportWarning(0,S):s.reportInfo(0,S)}else{let E=`${he.pretty(t.configuration,c.hash,he.Type.CODE)} \u2192 ${f?a.Cross:a.Check} ${q.prettyLocator(t.configuration,c.subject)} doesn't provide ${q.prettyIdent(t.configuration,c.ident)} to ${q.prettyLocator(t.configuration,p[0].requester)}${h}`;f?s.reportWarning(0,E):s.reportInfo(0,E)}}})).exitCode()}Ve();Wt();Ul();Ve();Ve();bt();Wt();var J2e=et(Ai()),EC=class extends ut{constructor(){super(...arguments);this.useYarnPath=ge.Boolean("--yarn-path",{description:"Set the yarnPath setting even if the version can be accessed by Corepack"});this.onlyIfNeeded=ge.Boolean("--only-if-needed",!1,{description:"Only lock the Yarn version if it isn't already locked"});this.version=ge.String()}static{this.paths=[["set","version"]]}static{this.usage=ot.Usage({description:"lock the Yarn version used by the project",details:"\n This command will set a specific release of Yarn to be used by Corepack: https://nodejs.org/api/corepack.html.\n\n By default it only will set the `packageManager` field at the root of your project, but if the referenced release cannot be represented this way, if you already have `yarnPath` configured, or if you set the `--yarn-path` command line flag, then the release will also be downloaded from the Yarn GitHub repository, stored inside your project, and referenced via the `yarnPath` settings from your project `.yarnrc.yml` file.\n\n A very good use case for this command is to enforce the version of Yarn used by any single member of your team inside the same project - by doing this you ensure that you have control over Yarn upgrades and downgrades (including on your deployment servers), and get rid of most of the headaches related to someone using a slightly different version and getting different behavior.\n\n The version specifier can be:\n\n - a tag:\n - `latest` / `berry` / `stable` -> the most recent stable berry (`>=2.0.0`) release\n - `canary` -> the most recent canary (release candidate) berry (`>=2.0.0`) release\n - `classic` -> the most recent classic (`^0.x || ^1.x`) release\n\n - a semver range (e.g. `2.x`) -> the most recent version satisfying the range (limited to berry releases)\n\n - a semver version (e.g. `2.4.1`, `1.22.1`)\n\n - a local file referenced through either a relative or absolute path\n\n - `self` -> the version used to invoke the command\n ",examples:[["Download the latest release from the Yarn repository","$0 set version latest"],["Download the latest canary release from the Yarn repository","$0 set version canary"],["Download the latest classic release from the Yarn repository","$0 set version classic"],["Download the most recent Yarn 3 build","$0 set version 3.x"],["Download a specific Yarn 2 build","$0 set version 2.0.0-rc.30"],["Switch back to a specific Yarn 1 release","$0 set version 1.22.1"],["Use a release from the local filesystem","$0 set version ./yarn.cjs"],["Use a release from a URL","$0 set version https://repo.yarnpkg.com/3.1.0/packages/yarnpkg-cli/bin/yarn.js"],["Download the version used to invoke the command","$0 set version self"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);if(this.onlyIfNeeded&&r.get("yarnPath")){let f=r.sources.get("yarnPath");if(!f)throw new Error("Assertion failed: Expected 'yarnPath' to have a source");let p=r.projectCwd??r.startingCwd;if(K.contains(p,f))return 0}let s=()=>{if(typeof un>"u")throw new nt("The --install flag can only be used without explicit version specifier from the Yarn CLI");return`file://${process.argv[1]}`},a,n=(f,p)=>({version:p,url:f.replace(/\{\}/g,p)});if(this.version==="self")a={url:s(),version:un??"self"};else if(this.version==="latest"||this.version==="berry"||this.version==="stable")a=n("https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js",await cS(r,"stable"));else if(this.version==="canary")a=n("https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js",await cS(r,"canary"));else if(this.version==="classic")a={url:"https://classic.yarnpkg.com/latest.js",version:"classic"};else if(this.version.match(/^https?:/))a={url:this.version,version:"remote"};else if(this.version.match(/^\.{0,2}[\\/]/)||ue.isAbsolute(this.version))a={url:`file://${K.resolve(ue.toPortablePath(this.version))}`,version:"file"};else if(Or.satisfiesWithPrereleases(this.version,">=2.0.0"))a=n("https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js",this.version);else if(Or.satisfiesWithPrereleases(this.version,"^0.x || ^1.x"))a=n("https://github.com/yarnpkg/yarn/releases/download/v{}/yarn-{}.js",this.version);else if(Or.validRange(this.version))a=n("https://repo.yarnpkg.com/{}/packages/yarnpkg-cli/bin/yarn.js",await bvt(r,this.version));else throw new nt(`Invalid version descriptor "${this.version}"`);return(await Ot.start({configuration:r,stdout:this.context.stdout,includeLogs:!this.context.quiet},async f=>{let p=async()=>{let h="file://";return a.url.startsWith(h)?(f.reportInfo(0,`Retrieving ${he.pretty(r,a.url,he.Type.PATH)}`),await le.readFilePromise(a.url.slice(h.length))):(f.reportInfo(0,`Downloading ${he.pretty(r,a.url,he.Type.URL)}`),await An.get(a.url,{configuration:r}))};await x5(r,a.version,p,{report:f,useYarnPath:this.useYarnPath})})).exitCode()}};async function bvt(t,e){let s=(await An.get("https://repo.yarnpkg.com/tags",{configuration:t,jsonResponse:!0})).tags.filter(a=>Or.satisfiesWithPrereleases(a,e));if(s.length===0)throw new nt(`No matching release found for range ${he.pretty(t,e,he.Type.RANGE)}.`);return s[0]}async function cS(t,e){let r=await An.get("https://repo.yarnpkg.com/tags",{configuration:t,jsonResponse:!0});if(!r.latest[e])throw new nt(`Tag ${he.pretty(t,e,he.Type.RANGE)} not found`);return r.latest[e]}async function x5(t,e,r,{report:s,useYarnPath:a}){let n,c=async()=>(typeof n>"u"&&(n=await r()),n);if(e===null){let te=await c();await le.mktempPromise(async ie=>{let Ae=K.join(ie,"yarn.cjs");await le.writeFilePromise(Ae,te);let{stdout:ce}=await Gr.execvp(process.execPath,[ue.fromPortablePath(Ae),"--version"],{cwd:ie,env:{...t.env,YARN_IGNORE_PATH:"1"}});if(e=ce.trim(),!J2e.default.valid(e))throw new Error(`Invalid semver version. ${he.pretty(t,"yarn --version",he.Type.CODE)} returned: ${e}`)})}let f=t.projectCwd??t.startingCwd,p=K.resolve(f,".yarn/releases"),h=K.resolve(p,`yarn-${e}.cjs`),E=K.relative(t.startingCwd,h),C=je.isTaggedYarnVersion(e),S=t.get("yarnPath"),P=!C,I=P||!!S||!!a;if(a===!1){if(P)throw new Yt(0,"You explicitly opted out of yarnPath usage in your command line, but the version you specified cannot be represented by Corepack");I=!1}else!I&&!process.env.COREPACK_ROOT&&(s.reportWarning(0,`You don't seem to have ${he.applyHyperlink(t,"Corepack","https://nodejs.org/api/corepack.html")} enabled; we'll have to rely on ${he.applyHyperlink(t,"yarnPath","https://yarnpkg.com/configuration/yarnrc#yarnPath")} instead`),I=!0);if(I){let te=await c();s.reportInfo(0,`Saving the new release in ${he.pretty(t,E,"magenta")}`),await le.removePromise(K.dirname(h)),await le.mkdirPromise(K.dirname(h),{recursive:!0}),await le.writeFilePromise(h,te,{mode:493}),await ze.updateConfiguration(f,{yarnPath:K.relative(f,h)})}else await le.removePromise(K.dirname(h)),await ze.updateConfiguration(f,{yarnPath:ze.deleteProperty});let R=await Ht.tryFind(f)||new Ht;R.packageManager=`yarn@${C?e:await cS(t,"stable")}`;let N={};R.exportTo(N);let U=K.join(f,Ht.fileName),W=`${JSON.stringify(N,null,R.indent)} `;return await le.changeFilePromise(U,W,{automaticNewlines:!0}),{bundleVersion:e}}function z2e(t){return Dr[zx(t)]}var Pvt=/## (?YN[0-9]{4}) - `(?[A-Z_]+)`\n\n(?
(?:.(?!##))+)/gs;async function xvt(t){let r=`https://repo.yarnpkg.com/${je.isTaggedYarnVersion(un)?un:await cS(t,"canary")}/packages/docusaurus/docs/advanced/01-general-reference/error-codes.mdx`,s=await An.get(r,{configuration:t});return new Map(Array.from(s.toString().matchAll(Pvt),({groups:a})=>{if(!a)throw new Error("Assertion failed: Expected the match to have been successful");let n=z2e(a.code);if(a.name!==n)throw new Error(`Assertion failed: Invalid error code data: Expected "${a.name}" to be named "${n}"`);return[a.code,a.details]}))}var IC=class extends ut{constructor(){super(...arguments);this.code=ge.String({required:!1,validator:$2(IE(),[X2(/^YN[0-9]{4}$/)])});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["explain"]]}static{this.usage=ot.Usage({description:"explain an error code",details:` When the code argument is specified, this command prints its name and its details. When used without arguments, this command lists all error codes and their names. `,examples:[["Explain an error code","$0 explain YN0006"],["List all error codes","$0 explain"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);if(typeof this.code<"u"){let s=z2e(this.code),a=he.pretty(r,s,he.Type.CODE),n=this.cli.format().header(`${this.code} - ${a}`),f=(await xvt(r)).get(this.code),p=typeof f<"u"?he.jsonOrPretty(this.json,r,he.tuple(he.Type.MARKDOWN,{text:f,format:this.cli.format(),paragraphs:!0})):`This error code does not have a description. You can help us by editing this page on GitHub \u{1F642}: ${he.jsonOrPretty(this.json,r,he.tuple(he.Type.URL,"https://github.com/yarnpkg/berry/blob/master/packages/docusaurus/docs/advanced/01-general-reference/error-codes.mdx"))} `;this.json?this.context.stdout.write(`${JSON.stringify({code:this.code,name:s,details:p})} `):this.context.stdout.write(`${n} ${p} `)}else{let s={children:je.mapAndFilter(Object.entries(Dr),([a,n])=>Number.isNaN(Number(a))?je.mapAndFilter.skip:{label:Vf(Number(a)),value:he.tuple(he.Type.CODE,n)})};Qs.emitTree(s,{configuration:r,stdout:this.context.stdout,json:this.json})}}};Ve();bt();Wt();var Z2e=et(Sa()),CC=class extends ut{constructor(){super(...arguments);this.all=ge.Boolean("-A,--all",!1,{description:"Print versions of a package from the whole project"});this.recursive=ge.Boolean("-R,--recursive",!1,{description:"Print information for all packages, including transitive dependencies"});this.extra=ge.Array("-X,--extra",[],{description:"An array of requests of extra data provided by plugins"});this.cache=ge.Boolean("--cache",!1,{description:"Print information about the cache entry of a package (path, size, checksum)"});this.dependents=ge.Boolean("--dependents",!1,{description:"Print all dependents for each matching package"});this.manifest=ge.Boolean("--manifest",!1,{description:"Print data obtained by looking at the package archive (license, homepage, ...)"});this.nameOnly=ge.Boolean("--name-only",!1,{description:"Only print the name for the matching packages"});this.virtuals=ge.Boolean("--virtuals",!1,{description:"Print each instance of the virtual packages"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.patterns=ge.Rest()}static{this.paths=[["info"]]}static{this.usage=ot.Usage({description:"see information related to packages",details:"\n This command prints various information related to the specified packages, accepting glob patterns.\n\n By default, if the locator reference is missing, Yarn will default to print the information about all the matching direct dependencies of the package for the active workspace. To instead print all versions of the package that are direct dependencies of any of your workspaces, use the `-A,--all` flag. Adding the `-R,--recursive` flag will also report transitive dependencies.\n\n Some fields will be hidden by default in order to keep the output readable, but can be selectively displayed by using additional options (`--dependents`, `--manifest`, `--virtuals`, ...) described in the option descriptions.\n\n Note that this command will only print the information directly related to the selected packages - if you wish to know why the package is there in the first place, use `yarn why` which will do just that (it also provides a `-R,--recursive` flag that may be of some help).\n ",examples:[["Show information about Lodash","$0 info lodash"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(!a&&!this.all)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let c=new Set(this.extra);this.cache&&c.add("cache"),this.dependents&&c.add("dependents"),this.manifest&&c.add("manifest");let f=(ie,{recursive:Ae})=>{let ce=ie.anchoredLocator.locatorHash,me=new Map,pe=[ce];for(;pe.length>0;){let Be=pe.shift();if(me.has(Be))continue;let Ce=s.storedPackages.get(Be);if(typeof Ce>"u")throw new Error("Assertion failed: Expected the package to be registered");if(me.set(Be,Ce),q.isVirtualLocator(Ce)&&pe.push(q.devirtualizeLocator(Ce).locatorHash),!(!Ae&&Be!==ce))for(let g of Ce.dependencies.values()){let we=s.storedResolutions.get(g.descriptorHash);if(typeof we>"u")throw new Error("Assertion failed: Expected the resolution to be registered");pe.push(we)}}return me.values()},p=({recursive:ie})=>{let Ae=new Map;for(let ce of s.workspaces)for(let me of f(ce,{recursive:ie}))Ae.set(me.locatorHash,me);return Ae.values()},h=({all:ie,recursive:Ae})=>ie&&Ae?s.storedPackages.values():ie?p({recursive:Ae}):f(a,{recursive:Ae}),E=({all:ie,recursive:Ae})=>{let ce=h({all:ie,recursive:Ae}),me=this.patterns.map(Ce=>{let g=q.parseLocator(Ce),we=Z2e.default.makeRe(q.stringifyIdent(g)),ye=q.isVirtualLocator(g),fe=ye?q.devirtualizeLocator(g):g;return se=>{let X=q.stringifyIdent(se);if(!we.test(X))return!1;if(g.reference==="unknown")return!0;let De=q.isVirtualLocator(se),Re=De?q.devirtualizeLocator(se):se;return!(ye&&De&&g.reference!==se.reference||fe.reference!==Re.reference)}}),pe=je.sortMap([...ce],Ce=>q.stringifyLocator(Ce));return{selection:pe.filter(Ce=>me.length===0||me.some(g=>g(Ce))),sortedLookup:pe}},{selection:C,sortedLookup:S}=E({all:this.all,recursive:this.recursive});if(C.length===0)throw new nt("No package matched your request");let P=new Map;if(this.dependents)for(let ie of S)for(let Ae of ie.dependencies.values()){let ce=s.storedResolutions.get(Ae.descriptorHash);if(typeof ce>"u")throw new Error("Assertion failed: Expected the resolution to be registered");je.getArrayWithDefault(P,ce).push(ie)}let I=new Map;for(let ie of S){if(!q.isVirtualLocator(ie))continue;let Ae=q.devirtualizeLocator(ie);je.getArrayWithDefault(I,Ae.locatorHash).push(ie)}let R={},N={children:R},U=r.makeFetcher(),W={project:s,fetcher:U,cache:n,checksums:s.storedChecksums,report:new Yi,cacheOptions:{skipIntegrityCheck:!0}},te=[async(ie,Ae,ce)=>{if(!Ae.has("manifest"))return;let me=await U.fetch(ie,W),pe;try{pe=await Ht.find(me.prefixPath,{baseFs:me.packageFs})}finally{me.releaseFs?.()}ce("Manifest",{License:he.tuple(he.Type.NO_HINT,pe.license),Homepage:he.tuple(he.Type.URL,pe.raw.homepage??null)})},async(ie,Ae,ce)=>{if(!Ae.has("cache"))return;let me=s.storedChecksums.get(ie.locatorHash)??null,pe=n.getLocatorPath(ie,me),Be;if(pe!==null)try{Be=await le.statPromise(pe)}catch{}let Ce=typeof Be<"u"?[Be.size,he.Type.SIZE]:void 0;ce("Cache",{Checksum:he.tuple(he.Type.NO_HINT,me),Path:he.tuple(he.Type.PATH,pe),Size:Ce})}];for(let ie of C){let Ae=q.isVirtualLocator(ie);if(!this.virtuals&&Ae)continue;let ce={},me={value:[ie,he.Type.LOCATOR],children:ce};if(R[q.stringifyLocator(ie)]=me,this.nameOnly){delete me.children;continue}let pe=I.get(ie.locatorHash);typeof pe<"u"&&(ce.Instances={label:"Instances",value:he.tuple(he.Type.NUMBER,pe.length)}),ce.Version={label:"Version",value:he.tuple(he.Type.NO_HINT,ie.version)};let Be=(g,we)=>{let ye={};if(ce[g]=ye,Array.isArray(we))ye.children=we.map(fe=>({value:fe}));else{let fe={};ye.children=fe;for(let[se,X]of Object.entries(we))typeof X>"u"||(fe[se]={label:se,value:X})}};if(!Ae){for(let g of te)await g(ie,c,Be);await r.triggerHook(g=>g.fetchPackageInfo,ie,c,Be)}ie.bin.size>0&&!Ae&&Be("Exported Binaries",[...ie.bin.keys()].map(g=>he.tuple(he.Type.PATH,g)));let Ce=P.get(ie.locatorHash);typeof Ce<"u"&&Ce.length>0&&Be("Dependents",Ce.map(g=>he.tuple(he.Type.LOCATOR,g))),ie.dependencies.size>0&&!Ae&&Be("Dependencies",[...ie.dependencies.values()].map(g=>{let we=s.storedResolutions.get(g.descriptorHash),ye=typeof we<"u"?s.storedPackages.get(we)??null:null;return he.tuple(he.Type.RESOLUTION,{descriptor:g,locator:ye})})),ie.peerDependencies.size>0&&Ae&&Be("Peer dependencies",[...ie.peerDependencies.values()].map(g=>{let we=ie.dependencies.get(g.identHash),ye=typeof we<"u"?s.storedResolutions.get(we.descriptorHash)??null:null,fe=ye!==null?s.storedPackages.get(ye)??null:null;return he.tuple(he.Type.RESOLUTION,{descriptor:g,locator:fe})}))}Qs.emitTree(N,{configuration:r,json:this.json,stdout:this.context.stdout,separators:this.nameOnly?0:2})}};Ve();bt();Bc();var sF=et(Rd());Wt();var k5=et(Ai());Ul();var kvt=[{selector:t=>t===-1,name:"nodeLinker",value:"node-modules"},{selector:t=>t!==-1&&t<8,name:"enableGlobalCache",value:!1},{selector:t=>t!==-1&&t<8,name:"compressionLevel",value:"mixed"}],wC=class extends ut{constructor(){super(...arguments);this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.immutable=ge.Boolean("--immutable",{description:"Abort with an error exit code if the lockfile was to be modified"});this.immutableCache=ge.Boolean("--immutable-cache",{description:"Abort with an error exit code if the cache folder was to be modified"});this.refreshLockfile=ge.Boolean("--refresh-lockfile",{description:"Refresh the package metadata stored in the lockfile"});this.checkCache=ge.Boolean("--check-cache",{description:"Always refetch the packages and ensure that their checksums are consistent"});this.checkResolutions=ge.Boolean("--check-resolutions",{description:"Validates that the package resolutions are coherent"});this.inlineBuilds=ge.Boolean("--inline-builds",{description:"Verbosely print the output of the build steps of dependencies"});this.mode=ge.String("--mode",{description:"Change what artifacts installs generate",validator:po(ec)});this.cacheFolder=ge.String("--cache-folder",{hidden:!0});this.frozenLockfile=ge.Boolean("--frozen-lockfile",{hidden:!0});this.ignoreEngines=ge.Boolean("--ignore-engines",{hidden:!0});this.nonInteractive=ge.Boolean("--non-interactive",{hidden:!0});this.preferOffline=ge.Boolean("--prefer-offline",{hidden:!0});this.production=ge.Boolean("--production",{hidden:!0});this.registry=ge.String("--registry",{hidden:!0});this.silent=ge.Boolean("--silent",{hidden:!0});this.networkTimeout=ge.String("--network-timeout",{hidden:!0})}static{this.paths=[["install"],ot.Default]}static{this.usage=ot.Usage({description:"install the project dependencies",details:"\n This command sets up your project if needed. The installation is split into four different steps that each have their own characteristics:\n\n - **Resolution:** First the package manager will resolve your dependencies. The exact way a dependency version is privileged over another isn't standardized outside of the regular semver guarantees. If a package doesn't resolve to what you would expect, check that all dependencies are correctly declared (also check our website for more information: ).\n\n - **Fetch:** Then we download all the dependencies if needed, and make sure that they're all stored within our cache (check the value of `cacheFolder` in `yarn config` to see where the cache files are stored).\n\n - **Link:** Then we send the dependency tree information to internal plugins tasked with writing them on the disk in some form (for example by generating the `.pnp.cjs` file you might know).\n\n - **Build:** Once the dependency tree has been written on the disk, the package manager will now be free to run the build scripts for all packages that might need it, in a topological order compatible with the way they depend on one another. See https://yarnpkg.com/advanced/lifecycle-scripts for detail.\n\n Note that running this command is not part of the recommended workflow. Yarn supports zero-installs, which means that as long as you store your cache and your `.pnp.cjs` file inside your repository, everything will work without requiring any install right after cloning your repository or switching branches.\n\n If the `--immutable` option is set (defaults to true on CI), Yarn will abort with an error exit code if the lockfile was to be modified (other paths can be added using the `immutablePatterns` configuration setting). For backward compatibility we offer an alias under the name of `--frozen-lockfile`, but it will be removed in a later release.\n\n If the `--immutable-cache` option is set, Yarn will abort with an error exit code if the cache folder was to be modified (either because files would be added, or because they'd be removed).\n\n If the `--refresh-lockfile` option is set, Yarn will keep the same resolution for the packages currently in the lockfile but will refresh their metadata. If used together with `--immutable`, it can validate that the lockfile information are consistent. This flag is enabled by default when Yarn detects it runs within a pull request context.\n\n If the `--check-cache` option is set, Yarn will always refetch the packages and will ensure that their checksum matches what's 1/ described in the lockfile 2/ inside the existing cache files (if present). This is recommended as part of your CI workflow if you're both following the Zero-Installs model and accepting PRs from third-parties, as they'd otherwise have the ability to alter the checked-in packages before submitting them.\n\n If the `--inline-builds` option is set, Yarn will verbosely print the output of the build steps of your dependencies (instead of writing them into individual files). This is likely useful mostly for debug purposes only when using Docker-like environments.\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n ",examples:[["Install the project","$0 install"],["Validate a project when using Zero-Installs","$0 install --immutable --immutable-cache"],["Validate a project when using Zero-Installs (slightly safer if you accept external PRs)","$0 install --immutable --immutable-cache --check-cache"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);typeof this.inlineBuilds<"u"&&r.useWithSource("",{enableInlineBuilds:this.inlineBuilds},r.startingCwd,{overwrite:!0});let s=!!process.env.FUNCTION_TARGET||!!process.env.GOOGLE_RUNTIME,a=await vI({configuration:r,stdout:this.context.stdout},[{option:this.ignoreEngines,message:"The --ignore-engines option is deprecated; engine checking isn't a core feature anymore",error:!sF.default.VERCEL},{option:this.registry,message:"The --registry option is deprecated; prefer setting npmRegistryServer in your .yarnrc.yml file"},{option:this.preferOffline,message:"The --prefer-offline flag is deprecated; use the --cached flag with 'yarn add' instead",error:!sF.default.VERCEL},{option:this.production,message:"The --production option is deprecated on 'install'; use 'yarn workspaces focus' instead",error:!0},{option:this.nonInteractive,message:"The --non-interactive option is deprecated",error:!s},{option:this.frozenLockfile,message:"The --frozen-lockfile option is deprecated; use --immutable and/or --immutable-cache instead",callback:()=>this.immutable=this.frozenLockfile},{option:this.cacheFolder,message:"The cache-folder option has been deprecated; use rc settings instead",error:!sF.default.NETLIFY}]);if(a!==null)return a;let n=this.mode==="update-lockfile";if(n&&(this.immutable||this.immutableCache))throw new nt(`${he.pretty(r,"--immutable",he.Type.CODE)} and ${he.pretty(r,"--immutable-cache",he.Type.CODE)} cannot be used with ${he.pretty(r,"--mode=update-lockfile",he.Type.CODE)}`);let c=(this.immutable??r.get("enableImmutableInstalls"))&&!n,f=this.immutableCache&&!n;if(r.projectCwd!==null){let R=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async N=>{let U=!1;await Rvt(r,c)&&(N.reportInfo(48,"Automatically removed core plugins that are now builtins \u{1F44D}"),U=!0),await Tvt(r,c)&&(N.reportInfo(48,"Automatically fixed merge conflicts \u{1F44D}"),U=!0),U&&N.reportSeparator()});if(R.hasErrors())return R.exitCode()}if(r.projectCwd!==null){let R=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async N=>{if(ze.telemetry?.isNew)ze.telemetry.commitTips(),N.reportInfo(65,"Yarn will periodically gather anonymous telemetry: https://yarnpkg.com/advanced/telemetry"),N.reportInfo(65,`Run ${he.pretty(r,"yarn config set --home enableTelemetry 0",he.Type.CODE)} to disable`),N.reportSeparator();else if(ze.telemetry?.shouldShowTips){let U=await An.get("https://repo.yarnpkg.com/tags",{configuration:r,jsonResponse:!0}).catch(()=>null);if(U!==null){let W=null;if(un!==null){let ie=k5.default.prerelease(un)?"canary":"stable",Ae=U.latest[ie];k5.default.gt(Ae,un)&&(W=[ie,Ae])}if(W)ze.telemetry.commitTips(),N.reportInfo(88,`${he.applyStyle(r,`A new ${W[0]} version of Yarn is available:`,he.Style.BOLD)} ${q.prettyReference(r,W[1])}!`),N.reportInfo(88,`Upgrade now by running ${he.pretty(r,`yarn set version ${W[1]}`,he.Type.CODE)}`),N.reportSeparator();else{let te=ze.telemetry.selectTip(U.tips);te&&(N.reportInfo(89,he.pretty(r,te.message,he.Type.MARKDOWN_INLINE)),te.url&&N.reportInfo(89,`Learn more at ${te.url}`),N.reportSeparator())}}}});if(R.hasErrors())return R.exitCode()}let{project:p,workspace:h}=await Tt.find(r,this.context.cwd),E=p.lockfileLastVersion;if(E!==null){let R=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async N=>{let U={};for(let W of kvt)W.selector(E)&&typeof r.sources.get(W.name)>"u"&&(r.use("",{[W.name]:W.value},p.cwd,{overwrite:!0}),U[W.name]=W.value);Object.keys(U).length>0&&(await ze.updateConfiguration(p.cwd,U),N.reportInfo(87,"Migrated your project to the latest Yarn version \u{1F680}"),N.reportSeparator())});if(R.hasErrors())return R.exitCode()}let C=await Jr.find(r,{immutable:f,check:this.checkCache});if(!h)throw new ar(p.cwd,this.context.cwd);await p.restoreInstallState({restoreResolutions:!1});let S=r.get("enableHardenedMode");S&&typeof r.sources.get("enableHardenedMode")>"u"&&await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,includeFooter:!1},async R=>{R.reportWarning(0,"Yarn detected that the current workflow is executed from a public pull request. For safety the hardened mode has been enabled."),R.reportWarning(0,`It will prevent malicious lockfile manipulations, in exchange for a slower install time. You can opt-out if necessary; check our ${he.applyHyperlink(r,"documentation","https://yarnpkg.com/features/security#hardened-mode")} for more details.`),R.reportSeparator()}),(this.refreshLockfile??S)&&(p.lockfileNeedsRefresh=!0);let P=this.checkResolutions??S;return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout,forceSectionAlignment:!0,includeLogs:!0,includeVersion:!0},async R=>{await p.install({cache:C,report:R,immutable:c,checkResolutions:P,mode:this.mode})})).exitCode()}},Qvt="<<<<<<<";async function Tvt(t,e){if(!t.projectCwd)return!1;let r=K.join(t.projectCwd,Er.lockfile);if(!await le.existsPromise(r)||!(await le.readFilePromise(r,"utf8")).includes(Qvt))return!1;if(e)throw new Yt(47,"Cannot autofix a lockfile when running an immutable install");let a=await Gr.execvp("git",["rev-parse","MERGE_HEAD","HEAD"],{cwd:t.projectCwd});if(a.code!==0&&(a=await Gr.execvp("git",["rev-parse","REBASE_HEAD","HEAD"],{cwd:t.projectCwd})),a.code!==0&&(a=await Gr.execvp("git",["rev-parse","CHERRY_PICK_HEAD","HEAD"],{cwd:t.projectCwd})),a.code!==0)throw new Yt(83,"Git returned an error when trying to find the commits pertaining to the conflict");let n=await Promise.all(a.stdout.trim().split(/\n/).map(async f=>{let p=await Gr.execvp("git",["show",`${f}:./${Er.lockfile}`],{cwd:t.projectCwd});if(p.code!==0)throw new Yt(83,`Git returned an error when trying to access the lockfile content in ${f}`);try{return cs(p.stdout)}catch{throw new Yt(46,"A variant of the conflicting lockfile failed to parse")}}));n=n.filter(f=>!!f.__metadata);for(let f of n){if(f.__metadata.version<7)for(let p of Object.keys(f)){if(p==="__metadata")continue;let h=q.parseDescriptor(p,!0),E=t.normalizeDependency(h),C=q.stringifyDescriptor(E);C!==p&&(f[C]=f[p],delete f[p])}for(let p of Object.keys(f)){if(p==="__metadata")continue;let h=f[p].checksum;typeof h>"u"||h.includes("/")||(f[p].checksum=`${f.__metadata.cacheKey}/${h}`)}}let c=Object.assign({},...n);c.__metadata.version=`${Math.min(...n.map(f=>parseInt(f.__metadata.version??0)))}`,c.__metadata.cacheKey="merged";for(let[f,p]of Object.entries(c))typeof p=="string"&&delete c[f];return await le.changeFilePromise(r,il(c),{automaticNewlines:!0}),!0}async function Rvt(t,e){if(!t.projectCwd)return!1;let r=[],s=K.join(t.projectCwd,".yarn/plugins/@yarnpkg");return await ze.updateConfiguration(t.projectCwd,{plugins:n=>{if(!Array.isArray(n))return n;let c=n.filter(f=>{if(!f.path)return!0;let p=K.resolve(t.projectCwd,f.path),h=hv.has(f.spec)&&K.contains(s,p);return h&&r.push(p),!h});return c.length===0?ze.deleteProperty:c.length===n.length?n:c}},{immutable:e})?(await Promise.all(r.map(async n=>{await le.removePromise(n)})),!0):!1}Ve();bt();Wt();var BC=class extends ut{constructor(){super(...arguments);this.all=ge.Boolean("-A,--all",!1,{description:"Link all workspaces belonging to the target projects to the current one"});this.private=ge.Boolean("-p,--private",!1,{description:"Also link private workspaces belonging to the target projects to the current one"});this.relative=ge.Boolean("-r,--relative",!1,{description:"Link workspaces using relative paths instead of absolute paths"});this.destinations=ge.Rest()}static{this.paths=[["link"]]}static{this.usage=ot.Usage({description:"connect the local project to another one",details:"\n This command will set a new `resolutions` field in the project-level manifest and point it to the workspace at the specified location (even if part of another project).\n ",examples:[["Register one or more remote workspaces for use in the current project","$0 link ~/ts-loader ~/jest"],["Register all workspaces from a remote project for use in the current project","$0 link ~/jest --all"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=s.topLevelWorkspace,f=[];for(let p of this.destinations){let h=K.resolve(this.context.cwd,ue.toPortablePath(p)),E=await ze.find(h,this.context.plugins,{useRc:!1,strict:!1}),{project:C,workspace:S}=await Tt.find(E,h);if(s.cwd===C.cwd)throw new nt(`Invalid destination '${p}'; Can't link the project to itself`);if(!S)throw new ar(C.cwd,h);if(this.all){let P=!1;for(let I of C.workspaces)I.manifest.name&&(!I.manifest.private||this.private)&&(f.push(I),P=!0);if(!P)throw new nt(`No workspace found to be linked in the target project: ${p}`)}else{if(!S.manifest.name)throw new nt(`The target workspace at '${p}' doesn't have a name and thus cannot be linked`);if(S.manifest.private&&!this.private)throw new nt(`The target workspace at '${p}' is marked private - use the --private flag to link it anyway`);f.push(S)}}for(let p of f){let h=q.stringifyIdent(p.anchoredLocator),E=this.relative?K.relative(s.cwd,p.cwd):p.cwd;c.manifest.resolutions.push({pattern:{descriptor:{fullName:h}},reference:`portal:${E}`})}return await s.installWithNewReport({stdout:this.context.stdout},{cache:n})}};Wt();var vC=class extends ut{constructor(){super(...arguments);this.args=ge.Proxy()}static{this.paths=[["node"]]}static{this.usage=ot.Usage({description:"run node with the hook already setup",details:` This command simply runs Node. It also makes sure to call it in a way that's compatible with the current project (for example, on PnP projects the environment will be setup in such a way that PnP will be correctly injected into the environment). The Node process will use the exact same version of Node as the one used to run Yarn itself, which might be a good way to ensure that your commands always use a consistent Node version. `,examples:[["Run a Node script","$0 node ./my-script.js"]]})}async execute(){return this.cli.run(["exec","node",...this.args])}};Ve();Wt();var SC=class extends ut{constructor(){super(...arguments);this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["plugin","check"]]}static{this.usage=ot.Usage({category:"Plugin-related commands",description:"find all third-party plugins that differ from their own spec",details:` Check only the plugins from https. If this command detects any plugin differences in the CI environment, it will throw an error. `,examples:[["find all third-party plugins that differ from their own spec","$0 plugin check"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=await ze.findRcFiles(this.context.cwd);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async n=>{for(let c of s)if(c.data?.plugins)for(let f of c.data.plugins){if(!f.checksum||!f.spec.match(/^https?:/))continue;let p=await An.get(f.spec,{configuration:r}),h=Nn.makeHash(p);if(f.checksum===h)continue;let E=he.pretty(r,f.path,he.Type.PATH),C=he.pretty(r,f.spec,he.Type.URL),S=`${E} is different from the file provided by ${C}`;n.reportJson({...f,newChecksum:h}),n.reportError(0,S)}})).exitCode()}};Ve();Ve();bt();Wt();var rBe=Ie("os");Ve();bt();Wt();var X2e=Ie("os");Ve();Bc();Wt();var Fvt="https://raw.githubusercontent.com/yarnpkg/berry/master/plugins.yml";async function vm(t,e){let r=await An.get(Fvt,{configuration:t}),s=cs(r.toString());return Object.fromEntries(Object.entries(s).filter(([a,n])=>!e||Or.satisfiesWithPrereleases(e,n.range??"<4.0.0-rc.1")))}var DC=class extends ut{constructor(){super(...arguments);this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["plugin","list"]]}static{this.usage=ot.Usage({category:"Plugin-related commands",description:"list the available official plugins",details:"\n This command prints the plugins available directly from the Yarn repository. Only those plugins can be referenced by name in `yarn plugin import`.\n ",examples:[["List the official plugins","$0 plugin list"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async a=>{let n=await vm(r,un);for(let[c,{experimental:f,...p}]of Object.entries(n)){let h=c;f&&(h+=" [experimental]"),a.reportJson({name:c,experimental:f,...p}),a.reportInfo(null,h)}})).exitCode()}};var Nvt=/^[0-9]+$/,Ovt=process.platform==="win32";function $2e(t){return Nvt.test(t)?`pull/${t}/head`:t}var Lvt=({repository:t,branch:e},r)=>[["git","init",ue.fromPortablePath(r)],["git","remote","add","origin",t],["git","fetch","origin","--depth=1",$2e(e)],["git","reset","--hard","FETCH_HEAD"]],Mvt=({branch:t})=>[["git","fetch","origin","--depth=1",$2e(t),"--force"],["git","reset","--hard","FETCH_HEAD"],["git","clean","-dfx","-e","packages/yarnpkg-cli/bundles"]],_vt=({plugins:t,noMinify:e},r,s)=>[["yarn","build:cli",...new Array().concat(...t.map(a=>["--plugin",K.resolve(s,a)])),...e?["--no-minify"]:[],"|"],[Ovt?"move":"mv","packages/yarnpkg-cli/bundles/yarn.js",ue.fromPortablePath(r),"|"]],bC=class extends ut{constructor(){super(...arguments);this.installPath=ge.String("--path",{description:"The path where the repository should be cloned to"});this.repository=ge.String("--repository","https://github.com/yarnpkg/berry.git",{description:"The repository that should be cloned"});this.branch=ge.String("--branch","master",{description:"The branch of the repository that should be cloned"});this.plugins=ge.Array("--plugin",[],{description:"An array of additional plugins that should be included in the bundle"});this.dryRun=ge.Boolean("-n,--dry-run",!1,{description:"If set, the bundle will be built but not added to the project"});this.noMinify=ge.Boolean("--no-minify",!1,{description:"Build a bundle for development (debugging) - non-minified and non-mangled"});this.force=ge.Boolean("-f,--force",!1,{description:"Always clone the repository instead of trying to fetch the latest commits"});this.skipPlugins=ge.Boolean("--skip-plugins",!1,{description:"Skip updating the contrib plugins"})}static{this.paths=[["set","version","from","sources"]]}static{this.usage=ot.Usage({description:"build Yarn from master",details:` This command will clone the Yarn repository into a temporary folder, then build it. The resulting bundle will then be copied into the local project. By default, it also updates all contrib plugins to the same commit the bundle is built from. This behavior can be disabled by using the \`--skip-plugins\` flag. `,examples:[["Build Yarn from master","$0 set version from sources"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Tt.find(r,this.context.cwd),a=typeof this.installPath<"u"?K.resolve(this.context.cwd,ue.toPortablePath(this.installPath)):K.resolve(ue.toPortablePath((0,X2e.tmpdir)()),"yarnpkg-sources",Nn.makeHash(this.repository).slice(0,6));return(await Ot.start({configuration:r,stdout:this.context.stdout},async c=>{await Q5(this,{configuration:r,report:c,target:a}),c.reportSeparator(),c.reportInfo(0,"Building a fresh bundle"),c.reportSeparator();let f=await Gr.execvp("git",["rev-parse","--short","HEAD"],{cwd:a,strict:!0}),p=K.join(a,`packages/yarnpkg-cli/bundles/yarn-${f.stdout.trim()}.js`);le.existsSync(p)||(await uS(_vt(this,p,a),{configuration:r,context:this.context,target:a}),c.reportSeparator());let h=await le.readFilePromise(p);if(!this.dryRun){let{bundleVersion:E}=await x5(r,null,async()=>h,{report:c});this.skipPlugins||await Uvt(this,E,{project:s,report:c,target:a})}})).exitCode()}};async function uS(t,{configuration:e,context:r,target:s}){for(let[a,...n]of t){let c=n[n.length-1]==="|";if(c&&n.pop(),c)await Gr.pipevp(a,n,{cwd:s,stdin:r.stdin,stdout:r.stdout,stderr:r.stderr,strict:!0});else{r.stdout.write(`${he.pretty(e,` $ ${[a,...n].join(" ")}`,"grey")} `);try{await Gr.execvp(a,n,{cwd:s,strict:!0})}catch(f){throw r.stdout.write(f.stdout||f.stack),f}}}}async function Q5(t,{configuration:e,report:r,target:s}){let a=!1;if(!t.force&&le.existsSync(K.join(s,".git"))){r.reportInfo(0,"Fetching the latest commits"),r.reportSeparator();try{await uS(Mvt(t),{configuration:e,context:t.context,target:s}),a=!0}catch{r.reportSeparator(),r.reportWarning(0,"Repository update failed; we'll try to regenerate it")}}a||(r.reportInfo(0,"Cloning the remote repository"),r.reportSeparator(),await le.removePromise(s),await le.mkdirPromise(s,{recursive:!0}),await uS(Lvt(t,s),{configuration:e,context:t.context,target:s}))}async function Uvt(t,e,{project:r,report:s,target:a}){let n=await vm(r.configuration,e),c=new Set(Object.keys(n));for(let f of r.configuration.plugins.keys())c.has(f)&&await T5(f,t,{project:r,report:s,target:a})}Ve();Ve();bt();Wt();var eBe=et(Ai()),tBe=Ie("vm");var PC=class extends ut{constructor(){super(...arguments);this.name=ge.String();this.checksum=ge.Boolean("--checksum",!0,{description:"Whether to care if this plugin is modified"})}static{this.paths=[["plugin","import"]]}static{this.usage=ot.Usage({category:"Plugin-related commands",description:"download a plugin",details:` This command downloads the specified plugin from its remote location and updates the configuration to reference it in further CLI invocations. Three types of plugin references are accepted: - If the plugin is stored within the Yarn repository, it can be referenced by name. - Third-party plugins can be referenced directly through their public urls. - Local plugins can be referenced by their path on the disk. If the \`--no-checksum\` option is set, Yarn will no longer care if the plugin is modified. Plugins cannot be downloaded from the npm registry, and aren't allowed to have dependencies (they need to be bundled into a single file, possibly thanks to the \`@yarnpkg/builder\` package). `,examples:[['Download and activate the "@yarnpkg/plugin-exec" plugin',"$0 plugin import @yarnpkg/plugin-exec"],['Download and activate the "@yarnpkg/plugin-exec" plugin (shorthand)',"$0 plugin import exec"],["Download and activate a community plugin","$0 plugin import https://example.org/path/to/plugin.js"],["Activate a local plugin","$0 plugin import ./path/to/plugin.js"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);return(await Ot.start({configuration:r,stdout:this.context.stdout},async a=>{let{project:n}=await Tt.find(r,this.context.cwd),c,f;if(this.name.match(/^\.{0,2}[\\/]/)||ue.isAbsolute(this.name)){let p=K.resolve(this.context.cwd,ue.toPortablePath(this.name));a.reportInfo(0,`Reading ${he.pretty(r,p,he.Type.PATH)}`),c=K.relative(n.cwd,p),f=await le.readFilePromise(p)}else{let p;if(this.name.match(/^https?:/)){try{new URL(this.name)}catch{throw new Yt(52,`Plugin specifier "${this.name}" is neither a plugin name nor a valid url`)}c=this.name,p=this.name}else{let h=q.parseLocator(this.name.replace(/^((@yarnpkg\/)?plugin-)?/,"@yarnpkg/plugin-"));if(h.reference!=="unknown"&&!eBe.default.valid(h.reference))throw new Yt(0,"Official plugins only accept strict version references. Use an explicit URL if you wish to download them from another location.");let E=q.stringifyIdent(h),C=await vm(r,un);if(!Object.hasOwn(C,E)){let S=`Couldn't find a plugin named ${q.prettyIdent(r,h)} on the remote registry. `;throw r.plugins.has(E)?S+=`A plugin named ${q.prettyIdent(r,h)} is already installed; possibly attempting to import a built-in plugin.`:S+=`Note that only the plugins referenced on our website (${he.pretty(r,"https://github.com/yarnpkg/berry/blob/master/plugins.yml",he.Type.URL)}) can be referenced by their name; any other plugin will have to be referenced through its public url (for example ${he.pretty(r,"https://github.com/yarnpkg/berry/raw/master/packages/plugin-typescript/bin/%40yarnpkg/plugin-typescript.js",he.Type.URL)}).`,new Yt(51,S)}c=E,p=C[E].url,h.reference!=="unknown"?p=p.replace(/\/master\//,`/${E}/${h.reference}/`):un!==null&&(p=p.replace(/\/master\//,`/@yarnpkg/cli/${un}/`))}a.reportInfo(0,`Downloading ${he.pretty(r,p,"green")}`),f=await An.get(p,{configuration:r})}await R5(c,f,{checksum:this.checksum,project:n,report:a})})).exitCode()}};async function R5(t,e,{checksum:r=!0,project:s,report:a}){let{configuration:n}=s,c={},f={exports:c};(0,tBe.runInNewContext)(e.toString(),{module:f,exports:c});let h=`.yarn/plugins/${f.exports.name}.cjs`,E=K.resolve(s.cwd,h);a.reportInfo(0,`Saving the new plugin in ${he.pretty(n,h,"magenta")}`),await le.mkdirPromise(K.dirname(E),{recursive:!0}),await le.writeFilePromise(E,e);let C={path:h,spec:t};r&&(C.checksum=Nn.makeHash(e)),await ze.addPlugin(s.cwd,[C])}var Hvt=({pluginName:t,noMinify:e},r)=>[["yarn",`build:${t}`,...e?["--no-minify"]:[],"|"]],xC=class extends ut{constructor(){super(...arguments);this.installPath=ge.String("--path",{description:"The path where the repository should be cloned to"});this.repository=ge.String("--repository","https://github.com/yarnpkg/berry.git",{description:"The repository that should be cloned"});this.branch=ge.String("--branch","master",{description:"The branch of the repository that should be cloned"});this.noMinify=ge.Boolean("--no-minify",!1,{description:"Build a plugin for development (debugging) - non-minified and non-mangled"});this.force=ge.Boolean("-f,--force",!1,{description:"Always clone the repository instead of trying to fetch the latest commits"});this.name=ge.String()}static{this.paths=[["plugin","import","from","sources"]]}static{this.usage=ot.Usage({category:"Plugin-related commands",description:"build a plugin from sources",details:` This command clones the Yarn repository into a temporary folder, builds the specified contrib plugin and updates the configuration to reference it in further CLI invocations. The plugins can be referenced by their short name if sourced from the official Yarn repository. `,examples:[['Build and activate the "@yarnpkg/plugin-exec" plugin',"$0 plugin import from sources @yarnpkg/plugin-exec"],['Build and activate the "@yarnpkg/plugin-exec" plugin (shorthand)',"$0 plugin import from sources exec"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=typeof this.installPath<"u"?K.resolve(this.context.cwd,ue.toPortablePath(this.installPath)):K.resolve(ue.toPortablePath((0,rBe.tmpdir)()),"yarnpkg-sources",Nn.makeHash(this.repository).slice(0,6));return(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{let{project:c}=await Tt.find(r,this.context.cwd),f=q.parseIdent(this.name.replace(/^((@yarnpkg\/)?plugin-)?/,"@yarnpkg/plugin-")),p=q.stringifyIdent(f),h=await vm(r,un);if(!Object.hasOwn(h,p))throw new Yt(51,`Couldn't find a plugin named "${p}" on the remote registry. Note that only the plugins referenced on our website (https://github.com/yarnpkg/berry/blob/master/plugins.yml) can be built and imported from sources.`);let E=p;await Q5(this,{configuration:r,report:n,target:s}),await T5(E,this,{project:c,report:n,target:s})})).exitCode()}};async function T5(t,{context:e,noMinify:r},{project:s,report:a,target:n}){let c=t.replace(/@yarnpkg\//,""),{configuration:f}=s;a.reportSeparator(),a.reportInfo(0,`Building a fresh ${c}`),a.reportSeparator(),await uS(Hvt({pluginName:c,noMinify:r},n),{configuration:f,context:e,target:n}),a.reportSeparator();let p=K.resolve(n,`packages/${c}/bundles/${t}.js`),h=await le.readFilePromise(p);await R5(t,h,{project:s,report:a})}Ve();bt();Wt();var kC=class extends ut{constructor(){super(...arguments);this.name=ge.String()}static{this.paths=[["plugin","remove"]]}static{this.usage=ot.Usage({category:"Plugin-related commands",description:"remove a plugin",details:` This command deletes the specified plugin from the .yarn/plugins folder and removes it from the configuration. **Note:** The plugins have to be referenced by their name property, which can be obtained using the \`yarn plugin runtime\` command. Shorthands are not allowed. `,examples:[["Remove a plugin imported from the Yarn repository","$0 plugin remove @yarnpkg/plugin-typescript"],["Remove a plugin imported from a local file","$0 plugin remove my-local-plugin"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Tt.find(r,this.context.cwd);return(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{let c=this.name,f=q.parseIdent(c);if(!r.plugins.has(c))throw new nt(`${q.prettyIdent(r,f)} isn't referenced by the current configuration`);let p=`.yarn/plugins/${c}.cjs`,h=K.resolve(s.cwd,p);le.existsSync(h)&&(n.reportInfo(0,`Removing ${he.pretty(r,p,he.Type.PATH)}...`),await le.removePromise(h)),n.reportInfo(0,"Updating the configuration..."),await ze.updateConfiguration(s.cwd,{plugins:E=>{if(!Array.isArray(E))return E;let C=E.filter(S=>S.path!==p);return C.length===0?ze.deleteProperty:C.length===E.length?E:C}})})).exitCode()}};Ve();Wt();var QC=class extends ut{constructor(){super(...arguments);this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["plugin","runtime"]]}static{this.usage=ot.Usage({category:"Plugin-related commands",description:"list the active plugins",details:` This command prints the currently active plugins. Will be displayed both builtin plugins and external plugins. `,examples:[["List the currently active plugins","$0 plugin runtime"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async a=>{for(let n of r.plugins.keys()){let c=this.context.plugins.plugins.has(n),f=n;c&&(f+=" [builtin]"),a.reportJson({name:n,builtin:c}),a.reportInfo(null,`${f}`)}})).exitCode()}};Ve();Ve();Wt();var TC=class extends ut{constructor(){super(...arguments);this.idents=ge.Rest()}static{this.paths=[["rebuild"]]}static{this.usage=ot.Usage({description:"rebuild the project's native packages",details:` This command will automatically cause Yarn to forget about previous compilations of the given packages and to run them again. Note that while Yarn forgets the compilation, the previous artifacts aren't erased from the filesystem and may affect the next builds (in good or bad). To avoid this, you may remove the .yarn/unplugged folder, or any other relevant location where packages might have been stored (Yarn may offer a way to do that automatically in the future). By default all packages will be rebuilt, but you can filter the list by specifying the names of the packages you want to clear from memory. `,examples:[["Rebuild all packages","$0 rebuild"],["Rebuild fsevents only","$0 rebuild fsevents"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);let c=new Set;for(let f of this.idents)c.add(q.parseIdent(f).identHash);if(await s.restoreInstallState({restoreResolutions:!1}),await s.resolveEverything({cache:n,report:new Yi}),c.size>0)for(let f of s.storedPackages.values())c.has(f.identHash)&&(s.storedBuildState.delete(f.locatorHash),s.skippedBuilds.delete(f.locatorHash));else s.storedBuildState.clear(),s.skippedBuilds.clear();return await s.installWithNewReport({stdout:this.context.stdout,quiet:this.context.quiet},{cache:n})}};Ve();Ve();Ve();Wt();var F5=et(Sa());Ul();var RC=class extends ut{constructor(){super(...arguments);this.all=ge.Boolean("-A,--all",!1,{description:"Apply the operation to all workspaces from the current project"});this.mode=ge.String("--mode",{description:"Change what artifacts installs generate",validator:po(ec)});this.patterns=ge.Rest()}static{this.paths=[["remove"]]}static{this.usage=ot.Usage({description:"remove dependencies from the project",details:` This command will remove the packages matching the specified patterns from the current workspace. If the \`--mode=\` option is set, Yarn will change which artifacts are generated. The modes currently supported are: - \`skip-build\` will not run the build scripts at all. Note that this is different from setting \`enableScripts\` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run. - \`update-lockfile\` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost. This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them. `,examples:[["Remove a dependency from the current project","$0 remove lodash"],["Remove a dependency from all workspaces at once","$0 remove lodash --all"],["Remove all dependencies starting with `eslint-`","$0 remove 'eslint-*'"],["Remove all dependencies with the `@babel` scope","$0 remove '@babel/*'"],["Remove all dependencies matching `react-dom` or `react-helmet`","$0 remove 'react-{dom,helmet}'"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=this.all?s.workspaces:[a],f=["dependencies","devDependencies","peerDependencies"],p=[],h=!1,E=[];for(let I of this.patterns){let R=!1,N=q.parseIdent(I);for(let U of c){let W=[...U.manifest.peerDependenciesMeta.keys()];for(let te of(0,F5.default)(W,I))U.manifest.peerDependenciesMeta.delete(te),h=!0,R=!0;for(let te of f){let ie=U.manifest.getForScope(te),Ae=[...ie.values()].map(ce=>q.stringifyIdent(ce));for(let ce of(0,F5.default)(Ae,q.stringifyIdent(N))){let{identHash:me}=q.parseIdent(ce),pe=ie.get(me);if(typeof pe>"u")throw new Error("Assertion failed: Expected the descriptor to be registered");U.manifest[te].delete(me),E.push([U,te,pe]),h=!0,R=!0}}}R||p.push(I)}let C=p.length>1?"Patterns":"Pattern",S=p.length>1?"don't":"doesn't",P=this.all?"any":"this";if(p.length>0)throw new nt(`${C} ${he.prettyList(r,p,he.Type.CODE)} ${S} match any packages referenced by ${P} workspace`);return h?(await r.triggerMultipleHooks(I=>I.afterWorkspaceDependencyRemoval,E),await s.installWithNewReport({stdout:this.context.stdout},{cache:n,mode:this.mode})):0}};Ve();Ve();Wt();var nBe=Ie("util"),FC=class extends ut{constructor(){super(...arguments);this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["run"]]}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);return(await Ot.start({configuration:r,stdout:this.context.stdout,json:this.json},async c=>{let f=a.manifest.scripts,p=je.sortMap(f.keys(),C=>C),h={breakLength:1/0,colors:r.get("enableColors"),maxArrayLength:2},E=p.reduce((C,S)=>Math.max(C,S.length),0);for(let[C,S]of f.entries())c.reportInfo(null,`${C.padEnd(E," ")} ${(0,nBe.inspect)(S,h)}`),c.reportJson({name:C,script:S})})).exitCode()}};Ve();Ve();Wt();var NC=class extends ut{constructor(){super(...arguments);this.inspect=ge.String("--inspect",!1,{tolerateBoolean:!0,description:"Forwarded to the underlying Node process when executing a binary"});this.inspectBrk=ge.String("--inspect-brk",!1,{tolerateBoolean:!0,description:"Forwarded to the underlying Node process when executing a binary"});this.topLevel=ge.Boolean("-T,--top-level",!1,{description:"Check the root workspace for scripts and/or binaries instead of the current one"});this.binariesOnly=ge.Boolean("-B,--binaries-only",!1,{description:"Ignore any user defined scripts and only check for binaries"});this.require=ge.String("--require",{description:"Forwarded to the underlying Node process when executing a binary"});this.silent=ge.Boolean("--silent",{hidden:!0});this.scriptName=ge.String();this.args=ge.Proxy()}static{this.paths=[["run"]]}static{this.usage=ot.Usage({description:"run a script defined in the package.json",details:` This command will run a tool. The exact tool that will be executed will depend on the current state of your workspace: - If the \`scripts\` field from your local package.json contains a matching script name, its definition will get executed. - Otherwise, if one of the local workspace's dependencies exposes a binary with a matching name, this binary will get executed. - Otherwise, if the specified name contains a colon character and if one of the workspaces in the project contains exactly one script with a matching name, then this script will get executed. Whatever happens, the cwd of the spawned process will be the workspace that declares the script (which makes it possible to call commands cross-workspaces using the third syntax). `,examples:[["Run the tests from the local workspace","$0 run test"],['Same thing, but without the "run" keyword',"$0 test"],["Inspect Webpack while running","$0 run --inspect-brk webpack"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a,locator:n}=await Tt.find(r,this.context.cwd);await s.restoreInstallState();let c=this.topLevel?s.topLevelWorkspace.anchoredLocator:n;if(!this.binariesOnly&&await In.hasPackageScript(c,this.scriptName,{project:s}))return await In.executePackageScript(c,this.scriptName,this.args,{project:s,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});let f=await In.getPackageAccessibleBinaries(c,{project:s});if(f.get(this.scriptName)){let h=[];return this.inspect&&(typeof this.inspect=="string"?h.push(`--inspect=${this.inspect}`):h.push("--inspect")),this.inspectBrk&&(typeof this.inspectBrk=="string"?h.push(`--inspect-brk=${this.inspectBrk}`):h.push("--inspect-brk")),this.require&&h.push(`--require=${this.require}`),await In.executePackageAccessibleBinary(c,this.scriptName,this.args,{cwd:this.context.cwd,project:s,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,nodeArgs:h,packageAccessibleBinaries:f})}if(!this.topLevel&&!this.binariesOnly&&a&&this.scriptName.includes(":")){let E=(await Promise.all(s.workspaces.map(async C=>C.manifest.scripts.has(this.scriptName)?C:null))).filter(C=>C!==null);if(E.length===1)return await In.executeWorkspaceScript(E[0],this.scriptName,this.args,{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr})}if(this.topLevel)throw this.scriptName==="node-gyp"?new nt(`Couldn't find a script name "${this.scriptName}" in the top-level (used by ${q.prettyLocator(r,n)}). This typically happens because some package depends on "node-gyp" to build itself, but didn't list it in their dependencies. To fix that, please run "yarn add node-gyp" into your top-level workspace. You also can open an issue on the repository of the specified package to suggest them to use an optional peer dependency.`):new nt(`Couldn't find a script name "${this.scriptName}" in the top-level (used by ${q.prettyLocator(r,n)}).`);{if(this.scriptName==="global")throw new nt("The 'yarn global' commands have been removed in 2.x - consider using 'yarn dlx' or a third-party plugin instead");let h=[this.scriptName].concat(this.args);for(let[E,C]of $I)for(let S of C)if(h.length>=S.length&&JSON.stringify(h.slice(0,S.length))===JSON.stringify(S))throw new nt(`Couldn't find a script named "${this.scriptName}", but a matching command can be found in the ${E} plugin. You can install it with "yarn plugin import ${E}".`);throw new nt(`Couldn't find a script named "${this.scriptName}".`)}}};Ve();Ve();Wt();var OC=class extends ut{constructor(){super(...arguments);this.descriptor=ge.String();this.resolution=ge.String()}static{this.paths=[["set","resolution"]]}static{this.usage=ot.Usage({description:"enforce a package resolution",details:'\n This command updates the resolution table so that `descriptor` is resolved by `resolution`.\n\n Note that by default this command only affect the current resolution table - meaning that this "manual override" will disappear if you remove the lockfile, or if the package disappear from the table. If you wish to make the enforced resolution persist whatever happens, edit the `resolutions` field in your top-level manifest.\n\n Note that no attempt is made at validating that `resolution` is a valid resolution entry for `descriptor`.\n ',examples:[["Force all instances of lodash@npm:^1.2.3 to resolve to 1.5.0","$0 set resolution lodash@npm:^1.2.3 npm:1.5.0"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(await s.restoreInstallState({restoreResolutions:!1}),!a)throw new ar(s.cwd,this.context.cwd);let c=q.parseDescriptor(this.descriptor,!0),f=q.makeDescriptor(c,this.resolution);return s.storedDescriptors.set(c.descriptorHash,c),s.storedDescriptors.set(f.descriptorHash,f),s.resolutionAliases.set(c.descriptorHash,f.descriptorHash),await s.installWithNewReport({stdout:this.context.stdout},{cache:n})}};Ve();bt();Wt();var iBe=et(Sa()),LC=class extends ut{constructor(){super(...arguments);this.all=ge.Boolean("-A,--all",!1,{description:"Unlink all workspaces belonging to the target project from the current one"});this.leadingArguments=ge.Rest()}static{this.paths=[["unlink"]]}static{this.usage=ot.Usage({description:"disconnect the local project from another one",details:` This command will remove any resolutions in the project-level manifest that would have been added via a yarn link with similar arguments. `,examples:[["Unregister a remote workspace in the current project","$0 unlink ~/ts-loader"],["Unregister all workspaces from a remote project in the current project","$0 unlink ~/jest --all"],["Unregister all previously linked workspaces","$0 unlink --all"],["Unregister all workspaces matching a glob","$0 unlink '@babel/*' 'pkg-{a,b}'"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);let c=s.topLevelWorkspace,f=new Set;if(this.leadingArguments.length===0&&this.all)for(let{pattern:p,reference:h}of c.manifest.resolutions)h.startsWith("portal:")&&f.add(p.descriptor.fullName);if(this.leadingArguments.length>0)for(let p of this.leadingArguments){let h=K.resolve(this.context.cwd,ue.toPortablePath(p));if(je.isPathLike(p)){let E=await ze.find(h,this.context.plugins,{useRc:!1,strict:!1}),{project:C,workspace:S}=await Tt.find(E,h);if(!S)throw new ar(C.cwd,h);if(this.all){for(let P of C.workspaces)P.manifest.name&&f.add(q.stringifyIdent(P.anchoredLocator));if(f.size===0)throw new nt("No workspace found to be unlinked in the target project")}else{if(!S.manifest.name)throw new nt("The target workspace doesn't have a name and thus cannot be unlinked");f.add(q.stringifyIdent(S.anchoredLocator))}}else{let E=[...c.manifest.resolutions.map(({pattern:C})=>C.descriptor.fullName)];for(let C of(0,iBe.default)(E,p))f.add(C)}}return c.manifest.resolutions=c.manifest.resolutions.filter(({pattern:p})=>!f.has(p.descriptor.fullName)),await s.installWithNewReport({stdout:this.context.stdout,quiet:this.context.quiet},{cache:n})}};Ve();Ve();Ve();Wt();var sBe=et(nS()),N5=et(Sa());Ul();var MC=class extends ut{constructor(){super(...arguments);this.interactive=ge.Boolean("-i,--interactive",{description:"Offer various choices, depending on the detected upgrade paths"});this.fixed=ge.Boolean("-F,--fixed",!1,{description:"Store dependency tags as-is instead of resolving them"});this.exact=ge.Boolean("-E,--exact",!1,{description:"Don't use any semver modifier on the resolved range"});this.tilde=ge.Boolean("-T,--tilde",!1,{description:"Use the `~` semver modifier on the resolved range"});this.caret=ge.Boolean("-C,--caret",!1,{description:"Use the `^` semver modifier on the resolved range"});this.recursive=ge.Boolean("-R,--recursive",!1,{description:"Resolve again ALL resolutions for those packages"});this.mode=ge.String("--mode",{description:"Change what artifacts installs generate",validator:po(ec)});this.patterns=ge.Rest()}static{this.paths=[["up"]]}static{this.usage=ot.Usage({description:"upgrade dependencies across the project",details:"\n This command upgrades the packages matching the list of specified patterns to their latest available version across the whole project (regardless of whether they're part of `dependencies` or `devDependencies` - `peerDependencies` won't be affected). This is a project-wide command: all workspaces will be upgraded in the process.\n\n If `-R,--recursive` is set the command will change behavior and no other switch will be allowed. When operating under this mode `yarn up` will force all ranges matching the selected packages to be resolved again (often to the highest available versions) before being stored in the lockfile. It however won't touch your manifests anymore, so depending on your needs you might want to run both `yarn up` and `yarn up -R` to cover all bases.\n\n If `-i,--interactive` is set (or if the `preferInteractive` settings is toggled on) the command will offer various choices, depending on the detected upgrade paths. Some upgrades require this flag in order to resolve ambiguities.\n\n The, `-C,--caret`, `-E,--exact` and `-T,--tilde` options have the same meaning as in the `add` command (they change the modifier used when the range is missing or a tag, and are ignored when the range is explicitly set).\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the latter will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n Generally you can see `yarn up` as a counterpart to what was `yarn upgrade --latest` in Yarn 1 (ie it ignores the ranges previously listed in your manifests), but unlike `yarn upgrade` which only upgraded dependencies in the current workspace, `yarn up` will upgrade all workspaces at the same time.\n\n This command accepts glob patterns as arguments (if valid Descriptors and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n **Note:** The ranges have to be static, only the package scopes and names can contain glob patterns.\n ",examples:[["Upgrade all instances of lodash to the latest release","$0 up lodash"],["Upgrade all instances of lodash to the latest release, but ask confirmation for each","$0 up lodash -i"],["Upgrade all instances of lodash to 1.2.3","$0 up lodash@1.2.3"],["Upgrade all instances of packages with the `@babel` scope to the latest release","$0 up '@babel/*'"],["Upgrade all instances of packages containing the word `jest` to the latest release","$0 up '*jest*'"],["Upgrade all instances of packages with the `@babel` scope to 7.0.0","$0 up '@babel/*@7.0.0'"]]})}static{this.schema=[tB("recursive",Wf.Forbids,["interactive","exact","tilde","caret"],{ignore:[void 0,!1]})]}async execute(){return this.recursive?await this.executeUpRecursive():await this.executeUpClassic()}async executeUpRecursive(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=[...s.storedDescriptors.values()],f=c.map(E=>q.stringifyIdent(E)),p=new Set;for(let E of this.patterns){if(q.parseDescriptor(E).range!=="unknown")throw new nt("Ranges aren't allowed when using --recursive");for(let C of(0,N5.default)(f,E)){let S=q.parseIdent(C);p.add(S.identHash)}}let h=c.filter(E=>p.has(E.identHash));for(let E of h)s.storedDescriptors.delete(E.descriptorHash),s.storedResolutions.delete(E.descriptorHash);return await s.installWithNewReport({stdout:this.context.stdout},{cache:n,mode:this.mode})}async executeUpClassic(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=this.fixed,f=r.isInteractive({interactive:this.interactive,stdout:this.context.stdout}),p=sS(this,s),h=f?["keep","reuse","project","latest"]:["project","latest"],E=[],C=[];for(let N of this.patterns){let U=!1,W=q.parseDescriptor(N),te=q.stringifyIdent(W);for(let ie of s.workspaces)for(let Ae of["dependencies","devDependencies"]){let me=[...ie.manifest.getForScope(Ae).values()].map(Be=>q.stringifyIdent(Be)),pe=te==="*"?me:(0,N5.default)(me,te);for(let Be of pe){let Ce=q.parseIdent(Be),g=ie.manifest[Ae].get(Ce.identHash);if(typeof g>"u")throw new Error("Assertion failed: Expected the descriptor to be registered");let we=q.makeDescriptor(Ce,W.range);E.push(Promise.resolve().then(async()=>[ie,Ae,g,await oS(we,{project:s,workspace:ie,cache:n,target:Ae,fixed:c,modifier:p,strategies:h})])),U=!0}}U||C.push(N)}if(C.length>1)throw new nt(`Patterns ${he.prettyList(r,C,he.Type.CODE)} don't match any packages referenced by any workspace`);if(C.length>0)throw new nt(`Pattern ${he.prettyList(r,C,he.Type.CODE)} doesn't match any packages referenced by any workspace`);let S=await Promise.all(E),P=await uA.start({configuration:r,stdout:this.context.stdout,suggestInstall:!1},async N=>{for(let[,,U,{suggestions:W,rejections:te}]of S){let ie=W.filter(Ae=>Ae.descriptor!==null);if(ie.length===0){let[Ae]=te;if(typeof Ae>"u")throw new Error("Assertion failed: Expected an error to have been set");let ce=this.cli.error(Ae);s.configuration.get("enableNetwork")?N.reportError(27,`${q.prettyDescriptor(r,U)} can't be resolved to a satisfying range ${ce}`):N.reportError(27,`${q.prettyDescriptor(r,U)} can't be resolved to a satisfying range (note: network resolution has been disabled) ${ce}`)}else ie.length>1&&!f&&N.reportError(27,`${q.prettyDescriptor(r,U)} has multiple possible upgrade strategies; use -i to disambiguate manually`)}});if(P.hasErrors())return P.exitCode();let I=!1,R=[];for(let[N,U,,{suggestions:W}]of S){let te,ie=W.filter(pe=>pe.descriptor!==null),Ae=ie[0].descriptor,ce=ie.every(pe=>q.areDescriptorsEqual(pe.descriptor,Ae));ie.length===1||ce?te=Ae:(I=!0,{answer:te}=await(0,sBe.prompt)({type:"select",name:"answer",message:`Which range do you want to use in ${q.prettyWorkspace(r,N)} \u276F ${U}?`,choices:W.map(({descriptor:pe,name:Be,reason:Ce})=>pe?{name:Be,hint:Ce,descriptor:pe}:{name:Be,hint:Ce,disabled:!0}),onCancel:()=>process.exit(130),result(pe){return this.find(pe,"descriptor")},stdin:this.context.stdin,stdout:this.context.stdout}));let me=N.manifest[U].get(te.identHash);if(typeof me>"u")throw new Error("Assertion failed: This descriptor should have a matching entry");if(me.descriptorHash!==te.descriptorHash)N.manifest[U].set(te.identHash,te),R.push([N,U,me,te]);else{let pe=r.makeResolver(),Be={project:s,resolver:pe},Ce=r.normalizeDependency(me),g=pe.bindDescriptor(Ce,N.anchoredLocator,Be);s.forgetResolution(g)}}return await r.triggerMultipleHooks(N=>N.afterWorkspaceDependencyReplacement,R),I&&this.context.stdout.write(` `),await s.installWithNewReport({stdout:this.context.stdout},{cache:n,mode:this.mode})}};Ve();Ve();Ve();Wt();var _C=class extends ut{constructor(){super(...arguments);this.recursive=ge.Boolean("-R,--recursive",!1,{description:"List, for each workspace, what are all the paths that lead to the dependency"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.peers=ge.Boolean("--peers",!1,{description:"Also print the peer dependencies that match the specified name"});this.package=ge.String()}static{this.paths=[["why"]]}static{this.usage=ot.Usage({description:"display the reason why a package is needed",details:` This command prints the exact reasons why a package appears in the dependency tree. If \`-R,--recursive\` is set, the listing will go in depth and will list, for each workspaces, what are all the paths that lead to the dependency. Note that the display is somewhat optimized in that it will not print the package listing twice for a single package, so if you see a leaf named "Foo" when looking for "Bar", it means that "Foo" already got printed higher in the tree. `,examples:[["Explain why lodash is used in your project","$0 why lodash"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=q.parseIdent(this.package).identHash,c=this.recursive?qvt(s,n,{configuration:r,peers:this.peers}):jvt(s,n,{configuration:r,peers:this.peers});Qs.emitTree(c,{configuration:r,stdout:this.context.stdout,json:this.json,separators:1})}};function jvt(t,e,{configuration:r,peers:s}){let a=je.sortMap(t.storedPackages.values(),f=>q.stringifyLocator(f)),n={},c={children:n};for(let f of a){let p={};for(let E of f.dependencies.values()){if(!s&&f.peerDependencies.has(E.identHash))continue;let C=t.storedResolutions.get(E.descriptorHash);if(!C)throw new Error("Assertion failed: The resolution should have been registered");let S=t.storedPackages.get(C);if(!S)throw new Error("Assertion failed: The package should have been registered");if(S.identHash!==e)continue;{let I=q.stringifyLocator(f);n[I]={value:[f,he.Type.LOCATOR],children:p}}let P=q.stringifyLocator(S);p[P]={value:[{descriptor:E,locator:S},he.Type.DEPENDENT]}}}return c}function qvt(t,e,{configuration:r,peers:s}){let a=je.sortMap(t.workspaces,S=>q.stringifyLocator(S.anchoredLocator)),n=new Set,c=new Set,f=S=>{if(n.has(S.locatorHash))return c.has(S.locatorHash);if(n.add(S.locatorHash),S.identHash===e)return c.add(S.locatorHash),!0;let P=!1;S.identHash===e&&(P=!0);for(let I of S.dependencies.values()){if(!s&&S.peerDependencies.has(I.identHash))continue;let R=t.storedResolutions.get(I.descriptorHash);if(!R)throw new Error("Assertion failed: The resolution should have been registered");let N=t.storedPackages.get(R);if(!N)throw new Error("Assertion failed: The package should have been registered");f(N)&&(P=!0)}return P&&c.add(S.locatorHash),P};for(let S of a)f(S.anchoredPackage);let p=new Set,h={},E={children:h},C=(S,P,I)=>{if(!c.has(S.locatorHash))return;let R=I!==null?he.tuple(he.Type.DEPENDENT,{locator:S,descriptor:I}):he.tuple(he.Type.LOCATOR,S),N={},U={value:R,children:N},W=q.stringifyLocator(S);if(P[W]=U,!(I!==null&&t.tryWorkspaceByLocator(S))&&!p.has(S.locatorHash)){p.add(S.locatorHash);for(let te of S.dependencies.values()){if(!s&&S.peerDependencies.has(te.identHash))continue;let ie=t.storedResolutions.get(te.descriptorHash);if(!ie)throw new Error("Assertion failed: The resolution should have been registered");let Ae=t.storedPackages.get(ie);if(!Ae)throw new Error("Assertion failed: The package should have been registered");C(Ae,N,te)}}};for(let S of a)C(S.anchoredPackage,h,null);return E}Ve();var W5={};Vt(W5,{GitFetcher:()=>AS,GitResolver:()=>pS,default:()=>uSt,gitUtils:()=>Qa});Ve();bt();var Qa={};Vt(Qa,{TreeishProtocols:()=>fS,clone:()=>G5,fetchBase:()=>bBe,fetchChangedFiles:()=>PBe,fetchChangedWorkspaces:()=>lSt,fetchRoot:()=>DBe,isGitUrl:()=>jC,lsRemote:()=>SBe,normalizeLocator:()=>aSt,normalizeRepoUrl:()=>UC,resolveUrl:()=>q5,splitRepoUrl:()=>G0,validateRepoUrl:()=>j5});Ve();bt();Wt();var wBe=et(EBe()),BBe=et(c6()),HC=et(Ie("querystring")),U5=et(Ai());function _5(t,e,r){let s=t.indexOf(r);return t.lastIndexOf(e,s>-1?s:1/0)}function IBe(t){try{return new URL(t)}catch{return}}function sSt(t){let e=_5(t,"@","#"),r=_5(t,":","#");return r>e&&(t=`${t.slice(0,r)}/${t.slice(r+1)}`),_5(t,":","#")===-1&&t.indexOf("//")===-1&&(t=`ssh://${t}`),t}function CBe(t){return IBe(t)||IBe(sSt(t))}function UC(t,{git:e=!1}={}){if(t=t.replace(/^git\+https:/,"https:"),t=t.replace(/^(?:github:|https:\/\/github\.com\/|git:\/\/github\.com\/)?(?!\.{1,2}\/)([a-zA-Z0-9._-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)(?:\.git)?(#.*)?$/,"https://github.com/$1/$2.git$3"),t=t.replace(/^https:\/\/github\.com\/(?!\.{1,2}\/)([a-zA-Z0-9._-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)\/tarball\/(.+)?$/,"https://github.com/$1/$2.git#$3"),e){let r=CBe(t);r&&(t=r.href),t=t.replace(/^git\+([^:]+):/,"$1:")}return t}function vBe(){return{...process.env,GIT_SSH_COMMAND:process.env.GIT_SSH_COMMAND||`${process.env.GIT_SSH||"ssh"} -o BatchMode=yes`}}var oSt=[/^ssh:/,/^git(?:\+[^:]+)?:/,/^(?:git\+)?https?:[^#]+\/[^#]+(?:\.git)(?:#.*)?$/,/^git@[^#]+\/[^#]+\.git(?:#.*)?$/,/^(?:github:|https:\/\/github\.com\/)?(?!\.{1,2}\/)([a-zA-Z._0-9-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z._0-9-]+?)(?:\.git)?(?:#.*)?$/,/^https:\/\/github\.com\/(?!\.{1,2}\/)([a-zA-Z0-9._-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)\/tarball\/(.+)?$/],fS=(a=>(a.Commit="commit",a.Head="head",a.Tag="tag",a.Semver="semver",a))(fS||{});function jC(t){return t?oSt.some(e=>!!t.match(e)):!1}function G0(t){t=UC(t);let e=t.indexOf("#");if(e===-1)return{repo:t,treeish:{protocol:"head",request:"HEAD"},extra:{}};let r=t.slice(0,e),s=t.slice(e+1);if(s.match(/^[a-z]+=/)){let a=HC.default.parse(s);for(let[p,h]of Object.entries(a))if(typeof h!="string")throw new Error(`Assertion failed: The ${p} parameter must be a literal string`);let n=Object.values(fS).find(p=>Object.hasOwn(a,p)),[c,f]=typeof n<"u"?[n,a[n]]:["head","HEAD"];for(let p of Object.values(fS))delete a[p];return{repo:r,treeish:{protocol:c,request:f},extra:a}}else{let a=s.indexOf(":"),[n,c]=a===-1?[null,s]:[s.slice(0,a),s.slice(a+1)];return{repo:r,treeish:{protocol:n,request:c},extra:{}}}}function aSt(t){return q.makeLocator(t,UC(t.reference))}function j5(t,{configuration:e}){let r=UC(t,{git:!0});if(!An.getNetworkSettings(`https://${(0,wBe.default)(r).resource}`,{configuration:e}).enableNetwork)throw new Yt(80,`Request to '${r}' has been blocked because of your configuration settings`);return r}async function SBe(t,e){let r=j5(t,{configuration:e}),s=await H5("listing refs",["ls-remote",r],{cwd:e.startingCwd,env:vBe()},{configuration:e,normalizedRepoUrl:r}),a=new Map,n=/^([a-f0-9]{40})\t([^\n]+)/gm,c;for(;(c=n.exec(s.stdout))!==null;)a.set(c[2],c[1]);return a}async function q5(t,e){let{repo:r,treeish:{protocol:s,request:a},extra:n}=G0(t),c=await SBe(r,e),f=(h,E)=>{switch(h){case"commit":{if(!E.match(/^[a-f0-9]{40}$/))throw new Error("Invalid commit hash");return HC.default.stringify({...n,commit:E})}case"head":{let C=c.get(E==="HEAD"?E:`refs/heads/${E}`);if(typeof C>"u")throw new Error(`Unknown head ("${E}")`);return HC.default.stringify({...n,commit:C})}case"tag":{let C=c.get(`refs/tags/${E}`);if(typeof C>"u")throw new Error(`Unknown tag ("${E}")`);return HC.default.stringify({...n,commit:C})}case"semver":{let C=Or.validRange(E);if(!C)throw new Error(`Invalid range ("${E}")`);let S=new Map([...c.entries()].filter(([I])=>I.startsWith("refs/tags/")).map(([I,R])=>[U5.default.parse(I.slice(10)),R]).filter(I=>I[0]!==null)),P=U5.default.maxSatisfying([...S.keys()],C);if(P===null)throw new Error(`No matching range ("${E}")`);return HC.default.stringify({...n,commit:S.get(P)})}case null:{let C;if((C=p("commit",E))!==null||(C=p("tag",E))!==null||(C=p("head",E))!==null)return C;throw E.match(/^[a-f0-9]+$/)?new Error(`Couldn't resolve "${E}" as either a commit, a tag, or a head - if a commit, use the 40-characters commit hash`):new Error(`Couldn't resolve "${E}" as either a commit, a tag, or a head`)}default:throw new Error(`Invalid Git resolution protocol ("${h}")`)}},p=(h,E)=>{try{return f(h,E)}catch{return null}};return UC(`${r}#${f(s,a)}`)}async function G5(t,e){return await e.getLimit("cloneConcurrency")(async()=>{let{repo:r,treeish:{protocol:s,request:a}}=G0(t);if(s!=="commit")throw new Error("Invalid treeish protocol when cloning");let n=j5(r,{configuration:e}),c=await le.mktempPromise(),f={cwd:c,env:vBe()};return await H5("cloning the repository",["clone","-c core.autocrlf=false",n,ue.fromPortablePath(c)],f,{configuration:e,normalizedRepoUrl:n}),await H5("switching branch",["checkout",`${a}`],f,{configuration:e,normalizedRepoUrl:n}),c})}async function DBe(t){let e,r=t;do{if(e=r,await le.existsPromise(K.join(e,".git")))return e;r=K.dirname(e)}while(r!==e);return null}async function bBe(t,{baseRefs:e}){if(e.length===0)throw new nt("Can't run this command with zero base refs specified.");let r=[];for(let f of e){let{code:p}=await Gr.execvp("git",["merge-base",f,"HEAD"],{cwd:t});p===0&&r.push(f)}if(r.length===0)throw new nt(`No ancestor could be found between any of HEAD and ${e.join(", ")}`);let{stdout:s}=await Gr.execvp("git",["merge-base","HEAD",...r],{cwd:t,strict:!0}),a=s.trim(),{stdout:n}=await Gr.execvp("git",["show","--quiet","--pretty=format:%s",a],{cwd:t,strict:!0}),c=n.trim();return{hash:a,title:c}}async function PBe(t,{base:e,project:r}){let s=je.buildIgnorePattern(r.configuration.get("changesetIgnorePatterns")),{stdout:a}=await Gr.execvp("git",["diff","--name-only",`${e}`],{cwd:t,strict:!0}),n=a.split(/\r\n|\r|\n/).filter(h=>h.length>0).map(h=>K.resolve(t,ue.toPortablePath(h))),{stdout:c}=await Gr.execvp("git",["ls-files","--others","--exclude-standard"],{cwd:t,strict:!0}),f=c.split(/\r\n|\r|\n/).filter(h=>h.length>0).map(h=>K.resolve(t,ue.toPortablePath(h))),p=[...new Set([...n,...f].sort())];return s?p.filter(h=>!K.relative(r.cwd,h).match(s)):p}async function lSt({ref:t,project:e}){if(e.configuration.projectCwd===null)throw new nt("This command can only be run from within a Yarn project");let r=[K.resolve(e.cwd,Er.lockfile),K.resolve(e.cwd,e.configuration.get("cacheFolder")),K.resolve(e.cwd,e.configuration.get("installStatePath")),K.resolve(e.cwd,e.configuration.get("virtualFolder"))];await e.configuration.triggerHook(c=>c.populateYarnPaths,e,c=>{c!=null&&r.push(c)});let s=await DBe(e.configuration.projectCwd);if(s==null)throw new nt("This command can only be run on Git repositories");let a=await bBe(s,{baseRefs:typeof t=="string"?[t]:e.configuration.get("changesetBaseRefs")}),n=await PBe(s,{base:a.hash,project:e});return new Set(je.mapAndFilter(n,c=>{let f=e.tryWorkspaceByFilePath(c);return f===null?je.mapAndFilter.skip:r.some(p=>c.startsWith(p))?je.mapAndFilter.skip:f}))}async function H5(t,e,r,{configuration:s,normalizedRepoUrl:a}){try{return await Gr.execvp("git",e,{...r,strict:!0})}catch(n){if(!(n instanceof Gr.ExecError))throw n;let c=n.reportExtra,f=n.stderr.toString();throw new Yt(1,`Failed ${t}`,p=>{p.reportError(1,` ${he.prettyField(s,{label:"Repository URL",value:he.tuple(he.Type.URL,a)})}`);for(let h of f.matchAll(/^(.+?): (.*)$/gm)){let[,E,C]=h;E=E.toLowerCase();let S=E==="error"?"Error":`${(0,BBe.default)(E)} Error`;p.reportError(1,` ${he.prettyField(s,{label:S,value:he.tuple(he.Type.NO_HINT,C)})}`)}c?.(p)})}}var AS=class{supports(e,r){return jC(e.reference)}getLocalPath(e,r){return null}async fetch(e,r){let s=r.checksums.get(e.locatorHash)||null,a=new Map(r.checksums);a.set(e.locatorHash,s);let n={...r,checksums:a},c=await this.downloadHosted(e,n);if(c!==null)return c;let[f,p,h]=await r.cache.fetchPackageFromCache(e,s,{onHit:()=>r.report.reportCacheHit(e),onMiss:()=>r.report.reportCacheMiss(e,`${q.prettyLocator(r.project.configuration,e)} can't be found in the cache and will be fetched from the remote repository`),loader:()=>this.cloneFromRemote(e,n),...r.cacheOptions});return{packageFs:f,releaseFs:p,prefixPath:q.getIdentVendorPath(e),checksum:h}}async downloadHosted(e,r){return r.project.configuration.reduceHook(s=>s.fetchHostedRepository,null,e,r)}async cloneFromRemote(e,r){let s=G0(e.reference),a=await G5(e.reference,r.project.configuration),n=K.resolve(a,s.extra.cwd??vt.dot),c=K.join(n,"package.tgz");await In.prepareExternalProject(n,c,{configuration:r.project.configuration,report:r.report,workspace:s.extra.workspace,locator:e});let f=await le.readFilePromise(c);return await je.releaseAfterUseAsync(async()=>await gs.convertToZip(f,{configuration:r.project.configuration,prefixPath:q.getIdentVendorPath(e),stripComponents:1}))}};Ve();Ve();var pS=class{supportsDescriptor(e,r){return jC(e.range)}supportsLocator(e,r){return jC(e.reference)}shouldPersistResolution(e,r){return!0}bindDescriptor(e,r,s){return e}getResolutionDependencies(e,r){return{}}async getCandidates(e,r,s){let a=await q5(e.range,s.project.configuration);return[q.makeLocator(e,a)]}async getSatisfying(e,r,s,a){let n=G0(e.range);return{locators:s.filter(f=>{if(f.identHash!==e.identHash)return!1;let p=G0(f.reference);return!(n.repo!==p.repo||n.treeish.protocol==="commit"&&n.treeish.request!==p.treeish.request)}),sorted:!1}}async resolve(e,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(e,r.fetchOptions),a=await je.releaseAfterUseAsync(async()=>await Ht.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...e,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"HARD",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var cSt={configuration:{changesetBaseRefs:{description:"The base git refs that the current HEAD is compared against when detecting changes. Supports git branches, tags, and commits.",type:"STRING",isArray:!0,isNullable:!1,default:["master","origin/master","upstream/master","main","origin/main","upstream/main"]},changesetIgnorePatterns:{description:"Array of glob patterns; files matching them will be ignored when fetching the changed files",type:"STRING",default:[],isArray:!0},cloneConcurrency:{description:"Maximal number of concurrent clones",type:"NUMBER",default:2}},fetchers:[AS],resolvers:[pS]};var uSt=cSt;Wt();var qC=class extends ut{constructor(){super(...arguments);this.since=ge.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.recursive=ge.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.noPrivate=ge.Boolean("--no-private",{description:"Exclude workspaces that have the private field set to true"});this.verbose=ge.Boolean("-v,--verbose",!1,{description:"Also return the cross-dependencies between workspaces"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["workspaces","list"]]}static{this.usage=ot.Usage({category:"Workspace-related commands",description:"list all available workspaces",details:"\n This command will print the list of all workspaces in the project.\n\n - If `--since` is set, Yarn will only list workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `--no-private` is set, Yarn will not list any workspaces that have the `private` field set to `true`.\n\n - If both the `-v,--verbose` and `--json` options are set, Yarn will also return the cross-dependencies between each workspaces (useful when you wish to automatically generate Buck / Bazel rules).\n "})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Tt.find(r,this.context.cwd);return(await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async n=>{let c=this.since?await Qa.fetchChangedWorkspaces({ref:this.since,project:s}):s.workspaces,f=new Set(c);if(this.recursive)for(let p of[...c].map(h=>h.getRecursiveWorkspaceDependents()))for(let h of p)f.add(h);for(let p of f){let{manifest:h}=p;if(h.private&&this.noPrivate)continue;let E;if(this.verbose){let C=new Set,S=new Set;for(let P of Ht.hardDependencies)for(let[I,R]of h.getForScope(P)){let N=s.tryWorkspaceByDescriptor(R);N===null?s.workspacesByIdent.has(I)&&S.add(R):C.add(N)}E={workspaceDependencies:Array.from(C).map(P=>P.relativeCwd),mismatchedWorkspaceDependencies:Array.from(S).map(P=>q.stringifyDescriptor(P))}}n.reportInfo(null,`${p.relativeCwd}`),n.reportJson({location:p.relativeCwd,name:h.name?q.stringifyIdent(h.name):null,...E})}})).exitCode()}};Ve();Ve();Wt();var GC=class extends ut{constructor(){super(...arguments);this.workspaceName=ge.String();this.commandName=ge.String();this.args=ge.Proxy()}static{this.paths=[["workspace"]]}static{this.usage=ot.Usage({category:"Workspace-related commands",description:"run a command within the specified workspace",details:` This command will run a given sub-command on a single workspace. `,examples:[["Add a package to a single workspace","yarn workspace components add -D react"],["Run build script on a single workspace","yarn workspace components run build"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=s.workspaces,c=new Map(n.map(p=>[q.stringifyIdent(p.anchoredLocator),p])),f=c.get(this.workspaceName);if(f===void 0){let p=Array.from(c.keys()).sort();throw new nt(`Workspace '${this.workspaceName}' not found. Did you mean any of the following: - ${p.join(` - `)}?`)}return this.cli.run([this.commandName,...this.args],{cwd:f.cwd})}};var fSt={configuration:{enableImmutableInstalls:{description:"If true (the default on CI), prevents the install command from modifying the lockfile",type:"BOOLEAN",default:xBe.isCI},defaultSemverRangePrefix:{description:"The default save prefix: '^', '~' or ''",type:"STRING",values:["^","~",""],default:"^"},preferReuse:{description:"If true, `yarn add` will attempt to reuse the most common dependency range in other workspaces.",type:"BOOLEAN",default:!1}},commands:[aC,lC,cC,uC,OC,bC,EC,qC,pC,hC,gC,dC,sC,oC,fC,AC,mC,yC,IC,CC,wC,BC,LC,vC,SC,xC,PC,kC,DC,QC,TC,RC,FC,NC,MC,_C,GC]},ASt=fSt;var Z5={};Vt(Z5,{default:()=>hSt});Ve();var Qt={optional:!0},V5=[["@tailwindcss/aspect-ratio@<0.2.1",{peerDependencies:{tailwindcss:"^2.0.2"}}],["@tailwindcss/line-clamp@<0.2.1",{peerDependencies:{tailwindcss:"^2.0.2"}}],["@fullhuman/postcss-purgecss@3.1.3 || 3.1.3-alpha.0",{peerDependencies:{postcss:"^8.0.0"}}],["@samverschueren/stream-to-observable@<0.3.1",{peerDependenciesMeta:{rxjs:Qt,zenObservable:Qt}}],["any-observable@<0.5.1",{peerDependenciesMeta:{rxjs:Qt,zenObservable:Qt}}],["@pm2/agent@<1.0.4",{dependencies:{debug:"*"}}],["debug@<4.2.0",{peerDependenciesMeta:{"supports-color":Qt}}],["got@<11",{dependencies:{"@types/responselike":"^1.0.0","@types/keyv":"^3.1.1"}}],["cacheable-lookup@<4.1.2",{dependencies:{"@types/keyv":"^3.1.1"}}],["http-link-dataloader@*",{peerDependencies:{graphql:"^0.13.1 || ^14.0.0"}}],["typescript-language-server@*",{dependencies:{"vscode-jsonrpc":"^5.0.1","vscode-languageserver-protocol":"^3.15.0"}}],["postcss-syntax@*",{peerDependenciesMeta:{"postcss-html":Qt,"postcss-jsx":Qt,"postcss-less":Qt,"postcss-markdown":Qt,"postcss-scss":Qt}}],["jss-plugin-rule-value-function@<=10.1.1",{dependencies:{"tiny-warning":"^1.0.2"}}],["ink-select-input@<4.1.0",{peerDependencies:{react:"^16.8.2"}}],["license-webpack-plugin@<2.3.18",{peerDependenciesMeta:{webpack:Qt}}],["snowpack@>=3.3.0",{dependencies:{"node-gyp":"^7.1.0"}}],["promise-inflight@*",{peerDependenciesMeta:{bluebird:Qt}}],["reactcss@*",{peerDependencies:{react:"*"}}],["react-color@<=2.19.0",{peerDependencies:{react:"*"}}],["gatsby-plugin-i18n@*",{dependencies:{ramda:"^0.24.1"}}],["useragent@^2.0.0",{dependencies:{request:"^2.88.0",yamlparser:"0.0.x",semver:"5.5.x"}}],["@apollographql/apollo-tools@<=0.5.2",{peerDependencies:{graphql:"^14.2.1 || ^15.0.0"}}],["material-table@^2.0.0",{dependencies:{"@babel/runtime":"^7.11.2"}}],["@babel/parser@*",{dependencies:{"@babel/types":"^7.8.3"}}],["fork-ts-checker-webpack-plugin@<=6.3.4",{peerDependencies:{eslint:">= 6",typescript:">= 2.7",webpack:">= 4","vue-template-compiler":"*"},peerDependenciesMeta:{eslint:Qt,"vue-template-compiler":Qt}}],["rc-animate@<=3.1.1",{peerDependencies:{react:">=16.9.0","react-dom":">=16.9.0"}}],["react-bootstrap-table2-paginator@*",{dependencies:{classnames:"^2.2.6"}}],["react-draggable@<=4.4.3",{peerDependencies:{react:">= 16.3.0","react-dom":">= 16.3.0"}}],["apollo-upload-client@<14",{peerDependencies:{graphql:"14 - 15"}}],["react-instantsearch-core@<=6.7.0",{peerDependencies:{algoliasearch:">= 3.1 < 5"}}],["react-instantsearch-dom@<=6.7.0",{dependencies:{"react-fast-compare":"^3.0.0"}}],["ws@<7.2.1",{peerDependencies:{bufferutil:"^4.0.1","utf-8-validate":"^5.0.2"},peerDependenciesMeta:{bufferutil:Qt,"utf-8-validate":Qt}}],["react-portal@<4.2.2",{peerDependencies:{"react-dom":"^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0"}}],["react-scripts@<=4.0.1",{peerDependencies:{react:"*"}}],["testcafe@<=1.10.1",{dependencies:{"@babel/plugin-transform-for-of":"^7.12.1","@babel/runtime":"^7.12.5"}}],["testcafe-legacy-api@<=4.2.0",{dependencies:{"testcafe-hammerhead":"^17.0.1","read-file-relative":"^1.2.0"}}],["@google-cloud/firestore@<=4.9.3",{dependencies:{protobufjs:"^6.8.6"}}],["gatsby-source-apiserver@*",{dependencies:{"babel-polyfill":"^6.26.0"}}],["@webpack-cli/package-utils@<=1.0.1-alpha.4",{dependencies:{"cross-spawn":"^7.0.3"}}],["gatsby-remark-prismjs@<3.3.28",{dependencies:{lodash:"^4"}}],["gatsby-plugin-favicon@*",{peerDependencies:{webpack:"*"}}],["gatsby-plugin-sharp@<=4.6.0-next.3",{dependencies:{debug:"^4.3.1"}}],["gatsby-react-router-scroll@<=5.6.0-next.0",{dependencies:{"prop-types":"^15.7.2"}}],["@rebass/forms@*",{dependencies:{"@styled-system/should-forward-prop":"^5.0.0"},peerDependencies:{react:"^16.8.6"}}],["rebass@*",{peerDependencies:{react:"^16.8.6"}}],["@ant-design/react-slick@<=0.28.3",{peerDependencies:{react:">=16.0.0"}}],["mqtt@<4.2.7",{dependencies:{duplexify:"^4.1.1"}}],["vue-cli-plugin-vuetify@<=2.0.3",{dependencies:{semver:"^6.3.0"},peerDependenciesMeta:{"sass-loader":Qt,"vuetify-loader":Qt}}],["vue-cli-plugin-vuetify@<=2.0.4",{dependencies:{"null-loader":"^3.0.0"}}],["vue-cli-plugin-vuetify@>=2.4.3",{peerDependencies:{vue:"*"}}],["@vuetify/cli-plugin-utils@<=0.0.4",{dependencies:{semver:"^6.3.0"},peerDependenciesMeta:{"sass-loader":Qt}}],["@vue/cli-plugin-typescript@<=5.0.0-alpha.0",{dependencies:{"babel-loader":"^8.1.0"}}],["@vue/cli-plugin-typescript@<=5.0.0-beta.0",{dependencies:{"@babel/core":"^7.12.16"},peerDependencies:{"vue-template-compiler":"^2.0.0"},peerDependenciesMeta:{"vue-template-compiler":Qt}}],["cordova-ios@<=6.3.0",{dependencies:{underscore:"^1.9.2"}}],["cordova-lib@<=10.0.1",{dependencies:{underscore:"^1.9.2"}}],["git-node-fs@*",{peerDependencies:{"js-git":"^0.7.8"},peerDependenciesMeta:{"js-git":Qt}}],["consolidate@<0.16.0",{peerDependencies:{mustache:"^3.0.0"},peerDependenciesMeta:{mustache:Qt}}],["consolidate@<=0.16.0",{peerDependencies:{velocityjs:"^2.0.1",tinyliquid:"^0.2.34","liquid-node":"^3.0.1",jade:"^1.11.0","then-jade":"*",dust:"^0.3.0","dustjs-helpers":"^1.7.4","dustjs-linkedin":"^2.7.5",swig:"^1.4.2","swig-templates":"^2.0.3","razor-tmpl":"^1.3.1",atpl:">=0.7.6",liquor:"^0.0.5",twig:"^1.15.2",ejs:"^3.1.5",eco:"^1.1.0-rc-3",jazz:"^0.0.18",jqtpl:"~1.1.0",hamljs:"^0.6.2",hamlet:"^0.3.3",whiskers:"^0.4.0","haml-coffee":"^1.14.1","hogan.js":"^3.0.2",templayed:">=0.2.3",handlebars:"^4.7.6",underscore:"^1.11.0",lodash:"^4.17.20",pug:"^3.0.0","then-pug":"*",qejs:"^3.0.5",walrus:"^0.10.1",mustache:"^4.0.1",just:"^0.1.8",ect:"^0.5.9",mote:"^0.2.0",toffee:"^0.3.6",dot:"^1.1.3","bracket-template":"^1.1.5",ractive:"^1.3.12",nunjucks:"^3.2.2",htmling:"^0.0.8","babel-core":"^6.26.3",plates:"~0.4.11","react-dom":"^16.13.1",react:"^16.13.1","arc-templates":"^0.5.3",vash:"^0.13.0",slm:"^2.0.0",marko:"^3.14.4",teacup:"^2.0.0","coffee-script":"^1.12.7",squirrelly:"^5.1.0",twing:"^5.0.2"},peerDependenciesMeta:{velocityjs:Qt,tinyliquid:Qt,"liquid-node":Qt,jade:Qt,"then-jade":Qt,dust:Qt,"dustjs-helpers":Qt,"dustjs-linkedin":Qt,swig:Qt,"swig-templates":Qt,"razor-tmpl":Qt,atpl:Qt,liquor:Qt,twig:Qt,ejs:Qt,eco:Qt,jazz:Qt,jqtpl:Qt,hamljs:Qt,hamlet:Qt,whiskers:Qt,"haml-coffee":Qt,"hogan.js":Qt,templayed:Qt,handlebars:Qt,underscore:Qt,lodash:Qt,pug:Qt,"then-pug":Qt,qejs:Qt,walrus:Qt,mustache:Qt,just:Qt,ect:Qt,mote:Qt,toffee:Qt,dot:Qt,"bracket-template":Qt,ractive:Qt,nunjucks:Qt,htmling:Qt,"babel-core":Qt,plates:Qt,"react-dom":Qt,react:Qt,"arc-templates":Qt,vash:Qt,slm:Qt,marko:Qt,teacup:Qt,"coffee-script":Qt,squirrelly:Qt,twing:Qt}}],["vue-loader@<=16.3.3",{peerDependencies:{"@vue/compiler-sfc":"^3.0.8",webpack:"^4.1.0 || ^5.0.0-0"},peerDependenciesMeta:{"@vue/compiler-sfc":Qt}}],["vue-loader@^16.7.0",{peerDependencies:{"@vue/compiler-sfc":"^3.0.8",vue:"^3.2.13"},peerDependenciesMeta:{"@vue/compiler-sfc":Qt,vue:Qt}}],["scss-parser@<=1.0.5",{dependencies:{lodash:"^4.17.21"}}],["query-ast@<1.0.5",{dependencies:{lodash:"^4.17.21"}}],["redux-thunk@<=2.3.0",{peerDependencies:{redux:"^4.0.0"}}],["skypack@<=0.3.2",{dependencies:{tar:"^6.1.0"}}],["@npmcli/metavuln-calculator@<2.0.0",{dependencies:{"json-parse-even-better-errors":"^2.3.1"}}],["bin-links@<2.3.0",{dependencies:{"mkdirp-infer-owner":"^1.0.2"}}],["rollup-plugin-polyfill-node@<=0.8.0",{peerDependencies:{rollup:"^1.20.0 || ^2.0.0"}}],["snowpack@<3.8.6",{dependencies:{"magic-string":"^0.25.7"}}],["elm-webpack-loader@*",{dependencies:{temp:"^0.9.4"}}],["winston-transport@<=4.4.0",{dependencies:{logform:"^2.2.0"}}],["jest-vue-preprocessor@*",{dependencies:{"@babel/core":"7.8.7","@babel/template":"7.8.6"},peerDependencies:{pug:"^2.0.4"},peerDependenciesMeta:{pug:Qt}}],["redux-persist@*",{peerDependencies:{react:">=16"},peerDependenciesMeta:{react:Qt}}],["sodium@>=3",{dependencies:{"node-gyp":"^3.8.0"}}],["babel-plugin-graphql-tag@<=3.1.0",{peerDependencies:{graphql:"^14.0.0 || ^15.0.0"}}],["@playwright/test@<=1.14.1",{dependencies:{"jest-matcher-utils":"^26.4.2"}}],...["babel-plugin-remove-graphql-queries@<3.14.0-next.1","babel-preset-gatsby-package@<1.14.0-next.1","create-gatsby@<1.14.0-next.1","gatsby-admin@<0.24.0-next.1","gatsby-cli@<3.14.0-next.1","gatsby-core-utils@<2.14.0-next.1","gatsby-design-tokens@<3.14.0-next.1","gatsby-legacy-polyfills@<1.14.0-next.1","gatsby-plugin-benchmark-reporting@<1.14.0-next.1","gatsby-plugin-graphql-config@<0.23.0-next.1","gatsby-plugin-image@<1.14.0-next.1","gatsby-plugin-mdx@<2.14.0-next.1","gatsby-plugin-netlify-cms@<5.14.0-next.1","gatsby-plugin-no-sourcemaps@<3.14.0-next.1","gatsby-plugin-page-creator@<3.14.0-next.1","gatsby-plugin-preact@<5.14.0-next.1","gatsby-plugin-preload-fonts@<2.14.0-next.1","gatsby-plugin-schema-snapshot@<2.14.0-next.1","gatsby-plugin-styletron@<6.14.0-next.1","gatsby-plugin-subfont@<3.14.0-next.1","gatsby-plugin-utils@<1.14.0-next.1","gatsby-recipes@<0.25.0-next.1","gatsby-source-shopify@<5.6.0-next.1","gatsby-source-wikipedia@<3.14.0-next.1","gatsby-transformer-screenshot@<3.14.0-next.1","gatsby-worker@<0.5.0-next.1"].map(t=>[t,{dependencies:{"@babel/runtime":"^7.14.8"}}]),["gatsby-core-utils@<2.14.0-next.1",{dependencies:{got:"8.3.2"}}],["gatsby-plugin-gatsby-cloud@<=3.1.0-next.0",{dependencies:{"gatsby-core-utils":"^2.13.0-next.0"}}],["gatsby-plugin-gatsby-cloud@<=3.2.0-next.1",{peerDependencies:{webpack:"*"}}],["babel-plugin-remove-graphql-queries@<=3.14.0-next.1",{dependencies:{"gatsby-core-utils":"^2.8.0-next.1"}}],["gatsby-plugin-netlify@3.13.0-next.1",{dependencies:{"gatsby-core-utils":"^2.13.0-next.0"}}],["clipanion-v3-codemod@<=0.2.0",{peerDependencies:{jscodeshift:"^0.11.0"}}],["react-live@*",{peerDependencies:{"react-dom":"*",react:"*"}}],["webpack@<4.44.1",{peerDependenciesMeta:{"webpack-cli":Qt,"webpack-command":Qt}}],["webpack@<5.0.0-beta.23",{peerDependenciesMeta:{"webpack-cli":Qt}}],["webpack-dev-server@<3.10.2",{peerDependenciesMeta:{"webpack-cli":Qt}}],["@docusaurus/responsive-loader@<1.5.0",{peerDependenciesMeta:{sharp:Qt,jimp:Qt}}],["eslint-module-utils@*",{peerDependenciesMeta:{"eslint-import-resolver-node":Qt,"eslint-import-resolver-typescript":Qt,"eslint-import-resolver-webpack":Qt,"@typescript-eslint/parser":Qt}}],["eslint-plugin-import@*",{peerDependenciesMeta:{"@typescript-eslint/parser":Qt}}],["critters-webpack-plugin@<3.0.2",{peerDependenciesMeta:{"html-webpack-plugin":Qt}}],["terser@<=5.10.0",{dependencies:{acorn:"^8.5.0"}}],["babel-preset-react-app@10.0.x <10.0.2",{dependencies:{"@babel/plugin-proposal-private-property-in-object":"^7.16.7"}}],["eslint-config-react-app@*",{peerDependenciesMeta:{typescript:Qt}}],["@vue/eslint-config-typescript@<11.0.0",{peerDependenciesMeta:{typescript:Qt}}],["unplugin-vue2-script-setup@<0.9.1",{peerDependencies:{"@vue/composition-api":"^1.4.3","@vue/runtime-dom":"^3.2.26"}}],["@cypress/snapshot@*",{dependencies:{debug:"^3.2.7"}}],["auto-relay@<=0.14.0",{peerDependencies:{"reflect-metadata":"^0.1.13"}}],["vue-template-babel-compiler@<1.2.0",{peerDependencies:{"vue-template-compiler":"^2.6.0"}}],["@parcel/transformer-image@<2.5.0",{peerDependencies:{"@parcel/core":"*"}}],["@parcel/transformer-js@<2.5.0",{peerDependencies:{"@parcel/core":"*"}}],["parcel@*",{peerDependenciesMeta:{"@parcel/core":Qt}}],["react-scripts@*",{peerDependencies:{eslint:"*"}}],["focus-trap-react@^8.0.0",{dependencies:{tabbable:"^5.3.2"}}],["react-rnd@<10.3.7",{peerDependencies:{react:">=16.3.0","react-dom":">=16.3.0"}}],["connect-mongo@<5.0.0",{peerDependencies:{"express-session":"^1.17.1"}}],["vue-i18n@<9",{peerDependencies:{vue:"^2"}}],["vue-router@<4",{peerDependencies:{vue:"^2"}}],["unified@<10",{dependencies:{"@types/unist":"^2.0.0"}}],["react-github-btn@<=1.3.0",{peerDependencies:{react:">=16.3.0"}}],["react-dev-utils@*",{peerDependencies:{typescript:">=2.7",webpack:">=4"},peerDependenciesMeta:{typescript:Qt}}],["@asyncapi/react-component@<=1.0.0-next.39",{peerDependencies:{react:">=16.8.0","react-dom":">=16.8.0"}}],["xo@*",{peerDependencies:{webpack:">=1.11.0"},peerDependenciesMeta:{webpack:Qt}}],["babel-plugin-remove-graphql-queries@<=4.20.0-next.0",{dependencies:{"@babel/types":"^7.15.4"}}],["gatsby-plugin-page-creator@<=4.20.0-next.1",{dependencies:{"fs-extra":"^10.1.0"}}],["gatsby-plugin-utils@<=3.14.0-next.1",{dependencies:{fastq:"^1.13.0"},peerDependencies:{graphql:"^15.0.0"}}],["gatsby-plugin-mdx@<3.1.0-next.1",{dependencies:{mkdirp:"^1.0.4"}}],["gatsby-plugin-mdx@^2",{peerDependencies:{gatsby:"^3.0.0-next"}}],["fdir@<=5.2.0",{peerDependencies:{picomatch:"2.x"},peerDependenciesMeta:{picomatch:Qt}}],["babel-plugin-transform-typescript-metadata@<=0.3.2",{peerDependencies:{"@babel/core":"^7","@babel/traverse":"^7"},peerDependenciesMeta:{"@babel/traverse":Qt}}],["graphql-compose@>=9.0.10",{peerDependencies:{graphql:"^14.2.0 || ^15.0.0 || ^16.0.0"}}],["vite-plugin-vuetify@<=1.0.2",{peerDependencies:{vue:"^3.0.0"}}],["webpack-plugin-vuetify@<=2.0.1",{peerDependencies:{vue:"^3.2.6"}}],["eslint-import-resolver-vite@<2.0.1",{dependencies:{debug:"^4.3.4",resolve:"^1.22.8"}}],["notistack@^3.0.0",{dependencies:{csstype:"^3.0.10"}}],["@fastify/type-provider-typebox@^5.0.0",{peerDependencies:{fastify:"^5.0.0"}}],["@fastify/type-provider-typebox@^4.0.0",{peerDependencies:{fastify:"^4.0.0"}}]];var K5;function kBe(){return typeof K5>"u"&&(K5=Ie("zlib").brotliDecompressSync(Buffer.from("G7weAByFTVk3Vs7UfHhq4yykgEM7pbW7TI43SG2S5tvGrwHBAzdz+s/npQ6tgEvobvxisrPIadkXeUAJotBn5bDZ5kAhcRqsIHe3F75Walet5hNalwgFDtxb0BiDUjiUQkjG0yW2hto9HPgiCkm316d6bC0kST72YN7D7rfkhCE9x4J0XwB0yavalxpUu2t9xszHrmtwalOxT7VslsxWcB1qpqZwERUra4psWhTV8BgwWeizurec82Caf1ABL11YMfbf8FJ9JBceZOkgmvrQPbC9DUldX/yMbmX06UQluCEjSwUoyO+EZPIjofr+/oAZUck2enraRD+oWLlnlYnj8xB+gwSo9lmmks4fXv574qSqcWA6z21uYkzMu3EWj+K23RxeQlLqiE35/rC8GcS4CGkKHKKq+zAIQwD9iRDNfiAqueLLpicFFrNsAI4zeTD/eO9MHcnRa5m8UT+M2+V+AkFST4BlKneiAQRSdST8KEAIyFlULt6wa9EBd0Ds28VmpaxquJdVt+nwdEs5xUskI13OVtFyY0UrQIRAlCuvvWivvlSKQfTO+2Q8OyUR1W5RvetaPz4jD27hdtwHFFA1Ptx6Ee/t2cY2rg2G46M1pNDRf2pWhvpy8pqMnuI3++4OF3+7OFIWXGjh+o7Nr2jNvbiYcQdQS1h903/jVFgOpA0yJ78z+x759bFA0rq+6aY5qPB4FzS3oYoLupDUhD9nDz6F6H7hpnlMf18KNKDu4IKjTWwrAnY6MFQw1W6ymOALHlFyCZmQhldg1MQHaMVVQTVgDC60TfaBqG++Y8PEoFhN/PBTZT175KNP/BlHDYGOOBmnBdzqJKplZ/ljiVG0ZBzfqeBRrrUkn6rA54462SgiliKoYVnbeptMdXNfAuaupIEi0bApF10TlgHfmEJAPUVidRVFyDupSem5po5vErPqWKhKbUIp0LozpYsIKK57dM/HKr+nguF+7924IIWMICkQ8JUigs9D+W+c4LnNoRtPPKNRUiCYmP+Jfo2lfKCKw8qpraEeWU3uiNRO6zcyKQoXPR5htmzzLznke7b4YbXW3I1lIRzmgG02Udb58U+7TpwyN7XymCgH+wuPDthZVQvRZuEP+SnLtMicz9m5zASWOBiAcLmkuFlTKuHspSIhCBD0yUPKcxu81A+4YD78rA2vtwsUEday9WNyrShyrl60rWmA+SmbYZkQOwFJWArxRYYc5jGhA5ikxYw1rx3ei4NmeX/lKiwpZ9Ln1tV2Ae7sArvxuVLbJjqJRjW1vFXAyHpvLG+8MJ6T2Ubx5M2KDa2SN6vuIGxJ9WQM9Mk3Q7aCNiZONXllhqq24DmoLbQfW2rYWsOgHWjtOmIQMyMKdiHZDjoyIq5+U700nZ6odJAoYXPQBvFNiQ78d5jaXliBqLTJEqUCwi+LiH2mx92EmNKDsJL74Z613+3lf20pxkV1+erOrjj8pW00vsPaahKUM+05ssd5uwM7K482KWEf3TCwlg/o3e5ngto7qSMz7YteIgCsF1UOcsLk7F7MxWbvrPMY473ew0G+noVL8EPbkmEMftMSeL6HFub/zy+2JQ==","base64")).toString()),K5}var J5;function QBe(){return typeof J5>"u"&&(J5=Ie("zlib").brotliDecompressSync(Buffer.from("G8MSIIzURnVBnObTcvb3XE6v2S9Qgc2K801Oa5otNKEtK8BINZNcaQHy+9/vf/WXBimwutXC33P2DPc64pps5rz7NGGWaOKNSPL4Y2KRE8twut2lFOIN+OXPtRmPMRhMTILib2bEQx43az2I5d3YS8Roa5UZpF/ujHb3Djd3GDvYUfvFYSUQ39vb2cmifp/rgB4J/65JK3wRBTvMBoNBmn3mbXC63/gbBkW/2IRPri0O8bcsRBsmarF328pAln04nyJFkwUAvNu934supAqLtyerZZpJ8I8suJHhf/ocMV+scKwa8NOiDKIPXw6Ex/EEZD6TEGaW8N5zvNHYF10l6Lfooj7D5W2k3dgvQSbp2Wv8TGOayS978gxlOLVjTGXs66ozewbrjwElLtyrYNnWTfzzdEutgROUFPVMhnMoy8EjJLLlWwIEoySxliim9kYW30JUHiPVyjt0iAw/ZpPmCbUCltYPnq6ZNblIKhTNhqS/oqC9iya5sGKZTOVsTEg34n92uZTf2iPpcZih8rPW8CzA+adIGmyCPcKdLMsBLShd+zuEbTrqpwuh+DLmracZcjPC5Sdf5odDAhKpFuOsQS67RT+1VgWWygSv3YwxDnylc04/PYuaMeIzhBkLrvs7e/OUzRTF56MmfY6rI63QtEjEQzq637zQqJ39nNhu3NmoRRhW/086bHGBUtx0PE0j3aEGvkdh9WJC8y8j8mqqke9/dQ5la+Q3ba4RlhvTbnfQhPDDab3tUifkjKuOsp13mXEmO00Mu88F/M67R7LXfoFDFLNtgCSWjWX+3Jn1371pJTK9xPBiMJafvDjtFyAzu8rxeQ0TKMQXNPs5xxiBOd+BRJP8KP88XPtJIbZKh/cdW8KvBUkpqKpGoiIaA32c3/JnQr4efXt85mXvidOvn/eU3Pase1typLYBalJ14mCso9h79nuMOuCa/kZAOkJHmTjP5RM2WNoPasZUAnT1TAE/NH25hUxcQv6hQWR/m1PKk4ooXMcM4SR1iYU3fUohvqk4RY2hbmTVVIXv6TvqO+0doOjgeVFAcom+RlwJQmOVH7pr1Q9LoJT6n1DeQEB+NHygsATbIwTcOKZlJsY8G4+suX1uQLjUWwLjjs0mvSvZcLTpIGAekeR7GCgl8eo3ndAqEe2XCav4huliHjdbIPBsGJuPX7lrO9HX1UbXRH5opOe1x6JsOSgHZR+EaxuXVhpLLxm6jk1LJtZfHSc6BKPun3CpYYVMJGwEUyk8MTGG0XL5MfEwaXpnc9TKnBmlGn6nHiGREc3ysn47XIBDzA+YvFdjZzVIEDcKGpS6PbUJehFRjEne8D0lVU1XuRtlgszq6pTNlQ/3MzNOEgCWPyTct22V2mEi2krizn5VDo9B19/X2DB3hCGRMM7ONbtnAcIx/OWB1u5uPbW1gsH8irXxT/IzG0PoXWYjhbMsH3KTuoOl5o17PulcgvsfTSnKFM354GWI8luqZnrswWjiXy3G+Vbyo1KMopFmmvBwNELgaS8z8dNZchx/Cl/xjddxhMcyqtzFyONb2Zdu90NkI8pAeufe7YlXrp53v8Dj/l8vWeVspRKBGXScBBPI/HinSTGmLDOGGOCIyH0JFdOZx0gWsacNlQLJMIrBhqRxXxHF/5pseWwejlAAvZ3klZSDSYY8mkToaWejXhgNomeGtx1DTLEUFMRkgF5yFB22WYdJnaWN14r1YJj81hGi45+jrADS5nYRhCiSlCJJ1nL8pYX+HDSMhdTEWyRcgHVp/IsUIZYMfT+YYncUQPgcxNGCHfZ88vDdrcUuaGIl6zhAsiaq7R5dfqrqXH/JcBhfjT8D0azayIyEz75Nxp6YkcyDxlJq3EXnJUpqDohJJOysL1t1uNiHESlvsxPb5cpbW0+ICZqJmUZus1BMW0F5IVBODLIo2zHHjA0=","base64")).toString()),J5}var z5;function TBe(){return typeof z5>"u"&&(z5=Ie("zlib").brotliDecompressSync(Buffer.from("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","base64")).toString()),z5}var RBe=new Map([[q.makeIdent(null,"fsevents").identHash,kBe],[q.makeIdent(null,"resolve").identHash,QBe],[q.makeIdent(null,"typescript").identHash,TBe]]),pSt={hooks:{registerPackageExtensions:async(t,e)=>{for(let[r,s]of V5)e(q.parseDescriptor(r,!0),s)},getBuiltinPatch:async(t,e)=>{let r="compat/";if(!e.startsWith(r))return;let s=q.parseIdent(e.slice(r.length)),a=RBe.get(s.identHash)?.();return typeof a<"u"?a:null},reduceDependency:async(t,e,r,s)=>typeof RBe.get(t.identHash)>"u"?t:q.makeDescriptor(t,q.makeRange({protocol:"patch:",source:q.stringifyDescriptor(t),selector:`optional!builtin`,params:null}))}},hSt=pSt;var g9={};Vt(g9,{ConstraintsCheckCommand:()=>XC,ConstraintsQueryCommand:()=>zC,ConstraintsSourceCommand:()=>ZC,default:()=>HSt});Ve();Ve();gS();var YC=class{constructor(e){this.project=e}createEnvironment(){let e=new WC(["cwd","ident"]),r=new WC(["workspace","type","ident"]),s=new WC(["ident"]),a={manifestUpdates:new Map,reportedErrors:new Map},n=new Map,c=new Map;for(let f of this.project.storedPackages.values()){let p=Array.from(f.peerDependencies.values(),h=>[q.stringifyIdent(h),h.range]);n.set(f.locatorHash,{workspace:null,ident:q.stringifyIdent(f),version:f.version,dependencies:new Map,peerDependencies:new Map(p.filter(([h])=>f.peerDependenciesMeta.get(h)?.optional!==!0)),optionalPeerDependencies:new Map(p.filter(([h])=>f.peerDependenciesMeta.get(h)?.optional===!0))})}for(let f of this.project.storedPackages.values()){let p=n.get(f.locatorHash);p.dependencies=new Map(Array.from(f.dependencies.values(),h=>{let E=this.project.storedResolutions.get(h.descriptorHash);if(typeof E>"u")throw new Error("Assertion failed: The resolution should have been registered");let C=n.get(E);if(typeof C>"u")throw new Error("Assertion failed: The package should have been registered");return[q.stringifyIdent(h),C]})),p.dependencies.delete(p.ident)}for(let f of this.project.workspaces){let p=q.stringifyIdent(f.anchoredLocator),h=f.manifest.exportTo({}),E=n.get(f.anchoredLocator.locatorHash);if(typeof E>"u")throw new Error("Assertion failed: The package should have been registered");let C=(R,N,{caller:U=ps.getCaller()}={})=>{let W=hS(R),te=je.getMapWithDefault(a.manifestUpdates,f.cwd),ie=je.getMapWithDefault(te,W),Ae=je.getSetWithDefault(ie,N);U!==null&&Ae.add(U)},S=R=>C(R,void 0,{caller:ps.getCaller()}),P=R=>{je.getArrayWithDefault(a.reportedErrors,f.cwd).push(R)},I=e.insert({cwd:f.relativeCwd,ident:p,manifest:h,pkg:E,set:C,unset:S,error:P});c.set(f,I);for(let R of Ht.allDependencies)for(let N of f.manifest[R].values()){let U=q.stringifyIdent(N),W=()=>{C([R,U],void 0,{caller:ps.getCaller()})},te=Ae=>{C([R,U],Ae,{caller:ps.getCaller()})},ie=null;if(R!=="peerDependencies"&&(R!=="dependencies"||!f.manifest.devDependencies.has(N.identHash))){let Ae=f.anchoredPackage.dependencies.get(N.identHash);if(Ae){if(typeof Ae>"u")throw new Error("Assertion failed: The dependency should have been registered");let ce=this.project.storedResolutions.get(Ae.descriptorHash);if(typeof ce>"u")throw new Error("Assertion failed: The resolution should have been registered");let me=n.get(ce);if(typeof me>"u")throw new Error("Assertion failed: The package should have been registered");ie=me}}r.insert({workspace:I,ident:U,range:N.range,type:R,resolution:ie,update:te,delete:W,error:P})}}for(let f of this.project.storedPackages.values()){let p=this.project.tryWorkspaceByLocator(f);if(!p)continue;let h=c.get(p);if(typeof h>"u")throw new Error("Assertion failed: The workspace should have been registered");let E=n.get(f.locatorHash);if(typeof E>"u")throw new Error("Assertion failed: The package should have been registered");E.workspace=h}return{workspaces:e,dependencies:r,packages:s,result:a}}async process(){let e=this.createEnvironment(),r={Yarn:{workspace:a=>e.workspaces.find(a)[0]??null,workspaces:a=>e.workspaces.find(a),dependency:a=>e.dependencies.find(a)[0]??null,dependencies:a=>e.dependencies.find(a),package:a=>e.packages.find(a)[0]??null,packages:a=>e.packages.find(a)}},s=await this.project.loadUserConfig();return s?.constraints?(await s.constraints(r),e.result):null}};Ve();Ve();Wt();var zC=class extends ut{constructor(){super(...arguments);this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.query=ge.String()}static{this.paths=[["constraints","query"]]}static{this.usage=ot.Usage({category:"Constraints-related commands",description:"query the constraints fact database",details:` This command will output all matches to the given prolog query. `,examples:[["List all dependencies throughout the workspace","yarn constraints query 'workspace_has_dependency(_, DependencyName, _, _).'"]]})}async execute(){let{Constraints:r}=await Promise.resolve().then(()=>(ES(),yS)),s=await ze.find(this.context.cwd,this.context.plugins),{project:a}=await Tt.find(s,this.context.cwd),n=await r.find(a),c=this.query;return c.endsWith(".")||(c=`${c}.`),(await Ot.start({configuration:s,json:this.json,stdout:this.context.stdout},async p=>{for await(let h of n.query(c)){let E=Array.from(Object.entries(h)),C=E.length,S=E.reduce((P,[I])=>Math.max(P,I.length),0);for(let P=0;P(ES(),yS)),s=await ze.find(this.context.cwd,this.context.plugins),{project:a}=await Tt.find(s,this.context.cwd),n=await r.find(a);this.context.stdout.write(this.verbose?n.fullSource:n.source)}};Ve();Ve();Wt();gS();var XC=class extends ut{constructor(){super(...arguments);this.fix=ge.Boolean("--fix",!1,{description:"Attempt to automatically fix unambiguous issues, following a multi-pass process"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["constraints"]]}static{this.usage=ot.Usage({category:"Constraints-related commands",description:"check that the project constraints are met",details:` This command will run constraints on your project and emit errors for each one that is found but isn't met. If any error is emitted the process will exit with a non-zero exit code. If the \`--fix\` flag is used, Yarn will attempt to automatically fix the issues the best it can, following a multi-pass process (with a maximum of 10 iterations). Some ambiguous patterns cannot be autofixed, in which case you'll have to manually specify the right resolution. For more information as to how to write constraints, please consult our dedicated page on our website: https://yarnpkg.com/features/constraints. `,examples:[["Check that all constraints are satisfied","yarn constraints"],["Autofix all unmet constraints","yarn constraints --fix"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Tt.find(r,this.context.cwd);await s.restoreInstallState();let a=await s.loadUserConfig(),n;if(a?.constraints)n=new YC(s);else{let{Constraints:h}=await Promise.resolve().then(()=>(ES(),yS));n=await h.find(s)}let c,f=!1,p=!1;for(let h=this.fix?10:1;h>0;--h){let E=await n.process();if(!E)break;let{changedWorkspaces:C,remainingErrors:S}=oF(s,E,{fix:this.fix}),P=[];for(let[I,R]of C){let N=I.manifest.indent;I.manifest=new Ht,I.manifest.indent=N,I.manifest.load(R),P.push(I.persistManifest())}if(await Promise.all(P),!(C.size>0&&h>1)){c=UBe(S,{configuration:r}),f=!1,p=!0;for(let[,I]of S)for(let R of I)R.fixable?f=!0:p=!1}}if(c.children.length===0)return 0;if(f){let h=p?`Those errors can all be fixed by running ${he.pretty(r,"yarn constraints --fix",he.Type.CODE)}`:`Errors prefixed by '\u2699' can be fixed by running ${he.pretty(r,"yarn constraints --fix",he.Type.CODE)}`;await Ot.start({configuration:r,stdout:this.context.stdout,includeNames:!1,includeFooter:!1},async E=>{E.reportInfo(0,h),E.reportSeparator()})}return c.children=je.sortMap(c.children,h=>h.value[1]),Qs.emitTree(c,{configuration:r,stdout:this.context.stdout,json:this.json,separators:1}),1}};gS();var USt={configuration:{enableConstraintsChecks:{description:"If true, constraints will run during installs",type:"BOOLEAN",default:!1},constraintsPath:{description:"The path of the constraints file.",type:"ABSOLUTE_PATH",default:"./constraints.pro"}},commands:[zC,ZC,XC],hooks:{async validateProjectAfterInstall(t,{reportError:e}){if(!t.configuration.get("enableConstraintsChecks"))return;let r=await t.loadUserConfig(),s;if(r?.constraints)s=new YC(t);else{let{Constraints:c}=await Promise.resolve().then(()=>(ES(),yS));s=await c.find(t)}let a=await s.process();if(!a)return;let{remainingErrors:n}=oF(t,a);if(n.size!==0)if(t.configuration.isCI)for(let[c,f]of n)for(let p of f)e(84,`${he.pretty(t.configuration,c.anchoredLocator,he.Type.IDENT)}: ${p.text}`);else e(84,`Constraint check failed; run ${he.pretty(t.configuration,"yarn constraints",he.Type.CODE)} for more details`)}}},HSt=USt;var d9={};Vt(d9,{CreateCommand:()=>$C,DlxCommand:()=>ew,default:()=>qSt});Ve();Wt();var $C=class extends ut{constructor(){super(...arguments);this.pkg=ge.String("-p,--package",{description:"The package to run the provided command from"});this.quiet=ge.Boolean("-q,--quiet",!1,{description:"Only report critical errors instead of printing the full install logs"});this.command=ge.String();this.args=ge.Proxy()}static{this.paths=[["create"]]}async execute(){let r=[];this.pkg&&r.push("--package",this.pkg),this.quiet&&r.push("--quiet");let s=this.command.replace(/^(@[^@/]+)(@|$)/,"$1/create$2"),a=q.parseDescriptor(s),n=a.name.match(/^create(-|$)/)?a:a.scope?q.makeIdent(a.scope,`create-${a.name}`):q.makeIdent(null,`create-${a.name}`),c=q.stringifyIdent(n);return a.range!=="unknown"&&(c+=`@${a.range}`),this.cli.run(["dlx",...r,c,...this.args])}};Ve();Ve();bt();Wt();var ew=class extends ut{constructor(){super(...arguments);this.packages=ge.Array("-p,--package",{description:"The package(s) to install before running the command"});this.quiet=ge.Boolean("-q,--quiet",!1,{description:"Only report critical errors instead of printing the full install logs"});this.command=ge.String();this.args=ge.Proxy()}static{this.paths=[["dlx"]]}static{this.usage=ot.Usage({description:"run a package in a temporary environment",details:"\n This command will install a package within a temporary environment, and run its binary script if it contains any. The binary will run within the current cwd.\n\n By default Yarn will download the package named `command`, but this can be changed through the use of the `-p,--package` flag which will instruct Yarn to still run the same command but from a different package.\n\n Using `yarn dlx` as a replacement of `yarn add` isn't recommended, as it makes your project non-deterministic (Yarn doesn't keep track of the packages installed through `dlx` - neither their name, nor their version).\n ",examples:[["Use create-vite to scaffold a new Vite project","yarn dlx create-vite"],["Install multiple packages for a single command",`yarn dlx -p typescript -p ts-node ts-node --transpile-only -e "console.log('hello!')"`]]})}async execute(){return ze.telemetry=null,await le.mktempPromise(async r=>{let s=K.join(r,`dlx-${process.pid}`);await le.mkdirPromise(s),await le.writeFilePromise(K.join(s,"package.json"),`{} `),await le.writeFilePromise(K.join(s,"yarn.lock"),"");let a=K.join(s,".yarnrc.yml"),n=await ze.findProjectCwd(this.context.cwd),f={enableGlobalCache:!(await ze.find(this.context.cwd,null,{strict:!1})).get("enableGlobalCache"),enableTelemetry:!1,logFilters:[{code:Vf(68),level:he.LogLevel.Discard}]},p=n!==null?K.join(n,".yarnrc.yml"):null;p!==null&&le.existsSync(p)?(await le.copyFilePromise(p,a),await ze.updateConfiguration(s,N=>{let U=je.toMerged(N,f);return Array.isArray(N.plugins)&&(U.plugins=N.plugins.map(W=>{let te=typeof W=="string"?W:W.path,ie=ue.isAbsolute(te)?te:ue.resolve(ue.fromPortablePath(n),te);return typeof W=="string"?ie:{path:ie,spec:W.spec}})),U})):await le.writeJsonPromise(a,f);let h=this.packages??[this.command],E=q.parseDescriptor(this.command).name,C=await this.cli.run(["add","--fixed","--",...h],{cwd:s,quiet:this.quiet});if(C!==0)return C;this.quiet||this.context.stdout.write(` `);let S=await ze.find(s,this.context.plugins),{project:P,workspace:I}=await Tt.find(S,s);if(I===null)throw new ar(P.cwd,s);await P.restoreInstallState();let R=await In.getWorkspaceAccessibleBinaries(I);return R.has(E)===!1&&R.size===1&&typeof this.packages>"u"&&(E=Array.from(R)[0][0]),await In.executeWorkspaceAccessibleBinary(I,E,this.args,{packageAccessibleBinaries:R,cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr})})}};var jSt={commands:[$C,ew]},qSt=jSt;var E9={};Vt(E9,{ExecFetcher:()=>CS,ExecResolver:()=>wS,default:()=>YSt,execUtils:()=>uF});Ve();Ve();bt();var fA="exec:";var uF={};Vt(uF,{loadGeneratorFile:()=>IS,makeLocator:()=>y9,makeSpec:()=>Ave,parseSpec:()=>m9});Ve();bt();function m9(t){let{params:e,selector:r}=q.parseRange(t),s=ue.toPortablePath(r);return{parentLocator:e&&typeof e.locator=="string"?q.parseLocator(e.locator):null,path:s}}function Ave({parentLocator:t,path:e,generatorHash:r,protocol:s}){let a=t!==null?{locator:q.stringifyLocator(t)}:{},n=typeof r<"u"?{hash:r}:{};return q.makeRange({protocol:s,source:e,selector:e,params:{...n,...a}})}function y9(t,{parentLocator:e,path:r,generatorHash:s,protocol:a}){return q.makeLocator(t,Ave({parentLocator:e,path:r,generatorHash:s,protocol:a}))}async function IS(t,e,r){let{parentLocator:s,path:a}=q.parseFileStyleRange(t,{protocol:e}),n=K.isAbsolute(a)?{packageFs:new Sn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(s,r),c=n.localPath?{packageFs:new Sn(vt.root),prefixPath:K.relative(vt.root,n.localPath)}:n;n!==c&&n.releaseFs&&n.releaseFs();let f=c.packageFs,p=K.join(c.prefixPath,a);return await f.readFilePromise(p,"utf8")}var CS=class{supports(e,r){return!!e.reference.startsWith(fA)}getLocalPath(e,r){let{parentLocator:s,path:a}=q.parseFileStyleRange(e.reference,{protocol:fA});if(K.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:K.resolve(n,a)}async fetch(e,r){let s=r.checksums.get(e.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(e,s,{onHit:()=>r.report.reportCacheHit(e),onMiss:()=>r.report.reportCacheMiss(e),loader:()=>this.fetchFromDisk(e,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:q.getIdentVendorPath(e),localPath:this.getLocalPath(e,r),checksum:c}}async fetchFromDisk(e,r){let s=await IS(e.reference,fA,r);return le.mktempPromise(async a=>{let n=K.join(a,"generator.js");return await le.writeFilePromise(n,s),le.mktempPromise(async c=>{if(await this.generatePackage(c,e,n,r),!le.existsSync(K.join(c,"build")))throw new Error("The script should have generated a build directory");return await gs.makeArchiveFromDirectory(K.join(c,"build"),{prefixPath:q.getIdentVendorPath(e),compressionLevel:r.project.configuration.get("compressionLevel")})})})}async generatePackage(e,r,s,a){return await le.mktempPromise(async n=>{let c=await In.makeScriptEnv({project:a.project,binFolder:n}),f=K.join(e,"runtime.js");return await le.mktempPromise(async p=>{let h=K.join(p,"buildfile.log"),E=K.join(e,"generator"),C=K.join(e,"build");await le.mkdirPromise(E),await le.mkdirPromise(C);let S={tempDir:ue.fromPortablePath(E),buildDir:ue.fromPortablePath(C),locator:q.stringifyLocator(r)};await le.writeFilePromise(f,` // Expose 'Module' as a global variable Object.defineProperty(global, 'Module', { get: () => require('module'), configurable: true, enumerable: false, }); // Expose non-hidden built-in modules as global variables for (const name of Module.builtinModules.filter((name) => name !== 'module' && !name.startsWith('_'))) { Object.defineProperty(global, name, { get: () => require(name), configurable: true, enumerable: false, }); } // Expose the 'execEnv' global variable Object.defineProperty(global, 'execEnv', { value: { ...${JSON.stringify(S)}, }, enumerable: true, }); `);let P=c.NODE_OPTIONS||"",I=/\s*--require\s+\S*\.pnp\.c?js\s*/g;P=P.replace(I," ").trim(),c.NODE_OPTIONS=P;let{stdout:R,stderr:N}=a.project.configuration.getSubprocessStreams(h,{header:`# This file contains the result of Yarn generating a package (${q.stringifyLocator(r)}) `,prefix:q.prettyLocator(a.project.configuration,r),report:a.report}),{code:U}=await Gr.pipevp(process.execPath,["--require",ue.fromPortablePath(f),ue.fromPortablePath(s),q.stringifyIdent(r)],{cwd:e,env:c,stdin:null,stdout:R,stderr:N});if(U!==0)throw le.detachTemp(p),new Error(`Package generation failed (exit code ${U}, logs can be found here: ${he.pretty(a.project.configuration,h,he.Type.PATH)})`)})})}};Ve();Ve();var GSt=2,wS=class{supportsDescriptor(e,r){return!!e.range.startsWith(fA)}supportsLocator(e,r){return!!e.reference.startsWith(fA)}shouldPersistResolution(e,r){return!1}bindDescriptor(e,r,s){return q.bindDescriptor(e,{locator:q.stringifyLocator(r)})}getResolutionDependencies(e,r){return{}}async getCandidates(e,r,s){if(!s.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let{path:a,parentLocator:n}=m9(e.range);if(n===null)throw new Error("Assertion failed: The descriptor should have been bound");let c=await IS(q.makeRange({protocol:fA,source:a,selector:a,params:{locator:q.stringifyLocator(n)}}),fA,s.fetchOptions),f=Nn.makeHash(`${GSt}`,c).slice(0,6);return[y9(e,{parentLocator:n,path:a,generatorHash:f,protocol:fA})]}async getSatisfying(e,r,s,a){let[n]=await this.getCandidates(e,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(e,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(e,r.fetchOptions),a=await je.releaseAfterUseAsync(async()=>await Ht.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...e,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"HARD",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var WSt={fetchers:[CS],resolvers:[wS]},YSt=WSt;var C9={};Vt(C9,{FileFetcher:()=>DS,FileResolver:()=>bS,TarballFileFetcher:()=>PS,TarballFileResolver:()=>xS,default:()=>JSt,fileUtils:()=>Pm});Ve();bt();var tw=/^(?:[a-zA-Z]:[\\/]|\.{0,2}\/)/,BS=/^[^?]*\.(?:tar\.gz|tgz)(?:::.*)?$/,ts="file:";var Pm={};Vt(Pm,{fetchArchiveFromLocator:()=>SS,makeArchiveFromLocator:()=>fF,makeBufferFromLocator:()=>I9,makeLocator:()=>rw,makeSpec:()=>pve,parseSpec:()=>vS});Ve();bt();function vS(t){let{params:e,selector:r}=q.parseRange(t),s=ue.toPortablePath(r);return{parentLocator:e&&typeof e.locator=="string"?q.parseLocator(e.locator):null,path:s}}function pve({parentLocator:t,path:e,hash:r,protocol:s}){let a=t!==null?{locator:q.stringifyLocator(t)}:{},n=typeof r<"u"?{hash:r}:{};return q.makeRange({protocol:s,source:e,selector:e,params:{...n,...a}})}function rw(t,{parentLocator:e,path:r,hash:s,protocol:a}){return q.makeLocator(t,pve({parentLocator:e,path:r,hash:s,protocol:a}))}async function SS(t,e){let{parentLocator:r,path:s}=q.parseFileStyleRange(t.reference,{protocol:ts}),a=K.isAbsolute(s)?{packageFs:new Sn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await e.fetcher.fetch(r,e),n=a.localPath?{packageFs:new Sn(vt.root),prefixPath:K.relative(vt.root,a.localPath)}:a;a!==n&&a.releaseFs&&a.releaseFs();let c=n.packageFs,f=K.join(n.prefixPath,s);return await je.releaseAfterUseAsync(async()=>await c.readFilePromise(f),n.releaseFs)}async function fF(t,{protocol:e,fetchOptions:r,inMemory:s=!1}){let{parentLocator:a,path:n}=q.parseFileStyleRange(t.reference,{protocol:e}),c=K.isAbsolute(n)?{packageFs:new Sn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(a,r),f=c.localPath?{packageFs:new Sn(vt.root),prefixPath:K.relative(vt.root,c.localPath)}:c;c!==f&&c.releaseFs&&c.releaseFs();let p=f.packageFs,h=K.join(f.prefixPath,n);return await je.releaseAfterUseAsync(async()=>await gs.makeArchiveFromDirectory(h,{baseFs:p,prefixPath:q.getIdentVendorPath(t),compressionLevel:r.project.configuration.get("compressionLevel"),inMemory:s}),f.releaseFs)}async function I9(t,{protocol:e,fetchOptions:r}){return(await fF(t,{protocol:e,fetchOptions:r,inMemory:!0})).getBufferAndClose()}var DS=class{supports(e,r){return!!e.reference.startsWith(ts)}getLocalPath(e,r){let{parentLocator:s,path:a}=q.parseFileStyleRange(e.reference,{protocol:ts});if(K.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:K.resolve(n,a)}async fetch(e,r){let s=r.checksums.get(e.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(e,s,{onHit:()=>r.report.reportCacheHit(e),onMiss:()=>r.report.reportCacheMiss(e,`${q.prettyLocator(r.project.configuration,e)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.fetchFromDisk(e,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:q.getIdentVendorPath(e),localPath:this.getLocalPath(e,r),checksum:c}}async fetchFromDisk(e,r){return fF(e,{protocol:ts,fetchOptions:r})}};Ve();Ve();var VSt=2,bS=class{supportsDescriptor(e,r){return e.range.match(tw)?!0:!!e.range.startsWith(ts)}supportsLocator(e,r){return!!e.reference.startsWith(ts)}shouldPersistResolution(e,r){return!1}bindDescriptor(e,r,s){return tw.test(e.range)&&(e=q.makeDescriptor(e,`${ts}${e.range}`)),q.bindDescriptor(e,{locator:q.stringifyLocator(r)})}getResolutionDependencies(e,r){return{}}async getCandidates(e,r,s){if(!s.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let{path:a,parentLocator:n}=vS(e.range);if(n===null)throw new Error("Assertion failed: The descriptor should have been bound");let c=await I9(q.makeLocator(e,q.makeRange({protocol:ts,source:a,selector:a,params:{locator:q.stringifyLocator(n)}})),{protocol:ts,fetchOptions:s.fetchOptions}),f=Nn.makeHash(`${VSt}`,c).slice(0,6);return[rw(e,{parentLocator:n,path:a,hash:f,protocol:ts})]}async getSatisfying(e,r,s,a){let[n]=await this.getCandidates(e,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(e,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(e,r.fetchOptions),a=await je.releaseAfterUseAsync(async()=>await Ht.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...e,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"HARD",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};Ve();var PS=class{supports(e,r){return BS.test(e.reference)?!!e.reference.startsWith(ts):!1}getLocalPath(e,r){return null}async fetch(e,r){let s=r.checksums.get(e.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(e,s,{onHit:()=>r.report.reportCacheHit(e),onMiss:()=>r.report.reportCacheMiss(e,`${q.prettyLocator(r.project.configuration,e)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.fetchFromDisk(e,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:q.getIdentVendorPath(e),checksum:c}}async fetchFromDisk(e,r){let s=await SS(e,r);return await gs.convertToZip(s,{configuration:r.project.configuration,prefixPath:q.getIdentVendorPath(e),stripComponents:1})}};Ve();Ve();Ve();var xS=class{supportsDescriptor(e,r){return BS.test(e.range)?!!(e.range.startsWith(ts)||tw.test(e.range)):!1}supportsLocator(e,r){return BS.test(e.reference)?!!e.reference.startsWith(ts):!1}shouldPersistResolution(e,r){return!1}bindDescriptor(e,r,s){return tw.test(e.range)&&(e=q.makeDescriptor(e,`${ts}${e.range}`)),q.bindDescriptor(e,{locator:q.stringifyLocator(r)})}getResolutionDependencies(e,r){return{}}async getCandidates(e,r,s){if(!s.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let{path:a,parentLocator:n}=vS(e.range);if(n===null)throw new Error("Assertion failed: The descriptor should have been bound");let c=rw(e,{parentLocator:n,path:a,hash:"",protocol:ts}),f=await SS(c,s.fetchOptions),p=Nn.makeHash(f).slice(0,6);return[rw(e,{parentLocator:n,path:a,hash:p,protocol:ts})]}async getSatisfying(e,r,s,a){let[n]=await this.getCandidates(e,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(e,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(e,r.fetchOptions),a=await je.releaseAfterUseAsync(async()=>await Ht.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...e,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"HARD",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var KSt={fetchers:[PS,DS],resolvers:[xS,bS]},JSt=KSt;var v9={};Vt(v9,{GithubFetcher:()=>kS,default:()=>ZSt,githubUtils:()=>AF});Ve();bt();var AF={};Vt(AF,{invalidGithubUrlMessage:()=>dve,isGithubUrl:()=>w9,parseGithubUrl:()=>B9});var hve=et(Ie("querystring")),gve=[/^https?:\/\/(?:([^/]+?)@)?github.com\/([^/#]+)\/([^/#]+)\/tarball\/([^/#]+)(?:#(.*))?$/,/^https?:\/\/(?:([^/]+?)@)?github.com\/([^/#]+)\/([^/#]+?)(?:\.git)?(?:#(.*))?$/];function w9(t){return t?gve.some(e=>!!t.match(e)):!1}function B9(t){let e;for(let f of gve)if(e=t.match(f),e)break;if(!e)throw new Error(dve(t));let[,r,s,a,n="master"]=e,{commit:c}=hve.default.parse(n);return n=c||n.replace(/[^:]*:/,""),{auth:r,username:s,reponame:a,treeish:n}}function dve(t){return`Input cannot be parsed as a valid GitHub URL ('${t}').`}var kS=class{supports(e,r){return!!w9(e.reference)}getLocalPath(e,r){return null}async fetch(e,r){let s=r.checksums.get(e.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(e,s,{onHit:()=>r.report.reportCacheHit(e),onMiss:()=>r.report.reportCacheMiss(e,`${q.prettyLocator(r.project.configuration,e)} can't be found in the cache and will be fetched from GitHub`),loader:()=>this.fetchFromNetwork(e,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:q.getIdentVendorPath(e),checksum:c}}async fetchFromNetwork(e,r){let s=await An.get(this.getLocatorUrl(e,r),{configuration:r.project.configuration});return await le.mktempPromise(async a=>{let n=new Sn(a);await gs.extractArchiveTo(s,n,{stripComponents:1});let c=Qa.splitRepoUrl(e.reference),f=K.join(a,"package.tgz");await In.prepareExternalProject(a,f,{configuration:r.project.configuration,report:r.report,workspace:c.extra.workspace,locator:e});let p=await le.readFilePromise(f);return await gs.convertToZip(p,{configuration:r.project.configuration,prefixPath:q.getIdentVendorPath(e),stripComponents:1})})}getLocatorUrl(e,r){let{auth:s,username:a,reponame:n,treeish:c}=B9(e.reference);return`https://${s?`${s}@`:""}github.com/${a}/${n}/archive/${c}.tar.gz`}};var zSt={hooks:{async fetchHostedRepository(t,e,r){if(t!==null)return t;let s=new kS;if(!s.supports(e,r))return null;try{return await s.fetch(e,r)}catch{return null}}}},ZSt=zSt;var S9={};Vt(S9,{TarballHttpFetcher:()=>TS,TarballHttpResolver:()=>RS,default:()=>$St});Ve();function QS(t){let e;try{e=new URL(t)}catch{return!1}return!(e.protocol!=="http:"&&e.protocol!=="https:"||!e.pathname.match(/(\.tar\.gz|\.tgz|\/[^.]+)$/))}var TS=class{supports(e,r){return QS(e.reference)}getLocalPath(e,r){return null}async fetch(e,r){let s=r.checksums.get(e.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(e,s,{onHit:()=>r.report.reportCacheHit(e),onMiss:()=>r.report.reportCacheMiss(e,`${q.prettyLocator(r.project.configuration,e)} can't be found in the cache and will be fetched from the remote server`),loader:()=>this.fetchFromNetwork(e,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:q.getIdentVendorPath(e),checksum:c}}async fetchFromNetwork(e,r){let s=await An.get(e.reference,{configuration:r.project.configuration});return await gs.convertToZip(s,{configuration:r.project.configuration,prefixPath:q.getIdentVendorPath(e),stripComponents:1})}};Ve();Ve();var RS=class{supportsDescriptor(e,r){return QS(e.range)}supportsLocator(e,r){return QS(e.reference)}shouldPersistResolution(e,r){return!0}bindDescriptor(e,r,s){return e}getResolutionDependencies(e,r){return{}}async getCandidates(e,r,s){return[q.convertDescriptorToLocator(e)]}async getSatisfying(e,r,s,a){let[n]=await this.getCandidates(e,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(e,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(e,r.fetchOptions),a=await je.releaseAfterUseAsync(async()=>await Ht.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...e,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"HARD",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var XSt={fetchers:[TS],resolvers:[RS]},$St=XSt;var D9={};Vt(D9,{InitCommand:()=>J0,InitInitializerCommand:()=>nw,default:()=>tDt});Wt();Ve();Ve();bt();Wt();var J0=class extends ut{constructor(){super(...arguments);this.private=ge.Boolean("-p,--private",!1,{description:"Initialize a private package"});this.workspace=ge.Boolean("-w,--workspace",!1,{description:"Initialize a workspace root with a `packages/` directory"});this.install=ge.String("-i,--install",!1,{tolerateBoolean:!0,description:"Initialize a package with a specific bundle that will be locked in the project"});this.name=ge.String("-n,--name",{description:"Initialize a package with the given name"});this.usev2=ge.Boolean("-2",!1,{hidden:!0});this.yes=ge.Boolean("-y,--yes",{hidden:!0})}static{this.paths=[["init"]]}static{this.usage=ot.Usage({description:"create a new package",details:"\n This command will setup a new package in your local directory.\n\n If the `-p,--private` or `-w,--workspace` options are set, the package will be private by default.\n\n If the `-w,--workspace` option is set, the package will be configured to accept a set of workspaces in the `packages/` directory.\n\n If the `-i,--install` option is given a value, Yarn will first download it using `yarn set version` and only then forward the init call to the newly downloaded bundle. Without arguments, the downloaded bundle will be `latest`.\n\n The initial settings of the manifest can be changed by using the `initScope` and `initFields` configuration values. Additionally, Yarn will generate an EditorConfig file whose rules can be altered via `initEditorConfig`, and will initialize a Git repository in the current directory.\n ",examples:[["Create a new package in the local directory","yarn init"],["Create a new private package in the local directory","yarn init -p"],["Create a new package and store the Yarn release inside","yarn init -i=latest"],["Create a new private package and defines it as a workspace root","yarn init -w"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=typeof this.install=="string"?this.install:this.usev2||this.install===!0?"latest":null;return s!==null?await this.executeProxy(r,s):await this.executeRegular(r)}async executeProxy(r,s){if(r.projectCwd!==null&&r.projectCwd!==this.context.cwd)throw new nt("Cannot use the --install flag from within a project subdirectory");le.existsSync(this.context.cwd)||await le.mkdirPromise(this.context.cwd,{recursive:!0});let a=K.join(this.context.cwd,Er.lockfile);le.existsSync(a)||await le.writeFilePromise(a,"");let n=await this.cli.run(["set","version",s],{quiet:!0});if(n!==0)return n;let c=[];return this.private&&c.push("-p"),this.workspace&&c.push("-w"),this.name&&c.push(`-n=${this.name}`),this.yes&&c.push("-y"),await le.mktempPromise(async f=>{let{code:p}=await Gr.pipevp("yarn",["init",...c],{cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,env:await In.makeScriptEnv({binFolder:f})});return p})}async initialize(){}async executeRegular(r){let s=null;try{s=(await Tt.find(r,this.context.cwd)).project}catch{s=null}le.existsSync(this.context.cwd)||await le.mkdirPromise(this.context.cwd,{recursive:!0});let a=await Ht.tryFind(this.context.cwd),n=a??new Ht,c=Object.fromEntries(r.get("initFields").entries());n.load(c),n.name=n.name??q.makeIdent(r.get("initScope"),this.name??K.basename(this.context.cwd)),n.packageManager=un&&je.isTaggedYarnVersion(un)?`yarn@${un}`:null,(!a&&this.workspace||this.private)&&(n.private=!0),this.workspace&&n.workspaceDefinitions.length===0&&(await le.mkdirPromise(K.join(this.context.cwd,"packages"),{recursive:!0}),n.workspaceDefinitions=[{pattern:"packages/*"}]);let f={};n.exportTo(f);let p=K.join(this.context.cwd,Ht.fileName);await le.changeFilePromise(p,`${JSON.stringify(f,null,2)} `,{automaticNewlines:!0});let h=[p],E=K.join(this.context.cwd,"README.md");if(le.existsSync(E)||(await le.writeFilePromise(E,`# ${q.stringifyIdent(n.name)} `),h.push(E)),!s||s.cwd===this.context.cwd){let C=K.join(this.context.cwd,Er.lockfile);le.existsSync(C)||(await le.writeFilePromise(C,""),h.push(C));let P=[".yarn/*","!.yarn/patches","!.yarn/plugins","!.yarn/releases","!.yarn/sdks","!.yarn/versions","","# Whether you use PnP or not, the node_modules folder is often used to store","# build artifacts that should be gitignored","node_modules","","# Swap the comments on the following lines if you wish to use zero-installs","# In that case, don't forget to run `yarn config set enableGlobalCache false`!","# Documentation here: https://yarnpkg.com/features/caching#zero-installs","","#!.yarn/cache",".pnp.*"].map(Ae=>`${Ae} `).join(""),I=K.join(this.context.cwd,".gitignore");le.existsSync(I)||(await le.writeFilePromise(I,P),h.push(I));let N=["/.yarn/** linguist-vendored","/.yarn/releases/* binary","/.yarn/plugins/**/* binary","/.pnp.* binary linguist-generated"].map(Ae=>`${Ae} `).join(""),U=K.join(this.context.cwd,".gitattributes");le.existsSync(U)||(await le.writeFilePromise(U,N),h.push(U));let W={"*":{charset:"utf-8",endOfLine:"lf",indentSize:2,indentStyle:"space",insertFinalNewline:!0}};je.mergeIntoTarget(W,r.get("initEditorConfig"));let te=`root = true `;for(let[Ae,ce]of Object.entries(W)){te+=` [${Ae}] `;for(let[me,pe]of Object.entries(ce)){let Be=me.replace(/[A-Z]/g,Ce=>`_${Ce.toLowerCase()}`);te+=`${Be} = ${pe} `}}let ie=K.join(this.context.cwd,".editorconfig");le.existsSync(ie)||(await le.writeFilePromise(ie,te),h.push(ie)),await this.cli.run(["install"],{quiet:!0}),await this.initialize(),le.existsSync(K.join(this.context.cwd,".git"))||(await Gr.execvp("git",["init"],{cwd:this.context.cwd}),await Gr.execvp("git",["add","--",...h],{cwd:this.context.cwd}),await Gr.execvp("git",["commit","--allow-empty","-m","First commit"],{cwd:this.context.cwd}))}}};var nw=class extends J0{constructor(){super(...arguments);this.initializer=ge.String();this.argv=ge.Proxy()}static{this.paths=[["init"]]}async initialize(){this.context.stdout.write(` `),await this.cli.run(["dlx",this.initializer,...this.argv],{quiet:!0})}};var eDt={configuration:{initScope:{description:"Scope used when creating packages via the init command",type:"STRING",default:null},initFields:{description:"Additional fields to set when creating packages via the init command",type:"MAP",valueDefinition:{description:"",type:"ANY"}},initEditorConfig:{description:"Extra rules to define in the generator editorconfig",type:"MAP",valueDefinition:{description:"",type:"ANY"}}},commands:[J0,nw]},tDt=eDt;var IY={};Vt(IY,{SearchCommand:()=>Iw,UpgradeInteractiveCommand:()=>Cw,default:()=>yQt});Ve();var yve=et(Ie("os"));function iw({stdout:t}){if(yve.default.endianness()==="BE")throw new Error("Interactive commands cannot be used on big-endian systems because ink depends on yoga-layout-prebuilt which only supports little-endian architectures");if(!t.isTTY)throw new Error("Interactive commands can only be used inside a TTY environment")}Wt();var kSe=et(G9()),W9={appId:"OFCNCOG2CU",apiKey:"6fe4476ee5a1832882e326b506d14126",indexName:"npm-search"},Xbt=(0,kSe.default)(W9.appId,W9.apiKey).initIndex(W9.indexName),Y9=async(t,e=0)=>await Xbt.search(t,{analyticsTags:["yarn-plugin-interactive-tools"],attributesToRetrieve:["name","version","owner","repository","humanDownloadsLast30Days"],page:e,hitsPerPage:10});var QD=["regular","dev","peer"],Iw=class extends ut{static{this.paths=[["search"]]}static{this.usage=ot.Usage({category:"Interactive commands",description:"open the search interface",details:` This command opens a fullscreen terminal interface where you can search for and install packages from the npm registry. `,examples:[["Open the search window","yarn search"]]})}async execute(){iw(this.context);let{Gem:e}=await Promise.resolve().then(()=>(YF(),cY)),{ScrollableItems:r}=await Promise.resolve().then(()=>(zF(),JF)),{useKeypress:s}=await Promise.resolve().then(()=>(PD(),DPe)),{useMinistore:a}=await Promise.resolve().then(()=>(gY(),hY)),{renderForm:n}=await Promise.resolve().then(()=>(eN(),$F)),{default:c}=await Promise.resolve().then(()=>et(OPe())),{Box:f,Text:p}=await Promise.resolve().then(()=>et(Vc())),{default:h,useEffect:E,useState:C}=await Promise.resolve().then(()=>et(hn())),S=await ze.find(this.context.cwd,this.context.plugins),P=()=>h.createElement(f,{flexDirection:"row"},h.createElement(f,{flexDirection:"column",width:48},h.createElement(f,null,h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},""),"/",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to move between packages.")),h.createElement(f,null,h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to select a package.")),h.createElement(f,null,h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," again to change the target."))),h.createElement(f,{flexDirection:"column"},h.createElement(f,{marginLeft:1},h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to install the selected packages.")),h.createElement(f,{marginLeft:1},h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to abort.")))),I=()=>h.createElement(h.Fragment,null,h.createElement(f,{width:15},h.createElement(p,{bold:!0,underline:!0,color:"gray"},"Owner")),h.createElement(f,{width:11},h.createElement(p,{bold:!0,underline:!0,color:"gray"},"Version")),h.createElement(f,{width:10},h.createElement(p,{bold:!0,underline:!0,color:"gray"},"Downloads"))),R=()=>h.createElement(f,{width:17},h.createElement(p,{bold:!0,underline:!0,color:"gray"},"Target")),N=({hit:pe,active:Be})=>{let[Ce,g]=a(pe.name,null);s({active:Be},(fe,se)=>{if(se.name!=="space")return;if(!Ce){g(QD[0]);return}let X=QD.indexOf(Ce)+1;X===QD.length?g(null):g(QD[X])},[Ce,g]);let we=q.parseIdent(pe.name),ye=q.prettyIdent(S,we);return h.createElement(f,null,h.createElement(f,{width:45},h.createElement(p,{bold:!0,wrap:"wrap"},ye)),h.createElement(f,{width:14,marginLeft:1},h.createElement(p,{bold:!0,wrap:"truncate"},pe.owner.name)),h.createElement(f,{width:10,marginLeft:1},h.createElement(p,{italic:!0,wrap:"truncate"},pe.version)),h.createElement(f,{width:16,marginLeft:1},h.createElement(p,null,pe.humanDownloadsLast30Days)))},U=({name:pe,active:Be})=>{let[Ce]=a(pe,null),g=q.parseIdent(pe);return h.createElement(f,null,h.createElement(f,{width:47},h.createElement(p,{bold:!0}," - ",q.prettyIdent(S,g))),QD.map(we=>h.createElement(f,{key:we,width:14,marginLeft:1},h.createElement(p,null," ",h.createElement(e,{active:Ce===we})," ",h.createElement(p,{bold:!0},we)))))},W=()=>h.createElement(f,{marginTop:1},h.createElement(p,null,"Powered by Algolia.")),ie=await n(({useSubmit:pe})=>{let Be=a();pe(Be);let Ce=Array.from(Be.keys()).filter(j=>Be.get(j)!==null),[g,we]=C(""),[ye,fe]=C(0),[se,X]=C([]),De=j=>{j.match(/\t| /)||we(j)},Re=async()=>{fe(0);let j=await Y9(g);j.query===g&&X(j.hits)},dt=async()=>{let j=await Y9(g,ye+1);j.query===g&&j.page-1===ye&&(fe(j.page),X([...se,...j.hits]))};return E(()=>{g?Re():X([])},[g]),h.createElement(f,{flexDirection:"column"},h.createElement(P,null),h.createElement(f,{flexDirection:"row",marginTop:1},h.createElement(p,{bold:!0},"Search: "),h.createElement(f,{width:41},h.createElement(c,{value:g,onChange:De,placeholder:"i.e. babel, webpack, react...",showCursor:!1})),h.createElement(I,null)),se.length?h.createElement(r,{radius:2,loop:!1,children:se.map(j=>h.createElement(N,{key:j.name,hit:j,active:!1})),willReachEnd:dt}):h.createElement(p,{color:"gray"},"Start typing..."),h.createElement(f,{flexDirection:"row",marginTop:1},h.createElement(f,{width:49},h.createElement(p,{bold:!0},"Selected:")),h.createElement(R,null)),Ce.length?Ce.map(j=>h.createElement(U,{key:j,name:j,active:!1})):h.createElement(p,{color:"gray"},"No selected packages..."),h.createElement(W,null))},{},{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});if(typeof ie>"u")return 1;let Ae=Array.from(ie.keys()).filter(pe=>ie.get(pe)==="regular"),ce=Array.from(ie.keys()).filter(pe=>ie.get(pe)==="dev"),me=Array.from(ie.keys()).filter(pe=>ie.get(pe)==="peer");return Ae.length&&await this.cli.run(["add",...Ae]),ce.length&&await this.cli.run(["add","--dev",...ce]),me&&await this.cli.run(["add","--peer",...me]),0}};Ve();Wt();fG();var qPe=et(Ai()),jPe=/^((?:[\^~]|>=?)?)([0-9]+)(\.[0-9]+)(\.[0-9]+)((?:-\S+)?)$/;function GPe(t,e){return t.length>0?[t.slice(0,e)].concat(GPe(t.slice(e),e)):[]}var Cw=class extends ut{static{this.paths=[["upgrade-interactive"]]}static{this.usage=ot.Usage({category:"Interactive commands",description:"open the upgrade interface",details:` This command opens a fullscreen terminal interface where you can see any out of date packages used by your application, their status compared to the latest versions available on the remote registry, and select packages to upgrade. `,examples:[["Open the upgrade window","yarn upgrade-interactive"]]})}async execute(){iw(this.context);let{ItemOptions:e}=await Promise.resolve().then(()=>(HPe(),UPe)),{Pad:r}=await Promise.resolve().then(()=>(EY(),_Pe)),{ScrollableItems:s}=await Promise.resolve().then(()=>(zF(),JF)),{useMinistore:a}=await Promise.resolve().then(()=>(gY(),hY)),{renderForm:n}=await Promise.resolve().then(()=>(eN(),$F)),{Box:c,Text:f}=await Promise.resolve().then(()=>et(Vc())),{default:p,useEffect:h,useRef:E,useState:C}=await Promise.resolve().then(()=>et(hn())),S=await ze.find(this.context.cwd,this.context.plugins),{project:P,workspace:I}=await Tt.find(S,this.context.cwd),R=await Jr.find(S);if(!I)throw new ar(P.cwd,this.context.cwd);await P.restoreInstallState({restoreResolutions:!1});let N=this.context.stdout.rows-7,U=(we,ye)=>{let fe=cCe(we,ye),se="";for(let X of fe)X.added?se+=he.pretty(S,X.value,"green"):X.removed||(se+=X.value);return se},W=(we,ye)=>{if(we===ye)return ye;let fe=q.parseRange(we),se=q.parseRange(ye),X=fe.selector.match(jPe),De=se.selector.match(jPe);if(!X||!De)return U(we,ye);let Re=["gray","red","yellow","green","magenta"],dt=null,j="";for(let rt=1;rt{let se=await Xu.fetchDescriptorFrom(we,fe,{project:P,cache:R,preserveModifier:ye,workspace:I});return se!==null?se.range:we.range},ie=async we=>{let ye=qPe.default.valid(we.range)?`^${we.range}`:we.range,[fe,se]=await Promise.all([te(we,we.range,ye).catch(()=>null),te(we,we.range,"latest").catch(()=>null)]),X=[{value:null,label:we.range}];return fe&&fe!==we.range?X.push({value:fe,label:W(we.range,fe)}):X.push({value:null,label:""}),se&&se!==fe&&se!==we.range?X.push({value:se,label:W(we.range,se)}):X.push({value:null,label:""}),X},Ae=()=>p.createElement(c,{flexDirection:"row"},p.createElement(c,{flexDirection:"column",width:49},p.createElement(c,{marginLeft:1},p.createElement(f,null,"Press ",p.createElement(f,{bold:!0,color:"cyanBright"},""),"/",p.createElement(f,{bold:!0,color:"cyanBright"},"")," to select packages.")),p.createElement(c,{marginLeft:1},p.createElement(f,null,"Press ",p.createElement(f,{bold:!0,color:"cyanBright"},""),"/",p.createElement(f,{bold:!0,color:"cyanBright"},"")," to select versions."))),p.createElement(c,{flexDirection:"column"},p.createElement(c,{marginLeft:1},p.createElement(f,null,"Press ",p.createElement(f,{bold:!0,color:"cyanBright"},"")," to install.")),p.createElement(c,{marginLeft:1},p.createElement(f,null,"Press ",p.createElement(f,{bold:!0,color:"cyanBright"},"")," to abort.")))),ce=()=>p.createElement(c,{flexDirection:"row",paddingTop:1,paddingBottom:1},p.createElement(c,{width:50},p.createElement(f,{bold:!0},p.createElement(f,{color:"greenBright"},"?")," Pick the packages you want to upgrade.")),p.createElement(c,{width:17},p.createElement(f,{bold:!0,underline:!0,color:"gray"},"Current")),p.createElement(c,{width:17},p.createElement(f,{bold:!0,underline:!0,color:"gray"},"Range")),p.createElement(c,{width:17},p.createElement(f,{bold:!0,underline:!0,color:"gray"},"Latest"))),me=({active:we,descriptor:ye,suggestions:fe})=>{let[se,X]=a(ye.descriptorHash,null),De=q.stringifyIdent(ye),Re=Math.max(0,45-De.length);return p.createElement(p.Fragment,null,p.createElement(c,null,p.createElement(c,{width:45},p.createElement(f,{bold:!0},q.prettyIdent(S,ye)),p.createElement(r,{active:we,length:Re})),p.createElement(e,{active:we,options:fe,value:se,skewer:!0,onChange:X,sizes:[17,17,17]})))},pe=({dependencies:we})=>{let[ye,fe]=C(we.map(()=>null)),se=E(!0),X=async De=>{let Re=await ie(De);return Re.filter(dt=>dt.label!=="").length<=1?null:{descriptor:De,suggestions:Re}};return h(()=>()=>{se.current=!1},[]),h(()=>{let De=Math.trunc(N*1.75),Re=we.slice(0,De),dt=we.slice(De),j=GPe(dt,N),rt=Re.map(X).reduce(async(Fe,Ne)=>{await Fe;let Pe=await Ne;Pe!==null&&se.current&&fe(Ye=>{let ke=Ye.findIndex(_e=>_e===null),it=[...Ye];return it[ke]=Pe,it})},Promise.resolve());j.reduce((Fe,Ne)=>Promise.all(Ne.map(Pe=>Promise.resolve().then(()=>X(Pe)))).then(async Pe=>{Pe=Pe.filter(Ye=>Ye!==null),await Fe,se.current&&fe(Ye=>{let ke=Ye.findIndex(it=>it===null);return Ye.slice(0,ke).concat(Pe).concat(Ye.slice(ke+Pe.length))})}),rt).then(()=>{se.current&&fe(Fe=>Fe.filter(Ne=>Ne!==null))})},[]),ye.length?p.createElement(s,{radius:N>>1,children:ye.map((De,Re)=>De!==null?p.createElement(me,{key:Re,active:!1,descriptor:De.descriptor,suggestions:De.suggestions}):p.createElement(f,{key:Re},"Loading..."))}):p.createElement(f,null,"No upgrades found")},Ce=await n(({useSubmit:we})=>{we(a());let ye=new Map;for(let se of P.workspaces)for(let X of["dependencies","devDependencies"])for(let De of se.manifest[X].values())P.tryWorkspaceByDescriptor(De)===null&&(De.range.startsWith("link:")||ye.set(De.descriptorHash,De));let fe=je.sortMap(ye.values(),se=>q.stringifyDescriptor(se));return p.createElement(c,{flexDirection:"column"},p.createElement(Ae,null),p.createElement(ce,null),p.createElement(pe,{dependencies:fe}))},{},{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});if(typeof Ce>"u")return 1;let g=!1;for(let we of P.workspaces)for(let ye of["dependencies","devDependencies"]){let fe=we.manifest[ye];for(let se of fe.values()){let X=Ce.get(se.descriptorHash);typeof X<"u"&&X!==null&&(fe.set(se.identHash,q.makeDescriptor(se,X)),g=!0)}}return g?await P.installWithNewReport({quiet:this.context.quiet,stdout:this.context.stdout},{cache:R}):0}};var mQt={commands:[Iw,Cw]},yQt=mQt;var wY={};Vt(wY,{default:()=>wQt});Ve();var RD="jsr:";Ve();Ve();function ww(t){let e=t.range.slice(4);if(Or.validRange(e))return q.makeDescriptor(t,`npm:${q.stringifyIdent(q.wrapIdentIntoScope(t,"jsr"))}@${e}`);let r=q.tryParseDescriptor(e,!0);if(r!==null)return q.makeDescriptor(t,`npm:${q.stringifyIdent(q.wrapIdentIntoScope(r,"jsr"))}@${r.range}`);throw new Error(`Invalid range: ${t.range}`)}function Bw(t){return q.makeLocator(q.wrapIdentIntoScope(t,"jsr"),`npm:${t.reference.slice(4)}`)}function CY(t){return q.makeLocator(q.unwrapIdentFromScope(t,"jsr"),`jsr:${t.reference.slice(4)}`)}var tN=class{supports(e,r){return e.reference.startsWith(RD)}getLocalPath(e,r){let s=Bw(e);return r.fetcher.getLocalPath(s,r)}fetch(e,r){let s=Bw(e);return r.fetcher.fetch(s,r)}};var rN=class{supportsDescriptor(e,r){return!!e.range.startsWith(RD)}supportsLocator(e,r){return!!e.reference.startsWith(RD)}shouldPersistResolution(e,r){let s=Bw(e);return r.resolver.shouldPersistResolution(s,r)}bindDescriptor(e,r,s){return e}getResolutionDependencies(e,r){return{inner:ww(e)}}async getCandidates(e,r,s){let a=s.project.configuration.normalizeDependency(ww(e));return(await s.resolver.getCandidates(a,r,s)).map(c=>CY(c))}async getSatisfying(e,r,s,a){let n=a.project.configuration.normalizeDependency(ww(e));return a.resolver.getSatisfying(n,r,s,a)}async resolve(e,r){let s=Bw(e),a=await r.resolver.resolve(s,r);return{...a,...CY(a)}}};var EQt=["dependencies","devDependencies","peerDependencies"];function IQt(t,e){for(let r of EQt)for(let s of t.manifest.getForScope(r).values()){if(!s.range.startsWith("jsr:"))continue;let a=ww(s),n=r==="dependencies"?q.makeDescriptor(s,"unknown"):null,c=n!==null&&t.manifest.ensureDependencyMeta(n).optional?"optionalDependencies":r;e[c][q.stringifyIdent(s)]=a.range}}var CQt={hooks:{beforeWorkspacePacking:IQt},resolvers:[rN],fetchers:[tN]},wQt=CQt;var BY={};Vt(BY,{LinkFetcher:()=>FD,LinkResolver:()=>ND,PortalFetcher:()=>OD,PortalResolver:()=>LD,default:()=>vQt});Ve();bt();var ih="portal:",sh="link:";var FD=class{supports(e,r){return!!e.reference.startsWith(sh)}getLocalPath(e,r){let{parentLocator:s,path:a}=q.parseFileStyleRange(e.reference,{protocol:sh});if(K.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:K.resolve(n,a)}async fetch(e,r){let{parentLocator:s,path:a}=q.parseFileStyleRange(e.reference,{protocol:sh}),n=K.isAbsolute(a)?{packageFs:new Sn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(s,r),c=n.localPath?{packageFs:new Sn(vt.root),prefixPath:K.relative(vt.root,n.localPath),localPath:vt.root}:n;n!==c&&n.releaseFs&&n.releaseFs();let f=c.packageFs,p=K.resolve(c.localPath??c.packageFs.getRealPath(),c.prefixPath,a);return n.localPath?{packageFs:new Sn(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot,discardFromLookup:!0,localPath:p}:{packageFs:new jf(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot,discardFromLookup:!0}}};Ve();bt();var ND=class{supportsDescriptor(e,r){return!!e.range.startsWith(sh)}supportsLocator(e,r){return!!e.reference.startsWith(sh)}shouldPersistResolution(e,r){return!1}bindDescriptor(e,r,s){return q.bindDescriptor(e,{locator:q.stringifyLocator(r)})}getResolutionDependencies(e,r){return{}}async getCandidates(e,r,s){let a=e.range.slice(sh.length);return[q.makeLocator(e,`${sh}${ue.toPortablePath(a)}`)]}async getSatisfying(e,r,s,a){let[n]=await this.getCandidates(e,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(e,r){return{...e,version:"0.0.0",languageName:r.project.configuration.get("defaultLanguageName"),linkType:"SOFT",conditions:null,dependencies:new Map,peerDependencies:new Map,dependenciesMeta:new Map,peerDependenciesMeta:new Map,bin:new Map}}};Ve();bt();var OD=class{supports(e,r){return!!e.reference.startsWith(ih)}getLocalPath(e,r){let{parentLocator:s,path:a}=q.parseFileStyleRange(e.reference,{protocol:ih});if(K.isAbsolute(a))return a;let n=r.fetcher.getLocalPath(s,r);return n===null?null:K.resolve(n,a)}async fetch(e,r){let{parentLocator:s,path:a}=q.parseFileStyleRange(e.reference,{protocol:ih}),n=K.isAbsolute(a)?{packageFs:new Sn(vt.root),prefixPath:vt.dot,localPath:vt.root}:await r.fetcher.fetch(s,r),c=n.localPath?{packageFs:new Sn(vt.root),prefixPath:K.relative(vt.root,n.localPath),localPath:vt.root}:n;n!==c&&n.releaseFs&&n.releaseFs();let f=c.packageFs,p=K.resolve(c.localPath??c.packageFs.getRealPath(),c.prefixPath,a);return n.localPath?{packageFs:new Sn(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot,localPath:p}:{packageFs:new jf(p,{baseFs:f}),releaseFs:c.releaseFs,prefixPath:vt.dot}}};Ve();Ve();bt();var LD=class{supportsDescriptor(e,r){return!!e.range.startsWith(ih)}supportsLocator(e,r){return!!e.reference.startsWith(ih)}shouldPersistResolution(e,r){return!1}bindDescriptor(e,r,s){return q.bindDescriptor(e,{locator:q.stringifyLocator(r)})}getResolutionDependencies(e,r){return{}}async getCandidates(e,r,s){let a=e.range.slice(ih.length);return[q.makeLocator(e,`${ih}${ue.toPortablePath(a)}`)]}async getSatisfying(e,r,s,a){let[n]=await this.getCandidates(e,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(e,r){if(!r.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let s=await r.fetchOptions.fetcher.fetch(e,r.fetchOptions),a=await je.releaseAfterUseAsync(async()=>await Ht.find(s.prefixPath,{baseFs:s.packageFs}),s.releaseFs);return{...e,version:a.version||"0.0.0",languageName:a.languageName||r.project.configuration.get("defaultLanguageName"),linkType:"SOFT",conditions:a.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(a.dependencies),peerDependencies:a.peerDependencies,dependenciesMeta:a.dependenciesMeta,peerDependenciesMeta:a.peerDependenciesMeta,bin:a.bin}}};var BQt={fetchers:[FD,OD],resolvers:[ND,LD]},vQt=BQt;var oV={};Vt(oV,{NodeModulesLinker:()=>XD,NodeModulesMode:()=>rV,PnpLooseLinker:()=>$D,default:()=>HTt});bt();Ve();bt();bt();var SY=(t,e)=>`${t}@${e}`,WPe=(t,e)=>{let r=e.indexOf("#"),s=r>=0?e.substring(r+1):e;return SY(t,s)};var VPe=(t,e={})=>{let r=e.debugLevel||Number(process.env.NM_DEBUG_LEVEL||-1),s=e.check||r>=9,a=e.hoistingLimits||new Map,n={check:s,debugLevel:r,hoistingLimits:a,fastLookupPossible:!0},c;n.debugLevel>=0&&(c=Date.now());let f=QQt(t,n),p=!1,h=0;do{let E=DY(f,[f],new Set([f.locator]),new Map,n);p=E.anotherRoundNeeded||E.isGraphChanged,n.fastLookupPossible=!1,h++}while(p);if(n.debugLevel>=0&&console.log(`hoist time: ${Date.now()-c}ms, rounds: ${h}`),n.debugLevel>=1){let E=MD(f);if(DY(f,[f],new Set([f.locator]),new Map,n).isGraphChanged)throw new Error(`The hoisting result is not terminal, prev tree: ${E}, next tree: ${MD(f)}`);let S=KPe(f);if(S)throw new Error(`${S}, after hoisting finished: ${MD(f)}`)}return n.debugLevel>=2&&console.log(MD(f)),TQt(f)},SQt=t=>{let e=t[t.length-1],r=new Map,s=new Set,a=n=>{if(!s.has(n)){s.add(n);for(let c of n.hoistedDependencies.values())r.set(c.name,c);for(let c of n.dependencies.values())n.peerNames.has(c.name)||a(c)}};return a(e),r},DQt=t=>{let e=t[t.length-1],r=new Map,s=new Set,a=new Set,n=(c,f)=>{if(s.has(c))return;s.add(c);for(let h of c.hoistedDependencies.values())if(!f.has(h.name)){let E;for(let C of t)E=C.dependencies.get(h.name),E&&r.set(E.name,E)}let p=new Set;for(let h of c.dependencies.values())p.add(h.name);for(let h of c.dependencies.values())c.peerNames.has(h.name)||n(h,p)};return n(e,a),r},YPe=(t,e)=>{if(e.decoupled)return e;let{name:r,references:s,ident:a,locator:n,dependencies:c,originalDependencies:f,hoistedDependencies:p,peerNames:h,reasons:E,isHoistBorder:C,hoistPriority:S,dependencyKind:P,hoistedFrom:I,hoistedTo:R}=e,N={name:r,references:new Set(s),ident:a,locator:n,dependencies:new Map(c),originalDependencies:new Map(f),hoistedDependencies:new Map(p),peerNames:new Set(h),reasons:new Map(E),decoupled:!0,isHoistBorder:C,hoistPriority:S,dependencyKind:P,hoistedFrom:new Map(I),hoistedTo:new Map(R)},U=N.dependencies.get(r);return U&&U.ident==N.ident&&N.dependencies.set(r,N),t.dependencies.set(N.name,N),N},bQt=(t,e)=>{let r=new Map([[t.name,[t.ident]]]);for(let a of t.dependencies.values())t.peerNames.has(a.name)||r.set(a.name,[a.ident]);let s=Array.from(e.keys());s.sort((a,n)=>{let c=e.get(a),f=e.get(n);if(f.hoistPriority!==c.hoistPriority)return f.hoistPriority-c.hoistPriority;{let p=c.dependents.size+c.peerDependents.size;return f.dependents.size+f.peerDependents.size-p}});for(let a of s){let n=a.substring(0,a.indexOf("@",1)),c=a.substring(n.length+1);if(!t.peerNames.has(n)){let f=r.get(n);f||(f=[],r.set(n,f)),f.indexOf(c)<0&&f.push(c)}}return r},vY=t=>{let e=new Set,r=(s,a=new Set)=>{if(!a.has(s)){a.add(s);for(let n of s.peerNames)if(!t.peerNames.has(n)){let c=t.dependencies.get(n);c&&!e.has(c)&&r(c,a)}e.add(s)}};for(let s of t.dependencies.values())t.peerNames.has(s.name)||r(s);return e},DY=(t,e,r,s,a,n=new Set)=>{let c=e[e.length-1];if(n.has(c))return{anotherRoundNeeded:!1,isGraphChanged:!1};n.add(c);let f=RQt(c),p=bQt(c,f),h=t==c?new Map:a.fastLookupPossible?SQt(e):DQt(e),E,C=!1,S=!1,P=new Map(Array.from(p.entries()).map(([R,N])=>[R,N[0]])),I=new Map;do{let R=kQt(t,e,r,h,P,p,s,I,a);R.isGraphChanged&&(S=!0),R.anotherRoundNeeded&&(C=!0),E=!1;for(let[N,U]of p)U.length>1&&!c.dependencies.has(N)&&(P.delete(N),U.shift(),P.set(N,U[0]),E=!0)}while(E);for(let R of c.dependencies.values())if(!c.peerNames.has(R.name)&&!r.has(R.locator)){r.add(R.locator);let N=DY(t,[...e,R],r,I,a);N.isGraphChanged&&(S=!0),N.anotherRoundNeeded&&(C=!0),r.delete(R.locator)}return{anotherRoundNeeded:C,isGraphChanged:S}},PQt=t=>{for(let[e,r]of t.dependencies)if(!t.peerNames.has(e)&&r.ident!==t.ident)return!0;return!1},xQt=(t,e,r,s,a,n,c,f,{outputReason:p,fastLookupPossible:h})=>{let E,C=null,S=new Set;p&&(E=`${Array.from(e).map(N=>Io(N)).join("\u2192")}`);let P=r[r.length-1],R=!(s.ident===P.ident);if(p&&!R&&(C="- self-reference"),R&&(R=s.dependencyKind!==1,p&&!R&&(C="- workspace")),R&&s.dependencyKind===2&&(R=!PQt(s),p&&!R&&(C="- external soft link with unhoisted dependencies")),R&&(R=!t.peerNames.has(s.name),p&&!R&&(C=`- cannot shadow peer: ${Io(t.originalDependencies.get(s.name).locator)} at ${E}`)),R){let N=!1,U=a.get(s.name);if(N=!U||U.ident===s.ident,p&&!N&&(C=`- filled by: ${Io(U.locator)} at ${E}`),N)for(let W=r.length-1;W>=1;W--){let ie=r[W].dependencies.get(s.name);if(ie&&ie.ident!==s.ident){N=!1;let Ae=f.get(P);Ae||(Ae=new Set,f.set(P,Ae)),Ae.add(s.name),p&&(C=`- filled by ${Io(ie.locator)} at ${r.slice(0,W).map(ce=>Io(ce.locator)).join("\u2192")}`);break}}R=N}if(R&&(R=n.get(s.name)===s.ident,p&&!R&&(C=`- filled by: ${Io(c.get(s.name)[0])} at ${E}`)),R){let N=!0,U=new Set(s.peerNames);for(let W=r.length-1;W>=1;W--){let te=r[W];for(let ie of U){if(te.peerNames.has(ie)&&te.originalDependencies.has(ie))continue;let Ae=te.dependencies.get(ie);Ae&&t.dependencies.get(ie)!==Ae&&(W===r.length-1?S.add(Ae):(S=null,N=!1,p&&(C=`- peer dependency ${Io(Ae.locator)} from parent ${Io(te.locator)} was not hoisted to ${E}`))),U.delete(ie)}if(!N)break}R=N}if(R&&!h)for(let N of s.hoistedDependencies.values()){let U=a.get(N.name)||t.dependencies.get(N.name);if(!U||N.ident!==U.ident){R=!1,p&&(C=`- previously hoisted dependency mismatch, needed: ${Io(N.locator)}, available: ${Io(U?.locator)}`);break}}return S!==null&&S.size>0?{isHoistable:2,dependsOn:S,reason:C}:{isHoistable:R?0:1,reason:C}},nN=t=>`${t.name}@${t.locator}`,kQt=(t,e,r,s,a,n,c,f,p)=>{let h=e[e.length-1],E=new Set,C=!1,S=!1,P=(U,W,te,ie,Ae)=>{if(E.has(ie))return;let ce=[...W,nN(ie)],me=[...te,nN(ie)],pe=new Map,Be=new Map;for(let fe of vY(ie)){let se=xQt(h,r,[h,...U,ie],fe,s,a,n,f,{outputReason:p.debugLevel>=2,fastLookupPossible:p.fastLookupPossible});if(Be.set(fe,se),se.isHoistable===2)for(let X of se.dependsOn){let De=pe.get(X.name)||new Set;De.add(fe.name),pe.set(X.name,De)}}let Ce=new Set,g=(fe,se,X)=>{if(!Ce.has(fe)){Ce.add(fe),Be.set(fe,{isHoistable:1,reason:X});for(let De of pe.get(fe.name)||[])g(ie.dependencies.get(De),se,p.debugLevel>=2?`- peer dependency ${Io(fe.locator)} from parent ${Io(ie.locator)} was not hoisted`:"")}};for(let[fe,se]of Be)se.isHoistable===1&&g(fe,se,se.reason);let we=!1;for(let fe of Be.keys())if(!Ce.has(fe)){S=!0;let se=c.get(ie);se&&se.has(fe.name)&&(C=!0),we=!0,ie.dependencies.delete(fe.name),ie.hoistedDependencies.set(fe.name,fe),ie.reasons.delete(fe.name);let X=h.dependencies.get(fe.name);if(p.debugLevel>=2){let De=Array.from(W).concat([ie.locator]).map(dt=>Io(dt)).join("\u2192"),Re=h.hoistedFrom.get(fe.name);Re||(Re=[],h.hoistedFrom.set(fe.name,Re)),Re.push(De),ie.hoistedTo.set(fe.name,Array.from(e).map(dt=>Io(dt.locator)).join("\u2192"))}if(!X)h.ident!==fe.ident&&(h.dependencies.set(fe.name,fe),Ae.add(fe));else for(let De of fe.references)X.references.add(De)}if(ie.dependencyKind===2&&we&&(C=!0),p.check){let fe=KPe(t);if(fe)throw new Error(`${fe}, after hoisting dependencies of ${[h,...U,ie].map(se=>Io(se.locator)).join("\u2192")}: ${MD(t)}`)}let ye=vY(ie);for(let fe of ye)if(Ce.has(fe)){let se=Be.get(fe);if((a.get(fe.name)===fe.ident||!ie.reasons.has(fe.name))&&se.isHoistable!==0&&ie.reasons.set(fe.name,se.reason),!fe.isHoistBorder&&me.indexOf(nN(fe))<0){E.add(ie);let De=YPe(ie,fe);P([...U,ie],ce,me,De,R),E.delete(ie)}}},I,R=new Set(vY(h)),N=Array.from(e).map(U=>nN(U));do{I=R,R=new Set;for(let U of I){if(U.locator===h.locator||U.isHoistBorder)continue;let W=YPe(h,U);P([],Array.from(r),N,W,R)}}while(R.size>0);return{anotherRoundNeeded:C,isGraphChanged:S}},KPe=t=>{let e=[],r=new Set,s=new Set,a=(n,c,f)=>{if(r.has(n)||(r.add(n),s.has(n)))return;let p=new Map(c);for(let h of n.dependencies.values())n.peerNames.has(h.name)||p.set(h.name,h);for(let h of n.originalDependencies.values()){let E=p.get(h.name),C=()=>`${Array.from(s).concat([n]).map(S=>Io(S.locator)).join("\u2192")}`;if(n.peerNames.has(h.name)){let S=c.get(h.name);(S!==E||!S||S.ident!==h.ident)&&e.push(`${C()} - broken peer promise: expected ${h.ident} but found ${S&&S.ident}`)}else{let S=f.hoistedFrom.get(n.name),P=n.hoistedTo.get(h.name),I=`${S?` hoisted from ${S.join(", ")}`:""}`,R=`${P?` hoisted to ${P}`:""}`,N=`${C()}${I}`;E?E.ident!==h.ident&&e.push(`${N} - broken require promise for ${h.name}${R}: expected ${h.ident}, but found: ${E.ident}`):e.push(`${N} - broken require promise: no required dependency ${h.name}${R} found`)}}s.add(n);for(let h of n.dependencies.values())n.peerNames.has(h.name)||a(h,p,n);s.delete(n)};return a(t,t.dependencies,t),e.join(` `)},QQt=(t,e)=>{let{identName:r,name:s,reference:a,peerNames:n}=t,c={name:s,references:new Set([a]),locator:SY(r,a),ident:WPe(r,a),dependencies:new Map,originalDependencies:new Map,hoistedDependencies:new Map,peerNames:new Set(n),reasons:new Map,decoupled:!0,isHoistBorder:!0,hoistPriority:0,dependencyKind:1,hoistedFrom:new Map,hoistedTo:new Map},f=new Map([[t,c]]),p=(h,E)=>{let C=f.get(h),S=!!C;if(!C){let{name:P,identName:I,reference:R,peerNames:N,hoistPriority:U,dependencyKind:W}=h,te=e.hoistingLimits.get(E.locator);C={name:P,references:new Set([R]),locator:SY(I,R),ident:WPe(I,R),dependencies:new Map,originalDependencies:new Map,hoistedDependencies:new Map,peerNames:new Set(N),reasons:new Map,decoupled:!0,isHoistBorder:te?te.has(P):!1,hoistPriority:U||0,dependencyKind:W||0,hoistedFrom:new Map,hoistedTo:new Map},f.set(h,C)}if(E.dependencies.set(h.name,C),E.originalDependencies.set(h.name,C),S){let P=new Set,I=R=>{if(!P.has(R)){P.add(R),R.decoupled=!1;for(let N of R.dependencies.values())R.peerNames.has(N.name)||I(N)}};I(C)}else for(let P of h.dependencies)p(P,C)};for(let h of t.dependencies)p(h,c);return c},bY=t=>t.substring(0,t.indexOf("@",1)),TQt=t=>{let e={name:t.name,identName:bY(t.locator),references:new Set(t.references),dependencies:new Set},r=new Set([t]),s=(a,n,c)=>{let f=r.has(a),p;if(n===a)p=c;else{let{name:h,references:E,locator:C}=a;p={name:h,identName:bY(C),references:E,dependencies:new Set}}if(c.dependencies.add(p),!f){r.add(a);for(let h of a.dependencies.values())a.peerNames.has(h.name)||s(h,a,p);r.delete(a)}};for(let a of t.dependencies.values())s(a,t,e);return e},RQt=t=>{let e=new Map,r=new Set([t]),s=c=>`${c.name}@${c.ident}`,a=c=>{let f=s(c),p=e.get(f);return p||(p={dependents:new Set,peerDependents:new Set,hoistPriority:0},e.set(f,p)),p},n=(c,f)=>{let p=!!r.has(f);if(a(f).dependents.add(c.ident),!p){r.add(f);for(let E of f.dependencies.values()){let C=a(E);C.hoistPriority=Math.max(C.hoistPriority,E.hoistPriority),f.peerNames.has(E.name)?C.peerDependents.add(f.ident):n(f,E)}}};for(let c of t.dependencies.values())t.peerNames.has(c.name)||n(t,c);return e},Io=t=>{if(!t)return"none";let e=t.indexOf("@",1),r=t.substring(0,e);r.endsWith("$wsroot$")&&(r=`wh:${r.replace("$wsroot$","")}`);let s=t.substring(e+1);if(s==="workspace:.")return".";if(s){let a=(s.indexOf("#")>0?s.split("#")[1]:s).replace("npm:","");return s.startsWith("virtual")&&(r=`v:${r}`),a.startsWith("workspace")&&(r=`w:${r}`,a=""),`${r}${a?`@${a}`:""}`}else return`${r}`};var MD=t=>{let e=0,r=(a,n,c="")=>{if(e>5e4||n.has(a))return"";e++;let f=Array.from(a.dependencies.values()).sort((h,E)=>h.name===E.name?0:h.name>E.name?1:-1),p="";n.add(a);for(let h=0;h":"")+(S!==E.name?`a:${E.name}:`:"")+Io(E.locator)+(C?` ${C}`:"")} `,p+=r(E,n,`${c}${h5e4?` Tree is too large, part of the tree has been dunped `:"")};var _D=(s=>(s.WORKSPACES="workspaces",s.DEPENDENCIES="dependencies",s.NONE="none",s))(_D||{}),JPe="node_modules",tg="$wsroot$";var UD=(t,e)=>{let{packageTree:r,hoistingLimits:s,errors:a,preserveSymlinksRequired:n}=NQt(t,e),c=null;if(a.length===0){let f=VPe(r,{hoistingLimits:s});c=LQt(t,f,e)}return{tree:c,errors:a,preserveSymlinksRequired:n}},gA=t=>`${t.name}@${t.reference}`,xY=t=>{let e=new Map;for(let[r,s]of t.entries())if(!s.dirList){let a=e.get(s.locator);a||(a={target:s.target,linkType:s.linkType,locations:[],aliases:s.aliases},e.set(s.locator,a)),a.locations.push(r)}for(let r of e.values())r.locations=r.locations.sort((s,a)=>{let n=s.split(K.delimiter).length,c=a.split(K.delimiter).length;return a===s?0:n!==c?c-n:a>s?1:-1});return e},zPe=(t,e)=>{let r=q.isVirtualLocator(t)?q.devirtualizeLocator(t):t,s=q.isVirtualLocator(e)?q.devirtualizeLocator(e):e;return q.areLocatorsEqual(r,s)},PY=(t,e,r,s)=>{if(t.linkType!=="SOFT")return!1;let a=ue.toPortablePath(r.resolveVirtual&&e.reference&&e.reference.startsWith("virtual:")?r.resolveVirtual(t.packageLocation):t.packageLocation);return K.contains(s,a)===null},FQt=t=>{let e=t.getPackageInformation(t.topLevel);if(e===null)throw new Error("Assertion failed: Expected the top-level package to have been registered");if(t.findPackageLocator(e.packageLocation)===null)throw new Error("Assertion failed: Expected the top-level package to have a physical locator");let s=ue.toPortablePath(e.packageLocation.slice(0,-1)),a=new Map,n={children:new Map},c=t.getDependencyTreeRoots(),f=new Map,p=new Set,h=(S,P)=>{let I=gA(S);if(p.has(I))return;p.add(I);let R=t.getPackageInformation(S);if(R){let N=P?gA(P):"";if(gA(S)!==N&&R.linkType==="SOFT"&&!S.reference.startsWith("link:")&&!PY(R,S,t,s)){let U=ZPe(R,S,t);(!f.get(U)||S.reference.startsWith("workspace:"))&&f.set(U,S)}for(let[U,W]of R.packageDependencies)W!==null&&(R.packagePeers.has(U)||h(t.getLocator(U,W),S))}};for(let S of c)h(S,null);let E=s.split(K.sep);for(let S of f.values()){let P=t.getPackageInformation(S),R=ue.toPortablePath(P.packageLocation.slice(0,-1)).split(K.sep).slice(E.length),N=n;for(let U of R){let W=N.children.get(U);W||(W={children:new Map},N.children.set(U,W)),N=W}N.workspaceLocator=S}let C=(S,P)=>{if(S.workspaceLocator){let I=gA(P),R=a.get(I);R||(R=new Set,a.set(I,R)),R.add(S.workspaceLocator)}for(let I of S.children.values())C(I,S.workspaceLocator||P)};for(let S of n.children.values())C(S,n.workspaceLocator);return a},NQt=(t,e)=>{let r=[],s=!1,a=new Map,n=FQt(t),c=t.getPackageInformation(t.topLevel);if(c===null)throw new Error("Assertion failed: Expected the top-level package to have been registered");let f=t.findPackageLocator(c.packageLocation);if(f===null)throw new Error("Assertion failed: Expected the top-level package to have a physical locator");let p=ue.toPortablePath(c.packageLocation.slice(0,-1)),h={name:f.name,identName:f.name,reference:f.reference,peerNames:c.packagePeers,dependencies:new Set,dependencyKind:1},E=new Map,C=(P,I)=>`${gA(I)}:${P}`,S=(P,I,R,N,U,W,te,ie)=>{let Ae=C(P,R),ce=E.get(Ae),me=!!ce;!me&&R.name===f.name&&R.reference===f.reference&&(ce=h,E.set(Ae,h));let pe=PY(I,R,t,p);if(!ce){let fe=0;pe?fe=2:I.linkType==="SOFT"&&R.name.endsWith(tg)&&(fe=1),ce={name:P,identName:R.name,reference:R.reference,dependencies:new Set,peerNames:fe===1?new Set:I.packagePeers,dependencyKind:fe},E.set(Ae,ce)}let Be;if(pe?Be=2:U.linkType==="SOFT"?Be=1:Be=0,ce.hoistPriority=Math.max(ce.hoistPriority||0,Be),ie&&!pe){let fe=gA({name:N.identName,reference:N.reference}),se=a.get(fe)||new Set;a.set(fe,se),se.add(ce.name)}let Ce=new Map(I.packageDependencies);if(e.project){let fe=e.project.workspacesByCwd.get(ue.toPortablePath(I.packageLocation.slice(0,-1)));if(fe){let se=new Set([...Array.from(fe.manifest.peerDependencies.values(),X=>q.stringifyIdent(X)),...Array.from(fe.manifest.peerDependenciesMeta.keys())]);for(let X of se)Ce.has(X)||(Ce.set(X,W.get(X)||null),ce.peerNames.add(X))}}let g=gA({name:R.name.replace(tg,""),reference:R.reference}),we=n.get(g);if(we)for(let fe of we)Ce.set(`${fe.name}${tg}`,fe.reference);(I!==U||I.linkType!=="SOFT"||!pe&&(!e.selfReferencesByCwd||e.selfReferencesByCwd.get(te)))&&N.dependencies.add(ce);let ye=R!==f&&I.linkType==="SOFT"&&!R.name.endsWith(tg)&&!pe;if(!me&&!ye){let fe=new Map;for(let[se,X]of Ce)if(X!==null){let De=t.getLocator(se,X),Re=t.getLocator(se.replace(tg,""),X),dt=t.getPackageInformation(Re);if(dt===null)throw new Error("Assertion failed: Expected the package to have been registered");let j=PY(dt,De,t,p);if(e.validateExternalSoftLinks&&e.project&&j){dt.packageDependencies.size>0&&(s=!0);for(let[Ye,ke]of dt.packageDependencies)if(ke!==null){let it=q.parseLocator(Array.isArray(ke)?`${ke[0]}@${ke[1]}`:`${Ye}@${ke}`);if(gA(it)!==gA(De)){let _e=Ce.get(Ye);if(_e){let x=q.parseLocator(Array.isArray(_e)?`${_e[0]}@${_e[1]}`:`${Ye}@${_e}`);zPe(x,it)||r.push({messageName:71,text:`Cannot link ${q.prettyIdent(e.project.configuration,q.parseIdent(De.name))} into ${q.prettyLocator(e.project.configuration,q.parseLocator(`${R.name}@${R.reference}`))} dependency ${q.prettyLocator(e.project.configuration,it)} conflicts with parent dependency ${q.prettyLocator(e.project.configuration,x)}`})}else{let x=fe.get(Ye);if(x){let w=x.target,b=q.parseLocator(Array.isArray(w)?`${w[0]}@${w[1]}`:`${Ye}@${w}`);zPe(b,it)||r.push({messageName:71,text:`Cannot link ${q.prettyIdent(e.project.configuration,q.parseIdent(De.name))} into ${q.prettyLocator(e.project.configuration,q.parseLocator(`${R.name}@${R.reference}`))} dependency ${q.prettyLocator(e.project.configuration,it)} conflicts with dependency ${q.prettyLocator(e.project.configuration,b)} from sibling portal ${q.prettyIdent(e.project.configuration,q.parseIdent(x.portal.name))}`})}else fe.set(Ye,{target:it.reference,portal:De})}}}}let rt=e.hoistingLimitsByCwd?.get(te),Fe=j?te:K.relative(p,ue.toPortablePath(dt.packageLocation))||vt.dot,Ne=e.hoistingLimitsByCwd?.get(Fe);S(se,dt,De,ce,I,Ce,Fe,rt==="dependencies"||Ne==="dependencies"||Ne==="workspaces")}}};return S(f.name,c,f,h,c,c.packageDependencies,vt.dot,!1),{packageTree:h,hoistingLimits:a,errors:r,preserveSymlinksRequired:s}};function ZPe(t,e,r){let s=r.resolveVirtual&&e.reference&&e.reference.startsWith("virtual:")?r.resolveVirtual(t.packageLocation):t.packageLocation;return ue.toPortablePath(s||t.packageLocation)}function OQt(t,e,r){let s=e.getLocator(t.name.replace(tg,""),t.reference),a=e.getPackageInformation(s);if(a===null)throw new Error("Assertion failed: Expected the package to be registered");return r.pnpifyFs?{linkType:"SOFT",target:ue.toPortablePath(a.packageLocation)}:{linkType:a.linkType,target:ZPe(a,t,e)}}var LQt=(t,e,r)=>{let s=new Map,a=(E,C,S)=>{let{linkType:P,target:I}=OQt(E,t,r);return{locator:gA(E),nodePath:C,target:I,linkType:P,aliases:S}},n=E=>{let[C,S]=E.split("/");return S?{scope:C,name:S}:{scope:null,name:C}},c=new Set,f=(E,C,S)=>{if(c.has(E))return;c.add(E);let P=Array.from(E.references).sort().join("#");for(let I of E.dependencies){let R=Array.from(I.references).sort().join("#");if(I.identName===E.identName.replace(tg,"")&&R===P)continue;let N=Array.from(I.references).sort(),U={name:I.identName,reference:N[0]},{name:W,scope:te}=n(I.name),ie=te?[te,W]:[W],Ae=K.join(C,JPe),ce=K.join(Ae,...ie),me=`${S}/${U.name}`,pe=a(U,S,N.slice(1)),Be=!1;if(pe.linkType==="SOFT"&&r.project){let Ce=r.project.workspacesByCwd.get(pe.target.slice(0,-1));Be=!!(Ce&&!Ce.manifest.name)}if(!I.name.endsWith(tg)&&!Be){let Ce=s.get(ce);if(Ce){if(Ce.dirList)throw new Error(`Assertion failed: ${ce} cannot merge dir node with leaf node`);{let ye=q.parseLocator(Ce.locator),fe=q.parseLocator(pe.locator);if(Ce.linkType!==pe.linkType)throw new Error(`Assertion failed: ${ce} cannot merge nodes with different link types ${Ce.nodePath}/${q.stringifyLocator(ye)} and ${S}/${q.stringifyLocator(fe)}`);if(ye.identHash!==fe.identHash)throw new Error(`Assertion failed: ${ce} cannot merge nodes with different idents ${Ce.nodePath}/${q.stringifyLocator(ye)} and ${S}/s${q.stringifyLocator(fe)}`);pe.aliases=[...pe.aliases,...Ce.aliases,q.parseLocator(Ce.locator).reference]}}s.set(ce,pe);let g=ce.split("/"),we=g.indexOf(JPe);for(let ye=g.length-1;we>=0&&ye>we;ye--){let fe=ue.toPortablePath(g.slice(0,ye).join(K.sep)),se=g[ye],X=s.get(fe);if(!X)s.set(fe,{dirList:new Set([se])});else if(X.dirList){if(X.dirList.has(se))break;X.dirList.add(se)}}}f(I,pe.linkType==="SOFT"?pe.target:ce,me)}},p=a({name:e.name,reference:Array.from(e.references)[0]},"",[]),h=p.target;return s.set(h,p),f(e,h,""),s};Ve();Ve();bt();bt();rA();Bc();var KY={};Vt(KY,{PnpInstaller:()=>jm,PnpLinker:()=>ig,UnplugCommand:()=>Sw,default:()=>pTt,getPnpPath:()=>sg,jsInstallUtils:()=>mA,pnpUtils:()=>ZD,quotePathIfNeeded:()=>Nxe});bt();var Fxe=Ie("url");Ve();Ve();bt();bt();var XPe={DEFAULT:{collapsed:!1,next:{"*":"DEFAULT"}},TOP_LEVEL:{collapsed:!1,next:{fallbackExclusionList:"FALLBACK_EXCLUSION_LIST",packageRegistryData:"PACKAGE_REGISTRY_DATA","*":"DEFAULT"}},FALLBACK_EXCLUSION_LIST:{collapsed:!1,next:{"*":"FALLBACK_EXCLUSION_ENTRIES"}},FALLBACK_EXCLUSION_ENTRIES:{collapsed:!0,next:{"*":"FALLBACK_EXCLUSION_DATA"}},FALLBACK_EXCLUSION_DATA:{collapsed:!0,next:{"*":"DEFAULT"}},PACKAGE_REGISTRY_DATA:{collapsed:!1,next:{"*":"PACKAGE_REGISTRY_ENTRIES"}},PACKAGE_REGISTRY_ENTRIES:{collapsed:!0,next:{"*":"PACKAGE_STORE_DATA"}},PACKAGE_STORE_DATA:{collapsed:!1,next:{"*":"PACKAGE_STORE_ENTRIES"}},PACKAGE_STORE_ENTRIES:{collapsed:!0,next:{"*":"PACKAGE_INFORMATION_DATA"}},PACKAGE_INFORMATION_DATA:{collapsed:!1,next:{packageDependencies:"PACKAGE_DEPENDENCIES","*":"DEFAULT"}},PACKAGE_DEPENDENCIES:{collapsed:!1,next:{"*":"PACKAGE_DEPENDENCY"}},PACKAGE_DEPENDENCY:{collapsed:!0,next:{"*":"DEFAULT"}}};function MQt(t,e,r){let s="";s+="[";for(let a=0,n=t.length;a"u"||(f!==0&&(a+=", "),a+=JSON.stringify(p),a+=": ",a+=iN(p,h,e,r).replace(/^ +/g,""),f+=1)}return a+="}",a}function HQt(t,e,r){let s=Object.keys(t),a=`${r} `,n="";n+=r,n+=`{ `;let c=0;for(let f=0,p=s.length;f"u"||(c!==0&&(n+=",",n+=` `),n+=a,n+=JSON.stringify(h),n+=": ",n+=iN(h,E,e,a).replace(/^ +/g,""),c+=1)}return c!==0&&(n+=` `),n+=r,n+="}",n}function iN(t,e,r,s){let{next:a}=XPe[r],n=a[t]||a["*"];return $Pe(e,n,s)}function $Pe(t,e,r){let{collapsed:s}=XPe[e];return Array.isArray(t)?s?MQt(t,e,r):_Qt(t,e,r):typeof t=="object"&&t!==null?s?UQt(t,e,r):HQt(t,e,r):JSON.stringify(t)}function exe(t){return $Pe(t,"TOP_LEVEL","")}function HD(t,e){let r=Array.from(t);Array.isArray(e)||(e=[e]);let s=[];for(let n of e)s.push(r.map(c=>n(c)));let a=r.map((n,c)=>c);return a.sort((n,c)=>{for(let f of s){let p=f[n]f[c]?1:0;if(p!==0)return p}return 0}),a.map(n=>r[n])}function jQt(t){let e=new Map,r=HD(t.fallbackExclusionList||[],[({name:s,reference:a})=>s,({name:s,reference:a})=>a]);for(let{name:s,reference:a}of r){let n=e.get(s);typeof n>"u"&&e.set(s,n=new Set),n.add(a)}return Array.from(e).map(([s,a])=>[s,Array.from(a)])}function qQt(t){return HD(t.fallbackPool||[],([e])=>e)}function GQt(t){let e=[],r=t.dependencyTreeRoots.find(s=>t.packageRegistry.get(s.name)?.get(s.reference)?.packageLocation==="./");for(let[s,a]of HD(t.packageRegistry,([n])=>n===null?"0":`1${n}`)){if(s===null)continue;let n=[];e.push([s,n]);for(let[c,{packageLocation:f,packageDependencies:p,packagePeers:h,linkType:E,discardFromLookup:C}]of HD(a,([S])=>S===null?"0":`1${S}`)){if(c===null)continue;let S=[];s!==null&&c!==null&&!p.has(s)&&S.push([s,c]);for(let[U,W]of p)S.push([U,W]);let P=HD(S,([U])=>U),I=h&&h.size>0?Array.from(h):void 0,N={packageLocation:f,packageDependencies:P,packagePeers:I,linkType:E,discardFromLookup:C||void 0};n.push([c,N]),r&&s===r.name&&c===r.reference&&e.unshift([null,[[null,N]]])}}return e}function jD(t){return{__info:["This file is automatically generated. Do not touch it, or risk","your modifications being lost."],dependencyTreeRoots:t.dependencyTreeRoots,enableTopLevelFallback:t.enableTopLevelFallback||!1,ignorePatternData:t.ignorePattern||null,pnpZipBackend:t.pnpZipBackend,fallbackExclusionList:jQt(t),fallbackPool:qQt(t),packageRegistryData:GQt(t)}}var nxe=et(rxe());function ixe(t,e){return[t?`${t} `:"",`/* eslint-disable */ `,`// @ts-nocheck `,`"use strict"; `,` `,e,` `,(0,nxe.default)()].join("")}function WQt(t){return JSON.stringify(t,null,2)}function YQt(t){return`'${t.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\n/g,`\\ `)}'`}function VQt(t){return[`const RAW_RUNTIME_STATE = `,`${YQt(exe(t))}; `,`function $$SETUP_STATE(hydrateRuntimeState, basePath) { `,` return hydrateRuntimeState(JSON.parse(RAW_RUNTIME_STATE), {basePath: basePath || __dirname}); `,`} `].join("")}function KQt(){return[`function $$SETUP_STATE(hydrateRuntimeState, basePath) { `,` const fs = require('fs'); `,` const path = require('path'); `,` const pnpDataFilepath = path.resolve(__dirname, ${JSON.stringify(Er.pnpData)}); `,` return hydrateRuntimeState(JSON.parse(fs.readFileSync(pnpDataFilepath, 'utf8')), {basePath: basePath || __dirname}); `,`} `].join("")}function sxe(t){let e=jD(t),r=VQt(e);return ixe(t.shebang,r)}function oxe(t){let e=jD(t),r=KQt(),s=ixe(t.shebang,r);return{dataFile:WQt(e),loaderFile:s}}bt();function QY(t,{basePath:e}){let r=ue.toPortablePath(e),s=K.resolve(r),a=t.ignorePatternData!==null?new RegExp(t.ignorePatternData):null,n=new Map,c=new Map(t.packageRegistryData.map(([C,S])=>[C,new Map(S.map(([P,I])=>{if(C===null!=(P===null))throw new Error("Assertion failed: The name and reference should be null, or neither should");let R=I.discardFromLookup??!1,N={name:C,reference:P},U=n.get(I.packageLocation);U?(U.discardFromLookup=U.discardFromLookup&&R,R||(U.locator=N)):n.set(I.packageLocation,{locator:N,discardFromLookup:R});let W=null;return[P,{packageDependencies:new Map(I.packageDependencies),packagePeers:new Set(I.packagePeers),linkType:I.linkType,discardFromLookup:R,get packageLocation(){return W||(W=K.join(s,I.packageLocation))}}]}))])),f=new Map(t.fallbackExclusionList.map(([C,S])=>[C,new Set(S)])),p=new Map(t.fallbackPool),h=t.dependencyTreeRoots,E=t.enableTopLevelFallback;return{basePath:r,dependencyTreeRoots:h,enableTopLevelFallback:E,fallbackExclusionList:f,pnpZipBackend:t.pnpZipBackend,fallbackPool:p,ignorePattern:a,packageLocatorsByLocations:n,packageRegistry:c}}bt();bt();var ah=Ie("module"),Hm=Ie("url"),HY=Ie("util");var ra=Ie("url");var uxe=et(Ie("assert"));var TY=Array.isArray,qD=JSON.stringify,GD=Object.getOwnPropertyNames,Um=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),RY=(t,e)=>RegExp.prototype.exec.call(t,e),FY=(t,...e)=>RegExp.prototype[Symbol.replace].apply(t,e),rg=(t,...e)=>String.prototype.endsWith.apply(t,e),NY=(t,...e)=>String.prototype.includes.apply(t,e),OY=(t,...e)=>String.prototype.lastIndexOf.apply(t,e),WD=(t,...e)=>String.prototype.indexOf.apply(t,e),axe=(t,...e)=>String.prototype.replace.apply(t,e),ng=(t,...e)=>String.prototype.slice.apply(t,e),dA=(t,...e)=>String.prototype.startsWith.apply(t,e),lxe=Map,cxe=JSON.parse;function YD(t,e,r){return class extends r{constructor(...s){super(e(...s)),this.code=t,this.name=`${r.name} [${t}]`}}}var fxe=YD("ERR_PACKAGE_IMPORT_NOT_DEFINED",(t,e,r)=>`Package import specifier "${t}" is not defined${e?` in package ${e}package.json`:""} imported from ${r}`,TypeError),LY=YD("ERR_INVALID_MODULE_SPECIFIER",(t,e,r=void 0)=>`Invalid module "${t}" ${e}${r?` imported from ${r}`:""}`,TypeError),Axe=YD("ERR_INVALID_PACKAGE_TARGET",(t,e,r,s=!1,a=void 0)=>{let n=typeof r=="string"&&!s&&r.length&&!dA(r,"./");return e==="."?((0,uxe.default)(s===!1),`Invalid "exports" main target ${qD(r)} defined in the package config ${t}package.json${a?` imported from ${a}`:""}${n?'; targets must start with "./"':""}`):`Invalid "${s?"imports":"exports"}" target ${qD(r)} defined for '${e}' in the package config ${t}package.json${a?` imported from ${a}`:""}${n?'; targets must start with "./"':""}`},Error),VD=YD("ERR_INVALID_PACKAGE_CONFIG",(t,e,r)=>`Invalid package config ${t}${e?` while importing ${e}`:""}${r?`. ${r}`:""}`,Error),pxe=YD("ERR_PACKAGE_PATH_NOT_EXPORTED",(t,e,r=void 0)=>e==="."?`No "exports" main defined in ${t}package.json${r?` imported from ${r}`:""}`:`Package subpath '${e}' is not defined by "exports" in ${t}package.json${r?` imported from ${r}`:""}`,Error);var oN=Ie("url");function hxe(t,e){let r=Object.create(null);for(let s=0;se):t+e}KD(r,t,s,c,a)}RY(dxe,ng(t,2))!==null&&KD(r,t,s,c,a);let p=new URL(t,s),h=p.pathname,E=new URL(".",s).pathname;if(dA(h,E)||KD(r,t,s,c,a),e==="")return p;if(RY(dxe,e)!==null){let C=n?axe(r,"*",()=>e):r+e;ZQt(C,s,c,a)}return n?new URL(FY(mxe,p.href,()=>e)):new URL(e,p)}function $Qt(t){let e=+t;return`${e}`!==t?!1:e>=0&&e<4294967295}function vw(t,e,r,s,a,n,c,f){if(typeof e=="string")return XQt(e,r,s,t,a,n,c,f);if(TY(e)){if(e.length===0)return null;let p;for(let h=0;hn?-1:n>a||r===-1?1:s===-1||t.length>e.length?-1:e.length>t.length?1:0}function eTt(t,e,r){if(typeof t=="string"||TY(t))return!0;if(typeof t!="object"||t===null)return!1;let s=GD(t),a=!1,n=0;for(let c=0;c=h.length&&rg(e,C)&&Exe(n,h)===1&&OY(h,"*")===E&&(n=h,c=ng(e,E,e.length-C.length))}}if(n){let p=r[n],h=vw(t,p,c,n,s,!0,!1,a);return h==null&&MY(e,t,s),h}MY(e,t,s)}function Cxe({name:t,base:e,conditions:r,readFileSyncFn:s}){if(t==="#"||dA(t,"#/")||rg(t,"/")){let c="is not a valid internal imports specifier name";throw new LY(t,c,(0,ra.fileURLToPath)(e))}let a,n=gxe(e,s);if(n.exists){a=(0,ra.pathToFileURL)(n.pjsonPath);let c=n.imports;if(c)if(Um(c,t)&&!NY(t,"*")){let f=vw(a,c[t],"",t,e,!1,!0,r);if(f!=null)return f}else{let f="",p,h=GD(c);for(let E=0;E=C.length&&rg(t,P)&&Exe(f,C)===1&&OY(C,"*")===S&&(f=C,p=ng(t,S,t.length-P.length))}}if(f){let E=c[f],C=vw(a,E,p,f,e,!0,!0,r);if(C!=null)return C}}}zQt(t,a,e)}bt();var rTt=new Set(["BUILTIN_NODE_RESOLUTION_FAILED","MISSING_DEPENDENCY","MISSING_PEER_DEPENDENCY","QUALIFIED_PATH_RESOLUTION_FAILED","UNDECLARED_DEPENDENCY"]);function ms(t,e,r={},s){s??=rTt.has(t)?"MODULE_NOT_FOUND":t;let a={configurable:!0,writable:!0,enumerable:!1};return Object.defineProperties(new Error(e),{code:{...a,value:s},pnpCode:{...a,value:t},data:{...a,value:r}})}function cf(t){return ue.normalize(ue.fromPortablePath(t))}var Sxe=et(Bxe());function Dxe(t){return nTt(),UY[t]}var UY;function nTt(){UY||(UY={"--conditions":[],...vxe(iTt()),...vxe(process.execArgv)})}function vxe(t){return(0,Sxe.default)({"--conditions":[String],"-C":"--conditions"},{argv:t,permissive:!0})}function iTt(){let t=[],e=sTt(process.env.NODE_OPTIONS||"",t);return t.length,e}function sTt(t,e){let r=[],s=!1,a=!0;for(let n=0;nparseInt(t,10)),bxe=yl>19||yl===19&&oh>=2||yl===18&&oh>=13,pdr=yl===20&&oh<6||yl===19&&oh>=3,hdr=yl>19||yl===19&&oh>=6,gdr=yl>=21||yl===20&&oh>=10||yl===18&&oh>=19,ddr=yl>=21||yl===20&&oh>=10||yl===18&&oh>=20,mdr=yl>=22;function Pxe(t){if(process.env.WATCH_REPORT_DEPENDENCIES&&process.send)if(t=t.map(e=>ue.fromPortablePath(Ao.resolveVirtual(ue.toPortablePath(e)))),bxe)process.send({"watch:require":t});else for(let e of t)process.send({"watch:require":e})}function jY(t,e){let r=Number(process.env.PNP_ALWAYS_WARN_ON_FALLBACK)>0,s=Number(process.env.PNP_DEBUG_LEVEL),a=/^(?![a-zA-Z]:[\\/]|\\\\|\.{0,2}(?:\/|$))((?:node:)?(?:@[^/]+\/)?[^/]+)\/*(.*|)$/,n=/^(\/|\.{1,2}(\/|$))/,c=/\/$/,f=/^\.{0,2}\//,p={name:null,reference:null},h=[],E=new Set;if(t.enableTopLevelFallback===!0&&h.push(p),e.compatibilityMode!==!1)for(let Fe of["react-scripts","gatsby"]){let Ne=t.packageRegistry.get(Fe);if(Ne)for(let Pe of Ne.keys()){if(Pe===null)throw new Error("Assertion failed: This reference shouldn't be null");h.push({name:Fe,reference:Pe})}}let{ignorePattern:C,packageRegistry:S,packageLocatorsByLocations:P}=t;function I(Fe,Ne){return{fn:Fe,args:Ne,error:null,result:null}}function R(Fe){let Ne=process.stderr?.hasColors?.()??process.stdout.isTTY,Pe=(it,_e)=>`\x1B[${it}m${_e}\x1B[0m`,Ye=Fe.error;console.error(Ye?Pe("31;1",`\u2716 ${Fe.error?.message.replace(/\n.*/s,"")}`):Pe("33;1","\u203C Resolution")),Fe.args.length>0&&console.error();for(let it of Fe.args)console.error(` ${Pe("37;1","In \u2190")} ${(0,HY.inspect)(it,{colors:Ne,compact:!0})}`);Fe.result&&(console.error(),console.error(` ${Pe("37;1","Out \u2192")} ${(0,HY.inspect)(Fe.result,{colors:Ne,compact:!0})}`));let ke=new Error().stack.match(/(?<=^ +)at.*/gm)?.slice(2)??[];if(ke.length>0){console.error();for(let it of ke)console.error(` ${Pe("38;5;244",it)}`)}console.error()}function N(Fe,Ne){if(e.allowDebug===!1)return Ne;if(Number.isFinite(s)){if(s>=2)return(...Pe)=>{let Ye=I(Fe,Pe);try{return Ye.result=Ne(...Pe)}catch(ke){throw Ye.error=ke}finally{R(Ye)}};if(s>=1)return(...Pe)=>{try{return Ne(...Pe)}catch(Ye){let ke=I(Fe,Pe);throw ke.error=Ye,R(ke),Ye}}}return Ne}function U(Fe){let Ne=g(Fe);if(!Ne)throw ms("INTERNAL","Couldn't find a matching entry in the dependency tree for the specified parent (this is probably an internal error)");return Ne}function W(Fe){if(Fe.name===null)return!0;for(let Ne of t.dependencyTreeRoots)if(Ne.name===Fe.name&&Ne.reference===Fe.reference)return!0;return!1}let te=new Set(["node","require",...Dxe("--conditions")]);function ie(Fe,Ne=te,Pe){let Ye=fe(K.join(Fe,"internal.js"),{resolveIgnored:!0,includeDiscardFromLookup:!0});if(Ye===null)throw ms("INTERNAL",`The locator that owns the "${Fe}" path can't be found inside the dependency tree (this is probably an internal error)`);let{packageLocation:ke}=U(Ye),it=K.join(ke,Er.manifest);if(!e.fakeFs.existsSync(it))return null;let _e=JSON.parse(e.fakeFs.readFileSync(it,"utf8"));if(_e.exports==null)return null;let x=K.contains(ke,Fe);if(x===null)throw ms("INTERNAL","unqualifiedPath doesn't contain the packageLocation (this is probably an internal error)");x!=="."&&!f.test(x)&&(x=`./${x}`);try{let w=Ixe({packageJSONUrl:(0,Hm.pathToFileURL)(ue.fromPortablePath(it)),packageSubpath:x,exports:_e.exports,base:Pe?(0,Hm.pathToFileURL)(ue.fromPortablePath(Pe)):null,conditions:Ne});return ue.toPortablePath((0,Hm.fileURLToPath)(w))}catch(w){throw ms("EXPORTS_RESOLUTION_FAILED",w.message,{unqualifiedPath:cf(Fe),locator:Ye,pkgJson:_e,subpath:cf(x),conditions:Ne},w.code)}}function Ae(Fe,Ne,{extensions:Pe}){let Ye;try{Ne.push(Fe),Ye=e.fakeFs.statSync(Fe)}catch{}if(Ye&&!Ye.isDirectory())return e.fakeFs.realpathSync(Fe);if(Ye&&Ye.isDirectory()){let ke;try{ke=JSON.parse(e.fakeFs.readFileSync(K.join(Fe,Er.manifest),"utf8"))}catch{}let it;if(ke&&ke.main&&(it=K.resolve(Fe,ke.main)),it&&it!==Fe){let _e=Ae(it,Ne,{extensions:Pe});if(_e!==null)return _e}}for(let ke=0,it=Pe.length;ke{let x=JSON.stringify(_e.name);if(Ye.has(x))return;Ye.add(x);let w=we(_e);for(let b of w)if(U(b).packagePeers.has(Fe))ke(b);else{let F=Pe.get(b.name);typeof F>"u"&&Pe.set(b.name,F=new Set),F.add(b.reference)}};ke(Ne);let it=[];for(let _e of[...Pe.keys()].sort())for(let x of[...Pe.get(_e)].sort())it.push({name:_e,reference:x});return it}function fe(Fe,{resolveIgnored:Ne=!1,includeDiscardFromLookup:Pe=!1}={}){if(pe(Fe)&&!Ne)return null;let Ye=K.relative(t.basePath,Fe);Ye.match(n)||(Ye=`./${Ye}`),Ye.endsWith("/")||(Ye=`${Ye}/`);do{let ke=P.get(Ye);if(typeof ke>"u"||ke.discardFromLookup&&!Pe){Ye=Ye.substring(0,Ye.lastIndexOf("/",Ye.length-2)+1);continue}return ke.locator}while(Ye!=="");return null}function se(Fe){try{return e.fakeFs.readFileSync(ue.toPortablePath(Fe),"utf8")}catch(Ne){if(Ne.code==="ENOENT")return;throw Ne}}function X(Fe,Ne,{considerBuiltins:Pe=!0}={}){if(Fe.startsWith("#"))throw new Error("resolveToUnqualified can not handle private import mappings");if(Fe==="pnpapi")return ue.toPortablePath(e.pnpapiResolution);if(Pe&&(0,ah.isBuiltin)(Fe))return null;let Ye=cf(Fe),ke=Ne&&cf(Ne);if(Ne&&pe(Ne)&&(!K.isAbsolute(Fe)||fe(Fe)===null)){let x=me(Fe,Ne);if(x===!1)throw ms("BUILTIN_NODE_RESOLUTION_FAILED",`The builtin node resolution algorithm was unable to resolve the requested module (it didn't go through the pnp resolver because the issuer was explicitely ignored by the regexp) Require request: "${Ye}" Required by: ${ke} `,{request:Ye,issuer:ke});return ue.toPortablePath(x)}let it,_e=Fe.match(a);if(_e){if(!Ne)throw ms("API_ERROR","The resolveToUnqualified function must be called with a valid issuer when the path isn't a builtin nor absolute",{request:Ye,issuer:ke});let[,x,w]=_e,b=fe(Ne);if(!b){let Te=me(Fe,Ne);if(Te===!1)throw ms("BUILTIN_NODE_RESOLUTION_FAILED",`The builtin node resolution algorithm was unable to resolve the requested module (it didn't go through the pnp resolver because the issuer doesn't seem to be part of the Yarn-managed dependency tree). Require path: "${Ye}" Required by: ${ke} `,{request:Ye,issuer:ke});return ue.toPortablePath(Te)}let F=U(b).packageDependencies.get(x),z=null;if(F==null&&b.name!==null){let Te=t.fallbackExclusionList.get(b.name);if(!Te||!Te.has(b.reference)){for(let It=0,qt=h.length;ItW(lt))?Z=ms("MISSING_PEER_DEPENDENCY",`${b.name} tried to access ${x} (a peer dependency) but it isn't provided by your application; this makes the require call ambiguous and unsound. Required package: ${x}${x!==Ye?` (via "${Ye}")`:""} Required by: ${b.name}@${b.reference} (via ${ke}) ${Te.map(lt=>`Ancestor breaking the chain: ${lt.name}@${lt.reference} `).join("")} `,{request:Ye,issuer:ke,issuerLocator:Object.assign({},b),dependencyName:x,brokenAncestors:Te}):Z=ms("MISSING_PEER_DEPENDENCY",`${b.name} tried to access ${x} (a peer dependency) but it isn't provided by its ancestors; this makes the require call ambiguous and unsound. Required package: ${x}${x!==Ye?` (via "${Ye}")`:""} Required by: ${b.name}@${b.reference} (via ${ke}) ${Te.map(lt=>`Ancestor breaking the chain: ${lt.name}@${lt.reference} `).join("")} `,{request:Ye,issuer:ke,issuerLocator:Object.assign({},b),dependencyName:x,brokenAncestors:Te})}else F===void 0&&(!Pe&&(0,ah.isBuiltin)(Fe)?W(b)?Z=ms("UNDECLARED_DEPENDENCY",`Your application tried to access ${x}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since ${x} isn't otherwise declared in your dependencies, this makes the require call ambiguous and unsound. Required package: ${x}${x!==Ye?` (via "${Ye}")`:""} Required by: ${ke} `,{request:Ye,issuer:ke,dependencyName:x}):Z=ms("UNDECLARED_DEPENDENCY",`${b.name} tried to access ${x}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since ${x} isn't otherwise declared in ${b.name}'s dependencies, this makes the require call ambiguous and unsound. Required package: ${x}${x!==Ye?` (via "${Ye}")`:""} Required by: ${ke} `,{request:Ye,issuer:ke,issuerLocator:Object.assign({},b),dependencyName:x}):W(b)?Z=ms("UNDECLARED_DEPENDENCY",`Your application tried to access ${x}, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound. Required package: ${x}${x!==Ye?` (via "${Ye}")`:""} Required by: ${ke} `,{request:Ye,issuer:ke,dependencyName:x}):Z=ms("UNDECLARED_DEPENDENCY",`${b.name} tried to access ${x}, but it isn't declared in its dependencies; this makes the require call ambiguous and unsound. Required package: ${x}${x!==Ye?` (via "${Ye}")`:""} Required by: ${b.name}@${b.reference} (via ${ke}) `,{request:Ye,issuer:ke,issuerLocator:Object.assign({},b),dependencyName:x}));if(F==null){if(z===null||Z===null)throw Z||new Error("Assertion failed: Expected an error to have been set");F=z;let Te=Z.message.replace(/\n.*/g,"");Z.message=Te,!E.has(Te)&&s!==0&&(E.add(Te),process.emitWarning(Z))}let $=Array.isArray(F)?{name:F[0],reference:F[1]}:{name:x,reference:F},oe=U($);if(!oe.packageLocation)throw ms("MISSING_DEPENDENCY",`A dependency seems valid but didn't get installed for some reason. This might be caused by a partial install, such as dev vs prod. Required package: ${$.name}@${$.reference}${$.name!==Ye?` (via "${Ye}")`:""} Required by: ${b.name}@${b.reference} (via ${ke}) `,{request:Ye,issuer:ke,dependencyLocator:Object.assign({},$)});let xe=oe.packageLocation;w?it=K.join(xe,w):it=xe}else if(K.isAbsolute(Fe))it=K.normalize(Fe);else{if(!Ne)throw ms("API_ERROR","The resolveToUnqualified function must be called with a valid issuer when the path isn't a builtin nor absolute",{request:Ye,issuer:ke});let x=K.resolve(Ne);Ne.match(c)?it=K.normalize(K.join(x,Fe)):it=K.normalize(K.join(K.dirname(x),Fe))}return K.normalize(it)}function De(Fe,Ne,Pe=te,Ye){if(n.test(Fe))return Ne;let ke=ie(Ne,Pe,Ye);return ke?K.normalize(ke):Ne}function Re(Fe,{extensions:Ne=Object.keys(ah.Module._extensions)}={}){let Pe=[],Ye=Ae(Fe,Pe,{extensions:Ne});if(Ye)return K.normalize(Ye);{Pxe(Pe.map(_e=>ue.fromPortablePath(_e)));let ke=cf(Fe),it=fe(Fe);if(it){let{packageLocation:_e}=U(it),x=!0;try{e.fakeFs.accessSync(_e)}catch(w){if(w?.code==="ENOENT")x=!1;else{let b=(w?.message??w??"empty exception thrown").replace(/^[A-Z]/,y=>y.toLowerCase());throw ms("QUALIFIED_PATH_RESOLUTION_FAILED",`Required package exists but could not be accessed (${b}). Missing package: ${it.name}@${it.reference} Expected package location: ${cf(_e)} `,{unqualifiedPath:ke,extensions:Ne})}}if(!x){let w=_e.includes("/unplugged/")?"Required unplugged package missing from disk. This may happen when switching branches without running installs (unplugged packages must be fully materialized on disk to work).":"Required package missing from disk. If you keep your packages inside your repository then restarting the Node process may be enough. Otherwise, try to run an install first.";throw ms("QUALIFIED_PATH_RESOLUTION_FAILED",`${w} Missing package: ${it.name}@${it.reference} Expected package location: ${cf(_e)} `,{unqualifiedPath:ke,extensions:Ne})}}throw ms("QUALIFIED_PATH_RESOLUTION_FAILED",`Qualified path resolution failed: we looked for the following paths, but none could be accessed. Source path: ${ke} ${Pe.map(_e=>`Not found: ${cf(_e)} `).join("")}`,{unqualifiedPath:ke,extensions:Ne})}}function dt(Fe,Ne,Pe){if(!Ne)throw new Error("Assertion failed: An issuer is required to resolve private import mappings");let Ye=Cxe({name:Fe,base:(0,Hm.pathToFileURL)(ue.fromPortablePath(Ne)),conditions:Pe.conditions??te,readFileSyncFn:se});if(Ye instanceof URL)return Re(ue.toPortablePath((0,Hm.fileURLToPath)(Ye)),{extensions:Pe.extensions});if(Ye.startsWith("#"))throw new Error("Mapping from one private import to another isn't allowed");return j(Ye,Ne,Pe)}function j(Fe,Ne,Pe={}){try{if(Fe.startsWith("#"))return dt(Fe,Ne,Pe);let{considerBuiltins:Ye,extensions:ke,conditions:it}=Pe,_e=X(Fe,Ne,{considerBuiltins:Ye});if(Fe==="pnpapi")return _e;if(_e===null)return null;let x=()=>Ne!==null?pe(Ne):!1,w=(!Ye||!(0,ah.isBuiltin)(Fe))&&!x()?De(Fe,_e,it,Ne):_e;return Re(w,{extensions:ke})}catch(Ye){throw Object.hasOwn(Ye,"pnpCode")&&Object.assign(Ye.data,{request:cf(Fe),issuer:Ne&&cf(Ne)}),Ye}}function rt(Fe){let Ne=K.normalize(Fe),Pe=Ao.resolveVirtual(Ne);return Pe!==Ne?Pe:null}return{VERSIONS:Be,topLevel:Ce,getLocator:(Fe,Ne)=>Array.isArray(Ne)?{name:Ne[0],reference:Ne[1]}:{name:Fe,reference:Ne},getDependencyTreeRoots:()=>[...t.dependencyTreeRoots],getAllLocators(){let Fe=[];for(let[Ne,Pe]of S)for(let Ye of Pe.keys())Ne!==null&&Ye!==null&&Fe.push({name:Ne,reference:Ye});return Fe},getPackageInformation:Fe=>{let Ne=g(Fe);if(Ne===null)return null;let Pe=ue.fromPortablePath(Ne.packageLocation);return{...Ne,packageLocation:Pe}},findPackageLocator:Fe=>fe(ue.toPortablePath(Fe)),resolveToUnqualified:N("resolveToUnqualified",(Fe,Ne,Pe)=>{let Ye=Ne!==null?ue.toPortablePath(Ne):null,ke=X(ue.toPortablePath(Fe),Ye,Pe);return ke===null?null:ue.fromPortablePath(ke)}),resolveUnqualified:N("resolveUnqualified",(Fe,Ne)=>ue.fromPortablePath(Re(ue.toPortablePath(Fe),Ne))),resolveRequest:N("resolveRequest",(Fe,Ne,Pe)=>{let Ye=Ne!==null?ue.toPortablePath(Ne):null,ke=j(ue.toPortablePath(Fe),Ye,Pe);return ke===null?null:ue.fromPortablePath(ke)}),resolveVirtual:N("resolveVirtual",Fe=>{let Ne=rt(ue.toPortablePath(Fe));return Ne!==null?ue.fromPortablePath(Ne):null})}}bt();var xxe=(t,e,r)=>{let s=jD(t),a=QY(s,{basePath:e}),n=ue.join(e,Er.pnpCjs);return jY(a,{fakeFs:r,pnpapiResolution:n})};var GY=et(Qxe());Wt();var mA={};Vt(mA,{checkManifestCompatibility:()=>Txe,extractBuildRequest:()=>aN,getExtractHint:()=>WY,hasBindingGyp:()=>YY});Ve();bt();function Txe(t){return q.isPackageCompatible(t,ps.getArchitectureSet())}function aN(t,e,r,{configuration:s}){let a=[];for(let n of["preinstall","install","postinstall"])e.manifest.scripts.has(n)&&a.push({type:0,script:n});return!e.manifest.scripts.has("install")&&e.misc.hasBindingGyp&&a.push({type:1,script:"node-gyp rebuild"}),a.length===0?null:t.linkType!=="HARD"?{skipped:!0,explain:n=>n.reportWarningOnce(6,`${q.prettyLocator(s,t)} lists build scripts, but is referenced through a soft link. Soft links don't support build scripts, so they'll be ignored.`)}:r&&r.built===!1?{skipped:!0,explain:n=>n.reportInfoOnce(5,`${q.prettyLocator(s,t)} lists build scripts, but its build has been explicitly disabled through configuration.`)}:!s.get("enableScripts")&&!r.built?{skipped:!0,explain:n=>n.reportWarningOnce(4,`${q.prettyLocator(s,t)} lists build scripts, but all build scripts have been disabled.`)}:Txe(t)?{skipped:!1,directives:a}:{skipped:!0,explain:n=>n.reportWarningOnce(76,`${q.prettyLocator(s,t)} The ${ps.getArchitectureName()} architecture is incompatible with this package, build skipped.`)}}var aTt=new Set([".exe",".bin",".h",".hh",".hpp",".c",".cc",".cpp",".java",".jar",".node"]);function WY(t){return t.packageFs.getExtractHint({relevantExtensions:aTt})}function YY(t){let e=K.join(t.prefixPath,"binding.gyp");return t.packageFs.existsSync(e)}var ZD={};Vt(ZD,{getUnpluggedPath:()=>zD});Ve();bt();function zD(t,{configuration:e}){return K.resolve(e.get("pnpUnpluggedFolder"),q.slugifyLocator(t))}var lTt=new Set([q.makeIdent(null,"open").identHash,q.makeIdent(null,"opn").identHash]),ig=class{constructor(){this.mode="strict";this.pnpCache=new Map}getCustomDataKey(){return JSON.stringify({name:"PnpLinker",version:2})}supportsPackage(e,r){return this.isEnabled(r)}async findPackageLocation(e,r){if(!this.isEnabled(r))throw new Error("Assertion failed: Expected the PnP linker to be enabled");let s=sg(r.project).cjs;if(!le.existsSync(s))throw new nt(`The project in ${he.pretty(r.project.configuration,`${r.project.cwd}/package.json`,he.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let a=je.getFactoryWithDefault(this.pnpCache,s,()=>je.dynamicRequire(s,{cachingStrategy:je.CachingStrategy.FsTime})),n={name:q.stringifyIdent(e),reference:e.reference},c=a.getPackageInformation(n);if(!c)throw new nt(`Couldn't find ${q.prettyLocator(r.project.configuration,e)} in the currently installed PnP map - running an install might help`);return ue.toPortablePath(c.packageLocation)}async findPackageLocator(e,r){if(!this.isEnabled(r))return null;let s=sg(r.project).cjs;if(!le.existsSync(s))return null;let n=je.getFactoryWithDefault(this.pnpCache,s,()=>je.dynamicRequire(s,{cachingStrategy:je.CachingStrategy.FsTime})).findPackageLocator(ue.fromPortablePath(e));return n?q.makeLocator(q.parseIdent(n.name),n.reference):null}makeInstaller(e){return new jm(e)}isEnabled(e){return!(e.project.configuration.get("nodeLinker")!=="pnp"||e.project.configuration.get("pnpMode")!==this.mode)}},jm=class{constructor(e){this.opts=e;this.mode="strict";this.asyncActions=new je.AsyncActions(10);this.packageRegistry=new Map;this.virtualTemplates=new Map;this.isESMLoaderRequired=!1;this.customData={store:new Map};this.unpluggedPaths=new Set;this.opts=e}attachCustomData(e){this.customData=e}async installPackage(e,r,s){let a=q.stringifyIdent(e),n=e.reference,c=!!this.opts.project.tryWorkspaceByLocator(e),f=q.isVirtualLocator(e),p=e.peerDependencies.size>0&&!f,h=!p&&!c,E=!p&&e.linkType!=="SOFT",C,S;if(h||E){let te=f?q.devirtualizeLocator(e):e;C=this.customData.store.get(te.locatorHash),typeof C>"u"&&(C=await cTt(r),e.linkType==="HARD"&&this.customData.store.set(te.locatorHash,C)),C.manifest.type==="module"&&(this.isESMLoaderRequired=!0),S=this.opts.project.getDependencyMeta(te,e.version)}let P=h?aN(e,C,S,{configuration:this.opts.project.configuration}):null,I=E?await this.unplugPackageIfNeeded(e,C,r,S,s):r.packageFs;if(K.isAbsolute(r.prefixPath))throw new Error(`Assertion failed: Expected the prefix path (${r.prefixPath}) to be relative to the parent`);let R=K.resolve(I.getRealPath(),r.prefixPath),N=VY(this.opts.project.cwd,R),U=new Map,W=new Set;if(f){for(let te of e.peerDependencies.values())U.set(q.stringifyIdent(te),null),W.add(q.stringifyIdent(te));if(!c){let te=q.devirtualizeLocator(e);this.virtualTemplates.set(te.locatorHash,{location:VY(this.opts.project.cwd,Ao.resolveVirtual(R)),locator:te})}}return je.getMapWithDefault(this.packageRegistry,a).set(n,{packageLocation:N,packageDependencies:U,packagePeers:W,linkType:e.linkType,discardFromLookup:r.discardFromLookup||!1}),{packageLocation:R,buildRequest:P}}async attachInternalDependencies(e,r){let s=this.getPackageInformation(e);for(let[a,n]of r){let c=q.areIdentsEqual(a,n)?n.reference:[q.stringifyIdent(n),n.reference];s.packageDependencies.set(q.stringifyIdent(a),c)}}async attachExternalDependents(e,r){for(let s of r)this.getDiskInformation(s).packageDependencies.set(q.stringifyIdent(e),e.reference)}async finalizeInstall(){if(this.opts.project.configuration.get("pnpMode")!==this.mode)return;let e=sg(this.opts.project);if(this.isEsmEnabled()||await le.removePromise(e.esmLoader),this.opts.project.configuration.get("nodeLinker")!=="pnp"){await le.removePromise(e.cjs),await le.removePromise(e.data),await le.removePromise(e.esmLoader),await le.removePromise(this.opts.project.configuration.get("pnpUnpluggedFolder"));return}for(let{locator:C,location:S}of this.virtualTemplates.values())je.getMapWithDefault(this.packageRegistry,q.stringifyIdent(C)).set(C.reference,{packageLocation:S,packageDependencies:new Map,packagePeers:new Set,linkType:"SOFT",discardFromLookup:!1});let r=this.opts.project.configuration.get("pnpFallbackMode"),s=this.opts.project.workspaces.map(({anchoredLocator:C})=>({name:q.stringifyIdent(C),reference:C.reference})),a=r!=="none",n=[],c=new Map,f=je.buildIgnorePattern([".yarn/sdks/**",...this.opts.project.configuration.get("pnpIgnorePatterns")]),p=this.packageRegistry,h=this.opts.project.configuration.get("pnpShebang"),E=this.opts.project.configuration.get("pnpZipBackend");if(r==="dependencies-only")for(let C of this.opts.project.storedPackages.values())this.opts.project.tryWorkspaceByLocator(C)&&n.push({name:q.stringifyIdent(C),reference:C.reference});return await this.asyncActions.wait(),await this.finalizeInstallWithPnp({dependencyTreeRoots:s,enableTopLevelFallback:a,fallbackExclusionList:n,fallbackPool:c,ignorePattern:f,pnpZipBackend:E,packageRegistry:p,shebang:h}),{customData:this.customData}}async transformPnpSettings(e){}isEsmEnabled(){if(this.opts.project.configuration.sources.has("pnpEnableEsmLoader"))return this.opts.project.configuration.get("pnpEnableEsmLoader");if(this.isESMLoaderRequired)return!0;for(let e of this.opts.project.workspaces)if(e.manifest.type==="module")return!0;return!1}async finalizeInstallWithPnp(e){let r=sg(this.opts.project),s=await this.locateNodeModules(e.ignorePattern);if(s.length>0){this.opts.report.reportWarning(31,"One or more node_modules have been detected and will be removed. This operation may take some time.");for(let n of s)await le.removePromise(n)}if(await this.transformPnpSettings(e),this.opts.project.configuration.get("pnpEnableInlining")){let n=sxe(e);await le.changeFilePromise(r.cjs,n,{automaticNewlines:!0,mode:493}),await le.removePromise(r.data)}else{let{dataFile:n,loaderFile:c}=oxe(e);await le.changeFilePromise(r.cjs,c,{automaticNewlines:!0,mode:493}),await le.changeFilePromise(r.data,n,{automaticNewlines:!0,mode:420})}this.isEsmEnabled()&&(this.opts.report.reportWarning(0,"ESM support for PnP uses the experimental loader API and is therefore experimental"),await le.changeFilePromise(r.esmLoader,(0,GY.default)(),{automaticNewlines:!0,mode:420}));let a=this.opts.project.configuration.get("pnpUnpluggedFolder");if(this.unpluggedPaths.size===0)await le.removePromise(a);else for(let n of await le.readdirPromise(a)){let c=K.resolve(a,n);this.unpluggedPaths.has(c)||await le.removePromise(c)}}async locateNodeModules(e){let r=[],s=e?new RegExp(e):null;for(let a of this.opts.project.workspaces){let n=K.join(a.cwd,"node_modules");if(s&&s.test(K.relative(this.opts.project.cwd,a.cwd))||!le.existsSync(n))continue;let c=await le.readdirPromise(n,{withFileTypes:!0}),f=c.filter(p=>!p.isDirectory()||p.name===".bin"||!p.name.startsWith("."));if(f.length===c.length)r.push(n);else for(let p of f)r.push(K.join(n,p.name))}return r}async unplugPackageIfNeeded(e,r,s,a,n){return this.shouldBeUnplugged(e,r,a)?this.unplugPackage(e,s,n):s.packageFs}shouldBeUnplugged(e,r,s){return typeof s.unplugged<"u"?s.unplugged:lTt.has(e.identHash)||e.conditions!=null?!0:r.manifest.preferUnplugged!==null?r.manifest.preferUnplugged:!!(aN(e,r,s,{configuration:this.opts.project.configuration})?.skipped===!1||r.misc.extractHint)}async unplugPackage(e,r,s){let a=zD(e,{configuration:this.opts.project.configuration});return this.opts.project.disabledLocators.has(e.locatorHash)?new Hf(a,{baseFs:r.packageFs,pathUtils:K}):(this.unpluggedPaths.add(a),s.holdFetchResult(this.asyncActions.set(e.locatorHash,async()=>{let n=K.join(a,r.prefixPath,".ready");await le.existsPromise(n)||(this.opts.project.storedBuildState.delete(e.locatorHash),await le.mkdirPromise(a,{recursive:!0}),await le.copyPromise(a,vt.dot,{baseFs:r.packageFs,overwrite:!1}),await le.writeFilePromise(n,""))})),new Sn(a))}getPackageInformation(e){let r=q.stringifyIdent(e),s=e.reference,a=this.packageRegistry.get(r);if(!a)throw new Error(`Assertion failed: The package information store should have been available (for ${q.prettyIdent(this.opts.project.configuration,e)})`);let n=a.get(s);if(!n)throw new Error(`Assertion failed: The package information should have been available (for ${q.prettyLocator(this.opts.project.configuration,e)})`);return n}getDiskInformation(e){let r=je.getMapWithDefault(this.packageRegistry,"@@disk"),s=VY(this.opts.project.cwd,e);return je.getFactoryWithDefault(r,s,()=>({packageLocation:s,packageDependencies:new Map,packagePeers:new Set,linkType:"SOFT",discardFromLookup:!1}))}};function VY(t,e){let r=K.relative(t,e);return r.match(/^\.{0,2}\//)||(r=`./${r}`),r.replace(/\/?$/,"/")}async function cTt(t){let e=await Ht.tryFind(t.prefixPath,{baseFs:t.packageFs})??new Ht,r=new Set(["preinstall","install","postinstall"]);for(let s of e.scripts.keys())r.has(s)||e.scripts.delete(s);return{manifest:{scripts:e.scripts,preferUnplugged:e.preferUnplugged,type:e.type},misc:{extractHint:WY(t),hasBindingGyp:YY(t)}}}Ve();Ve();Wt();var Rxe=et(Sa());var Sw=class extends ut{constructor(){super(...arguments);this.all=ge.Boolean("-A,--all",!1,{description:"Unplug direct dependencies from the entire project"});this.recursive=ge.Boolean("-R,--recursive",!1,{description:"Unplug both direct and transitive dependencies"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.patterns=ge.Rest()}static{this.paths=[["unplug"]]}static{this.usage=ot.Usage({description:"force the unpacking of a list of packages",details:"\n This command will add the selectors matching the specified patterns to the list of packages that must be unplugged when installed.\n\n A package being unplugged means that instead of being referenced directly through its archive, it will be unpacked at install time in the directory configured via `pnpUnpluggedFolder`. Note that unpacking packages this way is generally not recommended because it'll make it harder to store your packages within the repository. However, it's a good approach to quickly and safely debug some packages, and can even sometimes be required depending on the context (for example when the package contains shellscripts).\n\n Running the command will set a persistent flag inside your top-level `package.json`, in the `dependenciesMeta` field. As such, to undo its effects, you'll need to revert the changes made to the manifest and run `yarn install` to apply the modification.\n\n By default, only direct dependencies from the current workspace are affected. If `-A,--all` is set, direct dependencies from the entire project are affected. Using the `-R,--recursive` flag will affect transitive dependencies as well as direct ones.\n\n This command accepts glob patterns inside the scope and name components (not the range). Make sure to escape the patterns to prevent your own shell from trying to expand them.\n ",examples:[["Unplug the lodash dependency from the active workspace","yarn unplug lodash"],["Unplug all instances of lodash referenced by any workspace","yarn unplug lodash -A"],["Unplug all instances of lodash referenced by the active workspace and its dependencies","yarn unplug lodash -R"],["Unplug all instances of lodash, anywhere","yarn unplug lodash -AR"],["Unplug one specific version of lodash","yarn unplug lodash@1.2.3"],["Unplug all packages with the `@babel` scope","yarn unplug '@babel/*'"],["Unplug all packages (only for testing, not recommended)","yarn unplug -R '*'"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);if(r.get("nodeLinker")!=="pnp")throw new nt("This command can only be used if the `nodeLinker` option is set to `pnp`");await s.restoreInstallState();let c=new Set(this.patterns),f=this.patterns.map(P=>{let I=q.parseDescriptor(P),R=I.range!=="unknown"?I:q.makeDescriptor(I,"*");if(!Or.validRange(R.range))throw new nt(`The range of the descriptor patterns must be a valid semver range (${q.prettyDescriptor(r,R)})`);return N=>{let U=q.stringifyIdent(N);return!Rxe.default.isMatch(U,q.stringifyIdent(R))||N.version&&!Or.satisfiesWithPrereleases(N.version,R.range)?!1:(c.delete(P),!0)}}),p=()=>{let P=[];for(let I of s.storedPackages.values())!s.tryWorkspaceByLocator(I)&&!q.isVirtualLocator(I)&&f.some(R=>R(I))&&P.push(I);return P},h=P=>{let I=new Set,R=[],N=(U,W)=>{if(I.has(U.locatorHash))return;let te=!!s.tryWorkspaceByLocator(U);if(!(W>0&&!this.recursive&&te)&&(I.add(U.locatorHash),!s.tryWorkspaceByLocator(U)&&f.some(ie=>ie(U))&&R.push(U),!(W>0&&!this.recursive)))for(let ie of U.dependencies.values()){let Ae=s.storedResolutions.get(ie.descriptorHash);if(!Ae)throw new Error("Assertion failed: The resolution should have been registered");let ce=s.storedPackages.get(Ae);if(!ce)throw new Error("Assertion failed: The package should have been registered");N(ce,W+1)}};for(let U of P)N(U.anchoredPackage,0);return R},E,C;if(this.all&&this.recursive?(E=p(),C="the project"):this.all?(E=h(s.workspaces),C="any workspace"):(E=h([a]),C="this workspace"),c.size>1)throw new nt(`Patterns ${he.prettyList(r,c,he.Type.CODE)} don't match any packages referenced by ${C}`);if(c.size>0)throw new nt(`Pattern ${he.prettyList(r,c,he.Type.CODE)} doesn't match any packages referenced by ${C}`);E=je.sortMap(E,P=>q.stringifyLocator(P));let S=await Ot.start({configuration:r,stdout:this.context.stdout,json:this.json},async P=>{for(let I of E){let R=I.version??"unknown",N=s.topLevelWorkspace.manifest.ensureDependencyMeta(q.makeDescriptor(I,R));N.unplugged=!0,P.reportInfo(0,`Will unpack ${q.prettyLocator(r,I)} to ${he.pretty(r,zD(I,{configuration:r}),he.Type.PATH)}`),P.reportJson({locator:q.stringifyLocator(I),version:R})}await s.topLevelWorkspace.persistManifest(),this.json||P.reportSeparator()});return S.hasErrors()?S.exitCode():await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:n})}};var sg=t=>({cjs:K.join(t.cwd,Er.pnpCjs),data:K.join(t.cwd,Er.pnpData),esmLoader:K.join(t.cwd,Er.pnpEsmLoader)}),Nxe=t=>/\s/.test(t)?JSON.stringify(t):t;async function uTt(t,e,r){let s=/\s*--require\s+\S*\.pnp\.c?js\s*/g,a=/\s*--experimental-loader\s+\S*\.pnp\.loader\.mjs\s*/,n=(e.NODE_OPTIONS??"").replace(s," ").replace(a," ").trim();if(t.configuration.get("nodeLinker")!=="pnp"){e.NODE_OPTIONS=n||void 0;return}let c=sg(t),f=`--require ${Nxe(ue.fromPortablePath(c.cjs))}`;le.existsSync(c.esmLoader)&&(f=`${f} --experimental-loader ${(0,Fxe.pathToFileURL)(ue.fromPortablePath(c.esmLoader)).href}`),le.existsSync(c.cjs)&&(e.NODE_OPTIONS=n?`${f} ${n}`:f)}async function fTt(t,e){let r=sg(t);e(r.cjs),e(r.data),e(r.esmLoader),e(t.configuration.get("pnpUnpluggedFolder"))}var ATt={hooks:{populateYarnPaths:fTt,setupScriptEnvironment:uTt},configuration:{nodeLinker:{description:'The linker used for installing Node packages, one of: "pnp", "pnpm", or "node-modules"',type:"STRING",default:"pnp"},minizip:{description:"Whether Yarn should use minizip to extract archives",type:"BOOLEAN",default:!1},winLinkType:{description:"Whether Yarn should use Windows Junctions or symlinks when creating links on Windows.",type:"STRING",values:["junctions","symlinks"],default:"junctions"},pnpMode:{description:"If 'strict', generates standard PnP maps. If 'loose', merges them with the n_m resolution.",type:"STRING",default:"strict"},pnpShebang:{description:"String to prepend to the generated PnP script",type:"STRING",default:"#!/usr/bin/env node"},pnpIgnorePatterns:{description:"Array of glob patterns; files matching them will use the classic resolution",type:"STRING",default:[],isArray:!0},pnpZipBackend:{description:"Whether to use the experimental js implementation for the ZipFS",type:"STRING",values:["libzip","js"],default:"libzip"},pnpEnableEsmLoader:{description:"If true, Yarn will generate an ESM loader (`.pnp.loader.mjs`). If this is not explicitly set Yarn tries to automatically detect whether ESM support is required.",type:"BOOLEAN",default:!1},pnpEnableInlining:{description:"If true, the PnP data will be inlined along with the generated loader",type:"BOOLEAN",default:!0},pnpFallbackMode:{description:"If true, the generated PnP loader will follow the top-level fallback rule",type:"STRING",default:"dependencies-only"},pnpUnpluggedFolder:{description:"Folder where the unplugged packages must be stored",type:"ABSOLUTE_PATH",default:"./.yarn/unplugged"}},linkers:[ig],commands:[Sw]},pTt=ATt;var qxe=et(Uxe());Wt();var tV=et(Ie("crypto")),Gxe=et(Ie("fs")),Wxe=1,Ri="node_modules",lN=".bin",Yxe=".yarn-state.yml",kTt=1e3,rV=(s=>(s.CLASSIC="classic",s.HARDLINKS_LOCAL="hardlinks-local",s.HARDLINKS_GLOBAL="hardlinks-global",s))(rV||{}),XD=class{constructor(){this.installStateCache=new Map}getCustomDataKey(){return JSON.stringify({name:"NodeModulesLinker",version:3})}supportsPackage(e,r){return this.isEnabled(r)}async findPackageLocation(e,r){if(!this.isEnabled(r))throw new Error("Assertion failed: Expected the node-modules linker to be enabled");let s=r.project.tryWorkspaceByLocator(e);if(s)return s.cwd;let a=await je.getFactoryWithDefault(this.installStateCache,r.project.cwd,async()=>await eV(r.project,{unrollAliases:!0}));if(a===null)throw new nt("Couldn't find the node_modules state file - running an install might help (findPackageLocation)");let n=a.locatorMap.get(q.stringifyLocator(e));if(!n){let p=new nt(`Couldn't find ${q.prettyLocator(r.project.configuration,e)} in the currently installed node_modules map - running an install might help`);throw p.code="LOCATOR_NOT_INSTALLED",p}let c=n.locations.sort((p,h)=>p.split(K.sep).length-h.split(K.sep).length),f=K.join(r.project.configuration.startingCwd,Ri);return c.find(p=>K.contains(f,p))||n.locations[0]}async findPackageLocator(e,r){if(!this.isEnabled(r))return null;let s=await je.getFactoryWithDefault(this.installStateCache,r.project.cwd,async()=>await eV(r.project,{unrollAliases:!0}));if(s===null)return null;let{locationRoot:a,segments:n}=cN(K.resolve(e),{skipPrefix:r.project.cwd}),c=s.locationTree.get(a);if(!c)return null;let f=c.locator;for(let p of n){if(c=c.children.get(p),!c)break;f=c.locator||f}return q.parseLocator(f)}makeInstaller(e){return new $Y(e)}isEnabled(e){return e.project.configuration.get("nodeLinker")==="node-modules"}},$Y=class{constructor(e){this.opts=e;this.localStore=new Map;this.realLocatorChecksums=new Map;this.customData={store:new Map}}attachCustomData(e){this.customData=e}async installPackage(e,r){let s=K.resolve(r.packageFs.getRealPath(),r.prefixPath),a=this.customData.store.get(e.locatorHash);if(typeof a>"u"&&(a=await QTt(e,r),e.linkType==="HARD"&&this.customData.store.set(e.locatorHash,a)),!q.isPackageCompatible(e,this.opts.project.configuration.getSupportedArchitectures()))return{packageLocation:null,buildRequest:null};let n=new Map,c=new Set;n.has(q.stringifyIdent(e))||n.set(q.stringifyIdent(e),e.reference);let f=e;if(q.isVirtualLocator(e)){f=q.devirtualizeLocator(e);for(let E of e.peerDependencies.values())n.set(q.stringifyIdent(E),null),c.add(q.stringifyIdent(E))}let p={packageLocation:`${ue.fromPortablePath(s)}/`,packageDependencies:n,packagePeers:c,linkType:e.linkType,discardFromLookup:r.discardFromLookup??!1};this.localStore.set(e.locatorHash,{pkg:e,customPackageData:a,dependencyMeta:this.opts.project.getDependencyMeta(e,e.version),pnpNode:p});let h=r.checksum?r.checksum.substring(r.checksum.indexOf("/")+1):null;return this.realLocatorChecksums.set(f.locatorHash,h),{packageLocation:s,buildRequest:null}}async attachInternalDependencies(e,r){let s=this.localStore.get(e.locatorHash);if(typeof s>"u")throw new Error("Assertion failed: Expected information object to have been registered");for(let[a,n]of r){let c=q.areIdentsEqual(a,n)?n.reference:[q.stringifyIdent(n),n.reference];s.pnpNode.packageDependencies.set(q.stringifyIdent(a),c)}}async attachExternalDependents(e,r){throw new Error("External dependencies haven't been implemented for the node-modules linker")}async finalizeInstall(){if(this.opts.project.configuration.get("nodeLinker")!=="node-modules")return;let e=new Ao({baseFs:new tA({maxOpenFiles:80,readOnlyArchives:!0})}),r=await eV(this.opts.project),s=this.opts.project.configuration.get("nmMode");(r===null||s!==r.nmMode)&&(this.opts.project.storedBuildState.clear(),r={locatorMap:new Map,binSymlinks:new Map,locationTree:new Map,nmMode:s,mtimeMs:0});let a=new Map(this.opts.project.workspaces.map(S=>{let P=this.opts.project.configuration.get("nmHoistingLimits");try{P=je.validateEnum(_D,S.manifest.installConfig?.hoistingLimits??P)}catch{let I=q.prettyWorkspace(this.opts.project.configuration,S);this.opts.report.reportWarning(57,`${I}: Invalid 'installConfig.hoistingLimits' value. Expected one of ${Object.values(_D).join(", ")}, using default: "${P}"`)}return[S.relativeCwd,P]})),n=new Map(this.opts.project.workspaces.map(S=>{let P=this.opts.project.configuration.get("nmSelfReferences");return P=S.manifest.installConfig?.selfReferences??P,[S.relativeCwd,P]})),c={VERSIONS:{std:1},topLevel:{name:null,reference:null},getLocator:(S,P)=>Array.isArray(P)?{name:P[0],reference:P[1]}:{name:S,reference:P},getDependencyTreeRoots:()=>this.opts.project.workspaces.map(S=>{let P=S.anchoredLocator;return{name:q.stringifyIdent(P),reference:P.reference}}),getPackageInformation:S=>{let P=S.reference===null?this.opts.project.topLevelWorkspace.anchoredLocator:q.makeLocator(q.parseIdent(S.name),S.reference),I=this.localStore.get(P.locatorHash);if(typeof I>"u")throw new Error("Assertion failed: Expected the package reference to have been registered");return I.pnpNode},findPackageLocator:S=>{let P=this.opts.project.tryWorkspaceByCwd(ue.toPortablePath(S));if(P!==null){let I=P.anchoredLocator;return{name:q.stringifyIdent(I),reference:I.reference}}throw new Error("Assertion failed: Unimplemented")},resolveToUnqualified:()=>{throw new Error("Assertion failed: Unimplemented")},resolveUnqualified:()=>{throw new Error("Assertion failed: Unimplemented")},resolveRequest:()=>{throw new Error("Assertion failed: Unimplemented")},resolveVirtual:S=>ue.fromPortablePath(Ao.resolveVirtual(ue.toPortablePath(S)))},{tree:f,errors:p,preserveSymlinksRequired:h}=UD(c,{pnpifyFs:!1,validateExternalSoftLinks:!0,hoistingLimitsByCwd:a,project:this.opts.project,selfReferencesByCwd:n});if(!f){for(let{messageName:S,text:P}of p)this.opts.report.reportError(S,P);return}let E=xY(f);await MTt(r,E,{baseFs:e,project:this.opts.project,report:this.opts.report,realLocatorChecksums:this.realLocatorChecksums,loadManifest:async S=>{let P=q.parseLocator(S),I=this.localStore.get(P.locatorHash);if(typeof I>"u")throw new Error("Assertion failed: Expected the slot to exist");return I.customPackageData.manifest}});let C=[];for(let[S,P]of E.entries()){if(Jxe(S))continue;let I=q.parseLocator(S),R=this.localStore.get(I.locatorHash);if(typeof R>"u")throw new Error("Assertion failed: Expected the slot to exist");if(this.opts.project.tryWorkspaceByLocator(R.pkg))continue;let N=mA.extractBuildRequest(R.pkg,R.customPackageData,R.dependencyMeta,{configuration:this.opts.project.configuration});N&&C.push({buildLocations:P.locations,locator:I,buildRequest:N})}return h&&this.opts.report.reportWarning(72,`The application uses portals and that's why ${he.pretty(this.opts.project.configuration,"--preserve-symlinks",he.Type.CODE)} Node option is required for launching it`),{customData:this.customData,records:C}}};async function QTt(t,e){let r=await Ht.tryFind(e.prefixPath,{baseFs:e.packageFs})??new Ht,s=new Set(["preinstall","install","postinstall"]);for(let a of r.scripts.keys())s.has(a)||r.scripts.delete(a);return{manifest:{bin:r.bin,scripts:r.scripts},misc:{hasBindingGyp:mA.hasBindingGyp(e)}}}async function TTt(t,e,r,s,{installChangedByUser:a}){let n="";n+=`# Warning: This file is automatically generated. Removing it is fine, but will `,n+=`# cause your node_modules installation to become invalidated. `,n+=` `,n+=`__metadata: `,n+=` version: ${Wxe} `,n+=` nmMode: ${s.value} `;let c=Array.from(e.keys()).sort(),f=q.stringifyLocator(t.topLevelWorkspace.anchoredLocator);for(let E of c){let C=e.get(E);n+=` `,n+=`${JSON.stringify(E)}: `,n+=` locations: `;for(let S of C.locations){let P=K.contains(t.cwd,S);if(P===null)throw new Error(`Assertion failed: Expected the path to be within the project (${S})`);n+=` - ${JSON.stringify(P)} `}if(C.aliases.length>0){n+=` aliases: `;for(let S of C.aliases)n+=` - ${JSON.stringify(S)} `}if(E===f&&r.size>0){n+=` bin: `;for(let[S,P]of r){let I=K.contains(t.cwd,S);if(I===null)throw new Error(`Assertion failed: Expected the path to be within the project (${S})`);n+=` ${JSON.stringify(I)}: `;for(let[R,N]of P){let U=K.relative(K.join(S,Ri),N);n+=` ${JSON.stringify(R)}: ${JSON.stringify(U)} `}}}}let p=t.cwd,h=K.join(p,Ri,Yxe);a&&await le.removePromise(h),await le.changeFilePromise(h,n,{automaticNewlines:!0})}async function eV(t,{unrollAliases:e=!1}={}){let r=t.cwd,s=K.join(r,Ri,Yxe),a;try{a=await le.statPromise(s)}catch{}if(!a)return null;let n=cs(await le.readFilePromise(s,"utf8"));if(n.__metadata.version>Wxe)return null;let c=n.__metadata.nmMode||"classic",f=new Map,p=new Map;delete n.__metadata;for(let[h,E]of Object.entries(n)){let C=E.locations.map(P=>K.join(r,P)),S=E.bin;if(S)for(let[P,I]of Object.entries(S)){let R=K.join(r,ue.toPortablePath(P)),N=je.getMapWithDefault(p,R);for(let[U,W]of Object.entries(I))N.set(U,ue.toPortablePath([R,Ri,W].join(K.sep)))}if(f.set(h,{target:vt.dot,linkType:"HARD",locations:C,aliases:E.aliases||[]}),e&&E.aliases)for(let P of E.aliases){let{scope:I,name:R}=q.parseLocator(h),N=q.makeLocator(q.makeIdent(I,R),P),U=q.stringifyLocator(N);f.set(U,{target:vt.dot,linkType:"HARD",locations:C,aliases:[]})}}return{locatorMap:f,binSymlinks:p,locationTree:Vxe(f,{skipPrefix:t.cwd}),nmMode:c,mtimeMs:a.mtimeMs}}var bw=async(t,e)=>{if(t.split(K.sep).indexOf(Ri)<0)throw new Error(`Assertion failed: trying to remove dir that doesn't contain node_modules: ${t}`);try{let r;if(!e.innerLoop&&(r=await le.lstatPromise(t),!r.isDirectory()&&!r.isSymbolicLink()||r.isSymbolicLink()&&!e.isWorkspaceDir)){await le.unlinkPromise(t);return}let s=await le.readdirPromise(t,{withFileTypes:!0});for(let n of s){let c=K.join(t,n.name);n.isDirectory()?(n.name!==Ri||e&&e.innerLoop)&&await bw(c,{innerLoop:!0,contentsOnly:!1}):await le.unlinkPromise(c)}let a=!e.innerLoop&&e.isWorkspaceDir&&r?.isSymbolicLink();!e.contentsOnly&&!a&&await le.rmdirPromise(t)}catch(r){if(r.code!=="ENOENT"&&r.code!=="ENOTEMPTY")throw r}},Hxe=4,cN=(t,{skipPrefix:e})=>{let r=K.contains(e,t);if(r===null)throw new Error(`Assertion failed: Writing attempt prevented to ${t} which is outside project root: ${e}`);let s=r.split(K.sep).filter(p=>p!==""),a=s.indexOf(Ri),n=s.slice(0,a).join(K.sep),c=K.join(e,n),f=s.slice(a);return{locationRoot:c,segments:f}},Vxe=(t,{skipPrefix:e})=>{let r=new Map;if(t===null)return r;let s=()=>({children:new Map,linkType:"HARD"});for(let[a,n]of t.entries()){if(n.linkType==="SOFT"&&K.contains(e,n.target)!==null){let f=je.getFactoryWithDefault(r,n.target,s);f.locator=a,f.linkType=n.linkType}for(let c of n.locations){let{locationRoot:f,segments:p}=cN(c,{skipPrefix:e}),h=je.getFactoryWithDefault(r,f,s);for(let E=0;E{if(process.platform==="win32"&&r==="junctions"){let s;try{s=await le.lstatPromise(t)}catch{}if(!s||s.isDirectory()){await le.symlinkPromise(t,e,"junction");return}}await le.symlinkPromise(K.relative(K.dirname(e),t),e)};async function Kxe(t,e,r){let s=K.join(t,`${tV.default.randomBytes(16).toString("hex")}.tmp`);try{await le.writeFilePromise(s,r);try{await le.linkPromise(s,e)}catch{}}finally{await le.unlinkPromise(s)}}async function RTt({srcPath:t,dstPath:e,entry:r,globalHardlinksStore:s,baseFs:a,nmMode:n}){if(r.kind==="file"){if(n.value==="hardlinks-global"&&s&&r.digest){let f=K.join(s,r.digest.substring(0,2),`${r.digest.substring(2)}.dat`),p;try{let h=await le.statPromise(f);if(h&&(!r.mtimeMs||h.mtimeMs>r.mtimeMs||h.mtimeMs{await le.mkdirPromise(t,{recursive:!0});let f=async(E=vt.dot)=>{let C=K.join(e,E),S=await r.readdirPromise(C,{withFileTypes:!0}),P=new Map;for(let I of S){let R=K.join(E,I.name),N,U=K.join(C,I.name);if(I.isFile()){if(N={kind:"file",mode:(await r.lstatPromise(U)).mode},a.value==="hardlinks-global"){let W=await Nn.checksumFile(U,{baseFs:r,algorithm:"sha1"});N.digest=W}}else if(I.isDirectory())N={kind:"directory"};else if(I.isSymbolicLink())N={kind:"symlink",symlinkTo:await r.readlinkPromise(U)};else throw new Error(`Unsupported file type (file: ${U}, mode: 0o${await r.statSync(U).mode.toString(8).padStart(6,"0")})`);if(P.set(R,N),I.isDirectory()&&R!==Ri){let W=await f(R);for(let[te,ie]of W)P.set(te,ie)}}return P},p;if(a.value==="hardlinks-global"&&s&&c){let E=K.join(s,c.substring(0,2),`${c.substring(2)}.json`);try{p=new Map(Object.entries(JSON.parse(await le.readFilePromise(E,"utf8"))))}catch{p=await f()}}else p=await f();let h=!1;for(let[E,C]of p){let S=K.join(e,E),P=K.join(t,E);if(C.kind==="directory")await le.mkdirPromise(P,{recursive:!0});else if(C.kind==="file"){let I=C.mtimeMs;await RTt({srcPath:S,dstPath:P,entry:C,nmMode:a,baseFs:r,globalHardlinksStore:s}),C.mtimeMs!==I&&(h=!0)}else C.kind==="symlink"&&await nV(K.resolve(K.dirname(P),C.symlinkTo),P,n)}if(a.value==="hardlinks-global"&&s&&h&&c){let E=K.join(s,c.substring(0,2),`${c.substring(2)}.json`);await le.removePromise(E),await Kxe(s,E,Buffer.from(JSON.stringify(Object.fromEntries(p))))}};function NTt(t,e,r,s){let a=new Map,n=new Map,c=new Map,f=!1,p=(h,E,C,S,P)=>{let I=!0,R=K.join(h,E),N=new Set;if(E===Ri||E.startsWith("@")){let W;try{W=le.statSync(R)}catch{}I=!!W,W?W.mtimeMs>r?(f=!0,N=new Set(le.readdirSync(R))):N=new Set(C.children.get(E).children.keys()):f=!0;let te=e.get(h);if(te){let ie=K.join(h,Ri,lN),Ae;try{Ae=le.statSync(ie)}catch{}if(!Ae)f=!0;else if(Ae.mtimeMs>r){f=!0;let ce=new Set(le.readdirSync(ie)),me=new Map;n.set(h,me);for(let[pe,Be]of te)ce.has(pe)&&me.set(pe,Be)}else n.set(h,te)}}else I=P.has(E);let U=C.children.get(E);if(I){let{linkType:W,locator:te}=U,ie={children:new Map,linkType:W,locator:te};if(S.children.set(E,ie),te){let Ae=je.getSetWithDefault(c,te);Ae.add(R),c.set(te,Ae)}for(let Ae of U.children.keys())p(R,Ae,U,ie,N)}else U.locator&&s.storedBuildState.delete(q.parseLocator(U.locator).locatorHash)};for(let[h,E]of t){let{linkType:C,locator:S}=E,P={children:new Map,linkType:C,locator:S};if(a.set(h,P),S){let I=je.getSetWithDefault(c,E.locator);I.add(h),c.set(E.locator,I)}E.children.has(Ri)&&p(h,Ri,E,P,new Set)}return{locationTree:a,binSymlinks:n,locatorLocations:c,installChangedByUser:f}}function Jxe(t){let e=q.parseDescriptor(t);return q.isVirtualDescriptor(e)&&(e=q.devirtualizeDescriptor(e)),e.range.startsWith("link:")}async function OTt(t,e,r,{loadManifest:s}){let a=new Map;for(let[f,{locations:p}]of t){let h=Jxe(f)?null:await s(f,p[0]),E=new Map;if(h)for(let[C,S]of h.bin){let P=K.join(p[0],S);S!==""&&le.existsSync(P)&&E.set(C,S)}a.set(f,E)}let n=new Map,c=(f,p,h)=>{let E=new Map,C=K.contains(r,f);if(h.locator&&C!==null){let S=a.get(h.locator);for(let[P,I]of S){let R=K.join(f,ue.toPortablePath(I));E.set(P,R)}for(let[P,I]of h.children){let R=K.join(f,P),N=c(R,R,I);N.size>0&&n.set(f,new Map([...n.get(f)||new Map,...N]))}}else for(let[S,P]of h.children){let I=c(K.join(f,S),p,P);for(let[R,N]of I)E.set(R,N)}return E};for(let[f,p]of e){let h=c(f,f,p);h.size>0&&n.set(f,new Map([...n.get(f)||new Map,...h]))}return n}var jxe=(t,e)=>{if(!t||!e)return t===e;let r=q.parseLocator(t);q.isVirtualLocator(r)&&(r=q.devirtualizeLocator(r));let s=q.parseLocator(e);return q.isVirtualLocator(s)&&(s=q.devirtualizeLocator(s)),q.areLocatorsEqual(r,s)};function iV(t){return K.join(t.get("globalFolder"),"store")}function LTt(t,e){let r=s=>{let a=s.split(K.sep),n=a.lastIndexOf(Ri);if(n<0||n==a.length-1)throw new Error(`Assertion failed. Path is outside of any node_modules package ${s}`);return a.slice(0,n+(a[n+1].startsWith("@")?3:2)).join(K.sep)};for(let s of t.values())for(let[a,n]of s)e.has(r(n))&&s.delete(a)}async function MTt(t,e,{baseFs:r,project:s,report:a,loadManifest:n,realLocatorChecksums:c}){let f=K.join(s.cwd,Ri),{locationTree:p,binSymlinks:h,locatorLocations:E,installChangedByUser:C}=NTt(t.locationTree,t.binSymlinks,t.mtimeMs,s),S=Vxe(e,{skipPrefix:s.cwd}),P=[],I=async({srcDir:Be,dstDir:Ce,linkType:g,globalHardlinksStore:we,nmMode:ye,windowsLinkType:fe,packageChecksum:se})=>{let X=(async()=>{try{g==="SOFT"?(await le.mkdirPromise(K.dirname(Ce),{recursive:!0}),await nV(K.resolve(Be),Ce,fe)):await FTt(Ce,Be,{baseFs:r,globalHardlinksStore:we,nmMode:ye,windowsLinkType:fe,packageChecksum:se})}catch(De){throw De.message=`While persisting ${Be} -> ${Ce} ${De.message}`,De}finally{ie.tick()}})().then(()=>P.splice(P.indexOf(X),1));P.push(X),P.length>Hxe&&await Promise.race(P)},R=async(Be,Ce,g)=>{let we=(async()=>{let ye=async(fe,se,X)=>{try{X.innerLoop||await le.mkdirPromise(se,{recursive:!0});let De=await le.readdirPromise(fe,{withFileTypes:!0});for(let Re of De){if(!X.innerLoop&&Re.name===lN)continue;let dt=K.join(fe,Re.name),j=K.join(se,Re.name);Re.isDirectory()?(Re.name!==Ri||X&&X.innerLoop)&&(await le.mkdirPromise(j,{recursive:!0}),await ye(dt,j,{...X,innerLoop:!0})):me.value==="hardlinks-local"||me.value==="hardlinks-global"?await le.linkPromise(dt,j):await le.copyFilePromise(dt,j,Gxe.default.constants.COPYFILE_FICLONE)}}catch(De){throw X.innerLoop||(De.message=`While cloning ${fe} -> ${se} ${De.message}`),De}finally{X.innerLoop||ie.tick()}};await ye(Be,Ce,g)})().then(()=>P.splice(P.indexOf(we),1));P.push(we),P.length>Hxe&&await Promise.race(P)},N=async(Be,Ce,g)=>{if(g)for(let[we,ye]of Ce.children){let fe=g.children.get(we);await N(K.join(Be,we),ye,fe)}else{Ce.children.has(Ri)&&await bw(K.join(Be,Ri),{contentsOnly:!1});let we=K.basename(Be)===Ri&&p.has(K.join(K.dirname(Be)));await bw(Be,{contentsOnly:Be===f,isWorkspaceDir:we})}};for(let[Be,Ce]of p){let g=S.get(Be);for(let[we,ye]of Ce.children){if(we===".")continue;let fe=g&&g.children.get(we),se=K.join(Be,we);await N(se,ye,fe)}}let U=async(Be,Ce,g)=>{if(g){jxe(Ce.locator,g.locator)||await bw(Be,{contentsOnly:Ce.linkType==="HARD"});for(let[we,ye]of Ce.children){let fe=g.children.get(we);await U(K.join(Be,we),ye,fe)}}else{Ce.children.has(Ri)&&await bw(K.join(Be,Ri),{contentsOnly:!0});let we=K.basename(Be)===Ri&&S.has(K.join(K.dirname(Be)));await bw(Be,{contentsOnly:Ce.linkType==="HARD",isWorkspaceDir:we})}};for(let[Be,Ce]of S){let g=p.get(Be);for(let[we,ye]of Ce.children){if(we===".")continue;let fe=g&&g.children.get(we);await U(K.join(Be,we),ye,fe)}}let W=new Map,te=[];for(let[Be,Ce]of E)for(let g of Ce){let{locationRoot:we,segments:ye}=cN(g,{skipPrefix:s.cwd}),fe=S.get(we),se=we;if(fe){for(let X of ye)if(se=K.join(se,X),fe=fe.children.get(X),!fe)break;if(fe){let X=jxe(fe.locator,Be),De=e.get(fe.locator),Re=De.target,dt=se,j=De.linkType;if(X)W.has(Re)||W.set(Re,dt);else if(Re!==dt){let rt=q.parseLocator(fe.locator);q.isVirtualLocator(rt)&&(rt=q.devirtualizeLocator(rt)),te.push({srcDir:Re,dstDir:dt,linkType:j,realLocatorHash:rt.locatorHash})}}}}for(let[Be,{locations:Ce}]of e.entries())for(let g of Ce){let{locationRoot:we,segments:ye}=cN(g,{skipPrefix:s.cwd}),fe=p.get(we),se=S.get(we),X=we,De=e.get(Be),Re=q.parseLocator(Be);q.isVirtualLocator(Re)&&(Re=q.devirtualizeLocator(Re));let dt=Re.locatorHash,j=De.target,rt=g;if(j===rt)continue;let Fe=De.linkType;for(let Ne of ye)se=se.children.get(Ne);if(!fe)te.push({srcDir:j,dstDir:rt,linkType:Fe,realLocatorHash:dt});else for(let Ne of ye)if(X=K.join(X,Ne),fe=fe.children.get(Ne),!fe){te.push({srcDir:j,dstDir:rt,linkType:Fe,realLocatorHash:dt});break}}let ie=ho.progressViaCounter(te.length),Ae=a.reportProgress(ie),ce=s.configuration.get("nmMode"),me={value:ce},pe=s.configuration.get("winLinkType");try{let Be=me.value==="hardlinks-global"?`${iV(s.configuration)}/v1`:null;if(Be&&!await le.existsPromise(Be)){await le.mkdirpPromise(Be);for(let g=0;g<256;g++)await le.mkdirPromise(K.join(Be,g.toString(16).padStart(2,"0")))}for(let g of te)(g.linkType==="SOFT"||!W.has(g.srcDir))&&(W.set(g.srcDir,g.dstDir),await I({...g,globalHardlinksStore:Be,nmMode:me,windowsLinkType:pe,packageChecksum:c.get(g.realLocatorHash)||null}));await Promise.all(P),P.length=0;for(let g of te){let we=W.get(g.srcDir);g.linkType!=="SOFT"&&g.dstDir!==we&&await R(we,g.dstDir,{nmMode:me})}await Promise.all(P),await le.mkdirPromise(f,{recursive:!0}),LTt(h,new Set(te.map(g=>g.dstDir)));let Ce=await OTt(e,S,s.cwd,{loadManifest:n});await _Tt(h,Ce,s.cwd,pe),await TTt(s,e,Ce,me,{installChangedByUser:C}),ce=="hardlinks-global"&&me.value=="hardlinks-local"&&a.reportWarningOnce(74,"'nmMode' has been downgraded to 'hardlinks-local' due to global cache and install folder being on different devices")}finally{Ae.stop()}}async function _Tt(t,e,r,s){for(let a of t.keys()){if(K.contains(r,a)===null)throw new Error(`Assertion failed. Excepted bin symlink location to be inside project dir, instead it was at ${a}`);if(!e.has(a)){let n=K.join(a,Ri,lN);await le.removePromise(n)}}for(let[a,n]of e){if(K.contains(r,a)===null)throw new Error(`Assertion failed. Excepted bin symlink location to be inside project dir, instead it was at ${a}`);let c=K.join(a,Ri,lN),f=t.get(a)||new Map;await le.mkdirPromise(c,{recursive:!0});for(let p of f.keys())n.has(p)||(await le.removePromise(K.join(c,p)),process.platform==="win32"&&await le.removePromise(K.join(c,`${p}.cmd`)));for(let[p,h]of n){let E=f.get(p),C=K.join(c,p);E!==h&&(process.platform==="win32"?await(0,qxe.default)(ue.fromPortablePath(h),ue.fromPortablePath(C),{createPwshFile:!1}):(await le.removePromise(C),await nV(h,C,s),K.contains(r,await le.realpathPromise(h))!==null&&await le.chmodPromise(h,493)))}}}Ve();bt();rA();var $D=class extends ig{constructor(){super(...arguments);this.mode="loose"}makeInstaller(r){return new sV(r)}},sV=class extends jm{constructor(){super(...arguments);this.mode="loose"}async transformPnpSettings(r){let s=new Ao({baseFs:new tA({maxOpenFiles:80,readOnlyArchives:!0})}),a=xxe(r,this.opts.project.cwd,s),{tree:n,errors:c}=UD(a,{pnpifyFs:!1,project:this.opts.project});if(!n){for(let{messageName:C,text:S}of c)this.opts.report.reportError(C,S);return}let f=new Map;r.fallbackPool=f;let p=(C,S)=>{let P=q.parseLocator(S.locator),I=q.stringifyIdent(P);I===C?f.set(C,P.reference):f.set(C,[I,P.reference])},h=K.join(this.opts.project.cwd,Er.nodeModules),E=n.get(h);if(!(typeof E>"u")){if("target"in E)throw new Error("Assertion failed: Expected the root junction point to be a directory");for(let C of E.dirList){let S=K.join(h,C),P=n.get(S);if(typeof P>"u")throw new Error("Assertion failed: Expected the child to have been registered");if("target"in P)p(C,P);else for(let I of P.dirList){let R=K.join(S,I),N=n.get(R);if(typeof N>"u")throw new Error("Assertion failed: Expected the subchild to have been registered");if("target"in N)p(`${C}/${I}`,N);else throw new Error("Assertion failed: Expected the leaf junction to be a package")}}}}};var UTt={hooks:{cleanGlobalArtifacts:async t=>{let e=iV(t);await le.removePromise(e)}},configuration:{nmHoistingLimits:{description:"Prevents packages to be hoisted past specific levels",type:"STRING",values:["workspaces","dependencies","none"],default:"none"},nmMode:{description:"Defines in which measure Yarn must use hardlinks and symlinks when generated `node_modules` directories.",type:"STRING",values:["classic","hardlinks-local","hardlinks-global"],default:"classic"},nmSelfReferences:{description:"Defines whether the linker should generate self-referencing symlinks for workspaces.",type:"BOOLEAN",default:!0}},linkers:[XD,$D]},HTt=UTt;var oz={};Vt(oz,{NpmHttpFetcher:()=>rb,NpmRemapResolver:()=>nb,NpmSemverFetcher:()=>lh,NpmSemverResolver:()=>ib,NpmTagResolver:()=>sb,default:()=>ZHt,npmConfigUtils:()=>hi,npmHttpUtils:()=>an,npmPublishUtils:()=>B1});Ve();var nke=et(Ai());var oi="npm:";var an={};Vt(an,{AuthType:()=>eke,customPackageError:()=>qm,del:()=>tRt,get:()=>Gm,getIdentUrl:()=>uN,getPackageMetadata:()=>kw,handleInvalidAuthenticationError:()=>og,post:()=>$Tt,put:()=>eRt});Ve();Ve();bt();var cV=et(nS()),Xxe=et(mG()),$xe=et(Ai());var hi={};Vt(hi,{RegistryType:()=>zxe,getAuditRegistry:()=>jTt,getAuthConfiguration:()=>lV,getDefaultRegistry:()=>eb,getPublishRegistry:()=>qTt,getRegistryConfiguration:()=>Zxe,getScopeConfiguration:()=>aV,getScopeRegistry:()=>Pw,normalizeRegistry:()=>zc});var zxe=(s=>(s.AUDIT_REGISTRY="npmAuditRegistry",s.FETCH_REGISTRY="npmRegistryServer",s.PUBLISH_REGISTRY="npmPublishRegistry",s))(zxe||{});function zc(t){return t.replace(/\/$/,"")}function jTt({configuration:t}){return eb({configuration:t,type:"npmAuditRegistry"})}function qTt(t,{configuration:e}){return t.publishConfig?.registry?zc(t.publishConfig.registry):t.name?Pw(t.name.scope,{configuration:e,type:"npmPublishRegistry"}):eb({configuration:e,type:"npmPublishRegistry"})}function Pw(t,{configuration:e,type:r="npmRegistryServer"}){let s=aV(t,{configuration:e});if(s===null)return eb({configuration:e,type:r});let a=s.get(r);return a===null?eb({configuration:e,type:r}):zc(a)}function eb({configuration:t,type:e="npmRegistryServer"}){let r=t.get(e);return zc(r!==null?r:t.get("npmRegistryServer"))}function Zxe(t,{configuration:e}){let r=e.get("npmRegistries"),s=zc(t),a=r.get(s);if(typeof a<"u")return a;let n=r.get(s.replace(/^[a-z]+:/,""));return typeof n<"u"?n:null}var GTt=new Map([["npmRegistryServer","https://npm.jsr.io/"]]);function aV(t,{configuration:e}){if(t===null)return null;let s=e.get("npmScopes").get(t);return s||(t==="jsr"?GTt:null)}function lV(t,{configuration:e,ident:r}){let s=r&&aV(r.scope,{configuration:e});return s?.get("npmAuthIdent")||s?.get("npmAuthToken")?s:Zxe(t,{configuration:e})||e}var eke=(a=>(a[a.NO_AUTH=0]="NO_AUTH",a[a.BEST_EFFORT=1]="BEST_EFFORT",a[a.CONFIGURATION=2]="CONFIGURATION",a[a.ALWAYS_AUTH=3]="ALWAYS_AUTH",a))(eke||{});async function og(t,{attemptedAs:e,registry:r,headers:s,configuration:a}){if(AN(t))throw new Yt(41,"Invalid OTP token");if(t.originalError?.name==="HTTPError"&&t.originalError?.response.statusCode===401)throw new Yt(41,`Invalid authentication (${typeof e!="string"?`as ${await nRt(r,s,{configuration:a})}`:`attempted as ${e}`})`)}function qm(t,e){let r=t.response?.statusCode;return r?r===404?"Package not found":r>=500&&r<600?`The registry appears to be down (using a ${he.applyHyperlink(e,"local cache","https://yarnpkg.com/advanced/lexicon#local-cache")} might have protected you against such outages)`:null:null}function uN(t){return t.scope?`/@${t.scope}%2f${t.name}`:`/${t.name}`}var tke=new Map,WTt=new Map;async function YTt(t){return await je.getFactoryWithDefault(tke,t,async()=>{let e=null;try{e=await le.readJsonPromise(t)}catch{}return e})}async function VTt(t,e,{configuration:r,cached:s,registry:a,headers:n,version:c,...f}){return await je.getFactoryWithDefault(WTt,t,async()=>await Gm(uN(e),{...f,customErrorMessage:qm,configuration:r,registry:a,ident:e,headers:{...n,"If-None-Match":s?.etag,"If-Modified-Since":s?.lastModified},wrapNetworkRequest:async p=>async()=>{let h=await p();if(h.statusCode===304){if(s===null)throw new Error("Assertion failed: cachedMetadata should not be null");return{...h,body:s.metadata}}let E=JTt(JSON.parse(h.body.toString())),C={metadata:E,etag:h.headers.etag,lastModified:h.headers["last-modified"]};return tke.set(t,Promise.resolve(C)),Promise.resolve().then(async()=>{let S=`${t}-${process.pid}.tmp`;await le.mkdirPromise(K.dirname(S),{recursive:!0}),await le.writeJsonPromise(S,C,{compact:!0}),await le.renamePromise(S,t)}).catch(()=>{}),{...h,body:E}}}))}function KTt(t){return t.scope!==null?`@${t.scope}-${t.name}-${t.scope.length}`:t.name}async function kw(t,{cache:e,project:r,registry:s,headers:a,version:n,...c}){let{configuration:f}=r;s=tb(f,{ident:t,registry:s});let p=ZTt(f,s),h=K.join(p,`${KTt(t)}.json`),E=null;if(!r.lockfileNeedsRefresh&&(E=await YTt(h),E)){if(typeof n<"u"&&typeof E.metadata.versions[n]<"u")return E.metadata;if(f.get("enableOfflineMode")){let C=structuredClone(E.metadata),S=new Set;if(e){for(let I of Object.keys(C.versions)){let R=q.makeLocator(t,`npm:${I}`),N=e.getLocatorMirrorPath(R);(!N||!le.existsSync(N))&&(delete C.versions[I],S.add(I))}let P=C["dist-tags"].latest;if(S.has(P)){let I=Object.keys(E.metadata.versions).sort($xe.default.compare),R=I.indexOf(P);for(;S.has(I[R])&&R>=0;)R-=1;R>=0?C["dist-tags"].latest=I[R]:delete C["dist-tags"].latest}}return C}}return await VTt(h,t,{...c,configuration:f,cached:E,registry:s,headers:a,version:n})}var rke=["name","dist.tarball","bin","scripts","os","cpu","libc","dependencies","dependenciesMeta","optionalDependencies","peerDependencies","peerDependenciesMeta","deprecated"];function JTt(t){return{"dist-tags":t["dist-tags"],versions:Object.fromEntries(Object.entries(t.versions).map(([e,r])=>[e,(0,Xxe.default)(r,rke)]))}}var zTt=Nn.makeHash(...rke).slice(0,6);function ZTt(t,e){let r=XTt(t),s=new URL(e);return K.join(r,zTt,s.hostname)}function XTt(t){return K.join(t.get("globalFolder"),"metadata/npm")}async function Gm(t,{configuration:e,headers:r,ident:s,authType:a,registry:n,...c}){n=tb(e,{ident:s,registry:n}),s&&s.scope&&typeof a>"u"&&(a=1);let f=await fN(n,{authType:a,configuration:e,ident:s});f&&(r={...r,authorization:f});try{return await An.get(t.charAt(0)==="/"?`${n}${t}`:t,{configuration:e,headers:r,...c})}catch(p){throw await og(p,{registry:n,configuration:e,headers:r}),p}}async function $Tt(t,e,{attemptedAs:r,configuration:s,headers:a,ident:n,authType:c=3,registry:f,otp:p,...h}){f=tb(s,{ident:n,registry:f});let E=await fN(f,{authType:c,configuration:s,ident:n});E&&(a={...a,authorization:E}),p&&(a={...a,...xw(p)});try{return await An.post(f+t,e,{configuration:s,headers:a,...h})}catch(C){if(!AN(C)||p)throw await og(C,{attemptedAs:r,registry:f,configuration:s,headers:a}),C;p=await uV(C,{configuration:s});let S={...a,...xw(p)};try{return await An.post(`${f}${t}`,e,{configuration:s,headers:S,...h})}catch(P){throw await og(P,{attemptedAs:r,registry:f,configuration:s,headers:a}),P}}}async function eRt(t,e,{attemptedAs:r,configuration:s,headers:a,ident:n,authType:c=3,registry:f,otp:p,...h}){f=tb(s,{ident:n,registry:f});let E=await fN(f,{authType:c,configuration:s,ident:n});E&&(a={...a,authorization:E}),p&&(a={...a,...xw(p)});try{return await An.put(f+t,e,{configuration:s,headers:a,...h})}catch(C){if(!AN(C))throw await og(C,{attemptedAs:r,registry:f,configuration:s,headers:a}),C;p=await uV(C,{configuration:s});let S={...a,...xw(p)};try{return await An.put(`${f}${t}`,e,{configuration:s,headers:S,...h})}catch(P){throw await og(P,{attemptedAs:r,registry:f,configuration:s,headers:a}),P}}}async function tRt(t,{attemptedAs:e,configuration:r,headers:s,ident:a,authType:n=3,registry:c,otp:f,...p}){c=tb(r,{ident:a,registry:c});let h=await fN(c,{authType:n,configuration:r,ident:a});h&&(s={...s,authorization:h}),f&&(s={...s,...xw(f)});try{return await An.del(c+t,{configuration:r,headers:s,...p})}catch(E){if(!AN(E)||f)throw await og(E,{attemptedAs:e,registry:c,configuration:r,headers:s}),E;f=await uV(E,{configuration:r});let C={...s,...xw(f)};try{return await An.del(`${c}${t}`,{configuration:r,headers:C,...p})}catch(S){throw await og(S,{attemptedAs:e,registry:c,configuration:r,headers:s}),S}}}function tb(t,{ident:e,registry:r}){if(typeof r>"u"&&e)return Pw(e.scope,{configuration:t});if(typeof r!="string")throw new Error("Assertion failed: The registry should be a string");return zc(r)}async function fN(t,{authType:e=2,configuration:r,ident:s}){let a=lV(t,{configuration:r,ident:s}),n=rRt(a,e);if(!n)return null;let c=await r.reduceHook(f=>f.getNpmAuthenticationHeader,void 0,t,{configuration:r,ident:s});if(c)return c;if(a.get("npmAuthToken"))return`Bearer ${a.get("npmAuthToken")}`;if(a.get("npmAuthIdent")){let f=a.get("npmAuthIdent");return f.includes(":")?`Basic ${Buffer.from(f).toString("base64")}`:`Basic ${f}`}if(n&&e!==1)throw new Yt(33,"No authentication configured for request");return null}function rRt(t,e){switch(e){case 2:return t.get("npmAlwaysAuth");case 1:case 3:return!0;case 0:return!1;default:throw new Error("Unreachable")}}async function nRt(t,e,{configuration:r}){if(typeof e>"u"||typeof e.authorization>"u")return"an anonymous user";try{return(await An.get(new URL(`${t}/-/whoami`).href,{configuration:r,headers:e,jsonResponse:!0})).username??"an unknown user"}catch{return"an unknown user"}}async function uV(t,{configuration:e}){let r=t.originalError?.response.headers["npm-notice"];if(r&&(await Ot.start({configuration:e,stdout:process.stdout,includeFooter:!1},async a=>{if(a.reportInfo(0,r.replace(/(https?:\/\/\S+)/g,he.pretty(e,"$1",he.Type.URL))),!process.env.YARN_IS_TEST_ENV){let n=r.match(/open (https?:\/\/\S+)/i);if(n&&ps.openUrl){let{openNow:c}=await(0,cV.prompt)({type:"confirm",name:"openNow",message:"Do you want to try to open this url now?",required:!0,initial:!0,onCancel:()=>process.exit(130)});c&&(await ps.openUrl(n[1])||(a.reportSeparator(),a.reportWarning(0,"We failed to automatically open the url; you'll have to open it yourself in your browser of choice.")))}}}),process.stdout.write(` `)),process.env.YARN_IS_TEST_ENV)return process.env.YARN_INJECT_NPM_2FA_TOKEN||"";let{otp:s}=await(0,cV.prompt)({type:"password",name:"otp",message:"One-time password:",required:!0,onCancel:()=>process.exit(130)});return process.stdout.write(` `),s}function AN(t){if(t.originalError?.name!=="HTTPError")return!1;try{return(t.originalError?.response.headers["www-authenticate"].split(/,\s*/).map(r=>r.toLowerCase())).includes("otp")}catch{return!1}}function xw(t){return{"npm-otp":t}}var rb=class{supports(e,r){if(!e.reference.startsWith(oi))return!1;let{selector:s,params:a}=q.parseRange(e.reference);return!(!nke.default.valid(s)||a===null||typeof a.__archiveUrl!="string")}getLocalPath(e,r){return null}async fetch(e,r){let s=r.checksums.get(e.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(e,s,{onHit:()=>r.report.reportCacheHit(e),onMiss:()=>r.report.reportCacheMiss(e,`${q.prettyLocator(r.project.configuration,e)} can't be found in the cache and will be fetched from the remote server`),loader:()=>this.fetchFromNetwork(e,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:q.getIdentVendorPath(e),checksum:c}}async fetchFromNetwork(e,r){let{params:s}=q.parseRange(e.reference);if(s===null||typeof s.__archiveUrl!="string")throw new Error("Assertion failed: The archiveUrl querystring parameter should have been available");let a=await Gm(s.__archiveUrl,{customErrorMessage:qm,configuration:r.project.configuration,ident:e});return await gs.convertToZip(a,{configuration:r.project.configuration,prefixPath:q.getIdentVendorPath(e),stripComponents:1})}};Ve();var nb=class{supportsDescriptor(e,r){return!(!e.range.startsWith(oi)||!q.tryParseDescriptor(e.range.slice(oi.length),!0))}supportsLocator(e,r){return!1}shouldPersistResolution(e,r){throw new Error("Unreachable")}bindDescriptor(e,r,s){return e}getResolutionDependencies(e,r){let s=r.project.configuration.normalizeDependency(q.parseDescriptor(e.range.slice(oi.length),!0));return r.resolver.getResolutionDependencies(s,r)}async getCandidates(e,r,s){let a=s.project.configuration.normalizeDependency(q.parseDescriptor(e.range.slice(oi.length),!0));return await s.resolver.getCandidates(a,r,s)}async getSatisfying(e,r,s,a){let n=a.project.configuration.normalizeDependency(q.parseDescriptor(e.range.slice(oi.length),!0));return a.resolver.getSatisfying(n,r,s,a)}resolve(e,r){throw new Error("Unreachable")}};Ve();Ve();var ike=et(Ai());var lh=class t{supports(e,r){if(!e.reference.startsWith(oi))return!1;let s=new URL(e.reference);return!(!ike.default.valid(s.pathname)||s.searchParams.has("__archiveUrl"))}getLocalPath(e,r){return null}async fetch(e,r){let s=r.checksums.get(e.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(e,s,{onHit:()=>r.report.reportCacheHit(e),onMiss:()=>r.report.reportCacheMiss(e,`${q.prettyLocator(r.project.configuration,e)} can't be found in the cache and will be fetched from the remote registry`),loader:()=>this.fetchFromNetwork(e,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:q.getIdentVendorPath(e),checksum:c}}async fetchFromNetwork(e,r){let s;try{s=await Gm(t.getLocatorUrl(e),{customErrorMessage:qm,configuration:r.project.configuration,ident:e})}catch{s=await Gm(t.getLocatorUrl(e).replace(/%2f/g,"/"),{customErrorMessage:qm,configuration:r.project.configuration,ident:e})}return await gs.convertToZip(s,{configuration:r.project.configuration,prefixPath:q.getIdentVendorPath(e),stripComponents:1})}static isConventionalTarballUrl(e,r,{configuration:s}){let a=Pw(e.scope,{configuration:s}),n=t.getLocatorUrl(e);return r=r.replace(/^https?:(\/\/(?:[^/]+\.)?npmjs.org(?:$|\/))/,"https:$1"),a=a.replace(/^https:\/\/registry\.npmjs\.org($|\/)/,"https://registry.yarnpkg.com$1"),r=r.replace(/^https:\/\/registry\.npmjs\.org($|\/)/,"https://registry.yarnpkg.com$1"),r===a+n||r===a+n.replace(/%2f/g,"/")}static getLocatorUrl(e){let r=Or.clean(e.reference.slice(oi.length));if(r===null)throw new Yt(10,"The npm semver resolver got selected, but the version isn't semver");return`${uN(e)}/-/${e.name}-${r}.tgz`}};Ve();Ve();Ve();var fV=et(Ai());var pN=q.makeIdent(null,"node-gyp"),iRt=/\b(node-gyp|prebuild-install)\b/,ib=class{supportsDescriptor(e,r){return e.range.startsWith(oi)?!!Or.validRange(e.range.slice(oi.length)):!1}supportsLocator(e,r){if(!e.reference.startsWith(oi))return!1;let{selector:s}=q.parseRange(e.reference);return!!fV.default.valid(s)}shouldPersistResolution(e,r){return!0}bindDescriptor(e,r,s){return e}getResolutionDependencies(e,r){return{}}async getCandidates(e,r,s){let a=Or.validRange(e.range.slice(oi.length));if(a===null)throw new Error(`Expected a valid range, got ${e.range.slice(oi.length)}`);let n=await kw(e,{cache:s.fetchOptions?.cache,project:s.project,version:fV.default.valid(a.raw)?a.raw:void 0}),c=je.mapAndFilter(Object.keys(n.versions),h=>{try{let E=new Or.SemVer(h);if(a.test(E))return E}catch{}return je.mapAndFilter.skip}),f=c.filter(h=>!n.versions[h.raw].deprecated),p=f.length>0?f:c;return p.sort((h,E)=>-h.compare(E)),p.map(h=>{let E=q.makeLocator(e,`${oi}${h.raw}`),C=n.versions[h.raw].dist.tarball;return lh.isConventionalTarballUrl(E,C,{configuration:s.project.configuration})?E:q.bindLocator(E,{__archiveUrl:C})})}async getSatisfying(e,r,s,a){let n=Or.validRange(e.range.slice(oi.length));if(n===null)throw new Error(`Expected a valid range, got ${e.range.slice(oi.length)}`);return{locators:je.mapAndFilter(s,p=>{if(p.identHash!==e.identHash)return je.mapAndFilter.skip;let h=q.tryParseRange(p.reference,{requireProtocol:oi});if(!h)return je.mapAndFilter.skip;let E=new Or.SemVer(h.selector);return n.test(E)?{locator:p,version:E}:je.mapAndFilter.skip}).sort((p,h)=>-p.version.compare(h.version)).map(({locator:p})=>p),sorted:!0}}async resolve(e,r){let{selector:s}=q.parseRange(e.reference),a=Or.clean(s);if(a===null)throw new Yt(10,"The npm semver resolver got selected, but the version isn't semver");let n=await kw(e,{cache:r.fetchOptions?.cache,project:r.project,version:a});if(!Object.hasOwn(n,"versions"))throw new Yt(15,'Registry returned invalid data for - missing "versions" field');if(!Object.hasOwn(n.versions,a))throw new Yt(16,`Registry failed to return reference "${a}"`);let c=new Ht;if(c.load(n.versions[a]),!c.dependencies.has(pN.identHash)&&!c.peerDependencies.has(pN.identHash)){for(let f of c.scripts.values())if(f.match(iRt)){c.dependencies.set(pN.identHash,q.makeDescriptor(pN,"latest"));break}}return{...e,version:a,languageName:"node",linkType:"HARD",conditions:c.getConditions(),dependencies:r.project.configuration.normalizeDependencyMap(c.dependencies),peerDependencies:c.peerDependencies,dependenciesMeta:c.dependenciesMeta,peerDependenciesMeta:c.peerDependenciesMeta,bin:c.bin}}};Ve();Ve();var ske=et(Ai());var sb=class{supportsDescriptor(e,r){return!(!e.range.startsWith(oi)||!Up.test(e.range.slice(oi.length)))}supportsLocator(e,r){return!1}shouldPersistResolution(e,r){throw new Error("Unreachable")}bindDescriptor(e,r,s){return e}getResolutionDependencies(e,r){return{}}async getCandidates(e,r,s){let a=e.range.slice(oi.length),n=await kw(e,{cache:s.fetchOptions?.cache,project:s.project});if(!Object.hasOwn(n,"dist-tags"))throw new Yt(15,'Registry returned invalid data - missing "dist-tags" field');let c=n["dist-tags"];if(!Object.hasOwn(c,a))throw new Yt(16,`Registry failed to return tag "${a}"`);let f=c[a],p=q.makeLocator(e,`${oi}${f}`),h=n.versions[f].dist.tarball;return lh.isConventionalTarballUrl(p,h,{configuration:s.project.configuration})?[p]:[q.bindLocator(p,{__archiveUrl:h})]}async getSatisfying(e,r,s,a){let n=[];for(let c of s){if(c.identHash!==e.identHash)continue;let f=q.tryParseRange(c.reference,{requireProtocol:oi});if(!(!f||!ske.default.valid(f.selector))){if(f.params?.__archiveUrl){let p=q.makeRange({protocol:oi,selector:f.selector,source:null,params:null}),[h]=await a.resolver.getCandidates(q.makeDescriptor(e,p),r,a);if(c.reference!==h.reference)continue}n.push(c)}}return{locators:n,sorted:!1}}async resolve(e,r){throw new Error("Unreachable")}};var B1={};Vt(B1,{getGitHead:()=>JHt,getPublishAccess:()=>JNe,getReadmeContent:()=>zNe,makePublishBody:()=>KHt});Ve();Ve();bt();var $V={};Vt($V,{PackCommand:()=>Hw,default:()=>LNt,packUtils:()=>IA});Ve();Ve();Ve();bt();Wt();var IA={};Vt(IA,{genPackList:()=>ON,genPackStream:()=>XV,genPackageManifest:()=>OQe,hasPackScripts:()=>zV,prepareForPack:()=>ZV});Ve();bt();var JV=et(Sa()),FQe=et(kQe()),NQe=Ie("zlib"),DNt=["/package.json","/readme","/readme.*","/license","/license.*","/licence","/licence.*","/changelog","/changelog.*"],bNt=["/package.tgz",".github",".git",".hg","node_modules",".npmignore",".gitignore",".#*",".DS_Store"];async function zV(t){return!!(In.hasWorkspaceScript(t,"prepack")||In.hasWorkspaceScript(t,"postpack"))}async function ZV(t,{report:e},r){await In.maybeExecuteWorkspaceLifecycleScript(t,"prepack",{report:e});try{let s=K.join(t.cwd,Ht.fileName);await le.existsPromise(s)&&await t.manifest.loadFile(s,{baseFs:le}),await r()}finally{await In.maybeExecuteWorkspaceLifecycleScript(t,"postpack",{report:e})}}async function XV(t,e){typeof e>"u"&&(e=await ON(t));let r=new Set;for(let n of t.manifest.publishConfig?.executableFiles??new Set)r.add(K.normalize(n));for(let n of t.manifest.bin.values())r.add(K.normalize(n));let s=FQe.default.pack();process.nextTick(async()=>{for(let n of e){let c=K.normalize(n),f=K.resolve(t.cwd,c),p=K.join("package",c),h=await le.lstatPromise(f),E={name:p,mtime:new Date(fi.SAFE_TIME*1e3)},C=r.has(c)?493:420,S,P,I=new Promise((N,U)=>{S=N,P=U}),R=N=>{N?P(N):S()};if(h.isFile()){let N;c==="package.json"?N=Buffer.from(JSON.stringify(await OQe(t),null,2)):N=await le.readFilePromise(f),s.entry({...E,mode:C,type:"file"},N,R)}else h.isSymbolicLink()?s.entry({...E,mode:C,type:"symlink",linkname:await le.readlinkPromise(f)},R):R(new Error(`Unsupported file type ${h.mode} for ${ue.fromPortablePath(c)}`));await I}s.finalize()});let a=(0,NQe.createGzip)();return s.pipe(a),a}async function OQe(t){let e=JSON.parse(JSON.stringify(t.manifest.raw));return await t.project.configuration.triggerHook(r=>r.beforeWorkspacePacking,t,e),e}async function ON(t){let e=t.project,r=e.configuration,s={accept:[],reject:[]};for(let C of bNt)s.reject.push(C);for(let C of DNt)s.accept.push(C);s.reject.push(r.get("rcFilename"));let a=C=>{if(C===null||!C.startsWith(`${t.cwd}/`))return;let S=K.relative(t.cwd,C),P=K.resolve(vt.root,S);s.reject.push(P)};a(K.resolve(e.cwd,Er.lockfile)),a(r.get("cacheFolder")),a(r.get("globalFolder")),a(r.get("installStatePath")),a(r.get("virtualFolder")),a(r.get("yarnPath")),await r.triggerHook(C=>C.populateYarnPaths,e,C=>{a(C)});for(let C of e.workspaces){let S=K.relative(t.cwd,C.cwd);S!==""&&!S.match(/^(\.\.)?\//)&&s.reject.push(`/${S}`)}let n={accept:[],reject:[]},c=t.manifest.publishConfig?.main??t.manifest.main,f=t.manifest.publishConfig?.module??t.manifest.module,p=t.manifest.publishConfig?.browser??t.manifest.browser,h=t.manifest.publishConfig?.bin??t.manifest.bin;c!=null&&n.accept.push(K.resolve(vt.root,c)),f!=null&&n.accept.push(K.resolve(vt.root,f)),typeof p=="string"&&n.accept.push(K.resolve(vt.root,p));for(let C of h.values())n.accept.push(K.resolve(vt.root,C));if(p instanceof Map)for(let[C,S]of p.entries())n.accept.push(K.resolve(vt.root,C)),typeof S=="string"&&n.accept.push(K.resolve(vt.root,S));let E=t.manifest.files!==null;if(E){n.reject.push("/*");for(let C of t.manifest.files)LQe(n.accept,C,{cwd:vt.root})}return await PNt(t.cwd,{hasExplicitFileList:E,globalList:s,ignoreList:n})}async function PNt(t,{hasExplicitFileList:e,globalList:r,ignoreList:s}){let a=[],n=new jf(t),c=[[vt.root,[s]]];for(;c.length>0;){let[f,p]=c.pop(),h=await n.lstatPromise(f);if(!TQe(f,{globalList:r,ignoreLists:h.isDirectory()?null:p}))if(h.isDirectory()){let E=await n.readdirPromise(f),C=!1,S=!1;if(!e||f!==vt.root)for(let R of E)C=C||R===".gitignore",S=S||R===".npmignore";let P=S?await QQe(n,f,".npmignore"):C?await QQe(n,f,".gitignore"):null,I=P!==null?[P].concat(p):p;TQe(f,{globalList:r,ignoreLists:p})&&(I=[...p,{accept:[],reject:["**/*"]}]);for(let R of E)c.push([K.resolve(f,R),I])}else(h.isFile()||h.isSymbolicLink())&&a.push(K.relative(vt.root,f))}return a.sort()}async function QQe(t,e,r){let s={accept:[],reject:[]},a=await t.readFilePromise(K.join(e,r),"utf8");for(let n of a.split(/\n/g))LQe(s.reject,n,{cwd:e});return s}function xNt(t,{cwd:e}){let r=t[0]==="!";return r&&(t=t.slice(1)),t.match(/\.{0,1}\//)&&(t=K.resolve(e,t)),r&&(t=`!${t}`),t}function LQe(t,e,{cwd:r}){let s=e.trim();s===""||s[0]==="#"||t.push(xNt(s,{cwd:r}))}function TQe(t,{globalList:e,ignoreLists:r}){let s=NN(t,e.accept);if(s!==0)return s===2;let a=NN(t,e.reject);if(a!==0)return a===1;if(r!==null)for(let n of r){let c=NN(t,n.accept);if(c!==0)return c===2;let f=NN(t,n.reject);if(f!==0)return f===1}return!1}function NN(t,e){let r=e,s=[];for(let a=0;a{await ZV(a,{report:p},async()=>{p.reportJson({base:ue.fromPortablePath(a.cwd)});let h=await ON(a);for(let E of h)p.reportInfo(null,ue.fromPortablePath(E)),p.reportJson({location:ue.fromPortablePath(E)});if(!this.dryRun){let E=await XV(a,h);await le.mkdirPromise(K.dirname(c),{recursive:!0});let C=le.createWriteStream(c);E.pipe(C),await new Promise(S=>{C.on("finish",S)})}}),this.dryRun||(p.reportInfo(0,`Package archive generated in ${he.pretty(r,c,he.Type.PATH)}`),p.reportJson({output:ue.fromPortablePath(c)}))})).exitCode()}};function kNt(t,{workspace:e}){let r=t.replace("%s",QNt(e)).replace("%v",TNt(e));return ue.toPortablePath(r)}function QNt(t){return t.manifest.name!==null?q.slugifyIdent(t.manifest.name):"package"}function TNt(t){return t.manifest.version!==null?t.manifest.version:"unknown"}var RNt=["dependencies","devDependencies","peerDependencies"],FNt="workspace:",NNt=(t,e)=>{e.publishConfig&&(e.publishConfig.type&&(e.type=e.publishConfig.type),e.publishConfig.main&&(e.main=e.publishConfig.main),e.publishConfig.browser&&(e.browser=e.publishConfig.browser),e.publishConfig.module&&(e.module=e.publishConfig.module),e.publishConfig.exports&&(e.exports=e.publishConfig.exports),e.publishConfig.imports&&(e.imports=e.publishConfig.imports),e.publishConfig.bin&&(e.bin=e.publishConfig.bin));let r=t.project;for(let s of RNt)for(let a of t.manifest.getForScope(s).values()){let n=r.tryWorkspaceByDescriptor(a),c=q.parseRange(a.range);if(c.protocol===FNt)if(n===null){if(r.tryWorkspaceByIdent(a)===null)throw new Yt(21,`${q.prettyDescriptor(r.configuration,a)}: No local workspace found for this range`)}else{let f;q.areDescriptorsEqual(a,n.anchoredDescriptor)||c.selector==="*"?f=n.manifest.version??"0.0.0":c.selector==="~"||c.selector==="^"?f=`${c.selector}${n.manifest.version??"0.0.0"}`:f=c.selector;let p=s==="dependencies"?q.makeDescriptor(a,"unknown"):null,h=p!==null&&t.manifest.ensureDependencyMeta(p).optional?"optionalDependencies":s;e[h][q.stringifyIdent(a)]=f}}},ONt={hooks:{beforeWorkspacePacking:NNt},commands:[Hw]},LNt=ONt;var KNe=et(YQe());Ve();var YNe=et(WNe()),{env:Bt}=process,_Ht="application/vnd.in-toto+json",UHt="https://in-toto.io/Statement/v0.1",HHt="https://in-toto.io/Statement/v1",jHt="https://slsa.dev/provenance/v0.2",qHt="https://slsa.dev/provenance/v1",GHt="https://github.com/actions/runner",WHt="https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",YHt="https://github.com/npm/cli/gitlab",VHt="v0alpha1",VNe=async(t,e)=>{let r;if(Bt.GITHUB_ACTIONS){if(!Bt.ACTIONS_ID_TOKEN_REQUEST_URL)throw new Yt(91,'Provenance generation in GitHub Actions requires "write" access to the "id-token" permission');let s=(Bt.GITHUB_WORKFLOW_REF||"").replace(`${Bt.GITHUB_REPOSITORY}/`,""),a=s.indexOf("@"),n=s.slice(0,a),c=s.slice(a+1);r={_type:HHt,subject:t,predicateType:qHt,predicate:{buildDefinition:{buildType:WHt,externalParameters:{workflow:{ref:c,repository:`${Bt.GITHUB_SERVER_URL}/${Bt.GITHUB_REPOSITORY}`,path:n}},internalParameters:{github:{event_name:Bt.GITHUB_EVENT_NAME,repository_id:Bt.GITHUB_REPOSITORY_ID,repository_owner_id:Bt.GITHUB_REPOSITORY_OWNER_ID}},resolvedDependencies:[{uri:`git+${Bt.GITHUB_SERVER_URL}/${Bt.GITHUB_REPOSITORY}@${Bt.GITHUB_REF}`,digest:{gitCommit:Bt.GITHUB_SHA}}]},runDetails:{builder:{id:`${GHt}/${Bt.RUNNER_ENVIRONMENT}`},metadata:{invocationId:`${Bt.GITHUB_SERVER_URL}/${Bt.GITHUB_REPOSITORY}/actions/runs/${Bt.GITHUB_RUN_ID}/attempts/${Bt.GITHUB_RUN_ATTEMPT}`}}}}}else if(Bt.GITLAB_CI){if(!Bt.SIGSTORE_ID_TOKEN)throw new Yt(91,`Provenance generation in GitLab CI requires "SIGSTORE_ID_TOKEN" with "sigstore" audience to be present in "id_tokens". For more info see: https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html`);r={_type:UHt,subject:t,predicateType:jHt,predicate:{buildType:`${YHt}/${VHt}`,builder:{id:`${Bt.CI_PROJECT_URL}/-/runners/${Bt.CI_RUNNER_ID}`},invocation:{configSource:{uri:`git+${Bt.CI_PROJECT_URL}`,digest:{sha1:Bt.CI_COMMIT_SHA},entryPoint:Bt.CI_JOB_NAME},parameters:{CI:Bt.CI,CI_API_GRAPHQL_URL:Bt.CI_API_GRAPHQL_URL,CI_API_V4_URL:Bt.CI_API_V4_URL,CI_BUILD_BEFORE_SHA:Bt.CI_BUILD_BEFORE_SHA,CI_BUILD_ID:Bt.CI_BUILD_ID,CI_BUILD_NAME:Bt.CI_BUILD_NAME,CI_BUILD_REF:Bt.CI_BUILD_REF,CI_BUILD_REF_NAME:Bt.CI_BUILD_REF_NAME,CI_BUILD_REF_SLUG:Bt.CI_BUILD_REF_SLUG,CI_BUILD_STAGE:Bt.CI_BUILD_STAGE,CI_COMMIT_BEFORE_SHA:Bt.CI_COMMIT_BEFORE_SHA,CI_COMMIT_BRANCH:Bt.CI_COMMIT_BRANCH,CI_COMMIT_REF_NAME:Bt.CI_COMMIT_REF_NAME,CI_COMMIT_REF_PROTECTED:Bt.CI_COMMIT_REF_PROTECTED,CI_COMMIT_REF_SLUG:Bt.CI_COMMIT_REF_SLUG,CI_COMMIT_SHA:Bt.CI_COMMIT_SHA,CI_COMMIT_SHORT_SHA:Bt.CI_COMMIT_SHORT_SHA,CI_COMMIT_TIMESTAMP:Bt.CI_COMMIT_TIMESTAMP,CI_COMMIT_TITLE:Bt.CI_COMMIT_TITLE,CI_CONFIG_PATH:Bt.CI_CONFIG_PATH,CI_DEFAULT_BRANCH:Bt.CI_DEFAULT_BRANCH,CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX:Bt.CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX,CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX:Bt.CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX,CI_DEPENDENCY_PROXY_SERVER:Bt.CI_DEPENDENCY_PROXY_SERVER,CI_DEPENDENCY_PROXY_USER:Bt.CI_DEPENDENCY_PROXY_USER,CI_JOB_ID:Bt.CI_JOB_ID,CI_JOB_NAME:Bt.CI_JOB_NAME,CI_JOB_NAME_SLUG:Bt.CI_JOB_NAME_SLUG,CI_JOB_STAGE:Bt.CI_JOB_STAGE,CI_JOB_STARTED_AT:Bt.CI_JOB_STARTED_AT,CI_JOB_URL:Bt.CI_JOB_URL,CI_NODE_TOTAL:Bt.CI_NODE_TOTAL,CI_PAGES_DOMAIN:Bt.CI_PAGES_DOMAIN,CI_PAGES_URL:Bt.CI_PAGES_URL,CI_PIPELINE_CREATED_AT:Bt.CI_PIPELINE_CREATED_AT,CI_PIPELINE_ID:Bt.CI_PIPELINE_ID,CI_PIPELINE_IID:Bt.CI_PIPELINE_IID,CI_PIPELINE_SOURCE:Bt.CI_PIPELINE_SOURCE,CI_PIPELINE_URL:Bt.CI_PIPELINE_URL,CI_PROJECT_CLASSIFICATION_LABEL:Bt.CI_PROJECT_CLASSIFICATION_LABEL,CI_PROJECT_DESCRIPTION:Bt.CI_PROJECT_DESCRIPTION,CI_PROJECT_ID:Bt.CI_PROJECT_ID,CI_PROJECT_NAME:Bt.CI_PROJECT_NAME,CI_PROJECT_NAMESPACE:Bt.CI_PROJECT_NAMESPACE,CI_PROJECT_NAMESPACE_ID:Bt.CI_PROJECT_NAMESPACE_ID,CI_PROJECT_PATH:Bt.CI_PROJECT_PATH,CI_PROJECT_PATH_SLUG:Bt.CI_PROJECT_PATH_SLUG,CI_PROJECT_REPOSITORY_LANGUAGES:Bt.CI_PROJECT_REPOSITORY_LANGUAGES,CI_PROJECT_ROOT_NAMESPACE:Bt.CI_PROJECT_ROOT_NAMESPACE,CI_PROJECT_TITLE:Bt.CI_PROJECT_TITLE,CI_PROJECT_URL:Bt.CI_PROJECT_URL,CI_PROJECT_VISIBILITY:Bt.CI_PROJECT_VISIBILITY,CI_REGISTRY:Bt.CI_REGISTRY,CI_REGISTRY_IMAGE:Bt.CI_REGISTRY_IMAGE,CI_REGISTRY_USER:Bt.CI_REGISTRY_USER,CI_RUNNER_DESCRIPTION:Bt.CI_RUNNER_DESCRIPTION,CI_RUNNER_ID:Bt.CI_RUNNER_ID,CI_RUNNER_TAGS:Bt.CI_RUNNER_TAGS,CI_SERVER_HOST:Bt.CI_SERVER_HOST,CI_SERVER_NAME:Bt.CI_SERVER_NAME,CI_SERVER_PORT:Bt.CI_SERVER_PORT,CI_SERVER_PROTOCOL:Bt.CI_SERVER_PROTOCOL,CI_SERVER_REVISION:Bt.CI_SERVER_REVISION,CI_SERVER_SHELL_SSH_HOST:Bt.CI_SERVER_SHELL_SSH_HOST,CI_SERVER_SHELL_SSH_PORT:Bt.CI_SERVER_SHELL_SSH_PORT,CI_SERVER_URL:Bt.CI_SERVER_URL,CI_SERVER_VERSION:Bt.CI_SERVER_VERSION,CI_SERVER_VERSION_MAJOR:Bt.CI_SERVER_VERSION_MAJOR,CI_SERVER_VERSION_MINOR:Bt.CI_SERVER_VERSION_MINOR,CI_SERVER_VERSION_PATCH:Bt.CI_SERVER_VERSION_PATCH,CI_TEMPLATE_REGISTRY_HOST:Bt.CI_TEMPLATE_REGISTRY_HOST,GITLAB_CI:Bt.GITLAB_CI,GITLAB_FEATURES:Bt.GITLAB_FEATURES,GITLAB_USER_ID:Bt.GITLAB_USER_ID,GITLAB_USER_LOGIN:Bt.GITLAB_USER_LOGIN,RUNNER_GENERATE_ARTIFACTS_METADATA:Bt.RUNNER_GENERATE_ARTIFACTS_METADATA},environment:{name:Bt.CI_RUNNER_DESCRIPTION,architecture:Bt.CI_RUNNER_EXECUTABLE_ARCH,server:Bt.CI_SERVER_URL,project:Bt.CI_PROJECT_PATH,job:{id:Bt.CI_JOB_ID},pipeline:{id:Bt.CI_PIPELINE_ID,ref:Bt.CI_CONFIG_PATH}}},metadata:{buildInvocationId:`${Bt.CI_JOB_URL}`,completeness:{parameters:!0,environment:!0,materials:!1},reproducible:!1},materials:[{uri:`git+${Bt.CI_PROJECT_URL}`,digest:{sha1:Bt.CI_COMMIT_SHA}}]}}}else throw new Yt(91,"Provenance generation is only supported in GitHub Actions and GitLab CI");return YNe.attest(Buffer.from(JSON.stringify(r)),_Ht,e)};async function KHt(t,e,{access:r,tag:s,registry:a,gitHead:n,provenance:c}){let f=t.manifest.name,p=t.manifest.version,h=q.stringifyIdent(f),E=KNe.default.fromData(e,{algorithms:["sha1","sha512"]}),C=r??JNe(t,f),S=await zNe(t),P=await IA.genPackageManifest(t),I=`${h}-${p}.tgz`,R=new URL(`${zc(a)}/${h}/-/${I}`),N={[I]:{content_type:"application/octet-stream",data:e.toString("base64"),length:e.length}};if(c){let U={name:`pkg:npm/${h.replace(/^@/,"%40")}@${p}`,digest:{sha512:E.sha512[0].hexDigest()}},W=await VNe([U]),te=JSON.stringify(W);N[`${h}-${p}.sigstore`]={content_type:W.mediaType,data:te,length:te.length}}return{_id:h,_attachments:N,name:h,access:C,"dist-tags":{[s]:p},versions:{[p]:{...P,_id:`${h}@${p}`,name:h,version:p,gitHead:n,dist:{shasum:E.sha1[0].hexDigest(),integrity:E.sha512[0].toString(),tarball:R.toString()}}},readme:S}}async function JHt(t){try{let{stdout:e}=await Gr.execvp("git",["rev-parse","--revs-only","HEAD"],{cwd:t});return e.trim()===""?void 0:e.trim()}catch{return}}function JNe(t,e){let r=t.project.configuration;return t.manifest.publishConfig&&typeof t.manifest.publishConfig.access=="string"?t.manifest.publishConfig.access:r.get("npmPublishAccess")!==null?r.get("npmPublishAccess"):e.scope?"restricted":"public"}async function zNe(t){let e=ue.toPortablePath(`${t.cwd}/README.md`),r=t.manifest.name,a=`# ${q.stringifyIdent(r)} `;try{a=await le.readFilePromise(e,"utf8")}catch(n){if(n.code==="ENOENT")return a;throw n}return a}var sz={npmAlwaysAuth:{description:"URL of the selected npm registry (note: npm enterprise isn't supported)",type:"BOOLEAN",default:!1},npmAuthIdent:{description:"Authentication identity for the npm registry (_auth in npm and yarn v1)",type:"SECRET",default:null},npmAuthToken:{description:"Authentication token for the npm registry (_authToken in npm and yarn v1)",type:"SECRET",default:null}},ZNe={npmAuditRegistry:{description:"Registry to query for audit reports",type:"STRING",default:null},npmPublishRegistry:{description:"Registry to push packages to",type:"STRING",default:null},npmRegistryServer:{description:"URL of the selected npm registry (note: npm enterprise isn't supported)",type:"STRING",default:"https://registry.yarnpkg.com"}},zHt={configuration:{...sz,...ZNe,npmScopes:{description:"Settings per package scope",type:"MAP",valueDefinition:{description:"",type:"SHAPE",properties:{...sz,...ZNe}}},npmRegistries:{description:"Settings per registry",type:"MAP",normalizeKeys:zc,valueDefinition:{description:"",type:"SHAPE",properties:{...sz}}}},fetchers:[rb,lh],resolvers:[nb,ib,sb]},ZHt=zHt;var gz={};Vt(gz,{NpmAuditCommand:()=>S1,NpmInfoCommand:()=>D1,NpmLoginCommand:()=>b1,NpmLogoutCommand:()=>x1,NpmPublishCommand:()=>k1,NpmTagAddCommand:()=>T1,NpmTagListCommand:()=>Q1,NpmTagRemoveCommand:()=>R1,NpmWhoamiCommand:()=>F1,default:()=>ijt,npmAuditTypes:()=>sP,npmAuditUtils:()=>QL});Ve();Ve();Wt();var fz=et(Sa());Ul();var sP={};Vt(sP,{Environment:()=>nP,Severity:()=>iP});var nP=(s=>(s.All="all",s.Production="production",s.Development="development",s))(nP||{}),iP=(n=>(n.Info="info",n.Low="low",n.Moderate="moderate",n.High="high",n.Critical="critical",n))(iP||{});var QL={};Vt(QL,{allSeverities:()=>v1,getPackages:()=>uz,getReportTree:()=>lz,getSeverityInclusions:()=>az,getTopLevelDependencies:()=>cz});Ve();var XNe=et(Ai());var v1=["info","low","moderate","high","critical"];function az(t){if(typeof t>"u")return new Set(v1);let e=v1.indexOf(t),r=v1.slice(e);return new Set(r)}function lz(t){let e={},r={children:e};for(let[s,a]of je.sortMap(Object.entries(t),n=>n[0]))for(let n of je.sortMap(a,c=>`${c.id}`))e[`${s}/${n.id}`]={value:he.tuple(he.Type.IDENT,q.parseIdent(s)),children:{ID:typeof n.id<"u"&&{label:"ID",value:he.tuple(he.Type.ID,n.id)},Issue:{label:"Issue",value:he.tuple(he.Type.NO_HINT,n.title)},URL:typeof n.url<"u"&&{label:"URL",value:he.tuple(he.Type.URL,n.url)},Severity:{label:"Severity",value:he.tuple(he.Type.NO_HINT,n.severity)},"Vulnerable Versions":{label:"Vulnerable Versions",value:he.tuple(he.Type.RANGE,n.vulnerable_versions)},"Tree Versions":{label:"Tree Versions",children:[...n.versions].sort(XNe.default.compare).map(c=>({value:he.tuple(he.Type.REFERENCE,c)}))},Dependents:{label:"Dependents",children:je.sortMap(n.dependents,c=>q.stringifyLocator(c)).map(c=>({value:he.tuple(he.Type.LOCATOR,c)}))}}};return r}function cz(t,e,{all:r,environment:s}){let a=[],n=r?t.workspaces:[e],c=["all","production"].includes(s),f=["all","development"].includes(s);for(let p of n)for(let h of p.anchoredPackage.dependencies.values())(p.manifest.devDependencies.has(h.identHash)?!f:!c)||a.push({workspace:p,dependency:h});return a}function uz(t,e,{recursive:r}){let s=new Map,a=new Set,n=[],c=(f,p)=>{let h=t.storedResolutions.get(p.descriptorHash);if(typeof h>"u")throw new Error("Assertion failed: The resolution should have been registered");if(!a.has(h))a.add(h);else return;let E=t.storedPackages.get(h);if(typeof E>"u")throw new Error("Assertion failed: The package should have been registered");if(q.ensureDevirtualizedLocator(E).reference.startsWith("npm:")&&E.version!==null){let S=q.stringifyIdent(E),P=je.getMapWithDefault(s,S);je.getArrayWithDefault(P,E.version).push(f)}if(r)for(let S of E.dependencies.values())n.push([E,S])};for(let{workspace:f,dependency:p}of e)n.push([f.anchoredLocator,p]);for(;n.length>0;){let[f,p]=n.shift();c(f,p)}return s}var S1=class extends ut{constructor(){super(...arguments);this.all=ge.Boolean("-A,--all",!1,{description:"Audit dependencies from all workspaces"});this.recursive=ge.Boolean("-R,--recursive",!1,{description:"Audit transitive dependencies as well"});this.environment=ge.String("--environment","all",{description:"Which environments to cover",validator:po(nP)});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.noDeprecations=ge.Boolean("--no-deprecations",!1,{description:"Don't warn about deprecated packages"});this.severity=ge.String("--severity","info",{description:"Minimal severity requested for packages to be displayed",validator:po(iP)});this.excludes=ge.Array("--exclude",[],{description:"Array of glob patterns of packages to exclude from audit"});this.ignores=ge.Array("--ignore",[],{description:"Array of glob patterns of advisory ID's to ignore in the audit report"})}static{this.paths=[["npm","audit"]]}static{this.usage=ot.Usage({description:"perform a vulnerability audit against the installed packages",details:` This command checks for known security reports on the packages you use. The reports are by default extracted from the npm registry, and may or may not be relevant to your actual program (not all vulnerabilities affect all code paths). For consistency with our other commands the default is to only check the direct dependencies for the active workspace. To extend this search to all workspaces, use \`-A,--all\`. To extend this search to both direct and transitive dependencies, use \`-R,--recursive\`. Applying the \`--severity\` flag will limit the audit table to vulnerabilities of the corresponding severity and above. Valid values are ${v1.map(r=>`\`${r}\``).join(", ")}. If the \`--json\` flag is set, Yarn will print the output exactly as received from the registry. Regardless of this flag, the process will exit with a non-zero exit code if a report is found for the selected packages. If certain packages produce false positives for a particular environment, the \`--exclude\` flag can be used to exclude any number of packages from the audit. This can also be set in the configuration file with the \`npmAuditExcludePackages\` option. If particular advisories are needed to be ignored, the \`--ignore\` flag can be used with Advisory ID's to ignore any number of advisories in the audit report. This can also be set in the configuration file with the \`npmAuditIgnoreAdvisories\` option. To understand the dependency tree requiring vulnerable packages, check the raw report with the \`--json\` flag or use \`yarn why package\` to get more information as to who depends on them. `,examples:[["Checks for known security issues with the installed packages. The output is a list of known issues.","yarn npm audit"],["Audit dependencies in all workspaces","yarn npm audit --all"],["Limit auditing to `dependencies` (excludes `devDependencies`)","yarn npm audit --environment production"],["Show audit report as valid JSON","yarn npm audit --json"],["Audit all direct and transitive dependencies","yarn npm audit --recursive"],["Output moderate (or more severe) vulnerabilities","yarn npm audit --severity moderate"],["Exclude certain packages","yarn npm audit --exclude package1 --exclude package2"],["Ignore specific advisories","yarn npm audit --ignore 1234567 --ignore 7654321"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=cz(s,a,{all:this.all,environment:this.environment}),c=uz(s,n,{recursive:this.recursive}),f=Array.from(new Set([...r.get("npmAuditExcludePackages"),...this.excludes])),p=Object.create(null);for(let[N,U]of c)f.some(W=>fz.default.isMatch(N,W))||(p[N]=[...U.keys()]);let h=hi.getAuditRegistry({configuration:r}),E,C=await uA.start({configuration:r,stdout:this.context.stdout},async()=>{let N=an.post("/-/npm/v1/security/advisories/bulk",p,{authType:an.AuthType.BEST_EFFORT,configuration:r,jsonResponse:!0,registry:h}),U=this.noDeprecations?[]:await Promise.all(Array.from(Object.entries(p),async([te,ie])=>{let Ae=await an.getPackageMetadata(q.parseIdent(te),{project:s});return je.mapAndFilter(ie,ce=>{let{deprecated:me}=Ae.versions[ce];return me?[te,ce,me]:je.mapAndFilter.skip})})),W=await N;for(let[te,ie,Ae]of U.flat(1))Object.hasOwn(W,te)&&W[te].some(ce=>Or.satisfiesWithPrereleases(ie,ce.vulnerable_versions))||(W[te]??=[],W[te].push({id:`${te} (deprecation)`,title:(typeof Ae=="string"?Ae:"").trim()||"This package has been deprecated.",severity:"moderate",vulnerable_versions:ie}));E=W});if(C.hasErrors())return C.exitCode();let S=az(this.severity),P=Array.from(new Set([...r.get("npmAuditIgnoreAdvisories"),...this.ignores])),I=Object.create(null);for(let[N,U]of Object.entries(E)){let W=U.filter(te=>!fz.default.isMatch(`${te.id}`,P)&&S.has(te.severity));W.length>0&&(I[N]=W.map(te=>{let ie=c.get(N);if(typeof ie>"u")throw new Error("Assertion failed: Expected the registry to only return packages that were requested");let Ae=[...ie.keys()].filter(me=>Or.satisfiesWithPrereleases(me,te.vulnerable_versions)),ce=new Map;for(let me of Ae)for(let pe of ie.get(me))ce.set(pe.locatorHash,pe);return{...te,versions:Ae,dependents:[...ce.values()]}}))}let R=Object.keys(I).length>0;return R?(Qs.emitTree(lz(I),{configuration:r,json:this.json,stdout:this.context.stdout,separators:2}),1):(await Ot.start({configuration:r,includeFooter:!1,json:this.json,stdout:this.context.stdout},async N=>{N.reportInfo(1,"No audit suggestions")}),R?1:0)}};Ve();Ve();bt();Wt();var Az=et(Ai()),pz=Ie("util"),D1=class extends ut{constructor(){super(...arguments);this.fields=ge.String("-f,--fields",{description:"A comma-separated list of manifest fields that should be displayed"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.packages=ge.Rest()}static{this.paths=[["npm","info"]]}static{this.usage=ot.Usage({category:"Npm-related commands",description:"show information about a package",details:"\n This command fetches information about a package from the npm registry and prints it in a tree format.\n\n The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\n\n Append `@` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\n\n If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\n\n By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\n ",examples:[["Show all available information about react (except the `dist`, `readme`, and `users` fields)","yarn npm info react"],["Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)","yarn npm info react --json"],["Show all available information about react@16.12.0","yarn npm info react@16.12.0"],["Show all available information about react@next","yarn npm info react@next"],["Show the description of react","yarn npm info react --fields description"],["Show all available versions of react","yarn npm info react --fields versions"],["Show the readme of react","yarn npm info react --fields readme"],["Show a few fields of react","yarn npm info react --fields homepage,repository"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Tt.find(r,this.context.cwd),a=typeof this.fields<"u"?new Set(["name",...this.fields.split(/\s*,\s*/)]):null,n=[],c=!1,f=await Ot.start({configuration:r,includeFooter:!1,json:this.json,stdout:this.context.stdout},async p=>{for(let h of this.packages){let E;if(h==="."){let ie=s.topLevelWorkspace;if(!ie.manifest.name)throw new nt(`Missing ${he.pretty(r,"name",he.Type.CODE)} field in ${ue.fromPortablePath(K.join(ie.cwd,Er.manifest))}`);E=q.makeDescriptor(ie.manifest.name,"unknown")}else E=q.parseDescriptor(h);let C=an.getIdentUrl(E),S=hz(await an.get(C,{configuration:r,ident:E,jsonResponse:!0,customErrorMessage:an.customPackageError})),P=Object.keys(S.versions).sort(Az.default.compareLoose),R=S["dist-tags"].latest||P[P.length-1],N=Or.validRange(E.range);if(N){let ie=Az.default.maxSatisfying(P,N);ie!==null?R=ie:(p.reportWarning(0,`Unmet range ${q.prettyRange(r,E.range)}; falling back to the latest version`),c=!0)}else Object.hasOwn(S["dist-tags"],E.range)?R=S["dist-tags"][E.range]:E.range!=="unknown"&&(p.reportWarning(0,`Unknown tag ${q.prettyRange(r,E.range)}; falling back to the latest version`),c=!0);let U=S.versions[R],W={...S,...U,version:R,versions:P},te;if(a!==null){te={};for(let ie of a){let Ae=W[ie];if(typeof Ae<"u")te[ie]=Ae;else{p.reportWarning(1,`The ${he.pretty(r,ie,he.Type.CODE)} field doesn't exist inside ${q.prettyIdent(r,E)}'s information`),c=!0;continue}}}else this.json||(delete W.dist,delete W.readme,delete W.users),te=W;p.reportJson(te),this.json||n.push(te)}});pz.inspect.styles.name="cyan";for(let p of n)(p!==n[0]||c)&&this.context.stdout.write(` `),this.context.stdout.write(`${(0,pz.inspect)(p,{depth:1/0,colors:!0,compact:!1})} `);return f.exitCode()}};function hz(t){if(Array.isArray(t)){let e=[];for(let r of t)r=hz(r),r&&e.push(r);return e}else if(typeof t=="object"&&t!==null){let e={};for(let r of Object.keys(t)){if(r.startsWith("_"))continue;let s=hz(t[r]);s&&(e[r]=s)}return e}else return t||null}Ve();Ve();Wt();var $Ne=et(nS()),b1=class extends ut{constructor(){super(...arguments);this.scope=ge.String("-s,--scope",{description:"Login to the registry configured for a given scope"});this.publish=ge.Boolean("--publish",!1,{description:"Login to the publish registry"});this.alwaysAuth=ge.Boolean("--always-auth",{description:"Set the npmAlwaysAuth configuration"})}static{this.paths=[["npm","login"]]}static{this.usage=ot.Usage({category:"Npm-related commands",description:"store new login info to access the npm registry",details:"\n This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\n\n Adding the `-s,--scope` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n ",examples:[["Login to the default registry","yarn npm login"],["Login to the registry linked to the @my-scope registry","yarn npm login --scope my-scope"],["Login to the publish registry for the current package","yarn npm login --publish"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=await TL({configuration:r,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Ot.start({configuration:r,stdout:this.context.stdout,includeFooter:!1},async n=>{let c=await ejt({configuration:r,registry:s,report:n,stdin:this.context.stdin,stdout:this.context.stdout}),f=await XHt(s,c,r);return await $Ht(s,f,{alwaysAuth:this.alwaysAuth,scope:this.scope}),n.reportInfo(0,"Successfully logged in")})).exitCode()}};async function TL({scope:t,publish:e,configuration:r,cwd:s}){return t&&e?hi.getScopeRegistry(t,{configuration:r,type:hi.RegistryType.PUBLISH_REGISTRY}):t?hi.getScopeRegistry(t,{configuration:r}):e?hi.getPublishRegistry((await eC(r,s)).manifest,{configuration:r}):hi.getDefaultRegistry({configuration:r})}async function XHt(t,e,r){let s=`/-/user/org.couchdb.user:${encodeURIComponent(e.name)}`,a={_id:`org.couchdb.user:${e.name}`,name:e.name,password:e.password,type:"user",roles:[],date:new Date().toISOString()},n={attemptedAs:e.name,configuration:r,registry:t,jsonResponse:!0,authType:an.AuthType.NO_AUTH};try{return(await an.put(s,a,n)).token}catch(E){if(!(E.originalError?.name==="HTTPError"&&E.originalError?.response.statusCode===409))throw E}let c={...n,authType:an.AuthType.NO_AUTH,headers:{authorization:`Basic ${Buffer.from(`${e.name}:${e.password}`).toString("base64")}`}},f=await an.get(s,c);for(let[E,C]of Object.entries(f))(!a[E]||E==="roles")&&(a[E]=C);let p=`${s}/-rev/${a._rev}`;return(await an.put(p,a,c)).token}async function $Ht(t,e,{alwaysAuth:r,scope:s}){let a=c=>f=>{let p=je.isIndexableObject(f)?f:{},h=p[c],E=je.isIndexableObject(h)?h:{};return{...p,[c]:{...E,...r!==void 0?{npmAlwaysAuth:r}:{},npmAuthToken:e}}},n=s?{npmScopes:a(s)}:{npmRegistries:a(t)};return await ze.updateHomeConfiguration(n)}async function ejt({configuration:t,registry:e,report:r,stdin:s,stdout:a}){r.reportInfo(0,`Logging in to ${he.pretty(t,e,he.Type.URL)}`);let n=!1;if(e.match(/^https:\/\/npm\.pkg\.github\.com(\/|$)/)&&(r.reportInfo(0,"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions."),n=!0),r.reportSeparator(),t.env.YARN_IS_TEST_ENV)return{name:t.env.YARN_INJECT_NPM_USER||"",password:t.env.YARN_INJECT_NPM_PASSWORD||""};let c=await(0,$Ne.prompt)([{type:"input",name:"name",message:"Username:",required:!0,onCancel:()=>process.exit(130),stdin:s,stdout:a},{type:"password",name:"password",message:n?"Token:":"Password:",required:!0,onCancel:()=>process.exit(130),stdin:s,stdout:a}]);return r.reportSeparator(),c}Ve();Ve();Wt();var P1=new Set(["npmAuthIdent","npmAuthToken"]),x1=class extends ut{constructor(){super(...arguments);this.scope=ge.String("-s,--scope",{description:"Logout of the registry configured for a given scope"});this.publish=ge.Boolean("--publish",!1,{description:"Logout of the publish registry"});this.all=ge.Boolean("-A,--all",!1,{description:"Logout of all registries"})}static{this.paths=[["npm","logout"]]}static{this.usage=ot.Usage({category:"Npm-related commands",description:"logout of the npm registry",details:"\n This command will log you out by modifying your local configuration (in your home folder, never in the project itself) to delete all credentials linked to a registry.\n\n Adding the `-s,--scope` flag will cause the deletion to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the deletion to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n\n Adding the `-A,--all` flag will cause the deletion to be done against all registries and scopes.\n ",examples:[["Logout of the default registry","yarn npm logout"],["Logout of the @my-scope scope","yarn npm logout --scope my-scope"],["Logout of the publish registry for the current package","yarn npm logout --publish"],["Logout of all registries","yarn npm logout --all"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s=async()=>{let n=await TL({configuration:r,cwd:this.context.cwd,publish:this.publish,scope:this.scope}),c=await ze.find(this.context.cwd,this.context.plugins),f=q.makeIdent(this.scope??null,"pkg");return!hi.getAuthConfiguration(n,{configuration:c,ident:f}).get("npmAuthToken")};return(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{if(this.all&&(await rjt(),n.reportInfo(0,"Successfully logged out from everything")),this.scope){await eOe("npmScopes",this.scope),await s()?n.reportInfo(0,`Successfully logged out from ${this.scope}`):n.reportWarning(0,"Scope authentication settings removed, but some other ones settings still apply to it");return}let c=await TL({configuration:r,cwd:this.context.cwd,publish:this.publish});await eOe("npmRegistries",c),await s()?n.reportInfo(0,`Successfully logged out from ${c}`):n.reportWarning(0,"Registry authentication settings removed, but some other ones settings still apply to it")})).exitCode()}};function tjt(t,e){let r=t[e];if(!je.isIndexableObject(r))return!1;let s=new Set(Object.keys(r));if([...P1].every(n=>!s.has(n)))return!1;for(let n of P1)s.delete(n);if(s.size===0)return t[e]=void 0,!0;let a={...r};for(let n of P1)delete a[n];return t[e]=a,!0}async function rjt(){let t=e=>{let r=!1,s=je.isIndexableObject(e)?{...e}:{};s.npmAuthToken&&(delete s.npmAuthToken,r=!0);for(let a of Object.keys(s))tjt(s,a)&&(r=!0);if(Object.keys(s).length!==0)return r?s:e};return await ze.updateHomeConfiguration({npmRegistries:t,npmScopes:t})}async function eOe(t,e){return await ze.updateHomeConfiguration({[t]:r=>{let s=je.isIndexableObject(r)?r:{};if(!Object.hasOwn(s,e))return r;let a=s[e],n=je.isIndexableObject(a)?a:{},c=new Set(Object.keys(n));if([...P1].every(p=>!c.has(p)))return r;for(let p of P1)c.delete(p);if(c.size===0)return Object.keys(s).length===1?void 0:{...s,[e]:void 0};let f={};for(let p of P1)f[p]=void 0;return{...s,[e]:{...n,...f}}}})}Ve();Wt();var k1=class extends ut{constructor(){super(...arguments);this.access=ge.String("--access",{description:"The access for the published package (public or restricted)"});this.tag=ge.String("--tag","latest",{description:"The tag on the registry that the package should be attached to"});this.tolerateRepublish=ge.Boolean("--tolerate-republish",!1,{description:"Warn and exit when republishing an already existing version of a package"});this.otp=ge.String("--otp",{description:"The OTP token to use with the command"});this.provenance=ge.Boolean("--provenance",!1,{description:"Generate provenance for the package. Only available in GitHub Actions and GitLab CI. Can be set globally through the `npmPublishProvenance` setting or the `YARN_NPM_CONFIG_PROVENANCE` environment variable, or per-package through the `publishConfig.provenance` field in package.json."})}static{this.paths=[["npm","publish"]]}static{this.usage=ot.Usage({category:"Npm-related commands",description:"publish the active workspace to the npm registry",details:'\n This command will pack the active workspace into a fresh archive and upload it to the npm registry.\n\n The package will by default be attached to the `latest` tag on the registry, but this behavior can be overridden by using the `--tag` option.\n\n Note that for legacy reasons scoped packages are by default published with an access set to `restricted` (aka "private packages"). This requires you to register for a paid npm plan. In case you simply wish to publish a public scoped package to the registry (for free), just add the `--access public` flag. This behavior can be enabled by default through the `npmPublishAccess` settings.\n ',examples:[["Publish the active workspace","yarn npm publish"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);if(a.manifest.private)throw new nt("Private workspaces cannot be published");if(a.manifest.name===null||a.manifest.version===null)throw new nt("Workspaces must have valid names and versions to be published on an external registry");await s.restoreInstallState();let n=a.manifest.name,c=a.manifest.version,f=hi.getPublishRegistry(a.manifest,{configuration:r});return(await Ot.start({configuration:r,stdout:this.context.stdout},async h=>{if(this.tolerateRepublish)try{let E=await an.get(an.getIdentUrl(n),{configuration:r,registry:f,ident:n,jsonResponse:!0});if(!Object.hasOwn(E,"versions"))throw new Yt(15,'Registry returned invalid data for - missing "versions" field');if(Object.hasOwn(E.versions,c)){h.reportWarning(0,`Registry already knows about version ${c}; skipping.`);return}}catch(E){if(E.originalError?.response?.statusCode!==404)throw E}await In.maybeExecuteWorkspaceLifecycleScript(a,"prepublish",{report:h}),await IA.prepareForPack(a,{report:h},async()=>{let E=await IA.genPackList(a);for(let N of E)h.reportInfo(null,N);let C=await IA.genPackStream(a,E),S=await je.bufferStream(C),P=await B1.getGitHead(a.cwd),I=!1;a.manifest.publishConfig&&"provenance"in a.manifest.publishConfig?(I=!!a.manifest.publishConfig.provenance,I?h.reportInfo(null,"Generating provenance statement because `publishConfig.provenance` field is set."):h.reportInfo(null,"Skipping provenance statement because `publishConfig.provenance` field is set to false.")):this.provenance?(I=!0,h.reportInfo(null,"Generating provenance statement because `--provenance` flag is set.")):r.get("npmPublishProvenance")&&(I=!0,h.reportInfo(null,"Generating provenance statement because `npmPublishProvenance` setting is set."));let R=await B1.makePublishBody(a,S,{access:this.access,tag:this.tag,registry:f,gitHead:P,provenance:I});await an.put(an.getIdentUrl(n),R,{configuration:r,registry:f,ident:n,otp:this.otp,jsonResponse:!0})}),h.reportInfo(0,"Package archive published")})).exitCode()}};Ve();Wt();var tOe=et(Ai());Ve();bt();Wt();var Q1=class extends ut{constructor(){super(...arguments);this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.package=ge.String({required:!1})}static{this.paths=[["npm","tag","list"]]}static{this.usage=ot.Usage({category:"Npm-related commands",description:"list all dist-tags of a package",details:` This command will list all tags of a package from the npm registry. If the package is not specified, Yarn will default to the current workspace. `,examples:[["List all tags of package `my-pkg`","yarn npm tag list my-pkg"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n;if(typeof this.package<"u")n=q.parseIdent(this.package);else{if(!a)throw new ar(s.cwd,this.context.cwd);if(!a.manifest.name)throw new nt(`Missing 'name' field in ${ue.fromPortablePath(K.join(a.cwd,Er.manifest))}`);n=a.manifest.name}let c=await oP(n,r),p={children:je.sortMap(Object.entries(c),([h])=>h).map(([h,E])=>({value:he.tuple(he.Type.RESOLUTION,{descriptor:q.makeDescriptor(n,h),locator:q.makeLocator(n,E)})}))};return Qs.emitTree(p,{configuration:r,json:this.json,stdout:this.context.stdout})}};async function oP(t,e){let r=`/-/package${an.getIdentUrl(t)}/dist-tags`;return an.get(r,{configuration:e,ident:t,jsonResponse:!0,customErrorMessage:an.customPackageError})}var T1=class extends ut{constructor(){super(...arguments);this.package=ge.String();this.tag=ge.String()}static{this.paths=[["npm","tag","add"]]}static{this.usage=ot.Usage({category:"Npm-related commands",description:"add a tag for a specific version of a package",details:` This command will add a tag to the npm registry for a specific version of a package. If the tag already exists, it will be overwritten. `,examples:[["Add a `beta` tag for version `2.3.4-beta.4` of package `my-pkg`","yarn npm tag add my-pkg@2.3.4-beta.4 beta"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=q.parseDescriptor(this.package,!0),c=n.range;if(!tOe.default.valid(c))throw new nt(`The range ${he.pretty(r,n.range,he.Type.RANGE)} must be a valid semver version`);let f=hi.getPublishRegistry(a.manifest,{configuration:r}),p=he.pretty(r,n,he.Type.IDENT),h=he.pretty(r,c,he.Type.RANGE),E=he.pretty(r,this.tag,he.Type.CODE);return(await Ot.start({configuration:r,stdout:this.context.stdout},async S=>{let P=await oP(n,r);Object.hasOwn(P,this.tag)&&P[this.tag]===c&&S.reportWarning(0,`Tag ${E} is already set to version ${h}`);let I=`/-/package${an.getIdentUrl(n)}/dist-tags/${encodeURIComponent(this.tag)}`;await an.put(I,c,{configuration:r,registry:f,ident:n,jsonRequest:!0,jsonResponse:!0}),S.reportInfo(0,`Tag ${E} added to version ${h} of package ${p}`)})).exitCode()}};Ve();Wt();var R1=class extends ut{constructor(){super(...arguments);this.package=ge.String();this.tag=ge.String()}static{this.paths=[["npm","tag","remove"]]}static{this.usage=ot.Usage({category:"Npm-related commands",description:"remove a tag from a package",details:` This command will remove a tag from a package from the npm registry. `,examples:[["Remove the `beta` tag from package `my-pkg`","yarn npm tag remove my-pkg beta"]]})}async execute(){if(this.tag==="latest")throw new nt("The 'latest' tag cannot be removed.");let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=q.parseIdent(this.package),c=hi.getPublishRegistry(a.manifest,{configuration:r}),f=he.pretty(r,this.tag,he.Type.CODE),p=he.pretty(r,n,he.Type.IDENT),h=await oP(n,r);if(!Object.hasOwn(h,this.tag))throw new nt(`${f} is not a tag of package ${p}`);return(await Ot.start({configuration:r,stdout:this.context.stdout},async C=>{let S=`/-/package${an.getIdentUrl(n)}/dist-tags/${encodeURIComponent(this.tag)}`;await an.del(S,{configuration:r,registry:c,ident:n,jsonResponse:!0}),C.reportInfo(0,`Tag ${f} removed from package ${p}`)})).exitCode()}};Ve();Ve();Wt();var F1=class extends ut{constructor(){super(...arguments);this.scope=ge.String("-s,--scope",{description:"Print username for the registry configured for a given scope"});this.publish=ge.Boolean("--publish",!1,{description:"Print username for the publish registry"})}static{this.paths=[["npm","whoami"]]}static{this.usage=ot.Usage({category:"Npm-related commands",description:"display the name of the authenticated user",details:"\n Print the username associated with the current authentication settings to the standard output.\n\n When using `-s,--scope`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\n\n When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\n ",examples:[["Print username for the default registry","yarn npm whoami"],["Print username for the registry on a given scope","yarn npm whoami --scope company"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),s;return this.scope&&this.publish?s=hi.getScopeRegistry(this.scope,{configuration:r,type:hi.RegistryType.PUBLISH_REGISTRY}):this.scope?s=hi.getScopeRegistry(this.scope,{configuration:r}):this.publish?s=hi.getPublishRegistry((await eC(r,this.context.cwd)).manifest,{configuration:r}):s=hi.getDefaultRegistry({configuration:r}),(await Ot.start({configuration:r,stdout:this.context.stdout},async n=>{let c;try{c=await an.get("/-/whoami",{configuration:r,registry:s,authType:an.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?q.makeIdent(this.scope,""):void 0})}catch(f){if(f.response?.statusCode===401||f.response?.statusCode===403){n.reportError(41,"Authentication failed - your credentials may have expired");return}else throw f}n.reportInfo(0,c.username)})).exitCode()}};var njt={configuration:{npmPublishAccess:{description:"Default access of the published packages",type:"STRING",default:null},npmPublishProvenance:{description:"Whether to generate provenance for the published packages",type:"BOOLEAN",default:!1},npmAuditExcludePackages:{description:"Array of glob patterns of packages to exclude from npm audit",type:"STRING",default:[],isArray:!0},npmAuditIgnoreAdvisories:{description:"Array of glob patterns of advisory IDs to exclude from npm audit",type:"STRING",default:[],isArray:!0}},commands:[S1,D1,b1,x1,k1,T1,Q1,R1,F1]},ijt=njt;var wz={};Vt(wz,{PatchCommand:()=>U1,PatchCommitCommand:()=>_1,PatchFetcher:()=>fP,PatchResolver:()=>AP,default:()=>wjt,patchUtils:()=>hy});Ve();Ve();bt();rA();var hy={};Vt(hy,{applyPatchFile:()=>FL,diffFolders:()=>Iz,ensureUnpatchedDescriptor:()=>dz,ensureUnpatchedLocator:()=>OL,extractPackageToDisk:()=>Ez,extractPatchFlags:()=>lOe,isParentRequired:()=>yz,isPatchDescriptor:()=>NL,isPatchLocator:()=>Tg,loadPatchFiles:()=>uP,makeDescriptor:()=>LL,makeLocator:()=>mz,makePatchHash:()=>Cz,parseDescriptor:()=>lP,parseLocator:()=>cP,parsePatchFile:()=>aP,unpatchDescriptor:()=>Ejt,unpatchLocator:()=>Ijt});Ve();bt();Ve();bt();var sjt=/^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*/;function N1(t){return K.relative(vt.root,K.resolve(vt.root,ue.toPortablePath(t)))}function ojt(t){let e=t.trim().match(sjt);if(!e)throw new Error(`Bad header line: '${t}'`);return{original:{start:Math.max(Number(e[1]),1),length:Number(e[3]||1)},patched:{start:Math.max(Number(e[4]),1),length:Number(e[6]||1)}}}var ajt=420,ljt=493;var rOe=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),cjt=t=>({header:ojt(t),parts:[]}),ujt={"@":"header","-":"deletion","+":"insertion"," ":"context","\\":"pragma",undefined:"context"};function fjt(t){let e=[],r=rOe(),s="parsing header",a=null,n=null;function c(){a&&(n&&(a.parts.push(n),n=null),r.hunks.push(a),a=null)}function f(){c(),e.push(r),r=rOe()}for(let p=0;p0?"patch":"mode change",W=null;switch(U){case"rename":{if(!E||!C)throw new Error("Bad parser state: rename from & to not given");e.push({type:"rename",semverExclusivity:s,fromPath:N1(E),toPath:N1(C)}),W=C}break;case"file deletion":{let te=a||I;if(!te)throw new Error("Bad parse state: no path given for file deletion");e.push({type:"file deletion",semverExclusivity:s,hunk:N&&N[0]||null,path:N1(te),mode:RL(p),hash:S})}break;case"file creation":{let te=n||R;if(!te)throw new Error("Bad parse state: no path given for file creation");e.push({type:"file creation",semverExclusivity:s,hunk:N&&N[0]||null,path:N1(te),mode:RL(h),hash:P})}break;case"patch":case"mode change":W=R||n;break;default:je.assertNever(U);break}W&&c&&f&&c!==f&&e.push({type:"mode change",semverExclusivity:s,path:N1(W),oldMode:RL(c),newMode:RL(f)}),W&&N&&N.length&&e.push({type:"patch",semverExclusivity:s,path:N1(W),hunks:N,beforeHash:S,afterHash:P})}if(e.length===0)throw new Error("Unable to parse patch file: No changes found. Make sure the patch is a valid UTF8 encoded string");return e}function RL(t){let e=parseInt(t,8)&511;if(e!==ajt&&e!==ljt)throw new Error(`Unexpected file mode string: ${t}`);return e}function aP(t){let e=t.split(/\n/g);return e[e.length-1]===""&&e.pop(),Ajt(fjt(e))}function pjt(t){let e=0,r=0;for(let{type:s,lines:a}of t.parts)switch(s){case"context":r+=a.length,e+=a.length;break;case"deletion":e+=a.length;break;case"insertion":r+=a.length;break;default:je.assertNever(s);break}if(e!==t.header.original.length||r!==t.header.patched.length){let s=a=>a<0?a:`+${a}`;throw new Error(`hunk header integrity check failed (expected @@ ${s(t.header.original.length)} ${s(t.header.patched.length)} @@, got @@ ${s(e)} ${s(r)} @@)`)}}Ve();bt();var O1=class extends Error{constructor(r,s){super(`Cannot apply hunk #${r+1}`);this.hunk=s}};async function L1(t,e,r){let s=await t.lstatPromise(e),a=await r();typeof a<"u"&&(e=a),await t.lutimesPromise(e,s.atime,s.mtime)}async function FL(t,{baseFs:e=new Yn,dryRun:r=!1,version:s=null}={}){for(let a of t)if(!(a.semverExclusivity!==null&&s!==null&&!Or.satisfiesWithPrereleases(s,a.semverExclusivity)))switch(a.type){case"file deletion":if(r){if(!e.existsSync(a.path))throw new Error(`Trying to delete a file that doesn't exist: ${a.path}`)}else await L1(e,K.dirname(a.path),async()=>{await e.unlinkPromise(a.path)});break;case"rename":if(r){if(!e.existsSync(a.fromPath))throw new Error(`Trying to move a file that doesn't exist: ${a.fromPath}`)}else await L1(e,K.dirname(a.fromPath),async()=>{await L1(e,K.dirname(a.toPath),async()=>{await L1(e,a.fromPath,async()=>(await e.movePromise(a.fromPath,a.toPath),a.toPath))})});break;case"file creation":if(r){if(e.existsSync(a.path))throw new Error(`Trying to create a file that already exists: ${a.path}`)}else{let n=a.hunk?a.hunk.parts[0].lines.join(` `)+(a.hunk.parts[0].noNewlineAtEndOfFile?"":` `):"";await e.mkdirpPromise(K.dirname(a.path),{chmod:493,utimes:[fi.SAFE_TIME,fi.SAFE_TIME]}),await e.writeFilePromise(a.path,n,{mode:a.mode}),await e.utimesPromise(a.path,fi.SAFE_TIME,fi.SAFE_TIME)}break;case"patch":await L1(e,a.path,async()=>{await djt(a,{baseFs:e,dryRun:r})});break;case"mode change":{let c=(await e.statPromise(a.path)).mode;if(nOe(a.newMode)!==nOe(c))continue;await L1(e,a.path,async()=>{await e.chmodPromise(a.path,a.newMode)})}break;default:je.assertNever(a);break}}function nOe(t){return(t&64)>0}function iOe(t){return t.replace(/\s+$/,"")}function gjt(t,e){return iOe(t)===iOe(e)}async function djt({hunks:t,path:e},{baseFs:r,dryRun:s=!1}){let a=await r.statSync(e).mode,c=(await r.readFileSync(e,"utf8")).split(/\n/),f=[],p=0,h=0;for(let C of t){let S=Math.max(h,C.header.patched.start+p),P=Math.max(0,S-h),I=Math.max(0,c.length-S-C.header.original.length),R=Math.max(P,I),N=0,U=0,W=null;for(;N<=R;){if(N<=P&&(U=S-N,W=sOe(C,c,U),W!==null)){N=-N;break}if(N<=I&&(U=S+N,W=sOe(C,c,U),W!==null))break;N+=1}if(W===null)throw new O1(t.indexOf(C),C);f.push(W),p+=N,h=U+C.header.original.length}if(s)return;let E=0;for(let C of f)for(let S of C)switch(S.type){case"splice":{let P=S.index+E;c.splice(P,S.numToDelete,...S.linesToInsert),E+=S.linesToInsert.length-S.numToDelete}break;case"pop":c.pop();break;case"push":c.push(S.line);break;default:je.assertNever(S);break}await r.writeFilePromise(e,c.join(` `),{mode:a})}function sOe(t,e,r){let s=[];for(let a of t.parts)switch(a.type){case"context":case"deletion":{for(let n of a.lines){let c=e[r];if(c==null||!gjt(c,n))return null;r+=1}a.type==="deletion"&&(s.push({type:"splice",index:r-a.lines.length,numToDelete:a.lines.length,linesToInsert:[]}),a.noNewlineAtEndOfFile&&s.push({type:"push",line:""}))}break;case"insertion":s.push({type:"splice",index:r,numToDelete:0,linesToInsert:a.lines}),a.noNewlineAtEndOfFile&&s.push({type:"pop"});break;default:je.assertNever(a.type);break}return s}var yjt=/^builtin<([^>]+)>$/;function M1(t,e){let{protocol:r,source:s,selector:a,params:n}=q.parseRange(t);if(r!=="patch:")throw new Error("Invalid patch range");if(s===null)throw new Error("Patch locators must explicitly define their source");let c=a?a.split(/&/).map(E=>ue.toPortablePath(E)):[],f=n&&typeof n.locator=="string"?q.parseLocator(n.locator):null,p=n&&typeof n.version=="string"?n.version:null,h=e(s);return{parentLocator:f,sourceItem:h,patchPaths:c,sourceVersion:p}}function NL(t){return t.range.startsWith("patch:")}function Tg(t){return t.reference.startsWith("patch:")}function lP(t){let{sourceItem:e,...r}=M1(t.range,q.parseDescriptor);return{...r,sourceDescriptor:e}}function cP(t){let{sourceItem:e,...r}=M1(t.reference,q.parseLocator);return{...r,sourceLocator:e}}function Ejt(t){let{sourceItem:e}=M1(t.range,q.parseDescriptor);return e}function Ijt(t){let{sourceItem:e}=M1(t.reference,q.parseLocator);return e}function dz(t){if(!NL(t))return t;let{sourceItem:e}=M1(t.range,q.parseDescriptor);return e}function OL(t){if(!Tg(t))return t;let{sourceItem:e}=M1(t.reference,q.parseLocator);return e}function oOe({parentLocator:t,sourceItem:e,patchPaths:r,sourceVersion:s,patchHash:a},n){let c=t!==null?{locator:q.stringifyLocator(t)}:{},f=typeof s<"u"?{version:s}:{},p=typeof a<"u"?{hash:a}:{};return q.makeRange({protocol:"patch:",source:n(e),selector:r.join("&"),params:{...f,...p,...c}})}function LL(t,{parentLocator:e,sourceDescriptor:r,patchPaths:s}){return q.makeDescriptor(t,oOe({parentLocator:e,sourceItem:r,patchPaths:s},q.stringifyDescriptor))}function mz(t,{parentLocator:e,sourcePackage:r,patchPaths:s,patchHash:a}){return q.makeLocator(t,oOe({parentLocator:e,sourceItem:r,sourceVersion:r.version,patchPaths:s,patchHash:a},q.stringifyLocator))}function aOe({onAbsolute:t,onRelative:e,onProject:r,onBuiltin:s},a){let n=a.lastIndexOf("!");n!==-1&&(a=a.slice(n+1));let c=a.match(yjt);return c!==null?s(c[1]):a.startsWith("~/")?r(a.slice(2)):K.isAbsolute(a)?t(a):e(a)}function lOe(t){let e=t.lastIndexOf("!");return{optional:(e!==-1?new Set(t.slice(0,e).split(/!/)):new Set).has("optional")}}function yz(t){return aOe({onAbsolute:()=>!1,onRelative:()=>!0,onProject:()=>!1,onBuiltin:()=>!1},t)}async function uP(t,e,r){let s=t!==null?await r.fetcher.fetch(t,r):null,a=s&&s.localPath?{packageFs:new Sn(vt.root),prefixPath:K.relative(vt.root,s.localPath)}:s;s&&s!==a&&s.releaseFs&&s.releaseFs();let n=await je.releaseAfterUseAsync(async()=>await Promise.all(e.map(async c=>{let f=lOe(c),p=await aOe({onAbsolute:async h=>await le.readFilePromise(h,"utf8"),onRelative:async h=>{if(a===null)throw new Error("Assertion failed: The parent locator should have been fetched");return await a.packageFs.readFilePromise(K.join(a.prefixPath,h),"utf8")},onProject:async h=>await le.readFilePromise(K.join(r.project.cwd,h),"utf8"),onBuiltin:async h=>await r.project.configuration.firstHook(E=>E.getBuiltinPatch,r.project,h)},c);return{...f,source:p}})));for(let c of n)typeof c.source=="string"&&(c.source=c.source.replace(/\r\n?/g,` `));return n}async function Ez(t,{cache:e,project:r}){let s=r.storedPackages.get(t.locatorHash);if(typeof s>"u")throw new Error("Assertion failed: Expected the package to be registered");let a=OL(t),n=r.storedChecksums,c=new Yi,f=await le.mktempPromise(),p=K.join(f,"source"),h=K.join(f,"user"),E=K.join(f,".yarn-patch.json"),C=r.configuration.makeFetcher(),S=[];try{let P,I;if(t.locatorHash===a.locatorHash){let R=await C.fetch(t,{cache:e,project:r,fetcher:C,checksums:n,report:c});S.push(()=>R.releaseFs?.()),P=R,I=R}else P=await C.fetch(t,{cache:e,project:r,fetcher:C,checksums:n,report:c}),S.push(()=>P.releaseFs?.()),I=await C.fetch(t,{cache:e,project:r,fetcher:C,checksums:n,report:c}),S.push(()=>I.releaseFs?.());await Promise.all([le.copyPromise(p,P.prefixPath,{baseFs:P.packageFs}),le.copyPromise(h,I.prefixPath,{baseFs:I.packageFs}),le.writeJsonPromise(E,{locator:q.stringifyLocator(t),version:s.version})])}finally{for(let P of S)P()}return le.detachTemp(f),h}async function Iz(t,e){let r=ue.fromPortablePath(t).replace(/\\/g,"/"),s=ue.fromPortablePath(e).replace(/\\/g,"/"),{stdout:a,stderr:n}=await Gr.execvp("git",["-c","core.safecrlf=false","diff","--src-prefix=a/","--dst-prefix=b/","--ignore-cr-at-eol","--full-index","--no-index","--no-renames","--text",r,s],{cwd:ue.toPortablePath(process.cwd()),env:{...process.env,GIT_CONFIG_NOSYSTEM:"1",HOME:"",XDG_CONFIG_HOME:"",USERPROFILE:""}});if(n.length>0)throw new Error(`Unable to diff directories. Make sure you have a recent version of 'git' available in PATH. The following error was reported by 'git': ${n}`);let c=r.startsWith("/")?f=>f.slice(1):f=>f;return a.replace(new RegExp(`(a|b)(${je.escapeRegExp(`/${c(r)}/`)})`,"g"),"$1/").replace(new RegExp(`(a|b)${je.escapeRegExp(`/${c(s)}/`)}`,"g"),"$1/").replace(new RegExp(je.escapeRegExp(`${r}/`),"g"),"").replace(new RegExp(je.escapeRegExp(`${s}/`),"g"),"")}function Cz(t,e){let r=[];for(let{source:s}of t){if(s===null)continue;let a=aP(s);for(let n of a){let{semverExclusivity:c,...f}=n;c!==null&&e!==null&&!Or.satisfiesWithPrereleases(e,c)||r.push(JSON.stringify(f))}}return Nn.makeHash(`${3}`,...r).slice(0,6)}Ve();function cOe(t,{configuration:e,report:r}){for(let s of t.parts)for(let a of s.lines)switch(s.type){case"context":r.reportInfo(null,` ${he.pretty(e,a,"grey")}`);break;case"deletion":r.reportError(28,`- ${he.pretty(e,a,he.Type.REMOVED)}`);break;case"insertion":r.reportError(28,`+ ${he.pretty(e,a,he.Type.ADDED)}`);break;default:je.assertNever(s.type)}}var fP=class{supports(e,r){return!!Tg(e)}getLocalPath(e,r){return null}async fetch(e,r){let s=r.checksums.get(e.locatorHash)||null,[a,n,c]=await r.cache.fetchPackageFromCache(e,s,{onHit:()=>r.report.reportCacheHit(e),onMiss:()=>r.report.reportCacheMiss(e,`${q.prettyLocator(r.project.configuration,e)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.patchPackage(e,r),...r.cacheOptions});return{packageFs:a,releaseFs:n,prefixPath:q.getIdentVendorPath(e),localPath:this.getLocalPath(e,r),checksum:c}}async patchPackage(e,r){let{parentLocator:s,sourceLocator:a,sourceVersion:n,patchPaths:c}=cP(e),f=await uP(s,c,r),p=await le.mktempPromise(),h=K.join(p,"current.zip"),E=await r.fetcher.fetch(a,r),C=q.getIdentVendorPath(e),S=new hs(h,{create:!0,level:r.project.configuration.get("compressionLevel")});await je.releaseAfterUseAsync(async()=>{await S.copyPromise(C,E.prefixPath,{baseFs:E.packageFs,stableSort:!0})},E.releaseFs),S.saveAndClose();for(let{source:P,optional:I}of f){if(P===null)continue;let R=new hs(h,{level:r.project.configuration.get("compressionLevel")}),N=new Sn(K.resolve(vt.root,C),{baseFs:R});try{await FL(aP(P),{baseFs:N,version:n})}catch(U){if(!(U instanceof O1))throw U;let W=r.project.configuration.get("enableInlineHunks"),te=!W&&!I?" (set enableInlineHunks for details)":"",ie=`${q.prettyLocator(r.project.configuration,e)}: ${U.message}${te}`,Ae=ce=>{W&&cOe(U.hunk,{configuration:r.project.configuration,report:ce})};if(R.discardAndClose(),I){r.report.reportWarningOnce(66,ie,{reportExtra:Ae});continue}else throw new Yt(66,ie,Ae)}R.saveAndClose()}return new hs(h,{level:r.project.configuration.get("compressionLevel")})}};Ve();var AP=class{supportsDescriptor(e,r){return!!NL(e)}supportsLocator(e,r){return!!Tg(e)}shouldPersistResolution(e,r){return!1}bindDescriptor(e,r,s){let{patchPaths:a}=lP(e);return a.every(n=>!yz(n))?e:q.bindDescriptor(e,{locator:q.stringifyLocator(r)})}getResolutionDependencies(e,r){let{sourceDescriptor:s}=lP(e);return{sourceDescriptor:r.project.configuration.normalizeDependency(s)}}async getCandidates(e,r,s){if(!s.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let{parentLocator:a,patchPaths:n}=lP(e),c=await uP(a,n,s.fetchOptions),f=r.sourceDescriptor;if(typeof f>"u")throw new Error("Assertion failed: The dependency should have been resolved");let p=Cz(c,f.version);return[mz(e,{parentLocator:a,sourcePackage:f,patchPaths:n,patchHash:p})]}async getSatisfying(e,r,s,a){let[n]=await this.getCandidates(e,r,a);return{locators:s.filter(c=>c.locatorHash===n.locatorHash),sorted:!1}}async resolve(e,r){let{sourceLocator:s}=cP(e);return{...await r.resolver.resolve(s,r),...e}}};Ve();bt();Wt();var _1=class extends ut{constructor(){super(...arguments);this.save=ge.Boolean("-s,--save",!1,{description:"Add the patch to your resolution entries"});this.patchFolder=ge.String()}static{this.paths=[["patch-commit"]]}static{this.usage=ot.Usage({description:"generate a patch out of a directory",details:"\n By default, this will print a patchfile on stdout based on the diff between the folder passed in and the original version of the package. Such file is suitable for consumption with the `patch:` protocol.\n\n With the `-s,--save` option set, the patchfile won't be printed on stdout anymore and will instead be stored within a local file (by default kept within `.yarn/patches`, but configurable via the `patchFolder` setting). A `resolutions` entry will also be added to your top-level manifest, referencing the patched package via the `patch:` protocol.\n\n Note that only folders generated by `yarn patch` are accepted as valid input for `yarn patch-commit`.\n "})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=K.resolve(this.context.cwd,ue.toPortablePath(this.patchFolder)),c=K.join(n,"../source"),f=K.join(n,"../.yarn-patch.json");if(!le.existsSync(c))throw new nt("The argument folder didn't get created by 'yarn patch'");let p=await Iz(c,n),h=await le.readJsonPromise(f),E=q.parseLocator(h.locator,!0);if(!s.storedPackages.has(E.locatorHash))throw new nt("No package found in the project for the given locator");if(!this.save){this.context.stdout.write(p);return}let C=r.get("patchFolder"),S=K.join(C,`${q.slugifyLocator(E)}.patch`);await le.mkdirPromise(C,{recursive:!0}),await le.writeFilePromise(S,p);let P=[],I=new Map;for(let R of s.storedPackages.values()){if(q.isVirtualLocator(R))continue;let N=R.dependencies.get(E.identHash);if(!N)continue;let U=q.ensureDevirtualizedDescriptor(N),W=dz(U),te=s.storedResolutions.get(W.descriptorHash);if(!te)throw new Error("Assertion failed: Expected the resolution to have been registered");if(!s.storedPackages.get(te))throw new Error("Assertion failed: Expected the package to have been registered");let Ae=s.tryWorkspaceByLocator(R);if(Ae)P.push(Ae);else{let ce=s.originalPackages.get(R.locatorHash);if(!ce)throw new Error("Assertion failed: Expected the original package to have been registered");let me=ce.dependencies.get(N.identHash);if(!me)throw new Error("Assertion failed: Expected the original dependency to have been registered");I.set(me.descriptorHash,me)}}for(let R of P)for(let N of Ht.hardDependencies){let U=R.manifest[N].get(E.identHash);if(!U)continue;let W=LL(U,{parentLocator:null,sourceDescriptor:q.convertLocatorToDescriptor(E),patchPaths:[K.join(Er.home,K.relative(s.cwd,S))]});R.manifest[N].set(U.identHash,W)}for(let R of I.values()){let N=LL(R,{parentLocator:null,sourceDescriptor:q.convertLocatorToDescriptor(E),patchPaths:[K.join(Er.home,K.relative(s.cwd,S))]});s.topLevelWorkspace.manifest.resolutions.push({pattern:{descriptor:{fullName:q.stringifyIdent(N),description:R.range}},reference:N.range})}await s.persist()}};Ve();bt();Wt();var U1=class extends ut{constructor(){super(...arguments);this.update=ge.Boolean("-u,--update",!1,{description:"Reapply local patches that already apply to this packages"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.package=ge.String()}static{this.paths=[["patch"]]}static{this.usage=ot.Usage({description:"prepare a package for patching",details:"\n This command will cause a package to be extracted in a temporary directory intended to be editable at will.\n\n Once you're done with your changes, run `yarn patch-commit -s path` (with `path` being the temporary directory you received) to generate a patchfile and register it into your top-level manifest via the `patch:` protocol. Run `yarn patch-commit -h` for more details.\n\n Calling the command when you already have a patch won't import it by default (in other words, the default behavior is to reset existing patches). However, adding the `-u,--update` flag will import any current patch.\n "})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let c=q.parseLocator(this.package);if(c.reference==="unknown"){let f=je.mapAndFilter([...s.storedPackages.values()],p=>p.identHash!==c.identHash?je.mapAndFilter.skip:q.isVirtualLocator(p)?je.mapAndFilter.skip:Tg(p)!==this.update?je.mapAndFilter.skip:p);if(f.length===0)throw new nt("No package found in the project for the given locator");if(f.length>1)throw new nt(`Multiple candidate packages found; explicitly choose one of them (use \`yarn why \` to get more information as to who depends on them): ${f.map(p=>` - ${q.prettyLocator(r,p)}`).join("")}`);c=f[0]}if(!s.storedPackages.has(c.locatorHash))throw new nt("No package found in the project for the given locator");await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async f=>{let p=OL(c),h=await Ez(c,{cache:n,project:s});f.reportJson({locator:q.stringifyLocator(p),path:ue.fromPortablePath(h)});let E=this.update?" along with its current modifications":"";f.reportInfo(0,`Package ${q.prettyLocator(r,p)} got extracted with success${E}!`),f.reportInfo(0,`You can now edit the following folder: ${he.pretty(r,ue.fromPortablePath(h),"magenta")}`),f.reportInfo(0,`Once you are done run ${he.pretty(r,`yarn patch-commit -s ${process.platform==="win32"?'"':""}${ue.fromPortablePath(h)}${process.platform==="win32"?'"':""}`,"cyan")} and Yarn will store a patchfile based on your changes.`)})}};var Cjt={configuration:{enableInlineHunks:{description:"If true, the installs will print unmatched patch hunks",type:"BOOLEAN",default:!1},patchFolder:{description:"Folder where the patch files must be written",type:"ABSOLUTE_PATH",default:"./.yarn/patches"}},commands:[_1,U1],fetchers:[fP],resolvers:[AP]},wjt=Cjt;var Sz={};Vt(Sz,{PnpmLinker:()=>pP,default:()=>Pjt});Ve();bt();Wt();var pP=class{getCustomDataKey(){return JSON.stringify({name:"PnpmLinker",version:3})}supportsPackage(e,r){return this.isEnabled(r)}async findPackageLocation(e,r){if(!this.isEnabled(r))throw new Error("Assertion failed: Expected the pnpm linker to be enabled");let s=this.getCustomDataKey(),a=r.project.linkersCustomData.get(s);if(!a)throw new nt(`The project in ${he.pretty(r.project.configuration,`${r.project.cwd}/package.json`,he.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let n=a.pathsByLocator.get(e.locatorHash);if(typeof n>"u")throw new nt(`Couldn't find ${q.prettyLocator(r.project.configuration,e)} in the currently installed pnpm map - running an install might help`);return n.packageLocation}async findPackageLocator(e,r){if(!this.isEnabled(r))return null;let s=this.getCustomDataKey(),a=r.project.linkersCustomData.get(s);if(!a)throw new nt(`The project in ${he.pretty(r.project.configuration,`${r.project.cwd}/package.json`,he.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let n=e.match(/(^.*\/node_modules\/(@[^/]*\/)?[^/]+)(\/.*$)/);if(n){let p=a.locatorByPath.get(n[1]);if(p)return p}let c=e,f=e;do{f=c,c=K.dirname(f);let p=a.locatorByPath.get(f);if(p)return p}while(c!==f);return null}makeInstaller(e){return new Bz(e)}isEnabled(e){return e.project.configuration.get("nodeLinker")==="pnpm"}},Bz=class{constructor(e){this.opts=e;this.asyncActions=new je.AsyncActions(10);this.customData={pathsByLocator:new Map,locatorByPath:new Map};this.indexFolderPromise=ax(le,{indexPath:K.join(e.project.configuration.get("globalFolder"),"index")})}attachCustomData(e){}async installPackage(e,r,s){switch(e.linkType){case"SOFT":return this.installPackageSoft(e,r,s);case"HARD":return this.installPackageHard(e,r,s)}throw new Error("Assertion failed: Unsupported package link type")}async installPackageSoft(e,r,s){let a=K.resolve(r.packageFs.getRealPath(),r.prefixPath),n=this.opts.project.tryWorkspaceByLocator(e)?K.join(a,Er.nodeModules):null;return this.customData.pathsByLocator.set(e.locatorHash,{packageLocation:a,dependenciesLocation:n}),{packageLocation:a,buildRequest:null}}async installPackageHard(e,r,s){let a=vjt(e,{project:this.opts.project}),n=a.packageLocation;this.customData.locatorByPath.set(n,q.stringifyLocator(e)),this.customData.pathsByLocator.set(e.locatorHash,a),s.holdFetchResult(this.asyncActions.set(e.locatorHash,async()=>{await le.mkdirPromise(n,{recursive:!0}),await le.copyPromise(n,r.prefixPath,{baseFs:r.packageFs,overwrite:!1,linkStrategy:{type:"HardlinkFromIndex",indexPath:await this.indexFolderPromise,autoRepair:!0}})}));let f=q.isVirtualLocator(e)?q.devirtualizeLocator(e):e,p={manifest:await Ht.tryFind(r.prefixPath,{baseFs:r.packageFs})??new Ht,misc:{hasBindingGyp:mA.hasBindingGyp(r)}},h=this.opts.project.getDependencyMeta(f,e.version),E=mA.extractBuildRequest(e,p,h,{configuration:this.opts.project.configuration});return{packageLocation:n,buildRequest:E}}async attachInternalDependencies(e,r){if(this.opts.project.configuration.get("nodeLinker")!=="pnpm"||!uOe(e,{project:this.opts.project}))return;let s=this.customData.pathsByLocator.get(e.locatorHash);if(typeof s>"u")throw new Error(`Assertion failed: Expected the package to have been registered (${q.stringifyLocator(e)})`);let{dependenciesLocation:a}=s;a&&this.asyncActions.reduce(e.locatorHash,async n=>{await le.mkdirPromise(a,{recursive:!0});let c=await Sjt(a),f=new Map(c),p=[n],h=(C,S)=>{let P=S;uOe(S,{project:this.opts.project})||(this.opts.report.reportWarningOnce(0,"The pnpm linker doesn't support providing different versions to workspaces' peer dependencies"),P=q.devirtualizeLocator(S));let I=this.customData.pathsByLocator.get(P.locatorHash);if(typeof I>"u")throw new Error(`Assertion failed: Expected the package to have been registered (${q.stringifyLocator(S)})`);let R=q.stringifyIdent(C),N=K.join(a,R),U=K.relative(K.dirname(N),I.packageLocation),W=f.get(R);f.delete(R),p.push(Promise.resolve().then(async()=>{if(W){if(W.isSymbolicLink()&&await le.readlinkPromise(N)===U)return;await le.removePromise(N)}await le.mkdirpPromise(K.dirname(N)),process.platform=="win32"&&this.opts.project.configuration.get("winLinkType")==="junctions"?await le.symlinkPromise(I.packageLocation,N,"junction"):await le.symlinkPromise(U,N)}))},E=!1;for(let[C,S]of r)C.identHash===e.identHash&&(E=!0),h(C,S);!E&&!this.opts.project.tryWorkspaceByLocator(e)&&h(q.convertLocatorToDescriptor(e),e),p.push(Djt(a,f)),await Promise.all(p)})}async attachExternalDependents(e,r){throw new Error("External dependencies haven't been implemented for the pnpm linker")}async finalizeInstall(){let e=fOe(this.opts.project);if(this.opts.project.configuration.get("nodeLinker")!=="pnpm")await le.removePromise(e);else{let r;try{r=new Set(await le.readdirPromise(e))}catch{r=new Set}for(let{dependenciesLocation:s}of this.customData.pathsByLocator.values()){if(!s)continue;let a=K.contains(e,s);if(a===null)continue;let[n]=a.split(K.sep);r.delete(n)}await Promise.all([...r].map(async s=>{await le.removePromise(K.join(e,s))}))}return await this.asyncActions.wait(),await vz(e),this.opts.project.configuration.get("nodeLinker")!=="node-modules"&&await vz(Bjt(this.opts.project)),{customData:this.customData}}};function Bjt(t){return K.join(t.cwd,Er.nodeModules)}function fOe(t){return t.configuration.get("pnpmStoreFolder")}function vjt(t,{project:e}){let r=q.slugifyLocator(t),s=fOe(e),a=K.join(s,r,"package"),n=K.join(s,r,Er.nodeModules);return{packageLocation:a,dependenciesLocation:n}}function uOe(t,{project:e}){return!q.isVirtualLocator(t)||!e.tryWorkspaceByLocator(t)}async function Sjt(t){let e=new Map,r=[];try{r=await le.readdirPromise(t,{withFileTypes:!0})}catch(s){if(s.code!=="ENOENT")throw s}try{for(let s of r)if(!s.name.startsWith("."))if(s.name.startsWith("@")){let a=await le.readdirPromise(K.join(t,s.name),{withFileTypes:!0});if(a.length===0)e.set(s.name,s);else for(let n of a)e.set(`${s.name}/${n.name}`,n)}else e.set(s.name,s)}catch(s){if(s.code!=="ENOENT")throw s}return e}async function Djt(t,e){let r=[],s=new Set;for(let a of e.keys()){r.push(le.removePromise(K.join(t,a)));let n=q.tryParseIdent(a)?.scope;n&&s.add(`@${n}`)}return Promise.all(r).then(()=>Promise.all([...s].map(a=>vz(K.join(t,a)))))}async function vz(t){try{await le.rmdirPromise(t)}catch(e){if(e.code!=="ENOENT"&&e.code!=="ENOTEMPTY")throw e}}var bjt={configuration:{pnpmStoreFolder:{description:"By default, the store is stored in the 'node_modules/.store' of the project. Sometimes in CI scenario's it is convenient to store this in a different location so it can be cached and reused.",type:"ABSOLUTE_PATH",default:"./node_modules/.store"}},linkers:[pP]},Pjt=bjt;var Tz={};Vt(Tz,{StageCommand:()=>H1,default:()=>_jt,stageUtils:()=>_L});Ve();bt();Wt();Ve();bt();var _L={};Vt(_L,{ActionType:()=>Dz,checkConsensus:()=>ML,expandDirectory:()=>xz,findConsensus:()=>kz,findVcsRoot:()=>bz,genCommitMessage:()=>Qz,getCommitPrefix:()=>AOe,isYarnFile:()=>Pz});bt();var Dz=(n=>(n[n.CREATE=0]="CREATE",n[n.DELETE=1]="DELETE",n[n.ADD=2]="ADD",n[n.REMOVE=3]="REMOVE",n[n.MODIFY=4]="MODIFY",n))(Dz||{});async function bz(t,{marker:e}){do if(!le.existsSync(K.join(t,e)))t=K.dirname(t);else return t;while(t!=="/");return null}function Pz(t,{roots:e,names:r}){if(r.has(K.basename(t)))return!0;do if(!e.has(t))t=K.dirname(t);else return!0;while(t!=="/");return!1}function xz(t){let e=[],r=[t];for(;r.length>0;){let s=r.pop(),a=le.readdirSync(s);for(let n of a){let c=K.resolve(s,n);le.lstatSync(c).isDirectory()?r.push(c):e.push(c)}}return e}function ML(t,e){let r=0,s=0;for(let a of t)a!=="wip"&&(e.test(a)?r+=1:s+=1);return r>=s}function kz(t){let e=ML(t,/^(\w\(\w+\):\s*)?\w+s/),r=ML(t,/^(\w\(\w+\):\s*)?[A-Z]/),s=ML(t,/^\w\(\w+\):/);return{useThirdPerson:e,useUpperCase:r,useComponent:s}}function AOe(t){return t.useComponent?"chore(yarn): ":""}var xjt=new Map([[0,"create"],[1,"delete"],[2,"add"],[3,"remove"],[4,"update"]]);function Qz(t,e){let r=AOe(t),s=[],a=e.slice().sort((n,c)=>n[0]-c[0]);for(;a.length>0;){let[n,c]=a.shift(),f=xjt.get(n);t.useUpperCase&&s.length===0&&(f=`${f[0].toUpperCase()}${f.slice(1)}`),t.useThirdPerson&&(f+="s");let p=[c];for(;a.length>0&&a[0][0]===n;){let[,E]=a.shift();p.push(E)}p.sort();let h=p.shift();p.length===1?h+=" (and one other)":p.length>1&&(h+=` (and ${p.length} others)`),s.push(`${f} ${h}`)}return`${r}${s.join(", ")}`}var kjt="Commit generated via `yarn stage`",Qjt=11;async function pOe(t){let{code:e,stdout:r}=await Gr.execvp("git",["log","-1","--pretty=format:%H"],{cwd:t});return e===0?r.trim():null}async function Tjt(t,e){let r=[],s=e.filter(h=>K.basename(h.path)==="package.json");for(let{action:h,path:E}of s){let C=K.relative(t,E);if(h===4){let S=await pOe(t),{stdout:P}=await Gr.execvp("git",["show",`${S}:${C}`],{cwd:t,strict:!0}),I=await Ht.fromText(P),R=await Ht.fromFile(E),N=new Map([...R.dependencies,...R.devDependencies]),U=new Map([...I.dependencies,...I.devDependencies]);for(let[W,te]of U){let ie=q.stringifyIdent(te),Ae=N.get(W);Ae?Ae.range!==te.range&&r.push([4,`${ie} to ${Ae.range}`]):r.push([3,ie])}for(let[W,te]of N)U.has(W)||r.push([2,q.stringifyIdent(te)])}else if(h===0){let S=await Ht.fromFile(E);S.name?r.push([0,q.stringifyIdent(S.name)]):r.push([0,"a package"])}else if(h===1){let S=await pOe(t),{stdout:P}=await Gr.execvp("git",["show",`${S}:${C}`],{cwd:t,strict:!0}),I=await Ht.fromText(P);I.name?r.push([1,q.stringifyIdent(I.name)]):r.push([1,"a package"])}else throw new Error("Assertion failed: Unsupported action type")}let{code:a,stdout:n}=await Gr.execvp("git",["log",`-${Qjt}`,"--pretty=format:%s"],{cwd:t}),c=a===0?n.split(/\n/g).filter(h=>h!==""):[],f=kz(c);return Qz(f,r)}var Rjt={0:[" A ","?? "],4:[" M "],1:[" D "]},Fjt={0:["A "],4:["M "],1:["D "]},hOe={async findRoot(t){return await bz(t,{marker:".git"})},async filterChanges(t,e,r,s){let{stdout:a}=await Gr.execvp("git",["status","-s"],{cwd:t,strict:!0}),n=a.toString().split(/\n/g),c=s?.staged?Fjt:Rjt;return[].concat(...n.map(p=>{if(p==="")return[];let h=p.slice(0,3),E=K.resolve(t,p.slice(3));if(!s?.staged&&h==="?? "&&p.endsWith("/"))return xz(E).map(C=>({action:0,path:C}));{let S=[0,4,1].find(P=>c[P].includes(h));return S!==void 0?[{action:S,path:E}]:[]}})).filter(p=>Pz(p.path,{roots:e,names:r}))},async genCommitMessage(t,e){return await Tjt(t,e)},async makeStage(t,e){let r=e.map(s=>ue.fromPortablePath(s.path));await Gr.execvp("git",["add","--",...r],{cwd:t,strict:!0})},async makeCommit(t,e,r){let s=e.map(a=>ue.fromPortablePath(a.path));await Gr.execvp("git",["add","-N","--",...s],{cwd:t,strict:!0}),await Gr.execvp("git",["commit","-m",`${r} ${kjt} `,"--",...s],{cwd:t,strict:!0})},async makeReset(t,e){let r=e.map(s=>ue.fromPortablePath(s.path));await Gr.execvp("git",["reset","HEAD","--",...r],{cwd:t,strict:!0})}};var Njt=[hOe],H1=class extends ut{constructor(){super(...arguments);this.commit=ge.Boolean("-c,--commit",!1,{description:"Commit the staged files"});this.reset=ge.Boolean("-r,--reset",!1,{description:"Remove all files from the staging area"});this.dryRun=ge.Boolean("-n,--dry-run",!1,{description:"Print the commit message and the list of modified files without staging / committing"});this.update=ge.Boolean("-u,--update",!1,{hidden:!0})}static{this.paths=[["stage"]]}static{this.usage=ot.Usage({description:"add all yarn files to your vcs",details:"\n This command will add to your staging area the files belonging to Yarn (typically any modified `package.json` and `.yarnrc.yml` files, but also linker-generated files, cache data, etc). It will take your ignore list into account, so the cache files won't be added if the cache is ignored in a `.gitignore` file (assuming you use Git).\n\n Running `--reset` will instead remove them from the staging area (the changes will still be there, but won't be committed until you stage them back).\n\n Since the staging area is a non-existent concept in Mercurial, Yarn will always create a new commit when running this command on Mercurial repositories. You can get this behavior when using Git by using the `--commit` flag which will directly create a commit.\n ",examples:[["Adds all modified project files to the staging area","yarn stage"],["Creates a new commit containing all modified project files","yarn stage --commit"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s}=await Tt.find(r,this.context.cwd),{driver:a,root:n}=await Ojt(s.cwd),c=[r.get("cacheFolder"),r.get("globalFolder"),r.get("virtualFolder"),r.get("yarnPath")];await r.triggerHook(C=>C.populateYarnPaths,s,C=>{c.push(C)});let f=new Set;for(let C of c)for(let S of Ljt(n,C))f.add(S);let p=new Set([r.get("rcFilename"),Er.lockfile,Er.manifest]),h=await a.filterChanges(n,f,p),E=await a.genCommitMessage(n,h);if(this.dryRun)if(this.commit)this.context.stdout.write(`${E} `);else for(let C of h)this.context.stdout.write(`${ue.fromPortablePath(C.path)} `);else if(this.reset){let C=await a.filterChanges(n,f,p,{staged:!0});C.length===0?this.context.stdout.write("No staged changes found!"):await a.makeReset(n,C)}else h.length===0?this.context.stdout.write("No changes found!"):this.commit?await a.makeCommit(n,h,E):(await a.makeStage(n,h),this.context.stdout.write(E))}};async function Ojt(t){let e=null,r=null;for(let s of Njt)if((r=await s.findRoot(t))!==null){e=s;break}if(e===null||r===null)throw new nt("No stage driver has been found for your current project");return{driver:e,root:r}}function Ljt(t,e){let r=[];if(e===null)return r;for(;;){(e===t||e.startsWith(`${t}/`))&&r.push(e);let s;try{s=le.statSync(e)}catch{break}if(s.isSymbolicLink())e=K.resolve(K.dirname(e),le.readlinkSync(e));else break}return r}var Mjt={commands:[H1]},_jt=Mjt;var Rz={};Vt(Rz,{default:()=>Vjt});Ve();Ve();bt();var mOe=et(Ai());Ve();var gOe=et(G9()),Ujt="e8e1bd300d860104bb8c58453ffa1eb4",Hjt="OFCNCOG2CU",dOe=async(t,e)=>{let r=q.stringifyIdent(t),a=jjt(e).initIndex("npm-search");try{return(await a.getObject(r,{attributesToRetrieve:["types"]})).types?.ts==="definitely-typed"}catch{return!1}},jjt=t=>(0,gOe.default)(Hjt,Ujt,{requester:{async send(r){try{let s=await An.request(r.url,r.data||null,{configuration:t,headers:r.headers});return{content:s.body,isTimedOut:!1,status:s.statusCode}}catch(s){return{content:s.response.body,isTimedOut:!1,status:s.response.statusCode}}}}});var yOe=t=>t.scope?`${t.scope}__${t.name}`:`${t.name}`,qjt=async(t,e,r,s)=>{if(r.scope==="types")return;let{project:a}=t,{configuration:n}=a;if(!(n.get("tsEnableAutoTypes")??(le.existsSync(K.join(t.cwd,"tsconfig.json"))||le.existsSync(K.join(a.cwd,"tsconfig.json")))))return;let f=n.makeResolver(),p={project:a,resolver:f,report:new Yi};if(!await dOe(r,n))return;let E=yOe(r),C=q.parseRange(r.range).selector;if(!Or.validRange(C)){let N=n.normalizeDependency(r),U=await f.getCandidates(N,{},p);C=q.parseRange(U[0].reference).selector}let S=mOe.default.coerce(C);if(S===null)return;let P=`${Xu.Modifier.CARET}${S.major}`,I=q.makeDescriptor(q.makeIdent("types",E),P),R=je.mapAndFind(a.workspaces,N=>{let U=N.manifest.dependencies.get(r.identHash)?.descriptorHash,W=N.manifest.devDependencies.get(r.identHash)?.descriptorHash;if(U!==r.descriptorHash&&W!==r.descriptorHash)return je.mapAndFind.skip;let te=[];for(let ie of Ht.allDependencies){let Ae=N.manifest[ie].get(I.identHash);typeof Ae>"u"||te.push([ie,Ae])}return te.length===0?je.mapAndFind.skip:te});if(typeof R<"u")for(let[N,U]of R)t.manifest[N].set(U.identHash,U);else{try{let N=n.normalizeDependency(I);if((await f.getCandidates(N,{},p)).length===0)return}catch{return}t.manifest[Xu.Target.DEVELOPMENT].set(I.identHash,I)}},Gjt=async(t,e,r)=>{if(r.scope==="types")return;let{project:s}=t,{configuration:a}=s;if(!(a.get("tsEnableAutoTypes")??(le.existsSync(K.join(t.cwd,"tsconfig.json"))||le.existsSync(K.join(s.cwd,"tsconfig.json")))))return;let c=yOe(r),f=q.makeIdent("types",c);for(let p of Ht.allDependencies)typeof t.manifest[p].get(f.identHash)>"u"||t.manifest[p].delete(f.identHash)},Wjt=(t,e)=>{e.publishConfig&&e.publishConfig.typings&&(e.typings=e.publishConfig.typings),e.publishConfig&&e.publishConfig.types&&(e.types=e.publishConfig.types)},Yjt={configuration:{tsEnableAutoTypes:{description:"Whether Yarn should auto-install @types/ dependencies on 'yarn add'",type:"BOOLEAN",isNullable:!0,default:null}},hooks:{afterWorkspaceDependencyAddition:qjt,afterWorkspaceDependencyRemoval:Gjt,beforeWorkspacePacking:Wjt}},Vjt=Yjt;var Mz={};Vt(Mz,{VersionApplyCommand:()=>Y1,VersionCheckCommand:()=>V1,VersionCommand:()=>K1,default:()=>A6t,versionUtils:()=>W1});Ve();Ve();Wt();var W1={};Vt(W1,{Decision:()=>q1,applyPrerelease:()=>vOe,applyReleases:()=>Lz,applyStrategy:()=>HL,clearVersionFiles:()=>Fz,getUndecidedDependentWorkspaces:()=>gP,getUndecidedWorkspaces:()=>UL,openVersionFile:()=>G1,requireMoreDecisions:()=>c6t,resolveVersionFiles:()=>hP,suggestStrategy:()=>Oz,updateVersionFiles:()=>Nz,validateReleaseDecision:()=>j1});Ve();bt();Bc();Wt();var BOe=et(wOe()),TA=et(Ai()),l6t=/^(>=|[~^]|)(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$/,q1=(c=>(c.UNDECIDED="undecided",c.DECLINE="decline",c.MAJOR="major",c.MINOR="minor",c.PATCH="patch",c.PRERELEASE="prerelease",c))(q1||{});function j1(t){let e=TA.default.valid(t);return e||je.validateEnum((0,BOe.default)(q1,"UNDECIDED"),t)}async function hP(t,{prerelease:e=null}={}){let r=new Map,s=t.configuration.get("deferredVersionFolder");if(!le.existsSync(s))return r;let a=await le.readdirPromise(s);for(let n of a){if(!n.endsWith(".yml"))continue;let c=K.join(s,n),f=await le.readFilePromise(c,"utf8"),p=cs(f);for(let[h,E]of Object.entries(p.releases||{})){if(E==="decline")continue;let C=q.parseIdent(h),S=t.tryWorkspaceByIdent(C);if(S===null)throw new Error(`Assertion failed: Expected a release definition file to only reference existing workspaces (${K.basename(c)} references ${h})`);if(S.manifest.version===null)throw new Error(`Assertion failed: Expected the workspace to have a version (${q.prettyLocator(t.configuration,S.anchoredLocator)})`);let P=S.manifest.raw.stableVersion??S.manifest.version,I=r.get(S),R=HL(P,j1(E));if(R===null)throw new Error(`Assertion failed: Expected ${P} to support being bumped via strategy ${E}`);let N=typeof I<"u"?TA.default.gt(R,I)?R:I:R;r.set(S,N)}}return e&&(r=new Map([...r].map(([n,c])=>[n,vOe(c,{current:n.manifest.version,prerelease:e})]))),r}async function Fz(t){let e=t.configuration.get("deferredVersionFolder");le.existsSync(e)&&await le.removePromise(e)}async function Nz(t,e){let r=new Set(e),s=t.configuration.get("deferredVersionFolder");if(!le.existsSync(s))return;let a=await le.readdirPromise(s);for(let n of a){if(!n.endsWith(".yml"))continue;let c=K.join(s,n),f=await le.readFilePromise(c,"utf8"),p=cs(f),h=p?.releases;if(h){for(let E of Object.keys(h)){let C=q.parseIdent(E),S=t.tryWorkspaceByIdent(C);(S===null||r.has(S))&&delete p.releases[E]}Object.keys(p.releases).length>0?await le.changeFilePromise(c,il(new il.PreserveOrdering(p))):await le.unlinkPromise(c)}}}async function G1(t,{allowEmpty:e=!1}={}){let r=t.configuration;if(r.projectCwd===null)throw new nt("This command can only be run from within a Yarn project");let s=await Qa.fetchRoot(r.projectCwd),a=s!==null?await Qa.fetchBase(s,{baseRefs:r.get("changesetBaseRefs")}):null,n=s!==null?await Qa.fetchChangedFiles(s,{base:a.hash,project:t}):[],c=r.get("deferredVersionFolder"),f=n.filter(P=>K.contains(c,P)!==null);if(f.length>1)throw new nt(`Your current branch contains multiple versioning files; this isn't supported: - ${f.map(P=>ue.fromPortablePath(P)).join(` - `)}`);let p=new Set(je.mapAndFilter(n,P=>{let I=t.tryWorkspaceByFilePath(P);return I===null?je.mapAndFilter.skip:I}));if(f.length===0&&p.size===0&&!e)return null;let h=f.length===1?f[0]:K.join(c,`${Nn.makeHash(Math.random().toString()).slice(0,8)}.yml`),E=le.existsSync(h)?await le.readFilePromise(h,"utf8"):"{}",C=cs(E),S=new Map;for(let P of C.declined||[]){let I=q.parseIdent(P),R=t.getWorkspaceByIdent(I);S.set(R,"decline")}for(let[P,I]of Object.entries(C.releases||{})){let R=q.parseIdent(P),N=t.getWorkspaceByIdent(R);S.set(N,j1(I))}return{project:t,root:s,baseHash:a!==null?a.hash:null,baseTitle:a!==null?a.title:null,changedFiles:new Set(n),changedWorkspaces:p,releaseRoots:new Set([...p].filter(P=>P.manifest.version!==null)),releases:S,async saveAll(){let P={},I=[],R=[];for(let N of t.workspaces){if(N.manifest.version===null)continue;let U=q.stringifyIdent(N.anchoredLocator),W=S.get(N);W==="decline"?I.push(U):typeof W<"u"?P[U]=j1(W):p.has(N)&&R.push(U)}await le.mkdirPromise(K.dirname(h),{recursive:!0}),await le.changeFilePromise(h,il(new il.PreserveOrdering({releases:Object.keys(P).length>0?P:void 0,declined:I.length>0?I:void 0,undecided:R.length>0?R:void 0})))}}}function c6t(t){return UL(t).size>0||gP(t).length>0}function UL(t){let e=new Set;for(let r of t.changedWorkspaces)r.manifest.version!==null&&(t.releases.has(r)||e.add(r));return e}function gP(t,{include:e=new Set}={}){let r=[],s=new Map(je.mapAndFilter([...t.releases],([n,c])=>c==="decline"?je.mapAndFilter.skip:[n.anchoredLocator.locatorHash,n])),a=new Map(je.mapAndFilter([...t.releases],([n,c])=>c!=="decline"?je.mapAndFilter.skip:[n.anchoredLocator.locatorHash,n]));for(let n of t.project.workspaces)if(!(!e.has(n)&&(a.has(n.anchoredLocator.locatorHash)||s.has(n.anchoredLocator.locatorHash)))&&n.manifest.version!==null)for(let c of Ht.hardDependencies)for(let f of n.manifest.getForScope(c).values()){let p=t.project.tryWorkspaceByDescriptor(f);p!==null&&s.has(p.anchoredLocator.locatorHash)&&r.push([n,p])}return r}function Oz(t,e){let r=TA.default.clean(e);for(let s of Object.values(q1))if(s!=="undecided"&&s!=="decline"&&TA.default.inc(t,s)===r)return s;return null}function HL(t,e){if(TA.default.valid(e))return e;if(t===null)throw new nt(`Cannot apply the release strategy "${e}" unless the workspace already has a valid version`);if(!TA.default.valid(t))throw new nt(`Cannot apply the release strategy "${e}" on a non-semver version (${t})`);let r=TA.default.inc(t,e);if(r===null)throw new nt(`Cannot apply the release strategy "${e}" on the specified version (${t})`);return r}function Lz(t,e,{report:r,exact:s}){let a=new Map;for(let n of t.workspaces)for(let c of Ht.allDependencies)for(let f of n.manifest[c].values()){let p=t.tryWorkspaceByDescriptor(f);if(p===null||!e.has(p))continue;je.getArrayWithDefault(a,p).push([n,c,f.identHash])}for(let[n,c]of e){let f=n.manifest.version;n.manifest.version=c,TA.default.prerelease(c)===null?delete n.manifest.raw.stableVersion:n.manifest.raw.stableVersion||(n.manifest.raw.stableVersion=f);let p=n.manifest.name!==null?q.stringifyIdent(n.manifest.name):null;r.reportInfo(0,`${q.prettyLocator(t.configuration,n.anchoredLocator)}: Bumped to ${c}`),r.reportJson({cwd:ue.fromPortablePath(n.cwd),ident:p,oldVersion:f,newVersion:c});let h=a.get(n);if(!(typeof h>"u"))for(let[E,C,S]of h){let P=E.manifest[C].get(S);if(typeof P>"u")throw new Error("Assertion failed: The dependency should have existed");let I=P.range,R=!1;if(I.startsWith(Ei.protocol)&&(I=I.slice(Ei.protocol.length),R=!0,I===n.relativeCwd))continue;let N=I.match(l6t);if(!N){r.reportWarning(0,`Couldn't auto-upgrade range ${I} (in ${q.prettyLocator(t.configuration,E.anchoredLocator)})`);continue}let U=s?`${c}`:`${N[1]}${c}`;R&&(U=`${Ei.protocol}${U}`);let W=q.makeDescriptor(P,U);E.manifest[C].set(S,W)}}}var u6t=new Map([["%n",{extract:t=>t.length>=1?[t[0],t.slice(1)]:null,generate:(t=0)=>`${t+1}`}]]);function vOe(t,{current:e,prerelease:r}){let s=new TA.default.SemVer(e),a=s.prerelease.slice(),n=[];s.prerelease=[],s.format()!==t&&(a.length=0);let c=!0,f=r.split(/\./g);for(let p of f){let h=u6t.get(p);if(typeof h>"u")n.push(p),a[0]===p?a.shift():c=!1;else{let E=c?h.extract(a):null;E!==null&&typeof E[0]=="number"?(n.push(h.generate(E[0])),a=E[1]):(n.push(h.generate()),c=!1)}}return s.prerelease&&(s.prerelease=[]),`${t}-${n.join(".")}`}var Y1=class extends ut{constructor(){super(...arguments);this.all=ge.Boolean("--all",!1,{description:"Apply the deferred version changes on all workspaces"});this.dryRun=ge.Boolean("--dry-run",!1,{description:"Print the versions without actually generating the package archive"});this.prerelease=ge.String("--prerelease",{description:"Add a prerelease identifier to new versions",tolerateBoolean:!0});this.exact=ge.Boolean("--exact",!1,{description:"Use the exact version of each package, removes any range. Useful for nightly releases where the range might match another version."});this.recursive=ge.Boolean("-R,--recursive",{description:"Release the transitive workspaces as well"});this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}static{this.paths=[["version","apply"]]}static{this.usage=ot.Usage({category:"Release-related commands",description:"apply all the deferred version bumps at once",details:` This command will apply the deferred version changes and remove their definitions from the repository. Note that if \`--prerelease\` is set, the given prerelease identifier (by default \`rc.%n\`) will be used on all new versions and the version definitions will be kept as-is. By default only the current workspace will be bumped, but you can configure this behavior by using one of: - \`--recursive\` to also apply the version bump on its dependencies - \`--all\` to apply the version bump on all packages in the repository Note that this command will also update the \`workspace:\` references across all your local workspaces, thus ensuring that they keep referring to the same workspaces even after the version bump. `,examples:[["Apply the version change to the local workspace","yarn version apply"],["Apply the version change to all the workspaces in the local workspace","yarn version apply --all"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);if(!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState({restoreResolutions:!1});let c=await Ot.start({configuration:r,json:this.json,stdout:this.context.stdout},async f=>{let p=this.prerelease?typeof this.prerelease!="boolean"?this.prerelease:"rc.%n":null,h=await hP(s,{prerelease:p}),E=new Map;if(this.all)E=h;else{let C=this.recursive?a.getRecursiveWorkspaceDependencies():[a];for(let S of C){let P=h.get(S);typeof P<"u"&&E.set(S,P)}}if(E.size===0){let C=h.size>0?" Did you want to add --all?":"";f.reportWarning(0,`The current workspace doesn't seem to require a version bump.${C}`);return}Lz(s,E,{report:f,exact:this.exact}),this.dryRun||(p||(this.all?await Fz(s):await Nz(s,[...E.keys()])),f.reportSeparator())});return this.dryRun||c.hasErrors()?c.exitCode():await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:n})}};Ve();bt();Wt();var jL=et(Ai());var V1=class extends ut{constructor(){super(...arguments);this.interactive=ge.Boolean("-i,--interactive",{description:"Open an interactive interface used to set version bumps"})}static{this.paths=[["version","check"]]}static{this.usage=ot.Usage({category:"Release-related commands",description:"check that all the relevant packages have been bumped",details:"\n **Warning:** This command currently requires Git.\n\n This command will check that all the packages covered by the files listed in argument have been properly bumped or declined to bump.\n\n In the case of a bump, the check will also cover transitive packages - meaning that should `Foo` be bumped, a package `Bar` depending on `Foo` will require a decision as to whether `Bar` will need to be bumped. This check doesn't cross packages that have declined to bump.\n\n In case no arguments are passed to the function, the list of modified files will be generated by comparing the HEAD against `master`.\n ",examples:[["Check whether the modified packages need a bump","yarn version check"]]})}async execute(){return this.interactive?await this.executeInteractive():await this.executeStandard()}async executeInteractive(){iw(this.context);let{Gem:r}=await Promise.resolve().then(()=>(YF(),cY)),{ScrollableItems:s}=await Promise.resolve().then(()=>(zF(),JF)),{FocusRequest:a}=await Promise.resolve().then(()=>(fY(),PPe)),{useListInput:n}=await Promise.resolve().then(()=>(KF(),xPe)),{renderForm:c}=await Promise.resolve().then(()=>(eN(),$F)),{Box:f,Text:p}=await Promise.resolve().then(()=>et(Vc())),{default:h,useCallback:E,useState:C}=await Promise.resolve().then(()=>et(hn())),S=await ze.find(this.context.cwd,this.context.plugins),{project:P,workspace:I}=await Tt.find(S,this.context.cwd);if(!I)throw new ar(P.cwd,this.context.cwd);await P.restoreInstallState();let R=await G1(P);if(R===null||R.releaseRoots.size===0)return 0;if(R.root===null)throw new nt("This command can only be run on Git repositories");let N=()=>h.createElement(f,{flexDirection:"row",paddingBottom:1},h.createElement(f,{flexDirection:"column",width:60},h.createElement(f,null,h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},""),"/",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to select workspaces.")),h.createElement(f,null,h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},""),"/",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to select release strategies."))),h.createElement(f,{flexDirection:"column"},h.createElement(f,{marginLeft:1},h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to save.")),h.createElement(f,{marginLeft:1},h.createElement(p,null,"Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to abort.")))),U=({workspace:me,active:pe,decision:Be,setDecision:Ce})=>{let g=me.manifest.raw.stableVersion??me.manifest.version;if(g===null)throw new Error(`Assertion failed: The version should have been set (${q.prettyLocator(S,me.anchoredLocator)})`);if(jL.default.prerelease(g)!==null)throw new Error(`Assertion failed: Prerelease identifiers shouldn't be found (${g})`);let we=["undecided","decline","patch","minor","major"];n(Be,we,{active:pe,minus:"left",plus:"right",set:Ce});let ye=Be==="undecided"?h.createElement(p,{color:"yellow"},g):Be==="decline"?h.createElement(p,{color:"green"},g):h.createElement(p,null,h.createElement(p,{color:"magenta"},g)," \u2192 ",h.createElement(p,{color:"green"},jL.default.valid(Be)?Be:jL.default.inc(g,Be)));return h.createElement(f,{flexDirection:"column"},h.createElement(f,null,h.createElement(p,null,q.prettyLocator(S,me.anchoredLocator)," - ",ye)),h.createElement(f,null,we.map(fe=>h.createElement(f,{key:fe,paddingLeft:2},h.createElement(p,null,h.createElement(r,{active:fe===Be})," ",fe)))))},W=me=>{let pe=new Set(R.releaseRoots),Be=new Map([...me].filter(([Ce])=>pe.has(Ce)));for(;;){let Ce=gP({project:R.project,releases:Be}),g=!1;if(Ce.length>0){for(let[we]of Ce)if(!pe.has(we)){pe.add(we),g=!0;let ye=me.get(we);typeof ye<"u"&&Be.set(we,ye)}}if(!g)break}return{relevantWorkspaces:pe,relevantReleases:Be}},te=()=>{let[me,pe]=C(()=>new Map(R.releases)),Be=E((Ce,g)=>{let we=new Map(me);g!=="undecided"?we.set(Ce,g):we.delete(Ce);let{relevantReleases:ye}=W(we);pe(ye)},[me,pe]);return[me,Be]},ie=({workspaces:me,releases:pe})=>{let Be=[];Be.push(`${me.size} total`);let Ce=0,g=0;for(let we of me){let ye=pe.get(we);typeof ye>"u"?g+=1:ye!=="decline"&&(Ce+=1)}return Be.push(`${Ce} release${Ce===1?"":"s"}`),Be.push(`${g} remaining`),h.createElement(p,{color:"yellow"},Be.join(", "))},ce=await c(({useSubmit:me})=>{let[pe,Be]=te();me(pe);let{relevantWorkspaces:Ce}=W(pe),g=new Set([...Ce].filter(se=>!R.releaseRoots.has(se))),[we,ye]=C(0),fe=E(se=>{switch(se){case a.BEFORE:ye(we-1);break;case a.AFTER:ye(we+1);break}},[we,ye]);return h.createElement(f,{flexDirection:"column"},h.createElement(N,null),h.createElement(f,null,h.createElement(p,{wrap:"wrap"},"The following files have been modified in your local checkout.")),h.createElement(f,{flexDirection:"column",marginTop:1,paddingLeft:2},[...R.changedFiles].map(se=>h.createElement(f,{key:se},h.createElement(p,null,h.createElement(p,{color:"grey"},ue.fromPortablePath(R.root)),ue.sep,ue.relative(ue.fromPortablePath(R.root),ue.fromPortablePath(se)))))),R.releaseRoots.size>0&&h.createElement(h.Fragment,null,h.createElement(f,{marginTop:1},h.createElement(p,{wrap:"wrap"},"Because of those files having been modified, the following workspaces may need to be released again (note that private workspaces are also shown here, because even though they won't be published, releasing them will allow us to flag their dependents for potential re-release):")),g.size>3?h.createElement(f,{marginTop:1},h.createElement(ie,{workspaces:R.releaseRoots,releases:pe})):null,h.createElement(f,{marginTop:1,flexDirection:"column"},h.createElement(s,{active:we%2===0,radius:1,size:2,onFocusRequest:fe},[...R.releaseRoots].map(se=>h.createElement(U,{key:se.cwd,workspace:se,decision:pe.get(se)||"undecided",setDecision:X=>Be(se,X)}))))),g.size>0?h.createElement(h.Fragment,null,h.createElement(f,{marginTop:1},h.createElement(p,{wrap:"wrap"},"The following workspaces depend on other workspaces that have been marked for release, and thus may need to be released as well:")),h.createElement(f,null,h.createElement(p,null,"(Press ",h.createElement(p,{bold:!0,color:"cyanBright"},"")," to move the focus between the workspace groups.)")),g.size>5?h.createElement(f,{marginTop:1},h.createElement(ie,{workspaces:g,releases:pe})):null,h.createElement(f,{marginTop:1,flexDirection:"column"},h.createElement(s,{active:we%2===1,radius:2,size:2,onFocusRequest:fe},[...g].map(se=>h.createElement(U,{key:se.cwd,workspace:se,decision:pe.get(se)||"undecided",setDecision:X=>Be(se,X)}))))):null)},{versionFile:R},{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});if(typeof ce>"u")return 1;R.releases.clear();for(let[me,pe]of ce)R.releases.set(me,pe);await R.saveAll()}async executeStandard(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);return await s.restoreInstallState(),(await Ot.start({configuration:r,stdout:this.context.stdout},async c=>{let f=await G1(s);if(f===null||f.releaseRoots.size===0)return;if(f.root===null)throw new nt("This command can only be run on Git repositories");if(c.reportInfo(0,`Your PR was started right after ${he.pretty(r,f.baseHash.slice(0,7),"yellow")} ${he.pretty(r,f.baseTitle,"magenta")}`),f.changedFiles.size>0){c.reportInfo(0,"You have changed the following files since then:"),c.reportSeparator();for(let S of f.changedFiles)c.reportInfo(null,`${he.pretty(r,ue.fromPortablePath(f.root),"gray")}${ue.sep}${ue.relative(ue.fromPortablePath(f.root),ue.fromPortablePath(S))}`)}let p=!1,h=!1,E=UL(f);if(E.size>0){p||c.reportSeparator();for(let S of E)c.reportError(0,`${q.prettyLocator(r,S.anchoredLocator)} has been modified but doesn't have a release strategy attached`);p=!0}let C=gP(f);for(let[S,P]of C)h||c.reportSeparator(),c.reportError(0,`${q.prettyLocator(r,S.anchoredLocator)} doesn't have a release strategy attached, but depends on ${q.prettyWorkspace(r,P)} which is planned for release.`),h=!0;(p||h)&&(c.reportSeparator(),c.reportInfo(0,"This command detected that at least some workspaces have received modifications without explicit instructions as to how they had to be released (if needed)."),c.reportInfo(0,"To correct these errors, run `yarn version check --interactive` then follow the instructions."))})).exitCode()}};Ve();Wt();var qL=et(Ai());var K1=class extends ut{constructor(){super(...arguments);this.deferred=ge.Boolean("-d,--deferred",{description:"Prepare the version to be bumped during the next release cycle"});this.immediate=ge.Boolean("-i,--immediate",{description:"Bump the version immediately"});this.strategy=ge.String()}static{this.paths=[["version"]]}static{this.usage=ot.Usage({category:"Release-related commands",description:"apply a new version to the current package",details:"\n This command will bump the version number for the given package, following the specified strategy:\n\n - If `major`, the first number from the semver range will be increased (`X.0.0`).\n - If `minor`, the second number from the semver range will be increased (`0.X.0`).\n - If `patch`, the third number from the semver range will be increased (`0.0.X`).\n - If prefixed by `pre` (`premajor`, ...), a `-0` suffix will be set (`0.0.0-0`).\n - If `prerelease`, the suffix will be increased (`0.0.0-X`); the third number from the semver range will also be increased if there was no suffix in the previous version.\n - If `decline`, the nonce will be increased for `yarn version check` to pass without version bump.\n - If a valid semver range, it will be used as new version.\n - If unspecified, Yarn will ask you for guidance.\n\n For more information about the `--deferred` flag, consult our documentation (https://yarnpkg.com/features/release-workflow#deferred-versioning).\n ",examples:[["Immediately bump the version to the next major","yarn version major"],["Prepare the version to be bumped to the next major","yarn version major --deferred"]]})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd);if(!a)throw new ar(s.cwd,this.context.cwd);let n=r.get("preferDeferredVersions");this.deferred&&(n=!0),this.immediate&&(n=!1);let c=qL.default.valid(this.strategy),f=this.strategy==="decline",p;if(c)if(a.manifest.version!==null){let E=Oz(a.manifest.version,this.strategy);E!==null?p=E:p=this.strategy}else p=this.strategy;else{let E=a.manifest.version;if(!f){if(E===null)throw new nt("Can't bump the version if there wasn't a version to begin with - use 0.0.0 as initial version then run the command again.");if(typeof E!="string"||!qL.default.valid(E))throw new nt(`Can't bump the version (${E}) if it's not valid semver`)}p=j1(this.strategy)}if(!n){let C=(await hP(s)).get(a);if(typeof C<"u"&&p!=="decline"){let S=HL(a.manifest.version,p);if(qL.default.lt(S,C))throw new nt(`Can't bump the version to one that would be lower than the current deferred one (${C})`)}}let h=await G1(s,{allowEmpty:!0});return h.releases.set(a,p),await h.saveAll(),n?0:await this.cli.run(["version","apply"])}};var f6t={configuration:{deferredVersionFolder:{description:"Folder where are stored the versioning files",type:"ABSOLUTE_PATH",default:"./.yarn/versions"},preferDeferredVersions:{description:"If true, running `yarn version` will assume the `--deferred` flag unless `--immediate` is set",type:"BOOLEAN",default:!1}},commands:[Y1,V1,K1]},A6t=f6t;var _z={};Vt(_z,{WorkspacesFocusCommand:()=>J1,WorkspacesForeachCommand:()=>Z1,default:()=>g6t});Ve();Ve();Wt();var J1=class extends ut{constructor(){super(...arguments);this.json=ge.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=ge.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=ge.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=ge.Rest()}static{this.paths=[["workspaces","focus"]]}static{this.usage=ot.Usage({category:"Workspace-related commands",description:"install a single workspace and its dependencies",details:"\n This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\n\n Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\n\n If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\n "})}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd),n=await Jr.find(r);await s.restoreInstallState({restoreResolutions:!1});let c;if(this.all)c=new Set(s.workspaces);else if(this.workspaces.length===0){if(!a)throw new ar(s.cwd,this.context.cwd);c=new Set([a])}else c=new Set(this.workspaces.map(f=>s.getWorkspaceByIdent(q.parseIdent(f))));for(let f of c)for(let p of this.production?["dependencies"]:Ht.hardDependencies)for(let h of f.manifest.getForScope(p).values()){let E=s.tryWorkspaceByDescriptor(h);E!==null&&c.add(E)}for(let f of s.workspaces)c.has(f)?this.production&&f.manifest.devDependencies.clear():(f.manifest.installConfig=f.manifest.installConfig||{},f.manifest.installConfig.selfReferences=!1,f.manifest.dependencies.clear(),f.manifest.devDependencies.clear(),f.manifest.peerDependencies.clear(),f.manifest.scripts.clear());return await s.installWithNewReport({json:this.json,stdout:this.context.stdout},{cache:n,persistProject:!1})}};Ve();Ve();Ve();Wt();var z1=et(Sa()),DOe=et(Od());Ul();var Z1=class extends ut{constructor(){super(...arguments);this.from=ge.Array("--from",{description:"An array of glob pattern idents or paths from which to base any recursion"});this.all=ge.Boolean("-A,--all",{description:"Run the command on all workspaces of a project"});this.recursive=ge.Boolean("-R,--recursive",{description:"Run the command on the current workspace and all of its recursive dependencies"});this.worktree=ge.Boolean("-W,--worktree",{description:"Run the command on all workspaces of the current worktree"});this.verbose=ge.Counter("-v,--verbose",{description:"Increase level of logging verbosity up to 2 times"});this.parallel=ge.Boolean("-p,--parallel",!1,{description:"Run the commands in parallel"});this.interlaced=ge.Boolean("-i,--interlaced",!1,{description:"Print the output of commands in real-time instead of buffering it"});this.jobs=ge.String("-j,--jobs",{description:"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`",validator:mU([po(["unlimited"]),$2(dU(),[EU(),yU(1)])])});this.topological=ge.Boolean("-t,--topological",!1,{description:"Run the command after all workspaces it depends on (regular) have finished"});this.topologicalDev=ge.Boolean("--topological-dev",!1,{description:"Run the command after all workspaces it depends on (regular + dev) have finished"});this.include=ge.Array("--include",[],{description:"An array of glob pattern idents or paths; only matching workspaces will be traversed"});this.exclude=ge.Array("--exclude",[],{description:"An array of glob pattern idents or paths; matching workspaces won't be traversed"});this.publicOnly=ge.Boolean("--no-private",{description:"Avoid running the command on private workspaces"});this.since=ge.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.dryRun=ge.Boolean("-n,--dry-run",{description:"Print the commands that would be run, without actually running them"});this.commandName=ge.String();this.args=ge.Proxy()}static{this.paths=[["workspaces","foreach"]]}static{this.usage=ot.Usage({category:"Workspace-related commands",description:"run a command on all workspaces",details:"\n This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\n\n - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\n\n - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\n\n - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\n\n - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `-W,--worktree` is set, Yarn will find workspaces to run the command on by looking at the current worktree.\n\n - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\n\n - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - If `--dry-run` is set, Yarn will explain what it would do without actually doing anything.\n\n - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them. You can also use the `--no-private` flag to avoid running the command in private workspaces.\n\n The `-v,--verbose` flag can be passed up to twice: once to prefix output lines with the originating workspace's name, and again to include start/finish/timing log lines. Maximum verbosity is enabled by default in terminal environments.\n\n If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\n ",examples:[["Publish all packages","yarn workspaces foreach -A --no-private npm publish --tolerate-republish"],["Run the build script on all descendant packages","yarn workspaces foreach -A run build"],["Run the build script on current and all descendant packages in parallel, building package dependencies first","yarn workspaces foreach -Apt run build"],["Run the build script on several packages and all their dependencies, building dependencies first","yarn workspaces foreach -Rpt --from '{workspace-a,workspace-b}' run build"]]})}static{this.schema=[tB("all",Wf.Forbids,["from","recursive","since","worktree"],{missingIf:"undefined"}),IU(["all","recursive","since","worktree"],{missingIf:"undefined"})]}async execute(){let r=await ze.find(this.context.cwd,this.context.plugins),{project:s,workspace:a}=await Tt.find(r,this.context.cwd);if(!this.all&&!a)throw new ar(s.cwd,this.context.cwd);await s.restoreInstallState();let n=this.cli.process([this.commandName,...this.args]),c=n.path.length===1&&n.path[0]==="run"&&typeof n.scriptName<"u"?n.scriptName:null;if(n.path.length===0)throw new nt("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let f=Ce=>{this.dryRun&&this.context.stdout.write(`${Ce} `)},p=()=>{let Ce=this.from.map(g=>z1.default.matcher(g));return s.workspaces.filter(g=>{let we=q.stringifyIdent(g.anchoredLocator),ye=g.relativeCwd;return Ce.some(fe=>fe(we)||fe(ye))})},h=[];if(this.since?(f("Option --since is set; selecting the changed workspaces as root for workspace selection"),h=Array.from(await Qa.fetchChangedWorkspaces({ref:this.since,project:s}))):this.from?(f("Option --from is set; selecting the specified workspaces"),h=[...p()]):this.worktree?(f("Option --worktree is set; selecting the current workspace"),h=[a]):this.recursive?(f("Option --recursive is set; selecting the current workspace"),h=[a]):this.all&&(f("Option --all is set; selecting all workspaces"),h=[...s.workspaces]),this.dryRun&&!this.all){for(let Ce of h)f(` - ${Ce.relativeCwd} ${q.prettyLocator(r,Ce.anchoredLocator)}`);h.length>0&&f("")}let E;if(this.recursive?this.since?(f("Option --recursive --since is set; recursively selecting all dependent workspaces"),E=new Set(h.map(Ce=>[...Ce.getRecursiveWorkspaceDependents()]).flat())):(f("Option --recursive is set; recursively selecting all transitive dependencies"),E=new Set(h.map(Ce=>[...Ce.getRecursiveWorkspaceDependencies()]).flat())):this.worktree?(f("Option --worktree is set; recursively selecting all nested workspaces"),E=new Set(h.map(Ce=>[...Ce.getRecursiveWorkspaceChildren()]).flat())):E=null,E!==null&&(h=[...new Set([...h,...E])],this.dryRun))for(let Ce of E)f(` - ${Ce.relativeCwd} ${q.prettyLocator(r,Ce.anchoredLocator)}`);let C=[],S=!1;if(c?.includes(":")){for(let Ce of s.workspaces)if(Ce.manifest.scripts.has(c)&&(S=!S,S===!1))break}for(let Ce of h){if(c&&!Ce.manifest.scripts.has(c)&&!S&&!(await In.getWorkspaceAccessibleBinaries(Ce)).has(c)){f(`Excluding ${Ce.relativeCwd} because it doesn't have a "${c}" script`);continue}if(!(c===r.env.npm_lifecycle_event&&Ce.cwd===a.cwd)){if(this.include.length>0&&!z1.default.isMatch(q.stringifyIdent(Ce.anchoredLocator),this.include)&&!z1.default.isMatch(Ce.relativeCwd,this.include)){f(`Excluding ${Ce.relativeCwd} because it doesn't match the --include filter`);continue}if(this.exclude.length>0&&(z1.default.isMatch(q.stringifyIdent(Ce.anchoredLocator),this.exclude)||z1.default.isMatch(Ce.relativeCwd,this.exclude))){f(`Excluding ${Ce.relativeCwd} because it matches the --exclude filter`);continue}if(this.publicOnly&&Ce.manifest.private===!0){f(`Excluding ${Ce.relativeCwd} because it's a private workspace and --no-private was set`);continue}C.push(Ce)}}if(this.dryRun)return 0;let P=this.verbose??(this.context.stdout.isTTY?1/0:0),I=P>0,R=P>1,N=this.parallel?this.jobs==="unlimited"?1/0:Number(this.jobs)||Math.ceil(ps.availableParallelism()/2):1,U=N===1?!1:this.parallel,W=U?this.interlaced:!0,te=(0,DOe.default)(N),ie=new Map,Ae=new Set,ce=0,me=null,pe=!1,Be=await Ot.start({configuration:r,stdout:this.context.stdout,includePrefix:!1},async Ce=>{let g=async(we,{commandIndex:ye})=>{if(pe)return-1;!U&&R&&ye>1&&Ce.reportSeparator();let fe=p6t(we,{configuration:r,label:I,commandIndex:ye}),[se,X]=SOe(Ce,{prefix:fe,interlaced:W}),[De,Re]=SOe(Ce,{prefix:fe,interlaced:W});try{R&&Ce.reportInfo(null,`${fe?`${fe} `:""}Process started`);let dt=Date.now(),j=await this.cli.run([this.commandName,...this.args],{cwd:we.cwd,stdout:se,stderr:De})||0;se.end(),De.end(),await X,await Re;let rt=Date.now();if(R){let Fe=r.get("enableTimers")?`, completed in ${he.pretty(r,rt-dt,he.Type.DURATION)}`:"";Ce.reportInfo(null,`${fe?`${fe} `:""}Process exited (exit code ${j})${Fe}`)}return j===130&&(pe=!0,me=j),j}catch(dt){throw se.end(),De.end(),await X,await Re,dt}};for(let we of C)ie.set(we.anchoredLocator.locatorHash,we);for(;ie.size>0&&!Ce.hasErrors();){let we=[];for(let[X,De]of ie){if(Ae.has(De.anchoredDescriptor.descriptorHash))continue;let Re=!0;if(this.topological||this.topologicalDev){let dt=this.topologicalDev?new Map([...De.manifest.dependencies,...De.manifest.devDependencies]):De.manifest.dependencies;for(let j of dt.values()){let rt=s.tryWorkspaceByDescriptor(j);if(Re=rt===null||!ie.has(rt.anchoredLocator.locatorHash),!Re)break}}if(Re&&(Ae.add(De.anchoredDescriptor.descriptorHash),we.push(te(async()=>{let dt=await g(De,{commandIndex:++ce});return ie.delete(X),Ae.delete(De.anchoredDescriptor.descriptorHash),{workspace:De,exitCode:dt}})),!U))break}if(we.length===0){let X=Array.from(ie.values()).map(De=>q.prettyLocator(r,De.anchoredLocator)).join(", ");Ce.reportError(3,`Dependency cycle detected (${X})`);return}let ye=await Promise.all(we);ye.forEach(({workspace:X,exitCode:De})=>{De!==0&&Ce.reportError(0,`The command failed in workspace ${q.prettyLocator(r,X.anchoredLocator)} with exit code ${De}`)});let se=ye.map(X=>X.exitCode).find(X=>X!==0);(this.topological||this.topologicalDev)&&typeof se<"u"&&Ce.reportError(0,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return me!==null?me:Be.exitCode()}};function SOe(t,{prefix:e,interlaced:r}){let s=t.createStreamReporter(e),a=new je.DefaultStream;a.pipe(s,{end:!1}),a.on("finish",()=>{s.end()});let n=new Promise(f=>{s.on("finish",()=>{f(a.active)})});if(r)return[a,n];let c=new je.BufferStream;return c.pipe(a,{end:!1}),c.on("finish",()=>{a.end()}),[c,n]}function p6t(t,{configuration:e,commandIndex:r,label:s}){if(!s)return null;let n=`[${q.stringifyIdent(t.anchoredLocator)}]:`,c=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],f=c[r%c.length];return he.pretty(e,n,f)}var h6t={commands:[J1,Z1]},g6t=h6t;var tC=()=>({modules:new Map([["@yarnpkg/cli",$v],["@yarnpkg/core",Xv],["@yarnpkg/fslib",U2],["@yarnpkg/libzip",Iv],["@yarnpkg/parsers",K2],["@yarnpkg/shell",Dv],["clipanion",oB],["semver",d6t],["typanion",Ia],["@yarnpkg/plugin-essentials",Y5],["@yarnpkg/plugin-compat",Z5],["@yarnpkg/plugin-constraints",g9],["@yarnpkg/plugin-dlx",d9],["@yarnpkg/plugin-exec",E9],["@yarnpkg/plugin-file",C9],["@yarnpkg/plugin-git",W5],["@yarnpkg/plugin-github",v9],["@yarnpkg/plugin-http",S9],["@yarnpkg/plugin-init",D9],["@yarnpkg/plugin-interactive-tools",IY],["@yarnpkg/plugin-jsr",wY],["@yarnpkg/plugin-link",BY],["@yarnpkg/plugin-nm",oV],["@yarnpkg/plugin-npm",oz],["@yarnpkg/plugin-npm-cli",gz],["@yarnpkg/plugin-pack",$V],["@yarnpkg/plugin-patch",wz],["@yarnpkg/plugin-pnp",KY],["@yarnpkg/plugin-pnpm",Sz],["@yarnpkg/plugin-stage",Tz],["@yarnpkg/plugin-typescript",Rz],["@yarnpkg/plugin-version",Mz],["@yarnpkg/plugin-workspace-tools",_z]]),plugins:new Set(["@yarnpkg/plugin-essentials","@yarnpkg/plugin-compat","@yarnpkg/plugin-constraints","@yarnpkg/plugin-dlx","@yarnpkg/plugin-exec","@yarnpkg/plugin-file","@yarnpkg/plugin-git","@yarnpkg/plugin-github","@yarnpkg/plugin-http","@yarnpkg/plugin-init","@yarnpkg/plugin-interactive-tools","@yarnpkg/plugin-jsr","@yarnpkg/plugin-link","@yarnpkg/plugin-nm","@yarnpkg/plugin-npm","@yarnpkg/plugin-npm-cli","@yarnpkg/plugin-pack","@yarnpkg/plugin-patch","@yarnpkg/plugin-pnp","@yarnpkg/plugin-pnpm","@yarnpkg/plugin-stage","@yarnpkg/plugin-typescript","@yarnpkg/plugin-version","@yarnpkg/plugin-workspace-tools"])});function xOe({cwd:t,pluginConfiguration:e}){let r=new wa({binaryLabel:"Yarn Package Manager",binaryName:"yarn",binaryVersion:un??""});return Object.assign(r,{defaultContext:{...wa.defaultContext,cwd:t,plugins:e,quiet:!1,stdin:process.stdin,stdout:process.stdout,stderr:process.stderr}})}function m6t(t){if(je.parseOptionalBoolean(process.env.YARN_IGNORE_NODE))return!0;let r=process.versions.node,s=">=18.12.0";if(Or.satisfiesWithPrereleases(r,s))return!0;let a=new nt(`This tool requires a Node version compatible with ${s} (got ${r}). Upgrade Node, or set \`YARN_IGNORE_NODE=1\` in your environment.`);return wa.defaultContext.stdout.write(t.error(a)),!1}async function kOe({selfPath:t,pluginConfiguration:e}){return await ze.find(ue.toPortablePath(process.cwd()),e,{strict:!1,usePathCheck:t})}function y6t(t,e,{yarnPath:r}){if(!le.existsSync(r))return t.error(new Error(`The "yarn-path" option has been set, but the specified location doesn't exist (${r}).`)),1;process.on("SIGINT",()=>{});let s={stdio:"inherit",env:{...process.env,YARN_IGNORE_PATH:"1"}};try{(0,bOe.execFileSync)(process.execPath,[ue.fromPortablePath(r),...e],s)}catch(a){return a.status??1}return 0}function E6t(t,e){let r=null,s=e;return e.length>=2&&e[0]==="--cwd"?(r=ue.toPortablePath(e[1]),s=e.slice(2)):e.length>=1&&e[0].startsWith("--cwd=")?(r=ue.toPortablePath(e[0].slice(6)),s=e.slice(1)):e[0]==="add"&&e[e.length-2]==="--cwd"&&(r=ue.toPortablePath(e[e.length-1]),s=e.slice(0,e.length-2)),t.defaultContext.cwd=r!==null?K.resolve(r):K.cwd(),s}function I6t(t,{configuration:e}){if(!e.get("enableTelemetry")||POe.isCI||!process.stdout.isTTY)return;ze.telemetry=new XI(e,"puba9cdc10ec5790a2cf4969dd413a47270");let s=/^@yarnpkg\/plugin-(.*)$/;for(let a of e.plugins.keys())$I.has(a.match(s)?.[1]??"")&&ze.telemetry?.reportPluginName(a);t.binaryVersion&&ze.telemetry.reportVersion(t.binaryVersion)}function QOe(t,{configuration:e}){for(let r of e.plugins.values())for(let s of r.commands||[])t.register(s)}async function C6t(t,e,{selfPath:r,pluginConfiguration:s}){if(!m6t(t))return 1;let a=await kOe({selfPath:r,pluginConfiguration:s}),n=a.get("yarnPath"),c=a.get("ignorePath");if(n&&!c)return y6t(t,e,{yarnPath:n});delete process.env.YARN_IGNORE_PATH;let f=E6t(t,e);I6t(t,{configuration:a}),QOe(t,{configuration:a});let p=t.process(f,t.defaultContext);return p.help||ze.telemetry?.reportCommandName(p.path.join(" ")),await t.run(p,t.defaultContext)}async function XCe({cwd:t=K.cwd(),pluginConfiguration:e=tC()}={}){let r=xOe({cwd:t,pluginConfiguration:e}),s=await kOe({pluginConfiguration:e,selfPath:null});return QOe(r,{configuration:s}),r}async function KR(t,{cwd:e=K.cwd(),selfPath:r,pluginConfiguration:s}){let a=xOe({cwd:e,pluginConfiguration:s});function n(){wa.defaultContext.stdout.write(`ERROR: Yarn is terminating due to an unexpected empty event loop. Please report this issue at https://github.com/yarnpkg/berry/issues.`)}process.once("beforeExit",n);try{process.exitCode=42,process.exitCode=await C6t(a,t,{selfPath:r,pluginConfiguration:s})}catch(c){wa.defaultContext.stdout.write(a.error(c)),process.exitCode=1}finally{process.off("beforeExit",n),await le.rmtempPromise()}}KR(process.argv.slice(2),{cwd:K.cwd(),selfPath:ue.toPortablePath(ue.resolve(process.argv[1])),pluginConfiguration:tC()});})(); /** @license Copyright (c) 2015, Rebecca Turner Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /** @license Copyright Node.js contributors. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** @license The MIT License (MIT) Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** @license Copyright Joyent, Inc. and other Node contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*! Bundled license information: is-number/index.js: (*! * is-number * * Copyright (c) 2014-present, Jon Schlinkert. * Released under the MIT License. *) to-regex-range/index.js: (*! * to-regex-range * * Copyright (c) 2015-present, Jon Schlinkert. * Released under the MIT License. *) fill-range/index.js: (*! * fill-range * * Copyright (c) 2014-present, Jon Schlinkert. * Licensed under the MIT License. *) is-extglob/index.js: (*! * is-extglob * * Copyright (c) 2014-2016, Jon Schlinkert. * Licensed under the MIT License. *) is-glob/index.js: (*! * is-glob * * Copyright (c) 2014-2017, Jon Schlinkert. * Released under the MIT License. *) queue-microtask/index.js: (*! queue-microtask. MIT License. Feross Aboukhadijeh *) run-parallel/index.js: (*! run-parallel. MIT License. Feross Aboukhadijeh *) git-url-parse/lib/index.js: (*! * buildToken * Builds OAuth token prefix (helper function) * * @name buildToken * @function * @param {GitUrl} obj The parsed Git url object. * @return {String} token prefix *) object-assign/index.js: (* object-assign (c) Sindre Sorhus @license MIT *) react/cjs/react.production.min.js: (** @license React v17.0.2 * react.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. *) scheduler/cjs/scheduler.production.min.js: (** @license React v0.20.2 * scheduler.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. *) react-reconciler/cjs/react-reconciler.production.min.js: (** @license React v0.26.2 * react-reconciler.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. *) is-windows/index.js: (*! * is-windows * * Copyright © 2015-2018, Jon Schlinkert. * Released under the MIT License. *) */ ================================================ FILE: bindings/javascript/.yarnrc.yml ================================================ nodeLinker: node-modules yarnPath: .yarn/releases/yarn-4.9.2.cjs enableHardenedMode: false supportedArchitectures: cpu: - current - wasm32 ================================================ FILE: bindings/javascript/Cargo.toml ================================================ [package] name = "turso_node" version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true description = "The Turso database library Node bindings" [lints] workspace = true [lib] crate-type = ["cdylib", "lib"] [dependencies] turso_core = { workspace = true, features = ["fts"] } turso_parser = { workspace = true } napi = { version = "3.1.3", default-features = false, features = ["napi6"] } napi-derive = { version = "3.1.1", default-features = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing.workspace = true chrono = { workspace = true, default-features = false, features = ["clock"] } [features] browser = [] tracing_release = ["turso_core/tracing_release"] [build-dependencies] napi-build = "2.2.3" ================================================ FILE: bindings/javascript/Makefile ================================================ pack-native: npm publish --dry-run && npm pack pack-wasm: cp package.json package.native.json cp package.browser.json package.json npm publish --dry-run && npm pack; cp package.native.json package.json publish-native: npm publish --access public publish-wasm: cp package.json package.native.json cp package.browser.json package.json npm publish --access public; cp package.native.json package.json publish-native-next: npm publish --tag next --access public publish-wasm-next: cp package.json package.native.json cp package.browser.json package.json npm publish --tag next --access public; cp package.native.json package.json ================================================ FILE: bindings/javascript/README.md ================================================

Turso Database for JavaScript

npm

Chat with other users of Turso on Discord

--- ## About This package is the Turso in-memory database library for JavaScript. > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ## Features - **SQLite compatible:** SQLite query language and file format support ([status](https://github.com/tursodatabase/turso/blob/main/COMPAT.md)). - **In-process**: No network overhead, runs directly in your Node.js process - **TypeScript support**: Full TypeScript definitions included - **Cross-platform**: Supports Linux (x86 and arm64), macOS, Windows and browsers (through WebAssembly) ## Installation ```bash npm install @tursodatabase/database ``` ## Getting Started ### In-Memory Database ```javascript import { connect } from '@tursodatabase/database'; // Create an in-memory database const db = await connect(':memory:'); // Create a table db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)'); // Insert data const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); insert.run('Alice', 'alice@example.com'); insert.run('Bob', 'bob@example.com'); // Query data const users = db.prepare('SELECT * FROM users').all(); console.log(users); // Output: [ // { id: 1, name: 'Alice', email: 'alice@example.com' }, // { id: 2, name: 'Bob', email: 'bob@example.com' } // ] ``` ### File-Based Database ```javascript import { connect } from '@tursodatabase/database'; // Create or open a database file const db = await connect('my-database.db'); // Create a table db.exec(` CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Insert a post const insertPost = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)'); const result = insertPost.run('Hello World', 'This is my first blog post!'); console.log(`Inserted post with ID: ${result.lastInsertRowid}`); ``` ### Transactions ```javascript import { connect } from '@tursodatabase/database'; const db = await connect('transactions.db'); // Using transactions for atomic operations const transaction = db.transaction((users) => { const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); for (const user of users) { insert.run(user.name, user.email); } }); // Execute transaction transaction([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' } ]); ``` ### WebAssembly Support Turso Database can run in browsers using WebAssembly. Check the `browser.js` and WASM artifacts for browser usage. ## API Reference For complete API documentation, see [JavaScript API Reference](https://github.com/tursodatabase/turso/blob/main/docs/javascript-api-reference.md). ## Related Packages * The [@tursodatabase/serverless](https://www.npmjs.com/package/@tursodatabase/serverless) package provides a serverless driver with the same API. * The [@tursodatabase/sync](https://www.npmjs.com/package/@tursodatabase/sync) package provides bidirectional sync between a local Turso database and Turso Cloud. ## License This project is licensed under the [MIT license](https://github.com/tursodatabase/turso/blob/main/LICENSE.md). ## Support - [GitHub Issues](https://github.com/tursodatabase/turso/issues) - [Documentation](https://docs.turso.tech) - [Discord Community](https://tur.so/discord) ================================================ FILE: bindings/javascript/build.rs ================================================ extern crate napi_build; fn main() { napi_build::setup(); } ================================================ FILE: bindings/javascript/docs/API.md ================================================ # class Database The `Database` class represents a connection that can prepare and execute SQL statements. ## Methods ### new Database(path, [options]) ⇒ Database Creates a new database connection. | Param | Type | Description | | ------- | ------------------- | ------------------------- | | path | string | Path to the database file | | options | object | Options. | The `path` parameter points to the SQLite database file to open. If the file pointed to by `path` does not exists, it will be created. To open an in-memory database, please pass `:memory:` as the `path` parameter. The function returns a `Database` object. ### prepare(sql) ⇒ Statement Prepares a SQL statement for execution. | Param | Type | Description | | ----- | ------------------- | ------------------------------------ | | sql | string | The SQL statement string to prepare. | The function returns a `Statement` object. ### transaction(function) ⇒ function Returns a function that runs the given function in a transaction. | Param | Type | Description | | -------- | --------------------- | ------------------------------------- | | function | function | The function to run in a transaction. | ### pragma(string, [options]) ⇒ results Executes the given PRAGMA and returns its results. | Param | Type | Description | | -------- | --------------------- | ----------------------| | source | string | Pragma to be executed | | options | object | Options. | Most PRAGMA return a single value, the `simple: boolean` option is provided to return the first column of the first row. ```js db.pragma('cache_size = 32000'); console.log(db.pragma('cache_size', { simple: true })); // => 32000 ``` ### backup(destination, [options]) ⇒ promise This function is currently not supported. ### serialize([options]) ⇒ Buffer This function is currently not supported. ### function(name, [options], function) ⇒ this This function is currently not supported. ### aggregate(name, options) ⇒ this This function is currently not supported. ### table(name, definition) ⇒ this This function is currently not supported. ### loadExtension(path, [entryPoint]) ⇒ this Loads a SQLite3 extension. | Param | Type | Description | | ----- | ------------------- | --------------------------------------- | | path | string | The path to the extension to be loaded. | ### exec(sql) ⇒ this Executes a SQL statement. | Param | Type | Description | | ----- | ------------------- | ------------------------------------ | | sql | string | The SQL statement string to execute. | This can execute strings that contain multiple SQL statements. ### interrupt() ⇒ this Cancel ongoing operations and make them return at earliest opportunity. **Note:** This is an extension in libSQL and not available in `better-sqlite3`. This function is currently not supported. ### close() ⇒ this Closes the database connection. # class Statement ## Methods ### run([...bindParameters]) ⇒ object Executes the SQL statement and (currently) returns an array with results. **Note:** It should return an info object. | Param | Type | Description | | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | The returned info object contains two properties: `changes` that describes the number of modified rows and `info.lastInsertRowid` that represents the `rowid` of the last inserted row. This function is currently not supported. ### get([...bindParameters]) ⇒ row Executes the SQL statement and returns the first row. | Param | Type | Description | | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | ### all([...bindParameters]) ⇒ array of rows Executes the SQL statement and returns an array of the resulting rows. | Param | Type | Description | | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | ### iterate([...bindParameters]) ⇒ iterator Executes the SQL statement and returns an iterator to the resulting rows. | Param | Type | Description | | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | ### pluck([toggleState]) ⇒ this Makes the prepared statement only return the value of the first column of any rows that it retrieves. | Param | Type | Description | | --------- | -------------------- | -------------------------------------------------------------------------------------- | | pluckMode | boolean | Enable of disable pluck mode. If you don't pass the paramenter, pluck mode is enabled. | ```js stmt.pluck(); // plucking ON stmt.pluck(true); // plucking ON stmt.pluck(false); // plucking OFF ``` > NOTE: When plucking is turned on, raw mode is turned off (they are mutually exclusive options). ### expand([toggleState]) ⇒ this This function is currently not supported. ### raw([rawMode]) ⇒ this Makes the prepared statement return rows as arrays instead of objects. | Param | Type | Description | | ------- | -------------------- | --------------------------------------------------------------------------------- | | rawMode | boolean | Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled. | This function enables or disables raw mode. Prepared statements return objects by default, but if raw mode is enabled, the functions return arrays instead. ```js stmt.raw(); // raw mode ON stmt.raw(true); // raw mode ON stmt.raw(false); // raw mode OFF ``` > NOTE: When raw mode is turned on, plucking is turned off (they are mutually exclusive options). ### columns() ⇒ array of objects Returns the columns in the result set returned by this prepared statement. This function is currently not supported. ### bind([...bindParameters]) ⇒ this | Param | Type | Description | | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | Binds **permanently** the given parameters to the statement. After a statement's parameters are bound this way, you may no longer provide it with execution-specific (temporary) bound parameters. ================================================ FILE: bindings/javascript/docs/CONTRIBUTING.md ================================================ # Contributing So you want to contribute to Limbo's binding for the ~second~ best language in the world? Awesome. First things first you'll need to install [napi-rs](https://napi.rs/), follow the instructions [here](https://napi.rs/docs/introduction/getting-started) although is highly recommended to use `yarn` with: ```sh yarn global add @napi-rs/cli ``` Run `yarn build` to build our napi project and run `yarn test` to run our test suite, if nothing breaks you're ready to start! ## API You can check the API docs [here](./API.md), it aims to be fully compatible with [better-sqlite](https://github.com/WiseLibs/better-sqlite3/) and borrows some things from [libsql](https://github.com/tursodatabase/libsql-js). So if you find some incompability in behaviour and/or lack of functions/attributes, that's an issue and you should work on it for a great good :) ## Code Structure The Rust code for the bind is on [lib.rs](../src/lib.rs). It's exposed to JS users through [wrapper](../wrapper.js), where you can use some JS' ~weirdness~ facilities, for instance, since Rust doesn't have variadic functions the wrapper enables us to "normalize" `bindParameters` into an array. All tests should be within the [__test__](../__test__/) folder. # Before open a PR Please be assured that: - Your fix/feature has a test checking the new behaviour; - Your Rust code is formatted with `cargo fmt`; - Your JavaScript code is formatted with `tsserver` (VSCode's default); - If applicable, update the [API docs](./API.md) to match the current implementation; ================================================ FILE: bindings/javascript/package.json ================================================ { "type": "module", "scripts": { "build": "npm run build --workspaces", "build:native": "npm run build --workspace=packages/common --workspace=packages/native", "tsc-build": "npm run tsc-build --workspaces", "test": "npm run test --workspaces" }, "workspaces": [ "packages/common", "packages/wasm-common", "packages/native", "packages/wasm", "sync/packages/common", "sync/packages/native", "sync/packages/wasm" ], "version": "0.6.0-pre.4" } ================================================ FILE: bindings/javascript/packages/common/README.md ================================================ ## About This package is the Turso embedded database common JS library which is shared between final builds for Node and Browser. Do not use this package directly - instead you must use `@tursodatabase/database` or `@tursodatabase/database-wasm`. > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ================================================ FILE: bindings/javascript/packages/common/async-lock.ts ================================================ export class AsyncLock { locked: boolean; queue: any[]; constructor() { this.locked = false; this.queue = [] } async acquire() { if (!this.locked) { this.locked = true; return Promise.resolve(); } else { const block = new Promise(resolve => { this.queue.push(resolve) }); return block; } } release() { if (this.locked == false) { throw new Error("invalid state: lock was already unlocked"); } const item = this.queue.shift(); if (item != null) { this.locked = true; item(); } else { this.locked = false; } } } ================================================ FILE: bindings/javascript/packages/common/bind.ts ================================================ // Bind parameters to a statement. // // This function is used to bind parameters to a statement. It supports both // named and positional parameters, and nested arrays. // // The `stmt` parameter is a statement object. // The `params` parameter is an array of parameters. // // The function returns void. export function bindParams(stmt, params) { const len = params?.length; if (len === 0) { return; } if (len === 1) { const param = params[0]; if (isPlainObject(param)) { bindNamedParams(stmt, param); return; } if (Array.isArray(param)) { bindPositionalParams(stmt, [param]); return; } bindValue(stmt, 1, param); return; } bindPositionalParams(stmt, params); } // Check if object is plain (no prototype chain) function isPlainObject(obj) { if (!obj || typeof obj !== 'object') return false; const proto = Object.getPrototypeOf(obj); return proto === Object.prototype || proto === null; } // Handle named parameters function bindNamedParams(stmt, paramObj) { const paramCount = stmt.parameterCount(); for (let i = 1; i <= paramCount; i++) { const paramName = stmt.parameterName(i); if (paramName) { const key = paramName.substring(1); // Remove ':' or '$' prefix const value = paramObj[key]; if (value !== undefined) { bindValue(stmt, i, value); } } } } // Handle positional parameters (including nested arrays) function bindPositionalParams(stmt, params) { let bindIndex = 1; for (let i = 0; i < params.length; i++) { const param = params[i]; if (Array.isArray(param)) { for (let j = 0; j < param.length; j++) { bindValue(stmt, bindIndex++, param[j]); } } else { bindValue(stmt, bindIndex++, param); } } } function bindValue(stmt, index, value) { stmt.bindAt(index, value); } ================================================ FILE: bindings/javascript/packages/common/compat.ts ================================================ import { bindParams } from "./bind.js"; import { SqliteError } from "./sqlite-error.js"; import { NativeDatabase, NativeStatement, STEP_IO, STEP_ROW, STEP_DONE } from "./types.js"; const convertibleErrorTypes = { TypeError }; const CONVERTIBLE_ERROR_PREFIX = "[TURSO_CONVERT_TYPE]"; function convertError(err) { if ((err.code ?? "").startsWith(CONVERTIBLE_ERROR_PREFIX)) { return createErrorByName( err.code.substring(CONVERTIBLE_ERROR_PREFIX.length), err.message, ); } return new SqliteError(err.message, err.code, err.rawCode); } function createErrorByName(name, message) { const ErrorConstructor = convertibleErrorTypes[name]; if (!ErrorConstructor) { throw new Error(`unknown error type ${name} from Turso`); } return new ErrorConstructor(message); } /** * Database represents a connection that can prepare and execute SQL statements. */ class Database { name: string; readonly: boolean; open: boolean; memory: boolean; inTransaction: boolean; private db: NativeDatabase; private _inTransaction: boolean = false; /** * Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created. * * @constructor * @param {string} path - Path to the database file. * @param {Object} opts - Options for database behavior. * @param {boolean} [opts.readonly=false] - Open the database in read-only mode. * @param {boolean} [opts.fileMustExist=false] - If true, throws if database file does not exist. * @param {number} [opts.timeout=0] - Timeout duration in milliseconds for database operations. Defaults to 0 (no timeout). */ constructor(db: NativeDatabase) { this.db = db; this.db.connectSync(); Object.defineProperties(this, { name: { get: () => this.db.path }, readonly: { get: () => this.db.readonly }, open: { get: () => this.db.open }, memory: { get: () => this.db.memory }, inTransaction: { get: () => this._inTransaction }, }); } /** * Prepares a SQL statement for execution. * * @param {string} sql - The SQL statement string to prepare. */ prepare(sql) { if (!this.open) { throw new TypeError("The database connection is not open"); } if (!sql) { throw new RangeError("The supplied SQL string contains no statements"); } try { return new Statement(this.db.prepare(sql), this.db); } catch (err) { throw convertError(err); } } /** * Returns a function that executes the given function in a transaction. * * @param {function} fn - The function to wrap in a transaction. */ transaction(fn) { if (typeof fn !== "function") throw new TypeError("Expected first argument to be a function"); const db = this; const wrapTxn = (mode) => { return (...bindParameters) => { db.exec("BEGIN " + mode); db._inTransaction = true; try { const result = fn(...bindParameters); db.exec("COMMIT"); db._inTransaction = false; return result; } catch (err) { db.exec("ROLLBACK"); db._inTransaction = false; throw err; } }; }; const properties = { default: { value: wrapTxn("") }, deferred: { value: wrapTxn("DEFERRED") }, immediate: { value: wrapTxn("IMMEDIATE") }, exclusive: { value: wrapTxn("EXCLUSIVE") }, database: { value: this, enumerable: true }, }; Object.defineProperties(properties.default.value, properties); Object.defineProperties(properties.deferred.value, properties); Object.defineProperties(properties.immediate.value, properties); Object.defineProperties(properties.exclusive.value, properties); return properties.default.value; } pragma(source, options) { if (options == null) options = {}; if (typeof source !== "string") throw new TypeError("Expected first argument to be a string"); if (typeof options !== "object") throw new TypeError("Expected second argument to be an options object"); const pragma = `PRAGMA ${source}`; const stmt = this.prepare(pragma); try { const results = stmt.all(); return results; } finally { stmt.close(); } } backup(filename, options) { throw new Error("not implemented"); } serialize(options) { throw new Error("not implemented"); } function(name, options, fn) { throw new Error("not implemented"); } aggregate(name, options) { throw new Error("not implemented"); } table(name, factory) { throw new Error("not implemented"); } loadExtension(path) { throw new Error("not implemented"); } maxWriteReplicationIndex() { throw new Error("not implemented"); } /** * Executes the given SQL string * Unlike prepared statements, this can execute strings that contain multiple SQL statements * * @param {string} sql - The string containing SQL statements to execute */ exec(sql) { if (!this.open) { throw new TypeError("The database connection is not open"); } const exec = this.db.executor(sql); try { while (true) { const stepResult = exec.stepSync(); if (stepResult === STEP_IO) { this.db.ioLoopSync(); continue; } if (stepResult === STEP_DONE) { break; } if (stepResult === STEP_ROW) { // For exec(), we don't need the row data, just continue continue; } } } finally { exec.reset(); } } /** * Interrupts the database connection. */ interrupt() { throw new Error("not implemented"); } /** * Sets the default safe integers mode for all statements from this database. * * @param {boolean} [toggle] - Whether to use safe integers by default. */ defaultSafeIntegers(toggle) { this.db.defaultSafeIntegers(toggle); } /** * Closes the database connection. */ close() { this.db.close(); } } /** * Statement represents a prepared SQL statement that can be executed. */ class Statement { stmt: NativeStatement; db: NativeDatabase; constructor(stmt: NativeStatement, db: NativeDatabase) { this.stmt = stmt; this.db = db; } /** * Toggle raw mode. * * @param raw Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled. */ raw(raw) { this.stmt.raw(raw); return this; } /** * Toggle pluck mode. * * @param pluckMode Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled. */ pluck(pluckMode) { this.stmt.pluck(pluckMode); return this; } /** * Sets safe integers mode for this statement. * * @param {boolean} [toggle] - Whether to use safe integers. */ safeIntegers(toggle) { this.stmt.safeIntegers(toggle); return this; } /** * Get column information for the statement. * * @returns {Array} An array of column objects with name, column, table, database, and type properties. */ columns() { return this.stmt.columns(); } get source() { throw new Error("not implemented"); } get reader(): boolean { return this.stmt.columns().length > 0; } get database() { return this.db; } /** * Executes the SQL statement and returns an info object. */ run(...bindParameters) { const totalChangesBefore = this.db.totalChanges(); this.stmt.reset(); bindParams(this.stmt, bindParameters); for (; ;) { const stepResult = this.stmt.stepSync(); if (stepResult === STEP_IO) { this.db.ioLoopSync(); continue; } if (stepResult === STEP_DONE) { break; } if (stepResult === STEP_ROW) { // For run(), we don't need the row data, just continue continue; } } const lastInsertRowid = this.db.lastInsertRowid(); const changes = this.db.totalChanges() === totalChangesBefore ? 0 : this.db.changes(); return { changes, lastInsertRowid }; } /** * Executes the SQL statement and returns the first row. * * @param bindParameters - The bind parameters for executing the statement. */ get(...bindParameters) { this.stmt.reset(); bindParams(this.stmt, bindParameters); let row = undefined; for (; ;) { const stepResult = this.stmt.stepSync(); if (stepResult === STEP_IO) { this.db.ioLoopSync(); continue; } if (stepResult === STEP_DONE) { break; } if (stepResult === STEP_ROW && row === undefined) { row = this.stmt.row(); } } return row; } /** * Executes the SQL statement and returns an iterator to the resulting rows. * * @param bindParameters - The bind parameters for executing the statement. */ *iterate(...bindParameters) { this.stmt.reset(); bindParams(this.stmt, bindParameters); while (true) { const stepResult = this.stmt.stepSync(); if (stepResult === STEP_IO) { this.db.ioLoopSync(); continue; } if (stepResult === STEP_DONE) { break; } if (stepResult === STEP_ROW) { yield this.stmt.row(); } } } /** * Executes the SQL statement and returns an array of the resulting rows. * * @param bindParameters - The bind parameters for executing the statement. */ all(...bindParameters) { this.stmt.reset(); bindParams(this.stmt, bindParameters); const rows: any[] = []; for (; ;) { const stepResult = this.stmt.stepSync(); if (stepResult === STEP_IO) { this.db.ioLoopSync(); continue; } if (stepResult === STEP_DONE) { break; } if (stepResult === STEP_ROW) { rows.push(this.stmt.row()); } } return rows; } /** * Interrupts the statement. */ interrupt() { throw new Error("not implemented"); } /** * Binds the given parameters to the statement _permanently_ * * @param bindParameters - The bind parameters for binding the statement. * @returns this - Statement with binded parameters */ bind(...bindParameters) { try { bindParams(this.stmt, bindParameters); return this; } catch (err) { throw convertError(err); } } close() { this.stmt.finalize(); } } export { Database, Statement } ================================================ FILE: bindings/javascript/packages/common/index.ts ================================================ import { NativeDatabase, NativeStatement, DatabaseOpts, EncryptionCipher, EncryptionOpts } from "./types.js"; import { Database as DatabaseCompat, Statement as StatementCompat } from "./compat.js"; import { Database as DatabasePromise, Statement as StatementPromise } from "./promise.js"; import { SqliteError } from "./sqlite-error.js"; import { AsyncLock } from "./async-lock.js"; export { DatabaseOpts, EncryptionCipher, EncryptionOpts, DatabaseCompat, StatementCompat, DatabasePromise, StatementPromise, NativeDatabase, NativeStatement, SqliteError, AsyncLock } ================================================ FILE: bindings/javascript/packages/common/package.json ================================================ { "name": "@tursodatabase/database-common", "version": "0.6.0-pre.4", "repository": { "type": "git", "url": "https://github.com/tursodatabase/turso" }, "type": "module", "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", "packageManager": "yarn@4.9.2", "files": [ "dist/**", "README.md" ], "devDependencies": { "typescript": "^5.9.2", "vitest": "^3.2.4" }, "scripts": { "tsc-build": "npm exec tsc", "build": "npm run tsc-build", "test": "vitest --run" } } ================================================ FILE: bindings/javascript/packages/common/promise.test.ts ================================================ import { expect, test } from 'vitest' import { maybePromise } from './promise.js' test('drizzle-orm', async () => { const lazy = maybePromise(() => fetch('http://google.com')); let status, headers; //@ts-ignore lazy.apply(x => { status = x.status; }) //@ts-ignore lazy.apply(x => { headers = x.headers; }) let response = await lazy.resolve(); expect(response).not.toBeNull(); expect(status).toBe(200); expect(headers).not.toBeNull(); }) ================================================ FILE: bindings/javascript/packages/common/promise.ts ================================================ import { AsyncLock } from "./async-lock.js"; import { bindParams } from "./bind.js"; import { SqliteError } from "./sqlite-error.js"; import { NativeDatabase, NativeStatement, STEP_IO, STEP_ROW, STEP_DONE, DatabaseOpts } from "./types.js"; const convertibleErrorTypes = { TypeError }; const CONVERTIBLE_ERROR_PREFIX = "[TURSO_CONVERT_TYPE]"; function convertError(err) { if ((err.code ?? "").startsWith(CONVERTIBLE_ERROR_PREFIX)) { return createErrorByName( err.code.substring(CONVERTIBLE_ERROR_PREFIX.length), err.message, ); } return new SqliteError(err.message, err.code, err.rawCode); } function createErrorByName(name, message) { const ErrorConstructor = convertibleErrorTypes[name]; if (!ErrorConstructor) { throw new Error(`unknown error type ${name} from Turso`); } return new ErrorConstructor(message); } /** * Database represents a connection that can prepare and execute SQL statements. */ class Database { name: string; readonly: boolean; open: boolean; memory: boolean; inTransaction: boolean; private db: NativeDatabase; private ioStep: () => Promise; private execLock: AsyncLock; private _inTransaction: boolean = false; protected connected: boolean = false; constructor(db: NativeDatabase, ioStep?: () => Promise) { this.db = db; this.execLock = new AsyncLock(); this.ioStep = ioStep ?? (async () => { }); Object.defineProperties(this, { name: { get: () => this.db.path }, readonly: { get: () => this.db.readonly }, open: { get: () => this.db.open }, memory: { get: () => this.db.memory }, inTransaction: { get: () => this._inTransaction }, }); } /** * connect database */ async connect() { if (this.connected) { return; } await this.db.connectAsync(); this.connected = true; } /** * Prepares a SQL statement for execution. * * @param {string} sql - The SQL statement string to prepare. */ prepare(sql) { // Only throw if we connected before but now the database is closed // Allow implicit connection if not connected yet if (this.connected && !this.open) { throw new TypeError("The database connection is not open"); } if (!sql) { throw new RangeError("The supplied SQL string contains no statements"); } try { if (this.connected) { return new Statement(maybeValue(this.db.prepare(sql)), this.db, this.execLock, this.ioStep); } else { return new Statement(maybePromise(() => this.connect().then(() => this.db.prepare(sql))), this.db, this.execLock, this.ioStep) } } catch (err) { throw convertError(err); } } /** * Returns a function that executes the given function in a transaction. * * @param {function} fn - The function to wrap in a transaction. */ transaction(fn: (...any) => Promise) { if (typeof fn !== "function") throw new TypeError("Expected first argument to be a function"); const db = this; const wrapTxn = (mode) => { return async (...bindParameters) => { await db.exec("BEGIN " + mode); db._inTransaction = true; try { const result = await fn(...bindParameters); await db.exec("COMMIT"); db._inTransaction = false; return result; } catch (err) { await db.exec("ROLLBACK"); db._inTransaction = false; throw err; } }; }; const properties = { default: { value: wrapTxn("") }, deferred: { value: wrapTxn("DEFERRED") }, immediate: { value: wrapTxn("IMMEDIATE") }, exclusive: { value: wrapTxn("EXCLUSIVE") }, database: { value: this, enumerable: true }, }; Object.defineProperties(properties.default.value, properties); Object.defineProperties(properties.deferred.value, properties); Object.defineProperties(properties.immediate.value, properties); Object.defineProperties(properties.exclusive.value, properties); return properties.default.value; } async pragma(source, options) { if (options == null) options = {}; if (typeof source !== "string") throw new TypeError("Expected first argument to be a string"); if (typeof options !== "object") throw new TypeError("Expected second argument to be an options object"); const pragma = `PRAGMA ${source}`; const stmt = this.prepare(pragma); try { const results = await stmt.all(); return results; } finally { await stmt.close(); } } backup(filename, options) { throw new Error("not implemented"); } serialize(options) { throw new Error("not implemented"); } function(name, options, fn) { throw new Error("not implemented"); } aggregate(name, options) { throw new Error("not implemented"); } table(name, factory) { throw new Error("not implemented"); } loadExtension(path) { throw new Error("not implemented"); } maxWriteReplicationIndex() { throw new Error("not implemented"); } /** * Executes the given SQL string * Unlike prepared statements, this can execute strings that contain multiple SQL statements * * @param {string} sql - The string containing SQL statements to execute */ async exec(sql) { if (!this.open) { throw new TypeError("The database connection is not open"); } await this.execLock.acquire(); const exec = this.db.executor(sql); try { while (true) { const stepResult = exec.stepSync(); if (stepResult === STEP_IO) { await this.io(); continue; } if (stepResult === STEP_DONE) { break; } if (stepResult === STEP_ROW) { // For exec(), we don't need the row data, just continue continue; } } } finally { exec.reset(); this.execLock.release(); } } /** * Interrupts the database connection. */ interrupt() { throw new Error("not implemented"); } /** * Sets the default safe integers mode for all statements from this database. * * @param {boolean} [toggle] - Whether to use safe integers by default. */ defaultSafeIntegers(toggle) { this.db.defaultSafeIntegers(toggle); } /** * Closes the database connection. */ async close() { this.db.close(); } async io() { // For WASM browser builds, ioStep awaits a promise that resolves when // the OPFS Worker completes the I/O (via IONotifier in wasm-common). // For in-memory / Node.js builds, ioStep is a no-op since I/O is synchronous. await this.ioStep(); } } interface MaybeLazy { apply(fn: (value: T) => void); resolve(): Promise, must(): T; } function maybePromise(arg: () => Promise): MaybeLazy { let lazy = arg; let promise = null; let value = null; return { apply(fn) { let previous = lazy; lazy = async () => { const result = await previous(); fn(result); return result; } }, async resolve() { if (promise != null) { return await promise; } let valueResolve, valueReject; promise = new Promise((resolve, reject) => { valueResolve = x => { resolve(x); value = x; } valueReject = reject; }); await lazy().then(valueResolve, valueReject); return await promise; }, must() { if (value == null) { throw new Error(`database must be connected before execution the function`) } return value; }, } } function maybeValue(value: T): MaybeLazy { return { apply(fn) { fn(value); }, resolve() { return Promise.resolve(value); }, must() { return value; }, } } /** * Statement represents a prepared SQL statement that can be executed. */ class Statement { private stmt: MaybeLazy; private db: NativeDatabase; private execLock: AsyncLock; private ioStep: () => Promise; constructor(stmt: MaybeLazy, db: NativeDatabase, execLock: AsyncLock, ioStep: () => Promise) { this.stmt = stmt; this.db = db; this.execLock = execLock; this.ioStep = ioStep; } /** * Toggle raw mode. * * @param raw Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled. */ raw(raw) { this.stmt.apply(s => s.raw(raw)); return this; } /** * Toggle pluck mode. * * @param pluckMode Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled. */ pluck(pluckMode) { this.stmt.apply(s => s.pluck(pluckMode)); return this; } /** * Sets safe integers mode for this statement. * * @param {boolean} [toggle] - Whether to use safe integers. */ safeIntegers(toggle) { this.stmt.apply(s => s.safeIntegers(toggle)); return this; } /** * Get column information for the statement. * * @returns {Array} An array of column objects with name, column, table, database, and type properties. */ columns() { return this.stmt.must().columns(); } get source() { throw new Error("not implemented"); } get reader(): boolean { return this.stmt.must().columns().length > 0; } get database() { return this.db; } /** * Executes the SQL statement and returns an info object. */ async run(...bindParameters) { let stmt = await this.stmt.resolve(); bindParams(stmt, bindParameters); const totalChangesBefore = this.db.totalChanges(); await this.execLock.acquire(); try { while (true) { const stepResult = await stmt.stepSync(); if (stepResult === STEP_IO) { await this.io(); continue; } if (stepResult === STEP_DONE) { break; } if (stepResult === STEP_ROW) { // For run(), we don't need the row data, just continue continue; } } const lastInsertRowid = this.db.lastInsertRowid(); const changes = this.db.totalChanges() === totalChangesBefore ? 0 : this.db.changes(); return { changes, lastInsertRowid }; } finally { stmt.reset(); this.execLock.release(); } } /** * Executes the SQL statement and returns the first row. * * @param bindParameters - The bind parameters for executing the statement. */ async get(...bindParameters) { let stmt = await this.stmt.resolve(); bindParams(stmt, bindParameters); await this.execLock.acquire(); let row = undefined; try { while (true) { const stepResult = await stmt.stepSync(); if (stepResult === STEP_IO) { await this.io(); continue; } if (stepResult === STEP_DONE) { break; } if (stepResult === STEP_ROW && row === undefined) { row = stmt.row(); continue; } } return row; } finally { stmt.reset(); this.execLock.release(); } } /** * Executes the SQL statement and returns an iterator to the resulting rows. * * @param bindParameters - The bind parameters for executing the statement. */ async *iterate(...bindParameters) { let stmt = await this.stmt.resolve(); bindParams(stmt, bindParameters); await this.execLock.acquire(); try { while (true) { const stepResult = await stmt.stepSync(); if (stepResult === STEP_IO) { await this.io(); continue; } if (stepResult === STEP_DONE) { break; } if (stepResult === STEP_ROW) { yield stmt.row(); } } } finally { stmt.reset(); this.execLock.release(); } } /** * Executes the SQL statement and returns an array of the resulting rows. * * @param bindParameters - The bind parameters for executing the statement. */ async all(...bindParameters) { let stmt = await this.stmt.resolve(); bindParams(stmt, bindParameters); const rows: any[] = []; await this.execLock.acquire(); try { while (true) { const stepResult = await stmt.stepSync(); if (stepResult === STEP_IO) { await this.io(); continue; } if (stepResult === STEP_DONE) { break; } if (stepResult === STEP_ROW) { rows.push(stmt.row()); } } return rows; } finally { stmt.reset(); this.execLock.release(); } } async io() { await this.ioStep(); } /** * Interrupts the statement. */ interrupt() { throw new Error("not implemented"); } /** * Binds the given parameters to the statement _permanently_ * * @param bindParameters - The bind parameters for binding the statement. * @returns this - Statement with binded parameters */ bind(...bindParameters) { try { bindParams(this.stmt, bindParameters); return this; } catch (err) { throw convertError(err); } } close() { let stmt; try { stmt = this.stmt.must(); } catch (e) { // ignore error - if stmt wasn't initialized it's fine return; } stmt.finalize(); } } export { Database, Statement, maybePromise, maybeValue } ================================================ FILE: bindings/javascript/packages/common/sqlite-error.ts ================================================ export class SqliteError extends Error { name: string; code: string; rawCode: string; constructor(message, code, rawCode) { super(message); this.name = 'SqliteError'; this.code = code; this.rawCode = rawCode; (Error as any).captureStackTrace(this, SqliteError); } } ================================================ FILE: bindings/javascript/packages/common/tsconfig.json ================================================ { "compilerOptions": { "skipLibCheck": true, "declaration": true, "declarationMap": true, "module": "nodenext", "target": "esnext", "outDir": "dist/", "lib": [ "es2020" ], }, "include": [ "*" ] } ================================================ FILE: bindings/javascript/packages/common/types.ts ================================================ export type ExperimentalFeature = 'views' | 'strict' | 'encryption' | 'index_method' | 'autovacuum' | 'triggers' | 'attach'; /** Supported encryption ciphers for local database encryption. */ export type EncryptionCipher = 'aes128gcm' | 'aes256gcm' | 'aegis256' | 'aegis256x2' | 'aegis128l' | 'aegis128x2' | 'aegis128x4' /** Encryption configuration for local encryption. */ export interface EncryptionOpts { cipher: EncryptionCipher /** The hex-encoded encryption key */ hexkey: string } export interface DatabaseOpts { readonly?: boolean, fileMustExist?: boolean, timeout?: number tracing?: 'info' | 'debug' | 'trace' /** Experimental features to enable */ experimental?: ExperimentalFeature[] /** Optional local encryption configuration */ encryption?: EncryptionOpts } export interface NativeDatabase { memory: boolean, path: string, readonly: boolean; open: boolean; new(path: string): NativeDatabase; connectSync(); connectAsync(): Promise; ioLoopSync(); ioLoopAsync(): Promise; prepare(sql: string): NativeStatement; executor(sql: string): NativeExecutor; defaultSafeIntegers(toggle: boolean); totalChanges(): number; changes(): number; lastInsertRowid(): number; close(); } // Step result constants export const STEP_ROW = 1; export const STEP_DONE = 2; export const STEP_IO = 3; export interface TableColumn { name: string, type: string } export interface NativeExecutor { stepSync(): number; reset(); } export interface NativeStatement { stepAsync(): Promise; stepSync(): number; pluck(pluckMode: boolean); safeIntegers(toggle: boolean); raw(toggle: boolean); columns(): TableColumn[]; row(): any; reset(); finalize(); } ================================================ FILE: bindings/javascript/packages/native/README.md ================================================

Turso Database for JavaScript in Node

npm

Chat with other users of Turso on Discord

--- ## About This package is the Turso embedded database library for JavaScript in Node. > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ## Features - **SQLite compatible:** SQLite query language and file format support ([status](https://github.com/tursodatabase/turso/blob/main/COMPAT.md)). - **In-process**: No network overhead, runs directly in your Node.js process - **TypeScript support**: Full TypeScript definitions included - **Cross-platform**: Supports Linux (x86 and arm64), macOS, Windows (browser is supported in the separate package `@tursodatabase/database-wasm` package) ## Installation ```bash npm install @tursodatabase/database ``` ## Getting Started ### In-Memory Database ```javascript import { connect } from '@tursodatabase/database'; // Create an in-memory database const db = await connect(':memory:'); // Create a table await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)'); // Insert data const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); await insert.run('Alice', 'alice@example.com'); await insert.run('Bob', 'bob@example.com'); // Query data const users = await db.prepare('SELECT * FROM users').all(); console.log(users); // Output: [ // { id: 1, name: 'Alice', email: 'alice@example.com' }, // { id: 2, name: 'Bob', email: 'bob@example.com' } // ] ``` ### File-Based Database ```javascript import { connect } from '@tursodatabase/database'; // Create or open a database file const db = await connect('my-database.db'); // Create a table await db.exec(` CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Insert a post const insertPost = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)'); const result = await insertPost.run('Hello World', 'This is my first blog post!'); console.log(`Inserted post with ID: ${result.lastInsertRowid}`); ``` ### Transactions ```javascript import { connect } from '@tursodatabase/database'; const db = await connect('transactions.db'); // Using transactions for atomic operations const transaction = db.transaction(async (users) => { const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); for (const user of users) { await insert.run(user.name, user.email); } }); // Execute transaction await transaction([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' } ]); ``` ## API Reference For complete API documentation, see [JavaScript API Reference](https://github.com/tursodatabase/turso/blob/main/docs/javascript-api-reference.md). ## Related Packages * The [@tursodatabase/serverless](https://www.npmjs.com/package/@tursodatabase/serverless) package provides a serverless driver with the same API. * The [@tursodatabase/sync](https://www.npmjs.com/package/@tursodatabase/sync) package provides bidirectional sync between a local Turso database and Turso Cloud. ## License This project is licensed under the [MIT license](https://github.com/tursodatabase/turso/blob/main/LICENSE.md) ## Support - [GitHub Issues](https://github.com/tursodatabase/turso/issues) - [Documentation](https://docs.turso.tech) - [Discord Community](https://tur.so/discord) ================================================ FILE: bindings/javascript/packages/native/compat.test.ts ================================================ import { unlinkSync } from "node:fs"; import { expect, test } from 'vitest' import { Database } from './compat.js' test('insert returning test', () => { const db = new Database(':memory:'); db.prepare(`create table t (x);`).run(); const x1 = db.prepare(`insert into t values (1), (2) returning x`).get(); const x2 = db.prepare(`insert into t values (3), (4) returning x`).get(); expect(x1).toEqual({ x: 1 }); expect(x2).toEqual({ x: 3 }); const all = db.prepare(`select * from t`).all(); expect(all).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }]) }) test('in-memory db', () => { const db = new Database(":memory:"); db.exec("CREATE TABLE t(x)"); db.exec("INSERT INTO t VALUES (1), (2), (3)"); const stmt = db.prepare("SELECT * FROM t WHERE x % 2 = ?"); const rows = stmt.all([1]); expect(rows).toEqual([{ x: 1 }, { x: 3 }]); }) test('exec multiple statements', async () => { const db = new Database(":memory:"); db.exec("CREATE TABLE t(x); INSERT INTO t VALUES (1); INSERT INTO t VALUES (2)"); const stmt = db.prepare("SELECT * FROM t"); const rows = stmt.all(); expect(rows).toEqual([{ x: 1 }, { x: 2 }]); }) test('readonly-db', () => { const path = `test-${(Math.random() * 10000) | 0}.db`; try { { const rw = new Database(path); rw.exec("CREATE TABLE t(x)"); rw.exec("INSERT INTO t VALUES (1)"); rw.close(); } { const ro = new Database(path, { readonly: true }); expect(() => ro.exec("INSERT INTO t VALUES (2)")).toThrowError(/Resource is read-only/g); expect(ro.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }]) ro.close(); } } finally { unlinkSync(path); unlinkSync(`${path}-wal`); } }) test('file-must-exist', () => { const path = `test-${(Math.random() * 10000) | 0}.db`; expect(() => new Database(path, { fileMustExist: true })).toThrowError(/failed to open database/); }) test('on-disk db', () => { const path = `test-${(Math.random() * 10000) | 0}.db`; try { const db1 = new Database(path); db1.exec("CREATE TABLE t(x)"); db1.exec("INSERT INTO t VALUES (1), (2), (3)"); const stmt1 = db1.prepare("SELECT * FROM t WHERE x % 2 = ?"); expect(stmt1.columns()).toEqual([{ name: "x", column: null, database: null, table: null, type: null }]); const rows1 = stmt1.all([1]); expect(rows1).toEqual([{ x: 1 }, { x: 3 }]); db1.close(); const db2 = new Database(path); const stmt2 = db2.prepare("SELECT * FROM t WHERE x % 2 = ?"); expect(stmt2.columns()).toEqual([{ name: "x", column: null, database: null, table: null, type: null }]); const rows2 = stmt2.all([1]); expect(rows2).toEqual([{ x: 1 }, { x: 3 }]); db2.close(); } finally { unlinkSync(path); unlinkSync(`${path}-wal`); } }) test('attach', () => { const path1 = `test-${(Math.random() * 10000) | 0}.db`; const path2 = `test-${(Math.random() * 10000) | 0}.db`; try { const db1 = new Database(path1, { experimental: ["attach"] }); db1.exec("CREATE TABLE t(x)"); db1.exec("INSERT INTO t VALUES (1), (2), (3)"); const db2 = new Database(path2, { experimental: ["attach"] }); db2.exec("CREATE TABLE q(x)"); db2.exec("INSERT INTO q VALUES (4), (5), (6)"); db1.exec(`ATTACH '${path2}' as secondary`); const stmt = db1.prepare("SELECT * FROM t UNION ALL SELECT * FROM secondary.q"); expect(stmt.columns()).toEqual([{ name: "x", column: null, database: null, table: null, type: null }]); const rows = stmt.all([1]); expect(rows).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }, { x: 5 }, { x: 6 }]); } finally { unlinkSync(path1); unlinkSync(`${path1}-wal`); unlinkSync(path2); unlinkSync(`${path2}-wal`); } }) test('blobs', () => { const db = new Database(":memory:"); const rows = db.prepare("SELECT x'1020' as x").all(); expect(rows).toEqual([{ x: Buffer.from([16, 32]) }]) }) test('encryption', () => { const path = `test-encryption-${(Math.random() * 10000) | 0}.db`; const hexkey = 'b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327'; const wrongKey = 'aaaaaaa4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327'; try { const db = new Database(path, { encryption: { cipher: 'aegis256', hexkey } }); db.exec("CREATE TABLE t(x)"); db.exec("INSERT INTO t SELECT 'secret' FROM generate_series(1, 1024)"); db.exec("PRAGMA wal_checkpoint(truncate)"); db.close(); // lets re-open with the same key const db2 = new Database(path, { encryption: { cipher: 'aegis256', hexkey } }); const rows = db2.prepare("SELECT COUNT(*) as cnt FROM t").all(); expect(rows).toEqual([{ cnt: 1024 }]); db2.close(); // opening with wrong key MUST fail expect(() => { const db3 = new Database(path, { encryption: { cipher: 'aegis256', hexkey: wrongKey } }); db3.prepare("SELECT * FROM t").all(); }).toThrow(); // opening without encryption MUST fail expect(() => { const db5 = new Database(path); db5.prepare("SELECT * FROM t").all(); }).toThrow(); } finally { unlinkSync(path); } }) ================================================ FILE: bindings/javascript/packages/native/compat.ts ================================================ import { DatabaseCompat, NativeDatabase, SqliteError, DatabaseOpts, EncryptionCipher } from "@tursodatabase/database-common" import { Database as NativeDB, EncryptionCipher as NativeEncryptionCipher } from "#index"; // Map string cipher names to native enum values (lazy to avoid errors if native module lacks encryption) function getCipherValue(cipher: EncryptionCipher): number { if (!NativeEncryptionCipher) { throw new Error('Encryption is not supported in this build'); } const cipherMap: Record = { 'aes128gcm': NativeEncryptionCipher.Aes128Gcm, 'aes256gcm': NativeEncryptionCipher.Aes256Gcm, 'aegis256': NativeEncryptionCipher.Aegis256, 'aegis256x2': NativeEncryptionCipher.Aegis256x2, 'aegis128l': NativeEncryptionCipher.Aegis128l, 'aegis128x2': NativeEncryptionCipher.Aegis128x2, 'aegis128x4': NativeEncryptionCipher.Aegis128x4, }; return cipherMap[cipher]; } class Database extends DatabaseCompat { constructor(path: string, opts: DatabaseOpts = {}) { const nativeOpts: any = { ...opts }; if (opts.encryption) { nativeOpts.encryption = { cipher: getCipherValue(opts.encryption.cipher), hexkey: opts.encryption.hexkey, }; } super(new NativeDB(path, nativeOpts) as unknown as NativeDatabase) } } export { Database, SqliteError } ================================================ FILE: bindings/javascript/packages/native/index.d.ts ================================================ /* auto-generated by NAPI-RS */ /* eslint-disable */ export declare class BatchExecutor { stepSync(): number reset(): void } /** A database connection. */ export declare class Database { /** * Creates a new database instance. * * # Arguments * * `path` - The path to the database file. */ constructor(path: string, opts?: DatabaseOpts | undefined | null) /** * Connect the database synchronously * This method is idempotent and can be called multiple times safely until the database will be closed */ connectSync(): void /** * Connect the database asynchronously * This method is idempotent and can be called multiple times safely until the database will be closed */ connectAsync(): Promise /** Returns whether the database is in readonly-only mode. */ get readonly(): boolean /** Returns whether the database is in memory-only mode. */ get memory(): boolean /** Returns whether the database is in memory-only mode. */ get path(): string /** Returns whether the database connection is open. */ get open(): boolean /** * Prepares a statement for execution. * * # Arguments * * * `sql` - The SQL statement to prepare. * * # Returns * * A `Statement` instance. */ prepare(sql: string): Statement executor(sql: string): BatchExecutor /** * Returns the rowid of the last row inserted. * * # Returns * * The rowid of the last row inserted. */ lastInsertRowid(): number /** * Returns the number of changes made by the last statement. * * # Returns * * The number of changes made by the last statement. */ changes(): number /** * Returns the total number of changes made by all statements. * * # Returns * * The total number of changes made by all statements. */ totalChanges(): number /** * Closes the database connection. * * # Returns * * `Ok(())` if the database is closed successfully. */ close(): void /** * Sets the default safe integers mode for all statements from this database. * * # Arguments * * * `toggle` - Whether to use safe integers by default. */ defaultSafeIntegers(toggle?: boolean | undefined | null): void /** Runs the I/O loop synchronously. */ ioLoopSync(): void /** Runs the I/O loop asynchronously, returning a Promise. */ ioLoopAsync(): Promise /** Classify SQL statement. Returns "read", "write", "begin", "commit", or "rollback". */ classifySql(sql: string): string } /** A prepared statement. */ export declare class Statement { reset(): void /** Returns the number of parameters in the statement. */ parameterCount(): number /** * Returns the name of a parameter at a specific 1-based index. * * # Arguments * * * `index` - The 1-based parameter index. */ parameterName(index: number): string | null /** * Binds a parameter at a specific 1-based index with explicit type. * * # Arguments * * * `index` - The 1-based parameter index. * * `value_type` - The type constant (0=null, 1=int, 2=float, 3=text, 4=blob). * * `value` - The value to bind. */ bindAt(index: number, value: unknown): void /** * Step the statement and return result code (executed on the main thread): * 1 = Row available, 2 = Done, 3 = I/O needed */ stepSync(): number /** Get the current row data according to the presentation mode */ row(): unknown /** Sets the presentation mode to raw. */ raw(raw?: boolean | undefined | null): void /** Sets the presentation mode to pluck. */ pluck(pluck?: boolean | undefined | null): void /** * Sets safe integers mode for this statement. * * # Arguments * * * `toggle` - Whether to use safe integers. */ safeIntegers(toggle?: boolean | undefined | null): void /** Get column information for the statement */ columns(): Promise /** Finalizes the statement. */ finalize(): void } /** * Most of the options are aligned with better-sqlite API * (see https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#new-databasepath-options) */ export interface DatabaseOpts { readonly?: boolean timeout?: number fileMustExist?: boolean tracing?: string /** Experimental features to enable */ experimental?: Array /** Optional encryption configuration for local database encryption */ encryption?: EncryptionOpts } /** Supported encryption ciphers for local database encryption. */ export declare const enum EncryptionCipher { Aes128Gcm = 0, Aes256Gcm = 1, Aegis256 = 2, Aegis256x2 = 3, Aegis128l = 4, Aegis128x2 = 5, Aegis128x4 = 6 } /** Encryption configuration for local database encryption. */ export interface EncryptionOpts { /** The cipher to use for encryption */ cipher: EncryptionCipher /** The hex-encoded encryption key */ hexkey: string } ================================================ FILE: bindings/javascript/packages/native/index.js ================================================ // prettier-ignore /* eslint-disable */ // @ts-nocheck /* auto-generated by NAPI-RS */ import { createRequire } from 'node:module' const require = createRequire(import.meta.url) const __dirname = new URL('.', import.meta.url).pathname const { readFileSync } = require('node:fs') let nativeBinding = null const loadErrors = [] const isMusl = () => { let musl = false if (process.platform === 'linux') { musl = isMuslFromFilesystem() if (musl === null) { musl = isMuslFromReport() } if (musl === null) { musl = isMuslFromChildProcess() } } return musl } const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') const isMuslFromFilesystem = () => { try { return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') } catch { return null } } const isMuslFromReport = () => { let report = null if (typeof process.report?.getReport === 'function') { process.report.excludeNetwork = true report = process.report.getReport() } if (!report) { return null } if (report.header && report.header.glibcVersionRuntime) { return false } if (Array.isArray(report.sharedObjects)) { if (report.sharedObjects.some(isFileMusl)) { return true } } return false } const isMuslFromChildProcess = () => { try { return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') } catch (e) { // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false return false } } function requireNative() { if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { try { nativeBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); } catch (err) { loadErrors.push(err) } } else if (process.platform === 'android') { if (process.arch === 'arm64') { try { return require('./turso.android-arm64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-android-arm64') const bindingPackageVersion = require('@tursodatabase/database-android-arm64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'arm') { try { return require('./turso.android-arm-eabi.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-android-arm-eabi') const bindingPackageVersion = require('@tursodatabase/database-android-arm-eabi/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) } } else if (process.platform === 'win32') { if (process.arch === 'x64') { try { return require('./turso.win32-x64-msvc.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-win32-x64-msvc') const bindingPackageVersion = require('@tursodatabase/database-win32-x64-msvc/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'ia32') { try { return require('./turso.win32-ia32-msvc.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-win32-ia32-msvc') const bindingPackageVersion = require('@tursodatabase/database-win32-ia32-msvc/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'arm64') { try { return require('./turso.win32-arm64-msvc.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-win32-arm64-msvc') const bindingPackageVersion = require('@tursodatabase/database-win32-arm64-msvc/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) } } else if (process.platform === 'darwin') { try { return require('./turso.darwin-universal.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-darwin-universal') const bindingPackageVersion = require('@tursodatabase/database-darwin-universal/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } if (process.arch === 'x64') { try { return require('./turso.darwin-x64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-darwin-x64') const bindingPackageVersion = require('@tursodatabase/database-darwin-x64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'arm64') { try { return require('./turso.darwin-arm64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-darwin-arm64') const bindingPackageVersion = require('@tursodatabase/database-darwin-arm64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) } } else if (process.platform === 'freebsd') { if (process.arch === 'x64') { try { return require('./turso.freebsd-x64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-freebsd-x64') const bindingPackageVersion = require('@tursodatabase/database-freebsd-x64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'arm64') { try { return require('./turso.freebsd-arm64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-freebsd-arm64') const bindingPackageVersion = require('@tursodatabase/database-freebsd-arm64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) } } else if (process.platform === 'linux') { if (process.arch === 'x64') { if (isMusl()) { try { return require('./turso.linux-x64-musl.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-linux-x64-musl') const bindingPackageVersion = require('@tursodatabase/database-linux-x64-musl/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { try { return require('./turso.linux-x64-gnu.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-linux-x64-gnu') const bindingPackageVersion = require('@tursodatabase/database-linux-x64-gnu/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } } else if (process.arch === 'arm64') { if (isMusl()) { try { return require('./turso.linux-arm64-musl.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-linux-arm64-musl') const bindingPackageVersion = require('@tursodatabase/database-linux-arm64-musl/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { try { return require('./turso.linux-arm64-gnu.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-linux-arm64-gnu') const bindingPackageVersion = require('@tursodatabase/database-linux-arm64-gnu/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } } else if (process.arch === 'arm') { if (isMusl()) { try { return require('./turso.linux-arm-musleabihf.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-linux-arm-musleabihf') const bindingPackageVersion = require('@tursodatabase/database-linux-arm-musleabihf/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { try { return require('./turso.linux-arm-gnueabihf.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-linux-arm-gnueabihf') const bindingPackageVersion = require('@tursodatabase/database-linux-arm-gnueabihf/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } } else if (process.arch === 'riscv64') { if (isMusl()) { try { return require('./turso.linux-riscv64-musl.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-linux-riscv64-musl') const bindingPackageVersion = require('@tursodatabase/database-linux-riscv64-musl/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { try { return require('./turso.linux-riscv64-gnu.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-linux-riscv64-gnu') const bindingPackageVersion = require('@tursodatabase/database-linux-riscv64-gnu/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } } else if (process.arch === 'ppc64') { try { return require('./turso.linux-ppc64-gnu.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-linux-ppc64-gnu') const bindingPackageVersion = require('@tursodatabase/database-linux-ppc64-gnu/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 's390x') { try { return require('./turso.linux-s390x-gnu.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-linux-s390x-gnu') const bindingPackageVersion = require('@tursodatabase/database-linux-s390x-gnu/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) } } else if (process.platform === 'openharmony') { if (process.arch === 'arm64') { try { return require('./turso.openharmony-arm64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-openharmony-arm64') const bindingPackageVersion = require('@tursodatabase/database-openharmony-arm64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'x64') { try { return require('./turso.openharmony-x64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-openharmony-x64') const bindingPackageVersion = require('@tursodatabase/database-openharmony-x64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'arm') { try { return require('./turso.openharmony-arm.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/database-openharmony-arm') const bindingPackageVersion = require('@tursodatabase/database-openharmony-arm/package.json').version if (bindingPackageVersion !== '0.5.0-pre.20' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.20 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`)) } } else { loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) } } nativeBinding = requireNative() if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { try { nativeBinding = require('./turso.wasi.cjs') } catch (err) { if (process.env.NAPI_RS_FORCE_WASI) { loadErrors.push(err) } } if (!nativeBinding) { try { nativeBinding = require('@tursodatabase/database-wasm32-wasi') } catch (err) { if (process.env.NAPI_RS_FORCE_WASI) { loadErrors.push(err) } } } } if (!nativeBinding) { if (loadErrors.length > 0) { throw new Error( `Cannot find native binding. ` + `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', { cause: loadErrors } ) } throw new Error(`Failed to load native binding`) } const { BatchExecutor, Database, Statement, EncryptionCipher } = nativeBinding export { BatchExecutor } export { Database } export { Statement } export { EncryptionCipher } ================================================ FILE: bindings/javascript/packages/native/package.json ================================================ { "name": "@tursodatabase/database", "version": "0.6.0-pre.4", "repository": { "type": "git", "url": "https://github.com/tursodatabase/turso" }, "license": "MIT", "module": "./dist/promise.js", "main": "./dist/promise.js", "type": "module", "exports": { ".": "./dist/promise.js", "./compat": "./dist/compat.js" }, "files": [ "index.js", "dist/**", "README.md" ], "packageManager": "yarn@4.9.2", "devDependencies": { "@napi-rs/cli": "^3.1.5", "@types/node": "^24.3.1", "better-sqlite3": "^12.2.0", "drizzle-kit": "^0.31.4", "drizzle-orm": "^0.44.5", "typescript": "^5.9.2", "vitest": "^3.2.4" }, "scripts": { "napi-build": "napi build --platform --profile release-official --esm --manifest-path ../../Cargo.toml --output-dir .", "napi-dirs": "napi create-npm-dirs", "napi-artifacts": "napi artifacts --output-dir .", "tsc-build": "npm exec tsc", "build": "npm run napi-build && npm run tsc-build", "test": "vitest --run", "prepublishOnly": "npm run napi-dirs && npm run napi-artifacts && napi prepublish -t npm" }, "napi": { "binaryName": "turso", "targets": [ "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", "aarch64-apple-darwin", "aarch64-unknown-linux-gnu" ] }, "dependencies": { "@tursodatabase/database-common": "^0.6.0-pre.4" }, "imports": { "#index": "./index.js" } } ================================================ FILE: bindings/javascript/packages/native/promise.test.ts ================================================ import { unlinkSync } from "node:fs"; import { expect, test } from 'vitest' import { Database, connect } from './promise.js' import { sql } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/better-sqlite3'; test('drizzle-orm', async () => { const path = `test-${(Math.random() * 10000) | 0}.db`; try { const conn = await connect(path); const db = drizzle(conn); await db.run('CREATE TABLE t(x, y)'); let tasks = []; for (let i = 0; i < 1234; i++) { tasks.push(db.run(sql`INSERT INTO t VALUES (${i}, randomblob(${i} * 5))`)) } await Promise.all(tasks); expect(await db.all("SELECT COUNT(*) as cnt FROM t")).toEqual([{ cnt: 1234 }]) } finally { unlinkSync(path); unlinkSync(`${path}-wal`); } }) test('in-memory-db-async', async () => { const db = await connect(":memory:"); await db.exec("CREATE TABLE t(x)"); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); const stmt = db.prepare("SELECT * FROM t WHERE x % 2 = ?"); const rows = await stmt.all([1]); expect(rows).toEqual([{ x: 1 }, { x: 3 }]); }) test('exec multiple statements', async () => { const db = await connect(":memory:"); await db.exec("CREATE TABLE t(x); INSERT INTO t VALUES (1); INSERT INTO t VALUES (2)"); const stmt = db.prepare("SELECT * FROM t"); const rows = await stmt.all(); expect(rows).toEqual([{ x: 1 }, { x: 2 }]); }) test('readonly-db', async () => { const path = `test-${(Math.random() * 10000) | 0}.db`; try { { const rw = await connect(path); await rw.exec("CREATE TABLE t(x)"); await rw.exec("INSERT INTO t VALUES (1)"); rw.close(); } { const ro = await connect(path, { readonly: true }); await expect(async () => await ro.exec("INSERT INTO t VALUES (2)")).rejects.toThrowError(/Resource is read-only/g); expect(await ro.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }]) ro.close(); } } finally { unlinkSync(path); unlinkSync(`${path}-wal`); } }) test('file-must-exist', async () => { const path = `test-${(Math.random() * 10000) | 0}.db`; await expect(async () => await connect(path, { fileMustExist: true })).rejects.toThrowError(/failed to open database/); }) test('implicit connect', async () => { const db = new Database(':memory:'); const defer = db.prepare("SELECT * FROM t"); await expect(async () => await defer.all()).rejects.toThrowError(/no such table: t/); expect(() => db.prepare("SELECT * FROM t")).toThrowError(/no such table: t/); expect(await db.prepare("SELECT 1 as x").all()).toEqual([{ x: 1 }]); }) test('zero-limit-bug', async () => { const db = await connect(':memory:'); const create = db.prepare(`CREATE TABLE users (name TEXT NOT NULL);`); await create.run(); const insert = db.prepare( `insert into "users" values (?), (?), (?);`, ); await insert.run('John', 'Jane', 'Jack'); const stmt1 = db.prepare(`select * from "users" limit ?;`); expect(await stmt1.all(0)).toEqual([]); let rows = [{ name: 'John' }, { name: 'Jane' }, { name: 'Jack' }, { name: 'John' }, { name: 'Jane' }, { name: 'Jack' }]; for (const limit of [0, 1, 2, 3, 4, 5, 6, 7]) { const stmt2 = db.prepare(`select * from "users" union all select * from "users" limit ?;`); expect(await stmt2.all(limit)).toEqual(rows.slice(0, Math.min(limit, 6))); } }) test('avg-bug', async () => { const db = await connect(':memory:'); const create = db.prepare(`create table "aggregate_table" ( "id" integer primary key autoincrement not null, "name" text not null, "a" integer, "b" integer, "c" integer, "null_only" integer );`); await create.run(); const insert = db.prepare( `insert into "aggregate_table" ("id", "name", "a", "b", "c", "null_only") values (null, ?, ?, ?, ?, null), (null, ?, ?, ?, ?, null), (null, ?, ?, ?, ?, null), (null, ?, ?, ?, ?, null), (null, ?, ?, ?, ?, null), (null, ?, ?, ?, ?, null), (null, ?, ?, ?, ?, null);`, ); await insert.run( 'value 1', 5, 10, 20, 'value 1', 5, 20, 30, 'value 2', 10, 50, 60, 'value 3', 20, 20, null, 'value 4', null, 90, 120, 'value 5', 80, 10, null, 'value 6', null, null, 150, ); expect(await db.prepare(`select avg("a") from "aggregate_table";`).get()).toEqual({ 'avg (aggregate_table.a)': 24 }); expect(await db.prepare(`select avg("null_only") from "aggregate_table";`).get()).toEqual({ 'avg (aggregate_table.null_only)': null }); expect(await db.prepare(`select avg(distinct "b") from "aggregate_table";`).get()).toEqual({ 'avg (DISTINCT aggregate_table.b)': 42.5 }); }) test('insert returning test', async () => { const db = await connect(':memory:'); await db.prepare(`create table t (x);`).run(); const x1 = await db.prepare(`insert into t values (1), (2) returning x`).get(); const x2 = await db.prepare(`insert into t values (3), (4) returning x`).get(); expect(x1).toEqual({ x: 1 }); expect(x2).toEqual({ x: 3 }); const all = await db.prepare(`select * from t`).all(); expect(all).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }]) }) test('offset-bug', async () => { const db = await connect(":memory:"); await db.exec(`CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, verified integer not null default 0 );`); const insert = db.prepare(`INSERT INTO users (name) VALUES (?),(?);`); await insert.run('John', 'John1'); const stmt = db.prepare(`SELECT * FROM users LIMIT ? OFFSET ?;`); expect(await stmt.all(1, 1)).toEqual([{ id: 2, name: 'John1', verified: 0 }]) }) test('conflict-bug', async () => { const db = await connect(':memory:'); const create = db.prepare(`create table "conflict_chain_example" ( id integer not null unique, name text not null, email text not null, primary key (id, name) )`); await create.run(); await db.prepare(`insert into "conflict_chain_example" ("id", "name", "email") values (?, ?, ?), (?, ?, ?)`).run( 1, 'John', 'john@example.com', 2, 'John Second', '2john@example.com', ); const insert = db.prepare( `insert into "conflict_chain_example" ("id", "name", "email") values (?, ?, ?), (?, ?, ?) on conflict ("conflict_chain_example"."id", "conflict_chain_example"."name") do update set "email" = ? on conflict ("conflict_chain_example"."id") do nothing`, ); await insert.run(1, 'John', 'john@example.com', 2, 'Anthony', 'idthief@example.com', 'john1@example.com'); expect(await db.prepare("SELECT * FROM conflict_chain_example").all()).toEqual([ { id: 1, name: 'John', email: 'john1@example.com' }, { id: 2, name: 'John Second', email: '2john@example.com' } ]); }) test('on-disk db', async () => { const path = `test-${(Math.random() * 10000) | 0}.db`; try { const db1 = await connect(path); await db1.exec("CREATE TABLE t(x)"); await db1.exec("INSERT INTO t VALUES (1), (2), (3)"); const stmt1 = db1.prepare("SELECT * FROM t WHERE x % 2 = ?"); expect(stmt1.columns()).toEqual([{ name: "x", column: null, database: null, table: null, type: null }]); const rows1 = await stmt1.all([1]); expect(rows1).toEqual([{ x: 1 }, { x: 3 }]); db1.close(); const db2 = await connect(path); const stmt2 = db2.prepare("SELECT * FROM t WHERE x % 2 = ?"); expect(stmt2.columns()).toEqual([{ name: "x", column: null, database: null, table: null, type: null }]); const rows2 = await stmt2.all([1]); expect(rows2).toEqual([{ x: 1 }, { x: 3 }]); db2.close(); } finally { unlinkSync(path); unlinkSync(`${path}-wal`); } }) test('attach', async () => { const path1 = `test-${(Math.random() * 10000) | 0}.db`; const path2 = `test-${(Math.random() * 10000) | 0}.db`; try { const db1 = await connect(path1, { experimental: ["attach"] }); await db1.exec("CREATE TABLE t(x)"); await db1.exec("INSERT INTO t VALUES (1), (2), (3)"); const db2 = await connect(path2, { experimental: ["attach"] }); await db2.exec("CREATE TABLE q(x)"); await db2.exec("INSERT INTO q VALUES (4), (5), (6)"); await db1.exec(`ATTACH '${path2}' as secondary`); const stmt = db1.prepare("SELECT * FROM t UNION ALL SELECT * FROM secondary.q"); expect(stmt.columns()).toEqual([{ name: "x", column: null, database: null, table: null, type: null }]); const rows = await stmt.all([1]); expect(rows).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }, { x: 5 }, { x: 6 }]); } finally { unlinkSync(path1); unlinkSync(`${path1}-wal`); unlinkSync(path2); unlinkSync(`${path2}-wal`); } }) test('fts', async () => { const db = await connect(":memory:", { experimental: ["index_method"] }); await db.exec(` CREATE TABLE documents (id INTEGER PRIMARY KEY, title TEXT, body TEXT); INSERT INTO documents VALUES (1, 'Introduction to Rust', 'Rust is a systems programming language focused on safety and performance'); INSERT INTO documents VALUES (2, 'JavaScript Guide', 'JavaScript is a dynamic programming language used for web development'); INSERT INTO documents VALUES (3, 'Database Internals', 'Understanding how databases store and retrieve data efficiently'); CREATE INDEX documents_fts ON documents USING fts (title, body); `); // fts_match search const matchResults = await db.prepare( "SELECT id, title, fts_score(title, body, 'programming language') as score FROM documents WHERE fts_match(title, body, 'programming language')" ).all(); expect(matchResults.length).toBe(2); expect(matchResults.map(r => r.id).sort()).toEqual([1, 2]); for (const row of matchResults) { expect(row.score).toBeGreaterThan(0); } // fts_highlight const highlightResults = await db.prepare( "SELECT id, fts_highlight(title, '', '', 'Rust') as highlighted FROM documents WHERE fts_match(title, body, 'Rust')" ).all(); expect(highlightResults.length).toBe(1); expect(highlightResults[0].id).toBe(1); expect(highlightResults[0].highlighted).toContain(''); expect(highlightResults[0].highlighted).toContain('Rust'); // no match const noResults = await db.prepare( "SELECT * FROM documents WHERE fts_match(title, body, 'nonexistentterm')" ).all(); expect(noResults.length).toBe(0); }) test('blobs', async () => { const db = await connect(":memory:"); const rows = await db.prepare("SELECT x'1020' as x").all(); expect(rows).toEqual([{ x: Buffer.from([16, 32]) }]) }) test('example-1', async () => { const db = await connect(':memory:'); await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)'); const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); await insert.run('Alice', 'alice@example.com'); await insert.run('Bob', 'bob@example.com'); const users = await db.prepare('SELECT * FROM users').all(); expect(users).toEqual([ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' } ]); }) test('example-2', async () => { const db = await connect(':memory:'); await db.exec('CREATE TABLE users (name, email)'); // Using transactions for atomic operations const transaction = db.transaction(async (users) => { const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); for (const user of users) { await insert.run(user.name, user.email); } }); // Execute transaction await transaction([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' } ]); const rows = await db.prepare('SELECT * FROM users').all(); expect(rows).toEqual([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' } ]); }) ================================================ FILE: bindings/javascript/packages/native/promise.ts ================================================ import { DatabasePromise, NativeDatabase, SqliteError, DatabaseOpts } from "@tursodatabase/database-common" import { Database as NativeDB } from "#index"; class Database extends DatabasePromise { constructor(path: string, opts: DatabaseOpts = {}) { super(new NativeDB(path, opts) as unknown as NativeDatabase) } } /** * Creates a new database connection asynchronously. * * @param {string} path - Path to the database file. * @param {Object} opts - Options for database behavior. * @returns {Promise} - A promise that resolves to a Database instance. */ async function connect(path: string, opts: DatabaseOpts = {}): Promise { const db = new Database(path, opts); await db.connect(); return db; } export { connect, Database, SqliteError } ================================================ FILE: bindings/javascript/packages/native/tsconfig.json ================================================ { "compilerOptions": { "skipLibCheck": true, "declaration": true, "declarationMap": true, "module": "nodenext", "target": "esnext", "outDir": "dist/", "lib": [ "es2020" ], "paths": { "#index": [ "./index.js" ] } }, "include": [ "*" ] } ================================================ FILE: bindings/javascript/packages/native/turso-sql-runner-split.test.ts ================================================ import { expect, test } from 'vitest' import { splitStatements } from '../../turso-sql-split.mjs' test('splitStatements keeps trigger body as one statement', () => { const sql = ` CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); UPDATE stats SET count = count + 1; END; `; expect(splitStatements(sql)).toEqual([ `CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); UPDATE stats SET count = count + 1; END;` ]); }) test('splitStatements handles trigger followed by select', () => { const sql = ` CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); END; SELECT 1; `; expect(splitStatements(sql)).toEqual([ `CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); END;`, 'SELECT 1;' ]); }) test('splitStatements ignores END inside trigger string literal', () => { const sql = ` CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('END'); END; `; expect(splitStatements(sql)).toEqual([ `CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('END'); END;` ]); }) test('splitStatements handles create temp trigger and explain create trigger', () => { const tempTrigger = ` CREATE TEMP TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); END; `; const explainTrigger = ` EXPLAIN CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); END; `; expect(splitStatements(tempTrigger)).toHaveLength(1); expect(splitStatements(explainTrigger)).toHaveLength(1); }) ================================================ FILE: bindings/javascript/packages/wasm/README.md ================================================

Turso Database for JavaScript in Browser

npm

Chat with other users of Turso on Discord

--- ## About This package is the Turso embedded database library for JavaScript in Browser. > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ## Features - **SQLite compatible:** SQLite query language and file format support ([status](https://github.com/tursodatabase/turso/blob/main/COMPAT.md)). - **In-process**: No network overhead, runs directly in your Node.js process - **TypeScript support**: Full TypeScript definitions included ## Installation ```bash npm install @tursodatabase/database-wasm ``` ## Getting Started ### In-Memory Database ```javascript import { connect } from '@tursodatabase/database-wasm'; // Create an in-memory database const db = await connect(':memory:'); // Create a table await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)'); // Insert data const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); await insert.run('Alice', 'alice@example.com'); await insert.run('Bob', 'bob@example.com'); // Query data const users = await db.prepare('SELECT * FROM users').all(); console.log(users); // Output: [ // { id: 1, name: 'Alice', email: 'alice@example.com' }, // { id: 2, name: 'Bob', email: 'bob@example.com' } // ] ``` ### File-Based Database ```javascript import { connect } from '@tursodatabase/database-wasm'; // Create or open a database file const db = await connect('my-database.db'); // Create a table await db.exec(` CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Insert a post const insertPost = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)'); const result = await insertPost.run('Hello World', 'This is my first blog post!'); console.log(`Inserted post with ID: ${result.lastInsertRowid}`); ``` ### Transactions ```javascript import { connect } from '@tursodatabase/database-wasm'; const db = await connect('transactions.db'); // Using transactions for atomic operations const transaction = db.transaction(async (users) => { const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); for (const user of users) { await insert.run(user.name, user.email); } }); // Execute transaction await transaction([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' } ]); ``` ## API Reference For complete API documentation, see [JavaScript API Reference](https://github.com/tursodatabase/turso/blob/main/docs/javascript-api-reference.md). ## Related Packages * The [@tursodatabase/serverless](https://www.npmjs.com/package/@tursodatabase/serverless) package provides a serverless driver with the same API. * The [@tursodatabase/sync](https://www.npmjs.com/package/@tursodatabase/sync) package provides bidirectional sync between a local Turso database and Turso Cloud. ## License This project is licensed under the [MIT license](https://github.com/tursodatabase/turso/blob/main/LICENSE.md). ## Support - [GitHub Issues](https://github.com/tursodatabase/turso/issues) - [Documentation](https://docs.turso.tech) - [Discord Community](https://tur.so/discord) ================================================ FILE: bindings/javascript/packages/wasm/index-bundle.ts ================================================ import { setupMainThread } from "@tursodatabase/database-wasm-common"; //@ts-ignore import TursoWorker from "./worker.js?worker&inline"; const __wasmUrl = new URL('./turso.wasm32-wasi.wasm', import.meta.url).href; const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer()) export let MainWorker = null; const napiModule = await setupMainThread(__wasmFile, () => { const worker = new TursoWorker({ name: 'turso-database', type: 'module', }) MainWorker = worker; return worker }); export default napiModule.exports export const Database = napiModule.exports.Database export const Opfs = napiModule.exports.Opfs export const OpfsFile = napiModule.exports.OpfsFile export const Statement = napiModule.exports.Statement export const initThreadPool = napiModule.exports.initThreadPool ================================================ FILE: bindings/javascript/packages/wasm/index-default.ts ================================================ import { setupMainThread } from "@tursodatabase/database-wasm-common"; const __wasmUrl = new URL('./turso.wasm32-wasi.wasm', import.meta.url).href; const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer()) export let MainWorker = null; const napiModule = await setupMainThread(__wasmFile, () => { const worker = new Worker(new URL('./worker.js', import.meta.url), { name: 'turso-database', type: 'module', }) MainWorker = worker; return worker }); export default napiModule.exports export const Database = napiModule.exports.Database export const Opfs = napiModule.exports.Opfs export const OpfsFile = napiModule.exports.OpfsFile export const Statement = napiModule.exports.Statement export const initThreadPool = napiModule.exports.initThreadPool ================================================ FILE: bindings/javascript/packages/wasm/index-turbopack-hack.ts ================================================ import { setupMainThread } from "@tursodatabase/database-wasm-common"; import { tursoWasm } from "./wasm-inline.js"; // Next (turbopack) has issues with loading wasm module: https://github.com/vercel/next.js/issues/82520 // So, we inline wasm binary in the source code in order to avoid issues with loading it from the file const __wasmFile = await tursoWasm(); export let MainWorker = null; const napiModule = await setupMainThread(__wasmFile, () => { const worker = new Worker(new URL('./worker.js', import.meta.url), { name: 'turso-database', type: 'module', }) MainWorker = worker; return worker }); export default napiModule.exports export const Database = napiModule.exports.Database export const Opfs = napiModule.exports.Opfs export const OpfsFile = napiModule.exports.OpfsFile export const Statement = napiModule.exports.Statement export const initThreadPool = napiModule.exports.initThreadPool ================================================ FILE: bindings/javascript/packages/wasm/index-vite-dev-hack.ts ================================================ import { isWebWorker, setupMainThread, setupWebWorker } from "@tursodatabase/database-wasm-common"; import { tursoWasm } from "./wasm-inline.js"; let napiModule = { exports: { Database: {} as any, Opfs: {} as any, OpfsFile: {} as any, Statement: {} as any, initThreadPool: {} as any, } }; export let MainWorker = null; if (isWebWorker()) { setupWebWorker(); } else { // Vite has issues with loading wasm modules and worker in dev server: https://github.com/vitejs/vite/issues/8427 // So, the mitigation for dev server only is: // 1. inline wasm binary in the source code in order to avoid issues with loading it from the file // 2. use same file as worker entry point const __wasmFile = await tursoWasm(); napiModule = await setupMainThread(__wasmFile, () => { const worker = new Worker(import.meta.url, { name: 'turso-database', type: 'module', }) MainWorker = worker; return worker }); } export default napiModule.exports export const Database = napiModule.exports.Database export const Opfs = napiModule.exports.Opfs export const OpfsFile = napiModule.exports.OpfsFile export const Statement = napiModule.exports.Statement export const initThreadPool = napiModule.exports.initThreadPool ================================================ FILE: bindings/javascript/packages/wasm/package.json ================================================ { "name": "@tursodatabase/database-wasm", "version": "0.6.0-pre.4", "repository": { "type": "git", "url": "https://github.com/tursodatabase/turso" }, "type": "module", "license": "MIT", "main": "./dist/promise-default.js", "packageManager": "yarn@4.9.2", "files": [ "dist/**", "bundle/**", "README.md" ], "exports": { ".": { "default": "./dist/promise-default.js" }, "./bundle": { "default": "./bundle/main.es.js" }, "./vite": { "development": "./dist/promise-vite-dev-hack.js", "default": "./dist/promise-default.js" }, "./turbopack": { "default": "./dist/promise-turbopack-hack.js" } }, "devDependencies": { "@napi-rs/cli": "^3.1.5", "@vitest/browser": "^3.2.4", "playwright": "^1.55.0", "typescript": "^5.9.2", "vite": "^7.1.5", "vitest": "^3.2.4" }, "scripts": { "napi-build": "napi build --features browser --profile release-official --platform --target wasm32-wasip1-threads --no-js --manifest-path ../../Cargo.toml --output-dir . && rm index.d.ts turso.wasi* wasi* browser.js", "tsc-build": "npm exec tsc && cp turso.wasm32-wasi.wasm ./dist/turso.wasm32-wasi.wasm && WASM_FILE=turso.wasm32-wasi.wasm JS_FILE=./dist/wasm-inline.js node ../../scripts/inline-wasm-base64.js && npm run bundle", "bundle": "vite build", "build": "npm run napi-build && npm run tsc-build", "test": "CI=1 vitest --browser=chromium --run && CI=1 vitest --browser=firefox --run" }, "napi": { "binaryName": "turso", "targets": [ "wasm32-wasip1-threads" ] }, "dependencies": { "@tursodatabase/database-common": "^0.6.0-pre.4", "@tursodatabase/database-wasm-common": "^0.6.0-pre.4" } } ================================================ FILE: bindings/javascript/packages/wasm/promise-bundle.ts ================================================ import { DatabasePromise, DatabaseOpts, SqliteError, } from "@tursodatabase/database-common" import { registerFileAtWorker, unregisterFileAtWorker, ioNotifier } from "@tursodatabase/database-wasm-common"; import { initThreadPool, MainWorker, Database as NativeDatabase } from "./index-bundle.js"; async function init(): Promise { await initThreadPool(); if (MainWorker == null) { throw new Error("panic: MainWorker is not initialized"); } return MainWorker; } class Database extends DatabasePromise { #worker: Worker | null; constructor(path: string, opts: DatabaseOpts = {}) { super( new NativeDatabase(path, opts) as unknown as any, () => ioNotifier.waitForCompletion(), ) } /** * connect database and pre-open necessary files in the OPFS */ override async connect() { if (!this.memory) { const worker = await init(); await Promise.all([ registerFileAtWorker(worker, this.name), registerFileAtWorker(worker, `${this.name}-wal`) ]); this.#worker = worker; } await super.connect(); } /** * close the database and relevant files */ async close() { if (this.name != null && this.#worker != null) { await Promise.all([ unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, `${this.name}-wal`) ]); } await super.close(); } } /** * Creates a new database connection asynchronously. * * @param {string} path - Path to the database file. * @param {Object} opts - Options for database behavior. * @returns {Promise} - A promise that resolves to a Database instance. */ async function connect(path: string, opts: DatabaseOpts = {}): Promise { const db = new Database(path, opts); await db.connect(); return db; } export { connect, Database, SqliteError } ================================================ FILE: bindings/javascript/packages/wasm/promise-default.ts ================================================ import { DatabasePromise, DatabaseOpts, SqliteError, } from "@tursodatabase/database-common" import { registerFileAtWorker, unregisterFileAtWorker, ioNotifier } from "@tursodatabase/database-wasm-common"; import { initThreadPool, MainWorker, Database as NativeDatabase } from "./index-default.js"; async function init(): Promise { await initThreadPool(); if (MainWorker == null) { throw new Error("panic: MainWorker is not initialized"); } return MainWorker; } class Database extends DatabasePromise { #worker: Worker | null; constructor(path: string, opts: DatabaseOpts = {}) { const nativeDb = new NativeDatabase(path, opts); super( nativeDb as unknown as any, // In-memory databases use MemoryIO which completes I/O synchronously, // so there's no OPFS Worker dispatch and the IONotifier would never fire. // Use undefined (defaults to no-op) so the step loop retries immediately. (nativeDb as any).memory ? undefined : () => ioNotifier.waitForCompletion(), ) } /** * connect database and pre-open necessary files in the OPFS */ override async connect() { if (!this.memory) { const worker = await init(); await Promise.all([ registerFileAtWorker(worker, this.name), registerFileAtWorker(worker, `${this.name}-wal`) ]); this.#worker = worker; } await super.connect(); } /** * close the database and relevant files */ async close() { if (this.name != null && this.#worker != null) { await Promise.all([ unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, `${this.name}-wal`) ]); } await super.close(); } } /** * Creates a new database connection asynchronously. * * @param {string} path - Path to the database file. * @param {Object} opts - Options for database behavior. * @returns {Promise} - A promise that resolves to a Database instance. */ async function connect(path: string, opts: DatabaseOpts = {}): Promise { const db = new Database(path, opts); await db.connect(); return db; } export { connect, Database, SqliteError } ================================================ FILE: bindings/javascript/packages/wasm/promise-turbopack-hack.ts ================================================ import { DatabasePromise, DatabaseOpts, SqliteError, } from "@tursodatabase/database-common" import { registerFileAtWorker, unregisterFileAtWorker, ioNotifier } from "@tursodatabase/database-wasm-common"; import { initThreadPool, MainWorker, Database as NativeDatabase } from "./index-turbopack-hack.js"; async function init(): Promise { await initThreadPool(); if (MainWorker == null) { throw new Error("panic: MainWorker is not initialized"); } return MainWorker; } class Database extends DatabasePromise { #worker: Worker | null; constructor(path: string, opts: DatabaseOpts = {}) { super( new NativeDatabase(path, opts) as unknown as any, () => ioNotifier.waitForCompletion(), ) } /** * connect database and pre-open necessary files in the OPFS */ override async connect() { if (!this.memory) { const worker = await init(); await Promise.all([ registerFileAtWorker(worker, this.name), registerFileAtWorker(worker, `${this.name}-wal`) ]); this.#worker = worker; } await super.connect(); } /** * close the database and relevant files */ async close() { if (this.name != null && this.#worker != null) { await Promise.all([ unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, `${this.name}-wal`) ]); } await super.close(); } } /** * Creates a new database connection asynchronously. * * @param {string} path - Path to the database file. * @param {Object} opts - Options for database behavior. * @returns {Promise} - A promise that resolves to a Database instance. */ async function connect(path: string, opts: DatabaseOpts = {}): Promise { const db = new Database(path, opts); await db.connect(); return db; } export { connect, Database, SqliteError } ================================================ FILE: bindings/javascript/packages/wasm/promise-vite-dev-hack.ts ================================================ import { DatabasePromise, DatabaseOpts, SqliteError, } from "@tursodatabase/database-common" import { registerFileAtWorker, unregisterFileAtWorker, ioNotifier } from "@tursodatabase/database-wasm-common"; import { initThreadPool, MainWorker, Database as NativeDatabase } from "./index-vite-dev-hack.js"; async function init(): Promise { await initThreadPool(); if (MainWorker == null) { throw new Error("panic: MainWorker is not initialized"); } return MainWorker; } class Database extends DatabasePromise { #worker: Worker | null; constructor(path: string, opts: DatabaseOpts = {}) { super( new NativeDatabase(path, opts) as unknown as any, () => ioNotifier.waitForCompletion(), ) } /** * connect database and pre-open necessary files in the OPFS */ override async connect() { if (!this.memory) { const worker = await init(); await Promise.all([ registerFileAtWorker(worker, this.name), registerFileAtWorker(worker, `${this.name}-wal`) ]); this.#worker = worker; } await super.connect(); } /** * close the database and relevant files */ async close() { if (this.name != null && this.#worker != null) { await Promise.all([ unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, `${this.name}-wal`) ]); } await super.close(); } } /** * Creates a new database connection asynchronously. * * @param {string} path - Path to the database file. * @param {Object} opts - Options for database behavior. * @returns {Promise} - A promise that resolves to a Database instance. */ async function connect(path: string, opts: DatabaseOpts = {}): Promise { const db = new Database(path, opts); await db.connect(); return db; } export { connect, Database, SqliteError } ================================================ FILE: bindings/javascript/packages/wasm/promise.test.ts ================================================ import { expect, test, afterAll, beforeEach, afterEach } from 'vitest' import { connect, Database } from './promise-default.js' import { MainWorker } from './index-default.js' beforeEach((ctx) => { console.log(`[test:start] ${ctx.task.name}`); }) afterEach((ctx) => { console.log(`[test:end] ${ctx.task.name} (${ctx.task.result?.state})`); }) afterAll(() => { console.log('[afterAll] terminating MainWorker'); MainWorker?.terminate(); console.log('[afterAll] MainWorker terminated'); }) test('vector-test', async () => { const db = await connect(":memory:"); const v1 = new Array(1024).fill(0).map((_, i) => i); const v2 = new Array(1024).fill(0).map((_, i) => 1024 - i); const result = await db.prepare(`SELECT vector_distance_cos(vector32('${JSON.stringify(v1)}'), vector32('${JSON.stringify(v2)}')) as cosf32, vector_distance_cos(vector64('${JSON.stringify(v1)}'), vector64('${JSON.stringify(v2)}')) as cosf64, vector_distance_l2(vector32('${JSON.stringify(v1)}'), vector32('${JSON.stringify(v2)}')) as l2f32, vector_distance_l2(vector64('${JSON.stringify(v1)}'), vector64('${JSON.stringify(v2)}')) as l2f64 `).all(); console.info(result); await db.close(); }) test('explain', async () => { const db = await connect(":memory:"); const stmt = db.prepare("EXPLAIN SELECT 1"); expect(stmt.columns()).toEqual([ { "name": "addr", "type": "INTEGER", }, { "name": "opcode", "type": "TEXT", }, { "name": "p1", "type": "INTEGER", }, { "name": "p2", "type": "INTEGER", }, { "name": "p3", "type": "INTEGER", }, { "name": "p4", "type": "TEXT", }, { "name": "p5", "type": "INTEGER", }, { "name": "comment", "type": "TEXT", }, ].map(x => ({ ...x, column: null, database: null, table: null }))); expect(await stmt.all()).toEqual([ { "addr": 0, "comment": "Start at 3", "opcode": "Init", "p1": 0, "p2": 3, "p3": 0, "p4": "", "p5": 0, }, { "addr": 1, "comment": "output=r[1]", "opcode": "ResultRow", "p1": 1, "p2": 1, "p3": 0, "p4": "", "p5": 0, }, { "addr": 2, "comment": "", "opcode": "Halt", "p1": 0, "p2": 0, "p3": 0, "p4": "", "p5": 0, }, { "addr": 3, "comment": "r[1]=1", "opcode": "Integer", "p1": 1, "p2": 1, "p3": 0, "p4": "", "p5": 0, }, { "addr": 4, "comment": "", "opcode": "Goto", "p1": 0, "p2": 1, "p3": 0, "p4": "", "p5": 0, }, ]); await db.close(); }) test('in-memory db', async () => { const db = await connect(":memory:"); await db.exec("CREATE TABLE t(x)"); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); const stmt = db.prepare("SELECT * FROM t WHERE x % 2 = ?"); const rows = await stmt.all([1]); expect(rows).toEqual([{ x: 1 }, { x: 3 }]); await db.close(); }) test('implicit connect', async () => { const db = new Database(':memory:'); const defer = db.prepare("SELECT * FROM t"); await expect(async () => await defer.all()).rejects.toThrowError(/no such table: t/); expect(() => db.prepare("SELECT * FROM t")).toThrowError(/no such table: t/); expect(await db.prepare("SELECT 1 as x").all()).toEqual([{ x: 1 }]); await db.close(); }) test('on-disk db large inserts', async () => { const path = `test-${(Math.random() * 10000) | 0}.db`; const db1 = await connect(path); await db1.prepare("CREATE TABLE t(x)").run(); await db1.prepare("INSERT INTO t VALUES (randomblob(10 * 4096 + 0))").run(); await db1.prepare("INSERT INTO t VALUES (randomblob(10 * 4096 + 1))").run(); await db1.prepare("INSERT INTO t VALUES (randomblob(10 * 4096 + 2))").run(); const stmt1 = db1.prepare("SELECT length(x) as l FROM t"); expect(stmt1.columns()).toEqual([{ name: "l", column: null, database: null, table: null, type: null }]); const rows1 = await stmt1.all(); expect(rows1).toEqual([{ l: 10 * 4096 }, { l: 10 * 4096 + 1 }, { l: 10 * 4096 + 2 }]); await db1.exec("BEGIN"); await db1.exec("INSERT INTO t VALUES (1)"); await db1.exec("ROLLBACK"); const rows2 = await db1.prepare("SELECT length(x) as l FROM t").all(); expect(rows2).toEqual([{ l: 10 * 4096 }, { l: 10 * 4096 + 1 }, { l: 10 * 4096 + 2 }]); await db1.prepare("PRAGMA wal_checkpoint(TRUNCATE)").run(); await db1.close(); }) test('on-disk db', async () => { const path = `test-${(Math.random() * 10000) | 0}.db`; const db1 = await connect(path); await db1.exec("CREATE TABLE t(x)"); await db1.exec("INSERT INTO t VALUES (1), (2), (3)"); const stmt1 = db1.prepare("SELECT * FROM t WHERE x % 2 = ?"); expect(stmt1.columns()).toEqual([{ name: "x", column: null, database: null, table: null, type: null }]); const rows1 = await stmt1.all([1]); expect(rows1).toEqual([{ x: 1 }, { x: 3 }]); stmt1.close(); await db1.close(); const db2 = await connect(path); const stmt2 = db2.prepare("SELECT * FROM t WHERE x % 2 = ?"); expect(stmt2.columns()).toEqual([{ name: "x", column: null, database: null, table: null, type: null }]); const rows2 = await stmt2.all([1]); expect(rows2).toEqual([{ x: 1 }, { x: 3 }]); await db2.close(); }) // attach is not supported in browser for now // test('attach', async () => { // const path1 = `test-${(Math.random() * 10000) | 0}.db`; // const path2 = `test-${(Math.random() * 10000) | 0}.db`; // const db1 = await connect(path1); // await db1.exec("CREATE TABLE t(x)"); // await db1.exec("INSERT INTO t VALUES (1), (2), (3)"); // const db2 = await connect(path2); // await db2.exec("CREATE TABLE q(x)"); // await db2.exec("INSERT INTO q VALUES (4), (5), (6)"); // await db1.exec(`ATTACH '${path2}' as secondary`); // const stmt = db1.prepare("SELECT * FROM t UNION ALL SELECT * FROM secondary.q"); // expect(stmt.columns()).toEqual([{ name: "x", column: null, database: null, table: null, type: null }]); // const rows = await stmt.all([1]); // expect(rows).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }, { x: 5 }, { x: 6 }]); // }) test('blobs', async () => { const db = await connect(":memory:"); const rows = await db.prepare("SELECT x'1020' as x").all(); expect(rows).toEqual([{ x: new Uint8Array([16, 32]) }]) await db.close(); }) test('example-1', async () => { const db = await connect(':memory:'); await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)'); const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); await insert.run('Alice', 'alice@example.com'); await insert.run('Bob', 'bob@example.com'); const users = await db.prepare('SELECT * FROM users').all(); expect(users).toEqual([ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' } ]); await db.close(); }) test('example-2', async () => { const db = await connect(':memory:'); await db.exec('CREATE TABLE users (name, email)'); // Using transactions for atomic operations const transaction = db.transaction(async (users) => { const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); for (const user of users) { await insert.run(user.name, user.email); } }); // Execute transaction await transaction([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' } ]); const rows = await db.prepare('SELECT * FROM users').all(); expect(rows).toEqual([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' } ]); await db.close(); }) test('sorter-wasm', async () => { const db = await connect(':memory:'); await db.exec('CREATE TABLE t (k, v)'); for (let i = 0; i < 1024; i++) { await db.exec(`INSERT INTO t VALUES (${i}, randomblob(10 * 1024))`); } expect(await db.prepare("SELECT length(v) as l FROM (SELECT v FROM t ORDER BY k)").all()).toEqual(new Array(1024).fill({ l: 10 * 1024 })); await db.close(); }) test('hash-join-wasm', { timeout: 60_000 }, async () => { const db = await connect(':memory:'); await db.exec('CREATE TABLE a (k, v)'); await db.exec('CREATE TABLE b (k, v)'); for (let i = 0; i < 1024; i++) { await db.exec(`INSERT INTO a VALUES (${i}, randomblob(100 * 1024))`); } for (let i = 0; i < 1024; i++) { await db.exec(`INSERT INTO b VALUES (${i}, randomblob(100 * 1024))`); } expect(await db.prepare("SELECT length(a) as a, length(b) as b FROM (SELECT a.v as a, b.v as b FROM a INNER JOIN b ON a.k = b.k)").all()).toEqual(new Array(1024).fill({ a: 100 * 1024, b: 100 * 1024 })); await db.close(); }) ================================================ FILE: bindings/javascript/packages/wasm/tsconfig.json ================================================ { "compilerOptions": { "skipLibCheck": true, "declaration": true, "declarationMap": true, "module": "nodenext", "target": "esnext", "moduleResolution": "nodenext", "outDir": "dist/", "lib": [ "es2020", "DOM", "WebWorker" ] }, "include": [ "*" ] } ================================================ FILE: bindings/javascript/packages/wasm/vite ================================================ ================================================ FILE: bindings/javascript/packages/wasm/vite.config.js ================================================ import { resolve } from 'path'; import { defineConfig } from 'vite'; export default defineConfig({ base: './', build: { lib: { entry: resolve(__dirname, 'promise-bundle.ts'), name: 'database-wasm', fileName: format => `main.${format}.js`, formats: ['es'], }, rollupOptions: { output: { dir: 'bundle', } }, }, }); ================================================ FILE: bindings/javascript/packages/wasm/vitest.config.ts ================================================ import { defineConfig } from 'vitest/config' export default defineConfig({ define: { 'process.env.NODE_DEBUG_NATIVE': 'false', }, server: { headers: { "Cross-Origin-Embedder-Policy": "require-corp", "Cross-Origin-Opener-Policy": "same-origin" }, }, test: { testTimeout: 120_000, browser: { enabled: true, provider: 'playwright', instances: [ { browser: 'chromium' }, { browser: 'firefox' } ], }, }, }) ================================================ FILE: bindings/javascript/packages/wasm/wasm-inline.ts ================================================ const tursoWasmBase64 = '__PLACEHOLDER__'; async function convertBase64ToBinary(base64Url: string): Promise { const blob = await fetch(base64Url).then(res => res.blob()); return await blob.arrayBuffer(); } export async function tursoWasm(): Promise { return await convertBase64ToBinary(tursoWasmBase64); } ================================================ FILE: bindings/javascript/packages/wasm/worker.ts ================================================ import { setupWebWorker } from "@tursodatabase/database-wasm-common"; setupWebWorker(); ================================================ FILE: bindings/javascript/packages/wasm-common/README.md ================================================ ## About This package is the Turso embedded database common JS library which is shared between final builds for Node and Browser. Do not use this package directly - instead you must use `@tursodatabase/database` or `@tursodatabase/database-wasm`. > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ================================================ FILE: bindings/javascript/packages/wasm-common/index.ts ================================================ import { createOnMessage as __wasmCreateOnMessageForFsProxy, getDefaultContext as __emnapiGetDefaultContext, instantiateNapiModule as __emnapiInstantiateNapiModule, WASI as __WASI, instantiateNapiModuleSync, MessageHandler } from '@napi-rs/wasm-runtime' function getUint8ArrayFromMemory(memory: WebAssembly.Memory, ptr: number, len: number): Uint8Array { ptr = ptr >>> 0; return new Uint8Array(memory.buffer).subarray(ptr, ptr + len); } function getStringFromMemory(memory: WebAssembly.Memory, ptr: number, len: number): string { const shared = getUint8ArrayFromMemory(memory, ptr, len); const copy = new Uint8Array(shared.length); copy.set(shared); const decoder = new TextDecoder('utf-8'); return decoder.decode(copy); } interface BrowserImports { is_web_worker(): boolean; lookup_file(ptr: number, len: number): number; read(handle: number, ptr: number, len: number, offset: number): number; read_async(handle: number, ptr: number, len: number, offset: number, c: number); write(handle: number, ptr: number, len: number, offset: number): number; write_async(handle: number, ptr: number, len: number, offset: number, c: number); sync(handle: number): number; sync_async(handle: number, c: number); truncate(handle: number, len: number): number; truncate_async(handle: number, len: number, c: number); size(handle: number): number; } function panicMain(name): never { throw new Error(`method ${name} must be invoked only from the worker thread`); } function panicWorker(name): never { throw new Error(`method ${name} must be invoked only from the main thread`); } function mainImports(worker: Worker, completeOpfs: (c: any, r: any) => void): BrowserImports { return { is_web_worker(): boolean { return false; }, write_async(handle, ptr, len, offset, c) { writeFileAtWorker(worker, handle, ptr, len, offset) .then(result => { completeOpfs(c, result); }, err => { console.error('write_async', err); completeOpfs(c, -1); }); }, sync_async(handle, c) { syncFileAtWorker(worker, handle) .then(result => { completeOpfs(c, result); }, err => { console.error('sync_async', err); completeOpfs(c, -1); }); }, read_async(handle, ptr, len, offset, c) { readFileAtWorker(worker, handle, ptr, len, offset) .then(result => { completeOpfs(c, result); }, err => { console.error('read_async', err); completeOpfs(c, -1); }); }, truncate_async(handle, len, c) { truncateFileAtWorker(worker, handle, len) .then(result => { completeOpfs(c, result); }, err => { console.error('truncate_async', err); completeOpfs(c, -1); }); }, lookup_file(ptr, len): number { panicMain("lookup_file") }, read(handle, ptr, len, offset): number { panicMain("read") }, write(handle, ptr, len, offset): number { panicMain("write") }, sync(handle): number { panicMain("sync") }, truncate(handle, len): number { panicMain("truncate") }, size(handle): number { panicMain("size") } }; }; function workerImports(opfs: OpfsDirectory, memory: WebAssembly.Memory): BrowserImports { return { is_web_worker(): boolean { return true; }, lookup_file(ptr, len): number { try { const handle = opfs.lookupFileHandle(getStringFromMemory(memory, ptr, len)); return handle == null ? -404 : handle; } catch (e) { return -1; } }, read(handle, ptr, len, offset): number { try { return opfs.read(handle, getUint8ArrayFromMemory(memory, ptr, len), offset); } catch (e) { return -1; } }, write(handle, ptr, len, offset): number { try { return opfs.write(handle, getUint8ArrayFromMemory(memory, ptr, len), offset) } catch (e) { return -1; } }, sync(handle): number { try { return opfs.sync(handle); } catch (e) { return -1; } }, truncate(handle, len): number { try { opfs.truncate(handle, len); return 0; } catch (e) { return -1; } }, size(handle): number { try { return opfs.size(handle); } catch (e) { return -1; } }, read_async(handle, ptr, len, offset, completion) { panicWorker("read_async") }, write_async(handle, ptr, len, offset, completion) { panicWorker("write_async") }, sync_async(handle, completion) { panicWorker("sync_async") }, truncate_async(handle, len, c) { panicWorker("truncate_async") }, } } class OpfsDirectory { fileByPath: Map; fileByHandle: Map; fileHandleNo: number; constructor() { this.fileByPath = new Map(); this.fileByHandle = new Map(); this.fileHandleNo = 0; } async registerFile(path: string) { if (this.fileByPath.has(path)) { return; } const opfsRoot = await navigator.storage.getDirectory(); const opfsHandle = await opfsRoot.getFileHandle(path, { create: true }); const opfsSync = await opfsHandle.createSyncAccessHandle(); this.fileHandleNo += 1; this.fileByPath.set(path, { handle: this.fileHandleNo, sync: opfsSync }); this.fileByHandle.set(this.fileHandleNo, opfsSync); } async unregisterFile(path: string) { const file = this.fileByPath.get(path); if (file == null) { return; } this.fileByPath.delete(path); this.fileByHandle.delete(file.handle); file.sync.close(); } lookupFileHandle(path: string): number | null { try { const file = this.fileByPath.get(path); if (file == null) { return null; } return file.handle; } catch (e) { console.error('lookupFile', path, e); throw e; } } read(handle: number, buffer: Uint8Array, offset: number): number { try { const file = this.fileByHandle.get(handle); const result = file.read(buffer, { at: Number(offset) }); return result; } catch (e) { console.error('read', handle, buffer.length, offset, e); throw e; } } write(handle: number, buffer: Uint8Array, offset: number): number { try { const file = this.fileByHandle.get(handle); const result = file.write(buffer, { at: Number(offset) }); return result; } catch (e) { console.error('write', handle, buffer.length, offset, e); throw e; } } sync(handle: number): number { try { const file = this.fileByHandle.get(handle); file.flush(); return 0; } catch (e) { console.error('sync', handle, e); throw e; } } truncate(handle: number, size: number) { try { const file = this.fileByHandle.get(handle); file.truncate(size); return 0; } catch (e) { console.error('truncate', handle, size, e); throw e; } } size(handle: number): number { try { const file = this.fileByHandle.get(handle); const size = file.getSize() return size; } catch (e) { console.error('size', handle, e); throw e; } } } let workerRequestId = 0; function waitForWorkerResponse(worker: Worker, id: number, timeoutMs: number = 30_000): Promise { let waitResolve, waitReject; let settled = false; const callback = msg => { if (msg.data.__turso__ && msg.data.id == id) { settled = true; clearTimeout(stallTimer); if (msg.data.error != null) { waitReject(msg.data.error) } else { waitResolve(msg.data.result) } cleanup(); } }; const cleanup = () => worker.removeEventListener("message", callback); const stallTimer = setTimeout(() => { if (!settled) { settled = true; cleanup(); waitReject(new Error(`[turso:worker] request id=${id} timed out after ${timeoutMs}ms`)); } }, timeoutMs); worker.addEventListener("message", callback); const result = new Promise((resolve, reject) => { waitResolve = resolve; waitReject = reject; }); return result; } function readFileAtWorker(worker: Worker, handle: number, ptr: number, len: number, offset: number) { workerRequestId += 1; const currentId = workerRequestId; const promise = waitForWorkerResponse(worker, currentId); worker.postMessage({ __turso__: "read_async", handle: handle, ptr: ptr, len: len, offset: offset, id: currentId }); return promise; } function writeFileAtWorker(worker: Worker, handle: number, ptr: number, len: number, offset: number) { workerRequestId += 1; const currentId = workerRequestId; const promise = waitForWorkerResponse(worker, currentId); worker.postMessage({ __turso__: "write_async", handle: handle, ptr: ptr, len: len, offset: offset, id: currentId }); return promise; } function syncFileAtWorker(worker: Worker, handle: number) { workerRequestId += 1; const currentId = workerRequestId; const promise = waitForWorkerResponse(worker, currentId); worker.postMessage({ __turso__: "sync_async", handle: handle, id: currentId }); return promise; } function truncateFileAtWorker(worker: Worker, handle: number, len: number) { workerRequestId += 1; const currentId = workerRequestId; const promise = waitForWorkerResponse(worker, currentId); worker.postMessage({ __turso__: "truncate_async", handle: handle, len: len, id: currentId }); return promise; } function registerFileAtWorker(worker: Worker, path: string): Promise { workerRequestId += 1; const currentId = workerRequestId; const promise = waitForWorkerResponse(worker, currentId); worker.postMessage({ __turso__: "register", path: path, id: currentId }); return promise; } function unregisterFileAtWorker(worker: Worker, path: string): Promise { workerRequestId += 1; const currentId = workerRequestId; const promise = waitForWorkerResponse(worker, currentId); worker.postMessage({ __turso__: "unregister", path: path, id: currentId }); return promise; } function isWebWorker(): boolean { return typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope; } function setupWebWorker() { let opfs = new OpfsDirectory(); let memory = null; const handler = new MessageHandler({ onLoad({ wasmModule, wasmMemory }) { memory = wasmMemory; const wasi = new __WASI({ print: function () { // eslint-disable-next-line no-console console.log.apply(console, arguments) }, printErr: function () { // eslint-disable-next-line no-console console.error.apply(console, arguments) }, }) return instantiateNapiModuleSync(wasmModule, { childThread: true, wasi, overwriteImports(importObject) { importObject.env = { ...importObject.env, ...importObject.napi, ...importObject.emnapi, ...workerImports(opfs, memory), memory: wasmMemory, } }, }) }, }) globalThis.onmessage = async function (e) { if (e.data.__turso__ == 'register') { try { await opfs.registerFile(e.data.path); self.postMessage({ __turso__: true, id: e.data.id }); } catch (error) { self.postMessage({ __turso__: true, id: e.data.id, error: error }); } return; } else if (e.data.__turso__ == 'unregister') { try { await opfs.unregisterFile(e.data.path); self.postMessage({ __turso__: true, id: e.data.id }); } catch (error) { self.postMessage({ __turso__: true, id: e.data.id, error: error }); } return; } else if (e.data.__turso__ == 'read_async') { let result = opfs.read(e.data.handle, getUint8ArrayFromMemory(memory, e.data.ptr, e.data.len), e.data.offset); self.postMessage({ __turso__: true, id: e.data.id, result: result }); } else if (e.data.__turso__ == 'write_async') { let result = opfs.write(e.data.handle, getUint8ArrayFromMemory(memory, e.data.ptr, e.data.len), e.data.offset); self.postMessage({ __turso__: true, id: e.data.id, result: result }); } else if (e.data.__turso__ == 'sync_async') { let result = opfs.sync(e.data.handle); self.postMessage({ __turso__: true, id: e.data.id, result: result }); } else if (e.data.__turso__ == 'truncate_async') { let result = opfs.truncate(e.data.handle, e.data.len); self.postMessage({ __turso__: true, id: e.data.id, result: result }); } handler.handle(e) } } async function setupMainThread(wasmFile: ArrayBuffer, factory: () => Worker): Promise { const worker = factory(); let completeOpfs = null; const __emnapiContext = __emnapiGetDefaultContext() const __wasi = new __WASI({ version: 'preview1', }) const __sharedMemory = new WebAssembly.Memory({ initial: 4000, maximum: 65536, shared: true, }) const { instance: __napiInstance, module: __wasiModule, napiModule: __napiModule, } = await __emnapiInstantiateNapiModule(wasmFile, { context: __emnapiContext, asyncWorkPoolSize: 1, wasi: __wasi, onCreateWorker() { return worker; }, overwriteImports(importObject) { importObject.env = { ...importObject.env, ...importObject.napi, ...importObject.emnapi, ...mainImports(worker, (c, res) => completeOpfs(c, res)), memory: __sharedMemory, } return importObject }, beforeInit({ instance }) { for (const name of Object.keys(instance.exports)) { if (name.startsWith('__napi_register__')) { instance.exports[name]() } } }, }); const nativeCompleteOpfs = __napiModule.exports.completeOpfs; completeOpfs = (c, res) => { nativeCompleteOpfs(c, res); ioNotifier.notify(); }; return __napiModule; } class IONotifier { private waiters: Array<() => void> = []; waitForCompletion(): Promise { return new Promise(resolve => { this.waiters.push(resolve); }); } notify() { const waiters = this.waiters; this.waiters = []; for (const resolve of waiters) { resolve(); } } } const ioNotifier = new IONotifier(); export { OpfsDirectory, workerImports, mainImports as MainDummyImports, waitForWorkerResponse, registerFileAtWorker, unregisterFileAtWorker, isWebWorker, setupWebWorker, setupMainThread, ioNotifier } ================================================ FILE: bindings/javascript/packages/wasm-common/package.json ================================================ { "name": "@tursodatabase/database-wasm-common", "version": "0.6.0-pre.4", "repository": { "type": "git", "url": "https://github.com/tursodatabase/turso" }, "type": "module", "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } }, "packageManager": "yarn@4.9.2", "files": [ "dist/**", "README.md" ], "devDependencies": { "typescript": "^5.9.2" }, "scripts": { "tsc-build": "npm exec tsc", "build": "npm run tsc-build", "test": "echo 'no tests'" }, "dependencies": { "@napi-rs/wasm-runtime": "^1.0.5" } } ================================================ FILE: bindings/javascript/packages/wasm-common/tsconfig.json ================================================ { "compilerOptions": { "skipLibCheck": true, "declaration": true, "declarationMap": true, "module": "nodenext", "target": "esnext", "moduleResolution": "nodenext", "outDir": "dist/", "lib": [ "es2020", "DOM", "WebWorker" ], }, "include": [ "*" ] } ================================================ FILE: bindings/javascript/perf/package.json ================================================ { "name": "turso-perf", "type": "module", "private": true, "type": "module", "dependencies": { "better-sqlite3": "^9.5.0", "@tursodatabase/database": "../packages/native", "mitata": "^0.1.11" } } ================================================ FILE: bindings/javascript/perf/perf-better-sqlite3.js ================================================ import { run, bench, group, baseline } from 'mitata'; import Database from 'better-sqlite3'; const db = new Database(':memory:'); db.exec("CREATE TABLE users (id INTEGER, name TEXT, email TEXT)"); db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"); const stmtSelect = db.prepare("SELECT * FROM users WHERE id = ?"); const rawStmtSelect = db.prepare("SELECT * FROM users WHERE id = ?").raw(); const stmtInsert = db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)"); bench('Statement.get() with bind parameters [expanded]', () => { stmtSelect.get(1); }); bench('Statement.git() with bind parameters [raw]', () => { rawStmtSelect.get(1); }); bench('Statement.run() with bind parameters', () => { stmtInsert.run([1, 'foobar', 'foobar@example.com']); }); await run({ units: false, silent: false, avg: true, json: false, colors: true, min_max: true, percentiles: true, }); ================================================ FILE: bindings/javascript/perf/perf-turso.js ================================================ import { run, bench, group, baseline } from 'mitata'; import { Database } from '@tursodatabase/database'; const db = new Database(':memory:'); await db.exec("CREATE TABLE users (id INTEGER, name TEXT, email TEXT)"); await db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"); const stmtSelect = db.prepare("SELECT * FROM users WHERE id = ?"); const rawStmtSelect = db.prepare("SELECT * FROM users WHERE id = ?").raw(); const stmtInsert = db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)"); bench('Statement.get() with bind parameters [expanded]', async () => { await stmtSelect.get(1); }); bench('Statement.get() with bind parameters [raw]', async () => { await rawStmtSelect.get(1); }); bench('Statement.run() with bind parameters', async () => { await stmtInsert.run([1, 'foobar', 'foobar@example.com']); }); await run({ units: false, silent: false, avg: true, json: false, colors: true, min_max: true, percentiles: true, }); ================================================ FILE: bindings/javascript/replace.sh ================================================ sed -i "s/$NAME_FROM/$NAME_TO/g" packages/common/package.json sed -i "s/$NAME_FROM/$NAME_TO/g" packages/native/package.json sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/package.json sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser-common/package.json sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/common/package.json sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/native/package.json sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/browser/package.json sed -i "s/$VERSION_FROM/$VERSION_TO/g" packages/common/package.json sed -i "s/$VERSION_FROM/$VERSION_TO/g" packages/native/package.json sed -i "s/$VERSION_FROM/$VERSION_TO/g" packages/browser/package.json sed -i "s/$VERSION_FROM/$VERSION_TO/g" packages/browser-common/package.json sed -i "s/$VERSION_FROM/$VERSION_TO/g" sync/packages/common/package.json sed -i "s/$VERSION_FROM/$VERSION_TO/g" sync/packages/native/package.json sed -i "s/$VERSION_FROM/$VERSION_TO/g" sync/packages/browser/package.json sed -i "s/$NAME_FROM/$NAME_TO/g" packages/native/promise.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/native/compat.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser-common/index.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/promise.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/promise-bundle.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/promise-default.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/promise-vite-dev-hack.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/promise-turbopack-hack.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/index-default.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/index-bundle.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/index-vite-dev-hack.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/index-turbopack-hack.ts sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/worker.ts sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/native/promise.ts sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/browser/promise.ts sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/browser/promise-bundle.ts sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/browser/promise-default.ts sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/browser/promise-vite-dev-hack.ts sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/browser/promise-turbopack-hack.ts sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/browser/index-default.ts sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/browser/index-bundle.ts sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/browser/index-vite-dev-hack.ts sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/browser/index-turbopack-hack.ts sed -i "s/$NAME_FROM/$NAME_TO/g" sync/packages/browser/worker.ts ================================================ FILE: bindings/javascript/scripts/inline-wasm-base64.js ================================================ import { readFileSync, writeFileSync } from "node:fs"; const data = readFileSync(process.env.WASM_FILE); const b64 = data.toString("base64"); const dataUrl = "data:application/wasm;base64," + b64; const inlined = readFileSync(process.env.JS_FILE).toString("utf8"); const replaced = inlined.replace("__PLACEHOLDER__", dataUrl); writeFileSync(process.env.JS_FILE, replaced); ================================================ FILE: bindings/javascript/src/browser.rs ================================================ use std::{cell::RefCell, collections::HashMap, sync::Arc}; use napi::bindgen_prelude::*; use napi_derive::napi; use turso_core::{Clock, Completion, File, MonotonicInstant, WallClockInstant, IO}; pub struct NoopTask; impl Task for NoopTask { type Output = (); type JsValue = (); fn compute(&mut self) -> Result { Ok(()) } fn resolve(&mut self, _: Env, _: Self::Output) -> Result { Ok(()) } } #[napi] /// turso-db in the the browser requires explicit thread pool initialization /// so, we just put no-op task on the thread pool and force emnapi to allocate web worker pub fn init_thread_pool() -> napi::Result> { Ok(AsyncTask::new(NoopTask)) } #[napi] #[derive(Clone)] pub struct Opfs { inner: Arc, } pub struct OpfsInner { completion_no: RefCell, completions: RefCell>, } thread_local! { static OPFS: Arc = Arc::new(Opfs::default()); } #[napi] #[derive(Clone)] struct OpfsFile { handle: i32, opfs: Opfs, } unsafe impl Send for Opfs {} unsafe impl Sync for Opfs {} #[napi] pub fn complete_opfs(completion_no: u32, result: i32) { OPFS.with(|opfs| opfs.complete(completion_no, result)) } pub fn opfs() -> Arc { OPFS.with(|opfs| opfs.clone()) } impl Opfs { pub fn complete(&self, completion_no: u32, result: i32) { let completion = { let mut completions = self.inner.completions.borrow_mut(); completions.remove(&completion_no).unwrap() }; completion.complete(result); } fn register_completion(&self, c: Completion) -> u32 { let inner = &self.inner; *inner.completion_no.borrow_mut() += 1; let completion_no = *inner.completion_no.borrow(); tracing::debug!( "register completion: {} {:?}", completion_no, Arc::as_ptr(inner) ); inner.completions.borrow_mut().insert(completion_no, c); completion_no } } impl Clock for Opfs { fn current_time_monotonic(&self) -> MonotonicInstant { MonotonicInstant::now() } fn current_time_wall_clock(&self) -> WallClockInstant { WallClockInstant::now() } } impl Default for Opfs { fn default() -> Self { Self { #[allow(clippy::arc_with_non_send_sync)] inner: Arc::new(OpfsInner { completion_no: RefCell::new(0), completions: RefCell::new(HashMap::new()), }), } } } #[link(wasm_import_module = "env")] extern "C" { fn lookup_file(path: *const u8, path_len: usize) -> i32; fn read(handle: i32, buffer: *mut u8, buffer_len: usize, offset: i32) -> i32; fn write(handle: i32, buffer: *const u8, buffer_len: usize, offset: i32) -> i32; fn sync(handle: i32) -> i32; fn truncate(handle: i32, length: usize) -> i32; fn size(handle: i32) -> i32; fn write_async(handle: i32, buffer: *const u8, buffer_len: usize, offset: i32, c: u32); fn sync_async(handle: i32, c: u32); fn read_async(handle: i32, buffer: *mut u8, buffer_len: usize, offset: i32, c: u32); fn truncate_async(handle: i32, length: usize, c: u32); // fn size_async(handle: i32) -> i32; fn is_web_worker() -> bool; } fn is_web_worker_safe() -> bool { unsafe { is_web_worker() } } impl IO for Opfs { fn open_file( &self, path: &str, _: turso_core::OpenFlags, _: bool, ) -> turso_core::Result> { tracing::info!("open_file: {}", path); let result = unsafe { lookup_file(path.as_ptr(), path.len()) }; if result >= 0 { Ok(Arc::new(OpfsFile { handle: result, opfs: Opfs { inner: self.inner.clone(), }, })) } else if result == -404 { Err(turso_core::LimboError::InternalError(format!( "unexpected path {path}: files must be created in advance for OPFS IO" ))) } else { Err(turso_core::LimboError::InternalError(format!( "unexpected file lookup error: {result}" ))) } } fn remove_file(&self, _: &str) -> turso_core::Result<()> { Ok(()) } } impl File for OpfsFile { fn lock_file(&self, _: bool) -> turso_core::Result<()> { Ok(()) } fn unlock_file(&self) -> turso_core::Result<()> { Ok(()) } fn pread( &self, pos: u64, c: turso_core::Completion, ) -> turso_core::Result { let web_worker = is_web_worker_safe(); tracing::debug!( "pread({}, is_web_worker={}): pos={}", self.handle, web_worker, pos ); let handle = self.handle; let read_c = c.as_read(); let buffer = read_c.buf_arc(); let buffer = buffer.as_mut_slice(); if web_worker { let result = unsafe { read(handle, buffer.as_mut_ptr(), buffer.len(), pos as i32) }; c.complete(result as i32); } else { let completion_no = self.opfs.register_completion(c.clone()); unsafe { read_async( handle, buffer.as_mut_ptr(), buffer.len(), pos as i32, completion_no, ) }; } Ok(c) } fn pwrite( &self, pos: u64, buffer: Arc, c: turso_core::Completion, ) -> turso_core::Result { let web_worker = is_web_worker_safe(); tracing::debug!( "pwrite({}, is_web_worker={}): pos={}", self.handle, web_worker, pos ); let handle = self.handle; // Keep the buffer alive until the async write completes — write_async // passes a raw pointer to JavaScript which may fire the callback later. c.keep_write_buffer_alive(buffer.clone()); let buffer = buffer.as_slice(); if web_worker { let result = unsafe { write(handle, buffer.as_ptr(), buffer.len(), pos as i32) }; c.complete(result as i32); } else { let completion_no = self.opfs.register_completion(c.clone()); unsafe { write_async( handle, buffer.as_ptr(), buffer.len(), pos as i32, completion_no, ) }; } Ok(c) } fn sync( &self, c: turso_core::Completion, _sync_type: turso_core::io::FileSyncType, ) -> turso_core::Result { let web_worker = is_web_worker_safe(); tracing::debug!("sync({}, is_web_worker={})", self.handle, web_worker); let handle = self.handle; if web_worker { let result = unsafe { sync(handle) }; c.complete(result as i32); } else { let completion_no = self.opfs.register_completion(c.clone()); unsafe { sync_async(handle, completion_no) }; } Ok(c) } fn truncate( &self, len: u64, c: turso_core::Completion, ) -> turso_core::Result { let web_worker = is_web_worker_safe(); tracing::debug!( "truncate({}, is_web_worker={}): len={}", self.handle, web_worker, len ); let handle = self.handle; if web_worker { let result = unsafe { truncate(handle, len as usize) }; c.complete(result as i32); } else { let completion_no = self.opfs.register_completion(c.clone()); unsafe { truncate_async(handle, len as usize, completion_no) }; } Ok(c) } fn size(&self) -> turso_core::Result { if !is_web_worker_safe() { return Err(turso_core::LimboError::InternalError( "size can be called only from web worker context".to_string(), )); } tracing::debug!("size({})", self.handle); let handle = self.handle; let result = unsafe { size(handle) }; Ok(result as u64) } } ================================================ FILE: bindings/javascript/src/lib.rs ================================================ //! JavaScript bindings for the Turso library. //! //! These bindings provide a thin layer that exposes Turso's Rust API to JavaScript, //! maintaining close alignment with the underlying implementation while offering //! the following core database operations: //! //! - Opening and closing database connections //! - Preparing SQL statements //! - Binding parameters to prepared statements //! - Iterating through query results //! - Managing the I/O event loop #[cfg(feature = "browser")] pub mod browser; use napi::bindgen_prelude::*; use napi::{Env, Task}; use napi_derive::napi; use std::sync::{Mutex, OnceLock}; use std::{ cell::{Cell, RefCell}, num::NonZeroUsize, sync::Arc, }; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::fmt::format::FmtSpan; /// Step result constants const STEP_ROW: u32 = 1; const STEP_DONE: u32 = 2; const STEP_IO: u32 = 3; /// The presentation mode for rows. #[derive(Debug, Clone)] enum PresentationMode { Expanded, Raw, Pluck, } /// A database connection. #[napi] #[derive(Clone)] pub struct Database { inner: Option>, } /// database inner is Send to the worker for initial connection /// that's why we use OnceLock here - in order to make DatabaseInner Send and Sync pub struct DatabaseInner { path: String, opts: Option, io: Arc, connect: OnceLock, default_safe_integers: Mutex, } pub struct DatabaseConnect { // hold db reference in order to keep it alive // _db can be None if DB is controlled externally (for example, by sync-engine) _db: Option>, conn: Arc, } pub(crate) fn is_memory(path: &str) -> bool { path == ":memory:" } static TRACING_INIT: OnceLock<()> = OnceLock::new(); pub(crate) fn init_tracing(level_filter: &Option) { let Some(level_filter) = level_filter else { return; }; let level_filter = match level_filter.as_ref() { "error" => LevelFilter::ERROR, "warn" => LevelFilter::WARN, "info" => LevelFilter::INFO, "debug" => LevelFilter::DEBUG, "trace" => LevelFilter::TRACE, _ => return, }; TRACING_INIT.get_or_init(|| { tracing_subscriber::fmt() .with_ansi(false) .with_thread_ids(true) .with_span_events(FmtSpan::ACTIVE) .with_max_level(level_filter) .init(); }); } // for now we make DbTask unsound as turso_core::Database and turso_core::Connection are not fully thread-safe unsafe impl Send for DbTask {} pub enum DbTask { Connect { db: Arc }, } impl Task for DbTask { type Output = u32; type JsValue = u32; fn compute(&mut self) -> Result { match self { DbTask::Connect { db } => { connect_sync(db)?; Ok(0) } } } fn resolve(&mut self, _: Env, output: Self::Output) -> Result { Ok(output) } } /// Supported encryption ciphers for local database encryption. #[napi] #[derive(Clone, Copy)] pub enum EncryptionCipher { Aes128Gcm, Aes256Gcm, Aegis256, Aegis256x2, Aegis128l, Aegis128x2, Aegis128x4, } impl EncryptionCipher { fn as_str(&self) -> &'static str { match self { EncryptionCipher::Aes128Gcm => "aes128gcm", EncryptionCipher::Aes256Gcm => "aes256gcm", EncryptionCipher::Aegis256 => "aegis256", EncryptionCipher::Aegis256x2 => "aegis256x2", EncryptionCipher::Aegis128l => "aegis128l", EncryptionCipher::Aegis128x2 => "aegis128x2", EncryptionCipher::Aegis128x4 => "aegis128x4", } } } /// Encryption configuration for local database encryption. #[napi(object)] #[derive(Clone)] pub struct EncryptionOpts { /// The cipher to use for encryption pub cipher: EncryptionCipher, /// The hex-encoded encryption key pub hexkey: String, } /// Most of the options are aligned with better-sqlite API /// (see https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#new-databasepath-options) #[napi(object)] #[derive(Clone)] pub struct DatabaseOpts { pub readonly: Option, pub timeout: Option, pub file_must_exist: Option, pub tracing: Option, /// Experimental features to enable pub experimental: Option>, /// Optional encryption configuration for local database encryption pub encryption: Option, } fn step_sync(stmt: &Arc>) -> napi::Result { let mut stmt = stmt.borrow_mut(); match stmt.step() { Ok(turso_core::StepResult::Row) => Ok(STEP_ROW), Ok(turso_core::StepResult::IO) => Ok(STEP_IO), Ok(turso_core::StepResult::Done) => Ok(STEP_DONE), Ok(turso_core::StepResult::Interrupt) => { Err(create_generic_error("statement was interrupted")) } Ok(turso_core::StepResult::Busy) => Err(create_generic_error("database is locked")), Err(e) => Err(to_generic_error("step failed", e)), } } fn to_generic_error(message: &str, e: E) -> napi::Error { Error::new(Status::GenericFailure, format!("{message}: {e}")) } fn to_error(status: napi::Status, message: &str, e: E) -> napi::Error { Error::new(status, format!("{message}: {e}")) } fn create_generic_error(message: &str) -> napi::Error { Error::new(Status::GenericFailure, message) } fn create_error(status: napi::Status, message: &str) -> napi::Error { Error::new(status, message) } fn connect_sync(db: &DatabaseInner) -> napi::Result<()> { if db.connect.get().is_some() { return Ok(()); } let mut flags = turso_core::OpenFlags::Create; let mut busy_timeout = None; let mut core_opts = turso_core::DatabaseOpts::new(); let mut encryption_opts = None; if let Some(opts) = &db.opts { if opts.readonly == Some(true) { flags.set(turso_core::OpenFlags::ReadOnly, true); flags.set(turso_core::OpenFlags::Create, false); } if opts.file_must_exist == Some(true) { flags.set(turso_core::OpenFlags::Create, false); } if let Some(timeout) = opts.timeout { busy_timeout = Some(std::time::Duration::from_millis(timeout as u64)); } if let Some(experimental) = &opts.experimental { for feature in experimental { core_opts = match feature.as_str() { "views" => core_opts.with_views(true), "strict" => core_opts, // strict is always enabled, kept for backwards compatibility "custom_types" => core_opts.with_custom_types(true), "encryption" => core_opts.with_encryption(true), "index_method" => core_opts.with_index_method(true), "autovacuum" => core_opts.with_autovacuum(true), "attach" => core_opts.with_attach(true), _ => core_opts, }; } } if let Some(encryption) = &opts.encryption { encryption_opts = Some(turso_core::EncryptionOpts { cipher: encryption.cipher.as_str().to_string(), hexkey: encryption.hexkey.clone(), }); // Ensure encryption is enabled if encryption opts are provided core_opts = core_opts.with_encryption(true); } } let io = &db.io; // Parse encryption key if encryption options are provided let encryption_key = if let Some(opts) = &db.opts { if let Some(encryption) = &opts.encryption { Some( turso_core::EncryptionKey::from_hex_string(&encryption.hexkey) .map_err(|e| to_generic_error("invalid encryption key", e))?, ) } else { None } } else { None }; let db_core = turso_core::Database::open_file_with_flags( io.clone(), &db.path, flags, core_opts, encryption_opts, ) .map_err(|e| to_generic_error(&format!("failed to open database {}", db.path), e))?; // Use connect_with_encryption to properly set up encryption context // before the pager reads page 1. This is required for encrypted databases. let conn = db_core .connect_with_encryption(encryption_key) .map_err(|e| to_generic_error("failed to connect", e))?; if let Some(busy_timeout) = busy_timeout { conn.set_busy_timeout(busy_timeout); } let connect = DatabaseConnect { _db: Some(db_core), conn, }; // there can be races between concurrent connect - so let's ignore error in case of let _ = db.connect.set(connect); Ok(()) } #[napi] impl Database { /// Creates a new database instance. /// /// # Arguments /// * `path` - The path to the database file. #[napi(constructor)] pub fn new(path: String, opts: Option) -> napi::Result { let io: Arc = if is_memory(&path) { Arc::new(turso_core::MemoryIO::new()) } else { #[cfg(not(feature = "browser"))] { Arc::new( turso_core::PlatformIO::new() .map_err(|e| to_generic_error("failed to create IO", e))?, ) } #[cfg(feature = "browser")] { browser::opfs() } }; Self::new_with_io(path, io, opts) } pub fn new_with_io( path: String, io: Arc, opts: Option, ) -> napi::Result { if let Some(opts) = &opts { init_tracing(&opts.tracing); } Ok(Self { #[allow(clippy::arc_with_non_send_sync)] inner: Some(Arc::new(DatabaseInner { path, opts, io, connect: OnceLock::new(), default_safe_integers: Mutex::new(false), })), }) } pub fn set_connected(&self, conn: Arc) -> napi::Result<()> { let inner = self.inner()?; inner .connect .set(DatabaseConnect { _db: None, conn }) .map_err(|_| create_generic_error("database was already connected"))?; Ok(()) } fn inner(&self) -> napi::Result<&Arc> { let Some(inner) = &self.inner else { return Err(create_generic_error("database must be connected")); }; Ok(inner) } /// Connect the database synchronously /// This method is idempotent and can be called multiple times safely until the database will be closed #[napi] pub fn connect_sync(&self) -> napi::Result<()> { connect_sync(self.inner()?) } /// Connect the database asynchronously /// This method is idempotent and can be called multiple times safely until the database will be closed #[napi(ts_return_type = "Promise")] pub fn connect_async(&self) -> napi::Result> { Ok(AsyncTask::new(DbTask::Connect { db: self.inner()?.clone(), })) } fn conn(&self) -> Result> { let Some(DatabaseConnect { conn, .. }) = self.inner()?.connect.get() else { return Err(create_generic_error("database must be connected")); }; Ok(conn.clone()) } /// Returns whether the database is in readonly-only mode. #[napi(getter)] pub fn readonly(&self) -> napi::Result { Ok(self.conn()?.is_readonly(0)) } /// Returns whether the database is in memory-only mode. #[napi(getter)] pub fn memory(&self) -> napi::Result { Ok(is_memory(&self.inner()?.path)) } /// Returns whether the database is in memory-only mode. #[napi(getter)] pub fn path(&self) -> napi::Result { Ok(self.inner()?.path.clone()) } /// Returns whether the database connection is open. #[napi(getter)] pub fn open(&self) -> napi::Result { if self.inner.is_none() { return Ok(false); } Ok(self.inner()?.connect.get().is_some()) } /// Prepares a statement for execution. /// /// # Arguments /// /// * `sql` - The SQL statement to prepare. /// /// # Returns /// /// A `Statement` instance. #[napi] pub fn prepare(&self, sql: String) -> napi::Result { let stmt = self .conn()? .prepare(&sql) .map_err(|e| to_generic_error("prepare failed", e))?; let column_names: Vec = (0..stmt.num_columns()) .map(|i| std::ffi::CString::new(stmt.get_column_name(i).to_string()).unwrap()) .collect(); Ok(Statement { #[allow(clippy::arc_with_non_send_sync)] stmt: Some(Arc::new(RefCell::new(stmt))), column_names, mode: RefCell::new(PresentationMode::Expanded), safe_integers: Cell::new(*self.inner()?.default_safe_integers.lock().unwrap()), }) } #[napi] pub fn executor(&self, sql: String) -> napi::Result { Ok(BatchExecutor { conn: Some(self.conn()?), sql, position: 0, stmt: None, }) } /// Returns the rowid of the last row inserted. /// /// # Returns /// /// The rowid of the last row inserted. #[napi] pub fn last_insert_rowid(&self) -> napi::Result { Ok(self.conn()?.last_insert_rowid()) } /// Returns the number of changes made by the last statement. /// /// # Returns /// /// The number of changes made by the last statement. #[napi] pub fn changes(&self) -> napi::Result { Ok(self.conn()?.changes()) } /// Returns the total number of changes made by all statements. /// /// # Returns /// /// The total number of changes made by all statements. #[napi] pub fn total_changes(&self) -> napi::Result { Ok(self.conn()?.total_changes()) } /// Closes the database connection. /// /// # Returns /// /// `Ok(())` if the database is closed successfully. #[napi] pub fn close(&mut self) -> napi::Result<()> { let _ = self.inner.take(); Ok(()) } /// Sets the default safe integers mode for all statements from this database. /// /// # Arguments /// /// * `toggle` - Whether to use safe integers by default. #[napi(js_name = "defaultSafeIntegers")] pub fn default_safe_integers(&self, toggle: Option) -> napi::Result<()> { *self.inner()?.default_safe_integers.lock().unwrap() = toggle.unwrap_or(true); Ok(()) } /// Runs the I/O loop synchronously. #[napi] pub fn io_loop_sync(&self) -> napi::Result<()> { let io = &self.inner()?.io; io.step().map_err(|e| to_generic_error("IO error", e))?; Ok(()) } /// Runs the I/O loop asynchronously, returning a Promise. #[napi(ts_return_type = "Promise")] pub fn io_loop_async(&self) -> napi::Result> { let io = self.inner()?.io.clone(); Ok(AsyncTask::new(IoLoopTask { io })) } /// Classify SQL statement. Returns "read", "write", "begin", "commit", or "rollback". #[napi(js_name = "classifySql")] pub fn classify_sql(&self, sql: String) -> napi::Result { use turso_parser::{ast::Stmt, parser::Parser}; let mut parser = Parser::new(sql.as_bytes()); match parser.next_cmd() { Ok(Some(cmd)) => { if cmd.is_explain() { return Ok("read".to_string()); } let category = match cmd.stmt() { Stmt::Select(..) | Stmt::Pragma { .. } | Stmt::Attach { .. } | Stmt::Detach { .. } | Stmt::Reindex { .. } => "read", Stmt::Begin { .. } | Stmt::Savepoint { .. } => "begin", Stmt::Commit { .. } | Stmt::Release { .. } => "commit", Stmt::Rollback { .. } => "rollback", _ => "write", }; Ok(category.to_string()) } Ok(None) => Ok("read".to_string()), Err(e) => Err(napi::Error::from_reason(format!("classify failed: {e}"))), } } } #[napi] pub struct BatchExecutor { conn: Option>, sql: String, position: usize, stmt: Option>>, } #[napi] impl BatchExecutor { #[napi] pub fn step_sync(&mut self) -> Result { loop { if self.stmt.is_none() && self.position >= self.sql.len() { return Ok(STEP_DONE); } if self.stmt.is_none() { let conn = self.conn.as_ref().unwrap(); match conn.consume_stmt(&self.sql[self.position..]) { #[allow(clippy::arc_with_non_send_sync)] Ok(Some((stmt, offset))) => { self.position += offset; self.stmt = Some(Arc::new(RefCell::new(stmt))); } Ok(None) => return Ok(STEP_DONE), Err(err) => return Err(to_generic_error("failed to consume stmt", err)), } } let stmt = self.stmt.as_ref().unwrap(); match step_sync(stmt) { Ok(STEP_DONE) => { let _ = self.stmt.take(); continue; } result => return result, } } } #[napi] pub fn reset(&mut self) { let _ = self.conn.take(); let _ = self.stmt.take(); } } /// A prepared statement. #[napi] pub struct Statement { stmt: Option>>, column_names: Vec, mode: RefCell, safe_integers: Cell, } #[napi] impl Statement { pub fn stmt(&self) -> napi::Result<&Arc>> { self.stmt .as_ref() .ok_or_else(|| create_generic_error("statement has been finalized")) } #[napi] pub fn reset(&self) -> Result<()> { self.stmt()? .borrow_mut() .reset() .map_err(|e| to_generic_error("reset failed", e))?; Ok(()) } /// Returns the number of parameters in the statement. #[napi] pub fn parameter_count(&self) -> Result { Ok(self.stmt()?.borrow().parameters_count() as u32) } /// Returns the name of a parameter at a specific 1-based index. /// /// # Arguments /// /// * `index` - The 1-based parameter index. #[napi] pub fn parameter_name(&self, index: u32) -> Result> { let non_zero_idx = NonZeroUsize::new(index as usize).ok_or_else(|| { create_error(Status::InvalidArg, "parameter index must be greater than 0") })?; let stmt = self.stmt()?.borrow(); Ok(stmt.parameters().name(non_zero_idx)) } /// Binds a parameter at a specific 1-based index with explicit type. /// /// # Arguments /// /// * `index` - The 1-based parameter index. /// * `value_type` - The type constant (0=null, 1=int, 2=float, 3=text, 4=blob). /// * `value` - The value to bind. #[napi] pub fn bind_at(&self, index: u32, value: Unknown) -> Result<()> { let non_zero_idx = NonZeroUsize::new(index as usize).ok_or_else(|| { create_error(Status::InvalidArg, "parameter index must be greater than 0") })?; let value_type = value.get_type()?; let turso_value = match value_type { ValueType::Null => turso_core::Value::Null, ValueType::Number => { let n: f64 = unsafe { value.cast()? }; if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { turso_core::Value::from_i64(n as i64) } else { turso_core::Value::from_f64(n) } } ValueType::BigInt => { let bigint_str = value.coerce_to_string()?.into_utf8()?.as_str()?.to_owned(); let bigint_value = bigint_str .parse::() .map_err(|e| to_error(Status::NumberExpected, "failed to parse BigInt", e))?; turso_core::Value::from_i64(bigint_value) } ValueType::String => { let s = value.coerce_to_string()?.into_utf8()?; turso_core::Value::Text(s.as_str()?.to_owned().into()) } ValueType::Boolean => { let b: bool = unsafe { value.cast()? }; turso_core::Value::from_i64(if b { 1 } else { 0 }) } ValueType::Object => { let obj = value.coerce_to_object()?; if obj.is_buffer()? || obj.is_typedarray()? { let length = obj.get_named_property::("length")?; let mut bytes = Vec::with_capacity(length as usize); for i in 0..length { let byte = obj.get_element::(i)?; bytes.push(byte as u8); } turso_core::Value::Blob(bytes) } else { let s = value.coerce_to_string()?.into_utf8()?; turso_core::Value::Text(s.as_str()?.to_owned().into()) } } _ => { let s = value.coerce_to_string()?.into_utf8()?; turso_core::Value::Text(s.as_str()?.to_owned().into()) } }; self.stmt()?.borrow_mut().bind_at(non_zero_idx, turso_value); Ok(()) } /// Step the statement and return result code (executed on the main thread): /// 1 = Row available, 2 = Done, 3 = I/O needed #[napi] pub fn step_sync(&self) -> Result { step_sync(self.stmt()?) } /// Get the current row data according to the presentation mode #[napi] pub fn row<'env>(&self, env: &'env Env) -> Result> { let stmt = self.stmt()?.borrow(); let row_data = stmt .row() .ok_or_else(|| create_generic_error("no row data available"))?; let mode = self.mode.borrow(); let safe_integers = self.safe_integers.get(); let row_value = match *mode { PresentationMode::Raw => { let mut raw_array = env.create_array(row_data.len() as u32)?; for (idx, value) in row_data.get_values().enumerate() { let js_value = to_js_value(env, value, safe_integers)?; raw_array.set(idx as u32, js_value)?; } raw_array.coerce_to_object()?.to_unknown() } PresentationMode::Pluck => { let (_, value) = row_data.get_values().enumerate().next().ok_or_else(|| { create_generic_error("pluck mode requires at least one column in the result") })?; to_js_value(env, value, safe_integers)? } PresentationMode::Expanded => { let row = Object::new(env)?; let raw_row = row.raw(); let raw_env = env.raw(); for idx in 0..row_data.len() { let value = row_data.get_value(idx); let column_name = &self.column_names[idx]; let js_value = to_js_value(env, value, safe_integers)?; unsafe { napi::sys::napi_set_named_property( raw_env, raw_row, column_name.as_ptr(), js_value.raw(), ); } } row.to_unknown() } }; Ok(row_value) } /// Sets the presentation mode to raw. #[napi] pub fn raw(&mut self, raw: Option) { self.mode = RefCell::new(match raw { Some(false) => PresentationMode::Expanded, _ => PresentationMode::Raw, }); } /// Sets the presentation mode to pluck. #[napi] pub fn pluck(&mut self, pluck: Option) { self.mode = RefCell::new(match pluck { Some(false) => PresentationMode::Expanded, _ => PresentationMode::Pluck, }); } /// Sets safe integers mode for this statement. /// /// # Arguments /// /// * `toggle` - Whether to use safe integers. #[napi(js_name = "safeIntegers")] pub fn safe_integers(&self, toggle: Option) { self.safe_integers.set(toggle.unwrap_or(true)); } /// Get column information for the statement #[napi(ts_return_type = "Promise")] pub fn columns<'env>(&self, env: &'env Env) -> Result> { let stmt = self.stmt()?.borrow(); let column_count = stmt.num_columns(); let mut js_array = env.create_array(column_count as u32)?; for i in 0..column_count { let mut js_obj = Object::new(env)?; let column_name = stmt.get_column_name(i); let column_type = stmt.get_column_type_name(i); // Set the name property js_obj.set("name", column_name.as_ref())?; // Set type property if available match column_type { Some(type_str) => js_obj.set("type", type_str.as_str())?, None => js_obj.set("type", ToNapiValue::into_unknown(Null, env)?)?, } // For now, set other properties to null since turso_core doesn't provide this metadata js_obj.set("column", ToNapiValue::into_unknown(Null, env)?)?; js_obj.set("table", ToNapiValue::into_unknown(Null, env)?)?; js_obj.set("database", ToNapiValue::into_unknown(Null, env)?)?; js_array.set(i as u32, js_obj)?; } Ok(js_array) } /// Finalizes the statement. #[napi] pub fn finalize(&mut self) -> Result<()> { let _ = self.stmt.take(); Ok(()) } } /// Async task for running the I/O loop. pub struct IoLoopTask { // this field is public because it is also set in the sync package pub io: Arc, } impl Task for IoLoopTask { type Output = (); type JsValue = (); fn compute(&mut self) -> napi::Result { self.io .step() .map_err(|e| to_generic_error("IO error", e))?; Ok(()) } fn resolve(&mut self, _env: Env, _output: Self::Output) -> napi::Result { Ok(()) } } /// Convert a Turso value to a JavaScript value. fn to_js_value<'a>( env: &'a napi::Env, value: &turso_core::Value, safe_integers: bool, ) -> napi::Result> { match value { turso_core::Value::Null => ToNapiValue::into_unknown(Null, env), turso_core::Value::Numeric(turso_core::Numeric::Integer(i)) => { if safe_integers { let bigint = BigInt::from(*i); ToNapiValue::into_unknown(bigint, env) } else { ToNapiValue::into_unknown(*i as f64, env) } } turso_core::Value::Numeric(turso_core::Numeric::Float(f)) => { ToNapiValue::into_unknown(f64::from(*f), env) } turso_core::Value::Text(s) => ToNapiValue::into_unknown(s.as_str(), env), turso_core::Value::Blob(b) => { #[cfg(not(feature = "browser"))] { let buffer = Buffer::from(b.as_slice()); ToNapiValue::into_unknown(buffer, env) } // emnapi do not support Buffer #[cfg(feature = "browser")] { let buffer = Uint8Array::from(b.as_slice()); ToNapiValue::into_unknown(buffer, env) } } } } ================================================ FILE: bindings/javascript/sync/Cargo.toml ================================================ [package] name = "turso_sync_js" version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true [lints] workspace = true [lib] crate-type = ["cdylib"] [dependencies] napi = { version = "3.1.3", default-features = false, features = ["napi6"] } napi-derive = { version = "3.1.1", default-features = true } turso_sync_engine = { workspace = true } turso_core = { workspace = true, features = ["fts"] } turso_node = { workspace = true } genawaiter = { version = "0.99.1", default-features = false } tracing-subscriber = { workspace = true } tracing.workspace = true [build-dependencies] napi-build = "2.2.3" [features] browser = ["turso_node/browser"] ================================================ FILE: bindings/javascript/sync/README.md ================================================

Turso Sync for JavaScript

npm

Chat with other users of Turso on Discord

--- ## About This package is for syncing local Turso databases to the Turso Cloud and back. > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ## Installation ```bash npm install @tursodatabase/sync ``` ## Getting Started To sync a database hosted at [Turso Cloud](https://turso.tech): ```js import { connect } from '@tursodatabase/sync'; const db = await connect({ path: 'local.db', // path used as a prefix for local files created by sync-engine url: 'https://.turso.io', // URL of the remote database: turso db show authToken: '...', // auth token issued from the Turso Cloud: turso db tokens create clientName: 'turso-sync-example' // arbitrary client name }); // db has same functions as Database class from @tursodatabase/database package but adds few more methods for sync: await db.pull(); // pull changes from the remote await db.push(); // push changes to the remote await db.sync(); // pull & push changes ``` ## Related Packages * The [@tursodatabase/database](https://www.npmjs.com/package/@tursodatabase/database) package provides the Turso in-memory database, compatible with SQLite. * The [@tursodatabase/serverless](https://www.npmjs.com/package/@tursodatabase/serverless) package provides a serverless driver with the same API. ## License This project is licensed under the [MIT license](https://github.com/tursodatabase/turso/blob/main/LICENSE.md). ## Support - [GitHub Issues](https://github.com/tursodatabase/turso/issues) - [Documentation](https://docs.turso.tech) - [Discord Community](https://tur.so/discord) ================================================ FILE: bindings/javascript/sync/build.rs ================================================ fn main() { napi_build::setup(); } ================================================ FILE: bindings/javascript/sync/packages/common/README.md ================================================ ## About This package is the Turso Sync common JS library which is shared between final builds for Node and Browser. Do not use this package directly - instead you must use `@tursodatabase/sync` or `@tursodatabase/sync-wasm`. > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ================================================ FILE: bindings/javascript/sync/packages/common/index.ts ================================================ import { run, memoryIO, runner, SyncEngineGuards, Runner } from "./run.js" import { DatabaseOpts, ProtocolIo, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, DatabaseChangeType, EncryptionOpts, } from "./types.js" import { RemoteWriter, RemoteWriterConfig } from "./remote-writer.js" import { RemoteWriteStatement } from "./remote-write-statement.js" export { run, memoryIO, runner, SyncEngineGuards, Runner } export { RemoteWriter, RemoteWriteStatement } export type { DatabaseStats, DatabaseOpts, DatabaseChangeType, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, EncryptionOpts, RemoteWriterConfig, ProtocolIo, RunOpts, } ================================================ FILE: bindings/javascript/sync/packages/common/package.json ================================================ { "name": "@tursodatabase/sync-common", "version": "0.6.0-pre.4", "repository": { "type": "git", "url": "https://github.com/tursodatabase/turso" }, "type": "module", "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", "packageManager": "yarn@4.9.2", "files": [ "dist/**", "README.md" ], "devDependencies": { "typescript": "^5.9.2" }, "scripts": { "tsc-build": "npm exec tsc", "build": "npm run tsc-build", "test": "echo 'no tests'" }, "dependencies": { "@tursodatabase/database-common": "^0.6.0-pre.4", "@tursodatabase/serverless": "^0.2.2" } } ================================================ FILE: bindings/javascript/sync/packages/common/remote-write-statement.ts ================================================ import { RemoteWriter } from "./remote-writer.js"; /** * Wraps a local Statement and routes execution to remote when appropriate. * - If remoteWriter.isInTransaction → all go remote * - If !stmt.readonly → remote + pull after execution * - Otherwise → local Statement */ export class RemoteWriteStatement { private localStmt: any; private sql: string; private isStmtReadonly: boolean; private remoteWriter: RemoteWriter; private pullFn: () => Promise; private _boundArgs: any[] = []; constructor( localStmt: any, sql: string, isStmtReadonly: boolean, remoteWriter: RemoteWriter, pullFn: () => Promise, ) { this.localStmt = localStmt; this.sql = sql; this.isStmtReadonly = isStmtReadonly; this.remoteWriter = remoteWriter; this.pullFn = pullFn; } private shouldGoRemote(): boolean { return this.remoteWriter.isInTransaction || !this.isStmtReadonly; } private shouldPullAfter(): boolean { // Pull after standalone writes (not in transaction) return !this.isStmtReadonly && !this.remoteWriter.isInTransaction; } raw(toggle?: boolean) { this.localStmt.raw(toggle); return this; } pluck(toggle?: boolean) { this.localStmt.pluck(toggle); return this; } safeIntegers(toggle?: boolean) { this.localStmt.safeIntegers(toggle); return this; } columns() { return this.localStmt.columns(); } get reader(): boolean { return this.localStmt.reader; } bind(...bindParameters: any[]) { this._boundArgs = flattenArgs(bindParameters); this.localStmt.bind(...bindParameters); return this; } async run(...bindParameters: any[]) { if (!this.shouldGoRemote()) { return await this.localStmt.run(...bindParameters); } const args = bindParameters.length > 0 ? flattenArgs(bindParameters) : this._boundArgs; const result = await this.remoteWriter.execute(this.sql, args); if (this.shouldPullAfter()) { await this.pullFn(); } return { changes: result.rowsAffected, lastInsertRowid: result.lastInsertRowid, }; } async get(...bindParameters: any[]) { if (!this.shouldGoRemote()) { return await this.localStmt.get(...bindParameters); } const args = bindParameters.length > 0 ? flattenArgs(bindParameters) : this._boundArgs; const result = await this.remoteWriter.execute(this.sql, args); if (this.shouldPullAfter()) { await this.pullFn(); } return result.rows.length > 0 ? result.rows[0] : undefined; } async all(...bindParameters: any[]) { if (!this.shouldGoRemote()) { return await this.localStmt.all(...bindParameters); } const args = bindParameters.length > 0 ? flattenArgs(bindParameters) : this._boundArgs; const result = await this.remoteWriter.execute(this.sql, args); if (this.shouldPullAfter()) { await this.pullFn(); } return result.rows; } async *iterate(...bindParameters: any[]) { if (!this.shouldGoRemote()) { yield* this.localStmt.iterate(...bindParameters); return; } const args = bindParameters.length > 0 ? flattenArgs(bindParameters) : this._boundArgs; const result = await this.remoteWriter.execute(this.sql, args); if (this.shouldPullAfter()) { await this.pullFn(); } for (const row of result.rows) { yield row; } } close() { this.localStmt.close(); } } function flattenArgs(bindParameters: any[]): any[] { if (bindParameters.length === 1 && Array.isArray(bindParameters[0])) { return bindParameters[0]; } return bindParameters; } ================================================ FILE: bindings/javascript/sync/packages/common/remote-writer.ts ================================================ import { Session, type SessionConfig } from "@tursodatabase/serverless"; export interface RemoteWriterConfig { url: string; authToken?: string | (() => Promise); remoteEncryptionKey?: string; } /** * Manages remote write routing using Session from @tursodatabase/serverless. * For standalone writes (not in txn), creates a temporary Session per write. * For transactions, reuses Session (baton-based) across all statements. */ export class RemoteWriter { private session: Session | null = null; private _inRemoteTxn: boolean = false; private config: RemoteWriterConfig; constructor(config: RemoteWriterConfig) { this.config = config; } private async resolveAuthToken(): Promise { if (typeof this.config.authToken === "function") { return await this.config.authToken(); } return this.config.authToken; } private async createSession(): Promise { const authToken = await this.resolveAuthToken(); const sessionConfig: SessionConfig = { url: this.config.url, authToken, remoteEncryptionKey: this.config.remoteEncryptionKey, }; return new Session(sessionConfig); } /** * Execute single SQL on remote. Creates temp session if not in txn. */ async execute(sql: string, args: any[] = []): Promise<{ columns: string[]; rows: any[]; rowsAffected: number; lastInsertRowid: number | undefined; }> { if (this._inRemoteTxn) { return await this.session!.execute(sql, args); } const session = await this.createSession(); try { return await session.execute(sql, args); } finally { await session.close(); } } /** * Execute multi-statement SQL on remote (for exec() path). */ async sequence(sql: string): Promise { if (this._inRemoteTxn) { await this.session!.sequence(sql); return; } const session = await this.createSession(); try { await session.sequence(sql); } finally { await session.close(); } } /** * Begin a remote transaction. Creates a session and sends BEGIN. */ async beginTransaction(mode: string): Promise { this.session = await this.createSession(); await this.session.sequence("BEGIN " + mode); this._inRemoteTxn = true; } /** * Commit the remote transaction. Sends COMMIT and closes the session. */ async commitTransaction(): Promise { if (!this.session) { throw new Error("No active remote transaction"); } try { await this.session.sequence("COMMIT"); } finally { this._inRemoteTxn = false; await this.session.close(); this.session = null; } } /** * Rollback the remote transaction. Sends ROLLBACK and closes the session. */ async rollbackTransaction(): Promise { if (!this.session) { throw new Error("No active remote transaction"); } try { await this.session.sequence("ROLLBACK"); } finally { this._inRemoteTxn = false; await this.session.close(); this.session = null; } } /** * Execute SQL on remote with auto-detection of BEGIN/COMMIT/ROLLBACK. * Manages session lifecycle based on SQL category. */ async execRemote(sql: string, category: string): Promise<{ shouldPull: boolean }> { if (category === "begin") { this.session = await this.createSession(); await this.session.sequence(sql); this._inRemoteTxn = true; return { shouldPull: false }; } if (category === "commit") { if (!this.session) throw new Error("No active remote transaction"); try { await this.session.sequence(sql); } finally { this._inRemoteTxn = false; await this.session.close(); this.session = null; } return { shouldPull: true }; } if (category === "rollback") { if (!this.session) throw new Error("No active remote transaction"); try { await this.session.sequence(sql); } finally { this._inRemoteTxn = false; await this.session.close(); this.session = null; } return { shouldPull: false }; } await this.sequence(sql); return { shouldPull: !this._inRemoteTxn }; } get isInTransaction(): boolean { return this._inRemoteTxn; } async close(): Promise { if (this.session) { try { if (this._inRemoteTxn) { await this.session.sequence("ROLLBACK"); } } catch { // ignore errors during cleanup } await this.session.close(); this.session = null; this._inRemoteTxn = false; } } } ================================================ FILE: bindings/javascript/sync/packages/common/run.ts ================================================ "use strict"; import { GeneratorResponse, ProtocolIo, RunOpts } from "./types.js"; import { AsyncLock } from "@tursodatabase/database-common"; interface TrackPromise { promise: Promise, finished: boolean } function trackPromise(p: Promise): TrackPromise { let status = { promise: null, finished: false }; status.promise = p.finally(() => status.finished = true); return status; } function timeoutMs(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) } function normalizeUrl(url: string): string { return url.replace(/^libsql:\/\//, 'https://'); } async function process(opts: RunOpts, io: ProtocolIo, request: any) { const requestType = request.request(); const completion = request.completion(); if (requestType.type == 'Http') { let url: string | null = requestType.url; if (typeof opts.url == "function") { url = opts.url(); } else { url = opts.url; } if (url == null) { completion.poison(`url is empty - sync is paused`); return; } url = normalizeUrl(url); try { let headers = typeof opts.headers === "function" ? await opts.headers() : opts.headers; if (requestType.headers != null && requestType.headers.length > 0) { headers = { ...headers }; for (let header of requestType.headers) { headers[header[0]] = header[1]; } } const response = await fetch(`${url}${requestType.path}`, { method: requestType.method, headers: headers, body: requestType.body != null ? new Uint8Array(requestType.body) : null, }); completion.status(response.status); const reader = response.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) { completion.done(); break; } completion.pushBuffer(value); } } catch (error) { completion.poison(`fetch error: ${error}`); } } else if (requestType.type == 'FullRead') { try { const metadata = await io.read(requestType.path); if (metadata != null) { completion.pushBuffer(metadata); } completion.done(); } catch (error) { completion.poison(`metadata read error: ${error}`); } } else if (requestType.type == 'FullWrite') { try { await io.write(requestType.path, requestType.content); completion.done(); } catch (error) { completion.poison(`metadata write error: ${error}`); } } else if (requestType.type == 'Transform') { if (opts.transform == null) { completion.poison("transform is not set"); return; } const results = []; for (const mutation of requestType.mutations) { const result = opts.transform(mutation); if (result == null) { results.push({ type: 'Keep' }); } else if (result.operation == 'skip') { results.push({ type: 'Skip' }); } else if (result.operation == 'rewrite') { results.push({ type: 'Rewrite', stmt: result.stmt }); } else { completion.poison("unexpected transform operation"); return; } } completion.pushTransform(results); completion.done(); } } export function memoryIO(): ProtocolIo { let values = new Map(); return { async read(path: string): Promise { return values.get(path); }, async write(path: string, data: Buffer | Uint8Array): Promise { values.set(path, data); } } }; export interface Runner { wait(): Promise; } export function runner(opts: RunOpts, io: ProtocolIo, engine: any): Runner { let tasks = []; return { async wait() { for (let request = engine.protocolIo(); request != null; request = engine.protocolIo()) { tasks.push(trackPromise(process(opts, io, request))); } const tasksRace = tasks.length == 0 ? Promise.resolve() : Promise.race([timeoutMs(opts.preemptionMs), ...tasks.map(t => t.promise)]); await Promise.all([engine.ioLoopAsync(), tasksRace]); tasks = tasks.filter(t => !t.finished); engine.protocolIoStep(); }, } } export async function run(runner: Runner, generator: any): Promise { while (true) { const { type, ...rest }: GeneratorResponse = await generator.resumeAsync(null); if (type == 'Done') { return null; } if (type == 'SyncEngineStats') { return rest; } if (type == 'SyncEngineChanges') { //@ts-ignore return rest.changes; } await runner.wait(); } } export class SyncEngineGuards { waitLock: AsyncLock; pushLock: AsyncLock; pullLock: AsyncLock; checkpointLock: AsyncLock; constructor() { this.waitLock = new AsyncLock(); this.pushLock = new AsyncLock(); this.pullLock = new AsyncLock(); this.checkpointLock = new AsyncLock(); } async wait(f: () => Promise): Promise { try { await this.waitLock.acquire(); return await f(); } finally { this.waitLock.release(); } } async push(f: () => Promise) { try { await this.pushLock.acquire(); await this.pullLock.acquire(); await this.checkpointLock.acquire(); return await f(); } finally { this.pushLock.release(); this.pullLock.release(); this.checkpointLock.release(); } } async apply(f: () => Promise) { try { await this.waitLock.acquire(); await this.pushLock.acquire(); await this.pullLock.acquire(); await this.checkpointLock.acquire(); return await f(); } finally { this.waitLock.release(); this.pushLock.release(); this.pullLock.release(); this.checkpointLock.release(); } } async checkpoint(f: () => Promise) { try { await this.waitLock.acquire(); await this.pushLock.acquire(); await this.pullLock.acquire(); await this.checkpointLock.acquire(); return await f(); } finally { this.waitLock.release(); this.pushLock.release(); this.pullLock.release(); this.checkpointLock.release(); } } } ================================================ FILE: bindings/javascript/sync/packages/common/tsconfig.json ================================================ { "compilerOptions": { "skipLibCheck": true, "declaration": true, "declarationMap": true, "module": "nodenext", "target": "esnext", "moduleResolution": "nodenext", "outDir": "dist/", "lib": [ "es2020", "dom" ], }, "include": [ "*" ] } ================================================ FILE: bindings/javascript/sync/packages/common/types.ts ================================================ export declare const enum DatabaseChangeType { Insert = 'insert', Update = 'update', Delete = 'delete' } export interface DatabaseRowStatement { /** * SQL statements with positional placeholders (?) */ sql: string /** * values to substitute placeholders */ values: Array } /** * transformation result: * - skip: ignore the mutation completely and do not apply it * - rewrite: replace mutation with the provided statement * - null: do not change mutation and keep it as is */ export type DatabaseRowTransformResult = { operation: 'skip' } | { operation: 'rewrite', stmt: DatabaseRowStatement } | null; export interface DatabaseRowMutation { /** * unix seconds timestamp of the change */ changeTime: number /** * table name of the change */ tableName: string /** * rowid of the change */ id: number /** * type of the change (insert/delete/update) */ changeType: DatabaseChangeType /** * columns of the row before the change */ before?: Record /** * columns of the row after the change */ after?: Record /** * only updated columns of the row after the change */ updates?: Record } export type Transform = (arg: DatabaseRowMutation) => DatabaseRowTransformResult; export interface EncryptionOpts { // base64 encoded encryption key (must be either 16 or 32 bytes depending on the cipher) key: string, // encryption cipher algorithm // - aes256gcm, aes128gcm, chacha20poly1305: 28 reserved bytes // - aegis128l, aegis128x2, aegis128x4: 32 reserved bytes // - aegis256, aegis256x2, aegis256x4: 48 reserved bytes cipher: 'aes256gcm' | 'aes128gcm' | 'chacha20poly1305' | 'aegis128l' | 'aegis128x2' | 'aegis128x4' | 'aegis256' | 'aegis256x2' | 'aegis256x4' } export interface DatabaseOpts { /** * local path where to store all synced database files (e.g. local.db) * note, that synced database will write several files with that prefix * (e.g. local.db-info, local.db-wal, etc) * */ path: string; /** * optional url of the remote database (e.g. libsql://db-org.turso.io) * (if omitted - local-only database will be created) * * you can also provide function which will return URL or null * in this case local database will be created and sync will be "switched-on" whenever the url will return non-empty value * note, that all other parameters (like encryption) must be set in advance in order for the "deferred" sync to work properly */ url?: string | (() => string | null); /** * auth token for the remote database * (can be either static string or function which will provide short-lived credentials for every new request) */ authToken?: string | (() => Promise); /** * arbitrary client name which can be used to distinguish clients internally * the library will gurantee uniquiness of the clientId by appending unique suffix to the clientName */ clientName?: string; /** * optional remote encryption parameters if cloud database were encrypted by default */ remoteEncryption?: EncryptionOpts; /** * optional callback which will be called for every mutation before sending it to the remote * this callback can transform the update in order to support complex conflict resolution strategy */ transform?: Transform, /** * optional long-polling timeout for pull operation * if not set - no timeout is applied */ longPollTimeoutMs?: number, /** * optional parameter to enable internal logging for the database */ tracing?: 'error' | 'warn' | 'info' | 'debug' | 'trace', /** * When enabled, write statements execute on remote server instead of locally. * After each write (or transaction commit), changes are pulled for read-your-writes consistency. * Requires `url`. All explicit transactions go to remote. * WARNING: This feature is EXPERIMENTAL */ remoteWritesExperimental?: boolean; /** * optional parameter to enable partial sync for the database * WARNING: This feature is EXPERIMENTAL */ partialSyncExperimental?: { /* bootstrap strategy configuration - prefix strategy loads first N bytes locally at the startup - query strategy loads pages touched by the provided SQL statement */ bootstrapStrategy: { kind: 'prefix', length: number } | { kind: 'query', query: string }, /* optional segment size which makes sync engine to load pages in batches of segment_size bytes (so, if loading page 1 with segment_size=128kb then 32 pages [1..32] will be loaded) */ segmentSize?: number, /* optional parameter which makes sync engine to prefetch pages which probably will be accessed soon */ prefetch?: boolean, } } export interface DatabaseStats { /** * amount of local changes not sent to the remote */ cdcOperations: number; /** * size of the main WAL file in bytes */ mainWalSize: number; /** * size of the revert WAL file in bytes */ revertWalSize: number; /** * unix timestamp of last successful pull time */ lastPullUnixTime: number; /** * unix timestamp of last successful push time */ lastPushUnixTime: number | null; /** * opaque revision of the changes pulled locally from remote * (can be used as e-tag, but string must not be interpreted in any way and must be used as opaque value) */ revision: string | null; /** * total amount of sent bytes over the network */ networkSentBytes: number; /** * total amount of received bytes over the network */ networkReceivedBytes: number; } /* internal types used in the native/browser packages */ export interface RunOpts { preemptionMs: number, url: string | (() => string | null), headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>) transform?: Transform, } export interface ProtocolIo { read(path: string): Promise; write(path: string, content: Buffer | Uint8Array): Promise; } export type GeneratorResponse = { type: 'IO' } | { type: 'Done' } | ({ type: 'SyncEngineStats' } & DatabaseStats) | { type: 'SyncEngineChanges', changes: any } ================================================ FILE: bindings/javascript/sync/packages/native/README.md ================================================

Turso Database for JavaScript in Node

npm

Chat with other users of Turso on Discord

--- ## About This package is the Turso embedded database library for JavaScript in Node. > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ## Features - **SQLite compatible:** SQLite query language and file format support ([status](https://github.com/tursodatabase/turso/blob/main/COMPAT.md)). - **In-process**: No network overhead, runs directly in your Node.js process - **TypeScript support**: Full TypeScript definitions included - **Cross-platform**: Supports Linux (x86 and arm64), macOS, Windows (browser is supported in the separate package `@tursodatabase/database-wasm` package) ## Installation ```bash npm install @tursodatabase/database ``` ## Getting Started ### In-Memory Database ```javascript import { connect } from '@tursodatabase/database'; // Create an in-memory database const db = await connect(':memory:'); // Create a table await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)'); // Insert data const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); await insert.run('Alice', 'alice@example.com'); await insert.run('Bob', 'bob@example.com'); // Query data const users = await db.prepare('SELECT * FROM users').all(); console.log(users); // Output: [ // { id: 1, name: 'Alice', email: 'alice@example.com' }, // { id: 2, name: 'Bob', email: 'bob@example.com' } // ] ``` ### File-Based Database ```javascript import { connect } from '@tursodatabase/database'; // Create or open a database file const db = await connect('my-database.db'); // Create a table await db.exec(` CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Insert a post const insertPost = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)'); const result = await insertPost.run('Hello World', 'This is my first blog post!'); console.log(`Inserted post with ID: ${result.lastInsertRowid}`); ``` ### Transactions ```javascript import { connect } from '@tursodatabase/database'; const db = await connect('transactions.db'); // Using transactions for atomic operations const transaction = db.transaction(async (users) => { const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); for (const user of users) { await insert.run(user.name, user.email); } }); // Execute transaction await transaction([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' } ]); ``` ## API Reference For complete API documentation, see [JavaScript API Reference](https://github.com/tursodatabase/turso/blob/main/docs/javascript-api-reference.md). ## Related Packages * The [@tursodatabase/serverless](https://www.npmjs.com/package/@tursodatabase/serverless) package provides a serverless driver with the same API. * The [@tursodatabase/sync](https://www.npmjs.com/package/@tursodatabase/sync) package provides bidirectional sync between a local Turso database and Turso Cloud. ## License This project is licensed under the [MIT license](https://github.com/tursodatabase/turso/blob/main/LICENSE.md). ## Support - [GitHub Issues](https://github.com/tursodatabase/turso/issues) - [Documentation](https://docs.turso.tech) - [Discord Community](https://tur.so/discord) ================================================ FILE: bindings/javascript/sync/packages/native/index.d.ts ================================================ /* auto-generated by NAPI-RS */ /* eslint-disable */ export declare class BatchExecutor { stepSync(): number reset(): void } /** A database connection. */ export declare class Database { /** * Creates a new database instance. * * # Arguments * * `path` - The path to the database file. */ constructor(path: string, opts?: DatabaseOpts | undefined | null) /** * Connect the database synchronously * This method is idempotent and can be called multiple times safely until the database will be closed */ connectSync(): void /** * Connect the database asynchronously * This method is idempotent and can be called multiple times safely until the database will be closed */ connectAsync(): Promise /** Returns whether the database is in readonly-only mode. */ get readonly(): boolean /** Returns whether the database is in memory-only mode. */ get memory(): boolean /** Returns whether the database is in memory-only mode. */ get path(): string /** Returns whether the database connection is open. */ get open(): boolean /** * Prepares a statement for execution. * * # Arguments * * * `sql` - The SQL statement to prepare. * * # Returns * * A `Statement` instance. */ prepare(sql: string): Statement executor(sql: string): BatchExecutor /** * Returns the rowid of the last row inserted. * * # Returns * * The rowid of the last row inserted. */ lastInsertRowid(): number /** * Returns the number of changes made by the last statement. * * # Returns * * The number of changes made by the last statement. */ changes(): number /** * Returns the total number of changes made by all statements. * * # Returns * * The total number of changes made by all statements. */ totalChanges(): number /** * Closes the database connection. * * # Returns * * `Ok(())` if the database is closed successfully. */ close(): void /** * Sets the default safe integers mode for all statements from this database. * * # Arguments * * * `toggle` - Whether to use safe integers by default. */ defaultSafeIntegers(toggle?: boolean | undefined | null): void /** Runs the I/O loop synchronously. */ ioLoopSync(): void /** Runs the I/O loop asynchronously, returning a Promise. */ ioLoopAsync(): Promise /** Classify SQL statement. Returns "read", "write", "begin", "commit", or "rollback". */ classifySql(sql: string): string } /** A prepared statement. */ export declare class Statement { reset(): void /** Returns the number of parameters in the statement. */ parameterCount(): number /** * Returns the name of a parameter at a specific 1-based index. * * # Arguments * * * `index` - The 1-based parameter index. */ parameterName(index: number): string | null /** * Binds a parameter at a specific 1-based index with explicit type. * * # Arguments * * * `index` - The 1-based parameter index. * * `value_type` - The type constant (0=null, 1=int, 2=float, 3=text, 4=blob). * * `value` - The value to bind. */ bindAt(index: number, value: unknown): void /** * Step the statement and return result code (executed on the main thread): * 1 = Row available, 2 = Done, 3 = I/O needed */ stepSync(): number /** Get the current row data according to the presentation mode */ row(): unknown /** Sets the presentation mode to raw. */ raw(raw?: boolean | undefined | null): void /** Sets the presentation mode to pluck. */ pluck(pluck?: boolean | undefined | null): void /** * Sets safe integers mode for this statement. * * # Arguments * * * `toggle` - Whether to use safe integers. */ safeIntegers(toggle?: boolean | undefined | null): void /** Get column information for the statement */ columns(): Promise /** Finalizes the statement. */ finalize(): void } /** * Most of the options are aligned with better-sqlite API * (see https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#new-databasepath-options) */ export interface DatabaseOpts { readonly?: boolean timeout?: number fileMustExist?: boolean tracing?: string /** Experimental features to enable */ experimental?: Array /** Optional encryption configuration for local database encryption */ encryption?: EncryptionOpts } /** Supported encryption ciphers for local database encryption. */ export declare const enum EncryptionCipher { Aes128Gcm = 0, Aes256Gcm = 1, Aegis256 = 2, Aegis256x2 = 3, Aegis128l = 4, Aegis128x2 = 5, Aegis128x4 = 6 } /** Encryption configuration for local database encryption. */ export interface EncryptionOpts { /** The cipher to use for encryption */ cipher: EncryptionCipher /** The hex-encoded encryption key */ hexkey: string } export declare class GeneratorHolder { resumeSync(error?: string | undefined | null): GeneratorResponse resumeAsync(error?: string | undefined | null): Promise } export declare class JsDataCompletion { poison(err: string): void status(value: number): void pushBuffer(value: Buffer): void pushTransform(values: Array): void done(): void } export declare class JsProtocolIo { } export declare class JsProtocolRequestBytes { request(): JsProtocolRequest completion(): JsDataCompletion } export declare class SyncEngine { constructor(opts: SyncEngineOpts) connect(): GeneratorHolder ioLoopSync(): void /** Runs the I/O loop asynchronously, returning a Promise. */ ioLoopAsync(): Promise protocolIo(): JsProtocolRequestBytes | null protocolIoStep(): void push(): GeneratorHolder stats(): GeneratorHolder wait(): GeneratorHolder apply(changes: SyncEngineChanges): GeneratorHolder checkpoint(): GeneratorHolder db(): Database close(): void } export declare class SyncEngineChanges { empty(): boolean } export declare const enum DatabaseChangeTypeJs { Insert = 'insert', Update = 'update', Delete = 'delete' } export interface DatabaseRowMutationJs { changeTime: number tableName: string id: number changeType: DatabaseChangeTypeJs before?: Record after?: Record updates?: Record } export interface DatabaseRowStatementJs { sql: string values: Array } export type DatabaseRowTransformResultJs = | { type: 'Keep' } | { type: 'Skip' } | { type: 'Rewrite', stmt: DatabaseRowStatementJs } export type GeneratorResponse = | { type: 'IO' } | { type: 'Done' } | { type: 'SyncEngineStats', cdcOperations: number, mainWalSize: number, revertWalSize: number, lastPullUnixTime?: number, lastPushUnixTime?: number, revision?: string, networkSentBytes: number, networkReceivedBytes: number } | { type: 'SyncEngineChanges', changes: SyncEngineChanges } export type JsPartialBootstrapStrategy = | { type: 'Prefix', length: number } | { type: 'Query', query: string } export interface JsPartialSyncOpts { bootstrapStrategy: JsPartialBootstrapStrategy segmentSize?: number prefetch?: boolean } export type JsProtocolRequest = | { type: 'Http', url?: string, method: string, path: string, body?: Array, headers: Array<[string, string]> } | { type: 'FullRead', path: string } | { type: 'FullWrite', path: string, content: Array } | { type: 'Transform', mutations: Array } export interface SyncEngineOpts { path: string remoteUrl?: string clientName?: string walPullBatchSize?: number longPollTimeoutMs?: number tracing?: string tablesIgnore?: Array useTransform: boolean protocolVersion?: SyncEngineProtocolVersion bootstrapIfEmpty: boolean /** Encryption cipher for the Turso Cloud database. */ remoteEncryptionCipher?: string /** * Base64-encoded encryption key for the Turso Cloud database. * Must match the key used when creating the encrypted database. */ remoteEncryptionKey?: string partialSyncOpts?: JsPartialSyncOpts } export declare const enum SyncEngineProtocolVersion { Legacy = 'legacy', V1 = 'v1' } ================================================ FILE: bindings/javascript/sync/packages/native/index.js ================================================ // prettier-ignore /* eslint-disable */ // @ts-nocheck /* auto-generated by NAPI-RS */ import { createRequire } from 'node:module' const require = createRequire(import.meta.url) const __dirname = new URL('.', import.meta.url).pathname const { readFileSync } = require('node:fs') let nativeBinding = null const loadErrors = [] const isMusl = () => { let musl = false if (process.platform === 'linux') { musl = isMuslFromFilesystem() if (musl === null) { musl = isMuslFromReport() } if (musl === null) { musl = isMuslFromChildProcess() } } return musl } const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') const isMuslFromFilesystem = () => { try { return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') } catch { return null } } const isMuslFromReport = () => { let report = null if (typeof process.report?.getReport === 'function') { process.report.excludeNetwork = true report = process.report.getReport() } if (!report) { return null } if (report.header && report.header.glibcVersionRuntime) { return false } if (Array.isArray(report.sharedObjects)) { if (report.sharedObjects.some(isFileMusl)) { return true } } return false } const isMuslFromChildProcess = () => { try { return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') } catch (e) { // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false return false } } function requireNative() { if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { try { nativeBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); } catch (err) { loadErrors.push(err) } } else if (process.platform === 'android') { if (process.arch === 'arm64') { try { return require('./sync.android-arm64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-android-arm64') const bindingPackageVersion = require('@tursodatabase/sync-android-arm64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'arm') { try { return require('./sync.android-arm-eabi.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-android-arm-eabi') const bindingPackageVersion = require('@tursodatabase/sync-android-arm-eabi/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) } } else if (process.platform === 'win32') { if (process.arch === 'x64') { try { return require('./sync.win32-x64-msvc.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-win32-x64-msvc') const bindingPackageVersion = require('@tursodatabase/sync-win32-x64-msvc/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'ia32') { try { return require('./sync.win32-ia32-msvc.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-win32-ia32-msvc') const bindingPackageVersion = require('@tursodatabase/sync-win32-ia32-msvc/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'arm64') { try { return require('./sync.win32-arm64-msvc.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-win32-arm64-msvc') const bindingPackageVersion = require('@tursodatabase/sync-win32-arm64-msvc/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) } } else if (process.platform === 'darwin') { try { return require('./sync.darwin-universal.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-darwin-universal') const bindingPackageVersion = require('@tursodatabase/sync-darwin-universal/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } if (process.arch === 'x64') { try { return require('./sync.darwin-x64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-darwin-x64') const bindingPackageVersion = require('@tursodatabase/sync-darwin-x64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'arm64') { try { return require('./sync.darwin-arm64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-darwin-arm64') const bindingPackageVersion = require('@tursodatabase/sync-darwin-arm64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) } } else if (process.platform === 'freebsd') { if (process.arch === 'x64') { try { return require('./sync.freebsd-x64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-freebsd-x64') const bindingPackageVersion = require('@tursodatabase/sync-freebsd-x64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'arm64') { try { return require('./sync.freebsd-arm64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-freebsd-arm64') const bindingPackageVersion = require('@tursodatabase/sync-freebsd-arm64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) } } else if (process.platform === 'linux') { if (process.arch === 'x64') { if (isMusl()) { try { return require('./sync.linux-x64-musl.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-linux-x64-musl') const bindingPackageVersion = require('@tursodatabase/sync-linux-x64-musl/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { try { return require('./sync.linux-x64-gnu.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-linux-x64-gnu') const bindingPackageVersion = require('@tursodatabase/sync-linux-x64-gnu/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } } else if (process.arch === 'arm64') { if (isMusl()) { try { return require('./sync.linux-arm64-musl.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-linux-arm64-musl') const bindingPackageVersion = require('@tursodatabase/sync-linux-arm64-musl/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { try { return require('./sync.linux-arm64-gnu.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-linux-arm64-gnu') const bindingPackageVersion = require('@tursodatabase/sync-linux-arm64-gnu/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } } else if (process.arch === 'arm') { if (isMusl()) { try { return require('./sync.linux-arm-musleabihf.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-linux-arm-musleabihf') const bindingPackageVersion = require('@tursodatabase/sync-linux-arm-musleabihf/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { try { return require('./sync.linux-arm-gnueabihf.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-linux-arm-gnueabihf') const bindingPackageVersion = require('@tursodatabase/sync-linux-arm-gnueabihf/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } } else if (process.arch === 'riscv64') { if (isMusl()) { try { return require('./sync.linux-riscv64-musl.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-linux-riscv64-musl') const bindingPackageVersion = require('@tursodatabase/sync-linux-riscv64-musl/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { try { return require('./sync.linux-riscv64-gnu.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-linux-riscv64-gnu') const bindingPackageVersion = require('@tursodatabase/sync-linux-riscv64-gnu/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } } else if (process.arch === 'ppc64') { try { return require('./sync.linux-ppc64-gnu.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-linux-ppc64-gnu') const bindingPackageVersion = require('@tursodatabase/sync-linux-ppc64-gnu/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 's390x') { try { return require('./sync.linux-s390x-gnu.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-linux-s390x-gnu') const bindingPackageVersion = require('@tursodatabase/sync-linux-s390x-gnu/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) } } else if (process.platform === 'openharmony') { if (process.arch === 'arm64') { try { return require('./sync.openharmony-arm64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-openharmony-arm64') const bindingPackageVersion = require('@tursodatabase/sync-openharmony-arm64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'x64') { try { return require('./sync.openharmony-x64.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-openharmony-x64') const bindingPackageVersion = require('@tursodatabase/sync-openharmony-x64/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'arm') { try { return require('./sync.openharmony-arm.node') } catch (e) { loadErrors.push(e) } try { const binding = require('@tursodatabase/sync-openharmony-arm') const bindingPackageVersion = require('@tursodatabase/sync-openharmony-arm/package.json').version if (bindingPackageVersion !== '0.5.0-pre.12' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { throw new Error(`Native binding package version mismatch, expected 0.5.0-pre.12 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { loadErrors.push(e) } } else { loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`)) } } else { loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) } } nativeBinding = requireNative() if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { try { nativeBinding = require('./sync.wasi.cjs') } catch (err) { if (process.env.NAPI_RS_FORCE_WASI) { loadErrors.push(err) } } if (!nativeBinding) { try { nativeBinding = require('@tursodatabase/sync-wasm32-wasi') } catch (err) { if (process.env.NAPI_RS_FORCE_WASI) { loadErrors.push(err) } } } } if (!nativeBinding) { if (loadErrors.length > 0) { throw new Error( `Cannot find native binding. ` + `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', { cause: loadErrors } ) } throw new Error(`Failed to load native binding`) } const { BatchExecutor, Database, Statement, EncryptionCipher, GeneratorHolder, JsDataCompletion, JsProtocolIo, JsProtocolRequestBytes, SyncEngine, SyncEngineChanges, DatabaseChangeTypeJs, SyncEngineProtocolVersion } = nativeBinding export { BatchExecutor } export { Database } export { Statement } export { EncryptionCipher } export { GeneratorHolder } export { JsDataCompletion } export { JsProtocolIo } export { JsProtocolRequestBytes } export { SyncEngine } export { SyncEngineChanges } export { DatabaseChangeTypeJs } export { SyncEngineProtocolVersion } ================================================ FILE: bindings/javascript/sync/packages/native/package.json ================================================ { "name": "@tursodatabase/sync", "version": "0.6.0-pre.4", "repository": { "type": "git", "url": "https://github.com/tursodatabase/turso" }, "license": "MIT", "module": "./dist/promise.js", "main": "./dist/promise.js", "type": "module", "exports": { ".": "./dist/promise.js", "./compat": "./dist/compat.js" }, "files": [ "index.js", "dist/**", "README.md" ], "packageManager": "yarn@4.9.2", "devDependencies": { "@napi-rs/cli": "^3.1.5", "@types/node": "^24.3.1", "typescript": "^5.9.2", "vitest": "^3.2.4" }, "scripts": { "napi-build": "napi build --platform --profile release-official --esm --manifest-path ../../Cargo.toml --output-dir .", "napi-dirs": "napi create-npm-dirs", "napi-artifacts": "napi artifacts --output-dir .", "tsc-build": "npm exec tsc", "build": "npm run napi-build && npm run tsc-build", "test": "VITE_TURSO_DB_URL=http://jj--a--a.localhost:8080 vitest --run --exclude remote-write.test.ts", "test:remote-write": "vitest --run remote-write.test.ts", "prepublishOnly": "npm run napi-dirs && npm run napi-artifacts && napi prepublish -t npm" }, "napi": { "binaryName": "sync", "targets": [ "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", "aarch64-apple-darwin", "aarch64-unknown-linux-gnu" ] }, "dependencies": { "@tursodatabase/database-common": "^0.6.0-pre.4", "@tursodatabase/sync-common": "^0.6.0-pre.4" }, "imports": { "#index": "./index.js" } } ================================================ FILE: bindings/javascript/sync/packages/native/promise.test.ts ================================================ import { unlinkSync } from "node:fs"; import { expect, test } from 'vitest' import { connect, Database, DatabaseRowMutation, DatabaseRowTransformResult } from './promise.js' const localeCompare = (a, b) => a.x.localeCompare(b.x); const intCompare = (a, b) => a.x - b.x; function cleanup(path) { unlinkSync(path); unlinkSync(`${path}-wal`); unlinkSync(`${path}-info`); unlinkSync(`${path}-changes`); try { unlinkSync(`${path}-wal-revert`) } catch (e) { } } test('partial sync concurrency', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, }); await db.exec("CREATE TABLE IF NOT EXISTS partial(value BLOB)"); await db.exec("DELETE FROM partial"); await db.exec("INSERT INTO partial SELECT randomblob(1024) FROM generate_series(1, 2000)"); await db.push(); await db.close(); } const dbs = []; for (let i = 0; i < 16; i++) { dbs.push(await connect({ path: 'partial-1.db', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, partialSyncExperimental: { bootstrapStrategy: { kind: 'prefix', length: 128 * 1024 }, segmentSize: 128 * 1024, }, })); } const qs = []; for (const db of dbs) { qs.push(db.prepare("SELECT COUNT(*) as cnt FROM partial").all()); } const values = await Promise.all(qs); expect(values).toEqual(new Array(16).fill([{ cnt: 2000 }])) }) test('partial sync (prefix bootstrap strategy)', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, }); await db.exec("CREATE TABLE IF NOT EXISTS partial(value BLOB)"); await db.exec("DELETE FROM partial"); await db.exec("INSERT INTO partial SELECT randomblob(1024) FROM generate_series(1, 2000)"); await db.push(); await db.close(); } const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, partialSyncExperimental: { bootstrapStrategy: { kind: 'prefix', length: 128 * 1024 }, segmentSize: 4096, }, }); // 128kb plus some overhead expect((await db.stats()).networkReceivedBytes).toBeLessThanOrEqual(128 * (1024 + 128)); // select of one record shouldn't increase amount of received data expect(await db.prepare("SELECT length(value) as length FROM partial LIMIT 1").all()).toEqual([{ length: 1024 }]); expect((await db.stats()).networkReceivedBytes).toBeLessThanOrEqual(128 * (1024 + 128)); await db.prepare("INSERT INTO partial VALUES (-1)").run(); expect(await db.prepare("SELECT COUNT(*) as cnt FROM partial").all()).toEqual([{ cnt: 2001 }]); expect((await db.stats()).networkReceivedBytes).toBeGreaterThanOrEqual(2000 * 1024); }) test('partial sync (prefix bootstrap strategy; large segment size)', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, }); await db.exec("CREATE TABLE IF NOT EXISTS partial(value BLOB)"); await db.exec("DELETE FROM partial"); await db.exec("INSERT INTO partial SELECT randomblob(1024) FROM generate_series(1, 2000)"); await db.push(); await db.close(); } const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, partialSyncExperimental: { bootstrapStrategy: { kind: 'prefix', length: 128 * 1024 }, segmentSize: 128 * 1024, }, }); // 128kb plus some overhead expect((await db.stats()).networkReceivedBytes).toBeLessThanOrEqual(128 * (1024 + 128)); const startLast = performance.now(); // select of one record shouldn't increase amount of received data expect(await db.prepare("SELECT length(value) as length FROM partial LIMIT 1").all()).toEqual([{ length: 1024 }]); console.info('select last', 'elapsed', performance.now() - startLast); expect((await db.stats()).networkReceivedBytes).toBeLessThanOrEqual(2 * 128 * (1024 + 128)); await db.prepare("INSERT INTO partial VALUES (-1)").run(); const startAll = performance.now(); expect(await db.prepare("SELECT COUNT(*) as cnt FROM partial").all()).toEqual([{ cnt: 2001 }]); console.info('select all', 'elapsed', performance.now() - startAll); expect((await db.stats()).networkReceivedBytes).toBeGreaterThanOrEqual(2000 * 1024); }) test('partial sync (prefix bootstrap strategy; prefetch)', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, }); await db.exec("CREATE TABLE IF NOT EXISTS partial(value BLOB)"); await db.exec("DELETE FROM partial"); await db.exec("INSERT INTO partial SELECT randomblob(1024) FROM generate_series(1, 2000)"); await db.push(); await db.close(); } const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, partialSyncExperimental: { bootstrapStrategy: { kind: 'prefix', length: 128 * 1024 }, segmentSize: 4 * 1024, prefetch: true, }, }); // 128kb plus some overhead expect((await db.stats()).networkReceivedBytes).toBeLessThanOrEqual(128 * (1024 + 128)); const startLast = performance.now(); // select of one record shouldn't increase amount of received data expect(await db.prepare("SELECT length(value) as length FROM partial LIMIT 1").all()).toEqual([{ length: 1024 }]); console.info('select last', 'elapsed', performance.now() - startLast); expect((await db.stats()).networkReceivedBytes).toBeLessThanOrEqual(10 * 128 * (1024 + 128)); await db.prepare("INSERT INTO partial VALUES (-1)").run(); const startAll = performance.now(); expect(await db.prepare("SELECT COUNT(*) as cnt FROM partial").all()).toEqual([{ cnt: 2001 }]); console.info('select all', 'elapsed', performance.now() - startAll); expect((await db.stats()).networkReceivedBytes).toBeGreaterThanOrEqual(2000 * 1024); }) test('partial sync (query bootstrap strategy)', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, }); await db.exec("CREATE TABLE IF NOT EXISTS partial_keyed(key INTEGER PRIMARY KEY, value BLOB)"); await db.exec("DELETE FROM partial_keyed"); await db.exec("INSERT INTO partial_keyed SELECT value, randomblob(1024) FROM generate_series(1, 2000)"); await db.push(); await db.close(); } const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, partialSyncExperimental: { bootstrapStrategy: { kind: 'query', query: 'SELECT * FROM partial_keyed WHERE key = 1000' }, segmentSize: 4096, }, }); // we must sync only few pages expect((await db.stats()).networkReceivedBytes).toBeLessThanOrEqual(10 * (4096 + 128)); // select of one record shouldn't increase amount of received data by a lot expect(await db.prepare("SELECT length(value) as length FROM partial_keyed LIMIT 1").all()).toEqual([{ length: 1024 }]); expect((await db.stats()).networkReceivedBytes).toBeLessThanOrEqual(10 * (4096 + 128)); await db.prepare("INSERT INTO partial_keyed VALUES (-1, -1)").run(); const n1 = await db.stats(); // same as bootstrap query - we shouldn't bring any more pages expect(await db.prepare("SELECT length(value) as length FROM partial_keyed WHERE key = 1000").all()).toEqual([{ length: 1024 }]); const n2 = await db.stats(); expect(n1.networkReceivedBytes).toEqual(n2.networkReceivedBytes); }) test('concurrent-actions-consistency', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, }); await db.exec("CREATE TABLE IF NOT EXISTS rows(key TEXT PRIMARY KEY, value INTEGER)"); await db.exec("DELETE FROM rows"); await db.exec("INSERT INTO rows VALUES ('key', 0)"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); console.info('run_info', await db1.prepare("SELECT * FROM sqlite_master").all()); await db1.exec("PRAGMA busy_timeout=100"); const pull = async function (iterations: number) { for (let i = 0; i < iterations; i++) { console.info('pull', i); try { await db1.pull(); } catch (e) { console.error('pull', e); } await new Promise(resolve => setTimeout(resolve, 0)); } } const push = async function (iterations: number) { for (let i = 0; i < iterations; i++) { await new Promise(resolve => setTimeout(resolve, (Math.random() + 1))); console.info('push', i); try { if ((await db1.stats()).cdcOperations > 0) { const start = performance.now(); await db1.push(); console.info('push', performance.now() - start); } } catch (e) { console.error('push', e); } } } const run = async function (iterations: number) { let rows = 0; for (let i = 0; i < iterations; i++) { // console.info('run', i, rows); // console.info('run_info', 'update', 'start'); await db1.prepare("UPDATE rows SET value = value + 1 WHERE key = ?").run('key'); // console.info('run_info', 'update', 'end'); rows += 1; // console.info('run_info', 'select', 'start'); const { cnt } = await db1.prepare("SELECT value as cnt FROM rows WHERE key = ?").get(['key']); // console.info('run_info', 'select', 'end', cnt, '(', rows, ')'); expect(cnt).toBe(rows); await new Promise(resolve => setTimeout(resolve, 10 * (Math.random() + 1))); } } await Promise.all([pull(100), push(100), run(200)]); }) test('simple-db', async () => { const db = new Database({ path: ':memory:' }); expect(await db.prepare("SELECT 1 as x").all()).toEqual([{ x: 1 }]) await db.exec("CREATE TABLE t(x)"); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]) await expect(async () => await db.pull()).rejects.toThrowError(/sync is disabled as database was opened without sync support/); }) test('implicit connect', async () => { const db = new Database({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); const defer = db.prepare("SELECT * FROM not_found"); await expect(async () => await defer.all()).rejects.toThrowError(/no such table: not_found/); expect(() => db.prepare("SELECT * FROM not_found")).toThrowError(/no such table: not_found/); expect(await db.prepare("SELECT 1 as x").all()).toEqual([{ x: 1 }]); }) test('defered sync', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("DELETE FROM t"); await db.exec("INSERT INTO t VALUES (100)"); await db.push(); await db.close(); } let url = null; const db = new Database({ path: ':memory:', url: () => url }); await db.prepare("CREATE TABLE t(x)").run(); await db.prepare("INSERT INTO t VALUES (1), (2), (3), (42)").run(); expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 42 }]); await expect(async () => await db.pull()).rejects.toThrow(/url is empty - sync is paused/); url = process.env.VITE_TURSO_DB_URL; await db.pull(); expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 100 }, { x: 1 }, { x: 2 }, { x: 3 }, { x: 42 }]); }) test('encryption sync', async () => { const KEY = 'l/FWopMfZisTLgBX4A42AergrCrYKjiO3BfkJUwv83I='; const URL = 'http://encrypted--a--a.localhost:10000'; { const db = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("DELETE FROM t"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); const db2 = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); await db1.exec("INSERT INTO t VALUES (1), (2), (3)"); await db2.exec("INSERT INTO t VALUES (4), (5), (6)"); expect(await db1.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]); expect(await db2.prepare("SELECT * FROM t").all()).toEqual([{ x: 4 }, { x: 5 }, { x: 6 }]); await Promise.all([db1.push(), db2.push()]); await Promise.all([db1.pull(), db2.pull()]); const expected = [{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }, { x: 5 }, { x: 6 }]; expect((await db1.prepare("SELECT * FROM t").all()).sort(intCompare)).toEqual(expected.sort(intCompare)); expect((await db2.prepare("SELECT * FROM t").all()).sort(intCompare)).toEqual(expected.sort(intCompare)); }); test('defered encryption sync', async () => { const URL = 'http://encrypted--a--a.localhost:10000'; const KEY = 'l/FWopMfZisTLgBX4A42AergrCrYKjiO3BfkJUwv83I='; let url = null; { const db = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("DELETE FROM t"); await db.exec("INSERT INTO t VALUES (100)"); await db.push(); await db.close(); } const db = await connect({ path: ':memory:', url: () => url, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]); url = URL; await db.pull(); const expected = [{ x: 100 }, { x: 1 }, { x: 2 }, { x: 3 }]; expect((await db.prepare("SELECT * FROM t").all())).toEqual(expected); }); test('select-after-push', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("DELETE FROM t"); await db.push(); await db.close(); } { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); await db.push(); } { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); const rows = await db.prepare('SELECT * FROM t').all(); expect(rows).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]) } }) test('select-without-push', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("DELETE FROM t"); await db.push(); await db.close(); } { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); } { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); const rows = await db.prepare('SELECT * FROM t').all(); expect(rows).toEqual([]) } }) test('merge-non-overlapping-keys', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2')"); const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db2.exec("INSERT INTO q VALUES ('k3', 'value3'), ('k4', 'value4'), ('k5', 'value5')"); await Promise.all([db1.push(), db2.push()]); await Promise.all([db1.pull(), db2.pull()]); const rows1 = await db1.prepare('SELECT * FROM q').all(); const rows2 = await db1.prepare('SELECT * FROM q').all(); const expected = [{ x: 'k1', y: 'value1' }, { x: 'k2', y: 'value2' }, { x: 'k3', y: 'value3' }, { x: 'k4', y: 'value4' }, { x: 'k5', y: 'value5' }]; expect(rows1.sort(localeCompare)).toEqual(expected.sort(localeCompare)) expect(rows2.sort(localeCompare)).toEqual(expected.sort(localeCompare)) }) test('last-push-wins', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2'), ('k4', 'value4')"); const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db2.exec("INSERT INTO q VALUES ('k1', 'value3'), ('k2', 'value4'), ('k3', 'value5')"); await db2.push(); await db1.push(); await Promise.all([db1.pull(), db2.pull()]); const rows1 = await db1.prepare('SELECT * FROM q').all(); const rows2 = await db1.prepare('SELECT * FROM q').all(); const expected = [{ x: 'k1', y: 'value1' }, { x: 'k2', y: 'value2' }, { x: 'k3', y: 'value5' }, { x: 'k4', y: 'value4' }]; expect(rows1.sort(localeCompare)).toEqual(expected.sort(localeCompare)) expect(rows2.sort(localeCompare)).toEqual(expected.sort(localeCompare)) }) test('last-push-wins-with-delete', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2'), ('k4', 'value4')"); await db1.exec("DELETE FROM q") const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db2.exec("INSERT INTO q VALUES ('k1', 'value3'), ('k2', 'value4'), ('k3', 'value5')"); await db2.push(); await db1.push(); await Promise.all([db1.pull(), db2.pull()]); const rows1 = await db1.prepare('SELECT * FROM q').all(); const rows2 = await db1.prepare('SELECT * FROM q').all(); const expected = [{ x: 'k3', y: 'value5' }]; expect(rows1).toEqual(expected) expect(rows2).toEqual(expected) }) test('constraint-conflict', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS u(x TEXT PRIMARY KEY, y UNIQUE)"); await db.exec("DELETE FROM u"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db1.exec("INSERT INTO u VALUES ('k1', 'value1')"); const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db2.exec("INSERT INTO u VALUES ('k2', 'value1')"); await db1.push(); await expect(async () => await db2.push()).rejects.toThrow('SQLite error: UNIQUE constraint failed: u.y'); }) test('checkpoint', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); for (let i = 0; i < 1000; i++) { await db1.exec(`INSERT INTO q VALUES ('k${i}', 'v${i}')`); } expect((await db1.stats()).mainWalSize).toBeGreaterThan(4096 * 1000); await db1.checkpoint(); expect((await db1.stats()).mainWalSize).toBe(0); let revertWal = (await db1.stats()).revertWalSize; expect(revertWal).toBeLessThan(4096 * 1000 / 50); for (let i = 0; i < 1000; i++) { await db1.exec(`UPDATE q SET y = 'u${i}' WHERE x = 'k${i}'`); } await db1.checkpoint(); expect((await db1.stats()).revertWalSize).toBe(revertWal); }) test('persistence-push', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const path = `test-${(Math.random() * 10000) | 0}.db`; try { { const db1 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL }); await db1.exec(`INSERT INTO q VALUES ('k1', 'v1')`); await db1.exec(`INSERT INTO q VALUES ('k2', 'v2')`); await db1.close(); } { const db2 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL }); await db2.exec(`INSERT INTO q VALUES ('k3', 'v3')`); await db2.exec(`INSERT INTO q VALUES ('k4', 'v4')`); const stmt = db2.prepare('SELECT * FROM q'); const rows = await stmt.all(); const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }, { x: 'k3', y: 'v3' }, { x: 'k4', y: 'v4' }]; expect(rows).toEqual(expected) stmt.close(); await db2.close(); } { const db3 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL }); await db3.push(); await db3.close(); } { const db4 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL }); const rows = await db4.prepare('SELECT * FROM q').all(); const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }, { x: 'k3', y: 'v3' }, { x: 'k4', y: 'v4' }]; expect(rows).toEqual(expected) await db4.close(); } } finally { cleanup(path); } }) test('persistence-offline', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const path = `test-${(Math.random() * 10000) | 0}.db`; try { { const db = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL }); await db.exec(`INSERT INTO q VALUES ('k1', 'v1')`); await db.exec(`INSERT INTO q VALUES ('k2', 'v2')`); await db.push(); await db.close(); } { const db = await connect({ path: path, url: "https://not-valid-url.localhost" }); const rows = await db.prepare("SELECT * FROM q").all(); const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }]; expect(rows.sort(localeCompare)).toEqual(expected.sort(localeCompare)) await db.close(); } } finally { cleanup(path); } }) test('persistence-pull-push', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const path1 = `test-${(Math.random() * 10000) | 0}.db`; const path2 = `test-${(Math.random() * 10000) | 0}.db`; try { const db1 = await connect({ path: path1, url: process.env.VITE_TURSO_DB_URL }); await db1.exec(`INSERT INTO q VALUES ('k1', 'v1')`); await db1.exec(`INSERT INTO q VALUES ('k2', 'v2')`); const stats1 = await db1.stats(); const db2 = await connect({ path: path2, url: process.env.VITE_TURSO_DB_URL }); await db2.exec(`INSERT INTO q VALUES ('k3', 'v3')`); await db2.exec(`INSERT INTO q VALUES ('k4', 'v4')`); await Promise.all([db1.push(), db2.push()]); await Promise.all([db1.pull(), db2.pull()]); const stats2 = await db1.stats(); console.info(stats1, stats2); expect(stats1.revision).not.toBe(stats2.revision); const rows1 = await db1.prepare('SELECT * FROM q').all(); const rows2 = await db2.prepare('SELECT * FROM q').all(); const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }, { x: 'k3', y: 'v3' }, { x: 'k4', y: 'v4' }]; expect(rows1.sort(localeCompare)).toEqual(expected.sort(localeCompare)) expect(rows2.sort(localeCompare)).toEqual(expected.sort(localeCompare)) } finally { cleanup(path1); cleanup(path2); } }) test('update', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 5000 }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("INSERT INTO q VALUES ('1', '2')") await db.push(); await db.exec("INSERT INTO q VALUES ('1', '2') ON CONFLICT DO UPDATE SET y = '3'") await db.push(); }) test('concurrent-updates', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 5000 }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, }); await db1.exec("PRAGMA busy_timeout=100"); async function pull(db) { try { await db.pull(); } catch (e) { console.error('pull error', e); } finally { console.error('pull ok'); setTimeout(async () => await pull(db), 0); } } async function push(db) { try { await db.push(); } catch (e) { console.error('push error', e); } finally { console.error('push ok'); setTimeout(async () => await push(db), 0); } } setTimeout(async () => await pull(db1), 0) setTimeout(async () => await push(db1), 0) for (let i = 0; i < 1000; i++) { try { await Promise.all([ db1.exec(`INSERT INTO q VALUES ('1', 0) ON CONFLICT DO UPDATE SET y = randomblob(1024)`), db1.exec(`INSERT INTO q VALUES ('1', 0) ON CONFLICT DO UPDATE SET y = randomblob(1024)`) ]); console.info('insert ok'); } catch (e) { console.error('insert error', e); } await new Promise(resolve => setTimeout(resolve, 1)); } }) test('corruption-bug-1', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 5000 }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, }); for (let i = 0; i < 100; i++) { await db1.exec(`INSERT INTO q VALUES ('1', 0) ON CONFLICT DO UPDATE SET y = randomblob(1024)`); } await db1.pull(); await db1.push(); for (let i = 0; i < 100; i++) { await db1.exec(`INSERT INTO q VALUES ('1', 0) ON CONFLICT DO UPDATE SET y = randomblob(1024)`); } await db1.pull(); await db1.push(); }) test('pull-push-concurrent', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 5000 }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } let pullResolve = null; const pullFinish = new Promise(resolve => pullResolve = resolve); let pushResolve = null; const pushFinish = new Promise(resolve => pushResolve = resolve); let stopPull = false; let stopPush = false; const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); let pull = async () => { try { await db.pull(); } catch (e) { console.error('pull', e); } finally { if (!stopPull) { setTimeout(pull, 0); } else { pullResolve() } } } let push = async () => { try { if ((await db.stats()).cdcOperations > 0) { await db.push(); } } catch (e) { console.error('push', e); } finally { if (!stopPush) { setTimeout(push, 0); } else { pushResolve(); } } } setTimeout(pull, 0); setTimeout(push, 0); for (let i = 0; i < 1000; i++) { await db.exec(`INSERT INTO q VALUES ('k${i}', 'v${i}')`); } await new Promise(resolve => setTimeout(resolve, 1000)); stopPush = true; await pushFinish; stopPull = true; await pullFinish; console.info(await db.stats()); }) test('checkpoint-and-actions', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, }); await db.exec("CREATE TABLE IF NOT EXISTS rows(key TEXT PRIMARY KEY, value INTEGER)"); await db.exec("DELETE FROM rows"); await db.exec("INSERT INTO rows VALUES ('key', 0)"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db1.exec("PRAGMA busy_timeout=100"); const pull = async function (iterations: number) { for (let i = 0; i < iterations; i++) { try { await db1.pull(); } catch (e) { console.error('pull', e); } await new Promise(resolve => setTimeout(resolve, 0)); } } const push = async function (iterations: number) { for (let i = 0; i < iterations; i++) { await new Promise(resolve => setTimeout(resolve, 5)); try { if ((await db1.stats()).cdcOperations > 0) { const start = performance.now(); await db1.push(); console.info('push', performance.now() - start); } } catch (e) { console.error('push', e); } } } let rows = 0; const run = async function (iterations: number) { for (let i = 0; i < iterations; i++) { await db1.prepare("UPDATE rows SET value = value + 1 WHERE key = ?").run('key'); rows += 1; const { cnt } = await db1.prepare("SELECT value as cnt FROM rows WHERE key = ?").get(['key']); console.info('CHECK', cnt, rows); expect(cnt).toBe(rows); await new Promise(resolve => setTimeout(resolve, 10 * (1 + Math.random()))); } } // await run(100); await Promise.all([pull(40), push(40), run(100)]); }) test('transform', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, }); await db.exec("CREATE TABLE IF NOT EXISTS counter(key TEXT PRIMARY KEY, value INTEGER)"); await db.exec("DELETE FROM counter"); await db.exec("INSERT INTO counter VALUES ('1', 0)") await db.push(); await db.close(); } const transform = (m: DatabaseRowMutation) => ({ operation: 'rewrite', stmt: { sql: `UPDATE counter SET value = value + ? WHERE key = ?`, values: [m.after.value - m.before.value, m.after.key] } } as DatabaseRowTransformResult); const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform }); const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform }); await db1.exec("UPDATE counter SET value = value + 1 WHERE key = '1'"); await db2.exec("UPDATE counter SET value = value + 1 WHERE key = '1'"); await Promise.all([db1.push(), db2.push()]); await Promise.all([db1.pull(), db2.pull()]); const rows1 = await db1.prepare('SELECT * FROM counter').all(); const rows2 = await db2.prepare('SELECT * FROM counter').all(); expect(rows1).toEqual([{ key: '1', value: 2 }]); expect(rows2).toEqual([{ key: '1', value: 2 }]); }) test('transform-many', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, }); await db.exec("CREATE TABLE IF NOT EXISTS counter(key TEXT PRIMARY KEY, value INTEGER)"); await db.exec("DELETE FROM counter"); await db.exec("INSERT INTO counter VALUES ('1', 0)") await db.push(); await db.close(); } const transform = (m: DatabaseRowMutation) => ({ operation: 'rewrite', stmt: { sql: `UPDATE counter SET value = value + ? WHERE key = ?`, values: [m.after.value - m.before.value, m.after.key] } } as DatabaseRowTransformResult); const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform }); const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform }); for (let i = 0; i < 1002; i++) { await db1.exec("UPDATE counter SET value = value + 1 WHERE key = '1'"); } for (let i = 0; i < 1001; i++) { await db2.exec("UPDATE counter SET value = value + 1 WHERE key = '1'"); } let start = performance.now(); await Promise.all([db1.push(), db2.push()]); console.info('push', performance.now() - start); start = performance.now(); await Promise.all([db1.pull(), db2.pull()]); console.info('pull', performance.now() - start); const rows1 = await db1.prepare('SELECT * FROM counter').all(); const rows2 = await db2.prepare('SELECT * FROM counter').all(); expect(rows1).toEqual([{ key: '1', value: 1001 + 1002 }]); expect(rows2).toEqual([{ key: '1', value: 1001 + 1002 }]); }) ================================================ FILE: bindings/javascript/sync/packages/native/promise.ts ================================================ import { DatabasePromise } from "@tursodatabase/database-common" import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards, Runner, runner, RemoteWriter, RemoteWriteStatement } from "@tursodatabase/sync-common"; import { SyncEngine, SyncEngineProtocolVersion, Database as NativeDatabase } from "#index"; import { promises } from "node:fs"; let NodeIO: ProtocolIo = { async read(path: string): Promise { try { return await promises.readFile(path); } catch (error) { if (error.code === 'ENOENT') { return null; } throw error; } }, async write(path: string, data: Buffer | Uint8Array): Promise { const unix = Math.floor(Date.now() / 1000); const nonce = Math.floor(Math.random() * 1000000000); const tmp = `${path}.tmp.${unix}.${nonce}`; await promises.writeFile(tmp, new Uint8Array(data)); try { await promises.rename(tmp, path); } catch (err) { await promises.unlink(tmp); throw err; } } }; function memoryIO(): ProtocolIo { let values = new Map(); return { async read(path: string): Promise { return values.get(path); }, async write(path: string, data: Buffer | Uint8Array): Promise { values.set(path, data); } } }; function resolveUrl(url: string | (() => string | null)): string { if (typeof url === "function") { const resolved = url(); if (resolved == null) { throw new Error("remoteWritesExperimental requires a non-null URL"); } return resolved; } return url; } class Database extends DatabasePromise { #engine: any; #guards: SyncEngineGuards; #runner: Runner; #remoteWriter: RemoteWriter | null = null; #db: any; constructor(opts: DatabaseOpts) { if (opts.url == null) { const db = new NativeDatabase(opts.path, { tracing: opts.tracing }) as any; super(db); this.#db = db; this.#engine = null; return; } let partialSyncOpts = undefined; if (opts.partialSyncExperimental != null) { switch (opts.partialSyncExperimental.bootstrapStrategy.kind) { case "prefix": partialSyncOpts = { bootstrapStrategy: { type: "Prefix", length: opts.partialSyncExperimental.bootstrapStrategy.length }, segmentSize: opts.partialSyncExperimental.segmentSize, prefetch: opts.partialSyncExperimental.prefetch, }; break; case "query": partialSyncOpts = { bootstrapStrategy: { type: "Query", query: opts.partialSyncExperimental.bootstrapStrategy.query }, segmentSize: opts.partialSyncExperimental.segmentSize, prefetch: opts.partialSyncExperimental.prefetch, }; break; } } const engine = new SyncEngine({ path: opts.path, clientName: opts.clientName, useTransform: opts.transform != null, protocolVersion: SyncEngineProtocolVersion.V1, longPollTimeoutMs: opts.longPollTimeoutMs, tracing: opts.tracing, bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null, remoteEncryptionCipher: opts.remoteEncryption?.cipher, remoteEncryptionKey: opts.remoteEncryption?.key, partialSyncOpts: partialSyncOpts }); let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>); if (typeof opts.authToken == "function") { const authToken = opts.authToken; headers = async () => ({ ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }), ...(opts.remoteEncryption != null && { "x-turso-encryption-key": opts.remoteEncryption.key, "x-turso-encryption-cipher": opts.remoteEncryption.cipher, }) }); } else { const authToken = opts.authToken; headers = { ...(opts.authToken != null && { "Authorization": `Bearer ${authToken}` }), ...(opts.remoteEncryption != null && { "x-turso-encryption-key": opts.remoteEncryption.key, "x-turso-encryption-cipher": opts.remoteEncryption.cipher, }) }; } const runOpts = { url: opts.url, headers: headers, preemptionMs: 1, transform: opts.transform, }; const db = engine.db() as unknown as any; const memory = db.memory; const io = memory ? memoryIO() : NodeIO; const run = runner(runOpts, io, engine); super(engine.db() as unknown as any, () => run.wait()); this.#db = engine.db() as unknown as any; this.#runner = run; this.#engine = engine; this.#guards = new SyncEngineGuards(); // Initialize remote writer if remoteWrites is enabled if (opts.remoteWritesExperimental && opts.url) { const url = resolveUrl(opts.url); this.#remoteWriter = new RemoteWriter({ url, authToken: opts.authToken, remoteEncryptionKey: opts.remoteEncryption?.key, }); } } /** * connect database and initialize it in case of clean start */ override async connect() { if (this.connected) { return; } else if (this.#engine == null) { await super.connect(); } else { await run(this.#runner, this.#engine.connect()); } this.connected = true; } /** * pull new changes from the remote database * if {@link DatabaseOpts.longPollTimeoutMs} is set - then server will hold the connection open until either new changes will appear in the database or timeout occurs. * @returns true if new changes were pulled from the remote */ async pull() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } const changes = await this.#guards.wait(async () => await run(this.#runner, this.#engine.wait())); if (changes.empty()) { return false; } await this.#guards.apply(async () => await run(this.#runner, this.#engine.apply(changes))); return true; } /** * push new local changes to the remote database * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote */ async push() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } await this.#guards.push(async () => await run(this.#runner, this.#engine.push())); } /** * checkpoint WAL for local database */ async checkpoint() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } await this.#guards.checkpoint(async () => await run(this.#runner, this.#engine.checkpoint())); } /** * @returns statistic of current local database */ async stats(): Promise { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } return (await run(this.#runner, this.#engine.stats())); } /** * Executes the given SQL string. * When remoteWrites is enabled, write statements are sent to the remote server. */ override async exec(sql: string) { if (!this.#remoteWriter) return super.exec(sql); const category = this.#db.classifySql(sql); if (this.#remoteWriter.isInTransaction) { const { shouldPull } = await this.#remoteWriter.execRemote(sql, category); if (shouldPull) await this.pull(); return; } if (category === "read") return super.exec(sql); const { shouldPull } = await this.#remoteWriter.execRemote(sql, category); if (shouldPull) await this.pull(); } /** * Prepares a SQL statement for execution. * When remoteWrites is enabled, returns a wrapper that routes writes to remote. */ override prepare(sql: string) { const localStmt = super.prepare(sql); if (!this.#remoteWriter) { return localStmt; } const category = this.#db.classifySql(sql); const isReadonly = category === "read"; return new RemoteWriteStatement( localStmt, sql, isReadonly, this.#remoteWriter, () => this.pull(), ) as any; } /** * Returns a function that executes the given function in a transaction. * When remoteWrites is enabled, the entire transaction goes to remote. */ override transaction(fn: (...any) => Promise) { if (typeof fn !== "function") throw new TypeError("Expected first argument to be a function"); if (!this.#remoteWriter) { return super.transaction(fn); } const db = this; const remoteWriter = this.#remoteWriter; const wrapTxn = (mode: string) => { return async (...bindParameters: any[]) => { await remoteWriter.beginTransaction(mode); try { const result = await fn(...bindParameters); await remoteWriter.commitTransaction(); await db.pull(); return result; } catch (err) { await remoteWriter.rollbackTransaction(); throw err; } }; }; const properties = { default: { value: wrapTxn("") }, deferred: { value: wrapTxn("DEFERRED") }, immediate: { value: wrapTxn("IMMEDIATE") }, exclusive: { value: wrapTxn("EXCLUSIVE") }, database: { value: this, enumerable: true }, }; Object.defineProperties(properties.default.value, properties); Object.defineProperties(properties.deferred.value, properties); Object.defineProperties(properties.immediate.value, properties); Object.defineProperties(properties.exclusive.value, properties); return properties.default.value; } /** * close the database */ override async close(): Promise { if (this.#remoteWriter) { await this.#remoteWriter.close(); } await super.close(); if (this.#engine != null) { this.#engine.close(); } } } /** * Creates a new database connection asynchronously. * * @param {Object} opts - Options for database behavior. * @returns {Promise} - A promise that resolves to a Database instance. */ async function connect(opts: DatabaseOpts): Promise { const db = new Database(opts); await db.connect(); return db; } export { connect, Database } export type { DatabaseOpts, EncryptionOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult } ================================================ FILE: bindings/javascript/sync/packages/native/remote-write.test.ts ================================================ import { expect, test } from 'vitest' import { connect } from './promise.js' const TURSO_URL = process.env.TURSO_DATABASE_URL; const TURSO_TOKEN = process.env.TURSO_AUTH_TOKEN; function localSyncedDbOpts() { if (!TURSO_URL) throw new Error('TURSO_DATABASE_URL env var is required'); return { path: ':memory:', url: TURSO_URL, authToken: TURSO_TOKEN, }; } function remoteWriteOpts() { return { ...localSyncedDbOpts(), remoteWritesExperimental: true, }; } test('remote write: exec insert and read back', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_exec(id INTEGER PRIMARY KEY, value TEXT)"); await seed.exec("DELETE FROM rw_exec"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); await db.exec("INSERT INTO rw_exec VALUES (1, 'hello')"); const rows = await db.prepare("SELECT * FROM rw_exec").all(); expect(rows).toEqual([{ id: 1, value: 'hello' }]); await db.close(); }) test('remote write: prepared statement write', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_prepared(id INTEGER PRIMARY KEY, x INTEGER)"); await seed.exec("DELETE FROM rw_prepared"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); for (let i = 1; i <= 5; i++) { await db.prepare("INSERT INTO rw_prepared(x) VALUES (?)").run([i * 10]); } const rows = await db.prepare("SELECT x FROM rw_prepared ORDER BY x").all(); expect(rows).toEqual([{ x: 10 }, { x: 20 }, { x: 30 }, { x: 40 }, { x: 50 }]); await db.close(); }) test('remote write: reads stay local', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_read(id INTEGER PRIMARY KEY, value TEXT)"); await seed.exec("DELETE FROM rw_read"); await seed.exec("INSERT INTO rw_read VALUES (1, 'local')"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); const rows = await db.prepare("SELECT * FROM rw_read").all(); expect(rows).toEqual([{ id: 1, value: 'local' }]); await db.close(); }) test('remote write: transaction commit', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_txn(id INTEGER PRIMARY KEY, value INTEGER)"); await seed.exec("DELETE FROM rw_txn"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); const txn = db.transaction(async () => { await db.prepare("INSERT INTO rw_txn(value) VALUES (?)").run([1]); await db.prepare("INSERT INTO rw_txn(value) VALUES (?)").run([2]); await db.prepare("INSERT INTO rw_txn(value) VALUES (?)").run([3]); }); await txn(); const rows = await db.prepare("SELECT value FROM rw_txn ORDER BY value").all(); expect(rows).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }]); await db.close(); }) test('remote write: transaction rollback on error', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_rollback(id INTEGER PRIMARY KEY, value INTEGER)"); await seed.exec("DELETE FROM rw_rollback"); await seed.exec("INSERT INTO rw_rollback VALUES (1, 100)"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); const txn = db.transaction(async () => { await db.prepare("INSERT INTO rw_rollback(value) VALUES (?)").run([200]); throw new Error("deliberate rollback"); }); await expect(txn()).rejects.toThrow("deliberate rollback"); const rows = await db.prepare("SELECT value FROM rw_rollback ORDER BY value").all(); expect(rows).toEqual([{ value: 100 }]); await db.close(); }) test('remote write: exec with BEGIN/COMMIT', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_exec_txn(id INTEGER PRIMARY KEY, value TEXT)"); await seed.exec("DELETE FROM rw_exec_txn"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); await db.exec("BEGIN"); await db.exec("INSERT INTO rw_exec_txn VALUES (1, 'a')"); await db.exec("INSERT INTO rw_exec_txn VALUES (2, 'b')"); await db.exec("COMMIT"); const rows = await db.prepare("SELECT value FROM rw_exec_txn ORDER BY id").all(); expect(rows).toEqual([{ value: 'a' }, { value: 'b' }]); await db.close(); }) test('remote write: second client sees writes after pull', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_visible(id INTEGER PRIMARY KEY, value TEXT)"); await seed.exec("DELETE FROM rw_visible"); await seed.push(); await seed.close(); const db1 = await connect(remoteWriteOpts()); await db1.exec("INSERT INTO rw_visible VALUES (1, 'from-remote-write')"); // second client pulls and should see the remote write const db2 = await connect(localSyncedDbOpts()); const rows = await db2.prepare("SELECT * FROM rw_visible").all(); expect(rows).toEqual([{ id: 1, value: 'from-remote-write' }]); await db1.close(); await db2.close(); }) test('remote write: update and delete', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_update(id INTEGER PRIMARY KEY, value TEXT)"); await seed.exec("DELETE FROM rw_update"); await seed.exec("INSERT INTO rw_update VALUES (1, 'original')"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); await db.exec("UPDATE rw_update SET value = 'updated' WHERE id = 1"); let rows = await db.prepare("SELECT value FROM rw_update WHERE id = 1").all(); expect(rows).toEqual([{ value: 'updated' }]); await db.exec("DELETE FROM rw_update WHERE id = 1"); rows = await db.prepare("SELECT COUNT(*) as cnt FROM rw_update").all(); expect(rows).toEqual([{ cnt: 0 }]); await db.close(); }) test('remote write: unique conflict rejected at write time', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_conflict(id INTEGER PRIMARY KEY, key TEXT, value TEXT UNIQUE)"); await seed.exec("DELETE FROM rw_conflict"); await seed.push(); await seed.close(); const db1 = await connect(remoteWriteOpts()); const db2 = await connect(remoteWriteOpts()); // first client inserts successfully await db1.exec("INSERT INTO rw_conflict(key, value) VALUES ('a', 'taken')"); // second client tries the same unique value — must fail at write time, not at push await expect( db2.exec("INSERT INTO rw_conflict(key, value) VALUES ('b', 'taken')") ).rejects.toThrow(/UNIQUE constraint failed/); // both clients see exactly the same single row const rows1 = await db1.prepare("SELECT key, value FROM rw_conflict").all(); const rows2 = await db2.prepare("SELECT key, value FROM rw_conflict").all(); expect(rows1).toEqual([{ key: 'a', value: 'taken' }]); expect(rows2).toEqual([]); await db2.pull(); const rows3 = await db2.prepare("SELECT key, value FROM rw_conflict").all(); expect(rows3).toEqual([{ key: 'a', value: 'taken' }]); await db1.close(); await db2.close(); }) test('remote write: DDL create table and use it', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("DROP TABLE IF EXISTS rw_ddl"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); await db.exec("CREATE TABLE rw_ddl(id INTEGER PRIMARY KEY, name TEXT)"); await db.exec("INSERT INTO rw_ddl(name) VALUES ('after-create')"); const rows = await db.prepare("SELECT name FROM rw_ddl").all(); expect(rows).toEqual([{ name: 'after-create' }]); // second client should see the new table const db2 = await connect(localSyncedDbOpts()); const rows2 = await db2.prepare("SELECT name FROM rw_ddl").all(); expect(rows2).toEqual([{ name: 'after-create' }]); await db.close(); await db2.close(); }) test('remote write: DDL alter table adds column', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("DROP TABLE IF EXISTS rw_alter"); await seed.exec("CREATE TABLE rw_alter(id INTEGER PRIMARY KEY, a TEXT)"); await seed.exec("INSERT INTO rw_alter(a) VALUES ('before')"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); await db.exec("ALTER TABLE rw_alter ADD COLUMN b TEXT DEFAULT 'default_b'"); await db.exec("INSERT INTO rw_alter(a, b) VALUES ('after', 'new_b')"); const rows = await db.prepare("SELECT a, b FROM rw_alter ORDER BY a").all(); expect(rows).toEqual([ { a: 'after', b: 'new_b' }, { a: 'before', b: 'default_b' }, ]); await db.close(); }) test('remote write: DDL drop table', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_drop(id INTEGER PRIMARY KEY)"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); await db.exec("DROP TABLE rw_drop"); await expect(() => db.prepare("SELECT * FROM rw_drop")).toThrow(/no such table/); await db.close(); }) test('remote write: DDL create index', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_idx(id INTEGER PRIMARY KEY, value TEXT)"); await seed.exec("DELETE FROM rw_idx"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); await db.exec("CREATE INDEX IF NOT EXISTS idx_rw_value ON rw_idx(value)"); await db.exec("INSERT INTO rw_idx(value) VALUES ('indexed')"); const rows = await db.prepare("SELECT value FROM rw_idx").all(); expect(rows).toEqual([{ value: 'indexed' }]); await db.close(); }) test('remote write: read-only transaction goes remote, not local', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("CREATE TABLE IF NOT EXISTS rw_read_txn(id INTEGER PRIMARY KEY, value TEXT)"); await seed.exec("DELETE FROM rw_read_txn"); await seed.exec("INSERT INTO rw_read_txn VALUES (1, 'original')"); await seed.push(); await seed.close(); // db1 connects with remote writes — local replica has 'original' const db1 = await connect(remoteWriteOpts()); const localRows = await db1.prepare("SELECT value FROM rw_read_txn").all(); expect(localRows).toEqual([{ value: 'original' }]); // another client updates the remote behind db1's back const db2 = await connect(localSyncedDbOpts()); await db2.exec("UPDATE rw_read_txn SET value = 'updated' WHERE id = 1"); await db2.push(); await db2.close(); // db1's local replica is stale — outside a txn, reads are local const staleRows = await db1.prepare("SELECT value FROM rw_read_txn").all(); expect(staleRows).toEqual([{ value: 'original' }]); // inside a transaction, reads should go remote and see 'updated' await db1.exec("BEGIN"); const txnRows = await db1.prepare("SELECT value FROM rw_read_txn").all(); expect(txnRows[0]).toMatchObject({ value: 'updated' }); await db1.exec("COMMIT"); await db1.close(); }) test('remote write: insert returning', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("DROP TABLE IF EXISTS rw_returning"); await seed.exec("CREATE TABLE rw_returning(id INTEGER PRIMARY KEY, value TEXT)"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); const rows = await db.prepare("INSERT INTO rw_returning(value) VALUES (?) RETURNING id, value").all(['hello']); expect(rows).toMatchObject([{ id: 1, value: 'hello' }]); expect(await db.prepare("SELECT * FROM rw_returning").all()).toMatchObject([{ id: 1, value: 'hello' }]); const rows2 = await db.prepare("INSERT INTO rw_returning(value) VALUES (?) RETURNING id, value").all(['world']); expect(rows2).toMatchObject([{ id: 2, value: 'world' }]); expect(await db.prepare("SELECT * FROM rw_returning").all()).toMatchObject([ { id: 1, value: 'hello' }, { id: 2, value: 'world' }, ]); await db.close(); }) test('remote write: blob round-trip', async () => { const seed = await connect(localSyncedDbOpts()); await seed.exec("DROP TABLE IF EXISTS rw_blob"); await seed.exec("CREATE TABLE rw_blob(id INTEGER PRIMARY KEY, data BLOB)"); await seed.push(); await seed.close(); const db = await connect(remoteWriteOpts()); const blob = Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe, 0xfd]); await db.prepare("INSERT INTO rw_blob(data) VALUES (?)").run([blob]); const rows = await db.prepare("SELECT data FROM rw_blob").all(); expect(Buffer.from(rows[0].data)).toEqual(blob); await db.close(); }) ================================================ FILE: bindings/javascript/sync/packages/native/tsconfig.json ================================================ { "compilerOptions": { "skipLibCheck": true, "declaration": true, "declarationMap": true, "module": "nodenext", "target": "esnext", "outDir": "dist/", "lib": [ "es2020" ], "paths": { "#index": [ "./index.d.ts" ] } }, "include": [ "*" ], "exclude": [ "*.test.ts" ] } ================================================ FILE: bindings/javascript/sync/packages/wasm/README.md ================================================

Turso Database for JavaScript in Browser

npm

Chat with other users of Turso on Discord

--- ## About This package is the Turso embedded database library for JavaScript in Browser. > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ## Features - **SQLite compatible:** SQLite query language and file format support ([status](https://github.com/tursodatabase/turso/blob/main/COMPAT.md)). - **In-process**: No network overhead, runs directly in your Node.js process - **TypeScript support**: Full TypeScript definitions included ## Installation ```bash npm install @tursodatabase/database-wasm ``` ## Getting Started ### In-Memory Database ```javascript import { connect } from '@tursodatabase/database-wasm'; // Create an in-memory database const db = await connect(':memory:'); // Create a table await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)'); // Insert data const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); await insert.run('Alice', 'alice@example.com'); await insert.run('Bob', 'bob@example.com'); // Query data const users = await db.prepare('SELECT * FROM users').all(); console.log(users); // Output: [ // { id: 1, name: 'Alice', email: 'alice@example.com' }, // { id: 2, name: 'Bob', email: 'bob@example.com' } // ] ``` ### File-Based Database ```javascript import { connect } from '@tursodatabase/database-wasm'; // Create or open a database file const db = await connect('my-database.db'); // Create a table await db.exec(` CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Insert a post const insertPost = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)'); const result = await insertPost.run('Hello World', 'This is my first blog post!'); console.log(`Inserted post with ID: ${result.lastInsertRowid}`); ``` ### Transactions ```javascript import { connect } from '@tursodatabase/database-wasm'; const db = await connect('transactions.db'); // Using transactions for atomic operations const transaction = db.transaction(async (users) => { const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); for (const user of users) { await insert.run(user.name, user.email); } }); // Execute transaction await transaction([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' } ]); ``` ## API Reference For complete API documentation, see [JavaScript API Reference](https://github.com/tursodatabase/turso/blob/main/docs/javascript-api-reference.md). ## Related Packages * The [@tursodatabase/serverless](https://www.npmjs.com/package/@tursodatabase/serverless) package provides a serverless driver with the same API. * The [@tursodatabase/sync](https://www.npmjs.com/package/@tursodatabase/sync) package provides bidirectional sync between a local Turso database and Turso Cloud. ## License This project is licensed under the [MIT license](https://github.com/tursodatabase/turso/blob/main/LICENSE.md). ## Support - [GitHub Issues](https://github.com/tursodatabase/turso/issues) - [Documentation](https://docs.turso.tech) - [Discord Community](https://tur.so/discord) ================================================ FILE: bindings/javascript/sync/packages/wasm/cp-entrypoint.sh ================================================ sed 's/index-default.js/index-bundle.js/g' promise-default.ts > promise-bundle.ts sed 's/index-default.js/index-turbopack-hack.js/g' promise-default.ts > promise-turbopack-hack.ts sed 's/index-default.js/index-vite-dev-hack.js/g' promise-default.ts > promise-vite-dev-hack.ts ================================================ FILE: bindings/javascript/sync/packages/wasm/index-bundle.ts ================================================ import { setupMainThread } from "@tursodatabase/database-wasm-common"; //@ts-ignore import TursoWorker from "./worker.js?worker&inline"; export let MainWorker = null; const __wasmUrl = new URL('./sync.wasm32-wasi.wasm', import.meta.url).href; const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer()) const napiModule = await setupMainThread(__wasmFile, () => { const worker = new TursoWorker({ name: 'turso-database-sync', type: 'module', }) MainWorker = worker; return worker }); export default napiModule.exports export const Database = napiModule.exports.Database export const Statement = napiModule.exports.Statement export const Opfs = napiModule.exports.Opfs export const OpfsFile = napiModule.exports.OpfsFile export const connect = napiModule.exports.connect export const initThreadPool = napiModule.exports.initThreadPool export const GeneratorHolder = napiModule.exports.GeneratorHolder export const JsDataCompletion = napiModule.exports.JsDataCompletion export const JsProtocolIo = napiModule.exports.JsProtocolIo export const JsProtocolRequestBytes = napiModule.exports.JsProtocolRequestBytes export const SyncEngine = napiModule.exports.SyncEngine export const DatabaseChangeTypeJs = napiModule.exports.DatabaseChangeTypeJs export const SyncEngineProtocolVersion = napiModule.exports.SyncEngineProtocolVersion ================================================ FILE: bindings/javascript/sync/packages/wasm/index-default.ts ================================================ import { isWebWorker, setupWebWorker, setupMainThread } from "@tursodatabase/database-wasm-common"; export let MainWorker = null; const __wasmUrl = new URL('./sync.wasm32-wasi.wasm', import.meta.url).href; const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer()) const napiModule = await setupMainThread(__wasmFile, () => { const worker = new Worker(new URL('./worker.js', import.meta.url), { name: 'turso-database-sync', type: 'module', }) MainWorker = worker; return worker }); export default napiModule.exports export const Database = napiModule.exports.Database export const Statement = napiModule.exports.Statement export const Opfs = napiModule.exports.Opfs export const OpfsFile = napiModule.exports.OpfsFile export const connect = napiModule.exports.connect export const initThreadPool = napiModule.exports.initThreadPool export const GeneratorHolder = napiModule.exports.GeneratorHolder export const JsDataCompletion = napiModule.exports.JsDataCompletion export const JsProtocolIo = napiModule.exports.JsProtocolIo export const JsProtocolRequestBytes = napiModule.exports.JsProtocolRequestBytes export const SyncEngine = napiModule.exports.SyncEngine export const DatabaseChangeTypeJs = napiModule.exports.DatabaseChangeTypeJs export const SyncEngineProtocolVersion = napiModule.exports.SyncEngineProtocolVersion ================================================ FILE: bindings/javascript/sync/packages/wasm/index-turbopack-hack.ts ================================================ import { setupMainThread } from "@tursodatabase/database-wasm-common"; import { tursoWasm } from "./wasm-inline.js"; // Next (turbopack) has issues with loading wasm module: https://github.com/vercel/next.js/issues/82520 // So, we inline wasm binary in the source code in order to avoid issues with loading it from the file const __wasmFile = await tursoWasm(); export let MainWorker = null; const napiModule = await setupMainThread(__wasmFile, () => { const worker = new Worker(new URL('./worker.js', import.meta.url), { name: 'turso-database-sync', type: 'module', }) MainWorker = worker; return worker }); export default napiModule.exports export const Database = napiModule.exports.Database export const Statement = napiModule.exports.Statement export const Opfs = napiModule.exports.Opfs export const OpfsFile = napiModule.exports.OpfsFile export const connect = napiModule.exports.connect export const initThreadPool = napiModule.exports.initThreadPool export const GeneratorHolder = napiModule.exports.GeneratorHolder export const JsDataCompletion = napiModule.exports.JsDataCompletion export const JsProtocolIo = napiModule.exports.JsProtocolIo export const JsProtocolRequestBytes = napiModule.exports.JsProtocolRequestBytes export const SyncEngine = napiModule.exports.SyncEngine export const DatabaseChangeTypeJs = napiModule.exports.DatabaseChangeTypeJs export const SyncEngineProtocolVersion = napiModule.exports.SyncEngineProtocolVersion ================================================ FILE: bindings/javascript/sync/packages/wasm/index-vite-dev-hack.ts ================================================ import { isWebWorker, setupMainThread, setupWebWorker } from "@tursodatabase/database-wasm-common"; import { tursoWasm } from "./wasm-inline.js"; let napiModule = { exports: { Database: {} as any, Opfs: {} as any, OpfsFile: {} as any, Statement: {} as any, connect: {} as any, initThreadPool: {} as any, GeneratorHolder: {} as any, JsDataCompletion: {} as any, JsProtocolIo: {} as any, JsProtocolRequestBytes: {} as any, SyncEngine: {} as any, DatabaseChangeTypeJs: {} as any, SyncEngineProtocolVersion: {} as any, } }; export let MainWorker = null; if (isWebWorker()) { setupWebWorker(); } else { // Vite has issues with loading wasm modules and worker in dev server: https://github.com/vitejs/vite/issues/8427 // So, the mitigation for dev server only is: // 1. inline wasm binary in the source code in order to avoid issues with loading it from the file // 2. use same file as worker entry point const __wasmFile = await tursoWasm(); napiModule = await setupMainThread(__wasmFile, () => { const worker = new Worker(import.meta.url, { name: 'turso-database-sync', type: 'module', }) MainWorker = worker; return worker }); } export default napiModule.exports export const Database = napiModule.exports.Database export const Statement = napiModule.exports.Statement export const Opfs = napiModule.exports.Opfs export const OpfsFile = napiModule.exports.OpfsFile export const connect = napiModule.exports.connect export const initThreadPool = napiModule.exports.initThreadPool export const GeneratorHolder = napiModule.exports.GeneratorHolder export const JsDataCompletion = napiModule.exports.JsDataCompletion export const JsProtocolIo = napiModule.exports.JsProtocolIo export const JsProtocolRequestBytes = napiModule.exports.JsProtocolRequestBytes export const SyncEngine = napiModule.exports.SyncEngine export const DatabaseChangeTypeJs = napiModule.exports.DatabaseChangeTypeJs export const SyncEngineProtocolVersion = napiModule.exports.SyncEngineProtocolVersion ================================================ FILE: bindings/javascript/sync/packages/wasm/package.json ================================================ { "name": "@tursodatabase/sync-wasm", "version": "0.6.0-pre.4", "repository": { "type": "git", "url": "https://github.com/tursodatabase/turso" }, "type": "module", "license": "MIT", "main": "dist/promise.js", "packageManager": "yarn@4.9.2", "files": [ "dist/**", "bundle/**", "README.md" ], "exports": { ".": { "default": "./dist/promise-default.js" }, "./bundle": { "default": "./bundle/main.es.js" }, "./vite": { "development": "./dist/promise-vite-dev-hack.js", "default": "./dist/promise-default.js" }, "./turbopack": { "default": "./dist/promise-turbopack-hack.js" } }, "devDependencies": { "@napi-rs/cli": "^3.1.5", "@vitest/browser": "^3.2.4", "playwright": "^1.57.0", "typescript": "^5.9.2", "vite": "^7.1.5", "vitest": "^3.2.4" }, "scripts": { "napi-build": "napi build --features browser --profile release-official --platform --target wasm32-wasip1-threads --no-js --manifest-path ../../Cargo.toml --output-dir . && rm index.d.ts sync.wasi* wasi* browser.js", "tsc-build": "npm exec tsc && cp sync.wasm32-wasi.wasm ./dist/sync.wasm32-wasi.wasm && WASM_FILE=sync.wasm32-wasi.wasm JS_FILE=./dist/wasm-inline.js node ../../../scripts/inline-wasm-base64.js && npm run bundle", "bundle": "vite build", "build": "npm run napi-build && npm run tsc-build", "test": "VITE_TURSO_DB_URL=http://f--a--a.localhost:10000 CI=1 vitest --testTimeout 30000 --browser=chromium --run && VITE_TURSO_DB_URL=http://f--a--a.localhost:10000 CI=1 vitest --testTimeout 30000 --browser=firefox --run" }, "napi": { "binaryName": "sync", "targets": [ "wasm32-wasip1-threads" ] }, "imports": { "#index": "./index.js" }, "dependencies": { "@tursodatabase/database-common": "^0.6.0-pre.4", "@tursodatabase/database-wasm-common": "^0.6.0-pre.4", "@tursodatabase/sync-common": "^0.6.0-pre.4" } } ================================================ FILE: bindings/javascript/sync/packages/wasm/promise-bundle.ts ================================================ import { registerFileAtWorker, unregisterFileAtWorker, ioNotifier } from "@tursodatabase/database-wasm-common" import { DatabasePromise } from "@tursodatabase/database-common" import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards, Runner, runner, RemoteWriter, RemoteWriteStatement } from "@tursodatabase/sync-common"; import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker, Database as NativeDatabase } from "./index-bundle.js"; let BrowserIO: ProtocolIo = { async read(path: string): Promise { const result = localStorage.getItem(path); if (result == null) { return null; } return new TextEncoder().encode(result); }, async write(path: string, data: Buffer | Uint8Array): Promise { const array = new Uint8Array(data); const value = new TextDecoder('utf-8').decode(array); localStorage.setItem(path, value); } }; function memoryIO(): ProtocolIo { let values = new Map(); return { async read(path: string): Promise { return values.get(path); }, async write(path: string, data: Buffer | Uint8Array): Promise { values.set(path, data); } } }; async function init(): Promise { await initThreadPool(); if (MainWorker == null) { throw new Error("panic: MainWorker is not initialized"); } return MainWorker; } function resolveUrl(url: string | (() => string | null)): string { if (typeof url === "function") { const resolved = url(); if (resolved == null) { throw new Error("remoteWritesExperimental requires a non-null URL"); } return resolved; } return url; } class Database extends DatabasePromise { #runner: Runner; #engine: any; #io: ProtocolIo; #guards: SyncEngineGuards; #worker: Worker | null; #remoteWriter: RemoteWriter | null = null; #db: any; constructor(opts: DatabaseOpts) { if (opts.url == null) { const db = new NativeDatabase(opts.path, { tracing: opts.tracing }) as any; super( db, () => ioNotifier.waitForCompletion(), ); this.#db = db; this.#engine = null; return; } let partialSyncOpts = undefined; if (opts.partialSyncExperimental != null) { switch (opts.partialSyncExperimental.bootstrapStrategy.kind) { case "prefix": partialSyncOpts = { bootstrapStrategy: { type: "Prefix", length: opts.partialSyncExperimental.bootstrapStrategy.length }, segmentSize: opts.partialSyncExperimental.segmentSize, prefetch: opts.partialSyncExperimental.prefetch, }; break; case "query": partialSyncOpts = { bootstrapStrategy: { type: "Query", query: opts.partialSyncExperimental.bootstrapStrategy.query }, segmentSize: opts.partialSyncExperimental.segmentSize, prefetch: opts.partialSyncExperimental.prefetch, }; break; } } const engine = new SyncEngine({ path: opts.path, clientName: opts.clientName, useTransform: opts.transform != null, protocolVersion: SyncEngineProtocolVersion.V1, longPollTimeoutMs: opts.longPollTimeoutMs, tracing: opts.tracing, bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null, remoteEncryption: opts.remoteEncryption?.cipher, partialSyncOpts: partialSyncOpts }); let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>); if (typeof opts.authToken == "function") { const authToken = opts.authToken; headers = async () => ({ ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }), ...(opts.remoteEncryption != null && { "x-turso-encryption-key": opts.remoteEncryption.key, "x-turso-encryption-cipher": opts.remoteEncryption.cipher, }) }); } else { const authToken = opts.authToken; headers = { ...(opts.authToken != null && { "Authorization": `Bearer ${authToken}` }), ...(opts.remoteEncryption != null && { "x-turso-encryption-key": opts.remoteEncryption.key, "x-turso-encryption-cipher": opts.remoteEncryption.cipher, }) }; } const runOpts = { url: opts.url, headers: headers, preemptionMs: 1, transform: opts.transform, }; const db = engine.db() as unknown as any; const memory = db.memory; const io = memory ? memoryIO() : BrowserIO; const run = runner(runOpts, io, engine); super(db, () => run.wait()); this.#db = db; this.#runner = run; this.#engine = engine; this.#io = io; this.#guards = new SyncEngineGuards(); // Initialize remote writer if remoteWrites is enabled if (opts.remoteWritesExperimental && opts.url) { const url = resolveUrl(opts.url); this.#remoteWriter = new RemoteWriter({ url, authToken: opts.authToken, remoteEncryptionKey: opts.remoteEncryption?.key, }); } } /** * connect database and initialize it in case of clean start */ override async connect() { if (this.connected) { return; } else if (this.#engine == null) { if (!this.memory) { const worker = await init(); await Promise.all([ registerFileAtWorker(worker, this.name), registerFileAtWorker(worker, `${this.name}-wal`) ]); this.#worker = worker; } await super.connect(); } else { if (!this.memory) { this.#worker = await init(); await Promise.all([ registerFileAtWorker(this.#worker, this.name), registerFileAtWorker(this.#worker, `${this.name}-wal`), registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), registerFileAtWorker(this.#worker, `${this.name}-info`), registerFileAtWorker(this.#worker, `${this.name}-changes`), ]); } await run(this.#runner, this.#engine.connect()); } this.connected = true; } /** * pull new changes from the remote database * if {@link DatabaseOpts.longPollTimeoutMs} is set - then server will hold the connection open until either new changes will appear in the database or timeout occurs. * @returns true if new changes were pulled from the remote */ async pull() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } const changes = await this.#guards.wait(async () => await run(this.#runner, this.#engine.wait())); if (changes.empty()) { return false; } await this.#guards.apply(async () => await run(this.#runner, this.#engine.apply(changes))); return true; } /** * push new local changes to the remote database * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote */ async push() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } await this.#guards.push(async () => await run(this.#runner, this.#engine.push())); } /** * checkpoint WAL for local database */ async checkpoint() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } await this.#guards.checkpoint(async () => await run(this.#runner, this.#engine.checkpoint())); } /** * @returns statistic of current local database */ async stats(): Promise { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } return (await run(this.#runner, this.#engine.stats())); } /** * Executes the given SQL string. * When remoteWrites is enabled, write statements are sent to the remote server. */ override async exec(sql: string) { if (!this.#remoteWriter) return super.exec(sql); const category = this.#db.classifySql(sql); if (this.#remoteWriter.isInTransaction) { const { shouldPull } = await this.#remoteWriter.execRemote(sql, category); if (shouldPull) await this.pull(); return; } if (category === "read") return super.exec(sql); const { shouldPull } = await this.#remoteWriter.execRemote(sql, category); if (shouldPull) await this.pull(); } /** * Prepares a SQL statement for execution. * When remoteWrites is enabled, returns a wrapper that routes writes to remote. */ override prepare(sql: string) { const localStmt = super.prepare(sql); if (!this.#remoteWriter) { return localStmt; } const category = this.#db.classifySql(sql); const isReadonly = category === "read"; return new RemoteWriteStatement( localStmt, sql, isReadonly, this.#remoteWriter, () => this.pull(), ) as any; } /** * Returns a function that executes the given function in a transaction. * When remoteWrites is enabled, the entire transaction goes to remote. */ override transaction(fn: (...any) => Promise) { if (typeof fn !== "function") throw new TypeError("Expected first argument to be a function"); if (!this.#remoteWriter) { return super.transaction(fn); } const db = this; const remoteWriter = this.#remoteWriter; const wrapTxn = (mode: string) => { return async (...bindParameters: any[]) => { await remoteWriter.beginTransaction(mode); try { const result = await fn(...bindParameters); await remoteWriter.commitTransaction(); await db.pull(); return result; } catch (err) { await remoteWriter.rollbackTransaction(); throw err; } }; }; const properties = { default: { value: wrapTxn("") }, deferred: { value: wrapTxn("DEFERRED") }, immediate: { value: wrapTxn("IMMEDIATE") }, exclusive: { value: wrapTxn("EXCLUSIVE") }, database: { value: this, enumerable: true }, }; Object.defineProperties(properties.default.value, properties); Object.defineProperties(properties.deferred.value, properties); Object.defineProperties(properties.immediate.value, properties); Object.defineProperties(properties.exclusive.value, properties); return properties.default.value; } /** * close the database and relevant files */ async close() { if (this.#remoteWriter) { await this.#remoteWriter.close(); } if (this.#engine != null) { if (this.name != null && this.#worker != null) { await Promise.all([ unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, `${this.name}-wal`), unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), unregisterFileAtWorker(this.#worker, `${this.name}-info`), unregisterFileAtWorker(this.#worker, `${this.name}-changes`), ]); } this.#engine.close(); } await super.close(); } } /** * Creates a new database connection asynchronously. * * @param {Object} opts - Options for database behavior. * @returns {Promise} - A promise that resolves to a Database instance. */ async function connect(opts: DatabaseOpts): Promise { const db = new Database(opts); await db.connect(); return db; } export { connect, Database } export type { DatabaseOpts, EncryptionOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult } ================================================ FILE: bindings/javascript/sync/packages/wasm/promise-default.ts ================================================ import { registerFileAtWorker, unregisterFileAtWorker, ioNotifier } from "@tursodatabase/database-wasm-common" import { DatabasePromise } from "@tursodatabase/database-common" import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards, Runner, runner, RemoteWriter, RemoteWriteStatement } from "@tursodatabase/sync-common"; import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker, Database as NativeDatabase } from "./index-default.js"; let BrowserIO: ProtocolIo = { async read(path: string): Promise { const result = localStorage.getItem(path); if (result == null) { return null; } return new TextEncoder().encode(result); }, async write(path: string, data: Buffer | Uint8Array): Promise { const array = new Uint8Array(data); const value = new TextDecoder('utf-8').decode(array); localStorage.setItem(path, value); } }; function memoryIO(): ProtocolIo { let values = new Map(); return { async read(path: string): Promise { return values.get(path); }, async write(path: string, data: Buffer | Uint8Array): Promise { values.set(path, data); } } }; async function init(): Promise { await initThreadPool(); if (MainWorker == null) { throw new Error("panic: MainWorker is not initialized"); } return MainWorker; } function resolveUrl(url: string | (() => string | null)): string { if (typeof url === "function") { const resolved = url(); if (resolved == null) { throw new Error("remoteWritesExperimental requires a non-null URL"); } return resolved; } return url; } class Database extends DatabasePromise { #runner: Runner; #engine: any; #io: ProtocolIo; #guards: SyncEngineGuards; #worker: Worker | null; #remoteWriter: RemoteWriter | null = null; #db: any; constructor(opts: DatabaseOpts) { if (opts.url == null) { const db = new NativeDatabase(opts.path, { tracing: opts.tracing }) as any; super( db, () => ioNotifier.waitForCompletion(), ); this.#db = db; this.#engine = null; return; } let partialSyncOpts = undefined; if (opts.partialSyncExperimental != null) { switch (opts.partialSyncExperimental.bootstrapStrategy.kind) { case "prefix": partialSyncOpts = { bootstrapStrategy: { type: "Prefix", length: opts.partialSyncExperimental.bootstrapStrategy.length }, segmentSize: opts.partialSyncExperimental.segmentSize, prefetch: opts.partialSyncExperimental.prefetch, }; break; case "query": partialSyncOpts = { bootstrapStrategy: { type: "Query", query: opts.partialSyncExperimental.bootstrapStrategy.query }, segmentSize: opts.partialSyncExperimental.segmentSize, prefetch: opts.partialSyncExperimental.prefetch, }; break; } } const engine = new SyncEngine({ path: opts.path, clientName: opts.clientName, useTransform: opts.transform != null, protocolVersion: SyncEngineProtocolVersion.V1, longPollTimeoutMs: opts.longPollTimeoutMs, tracing: opts.tracing, bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null, remoteEncryption: opts.remoteEncryption?.cipher, partialSyncOpts: partialSyncOpts }); let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>); if (typeof opts.authToken == "function") { const authToken = opts.authToken; headers = async () => ({ ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }), ...(opts.remoteEncryption != null && { "x-turso-encryption-key": opts.remoteEncryption.key, "x-turso-encryption-cipher": opts.remoteEncryption.cipher, }) }); } else { const authToken = opts.authToken; headers = { ...(opts.authToken != null && { "Authorization": `Bearer ${authToken}` }), ...(opts.remoteEncryption != null && { "x-turso-encryption-key": opts.remoteEncryption.key, "x-turso-encryption-cipher": opts.remoteEncryption.cipher, }) }; } const runOpts = { url: opts.url, headers: headers, preemptionMs: 1, transform: opts.transform, }; const db = engine.db() as unknown as any; const memory = db.memory; const io = memory ? memoryIO() : BrowserIO; const run = runner(runOpts, io, engine); super(db, () => run.wait()); this.#db = db; this.#runner = run; this.#engine = engine; this.#io = io; this.#guards = new SyncEngineGuards(); // Initialize remote writer if remoteWrites is enabled if (opts.remoteWritesExperimental && opts.url) { const url = resolveUrl(opts.url); this.#remoteWriter = new RemoteWriter({ url, authToken: opts.authToken, remoteEncryptionKey: opts.remoteEncryption?.key, }); } } /** * connect database and initialize it in case of clean start */ override async connect() { if (this.connected) { return; } else if (this.#engine == null) { if (!this.memory) { const worker = await init(); await Promise.all([ registerFileAtWorker(worker, this.name), registerFileAtWorker(worker, `${this.name}-wal`) ]); this.#worker = worker; } await super.connect(); } else { if (!this.memory) { this.#worker = await init(); await Promise.all([ registerFileAtWorker(this.#worker, this.name), registerFileAtWorker(this.#worker, `${this.name}-wal`), registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), registerFileAtWorker(this.#worker, `${this.name}-info`), registerFileAtWorker(this.#worker, `${this.name}-changes`), ]); } await run(this.#runner, this.#engine.connect()); } this.connected = true; } /** * pull new changes from the remote database * if {@link DatabaseOpts.longPollTimeoutMs} is set - then server will hold the connection open until either new changes will appear in the database or timeout occurs. * @returns true if new changes were pulled from the remote */ async pull() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } const changes = await this.#guards.wait(async () => await run(this.#runner, this.#engine.wait())); if (changes.empty()) { return false; } await this.#guards.apply(async () => await run(this.#runner, this.#engine.apply(changes))); return true; } /** * push new local changes to the remote database * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote */ async push() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } await this.#guards.push(async () => await run(this.#runner, this.#engine.push())); } /** * checkpoint WAL for local database */ async checkpoint() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } await this.#guards.checkpoint(async () => await run(this.#runner, this.#engine.checkpoint())); } /** * @returns statistic of current local database */ async stats(): Promise { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } return (await run(this.#runner, this.#engine.stats())); } /** * Executes the given SQL string. * When remoteWrites is enabled, write statements are sent to the remote server. */ override async exec(sql: string) { if (!this.#remoteWriter) return super.exec(sql); const category = this.#db.classifySql(sql); if (this.#remoteWriter.isInTransaction) { const { shouldPull } = await this.#remoteWriter.execRemote(sql, category); if (shouldPull) await this.pull(); return; } if (category === "read") return super.exec(sql); const { shouldPull } = await this.#remoteWriter.execRemote(sql, category); if (shouldPull) await this.pull(); } /** * Prepares a SQL statement for execution. * When remoteWrites is enabled, returns a wrapper that routes writes to remote. */ override prepare(sql: string) { const localStmt = super.prepare(sql); if (!this.#remoteWriter) { return localStmt; } const category = this.#db.classifySql(sql); const isReadonly = category === "read"; return new RemoteWriteStatement( localStmt, sql, isReadonly, this.#remoteWriter, () => this.pull(), ) as any; } /** * Returns a function that executes the given function in a transaction. * When remoteWrites is enabled, the entire transaction goes to remote. */ override transaction(fn: (...any) => Promise) { if (typeof fn !== "function") throw new TypeError("Expected first argument to be a function"); if (!this.#remoteWriter) { return super.transaction(fn); } const db = this; const remoteWriter = this.#remoteWriter; const wrapTxn = (mode: string) => { return async (...bindParameters: any[]) => { await remoteWriter.beginTransaction(mode); try { const result = await fn(...bindParameters); await remoteWriter.commitTransaction(); await db.pull(); return result; } catch (err) { await remoteWriter.rollbackTransaction(); throw err; } }; }; const properties = { default: { value: wrapTxn("") }, deferred: { value: wrapTxn("DEFERRED") }, immediate: { value: wrapTxn("IMMEDIATE") }, exclusive: { value: wrapTxn("EXCLUSIVE") }, database: { value: this, enumerable: true }, }; Object.defineProperties(properties.default.value, properties); Object.defineProperties(properties.deferred.value, properties); Object.defineProperties(properties.immediate.value, properties); Object.defineProperties(properties.exclusive.value, properties); return properties.default.value; } /** * close the database and relevant files */ async close() { if (this.#remoteWriter) { await this.#remoteWriter.close(); } if (this.#engine != null) { if (this.name != null && this.#worker != null) { await Promise.all([ unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, `${this.name}-wal`), unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), unregisterFileAtWorker(this.#worker, `${this.name}-info`), unregisterFileAtWorker(this.#worker, `${this.name}-changes`), ]); } this.#engine.close(); } await super.close(); } } /** * Creates a new database connection asynchronously. * * @param {Object} opts - Options for database behavior. * @returns {Promise} - A promise that resolves to a Database instance. */ async function connect(opts: DatabaseOpts): Promise { const db = new Database(opts); await db.connect(); return db; } export { connect, Database } export type { DatabaseOpts, EncryptionOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult } ================================================ FILE: bindings/javascript/sync/packages/wasm/promise-turbopack-hack.ts ================================================ import { registerFileAtWorker, unregisterFileAtWorker, ioNotifier } from "@tursodatabase/database-wasm-common" import { DatabasePromise } from "@tursodatabase/database-common" import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards, Runner, runner, RemoteWriter, RemoteWriteStatement } from "@tursodatabase/sync-common"; import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker, Database as NativeDatabase } from "./index-turbopack-hack.js"; let BrowserIO: ProtocolIo = { async read(path: string): Promise { const result = localStorage.getItem(path); if (result == null) { return null; } return new TextEncoder().encode(result); }, async write(path: string, data: Buffer | Uint8Array): Promise { const array = new Uint8Array(data); const value = new TextDecoder('utf-8').decode(array); localStorage.setItem(path, value); } }; function memoryIO(): ProtocolIo { let values = new Map(); return { async read(path: string): Promise { return values.get(path); }, async write(path: string, data: Buffer | Uint8Array): Promise { values.set(path, data); } } }; async function init(): Promise { await initThreadPool(); if (MainWorker == null) { throw new Error("panic: MainWorker is not initialized"); } return MainWorker; } function resolveUrl(url: string | (() => string | null)): string { if (typeof url === "function") { const resolved = url(); if (resolved == null) { throw new Error("remoteWritesExperimental requires a non-null URL"); } return resolved; } return url; } class Database extends DatabasePromise { #runner: Runner; #engine: any; #io: ProtocolIo; #guards: SyncEngineGuards; #worker: Worker | null; #remoteWriter: RemoteWriter | null = null; #db: any; constructor(opts: DatabaseOpts) { if (opts.url == null) { const db = new NativeDatabase(opts.path, { tracing: opts.tracing }) as any; super( db, () => ioNotifier.waitForCompletion(), ); this.#db = db; this.#engine = null; return; } let partialSyncOpts = undefined; if (opts.partialSyncExperimental != null) { switch (opts.partialSyncExperimental.bootstrapStrategy.kind) { case "prefix": partialSyncOpts = { bootstrapStrategy: { type: "Prefix", length: opts.partialSyncExperimental.bootstrapStrategy.length }, segmentSize: opts.partialSyncExperimental.segmentSize, prefetch: opts.partialSyncExperimental.prefetch, }; break; case "query": partialSyncOpts = { bootstrapStrategy: { type: "Query", query: opts.partialSyncExperimental.bootstrapStrategy.query }, segmentSize: opts.partialSyncExperimental.segmentSize, prefetch: opts.partialSyncExperimental.prefetch, }; break; } } const engine = new SyncEngine({ path: opts.path, clientName: opts.clientName, useTransform: opts.transform != null, protocolVersion: SyncEngineProtocolVersion.V1, longPollTimeoutMs: opts.longPollTimeoutMs, tracing: opts.tracing, bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null, remoteEncryption: opts.remoteEncryption?.cipher, partialSyncOpts: partialSyncOpts }); let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>); if (typeof opts.authToken == "function") { const authToken = opts.authToken; headers = async () => ({ ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }), ...(opts.remoteEncryption != null && { "x-turso-encryption-key": opts.remoteEncryption.key, "x-turso-encryption-cipher": opts.remoteEncryption.cipher, }) }); } else { const authToken = opts.authToken; headers = { ...(opts.authToken != null && { "Authorization": `Bearer ${authToken}` }), ...(opts.remoteEncryption != null && { "x-turso-encryption-key": opts.remoteEncryption.key, "x-turso-encryption-cipher": opts.remoteEncryption.cipher, }) }; } const runOpts = { url: opts.url, headers: headers, preemptionMs: 1, transform: opts.transform, }; const db = engine.db() as unknown as any; const memory = db.memory; const io = memory ? memoryIO() : BrowserIO; const run = runner(runOpts, io, engine); super(db, () => run.wait()); this.#db = db; this.#runner = run; this.#engine = engine; this.#io = io; this.#guards = new SyncEngineGuards(); // Initialize remote writer if remoteWrites is enabled if (opts.remoteWritesExperimental && opts.url) { const url = resolveUrl(opts.url); this.#remoteWriter = new RemoteWriter({ url, authToken: opts.authToken, remoteEncryptionKey: opts.remoteEncryption?.key, }); } } /** * connect database and initialize it in case of clean start */ override async connect() { if (this.connected) { return; } else if (this.#engine == null) { if (!this.memory) { const worker = await init(); await Promise.all([ registerFileAtWorker(worker, this.name), registerFileAtWorker(worker, `${this.name}-wal`) ]); this.#worker = worker; } await super.connect(); } else { if (!this.memory) { this.#worker = await init(); await Promise.all([ registerFileAtWorker(this.#worker, this.name), registerFileAtWorker(this.#worker, `${this.name}-wal`), registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), registerFileAtWorker(this.#worker, `${this.name}-info`), registerFileAtWorker(this.#worker, `${this.name}-changes`), ]); } await run(this.#runner, this.#engine.connect()); } this.connected = true; } /** * pull new changes from the remote database * if {@link DatabaseOpts.longPollTimeoutMs} is set - then server will hold the connection open until either new changes will appear in the database or timeout occurs. * @returns true if new changes were pulled from the remote */ async pull() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } const changes = await this.#guards.wait(async () => await run(this.#runner, this.#engine.wait())); if (changes.empty()) { return false; } await this.#guards.apply(async () => await run(this.#runner, this.#engine.apply(changes))); return true; } /** * push new local changes to the remote database * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote */ async push() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } await this.#guards.push(async () => await run(this.#runner, this.#engine.push())); } /** * checkpoint WAL for local database */ async checkpoint() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } await this.#guards.checkpoint(async () => await run(this.#runner, this.#engine.checkpoint())); } /** * @returns statistic of current local database */ async stats(): Promise { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } return (await run(this.#runner, this.#engine.stats())); } /** * Executes the given SQL string. * When remoteWrites is enabled, write statements are sent to the remote server. */ override async exec(sql: string) { if (!this.#remoteWriter) return super.exec(sql); const category = this.#db.classifySql(sql); if (this.#remoteWriter.isInTransaction) { const { shouldPull } = await this.#remoteWriter.execRemote(sql, category); if (shouldPull) await this.pull(); return; } if (category === "read") return super.exec(sql); const { shouldPull } = await this.#remoteWriter.execRemote(sql, category); if (shouldPull) await this.pull(); } /** * Prepares a SQL statement for execution. * When remoteWrites is enabled, returns a wrapper that routes writes to remote. */ override prepare(sql: string) { const localStmt = super.prepare(sql); if (!this.#remoteWriter) { return localStmt; } const category = this.#db.classifySql(sql); const isReadonly = category === "read"; return new RemoteWriteStatement( localStmt, sql, isReadonly, this.#remoteWriter, () => this.pull(), ) as any; } /** * Returns a function that executes the given function in a transaction. * When remoteWrites is enabled, the entire transaction goes to remote. */ override transaction(fn: (...any) => Promise) { if (typeof fn !== "function") throw new TypeError("Expected first argument to be a function"); if (!this.#remoteWriter) { return super.transaction(fn); } const db = this; const remoteWriter = this.#remoteWriter; const wrapTxn = (mode: string) => { return async (...bindParameters: any[]) => { await remoteWriter.beginTransaction(mode); try { const result = await fn(...bindParameters); await remoteWriter.commitTransaction(); await db.pull(); return result; } catch (err) { await remoteWriter.rollbackTransaction(); throw err; } }; }; const properties = { default: { value: wrapTxn("") }, deferred: { value: wrapTxn("DEFERRED") }, immediate: { value: wrapTxn("IMMEDIATE") }, exclusive: { value: wrapTxn("EXCLUSIVE") }, database: { value: this, enumerable: true }, }; Object.defineProperties(properties.default.value, properties); Object.defineProperties(properties.deferred.value, properties); Object.defineProperties(properties.immediate.value, properties); Object.defineProperties(properties.exclusive.value, properties); return properties.default.value; } /** * close the database and relevant files */ async close() { if (this.#remoteWriter) { await this.#remoteWriter.close(); } if (this.#engine != null) { if (this.name != null && this.#worker != null) { await Promise.all([ unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, `${this.name}-wal`), unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), unregisterFileAtWorker(this.#worker, `${this.name}-info`), unregisterFileAtWorker(this.#worker, `${this.name}-changes`), ]); } this.#engine.close(); } await super.close(); } } /** * Creates a new database connection asynchronously. * * @param {Object} opts - Options for database behavior. * @returns {Promise} - A promise that resolves to a Database instance. */ async function connect(opts: DatabaseOpts): Promise { const db = new Database(opts); await db.connect(); return db; } export { connect, Database } export type { DatabaseOpts, EncryptionOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult } ================================================ FILE: bindings/javascript/sync/packages/wasm/promise-vite-dev-hack.ts ================================================ import { registerFileAtWorker, unregisterFileAtWorker, ioNotifier } from "@tursodatabase/database-wasm-common" import { DatabasePromise } from "@tursodatabase/database-common" import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards, Runner, runner, RemoteWriter, RemoteWriteStatement } from "@tursodatabase/sync-common"; import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker, Database as NativeDatabase } from "./index-vite-dev-hack.js"; let BrowserIO: ProtocolIo = { async read(path: string): Promise { const result = localStorage.getItem(path); if (result == null) { return null; } return new TextEncoder().encode(result); }, async write(path: string, data: Buffer | Uint8Array): Promise { const array = new Uint8Array(data); const value = new TextDecoder('utf-8').decode(array); localStorage.setItem(path, value); } }; function memoryIO(): ProtocolIo { let values = new Map(); return { async read(path: string): Promise { return values.get(path); }, async write(path: string, data: Buffer | Uint8Array): Promise { values.set(path, data); } } }; async function init(): Promise { await initThreadPool(); if (MainWorker == null) { throw new Error("panic: MainWorker is not initialized"); } return MainWorker; } function resolveUrl(url: string | (() => string | null)): string { if (typeof url === "function") { const resolved = url(); if (resolved == null) { throw new Error("remoteWritesExperimental requires a non-null URL"); } return resolved; } return url; } class Database extends DatabasePromise { #runner: Runner; #engine: any; #io: ProtocolIo; #guards: SyncEngineGuards; #worker: Worker | null; #remoteWriter: RemoteWriter | null = null; #db: any; constructor(opts: DatabaseOpts) { if (opts.url == null) { const db = new NativeDatabase(opts.path, { tracing: opts.tracing }) as any; super( db, () => ioNotifier.waitForCompletion(), ); this.#db = db; this.#engine = null; return; } let partialSyncOpts = undefined; if (opts.partialSyncExperimental != null) { switch (opts.partialSyncExperimental.bootstrapStrategy.kind) { case "prefix": partialSyncOpts = { bootstrapStrategy: { type: "Prefix", length: opts.partialSyncExperimental.bootstrapStrategy.length }, segmentSize: opts.partialSyncExperimental.segmentSize, prefetch: opts.partialSyncExperimental.prefetch, }; break; case "query": partialSyncOpts = { bootstrapStrategy: { type: "Query", query: opts.partialSyncExperimental.bootstrapStrategy.query }, segmentSize: opts.partialSyncExperimental.segmentSize, prefetch: opts.partialSyncExperimental.prefetch, }; break; } } const engine = new SyncEngine({ path: opts.path, clientName: opts.clientName, useTransform: opts.transform != null, protocolVersion: SyncEngineProtocolVersion.V1, longPollTimeoutMs: opts.longPollTimeoutMs, tracing: opts.tracing, bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null, remoteEncryption: opts.remoteEncryption?.cipher, partialSyncOpts: partialSyncOpts }); let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>); if (typeof opts.authToken == "function") { const authToken = opts.authToken; headers = async () => ({ ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }), ...(opts.remoteEncryption != null && { "x-turso-encryption-key": opts.remoteEncryption.key, "x-turso-encryption-cipher": opts.remoteEncryption.cipher, }) }); } else { const authToken = opts.authToken; headers = { ...(opts.authToken != null && { "Authorization": `Bearer ${authToken}` }), ...(opts.remoteEncryption != null && { "x-turso-encryption-key": opts.remoteEncryption.key, "x-turso-encryption-cipher": opts.remoteEncryption.cipher, }) }; } const runOpts = { url: opts.url, headers: headers, preemptionMs: 1, transform: opts.transform, }; const db = engine.db() as unknown as any; const memory = db.memory; const io = memory ? memoryIO() : BrowserIO; const run = runner(runOpts, io, engine); super(db, () => run.wait()); this.#db = db; this.#runner = run; this.#engine = engine; this.#io = io; this.#guards = new SyncEngineGuards(); // Initialize remote writer if remoteWrites is enabled if (opts.remoteWritesExperimental && opts.url) { const url = resolveUrl(opts.url); this.#remoteWriter = new RemoteWriter({ url, authToken: opts.authToken, remoteEncryptionKey: opts.remoteEncryption?.key, }); } } /** * connect database and initialize it in case of clean start */ override async connect() { if (this.connected) { return; } else if (this.#engine == null) { if (!this.memory) { const worker = await init(); await Promise.all([ registerFileAtWorker(worker, this.name), registerFileAtWorker(worker, `${this.name}-wal`) ]); this.#worker = worker; } await super.connect(); } else { if (!this.memory) { this.#worker = await init(); await Promise.all([ registerFileAtWorker(this.#worker, this.name), registerFileAtWorker(this.#worker, `${this.name}-wal`), registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), registerFileAtWorker(this.#worker, `${this.name}-info`), registerFileAtWorker(this.#worker, `${this.name}-changes`), ]); } await run(this.#runner, this.#engine.connect()); } this.connected = true; } /** * pull new changes from the remote database * if {@link DatabaseOpts.longPollTimeoutMs} is set - then server will hold the connection open until either new changes will appear in the database or timeout occurs. * @returns true if new changes were pulled from the remote */ async pull() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } const changes = await this.#guards.wait(async () => await run(this.#runner, this.#engine.wait())); if (changes.empty()) { return false; } await this.#guards.apply(async () => await run(this.#runner, this.#engine.apply(changes))); return true; } /** * push new local changes to the remote database * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote */ async push() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } await this.#guards.push(async () => await run(this.#runner, this.#engine.push())); } /** * checkpoint WAL for local database */ async checkpoint() { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } await this.#guards.checkpoint(async () => await run(this.#runner, this.#engine.checkpoint())); } /** * @returns statistic of current local database */ async stats(): Promise { if (this.#engine == null) { throw new Error("sync is disabled as database was opened without sync support") } return (await run(this.#runner, this.#engine.stats())); } /** * Executes the given SQL string. * When remoteWrites is enabled, write statements are sent to the remote server. */ override async exec(sql: string) { if (!this.#remoteWriter) return super.exec(sql); const category = this.#db.classifySql(sql); if (this.#remoteWriter.isInTransaction) { const { shouldPull } = await this.#remoteWriter.execRemote(sql, category); if (shouldPull) await this.pull(); return; } if (category === "read") return super.exec(sql); const { shouldPull } = await this.#remoteWriter.execRemote(sql, category); if (shouldPull) await this.pull(); } /** * Prepares a SQL statement for execution. * When remoteWrites is enabled, returns a wrapper that routes writes to remote. */ override prepare(sql: string) { const localStmt = super.prepare(sql); if (!this.#remoteWriter) { return localStmt; } const category = this.#db.classifySql(sql); const isReadonly = category === "read"; return new RemoteWriteStatement( localStmt, sql, isReadonly, this.#remoteWriter, () => this.pull(), ) as any; } /** * Returns a function that executes the given function in a transaction. * When remoteWrites is enabled, the entire transaction goes to remote. */ override transaction(fn: (...any) => Promise) { if (typeof fn !== "function") throw new TypeError("Expected first argument to be a function"); if (!this.#remoteWriter) { return super.transaction(fn); } const db = this; const remoteWriter = this.#remoteWriter; const wrapTxn = (mode: string) => { return async (...bindParameters: any[]) => { await remoteWriter.beginTransaction(mode); try { const result = await fn(...bindParameters); await remoteWriter.commitTransaction(); await db.pull(); return result; } catch (err) { await remoteWriter.rollbackTransaction(); throw err; } }; }; const properties = { default: { value: wrapTxn("") }, deferred: { value: wrapTxn("DEFERRED") }, immediate: { value: wrapTxn("IMMEDIATE") }, exclusive: { value: wrapTxn("EXCLUSIVE") }, database: { value: this, enumerable: true }, }; Object.defineProperties(properties.default.value, properties); Object.defineProperties(properties.deferred.value, properties); Object.defineProperties(properties.immediate.value, properties); Object.defineProperties(properties.exclusive.value, properties); return properties.default.value; } /** * close the database and relevant files */ async close() { if (this.#remoteWriter) { await this.#remoteWriter.close(); } if (this.#engine != null) { if (this.name != null && this.#worker != null) { await Promise.all([ unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, `${this.name}-wal`), unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), unregisterFileAtWorker(this.#worker, `${this.name}-info`), unregisterFileAtWorker(this.#worker, `${this.name}-changes`), ]); } this.#engine.close(); } await super.close(); } } /** * Creates a new database connection asynchronously. * * @param {Object} opts - Options for database behavior. * @returns {Promise} - A promise that resolves to a Database instance. */ async function connect(opts: DatabaseOpts): Promise { const db = new Database(opts); await db.connect(); return db; } export { connect, Database } export type { DatabaseOpts, EncryptionOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult } ================================================ FILE: bindings/javascript/sync/packages/wasm/promise.test.ts ================================================ import { expect, test, afterAll } from 'vitest' import { Database, connect, DatabaseRowMutation, DatabaseRowTransformResult } from './promise-default.js' import { MainWorker } from './index-default.js' afterAll(() => { MainWorker?.terminate(); }) const localeCompare = (a, b) => a.x.localeCompare(b.x); const intCompare = (a, b) => a.x - b.x; test('open non-sync db', async () => { const db = await connect({ path: 'local.db' }); await db.exec("CREATE TABLE t(x)"); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); expect(await (await db.prepare("SELECT * FROM t").all())).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]) }) test('checkpoint-and-actions', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 100, }); await db.exec("CREATE TABLE IF NOT EXISTS rows(key TEXT PRIMARY KEY, value INTEGER)"); await db.exec("DELETE FROM rows"); await db.exec("INSERT INTO rows VALUES ('key', 0)"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db1.exec("PRAGMA busy_timeout=100"); console.info('run_info', await db1.prepare("SELECT * FROM sqlite_master").all()); const pull = async function (iterations: number) { for (let i = 0; i < iterations; i++) { console.info('pull', i); try { await db1.pull(); } catch (e) { console.error('pull', e); } await new Promise(resolve => setTimeout(resolve, 0)); } } const push = async function (iterations: number) { for (let i = 0; i < iterations; i++) { await new Promise(resolve => setTimeout(resolve, 10)); console.info('push', i); try { if ((await db1.stats()).cdcOperations > 0) { const start = performance.now(); await db1.push(); console.info('push', performance.now() - start); } } catch (e) { console.error('push', e); } } } const run = async function (iterations: number) { let rows = 0; for (let i = 0; i < iterations; i++) { console.info('run', i, rows); await db1.prepare("UPDATE rows SET value = value + 1 WHERE key = ?").run('key'); rows += 1; const { cnt } = await db1.prepare("SELECT value as cnt FROM rows WHERE key = ?").get(['key']); expect(cnt).toBe(rows); await new Promise(resolve => setTimeout(resolve, 1)); } } await Promise.all([pull(20), push(20), run(1000)]); }) test('implicit connect', async () => { const db = new Database({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); const defer = db.prepare("SELECT * FROM not_found"); await expect(async () => await defer.all()).rejects.toThrowError(/no such table: not_found/); expect(() => db.prepare("SELECT * FROM not_found")).toThrowError(/no such table: not_found/); expect(await db.prepare("SELECT 1 as x").all()).toEqual([{ x: 1 }]); }) test('simple-db', async () => { const db = new Database({ path: ':memory:' }); expect(await db.prepare("SELECT 1 as x").all()).toEqual([{ x: 1 }]) await db.exec("CREATE TABLE t(x)"); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]) await expect(async () => await db.pull()).rejects.toThrowError(/sync is disabled as database was opened without sync support/); }) test('reconnect-db', async () => { { const db = await connect({ path: 'local.db', url: process.env.VITE_TURSO_DB_URL }); const stmt = db.prepare("SELECT * FROM turso_cdc"); expect(await stmt.all()).toEqual([]) stmt.close(); } { const db = await connect({ path: 'local.db', url: process.env.VITE_TURSO_DB_URL }); const stmt = db.prepare("SELECT * FROM turso_cdc"); expect(await stmt.all()).toEqual([]) stmt.close(); } }) test('implicit connect', async () => { const db = new Database({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); const defer = db.prepare("SELECT * FROM not_found"); await expect(async () => await defer.all()).rejects.toThrowError(/no such table: not_found/); expect(() => db.prepare("SELECT * FROM not_found")).toThrowError(/no such table: not_found/); expect(await db.prepare("SELECT 1 as x").all()).toEqual([{ x: 1 }]); }) test('defered sync', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("DELETE FROM t"); await db.exec("INSERT INTO t VALUES (100)"); await db.push(); await db.close(); } let url = null; const db = new Database({ path: ':memory:', url: () => url }); await db.prepare("CREATE TABLE t(x)").run(); await db.prepare("INSERT INTO t VALUES (1), (2), (3), (42)").run(); expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 42 }]); await expect(async () => await db.pull()).rejects.toThrow(/url is empty - sync is paused/); url = process.env.VITE_TURSO_DB_URL; await db.pull(); expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 100 }, { x: 1 }, { x: 2 }, { x: 3 }, { x: 42 }]); }) test('encryption sync', async () => { const KEY = 'l/FWopMfZisTLgBX4A42AergrCrYKjiO3BfkJUwv83I='; const URL = 'http://encrypted--a--a.localhost:10000'; { const db = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("DELETE FROM t"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); const db2 = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); await db1.exec("INSERT INTO t VALUES (1), (2), (3)"); await db2.exec("INSERT INTO t VALUES (4), (5), (6)"); expect(await db1.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]); expect(await db2.prepare("SELECT * FROM t").all()).toEqual([{ x: 4 }, { x: 5 }, { x: 6 }]); await Promise.all([db1.push(), db2.push()]); await Promise.all([db1.pull(), db2.pull()]); const expected = [{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }, { x: 5 }, { x: 6 }]; expect((await db1.prepare("SELECT * FROM t").all()).sort(intCompare)).toEqual(expected.sort(intCompare)); expect((await db2.prepare("SELECT * FROM t").all()).sort(intCompare)).toEqual(expected.sort(intCompare)); }); test('defered encryption sync', async () => { const URL = 'http://encrypted--a--a.localhost:10000'; const KEY = 'l/FWopMfZisTLgBX4A42AergrCrYKjiO3BfkJUwv83I='; let url = null; { const db = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("DELETE FROM t"); await db.exec("INSERT INTO t VALUES (100)"); await db.push(); await db.close(); } const db = await connect({ path: ':memory:', url: () => url, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]); url = URL; await db.pull(); const expected = [{ x: 100 }, { x: 1 }, { x: 2 }, { x: 3 }]; expect((await db.prepare("SELECT * FROM t").all())).toEqual(expected); }); test('select-after-push', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("DELETE FROM t"); await db.push(); await db.close(); } { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); await db.push(); } { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); const rows = await db.prepare('SELECT * FROM t').all(); expect(rows).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]) } }) test('select-without-push', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); await db.exec("DELETE FROM t"); await db.push(); await db.close(); } { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); } { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); const rows = await db.prepare('SELECT * FROM t').all(); expect(rows).toEqual([]) } }) test('merge-non-overlapping-keys', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2')"); const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db2.exec("INSERT INTO q VALUES ('k3', 'value3'), ('k4', 'value4'), ('k5', 'value5')"); await Promise.all([db1.push(), db2.push()]); await Promise.all([db1.pull(), db2.pull()]); const rows1 = await db1.prepare('SELECT * FROM q').all(); const rows2 = await db1.prepare('SELECT * FROM q').all(); const expected = [{ x: 'k1', y: 'value1' }, { x: 'k2', y: 'value2' }, { x: 'k3', y: 'value3' }, { x: 'k4', y: 'value4' }, { x: 'k5', y: 'value5' }]; expect(rows1.sort(localeCompare)).toEqual(expected.sort(localeCompare)) expect(rows2.sort(localeCompare)).toEqual(expected.sort(localeCompare)) }) test('last-push-wins', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2'), ('k4', 'value4')"); const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db2.exec("INSERT INTO q VALUES ('k1', 'value3'), ('k2', 'value4'), ('k3', 'value5')"); await db2.push(); await db1.push(); await Promise.all([db1.pull(), db2.pull()]); const rows1 = await db1.prepare('SELECT * FROM q').all(); const rows2 = await db1.prepare('SELECT * FROM q').all(); const expected = [{ x: 'k1', y: 'value1' }, { x: 'k2', y: 'value2' }, { x: 'k3', y: 'value5' }, { x: 'k4', y: 'value4' }]; expect(rows1.sort(localeCompare)).toEqual(expected.sort(localeCompare)) expect(rows2.sort(localeCompare)).toEqual(expected.sort(localeCompare)) }) test('last-push-wins-with-delete', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2'), ('k4', 'value4')"); await db1.exec("DELETE FROM q") const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db2.exec("INSERT INTO q VALUES ('k1', 'value3'), ('k2', 'value4'), ('k3', 'value5')"); await db2.push(); await db1.push(); await Promise.all([db1.pull(), db2.pull()]); const rows1 = await db1.prepare('SELECT * FROM q').all(); const rows2 = await db1.prepare('SELECT * FROM q').all(); const expected = [{ x: 'k3', y: 'value5' }]; expect(rows1).toEqual(expected) expect(rows2).toEqual(expected) }) test('constraint-conflict', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS u(x TEXT PRIMARY KEY, y UNIQUE)"); await db.exec("DELETE FROM u"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db1.exec("INSERT INTO u VALUES ('k1', 'value1')"); const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db2.exec("INSERT INTO u VALUES ('k2', 'value1')"); await db1.push(); await expect(async () => await db2.push()).rejects.toThrow('SQLite error: UNIQUE constraint failed: u.y'); }) test('checkpoint', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); for (let i = 0; i < 1000; i++) { await db1.exec(`INSERT INTO q VALUES ('k${i}', 'v${i}')`); } expect((await db1.stats()).mainWalSize).toBeGreaterThan(4096 * 1000); await db1.checkpoint(); expect((await db1.stats()).mainWalSize).toBe(0); let revertWal = (await db1.stats()).revertWalSize; expect(revertWal).toBeLessThan(4096 * 1000 / 100); for (let i = 0; i < 1000; i++) { await db1.exec(`UPDATE q SET y = 'u${i}' WHERE x = 'k${i}'`); } await db1.checkpoint(); expect((await db1.stats()).revertWalSize).toBe(revertWal); }) test('persistence-push', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const path = `test-${(Math.random() * 10000) | 0}.db`; { const db1 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL }); await db1.exec(`INSERT INTO q VALUES ('k1', 'v1')`); await db1.exec(`INSERT INTO q VALUES ('k2', 'v2')`); await db1.close(); } { const db2 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL }); await db2.exec(`INSERT INTO q VALUES ('k3', 'v3')`); await db2.exec(`INSERT INTO q VALUES ('k4', 'v4')`); const stmt = db2.prepare('SELECT * FROM q'); const rows = await stmt.all(); const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }, { x: 'k3', y: 'v3' }, { x: 'k4', y: 'v4' }]; expect(rows).toEqual(expected) stmt.close(); await db2.close(); } { const db3 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL }); await db3.push(); await db3.close(); } { const db4 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL }); const rows = await db4.prepare('SELECT * FROM q').all(); const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }, { x: 'k3', y: 'v3' }, { x: 'k4', y: 'v4' }]; expect(rows).toEqual(expected) await db4.close(); } }) test('persistence-offline', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const path = `test-${(Math.random() * 10000) | 0}.db`; { const db = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL }); await db.exec(`INSERT INTO q VALUES ('k1', 'v1')`); await db.exec(`INSERT INTO q VALUES ('k2', 'v2')`); await db.push(); await db.close(); } { const db = await connect({ path: path, url: "https://not-valid-url.localhost" }); const rows = await db.prepare("SELECT * FROM q").all(); const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }]; expect(rows.sort(localeCompare)).toEqual(expected.sort(localeCompare)) await db.close(); } }) test('persistence-pull-push', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } const path1 = `test-${(Math.random() * 10000) | 0}.db`; const path2 = `test-${(Math.random() * 10000) | 0}.db`; const db1 = await connect({ path: path1, url: process.env.VITE_TURSO_DB_URL }); await db1.exec(`INSERT INTO q VALUES ('k1', 'v1')`); await db1.exec(`INSERT INTO q VALUES ('k2', 'v2')`); const stats1 = await db1.stats(); const db2 = await connect({ path: path2, url: process.env.VITE_TURSO_DB_URL }); await db2.exec(`INSERT INTO q VALUES ('k3', 'v3')`); await db2.exec(`INSERT INTO q VALUES ('k4', 'v4')`); await Promise.all([db1.push(), db2.push()]); await Promise.all([db1.pull(), db2.pull()]); const stats2 = await db1.stats(); console.info(stats1, stats2); expect(stats1.revision).not.toBe(stats2.revision); const rows1 = await db1.prepare('SELECT * FROM q').all(); const rows2 = await db2.prepare('SELECT * FROM q').all(); const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }, { x: 'k3', y: 'v3' }, { x: 'k4', y: 'v4' }]; expect(rows1.sort(localeCompare)).toEqual(expected.sort(localeCompare)) expect(rows2.sort(localeCompare)).toEqual(expected.sort(localeCompare)) }) test('pull-push-concurrent', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 5000 }); await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); await db.exec("DELETE FROM q"); await db.push(); await db.close(); } let pullResolve = null; const pullFinish = new Promise(resolve => pullResolve = resolve); let pushResolve = null; const pushFinish = new Promise(resolve => pushResolve = resolve); let stopPull = false; let stopPush = false; const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); let pull = async () => { try { await db.pull(); } catch (e) { console.error('pull', e); } finally { if (!stopPull) { setTimeout(pull, 0); } else { pullResolve() } } } let push = async () => { try { if ((await db.stats()).cdcOperations > 0) { await db.push(); } } catch (e) { console.error('push', e); } finally { if (!stopPush) { setTimeout(push, 0); } else { pushResolve(); } } } setTimeout(pull, 0); setTimeout(push, 0); for (let i = 0; i < 1000; i++) { await db.exec(`INSERT INTO q VALUES ('k${i}', 'v${i}')`); } await new Promise(resolve => setTimeout(resolve, 1000)); stopPush = true; await pushFinish; stopPull = true; await pullFinish; console.info(await db.stats()); }) test('concurrent-updates', { timeout: 60000 }, async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 10 }); await db.exec("CREATE TABLE IF NOT EXISTS three(x TEXT PRIMARY KEY, y, z)"); await db.exec("DELETE FROM three"); await db.push(); await db.close(); } let stop = false; const dbs = []; for (let i = 0; i < 8; i++) { dbs.push(await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL })); } async function pull(db, i) { try { await db.pull(); } catch (e) { console.error('pull', i, e); } finally { if (!stop) { setTimeout(async () => await pull(db, i), 0); } } } async function push(db, i) { try { await db.push(); } catch (e) { console.error('push', i, e); } finally { if (!stop) { setTimeout(async () => await push(db, i), 0); } } } for (let i = 0; i < dbs.length; i++) { setTimeout(async () => await pull(dbs[i], i), 0) setTimeout(async () => await push(dbs[i], i), 0) } for (let i = 0; i < 1000; i++) { try { const tasks = []; for (let s = 0; s < dbs.length; s++) { tasks.push(dbs[s].exec(`INSERT INTO three VALUES ('${s}', 0, randomblob(128)) ON CONFLICT DO UPDATE SET y = y + 1, z = randomblob(128)`)); } await Promise.all(tasks); } catch (e) { // ignore } await new Promise(resolve => setTimeout(resolve, 1)); } stop = true; await Promise.all(dbs.map(db => db.push())); await Promise.all(dbs.map(db => db.pull())); let results = []; for (let i = 0; i < dbs.length; i++) { results.push(await dbs[i].prepare('SELECT x, y FROM three').all()); } for (let i = 0; i < dbs.length; i++) { expect(results[i]).toEqual(results[0]); for (let s = 0; s < dbs.length; s++) { expect(results[i][s].y).toBeGreaterThan(500); } } }) test('transform', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, }); await db.exec("CREATE TABLE IF NOT EXISTS counter(key TEXT PRIMARY KEY, value INTEGER)"); await db.exec("DELETE FROM counter"); await db.exec("INSERT INTO counter VALUES ('1', 0)") await db.push(); await db.close(); } const transform = (m: DatabaseRowMutation) => ({ operation: 'rewrite', stmt: { sql: `UPDATE counter SET value = value + ? WHERE key = ?`, values: [m.after.value - m.before.value, m.after.key] } } as DatabaseRowTransformResult); const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform }); const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform }); await db1.exec("UPDATE counter SET value = value + 1 WHERE key = '1'"); await db2.exec("UPDATE counter SET value = value + 1 WHERE key = '1'"); await Promise.all([db1.push(), db2.push()]); await Promise.all([db1.pull(), db2.pull()]); const rows1 = await db1.prepare('SELECT * FROM counter').all(); const rows2 = await db2.prepare('SELECT * FROM counter').all(); expect(rows1).toEqual([{ key: '1', value: 2 }]); expect(rows2).toEqual([{ key: '1', value: 2 }]); }) test('transform-many', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, }); await db.exec("CREATE TABLE IF NOT EXISTS counter(key TEXT PRIMARY KEY, value INTEGER)"); await db.exec("DELETE FROM counter"); await db.exec("INSERT INTO counter VALUES ('1', 0)") await db.push(); await db.close(); } const transform = (m: DatabaseRowMutation) => ({ operation: 'rewrite', stmt: { sql: `UPDATE counter SET value = value + ? WHERE key = ?`, values: [m.after.value - m.before.value, m.after.key] } } as DatabaseRowTransformResult); const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform }); const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform }); for (let i = 0; i < 1002; i++) { await db1.exec("UPDATE counter SET value = value + 1 WHERE key = '1'"); } for (let i = 0; i < 1001; i++) { await db2.exec("UPDATE counter SET value = value + 1 WHERE key = '1'"); } let start = performance.now(); await Promise.all([db1.push(), db2.push()]); console.info('push', performance.now() - start); start = performance.now(); await Promise.all([db1.pull(), db2.pull()]); console.info('pull', performance.now() - start); const rows1 = await db1.prepare('SELECT * FROM counter').all(); const rows2 = await db2.prepare('SELECT * FROM counter').all(); expect(rows1).toEqual([{ key: '1', value: 1001 + 1002 }]); expect(rows2).toEqual([{ key: '1', value: 1001 + 1002 }]); }) ================================================ FILE: bindings/javascript/sync/packages/wasm/tsconfig.json ================================================ { "compilerOptions": { "skipLibCheck": true, "declaration": true, "declarationMap": true, "module": "nodenext", "target": "esnext", "moduleResolution": "nodenext", "outDir": "dist/", "lib": [ "es2020", "DOM", "WebWorker" ], "paths": { "#index": [ "./index.js" ] } }, "include": [ "*" ] } ================================================ FILE: bindings/javascript/sync/packages/wasm/vite.config.js ================================================ import { resolve } from 'path'; import { defineConfig } from 'vite'; export default defineConfig({ base: './', build: { lib: { entry: resolve(__dirname, 'promise-bundle.ts'), name: 'sync-wasm', fileName: format => `main.${format}.js`, formats: ['es'], }, rollupOptions: { output: { dir: 'bundle', } }, }, }); ================================================ FILE: bindings/javascript/sync/packages/wasm/vitest.config.ts ================================================ import { defineConfig } from 'vitest/config' export default defineConfig({ define: { 'process.env.NODE_DEBUG_NATIVE': 'false', }, server: { headers: { "Cross-Origin-Embedder-Policy": "require-corp", "Cross-Origin-Opener-Policy": "same-origin" }, }, test: { browser: { enabled: true, provider: 'playwright', instances: [ { browser: 'chromium' }, { browser: 'firefox' } ], }, }, }) ================================================ FILE: bindings/javascript/sync/packages/wasm/wasm-inline.ts ================================================ const tursoWasmBase64 = '__PLACEHOLDER__'; async function convertBase64ToBinary(base64Url: string): Promise { const blob = await fetch(base64Url).then(res => res.blob()); return await blob.arrayBuffer(); } export async function tursoWasm(): Promise { return await convertBase64ToBinary(tursoWasmBase64); } ================================================ FILE: bindings/javascript/sync/packages/wasm/worker.ts ================================================ import { setupWebWorker } from "@tursodatabase/database-wasm-common"; setupWebWorker(); ================================================ FILE: bindings/javascript/sync/src/generator.rs ================================================ use napi::{bindgen_prelude::AsyncTask, Env, Task}; use napi_derive::napi; use std::{ future::Future, sync::{Arc, Mutex}, }; use turso_sync_engine::types::{DbChangesStatus, SyncEngineIoResult}; pub trait Generator { fn resume(&mut self, result: Option) -> napi::Result; } impl>> Generator for genawaiter::sync::Gen, F> { fn resume(&mut self, error: Option) -> napi::Result { let result = match error { Some(err) => Err(turso_sync_engine::errors::Error::DatabaseSyncEngineError( format!("JsSyncEngineIo error: {err}"), )), None => Ok(()), }; match self.resume_with(result) { genawaiter::GeneratorState::Yielded(SyncEngineIoResult::IO) => { Ok(GeneratorResponse::IO) } genawaiter::GeneratorState::Complete(Ok(())) => Ok(GeneratorResponse::Done), genawaiter::GeneratorState::Complete(Err(err)) => Err(napi::Error::new( napi::Status::GenericFailure, format!("sync engine operation failed: {err}"), )), } } } #[napi] pub struct SyncEngineChanges { pub(crate) status: Box>, } #[napi(discriminant = "type", object_from_js = false)] pub enum GeneratorResponse { IO, Done, SyncEngineStats { cdc_operations: i64, main_wal_size: i64, revert_wal_size: i64, last_pull_unix_time: Option, last_push_unix_time: Option, revision: Option, network_sent_bytes: i64, network_received_bytes: i64, }, SyncEngineChanges { changes: SyncEngineChanges, }, } #[napi] impl SyncEngineChanges { #[napi] pub fn empty(&self) -> bool { let Some(changes) = self.status.as_ref() else { return true; }; changes.file_slot.is_none() } } #[napi] #[derive(Clone)] pub struct GeneratorHolder { pub(crate) generator: Arc>, pub(crate) response: Arc>>, } pub struct ResumeTask { holder: GeneratorHolder, error: Option, } unsafe impl Send for ResumeTask {} impl Task for ResumeTask { type Output = GeneratorResponse; type JsValue = GeneratorResponse; fn compute(&mut self) -> napi::Result { resume_sync(&self.holder, self.error.take()) } fn resolve(&mut self, _: Env, output: Self::Output) -> napi::Result { Ok(output) } } fn resume_sync(holder: &GeneratorHolder, error: Option) -> napi::Result { let result = holder.generator.lock().unwrap().resume(error)?; if let GeneratorResponse::Done = result { let response = holder.response.lock().unwrap().take(); Ok(response.unwrap_or(GeneratorResponse::Done)) } else { Ok(result) } } #[napi] impl GeneratorHolder { #[napi] pub fn resume_sync(&self, error: Option) -> napi::Result { resume_sync(self, error) } #[napi] pub fn resume_async(&self, error: Option) -> napi::Result> { Ok(AsyncTask::new(ResumeTask { holder: self.clone(), error, })) } } ================================================ FILE: bindings/javascript/sync/src/js_protocol_io.rs ================================================ #![deny(clippy::all)] use std::{ collections::VecDeque, sync::{Arc, Mutex, MutexGuard}, }; use napi::bindgen_prelude::*; use napi_derive::napi; use turso_sync_engine::{ database_sync_engine_io::{DataCompletion, DataPollResult, SyncEngineIo}, types::{DatabaseRowTransformResult, DatabaseStatementReplay}, }; use crate::{ core_change_type_to_js, core_values_map_to_js, js_value_to_core, DatabaseRowMutationJs, DatabaseRowTransformResultJs, }; #[napi] pub enum JsProtocolRequest { Http { url: Option, method: String, path: String, body: Option>, headers: Vec<(String, String)>, }, FullRead { path: String, }, FullWrite { path: String, content: Vec, }, Transform { mutations: Vec, }, } #[derive(Clone)] #[napi] pub struct JsDataCompletion(Arc>); pub struct JsBytesPollResult(Buffer); impl DataPollResult for JsBytesPollResult { fn data(&self) -> &[u8] { &self.0 } } pub struct JsTransformPollResult(Vec); impl DataPollResult for JsTransformPollResult { fn data(&self) -> &[DatabaseRowTransformResult] { &self.0 } } struct JsDataCompletionInner { status: Option, chunks: VecDeque, transformed: VecDeque, finished: bool, err: Option, } impl JsDataCompletion { fn inner(&self) -> turso_sync_engine::Result> { let inner = self.0.lock().unwrap(); if let Some(err) = &inner.err { return Err(turso_sync_engine::errors::Error::DatabaseSyncEngineError( err.clone(), )); } Ok(inner) } } impl DataCompletion for JsDataCompletion { type DataPollResult = JsBytesPollResult; fn status(&self) -> turso_sync_engine::Result> { let inner = self.inner()?; Ok(inner.status) } fn poll_data(&self) -> turso_sync_engine::Result> { let mut inner = self.inner()?; let chunk = inner.chunks.pop_front(); Ok(chunk.map(JsBytesPollResult)) } fn is_done(&self) -> turso_sync_engine::Result { let inner = self.inner()?; Ok(inner.finished) } } impl DataCompletion for JsDataCompletion { type DataPollResult = JsTransformPollResult; fn status(&self) -> turso_sync_engine::Result> { let inner = self.inner()?; Ok(inner.status) } fn poll_data(&self) -> turso_sync_engine::Result> { let mut inner = self.inner()?; let chunk = inner.transformed.drain(..).collect::>(); if chunk.is_empty() { Ok(None) } else { Ok(Some(JsTransformPollResult(chunk))) } } fn is_done(&self) -> turso_sync_engine::Result { let inner = self.inner()?; Ok(inner.finished) } } #[napi] impl JsDataCompletion { #[napi] pub fn poison(&self, err: String) { let mut completion = self.0.lock().unwrap(); completion.err = Some(err); } #[napi] pub fn status(&self, value: u32) { let mut completion = self.0.lock().unwrap(); completion.status = Some(value as u16); } #[napi] pub fn push_buffer(&self, value: Buffer) { let mut completion = self.0.lock().unwrap(); completion.chunks.push_back(value); } #[napi] pub fn push_transform(&self, values: Vec) { let mut completion = self.0.lock().unwrap(); for value in values { completion.transformed.push_back(match value { DatabaseRowTransformResultJs::Keep => DatabaseRowTransformResult::Keep, DatabaseRowTransformResultJs::Skip => DatabaseRowTransformResult::Skip, DatabaseRowTransformResultJs::Rewrite { stmt } => { DatabaseRowTransformResult::Rewrite(DatabaseStatementReplay { sql: stmt.sql, values: stmt.values.into_iter().map(js_value_to_core).collect(), }) } }); } } #[napi] pub fn done(&self) { let mut completion = self.0.lock().unwrap(); completion.finished = true; } } #[napi] pub struct JsProtocolRequestBytes { request: Arc>>, completion: JsDataCompletion, } #[napi] impl JsProtocolRequestBytes { #[napi] pub fn request(&self) -> JsProtocolRequest { let mut request = self.request.lock().unwrap(); request.take().unwrap() } #[napi] pub fn completion(&self) -> JsDataCompletion { self.completion.clone() } } impl SyncEngineIo for JsProtocolIo { type DataCompletionBytes = JsDataCompletion; type DataCompletionTransform = JsDataCompletion; fn http( &self, url: Option<&str>, method: &str, path: &str, body: Option>, headers: &[(&str, &str)], ) -> turso_sync_engine::Result { Ok(self.add_request(JsProtocolRequest::Http { url: url.map(|x| x.to_string()), method: method.to_string(), path: path.to_string(), body, headers: headers .iter() .map(|x| (x.0.to_string(), x.1.to_string())) .collect(), })) } fn full_read(&self, path: &str) -> turso_sync_engine::Result { Ok(self.add_request(JsProtocolRequest::FullRead { path: path.to_string(), })) } fn full_write( &self, path: &str, content: Vec, ) -> turso_sync_engine::Result { Ok(self.add_request(JsProtocolRequest::FullWrite { path: path.to_string(), content, })) } fn transform( &self, mutations: Vec, ) -> turso_sync_engine::Result { Ok(self.add_request(JsProtocolRequest::Transform { mutations: mutations .into_iter() .filter_map(|mutation| { let change_type = core_change_type_to_js(mutation.change_type)?; Some(DatabaseRowMutationJs { change_time: mutation.change_time as i64, table_name: mutation.table_name, id: mutation.id, change_type, before: mutation.before.map(core_values_map_to_js), after: mutation.after.map(core_values_map_to_js), updates: mutation.updates.map(core_values_map_to_js), }) }) .collect(), })) } fn add_io_callback(&self, callback: Box bool + Send>) { let mut work = self.work.lock().unwrap(); work.push_back(callback); } fn step_io_callbacks(&self) { let mut items = { let mut work = self.work.lock().unwrap(); work.drain(..).collect::>() }; let length = items.len(); for _ in 0..length { let mut item = items.pop_front().unwrap(); if item() { continue; } items.push_back(item); } { let mut work = self.work.lock().unwrap(); work.extend(items); } } } #[napi] pub struct JsProtocolIo { requests: Mutex>, work: Mutex bool + Send>>>, } impl Default for JsProtocolIo { fn default() -> Self { Self { requests: Mutex::new(Vec::new()), work: Mutex::new(VecDeque::new()), } } } impl JsProtocolIo { pub fn take_request(&self) -> Option { self.requests.lock().unwrap().pop() } fn add_request(&self, request: JsProtocolRequest) -> JsDataCompletion { let completion = JsDataCompletionInner { chunks: VecDeque::new(), transformed: VecDeque::new(), finished: false, err: None, status: None, }; let completion = JsDataCompletion(Arc::new(Mutex::new(completion))); let mut requests = self.requests.lock().unwrap(); requests.push(JsProtocolRequestBytes { request: Arc::new(Mutex::new(Some(request))), completion: completion.clone(), }); completion } } ================================================ FILE: bindings/javascript/sync/src/lib.rs ================================================ #![allow(clippy::await_holding_lock)] #![allow(clippy::type_complexity)] pub mod generator; pub mod js_protocol_io; use std::{ collections::HashMap, sync::{Arc, Mutex, RwLock, RwLockReadGuard}, }; use napi::bindgen_prelude::{AsyncTask, Either5, Null}; use napi_derive::napi; use turso_node::{DatabaseOpts, IoLoopTask}; use turso_sync_engine::{ database_sync_engine::{DatabaseSyncEngine, DatabaseSyncEngineOpts}, database_sync_engine_io::SyncEngineIo, database_sync_operations::SyncEngineIoStats, types::{ Coro, DatabaseChangeType, DatabaseSyncEngineProtocolVersion, PartialBootstrapStrategy, PartialSyncOpts, }, }; use crate::{ generator::{GeneratorHolder, GeneratorResponse, SyncEngineChanges}, js_protocol_io::{JsProtocolIo, JsProtocolRequestBytes}, }; #[napi] pub struct SyncEngine { opts: SyncEngineOptsFilled, io: Option>, protocol: Option>, sync_engine: Arc>>>, db: Arc>, } #[napi(string_enum = "lowercase")] pub enum DatabaseChangeTypeJs { Insert, Update, Delete, } #[napi(string_enum = "lowercase")] pub enum SyncEngineProtocolVersion { Legacy, V1, } fn core_change_type_to_js(value: DatabaseChangeType) -> Option { match value { DatabaseChangeType::Delete => Some(DatabaseChangeTypeJs::Delete), DatabaseChangeType::Update => Some(DatabaseChangeTypeJs::Update), DatabaseChangeType::Insert => Some(DatabaseChangeTypeJs::Insert), DatabaseChangeType::Commit => None, } } fn js_value_to_core(value: Either5>) -> turso_core::Value { match value { Either5::A(_) => turso_core::Value::Null, Either5::B(value) => turso_core::Value::from_i64(value), Either5::C(value) => turso_core::Value::from_f64(value), Either5::D(value) => turso_core::Value::Text(turso_core::types::Text::new(value)), Either5::E(value) => turso_core::Value::Blob(value), } } fn core_value_to_js(value: turso_core::Value) -> Either5> { match value { turso_core::Value::Null => Either5::>::A(Null), turso_core::Value::Numeric(turso_core::Numeric::Integer(value)) => { Either5::>::B(value) } turso_core::Value::Numeric(turso_core::Numeric::Float(value)) => { Either5::>::C(f64::from(value)) } turso_core::Value::Text(value) => { Either5::>::D(value.as_str().to_string()) } turso_core::Value::Blob(value) => Either5::>::E(value), } } fn core_values_map_to_js( value: HashMap, ) -> HashMap>> { let mut result = HashMap::new(); for (key, value) in value { result.insert(key, core_value_to_js(value)); } result } #[napi(object)] pub struct DatabaseRowMutationJs { pub change_time: i64, pub table_name: String, pub id: i64, pub change_type: DatabaseChangeTypeJs, pub before: Option>>>, pub after: Option>>>, pub updates: Option>>>, } #[napi(object)] #[derive(Debug)] pub struct DatabaseRowStatementJs { pub sql: String, pub values: Vec>>, } #[napi(discriminant = "type")] #[derive(Debug)] pub enum DatabaseRowTransformResultJs { Keep, Skip, Rewrite { stmt: DatabaseRowStatementJs }, } #[napi(discriminant = "type")] #[derive(Debug)] pub enum JsPartialBootstrapStrategy { Prefix { length: i64 }, Query { query: String }, } #[napi(object)] pub struct JsPartialSyncOpts { pub bootstrap_strategy: JsPartialBootstrapStrategy, pub segment_size: Option, pub prefetch: Option, } #[napi(object, object_to_js = false)] pub struct SyncEngineOpts { pub path: String, pub remote_url: Option, pub client_name: Option, pub wal_pull_batch_size: Option, pub long_poll_timeout_ms: Option, pub tracing: Option, pub tables_ignore: Option>, pub use_transform: bool, pub protocol_version: Option, pub bootstrap_if_empty: bool, /// Encryption cipher for the Turso Cloud database. pub remote_encryption_cipher: Option, /// Base64-encoded encryption key for the Turso Cloud database. /// Must match the key used when creating the encrypted database. pub remote_encryption_key: Option, pub partial_sync_opts: Option, } struct SyncEngineOptsFilled { pub path: String, pub remote_url: Option, pub client_name: String, pub wal_pull_batch_size: u32, pub long_poll_timeout: Option, pub tables_ignore: Vec, pub use_transform: bool, pub protocol_version: DatabaseSyncEngineProtocolVersion, pub bootstrap_if_empty: bool, pub remote_encryption_cipher: Option, pub remote_encryption_key: Option, pub partial_sync_opts: Option, } #[derive(Debug, Clone, Copy)] enum CipherMode { Aes256Gcm, Aes128Gcm, ChaCha20Poly1305, Aegis128L, Aegis128X2, Aegis128X4, Aegis256, Aegis256X2, Aegis256X4, } impl CipherMode { /// Returns the total metadata size (nonce + tag) for this cipher mode. /// These values match the Turso Cloud encryption settings. fn required_metadata_size(&self) -> usize { match self { CipherMode::Aes256Gcm | CipherMode::Aes128Gcm | CipherMode::ChaCha20Poly1305 => 28, CipherMode::Aegis128L | CipherMode::Aegis128X2 | CipherMode::Aegis128X4 => 32, CipherMode::Aegis256 | CipherMode::Aegis256X2 | CipherMode::Aegis256X4 => 48, } } } #[napi] impl SyncEngine { #[napi(constructor)] pub fn new(opts: SyncEngineOpts) -> napi::Result { let is_memory = opts.path == ":memory:"; let io: Arc = if is_memory { Arc::new(turso_core::MemoryIO::new()) } else { #[cfg(all(target_os = "linux", not(feature = "browser")))] { if opts.partial_sync_opts.is_none() { Arc::new(turso_core::PlatformIO::new().map_err(|e| { napi::Error::new( napi::Status::GenericFailure, format!("Failed to create platform IO: {e}"), ) })?) } else { use turso_sync_engine::sparse_io::SparseLinuxIo; Arc::new(SparseLinuxIo::new().map_err(|e| { napi::Error::new( napi::Status::GenericFailure, format!("Failed to create sparse IO: {e}"), ) })?) } } #[cfg(all(not(target_os = "linux"), not(feature = "browser")))] { Arc::new(turso_core::PlatformIO::new().map_err(|e| { napi::Error::new( napi::Status::GenericFailure, format!("Failed to create platform IO: {e}"), ) })?) } #[cfg(feature = "browser")] { turso_node::browser::opfs() } }; #[allow(clippy::arc_with_non_send_sync)] let db = Arc::new(Mutex::new(turso_node::Database::new_with_io( opts.path.clone(), io.clone(), Some(DatabaseOpts { file_must_exist: None, readonly: None, timeout: None, tracing: opts.tracing.clone(), experimental: None, encryption: None, // Local encryption not supported in sync mode }), )?)); let opts_filled = SyncEngineOptsFilled { path: opts.path, remote_url: opts.remote_url, client_name: opts .client_name .unwrap_or_else(|| "turso-sync-js".to_string()), wal_pull_batch_size: opts.wal_pull_batch_size.unwrap_or(100), long_poll_timeout: opts .long_poll_timeout_ms .map(|x| std::time::Duration::from_millis(x as u64)), tables_ignore: opts.tables_ignore.unwrap_or_default(), use_transform: opts.use_transform, protocol_version: match opts.protocol_version { Some(SyncEngineProtocolVersion::Legacy) | None => { DatabaseSyncEngineProtocolVersion::Legacy } _ => DatabaseSyncEngineProtocolVersion::V1, }, bootstrap_if_empty: opts.bootstrap_if_empty, remote_encryption_cipher: match opts.remote_encryption_cipher.as_deref() { Some("aes256gcm") | Some("aes-256-gcm") => Some(CipherMode::Aes256Gcm), Some("aes128gcm") | Some("aes-128-gcm") => Some(CipherMode::Aes128Gcm), Some("chacha20poly1305") | Some("chacha20-poly1305") => { Some(CipherMode::ChaCha20Poly1305) } Some("aegis128l") | Some("aegis-128l") => Some(CipherMode::Aegis128L), Some("aegis128x2") | Some("aegis-128x2") => Some(CipherMode::Aegis128X2), Some("aegis128x4") | Some("aegis-128x4") => Some(CipherMode::Aegis128X4), Some("aegis256") | Some("aegis-256") => Some(CipherMode::Aegis256), Some("aegis256x2") | Some("aegis-256x2") => Some(CipherMode::Aegis256X2), Some("aegis256x4") | Some("aegis-256x4") => Some(CipherMode::Aegis256X4), None => None, _ => { return Err(napi::Error::new( napi::Status::GenericFailure, "unsupported remote cipher. Supported: aes256gcm, aes128gcm, \ chacha20poly1305, aegis128l, aegis128x2, aegis128x4, aegis256, \ aegis256x2, aegis256x4", )) } }, partial_sync_opts: match opts.partial_sync_opts { Some(partial_sync_opts) => match partial_sync_opts.bootstrap_strategy { JsPartialBootstrapStrategy::Prefix { length } => Some(PartialSyncOpts { bootstrap_strategy: Some(PartialBootstrapStrategy::Prefix { length: length as usize, }), segment_size: partial_sync_opts.segment_size.unwrap_or(0) as usize, prefetch: partial_sync_opts.prefetch.unwrap_or(false), }), JsPartialBootstrapStrategy::Query { query } => Some(PartialSyncOpts { bootstrap_strategy: Some(PartialBootstrapStrategy::Query { query }), segment_size: partial_sync_opts.segment_size.unwrap_or(0) as usize, prefetch: partial_sync_opts.prefetch.unwrap_or(false), }), }, None => None, }, remote_encryption_key: opts.remote_encryption_key.clone(), }; Ok(SyncEngine { opts: opts_filled, #[allow(clippy::arc_with_non_send_sync)] sync_engine: Arc::new(RwLock::new(None)), io: Some(io), protocol: Some(Arc::new(JsProtocolIo::default())), #[allow(clippy::arc_with_non_send_sync)] db, }) } #[napi] pub fn connect(&mut self) -> napi::Result { let opts = DatabaseSyncEngineOpts { client_name: self.opts.client_name.clone(), remote_url: self.opts.remote_url.clone(), wal_pull_batch_size: self.opts.wal_pull_batch_size as u64, long_poll_timeout: self.opts.long_poll_timeout, tables_ignore: self.opts.tables_ignore.clone(), use_transform: self.opts.use_transform, protocol_version_hint: self.opts.protocol_version, bootstrap_if_empty: self.opts.bootstrap_if_empty, reserved_bytes: self .opts .remote_encryption_cipher .map(|x| x.required_metadata_size()) .unwrap_or(0), partial_sync_opts: self.opts.partial_sync_opts.clone(), remote_encryption_key: self.opts.remote_encryption_key.clone(), }; let io = self.io()?; let protocol = self.protocol()?; let sync_engine = self.sync_engine.clone(); let db = self.db.clone(); let path = self.opts.path.clone(); let generator = genawaiter::sync::Gen::new(|coro| async move { let coro = Coro::new((), coro); let initialized = DatabaseSyncEngine::create_db( &coro, io.clone(), SyncEngineIoStats::new(protocol), &path, opts, ) .await?; let connection = initialized.connect_rw(&coro).await?; db.lock().unwrap().set_connected(connection).map_err(|e| { turso_sync_engine::errors::Error::DatabaseSyncEngineError(format!( "failed to connect sync engine: {e}" )) })?; *sync_engine.write().unwrap() = Some(initialized); Ok(()) }); Ok(GeneratorHolder { #[allow(clippy::arc_with_non_send_sync)] generator: Arc::new(Mutex::new(generator)), response: Arc::new(Mutex::new(None)), }) } #[napi] pub fn io_loop_sync(&self) -> napi::Result<()> { self.io()?.step().map_err(|e| { napi::Error::new(napi::Status::GenericFailure, format!("IO error: {e}")) })?; Ok(()) } /// Runs the I/O loop asynchronously, returning a Promise. #[napi(ts_return_type = "Promise")] pub fn io_loop_async(&self) -> napi::Result> { let io = self.io()?; Ok(AsyncTask::new(IoLoopTask { io })) } #[napi] pub fn protocol_io(&self) -> napi::Result> { Ok(self.protocol()?.take_request()) } #[napi] pub fn protocol_io_step(&self) -> napi::Result<()> { self.protocol()?.step_io_callbacks(); Ok(()) } #[napi] pub fn push(&self) -> GeneratorHolder { self.run(async move |coro, guard| { let sync_engine = try_read(guard)?; let sync_engine = try_unwrap(&sync_engine)?; sync_engine.push_changes_to_remote(coro).await?; Ok(None) }) } #[napi] pub fn stats(&self) -> GeneratorHolder { self.run(async move |coro, guard| { let sync_engine = try_read(guard)?; let sync_engine = try_unwrap(&sync_engine)?; let stats = sync_engine.stats(coro).await?; Ok(Some(GeneratorResponse::SyncEngineStats { cdc_operations: stats.cdc_operations, main_wal_size: stats.main_wal_size as i64, revert_wal_size: stats.revert_wal_size as i64, last_pull_unix_time: stats.last_pull_unix_time, last_push_unix_time: stats.last_push_unix_time, revision: stats.revision, network_sent_bytes: stats.network_sent_bytes as i64, network_received_bytes: stats.network_received_bytes as i64, })) }) } #[napi] pub fn wait(&self) -> GeneratorHolder { self.run(async move |coro, guard| { let sync_engine = try_read(guard)?; let sync_engine = try_unwrap(&sync_engine)?; Ok(Some(GeneratorResponse::SyncEngineChanges { changes: SyncEngineChanges { status: Box::new(Some(sync_engine.wait_changes_from_remote(coro).await?)), }, })) }) } #[napi] pub fn apply(&self, changes: &mut SyncEngineChanges) -> GeneratorHolder { let status = changes.status.take().unwrap(); self.run(async move |coro, guard| { let sync_engine = try_read(guard)?; let sync_engine = try_unwrap(&sync_engine)?; sync_engine.apply_changes_from_remote(coro, status).await?; Ok(None) }) } #[napi] pub fn checkpoint(&self) -> GeneratorHolder { self.run(async move |coro, guard| { let sync_engine = try_read(guard)?; let sync_engine = try_unwrap(&sync_engine)?; sync_engine.checkpoint(coro).await?; Ok(None) }) } #[napi] pub fn db(&self) -> napi::Result { Ok(self.db.lock().unwrap().clone()) } #[napi] pub fn close(&mut self) { let _ = self.sync_engine.write().unwrap().take(); let _ = self.db.lock().unwrap().close(); let _ = self.io.take(); let _ = self.protocol.take(); } fn io(&self) -> napi::Result> { if self.io.is_none() { return Err(napi::Error::new( napi::Status::GenericFailure, "sync engine was closed", )); } Ok(self.io.as_ref().unwrap().clone()) } fn protocol(&self) -> napi::Result> { if self.protocol.is_none() { return Err(napi::Error::new( napi::Status::GenericFailure, "sync engine was closed", )); } Ok(self.protocol.as_ref().unwrap().clone()) } fn run( &self, f: impl AsyncFnOnce( &Coro<()>, &Arc>>>, ) -> turso_sync_engine::Result> + 'static, ) -> GeneratorHolder { let response = Arc::new(Mutex::new(None)); let sync_engine = self.sync_engine.clone(); #[allow(clippy::await_holding_lock)] let generator = genawaiter::sync::Gen::new({ let response = response.clone(); |coro| async move { let coro = Coro::new((), coro); *response.lock().unwrap() = f(&coro, &sync_engine).await?; Ok(()) } }); GeneratorHolder { generator: Arc::new(Mutex::new(generator)), response, } } } fn try_read( sync_engine: &RwLock>>, ) -> turso_sync_engine::Result>>> { let Ok(sync_engine) = sync_engine.try_read() else { let nasty_error = "sync_engine is busy".to_string(); return Err(turso_sync_engine::errors::Error::DatabaseSyncEngineError( nasty_error, )); }; Ok(sync_engine) } fn try_unwrap<'a>( sync_engine: &'a RwLockReadGuard<'_, Option>>, ) -> turso_sync_engine::Result<&'a DatabaseSyncEngine> { let Some(sync_engine) = sync_engine.as_ref() else { let error = "sync_engine must be initialized".to_string(); return Err(turso_sync_engine::errors::Error::DatabaseSyncEngineError( error, )); }; Ok(sync_engine) } ================================================ FILE: bindings/javascript/turso-sql-runner.mjs ================================================ #!/usr/bin/env node /** * SQL runner script for test-runner JavaScript backend. * Reads SQL from stdin, executes via @tursodatabase/database, outputs pipe-separated results. * * Usage: node turso-sql-runner.mjs [--readonly] * * This script expects to be run from the bindings/javascript directory where * the @tursodatabase/database package is available. * * Known limitations: * - JavaScript's number type doesn't distinguish between 1 and 1.0, so float * formatting may differ from the Rust backend for whole-number floats. * - Very large integers (exceeding i64) may have precision loss as JavaScript * numbers are IEEE 754 doubles with 53 bits of mantissa precision. */ import { pathToFileURL } from 'node:url'; import { splitStatements } from './turso-sql-split.mjs'; async function readStdin() { const chunks = []; for await (const chunk of process.stdin) { chunks.push(chunk); } return Buffer.concat(chunks).toString('utf-8'); } function formatValue(value) { if (value === null || value === undefined) { return ''; } if (typeof value === 'bigint') { return value.toString(); } if (typeof value === 'number') { // Handle special float values to match SQLite output if (value === Infinity) { return 'Inf'; } if (value === -Infinity) { return '-Inf'; } if (Number.isNaN(value)) { return ''; // SQLite returns NULL for NaN } // For integers, use toString() directly if (Number.isInteger(value)) { return value.toString(); } // SQLite uses %.15g format (15 significant digits, trailing zeros removed) // toPrecision gives significant digits, parseFloat removes trailing zeros return parseFloat(value.toPrecision(15)).toString(); } if (value instanceof Uint8Array || Buffer.isBuffer(value)) { // Output blob as raw bytes (matches SQLite/Rust backend behavior) // This will display as text if the bytes are printable ASCII return Buffer.from(value).toString('utf-8'); } return String(value); } function formatRow(row) { // Row is an array in raw mode return row.map(formatValue).join('|'); } async function main() { const args = process.argv.slice(2); if (args.length < 1) { console.error('Usage: turso-sql-runner.mjs [--readonly]'); process.exit(1); } const dbPath = args[0]; const readonly = args.includes('--readonly'); const sql = await readStdin(); if (!sql.trim()) { process.exit(0); } let db; try { const { connect } = await import('@tursodatabase/database'); db = await connect(dbPath, { readonly, experimental: ['triggers', 'attach'] }); // Enable safe integers to preserve precision for large integers db.defaultSafeIntegers(true); } catch (err) { console.error(`Error: ${err.message}`); process.exit(1); } try { // Split into individual statements, filtering out comments and empty lines const statements = splitStatements(sql); // Accumulate results from ALL queries (matches Rust backend behavior) const allResults = []; for (const stmt of statements) { const trimmed = stmt.trim(); if (!trimmed) continue; const prepared = db.prepare(trimmed); prepared.raw(true); const rows = await prepared.all(); allResults.push(...rows); prepared.close(); } // Output all accumulated results for (const row of allResults) { console.log(formatRow(row)); } } catch (err) { // Output error in a format the test runner can detect console.log(`Error: ${err.message}`); process.exit(0); // Exit 0 so the error can be captured as output } finally { if (db) { await db.close(); } } } const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; if (isMain) { main().catch(err => { console.error(`Error: ${err.message}`); process.exit(1); }); } ================================================ FILE: bindings/javascript/turso-sql-split.mjs ================================================ const ReadState = Object.freeze({ Invalid: 'Invalid', Start: 'Start', Normal: 'Normal', Explain: 'Explain', Create: 'Create', Trigger: 'Trigger', Semi: 'Semi', End: 'End', }); const Token = Object.freeze({ TkSemi: 'TkSemi', TkWhitespace: 'TkWhitespace', TkOther: 'TkOther', TkExplain: 'TkExplain', TkCreate: 'TkCreate', TkTemp: 'TkTemp', TkTrigger: 'TkTrigger', TkEnd: 'TkEnd', }); function isAsciiWhitespace(char) { const code = char.charCodeAt(0); return code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0b || code === 0x0c || code === 0x0d; } function isAsciiAlpha(char) { const code = char.charCodeAt(0); return (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a); } function isAsciiAlnum(char) { const code = char.charCodeAt(0); return isAsciiAlpha(char) || (code >= 0x30 && code <= 0x39); } function classifyKeyword(word) { switch (word) { case 'EXPLAIN': return Token.TkExplain; case 'CREATE': return Token.TkCreate; case 'TEMP': case 'TEMPORARY': return Token.TkTemp; case 'TRIGGER': return Token.TkTrigger; case 'END': return Token.TkEnd; default: return Token.TkOther; } } function nextToken(sql, start) { let i = start; while (i < sql.length) { const char = sql[i]; const nextChar = sql[i + 1]; if (char === '\'' || char === '"' || char === '`' || char === '[') { const endChar = char === '[' ? ']' : char; i++; while (i < sql.length) { const current = sql[i]; i++; if (current === endChar) { break; } } continue; } if (char === '-' && nextChar === '-') { i += 2; while (i < sql.length && sql[i] !== '\n') { i++; } continue; } if (char === '/' && nextChar === '*') { i += 2; let sawStar = false; while (i < sql.length) { const current = sql[i]; i++; if (sawStar && current === '/') { break; } sawStar = current === '*'; } continue; } if (char === ';') { return { token: Token.TkSemi, nextIndex: i + 1, tokenEnd: i + 1, }; } if (isAsciiWhitespace(char)) { return { token: Token.TkWhitespace, nextIndex: i + 1, tokenEnd: i + 1, }; } if (isAsciiAlpha(char) || char === '_') { let end = i + 1; while (end < sql.length && (isAsciiAlnum(sql[end]) || sql[end] === '_')) { end++; } return { token: classifyKeyword(sql.slice(i, end).toUpperCase()), nextIndex: end, tokenEnd: end, }; } return { token: Token.TkOther, nextIndex: i + 1, tokenEnd: i + 1, }; } return null; } function transition(state, token) { switch (state) { case ReadState.Invalid: switch (token) { case Token.TkSemi: return ReadState.Start; case Token.TkWhitespace: return ReadState.Invalid; case Token.TkExplain: return ReadState.Explain; case Token.TkCreate: return ReadState.Create; default: return ReadState.Normal; } case ReadState.Start: switch (token) { case Token.TkSemi: return ReadState.Start; case Token.TkWhitespace: return ReadState.Start; case Token.TkExplain: return ReadState.Explain; case Token.TkCreate: return ReadState.Create; default: return ReadState.Normal; } case ReadState.Normal: switch (token) { case Token.TkSemi: return ReadState.Start; case Token.TkWhitespace: return ReadState.Normal; default: return ReadState.Normal; } case ReadState.Explain: switch (token) { case Token.TkSemi: return ReadState.Start; case Token.TkWhitespace: return ReadState.Explain; case Token.TkCreate: return ReadState.Create; case Token.TkExplain: case Token.TkTemp: case Token.TkTrigger: case Token.TkEnd: return ReadState.Normal; default: return ReadState.Explain; } case ReadState.Create: switch (token) { case Token.TkSemi: return ReadState.Start; case Token.TkWhitespace: return ReadState.Create; case Token.TkTemp: return ReadState.Create; case Token.TkTrigger: return ReadState.Trigger; default: return ReadState.Normal; } case ReadState.Trigger: switch (token) { case Token.TkSemi: return ReadState.Semi; case Token.TkWhitespace: return ReadState.Trigger; default: return ReadState.Trigger; } case ReadState.Semi: switch (token) { case Token.TkSemi: return ReadState.Semi; case Token.TkWhitespace: return ReadState.Semi; case Token.TkEnd: return ReadState.End; default: return ReadState.Trigger; } case ReadState.End: switch (token) { case Token.TkSemi: return ReadState.Start; case Token.TkWhitespace: return ReadState.End; default: return ReadState.Trigger; } default: return ReadState.Invalid; } } function hasMeaningfulSql(sql) { let index = 0; while (true) { const token = nextToken(sql, index); if (!token) { return false; } if (token.token !== Token.TkWhitespace && token.token !== Token.TkSemi) { return true; } index = token.nextIndex; } } /** * Split SQL text into individual statements using sqlite3_complete-like semantics. * * This matches testing/sqltests/src/parser/sql_complete.rs so CREATE TRIGGER bodies * containing semicolons are treated as a single statement until the ;END; sentinel. */ function splitStatements(sql) { const statements = []; let state = ReadState.Invalid; let statementStart = 0; let index = 0; while (true) { const tokenInfo = nextToken(sql, index); if (!tokenInfo) { break; } const newState = transition(state, tokenInfo.token); if (newState === ReadState.Start && state !== ReadState.Start) { const candidate = sql.slice(statementStart, tokenInfo.tokenEnd).trim(); if (candidate && hasMeaningfulSql(candidate)) { statements.push(candidate); } statementStart = tokenInfo.tokenEnd; } state = newState; index = tokenInfo.nextIndex; } const remainder = sql.slice(statementStart).trim(); if (remainder && hasMeaningfulSql(remainder)) { statements.push(remainder); } return statements; } export { splitStatements }; ================================================ FILE: bindings/python/Cargo.toml ================================================ [package] name = "py-turso" version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true publish = false [lints] workspace = true [lib] name = "_turso" crate-type = ["cdylib"] [features] # must be enabled when building with `cargo build`, maturin enables this automatically extension-module = ["pyo3/extension-module"] [dependencies] anyhow = "1.0" turso_sdk_kit = { workspace = true } turso_sync_sdk_kit = { workspace = true } pyo3 = { version = "0.27.1", features = ["anyhow"] } [build-dependencies] version_check = "0.9.5" # used where logic has to be version/distribution specific, e.g. pypy pyo3-build-config = { version = "0.27.0" } ================================================ FILE: bindings/python/README.md ================================================

Turso Database for Python

PyPI

Chat with other users of Turso on Discord

--- ## About > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups. ## Features - **SQLite compatible:** SQLite query language and file format support ([status](https://github.com/tursodatabase/turso/blob/main/COMPAT.md)). - **In-process**: No network overhead, runs directly in your Python process - **Cross-platform**: Supports Linux, macOS, Windows - **Remote partial sync**: Bootstrap from a remote database, pull remote changes, and push local changes when online — all while enjoying a fully operational database offline. - **Asyncio support**: Built-in integration with asyncio to ensure queries won’t block your event loop ## Installation ```bash uv pip install pyturso ``` ## Database driver A minimal DB‑API 2.0 example using an in‑memory database: ```python import turso # Standard DB-API usage conn = turso.connect(":memory:") cur = conn.cursor() cur.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT)") cur.execute("INSERT INTO users VALUES (1, 'alice'), (2, 'bob')") cur.execute("SELECT * FROM users ORDER BY id") rows = cur.fetchall() print(rows) # [(1, 'alice'), (2, 'bob')] conn.close() ``` ## Database driver (asyncio) Non-blocking access with asyncio: ```python import asyncio import turso.aio async def main(): # Connect and use as an async context manager async with turso.aio.connect(":memory:") as conn: # Executes multiple statements await conn.executescript(""" CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); INSERT INTO t(name) VALUES ('alice'), ('bob'); """) # Use a cursor for parameterized queries cur = conn.cursor() await cur.execute("SELECT COUNT(*) FROM t WHERE name LIKE ?", ("a%",)) count = (await cur.fetchone())[0] print(count) # 1 # JSON and generate_series also available cur = conn.cursor() await cur.execute("SELECT SUM(value) FROM generate_series(1, 10)") print((await cur.fetchone())[0]) # 55 asyncio.run(main()) ``` ## Synchronization driver Use a remote Turso database while working locally. You can bootstrap local state from the remote, pull remote changes, and push local commits. Note: You need a Turso remote URL. See the Turso docs for provisioning and authentication. ```python import turso.sync # Connect a local database to a remote Turso database conn = turso.sync.connect( ":memory:", # local db path (or a file path) remote_url="https://..turso.io" # your remote URL ) # Read data (fetched from remote if not present locally yet) rows = conn.execute("SELECT * FROM t").fetchall() print(rows) # Pull new changes from remote into local changed = conn.pull() print("Pulled:", changed) # True if there were new remote changes # Make local changes conn.execute("INSERT INTO t VALUES ('push works')") conn.commit() # Push local commits to remote conn.push() # Optional: inspect and manage sync state stats = conn.stats() print("Network received (bytes):", stats.network_received_bytes) conn.checkpoint() # compact local WAL after many writes conn.close() ``` Partial bootstrap to reduce initial network cost: ```python import turso.sync conn = turso.sync.connect( "local.db", remote_url="https://..turso.io", # fetch first 128 KiB upfront partial_sync_experimental=turso.sync.PartialSyncOpts( bootstrap_strategy=turso.sync.PartialSyncPrefixBootstrap(length=128 * 1024), ), ) ``` ## Synchronization driver (asyncio) The same sync primitives, but fully async: ```python import asyncio async def main(): conn = await turso.aio.sync.connect(":memory:", remote_url="https://..turso.io") # Read data rows = await (await conn.execute("SELECT * FROM t")).fetchall() print(rows) # Pull and push await conn.pull() await conn.execute("INSERT INTO t VALUES ('hello from asyncio')") await conn.commit() await conn.push() # Stats and maintenance stats = await conn.stats() print("Main WAL size:", stats.main_wal_size) await conn.checkpoint() await conn.close() asyncio.run(main()) ``` ## License This project is licensed under the [MIT license](../../LICENSE.md). ## Support - [GitHub Issues](https://github.com/tursodatabase/turso/issues) - [Documentation](https://docs.turso.tech) - [Discord Community](https://tur.so/discord) ================================================ FILE: bindings/python/SQLALCHEMY_DIALECT.md ================================================ # SQLAlchemy Dialect for Pyturso This document describes the SQLAlchemy dialect implementation for pyturso. ## Status: Implemented The SQLAlchemy dialect is fully implemented with two dialects: - `sqlite+turso://` - Basic local database connections - `sqlite+turso_sync://` - Sync-enabled connections with remote database support ## Installation ```bash pip install pyturso[sqlalchemy] ``` ## Quick Start ### Basic Local Connection ```python from sqlalchemy import create_engine, text # In-memory database engine = create_engine("sqlite+turso:///:memory:") # File-based database engine = create_engine("sqlite+turso:///path/to/database.db") with engine.connect() as conn: conn.execute(text("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")) conn.execute(text("INSERT INTO users (name) VALUES ('Alice')")) conn.commit() result = conn.execute(text("SELECT * FROM users")) for row in result: print(row) ``` ### Sync-Enabled Connection (Remote Sync) ```python from sqlalchemy import create_engine, text from turso.sqlalchemy import get_sync_connection # Via URL query parameters engine = create_engine( "sqlite+turso_sync:///local.db" "?remote_url=https://your-db.turso.io" "&auth_token=your-token" ) # Or via connect_args (supports callables for dynamic tokens) engine = create_engine( "sqlite+turso_sync:///local.db", connect_args={ "remote_url": "https://your-db.turso.io", "auth_token": lambda: get_fresh_token(), } ) with engine.connect() as conn: # Access sync operations sync = get_sync_connection(conn) sync.pull() # Pull changes from remote result = conn.execute(text("SELECT * FROM users")) conn.execute(text("INSERT INTO users (name) VALUES ('Bob')")) conn.commit() sync.push() # Push changes to remote ``` ### ORM Usage ```python from sqlalchemy import create_engine, Column, Integer, String from sqlalchemy.orm import declarative_base, Session Base = declarative_base() class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True) name = Column(String(100)) engine = create_engine("sqlite+turso:///:memory:") Base.metadata.create_all(engine) with Session(engine) as session: session.add(User(name="Alice")) session.commit() users = session.query(User).all() ``` ## URL Formats ### Basic Dialect (`sqlite+turso://`) ``` sqlite+turso:///path/to/database.db sqlite+turso:///:memory: sqlite+turso:///db.db?isolation_level=IMMEDIATE ``` Query parameters: - `isolation_level` - Transaction isolation level (DEFERRED, IMMEDIATE, EXCLUSIVE, AUTOCOMMIT) - `experimental_features` - Comma-separated feature flags ### Sync Dialect (`sqlite+turso_sync://`) ``` sqlite+turso_sync:///local.db?remote_url=https://db.turso.io&auth_token=xxx ``` Query parameters: - `remote_url` (required) - Remote Turso/libsql server URL - `auth_token` - Authentication token - `client_name` - Client identifier (default: turso-sqlalchemy) - `long_poll_timeout_ms` - Long poll timeout in milliseconds - `bootstrap_if_empty` - Bootstrap from remote if local empty (default: true) - `isolation_level` - Transaction isolation level - `experimental_features` - Comma-separated feature flags URL validation: - Username/password in URL raises `ValueError` (use `auth_token` instead) - Host/port in URL raises `ValueError` (use `remote_url` query param instead) - Unrecognized query parameters emit a `UserWarning` ## Sync Operations The `get_sync_connection()` helper provides access to sync-specific methods: ```python from turso.sqlalchemy import get_sync_connection with engine.connect() as conn: sync = get_sync_connection(conn) # Pull changes from remote (returns True if updates were pulled) if sync.pull(): print("Pulled new changes!") # Push local changes to remote sync.push() # Checkpoint the WAL sync.checkpoint() # Get sync statistics stats = sync.stats() print(f"Network received: {stats.network_received_bytes} bytes") ``` `get_sync_connection()` raises `TypeError` if called on a non-sync connection (e.g. a plain `sqlite+turso://` or standard `sqlite://` engine). ## Architecture ``` _TursoDialectMixin (reflection overrides) │ │ SQLiteDialect_pysqlite (SQLAlchemy built-in) │ │ ├───────────┤ │ │ ├── TursoDialect (sqlite+turso://) │ ├── uses turso.connect() │ └── pool: SingletonThreadPool (:memory:) / QueuePool (file) │ └── TursoSyncDialect (sqlite+turso_sync://) ├── uses turso.sync.connect() ├── pool: SingletonThreadPool (:memory:) / QueuePool (file) └── get_sync_connection() → ConnectionSync (pull/push/checkpoint/stats) ``` Both dialects use Python MRO: `_TursoDialectMixin` provides PRAGMA-related overrides, `SQLiteDialect_pysqlite` provides core SQLite dialect behavior. ## What Pyturso Provides | Requirement | Status | |-------------|--------| | `apilevel = "2.0"` | Provided | | `threadsafety = 1` | Provided | | `paramstyle = "qmark"` | Provided | | `sqlite_version` | Provided | | `sqlite_version_info` | Provided | | `connect()` function | Provided | | `Connection` class | Provided | | `Cursor` class | Provided | | Exception hierarchy | Provided | Both `turso` and `turso.sync` modules expose the full DB-API 2.0 interface including exception hierarchy (`Warning`, `Error`, `InterfaceError`, `DatabaseError`, `DataError`, `OperationalError`, `IntegrityError`, `InternalError`, `ProgrammingError`, `NotSupportedError`). ## Dialect Overrides Both dialects share these overrides via `_TursoDialectMixin` and direct method implementations: ### Class Attributes - `supports_statement_cache = True` - Enables SQLAlchemy statement caching for performance - `supports_native_datetime = False` - Turso handles datetime as strings, not native types ### Method Overrides - `import_dbapi()` - Returns `turso` or `turso.sync` module - `create_connect_args()` - Parses URL to connection arguments - `on_connect()` - Returns `None` (skips REGEXP function setup that pysqlite does, since turso doesn't support `create_function`) - `get_isolation_level()` - Returns `SERIALIZABLE` (turso doesn't support `PRAGMA read_uncommitted`) - `set_isolation_level()` - No-op (isolation set at connection time via `isolation_level` param) - `get_pool_class()` - Returns `SingletonThreadPool` for `:memory:`, `QueuePool` for file databases ### Reflection Overrides (via `_TursoDialectMixin`) Single-table methods (return empty list): - `get_foreign_keys()` - `PRAGMA foreign_key_list` not supported - `get_indexes()` - `PRAGMA index_list` not supported - `get_unique_constraints()` - Relies on `PRAGMA index_list` - `get_check_constraints()` - `sqlite_master` parsing not fully supported Multi-table methods (return empty dict): - `get_multi_indexes()` - `get_multi_unique_constraints()` - `get_multi_foreign_keys()` - `get_multi_check_constraints()` ## Limitations ### Table Reflection Turso doesn't support some SQLite PRAGMAs used for table reflection: - `PRAGMA foreign_key_list` - Foreign key introspection - `PRAGMA index_list` - Index introspection This means: - `inspector.get_foreign_keys()` returns empty list - `inspector.get_indexes()` returns empty list - `inspector.get_unique_constraints()` returns empty list - `inspector.get_check_constraints()` returns empty list - Foreign keys, indexes, and constraints still **work** at runtime, just can't be introspected - `inspector.get_table_names()` and `inspector.get_columns()` work normally This doesn't affect normal usage including: - Pandas `df.to_sql()` with `if_exists='replace'` - SQLAlchemy ORM operations - Alembic migrations (when using `--autogenerate`, manually verify FK/index changes) ### Native Datetime `supports_native_datetime` is set to `False`. Datetime columns should use `String` type and store ISO format strings. SQLAlchemy's `DateTime` type will still work but values are stored/retrieved as strings. ## Entry Points Dialects are registered via `pyproject.toml` entry points: ```toml [project.entry-points."sqlalchemy.dialects"] "sqlite.turso" = "turso.sqlalchemy:TursoDialect" "sqlite.turso_sync" = "turso.sqlalchemy:TursoSyncDialect" ``` ## Files - `turso/sqlalchemy/__init__.py` - Module exports (`TursoDialect`, `TursoSyncDialect`, `get_sync_connection`) - `turso/sqlalchemy/dialect.py` - Dialect implementations and `_TursoDialectMixin` - `tests/test_sqlalchemy.py` - Tests (28 tests across 8 test classes) ## References - [SQLAlchemy SQLite Dialect Docs](https://docs.sqlalchemy.org/en/20/dialects/sqlite.html) - [SQLAlchemy Dialect Creation Guide](https://github.com/sqlalchemy/sqlalchemy/blob/main/README.dialects.rst) - [pysqlite Dialect Source](https://github.com/sqlalchemy/sqlalchemy/blob/main/lib/sqlalchemy/dialects/sqlite/pysqlite.py) ================================================ FILE: bindings/python/build.rs ================================================ fn main() { pyo3_build_config::use_pyo3_cfgs(); } ================================================ FILE: bindings/python/py-bindings-db-aio.mdx ================================================ --- name: 2025-11-26-py-bindings-async --- Turso - is the SQLite compatible database written in Rust. One of the important features of the Turso - is async IO execution which can be used with modern storage backend like IO uring. Your task is to generate ASYNC Python driver with the API similar aiosqlite (which is similar to DB-API 2 of sqlite module) by REUSING current sync driver imiplementation. # Rules General rules for driver implementation you **MUST** follow and never go against these rules: - STRUCTURE of the implementation * Declaration order of elements and semantic blocks MUST be exsactly the same * (details and full enumerations omited in the example for brevity but you must generate full code) ```py # all imports must be at the beginning - no imports in the middle of function from typing import ... from queue import SimpleQueue from .worker import Worker from .lib import ( Connection as BlockingConnection, Cursor as BlockingCursor, ... ) # Connection goes FIRST class Connection: def __init__(connector: Callable[[], BlockingConnection]): ... # internal worker MUST be stopped when connection goes out of context scope # close must wait for worker to be stopped async def close(...): ... # make Connection instance awaitable def __await__(...): ... async def __aenter__(...): ... # just close the connection - do not add any extra logic async def __aexit__(...): ... await self.close() ... # Cursor goes SECOND class Cursor: ... # connect is not async because it returns awaitable Connection # same signature as in the lib.py def connect(...): ... ``` - USE already implemented driver - DO NOT copy it - USE primitives from the queue module in order to send work to the separate Thread - USE per-connection thread to execute database work - FOLLOW the pattern: ```py # create future for completion future = asyncio.get_event_loop().create_future() # put the work to the unbounded queue queue.put_nowait((future, function)) ``` - NEVER block main event loop (e.g., do not use Queue with block=True - prefer unbounded SimpleQueue instead) - REUSE data structures from lib.py if possible (e.g. exceptions) - DO NOT accept extra `loop` parameter - ALWAYS use `asyncio.get_event_loop()` - CREATE separate thread to execute Turso request and proxy all methods to these thread in order to not block event loop with database work - DO NOT reimplement logic of the execution - reuse methods and classes from lib.py - AVOID cryptic names - prefer short but concise names (wr is BAD, full_write is GOOD) - NEVER put import in the middle of a function - always put all necessary immports at the beginning of the file - FOCUS on code readability: if possible extract helper function but make sure that it will be used more than once and that it really contribute to the code readability - WATCH OUT for variables scopes and do not use variables which are no longer accessible - WATCH OUT for operations atomicity: DO NOT split atomic work in units which may be mixed with other work in async context - breaking atomicity guarantees - USE forward reference string in when return method type depends on its class: ```py class T: def f(self) -> 'T': ``` # Implementation - Put compact citations from the official DB-API doc if this is helpful - Driver must implement context API for Python to be used like `with ... as conn:` ... # Blocking driver For turso db execution you MUST reuse blocking implementation: # SQLite-like DB API Make driver API similar to the SQLite DB-API2 for the python. Pay additional attention to the following aspects: * SQLite DB-API2 implementation implicitly opens transaction for DML queries in the execute(...) method: > If autocommit is LEGACY_TRANSACTION_CONTROL, isolation_level is not None, sql is an INSERT, UPDATE, DELETE, or REPLACE statement, and there is no open transaction, a transaction is implicitly opened before executing sql. - MAKE SURE this logic implemented properly * Implement .rowcount property correctly, be careful with `executemany(...)` methods as it must return rowcount of all executed statements (not just last statement) * Convert exceptions from rust layer to appropriate exceptions documented in the sqlite3 db-api2 docs * BE CAREFUL with implementation of transaction control. Make sure that in LEGACY_TRANSACTION_CONTROL mode implicit transaction will be properly commited in case of cursor close Also, make types and API compatible with aiosqlite (note, that aiosqlite implements some other methods, which can be omitted now, like interrupt) ================================================ FILE: bindings/python/py-bindings-db.mdx ================================================ --- name: 2025-11-26-py-bindings --- Turso - is the SQLite compatible database written in Rust. One of the important features of the Turso - is async IO execution which can be used with modern storage backend like IO uring. Your task is to generate Python driver with the API similar to the SQLite DB-api2 # Rules General rules for driver implementation you **MUST** follow and never go against these rules: - STRUCTURE of the implementation * Declaration order of elements and semantic blocks MUST be exsactly the same * (details and full enumerations omited in the example for brevity but you must generate full code) ```py # all imports must be at the beginning - no imports in the middle of function from typing import ... from ._turso import ( ... ) # DB-API 2.0 module attributes apilevel = "2.0" ... # Exception hierarchy following DB-API 2.0 class Warning(Exception): ... # more ... def _map_turso_exception(exc: Exception) -> Exception: """Maps Turso-specific exceptions to DB-API 2.0 exception hierarchy""" if isinstance(exc, Busy): ... ... # Connection goes FIRST class Connection: ... # Cursor goes SECOND class Cursor: ... # Row goes THIRD class Row: ... def connect( path: str, *, experimental_features: Optional[str] = None, isolation_level: Optional[str] = "DEFERRED", extra_io: Optional[Callable[[], None]] = None # extra IO which must be called after stmt.run_io() invocations in the driver ): ... # Make it easy to enable logging with native `logging` Python module # (import logging only inside this function - make an exception here) def setup_logging(level: Optional[int] = None) -> None: ... ``` - AVOID unnecessary FFI calls as their cost is non zero - AVOID unnecessary strings transformations - replace them with more efficient alternatives if possible - DO NOT ever mix `PyTursoStatement::execute` and `PyTursoStatement::step` methods: every statement must be either "stepped" or "executed" * This is because `execute` ignores all rows - NEVER put import in the middle of a function - always put all necessary immports at the beginning of the file - SQL query can be arbitrary, be very careful writing the code which relies on properties derived from the simple string analysis * ONLY ANALYZE SQL statement to detect DML statement and open implicit transaction * DO NOT check for any symbols to detect multi statements, named parameters, etc - this is error prone. Use provided methods and avoid certain checks if they are impossible with current API provided from the Rust - FOCUS on code readability: if possible extract helper function but make sure that it will be used more than once and that it really contribute to the code readability - WATCH OUT for variables scopes and do not use variables which are no longer accessible - DO NOT TRACK transaction state manually and use `get_auto_commit` method - otherwise it can be hard to properly implement implicit transaction rules of DB API2 - USE forward reference string in when return method type depends on its class: ```py class T: def f(self) -> 'T': ``` # Implementation - Accept extra_io optional parameter in the driver which will run after stmt.run_io() whenever statement execution returned TURSO_IO status - Put compact citations from the official DB-API doc if this is helpful - Driver must implement context API for Python to be used like `with ... as conn:` ... - Driver implementation must be type-friendly - emit types everywhere at API boundary (public methods, class fields, etc) * DO NOT forget that constructor of Row must have following signature: ```py class Row(Sequence[Any]): def __new__(cls, cursor: Cursor, data: tuple[Any, ...], /) -> Self: ... ``` * Make typings compatible with official types: # Bindings You must use bindings in the lib.rs written with `pyo3` library which has certain conventions. Remember, that it can accept `py: Python` argument which will be passed implicitly and exported bindings will not have this extra arg # SQLite-like DB API Make driver API similar to the SQLite DB-API2 for the python. Pay additional attention to the following aspects: * SQLite DB-API2 implementation implicitly opens transaction for DML queries in the execute(...) method: > If autocommit is LEGACY_TRANSACTION_CONTROL, isolation_level is not None, sql is an INSERT, UPDATE, DELETE, or REPLACE statement, and there is no open transaction, a transaction is implicitly opened before executing sql. - MAKE SURE this logic implemented properly * Implement .rowcount property correctly, be careful with `executemany(...)` methods as it must return rowcount of all executed statements (not just last statement) * Convert exceptions from rust layer to appropriate exceptions documented in the sqlite3 db-api2 docs * BE CAREFUL with implementation of transaction control. Make sure that in LEGACY_TRANSACTION_CONTROL mode implicit transaction will be properly commited in case of cursor close ================================================ FILE: bindings/python/py-bindings-sync-aio.mdx ================================================ --- name: 2025-11-26-py-bindings-async-sync --- Turso - is the SQLite compatible database written in Rust. One of the important features of the Turso - is native ability to sync database with the Cloud in both directions (push local changes and pull remote changes). Your task is to generate ASYNC wrapper for synchronization EXTRA functionality built on top of the existing Python driver which will extend regular embedded with sync capability. Do not modify existing driver - its already implemented in the lib.py and lib_sync.py. Your task is to write extra code which will use abstractions and provide async API support in the Python on top of it in the lib_sync_aio.py file. # Rules General rules for driver implementation you **MUST** follow and never go against these rules: - USE already implemented drivers - DO NOT copy it - STRUCTURE of the implementation * Declaration order of elements and semantic blocks MUST be exsactly the same * (details and full enumerations omited in the example for brevity but you must generate full code) ```py # ALL imports MUST be at the beginning - no imports in the middle of function from typing import ... from queue import SimpleQueue from .worker import Worker from .lib_sync import ( ConnectionSync as BlockingConnectionSync ... ) from .lib_aio import ( Connection as NonBlockingConnection ... ) class ConnectionSync(NonBlockingConnection): def __init__(connector: Callable[[], BlockingConnectionSync]): ... # internal worker MUST be stopped when connection goes out of context scope def close(...): ... # make ConnectionSync instance awaitable # emit proper typings because delegation of __await__ to the base class will change the return type and make extra methods (pull/push/etc) unavailable def __await__(...): ... async def __aenter__(...): ... async def __aexit__(...): await self.close() # returns True of new updates were pulled; False if no new updates were fetched; determine changes by inspecting .empty() method of changes async def pull(self) -> bool: ... async def push(self) -> None: ... async def checkpoint(self) -> None: ... async def stats(self) -> None: ... # connect is not async because it returns awaitable ConnectionSync # same signature as in the lib_sync.py def connect_sync(...) -> ConnectionSync: ... ``` - FOLLOW the pattern: ```py # create future for completion future = asyncio.get_event_loop().create_future() # put the work to the unbounded queue queue.put_nowait((future, function)) ``` - AVOID cryptic names - prefer short but concise names (wr is BAD, full_write is GOOD) - NEVER EVER put import in the middle of a function - always put all necessary immports at the beginning of the file - FOCUS on code readability: extract helper functions if it will contribute to the code readability (do not overoptimize - it's fine to have some logic inlined especially if it is not repeated anywhere else) - WATCH OUT for variables scopes and do not use variables which are no longer accessible # Implementation - Annotate public API with types - Add comments about public API fields/functions to clarify meaning and usage scenarios # Blocking sync driver For turso sync execution you MUST reuse blocking implementation: # Non-blocking db driver You must integrate with non-blocking async db driver implementation: ================================================ FILE: bindings/python/py-bindings-sync.mdx ================================================ --- name: 2025-11-26-py-bindings --- Turso - is the SQLite compatible database written in Rust. One of the important features of the Turso - is native ability to sync database with the Cloud in both directions (push local changes and pull remote changes). Your task is to generate EXTRA functionality on top of the existing Python driver which will extend regular embedded with sync capability. Do not modify existing driver - its already implemented in the lib.py Your task is to write extra code which will use abstractions lib.py and build sync support in the Python on top of it in the lib_sync.py file. # Rules General rules for driver implementation you **MUST** follow and never go against these rules: - USE already implemented driver - DO NOT copy it - SET async_io=True for the driver database configuration - because partial sync support requires TURSO_IO to handled externally from the bindings - STRUCTURE of the implementation - Declaration order of elements and semantic blocks MUST be exsactly the same - (details and full enumerations omited in the example for brevity but you must generate full code) ```py # ALL imports MUST be at the beginning - no imports in the middle of function from typing import ... from dataclasses import dataclass # for HTTP IO import urllib.request import urllib.error from .lib import Connection as _Connection from ._turso import ( ... ) class ConnectionSync(_Connection): def __init__(...): ... def pull(self) -> bool: ... # returns True of new updates were pulled; False if no new updates were fetched; determine changes by inspecting .empty() method of changes def push(self) -> None: ... def checkpoint(self) -> None: ... def stats(self) -> None: ... @dataclass class PartialSyncPrefixBootstrap: # Bootstraps DB by fetching first N bytes/pages; enables partial sync length: int @dataclass class PartialSyncQueryBootstrap: # Bootstraps DB by fetching pages touched by given SQL query on server query: str @dataclass class PartialSyncOpts: bootstrap_strategy: Union[PartialSyncPrefixBootstrap, PartialSyncQueryBootstrap] segment_size: Optional[int] = None prefetch: Optional[bool] = None def connect_sync( path: str, # path to the main database file locally # remote url for the sync - can be lambda which will be evaluated on every http request; if lambda returns None - internal http processing must return error which will bubble-up in the sync engine # remote_url MUST be used in all sync engine operations: during bootstrap and all further operations # remote_url must accept either http://, https:// or libsql:// protocol, where later must be just replaced with https:// under the hood for now remote_url: Union[str, Callable[[], Optional[str]]], *, # token for remote authentication - can be lambda which will be evaluted on every http request; if lambda returns None - internal http processing must return error which will bubble-up in the sync engine # auth token value ("fixed" or got from lambda) WILL not have any prefix and must be used as "Authorization" header prepended with "Bearer " prefix auth_token: Optional[Union[str, Callable[[], Optional[str]]]], client_name: Optional[str], # optional unique client name (library MUST use `turso-sync-py` if omitted) long_poll_timeout_ms: Optional[number], # long polling timeout bootstrap_if_empty: bool = True, # if not set, initial bootstrap phase will be skipped and caller must call .pull(...) explicitly in order to get initial state from remote partial_sync_experimental: Optional[PartialSyncOpts] = None, # EXPERIMENTAL partial sync configuration experimental_features: Optional[str] = None, # pass it as-is to the underlying connection isolation_level: Optional[str] = "DEFERRED", # pass it as-is to the underlying connection ) -> ConnectionSync: ... ``` - STREAM data from the http request to the completion in chunks and spin async operation in between in order to prevent loading whole response in memory ```py # event loop for async operation "op" while True: chunk = e.read(CHUNK_SIZE) if not chunk: break io_item.push_buffer(chunk) op.resume() # assert that None is returned ``` - AVOID unnecessary FFI calls as their cost is non zero - AVOID unnecessary strings transformations - replace them with more efficient alternatives if possible - AVOID cryptic names - prefer short but concise names (wr is BAD, full_write is GOOD) - AVOID duplication of IO processing between `connect_sync` function and methods of `ConnectionSync` class - DO NOT use getattr unless this is necessary - use simple field access through dot - NEVER EVER put import in the middle of a function - always put all necessary immports at the beginning of the file - FOCUS on code readability: extract helper functions if it will contribute to the code readability (do not overoptimize - it's fine to have some logic inlined especially if it is not repeated anywhere else) - WATCH OUT for variables scopes and do not use variables which are no longer accessible # Implementation - Annotate public API with types - Add comments about public API fields/functions to clarify meaning and usage scenarios - Use `create()` method for creation of the synced database for now - DO NOT use init + open pair - Access #[pyo3(get)] fields through dot (e.g. stats.cdc_operations) # Bindings You must use bindings in the lib.rs written with `pyo3` library which has certain conventions. Remember, that it can accept `py: Python` argument which will be passed implicitly and exported bindings will not have this extra arg # Driver You must integrate with current driver implementation: ================================================ FILE: bindings/python/py-bindings-tests-aio.mdx ================================================ --- name: 2025-11-26-py-bindings-tests --- Turso - is the **SQLite compatible** database written in Rust. Your task is to generate tests for Python driver with the API similar to the SQLite DB-api2 # Rules General rules for driver implementation you **MUST** follow and never go against these rules: - Inspect tests in the test_database.py file and ALWAYS append new tests in the end - DO NOT change current content of the test_database.py file - ONLY APPEND new tests - DO NOT duplicate already existing tests - DO NOT test not implemented features - DO COVER all essential methods currently implemented in the driver - FOLLOW programming style of the test_database_aio.py file # Test case category 1: Driver Generate tests which will cover API of the driver surface - DRIVER: cover the async API - DRIVER: create tests for async execution so database operations doesn't block main event loop Inspect implementaton of the driver here: # Test case category 2: SQL Generate tests which will cover generic use of SQL. **Non exhaustive** list of things to check: - Subqueries - INSERT ... RETURNING ... * Make additional test case for scenario, where multiple values were inserted, but only one row were fetch * Make sure that in this case transaction will be properly commited even when not all rows were consumed - CONFLICT clauses (and how driver inform caller about conflict) - Basic DDL statements (CREATE/DELETE) - More complex DDL statements (ALTER TABLE) - Builtin virtual tables (generate_series) - JOIN - JSON functions # Supported functions - SQLITE: generate_series is not enabled by default in the sqlite3 module - TURSO: ORDER BY is not supported for compound SELECTs yet - TURSO: Recursive CTEs are not yet supported - TURSO: Inspect compatibility file in order to understand what subset of SQLite query language is supported by the turso at the moment ================================================ FILE: bindings/python/py-bindings-tests.mdx ================================================ --- name: 2025-11-26-py-bindings-tests --- Turso - is the **SQLite compatible** database written in Rust. Your task is to generate tests for Python driver with the API similar to the SQLite DB-api2 # Rules General rules for driver implementation you **MUST** follow and never go against these rules: - Inspect tests in the test_database.py file and ALWAYS append new tests in the end - DO NOT change current content of the test_database.py file - ONLY APPEND new tests - DO NOT duplicate already existing tests - DO NOT test not implemented features - DO COVER all essential methods currently implemented in the driver - FOLLOW programming style of the test_database.py file # Test case category 1: DB API2 Generate tests which will cover API of the driver surface Inspect implementaton of the driver here: # Test case category 2: SQL Generate tests which will cover generic use of SQL. **Non exhaustive** list of things to check: - Subqueries - INSERT ... RETURNING ... * Make additional test case for scenario, where multiple values were inserted, but only one row were fetch * Make sure that in this case transaction will be properly commited even when not all rows were consumed - CONFLICT clauses (and how driver inform caller about conflict) - Basic DDL statements (CREATE/DELETE) - More complex DDL statements (ALTER TABLE) - Builtin virtual tables (generate_series) - JOIN - JSON functions # Supported functions - DRIVER: .rowcount works correctly only for DML statements * DO NOT test it with DQL/DDL statements - DRIVER: .lastrowid is not implemented right now * DO NOT test it at all - SQLITE: generate_series is not enabled by default in the sqlite3 module - TURSO: ORDER BY is not supported for compound SELECTs yet - TURSO: Recursive CTEs are not yet supported - TURSO: Inspect compatibility file in order to understand what subset of SQLite query language is supported by the turso at the moment ================================================ FILE: bindings/python/pyproject.toml ================================================ [build-system] requires = ['maturin>=1,<2', 'typing_extensions'] build-backend = 'maturin' [project] name = 'pyturso' description = "Turso is a work-in-progress, in-process OLTP database management system, compatible with SQLite." requires-python = '>=3.9' classifiers = [ 'Development Status :: 3 - Alpha', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Rust', 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX :: Linux', 'Operating System :: Microsoft :: Windows', 'Operating System :: MacOS', 'Topic :: Database', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Database :: Database Engines/Servers', ] dependencies = ['typing-extensions >=4.6.0,!=4.7.0'] dynamic = ['readme', 'version'] [project.optional-dependencies] dev = [ "mypy==1.11.0", "pytest==8.3.1", "pytest-cov==5.0.0", "ruff==0.5.4", "coverage==7.6.1", "maturin==1.7.8", ] sqlalchemy = [ "sqlalchemy>=2.0", ] [project.urls] Homepage = "https://github.com/tursodatabase/turso" Source = "https://github.com/tursodatabase/turso" [project.entry-points."sqlalchemy.dialects"] "sqlite.turso" = "turso.sqlalchemy:TursoDialect" "sqlite.turso_sync" = "turso.sqlalchemy:TursoSyncDialect" [tool.maturin] bindings = 'pyo3' module-name = "turso._turso" features = ["pyo3/extension-module"] [tool.pip-tools] strip-extras = true header = false upgrade = false [tool.pytest.ini_options] testpaths = 'tests' log_format = '%(name)s %(levelname)s: %(message)s' asyncio_default_fixture_loop_scope = "function" [tool.coverage.run] source = ['turso'] branch = true [tool.coverage.report] precision = 2 exclude_lines = [ 'pragma: no cover', 'raise NotImplementedError', 'if TYPE_CHECKING:', '@overload', ] [dependency-groups] dev = [ "coverage>=7.6.1", "iniconfig>=2.1.0", "maturin>=1.7.8", "mypy>=1.11.0", "mypy-extensions>=1.1.0", "pluggy>=1.6.0", "pytest>=8.3.1", "pytest-asyncio>=1.3.0", "pytest-cov>=5.0.0", "requests>=2.32.5", "ruff>=0.5.4", "typing-extensions>=4.13.0", ] ================================================ FILE: bindings/python/src/lib.rs ================================================ use pyo3::{ pymodule, types::{PyModule, PyModuleMethods}, wrap_pyfunction, Bound, PyResult, }; use turso_sdk_kit::rsapi::TursoDatabase; use crate::{ turso::{ py_turso_database_open, py_turso_setup, Busy, Constraint, Corrupt, DatabaseFull, Interrupt, Misuse, NotAdb, PyTursoConnection, PyTursoDatabase, PyTursoDatabaseConfig, PyTursoEncryptionConfig, PyTursoExecutionResult, PyTursoLog, PyTursoSetupConfig, PyTursoStatement, PyTursoStatusCode, Readonly, }, turso_sync::{ py_turso_sync_new, PyRemoteEncryptionCipher, PyTursoAsyncOperation, PyTursoAsyncOperationResultKind, PyTursoPartialSyncOpts, PyTursoSyncDatabase, PyTursoSyncDatabaseChanges, PyTursoSyncDatabaseConfig, PyTursoSyncDatabaseStats, PyTursoSyncIoItem, PyTursoSyncIoItemRequestKind, }, }; pub mod turso; pub mod turso_sync; #[pymodule] fn _turso(m: &Bound) -> PyResult<()> { m.add("__version__", TursoDatabase::version())?; // database exports m.add_function(wrap_pyfunction!(py_turso_setup, m)?)?; m.add_function(wrap_pyfunction!(py_turso_database_open, m)?)?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add("Busy", m.py().get_type::())?; m.add("Interrupt", m.py().get_type::())?; m.add("Error", m.py().get_type::())?; m.add("Misuse", m.py().get_type::())?; m.add("Constraint", m.py().get_type::())?; m.add("Readonly", m.py().get_type::())?; m.add("DatabaseFull", m.py().get_type::())?; m.add("NotAdb", m.py().get_type::())?; m.add("Corrupt", m.py().get_type::())?; // sync exports m.add_function(wrap_pyfunction!(py_turso_sync_new, m)?)?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; Ok(()) } ================================================ FILE: bindings/python/src/turso.rs ================================================ use pyo3::{ prelude::*, types::{PyBytes, PyTuple}, }; use std::sync::Arc; use turso_sdk_kit::rsapi::{ self, EncryptionOpts, Numeric, TursoError, TursoStatusCode, Value, ValueRef, }; use pyo3::create_exception; use pyo3::exceptions::PyException; // support equality for status codes #[pyclass(eq, eq_int)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] // Add necessary traits for your use case pub enum PyTursoStatusCode { Ok = 0, Done = 1, Row = 2, Io = 3, } create_exception!(turso, Busy, PyException, "database is locked"); create_exception!( turso, BusySnapshot, PyException, "database snapshot is stale" ); create_exception!(turso, Interrupt, PyException, "interrupted"); create_exception!(turso, Error, PyException, "generic error"); create_exception!(turso, Misuse, PyException, "API misuse"); create_exception!(turso, Constraint, PyException, "constraint error"); create_exception!(turso, Readonly, PyException, "database is readonly"); create_exception!(turso, DatabaseFull, PyException, "database is full"); create_exception!(turso, NotAdb, PyException, "not a database`"); create_exception!(turso, Corrupt, PyException, "database corrupted"); create_exception!(turso, IoError, PyException, "I/O error"); pub(crate) fn turso_error_to_py_err(err: TursoError) -> PyErr { match err { rsapi::TursoError::Busy(message) => Busy::new_err(message), rsapi::TursoError::BusySnapshot(message) => BusySnapshot::new_err(message), rsapi::TursoError::Interrupt(message) => Interrupt::new_err(message), rsapi::TursoError::Error(message) => Error::new_err(message), rsapi::TursoError::Misuse(message) => Misuse::new_err(message), rsapi::TursoError::Constraint(message) => Constraint::new_err(message), rsapi::TursoError::Readonly(message) => Readonly::new_err(message), rsapi::TursoError::DatabaseFull(message) => DatabaseFull::new_err(message), rsapi::TursoError::NotAdb(message) => NotAdb::new_err(message), rsapi::TursoError::Corrupt(message) => Corrupt::new_err(message), rsapi::TursoError::IoError(kind, op) => IoError::new_err(format!("{op}: {kind:?}")), } } fn turso_status_to_py(status: TursoStatusCode) -> PyTursoStatusCode { match status { TursoStatusCode::Done => PyTursoStatusCode::Done, TursoStatusCode::Row => PyTursoStatusCode::Row, TursoStatusCode::Io => PyTursoStatusCode::Io, } } #[pyclass] pub struct PyTursoExecutionResult { #[pyo3(get)] pub status: PyTursoStatusCode, #[pyo3(get)] pub rows_changed: u64, } #[pyclass] pub struct PyTursoLog { #[pyo3(get)] pub message: String, #[pyo3(get)] pub target: String, #[pyo3(get)] pub file: String, #[pyo3(get)] pub timestamp: u64, #[pyo3(get)] pub line: usize, #[pyo3(get)] pub level: String, } #[pyclass] pub struct PyTursoSetupConfig { pub logger: Option>, pub log_level: Option, } #[pymethods] impl PyTursoSetupConfig { #[new] #[pyo3(signature = (logger, log_level))] fn new(logger: Option>, log_level: Option) -> Self { Self { logger, log_level } } } #[pyclass] #[derive(Clone)] pub struct PyTursoEncryptionConfig { pub cipher: String, pub hexkey: String, } #[pymethods] impl PyTursoEncryptionConfig { #[new] #[pyo3(signature = (cipher, hexkey))] fn new(cipher: String, hexkey: String) -> Self { Self { cipher, hexkey } } } #[pyclass] pub struct PyTursoDatabaseConfig { pub path: String, /// comma-separated list of experimental features to enable /// this field is intentionally just a string in order to make enablement of experimental features as flexible as possible pub experimental_features: Option, /// optional VFS parameter explicitly specifying FS backend for the database. /// Available options are: /// - "memory": in-memory backend /// - "syscall": generic syscall backend /// - "io_uring": IO uring (supported only on Linux) pub vfs: Option, /// optional encryption parameters /// as encryption is experimental - experimental_features must have "encryption" in the list pub encryption: Option, } #[pymethods] impl PyTursoDatabaseConfig { #[new] #[pyo3(signature = (path, experimental_features=None, vfs=None, encryption=None))] fn new( path: String, experimental_features: Option, vfs: Option, encryption: Option<&PyTursoEncryptionConfig>, ) -> Self { Self { path, experimental_features, vfs, encryption: encryption.cloned(), } } } #[pyclass] pub struct PyTursoDatabase { database: Arc, } /// Setup logging for the turso globally /// Only first invocation has effect - all subsequent updates will be ignored #[pyfunction] pub fn py_turso_setup(py: Python, config: &PyTursoSetupConfig) -> PyResult<()> { rsapi::turso_setup(rsapi::TursoSetupConfig { logger: if let Some(logger) = &config.logger { let logger = logger.clone_ref(py); Some(Box::new(move |log| { Python::attach(|py| { let py_log = PyTursoLog { message: log.message.to_string(), target: log.target.to_string(), file: log.file.to_string(), timestamp: log.timestamp, line: log.line, level: log.level.to_string(), }; logger.call1(py, (py_log,)).unwrap(); }) })) } else { None }, log_level: config.log_level.clone(), }) .map_err(turso_error_to_py_err)?; Ok(()) } /// Open the database #[pyfunction] pub fn py_turso_database_open(config: &PyTursoDatabaseConfig) -> PyResult { let database = rsapi::TursoDatabase::new(rsapi::TursoDatabaseConfig { path: config.path.clone(), experimental_features: config.experimental_features.clone(), async_io: false, encryption: config.encryption.as_ref().map(|encryption| EncryptionOpts { cipher: encryption.cipher.clone(), hexkey: encryption.hexkey.clone(), }), vfs: config.vfs.clone(), io: None, db_file: None, }); let result = database.open().map_err(turso_error_to_py_err)?; // async_io is false - so db.open() will return result immediately assert!(!result.is_io()); Ok(PyTursoDatabase { database }) } #[pymethods] impl PyTursoDatabase { pub fn connect(&self) -> PyResult { Ok(PyTursoConnection { connection: self.database.connect().map_err(turso_error_to_py_err)?, }) } } #[pyclass] pub struct PyTursoConnection { pub(crate) connection: Arc, } #[pymethods] impl PyTursoConnection { /// prepare single statement from the string pub fn prepare_single(&self, sql: &str) -> PyResult { Ok(PyTursoStatement { statement: self .connection .prepare_single(sql) .map_err(turso_error_to_py_err)?, }) } /// prepare first statement from the string which can have multiple statements separated by semicolon /// returns None if string has no statements /// returns Some with prepared statement and position in the string right after the prepared statement end pub fn prepare_first(&self, sql: &str) -> PyResult> { match self .connection .prepare_first(sql) .map_err(turso_error_to_py_err)? { Some((statement, tail_idx)) => Ok(Some((PyTursoStatement { statement }, tail_idx))), None => Ok(None), } } /// Get the auto_commmit mode for the connection pub fn get_auto_commit(&self) -> PyResult { Ok(self.connection.get_auto_commit()) } /// Close the connection /// (caller must ensure that no operations over connection or derived statements will happen after the call) pub fn close(&self) -> PyResult<()> { self.connection.close().map_err(turso_error_to_py_err) } } #[pyclass] pub struct PyTursoStatement { statement: Box, } #[pymethods] impl PyTursoStatement { /// binds positional parameters to the statement pub fn bind(&mut self, parameters: Bound) -> PyResult<()> { let len = parameters.len(); for i in 0..len { let parameter = parameters.get_item(i)?; self.statement .bind_positional(i + 1, py_to_db_value(parameter)?) .map_err(turso_error_to_py_err)?; } Ok(()) } /// step one iteration of the statement execution /// Returns [PyTursoStatusCode::Done] when execution is finished /// Returns [PyTursoStatusCode::Row] when execution generated a row which can be consumed with [Self::row] method /// Returns [PyTursoStatusCode::Io] when async_io is set and execution needs IO in order to make progress /// /// The caller must always either use [Self::step] or [Self::execute] methods for single statement - but never mix them together pub fn step(&mut self) -> PyResult { Ok(turso_status_to_py( self.statement.step(None).map_err(turso_error_to_py_err)?, )) } /// execute statement and ignore all rows generated by it /// Returns [PyTursoStatusCode::Done] when execution is finished /// Returns [PyTursoStatusCode::Io] when async_io is set and execution needs IO in order to make progress /// /// Note, that execute never returns Row status code /// /// The caller must always either use [Self::step] or [Self::execute] methods for single statement - but never mix them together pub fn execute(&mut self) -> PyResult { let result = self .statement .execute(None) .map_err(turso_error_to_py_err)?; Ok(PyTursoExecutionResult { status: turso_status_to_py(result.status), rows_changed: result.rows_changed, }) } /// Run one iteration of IO backend pub fn run_io(&self) -> PyResult<()> { self.statement.run_io().map_err(turso_error_to_py_err)?; Ok(()) } /// Get column names of the statement pub fn columns(&self, py: Python) -> PyResult> { let columns_count = self.statement.column_count(); let mut columns = Vec::with_capacity(columns_count); for i in 0..columns_count { columns.push( self.statement .column_name(i) .map_err(turso_error_to_py_err)? .to_string(), ); } Ok(PyTuple::new(py, columns.into_iter())?.unbind()) } /// Get tuple with current row values /// This method is only valid to call after [Self::step] returned [PyTursoStatusCode::Row] status code pub fn row(&self, py: Python) -> PyResult> { let columns_count = self.statement.column_count(); let mut py_values = Vec::with_capacity(columns_count); for i in 0..columns_count { py_values.push(db_value_to_py( py, self.statement.row_value(i).map_err(turso_error_to_py_err)?, )?); } Ok(PyTuple::new(py, &py_values)?.into_pyobject(py)?.into()) } /// Finalize statement execution /// This method must be called when statement is no longer need /// It will perform necessary cleanup and run any unfinished statement operations to completion /// (for example, in `INSERT INTO ... RETURNING ...` query, finalize is essential as it will make sure that all inserts will be completed, even if only few first rows were consumed by the caller) /// /// Note, that if statement wasn't started (no step / execute methods was called) - finalize will not execute the statement pub fn finalize(&mut self) -> PyResult { Ok(turso_status_to_py( self.statement .finalize(None) .map_err(turso_error_to_py_err)?, )) } /// Reset the statement by clearing bindings and reclaiming memory of the program from previous run /// This will also abort last operation if any was unfinished (but if transaction was opened before this statement - its state will be untouched, reset will only affect operation within current statement) pub fn reset(&mut self) -> PyResult<()> { self.statement.reset().map_err(turso_error_to_py_err)?; Ok(()) } } fn db_value_to_py(py: Python, value: rsapi::ValueRef) -> PyResult> { match value { ValueRef::Null => Ok(py.None()), ValueRef::Numeric(Numeric::Integer(i)) => Ok(i.into_pyobject(py)?.into()), ValueRef::Numeric(Numeric::Float(f)) => Ok(f64::from(f).into_pyobject(py)?.into()), ValueRef::Text(s) => Ok(s.as_str().into_pyobject(py)?.into()), ValueRef::Blob(b) => Ok(PyBytes::new(py, b).into()), } } /// Converts a Python object to a Turso Value fn py_to_db_value(obj: Bound) -> PyResult { if obj.is_none() { Ok(Value::Null) } else if let Ok(integer) = obj.extract::() { Ok(Value::from_i64(integer)) } else if let Ok(float) = obj.extract::() { Ok(Value::from_f64(float)) } else if let Ok(string) = obj.extract::() { Ok(Value::Text(string.into())) } else if let Ok(bytes) = obj.cast::() { Ok(Value::Blob(bytes.as_bytes().to_vec())) } else { Err(Error::new_err( "unexpected parameter value, only None, numbers, strings and bytes are supported" .to_string(), )) } } ================================================ FILE: bindings/python/src/turso_sync.rs ================================================ use std::sync::Arc; use pyo3::{ pyclass, pyfunction, pymethods, types::{PyBytes, PyList, PyTuple}, PyResult, Python, }; use turso_sdk_kit::rsapi::{TursoDatabaseConfig, TursoStatusCode}; use turso_sync_sdk_kit::{ rsapi::{ self, PartialBootstrapStrategy, PartialSyncOpts, TursoDatabaseSync, TursoDatabaseSyncChanges, }, sync_engine_io::SyncEngineIoQueueItem, turso_async_operation::{TursoAsyncOperationResult, TursoDatabaseAsyncOperation}, }; use crate::turso::{ turso_error_to_py_err, Error, Misuse, PyTursoConnection, PyTursoDatabaseConfig, }; #[pyclass] #[derive(Clone)] pub struct PyTursoPartialSyncOpts { // prefix bootstrap strategy which will enable partial sync which lazily pull necessary pages on demand and bootstrap db with pages from first N bytes of the db pub bootstrap_strategy_prefix: Option, // query bootstrap strategy which will enable partial sync which lazily pull necessary pages on demand and bootstrap db with pages touched by the server with given SQL query pub bootstrap_strategy_query: Option, pub segment_size: Option, pub prefetch: Option, } #[pymethods] impl PyTursoPartialSyncOpts { #[new] #[pyo3(signature = ( bootstrap_strategy_prefix=None, bootstrap_strategy_query=None, segment_size=None, prefetch=None, ))] fn new( bootstrap_strategy_prefix: Option, bootstrap_strategy_query: Option, segment_size: Option, prefetch: Option, ) -> Self { Self { bootstrap_strategy_prefix, bootstrap_strategy_query, segment_size, prefetch, } } } /// Encryption cipher for Turso Cloud remote encryption. /// These match the server-side encryption settings. #[pyclass(eq, eq_int)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PyRemoteEncryptionCipher { Aes256Gcm, Aes128Gcm, ChaCha20Poly1305, Aegis128L, Aegis128X2, Aegis128X4, Aegis256, Aegis256X2, Aegis256X4, } impl PyRemoteEncryptionCipher { /// Returns the total reserved bytes as required by the server pub fn reserved_bytes(&self) -> usize { match self { Self::Aes256Gcm | Self::Aes128Gcm | Self::ChaCha20Poly1305 => 28, Self::Aegis128L | Self::Aegis128X2 | Self::Aegis128X4 => 32, Self::Aegis256 | Self::Aegis256X2 | Self::Aegis256X4 => 48, } } } #[pyclass] pub struct PyTursoSyncDatabaseConfig { // path to the main database file (auxilary files like metadata, WAL, revert, changes will derive names from this path) pub path: String, // optional remote url (libsql://..., https://... or http://...) // this URL will be saved in the database metadata file in order to be able to reuse it if later client will be constructed without explicit remote url pub remote_url: Option, // arbitrary client name which will be used as a prefix for unique client id pub client_name: String, // long poll timeout for pull method (if set, server will hold connection for the given timeout until new changes will appear) pub long_poll_timeout_ms: Option, // bootstrap db if empty; if set - client will be able to connect to fresh db only when network is online pub bootstrap_if_empty: bool, // reserved bytes which must be set for the database - necessary if remote encryption is set for the db in cloud pub reserved_bytes: Option, pub partial_sync: Option, // base64-encoded encryption key for the encrypted Turso Cloud databases pub remote_encryption_key: Option, // encryption cipher for the remote database (used to calculate reserved_bytes) pub remote_encryption_cipher: Option, } #[pymethods] impl PyTursoSyncDatabaseConfig { #[new] #[pyo3(signature = ( path, client_name, remote_url=None, long_poll_timeout_ms=None, bootstrap_if_empty=true, reserved_bytes=None, partial_sync=None, remote_encryption_key=None, remote_encryption_cipher=None, ))] #[allow(clippy::too_many_arguments)] fn new( path: String, client_name: String, remote_url: Option, long_poll_timeout_ms: Option, bootstrap_if_empty: bool, reserved_bytes: Option, partial_sync: Option<&PyTursoPartialSyncOpts>, remote_encryption_key: Option, remote_encryption_cipher: Option, ) -> Self { Self { path, remote_url, client_name, long_poll_timeout_ms, bootstrap_if_empty, reserved_bytes, partial_sync: partial_sync.cloned(), remote_encryption_key, remote_encryption_cipher, } } } /// Creates database sync holder but do not open it #[pyfunction] pub fn py_turso_sync_new( db_config: &PyTursoDatabaseConfig, sync_config: &PyTursoSyncDatabaseConfig, ) -> PyResult { let db_config = TursoDatabaseConfig { path: db_config.path.clone(), experimental_features: db_config.experimental_features.clone(), async_io: true, // we will drive IO externally which is especially important for partial sync encryption: None, vfs: None, io: None, db_file: None, }; // calculate and set reserved_bytes from cipher if necessary let reserved_bytes = sync_config .remote_encryption_cipher .map(|c| c.reserved_bytes()) .or(sync_config.reserved_bytes); let sync_config = rsapi::TursoDatabaseSyncConfig { path: sync_config.path.clone(), remote_url: sync_config.remote_url.clone(), client_name: sync_config.client_name.clone(), bootstrap_if_empty: sync_config.bootstrap_if_empty, long_poll_timeout_ms: sync_config.long_poll_timeout_ms, reserved_bytes, partial_sync_opts: match &sync_config.partial_sync { Some(config) => { if let Some(length) = config.bootstrap_strategy_prefix { Some(PartialSyncOpts { bootstrap_strategy: Some(PartialBootstrapStrategy::Prefix { length }), segment_size: config.segment_size.unwrap_or(0), prefetch: config.prefetch.unwrap_or(false), }) } else { config .bootstrap_strategy_query .as_ref() .map(|query| PartialSyncOpts { bootstrap_strategy: Some(PartialBootstrapStrategy::Query { query: query.clone(), }), segment_size: config.segment_size.unwrap_or(0), prefetch: config.prefetch.unwrap_or(false), }) } } None => None, }, remote_encryption_key: sync_config.remote_encryption_key.clone(), }; let database = TursoDatabaseSync::>::new(db_config, sync_config).map_err(turso_error_to_py_err)?; Ok(PyTursoSyncDatabase { database }) } #[pyclass] pub struct PyTursoSyncDatabase { database: Arc>>, } #[pyclass] pub struct PyTursoAsyncOperation { operation: Box, } #[pyclass(eq, eq_int)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] // Add necessary traits for your use case pub enum PyTursoAsyncOperationResultKind { /// async operation has no return value ("void" operation) /// Note, that Python bindings have "No" as value because it's impossible to use "None" language keyword here No = 0, /// async operation returned [PyTursoConnection] instance Connection = 1, /// async operation returned [PyTursoSyncDatabaseStats] Stats = 2, /// async operation returned [PyTursoSyncDatabaseChanges] Changes = 3, } #[pyclass] pub struct PyTursoSyncDatabaseStats { #[pyo3(get)] pub cdc_operations: i64, #[pyo3(get)] pub main_wal_size: u64, #[pyo3(get)] pub revert_wal_size: i64, #[pyo3(get)] pub last_pull_unix_time: Option, #[pyo3(get)] pub last_push_unix_time: Option, #[pyo3(get)] pub revision: Option, #[pyo3(get)] pub network_sent_bytes: i64, #[pyo3(get)] pub network_received_bytes: i64, } #[pymethods] impl PyTursoSyncDatabaseStats { fn __repr__(&self) -> PyResult { Ok(format!( "PyTursoSyncDatabaseStats( cdc_operations={}, main_wal_size={}, revert_wal_size={}, last_pull_unix_time={}, last_push_unix_time={}, revision={}, network_sent_bytes={}, network_received_bytes={} )", self.cdc_operations, self.main_wal_size, self.revert_wal_size, self.last_pull_unix_time .map(|x| x.to_string()) .unwrap_or_else(|| "None".to_string()), self.last_push_unix_time .map(|x| x.to_string()) .unwrap_or_else(|| "None".to_string()), self.revision .as_ref() .map(|x| format!("\"{x}\"")) .unwrap_or_else(|| "None".to_string()), self.network_sent_bytes, self.network_received_bytes, )) } fn __str__(&self) -> PyResult { self.__repr__() } } /// changes container fetched from remote; must be passed to the [PyTursoSyncDatabase::apply_changes] method #[pyclass] pub struct PyTursoSyncDatabaseChanges { changes: Option>, } #[pymethods] impl PyTursoSyncDatabaseChanges { /// check if some changes were fetched from remote pub fn empty(&self) -> PyResult { let Some(changes) = &self.changes else { return Err(Misuse::new_err("changes were already applied".to_string())); }; Ok(changes.empty()) } } #[pyclass] pub struct PyTursoAsyncOperationResult { #[pyo3(get)] pub kind: PyTursoAsyncOperationResultKind, #[pyo3(get)] pub connection: Option>, #[pyo3(get)] pub changes: Option>, #[pyo3(get)] pub stats: Option>, } #[pymethods] impl PyTursoAsyncOperation { /// Resume async operation execution /// If returns Ok(false) - operation is not finished yet and must be resumed after one iteration of sync engine IO /// If returns Ok(true) - operation is finished and final result can be inspected with [Self::take_result] method /// It's safe to call resume multiple times even after operation completion (in case of repeat calls after completion - final result always will be returned) pub fn resume(&self) -> PyResult { let result = self.operation.resume().map_err(turso_error_to_py_err)?; if result == TursoStatusCode::Io { Ok(false) } else if result == TursoStatusCode::Done { Ok(true) } else { Err(Error::new_err("unexpected resume status".to_string())) } } /// Extract final result after operation completion /// This function can be called at most once as final result will be consumed after first call pub fn take_result(&self, py: Python) -> PyResult { let result = self.operation.take_result(); match result { Ok(TursoAsyncOperationResult::Changes { changes }) => Ok(PyTursoAsyncOperationResult { kind: PyTursoAsyncOperationResultKind::Changes, changes: Some(pyo3::Py::new( py, PyTursoSyncDatabaseChanges { changes: Some(changes), }, )?), connection: None, stats: None, }), Ok(TursoAsyncOperationResult::Connection { connection }) => { Ok(PyTursoAsyncOperationResult { kind: PyTursoAsyncOperationResultKind::Connection, changes: None, connection: Some(pyo3::Py::new(py, PyTursoConnection { connection })?), stats: None, }) } Ok(TursoAsyncOperationResult::Stats { stats }) => Ok(PyTursoAsyncOperationResult { kind: PyTursoAsyncOperationResultKind::Stats, changes: None, connection: None, stats: Some(pyo3::Py::new( py, PyTursoSyncDatabaseStats { cdc_operations: stats.cdc_operations, main_wal_size: stats.main_wal_size, revert_wal_size: stats.revert_wal_size as i64, last_pull_unix_time: stats.last_pull_unix_time, last_push_unix_time: stats.last_push_unix_time, revision: stats.revision, network_sent_bytes: stats.network_sent_bytes as i64, network_received_bytes: stats.network_received_bytes as i64, }, )?), }), // The only possible error is Misuse in case when operation doesn't have any result Err(..) => Ok(PyTursoAsyncOperationResult { kind: PyTursoAsyncOperationResultKind::No, changes: None, connection: None, stats: None, }), } } } #[pyclass(eq, eq_int)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] // Add necessary traits for your use case pub enum PyTursoSyncIoItemRequestKind { /// HTTP IO operation Http = 0, /// Atomic read IO operation; in case of not found error - sync engine expects empty response (not error) from the caller FullRead = 1, /// Atomic write IO operation FullWrite = 2, } #[pyclass] pub struct PyTursoSyncIoItemHttpRequest { /// optional HTTP url #[pyo3(get)] pub url: Option, /// HTTP method (e.g. POST / GET) #[pyo3(get)] pub method: String, /// HTTP path (url is controlled outside of the sync engine) #[pyo3(get)] pub path: String, /// optional body of the request #[pyo3(get)] pub body: Option>, /// Headers as list of tuples: list[(str, str)] #[pyo3(get)] pub headers: pyo3::Py, } #[pyclass] pub struct PyTursoSyncIoItemFullReadRequest { /// path of the file to read #[pyo3(get)] pub path: String, } #[pyclass] pub struct PyTursoSyncIoItemFullWriteRequest { /// path of the file to write #[pyo3(get)] pub path: String, /// content of the file to write #[pyo3(get)] pub content: pyo3::Py, } #[pyclass] pub struct PyTursoSyncIoItemRequest { #[pyo3(get)] pub kind: PyTursoSyncIoItemRequestKind, #[pyo3(get)] pub http: Option>, #[pyo3(get)] pub full_read: Option>, #[pyo3(get)] pub full_write: Option>, } #[pyclass] pub struct PyTursoSyncIoItem { item: Box>>, } #[pymethods] impl PyTursoSyncIoItem { /// Get IO request representation from the sync engine IO queue item pub fn request(&self, py: pyo3::Python) -> PyResult { match self.item.get_request() { turso_sync_sdk_kit::sync_engine_io::SyncEngineIoRequest::Http { url, method, path, body, headers, } => Ok(PyTursoSyncIoItemRequest { kind: PyTursoSyncIoItemRequestKind::Http, full_read: None, full_write: None, http: Some(pyo3::Py::new( py, PyTursoSyncIoItemHttpRequest { url: url.clone(), method: method.clone(), path: path.clone(), body: body .as_ref() .map(|body| PyBytes::new(py, body.as_ref()).unbind()), headers: { let mut tuples = Vec::new(); for (key, value) in headers { tuples .push(PyTuple::new(py, [key.clone(), value.clone()])?.unbind()); } PyList::new(py, tuples.into_iter())?.unbind() }, }, )?), }), turso_sync_sdk_kit::sync_engine_io::SyncEngineIoRequest::FullRead { path } => { Ok(PyTursoSyncIoItemRequest { kind: PyTursoSyncIoItemRequestKind::FullRead, full_read: Some(pyo3::Py::new( py, PyTursoSyncIoItemFullReadRequest { path: path.clone() }, )?), full_write: None, http: None, }) } turso_sync_sdk_kit::sync_engine_io::SyncEngineIoRequest::FullWrite { path, content, } => Ok(PyTursoSyncIoItemRequest { kind: PyTursoSyncIoItemRequestKind::FullWrite, full_read: None, full_write: Some(pyo3::Py::new( py, PyTursoSyncIoItemFullWriteRequest { path: path.clone(), content: PyBytes::new(py, content.as_ref()).unbind(), }, )?), http: None, }), } } /// set error as the final completion result of the IO queue item pub fn poison(&self, error: String) { self.item.get_completion().poison(error); } /// set IO completion as finished successfully ([Self::done] and [Self::poison] are mutually exclusive) pub fn done(&self) { self.item.get_completion().done(); } /// push bytes to the IO completion pub fn push_buffer(&self, buffer: &[u8]) { self.item.get_completion().push_buffer(buffer.to_vec()); } /// set HTTP status to the IO completion pub fn status(&self, status: u32) { self.item.get_completion().status(status); } } #[pymethods] impl PyTursoSyncDatabase { /// Open prepared synced database, fail if no properly setup database exists /// AsyncOperation returns No pub fn open(&self) -> PyTursoAsyncOperation { PyTursoAsyncOperation { operation: self.database.open(), } } /// Prepare synced database and open it /// AsyncOperation returns No pub fn create(&self) -> PyTursoAsyncOperation { PyTursoAsyncOperation { operation: self.database.create(), } } /// Create [PyTursoConnection] connection /// synced database must be opened before that operation (with either turso_database_sync_create or turso_database_sync_open) /// AsyncOperation returns Connection pub fn connect(&self) -> PyTursoAsyncOperation { PyTursoAsyncOperation { operation: self.database.connect(), } } /// Collect stats about synced database /// AsyncOperation returns Stats pub fn stats(&self) -> PyTursoAsyncOperation { PyTursoAsyncOperation { operation: self.database.stats(), } } /// Checkpoint WAL of the synced database /// AsyncOperation returns No pub fn checkpoint(&self) -> PyTursoAsyncOperation { PyTursoAsyncOperation { operation: self.database.checkpoint(), } } /// Push local changes to remote /// AsyncOperation returns No pub fn push_changes(&self) -> PyTursoAsyncOperation { PyTursoAsyncOperation { operation: self.database.push_changes(), } } /// Wait for remote changes /// AsyncOperation returns Changes pub fn wait_changes(&self) -> PyTursoAsyncOperation { PyTursoAsyncOperation { operation: self.database.wait_changes(), } } /// Apply remote changes locally /// AsyncOperation returns No pub fn apply_changes( &self, changes: &mut PyTursoSyncDatabaseChanges, ) -> PyResult { let Some(changes) = changes.changes.take() else { return Err(Misuse::new_err( "changes were already applied before".to_string(), )); }; Ok(PyTursoAsyncOperation { operation: self.database.apply_changes(changes), }) } /// Run extra database callbacks after IO execution pub fn step_io_callbacks(&self) { self.database.step_io_callbacks(); } /// Try to take IO request from the sync engine IO queue pub fn take_io_item(&self) -> Option { self.database .take_io_item() .map(|t| PyTursoSyncIoItem { item: t }) } } ================================================ FILE: bindings/python/tests/__init__.py ================================================ ================================================ FILE: bindings/python/tests/test_database.py ================================================ import logging import os import sqlite3 import pytest import turso logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", force=True) def connect(provider, database): if provider == "turso": return turso.connect(database) if provider == "sqlite3": return sqlite3.connect(database) raise Exception(f"Provider `{provider}` is not supported") @pytest.fixture(autouse=True) def setup_database(): db_path = "tests/database.db" db_wal_path = "tests/database.db-wal" # Ensure the database file is created fresh for each test try: if os.path.exists(db_path): os.remove(db_path) if os.path.exists(db_wal_path): os.remove(db_wal_path) except PermissionError as e: print(f"Failed to clean up: {e}") # Create a new database file conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, username TEXT)") cursor.execute(""" INSERT INTO users (id, username) SELECT 1, 'alice' WHERE NOT EXISTS (SELECT 1 FROM users WHERE id = 1) """) cursor.execute(""" INSERT INTO users (id, username) SELECT 2, 'bob' WHERE NOT EXISTS (SELECT 1 FROM users WHERE id = 2) """) conn.commit() conn.close() yield db_path # Cleanup after the test try: if os.path.exists(db_path): os.remove(db_path) if os.path.exists(db_wal_path): os.remove(db_wal_path) except PermissionError as e: print(f"Failed to clean up: {e}") @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_fetchall_select_all_users(provider, setup_database): conn = connect(provider, setup_database) cursor = conn.cursor() cursor.execute("SELECT * FROM users") users = cursor.fetchall() conn.close() assert users assert users == [(1, "alice"), (2, "bob")] @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_fetchall_select_user_ids(provider): conn = connect(provider, "tests/database.db") cursor = conn.cursor() cursor.execute("SELECT id FROM users") user_ids = cursor.fetchall() conn.close() assert user_ids assert user_ids == [(1,), (2,)] @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_in_memory_fetchone_select_all_users(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT)") cursor.execute("INSERT INTO users VALUES (1, 'alice')") cursor.execute("SELECT * FROM users") alice = cursor.fetchone() conn.close() assert alice assert alice == (1, "alice") @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_in_memory_index(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE users (name TEXT PRIMARY KEY, email TEXT)") cursor.execute("CREATE INDEX email_idx ON users(email)") cursor.execute("INSERT INTO users VALUES ('alice', 'a@b.c'), ('bob', 'b@d.e')") cursor.execute("SELECT * FROM users WHERE email = 'a@b.c'") alice = cursor.fetchall() cursor.execute("SELECT * FROM users WHERE email = 'b@d.e'") bob = cursor.fetchall() conn.close() assert alice == [("alice", "a@b.c")] assert bob == [("bob", "b@d.e")] @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_fetchone_select_all_users(provider): conn = connect(provider, "tests/database.db") cursor = conn.cursor() cursor.execute("SELECT * FROM users") alice = cursor.fetchone() assert alice assert alice == (1, "alice") bob = cursor.fetchone() conn.close() assert bob assert bob == (2, "bob") @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_fetchone_select_max_user_id(provider): conn = connect(provider, "tests/database.db") cursor = conn.cursor() cursor.execute("SELECT MAX(id) FROM users") max_id = cursor.fetchone() conn.close() assert max_id assert max_id == (2,) # Test case for: https://github.com/tursodatabase/turso/issues/494 @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_commit(provider): conn = connect(provider, "tests/database.db") cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS users_b ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, email TEXT NOT NULL, role TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT (datetime('now')) ) """) conn.commit() sample_users = [ ("alice", "alice@example.com", "admin"), ("bob", "bob@example.com", "user"), ("charlie", "charlie@example.com", "moderator"), ("diana", "diana@example.com", "user"), ] for username, email, role in sample_users: cur.execute("INSERT INTO users_b (username, email, role) VALUES (?, ?, ?)", (username, email, role)) conn.commit() # Now query the table res = cur.execute("SELECT * FROM users_b") record = res.fetchone() conn.close() assert record # Test case for: https://github.com/tursodatabase/turso/issues/2002 @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_first_rollback(provider, tmp_path): db_file = tmp_path / "test_first_rollback.db" conn = connect(provider, str(db_file)) cur = conn.cursor() cur.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT)") cur.execute("INSERT INTO users VALUES (1, 'alice')") cur.execute("INSERT INTO users VALUES (2, 'bob')") conn.rollback() cur.execute("SELECT * FROM users") users = cur.fetchall() assert users == [] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_with_statement(provider): with connect(provider, "tests/database.db") as conn: cursor = conn.cursor() cursor.execute("SELECT MAX(id) FROM users") max_id = cursor.fetchone() assert max_id assert max_id == (2,) # DB-API 2.0 tests @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_cursor_description(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT, value REAL)") cursor.execute("INSERT INTO test VALUES (1, 'test', 3.14)") cursor.execute("SELECT * FROM test") assert cursor.description is not None assert len(cursor.description) == 3 assert cursor.description[0][0] == "id" assert cursor.description[1][0] == "name" assert cursor.description[2][0] == "value" conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_cursor_rowcount_insert(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'alice'), (2, 'bob'), (3, 'charlie')") assert cursor.rowcount == 3 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_cursor_rowcount_update(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'alice'), (2, 'bob')") cursor.execute("UPDATE test SET name = 'updated' WHERE id = 1") assert cursor.rowcount == 1 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_cursor_rowcount_delete(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'alice'), (2, 'bob'), (3, 'charlie')") cursor.execute("DELETE FROM test WHERE id > 1") assert cursor.rowcount == 2 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_cursor_fetchmany(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("INSERT INTO test VALUES (1), (2), (3), (4), (5)") cursor.execute("SELECT * FROM test") cursor.arraysize = 2 rows = cursor.fetchmany() assert len(rows) == 2 assert rows == [(1,), (2,)] rows = cursor.fetchmany(3) assert len(rows) == 3 assert rows == [(3,), (4,), (5,)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_cursor_iterator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("INSERT INTO test VALUES (1), (2), (3)") cursor.execute("SELECT * FROM test") rows = list(cursor) assert rows == [(1,), (2,), (3,)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_cursor_close(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.close() with pytest.raises(Exception): cursor.execute("SELECT * FROM test") conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_connection_execute(provider): conn = connect(provider, ":memory:") conn.execute("CREATE TABLE test (id INTEGER, name TEXT)") cursor = conn.execute("INSERT INTO test VALUES (?, ?)", (1, "alice")) assert cursor.rowcount == 1 cursor = conn.execute("SELECT * FROM test") rows = cursor.fetchall() assert rows == [(1, "alice")] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_connection_executemany(provider): conn = connect(provider, ":memory:") conn.execute("CREATE TABLE test (id INTEGER, name TEXT)") data = [(1, "alice"), (2, "bob"), (3, "charlie")] cursor = conn.executemany("INSERT INTO test VALUES (?, ?)", data) assert cursor.rowcount == 3 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_connection_executescript(provider): conn = connect(provider, ":memory:") script = """ CREATE TABLE test (id INTEGER, name TEXT); INSERT INTO test VALUES (1, 'alice'); INSERT INTO test VALUES (2, 'bob'); """ conn.executescript(script) cursor = conn.execute("SELECT * FROM test") rows = cursor.fetchall() assert len(rows) == 2 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_row_factory(provider): conn = connect(provider, ":memory:") conn.row_factory = turso.Row if provider == "turso" else sqlite3.Row cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'alice')") cursor.execute("SELECT * FROM test") row = cursor.fetchone() assert row["id"] == 1 assert row["name"] == "alice" assert row[0] == 1 assert row[1] == "alice" conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_row_factory_keys(provider): conn = connect(provider, ":memory:") conn.row_factory = turso.Row if provider == "turso" else sqlite3.Row cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT, value REAL)") cursor.execute("INSERT INTO test VALUES (1, 'alice', 3.14)") cursor.execute("SELECT * FROM test") row = cursor.fetchone() keys = row.keys() assert keys == ["id", "name", "value"] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_parameterized_query(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT)") cursor.execute("INSERT INTO test VALUES (?, ?)", (1, "alice")) cursor.execute("INSERT INTO test VALUES (?, ?)", (2, "bob")) cursor.execute("SELECT * FROM test WHERE id = ?", (1,)) row = cursor.fetchone() assert row == (1, "alice") conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_executemany_with_parameters(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT)") data = [(1, "alice"), (2, "bob"), (3, "charlie")] cursor.executemany("INSERT INTO test VALUES (?, ?)", data) cursor.execute("SELECT COUNT(*) FROM test") count = cursor.fetchone()[0] assert count == 3 conn.close() # SQL tests @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_subquery(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value INTEGER)") cursor.execute("INSERT INTO test VALUES (1, 10), (2, 20), (3, 30)") cursor.execute("SELECT id FROM test WHERE value > (SELECT AVG(value) FROM test)") rows = cursor.fetchall() assert rows == [(3,)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_insert_returning(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)") cursor.execute("INSERT INTO test (id, name) VALUES (1, 'alice') RETURNING id, name") row = cursor.fetchone() assert row == (1, "alice") conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_insert_returning_partial_fetch(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)") cursor.execute("INSERT INTO test (id, name) VALUES (1, 'alice'), (2, 'bob'), (3, 'charlie') RETURNING id, name") row = cursor.fetchone() assert row == (1, "alice") cursor.close() cursor = conn.cursor() cursor.execute("SELECT COUNT(*) FROM test") count = cursor.fetchone()[0] assert count == 3 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_conflict_clause_ignore(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'alice')") cursor.execute("INSERT OR IGNORE INTO test VALUES (1, 'bob')") cursor.execute("SELECT * FROM test") rows = cursor.fetchall() assert rows == [(1, "alice")] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_conflict_clause_replace(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'alice')") cursor.execute("INSERT OR REPLACE INTO test VALUES (1, 'bob')") cursor.execute("SELECT * FROM test") rows = cursor.fetchall() assert rows == [(1, "bob")] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_conflict_clause_rollback(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'alice')") try: cursor.execute("INSERT OR ROLLBACK INTO test VALUES (1, 'bob')") except Exception: pass cursor.execute("SELECT * FROM test") rows = cursor.fetchall() assert len(rows) <= 1 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_drop_table(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("DROP TABLE test") try: cursor.execute("SELECT * FROM test") assert False, "Table should not exist" except Exception: pass conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_alter_table_add_column(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("INSERT INTO test VALUES (1)") cursor.execute("ALTER TABLE test ADD COLUMN name TEXT") cursor.execute("UPDATE test SET name = 'alice' WHERE id = 1") cursor.execute("SELECT * FROM test") row = cursor.fetchone() assert row == (1, "alice") conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_alter_table_rename(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("INSERT INTO test VALUES (1)") cursor.execute("ALTER TABLE test RENAME TO new_test") cursor.execute("SELECT * FROM new_test") row = cursor.fetchone() assert row == (1,) conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_inner_join(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE users (id INTEGER, name TEXT)") cursor.execute("CREATE TABLE orders (id INTEGER, user_id INTEGER, item TEXT)") cursor.execute("INSERT INTO users VALUES (1, 'alice'), (2, 'bob')") cursor.execute("INSERT INTO orders VALUES (1, 1, 'book'), (2, 1, 'pen'), (3, 2, 'notebook')") cursor.execute(""" SELECT users.name, orders.item FROM users INNER JOIN orders ON users.id = orders.user_id WHERE users.id = 1 """) rows = cursor.fetchall() assert rows == [("alice", "book"), ("alice", "pen")] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_left_join(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE users (id INTEGER, name TEXT)") cursor.execute("CREATE TABLE orders (id INTEGER, user_id INTEGER, item TEXT)") cursor.execute("INSERT INTO users VALUES (1, 'alice'), (2, 'bob'), (3, 'charlie')") cursor.execute("INSERT INTO orders VALUES (1, 1, 'book'), (2, 2, 'pen')") cursor.execute(""" SELECT users.name, orders.item FROM users LEFT JOIN orders ON users.id = orders.user_id """) rows = cursor.fetchall() assert len(rows) == 3 assert ("charlie", None) in rows conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_json_extract(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, data TEXT)") cursor.execute('INSERT INTO test VALUES (1, \'{"name": "alice", "age": 30}\')') cursor.execute("SELECT json_extract(data, '$.name') FROM test") row = cursor.fetchone() assert row[0] == "alice" conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_json_array(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("SELECT json_array(1, 2, 3)") row = cursor.fetchone() assert row[0] == "[1,2,3]" conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_json_object(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("SELECT json_object('name', 'alice', 'age', 30)") row = cursor.fetchone() assert "alice" in row[0] assert "30" in row[0] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_aggregate_functions(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (value INTEGER)") cursor.execute("INSERT INTO test VALUES (10), (20), (30), (40)") cursor.execute("SELECT AVG(value), SUM(value), MIN(value), MAX(value), COUNT(*) FROM test") row = cursor.fetchone() assert row == (25.0, 100, 10, 40, 4) conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_group_by(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (category TEXT, value INTEGER)") cursor.execute("INSERT INTO test VALUES ('A', 10), ('A', 20), ('B', 30), ('B', 40)") cursor.execute("SELECT category, SUM(value) FROM test GROUP BY category") rows = cursor.fetchall() assert rows == [("A", 30), ("B", 70)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_having_clause(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (category TEXT, value INTEGER)") cursor.execute("INSERT INTO test VALUES ('A', 10), ('A', 20), ('B', 5), ('B', 10)") cursor.execute("SELECT category, SUM(value) as total FROM test GROUP BY category HAVING total > 20") rows = cursor.fetchall() assert rows == [("A", 30)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_create_view(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value INTEGER)") cursor.execute("INSERT INTO test VALUES (1, 10), (2, 20), (3, 30)") cursor.execute("CREATE VIEW test_view AS SELECT id, value * 2 as doubled FROM test") cursor.execute("SELECT * FROM test_view WHERE id = 2") row = cursor.fetchone() assert row == (2, 40) conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_drop_view(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("CREATE VIEW test_view AS SELECT * FROM test") cursor.execute("DROP VIEW test_view") try: cursor.execute("SELECT * FROM test_view") assert False, "View should not exist" except Exception: pass conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_with_cte(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value INTEGER)") cursor.execute("INSERT INTO test VALUES (1, 10), (2, 20), (3, 30)") cursor.execute(""" WITH doubled AS (SELECT id, value * 2 as doubled_value FROM test) SELECT * FROM doubled WHERE doubled_value > 30 """) rows = cursor.fetchall() assert rows == [(2, 40), (3, 60)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_case_expression(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value INTEGER)") cursor.execute("INSERT INTO test VALUES (1, 10), (2, 20), (3, 30)") cursor.execute(""" SELECT id, CASE WHEN value < 15 THEN 'low' WHEN value < 25 THEN 'medium' ELSE 'high' END as category FROM test """) rows = cursor.fetchall() assert rows == [(1, "low"), (2, "medium"), (3, "high")] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_between_operator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value INTEGER)") cursor.execute("INSERT INTO test VALUES (1, 10), (2, 20), (3, 30), (4, 40)") cursor.execute("SELECT * FROM test WHERE value BETWEEN 15 AND 35") rows = cursor.fetchall() assert rows == [(2, 20), (3, 30)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_in_operator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'alice'), (2, 'bob'), (3, 'charlie')") cursor.execute("SELECT * FROM test WHERE name IN ('alice', 'charlie')") rows = cursor.fetchall() assert rows == [(1, "alice"), (3, "charlie")] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_like_operator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'alice'), (2, 'bob'), (3, 'alicia')") cursor.execute("SELECT * FROM test WHERE name LIKE 'ali%'") rows = cursor.fetchall() assert len(rows) == 2 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_glob_operator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'alice'), (2, 'bob'), (3, 'alicia')") cursor.execute("SELECT * FROM test WHERE name GLOB 'ali*'") rows = cursor.fetchall() assert len(rows) == 2 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_exists_operator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE users (id INTEGER, name TEXT)") cursor.execute("CREATE TABLE orders (id INTEGER, user_id INTEGER)") cursor.execute("INSERT INTO users VALUES (1, 'alice'), (2, 'bob')") cursor.execute("INSERT INTO orders VALUES (1, 1)") cursor.execute(""" SELECT name FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id) """) rows = cursor.fetchall() assert rows == [("alice",)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_transaction_begin_commit(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value TEXT)") cursor.execute("BEGIN") cursor.execute("INSERT INTO test VALUES (1, 'test')") cursor.execute("COMMIT") cursor.execute("SELECT * FROM test") rows = cursor.fetchall() assert rows == [(1, "test")] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_transaction_begin_rollback(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value TEXT)") cursor.execute("BEGIN") cursor.execute("INSERT INTO test VALUES (2, 'rollback')") cursor.execute("ROLLBACK") cursor.execute("SELECT * FROM test") rows = cursor.fetchall() assert rows == [] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_multiple_cursors_same_connection(provider): conn = connect(provider, ":memory:") cursor1 = conn.cursor() cursor2 = conn.cursor() cursor1.execute("CREATE TABLE test (id INTEGER)") cursor1.execute("INSERT INTO test VALUES (1), (2)") cursor2.execute("SELECT * FROM test") rows = cursor2.fetchall() assert len(rows) == 2 cursor1.close() cursor2.close() conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_cursor_description_before_execute(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() assert cursor.description is None cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("SELECT * FROM test") assert cursor.description is not None conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_cursor_arraysize_default(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() assert cursor.arraysize == 1 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_empty_fetchall(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("SELECT * FROM test") rows = cursor.fetchall() assert rows == [] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_empty_fetchone(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("SELECT * FROM test") row = cursor.fetchone() assert row is None conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_empty_fetchmany(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("SELECT * FROM test") rows = cursor.fetchmany(5) assert rows == [] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_unicode_data(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, text TEXT)") cursor.execute("INSERT INTO test VALUES (?, ?)", (1, "Hello 世界 🌍")) cursor.execute("SELECT text FROM test") row = cursor.fetchone() assert row[0] == "Hello 世界 🌍" conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_null_values(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value TEXT)") cursor.execute("INSERT INTO test VALUES (1, NULL)") cursor.execute("SELECT * FROM test") row = cursor.fetchone() assert row == (1, None) conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_blob_data(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, data BLOB)") blob_data = b"\x00\x01\x02\x03\x04" cursor.execute("INSERT INTO test VALUES (?, ?)", (1, blob_data)) cursor.execute("SELECT data FROM test") row = cursor.fetchone() assert row[0] == blob_data conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_limit_offset(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("INSERT INTO test VALUES (1), (2), (3), (4), (5)") cursor.execute("SELECT * FROM test LIMIT 2 OFFSET 2") rows = cursor.fetchall() assert rows == [(3,), (4,)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_order_by_desc(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value INTEGER)") cursor.execute("INSERT INTO test VALUES (1, 30), (2, 10), (3, 20)") cursor.execute("SELECT * FROM test ORDER BY value DESC") rows = cursor.fetchall() assert rows == [(1, 30), (3, 20), (2, 10)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_distinct(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (value TEXT)") cursor.execute("INSERT INTO test VALUES ('a'), ('b'), ('a'), ('c'), ('b')") cursor.execute("SELECT DISTINCT value FROM test ORDER BY value") rows = cursor.fetchall() assert rows == [("a",), ("b",), ("c",)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_coalesce_function(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, a TEXT, b TEXT)") cursor.execute("INSERT INTO test VALUES (1, NULL, 'fallback'), (2, 'value', 'fallback')") cursor.execute("SELECT COALESCE(a, b) FROM test ORDER BY id") rows = cursor.fetchall() assert rows == [("fallback",), ("value",)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_union_operator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE t1 (value INTEGER)") cursor.execute("CREATE TABLE t2 (value INTEGER)") cursor.execute("INSERT INTO t1 VALUES (1), (2)") cursor.execute("INSERT INTO t2 VALUES (2), (3)") cursor.execute("SELECT value FROM t1 UNION SELECT value FROM t2") rows = cursor.fetchall() assert len(rows) == 3 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_union_all_operator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE t1 (value INTEGER)") cursor.execute("CREATE TABLE t2 (value INTEGER)") cursor.execute("INSERT INTO t1 VALUES (1), (2)") cursor.execute("INSERT INTO t2 VALUES (2), (3)") cursor.execute("SELECT value FROM t1 UNION ALL SELECT value FROM t2") rows = cursor.fetchall() assert len(rows) == 4 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_is_null_operator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'a'), (2, NULL), (3, 'c')") cursor.execute("SELECT id FROM test WHERE value IS NULL") rows = cursor.fetchall() assert rows == [(2,)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_is_not_null_operator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'a'), (2, NULL), (3, 'c')") cursor.execute("SELECT id FROM test WHERE value IS NOT NULL ORDER BY id") rows = cursor.fetchall() assert rows == [(1,), (3,)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_not_in_operator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, name TEXT)") cursor.execute("INSERT INTO test VALUES (1, 'alice'), (2, 'bob'), (3, 'charlie')") cursor.execute("SELECT * FROM test WHERE name NOT IN ('alice', 'charlie') ORDER BY id") rows = cursor.fetchall() assert rows == [(2, "bob")] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_not_exists_operator(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE users (id INTEGER, name TEXT)") cursor.execute("CREATE TABLE orders (id INTEGER, user_id INTEGER)") cursor.execute("INSERT INTO users VALUES (1, 'alice'), (2, 'bob')") cursor.execute("INSERT INTO orders VALUES (1, 1)") cursor.execute(""" SELECT name FROM users WHERE NOT EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id) """) rows = cursor.fetchall() assert rows == [("bob",)] conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_substr_function(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("SELECT substr('Hello World', 1, 5)") row = cursor.fetchone() assert row[0] == "Hello" conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_length_function(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("SELECT length('Hello')") row = cursor.fetchone() assert row[0] == 5 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_upper_lower_functions(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("SELECT upper('hello'), lower('WORLD')") row = cursor.fetchone() assert row == ("HELLO", "world") conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_trim_functions(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("SELECT trim(' hello '), ltrim(' hello'), rtrim('hello ')") row = cursor.fetchone() assert row == ("hello", "hello", "hello") conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_replace_function(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("SELECT replace('Hello World', 'World', 'Python')") row = cursor.fetchone() assert row[0] == "Hello Python" conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_abs_function(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("SELECT abs(-42), abs(42)") row = cursor.fetchone() assert row == (42, 42) conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_typeof_function(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("SELECT typeof(123), typeof('text'), typeof(NULL), typeof(3.14)") row = cursor.fetchone() assert row == ("integer", "text", "null", "real") conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_create_table_if_not_exists(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS test (id INTEGER)") cursor.execute("CREATE TABLE IF NOT EXISTS test (id INTEGER)") cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test'") row = cursor.fetchone() assert row is not None conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_drop_table_if_exists(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER)") cursor.execute("DROP TABLE IF EXISTS test") cursor.execute("DROP TABLE IF EXISTS test") cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test'") row = cursor.fetchone() assert row is None conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_multiple_ctes(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (value INTEGER)") cursor.execute("INSERT INTO test VALUES (10), (20), (30)") cursor.execute(""" WITH doubled AS (SELECT value * 2 as v FROM test), tripled AS (SELECT value * 3 as v FROM test) SELECT doubled.v, tripled.v FROM doubled, tripled WHERE doubled.v = 20 AND tripled.v = 30 """) row = cursor.fetchone() assert row == (20, 30) conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_nested_subqueries(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, value INTEGER)") cursor.execute("INSERT INTO test VALUES (1, 10), (2, 20), (3, 30), (4, 40)") cursor.execute(""" SELECT id FROM test WHERE value > ( SELECT AVG(value) FROM test WHERE value > (SELECT MIN(value) FROM test) ) """) rows = cursor.fetchall() assert len(rows) > 0 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_correlated_subquery(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("CREATE TABLE test (id INTEGER, category TEXT, value INTEGER)") cursor.execute("INSERT INTO test VALUES (1, 'A', 10), (2, 'A', 20), (3, 'B', 15), (4, 'B', 25)") cursor.execute(""" SELECT t1.id, t1.value FROM test t1 WHERE t1.value > (SELECT AVG(t2.value) FROM test t2 WHERE t2.category = t1.category) """) rows = cursor.fetchall() assert len(rows) == 2 conn.close() # Additional tests appended @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_executemany_requires_dml(provider): conn = connect(provider, ":memory:") cur = conn.cursor() with pytest.raises(Exception): cur.executemany("SELECT 1", [()]) conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_execute_multiple_statements_prohibited(provider): conn = connect(provider, ":memory:") cur = conn.cursor() with pytest.raises(Exception): cur.execute("SELECT 1; SELECT 2") conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_description_none_after_insert(provider): conn = connect(provider, ":memory:") cur = conn.cursor() cur.execute("CREATE TABLE test (id INTEGER)") cur.execute("INSERT INTO test VALUES (1), (2)") assert cur.description is None conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_rowcount_select_is_minus_one(provider): conn = connect(provider, ":memory:") cur = conn.cursor() cur.execute("CREATE TABLE test (id INTEGER)") cur.execute("INSERT INTO test VALUES (1), (2)") cur.execute("SELECT * FROM test") assert cur.rowcount == -1 conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_cursor_setinput_output_size_noop(provider): conn = connect(provider, ":memory:") cur = conn.cursor() # Should not raise cur.setinputsizes([None]) cur.setoutputsize(1024, 0) conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_custom_row_factory_callable(provider): conn = connect(provider, ":memory:") # row factory that returns a dict for each row def dict_factory(cursor, row): return {cursor.description[i][0]: row[i] for i in range(len(row))} conn.row_factory = dict_factory cur = conn.cursor() cur.execute("CREATE TABLE test (id INTEGER, name TEXT)") cur.execute("INSERT INTO test VALUES (1, 'alice')") cur.execute("SELECT * FROM test") row = cur.fetchone() assert isinstance(row, dict) assert row["id"] == 1 assert row["name"] == "alice" conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_in_transaction_toggle_with_commit(provider): conn = connect(provider, ":memory:") cur = conn.cursor() cur.execute("CREATE TABLE test (id INTEGER)") # Before DML, should not be in a transaction assert not conn.in_transaction # DML should start a transaction in legacy mode cur.execute("INSERT INTO test VALUES (1)") assert conn.in_transaction conn.commit() assert not conn.in_transaction conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_isolation_level_none_autocommit(provider, tmp_path): db_file = tmp_path / "auto_commit.db" if provider == "turso": conn = turso.connect(str(db_file), isolation_level=None) else: conn = sqlite3.connect(str(db_file)) conn.isolation_level = None cur = conn.cursor() cur.execute("CREATE TABLE test (id INTEGER)") cur.execute("INSERT INTO test VALUES (1)") # No explicit commit; in autocommit mode data should persist conn.close() conn2 = connect(provider, str(db_file)) cur2 = conn2.cursor() cur2.execute("SELECT COUNT(*) FROM test") count = cur2.fetchone()[0] assert count == 1 conn2.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_generate_series_virtual_table(provider): conn = connect(provider, ":memory:") cur = conn.cursor() if provider == "turso": cur.execute("SELECT value FROM generate_series(1, 3)") rows = cur.fetchall() assert rows == [(1,), (2,), (3,)] else: with pytest.raises(Exception): cur.execute("SELECT value FROM generate_series(1, 3)") conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_connection_exception_attributes_present(provider): conn = connect(provider, ":memory:") # Ensure DB-API exception classes are exposed on the connection assert issubclass(conn.Error, Exception) assert issubclass(conn.DatabaseError, Exception) assert issubclass(conn.ProgrammingError, Exception) conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_insert_returning_single_and_multiple_commit_without_consuming(provider): # turso.setup_logging(level=logging.DEBUG) conn = connect(provider, ":memory:") try: cur = conn.cursor() cur.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)") cur.execute( "INSERT INTO t(name) VALUES (?), (?) RETURNING id", ("bob", "alice"), ) cur.fetchone() with pytest.raises(Exception): conn.commit() finally: conn.close() @pytest.mark.parametrize("provider", ["sqlite3", "turso"]) def test_pragma_integrity_check(provider): conn = connect(provider, ":memory:") cursor = conn.cursor() cursor.execute("PRAGMA integrity_check") # Verify fetchone returns the expected result, not None # Bug: missing add_pragma_result_column in translate_integrity_check # caused column_count to be 0, making execute() finalize the statement # and leaving fetchone() to return None row = cursor.fetchone() assert row is not None, "PRAGMA integrity_check should return a row" assert row == ("ok",) conn.close() def test_encryption_enabled(tmp_path): tmp_path = tmp_path / "local.db" conn = turso.connect( str(tmp_path), experimental_features="encryption", encryption=turso.EncryptionOpts( cipher="aegis256", hexkey="b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327" ), ) cursor = conn.cursor() cursor.execute("create table t(x)") cursor.execute("insert into t select 'secret' from generate_series(1, 1024)") conn.commit() cursor.execute("pragma wal_checkpoint(truncate)").fetchall() conn.commit() content = open(tmp_path, "rb").read() assert len(content) > 16 * 1024 assert b"secret" not in content def test_encryption_disabled(tmp_path): tmp_path = tmp_path / "local.db" conn = turso.connect( str(tmp_path), ) cursor = conn.cursor() cursor.execute("create table t(x)") cursor.execute("insert into t select 'secret' from generate_series(1, 1024)") conn.commit() cursor.execute("pragma wal_checkpoint(truncate)").fetchall() conn.commit() content = open(tmp_path, "rb").read() assert len(content) > 16 * 1024 assert b"secret" in content def test_encryption(tmp_path): tmp_path = tmp_path / "local.db" hexkey = "b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327" wrong_key = "aaaaaaa4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327" conn = turso.connect( str(tmp_path), experimental_features="encryption", encryption=turso.EncryptionOpts(cipher="aegis256", hexkey=hexkey), ) cursor = conn.cursor() cursor.execute("create table t(x)") cursor.execute("insert into t select 'secret' from generate_series(1, 1024)") conn.commit() cursor.execute("pragma wal_checkpoint(truncate)").fetchall() conn.commit() conn.close() # verify we can re-open with the same key conn2 = turso.connect( str(tmp_path), experimental_features="encryption", encryption=turso.EncryptionOpts(cipher="aegis256", hexkey=hexkey), ) cursor2 = conn2.cursor() cursor2.execute("select count(*) from t") assert cursor2.fetchone()[0] == 1024 conn2.close() # verify opening with wrong key fails with pytest.raises(Exception): conn3 = turso.connect( str(tmp_path), experimental_features="encryption", encryption=turso.EncryptionOpts(cipher="aegis256", hexkey=wrong_key), ) cursor3 = conn3.cursor() cursor3.execute("select * from t") cursor3.fetchone() # trigger actual data read to cause decryption error # verify opening without encryption fails with pytest.raises(Exception): conn5 = turso.connect(str(tmp_path)) cursor5 = conn5.cursor() cursor5.execute("select * from t") cursor5.fetchone() # trigger actual data read to cause decryption error ================================================ FILE: bindings/python/tests/test_database_aio.py ================================================ import asyncio import logging import os import sqlite3 import pytest import turso.aio logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", force=True) @pytest.fixture(autouse=True) def setup_database(): db_path = "tests/database.db" db_wal_path = "tests/database.db-wal" # Ensure the database file is created fresh for each test try: if os.path.exists(db_path): os.remove(db_path) if os.path.exists(db_wal_path): os.remove(db_wal_path) except PermissionError as e: print(f"Failed to clean up: {e}") # Create a new database file conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, username TEXT)") cursor.execute(""" INSERT INTO users (id, username) SELECT 1, 'alice' WHERE NOT EXISTS (SELECT 1 FROM users WHERE id = 1) """) cursor.execute(""" INSERT INTO users (id, username) SELECT 2, 'bob' WHERE NOT EXISTS (SELECT 1 FROM users WHERE id = 2) """) conn.commit() conn.close() yield db_path # Cleanup after the test try: if os.path.exists(db_path): os.remove(db_path) if os.path.exists(db_wal_path): os.remove(db_wal_path) except PermissionError as e: print(f"Failed to clean up: {e}") @pytest.mark.asyncio async def test_connection_execute_helpers_and_context_manager(): async with turso.aio.connect(":memory:") as conn: await conn.executescript(""" CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); INSERT INTO t(name) VALUES ('alice'); """) cur = await conn.execute("SELECT COUNT(*) FROM t") count = (await cur.fetchone())[0] assert count == 1 @pytest.mark.asyncio async def test_subqueries_and_join(): conn = await turso.aio.connect(":memory:") try: cur = conn.cursor() await cur.executescript(""" CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT); CREATE TABLE profiles (user_id INTEGER, city TEXT); INSERT INTO users (id, username) VALUES (1, 'alice'), (2, 'bob'), (3, 'adam'); INSERT INTO profiles (user_id, city) VALUES (1, 'NY'), (2, 'SF'), (3, 'LA'); """) # Subquery in WHERE await cur.execute(""" SELECT username FROM users WHERE id IN (SELECT id FROM users WHERE username LIKE 'a%') ORDER BY username """) rows = await cur.fetchall() assert [r[0] for r in rows] == ["adam", "alice"] # JOIN with subquery await cur.execute( """ SELECT u.username, p.city FROM users u JOIN (SELECT user_id, city FROM profiles) p ON u.id = p.user_id WHERE u.username = ? """, ("alice",), ) row = await cur.fetchone() assert row == ("alice", "NY") finally: await conn.close() @pytest.mark.asyncio async def test_conflict_do_nothing_and_rowcount(): conn = await turso.aio.connect(":memory:") try: cur = conn.cursor() await cur.execute("CREATE TABLE t (name TEXT PRIMARY KEY, val INT)") await cur.execute("INSERT INTO t(name, val) VALUES (?, ?)", ("x", 1)) await conn.commit() # Conflict should not raise and rowcount should reflect 0 affected rows await cur.execute("INSERT INTO t(name, val) VALUES (?, ?) ON CONFLICT(name) DO NOTHING", ("x", 2)) assert cur.rowcount == 0 await cur.execute("SELECT val FROM t WHERE name = ?", ("x",)) val = (await cur.fetchone())[0] assert val == 1 finally: await conn.close() @pytest.mark.asyncio async def test_ddl_alter_table_add_column_with_default(): conn = await turso.aio.connect(":memory:") try: cur = conn.cursor() await cur.executescript(""" CREATE TABLE items (id INT PRIMARY KEY, name TEXT); ALTER TABLE items ADD COLUMN price INT DEFAULT 0; INSERT INTO items (id, name) VALUES (1, 'a'), (2, 'b'); """) await cur.execute("SELECT price FROM items WHERE id = 1") assert (await cur.fetchone())[0] == 0 # Update and verify await cur.execute("UPDATE items SET price = ? WHERE id = ?", (10, 2)) await conn.commit() await cur.execute("SELECT price FROM items WHERE id = 2") assert (await cur.fetchone())[0] == 10 finally: await conn.close() @pytest.mark.asyncio async def test_generate_series_virtual_table_and_join(): conn = await turso.aio.connect(":memory:") try: cur = conn.cursor() # Simple usage await cur.execute("SELECT sum(value) FROM generate_series(1, 100)") total = (await cur.fetchone())[0] assert total == 5050 # Join with a real table await cur.executescript(""" CREATE TABLE nums (n INT PRIMARY KEY); INSERT INTO nums (n) VALUES (1), (3), (5); """) await cur.execute(""" SELECT gs.value FROM generate_series(1, 5) AS gs JOIN nums ON nums.n = gs.value ORDER BY gs.value """) assert [r[0] for r in await cur.fetchall()] == [1, 3, 5] finally: await conn.close() @pytest.mark.asyncio async def test_json_functions_extract_patch_and_array_length(): conn = await turso.aio.connect(":memory:") try: cur = conn.cursor() await cur.execute("CREATE TABLE docs (id INT PRIMARY KEY, doc TEXT)") obj = '{"a":1,"b":{"c":2},"arr":[10,20]}' await cur.execute("INSERT INTO docs (id, doc) VALUES (?, ?)", (1, obj)) await cur.execute("SELECT json_extract(doc, '$.b.c') FROM docs WHERE id = 1") assert (await cur.fetchone())[0] == 2 await cur.execute("SELECT json_extract(json_patch(doc, '{\"a\":5}'), '$.a') FROM docs WHERE id = 1") assert (await cur.fetchone())[0] == 5 await cur.execute("SELECT json_array_length(doc, '$.arr') FROM docs WHERE id = 1") assert (await cur.fetchone())[0] == 2 finally: await conn.close() @pytest.mark.asyncio async def test_async_operations_do_not_block_event_loop(): import time async with turso.aio.connect(":memory:") as conn: count = 1_000_000 await conn.execute("CREATE TABLE t (id INTEGER)") cur = await conn.execute(f"""SELECT SUM(value) FROM generate_series(1, {count})""") start = time.time() task = asyncio.create_task(cur.fetchone()) assert (time.time() - start) < 0.001 assert await task == (count * (count + 1) // 2,) @pytest.mark.asyncio async def test_operation_after_connection_close_raises(): import turso as _t conn = await turso.aio.connect(":memory:") cur = conn.cursor() await cur.execute("CREATE TABLE t (id INT)") await conn.close() with pytest.raises(_t.ProgrammingError): await conn.execute("SELECT 1") @pytest.mark.asyncio async def test_cursor_async_context_manager_closes_cursor(): import turso as _t conn = await turso.aio.connect(":memory:") try: async with conn.cursor() as cur: await cur.execute("SELECT 1") assert (await cur.fetchone())[0] == 1 # Cursor is closed after context manager exit with pytest.raises(_t.ProgrammingError): await cur.fetchone() finally: await conn.close() ================================================ FILE: bindings/python/tests/test_database_sync.py ================================================ import logging import multiprocessing import os import tempfile import time import turso import turso.sync from .utils import TursoServer logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", force=True) def test_bootstrap(): with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync')") server.db_sql("SELECT * FROM t") conn = turso.sync.connect(":memory:", server.db_url()) rows = conn.execute("SELECT * FROM t").fetchall() assert rows == [("hello",), ("turso",), ("sync",)] def test_pull(): with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync')") server.db_sql("SELECT * FROM t") conn = turso.sync.connect(":memory:", server.db_url()) rows = conn.execute("SELECT * FROM t").fetchall() assert rows == [("hello",), ("turso",), ("sync",)] server.db_sql("INSERT INTO t VALUES ('pull works')") rows = conn.execute("SELECT * FROM t").fetchall() assert rows == [("hello",), ("turso",), ("sync",)] assert conn.pull() rows = conn.execute("SELECT * FROM t").fetchall() assert rows == [("hello",), ("turso",), ("sync",), ("pull works",)] assert not conn.pull() def test_push(): with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync')") server.db_sql("SELECT * FROM t") conn = turso.sync.connect(":memory:", server.db_url()) rows = conn.execute("SELECT * FROM t").fetchall() assert rows == [("hello",), ("turso",), ("sync",)] conn.execute("INSERT INTO t VALUES ('push works')") conn.commit() r1 = server.db_sql("SELECT * FROM t") assert r1 == [["hello"], ["turso"], ["sync"]] conn.push() r2 = server.db_sql("SELECT * FROM t") assert r2 == [["hello"], ["turso"], ["sync"], ["push works"]] def test_checkpoint(): # turso.setup_logging(level=logging.DEBUG) with TursoServer() as server: conn = turso.sync.connect(":memory:", remote_url=server.db_url()) conn.execute("CREATE TABLE t(x)") conn.commit() for i in range(1024): conn.execute(f"INSERT INTO t VALUES ({i})") conn.commit() stats1 = conn.stats() conn.checkpoint() stats2 = conn.stats() assert stats1.main_wal_size > 1024 * 1024 assert stats1.revert_wal_size == 0 assert stats2.main_wal_size == 0 assert stats2.revert_wal_size < 8 * 1024 conn.push() assert server.db_sql("SELECT SUM(x) FROM t") == [[f"{1024 * 1023 // 2}"]] def test_partial_sync(): # turso.setup_logging(level=logging.DEBUG) with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t SELECT randomblob(1024) FROM generate_series(1, 2000)") conn_full = turso.sync.connect(":memory:", remote_url=server.db_url()) assert conn_full.execute("SELECT LENGTH(x) FROM t LIMIT 1").fetchall() == [(1024,)] assert conn_full.stats().network_received_bytes > 2000 * 1024 assert conn_full.execute("SELECT SUM(LENGTH(x)) FROM t").fetchall() == [(2000 * 1024,)] conn_partial = turso.sync.connect( ":memory:", remote_url=server.db_url(), partial_sync_experimental=turso.sync.PartialSyncOpts( bootstrap_strategy=turso.sync.PartialSyncPrefixBootstrap(length=128 * 1024), ), ) assert conn_partial.execute("SELECT LENGTH(x) FROM t LIMIT 1").fetchall() == [(1024,)] assert conn_partial.stats().network_received_bytes < 256 * (1024 + 10) start = time.time() assert conn_partial.execute("SELECT SUM(LENGTH(x)) FROM t").fetchall() == [(2000 * 1024,)] print(time.time() - start) assert conn_partial.stats().network_received_bytes > 2000 * 1024 def test_partial_sync_segment_size(): # turso.setup_logging(level=logging.DEBUG) with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t SELECT randomblob(1024) FROM generate_series(1, 256)") conn_full = turso.sync.connect(":memory:", remote_url=server.db_url()) assert conn_full.execute("SELECT LENGTH(x) FROM t LIMIT 1").fetchall() == [(1024,)] assert conn_full.stats().network_received_bytes > 256 * 1024 assert conn_full.execute("SELECT SUM(LENGTH(x)) FROM t").fetchall() == [(256 * 1024,)] conn_partial = turso.sync.connect( ":memory:", remote_url=server.db_url(), partial_sync_experimental=turso.sync.PartialSyncOpts( bootstrap_strategy=turso.sync.PartialSyncPrefixBootstrap(length=128 * 1024), segment_size=4 * 1024, ), ) assert conn_partial.execute("SELECT LENGTH(x) FROM t LIMIT 1").fetchall() == [(1024,)] assert conn_partial.stats().network_received_bytes < 128 * 1024 * 1.5 start = time.time() assert conn_partial.execute("SELECT SUM(LENGTH(x)) FROM t").fetchall() == [(256 * 1024,)] print(time.time() - start) assert conn_partial.stats().network_received_bytes > 256 * 1024 def test_partial_sync_prefetch(): # turso.setup_logging(level=logging.DEBUG) with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t SELECT randomblob(1024) FROM generate_series(1, 2000)") conn_full = turso.sync.connect(":memory:", remote_url=server.db_url()) assert conn_full.execute("SELECT LENGTH(x) FROM t LIMIT 1").fetchall() == [(1024,)] assert conn_full.stats().network_received_bytes > 2000 * 1024 assert conn_full.execute("SELECT SUM(LENGTH(x)) FROM t").fetchall() == [(2000 * 1024,)] conn_partial = turso.sync.connect( ":memory:", remote_url=server.db_url(), partial_sync_experimental=turso.sync.PartialSyncOpts( bootstrap_strategy=turso.sync.PartialSyncPrefixBootstrap(length=128 * 1024), segment_size=4 * 1024, prefetch=True, ), ) assert conn_partial.execute("SELECT LENGTH(x) FROM t LIMIT 1").fetchall() == [(1024,)] assert conn_partial.stats().network_received_bytes < 1200 * 1024 start = time.time() assert conn_partial.execute("SELECT SUM(LENGTH(x)) FROM t").fetchall() == [(2000 * 1024,)] print(time.time() - start) assert conn_partial.stats().network_received_bytes > 2000 * 1024 def run_full(path: str, remote_url: str, barrier: any): barrier.wait(timeout=60) try: print(turso.sync.connect(path, remote_url=remote_url)) except Exception as e: print("valid error", e, type(e), isinstance(e, turso.Error), turso.Error) def test_bootstrap_concurrency(): # turso.setup_logging(level=logging.DEBUG) with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t SELECT randomblob(1024) FROM generate_series(1, 2000)") with tempfile.TemporaryDirectory(prefix="pyturso-") as dir: path = os.path.join(dir, "local.db") print(path) barrier = multiprocessing.Barrier(2) t1 = multiprocessing.Process(target=run_full, args=(path, server.db_url(), barrier)) t2 = multiprocessing.Process(target=run_full, args=(path, server.db_url(), barrier)) t1.start() t2.start() t1.join(timeout=120) t2.join(timeout=120) if t1.is_alive(): t1.kill() t1.join() if t2.is_alive(): t2.kill() t2.join() assert t1.exitcode == 0, f"t1 exitcode: {t1.exitcode}" assert t2.exitcode == 0, f"t2 exitcode: {t2.exitcode}" def test_configuration_persistence(): with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t VALUES (42)") with tempfile.TemporaryDirectory(prefix="pyturso-") as dir: path = os.path.join(dir, "local.db") print(path) conn1 = turso.sync.connect(path, remote_url=server.db_url()) assert conn1.execute("SELECT * FROM t").fetchall() == [(42,)] conn1.close() server.db_sql("INSERT INTO t VALUES (43)") assert "http://localhost" in open(f"{path}-info", "r").read() conn2 = turso.sync.connect(path) conn2.pull() assert conn2.execute("SELECT * FROM t").fetchall() == [(42,), (43,)] conn2.close() ================================================ FILE: bindings/python/tests/test_database_sync_aio.py ================================================ import logging import time import pytest import turso.aio.sync from .utils import TursoServer logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", force=True) @pytest.mark.asyncio async def test_bootstrap(): with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync')") server.db_sql("SELECT * FROM t") conn = await turso.aio.sync.connect(":memory:", server.db_url()) rows = await (await conn.execute("SELECT * FROM t")).fetchall() assert rows == [("hello",), ("turso",), ("sync",)] @pytest.mark.asyncio async def test_pull(): with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync')") server.db_sql("SELECT * FROM t") conn = await turso.aio.sync.connect(":memory:", server.db_url()) rows = await (await conn.execute("SELECT * FROM t")).fetchall() assert rows == [("hello",), ("turso",), ("sync",)] server.db_sql("INSERT INTO t VALUES ('pull works')") rows = await (await conn.execute("SELECT * FROM t")).fetchall() assert rows == [("hello",), ("turso",), ("sync",)] assert await conn.pull() rows = await (await conn.execute("SELECT * FROM t")).fetchall() assert rows == [("hello",), ("turso",), ("sync",), ("pull works",)] assert not await conn.pull() @pytest.mark.asyncio async def test_push(): with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync')") server.db_sql("SELECT * FROM t") conn = await turso.aio.sync.connect(":memory:", server.db_url()) rows = await (await conn.execute("SELECT * FROM t")).fetchall() assert rows == [("hello",), ("turso",), ("sync",)] await conn.execute("INSERT INTO t VALUES ('push works')") await conn.commit() r1 = server.db_sql("SELECT * FROM t") assert r1 == [["hello"], ["turso"], ["sync"]] await conn.push() r2 = server.db_sql("SELECT * FROM t") assert r2 == [["hello"], ["turso"], ["sync"], ["push works"]] @pytest.mark.asyncio async def test_checkpoint(): # turso.setup_logging(level=logging.DEBUG) with TursoServer() as server: conn = await turso.aio.sync.connect(":memory:", remote_url=server.db_url()) await conn.execute("CREATE TABLE t(x)") await conn.commit() for i in range(1024): await conn.execute(f"INSERT INTO t VALUES ({i})") await conn.commit() stats1 = await conn.stats() await conn.checkpoint() stats2 = await conn.stats() assert stats1.main_wal_size > 1024 * 1024 assert stats1.revert_wal_size == 0 assert stats2.main_wal_size == 0 assert stats2.revert_wal_size < 8 * 1024 await conn.push() assert server.db_sql("SELECT SUM(x) FROM t") == [[f"{1024 * 1023 // 2}"]] @pytest.mark.asyncio async def test_partial_sync(): # turso.setup_logging(level=logging.DEBUG) with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t SELECT randomblob(1024) FROM generate_series(1, 2000)") conn_full = await turso.aio.sync.connect(":memory:", remote_url=server.db_url()) assert await (await conn_full.execute("SELECT LENGTH(x) FROM t LIMIT 1")).fetchall() == [(1024,)] assert (await conn_full.stats()).network_received_bytes > 2000 * 1024 assert await (await conn_full.execute("SELECT SUM(LENGTH(x)) FROM t")).fetchall() == [(2000 * 1024,)] conn_partial = await turso.aio.sync.connect( ":memory:", remote_url=server.db_url(), partial_sync_experimental=turso.aio.sync.PartialSyncOpts( bootstrap_strategy=turso.aio.sync.PartialSyncPrefixBootstrap(length=128 * 1024), ), ) assert await (await conn_partial.execute("SELECT LENGTH(x) FROM t LIMIT 1")).fetchall() == [(1024,)] assert (await conn_partial.stats()).network_received_bytes < 256 * (1024 + 10) start = time.time() assert await (await conn_partial.execute("SELECT SUM(LENGTH(x)) FROM t")).fetchall() == [(2000 * 1024,)] print(time.time() - start) assert (await conn_partial.stats()).network_received_bytes > 2000 * 1024 @pytest.mark.asyncio async def test_partial_sync_segment_size(): # turso.setup_logging(level=logging.DEBUG) with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t SELECT randomblob(1024) FROM generate_series(1, 256)") conn_full = await turso.aio.sync.connect(":memory:", remote_url=server.db_url()) assert await (await conn_full.execute("SELECT LENGTH(x) FROM t LIMIT 1")).fetchall() == [(1024,)] assert (await conn_full.stats()).network_received_bytes > 256 * 1024 assert await (await conn_full.execute("SELECT SUM(LENGTH(x)) FROM t")).fetchall() == [(256 * 1024,)] conn_partial = await turso.aio.sync.connect( ":memory:", remote_url=server.db_url(), partial_sync_experimental=turso.aio.sync.PartialSyncOpts( bootstrap_strategy=turso.aio.sync.PartialSyncPrefixBootstrap(length=128 * 1024), segment_size=4 * 1024, ), ) assert await (await conn_partial.execute("SELECT LENGTH(x) FROM t LIMIT 1")).fetchall() == [(1024,)] assert (await conn_partial.stats()).network_received_bytes < 128 * 1024 * 1.5 start = time.time() assert await (await conn_partial.execute("SELECT SUM(LENGTH(x)) FROM t")).fetchall() == [(256 * 1024,)] print(time.time() - start) assert (await conn_partial.stats()).network_received_bytes > 256 * 1024 @pytest.mark.asyncio async def test_partial_sync_prefetch(): # turso.setup_logging(level=logging.DEBUG) with TursoServer() as server: server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t SELECT randomblob(1024) FROM generate_series(1, 2000)") conn_full = await turso.aio.sync.connect(":memory:", remote_url=server.db_url()) assert await (await conn_full.execute("SELECT LENGTH(x) FROM t LIMIT 1")).fetchall() == [(1024,)] assert (await conn_full.stats()).network_received_bytes > 2000 * 1024 assert await (await conn_full.execute("SELECT SUM(LENGTH(x)) FROM t")).fetchall() == [(2000 * 1024,)] # turso.setup_logging(logging.DEBUG) conn_partial = await turso.aio.sync.connect( ":memory:", remote_url=server.db_url(), partial_sync_experimental=turso.aio.sync.PartialSyncOpts( bootstrap_strategy=turso.aio.sync.PartialSyncPrefixBootstrap(length=128 * 1024), segment_size=4 * 1024, prefetch=True, ), ) assert await (await conn_partial.execute("SELECT LENGTH(x) FROM t LIMIT 1")).fetchall() == [(1024,)] assert (await conn_partial.stats()).network_received_bytes < 1200 * 1024 start = time.time() assert await (await conn_partial.execute("SELECT SUM(LENGTH(x)) FROM t")).fetchall() == [(2000 * 1024,)] print(time.time() - start) assert (await conn_partial.stats()).network_received_bytes > 2000 * 1024 ================================================ FILE: bindings/python/tests/test_sqlalchemy.py ================================================ """Tests for the SQLAlchemy dialect.""" import pytest # Skip all tests if SQLAlchemy is not installed sqlalchemy = pytest.importorskip("sqlalchemy") from sqlalchemy import Column, Integer, String, create_engine, text # noqa: E402 from sqlalchemy.engine import URL # noqa: E402 from sqlalchemy.orm import Session, declarative_base # noqa: E402 from turso.sqlalchemy import TursoDialect, TursoSyncDialect, get_sync_connection # noqa: E402 class TestTursoDialectImport: """Test TursoDialect module imports.""" def test_import_dbapi(self): """Test that import_dbapi returns turso module.""" dbapi = TursoDialect.import_dbapi() assert hasattr(dbapi, "connect") assert hasattr(dbapi, "Connection") assert hasattr(dbapi, "Cursor") assert dbapi.apilevel == "2.0" def test_dialect_attributes(self): """Test dialect class attributes.""" assert TursoDialect.name == "sqlite" assert TursoDialect.driver == "turso" class TestTursoSyncDialectImport: """Test TursoSyncDialect module imports.""" def test_import_dbapi(self): """Test that import_dbapi returns turso.sync module.""" dbapi = TursoSyncDialect.import_dbapi() assert hasattr(dbapi, "connect") assert hasattr(dbapi, "ConnectionSync") def test_dialect_attributes(self): """Test dialect class attributes.""" assert TursoSyncDialect.name == "sqlite" assert TursoSyncDialect.driver == "turso_sync" class TestTursoDialectURLParsing: """Test URL parsing for basic TursoDialect.""" def test_memory_database(self): """Test in-memory database URL.""" dialect = TursoDialect() url = URL.create("sqlite+turso", database=":memory:") args, kwargs = dialect.create_connect_args(url) assert args == [":memory:"] assert kwargs == {} def test_file_database(self): """Test file-based database URL.""" dialect = TursoDialect() url = URL.create("sqlite+turso", database="/path/to/db.db") args, kwargs = dialect.create_connect_args(url) assert args == ["/path/to/db.db"] def test_isolation_level_param(self): """Test isolation_level query parameter.""" dialect = TursoDialect() url = URL.create( "sqlite+turso", database="test.db", query={"isolation_level": "IMMEDIATE"} ) args, kwargs = dialect.create_connect_args(url) assert args == ["test.db"] assert kwargs["isolation_level"] == "IMMEDIATE" def test_autocommit_isolation_level(self): """Test AUTOCOMMIT isolation level converts to None.""" dialect = TursoDialect() url = URL.create( "sqlite+turso", database="test.db", query={"isolation_level": "AUTOCOMMIT"} ) args, kwargs = dialect.create_connect_args(url) assert kwargs["isolation_level"] is None class TestTursoSyncDialectURLParsing: """Test URL parsing for TursoSyncDialect.""" def test_basic_url_parsing(self): """Test basic URL with path and remote_url.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", database="/path/to/db.db", query={"remote_url": "https://db.turso.io"}, ) args, kwargs = dialect.create_connect_args(url) assert args == ["/path/to/db.db", "https://db.turso.io"] assert kwargs.get("client_name") == "turso-sqlalchemy" def test_memory_database(self): """Test in-memory database URL.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", database=":memory:", query={"remote_url": "https://db.turso.io"}, ) args, kwargs = dialect.create_connect_args(url) assert args[0] == ":memory:" def test_all_query_params(self): """Test all supported query parameters.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", database="test.db", query={ "remote_url": "https://db.turso.io", "auth_token": "secret", "client_name": "my-app", "long_poll_timeout_ms": "5000", "bootstrap_if_empty": "false", "isolation_level": "IMMEDIATE", }, ) args, kwargs = dialect.create_connect_args(url) assert args == ["test.db", "https://db.turso.io"] assert kwargs["auth_token"] == "secret" assert kwargs["client_name"] == "my-app" assert kwargs["long_poll_timeout_ms"] == 5000 assert kwargs["bootstrap_if_empty"] is False assert kwargs["isolation_level"] == "IMMEDIATE" def test_bootstrap_if_empty_variations(self): """Test various boolean string representations.""" dialect = TursoSyncDialect() for true_val in ["true", "True", "TRUE", "1", "yes"]: url = URL.create( "sqlite+turso_sync", database="test.db", query={"remote_url": "https://db.turso.io", "bootstrap_if_empty": true_val}, ) _, kwargs = dialect.create_connect_args(url) assert kwargs["bootstrap_if_empty"] is True, f"Failed for {true_val}" for false_val in ["false", "False", "FALSE", "0", "no"]: url = URL.create( "sqlite+turso_sync", database="test.db", query={"remote_url": "https://db.turso.io", "bootstrap_if_empty": false_val}, ) _, kwargs = dialect.create_connect_args(url) assert kwargs["bootstrap_if_empty"] is False, f"Failed for {false_val}" def test_rejects_username_password(self): """Test that username/password in URL raises error.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", username="user", password="pass", database="test.db", query={"remote_url": "https://db.turso.io"}, ) with pytest.raises(ValueError, match="username/password"): dialect.create_connect_args(url) def test_rejects_host_port(self): """Test that host/port in URL raises error.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", host="localhost", port=5432, database="test.db", query={"remote_url": "https://db.turso.io"}, ) with pytest.raises(ValueError, match="host/port"): dialect.create_connect_args(url) def test_warns_on_unknown_params(self): """Test that unknown query parameters trigger a warning.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", database="test.db", query={ "remote_url": "https://db.turso.io", "unknown_param": "value", }, ) with pytest.warns(UserWarning, match="unknown_param"): dialect.create_connect_args(url) def test_sync_url_query_param_compat(self): """Test that sync_url is accepted as alias for remote_url in URL query params.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", database="/path/to/db.db", query={"sync_url": "https://db.turso.io"}, ) args, kwargs = dialect.create_connect_args(url) assert args == ["/path/to/db.db", "https://db.turso.io"] def test_remote_url_takes_precedence_over_sync_url(self): """Test that remote_url is preferred when both are provided.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", database="test.db", query={ "remote_url": "https://primary.turso.io", "sync_url": "https://fallback.turso.io", }, ) args, _ = dialect.create_connect_args(url) assert args == ["test.db", "https://primary.turso.io"] def test_sync_url_connect_args_compat(self): """Test that sync_url in connect_args is remapped to remote_url.""" dialect = TursoSyncDialect() # Simulate what SQLAlchemy does: dialect.connect(*cargs, **cparams) # where cparams includes connect_args merged in. # We can't call connect() directly without a real DB, but we can # verify the remapping logic by checking the method exists and # testing the URL param path covers the same alias. url = URL.create( "sqlite+turso_sync", database="test.db", query={"sync_url": "https://db.turso.io", "auth_token": "secret"}, ) args, kwargs = dialect.create_connect_args(url) assert args == ["test.db", "https://db.turso.io"] assert kwargs["auth_token"] == "secret" class TestPoolClass: """Test connection pool class selection.""" def test_memory_uses_singleton_pool(self): """Test that :memory: databases use SingletonThreadPool.""" from sqlalchemy import pool dialect = TursoDialect() url = URL.create("sqlite+turso", database=":memory:") pool_class = dialect.get_pool_class(url) assert pool_class is pool.SingletonThreadPool def test_file_uses_queue_pool(self): """Test that file databases use QueuePool.""" from sqlalchemy import pool dialect = TursoDialect() url = URL.create("sqlite+turso", database="test.db") pool_class = dialect.get_pool_class(url) assert pool_class is pool.QueuePool class TestGetSyncConnectionErrors: """Test get_sync_connection error handling.""" def test_raises_for_non_sync_dialect(self): """Test that get_sync_connection raises for non-turso connections.""" engine = create_engine("sqlite:///:memory:") with engine.connect() as conn: with pytest.raises(TypeError, match="ConnectionSync"): get_sync_connection(conn) class TestBasicDialectIntegration: """Integration tests for the basic TursoDialect.""" def test_basic_crud(self): """Test basic CRUD operations through SQLAlchemy.""" engine = create_engine("sqlite+turso:///:memory:") with engine.connect() as conn: conn.execute(text("CREATE TABLE test (id INTEGER, name TEXT)")) conn.execute(text("INSERT INTO test VALUES (1, 'alice')")) conn.commit() result = conn.execute(text("SELECT * FROM test")) rows = result.fetchall() assert rows == [(1, "alice")] def test_orm_usage(self): """Test ORM usage with basic dialect.""" Base = declarative_base() class Item(Base): __tablename__ = "items" id = Column(Integer, primary_key=True) name = Column(String(100)) engine = create_engine("sqlite+turso:///:memory:") Base.metadata.create_all(engine) with Session(engine) as session: session.add(Item(name="Widget")) session.commit() items = session.query(Item).all() assert len(items) == 1 assert items[0].name == "Widget" def test_pandas_to_sql(self): """Test Pandas DataFrame.to_sql() with if_exists='replace'.""" pd = pytest.importorskip("pandas") df = pd.DataFrame({ "id": [1, 2, 3], "name": ["Alice", "Bob", "Charlie"], }) engine = create_engine("sqlite+turso:///:memory:") with engine.connect() as conn: # First insert df.to_sql("test_table", conn, if_exists="replace", index=False) # Verify result = pd.read_sql("SELECT * FROM test_table", conn) assert len(result) == 3 # Replace (this tests table reflection which was failing) df.to_sql("test_table", conn, if_exists="replace", index=False) # Verify again result = pd.read_sql("SELECT * FROM test_table", conn) assert len(result) == 3 def test_table_reflection_returns_empty(self): """Test that table reflection methods return empty (not error).""" engine = create_engine("sqlite+turso:///:memory:") with engine.connect() as conn: conn.execute(text("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")) conn.commit() # Get inspector from sqlalchemy import inspect inspector = inspect(engine) # These should return empty lists, not raise errors fks = inspector.get_foreign_keys("test") assert fks == [] indexes = inspector.get_indexes("test") assert indexes == [] ucs = inspector.get_unique_constraints("test") assert ucs == [] class TestSyncDialectIntegration: """Integration tests for sync dialect with real sync server. These tests require a running sync server. They will be skipped if the server is not available. """ @pytest.fixture def server(self): """Create a TursoServer for testing.""" pytest.importorskip("requests") from tests.utils import TursoServer try: with TursoServer() as s: yield s except Exception as e: pytest.skip(f"TursoServer not available: {e}") def test_basic_sync_connection(self, server): """Test creating a sync connection through SQLAlchemy.""" engine = create_engine( "sqlite+turso_sync:///:memory:", connect_args={"remote_url": server.db_url()}, ) with engine.connect() as conn: conn.execute(text("CREATE TABLE test (id INTEGER, name TEXT)")) conn.execute(text("INSERT INTO test VALUES (1, 'alice')")) conn.commit() result = conn.execute(text("SELECT * FROM test")) rows = result.fetchall() assert rows == [(1, "alice")] def test_get_sync_connection(self, server): """Test that get_sync_connection returns ConnectionSync.""" from turso.lib_sync import ConnectionSync engine = create_engine( "sqlite+turso_sync:///:memory:", connect_args={"remote_url": server.db_url()}, ) with engine.connect() as conn: sync = get_sync_connection(conn) assert isinstance(sync, ConnectionSync) def test_sync_operations(self, server): """Test pull/push through SQLAlchemy connection.""" # Create initial data on server server.db_sql("CREATE TABLE t(x)") server.db_sql("INSERT INTO t VALUES ('remote')") engine = create_engine( "sqlite+turso_sync:///:memory:", connect_args={"remote_url": server.db_url()}, ) with engine.connect() as conn: sync = get_sync_connection(conn) # Data should be bootstrapped result = conn.execute(text("SELECT * FROM t")) assert result.fetchall() == [("remote",)] # Insert locally conn.execute(text("INSERT INTO t VALUES ('local')")) conn.commit() # Push to remote sync.push() # Verify on remote remote_rows = server.db_sql("SELECT * FROM t") assert ["local"] in remote_rows def test_orm_with_sync(self, server): """Test ORM usage with sync dialect.""" Base = declarative_base() class Item(Base): __tablename__ = "items" id = Column(Integer, primary_key=True) name = Column(String(100)) engine = create_engine( "sqlite+turso_sync:///:memory:", connect_args={"remote_url": server.db_url()}, ) Base.metadata.create_all(engine) with Session(engine) as session: session.add(Item(name="Widget")) session.commit() items = session.query(Item).all() assert len(items) == 1 assert items[0].name == "Widget" def test_url_with_remote_url_param(self, server): """Test passing remote_url as URL query parameter.""" db_url = server.db_url() engine = create_engine(f"sqlite+turso_sync:///:memory:?remote_url={db_url}") with engine.connect() as conn: conn.execute(text("CREATE TABLE test (x TEXT)")) conn.execute(text("INSERT INTO test VALUES ('hello')")) conn.commit() result = conn.execute(text("SELECT * FROM test")) assert result.fetchall() == [("hello",)] # ── Phase 1: Missing Unit Tests ────────────────────────────────── class TestTursoDialectMixin: """Test the _TursoDialectMixin methods via inspector and directly.""" @pytest.fixture def engine(self): return create_engine("sqlite+turso:///:memory:") @pytest.fixture def inspector(self, engine): from sqlalchemy import inspect with engine.connect() as conn: conn.execute(text("CREATE TABLE alpha (id INTEGER PRIMARY KEY, name TEXT)")) conn.execute(text("CREATE TABLE beta (id INTEGER PRIMARY KEY, val REAL)")) conn.commit() return inspect(engine) def test_get_check_constraints_returns_empty(self, inspector): """get_check_constraints returns empty list for any table.""" assert inspector.get_check_constraints("alpha") == [] def test_get_table_names_works(self, inspector): """inspector.get_table_names() returns the created tables.""" tables = inspector.get_table_names() assert "alpha" in tables assert "beta" in tables def test_get_columns_works(self, inspector): """inspector.get_columns() returns column info.""" cols = inspector.get_columns("alpha") col_names = [c["name"] for c in cols] assert "id" in col_names assert "name" in col_names def test_multi_indexes_returns_empty(self, inspector): """get_multi_indexes returns empty dict for multiple tables.""" dialect = TursoDialect() with inspector.bind.connect() as conn: result = dialect.get_multi_indexes(conn, filter_names=["alpha", "beta"]) assert result == {} def test_multi_unique_constraints_returns_empty(self, inspector): """get_multi_unique_constraints returns empty dict.""" dialect = TursoDialect() with inspector.bind.connect() as conn: result = dialect.get_multi_unique_constraints(conn, filter_names=["alpha", "beta"]) assert result == {} def test_multi_foreign_keys_returns_empty(self, inspector): """get_multi_foreign_keys returns empty dict.""" dialect = TursoDialect() with inspector.bind.connect() as conn: result = dialect.get_multi_foreign_keys(conn, filter_names=["alpha", "beta"]) assert result == {} def test_multi_check_constraints_returns_empty(self, inspector): """get_multi_check_constraints returns empty dict.""" dialect = TursoDialect() with inspector.bind.connect() as conn: result = dialect.get_multi_check_constraints(conn, filter_names=["alpha", "beta"]) assert result == {} def test_multi_table_reflection(self, inspector): """Reflection with multiple tables: all tables visible, constraints empty.""" tables = inspector.get_table_names() assert len(tables) >= 2 for table in ["alpha", "beta"]: assert inspector.get_foreign_keys(table) == [] assert inspector.get_indexes(table) == [] assert inspector.get_unique_constraints(table) == [] assert inspector.get_check_constraints(table) == [] class TestTursoDialectMethods: """Test TursoDialect methods not covered by integration tests.""" def test_on_connect_returns_none(self): """on_connect returns None — skips REGEXP setup.""" dialect = TursoDialect() assert dialect.on_connect() is None def test_get_isolation_level_returns_serializable(self): """get_isolation_level returns SERIALIZABLE.""" dialect = TursoDialect() assert dialect.get_isolation_level(None) == "SERIALIZABLE" def test_set_isolation_level_is_noop(self): """set_isolation_level doesn't raise for any value.""" dialect = TursoDialect() dialect.set_isolation_level(None, "SERIALIZABLE") dialect.set_isolation_level(None, "READ UNCOMMITTED") # No assertion needed — just verify no exception def test_supports_statement_cache(self): """Statement caching is enabled.""" assert TursoDialect.supports_statement_cache is True def test_supports_native_datetime_false(self): """Native datetime is disabled (turso handles datetime differently).""" assert TursoDialect.supports_native_datetime is False def test_experimental_features_param(self): """experimental_features query parameter is passed through.""" dialect = TursoDialect() url = URL.create( "sqlite+turso", database="test.db", query={"experimental_features": "feat1,feat2"} ) args, kwargs = dialect.create_connect_args(url) assert kwargs["experimental_features"] == "feat1,feat2" def test_default_database_memory(self): """URL with no database defaults to :memory:.""" dialect = TursoDialect() url = URL.create("sqlite+turso") args, kwargs = dialect.create_connect_args(url) assert args == [":memory:"] class TestTursoSyncDialectMethods: """Test TursoSyncDialect methods parallel to TursoDialect.""" def test_on_connect_returns_none(self): """Sync on_connect also returns None.""" dialect = TursoSyncDialect() assert dialect.on_connect() is None def test_get_isolation_level_returns_serializable(self): """Sync get_isolation_level returns SERIALIZABLE.""" dialect = TursoSyncDialect() assert dialect.get_isolation_level(None) == "SERIALIZABLE" def test_set_isolation_level_is_noop(self): """Sync set_isolation_level is also a no-op.""" dialect = TursoSyncDialect() dialect.set_isolation_level(None, "SERIALIZABLE") def test_supports_statement_cache(self): assert TursoSyncDialect.supports_statement_cache is True def test_supports_native_datetime_false(self): assert TursoSyncDialect.supports_native_datetime is False class TestTursoSyncDialectEdgeCases: """Edge cases for TursoSyncDialect URL parsing.""" def test_autocommit_isolation_level(self): """AUTOCOMMIT converts to None in sync dialect URL.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", database="test.db", query={"remote_url": "https://db.turso.io", "isolation_level": "AUTOCOMMIT"}, ) _, kwargs = dialect.create_connect_args(url) assert kwargs["isolation_level"] is None def test_no_remote_url(self): """URL without remote_url returns single-element positional args.""" dialect = TursoSyncDialect() url = URL.create("sqlite+turso_sync", database="test.db") args, kwargs = dialect.create_connect_args(url) assert args == ["test.db"] assert "remote_url" not in kwargs def test_experimental_features_param(self): """experimental_features in sync URL is passed through.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", database="test.db", query={ "remote_url": "https://db.turso.io", "experimental_features": "mvcc", }, ) _, kwargs = dialect.create_connect_args(url) assert kwargs["experimental_features"] == "mvcc" def test_long_poll_timeout_ms_integer_conversion(self): """long_poll_timeout_ms is converted from string to int.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", database="test.db", query={ "remote_url": "https://db.turso.io", "long_poll_timeout_ms": "3000", }, ) _, kwargs = dialect.create_connect_args(url) assert kwargs["long_poll_timeout_ms"] == 3000 assert isinstance(kwargs["long_poll_timeout_ms"], int) def test_default_client_name(self): """Default client_name is 'turso-sqlalchemy' when not specified.""" dialect = TursoSyncDialect() url = URL.create( "sqlite+turso_sync", database="test.db", query={"remote_url": "https://db.turso.io"}, ) _, kwargs = dialect.create_connect_args(url) assert kwargs["client_name"] == "turso-sqlalchemy" def test_sync_pool_class_memory(self): """Sync dialect uses SingletonThreadPool for :memory:.""" from sqlalchemy import pool dialect = TursoSyncDialect() url = URL.create("sqlite+turso_sync", database=":memory:") assert dialect.get_pool_class(url) is pool.SingletonThreadPool def test_sync_pool_class_file(self): """Sync dialect uses QueuePool for file databases.""" from sqlalchemy import pool dialect = TursoSyncDialect() url = URL.create("sqlite+turso_sync", database="test.db") assert dialect.get_pool_class(url) is pool.QueuePool class TestDBAPI2ModuleAttributes: """Test DB-API 2.0 module-level attributes for both modules.""" def test_turso_module_attributes(self): """turso module has all required DB-API 2.0 attributes.""" import turso assert turso.apilevel == "2.0" assert turso.paramstyle == "qmark" assert turso.threadsafety == 1 def test_turso_sync_module_attributes(self): """turso.sync module has all DB-API 2.0 attributes.""" import turso.sync assert turso.sync.apilevel == "2.0" assert turso.sync.paramstyle == "qmark" assert turso.sync.threadsafety == 1 def test_turso_sync_exception_hierarchy(self): """Exception classes are properly re-exported in turso.sync.""" import turso.sync # All DB-API 2.0 required exception classes assert issubclass(turso.sync.Warning, Exception) assert issubclass(turso.sync.Error, Exception) assert issubclass(turso.sync.InterfaceError, turso.sync.Error) assert issubclass(turso.sync.DatabaseError, turso.sync.Error) assert issubclass(turso.sync.DataError, turso.sync.DatabaseError) assert issubclass(turso.sync.OperationalError, turso.sync.DatabaseError) assert issubclass(turso.sync.IntegrityError, turso.sync.DatabaseError) assert issubclass(turso.sync.InternalError, turso.sync.DatabaseError) assert issubclass(turso.sync.ProgrammingError, turso.sync.DatabaseError) assert issubclass(turso.sync.NotSupportedError, turso.sync.DatabaseError) def test_turso_sync_sqlite_version(self): """sqlite_version and sqlite_version_info are available in turso.sync.""" import turso.sync assert isinstance(turso.sync.sqlite_version, str) assert "." in turso.sync.sqlite_version assert isinstance(turso.sync.sqlite_version_info, tuple) assert len(turso.sync.sqlite_version_info) == 3 assert all(isinstance(p, int) for p in turso.sync.sqlite_version_info) class TestEntryPointRegistration: """Test SQLAlchemy dialect entry points resolve correctly.""" def test_turso_dialect_resolves(self): """create_engine('sqlite+turso://') resolves to TursoDialect.""" engine = create_engine("sqlite+turso:///:memory:") assert isinstance(engine.dialect, TursoDialect) def test_turso_sync_dialect_resolves(self): """create_engine('sqlite+turso_sync://') resolves to TursoSyncDialect.""" # Verify the dialect class resolves via URL scheme dialect = TursoSyncDialect() assert dialect.name == "sqlite" assert dialect.driver == "turso_sync" # Also verify the entry point is loadable url = URL.create("sqlite+turso_sync", database=":memory:") dialect_cls = url.get_dialect() assert dialect_cls is TursoSyncDialect # ── Phase 2: Extended Integration Tests ────────────────────────── class TestSQLFeatureCoverage: """Test SQL features work through SQLAlchemy with turso dialect.""" @pytest.fixture def engine(self): return create_engine("sqlite+turso:///:memory:") def test_transaction_commit_rollback(self, engine): """Explicit commit and rollback work correctly.""" with engine.connect() as conn: conn.execute(text("CREATE TABLE txn_test (id INTEGER, val TEXT)")) conn.commit() # Insert and rollback conn.execute(text("INSERT INTO txn_test VALUES (1, 'should_vanish')")) conn.rollback() result = conn.execute(text("SELECT COUNT(*) FROM txn_test")) assert result.scalar() == 0 # Insert and commit conn.execute(text("INSERT INTO txn_test VALUES (2, 'should_stay')")) conn.commit() result = conn.execute(text("SELECT val FROM txn_test WHERE id = 2")) assert result.scalar() == "should_stay" def test_multiple_tables_with_join(self, engine): """ORM with multiple related tables and JOIN queries.""" from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship Base = declarative_base() class Author(Base): __tablename__ = "authors" id = Column(Integer, primary_key=True) name = Column(String(100)) books = relationship("Book", back_populates="author") class Book(Base): __tablename__ = "books" id = Column(Integer, primary_key=True) title = Column(String(200)) author_id = Column(Integer, ForeignKey("authors.id")) author = relationship("Author", back_populates="books") Base.metadata.create_all(engine) with Session(engine) as session: a = Author(name="Tolkien") a.books = [Book(title="The Hobbit"), Book(title="LOTR")] session.add(a) session.commit() result = ( session.query(Book) .join(Author) .filter(Author.name == "Tolkien") .all() ) assert len(result) == 2 assert {b.title for b in result} == {"The Hobbit", "LOTR"} def test_batch_insert_via_orm(self, engine): """Adding multiple ORM objects in one session.""" Base = declarative_base() class Record(Base): __tablename__ = "records" id = Column(Integer, primary_key=True) value = Column(Integer) Base.metadata.create_all(engine) with Session(engine) as session: session.add_all([Record(value=i) for i in range(100)]) session.commit() count = session.query(Record).count() assert count == 100 def test_null_handling(self, engine): """NULL values round-trip correctly.""" with engine.connect() as conn: conn.execute(text("CREATE TABLE nullable (id INTEGER, val TEXT)")) conn.execute(text("INSERT INTO nullable VALUES (1, NULL)")) conn.commit() result = conn.execute(text("SELECT val FROM nullable WHERE id = 1")) assert result.scalar() is None def test_unicode_data(self, engine): """Unicode strings round-trip correctly.""" with engine.connect() as conn: conn.execute(text("CREATE TABLE uni (id INTEGER, val TEXT)")) conn.execute(text("INSERT INTO uni VALUES (1, '日本語テスト')")) conn.execute(text("INSERT INTO uni VALUES (2, '🚀🎉')")) conn.commit() rows = conn.execute(text("SELECT val FROM uni ORDER BY id")).fetchall() assert rows[0][0] == "日本語テスト" assert rows[1][0] == "🚀🎉" def test_datetime_handling(self, engine): """Datetime values round-trip as strings since supports_native_datetime=False.""" from datetime import datetime Base = declarative_base() class Event(Base): __tablename__ = "events" id = Column(Integer, primary_key=True) name = Column(String(100)) ts = Column(String) # Store as string — no native datetime Base.metadata.create_all(engine) now = datetime.now().isoformat() with Session(engine) as session: session.add(Event(name="launch", ts=now)) session.commit() event = session.query(Event).first() assert event.ts == now def test_sql_syntax_error(self, engine): """SQL errors propagate correctly through SQLAlchemy.""" from sqlalchemy.exc import DatabaseError as SADatabaseError with engine.connect() as conn: with pytest.raises(SADatabaseError): conn.execute(text("SELEKT * FORM nonexistent")) def test_integrity_error_propagation(self, engine): """Unique constraint violations raise IntegrityError.""" from sqlalchemy.exc import IntegrityError as SAIntegrityError with engine.connect() as conn: conn.execute(text("CREATE TABLE uniq (id INTEGER PRIMARY KEY, email TEXT UNIQUE)")) conn.execute(text("INSERT INTO uniq VALUES (1, 'a@b.com')")) conn.commit() with pytest.raises(SAIntegrityError): conn.execute(text("INSERT INTO uniq VALUES (2, 'a@b.com')")) def test_multiple_connections_same_engine(self, engine): """Engine handles multiple sequential connections.""" with engine.connect() as conn: conn.execute(text("CREATE TABLE multi (id INTEGER)")) conn.execute(text("INSERT INTO multi VALUES (1)")) conn.commit() with engine.connect() as conn: result = conn.execute(text("SELECT * FROM multi")) assert result.fetchall() == [(1,)] def test_context_manager_cleanup(self, engine): """engine.connect() as context manager properly cleans up.""" # Should not leak connections or raise on exit for _ in range(10): with engine.connect() as conn: conn.execute(text("SELECT 1")) def test_large_text_data(self, engine): """Large text values round-trip correctly.""" with engine.connect() as conn: conn.execute(text("CREATE TABLE big (id INTEGER, content TEXT)")) large = "x" * 100_000 conn.execute(text("INSERT INTO big VALUES (1, :content)"), {"content": large}) conn.commit() result = conn.execute(text("SELECT content FROM big WHERE id = 1")) assert result.scalar() == large ================================================ FILE: bindings/python/tests/utils.py ================================================ import os import random import string import subprocess import time import requests def random_str() -> str: return "".join([random.choice(string.ascii_letters) for _ in range(8)]) def handle_response(r): if r.status_code == 400 and "already exists" in r.text: return r.raise_for_status() ADMIN_URL = "http://localhost:8081" USER_URL = "http://localhost:8080" class TursoServer: def __init__(self): if "LOCAL_SYNC_SERVER" not in os.environ: name = random_str() tokens = USER_URL.split("://") handle_response(requests.post(ADMIN_URL + f"/v1/tenants/{name}")) handle_response(requests.post(ADMIN_URL + f"/v1/tenants/{name}/groups/{name}")) handle_response(requests.post(ADMIN_URL + f"/v1/tenants/{name}/groups/{name}/databases/{name}")) self._user_url = USER_URL self._db_url = f"{tokens[0]}://{name}--{name}--{name}.{tokens[1]}" self._host = f"{name}--{name}--{name}.localhost" self._server = None else: # Retry with different ports in case the chosen port is # unavailable (common on Windows where OS reserves port ranges). max_attempts = 5 for attempt in range(max_attempts): port = random.randint(10_000, 65535) self._server = subprocess.Popen( [os.environ["LOCAL_SYNC_SERVER"], "--sync-server", f"0.0.0.0:{port}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) self._user_url = f"http://localhost:{port}" self._db_url = f"http://localhost:{port}" self._host = "" # wait for server to be available deadline = time.time() + 30 while time.time() < deadline: rc = self._server.poll() if rc is not None: stderr = self._server.stderr.read().decode(errors="replace") if ( "os error 10013" in stderr or "os error 10048" in stderr or "address already in use" in stderr.lower() ): break # retry with a different port raise RuntimeError( f"sync server exited with code {rc} before accepting connections\nstderr: {stderr}" ) try: requests.get(self._user_url, timeout=5) break except Exception: time.sleep(0.1) else: stderr = "" if self._server.poll() is not None: stderr = self._server.stderr.read().decode(errors="replace") self._server.kill() raise TimeoutError( f"sync server did not become available within 30s\nstderr: {stderr}" ) # If the inner loop broke out due to a port conflict, retry if self._server.poll() is not None: if attempt == max_attempts - 1: raise RuntimeError( f"sync server failed to bind after {max_attempts} port attempts" ) continue break # server is up def __enter__(self): return self def __exit__(self, type, value, traceback): if self._server: self._server.kill() def db_url(self) -> str: return self._db_url def db_sql(self, sql: str): result = requests.post( self._user_url + "/v2/pipeline", json={"requests": [{"type": "execute", "stmt": {"sql": sql}}]}, headers={"Host": self._host}, ) result.raise_for_status() result = result.json() if result["results"][0]["type"] != "ok": raise Exception(f"remote sql execution failed: {result}") return [[cell["value"] for cell in row] for row in result["results"][0]["response"]["result"]["rows"]] ================================================ FILE: bindings/python/turso/__init__.py ================================================ import logging logging.getLogger(__name__).addHandler(logging.NullHandler()) from .lib import ( # noqa: E402 Connection, Cursor, DatabaseError, DataError, EncryptionOpts, Error, IntegrityError, InterfaceError, InternalError, NotSupportedError, OperationalError, ProgrammingError, Row, Warning, apilevel, connect, paramstyle, setup_logging, sqlite_version, sqlite_version_info, threadsafety, ) __all__ = [ "Connection", "Cursor", "Row", "connect", "setup_logging", "Warning", "DatabaseError", "DataError", "Error", "IntegrityError", "InterfaceError", "InternalError", "NotSupportedError", "OperationalError", "ProgrammingError", "apilevel", "paramstyle", "sqlite_version", "sqlite_version_info", "threadsafety", "EncryptionOpts", ] ================================================ FILE: bindings/python/turso/aio/__init__.py ================================================ from ..lib_aio import Connection, Cursor, connect __all__ = [ "connect", "Connection", "Cursor", ] ================================================ FILE: bindings/python/turso/aio/sync/__init__.py ================================================ from ...lib_sync import ( ConnectionSync, PartialSyncOpts, PartialSyncPrefixBootstrap, PartialSyncQueryBootstrap, ) from ...lib_sync_aio import ( connect_sync as connect, ) __all__ = [ "connect", "ConnectionSync", "PartialSyncOpts", "PartialSyncPrefixBootstrap", "PartialSyncQueryBootstrap", ] ================================================ FILE: bindings/python/turso/lib.py ================================================ from __future__ import annotations from collections.abc import Iterable, Iterator, Mapping, Sequence from dataclasses import dataclass from types import TracebackType from typing import Any, Callable, Optional, TypeVar from ._turso import ( Busy, Constraint, Corrupt, DatabaseFull, Interrupt, Misuse, NotAdb, PyTursoConnection, PyTursoDatabase, PyTursoDatabaseConfig, PyTursoEncryptionConfig, PyTursoExecutionResult, PyTursoLog, PyTursoSetupConfig, PyTursoStatement, PyTursoStatusCode, py_turso_database_open, py_turso_setup, ) from ._turso import ( Error as TursoError, ) from ._turso import ( PyTursoStatusCode as Status, ) # DB-API 2.0 module attributes apilevel = "2.0" threadsafety = 1 # 1 means: Threads may share the module, but not connections. paramstyle = "qmark" # Only positional parameters are supported. def _get_sqlite_version() -> tuple[str, tuple[int, int, int]]: """Get SQLite version from a temporary connection.""" try: cfg = PyTursoDatabaseConfig(path=":memory:") db = py_turso_database_open(cfg) conn = db.connect() stmt = conn.prepare("SELECT sqlite_version()") result = stmt.step() version_str = result[0] parts = tuple(int(p) for p in version_str.split(".")) # Ensure we have exactly 3 parts while len(parts) < 3: parts = (*parts, 0) return version_str, parts[:3] except Exception: # Fallback to a known compatible version return "3.45.0", (3, 45, 0) sqlite_version, sqlite_version_info = _get_sqlite_version() # Exception hierarchy following DB-API 2.0 class Warning(Exception): pass class Error(Exception): pass class InterfaceError(Error): pass class DatabaseError(Error): pass class DataError(DatabaseError): pass class OperationalError(DatabaseError): pass class IntegrityError(DatabaseError): pass class InternalError(DatabaseError): pass class ProgrammingError(DatabaseError): pass class NotSupportedError(DatabaseError): pass def _map_turso_exception(exc: Exception) -> Exception: """Maps Turso-specific exceptions to DB-API 2.0 exception hierarchy""" if isinstance(exc, Busy): return OperationalError(str(exc)) if isinstance(exc, Interrupt): return OperationalError(str(exc)) if isinstance(exc, Misuse): return InterfaceError(str(exc)) if isinstance(exc, Constraint): return IntegrityError(str(exc)) if isinstance(exc, TursoError): # Generic Turso error -> DatabaseError return DatabaseError(str(exc)) if isinstance(exc, DatabaseFull): return OperationalError(str(exc)) if isinstance(exc, NotAdb): return DatabaseError(str(exc)) if isinstance(exc, Corrupt): return DatabaseError(str(exc)) return exc # Internal helpers _DBCursorT = TypeVar("_DBCursorT", bound="Cursor") def _first_keyword(sql: str) -> str: """ Return the first SQL keyword (uppercased) ignoring leading whitespace and single-line and multi-line comments. This is intentionally minimal and only used to detect DML for implicit transaction handling. It may not handle all edge cases (e.g. complex WITH). """ i = 0 n = len(sql) while i < n: c = sql[i] if c.isspace(): i += 1 continue if c == "-" and i + 1 < n and sql[i + 1] == "-": # line comment i += 2 while i < n and sql[i] not in ("\r", "\n"): i += 1 continue if c == "/" and i + 1 < n and sql[i + 1] == "*": # block comment i += 2 while i + 1 < n and not (sql[i] == "*" and sql[i + 1] == "/"): i += 1 i = min(i + 2, n) continue break # read token j = i while j < n and (sql[j].isalpha() or sql[j] == "_"): j += 1 return sql[i:j].upper() def _is_dml(sql: str) -> bool: kw = _first_keyword(sql) if kw in ("INSERT", "UPDATE", "DELETE", "REPLACE"): return True # "WITH" can also prefix DML, but we conservatively skip it to avoid false positives. return False def _is_insert_or_replace(sql: str) -> bool: kw = _first_keyword(sql) return kw in ("INSERT", "REPLACE") def _run_execute_with_io(stmt: PyTursoStatement, extra_io: Optional[Callable[[], None]]) -> PyTursoExecutionResult: """ Run PyTursoStatement.execute() handling potential async IO loops. """ while True: result = stmt.execute() status = result.status if status == Status.Io: stmt.run_io() if extra_io: extra_io() continue return result def _step_once_with_io(stmt: PyTursoStatement, extra_io: Optional[Callable[[], None]]) -> PyTursoStatusCode: """ Run PyTursoStatement.step() once handling potential async IO loops. """ while True: status = stmt.step() if status == Status.Io: stmt.run_io() if extra_io: extra_io() continue return status @dataclass class _Prepared: stmt: PyTursoStatement tail_index: int has_columns: bool column_names: tuple[str, ...] # Connection goes FIRST class Connection: """ A connection to a Turso (SQLite-compatible) database. Similar to sqlite3.Connection with a subset of features focusing on DB-API 2.0. """ # Expose exception classes as attributes like sqlite3.Connection does @property def DataError(self) -> type[DataError]: return DataError @property def DatabaseError(self) -> type[DatabaseError]: return DatabaseError @property def Error(self) -> type[Error]: return Error @property def IntegrityError(self) -> type[IntegrityError]: return IntegrityError @property def InterfaceError(self) -> type[InterfaceError]: return InterfaceError @property def InternalError(self) -> type[InternalError]: return InternalError @property def NotSupportedError(self) -> type[NotSupportedError]: return NotSupportedError @property def OperationalError(self) -> type[OperationalError]: return OperationalError @property def ProgrammingError(self) -> type[ProgrammingError]: return ProgrammingError @property def Warning(self) -> type[Warning]: return Warning def __init__( self, conn: PyTursoConnection, *, isolation_level: Optional[str] = "DEFERRED", extra_io: Optional[Callable[[], None]] = None, ) -> None: self._conn: PyTursoConnection = conn # autocommit behavior: # - True: SQLite autocommit mode; commit/rollback are no-ops. # - False: PEP 249 compliant: ensure a transaction is always open. # We'll use BEGIN DEFERRED after commit/rollback. # - "LEGACY": implicit transactions on DML when isolation_level is not None. self._autocommit_mode: object | bool = "LEGACY" self.isolation_level: Optional[str] = isolation_level self.row_factory: Callable[[Cursor, Row], object] | type[Row] | None = None self.text_factory: Any = str self.extra_io = extra_io # If autocommit is False, ensure a transaction is open if self._autocommit_mode is False: self._ensure_transaction_open() def _ensure_transaction_open(self) -> None: """ Ensure a transaction is open when autocommit is False. """ try: if self._conn.get_auto_commit(): # No transaction active -> open new one according to isolation_level (default to DEFERRED) level = self.isolation_level or "DEFERRED" self._exec_ddl_only(f"BEGIN {level}") except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) def _exec_ddl_only(self, sql: str) -> None: """ Execute a SQL statement that does not produce rows and ignore any result rows. """ try: stmt = self._conn.prepare_single(sql) _run_execute_with_io(stmt, self.extra_io) # finalize to ensure completion; finalize never mixes with execute stmt.finalize() except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) def _prepare_first(self, sql: str) -> _Prepared: """ Prepare the first statement in the given SQL string and return metadata. """ try: opt = self._conn.prepare_first(sql) except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) if opt is None: raise ProgrammingError("no SQL statements to execute") stmt, tail_idx = opt # Determine whether statement returns columns (rows) try: columns = tuple(stmt.columns()) except Exception as exc: # noqa: BLE001 # Clean up statement before re-raising try: stmt.finalize() except Exception: pass raise _map_turso_exception(exc) has_cols = len(columns) > 0 return _Prepared(stmt=stmt, tail_index=tail_idx, has_columns=has_cols, column_names=columns) def _raise_if_multiple_statements(self, sql: str, tail_index: int) -> None: """ Ensure there is no second statement after the first one; otherwise raise ProgrammingError. """ # Skip any trailing whitespace/comments after tail_index, and check if another statement exists. rest = sql[tail_index:] try: nxt = self._conn.prepare_first(rest) if nxt is not None: # Clean-up the prepared second statement immediately second_stmt, _ = nxt try: second_stmt.finalize() except Exception: pass raise ProgrammingError("You can only execute one statement at a time") except ProgrammingError: raise except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) @property def in_transaction(self) -> bool: try: return not self._conn.get_auto_commit() except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) # Provide autocommit property for sqlite3-like API (optional) @property def autocommit(self) -> object | bool: return self._autocommit_mode @autocommit.setter def autocommit(self, val: object | bool) -> None: # Accept True, False, or "LEGACY" if val not in (True, False, "LEGACY"): raise ProgrammingError("autocommit must be True, False, or 'LEGACY'") self._autocommit_mode = val # If switching to False, ensure a transaction is open if val is False: self._ensure_transaction_open() # If switching to True or LEGACY, nothing else to do immediately. def close(self) -> None: # In sqlite3: If autocommit is False, pending transaction is implicitly rolled back. try: if self._autocommit_mode is False and self.in_transaction: try: self._exec_ddl_only("ROLLBACK") except Exception: # As sqlite3 does, ignore rollback failure on close pass self._conn.close() except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) def commit(self) -> None: try: if self._autocommit_mode is True: # No-op in SQLite autocommit mode return if self.in_transaction: self._exec_ddl_only("COMMIT") if self._autocommit_mode is False: # Re-open a transaction to maintain PEP 249 behavior self._ensure_transaction_open() except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) def rollback(self) -> None: try: if self._autocommit_mode is True: # No-op in SQLite autocommit mode return if self.in_transaction: self._exec_ddl_only("ROLLBACK") if self._autocommit_mode is False: # Re-open a transaction to maintain PEP 249 behavior self._ensure_transaction_open() except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) def _maybe_implicit_begin(self, sql: str) -> None: """ Implement sqlite3 legacy implicit transaction behavior: If autocommit is LEGACY_TRANSACTION_CONTROL, isolation_level is not None, sql is a DML (INSERT/UPDATE/DELETE/REPLACE), and there is no open transaction, issue: BEGIN """ if self._autocommit_mode == "LEGACY" and self.isolation_level is not None: if not self.in_transaction and _is_dml(sql): level = self.isolation_level or "DEFERRED" self._exec_ddl_only(f"BEGIN {level}") def cursor(self, factory: Optional[Callable[[Connection], _DBCursorT]] = None) -> _DBCursorT | Cursor: if factory is None: return Cursor(self) return factory(self) def execute(self, sql: str, parameters: Sequence[Any] | Mapping[str, Any] = ()) -> Cursor: cur = self.cursor() cur.execute(sql, parameters) return cur def executemany(self, sql: str, parameters: Iterable[Sequence[Any] | Mapping[str, Any]]) -> Cursor: cur = self.cursor() cur.executemany(sql, parameters) return cur def executescript(self, sql_script: str) -> Cursor: cur = self.cursor() cur.executescript(sql_script) return cur def __call__(self, sql: str) -> PyTursoStatement: # Shortcut to prepare a single statement try: return self._conn.prepare_single(sql) except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) def __enter__(self) -> "Connection": return self def __exit__( self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None, ) -> bool: # sqlite3 behavior: In context manager, if no exception -> commit, else rollback (legacy and PEP 249 modes) try: if type is None: self.commit() else: self.rollback() finally: # Always propagate exceptions (returning False) return False # Cursor goes SECOND class Cursor: arraysize: int def __init__(self, connection: Connection, /) -> None: self._connection: Connection = connection self.arraysize = 1 self.row_factory: Callable[[Cursor, Row], object] | type[Row] | None = connection.row_factory # State for the last executed statement self._active_stmt: Optional[PyTursoStatement] = None self._active_has_rows: bool = False self._description: Optional[tuple[tuple[str, None, None, None, None, None, None], ...]] = None self._lastrowid: Optional[int] = None self._rowcount: int = -1 self._closed: bool = False @property def connection(self) -> Connection: return self._connection def close(self) -> None: if self._closed: return try: # Finalize any active statement to ensure completion. if self._active_stmt is not None: try: self._active_stmt.finalize() except Exception: pass finally: self._active_stmt = None self._active_has_rows = False self._closed = True def _ensure_open(self) -> None: if self._closed: raise ProgrammingError("Cannot operate on a closed cursor") @property def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...] | None: return self._description @property def lastrowid(self) -> int | None: return self._lastrowid @property def rowcount(self) -> int: return self._rowcount def _reset_last_result(self) -> None: # Ensure any previous statement is finalized to not leak resources if self._active_stmt is not None: try: self._active_stmt.finalize() except Exception: pass self._active_stmt = None self._active_has_rows = False self._description = None self._rowcount = -1 # Do not reset lastrowid here; sqlite3 preserves lastrowid until next insert. @staticmethod def _to_positional_params(parameters: Sequence[Any] | Mapping[str, Any]) -> tuple[Any, ...]: if isinstance(parameters, Mapping): # Named placeholders are not supported raise ProgrammingError("Named parameters are not supported; use positional parameters with '?'") if parameters is None: return () if isinstance(parameters, tuple): return parameters # Convert arbitrary sequences to tuple efficiently return tuple(parameters) def _maybe_implicit_begin(self, sql: str) -> None: self._connection._maybe_implicit_begin(sql) def _prepare_single_statement(self, sql: str) -> _Prepared: prepared = self._connection._prepare_first(sql) # Ensure there are no further statements self._connection._raise_if_multiple_statements(sql, prepared.tail_index) return prepared def execute(self, sql: str, parameters: Sequence[Any] | Mapping[str, Any] = ()) -> "Cursor": self._ensure_open() self._reset_last_result() # Implement legacy implicit transactions if needed self._maybe_implicit_begin(sql) # Prepare exactly one statement prepared = self._prepare_single_statement(sql) stmt = prepared.stmt try: # Bind positional parameters params = self._to_positional_params(parameters) if params: stmt.bind(params) if prepared.has_columns: # Stepped statement (e.g., SELECT or DML with RETURNING) self._active_stmt = stmt self._active_has_rows = True # Set description immediately (even if there are no rows) self._description = tuple((name, None, None, None, None, None, None) for name in prepared.column_names) # For statements that return rows, DB-API specifies rowcount is -1 self._rowcount = -1 # Do not compute lastrowid here else: # Executed statement (no rows returned) result = _run_execute_with_io(stmt, self._connection.extra_io) # rows_changed from execution result self._rowcount = int(result.rows_changed) # Set description to None self._description = None # Set lastrowid for INSERT/REPLACE (best-effort) self._lastrowid = self._fetch_last_insert_rowid_if_needed(sql, result.rows_changed) # Finalize the statement to release resources stmt.finalize() except Exception as exc: # noqa: BLE001 # Ensure cleanup on error try: stmt.finalize() except Exception: pass raise _map_turso_exception(exc) return self def _fetch_last_insert_rowid_if_needed(self, sql: str, rows_changed: int) -> Optional[int]: if rows_changed <= 0 or not _is_insert_or_replace(sql): return self._lastrowid # Query last_insert_rowid(); this is connection-scoped and cheap try: q = self._connection._conn.prepare_single("SELECT last_insert_rowid()") # No parameters; this produces a single-row single-column result # Use stepping to fetch the row status = _step_once_with_io(q, self._connection.extra_io) if status == Status.Row: py_row = q.row() # row() returns a Python tuple with one element # We avoid complex conversions: take first item value = tuple(py_row)[0] # type: ignore[call-arg] # Finalize to complete q.finalize() if isinstance(value, int): return value try: return int(value) except Exception: return self._lastrowid # Finalize anyway q.finalize() except Exception: # Ignore errors; lastrowid remains unchanged on failure pass return self._lastrowid def executemany(self, sql: str, seq_of_parameters: Iterable[Sequence[Any] | Mapping[str, Any]]) -> "Cursor": self._ensure_open() self._reset_last_result() # executemany only accepts DML; enforce this to match sqlite3 semantics if not _is_dml(sql): raise ProgrammingError("executemany() requires a single DML (INSERT/UPDATE/DELETE/REPLACE) statement") # Implement legacy implicit transaction: same as execute() self._maybe_implicit_begin(sql) prepared = self._prepare_single_statement(sql) stmt = prepared.stmt try: # For executemany, discard any rows produced (even if RETURNING was used) # Therefore we ALWAYS use execute() path per-iteration. for parameters in seq_of_parameters: # Reset previous bindings and program memory before reusing stmt.reset() params = self._to_positional_params(parameters) if params: stmt.bind(params) result = _run_execute_with_io(stmt, self._connection.extra_io) # rowcount is "the number of modified rows" for the LAST executed statement only self._rowcount = int(result.rows_changed) + (self._rowcount if self._rowcount != -1 else 0) # After loop, finalize statement stmt.finalize() # Cursor description is None for DML executed via executemany() self._description = None # sqlite3 leaves lastrowid unchanged for executemany except Exception as exc: # noqa: BLE001 try: stmt.finalize() except Exception: pass raise _map_turso_exception(exc) return self def executescript(self, sql_script: str) -> "Cursor": self._ensure_open() self._reset_last_result() # sqlite3 behavior: If autocommit is LEGACY and there is a pending transaction, implicitly COMMIT first if self._connection._autocommit_mode == "LEGACY" and self._connection.in_transaction: try: self._connection._exec_ddl_only("COMMIT") except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) # Iterate over statements in the script and execute them, discarding rows sql = sql_script total_rowcount = -1 try: offset = 0 while True: opt = self._connection._conn.prepare_first(sql[offset:]) if opt is None: break stmt, tail = opt # Note: per DB-API, any resulting rows are discarded result = _run_execute_with_io(stmt, self._connection.extra_io) total_rowcount = int(result.rows_changed) if result.rows_changed > 0 else total_rowcount # finalize to ensure completion stmt.finalize() offset += tail except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) self._description = None self._rowcount = total_rowcount return self def _fetchone_tuple(self) -> Optional[tuple[Any, ...]]: """ Fetch one row as a plain Python tuple, or return None if no more rows. """ if not self._active_has_rows or self._active_stmt is None: return None try: status = _step_once_with_io(self._active_stmt, self._connection.extra_io) if status == Status.Row: row_tuple = tuple(self._active_stmt.row()) # type: ignore[call-arg] return row_tuple # status == Done: finalize and clean up self._active_stmt.finalize() self._active_stmt = None self._active_has_rows = False return None except Exception as exc: # noqa: BLE001 # Finalize and clean up on error try: if self._active_stmt is not None: self._active_stmt.finalize() except Exception: pass self._active_stmt = None self._active_has_rows = False raise _map_turso_exception(exc) def _apply_row_factory(self, row_values: tuple[Any, ...]) -> Any: rf = self.row_factory if rf is None: return row_values if isinstance(rf, type) and issubclass(rf, Row): return rf(self, Row(self, row_values)) # type: ignore[call-arg] if callable(rf): return rf(self, Row(self, row_values)) # type: ignore[misc] # Fallback: return tuple return row_values def fetchone(self) -> Any: self._ensure_open() row = self._fetchone_tuple() if row is None: return None return self._apply_row_factory(row) def fetchmany(self, size: Optional[int] = None) -> list[Any]: self._ensure_open() if size is None: size = self.arraysize if size < 0: raise ValueError("size must be non-negative") result: list[Any] = [] for _ in range(size): row = self._fetchone_tuple() if row is None: break result.append(self._apply_row_factory(row)) return result def fetchall(self) -> list[Any]: self._ensure_open() result: list[Any] = [] while True: row = self._fetchone_tuple() if row is None: break result.append(self._apply_row_factory(row)) return result def setinputsizes(self, sizes: Any, /) -> None: # No-op for DB-API compliance return None def setoutputsize(self, size: Any, column: Any = None, /) -> None: # No-op for DB-API compliance return None def __iter__(self) -> "Cursor": return self def __next__(self) -> Any: row = self.fetchone() if row is None: raise StopIteration return row # Row goes THIRD class Row(Sequence[Any]): """ sqlite3.Row-like container supporting index and name-based access. """ def __new__(cls, cursor: Cursor, data: tuple[Any, ...], /) -> "Row": obj = super().__new__(cls) # Attach metadata obj._cursor = cursor obj._data = data # Build mapping from column name to index desc = cursor.description or () obj._keys = tuple(col[0] for col in desc) obj._index = {name: idx for idx, name in enumerate(obj._keys)} return obj def keys(self) -> list[str]: return list(self._keys) def __getitem__(self, key: int | str | slice, /) -> Any: if isinstance(key, slice): return self._data[key] if isinstance(key, int): return self._data[key] # key is column name idx = self._index.get(key) if idx is None: raise KeyError(key) return self._data[idx] def __hash__(self) -> int: return hash((self._keys, self._data)) def __iter__(self) -> Iterator[Any]: return iter(self._data) def __len__(self) -> int: return len(self._data) def __eq__(self, value: object, /) -> bool: if not isinstance(value, Row): return NotImplemented # type: ignore[return-value] return self._keys == value._keys and self._data == value._data def __ne__(self, value: object, /) -> bool: if not isinstance(value, Row): return NotImplemented # type: ignore[return-value] return not self.__eq__(value) # The rest return NotImplemented for non-Row comparisons def __lt__(self, value: object, /) -> bool: if not isinstance(value, Row): return NotImplemented # type: ignore[return-value] return (self._keys, self._data) < (value._keys, value._data) def __le__(self, value: object, /) -> bool: if not isinstance(value, Row): return NotImplemented # type: ignore[return-value] return (self._keys, self._data) <= (value._keys, value._data) def __gt__(self, value: object, /) -> bool: if not isinstance(value, Row): return NotImplemented # type: ignore[return-value] return (self._keys, self._data) > (value._keys, value._data) def __ge__(self, value: object, /) -> bool: if not isinstance(value, Row): return NotImplemented # type: ignore[return-value] return (self._keys, self._data) >= (value._keys, value._data) @dataclass class EncryptionOpts: cipher: str hexkey: str def connect( database: str, *, experimental_features: Optional[str] = None, vfs: Optional[str] = None, encryption: Optional[EncryptionOpts] = None, isolation_level: Optional[str] = "DEFERRED", extra_io: Optional[Callable[[], None]] = None, ) -> Connection: """ Open a Turso (SQLite-compatible) database and return a Connection. Parameters: - database: path or identifier of the database. - experimental_features: comma-separated list of features to enable. - isolation_level: one of "DEFERRED" (default), "IMMEDIATE", "EXCLUSIVE", or None. """ try: cfg = PyTursoDatabaseConfig( path=database, experimental_features=experimental_features, vfs=vfs, encryption=PyTursoEncryptionConfig(cipher=encryption.cipher, hexkey=encryption.hexkey) if encryption else None, ) db: PyTursoDatabase = py_turso_database_open(cfg) conn: PyTursoConnection = db.connect() return Connection(conn, isolation_level=isolation_level, extra_io=extra_io) except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) # Make it easy to enable logging with native `logging` Python module def setup_logging(level: Optional[int] = None) -> None: """ Setup Turso logging to integrate with Python's logging module. Usage: import turso turso.setup_logging(logging.DEBUG) """ import logging level = level or logging.INFO logger = logging.getLogger("turso") logger.setLevel(level) def _py_logger(log: PyTursoLog) -> None: # Map Rust/Turso log level strings to Python logging levels (best-effort) lvl_map = { "ERROR": logging.ERROR, "WARN": logging.WARNING, "INFO": logging.INFO, "DEBUG": logging.DEBUG, "TRACE": logging.DEBUG, } py_level = lvl_map.get(log.level.upper(), level) logger.log( py_level, "%s [%s:%s] %s", log.target, log.file, log.line, log.message, ) try: py_turso_setup( PyTursoSetupConfig( logger=_py_logger, log_level={ logging.ERROR: "error", logging.WARN: "warn", logging.INFO: "info", logging.DEBUG: "debug", }[level], ) ) except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) ================================================ FILE: bindings/python/turso/lib_aio.py ================================================ from __future__ import annotations import asyncio from queue import SimpleQueue from typing import Any, Callable, Iterable, Mapping, Optional, Sequence from .lib import ( Connection as BlockingConnection, ) from .lib import ( Cursor as BlockingCursor, ) from .lib import ( ProgrammingError, ) from .lib import ( connect as blocking_connect, ) from .worker import STOP_RUNNING_SENTINEL, Worker # Connection goes FIRST class Connection: def __init__(self, connector: Callable[[], BlockingConnection]) -> None: # Event loop and per-connection worker thread state self._loop = asyncio.get_event_loop() self._queue: SimpleQueue[tuple[asyncio.Future, Callable[[], Any]]] = SimpleQueue() self._worker = Worker(self._queue, self._loop) self._connector = connector # Underlying blocking connection created in worker thread self._conn: Optional[BlockingConnection] = None self._closed: bool = False # Schedule connection creation as the very first job in the worker self._open_future: asyncio.Future[Connection] = self._loop.create_future() def _open() -> Connection: # Create the blocking connection inside the worker thread once if self._conn is None: self._conn = self._connector() return self self._queue.put_nowait((self._open_future, _open)) self._worker.start() # Cached properties mirrored to the underlying connection. # Setters will enqueue mutation jobs; we keep local cache for getters. self._isolation_level_cache: Optional[str] = None self._row_factory_cache: Any = None self._text_factory_cache: Any = None self._autocommit_cache: object | bool | None = None async def close(self) -> None: if self._closed: return # Ensure underlying Connection.close() is called in worker thread def _do_close() -> None: if self._conn is not None: self._conn.close() await self._run(lambda: (_do_close(), None)[1]) # schedule and await completion of close # Request worker stop; we must not block the event loop while waiting. # Note: STOP_RUNNING_SENTINEL item will terminate the worker loop. stop_future = self._loop.create_future() self._queue.put_nowait((stop_future, lambda: STOP_RUNNING_SENTINEL)) # Wait for the worker thread to terminate without blocking the loop await self._loop.run_in_executor(None, self._worker.join) self._closed = True def __await__(self): async def _await_open() -> "Connection": await self._open_future return self return _await_open().__await__() async def __aenter__(self) -> "Connection": await self return self async def __aexit__(self, exc_type, exc, tb) -> None: # Just close the connection - do not add any extra logic await self.close() # Internal helper: schedule a callable to run in the worker thread and await its result. async def _run(self, func: Callable[[], Any]) -> Any: if self._closed: raise ProgrammingError("Cannot operate on a closed connection") fut = self._loop.create_future() self._queue.put_nowait((fut, func)) return await fut # Internal helper: enqueue a callable but do not await completion (used for property setters). def _run_nowait(self, func: Callable[[], Any]) -> None: if self._closed: raise ProgrammingError("Cannot operate on a closed connection") fut = self._loop.create_future() self._queue.put_nowait((fut, func)) # Cursor factory returning async Cursor wrapper def cursor(self, factory: Optional[Callable[[BlockingConnection], BlockingCursor]] = None) -> "Cursor": # Creation of the underlying blocking cursor is enqueued to preserve thread affinity. return Cursor(self, factory=factory) # Helpers similar to aiosqlite async def execute(self, sql: str, parameters: Sequence[Any] | Mapping[str, Any] = ()) -> "Cursor": cur = self.cursor() await cur.execute(sql, parameters) return cur async def executemany(self, sql: str, parameters: Iterable[Sequence[Any] | Mapping[str, Any]]) -> "Cursor": cur = self.cursor() await cur.executemany(sql, parameters) return cur async def executescript(self, sql_script: str) -> "Cursor": cur = self.cursor() await cur.executescript(sql_script) return cur async def commit(self) -> None: await self._run(lambda: self._conn.commit()) # type: ignore[union-attr] async def rollback(self) -> None: await self._run(lambda: self._conn.rollback()) # type: ignore[union-attr] # Read/write properties mirrored to the underlying blocking connection. # DB-API hints: # - isolation_level controls implicit transactions (BEGIN DEFERRED/IMMEDIATE/EXCLUSIVE or None for autocommit). @property def isolation_level(self) -> Optional[str]: return self._isolation_level_cache @isolation_level.setter def isolation_level(self, value: Optional[str]) -> None: self._isolation_level_cache = value def _set() -> None: # Will be applied in the worker thread before subsequent operations self._conn.isolation_level = value # type: ignore[union-attr] self._run_nowait(_set) @property def row_factory(self) -> Any: return self._row_factory_cache @row_factory.setter def row_factory(self, value: Any) -> None: self._row_factory_cache = value def _set() -> None: self._conn.row_factory = value # type: ignore[union-attr] self._run_nowait(_set) @property def text_factory(self) -> Any: return self._text_factory_cache @text_factory.setter def text_factory(self, value: Any) -> None: self._text_factory_cache = value def _set() -> None: self._conn.text_factory = value # type: ignore[union-attr] self._run_nowait(_set) @property def autocommit(self) -> object | bool | None: return self._autocommit_cache @autocommit.setter def autocommit(self, value: object | bool) -> None: self._autocommit_cache = value def _set() -> None: self._conn.autocommit = value # type: ignore[union-attr] self._run_nowait(_set) # Cursor goes SECOND class Cursor: def __init__( self, connection: Connection, factory: Optional[Callable[[BlockingConnection], BlockingCursor]] = None ): self._connection: Connection = connection self._loop = asyncio.get_event_loop() # Underlying blocking cursor and its creation job self._cursor_created: bool = False self._bcursor: Optional[BlockingCursor] = None # Cursor attributes (DB-API) self.arraysize: int = 1 self._description: Optional[tuple[tuple[str, None, None, None, None, None, None], ...]] = None self._lastrowid: Optional[int] = None self._rowcount: int = -1 self._closed: bool = False # Enqueue creation of the underlying blocking cursor in the worker thread def _create() -> None: if self._cursor_created: return if factory is None: self._bcursor = self._connection._conn.cursor() # type: ignore[union-attr] else: # Use provided factory to create BlockingCursor from BlockingConnection self._bcursor = factory(self._connection._conn) # type: ignore[union-attr] self._cursor_created = True # Apply initial arraysize if any self._bcursor.arraysize = self.arraysize # type: ignore[union-attr] self._connection._run_nowait(_create) @property def connection(self) -> Connection: return self._connection async def close(self) -> None: if self._closed: return def _close() -> None: if self._bcursor is not None: self._bcursor.close() await self._connection._run(_close) self._closed = True # Internal helpers for updating cached metadata after execute-like calls def _update_meta_cache(self, description, lastrowid, rowcount) -> None: self._description = description self._lastrowid = lastrowid self._rowcount = rowcount if rowcount is not None else -1 async def execute(self, sql: str, parameters: Sequence[Any] | Mapping[str, Any] = ()) -> "Cursor": self._ensure_open() def _exec() -> tuple[Any, Any, Any]: # Perform the execute and collect metadata cur = self._bcursor # type: ignore[assignment] cur.execute(sql, parameters) return (cur.description, cur.lastrowid, cur.rowcount) description, lastrowid, rowcount = await self._connection._run(_exec) self._update_meta_cache(description, lastrowid, rowcount) return self async def executemany(self, sql: str, parameters: Iterable[Sequence[Any] | Mapping[str, Any]]) -> "Cursor": self._ensure_open() def _execm() -> tuple[Any, Any, Any]: cur = self._bcursor # type: ignore[assignment] cur.executemany(sql, parameters) return (cur.description, cur.lastrowid, cur.rowcount) description, lastrowid, rowcount = await self._connection._run(_execm) self._update_meta_cache(description, lastrowid, rowcount) return self async def executescript(self, sql_script: str) -> "Cursor": self._ensure_open() def _execs() -> tuple[Any, Any, Any]: cur = self._bcursor # type: ignore[assignment] cur.executescript(sql_script) return (cur.description, cur.lastrowid, cur.rowcount) description, lastrowid, rowcount = await self._connection._run(_execs) self._update_meta_cache(description, lastrowid, rowcount) return self async def fetchone(self) -> Any: self._ensure_open() def _one() -> Any: return self._bcursor.fetchone() # type: ignore[union-attr] return await self._connection._run(_one) async def fetchmany(self, size: Optional[int] = None) -> list[Any]: self._ensure_open() def _many() -> list[Any]: n = self.arraysize if size is None else size return list(self._bcursor.fetchmany(n)) # type: ignore[union-attr] return await self._connection._run(_many) async def fetchall(self) -> list[Any]: self._ensure_open() def _all() -> list[Any]: return list(self._bcursor.fetchall()) # type: ignore[union-attr] return await self._connection._run(_all) def _ensure_open(self) -> None: if self._closed: raise ProgrammingError("Cannot operate on a closed cursor") # Properties reflecting DB-API attributes of the last executed statement @property def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...] | None: return self._description @property def lastrowid(self) -> int | None: return self._lastrowid @property def rowcount(self) -> int: return self._rowcount # Make cursor usable as async context manager, similar to aiosqlite async def __aenter__(self) -> "Cursor": return self async def __aexit__(self, exc_type, exc, tb) -> None: await self.close() # connect is not async because it returns awaitable Connection # same signature as in the lib.py def connect( database: str, *, experimental_features: Optional[str] = None, isolation_level: Optional[str] = "DEFERRED", extra_io: Optional[Callable[[], None]] = None, ) -> Connection: # Create a connector that opens a blocking Connection using the existing driver. def _connector() -> BlockingConnection: conn = blocking_connect( database, experimental_features=experimental_features, isolation_level=isolation_level, extra_io=extra_io, ) return conn return Connection(_connector) ================================================ FILE: bindings/python/turso/lib_sync.py ================================================ from __future__ import annotations import os import urllib.error # for HTTP IO import urllib.request from dataclasses import dataclass from typing import Any, Callable, Iterable, Optional, Tuple, Union from ._turso import ( Misuse, PyTursoAsyncOperation, PyTursoAsyncOperationResultKind, PyTursoConnection, PyTursoDatabaseConfig, PyTursoPartialSyncOpts, PyTursoSyncDatabase, PyTursoSyncDatabaseConfig, PyTursoSyncDatabaseStats, PyTursoSyncIoItem, PyTursoSyncIoItemRequestKind, py_turso_sync_new, ) from ._turso import ( PyRemoteEncryptionCipher as RemoteEncryptionCipher, ) from .lib import Connection as _Connection from .lib import _map_turso_exception # Constants _HTTP_CHUNK_SIZE = 64 * 1024 # 64 KiB @dataclass class PartialSyncPrefixBootstrap: # Bootstraps DB by fetching first N bytes/pages; enables partial sync length: int @dataclass class PartialSyncQueryBootstrap: # Bootstraps DB by fetching pages touched by given SQL query on server query: str @dataclass class PartialSyncOpts: bootstrap_strategy: Union[PartialSyncPrefixBootstrap, PartialSyncQueryBootstrap] segment_size: Optional[int] = None prefetch: Optional[bool] = None class _HttpContext: """ Resolved network/auth configuration used by sync engine IO handler. remote_url and auth_token can be static strings or callables (evaluated per request). """ def __init__( self, remote_url: Optional[Union[str, Callable[[], Optional[str]]]], auth_token: Optional[Union[str, Callable[[], Optional[str]]]], client_name: str, ) -> None: self.remote_url = remote_url self.auth_token = auth_token self.client_name = client_name def _eval(self, v: Optional[Union[str, Callable[[], Optional[str]]]]) -> Optional[str]: if callable(v): return v() return v def base_url(self) -> Optional[str]: return self._eval(self.remote_url) def token(self) -> Optional[str]: if self.auth_token is None: return None return self._eval(self.auth_token) def _join_url(base: str, path: str) -> str: if not base: return path if base.endswith("/") and path.startswith("/"): return base[:-1] + path if not base.endswith("/") and not path.startswith("/"): return base + "/" + path return base + path def _headers_iter_to_pairs(headers: Iterable[Tuple[str, str]]) -> list[tuple[str, str]]: pairs: list[tuple[str, str]] = [] for h in headers: try: k, v = h except Exception: # best-effort skip invalid headers continue pairs.append((str(k), str(v))) return pairs # ruff: noqa: C901 def _process_http_item( sync: PyTursoSyncDatabase, io_item: PyTursoSyncIoItem, req_kind: Any, ctx: _HttpContext, current_op: Optional[PyTursoAsyncOperation], ) -> None: """ Execute HTTP request, stream response to sync io completion. """ # Access request fields method = req_kind.method path = req_kind.path body: Optional[bytes] = None if req_kind.body is not None: # req_kind.body is PyBytes -> bytes body = bytes(req_kind.body) headers_list = [] if req_kind.headers is not None: headers_list = _headers_iter_to_pairs(req_kind.headers) # list[(k,v)] try: base_url = ctx.base_url() except Exception as e: io_item.poison(f"remote url unavailable: {e}") return # Build full URL url = base_url if base_url else req_kind.url if not url: io_item.poison("remote url unavailable") raise RuntimeError("remote_url is not available") url = _join_url(url, path) # Build request request = urllib.request.Request(url=url, data=body, method=method) # Add provided headers seen_auth = False for k, v in headers_list: request.add_header(k, v) if k.lower() == "authorization": seen_auth = True # Add Authorization if not present and token provided token = None try: token = ctx.token() except Exception: # token resolver failure -> bubble up as IO error io_item.poison("auth token resolver failed") return if token is None and not seen_auth: # No token provided; some endpoints can be public; proceed without it. pass elif token is not None and not seen_auth: request.add_header("Authorization", f"Bearer {token}") # Add a clear user-agent to help server logs if "User-Agent" not in request.headers: request.add_header("User-Agent", f"{ctx.client_name}") # Perform request try: with urllib.request.urlopen(request) as resp: status = getattr(resp, "status", None) if status is None: try: status = resp.getcode() except Exception: status = 200 io_item.status(int(status)) # Stream response in chunks while True: chunk = resp.read(_HTTP_CHUNK_SIZE) if not chunk: break io_item.push_buffer(chunk) if current_op is not None: # The operation should still be waiting for IO r = current_op.resume() # Per contract, while streaming response operation must not finish # We don't raise if it did, but assert in debug builds try: assert r is None except Exception: # continue anyway pass io_item.done() except urllib.error.HTTPError as e: # HTTPError has a response body we may stream to completion status = getattr(e, "code", 500) io_item.status(int(status)) try: # e.read() may not be available in all Python versions; use e.fp if present stream = e # Attempt to read the error body and forward it while True: chunk = stream.read(_HTTP_CHUNK_SIZE) if not chunk: break io_item.push_buffer(chunk) if current_op is not None: r = current_op.resume() try: assert r is None except Exception: pass except Exception: # ignore body read failures pass finally: io_item.done() except urllib.error.URLError as e: io_item.poison(f"network error: {e.reason}") except Exception as e: io_item.poison(f"http error: {e}") def _process_full_read_item(io_item: PyTursoSyncIoItem, req_kind: Any) -> None: """ Fulfill full file read request by streaming file content if exists. On not found - send empty response (not error). """ path = req_kind.path try: with open(path, "rb") as f: while True: chunk = f.read(_HTTP_CHUNK_SIZE) if not chunk: break io_item.push_buffer(chunk) io_item.done() except FileNotFoundError: # On not found engine expects empty response, not error io_item.done() except Exception as e: io_item.poison(f"fs read error: {e}") def _process_full_write_item(io_item: PyTursoSyncIoItem, req_kind: Any) -> None: """ Fulfill full file write request by writing provided content atomically. """ path = req_kind.path content: bytes = bytes(req_kind.content) if req_kind.content is not None else b"" # Ensure parent directory exists try: parent = os.path.dirname(path) if parent and not os.path.exists(parent): os.makedirs(parent, exist_ok=True) except Exception: # ignore directory creation errors, attempt to write anyway pass try: with open(path, "wb") as f: # Write in chunks if content is large view = memoryview(content) offset = 0 length = len(view) while offset < length: end = min(offset + _HTTP_CHUNK_SIZE, length) f.write(view[offset:end]) offset = end io_item.done() except Exception as e: io_item.poison(f"fs write error: {e}") def _drain_sync_io( sync: PyTursoSyncDatabase, ctx: _HttpContext, *, current_op: Optional[PyTursoAsyncOperation] = None, ) -> None: """ Drain all pending IO items from sync engine queue and process them. """ while True: item = sync.take_io_item() try: # tricky: we must do step_io_callbacks even if there is no IO in the queue if item is None: break req = item.request() if req.kind == PyTursoSyncIoItemRequestKind.Http and req.http is not None: _process_http_item(sync, item, req.http, ctx, current_op) elif req.kind == PyTursoSyncIoItemRequestKind.FullRead and req.full_read is not None: _process_full_read_item(item, req.full_read) elif req.kind == PyTursoSyncIoItemRequestKind.FullWrite and req.full_write is not None: _process_full_write_item(item, req.full_write) else: item.poison("unknown io request kind") except Exception as e: # Safety net: poison unexpected failures try: item.poison(f"io processing error: {e}") except Exception: pass finally: # Allow engine to run any post-io callbacks sync.step_io_callbacks() def _run_op( sync: PyTursoSyncDatabase, op: PyTursoAsyncOperation, ctx: _HttpContext, ) -> Any: """ Drive async operation to completion, servicing sync engine IO in between. Returns operation result payload depending on kind: - No: returns None - Connection: returns PyTursoConnection - Changes: returns PyTursoSyncDatabaseChanges - Stats: returns PyTursoSyncDatabaseStats """ while True: try: finished = op.resume() except Exception as exc: # noqa: BLE001 raise _map_turso_exception(exc) if not finished: # Needs IO _drain_sync_io(sync, ctx, current_op=op) continue # Finished res = op.take_result() if res.kind == PyTursoAsyncOperationResultKind.No: return None if res.kind == PyTursoAsyncOperationResultKind.Connection and res.connection is not None: return res.connection if res.kind == PyTursoAsyncOperationResultKind.Changes and res.changes is not None: return res.changes if res.kind == PyTursoAsyncOperationResultKind.Stats and res.stats is not None: return res.stats # Unexpected; return None return None class ConnectionSync(_Connection): """ Synchronized connection that extends regular embedded driver with push/pull and remote bootstrap capabilities. """ def __init__( self, conn: PyTursoConnection, *, sync: PyTursoSyncDatabase, http_ctx: _HttpContext, isolation_level: Optional[str] = "DEFERRED", ) -> None: # Provide extra_io hook so statements can make progress with sync engine (partial sync) def _extra_io() -> None: _drain_sync_io(sync, http_ctx, current_op=None) super().__init__(conn, isolation_level=isolation_level, extra_io=_extra_io) self._sync: PyTursoSyncDatabase = sync self._http_ctx: _HttpContext = http_ctx def pull(self) -> bool: """ Pull remote changes and apply locally. Returns True if new updates were pulled; False otherwise. """ # Wait for changes changes = _run_op(self._sync, self._sync.wait_changes(), self._http_ctx) # determine if empty before applying if changes is None: # Should not happen; treat as no changes return False is_empty = bool(changes.empty()) if is_empty: return False # Apply non-empty changes op = self._sync.apply_changes(changes) _run_op(self._sync, op, self._http_ctx) return True def push(self) -> None: """ Push local changes to remote. """ _run_op(self._sync, self._sync.push_changes(), self._http_ctx) def checkpoint(self) -> None: """ Checkpoint the WAL of the synced database. """ _run_op(self._sync, self._sync.checkpoint(), self._http_ctx) def stats(self) -> PyTursoSyncDatabaseStats: """ Collect stats about the synced database. """ stats = _run_op(self._sync, self._sync.stats(), self._http_ctx) return stats def connect_sync( path: str, remote_url: Optional[Union[str, Callable[[], Optional[str]]]] = None, *, auth_token: Optional[Union[str, Callable[[], Optional[str]]]] = None, client_name: Optional[str] = None, long_poll_timeout_ms: Optional[int] = None, bootstrap_if_empty: bool = True, partial_sync_experimental: Optional[PartialSyncOpts] = None, experimental_features: Optional[str] = None, isolation_level: Optional[str] = "DEFERRED", remote_encryption_key: Optional[str] = None, remote_encryption_cipher: Optional[RemoteEncryptionCipher] = None, ) -> ConnectionSync: """ Create and open a synchronized database connection. - path: path to the main database file locally - remote_url: remote url for the sync - can be lambda evaluated on every http request - auth_token: optional token or lambda returning token, used as Authorization: Bearer - client_name: optional unique client name (defaults to 'turso-sync-py') - long_poll_timeout_ms: timeout for long polling during pull - bootstrap_if_empty: if True and db empty, bootstrap from remote during create() - partial_sync_experimental: EXPERIMENTAL partial sync configuration - experimental_features, isolation_level: passed to underlying connection - remote_encryption_key: base64-encoded encryption key for encrypted Turso Cloud databases - remote_encryption_cipher: encryption cipher for the remote database (used to calculate reserved_bytes) """ # Resolve client name cname = client_name or "turso-sync-py" if remote_url and isinstance(remote_url, str) and remote_url.startswith("libsql://"): remote_url = remote_url.replace("libsql://", "https://", 1) http_ctx = _HttpContext(remote_url=remote_url, auth_token=auth_token, client_name=cname) # Database config: async_io must be True to let Python drive IO db_cfg = PyTursoDatabaseConfig( path=path, experimental_features=experimental_features, ) # Sync config with optional partial bootstrap strategy prefix_len: Optional[int] = None query_str: Optional[str] = None if partial_sync_experimental is not None and isinstance( partial_sync_experimental.bootstrap_strategy, PartialSyncPrefixBootstrap ): prefix_len = int(partial_sync_experimental.bootstrap_strategy.length) elif partial_sync_experimental is not None and isinstance( partial_sync_experimental.bootstrap_strategy, PartialSyncQueryBootstrap ): query_str = str(partial_sync_experimental.bootstrap_strategy.query) sync_cfg = PyTursoSyncDatabaseConfig( path=path, remote_url=remote_url, client_name=cname, long_poll_timeout_ms=long_poll_timeout_ms, bootstrap_if_empty=bootstrap_if_empty, reserved_bytes=None, partial_sync=PyTursoPartialSyncOpts( bootstrap_strategy_prefix=prefix_len, bootstrap_strategy_query=query_str, segment_size=partial_sync_experimental.segment_size, prefetch=partial_sync_experimental.prefetch, ) if partial_sync_experimental is not None else None, remote_encryption_key=remote_encryption_key, remote_encryption_cipher=remote_encryption_cipher, ) # Create sync database holder sync_db: PyTursoSyncDatabase = py_turso_sync_new(db_cfg, sync_cfg) # Prepare + open the database with create() _run_op(sync_db, sync_db.create(), http_ctx) # Connect to obtain PyTursoConnection conn_obj = _run_op(sync_db, sync_db.connect(), http_ctx) if not isinstance(conn_obj, PyTursoConnection): raise Misuse("sync connect did not return a connection") # Wrap into ConnectionSync that integrates sync IO into DB operations return ConnectionSync(conn_obj, sync=sync_db, http_ctx=http_ctx, isolation_level=isolation_level) ================================================ FILE: bindings/python/turso/lib_sync_aio.py ================================================ from __future__ import annotations from typing import Callable, Optional, Union, cast from .lib_aio import ( Connection as NonBlockingConnection, ) from .lib_sync import ( ConnectionSync as BlockingConnectionSync, ) from .lib_sync import ( PartialSyncOpts, PyTursoSyncDatabaseStats, ) from .lib_sync import ( connect_sync as blocking_connect_sync, ) class ConnectionSync(NonBlockingConnection): def __init__(self, connector: Callable[[], BlockingConnectionSync]) -> None: # Use the non-blocking driver base - runs a background worker thread # that owns the underlying blocking connection instance. super().__init__(connector) async def close(self) -> None: # Ensure worker is shut down and underlying blocking connection closed await super().close() # Make ConnectionSync instance awaitable with correct return typing def __await__(self): async def _await_open() -> "ConnectionSync": await self._open_future return self # the underlying connection is created at this point return _await_open().__await__() async def __aenter__(self) -> "ConnectionSync": await self return self async def __aexit__(self, exc_type, exc, tb) -> None: await self.close() # Synchronization API (async wrappers scheduling work on the worker thread) async def pull(self) -> bool: # Pull remote changes and apply locally; returns True if any updates were fetched return await self._run(lambda: cast(BlockingConnectionSync, self._conn).pull()) # type: ignore[union-attr] async def push(self) -> None: # Push local changes to the remote await self._run(lambda: cast(BlockingConnectionSync, self._conn).push()) # type: ignore[union-attr] async def checkpoint(self) -> None: # Checkpoint the WAL of the synced database await self._run(lambda: cast(BlockingConnectionSync, self._conn).checkpoint()) # type: ignore[union-attr] async def stats(self) -> PyTursoSyncDatabaseStats: # Collect stats about the synced database return await self._run(lambda: cast(BlockingConnectionSync, self._conn).stats()) # type: ignore[union-attr] # connect is not async because it returns awaitable ConnectionSync # Same signature as in the lib_sync.connect_sync def connect_sync( path: str, remote_url: Union[str, Callable[[], Optional[str]]], *, auth_token: Optional[Union[str, Callable[[], Optional[str]]]] = None, client_name: Optional[str] = None, long_poll_timeout_ms: Optional[int] = None, bootstrap_if_empty: bool = True, partial_sync_experimental: Optional[PartialSyncOpts] = None, experimental_features: Optional[str] = None, isolation_level: Optional[str] = "DEFERRED", ) -> ConnectionSync: # Connector creating the blocking synchronized connection in the worker thread def _connector() -> BlockingConnectionSync: return blocking_connect_sync( path, remote_url, auth_token=auth_token, client_name=client_name, long_poll_timeout_ms=long_poll_timeout_ms, bootstrap_if_empty=bootstrap_if_empty, partial_sync_experimental=partial_sync_experimental, experimental_features=experimental_features, isolation_level=isolation_level, ) # Return awaitable async wrapper with sync extras return ConnectionSync(_connector) ================================================ FILE: bindings/python/turso/py.typed ================================================ ================================================ FILE: bindings/python/turso/sqlalchemy/__init__.py ================================================ """SQLAlchemy dialect for pyturso. This module provides SQLAlchemy integration for pyturso: - TursoDialect: Basic local database connections (sqlite+turso://) - TursoSyncDialect: Sync-enabled connections with remote support (sqlite+turso_sync://) - get_sync_connection: Helper to access sync methods from SQLAlchemy connections Usage: from sqlalchemy import create_engine, text # Basic local connection engine = create_engine("sqlite+turso:///app.db") # Sync-enabled connection with remote engine = create_engine( "sqlite+turso_sync:///local.db" "?remote_url=https://my-db.turso.io" "&auth_token=your-token" ) # Access sync operations from turso.sqlalchemy import get_sync_connection with engine.connect() as conn: sync = get_sync_connection(conn) sync.pull() # Pull remote changes result = conn.execute(text("SELECT * FROM users")) conn.commit() sync.push() # Push local changes """ from .dialect import TursoDialect, TursoSyncDialect, get_sync_connection __all__ = [ "TursoDialect", "TursoSyncDialect", "get_sync_connection", ] ================================================ FILE: bindings/python/turso/sqlalchemy/dialect.py ================================================ """SQLAlchemy dialects for pyturso. This module provides two SQLAlchemy dialects: - TursoDialect: Basic local database connections (sqlite+turso://) - TursoSyncDialect: Sync-enabled connections with remote support (sqlite+turso_sync://) """ from __future__ import annotations import logging from typing import TYPE_CHECKING, Any, Dict, List from sqlalchemy import pool from sqlalchemy.dialects.sqlite.pysqlite import SQLiteDialect_pysqlite from sqlalchemy.engine import URL from sqlalchemy.engine.reflection import ObjectKind if TYPE_CHECKING: from sqlalchemy.engine.interfaces import ConnectArgsType, ReflectedForeignKeyConstraint, ReflectedIndex from sqlalchemy.pool import Pool logger = logging.getLogger(__name__) class _TursoDialectMixin: """ Mixin providing Turso-specific overrides for SQLAlchemy dialect. Turso doesn't support all SQLite PRAGMAs. This mixin overrides methods that would otherwise fail due to unsupported PRAGMAs: - PRAGMA foreign_key_list (not supported) - PRAGMA index_list (not supported) """ def get_foreign_keys( self, connection, table_name, schema=None, **kw, ) -> List[ReflectedForeignKeyConstraint]: """ Return foreign keys for a table. Turso doesn't support PRAGMA foreign_key_list, so we return an empty list. Foreign key constraints are still enforced at write time if defined. """ logger.debug( "PRAGMA foreign_key_list not supported; foreign key reflection unavailable for table '%s'", table_name, ) return [] def get_indexes( self, connection, table_name, schema=None, **kw, ) -> List[ReflectedIndex]: """ Return indexes for a table. Turso doesn't support PRAGMA index_list, so we return an empty list. Indexes still exist and are used for query optimization. """ logger.debug( "PRAGMA index_list not supported; index reflection unavailable for table '%s'", table_name, ) return [] def get_unique_constraints( self, connection, table_name, schema=None, **kw, ) -> List[Dict[str, Any]]: """ Return unique constraints for a table. This also relies on PRAGMA index_list which Turso doesn't support. """ logger.debug( "PRAGMA index_list not supported; unique constraint reflection unavailable for table '%s'", table_name, ) return [] def get_check_constraints( self, connection, table_name, schema=None, **kw, ) -> List[Dict[str, Any]]: """ Return check constraints for a table. SQLite stores these in sqlite_master which Turso may not fully support. """ logger.debug( "check constraint reflection not supported for table '%s'", table_name, ) return [] def get_multi_indexes( self, connection, schema=None, filter_names=None, kind=ObjectKind.TABLE, scope=None, **kw, ) -> Dict[Any, List[ReflectedIndex]]: """Return indexes for multiple tables.""" logger.debug("PRAGMA index_list not supported; multi-index reflection unavailable") return {} def get_multi_unique_constraints( self, connection, schema=None, filter_names=None, kind=ObjectKind.TABLE, scope=None, **kw, ) -> Dict[Any, List[Dict[str, Any]]]: """Return unique constraints for multiple tables.""" logger.debug("PRAGMA index_list not supported; multi-unique-constraint reflection unavailable") return {} def get_multi_foreign_keys( self, connection, schema=None, filter_names=None, kind=ObjectKind.TABLE, scope=None, **kw, ) -> Dict[Any, List[ReflectedForeignKeyConstraint]]: """Return foreign keys for multiple tables.""" logger.debug("PRAGMA foreign_key_list not supported; multi-foreign-key reflection unavailable") return {} def get_multi_check_constraints( self, connection, schema=None, filter_names=None, kind=ObjectKind.TABLE, scope=None, **kw, ) -> Dict[Any, List[Dict[str, Any]]]: """Return check constraints for multiple tables.""" logger.debug("multi-check-constraint reflection not supported") return {} def get_temp_table_names(self, connection, **kw) -> List[str]: """Return temporary table names. Turso doesn't support sqlite_temp_master, so we return an empty list. """ logger.debug("sqlite_temp_master not supported; temp table reflection unavailable") return [] def get_temp_view_names(self, connection, **kw) -> List[str]: """Return temporary view names. Turso doesn't support sqlite_temp_master, so we return an empty list. """ logger.debug("sqlite_temp_master not supported; temp view reflection unavailable") return [] class TursoDialect(_TursoDialectMixin, SQLiteDialect_pysqlite): """ SQLAlchemy dialect for pyturso local database connections. This dialect uses turso.connect() for local SQLite-compatible databases. Usage: from sqlalchemy import create_engine # File-based database engine = create_engine("sqlite+turso:///path/to/database.db") # In-memory database engine = create_engine("sqlite+turso:///:memory:") # With options engine = create_engine( "sqlite+turso:///db.db", connect_args={"isolation_level": "IMMEDIATE"} ) """ name = "sqlite" driver = "turso" # Enable statement caching for better performance supports_statement_cache = True # Disable native_datetime since turso handles datetime differently supports_native_datetime = False @classmethod def import_dbapi(cls): """Import the turso module as DBAPI.""" import turso return turso def on_connect(self): """ Return a callable to run on each new connection. We override this to skip the REGEXP function setup that pysqlite does, since turso doesn't support create_function. """ # Skip the parent's on_connect which tries to register REGEXP # Return None to indicate no special connection setup needed return None def get_isolation_level(self, dbapi_connection): """ Return the current isolation level. Turso doesn't support PRAGMA read_uncommitted, so we return SERIALIZABLE as the default (which is what SQLite uses). """ return "SERIALIZABLE" def set_isolation_level(self, dbapi_connection, level): """ Set the isolation level. Turso handles isolation through the isolation_level connection parameter, not through PRAGMA statements. This is a no-op since the isolation level is set at connection time. """ # No-op: turso handles isolation via connection parameter pass def create_connect_args(self, url: URL) -> ConnectArgsType: """ Create connection arguments from SQLAlchemy URL. The URL format is: sqlite+turso:///path/to/database.db Query parameters supported: - isolation_level: Transaction isolation level - experimental_features: Comma-separated feature flags """ opts = url.translate_connect_args() # 'database' key becomes the positional argument database = opts.pop("database", ":memory:") # Remove unsupported URL components opts.pop("username", None) opts.pop("password", None) opts.pop("host", None) opts.pop("port", None) # Extract query parameters query_params = dict(url.query) kwargs: Dict[str, Any] = {} # Handle isolation_level isolation_level = query_params.pop("isolation_level", None) if isolation_level: if isolation_level.upper() == "AUTOCOMMIT": kwargs["isolation_level"] = None else: kwargs["isolation_level"] = isolation_level # Handle experimental_features experimental_features = query_params.pop("experimental_features", None) if experimental_features: kwargs["experimental_features"] = experimental_features return ([database], kwargs) def get_pool_class(self, url: URL) -> type[Pool]: """Return the connection pool class.""" if url.database == ":memory:": return pool.SingletonThreadPool return pool.QueuePool class TursoSyncDialect(_TursoDialectMixin, SQLiteDialect_pysqlite): """ SQLAlchemy dialect for pyturso sync-enabled connections. This dialect uses turso.sync.connect() which provides: - Local SQLite database with remote sync capabilities - pull() - Pull changes from remote - push() - Push changes to remote - checkpoint() - Checkpoint the WAL - stats() - Get sync statistics Usage: from sqlalchemy import create_engine engine = create_engine( "sqlite+turso_sync:///local.db" "?remote_url=https://your-db.turso.io" "&auth_token=your-token" ) # Or with connect_args: engine = create_engine( "sqlite+turso_sync:///local.db", connect_args={ "remote_url": "https://your-db.turso.io", "auth_token": "your-token", } ) # Access sync operations: from turso.sqlalchemy import get_sync_connection with engine.connect() as conn: sync = get_sync_connection(conn) sync.pull() # Pull remote changes # ... execute queries ... sync.push() # Push local changes """ name = "sqlite" driver = "turso_sync" # Enable statement caching for better performance supports_statement_cache = True # Disable native_datetime since turso handles datetime differently supports_native_datetime = False @classmethod def import_dbapi(cls): """Import the turso.sync module as DBAPI.""" import turso.sync return turso.sync def connect(self, *cargs, **cparams): """Remap sync_url to remote_url for libsql-sqlalchemy compatibility.""" if "sync_url" in cparams and "remote_url" not in cparams: cparams["remote_url"] = cparams.pop("sync_url") return super().connect(*cargs, **cparams) def on_connect(self): """ Return a callable to run on each new connection. We override this to skip the REGEXP function setup that pysqlite does, since turso doesn't support create_function. """ return None def get_isolation_level(self, dbapi_connection): """ Return the current isolation level. Turso doesn't support PRAGMA read_uncommitted, so we return SERIALIZABLE as the default. """ return "SERIALIZABLE" def set_isolation_level(self, dbapi_connection, level): """ Set the isolation level. Turso handles isolation through the isolation_level connection parameter. This is a no-op since the isolation level is set at connection time. """ pass @staticmethod def _validate_sync_url(opts: Dict[str, Any]) -> None: """Reject URL components that TursoSyncDialect doesn't support.""" if opts.get("username") or opts.get("password"): raise ValueError( "TursoSyncDialect does not support username/password in URL. " "Use auth_token query parameter or connect_args instead." ) if opts.get("host") or opts.get("port"): raise ValueError( "TursoSyncDialect does not support host/port in URL. " "The local database path goes after ':///', and remote_url " "is specified as a query parameter." ) @staticmethod def _extract_sync_params(query_params: Dict[str, str]) -> Dict[str, Any]: """Extract and convert sync-specific query parameters into kwargs.""" kwargs: Dict[str, Any] = {} auth_token = query_params.pop("auth_token", None) if auth_token: kwargs["auth_token"] = auth_token client_name = query_params.pop("client_name", None) kwargs["client_name"] = client_name or "turso-sqlalchemy" long_poll_timeout_ms = query_params.pop("long_poll_timeout_ms", None) if long_poll_timeout_ms: kwargs["long_poll_timeout_ms"] = int(long_poll_timeout_ms) bootstrap_if_empty = query_params.pop("bootstrap_if_empty", None) if bootstrap_if_empty is not None: kwargs["bootstrap_if_empty"] = bootstrap_if_empty.lower() in ( "true", "1", "yes", ) return kwargs def create_connect_args(self, url: URL) -> ConnectArgsType: """ Create connection arguments from SQLAlchemy URL. The URL format is: sqlite+turso_sync:///path/to/local.db?remote_url=...&auth_token=... Query parameters: - remote_url (required): Remote Turso/libsql server URL - auth_token: Authentication token - client_name: Client identifier (default: turso-sqlalchemy) - long_poll_timeout_ms: Long poll timeout in milliseconds - bootstrap_if_empty: Bootstrap from remote if local empty (default: true) - isolation_level: Transaction isolation level - experimental_features: Comma-separated feature flags """ opts = url.translate_connect_args() path = opts.pop("database", ":memory:") self._validate_sync_url(opts) query_params = dict(url.query) # Accept both remote_url and sync_url (libsql-sqlalchemy compat) remote_url = query_params.pop("remote_url", None) or query_params.pop("sync_url", None) kwargs = self._extract_sync_params(query_params) # Handle isolation_level isolation_level = query_params.pop("isolation_level", None) if isolation_level: if isolation_level.upper() == "AUTOCOMMIT": kwargs["isolation_level"] = None else: kwargs["isolation_level"] = isolation_level # Handle experimental_features experimental_features = query_params.pop("experimental_features", None) if experimental_features: kwargs["experimental_features"] = experimental_features # Warn about unused query parameters if query_params: import warnings warnings.warn( f"Unrecognized query parameters ignored: {list(query_params.keys())}", UserWarning, stacklevel=2, ) # Return (args, kwargs) for turso.sync.connect(path, remote_url, **kwargs) if remote_url: return ([path, remote_url], kwargs) else: # If no remote_url provided, let turso.sync.connect raise the error # This allows connect_args to provide remote_url instead return ([path], kwargs) def get_pool_class(self, url: URL) -> type[Pool]: """ Return the connection pool class. For sync connections with file databases, use QueuePool. For :memory: databases, use SingletonThreadPool. """ if url.database == ":memory:": return pool.SingletonThreadPool return pool.QueuePool def get_sync_connection(connection): """ Get the underlying turso.sync.ConnectionSync from a SQLAlchemy connection. This provides access to sync-specific methods: - pull() - Pull changes from remote, returns True if updates were pulled - push() - Push changes to remote - checkpoint() - Checkpoint the WAL - stats() - Get sync statistics Usage: from turso.sqlalchemy import get_sync_connection with engine.connect() as conn: sync = get_sync_connection(conn) # Pull latest changes before querying sync.pull() result = conn.execute(text("SELECT * FROM users")) # After modifications, push to remote conn.execute(text("INSERT INTO users ...")) conn.commit() sync.push() Args: connection: A SQLAlchemy Connection object Returns: The underlying turso.sync.ConnectionSync object Raises: TypeError: If the connection is not a Turso sync connection """ from turso.lib_sync import ConnectionSync # Get the raw DBAPI connection # SQLAlchemy 2.0: connection.connection.dbapi_connection # SQLAlchemy 1.4: connection.connection raw_conn = getattr(connection, "connection", None) if raw_conn is None: raise TypeError("Cannot get raw connection from SQLAlchemy connection") # Handle SQLAlchemy 2.0 pooled connection wrapper dbapi_conn = getattr(raw_conn, "dbapi_connection", raw_conn) if not isinstance(dbapi_conn, ConnectionSync): raise TypeError( f"Expected turso.sync.ConnectionSync, got {type(dbapi_conn).__name__}. " "This function only works with sqlite+turso_sync:// connections." ) return dbapi_conn ================================================ FILE: bindings/python/turso/sync/__init__.py ================================================ from ..lib import ( # Exception classes DatabaseError, DataError, Error, IntegrityError, InterfaceError, InternalError, NotSupportedError, OperationalError, ProgrammingError, Warning, # DB-API 2.0 module-level attributes required by SQLAlchemy apilevel, paramstyle, sqlite_version, sqlite_version_info, threadsafety, ) from ..lib_sync import ( ConnectionSync, PartialSyncOpts, PartialSyncPrefixBootstrap, PartialSyncQueryBootstrap, RemoteEncryptionCipher, ) from ..lib_sync import ( connect_sync as connect, ) __all__ = [ "connect", "ConnectionSync", "PartialSyncOpts", "PartialSyncPrefixBootstrap", "PartialSyncQueryBootstrap", "RemoteEncryptionCipher", # DB-API 2.0 module attributes "apilevel", "paramstyle", "threadsafety", "sqlite_version", "sqlite_version_info", # Exception classes "Warning", "Error", "InterfaceError", "DatabaseError", "DataError", "OperationalError", "IntegrityError", "InternalError", "ProgrammingError", "NotSupportedError", ] ================================================ FILE: bindings/python/turso/worker.py ================================================ import asyncio from queue import SimpleQueue from threading import Thread from typing import Any, Callable STOP_RUNNING_SENTINEL = object() class Worker(Thread): """ Dedicated worker thread executing database operations sequentially. The worker consumes (future, callable) items from the unbounded SimpleQueue. It executes the callable, then sets result or mapped exception on the future using loop.call_soon_threadsafe to synchronize with the event loop thread. If work item return STOP_RUNNING_SENTINEL value - it stops the execution (e.g. this can be used to stop worker when connection is about to close) """ def __init__( self, queue: SimpleQueue[tuple[asyncio.Future, Callable[[], Any]] | None], loop: asyncio.AbstractEventLoop, ) -> None: super().__init__(name="turso-async-worker", daemon=True) self._queue = queue self._loop = loop def run(self) -> None: while True: item = self._queue.get() fut, func = item if fut.cancelled(): # Still consume but skip execution if already cancelled continue try: result = func() if result is STOP_RUNNING_SENTINEL: break except Exception as e: self._loop.call_soon_threadsafe(fut.set_exception, e) else: self._loop.call_soon_threadsafe(fut.set_result, result) ================================================ FILE: bindings/react-native/.gitignore ================================================ # Node node_modules/ lib/ # Built Rust libraries libs/ # iOS ios/Pods/ *.xcworkspace *.xcuserdata # Android android/build/ android/.gradle/ android/.cxx/ *.apk *.aab *.dex *.class # IDE .idea/ .vscode/ *.swp *.swo # OS .DS_Store Thumbs.db # Logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* !CMakeLists.txt ================================================ FILE: bindings/react-native/Makefile ================================================ OS := $(shell uname) CFLAGS := -Iinclude LDFLAGS := -lm ARCHS_IOS = aarch64-apple-ios aarch64-apple-ios-sim ARCHS_ANDROID = aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android IOS_LIB_FILE = libturso_sync_sdk_kit.dylib IOS_FINAL_LIB_FILE = turso-sync-sdk-kit LIB_FILE = libturso_sync_sdk_kit.so HEADERS = turso_sync.h turso.h SDK_KIT_DIR = ../../sdk-kit SYNC_SDK_KIT_DIR = ../../sync/sdk-kit ifeq ($(OS),Darwin) CFLAGS += -framework Security -framework CoreServices endif .PHONY: all $(ARCHS_IOS) ios $(ARCHS_ANDROID) android notify: osascript -e 'display notification "Build completed" with title "turso"' echo "🟢 Build Completed!" lint: cargo clippy --all-features --all-targets -- --no-deps -D warnings fix: cargo clippy --all-targets --all-features --fix --allow-dirty --allow-staged cargo fmt --all nuke: rm -rf ./target ios: clean ios-build package-dylibs notify clean: rm -rf libs/ios package-dylibs: mkdir -p libs/ios rm -rf libs/ios/turso*.xcframework cp -r templates/turso*.xcframework libs/ios cp "$(SDK_KIT_DIR)/turso.h" libs/ios/ cp "$(SYNC_SDK_KIT_DIR)/turso_sync.h" libs/ios/ # Pack device dylibs cp ../../target/aarch64-apple-ios/release/$(IOS_LIB_FILE) libs/ios/$(IOS_FINAL_LIB_FILE) cp libs/ios/$(IOS_FINAL_LIB_FILE) libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64/turso-sync-sdk-kit.framework/ install_name_tool -id @rpath/turso-sync-sdk-kit.framework/$(IOS_FINAL_LIB_FILE) libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64/turso-sync-sdk-kit.framework/$(IOS_FINAL_LIB_FILE) codesign -f -s - --identifier com.turso.turso-sync-sdk-kit libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64/turso-sync-sdk-kit.framework/$(IOS_FINAL_LIB_FILE) mkdir -p libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64/turso-sync-sdk-kit.framework/Headers cp "$(SDK_KIT_DIR)/turso.h" libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64/turso-sync-sdk-kit.framework/Headers/ cp "$(SYNC_SDK_KIT_DIR)/turso_sync.h" libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64/turso-sync-sdk-kit.framework/Headers/ # Pack simulator dylibs cp ../../target/aarch64-apple-ios-sim/release/$(IOS_LIB_FILE) libs/ios/$(IOS_FINAL_LIB_FILE) cp libs/ios/$(IOS_FINAL_LIB_FILE) libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64-simulator/turso-sync-sdk-kit.framework/ install_name_tool -id @rpath/turso-sync-sdk-kit.framework/$(IOS_FINAL_LIB_FILE) libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64-simulator/turso-sync-sdk-kit.framework/$(IOS_FINAL_LIB_FILE) codesign -f -s - --identifier com.turso.turso-sync-sdk-kit libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64-simulator/turso-sync-sdk-kit.framework/$(IOS_FINAL_LIB_FILE) mkdir -p libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64-simulator/turso-sync-sdk-kit.framework/Headers cp "$(SDK_KIT_DIR)/turso.h" libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64-simulator/turso-sync-sdk-kit.framework/Headers/ cp "$(SYNC_SDK_KIT_DIR)/turso_sync.h" libs/ios/turso-sync-sdk-kit.xcframework/ios-arm64-simulator/turso-sync-sdk-kit.framework/Headers/ ios-build: rustup target add aarch64-apple-ios cargo build --release --target aarch64-apple-ios --package turso_sync_sdk_kit rustup target add aarch64-apple-ios-sim cargo build --release --target aarch64-apple-ios-sim --package turso_sync_sdk_kit android: $(ARCHS_ANDROID) rm -rf libs/android mkdir -p libs/android/arm64-v8a mkdir -p libs/android/armeabi-v7a mkdir -p libs/android/x86_64 mkdir -p libs/android/x86 cp ../../target/aarch64-linux-android/release/$(LIB_FILE) libs/android/arm64-v8a/$(LIB_FILE) cp ../../target/armv7-linux-androideabi/release/$(LIB_FILE) libs/android/armeabi-v7a/$(LIB_FILE) cp ../../target/x86_64-linux-android/release/$(LIB_FILE) libs/android/x86_64/$(LIB_FILE) cp ../../target/i686-linux-android/release/$(LIB_FILE) libs/android/x86/$(LIB_FILE) cp "$(SDK_KIT_DIR)/turso.h" libs/android cp "$(SYNC_SDK_KIT_DIR)/turso_sync.h" libs/android $(ARCHS_ANDROID): %: rustup target add $@ cargo ndk --target $@ --platform 31 build --package turso_sync_sdk_kit --release ================================================ FILE: bindings/react-native/README.md ================================================ # Turso Sync React Native SDK React Native bindings for Turso embedded replicas - sync your local SQLite database with Turso cloud. ## Installation ```bash npm install @tursodatabase/sync-react-native ``` ### iOS ```bash cd ios && pod install ``` ### Android Requires `minSdkVersion` 21+ in `android/build.gradle`. ## Quick Start ```typescript import { Database, getDbPath } from '@tursodatabase/sync-react-native'; // Get platform-specific writable path const dbPath = getDbPath('myapp.db'); // Create database with sync const db = new Database({ path: dbPath, url: 'libsql://your-db.turso.io', authToken: 'your-auth-token', }); // Connect (bootstraps from remote if empty) await db.connect(); // Query local replica (fast) const users = await db.all('SELECT * FROM users'); // Make local changes await db.run('INSERT INTO users (name) VALUES (?)', ['Alice']); // Sync with remote await db.push(); // Push local changes await db.pull(); // Pull remote changes // Close when done await db.close(); ``` ## Local-Only Database ```typescript const db = new Database({ path: getDbPath('local.db') }); await db.connect(); await db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)'); await db.run('INSERT INTO users (name) VALUES (?)', ['Bob']); const user = await db.get('SELECT * FROM users WHERE id = ?', [1]); await db.close(); ``` ## Encrypted Remote Database ```typescript const db = new Database({ path: getDbPath('encrypted.db'), url: 'libsql://your-db.turso.io', authToken: 'your-auth-token', remoteEncryption: { cipher: 'aes256gcm', key: 'base64-encoded-key', }, }); ``` ## API ### Database Methods | Method | Description | |--------|-------------| | `connect()` | Open/bootstrap the database | | `exec(sql)` | Execute SQL (no results) | | `run(sql, params?)` | Execute SQL, return `{ changes, lastInsertRowid }` | | `get(sql, params?)` | Query single row | | `all(sql, params?)` | Query all rows | | `prepare(sql)` | Create prepared statement | | `close()` | Close database | ### Sync Methods (when `url` is provided) | Method | Description | |--------|-------------| | `push()` | Push local changes to remote | | `pull()` | Pull remote changes to local | | `sync()` | Push then pull | | `stats()` | Get sync statistics | ### Transactions ```typescript await db.transaction(async () => { await db.run('INSERT INTO users (name) VALUES (?)', ['Alice']); await db.run('INSERT INTO users (name) VALUES (?)', ['Bob']); // Commits on success, rolls back on error }); ``` ## License This project is licensed under the [MIT license](https://github.com/tursodatabase/turso/blob/main/LICENSE.md). ## Links - [Turso Documentation](https://docs.turso.tech) - [GitHub](https://github.com/tursodatabase/turso) - [npm](https://www.npmjs.com/package/@tursodatabase/sync-react-native) ================================================ FILE: bindings/react-native/android/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.22) project(turso-sync-react-native) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Find packages find_package(ReactAndroid REQUIRED CONFIG) find_package(fbjni REQUIRED CONFIG) # Source files set(TURSO_SOURCES cpp-adapter.cpp ../cpp/TursoHostObject.cpp ../cpp/TursoDatabaseHostObject.cpp ../cpp/TursoConnectionHostObject.cpp ../cpp/TursoStatementHostObject.cpp ../cpp/TursoSyncDatabaseHostObject.cpp ../cpp/TursoSyncOperationHostObject.cpp ../cpp/TursoSyncIoItemHostObject.cpp ../cpp/TursoSyncChangesHostObject.cpp ) # Create shared library add_library(${PROJECT_NAME} SHARED ${TURSO_SOURCES}) # 16KB page alignment for Android 15+ compatibility target_link_options(${PROJECT_NAME} PRIVATE "-Wl,-z,max-page-size=16384") # Include directories target_include_directories(${PROJECT_NAME} PRIVATE ../cpp ../libs/android ) # Pre-built Rust shared library (.so) loaded at runtime add_library(turso_sync_sdk_kit SHARED IMPORTED) # IMPORTED_NO_SONAME is important to properly adjust import paths for runtime by linker (so they will not be reused from build time) set_target_properties(turso_sync_sdk_kit PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../libs/android/${ANDROID_ABI}/libturso_sync_sdk_kit.so IMPORTED_NO_SONAME TRUE ) # Link libraries (record runtime dependency on Rust .so) target_link_libraries(${PROJECT_NAME} ReactAndroid::jsi ReactAndroid::reactnative fbjni::fbjni turso_sync_sdk_kit android log ) ================================================ FILE: bindings/react-native/android/build.gradle ================================================ buildscript { repositories { google() mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:8.2.0" } } plugins { id "com.android.library" } def reactNativeArchitectures() { def value = project.getProperties().get("reactNativeArchitectures") return value ? value.split(",") : ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"] } android { namespace "com.turso.sync.reactnative" compileSdk 34 defaultConfig { minSdk 24 targetSdk 34 externalNativeBuild { cmake { cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all" abiFilters(*reactNativeArchitectures()) arguments "-DANDROID_STL=c++_shared" } } } buildFeatures { prefab true buildConfig true } externalNativeBuild { cmake { path "CMakeLists.txt" } } buildTypes { release { minifyEnabled false } } packaging { excludes += [ "**/libjsi.so", "**/libfbjni.so", "**/libc++_shared.so", "**/libreactnative.so" ] } sourceSets { main { jniLibs.srcDirs = ["../libs/android"] } } } repositories { google() mavenCentral() } dependencies { compileOnly "com.facebook.react:react-android" } // Task to build Rust library for Android task buildRustLibrary(type: Exec) { workingDir "${projectDir}/.." commandLine "make", "android" } preBuild.dependsOn buildRustLibrary ================================================ FILE: bindings/react-native/android/cpp-adapter.cpp ================================================ #include "TursoHostObject.h" #include #include #include #include #include namespace jsi = facebook::jsi; namespace react = facebook::react; namespace jni = facebook::jni; // This file is not using raw jni but rather fbjni, do not change how the native // functions are registered // https://github.com/facebookincubator/fbjni/blob/main/docs/quickref.md struct TursoBridge : jni::JavaClass { static constexpr auto kJavaDescriptor = "Lcom/turso/sync/reactnative/TursoBridge;"; static void registerNatives() { javaClassStatic()->registerNatives( {makeNativeMethod("installNativeJsi", TursoBridge::installNativeJsi), makeNativeMethod("clearStateNativeJsi", TursoBridge::clearStateNativeJsi)}); } private: static void installNativeJsi( jni::alias_ref thiz, jlong jsiRuntimePtr, jni::alias_ref jsCallInvokerHolder, jni::alias_ref dbPath) { auto jsiRuntime = reinterpret_cast(jsiRuntimePtr); auto jsCallInvoker = jsCallInvokerHolder->cthis()->getCallInvoker(); std::string dbPathStr = dbPath->toStdString(); turso::install(*jsiRuntime, jsCallInvoker, dbPathStr.c_str()); } static void clearStateNativeJsi(jni::alias_ref thiz) { turso::invalidate(); } }; JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) { return jni::initialize(vm, [] { TursoBridge::registerNatives(); }); } ================================================ FILE: bindings/react-native/android/src/main/AndroidManifest.xml ================================================ ================================================ FILE: bindings/react-native/android/src/main/java/com/turso/sync/reactnative/TursoBridge.java ================================================ package com.turso.sync.reactnative; import com.facebook.react.bridge.ReactContext; import com.facebook.react.turbomodule.core.CallInvokerHolderImpl; import java.lang.Exception; // bridge layer, installNativeJsi/clearStateNativeJsi methods are set through cpp-adapter.cpp public class TursoBridge { private static final TursoBridge instance = new TursoBridge(); public static TursoBridge getInstance() { return instance; } private TursoBridge() { } private native void installNativeJsi( long jsContextNativePointer, CallInvokerHolderImpl jsCallInvokerHolder, String dbPath); private native void clearStateNativeJsi(); public void install(ReactContext context) throws Exception { long jsContextPointer = context.getJavaScriptContextHolder().get(); if (jsContextPointer == 0) { throw new Exception("jsContextPointer == 0"); } CallInvokerHolderImpl jsCallInvokerHolder = (CallInvokerHolderImpl) context.getCatalystInstance() .getJSCallInvokerHolder(); // getDatabasePath(...) returns file path - so we pass dummy value and remove it // after to get directory path String dbPath = context.getDatabasePath("tursoDatabaseFile").getAbsolutePath().replace("tursoDatabaseFile", ""); installNativeJsi(jsContextPointer, jsCallInvokerHolder, dbPath); } public void invalidate() { clearStateNativeJsi(); } } ================================================ FILE: bindings/react-native/android/src/main/java/com/turso/sync/reactnative/TursoModule.java ================================================ package com.turso.sync.reactnative; import java.io.File; import java.lang.Exception; import java.util.HashMap; import java.util.Map; import androidx.annotation.NonNull; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.turbomodule.core.CallInvokerHolderImpl; // The React Native bridge - exposes methods to JavaScript @ReactModule(name = TursoModule.NAME) public class TursoModule extends ReactContextBaseJavaModule { public static final String NAME = "Turso"; static { System.loadLibrary("turso_sync_sdk_kit"); System.loadLibrary("turso-sync-react-native"); } public TursoModule(ReactApplicationContext reactContext) { super(reactContext); } @Override @NonNull public String getName() { return NAME; } @Override public Map getConstants() { final Map constants = new HashMap<>(); ReactApplicationContext context = getReactApplicationContext(); // getDatabasePath(...) returns file path - so we pass dummy value and remove it // after to get directory path String dbPath = context.getDatabasePath("tursoDatabaseFile").getAbsolutePath().replace("tursoDatabaseFile", ""); constants.put("ANDROID_DATABASE_PATH", dbPath); String filesPath = context.getFilesDir().getAbsolutePath(); constants.put("ANDROID_FILES_PATH", filesPath); File externalFilesDir = context.getExternalFilesDir(null); constants.put("ANDROID_EXTERNAL_FILES_PATH", externalFilesDir != null ? externalFilesDir.getAbsolutePath() : null); // populate Android and IOS constants to simplify JS code (e.g. // IOS_DOCUMENT_PATH ?? ANDROID_DATABASE_PATH) constants.put("IOS_DOCUMENT_PATH", null); constants.put("IOS_LIBRARY_PATH", null); return constants; } @ReactMethod(isBlockingSynchronousMethod = true) public boolean install() { try { ReactApplicationContext context = getReactApplicationContext(); // Install native module TursoBridge.getInstance().install(context); return true; } catch (Exception e) { return false; } } @Override public void invalidate() { super.invalidate(); TursoBridge.getInstance().invalidate(); } private native void installNativeJsi(long jsiRuntimePtr, Object callInvokerHolder, String dbPath); private native void clearStateNativeJsi(); } ================================================ FILE: bindings/react-native/android/src/main/java/com/turso/sync/reactnative/TursoPackage.java ================================================ package com.turso.sync.reactnative; import androidx.annotation.NonNull; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; // Entry point for React Native's module registration system public class TursoPackage implements ReactPackage { @NonNull @Override public List createNativeModules(@NonNull ReactApplicationContext reactContext) { List modules = new ArrayList<>(); modules.add(new TursoModule(reactContext)); return modules; } @NonNull @Override public List createViewManagers(@NonNull ReactApplicationContext reactContext) { return Collections.emptyList(); } } ================================================ FILE: bindings/react-native/cpp/TursoConnectionHostObject.cpp ================================================ #include "TursoConnectionHostObject.h" #include "TursoStatementHostObject.h" extern "C" { #include } namespace turso { TursoConnectionHostObject::~TursoConnectionHostObject() { if (conn_) { turso_connection_deinit(conn_); conn_ = nullptr; } } void TursoConnectionHostObject::throwError(jsi::Runtime &rt, const char *error) { throw jsi::JSError(rt, error ? error : "Unknown error"); } jsi::Value TursoConnectionHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto propName = name.utf8(rt); if (propName == "prepareSingle") { return jsi::Function::createFromHostFunction( rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->prepareSingle(rt, args, count); } ); } if (propName == "prepareFirst") { return jsi::Function::createFromHostFunction( rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->prepareFirst(rt, args, count); } ); } if (propName == "lastInsertRowid") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->lastInsertRowid(rt); } ); } if (propName == "getAutocommit") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->getAutocommit(rt); } ); } if (propName == "setBusyTimeout") { return jsi::Function::createFromHostFunction( rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->setBusyTimeout(rt, args, count); } ); } if (propName == "close") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->close(rt); } ); } return jsi::Value::undefined(); } void TursoConnectionHostObject::set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) { // Read-only object } std::vector TursoConnectionHostObject::getPropertyNames(jsi::Runtime &rt) { std::vector props; props.emplace_back(jsi::PropNameID::forAscii(rt, "prepareSingle")); props.emplace_back(jsi::PropNameID::forAscii(rt, "prepareFirst")); props.emplace_back(jsi::PropNameID::forAscii(rt, "lastInsertRowid")); props.emplace_back(jsi::PropNameID::forAscii(rt, "getAutocommit")); props.emplace_back(jsi::PropNameID::forAscii(rt, "setBusyTimeout")); props.emplace_back(jsi::PropNameID::forAscii(rt, "close")); return props; } // 1:1 C API mapping - NO logic, just calls through to C API jsi::Value TursoConnectionHostObject::prepareSingle(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isString()) { throw jsi::JSError(rt, "prepareSingle: expected string argument"); } std::string sql = args[0].asString(rt).utf8(rt); turso_statement_t* statement = nullptr; const char* error = nullptr; turso_status_code_t status = turso_connection_prepare_single(conn_, sql.c_str(), &statement, &error); if (status != TURSO_OK) { throwError(rt, error); } // Wrap statement in TursoStatementHostObject auto statementObj = std::make_shared(statement); return jsi::Object::createFromHostObject(rt, statementObj); } jsi::Value TursoConnectionHostObject::prepareFirst(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isString()) { throw jsi::JSError(rt, "prepareFirst: expected string argument"); } std::string sql = args[0].asString(rt).utf8(rt); turso_statement_t* statement = nullptr; size_t tail_idx = 0; const char* error = nullptr; turso_status_code_t status = turso_connection_prepare_first(conn_, sql.c_str(), &statement, &tail_idx, &error); if (status != TURSO_OK) { throwError(rt, error); } // If statement is null, return null (no valid statement parsed) if (!statement) { return jsi::Value::null(); } // Return object with statement and tail_idx jsi::Object result(rt); auto statementObj = std::make_shared(statement); result.setProperty(rt, "statement", jsi::Object::createFromHostObject(rt, statementObj)); result.setProperty(rt, "tailIdx", jsi::Value(static_cast(tail_idx))); return result; } jsi::Value TursoConnectionHostObject::lastInsertRowid(jsi::Runtime &rt) { int64_t rowid = turso_connection_last_insert_rowid(conn_); return jsi::Value(static_cast(rowid)); } jsi::Value TursoConnectionHostObject::getAutocommit(jsi::Runtime &rt) { bool autocommit = turso_connection_get_autocommit(conn_); return jsi::Value(autocommit); } jsi::Value TursoConnectionHostObject::setBusyTimeout(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isNumber()) { throw jsi::JSError(rt, "setBusyTimeout: expected number argument"); } int64_t timeout_ms = static_cast(args[0].asNumber()); turso_connection_set_busy_timeout_ms(conn_, timeout_ms); return jsi::Value::undefined(); } jsi::Value TursoConnectionHostObject::close(jsi::Runtime &rt) { if (!conn_) { return jsi::Value::undefined(); } const char* error = nullptr; turso_status_code_t status = turso_connection_close(conn_, &error); conn_ = nullptr; // Prevent destructor from calling deinit on closed connection if (status != TURSO_OK) { throwError(rt, error); } return jsi::Value::undefined(); } } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoConnectionHostObject.h ================================================ #pragma once #include #include #include // Forward declarations for Turso C API types extern "C" { struct turso_connection; struct turso_statement; typedef struct turso_connection turso_connection_t; typedef struct turso_statement turso_statement_t; } namespace turso { using namespace facebook; /** * TursoConnectionHostObject wraps turso_connection_t* (core SDK-KIT type for database connection). * This is a THIN wrapper - 1:1 mapping of SDK-KIT C API with NO logic. * All logic belongs in TypeScript or Rust, not here. */ class TursoConnectionHostObject : public jsi::HostObject { public: TursoConnectionHostObject(turso_connection_t* conn) : conn_(conn) {} ~TursoConnectionHostObject(); // JSI HostObject interface jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override; void set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) override; std::vector getPropertyNames(jsi::Runtime &rt) override; // Direct access to wrapped pointer (for internal use) turso_connection_t* getConnection() const { return conn_; } private: turso_connection_t* conn_ = nullptr; // Helper to throw JS errors void throwError(jsi::Runtime &rt, const char *error); // 1:1 C API mapping methods (NO logic - just calls through to C API) jsi::Value prepareSingle(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value prepareFirst(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value lastInsertRowid(jsi::Runtime &rt); jsi::Value getAutocommit(jsi::Runtime &rt); jsi::Value setBusyTimeout(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value close(jsi::Runtime &rt); }; } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoDatabaseHostObject.cpp ================================================ #include "TursoDatabaseHostObject.h" #include "TursoConnectionHostObject.h" extern "C" { #include } namespace turso { TursoDatabaseHostObject::~TursoDatabaseHostObject() { if (db_) { turso_database_deinit(db_); db_ = nullptr; } } void TursoDatabaseHostObject::throwError(jsi::Runtime &rt, const char *error) { throw jsi::JSError(rt, error ? error : "Unknown error"); } jsi::Value TursoDatabaseHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto propName = name.utf8(rt); if (propName == "open") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->open(rt); } ); } if (propName == "connect") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->connect(rt); } ); } if (propName == "close") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->close(rt); } ); } return jsi::Value::undefined(); } void TursoDatabaseHostObject::set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) { // Read-only object } std::vector TursoDatabaseHostObject::getPropertyNames(jsi::Runtime &rt) { std::vector props; props.emplace_back(jsi::PropNameID::forAscii(rt, "open")); props.emplace_back(jsi::PropNameID::forAscii(rt, "connect")); props.emplace_back(jsi::PropNameID::forAscii(rt, "close")); return props; } // 1:1 C API mapping - NO logic, just calls through to C API jsi::Value TursoDatabaseHostObject::open(jsi::Runtime &rt) { const char* error = nullptr; turso_status_code_t status = turso_database_open(db_, &error); if (status != TURSO_OK) { throwError(rt, error); } return jsi::Value::undefined(); } jsi::Value TursoDatabaseHostObject::connect(jsi::Runtime &rt) { turso_connection_t* connection = nullptr; const char* error = nullptr; turso_status_code_t status = turso_database_connect(db_, &connection, &error); if (status != TURSO_OK) { throwError(rt, error); } // Wrap connection in TursoConnectionHostObject auto connectionObj = std::make_shared(connection); return jsi::Object::createFromHostObject(rt, connectionObj); } jsi::Value TursoDatabaseHostObject::close(jsi::Runtime &rt) { // turso_database_close doesn't exist in the C API // Closing happens in destructor via turso_database_deinit return jsi::Value::undefined(); } } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoDatabaseHostObject.h ================================================ #pragma once #include #include #include // Forward declarations for Turso C API types extern "C" { struct turso_database; struct turso_connection; typedef struct turso_database turso_database_t; typedef struct turso_connection turso_connection_t; } namespace turso { using namespace facebook; /** * TursoDatabaseHostObject wraps turso_database_t* (core SDK-KIT type for local database). * This is a THIN wrapper - 1:1 mapping of SDK-KIT C API with NO logic. * All logic belongs in TypeScript or Rust, not here. */ class TursoDatabaseHostObject : public jsi::HostObject { public: TursoDatabaseHostObject(turso_database_t* db) : db_(db) {} ~TursoDatabaseHostObject(); // JSI HostObject interface jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override; void set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) override; std::vector getPropertyNames(jsi::Runtime &rt) override; // Direct access to wrapped pointer (for internal use) turso_database_t* getDatabase() const { return db_; } private: turso_database_t* db_ = nullptr; // Helper to throw JS errors void throwError(jsi::Runtime &rt, const char *error); // 1:1 C API mapping methods (NO logic - just calls through to C API) jsi::Value open(jsi::Runtime &rt); jsi::Value connect(jsi::Runtime &rt); jsi::Value close(jsi::Runtime &rt); }; } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoHostObject.cpp ================================================ #include "TursoHostObject.h" #include "TursoDatabaseHostObject.h" #include "TursoSyncDatabaseHostObject.h" #include // For FILE, fopen, fread, fwrite, fclose, fseek, ftell, remove, rename #include // For additional standard library functions #include // For strrchr #include // For fsync, close #include // For open, O_RDONLY #include // For std::mutex, std::lock_guard extern "C" { #include #include } namespace turso { /** * Durable fsync: on Apple, fsync() only flushes to disk cache, * so we need F_FULLFSYNC for true persistence. On Linux/Android, * plain fsync() is sufficient. */ static int durable_fsync(int fd) { #ifdef __APPLE__ return fcntl(fd, F_FULLFSYNC); #else return fsync(fd); #endif } using namespace facebook; // Global base path for database files static std::string g_basePath; // Logger callback state — the Rust tracing subscriber may fire from any thread, // so we copy log data in the C callback and schedule JS execution via CallInvoker. static jsi::Runtime* g_runtime = nullptr; static std::shared_ptr g_callInvoker; static std::shared_ptr g_loggerFn; static std::mutex g_loggerMutex; /** * Normalize a database path: * - If path is absolute (starts with '/'), use as-is * - If path is ':memory:', use as-is * - Otherwise, prepend basePath */ static std::string normalizePath(const std::string &path) { // Special cases: absolute path or in-memory if (path.empty() || path[0] == '/' || path == ":memory:") { return path; } // Relative path - prepend basePath if (g_basePath.empty()) { return path; } // Combine basePath + path if (g_basePath[g_basePath.length() - 1] == '/') { return g_basePath + path; } else { return g_basePath + "/" + path; } } /** * Map turso_tracing_level_t enum to JS-friendly string. */ static const char* tracingLevelToString(turso_tracing_level_t level) { switch (level) { case TURSO_TRACING_LEVEL_ERROR: return "error"; case TURSO_TRACING_LEVEL_WARN: return "warn"; case TURSO_TRACING_LEVEL_INFO: return "info"; case TURSO_TRACING_LEVEL_DEBUG: return "debug"; case TURSO_TRACING_LEVEL_TRACE: return "trace"; default: return "error"; } } /** * C callback invoked by the Rust tracing subscriber (possibly from any thread). * Copies all string data synchronously, then schedules a JS call on the JS thread. */ static void turso_logger_callback(const turso_log_t *log) { std::lock_guard lock(g_loggerMutex); if (!g_loggerFn || !g_callInvoker || !g_runtime) { return; } // Copy all data — the turso_log_t fields are only valid during this callback. std::string message = log->message ? log->message : ""; std::string target = log->target ? log->target : ""; std::string file = log->file ? log->file : ""; uint64_t timestamp = log->timestamp; size_t line = log->line; const char* level = tracingLevelToString(log->level); std::string levelStr(level); // Prevent captures from preventing cleanup — capture shared_ptr copies auto callInvoker = g_callInvoker; auto loggerFn = g_loggerFn; callInvoker->invokeAsync( [loggerFn, message = std::move(message), target = std::move(target), file = std::move(file), timestamp, line, levelStr = std::move(levelStr)] (jsi::Runtime &rt) { try { jsi::Object logObj(rt); logObj.setProperty(rt, "message", jsi::String::createFromUtf8(rt, message)); logObj.setProperty(rt, "target", jsi::String::createFromUtf8(rt, target)); logObj.setProperty(rt, "file", jsi::String::createFromUtf8(rt, file)); logObj.setProperty(rt, "timestamp", static_cast(timestamp)); logObj.setProperty(rt, "line", static_cast(line)); logObj.setProperty(rt, "level", jsi::String::createFromUtf8(rt, levelStr)); loggerFn->call(rt, logObj); } catch (...) { // Logger must never crash the app — swallow all exceptions. } }); } void install( jsi::Runtime &rt, const std::shared_ptr &invoker, const char *basePath) { g_basePath = basePath ? basePath : ""; g_runtime = &rt; g_callInvoker = invoker; // Create the module object jsi::Object module(rt); // newDatabase(path, dbConfig) -> TursoDatabaseHostObject // Factory for creating local-only databases auto newDatabase = jsi::Function::createFromHostFunction( rt, jsi::PropNameID::forAscii(rt, "newDatabase"), 1, // min args [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { if (count < 1 || !args[0].isString()) { throw jsi::JSError(rt, "newDatabase() requires path string as first argument"); } std::string path = args[0].asString(rt).utf8(rt); // Normalize path (prepend basePath if relative) std::string normalizedPath = normalizePath(path); // Build database config turso_database_config_t db_config = {0}; db_config.async_io = 1; // Default to async IO for React Native db_config.path = normalizedPath.c_str(); db_config.experimental_features = nullptr; db_config.vfs = nullptr; db_config.encryption_cipher = nullptr; db_config.encryption_hexkey = nullptr; // Parse optional dbConfig object (second argument) if (count >= 2 && args[1].isObject()) { jsi::Object config = args[1].asObject(rt); // Parse async_io if provided if (config.hasProperty(rt, "async_io")) { db_config.async_io = config.getProperty(rt, "async_io").getBool() ? 1 : 0; } } // Create database instance const turso_database_t* database = nullptr; const char* error = nullptr; turso_status_code_t status = turso_database_new(&db_config, &database, &error); if (status != TURSO_OK) { std::string errorMsg = error ? error : "Failed to create database"; throw jsi::JSError(rt, errorMsg); } // Wrap in TursoDatabaseHostObject auto dbObj = std::make_shared( const_cast(database) ); return jsi::Object::createFromHostObject(rt, dbObj); }); // newSyncDatabase(dbConfig, syncConfig) -> TursoSyncDatabaseHostObject // Factory for creating sync-enabled embedded replica databases auto newSyncDatabase = jsi::Function::createFromHostFunction( rt, jsi::PropNameID::forAscii(rt, "newSyncDatabase"), 2, // min args [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { if (count < 2 || !args[0].isObject() || !args[1].isObject()) { throw jsi::JSError(rt, "newSyncDatabase() requires dbConfig and syncConfig objects"); } jsi::Object dbConfigObj = args[0].asObject(rt); jsi::Object syncConfigObj = args[1].asObject(rt); // Parse dbConfig if (!dbConfigObj.hasProperty(rt, "path")) { throw jsi::JSError(rt, "dbConfig must have 'path' property"); } std::string path = dbConfigObj.getProperty(rt, "path").asString(rt).utf8(rt); // Normalize path (prepend basePath if relative) std::string normalizedPath = normalizePath(path); turso_database_config_t db_config = {0}; db_config.async_io = 1; // Default to async IO for React Native db_config.path = normalizedPath.c_str(); // Parse async_io if provided in dbConfig if (dbConfigObj.hasProperty(rt, "async_io")) { db_config.async_io = dbConfigObj.getProperty(rt, "async_io").getBool() ? 1 : 0; } db_config.experimental_features = nullptr; db_config.vfs = nullptr; db_config.encryption_cipher = nullptr; db_config.encryption_hexkey = nullptr; // Parse syncConfig turso_sync_database_config_t sync_config = {0}; // path (already set in db_config, but sync_config also needs it) sync_config.path = normalizedPath.c_str(); // remoteUrl (optional) std::string remoteUrl; if (syncConfigObj.hasProperty(rt, "remoteUrl")) { jsi::Value remoteUrlVal = syncConfigObj.getProperty(rt, "remoteUrl"); if (!remoteUrlVal.isNull() && !remoteUrlVal.isUndefined()) { remoteUrl = remoteUrlVal.asString(rt).utf8(rt); sync_config.remote_url = remoteUrl.c_str(); } else { sync_config.remote_url = nullptr; } } else { sync_config.remote_url = nullptr; } // clientName (optional) std::string clientName; if (syncConfigObj.hasProperty(rt, "clientName")) { jsi::Value clientNameVal = syncConfigObj.getProperty(rt, "clientName"); if (!clientNameVal.isNull() && !clientNameVal.isUndefined()) { clientName = clientNameVal.asString(rt).utf8(rt); sync_config.client_name = clientName.c_str(); } else { sync_config.client_name = nullptr; } } else { sync_config.client_name = nullptr; } // longPollTimeoutMs if (syncConfigObj.hasProperty(rt, "longPollTimeoutMs")) { jsi::Value longPollVal = syncConfigObj.getProperty(rt, "longPollTimeoutMs"); if (!longPollVal.isNull() && !longPollVal.isUndefined()) { sync_config.long_poll_timeout_ms = static_cast(longPollVal.asNumber()); } else { sync_config.long_poll_timeout_ms = 0; } } else { sync_config.long_poll_timeout_ms = 0; } // bootstrapIfEmpty if (syncConfigObj.hasProperty(rt, "bootstrapIfEmpty")) { jsi::Value bootstrapVal = syncConfigObj.getProperty(rt, "bootstrapIfEmpty"); if (!bootstrapVal.isNull() && !bootstrapVal.isUndefined()) { sync_config.bootstrap_if_empty = bootstrapVal.getBool(); } else { sync_config.bootstrap_if_empty = false; } } else { sync_config.bootstrap_if_empty = false; } // reservedBytes if (syncConfigObj.hasProperty(rt, "reservedBytes")) { jsi::Value reservedVal = syncConfigObj.getProperty(rt, "reservedBytes"); if (!reservedVal.isNull() && !reservedVal.isUndefined()) { sync_config.reserved_bytes = static_cast(reservedVal.asNumber()); } else { sync_config.reserved_bytes = 0; } } else { sync_config.reserved_bytes = 0; } // Partial sync options if (syncConfigObj.hasProperty(rt, "partialBootstrapStrategyPrefix")) { jsi::Value prefixVal = syncConfigObj.getProperty(rt, "partialBootstrapStrategyPrefix"); if (!prefixVal.isNull() && !prefixVal.isUndefined()) { sync_config.partial_bootstrap_strategy_prefix = static_cast(prefixVal.asNumber()); } else { sync_config.partial_bootstrap_strategy_prefix = 0; } } else { sync_config.partial_bootstrap_strategy_prefix = 0; } std::string partialBootstrapStrategyQuery; if (syncConfigObj.hasProperty(rt, "partialBootstrapStrategyQuery")) { jsi::Value queryVal = syncConfigObj.getProperty(rt, "partialBootstrapStrategyQuery"); if (!queryVal.isNull() && !queryVal.isUndefined()) { partialBootstrapStrategyQuery = queryVal.asString(rt).utf8(rt); sync_config.partial_bootstrap_strategy_query = partialBootstrapStrategyQuery.c_str(); } else { sync_config.partial_bootstrap_strategy_query = nullptr; } } else { sync_config.partial_bootstrap_strategy_query = nullptr; } if (syncConfigObj.hasProperty(rt, "partialBootstrapSegmentSize")) { jsi::Value segmentVal = syncConfigObj.getProperty(rt, "partialBootstrapSegmentSize"); if (!segmentVal.isNull() && !segmentVal.isUndefined()) { sync_config.partial_bootstrap_segment_size = static_cast(segmentVal.asNumber()); } else { sync_config.partial_bootstrap_segment_size = 0; } } else { sync_config.partial_bootstrap_segment_size = 0; } if (syncConfigObj.hasProperty(rt, "partialBootstrapPrefetch")) { jsi::Value prefetchVal = syncConfigObj.getProperty(rt, "partialBootstrapPrefetch"); if (!prefetchVal.isNull() && !prefetchVal.isUndefined()) { sync_config.partial_bootstrap_prefetch = prefetchVal.getBool(); } else { sync_config.partial_bootstrap_prefetch = false; } } else { sync_config.partial_bootstrap_prefetch = false; } // Remote encryption options std::string remoteEncryptionKey; if (syncConfigObj.hasProperty(rt, "remoteEncryptionKey")) { jsi::Value keyVal = syncConfigObj.getProperty(rt, "remoteEncryptionKey"); if (!keyVal.isNull() && !keyVal.isUndefined()) { remoteEncryptionKey = keyVal.asString(rt).utf8(rt); sync_config.remote_encryption_key = remoteEncryptionKey.c_str(); } else { sync_config.remote_encryption_key = nullptr; } } else { sync_config.remote_encryption_key = nullptr; } std::string remoteEncryptionCipher; if (syncConfigObj.hasProperty(rt, "remoteEncryptionCipher")) { jsi::Value cipherVal = syncConfigObj.getProperty(rt, "remoteEncryptionCipher"); if (!cipherVal.isNull() && !cipherVal.isUndefined()) { remoteEncryptionCipher = cipherVal.asString(rt).utf8(rt); sync_config.remote_encryption_cipher = remoteEncryptionCipher.c_str(); } else { sync_config.remote_encryption_cipher = nullptr; } } else { sync_config.remote_encryption_cipher = nullptr; } // Create sync database instance const turso_sync_database_t* database = nullptr; const char* error = nullptr; turso_status_code_t status = turso_sync_database_new(&db_config, &sync_config, &database, &error); if (status != TURSO_OK) { std::string errorMsg = error ? error : "Failed to create sync database"; throw jsi::JSError(rt, errorMsg); } // Wrap in TursoSyncDatabaseHostObject auto dbObj = std::make_shared( const_cast(database) ); return jsi::Object::createFromHostObject(rt, dbObj); }); // version() -> string auto version = jsi::Function::createFromHostFunction( rt, jsi::PropNameID::forAscii(rt, "version"), 0, [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { const char *ver = turso_version(); return jsi::String::createFromUtf8(rt, ver); }); // setup(options) -> void auto setup = jsi::Function::createFromHostFunction( rt, jsi::PropNameID::forAscii(rt, "setup"), 1, [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { if (count < 1 || !args[0].isObject()) { throw jsi::JSError(rt, "setup() requires an options object"); } jsi::Object options = args[0].asObject(rt); std::string logLevelStr; // Get log level if provided if (options.hasProperty(rt, "logLevel")) { jsi::Value logLevelVal = options.getProperty(rt, "logLevel"); if (logLevelVal.isString()) { logLevelStr = logLevelVal.asString(rt).utf8(rt); } } turso_config_t config = {nullptr, logLevelStr.empty() ? nullptr : logLevelStr.c_str()}; // Wire up logger callback if provided if (options.hasProperty(rt, "logger")) { jsi::Value loggerVal = options.getProperty(rt, "logger"); if (loggerVal.isObject() && loggerVal.asObject(rt).isFunction(rt)) { { std::lock_guard lock(g_loggerMutex); g_loggerFn = std::make_shared( loggerVal.asObject(rt).asFunction(rt)); } config.logger = turso_logger_callback; } } // Call turso_setup const char *error = nullptr; turso_status_code_t status = turso_setup(&config, &error); if (status != TURSO_OK) { std::string errorMsg = error ? error : "Unknown error in turso_setup"; throw jsi::JSError(rt, errorMsg); } return jsi::Value::undefined(); }); // fsReadFile(path) -> ArrayBuffer auto fsReadFile = jsi::Function::createFromHostFunction( rt, jsi::PropNameID::forAscii(rt, "fsReadFile"), 1, [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { if (count < 1 || !args[0].isString()) { throw jsi::JSError(rt, "fsReadFile() requires path string"); } std::string path = args[0].asString(rt).utf8(rt); // Open file for reading FILE* file = fopen(path.c_str(), "rb"); if (!file) { // File not found - return null (caller will handle as empty) return jsi::Value::null(); } // Get file size fseek(file, 0, SEEK_END); long size = ftell(file); fseek(file, 0, SEEK_SET); if (size <= 0) { fclose(file); // Empty file - return empty ArrayBuffer jsi::Function arrayBufferCtor = rt.global().getPropertyAsFunction(rt, "ArrayBuffer"); jsi::Object arrayBuffer = arrayBufferCtor.callAsConstructor(rt, 0).asObject(rt); return arrayBuffer; } // Read file contents jsi::Function arrayBufferCtor = rt.global().getPropertyAsFunction(rt, "ArrayBuffer"); jsi::Object arrayBuffer = arrayBufferCtor.callAsConstructor(rt, static_cast(size)).asObject(rt); jsi::ArrayBuffer buf = arrayBuffer.getArrayBuffer(rt); size_t bytesRead = fread(buf.data(rt), 1, size, file); fclose(file); if (bytesRead != static_cast(size)) { throw jsi::JSError(rt, "Failed to read complete file"); } return arrayBuffer; }); // fsWriteFile(path, arrayBuffer) -> void auto fsWriteFile = jsi::Function::createFromHostFunction( rt, jsi::PropNameID::forAscii(rt, "fsWriteFile"), 2, [](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { if (count < 2 || !args[0].isString() || !args[1].isObject()) { throw jsi::JSError(rt, "fsWriteFile() requires path string and ArrayBuffer"); } std::string path = args[0].asString(rt).utf8(rt); jsi::ArrayBuffer buffer = args[1].asObject(rt).getArrayBuffer(rt); // Write atomically: write to temp, fsync, rename, fsync dir std::string tempPath = path + ".tmp"; FILE* file = fopen(tempPath.c_str(), "wb"); if (!file) { throw jsi::JSError(rt, "Failed to open file for writing"); } size_t size = buffer.size(rt); if (size > 0) { size_t written = fwrite(buffer.data(rt), 1, size, file); if (written != size) { fclose(file); remove(tempPath.c_str()); throw jsi::JSError(rt, "Failed to write complete file"); } } // Flush to OS and sync to disk before rename if (fflush(file) != 0 || durable_fsync(fileno(file)) != 0) { fclose(file); remove(tempPath.c_str()); throw jsi::JSError(rt, "Failed to sync file to disk"); } fclose(file); // Atomic rename (replaces old file) if (rename(tempPath.c_str(), path.c_str()) != 0) { remove(tempPath.c_str()); throw jsi::JSError(rt, "Failed to rename temp file"); } // Fsync parent directory to ensure rename is durable std::string dirPath = path; auto lastSlash = dirPath.rfind('/'); if (lastSlash != std::string::npos) { dirPath.resize(lastSlash); int dirFd = open(dirPath.c_str(), O_RDONLY); if (dirFd >= 0) { durable_fsync(dirFd); close(dirFd); } } return jsi::Value::undefined(); }); module.setProperty(rt, "newDatabase", std::move(newDatabase)); module.setProperty(rt, "newSyncDatabase", std::move(newSyncDatabase)); module.setProperty(rt, "version", std::move(version)); module.setProperty(rt, "setup", std::move(setup)); module.setProperty(rt, "fsReadFile", std::move(fsReadFile)); module.setProperty(rt, "fsWriteFile", std::move(fsWriteFile)); // Install as global __TursoProxy rt.global().setProperty(rt, "__TursoProxy", std::move(module)); } void invalidate() { std::lock_guard lock(g_loggerMutex); g_loggerFn.reset(); g_callInvoker.reset(); g_runtime = nullptr; } } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoHostObject.h ================================================ #pragma once #include #include #include #include namespace turso { using namespace facebook; /** * Install the Turso module into the JSI runtime. * This creates a global __TursoProxy object with the open() function. */ void install( jsi::Runtime &rt, const std::shared_ptr &invoker, const char *basePath ); void invalidate(); } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoStatementHostObject.cpp ================================================ #include "TursoStatementHostObject.h" extern "C" { #include } namespace turso { TursoStatementHostObject::~TursoStatementHostObject() { if (stmt_) { turso_statement_deinit(stmt_); stmt_ = nullptr; } } void TursoStatementHostObject::throwError(jsi::Runtime &rt, const char *error) { throw jsi::JSError(rt, error ? error : "Unknown error"); } jsi::Value TursoStatementHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto propName = name.utf8(rt); if (propName == "bindPositionalNull") { return jsi::Function::createFromHostFunction(rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->bindPositionalNull(rt, args, count); }); } if (propName == "bindPositionalInt") { return jsi::Function::createFromHostFunction(rt, name, 2, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->bindPositionalInt(rt, args, count); }); } if (propName == "bindPositionalDouble") { return jsi::Function::createFromHostFunction(rt, name, 2, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->bindPositionalDouble(rt, args, count); }); } if (propName == "bindPositionalBlob") { return jsi::Function::createFromHostFunction(rt, name, 2, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->bindPositionalBlob(rt, args, count); }); } if (propName == "bindPositionalText") { return jsi::Function::createFromHostFunction(rt, name, 2, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->bindPositionalText(rt, args, count); }); } if (propName == "execute") { return jsi::Function::createFromHostFunction(rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->execute(rt); }); } if (propName == "step") { return jsi::Function::createFromHostFunction(rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->step(rt); }); } if (propName == "runIo") { return jsi::Function::createFromHostFunction(rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->runIo(rt); }); } if (propName == "reset") { return jsi::Function::createFromHostFunction(rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->reset(rt); }); } if (propName == "finalize") { return jsi::Function::createFromHostFunction(rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->finalize(rt); }); } if (propName == "nChange") { return jsi::Function::createFromHostFunction(rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->nChange(rt); }); } if (propName == "columnCount") { return jsi::Function::createFromHostFunction(rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->columnCount(rt); }); } if (propName == "columnName") { return jsi::Function::createFromHostFunction(rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->columnName(rt, args, count); }); } if (propName == "rowValueKind") { return jsi::Function::createFromHostFunction(rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->rowValueKind(rt, args, count); }); } if (propName == "rowValueBytesCount") { return jsi::Function::createFromHostFunction(rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->rowValueBytesCount(rt, args, count); }); } if (propName == "rowValueBytesPtr") { return jsi::Function::createFromHostFunction(rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->rowValueBytesPtr(rt, args, count); }); } if (propName == "rowValueText") { return jsi::Function::createFromHostFunction(rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->rowValueText(rt, args, count); }); } if (propName == "rowValueInt") { return jsi::Function::createFromHostFunction(rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->rowValueInt(rt, args, count); }); } if (propName == "rowValueDouble") { return jsi::Function::createFromHostFunction(rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->rowValueDouble(rt, args, count); }); } if (propName == "namedPosition") { return jsi::Function::createFromHostFunction(rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->namedPosition(rt, args, count); }); } if (propName == "parametersCount") { return jsi::Function::createFromHostFunction(rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->parametersCount(rt); }); } if (propName == "getAllRows") { return jsi::Function::createFromHostFunction(rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->getAllRows(rt); }); } return jsi::Value::undefined(); } void TursoStatementHostObject::set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) { // Read-only object } std::vector TursoStatementHostObject::getPropertyNames(jsi::Runtime &rt) { std::vector props; props.emplace_back(jsi::PropNameID::forAscii(rt, "bindPositionalNull")); props.emplace_back(jsi::PropNameID::forAscii(rt, "bindPositionalInt")); props.emplace_back(jsi::PropNameID::forAscii(rt, "bindPositionalDouble")); props.emplace_back(jsi::PropNameID::forAscii(rt, "bindPositionalBlob")); props.emplace_back(jsi::PropNameID::forAscii(rt, "bindPositionalText")); props.emplace_back(jsi::PropNameID::forAscii(rt, "execute")); props.emplace_back(jsi::PropNameID::forAscii(rt, "step")); props.emplace_back(jsi::PropNameID::forAscii(rt, "runIo")); props.emplace_back(jsi::PropNameID::forAscii(rt, "reset")); props.emplace_back(jsi::PropNameID::forAscii(rt, "finalize")); props.emplace_back(jsi::PropNameID::forAscii(rt, "nChange")); props.emplace_back(jsi::PropNameID::forAscii(rt, "columnCount")); props.emplace_back(jsi::PropNameID::forAscii(rt, "columnName")); props.emplace_back(jsi::PropNameID::forAscii(rt, "rowValueKind")); props.emplace_back(jsi::PropNameID::forAscii(rt, "rowValueBytesCount")); props.emplace_back(jsi::PropNameID::forAscii(rt, "rowValueBytesPtr")); props.emplace_back(jsi::PropNameID::forAscii(rt, "rowValueText")); props.emplace_back(jsi::PropNameID::forAscii(rt, "rowValueInt")); props.emplace_back(jsi::PropNameID::forAscii(rt, "rowValueDouble")); props.emplace_back(jsi::PropNameID::forAscii(rt, "namedPosition")); props.emplace_back(jsi::PropNameID::forAscii(rt, "parametersCount")); props.emplace_back(jsi::PropNameID::forAscii(rt, "getAllRows")); return props; } // 1:1 C API mapping - NO logic, just calls through to C API jsi::Value TursoStatementHostObject::bindPositionalNull(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isNumber()) { throw jsi::JSError(rt, "bindPositionalNull: expected number argument (position)"); } size_t position = static_cast(args[0].asNumber()); turso_status_code_t status = turso_statement_bind_positional_null(stmt_, position); return jsi::Value(static_cast(status)); } jsi::Value TursoStatementHostObject::bindPositionalInt(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 2 || !args[0].isNumber() || !args[1].isNumber()) { throw jsi::JSError(rt, "bindPositionalInt: expected two number arguments (position, value)"); } size_t position = static_cast(args[0].asNumber()); int64_t value = static_cast(args[1].asNumber()); turso_status_code_t status = turso_statement_bind_positional_int(stmt_, position, value); return jsi::Value(static_cast(status)); } jsi::Value TursoStatementHostObject::bindPositionalDouble(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 2 || !args[0].isNumber() || !args[1].isNumber()) { throw jsi::JSError(rt, "bindPositionalDouble: expected two number arguments (position, value)"); } size_t position = static_cast(args[0].asNumber()); double value = args[1].asNumber(); turso_status_code_t status = turso_statement_bind_positional_double(stmt_, position, value); return jsi::Value(static_cast(status)); } jsi::Value TursoStatementHostObject::bindPositionalBlob(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 2 || !args[0].isNumber() || !args[1].isObject()) { throw jsi::JSError(rt, "bindPositionalBlob: expected number and ArrayBuffer arguments"); } size_t position = static_cast(args[0].asNumber()); // Get ArrayBuffer auto arrayBuffer = args[1].asObject(rt).getArrayBuffer(rt); const char* data = reinterpret_cast(arrayBuffer.data(rt)); size_t len = arrayBuffer.size(rt); turso_status_code_t status = turso_statement_bind_positional_blob(stmt_, position, data, len); return jsi::Value(static_cast(status)); } jsi::Value TursoStatementHostObject::bindPositionalText(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 2 || !args[0].isNumber() || !args[1].isString()) { throw jsi::JSError(rt, "bindPositionalText: expected number and string arguments"); } size_t position = static_cast(args[0].asNumber()); std::string value = args[1].asString(rt).utf8(rt); turso_status_code_t status = turso_statement_bind_positional_text(stmt_, position, value.c_str(), value.length()); return jsi::Value(static_cast(status)); } jsi::Value TursoStatementHostObject::execute(jsi::Runtime &rt) { uint64_t rows_changed = 0; const char* error = nullptr; turso_status_code_t status = turso_statement_execute(stmt_, &rows_changed, &error); if (status != TURSO_OK && status != TURSO_DONE && status != TURSO_IO) { throwError(rt, error); } // Return object with status and rows_changed jsi::Object result(rt); result.setProperty(rt, "status", jsi::Value(static_cast(status))); result.setProperty(rt, "rowsChanged", jsi::Value(static_cast(rows_changed))); return result; } jsi::Value TursoStatementHostObject::step(jsi::Runtime &rt) { const char* error = nullptr; turso_status_code_t status = turso_statement_step(stmt_, &error); if (status != TURSO_OK && status != TURSO_DONE && status != TURSO_ROW && status != TURSO_IO) { throwError(rt, error); } return jsi::Value(static_cast(status)); } jsi::Value TursoStatementHostObject::runIo(jsi::Runtime &rt) { const char* error = nullptr; turso_status_code_t status = turso_statement_run_io(stmt_, &error); if (status != TURSO_OK) { throwError(rt, error); } return jsi::Value(static_cast(status)); } jsi::Value TursoStatementHostObject::reset(jsi::Runtime &rt) { const char* error = nullptr; turso_status_code_t status = turso_statement_reset(stmt_, &error); if (status != TURSO_OK) { throwError(rt, error); } return jsi::Value::undefined(); } jsi::Value TursoStatementHostObject::finalize(jsi::Runtime &rt) { if (!stmt_) { return jsi::Value(static_cast(TURSO_DONE)); } const char* error = nullptr; turso_status_code_t status = turso_statement_finalize(stmt_, &error); if (status == TURSO_DONE) { stmt_ = nullptr; // Prevent destructor from calling deinit on finalized statement } if (status != TURSO_DONE && status != TURSO_IO) { throwError(rt, error); } return jsi::Value(static_cast(status)); } jsi::Value TursoStatementHostObject::nChange(jsi::Runtime &rt) { int64_t n = turso_statement_n_change(stmt_); return jsi::Value(static_cast(n)); } jsi::Value TursoStatementHostObject::columnCount(jsi::Runtime &rt) { int64_t count = turso_statement_column_count(stmt_); return jsi::Value(static_cast(count)); } jsi::Value TursoStatementHostObject::columnName(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isNumber()) { throw jsi::JSError(rt, "columnName: expected number argument (index)"); } size_t index = static_cast(args[0].asNumber()); const char* name = turso_statement_column_name(stmt_, index); if (!name) { return jsi::Value::null(); } std::string nameStr(name); turso_str_deinit(name); // Free the C string return jsi::String::createFromUtf8(rt, nameStr); } jsi::Value TursoStatementHostObject::rowValueKind(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isNumber()) { throw jsi::JSError(rt, "rowValueKind: expected number argument (index)"); } size_t index = static_cast(args[0].asNumber()); turso_type_t kind = turso_statement_row_value_kind(stmt_, index); return jsi::Value(static_cast(kind)); } jsi::Value TursoStatementHostObject::rowValueBytesCount(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isNumber()) { throw jsi::JSError(rt, "rowValueBytesCount: expected number argument (index)"); } size_t index = static_cast(args[0].asNumber()); int64_t bytes = turso_statement_row_value_bytes_count(stmt_, index); return jsi::Value(static_cast(bytes)); } jsi::Value TursoStatementHostObject::rowValueBytesPtr(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isNumber()) { throw jsi::JSError(rt, "rowValueBytesPtr: expected number argument (index)"); } size_t index = static_cast(args[0].asNumber()); const char* ptr = turso_statement_row_value_bytes_ptr(stmt_, index); int64_t bytes = turso_statement_row_value_bytes_count(stmt_, index); if (!ptr || bytes <= 0) { return jsi::Value::null(); } // Create ArrayBuffer and copy data jsi::Function arrayBufferCtor = rt.global().getPropertyAsFunction(rt, "ArrayBuffer"); jsi::Object arrayBuffer = arrayBufferCtor.callAsConstructor(rt, static_cast(bytes)).asObject(rt); jsi::ArrayBuffer buf = arrayBuffer.getArrayBuffer(rt); memcpy(buf.data(rt), ptr, bytes); return arrayBuffer; } jsi::Value TursoStatementHostObject::rowValueText(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isNumber()) { throw jsi::JSError(rt, "rowValueText: expected number argument (index)"); } size_t index = static_cast(args[0].asNumber()); const char* ptr = turso_statement_row_value_bytes_ptr(stmt_, index); int64_t bytes = turso_statement_row_value_bytes_count(stmt_, index); if (!ptr || bytes < 0) { return jsi::String::createFromUtf8(rt, ""); } // Create jsi::String directly from UTF-8 bytes // This avoids the round-trip through ArrayBuffer and decoding in JS return jsi::String::createFromUtf8(rt, reinterpret_cast(ptr), static_cast(bytes)); } jsi::Value TursoStatementHostObject::rowValueInt(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isNumber()) { throw jsi::JSError(rt, "rowValueInt: expected number argument (index)"); } size_t index = static_cast(args[0].asNumber()); int64_t value = turso_statement_row_value_int(stmt_, index); return jsi::Value(static_cast(value)); } jsi::Value TursoStatementHostObject::rowValueDouble(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isNumber()) { throw jsi::JSError(rt, "rowValueDouble: expected number argument (index)"); } size_t index = static_cast(args[0].asNumber()); double value = turso_statement_row_value_double(stmt_, index); return jsi::Value(value); } jsi::Value TursoStatementHostObject::namedPosition(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isString()) { throw jsi::JSError(rt, "namedPosition: expected string argument (name)"); } std::string name = args[0].asString(rt).utf8(rt); int64_t position = turso_statement_named_position(stmt_, name.c_str()); return jsi::Value(static_cast(position)); } jsi::Value TursoStatementHostObject::parametersCount(jsi::Runtime &rt) { int64_t count = turso_statement_parameters_count(stmt_); return jsi::Value(static_cast(count)); } jsi::Value TursoStatementHostObject::getAllRows(jsi::Runtime &rt) { int64_t colCount = turso_statement_column_count(stmt_); if (colCount < 0) { // defensive fallback to 0 to not allocate a vector with a negative size colCount = 0; } // Cache column names upfront (one allocation per query, not per row) std::vector colNames(colCount); for (int64_t i = 0; i < colCount; ++i) { const char* name = turso_statement_column_name(stmt_, static_cast(i)); if (name) { colNames[i] = std::string(name); turso_str_deinit(name); } } // Collect rows into a vector first, then build the jsi::Array at the end // (jsi::Array needs a size upfront) std::vector rowObjects; while (true) { const char* error = nullptr; turso_status_code_t status = turso_statement_step(stmt_, &error); if (status == TURSO_DONE) { break; } if (status == TURSO_IO) { // Return partial results so JS can handle IO and fall back jsi::Array rows(rt, rowObjects.size()); for (size_t i = 0; i < rowObjects.size(); ++i) { rows.setValueAtIndex(rt, i, std::move(rowObjects[i])); } jsi::Object result(rt); result.setProperty(rt, "status", jsi::Value(static_cast(TURSO_IO))); result.setProperty(rt, "rows", std::move(rows)); return result; } if (status != TURSO_ROW) { throwError(rt, error); } // Read all columns for this row jsi::Object row(rt); for (int64_t i = 0; i < colCount; ++i) { size_t idx = static_cast(i); turso_type_t kind = turso_statement_row_value_kind(stmt_, idx); switch (kind) { case TURSO_TYPE_INTEGER: { int64_t val = turso_statement_row_value_int(stmt_, idx); row.setProperty(rt, colNames[i].c_str(), jsi::Value(static_cast(val))); break; } case TURSO_TYPE_REAL: { double val = turso_statement_row_value_double(stmt_, idx); row.setProperty(rt, colNames[i].c_str(), jsi::Value(val)); break; } case TURSO_TYPE_TEXT: { const char* ptr = turso_statement_row_value_bytes_ptr(stmt_, idx); int64_t len = turso_statement_row_value_bytes_count(stmt_, idx); if (ptr && len >= 0) { row.setProperty(rt, colNames[i].c_str(), jsi::String::createFromUtf8(rt, reinterpret_cast(ptr), static_cast(len))); } else { row.setProperty(rt, colNames[i].c_str(), jsi::String::createFromUtf8(rt, "")); } break; } case TURSO_TYPE_BLOB: { const char* ptr = turso_statement_row_value_bytes_ptr(stmt_, idx); int64_t len = turso_statement_row_value_bytes_count(stmt_, idx); size_t blobLen = (len > 0) ? static_cast(len) : 0; jsi::Function arrayBufferCtor = rt.global().getPropertyAsFunction(rt, "ArrayBuffer"); jsi::Object arrayBuffer = arrayBufferCtor.callAsConstructor(rt, static_cast(blobLen)).asObject(rt); if (ptr && blobLen > 0) { jsi::ArrayBuffer buf = arrayBuffer.getArrayBuffer(rt); memcpy(buf.data(rt), ptr, blobLen); } row.setProperty(rt, colNames[i].c_str(), std::move(arrayBuffer)); break; } case TURSO_TYPE_NULL: default: row.setProperty(rt, colNames[i].c_str(), jsi::Value::null()); break; } } rowObjects.push_back(std::move(row)); } // Build final array jsi::Array rows(rt, rowObjects.size()); for (size_t i = 0; i < rowObjects.size(); ++i) { rows.setValueAtIndex(rt, i, std::move(rowObjects[i])); } jsi::Object result(rt); result.setProperty(rt, "status", jsi::Value(static_cast(TURSO_DONE))); result.setProperty(rt, "rows", std::move(rows)); return result; } } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoStatementHostObject.h ================================================ #pragma once #include #include #include // Forward declarations for Turso C API types extern "C" { struct turso_statement; typedef struct turso_statement turso_statement_t; } namespace turso { using namespace facebook; /** * TursoStatementHostObject wraps turso_statement_t* (core SDK-KIT type for prepared statement). * This is a THIN wrapper - 1:1 mapping of SDK-KIT C API with NO logic. * All logic belongs in TypeScript or Rust, not here. */ class TursoStatementHostObject : public jsi::HostObject { public: TursoStatementHostObject(turso_statement_t* stmt) : stmt_(stmt) {} ~TursoStatementHostObject(); // JSI HostObject interface jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override; void set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) override; std::vector getPropertyNames(jsi::Runtime &rt) override; // Direct access to wrapped pointer (for internal use) turso_statement_t* getStatement() const { return stmt_; } private: turso_statement_t* stmt_ = nullptr; // Helper to throw JS errors void throwError(jsi::Runtime &rt, const char *error); // 1:1 C API mapping methods (NO logic - just calls through to C API) jsi::Value bindPositionalNull(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value bindPositionalInt(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value bindPositionalDouble(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value bindPositionalBlob(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value bindPositionalText(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value execute(jsi::Runtime &rt); jsi::Value step(jsi::Runtime &rt); jsi::Value runIo(jsi::Runtime &rt); jsi::Value reset(jsi::Runtime &rt); jsi::Value finalize(jsi::Runtime &rt); jsi::Value nChange(jsi::Runtime &rt); jsi::Value columnCount(jsi::Runtime &rt); jsi::Value columnName(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value rowValueKind(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value rowValueBytesCount(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value rowValueBytesPtr(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value rowValueText(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value rowValueInt(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value rowValueDouble(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value namedPosition(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value parametersCount(jsi::Runtime &rt); jsi::Value getAllRows(jsi::Runtime &rt); }; } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoSyncChangesHostObject.cpp ================================================ #include "TursoSyncChangesHostObject.h" extern "C" { #include } namespace turso { TursoSyncChangesHostObject::~TursoSyncChangesHostObject() { // Only deinit if not consumed (ownership not transferred to applyChanges) if (changes_ && !consumed_) { turso_sync_changes_deinit(changes_); changes_ = nullptr; } } void TursoSyncChangesHostObject::throwError(jsi::Runtime &rt, const char *error) { throw jsi::JSError(rt, error ? error : "Unknown error"); } jsi::Value TursoSyncChangesHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto propName = name.utf8(rt); // Currently, turso_sync_changes_t is mostly opaque in the C API // No methods exposed yet (like isEmpty) // This object is primarily meant to be passed to applyChanges() // If the C API adds methods in the future, they can be added here return jsi::Value::undefined(); } void TursoSyncChangesHostObject::set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) { // Read-only object } std::vector TursoSyncChangesHostObject::getPropertyNames(jsi::Runtime &rt) { return {}; } } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoSyncChangesHostObject.h ================================================ #pragma once #include #include #include // Forward declarations for Turso C API types extern "C" { struct turso_sync_changes; typedef struct turso_sync_changes turso_sync_changes_t; } namespace turso { using namespace facebook; /** * TursoSyncChangesHostObject wraps turso_sync_changes_t* (changes fetched from remote). * This is a THIN wrapper - 1:1 mapping of SDK-KIT C API with NO logic. * All logic belongs in TypeScript or Rust, not here. * * IMPORTANT: This object can be consumed by applyChanges(), after which it must NOT be used. */ class TursoSyncChangesHostObject : public jsi::HostObject { public: TursoSyncChangesHostObject(turso_sync_changes_t* changes) : changes_(changes), consumed_(false) {} ~TursoSyncChangesHostObject(); // JSI HostObject interface jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override; void set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) override; std::vector getPropertyNames(jsi::Runtime &rt) override; // Direct access to wrapped pointer (for internal use) turso_sync_changes_t* getChanges() const { return changes_; } // Mark this object as consumed (ownership transferred to applyChanges) void markConsumed() { consumed_ = true; } private: turso_sync_changes_t* changes_ = nullptr; bool consumed_ = false; // If true, changes ownership was transferred // Helper to throw JS errors void throwError(jsi::Runtime &rt, const char *error); // 1:1 C API mapping methods (NO logic - just calls through to C API) // Currently, there are no operations on turso_sync_changes_t other than passing it to applyChanges // The C API doesn't expose methods like isEmpty() yet, so this object is mostly opaque }; } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoSyncDatabaseHostObject.cpp ================================================ #include "TursoSyncDatabaseHostObject.h" #include "TursoSyncOperationHostObject.h" #include "TursoSyncIoItemHostObject.h" #include "TursoSyncChangesHostObject.h" extern "C" { #include } namespace turso { TursoSyncDatabaseHostObject::~TursoSyncDatabaseHostObject() { if (db_) { turso_sync_database_deinit(db_); db_ = nullptr; } } void TursoSyncDatabaseHostObject::throwError(jsi::Runtime &rt, const char *error) { throw jsi::JSError(rt, error ? error : "Unknown error"); } jsi::Value TursoSyncDatabaseHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto propName = name.utf8(rt); if (propName == "open") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->open(rt); } ); } if (propName == "create") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->create(rt); } ); } if (propName == "connect") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->connect(rt); } ); } if (propName == "stats") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->stats(rt); } ); } if (propName == "checkpoint") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->checkpoint(rt); } ); } if (propName == "pushChanges") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->pushChanges(rt); } ); } if (propName == "waitChanges") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->waitChanges(rt); } ); } if (propName == "applyChanges") { return jsi::Function::createFromHostFunction( rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->applyChanges(rt, args, count); } ); } if (propName == "ioTakeItem") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->ioTakeItem(rt); } ); } if (propName == "ioStepCallbacks") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->ioStepCallbacks(rt); } ); } if (propName == "close") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->close(rt); } ); } return jsi::Value::undefined(); } void TursoSyncDatabaseHostObject::set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) { // Read-only object } std::vector TursoSyncDatabaseHostObject::getPropertyNames(jsi::Runtime &rt) { std::vector props; props.emplace_back(jsi::PropNameID::forAscii(rt, "open")); props.emplace_back(jsi::PropNameID::forAscii(rt, "create")); props.emplace_back(jsi::PropNameID::forAscii(rt, "connect")); props.emplace_back(jsi::PropNameID::forAscii(rt, "stats")); props.emplace_back(jsi::PropNameID::forAscii(rt, "checkpoint")); props.emplace_back(jsi::PropNameID::forAscii(rt, "pushChanges")); props.emplace_back(jsi::PropNameID::forAscii(rt, "waitChanges")); props.emplace_back(jsi::PropNameID::forAscii(rt, "applyChanges")); props.emplace_back(jsi::PropNameID::forAscii(rt, "ioTakeItem")); props.emplace_back(jsi::PropNameID::forAscii(rt, "ioStepCallbacks")); props.emplace_back(jsi::PropNameID::forAscii(rt, "close")); return props; } // 1:1 C API mapping - NO logic, just calls through to C API jsi::Value TursoSyncDatabaseHostObject::open(jsi::Runtime &rt) { const turso_sync_operation_t* operation = nullptr; const char* error = nullptr; turso_status_code_t status = turso_sync_database_open(db_, &operation, &error); if (status != TURSO_OK) { throwError(rt, error); } // Wrap operation in TursoSyncOperationHostObject // Cast away const since we're transferring ownership auto operationObj = std::make_shared( const_cast(operation) ); return jsi::Object::createFromHostObject(rt, operationObj); } jsi::Value TursoSyncDatabaseHostObject::create(jsi::Runtime &rt) { const turso_sync_operation_t* operation = nullptr; const char* error = nullptr; turso_status_code_t status = turso_sync_database_create(db_, &operation, &error); if (status != TURSO_OK) { throwError(rt, error); } auto operationObj = std::make_shared( const_cast(operation) ); return jsi::Object::createFromHostObject(rt, operationObj); } jsi::Value TursoSyncDatabaseHostObject::connect(jsi::Runtime &rt) { const turso_sync_operation_t* operation = nullptr; const char* error = nullptr; turso_status_code_t status = turso_sync_database_connect(db_, &operation, &error); if (status != TURSO_OK) { throwError(rt, error); } auto operationObj = std::make_shared( const_cast(operation) ); return jsi::Object::createFromHostObject(rt, operationObj); } jsi::Value TursoSyncDatabaseHostObject::stats(jsi::Runtime &rt) { const turso_sync_operation_t* operation = nullptr; const char* error = nullptr; turso_status_code_t status = turso_sync_database_stats(db_, &operation, &error); if (status != TURSO_OK) { throwError(rt, error); } auto operationObj = std::make_shared( const_cast(operation) ); return jsi::Object::createFromHostObject(rt, operationObj); } jsi::Value TursoSyncDatabaseHostObject::checkpoint(jsi::Runtime &rt) { const turso_sync_operation_t* operation = nullptr; const char* error = nullptr; turso_status_code_t status = turso_sync_database_checkpoint(db_, &operation, &error); if (status != TURSO_OK) { throwError(rt, error); } auto operationObj = std::make_shared( const_cast(operation) ); return jsi::Object::createFromHostObject(rt, operationObj); } jsi::Value TursoSyncDatabaseHostObject::pushChanges(jsi::Runtime &rt) { const turso_sync_operation_t* operation = nullptr; const char* error = nullptr; turso_status_code_t status = turso_sync_database_push_changes(db_, &operation, &error); if (status != TURSO_OK) { throwError(rt, error); } auto operationObj = std::make_shared( const_cast(operation) ); return jsi::Object::createFromHostObject(rt, operationObj); } jsi::Value TursoSyncDatabaseHostObject::waitChanges(jsi::Runtime &rt) { const turso_sync_operation_t* operation = nullptr; const char* error = nullptr; turso_status_code_t status = turso_sync_database_wait_changes(db_, &operation, &error); if (status != TURSO_OK) { throwError(rt, error); } auto operationObj = std::make_shared( const_cast(operation) ); return jsi::Object::createFromHostObject(rt, operationObj); } jsi::Value TursoSyncDatabaseHostObject::applyChanges(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isObject()) { throw jsi::JSError(rt, "applyChanges: expected TursoSyncChanges object"); } // Extract the TursoSyncChangesHostObject from the JSI object auto changesHostObj = std::dynamic_pointer_cast( args[0].asObject(rt).asHostObject(rt) ); if (!changesHostObj) { throw jsi::JSError(rt, "applyChanges: invalid TursoSyncChanges object"); } const turso_sync_changes_t* changes = changesHostObj->getChanges(); const turso_sync_operation_t* operation = nullptr; const char* error = nullptr; turso_status_code_t status = turso_sync_database_apply_changes(db_, changes, &operation, &error); // Note: changes ownership is transferred to turso_sync_database_apply_changes // Mark the changes object as consumed so it won't try to deinit changesHostObj->markConsumed(); if (status != TURSO_OK) { throwError(rt, error); } auto operationObj = std::make_shared( const_cast(operation) ); return jsi::Object::createFromHostObject(rt, operationObj); } jsi::Value TursoSyncDatabaseHostObject::ioTakeItem(jsi::Runtime &rt) { const turso_sync_io_item_t* item = nullptr; const char* error = nullptr; turso_status_code_t status = turso_sync_database_io_take_item(db_, &item, &error); if (status != TURSO_OK) { throwError(rt, error); } // If no item available, return null if (!item) { return jsi::Value::null(); } auto itemObj = std::make_shared( const_cast(item) ); return jsi::Object::createFromHostObject(rt, itemObj); } jsi::Value TursoSyncDatabaseHostObject::ioStepCallbacks(jsi::Runtime &rt) { const char* error = nullptr; turso_status_code_t status = turso_sync_database_io_step_callbacks(db_, &error); if (status != TURSO_OK) { throwError(rt, error); } return jsi::Value::undefined(); } jsi::Value TursoSyncDatabaseHostObject::close(jsi::Runtime &rt) { // turso_sync_database_close doesn't exist in the C API // Closing happens in destructor via turso_sync_database_deinit return jsi::Value::undefined(); } } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoSyncDatabaseHostObject.h ================================================ #pragma once #include #include #include // Forward declarations for Turso C API types extern "C" { struct turso_sync_database; struct turso_sync_operation; struct turso_sync_io_item; struct turso_sync_changes; typedef struct turso_sync_database turso_sync_database_t; typedef struct turso_sync_operation turso_sync_operation_t; typedef struct turso_sync_io_item turso_sync_io_item_t; typedef struct turso_sync_changes turso_sync_changes_t; } namespace turso { using namespace facebook; /** * TursoSyncDatabaseHostObject wraps turso_sync_database_t* (sync SDK-KIT type for embedded replica). * This is a THIN wrapper - 1:1 mapping of SDK-KIT C API with NO logic. * All logic belongs in TypeScript or Rust, not here. */ class TursoSyncDatabaseHostObject : public jsi::HostObject { public: TursoSyncDatabaseHostObject(turso_sync_database_t* db) : db_(db) {} ~TursoSyncDatabaseHostObject(); // JSI HostObject interface jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override; void set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) override; std::vector getPropertyNames(jsi::Runtime &rt) override; // Direct access to wrapped pointer (for internal use) turso_sync_database_t* getSyncDatabase() const { return db_; } private: turso_sync_database_t* db_ = nullptr; // Helper to throw JS errors void throwError(jsi::Runtime &rt, const char *error); // 1:1 C API mapping methods (NO logic - just calls through to C API) jsi::Value open(jsi::Runtime &rt); jsi::Value create(jsi::Runtime &rt); jsi::Value connect(jsi::Runtime &rt); jsi::Value stats(jsi::Runtime &rt); jsi::Value checkpoint(jsi::Runtime &rt); jsi::Value pushChanges(jsi::Runtime &rt); jsi::Value waitChanges(jsi::Runtime &rt); jsi::Value applyChanges(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value ioTakeItem(jsi::Runtime &rt); jsi::Value ioStepCallbacks(jsi::Runtime &rt); jsi::Value close(jsi::Runtime &rt); }; } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoSyncIoItemHostObject.cpp ================================================ #include "TursoSyncIoItemHostObject.h" extern "C" { #include } namespace turso { TursoSyncIoItemHostObject::~TursoSyncIoItemHostObject() { if (item_) { turso_sync_database_io_item_deinit(item_); item_ = nullptr; } } void TursoSyncIoItemHostObject::throwError(jsi::Runtime &rt, const char *error) { throw jsi::JSError(rt, error ? error : "Unknown error"); } jsi::Value TursoSyncIoItemHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto propName = name.utf8(rt); if (propName == "getKind") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->getKind(rt); } ); } if (propName == "getHttpRequest") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->getHttpRequest(rt); } ); } if (propName == "getFullReadPath") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->getFullReadPath(rt); } ); } if (propName == "getFullWriteRequest") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->getFullWriteRequest(rt); } ); } if (propName == "poison") { return jsi::Function::createFromHostFunction( rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->poison(rt, args, count); } ); } if (propName == "setStatus") { return jsi::Function::createFromHostFunction( rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->setStatus(rt, args, count); } ); } if (propName == "pushBuffer") { return jsi::Function::createFromHostFunction( rt, name, 1, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *args, size_t count) -> jsi::Value { return this->pushBuffer(rt, args, count); } ); } if (propName == "done") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->done(rt); } ); } return jsi::Value::undefined(); } void TursoSyncIoItemHostObject::set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) { // Read-only object } std::vector TursoSyncIoItemHostObject::getPropertyNames(jsi::Runtime &rt) { std::vector props; props.emplace_back(jsi::PropNameID::forAscii(rt, "getKind")); props.emplace_back(jsi::PropNameID::forAscii(rt, "getHttpRequest")); props.emplace_back(jsi::PropNameID::forAscii(rt, "getFullReadPath")); props.emplace_back(jsi::PropNameID::forAscii(rt, "getFullWriteRequest")); props.emplace_back(jsi::PropNameID::forAscii(rt, "poison")); props.emplace_back(jsi::PropNameID::forAscii(rt, "setStatus")); props.emplace_back(jsi::PropNameID::forAscii(rt, "pushBuffer")); props.emplace_back(jsi::PropNameID::forAscii(rt, "done")); return props; } // 1:1 C API mapping - NO logic, just calls through to C API jsi::Value TursoSyncIoItemHostObject::getKind(jsi::Runtime &rt) { turso_sync_io_request_type_t kind = turso_sync_database_io_request_kind(item_); // Return string representation switch (kind) { case TURSO_SYNC_IO_HTTP: return jsi::String::createFromAscii(rt, "HTTP"); case TURSO_SYNC_IO_FULL_READ: return jsi::String::createFromAscii(rt, "FULL_READ"); case TURSO_SYNC_IO_FULL_WRITE: return jsi::String::createFromAscii(rt, "FULL_WRITE"); default: return jsi::String::createFromAscii(rt, "NONE"); } } jsi::Value TursoSyncIoItemHostObject::getHttpRequest(jsi::Runtime &rt) { turso_sync_io_http_request_t request; turso_status_code_t status = turso_sync_database_io_request_http(item_, &request); if (status != TURSO_OK) { throw jsi::JSError(rt, "Failed to get HTTP request"); } jsi::Object result(rt); // URL (may be null/empty) if (request.url.ptr && request.url.len > 0) { std::string url(static_cast(request.url.ptr), request.url.len); result.setProperty(rt, "url", jsi::String::createFromUtf8(rt, url)); } else { result.setProperty(rt, "url", jsi::Value::null()); } // Method if (request.method.ptr && request.method.len > 0) { std::string method(static_cast(request.method.ptr), request.method.len); result.setProperty(rt, "method", jsi::String::createFromUtf8(rt, method)); } // Path if (request.path.ptr && request.path.len > 0) { std::string path(static_cast(request.path.ptr), request.path.len); result.setProperty(rt, "path", jsi::String::createFromUtf8(rt, path)); } // Body (may be empty) if (request.body.ptr && request.body.len > 0) { // Create ArrayBuffer for body jsi::Function arrayBufferCtor = rt.global().getPropertyAsFunction(rt, "ArrayBuffer"); jsi::Object arrayBuffer = arrayBufferCtor.callAsConstructor(rt, static_cast(request.body.len)).asObject(rt); jsi::ArrayBuffer buf = arrayBuffer.getArrayBuffer(rt); memcpy(buf.data(rt), request.body.ptr, request.body.len); result.setProperty(rt, "body", arrayBuffer); } else { result.setProperty(rt, "body", jsi::Value::null()); } // Headers jsi::Object headers(rt); for (int32_t i = 0; i < request.headers; i++) { turso_sync_io_http_header_t header; turso_status_code_t header_status = turso_sync_database_io_request_http_header(item_, i, &header); if (header_status == TURSO_OK && header.key.ptr && header.value.ptr) { std::string key(static_cast(header.key.ptr), header.key.len); std::string value(static_cast(header.value.ptr), header.value.len); headers.setProperty(rt, key.c_str(), jsi::String::createFromUtf8(rt, value)); } } result.setProperty(rt, "headers", headers); return result; } jsi::Value TursoSyncIoItemHostObject::getFullReadPath(jsi::Runtime &rt) { turso_sync_io_full_read_request_t request; turso_status_code_t status = turso_sync_database_io_request_full_read(item_, &request); if (status != TURSO_OK) { throw jsi::JSError(rt, "Failed to get full read request"); } if (request.path.ptr && request.path.len > 0) { std::string path(static_cast(request.path.ptr), request.path.len); return jsi::String::createFromUtf8(rt, path); } return jsi::Value::null(); } jsi::Value TursoSyncIoItemHostObject::getFullWriteRequest(jsi::Runtime &rt) { turso_sync_io_full_write_request_t request; turso_status_code_t status = turso_sync_database_io_request_full_write(item_, &request); if (status != TURSO_OK) { throw jsi::JSError(rt, "Failed to get full write request"); } jsi::Object result(rt); // Path if (request.path.ptr && request.path.len > 0) { std::string path(static_cast(request.path.ptr), request.path.len); result.setProperty(rt, "path", jsi::String::createFromUtf8(rt, path)); } // Content if (request.content.ptr && request.content.len > 0) { jsi::Function arrayBufferCtor = rt.global().getPropertyAsFunction(rt, "ArrayBuffer"); jsi::Object arrayBuffer = arrayBufferCtor.callAsConstructor(rt, static_cast(request.content.len)).asObject(rt); jsi::ArrayBuffer buf = arrayBuffer.getArrayBuffer(rt); memcpy(buf.data(rt), request.content.ptr, request.content.len); result.setProperty(rt, "content", arrayBuffer); } else { result.setProperty(rt, "content", jsi::Value::null()); } return result; } jsi::Value TursoSyncIoItemHostObject::poison(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isString()) { throw jsi::JSError(rt, "poison: expected string argument (error message)"); } std::string error = args[0].asString(rt).utf8(rt); turso_slice_ref_t error_slice; error_slice.ptr = error.c_str(); error_slice.len = error.length(); turso_status_code_t status = turso_sync_database_io_poison(item_, &error_slice); if (status != TURSO_OK) { throw jsi::JSError(rt, "Failed to poison IO item"); } return jsi::Value::undefined(); } jsi::Value TursoSyncIoItemHostObject::setStatus(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isNumber()) { throw jsi::JSError(rt, "setStatus: expected number argument (status code)"); } int32_t status_code = static_cast(args[0].asNumber()); turso_status_code_t status = turso_sync_database_io_status(item_, status_code); if (status != TURSO_OK) { throw jsi::JSError(rt, "Failed to set IO status"); } return jsi::Value::undefined(); } jsi::Value TursoSyncIoItemHostObject::pushBuffer(jsi::Runtime &rt, const jsi::Value *args, size_t count) { if (count < 1 || !args[0].isObject()) { throw jsi::JSError(rt, "pushBuffer: expected ArrayBuffer argument"); } auto arrayBuffer = args[0].asObject(rt).getArrayBuffer(rt); const char* data = reinterpret_cast(arrayBuffer.data(rt)); size_t len = arrayBuffer.size(rt); turso_slice_ref_t buffer_slice; buffer_slice.ptr = data; buffer_slice.len = len; turso_status_code_t status = turso_sync_database_io_push_buffer(item_, &buffer_slice); if (status != TURSO_OK) { throw jsi::JSError(rt, "Failed to push buffer to IO item"); } return jsi::Value::undefined(); } jsi::Value TursoSyncIoItemHostObject::done(jsi::Runtime &rt) { turso_status_code_t status = turso_sync_database_io_done(item_); if (status != TURSO_OK) { throw jsi::JSError(rt, "Failed to mark IO item as done"); } return jsi::Value::undefined(); } } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoSyncIoItemHostObject.h ================================================ #pragma once #include #include #include // Forward declarations for Turso C API types extern "C" { struct turso_sync_io_item; typedef struct turso_sync_io_item turso_sync_io_item_t; } namespace turso { using namespace facebook; /** * TursoSyncIoItemHostObject wraps turso_sync_io_item_t* (IO queue item for fetch/fs operations). * This is a THIN wrapper - 1:1 mapping of SDK-KIT C API with NO logic. * All logic belongs in TypeScript or Rust, not here. */ class TursoSyncIoItemHostObject : public jsi::HostObject { public: TursoSyncIoItemHostObject(turso_sync_io_item_t* item) : item_(item) {} ~TursoSyncIoItemHostObject(); // JSI HostObject interface jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override; void set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) override; std::vector getPropertyNames(jsi::Runtime &rt) override; // Direct access to wrapped pointer (for internal use) turso_sync_io_item_t* getIoItem() const { return item_; } private: turso_sync_io_item_t* item_ = nullptr; // Helper to throw JS errors void throwError(jsi::Runtime &rt, const char *error); // 1:1 C API mapping methods (NO logic - just calls through to C API) jsi::Value getKind(jsi::Runtime &rt); jsi::Value getHttpRequest(jsi::Runtime &rt); jsi::Value getFullReadPath(jsi::Runtime &rt); jsi::Value getFullWriteRequest(jsi::Runtime &rt); jsi::Value poison(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value setStatus(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value pushBuffer(jsi::Runtime &rt, const jsi::Value *args, size_t count); jsi::Value done(jsi::Runtime &rt); }; } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoSyncOperationHostObject.cpp ================================================ #include "TursoSyncOperationHostObject.h" #include "TursoConnectionHostObject.h" #include "TursoSyncChangesHostObject.h" extern "C" { #include } namespace turso { TursoSyncOperationHostObject::~TursoSyncOperationHostObject() { if (op_) { turso_sync_operation_deinit(op_); op_ = nullptr; } } void TursoSyncOperationHostObject::throwError(jsi::Runtime &rt, const char *error) { throw jsi::JSError(rt, error ? error : "Unknown error"); } jsi::Value TursoSyncOperationHostObject::get(jsi::Runtime &rt, const jsi::PropNameID &name) { auto propName = name.utf8(rt); if (propName == "resume") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->resume(rt); } ); } if (propName == "resultKind") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->resultKind(rt); } ); } if (propName == "extractConnection") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->extractConnection(rt); } ); } if (propName == "extractChanges") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->extractChanges(rt); } ); } if (propName == "extractStats") { return jsi::Function::createFromHostFunction( rt, name, 0, [this](jsi::Runtime &rt, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value { return this->extractStats(rt); } ); } return jsi::Value::undefined(); } void TursoSyncOperationHostObject::set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) { // Read-only object } std::vector TursoSyncOperationHostObject::getPropertyNames(jsi::Runtime &rt) { std::vector props; props.emplace_back(jsi::PropNameID::forAscii(rt, "resume")); props.emplace_back(jsi::PropNameID::forAscii(rt, "resultKind")); props.emplace_back(jsi::PropNameID::forAscii(rt, "extractConnection")); props.emplace_back(jsi::PropNameID::forAscii(rt, "extractChanges")); props.emplace_back(jsi::PropNameID::forAscii(rt, "extractStats")); return props; } // 1:1 C API mapping - NO logic, just calls through to C API jsi::Value TursoSyncOperationHostObject::resume(jsi::Runtime &rt) { const char* error = nullptr; turso_status_code_t status = turso_sync_operation_resume(op_, &error); if (status != TURSO_OK && status != TURSO_DONE && status != TURSO_IO) { throwError(rt, error); } // Return status code (TURSO_DONE, TURSO_IO, etc.) return jsi::Value(static_cast(status)); } jsi::Value TursoSyncOperationHostObject::resultKind(jsi::Runtime &rt) { turso_sync_operation_result_type_t kind = turso_sync_operation_result_kind(op_); return jsi::Value(static_cast(kind)); } jsi::Value TursoSyncOperationHostObject::extractConnection(jsi::Runtime &rt) { const turso_connection_t* connection = nullptr; turso_status_code_t status = turso_sync_operation_result_extract_connection(op_, &connection); if (status != TURSO_OK) { throw jsi::JSError(rt, "Failed to extract connection from operation result"); } auto connectionObj = std::make_shared( const_cast(connection) ); return jsi::Object::createFromHostObject(rt, connectionObj); } jsi::Value TursoSyncOperationHostObject::extractChanges(jsi::Runtime &rt) { const turso_sync_changes_t* changes = nullptr; turso_status_code_t status = turso_sync_operation_result_extract_changes(op_, &changes); if (status != TURSO_OK) { throw jsi::JSError(rt, "Failed to extract changes from operation result"); } // If no changes, return null if (!changes) { return jsi::Value::null(); } auto changesObj = std::make_shared( const_cast(changes) ); return jsi::Object::createFromHostObject(rt, changesObj); } jsi::Value TursoSyncOperationHostObject::extractStats(jsi::Runtime &rt) { turso_sync_stats_t stats; turso_status_code_t status = turso_sync_operation_result_extract_stats(op_, &stats); if (status != TURSO_OK) { throw jsi::JSError(rt, "Failed to extract stats from operation result"); } // Convert stats to JS object jsi::Object result(rt); result.setProperty(rt, "cdcOperations", jsi::Value(static_cast(stats.cdc_operations))); result.setProperty(rt, "mainWalSize", jsi::Value(static_cast(stats.main_wal_size))); result.setProperty(rt, "revertWalSize", jsi::Value(static_cast(stats.revert_wal_size))); result.setProperty(rt, "lastPullUnixTime", jsi::Value(static_cast(stats.last_pull_unix_time))); result.setProperty(rt, "lastPushUnixTime", jsi::Value(static_cast(stats.last_push_unix_time))); result.setProperty(rt, "networkSentBytes", jsi::Value(static_cast(stats.network_sent_bytes))); result.setProperty(rt, "networkReceivedBytes", jsi::Value(static_cast(stats.network_received_bytes))); // Convert revision slice to string (if available) if (stats.revision.ptr && stats.revision.len > 0) { std::string revision(static_cast(stats.revision.ptr), stats.revision.len); result.setProperty(rt, "revision", jsi::String::createFromUtf8(rt, revision)); } else { result.setProperty(rt, "revision", jsi::Value::null()); } return result; } } // namespace turso ================================================ FILE: bindings/react-native/cpp/TursoSyncOperationHostObject.h ================================================ #pragma once #include #include #include // Forward declarations for Turso C API types extern "C" { struct turso_sync_operation; struct turso_connection; struct turso_sync_changes; typedef struct turso_sync_operation turso_sync_operation_t; typedef struct turso_connection turso_connection_t; typedef struct turso_sync_changes turso_sync_changes_t; } namespace turso { using namespace facebook; /** * TursoSyncOperationHostObject wraps turso_sync_operation_t* (async operation handle). * This is a THIN wrapper - 1:1 mapping of SDK-KIT C API with NO logic. * All logic belongs in TypeScript or Rust, not here. */ class TursoSyncOperationHostObject : public jsi::HostObject { public: TursoSyncOperationHostObject(turso_sync_operation_t* op) : op_(op) {} ~TursoSyncOperationHostObject(); // JSI HostObject interface jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override; void set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value) override; std::vector getPropertyNames(jsi::Runtime &rt) override; // Direct access to wrapped pointer (for internal use) turso_sync_operation_t* getOperation() const { return op_; } private: turso_sync_operation_t* op_ = nullptr; // Helper to throw JS errors void throwError(jsi::Runtime &rt, const char *error); // 1:1 C API mapping methods (NO logic - just calls through to C API) jsi::Value resume(jsi::Runtime &rt); jsi::Value resultKind(jsi::Runtime &rt); jsi::Value extractConnection(jsi::Runtime &rt); jsi::Value extractChanges(jsi::Runtime &rt); jsi::Value extractStats(jsi::Runtime &rt); }; } // namespace turso ================================================ FILE: bindings/react-native/ios/TursoModule.h ================================================ #import #import @interface Turso : NSObject @property (nonatomic, assign) BOOL setBridgeOnMainQueue; @end ================================================ FILE: bindings/react-native/ios/TursoModule.mm ================================================ #import "TursoModule.h" #import #import #import #import #import "TursoHostObject.h" @implementation Turso @synthesize bridge = _bridge; RCT_EXPORT_MODULE() + (BOOL)requiresMainQueueSetup { return YES; } - (NSDictionary *)constantsToExport { // Get documents directory NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsPath = [paths firstObject]; // Get library directory NSArray *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); NSString *libraryPath = [libraryPaths firstObject]; // Check for app group (for sharing data between app and extensions) NSString *appGroup = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"Turso_AppGroup"]; if (appGroup) { NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroup]; if (containerURL) { documentsPath = [containerURL path]; } } return @{ @"IOS_DOCUMENT_PATH": documentsPath ?: [NSNull null], @"IOS_LIBRARY_PATH": libraryPath ?: [NSNull null], @"ANDROID_DATABASE_PATH": [NSNull null], @"ANDROID_FILES_PATH": [NSNull null], @"ANDROID_EXTERNAL_FILES_PATH": [NSNull null] }; } - (void)setBridge:(RCTBridge *)bridge { _bridge = bridge; RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge; if (!cxxBridge.runtime) { return; } [self installLibrary]; } - (void)installLibrary { RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge; if (!cxxBridge.runtime) { return; } facebook::jsi::Runtime *runtime = (facebook::jsi::Runtime *)cxxBridge.runtime; if (!runtime) { return; } // Get the documents directory for database storage NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsPath = [paths firstObject]; // Check for app group (for sharing data between app and extensions) NSString *appGroup = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"Turso_AppGroup"]; if (appGroup) { NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroup]; if (containerURL) { documentsPath = [containerURL path]; } } // Get the call invoker auto callInvoker = _bridge.jsCallInvoker; // Install the Turso module turso::install(*runtime, callInvoker, [documentsPath UTF8String]); } // Synchronous method to check if the module is installed RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) { [self installLibrary]; return @YES; } @end ================================================ FILE: bindings/react-native/package.json ================================================ { "name": "@tursodatabase/sync-react-native", "version": "0.6.0-pre.4", "description": "React Native bindings for TursoDB", "main": "lib/commonjs/index.js", "module": "lib/module/index.js", "types": "lib/typescript/index.d.ts", "react-native": "src/index.ts", "source": "src/index.ts", "files": [ "src", "lib", "libs", "cpp", "ios", "android", "node/dist", "node/package.json", "*.podspec", "*.rb", "!ios/build", "!android/build", "!android/gradle", "!android/gradlew", "!android/gradlew.bat", "!android/local.properties", "!**/__tests__", "!**/__fixtures__", "!**/__mocks__", "!**/.*" ], "scripts": { "typescript": "tsc --noEmit", "lint": "eslint \"src/**/*.{ts,tsx}\"", "clean": "del-cli lib", "build": "bob build", "build:rust:ios": "make ios", "build:rust:android": "make android", "prepare": "bob build" }, "keywords": [ "react-native", "ios", "android", "sqlite", "database", "turso", "tursodb" ], "repository": { "type": "git", "url": "git+https://github.com/tursodatabase/turso.git", "directory": "bindings/react-native" }, "author": "Turso (https://turso.tech)", "license": "MIT", "bugs": { "url": "https://github.com/tursodatabase/turso/issues" }, "homepage": "https://github.com/tursodatabase/turso#readme", "publishConfig": { "registry": "https://registry.npmjs.org/" }, "devDependencies": { "@types/react": "^18.2.0", "del-cli": "^5.1.0", "eslint": "^8.57.0", "react": "^18.2.0", "react-native": "^0.76.0", "react-native-builder-bob": "^0.30.0", "typescript": "^5.3.0" }, "dependencies": { "@babel/runtime": "^7.25.0" }, "peerDependencies": { "react": ">=18.0.0", "react-native": ">=0.76.0" }, "engines": { "node": ">=18" }, "react-native-builder-bob": { "source": "src", "output": "lib", "targets": [ "commonjs", "module", [ "typescript", { "project": "tsconfig.build.json" } ] ] } } ================================================ FILE: bindings/react-native/src/AsyncLock.ts ================================================ /** * AsyncLock * * Simple FIFO queue-based async lock to serialize database operations. * Prevents concurrent access to the native SQLite connection. */ export class AsyncLock { locked: boolean; queue: any[]; constructor() { this.locked = false; this.queue = []; } async acquire() { if (!this.locked) { this.locked = true; return Promise.resolve(); } else { const block = new Promise((resolve) => { this.queue.push(resolve); }); return block; } } release() { if (this.locked == false) { throw new Error('invalid state: lock was already unlocked'); } const item = this.queue.shift(); if (item != null) { this.locked = true; item(); } else { this.locked = false; } } } ================================================ FILE: bindings/react-native/src/Database.ts ================================================ /** * Database * * Unified high-level API for both local and sync databases. * Constructor determines whether to use local-only or sync mode based on config. */ import { AsyncLock } from './AsyncLock'; import { Statement } from './Statement'; import type { NativeDatabase, NativeSyncDatabase, NativeConnection, Row, RunResult, BindParams, DatabaseOpts, SyncStats, EncryptionOpts, } from './types'; import { driveVoidOperation, driveConnectionOperation, driveChangesOperation, driveStatsOperation, } from './internal/asyncOperation'; import { drainSyncIo } from './internal/ioProcessor'; /** * Check if config has sync properties (url field) */ function isSyncConfig(opts: DatabaseOpts): boolean { return opts.url !== undefined && opts.url !== null; } /** * Calculate reserved bytes based on encryption cipher. * These values match the Turso Cloud encryption settings. */ function getReservedBytesForCipher(encryption: EncryptionOpts | undefined): number { if (!encryption) { return 0; } switch (encryption.cipher) { case 'aes256gcm': case 'aes128gcm': case 'chacha20poly1305': return 28; case 'aegis128l': case 'aegis128x2': case 'aegis128x4': return 32; case 'aegis256': case 'aegis256x2': case 'aegis256x4': return 48; default: return 0; } } /** * Database class - works for both local-only and sync databases * * All database operations are async to properly handle IO requirements: * - For local databases: async allows yielding to JS event loop * - For sync databases: async required for network operations * - For partial sync: async required to load missing pages on-demand */ export class Database { private _opts: DatabaseOpts; private _nativeDb: NativeDatabase | null = null; private _nativeSyncDb: NativeSyncDatabase | null = null; private _connection: NativeConnection | null = null; private _isSync = false; private _connected = false; private _closed = false; private _execLock: AsyncLock; private _extraIo?: () => Promise; private _ioContext?: { authToken?: string | (() => string | Promise | null); baseUrl?: string | (() => string | null); }; /** * Create a new database (doesn't connect yet - call connect()) * * @param opts - Database options */ constructor(opts: DatabaseOpts) { this._opts = opts; this._isSync = isSyncConfig(opts); this._execLock = new AsyncLock(); } /** * Connect to the database (matches JavaScript bindings) * For local databases: opens immediately * For sync databases: bootstraps if needed */ async connect(): Promise { if (this._connected) { return; } if (this._isSync) { await this.initSyncDatabase(); } else { this.initLocalDatabase(); } this._connected = true; } /** * Initialize local-only database */ private initLocalDatabase(): void { if (typeof __TursoProxy === 'undefined') { throw new Error('Turso native module not loaded'); } const dbConfig = { path: this._opts.path, async_io: false, // use blocking IO for local database }; // Create native database (path normalization happens in C++ JSI layer) this._nativeDb = __TursoProxy.newDatabase(this._opts.path, dbConfig); // Open database this._nativeDb.open(); // Get connection this._connection = this._nativeDb.connect(); } /** * Initialize sync database */ private async initSyncDatabase(): Promise { if (typeof __TursoProxy === 'undefined') { throw new Error('Turso native module not loaded'); } // Get URL (can be string or function) let url: string | null = null; if (typeof this._opts.url === 'function') { url = this._opts.url(); } else if (typeof this._opts.url === 'string') { url = this._opts.url; } // Build dbConfig (path normalization happens in C++ JSI layer) const dbConfig = { path: this._opts.path, async_io: true, // use async IO for synced database as we have network IO loop externally from the turso core }; // Calculate reserved bytes from cipher const reservedBytes = getReservedBytesForCipher(this._opts.remoteEncryption); // Build syncConfig with all options const syncConfig: any = { remoteUrl: url, clientName: this._opts.clientName || 'turso-sync-react-native', longPollTimeoutMs: this._opts.longPollTimeoutMs, bootstrapIfEmpty: this._opts.bootstrapIfEmpty ?? true, reservedBytes: reservedBytes, // Remote encryption options (key is passed to sync engine for HTTP headers) remoteEncryptionKey: this._opts.remoteEncryption?.key, remoteEncryptionCipher: this._opts.remoteEncryption?.cipher, }; // Add partial sync options if present if (this._opts.partialSyncExperimental) { const partial = this._opts.partialSyncExperimental; if (partial.bootstrapStrategy.kind === 'prefix') { syncConfig.partialBootstrapStrategyPrefix = partial.bootstrapStrategy.length; } else if (partial.bootstrapStrategy.kind === 'query') { syncConfig.partialBootstrapStrategyQuery = partial.bootstrapStrategy.query; } syncConfig.partialBootstrapSegmentSize = partial.segmentSize; syncConfig.partialBootstrapPrefetch = partial.prefetch; } // Create native sync database this._nativeSyncDb = __TursoProxy.newSyncDatabase(dbConfig, syncConfig); // Create IO context with auth token and base URL this._ioContext = { authToken: this._opts.authToken, baseUrl: this._opts.url, }; // Create extraIo callback for partial sync support // This callback drains the sync engine's IO queue during statement execution this._extraIo = async () => { if (this._nativeSyncDb && this._ioContext) { await drainSyncIo(this._nativeSyncDb, this._ioContext); } }; // Bootstrap/open database const operation = this._nativeSyncDb.create(); await driveVoidOperation(operation, this._nativeSyncDb, this._ioContext); // Get connection const connOperation = this._nativeSyncDb.connect(); this._connection = await driveConnectionOperation(connOperation, this._nativeSyncDb, this._ioContext); } /** * Prepare a SQL statement * * @param sql - SQL statement to prepare * @returns Prepared statement */ prepare(sql: string): Statement { this.checkOpen(); if (!this._connection) { throw new Error('No connection available'); } const nativeStmt = this._connection.prepareSingle(sql); return new Statement(nativeStmt, this._connection!, this._execLock, this._extraIo); } /** * Execute SQL without returning results (for DDL, multi-statement SQL) * * @param sql - SQL to execute */ async exec(sql: string): Promise { this.checkOpen(); if (!this._connection) { throw new Error('No connection available'); } await this._execLock.acquire(); try { // Use prepareFirst to handle multiple statements let remaining = sql.trim(); while (remaining.length > 0) { const result = this._connection.prepareFirst(remaining); if (!result) { break; // No more statements (C++ returns null when nothing to parse) } // Wrap in Statement to get IO handling (no lock — we already hold it) const stmt = new Statement(result.statement, this._connection!, null, this._extraIo); try { // Execute - will handle IO if needed await stmt.rawRun(); } finally { stmt.finalize(); } // Move to next statement remaining = sql.substring(result.tailIdx).trim(); } } finally { this._execLock.release(); } } /** * Execute statement and return result info * * @param sql - SQL statement * @param params - Bind parameters * @returns Run result with changes and lastInsertRowid */ async run(sql: string, ...params: BindParams[]): Promise { const stmt = this.prepare(sql); try { return await stmt.run(...params); } finally { stmt.finalize(); } } /** * Execute query and return first row * * @param sql - SQL query * @param params - Bind parameters * @returns First row or undefined */ async get(sql: string, ...params: BindParams[]): Promise { const stmt = this.prepare(sql); try { return await stmt.get(...params); } finally { stmt.finalize(); } } /** * Execute query and return all rows * * @param sql - SQL query * @param params - Bind parameters * @returns All rows */ async all(sql: string, ...params: BindParams[]): Promise { const stmt = this.prepare(sql); try { return await stmt.all(...params); } finally { stmt.finalize(); } } /** * Execute function within a transaction * * @param fn - Function to execute * @returns Function result */ async transaction(fn: () => T | Promise): Promise { this.checkOpen(); await this.exec('BEGIN'); try { const result = await fn(); await this.exec('COMMIT'); return result; } catch (error) { await this.exec('ROLLBACK'); throw error; } } /** * Push local changes to remote (sync databases only) */ async push(): Promise { if (!this._isSync || !this._nativeSyncDb || !this._ioContext) { throw new Error('push() is only available for sync databases'); } const operation = this._nativeSyncDb.pushChanges(); await driveVoidOperation(operation, this._nativeSyncDb, this._ioContext); } /** * Pull remote changes and apply locally (sync databases only) * * @returns true if changes were applied, false if no changes */ async pull(): Promise { if (!this._isSync || !this._nativeSyncDb || !this._ioContext) { throw new Error('pull() is only available for sync databases'); } // Wait for changes const waitOperation = this._nativeSyncDb.waitChanges(); const changes = await driveChangesOperation(waitOperation, this._nativeSyncDb, this._ioContext); // If no changes, return false if (!changes) { return false; } // Apply changes const applyOperation = this._nativeSyncDb.applyChanges(changes); await driveVoidOperation(applyOperation, this._nativeSyncDb, this._ioContext); return true; } /** * Get sync statistics (sync databases only) * * @returns Sync stats */ async stats(): Promise { if (!this._isSync || !this._nativeSyncDb || !this._ioContext) { throw new Error('stats() is only available for sync databases'); } const operation = this._nativeSyncDb.stats(); return driveStatsOperation(operation, this._nativeSyncDb, this._ioContext); } /** * Checkpoint database (sync databases only) */ async checkpoint(): Promise { if (!this._isSync || !this._nativeSyncDb || !this._ioContext) { throw new Error('checkpoint() is only available for sync databases'); } const operation = this._nativeSyncDb.checkpoint(); await driveVoidOperation(operation, this._nativeSyncDb, this._ioContext); } /** * Close the database */ close(): void { if (this._closed) { return; } if (this._connection) { this._connection.close(); this._connection = null; } if (this._nativeDb) { this._nativeDb.close(); this._nativeDb = null; } if (this._nativeSyncDb) { this._nativeSyncDb.close(); this._nativeSyncDb = null; } this._connected = false; this._closed = true; } /** * Get database path */ get path(): string { return this._opts.path; } /** * Check if database is a sync database */ get isSync(): boolean { return this._isSync; } /** * Check if database is open */ get open(): boolean { return !this._closed && this._connection !== null; } /** * Check if in transaction */ get inTransaction(): boolean { if (!this._connection) { return false; } return !this._connection.getAutocommit(); } /** * Get last insert rowid */ get lastInsertRowid(): number { if (!this._connection) { return 0; } return this._connection.lastInsertRowid(); } /** * Check if open and throw if not */ private checkOpen(): void { if (this._closed) { throw new Error('Database is closed'); } if (!this._connected || !this._connection) { throw new Error('Database not connected. Call connect() first.'); } } } ================================================ FILE: bindings/react-native/src/Statement.ts ================================================ /** * Statement * * High-level wrapper around NativeStatement providing a clean API. * Handles parameter binding, row conversion, and result collection. */ import type { AsyncLock } from './AsyncLock'; import type { NativeConnection, NativeStatement, SQLiteValue, BindParams, Row, RunResult, } from './types'; import { TursoStatus, TursoType } from './types'; /** * Prepared SQL statement */ export class Statement { private _statement: NativeStatement; private _connection: NativeConnection; private _execLock: AsyncLock | null; private _finalized = false; private _extraIo?: () => Promise; constructor(statement: NativeStatement, connection: NativeConnection, execLock: AsyncLock | null, extraIo?: () => Promise) { this._statement = statement; this._connection = connection; this._execLock = execLock; this._extraIo = extraIo; } /** * Bind parameters to the statement * * @param params - Parameters to bind (array, object, or single value) * @returns this for chaining */ bind(...params: BindParams[]): this { if (this._finalized) { throw new Error('Statement has been finalized'); } // Flatten parameters if single array passed let flatParams: SQLiteValue[]; if (params.length === 1 && Array.isArray(params[0])) { flatParams = params[0]; } else if (params.length === 1 && typeof params[0] === 'object' && params[0] !== null) { // Named parameters const namedParams = params[0] as Record; this.bindNamed(namedParams); return this; } else { flatParams = params as SQLiteValue[]; } // Bind positional parameters this.bindPositional(flatParams); return this; } /** * Bind positional parameters (1-indexed) * * @param params - Array of values to bind */ private bindPositional(params: SQLiteValue[]): void { for (let i = 0; i < params.length; i++) { const position = i + 1; // 1-indexed const value = params[i]!; this.bindValue(position, value); } } /** * Bind named parameters * * @param params - Object with named parameters */ private bindNamed(params: Record): void { for (const [name, value] of Object.entries(params)) { // Get position for named parameter const position = this._statement.namedPosition(name); if (position < 0) { throw new Error(`Unknown parameter name: ${name}`); } this.bindValue(position, value); } } /** * Bind a single value at a position * * @param position - 1-indexed position * @param value - Value to bind */ private bindValue(position: number, value: SQLiteValue): void { if (value === null || value === undefined) { this._statement.bindPositionalNull(position); } else if (typeof value === 'number') { // Check if integer or float if (Number.isInteger(value)) { this._statement.bindPositionalInt(position, value); } else { this._statement.bindPositionalDouble(position, value); } } else if (typeof value === 'string') { this._statement.bindPositionalText(position, value); } else if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { const buffer = value as unknown as ArrayBuffer; this._statement.bindPositionalBlob(position, buffer); } else { throw new Error(`Unsupported parameter type: ${typeof value}`); } } /** * Execute statement without returning rows (for INSERT, UPDATE, DELETE) * * @param params - Optional parameters to bind * @returns Result with changes and lastInsertRowid */ async run(...params: BindParams[]): Promise { if (this._finalized) { throw new Error('Statement has been finalized'); } if (this._execLock) { await this._execLock.acquire(); } try { // Bind parameters inside the lock to prevent concurrent bind/execute races if (params.length > 0) { this.bind(...params); } return await this._runInner(); } finally { this._statement.reset(); if (this._execLock) { this._execLock.release(); } } } /** * Execute without acquiring the lock (caller already holds it). * Used by Database.exec() and Transaction which acquire the lock once. */ async rawRun(): Promise { if (this._finalized) { throw new Error('Statement has been finalized'); } try { return await this._runInner(); } finally { this._statement.reset(); } } private async _runInner(): Promise { // Execute statement with IO handling const result = await this.executeWithIo(); return { changes: result.rowsChanged, lastInsertRowid: this._connection ? this._connection.lastInsertRowid() : 0, }; } /** * Execute statement handling potential IO (for partial sync) * Matches Python's _run_execute_with_io pattern * * @returns Execution result */ private async executeWithIo(): Promise<{ status: number; rowsChanged: number }> { while (true) { const result = this._statement.execute(); if (result.status === TursoStatus.IO) { // Statement needs IO (e.g., loading missing pages with partial sync) this._statement.runIo(); // Drain sync engine IO queue if (this._extraIo) { await this._extraIo(); } continue; } if (result.status !== TursoStatus.DONE) { throw new Error(`Statement execution failed with status: ${result.status}`); } return result; } } /** * Step statement once handling potential IO (for partial sync) * Matches Python's _step_once_with_io pattern * * @returns Status code */ private async stepWithIo(): Promise { while (true) { const status = this._statement.step(); if (status === TursoStatus.IO) { // Statement needs IO (e.g., loading missing pages with partial sync) this._statement.runIo(); // Drain sync engine IO queue if (this._extraIo) { await this._extraIo(); } continue; } return status; } } /** * Execute statement and return first row * * @param params - Optional parameters to bind * @returns First row or undefined */ async get(...params: BindParams[]): Promise { if (this._finalized) { throw new Error('Statement has been finalized'); } if (this._execLock) { await this._execLock.acquire(); } try { // Bind parameters inside the lock to prevent concurrent bind/execute races if (params.length > 0) { this.bind(...params); } // Step once with async IO handling const status = await this.stepWithIo(); if (status === TursoStatus.ROW) { const row = this.readRow(); return row; } if (status === TursoStatus.DONE) { return undefined; } throw new Error(`Statement step failed with status: ${status}`); } finally { this._statement.reset(); if (this._execLock) { this._execLock.release(); } } } /** * Execute statement and return all rows * * @param params - Optional parameters to bind * @returns Array of rows */ async all(...params: BindParams[]): Promise { if (this._finalized) { throw new Error('Statement has been finalized'); } if (this._execLock) { await this._execLock.acquire(); } try { // Bind parameters inside the lock to prevent concurrent bind/execute races if (params.length > 0) { this.bind(...params); } // Fast path: native bulk read (handles step+read loop in C++) // Re-enters native after each IO resolution to stay on the fast path let rows: Row[] = []; const MAX_IO_RETRIES = 1000000; for (let ioRetries = 0; ioRetries < MAX_IO_RETRIES; ioRetries++) { const bulk = this._statement.getAllRows(); if (bulk.rows && bulk.rows.length > 0) { rows = rows.concat(bulk.rows); } if (bulk.status === TursoStatus.DONE) { return rows; } if (bulk.status === TursoStatus.IO) { this._statement.runIo(); if (this._extraIo) { await this._extraIo(); } continue; } throw new Error(`getAllRows failed with status: ${bulk.status}`); } throw new Error(`getAllRows: exceeded ${MAX_IO_RETRIES} IO retries`); } finally { this._statement.reset(); if (this._execLock) { this._execLock.release(); } } } /** * Read current row into an object * * @returns Row object with column name keys */ private readRow(): Row { const row: Row = {}; const columnCount = this._statement.columnCount(); for (let i = 0; i < columnCount; i++) { const name = this._statement.columnName(i); if (!name) { throw new Error(`Failed to get column name at index ${i}`); } const value = this.readColumnValue(i); row[name] = value; } return row; } /** * Read value at column index * * @param index - Column index * @returns Column value */ private readColumnValue(index: number): SQLiteValue { const kind = this._statement.rowValueKind(index); switch (kind) { case TursoType.NULL: return null; case TursoType.INTEGER: return this._statement.rowValueInt(index); case TursoType.REAL: return this._statement.rowValueDouble(index); case TursoType.TEXT: // Use rowValueText which directly returns a string from C++ (avoids encoding issues) return this._statement.rowValueText(index); case TursoType.BLOB: return this._statement.rowValueBytesPtr(index) || new ArrayBuffer(0); default: throw new Error(`Unknown column type: ${kind}`); } } /** * Reset statement for re-execution * * @returns this for chaining */ reset(): this { if (this._finalized) { throw new Error('Statement has been finalized'); } this._statement.reset(); return this; } /** * Finalize and release statement resources */ async finalize(): Promise { if (this._finalized) { return; } if (this._execLock) { await this._execLock.acquire(); } try { while (true) { const status = this._statement.finalize(); if (status === TursoStatus.IO) { // Statement needs IO (e.g., loading missing pages with partial sync) this._statement.runIo(); // Drain sync engine IO queue if (this._extraIo) { await this._extraIo(); } continue; } if (status !== TursoStatus.DONE) { throw new Error(`Statement finalization failed with status: ${status}`); } break; } this._finalized = true; } finally { if (this._execLock) { this._execLock.release(); } } } /** * Check if statement has been finalized */ get finalized(): boolean { return this._finalized; } } ================================================ FILE: bindings/react-native/src/index.ts ================================================ /** * Turso React Native SDK * * Main entry point for the SDK. Supports both local-only and sync databases. */ import { NativeModules } from 'react-native'; import { Database } from './Database'; import type { DatabaseOpts, TursoLoggerFn, TursoNativeModule, TursoProxy as TursoProxyType, } from './types'; import { setFileSystemImpl } from './internal/ioProcessor'; // Re-export all public types export type { // Core types SQLiteValue, BindParams, Row, RunResult, // Database config DatabaseOpts, EncryptionOpts, // Sync types SyncStats, // Logging TursoLog, TursoLoggerFn, TursoTracingLevel, // Enums TursoStatus, TursoType, } from './types'; // Re-export classes export { Database } from './Database'; export { Statement } from './Statement'; // Export file system configuration function export { setFileSystemImpl } from './internal/ioProcessor'; // Get the native module const TursoNative: TursoNativeModule | undefined = NativeModules.Turso; // Check if native module is available if (!TursoNative) { throw new Error( `@tursodatabase/sync-react-native: Native module not found. Make sure you have properly linked the library.\n` + `- iOS: Run 'pod install' in your ios directory\n` + `- Android: Make sure the package is properly included in your MainApplication.java` ); } // Install the JSI bindings const installed = TursoNative.install(); if (!installed) { throw new Error( '@tursodatabase/sync-react-native: Failed to install JSI bindings. Make sure the New Architecture is enabled.' ); } // Get the proxy that was installed on the global object // __TursoProxy is declared globally in types.ts const TursoProxy: TursoProxyType = __TursoProxy; if (!TursoProxy) { throw new Error( '@tursodatabase/sync-react-native: JSI bindings not found on global object. This is a bug.' ); } /** * Helper function to construct a database path in a writable directory. * * @param filename - Database filename (e.g., 'mydb.db') * @returns Absolute path to the database file * * @example * ```ts * import { getDbPath, connect } from '@tursodatabase/sync-react-native'; * * const dbPath = getDbPath('mydb.db'); * const db = await connect({ path: dbPath }); * ``` */ export function getDbPath(filename: string): string { const basePath = paths.database; if (!basePath || basePath === '.') { throw new Error( 'Unable to get database path for this platform. ' + 'Make sure the native module is properly loaded.' ); } return `${basePath}/${filename}`; } /** * Connect to a database asynchronously (matches JavaScript bindings API) * * This is the main entry point for the SDK, matching the API from * @tursodatabase/sync-native and @tursodatabase/database-native. * * **Path handling**: Relative paths are automatically placed in writable directories: * - Android: app's database directory (`/data/data/com.app/databases/`) * - iOS: app's documents directory * * Absolute paths and `:memory:` are used as-is. * * @param opts - Database options * @returns Promise resolving to Database instance * * @example Local database (relative path) * ```ts * import { connect } from '@tursodatabase/sync-react-native'; * * // Relative path automatically placed in writable directory * const db = await connect({ path: 'local.db' }); * await db.exec('CREATE TABLE users (id INTEGER, name TEXT)'); * ``` * * @example Using :memory: for in-memory database * ```ts * const db = await connect({ path: ':memory:' }); * ``` * * @example Sync database * ```ts * const db = await connect({ * path: 'replica.db', * url: 'libsql://mydb.turso.io', * authToken: 'token-here', * }); * const users = await db.all('SELECT * FROM users'); * await db.push(); * await db.pull(); * ``` * * @example Using absolute path (advanced) * ```ts * import { connect, paths } from '@tursodatabase/sync-react-native'; * * const db = await connect({ path: `${paths.database}/mydb.db` }); * ``` */ export async function connect(opts: DatabaseOpts): Promise { const db = new Database(opts); await db.connect(); return db; } /** * Returns the Turso library version. */ export function version(): string { return TursoProxy.version(); } /** * Configure Turso settings such as logging. * Should be called before any database operations. * * @param options - Configuration options * @example * ```ts * import { setup } from '@tursodatabase/sync-react-native'; * * setup({ * logLevel: 'debug', * logger: (log) => { * console.log(`[${log.level}] ${log.target}: ${log.message}`); * }, * }); * ``` */ export function setup(options: {logLevel?: string; logger?: TursoLoggerFn}): void { TursoProxy.setup(options); } /** * Platform-specific writable directory path for database files. * * NOTE: With automatic path normalization, you typically don't need this. * Just pass relative paths like 'mydb.db' and they'll be placed in the correct directory. * * @example * ```ts * import { paths, connect } from '@tursodatabase/sync-react-native'; * * const dbPath = `${paths.database}/mydb.db`; * const db = await connect({ path: dbPath }); * ``` */ export const paths = { /** * Writable directory for database files. * - iOS: App's Documents directory * - Android: App's database directory */ get database(): string { return TursoNative?.IOS_DOCUMENT_PATH || TursoNative?.ANDROID_DATABASE_PATH || '.'; }, }; // Default export export default { connect, version, setup, setFileSystemImpl, getDbPath, paths, Database, }; ================================================ FILE: bindings/react-native/src/internal/asyncOperation.ts ================================================ /** * Async Operation Driver * * Drives async operations returned by sync SDK-KIT methods. * This is where ALL the async logic lives - the C++ layer is just a thin bridge. * * Key responsibilities: * - Call resume() in a loop until DONE * - When IO is needed, process all pending IO items * - Extract and return the final result */ import type { NativeSyncOperation, NativeSyncDatabase, NativeConnection, NativeSyncChanges, SyncStats, } from '../types'; import { TursoStatus, SyncOperationResultType } from '../types'; import { processIoItem } from './ioProcessor'; import type { IoContext } from './ioProcessor'; /** * Drive an async operation to completion * * @param operation - The native operation to drive * @param database - The native sync database (for IO queue access) * @param context - IO context with auth and URL information * @returns Promise that resolves when operation completes */ export async function driveOperation( operation: NativeSyncOperation, database: NativeSyncDatabase, context: IoContext ): Promise { while (true) { // Resume the operation const status = operation.resume(); // Operation completed successfully if (status === TursoStatus.DONE) { // Extract and return the result based on result type const resultKind = operation.resultKind(); switch (resultKind) { case SyncOperationResultType.NONE: return undefined as T; case SyncOperationResultType.CONNECTION: return operation.extractConnection() as T; case SyncOperationResultType.CHANGES: return operation.extractChanges() as T; case SyncOperationResultType.STATS: return operation.extractStats() as T; default: throw new Error(`Unknown result type: ${resultKind}`); } } // Operation needs IO if (status === TursoStatus.IO) { // Process all pending IO items await processIoQueue(database, context); // Step callbacks after IO processing database.ioStepCallbacks(); // Continue resume loop continue; } // Any other status is an error throw new Error(`Unexpected status from operation.resume(): ${status}`); } } /** * Process all pending IO items in the queue * * @param database - The native sync database * @param context - IO context with auth and URL information */ async function processIoQueue(database: NativeSyncDatabase, context: IoContext): Promise { const promises: Promise[] = []; // Take all available IO items from the queue while (true) { const ioItem = database.ioTakeItem(); if (!ioItem) { break; // No more items } // Process each item (potentially in parallel) promises.push(processIoItem(ioItem, context)); } // Wait for all IO operations to complete await Promise.all(promises); } /** * Helper type for operations that return connections */ export async function driveConnectionOperation( operation: NativeSyncOperation, database: NativeSyncDatabase, context: IoContext ): Promise { return driveOperation(operation, database, context); } /** * Helper type for operations that return changes */ export async function driveChangesOperation( operation: NativeSyncOperation, database: NativeSyncDatabase, context: IoContext ): Promise { return driveOperation(operation, database, context); } /** * Helper type for operations that return stats */ export async function driveStatsOperation( operation: NativeSyncOperation, database: NativeSyncDatabase, context: IoContext ): Promise { return driveOperation(operation, database, context); } /** * Helper type for operations that return void */ export async function driveVoidOperation( operation: NativeSyncOperation, database: NativeSyncDatabase, context: IoContext ): Promise { return driveOperation(operation, database, context); } ================================================ FILE: bindings/react-native/src/internal/ioProcessor.ts ================================================ /** * IO Processor * * Processes IO requests from the sync engine using JavaScript APIs. * This is the key benefit of the thin JSI layer - all IO is handled by * React Native's standard APIs (fetch, file system), not C++ code. * * Benefits: * - Network requests visible in React Native debugger * - Can customize fetch (add proxies, custom headers, etc.) * - Easier to test (can mock fetch) * - Uses platform-native networking (not C++ HTTP libraries) */ import type { NativeSyncIoItem, NativeSyncDatabase } from '../types'; /** * IO context contains auth and URL information for HTTP requests */ export interface IoContext { /** Auth token for HTTP requests */ authToken?: string | (() => string | Promise | null); /** Base URL for normalization (e.g., 'libsql://mydb.turso.io') */ baseUrl?: string | (() => string | null); } // Allow users to optionally override file system implementation let fsReadFileOverride: ((path: string) => Promise) | null = null; let fsWriteFileOverride: ((path: string, data: Uint8Array) => Promise) | null = null; /** * Set custom file system implementation (optional) * By default, uses built-in JSI file system functions. * Only call this if you need custom behavior (e.g., encryption, compression). * * @param readFile - Function to read file as Uint8Array * @param writeFile - Function to write Uint8Array to file */ export function setFileSystemImpl( readFile: (path: string) => Promise, writeFile: (path: string, data: Uint8Array) => Promise ): void { fsReadFileOverride = readFile; fsWriteFileOverride = writeFile; } /** * Read file using custom implementation or built-in JSI function */ async function fsReadFile(path: string): Promise { if (fsReadFileOverride) { return fsReadFileOverride(path); } // Use built-in JSI function const buffer = __TursoProxy.fsReadFile(path); if (buffer === null) { // File not found - return empty return new Uint8Array(0); } return new Uint8Array(buffer); } /** * Write file using custom implementation or built-in JSI function */ async function fsWriteFile(path: string, data: Uint8Array): Promise { if (fsWriteFileOverride) { return fsWriteFileOverride(path, data); } // Use built-in JSI function __TursoProxy.fsWriteFile(path, data.buffer as ArrayBuffer); } /** * Process a single IO item * * @param item - The IO item to process * @param context - IO context with auth and URL information */ export async function processIoItem(item: NativeSyncIoItem, context: IoContext): Promise { try { const kind = item.getKind(); switch (kind) { case 'HTTP': await processHttpRequest(item, context); break; case 'FULL_READ': await processFullRead(item); break; case 'FULL_WRITE': await processFullWrite(item); break; case 'NONE': default: // Nothing to do item.done(); break; } } catch (error) { // Poison the item with error message const errorMsg = error instanceof Error ? error.message : String(error); item.poison(errorMsg); } } /** * Normalize URL from libsql:// to https:// */ function normalizeUrl(url: string): string { if (url.startsWith('libsql://')) { return url.replace('libsql://', 'https://'); } return url; } /** * Get auth token from context (handles both string and async function) */ async function getAuthToken(context: IoContext): Promise { if (!context.authToken) { return null; } if (typeof context.authToken === 'function') { const result = await context.authToken(); return result ?? null; } return context.authToken; } /** * Get base URL from context (handles both string and function) */ function getBaseUrl(context: IoContext): string | null { if (!context.baseUrl) { return null; } if (typeof context.baseUrl === 'function') { return context.baseUrl() ?? null; } return context.baseUrl; } /** * Process an HTTP request using fetch() * * @param item - The IO item * @param context - IO context with auth and URL information */ async function processHttpRequest(item: NativeSyncIoItem, context: IoContext): Promise { const request = item.getHttpRequest(); // Resolve base URL: prefer context.baseUrl (from opts), fall back to request.url const rawBaseUrl = getBaseUrl(context) ?? request.url; if (!rawBaseUrl) { throw new Error('HTTP request missing URL: no URL in request and no baseUrl in context'); } // Normalize URL (libsql:// -> https://) const baseUrl = normalizeUrl(rawBaseUrl); // Build full URL by combining base URL with path let fullUrl = baseUrl; if (request.path) { // Ensure proper URL formatting (avoid double slashes, ensure single slash) if (baseUrl.endsWith('/') && request.path.startsWith('/')) { fullUrl = baseUrl + request.path.substring(1); } else if (!baseUrl.endsWith('/') && !request.path.startsWith('/')) { fullUrl = baseUrl + '/' + request.path; } else { fullUrl = baseUrl + request.path; } } // Build fetch options const options: RequestInit = { method: request.method, headers: { ...request.headers }, }; // Inject Authorization header if auth token is available const authToken = await getAuthToken(context); if (authToken) { (options.headers as Record)['Authorization'] = `Bearer ${authToken}`; } // Add body if present if (request.body) { options.body = request.body; } // Make the HTTP request let response; try { response = await fetch(fullUrl, options); } catch (e) { // Detailed error logging const errorDetails = { url: fullUrl, method: request.method, hasBody: !!request.body, bodySize: request.body ? request.body.byteLength : 0, bodyType: request.body ? Object.prototype.toString.call(options.body) : 'none', error: e instanceof Error ? { message: e.message, name: e.name, stack: e.stack, } : String(e), }; console.error('[Turso HTTP] Request failed:', JSON.stringify(errorDetails, null, 2)); throw new Error(`HTTP request failed: ${e instanceof Error ? e.message : String(e)}. URL: ${fullUrl}, Method: ${request.method}, Body size: ${request.body ? request.body.byteLength : 0} bytes`); } // Set status code item.setStatus(response.status); // Read response body and push to item const responseData = await response.arrayBuffer(); if (responseData.byteLength > 0) { item.pushBuffer(responseData); } // Mark as done item.done(); } /** * Process a full read request (atomic file read) * * @param item - The IO item */ async function processFullRead(item: NativeSyncIoItem): Promise { const path = item.getFullReadPath(); try { // Read the file (uses built-in JSI function or custom override) const data = await fsReadFile(path); // Push data to item if (data.byteLength > 0) { item.pushBuffer(data.buffer as ArrayBuffer); } // Mark as done item.done(); } catch (error) { // File not found is okay - treat as empty file if (isFileNotFoundError(error)) { // Empty file - just mark as done without pushing data item.done(); } else { throw error; } } } /** * Process a full write request (atomic file write) * * @param item - The IO item */ async function processFullWrite(item: NativeSyncIoItem): Promise { const request = item.getFullWriteRequest(); // Convert ArrayBuffer to Uint8Array const data = request.content ? new Uint8Array(request.content) : new Uint8Array(0); // Write the file atomically (uses built-in JSI function or custom override) await fsWriteFile(request.path, data); // Mark as done item.done(); } /** * Check if error is a file-not-found error * * @param error - The error to check * @returns true if file not found */ function isFileNotFoundError(error: unknown): boolean { if (error instanceof Error) { const message = error.message.toLowerCase(); return ( message.includes('enoent') || message.includes('not found') || message.includes('no such file') ); } return false; } /** * Drain all pending IO items from sync engine queue and process them. * This is called during statement execution when partial sync needs to load missing pages. * * @param database - The native sync database * @param context - IO context with auth and URL information */ export async function drainSyncIo(database: NativeSyncDatabase, context: IoContext): Promise { const promises: Promise[] = []; // Take all available IO items from the queue while (true) { const ioItem = database.ioTakeItem(); if (!ioItem) { break; // No more items } // Process each item promises.push(processIoItem(ioItem, context)); } // Wait for all IO operations to complete await Promise.all(promises); // Step callbacks after IO processing // This allows the sync engine to run any post-IO callbacks database.ioStepCallbacks(); } ================================================ FILE: bindings/react-native/src/types.ts ================================================ /** * Turso React Native SDK-KIT Types * * Clean TypeScript types matching the SDK-KIT C API patterns. * All logic lives in TypeScript or Rust - the C++ layer is just a thin bridge. */ // ============================================================================ // Core SDK-KIT Types (Local Database) // ============================================================================ /** * Native database interface (local-only) * Thin wrapper around TursoDatabaseHostObject */ export interface NativeDatabase { open(): void; connect(): NativeConnection; close(): void; } /** * Native connection interface * Thin wrapper around TursoConnectionHostObject */ export interface NativeConnection { prepareSingle(sql: string): NativeStatement; prepareFirst(sql: string): { statement: NativeStatement; tailIdx: number } | null; lastInsertRowid(): number; getAutocommit(): boolean; setBusyTimeout(timeoutMs: number): void; close(): void; } /** * Native statement interface * Thin wrapper around TursoStatementHostObject */ export interface NativeStatement { // Bind methods bindPositionalNull(position: number): number; bindPositionalInt(position: number, value: number): number; bindPositionalDouble(position: number, value: number): number; bindPositionalBlob(position: number, value: ArrayBuffer): number; bindPositionalText(position: number, value: string): number; // Execution methods execute(): { status: number; rowsChanged: number }; step(): number; // Returns status code runIo(): number; reset(): void; finalize(): number; // Query methods nChange(): number; columnCount(): number; columnName(index: number): string | null; rowValueKind(index: number): number; // TursoType enum rowValueBytesCount(index: number): number; rowValueBytesPtr(index: number): ArrayBuffer | null; rowValueText(index: number): string; rowValueInt(index: number): number; rowValueDouble(index: number): number; // Parameter methods namedPosition(name: string): number; parametersCount(): number; // Bulk row reading (native-side step+read loop) getAllRows(): { status: number; rows: Row[] }; } // ============================================================================ // Sync SDK-KIT Types (Embedded Replica) // ============================================================================ /** * Native sync database interface (embedded replica) * Thin wrapper around TursoSyncDatabaseHostObject */ export interface NativeSyncDatabase { // Async operations - return NativeSyncOperation open(): NativeSyncOperation; create(): NativeSyncOperation; connect(): NativeSyncOperation; stats(): NativeSyncOperation; checkpoint(): NativeSyncOperation; pushChanges(): NativeSyncOperation; waitChanges(): NativeSyncOperation; applyChanges(changes: NativeSyncChanges): NativeSyncOperation; // IO queue management ioTakeItem(): NativeSyncIoItem | null; ioStepCallbacks(): void; close(): void; } /** * Native sync operation interface * Thin wrapper around TursoSyncOperationHostObject * Represents an async operation that must be driven by calling resume() */ export interface NativeSyncOperation { resume(): number; // Returns status code (TURSO_DONE, TURSO_IO, etc.) resultKind(): number; // Returns result type enum extractConnection(): NativeConnection; extractChanges(): NativeSyncChanges | null; extractStats(): SyncStats; } /** * Native sync IO item interface * Thin wrapper around TursoSyncIoItemHostObject * Represents an IO request that JavaScript must process using fetch() or fs */ export interface NativeSyncIoItem { getKind(): 'HTTP' | 'FULL_READ' | 'FULL_WRITE' | 'NONE'; getHttpRequest(): HttpRequest; getFullReadPath(): string; getFullWriteRequest(): FullWriteRequest; // Completion methods poison(error: string): void; setStatus(statusCode: number): void; pushBuffer(data: ArrayBuffer): void; done(): void; } /** * Native sync changes interface * Thin wrapper around TursoSyncChangesHostObject * Represents changes fetched from remote (opaque, passed to applyChanges) */ export interface NativeSyncChanges { // Mostly opaque - just passed to applyChanges() } // ============================================================================ // Turso Status Codes // ============================================================================ export enum TursoStatus { OK = 0, DONE = 1, ROW = 2, IO = 3, BUSY = 4, INTERRUPT = 5, BUSY_SNAPSHOT = 6, ERROR = 127, MISUSE = 128, CONSTRAINT = 129, READONLY = 130, DATABASE_FULL = 131, NOTADB = 132, CORRUPT = 133, IOERR = 134, } // ============================================================================ // Turso Value Types // ============================================================================ export enum TursoType { UNKNOWN = 0, INTEGER = 1, REAL = 2, TEXT = 3, BLOB = 4, NULL = 5, } // ============================================================================ // Tracing / Logging Types // ============================================================================ /** * Log level for Turso tracing */ export type TursoTracingLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace'; /** * A single log entry emitted by the Turso engine */ export interface TursoLog { message: string; target: string; file: string; timestamp: number; line: number; level: TursoTracingLevel; } /** * Logger callback function */ export type TursoLoggerFn = (log: TursoLog) => void; // ============================================================================ // Sync Operation Result Types // ============================================================================ export enum SyncOperationResultType { NONE = 0, CONNECTION = 1, CHANGES = 2, STATS = 3, } // ============================================================================ // Public API Types (High-level TypeScript) // ============================================================================ /** * Supported SQLite value types for the public API */ export type SQLiteValue = null | number | string | ArrayBuffer; /** * Parameters that can be bound to SQL statements */ export type BindParams = | SQLiteValue[] | Record | SQLiteValue; /** * Result of a run() or exec() operation */ export interface RunResult { /** Number of rows changed by the statement */ changes: number; /** Last inserted row ID */ lastInsertRowid: number; } /** * A row returned from a query */ export type Row = Record; /** * Encryption options (matches JavaScript bindings) */ export interface EncryptionOpts { /** base64 encoded encryption key (must be either 16 or 32 bytes depending on cipher) */ key: string; /** * encryption cipher algorithm * - aes256gcm, aes128gcm, chacha20poly1305: 28 reserved bytes * - aegis128l, aegis128x2, aegis128x4: 32 reserved bytes * - aegis256, aegis256x2, aegis256x4: 48 reserved bytes */ cipher: | 'aes256gcm' | 'aes128gcm' | 'chacha20poly1305' | 'aegis128l' | 'aegis128x2' | 'aegis128x4' | 'aegis256' | 'aegis256x2' | 'aegis256x4'; } /** * Database options (matches JavaScript bindings) * Single unified config for both local and sync databases */ export interface DatabaseOpts { /** * Local path where to store database file (e.g. local.db) * Sync database will write several files with that prefix * (e.g. local.db-info, local.db-wal, etc) */ path: string; /** * Optional URL of the remote database (e.g. libsql://db-org.turso.io) * If omitted - local-only database will be created * * You can also provide function which will return URL or null * In this case local database will be created and sync will be "switched-on" * whenever the url returns non-empty value */ url?: string | (() => string | null); /** * Auth token for the remote database * (can be either static string or function which will provide short-lived credentials) */ authToken?: string | (() => Promise); /** * Arbitrary client name which can be used to distinguish clients internally * The library will guarantee uniqueness of the clientId by appending unique suffix */ clientName?: string; /** * Optional remote encryption parameters if cloud database was encrypted */ remoteEncryption?: EncryptionOpts; /** * Optional long-polling timeout for pull operation * If not set - no timeout is applied */ longPollTimeoutMs?: number; /** * Bootstrap database if empty; if set - client will be able to connect * to fresh db only when network is online */ bootstrapIfEmpty?: boolean; /** * Optional parameter to enable partial sync for the database * WARNING: This feature is EXPERIMENTAL */ partialSyncExperimental?: { /** * Bootstrap strategy configuration * - prefix strategy loads first N bytes locally at startup * - query strategy loads pages touched by the provided SQL statement */ bootstrapStrategy: | { kind: 'prefix'; length: number } | { kind: 'query'; query: string }; /** * Optional segment size which makes sync engine load pages in batches * (so, if loading page 1 with segment_size=128kb then 32 pages [1..32] will be loaded) */ segmentSize?: number; /** * Optional parameter which makes sync engine prefetch pages which probably * will be accessed soon */ prefetch?: boolean; }; } /** * Sync stats returned by stats() operation */ export interface SyncStats { cdcOperations: number; mainWalSize: number; revertWalSize: number; lastPullUnixTime: number; lastPushUnixTime: number; networkSentBytes: number; networkReceivedBytes: number; revision: string | null; } /** * HTTP request from sync engine */ export interface HttpRequest { url: string | null; method: string; path: string; headers: Record; body: ArrayBuffer | null; } /** * Full write request from sync engine */ export interface FullWriteRequest { path: string; content: ArrayBuffer | null; } // ============================================================================ // Global Turso Proxy Interface // ============================================================================ /** * Native proxy interface exposed via JSI */ export interface TursoProxy { newDatabase(path: string, config?: any): NativeDatabase; newSyncDatabase(dbConfig: any, syncConfig: any): NativeSyncDatabase; version(): string; setup(options: { logLevel?: string; logger?: TursoLoggerFn }): void; fsReadFile(path: string): ArrayBuffer | null; fsWriteFile(path: string, data: ArrayBuffer): void; } /** * Native module interface (React Native bridge) */ export interface TursoNativeModule { install(): boolean; // Constants exposed by native module ANDROID_DATABASE_PATH?: string; ANDROID_FILES_PATH?: string; ANDROID_EXTERNAL_FILES_PATH?: string; IOS_DOCUMENT_PATH?: string; IOS_LIBRARY_PATH?: string; } /** * Global __TursoProxy object injected by native code */ declare global { // eslint-disable-next-line no-var var __TursoProxy: TursoProxy; } export {}; ================================================ FILE: bindings/react-native/templates/turso-sync-sdk-kit.xcframework/Info.plist ================================================ AvailableLibraries LibraryIdentifier ios-arm64 LibraryPath turso-sync-sdk-kit.framework SupportedArchitectures arm64 SupportedPlatform ios LibraryIdentifier ios-arm64-simulator LibraryPath turso-sync-sdk-kit.framework SupportedArchitectures arm64 SupportedPlatform ios SupportedPlatformVariant simulator CFBundlePackageType XFWK XCFrameworkFormatVersion 1.0 CFBundleVersion 1.0.0 CFBundleShortVersionString 1.0.0 ================================================ FILE: bindings/react-native/templates/turso-sync-sdk-kit.xcframework/ios-arm64/turso-sync-sdk-kit.framework/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable turso-sync-sdk-kit CFBundleIdentifier com.turso.turso-sync-sdk-kit CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType FMWK CFBundleSignature ???? CFBundleVersion 1.0.0 CFBundleShortVersionString 1.0.0 MinimumOSVersion 13.0 ================================================ FILE: bindings/react-native/templates/turso-sync-sdk-kit.xcframework/ios-arm64-simulator/turso-sync-sdk-kit.framework/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable turso-sync-sdk-kit CFBundleIdentifier com.turso.turso-sync-sdk-kit CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType FMWK CFBundleSignature ???? CFBundleVersion 1.0.0 CFBundleShortVersionString 1.0.0 MinimumOSVersion 13.0 ================================================ FILE: bindings/react-native/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "declaration": true, "declarationMap": true, "outDir": "lib/typescript", "rootDir": "src" }, "include": ["src"], "exclude": ["**/__tests__", "**/__mocks__"] } ================================================ FILE: bindings/react-native/tsconfig.json ================================================ { "compilerOptions": { "rootDir": ".", "allowUnreachableCode": false, "allowUnusedLabels": false, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "jsx": "react", "lib": ["ES2020"], "module": "ESNext", "moduleResolution": "node", "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noImplicitUseStrict": false, "noStrictGenericChecks": false, "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noUnusedParameters": true, "resolveJsonModule": true, "skipLibCheck": true, "strict": true, "target": "ES2020", "verbatimModuleSyntax": true } } ================================================ FILE: bindings/react-native/turso-sync-react-native.podspec ================================================ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) Pod::Spec.new do |s| s.name = "turso-sync-react-native" s.version = package["version"] s.summary = package["description"] s.homepage = package["homepage"] s.license = package["license"] s.authors = package["author"] s.platforms = { :ios => "13.0" } s.source = { :git => "https://github.com/tursodatabase/turso.git", :tag => "#{s.version}" } s.source_files = [ "ios/**/*.{h,m,mm}", "cpp/**/*.{h,hpp,cpp}" ] # Vendored Rust XCFramework (handles device + simulator automatically) s.vendored_frameworks = "libs/ios/turso-sync-sdk-kit.xcframework" # Explicitly export C headers from the vendored xcframework s.static_framework = true s.public_header_files = "libs/ios/turso-sync-sdk-kit.xcframework/**/Headers/*.h" s.header_mappings_dir = "libs/ios/turso-sync-sdk-kit.xcframework" # Header search paths s.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", "HEADER_SEARCH_PATHS" => [ "$(PODS_TARGET_SRCROOT)/cpp", "$(PODS_TARGET_SRCROOT)/libs/ios" ].join(" "), "OTHER_LDFLAGS" => "-lc++", "DEFINES_MODULE" => "YES", "GCC_PRECOMPILE_PREFIX_HEADER" => "NO" } # User header search paths for consumers s.user_target_xcconfig = { "HEADER_SEARCH_PATHS" => "$(PODS_ROOT)/turso-sync-react-native/cpp $(PODS_ROOT)/turso-sync-react-native/libs/ios" } # Build script to compile Rust s.script_phase = { :name => "Build Turso Rust Library", :script => 'set -e; cd "${PODS_TARGET_SRCROOT}"; if [ ! -d "libs/ios/turso-sync-sdk-kit.xcframework" ]; then echo "Building Rust library for iOS..."; make ios; fi', :execution_position => :before_compile, :shell_path => "/bin/bash" } # Install React Native module dependencies (includes React-Core, turbomodule, etc.) install_modules_dependencies(s) end ================================================ FILE: bindings/rust/Cargo.toml ================================================ # Copyright 2025 the Turso authors. All rights reserved. MIT license. [package] name = "turso" version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true description = "Turso Rust API" [lints] workspace = true [features] default = ["mimalloc"] mimalloc = ["dep:mimalloc"] sync = [ "dep:hyper", "dep:tokio", "dep:hyper-tls", "dep:hyper-util", "dep:http-body-util", "dep:bytes", ] [dependencies] turso_sdk_kit = { workspace = true } turso_sync_sdk_kit = { workspace = true } thiserror = { workspace = true } tracing-subscriber.workspace = true tracing.workspace = true mimalloc = { workspace = true, optional = true } hyper = { version = "1.8.1", features = ["http1"], optional = true } tokio = { workspace = true, features = ["full"], optional = true } hyper-tls = { version = "0.6.0", optional = true } hyper-util = { version = "0.1.19", features = [ "http1", "tokio", ], optional = true } http-body-util = { version = "0.1.3", optional = true } bytes = { version = "1.11.0", optional = true } [[example]] name = "sync_example" required-features = ["sync"] [[example]] name = "concurrent_writes" [dev-dependencies] tempfile = { workspace = true } tokio = { workspace = true, features = ["full"] } rand.workspace = true rand_chacha = { workspace = true } anyhow.workspace = true reqwest = { version = "0.12.28", features = ["json"] } serde_json.workspace = true ================================================ FILE: bindings/rust/README.md ================================================ # turso The next evolution of SQLite: A high-performance, SQLite-compatible database library for Rust ## Features - **SQLite Compatible**: Similar interface to rusqlite with familiar API apart from using async Rust - **High Performance**: Built with Rust for maximum speed and efficiency - **Async/Await Support**: Native async operations with tokio support - **In-Process**: No network overhead, runs directly in your application - **Cross-Platform**: Supports Linux, macOS, and Windows - **Transaction Support**: Full ACID transactions with rollback support - **Prepared Statements**: Optimized query execution with parameter binding - **Cloud Sync**: Sync with Turso Cloud using `Builder::new_remote()` (optional `sync` feature) ## Installation Add this to your `Cargo.toml`: ```toml [dependencies] turso = "0.4.3" tokio = { version = "1.0", features = ["full"] } ``` For cloud sync capabilities, enable the `sync` feature: ```toml [dependencies] turso = { version = "0.4.3", features = ["sync"] } tokio = { version = "1.0", features = ["full"] } ``` ## Quick Start ### In-Memory Database ```rust use turso::Builder; #[tokio::main] async fn main() -> turso::Result<()> { // Create an in-memory database let db = Builder::new_local(":memory:").build().await?; let conn = db.connect()?; // Create a table conn.execute( "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)", () ).await?; // Insert data conn.execute( "INSERT INTO users (name, email) VALUES (?1, ?2)", ["Alice", "alice@example.com"] ).await?; conn.execute( "INSERT INTO users (name, email) VALUES (?1, ?2)", ["Bob", "bob@example.com"] ).await?; // Query data let mut rows = conn.query("SELECT * FROM users", ()).await?; while let Some(row) = rows.next().await? { let id = row.get_value(0)?; let name = row.get_value(1)?; let email = row.get_value(2)?; println!("User: {} - {} ({})", id.as_integer().unwrap_or(&0), name.as_text().unwrap_or(&"".to_string()), email.as_text().unwrap_or(&"".to_string()) ); } Ok(()) } ``` ### File-Based Database ```rust use turso::Builder; #[tokio::main] async fn main() -> turso::Result<()> { // Create or open a database file let db = Builder::new_local("my-database.db").build().await?; let conn = db.connect()?; // Create a table conn.execute( r#"CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )"#, () ).await?; // Insert a post let rows_affected = conn.execute( "INSERT INTO posts (title, content) VALUES (?1, ?2)", ["Hello World", "This is my first blog post!"] ).await?; println!("Inserted {} rows", rows_affected); Ok(()) } ``` ### Synced Database Sync your local database with Turso Cloud: ```rust use turso::sync::Builder; #[tokio::main] async fn main() -> turso::Result<()> { // Create a synced database let db = Builder::new_remote("local.db") .with_remote_url("libsql://your-database.turso.io") .with_auth_token("your-token") .build() .await?; let conn = db.connect().await?; // Create a table and insert data conn.execute( "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, content TEXT)", () ).await?; conn.execute( "INSERT INTO notes (content) VALUES (?1)", ["My first synced note"] ).await?; // Push local changes to remote db.push().await?; // Pull remote changes to local db.pull().await?; Ok(()) } ``` ## API Reference ### Builder Create a new database: ```rust let db = Builder::new_local(":memory:").build().await?; let db = Builder::new_local("data.db").build().await?; ``` ### Connection Execute queries and statements: ```rust // Execute SQL directly let rows_affected = conn.execute("INSERT INTO users (name) VALUES (?1)", ["Alice"]).await?; // Query for multiple rows let mut rows = conn.query("SELECT * FROM users WHERE age > ?1", [18]).await?; // Prepare statements for reuse let mut stmt = conn.prepare("SELECT * FROM users WHERE id = ?1").await?; let mut rows = stmt.query([42]).await?; // Execute prepared statements let rows_affected = stmt.execute(["Alice"]).await?; ``` ### Working with Results ```rust use futures_util::TryStreamExt; let mut rows = conn.query("SELECT name, email FROM users", ()).await?; while let Some(row) = rows.try_next().await? { let name = row.get_value(0)?.as_text().unwrap_or(&"".to_string()); let email = row.get_value(1)?.as_text().unwrap_or(&"".to_string()); println!("{}: {}", name, email); } ``` ### Sync API Reference #### sync::Builder Create a synced database that synchronizes with Turso Cloud: ```rust use turso::sync::Builder; let db = Builder::new_remote("local.db") // Local database path (or ":memory:") .with_remote_url("libsql://db.turso.io") // Remote URL (https://, http://, or libsql://) .with_auth_token("your-token") // Authorization token .bootstrap_if_empty(true) // Download schema on first sync (default: true) .with_remote_encryption("base64-encoded-key", RemoteEncryptionCipher::Aes256Gcm) // Optional remote encryption .build() .await?; ``` #### sync::Database Operations for synced databases: ```rust // Push local changes to remote db.push().await?; // Pull remote changes (returns true if changes were applied) let had_changes = db.pull().await?; // Force WAL checkpoint db.checkpoint().await?; // Get sync statistics let stats = db.stats().await?; println!("Received: {} bytes", stats.network_received_bytes); println!("Sent: {} bytes", stats.network_sent_bytes); println!("WAL size: {} bytes", stats.main_wal_size); ``` ## License MIT ## Support - [GitHub Issues](https://github.com/tursodatabase/turso/issues) - [Discord Community](https://discord.gg/turso) ================================================ FILE: bindings/rust/examples/README.md ================================================ # Rust Examples ```bash cargo run --package turso --example example cargo run --package turso --example example_struct cargo run --package turso --example concurrent_writes cargo run --package turso --example sync_example --features sync # requires Turso Cloud ``` ## Examples | Example | Description | |---------|-------------| | `example` | Basic queries, prepared statements, and pragma usage | | `example_struct` | Mapping rows to structs using transactions | | `concurrent_writes` | MVCC mode: 16 concurrent writers using `BEGIN CONCURRENT` | | `sync_example` | Syncing with Turso Cloud (set `TURSO_REMOTE_URL` / `TURSO_AUTH_TOKEN`) | ================================================ FILE: bindings/rust/examples/concurrent_writes.rs ================================================ //! Concurrent writes with MVCC //! //! `BEGIN CONCURRENT` lets multiple connections write at the same time without //! holding an exclusive lock. Conflicts are detected at commit time: if two //! transactions worked on same rows, the later one receives a conflict //! error and must roll back and retry. use rand::Rng; use tempfile::NamedTempFile; use turso::{Builder, Error}; fn is_retryable(e: &Error) -> bool { matches!(e, Error::Busy(_) | Error::BusySnapshot(_)) || matches!(e, Error::Error(msg) if msg.contains("conflict")) } #[tokio::main] async fn main() -> Result<(), Error> { let tmp = NamedTempFile::new().expect("failed to create temp file"); let db = Builder::new_local(tmp.path().to_str().unwrap()) .build() .await?; let conn = db.connect()?; conn.pragma_update("journal_mode", "'mvcc'").await?; conn.execute("CREATE TABLE hits (val INTEGER)", ()).await?; let mut handles = Vec::new(); for _ in 0..16 { let db = db.clone(); handles.push(tokio::spawn(async move { let val = rand::rng().random_range(1..=100); let conn = db.connect()?; loop { conn.execute("BEGIN CONCURRENT", ()).await?; let result = conn .execute(&format!("INSERT INTO hits VALUES ({val})"), ()) .await .and(conn.execute("COMMIT", ()).await); match result { Ok(_) => return Ok::<_, Error>(val), Err(ref e) if is_retryable(e) => { let _ = conn.execute("ROLLBACK", ()).await; tokio::task::yield_now().await; } Err(e) => { let _ = conn.execute("ROLLBACK", ()).await; return Err(e); } } } })); } for handle in handles { let val = handle.await.expect("task panicked")?; println!("inserted val={val}"); } let mut rows = conn.query("SELECT COUNT(*) FROM hits", ()).await?; if let Some(row) = rows.next().await? { println!("total rows: {}", row.get::(0)?); } Ok(()) } ================================================ FILE: bindings/rust/examples/example.rs ================================================ use turso::{Builder, Error}; #[tokio::main] async fn main() -> Result<(), Error> { let db = Builder::new_local(":memory:") .build() .await .expect("Turso Failed to Build memory db"); let conn = db.connect()?; conn.query("select 1; select 1;", ()).await?; conn.execute( "CREATE TABLE IF NOT EXISTS users (email TEXT, age INTEGER)", (), ) .await?; conn.pragma_query("journal_mode", |row| { println!("{:?}", row.get_value(0)); Ok(()) }) .await?; let mut stmt = conn .prepare("INSERT INTO users (email, age) VALUES (?1, ?2)") .await?; stmt.execute(["foo@example.com", &21.to_string()]).await?; let mut stmt = conn.prepare("SELECT * FROM users WHERE email = ?1").await?; let mut rows = stmt.query(["foo@example.com"]).await?; let row = rows.next().await?; assert!( row.is_some(), "The row that was just inserted hasn't been found" ); if let Some(row_values) = row { let email = row_values.get_value(0)?; let age = row_values.get_value(1)?; println!("Row: {email:?} {age:?}"); } Ok(()) } ================================================ FILE: bindings/rust/examples/example_struct.rs ================================================ use turso::{transaction::Transaction, Builder, Connection, Error}; #[derive(Debug)] struct User { email: String, age: i32, } async fn create_tables(conn: &Connection) -> Result<(), Error> { conn.execute( "CREATE TABLE IF NOT EXISTS users (email TEXT, age INTEGER)", (), ) .await?; Ok(()) } async fn insert_users(tx: &Transaction<'_>) -> Result<(), Error> { let mut stmt = tx .prepare("INSERT INTO users (email, age) VALUES (?1, ?2)") .await?; stmt.execute(["foo@example.com", &21.to_string()]).await?; stmt.execute(["bar@example.com", &22.to_string()]).await?; Ok(()) } async fn list_users(conn: &Connection) -> Result<(), Error> { let mut stmt = conn .prepare("SELECT * FROM users WHERE email like ?1") .await?; let mut rows = stmt.query(["%@example.com"]).await?; while let Some(row) = rows.next().await? { let u: User = User { email: row.get(0)?, age: row.get(1)?, }; println!("Row: {} {}", u.email, u.age); } Ok(()) } #[tokio::main] async fn main() -> Result<(), Error> { let db = Builder::new_local(":memory:") .build() .await .expect("Turso Failed to Build memory db"); let mut conn = db.connect()?; create_tables(&conn).await?; let tx = conn.transaction().await?; insert_users(&tx).await?; tx.commit().await?; list_users(&conn).await?; Ok(()) } ================================================ FILE: bindings/rust/examples/sync_example.rs ================================================ //! Turso Database Sync example with Turso Cloud (with optional remote encryption) //! //! Environment variables: //! TURSO_REMOTE_URL - Remote database URL (default: http://localhost:8080) //! TURSO_AUTH_TOKEN - Auth token (optional) //! TURSO_REMOTE_ENCRYPTION_KEY - Base64-encoded encryption key (optional) //! TURSO_REMOTE_ENCRYPTION_CIPHER - Cipher name (default: aes256gcm) //! use std::env; use turso::sync::{Builder, RemoteEncryptionCipher}; use turso::Error; #[tokio::main] async fn main() -> Result<(), Error> { let remote_url = env::var("TURSO_REMOTE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()); let auth_token = env::var("TURSO_AUTH_TOKEN").ok(); let encryption_key = env::var("TURSO_REMOTE_ENCRYPTION_KEY").ok(); let encryption_cipher = env::var("TURSO_REMOTE_ENCRYPTION_CIPHER") .unwrap_or_else(|_| "aes256gcm".to_string()) .parse::() .expect("invalid cipher"); println!("Remote URL: {remote_url}"); println!("Auth Token: {}", auth_token.is_some()); println!("Encryption: {}", encryption_key.is_some()); if encryption_key.is_some() { println!("Cipher: {encryption_cipher:?}"); } let mut builder = Builder::new_remote(":memory:").with_remote_url(&remote_url); if let Some(token) = auth_token { builder = builder.with_auth_token(token); } if let Some(key) = encryption_key { builder = builder.with_remote_encryption(key, encryption_cipher); } let db = builder.build().await?; let conn = db.connect().await?; conn.execute("CREATE TABLE IF NOT EXISTS t (x TEXT)", ()) .await?; let mut stmt = conn.prepare("SELECT COUNT(*) FROM t").await?; let mut rows = stmt.query(()).await?; let count: i64 = if let Some(row) = rows.next().await? { row.get(0)? } else { 0 }; let next = count + 1; conn.execute(&format!("INSERT INTO t VALUES ('hello sync #{next}')"), ()) .await?; db.push().await?; println!("\nTest table contents:"); let mut stmt = conn.prepare("SELECT * FROM t").await?; let mut rows = stmt.query(()).await?; while let Some(row) = rows.next().await? { println!(" Row: {:?}", row.get_value(0)?); } // query sqlite_master for all tables println!("\nDatabase tables:"); let mut stmt = conn .prepare("SELECT name, type FROM sqlite_master WHERE type='table'") .await?; let mut rows = stmt.query(()).await?; while let Some(row) = rows.next().await? { let name = row.get_value(0)?; let typ = row.get_value(1)?; println!(" - {typ:?}: {name:?}"); } // sho database stats let stats = db.stats().await?; println!("\nDatabase stats:"); println!(" Network received: {} bytes", stats.network_received_bytes); println!(" Network sent: {} bytes", stats.network_sent_bytes); println!(" Main WAL size: {} bytes", stats.main_wal_size); println!("\nDone!"); Ok(()) } ================================================ FILE: bindings/rust/rust-driver-sync.mdx ================================================ --- name: 2025-12-24-rust-driver-sync --- Turso - is the SQLite compatible database written in Rust. One of the important features of the Turso - is native ability to sync database with the Cloud in both directions (push local changes and pull remote changes). Your task is to generate EXTRA functionality on top of the existing Rust driver which will extend regular embedded database with sync capability. Do not modify existing driver - its already implemented in the lib.rs Your task is to write extra code which will use abstractions from lib.rs and build sync support in the Rust on top of it in the lib.rs file. # Rules General rules for driver implementation you **MUST** follow and never go against these rules: - USE already implemented driver - DO NOT copy it - SET async_io=True for the driver database configuration - because partial sync support requires TURSO_IO to handled externally from the bindings - STRUCTURE of the implementation * Declaration order of elements and semantic blocks MUST be exsactly the same * (details and full enumerations omited in the example for brevity but you must generate full code) ```rs /// A builder for `Database`. pub struct Builder { path: String, remote_url: String, auth_token: Option, client_name: Option, long_poll_timeout: Option, bootstrap_if_empty: bool, partial_sync_config_experimental: Option, } pub struct Database { ... } impl Database { pub async fn push(&self) -> crate::Result<()> { ... } pub async fn pull(&self) -> crate::Result { ... } pub async fn checkpoint(&self) -> crate::Result<()> { ... } pub async fn stats(&self) -> crate::Result { ... } pub async fn connect(&self) -> crate::Result { ... } } impl Builder { pub fn new_remote(path: &str, remote_url: &str) -> Self { ... } /// ... more methods to configure builder with extra parameters ... /// Build the synced database. pub async fn build(self) -> crate::Result { ... } } ``` - STREAM data from the http request to the completion in chunks and spin async operation in between in order to prevent loading whole response in memory - FOCUS on code readability: extract helper functions if it will contribute to the code readability (do not overoptimize - it's fine to have some logic inlined especially if it is not repeated anywhere else) - WATCH OUT for variables scopes and do not use variables which are no longer accessible - USE hyper to perform HTTP(s) requests - ACCEPT https://, http:// protocols and also extra libsql:// protocol which internally should be translated to https:// - You MUST implement custom poll future to interact with `TursoDatabaseAsyncOperation` - You MUST create separate tokio thread for processing HTTP requests queue - You MUST provide extra_io callback which will send IO request from the SyncEngineIO queue to separate IO thread - You MUST call `step_io_callbacks` in the IO thread after making some progress with IO (pushing more data, finishing request, etc) - As IO worker don't have a guaranteed way to track relation between IO and request - you MUST await all futures when IO progressed from IO thread * Do this in the IO worker main loop - EXTRACT ALL constants at the top of the sync file - DO NOT LOAD full http response body in memory - instead stream it to the completion with `push_buffer` method - You MUST support TLS (HTTPS) and add necessary TLS connnector for that purpose # Implementation - Annotate public API with types - Add comments about public API fields/functions to clarify meaning and usage scenarios - Use `turso_sync_database_create()` method for creation of the synced database for now - DO NOT use init + open pair - Be careful with hyper typing: `RequestBuilder` is typed by the `body` type and this can cause conflict if you are using `Full` and `Empty` as body - Implement proper waking machinery which will connect IoWorker with pending futures - Use `Client, Full>` for hyper client * With `HttpConnector` from `hyper_util::client::legacy::connect::HttpConnector` - Use `turso-sync-rust` as client_name if not set by user - Write and Read files (FullRead / FullWrite) in NON-CHUNKED mode - since files are usually small (but HTTP payloads can be huge) # driver Current dreiver implementation consist of following main components: # sdk-kit Your implementation must use sdk-kit for embedded db and sync extension Look at the rust API of the kit here: Sync engine provide simple "step"-based API and you MUST integrate it to the Rust driver async: Be careful with TursoDatabaseAsyncOperation ownership as it is wrapped in unique Box container. You must use it from single place. # hyper Use following example to get up-to date understanding of Hyper API: For HTTPS project already installed `hyper_tls` dep. You can inspect example here: ================================================ FILE: bindings/rust/src/connection.rs ================================================ use crate::assert_send_sync; use crate::transaction::DropBehavior; use crate::transaction::TransactionBehavior; use crate::Error; use crate::IntoParams; use crate::Row; use crate::Rows; use crate::Statement; use std::fmt::Debug; use std::sync::atomic::AtomicU8; use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::Mutex; use std::task::Waker; pub type Result = std::result::Result; /// Atomic wrapper for [DropBehavior] pub(crate) struct AtomicDropBehavior { inner: AtomicU8, } impl AtomicDropBehavior { fn new(behavior: DropBehavior) -> Self { Self { inner: AtomicU8::new(behavior.into()), } } fn load(&self, ordering: Ordering) -> DropBehavior { self.inner.load(ordering).into() } pub(crate) fn store(&self, behavior: DropBehavior, ordering: Ordering) { self.inner.store(behavior.into(), ordering); } } // A database connection. pub struct Connection { /// Inner is an Option so that when a Connection is dropped we can take the inner /// (Actual connection) out of it and put it back into the ConnectionPool /// the only time inner will be None is just before the Connection is freed after the /// inner connection has been recyled into the connection pool inner: Option>, pub(crate) transaction_behavior: TransactionBehavior, /// If there is a dangling transaction after it was dropped without being finished, /// [Connection::dangling_tx] will be set to the [DropBehavior] of the dangling transaction, /// and the corresponding action will be taken when a new transaction is requested /// or the connection queries/executes. /// We cannot do this eagerly on Drop because drop is not async. /// /// By default, the value is [DropBehavior::Ignore] which effectively does nothing. pub(crate) dangling_tx: AtomicDropBehavior, pub(crate) extra_io: Option Result<()> + Send + Sync>>, } assert_send_sync!(Connection); impl Clone for Connection { fn clone(&self) -> Self { Self { inner: self.inner.clone(), transaction_behavior: self.transaction_behavior, dangling_tx: AtomicDropBehavior::new(self.dangling_tx.load(Ordering::SeqCst)), extra_io: self.extra_io.clone(), } } } impl Connection { pub fn create( conn: Arc, extra_io: Option Result<()> + Send + Sync>>, ) -> Self { #[allow(clippy::arc_with_non_send_sync)] let connection = Connection { inner: Some(conn), transaction_behavior: TransactionBehavior::Deferred, dangling_tx: AtomicDropBehavior::new(DropBehavior::Ignore), extra_io, }; connection } pub(crate) async fn maybe_handle_dangling_tx(&self) -> Result<()> { match self.dangling_tx.load(Ordering::SeqCst) { DropBehavior::Rollback => { let mut stmt = self.prepare("ROLLBACK").await?; stmt.execute(()).await?; self.dangling_tx .store(DropBehavior::Ignore, Ordering::SeqCst); } DropBehavior::Commit => { let mut stmt = self.prepare("COMMIT").await?; stmt.execute(()).await?; self.dangling_tx .store(DropBehavior::Ignore, Ordering::SeqCst); } DropBehavior::Ignore => {} DropBehavior::Panic => { panic!("Transaction dropped unexpectedly."); } } Ok(()) } /// Query the database with SQL. pub async fn query(&self, sql: impl AsRef, params: impl IntoParams) -> Result { self.maybe_handle_dangling_tx().await?; let mut stmt = self.prepare(sql).await?; stmt.query(params).await } /// Execute SQL statement on the database. pub async fn execute(&self, sql: impl AsRef, params: impl IntoParams) -> Result { self.maybe_handle_dangling_tx().await?; let mut stmt = self.prepare(sql).await?; stmt.execute(params).await } /// get the inner connection fn get_inner_connection(&self) -> Result> { match &self.inner { Some(inner) => Ok(inner.clone()), None => Err(Error::Misuse("inner connection must be set".to_string())), } } /// Execute a batch of SQL statements on the database. pub async fn execute_batch(&self, sql: impl AsRef) -> Result<()> { self.maybe_handle_dangling_tx().await?; self.prepare_execute_batch(sql).await?; Ok(()) } /// Prepare a SQL statement for later execution. pub async fn prepare(&self, sql: impl AsRef) -> Result { let conn = self.get_inner_connection()?; let stmt = conn.prepare_single(sql)?; #[allow(clippy::arc_with_non_send_sync)] let statement = Statement { conn: self.clone(), inner: Arc::new(Mutex::new(stmt)), }; Ok(statement) } /// Prepare a SQL statement for later execution, caching it in the connection. pub async fn prepare_cached(&self, sql: impl AsRef) -> Result { let conn = self.get_inner_connection()?; let stmt = conn.prepare_cached(sql)?; #[allow(clippy::arc_with_non_send_sync)] let statement = Statement { conn: self.clone(), inner: Arc::new(Mutex::new(stmt)), }; Ok(statement) } async fn prepare_execute_batch(&self, sql: impl AsRef) -> Result<()> { self.maybe_handle_dangling_tx().await?; let conn = self.get_inner_connection()?; let mut sql = sql.as_ref(); while let Some((stmt, offset)) = conn.prepare_first(sql)? { let mut stmt = Statement { conn: self.clone(), inner: Arc::new(Mutex::new(stmt)), }; let _ = stmt.execute(()).await?; sql = &sql[offset..]; } Ok(()) } /// Query a pragma. pub async fn pragma_query(&self, pragma_name: &str, mut f: F) -> Result<()> where F: FnMut(&Row) -> std::result::Result<(), turso_sdk_kit::rsapi::TursoError>, { let sql = format!("PRAGMA {pragma_name}"); let mut stmt = self.prepare(&sql).await?; let mut rows = stmt.query(()).await?; while let Some(row) = rows.next().await? { f(&row)?; } Ok(()) } /// Set a pragma value. pub async fn pragma_update( &self, pragma_name: &str, pragma_value: V, ) -> Result> { let sql = format!("PRAGMA {pragma_name} = {pragma_value}"); let mut stmt = self.prepare(&sql).await?; let mut rows = stmt.query(()).await?; let mut collected = Vec::new(); while let Some(row) = rows.next().await? { collected.push(row); } Ok(collected) } /// Returns the rowid of the last row inserted. pub fn last_insert_rowid(&self) -> i64 { let conn = self.get_inner_connection().unwrap(); conn.last_insert_rowid() } /// Flush dirty pages to disk. /// This will write the dirty pages to the WAL. pub fn cacheflush(&self) -> Result<()> { let conn = self.get_inner_connection()?; conn.cacheflush()?; Ok(()) } pub fn is_autocommit(&self) -> Result { let conn = self.get_inner_connection()?; Ok(conn.get_auto_commit()) } /// Sets maximum total accumuated timeout. If the duration is None or Zero, we unset the busy handler for this Connection /// /// This api defers slighty from: https://www.sqlite.org/c3ref/busy_timeout.html /// /// Instead of sleeping for linear amount of time specified by the user, /// we will sleep in phases, until the the total amount of time is reached. /// This means we first sleep of 1ms, then if we still return busy, we sleep for 2 ms, and repeat until a maximum of 100 ms per phase. /// /// Example: /// 1. Set duration to 5ms /// 2. Step through query -> returns Busy -> sleep/yield for 1 ms /// 3. Step through query -> returns Busy -> sleep/yield for 2 ms /// 4. Step through query -> returns Busy -> sleep/yield for 2 ms (totaling 5 ms of sleep) /// 5. Step through query -> returns Busy -> return Busy to user pub fn busy_timeout(&self, duration: std::time::Duration) -> Result<()> { let conn = self.get_inner_connection()?; conn.set_busy_timeout(duration); Ok(()) } } impl Debug for Connection { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Connection").finish() } } ================================================ FILE: bindings/rust/src/lib.rs ================================================ //! # Turso bindings for Rust //! //! Turso is an in-process SQL database engine, compatible with SQLite. //! //! ## Getting Started //! //! To get started, you first need to create a [`Database`] object and then open a [`Connection`] to it, which you use to query: //! //! ```rust,no_run //! # async fn run() { //! use turso::Builder; //! //! let db = Builder::new_local(":memory:").build().await.unwrap(); //! let conn = db.connect().unwrap(); //! conn.execute("CREATE TABLE IF NOT EXISTS users (email TEXT)", ()).await.unwrap(); //! conn.execute("INSERT INTO users (email) VALUES ('alice@example.org')", ()).await.unwrap(); //! # } //! ``` //! //! You can also prepare statements with the [`Connection`] object and then execute the [`Statement`] objects: //! //! ```rust,no_run //! # async fn run() { //! # use turso::Builder; //! # let db = Builder::new_local(":memory:").build().await.unwrap(); //! # let conn = db.connect().unwrap(); //! let mut stmt = conn.prepare("SELECT * FROM users WHERE email = ?1").await.unwrap(); //! let mut rows = stmt.query(["foo@example.com"]).await.unwrap(); //! let row = rows.next().await.unwrap().unwrap(); //! let value = row.get_value(0).unwrap(); //! println!("Row: {:?}", value); //! # } //! ``` #[cfg(all(feature = "mimalloc", not(target_family = "wasm"), not(miri)))] #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; pub mod connection; pub mod params; mod rows; pub mod transaction; pub mod value; #[cfg(feature = "sync")] pub mod sync; pub use connection::Connection; use turso_sdk_kit::rsapi::TursoError; pub use value::Value; pub use params::params_from_iter; pub use params::IntoParams; use std::fmt::Debug; use std::future::Future; use std::sync::Arc; use std::sync::Mutex; use std::task::Poll; // Re-exports rows pub use crate::rows::{Row, Rows}; /// Assert that a type implements both Send and Sync at compile time. /// Usage: assert_send_sync!(MyType); /// Usage: assert_send_sync!(Type1, Type2, Type3); macro_rules! assert_send_sync { ($($t:ty),+ $(,)?) => { #[cfg(test)] $(const _: () = { const fn _assert_send() {} const fn _assert_sync() {} _assert_send::<$t>(); _assert_sync::<$t>(); };)+ }; } pub(crate) use assert_send_sync; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("SQL conversion failure: `{0}`")] ToSqlConversionFailure(BoxError), #[error("Query returned no rows")] QueryReturnedNoRows, #[error("Conversion failure: `{0}`")] ConversionFailure(String), #[error("{0}")] Busy(String), #[error("{0}")] BusySnapshot(String), #[error("{0}")] Interrupt(String), #[error("{0}")] Error(String), #[error("{0}")] Misuse(String), #[error("{0}")] Constraint(String), #[error("{0}")] Readonly(String), #[error("{0}")] DatabaseFull(String), #[error("{0}")] NotAdb(String), #[error("{0}")] Corrupt(String), #[error("I/O error ({1}): {0}")] IoError(std::io::ErrorKind, &'static str), } impl From for Error { fn from(value: turso_sdk_kit::rsapi::TursoError) -> Self { match value { turso_sdk_kit::rsapi::TursoError::Busy(err) => Error::Busy(err), turso_sdk_kit::rsapi::TursoError::BusySnapshot(err) => Error::BusySnapshot(err), turso_sdk_kit::rsapi::TursoError::Interrupt(err) => Error::Interrupt(err), turso_sdk_kit::rsapi::TursoError::Error(err) => Error::Error(err), turso_sdk_kit::rsapi::TursoError::Misuse(err) => Error::Misuse(err), turso_sdk_kit::rsapi::TursoError::Constraint(err) => Error::Constraint(err), turso_sdk_kit::rsapi::TursoError::Readonly(err) => Error::Readonly(err), turso_sdk_kit::rsapi::TursoError::DatabaseFull(err) => Error::DatabaseFull(err), turso_sdk_kit::rsapi::TursoError::NotAdb(err) => Error::NotAdb(err), turso_sdk_kit::rsapi::TursoError::Corrupt(err) => Error::Corrupt(err), turso_sdk_kit::rsapi::TursoError::IoError(kind, op) => Error::IoError(kind, op), } } } pub(crate) type BoxError = Box; pub type Result = std::result::Result; pub type EncryptionOpts = turso_sdk_kit::rsapi::EncryptionOpts; /// A builder for `Database`. pub struct Builder { path: String, enable_encryption: bool, enable_attach: bool, enable_custom_types: bool, enable_index_method: bool, enable_materialized_views: bool, vfs: Option, encryption_opts: Option, } impl Builder { /// Create a new local database. pub fn new_local(path: &str) -> Self { Self { path: path.to_string(), enable_encryption: false, enable_attach: false, enable_custom_types: false, enable_index_method: false, enable_materialized_views: false, vfs: None, encryption_opts: None, } } pub fn experimental_encryption(mut self, encryption_enabled: bool) -> Self { self.enable_encryption = encryption_enabled; self } pub fn with_encryption(mut self, opts: turso_sdk_kit::rsapi::EncryptionOpts) -> Self { self.encryption_opts = Some(opts); self } /// Kept for backwards compatibility. Triggers are now always enabled. pub fn experimental_triggers(self, _triggers_enabled: bool) -> Self { self } pub fn experimental_attach(mut self, attach_enabled: bool) -> Self { self.enable_attach = attach_enabled; self } /// Kept for backwards compatibility. Strict tables are now always enabled. pub fn experimental_strict(self, _strict_enabled: bool) -> Self { self } pub fn experimental_custom_types(mut self, custom_types_enabled: bool) -> Self { self.enable_custom_types = custom_types_enabled; self } pub fn experimental_index_method(mut self, index_method_enabled: bool) -> Self { self.enable_index_method = index_method_enabled; self } pub fn experimental_materialized_views(mut self, enabled: bool) -> Self { self.enable_materialized_views = enabled; self } pub fn with_io(mut self, vfs: String) -> Self { self.vfs = Some(vfs); self } fn build_features_string(&self) -> Option { let mut features = Vec::new(); if self.enable_encryption { features.push("encryption"); } if self.enable_attach { features.push("attach"); } if self.enable_custom_types { features.push("custom_types"); } if self.enable_index_method { features.push("index_method"); } if self.enable_materialized_views { features.push("views"); } if features.is_empty() { return None; } Some(features.join(",")) } /// Build the database. #[allow(unused_variables, clippy::arc_with_non_send_sync)] pub async fn build(self) -> Result { let features = self.build_features_string(); let db = turso_sdk_kit::rsapi::TursoDatabase::new(turso_sdk_kit::rsapi::TursoDatabaseConfig { path: self.path, experimental_features: features, async_io: true, encryption: self.encryption_opts, vfs: self.vfs, io: None, db_file: None, }); while let Some(io_c) = db.open()?.io() { // At this point IO must already be created let io = db .io() .expect("IO must have been set on the first call to db open"); io_c.wait_async(io.as_ref()) .await .map_err(TursoError::from)?; } Ok(Database { inner: db }) } } /// A database. /// /// The `Database` object points to a database and allows you to connect to it #[derive(Clone)] pub struct Database { inner: Arc, } impl Debug for Database { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Database").finish() } } impl Database { /// Connect to the database. pub fn connect(&self) -> Result { let conn = self.inner.connect()?; Ok(Connection::create(conn, None)) } } /// A prepared statement. #[derive(Clone)] pub struct Statement { conn: Connection, inner: Arc>>, } struct Execute { stmt: Statement, } assert_send_sync!(Execute); impl Future for Execute { type Output = Result; fn poll( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { match self.stmt.step(None, cx)? { Poll::Ready(_) => { let n_change = self.stmt.inner.lock().unwrap().n_change(); Poll::Ready(Ok(n_change as u64)) } Poll::Pending => Poll::Pending, } } } impl Statement { fn step( &self, columns: Option, cx: &mut std::task::Context<'_>, ) -> Poll>> { let mut stmt = self.inner.lock().unwrap(); match stmt.step(Some(cx.waker()))? { turso_sdk_kit::rsapi::TursoStatusCode::Row => { if let Some(columns) = columns { let mut values = Vec::with_capacity(columns); for i in 0..columns { let value = stmt.row_value(i)?; values.push(value.to_owned()); } Poll::Ready(Ok(Some(Row { values }))) } else { Poll::Ready(Err(Error::Misuse( "unexpected row during execution".to_string(), ))) } } turso_sdk_kit::rsapi::TursoStatusCode::Done => Poll::Ready(Ok(None)), turso_sdk_kit::rsapi::TursoStatusCode::Io => { stmt.run_io()?; if let Some(extra_io) = &self.conn.extra_io { extra_io(cx.waker().clone())?; } Poll::Pending } } } /// Query the database with this prepared statement. pub async fn query(&mut self, params: impl IntoParams) -> Result { self.reset()?; let mut stmt = self.inner.lock().unwrap(); let params = params.into_params()?; match params { params::Params::None => (), params::Params::Positional(values) => { for (i, value) in values.into_iter().enumerate() { stmt.bind_positional(i + 1, value.into())?; } } params::Params::Named(values) => { for (name, value) in values.into_iter() { let position = stmt.named_position(name)?; stmt.bind_positional(position, value.into())?; } } } let rows = Rows::new(self.clone()); Ok(rows) } /// Execute this prepared statement. pub async fn execute(&mut self, params: impl IntoParams) -> Result { { // Reset the statement before executing self.inner.lock().unwrap().reset()?; } let params = params.into_params()?; match params { params::Params::None => (), params::Params::Positional(values) => { for (i, value) in values.into_iter().enumerate() { let mut stmt = self.inner.lock().unwrap(); stmt.bind_positional(i + 1, value.into())?; } } params::Params::Named(values) => { for (name, value) in values.into_iter() { let mut stmt = self.inner.lock().unwrap(); let position = stmt.named_position(name)?; stmt.bind_positional(position, value.into())?; } } } let execute = Execute { stmt: self.clone() }; execute.await } /// Returns the number of columns in the result set. pub fn column_count(&self) -> usize { self.inner.lock().unwrap().column_count() } /// Returns the name of the column at the given index. pub fn column_name(&self, idx: usize) -> Result { let stmt = self.inner.lock().unwrap(); if idx >= stmt.column_count() { return Err(Error::Misuse(format!( "column index {idx} out of bounds (statement has {} columns)", stmt.column_count() ))); } Ok(stmt .column_name(idx) .expect("column index must be within valid range") .into_owned()) } /// Returns the names of all columns in the result set. pub fn column_names(&self) -> Vec { let stmt = self.inner.lock().unwrap(); let n = stmt.column_count(); (0..n) .map(|i| { stmt.column_name(i) .expect("column index must be within valid range") .into_owned() }) .collect() } /// Returns the index of the column with the given name. pub fn column_index(&self, name: &str) -> Result { let stmt = self.inner.lock().unwrap(); let n = stmt.column_count(); for i in 0..n { let col_name = stmt .column_name(i) .expect("column index must be within valid range"); if col_name.eq_ignore_ascii_case(name) { return Ok(i); } } Err(Error::Misuse(format!( "column '{name}' not found in result set" ))) } /// Returns columns of the result of this prepared statement. pub fn columns(&self) -> Vec { let stmt = self.inner.lock().unwrap(); let n = stmt.column_count(); let mut cols = Vec::with_capacity(n); for i in 0..n { let name = stmt .column_name(i) .expect("column index must be within valid range") .into_owned(); let decl_type = stmt.column_decltype(i); cols.push(Column { name, decl_type }); } cols } /// Reset internal statement state after previous execution so it can be reused again pub fn reset(&self) -> Result<()> { let mut stmt = self.inner.lock().unwrap(); stmt.reset()?; Ok(()) } /// Execute a query that returns the first [`Row`]. /// /// # Errors /// /// - Returns `QueryReturnedNoRows` if no rows were returned. pub async fn query_row(&mut self, params: impl IntoParams) -> Result { let mut rows = self.query(params).await?; let first_row = rows.next().await?.ok_or(Error::QueryReturnedNoRows)?; // Discard remaining rows so that the statement is executed to completion // Otherwise Drop of the statement will cause transaction rollback while rows.next().await?.is_some() {} Ok(first_row) } } /// Column information. pub struct Column { name: String, decl_type: Option, } impl Column { /// Return the name of the column. pub fn name(&self) -> &str { &self.name } /// Returns the type of the column. pub fn decl_type(&self) -> Option<&str> { self.decl_type.as_deref() } } pub trait IntoValue { fn into_value(self) -> Result; } #[derive(Debug, Clone)] pub enum Params { None, Positional(Vec), Named(Vec<(String, Value)>), } pub struct Transaction {} #[cfg(test)] mod tests { use super::*; use tempfile::NamedTempFile; #[tokio::test] async fn test_database_persistence() -> Result<()> { let temp_file = NamedTempFile::new().unwrap(); let db_path = temp_file.path().to_str().unwrap(); // First, create the database, a table, and insert some data { let db = Builder::new_local(db_path).build().await?; let conn = db.connect()?; conn.execute( "CREATE TABLE test_persistence (id INTEGER PRIMARY KEY, name TEXT NOT NULL);", (), ) .await?; conn.execute("INSERT INTO test_persistence (name) VALUES ('Alice');", ()) .await?; conn.execute("INSERT INTO test_persistence (name) VALUES ('Bob');", ()) .await?; } // db and conn are dropped here, simulating closing // Now, re-open the database and check if the data is still there let db = Builder::new_local(db_path).build().await?; let conn = db.connect()?; let mut rows = conn .query("SELECT name FROM test_persistence ORDER BY id;", ()) .await?; let row1 = rows.next().await?.expect("Expected first row"); assert_eq!(row1.get_value(0)?, Value::Text("Alice".to_string())); let row2 = rows.next().await?.expect("Expected second row"); assert_eq!(row2.get_value(0)?, Value::Text("Bob".to_string())); assert!(rows.next().await?.is_none(), "Expected no more rows"); Ok(()) } #[tokio::test] async fn test_database_persistence_many_frames() -> Result<()> { let temp_file = NamedTempFile::new().unwrap(); let db_path = temp_file.path().to_str().unwrap(); const NUM_INSERTS: usize = 100; const TARGET_STRING_LEN: usize = 1024; // 1KB let mut original_data = Vec::with_capacity(NUM_INSERTS); for i in 0..NUM_INSERTS { let prefix = format!("test_string_{i:04}_"); let padding_len = TARGET_STRING_LEN.saturating_sub(prefix.len()); let padding: String = "A".repeat(padding_len); original_data.push(format!("{prefix}{padding}")); } // First, create the database, a table, and insert many large strings { let db = Builder::new_local(db_path).build().await?; let conn = db.connect()?; conn.execute( "CREATE TABLE test_large_persistence (id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT NOT NULL);", (), ) .await?; for data_val in &original_data { conn.execute( "INSERT INTO test_large_persistence (data) VALUES (?);", params::Params::Positional(vec![Value::Text(data_val.clone())]), ) .await?; } } // db and conn are dropped here, simulating closing { // Now, re-open the database and check if the data is still there let db = Builder::new_local(db_path).build().await?; let conn = db.connect()?; let mut rows = conn .query("SELECT data FROM test_large_persistence ORDER BY id;", ()) .await?; for (i, value) in original_data.iter().enumerate().take(NUM_INSERTS) { let row = rows .next() .await? .unwrap_or_else(|| panic!("Expected row {i} but found None")); assert_eq!( row.get_value(0)?, Value::Text(value.clone()), "Mismatch in retrieved data for row {i}" ); } assert!( rows.next().await?.is_none(), "Expected no more rows after retrieving all inserted data" ); // Delete the WAL file only and try to re-open and query let wal_path = format!("{db_path}-wal"); std::fs::remove_file(&wal_path) .map_err(|e| eprintln!("Warning: Failed to delete WAL file for test: {e}")) .unwrap(); } // Attempt to re-open the database after deleting WAL and assert that table is missing. let db_after_wal_delete = Builder::new_local(db_path).build().await?; let conn_after_wal_delete = db_after_wal_delete.connect()?; let query_result_after_wal_delete = conn_after_wal_delete .query("SELECT data FROM test_large_persistence ORDER BY id;", ()) .await; match query_result_after_wal_delete { Ok(_) => panic!("Query succeeded after WAL deletion and DB reopen, but was expected to fail because the table definition should have been in the WAL."), Err(Error::Error(msg)) => { assert!( msg.contains("no such table: test_large_persistence"), "Expected 'test_large_persistence not found' error, but got: {msg}" ); } Err(e) => panic!( "Expected SqlExecutionFailure for 'no such table', but got a different error: {e:?}" ), } Ok(()) } #[tokio::test] async fn test_rows_column_names() -> Result<()> { let db = Builder::new_local(":memory:").build().await?; let conn = db.connect()?; conn.execute( "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);", (), ) .await?; conn.execute( "INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.org');", (), ) .await?; let rows = conn.query("SELECT id, name, email FROM users;", ()).await?; // columns() let columns = rows.columns(); let names: Vec<&str> = columns.iter().map(|c| c.name()).collect(); assert_eq!(names, vec!["id", "name", "email"]); // column_count() assert_eq!(rows.column_count(), 3); // column_name() assert_eq!(rows.column_name(0)?, "id"); assert_eq!(rows.column_name(1)?, "name"); assert_eq!(rows.column_name(2)?, "email"); assert!(rows.column_name(3).is_err()); // column_names() assert_eq!(rows.column_names(), vec!["id", "name", "email"]); // column_index() assert_eq!(rows.column_index("id")?, 0); assert_eq!(rows.column_index("name")?, 1); assert_eq!(rows.column_index("email")?, 2); assert_eq!(rows.column_index("EMAIL")?, 2); // case-insensitive assert!(rows.column_index("nonexistent").is_err()); Ok(()) } #[tokio::test] async fn test_database_persistence_write_one_frame_many_times() -> Result<()> { let temp_file = NamedTempFile::new().unwrap(); let db_path = temp_file.path().to_str().unwrap(); for i in 0..100 { { let db = Builder::new_local(db_path).build().await?; let conn = db.connect()?; conn.execute("CREATE TABLE IF NOT EXISTS test_persistence (id INTEGER PRIMARY KEY, name TEXT NOT NULL);", ()).await?; conn.execute("INSERT INTO test_persistence (name) VALUES ('Alice');", ()) .await?; } { let db = Builder::new_local(db_path).build().await?; let conn = db.connect()?; let mut rows_iter = conn .query("SELECT count(*) FROM test_persistence;", ()) .await?; let rows = rows_iter.next().await?.unwrap(); assert_eq!(rows.get_value(0)?, Value::Integer(i as i64 + 1)); assert!(rows_iter.next().await?.is_none()); } } Ok(()) } #[tokio::test] async fn test_parallel_writes_and_wal_size() -> Result<()> { let temp_dir = tempfile::tempdir().unwrap(); let db_path = temp_dir.path().join("test.db"); let db_path_str = db_path.to_str().unwrap(); let db = Builder::new_local(db_path_str).build().await?; let conn = db.connect()?; conn.execute( "CREATE TABLE test_data (id INTEGER PRIMARY KEY AUTOINCREMENT, payload TEXT NOT NULL);", (), ) .await?; // Generate a ~200KB payload let payload = "X".repeat(200 * 1024); // Parallel writes: spawn 8 connections, each inserting 5 rows let mut handles = Vec::new(); for conn_id in 0..8u32 { let db = db.clone(); let payload = payload.clone(); handles.push(tokio::spawn(async move { let conn = db.connect().unwrap(); for row_id in 0..5u32 { let tag = format!("conn{conn_id}_row{row_id}"); let data = format!("{tag}_{payload}"); loop { match conn .execute( "INSERT INTO test_data (payload) VALUES (?);", params::Params::Positional(vec![Value::Text(data.clone())]), ) .await { Ok(_) => break, Err(Error::Busy(_)) => { tokio::time::sleep(std::time::Duration::from_millis(10)).await; continue; } Err(e) => panic!("Insert failed: {e:?}"), } } } })); } for h in handles { h.await.unwrap(); } // Sequential writes: 3 more large inserts for i in 0..3 { let data = format!("sequential_{i}_{payload}"); conn.execute( "INSERT INTO test_data (payload) VALUES (?);", params::Params::Positional(vec![Value::Text(data)]), ) .await?; } // Verify row count: 8*5 + 3 = 43 let mut rows = conn.query("SELECT count(*) FROM test_data;", ()).await?; let row = rows.next().await?.unwrap(); assert_eq!(row.get_value(0)?, Value::Integer(43)); // Report WAL size let wal_path = format!("{db_path_str}-wal"); let wal_size = std::fs::metadata(&wal_path).map(|m| m.len()).unwrap_or(0); eprintln!( "WAL size after all writes: {} bytes ({:.2} KB)", wal_size, wal_size as f64 / 1024.0 ); assert!(wal_size > 0, "WAL file should exist and be non-empty"); Ok(()) } } ================================================ FILE: bindings/rust/src/params.rs ================================================ //! This module contains all `Param` related utilities and traits. use std::borrow::Cow; use crate::{Error, Result, Value}; mod sealed { pub trait Sealed {} } use sealed::Sealed; /// Converts some type into parameters that can be passed /// to libsql. /// /// The trait is sealed and not designed to be implemented by hand /// but instead provides a few ways to use it. /// /// # Passing parameters to libsql /// /// Many functions in this library let you pass parameters to libsql. Doing this /// lets you avoid any risk of SQL injection, and is simpler than escaping /// things manually. These functions generally contain some parameter that generically /// accepts some implementation this trait. /// /// # Positional parameters /// /// These can be supplied in a few ways: /// /// - For heterogeneous parameter lists of 16 or less items a tuple syntax is supported /// by doing `(1, "foo")`. /// - For hetergeneous parameter lists of 16 or greater, the [`turso::params!`] is supported /// by doing `turso::params![1, "foo"]`. /// - For homogeneous parameter types (where they are all the same type), const arrays are /// supported by doing `[1, 2, 3]`. /// /// # Example (positional) /// /// ```rust,no_run /// # use turso::{Connection, params}; /// # async fn run(conn: Connection) -> turso::Result<()> { /// let mut stmt = conn.prepare("INSERT INTO test (a, b) VALUES (?1, ?2)").await?; /// /// // Using a tuple: /// stmt.execute((0, "foobar")).await?; /// /// // Using `turso::params!`: /// stmt.execute(params![1i32, "blah"]).await?; /// /// // array literal — non-references /// stmt.execute([2i32, 3i32]).await?; /// /// // array literal — references /// stmt.execute(["foo", "bar"]).await?; /// /// // Slice literal, references: /// stmt.execute([2i32, 3i32]).await?; /// /// # Ok(()) /// # } /// ``` /// /// # Named parameters /// /// Named parameter keys must include the SQL prefix used in the statement, /// for example `:name`, `@name`, `$name`, or `?1`. /// /// - For heterogeneous parameter lists of 16 or less items a tuple syntax is supported /// by doing `((":key1", 1), (":key2", "foo"))`. /// - For heterogeneous parameter lists of 16 or greater, the [`turso::params!`] is supported /// by doing `turso::named_params![":key1": 1, ":key2": "foo"]`. /// - For homogeneous parameter types (where they are all the same type), const arrays are /// supported by doing `[(":key1", 1), (":key2", 2), (":key3", 3)]`. /// /// # Example (named) /// /// ```rust,no_run /// # use turso::{Connection, named_params}; /// # async fn run(conn: Connection) -> turso::Result<()> { /// let mut stmt = conn.prepare("INSERT INTO test (a, b) VALUES (:key1, :key2)").await?; /// /// // Using a tuple: /// stmt.execute(((":key1", 0), (":key2", "foobar"))).await?; /// /// // Using `turso::named_params!`: /// stmt.execute(named_params! {":key1": 1i32, ":key2": "blah" }).await?; /// /// // const array: /// stmt.execute([(":key1", 2i32), (":key2", 3i32)]).await?; /// /// # Ok(()) /// # } /// ``` pub trait IntoParams: Sealed { // Hide this because users should not be implementing this // themselves. We should consider sealing this trait. #[doc(hidden)] fn into_params(self) -> Result; } #[derive(Debug, Clone)] #[doc(hidden)] pub enum Params { None, Positional(Vec), Named(Vec<(Cow<'static, str>, Value)>), } /// Convert an owned iterator into Params. /// /// # Example /// /// ```rust /// # use turso::{Connection, params_from_iter, Rows}; /// # async fn run(conn: &Connection) { /// /// let iter = vec![1, 2, 3]; /// /// conn.query( /// "SELECT * FROM users WHERE id IN (?1, ?2, ?3)", /// params_from_iter(iter) /// ) /// .await /// .unwrap(); /// # } /// ``` pub fn params_from_iter(iter: I) -> impl IntoParams where I: IntoIterator, I::Item: IntoValue, { iter.into_iter().collect::>() } impl Sealed for () {} impl IntoParams for () { fn into_params(self) -> Result { Ok(Params::None) } } impl Sealed for Params {} impl IntoParams for Params { fn into_params(self) -> Result { Ok(self) } } impl Sealed for Vec {} impl IntoParams for Vec { fn into_params(self) -> Result { let values = self .into_iter() .map(|i| i.into_value()) .collect::>>()?; Ok(Params::Positional(values)) } } impl Sealed for Vec<(String, T)> {} impl IntoParams for Vec<(String, T)> { fn into_params(self) -> Result { let values = self .into_iter() .map(|(k, v)| Ok((Cow::Owned(k), v.into_value()?))) .collect::>>()?; Ok(Params::Named(values)) } } impl Sealed for [T; N] {} impl IntoParams for [T; N] { fn into_params(self) -> Result { self.into_iter().collect::>().into_params() } } // Named parameters with static string keys to avoid String allocations. impl Sealed for [(&'static str, T); N] {} impl IntoParams for [(&'static str, T); N] { fn into_params(self) -> Result { let values = self .into_iter() .map(|(k, v)| Ok((Cow::Borrowed(k), v.into_value()?))) .collect::>>()?; Ok(Params::Named(values)) } } impl Sealed for &[T; N] {} impl IntoParams for &[T; N] { fn into_params(self) -> Result { self.iter().cloned().collect::>().into_params() } } // NOTICE: heavily inspired by rusqlite macro_rules! tuple_into_params { ($count:literal : $(($field:tt $ftype:ident)),* $(,)?) => { impl<$($ftype,)*> Sealed for ($($ftype,)*) where $($ftype: IntoValue,)* {} impl<$($ftype,)*> IntoParams for ($($ftype,)*) where $($ftype: IntoValue,)* { fn into_params(self) -> Result { let params = Params::Positional(vec![$(self.$field.into_value()?),*]); Ok(params) } } } } macro_rules! named_tuple_into_params { ($count:literal : $(($field:tt $ftype:ident)),* $(,)?) => { impl<$($ftype,)*> Sealed for ($((&'static str, $ftype),)*) where $($ftype: IntoValue,)* {} impl<$($ftype,)*> IntoParams for ($((&'static str, $ftype),)*) where $($ftype: IntoValue,)* { fn into_params(self) -> Result { let params = Params::Named(vec![$((Cow::Borrowed(self.$field.0), self.$field.1.into_value()?)),*]); Ok(params) } } } } named_tuple_into_params!(1: (0 A)); named_tuple_into_params!(2: (0 A), (1 B)); named_tuple_into_params!(3: (0 A), (1 B), (2 C)); named_tuple_into_params!(4: (0 A), (1 B), (2 C), (3 D)); named_tuple_into_params!(5: (0 A), (1 B), (2 C), (3 D), (4 E)); named_tuple_into_params!(6: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F)); named_tuple_into_params!(7: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G)); named_tuple_into_params!(8: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H)); named_tuple_into_params!(9: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I)); named_tuple_into_params!(10: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J)); named_tuple_into_params!(11: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K)); named_tuple_into_params!(12: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L)); named_tuple_into_params!(13: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M)); named_tuple_into_params!(14: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N)); named_tuple_into_params!(15: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N), (14 O)); named_tuple_into_params!(16: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N), (14 O), (15 P)); tuple_into_params!(1: (0 A)); tuple_into_params!(2: (0 A), (1 B)); tuple_into_params!(3: (0 A), (1 B), (2 C)); tuple_into_params!(4: (0 A), (1 B), (2 C), (3 D)); tuple_into_params!(5: (0 A), (1 B), (2 C), (3 D), (4 E)); tuple_into_params!(6: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F)); tuple_into_params!(7: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G)); tuple_into_params!(8: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H)); tuple_into_params!(9: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I)); tuple_into_params!(10: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J)); tuple_into_params!(11: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K)); tuple_into_params!(12: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L)); tuple_into_params!(13: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M)); tuple_into_params!(14: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N)); tuple_into_params!(15: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N), (14 O)); tuple_into_params!(16: (0 A), (1 B), (2 C), (3 D), (4 E), (5 F), (6 G), (7 H), (8 I), (9 J), (10 K), (11 L), (12 M), (13 N), (14 O), (15 P)); // TODO: Should we rename this to `ToSql` which makes less sense but // matches the error variant we have in `Error`. Or should we change the // error variant to match this breaking the few people that currently use // this error variant. pub trait IntoValue { fn into_value(self) -> Result; } impl IntoValue for T where T: TryInto, T::Error: Into, { fn into_value(self) -> Result { self.try_into() .map_err(|e| Error::ToSqlConversionFailure(e.into())) } } impl IntoValue for Result { fn into_value(self) -> Result { self } } /// Construct positional params from a heterogeneous set of params types. #[macro_export] macro_rules! params { () => { () }; ($($value:expr),* $(,)?) => {{ use $crate::params::IntoValue; [$($value.into_value()),*] }}; } /// Construct named params from a heterogeneous set of params types. #[macro_export] macro_rules! named_params { () => { () }; ($($param_name:literal: $value:expr),* $(,)?) => {{ use $crate::params::IntoValue; [$(($param_name, $value.into_value())),*] }}; } #[cfg(test)] mod tests { use crate::Value; #[test] fn test_serialize_array() { assert_eq!( params!([0; 16])[0].as_ref().unwrap(), &Value::Blob(vec![0; 16]) ); } } ================================================ FILE: bindings/rust/src/rows.rs ================================================ use crate::{assert_send_sync, Column, Error, Result, Statement, Value}; use std::fmt::Debug; use std::future::Future; /// Results of a prepared statement query. pub struct Rows { inner: Statement, } impl Rows { pub(crate) fn new(inner: Statement) -> Self { Self { inner } } /// Returns the number of columns in the result set. pub fn column_count(&self) -> usize { self.inner.column_count() } /// Returns the name of the column at the given index. pub fn column_name(&self, idx: usize) -> Result { self.inner.column_name(idx) } /// Returns the names of all columns in the result set. pub fn column_names(&self) -> Vec { self.inner.column_names() } /// Returns the index of the column with the given name. pub fn column_index(&self, name: &str) -> Result { self.inner.column_index(name) } /// Returns columns of the result set. pub fn columns(&self) -> Vec { self.inner.columns() } /// Fetch the next row of this result set. pub async fn next(&mut self) -> Result> { struct Next { columns: usize, stmt: Statement, } impl Future for Next { type Output = Result>; fn poll( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { self.stmt.step(Some(self.columns), cx) } } assert_send_sync!(Next); let next = Next { columns: self.inner.inner.lock().unwrap().column_count(), stmt: self.inner.clone(), }; next.await } } /// Query result row. #[derive(Debug, PartialEq)] pub struct Row { pub(crate) values: Vec, } impl Row { pub fn get_value(&self, idx: usize) -> Result { let val = self.values.get(idx).ok_or_else(|| { Error::Misuse(format!( "column index {idx} out of bounds (row has {} columns)", self.values.len() )) })?; match val { turso_sdk_kit::rsapi::Value::Numeric(turso_sdk_kit::rsapi::Numeric::Integer(i)) => { Ok(Value::Integer(*i)) } turso_sdk_kit::rsapi::Value::Numeric(turso_sdk_kit::rsapi::Numeric::Float(f)) => { Ok(Value::Real(f64::from(*f))) } turso_sdk_kit::rsapi::Value::Null => Ok(Value::Null), turso_sdk_kit::rsapi::Value::Text(text) => { Ok(Value::Text(text.value.clone().into_owned())) } turso_sdk_kit::rsapi::Value::Blob(items) => Ok(Value::Blob(items.to_vec())), } } pub fn get(&self, idx: usize) -> Result where T: turso_sdk_kit::rsapi::FromValue, { let val = self.values.get(idx).ok_or_else(|| { Error::Misuse(format!( "column index {idx} out of bounds (row has {} columns)", self.values.len() )) })?; T::from_sql(val.clone()).map_err(|err| Error::ConversionFailure(err.to_string())) } pub fn column_count(&self) -> usize { self.values.len() } } ================================================ FILE: bindings/rust/src/sync.rs ================================================ use std::{ future::Future, io::ErrorKind, pin::Pin, sync::{Arc, Mutex}, task::{Context, Poll, Waker}, time::Duration, }; use bytes::Bytes; use http_body_util::{BodyExt, Full}; use hyper::{header::AUTHORIZATION, Request}; use hyper_tls::HttpsConnector; use hyper_util::{ client::legacy::{connect::HttpConnector, Client}, rt::TokioExecutor, }; use tokio::sync::mpsc; use crate::{connection::Connection, Error, Result}; // Public re-exports of sync types for users of this crate. pub use turso_sync_sdk_kit::rsapi::DatabaseSyncStats; pub use turso_sync_sdk_kit::rsapi::PartialBootstrapStrategy; pub use turso_sync_sdk_kit::rsapi::PartialSyncOpts; // Constants used across the sync module const DEFAULT_CLIENT_NAME: &str = "turso-sync-rust"; /// Encryption cipher for Turso Cloud remote encryption. /// These match the server-side encryption settings. #[derive(Debug, Clone, Copy)] pub enum RemoteEncryptionCipher { Aes256Gcm, Aes128Gcm, ChaCha20Poly1305, Aegis128L, Aegis128X2, Aegis128X4, Aegis256, Aegis256X2, Aegis256X4, } impl RemoteEncryptionCipher { /// Returns the total reserved bytes as required by the server pub fn reserved_bytes(&self) -> usize { match self { Self::Aes256Gcm | Self::Aes128Gcm | Self::ChaCha20Poly1305 => 28, Self::Aegis128L | Self::Aegis128X2 | Self::Aegis128X4 => 32, Self::Aegis256 | Self::Aegis256X2 | Self::Aegis256X4 => 48, } } } impl std::str::FromStr for RemoteEncryptionCipher { type Err = String; fn from_str(s: &str) -> std::result::Result { match s.to_lowercase().as_str() { "aes256gcm" | "aes-256-gcm" => Ok(Self::Aes256Gcm), "aes128gcm" | "aes-128-gcm" => Ok(Self::Aes128Gcm), "chacha20poly1305" | "chacha20-poly1305" => Ok(Self::ChaCha20Poly1305), "aegis128l" | "aegis-128l" => Ok(Self::Aegis128L), "aegis128x2" | "aegis-128x2" => Ok(Self::Aegis128X2), "aegis128x4" | "aegis-128x4" => Ok(Self::Aegis128X4), "aegis256" | "aegis-256" => Ok(Self::Aegis256), "aegis256x2" | "aegis-256x2" => Ok(Self::Aegis256X2), "aegis256x4" | "aegis-256x4" => Ok(Self::Aegis256X4), _ => Err(format!( "unknown cipher: '{s}'. Supported: aes256gcm, aes128gcm, chacha20poly1305, \ aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4" )), } } } // Builder for a synced database. pub struct Builder { // Absolute or relative path to local database file (":memory:" is supported). path: String, // Remote URL base. Supports https://, http:// and libsql:// (translated to https://). remote_url: Option, // Optional authorization token (e.g., Bearer token). auth_token: Option, // Optional custom client identifier used by the sync engine for telemetry/tracing. client_name: Option, // Optional long-poll timeout when waiting for server changes. long_poll_timeout: Option, // Whether to bootstrap a database if it's empty (download schema and initial data). bootstrap_if_empty: bool, // Partial sync configuration (EXPERIMENTAL). partial_sync_config_experimental: Option, // Encryption key (base64-encoded) for the Turso Cloud database remote_encryption_key: Option, // Encryption cipher for the Turso Cloud database remote_encryption_cipher: Option, } impl Builder { // Create a new Builder for a synced database. pub fn new_remote(path: &str) -> Self { Self { path: path.to_string(), remote_url: None, auth_token: None, client_name: None, long_poll_timeout: None, bootstrap_if_empty: true, partial_sync_config_experimental: None, remote_encryption_key: None, remote_encryption_cipher: None, } } // Set remote_url for HTTP requests. // If remote_url omitted in configuration - tursodb will try to load it from the metadata file pub fn with_remote_url(mut self, remote_url: impl Into) -> Self { self.remote_url = Some(remote_url.into()); self } // Set optional authorization token for HTTP requests. pub fn with_auth_token(mut self, token: impl Into) -> Self { self.auth_token = Some(token.into()); self } // Set custom client name (defaults to 'turso-sync-rust'). pub fn with_client_name(mut self, name: impl Into) -> Self { self.client_name = Some(name.into()); self } // Set long poll timeout for waiting remote changes. pub fn with_long_poll_timeout(mut self, timeout: Duration) -> Self { self.long_poll_timeout = Some(timeout); self } // Configure bootstrap behavior for empty databases. pub fn bootstrap_if_empty(mut self, enable: bool) -> Self { self.bootstrap_if_empty = enable; self } // Set experimental partial sync configuration. pub fn with_partial_sync_opts_experimental(mut self, opts: PartialSyncOpts) -> Self { self.partial_sync_config_experimental = Some(opts); self } /// Set encryption key (base64-encoded) and cipher for the Turso Cloud database. /// The cipher is used to calculate the correct reserved_bytes for the database. pub fn with_remote_encryption( mut self, base64_key: impl Into, cipher: RemoteEncryptionCipher, ) -> Self { self.remote_encryption_key = Some(base64_key.into()); self.remote_encryption_cipher = Some(cipher); self } /// Set encryption key (base64-encoded) for the Turso Cloud database. /// The key will be sent as x-turso-encryption-key header with sync HTTP requests. /// Note: For deferred sync (no initial bootstrap), use with_remote_encryption() instead /// to also specify the cipher for correct reserved_bytes calculation. pub fn with_remote_encryption_key(mut self, base64_key: impl Into) -> Self { self.remote_encryption_key = Some(base64_key.into()); self } // Build the synced database object, initialize and open it. pub async fn build(self) -> Result { // Build core database config for the embedded engine. let db_config = turso_sdk_kit::rsapi::TursoDatabaseConfig { path: self.path.clone(), experimental_features: None, // IMPORTANT: async IO must be turned on to delegate IO to this layer. async_io: true, encryption: None, vfs: None, io: None, db_file: None, }; let url = if let Some(remote_url) = &self.remote_url { Some(normalize_base_url(remote_url).map_err(Error::Error)?) } else { None }; // Calculate reserved_bytes from cipher if provided. let reserved_bytes = self .remote_encryption_cipher .map(|cipher| cipher.reserved_bytes()); // Build sync engine config. let sync_config = turso_sync_sdk_kit::rsapi::TursoDatabaseSyncConfig { path: self.path.clone(), remote_url: url.clone(), client_name: self .client_name .clone() .unwrap_or_else(|| DEFAULT_CLIENT_NAME.to_string()), long_poll_timeout_ms: self .long_poll_timeout .map(|d| d.as_millis().min(u32::MAX as u128) as u32), bootstrap_if_empty: self.bootstrap_if_empty, reserved_bytes, partial_sync_opts: self.partial_sync_config_experimental.clone(), remote_encryption_key: self.remote_encryption_key.clone(), }; // Create sync wrapper. let sync = turso_sync_sdk_kit::rsapi::TursoDatabaseSync::::new(db_config, sync_config) .map_err(Error::from)?; // IO worker will process SyncEngine IO queue on a dedicated tokio thread. let io_worker = IoWorker::spawn(sync.clone(), url, self.auth_token.clone()); // Create (bootstrap + open) database in one go. let op = sync.create(); drive_operation(op, io_worker.clone()).await?; Ok(Database { sync, io: io_worker, }) } } // Synced Database handle. #[derive(Clone)] pub struct Database { sync: Arc>, io: Arc, } impl Database { // Push local changes to the remote. pub async fn push(&self) -> Result<()> { let op = self.sync.push_changes(); drive_operation(op, self.io.clone()).await?; Ok(()) } // Pull remote changes; returns true if any changes were applied. pub async fn pull(&self) -> Result { // First, wait for changes... let op = self.sync.wait_changes(); let result = drive_operation_result(op, self.io.clone()).await?; let mut has_changes = false; if let Some( turso_sync_sdk_kit::turso_async_operation::TursoAsyncOperationResult::Changes { changes, }, ) = result { if !changes.empty() { has_changes = true; // Then, apply them. let op_apply = self.sync.apply_changes(changes); drive_operation(op_apply, self.io.clone()).await?; } } Ok(has_changes) } // Force WAL checkpoint for the main database. pub async fn checkpoint(&self) -> Result<()> { let op = self.sync.checkpoint(); drive_operation(op, self.io.clone()).await?; Ok(()) } // Retrieve sync statistics for the database. pub async fn stats(&self) -> Result { let op = self.sync.stats(); let result = drive_operation_result(op, self.io.clone()).await?; match result { Some(turso_sync_sdk_kit::turso_async_operation::TursoAsyncOperationResult::Stats { stats, }) => Ok(stats), _ => Err(Error::Misuse( "unexpected result type from stats operation".to_string(), )), } } // Create a SQL connection to the synced database. pub async fn connect(&self) -> Result { let op = self.sync.connect(); let result = drive_operation_result(op, self.io.clone()).await?; match result { Some( turso_sync_sdk_kit::turso_async_operation::TursoAsyncOperationResult::Connection { connection, }, ) => { // Provide extra_io callback to kick IO worker when driver needs to make progress. let io = self.io.clone(); let extra_io = Arc::new(move |waker| { io.register(waker); io.kick(); Ok(()) }); Ok(Connection::create(connection, Some(extra_io))) } _ => Err(Error::Misuse( "unexpected result type from connect operation".to_string(), )), } } } // Drive an operation that has no result (returns None when done). async fn drive_operation( op: Box, io: Arc, ) -> Result<()> { let fut = AsyncOpFuture::new(op, io); fut.await.map(|_| ()) } // Drive an operation and retrieve its result (if any). async fn drive_operation_result( op: Box, io: Arc, ) -> Result> { let fut = AsyncOpFuture::new(op, io); fut.await } // Custom Future that integrates with TursoDatabaseAsyncOperation and our IO worker. struct AsyncOpFuture { op: Option>, io: Arc, } impl AsyncOpFuture { fn new( op: Box, io: Arc, ) -> Self { Self { op: Some(op), io } } } impl Future for AsyncOpFuture { type Output = Result>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = unsafe { self.get_unchecked_mut() }; let Some(op) = &this.op else { return Poll::Ready(Err(Error::Misuse( "operation future has been already completed".to_string(), ))); }; this.io.register(cx.waker().clone()); // Try to resume the operation. match op.resume() { Ok(turso_sdk_kit::rsapi::TursoStatusCode::Done) => { // Try to take the result (may be None). let result = op.take_result().map(Some).or_else(|err| match err { turso_sdk_kit::rsapi::TursoError::Misuse(msg) if msg.contains("operation has no result") => { Ok(None) } other => Err(Error::from(other)), })?; // Drop the op and complete. this.op.take(); Poll::Ready(Ok(result)) } Ok(turso_sdk_kit::rsapi::TursoStatusCode::Io) => { // Kick IO worker to process queued IO. this.io.kick(); // Wait until IO worker makes progress and wakes us. Poll::Pending } Ok(turso_sdk_kit::rsapi::TursoStatusCode::Row) => { // Not expected from top-level sync operations. Poll::Ready(Err(Error::Misuse( "unexpected row status in sync operation".to_string(), ))) } Err(e) => Poll::Ready(Err(Error::from(e))), } } } // Normalize remote base URL, mapping libsql:// to https:// and validating allowed schemes. fn normalize_base_url(input: &str) -> std::result::Result { let s = input.trim(); let s = if let Some(rest) = s.strip_prefix("libsql://") { format!("https://{rest}") } else { s.to_string() }; // Accept http or https only if !(s.starts_with("https://") || s.starts_with("http://")) { return Err(format!("unsupported remote URL scheme: {input}")); } // Ensure no trailing slash to make join predictable. let base = s.trim_end_matches('/').to_string(); Ok(base) } // The IO worker owns a dedicated Tokio runtime on a separate thread, and processes // the SyncEngine IO queue (HTTP and atomic file operations). struct IoWorker { // Reference to the sync database to pull IO items from its queue. sync: Arc>, // Normalized base URL (http/https). base_url: Option, // Optional auth token. auth_token: Option, // Channel to wake the worker to process IO. tx: mpsc::UnboundedSender<()>, // Wakers to notify pending futures when IO makes progress. wakers: Arc>>, } impl IoWorker { fn spawn( sync: Arc>, base_url: Option, auth_token: Option, ) -> Arc { let (tx, rx) = mpsc::unbounded_channel::<()>(); let wakers = Arc::new(Mutex::new(Vec::new())); let worker = Arc::new(Self { sync, base_url, auth_token, tx, wakers: wakers.clone(), }); // Spin a separate Tokio runtime on its own thread to process IO queue. let worker_clone = worker.clone(); std::thread::Builder::new() .name("turso-sync-io".to_string()) .spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("failed to build IO runtime"); rt.block_on(async move { IoWorker::run_loop(worker_clone, rx, wakers).await; }); }) .expect("failed to spawn IO worker thread"); worker } // Register a waker to be awakened upon IO progress. fn register(&self, waker: Waker) { let mut wakers = self.wakers.lock().unwrap(); wakers.push(waker); } // Kick the IO worker to process IO queue. fn kick(&self) { let _ = self.tx.send(()); } // Called from the IO thread once progress has been made to notify all pending futures. fn notify_progress(wakers: &Arc>>) { let wakers = { let mut guard = wakers.lock().unwrap(); std::mem::take(&mut *guard) }; for w in wakers { w.wake(); } } async fn run_loop( this: Arc, mut rx: mpsc::UnboundedReceiver<()>, wakers: Arc>>, ) { // Create HTTPS-capable Hyper client. let mut http_connector = HttpConnector::new(); http_connector.enforce_http(false); let https: HttpsConnector = HttpsConnector::new(); let client: Client, Full> = Client::builder(TokioExecutor::new()).build::<_, Full>(https); while rx.recv().await.is_some() { // Process all pending items in the sync IO queue. let mut made_progress = false; loop { let item = this.sync.take_io_item(); let Some(item) = item else { this.sync.step_io_callbacks(); IoWorker::notify_progress(&wakers); break; }; made_progress = true; match item.get_request() { turso_sync_sdk_kit::sync_engine_io::SyncEngineIoRequest::Http { url, method, path, body, headers, } => { IoWorker::process_http( &this, &client, url.as_deref(), method, path, body.as_ref().map(|v| Bytes::from(v.clone())), headers, item.get_completion().clone(), ) .await; } turso_sync_sdk_kit::sync_engine_io::SyncEngineIoRequest::FullRead { path } => { IoWorker::process_full_read( path, item.get_completion().clone(), &this.sync, ) .await; } turso_sync_sdk_kit::sync_engine_io::SyncEngineIoRequest::FullWrite { path, content, } => { IoWorker::process_full_write( path, content, item.get_completion().clone(), &this.sync, ) .await; } } } // Run queued IO callbacks and wake all pending ops, yielding control // to allow them to make progress before we loop again. if made_progress { this.sync.step_io_callbacks(); IoWorker::notify_progress(&wakers); // Let waiting tasks run on their executors. tokio::task::yield_now().await; } } } #[allow(clippy::too_many_arguments)] async fn process_http( this: &Arc, client: &Client, Full>, url: Option<&str>, method: &str, path: &str, body: Option, headers: &[(String, String)], completion: turso_sync_sdk_kit::sync_engine_io::SyncEngineIoCompletion, ) { // Build full URL. let full_url = if path.starts_with("http://") || path.starts_with("https://") { path.to_string() } else { // Ensure the path begins with '/' let p = if path.starts_with('/') { path.to_string() } else { format!("/{path}") }; let Some(url) = this.base_url.as_deref().or(url) else { completion.poison("remote_url is not available".to_string()); return; }; format!("{url}{p}") }; let mut builder = Request::builder().method(method).uri(&full_url); // Set headers from request if let Some(headers_map) = builder.headers_mut() { for (k, v) in headers { if let Ok(name) = hyper::header::HeaderName::try_from(k.as_str()) { if let Ok(value) = hyper::header::HeaderValue::try_from(v.as_str()) { headers_map.insert(name, value); } } } // Add Authorization header if not already set if let Some(token) = &this.auth_token { if !headers_map.contains_key(AUTHORIZATION) { let value = format!("Bearer {token}"); if let Ok(hv) = hyper::header::HeaderValue::try_from(value.as_str()) { headers_map.insert(AUTHORIZATION, hv); } } } } // Body must be Full to match the client type. let req_body = Full::new(body.unwrap_or_default()); let request = match builder.body(req_body) { Ok(r) => r, Err(err) => { completion.poison(format!("failed to build request: {err}")); this.sync.step_io_callbacks(); return; } }; let mut response = match client.request(request).await { Ok(r) => r, Err(err) => { completion.poison(format!("http request failed: {err}")); this.sync.step_io_callbacks(); return; } }; // Propagate status let status = response.status().as_u16(); completion.status(status as u32); this.sync.step_io_callbacks(); IoWorker::notify_progress(&this.wakers); // Stream response body in chunks while let Some(frame_res) = response.body_mut().frame().await { match frame_res { Ok(frame) => { if let Some(chunk) = frame.data_ref() { completion.push_buffer(chunk.clone()); this.sync.step_io_callbacks(); IoWorker::notify_progress(&this.wakers); } } Err(err) => { completion.poison(format!("error reading response body: {err}")); this.sync.step_io_callbacks(); IoWorker::notify_progress(&this.wakers); return; } } } // Done streaming completion.done(); this.sync.step_io_callbacks(); IoWorker::notify_progress(&this.wakers); } async fn process_full_read( path: &str, completion: turso_sync_sdk_kit::sync_engine_io::SyncEngineIoCompletion, sync: &Arc>, ) { match tokio::fs::read(path).await { Ok(content) => { completion.push_buffer(Bytes::from(content)); completion.done(); } Err(err) if err.kind() == ErrorKind::NotFound => completion.done(), Err(err) => { completion.poison(format!("full read failed for {path}: {err}")); } } // Step callbacks after progress. sync.step_io_callbacks(); } async fn process_full_write( path: &str, content: &Vec, completion: turso_sync_sdk_kit::sync_engine_io::SyncEngineIoCompletion, sync: &Arc>, ) { // Write the whole content in one go (non-chunked) match tokio::fs::write(path, content).await { Ok(_) => { // For full write there is no data to stream back; just finish. completion.done(); } Err(err) => { completion.poison(format!("full write failed for {path}: {err}")); } } // Step callbacks after progress. sync.step_io_callbacks(); } } #[cfg(test)] mod tests { use anyhow::{anyhow, Context, Result}; use rand::{distr::Alphanumeric, Rng}; use reqwest::Client; use serde_json::json; use std::{ env, process::{Child, Command, Stdio}, thread::sleep, time::Duration, }; use tempfile::TempDir; use turso_sync_sdk_kit::rsapi::PartialBootstrapStrategy; use crate::sync::PartialSyncOpts; use crate::{Rows, Value}; const ADMIN_URL: &str = "http://localhost:8081"; const USER_URL: &str = "http://localhost:8080"; fn random_str() -> String { rand::rng() .sample_iter(&Alphanumeric) .take(8) .map(char::from) .collect() } async fn handle_response(resp: reqwest::Response) -> Result<()> { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); if status == 400 && text.contains("already exists") { return Ok(()); } if !status.is_success() { return Err(anyhow!("request failed: {status} {text}")); } Ok(()) } pub struct TursoServer { user_url: String, db_url: String, host: String, server: Option, client: Client, } impl TursoServer { pub async fn new() -> Result { let client = Client::new(); if env::var("LOCAL_SYNC_SERVER").is_err() { let name = random_str(); let tokens: Vec<&str> = USER_URL.split("://").collect(); handle_response( client .post(format!("{ADMIN_URL}/v1/tenants/{name}")) .send() .await?, ) .await?; handle_response( client .post(format!("{ADMIN_URL}/v1/tenants/{name}/groups/{name}")) .send() .await?, ) .await?; handle_response( client .post(format!( "{ADMIN_URL}/v1/tenants/{name}/groups/{name}/databases/{name}" )) .send() .await?, ) .await?; Ok(Self { user_url: USER_URL.to_string(), db_url: format!("{}://{}--{}--{}.{}", tokens[0], name, name, name, tokens[1]), host: format!("{name}--{name}--{name}.localhost"), server: None, client, }) } else { let port: u16 = rand::rng().random_range(10_000..=65_535); let server_bin = env::var("LOCAL_SYNC_SERVER").unwrap(); let child = Command::new(server_bin) .args(["--sync-server", &format!("0.0.0.0:{port}")]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .context("failed to spawn local sync server")?; let user_url = format!("http://localhost:{port}"); // wait for server readiness loop { if client.get(&user_url).send().await.is_ok() { break; } sleep(Duration::from_millis(100)); } Ok(Self { user_url: user_url.clone(), db_url: user_url, host: String::new(), server: Some(child), client, }) } } pub fn db_url(&self) -> &str { &self.db_url } pub async fn db_sql(&self, sql: &str) -> Result>> { let resp = self .client .post(format!("{}/v2/pipeline", self.user_url)) .header("Host", &self.host) .json(&json!({ "requests": [{ "type": "execute", "stmt": { "sql": sql } }] })) .send() .await? .error_for_status()?; let value: serde_json::Value = resp.json().await?; let result = &value["results"][0]; if result["type"] != "ok" { return Err(anyhow!("remote sql execution failed: {value}")); } let rows = result["response"]["result"]["rows"] .as_array() .ok_or_else(|| anyhow!("invalid response shape"))?; Ok(rows .iter() .map(|row| { row.as_array() .unwrap() .iter() .map(|cell| match cell["value"].clone() { serde_json::Value::Null => Value::Null, serde_json::Value::Number(number) => { if number.is_i64() { Value::Integer(number.as_i64().unwrap()) } else { Value::Real(number.as_f64().unwrap()) } } serde_json::Value::String(s) => Value::Text(s), _ => panic!("unexpected json output"), }) .collect() }) .collect()) } } impl Drop for TursoServer { fn drop(&mut self) { if let Some(child) = &mut self.server { let _ = child.kill(); } } } async fn all_rows(mut rows: Rows) -> Result>> { let mut result = Vec::new(); while let Some(row) = rows.next().await? { result.push(row.values.into_iter().map(|x| x.into()).collect()); } Ok(result) } #[tokio::test] pub async fn test_sync_bootstrap() { let _ = tracing_subscriber::fmt::try_init(); let server = TursoServer::new().await.unwrap(); server.db_sql("CREATE TABLE t(x)").await.unwrap(); server .db_sql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync')") .await .unwrap(); server.db_sql("SELECT * FROM t").await.unwrap(); let db = crate::sync::Builder::new_remote(":memory:") .with_remote_url(server.db_url()) .build() .await .unwrap(); let conn = db.connect().await.unwrap(); let rows = conn.query("SELECT * FROM t", ()).await.unwrap(); let all = all_rows(rows).await.unwrap(); assert_eq!( all, vec![ vec![Value::Text("hello".to_string())], vec![Value::Text("turso".to_string())], vec![Value::Text("sync".to_string())], ] ); } #[tokio::test] pub async fn test_sync_bootstrap_persistence() { let _ = tracing_subscriber::fmt::try_init(); let dir = TempDir::new().unwrap(); let server = TursoServer::new().await.unwrap(); server.db_sql("CREATE TABLE t(x)").await.unwrap(); server .db_sql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync')") .await .unwrap(); server.db_sql("SELECT * FROM t").await.unwrap(); let db = crate::sync::Builder::new_remote(dir.path().join("local.db").to_str().unwrap()) .with_remote_url(server.db_url()) .build() .await .unwrap(); let conn = db.connect().await.unwrap(); let rows = conn.query("SELECT * FROM t", ()).await.unwrap(); let all = all_rows(rows).await.unwrap(); assert_eq!( all, vec![ vec![Value::Text("hello".to_string())], vec![Value::Text("turso".to_string())], vec![Value::Text("sync".to_string())], ] ); } #[tokio::test] pub async fn test_sync_config_persistence() { let _ = tracing_subscriber::fmt::try_init(); let dir = TempDir::new().unwrap(); let server = TursoServer::new().await.unwrap(); server.db_sql("CREATE TABLE t(x)").await.unwrap(); server.db_sql("INSERT INTO t VALUES (42)").await.unwrap(); { let db1 = crate::sync::Builder::new_remote(dir.path().join("local.db").to_str().unwrap()) .with_remote_url(server.db_url()) .build() .await .unwrap(); let conn = db1.connect().await.unwrap(); let rows = conn.query("SELECT * FROM t", ()).await.unwrap(); let all = all_rows(rows).await.unwrap(); assert_eq!(all, vec![vec![Value::Integer(42)],]); } server.db_sql("INSERT INTO t VALUES (41)").await.unwrap(); { let db2 = crate::sync::Builder::new_remote(dir.path().join("local.db").to_str().unwrap()) .build() .await .unwrap(); db2.pull().await.unwrap(); let conn = db2.connect().await.unwrap(); let rows = conn.query("SELECT * FROM t", ()).await.unwrap(); let all = all_rows(rows).await.unwrap(); assert_eq!( all, vec![vec![Value::Integer(42)], vec![Value::Integer(41)],] ); } } #[tokio::test] pub async fn test_sync_pull() { let _ = tracing_subscriber::fmt::try_init(); let server = TursoServer::new().await.unwrap(); server.db_sql("CREATE TABLE t(x)").await.unwrap(); server .db_sql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync')") .await .unwrap(); server.db_sql("SELECT * FROM t").await.unwrap(); let db = crate::sync::Builder::new_remote(":memory:") .with_remote_url(server.db_url()) .build() .await .unwrap(); let conn = db.connect().await.unwrap(); let rows = conn.query("SELECT * FROM t", ()).await.unwrap(); let all = all_rows(rows).await.unwrap(); assert_eq!( all, vec![ vec![Value::Text("hello".to_string())], vec![Value::Text("turso".to_string())], vec![Value::Text("sync".to_string())], ] ); server .db_sql("INSERT INTO t VALUES ('pull works')") .await .unwrap(); let rows = conn.query("SELECT * FROM t", ()).await.unwrap(); let all = all_rows(rows).await.unwrap(); assert_eq!( all, vec![ vec![Value::Text("hello".to_string())], vec![Value::Text("turso".to_string())], vec![Value::Text("sync".to_string())], ] ); db.pull().await.unwrap(); let rows = conn.query("SELECT * FROM t", ()).await.unwrap(); let all = all_rows(rows).await.unwrap(); assert_eq!( all, vec![ vec![Value::Text("hello".to_string())], vec![Value::Text("turso".to_string())], vec![Value::Text("sync".to_string())], vec![Value::Text("pull works".to_string())], ] ); } #[tokio::test] pub async fn test_sync_push() { let _ = tracing_subscriber::fmt::try_init(); let server = TursoServer::new().await.unwrap(); server.db_sql("CREATE TABLE t(x)").await.unwrap(); server .db_sql("INSERT INTO t VALUES ('hello'), ('turso'), ('sync')") .await .unwrap(); server.db_sql("SELECT * FROM t").await.unwrap(); let db = crate::sync::Builder::new_remote(":memory:") .with_remote_url(server.db_url()) .build() .await .unwrap(); let conn = db.connect().await.unwrap(); let rows = conn.query("SELECT * FROM t", ()).await.unwrap(); let all = all_rows(rows).await.unwrap(); assert_eq!( all, vec![ vec![Value::Text("hello".to_string())], vec![Value::Text("turso".to_string())], vec![Value::Text("sync".to_string())], ] ); conn.execute("INSERT INTO t VALUES ('push works')", ()) .await .unwrap(); let all = server.db_sql("SELECT * FROM t").await.unwrap(); assert_eq!( all, vec![ vec![Value::Text("hello".to_string())], vec![Value::Text("turso".to_string())], vec![Value::Text("sync".to_string())], ] ); db.push().await.unwrap(); let rows = conn.query("SELECT * FROM t", ()).await.unwrap(); let all = all_rows(rows).await.unwrap(); assert_eq!( all, vec![ vec![Value::Text("hello".to_string())], vec![Value::Text("turso".to_string())], vec![Value::Text("sync".to_string())], vec![Value::Text("push works".to_string())], ] ); } #[tokio::test] pub async fn test_sync_checkpoint() { let _ = tracing_subscriber::fmt::try_init(); let server = TursoServer::new().await.unwrap(); let db = crate::sync::Builder::new_remote(":memory:") .with_remote_url(server.db_url()) .build() .await .unwrap(); let conn = db.connect().await.unwrap(); conn.execute("CREATE TABLE t(x)", ()).await.unwrap(); for i in 0..1024 { conn.execute("INSERT INTO t VALUES (?)", (i,)) .await .unwrap(); } let stats1 = db.stats().await.unwrap(); assert!(stats1.main_wal_size > 1024 * 1024); db.checkpoint().await.unwrap(); let stats2 = db.stats().await.unwrap(); assert!(stats2.main_wal_size < 8 * 1024); } #[tokio::test] pub async fn test_sync_partial() { let _ = tracing_subscriber::fmt::try_init(); let server = TursoServer::new().await.unwrap(); server.db_sql("CREATE TABLE t(x)").await.unwrap(); server .db_sql("INSERT INTO t SELECT randomblob(1024) FROM generate_series(1, 2000)") .await .unwrap(); { let full_db = crate::sync::Builder::new_remote(":memory:") .with_remote_url(server.db_url()) .build() .await .unwrap(); let conn = full_db.connect().await.unwrap(); let _ = all_rows( conn.query("SELECT LENGTH(x) FROM t LIMIT 1", ()) .await .unwrap(), ) .await .unwrap(); assert!(full_db.stats().await.unwrap().network_received_bytes > 2000 * 1024); } { let partial_db = crate::sync::Builder::new_remote(":memory:") .with_remote_url(server.db_url()) .with_partial_sync_opts_experimental(PartialSyncOpts { bootstrap_strategy: Some(PartialBootstrapStrategy::Prefix { length: 128 * 1024, }), segment_size: 128 * 1024, prefetch: false, }) .build() .await .unwrap(); let conn = partial_db.connect().await.unwrap(); let _ = all_rows( conn.query("SELECT LENGTH(x) FROM t LIMIT 1", ()) .await .unwrap(), ) .await .unwrap(); assert!(partial_db.stats().await.unwrap().network_received_bytes < 256 * (1024 + 10)); let before = tokio::time::Instant::now(); let all = all_rows( conn.query("SELECT SUM(LENGTH(x)) FROM t", ()) .await .unwrap(), ) .await .unwrap(); println!( "duration: {:?}", tokio::time::Instant::now().duration_since(before) ); assert_eq!(all, vec![vec![Value::Integer(2000 * 1024)]]); assert!(partial_db.stats().await.unwrap().network_received_bytes > 2000 * 1024); } } #[tokio::test] pub async fn test_sync_partial_segment_size() { let _ = tracing_subscriber::fmt::try_init(); let server = TursoServer::new().await.unwrap(); server.db_sql("CREATE TABLE t(x)").await.unwrap(); server .db_sql("INSERT INTO t SELECT randomblob(1024) FROM generate_series(1, 256)") .await .unwrap(); { let full_db = crate::sync::Builder::new_remote(":memory:") .with_remote_url(server.db_url()) .build() .await .unwrap(); let conn = full_db.connect().await.unwrap(); let _ = all_rows( conn.query("SELECT LENGTH(x) FROM t LIMIT 1", ()) .await .unwrap(), ) .await .unwrap(); assert!(full_db.stats().await.unwrap().network_received_bytes > 256 * 1024); } { let partial_db = crate::sync::Builder::new_remote(":memory:") .with_remote_url(server.db_url()) .with_partial_sync_opts_experimental(PartialSyncOpts { bootstrap_strategy: Some(PartialBootstrapStrategy::Prefix { length: 128 * 1024, }), segment_size: 4 * 1024, prefetch: false, }) .build() .await .unwrap(); let conn = partial_db.connect().await.unwrap(); let _ = all_rows( conn.query("SELECT LENGTH(x) FROM t LIMIT 1", ()) .await .unwrap(), ) .await .unwrap(); assert!(partial_db.stats().await.unwrap().network_received_bytes < 128 * 1024 * 3 / 2); let before = tokio::time::Instant::now(); let all = all_rows( conn.query("SELECT SUM(LENGTH(x)) FROM t", ()) .await .unwrap(), ) .await .unwrap(); println!( "duration segment size: {:?}", tokio::time::Instant::now().duration_since(before) ); assert_eq!(all, vec![vec![Value::Integer(256 * 1024)]]); assert!(partial_db.stats().await.unwrap().network_received_bytes > 256 * 1024); } } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub async fn test_sync_partial_prefetch() { let _ = tracing_subscriber::fmt::try_init(); let server = TursoServer::new().await.unwrap(); server.db_sql("CREATE TABLE t(x)").await.unwrap(); server .db_sql("INSERT INTO t SELECT randomblob(1024) FROM generate_series(1, 2000)") .await .unwrap(); { let full_db = crate::sync::Builder::new_remote(":memory:") .with_remote_url(server.db_url()) .build() .await .unwrap(); let conn = full_db.connect().await.unwrap(); let _ = all_rows( conn.query("SELECT LENGTH(x) FROM t LIMIT 1", ()) .await .unwrap(), ) .await .unwrap(); assert!(full_db.stats().await.unwrap().network_received_bytes > 2000 * 1024); } { let partial_db = crate::sync::Builder::new_remote(":memory:") .with_remote_url(server.db_url()) .with_partial_sync_opts_experimental(PartialSyncOpts { bootstrap_strategy: Some(PartialBootstrapStrategy::Prefix { length: 128 * 1024, }), segment_size: 128 * 1024, prefetch: true, }) .build() .await .unwrap(); let conn = partial_db.connect().await.unwrap(); let _ = all_rows( conn.query("SELECT LENGTH(x) FROM t LIMIT 1", ()) .await .unwrap(), ) .await .unwrap(); assert!(partial_db.stats().await.unwrap().network_received_bytes < 1300 * (1024 + 10)); let before = tokio::time::Instant::now(); let all = all_rows( conn.query("SELECT SUM(LENGTH(x)) FROM t", ()) .await .unwrap(), ) .await .unwrap(); println!( "duration prefetch: {:?}", tokio::time::Instant::now().duration_since(before) ); assert_eq!(all, vec![vec![Value::Integer(2000 * 1024)]]); assert!(partial_db.stats().await.unwrap().network_received_bytes > 2000 * 1024); } } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] pub async fn test_sync_parallel_writes_with_sync_ops() { use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tokio::sync::Mutex as TokioMutex; let _ = tracing_subscriber::fmt::try_init(); let server = TursoServer::new().await.unwrap(); let db = crate::sync::Builder::new_remote(":memory:") .with_remote_url(server.db_url()) .build() .await .unwrap(); let conn = db.connect().await.unwrap(); conn.execute( "CREATE TABLE test_data (id INTEGER PRIMARY KEY AUTOINCREMENT, payload TEXT NOT NULL)", (), ) .await .unwrap(); // ~200KB payload per row let payload = "X".repeat(200 * 1024); let done = Arc::new(AtomicBool::new(false)); let sync_lock = Arc::new(TokioMutex::new(())); // Spawn periodic push/pull/checkpoint task (sequential, guarded by sync_lock) let sync_db = db.clone(); let sync_done = done.clone(); let sync_lock_clone = sync_lock.clone(); let sync_task = tokio::spawn(async move { let mut cycle = 0u32; while !sync_done.load(Ordering::Relaxed) { tokio::time::sleep(Duration::from_millis(100)).await; let _guard = sync_lock_clone.lock().await; eprintln!("sync cycle {cycle}: push"); if let Err(e) = sync_db.push().await { eprintln!("push error (cycle {cycle}): {e}"); } eprintln!("sync cycle {cycle}: pull"); if let Err(e) = sync_db.pull().await { eprintln!("pull error (cycle {cycle}): {e}"); } eprintln!("sync cycle {cycle}: checkpoint"); if let Err(e) = sync_db.checkpoint().await { eprintln!("checkpoint error (cycle {cycle}): {e}"); } cycle += 1; } cycle }); // Parallel writes: 4 connections, each inserting 5 rows (~200KB each) let mut write_handles = Vec::new(); let mut connections = Vec::new(); let (conn_cnt, iterations_cnt, after_cnt) = (8u32, 100u32, 100u32); for _ in 0..conn_cnt { let db = db.clone(); let conn = db.connect().await.unwrap(); conn.execute("PRAGMA busy_timeout=5000", ()).await.unwrap(); connections.push(Some((db, conn))); } for conn_id in 0..conn_cnt { let (_, conn) = connections[conn_id as usize].take().unwrap(); let payload = payload.clone(); write_handles.push(tokio::spawn(async move { for row_id in 0..iterations_cnt { let tag = format!("conn{conn_id}_row{row_id}"); let data = format!("{tag}_{payload}"); loop { match conn .execute( "INSERT INTO test_data (payload) VALUES (?)", crate::params::Params::Positional(vec![Value::Text(data.clone())]), ) .await { Ok(_) => break, Err(crate::Error::Busy(_)) => { tokio::time::sleep(Duration::from_millis(10)).await; continue; } Err(e) => panic!("insert failed (conn{conn_id}, row{row_id}): {e:?}"), } } } })); } for h in write_handles { h.await.unwrap(); } // Sequential writes: 3 more large inserts for i in 0..after_cnt { let data = format!("sequential_{i}_{payload}"); conn.execute( "INSERT INTO test_data (payload) VALUES (?)", crate::params::Params::Positional(vec![Value::Text(data)]), ) .await .unwrap(); } // Signal sync task to stop and wait for it done.store(true, Ordering::Relaxed); let sync_cycles = sync_task.await.unwrap(); eprintln!("completed {sync_cycles} sync cycles during writes"); let rows = conn .query("SELECT count(*) FROM test_data", ()) .await .unwrap(); let all = all_rows(rows).await.unwrap(); assert_eq!( all, vec![vec![Value::Integer( (after_cnt + conn_cnt * iterations_cnt) as i64 )]] ); // Report WAL size via stats let stats = db.stats().await.unwrap(); eprintln!( "WAL size after all writes: {} bytes ({:.2} KB)", stats.main_wal_size, stats.main_wal_size as f64 / 1024.0 ); } } ================================================ FILE: bindings/rust/src/transaction.rs ================================================ use std::{ops::Deref, sync::atomic::Ordering}; use crate::{Connection, Result, Statement}; /// Options for transaction behavior. See [BEGIN /// TRANSACTION](http://www.sqlite.org/lang_transaction.html) for details. #[derive(Copy, Clone)] #[non_exhaustive] pub enum TransactionBehavior { /// DEFERRED means that the transaction does not actually start until the /// database is first accessed. Deferred, /// IMMEDIATE cause the database connection to start a new write /// immediately, without waiting for a writes statement. Immediate, /// EXCLUSIVE prevents other database connections from reading the database /// while the transaction is underway. Exclusive, } /// Options for how a Transaction should behave when it is dropped. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum DropBehavior { /// Roll back the changes. This is the default. Rollback, /// Commit the changes. Commit, /// Do not commit or roll back changes - this will leave the transaction or /// savepoint open, so should be used with care. Ignore, /// Panic. Used to enforce intentional behavior during development. Panic, } impl From for u8 { fn from(behavior: DropBehavior) -> Self { match behavior { DropBehavior::Rollback => 0, DropBehavior::Commit => 1, DropBehavior::Ignore => 2, DropBehavior::Panic => 3, } } } impl From for DropBehavior { fn from(value: u8) -> Self { match value { 0 => DropBehavior::Rollback, 1 => DropBehavior::Commit, 2 => DropBehavior::Ignore, 3 => DropBehavior::Panic, _ => panic!("Invalid drop behavior: {value}"), } } } /// Represents a transaction on a database connection. /// /// ## Note /// /// Transactions will roll back by default. Use `commit` method to explicitly /// commit the transaction, or use `set_drop_behavior` to change what happens /// on the next access to the connection after the transaction is dropped. /// /// ## Example /// /// ```rust,no_run /// # use turso::{Connection, Result}; /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) } /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) } /// async fn perform_queries(conn: &mut Connection) -> Result<()> { /// let tx = conn.transaction().await?; /// /// do_queries_part_1(&tx)?; // tx causes rollback if this fails /// do_queries_part_2(&tx)?; // tx causes rollback if this fails /// /// tx.commit().await /// } /// ``` #[derive(Debug)] pub struct Transaction<'conn> { conn: &'conn Connection, drop_behavior: DropBehavior, in_progress: bool, } impl Transaction<'_> { /// Begin a new transaction. Cannot be nested; /// /// Even though we don't mutate the connection, we take a `&mut Connection` /// to prevent nested transactions on the same connection. For cases /// where this is unacceptable, [`Transaction::new_unchecked`] is available. #[inline] pub async fn new( conn: &mut Connection, behavior: TransactionBehavior, ) -> Result> { Self::new_unchecked(conn, behavior).await } /// Begin a new transaction, failing if a transaction is open. /// /// If a transaction is already open, this will return an error. Where /// possible, [`Transaction::new`] should be preferred, as it provides a /// compile-time guarantee that transactions are not nested. #[inline] pub async fn new_unchecked( conn: &Connection, behavior: TransactionBehavior, ) -> Result> { let query = match behavior { TransactionBehavior::Deferred => "BEGIN DEFERRED", TransactionBehavior::Immediate => "BEGIN IMMEDIATE", TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE", }; // TODO: Use execute_batch instead conn.execute(query, ()).await.map(move |_| Transaction { conn, drop_behavior: DropBehavior::Rollback, in_progress: true, }) } // Use the Connection to Prepare a statement. // This allows a database update function to be passed a transaction, // prepare a statement, and use it without needing direct access to the // Connection pub async fn prepare(&self, sql: &str) -> Result { self.conn.prepare(sql).await } /// Get the current setting for what happens to the transaction when it is /// dropped. #[inline] #[must_use] pub fn drop_behavior(&self) -> DropBehavior { self.drop_behavior } /// Configure the transaction to perform the specified action when it is /// dropped. #[inline] pub fn set_drop_behavior(&mut self, drop_behavior: DropBehavior) { self.drop_behavior = drop_behavior; } /// A convenience method which consumes and commits a transaction. #[inline] pub async fn commit(mut self) -> Result<()> { self._commit().await } #[inline] async fn _commit(&mut self) -> Result<()> { self.conn.execute("COMMIT", ()).await?; self.in_progress = false; Ok(()) } /// A convenience method which consumes and rolls back a transaction. #[inline] pub async fn rollback(mut self) -> Result<()> { self._rollback().await } #[inline] async fn _rollback(&mut self) -> Result<()> { self.conn.execute("ROLLBACK", ()).await?; self.in_progress = false; Ok(()) } /// Consumes the transaction, committing or rolling back according to the /// current setting (see `drop_behavior`). /// /// Functionally equivalent to the `Drop` implementation, but allows /// callers to see any errors that occur. #[inline] pub async fn finish(mut self) -> Result<()> { self._finish().await } #[inline] async fn _finish(&mut self) -> Result<()> { if self.conn.is_autocommit()? { return Ok(()); } match self.drop_behavior() { DropBehavior::Commit => { if (self._commit().await).is_err() { self._rollback().await } else { Ok(()) } } DropBehavior::Rollback => self._rollback().await, DropBehavior::Ignore => Ok(()), DropBehavior::Panic => panic!("Transaction dropped unexpectedly."), } } } impl Deref for Transaction<'_> { type Target = Connection; #[inline] fn deref(&self) -> &Connection { self.conn } } impl Drop for Transaction<'_> { #[inline] fn drop(&mut self) { if self.in_progress { self.conn .dangling_tx .store(self.drop_behavior(), Ordering::SeqCst); } else { self.conn .dangling_tx .store(DropBehavior::Ignore, Ordering::SeqCst); } } } impl Connection { /// Begin a new transaction with the default behavior (DEFERRED). /// /// The transaction defaults to rolling back on the next access to the connection /// if it is not finished when the transaction is dropped. If you /// want the transaction to commit, you must call /// [`commit`](Transaction::commit) or /// [`set_drop_behavior(DropBehavior::Commit)`](Transaction::set_drop_behavior). /// /// ## Example /// /// ```rust,no_run /// # use turso::{Connection, Result}; /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) } /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) } /// async fn perform_queries(conn: &mut Connection) -> Result<()> { /// let tx = conn.transaction().await?; /// /// do_queries_part_1(&tx)?; // tx causes rollback if this fails /// do_queries_part_2(&tx)?; // tx causes rollback if this fails /// /// tx.commit().await /// } /// ``` /// /// # Failure /// /// Will return `Err` if the call fails. #[inline] pub async fn transaction(&mut self) -> Result> { self.transaction_with_behavior(self.transaction_behavior) .await } /// Begin a new transaction with a specified behavior. /// /// See [`transaction`](Connection::transaction). /// /// # Failure /// /// Will return `Err` if the call fails. #[inline] pub async fn transaction_with_behavior( &mut self, behavior: TransactionBehavior, ) -> Result> { self.maybe_handle_dangling_tx().await?; Transaction::new(self, behavior).await } /// Begin a new transaction with the default behavior (DEFERRED). /// /// Attempt to open a nested transaction will result in a SQLite error. /// `Connection::transaction` prevents this at compile time by taking `&mut /// self`, but `Connection::unchecked_transaction()` may be used to defer /// the checking until runtime. /// /// See [`Connection::transaction`] and [`Transaction::new_unchecked`] /// (which can be used if the default transaction behavior is undesirable). /// /// ## Example /// /// ```rust,no_run /// # use turso::{Connection, Result}; /// # use std::rc::Rc; /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) } /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) } /// async fn perform_queries(conn: Rc) -> Result<()> { /// let tx = conn.unchecked_transaction().await?; /// /// do_queries_part_1(&tx)?; // tx causes rollback if this fails /// do_queries_part_2(&tx)?; // tx causes rollback if this fails /// /// tx.commit().await /// } /// ``` /// /// # Failure /// /// Will return `Err` if the underlying SQLite call fails. The specific /// error returned if transactions are nested is currently unspecified. pub async fn unchecked_transaction(&self) -> Result> { Transaction::new_unchecked(self, self.transaction_behavior).await } /// Set the default transaction behavior for the connection. /// /// ## Note /// /// This will only apply to transactions initiated by [`transaction`](Connection::transaction) /// or [`unchecked_transaction`](Connection::unchecked_transaction). /// /// ## Example /// /// ```rust,no_run /// # use turso::{Connection, Result}; /// # use turso::transaction::TransactionBehavior; /// # fn do_queries_part_1(_conn: &Connection) -> Result<()> { Ok(()) } /// # fn do_queries_part_2(_conn: &Connection) -> Result<()> { Ok(()) } /// async fn perform_queries(conn: &mut Connection) -> Result<()> { /// conn.set_transaction_behavior(TransactionBehavior::Immediate); /// /// let tx = conn.transaction().await?; /// /// do_queries_part_1(&tx)?; // tx causes rollback if this fails /// do_queries_part_2(&tx)?; // tx causes rollback if this fails /// /// tx.commit().await /// } /// ``` pub fn set_transaction_behavior(&mut self, behavior: TransactionBehavior) { self.transaction_behavior = behavior; } } #[cfg(test)] mod test { use crate::{Builder, Connection, Error, Result}; use super::DropBehavior; async fn checked_memory_handle() -> Result { let db = Builder::new_local(":memory:").build().await?; let conn = db.connect()?; conn.execute("CREATE TABLE foo (x INTEGER)", ()).await?; Ok(conn) } #[tokio::test] async fn test_drop_rollback_on_new_transaction() { let mut conn = checked_memory_handle().await.unwrap(); { let tx = conn.transaction().await.unwrap(); tx.execute("INSERT INTO foo VALUES(?)", &[1]).await.unwrap(); // Drop without finish - should be rolled back when next transaction starts } // Start a new transaction - this should rollback the dangling one let tx = conn.transaction().await.unwrap(); tx.execute("INSERT INTO foo VALUES(?)", &[2]).await.unwrap(); let result = tx .prepare("SELECT SUM(x) FROM foo") .await .unwrap() .query_row(()) .await .unwrap(); // The insert from the dropped transaction should have been rolled back assert_eq!(2, result.get::(0).unwrap()); tx.finish().await.unwrap(); } #[tokio::test] async fn test_drop_rollback_on_query() { let mut conn = checked_memory_handle().await.unwrap(); { let tx = conn.transaction().await.unwrap(); tx.execute("INSERT INTO foo VALUES(?)", &[1]).await.unwrap(); // Drop without finish - should be rolled back when conn.query is called } // Using conn.query should rollback the dangling transaction let mut rows = conn.query("SELECT count(*) FROM foo", ()).await.unwrap(); let result = rows.next().await.unwrap().unwrap(); // The insert from the dropped transaction should have been rolled back assert_eq!(0, result.get::(0).unwrap()); } #[tokio::test] async fn test_drop_rollback_on_execute() { let mut conn = checked_memory_handle().await.unwrap(); { let tx = conn.transaction().await.unwrap(); tx.execute("INSERT INTO foo VALUES(?)", &[1]).await.unwrap(); // Drop without finish - should be rolled back when conn.execute is called } // Using conn.execute should rollback the dangling transaction conn.execute("INSERT INTO foo VALUES(?)", &[2]) .await .unwrap(); let mut rows = conn.query("SELECT count(*) FROM foo", ()).await.unwrap(); let result = rows.next().await.unwrap().unwrap(); // The insert from the dropped transaction should have been rolled back assert_eq!(1, result.get::(0).unwrap()); } #[tokio::test] async fn test_drop() -> Result<()> { let _ = tracing_subscriber::fmt::try_init(); let mut conn = checked_memory_handle().await?; { let tx = conn.transaction().await?; tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?; // default: rollback } { let mut tx = conn.transaction().await?; tx.execute("INSERT INTO foo VALUES(?)", &[2]).await?; tx.set_drop_behavior(DropBehavior::Commit); } { let tx = conn.transaction().await?; let result = tx .prepare("SELECT SUM(x) FROM foo") .await? .query_row(()) .await?; assert_eq!(2, result.get::(0)?); } Ok(()) } fn assert_nested_tx_error(e: Error) { if let Error::Error(e) = &e { assert!(e.contains("transaction")); } else { panic!("Unexpected error type: {e:?}"); } } #[tokio::test] async fn test_unchecked_nesting() -> Result<()> { let conn = checked_memory_handle().await?; { let tx = conn.unchecked_transaction().await?; let e = tx.unchecked_transaction().await.unwrap_err(); assert_nested_tx_error(e); tx.finish().await?; // default: rollback } { let tx = conn.unchecked_transaction().await?; tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?; // Ensure this doesn't interfere with ongoing transaction let e = tx.unchecked_transaction().await.unwrap_err(); assert_nested_tx_error(e); tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?; tx.commit().await?; } let result = conn .prepare("SELECT SUM(x) FROM foo") .await? .query_row(()) .await?; assert_eq!(2, result.get::(0)?); Ok(()) } #[tokio::test] async fn test_explicit_rollback_commit() -> Result<()> { let mut conn = checked_memory_handle().await?; { let tx = conn.transaction().await?; tx.execute("INSERT INTO foo VALUES(?)", &[1]).await?; tx.rollback().await?; // This is a current Turso's limitation. // Since we don't have support for savepoints yet, // a rollback ends with a transaction so we need to immediately open a new one. let tx = conn.transaction().await?; tx.execute("INSERT INTO foo VALUES(?)", &[2]).await?; tx.commit().await?; } { let tx = conn.transaction().await?; tx.execute("INSERT INTO foo VALUES(?)", &[4]).await?; tx.commit().await?; } { let result = conn .prepare("SELECT SUM(x) FROM foo") .await? .query_row(()) .await?; assert_eq!(6, result.get::(0)?); } Ok(()) } } ================================================ FILE: bindings/rust/src/value.rs ================================================ use std::str::FromStr; use crate::{Error, Result}; #[derive(Clone, Debug, PartialEq)] pub enum Value { Null, Integer(i64), Real(f64), Text(String), Blob(Vec), } /// The possible types a column can be in libsql. #[derive(Debug, Copy, Clone)] pub enum ValueType { Integer = 1, Real, Text, Blob, Null, } impl FromStr for ValueType { type Err = (); fn from_str(s: &str) -> std::result::Result { match s { "TEXT" => Ok(ValueType::Text), "INTEGER" => Ok(ValueType::Integer), "BLOB" => Ok(ValueType::Blob), "NULL" => Ok(ValueType::Null), "REAL" => Ok(ValueType::Real), _ => Err(()), } } } impl Value { /// Returns `true` if the value is [`Null`]. /// /// [`Null`]: Value::Null #[must_use] pub fn is_null(&self) -> bool { matches!(self, Self::Null) } /// Returns `true` if the value is [`Integer`]. /// /// [`Integer`]: Value::Integer #[must_use] pub fn is_integer(&self) -> bool { matches!(self, Self::Integer(..)) } /// Returns `true` if the value is [`Real`]. /// /// [`Real`]: Value::Real #[must_use] pub fn is_real(&self) -> bool { matches!(self, Self::Real(..)) } pub fn as_real(&self) -> Option<&f64> { if let Self::Real(v) = self { Some(v) } else { None } } /// Returns `true` if the value is [`Text`]. /// /// [`Text`]: Value::Text #[must_use] pub fn is_text(&self) -> bool { matches!(self, Self::Text(..)) } pub fn as_text(&self) -> Option<&String> { if let Self::Text(v) = self { Some(v) } else { None } } pub fn as_integer(&self) -> Option<&i64> { if let Self::Integer(v) = self { Some(v) } else { None } } /// Returns `true` if the value is [`Blob`]. /// /// [`Blob`]: Value::Blob #[must_use] pub fn is_blob(&self) -> bool { matches!(self, Self::Blob(..)) } pub fn as_blob(&self) -> Option<&Vec> { if let Self::Blob(v) = self { Some(v) } else { None } } } impl From for Value { fn from(val: turso_sdk_kit::rsapi::Value) -> Self { match val { turso_sdk_kit::rsapi::Value::Null => Value::Null, turso_sdk_kit::rsapi::Value::Numeric(turso_sdk_kit::rsapi::Numeric::Integer(n)) => { Value::Integer(n) } turso_sdk_kit::rsapi::Value::Numeric(turso_sdk_kit::rsapi::Numeric::Float(n)) => { Value::Real(f64::from(n)) } turso_sdk_kit::rsapi::Value::Text(t) => Value::Text(t.into()), turso_sdk_kit::rsapi::Value::Blob(items) => Value::Blob(items), } } } impl From for turso_sdk_kit::rsapi::Value { fn from(val: Value) -> Self { match val { Value::Null => turso_sdk_kit::rsapi::Value::Null, Value::Integer(n) => turso_sdk_kit::rsapi::Value::from_i64(n), Value::Real(n) => turso_sdk_kit::rsapi::Value::from_f64(n), Value::Text(t) => turso_sdk_kit::rsapi::Value::from_text(t), Value::Blob(items) => turso_sdk_kit::rsapi::Value::from_blob(items), } } } impl From for Value { fn from(value: i8) -> Value { Value::Integer(value as i64) } } impl From for Value { fn from(value: i16) -> Value { Value::Integer(value as i64) } } impl From for Value { fn from(value: i32) -> Value { Value::Integer(value as i64) } } impl From for Value { fn from(value: i64) -> Value { Value::Integer(value) } } impl From for Value { fn from(value: u8) -> Value { Value::Integer(value as i64) } } impl From for Value { fn from(value: u16) -> Value { Value::Integer(value as i64) } } impl From for Value { fn from(value: u32) -> Value { Value::Integer(value as i64) } } impl TryFrom for Value { type Error = Error; fn try_from(value: u64) -> Result { if value > i64::MAX as u64 { Err(Error::ToSqlConversionFailure( "u64 is too large to fit in an i64".into(), )) } else { Ok(Value::Integer(value as i64)) } } } impl From for Value { fn from(value: f32) -> Value { Value::Real(value as f64) } } impl From for Value { fn from(value: f64) -> Value { Value::Real(value) } } impl From<&str> for Value { fn from(value: &str) -> Value { Value::Text(value.to_owned()) } } impl From for Value { fn from(value: String) -> Value { Value::Text(value) } } impl From<&[u8]> for Value { fn from(value: &[u8]) -> Value { Value::Blob(value.to_owned()) } } impl From> for Value { fn from(value: Vec) -> Value { Value::Blob(value) } } impl From for Value { fn from(value: bool) -> Value { Value::Integer(value as i64) } } impl From> for Value where T: Into, { fn from(value: Option) -> Self { match value { Some(inner) => inner.into(), None => Value::Null, } } } /// A borrowed version of `Value`. #[derive(Debug)] pub enum ValueRef<'a> { Null, Integer(i64), Real(f64), Text(&'a [u8]), Blob(&'a [u8]), } impl ValueRef<'_> { pub fn data_type(&self) -> ValueType { match *self { ValueRef::Null => ValueType::Null, ValueRef::Integer(_) => ValueType::Integer, ValueRef::Real(_) => ValueType::Real, ValueRef::Text(_) => ValueType::Text, ValueRef::Blob(_) => ValueType::Blob, } } /// Returns `true` if the value ref is [`Null`]. /// /// [`Null`]: ValueRef::Null #[must_use] pub fn is_null(&self) -> bool { matches!(self, Self::Null) } /// Returns `true` if the value ref is [`Integer`]. /// /// [`Integer`]: ValueRef::Integer #[must_use] pub fn is_integer(&self) -> bool { matches!(self, Self::Integer(..)) } pub fn as_integer(&self) -> Option<&i64> { if let Self::Integer(v) = self { Some(v) } else { None } } /// Returns `true` if the value ref is [`Real`]. /// /// [`Real`]: ValueRef::Real #[must_use] pub fn is_real(&self) -> bool { matches!(self, Self::Real(..)) } pub fn as_real(&self) -> Option<&f64> { if let Self::Real(v) = self { Some(v) } else { None } } /// Returns `true` if the value ref is [`Text`]. /// /// [`Text`]: ValueRef::Text #[must_use] pub fn is_text(&self) -> bool { matches!(self, Self::Text(..)) } pub fn as_text(&self) -> Option<&[u8]> { if let Self::Text(v) = self { Some(v) } else { None } } /// Returns `true` if the value ref is [`Blob`]. /// /// [`Blob`]: ValueRef::Blob #[must_use] pub fn is_blob(&self) -> bool { matches!(self, Self::Blob(..)) } pub fn as_blob(&self) -> Option<&[u8]> { if let Self::Blob(v) = self { Some(v) } else { None } } } impl From> for Value { fn from(vr: ValueRef<'_>) -> Value { match vr { ValueRef::Null => Value::Null, ValueRef::Integer(i) => Value::Integer(i), ValueRef::Real(r) => Value::Real(r), ValueRef::Text(s) => Value::Text(String::from_utf8_lossy(s).to_string()), ValueRef::Blob(b) => Value::Blob(b.to_vec()), } } } impl<'a> From<&'a str> for ValueRef<'a> { fn from(s: &str) -> ValueRef<'_> { ValueRef::Text(s.as_bytes()) } } impl<'a> From<&'a [u8]> for ValueRef<'a> { fn from(s: &[u8]) -> ValueRef<'_> { ValueRef::Blob(s) } } impl<'a> From<&'a Value> for ValueRef<'a> { fn from(v: &'a Value) -> ValueRef<'a> { match *v { Value::Null => ValueRef::Null, Value::Integer(i) => ValueRef::Integer(i), Value::Real(r) => ValueRef::Real(r), Value::Text(ref s) => ValueRef::Text(s.as_bytes()), Value::Blob(ref b) => ValueRef::Blob(b), } } } impl<'a, T> From> for ValueRef<'a> where T: Into>, { #[inline] fn from(s: Option) -> ValueRef<'a> { match s { Some(x) => x.into(), None => ValueRef::Null, } } } ================================================ FILE: bindings/rust/tests/integration_tests.rs ================================================ use tokio::fs; use turso::{Builder, EncryptionOpts, Error, Value}; #[tokio::test] async fn test_rows_next() { let builder = Builder::new_local(":memory:"); let db = builder.build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE test (x INTEGER)", ()) .await .unwrap(); conn.execute("INSERT INTO test (x) VALUES (1)", ()) .await .unwrap(); assert_eq!(conn.last_insert_rowid(), 1); conn.execute("INSERT INTO test (x) VALUES (2)", ()) .await .unwrap(); assert_eq!(conn.last_insert_rowid(), 2); conn.execute( "INSERT INTO test (x) VALUES (:x)", vec![(":x".to_string(), Value::Integer(3))], ) .await .unwrap(); assert_eq!(conn.last_insert_rowid(), 3); conn.execute( "INSERT INTO test (x) VALUES (@x)", vec![("@x".to_string(), Value::Integer(4))], ) .await .unwrap(); assert_eq!(conn.last_insert_rowid(), 4); conn.execute( "INSERT INTO test (x) VALUES ($x)", vec![("$x".to_string(), Value::Integer(5))], ) .await .unwrap(); assert_eq!(conn.last_insert_rowid(), 5); let mut res = conn.query("SELECT * FROM test", ()).await.unwrap(); assert_eq!( res.next().await.unwrap().unwrap().get_value(0).unwrap(), 1.into() ); assert_eq!( res.next().await.unwrap().unwrap().get_value(0).unwrap(), 2.into() ); assert_eq!( res.next().await.unwrap().unwrap().get_value(0).unwrap(), 3.into() ); assert_eq!( res.next().await.unwrap().unwrap().get_value(0).unwrap(), 4.into() ); assert_eq!( res.next().await.unwrap().unwrap().get_value(0).unwrap(), 5.into() ); assert!(res.next().await.unwrap().is_none()); } #[tokio::test] async fn test_cacheflush() { let builder = Builder::new_local("test.db"); let db = builder.build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE IF NOT EXISTS asdf (x INTEGER)", ()) .await .unwrap(); // Tests if cache flush breaks transaction isolation conn.execute("BEGIN", ()).await.unwrap(); conn.execute("INSERT INTO asdf (x) VALUES (1)", ()) .await .unwrap(); conn.cacheflush().unwrap(); conn.execute("ROLLBACK", ()).await.unwrap(); conn.execute("INSERT INTO asdf (x) VALUES (2)", ()) .await .unwrap(); conn.execute("INSERT INTO asdf (x) VALUES (3)", ()) .await .unwrap(); let mut res = conn.query("SELECT * FROM asdf", ()).await.unwrap(); assert_eq!( res.next().await.unwrap().unwrap().get_value(0).unwrap(), 2.into() ); assert_eq!( res.next().await.unwrap().unwrap().get_value(0).unwrap(), 3.into() ); // Tests if cache flush doesn't break a committed transaction conn.execute("BEGIN", ()).await.unwrap(); conn.execute("INSERT INTO asdf (x) VALUES (1)", ()) .await .unwrap(); conn.cacheflush().unwrap(); conn.execute("COMMIT", ()).await.unwrap(); let mut res = conn .query("SELECT * FROM asdf WHERE x = 1", ()) .await .unwrap(); assert_eq!( res.next().await.unwrap().unwrap().get_value(0).unwrap(), 1.into() ); fs::remove_file("test.db").await.unwrap(); fs::remove_file("test.db-wal").await.unwrap(); } #[tokio::test] async fn test_rows_returned() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); //--- CRUD Operations ---// conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)", ()) .await .unwrap(); let changed = conn .execute("INSERT INTO t VALUES (1,'hello')", ()) .await .unwrap(); let changed1 = conn .execute("INSERT INTO t VALUES (2,'hi')", ()) .await .unwrap(); let changed2 = conn .execute("UPDATE t SET val='hi' WHERE id=1", ()) .await .unwrap(); let changed3 = conn .execute("DELETE FROM t WHERE val='hi'", ()) .await .unwrap(); assert_eq!(changed, 1); assert_eq!(changed1, 1); assert_eq!(changed2, 1); assert_eq!(changed3, 2); //--- A more complicated example of insert with a select join subquery ---// conn.execute( "CREATE TABLE authors ( id INTEGER PRIMARY KEY, name TEXT NOT NULL); ", (), ) .await .unwrap(); conn.execute( "CREATE TABLE books ( id INTEGER PRIMARY KEY, author_id INTEGER NOT NULL REFERENCES authors(id), title TEXT NOT NULL); " ,() ).await.unwrap(); conn.execute( "CREATE TABLE prize_winners ( book_id INTEGER PRIMARY KEY, author_name TEXT NOT NULL);", (), ) .await .unwrap(); conn.execute( "INSERT INTO authors (id, name) VALUES (1, 'Alice'), (2, 'Bob');", (), ) .await .unwrap(); conn.execute( "INSERT INTO books (id, author_id, title) VALUES (1, 1, 'Rust in Action'), (2, 1, 'Async Adventures'), (3, 1, 'Fearless Concurrency'), (4, 1, 'Unsafe Tales'), (5, 1, 'Zero-Cost Futures'), (6, 2, 'Learning SQL');", () ).await.unwrap(); let rows_changed = conn .execute( " INSERT INTO prize_winners (book_id, author_name) SELECT b.id, a.name FROM books b JOIN authors a ON a.id = b.author_id WHERE a.id = 1; -- Alice's five books ", (), ) .await .unwrap(); assert_eq!(rows_changed, 5); } #[tokio::test] pub async fn test_execute_batch() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute_batch("CREATE TABLE authors ( id INTEGER PRIMARY KEY, name TEXT NOT NULL);CREATE TABLE books ( id INTEGER PRIMARY KEY, author_id INTEGER NOT NULL REFERENCES authors(id), title TEXT NOT NULL); INSERT INTO authors (id, name) VALUES (1, 'Alice'), (2, 'Bob');") .await .unwrap(); let mut rows = conn .query("SELECT COUNT(*) FROM authors;", ()) .await .unwrap(); if let Some(row) = rows.next().await.unwrap() { assert_eq!(row.get_value(0).unwrap(), Value::Integer(2)); } } #[tokio::test] async fn test_query_row_returns_first_row() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE users (id INTEGER, name TEXT)", ()) .await .unwrap(); conn.execute("INSERT INTO users VALUES (1, 'Frodo')", ()) .await .unwrap(); let row = conn .prepare("SELECT id FROM users WHERE name = ?") .await .unwrap() .query_row(&["Frodo"]) .await .unwrap(); let id: i64 = row.get(0).unwrap(); assert_eq!(id, 1); } #[tokio::test] async fn test_query_row_returns_no_rows_error() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE users (id INTEGER, name TEXT)", ()) .await .unwrap(); let result = conn .prepare("SELECT id FROM users WHERE name = ?") .await .unwrap() .query_row(&["Ghost"]) .await; assert!(matches!(result, Err(Error::QueryReturnedNoRows))); } #[tokio::test] async fn test_row_get_column_typed() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE v (n INTEGER, label TEXT)", ()) .await .unwrap(); conn.execute("INSERT INTO v VALUES (42, 'answer')", ()) .await .unwrap(); let mut rows = conn.query("SELECT * FROM v", ()).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); let n: i64 = row.get(0).unwrap(); let label: String = row.get(1).unwrap(); assert_eq!(n, 42); assert_eq!(label, "answer"); } #[tokio::test] async fn test_row_get_conversion_error() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t (x TEXT)", ()).await.unwrap(); conn.execute("INSERT INTO t VALUES (NULL)", ()) .await .unwrap(); let mut rows = conn.query("SELECT x FROM t", ()).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); // Attempt to convert TEXT into integer (should fail) let result: Result = row.get(0); assert!(matches!(result, Err(Error::ConversionFailure(_)))); } #[tokio::test] async fn test_index() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE users (name TEXT PRIMARY KEY, email TEXT)", ()) .await .unwrap(); conn.execute("CREATE INDEX email_idx ON users(email)", ()) .await .unwrap(); conn.execute( "INSERT INTO users VALUES ('alice', 'a@b.c'), ('bob', 'b@d.e')", (), ) .await .unwrap(); let mut rows = conn .query("SELECT * FROM users WHERE email = 'a@b.c'", ()) .await .unwrap(); let row = rows.next().await.unwrap().unwrap(); assert!(row.get::(0).unwrap() == "alice"); assert!(row.get::(1).unwrap() == "a@b.c"); assert!(rows.next().await.unwrap().is_none()); let mut rows = conn .query("SELECT * FROM users WHERE email = 'b@d.e'", ()) .await .unwrap(); let row = rows.next().await.unwrap().unwrap(); assert!(row.get::(0).unwrap() == "bob"); assert!(row.get::(1).unwrap() == "b@d.e"); assert!(rows.next().await.unwrap().is_none()); } #[tokio::test] /// Tests that concurrent statements that error out and rollback can do so without panicking async fn test_concurrent_unique_constraint_regression() { use std::sync::Arc; use tokio::sync::Barrier; let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute( "CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE, created_at DATETIME )", (), ) .await .unwrap(); // Insert initial seed data conn.execute( "INSERT INTO users (email, created_at) VALUES (:email, :created_at)", vec![ (":email".to_string(), Value::Text("seed@example.com".into())), (":created_at".to_string(), Value::Text("whatever".into())), ], ) .await .unwrap(); let barrier = Arc::new(Barrier::new(8)); let mut handles = Vec::new(); // Spawn 8 concurrent workers for _ in 0..8 { let conn = db.connect().unwrap(); let barrier = barrier.clone(); handles.push(tokio::spawn(async move { barrier.wait().await; let mut prepared_stmt = conn .prepare("INSERT INTO users (email, created_at) VALUES (:email, :created_at)") .await .unwrap(); for i in 0..1000 { let email = match i % 3 { 0 => "seed@example.com", 1 => "dup@example.com", 2 => "dapper@example.com", _ => panic!("Invalid email index: {i}"), }; let result = prepared_stmt .execute(vec![ (":email".to_string(), Value::Text(email.into())), (":created_at".to_string(), Value::Text("whatever".into())), ]) .await; match result { Ok(_) => (), Err(Error::Constraint(e)) if e.contains("UNIQUE constraint failed") => {} Err(Error::Busy(e)) if e.contains("database is locked") => {} Err(e) => { panic!("Error executing statement: {e:?}"); } } } })); } // Wait for all workers to complete for handle in handles { handle.await.unwrap(); } } #[tokio::test] async fn test_statement_query_resets_before_execution() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)", ()) .await .unwrap(); for i in 0..5 { conn.execute(&format!("INSERT INTO t VALUES ({i}, 'value_{i}')"), ()) .await .unwrap(); } let mut stmt = conn .prepare("SELECT id, value FROM t ORDER BY id") .await .unwrap(); let mut rows = stmt.query(()).await.unwrap(); let mut count = 0; while let Some(row) = rows.next().await.unwrap() { let id: i64 = row.get(0).unwrap(); assert_eq!(id, count); count += 1; } assert_eq!(count, 5); let mut rows = stmt.query(()).await.unwrap(); let mut count = 0; while let Some(row) = rows.next().await.unwrap() { let id: i64 = row.get(0).unwrap(); assert_eq!(id, count); count += 1; } // this will return 0 rows if query() does not reset the statement assert_eq!(count, 5, "Second query() should return all rows again"); } #[tokio::test] async fn test_encryption() { let temp_dir = tempfile::tempdir().unwrap(); let db_file = temp_dir.path().join("test-encrypted.db"); let db_file = db_file.to_str().unwrap(); let hexkey = "b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327"; let wrong_key = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; let encryption_opts = EncryptionOpts { hexkey: hexkey.to_string(), cipher: "aegis256".to_string(), }; // 1. Create encrypted database and insert data { let builder = Builder::new_local(db_file) .experimental_encryption(true) .with_encryption(encryption_opts.clone()); let db = builder.build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute( "CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT);", (), ) .await .unwrap(); conn.execute("INSERT INTO test (value) VALUES ('secret_data')", ()) .await .unwrap(); let mut row_count = 0; let mut rows = conn.query("SELECT * FROM test", ()).await.unwrap(); while let Some(row) = rows.next().await.unwrap() { assert_eq!(row.get::(0).unwrap(), 1); assert_eq!(row.get::(1).unwrap(), "secret_data"); row_count += 1; } assert_eq!(row_count, 1); // Checkpoint to ensure data is written to main db file let mut rows = conn .query("PRAGMA wal_checkpoint(TRUNCATE)", ()) .await .unwrap(); while rows.next().await.unwrap().is_some() {} } // 2. Verify data is encrypted on disk let content = std::fs::read(db_file).unwrap(); assert!(content.len() > 1024); assert!( !content.windows(11).any(|w| w == b"secret_data"), "Plaintext should not appear in encrypted database file" ); // 3. Reopen with correct key and verify data { let builder = Builder::new_local(db_file) .experimental_encryption(true) .with_encryption(encryption_opts.clone()); let db = builder.build().await.unwrap(); let conn = db.connect().unwrap(); let mut row_count = 0; let mut rows = conn.query("SELECT * FROM test", ()).await.unwrap(); while let Some(row) = rows.next().await.unwrap() { assert_eq!(row.get::(0).unwrap(), 1); assert_eq!(row.get::(1).unwrap(), "secret_data"); row_count += 1; } assert_eq!(row_count, 1); } // 4. Verify opening with wrong key fails { let wrong_opts = EncryptionOpts { hexkey: wrong_key.to_string(), cipher: "aegis256".to_string(), }; let builder = Builder::new_local(db_file) .experimental_encryption(true) .with_encryption(wrong_opts); let result = builder.build().await; assert!(result.is_err(), "Opening with wrong key should fail"); } // 5. Verify opening without encryption fails { let builder = Builder::new_local(db_file).experimental_encryption(true); let result = builder.build().await; assert!( result.is_err(), "Opening encrypted database without key should fail" ); } } #[tokio::test] /// This results in a panic if the query isn't correctly reset async fn test_query_without_reset_does_not_panic() { let tempfile = tempfile::NamedTempFile::new().unwrap(); let db = Builder::new_local(tempfile.path().to_str().unwrap()) .build() .await .unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)", ()) .await .unwrap(); for i in 0..10 { conn.execute(&format!("INSERT INTO t VALUES ({i}, 'val')"), ()) .await .unwrap(); } let mut stmts: Vec> = Vec::new(); for round in 0..4 { for i in 0..30 { let id = 100 + round * 100 + i; let sql = match i % 4 { 0 => format!("INSERT INTO t VALUES ({id}, 'new')"), 1 => "SELECT * FROM t".to_string(), 2 => format!("UPDATE t SET value = 'upd' WHERE id = {}", i % 10), _ => format!("DELETE FROM t WHERE id = {}", 1000 + i), }; if let Ok(s) = conn.prepare(&sql).await { stmts.push(Some(s)); } } for i in (0..stmts.len()).step_by(7) { if let Some(Some(stmt)) = stmts.get_mut(i) { if let Ok(mut rows) = stmt.query(()).await { let _ = rows.next().await; } } } for i in 0..3 { let _ = conn .execute( &format!("INSERT INTO t VALUES ({}, 'x')", 2000 + round * 10 + i), (), ) .await; } for i in (0..stmts.len()).step_by(13) { stmts[i] = None; } for i in (0..stmts.len()).step_by(5) { if let Some(Some(stmt)) = stmts.get_mut(i) { if let Ok(mut rows) = stmt.query(()).await { let _ = rows.next().await; } } } } } // Test Transaction.prepare #[tokio::test] async fn test_transaction_prepared_statement() { let db = Builder::new_local(":memory:").build().await.unwrap(); let mut conn = db.connect().unwrap(); conn.execute("CREATE TABLE users (id INTEGER, name TEXT)", ()) .await .unwrap(); let tx = conn.transaction().await.unwrap(); let mut stmt = tx .prepare("INSERT INTO users VALUES (?1, ?2)") .await .unwrap(); stmt.execute(["1", "Frodo"]).await.unwrap(); tx.commit().await.unwrap(); let row = conn .prepare("SELECT id FROM users WHERE name = ?") .await .unwrap() .query_row(&["Frodo"]) .await .unwrap(); let id: i64 = row.get(0).unwrap(); assert_eq!(id, 1); } #[tokio::test] async fn test_row_get_value_out_of_bounds() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t (x INTEGER)", ()) .await .unwrap(); conn.execute("INSERT INTO t VALUES (1)", ()).await.unwrap(); let mut rows = conn.query("SELECT x FROM t", ()).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); // Valid index works assert!(row.get_value(0).is_ok()); // Out of bounds returns error instead of panicking let result = row.get_value(999); assert!(matches!(result, Err(Error::Misuse(_)))); // Also test get() for OOB let result: Result = row.get(999); assert!(matches!(result, Err(Error::Misuse(_)))); } // Test Connection clone #[tokio::test] async fn test_connection_clone() { let db = Builder::new_local(":memory:").build().await.unwrap(); let mut conn = db.connect().unwrap(); conn.execute("CREATE TABLE users (id INTEGER, name TEXT)", ()) .await .unwrap(); let tx = conn.transaction().await.unwrap(); let mut stmt = tx .prepare("INSERT INTO users VALUES (?1, ?2)") .await .unwrap(); stmt.execute(["1", "Frodo"]).await.unwrap(); tx.commit().await.unwrap(); let conn2 = conn.clone(); let row = conn2 .prepare("SELECT id FROM users WHERE name = ?") .await .unwrap() .query_row(&["Frodo"]) .await .unwrap(); let id: i64 = row.get(0).unwrap(); assert_eq!(id, 1); } #[tokio::test] async fn test_insert_returning_partial_consume() { // Regression test for: INSERT...RETURNING should insert all rows even if // only some RETURNING values are consumed before the statement is dropped/reset. // This matches the sqlite3 bindings fix in commit e39e60ef1. let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t (x INTEGER)", ()) .await .unwrap(); // Use query() to get RETURNING values, but only consume first row let mut stmt = conn .prepare("INSERT INTO t (x) VALUES (1), (2), (3) RETURNING x") .await .unwrap(); let mut rows = stmt.query(()).await.unwrap(); // Only consume first row let first_row = rows.next().await.unwrap().unwrap(); assert_eq!(first_row.get::(0).unwrap(), 1); // Drop the rows iterator without consuming remaining rows drop(rows); drop(stmt); // All 3 rows should have been inserted despite only consuming 1 RETURNING value let mut count_rows = conn.query("SELECT COUNT(*) FROM t", ()).await.unwrap(); let count: i64 = count_rows.next().await.unwrap().unwrap().get(0).unwrap(); assert_eq!( count, 3, "All 3 rows should be inserted even if RETURNING was partially consumed" ); } #[tokio::test] async fn test_transaction_commit_without_mvcc() { // Regression test: COMMIT should work for non-MVCC transactions. // The op_auto_commit function must check TransactionState, not just MVCC tx. let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)", ()) .await .unwrap(); // Begin explicit transaction conn.execute("BEGIN IMMEDIATE TRANSACTION", ()) .await .unwrap(); // Insert data within transaction conn.execute("INSERT INTO test (id, value) VALUES (1, 'hello')", ()) .await .unwrap(); // Commit should succeed conn.execute("COMMIT", ()) .await .expect("COMMIT should succeed for non-MVCC transactions"); // Verify data was committed let mut rows = conn .query("SELECT value FROM test WHERE id = 1", ()) .await .unwrap(); let row = rows.next().await.unwrap().unwrap(); let value: String = row.get(0).unwrap(); assert_eq!(value, "hello", "Data should be committed"); } #[tokio::test] async fn test_transaction_with_insert_returning_then_commit() { // Regression test: Combining INSERT...RETURNING (partial consume) with explicit transaction. // This tests the interaction between the reset-to-completion fix and transaction commit. let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t (x INTEGER)", ()) .await .unwrap(); // Begin transaction conn.execute("BEGIN IMMEDIATE TRANSACTION", ()) .await .unwrap(); // INSERT...RETURNING, only consume first row let mut stmt = conn .prepare("INSERT INTO t (x) VALUES (1), (2), (3) RETURNING x") .await .unwrap(); let mut rows = stmt.query(()).await.unwrap(); let first = rows.next().await.unwrap().unwrap(); assert_eq!(first.get::(0).unwrap(), 1); drop(rows); drop(stmt); // Commit should succeed even after partial RETURNING consumption conn.execute("COMMIT", ()) .await .expect("COMMIT should succeed after INSERT...RETURNING"); // Verify all 3 rows were inserted let mut count_rows = conn.query("SELECT COUNT(*) FROM t", ()).await.unwrap(); let count: i64 = count_rows.next().await.unwrap().unwrap().get(0).unwrap(); assert_eq!(count, 3, "All rows should be committed"); } #[tokio::test] async fn test_prepare_cached_basic() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", ()) .await .unwrap(); // First call should cache the statement let mut stmt1 = conn .prepare_cached("SELECT * FROM users WHERE id = ?") .await .unwrap(); // Insert some data and query conn.execute("INSERT INTO users VALUES (1, 'Alice')", ()) .await .unwrap(); let mut rows = stmt1.query(vec![Value::Integer(1)]).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); assert_eq!(row.get::(0).unwrap(), 1); assert_eq!(row.get::(1).unwrap(), "Alice"); drop(rows); drop(stmt1); // Second call should use cached statement let mut stmt2 = conn .prepare_cached("SELECT * FROM users WHERE id = ?") .await .unwrap(); let mut rows = stmt2.query(vec![Value::Integer(1)]).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); assert_eq!(row.get::(0).unwrap(), 1); assert_eq!(row.get::(1).unwrap(), "Alice"); } #[tokio::test] async fn test_prepare_cached_reprepare_on_query_only_change() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t (id INTEGER)", ()) .await .unwrap(); let mut stmt = conn .prepare_cached("INSERT INTO t VALUES (?)") .await .unwrap(); conn.execute("PRAGMA query_only=1", ()).await.unwrap(); let err = stmt.execute(vec![Value::Integer(1)]).await.unwrap_err(); assert!(err.to_string().to_ascii_lowercase().contains("query_only")); let mut rows = conn.query("SELECT COUNT(*) FROM t", ()).await.unwrap(); let count: i64 = rows.next().await.unwrap().unwrap().get(0).unwrap(); assert_eq!(count, 0); } #[tokio::test] async fn test_prepare_cached_batch_insert_delete_pattern() { #[derive(Clone)] struct Host { name: String, app: String, address: String, namespace: String, cloud_cluster_name: String, allowed_ips: Vec, updated_at: std::time::SystemTime, deleted: bool, } fn serialize_allowed_ips(allowed_ips: &[String]) -> String { allowed_ips.join(",") } fn system_time_to_unix_seconds(ts: std::time::SystemTime) -> i64 { let duration = ts .duration_since(std::time::UNIX_EPOCH) .expect("system time should be after unix epoch"); duration.as_secs() as i64 } async fn insert_hosts(conn: &turso::Connection, hosts: &[Host]) -> Result<(), Error> { if hosts.is_empty() { return Ok(()); } conn.execute("BEGIN", ()).await?; let mut insert_stmt = conn .prepare_cached( "INSERT INTO hosts (name, app, address, namespace, cloud_cluster_name, allowed_ips, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(name) DO UPDATE SET app = excluded.app, address = excluded.address, namespace = excluded.namespace, cloud_cluster_name = excluded.cloud_cluster_name, allowed_ips = excluded.allowed_ips, updated_at = excluded.updated_at", ) .await?; let mut delete_stmt = conn .prepare_cached("DELETE FROM hosts WHERE name = ?1") .await?; let result = async { for host in hosts { if host.deleted { delete_stmt.execute([host.name.as_str()]).await?; continue; } let allowed_ips = serialize_allowed_ips(&host.allowed_ips); let updated_at = system_time_to_unix_seconds(host.updated_at); insert_stmt .execute(( host.name.as_str(), host.app.as_str(), host.address.as_str(), host.namespace.as_str(), host.cloud_cluster_name.as_str(), allowed_ips, updated_at, )) .await?; } Ok(()) } .await; match result { Ok(()) => { conn.execute("COMMIT", ()).await?; Ok(()) } Err(err) => { let _ = conn.execute("ROLLBACK", ()).await; Err(err) } } } let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute( "CREATE TABLE hosts ( name TEXT PRIMARY KEY, app TEXT, address TEXT, namespace TEXT, cloud_cluster_name TEXT, allowed_ips TEXT, updated_at INTEGER )", (), ) .await .unwrap(); let base_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_700_000_000); let hosts = vec![ Host { name: "a".to_string(), app: "app_a".to_string(), address: "10.0.0.1".to_string(), namespace: "ns".to_string(), cloud_cluster_name: "cluster".to_string(), allowed_ips: vec!["10.0.0.0/24".to_string()], updated_at: base_time, deleted: false, }, Host { name: "b".to_string(), app: "app_b".to_string(), address: "10.0.0.2".to_string(), namespace: "ns".to_string(), cloud_cluster_name: "cluster".to_string(), allowed_ips: vec!["10.0.1.0/24".to_string()], updated_at: base_time, deleted: false, }, Host { name: "a".to_string(), app: "app_a".to_string(), address: "10.0.0.1".to_string(), namespace: "ns".to_string(), cloud_cluster_name: "cluster".to_string(), allowed_ips: vec!["10.0.0.0/24".to_string()], updated_at: base_time, deleted: true, }, ]; insert_hosts(&conn, &hosts).await.unwrap(); let mut rows = conn .query("SELECT name FROM hosts ORDER BY name", ()) .await .unwrap(); let first = rows .next() .await .unwrap() .unwrap() .get::(0) .unwrap(); assert_eq!(first, "b"); assert!(rows.next().await.unwrap().is_none()); } #[tokio::test] async fn test_prepare_cached_multiple_statements() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t (id INTEGER, value TEXT)", ()) .await .unwrap(); // Cache multiple different statements let queries = vec![ "SELECT * FROM t WHERE id = ?", "SELECT * FROM t WHERE value = ?", "INSERT INTO t VALUES (?, ?)", ]; for query in &queries { let _ = conn.prepare_cached(*query).await.unwrap(); } // All should be cached and work correctly let mut stmt1 = conn.prepare_cached(queries[0]).await.unwrap(); let mut stmt2 = conn.prepare_cached(queries[1]).await.unwrap(); let mut stmt3 = conn.prepare_cached(queries[2]).await.unwrap(); // Insert data stmt3 .execute(vec![Value::Integer(1), Value::Text("test".into())]) .await .unwrap(); // Query using both cached SELECT statements let mut rows = stmt1.query(vec![Value::Integer(1)]).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); assert_eq!(row.get::(0).unwrap(), 1); drop(rows); let mut rows = stmt2.query(vec![Value::Text("test".into())]).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); assert_eq!(row.get::(1).unwrap(), "test"); } #[tokio::test] async fn test_prepare_cached_independent_state() { // Verify that each cached statement has independent execution state let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t (id INTEGER)", ()) .await .unwrap(); for i in 1..=5 { conn.execute(&format!("INSERT INTO t VALUES ({i})"), ()) .await .unwrap(); } let query = "SELECT * FROM t ORDER BY id"; // Get two statements from cache let mut stmt1 = conn.prepare_cached(query).await.unwrap(); let mut stmt2 = conn.prepare_cached(query).await.unwrap(); // Start iterating with stmt1 let mut rows1 = stmt1.query(()).await.unwrap(); let row1 = rows1.next().await.unwrap().unwrap(); assert_eq!(row1.get::(0).unwrap(), 1); // Start iterating with stmt2 - should have its own state let mut rows2 = stmt2.query(()).await.unwrap(); let row2 = rows2.next().await.unwrap().unwrap(); assert_eq!(row2.get::(0).unwrap(), 1); // Continue with stmt1 - should be at next row let row1 = rows1.next().await.unwrap().unwrap(); assert_eq!(row1.get::(0).unwrap(), 2); // Continue with stmt2 - should also be at next row (independent state) let row2 = rows2.next().await.unwrap().unwrap(); assert_eq!(row2.get::(0).unwrap(), 2); } #[tokio::test] async fn test_prepare_cached_with_parameters() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute( "CREATE TABLE users (id INTEGER, name TEXT, age INTEGER)", (), ) .await .unwrap(); conn.execute("INSERT INTO users VALUES (1, 'Alice', 30)", ()) .await .unwrap(); conn.execute("INSERT INTO users VALUES (2, 'Bob', 25)", ()) .await .unwrap(); conn.execute("INSERT INTO users VALUES (3, 'Charlie', 35)", ()) .await .unwrap(); let query = "SELECT name FROM users WHERE age > ?"; // Use cached statement with different parameters let mut stmt = conn.prepare_cached(query).await.unwrap(); let mut rows = stmt.query(vec![Value::Integer(25)]).await.unwrap(); let mut names = Vec::new(); while let Some(row) = rows.next().await.unwrap() { names.push(row.get::(0).unwrap()); } assert_eq!(names.len(), 2); assert!(names.contains(&"Alice".to_string())); assert!(names.contains(&"Charlie".to_string())); drop(rows); // Reuse cached statement with different parameter let mut rows = stmt.query(vec![Value::Integer(30)]).await.unwrap(); let mut names = Vec::new(); while let Some(row) = rows.next().await.unwrap() { names.push(row.get::(0).unwrap()); } assert_eq!(names.len(), 1); assert_eq!(names[0], "Charlie"); } #[tokio::test] async fn test_prepare_cached_stress() { // Stress test to ensure cache works correctly under repeated use let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)", ()) .await .unwrap(); let insert_query = "INSERT INTO t (id, value) VALUES (?, ?)"; let select_query = "SELECT value FROM t WHERE id = ?"; // Insert many rows using cached statement for i in 0..100 { let mut stmt = conn.prepare_cached(insert_query).await.unwrap(); stmt.execute(vec![Value::Integer(i), Value::Text(format!("value_{i}"))]) .await .unwrap(); } // Query many times using cached statement for i in 0..100 { let mut stmt = conn.prepare_cached(select_query).await.unwrap(); let mut rows = stmt.query(vec![Value::Integer(i)]).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); assert_eq!(row.get::(0).unwrap(), format!("value_{i}")); } } #[tokio::test] async fn test_prepare_vs_prepare_cached_equivalence() { // Verify that prepare_cached produces same results as prepare let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t (x INTEGER, y TEXT)", ()) .await .unwrap(); conn.execute("INSERT INTO t VALUES (1, 'a'), (2, 'b'), (3, 'c')", ()) .await .unwrap(); let query = "SELECT * FROM t ORDER BY x"; // Results from prepare let mut stmt1 = conn.prepare(query).await.unwrap(); let mut rows1 = stmt1.query(()).await.unwrap(); let mut results1 = Vec::new(); while let Some(row) = rows1.next().await.unwrap() { results1.push((row.get::(0).unwrap(), row.get::(1).unwrap())); } // Results from prepare_cached let mut stmt2 = conn.prepare_cached(query).await.unwrap(); let mut rows2 = stmt2.query(()).await.unwrap(); let mut results2 = Vec::new(); while let Some(row) = rows2.next().await.unwrap() { results2.push((row.get::(0).unwrap(), row.get::(1).unwrap())); } // Should produce identical results assert_eq!(results1, results2); assert_eq!( results1, vec![ (1, "a".to_string()), (2, "b".to_string()), (3, "c".to_string()), ] ); } /// This will fail if self.once is not reset in ProgramState::reset. #[tokio::test] async fn test_once_not_cleared_on_reset_with_coroutine() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); // This query generates bytecode with Once inside a coroutine: // The outer FROM-clause subquery creates a coroutine, and the inner // scalar subquery (SELECT 1) uses Once to evaluate only once per execution. let mut stmt = conn .prepare("SELECT * FROM (SELECT (SELECT 1))") .await .unwrap(); let mut rows = stmt.query(()).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); let value: i64 = row.get(0).unwrap(); assert_eq!(value, 1); assert!(rows.next().await.unwrap().is_none()); drop(rows); stmt.reset().unwrap(); let mut rows = stmt.query(()).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); assert_eq!( row.get_value(0).unwrap(), Value::Integer(1), "Second execution should return 1, not Null. Bug: state.once not cleared in reset()" ); } #[tokio::test] async fn test_strict_tables() { let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); // Create a STRICT table conn.execute( "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT) STRICT", (), ) .await .unwrap(); // Insert valid data conn.execute("INSERT INTO users VALUES (1, 'Alice')", ()) .await .unwrap(); // Query the data let mut rows = conn.query("SELECT id, name FROM users", ()).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); assert_eq!(row.get::(0).unwrap(), 1); assert_eq!(row.get::(1).unwrap(), "Alice"); } // Helper to collect all integer values from a single-column query. async fn collect_ids(conn: &turso::Connection, sql: &str) -> Vec { let mut rows = conn.query(sql, ()).await.unwrap(); let mut ids = Vec::new(); while let Some(row) = rows.next().await.unwrap() { let id: i64 = row.get(0).unwrap(); ids.push(id); } ids } #[tokio::test] async fn test_check_on_conflict_fail() { // FAIL: error on the violating statement, transaction stays active. // Prior inserts within the transaction are preserved and can be committed. let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute( "CREATE TABLE t(id INTEGER PRIMARY KEY, value INTEGER CHECK(value > 0))", (), ) .await .unwrap(); conn.execute("BEGIN", ()).await.unwrap(); conn.execute("INSERT INTO t VALUES(1, 10)", ()) .await .unwrap(); // This should fail but keep the transaction active let err = conn .execute("INSERT OR FAIL INTO t VALUES(2, -5)", ()) .await; assert!( err.is_err(), "INSERT OR FAIL should error on CHECK violation" ); // Transaction is still active — commit it conn.execute("COMMIT", ()).await.unwrap(); // Row 1 should have survived let ids = collect_ids(&conn, "SELECT id FROM t ORDER BY id").await; assert_eq!(ids, vec![1]); } #[tokio::test] async fn test_check_on_conflict_abort() { // ABORT (default): error on the violating statement, transaction stays active. let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute( "CREATE TABLE t(id INTEGER PRIMARY KEY, value INTEGER CHECK(value > 0))", (), ) .await .unwrap(); conn.execute("BEGIN", ()).await.unwrap(); conn.execute("INSERT INTO t VALUES(1, 10)", ()) .await .unwrap(); let err = conn .execute("INSERT OR ABORT INTO t VALUES(2, -5)", ()) .await; assert!( err.is_err(), "INSERT OR ABORT should error on CHECK violation" ); conn.execute("COMMIT", ()).await.unwrap(); let ids = collect_ids(&conn, "SELECT id FROM t ORDER BY id").await; assert_eq!(ids, vec![1]); } #[tokio::test] async fn test_check_on_conflict_rollback() { // ROLLBACK: rolls back the entire transaction. // Prior inserts within the transaction are lost, but committed rows survive. let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute( "CREATE TABLE t(id INTEGER PRIMARY KEY, value INTEGER CHECK(value > 0))", (), ) .await .unwrap(); // Commit row 1 outside the transaction conn.execute("INSERT INTO t VALUES(1, 10)", ()) .await .unwrap(); conn.execute("BEGIN", ()).await.unwrap(); conn.execute("INSERT INTO t VALUES(2, 20)", ()) .await .unwrap(); // This should fail AND roll back the transaction let err = conn .execute("INSERT OR ROLLBACK INTO t VALUES(3, -5)", ()) .await; assert!( err.is_err(), "INSERT OR ROLLBACK should error on CHECK violation" ); // Transaction was rolled back — row 2 is lost, row 1 survives let ids = collect_ids(&conn, "SELECT id FROM t ORDER BY id").await; assert_eq!(ids, vec![1]); } #[tokio::test] async fn test_check_on_conflict_replace() { // REPLACE: for CHECK constraints, behaves like ABORT. // Error, transaction stays active. let db = Builder::new_local(":memory:").build().await.unwrap(); let conn = db.connect().unwrap(); conn.execute( "CREATE TABLE t(id INTEGER PRIMARY KEY, value INTEGER CHECK(value > 0))", (), ) .await .unwrap(); conn.execute("BEGIN", ()).await.unwrap(); conn.execute("INSERT INTO t VALUES(1, 10)", ()) .await .unwrap(); let err = conn .execute("INSERT OR REPLACE INTO t VALUES(1, -5)", ()) .await; assert!( err.is_err(), "INSERT OR REPLACE should error on CHECK violation" ); conn.execute("COMMIT", ()).await.unwrap(); let ids = collect_ids(&conn, "SELECT id FROM t ORDER BY id").await; assert_eq!(ids, vec![1]); } use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; use std::sync::Arc; use std::time::Duration; use tempfile::tempdir; use tokio::sync::Barrier; #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_lost_updates() { let (db, _dir) = setup_mvcc_db( "CREATE TABLE counter(id INTEGER PRIMARY KEY, val INTEGER); INSERT INTO counter VALUES(1, 0);", ) .await; let num_workers: usize = 16; let rounds: i64 = 100; let total_committed = Arc::new(AtomicI64::new(0)); for _round in 0..rounds { let barrier = Arc::new(Barrier::new(num_workers)); let mut handles = Vec::new(); for _ in 0..num_workers { let conn = db.connect().unwrap(); let barrier = barrier.clone(); let total_committed = total_committed.clone(); handles.push(tokio::spawn(async move { barrier.wait().await; if conn.execute("BEGIN CONCURRENT", ()).await.is_err() { return; } if conn .execute("UPDATE counter SET val = val + 1 WHERE id = 1", ()) .await .is_err() { let _ = conn.execute("ROLLBACK", ()).await; return; } match conn.execute("COMMIT", ()).await { Ok(_) => { total_committed.fetch_add(1, Ordering::Relaxed); } Err(_) => { let _ = conn.execute("ROLLBACK", ()).await; } } })); } for handle in handles { handle.await.unwrap(); } } let conn = db.connect().unwrap(); let val = query_i64(&conn, "SELECT val FROM counter WHERE id = 1").await; let committed = total_committed.load(Ordering::Relaxed); assert_eq!( val, committed, "Lost updates! counter={val} but {committed} transactions committed successfully" ); } /// Helper: create MVCC-enabled file-backed database with given schema async fn setup_mvcc_db(schema: &str) -> (turso::Database, tempfile::TempDir) { setup_mvcc_db_with_options(schema).await } /// Helper: create MVCC-enabled file-backed database with options async fn setup_mvcc_db_with_options(schema: &str) -> (turso::Database, tempfile::TempDir) { let dir = tempdir().unwrap(); let db_path = dir.path().join("test.db"); let builder = Builder::new_local(db_path.to_str().unwrap()); let db = builder.build().await.unwrap(); let conn = db.connect().unwrap(); // PRAGMA journal_mode returns a row, so use query() to consume it let mut rows = conn .query("PRAGMA journal_mode = 'mvcc'", ()) .await .unwrap(); while let Ok(Some(_)) = rows.next().await {} drop(rows); if !schema.is_empty() { conn.execute_batch(schema).await.unwrap(); } (db, dir) } /// Helper: query a single i64 value async fn query_i64(conn: &turso::Connection, sql: &str) -> i64 { let mut rows = conn.query(sql, ()).await.unwrap(); let row = rows.next().await.unwrap().unwrap(); row.get::(0).unwrap() } #[tokio::test(flavor = "multi_thread", worker_threads = 8)] #[ignore = "FIXME: This test hangs on main"] async fn test_deadlock_join_during_writes() { let (db, _dir) = setup_mvcc_db( "CREATE TABLE orders(id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER); CREATE TABLE customers(id INTEGER PRIMARY KEY, name TEXT); INSERT INTO customers VALUES(1, 'alice'); INSERT INTO customers VALUES(2, 'bob'); INSERT INTO customers VALUES(3, 'charlie');", ) .await; let done = Arc::new(AtomicBool::new(false)); let mut handles = vec![]; // Writers: insert orders for various customers for w in 0..4 { let db = db.clone(); let done = done.clone(); handles.push(tokio::spawn(async move { let conn = db.connect().unwrap(); let mut i = 0u64; while !done.load(Ordering::Relaxed) { let id = (w as u64) * 100000 + i; let cust = (i % 3) + 1; let _ = conn.execute("BEGIN CONCURRENT", ()).await; let _ = conn .execute( &format!("INSERT INTO orders VALUES({}, {}, {})", id, cust, 10), (), ) .await; let _ = conn.execute("COMMIT", ()).await; i += 1; } })); } // Readers: do JOINs (THIS IS WHAT TRIGGERS THE HANG) for _ in 0..4 { let db = db.clone(); let done = done.clone(); handles.push(tokio::spawn(async move { let conn = db.connect().unwrap(); while !done.load(Ordering::Relaxed) { let _ = conn.execute("BEGIN CONCURRENT", ()).await; let _orphans = match conn .query( "SELECT COUNT(*) FROM orders o LEFT JOIN customers c ON o.customer_id = c.id WHERE c.id IS NULL", (), ) .await { Ok(mut rows) => match rows.next().await { Ok(Some(row)) => row.get::(0).unwrap_or(0), _ => 0, }, Err(_) => 0, }; let _ = conn.execute("COMMIT", ()).await; } })); } // If this test hangs here, the bug is confirmed. tokio::time::sleep(Duration::from_secs(3)).await; done.store(true, Ordering::Relaxed); for handle in handles { // This await will never return if threads are deadlocked handle.await.unwrap(); } } #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_snapshot_isolation_violation() { let (db, _dir) = setup_mvcc_db("CREATE TABLE t(id INTEGER PRIMARY KEY, val INTEGER)").await; let done = Arc::new(AtomicBool::new(false)); let violation_found = Arc::new(AtomicBool::new(false)); let mut handles = Vec::new(); // 4 writers: continuously insert batches of 5 rows for w in 0..4i64 { let conn = db.connect().unwrap(); let done = done.clone(); handles.push(tokio::spawn(async move { let mut i = 0i64; while !done.load(Ordering::Relaxed) { if conn.execute("BEGIN CONCURRENT", ()).await.is_err() { continue; } let mut ok = true; for j in 0..5i64 { let id = w * 100_000 + i * 5 + j; if conn .execute(&format!("INSERT INTO t VALUES({id}, {id})"), ()) .await .is_err() { ok = false; break; } } if ok { if conn.execute("COMMIT", ()).await.is_err() { let _ = conn.execute("ROLLBACK", ()).await; } } else { let _ = conn.execute("ROLLBACK", ()).await; } i += 1; } })); } // 4 readers: open snapshot, read COUNT(*) twice, assert they match for _ in 0..4 { let conn = db.connect().unwrap(); let done = done.clone(); let violation_found = violation_found.clone(); handles.push(tokio::spawn(async move { while !done.load(Ordering::Relaxed) { if conn.execute("BEGIN CONCURRENT", ()).await.is_err() { continue; } let count1 = query_i64(&conn, "SELECT COUNT(*) FROM t").await; tokio::task::yield_now().await; // Let writers commit between reads let count2 = query_i64(&conn, "SELECT COUNT(*) FROM t").await; let _ = conn.execute("COMMIT", ()).await; if count1 != count2 { violation_found.store(true, Ordering::Relaxed); eprintln!( "VIOLATION: COUNT changed {} -> {} within same txn (delta={})", count1, count2, count2 - count1 ); } } })); } tokio::time::sleep(Duration::from_secs(3)).await; done.store(true, Ordering::Relaxed); for handle in handles { let _ = handle.await; } assert!( !violation_found.load(Ordering::Relaxed), "Snapshot isolation violated: COUNT(*) changed within a single BEGIN CONCURRENT txn" ); } #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_ghost_commits() { for iteration in 0..500 { if iteration % 100 == 0 { eprintln!("test_ghost_commits: Iteration {iteration}"); } let (db, _dir) = setup_mvcc_db("CREATE TABLE t(id INTEGER PRIMARY KEY, val INTEGER)").await; let num_workers: usize = 8; let ops_per_worker: i64 = 100; let barrier = Arc::new(Barrier::new(num_workers)); let mut handles = Vec::new(); for worker_id in 0..num_workers as i64 { let conn = db.connect().unwrap(); let barrier = barrier.clone(); handles.push(tokio::spawn(async move { barrier.wait().await; let mut successes = 0i64; let mut errors = 0i64; for i in 0..ops_per_worker { let id = worker_id * 10_000 + i; // Autocommit INSERT (no explicit BEGIN/COMMIT) match conn .execute(&format!("INSERT INTO t VALUES({id}, {i})"), ()) .await { Ok(_) => successes += 1, Err(turso::Error::Busy(_) | turso::Error::BusySnapshot(_)) => errors += 1, // Busy("database is locked") Err(e) => panic!("unexpected error: {e:?}"), } } (successes, errors) })); } let mut total_successes = 0i64; let mut total_errors = 0i64; for handle in handles { let (s, e) = handle.await.unwrap(); total_successes += s; total_errors += e; } let conn = db.connect().unwrap(); let actual_rows = query_i64(&conn, "SELECT COUNT(*) FROM t").await; if iteration % 100 == 0 { eprintln!("test_ghost_commits: Iteration {iteration}, actual_rows={actual_rows}, total_successes={total_successes}, total_errors={total_errors}"); } assert_eq!( actual_rows, total_successes, "Ghost commits! {actual_rows} rows in DB but only {total_successes} reported as Ok ({total_errors} errors). \ {} inserts committed despite returning Busy.", total_successes - actual_rows, ); } } /// AUTOINCREMENT is not supported in MVCC mode. Verify that CREATE TABLE /// with AUTOINCREMENT fails with a clear error message. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_autoincrement_blocked_in_mvcc() { let (db, _dir) = setup_mvcc_db("").await; let conn = db.connect().unwrap(); // CREATE TABLE with AUTOINCREMENT should fail let result = conn .execute( "CREATE TABLE t(a INTEGER PRIMARY KEY AUTOINCREMENT, b TEXT)", (), ) .await; assert!( result.is_err(), "CREATE TABLE with AUTOINCREMENT should fail in MVCC mode" ); let err = result.unwrap_err().to_string(); assert!( err.contains("AUTOINCREMENT is not supported in MVCC mode"), "unexpected error: {err}" ); // Regular tables without AUTOINCREMENT should still work conn.execute("CREATE TABLE t(a INTEGER PRIMARY KEY, b TEXT)", ()) .await .unwrap(); conn.execute("INSERT INTO t VALUES (1, 'hello')", ()) .await .unwrap(); let count = query_i64(&conn, "SELECT COUNT(*) FROM t").await; assert_eq!(count, 1); } ================================================ FILE: bindings/rust/tests/test_deadlock_join.rs ================================================ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use tempfile::tempdir; use turso::Builder; async fn setup_mvcc_db(schema: &str) -> (turso::Database, tempfile::TempDir) { let dir = tempdir().unwrap(); let db_path = dir.path().join("test.db"); let db = Builder::new_local(db_path.to_str().unwrap()) .build() .await .unwrap(); let conn = db.connect().unwrap(); let mut rows = conn .query("PRAGMA journal_mode = 'mvcc'", ()) .await .unwrap(); while let Ok(Some(_)) = rows.next().await {} drop(rows); if !schema.is_empty() { conn.execute_batch(schema).await.unwrap(); } (db, dir) } #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_deadlock_join_during_writes() { let (db, _dir) = setup_mvcc_db( "CREATE TABLE orders(id INTEGER PRIMARY KEY, customer_id INTEGER, amount INTEGER); CREATE TABLE customers(id INTEGER PRIMARY KEY, name TEXT); INSERT INTO customers VALUES(1, 'alice'); INSERT INTO customers VALUES(2, 'bob'); INSERT INTO customers VALUES(3, 'charlie');", ) .await; let done = Arc::new(AtomicBool::new(false)); let mut handles = vec![]; // Writers: insert orders for various customers for w in 0..4 { let db = db.clone(); let done = done.clone(); handles.push(tokio::spawn(async move { let conn = db.connect().unwrap(); let mut i = 0u64; while !done.load(Ordering::Relaxed) { let id = (w as u64) * 100000 + i; let cust = (i % 3) + 1; let _ = conn.execute("BEGIN CONCURRENT", ()).await; let _ = conn .execute( &format!("INSERT INTO orders VALUES({}, {}, {})", id, cust, 10), (), ) .await; let _ = conn.execute("COMMIT", ()).await; i += 1; } })); } // Readers: do JOINs (THIS IS WHAT TRIGGERS THE HANG) for _ in 0..4 { let db = db.clone(); let done = done.clone(); handles.push(tokio::spawn(async move { let conn = db.connect().unwrap(); while !done.load(Ordering::Relaxed) { let _ = conn.execute("BEGIN CONCURRENT", ()).await; let _orphans = match conn .query( "SELECT COUNT(*) FROM orders o LEFT JOIN customers c ON o.customer_id = c.id WHERE c.id IS NULL", (), ) .await { Ok(mut rows) => match rows.next().await { Ok(Some(row)) => row.get::(0).unwrap_or(0), _ => 0, }, Err(_) => 0, }; let _ = conn.execute("COMMIT", ()).await; } })); } // If this test hangs here, the bug is confirmed. tokio::time::sleep(Duration::from_secs(3)).await; done.store(true, Ordering::Relaxed); for handle in handles { // This await will never return if threads are deadlocked handle.await.unwrap(); } } ================================================ FILE: bindings/tcl/Makefile ================================================ # Makefile for the Turso TCL extension. # # The shared library wraps libturso_sqlite3 and exposes an in-process sqlite3 # Tcl command compatible with the upstream SQLite Tcl binding, enabling the # testing/sqlite3 harness to run without a subprocess per statement. # # Primary targets (Linux / Docker — no host toolchain required): # docker-build Build libturso_tcl.so inside a Debian container. # docker-test Build and run the probe tests inside a Debian container. # # Local targets (requires TCL dev headers on the host): # build Build for the current platform (macOS → .dylib, Linux → .so). # clean Remove built artefacts. # # Usage: # make docker-build # produce bindings/tcl/libturso_tcl.so # make docker-test # build + run probe tests # make build # local build when TCL headers are available # make clean ROOT_DIR := $(shell cd ../.. && pwd) TURSO_LIB := $(ROOT_DIR)/target/debug SRC := $(ROOT_DIR)/bindings/tcl/turso_tcl.c INCLUDE := $(ROOT_DIR)/sqlite3/include # Default target — local build (no Docker required). .PHONY: all all: build # ---- Docker ----------------------------------------------------------------- DOCKER_IMAGE ?= rust:bookworm .PHONY: docker-build docker-build: docker run --rm \ -v "$(ROOT_DIR):/workspace" \ -w /workspace \ $(DOCKER_IMAGE) \ bash -c '\ apt-get update -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq tcl-dev && \ cargo build -p turso_sqlite3 2>&1 && \ gcc -shared -fPIC -o bindings/tcl/libturso_tcl.so \ bindings/tcl/turso_tcl.c \ -I sqlite3/include \ -I /usr/include/tcl8.6 \ -L target/debug \ -lturso_sqlite3 \ -Wl,-rpath,'"'"'$$ORIGIN/../../target/debug'"'"' \ 2>&1 && \ echo "Build OK: bindings/tcl/libturso_tcl.so" \ ' # Build the shared library and run the probe tests in one container so the # apt install and cargo build happen only once. .PHONY: docker-test docker-test: docker run --rm \ -v "$(ROOT_DIR):/workspace" \ -w /workspace \ $(DOCKER_IMAGE) \ bash -c '\ apt-get update -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq tcl-dev && \ cargo build -p turso_sqlite3 2>&1 && \ gcc -shared -fPIC -o bindings/tcl/libturso_tcl.so \ bindings/tcl/turso_tcl.c \ -I sqlite3/include \ -I /usr/include/tcl8.6 \ -L target/debug \ -lturso_sqlite3 \ -Wl,-rpath,'"'"'$$ORIGIN/../../target/debug'"'"' \ 2>&1 && \ echo "Build OK: bindings/tcl/libturso_tcl.so" && \ LD_LIBRARY_PATH=target/debug tclsh bindings/tcl/test_probes.tcl \ ' # ---- Local build (macOS / Linux with TCL dev headers) ----------------------- UNAME_S := $(shell uname -s 2>/dev/null) ifeq ($(UNAME_S),Darwin) # Prefer Homebrew tcl-tk via pkg-config; fall back to the Xcode SDK # Tcl.framework headers present on any machine with Command Line Tools. _TCL_SDK_HDR := $(shell xcrun --show-sdk-path 2>/dev/null)/System/Library/Frameworks/Tcl.framework/Versions/8.5/Headers TCL_INCLUDE ?= $(shell pkg-config --cflags tcl 2>/dev/null || echo "-I$(_TCL_SDK_HDR)") TCL_LIBS ?= $(shell pkg-config --libs tcl 2>/dev/null || echo "-framework Tcl") SHARED_FLAG = -dynamiclib TARGET = $(ROOT_DIR)/bindings/tcl/libturso_tcl.dylib else TCL_INCLUDE ?= $(shell pkg-config --cflags tcl 2>/dev/null || echo "-I/usr/include/tcl8.6") TCL_LIBS ?= $(shell pkg-config --libs tcl 2>/dev/null || echo "-ltcl8.6") SHARED_FLAG = -shared -fPIC TARGET = $(ROOT_DIR)/bindings/tcl/libturso_tcl.so endif .PHONY: build build: cargo build -p turso_sqlite3 gcc $(SHARED_FLAG) -o $(TARGET) \ $(SRC) \ $(TCL_INCLUDE) \ -I$(INCLUDE) \ -L$(TURSO_LIB) \ -lturso_sqlite3 \ $(TCL_LIBS) \ -Wl,-rpath,'$(TURSO_LIB)' @echo "Built: $(TARGET)" # ---- Clean ------------------------------------------------------------------ .PHONY: clean clean: rm -f $(ROOT_DIR)/bindings/tcl/libturso_tcl.so \ $(ROOT_DIR)/bindings/tcl/libturso_tcl.dylib ================================================ FILE: bindings/tcl/test_probes.tcl ================================================ # Probe tests for the native Turso TCL extension (bindings/tcl/turso_tcl.c). # # Validates the three capabilities that the subprocess shim cannot provide: # 1. Real engine error codes via [db errorcode]. # 2. Accurate DML change counters via [db changes] / [db total_changes]. # 3. In-process Tcl function registration via [db func]. # # Run via: # LD_LIBRARY_PATH=target/debug tclsh bindings/tcl/test_probes.tcl # # Exit code: 0 on success, 1 on any failure. set pass 0 set fail 0 proc ok {label} { puts "PASS $label" incr ::pass } proc fail {label want got} { puts "FAIL $label" puts " want: $want" puts " got: $got" incr ::fail } proc assert_eq {label want got} { if {$got eq $want} { ok $label } else { fail $label $want $got } } proc assert_ne {label not_want got} { if {$got ne $not_want} { ok $label } else { fail $label "anything other than $not_want" $got } } # --------------------------------------------------------------------------- # Load the native extension. # --------------------------------------------------------------------------- set here [file dirname [file normalize [info script]]] set lib [file join $here libturso_tcl.so] if {![file exists $lib]} { puts "ERROR: $lib not found — run 'make docker-build' first" exit 1 } if {[catch {load $lib Tursotcl} err]} { puts "ERROR: failed to load $lib: $err" exit 1 } # Use an in-memory database so tests leave no files on disk. sqlite3 db :memory: # --------------------------------------------------------------------------- # Probe 1: error code fidelity. # The subprocess shim always returned 0; the native module returns the real # SQLite result code from the engine. # --------------------------------------------------------------------------- catch {db eval {SELECT * FROM no_such_table;}} assert_ne "errorcode is non-zero after bad query" 0 [db errorcode] # --------------------------------------------------------------------------- # Probe 2: DML change counters. # The subprocess shim always returned 0; the native module tracks them via # sqlite3_changes() and sqlite3_total_changes(). # --------------------------------------------------------------------------- db eval {CREATE TABLE tc(x);} db eval {INSERT INTO tc VALUES(1);} assert_eq "changes is 1 after single INSERT" 1 [db changes] db eval {INSERT INTO tc VALUES(2);} assert_ne "total_changes accumulates across stmts" 0 [db total_changes] # --------------------------------------------------------------------------- # Probe 3: in-process Tcl function registration. # The subprocess shim accepted [db func] but did not wire the callback into # the SQL engine; the native module routes calls through sqlite3_create_function. # --------------------------------------------------------------------------- db func my_echo {x} { return $x } set result [db eval {SELECT my_echo(42);}] assert_eq "db func result echoed back" 42 $result db func add2 {a b} { expr {$a + $b} } set result [db eval {SELECT add2(3, 7);}] assert_eq "db func with two args" 10 $result # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- db close puts "" puts "$pass passed, $fail failed" if {$fail > 0} { exit 1 } ================================================ FILE: bindings/tcl/turso_tcl.c ================================================ /* * turso_tcl.c — Native Tcl extension for Turso/Limbo database. * * Provides the `sqlite3` Tcl command that creates in-process database * connections, replacing the subprocess-based shim in testing/sqlite3/tester.tcl. * * Supported db sub-commands: * eval SQL ?array? ?script? — execute SQL, return results as list * one SQL — return first column of first row * exists SQL — return 1 if query returns any row * changes — rows affected by last DML * total_changes — total rows changed since open * last_insert_rowid — rowid of last INSERT * errorcode — most recent error code * errmsg — most recent error message * null ?value? — get/set NULL representation string * func name ?arg...? body — register a Tcl-backed scalar SQL function * close — close database and delete command * limit ... — stub returning a default value */ #include #include #include #include #define TURSO_TCL_VERSION "1.0" #define MAX_FUNC_ARGS 64 /* ------------------------------------------------------------------ */ /* TursoDb — state for a single open database connection */ /* ------------------------------------------------------------------ */ typedef struct TursoDb { sqlite3 *db; Tcl_Interp *interp; Tcl_Obj *null_obj; /* replacement string for NULL values */ } TursoDb; /* ------------------------------------------------------------------ */ /* TclFuncData — state for a Tcl-backed scalar SQL function */ /* ------------------------------------------------------------------ */ typedef struct TclFuncData { Tcl_Interp *interp; Tcl_Obj *script; /* function body */ int n_args; Tcl_Obj *arg_names[MAX_FUNC_ARGS]; /* argument variable names */ } TclFuncData; /* ------------------------------------------------------------------ */ /* Value helpers */ /* ------------------------------------------------------------------ */ /* Convert a column value to a Tcl_Obj. */ static Tcl_Obj *column_to_obj(sqlite3_stmt *stmt, int i, const char *null_str) { int ctype = sqlite3_column_type(stmt, i); switch (ctype) { case SQLITE_INTEGER: return Tcl_NewWideIntObj((Tcl_WideInt)sqlite3_column_int64(stmt, i)); case SQLITE_FLOAT: return Tcl_NewDoubleObj(sqlite3_column_double(stmt, i)); case SQLITE_TEXT: { const char *text = (const char *)sqlite3_column_text(stmt, i); return Tcl_NewStringObj(text ? text : "", -1); } case SQLITE_BLOB: { const void *blob = sqlite3_column_blob(stmt, i); int nbytes = sqlite3_column_bytes(stmt, i); return Tcl_NewByteArrayObj((const unsigned char *)blob, nbytes); } default: /* NULL */ return Tcl_NewStringObj(null_str ? null_str : "", -1); } } /* Convert a function argument (sqlite3_value*) to a Tcl_Obj. */ static Tcl_Obj *value_to_obj(void *argv_i) { int vtype = sqlite3_value_type(argv_i); switch (vtype) { case SQLITE_INTEGER: return Tcl_NewWideIntObj((Tcl_WideInt)sqlite3_value_int64(argv_i)); case SQLITE_FLOAT: return Tcl_NewDoubleObj(sqlite3_value_double(argv_i)); case SQLITE_TEXT: { const char *text = (const char *)sqlite3_value_text(argv_i); return Tcl_NewStringObj(text ? text : "", -1); } case SQLITE_BLOB: { const void *blob = sqlite3_value_blob(argv_i); int nbytes = sqlite3_value_bytes(argv_i); return Tcl_NewByteArrayObj((const unsigned char *)blob, nbytes); } default: /* NULL */ return Tcl_NewStringObj("", 0); } } /* ------------------------------------------------------------------ */ /* Tcl scalar function bridge */ /* ------------------------------------------------------------------ */ static void tcl_scalar_bridge(void *ctx, int argc, void **argv) { TclFuncData *func = (TclFuncData *)sqlite3_user_data(ctx); Tcl_Interp *interp = func->interp; int i, rc; /* Bind argument variables in the calling scope. */ for (i = 0; i < argc && i < func->n_args; i++) { Tcl_Obj *val = value_to_obj(argv[i]); if (Tcl_ObjSetVar2(interp, func->arg_names[i], NULL, val, TCL_LEAVE_ERR_MSG) == NULL) { sqlite3_result_error(ctx, Tcl_GetString(Tcl_GetObjResult(interp)), -1); return; } } /* Evaluate the script body. */ rc = Tcl_EvalObjEx(interp, func->script, 0); if (rc == TCL_ERROR) { const char *err = Tcl_GetString(Tcl_GetObjResult(interp)); sqlite3_result_error(ctx, err, -1); return; } /* Convert the Tcl result to an SQL value. */ Tcl_Obj *result = Tcl_GetObjResult(interp); Tcl_WideInt ival; double dval; if (Tcl_GetWideIntFromObj(NULL, result, &ival) == TCL_OK) { sqlite3_result_int64(ctx, (int64_t)ival); } else if (Tcl_GetDoubleFromObj(NULL, result, &dval) == TCL_OK) { sqlite3_result_double(ctx, dval); } else { int slen; const char *str = Tcl_GetStringFromObj(result, &slen); sqlite3_result_text(ctx, str, slen, SQLITE_TRANSIENT); } } static void tcl_func_destroy(void *pApp) { TclFuncData *func = (TclFuncData *)pApp; int i; if (!func) return; if (func->script) Tcl_DecrRefCount(func->script); for (i = 0; i < func->n_args; i++) { if (func->arg_names[i]) Tcl_DecrRefCount(func->arg_names[i]); } Tcl_Free((char *)func); } /* ------------------------------------------------------------------ */ /* Multi-statement SQL execution helpers */ /* ------------------------------------------------------------------ */ /* * Execute all statements in `sql`, collecting result rows from the last * statement that returns rows into `result_list`. * Returns TCL_OK or TCL_ERROR; sets the interpreter result on error. */ static int exec_sql_collect(Tcl_Interp *interp, sqlite3 *db, const char *sql, const char *null_str, Tcl_Obj **result_list_out) { Tcl_Obj *result_list = Tcl_NewListObj(0, NULL); Tcl_IncrRefCount(result_list); const char *remaining = sql; int rc; while (remaining && *remaining) { /* skip leading whitespace and bare semicolons */ while (*remaining == ' ' || *remaining == '\n' || *remaining == '\t' || *remaining == '\r' || *remaining == ';') { remaining++; } if (!*remaining) break; sqlite3_stmt *stmt = NULL; const char *tail = NULL; rc = sqlite3_prepare_v2(db, remaining, -1, &stmt, &tail); if (rc != SQLITE_OK) { Tcl_DecrRefCount(result_list); Tcl_SetResult(interp, (char *)sqlite3_errmsg(db), TCL_VOLATILE); return TCL_ERROR; } if (!stmt) { /* empty / comment-only statement */ remaining = tail; continue; } /* reset the list for each non-empty statement so the caller sees the results of the final one (matches SQLite tclsqlite behaviour) */ Tcl_DecrRefCount(result_list); result_list = Tcl_NewListObj(0, NULL); Tcl_IncrRefCount(result_list); int ncols = sqlite3_column_count(stmt); while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { int i; for (i = 0; i < ncols; i++) { Tcl_Obj *val = column_to_obj(stmt, i, null_str); Tcl_ListObjAppendElement(interp, result_list, val); } } sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { Tcl_DecrRefCount(result_list); Tcl_SetResult(interp, (char *)sqlite3_errmsg(db), TCL_VOLATILE); return TCL_ERROR; } remaining = tail; } *result_list_out = result_list; return TCL_OK; } /* ------------------------------------------------------------------ */ /* db command dispatcher */ /* ------------------------------------------------------------------ */ static void TursoDbFree(ClientData cd) { TursoDb *tdb = (TursoDb *)cd; if (!tdb) return; if (tdb->db) sqlite3_close(tdb->db); if (tdb->null_obj) Tcl_DecrRefCount(tdb->null_obj); Tcl_Free((char *)tdb); } static int TursoDbCmd(ClientData cd, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[]) { TursoDb *tdb = (TursoDb *)cd; static const char *cmds[] = { "eval", "one", "exists", "changes", "total_changes", "last_insert_rowid", "errorcode", "errmsg", "null", "func", "function", "close", "limit", NULL }; enum { CMD_EVAL, CMD_ONE, CMD_EXISTS, CMD_CHANGES, CMD_TOTAL_CHANGES, CMD_LAST_INSERT_ROWID, CMD_ERRORCODE, CMD_ERRMSG, CMD_NULL, CMD_FUNC, CMD_FUNCTION, CMD_CLOSE, CMD_LIMIT }; int cmdIdx; if (objc < 2) { Tcl_WrongNumArgs(interp, 1, objv, "subcommand ?args?"); return TCL_ERROR; } if (Tcl_GetIndexFromObj(interp, objv[1], cmds, "subcommand", 0, &cmdIdx) != TCL_OK) { return TCL_ERROR; } switch (cmdIdx) { /* ---- simple counters / metadata ---- */ case CMD_CHANGES: Tcl_SetObjResult(interp, Tcl_NewIntObj(sqlite3_changes(tdb->db))); return TCL_OK; case CMD_TOTAL_CHANGES: Tcl_SetObjResult(interp, Tcl_NewIntObj(sqlite3_total_changes(tdb->db))); return TCL_OK; case CMD_LAST_INSERT_ROWID: Tcl_SetObjResult(interp, Tcl_NewWideIntObj((Tcl_WideInt)sqlite3_last_insert_rowid(tdb->db))); return TCL_OK; case CMD_ERRORCODE: Tcl_SetObjResult(interp, Tcl_NewIntObj(sqlite3_errcode(tdb->db))); return TCL_OK; case CMD_ERRMSG: Tcl_SetResult(interp, (char *)sqlite3_errmsg(tdb->db), TCL_VOLATILE); return TCL_OK; /* ---- null value string ---- */ case CMD_NULL: if (objc == 3) { if (tdb->null_obj) Tcl_DecrRefCount(tdb->null_obj); tdb->null_obj = objv[2]; Tcl_IncrRefCount(tdb->null_obj); } Tcl_SetObjResult(interp, tdb->null_obj ? tdb->null_obj : Tcl_NewStringObj("", 0)); return TCL_OK; /* ---- close ---- */ case CMD_CLOSE: Tcl_DeleteCommand(interp, Tcl_GetString(objv[0])); return TCL_OK; /* ---- limit (stub) ---- */ case CMD_LIMIT: Tcl_SetObjResult(interp, Tcl_NewIntObj(1000000)); return TCL_OK; /* ---- eval ---- */ case CMD_EVAL: { if (objc < 3 || objc > 5) { Tcl_WrongNumArgs(interp, 2, objv, "sql ?array? ?script?"); return TCL_ERROR; } const char *sql = Tcl_GetString(objv[2]); const char *null_str = tdb->null_obj ? Tcl_GetString(tdb->null_obj) : ""; /* db eval sql — collect all result values into a flat list */ if (objc == 3) { Tcl_Obj *result_list = NULL; int rc = exec_sql_collect(interp, tdb->db, sql, null_str, &result_list); if (rc != TCL_OK) return rc; Tcl_SetObjResult(interp, result_list); Tcl_DecrRefCount(result_list); return TCL_OK; } /* db eval sql array script — per-row callback */ if (objc == 5) { Tcl_Obj *array_name = objv[3]; Tcl_Obj *script = objv[4]; const char *remaining = sql; int loop_rc = TCL_OK; while (remaining && *remaining) { while (*remaining == ' ' || *remaining == '\n' || *remaining == '\t' || *remaining == '\r' || *remaining == ';') { remaining++; } if (!*remaining) break; sqlite3_stmt *stmt = NULL; const char *tail = NULL; int rc = sqlite3_prepare_v2(tdb->db, remaining, -1, &stmt, &tail); if (rc != SQLITE_OK) { Tcl_SetResult(interp, (char *)sqlite3_errmsg(tdb->db), TCL_VOLATILE); return TCL_ERROR; } if (!stmt) { remaining = tail; continue; } int ncols = sqlite3_column_count(stmt); /* Set array(*) to the list of column names. */ Tcl_Obj *col_list = Tcl_NewListObj(0, NULL); int i; for (i = 0; i < ncols; i++) { const char *col = sqlite3_column_name(stmt, i); Tcl_ListObjAppendElement(interp, col_list, Tcl_NewStringObj(col ? col : "", -1)); } Tcl_ObjSetVar2(interp, array_name, Tcl_NewStringObj("*", 1), col_list, 0); while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { for (i = 0; i < ncols; i++) { const char *col = sqlite3_column_name(stmt, i); Tcl_Obj *val = column_to_obj(stmt, i, null_str); Tcl_ObjSetVar2(interp, array_name, Tcl_NewStringObj(col ? col : "", -1), val, 0); } loop_rc = Tcl_EvalObjEx(interp, script, 0); if (loop_rc == TCL_BREAK) { loop_rc = TCL_OK; break; } else if (loop_rc == TCL_CONTINUE) { loop_rc = TCL_OK; } else if (loop_rc != TCL_OK) { break; } } sqlite3_finalize(stmt); if (loop_rc != TCL_OK) return loop_rc; if (rc != SQLITE_DONE && rc != SQLITE_ROW) { Tcl_SetResult(interp, (char *)sqlite3_errmsg(tdb->db), TCL_VOLATILE); return TCL_ERROR; } remaining = tail; } Tcl_ResetResult(interp); return TCL_OK; } /* objc == 4: not a standard form we support */ Tcl_WrongNumArgs(interp, 2, objv, "sql ?array script?"); return TCL_ERROR; } /* ---- one ---- */ case CMD_ONE: { if (objc != 3) { Tcl_WrongNumArgs(interp, 2, objv, "sql"); return TCL_ERROR; } const char *sql = Tcl_GetString(objv[2]); const char *null_str = tdb->null_obj ? Tcl_GetString(tdb->null_obj) : ""; sqlite3_stmt *stmt = NULL; int rc = sqlite3_prepare_v2(tdb->db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { Tcl_SetResult(interp, (char *)sqlite3_errmsg(tdb->db), TCL_VOLATILE); return TCL_ERROR; } Tcl_Obj *result = Tcl_NewStringObj(null_str, -1); if (sqlite3_step(stmt) == SQLITE_ROW) { result = column_to_obj(stmt, 0, null_str); } sqlite3_finalize(stmt); Tcl_SetObjResult(interp, result); return TCL_OK; } /* ---- exists ---- */ case CMD_EXISTS: { if (objc != 3) { Tcl_WrongNumArgs(interp, 2, objv, "sql"); return TCL_ERROR; } const char *sql = Tcl_GetString(objv[2]); sqlite3_stmt *stmt = NULL; int rc = sqlite3_prepare_v2(tdb->db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { Tcl_SetResult(interp, (char *)sqlite3_errmsg(tdb->db), TCL_VOLATILE); return TCL_ERROR; } int exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0; sqlite3_finalize(stmt); Tcl_SetObjResult(interp, Tcl_NewBooleanObj(exists)); return TCL_OK; } /* ---- func / function ---- */ case CMD_FUNC: case CMD_FUNCTION: { /* * db func name ?arglist? body * db function name ?arglist? body * * Registers a Tcl proc body as a scalar SQL function. The arglist * mirrors proc syntax: it may be a single Tcl list object ({a b}) or * multiple individual words (a b) — both result in named variables * being bound before the body is evaluated. * * objv[2] = function name * objv[3..objc-2] = argument variable names, OR a single Tcl list * objv[objc-1] = script body */ if (objc < 4) { Tcl_WrongNumArgs(interp, 2, objv, "name ?arglist? body"); return TCL_ERROR; } const char *func_name = Tcl_GetString(objv[2]); Tcl_Obj *body = objv[objc - 1]; int i; /* Resolve the argument variable names. * * objc == 4: db func name body → no named args * objc == 5: db func name argspec body → argspec is a Tcl list * objc >= 6: db func name a b … body → each word is a name */ int n_args = 0; Tcl_Obj **arg_objs = NULL; if (objc == 5) { /* Single argspec object — split it as a Tcl list so that both * `db func f x body` and `db func f {x y} body` work. */ if (Tcl_ListObjGetElements(interp, objv[3], &n_args, &arg_objs) != TCL_OK) { return TCL_ERROR; } } else if (objc > 5) { n_args = objc - 4; arg_objs = (Tcl_Obj **)&objv[3]; } TclFuncData *func_data = (TclFuncData *)Tcl_Alloc(sizeof(TclFuncData)); memset(func_data, 0, sizeof(TclFuncData)); func_data->interp = interp; func_data->script = body; Tcl_IncrRefCount(body); func_data->n_args = (n_args < MAX_FUNC_ARGS) ? n_args : MAX_FUNC_ARGS; for (i = 0; i < func_data->n_args; i++) { func_data->arg_names[i] = arg_objs[i]; Tcl_IncrRefCount(func_data->arg_names[i]); } int sql_n_args = (n_args == 0) ? -1 : n_args; int rc = sqlite3_create_function_v2( tdb->db, func_name, sql_n_args, 0, /* SQLITE_UTF8 */ (void *)func_data, (void (*)(void))tcl_scalar_bridge, NULL, NULL, (void (*)(void))tcl_func_destroy ); if (rc != SQLITE_OK) { tcl_func_destroy(func_data); Tcl_SetResult(interp, (char *)sqlite3_errmsg(tdb->db), TCL_VOLATILE); return TCL_ERROR; } return TCL_OK; } } /* switch */ return TCL_OK; } /* ------------------------------------------------------------------ */ /* sqlite3 open command */ /* ------------------------------------------------------------------ */ static int TursoOpenCmd(ClientData cd, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[]) { (void)cd; if (objc < 3) { Tcl_WrongNumArgs(interp, 1, objv, "name filename ?options?"); return TCL_ERROR; } const char *handle_name = Tcl_GetString(objv[1]); const char *filename = Tcl_GetString(objv[2]); sqlite3 *db = NULL; int rc = sqlite3_open(filename, &db); if (rc != SQLITE_OK) { const char *errmsg = db ? sqlite3_errmsg(db) : "out of memory"; Tcl_SetResult(interp, (char *)errmsg, TCL_VOLATILE); if (db) sqlite3_close(db); return TCL_ERROR; } TursoDb *tdb = (TursoDb *)Tcl_Alloc(sizeof(TursoDb)); tdb->db = db; tdb->interp = interp; tdb->null_obj = NULL; Tcl_CreateObjCommand(interp, handle_name, TursoDbCmd, (ClientData)tdb, TursoDbFree); Tcl_SetResult(interp, (char *)handle_name, TCL_VOLATILE); return TCL_OK; } /* ------------------------------------------------------------------ */ /* Extension initialisation */ /* ------------------------------------------------------------------ */ int Tursotcl_Init(Tcl_Interp *interp) { if (Tcl_InitStubs(interp, TCL_VERSION, 0) == NULL) { return TCL_ERROR; } Tcl_CreateObjCommand(interp, "sqlite3", TursoOpenCmd, NULL, NULL); Tcl_PkgProvide(interp, "tursotcl", TURSO_TCL_VERSION); return TCL_OK; } ================================================ FILE: cli/Cargo.toml ================================================ # Copyright 2023 the Limbo authors. All rights reserved. MIT license. [package] authors.workspace = true default-run = "tursodb" description = "The Turso interactive SQL shell" edition.workspace = true license.workspace = true name = "turso_cli" repository.workspace = true version.workspace = true [package.metadata.dist] dist = true [[bin]] name = "tursodb" path = "main.rs" [dependencies] anyhow.workspace = true cfg-if = { workspace = true } clap = { workspace = true, features = ["derive"] } clap_complete = { version = "=4.5.47", features = ["unstable-dynamic"] } comfy-table = "7.1.4" csv = "1.3.1" ctrlc = "3.4.4" dirs = "5.0.1" env_logger = { workspace = true } libc = "0.2.172" turso_core = { workspace = true, default-features = true, features = [ "cli_only", "conn_raw_api", "fs", "fts", ] } turso_sync_engine = { workspace = true } limbo_completion = { workspace = true, features = ["static"] } miette = { workspace = true, features = ["fancy"] } nu-ansi-term = { version = "0.50.1", features = [ "serde", "derive_serde_style", ] } rustyline = { version = "15.0.0", default-features = true, features = [ "derive", ] } shlex = "1.3.0" syntect = { git = "https://github.com/trishume/syntect.git", rev = "64644ffe064457265cbcee12a0c1baf9485ba6ee", default-features = false, features = ["default-fancy"] } tracing = { workspace = true } tracing-appender = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } toml = { version = "0.8.20", features = ["preserve_order"] } schemars = { version = "0.8.22", features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } validator = { version = "0.20.0", features = ["derive"] } toml_edit = { version = "0.22.24", features = ["serde"] } serde_json = "1.0" termimad = "0.30" include_dir = "0.7" rand = "0.8" mimalloc = { workspace = true, optional = true } prost = "0.14.1" roaring = "0.11.2" bytes = "1.11.0" itertools.workspace = true [features] default = ["io_uring", "mimalloc"] io_uring = ["turso_core/io_uring"] experimental_win_iocp = ["turso_core/experimental_win_iocp"] tracing_release = ["turso_core/tracing_release"] mimalloc = ["dep:mimalloc"] fts = ["turso_core/fts"] mvcc_repl = [] [build-dependencies] syntect = { git = "https://github.com/trishume/syntect.git", rev = "64644ffe064457265cbcee12a0c1baf9485ba6ee", default-features = false, features = ["default-fancy"] } include_dir = "0.7" ================================================ FILE: cli/SQL.sublime-syntax ================================================ %YAML 1.2 --- name: SQL scope: source.sql version: 2 hidden: true variables: string_escape: (?:\\.) simple_identifier: (?:\w+) simple_identifier_break: (?!\w) toplevel_reserved: |- (?xi: alter | analyze | create | cross | delete | drop | explain | from | grant | group | inner | insert | join | left | on | order | outer | right | select | set | truncate | union | update | where ) additional_toplevel_reserved: (?!) # TODO: not all are supported by all dialects! ddl_target: |- (?xi: {{ddl_target_other}} | (?: {{ddl_target_function_modifier}} \s+ )? {{ddl_target_function}} | (?: {{ddl_target_index_modifier}} \s+ )? index | (?: {{ddl_target_table_modifier}} \s+ )? table ) ddl_target_function: |- (?xi: procedure | function ) ddl_target_other: |- (?xi: aggregate | column | constraint | conversion | database | domain | group | language | member | operator\s+class | operator | role | rule | schema | sequence | tablespace | trigger | type | user | view ) ddl_target_inside_alter_table: |- (?xi: constraint ) ddl_target_function_modifier: |- (?xi: aggregate ) ddl_target_index_modifier: |- (?xi: clustered | fulltext | spatial | unique ) ddl_target_table_modifier: |- (?xi: temp(?:orary)? ) function_parameter_modifier: |- (?xi: inout | in | out ) simple_types: |- (?xi: boolean | bool | year ) types_with_optional_number: |- (?xi: bit | datetime | int | number | n?(?:var)?char | varbinary ) type_modifiers: |- (?xi: signed | unsigned | zerofill ) builtin_scalar_functions: |- (?xi: current_(?: date | time(?:stamp)? ) ) builtin_user_functions: |- (?xi: (?: current | session | system )_user ) contexts: prototype: - include: comments main: - include: sql sql: - include: statements - include: statement-terminators - include: expressions-or-column-names statements: - include: create-statements - include: drop-statements - include: alter-statements - include: dml-statements - include: grant-statements - include: revoke-statements - include: explain-statements - include: analyze-statements - include: other-statements ###[ COMMENTS ]################################################################ comments: - include: double-dash-comments - include: block-comments double-dash-comments: - match: "--" scope: punctuation.definition.comment.sql push: inside-double-dash-comment block-comments: - match: /\*(?:\*(?!/))+ scope: punctuation.definition.comment.begin.sql push: inside-comment-docblock - match: /\* scope: punctuation.definition.comment.begin.sql push: inside-comment-block inside-double-dash-comment: - meta_include_prototype: false - meta_scope: comment.line.double-dash.sql - match: \n pop: 1 inside-comment-docblock: - meta_include_prototype: false - meta_scope: comment.block.documentation.sql - match: \*+/ scope: punctuation.definition.comment.end.sql pop: 1 - match: ^\s*(\*)(?!\**/) captures: 1: punctuation.definition.comment.sql inside-comment-block: - meta_include_prototype: false - meta_scope: comment.block.sql - match: \*+/ scope: punctuation.definition.comment.end.sql pop: 1 ###[ DDL CREATE STATEMENTS ]################################################### create-statements: - match: \b(?i:create)\b scope: keyword.other.ddl.sql push: - create-meta - create-target create-meta: - meta_include_prototype: false - meta_scope: meta.statement.create.sql - include: immediately-pop create-target: - include: create-function - include: create-index - include: create-table - include: create-other - include: else-pop create-function: - match: \b(?:({{ddl_target_function_modifier}})\s+)?({{ddl_target_function}})\b captures: 1: keyword.other.ddl.sql 2: keyword.other.ddl.sql set: - expect-function-characteristics - expect-function-parameters - expect-function-creation-name - create-function-condition create-function-condition: - meta_include_prototype: false - include: maybe-condition create-index: - match: \b(?i:(?:({{ddl_target_index_modifier}})\s+)?(index))\b captures: 1: keyword.other.ddl.sql 2: keyword.other.ddl.sql set: - create-index-args - expect-index-creation-name - create-index-condition create-index-condition: - meta_include_prototype: false - include: maybe-condition create-index-args: - meta_scope: meta.index.sql - include: on-table-names - include: create-common-args create-table: - match: \b(?i:(?:({{ddl_target_table_modifier}})\s+)?(table))\b captures: 1: keyword.other.ddl.sql 2: keyword.other.ddl.sql set: - create-table-args - maybe-column-declaration-list - expect-table-creation-name - create-table-condition create-table-condition: - meta_include_prototype: false - include: maybe-condition create-table-args: - meta_scope: meta.table.sql - include: create-common-args create-other: - match: \b{{ddl_target_other}}\b scope: keyword.other.ddl.sql set: - create-other-args - expect-other-creation-name - create-other-condition create-other-condition: - meta_include_prototype: false - include: maybe-condition create-other-args: - match: \b(?i:(as)\s+(table))\b captures: 1: keyword.context.block.sql 2: support.type.sql set: maybe-column-declaration-list - match: \b(?i:as)\b scope: keyword.context.block.sql pop: 1 - include: grant - include: on-table-names - include: create-common-args create-common-args: - include: expressions - include: pop-on-top-level-reserved-word ###[ DDL DROP STATEMENTS ]##################################################### drop-statements: - match: \b(?i:drop)\b scope: keyword.other.ddl.sql push: - drop-meta - drop-target drop-meta: - meta_include_prototype: false - meta_scope: meta.statement.drop.sql - include: immediately-pop drop-target: - include: drop-function - include: drop-index - include: drop-table - include: drop-other - include: else-pop drop-function: - match: \b{{ddl_target_function}}\b scope: keyword.other.ddl.sql set: - drop-function-args - expect-function-name - drop-function-condition drop-function-condition: - meta_include_prototype: false - include: maybe-condition drop-function-args: - meta_include_prototype: false - meta_scope: meta.function.sql - include: immediately-pop drop-index: - match: \b(?i:index)\b scope: keyword.other.ddl.sql set: - drop-index-args - expect-index-name - drop-index-condition drop-index-condition: - meta_include_prototype: false - include: maybe-condition drop-index-args: - meta_scope: meta.index.sql - include: maybe-on-table-name drop-table: - match: \b(?i:(?:({{ddl_target_table_modifier}})\s+)?(table))\b captures: 1: keyword.other.ddl.sql 2: keyword.other.ddl.sql set: - drop-table-args - expect-table-name - drop-table-condition drop-table-condition: - meta_include_prototype: false - include: maybe-condition drop-table-args: - meta_include_prototype: false - meta_scope: meta.table.sql - include: immediately-pop drop-other: - match: \b(?i:user)\b scope: storage.type.sql set: - expect-user-name - maybe-condition - match: \b{{ddl_target_other}}\b scope: keyword.other.ddl.sql set: - drop-other-args - expect-other-name - drop-other-condition drop-other-condition: - meta_include_prototype: false - include: maybe-condition drop-other-args: - include: maybe-on-table-name ###[ DDL ALTER STATEMENTS ]#################################################### alter-statements: - match: \b(?i:alter)\b scope: keyword.other.ddl.sql push: - alter-meta - alter-target alter-meta: - meta_include_prototype: false - meta_scope: meta.statement.alter.sql - include: immediately-pop alter-target: - include: alter-function - include: alter-index - include: alter-table - include: alter-other - include: else-pop ###[ DDL ALTER FUNCTION STATEMENTS ]########################################### alter-function: - match: \b(?:({{ddl_target_function_modifier}})\s+)?({{ddl_target_function}})\b captures: 1: keyword.other.ddl.sql 2: keyword.other.ddl.sql set: - expect-function-characteristics - expect-function-parameters - expect-function-name - alter-function-condition alter-function-condition: - meta_include_prototype: false - include: maybe-condition ###[ DDL ALTER INDEX STATEMENTS ]############################################## alter-index: - match: \b(?i:(?:({{ddl_target_index_modifier}})\s+)?(index))\b captures: 1: keyword.other.ddl.sql 2: keyword.other.ddl.sql set: - alter-index-args - expect-index-name - alter-index-condition alter-index-condition: - meta_include_prototype: false - include: maybe-condition alter-index-args: - include: alter-common - include: pop-on-top-level-reserved-word - include: expressions-or-column-names ###[ DDL ALTER TABLE STATEMENTS ]############################################## alter-table: - match: \b(?i:(?:({{ddl_target_table_modifier}})\s+)?(table))\b captures: 1: keyword.other.ddl.sql 2: keyword.other.ddl.sql set: - alter-table-args - expect-table-name - alter-table-condition alter-table-condition: - meta_include_prototype: false - include: maybe-condition alter-table-args: - include: alter-columns - include: alter-common - include: pop-on-top-level-reserved-word - include: expressions-or-column-names alter-other: - match: \b{{ddl_target_other}}\b scope: keyword.other.ddl.sql set: - alter-other-args - expect-other-name - alter-other-condition alter-other-condition: - meta_include_prototype: false - include: maybe-condition alter-other-args: - include: alter-common - include: else-pop alter-columns: - match: \b(?i:(?:(add|alter)\b(?:\s+(column)\b|(?!\s+(?:table|{{ddl_target_inside_alter_table}})\b)))) captures: 1: keyword.other.ddl.sql 2: keyword.other.ddl.sql push: - expect-type - expect-column-name-declaration - match: \b(?i:(drop)(?:\s+(column)\b|(?!\s+{{ddl_target}}\b))) scope: keyword.other.ddl.sql push: expect-column-name alter-common: - match: \b(?i:(add)\s+(constraint))\b captures: 1: keyword.other.ddl.sql 2: keyword.other.ddl.sql push: - maybe-column-modifier - expect-constraint-name - match: \b(?i:(add)\s+(?:({{ddl_target_index_modifier}})\s+)?(index))\b captures: 1: keyword.other.ddl.sql 2: keyword.other.ddl.sql 3: keyword.other.ddl.sql - match: \b(?i:(add)\s+(primary|foreign)\s+(key))\b captures: 1: keyword.other.ddl.sql 2: keyword.other.ddl.sql 3: keyword.other.ddl.sql ###[ DDL STATEMENT PROTOTYPES ]################################################ maybe-on-table-name: - include: on-table-names - include: else-pop on-table-names: - match: \b(?i:on)\b scope: keyword.other.sql push: expect-table-name ###[ DML STATEMENTS ]########################################################## dml-statements: - match: \b(?i:select)\b scope: keyword.other.dml.sql - match: \b(?i:union(?:\s+all)?)\b scope: keyword.other.dml.sql - match: \b(?i:(?:delete(?:\s+from)?))\b scope: keyword.other.dml.sql push: dml-delete - match: \b(?i:update)\b scope: keyword.other.dml.sql push: dml-update - match: \b(?i:(?:insert\s+into|truncate))\b scope: keyword.other.dml.sql push: expect-table-name - include: set-statements # expressions - match: \b(?i:(?:default\s+)?values)\b scope: keyword.other.dml.II.sql - include: distinct - include: join-expressions - match: \b(?i:group\s+by|order\s+by|having|where)\b scope: keyword.other.dml.sql - match: \b(?i:from)\b scope: keyword.other.dml.sql push: table-name-or-subquery - match: \b(?i:asc|desc)\b scope: keyword.other.order.sql dml-delete: - include: expect-table-name dml-update: - match: (?={{simple_identifier}}\s*=) pop: 1 - include: expect-table-name distinct: - match: \b(?i:distinct)\b scope: keyword.other.dml.sql join-expressions: - match: \b(?i:(?:(?:cross|inner|(?:full|left|right)(?:\s+outer)?)\s+)?join)\b scope: keyword.other.dml.sql push: - join-condition - table-name-or-subquery join-condition: - match: \b(?i:on)\b scope: keyword.control.conditional.sql set: conditional-expression - include: else-pop conditional-expression: - match: (?=[,;)}]|\b(?:{{toplevel_reserved}}|{{additional_toplevel_reserved}})\b) pop: 1 - include: expressions - include: expect-column-names ###[ DML SET STATEMENTS ]###################################################### set-statements: - match: \b(?i:set)\b(?!\s*\() scope: keyword.other.dml.sql push: - set-meta - set-target set-meta: - meta_include_prototype: false - meta_scope: meta.statement.set.sql - include: immediately-pop set-target: - include: else-pop ###[ GRANT STATEMENTS ]######################################################## grant-statements: - match: \b(?i:grant(?:\s+with\s+grant\s+option)?)\b scope: keyword.other.authorization.sql push: - grant-meta - grant grant-meta: - meta_include_prototype: false - meta_scope: meta.statement.grant.sql - include: immediately-pop ###[ REVOKE STATEMENTS ]####################################################### revoke-statements: - match: \b(?i:revoke)\b scope: keyword.other.ddl.sql push: - revoke-meta - grant revoke-meta: - meta_include_prototype: false - meta_scope: meta.statement.revoke.sql - include: immediately-pop ###[ EXPLAIN STATEMENTS ]###################################################### explain-statements: - match: \b(?i:explain)\b scope: keyword.other.ddl.sql ###[ ANALYZE STATEMENTS ]###################################################### analyze-statements: - match: \b(?i:analyze)\b scope: keyword.other.ddl.sql set: expect-table-name ###[ OTHER STATEMENTS ]######################################################## other-statements: [] ###[ EXPRESSIONS ]############################################################# expressions-or-column-names: - include: wildcard-identifiers - include: expressions - include: expect-column-names expressions: - include: groups - include: comma-separators - include: operators - include: column-alias-expressions - include: case-expressions - include: collate-expressions - include: constraint-expressions - include: literals-and-variables - include: function-calls - include: illegal-stray-brackets - include: illegal-stray-parens - match: (?=;) pop: 1 column-alias-expressions: - match: \b(?i:as)\b scope: keyword.operator.assignment.alias.sql push: expect-column-alias-name table-alias-expression: - match: \b(?i:as)\b scope: keyword.operator.assignment.alias.sql set: expect-table-alias-name case-expressions: - match: \b(?i:case)\b scope: keyword.control.conditional.case.sql push: inside-case-expression inside-case-expression: - meta_scope: meta.statement.conditional.case.sql - match: \b(?i:end)\b scope: keyword.control.conditional.end.sql pop: 1 - match: \b(?i:(case)\s+(when))\b captures: 1: keyword.control.conditional.case.sql 2: keyword.control.conditional.when.sql - match: \b(?i:when)\b scope: keyword.control.conditional.when.sql - match: \b(?i:then)\b scope: keyword.control.conditional.then.sql - match: \b(?i:else)\b scope: keyword.control.conditional.else.sql - include: expressions-or-column-names collate-expressions: - match: \b(?i:collate)\b scope: keyword.other.sql push: inside-collate-expression inside-collate-expression: - match: "{{simple_identifier}}" scope: support.constant.sql pop: 1 - include: else-pop constraint-expressions: - match: \b(?i:constraint)\b scope: storage.modifier.sql push: expect-constraint-name maybe-check: - match: \b(?i:check)\b scope: keyword.other.sql set: maybe-group - include: else-pop maybe-column: - match: \b(?i:column)\b scope: keyword.other.ddl.sql pop: 1 - include: else-pop maybe-to: - match: \b(?i:to)\b scope: keyword.other.ddl.sql pop: 1 - include: else-pop ###[ FUNCTION EXPRESSIONS ]#################################################### function-calls: - include: built-in-aggregate-function-calls - include: built-in-scalar-function-calls - include: built-in-user-function-calls - include: user-defined-function-calls built-in-aggregate-function-calls: # List of SQL99 built-in functions from http://www.oreilly.com/catalog/sqlnut/chapter/ch04.html - match: \b(?i:AVG|COUNT|MIN|MAX|SUM)(?=\s*\() scope: support.function.aggregate.sql push: function-call-arguments built-in-scalar-function-calls: # List of SQL99 built-in functions from http://www.oreilly.com/catalog/sqlnut/chapter/ch04.html - match: \b{{builtin_scalar_functions}}\b scope: support.function.scalar.sql push: function-call-arguments built-in-user-function-calls: - match: \b{{builtin_user_functions}}\b scope: support.function.user.sql push: function-call-arguments built-in-user-function-call: - match: \b{{builtin_user_functions}}\b scope: support.function.user.sql set: function-call-arguments user-defined-function-calls: - match: \b{{simple_identifier}}(?=\s*\() scope: support.function.sql push: function-call-arguments function-call-arguments: - meta_include_prototype: false - meta_scope: meta.function-call.sql - match: \( scope: meta.group.sql punctuation.section.arguments.begin.sql set: inside-function-call-arguments - include: else-pop inside-function-call-arguments: - meta_content_scope: meta.function-call.sql meta.group.sql - match: \) scope: meta.function-call.sql meta.group.sql punctuation.section.arguments.end.sql pop: 1 - match: "," scope: punctuation.separator.arguments.sql - include: distinct - include: expressions-or-column-names ###[ GROUPS EXPRESSIONS ]###################################################### maybe-group: - include: group - include: else-pop group: - match: \( scope: punctuation.section.group.begin.sql set: inside-group groups: - match: \( scope: punctuation.section.group.begin.sql push: inside-group inside-group: - meta_scope: meta.group.sql - match: \) scope: punctuation.section.group.end.sql pop: 1 - include: sql ###[ COLUMN EXPRESSIONS ]###################################################### expect-column-declaration: - include: column-declaration-list - match: (?=\S) set: - maybe-column-modifier - after-type - expect-type - column-name-declaration - single-identifier maybe-column-declaration-list: - include: column-declaration-list - include: else-pop column-declaration-list: - match: \( scope: punctuation.section.group.begin.sql set: inside-column-declaration-list inside-column-declaration-list: - meta_scope: meta.group.table-columns.sql - match: \) scope: punctuation.section.group.end.sql pop: 1 - include: column-modifiers - include: expressions - include: expect-column-declarations expect-column-declarations: - match: (?=\S) push: - maybe-column-modifier - after-type - expect-type - column-name-declaration - single-identifier maybe-column-modifier: - include: column-modifiers - include: else-pop column-modifiers: - match: \b(?i:check)\b scope: keyword.other.sql - match: |- \b(?xi: (?: (?: fulltext | primary | unique ) \s+ )? key | on \s+ (?: delete | update ) (?: \s+ cascade )? | default )\b scope: storage.modifier.sql - match: |- \b(?xi: foreign\s+key )\b scope: storage.modifier.sql push: column-name-list - match: \b(?i:unique)\b scope: storage.modifier.sql push: maybe-column-name-list - match: \b(?i:references)\b scope: storage.modifier.sql push: - maybe-column-name-list - expect-table-name maybe-column-name-list: - include: column-name-list - include: else-pop column-name-list: - match: \( scope: punctuation.section.group.begin.sql set: inside-column-name-list column-name-lists: - match: \( scope: punctuation.section.group.begin.sql push: inside-column-name-list inside-column-name-list: - meta_scope: meta.group.table-columns.sql - match: \) scope: punctuation.section.group.end.sql pop: 1 - include: expressions-or-column-names ###[ FUNCTION EXPRESSIONS ]#################################################### expect-function-parameters: - match: \( scope: punctuation.section.group.begin.sql set: inside-function-parameters - include: else-pop inside-function-parameters: - clear_scopes: 1 - meta_scope: meta.function.parameters.sql meta.group.sql - match: \) scope: punctuation.section.group.end.sql pop: 1 - include: comma-separators - match: (?=\S) push: - expect-type - expect-parameter-name - maybe-parameter-modifier expect-parameter-name: - match: "{{simple_identifier}}" scope: variable.parameter.sql pop: 1 - include: else-pop maybe-parameter-modifier: - match: \b{{function_parameter_modifier}}\b scope: storage.modifier.sql pop: 1 - include: else-pop expect-function-characteristics: - meta_scope: meta.function.sql - match: \b(?i:as|return)\b scope: keyword.context.block.sql pop: 1 - match: \b(?i:language)\b scope: storage.modifier.sql push: expect-function-language-name - match: \b(?i:returns)\b scope: keyword.other.ddl.sql push: expect-type - include: create-common-args expect-function-language-name: - match: "{{simple_identifier}}" scope: constant.other.language.sql pop: 1 - include: else-pop ###[ USER MANAGEMENT EXPRESSIONS ]############################################# grant: - match: \b(?i:to)\b scope: keyword.context.sql push: expect-user-name - include: comma-separators - include: user-privileges - include: pop-on-top-level-reserved-word user-privileges: - include: column-name-lists - match: \b(?i:all(?:\s+privileges)?)\b scope: constant.language.sql - match: \b(?i:(?:alter|create|drop|grant|revoke)\s+{{ddl_target}})\b scope: constant.language.sql - match: \b(?i:select|insert|update|delete|truncate|execute)\b scope: constant.language.sql ###[ TABLE NAMES OR SUBQUERIES ]############################################### table-name-or-subquery: - meta_include_prototype: false - include: table-subquery - include: table-name-or-function-call table-subquery: - match: (?=\() set: - maybe-table-alias - group table-name-or-function-call: - match: (?=\S) pop: 1 # pop `table-name-or-subquery` before evaluating branches branch_point: table-name-or-function-call branch: - table-name-not-function-call - table-valued-function-call table-name-not-function-call: - meta_include_prototype: false - match: "" set: - maybe-table-alias - table-name-fail-if-function-call - table-name - single-identifier table-name-fail-if-function-call: - meta_include_prototype: false - match: (?=\() fail: table-name-or-function-call - match: (?=\S) pop: 1 table-valued-function-call: - meta_include_prototype: false - match: "" set: - maybe-table-alias - function-call-arguments - table-valued-function-name - single-identifier table-valued-function-name: - meta_include_prototype: false - meta_content_scope: meta.table-valued-function-name.sql - include: immediately-pop maybe-table-alias: - include: pop-on-top-level-reserved-word - include: table-timespecs - include: table-alias-expression - include: expect-table-alias-name table-timespecs: - match: \b(?i:for\s+system_time)\b scope: keyword.other.dml.sql push: table-timespec-args table-timespec-args: - match: \b(?i:as\s+of|between|and|from|to)\b scope: keyword.operator.logical.sql push: - table-timespec-expression - table-timespec-type - match: \b(?i:all)\b scope: constant.other.sql pop: 1 - include: else-pop table-timespec-type: - match: \b(?i:timestamp|transaction)\b scope: storage.type.sql set: else-pop - include: else-pop table-timespec-expression: - include: expressions - include: immediately-pop ###[ TYPES ]################################################################### expect-type: - meta_include_prototype: false - include: comments - include: built-in-type - include: expect-user-type built-in-types: - match: \b(?i:enum)\b scope: storage.type.sql push: - after-type - maybe-group - match: |- (?x) \b(?: {{simple_types}} | {{types_with_optional_number}} ) (?: ((\()(\d+)(?:\s*(,)\s*(\d+))?(\)) | \b(?!\s*\() ) ) scope: storage.type.sql captures: 1: meta.parens.sql 2: punctuation.definition.parens.begin.sql 3: meta.number.integer.decimal.sql constant.numeric.value.sql 4: punctuation.separator.sequence.sql 5: meta.number.integer.decimal.sql constant.numeric.value.sql 6: punctuation.definition.parens.end.sql push: after-type - match: \b{{type_modifiers}}\b scope: storage.modifier.sql built-in-type: - match: \b(?i:enum)\b scope: storage.type.sql set: - after-type - maybe-group - match: |- (?x) \b(?: {{simple_types}} | {{types_with_optional_number}} ) (?: ((\()(\d+)(?:\s*(,)\s*(\d+))?(\)) | \b(?!\s*\() ) ) scope: storage.type.sql captures: 1: meta.parens.sql 2: punctuation.definition.parens.begin.sql 3: meta.number.integer.decimal.sql constant.numeric.value.sql 4: punctuation.separator.sequence.sql 5: meta.number.integer.decimal.sql constant.numeric.value.sql 6: punctuation.definition.parens.end.sql set: after-type expect-user-type: - match: (?=\S) set: [maybe-group, after-type, inside-user-type] inside-user-type: # note: may contain foreign variable interpolation - meta_scope: support.type.sql - match: "{{simple_identifier_break}}" pop: 1 after-type: - match: \b{{type_modifiers}}\b scope: storage.modifier.sql pop: 1 - include: assignment-operators - include: else-pop ###[ IDENTIFIERS ]############################################################# expect-table-alias-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [table-alias-name, single-identifier] expect-column-alias-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [column-alias-name, single-identifier] table-alias-name: - meta_include_prototype: false - meta_content_scope: meta.alias.table.sql - include: immediately-pop column-alias-name: - meta_include_prototype: false - meta_content_scope: meta.alias.column.sql - include: immediately-pop expect-column-names: - match: (?=\S) push: [maybe-operator, column-name, single-identifier] expect-column-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [column-name, single-identifier] column-name: - meta_include_prototype: false - meta_content_scope: meta.column-name.sql - include: immediately-pop expect-column-name-declaration: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [column-name-declaration, single-identifier] column-name-declaration: - meta_include_prototype: false - meta_content_scope: meta.column-name.sql variable.other.member.declaration.sql - include: immediately-pop expect-constraint-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - match: (?=(?i:check|foreign|primary|unique|index|key|using|with)\b) pop: 1 - include: comments - match: (?=\S) set: [constraint-name, single-identifier] constraint-name: - meta_include_prototype: false - meta_content_scope: meta.constraint-name.sql - include: immediately-pop expect-database-creation-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [database-creation-name, single-identifier] database-creation-name: - meta_include_prototype: false - meta_content_scope: entity.name.struct.database.sql - include: immediately-pop expect-database-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [database-name, single-identifier] database-name: - meta_include_prototype: false - meta_content_scope: meta.database-name.sql - include: immediately-pop expect-event-creation-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [event-creation-name, single-identifier] event-creation-name: - meta_include_prototype: false - meta_scope: entity.name.event.sql - include: immediately-pop expect-event-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [event-name, single-identifier] event-name: - meta_include_prototype: false - meta_scope: meta.event-name.sql - include: immediately-pop expect-index-creation-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [index-creation-name, single-identifier] index-creation-name: - meta_include_prototype: false - meta_scope: entity.name.struct.index.sql - include: immediately-pop expect-index-names: - match: (?=\S) push: [index-name, single-identifier] expect-index-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [index-name, single-identifier] index-name: - meta_include_prototype: false - meta_scope: meta.index-name.sql - include: immediately-pop expect-function-creation-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [procedure-creation-name, single-identifier] expect-partition-creation-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [partition-creation-name, single-identifier] partition-creation-name: - meta_include_prototype: false - meta_scope: entity.name.struct.partition.sql - include: immediately-pop expect-partition-names: - match: (?=\S) push: [partition-name, single-identifier] expect-partition-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [partition-name, single-identifier] partition-name: - meta_include_prototype: false - meta_scope: meta.partition-name.sql - include: immediately-pop procedure-creation-name: - meta_include_prototype: false - meta_content_scope: entity.name.function.sql - include: immediately-pop expect-function-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [procedure-name, single-identifier] procedure-name: - meta_include_prototype: false - meta_content_scope: meta.procedure-name.sql - include: immediately-pop expect-table-creation-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [table-creation-name, single-identifier] table-creation-name: - meta_include_prototype: false - meta_scope: entity.name.struct.table.sql - include: immediately-pop expect-table-names: - match: (?=\S) push: [table-name, single-identifier] expect-table-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [table-name, single-identifier] table-name: - meta_include_prototype: false - meta_content_scope: meta.table-name.sql - include: immediately-pop expect-user-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - include: built-in-user-function-call - match: (?=\S) set: [user-name, single-identifier] user-name: - meta_include_prototype: false - meta_content_scope: meta.username.sql - include: immediately-pop expect-user-name-declaration: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [user-name-declaration, single-identifier] user-name-declaration: - meta_include_prototype: false - meta_content_scope: entity.name.user.sql - include: immediately-pop expect-type-creation-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [type-creation-name, single-identifier] type-creation-name: - meta_include_prototype: false - meta_scope: entity.name.type.cql - include: immediately-pop expect-other-creation-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [other-creation-name, single-identifier] other-creation-name: - meta_include_prototype: false - meta_scope: entity.name.struct.other.sql - include: immediately-pop expect-other-name: # prevent prototypes from inheriting syntaxes - meta_include_prototype: false - include: comments - match: (?=\S) set: [other-name, single-identifier] other-name: - meta_include_prototype: false - meta_scope: meta.other-name.sql - include: immediately-pop single-identifier: - meta_include_prototype: false - include: pop-on-top-level-reserved-word - match: "" set: - maybe-identifier-accessor - identifier-part maybe-identifier-accessor: - meta_include_prototype: false - match: \s*(\.)\s*(\*) captures: 1: punctuation.accessor.dot.sql 2: constant.other.wildcard.asterisk.sql pop: 1 - match: \s*(\.) captures: 1: punctuation.accessor.dot.sql set: single-identifier - include: immediately-pop identifier-part: - meta_include_prototype: false - include: backtick-quoted-identifier-part - include: double-quoted-identifier-part - include: single-quoted-identifier-part - include: simple-identifier-part backtick-quoted-identifier-part: - match: \` scope: punctuation.definition.identifier.begin.sql set: inside-backtick-quoted-identifier-part inside-backtick-quoted-identifier-part: # note: may contain foreign variable interpolation - match: \` scope: punctuation.definition.identifier.end.sql pop: 1 double-quoted-identifier-part: - match: \" scope: punctuation.definition.identifier.begin.sql set: inside-double-quoted-identifier-part inside-double-quoted-identifier-part: # note: may contain foreign variable interpolation - match: \" scope: punctuation.definition.identifier.end.sql pop: 1 single-quoted-identifier-part: - match: \' scope: punctuation.definition.identifier.begin.sql set: inside-single-quoted-identifier-part inside-single-quoted-identifier-part: # note: may contain foreign variable interpolation - match: \' scope: punctuation.definition.identifier.end.sql pop: 1 simple-identifier-part: - match: (?=\S) set: inside-simple-identifier-part inside-simple-identifier-part: # note: may contain foreign variable interpolation - match: "{{simple_identifier_break}}" pop: 1 wildcard-identifiers: - match: \* scope: constant.other.wildcard.asterisk.sql ###[ LITERALS ]################################################################ literals-and-variables: - include: built-in-types - include: constants - include: numbers - include: strings constants: - match: \b(?i:null)\b scope: constant.language.null.sql booleans: - match: \b(?i:true)\b scope: constant.language.boolean.true.sql - match: \b(?i:false)\b scope: constant.language.boolean.false.sql numbers: - match: \b\d+(\.)\d+\b scope: meta.number.float.decimal.sql constant.numeric.value.sql captures: 1: punctuation.separator.decimal.sql - match: \b\d+\b scope: meta.number.integer.decimal.sql constant.numeric.value.sql strings: - match: \' scope: punctuation.definition.string.begin.sql push: inside-single-quoted-string inside-single-quoted-string: - meta_include_prototype: false - meta_scope: meta.string.sql string.quoted.single.sql - match: \'\' scope: constant.character.escape.sql - match: \' scope: punctuation.definition.string.end.sql pop: 1 - include: string-escapes string-escapes: - match: "{{string_escape}}" scope: constant.character.escape.sql ###[ LIKE EXPRESSIONS ]######################################################## like-expressions: - match: \b(?i:like)\b scope: keyword.operator.logical.sql branch_point: like-expressions branch: - like-string-not-followed-by-escape - like-string-followed-by-escape-slash - like-string-followed-by-escape-caret - like-string-followed-by-escape-hash - like-string-followed-by-unknown-escape like-string-not-followed-by-escape: - meta_include_prototype: false - match: \' scope: punctuation.definition.string.begin.sql set: - like-escape-fail - inside-like-single-quoted-string - include: else-pop like-string-followed-by-escape-slash: - meta_include_prototype: false - match: \' scope: punctuation.definition.string.begin.sql set: - like-escape-character-slash - like-escape - inside-like-single-quoted-string-slash-escape - include: else-pop like-string-followed-by-escape-caret: - meta_include_prototype: false - match: \' scope: punctuation.definition.string.begin.sql set: - like-escape-character-caret - like-escape - inside-like-single-quoted-string-caret-escape - include: else-pop like-string-followed-by-escape-hash: - meta_include_prototype: false - match: \' scope: punctuation.definition.string.begin.sql set: - like-escape-character-hash - like-escape - inside-like-single-quoted-string-hash-escape - include: else-pop like-string-followed-by-unknown-escape: - meta_include_prototype: false - match: \' scope: punctuation.definition.string.begin.sql set: - like-escape-character-any - like-escape - inside-like-single-quoted-string - include: else-pop inside-like-single-quoted-string-slash-escape: - meta_include_prototype: false - meta_scope: meta.string.like.sql string.quoted.single.sql - match: \\. scope: constant.character.escape.sql - include: inside-like-single-quoted-string inside-like-single-quoted-string-caret-escape: - meta_include_prototype: false - meta_scope: meta.string.like.sql string.quoted.single.sql - match: \^. scope: constant.character.escape.sql - include: inside-like-single-quoted-string inside-like-single-quoted-string-hash-escape: - meta_include_prototype: false - meta_scope: meta.string.like.sql string.quoted.single.sql - match: "#." scope: constant.character.escape.sql - include: inside-like-single-quoted-string inside-like-single-quoted-string: - meta_include_prototype: false - meta_scope: meta.string.like.sql string.quoted.single.sql - match: \' scope: punctuation.definition.string.end.sql pop: 1 - match: "%" scope: constant.other.wildcard.percent.sql - match: "_" scope: constant.other.wildcard.underscore.sql like-else-fail: - match: (?=\S) fail: like-expressions like-escape-fail: - match: \b(?i:escape)\b fail: like-expressions - include: else-pop like-escape: - match: \b(?i:escape)\b scope: keyword.operator.word.sql pop: 1 - include: else-pop like-escape-character-any: - meta_include_prototype: false - match: (\')([^'])(\') scope: meta.string.escape.sql string.quoted.single.sql captures: 1: punctuation.definition.string.begin.sql 2: constant.character.escape.sql 3: punctuation.definition.string.end.sql pop: 1 - include: else-pop like-escape-character-caret: - meta_include_prototype: false - match: (\')(\^)(\') scope: meta.string.escape.sql string.quoted.single.sql captures: 1: punctuation.definition.string.begin.sql 2: constant.character.escape.sql 3: punctuation.definition.string.end.sql pop: 1 - include: like-else-fail like-escape-character-slash: - meta_include_prototype: false - match: (\')(\\)(\') scope: meta.string.escape.sql string.quoted.single.sql captures: 1: punctuation.definition.string.begin.sql 2: constant.character.escape.sql 3: punctuation.definition.string.end.sql pop: 1 - include: like-else-fail like-escape-character-hash: - meta_include_prototype: false - match: (\')(#)(\') scope: meta.string.escape.sql string.quoted.single.sql captures: 1: punctuation.definition.string.begin.sql 2: constant.character.escape.sql 3: punctuation.definition.string.end.sql pop: 1 - include: like-else-fail ###[ OPERATORS ]############################################################### maybe-condition: - meta_include_prototype: false - include: conditions - include: else-pop conditions: - match: \b(?i:if)\b scope: keyword.control.conditional.if.sql - include: logical-operators maybe-operator: - meta_include_prototype: false - match: "<=>|[!<>]?=|<>|<|>" scope: keyword.operator.comparison.sql pop: 1 - match: "[-+/*]" scope: keyword.operator.arithmetic.sql pop: 1 - match: \b(?i:and|or|having|exists|between|in|not|is)\b scope: keyword.operator.logical.sql pop: 1 - include: assignment-operators - include: else-pop operators: - match: "<=>|[!<>]?=|<>|<|>" scope: keyword.operator.comparison.sql - match: "[-+/*]" scope: keyword.operator.arithmetic.sql - include: logical-operators assignment-operators: - match: "=" scope: keyword.operator.assignment.sql logical-operators: - match: \b(?i:and|or|having|exists|between|in|not|is)\b scope: keyword.operator.logical.sql comma-separators: - match: "," scope: punctuation.separator.sequence.sql statement-terminators: - match: ";" scope: punctuation.terminator.statement.sql ###[ ILLEGALS ]################################################################ illegal-stray-brackets: - match: \] scope: invalid.illegal.stray.sql illegal-stray-parens: - match: \) scope: invalid.illegal.stray.sql ###[ PROTOTYPES ]############################################################## else-pop: - match: (?=\S) pop: 1 immediately-pop: - match: "" pop: 1 pop-on-top-level-reserved-word: - match: (?=[;)}]|\b(?:{{toplevel_reserved}}|{{additional_toplevel_reserved}})\b) pop: 1 ================================================ FILE: cli/app.rs ================================================ use crate::{ commands::{ args::{EchoMode, HeadersMode, ParameterArgs, ParameterCommand, TimerMode}, import::ImportFile, Command, CommandParser, }, config::Config, helper::LimboHelper, input::{ get_io, get_writer, ApplyWriter, DbLocation, NoopProgress, OutputMode, ProgressSink, Settings, StderrProgress, }, manual, opcodes_dictionary::OPCODE_DESCRIPTIONS, read_state_machine::ReadState, HISTORY_FILE, }; use anyhow::{anyhow, Context}; use clap::Parser; use comfy_table::{Attribute, Cell, CellAlignment, ContentArrangement, Row, Table}; use rustyline::{error::ReadlineError, history::DefaultHistory, Editor}; use std::num::NonZeroUsize; use std::{ fs::File, io::{self, BufRead, BufReader, IsTerminal, Write}, mem::{forget, ManuallyDrop}, path::PathBuf, sync::{ atomic::{AtomicUsize, Ordering}, Arc, }, time::{Duration, Instant}, }; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use turso_core::{ io_error, Connection, Database, LimboError, Numeric, OpenFlags, QueryMode, Statement, Value, }; #[derive(Parser, Debug)] #[command(name = "Turso")] #[command(author, version, about, long_about = None)] pub struct Opts { #[clap(index = 1, help = "SQLite database file", default_value = ":memory:")] pub database: Option, #[clap(index = 2, help = "Optional SQL command to execute")] pub sql: Option, #[clap(short = 'm', long, default_value_t = OutputMode::Pretty)] pub output_mode: OutputMode, #[clap(short, long, default_value = "")] pub output: String, #[clap( short, long, help = "don't display program information on start", default_value_t = false )] pub quiet: bool, #[clap(short, long, help = "Print commands before execution")] pub echo: bool, #[clap( short = 'v', long, help = "Select VFS. options are io_uring (if feature enabled), experimental_win_iocp (if feature enabled on windows), memory, and syscall" )] pub vfs: Option, #[clap(long, help = "Open the database in read-only mode")] pub readonly: bool, #[clap(long, help = "Enable experimental views feature")] pub experimental_views: bool, #[clap( long, help = "Enable experimental custom types (CREATE TYPE / DROP TYPE)" )] pub experimental_custom_types: bool, #[clap(short = 't', long, help = "specify output file for log traces")] pub tracing_output: Option, #[clap(long, help = "Start MCP server instead of interactive shell")] pub mcp: bool, #[clap( long, help = "Start sync server instead of interactive shell and listen at given address (e.g. 0.0.0.0:8080)" )] pub sync_server: Option, #[clap(long, help = "Enable experimental encryption feature")] pub experimental_encryption: bool, #[clap(long, help = "Enable experimental index method feature")] pub experimental_index_method: bool, #[clap(long, help = "Enable experimental autovacuum feature")] pub experimental_autovacuum: bool, #[clap(long, help = "Enable experimental attach feature")] pub experimental_attach: bool, #[cfg(feature = "mvcc_repl")] #[clap(long, help = "Start MVCC concurrent transaction harness")] pub mvcc: bool, #[clap( long, help = "Enable unsafe testing features (e.g. sqlite_dbpage writes)" )] pub unsafe_testing: bool, } const PROMPT: &str = "turso> "; pub struct Limbo { pub prompt: String, io: Arc, writer: Option>, conn: Arc, pub interrupt_count: Arc, input_buff: ManuallyDrop, pub(crate) opts: Settings, db_opts: turso_core::DatabaseOpts, read_state: ReadState, pub rl: Option>, config: Option, had_query_error: bool, parameter_bindings: Vec, } #[derive(Clone)] struct ParameterBinding { name: Box, index: Option, value: Value, } struct QueryStatistics { io_time_elapsed_samples: Vec, execute_time_elapsed_samples: Vec, } /// A lending iterator over query result rows with optional statistics tracking. struct RowStepper<'a> { rows: &'a mut Statement, stats: Option>, } impl<'a> RowStepper<'a> { fn new(rows: &'a mut Statement, stats: Option<&'a mut QueryStatistics>) -> Self { Self { rows, stats: stats.map(std::cell::RefCell::new), } } /// Advances to the next row, returning it if available. /// Returns Ok(Some(row)) for each row, Ok(None) when done, or Err on failure. fn next_row(&mut self) -> Result, LimboError> { let execution_time = std::cell::Cell::new(Instant::now()); let io_time = std::cell::Cell::new(Instant::now()); let result = self.rows.run_one_step_blocking( || { // Push execution sample to not count IO time in execution if let Some(stats) = self.stats.as_ref() { stats .borrow_mut() .execute_time_elapsed_samples .push(execution_time.get().elapsed()); } // Start io timer io_time.set(Instant::now()); Ok(()) }, || { // Push sample when we end IO if let Some(stats) = self.stats.as_ref() { stats .borrow_mut() .io_time_elapsed_samples .push(io_time.get().elapsed()); } // Restart Execution timer execution_time.set(Instant::now()); Ok(()) }, ); match result { Ok(row_opt) => { if let Some(stats) = self.stats.as_ref() { stats .borrow_mut() .execute_time_elapsed_samples .push(execution_time.get().elapsed()); } Ok(row_opt) } Err(e) => { if let Some(stats) = self.stats.as_ref() { stats .borrow_mut() .execute_time_elapsed_samples .push(execution_time.get().elapsed()); } Err(e) } } } } /// metadata from db, fetched from pragmas struct DbMetadata { page_size: i64, page_count: i64, filename: String, } struct DbPage<'a> { pgno: i64, data: &'a [u8], } impl Limbo { pub fn new() -> anyhow::Result<(Self, WorkerGuard)> { let mut opts = Opts::parse(); let guard = Self::init_tracing(&opts)?; let db_file = opts .database .as_ref() .map_or(":memory:".to_string(), |p| p.to_string_lossy().to_string()); let db_opts = turso_core::DatabaseOpts::new() .with_views(opts.experimental_views) .with_custom_types(opts.experimental_custom_types) .with_encryption(opts.experimental_encryption) .with_index_method(opts.experimental_index_method) .with_autovacuum(opts.experimental_autovacuum) .with_attach(opts.experimental_attach) .with_unsafe_testing(opts.unsafe_testing); let db_file = normalize_db_path(db_file); let (io, conn) = if db_file.starts_with("file:") { Connection::from_uri(&db_file, db_opts)? } else { let flags = if opts.readonly { OpenFlags::default().union(OpenFlags::ReadOnly) } else { OpenFlags::default() }; let (io, db) = Database::open_new( &db_file, opts.vfs.as_ref(), flags, db_opts.turso_cli(), None, )?; let conn = db.connect()?; (io, conn) }; unsafe { let mut ext_api = conn._build_turso_ext(); if !limbo_completion::register_extension_static(&mut ext_api).is_ok() { return Err(anyhow!( "Failed to register completion extension".to_string() )); } conn._free_extension_ctx(ext_api); } let interrupt_count = Arc::new(AtomicUsize::new(0)); { let interrupt_count: Arc = Arc::clone(&interrupt_count); ctrlc::set_handler(move || { // Increment the interrupt count on Ctrl-C interrupt_count.fetch_add(1, Ordering::Release); }) .expect("Error setting Ctrl-C handler"); } let sql = opts.sql.take(); let has_sql = sql.is_some(); let quiet = opts.quiet || !IsTerminal::is_terminal(&std::io::stdin()); let config = Config::for_output_mode(opts.output_mode); let mut app = Self { prompt: PROMPT.to_string(), io, writer: Some(get_writer(&opts.output)), conn, interrupt_count, input_buff: ManuallyDrop::new(sql.unwrap_or_default()), read_state: ReadState::default(), opts: Settings::from(opts), db_opts, rl: None, config: Some(config), had_query_error: false, parameter_bindings: Vec::new(), }; app.first_run(has_sql, quiet)?; Ok((app, guard)) } pub fn with_config(mut self, config: Config) -> Self { self.config = Some(config); self } pub fn with_readline(mut self, mut rl: Editor) -> Self { let h = LimboHelper::new( self.conn.clone(), self.config.as_ref().map(|c| c.highlight.clone()), ); rl.set_helper(Some(h)); self.rl = Some(rl); self } fn first_run(&mut self, has_sql: bool, quiet: bool) -> Result<(), LimboError> { // Skip startup messages and SQL execution in MCP/SyncServer mode if self.is_mcp_mode() || self.is_sync_server_mode() { return Ok(()); } if has_sql { self.handle_first_input()?; } if !quiet { self.writeln_fmt(format_args!("Turso v{}", env!("CARGO_PKG_VERSION"))) .map_err(|e| io_error(e, "write"))?; self.writeln("Enter \".help\" for usage hints.") .map_err(|e| io_error(e, "write"))?; // Add random feature hint if let Some(hint) = manual::get_random_feature_hint() { self.writeln(&hint).map_err(|e| io_error(e, "write"))?; } self.writeln( "This software is in BETA, use caution with production data and ensure you have backups." ).map_err(|e| io_error(e, "write"))?; self.display_in_memory().map_err(|e| io_error(e, "write"))?; } Ok(()) } fn handle_first_input(&mut self) -> Result<(), LimboError> { self.consume(true); self.close_conn()?; std::process::exit(i32::from(self.had_query_error)); } fn set_multiline_prompt(&mut self) { self.prompt = match self.input_buff.chars().fold(0, |acc, c| match c { '(' => acc + 1, ')' => acc - 1, _ => acc, }) { n if n < 0 => String::from(")x!...>"), 0 => String::from(" ...> "), n if n < 10 => format!("(x{n}...> "), _ => String::from("(.....> "), }; } #[cfg(not(target_family = "wasm"))] fn handle_load_extension(&mut self, path: &str) -> Result<(), String> { let ext_path = turso_core::resolve_ext_path(path).map_err(|e| e.to_string())?; self.conn .load_extension(ext_path) .map_err(|e| e.to_string()) } fn display_in_memory(&mut self) -> io::Result<()> { if self.opts.db_file == ":memory:" { self.writeln("Connected to a transient in-memory database.")?; self.writeln("Use \".open FILENAME\" to reopen on a persistent database")?; } Ok(()) } fn show_info(&mut self) -> io::Result<()> { let opts = format!("{}", self.opts); self.writeln(opts) } fn display_stats(&mut self, args: crate::commands::args::StatsArgs) -> io::Result<()> { use crate::commands::args::StatsToggle; // Handle on/off toggle if let Some(toggle) = args.toggle { match toggle { StatsToggle::On => { self.opts.stats = true; self.writeln("Stats display enabled.")?; } StatsToggle::Off => { self.opts.stats = false; self.writeln("Stats display disabled.")?; } } return Ok(()); } // Display all metrics let output = { let metrics = self.conn.metrics.read(); format!("{metrics}") }; self.writeln(output)?; if args.reset { self.conn.metrics.write().reset(); self.writeln("Statistics reset.")?; } Ok(()) } pub fn reset_input(&mut self) { self.prompt = PROMPT.to_string(); self.input_buff.clear(); self.read_state = ReadState::default(); } pub fn close_conn(&mut self) -> Result<(), LimboError> { self.conn.close() } pub fn get_connection(&self) -> Arc { self.conn.clone() } pub fn is_mcp_mode(&self) -> bool { self.opts.mcp } pub fn is_sync_server_mode(&self) -> bool { self.opts.sync_server_address.is_some() } pub fn get_interrupt_count(&self) -> Arc { self.interrupt_count.clone() } pub fn has_query_error(&self) -> bool { self.had_query_error } fn toggle_echo(&mut self, arg: EchoMode) { match arg { EchoMode::On => self.opts.echo = true, EchoMode::Off => self.opts.echo = false, } } fn open_db(&mut self, path: &str, vfs_name: Option<&str>) -> anyhow::Result<()> { self.conn.close()?; let (io, db) = if let Some(vfs_name) = vfs_name { self.conn.open_new(path, vfs_name)? } else { let io = { match path { ":memory:" => get_io(DbLocation::Memory, &self.opts.io.to_string())?, _path => get_io(DbLocation::Path, &self.opts.io.to_string())?, } }; ( io.clone(), Database::open_file_with_flags( io.clone(), path, OpenFlags::default(), self.db_opts, None, )?, ) }; self.io = io; self.conn = db.connect()?; self.opts.db_file = path.to_string(); Ok(()) } fn set_output_file(&mut self, path: &str) -> Result<(), String> { if path.is_empty() || path.trim().eq_ignore_ascii_case("stdout") { self.set_output_stdout(); return Ok(()); } match std::fs::File::create(path) { Ok(file) => { self.writer = Some(Box::new(file)); self.opts.is_stdout = false; self.opts.output_mode = OutputMode::List; self.opts.output_filename = path.to_string(); Ok(()) } Err(e) => Err(e.to_string()), } } fn set_output_stdout(&mut self) { let _ = self.writer.as_mut().unwrap().flush(); self.writer = Some(Box::new(io::stdout())); self.opts.is_stdout = true; } fn set_mode(&mut self, mode: OutputMode) -> Result<(), String> { if mode == OutputMode::Pretty && !self.opts.is_stdout { Err("pretty output can only be written to a tty".to_string()) } else { self.opts.output_mode = mode; Ok(()) } } fn write_fmt(&mut self, fmt: std::fmt::Arguments) -> io::Result<()> { self.writer.as_mut().unwrap().write_fmt(fmt) } fn writeln_fmt(&mut self, fmt: std::fmt::Arguments) -> io::Result<()> { self.writer.as_mut().unwrap().write_fmt(fmt)?; self.writer.as_mut().unwrap().write_all(b"\n") } fn write>(&mut self, data: D) -> io::Result<()> { self.writer.as_mut().unwrap().write_all(data.as_ref()) } fn writeln>(&mut self, data: D) -> io::Result<()> { self.writer.as_mut().unwrap().write_all(data.as_ref())?; self.writer.as_mut().unwrap().write_all(b"\n") } fn run_query(&mut self, input: &str) { let echo = self.opts.echo; if echo { let _ = self.writeln(input); } let start = Instant::now(); let mut stats = if self.opts.timer { Some(QueryStatistics { io_time_elapsed_samples: vec![], execute_time_elapsed_samples: vec![], }) } else { None }; let conn = self.conn.clone(); let runner = conn.query_runner(input.as_bytes()); let had_error_before = self.had_query_error; let capture_stats = self.opts.stats; let mut last_stmt_metrics = None; for mut output in runner { if let Ok(Some(ref mut stmt)) = output { self.apply_parameter_bindings(stmt); } if self .print_query_result(input, &mut output, stats.as_mut()) .is_err() || self.had_query_error != had_error_before { self.had_query_error = true; break; } // Capture metrics after stepping, before the Statement is dropped if capture_stats { if let Ok(Some(ref stmt)) = output { last_stmt_metrics = Some(stmt.metrics().clone()); } } } self.print_query_performance_stats(start, stats.as_ref()); // Display stats if enabled if let Some(ref last) = last_stmt_metrics { let _ = self.writeln(format!("\n{last}")); } } fn apply_parameter_bindings(&self, stmt: &mut Statement) { for binding in &self.parameter_bindings { if let Some(index) = binding.index { if stmt.parameters().has_slot(index) { stmt.bind_at(index, binding.value.clone()); } continue; } if let Some(index) = stmt.parameter_index(&binding.name) { stmt.bind_at(index, binding.value.clone()); } } } fn handle_parameter_command(&mut self, args: ParameterArgs) -> Result<(), String> { match args.command { ParameterCommand::Set(args) => { validate_parameter_name(&args.name)?; let index = parameter_name_to_index(&args.name); let value = parse_parameter_value(&args.value)?; if let Some(existing) = self .parameter_bindings .iter_mut() .find(|binding| binding.name.as_ref() == args.name) { existing.index = index; existing.value = value; } else { self.parameter_bindings.push(ParameterBinding { name: args.name.into_boxed_str(), index, value, }); } Ok(()) } ParameterCommand::List => self.list_parameter_bindings(), ParameterCommand::Clear(args) => { if let Some(name) = args.name { validate_parameter_name(&name)?; self.parameter_bindings .retain(|binding| binding.name.as_ref() != name); } else { self.parameter_bindings.clear(); } Ok(()) } } } fn list_parameter_bindings(&mut self) -> Result<(), String> { if self.parameter_bindings.is_empty() { return Ok(()); } let writer = self .writer .as_mut() .ok_or_else(|| "writer is not initialized".to_string())?; for binding in &self.parameter_bindings { writer .write_fmt(format_args!("{} = {}\n", binding.name, binding.value)) .map_err(|e| e.to_string())?; } Ok(()) } fn print_query_performance_stats(&mut self, start: Instant, stats: Option<&QueryStatistics>) { let elapsed_as_str = |duration: Duration| { if duration.as_secs() >= 1 { format!("{} s", duration.as_secs_f64()) } else if duration.as_millis() >= 1 { format!("{} ms", duration.as_millis() as f64) } else if duration.as_micros() >= 1 { format!("{} us", duration.as_micros() as f64) } else { format!("{} ns", duration.as_nanos()) } }; let sample_stats_as_str = |name: &str, samples: &Vec| { if samples.is_empty() { return format!("{name}: No samples available"); } let avg_time_spent = samples.iter().sum::() / samples.len() as u32; let total_time = samples.iter().fold(Duration::ZERO, |acc, x| acc + *x); format!( "{}: avg={}, total={}", name, elapsed_as_str(avg_time_spent), elapsed_as_str(total_time), ) }; if self.opts.timer { if let Some(stats) = stats { let _ = self.writeln("Command stats:\n----------------------------"); let _ = self.writeln(format!( "total: {} (this includes parsing/coloring of cli app)\n", elapsed_as_str(start.elapsed()) )); let _ = self.writeln("query execution stats:\n----------------------------"); let _ = self.writeln(sample_stats_as_str( "Execution", &stats.execute_time_elapsed_samples, )); let _ = self.writeln(sample_stats_as_str("I/O", &stats.io_time_elapsed_samples)); } } } fn reset_line(&mut self) { // Entry is auto added to history // self.rl.add_history_entry(line.to_owned())?; self.interrupt_count.store(0, Ordering::Release); } // consume will consume `input_buff` pub fn consume(&mut self, flush: bool) { if self.input_buff.trim().is_empty() { return; } self.reset_line(); // we are taking ownership of input_buff here // its always safe because we split the string in two parts fn take_usable_part(app: &mut Limbo) -> (String, usize) { let ptr = app.input_buff.as_mut_ptr(); let (len, cap) = (app.input_buff.len(), app.input_buff.capacity()); app.input_buff = ManuallyDrop::new(unsafe { String::from_raw_parts(ptr.add(len), 0, cap - len) }); (unsafe { String::from_raw_parts(ptr, len, len) }, unsafe { ptr.add(len).addr() }) } fn concat_usable_part(app: &mut Limbo, mut part: String, old_address: usize) { let ptr = app.input_buff.as_mut_ptr(); let (len, cap) = (app.input_buff.len(), app.input_buff.capacity()); // if the address is not the same, meaning the string has been reallocated // so we just drop the part we took earlier if ptr.addr() != old_address || !app.input_buff.is_empty() { return; } let head_ptr = part.as_mut_ptr(); let (head_len, head_cap) = (part.len(), part.capacity()); forget(part); // move this part into `input_buff` app.input_buff = ManuallyDrop::new(unsafe { String::from_raw_parts(head_ptr, head_len + len, head_cap + cap) }); } let value = self.input_buff.trim(); let is_dot_command = value.starts_with('.'); let is_complete = self.read_state.is_complete(); match (is_dot_command, is_complete) { (true, _) => { let (owned_value, old_address) = take_usable_part(self); self.handle_dot_command(owned_value.trim().strip_prefix('.').unwrap()); concat_usable_part(self, owned_value, old_address); self.reset_input(); } (false, true) => { let (owned_value, old_address) = take_usable_part(self); self.run_query(owned_value.trim()); concat_usable_part(self, owned_value, old_address); self.reset_input(); } (false, false) if flush => { let (owned_value, old_address) = take_usable_part(self); self.run_query(owned_value.trim()); concat_usable_part(self, owned_value, old_address); self.reset_input(); } (false, false) => { self.set_multiline_prompt(); } } } pub fn handle_dot_command(&mut self, line: &str) { let first = line.split_whitespace().next(); let parse = match first { Some("parameter") | Some("param") => { let args = shlex::split(line).unwrap_or_else(|| { line.split_whitespace() .map(str::to_owned) .collect::>() }); if args.is_empty() { return; } CommandParser::try_parse_from(args) } _ => { let args = line.split_whitespace(); CommandParser::try_parse_from(args) } }; match parse { Err(err) => { // Let clap print with Styled Colors instead let _ = err.print(); } Ok(cmd) => match cmd.command { Command::Exit(args) => { self.save_history(); std::process::exit(args.code); } Command::Quit => { let _ = self.writeln("Exiting Turso SQL Shell."); let _ = self.close_conn(); self.save_history(); std::process::exit(0) } Command::Open(args) => { if let Err(e) = self.open_db(&args.path, args.vfs_name.as_deref()) { let _ = self.writeln(e.to_string()); } } Command::Schema(args) => { if let Err(e) = self.display_schema(args.table_name.as_deref()) { let _ = self.writeln(e.to_string()); } } Command::Tables(args) => { if let Err(e) = self.display_tables(args.pattern.as_deref()) { let _ = self.writeln(e.to_string()); } } Command::Databases => { if let Err(e) = self.display_databases() { let _ = self.writeln(e.to_string()); } } Command::Opcodes(args) => { if let Some(opcode) = args.opcode { for op in &OPCODE_DESCRIPTIONS { if op.name.eq_ignore_ascii_case(opcode.trim()) { let _ = self.writeln_fmt(format_args!("{op}")); } } } else { for op in &OPCODE_DESCRIPTIONS { let _ = self.writeln_fmt(format_args!("{op}\n")); } } } Command::NullValue(args) => { self.opts.null_value = args.value; } Command::OutputMode(args) => { if let Err(e) = self.set_mode(args.mode) { let _ = self.writeln_fmt(format_args!("Error: {e}")); } } Command::SetOutput(args) => { if let Some(path) = args.path { if let Err(e) = self.set_output_file(&path) { let _ = self.writeln_fmt(format_args!("Error: {e}")); } } else { self.set_output_stdout(); } } Command::Echo(args) => { self.toggle_echo(args.mode); } Command::Cwd(args) => { let _ = std::env::set_current_dir(args.directory); } Command::ShowInfo => { let _ = self.show_info(); } Command::Stats(args) => { if let Err(e) = self.display_stats(args) { let _ = self.writeln(e.to_string()); } } Command::Import(args) => { let w = self.writer.as_mut().unwrap(); let mut import_file = ImportFile::new(self.conn.clone(), w); import_file.import(args) } Command::LoadExtension(args) => { #[cfg(not(target_family = "wasm"))] if let Err(e) = self.handle_load_extension(&args.path) { let _ = self.writeln(&e); } } Command::Dump => { if let Err(e) = self.dump_database() { let _ = self.writeln_fmt(format_args!("/****** ERROR: {e} ******/")); } } Command::DbConfig(_args) => { let _ = self.writeln("dbconfig currently ignored"); } Command::ListVfs => { let _ = self.writeln("Available VFS modules:"); self.conn.list_vfs().iter().for_each(|v| { let _ = self.writeln(v); }); } Command::ListIndexes(args) => { if let Err(e) = self.display_indexes(args.tbl_name) { let _ = self.writeln(e.to_string()); } } Command::Timer(timer_mode) => { self.opts.timer = match timer_mode.mode { TimerMode::On => true, TimerMode::Off => false, }; } Command::Headers(headers_mode) => { self.opts.headers = match headers_mode.mode { HeadersMode::On => true, HeadersMode::Off => false, }; } Command::Clone(args) => { if let Err(e) = self.clone_database(&args.output_file) { let _ = self.writeln(e.to_string()); } } Command::Manual(args) => { let w = self.writer.as_mut().unwrap(); if let Err(e) = manual::display_manual(args.page.as_deref(), w) { let _ = self.writeln(e.to_string()); } } Command::Read(args) => { if let Err(e) = self.read_sql_file(&args.path) { let _ = self.writeln(e.to_string()); } } Command::Parameter(args) => { if let Err(e) = self.handle_parameter_command(args) { let _ = self.writeln_fmt(format_args!("Error: {e}")); } } Command::Dbtotxt(args) => { if let Err(e) = self.dump_database_as_text(args.page_no) { let _ = self.writeln_fmt(format_args!("ERROR:{e}")); } } }, } } fn print_query_result( &mut self, sql: &str, output: &mut Result, LimboError>, statistics: Option<&mut QueryStatistics>, ) -> anyhow::Result<()> { match output { Ok(Some(ref mut rows)) => { let query_mode = rows.get_query_mode(); let output_mode = self.opts.output_mode; match (output_mode, query_mode) { (_, QueryMode::ExplainQueryPlan) => { self.print_explain_query_plan(rows, statistics)?; } (_, QueryMode::Explain) => { self.print_explain(rows, statistics)?; } (OutputMode::List, _) => { self.print_list_mode(rows, statistics)?; } (OutputMode::Pretty, _) => { self.print_pretty_mode(rows, statistics)?; } (OutputMode::Line, _) => { self.print_line_mode(rows, statistics)?; } } } Ok(None) => {} Err(ref err) => { match err { LimboError::Busy => {} LimboError::Interrupt => {} _ => { let report = miette::Error::from(err.clone()).with_source_code(sql.to_owned()); let _ = self.writeln_fmt(format_args!("{report:?}")); } } anyhow::bail!("We have to throw here, even if we printed error"); } } Ok(()) } fn print_explain_query_plan( &mut self, rows: &mut Statement, statistics: Option<&mut QueryStatistics>, ) -> turso_core::Result<()> { struct Entry { id: usize, detail: String, child_prefix: String, children: Vec, } fn add_children(id: usize, parent_id: usize, detail: String, current: &mut Entry) -> bool { if current.id == parent_id { current.children.push(Entry { id, detail, child_prefix: current.child_prefix.clone() + " ", children: vec![], }); if current.children.len() > 1 { let idx = current.children.len() - 2; current.children[idx].child_prefix = current.child_prefix.clone() + "| "; } return false; } for child in &mut current.children { if !add_children(id, parent_id, detail.clone(), child) { return false; } } true } fn print_entry(app: &mut Limbo, entry: &Entry, prefix: &str) { writeln!(app, "{}{}", prefix, entry.detail).unwrap(); for (i, child) in entry.children.iter().enumerate() { let is_last = i == entry.children.len() - 1; let child_prefix = format!( "{}{}", entry.child_prefix, if is_last { "`--" } else { "|--" } ); print_entry(app, child, child_prefix.as_str()); } } let mut root = Entry { id: 0, detail: "QUERY PLAN".to_owned(), child_prefix: "".to_owned(), children: vec![], }; let mut stepper = RowStepper::new(rows, statistics); loop { match stepper.next_row() { Ok(Some(row)) => { let id = row.get_value(0).as_uint() as usize; let parent_id = row.get_value(1).as_uint() as usize; let detail = row.get_value(3).to_string(); add_children(id, parent_id, detail, &mut root); } Ok(None) => break, Err(e) => { self.handle_step_error(e); break; } } } print_entry(self, &root, ""); Ok(()) } fn print_explain( &mut self, rows: &mut Statement, statistics: Option<&mut QueryStatistics>, ) -> turso_core::Result<()> { fn get_explain_indent( indent_count: usize, curr_insn: &str, prev_insn: &str, p1: &str, unclosed_begin_subrtns: &mut Vec, ) -> usize { let indent_count = match prev_insn { "Rewind" | "Last" | "SorterSort" | "SeekGE" | "SeekGT" | "SeekLE" | "SeekLT" | "BeginSubrtn" | "IndexMethodQuery" => indent_count + 1, _ => indent_count, }; if curr_insn == "BeginSubrtn" { unclosed_begin_subrtns.push(p1.to_string()); } match curr_insn { "Next" | "SorterNext" | "Prev" => indent_count.saturating_sub(1), "Return" => { let matching = unclosed_begin_subrtns.iter().position(|b| b == p1); if let Some(idx) = matching { unclosed_begin_subrtns.remove(idx); indent_count.saturating_sub(1) } else { indent_count } } _ => indent_count, } } let _ = self.writeln("addr opcode p1 p2 p3 p4 p5 comment"); let _ = self.writeln("---- ----------------- ---- ---- ---- ------------- -- -------"); let mut prev_insn = String::new(); let mut indent_count = 0; let indent = " "; let mut unclosed_begin_subrtns = vec![]; let mut stepper = RowStepper::new(rows, statistics); loop { match stepper.next_row() { Ok(Some(row)) => { let insn = row.get_value(1).to_string(); let p1 = row.get_value(2).to_string(); indent_count = get_explain_indent( indent_count, &insn, &prev_insn, &p1, &mut unclosed_begin_subrtns, ); let _ = self.writeln(format!( "{:<4} {:<17} {:<4} {:<4} {:<4} {:<13} {:<2} {}", row.get_value(0).to_string(), &(indent.repeat(indent_count) + &insn), p1, row.get_value(3).to_string(), row.get_value(4).to_string(), row.get_value(5).to_string(), row.get_value(6).to_string(), row.get_value(7), )); prev_insn = insn; } Ok(None) => break, Err(e) => { self.handle_step_error(e); break; } } } Ok(()) } fn print_list_mode( &mut self, rows: &mut Statement, statistics: Option<&mut QueryStatistics>, ) -> turso_core::Result<()> { let num_columns = rows.num_columns(); let column_names: Vec = (0..num_columns) .map(|i| rows.get_column_name(i).to_string()) .collect(); let print_headers = self.opts.headers; let null_value = self.opts.null_value.clone(); let mut headers_printed = false; let mut stepper = RowStepper::new(rows, statistics); loop { match stepper.next_row() { Ok(Some(row)) => { if print_headers && !headers_printed { for (i, name) in column_names.iter().enumerate() { if i > 0 { let _ = self.write(b"|"); } let _ = self.write(name.as_bytes()); } let _ = self.writeln(""); headers_printed = true; } for (i, value) in row.get_values().enumerate() { if i > 0 { let _ = self.write(b"|"); } if matches!(value, Value::Null) { let _ = self.write(null_value.as_bytes()); } else { write!(self, "{value}").map_err(|e| io_error(e, "write"))?; } } let _ = self.writeln(""); } Ok(None) => break, Err(e) => { self.handle_step_error(e); break; } } } Ok(()) } fn print_pretty_mode( &mut self, rows: &mut Statement, statistics: Option<&mut QueryStatistics>, ) -> turso_core::Result<()> { let config = self.config.as_ref().unwrap(); let null_value = self.opts.null_value.clone(); let num_columns = rows.num_columns(); let column_names: Vec = (0..num_columns) .map(|i| rows.get_column_name(i).to_string()) .collect(); let header_color = config.table.header_color.as_comfy_table_color(); let column_colors: Vec<_> = config .table .column_colors .iter() .map(|c| c.as_comfy_table_color()) .collect(); let mut table = Table::new(); table .set_content_arrangement(ContentArrangement::Dynamic) .set_truncation_indicator("…") .apply_modifier("││──├─┼┤│─┼├┤┬┴┌┐└┘"); if num_columns > 0 { let header = column_names .iter() .map(|name| { Cell::new(name) .add_attribute(Attribute::Bold) .fg(header_color) }) .collect::>(); table.set_header(header); } let mut stepper = RowStepper::new(rows, statistics); loop { match stepper.next_row() { Ok(Some(row)) => { let mut table_row = Row::new(); table_row.max_height(1); for (idx, value) in row.get_values().enumerate() { let (content, alignment) = match value { Value::Null => (null_value.clone(), CellAlignment::Left), Value::Numeric(_) => (format!("{value}"), CellAlignment::Right), Value::Text(_) => (format!("{value}"), CellAlignment::Left), Value::Blob(_) => (format!("{value}"), CellAlignment::Left), }; table_row.add_cell( Cell::new(content) .set_alignment(alignment) .fg(column_colors[idx % column_colors.len()]), ); } table.add_row(table_row); } Ok(None) => break, Err(e) => { self.handle_step_error(e); break; } } } if !table.is_empty() { writeln!(self, "{table}").map_err(|e| io_error(e, "write"))?; } Ok(()) } fn print_line_mode( &mut self, rows: &mut Statement, statistics: Option<&mut QueryStatistics>, ) -> turso_core::Result<()> { let num_columns = rows.num_columns(); let column_names: Vec = (0..num_columns) .map(|i| rows.get_column_name(i).to_string()) .collect(); let max_width = column_names.iter().map(|n| n.len()).max().unwrap_or(0); let formatted_columns: Vec = column_names .iter() .map(|n| format!("{n:>max_width$}")) .collect(); let null_value = self.opts.null_value.clone(); let mut first_row_printed = false; let mut stepper = RowStepper::new(rows, statistics); loop { match stepper.next_row() { Ok(Some(row)) => { if first_row_printed { self.writeln("").map_err(|e| io_error(e, "write"))?; } else { first_row_printed = true; } for (i, value) in row.get_values().enumerate() { self.write(&formatted_columns[i]) .map_err(|e| io_error(e, "write"))?; self.write(b" = ").map_err(|e| io_error(e, "write"))?; if matches!(value, Value::Null) { self.write(null_value.as_bytes()) .map_err(|e| io_error(e, "write"))?; } else { write!(self, "{value}").map_err(|e| io_error(e, "write"))?; } self.writeln("").map_err(|e| io_error(e, "write"))?; } } Ok(None) => break, Err(e) => { self.handle_step_error(e); break; } } } Ok(()) } fn handle_step_error(&mut self, err: LimboError) { self.had_query_error = true; match err { LimboError::Interrupt => { let _ = self.writeln(LimboError::Interrupt.to_string()); } LimboError::Busy => { let _ = self.writeln("database is busy"); } _ => { let _ = self.writeln_fmt(format_args!("Error: {err}")); } } } pub fn init_tracing(opts: &Opts) -> Result { let ((non_blocking, guard), should_emit_ansi) = if let Some(file) = &opts.tracing_output { ( tracing_appender::non_blocking( std::fs::File::options() .append(true) .create(true) .open(file)?, ), false, ) } else { ( tracing_appender::non_blocking(std::io::stderr()), IsTerminal::is_terminal(&std::io::stderr()), ) }; let default_env_filter = EnvFilter::builder() .with_default_directive(tracing::level_filters::LevelFilter::WARN.into()) .from_env_lossy(); // Disable rustyline traces if let Err(e) = tracing_subscriber::registry() .with( tracing_subscriber::fmt::layer() .with_writer(non_blocking) .with_line_number(true) .with_thread_ids(true) .with_ansi(should_emit_ansi), ) .with(default_env_filter.add_directive("rustyline=off".parse().unwrap())) .try_init() { println!("Unable to setup tracing appender: {e:?}"); } Ok(guard) } fn print_schema_entry(&mut self, db_display_name: &str, row: &turso_core::Row) -> bool { if let (Ok(Value::Text(schema)), Ok(Value::Text(obj_type)), Ok(Value::Text(obj_name))) = ( row.get::<&Value>(0), row.get::<&Value>(1), row.get::<&Value>(2), ) { let modified_schema = if db_display_name == "main" { schema.as_str().to_string() } else { // We need to modify the SQL to include the database prefix in table names // This is a simple approach - for CREATE TABLE statements, insert db name after "TABLE " // For CREATE INDEX statements, insert db name after "ON " let schema_str = schema.as_str(); if schema_str.to_uppercase().contains("CREATE TABLE ") { // Find "CREATE TABLE " and insert database name after it if let Some(pos) = schema_str.to_uppercase().find("CREATE TABLE ") { let before = &schema_str[..pos + "CREATE TABLE ".len()]; let after = &schema_str[pos + "CREATE TABLE ".len()..]; format!("{before}{db_display_name}.{after}") } else { schema_str.to_string() } } else if schema_str.to_uppercase().contains(" ON ") { // For indexes, find " ON " and insert database name after it if let Some(pos) = schema_str.to_uppercase().find(" ON ") { let before = &schema_str[..pos + " ON ".len()]; let after = &schema_str[pos + " ON ".len()..]; format!("{before}{db_display_name}.{after}") } else { schema_str.to_string() } } else { schema_str.to_string() } }; let _ = self.writeln_fmt(format_args!("{modified_schema};")); // For views, add the column comment like SQLite does if obj_type.as_str() == "view" { let columns = self .get_view_columns(obj_name.as_str()) .unwrap_or_else(|_| "x".to_string()); let _ = self.writeln_fmt(format_args!("/* {}({}) */", obj_name.as_str(), columns)); } true } else { false } } /// Get column names for a view to generate the SQLite-compatible comment fn get_view_columns(&mut self, view_name: &str) -> anyhow::Result { // Get column information using PRAGMA table_info let pragma_sql = format!("PRAGMA table_info({view_name})"); let mut columns = Vec::new(); let handler = |row: &turso_core::Row| { // Column name is in the second column (index 1) of PRAGMA table_info if let Ok(Value::Text(col_name)) = row.get::<&Value>(1) { columns.push(col_name.as_str().to_string()); } Ok(()) }; if let Err(err) = self.handle_row(&pragma_sql, handler) { return Err(anyhow::anyhow!( "Error retrieving columns for view '{}': {}", view_name, err )); } if columns.is_empty() { anyhow::bail!("PRAGMA table_info returned no columns for view '{}'. The view may be corrupted or the database schema is invalid.", view_name); } Ok(columns.join(",")) } fn query_one_table_schema( &mut self, db_prefix: &str, db_display_name: &str, table_name: &str, ) -> anyhow::Result { // Yeah, sqlite also has this hardcoded: https://github.com/sqlite/sqlite/blob/31efe5a0f2f80a263457a1fc6524783c0c45769b/src/shell.c.in#L10765 match table_name { "sqlite_master" | "sqlite_schema" | "sqlite_temp_master" | "sqlite_temp_schema" => { let schema = format!( "CREATE TABLE {table_name} (\n type text,\n name text,\n tbl_name text,\n rootpage integer,\n sql text\n);", ); let _ = self.writeln(&schema); return Ok(true); } _ => {} } let sql = format!( "SELECT sql, type, name FROM {db_prefix}.sqlite_schema WHERE type IN ('table', 'index', 'view') AND (tbl_name = '{table_name}' OR name = '{table_name}') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__turso_internal_%' ORDER BY CASE type WHEN 'table' THEN 1 WHEN 'view' THEN 2 WHEN 'index' THEN 3 END, rowid" ); let mut found = false; match self.conn.query(&sql) { Ok(Some(ref mut rows)) => { let res = rows.run_with_row_callback(|row| { found |= self.print_schema_entry(db_display_name, row); Ok(()) }); match res { Ok(_) => {} Err(LimboError::Interrupt) => { let _ = self.writeln(LimboError::Interrupt.to_string()); } Err(LimboError::Busy) => { let _ = self.writeln("database is busy"); } Err(err) => return Err(anyhow::anyhow!(err)), } } Ok(None) => {} Err(_) => {} // Table not found in this database } Ok(found) } fn query_all_tables_schema( &mut self, db_prefix: &str, db_display_name: &str, ) -> anyhow::Result<()> { let sql = format!("SELECT sql, type, name FROM {db_prefix}.sqlite_schema WHERE type IN ('table', 'index', 'view') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__turso_internal_%' ORDER BY CASE type WHEN 'table' THEN 1 WHEN 'view' THEN 2 WHEN 'index' THEN 3 END, rowid"); match self.conn.query(&sql) { Ok(Some(ref mut rows)) => { let res = rows.run_with_row_callback(|row| { self.print_schema_entry(db_display_name, row); Ok(()) }); match res { Ok(_) => {} Err(LimboError::Busy) => { let _ = self.writeln("database is busy"); } Err(LimboError::Interrupt) => { let _ = self.writeln(LimboError::Interrupt.to_string()); } Err(err) => return Err(anyhow!(err)), } } Ok(None) => {} Err(err) => { // If we can't access this database's schema, just skip it if !err.to_string().contains("no such table") { eprintln!( "Warning: Could not query schema for database '{db_display_name}': {err}" ); } } } Ok(()) } fn display_schema(&mut self, table: Option<&str>) -> anyhow::Result<()> { match table { Some(table_spec) => { // Parse table name to handle database prefixes (e.g., "db.table") let clean_table_spec = table_spec.trim_end_matches(';'); let (target_db, table_name) = if let Some((db, tbl)) = clean_table_spec.split_once('.') { (db, tbl) } else { ("main", clean_table_spec) }; // Query only the specific table in the specific database if target_db == "main" { self.query_one_table_schema("main", "main", table_name)?; } else { // Check if the database is attached let attached_databases = self.conn.list_attached_databases(); if attached_databases.contains(&target_db.to_string()) { self.query_one_table_schema(target_db, target_db, table_name)?; } } } None => { // Show schema for all tables in all databases let attached_databases = self.conn.list_attached_databases(); // Query main database first self.query_all_tables_schema("main", "main")?; // Query all attached databases for db_name in attached_databases { self.query_all_tables_schema(&db_name, &db_name)?; } } } Ok(()) } fn display_indexes(&mut self, maybe_table: Option) -> anyhow::Result<()> { let mut indexes = String::new(); for name in self.database_names()? { let prefix = (name != "main").then_some(&name); let sql = match maybe_table { Some(ref tbl_name) => format!( "SELECT name FROM {name}.sqlite_schema WHERE type='index' AND tbl_name = '{tbl_name}' ORDER BY 1" ), None => format!("SELECT name FROM {name}.sqlite_schema WHERE type='index' ORDER BY 1"), }; let handler = |row: &turso_core::Row| { if let Ok(Value::Text(idx)) = row.get::<&Value>(0) { if let Some(prefix) = prefix { indexes.push_str(prefix); indexes.push('.'); } indexes.push_str(idx.as_str()); indexes.push(' '); } Ok(()) }; if let Err(err) = self.handle_row(&sql, handler) { if err.to_string().contains("no such table: sqlite_schema") { return Err(anyhow::anyhow!("Unable to access database schema. The database may be using an older SQLite version or may not be properly initialized.")); } else { return Err(anyhow::anyhow!("Error querying schema: {}", err)); } } } if !indexes.is_empty() { let _ = self.writeln(indexes.trim_end().as_bytes()); } Ok(()) } fn display_tables(&mut self, pattern: Option<&str>) -> anyhow::Result<()> { let mut tables = String::new(); for name in self.database_names()? { let prefix = (name != "main").then_some(&name); let sql = match pattern { Some(pattern) => format!( "SELECT name FROM {name}.sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name LIKE '{pattern}' ORDER BY 1" ), None => format!( "SELECT name FROM {name}.sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY 1" ), }; let handler = |row: &turso_core::Row| { if let Ok(Value::Text(table)) = row.get::<&Value>(0) { if let Some(prefix) = prefix { tables.push_str(prefix); tables.push('.'); } tables.push_str(table.as_str()); tables.push(' '); } Ok(()) }; if let Err(e) = self.handle_row(&sql, handler) { if e.to_string().contains("no such table: sqlite_schema") { return Err(anyhow::anyhow!("Unable to access database schema. The database may be using an older SQLite version or may not be properly initialized.")); } else { return Err(anyhow::anyhow!("Error querying schema: {}", e)); } } } if !tables.is_empty() { let _ = self.writeln(tables.trim_end().as_bytes()); } else if let Some(pattern) = pattern { let _ = self.writeln_fmt(format_args!( "Error: Tables with pattern '{pattern}' not found." )); } else { let _ = self.writeln(b"No tables found in the database."); } Ok(()) } fn database_names(&mut self) -> anyhow::Result> { let sql = "PRAGMA database_list"; let mut db_names: Vec = Vec::new(); let handler = |row: &turso_core::Row| { if let Ok(Value::Text(name)) = row.get::<&Value>(1) { db_names.push(name.to_string()); } Ok(()) }; match self.handle_row(sql, handler) { Ok(_) => Ok(db_names), Err(e) => Err(anyhow::anyhow!("Error in database list: {}", e)), } } fn handle_row(&mut self, sql: &str, handler: F) -> anyhow::Result<()> where F: FnMut(&turso_core::Row) -> turso_core::Result<()>, { match self.conn.query(sql) { Ok(Some(ref mut rows)) => { let res = rows.run_with_row_callback(handler); match res { Ok(_) => {} Err(LimboError::Busy) => { let _ = self.writeln("database is busy"); } Err(LimboError::Interrupt) => { let _ = self.writeln(LimboError::Interrupt.to_string()); } Err(err) => return Err(anyhow!(err)), } } Ok(None) => { let _ = self.writeln("No results returned from the query."); } Err(err) => { return Err(anyhow::anyhow!("Error querying database: {}", err)); } } Ok(()) } fn display_databases(&mut self) -> anyhow::Result<()> { let sql = "PRAGMA database_list"; let conn = self.conn.clone(); let mut databases = Vec::new(); self.handle_row(sql, |row| { if let ( Ok(Value::Numeric(Numeric::Integer(seq))), Ok(Value::Text(name)), Ok(file_value), ) = ( row.get::<&Value>(0), row.get::<&Value>(1), row.get::<&Value>(2), ) { let file = match file_value { Value::Text(path) => path.as_str(), Value::Null => "", _ => "", }; // Format like SQLite: "main: /path/to/file r/w" let file_display = if file.is_empty() { "\"\"".to_string() } else { file.to_string() }; // Detect readonly mode from connection let mode = if conn.is_readonly(*seq as usize) { "r/o" } else { "r/w" }; databases.push(format!("{}: {} {}", name.as_str(), file_display, mode)); } Ok(()) })?; for db in databases { let _ = self.writeln(db); } Ok(()) } // readline will read inputs from rustyline or stdin // and write it to input_buff. pub fn readline(&mut self) -> Result<(), ReadlineError> { use std::fmt::Write; if let Some(rl) = &mut self.rl { let result = rl.readline(&self.prompt)?; self.read_state.process(&result); let _ = self.input_buff.write_str(result.as_str()); } else { let mut reader = std::io::stdin().lock(); let prev_len = self.input_buff.len(); if reader.read_line(&mut self.input_buff)? == 0 { return Err(ReadlineError::Eof); } self.read_state.process(&self.input_buff[prev_len..]); } if !self.input_buff.ends_with(char::is_whitespace) { let _ = self.input_buff.write_char('\n'); } Ok(()) } pub fn dump_database_from_conn( fk: bool, conn: Arc, out: &mut W, mut progress: P, ) -> anyhow::Result<()> { // Snapshot for consistency Self::exec_all_conn(&conn, "BEGIN")?; // FIXME: we don't yet support PRAGMA foreign_keys=OFF internally, // so for now this hacky boolean that decides not to emit it when cloning if fk { writeln!(out, "PRAGMA foreign_keys=OFF;")?; } writeln!(out, "BEGIN TRANSACTION;")?; // FIXME: At this point, SQLite executes the following: // sqlite3_exec(p->db, "SAVEPOINT dump; PRAGMA writable_schema=ON", 0, 0, 0); // we don't have those yet, so don't. // Emit CREATE TYPE statements from __turso_internal_types before table DDL, // so that tables referencing custom types can be restored correctly. Self::dump_custom_types(&conn, out)?; let q_tables = r#" SELECT name, sql FROM sqlite_schema WHERE type='table' AND sql NOT NULL ORDER BY tbl_name = 'sqlite_sequence', rowid "#; if let Some(mut rows) = conn.query(q_tables)? { rows.run_with_row_callback(|row| { let name: &str = row.get::<&str>(0)?; // Skip sqlite_sequence and internal types metadata table if name == "sqlite_sequence" || name == turso_core::schema::TURSO_TYPES_TABLE_NAME { return Ok(()); } let ddl: &str = row.get::<&str>(1)?; writeln!(out, "{ddl};").map_err(|e| io_error(e, "write"))?; Self::dump_table_from_conn(&conn, out, name, &mut progress)?; progress.on(name); Ok(()) })?; } Self::dump_sqlite_sequence(&conn, out)?; Self::dump_schema_objects(&conn, out, &mut progress)?; Self::exec_all_conn(&conn, "COMMIT")?; writeln!(out, "COMMIT;")?; Ok(()) } fn exec_all_conn(conn: &Arc, sql: &str) -> turso_core::Result<()> { if let Some(mut rows) = conn.query(sql)? { rows.run_with_row_callback(|_| Ok(()))?; } Ok(()) } fn dump_table_from_conn( conn: &Arc, out: &mut W, table_name: &str, progress: &mut P, ) -> turso_core::Result<()> { let pragma = format!("PRAGMA table_info({})", quote_ident(table_name)); let (mut cols, mut types) = (Vec::new(), Vec::new()); if let Some(mut rows) = conn.query(pragma)? { rows.run_with_row_callback(|row| { let ty = row.get::<&str>(2)?.to_string(); let name = row.get::<&str>(1)?.to_string(); match ty.as_str() { "index" => progress.on(&name), "view" => progress.on(&name), "trigger" => progress.on(&name), _ => {} } cols.push(name); types.push(ty); Ok(()) })?; } // FIXME: sqlite has logic to check rowid and optionally preserve it, but it requires // pragma index_list, and it seems to be relevant only for indexes. let cols_str = cols .iter() .map(|c| quote_ident(c)) .collect::>() .join(", "); let select = format!("SELECT {cols_str} FROM {}", quote_ident(table_name)); if let Some(mut rows) = conn.query(select)? { rows.run_with_row_callback(|row| { write!(out, "INSERT INTO {} VALUES(", quote_ident(table_name)) .map_err(|e| io_error(e, "write"))?; for i in 0..cols.len() { if i > 0 { out.write_all(b",").map_err(|e| io_error(e, "write"))?; } let v = row.get::<&Value>(i)?; Self::write_sql_value_from_value(out, v).map_err(|e| io_error(e, "write"))?; } out.write_all(b");\n").map_err(|e| io_error(e, "write"))?; Ok(()) })?; } Ok(()) } fn dump_custom_types(conn: &Arc, out: &mut W) -> anyhow::Result<()> { // Check if the internal types table exists before querying it. let check = format!( "SELECT 1 FROM sqlite_schema WHERE name='{}' AND type='table'", turso_core::schema::TURSO_TYPES_TABLE_NAME ); let mut has_types = false; if let Some(mut rows) = conn.query(&check)? { rows.run_with_row_callback(|_| { has_types = true; Ok(()) })?; } if !has_types { return Ok(()); } let q = format!( "SELECT sql FROM {} ORDER BY rowid", turso_core::schema::TURSO_TYPES_TABLE_NAME ); if let Some(mut rows) = conn.query(&q)? { rows.run_with_row_callback(|row| { let sql: &str = row.get::<&str>(0)?; writeln!(out, "{sql};").map_err(|e| io_error(e, "write"))?; Ok(()) })?; } Ok(()) } fn dump_sqlite_sequence(conn: &Arc, out: &mut W) -> anyhow::Result<()> { let mut has_seq = false; if let Some(mut rows) = conn.query("SELECT 1 FROM sqlite_schema WHERE name='sqlite_sequence' AND type='table'")? { rows.run_with_row_callback(|_| { has_seq = true; Ok(()) })?; } if !has_seq { return Ok(()); } writeln!(out, "DELETE FROM sqlite_sequence;")?; if let Some(mut rows) = conn.query("SELECT name, seq FROM sqlite_sequence")? { rows.run_with_row_callback(|r| { let name = r.get::<&str>(0)?; let seq = r.get::(1)?; writeln!( out, "INSERT INTO sqlite_sequence(name,seq) VALUES({},{});", sql_quote_string(name), seq ) .map_err(|e| io_error(e, "write"))?; Ok(()) })?; } Ok(()) } fn dump_schema_objects( conn: &Arc, out: &mut W, progress: &mut P, ) -> anyhow::Result<()> { // SQLite’s shell usually emits views after tables. // Emit only user objects: sql NOT NULL and name NOT LIKE 'sqlite_%' let sql = r#" SELECT name, sql FROM sqlite_schema WHERE sql NOT NULL AND name NOT LIKE 'sqlite_%' AND type IN ('index','trigger','view') ORDER BY CASE type WHEN 'view' THEN 1 WHEN 'index' THEN 2 WHEN 'trigger' THEN 3 END, rowid "#; if let Some(mut rows) = conn.query(sql)? { rows.run_with_row_callback(|row| { let ddl: &str = row.get::<&str>(1)?; let name: &str = row.get::<&str>(0)?; progress.on(name); writeln!(out, "{ddl};").map_err(|e| io_error(e, "write"))?; Ok(()) })?; } Ok(()) } fn write_sql_value_from_value(out: &mut W, v: &Value) -> io::Result<()> { match v { Value::Null => out.write_all(b"NULL"), Value::Numeric(Numeric::Integer(i)) => out.write_all(format!("{i}").as_bytes()), Value::Numeric(Numeric::Float(f)) => write!(out, "{}", f64::from(*f)).map(|_| ()), Value::Text(s) => { out.write_all(b"'")?; let bytes = s.value.as_bytes(); let mut i = 0; while i < bytes.len() { let b = bytes[i]; if b == b'\'' { out.write_all(b"''")?; } else { out.write_all(&[b])?; } i += 1; } out.write_all(b"'") } Value::Blob(b) => { out.write_all(b"X'")?; const HEX: &[u8; 16] = b"0123456789abcdef"; for &byte in b { out.write_all(&[HEX[(byte >> 4) as usize], HEX[(byte & 0x0F) as usize]])?; } out.write_all(b"'") } } } fn dump_database(&mut self) -> anyhow::Result<()> { // Move writer out so we don’t hold a field-borrow of self during the call. let mut out = std::mem::take(&mut self.writer).unwrap(); let conn = self.conn.clone(); // dont print progress because it would interfere with piping output of .dump let res = Self::dump_database_from_conn(true, conn, &mut out, NoopProgress); // Put writer back self.writer = Some(out); res } fn clone_database(&mut self, output_file: &str) -> anyhow::Result<()> { use std::path::Path; if Path::new(output_file).exists() { anyhow::bail!("Refusing to overwrite existing file: {output_file}"); } let io: Arc = Arc::new(turso_core::PlatformIO::new()?); let db = Database::open_file(io.clone(), output_file)?; let target = db.connect()?; let mut applier = ApplyWriter::new(&target); Self::dump_database_from_conn(false, self.conn.clone(), &mut applier, StderrProgress)?; applier.finish()?; Ok(()) } fn read_sql_file(&mut self, path: &str) -> anyhow::Result<()> { let file = File::open(path).map_err(|e| anyhow!("Error: cannot open \"{}\" – {}", path, e))?; let reader = BufReader::new(file); let mut query_buffer = String::new(); let mut state = ReadState::default(); for line in reader.lines() { let line = line .map_err(|e| anyhow!("Error: file \"{}\" is not valid UTF-8 text – {}", path, e))?; if !query_buffer.is_empty() { query_buffer.push('\n'); } query_buffer.push_str(&line); state.process(&line); if state.is_complete() { self.run_query(&query_buffer); query_buffer.clear(); state = ReadState::default(); } } let remaining = query_buffer.trim(); if !remaining.is_empty() { self.run_query(remaining); } query_buffer.clear(); Ok(()) } fn save_history(&mut self) { if let Some(rl) = &mut self.rl { let _ = rl.save_history(HISTORY_FILE.as_path()); } } fn fetch_db_metadata(&mut self) -> anyhow::Result { let page_size: i64 = if let Some(mut rows) = self.conn.query("PRAGMA page_size")? { fetch_single_i64(&mut rows).context("Failed to execute PRAGMA page_size")? } else { anyhow::bail!("Failed to prepare PRAGMA page_size"); }; let page_count: i64 = if let Some(mut rows) = self.conn.query("PRAGMA page_count")? { fetch_single_i64(&mut rows).context("Failed to execute PRAGMA page_count")? } else { anyhow::bail!("Failed to prepare PRAGMA page_count"); }; let filename = PathBuf::from(self.opts.db_file.clone()) .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); Ok(DbMetadata { page_size, page_count, filename, }) } fn write_page_hexdump(&mut self, page: &DbPage, page_size: i64) -> anyhow::Result<()> { let mut seen_page_label = false; for (i, chunk) in page.data.chunks(16).enumerate() { if chunk.iter().all(|&b| b == 0) { continue; } if !seen_page_label { writeln!( self, "| page {} offset {}", page.pgno, (page.pgno - 1) * page_size )?; seen_page_label = true; } // Line offset write!(self, "| {:5}:", i * 16)?; // Hex bytes for byte in chunk { write!(self, " {byte:02x}")?; } for _ in 0..(16 - chunk.len()) { write!(self, " ")?; // Pad partial lines } write!(self, " ")?; // ASCII for &byte in chunk { let ch = match byte { b' '..=b'~' if ![b'{', b'}', b'"', b'\\'].contains(&byte) => byte as char, _ => '.', }; write!(self, "{ch}")?; } writeln!(self)?; } Ok(()) } fn dump_database_as_text(&mut self, page_no: Option) -> anyhow::Result<()> { let metadata = self.fetch_db_metadata()?; tracing::debug!( page_size = metadata.page_size, page_count = metadata.page_count, "Fetched metadata" ); if let Some(pgno) = page_no { if pgno <= 0 { anyhow::bail!("Page number must be a positive integer."); } if pgno > metadata.page_count { anyhow::bail!( "Page number {pgno} is out of bounds. The database only has {} pages.", metadata.page_count ); } } writeln!( self, "| size {} pagesize {} filename {}", metadata.page_count * metadata.page_size, metadata.page_size, &metadata.filename )?; let dump_sql = if let Some(pgno) = page_no { format!("SELECT pgno, data FROM sqlite_dbpage WHERE pgno = {pgno}") } else { "SELECT pgno, data FROM sqlite_dbpage ORDER BY pgno".to_string() }; let mut pages: Vec<(i64, Vec)> = Vec::new(); if let Some(mut rows) = self.conn.query(&dump_sql)? { rows.run_with_row_callback(|row| { let pgno: i64 = row.get(0)?; let value: &Value = row.get(1)?; let data: Vec = match value { Value::Blob(bytes) => bytes.clone(), _ => vec![], }; pages.push((pgno, data)); Ok(()) })?; } for (pgno, data) in &pages { let page = DbPage { pgno: *pgno, data }; self.write_page_hexdump(&page, metadata.page_size)?; } writeln!(self, "| end {}", &metadata.filename)?; Ok(()) } } fn quote_ident(s: &str) -> String { let mut out = String::with_capacity(s.len() + 2); out.push('"'); for ch in s.chars() { if ch == '"' { out.push('"'); } out.push(ch); } out.push('"'); out } fn sql_quote_string(s: &str) -> String { let mut out = String::with_capacity(s.len() + 2); out.push('\''); for ch in s.chars() { if ch == '\'' { out.push('\''); } out.push(ch); } out.push('\''); out } fn validate_parameter_name(name: &str) -> Result<(), String> { if name.is_empty() { return Err("parameter name cannot be empty".to_string()); } match name.as_bytes()[0] { b':' | b'@' | b'$' | b'#' => Ok(()), b'?' => { let Some(rest) = name.strip_prefix('?') else { return Err("invalid parameter name".to_string()); }; if rest.is_empty() { return Err("parameter name '?N' must include digits".to_string()); } if rest.parse::().ok().filter(|idx| *idx > 0).is_none() { return Err("parameter name '?N' must use an index >= 1".to_string()); } Ok(()) } _ => Err("parameter name must start with one of ':', '@', '$', '#', '?'".to_string()), } } fn parameter_name_to_index(name: &str) -> Option { let value = name.strip_prefix('?')?.parse::().ok()?; value.try_into().ok() } fn parse_parameter_value(value: &str) -> Result { let value = value.trim(); if value.eq_ignore_ascii_case("null") { return Ok(Value::Null); } if let Ok(integer) = value.parse::() { return Ok(Value::from_i64(integer)); } if value.contains(['.', 'e', 'E']) { if let Ok(float) = value.parse::() { return Ok(Value::from_f64(float)); } } if let Some(hex) = value .strip_prefix("x'") .or_else(|| value.strip_prefix("X'")) .and_then(|stripped| stripped.strip_suffix('\'')) { return parse_hex_blob(hex).map(Value::from_blob); } if let Some(inner) = value .strip_prefix('\'') .and_then(|stripped| stripped.strip_suffix('\'')) { return Ok(Value::build_text(unescape_single_quoted(inner))); } Ok(Value::build_text(value.to_owned())) } fn parse_hex_blob(hex: &str) -> Result, String> { if !hex.len().is_multiple_of(2) { return Err("hex blob literal must contain an even number of digits".to_string()); } let mut out = Vec::with_capacity(hex.len() / 2); let mut bytes = hex.as_bytes().iter().copied(); while let (Some(hi), Some(lo)) = (bytes.next(), bytes.next()) { let h = decode_hex_nibble(hi)?; let l = decode_hex_nibble(lo)?; out.push((h << 4) | l); } Ok(out) } fn decode_hex_nibble(byte: u8) -> Result { match byte { b'0'..=b'9' => Ok(byte - b'0'), b'a'..=b'f' => Ok(10 + (byte - b'a')), b'A'..=b'F' => Ok(10 + (byte - b'A')), _ => Err("hex blob literal contains non-hex characters".to_string()), } } fn unescape_single_quoted(s: &str) -> String { if !s.contains("''") { return s.to_owned(); } s.replace("''", "'") } impl Drop for Limbo { fn drop(&mut self) { self.save_history(); unsafe { ManuallyDrop::drop(&mut self.input_buff); } } } fn fetch_single_i64(rows: &mut turso_core::Statement) -> anyhow::Result { let mut result: Option = None; rows.run_with_row_callback(|row| { result = Some(row.get(0)?); Ok(()) })?; result.ok_or_else(|| anyhow!("query did not return a row")) } /// Normalize `path?key=val` to `file:path?key=val` so query parameters /// are parsed as URI options (e.g. `?locking=shared_reads`) instead of /// being treated as part of the filename. /// /// Only the *last* `?` that introduces a valid `key=value` query string is /// treated as the query separator. Earlier `?` characters are /// percent-encoded (`%3F`) so they remain part of the filename. /// A trailing `?` with no `key=value` pair is left alone (it is just part /// of the filename). fn normalize_db_path(db_file: String) -> String { if db_file.starts_with("file:") { return db_file; } // Walk from the right to find the last '?' whose suffix looks like // query parameters (contains at least one '='). if let Some(pos) = db_file.rfind('?') { let query = &db_file[pos + 1..]; if query.contains('=') { let path = &db_file[..pos]; // Percent-encode any '?' inside the path portion so the URI // parser does not mistake them for the query separator. let encoded_path = path.replace('?', "%3F"); return format!("file:{encoded_path}?{query}"); } } db_file } #[cfg(test)] mod tests { use super::*; #[test] fn test_normalize_db_path_adds_file_prefix_for_query_params() { assert_eq!( normalize_db_path("test.db?locking=shared_reads".into()), "file:test.db?locking=shared_reads" ); } #[test] fn test_normalize_db_path_preserves_existing_file_prefix() { assert_eq!( normalize_db_path("file:test.db?mode=ro".into()), "file:test.db?mode=ro" ); } #[test] fn test_normalize_db_path_preserves_file_triple_slash() { assert_eq!( normalize_db_path("file:///tmp/test.db?mode=ro".into()), "file:///tmp/test.db?mode=ro" ); } #[test] fn test_normalize_db_path_plain_path_unchanged() { assert_eq!(normalize_db_path("test.db".into()), "test.db"); } #[test] fn test_normalize_db_path_memory_unchanged() { assert_eq!(normalize_db_path(":memory:".into()), ":memory:"); } #[test] fn test_normalize_db_path_multiple_query_params() { assert_eq!( normalize_db_path("test.db?locking=shared_reads&cache=shared".into()), "file:test.db?locking=shared_reads&cache=shared" ); } #[test] fn test_normalize_db_path_absolute_path_with_query() { assert_eq!( normalize_db_path("/tmp/my.db?mode=ro".into()), "file:/tmp/my.db?mode=ro" ); } #[test] fn test_normalize_db_path_question_mark_in_filename_no_query() { // '?' is legitimately part of the filename, no key=value follows assert_eq!(normalize_db_path("what?.db".into()), "what?.db"); } #[test] fn test_normalize_db_path_filename_contains_question_mark_with_query() { // File is literally "foo.bar?mode=ro", opened with ?mode=ro query. // The '?' in the filename must be percent-encoded so the URI parser // treats only the last ?mode=ro as the query string. assert_eq!( normalize_db_path("foo.bar?mode=ro?mode=ro".into()), "file:foo.bar%3Fmode=ro?mode=ro" ); } } ================================================ FILE: cli/build.rs ================================================ //! Build.rs script to generate a binary syntax set for syntect //! based on the SQL.sublime-syntax file. use std::env; use std::path::Path; use syntect::dumps::dump_to_uncompressed_file; use syntect::parsing::SyntaxDefinition; use syntect::parsing::SyntaxSet; fn main() { println!("cargo::rerun-if-changed=SQL.sublime-syntax"); println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=manuals"); let out_dir = env::var_os("OUT_DIR").unwrap(); let syntax = SyntaxDefinition::load_from_str(include_str!("./SQL.sublime-syntax"), false, None).unwrap(); let mut ps = SyntaxSet::new().into_builder(); ps.add(syntax); let ps = ps.build(); dump_to_uncompressed_file( &ps, Path::new(&out_dir).join("SQL_syntax_set_dump.packdump"), ) .unwrap(); } ================================================ FILE: cli/commands/args.rs ================================================ use clap::{Args, ValueEnum}; use clap_complete::{ArgValueCompleter, CompletionCandidate, PathCompleter}; use crate::{input::OutputMode, opcodes_dictionary::OPCODE_DESCRIPTIONS}; #[derive(Debug, Clone, Args)] pub struct IndexesArgs { /// Name of table pub tbl_name: Option, } #[derive(Debug, Clone, Args)] pub struct ExitArgs { /// Exit code #[arg(default_value_t = 0)] pub code: i32, } #[derive(Debug, Clone, Args)] pub struct OpenArgs { /// Path to open database #[arg(add = ArgValueCompleter::new(PathCompleter::file()))] pub path: String, // TODO see how to have this completed with the output of List Vfs function // Currently not possible to pass arbitrary /// Name of VFS pub vfs_name: Option, } #[derive(Debug, Clone, Args)] pub struct SchemaArgs { // TODO depends on PRAGMA table_list for completions /// Table name to visualize schema pub table_name: Option, } #[derive(Debug, Clone, Args)] pub struct SetOutputArgs { /// File path to send output to #[arg(add = ArgValueCompleter::new(PathCompleter::file()))] pub path: Option, } #[derive(Debug, Clone, Args)] pub struct OutputModeArgs { #[arg(value_enum)] pub mode: OutputMode, } fn opcodes_completer(current: &std::ffi::OsStr) -> Vec { let mut completions = vec![]; let Some(current) = current.to_str() else { return completions; }; let current = current.to_lowercase(); let opcodes = &OPCODE_DESCRIPTIONS; for op in opcodes { // TODO if someone know how to do prefix_match with case insensitve in Rust // without converting the String to lowercase first, please fix this. let op_name = op.name.to_ascii_lowercase(); if op_name.starts_with(¤t) { completions.push(CompletionCandidate::new(op.name).help(Some(op.description.into()))); } } completions } #[derive(Debug, Clone, Args)] pub struct OpcodesArgs { /// Opcode to display description #[arg(add = ArgValueCompleter::new(opcodes_completer))] pub opcode: Option, } #[derive(Debug, Clone, Args)] pub struct CwdArgs { /// Target directory #[arg(add = ArgValueCompleter::new(PathCompleter::dir()))] pub directory: String, } #[derive(Debug, Clone, Args)] pub struct NullValueArgs { pub value: String, } #[derive(Debug, Clone, Args)] pub struct StatsArgs { /// Toggle stats mode: on or off #[arg(value_enum)] pub toggle: Option, /// Reset stats after displaying #[arg(long, short, default_value_t = false)] pub reset: bool, } #[derive(Debug, ValueEnum, Clone)] pub enum StatsToggle { /// Enable automatic stats display after each statement On, /// Disable automatic stats display Off, } #[derive(Debug, Clone, Args)] pub struct EchoArgs { #[arg(value_enum)] pub mode: EchoMode, } #[derive(Debug, ValueEnum, Clone)] pub enum EchoMode { On, Off, } #[derive(Debug, Clone, Args)] pub struct TablesArgs { pub pattern: Option, } #[derive(Debug, Clone, Args)] pub struct LoadExtensionArgs { /// Path to extension file #[arg(add = ArgValueCompleter::new(PathCompleter::file()))] pub path: String, } #[derive(Debug, ValueEnum, Clone)] pub enum TimerMode { On, Off, } #[derive(Debug, Clone, Args)] pub struct TimerArgs { #[arg(value_enum)] pub mode: TimerMode, } #[derive(Debug, ValueEnum, Clone)] pub enum DbConfigMode { On, Off, } #[derive(Debug, Clone, Args)] pub struct DbConfigArgs { pub config: Option, #[arg(value_enum)] pub mode: Option, } #[derive(Debug, Clone, Args)] pub struct HeadersArgs { pub mode: HeadersMode, } #[derive(Debug, Clone, Args)] pub struct CloneArgs { pub output_file: String, } #[derive(Debug, Clone, Args)] pub struct ManualArgs { /// The manual page to display (e.g., "mcp") pub page: Option, } #[derive(Debug, Clone, Args)] pub struct ReadArgs { /// Path to the SQL file to execute #[arg(add = ArgValueCompleter::new(PathCompleter::file()))] pub path: String, } #[derive(Debug, Clone, Args)] pub struct ParameterArgs { #[command(subcommand)] pub command: ParameterCommand, } #[derive(Debug, Clone, clap::Subcommand)] pub enum ParameterCommand { /// Set a parameter value Set(ParameterSetArgs), /// List all stored parameters List, /// Clear one parameter or all parameters Clear(ParameterClearArgs), } #[derive(Debug, Clone, Args)] pub struct ParameterSetArgs { /// Parameter name like :name, @name, $name, ?1 pub name: String, /// Parameter value pub value: String, } #[derive(Debug, Clone, Args)] pub struct ParameterClearArgs { /// Parameter name to clear. If omitted, clears all. pub name: Option, } #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] pub enum HeadersMode { On, Off, } #[derive(Debug, Clone, clap::Parser)] pub struct DbtotxtArgs { #[clap(long = "page")] pub page_no: Option, } ================================================ FILE: cli/commands/import.rs ================================================ use clap::Args; use clap_complete::{ArgValueCompleter, PathCompleter}; use std::{fs::File, io::Write, path::PathBuf, sync::Arc}; use turso_core::{Connection, LimboError}; #[derive(Debug, Clone, Args)] pub struct ImportArgs { /// Use , and \n as column and row separators #[arg(long, default_value = "true")] csv: bool, /// "Verbose" - increase auxiliary output #[arg(short, default_value = "false")] verbose: bool, /// Skip the first N rows of input #[arg(long, default_value = "0")] skip: u64, #[arg(add = ArgValueCompleter::new(PathCompleter::file()))] file: PathBuf, table: String, } pub struct ImportFile<'a> { conn: Arc, writer: &'a mut dyn Write, } impl<'a> ImportFile<'a> { pub fn new(conn: Arc, writer: &'a mut dyn Write) -> Self { Self { conn, writer } } pub fn import(&mut self, args: ImportArgs) { self.import_csv(args); } pub fn import_csv(&mut self, args: ImportArgs) { // Check if the target table exists let table_check_query = format!( "SELECT name FROM sqlite_master WHERE type='table' AND name='{}';", args.table ); let mut table_exists = false; match self.conn.query(table_check_query) { Ok(rows) => { if let Some(mut rows) = rows { let res = rows.run_with_row_callback(|_| { table_exists = true; Ok(()) }); if let Err(e) = res { let _ = self.writer.write_all( format!("Error checking table existence: {e:?}\n").as_bytes(), ); return; } } } Err(e) => { let _ = self .writer .write_all(format!("Error checking table existence: {e:?}\n").as_bytes()); return; } } let file = match File::open(args.file) { Ok(file) => file, Err(e) => { let _ = self.writer.write_all(format!("{e:?}\n").as_bytes()); return; } }; let mut rdr = csv::ReaderBuilder::new() .has_headers(false) .from_reader(file); let mut success_rows = 0u64; let mut failed_rows = 0u64; let mut records = rdr.records().skip(args.skip as usize).peekable(); // If table doesn't exist, use first row as header to create table if !table_exists { if let Some(Ok(header)) = records.next() { let columns = header .iter() .map(normalize_ident) .collect::>() .join(", "); let create_table = format!("CREATE TABLE {} ({});", args.table, columns); let rows = match self.conn.query(create_table) { Ok(rows) => rows, Err(e) => { let _ = self .writer .write_all(format!("Error creating table: {e:?}\n").as_bytes()); return; } }; let Some(mut rows) = rows else { let _ = self.writer.write_all(b"Error creating table\n"); return; }; let res = rows.run_with_row_callback(|_| { // Not expected for CREATE TABLE panic!("Unexpected row for CREATE TABLE"); }); match res { Ok(_) => {} Err(LimboError::Busy | LimboError::Interrupt) => { let _ = self .writer .write_all("Error creating table: interrupted / busy\n".as_bytes()); return; } Err(e) => { let _ = self.writer.write_all( format!("Error checking table existence: {e:?}\n").as_bytes(), ); return; } } } else { let _ = self.writer.write_all(b"Error: Empty input file\n"); return; } } /// TODO: should this be in a single transaction (i.e. all or nothing)? const CSV_INSERT_BATCH_SIZE: usize = 1000; let mut batch = Vec::with_capacity(CSV_INSERT_BATCH_SIZE); for result in records { let record = match result { Ok(r) => r, Err(e) => { failed_rows += 1; let _ = self .writer .write_all(format!("Error reading row: {e:?}\n").as_bytes()); continue; } }; if !record.is_empty() { let values: Vec = record .iter() .map(|r| format!("'{}'", r.replace("'", "''"))) .collect(); batch.push(values.join(",")); if batch.len() >= CSV_INSERT_BATCH_SIZE { println!("Inserting batch of {} rows", batch.len()); let insert_string = format!("INSERT INTO {} VALUES ({});", args.table, batch.join("),(")); match self.conn.query(insert_string) { Ok(rows) => { if let Some(mut rows) = rows { let res = rows.run_with_row_callback(|_| { panic!("Unexpected row for INSERT"); }); match res { Ok(_) => { success_rows += batch.len() as u64; } Err(LimboError::Interrupt) => { let _ = self.writer.write_all(b"interrupt\n"); failed_rows += batch.len() as u64; } Err(LimboError::Busy) => { let _ = self.writer.write_all(b"database is busy\n"); failed_rows += batch.len() as u64; } Err(e) => { let _ = self.writer.write_all( format!("Error executing query: {e:?}\n").as_bytes(), ); failed_rows += batch.len() as u64; } } } else { success_rows += batch.len() as u64; } } Err(e) => { let _ = self .writer .write_all(format!("Error executing query: {e:?}\n").as_bytes()); failed_rows += batch.len() as u64; } } batch.clear(); } } } // Insert remaining records if !batch.is_empty() { let insert_string = format!("INSERT INTO {} VALUES ({});", args.table, batch.join("),(")); match self.conn.query(insert_string) { Ok(rows) => { if let Some(mut rows) = rows { let res = rows.run_with_row_callback(|_| { panic!("Unexpected row for INSERT"); }); match res { Ok(_) => { success_rows += batch.len() as u64; } Err(LimboError::Interrupt) => { let _ = self.writer.write_all(b"interrupt\n"); failed_rows += batch.len() as u64; } Err(LimboError::Busy) => { let _ = self.writer.write_all(b"database is busy\n"); failed_rows += batch.len() as u64; } Err(e) => { let _ = self.writer.write_all( format!("Error executing query: {e:?}\n").as_bytes(), ); failed_rows += batch.len() as u64; } } } else { success_rows += batch.len() as u64; } } Err(e) => { let _ = self .writer .write_all(format!("Error executing query: {e:?}\n").as_bytes()); failed_rows += batch.len() as u64; } } } if args.verbose { let _ = self.writer.write_all( format!( "Added {} rows with {} errors using {} lines of input", success_rows, failed_rows, success_rows + failed_rows, ) .as_bytes(), ); } } } // https://sqlite.org/lang_keywords.html const QUOTE_PAIRS: &[(char, char)] = &[('"', '"'), ('[', ']'), ('`', '`')]; pub fn normalize_ident(identifier: &str) -> String { let quote_pair = QUOTE_PAIRS .iter() .find(|&(start, end)| identifier.starts_with(*start) && identifier.ends_with(*end)); if let Some(&(_, _)) = quote_pair { &identifier[1..identifier.len() - 1] } else { identifier } .to_lowercase() } ================================================ FILE: cli/commands/mod.rs ================================================ pub mod args; pub mod import; use args::{ CwdArgs, DbConfigArgs, DbtotxtArgs, EchoArgs, ExitArgs, HeadersArgs, IndexesArgs, LoadExtensionArgs, ManualArgs, NullValueArgs, OpcodesArgs, OpenArgs, OutputModeArgs, ParameterArgs, ReadArgs, SchemaArgs, SetOutputArgs, StatsArgs, TablesArgs, TimerArgs, }; use clap::Parser; use import::ImportArgs; use crate::{ commands::args::CloneArgs, input::{AFTER_HELP_MSG, BEFORE_HELP_MSG}, }; #[derive(Parser, Debug)] #[command( multicall = true, arg_required_else_help(false), before_help(BEFORE_HELP_MSG), after_help(AFTER_HELP_MSG), // help_template(HELP_TEMPLATE) )] pub struct CommandParser { #[command(subcommand)] pub command: Command, } #[derive(Debug, Clone, clap::Subcommand)] #[command(disable_help_flag(false), disable_version_flag(true))] pub enum Command { /// Exit this program with return-code CODE #[command(display_name = ".exit", alias = "ex", alias = "exi")] Exit(ExitArgs), /// Quit the shell #[command(display_name = ".quit", alias = "q", alias = "qu", alias = "qui")] Quit, /// Open a database file #[command(display_name = ".open")] Open(OpenArgs), /// Display schema for a table #[command(display_name = ".schema")] Schema(SchemaArgs), /// Set output file (or stdout if empty) #[command(name = "output", display_name = ".output")] SetOutput(SetOutputArgs), /// Set output display mode #[command(name = "mode", display_name = ".mode", arg_required_else_help(false))] OutputMode(OutputModeArgs), /// Show vdbe opcodes #[command(name = "opcodes", display_name = ".opcodes")] Opcodes(OpcodesArgs), /// Change the current working directory #[command(name = "cd", display_name = ".cd")] Cwd(CwdArgs), /// Display information about settings #[command(name = "show", display_name = ".show")] ShowInfo, /// Set the value of NULL to be printed in 'list' mode #[command(name = "nullvalue", display_name = ".nullvalue")] NullValue(NullValueArgs), /// Toggle 'echo' mode to repeat commands before execution #[command(display_name = ".echo")] Echo(EchoArgs), /// Display tables Tables(TablesArgs), /// Display attached databases Databases, /// Import data from FILE into TABLE #[command(name = "import", display_name = ".import")] Import(ImportArgs), /// Loads an extension library #[command(name = "load", display_name = ".load")] LoadExtension(LoadExtensionArgs), /// Dump the current database as a list of SQL statements Dump, /// Print or set the current configuration for the database. Currently ignored. #[command(name = "dbconfig", display_name = ".dbconfig")] DbConfig(DbConfigArgs), /// Display database statistics #[command(name = "stats", display_name = ".stats")] Stats(StatsArgs), /// List vfs modules available #[command(name = "vfslist", display_name = ".vfslist")] ListVfs, /// Show names of indexes #[command(name = "indexes", display_name = ".indexes")] ListIndexes(IndexesArgs), #[command(name = "timer", display_name = ".timer")] Timer(TimerArgs), /// Toggle column headers on/off in list mode #[command(name = "headers", display_name = ".headers")] Headers(HeadersArgs), #[command(name = "clone", display_name = ".clone")] Clone(CloneArgs), /// Display manual pages for features #[command(name = "manual", display_name = ".manual", alias = "man")] Manual(ManualArgs), /// Execute SQL statements from a file #[command(name = "read", display_name = ".read")] Read(ReadArgs), /// Manage SQL parameter bindings #[command(name = "parameter", display_name = ".parameter", alias = "param")] Parameter(ParameterArgs), #[command(name = "dbtotxt", display_name = ".dbtotxt")] Dbtotxt(DbtotxtArgs), } const _HELP_TEMPLATE: &str = "{before-help}{name} {usage-heading} {usage} {all-args}{after-help} "; #[cfg(test)] mod tests { use super::CommandParser; #[test] fn cli_assert() { use clap::CommandFactory; CommandParser::command().debug_assert(); } } ================================================ FILE: cli/config/mod.rs ================================================ mod palette; mod terminal; use crate::input::OutputMode; use crate::HOME_DIR; use nu_ansi_term::Color; use palette::LimboColor; use schemars::JsonSchema; use serde::{Deserialize, Deserializer}; use std::fmt::Debug; use std::fs::read_to_string; use std::path::PathBuf; use std::sync::LazyLock; use terminal::{TerminalDetector, TerminalTheme}; use validator::Validate; pub static CONFIG_DIR: LazyLock = LazyLock::new(|| HOME_DIR.join(".config/limbo")); fn ok_or_default<'de, T, D>(deserializer: D) -> Result where T: Deserialize<'de> + Default + Validate + Debug, D: Deserializer<'de>, { let v: toml::Value = Deserialize::deserialize(deserializer)?; let x = T::deserialize(v) .map(|v| { let validate = v.validate(); if validate.is_err() { tracing::error!( "Invalid value for {}.\n Original config value: {:?}", validate.unwrap_err(), v ); T::default() } else { v } }) .unwrap_or_default(); Ok(x) } #[derive(Debug, Deserialize, Clone, Default, JsonSchema)] #[serde(default, deny_unknown_fields)] pub struct Config { #[serde(deserialize_with = "ok_or_default")] pub table: TableConfig, pub highlight: HighlightConfig, } impl Config { pub fn from_config_file(path: PathBuf) -> Self { if let Some(config) = Self::read_config_str(path) { Self::from_config_str(&config) } else { Self::default() } } pub fn from_config_str(config: &str) -> Self { toml::from_str(config) .inspect_err(|err| tracing::error!("{}", err)) .unwrap_or_default() } fn read_config_str(path: PathBuf) -> Option { if path.exists() { tracing::trace!("Trying to read from {:?}", path); let result = read_to_string(path); if result.is_err() { tracing::debug!("Error reading file: {:?}", result); } else { tracing::trace!("File read successfully"); }; result.ok() } else { None } } pub fn for_output_mode(mode: OutputMode) -> Self { let table = if mode == OutputMode::Pretty { TableConfig::adaptive_colors() } else { TableConfig::no_colors() }; Self { table, highlight: HighlightConfig::default(), } } } #[derive(Debug, Deserialize, Clone, JsonSchema, Validate)] #[serde(default, deny_unknown_fields)] pub struct TableConfig { #[serde(default = "TableConfig::default_header_color")] pub header_color: LimboColor, #[serde(default = "TableConfig::default_column_colors")] #[validate(length(min = 1))] pub column_colors: Vec, } impl Default for TableConfig { fn default() -> Self { // Always use adaptive colors based on terminal theme Self::adaptive_colors() } } impl TableConfig { // These methods are needed for serde default attributes fn default_header_color() -> LimboColor { // Use adaptive colors for serde defaults too Self::adaptive_colors().header_color } fn default_column_colors() -> Vec { // Use adaptive colors for serde defaults too Self::adaptive_colors().column_colors } /// Get adaptive colors based on detected terminal theme pub fn adaptive_colors() -> Self { let theme = TerminalDetector::detect_theme(); match theme { TerminalTheme::Light => Self::light_theme_colors(), TerminalTheme::Dark => Self::dark_theme_colors(), TerminalTheme::Unknown => Self::no_colors(), // No colors for unsupported platforms } } /// No colors configuration - for Windows or when detection fails fn no_colors() -> Self { Self { header_color: LimboColor(Color::Default), column_colors: vec![LimboColor(Color::Default)], } } /// Colors optimized for light terminal backgrounds fn light_theme_colors() -> Self { Self { header_color: LimboColor(Color::Black), column_colors: vec![ LimboColor(Color::Fixed(22)), // Dark green LimboColor(Color::Fixed(17)), // Dark blue LimboColor(Color::Fixed(88)), // Dark red LimboColor(Color::Fixed(94)), // Orange LimboColor(Color::Fixed(55)), // Purple ], } } /// Colors optimized for dark terminal backgrounds fn dark_theme_colors() -> Self { Self { header_color: LimboColor(Color::LightGray), column_colors: vec![ LimboColor(Color::LightGreen), LimboColor(Color::LightBlue), LimboColor(Color::LightCyan), LimboColor(Color::LightYellow), LimboColor(Color::LightMagenta), ], } } } #[derive(Debug, Deserialize, Clone, JsonSchema)] #[serde(default, deny_unknown_fields)] pub struct HighlightConfig { pub enable: bool, pub theme: String, pub prompt: LimboColor, pub hint: LimboColor, pub candidate: LimboColor, } impl Default for HighlightConfig { fn default() -> Self { Self { enable: true, theme: "base16-ocean.dark".to_string(), prompt: LimboColor(Color::Rgb(34u8, 197u8, 94u8)), hint: LimboColor(Color::Rgb(107u8, 114u8, 128u8)), candidate: LimboColor(Color::Green), } } } ================================================ FILE: cli/config/palette.rs ================================================ use core::fmt; use std::{ fmt::Display, ops::{Deref, DerefMut}, }; use nu_ansi_term::Color; use schemars::JsonSchema; use serde::{ de::{self, Visitor}, Deserialize, Deserializer, Serialize, }; use tracing::trace; use validator::Validate; #[derive(Debug, Clone, Serialize)] pub struct LimboColor(pub Color); impl TryFrom<&str> for LimboColor { type Error = String; fn try_from(value: &str) -> Result { // Parse RGB hex values trace!("Parsing color_string: {}", value); let color = match value.chars().collect::>()[..] { // #rrggbb hex color ['#', r1, r2, g1, g2, b1, b2] => { let r = u8::from_str_radix(&format!("{r1}{r2}"), 16).map_err(|e| e.to_string())?; let g = u8::from_str_radix(&format!("{g1}{g2}"), 16).map_err(|e| e.to_string())?; let b = u8::from_str_radix(&format!("{b1}{b2}"), 16).map_err(|e| e.to_string())?; Some(Color::Rgb(r, g, b)) } // #rgb shorthand hex color ['#', r, g, b] => { let r = u8::from_str_radix(&format!("{r}{r}"), 16).map_err(|e| e.to_string())?; let g = u8::from_str_radix(&format!("{g}{g}"), 16).map_err(|e| e.to_string())?; let b = u8::from_str_radix(&format!("{b}{b}"), 16).map_err(|e| e.to_string())?; Some(Color::Rgb(r, g, b)) } // 0-255 color code [c1, c2, c3] => { if let Ok(ansi_color_num) = str::parse::(&format!("{c1}{c2}{c3}")) { Some(Color::Fixed(ansi_color_num)) } else { None } } [c1, c2] => { if let Ok(ansi_color_num) = str::parse::(&format!("{c1}{c2}")) { Some(Color::Fixed(ansi_color_num)) } else { None } } [c1] => { if let Ok(ansi_color_num) = str::parse::(&format!("{c1}")) { Some(Color::Fixed(ansi_color_num)) } else { None } } // unknown format _ => None, }; if let Some(color) = color { return Ok(LimboColor(color)); } // Check for any predefined color strings // There are no predefined enums for bright colors, so we use Color::Fixed let predefined_color = match value.to_lowercase().as_str() { "black" => Color::Black, "red" => Color::Red, "green" => Color::Green, "yellow" => Color::Yellow, "blue" => Color::Blue, "purple" => Color::Purple, "cyan" => Color::Cyan, "magenta" => Color::Magenta, "white" => Color::White, "bright-black" => Color::DarkGray, // "bright-black" is dark grey "bright-red" => Color::LightRed, "bright-green" => Color::LightGreen, "bright-yellow" => Color::LightYellow, "bright-blue" => Color::LightBlue, "bright-cyan" => Color::LightCyan, "birght-magenta" => Color::LightMagenta, "bright-white" => Color::LightGray, "dark-red" => Color::Fixed(1), "dark-green" => Color::Fixed(2), "dark-yellow" => Color::Fixed(3), "dark-blue" => Color::Fixed(4), "dark-magenta" => Color::Fixed(5), "dark-cyan" => Color::Fixed(6), "grey" => Color::Fixed(7), "dark-grey" => Color::Fixed(8), _ => return Err(format!("Could not parse color in string: {value}")), }; trace!("Read predefined color: {}", value); Ok(LimboColor(predefined_color)) } } impl Display for LimboColor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let val = match self.0 { Color::Black => "black".to_string(), Color::Red => "red".to_string(), Color::Green => "green".to_string(), Color::Yellow => "yellow".to_string(), Color::Blue => "blue".to_string(), Color::Purple => "purple".to_string(), Color::Cyan => "cyan".to_string(), Color::Magenta => "magenta".to_string(), Color::White => "white".to_string(), Color::DarkGray => "bright-black".to_string(), // "bright-black" is dark grey Color::LightRed => "bright-red".to_string(), Color::LightGreen => "bright-green".to_string(), Color::LightYellow => "bright-yellow".to_string(), Color::LightBlue => "bright-blue".to_string(), Color::LightCyan => "bright-cyan".to_string(), Color::LightMagenta | Color::LightPurple => "bright-magenta".to_string(), Color::LightGray => "bright-white".to_string(), Color::Fixed(1) => "dark-red".to_string(), Color::Fixed(2) => "dark-green".to_string(), Color::Fixed(3) => "dark-yellow".to_string(), Color::Fixed(4) => "dark-blue".to_string(), Color::Fixed(5) => "dark-magenta".to_string(), Color::Fixed(6) => "dark-cyan".to_string(), Color::Fixed(7) => "grey".to_string(), Color::Fixed(8) => "dark-grey".to_string(), Color::Rgb(r, g, b) => format!("#{r:x}{g:x}{b:X}"), Color::Fixed(ansi_color_num) => format!("{ansi_color_num}"), Color::Default => unreachable!(), }; write!(f, "{val}") } } impl From for LimboColor { fn from(value: comfy_table::Color) -> Self { let color = match value { comfy_table::Color::Rgb { r, g, b } => Color::Rgb(r, g, b), comfy_table::Color::AnsiValue(ansi_color_num) => Color::Fixed(ansi_color_num), comfy_table::Color::Black => Color::Black, comfy_table::Color::Red => Color::Red, comfy_table::Color::Green => Color::Green, comfy_table::Color::Yellow => Color::Yellow, comfy_table::Color::Blue => Color::Blue, comfy_table::Color::Cyan => Color::Cyan, comfy_table::Color::Magenta => Color::Magenta, comfy_table::Color::White => Color::White, comfy_table::Color::DarkRed => Color::Fixed(1), comfy_table::Color::DarkGreen => Color::Fixed(2), comfy_table::Color::DarkYellow => Color::Fixed(3), comfy_table::Color::DarkBlue => Color::Fixed(4), comfy_table::Color::DarkMagenta => Color::Fixed(5), comfy_table::Color::DarkCyan => Color::Fixed(6), comfy_table::Color::Grey => Color::Fixed(7), comfy_table::Color::DarkGrey => Color::Fixed(8), comfy_table::Color::Reset => unreachable!(), // Should never have Reset Color here }; LimboColor(color) } } impl<'de> Deserialize<'de> for LimboColor { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct LimboColorVisitor; impl Visitor<'_> for LimboColorVisitor { type Value = LimboColor; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("struct LimboColor") } fn visit_str(self, v: &str) -> Result where E: de::Error, { LimboColor::try_from(v).map_err(de::Error::custom) } } deserializer.deserialize_str(LimboColorVisitor) } } impl JsonSchema for LimboColor { fn schema_name() -> String { "LimboColor".into() } fn schema_id() -> std::borrow::Cow<'static, str> { // Include the module, in case a type with the same name is in another module/crate std::borrow::Cow::Borrowed(concat!(module_path!(), "::LimboColor")) } fn json_schema(generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { generator.subschema_for::() } } impl Deref for LimboColor { type Target = Color; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for LimboColor { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl Validate for LimboColor { fn validate(&self) -> Result<(), validator::ValidationErrors> { Ok(()) } } impl LimboColor { pub fn as_comfy_table_color(&self) -> comfy_table::Color { match self.0 { Color::Black => comfy_table::Color::Black, Color::Red => comfy_table::Color::Red, Color::Green => comfy_table::Color::Green, Color::Yellow => comfy_table::Color::Yellow, Color::Blue => comfy_table::Color::Blue, Color::Magenta | Color::Purple => comfy_table::Color::Magenta, Color::Cyan => comfy_table::Color::Cyan, Color::White | Color::Default => comfy_table::Color::White, Color::Fixed(1) => comfy_table::Color::DarkRed, Color::Fixed(2) => comfy_table::Color::DarkGreen, Color::Fixed(3) => comfy_table::Color::DarkYellow, Color::Fixed(4) => comfy_table::Color::DarkBlue, Color::Fixed(5) => comfy_table::Color::DarkMagenta, Color::Fixed(6) => comfy_table::Color::DarkCyan, Color::Fixed(7) => comfy_table::Color::Grey, Color::Fixed(8) => comfy_table::Color::DarkGrey, Color::DarkGray => comfy_table::Color::AnsiValue(241), Color::LightRed => comfy_table::Color::AnsiValue(9), Color::LightGreen => comfy_table::Color::AnsiValue(10), Color::LightYellow => comfy_table::Color::AnsiValue(11), Color::LightBlue => comfy_table::Color::AnsiValue(12), Color::LightMagenta | Color::LightPurple => comfy_table::Color::AnsiValue(13), Color::LightCyan => comfy_table::Color::AnsiValue(14), Color::LightGray => comfy_table::Color::AnsiValue(15), Color::Rgb(r, g, b) => comfy_table::Color::Rgb { r, g, b }, Color::Fixed(ansi_color_num) => comfy_table::Color::AnsiValue(ansi_color_num), } } } ================================================ FILE: cli/config/terminal.rs ================================================ #[cfg(unix)] use std::io::{self, IsTerminal, Read, Write}; #[cfg(unix)] use std::time::Duration; #[derive(Debug, Clone, Copy, PartialEq)] pub enum TerminalTheme { Light, Dark, Unknown, // No colors - can't detect or unsupported platform } pub struct TerminalDetector; #[cfg(target_os = "windows")] impl TerminalDetector { /// Windows: Always return Unknown (no colors) /// Terminal detection is unreliable on Windows, so we disable colors entirely pub fn detect_theme() -> TerminalTheme { TerminalTheme::Unknown } } #[cfg(unix)] impl TerminalDetector { /// Detects terminal background using ANSI escape sequences on Unix systems pub fn detect_theme() -> TerminalTheme { // Only works on interactive terminals where both stdin and stdout are terminals if !io::stdin().is_terminal() || !io::stdout().is_terminal() { return TerminalTheme::Unknown; // No colors for non-interactive } // Try ANSI escape sequence method if let Some(theme) = Self::detect_via_ansi_query() { return theme; } // Fallback - return Unknown (no colors) if detection fails TerminalTheme::Unknown } /// Query terminal background color using ANSI escape sequence OSC 11 fn detect_via_ansi_query() -> Option { // Save current terminal settings let original_termios = Self::save_terminal_settings()?; // Set terminal to raw mode Self::set_raw_mode()?; // Send query and read response let theme = Self::query_background_color(); // Restore terminal settings Self::restore_terminal_settings(&original_termios); theme } /// Save current terminal settings fn save_terminal_settings() -> Option { use std::os::unix::io::AsRawFd; let stdin_fd = io::stdin().as_raw_fd(); let mut termios = unsafe { std::mem::zeroed::() }; unsafe { if libc::tcgetattr(stdin_fd, &mut termios) == 0 { Some(termios) } else { None } } } /// Set terminal to raw mode fn set_raw_mode() -> Option<()> { use std::os::unix::io::AsRawFd; let stdin_fd = io::stdin().as_raw_fd(); let mut termios = unsafe { std::mem::zeroed::() }; unsafe { if libc::tcgetattr(stdin_fd, &mut termios) != 0 { return None; } // Set raw mode: disable canonical mode, echo, and signals termios.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG); termios.c_iflag &= !(libc::IXON | libc::ICRNL); termios.c_oflag &= !libc::OPOST; // Set minimum characters to read and timeout termios.c_cc[libc::VMIN] = 0; termios.c_cc[libc::VTIME] = 1; // 0.1 second timeout if libc::tcsetattr(stdin_fd, libc::TCSANOW, &termios) == 0 { Some(()) } else { None } } } /// Restore terminal settings fn restore_terminal_settings(original: &libc::termios) { use std::os::unix::io::AsRawFd; let stdin_fd = io::stdin().as_raw_fd(); unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, original); } } /// Send background color query and read response fn query_background_color() -> Option { // Send OSC 11 query: ESC ] 11 ; ? ESC \ print!("\x1b]11;?\x1b\\"); io::stdout().flush().ok()?; // Read response with timeout let mut buffer = [0u8; 256]; let mut total_read = 0; // Try to read response for up to 500ms let start_time = std::time::Instant::now(); while start_time.elapsed() < Duration::from_millis(500) { match io::stdin().read(&mut buffer[total_read..]) { Ok(0) => { // No data available, sleep briefly and continue std::thread::sleep(Duration::from_millis(10)); continue; } Ok(bytes_read) => { total_read += bytes_read; let response = String::from_utf8_lossy(&buffer[..total_read]); // Look for end of response (ESC \ or BEL) if response.contains('\x07') || response.contains("\x1b\\") { return Self::parse_ansi_color_response(&response); } // Prevent buffer overflow if total_read >= buffer.len() - 1 { break; } } Err(_) => { // Error reading, sleep briefly and continue std::thread::sleep(Duration::from_millis(10)); } } } None } /// Parse ANSI color response to determine if background is light or dark fn parse_ansi_color_response(response: &str) -> Option { // Look for patterns like: ]11;rgb:RRRR/GGGG/BBBB or ]11;#RRGGBB // Try hex format first: ]11;#RRGGBB if let Some(start) = response.find("]11;#") { let color_part = &response[start + 5..]; if let Some(hex_end) = color_part.find(|c: char| !c.is_ascii_hexdigit()) { let hex_color = &color_part[..hex_end]; if hex_color.len() >= 6 { return Self::parse_hex_color(&hex_color[..6]); } } } // Try rgb: format: ]11;rgb:RRRR/GGGG/BBBB if let Some(start) = response.find("rgb:") { let color_part = &response[start + 4..]; // Parse RGB values (format: RRRR/GGGG/BBBB) let parts: Vec<&str> = color_part.split('/').take(3).collect(); if parts.len() == 3 { if let (Ok(r), Ok(g), Ok(b)) = ( u16::from_str_radix(&parts[0][..parts[0].len().min(4)], 16), u16::from_str_radix(&parts[1][..parts[1].len().min(4)], 16), u16::from_str_radix(&parts[2][..parts[2].len().min(4)], 16), ) { // Convert to 0-255 range let r = (r >> 8) as u8; let g = (g >> 8) as u8; let b = (b >> 8) as u8; return Some(Self::classify_color_brightness(r, g, b)); } } } None } /// Parse hex color format (#RRGGBB) fn parse_hex_color(hex: &str) -> Option { if hex.len() != 6 { return None; } let r = u8::from_str_radix(&hex[0..2], 16).ok()?; let g = u8::from_str_radix(&hex[2..4], 16).ok()?; let b = u8::from_str_radix(&hex[4..6], 16).ok()?; Some(Self::classify_color_brightness(r, g, b)) } /// Classify color brightness using perceived luminance fn classify_color_brightness(r: u8, g: u8, b: u8) -> TerminalTheme { // Use ITU-R BT.709 luma coefficients for perceived brightness let luminance = 0.2126 * r as f32 + 0.7152 * g as f32 + 0.0722 * b as f32; // Threshold around 128 (middle gray) if luminance > 128.0 { TerminalTheme::Light } else { TerminalTheme::Dark } } } #[cfg(test)] mod tests { use super::*; #[cfg(unix)] mod unix_tests { use super::*; #[test] fn test_hex_color_parsing() { // Test light colors assert_eq!( TerminalDetector::parse_hex_color("ffffff"), Some(TerminalTheme::Light) ); assert_eq!( TerminalDetector::parse_hex_color("f0f0f0"), Some(TerminalTheme::Light) ); // Test dark colors assert_eq!( TerminalDetector::parse_hex_color("000000"), Some(TerminalTheme::Dark) ); assert_eq!( TerminalDetector::parse_hex_color("202020"), Some(TerminalTheme::Dark) ); // Test invalid input assert_eq!(TerminalDetector::parse_hex_color("invalid"), None); assert_eq!(TerminalDetector::parse_hex_color("12345"), None); } #[test] fn test_brightness_classification() { // Pure white assert_eq!( TerminalDetector::classify_color_brightness(255, 255, 255), TerminalTheme::Light ); // Pure black assert_eq!( TerminalDetector::classify_color_brightness(0, 0, 0), TerminalTheme::Dark ); // Medium gray (should be close to threshold) assert_eq!( TerminalDetector::classify_color_brightness(128, 128, 128), TerminalTheme::Dark // Slightly below threshold ); // Light gray assert_eq!( TerminalDetector::classify_color_brightness(200, 200, 200), TerminalTheme::Light ); } #[test] fn test_ansi_response_parsing() { // Test hex format response let hex_response = "\x1b]11;#ffffff\x1b\\"; assert_eq!( TerminalDetector::parse_ansi_color_response(hex_response), Some(TerminalTheme::Light) ); // Test rgb format response let rgb_response = "\x1b]11;rgb:0000/0000/0000\x1b\\"; assert_eq!( TerminalDetector::parse_ansi_color_response(rgb_response), Some(TerminalTheme::Dark) ); // Test invalid response let invalid_response = "invalid response"; assert_eq!( TerminalDetector::parse_ansi_color_response(invalid_response), None ); } } } ================================================ FILE: cli/docs/config.md ================================================ # Config Config folder should be located at `$HOME/.config/limbo`. The config file inside should be named `limbo.toml`. Optionally you can have a `themes` folder whithin to store `.tmTheme` files to be discovered by the CLI on startup. Describes the Limbo Config file for the CLI\ **Note**: Colors can be inputted as - Rrggbb string -> `"#010101"` - Rgb string -> `"#A3F"` - 256 Ansi Color -> `"100"` - Predefined Color Names: - `"black"` - `"red"` - `"green"` - `"yellow"` - `"blue"` - `"purple"` - `"cyan"` - `"magenta"` - `"white"` - `"grey"` - `"bright-black"` - `"bright-red"` - `"bright-green"` - `"bright-yellow"` - `"bright-blue"` - `"bright-cyan"` - `"bright-magenta"` - `"bright-white"` - `"dark-red"` - `"dark-green"` - `"dark-yellow"` - `"dark-blue"` - `"dark-magenta"` - `"dark-cyan"` - `"dark-grey"` ## `table` ### `column_colors` **Type**: `List[Color]`\ *Example*: `["cyan"]` ### `header_color` **Type**: `Color`\ *Example*: `"red"` ## `highlight` ### `enable` **Type**: `bool`\ *Example*: `true` ### `theme` **Type**: `String`\ *Example*: `"base16-ocean.dark"` Preloaded themes: - `base16-ocean.dark` - `base16-eighties.dark` - `base16-mocha.dark` - `base16-ocean.light` You can reference a custom theme in your `themes` folder directly by name from the config file. *Example*: Folder structure ``` limbo ├── limbo.toml └── themes └── Amy.tmTheme ``` `limbo.toml` ```toml [highlight] theme = "Amy" ``` ### `prompt` **Type**: `Color`\ *Example*: `"green"` ### `hint` **Type**: `Color`\ *Example*: `"grey"` ### `candidate` **Type**: `Color`\ *Example*: `"yellow"` ## Example `limbo.toml` ```toml [table] column_colors = ["cyan", "black", "#010101"] header_color = "red" [highlight] enable = true prompt = "bright-blue" theme = "base16-ocean.light" hint = "123" candidate = "dark-yellow" ``` ================================================ FILE: cli/docs/internal/commands.md ================================================ # Cli Internal Docs ## Repl Custom Commands Arg Parser To distinguish between normal SQL queries and custom commands, we prefix a "." before our desired command. This signals to the to the REPL that you intend to use a custom command. To implement this we use CLAP with multicall. It is very important we use multicall, else this will not work ## Adding new Commands To add new commands, we need to modify three places: - `commands/mod.rs` - `commands/args.rs` or create a new file under `commands` that will describe how you will use the Args for your command - `app.rs` to handle the execution of the command `commands/mod.rs` ```rust pub enum Command { ... /// Descriptive Message for your command Example(ExampleArgs), } ``` `commands/args.rs` ```rust #[derive(Debug, Clone, Args)] pub struct ExampleArgs { /// Example arg pub example: String, } ``` `app.rs` ```rust pub fn handle_dot_command(&mut self, line: &str) { ... Ok(cmd) => match cmd.command { ... Command::Example(args) => { println!("{}", args.example); } } } ``` Every single option that is available to CLAP is available here. To facilitate the creation of more helpful help messages, please use '///' in your args and command creation, so that CLAP can capture them in the codegen and create their descriptions. ================================================ FILE: cli/helper.rs ================================================ use clap::Parser; use nu_ansi_term::{Color, Style}; use rustyline::completion::{extract_word, Completer, Pair}; use rustyline::highlight::Highlighter; use rustyline::hint::HistoryHinter; use rustyline::{Completer, Helper, Hinter, Validator}; use shlex::Shlex; use std::cell::RefCell; use std::marker::PhantomData; use std::sync::Arc; use std::{ffi::OsString, path::PathBuf, str::FromStr as _}; use syntect::dumps::from_uncompressed_data; use syntect::easy::HighlightLines; use syntect::highlighting::ThemeSet; use syntect::parsing::{Scope, SyntaxSet}; use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings}; use turso_core::Connection; use crate::commands::CommandParser; use crate::config::{HighlightConfig, CONFIG_DIR}; macro_rules! try_result { ($expr:expr, $err:expr) => { match $expr { Ok(val) => val, Err(_) => return Ok($err), } }; } #[derive(Helper, Completer, Hinter, Validator)] pub struct LimboHelper { #[rustyline(Completer)] completer: SqlCompleter, syntax_set: SyntaxSet, theme_set: ThemeSet, syntax_config: HighlightConfig, #[rustyline(Hinter)] hinter: HistoryHinter, } impl LimboHelper { pub fn new(conn: Arc, syntax_config: Option) -> Self { // Load only predefined syntax let ps = from_uncompressed_data(include_bytes!(concat!( env!("OUT_DIR"), "/SQL_syntax_set_dump.packdump" ))) .unwrap(); let mut ts = ThemeSet::load_defaults(); let theme_dir = CONFIG_DIR.join("themes"); if theme_dir.exists() { if let Err(err) = ts.add_from_folder(theme_dir) { tracing::error!("{err}"); } } LimboHelper { completer: SqlCompleter::new(conn), syntax_set: ps, theme_set: ts, syntax_config: syntax_config.unwrap_or_default(), hinter: HistoryHinter::new(), } } } impl Highlighter for LimboHelper { fn highlight<'l>(&self, line: &'l str, pos: usize) -> std::borrow::Cow<'l, str> { let _ = pos; if self.syntax_config.enable { // TODO use lifetimes to store highlight lines let syntax = self .syntax_set .find_syntax_by_scope(Scope::new("source.sql").unwrap()) .unwrap(); let theme = self .theme_set .themes .get(&self.syntax_config.theme) .unwrap_or(&self.theme_set.themes["base16-ocean.dark"]); let mut h = HighlightLines::new(syntax, theme); let ranges = { let mut ret_ranges = Vec::new(); for new_line in LinesWithEndings::from(line) { let ranges: Vec<(syntect::highlighting::Style, &str)> = h.highlight_line(new_line, &self.syntax_set).unwrap(); ret_ranges.extend(ranges); } ret_ranges }; let mut ret_line = as_24_bit_terminal_escaped(&ranges[..], false); // Push this escape sequence to reset terminal color modes at the end of the string ret_line.push_str("\x1b[0m"); std::borrow::Cow::Owned(ret_line) } else { // Appease Pekka in syntax highlighting let style = Style::new().fg(Color::White); // Standard shell text color let styled_str = style.paint(line); std::borrow::Cow::Owned(styled_str.to_string()) } } fn highlight_prompt<'b, 's: 'b, 'p: 'b>( &'s self, prompt: &'p str, default: bool, ) -> std::borrow::Cow<'b, str> { let _ = default; // Dark emerald green for prompt let style = Style::new().bold().fg(self.syntax_config.prompt.0); let styled_str = style.paint(prompt); std::borrow::Cow::Owned(styled_str.to_string()) } fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> { let style = Style::new().bold().fg(self.syntax_config.hint.0); // Brighter dark grey for hints let styled_str = style.paint(hint); std::borrow::Cow::Owned(styled_str.to_string()) } fn highlight_candidate<'c>( &self, candidate: &'c str, completion: rustyline::CompletionType, ) -> std::borrow::Cow<'c, str> { let _ = completion; let style = Style::new().fg(self.syntax_config.candidate.0); let styled_str = style.paint(candidate); std::borrow::Cow::Owned(styled_str.to_string()) } fn highlight_char(&self, line: &str, pos: usize, kind: rustyline::highlight::CmdKind) -> bool { let _ = (line, pos); !matches!(kind, rustyline::highlight::CmdKind::MoveCursor) } } pub struct SqlCompleter { conn: Arc, // Has to be a ref cell as Rustyline takes immutable reference to self // This problem would be solved with Reedline as it uses &mut self for completions cmd: RefCell, _cmd_phantom: PhantomData, } impl SqlCompleter { pub fn new(conn: Arc) -> Self { Self { conn, cmd: C::command().into(), _cmd_phantom: PhantomData, } } fn dot_completion( &self, mut line: &str, mut pos: usize, ) -> rustyline::Result<(usize, Vec)> { // TODO maybe check to see if the line is empty and then just output the command names line = &line[1..]; pos -= 1; let (prefix_pos, _) = extract_word(line, pos, ESCAPE_CHAR, default_break_chars); let args = Shlex::new(line); let mut args = std::iter::once("".to_owned()) .chain(args) .map(OsString::from) .collect::>(); if line.ends_with(' ') { args.push(OsString::new()); } let arg_index = args.len() - 1; // dbg!(&pos, line, &args, arg_index); let mut cmd = self.cmd.borrow_mut(); match clap_complete::engine::complete( &mut cmd, args, arg_index, PathBuf::from_str(".").ok().as_deref(), ) { Ok(candidates) => { let candidates = candidates .iter() .map(|candidate| Pair { display: candidate.get_value().to_string_lossy().into_owned(), replacement: candidate.get_value().to_string_lossy().into_owned(), }) .collect::>(); Ok((prefix_pos + 1, candidates)) } Err(_) => Ok((prefix_pos + 1, Vec::new())), } } fn sql_completion(&self, line: &str, pos: usize) -> rustyline::Result<(usize, Vec)> { // TODO: have to differentiate words if they are enclosed in single of double quotes let (prefix_pos, prefix) = extract_word(line, pos, ESCAPE_CHAR, default_break_chars); let mut candidates = Vec::new(); let query = try_result!( self.conn.query(format!( "SELECT candidate FROM completion('{prefix}', '{line}') ORDER BY 1;" )), (prefix_pos, candidates) ); if let Some(mut rows) = query { try_result!( rows.run_with_row_callback(|row| { let completion: &str = row.get::<&str>(0)?; let pair = Pair { display: completion.to_string(), replacement: completion.to_string(), }; candidates.push(pair); Ok(()) }), (prefix_pos, candidates) ); } Ok((prefix_pos, candidates)) } } // Got this from the FilenameCompleter. // TODO have to see what chars break words in Sqlite cfg_if::cfg_if! { if #[cfg(unix)] { // rl_basic_word_break_characters, rl_completer_word_break_characters const fn default_break_chars(c : char) -> bool { matches!(c, ' ' | '\t' | '\n' | '"' | '\\' | '\'' | '`' | '@' | '$' | '>' | '<' | '=' | ';' | '|' | '&' | '{' | '(' | '\0') } const ESCAPE_CHAR: Option = Some('\\'); // In double quotes, not all break_chars need to be escaped // https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html #[allow(dead_code)] const fn double_quotes_special_chars(c: char) -> bool { matches!(c, '"' | '$' | '\\' | '`') } } else if #[cfg(windows)] { // Remove \ to make file completion works on windows const fn default_break_chars(c: char) -> bool { matches!(c, ' ' | '\t' | '\n' | '"' | '\'' | '`' | '@' | '$' | '>' | '<' | '=' | ';' | '|' | '&' | '{' | '(' | '\0') } const ESCAPE_CHAR: Option = None; #[allow(dead_code)] const fn double_quotes_special_chars(c: char) -> bool { c == '"' } // TODO Validate: only '"' ? } else if #[cfg(target_arch = "wasm32")] { const fn default_break_chars(c: char) -> bool { false } const ESCAPE_CHAR: Option = None; #[allow(dead_code)] const fn double_quotes_special_chars(c: char) -> bool { false } } } impl Completer for SqlCompleter { type Candidate = Pair; fn complete( &self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>, ) -> rustyline::Result<(usize, Vec)> { if line.starts_with(".") { self.dot_completion(line, pos) } else { self.sql_completion(line, pos) } } } ================================================ FILE: cli/input.rs ================================================ use crate::app::Opts; use clap::ValueEnum; use std::{ fmt::{Display, Formatter}, io::{self, Write}, sync::Arc, }; use turso_core::LimboError; #[derive(Copy, Clone)] pub enum DbLocation { Memory, Path, } #[allow(clippy::enum_variant_names)] #[derive(Clone, Debug)] pub enum Io { Syscall, #[cfg(all(target_os = "linux", feature = "io_uring"))] IoUring, #[cfg(all(target_os = "windows", feature = "experimental_win_iocp"))] WindowsIOCP, External(String), Memory, } impl Display for Io { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Io::Memory => write!(f, "memory"), Io::Syscall => write!(f, "syscall"), #[cfg(all(target_os = "linux", feature = "io_uring"))] Io::IoUring => write!(f, "io_uring"), #[cfg(all(target_os = "windows", feature = "experimental_win_iocp"))] Io::WindowsIOCP => write!(f, "experimental_win_iocp"), Io::External(str) => write!(f, "{str}"), } } } impl Default for Io { /// Custom Default impl with cfg! macro, to provide compile-time default to Clap based on platform /// The cfg! could be elided, but Clippy complains /// The default value can still be overridden with the Clap argument fn default() -> Self { match cfg!(all(target_os = "linux", feature = "io_uring")) { true => { #[cfg(all(target_os = "linux", feature = "io_uring"))] { Io::Syscall // FIXME: make io_uring faster so it can be the default } #[cfg(any( not(target_os = "linux"), all(target_os = "linux", not(feature = "io_uring")) ))] { Io::Syscall } } false => Io::Syscall, } } } #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] pub enum OutputMode { List, Pretty, Line, } impl std::fmt::Display for OutputMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.to_possible_value() .expect("no values are skipped") .get_name() .fmt(f) } } pub struct Settings { pub output_filename: String, pub db_file: String, pub null_value: String, pub output_mode: OutputMode, pub echo: bool, pub is_stdout: bool, pub io: Io, pub timer: bool, pub headers: bool, pub mcp: bool, pub sync_server_address: Option, pub stats: bool, } impl From for Settings { fn from(opts: Opts) -> Self { Self { null_value: String::new(), output_mode: opts.output_mode, echo: false, is_stdout: opts.output.is_empty(), output_filename: opts.output, db_file: opts .database .as_ref() .map_or(":memory:".to_string(), |p| p.to_string_lossy().to_string()), io: match opts.vfs.as_ref().unwrap_or(&String::new()).as_str() { "memory" | ":memory:" => Io::Memory, "syscall" => Io::Syscall, #[cfg(all(target_os = "linux", feature = "io_uring"))] "io_uring" => Io::IoUring, #[cfg(all(target_os = "windows", feature = "experimental_win_iocp"))] "experimental_win_iocp" => Io::WindowsIOCP, "" => Io::default(), vfs => Io::External(vfs.to_string()), }, timer: false, headers: false, mcp: opts.mcp, sync_server_address: opts.sync_server, stats: false, } } } impl std::fmt::Display for Settings { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Settings:\nOutput mode: {}\nDB: {}\nOutput: {}\nNull value: {}\nCWD: {}\nEcho: {}\nHeaders: {}", self.output_mode, self.db_file, match self.is_stdout { true => "STDOUT", false => &self.output_filename, }, self.null_value, std::env::current_dir().unwrap().display(), match self.echo { true => "on", false => "off", }, match self.headers { true => "on", false => "off", } ) } } pub fn get_writer(output: &str) -> Box { match output { "" => Box::new(io::stdout()), _ => match std::fs::File::create(output) { Ok(file) => Box::new(file), Err(e) => { eprintln!("Error: {e}"); Box::new(io::stdout()) } }, } } pub fn get_io(db_location: DbLocation, io_choice: &str) -> anyhow::Result> { Ok(match db_location { DbLocation::Memory => Arc::new(turso_core::MemoryIO::new()), DbLocation::Path => { match io_choice { "memory" => Arc::new(turso_core::MemoryIO::new()), "syscall" => { // We are building for Linux/macOS and syscall backend has been selected #[cfg(target_family = "unix")] { Arc::new(turso_core::UnixIO::new()?) } // We are not building for Linux/macOS and syscall backend has been selected #[cfg(not(target_family = "unix"))] { Arc::new(turso_core::PlatformIO::new()?) } } // We are building for Linux and io_uring backend has been selected #[cfg(all(target_os = "linux", feature = "io_uring"))] "io_uring" => Arc::new(turso_core::UringIO::new()?), #[cfg(all(target_os = "windows", feature = "experimental_win_iocp"))] "experimental_win_iocp" => Arc::new(turso_core::WindowsIOCP::new()?), _ => Arc::new(turso_core::PlatformIO::new()?), } } }) } pub struct ApplyWriter<'a> { target: &'a Arc, // accumulate raw bytes to support non-utf8 BLOB types buf: Vec, } impl<'a> ApplyWriter<'a> { pub fn new(target: &'a Arc) -> Self { Self { target, buf: Vec::new(), } } // Find the next statement terminator ;\n or ;\r\n in a byte buffer. // Returns (end_idx_inclusive, drain_len), where drain_len includes the newline(s). fn find_stmt_end(buf: &[u8]) -> Option<(usize, usize)> { let mut i = 0; while i < buf.len() { // Look for ';' if buf[i] == b';' { // Accept ;\n if i + 1 < buf.len() && buf[i + 1] == b'\n' { return Some((i, 2)); } // Accept ;\r\n if i + 2 < buf.len() && buf[i + 1] == b'\r' && buf[i + 2] == b'\n' { return Some((i, 3)); } } i += 1; } None } pub fn flush_complete_statements(&mut self) -> io::Result<()> { while let Some((end_inclusive, drain_len)) = Self::find_stmt_end(&self.buf) { // Copy stmt bytes [0..=end_inclusive] let stmt_bytes = self.buf[..=end_inclusive].to_vec(); // Drain including the trailing newline(s) self.buf.drain(..end_inclusive + drain_len); self.exec_stmt_bytes(&stmt_bytes)?; } Ok(()) } // Handle final trailing statement that ends with ';' followed only by ASCII whitespace. pub fn finish(mut self) -> io::Result<()> { // Skip if buffer empty or no ';' if let Some(semicolon_pos) = self.buf.iter().rposition(|&b| b == b';') { // Are all bytes after ';' ASCII whitespace? if self.buf[semicolon_pos + 1..] .iter() .all(|&b| matches!(b, b' ' | b'\t' | b'\r' | b'\n')) { let stmt_bytes = self.buf[..=semicolon_pos].to_vec(); self.buf.clear(); self.exec_stmt_bytes(&stmt_bytes)?; } } Ok(()) } fn exec_stmt_bytes(&self, stmt_bytes: &[u8]) -> io::Result<()> { // SQL must be UTF-8. If not, surface a clear error. let sql = std::str::from_utf8(stmt_bytes).map_err(|e| { io::Error::new(io::ErrorKind::InvalidData, format!("non-UTF8 SQL: {e}")) })?; self.exec_stmt(sql) .map_err(|e| io::Error::other(e.to_string())) } fn exec_stmt(&self, sql: &str) -> Result<(), LimboError> { match self.target.query(sql) { Ok(Some(mut rows)) => { rows.run_with_row_callback(|_| Ok(()))?; } Ok(None) => {} Err(e) => return Err(e), } Ok(()) } } impl<'a> Write for ApplyWriter<'a> { fn write(&mut self, data: &[u8]) -> io::Result { self.buf.extend_from_slice(data); self.flush_complete_statements()?; Ok(data.len()) } fn flush(&mut self) -> io::Result<()> { self.flush_complete_statements() } } pub trait ProgressSink { fn on(&mut self, _p: S) {} } pub struct NoopProgress; impl ProgressSink for NoopProgress {} pub struct StderrProgress; impl ProgressSink for StderrProgress { fn on(&mut self, s: S) { eprintln!("{s}... done"); } } pub const BEFORE_HELP_MSG: &str = r#" Turso SQL Shell Help ============== Welcome to the Turso SQL Shell! You can execute any standard SQL command here. In addition to standard SQL commands, the following special commands are available:"#; pub const AFTER_HELP_MSG: &str = r#"Usage Examples: --------------- 1. To quit the Turso SQL Shell: .quit 2. To open a database file at path './employees.db': .open employees.db 3. To view the schema of a table named 'employees': .schema employees 4. To list all tables: .tables 5. To list all databases: .databases 6. To list all available SQL opcodes: .opcodes 7. To change the current output mode to 'pretty': .mode pretty 8. Send output to STDOUT if no file is specified: .output 9. To change the current working directory to '/tmp': .cd /tmp 10. Show the current values of settings: .show 11. To import csv file 'sample.csv' into 'csv_table' table: .import --csv sample.csv csv_table 12. To display the database contents as SQL: .dump 13. To load an extension library: .load /target/debug/liblimbo_regexp 14. To list all available VFS: .listvfs 15. To show names of indexes: .indexes ?TABLE? 16. To turn on column headers in list mode: .headers on 17. To turn off column headers in list mode: .headers off 18. To clone the open database to another file: .clone output_file.db 19. To view manual pages for features: .manual mcp # View MCP server documentation .man # List all available manuals 20. To bind parameters for subsequent SQL statements: .parameter set :name alice .parameter list .parameter clear :name Note: - All SQL commands must end with a semicolon (;). - Special commands start with a dot (.) and are not required to end with a semicolon."#; ================================================ FILE: cli/main.rs ================================================ #![allow(clippy::arc_with_non_send_sync)] mod app; mod commands; mod config; mod helper; mod input; mod manual; mod mcp_server; mod opcodes_dictionary; mod read_state_machine; mod sync_server; #[cfg(feature = "mvcc_repl")] mod mvcc_repl; use config::CONFIG_DIR; use mcp_server::TursoMcpServer; use rustyline::{error::ReadlineError, Config, Editor}; use std::{ path::PathBuf, sync::{atomic::Ordering, LazyLock}, }; use crate::sync_server::TursoSyncServer; #[cfg(all(feature = "mimalloc", not(target_family = "wasm"), not(miri)))] #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; fn rustyline_config() -> Config { Config::builder() .completion_type(rustyline::CompletionType::List) .auto_add_history(true) .build() } pub static HOME_DIR: LazyLock = LazyLock::new(|| dirs::home_dir().expect("Could not determine home directory")); pub static HISTORY_FILE: LazyLock = LazyLock::new(|| HOME_DIR.join(".limbo_history")); fn run_mcp_server(app: app::Limbo) -> anyhow::Result<()> { let conn = app.get_connection(); let interrupt_count = app.get_interrupt_count(); let mcp_server = TursoMcpServer::new(conn, interrupt_count); mcp_server.run() } fn run_sync_server(app: app::Limbo) -> anyhow::Result<()> { let address = app.opts.sync_server_address.clone().unwrap(); let conn = app.get_connection(); let interrupt_count = app.get_interrupt_count(); let sync_server = TursoSyncServer::new(address, conn, interrupt_count); sync_server.run() } fn main() -> anyhow::Result<()> { #[cfg(feature = "mvcc_repl")] { use clap::Parser as _; let opts = app::Opts::parse(); if opts.mvcc { let path = opts .database .as_ref() .and_then(|p| p.to_str()) .unwrap_or(":memory:") .to_owned(); return mvcc_repl::run(&path); } } let (mut app, _guard) = app::Limbo::new()?; if app.is_mcp_mode() { return run_mcp_server(app); } if app.is_sync_server_mode() { return run_sync_server(app); } let interactive_stdin = std::io::IsTerminal::is_terminal(&std::io::stdin()); if interactive_stdin { let mut rl = Editor::with_config(rustyline_config())?; if HISTORY_FILE.exists() { rl.load_history(HISTORY_FILE.as_path())?; } let config_file = CONFIG_DIR.join("limbo.toml"); let config = config::Config::from_config_file(config_file); tracing::info!("Configuration: {:?}", config); app = app.with_config(config); app = app.with_readline(rl); } else { tracing::debug!("not in tty"); } loop { match app.readline() { Ok(_) => app.consume(false), Err(ReadlineError::Interrupted) => { // At prompt, increment interrupt count if app.interrupt_count.fetch_add(1, Ordering::SeqCst) >= 1 { eprintln!("Interrupted. Exiting..."); let _ = app.close_conn(); break; } println!("Use .quit to exit or press Ctrl-C again to force quit."); app.reset_input(); continue; } Err(ReadlineError::Eof) => { // consume remaining input before exit app.consume(true); let _ = app.close_conn(); break; } Err(err) => { let _ = app.close_conn(); anyhow::bail!(err) } } } if !interactive_stdin && app.has_query_error() { std::process::exit(1); } Ok(()) } ================================================ FILE: cli/manual.rs ================================================ use include_dir::{include_dir, Dir}; use rand::seq::SliceRandom; use std::io::{stdout, IsTerminal, Write}; use termimad::{ crossterm::{ event::{read, Event, KeyCode}, queue, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }, Area, MadSkin, MadView, }; static MANUAL_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/manuals"); /// Get a random feature to highlight from available manuals pub fn get_random_feature_hint() -> Option { let features: Vec<(&str, String)> = MANUAL_DIR .files() .filter_map(|file| { let path = file.path(); let name = path.file_stem()?.to_str()?; if name == "index" { return None; } let content = file.contents_utf8()?; let display_name = extract_display_name(content).unwrap_or_else(|| name.to_string()); Some((name, display_name)) }) .collect(); if features.is_empty() { return None; } features .choose(&mut rand::thread_rng()) .map(|(feature, display_name)| { format!("Did you know that Turso supports {display_name}? Type .manual {feature} to learn more.") }) } fn extract_display_name(content: &str) -> Option { if !content.starts_with("---") { return None; } let lines: Vec<&str> = content.lines().collect(); let end_idx = lines[1..].iter().position(|&line| line == "---")? + 1; for line in &lines[1..end_idx] { if let Some(display_name) = line.strip_prefix("display_name: ") { return Some(display_name.trim_matches('"').to_string()); } } None } fn strip_frontmatter(content: &str) -> &str { if !content.starts_with("---") { return content; } if let Some(end_pos) = content[3..].find("\n---\n") { &content[end_pos + 7..] } else { content } } // not ideal but enough for our usecase , probably overkill maybe. fn levenshtein(a: &str, b: &str) -> usize { let a_chars: Vec<_> = a.chars().collect(); let b_chars: Vec<_> = b.chars().collect(); let (a_len, b_len) = (a_chars.len(), b_chars.len()); if a_len == 0 { return b_len; } if b_len == 0 { return a_len; } let mut prev_row: Vec = (0..=b_len).collect(); let mut current_row = vec![0; b_len + 1]; for i in 1..=a_len { current_row[0] = i; for j in 1..=b_len { let substitution_cost = if a_chars[i - 1] == b_chars[j - 1] { 0 } else { 1 }; current_row[j] = (prev_row[j] + 1) .min(current_row[j - 1] + 1) .min(prev_row[j - 1] + substitution_cost); } prev_row.clone_from_slice(¤t_row); } prev_row[b_len] } fn find_closest_manual_page<'a>( page_name: &str, available_pages: impl Iterator, ) -> Option<&'a str> { const RELATIVE_SIMILARITY_THRESHOLD: f64 = 0.4; available_pages .filter_map(|candidate| { let distance = levenshtein(page_name, candidate); let longer_len = std::cmp::max(page_name.chars().count(), candidate.chars().count()); if longer_len == 0 { return None; } let relative_distance = distance as f64 / longer_len as f64; if relative_distance < RELATIVE_SIMILARITY_THRESHOLD { Some((candidate, distance)) } else { None } }) .min_by_key(|&(_, score)| score) .map(|(name, _)| name) } pub fn display_manual(page: Option<&str>, writer: &mut dyn Write) -> anyhow::Result<()> { let page_name = page.unwrap_or("index"); let file_name = format!("{page_name}.md"); if let Some(file) = MANUAL_DIR.get_file(&file_name) { let content = file .contents_utf8() .ok_or_else(|| anyhow::anyhow!("Failed to read manual page: {}", page_name))?; let content = strip_frontmatter(content); if IsTerminal::is_terminal(&std::io::stdout()) { render_in_terminal(content)?; } else { writeln!(writer, "{content}")?; } Ok(()) } else if page.is_none() { // If no page specified, list available pages return list_available_manuals(writer); } else { let available_pages = MANUAL_DIR .files() .filter_map(|file| file.path().file_stem().and_then(|stem| stem.to_str())); let mut error_message = format!("Manual page not found: {page_name}"); if let Some(suggestion) = find_closest_manual_page(page_name, available_pages) { error_message.push_str(&format!("\n\nDid you mean '.manual {suggestion}'?")); } Err(anyhow::anyhow!(error_message)) } } fn render_in_terminal(content: &str) -> anyhow::Result<()> { // Create a skin with nice styling let mut skin = MadSkin::default(); // Customize the skin for better appearance skin.set_headers_fg(termimad::crossterm::style::Color::Cyan); skin.bold.set_fg(termimad::crossterm::style::Color::Yellow); skin.italic .set_fg(termimad::crossterm::style::Color::Magenta); skin.inline_code .set_fg(termimad::crossterm::style::Color::Green); skin.code_block .set_fg(termimad::crossterm::style::Color::Green); let mut w = stdout(); queue!(w, EnterAlternateScreen)?; enable_raw_mode()?; let area = Area::full_screen(); let mut view = MadView::from(content.to_string(), area, skin); loop { view.write_on(&mut w)?; w.flush()?; match read()? { Event::Key(key) => match key.code { KeyCode::Up | KeyCode::Char('k') => view.try_scroll_lines(-1), KeyCode::Down | KeyCode::Char('j') => view.try_scroll_lines(1), KeyCode::PageUp => view.try_scroll_pages(-1), KeyCode::PageDown => view.try_scroll_pages(1), KeyCode::Char('g') => view.scroll = 0, KeyCode::Char('G') => view.try_scroll_lines(i32::MAX), KeyCode::Esc | KeyCode::Char('q') | KeyCode::Enter => break, _ => {} }, Event::Resize(width, height) => { let new_area = Area::new(0, 0, width, height); view.resize(&new_area); } _ => {} } } disable_raw_mode()?; queue!(w, LeaveAlternateScreen)?; w.flush()?; Ok(()) } fn list_available_manuals(writer: &mut dyn Write) -> anyhow::Result<()> { writeln!(writer, "Available manual pages:")?; writeln!(writer)?; let mut pages: Vec = MANUAL_DIR .files() .filter_map(|file| file.path().file_stem()?.to_str().map(String::from)) .collect(); pages.sort(); for page in &pages { writeln!(writer, " .manual {page} # or .man {page}")?; } if pages.is_empty() { writeln!(writer, " (No manual pages found)")?; } writeln!(writer)?; writeln!(writer, "Usage: .manual or .man ")?; Ok(()) } ================================================ FILE: cli/manuals/cdc.md ================================================ --- display_name: "Change Data Capture" --- # CDC - Change Data Capture ## Overview Change Data Capture (CDC) allows you to track and capture all data changes (inserts, updates, deletes) made to your database tables. This is useful for building reactive applications, syncing data between systems, replication, auditing, and more. ## Enabling CDC CDC is enabled per connection using the PRAGMA command: ```sql PRAGMA capture_data_changes_conn('[,]'); ``` ### Parameters - **mode**: The capture mode (see below) - **table_name**: Optional custom table name for storing changes (defaults to `turso_cdc`) ### Capture Modes - **`off`**: Disable CDC for this connection - **`id`**: Capture only the primary key/rowid of changed rows - **`before`**: Capture row state before changes (for updates/deletes) - **`after`**: Capture row state after changes (for inserts/updates) - **`full`**: Capture both before and after states, plus update details ## Examples ### Basic Usage Enable CDC with ID mode (captures primary keys only): ```sql PRAGMA capture_data_changes_conn('id'); ``` ### Using Different Modes Capture the state before changes: ```sql PRAGMA capture_data_changes_conn('before'); ``` Capture the state after changes: ```sql PRAGMA capture_data_changes_conn('after'); ``` Capture complete change information: ```sql PRAGMA capture_data_changes_conn('full'); ``` ### Custom CDC Table Store changes in a custom table instead of the default `turso_cdc`: ```sql PRAGMA capture_data_changes_conn('full,my_changes_table'); ``` ### Disable CDC Turn off CDC for the current connection: ```sql PRAGMA capture_data_changes_conn('off'); ``` ## CDC Table Structure The CDC table (default name: `turso_cdc`) contains the following columns: | Column | Type | Description | |--------|------|-------------| | `change_id` | INTEGER | Auto-incrementing unique identifier for each change | | `change_time` | INTEGER | Timestamp of the change (Unix epoch) | | `change_type` | INTEGER | Type of change: 1 (INSERT), 0 (UPDATE), -1 (DELETE) | | `table_name` | TEXT | Name of the table that was changed | | `id` | varies | Primary key/rowid of the changed row | | `before` | BLOB | Row data before the change (for modes: before, full) | | `after` | BLOB | Row data after the change (for modes: after, full) | | `updates` | BLOB | Details of updated columns (for mode: full) | ## Querying Changes Once CDC is enabled, you can query the changes table like any other table: ```sql -- View all captured changes SELECT * FROM turso_cdc; -- View only inserts SELECT * FROM turso_cdc WHERE change_type = 1; -- View only updates SELECT * FROM turso_cdc WHERE change_type = 0; -- View only deletes SELECT * FROM turso_cdc WHERE change_type = -1; -- View changes for a specific table SELECT * FROM turso_cdc WHERE table_name = 'users'; -- View recent changes (last hour) SELECT * FROM turso_cdc WHERE change_time > unixepoch() - 3600; ``` ## Practical Example ```sql -- Create a table CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT, email TEXT ); -- Enable full CDC PRAGMA capture_data_changes_conn('full'); -- Make some changes INSERT INTO users VALUES (1, 'Alice', 'alice@example.com'); INSERT INTO users VALUES (2, 'Bob', 'bob@example.com'); UPDATE users SET email = 'alice@newdomain.com' WHERE id = 1; DELETE FROM users WHERE id = 2; -- View the captured changes SELECT change_type, table_name, id FROM turso_cdc; -- Results will show: -- 1 (INSERT) for Alice -- 1 (INSERT) for Bob -- 0 (UPDATE) for Alice's email change -- -1 (DELETE) for Bob ``` ## Multiple Connections Each connection can have its own CDC configuration: ```sql -- Connection 1: Capture to 'audit_log' table PRAGMA capture_data_changes_conn('full,audit_log'); -- Connection 2: Capture to 'sync_queue' table PRAGMA capture_data_changes_conn('id,sync_queue'); -- Changes from Connection 1 go to 'audit_log' -- Changes from Connection 2 go to 'sync_queue' ``` ## Transactions CDC respects transaction boundaries. Changes are only recorded when a transaction commits: ```sql BEGIN; INSERT INTO users VALUES (3, 'Charlie', 'charlie@example.com'); UPDATE users SET name = 'Charles' WHERE id = 3; -- CDC table is not yet updated COMMIT; -- Now both the INSERT and UPDATE appear in the CDC table ``` If a transaction rolls back, no CDC entries are created for those changes. ## Schema Changes CDC also tracks schema changes when using full mode: ```sql PRAGMA capture_data_changes_conn('full'); CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT); -- Recorded in CDC as change to sqlite_schema DROP TABLE products; -- Also recorded as a schema change ``` ================================================ FILE: cli/manuals/custom-types.md ================================================ --- display_name: "custom types" --- # Custom Types ## Overview Turso extends SQLite's STRICT table type system with user-defined custom types. Custom types let you define how values are encoded before storage and decoded when read, enforce domain constraints at the storage layer, attach operators, and provide defaults — all declared in pure SQL. Custom types work only with **STRICT** tables (which are always enabled): ``` tursodb mydb.db ``` Without this flag, `CREATE TYPE`, `DROP TYPE`, the `sqlite_turso_types` virtual table, and all built-in custom types (date, varchar, numeric, etc.) are unavailable. `PRAGMA list_types` will only show the five base SQLite types (INTEGER, REAL, TEXT, BLOB, ANY). ## Creating a Type ```sql CREATE TYPE type_name BASE base_type ENCODE encode_expr DECODE decode_expr [OPERATOR 'op' [function_name] ...] [DEFAULT default_expr]; ``` - **BASE** — The underlying SQLite storage type (`text`, `integer`, `real`, `blob`). - **ENCODE** — Expression applied to `value` before writing to disk. - **DECODE** — Expression applied to `value` when reading from disk. - **OPERATOR** — (Optional) Custom operator overloads for the type. If `function_name` is omitted, the base type's built-in comparison is used (see [Ordering](#ordering) below). - **DEFAULT** — (Optional) Default value for columns of this type when no value is supplied. The special identifier `value` refers to the input being encoded or decoded. ## Dropping a Type ```sql DROP TYPE type_name; DROP TYPE IF EXISTS type_name; ``` A type cannot be dropped while any table has a column using it. ## Basic Examples ### Identity Type (Passthrough) The simplest custom type — stores and reads values unchanged: ```sql CREATE TYPE passthrough BASE text ENCODE value DECODE value; CREATE TABLE t1(val passthrough) STRICT; INSERT INTO t1 VALUES ('hello'); SELECT val FROM t1; -- hello ``` ### Reversed Text Encode reverses the string for storage; decode reverses it back: ```sql CREATE TYPE reversed BASE text ENCODE string_reverse(value) DECODE string_reverse(value); CREATE TABLE t1(val reversed) STRICT; INSERT INTO t1 VALUES ('hello'); SELECT val FROM t1; -- hello (stored on disk as 'olleh') ``` ### Cents (Expression-Based Encode/Decode) Store monetary values as integers (cents) but present them as whole units: ```sql CREATE TYPE cents BASE integer ENCODE value * 100 DECODE value / 100; CREATE TABLE prices(amount cents) STRICT; INSERT INTO prices VALUES (42); SELECT amount FROM prices; -- 42 (stored on disk as 4200) ``` ### JSON Validation Use `json()` as the encoder to reject malformed JSON at insert time: ```sql CREATE TYPE jsontype BASE text ENCODE json(value) DECODE value; CREATE TABLE t1(val jsontype) STRICT; INSERT INTO t1 VALUES ('{"key": 1}'); -- OK INSERT INTO t1 VALUES ('not json'); -- Error: malformed JSON ``` ## Operators Custom types can overload SQL operators so expressions like `val + val` or `val < 10` call user-defined functions: ```sql CREATE TYPE uint BASE text ENCODE test_uint_encode(value) DECODE test_uint_decode(value) OPERATOR '+' (uint) -> test_uint_add OPERATOR '<' (uint) -> test_uint_lt OPERATOR '=' (uint) -> test_uint_eq; CREATE TABLE t1(val uint) STRICT; INSERT INTO t1 VALUES (20); INSERT INTO t1 VALUES (30); SELECT val + val FROM t1; -- 40 -- 60 SELECT val FROM t1 WHERE val < 25; -- 20 ``` ## Ordering Sorting and indexing always operate on **encoded (on-disk) values**, not decoded values. DECODE is purely a presentation layer — it controls how values appear in query results, but has no effect on sort order or index structure. Custom types that support ordering must declare `OPERATOR '<'`. Without it, `ORDER BY` and `CREATE INDEX` on columns of that type are **forbidden** — attempting either produces a clear error. ### Naked `OPERATOR '<'` (Base Type Comparison) A naked `OPERATOR '<'` (no function name) tells Turso to compare encoded values using the base type's built-in comparison. This works correctly when the encoding preserves the desired sort order: ```sql -- ENCODE value * 100 is monotonic: 10→100, 20→200, 30→300. -- Sorting encoded integers preserves numeric order. CREATE TYPE cents BASE integer ENCODE value * 100 DECODE value / 100 OPERATOR '<'; CREATE TABLE prices(id INTEGER PRIMARY KEY, amount cents) STRICT; INSERT INTO prices VALUES (1, 30), (2, 10), (3, 20); SELECT amount FROM prices ORDER BY amount; -- 10 -- 20 -- 30 ``` If the encoding does **not** preserve order, the sort will reflect the encoded representation: ```sql -- string_reverse is NOT monotonic: encoded text sorts differently than decoded. -- Encoded: apple→elppa, banana→ananab, cherry→yrrehc. -- Encoded text sort: ananab < elppa < yrrehc → display: banana, apple, cherry. CREATE TYPE reversed BASE text ENCODE string_reverse(value) DECODE string_reverse(value) OPERATOR '<'; CREATE TABLE t(id INTEGER PRIMARY KEY, val reversed) STRICT; INSERT INTO t VALUES (1, 'apple'), (2, 'banana'), (3, 'cherry'); SELECT val FROM t ORDER BY val; -- banana -- apple -- cherry ``` ### `OPERATOR '<'` with a Function (Custom Comparator) For types where the base type comparison on encoded values is not suitable, provide a custom comparator function. The comparator transforms encoded values before comparing: ```sql -- numeric stores values as blobs; standard blob comparison is wrong. -- numeric_lt knows how to compare encoded blobs numerically. CREATE TYPE numeric(precision, scale) BASE blob ENCODE numeric_encode(value, precision, scale) DECODE numeric_decode(value) OPERATOR '<' numeric_lt; ``` A comparator can also recover a desired sort order from a non-order-preserving encoding: ```sql -- Same encoding as above, but the comparator reverses encoded values -- before comparing, recovering alphabetical order. CREATE TYPE reversed_alpha BASE text ENCODE string_reverse(value) DECODE string_reverse(value) OPERATOR '<' string_reverse; CREATE TABLE t(id INTEGER PRIMARY KEY, val reversed_alpha) STRICT; INSERT INTO t VALUES (1, 'apple'), (2, 'banana'), (3, 'cherry'); SELECT val FROM t ORDER BY val; -- apple -- banana -- cherry ``` ### Non-Orderable Types Types without `OPERATOR '<'` cannot be used in `ORDER BY` or `CREATE INDEX`: ```sql CREATE TYPE mytype BASE text ENCODE value DECODE value; CREATE TABLE t(val mytype) STRICT; SELECT val FROM t ORDER BY val; -- Error: cannot ORDER BY column 'val' of type 'mytype': type does not declare OPERATOR '<' CREATE INDEX idx ON t(val); -- Error: cannot create index on column 'val' of type 'mytype': type does not declare OPERATOR '<' ``` Expression indexes that compute a regular value from a non-orderable column are still allowed: ```sql CREATE INDEX idx ON t(length(val)); -- OK: length() returns an integer ``` ### Built-In Types with Ordering The following built-in types declare `OPERATOR '<'` and support `ORDER BY` and indexing: `date`, `time`, `timestamp`, `varchar`, `smallint`, `boolean`, `uuid`, `bytea`, `numeric`. Types without ordering support: `json`, `jsonb`, `inet`. ## Defaults ### Type-Level Default A default defined on the type applies to all columns of that type unless overridden: ```sql CREATE TYPE uint BASE text ENCODE test_uint_encode(value) DECODE test_uint_decode(value) DEFAULT 0; CREATE TABLE t1(id INTEGER PRIMARY KEY, val uint) STRICT; INSERT INTO t1(id) VALUES (1); SELECT id, val FROM t1; -- 1|0 ``` ### Column-Level Override A column definition can override the type's default: ```sql CREATE TABLE t1(id INTEGER PRIMARY KEY, val uint DEFAULT 42) STRICT; INSERT INTO t1(id) VALUES (1); SELECT id, val FROM t1; -- 1|42 ``` ### Function Default The default can be an expression or function call: ```sql CREATE TYPE reversed BASE text ENCODE string_reverse(value) DECODE string_reverse(value) DEFAULT string_reverse('auto'); CREATE TABLE t1(id INTEGER PRIMARY KEY, val reversed) STRICT; INSERT INTO t1(id) VALUES (1); SELECT id, val FROM t1; -- 1|otua ``` ## Validation with CASE/RAISE Use `CASE ... ELSE RAISE(ABORT, ...)` in the ENCODE expression to validate values and reject invalid input with a clear error message: ```sql CREATE TYPE positive_int BASE integer ENCODE CASE WHEN value > 0 THEN value ELSE RAISE(ABORT, 'value must be positive') END DECODE value; CREATE TABLE t1(val positive_int) STRICT; INSERT INTO t1 VALUES (42); -- OK INSERT INTO t1 VALUES (-1); -- Error: value must be positive ``` This pattern is how built-in types like `varchar` and `smallint` enforce their constraints: ```sql -- varchar checks length against the maxlen parameter CREATE TYPE varchar(maxlen) BASE text ENCODE CASE WHEN length(value) <= maxlen THEN value ELSE RAISE(ABORT, 'value too long for varchar') END DECODE value; -- smallint checks the integer range CREATE TYPE smallint BASE integer ENCODE CASE WHEN value BETWEEN -32768 AND 32767 THEN value ELSE RAISE(ABORT, 'integer out of range for smallint') END DECODE value; ``` ## Parametric Types Types can declare parameters that are substituted into ENCODE/DECODE expressions. Parameters are specified in parentheses after the type name: ```sql CREATE TYPE varchar(maxlen) BASE text ENCODE CASE WHEN length(value) <= maxlen THEN value ELSE RAISE(ABORT, 'value too long for varchar') END DECODE value; CREATE TABLE t1(name varchar(10)) STRICT; INSERT INTO t1 VALUES ('hello'); -- OK (length 5 <= 10) INSERT INTO t1 VALUES ('toolongname'); -- Error: value too long for varchar ``` When a column is declared as `varchar(10)`, the parameter `maxlen` is replaced with `10` in the ENCODE expression. ## Encode Validation Encoding runs **before** constraint checks (NOT NULL, type affinity). If an encode function returns NULL for a NOT NULL or PRIMARY KEY column, the insert is rejected: ```sql CREATE TYPE my_uuid BASE text ENCODE uuid_blob(value) DECODE uuid_str(value); CREATE TABLE t1(id my_uuid PRIMARY KEY, name TEXT) STRICT; INSERT INTO t1 VALUES ('invalid-uuid', 'bad'); -- Error: NOT NULL constraint failed (uuid_blob returned NULL) ``` ## CHECK Constraints In STRICT tables, CHECK constraint comparisons are type-checked at table creation time. A custom type column cannot be directly compared to a raw literal — the types must match. Use `CAST` to convert literals to the custom type: ```sql -- ERROR: type mismatch in CHECK constraint (cents vs INTEGER) CREATE TABLE t1(amount cents CHECK(amount < 50)) STRICT; -- OK: CAST converts the literal to cents, both sides have the same type CREATE TABLE t1(amount cents CHECK(amount < CAST(50 AS cents))) STRICT; ``` This rule applies to all comparisons in STRICT tables, not just custom types: ```sql -- ERROR: type mismatch (INTEGER vs TEXT) CREATE TABLE t1(age INTEGER CHECK(age < 'old')) STRICT; -- OK: same types CREATE TABLE t1(age INTEGER CHECK(age >= 18)) STRICT; ``` Function calls in CHECK expressions also require CAST, because the return type cannot be determined at table creation time: ```sql -- ERROR: cannot determine return type of length() CREATE TABLE t1(name TEXT CHECK(length(name) < 10)) STRICT; -- OK: CAST makes the type explicit CREATE TABLE t1(name TEXT CHECK(CAST(length(name) AS INTEGER) < 10)) STRICT; ``` ## NULL Handling NULL values bypass encoding and decoding entirely: ```sql CREATE TYPE uint BASE text ENCODE test_uint_encode(value) DECODE test_uint_decode(value); CREATE TABLE t1(val uint) STRICT; INSERT INTO t1 VALUES (NULL); SELECT COALESCE(val, 'IS_NULL') FROM t1; -- IS_NULL ``` ## CAST Support You can cast values to a custom type, which applies the encode function: ```sql CREATE TYPE reversed BASE text ENCODE string_reverse(value) DECODE string_reverse(value); SELECT CAST('hello' AS reversed); -- olleh ``` ## Inspecting Types ### PRAGMA list_types List all available types (built-in and custom) with their metadata: ```sql PRAGMA list_types; -- type | parent | encode | decode | default | operators -- INTEGER | | | | | -- REAL | | | | | -- TEXT | | | | | -- BLOB | | | | | -- ANY | | | | | -- uint | text | test_uint_encode(...) | test_uint_decode(...) | 0 | +(uint) -> test_uint_add ``` ### sqlite_turso_types All types (built-in and user-defined) are available through the `sqlite_turso_types` virtual table: ```sql SELECT name, sql FROM sqlite_turso_types; ``` ## Using with ALTER TABLE Custom types work with `ALTER TABLE ADD COLUMN`: ```sql CREATE TYPE uint BASE text ENCODE test_uint_encode(value) DECODE test_uint_decode(value); CREATE TABLE t1(id INTEGER PRIMARY KEY) STRICT; ALTER TABLE t1 ADD COLUMN val uint; INSERT INTO t1 VALUES (1, 42); SELECT id, val FROM t1; -- 1|42 ``` ## Restrictions - Custom types require **STRICT** tables. - A type cannot be dropped while any table column uses it. - `CREATE TYPE IF NOT EXISTS` silently succeeds if the type already exists. - Encode/decode expressions use the identifier `value` to reference the input. ================================================ FILE: cli/manuals/encryption.md ================================================ --- display_name: "encryption at-rest" --- # Encryption - At-Rest Database Encryption ## Overview Turso supports transparent at-rest encryption to protect your database files from unauthorized access. When enabled, all data written to disk is automatically encrypted, and decrypted when read, with no changes required to your application code. ## Supported Ciphers Turso supports multiple encryption algorithms with different performance and security characteristics: ### AES-GCM Family - **`aes128gcm`** - AES-128 in Galois/Counter Mode (16-byte key) - **`aes256gcm`** - AES-256 in Galois/Counter Mode (32-byte key) ### AEGIS Family (High Performance) - **`aegis256`** - AEGIS-256 (32-byte key) - Recommended for most use cases - **`aegis128l`** - AEGIS-128L (16-byte key) - **`aegis128x2`** - AEGIS-128 with 2x parallelization (16-byte key) - **`aegis128x4`** - AEGIS-128 with 4x parallelization (16-byte key) - **`aegis256x2`** - AEGIS-256 with 2x parallelization (32-byte key) - **`aegis256x4`** - AEGIS-256 with 4x parallelization (32-byte key) **Note:** AEGIS ciphers generally offer better performance than AES-GCM while maintaining excellent security properties. AEGIS-256 is recommended as the default choice. ## Generating Encryption Keys Generate a secure encryption key using OpenSSL: ```bash # For 32-byte key (256-bit) - use with aes256gcm, aegis256, etc. openssl rand -hex 32 # For 16-byte key (128-bit) - use with aes128gcm, aegis128l, etc. openssl rand -hex 16 ``` Example output: ``` 2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d ``` **Important:** Store your encryption key securely. If you lose the key, your encrypted data cannot be recovered. ## Creating an Encrypted Database ### Method 1: Using PRAGMAs Start Turso and set encryption parameters before creating tables. Do note that encryption is an experimental feature that must be explicitly enabled: ```bash tursodb --experimental-encryption database.db ``` Then in the SQL shell: ```sql PRAGMA cipher = 'aegis256'; PRAGMA hexkey = '2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d'; -- Now create your tables and insert data CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); INSERT INTO users VALUES (1, 'Alice'); ``` ### Method 2: Using URI Parameters Specify encryption parameters directly in the database URI: ```bash tursodb --experimental-encryption "file:database.db?cipher=aegis256&hexkey=2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d" ``` ## Opening an Encrypted Database **Important:** To open an existing encrypted database, you MUST provide the cipher and key as URI parameters: ```bash tursodb --experimental-encryption "file:database.db?cipher=aegis256&hexkey=2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d" ``` Attempting to open an encrypted database without the correct cipher and key will fail. ## Troubleshooting ### "Database is encrypted or is not a database" This error occurs when: - Opening an encrypted database without providing cipher/key - Using the wrong cipher or key - The database file is corrupted ### "Invalid hex string" - Ensure your key is valid hexadecimal (0-9, a-f) - Check the key length matches your cipher (32 hex chars for 16 bytes, 64 for 32 bytes) ================================================ FILE: cli/manuals/index.md ================================================ # Turso Manual Pages Welcome to the Turso manual pages. These pages provide detailed documentation for various features and capabilities. ## Available Manuals ### custom-types - Custom Types for STRICT Tables Define user-defined types with custom encode/decode logic, operator overloads, and defaults for STRICT tables. ``` .manual custom-types ``` ### cdc - Change Data Capture Track and capture all data changes made to your database tables for replication, syncing, and reactive applications. ``` .manual cdc ``` ### encryption - At-Rest Database Encryption Protect your database files with transparent encryption using AES-GCM or high-performance AEGIS ciphers. ``` .manual encryption ``` ### mcp - Model Context Protocol Learn about Turso's built-in MCP server that enables AI assistants and other tools to interact with your databases. ``` .manual mcp ``` ### vector - Vector Search Build similarity search and semantic search applications using vector embeddings and distance functions. ``` .manual vector ``` ### materialized-views - Live Materialized Views Create automatically updating views that use Incremental View Maintenance to stay current with minimal overhead. ``` .manual materialized-views ``` ## Usage To view a manual page, use the `.manual` or `.man` command: ``` .manual # Full command .man # Short alias ``` ### Examples ``` .manual mcp # View the MCP server documentation .man mcp # Same as above, using the alias ``` ## Adding More Manuals Additional manual pages will be added for other features as they become available. ================================================ FILE: cli/manuals/materialized-views.md ================================================ --- display_name: "live materialized views" --- # Live Materialized Views Live materialized views in Turso are automatically updating database objects that maintain query results in real-time. Unlike traditional materialized views that require manual refresh, Turso's live materialized views use Incremental View Maintenance (IVM) to stay current with minimal overhead. ## Enabling Materialized Views Materialized views are an experimental feature that must be explicitly enabled: ```bash tursodb --experimental-views your_database.db ``` ## What Makes Them Special Traditional materialized views store a snapshot of query results that becomes stale as the underlying data changes. You must manually refresh them, which often means re-executing the entire query. That is a costly operation for large datasets. Turso's materialized views are different. They automatically update themselves by tracking only the changes to the underlying tables. When you insert, update, or delete a row, the materialized view calculates just the incremental changes needed to stay current. This means: - **No manual refresh required** - Views are always up-to-date - **Efficient updates** - Only processes changed data, not the entire dataset - **Real-time consistency** - Changes are reflected immediately - **Scalable performance** - Update cost is proportional to the size of changes, not the size of the table ## How Incremental View Maintenance Works Instead of re-computing the entire view when data changes, IVM tracks what has changed and updates only the affected portions of the materialized view. For example: - When you insert a new row, IVM adds only that row's contribution to the view - When you delete a row, IVM removes only that row's contribution - When you update a row, IVM treats it as a delete of the old value followed by an insert of the new value This approach is particularly powerful for aggregations. If you have a view that calculates the sum of millions of rows, adding one new row only requires adding that single value to the existing sum—not re-summing all million rows. ## Transactional Consistency Because live materialized views are instantly updated, they are fully transactional. Views are updated inside the same transaction as the base table modifications, ensuring: - **Atomic updates** - View changes are committed or rolled back together with base table changes - **Consistency** - Views never show partial updates or inconsistent state - **Isolation** - Other transactions see either the complete change or none of it - **Durability** - View updates are persisted with the same guarantees as regular tables If a transaction rolls back, all changes—including those to materialized views—are rolled back together. ## Creating Materialized Views Create a materialized view using standard SQL syntax: ```sql CREATE MATERIALIZED VIEW sales_summary AS SELECT product_id, COUNT(*) as total_sales, SUM(amount) as revenue, AVG(amount) as avg_sale_amount FROM sales GROUP BY product_id; ``` Once created, you can query the materialized view like any table: ```sql SELECT * FROM sales_summary WHERE revenue > 10000; ``` ## Use Cases Materialized views excel in scenarios where: 1. **Dashboard queries** - Complex aggregations that power real-time dashboards 2. **Reporting** - Pre-computed summaries for business intelligence 3. **Denormalization** - Maintaining denormalized data without manual updates 4. **Performance optimization** - Expensive joins or aggregations that are frequently queried ## Current Limitations As an experimental feature, materialized views in Turso currently have some limitations: - Not all SQL functions are supported in view definitions - Views cannot reference other views ## Performance Considerations While materialized views provide excellent query performance, they do add overhead to write operations. Each insert, update, or delete must also update any dependent materialized views. Consider this trade-off when designing your schema: - Use materialized views for frequently-read, infrequently-written data - Avoid creating too many materialized views on highly volatile tables - Monitor the performance impact on write operations ================================================ FILE: cli/manuals/mcp.md ================================================ --- display_name: "a built-in MCP server" --- # MCP Server - Model Context Protocol ## Overview Turso includes a built-in MCP (Model Context Protocol) server that allows AI assistants and other tools to interact with your databases programmatically. ## Starting the MCP Server To start Turso in MCP server mode, use the `--mcp` flag: ```bash /path/to/tursodb --mcp ``` This will start an MCP server that listens on stdio for commands. The server starts without a database connection, allowing you to select or create databases using MCP commands. ## Available Tools The MCP server exposes the following tools: ### `query` Execute a SQL query and get results. **Parameters:** - `sql` (string, required): The SQL query to execute **Example:** ```json { "tool": "query", "arguments": { "sql": "SELECT * FROM users WHERE age > 21" } } ``` ### `execute` Execute a SQL statement that modifies data (INSERT, UPDATE, DELETE). **Parameters:** - `sql` (string, required): The SQL statement to execute **Example:** ```json { "tool": "execute", "arguments": { "sql": "INSERT INTO users (name, age) VALUES ('Alice', 30)" } } ``` ### `list_tables` List all tables in the database. **Example:** ```json { "tool": "list_tables", "arguments": {} } ``` ### `describe_table` Get the schema of a specific table. **Parameters:** - `table` (string, required): The name of the table to describe **Example:** ```json { "tool": "describe_table", "arguments": { "table": "users" } } ``` ## Integration with AI Assistants ### Claude Desktop To use with Claude Desktop, add the following to your Claude Desktop configuration: ```json { "mcpServers": { "turso": { "command": "/path/to/tursodb", "args": ["--mcp"] } } } ``` Note: You must use the full path to the tursodb executable as Claude Desktop may not recognize items in your PATH. ### Other MCP Clients The Turso MCP server follows the standard MCP protocol and can be used with any MCP-compatible client. ## Example Session Here's an example of using the MCP server: 1. **Start the server:** ```bash /path/to/tursodb --mcp ``` 2. **Query data:** ``` > What tables are in the database? [Uses list_tables tool] > Show me all users older than 25 [Uses query tool with "SELECT * FROM users WHERE age > 25"] ``` 3. **Modify data:** ``` > Add a new user named Bob who is 28 years old [Uses execute tool with INSERT statement] ``` ## Troubleshooting ### Server doesn't start - Ensure the tursodb executable path is correct - Check that you're using the full path to the executable ### Commands fail - Verify SQL syntax is correct - Check that tables and columns exist - Ensure you have write permissions if modifying data ## See Also - MCP Protocol Documentation: https://modelcontextprotocol.io - Turso Documentation: https://turso.tech/docs ================================================ FILE: cli/manuals/vector.md ================================================ --- display_name: "vector search" --- # Vector Search ## Overview Turso supports vector operations for building similarity search and semantic search applications. Vectors are stored as BLOBs and can be searched using distance functions to find similar items. **Important:** Vector indexes are not yet supported. All vector searches currently use brute-force scanning, which means searching scales linearly with the number of rows. ## Vector Types Turso supports two vector formats: - **`vector32`** - 32-bit floating-point vectors (4 bytes per dimension) - **`vector64`** - 64-bit floating-point vectors (8 bytes per dimension) ## Creating and Storing Vectors Vectors are stored in vector columns as-is, and represented on-disk as BLOBs. Embeddings are interpreted and validated at runtime. In order for embedding to be valid, it must be either JSON array of float values OR binary blob created with turso vector functiosn `vector32` / `vector64`. ### Basic Example ```sql -- Create a table with vector embeddings CREATE TABLE documents ( id INTEGER PRIMARY KEY, content TEXT, embedding BLOB -- Store vector as BLOB ); -- Insert vectors using vector32() or vector64() INSERT INTO documents VALUES (1, 'Introduction to databases', vector32('[0.1, 0.2, 0.3, 0.4]')), (2, 'SQL query optimization', vector32('[0.2, 0.1, 0.4, 0.3]')), (3, 'Vector similarity search', vector32('[0.4, 0.3, 0.2, 0.1]')); ``` ### Working with Higher Dimensions Real embeddings typically have hundreds or thousands of dimensions: ```sql -- Example with 1536-dimensional embeddings (like OpenAI's ada-002) CREATE TABLE embeddings ( id INTEGER PRIMARY KEY, text TEXT, vector BLOB ); -- Insert a 1536-dimensional vector INSERT INTO embeddings VALUES (1, 'Sample text', vector32('[0.001, 0.002, ..., 0.1536]')); ``` ## Vector Functions ### Creation Functions - **`vector32(text)`** - Create a 32-bit float vector from JSON array - **`vector64(text)`** - Create a 64-bit float vector from JSON array ### Distance Functions - **`vector_distance_l2(v1, v2)`** - Euclidean (L2) distance between vectors - **`vector_distance_cos(v1, v2)`** - Cosine distance (1 - cosine similarity) ### Utility Functions - **`vector_extract(blob)`** - Convert vector BLOB back to JSON text - **`vector_concat(v1, v2)`** - Concatenate two vectors - **`vector_slice(v, start, end)`** - Extract a portion of a vector ## Similarity Search Examples ### Finding Similar Documents ```sql -- Find documents similar to a query vector WITH query AS ( SELECT vector32('[0.15, 0.25, 0.35, 0.45]') AS query_vector ) SELECT id, content, vector_distance_l2(embedding, query_vector) AS distance FROM documents, query ORDER BY distance LIMIT 5; ``` ### Cosine Similarity Search Cosine similarity is often preferred for text embeddings: ```sql -- Find semantically similar documents using cosine distance WITH query AS ( SELECT vector32('[0.15, 0.25, 0.35, 0.45]') AS query_vector ) SELECT id, content, vector_distance_cos(embedding, query_vector) AS cosine_distance FROM documents, query ORDER BY cosine_distance LIMIT 5; ``` ### Threshold-Based Search Find all vectors within a certain distance: ```sql -- Find all documents within distance threshold WITH query AS ( SELECT vector32('[0.15, 0.25, 0.35, 0.45]') AS query_vector ) SELECT id, content, vector_distance_l2(embedding, query_vector) AS distance FROM documents, query WHERE vector_distance_l2(embedding, query_vector) < 0.5 ORDER BY distance; ``` ## Working with Vector Data ### Inspecting Vectors ```sql -- Extract and view vector data as JSON SELECT id, vector_extract(embedding) AS vector_json FROM documents LIMIT 3; ``` ### Vector Operations ```sql -- Concatenate two vectors SELECT vector_concat( vector32('[1.0, 2.0]'), vector32('[3.0, 4.0]') ) AS concatenated; -- Slice a vector (extract dimensions 2-4) SELECT vector_slice( vector32('[1.0, 2.0, 3.0, 4.0, 5.0]'), 2, 4 ) AS sliced; ``` ## Building a Semantic Search Application Here's a complete example of a semantic search application: ```sql -- 1. Create schema CREATE TABLE articles ( id INTEGER PRIMARY KEY, title TEXT, content TEXT, embedding BLOB ); -- 2. Insert pre-computed embeddings INSERT INTO articles VALUES (1, 'Database Fundamentals', 'An introduction to relational databases...', vector32('[0.12, -0.34, 0.56, ...]')), (2, 'Machine Learning Basics', 'Understanding neural networks and deep learning...', vector32('[0.23, 0.45, -0.67, ...]')), (3, 'Web Development Guide', 'Modern web applications with JavaScript...', vector32('[0.34, -0.12, 0.78, ...]')); -- 3. Search for similar articles WITH search_embedding AS ( -- This would come from your embedding model for the search query SELECT vector32('[0.15, -0.30, 0.60, ...]') AS query_vec ) SELECT a.id, a.title, vector_distance_cos(a.embedding, s.query_vec) AS similarity_score FROM articles a, search_embedding s ORDER BY similarity_score LIMIT 10; ``` ## Performance Considerations Since vector indexes are not yet implemented, keep in mind: - **Linear scan**: Every search examines all rows in the table - **Memory usage**: Vectors consume significant space (4 bytes × dimensions for vector32) - **Optimization tips**: - Use smaller dimensions when possible - Pre-filter data with WHERE clauses before distance calculations - Consider partitioning large datasets - Use vector32 instead of vector64 unless high precision is needed ## Common Use Cases - **Semantic search**: Find documents by meaning rather than keywords - **Recommendation systems**: Find similar items based on embeddings - **Duplicate detection**: Identify near-duplicate content - **Image similarity**: Search for similar images using visual embeddings - **Anomaly detection**: Find outliers in high-dimensional data ================================================ FILE: cli/mcp_server.rs ================================================ use anyhow::Result; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::io::{self, BufRead, BufReader, Write}; use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::mpsc; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; use turso_core::{Connection, Database, DatabaseOpts, Numeric, OpenFlags, Value as DbValue}; #[derive(Debug, Serialize, Deserialize)] struct JsonRpcRequest { jsonrpc: String, id: Option, method: String, params: Option, } #[derive(Debug, Serialize, Deserialize)] struct JsonRpcResponse { jsonrpc: String, id: Option, #[serde(skip_serializing_if = "Option::is_none")] result: Option, #[serde(skip_serializing_if = "Option::is_none")] error: Option, } #[derive(Debug, Serialize, Deserialize)] struct JsonRpcError { code: i32, message: String, #[serde(skip_serializing_if = "Option::is_none")] data: Option, } #[derive(Debug, Serialize, Deserialize)] struct InitializeRequest { #[serde(rename = "protocolVersion")] protocol_version: String, capabilities: Value, #[serde(rename = "clientInfo")] client_info: Value, } #[derive(Debug, Serialize, Deserialize)] struct CallToolRequest { name: String, arguments: Option, } pub struct TursoMcpServer { conn: Arc>>, interrupt_count: Arc, current_db_path: Arc>>, } impl TursoMcpServer { pub fn new(conn: Arc, interrupt_count: Arc) -> Self { Self { conn: Arc::new(Mutex::new(conn)), interrupt_count, current_db_path: Arc::new(Mutex::new(None)), } } pub fn run(&self) -> Result<()> { let stdout = io::stdout(); let mut stdout_lock = stdout.lock(); // Create a channel to receive lines from stdin let (tx, rx) = mpsc::channel(); // Spawn a thread to read from stdin thread::spawn(move || { let stdin = io::stdin(); let reader = BufReader::new(stdin); for line in reader.lines() { match line { Ok(line) => { if tx.send(Ok(line)).is_err() { break; // Main thread has dropped the receiver } } Err(e) => { let _ = tx.send(Err(e)); break; } } } }); loop { // Check if we've been interrupted if self.interrupt_count.load(Ordering::SeqCst) > 0 { eprintln!("MCP server interrupted, shutting down..."); break; } // Try to receive a line with a timeout so we can check for interruption match rx.recv_timeout(Duration::from_millis(100)) { Ok(Ok(line)) => { if line.trim().is_empty() { continue; } let request: JsonRpcRequest = match serde_json::from_str(&line) { Ok(req) => req, Err(e) => { eprintln!("Failed to parse JSON-RPC request: {e}"); continue; } }; let response = self.handle_request(request); // Don't send a response for notifications (when id is None) if response.id.is_some() || response.error.is_some() { let response_json = serde_json::to_string(&response)?; writeln!(stdout_lock, "{response_json}")?; stdout_lock.flush()?; } } Ok(Err(_)) => { // Error reading from stdin break; } Err(mpsc::RecvTimeoutError::Timeout) => { // Timeout - continue loop to check for interruption continue; } Err(mpsc::RecvTimeoutError::Disconnected) => { // Stdin thread has finished (EOF) break; } } } Ok(()) } fn handle_request(&self, request: JsonRpcRequest) -> JsonRpcResponse { // Check if this is a notification (no id field means it's a notification) // Notifications should not receive a response according to JSON-RPC spec if request.id.is_none() { // For notifications, we return a special response that the caller should ignore return JsonRpcResponse { jsonrpc: "2.0".to_string(), id: None, result: None, error: None, }; } match request.method.as_str() { "initialize" => self.handle_initialize(request), "tools/list" => self.handle_list_tools(request), "tools/call" => self.handle_call_tool(request), _ => JsonRpcResponse { jsonrpc: "2.0".to_string(), id: request.id, result: None, error: Some(JsonRpcError { code: -32601, message: "Method not found".to_string(), data: None, }), }, } } fn handle_initialize(&self, request: JsonRpcRequest) -> JsonRpcResponse { JsonRpcResponse { jsonrpc: "2.0".to_string(), id: request.id, result: Some(json!({ "protocolVersion": "2024-11-05", "capabilities": { "tools": {} }, "serverInfo": { "name": "turso-mcp", "version": "1.0.0" } })), error: None, } } fn handle_list_tools(&self, request: JsonRpcRequest) -> JsonRpcResponse { JsonRpcResponse { jsonrpc: "2.0".to_string(), id: request.id, result: Some(json!({ "tools": [ { "name": "open_database", "description": "Open or create a database file. Creates parent directories if needed.", "inputSchema": { "type": "object", "properties": { "path": { "type": "string", "description": "Path to the database file (absolute or relative). Use ':memory:' for in-memory database." } }, "required": ["path"] } }, { "name": "current_database", "description": "Get the path of the currently open database", "inputSchema": { "type": "object", "properties": {}, "required": [] } }, { "name": "list_tables", "description": "List all tables in the database", "inputSchema": { "type": "object", "properties": {}, "required": [] } }, { "name": "describe_table", "description": "Describe the structure of a specific table", "inputSchema": { "type": "object", "properties": { "table_name": { "type": "string", "description": "Name of the table to describe" } }, "required": ["table_name"] } }, { "name": "execute_query", "description": "Execute a read-only SELECT query", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The SELECT query to execute" } }, "required": ["query"] } }, { "name": "insert_data", "description": "Insert new data into a table", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The INSERT statement to execute" } }, "required": ["query"] } }, { "name": "update_data", "description": "Update existing data in a table", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The UPDATE statement to execute" } }, "required": ["query"] } }, { "name": "delete_data", "description": "Delete data from a table", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The DELETE statement to execute" } }, "required": ["query"] } }, { "name": "schema_change", "description": "Execute schema modification statements (CREATE TABLE, ALTER TABLE, DROP TABLE)", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The schema modification statement to execute" } }, "required": ["query"] } } ] })), error: None, } } fn handle_call_tool(&self, request: JsonRpcRequest) -> JsonRpcResponse { let tool_request: CallToolRequest = match request.params.as_ref() { Some(params) => match serde_json::from_value(params.clone()) { Ok(req) => req, Err(e) => { return JsonRpcResponse { jsonrpc: "2.0".to_string(), id: request.id, result: None, error: Some(JsonRpcError { code: -32602, message: format!("Invalid params: {e}"), data: None, }), }; } }, None => { return JsonRpcResponse { jsonrpc: "2.0".to_string(), id: request.id, result: None, error: Some(JsonRpcError { code: -32602, message: "Missing params".to_string(), data: None, }), }; } }; let result = match tool_request.name.as_str() { "open_database" => self.open_database(&tool_request.arguments), "current_database" => self.current_database(), "list_tables" => self.list_tables(), "describe_table" => self.describe_table(&tool_request.arguments), "execute_query" => self.execute_query(&tool_request.arguments), "insert_data" => self.insert_data(&tool_request.arguments), "update_data" => self.update_data(&tool_request.arguments), "delete_data" => self.delete_data(&tool_request.arguments), "schema_change" => self.schema_change(&tool_request.arguments), _ => { return JsonRpcResponse { jsonrpc: "2.0".to_string(), id: request.id, result: None, error: Some(JsonRpcError { code: -32601, message: format!("Unknown tool: {}", tool_request.name), data: None, }), }; } }; JsonRpcResponse { jsonrpc: "2.0".to_string(), id: request.id, result: Some(json!({ "content": [{ "type": "text", "text": result }] })), error: None, } } fn open_database(&self, arguments: &Option) -> String { let path = match arguments { Some(args) => match args.get("path") { Some(Value::String(p)) => p.clone(), _ => return "Missing or invalid path parameter".to_string(), }, None => return "Missing path parameter".to_string(), }; // Create parent directories if needed if path != ":memory:" { let db_path = PathBuf::from(&path); if let Some(parent) = db_path.parent() { if !parent.exists() { if let Err(e) = std::fs::create_dir_all(parent) { return format!("Failed to create parent directories: {e}"); } } } } // Open the new database connection let conn = if path == ":memory:" || path.contains([':', '?', '&', '#']) { match Connection::from_uri(&path, DatabaseOpts::default()) { Ok((_io, c)) => c, Err(e) => return format!("Failed to open database '{path}': {e}"), } } else { match Database::open_new( &path, None::<&str>, OpenFlags::default(), DatabaseOpts::new().with_autovacuum(false), None, ) { Ok((_io, db)) => match db.connect() { Ok(c) => c, Err(e) => return format!("Failed to connect to database '{path}': {e}"), }, Err(e) => return format!("Failed to open database '{path}': {e}"), } }; // Update the connection and path *self.conn.lock().unwrap() = conn; *self.current_db_path.lock().unwrap() = Some(path.clone()); format!("Successfully opened database: {path}") } fn current_database(&self) -> String { match &*self.current_db_path.lock().unwrap() { Some(path) => format!("Current database: {path}"), None => "Current database: :memory: (default)".to_string(), } } fn list_tables(&self) -> String { let query = "SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY 1"; let conn = self.conn.lock().unwrap().clone(); match conn.query(query) { Ok(Some(mut rows)) => { let mut tables = Vec::new(); let res = rows.run_with_row_callback(|row| { if let Ok(DbValue::Text(table)) = row.get::<&DbValue>(0) { tables.push(table.to_string()); } Ok(()) }); if let Err(err) = res { return err.to_string(); } if tables.is_empty() { "No tables found in the database".to_string() } else { tables.join(", ") } } Ok(None) => "No results returned from the query".to_string(), Err(e) => format!("Error querying database: {e}"), } } fn describe_table(&self, arguments: &Option) -> String { let table_name = match arguments { Some(args) => match args.get("table_name") { Some(Value::String(name)) => name, _ => return "Missing or invalid table_name parameter".to_string(), }, None => return "Missing table_name parameter".to_string(), }; let query = format!("PRAGMA table_info({table_name})"); let conn = self.conn.lock().unwrap().clone(); match conn.query(&query) { Ok(Some(mut rows)) => { let mut columns = Vec::new(); let res = rows.run_with_row_callback(|row| { if let (Ok(col_name), Ok(col_type), Ok(not_null), Ok(default_value), Ok(pk)) = ( row.get::<&DbValue>(1), row.get::<&DbValue>(2), row.get::<&DbValue>(3), row.get::<&DbValue>(4), row.get::<&DbValue>(5), ) { let default_str = if matches!(default_value, DbValue::Null) { "".to_string() } else { format!("DEFAULT {default_value}") }; columns.push( format!( "{} {} {} {} {}", col_name, col_type, if matches!(not_null, DbValue::Numeric(Numeric::Integer(1))) { "NOT NULL" } else { "NULL" }, default_str, if matches!(pk, DbValue::Numeric(Numeric::Integer(1))) { "PRIMARY KEY" } else { "" } ) .trim() .to_string(), ); } Ok(()) }); if let Err(err) = res { return err.to_string(); } if columns.is_empty() { format!("Table '{table_name}' not found") } else { format!("Table '{table_name}' columns:\n{}", columns.join("\n")) } } Ok(None) => format!("Table '{table_name}' not found"), Err(e) => format!("Error querying database: {e}"), } } fn execute_query(&self, arguments: &Option) -> String { let query = match arguments { Some(args) => match args.get("query") { Some(Value::String(q)) => q, _ => return "Missing or invalid query parameter".to_string(), }, None => return "Missing query parameter".to_string(), }; // Basic validation to ensure it's a read-only query let trimmed_query = query.trim().to_lowercase(); if !trimmed_query.starts_with("select") { return "Only SELECT queries are allowed".to_string(); } let conn = self.conn.lock().unwrap().clone(); match conn.query(query) { Ok(Some(mut rows)) => { let mut results = Vec::new(); // Get column names let headers: Vec = (0..rows.num_columns()) .map(|i| rows.get_column_name(i).to_string()) .collect(); // Get the data let res = rows.run_with_row_callback(|row| { let mut row_data = Vec::new(); for value in row.get_values() { row_data.push(value.to_string()); } results.push(row_data); Ok(()) }); if let Err(err) = res { return err.to_string(); } // Format results as text table let mut output = String::new(); if !headers.is_empty() { output.push_str(&headers.join(" | ")); output.push('\n'); output.push_str(&"-".repeat(headers.join(" | ").len())); output.push('\n'); } for row in results { output.push_str(&row.join(" | ")); output.push('\n'); } if output.is_empty() { "No results returned from the query".to_string() } else { output } } Ok(None) => "No results returned from the query".to_string(), Err(e) => format!("Error executing query: {e}"), } } fn insert_data(&self, arguments: &Option) -> String { let query = match arguments { Some(args) => match args.get("query") { Some(Value::String(q)) => q, _ => return "Missing or invalid query parameter".to_string(), }, None => return "Missing query parameter".to_string(), }; // Basic validation to ensure it's an INSERT query let trimmed_query = query.trim().to_lowercase(); if !trimmed_query.starts_with("insert") { return "Only INSERT statements are allowed".to_string(); } let conn = self.conn.lock().unwrap().clone(); match conn.execute(query) { Ok(()) => "INSERT successful.".to_string(), Err(e) => format!("Error executing INSERT: {e}"), } } fn update_data(&self, arguments: &Option) -> String { let query = match arguments { Some(args) => match args.get("query") { Some(Value::String(q)) => q, _ => return "Missing or invalid query parameter".to_string(), }, None => return "Missing query parameter".to_string(), }; // Basic validation to ensure it's an UPDATE query let trimmed_query = query.trim().to_lowercase(); if !trimmed_query.starts_with("update") { return "Only UPDATE statements are allowed".to_string(); } let conn = self.conn.lock().unwrap().clone(); match conn.execute(query) { Ok(()) => "UPDATE successful.".to_string(), Err(e) => format!("Error executing UPDATE: {e}"), } } fn delete_data(&self, arguments: &Option) -> String { let query = match arguments { Some(args) => match args.get("query") { Some(Value::String(q)) => q, _ => return "Missing or invalid query parameter".to_string(), }, None => return "Missing query parameter".to_string(), }; // Basic validation to ensure it's a DELETE query let trimmed_query = query.trim().to_lowercase(); if !trimmed_query.starts_with("delete") { return "Only DELETE statements are allowed".to_string(); } let conn = self.conn.lock().unwrap().clone(); match conn.execute(query) { Ok(()) => "DELETE successful.".to_string(), Err(e) => format!("Error executing DELETE: {e}"), } } fn schema_change(&self, arguments: &Option) -> String { let query = match arguments { Some(args) => match args.get("query") { Some(Value::String(q)) => q, _ => return "Missing or invalid query parameter".to_string(), }, None => return "Missing query parameter".to_string(), }; // Basic validation to ensure it's a schema modification query let trimmed_query = query.trim().to_lowercase(); if !trimmed_query.starts_with("create") && !trimmed_query.starts_with("alter") && !trimmed_query.starts_with("drop") { return "Only CREATE, ALTER, and DROP statements are allowed".to_string(); } let conn = self.conn.lock().unwrap().clone(); match conn.execute(query) { Ok(()) => "Schema change successful.".to_string(), Err(e) => format!("Error executing schema change: {e}"), } } } ================================================ FILE: cli/mvcc_repl.rs ================================================ //! MVCC Concurrent Transaction REPL //! //! Utility for interactive testing of concurrent MVCC transactions. //! //! # Overview //! //! This provides an interactive REPL where you can drive multiple in-process database //! connections and test concurrent transaction behavior, conflict detection, and isolation //! semantics. Connections are created lazily, when they are first used. //! //! # Invocation //! //! ```bash //! cargo run --bin tursodb --features mvcc_repl -- --mvcc [path] //! ``` //! //! # Usage Examples //! //! Once started, you'll see a prompt where you can run SQL on specific connections: //! //! ```text //! mvcc> conn1 CREATE TABLE t(x INT) //! [conn1] OK //! //! mvcc> conn1 BEGIN CONCURRENT //! [conn1] OK //! //! mvcc> conn2 BEGIN CONCURRENT //! [conn2] OK //! //! mvcc> conn1 INSERT INTO t VALUES (42) //! [conn1] OK //! //! mvcc> conn2 INSERT INTO t VALUES (42) //! [conn2] ERROR: write-write conflict (transaction rolled back) //! ``` use anyhow::Context as _; use rustyline::DefaultEditor; use std::{collections::HashMap, sync::Arc}; use turso_core::{Connection, Database, DatabaseOpts, LimboError, OpenFlags, Value}; pub fn run(path: &str) -> anyhow::Result<()> { let interactive_stdin = std::io::IsTerminal::is_terminal(&std::io::stdin()); if interactive_stdin { println!("MVCC REPL"); println!("Type `connN SQL` to run SQL on connection N (e.g. `conn1 BEGIN CONCURRENT;`)"); println!("Connections are auto-created on first use"); println!(); } let db = open_mvcc_db(path)?; let mut connections: HashMap> = HashMap::new(); let mut rl = DefaultEditor::new()?; loop { match rl.readline("mvcc> ") { Ok(line) => { let line = line.trim(); if line.is_empty() { continue; } if line == ".quit" { break; } if let Some((conn_name, sql)) = parse_conn_input(line) { let conn = connections .entry(conn_name.clone()) .or_insert_with(|| db.connect().expect("failed to create connection")); match execute_and_display(conn, sql, &conn_name) { Ok(()) => {} Err(e) => { eprintln!("[{conn_name}] ERROR: {e}"); } } } else { eprintln!("Error: format is `connN SQL` (e.g. `conn1 SELECT * FROM t`)"); } } Err(rustyline::error::ReadlineError::Eof) => break, Err(rustyline::error::ReadlineError::Interrupted) => { println!(); continue; } Err(e) => anyhow::bail!(e), } } Ok(()) } fn open_mvcc_db(path: &str) -> anyhow::Result> { let (_, db) = Database::open_new::<&str>( path, None, OpenFlags::default(), DatabaseOpts::default(), None, ) .context("failed to open database")?; let boot = db.connect()?; boot.execute("PRAGMA journal_mode = 'mvcc'")?; boot.close()?; Ok(db) } fn parse_conn_input(line: &str) -> Option<(String, &str)> { let (first, rest) = line.split_once(char::is_whitespace)?; if !first.starts_with("conn") { return None; } let num_part = &first[4..]; num_part.parse::().ok()?; Some((first.to_string(), rest.trim())) } fn execute_and_display(conn: &Arc, sql: &str, conn_name: &str) -> anyhow::Result<()> { let mut stmt = conn.prepare(sql).map_err(|e| anyhow::anyhow!("{}", e))?; match stmt.run_collect_rows() { Ok(rows) => { if rows.is_empty() { println!("[{conn_name}] OK"); } else { for row in rows { let formatted: Vec = row.iter().map(fmt_value).collect(); println!("[{conn_name}] {}", formatted.join(" | ")); } } Ok(()) } Err(LimboError::WriteWriteConflict) => Err(anyhow::anyhow!( "write-write conflict (transaction rolled back)" )), Err(e) => Err(anyhow::anyhow!("{e}")), } } fn fmt_value(v: &Value) -> String { use turso_core::Numeric; match v { Value::Null => "NULL".to_string(), Value::Numeric(Numeric::Integer(i)) => i.to_string(), Value::Numeric(Numeric::Float(f)) => { let fval: f64 = (*f).into(); // Format floats without trailing zeros for cleaner display if fval.fract() == 0.0 && fval.abs() < 1e10 { format!("{fval:.1}") } else { format!("{fval}") } } Value::Text(s) => s.to_string(), Value::Blob(b) => format!("", b.len()), } } ================================================ FILE: cli/opcodes_dictionary.rs ================================================ // This source code is derived from SQLite project, which is in public domain: // /// https://www.sqlite.org/copyright.html use std::fmt::Display; pub struct OpCodeDescription { pub name: &'static str, pub description: &'static str, } impl Display for OpCodeDescription { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}\n-------\n{}", self.name, self.description) } } // https://www.sqlite.org/opcode.html pub const OPCODE_DESCRIPTIONS: [OpCodeDescription; 189] = [ OpCodeDescription { name: "Abortable", description: "Verify that an Abort can happen. Assert if an Abort at this point might cause database corruption. This opcode only appears in debugging builds. An Abort is safe if either there have been no writes, or if there is an active statement journal." }, OpCodeDescription { name: "Add", description: "Add the value in register P1 to the value in register P2 and store the result in register P3. If either input is NULL, the result is NULL." }, OpCodeDescription { name: "AddImm", description: "Add the constant P2 to the value in register P1. The result is always an integer. To force any register to be an integer, just add 0." }, OpCodeDescription { name: "Affinity", description: "Apply affinities to a range of P2 registers starting with P1. P4 is a string that is P2 characters long. The N-th character of the string indicates the column affinity that should be used for the N-th memory cell in the range." }, OpCodeDescription { name: "AggFinal", description: "P1 is the memory location that is the accumulator for an aggregate or window function. Execute the finalizer function for an aggregate and store the result in P1. P2 is the number of arguments that the step function takes and P4 is a pointer to the FuncDef for this function. The P2 argument is not used by this opcode. It is only there to disambiguate functions that can take varying numbers of arguments. The P4 argument is only needed for the case where the step function was not previously called." }, OpCodeDescription { name: "AggInverse", description: "Execute the xInverse function for an aggregate. The function has P5 arguments. P4 is a pointer to the FuncDef structure that specifies the function. Register P3 is the accumulator. The P5 arguments are taken from register P2 and its successors." }, OpCodeDescription { name: "AggStep", description: "Execute the xStep function for an aggregate. The function has P5 arguments. P4 is a pointer to the FuncDef structure that specifies the function. Register P3 is the accumulator. The P5 arguments are taken from register P2 and its successors." }, OpCodeDescription { name: "AggStep1", description: "Execute the xStep (if P1==0) or xInverse (if P1!=0) function for an aggregate. The function has P5 arguments. P4 is a pointer to the FuncDef structure that specifies the function. Register P3 is the accumulator. The P5 arguments are taken from register P2 and its successors. This opcode is initially coded as OP_AggStep0. On first evaluation, the FuncDef stored in P4 is converted into an sqlite3_context and the opcode is changed. In this way, the initialization of the sqlite3_context only happens once, instead of on each call to the step function." }, OpCodeDescription { name: "AggValue", description: "Invoke the xValue() function and store the result in register P3. P2 is the number of arguments that the step function takes and P4 is a pointer to the FuncDef for this function. The P2 argument is not used by this opcode. It is only there to disambiguate functions that can take varying numbers of arguments. The P4 argument is only needed for the case where the step function was not previously called." }, OpCodeDescription { name: "And", description: "Take the logical AND of the values in registers P1 and P2 and write the result into register P3. If either P1 or P2 is 0 (false) then the result is 0 even if the other input is NULL. A NULL and true or two NULLs give a NULL output." }, OpCodeDescription { name: "AutoCommit", description: "Set the database auto-commit flag to P1 (1 or 0). If P2 is true, roll back any currently active btree transactions. If there are any active VMs (apart from this one), then a ROLLBACK fails. A COMMIT fails if there are active writing VMs or active VMs that use shared cache. This instruction causes the VM to halt." }, OpCodeDescription { name: "BeginSubrtn", description: "Mark the beginning of a subroutine that can be entered in-line or that can be called using Gosub. The subroutine should be terminated by an Return instruction that has a P1 operand that is the same as the P2 operand to this opcode and that has P3 set to 1. If the subroutine is entered in-line, then the Return will simply fall through. But if the subroutine is entered using Gosub, then the Return will jump back to the first instruction after the Gosub. This routine works by loading a NULL into the P2 register. When the return address register contains a NULL, the Return instruction is a no-op that simply falls through to the next instruction (assuming that the Return opcode has a P3 value of 1). Thus if the subroutine is entered in-line, then the Return will cause in-line execution to continue. But if the subroutine is entered via Gosub, then the Return will cause a return to the address following the Gosub. This opcode is identical to Null. It has a different name only to make the byte code easier to read and verify." }, OpCodeDescription { name: "BitAnd", description: "Take the bit-wise AND of the values in register P1 and P2 and store the result in register P3. If either input is NULL, the result is NULL." }, OpCodeDescription { name: "BitNot", description: "Interpret the content of register P1 as an integer. Store the ones-complement of the P1 value into register P2. If P1 holds a NULL then store a NULL in P2." }, OpCodeDescription { name: "BitOr", description: "Take the bit-wise OR of the values in register P1 and P2 and store the result in register P3. If either input is NULL, the result is NULL." }, OpCodeDescription { name: "Blob", description: "P4 points to a blob of data P1 bytes long. Store this blob in register P2. If P4 is a NULL pointer, then construct a zero-filled blob that is P1 bytes long in P2." }, OpCodeDescription { name: "Cast", description: "Force the value in register P1 to be the type defined by P2. P2=='A' → BLOB P2=='B' → TEXT P2=='C' → NUMERIC P2=='D' → INTEGER P2=='E' → REAL A NULL value is not changed by this routine. It remains NULL." }, OpCodeDescription { name: "Checkpoint", description: "Checkpoint database P1. This is a no-op if P1 is not currently in WAL mode. Parameter P2 is one of SQLITE_CHECKPOINT_PASSIVE, FULL, RESTART, or TRUNCATE. Write 1 or 0 into mem[P3] if the checkpoint returns SQLITE_BUSY or not, respectively. Write the number of pages in the WAL after the checkpoint into mem[P3+1] and the number of pages in the WAL that have been checkpointed after the checkpoint completes into mem[P3+2]. However on an error, mem[P3+1] and mem[P3+2] are initialized to -1." }, OpCodeDescription { name: "Clear", description: "Delete all contents of the database table or index whose root page in the database file is given by P1. But, unlike Destroy, do not remove the table or index from the database file. The table being cleared is in the main database file if P2==0. If P2==1 then the table to be cleared is in the auxiliary database file that is used to store tables create using CREATE TEMPORARY TABLE. If the P3 value is non-zero, then the row change count is incremented by the number of rows in the table being cleared. If P3 is greater than zero, then the value stored in register P3 is also incremented by the number of rows in the table being cleared. See also: Destroy" }, OpCodeDescription { name: "Close", description: "Close a cursor previously opened as P1. If P1 is not currently open, this instruction is a no-op." }, OpCodeDescription { name: "ClrSubtype", description: "Clear the subtype from register P1." }, OpCodeDescription { name: "CollSeq", description: "P4 is a pointer to a CollSeq object. If the next call to a user function or aggregate calls sqlite3GetFuncCollSeq(), this collation sequence will be returned. This is used by the built-in min(), max() and nullif() functions. If P1 is not zero, then it is a register that a subsequent min() or max() aggregate will set to 1 if the current row is not the minimum or maximum. The P1 register is initialized to 0 by this instruction. The interface used by the implementation of the aforementioned functions to retrieve the collation sequence set by this opcode is not available publicly. Only built-in functions have access to this feature." }, OpCodeDescription { name: "Column", description: "Interpret the data that cursor P1 points to as a structure built using the MakeRecord instruction. (See the MakeRecord opcode for additional information about the format of the data.) Extract the P2-th column from this record. If there are less than (P2+1) values in the record, extract a NULL. The value extracted is stored in register P3. If the record contains fewer than P2 fields, then extract a NULL. Or, if the P4 argument is a P4_MEM use the value of the P4 argument as the result. If the OPFLAG_LENGTHARG bit is set in P5 then the result is guaranteed to only be used by the length() function or the equivalent. The content of large blobs is not loaded, thus saving CPU cycles. If the OPFLAG_TYPEOFARG bit is set then the result will only be used by the typeof() function or the IS NULL or IS NOT NULL operators or the equivalent. In this case, all content loading can be omitted." }, OpCodeDescription { name: "ColumnsUsed", description: "This opcode (which only exists if SQLite was compiled with SQLITE_ENABLE_COLUMN_USED_MASK) identifies which columns of the table or index for cursor P1 are used. P4 is a 64-bit integer (P4_INT64) in which the first 63 bits are one for each of the first 63 columns of the table or index that are actually used by the cursor. The high-order bit is set if any column after the 64th is used." }, OpCodeDescription { name: "Compare", description: "Compare two vectors of registers in reg(P1)..reg(P1+P3-1) (call this vector \"A\") and in reg(P2)..reg(P2+P3-1) (\"B\"). Save the result of the comparison for use by the next Jump instruct. If P5 has the OPFLAG_PERMUTE bit set, then the order of comparison is determined by the most recent Permutation operator. If the OPFLAG_PERMUTE bit is clear, then register are compared in sequential order. P4 is a KeyInfo structure that defines collating sequences and sort orders for the comparison. The permutation applies to registers only. The KeyInfo elements are used sequentially. The comparison is a sort comparison, so NULLs compare equal, NULLs are less than numbers, numbers are less than strings, and strings are less than blobs. This opcode must be immediately followed by an Jump opcode." }, OpCodeDescription { name: "Concat", description: "Add the text in register P1 onto the end of the text in register P2 and store the result in register P3. If either the P1 or P2 text are NULL then store NULL in P3. P3 = P2 || P1 It is illegal for P1 and P3 to be the same register. Sometimes, if P3 is the same register as P2, the implementation is able to avoid a memcpy()." }, OpCodeDescription { name: "Copy", description: "Make a copy of registers P1..P1+P3 into registers P2..P2+P3. If the 0x0002 bit of P5 is set then also clear the MEM_Subtype flag in the destination. The 0x0001 bit of P5 indicates that this Copy opcode cannot be merged. The 0x0001 bit is used by the query planner and does not come into play during query execution. This instruction makes a deep copy of the value. A duplicate is made of any string or blob constant. See also SCopy." }, OpCodeDescription { name: "Count", description: "Store the number of entries (an integer value) in the table or index opened by cursor P1 in register P2. If P3==0, then an exact count is obtained, which involves visiting every btree page of the table. But if P3 is non-zero, an estimate is returned based on the current cursor position." }, OpCodeDescription { name: "CreateBtree", description: "Allocate a new b-tree in the main database file if P1==0 or in the TEMP database file if P1==1 or in an attached database if P1>1. The P3 argument must be 1 (BTREE_INTKEY) for a rowid table it must be 2 (BTREE_BLOBKEY) for an index or WITHOUT ROWID table. The root page number of the new b-tree is stored in register P2." }, OpCodeDescription { name: "CursorHint", description: "Provide a hint to cursor P1 that it only needs to return rows that satisfy the Expr in P4. TK_REGISTER terms in the P4 expression refer to values currently held in registers. TK_COLUMN terms in the P4 expression refer to columns in the b-tree to which cursor P1 is pointing." }, OpCodeDescription { name: "CursorLock", description: "Lock the btree to which cursor P1 is pointing so that the btree cannot be written by an other cursor." }, OpCodeDescription { name: "CursorUnlock", description: "Unlock the btree to which cursor P1 is pointing so that it can be written by other cursors." }, OpCodeDescription { name: "DecrJumpZero", description: "Register P1 must hold an integer. Decrement the value in P1 and jump to P2 if the new value is exactly zero." }, OpCodeDescription { name: "DeferredSeek", description: "P1 is an open index cursor and P3 is a cursor on the corresponding table. This opcode does a deferred seek of the P3 table cursor to the row that corresponds to the current row of P1. This is a deferred seek. Nothing actually happens until the cursor is used to read a record. That way, if no reads occur, no unnecessary I/O happens. P4 may be an array of integers (type P4_INTARRAY) containing one entry for each column in the P3 table. If array entry a(i) is non-zero, then reading column a(i)-1 from cursor P3 is equivalent to performing the deferred seek and then reading column i from P1. This information is stored in P3 and used to redirect reads against P3 over to P1, thus possibly avoiding the need to seek and read cursor P3." }, OpCodeDescription { name: "Delete", description: "Delete the record at which the P1 cursor is currently pointing. If the OPFLAG_SAVEPOSITION bit of the P5 parameter is set, then the cursor will be left pointing at either the next or the previous record in the table. If it is left pointing at the next record, then the next Next instruction will be a no-op. As a result, in this case it is ok to delete a record from within a Next loop. If OPFLAG_SAVEPOSITION bit of P5 is clear, then the cursor will be left in an undefined state. If the OPFLAG_AUXDELETE bit is set on P5, that indicates that this delete is one of several associated with deleting a table row and all its associated index entries. Exactly one of those deletes is the \"primary\" delete. The others are all on OPFLAG_FORDELETE cursors or else are marked with the AUXDELETE flag. If the OPFLAG_NCHANGE (0x01) flag of P2 (NB: P2 not P5) is set, then the row change count is incremented (otherwise not). If the OPFLAG_ISNOOP (0x40) flag of P2 (not P5!) is set, then the pre-update-hook for deletes is run, but the btree is otherwise unchanged. This happens when the Delete is to be shortly followed by an Insert with the same key, causing the btree entry to be overwritten. P1 must not be pseudo-table. It has to be a real table with multiple rows. If P4 is not NULL then it points to a Table object. In this case either the update or pre-update hook, or both, may be invoked. The P1 cursor must have been positioned using NotFound prior to invoking this opcode in this case. Specifically, if one is configured, the pre-update hook is invoked if P4 is not NULL. The update-hook is invoked if one is configured, P4 is not NULL, and the OPFLAG_NCHANGE flag is set in P2. If the OPFLAG_ISUPDATE flag is set in P2, then P3 contains the address of the memory cell that contains the value that the rowid of the row will be set to by the update." }, OpCodeDescription { name: "Destroy", description: "Delete an entire database table or index whose root page in the database file is given by P1. The table being destroyed is in the main database file if P3==0. If P3==1 then the table to be destroyed is in the auxiliary database file that is used to store tables create using CREATE TEMPORARY TABLE. If AUTOVACUUM is enabled then it is possible that another root page might be moved into the newly deleted root page in order to keep all root pages contiguous at the beginning of the database. The former value of the root page that moved - its value before the move occurred - is stored in register P2. If no page movement was required (because the table being dropped was already the last one in the database) then a zero is stored in register P2. If AUTOVACUUM is disabled then a zero is stored in register P2. This opcode throws an error if there are any active reader VMs when it is invoked. This is done to avoid the difficulty associated with updating existing cursors when a root page is moved in an AUTOVACUUM database. This error is thrown even if the database is not an AUTOVACUUM db in order to avoid introducing an incompatibility between autovacuum and non-autovacuum modes. See also: Clear" }, OpCodeDescription { name: "Divide", description: "Divide the value in register P1 by the value in register P2 and store the result in register P3 (P3=P2/P1). If the value in register P1 is zero, then the result is NULL. If either input is NULL, the result is NULL." }, OpCodeDescription { name: "DropIndex", description: "Remove the internal (in-memory) data structures that describe the index named P4 in database P1. This is called after an index is dropped from disk (using the Destroy opcode) in order to keep the internal representation of the schema consistent with what is on disk." }, OpCodeDescription { name: "DropTable", description: "Remove the internal (in-memory) data structures that describe the table named P4 in database P1. This is called after a table is dropped from disk (using the Destroy opcode) in order to keep the internal representation of the schema consistent with what is on disk." }, OpCodeDescription { name: "DropTrigger", description: "Remove the internal (in-memory) data structures that describe the trigger named P4 in database P1. This is called after a trigger is dropped from disk (using the Destroy opcode) in order to keep the internal representation of the schema consistent with what is on disk." }, OpCodeDescription { name: "ElseEq", description: "This opcode must follow an Lt or Gt comparison operator. There can be zero or more OP_ReleaseReg opcodes intervening, but no other opcodes are allowed to occur between this instruction and the previous Lt or Gt. If the result of an Eq comparison on the same two operands as the prior Lt or Gt would have been true, then jump to P2. If the result of an Eq comparison on the two previous operands would have been false or NULL, then fall through." }, OpCodeDescription { name: "EndCoroutine", description: "The instruction at the address in register P1 is a Yield. Jump to the P2 parameter of that Yield. After the jump, the value register P1 is left with a value such that subsequent OP_Yields go back to the this same EndCoroutine instruction. See also: InitCoroutine" }, OpCodeDescription { name: "Eq", description: "Compare the values in register P1 and P3. If reg(P3)==reg(P1) then jump to address P2. The SQLITE_AFF_MASK portion of P5 must be an affinity character - SQLITE_AFF_TEXT, SQLITE_AFF_INTEGER, and so forth. An attempt is made to coerce both inputs according to this affinity before the comparison is made. If the SQLITE_AFF_MASK is 0x00, then numeric affinity is used. Note that the affinity conversions are stored back into the input registers P1 and P3. So this opcode can cause persistent changes to registers P1 and P3. Once any conversions have taken place, and neither value is NULL, the values are compared. If both values are blobs then memcmp() is used to determine the results of the comparison. If both values are text, then the appropriate collating function specified in P4 is used to do the comparison. If P4 is not specified then memcmp() is used to compare text string. If both values are numeric, then a numeric comparison is used. If the two values are of different types, then numbers are considered less than strings and strings are considered less than blobs. If SQLITE_NULLEQ is set in P5 then the result of comparison is always either true or false and is never NULL. If both operands are NULL then the result of comparison is true. If either operand is NULL then the result is false. If neither operand is NULL the result is the same as it would be if the SQLITE_NULLEQ flag were omitted from P5. This opcode saves the result of comparison for use by the new Jump opcode." }, OpCodeDescription { name: "Expire", description: "Cause precompiled statements to expire. When an expired statement is executed using sqlite3_step() it will either automatically reprepare itself (if it was originally created using sqlite3_prepare_v2()) or it will fail with SQLITE_SCHEMA. If P1 is 0, then all SQL statements become expired. If P1 is non-zero, then only the currently executing statement is expired. If P2 is 0, then SQL statements are expired immediately. If P2 is 1, then running SQL statements are allowed to continue to run to completion. The P2==1 case occurs when a CREATE INDEX or similar schema change happens that might help the statement run faster but which does not affect the correctness of operation." }, OpCodeDescription { name: "Filter", description: "Compute a hash on the key contained in the P4 registers starting with r[P3]. Check to see if that hash is found in the bloom filter hosted by register P1. If it is not present then maybe jump to P2. Otherwise fall through. False negatives are harmless. It is always safe to fall through, even if the value is in the bloom filter. A false negative causes more CPU cycles to be used, but it should still yield the correct answer. However, an incorrect answer may well arise from a false positive - if the jump is taken when it should fall through." }, OpCodeDescription { name: "FilterAdd", description: "Compute a hash on the P4 registers starting with r[P3] and add that hash to the bloom filter contained in r[P1]." }, OpCodeDescription { name: "FinishSeek", description: "If cursor P1 was previously moved via DeferredSeek, complete that seek operation now, without further delay. If the cursor seek has already occurred, this instruction is a no-op." }, OpCodeDescription { name: "FkCheck", description: "Halt with an SQLITE_CONSTRAINT error if there are any unresolved foreign key constraint violations. If there are no foreign key constraint violations, this is a no-op. FK constraint violations are also checked when the prepared statement exits. This opcode is used to raise foreign key constraint errors prior to returning results such as a row change count or the result of a RETURNING clause." }, OpCodeDescription { name: "FkCounter", description: "Increment a \"constraint counter\" by P2 (P2 may be negative or positive). If P1 is non-zero, the database constraint counter is incremented (deferred foreign key constraints). Otherwise, if P1 is zero, the statement counter is incremented (immediate foreign key constraints)." }, OpCodeDescription { name: "FkIfZero", description: "This opcode tests if a foreign key constraint-counter is currently zero. If so, jump to instruction P2. Otherwise, fall through to the next instruction. If P1 is non-zero, then the jump is taken if the database constraint-counter is zero (the one that counts deferred constraint violations). If P1 is zero, the jump is taken if the statement constraint-counter is zero (immediate foreign key constraint violations)." }, OpCodeDescription { name: "Found", description: "If P4==0 then register P3 holds a blob constructed by MakeRecord. If P4>0 then register P3 is the first of P4 registers that form an unpacked record. Cursor P1 is on an index btree. If the record identified by P3 and P4 is a prefix of any entry in P1 then a jump is made to P2 and P1 is left pointing at the matching entry. This operation leaves the cursor in a state where it can be advanced in the forward direction. The Next instruction will work, but not the Prev instruction. See also: NotFound, NoConflict, NotExists. SeekGe" }, OpCodeDescription { name: "Function", description: "Invoke a user function (P4 is a pointer to an sqlite3_context object that contains a pointer to the function to be run) with arguments taken from register P2 and successors. The number of arguments is in the sqlite3_context object that P4 points to. The result of the function is stored in register P3. Register P3 must not be one of the function inputs. P1 is a 32-bit bitmask indicating whether or not each argument to the function was determined to be constant at compile time. If the first argument was constant then bit 0 of P1 is set. This is used to determine whether meta data associated with a user function argument using the sqlite3_set_auxdata() API may be safely retained until the next invocation of this opcode. See also: AggStep, AggFinal, PureFunc" }, OpCodeDescription { name: "Ge", description: "This works just like the Lt opcode except that the jump is taken if the content of register P3 is greater than or equal to the content of register P1. See the Lt opcode for additional information." }, OpCodeDescription { name: "GetSubtype", description: "Extract the subtype value from register P1 and write that subtype into register P2. If P1 has no subtype, then P1 gets a NULL." }, OpCodeDescription { name: "Gosub", description: "Write the current address onto register P1 and then jump to address P2." }, OpCodeDescription { name: "Goto", description: "An unconditional jump to address P2. The next instruction executed will be the one at index P2 from the beginning of the program. The P1 parameter is not actually used by this opcode. However, it is sometimes set to 1 instead of 0 as a hint to the command-line shell that this Goto is the bottom of a loop and that the lines from P2 down to the current line should be indented for EXPLAIN output." }, OpCodeDescription { name: "Gt", description: "This works just like the Lt opcode except that the jump is taken if the content of register P3 is greater than the content of register P1. See the Lt opcode for additional information." }, OpCodeDescription { name: "Halt", description: "Exit immediately. All open cursors, etc are closed automatically. P1 is the result code returned by sqlite3_exec(), sqlite3_reset(), or sqlite3_finalize(). For a normal halt, this should be SQLITE_OK (0). For errors, it can be some other value. If P1!=0 then P2 will determine whether or not to rollback the current transaction. Do not rollback if P2==OE_Fail. Do the rollback if P2==OE_Rollback. If P2==OE_Abort, then back out all changes that have occurred during this execution of the VDBE, but do not rollback the transaction. If P4 is not null then it is an error message string. P5 is a value between 0 and 4, inclusive, that modifies the P4 string. 0: (no change) 1: NOT NULL constraint failed: P4 2: UNIQUE constraint failed: P4 3: CHECK constraint failed: P4 4: FOREIGN KEY constraint failed: P4 If P5 is not zero and P4 is NULL, then everything after the \":\" is omitted. There is an implied \"Halt 0 0 0\" instruction inserted at the very end of every program. So a jump past the last instruction of the program is the same as executing Halt." }, OpCodeDescription { name: "HaltIfNull", description: "Check the value in register P3. If it is NULL then Halt using parameter P1, P2, and P4 as if this were a Halt instruction. If the value in register P3 is not NULL, then this routine is a no-op. The P5 parameter should be 1." }, OpCodeDescription { name: "IdxDelete", description: "The content of P3 registers starting at register P2 form an unpacked index key. This opcode removes that entry from the index opened by cursor P1. If P5 is not zero, then raise an SQLITE_CORRUPT_INDEX error if no matching index entry is found. This happens when running an UPDATE or DELETE statement and the index entry to be updated or deleted is not found. For some uses of IdxDelete (example: the EXCEPT operator) it does not matter that no matching entry is found. For those cases, P5 is zero. Also, do not raise this (self-correcting and non-critical) error if in writable_schema mode." }, OpCodeDescription { name: "IdxGE", description: "The P4 register values beginning with P3 form an unpacked index key that omits the PRIMARY KEY. Compare this key value against the index that P1 is currently pointing to, ignoring the PRIMARY KEY or ROWID fields at the end. If the P1 index entry is greater than or equal to the key value then jump to P2. Otherwise fall through to the next instruction." }, OpCodeDescription { name: "IdxGT", description: "The P4 register values beginning with P3 form an unpacked index key that omits the PRIMARY KEY. Compare this key value against the index that P1 is currently pointing to, ignoring the PRIMARY KEY or ROWID fields at the end. If the P1 index entry is greater than the key value then jump to P2. Otherwise fall through to the next instruction." }, OpCodeDescription { name: "IdxInsert", description: "Register P2 holds an SQL index key made using the MakeRecord instructions. This opcode writes that key into the index P1. Data for the entry is nil. If P4 is not zero, then it is the number of values in the unpacked key of reg(P2). In that case, P3 is the index of the first register for the unpacked key. The availability of the unpacked key can sometimes be an optimization. If P5 has the OPFLAG_APPEND bit set, that is a hint to the b-tree layer that this insert is likely to be an append. If P5 has the OPFLAG_NCHANGE bit set, then the change counter is incremented by this instruction. If the OPFLAG_NCHANGE bit is clear, then the change counter is unchanged. If the OPFLAG_USESEEKRESULT flag of P5 is set, the implementation might run faster by avoiding an unnecessary seek on cursor P1. However, the OPFLAG_USESEEKRESULT flag must only be set if there have been no prior seeks on the cursor or if the most recent seek used a key equivalent to P2. This instruction only works for indices. The equivalent instruction for tables is Insert." }, OpCodeDescription { name: "IdxLE", description: "The P4 register values beginning with P3 form an unpacked index key that omits the PRIMARY KEY or ROWID. Compare this key value against the index that P1 is currently pointing to, ignoring the PRIMARY KEY or ROWID on the P1 index. If the P1 index entry is less than or equal to the key value then jump to P2. Otherwise fall through to the next instruction." }, OpCodeDescription { name: "IdxLT", description: "The P4 register values beginning with P3 form an unpacked index key that omits the PRIMARY KEY or ROWID. Compare this key value against the index that P1 is currently pointing to, ignoring the PRIMARY KEY or ROWID on the P1 index. If the P1 index entry is less than the key value then jump to P2. Otherwise fall through to the next instruction." }, OpCodeDescription { name: "IdxRowid", description: "Write into register P2 an integer which is the last entry in the record at the end of the index key pointed to by cursor P1. This integer should be the rowid of the table entry to which this index entry points. See also: Rowid, MakeRecord." }, OpCodeDescription { name: "If", description: "Jump to P2 if the value in register P1 is true. The value is considered true if it is numeric and non-zero. If the value in P1 is NULL then take the jump if and only if P3 is non-zero." }, OpCodeDescription { name: "IfNoHope", description: "Register P3 is the first of P4 registers that form an unpacked record. Cursor P1 is an index btree. P2 is a jump destination. In other words, the operands to this opcode are the same as the operands to NotFound and IdxGT. This opcode is an optimization attempt only. If this opcode always falls through, the correct answer is still obtained, but extra work is performed. A value of N in the seekHit flag of cursor P1 means that there exists a key P3:N that will match some record in the index. We want to know if it is possible for a record P3:P4 to match some record in the index. If it is not possible, we can skip some work. So if seekHit is less than P4, attempt to find out if a match is possible by running NotFound. This opcode is used in IN clause processing for a multi-column key. If an IN clause is attached to an element of the key other than the left-most element, and if there are no matches on the most recent seek over the whole key, then it might be that one of the key element to the left is prohibiting a match, and hence there is \"no hope\" of any match regardless of how many IN clause elements are checked. In such a case, we abandon the IN clause search early, using this opcode. The opcode name comes from the fact that the jump is taken if there is \"no hope\" of achieving a match. See also: NotFound, SeekHit" }, OpCodeDescription { name: "IfNot", description: "Jump to P2 if the value in register P1 is False. The value is considered false if it has a numeric value of zero. If the value in P1 is NULL then take the jump if and only if P3 is non-zero." }, OpCodeDescription { name: "IfNotOpen", description: "If cursor P1 is not open or if P1 is set to a NULL row using the NullRow opcode, then jump to instruction P2. Otherwise, fall through." }, OpCodeDescription { name: "IfNotZero", description: "Register P1 must contain an integer. If the content of register P1 is initially greater than zero, then decrement the value in register P1. If it is non-zero (negative or positive) and then also jump to P2. If register P1 is initially zero, leave it unchanged and fall through." }, OpCodeDescription { name: "IfNullRow", description: "Check the cursor P1 to see if it is currently pointing at a NULL row. If it is, then set register P3 to NULL and jump immediately to P2. If P1 is not on a NULL row, then fall through without making any changes. If P1 is not an open cursor, then this opcode is a no-op." }, OpCodeDescription { name: "IfPos", description: "Register P1 must contain an integer. If the value of register P1 is 1 or greater, subtract P3 from the value in P1 and jump to P2. If the initial value of register P1 is less than 1, then the value is unchanged and control passes through to the next instruction." }, OpCodeDescription { name: "IfSizeBetween", description: "Let N be the approximate number of rows in the table or index with cursor P1 and let X be 10*log2(N) if N is positive or -1 if N is zero. Jump to P2 if X is in between P3 and P4, inclusive." }, OpCodeDescription { name: "IncrVacuum", description: "Perform a single step of the incremental vacuum procedure on the P1 database. If the vacuum has finished, jump to instruction P2. Otherwise, fall through to the next instruction." }, OpCodeDescription { name: "Init", description: "Programs contain a single instance of this opcode as the very first opcode. If tracing is enabled (by the sqlite3_trace()) interface, then the UTF-8 string contained in P4 is emitted on the trace callback. Or if P4 is blank, use the string returned by sqlite3_sql(). If P2 is not zero, jump to instruction P2. Increment the value of P1 so that Once opcodes will jump the first time they are evaluated for this run. If P3 is not zero, then it is an address to jump to if an SQLITE_CORRUPT error is encountered." }, OpCodeDescription { name: "InitCoroutine", description: "Set up register P1 so that it will Yield to the coroutine located at address P3. If P2!=0 then the coroutine implementation immediately follows this opcode. So jump over the coroutine implementation to address P2. See also: EndCoroutine" }, OpCodeDescription { name: "Insert", description: "Write an entry into the table of cursor P1. A new entry is created if it doesn't already exist or the data for an existing entry is overwritten. The data is the value MEM_Blob stored in register number P2. The key is stored in register P3. The key must be a MEM_Int. If the OPFLAG_NCHANGE flag of P5 is set, then the row change count is incremented (otherwise not). If the OPFLAG_LASTROWID flag of P5 is set, then rowid is stored for subsequent return by the sqlite3_last_insert_rowid() function (otherwise it is unmodified). If the OPFLAG_USESEEKRESULT flag of P5 is set, the implementation might run faster by avoiding an unnecessary seek on cursor P1. However, the OPFLAG_USESEEKRESULT flag must only be set if there have been no prior seeks on the cursor or if the most recent seek used a key equal to P3. If the OPFLAG_ISUPDATE flag is set, then this opcode is part of an UPDATE operation. Otherwise (if the flag is clear) then this opcode is part of an INSERT operation. The difference is only important to the update hook. Parameter P4 may point to a Table structure, or may be NULL. If it is not NULL, then the update-hook (sqlite3.xUpdateCallback) is invoked following a successful insert. (WARNING/TODO: If P1 is a pseudo-cursor and P2 is dynamically allocated, then ownership of P2 is transferred to the pseudo-cursor and register P2 becomes ephemeral. If the cursor is changed, the value of register P2 will then change. Make sure this does not cause any problems.) This instruction only works on tables. The equivalent instruction for indices is IdxInsert." }, OpCodeDescription { name: "Int64", description: "P4 is a pointer to a 64-bit integer value. Write that value into register P2." }, OpCodeDescription { name: "IntCopy", description: "Transfer the integer value held in register P1 into register P2. This is an optimized version of SCopy that works only for integer values." }, OpCodeDescription { name: "Integer", description: "The 64-bit integer value P1 is written into register P2. This is different from SQLite, where this opcode is used for 32-bit integers" }, OpCodeDescription { name: "IntegrityCk", description: "Do an analysis of the currently open database. Store in register (P1+1) the text of an error message describing any problems. If no problems are found, store a NULL in register (P1+1). The register (P1) contains one less than the maximum number of allowed errors. At most reg(P1) errors will be reported. In other words, the analysis stops as soon as reg(P1) errors are seen. Reg(P1) is updated with the number of errors remaining. The root page numbers of all tables in the database are integers stored in P4_INTARRAY argument. If P5 is not zero, the check is done on the auxiliary database file, not the main database file. This opcode is used to implement the integrity_check pragma." }, OpCodeDescription { name: "IsNull", description: "Jump to P2 if the value in register P1 is NULL." }, OpCodeDescription { name: "IsTrue", description: "This opcode implements the IS TRUE, IS FALSE, IS NOT TRUE, and IS NOT FALSE operators. Interpret the value in register P1 as a boolean value. Store that boolean (a 0 or 1) in register P2. Or if the value in register P1 is NULL, then the P3 is stored in register P2. Invert the answer if P4 is 1. The logic is summarized like this: If P3==0 and P4==0 then r[P2] := r[P1] IS TRUE If P3==1 and P4==1 then r[P2] := r[P1] IS FALSE If P3==0 and P4==1 then r[P2] := r[P1] IS NOT TRUE If P3==1 and P4==0 then r[P2] := r[P1] IS NOT FALSE" }, OpCodeDescription { name: "sType", description: "Jump to P2 if the type of a column in a btree is one of the types specified by the P5 bitmask. P1 is normally a cursor on a btree for which the row decode cache is valid through at least column P3. In other words, there should have been a prior Column for column P3 or greater. If the cursor is not valid, then this opcode might give spurious results. The the btree row has fewer than P3 columns, then use P4 as the datatype. If P1 is -1, then P3 is a register number and the datatype is taken from the value in that register. P5 is a bitmask of data types. SQLITE_INTEGER is the least significant (0x01) bit. SQLITE_FLOAT is the 0x02 bit. SQLITE_TEXT is 0x04. SQLITE_BLOB is 0x08. SQLITE_NULL is 0x10. WARNING: This opcode does not reliably distinguish between NULL and REAL when P1>=0. If the database contains a NaN value, this opcode will think that the datatype is REAL when it should be NULL. When P1<0 and the value is already stored in register P3, then this opcode does reliably distinguish between NULL and REAL. The problem only arises then P1>=0. Take the jump to address P2 if and only if the datatype of the value determined by P1 and P3 corresponds to one of the bits in the P5 bitmask." }, OpCodeDescription { name: "JournalMode", description: "Change the journal mode of database P1 to P3. P3 must be one of the PAGER_JOURNALMODE_XXX values. If changing between the various rollback modes (delete, truncate, persist, off and memory), this is a simple operation. No IO is required. If changing into or out of WAL mode the procedure is more complicated. Write a string containing the final journal-mode to register P2." }, OpCodeDescription { name: "Jump", description: "Jump to the instruction at address P1, P2, or P3 depending on whether in the most recent Compare instruction the P1 vector was less than, equal to, or greater than the P2 vector, respectively. This opcode must immediately follow an Compare opcode." }, OpCodeDescription { name: "Last", description: "The next use of the Rowid or Column or Prev instruction for P1 will refer to the last entry in the database table or index. If the table or index is empty and P2>0, then jump immediately to P2. If P2 is 0 or if the table or index is not empty, fall through to the following instruction. This opcode leaves the cursor configured to move in reverse order, from the end toward the beginning. In other words, the cursor is configured to use Prev, not Next." }, OpCodeDescription { name: "Le", description: "This works just like the Lt opcode except that the jump is taken if the content of register P3 is less than or equal to the content of register P1. See the Lt opcode for additional information." }, OpCodeDescription { name: "LoadAnalysis", description: "Read the sqlite_stat1 table for database P1 and load the content of that table into the internal index hash table. This will cause the analysis to be used when preparing all subsequent queries." }, OpCodeDescription { name: "Lt", description: "Compare the values in register P1 and P3. If reg(P3)0 then P3 is a register in the root frame of this VDBE that holds the largest previously generated record number. No new record numbers are allowed to be less than this value. When this value reaches its maximum, an SQLITE_FULL error is generated. The P3 register is updated with the ' generated record number. This P3 mechanism is used to help implement the AUTOINCREMENT feature." }, OpCodeDescription { name: "Next", description: "Advance cursor P1 so that it points to the next key/data pair in its table or index. If there are no more key/value pairs then fall through to the following instruction. But if the cursor advance was successful, jump immediately to P2. The Next opcode is only valid following an SeekGT, SeekGE, or Rewind opcode used to position the cursor. Next is not allowed to follow SeekLT, SeekLE, or Last. The P1 cursor must be for a real table, not a pseudo-table. P1 must have been opened prior to this opcode or the program will segfault. The P3 value is a hint to the btree implementation. If P3==1, that means P1 is an SQL index and that this instruction could have been omitted if that index had been unique. P3 is usually 0. P3 is always either 0 or 1. If P5 is positive and the jump is taken, then event counter number P5-1 in the prepared statement is incremented. See also: Prev" }, OpCodeDescription { name: "NoConflict", description: "If P4==0 then register P3 holds a blob constructed by MakeRecord. If P4>0 then register P3 is the first of P4 registers that form an unpacked record. Cursor P1 is on an index btree. If the record identified by P3 and P4 contains any NULL value, jump immediately to P2. If all terms of the record are not-NULL then a check is done to determine if any row in the P1 index btree has a matching key prefix. If there are no matches, jump immediately to P2. If there is a match, fall through and leave the P1 cursor pointing to the matching row. This opcode is similar to NotFound with the exceptions that the branch is always taken if any part of the search key input is NULL. This operation leaves the cursor in a state where it cannot be advanced in either direction. In other words, the Next and Prev opcodes do not work after this operation. See also: NotFound, Found, NotExists" }, OpCodeDescription { name: "Noop", description: "Do nothing. This instruction is often useful as a jump destination." }, OpCodeDescription { name: "Not", description: "Interpret the value in register P1 as a boolean value. Store the boolean complement in register P2. If the value in register P1 is NULL, then a NULL is stored in P2." }, OpCodeDescription { name: "NotExists", description: "P1 is the index of a cursor open on an SQL table btree (with integer keys). P3 is an integer rowid. If P1 does not contain a record with rowid P3 then jump immediately to P2. Or, if P2 is 0, raise an SQLITE_CORRUPT error. If P1 does contain a record with rowid P3 then leave the cursor pointing at that record and fall through to the next instruction. The SeekRowid opcode performs the same operation but also allows the P3 register to contain a non-integer value, in which case the jump is always taken. This opcode requires that P3 always contain an integer. The NotFound opcode performs the same operation on index btrees (with arbitrary multi-value keys). This opcode leaves the cursor in a state where it cannot be advanced in either direction. In other words, the Next and Prev opcodes will not work following this opcode. See also: Found, NotFound, NoConflict, SeekRowid" }, OpCodeDescription { name: "NotFound", description: "If P4==0 then register P3 holds a blob constructed by MakeRecord. If P4>0 then register P3 is the first of P4 registers that form an unpacked record. Cursor P1 is on an index btree. If the record identified by P3 and P4 is not the prefix of any entry in P1 then a jump is made to P2. If P1 does contain an entry whose prefix matches the P3/P4 record then control falls through to the next instruction and P1 is left pointing at the matching entry. This operation leaves the cursor in a state where it cannot be advanced in either direction. In other words, the Next and Prev opcodes do not work after this operation. See also: Found, NotExists, NoConflict, IfNoHope" }, OpCodeDescription { name: "NotNull", description: "Jump to P2 if the value in register P1 is not NULL." }, OpCodeDescription { name: "Null", description: "Write a NULL into registers P2. If P3 greater than P2, then also write NULL into register P3 and every register in between P2 and P3. If P3 is less than P2 (typically P3 is zero) then only register P2 is set to NULL. If the P1 value is non-zero, then also set the MEM_Cleared flag so that NULL values will not compare equal even if SQLITE_NULLEQ is set on Ne or Eq." }, OpCodeDescription { name: "NullRow", description: "Move the cursor P1 to a null row. Any Column operations that occur while the cursor is on the null row will always write a NULL. If cursor P1 is not previously opened, open it now to a special pseudo-cursor that always returns NULL for every column." }, OpCodeDescription { name: "Offset", description: "Store in register r[P3] the byte offset into the database file that is the start of the payload for the record at which that cursor P1 is currently pointing. P2 is the column number for the argument to the sqlite_offset() function. This opcode does not use P2 itself, but the P2 value is used by the code generator. The P1, P2, and P3 operands to this opcode are the same as for Column. This opcode is only available if SQLite is compiled with the -DSQLITE_ENABLE_OFFSET_SQL_FUNC option." }, OpCodeDescription { name: "OffsetLimit", description: "This opcode performs a commonly used computation associated with LIMIT and OFFSET processing. r[P1] holds the limit counter. r[P3] holds the offset counter. The opcode computes the combined value of the LIMIT and OFFSET and stores that value in r[P2]. The r[P2] value computed is the total number of rows that will need to be visited in order to complete the query. If r[P3] is zero or negative, that means there is no OFFSET and r[P2] is set to be the value of the LIMIT, r[P1]. if r[P1] is zero or negative, that means there is no LIMIT and r[P2] is set to -1. Otherwise, r[P2] is set to the sum of r[P1] and r[P3]." }, OpCodeDescription { name: "Once", description: "Fall through to the next instruction the first time this opcode is encountered on each invocation of the byte-code program. Jump to P2 on the second and all subsequent encounters during the same invocation. Top-level programs determine first invocation by comparing the P1 operand against the P1 operand on the Init opcode at the beginning of the program. If the P1 values differ, then fall through and make the P1 of this opcode equal to the P1 of Init. If P1 values are the same then take the jump. For subprograms, there is a bitmask in the VdbeFrame that determines whether or not the jump should be taken. The bitmask is necessary because the self-altering code trick does not work for recursive triggers." }, OpCodeDescription { name: "OpenAutoindex", description: "This opcode works the same as OpenEphemeral. It has a different name to distinguish its use. Tables created using by this opcode will be used for automatically created transient indices in joins." }, OpCodeDescription { name: "OpenDup", description: "Open a new cursor P1 that points to the same ephemeral table as cursor P2. The P2 cursor must have been opened by a prior OpenEphemeral opcode. Only ephemeral cursors may be duplicated. Duplicate ephemeral cursors are used for self-joins of materialized views." }, OpCodeDescription { name: "OpenEphemeral", description: "Open a new cursor P1 to a transient table. The cursor is always opened read/write even if the main database is read-only. The ephemeral table is deleted automatically when the cursor is closed. If the cursor P1 is already opened on an ephemeral table, the table is cleared (all content is erased). P2 is the number of columns in the ephemeral table. The cursor points to a BTree table if P4==0 and to a BTree index if P4 is not 0. If P4 is not NULL, it points to a KeyInfo structure that defines the format of keys in the index. The P5 parameter can be a mask of the BTREE_* flags defined in btree.h. These flags control aspects of the operation of the btree. The BTREE_OMIT_JOURNAL and BTREE_SINGLE flags are added automatically. If P3 is positive, then reg[P3] is modified slightly so that it can be used as zero-length data for Insert. This is an optimization that avoids an extra Blob opcode to initialize that register." }, OpCodeDescription { name: "OpenPseudo", description: "Open a new cursor that points to a fake table that contains a single row of data. The content of that one row is the content of memory register P2. In other words, cursor P1 becomes an alias for the MEM_Blob content contained in register P2. A pseudo-table created by this opcode is used to hold a single row output from the sorter so that the row can be decomposed into individual columns using the Column opcode. The Column opcode is the only cursor opcode that works with a pseudo-table. P3 is the number of fields in the records that will be stored by the pseudo-table. If P2 is 0 or negative then the pseudo-cursor will return NULL for every column."}, OpCodeDescription { name: "OpenRead", description: "Open a read-only cursor for the database table whose root page is P2 in a database file. The database file is determined by P3. P3==0 means the main database, P3==1 means the database used for temporary tables, and P3>1 means used the corresponding attached database. Give the new cursor an identifier of P1. The P1 values need not be contiguous but all P1 values should be small integers. It is an error for P1 to be negative. Allowed P5 bits: 0x02 OPFLAG_SEEKEQ: This cursor will only be used for equality lookups (implemented as a pair of opcodes SeekGE/IdxGT of SeekLE/IdxLT) The P4 value may be either an integer (P4_INT32) or a pointer to a KeyInfo structure (P4_KEYINFO). If it is a pointer to a KeyInfo object, description: then table being opened must be an index b-tree where the KeyInfo object defines the content and collating sequence of that index b-tree. Otherwise, if P4 is an integer value, then the table being opened must be a table b-tree with a number of columns no less than the value of P4. See also: OpenWrite, ReopenIdx" }, OpCodeDescription { name: "OpenWrite", description: "Open a read/write cursor named P1 on the table or index whose root page is P2 (or whose root page is held in register P2 if the OPFLAG_P2ISREG bit is set in P5 - see below). The P4 value may be either an integer (P4_INT32) or a pointer to a KeyInfo structure (P4_KEYINFO). If it is a pointer to a KeyInfo object, then table being opened must be an index b-tree where the KeyInfo object defines the content and collating sequence of that index b-tree. Otherwise, if P4 is an integer value, then the table being opened must be a table b-tree with a number of columns no less than the value of P4. Allowed P5 bits: }, 0x02 OPFLAG_SEEKEQ: This cursor will only be used for equality lookups (implemented as a pair of opcodes SeekGE/IdxGT of SeekLE/IdxLT) 0x08 OPFLAG_FORDELETE: This cursor is used only to seek and subsequently delete entries in an index btree. This is a hint to the storage engine that the storage engine is allowed to ignore. The hint is not used by the official SQLite b*tree storage engine, but is used by COMDB2. 0x10 OPFLAG_P2ISREG: Use the content of register P2 as the root page, not the value of P2 itself. This instruction works like OpenRead except that it opens the cursor in read/write mode. See also: OpenRead, ReopenIdx" }, OpCodeDescription { name: "Or", description: "Take the logical OR of the values in register P1 and P2 and store the answer in register P3. If either P1 or P2 is nonzero (true) then the result is 1 (true) even if the other input is NULL. A NULL and false or two NULLs give a NULL output." }, OpCodeDescription { name: "Pagecount", description: "Write the current number of pages in database P1 to memory cell P2." }, OpCodeDescription { name: "Param", description: "This opcode is only ever present in sub-programs called via the Program instruction. Copy a value currently stored in a memory cell of the calling (parent) frame to cell P2 in the current frames address space. This is used by trigger programs to access the new.* and old.* values. The address of the cell in the parent frame is determined by adding the value of the P1 argument to the value of the P1 argument to the calling Program instruction." }, OpCodeDescription { name: "ParseSchema", description: "Read and parse all entries from the schema table of database P1 that match the WHERE clause P4. If P4 is a NULL pointer, then the entire schema for P1 is reparsed. This opcode invokes the parser to create a new virtual machine, then runs the new virtual machine. It is thus a re-entrant opcode." }, OpCodeDescription { name: "Permutation", description: "Set the permutation used by the Compare operator in the next instruction. The permutation is stored in the P4 operand. The permutation is only valid for the next opcode which must be an Compare that has the OPFLAG_PERMUTE bit set in P5. The first integer in the P4 integer array is the length of the array and does not become part of the permutation." }, OpCodeDescription { name: "Prev", description: "Back up cursor P1 so that it points to the previous key/data pair in its table or index. If there is no previous key/value pairs then fall through to the following instruction. But if the cursor backup was successful, jump immediately to P2. The Prev opcode is only valid following an SeekLT, SeekLE, or Last opcode used to position the cursor. Prev is not allowed to follow SeekGT, SeekGE, or Rewind. The P1 cursor must be for a real table, not a pseudo-table. If P1 is not open then the behavior is undefined. The P3 value is a hint to the btree implementation. If P3==1, that means P1 is an SQL index and that this instruction could have been omitted if that index had been unique. P3 is usually 0. P3 is always either 0 or 1. If P5 is positive and the jump is taken, then event counter number P5-1 in the prepared statement is incremented." }, OpCodeDescription { name: "Program", description: "Execute the trigger program passed as P4 (type P4_SUBPROGRAM). P1 contains the address of the memory cell that contains the first memory cell in an array of values used as arguments to the sub-program. P2 contains the address to jump to if the sub-program throws an IGNORE exception using the RAISE() function. P2 might be zero, if there is no possibility that an IGNORE exception will be raised. Register P3 contains the address of a memory cell in this (the parent) VM that is used to allocate the memory required by the sub-vdbe at runtime. P4 is a pointer to the VM containing the trigger program. If P5 is non-zero, then recursive program invocation is enabled." }, OpCodeDescription { name: "PureFunc", description: "Invoke a user function (P4 is a pointer to an sqlite3_context object that contains a pointer to the function to be run) with arguments taken from register P2 and successors. The number of arguments is in the sqlite3_context object that P4 points to. The result of the function is stored in register P3. Register P3 must not be one of the function inputs. P1 is a 32-bit bitmask indicating whether or not each argument to the function was determined to be constant at compile time. If the first argument was constant then bit 0 of P1 is set. This is used to determine whether meta data associated with a user function argument using the sqlite3_set_auxdata() API may be safely retained until the next invocation of this opcode. This opcode works exactly like Function. The only difference is in its name. This opcode is used in places where the function must be purely non-deterministic. Some built-in date/time functions can be either deterministic of non-deterministic, depending on their arguments. When those function are used in a non-deterministic way, they will check to see if they were called using PureFunc instead of Function, and if they were, they throw an error. See also: AggStep, AggFinal, Function" }, OpCodeDescription { name: "ReadCookie", description: "Read cookie number P3 from database P1 and write it into register P2. P3==1 is the schema version. P3==2 is the database format. P3==3 is the recommended pager cache size, and so forth. P1==0 is the main database file and P1==1 is the database file used to store temporary tables. There must be a read-lock on the database (either a transaction must be started or there must be an open cursor) before executing this instruction." }, OpCodeDescription { name: "Real", description: "P4 is a pointer to a 64-bit floating point value. Write that value into register P2." }, OpCodeDescription { name: "RealAffinity", description: "If register P1 holds an integer convert it to a real value. This opcode is used when extracting information from a column that has REAL affinity. Such column values may still be stored as integers, for space efficiency, but after extraction we want them to have only a real value." }, OpCodeDescription { name: "ReleaseReg", description: "Release registers from service. Any content that was in the the registers is unreliable after this opcode completes. The registers released will be the P2 registers starting at P1, except if bit ii of P3 set, then do not release register P1+ii. In other words, P3 is a mask of registers to preserve. Releasing a register clears the Mem.pScopyFrom pointer. That means that if the content of the released register was set using SCopy, a change to the value of the source register for the SCopy will no longer generate an assertion fault in sqlite3VdbeMemAboutToChange(). If P5 is set, then all released registers have their type set to MEM_Undefined so that any subsequent attempt to read the released register (before it is reinitialized) will generate an assertion fault. P5 ought to be set on every call to this opcode. However, there are places in the code generator will release registers before their are used, under the (valid) assumption that the registers will not be reallocated for some other purpose before they are used and hence are safe to release. This opcode is only available in testing and debugging builds. It is not generated for release builds. The purpose of this opcode is to help validate the generated bytecode. This opcode does not actually contribute to computing an answer." }, OpCodeDescription { name: "Remainder", description: "Compute the remainder after integer register P2 is divided by register P1 and store the result in register P3. If the value in register P1 is zero the result is NULL. If either operand is NULL, the result is NULL." }, OpCodeDescription { name: "ReopenIdx", description: "The ReopenIdx opcode works like OpenRead except that it first checks to see if the cursor on P1 is already open on the same b-tree and if it is this opcode becomes a no-op. In other words, if the cursor is already open, do not reopen it. The ReopenIdx opcode may only be used with P5==0 or P5==OPFLAG_SEEKEQ and with P4 being a P4_KEYINFO object. Furthermore, the P3 value must be the same as every other ReopenIdx or OpenRead for the same cursor number. Allowed P5 bits: 0x02 OPFLAG_SEEKEQ: This cursor will only be used for equality lookups (implemented as a pair of opcodes SeekGE/IdxGT of SeekLE/IdxLT) See also: OpenRead, description: OpenWrite" }, OpCodeDescription { name: "ResetCount", description: "The value of the change counter is copied to the database handle change counter (returned by subsequent calls to sqlite3_changes()). Then the VMs internal change counter resets to 0. This is used by trigger programs." }, OpCodeDescription { name: "ResetSorter", description: "Delete all contents from the ephemeral table or sorter that is open on cursor P1. This opcode only works for cursors used for sorting and opened with OpenEphemeral or SorterOpen." }, OpCodeDescription { name: "ResultRow", description: "The registers P1 through P1+P2-1 contain a single row of results. This opcode causes the sqlite3_step() call to terminate with an SQLITE_ROW return code and it sets up the sqlite3_stmt structure to provide access to the r(P1)..r(P1+P2-1) values as the result row." }, OpCodeDescription { name: "Return", description: "Jump to the address stored in register P1. If P1 is a return address register, then this accomplishes a return from a subroutine. If P3 is 1, then the jump is only taken if register P1 holds an integer values, otherwise execution falls through to the next opcode, and the Return becomes a no-op. If P3 is 0, then register P1 must hold an integer or else an assert() is raised. P3 should be set to 1 when this opcode is used in combination with BeginSubrtn, and set to 0 otherwise. The value in register P1 is unchanged by this opcode. P2 is not used by the byte-code engine. However, if P2 is positive and also less than the current address, then the \"EXPLAIN\" output formatter in the CLI will indent all opcodes from the P2 opcode up to be not including the current Return. P2 should be the first opcode in the subroutine from which this opcode is returning. Thus the P2 value is a byte-code indentation hint. See tag-20220407a in wherecode.c and shell.c." }, OpCodeDescription { name: "Rewind", description: "The next use of the Rowid or Column or Next instruction for P1 will refer to the first entry in the database table or index. If the table or index is empty, jump immediately to P2. If the table or index is not empty, fall through to the following instruction. If P2 is zero, that is an assertion that the P1 table is never empty and hence the jump will never be taken. This opcode leaves the cursor configured to move in forward order, from the beginning toward the end. In other words, the cursor is configured to use Next, not Prev." }, OpCodeDescription { name: "RowCell", description: "P1 and P2 are both open cursors. Both must be opened on the same type of table - intkey or index. This opcode is used as part of copying the current row from P2 into P1. If the cursors are opened on intkey tables, register P3 contains the rowid to use with the new record in P1. If they are opened on index tables, P3 is not used. This opcode must be followed by either an Insert or InsertIdx opcode with the OPFLAG_PREFORMAT flag set to complete the insert operation." }, OpCodeDescription { name: "RowData", description: "Write into register P2 the complete row content for the row at which cursor P1 is currently pointing. There is no interpretation of the data. It is just copied onto the P2 register exactly as it is found in the database file. If cursor P1 is an index, then the content is the key of the row. If cursor P2 is a table, then the content extracted is the data. If the P1 cursor must be pointing to a valid row (not a NULL row) of a real table, not a pseudo-table. If P3!=0 then this opcode is allowed to make an ephemeral pointer into the database page. That means that the content of the output register will be invalidated as soon as the cursor moves - including moves caused by other cursors that \"save\" the current cursors position in order that they can write to the same table. If P3==0 then a copy of the data is made into memory. P3!=0 is faster, but P3==0 is safer. If P3!=0 then the content of the P2 register is unsuitable for use in OP_Result and any OP_Result will invalidate the P2 register content. The P2 register content is invalidated by opcodes like Function or by any use of another cursor pointing to the same table." }, OpCodeDescription { name: "Rowid", description: "Store in register P2 an integer which is the key of the table entry that P1 is currently point to. P1 can be either an ordinary table or a virtual table. There used to be a separate OP_VRowid opcode for use with virtual tables, but this one opcode now works for both table types." }, OpCodeDescription { name: "RowSetAdd", description: "Insert the integer value held by register P2 into a RowSet object held in register P1. An assertion fails if P2 is not an integer." }, OpCodeDescription { name: "RowSetRead", description: "Extract the smallest value from the RowSet object in P1 and put that value into register P3. Or, if RowSet object P1 is initially empty, leave P3 unchanged and jump to instruction P2." }, OpCodeDescription { name: "RowSetTest", description: "Register P3 is assumed to hold a 64-bit integer value. If register P1 contains a RowSet object and that RowSet object contains the value held in P3, jump to register P2. Otherwise, insert the integer in P3 into the RowSet and continue on to the next opcode. The RowSet object is optimized for the case where sets of integers are inserted in distinct phases, which each set contains no duplicates. Each set is identified by a unique P4 value. The first set must have P4==0, the final set must have P4==-1, and for all other sets must have P4>0. This allows optimizations: (a) when P4==0 there is no need to test the RowSet object for P3, as it is guaranteed not to contain it, (b) when P4==-1 there is no need to insert the value, as it will never be tested for, and (c) when a value that is part of set X is inserted, there is no need to search to see if the same value was previously inserted as part of set X (only if it was previously inserted as part of some other set)." }, OpCodeDescription { name: "Savepoint", description: "Open, release or rollback the savepoint named by parameter P4, depending on the value of P1. To open a new savepoint set P1==0 (SAVEPOINT_BEGIN). To release (commit) an existing savepoint set P1==1 (SAVEPOINT_RELEASE). To rollback an existing savepoint set P1==2 (SAVEPOINT_ROLLBACK)." }, OpCodeDescription { name: "SCopy", description: "Make a shallow copy of register P1 into register P2. This instruction makes a shallow copy of the value. If the value is a string or blob, then the copy is only a pointer to the original and hence if the original changes so will the copy. Worse, if the original is deallocated, the copy becomes invalid. Thus the program must guarantee that the original will not change during the lifetime of the copy. Use Copy to make a complete copy." }, OpCodeDescription { name: "SeekEnd", description: "Position cursor P1 at the end of the btree for the purpose of appending a new entry onto the btree. It is assumed that the cursor is used only for appending and so if the cursor is valid, then the cursor must already be pointing at the end of the btree and so no changes are made to the cursor." }, OpCodeDescription { name: "SeekGE", description: "If cursor P1 refers to an SQL table (B-Tree that uses integer keys), use the value in register P3 as the key. If cursor P1 refers to an SQL index, then P3 is the first in an array of P4 registers that are used as an unpacked index key. Reposition cursor P1 so that it points to the smallest entry that is greater than or equal to the key value. If there are no records greater than or equal to the key and P2 is not zero, then jump to P2. If the cursor P1 was opened using the OPFLAG_SEEKEQ flag, then this opcode will either land on a record that exactly matches the key, or else it will cause a jump to P2. When the cursor is OPFLAG_SEEKEQ, this opcode must be followed by an IdxLE opcode with the same arguments. The IdxGT opcode will be skipped if this opcode succeeds, but the IdxGT opcode will be used on subsequent loop iterations. The OPFLAG_SEEKEQ flags is a hint to the btree layer to say that this is an equality search. This opcode leaves the cursor configured to move in forward order, from the beginning toward the end. In other words, the cursor is configured to use Next, not Prev. See also: Found, NotFound, SeekLt, SeekGt, SeekLe" }, OpCodeDescription { name: "SeekGT", description: "If cursor P1 refers to an SQL table (B-Tree that uses integer keys), use the value in register P3 as a key. If cursor P1 refers to an SQL index, then P3 is the first in an array of P4 registers that are used as an unpacked index key. Reposition cursor P1 so that it points to the smallest entry that is greater than the key value. If there are no records greater than the key and P2 is not zero, then jump to P2. This opcode leaves the cursor configured to move in forward order, from the beginning toward the end. In other words, the cursor is configured to use Next, not Prev. See also: Found, NotFound, SeekLt, SeekGe, SeekLe" }, OpCodeDescription { name: "SeekHit", description: "Increase or decrease the seekHit value for cursor P1, if necessary, so that it is no less than P2 and no greater than P3. The seekHit integer represents the maximum of terms in an index for which there is known to be at least one match. If the seekHit value is smaller than the total number of equality terms in an index lookup, then the IfNoHope opcode might run to see if the IN loop can be abandoned early, thus saving work. This is part of the IN-early-out optimization. P1 must be a valid b-tree cursor." }, OpCodeDescription { name: "SeekLE", description: "If cursor P1 refers to an SQL table (B-Tree that uses integer keys), use the value in register P3 as a key. If cursor P1 refers to an SQL index, then P3 is the first in an array of P4 registers that are used as an unpacked index key. Reposition cursor P1 so that it points to the largest entry that is less than or equal to the key value. If there are no records less than or equal to the key and P2 is not zero, then jump to P2. This opcode leaves the cursor configured to move in reverse order, from the end toward the beginning. In other words, the cursor is configured to use Prev, not Next. If the cursor P1 was opened using the OPFLAG_SEEKEQ flag, then this opcode will either land on a record that exactly matches the key, or else it will cause a jump to P2. When the cursor is OPFLAG_SEEKEQ, this opcode must be followed by an IdxLE opcode with the same arguments. The IdxGE opcode will be skipped if this opcode succeeds, but the IdxGE opcode will be used on subsequent loop iterations. The OPFLAG_SEEKEQ flags is a hint to the btree layer to say that this is an equality search. See also: Found, NotFound, SeekGt, SeekGe, SeekLt" }, OpCodeDescription { name: "SeekLT", description: "If cursor P1 refers to an SQL table (B-Tree that uses integer keys), use the value in register P3 as a key. If cursor P1 refers to an SQL index, then P3 is the first in an array of P4 registers that are used as an unpacked index key. Reposition cursor P1 so that it points to the largest entry that is less than the key value. If there are no records less than the key and P2 is not zero, then jump to P2. This opcode leaves the cursor configured to move in reverse order, from the end toward the beginning. In other words, the cursor is configured to use Prev, not Next. See also: Found, NotFound, SeekGt, SeekGe, SeekLe" }, OpCodeDescription { name: "SeekRowid", description: "P1 is the index of a cursor open on an SQL table btree (with integer keys). If register P3 does not contain an integer or if P1 does not contain a record with rowid P3 then jump immediately to P2. Or, if P2 is 0, raise an SQLITE_CORRUPT error. If P1 does contain a record with rowid P3 then leave the cursor pointing at that record and fall through to the next instruction. The NotExists opcode performs the same operation, but with NotExists the P3 register must be guaranteed to contain an integer value. With this opcode, register P3 might not contain an integer. The NotFound opcode performs the same operation on index btrees (with arbitrary multi-value keys). This opcode leaves the cursor in a state where it cannot be advanced in either direction. In other words, the Next and Prev opcodes will not work following this opcode. See also: Found, NotFound, NoConflict, SeekRowid" }, OpCodeDescription { name: "SeekScan", description: "This opcode is a prefix opcode to SeekGE. In other words, this opcode must be immediately followed by SeekGE. This constraint is checked by assert() statements. This opcode uses the P1 through P4 operands of the subsequent SeekGE. In the text that follows, the operands of the subsequent SeekGE opcode are denoted as SeekOP.P1 through SeekOP.P4. Only the P1, P2 and P5 operands of this opcode are also used, and are called This.P1, This.P2 and This.P5. This opcode helps to optimize IN operators on a multi-column index where the IN operator is on the later terms of the index by avoiding unnecessary seeks on the btree, substituting steps to the next row of the b-tree instead. A correct answer is obtained if this opcode is omitted or is a no-op. The SeekGE.P3 and SeekGE.P4 operands identify an unpacked key which is the desired entry that we want the cursor SeekGE.P1 to be pointing to. Call this SeekGE.P3/P4 row the \"target\". If the SeekGE.P1 cursor is not currently pointing to a valid row, then this opcode is a no-op and control passes through into the SeekGE. If the SeekGE.P1 cursor is pointing to a valid row, then that row might be the target row, or it might be near and slightly before the target row, or it might be after the target row. If the cursor is currently before the target row, then this opcode attempts to position the cursor on or after the target row by invoking sqlite3BtreeStep() on the cursor between 1 and This.P1 times. The This.P5 parameter is a flag that indicates what to do if the cursor ends up pointing at a valid row that is past the target row. If This.P5 is false (0) then a jump is made to SeekGE.P2. If This.P5 is true (non-zero) then a jump is made to This.P2. The P5==0 case occurs when there are no inequality constraints to the right of the IN constraint. The jump to SeekGE.P2 ends the loop. The P5!=0 case occurs when there are inequality constraints to the right of the IN operator. In that case, the This.P2 will point either directly to or to setup code prior to the IdxGT or IdxGE opcode that checks for loop terminate. Possible outcomes from this opcode: If the cursor is initially not pointed to any valid row, description: then fall through into the subsequent SeekGE opcode. If the cursor is left pointing to a row that is before the target row, description: even after making as many as This.P1 calls to sqlite3BtreeNext(), then also fall through into SeekGE. If the cursor is left pointing at the target row, description: either because it was at the target row to begin with or because one or more sqlite3BtreeNext() calls moved the cursor to the target row, then jump to This.P2.., If the cursor started out before the target row and a call to to sqlite3BtreeNext() moved the cursor off the end of the index (indicating that the target row definitely does not exist in the btree) then jump to SeekGE.P2, description: ending the loop. If the cursor ends up on a valid row that is past the target row (indicating that the target row does not exist in the btree) then jump to SeekOP.P2 if This.P5==0 or to This.P2 if This.P5>0." }, OpCodeDescription { name: "Sequence", description: "Find the next available sequence number for cursor P1. Write the sequence number into register P2. The sequence number on the cursor is incremented after this instruction." }, OpCodeDescription { name: "SequenceTest", description: "P1 is a sorter cursor. If the sequence counter is currently zero, jump to P2. Regardless of whether or not the jump is taken, increment the the sequence value." }, OpCodeDescription { name: "SetCookie", description: "Write the integer value P3 into cookie number P2 of database P1. P2==1 is the schema version. P2==2 is the database format. P2==3 is the recommended pager cache size, and so forth. P1==0 is the main database file and P1==1 is the database file used to store temporary tables. A transaction must be started before executing this opcode. If P2 is the SCHEMA_VERSION cookie (cookie number 1) then the internal schema version is set to P3-P5. The \"PRAGMA schema_version=N\" statement has P5 set to 1, so that the internal schema version will be different from the database schema version, resulting in a schema reset." }, OpCodeDescription { name: "SetSubtype", description: "Set the subtype value of register P2 to the integer from register P1. If P1 is NULL, clear the subtype from p2." }, OpCodeDescription { name: "ShiftLeft", description: "Shift the integer value in register P2 to the left by the number of bits specified by the integer in register P1. Store the result in register P3. If either input is NULL, the result is NULL." }, OpCodeDescription { name: "ShiftRight", description: "Shift the integer value in register P2 to the right by the number of bits specified by the integer in register P1. Store the result in register P3. If either input is NULL, the result is NULL." }, OpCodeDescription { name: "SoftNull", description: "Set register P1 to have the value NULL as seen by the MakeRecord instruction, but do not free any string or blob memory associated with the register, so that if the value was a string or blob that was previously copied using SCopy, the copies will continue to be valid." }, OpCodeDescription { name: "Sort", description: "This opcode does exactly the same thing as Rewind except that it increments an undocumented global variable used for testing. Sorting is accomplished by writing records into a sorting index, then rewinding that index and playing it back from beginning to end. We use the Sort opcode instead of Rewind to do the rewinding so that the global variable will be incremented and regression tests can determine whether or not the optimizer is correctly optimizing out sorts." }, OpCodeDescription { name: "SorterCompare", description: "P1 is a sorter cursor. This instruction compares a prefix of the record blob in register P3 against a prefix of the entry that the sorter cursor currently points to. Only the first P4 fields of r[P3] and the sorter record are compared. If either P3 or the sorter contains a NULL in one of their significant fields (not counting the P4 fields at the end which are ignored) then the comparison is assumed to be equal. Fall through to next instruction if the two records compare equal to each other. Jump to P2 if they are different." }, OpCodeDescription { name: "SorterData", description: "Write into register P2 the current sorter data for sorter cursor P1. Then clear the column header cache on cursor P3. This opcode is normally used to move a record out of the sorter and into a register that is the source for a pseudo-table cursor created using OpenPseudo. That pseudo-table cursor is the one that is identified by parameter P3. Clearing the P3 column cache as part of this opcode saves us from having to issue a separate NullRow instruction to clear that cache." }, OpCodeDescription { name: "SorterInsert", description: "Register P2 holds an SQL index key made using the MakeRecord instructions. This opcode writes that key into the sorter P1. Data for the entry is nil." }, OpCodeDescription { name: "SorterNext", description: "This opcode works just like Next except that P1 must be a sorter object for which the SorterSort opcode has been invoked. This opcode advances the cursor to the next sorted record, or jumps to P2 if there are no more sorted records." }, OpCodeDescription { name: "SorterOpen", description: "This opcode works like OpenEphemeral except that it opens a transient index that is specifically designed to sort large tables using an external merge-sort algorithm. If argument P3 is non-zero, then it indicates that the sorter may assume that a stable sort considering the first P3 fields of each key is sufficient to produce the required results." }, OpCodeDescription { name: "SorterSort", description: "After all records have been inserted into the Sorter object identified by P1, invoke this opcode to actually do the sorting. Jump to P2 if there are no records to be sorted. This opcode is an alias for Sort and Rewind that is used for Sorter objects." }, OpCodeDescription { name: "SqlExec", description: "Run the SQL statement or statements specified in the P4 string. The P1 parameter is a bitmask of options: 0x0001 Disable Auth and Trace callbacks while the statements in P4 are running. 0x0002 Set db->nAnalysisLimit to P2 while the statements in P4 are running." }, OpCodeDescription { name: "String", description: "The string value P4 of length P1 (bytes) is stored in register P2. If P3 is not zero and the content of register P3 is equal to P5, then the datatype of the register P2 is converted to BLOB. The content is the same sequence of bytes, it is merely interpreted as a BLOB instead of a string, as if it had been CAST. In other words: if( P3!=0 and reg[P3]==P5 ) reg[P2] := CAST(reg[P2] as BLOB)" }, OpCodeDescription { name: "String8", description: "P4 points to a nul terminated UTF-8 string. This opcode is transformed into a String opcode before it is executed for the first time. During this transformation, the length of string P4 is computed and stored as the P1 parameter." }, OpCodeDescription { name: "Subtract", description: "Subtract the value in register P1 from the value in register P2 and store the result in register P3. If either input is NULL, the result is NULL." }, OpCodeDescription { name: "TableLock", description: "Obtain a lock on a particular table. This instruction is only used when the shared-cache feature is enabled. P1 is the index of the database in sqlite3.aDb[] of the database on which the lock is acquired. A readlock is obtained if P3==0 or a write lock if P3==1. P2 contains the root-page of the table to lock. P4 contains a pointer to the name of the table being locked. This is only used to generate an error message if the lock cannot be obtained." }, OpCodeDescription { name: "Trace", description: "Write P4 on the statement trace output if statement tracing is enabled. Operand P1 must be 0x7fffffff and P2 must positive." }, OpCodeDescription { name: "Transaction", description: "Begin a transaction on database P1 if a transaction is not already active. If P2 is non-zero, then a write-transaction is started, or if a read-transaction is already active, it is upgraded to a write-transaction. If P2 is zero, then a read-transaction is started. If P2 is 2 or more then an exclusive transaction is started. P1 is the index of the database file on which the transaction is started. Index 0 is the main database file and index 1 is the file used for temporary tables. Indices of 2 or more are used for attached databases. If a write-transaction is started and the Vdbe.usesStmtJournal flag is true (this flag is set if the Vdbe may modify more than one row and may throw an ABORT exception), a statement transaction may also be opened. More specifically, a statement transaction is opened iff the database connection is currently not in autocommit mode, or if there are other active statements. A statement transaction allows the changes made by this VDBE to be rolled back after an error without having to roll back the entire transaction. If no error is encountered, the statement transaction will automatically commit when the VDBE halts. If P5!=0 then this opcode also checks the schema cookie against P3 and the schema generation counter against P4. The cookie changes its value whenever the database schema changes. This operation is used to detect when that the cookie has changed and that the current process needs to reread the schema. If the schema cookie in P3 differs from the schema cookie in the database header or if the schema generation counter in P4 differs from the current generation counter, then an SQLITE_SCHEMA error is raised and execution halts. The sqlite3_step() wrapper function might then reprepare the statement and rerun it from the beginning." }, OpCodeDescription { name: "TypeCheck", description: "Apply affinities to the range of P2 registers beginning with P1. Take the affinities from the Table object in P4. If any value cannot be coerced into the correct type, then raise an error. This opcode is similar to Affinity except that this opcode forces the register type to the Table column type. This is used to implement \"strict affinity\". GENERATED ALWAYS AS ... STATIC columns are only checked if P3 is zero. When P3 is non-zero, no type checking occurs for static generated columns. Virtual columns are computed at query time and so they are never checked. Preconditions: P2 should be the number of non-virtual columns in the table of P4. Table P4 should be a STRICT table. If any precondition is false, an assertion fault occurs." }, OpCodeDescription { name: "Vacuum", description: "Vacuum the entire database P1. P1 is 0 for \"main\", and 2 or more for an attached database. The \"temp\" database may not be vacuumed. If P2 is not zero, then it is a register holding a string which is the file into which the result of vacuum should be written. When P2 is zero, the vacuum overwrites the original database." }, OpCodeDescription { name: "Variable", description: "Transfer the values of bound parameter P1 into register P2" }, OpCodeDescription { name: "VBegin", description: "P4 may be a pointer to an sqlite3_vtab structure. If so, call the xBegin method for that table. Also, whether or not P4 is set, check that this is not being called from within a callback to a virtual table xSync() method. If it is, the error code will be set to SQLITE_LOCKED." }, OpCodeDescription { name: "VCheck", description: "P4 is a pointer to a Table object that is a virtual table in schema P1 that supports the xIntegrity() method. This opcode runs the xIntegrity() method for that virtual table, using P3 as the integer argument. If an error is reported back, the table name is prepended to the error message and that message is stored in P2. If no errors are seen, register P2 is set to NULL." }, OpCodeDescription { name: "VColumn", description: "Store in register P3 the value of the P2-th column of the current row of the virtual-table of cursor P1. If the VColumn opcode is being used to fetch the value of an unchanging column during an UPDATE operation, then the P5 value is OPFLAG_NOCHNG. This will cause the sqlite3_vtab_nochange() function to return true inside the xColumn method of the virtual table implementation. The P5 column might also contain other bits (OPFLAG_LENGTHARG or OPFLAG_TYPEOFARG) but those bits are unused by VColumn." }, OpCodeDescription { name: "VCreate", description: "P2 is a register that holds the name of a virtual table in database P1. Call the xCreate method for that table." }, OpCodeDescription { name: "VDestroy", description: "P4 is the name of a virtual table in database P1. Call the xDestroy method of that table." }, OpCodeDescription { name: "VFilter", description: "P1 is a cursor opened using VOpen. P2 is an address to jump to if the filtered result set is empty. P4 is either NULL or a string that was generated by the xBestIndex method of the module. The interpretation of the P4 string is left to the module implementation. This opcode invokes the xFilter method on the virtual table specified by P1. The integer query plan parameter to xFilter is stored in register P3. Register P3+1 stores the argc parameter to be passed to the xFilter method. Registers P3+2..P3+1+argc are the argc additional parameters which are passed to xFilter as argv. Register P3+2 becomes argv[0] when passed to xFilter. A jump is made to P2 if the result set after filtering would be empty." }, OpCodeDescription { name: "VInitIn", description: "Set register P2 to be a pointer to a ValueList object for cursor P1 with cache register P3 and output register P3+1. This ValueList object can be used as the first argument to sqlite3_vtab_in_first() and sqlite3_vtab_in_next() to extract all of the values stored in the P1 cursor. Register P3 is used to hold the values returned by sqlite3_vtab_in_first() and sqlite3_vtab_in_next()." }, OpCodeDescription { name: "VNext", description: "Advance virtual table P1 to the next row in its result set and jump to instruction P2. Or, if the virtual table has reached the end of its result set, then fall through to the next instruction." }, OpCodeDescription { name: "VOpen", description: "P4 is a pointer to a virtual table object, an sqlite3_vtab structure. P1 is a cursor number. This opcode opens a cursor to the virtual table and stores that cursor in P1." }, OpCodeDescription { name: "VRename", description: "P4 is a pointer to a virtual table object, an sqlite3_vtab structure. This opcode invokes the corresponding xRename method. The value in register P1 is passed as the zName argument to the xRename method." }, OpCodeDescription { name: "VUpdate", description: "P4 is a pointer to a virtual table object, an sqlite3_vtab structure. This opcode invokes the corresponding xUpdate method. P2 values are contiguous memory cells starting at P3 to pass to the xUpdate invocation. The value in register (P3+P2-1) corresponds to the p2th element of the argv array passed to xUpdate. The xUpdate method will do a DELETE or an INSERT or both. The argv[0] element (which corresponds to memory cell P3) is the rowid of a row to delete. If argv[0] is NULL then no deletion occurs. The argv[1] element is the rowid of the new row. This can be NULL to have the virtual table select the new rowid for itself. The subsequent elements in the array are the values of columns in the new row. If P2==1 then no insert is performed. argv[0] is the rowid of a row to delete. P1 is a boolean flag. If it is set to true and the xUpdate call is successful, then the value returned by sqlite3_last_insert_rowid() is set to the value of the rowid for the row just inserted. P5 is the error actions (OE_Replace, OE_Fail, OE_Ignore, etc) to apply in the case of a constraint failure on an insert or update." }, OpCodeDescription { name: "Yield", description: "Swap the program counter with the value in register P1. This has the effect of yielding to a coroutine. If the coroutine that is launched by this instruction ends with Yield or Return then continue to the next instruction. But if the coroutine launched by this instruction ends with EndCoroutine, then jump to P2 rather than continuing with the next instruction. See also: InitCoroutine" }, OpCodeDescription { name: "ZeroOrNull", description: "If both registers P1 and P3 are NOT NULL, then store a zero in register P2. If either registers P1 or P3 are NULL then put a NULL in register P2." }, ]; ================================================ FILE: cli/read_state_machine.rs ================================================ use std::ops::ControlFlow; use itertools::Itertools; /// State machine for determining if a SQL statement is complete. /// Based on SQLite's `sqlite3_complete()` from src/complete.c /// /// This handles the tricky case of triggers which contain semicolons /// in their body but should only be considered complete when the /// `;END;` pattern is seen. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ReadState { /// No non-whitespace seen yet (initial state) #[default] Invalid, /// A complete statement was just finished (terminal state) Start, /// In the middle of an ordinary statement Normal, /// Saw EXPLAIN at the start, watching for CREATE Explain, /// Saw CREATE (possibly after EXPLAIN), watching for TRIGGER Create, /// Inside a trigger definition, need ;END; to escape Trigger, /// Just saw a semicolon inside a trigger, looking for END Semi, /// Saw ;END in trigger, one more semicolon completes it End, } /// Token types recognized by the state machine #[expect(clippy::enum_variant_names)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Token { TkSemi, TkWhitespace, TkOther, TkExplain, TkCreate, TkTemp, TkTrigger, TkEnd, } struct Tokenizer<'a> { chars: std::iter::Peekable>, } impl<'a> Tokenizer<'a> { fn new(chars: std::iter::Peekable>) -> Self { Self { chars } } /// Read an identifier/keyword and classify it fn read_keyword(&mut self, first: char) -> Token { let word: String = std::iter::once(first) .chain( self.chars .peeking_take_while(|c| c.is_ascii_alphanumeric() || *c == '_'), ) .collect(); match word.to_ascii_uppercase().as_str() { "EXPLAIN" => Token::TkExplain, "CREATE" => Token::TkCreate, "TEMP" | "TEMPORARY" => Token::TkTemp, "TRIGGER" => Token::TkTrigger, "END" => Token::TkEnd, _ => Token::TkOther, } } } impl<'a> Iterator for Tokenizer<'a> { type Item = Token; fn next(&mut self) -> Option { loop { let c = self.chars.next()?; let token = match c { '\'' | '"' | '`' | '[' => { let end_char = if c == '[' { ']' } else { c }; // Consumes all tokens between the delimeters self.chars .by_ref() .take_while_inclusive(|&ch| ch != end_char) .for_each(drop); continue; } // Handle Comments '-' if self.chars.peek() == Some(&'-') => { self.chars.next(); // Consume second `-` // Consume until you find a new line self.chars.by_ref().find(|&ch| ch == '\n'); continue; } '/' if self.chars.peek() == Some(&'*') => { // Consumes until you find a `*/` let _ = self.chars.by_ref().try_fold(false, |saw_star, c| { if saw_star && c == '/' { ControlFlow::Break(()) } else { ControlFlow::Continue(c == '*') } }); continue; } ';' => Token::TkSemi, c if c.is_ascii_whitespace() => Token::TkWhitespace, c if c.is_ascii_alphabetic() || c == '_' => self.read_keyword(c), _ => Token::TkOther, }; break Some(token); } } } impl ReadState { /// Returns true if the state machine is in a "complete" state, /// meaning the accumulated SQL forms a complete statement. pub fn is_complete(&self) -> bool { matches!(self, ReadState::Start) } // Copied form SQLite /// Process a single character and return the new state. /// This should be called for each character in the input. fn transition(&self, token: Token) -> ReadState { use ReadState::*; use Token::*; match (self, token) { // State 0: INVALID - nothing meaningful seen yet (Invalid, TkSemi) => Start, (Invalid, TkWhitespace) => Invalid, (Invalid, TkOther) => Normal, (Invalid, TkExplain) => Explain, (Invalid, TkCreate) => Create, (Invalid, TkTemp) => Normal, (Invalid, TkTrigger) => Normal, (Invalid, TkEnd) => Normal, // State 1: START - complete statement, ready for new one (Start, TkSemi) => Start, (Start, TkWhitespace) => Start, (Start, TkOther) => Normal, (Start, TkExplain) => Explain, (Start, TkCreate) => Create, (Start, TkTemp) => Normal, (Start, TkTrigger) => Normal, (Start, TkEnd) => Normal, // State 2: NORMAL - in middle of ordinary statement (Normal, TkSemi) => Start, (Normal, TkWhitespace) => Normal, (Normal, _) => Normal, // State 3: EXPLAIN - saw EXPLAIN, watching for CREATE (Explain, TkSemi) => Start, (Explain, TkWhitespace) => Explain, (Explain, TkOther) => Explain, (Explain, TkExplain) => Normal, (Explain, TkCreate) => Create, (Explain, TkTemp) => Normal, (Explain, TkTrigger) => Normal, (Explain, TkEnd) => Normal, // State 4: CREATE - saw CREATE, watching for TRIGGER (Create, TkSemi) => Start, (Create, TkWhitespace) => Create, (Create, TkOther) => Normal, (Create, TkExplain) => Normal, (Create, TkCreate) => Normal, (Create, TkTemp) => Create, // CREATE TEMP still watching (Create, TkTrigger) => Trigger, // Enter trigger mode! (Create, TkEnd) => Normal, // State 5: TRIGGER - inside trigger body, need ;END; to escape (Trigger, TkSemi) => Semi, (Trigger, TkWhitespace) => Trigger, (Trigger, _) => Trigger, // State 6: SEMI - saw ; in trigger, looking for END (Semi, TkSemi) => Semi, (Semi, TkWhitespace) => Semi, (Semi, TkEnd) => End, (Semi, _) => Trigger, // false alarm, back to body // State 7: END - saw ;END, one more ; completes (End, TkSemi) => Start, // ;END; - COMPLETE! (End, TkWhitespace) => End, (End, _) => Trigger, // false alarm } } /// Process a SQL string and update the state. /// Returns the new state after processing all input. pub fn process(&mut self, sql: &str) { let chars = sql.chars().peekable(); *self = Tokenizer::new(chars).fold(*self, |state, token| state.transition(token)); } } #[cfg(test)] mod tests { use super::*; fn is_complete(sql: &str) -> bool { let mut state = ReadState::default(); state.process(sql); state.is_complete() } #[test] fn test_simple_statements() { assert!(is_complete("SELECT 1;")); assert!(is_complete("SELECT * FROM foo;")); assert!(is_complete("INSERT INTO foo VALUES (1, 2, 3);")); assert!(!is_complete("SELECT 1")); assert!(!is_complete("SELECT * FROM")); } #[test] fn test_multiple_statements() { assert!(is_complete("SELECT 1; SELECT 2;")); assert!(!is_complete("SELECT 1; SELECT 2")); } #[test] fn test_string_with_semicolon() { assert!(!is_complete("SELECT ';'")); assert!(is_complete("SELECT ';';")); assert!(!is_complete("SELECT 'test;test'")); assert!(is_complete("SELECT 'test;test';")); } #[test] fn test_comments() { assert!(is_complete("SELECT 1; -- comment")); assert!(!is_complete("SELECT 1 -- comment;")); assert!(is_complete("SELECT /* ; */ 1;")); assert!(!is_complete("SELECT 1 /* ; */")); } #[test] fn test_simple_trigger() { let trigger = r#" CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); END; "#; assert!(is_complete(trigger)); } #[test] fn test_trigger_incomplete() { let trigger = r#" CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); "#; assert!(!is_complete(trigger)); } #[test] fn test_trigger_multiple_statements() { let trigger = r#" CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); UPDATE stats SET count = count + 1; END; "#; assert!(is_complete(trigger)); } #[test] fn test_create_temp_trigger() { let trigger = r#" CREATE TEMP TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); END; "#; assert!(is_complete(trigger)); } #[test] fn test_create_temporary_trigger() { let trigger = r#" CREATE TEMPORARY TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); END; "#; assert!(is_complete(trigger)); } #[test] fn test_explain_create_trigger() { let trigger = r#" EXPLAIN CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('inserted'); END; "#; assert!(is_complete(trigger)); } #[test] fn test_end_in_string_inside_trigger() { // END inside a string shouldn't end the trigger let trigger = r#" CREATE TRIGGER log_insert AFTER INSERT ON users BEGIN INSERT INTO log VALUES('END'); END; "#; assert!(is_complete(trigger)); } #[test] fn test_create_table_not_trigger() { assert!(is_complete("CREATE TABLE foo (id INT);")); assert!(!is_complete("CREATE TABLE foo (id INT)")); } #[test] fn test_empty_and_whitespace() { assert!(!is_complete("")); assert!(!is_complete(" ")); assert!(!is_complete("\n\t\n")); assert!(is_complete(";")); assert!(is_complete(" ; ")); } #[test] fn test_quoted_identifiers() { assert!(is_complete(r#"SELECT "column;name" FROM foo;"#)); assert!(is_complete("SELECT `column;name` FROM foo;")); assert!(is_complete("SELECT [column;name] FROM foo;")); } #[test] fn test_escaped_quotes() { assert!(is_complete("SELECT 'it''s';")); assert!(is_complete(r#"SELECT "col""name";"#)); } #[test] fn test_non_terminated_literal() { assert!(!is_complete( "create virtual table t1 using csv(data=\"12');" )); } } // create virtual table t1 using csv(data=\"12'); ================================================ FILE: cli/sync_server.mdx ================================================ --- name: local-sync-server --- Generate MDX documentation file in Mintlify format for the sync engine open protocol You will be provided with a prompt which is used to generate simple implementation of the server. Your task is to extract protocol implementation from this prompt and compose a documentation with PROTOCOL description: - Document endpoint which MUST be implemented to implement sync engine protocol - Document sql over http protocol which is used for `/v2/pipeline` endpoint - Document `/pull-updates` endpoint - The documentation MUST ONLY mention contracts, logic and flow. No code should be mentioned Structure documentation well and Use Mintlify rich formatting tools in order to make documentation easy to read and navigatable: * Give high level overview of sync protocol and its main components in the beginning * This is protocol to support bidirectional of sqlite-compatible database between server and client * Describe /v2/pipeline endpoint * List contract spec * Describe /pull-updates endpoint * Use mintlify components if appropriate * ```....``` code blocks to emit contracts like protobuf schemas * Callouts (``, ``, etc) to extract portion of information into separate block visible to the reader * Use markdown markup features if appropriate * Headers and subheaders for structuring the docs and automatically build table of content by Mintlify * Text emphasis (bold, italic) to highlight important words # Goal Generate simple implementation of sync server using tursodatabase - rewrite of the SQLite. The implementation must maintain turso database file locally at given path and disable checkpoint for it. ```rs use anyhow::Result; // ... more imports here ... pub struct TursoSyncServer { // listen address (e.g. 0.0.0.0:8080) address: String, conn: Arc>>, // stop server if interrupt_count > 0 (do this check in the main server event loop) interrupt_count: Arc, } impl TursoSyncServer { pub fn new(address: String, conn: Arc, interrupt_count: Arc) -> Self { Self { address, conn: Arc::new(Mutex::new(conn)), interrupt_count, } } pub fn run(&self) -> Result<()> { // implement logic here } fn sql_over_http(&self, query: ...) -> Result<()> { ... } fn pull_updates(&self, query: ...) -> Result<()> { ... } } ``` - Use `::decode(...)` to access methods associated with the trait `prost::Message` - `from_i32` method in `prost` is deprecated * `use of deprecated associated function `turso_sync_engine::server_proto::PageUpdatesEncodingReq::from_i32`: Use the TryFrom implementation instead` - Start separate thread to monitor `interrupt_count` because otherwise server will be blocked at syscall and will be unable to shutdown - Add logging * Trace all request information with tracing::info!(...) * Add debug logs for more low level info about sync server internal logic * Add error logs if SQL over http execution statement failed or something bad happened * Add debug logs with executed SQL statements to simplify tracing execution - Use `run_collect_rows`/`run_ignore_rows` helpers to execute `Statement` - Implement simple non-chunked http/1.1 server - always respond with full Content-Length - Use `application/protobuf` as content type for protobuf payloads - Format HTTP response only in one place - all internal functions must return either raw body (without headers and http preamble) or some Rust structs - Server must process one request at the time (`/v2/pipeline` or `/pull-updates`) in order to provide simple and safe concurrency guarantees * Note, that you MUST hold the lock for the whole duration of request execution * If you will drop lock guard soon - you will allow multiple connections to be processed at the same time ```rs let conn = { self.conn.lock().unwrap().clone() }; // this is wrong! ``` - USE rocket library for http server - DO NOT use tokio - use simple threads instead # Endpoints Sync server must support 2 endpoints: 1. POST /v2/pipeline - Method executes hrana (SQL-over-HTTP) commands encoded with JSON 2. POST /pull-updates endpoint which fetch page updates since revision provided by the client - Method expects `PullUpdatesRequest` protobuf message and respond with sequence of length-delimited messages - First it respond with `PullUpdatesResponseHeader` - After, it sends multiple `PullUpdatesPageData` - The method recognizes protobuf contracts sent by HTTP/1.1 - Implement simple http/1.1 server but use binary protobuf as payloads: - You MUST return an error if zstd encoding is used - You MUST ignore `server_query_selector` field - You MUST ignore client_pages field - You MUST decode `server_pages_selector` and send only pages from the selector if it is set - BE CAREFUL: sync protocol use zero-based page identifers while core API sometimes uses 1-based indexing The contracts which client uses to interact with server (protobufs and JSONs) are listed here: # Core API The main database API is in the lib.rs: - Generate outline of the turso core API which later can be used to write code easily without errors - Include information about main Database/Connection/Statement methods - Includ Rust signatures in order to use the outline for code generation - Document extra WAL API as it will be important later # pull updates - In order to implement pull-updates endpoint use extra WAL API exposed by the core database api: `wal_state` and `wal_get_frame` - Use offset in WAL files as a simplest revision string - `wal_get_frame` reads the frame from the WAL which has additional 24 bytes header with extra meta * pub const WAL_FRAME_HEADER_SIZE: usize = 24; * Do not forget to cut this meta to get page content - sync server must work only with page_size = 4096 (4kb) - If server_revision is not set - take latest commited frame offset - If client_revision is not set - use zero for this - Pull updates logic should take all frames between [client_revision..server_revision] and send latest versions of unique pages changed in this range * Iterates backward and maintain HashSet of changes pages in memory during request execution * Filter out pages which are not included in the server_pages_selector if it was set # sql over http - Implement batch conditions - they are important especiall auto-commit handling - Implement only positional arguments - named parameters are not used in the sync protocol at client side # dependencies Currently available dependencies are: ================================================ FILE: cli/sync_server.rs ================================================ use std::collections::HashSet; use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; use anyhow::{anyhow, Result}; use bytes::Bytes; use prost::Message; use roaring::RoaringBitmap; use tracing::{debug, error, info}; use turso_core::{Connection, Value as CoreValue}; use turso_sync_engine::server_proto::{ BatchCond, BatchResult, BatchStep, BatchStreamReq, BatchStreamResp, Col, Error, ExecuteStreamReq, ExecuteStreamResp, PageData, PageSetRawEncodingProto, PageUpdatesEncodingReq, PipelineReqBody, PipelineRespBody, PullUpdatesReqProtoBody, PullUpdatesRespProtoBody, Row, StmtResult, StreamRequest, StreamResponse, StreamResult, Value, }; const WAL_FRAME_HEADER_SIZE: usize = 24; const PAGE_SIZE: usize = 4096; pub struct TursoSyncServer { address: String, conn: Arc>>, interrupt_count: Arc, } impl TursoSyncServer { pub fn new(address: String, conn: Arc, interrupt_count: Arc) -> Self { conn.wal_auto_checkpoint_disable(); Self { address, conn: Arc::new(Mutex::new(conn)), interrupt_count, } } pub fn run(&self) -> Result<()> { info!("Starting TursoSyncServer on {}", self.address); let listener = TcpListener::bind(&self.address)?; listener.set_nonblocking(true)?; let interrupt_count = self.interrupt_count.clone(); let shutdown_flag = Arc::new(std::sync::atomic::AtomicBool::new(false)); let shutdown_flag_clone = shutdown_flag.clone(); let monitor_handle = thread::spawn(move || loop { if interrupt_count.load(Ordering::SeqCst) > 0 { debug!("Interrupt detected, signaling shutdown"); shutdown_flag_clone.store(true, Ordering::SeqCst); break; } thread::sleep(std::time::Duration::from_millis(100)); }); loop { if shutdown_flag.load(Ordering::SeqCst) { info!("Shutdown signal received, stopping server"); break; } match listener.accept() { Ok((stream, addr)) => { info!("Accepted connection from {}", addr); if let Err(e) = self.handle_connection(stream) { error!("Error handling connection: {}", e); } } Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { thread::sleep(std::time::Duration::from_millis(10)); continue; } Err(e) => { error!("Error accepting connection: {}", e); } } } let _ = monitor_handle.join(); info!("TursoSyncServer stopped"); Ok(()) } fn handle_connection(&self, mut stream: TcpStream) -> Result<()> { stream.set_nonblocking(false)?; stream.set_read_timeout(Some(std::time::Duration::from_secs(30)))?; let mut buffer = [0u8; 8192]; let mut request_data = Vec::new(); loop { let n = stream.read(&mut buffer)?; if n == 0 { break; } request_data.extend_from_slice(&buffer[..n]); if let Some(header_end) = find_header_end(&request_data) { let headers = String::from_utf8_lossy(&request_data[..header_end]); if let Some(content_length) = parse_content_length(&headers) { let body_start = header_end + 4; let total_expected = body_start + content_length; while request_data.len() < total_expected { let n = stream.read(&mut buffer)?; if n == 0 { break; } request_data.extend_from_slice(&buffer[..n]); } } break; } } let (method, path, body) = parse_http_request(&request_data)?; info!("Request: {} {}", method, path); let response = match (method.as_str(), path.as_str()) { ("POST", "/v2/pipeline") => { debug!("Handling /v2/pipeline request"); self.handle_pipeline(&body) } ("POST", "/pull-updates") => { debug!("Handling /pull-updates request"); self.handle_pull_updates(&body) } _ => { info!("Unknown endpoint: {} {}", method, path); Ok(HttpResponse { status: 404, content_type: "text/plain".to_string(), body: b"Not Found".to_vec(), }) } }; let http_response = match response { Ok(resp) => resp, Err(e) => { error!("Request error: {}", e); HttpResponse { status: 500, content_type: "text/plain".to_string(), body: format!("Internal Server Error: {e}").into_bytes(), } } }; let response_bytes = format_http_response(&http_response); stream.write_all(&response_bytes)?; stream.flush()?; Ok(()) } fn handle_pipeline(&self, body: &[u8]) -> Result { let req: PipelineReqBody = serde_json::from_slice(body) .map_err(|e| anyhow!("Failed to parse pipeline request: {}", e))?; debug!("Pipeline request: {:?}", req); let conn = self.conn.lock().unwrap(); let mut results = Vec::new(); for request in req.requests { let result = match request { StreamRequest::Execute(exec_req) => self.execute_statement(&conn, &exec_req), StreamRequest::Batch(batch_req) => self.execute_batch(&conn, &batch_req), StreamRequest::None => StreamResult::Error { error: Error { message: "Unknown request type".to_string(), code: "UNKNOWN".to_string(), }, }, }; results.push(result); } let resp = PipelineRespBody { baton: req.baton, base_url: None, results, }; let body = serde_json::to_vec(&resp)?; Ok(HttpResponse { status: 200, content_type: "application/json".to_string(), body, }) } fn execute_statement(&self, conn: &Arc, req: &ExecuteStreamReq) -> StreamResult { let sql = match &req.stmt.sql { Some(s) => s.clone(), None => { return StreamResult::Error { error: Error { message: "No SQL provided".to_string(), code: "NO_SQL".to_string(), }, } } }; debug!("Executing SQL: {}", sql); let mut stmt = match conn.prepare(&sql) { Ok(s) => s, Err(e) => { error!("Failed to prepare statement: {}", e); return StreamResult::Error { error: Error { message: e.to_string(), code: "PREPARE_ERROR".to_string(), }, }; } }; for (i, arg) in req.stmt.args.iter().enumerate() { let core_value = convert_value_to_core(arg); stmt.bind_at(std::num::NonZero::new(i + 1).unwrap(), core_value); } let want_rows = req.stmt.want_rows.unwrap_or(true); if want_rows { match stmt.run_collect_rows() { Ok(rows) => { let cols: Vec
= (0..stmt.num_columns()) .map(|i| Col { name: Some(stmt.get_column_name(i).to_string()), decltype: stmt.get_column_decltype(i), }) .collect(); let result_rows: Vec = rows .into_iter() .map(|row| Row { values: row.into_iter().map(convert_core_to_value).collect(), }) .collect(); StreamResult::Ok { response: StreamResponse::Execute(ExecuteStreamResp { result: StmtResult { cols, rows: result_rows, affected_row_count: 0, last_insert_rowid: None, replication_index: None, rows_read: 0, rows_written: 0, query_duration_ms: 0.0, }, }), } } Err(e) => { error!("Failed to execute statement: {}", e); StreamResult::Error { error: Error { message: e.to_string(), code: "EXECUTE_ERROR".to_string(), }, } } } } else { match stmt.run_ignore_rows() { Ok(()) => StreamResult::Ok { response: StreamResponse::Execute(ExecuteStreamResp { result: StmtResult { cols: vec![], rows: vec![], affected_row_count: 0, last_insert_rowid: None, replication_index: None, rows_read: 0, rows_written: 0, query_duration_ms: 0.0, }, }), }, Err(e) => { error!("Failed to execute statement: {}", e); StreamResult::Error { error: Error { message: e.to_string(), code: "EXECUTE_ERROR".to_string(), }, } } } } } fn execute_batch(&self, conn: &Arc, req: &BatchStreamReq) -> StreamResult { let batch = &req.batch; let mut step_results: Vec> = Vec::with_capacity(batch.steps.len()); let mut step_errors: Vec> = Vec::with_capacity(batch.steps.len()); for (step_idx, step) in batch.steps.iter().enumerate() { let should_execute = match &step.condition { None => true, Some(cond) => Self::evaluate_condition(cond, &step_results, &step_errors, conn), }; if should_execute { let result = self.execute_batch_step(conn, step); match result { Ok(stmt_result) => { step_results.push(Some(stmt_result)); step_errors.push(None); } Err(e) => { error!("Batch step {} failed: {}", step_idx, e); step_results.push(None); step_errors.push(Some(Error { message: e.to_string(), code: "BATCH_STEP_ERROR".to_string(), })); } } } else { step_results.push(None); step_errors.push(None); } } StreamResult::Ok { response: StreamResponse::Batch(BatchStreamResp { result: BatchResult { step_results, step_errors, replication_index: None, }, }), } } fn evaluate_condition( cond: &BatchCond, step_results: &[Option], step_errors: &[Option], conn: &Arc, ) -> bool { match cond { BatchCond::None => true, BatchCond::Ok { step } => { let idx = *step as usize; idx < step_results.len() && step_results[idx].is_some() } BatchCond::Error { step } => { let idx = *step as usize; idx < step_errors.len() && step_errors[idx].is_some() } BatchCond::Not { cond } => { !Self::evaluate_condition(cond, step_results, step_errors, conn) } BatchCond::And(list) => list .conds .iter() .all(|c| Self::evaluate_condition(c, step_results, step_errors, conn)), BatchCond::Or(list) => list .conds .iter() .any(|c| Self::evaluate_condition(c, step_results, step_errors, conn)), BatchCond::IsAutocommit {} => conn.get_auto_commit(), } } fn execute_batch_step(&self, conn: &Arc, step: &BatchStep) -> Result { let sql = step .stmt .sql .as_ref() .ok_or_else(|| anyhow!("No SQL in batch step"))?; debug!("Executing batch step SQL: {}", sql); let mut stmt = conn.prepare(sql)?; for (i, arg) in step.stmt.args.iter().enumerate() { let core_value = convert_value_to_core(arg); stmt.bind_at(std::num::NonZero::new(i + 1).unwrap(), core_value); } let want_rows = step.stmt.want_rows.unwrap_or(true); if want_rows { let rows = stmt.run_collect_rows()?; let cols: Vec= (0..stmt.num_columns()) .map(|i| Col { name: Some(stmt.get_column_name(i).to_string()), decltype: stmt.get_column_decltype(i), }) .collect(); let result_rows: Vec = rows .into_iter() .map(|row| Row { values: row.into_iter().map(convert_core_to_value).collect(), }) .collect(); Ok(StmtResult { cols, rows: result_rows, affected_row_count: 0, last_insert_rowid: None, replication_index: None, rows_read: 0, rows_written: 0, query_duration_ms: 0.0, }) } else { stmt.run_ignore_rows()?; Ok(StmtResult { cols: vec![], rows: vec![], affected_row_count: 0, last_insert_rowid: None, replication_index: None, rows_read: 0, rows_written: 0, query_duration_ms: 0.0, }) } } fn handle_pull_updates(&self, body: &[u8]) -> Result { let req = ::decode(body) .map_err(|e| anyhow!("Failed to decode PullUpdatesRequest: {}", e))?; debug!( "Pull updates request: server_revision={}, client_revision={}", req.server_revision, req.client_revision ); let encoding = PageUpdatesEncodingReq::try_from(req.encoding).unwrap_or(PageUpdatesEncodingReq::Raw); if encoding == PageUpdatesEncodingReq::Zstd { return Err(anyhow!("Zstd encoding is not supported")); } let conn = self.conn.lock().unwrap(); let wal_state = conn.wal_state()?; debug!("WAL state: max_frame={}", wal_state.max_frame); let server_revision: u64 = if req.server_revision.is_empty() { wal_state.max_frame } else { req.server_revision.parse().unwrap_or(wal_state.max_frame) }; let client_revision: u64 = if req.client_revision.is_empty() { 0 } else { req.client_revision.parse().unwrap_or(0) }; debug!( "Using server_revision={}, client_revision={}", server_revision, client_revision ); let pages_selector: Option = if !req.server_pages_selector.is_empty() { Some( RoaringBitmap::deserialize_from(&req.server_pages_selector[..]) .map_err(|e| anyhow!("Failed to parse server_pages_selector: {}", e))?, ) } else { None }; let mut seen_pages: HashSet = HashSet::new(); let mut pages_to_send: Vec<(u32, Vec)> = Vec::new(); let frame_size = WAL_FRAME_HEADER_SIZE + PAGE_SIZE; let mut frame_buffer = vec![0u8; frame_size]; debug!( "pull-updates: scanning WAL frames {}..={} (client_revision={}, server_revision={})", client_revision + 1, server_revision, client_revision, server_revision ); if server_revision > client_revision { for frame_no in (client_revision + 1..=server_revision).rev() { let frame_info = conn.wal_get_frame(frame_no, &mut frame_buffer)?; let page_no = frame_info.page_no; // WAL uses 1-based page numbers, sync protocol uses 0-based let page_id = page_no - 1; if seen_pages.contains(&page_no) { continue; } if let Some(ref selector) = pages_selector { if !selector.contains(page_id) { continue; } } seen_pages.insert(page_no); let type_byte = frame_buffer[WAL_FRAME_HEADER_SIZE]; debug!( "pull-updates: including page_no={}, frame_no={}, type_byte={}, db_size={}", page_no, frame_no, type_byte, frame_info.db_size ); let page_data = frame_buffer[WAL_FRAME_HEADER_SIZE..].to_vec(); pages_to_send.push((page_id, page_data)); } } debug!( "pull-updates: sending {} pages, seen_pages={:?}", pages_to_send.len(), seen_pages ); pages_to_send.reverse(); let db_size = if wal_state.max_frame > 0 { let mut last_frame = vec![0u8; frame_size]; let last_info = conn.wal_get_frame(wal_state.max_frame, &mut last_frame)?; last_info.db_size as u64 } else { 0 }; let header = PullUpdatesRespProtoBody { server_revision: server_revision.to_string(), db_size, raw_encoding: Some(PageSetRawEncodingProto {}), zstd_encoding: None, }; let mut response_body = Vec::new(); let header_bytes = header.encode_to_vec(); encode_length_delimited(&mut response_body, &header_bytes); for (page_id, page_data) in pages_to_send { let page_msg = PageData { page_id: page_id as u64, encoded_page: Bytes::from(page_data), }; let page_bytes = page_msg.encode_to_vec(); encode_length_delimited(&mut response_body, &page_bytes); } debug!( "Sending {} bytes in pull-updates response", response_body.len() ); Ok(HttpResponse { status: 200, content_type: "application/protobuf".to_string(), body: response_body, }) } } struct HttpResponse { status: u16, content_type: String, body: Vec, } fn find_header_end(data: &[u8]) -> Option { (0..data.len().saturating_sub(3)).find(|&i| &data[i..i + 4] == b"\r\n\r\n") } fn parse_content_length(headers: &str) -> Option { for line in headers.lines() { let lower = line.to_lowercase(); if lower.starts_with("content-length:") { let value = line.split(':').nth(1)?.trim(); return value.parse().ok(); } } None } fn parse_http_request(data: &[u8]) -> Result<(String, String, Vec)> { let header_end = find_header_end(data).ok_or_else(|| anyhow!("Invalid HTTP request"))?; let headers = String::from_utf8_lossy(&data[..header_end]); let first_line = headers .lines() .next() .ok_or_else(|| anyhow!("Empty request"))?; let parts: Vec<&str> = first_line.split_whitespace().collect(); if parts.len() < 2 { return Err(anyhow!("Invalid request line")); } let method = parts[0].to_string(); let path = parts[1].to_string(); let body = data[header_end + 4..].to_vec(); Ok((method, path, body)) } fn format_http_response(resp: &HttpResponse) -> Vec { let status_text = match resp.status { 200 => "OK", 404 => "Not Found", 500 => "Internal Server Error", _ => "Unknown", }; let header = format!( "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", resp.status, status_text, resp.content_type, resp.body.len() ); let mut result = header.into_bytes(); result.extend_from_slice(&resp.body); result } fn encode_length_delimited(output: &mut Vec, data: &[u8]) { let mut len = data.len(); while len >= 0x80 { output.push((len as u8) | 0x80); len >>= 7; } output.push(len as u8); output.extend_from_slice(data); } fn convert_value_to_core(value: &Value) -> CoreValue { match value { Value::None | Value::Null => CoreValue::Null, Value::Integer { value } => CoreValue::from_i64(*value), Value::Float { value } => CoreValue::from_f64(*value), Value::Text { value } => CoreValue::Text(turso_core::types::Text { value: std::borrow::Cow::Owned(value.clone()), subtype: turso_core::types::TextSubtype::Text, }), Value::Blob { value } => CoreValue::Blob(value.to_vec()), } } fn convert_core_to_value(value: CoreValue) -> Value { match value { CoreValue::Null => Value::Null, CoreValue::Numeric(turso_core::Numeric::Integer(v)) => Value::Integer { value: v }, CoreValue::Numeric(turso_core::Numeric::Float(v)) => Value::Float { value: f64::from(v), }, CoreValue::Text(t) => Value::Text { value: t.value.to_string(), }, CoreValue::Blob(b) => Value::Blob { value: Bytes::from(b), }, } } ================================================ FILE: cli/tests/non_interactive_exit_code.rs ================================================ use std::io::Write; use std::process::{Command, Stdio}; // --------------------------------------------------------------------------- // A. SQL argument mode // --------------------------------------------------------------------------- /// A1: Success path returns 0 #[test] fn sql_argument_returns_exit_code_zero_on_success() { let status = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .arg("select 'one'; select 'two';") .status() .expect("failed to run tursodb"); assert_eq!(status.code(), Some(0)); } /// A2: Parse/prepare failure returns non-zero #[test] fn sql_argument_returns_exit_code_one_on_query_failure() { let status = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .arg("select 'one'; select * from t; select 'two';") .status() .expect("failed to run tursodb"); assert_eq!(status.code(), Some(1)); } /// A3: Fail-fast on parse/prepare failure — statements after error do not execute #[test] fn sql_argument_stops_execution_after_first_error() { let output = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .arg("select 'one'; select * from t; select 'two';") .output() .expect("failed to run tursodb"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("one"), "first query should execute"); assert!( !stdout.contains("two"), "query after error should not execute" ); assert_eq!(output.status.code(), Some(1)); } /// A4: Runtime/step failure (constraint violation) returns non-zero #[test] fn sql_argument_runtime_error_returns_nonzero() { let sql = "create table t(x integer primary key); \ insert into t values(1); \ insert into t values(1); \ select 'after';"; let status = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .arg(sql) .status() .expect("failed to run tursodb"); assert_eq!(status.code(), Some(1)); } /// A5: Fail-fast on runtime/step failure — statements after constraint violation do not execute #[test] fn sql_argument_runtime_error_stops_execution() { let sql = "create table t(x integer primary key); \ insert into t values(1); \ insert into t values(1); \ select 'after';"; let output = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .arg(sql) .output() .expect("failed to run tursodb"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( !stdout.contains("after"), "query after runtime error should not execute" ); assert_eq!(output.status.code(), Some(1)); } /// A6: Syntax error returns non-zero #[test] fn sql_argument_syntax_error_returns_nonzero() { let status = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .arg("select from;") .status() .expect("failed to run tursodb"); assert_eq!(status.code(), Some(1)); } /// A7: Empty SQL string returns 0 #[test] fn sql_argument_empty_string_returns_zero() { let status = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .arg("") .status() .expect("failed to run tursodb"); assert_eq!(status.code(), Some(0)); } /// A8: sqlite_dbpage updates require unsafe testing mode #[test] fn sqlite_dbpage_update_requires_unsafe_testing() { let sql = "create table t(x); update sqlite_dbpage set data = data where pgno = 1; select 'after_update';"; let output = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .arg(sql) .output() .expect("failed to run tursodb"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( !stdout.contains("after_update"), "query after sqlite_dbpage update should not execute without unsafe testing" ); assert_eq!(output.status.code(), Some(1)); } /// A9: sqlite_dbpage updates succeed with unsafe testing mode #[test] fn sqlite_dbpage_update_allows_unsafe_testing() { let sql = "create table t(x); update sqlite_dbpage set data = data where pgno = 1; select 'after_update';"; let output = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg("--unsafe-testing") .arg(":memory:") .arg(sql) .output() .expect("failed to run tursodb"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("after_update"), "expected query after update to run" ); assert_eq!(output.status.code(), Some(0)); } // --------------------------------------------------------------------------- // B. Piped stdin mode // --------------------------------------------------------------------------- /// B8: Success path returns 0 #[test] fn piped_stdin_returns_exit_code_zero_on_success() { let mut child = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .stdin(Stdio::piped()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("failed to run tursodb"); let mut stdin = child.stdin.take().unwrap(); stdin.write_all(b"select 1;\n").unwrap(); drop(stdin); let status = child.wait().expect("failed to wait"); assert_eq!(status.code(), Some(0)); } /// B9: Parse/prepare failure returns non-zero #[test] fn piped_stdin_returns_exit_code_one_on_query_failure() { let mut child = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .stdin(Stdio::piped()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("failed to run tursodb"); let mut stdin = child.stdin.take().unwrap(); stdin.write_all(b"select * from nonexistent;\n").unwrap(); drop(stdin); let status = child.wait().expect("failed to wait"); assert_eq!(status.code(), Some(1)); } /// B10: Fail-fast in piped multi-statement failure #[test] fn piped_stdin_stops_execution_after_first_error() { let mut child = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn() .expect("failed to run tursodb"); let mut stdin = child.stdin.take().unwrap(); stdin .write_all(b"select 'one'; select * from missing; select 'two';\n") .unwrap(); drop(stdin); let output = child.wait_with_output().expect("failed to wait"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("one"), "first query should execute"); assert!( !stdout.contains("two"), "query after error should not execute" ); assert_eq!(output.status.code(), Some(1)); } /// B11: Runtime/step failure in piped mode returns non-zero #[test] fn piped_stdin_runtime_error_returns_nonzero() { let mut child = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .stdin(Stdio::piped()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("failed to run tursodb"); let mut stdin = child.stdin.take().unwrap(); stdin .write_all( b"create table t(x integer primary key);\n\ insert into t values(1);\n\ insert into t values(1);\n", ) .unwrap(); drop(stdin); let status = child.wait().expect("failed to wait"); assert_eq!(status.code(), Some(1)); } /// C1: .read handles multi-line CREATE TRIGGER correctly #[test] fn dot_read_handles_trigger_statements() { let sql = "\ CREATE TABLE t(id INTEGER PRIMARY KEY, val TEXT);\n\ CREATE TABLE log(msg TEXT);\n\ CREATE TRIGGER tr1 AFTER INSERT ON t BEGIN\n\ INSERT INTO log VALUES ('inserted ' || NEW.val);\n\ END;\n\ INSERT INTO t VALUES (1, 'hello');\n\ SELECT msg FROM log;\n"; let sql_path = std::env::temp_dir().join("limbo_test_dot_read_trigger.sql"); std::fs::write(&sql_path, sql).expect("failed to write sql file"); let dot_read = format!(".read {}", sql_path.display()); let mut child = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .expect("failed to run tursodb"); let mut stdin = child.stdin.take().unwrap(); stdin.write_all(dot_read.as_bytes()).unwrap(); stdin.write_all(b"\n").unwrap(); drop(stdin); let output = child.wait_with_output().expect("failed to wait"); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); std::fs::remove_file(&sql_path).ok(); assert!( !stderr.contains("unexpected end of file"), "trigger should not produce parse errors, stderr: {stderr}" ); assert!( !stderr.contains("no such column"), "NEW.val should be resolved inside trigger, stderr: {stderr}" ); assert!( stdout.contains("inserted hello"), "trigger should fire and insert into log, stdout: {stdout}" ); } /// B12: Empty piped stdin returns 0 #[test] fn piped_stdin_empty_returns_zero() { let mut child = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .stdin(Stdio::piped()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("failed to run tursodb"); // Close stdin immediately — no input drop(child.stdin.take()); let status = child.wait().expect("failed to wait"); assert_eq!(status.code(), Some(0)); } ================================================ FILE: cli/tests/parameter_bindings.rs ================================================ use std::io::Write; use std::process::{Command, Output, Stdio}; fn run_cli(input: &[u8]) -> Output { let mut child = Command::new(env!("CARGO_BIN_EXE_tursodb")) .arg(":memory:") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn() .expect("failed to run tursodb"); let mut stdin = child.stdin.take().expect("failed to take stdin"); stdin.write_all(input).expect("failed to write stdin"); drop(stdin); child.wait_with_output().expect("failed to wait for output") } fn stdout_lines(output: &Output) -> Vec<&str> { let s = std::str::from_utf8(&output.stdout).expect("non-utf8 stdout"); s.lines().collect() } #[test] fn parameter_set_binds_named_slot() { let output = run_cli(b".mode list\n.parameter set :x 41\nselect :x;\n"); assert_eq!(output.status.code(), Some(0)); assert_eq!(stdout_lines(&output), vec!["41"]); } #[test] fn parameter_set_binds_positional_slot() { let output = run_cli(b".mode list\n.parameter set ?1 9\nselect ?1;\n"); assert_eq!(output.status.code(), Some(0)); assert_eq!(stdout_lines(&output), vec!["9"]); } #[test] fn parameter_clear_removes_binding() { let output = run_cli(b".mode list\n.parameter set :x 41\n.parameter clear :x\nselect :x;\n"); assert_eq!(output.status.code(), Some(0)); assert_eq!(stdout_lines(&output), vec![""]); } #[test] fn parameter_set_rejects_bare_name() { let output = run_cli(b".mode list\n.parameter set x 41\nselect :x;\n"); assert_eq!(output.status.code(), Some(0)); let lines = stdout_lines(&output); assert!( lines .iter() .any(|l| l.contains("Error: parameter name must start with one of")), "expected bare-name validation error, got: {lines:?}" ); } #[test] fn parameter_set_supports_quoted_multi_word_text() { let output = run_cli(b".mode list\n.parameter set :msg \"hello world\"\nselect :msg;\n"); assert_eq!(output.status.code(), Some(0)); assert_eq!(stdout_lines(&output), vec!["hello world"]); } #[test] fn parameter_clear_only_removes_requested_name() { let output = run_cli( b".mode list\n.parameter set :x 1\n.parameter set @x 2\n.parameter clear :x\nselect :x, @x;\n", ); assert_eq!(output.status.code(), Some(0)); assert_eq!(stdout_lines(&output), vec!["|2"]); } #[test] fn parameter_set_rejects_zero_positional_index() { let output = run_cli(b".mode list\n.parameter set ?0 41\n"); assert_eq!(output.status.code(), Some(0)); let lines = stdout_lines(&output); assert!( lines .iter() .any(|l| l.contains("?N' must use an index >= 1")), "expected positional index bounds validation error, got: {lines:?}" ); } #[test] fn parameter_set_mixed_named_and_positional() { let output = run_cli( b".mode list\n.parameter set :name alice\n.parameter set ?2 30\nselect :name, ?2;\n", ); assert_eq!(output.status.code(), Some(0)); assert_eq!(stdout_lines(&output), vec!["alice|30"]); } #[test] fn parameter_set_anonymous_positional() { let output = run_cli(b".mode list\n.parameter set ?1 first\n.parameter set ?2 second\nselect ?, ?;\n"); assert_eq!(output.status.code(), Some(0)); assert_eq!(stdout_lines(&output), vec!["first|second"]); } #[test] fn parameter_set_mixed_named_and_anonymous_positional() { let output = run_cli( b".mode list\n.parameter set :name alice\n.parameter set ?2 30\nselect :name, ?;\n", ); assert_eq!(output.status.code(), Some(0)); assert_eq!(stdout_lines(&output), vec!["alice|30"]); } #[test] fn parameter_set_parses_hex_blob_literal() { let output = run_cli(b".mode list\n.parameter set :blob \"x'4142'\"\nselect :blob;\n"); assert_eq!(output.status.code(), Some(0)); assert_eq!(stdout_lines(&output), vec!["AB"]); } ================================================ FILE: core/Cargo.toml ================================================ # Copyright 2023-2025 the Turso authors. All rights reserved. MIT license. [package] name = "turso_core" version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true description = "The Turso database library" [lints] workspace = true [lib] name = "turso_core" path = "lib.rs" [features] default = ["fs", "uuid", "time", "json", "series", "encryption"] tracing_release = ["tracing/release_max_level_info"] conn_raw_api = [] fs = ["turso_ext/vfs"] json = [] uuid = ["dep:uuid"] io_uring = ["dep:io-uring", "rustix/io_uring"] experimental_win_iocp = [] time = [] fuzz = [] omit_autovacuum = [] simulator = ["fuzz", "serde"] serde = ["dep:serde"] series = [] encryption = [] checksum = [] cli_only = [] test_helper = [] bench = [] nanosecond-bench = ["bench"] fts = ["dep:tantivy"] codspeed = ["bench"] optimizer_params = ["serde", "dep:serde_json"] [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { version = "0.61.2", features = [ "Win32_System_IO", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_Diagnostics_Debug", ] } [target.'cfg(target_os = "linux")'.dependencies] io-uring = { version = "0.7.5", optional = true } libc = { version = "0.2.172" } [target.'cfg(target_family = "unix")'.dependencies] polling = "3.7.4" rustix = { version = "1.0.5", features = ["fs"] } libc = { version = "0.2.172" } [target.'cfg(not(target_family = "wasm"))'.dependencies] libloading = "0.8.6" tantivy = { version = "0.25.0", optional = true } [dependencies] turso_ext = { workspace = true, features = ["core_only"] } cfg_block = "0.1.1" fallible-iterator = { workspace = true } hex = { workspace = true } thiserror = { workspace = true } regex = { workspace = true } regex-syntax = { workspace = true, default-features = false, features = [ "unicode", ] } chrono = { workspace = true, default-features = false, features = ["clock"] } rand = { workspace = true } libm = "0.2" turso_macros = { workspace = true } miette = { workspace = true } strum = { workspace = true } parking_lot = { workspace = true, features = ["arc_lock"] } crossbeam-skiplist = "0.1.3" tracing = { workspace = true } ryu = "1.0.19" uncased = "0.9.10" strum_macros = { workspace = true } bitflags = { workspace = true } serde = { workspace = true, optional = true, features = ["derive"] } serde_json = { workspace = true, optional = true } pastey = "0.2.1" uuid = { version = "1.11.0", features = ["v4", "v5", "v7"], optional = true } tempfile = { workspace = true } pack1 = { version = "1.0.0", features = ["bytemuck"] } bytemuck = "1.23.1" aes-gcm = { version = "0.10.3" } aes = { version = "0.8.4" } turso_parser = { workspace = true } twox-hash = "2.1.1" intrusive-collections = "0.9.7" roaring = "0.11.2" arc-swap = "1.7" rustc-hash = "2.0" either = { workspace = true } tracing-subscriber.workspace = true rapidhash = "4.1.1" branches = { version = "0.4.3", default-features = false } bumpalo = { version = "3", features = ["collections"] } smallvec = "1.15.1" fastbloom = "0.14.1" crc32c = "0.6.8" bigdecimal = "0.4" num-bigint = "0.4" num-traits = "0.2" [target.'cfg(not(any(target_family = "wasm", all(target_os = "windows", target_arch = "aarch64"))))'.dependencies] simsimd = "6.5.3" [target.'cfg(antithesis)'.dependencies] antithesis_sdk = { workspace = true, features = ["full"] } serde_json = { workspace = true } [target.'cfg(loom)'.dependencies] loom = { workspace = true } [target.'cfg(shuttle)'.dependencies] shuttle = { workspace = true } # Use pure-rust for Android and MacOS to avoid C cross-compilation issues [target.'cfg(any(target_os = "android", target_os = "macos"))'.dependencies] aegis = { version = "0.9.5", features = ["pure-rust"] } [target.'cfg(not(any(target_os = "android", target_os = "macos")))'.dependencies] aegis = "0.9.5" [build-dependencies] chrono = { workspace = true, default-features = false } built = { version = "0.7.5", features = ["chrono"] } [target.'cfg(not(target_family = "windows"))'.dev-dependencies] pprof = { version = "0.14.0", features = ["criterion", "flamegraph"] } [dev-dependencies] memory-stats = "1.2.0" criterion = { workspace = true, features = [ "html_reports", "async", "async_futures", ] } codspeed-criterion-compat = { workspace = true, features = [ "html_reports", "async", "async_futures", ] } rstest = "0.18.2" rusqlite = { workspace = true, features = ["series"] } quickcheck = { version = "1.0", default-features = false } quickcheck_macros = { version = "1.0", default-features = false } rand_chacha = { workspace = true } env_logger = { workspace = true } test-log = { version = "0.2.17", features = ["trace"] } sorted-vec = "0.8.6" mimalloc = { workspace = true, default-features = false } divan.workspace = true [[bench]] name = "benchmark" harness = false [[bench]] name = "mvcc_benchmark" harness = false [[bench]] name = "json_benchmark" harness = false [[bench]] name = "tpc_h_benchmark" harness = false [[bench]] name = "sql_functions" harness = false required-features = ["bench"] [[bench]] name = "hash_spill_benchmark" harness = false required-features = ["bench"] [[bench]] name = "write_perf_benchmark" harness = false [[bench]] name = "fts_benchmark" harness = false required-features = ["fts"] [[bench]] name = "graph_queries_benchmark" harness = false ================================================ FILE: core/assert.rs ================================================ /// Assert that a type implements Send at compile time. /// Usage: assert_send!(MyType); /// Usage: assert_send!(Type1, Type2, Type3); macro_rules! assert_send { ($($t:ty),+ $(,)?) => { #[cfg(test)] $(const _: () = { const fn _assert_send() {} _assert_send::<$t>(); };)+ }; } pub(crate) use assert_send; /// Assert that a type implements Sync at compile time. /// Usage: assert_sync!(MyType); /// Usage: assert_sync!(Type1, Type2, Type3); macro_rules! assert_sync { ($($t:ty),+ $(,)?) => { #[cfg(test)] $(const _: () = { const fn _assert_sync() {} _assert_sync::<$t>(); };)+ }; } pub(crate) use assert_sync; /// Assert that a type implements both Send and Sync at compile time. /// Usage: assert_send_sync!(MyType); /// Usage: assert_send_sync!(Type1, Type2, Type3); macro_rules! assert_send_sync { ($($t:ty),+ $(,)?) => { #[cfg(test)] $(const _: () = { const fn _assert_send() {} const fn _assert_sync() {} _assert_send::<$t>(); _assert_sync::<$t>(); };)+ }; } pub(crate) use assert_send_sync; ================================================ FILE: core/benches/benchmark.rs ================================================ #[cfg(not(feature = "codspeed"))] use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; #[cfg(not(feature = "codspeed"))] use pprof::criterion::{Output, PProfProfiler}; #[cfg(feature = "codspeed")] use codspeed_criterion_compat::{ black_box, criterion_group, criterion_main, BenchmarkId, Criterion, }; use regex::Regex; use std::{ sync::Arc, time::{Duration, Instant}, }; use tempfile::TempDir; use turso_core::{Database, LimboError, PlatformIO, StepResult}; #[cfg(not(target_family = "wasm"))] #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; fn rusqlite_open() -> rusqlite::Connection { let sqlite_conn = rusqlite::Connection::open("../testing/system/testing.db").unwrap(); sqlite_conn .pragma_update(None, "locking_mode", "EXCLUSIVE") .unwrap(); sqlite_conn } fn setup_rusqlite(temp_dir: &TempDir, query: &str) -> rusqlite::Connection { let db_path = temp_dir.path().join("bench.db"); let sqlite_conn = rusqlite::Connection::open(db_path).unwrap(); sqlite_conn .pragma_update(None, "synchronous", "FULL") .unwrap(); sqlite_conn .pragma_update(None, "journal_mode", "WAL") .unwrap(); sqlite_conn .pragma_update(None, "locking_mode", "EXCLUSIVE") .unwrap(); let journal_mode = sqlite_conn .pragma_query_value(None, "journal_mode", |row| row.get::<_, String>(0)) .unwrap(); assert_eq!(journal_mode.to_lowercase(), "wal"); let synchronous = sqlite_conn .pragma_query_value(None, "synchronous", |row| row.get::<_, usize>(0)) .unwrap(); const FULL: usize = 2; assert_eq!(synchronous, FULL); // load the generate_series extension rusqlite::vtab::series::load_module(&sqlite_conn).unwrap(); // Create test table sqlite_conn.execute(query, []).unwrap(); sqlite_conn } fn bench_open(criterion: &mut Criterion) { // https://github.com/tursodatabase/turso/issues/174 // The rusqlite benchmark crashes on Mac M1 when using the flamegraph features let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); if !std::fs::exists("../testing/system/schema_5k.db").unwrap() { #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/schema_5k.db").unwrap(); let conn = db.connect().unwrap(); for i in 0..5000 { conn.execute( format!("CREATE TABLE table_{i} ( id INTEGER PRIMARY KEY, name TEXT, value INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )") ).unwrap(); } } let mut group = criterion.benchmark_group("Open/Connect"); group.bench_function(BenchmarkId::new("limbo_schema", ""), |b| { b.iter(|| { #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/schema_5k.db").unwrap(); let conn = db.connect().unwrap(); conn.execute("SELECT * FROM table_0").unwrap(); }); }); if enable_rusqlite { group.bench_function(BenchmarkId::new("sqlite_schema", ""), |b| { b.iter(|| { let conn = rusqlite::Connection::open("../testing/system/schema_5k.db").unwrap(); conn.execute("SELECT * FROM table_0", ()).unwrap(); }); }); } group.finish(); } fn bench_alter(criterion: &mut Criterion) { // https://github.com/tursodatabase/turso/issues/174 // The rusqlite benchmark crashes on Mac M1 when using the flamegraph features let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); if !std::fs::exists("../testing/system/schema_5k.db").unwrap() { #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/schema_5k.db").unwrap(); let conn = db.connect().unwrap(); for i in 0..5000 { conn.execute( format!("CREATE TABLE table_{i} ( id INTEGER PRIMARY KEY, name TEXT, value INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )") ).unwrap(); } } let mut group = criterion.benchmark_group("`ALTER TABLE _ RENAME TO _`"); group.bench_function(BenchmarkId::new("limbo_rename_table", ""), |b| { #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/schema_5k.db").unwrap(); let conn = db.connect().unwrap(); b.iter_custom(|iters| { (0..iters) .map(|_| { conn.execute("CREATE TABLE x(a)").unwrap(); let elapsed = { let start = Instant::now(); conn.execute("ALTER TABLE x RENAME TO y").unwrap(); start.elapsed() }; conn.execute("DROP TABLE y").unwrap(); elapsed }) .sum::() }); }); if enable_rusqlite { group.bench_function(BenchmarkId::new("sqlite_rename_table", ""), |b| { let conn = rusqlite::Connection::open("../testing/system/schema_5k.db").unwrap(); b.iter_custom(|iters| { (0..iters) .map(|_| { conn.execute("CREATE TABLE x(a)", ()).unwrap(); let elapsed = { let start = Instant::now(); conn.execute("ALTER TABLE x RENAME TO y", ()).unwrap(); start.elapsed() }; conn.execute("DROP TABLE y", ()).unwrap(); elapsed }) .sum::() }); }); } group.finish(); let mut group = criterion.benchmark_group("`ALTER TABLE _ RENAME COLUMN _ TO _`"); group.bench_function(BenchmarkId::new("limbo_rename_column", ""), |b| { #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/schema_5k.db").unwrap(); let conn = db.connect().unwrap(); b.iter_custom(|iters| { (0..iters) .map(|_| { conn.execute("CREATE TABLE x(a)").unwrap(); let elapsed = { let start = Instant::now(); conn.execute("ALTER TABLE x RENAME COLUMN a TO b").unwrap(); start.elapsed() }; conn.execute("DROP TABLE x").unwrap(); elapsed }) .sum::() }); }); if enable_rusqlite { group.bench_function(BenchmarkId::new("sqlite_rename_column", ""), |b| { let conn = rusqlite::Connection::open("../testing/system/schema_5k.db").unwrap(); b.iter_custom(|iters| { (0..iters) .map(|_| { conn.execute("CREATE TABLE x(a)", ()).unwrap(); let elapsed = { let start = Instant::now(); conn.execute("ALTER TABLE x RENAME COLUMN a TO b", ()) .unwrap(); start.elapsed() }; conn.execute("DROP TABLE x", ()).unwrap(); elapsed }) .sum::() }); }); } group.finish(); let mut group = criterion.benchmark_group("`ALTER TABLE _ ADD COLUMN _`"); group.bench_function(BenchmarkId::new("limbo_add_column", ""), |b| { #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/schema_5k.db").unwrap(); let conn = db.connect().unwrap(); b.iter_custom(|iters| { (0..iters) .map(|_| { conn.execute("CREATE TABLE x(a)").unwrap(); let elapsed = { let start = Instant::now(); conn.execute("ALTER TABLE x ADD COLUMN b").unwrap(); start.elapsed() }; conn.execute("DROP TABLE x").unwrap(); elapsed }) .sum::() }); }); if enable_rusqlite { group.bench_function(BenchmarkId::new("sqlite_add_column", ""), |b| { let conn = rusqlite::Connection::open("../testing/system/schema_5k.db").unwrap(); b.iter_custom(|iters| { (0..iters) .map(|_| { conn.execute("CREATE TABLE x(a)", ()).unwrap(); let elapsed = { let start = Instant::now(); conn.execute("ALTER TABLE x ADD COLUMN b", ()).unwrap(); start.elapsed() }; conn.execute("DROP TABLE x", ()).unwrap(); elapsed }) .sum::() }); }); } group.finish(); let mut group = criterion.benchmark_group("`ALTER TABLE _ DROP COLUMN _`"); group.bench_function(BenchmarkId::new("limbo_drop_column", ""), |b| { #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/schema_5k.db").unwrap(); let conn = db.connect().unwrap(); b.iter_custom(|iters| { (0..iters) .map(|_| { conn.execute("CREATE TABLE x(a, b)").unwrap(); let elapsed = { let start = Instant::now(); conn.execute("ALTER TABLE x DROP COLUMN b").unwrap(); start.elapsed() }; conn.execute("DROP TABLE x").unwrap(); elapsed }) .sum::() }); }); if enable_rusqlite { group.bench_function(BenchmarkId::new("sqlite_drop_column", ""), |b| { let conn = rusqlite::Connection::open("../testing/system/schema_5k.db").unwrap(); b.iter_custom(|iters| { (0..iters) .map(|_| { conn.execute("CREATE TABLE x(a, b)", ()).unwrap(); let elapsed = { let start = Instant::now(); conn.execute("ALTER TABLE x DROP COLUMN b", ()).unwrap(); start.elapsed() }; conn.execute("DROP TABLE x", ()).unwrap(); elapsed }) .sum::() }); }); } group.finish(); } fn bench_prepare_query(criterion: &mut Criterion) { // https://github.com/tursodatabase/turso/issues/174 // The rusqlite benchmark crashes on Mac M1 when using the flamegraph features let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/testing.db").unwrap(); let limbo_conn = db.connect().unwrap(); let queries = [ "SELECT 1", "SELECT * FROM users LIMIT 1", "SELECT first_name, count(1) FROM users GROUP BY first_name HAVING count(1) > 1 ORDER BY count(1) LIMIT 1", "SELECT first_name, last_name, state, city, age + 10, LENGTH(email), UPPER(first_name), LOWER(last_name), SUBSTR(phone_number, 1, 3), zipcode || '-' || state, AVG(age) + 5, MAX(age) - MIN(age), ROUND(AVG(age), 1), SUM(age) / COUNT(*), COUNT(*), COUNT(email), SUM(age), AVG(age), MIN(age), MAX(age), SUM(CASE WHEN age >= 18 THEN 1 ELSE 0 END), SUM(CASE WHEN age < 18 THEN 1 ELSE 0 END), AVG(CASE WHEN age >= 18 THEN age ELSE NULL END), MAX(CASE WHEN age >= 18 THEN age ELSE NULL END) FROM users GROUP BY state, city", ]; let whitespace_re = Regex::new(r"\s+").unwrap(); for query in queries.iter() { // Normalize whitespace in the query string by replacing all sequences of whitespace with a single space. let query = whitespace_re.replace_all(query, " ").to_string(); let query = query.as_str(); let byte_index: usize = query.chars().take(50).map(|c| c.len_utf8()).sum(); let mut group = criterion.benchmark_group(format!("Prepare `{query}`")); group.bench_with_input( // Limit the size of the benchmark id so that Codspeed does not through errors BenchmarkId::new("limbo_parse_query", &query[..byte_index]), query, |b, query| { b.iter(|| { limbo_conn.prepare(query).unwrap(); }); }, ); if enable_rusqlite { let sqlite_conn = rusqlite_open(); group.bench_with_input( BenchmarkId::new("sqlite_parse_query", &query[..byte_index]), query, |b, query| { b.iter(|| { sqlite_conn.prepare(query).unwrap(); }); }, ); } group.finish(); } } fn bench_execute_select_rows(criterion: &mut Criterion) { // https://github.com/tursodatabase/turso/issues/174 // The rusqlite benchmark crashes on Mac M1 when using the flamegraph features let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/testing.db").unwrap(); let limbo_conn = db.connect().unwrap(); let mut group = criterion.benchmark_group("Execute `SELECT * FROM users LIMIT ?`"); for i in [1, 10, 50, 100] { group.bench_with_input( BenchmarkId::new("limbo_execute_select_rows", i), &i, |b, i| { // TODO: LIMIT doesn't support query parameters. let mut stmt = limbo_conn .prepare(format!("SELECT * FROM users LIMIT {}", *i)) .unwrap(); b.iter(|| { loop { match stmt.step().unwrap() { turso_core::StepResult::Row => { black_box(stmt.row()); } turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } stmt.reset().unwrap(); }); }, ); if enable_rusqlite { let sqlite_conn = rusqlite_open(); group.bench_with_input( BenchmarkId::new("sqlite_execute_select_rows", i), &i, |b, i| { // TODO: Use parameters once we fix the above. let mut stmt = sqlite_conn .prepare(&format!("SELECT * FROM users LIMIT {}", *i)) .unwrap(); b.iter(|| { let mut rows = stmt.raw_query(); while let Some(row) = rows.next().unwrap() { black_box(row); } }); }, ); } } group.finish(); } fn bench_execute_select_1(criterion: &mut Criterion) { // https://github.com/tursodatabase/turso/issues/174 // The rusqlite benchmark crashes on Mac M1 when using the flamegraph features let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/testing.db").unwrap(); let limbo_conn = db.connect().unwrap(); let mut group = criterion.benchmark_group("Execute `SELECT 1`"); group.bench_function("limbo_execute_select_1", |b| { let mut stmt = limbo_conn.prepare("SELECT 1").unwrap(); b.iter(|| { loop { match stmt.step().unwrap() { turso_core::StepResult::Row => { black_box(stmt.row()); } turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } stmt.reset().unwrap(); }); }); if enable_rusqlite { let sqlite_conn = rusqlite_open(); group.bench_function("sqlite_execute_select_1", |b| { let mut stmt = sqlite_conn.prepare("SELECT 1").unwrap(); b.iter(|| { let mut rows = stmt.raw_query(); while let Some(row) = rows.next().unwrap() { black_box(row); } }); }); } group.finish(); } fn bench_execute_select_count(criterion: &mut Criterion) { // https://github.com/tursodatabase/turso/issues/174 // The rusqlite benchmark crashes on Mac M1 when using the flamegraph features let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/testing.db").unwrap(); let limbo_conn = db.connect().unwrap(); let mut group = criterion.benchmark_group("Execute `SELECT count() FROM users`"); group.bench_function("limbo_execute_select_count", |b| { let mut stmt = limbo_conn.prepare("SELECT count() FROM users").unwrap(); b.iter(|| { loop { match stmt.step().unwrap() { turso_core::StepResult::Row => { black_box(stmt.row()); } turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } stmt.reset().unwrap(); }); }); if enable_rusqlite { let sqlite_conn = rusqlite_open(); group.bench_function("sqlite_execute_select_count", |b| { let mut stmt = sqlite_conn.prepare("SELECT count() FROM users").unwrap(); b.iter(|| { let mut rows = stmt.raw_query(); while let Some(row) = rows.next().unwrap() { black_box(row); } }); }); } group.finish(); } fn bench_insert_rows(criterion: &mut Criterion) { // The rusqlite benchmark crashes on Mac M1 when using the flamegraph features let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); let mut group = criterion.benchmark_group("Insert rows in batches"); // Test different batch sizes for batch_size in [1, 10, 100] { let temp_dir = tempfile::tempdir().unwrap(); let db_path = temp_dir.path().join("bench.db"); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io.clone(), db_path.to_str().unwrap()).unwrap(); let limbo_conn = db.connect().unwrap(); let mut stmt = limbo_conn .query("CREATE TABLE test (id INTEGER, value TEXT)") .unwrap() .unwrap(); loop { match stmt.step().unwrap() { turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Row => { unreachable!(); } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } group.bench_function(format!("limbo_insert_{batch_size}_rows"), |b| { let mut values = String::from("INSERT INTO test VALUES "); for i in 0..batch_size { if i > 0 { values.push(','); } values.push_str(&format!("({}, '{}')", i, format_args!("value_{i}"))); } let mut stmt = limbo_conn.prepare(&values).unwrap(); b.iter(|| { loop { match stmt.step().unwrap() { turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Row => { unreachable!(); } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } stmt.reset().unwrap(); }); }); if enable_rusqlite { let temp_dir = tempfile::tempdir().unwrap(); let db_path = temp_dir.path().join("bench.db"); let sqlite_conn = rusqlite::Connection::open(db_path).unwrap(); sqlite_conn .pragma_update(None, "synchronous", "FULL") .unwrap(); sqlite_conn .pragma_update(None, "journal_mode", "WAL") .unwrap(); sqlite_conn .pragma_update(None, "locking_mode", "EXCLUSIVE") .unwrap(); let journal_mode = sqlite_conn .pragma_query_value(None, "journal_mode", |row| row.get::<_, String>(0)) .unwrap(); assert_eq!(journal_mode.to_lowercase(), "wal"); let synchronous = sqlite_conn .pragma_query_value(None, "synchronous", |row| row.get::<_, usize>(0)) .unwrap(); const FULL: usize = 2; assert_eq!(synchronous, FULL); // Create test table sqlite_conn .execute("CREATE TABLE test (id INTEGER, value TEXT)", []) .unwrap(); sqlite_conn .pragma_update(None, "locking_mode", "EXCLUSIVE") .unwrap(); group.bench_function(format!("sqlite_insert_{batch_size}_rows"), |b| { let mut values = String::from("INSERT INTO test VALUES "); for i in 0..batch_size { if i > 0 { values.push(','); } values.push_str(&format!("({}, '{}')", i, format_args!("value_{i}"))); } let mut stmt = sqlite_conn.prepare(&values).unwrap(); b.iter(|| { let mut rows = stmt.raw_query(); while let Some(row) = rows.next().unwrap() { black_box(row); } }); }); } } group.finish(); } #[inline(never)] fn bench_limbo( mvcc: bool, num_connections: i64, num_batch_inserts: i64, num_inserts_per_batch: usize, ) { struct ConnectionState { conn: Arc, inserts: Vec, current_statement: Option, } #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let temp_dir = tempfile::tempdir().unwrap(); let path = temp_dir.path().join("bench.db"); let db = Database::open_file(io, path.to_str().unwrap()).unwrap(); let mut connecitons = Vec::new(); { let conn = db.connect().unwrap(); if mvcc { conn.execute("PRAGMA journal_mode = 'mvcc'").unwrap(); } conn.execute("CREATE TABLE test (x)").unwrap(); conn.close().unwrap(); } let inserts = generate_inserts_per_connection(num_connections, num_batch_inserts, num_inserts_per_batch); for i in 0..num_connections { let conn = db.connect().unwrap(); let inserts = inserts[i as usize].clone(); connecitons.push(ConnectionState { conn, inserts, current_statement: None, }); } loop { let mut all_finished = true; for conn in &mut connecitons { if !conn.inserts.is_empty() || conn.current_statement.is_some() { all_finished = false; break; } } for conn in connecitons.iter_mut() { if conn.current_statement.is_none() && !conn.inserts.is_empty() { let write = conn.inserts.pop().unwrap(); conn.current_statement = Some(conn.conn.prepare(&write).unwrap()); } if conn.current_statement.is_none() { continue; } let stmt = conn.current_statement.as_mut().unwrap(); match stmt.step().unwrap() { // These you be only possible cases in write concurrency. // No rows because insert doesn't return // No interrupt because insert doesn't interrupt // No busy because insert in mvcc should be multi concurrent write StepResult::Done => { conn.current_statement = None; } StepResult::IO => { // let's skip doing I/O here, we want to perform io only after all the statements are stepped } StepResult::Busy => { // We need to restart statement if mvcc { unreachable!(); } stmt.reset().unwrap(); } _ => { unreachable!() } } } db.io.step().unwrap(); if all_finished { break; } } } #[inline(never)] fn bench_limbo_mvcc( mvcc: bool, num_connections: i64, num_batch_inserts: i64, num_inserts_per_batch: usize, ) { struct ConnectionState { conn: Arc, inserts: Vec, current_statement: Option, current_insert: Option, } #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let temp_dir = tempfile::tempdir().unwrap(); let path = temp_dir.path().join("bench.db"); let db = Database::open_file(io, path.to_str().unwrap()).unwrap(); let mut connecitons = Vec::new(); let conn0 = db.connect().unwrap(); if mvcc { conn0.execute("PRAGMA journal_mode = 'mvcc'").unwrap(); } conn0.execute("CREATE TABLE test (x)").unwrap(); let inserts = generate_inserts_per_connection(num_connections, num_batch_inserts, num_inserts_per_batch); for i in 0..num_connections { let conn = db.connect().unwrap(); let inserts = inserts[i as usize].clone(); connecitons.push(ConnectionState { conn, inserts, current_statement: None, current_insert: None, }); } loop { let all_finished = connecitons .iter() .all(|conn| conn.inserts.is_empty() && conn.current_statement.is_none()); for conn in connecitons.iter_mut() { if conn.current_statement.is_none() && !conn.inserts.is_empty() { let write = conn.inserts.pop().unwrap(); conn.conn.execute("BEGIN CONCURRENT").unwrap(); conn.current_statement = Some(conn.conn.prepare(&write).unwrap()); conn.current_insert = Some(write); } if conn.current_statement.is_none() { continue; } let stmt = conn.current_statement.as_mut().unwrap(); let is_commit = stmt.get_sql() == "COMMIT"; match stmt.step() { // These you be only possible cases in write concurrency. // No rows because insert doesn't return // No interrupt because insert doesn't interrupt // No busy because insert in mvcc should be multi concurrent write Ok(StepResult::Done) => { if is_commit { // COMMIT finished, clear statement to start next transaction conn.current_statement = None; conn.current_insert = None; } else { // INSERT finished, now do commit conn.current_statement = Some(conn.conn.prepare("COMMIT").unwrap()); } } Ok(StepResult::IO) => { // let's skip doing I/O here, we want to perform io only after all the statements are stepped } Ok(StepResult::Busy) => { // We need to restart statement if mvcc { unreachable!(); } println!("resetting statement"); stmt.reset().unwrap(); } Err(err) => { if let LimboError::SchemaUpdated = err { conn.current_statement = Some( conn.conn .prepare(conn.current_insert.clone().as_ref().unwrap()) .unwrap(), ); continue; } panic!("unexpected error: {err:?}"); } _ => { unreachable!() } } } db.io.step().unwrap(); if all_finished { break; } } } fn generate_inserts_per_connection( num_connections: i64, num_batch_inserts: i64, num_inserts_per_batch: usize, ) -> Vec> { let mut inserts = vec![]; for i in 0..num_connections { let mut inserts_per_connection = vec![]; for j in 0..num_batch_inserts { inserts_per_connection.push(generate_batch_insert( num_batch_inserts * (i + j), num_inserts_per_batch, )); } inserts.push(inserts_per_connection); } inserts } fn generate_batch_insert(start: i64, num: usize) -> String { let mut inserts = String::from("INSERT INTO test (x) VALUES "); for i in 0..num { inserts.push_str(&format!("({})", start + i as i64)); if i < num - 1 { inserts.push(','); } } inserts } fn bench_concurrent_writes(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("Concurrent writes"); let num_connections = 4; let num_batch_inserts = 50; let num_inserts_per_batch = 50_usize; group.bench_function("limbo_wal_concurrent_writes", |b| { b.iter(|| { bench_limbo( false, num_connections, num_batch_inserts, num_inserts_per_batch, ); }); }); group.bench_function("limbo_mvcc_concurrent_writes", |b| { b.iter(|| { bench_limbo_mvcc( true, num_connections, num_batch_inserts, num_inserts_per_batch, ); }); }); group.bench_function("sqlite_concurrent_writes", |b| { let inserts = generate_inserts_per_connection( num_connections, num_batch_inserts, num_inserts_per_batch, ); b.iter(|| { let temp_dir = tempfile::tempdir().unwrap(); let path = temp_dir.path().join("bench.db"); { let conn = rusqlite::Connection::open(path.to_str().unwrap()).unwrap(); conn.pragma_update(None, "synchronous", "FULL").unwrap(); conn.pragma_update(None, "journal_mode", "WAL").unwrap(); conn.pragma_update(None, "locking_mode", "EXCLUSIVE") .unwrap(); conn.execute("CREATE TABLE test (x INTEGER)", []).unwrap(); } for i in 0..num_connections { let conn = rusqlite::Connection::open(path.to_str().unwrap()).unwrap(); for j in 0..num_batch_inserts { conn.execute(&inserts[i as usize][j as usize], []).unwrap(); } } }); }); } fn bench_insert_randomblob(criterion: &mut Criterion) { // The rusqlite benchmark crashes on Mac M1 when using the flamegraph features let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); let mut group = criterion.benchmark_group("Insert rows in batches"); // Test different batch sizes for batch_size in [1, 10, 100] { let temp_dir = tempfile::tempdir().unwrap(); let db_path = temp_dir.path().join("bench.db"); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io.clone(), db_path.to_str().unwrap()).unwrap(); let limbo_conn = db.connect().unwrap(); let mut stmt = limbo_conn.query("CREATE TABLE test(x)").unwrap().unwrap(); loop { match stmt.step().unwrap() { turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Row => { unreachable!(); } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } let random_blob = format!( "INSERT INTO test select randomblob(1024 * 100) from generate_series(1, {batch_size});" ); group.bench_function(format!("limbo_insert_{batch_size}_randomblob"), |b| { let mut stmt = limbo_conn.prepare(&random_blob).unwrap(); b.iter(|| { loop { match stmt.step().unwrap() { turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Row => { unreachable!(); } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } stmt.reset().unwrap(); }); }); if enable_rusqlite { let temp_dir = tempfile::tempdir().unwrap(); let sqlite_conn = setup_rusqlite(&temp_dir, "CREATE TABLE test(x)"); group.bench_function(format!("sqlite_insert_{batch_size}_randomblob"), |b| { let mut stmt = sqlite_conn.prepare(&random_blob).unwrap(); b.iter(|| { let mut rows = stmt.raw_query(); while let Some(row) = rows.next().unwrap() { black_box(row); } }); }); } } group.finish(); } #[cfg(not(feature = "codspeed"))] criterion_group! { name = benches; config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); targets = bench_open, bench_alter, bench_prepare_query, bench_execute_select_1, bench_execute_select_rows, bench_execute_select_count, bench_insert_rows, bench_concurrent_writes, bench_insert_randomblob } #[cfg(feature = "codspeed")] criterion_group! { name = benches; config = Criterion::default(); targets = bench_open, bench_alter, bench_prepare_query, bench_execute_select_1, bench_execute_select_rows, bench_execute_select_count, bench_insert_rows, bench_concurrent_writes, bench_insert_randomblob } criterion_main!(benches); ================================================ FILE: core/benches/fts_benchmark.rs ================================================ //! FTS Query Performance Benchmarks //! //! Measures full-text search query performance including: //! - Cold query (first query after index creation, no cached directory) //! - Warm query (repeated queries with cached directory) //! - Insert + query lifecycle (write, commit, query) //! //! Run with: cargo bench --bench fts_benchmark --features fts #[cfg(not(feature = "codspeed"))] use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; #[cfg(not(feature = "codspeed"))] use pprof::criterion::{Output, PProfProfiler}; #[cfg(feature = "codspeed")] use codspeed_criterion_compat::{criterion_group, criterion_main, BenchmarkId, Criterion}; use std::sync::Arc; use tempfile::TempDir; use turso_core::{Database, DatabaseOpts, OpenFlags, PlatformIO, StepResult}; #[cfg(not(target_family = "wasm"))] #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; /// Helper to execute a statement to completion, stepping through IO. fn run_to_completion( stmt: &mut turso_core::Statement, db: &Arc, ) -> turso_core::Result<()> { loop { match stmt.step()? { StepResult::IO => { db.io.step()?; } StepResult::Done => break, StepResult::Row => {} StepResult::Interrupt | StepResult::Busy => { panic!("Unexpected step result"); } } } Ok(()) } /// Helper to step a statement and count result rows. fn run_and_count_rows( stmt: &mut turso_core::Statement, db: &Arc, ) -> turso_core::Result { let mut count = 0; loop { match stmt.step()? { StepResult::IO => { db.io.step()?; } StepResult::Done => break, StepResult::Row => { count += 1; } StepResult::Interrupt | StepResult::Busy => { panic!("Unexpected step result"); } } } Ok(count) } /// Setup a database with an FTS-indexed table populated with `row_count` rows. fn setup_fts_db(temp_dir: &TempDir, row_count: usize) -> Arc { let db_path = temp_dir.path().join("fts_bench.db"); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let opts = DatabaseOpts::new().with_index_method(true); let db = Database::open_file_with_flags( io, db_path.to_str().unwrap(), OpenFlags::default(), opts, None, ) .unwrap(); let conn = db.connect().unwrap(); // Create table and FTS index conn.execute("CREATE TABLE docs (id INTEGER PRIMARY KEY, title TEXT, body TEXT)") .unwrap(); conn.execute("CREATE INDEX docs_fts ON docs USING fts (title, body)") .unwrap(); // Insert rows in batches of 500 let batch_size = 500; for batch_start in (0..row_count).step_by(batch_size) { let batch_end = (batch_start + batch_size).min(row_count); let mut sql = String::from("INSERT INTO docs (id, title, body) VALUES "); for i in batch_start..batch_end { if i > batch_start { sql.push(','); } // Vary content so term dictionaries have realistic distribution let word_a = match i % 7 { 0 => "database", 1 => "performance", 2 => "optimization", 3 => "benchmark", 4 => "storage", 5 => "indexing", _ => "computing", }; let word_b = match i % 5 { 0 => "systems", 1 => "analysis", 2 => "engineering", 3 => "architecture", _ => "design", }; sql.push_str(&format!( "({i}, '{word_a} document {i}', 'This is the body of document {i} about {word_a} and {word_b} with additional text for realistic content size')" )); } conn.execute(&sql).unwrap(); } db } /// Benchmark: Cold FTS query (no cached directory — measures full loading pipeline) /// /// This measures the worst-case: open_read must scan the BTree catalog, /// load hot files, create the Tantivy Index, build a Reader+Searcher, /// parse the query, and execute the search. Each iteration uses a fresh /// connection to avoid directory cache hits. fn bench_fts_cold_query(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("FTS Cold Query"); group.sample_size(20); // Cold queries are slow; reduce samples for row_count in [1000, 5000, 10000] { let temp_dir = tempfile::tempdir().unwrap(); let db = setup_fts_db(&temp_dir, row_count); group.bench_function( BenchmarkId::new("cold_query", format!("{row_count}_rows")), |b| { b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { // Fresh connection = no cached directory let conn = db.connect().unwrap(); let start = std::time::Instant::now(); let mut stmt = conn .query( "SELECT id, title FROM docs WHERE (title, body) MATCH 'database'", ) .unwrap() .unwrap(); let _rows = run_and_count_rows(&mut stmt, &db).unwrap(); total += start.elapsed(); } total }); }, ); } group.finish(); } /// Benchmark: Warm FTS query (cached directory — measures query-only path) /// /// After the first query loads and caches the directory, subsequent queries /// skip the catalog scan and PreloadingEssentials entirely. This measures /// the pure query execution path: Index::open (from cached directory), /// Reader+Searcher creation, query parsing, and search. fn bench_fts_warm_query(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("FTS Warm Query"); for row_count in [1000, 5000, 10000] { let temp_dir = tempfile::tempdir().unwrap(); let db = setup_fts_db(&temp_dir, row_count); let conn = db.connect().unwrap(); // Warm up: run one query to populate the directory cache let mut stmt = conn .query("SELECT id FROM docs WHERE (title, body) MATCH 'database'") .unwrap() .unwrap(); run_to_completion(&mut stmt, &db).unwrap(); group.bench_function( BenchmarkId::new("warm_query", format!("{row_count}_rows")), |b| { b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); let mut stmt = conn .query( "SELECT id, title FROM docs WHERE (title, body) MATCH 'database'", ) .unwrap() .unwrap(); let _rows = run_and_count_rows(&mut stmt, &db).unwrap(); total += start.elapsed(); } total }); }, ); } group.finish(); } /// Benchmark: FTS query with different search selectivity /// /// Measures how the number of matching documents affects query time. /// "database" matches ~1/7 of docs, "performance" matches ~1/7, /// "database performance" (AND) matches fewer. fn bench_fts_query_selectivity(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("FTS Query Selectivity"); let row_count = 10000; let temp_dir = tempfile::tempdir().unwrap(); let db = setup_fts_db(&temp_dir, row_count); let conn = db.connect().unwrap(); // Warm up let mut stmt = conn .query("SELECT id FROM docs WHERE (title, body) MATCH 'database'") .unwrap() .unwrap(); run_to_completion(&mut stmt, &db).unwrap(); let queries = [ ("single_common_term", "database"), ("single_uncommon_term", "optimization"), ("two_term_and", "database engineering"), ("phrase_query", "\"database document\""), ]; for (name, query_term) in queries { let sql = format!("SELECT id, title FROM docs WHERE (title, body) MATCH '{query_term}'"); group.bench_function(BenchmarkId::new("selectivity", name), |b| { b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); let mut stmt = conn.query(&sql).unwrap().unwrap(); let _rows = run_and_count_rows(&mut stmt, &db).unwrap(); total += start.elapsed(); } total }); }); } group.finish(); } /// Benchmark: Insert + query lifecycle /// /// Measures the cost of inserting new rows, committing, and then querying. /// This exercises the full write path (IndexWriter, segment creation, BTree flush) /// followed by directory cache invalidation and a cold re-query. fn bench_fts_insert_then_query(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("FTS Insert+Query Lifecycle"); group.sample_size(20); for row_count in [1000, 5000] { let temp_dir = tempfile::tempdir().unwrap(); let db = setup_fts_db(&temp_dir, row_count); let conn = db.connect().unwrap(); // Use a shared counter that persists across warmup + sampling invocations let counter = std::cell::Cell::new(row_count + 1_000_000); group.bench_function( BenchmarkId::new("insert_query", format!("{row_count}_rows")), |b| { b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); // Insert 10 new rows (use rowid=NULL to auto-assign) let c = counter.get(); let mut sql = String::from("INSERT INTO docs (id, title, body) VALUES "); for j in 0..10 { if j > 0 { sql.push(','); } let id = c + j; sql.push_str(&format!( "({id}, 'new document {id}', 'freshly inserted content about database systems')" )); } counter.set(c + 10); conn.execute(&sql).unwrap(); // Query (exercises cache invalidation + re-query) let mut stmt = conn .query( "SELECT id, title FROM docs WHERE (title, body) MATCH 'database'", ) .unwrap() .unwrap(); let _rows = run_and_count_rows(&mut stmt, &db).unwrap(); total += start.elapsed(); } total }); }, ); } group.finish(); } #[cfg(not(feature = "codspeed"))] criterion_group! { name = fts_benches; config = Criterion::default() .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))) .sample_size(50); targets = bench_fts_cold_query, bench_fts_warm_query, bench_fts_query_selectivity, bench_fts_insert_then_query } #[cfg(feature = "codspeed")] criterion_group! { name = fts_benches; config = Criterion::default().sample_size(50); targets = bench_fts_cold_query, bench_fts_warm_query, bench_fts_query_selectivity, bench_fts_insert_then_query } criterion_main!(fts_benches); ================================================ FILE: core/benches/graph_queries_benchmark.rs ================================================ use std::sync::Arc; #[cfg(not(feature = "codspeed"))] use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, SamplingMode}; #[cfg(not(feature = "codspeed"))] use pprof::criterion::{Output, PProfProfiler}; #[cfg(feature = "codspeed")] use codspeed_criterion_compat::{ black_box, criterion_group, criterion_main, BenchmarkId, Criterion, SamplingMode, }; use turso_core::{Database, PlatformIO}; const DB_PATH: &str = "../perf/graph-queries/graph-queries.db"; const DB_PATH_ANALYZED: &str = "../perf/graph-queries/graph-queries-analyzed.db"; macro_rules! gq_query { ($name:literal) => { ( $name, include_str!(concat!("../../perf/graph-queries/queries/", $name, ".sql")), ) }; } fn rusqlite_open(path: &str) -> rusqlite::Connection { let conn = rusqlite::Connection::open(path).unwrap(); conn.pragma_update(None, "locking_mode", "EXCLUSIVE") .unwrap(); conn } fn bench_graph_queries(criterion: &mut Criterion) { let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io.clone(), DB_PATH).unwrap(); let limbo_conn = db.connect().unwrap(); let db_analyzed = Database::open_file(io, DB_PATH_ANALYZED).unwrap(); let limbo_conn_analyzed = db_analyzed.connect().unwrap(); let queries = [ gq_query!("a_cooccurrence"), gq_query!("b_or_join"), gq_query!("c_edge_counts"), gq_query!("d_inlist_union"), gq_query!("e_activity_agg"), gq_query!("f1_streak_current"), gq_query!("f2_streak_longest"), gq_query!("3_aggregate_or_in"), ]; for (name, query) in queries.iter() { let mut group = criterion.benchmark_group(format!("GraphQuery `{name}`")); group.sampling_mode(SamplingMode::Flat); group.sample_size(10); group.bench_with_input(BenchmarkId::new("limbo", name), query, |b, query| { let mut stmt = limbo_conn.prepare(query).unwrap(); b.iter(|| { loop { match stmt.step().unwrap() { turso_core::StepResult::Row => { black_box(stmt.row()); } turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } stmt.reset().unwrap(); }); }); group.bench_with_input( BenchmarkId::new("limbo_analyzed", name), query, |b, query| { let mut stmt = limbo_conn_analyzed.prepare(query).unwrap(); b.iter(|| { loop { match stmt.step().unwrap() { turso_core::StepResult::Row => { black_box(stmt.row()); } turso_core::StepResult::IO => { db_analyzed.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } stmt.reset().unwrap(); }); }, ); if enable_rusqlite { let sqlite_conn = rusqlite_open(DB_PATH); group.bench_with_input(BenchmarkId::new("sqlite", name), query, |b, query| { let mut stmt = sqlite_conn.prepare(query).unwrap(); b.iter(|| { let mut rows = stmt.raw_query(); while let Some(row) = rows.next().unwrap() { black_box(row); } }); }); let sqlite_conn_analyzed = rusqlite_open(DB_PATH_ANALYZED); group.bench_with_input( BenchmarkId::new("sqlite_analyzed", name), query, |b, query| { let mut stmt = sqlite_conn_analyzed.prepare(query).unwrap(); b.iter(|| { let mut rows = stmt.raw_query(); while let Some(row) = rows.next().unwrap() { black_box(row); } }); }, ); } group.finish(); } } #[cfg(not(feature = "codspeed"))] criterion_group! { name = benches; config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); targets = bench_graph_queries } #[cfg(feature = "codspeed")] criterion_group! { name = benches; config = Criterion::default(); targets = bench_graph_queries } criterion_main!(benches); ================================================ FILE: core/benches/hash_spill_benchmark.rs ================================================ #[cfg(not(feature = "codspeed"))] use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; #[cfg(not(feature = "codspeed"))] use pprof::criterion::{Output, PProfProfiler}; #[cfg(feature = "codspeed")] use codspeed_criterion_compat::{ black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput, }; use std::sync::Arc; use turso_core::types::Value; use turso_core::vdbe::hash_table::{HashTable, HashTableConfig}; use turso_core::vdbe::CollationSeq; use turso_core::{IOResult, MemoryIO, Numeric}; #[cfg(not(target_family = "wasm"))] #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; /// Create a hash table with the given memory budget fn create_hash_table(mem_budget: usize) -> HashTable { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 64, mem_budget, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: turso_core::TempStore::Default, track_matched: false, partition_count: None, }; HashTable::new(config, io) } /// Insert entries with integer keys and no payload fn insert_integer_entries(ht: &mut HashTable, count: usize) { for i in 0..count { let key = vec![Value::from_i64(i as i64)]; let _ = ht.insert(key, i as i64, vec![], None); } } /// Insert entries with integer keys and text payload fn insert_entries_with_text_payload(ht: &mut HashTable, count: usize, text_size: usize) { let payload_text: String = "x".repeat(text_size); for i in 0..count { let key = vec![Value::from_i64(i as i64)]; let payload = vec![Value::Text(payload_text.clone().into())]; let _ = ht.insert(key, i as i64, payload, None); } } /// Insert entries with text keys (for NOCASE hash testing) fn insert_text_key_entries(ht: &mut HashTable, count: usize) { for i in 0..count { let key = vec![Value::Text(format!("key_{i}").into())]; let _ = ht.insert(key, i as i64, vec![], None); } } /// Benchmark: Build phase with tight memory budget (forces frequent spilling) fn bench_build_tight_budget(c: &mut Criterion) { let mut group = c.benchmark_group("HashTable Build (Tight Budget)"); for count in [1000, 5000, 10000] { group.throughput(Throughput::Elements(count as u64)); // 32KB budget - will spill frequently group.bench_with_input( BenchmarkId::new("integer_keys", count), &count, |b, &count| { b.iter(|| { let mut ht = create_hash_table(32 * 1024); insert_integer_entries(&mut ht, count); let _ = ht.finalize_build(None); black_box(ht.has_spilled()) }); }, ); // With 100-byte text payload per entry group.bench_with_input( BenchmarkId::new("with_100b_payload", count), &count, |b, &count| { b.iter(|| { let mut ht = create_hash_table(32 * 1024); insert_entries_with_text_payload(&mut ht, count, 100); let _ = ht.finalize_build(None); black_box(ht.has_spilled()) }); }, ); } group.finish(); } /// Benchmark: Build phase with relaxed memory budget (occasional spilling) fn bench_build_relaxed_budget(c: &mut Criterion) { let mut group = c.benchmark_group("HashTable Build (Relaxed Budget)"); for count in [1000, 5000, 10000] { group.throughput(Throughput::Elements(count as u64)); // 256KB budget - will spill less frequently group.bench_with_input( BenchmarkId::new("integer_keys", count), &count, |b, &count| { b.iter(|| { let mut ht = create_hash_table(256 * 1024); insert_integer_entries(&mut ht, count); let _ = ht.finalize_build(None); black_box(ht.has_spilled()) }); }, ); // With 100-byte text payload group.bench_with_input( BenchmarkId::new("with_100b_payload", count), &count, |b, &count| { b.iter(|| { let mut ht = create_hash_table(256 * 1024); insert_entries_with_text_payload(&mut ht, count, 100); let _ = ht.finalize_build(None); black_box(ht.has_spilled()) }); }, ); } group.finish(); } /// Benchmark: Build + Probe with spilling fn bench_build_and_probe(c: &mut Criterion) { let mut group = c.benchmark_group("HashTable Build+Probe"); for count in [1000, 5000] { group.throughput(Throughput::Elements(count as u64 * 2)); // build + probe group.bench_with_input( BenchmarkId::new("tight_budget", count), &count, |b, &count| { b.iter(|| { // Build phase let mut ht = create_hash_table(32 * 1024); insert_integer_entries(&mut ht, count); let _ = ht.finalize_build(None); // Probe phase - look up every key let mut found = 0; for i in 0..count { let key = vec![Value::Numeric(Numeric::Integer(i as i64))]; if ht.has_spilled() { let partition_idx = ht.partition_for_keys(&key); if !ht.is_partition_loaded(partition_idx) { while let Ok(IOResult::IO(_)) = ht.load_spilled_partition(partition_idx, None) { } } if ht.probe_partition(partition_idx, &key, None).is_some() { found += 1; } } else if ht.probe(key, None).is_some() { found += 1; } } black_box(found) }); }, ); group.bench_with_input( BenchmarkId::new("relaxed_budget", count), &count, |b, &count| { b.iter(|| { // Build phase let mut ht = create_hash_table(256 * 1024); insert_integer_entries(&mut ht, count); let _ = ht.finalize_build(None); // Probe phase let mut found = 0; for i in 0..count { let key = vec![Value::from_i64(i as i64)]; if ht.has_spilled() { let partition_idx = ht.partition_for_keys(&key); if !ht.is_partition_loaded(partition_idx) { while let Ok(IOResult::IO(_)) = ht.load_spilled_partition(partition_idx, None) { } } if ht.probe_partition(partition_idx, &key, None).is_some() { found += 1; } } else if ht.probe(key, None).is_some() { found += 1; } } black_box(found) }); }, ); } group.finish(); } /// Benchmark: Text key hashing (tests NOCASE optimization) fn bench_text_key_hashing(c: &mut Criterion) { let mut group = c.benchmark_group("HashTable Text Keys"); for count in [1000, 5000] { group.throughput(Throughput::Elements(count as u64)); // Binary collation group.bench_with_input( BenchmarkId::new("binary_collation", count), &count, |b, &count| { b.iter(|| { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 64, mem_budget: 64 * 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: turso_core::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); insert_text_key_entries(&mut ht, count); let _ = ht.finalize_build(None); black_box(ht.has_spilled()) }); }, ); // NOCASE collation (tests allocation-free hash optimization) group.bench_with_input( BenchmarkId::new("nocase_collation", count), &count, |b, &count| { b.iter(|| { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 64, mem_budget: 64 * 1024, num_keys: 1, collations: vec![CollationSeq::NoCase], temp_store: turso_core::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); insert_text_key_entries(&mut ht, count); let _ = ht.finalize_build(None); black_box(ht.has_spilled()) }); }, ); } group.finish(); } /// Benchmark: Large payload serialization fn bench_large_payload_spill(c: &mut Criterion) { let mut group = c.benchmark_group("HashTable Large Payload Spill"); for payload_size in [100, 500, 1000] { let count = 1000; group.throughput(Throughput::Bytes((count * payload_size) as u64)); group.bench_with_input( BenchmarkId::new("payload_bytes", payload_size), &payload_size, |b, &payload_size| { b.iter(|| { let mut ht = create_hash_table(32 * 1024); // Tight budget to force spilling insert_entries_with_text_payload(&mut ht, count, payload_size); let _ = ht.finalize_build(None); black_box(ht.has_spilled()) }); }, ); } group.finish(); } #[cfg(not(feature = "codspeed"))] criterion_group! { name = benches; config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); targets = bench_build_tight_budget, bench_build_relaxed_budget, bench_build_and_probe, bench_text_key_hashing, bench_large_payload_spill } #[cfg(feature = "codspeed")] criterion_group! { name = benches; config = Criterion::default(); targets = bench_build_tight_budget, bench_build_relaxed_budget, bench_build_and_probe, bench_text_key_hashing, bench_large_payload_spill } criterion_main!(benches); ================================================ FILE: core/benches/json_benchmark.rs ================================================ #[cfg(not(feature = "codspeed"))] use criterion::{black_box, criterion_group, criterion_main, Criterion}; #[cfg(not(feature = "codspeed"))] use pprof::{ criterion::{Output, PProfProfiler}, flamegraph::Options, }; #[cfg(feature = "codspeed")] use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Criterion}; use std::sync::Arc; use turso_core::{Database, PlatformIO}; // Title: JSONB Function Benchmarking fn rusqlite_open() -> rusqlite::Connection { let sqlite_conn = rusqlite::Connection::open("../testing/system/testing.db").unwrap(); sqlite_conn .pragma_update(None, "locking_mode", "EXCLUSIVE") .unwrap(); sqlite_conn } fn bench(criterion: &mut Criterion) { // Flag to disable rusqlite benchmarks if needed let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/testing.db").unwrap(); let limbo_conn = db.connect().unwrap(); // Benchmark JSONB with different payload sizes let json_sizes = [ ("Small", r#"{"id": 1, "name": "Test"}"#), ( "Medium", r#"{"id": 1, "name": "Test", "attributes": {"color": "blue", "size": "medium", "tags": ["tag1", "tag2", "tag3"]}}"#, ), ( "Large", r#"[{"metadata":{"title":"Standard JSON Test File","description":"A complex JSON file for testing parsers and serializers (Standard JSON only)","version":"1.0.0","generated":"2025-03-12T12:00:00Z","author":"Claude AI"},"primitives":{"null_value":null,"boolean_values":{"true_value":true,"false_value":false},"number_values":{"integer":42,"negative":-273,"zero":0,"large_integer":9007199254740991,"small_integer":-9007199254740991,"decimal":3.14159265358979,"negative_decimal":-2.71828,"exponent_positive":6.022e+23,"exponent_negative":1.602e-19},"string_values":{"empty":"","simple":"Hello, world!","unicode":"你好,世界!😀🌍🚀","quotes":"She said \"Hello!\" to me.","backslash":"C:\\Program Files\\App\\","controls":"Line1\nLine2\tTabbed\rCarriage\bBackspace\fForm-feed","unicode_escapes":"Copyright: ©, Emoji: 😀","all_escapes":"\\b\\f\\n\\r\\t\\\"\\\\"}},"arrays":{"empty_array":[],"homogeneous":[1,2,3,4,5,6,7,8,9,10],"heterogeneous":[null,true,42,"string",{"key":"value"},[1,2,3]],"nested":[[1,2,3],[4,5,6],[7,8,9]],"deep":[[[[[[[[[["Very deep"]]]]]]]]]],"large":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99]},"objects":{"empty_object":{},"simple_object":{"key1":"value1","key2":"value2"},"nested_object":{"level1":{"level2":{"level3":{"level4":{"level5":"Deep nesting"}}}}},"complex_keys":{"simple":"value","with spaces":"value","with-dash":"value","with_underscore":"value","with.dot":"value","with:colon":"value","with@symbol":"value","withUnicode":"value","withEmoji":"value","withQuotes":"value","withBackslashes":"value"}},"edge_cases":{"zero_byte_string":"","one_byte_string":"x","long_string":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","almost_too_deep":{"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":{"i":{"j":{"k":{"l":{"m":{"n":{"o":{"p":{"q":{"r":{"s":{"t":{"u":{"v":{"w":{"x":{"y":{"z":"Deep nesting test"}}}}}}}}}}}}}}}}}}}}}}}}}}},"many_properties":{"prop01":1,"prop02":2,"prop03":3,"prop04":4,"prop05":5,"prop06":6,"prop07":7,"prop08":8,"prop09":9,"prop10":10,"prop11":11,"prop12":12,"prop13":13,"prop14":14,"prop15":15,"prop16":16,"prop17":17,"prop18":18,"prop19":19,"prop20":20,"prop21":21,"prop22":22,"prop23":23,"prop24":24,"prop25":25,"prop26":26,"prop27":27,"prop28":28,"prop29":29,"prop30":30,"prop31":31,"prop32":32,"prop33":33,"prop34":34,"prop35":35,"prop36":36,"prop37":37,"prop38":38,"prop39":39,"prop40":40,"prop41":41,"prop42":42,"prop43":43,"prop44":44,"prop45":45,"prop46":46,"prop47":47,"prop48":48,"prop49":49,"prop50":50}},{"standard_features":{"numeric_literals":{"decimal_integer":12345,"negative_integer":-12345,"decimal_fraction":123.45,"negative_fraction":-123.45,"exponential_positive":123400,"exponential_negative":0.00001234},"string_escapes":{"quotation_mark":"Quote: \"Hello\"","reverse_solidus":"Backslash: \\","solidus":"Slash: / (optional escape)","backspace":"Control: \b","formfeed":"Control: \f","newline":"Control: \n","carriage_return":"Control: \r","tab":"Control: \t","unicode":"Unicode: © € ☃"}},"generated_data":{"people":[{"id":1,"name":"John Smith","email":"john.smith@example.com","age":42,"address":{"street":"123 Main St","city":"Anytown","state":"CA","zip":"12345"},"phone_numbers":[{"type":"home","number":"555-1234"},{"type":"work","number":"555-5678"}],"tags":["employee","manager","developer"],"active":true},{"id":2,"name":"Jane Doe","email":"jane.doe@example.com","age":36,"address":{"street":"456 Elm St","city":"Othertown","state":"NY","zip":"67890"},"phone_numbers":[{"type":"mobile","number":"555-9012"}],"tags":["employee","designer"],"active":true},{"id":3,"name":"Bob Johnson","email":"bob.johnson@example.com","age":51,"address":{"street":"789 Oak St","city":"Somewhere","state":"TX","zip":"45678"},"phone_numbers":[{"type":"home","number":"555-3456"},{"type":"work","number":"555-7890"},{"type":"mobile","number":"555-1234"}],"tags":["employee","manager","sales"],"active":false}],"products":[{"id":"P001","name":"Smartphone","category":"Electronics","price":799.99,"features":["5G","Dual Camera","Fast Charging"],"specifications":{"dimensions":{"width":71.5,"height":146.7,"depth":7.4},"weight":174,"display":{"type":"OLED","size":6.1,"resolution":"1170x2532"},"processor":"A14 Bionic","memory":128},"in_stock":true,"release_date":"2023-09-15"},{"id":"P002","name":"Laptop","category":"Electronics","price":1299.99,"features":["16GB RAM","512GB SSD","Retina Display"],"specifications":{"dimensions":{"width":304.1,"height":212.4,"depth":15.6},"weight":1400,"display":{"type":"IPS","size":13.3,"resolution":"2560x1600"},"processor":"Intel Core i7","memory":512},"in_stock":true,"release_date":"2023-06-10"},{"id":"P003","name":"Wireless Headphones","category":"Audio","price":249.99,"features":["Noise Cancellation","20h Battery","Bluetooth 5.0"],"specifications":{"dimensions":{"width":168,"height":162,"depth":83},"weight":254,"driver":{"type":"Dynamic","size":40},"battery":{"capacity":500,"life":20}},"in_stock":false,"release_date":"2023-03-22"}],"orders":[{"id":"ORD-2023-001","customer_id":1,"date":"2023-01-15T10:30:00Z","items":[{"product_id":"P001","quantity":1,"price":799.99},{"product_id":"P003","quantity":2,"price":249.99}],"total":1299.97,"status":"delivered","shipping":{"address":{"street":"123 Main St","city":"Anytown","state":"CA","zip":"12345"},"method":"express","cost":15.99,"tracking_number":"SHP-123456789"},"payment":{"method":"credit_card","transaction_id":"TRX-987654321","status":"completed"}},{"id":"ORD-2023-002","customer_id":2,"date":"2023-02-20T14:45:00Z","items":[{"product_id":"P002","quantity":1,"price":1299.99}],"total":1299.99,"status":"shipped","shipping":{"address":{"street":"456 Elm St","city":"Othertown","state":"NY","zip":"67890"},"method":"standard","cost":9.99,"tracking_number":"SHP-234567890"},"payment":{"method":"paypal","transaction_id":"TRX-876543210","status":"completed"}},{"id":"ORD-2023-003","customer_id":3,"date":"2023-03-05T09:15:00Z","items":[{"product_id":"P001","quantity":1,"price":799.99},{"product_id":"P002","quantity":1,"price":1299.99},{"product_id":"P003","quantity":1,"price":249.99}],"total":2349.97,"status":"processing","shipping":{"address":{"street":"789 Oak St","city":"Somewhere","state":"TX","zip":"45678"},"method":"express","cost":25.99,"tracking_number":null},"payment":{"method":"bank_transfer","transaction_id":"TRX-765432109","status":"pending"}}]},"repeated_property_names":{"data":{"data":{"data":{"data":"Nested properties with the same name"}}}},"large_strings":{"base64_data":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAJ5SURBVHjalFJbSBRRGP7OzO7sbmrqbmo6lom5CYVEkiBIUKEPvRQFRlEEPvTSa0+9By+9BhFkIAhCQSJhWlEpaKVpZaSVpplpbre1vM26u7ObzpyZ0892MJMM6oOf833nO/f/ZwghWE86mOIq7YSQs3r0kqDgr8I2zlKb3k/G57fQz8Z9fOh6HDUsL9AcC5m2Zt4ZBBnLEbBCMfDNVv8s0T8f5sJaMCuW/51sDBQKYOcSUA68JgLhODzT8ToQh1ZeyhKtVMlgXl3NWZbDqcl3aOrtRFPvC/i5XJYk0UTjdiVduTuOleRCUomTiMdho+MxNvX1YlPfSwRMZthsdtllBUAIY27BkRWAbCx+jn+IQJB9NKwMQ0ehFYEVUIMQAh3DQKfRQqPTQR6FAPF4ImuYUQh7eGEuY+MJASMHg8kEvVYLPcsukBACVgiK9iS8ZtMBAQFDMeBp8NJQr9WAp03JOkgHBBZvitg8lkQRbr8fJJHMe5AgYj7iJYcPXPfNexf5rDFK0eFwQM+wYCgWLM2s1IWgYDqPrcVpXJZzUSY+Bp9t82wy6YG5sAJVlRtw0eWCw2RKt5oZiCQSYCkWT5bMOCK/AQgF1eofSDgOa4tKcKCuTnawGTFxLMrZGBpW38Md7SYI6Rts1lrZPRZ0g9d+w5LiiMx2lRVYjO15lTjbfw9sNIw9xdXYXVGBzTq99AVpcPncAz/f0cHnsojQaU3JF4jxhKaFQNUl4PJN4OJ5ICQAeg4jnvmMWWcnCErB5rHyD7Rmf3d1KbH+cOQx2XTkLJ5vPYL+8lrUL30FBjwYtDkw4nBmxNmKPwMAKrUbALKE+vwAAAAASUVORK5CYII=","lorem_ipsum":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"},"binary_data_sizes":{"small_payload":{"description":"Small payload (0-11 bytes in header)","data":[1,2,3,4,5]},"medium_payload":{"description":"Medium payload (1-byte size in header)","data":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50]},"large_payload":{"description":"Simulation of large payload (2-byte size in header)","data_description":"Would normally contain 256-65535 bytes"},"extra_large_payload":{"description":"Simulation of extra large payload (4-byte size in header)","data_description":"Would normally contain >65535 bytes"}},"stress_test":{"recursive_structure":{"name":"Level 1","children":[{"name":"Level 1.1","children":[{"name":"Level 1.1.1","children":[]},{"name":"Level 1.1.2","children":[{"name":"Level 1.1.2.1","children":[]}]}]},{"name":"Level 1.2","children":[{"name":"Level 1.2.1","children":[]}]}]},"long_array_nested_objects":[{"id":1,"data":{"value":"test1"}},{"id":2,"data":{"value":"test2"}},{"id":3,"data":{"value":"test3"}},{"id":4,"data":{"value":"test4"}},{"id":5,"data":{"value":"test5"}},{"id":6,"data":{"value":"test6"}},{"id":7,"data":{"value":"test7"}},{"id":8,"data":{"value":"test8"}},{"id":9,"data":{"value":"test9"}},{"id":10,"data":{"value":"test10"}},{"id":11,"data":{"value":"test11"}},{"id":12,"data":{"value":"test12"}},{"id":13,"data":{"value":"test13"}},{"id":14,"data":{"value":"test14"}},{"id":15,"data":{"value":"test15"}},{"id":16,"data":{"value":"test16"}},{"id":17,"data":{"value":"test17"}},{"id":18,"data":{"value":"test18"}},{"id":19,"data":{"value":"test19"}},{"id":20,"data":{"value":"test20"}}]}}]"#, ), // Generate a larger JSON object with 20 nested items ( "Real world json #1", r#"{ "user": { "id": "usr_7f8d3a2e", "name": "Jane Smith", "email": "jane.smith@example.com", "verified": true, "created_at": "2023-05-12T15:42:31Z", "preferences": { "theme": "dark", "notifications": { "email": true, "push": false, "sms": true }, "language": "en-US" }, "subscription": { "plan": "premium", "status": "active", "next_billing_date": "2024-05-12" }, "address": { "street": "123 Main St", "city": "Boston", "state": "MA", "zip": "02108", "country": "USA" } }, "meta": { "request_id": "req_9d7e6c5b4a3f2e1d", "timestamp": 1683905123 } }"#, ), ( "Real world json 2", r#"{ "products": [ { "id": "p-1001", "name": "Wireless Headphones", "price": 79.99, "currency": "USD", "in_stock": true, "quantity": 45, "categories": ["electronics", "audio", "wireless"], "ratings": { "average": 4.7, "count": 238 }, "specs": { "brand": "SoundMax", "color": "black", "connectivity": "Bluetooth 5.0", "battery_life": "20 hours" }, "images": [ "https://example.com/products/headphones-1.jpg", "https://example.com/products/headphones-2.jpg" ] }, { "id": "p-1002", "name": "Smart Watch", "price": 149.99, "currency": "USD", "in_stock": true, "quantity": 28, "categories": ["electronics", "wearables", "fitness"], "ratings": { "average": 4.3, "count": 182 }, "specs": { "brand": "TechFit", "color": "silver", "display": "AMOLED", "waterproof": true }, "images": [ "https://example.com/products/smartwatch-1.jpg", "https://example.com/products/smartwatch-2.jpg" ] } ], "pagination": { "total": 237, "page": 1, "per_page": 2, "next_page": 2 } } "#, ), ( "Real world json 3", r#"{ "app_name": "DataProcessor", "version": "2.1.3", "environment": "production", "debug": false, "log_level": "info", "database": { "main": { "host": "db-primary.internal", "port": 5432, "name": "app_production", "user": "app_user", "max_connections": 50, "timeout_ms": 5000 }, "replica": { "host": "db-replica.internal", "port": 5432, "name": "app_production_replica", "user": "app_readonly", "max_connections": 25, "timeout_ms": 3000 } }, "cache": { "enabled": true, "ttl_seconds": 3600, "max_size_mb": 512 }, "api": { "host": "0.0.0.0", "port": 8080, "rate_limit": { "requests_per_minute": 120, "burst": 30 }, "timeouts": { "read_ms": 5000, "write_ms": 10000, "idle_ms": 60000 } }, "feature_flags": { "new_dashboard": true, "beta_analytics": false, "improved_search": true } } "#, ), ( "Real world json 4", r#"{ "app_name": "DataProcessor", "version": "2.1.3", "environment": "production", "debug": false, "log_level": "info", "database": { "main": { "host": "db-primary.internal", "port": 5432, "name": "app_production", "user": "app_user", "max_connections": 50, "timeout_ms": 5000 }, "replica": { "host": "db-replica.internal", "port": 5432, "name": "app_production_replica", "user": "app_readonly", "max_connections": 25, "timeout_ms": 3000 } }, "cache": { "enabled": true, "ttl_seconds": 3600, "max_size_mb": 512 }, "api": { "host": "0.0.0.0", "port": 8080, "rate_limit": { "requests_per_minute": 120, "burst": 30 }, "timeouts": { "read_ms": 5000, "write_ms": 10000, "idle_ms": 60000 } }, "feature_flags": { "new_dashboard": true, "beta_analytics": false, "improved_search": true } }"#, ), ( "Real world json 5", r#" { "app_name": "DataProcessor", "version": "2.1.3", "environment": "production", "debug": false, "log_level": "info", "database": { "main": { "host": "db-primary.internal", "port": 5432, "name": "app_production", "user": "app_user", "max_connections": 50, "timeout_ms": 5000 }, "replica": { "host": "db-replica.internal", "port": 5432, "name": "app_production_replica", "user": "app_readonly", "max_connections": 25, "timeout_ms": 3000 } }, "cache": { "enabled": true, "ttl_seconds": 3600, "max_size_mb": 512 }, "api": { "host": "0.0.0.0", "port": 8080, "rate_limit": { "requests_per_minute": 120, "burst": 30 }, "timeouts": { "read_ms": 5000, "write_ms": 10000, "idle_ms": 60000 } }, "feature_flags": { "new_dashboard": true, "beta_analytics": false, "improved_search": true } }"#, ), ( "Real world json 6", r#" { "event_id": "evt_0ab1cde23f4g5h6i", "event_type": "page_view", "timestamp": "2024-03-12T08:14:27.345Z", "user": { "id": "u_789012", "anonymous_id": "anon_6c7d8e9f0a", "device_id": "dev_3e4f5g6h7i", "session_id": "sess_1b2c3d4e5f" }, "context": { "page": { "url": "https://example.com/products/smart-home", "title": "Smart Home Products | Example Store", "referrer": "https://google.com", "path": "/products/smart-home" }, "device": { "type": "desktop", "manufacturer": "Apple", "model": "MacBook Pro", "screen": { "width": 1440, "height": 900 } }, "browser": { "name": "Chrome", "version": "99.0.4844.51" }, "os": { "name": "macOS", "version": "12.3" }, "location": { "country": "US", "region": "CA", "city": "San Francisco", "timezone": "America/Los_Angeles" } }, "properties": { "duration_ms": 5327, "is_logged_in": true, "tags": ["homepage", "featured", "promo"], "utm": { "source": "newsletter", "medium": "email", "campaign": "spring_sale_2024" } } } "#, ), ( "Deeply nested", r#"{ "config": { "level1": { "level2": { "level3": { "level4": { "level5": { "level6": { "level7": { "value": "deeply nested value", "enabled": true, "numbers": [1, 2, 3, 4, 5], "settings": { "mode": "advanced", "retries": 3 } } } } } } } } } }"#, ), ( "Array heavy", r#"{ "feed": { "user_id": "u_12345", "posts": [ { "id": "post_001", "author": "user_789", "content": "Just launched our new product! Check it out at example.com/new", "timestamp": "2024-03-13T14:27:32Z", "likes": 24, "comments": [ { "id": "comment_001", "author": "user_456", "content": "Looks amazing! Cant wait to try it.", "timestamp": "2024-03-13T14:35:12Z", "likes": 3 }, { "id": "comment_002", "author": "user_789", "content": "Thanks! Let me know what you think after youve tried it.", "timestamp": "2024-03-13T14:42:45Z", "likes": 1 } ], "tags": ["product", "launch", "technology"] }, { "id": "post_002", "author": "user_123", "content": "Beautiful day for hiking! #nature #outdoors", "timestamp": "2024-03-13T11:15:22Z", "likes": 57, "comments": [ { "id": "comment_003", "author": "user_345", "content": "Where is this? So beautiful!", "timestamp": "2024-03-13T11:22:05Z", "likes": 2 }, { "id": "comment_004", "author": "user_123", "content": "Mount Rainier National Park!", "timestamp": "2024-03-13T11:30:16Z", "likes": 3 } ], "tags": ["nature", "outdoors", "hiking"], "location": { "name": "Mount Rainier National Park", "latitude": 46.8800, "longitude": -121.7269 } } ], "has_more": true, "next_cursor": "cursor_xyz123" } }"#, ), ]; for (size_name, json_payload) in json_sizes.iter() { let query = format!("SELECT jsonb('{}')", json_payload.replace("'", "\\'")); let mut group = criterion.benchmark_group(format!("JSONB Size - {size_name}")); group.bench_function("Limbo", |b| { let mut stmt = limbo_conn.prepare(&query).unwrap(); b.iter(|| { loop { match stmt.step().unwrap() { turso_core::StepResult::Row => {} turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } stmt.reset().unwrap(); }); }); if enable_rusqlite { let sqlite_conn = rusqlite_open(); group.bench_function("Sqlite3", |b| { let mut stmt = sqlite_conn.prepare(&query).unwrap(); b.iter(|| { let mut rows = stmt.raw_query(); while let Some(row) = rows.next().unwrap() { black_box(row); } }); }); } group.finish(); } } fn bench_sequential_jsonb(criterion: &mut Criterion) { // Flag to disable rusqlite benchmarks if needed let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/testing.db").unwrap(); let limbo_conn = db.connect().unwrap(); // Select a subset of JSON payloads to use in the sequential test let json_payloads = [ ("Small", r#"{"id": 1, "name": "Test"}"#), ( "Medium", r#"{"id": 1, "name": "Test", "attributes": {"color": "blue", "size": "medium", "tags": ["tag1", "tag2", "tag3"]}}"#, ), ( "Real world json #1", r#"{ "user": { "id": "usr_7f8d3a2e", "name": "Jane Smith", "email": "jane.smith@example.com", "verified": true, "created_at": "2023-05-12T15:42:31Z", "preferences": { "theme": "dark", "notifications": { "email": true, "push": false, "sms": true }, "language": "en-US" } } }"#, ), ( "Real world json #2", r#"{ "feed": { "user_id": "u_12345", "posts": [ { "id": "post_001", "author": "user_789", "content": "Just launched our new product! Check it out at example.com/new", "timestamp": "2024-03-13T14:27:32Z", "likes": 24, "comments": [ { "id": "comment_001", "author": "user_456", "content": "Looks amazing! Cant wait to try it.", "timestamp": "2024-03-13T14:35:12Z", "likes": 3 }, { "id": "comment_002", "author": "user_789", "content": "Thanks! Let me know what you think after youve tried it.", "timestamp": "2024-03-13T14:42:45Z", "likes": 1 } ], "tags": ["product", "launch", "technology"] }, { "id": "post_002", "author": "user_123", "content": "Beautiful day for hiking! #nature #outdoors", "timestamp": "2024-03-13T11:15:22Z", "likes": 57, "comments": [ { "id": "comment_003", "author": "user_345", "content": "Where is this? So beautiful!", "timestamp": "2024-03-13T11:22:05Z", "likes": 2 }, { "id": "comment_004", "author": "user_123", "content": "Mount Rainier National Park!", "timestamp": "2024-03-13T11:30:16Z", "likes": 3 } ], "tags": ["nature", "outdoors", "hiking"], "location": { "name": "Mount Rainier National Park", "latitude": 46.8800, "longitude": -121.7269 } } ], "has_more": true, "next_cursor": "cursor_xyz123" } }"#, ), ]; // Create a query that calls jsonb() multiple times in sequence let query = format!( "SELECT jsonb('{}'), jsonb('{}'), jsonb('{}'), jsonb('{}'), jsonb('{}'), jsonb('{}'), jsonb('{}'), jsonb('{}')", json_payloads[0].1.replace("'", "\\'"), json_payloads[1].1.replace("'", "\\'"), json_payloads[2].1.replace("'", "\\'"), json_payloads[3].1.replace("'", "\\'"), json_payloads[0].1.replace("'", "\\'"), json_payloads[1].1.replace("'", "\\'"), json_payloads[2].1.replace("'", "\\'"), json_payloads[3].1.replace("'", "\\'"), ); let mut group = criterion.benchmark_group("Sequential JSONB Calls"); group.bench_function("Limbo - Sequential", |b| { let mut stmt = limbo_conn.prepare(&query).unwrap(); b.iter(|| { loop { match stmt.step().unwrap() { turso_core::StepResult::Row => {} turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } stmt.reset().unwrap(); }); }); if enable_rusqlite { let sqlite_conn = rusqlite_open(); group.bench_function("Sqlite3 - Sequential", |b| { let mut stmt = sqlite_conn.prepare(&query).unwrap(); b.iter(|| { let mut rows = stmt.raw_query(); while let Some(row) = rows.next().unwrap() { black_box(row); } }); }); } group.finish(); } fn bench_json_patch(criterion: &mut Criterion) { let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, "../testing/system/testing.db").unwrap(); let limbo_conn = db.connect().unwrap(); let json_patch_cases = [ ( "Simple Property Update", r#"{"name": "Original", "value": 42}"#, r#"{"name": "Updated", "value": 100}"#, ), ( "Add New Property", r#"{"name": "Original", "value": 42}"#, r#"{"name": "Original", "value": 42, "description": "Added field"}"#, ), ( "Remove Property", r#"{"name": "Original", "value": 42, "toRemove": true}"#, r#"{"name": "Original", "value": 42}"#, ), ( "Nested Property Update", r#"{"name": "Original", "value": 42, "nested": {"a": 1, "b": 2}}"#, r#"{"name": "Updated", "value": 42, "nested": {"a": 10, "b": 2, "c": 3}}"#, ), ( "Array Update", r#"{"items": ["apple", "banana", "cherry"]}"#, r#"{"items": ["avocado", "cherry", "dragon fruit"]}"#, ), ( "Complex User Object Update", r#"{ "user": { "id": "usr_7f8d3a2e", "name": "Jane Smith", "email": "jane.smith@example.com", "verified": true, "preferences": { "theme": "dark", "notifications": { "email": true, "push": false, "sms": true }, "language": "en-US" } } }"#, r#"{ "user": { "id": "usr_7f8d3a2e", "name": "Jane Doe", "email": "jane.doe@example.com", "verified": true, "preferences": { "theme": "light", "notifications": { "email": true, "push": true, "sms": false }, "language": "en-US" }, "subscription": { "plan": "premium", "status": "active" } } }"#, ), ( "Large Config Update", r#"{ "app_name": "DataProcessor", "version": "2.1.3", "environment": "production", "debug": false, "log_level": "info", "database": { "main": { "host": "db-primary.internal", "port": 5432, "name": "app_production", "user": "app_user", "max_connections": 50, "timeout_ms": 5000 }, "replica": { "host": "db-replica.internal", "port": 5432, "name": "app_production_replica", "user": "app_readonly", "max_connections": 25, "timeout_ms": 3000 } }, "cache": { "enabled": true, "ttl_seconds": 3600, "max_size_mb": 512 }, "api": { "host": "0.0.0.0", "port": 8080, "rate_limit": { "requests_per_minute": 120, "burst": 30 }, "timeouts": { "read_ms": 5000, "write_ms": 10000, "idle_ms": 60000 } }, "feature_flags": { "new_dashboard": true, "beta_analytics": false, "improved_search": true } }"#, r#"{ "app_name": "DataProcessor", "version": "2.2.0", "environment": "production", "debug": true, "log_level": "debug", "database": { "main": { "host": "db-primary.internal", "port": 5432, "name": "app_production", "user": "app_user", "max_connections": 100, "timeout_ms": 5000 }, "replica": { "host": "db-replica.internal", "port": 5432, "name": "app_production_replica", "user": "app_readonly", "max_connections": 25, "timeout_ms": 3000 }, "backup": { "host": "db-backup.internal", "port": 5432 } }, "cache": { "enabled": true, "ttl_seconds": 3600, "max_size_mb": 1024 }, "api": { "host": "0.0.0.0", "port": 8080, "rate_limit": { "requests_per_minute": 240, "burst": 30 }, "timeouts": { "read_ms": 5000, "write_ms": 10000, "idle_ms": 60000 } }, "feature_flags": { "new_dashboard": true, "beta_analytics": true, "improved_search": true, "ai_recommendations": true } }"#, ), ( "Deeply Nested Social Feed Update", r#"{ "feed": { "user_id": "u_12345", "posts": [ { "id": "post_001", "author": "user_789", "content": "Just launched our new product!", "likes": 24, "comments": [ { "id": "comment_001", "author": "user_456", "content": "Looks amazing!", "likes": 3 }, { "id": "comment_002", "author": "user_789", "content": "Thanks!", "likes": 1 } ] } ] } }"#, r#"{ "feed": { "user_id": "u_12345", "posts": [ { "id": "post_001", "author": "user_789", "content": "Updated product announcement!", "likes": 35, "comments": [ { "id": "comment_001", "author": "user_456", "content": "This is incredible!", "likes": 5 }, { "id": "comment_002", "author": "user_789", "content": "Thanks!", "likes": 1 }, { "id": "comment_003", "author": "user_555", "content": "Just ordered one!", "likes": 0 } ], "tags": ["product", "launch"] } ] } }"#, ), ]; for (case_name, target_json, patch_json) in json_patch_cases.iter() { let query = format!( "SELECT json_patch('{}', '{}')", target_json.replace("'", "''"), patch_json.replace("'", "''") ); let mut group = criterion.benchmark_group(format!("JSON Patch - {case_name}")); group.bench_function("Limbo", |b| { let mut stmt = limbo_conn.prepare(&query).unwrap(); b.iter(|| { loop { match stmt.step().unwrap() { turso_core::StepResult::Row => {} turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } stmt.reset().unwrap(); }); }); if enable_rusqlite { let sqlite_conn = rusqlite_open(); group.bench_function("Sqlite3", |b| { let mut stmt = sqlite_conn.prepare(&query).unwrap(); b.iter(|| { let mut rows = stmt.raw_query(); while let Some(row) = rows.next().unwrap() { black_box(row); } }); }); } group.finish(); } } #[cfg(not(feature = "codspeed"))] criterion_group! { name = benches; config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(Some(Options::default())))); targets = bench, bench_sequential_jsonb, bench_json_patch } #[cfg(feature = "codspeed")] criterion_group! { name = benches; config = Criterion::default(); targets = bench, bench_sequential_jsonb, bench_json_patch } criterion_main!(benches); ================================================ FILE: core/benches/mvcc_benchmark.rs ================================================ use std::sync::Arc; #[cfg(not(feature = "codspeed"))] use criterion::{ async_executor::FuturesExecutor, criterion_group, criterion_main, Criterion, Throughput, }; #[cfg(not(feature = "codspeed"))] use pprof::criterion::{Output, PProfProfiler}; #[cfg(feature = "codspeed")] use codspeed_criterion_compat::{ async_executor::FuturesExecutor, criterion_group, criterion_main, Criterion, Throughput, }; use turso_core::mvcc::clock::MvccClock; use turso_core::mvcc::database::{MvStore, Row, RowID, RowKey}; use turso_core::types::{IOResult, ImmutableRecord, Text}; use turso_core::{Connection, Database, MemoryIO, Value}; struct BenchDb { _db: Arc, conn: Arc, mvcc_store: Arc>, } fn bench_db() -> BenchDb { let io = Arc::new(MemoryIO::new()); let db = Database::open_file(io, ":memory:").unwrap(); let conn = db.connect().unwrap(); // Enable MVCC via PRAGMA conn.execute("PRAGMA journal_mode = 'mvcc'").unwrap(); let mvcc_store = db.get_mv_store().clone().unwrap(); BenchDb { _db: db, conn, mvcc_store, } } fn bench(c: &mut Criterion) { let mut group = c.benchmark_group("mvcc-ops-throughput"); group.throughput(Throughput::Elements(1)); group.bench_function("begin_tx + rollback_tx", |b| { let db = bench_db(); b.to_async(FuturesExecutor).iter(|| async { let conn = db.conn.clone(); let tx_id = db.mvcc_store.begin_tx(conn.get_pager()).unwrap(); db.mvcc_store .rollback_tx(tx_id, conn.get_pager(), &conn, turso_core::MAIN_DB_ID); }) }); let db = bench_db(); group.bench_function("begin_tx + commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let conn = &db.conn; let tx_id = db.mvcc_store.begin_tx(conn.get_pager()).unwrap(); let mv_store = &db.mvcc_store; let mut sm = mv_store.commit_tx(tx_id, conn).unwrap(); // TODO: sync IO hack loop { let res = sm.step(mv_store).unwrap(); match res { IOResult::IO(io) => io.wait(db._db.io.as_ref()).unwrap(), IOResult::Done(_) => break, } } }) }); let db = bench_db(); group.bench_function("begin_tx-read-commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let conn = &db.conn; let tx_id = db.mvcc_store.begin_tx(conn.get_pager()).unwrap(); db.mvcc_store .read( tx_id, &RowID { table_id: (-2).into(), row_id: RowKey::Int(1), }, ) .unwrap(); let mv_store = &db.mvcc_store; let mut sm = mv_store.commit_tx(tx_id, conn).unwrap(); // TODO: sync IO hack loop { let res = sm.step(mv_store).unwrap(); match res { IOResult::IO(io) => io.wait(db._db.io.as_ref()).unwrap(), IOResult::Done(_) => break, } } }) }); let db = bench_db(); let record = ImmutableRecord::from_values(&vec![Value::Text(Text::new("World"))], 1); let record_data = record.as_blob(); group.bench_function("begin_tx-update-commit_tx", |b| { b.to_async(FuturesExecutor).iter(|| async { let conn = &db.conn; let tx_id = db.mvcc_store.begin_tx(conn.get_pager()).unwrap(); db.mvcc_store .update( tx_id, Row::new_table_row( RowID::new((-2).into(), RowKey::Int(1)), record_data.clone(), 1, ), ) .unwrap(); let mv_store = &db.mvcc_store; let mut sm = mv_store.commit_tx(tx_id, conn).unwrap(); // TODO: sync IO hack loop { let res = sm.step(mv_store).unwrap(); match res { IOResult::IO(io) => io.wait(db._db.io.as_ref()).unwrap(), IOResult::Done(_) => break, } } }) }); let db = bench_db(); let tx_id = db.mvcc_store.begin_tx(db.conn.get_pager()).unwrap(); db.mvcc_store .insert( tx_id, Row::new_table_row( RowID::new((-2).into(), RowKey::Int(1)), record_data.clone(), 1, ), ) .unwrap(); group.bench_function("read", |b| { b.to_async(FuturesExecutor).iter(|| async { db.mvcc_store .read( tx_id, &RowID { table_id: (-2).into(), row_id: RowKey::Int(1), }, ) .unwrap(); }) }); let db = bench_db(); let tx_id = db.mvcc_store.begin_tx(db.conn.get_pager()).unwrap(); db.mvcc_store .insert( tx_id, Row::new_table_row( RowID::new((-2).into(), RowKey::Int(1)), record_data.clone(), 1, ), ) .unwrap(); group.bench_function("update", |b| { b.to_async(FuturesExecutor).iter(|| async { db.mvcc_store .update( tx_id, Row::new_table_row( RowID::new((-2).into(), RowKey::Int(1)), record_data.clone(), 1, ), ) .unwrap(); }) }); } #[cfg(not(feature = "codspeed"))] criterion_group! { name = benches; config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); targets = bench } #[cfg(feature = "codspeed")] criterion_group! { name = benches; config = Criterion::default(); targets = bench } criterion_main!(benches); ================================================ FILE: core/benches/sql_functions/datetime.rs ================================================ use divan::{black_box, Bencher}; use turso_core::functions::datetime::{ exec_date, exec_datetime_full, exec_julianday, exec_strftime, exec_time, exec_timediff, exec_unixepoch, }; use turso_core::types::Value; // ============================================================================= // Fast Path Parsing Benchmarks // These formats use the optimized custom parser (no chrono overhead) // ============================================================================= #[divan::bench] fn fast_path_date_only(bencher: Bencher) { // YYYY-MM-DD (10 chars) - fast path let args = [Value::build_text("2024-07-21")]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } #[divan::bench] fn fast_path_datetime_hhmm(bencher: Bencher) { // YYYY-MM-DD HH:MM (16 chars) - fast path let args = [Value::build_text("2024-07-21 14:30")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn fast_path_datetime_hhmmss(bencher: Bencher) { // YYYY-MM-DD HH:MM:SS (19 chars) - fast path let args = [Value::build_text("2024-07-21 14:30:45")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn fast_path_datetime_with_frac(bencher: Bencher) { // YYYY-MM-DD HH:MM:SS.fff (23 chars) - fast path let args = [Value::build_text("2024-07-21 14:30:45.123")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn fast_path_datetime_t_separator(bencher: Bencher) { // YYYY-MM-DDTHH:MM:SS - fast path with T separator let args = [Value::build_text("2024-07-21T14:30:45")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn fast_path_time_hhmm(bencher: Bencher) { // HH:MM (5 chars) - fast path let args = [Value::build_text("14:30")]; bencher.bench_local(|| black_box(exec_time(black_box(&args)))); } #[divan::bench] fn fast_path_time_hhmmss(bencher: Bencher) { // HH:MM:SS (8 chars) - fast path let args = [Value::build_text("14:30:45")]; bencher.bench_local(|| black_box(exec_time(black_box(&args)))); } #[divan::bench] fn fast_path_time_with_frac(bencher: Bencher) { // HH:MM:SS.fff - fast path let args = [Value::build_text("14:30:45.123")]; bencher.bench_local(|| black_box(exec_time(black_box(&args)))); } // ============================================================================= // Slow Path Parsing Benchmarks // These formats require chrono's parser (timezone handling) // ============================================================================= #[divan::bench] fn slow_path_datetime_utc_z(bencher: Bencher) { // Ends with Z - triggers slow path for timezone let args = [Value::build_text("2024-07-21 14:30:45Z")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn slow_path_datetime_tz_offset(bencher: Bencher) { // Has timezone offset - slow path let args = [Value::build_text("2024-07-21 14:30:45+02:00")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn slow_path_datetime_negative_tz(bencher: Bencher) { // Negative timezone offset - slow path let args = [Value::build_text("2024-07-21 14:30:45-05:00")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn slow_path_time_with_tz(bencher: Bencher) { // Time with timezone - slow path let args = [Value::build_text("14:30:45+02:00")]; bencher.bench_local(|| black_box(exec_time(black_box(&args)))); } // ============================================================================= // Numeric Input Benchmarks (Julian Day) // ============================================================================= #[divan::bench] fn julian_day_float_input(bencher: Bencher) { // Float julian day value let args = [Value::from_f64(2460512.5)]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } #[divan::bench] fn julian_day_integer_input(bencher: Bencher) { // Integer julian day value let args = [Value::from_i64(2460513)]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } #[divan::bench] fn julian_day_string_numeric(bencher: Bencher) { // Numeric string that parses as julian day let args = [Value::build_text("2460512.5")]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } // ============================================================================= // Output Type Benchmarks // ============================================================================= #[divan::bench] fn output_date(bencher: Bencher) { let args = [Value::build_text("2024-07-21 14:30:45")]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } #[divan::bench] fn output_time(bencher: Bencher) { let args = [Value::build_text("2024-07-21 14:30:45")]; bencher.bench_local(|| black_box(exec_time(black_box(&args)))); } #[divan::bench] fn output_datetime(bencher: Bencher) { let args = [Value::build_text("2024-07-21 14:30:45")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn output_julianday(bencher: Bencher) { let args = [Value::build_text("2024-07-21 14:30:45")]; bencher.bench_local(|| black_box(exec_julianday(black_box(&args)))); } #[divan::bench] fn output_unixepoch(bencher: Bencher) { let args = [Value::build_text("2024-07-21 14:30:45")]; bencher.bench_local(|| black_box(exec_unixepoch(black_box(&args)))); } // ============================================================================= // strftime Benchmarks // ============================================================================= #[divan::bench] fn strftime_simple_format(bencher: Bencher) { let args = [ Value::build_text("%Y-%m-%d"), Value::build_text("2024-07-21 14:30:45"), ]; bencher.bench_local(|| black_box(exec_strftime(black_box(&args)))); } #[divan::bench] fn strftime_complex_format(bencher: Bencher) { let args = [ Value::build_text("%Y-%m-%d %H:%M:%S"), Value::build_text("2024-07-21 14:30:45"), ]; bencher.bench_local(|| black_box(exec_strftime(black_box(&args)))); } #[divan::bench] fn strftime_with_julian(bencher: Bencher) { // %J is SQLite-specific julian day format let args = [ Value::build_text("%J"), Value::build_text("2024-07-21 14:30:45"), ]; bencher.bench_local(|| black_box(exec_strftime(black_box(&args)))); } #[divan::bench] fn strftime_weekday_format(bencher: Bencher) { let args = [ Value::build_text("%w %W"), Value::build_text("2024-07-21 14:30:45"), ]; bencher.bench_local(|| black_box(exec_strftime(black_box(&args)))); } // ============================================================================= // Modifier Benchmarks // ============================================================================= #[divan::bench] fn modifier_add_days(bencher: Bencher) { let args = [ Value::build_text("2024-07-21"), Value::build_text("+5 days"), ]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } #[divan::bench] fn modifier_add_fractional_days(bencher: Bencher) { // Fractional modifier (new feature from PR) let args = [ Value::build_text("2024-07-21 12:00:00"), Value::build_text("+1.5 days"), ]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn modifier_add_months(bencher: Bencher) { let args = [ Value::build_text("2024-07-21"), Value::build_text("+3 months"), ]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } #[divan::bench] fn modifier_start_of_month(bencher: Bencher) { let args = [ Value::build_text("2024-07-21 14:30:45"), Value::build_text("start of month"), ]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } #[divan::bench] fn modifier_start_of_year(bencher: Bencher) { let args = [ Value::build_text("2024-07-21 14:30:45"), Value::build_text("start of year"), ]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } #[divan::bench] fn modifier_weekday(bencher: Bencher) { let args = [ Value::build_text("2024-07-21"), Value::build_text("weekday 0"), ]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } #[divan::bench] fn modifier_unixepoch(bencher: Bencher) { // unixepoch modifier for numeric input let args = [Value::from_i64(1721577045), Value::build_text("unixepoch")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn modifier_auto_unixepoch(bencher: Bencher) { // auto modifier detecting unix epoch let args = [Value::from_i64(1721577045), Value::build_text("auto")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn modifier_auto_julianday(bencher: Bencher) { // auto modifier detecting julian day let args = [Value::from_f64(2460512.5), Value::build_text("auto")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn modifier_localtime(bencher: Bencher) { let args = [ Value::build_text("2024-07-21 14:30:45"), Value::build_text("localtime"), ]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn modifier_chain_multiple(bencher: Bencher) { // Multiple modifiers chained let args = [ Value::build_text("2024-07-21"), Value::build_text("+1 month"), Value::build_text("start of month"), Value::build_text("+7 days"), ]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } // ============================================================================= // timediff Benchmarks // ============================================================================= #[divan::bench] fn timediff_same_day(bencher: Bencher) { let args = [ Value::build_text("2024-07-21 14:30:45"), Value::build_text("2024-07-21 10:15:30"), ]; bencher.bench_local(|| black_box(exec_timediff(black_box(&args)))); } #[divan::bench] fn timediff_different_days(bencher: Bencher) { let args = [ Value::build_text("2024-07-25 14:30:45"), Value::build_text("2024-07-21 10:15:30"), ]; bencher.bench_local(|| black_box(exec_timediff(black_box(&args)))); } #[divan::bench] fn timediff_different_years(bencher: Bencher) { let args = [ Value::build_text("2024-07-21 14:30:45"), Value::build_text("2020-01-15 10:15:30"), ]; bencher.bench_local(|| black_box(exec_timediff(black_box(&args)))); } // ============================================================================= // Special Cases // ============================================================================= #[divan::bench] fn special_now(bencher: Bencher) { let args = [Value::build_text("now")]; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn special_no_args_current_time(bencher: Bencher) { // No arguments - returns current time let args: [Value; 0] = []; bencher.bench_local(|| black_box(exec_datetime_full(black_box(&args)))); } #[divan::bench] fn special_subsec_modifier(bencher: Bencher) { let args = [ Value::build_text("2024-07-21 14:30:45.123456"), Value::build_text("subsec"), ]; bencher.bench_local(|| black_box(exec_time(black_box(&args)))); } #[divan::bench] fn special_date_overflow(bencher: Bencher) { // Invalid date that overflows (Feb 30 -> Mar 2) let args = [Value::build_text("2024-02-30")]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } #[divan::bench] fn special_negative_year(bencher: Bencher) { // Negative year (BCE dates) let args = [Value::build_text("-0044-03-15")]; bencher.bench_local(|| black_box(exec_date(black_box(&args)))); } ================================================ FILE: core/benches/sql_functions/likeop.rs ================================================ use divan::{black_box, Bencher}; use turso_core::types::Value; // ============================================================================= // LIKE Pattern Matching Benchmarks // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn like_simple_exact_match(bencher: Bencher) { bencher.bench_local(|| { black_box(Value::exec_like( black_box("hello"), black_box("hello"), Some('\\'), )) .unwrap() }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn like_simple_no_match(bencher: Bencher) { bencher.bench_local(|| { black_box(Value::exec_like( black_box("hello"), black_box("world"), Some('\\'), )) .unwrap() }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn like_percent_prefix(bencher: Bencher) { // Pattern: %world - matches anything ending with "world" bencher.bench_local(|| { black_box(Value::exec_like( black_box("%world"), black_box("hello world"), Some('\\'), )) .unwrap() }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn like_percent_suffix(bencher: Bencher) { // Pattern: hello% - matches anything starting with "hello" bencher.bench_local(|| { black_box(Value::exec_like( black_box("hello%"), black_box("hello world"), Some('\\'), )) .unwrap() }); } #[divan::bench] fn like_percent_both(bencher: Bencher) { // Pattern: %llo wor% - matches anything containing "llo wor" bencher.bench_local(|| { black_box(Value::exec_like( black_box("%llo wor%"), black_box("hello world"), Some('\\'), )) .unwrap() }); } #[divan::bench] fn like_underscore_single(bencher: Bencher) { // Pattern: h_llo - matches "hello", "hallo", etc. bencher.bench_local(|| { black_box(Value::exec_like( black_box("h_llo"), black_box("hello"), Some('\\'), )) .unwrap() }); } #[divan::bench] fn like_underscore_multiple(bencher: Bencher) { // Pattern: h___o - matches 5 character words starting with h, ending with o bencher.bench_local(|| { black_box(Value::exec_like( black_box("h___o"), black_box("hello"), Some('\\'), )) .unwrap() }); } #[divan::bench] fn like_mixed_wildcards(bencher: Bencher) { // Pattern: %h_llo% - complex pattern bencher.bench_local(|| { black_box(Value::exec_like( black_box("%h_llo%"), black_box("say hello world"), Some('\\'), )) .unwrap() }); } #[divan::bench] fn like_escape_percent(bencher: Bencher) { // Testing escaped percent sign bencher.bench_local(|| { black_box(Value::exec_like( black_box("100\\%"), black_box("100%"), Some('\\'), )) .unwrap() }); } #[divan::bench] fn like_escape_underscore(bencher: Bencher) { // Testing escaped underscore bencher.bench_local(|| { black_box(Value::exec_like( black_box("file\\_name"), black_box("file_name"), Some('\\'), )) .unwrap() }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn like_case_insensitive(bencher: Bencher) { // LIKE is case-insensitive by default bencher.bench_local(|| { black_box(Value::exec_like( black_box("HELLO"), black_box("hello"), Some('\\'), )) .unwrap() }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn like_long_pattern(bencher: Bencher) { let pattern = "The quick brown fox %"; let text = "The quick brown fox jumps over the lazy dog"; bencher.bench_local(|| { black_box(Value::exec_like( black_box(pattern), black_box(text), Some('\\'), )) .unwrap() }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn like_long_text_short_pattern(bencher: Bencher) { let pattern = "%dog"; let text = "The quick brown fox jumps over the lazy dog"; bencher.bench_local(|| { black_box(Value::exec_like( black_box(pattern), black_box(text), Some('\\'), )) .unwrap() }); } #[divan::bench] fn like_many_percent_wildcards(bencher: Bencher) { // Pattern with multiple % wildcards - can be expensive let pattern = "%quick%fox%lazy%"; let text = "The quick brown fox jumps over the lazy dog"; bencher.bench_local(|| { black_box(Value::exec_like( black_box(pattern), black_box(text), Some('\\'), )) .unwrap() }); } // ============================================================================= // GLOB Pattern Matching Benchmarks // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn glob_simple_exact_match(bencher: Bencher) { bencher.bench_local(|| black_box(Value::exec_glob(black_box("hello"), black_box("hello")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn glob_simple_no_match(bencher: Bencher) { bencher.bench_local(|| black_box(Value::exec_glob(black_box("hello"), black_box("world")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn glob_star_prefix(bencher: Bencher) { // Pattern: *world - matches anything ending with "world" bencher.bench_local(|| { black_box(Value::exec_glob( black_box("*world"), black_box("hello world"), )) }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn glob_star_suffix(bencher: Bencher) { // Pattern: hello* - matches anything starting with "hello" bencher.bench_local(|| { black_box(Value::exec_glob( black_box("hello*"), black_box("hello world"), )) }); } #[divan::bench] fn glob_star_both(bencher: Bencher) { // Pattern: *llo wor* - matches anything containing "llo wor" bencher.bench_local(|| { black_box(Value::exec_glob( black_box("*llo wor*"), black_box("hello world"), )) }); } #[divan::bench] fn glob_question_single(bencher: Bencher) { // Pattern: h?llo - matches "hello", "hallo", etc. bencher.bench_local(|| black_box(Value::exec_glob(black_box("h?llo"), black_box("hello")))); } #[divan::bench] fn glob_question_multiple(bencher: Bencher) { // Pattern: h???o - matches 5 character words starting with h, ending with o bencher.bench_local(|| black_box(Value::exec_glob(black_box("h???o"), black_box("hello")))); } #[divan::bench] fn glob_character_class(bencher: Bencher) { // Pattern: [abc]* - matches words starting with a, b, or c bencher.bench_local(|| black_box(Value::exec_glob(black_box("[abc]*"), black_box("apple")))); } #[divan::bench] fn glob_character_class_range(bencher: Bencher) { // Pattern: [a-z]* - matches words starting with lowercase letter bencher.bench_local(|| black_box(Value::exec_glob(black_box("[a-z]*"), black_box("hello")))); } #[divan::bench] fn glob_character_class_negation(bencher: Bencher) { // Pattern: [^0-9]* - matches words not starting with digit bencher.bench_local(|| black_box(Value::exec_glob(black_box("[^0-9]*"), black_box("hello")))); } #[divan::bench] fn glob_mixed_wildcards(bencher: Bencher) { // Complex pattern with multiple wildcard types bencher.bench_local(|| { black_box(Value::exec_glob( black_box("*h?llo*"), black_box("say hello world"), )) }); } #[divan::bench] fn glob_file_path_pattern(bencher: Bencher) { // Common use case: file path matching bencher.bench_local(|| { black_box(Value::exec_glob( black_box("*/src/*.rs"), black_box("/home/user/src/main.rs"), )) }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn glob_long_pattern(bencher: Bencher) { let pattern = "The quick brown fox *"; let text = "The quick brown fox jumps over the lazy dog"; bencher.bench_local(|| black_box(Value::exec_glob(black_box(pattern), black_box(text)))); } #[divan::bench] fn glob_many_star_wildcards(bencher: Bencher) { // Pattern with multiple * wildcards let pattern = "*quick*fox*lazy*"; let text = "The quick brown fox jumps over the lazy dog"; bencher.bench_local(|| black_box(Value::exec_glob(black_box(pattern), black_box(text)))); } // ============================================================================= // GLOB with Cache Benchmarks // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn glob_with_cache_first_call(bencher: Bencher) { bencher.bench_local(|| { black_box(Value::exec_glob( black_box("hello*"), black_box("hello world"), )) }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn glob_with_cache_cached_hit(bencher: Bencher) { // Warm up the cache bencher.bench_local(|| { black_box(Value::exec_glob( black_box("hello*"), black_box("hello world"), )) }); } #[divan::bench] fn glob_complex_pattern_cached(bencher: Bencher) { let pattern = "*quick*fox*lazy*"; let text = "The quick brown fox jumps over the lazy dog"; // Warm up the cache bencher.bench_local(|| black_box(Value::exec_glob(black_box(pattern), black_box(text)))); } // ============================================================================= // Edge Cases and Special Patterns // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn like_empty_pattern(bencher: Bencher) { bencher.bench_local(|| { black_box(Value::exec_like(black_box(""), black_box(""), Some('\\'))).unwrap() }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn like_only_percent(bencher: Bencher) { // % matches everything bencher.bench_local(|| { black_box(Value::exec_like( black_box("%"), black_box("any string at all"), Some('\\'), )) .unwrap() }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn glob_only_star(bencher: Bencher) { // * matches everything bencher.bench_local(|| { black_box(Value::exec_glob( black_box("*"), black_box("any string at all"), )) }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn like_special_regex_chars(bencher: Bencher) { // Pattern with characters that are special in regex (checking for regression/bugs) bencher.bench_local(|| { black_box(Value::exec_like( black_box("test.file"), black_box("test.file"), Some('\\'), )) .unwrap() }); } #[divan::bench] fn glob_bracket_special_cases(bencher: Bencher) { // Test bracket edge cases bencher.bench_local(|| black_box(Value::exec_glob(black_box("a[]]b"), black_box("a]b")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn like_unicode_pattern(bencher: Bencher) { bencher.bench_local(|| { black_box(Value::exec_like( black_box("héllo%"), black_box("héllo world"), Some('\\'), )) .unwrap() }); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn glob_unicode_pattern(bencher: Bencher) { bencher.bench_local(|| { black_box(Value::exec_glob( black_box("héllo*"), black_box("héllo world"), )) }); } ================================================ FILE: core/benches/sql_functions/main.rs ================================================ mod datetime; mod likeop; mod numeric; mod value; use divan::AllocProfiler; use mimalloc::MiMalloc; #[global_allocator] static ALLOC: AllocProfiler = AllocProfiler::new(MiMalloc); fn main() { divan::main(); } ================================================ FILE: core/benches/sql_functions/numeric.rs ================================================ use divan::{black_box, Bencher}; #[cfg(feature = "nanosecond-bench")] use turso_core::numeric::str_to_i64; use turso_core::numeric::{format_float, str_to_f64, Numeric}; use turso_core::types::Value; // ============================================================================= // str_to_i64 Benchmarks - Integer String Parsing // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn str_to_i64_simple_positive(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_i64(black_box("12345")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn str_to_i64_simple_negative(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_i64(black_box("-12345")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn str_to_i64_with_plus_sign(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_i64(black_box("+12345")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn str_to_i64_max_value(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_i64(black_box("9223372036854775807")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn str_to_i64_min_value(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_i64(black_box("-9223372036854775808")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn str_to_i64_overflow(bencher: Bencher) { // Should return i64::MAX bencher.bench_local(|| black_box(str_to_i64(black_box("99999999999999999999999")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn str_to_i64_with_whitespace(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_i64(black_box(" 12345 ")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn str_to_i64_zero(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_i64(black_box("0")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn str_to_i64_empty(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_i64(black_box("")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn str_to_i64_non_numeric(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_i64(black_box("abc")))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn str_to_i64_mixed_content(bencher: Bencher) { // Should parse leading digits bencher.bench_local(|| black_box(str_to_i64(black_box("123abc456")))); } // ============================================================================= // str_to_f64 Benchmarks - Float String Parsing // ============================================================================= #[divan::bench] fn str_to_f64_simple_integer(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box("12345")))); } #[divan::bench] fn str_to_f64_simple_decimal(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box("123.456")))); } #[divan::bench] fn str_to_f64_negative_decimal(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box("-123.456")))); } #[divan::bench] fn str_to_f64_scientific_positive_exp(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box("1.23e10")))); } #[divan::bench] fn str_to_f64_scientific_negative_exp(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box("1.23e-10")))); } #[divan::bench] fn str_to_f64_scientific_uppercase(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box("1.23E10")))); } #[divan::bench] fn str_to_f64_very_small(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box("0.000000000001")))); } #[divan::bench] fn str_to_f64_very_large(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box("999999999999999999999")))); } #[divan::bench] fn str_to_f64_leading_decimal(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box(".456")))); } #[divan::bench] fn str_to_f64_trailing_decimal(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box("123.")))); } #[divan::bench] fn str_to_f64_with_whitespace(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box(" 123.456 ")))); } #[divan::bench] fn str_to_f64_zero(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box("0.0")))); } #[divan::bench] fn str_to_f64_many_decimal_places(bencher: Bencher) { bencher.bench_local(|| black_box(str_to_f64(black_box("3.141592653589793238462643383279")))); } #[divan::bench] fn str_to_f64_prefix_only(bencher: Bencher) { // Should parse leading number bencher.bench_local(|| black_box(str_to_f64(black_box("123.456abc")))); } // ============================================================================= // format_float Benchmarks - Float to String Formatting // ============================================================================= #[divan::bench] fn format_float_simple(bencher: Bencher) { bencher.bench_local(|| black_box(format_float(black_box(123.456)))); } #[divan::bench] fn format_float_integer(bencher: Bencher) { bencher.bench_local(|| black_box(format_float(black_box(12345.0)))); } #[divan::bench] fn format_float_negative(bencher: Bencher) { bencher.bench_local(|| black_box(format_float(black_box(-123.456)))); } #[divan::bench] fn format_float_zero(bencher: Bencher) { bencher.bench_local(|| black_box(format_float(black_box(0.0)))); } #[divan::bench] fn format_float_very_small(bencher: Bencher) { bencher.bench_local(|| black_box(format_float(black_box(1e-100)))); } #[divan::bench] fn format_float_very_large(bencher: Bencher) { bencher.bench_local(|| black_box(format_float(black_box(1e100)))); } #[divan::bench] fn format_float_scientific_needed(bencher: Bencher) { // Should trigger scientific notation bencher.bench_local(|| black_box(format_float(black_box(9.93e-322)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn format_float_nan(bencher: Bencher) { bencher.bench_local(|| black_box(format_float(black_box(f64::NAN)))); } #[divan::bench] fn format_float_infinity(bencher: Bencher) { bencher.bench_local(|| black_box(format_float(black_box(f64::INFINITY)))); } #[divan::bench] fn format_float_neg_infinity(bencher: Bencher) { bencher.bench_local(|| black_box(format_float(black_box(f64::NEG_INFINITY)))); } #[divan::bench] fn format_float_precision_edge(bencher: Bencher) { // Test precision handling bencher.bench_local(|| black_box(format_float(black_box(0.1 + 0.2)))); } // ============================================================================= // Numeric Type Conversion Benchmarks // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_from_integer_value(bencher: Bencher) { let value = Value::from_i64(12345); bencher.bench_local(|| black_box(Numeric::from_value(black_box(&value)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_from_float_value(bencher: Bencher) { let value = Value::from_f64(123.456); bencher.bench_local(|| black_box(Numeric::from_value(black_box(&value)))); } #[divan::bench] fn numeric_from_text_integer(bencher: Bencher) { let value = Value::build_text("12345"); bencher.bench_local(|| black_box(Numeric::from_value(black_box(&value)))); } #[divan::bench] fn numeric_from_text_float(bencher: Bencher) { let value = Value::build_text("123.456"); bencher.bench_local(|| black_box(Numeric::from_value(black_box(&value)))); } #[divan::bench] fn numeric_from_text_scientific(bencher: Bencher) { let value = Value::build_text("1.23e10"); bencher.bench_local(|| black_box(Numeric::from_value(black_box(&value)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_from_null(bencher: Bencher) { let value = Value::Null; bencher.bench_local(|| black_box(Numeric::from_value(black_box(&value)))); } #[divan::bench] fn numeric_from_blob(bencher: Bencher) { let value = Value::Blob(b"12345".to_vec()); bencher.bench_local(|| black_box(Numeric::from_value(black_box(&value)))); } #[divan::bench] fn numeric_from_string_ref(bencher: Bencher) { bencher.bench_local(|| black_box(Numeric::from(black_box("123.456")))); } // ============================================================================= // Numeric Arithmetic Benchmarks // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_add_integers(bencher: Bencher) { let a = Numeric::Integer(1000); let b = Numeric::Integer(2000); bencher.bench_local(|| black_box(black_box(a).checked_add(black_box(b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_add_floats(bencher: Bencher) { let a = Numeric::from("100.5"); let b = Numeric::from("200.5"); bencher.bench_local(|| black_box(black_box(a).checked_add(black_box(b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_add_mixed(bencher: Bencher) { let a = Numeric::Integer(100); let b = Numeric::from("200.5"); bencher.bench_local(|| black_box(black_box(a).checked_add(black_box(b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_sub_integers(bencher: Bencher) { let a = Numeric::Integer(2000); let b = Numeric::Integer(1000); bencher.bench_local(|| black_box(black_box(a).checked_sub(black_box(b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_mul_integers(bencher: Bencher) { let a = Numeric::Integer(100); let b = Numeric::Integer(200); bencher.bench_local(|| black_box(black_box(a).checked_mul(black_box(b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_div_integers(bencher: Bencher) { let a = Numeric::Integer(1000); let b = Numeric::Integer(10); bencher.bench_local(|| black_box(black_box(a).checked_div(black_box(b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_neg_integer(bencher: Bencher) { let a = Numeric::Integer(12345); bencher.bench_local(|| black_box(-black_box(a))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_add_overflow(bencher: Bencher) { // Should overflow to float let a = Numeric::Integer(i64::MAX); let b = Numeric::Integer(1); bencher.bench_local(|| black_box(black_box(a).checked_add(black_box(b)))); } // ============================================================================= // Numeric Strict Conversion Benchmarks // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_strict_from_integer(bencher: Bencher) { let value = Value::from_i64(12345); bencher.bench_local(|| black_box(Numeric::from_value_strict(black_box(&value)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_strict_from_float(bencher: Bencher) { let value = Value::from_f64(123.456); bencher.bench_local(|| black_box(Numeric::from_value_strict(black_box(&value)))); } #[divan::bench] fn numeric_strict_from_text_valid(bencher: Bencher) { let value = Value::build_text("123.456"); bencher.bench_local(|| black_box(Numeric::from_value_strict(black_box(&value)))); } #[divan::bench] fn numeric_strict_from_text_invalid_prefix(bencher: Bencher) { // Should return Null in strict mode let value = Value::build_text("123abc"); bencher.bench_local(|| black_box(Numeric::from_value_strict(black_box(&value)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn numeric_strict_from_blob(bencher: Bencher) { // Blob is always Null in strict mode let value = Value::Blob(b"12345".to_vec()); bencher.bench_local(|| black_box(Numeric::from_value_strict(black_box(&value)))); } ================================================ FILE: core/benches/sql_functions/value.rs ================================================ use divan::{black_box, Bencher}; use turso_core::types::Value; // ============================================================================= // String Case Functions // ============================================================================= #[divan::bench] fn lower_short_string(bencher: Bencher) { let value = Value::build_text("HELLO"); bencher.bench_local(|| black_box(black_box(&value).exec_lower())); } #[divan::bench] fn lower_long_string(bencher: Bencher) { let value = Value::build_text("THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG"); bencher.bench_local(|| black_box(black_box(&value).exec_lower())); } #[divan::bench] fn lower_integer(bencher: Bencher) { let value = Value::from_i64(12345); bencher.bench_local(|| black_box(black_box(&value).exec_lower())); } #[divan::bench] fn upper_short_string(bencher: Bencher) { let value = Value::build_text("hello"); bencher.bench_local(|| black_box(black_box(&value).exec_upper())); } #[divan::bench] fn upper_long_string(bencher: Bencher) { let value = Value::build_text("the quick brown fox jumps over the lazy dog"); bencher.bench_local(|| black_box(black_box(&value).exec_upper())); } #[divan::bench] fn upper_integer(bencher: Bencher) { let value = Value::from_i64(12345); bencher.bench_local(|| black_box(black_box(&value).exec_upper())); } // ============================================================================= // Length Functions // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn length_short_text(bencher: Bencher) { let value = Value::build_text("hello"); bencher.bench_local(|| black_box(black_box(&value).exec_length())); } #[divan::bench] fn length_long_text(bencher: Bencher) { let value = Value::build_text("the quick brown fox jumps over the lazy dog"); bencher.bench_local(|| black_box(black_box(&value).exec_length())); } #[divan::bench] fn length_unicode_text(bencher: Bencher) { let value = Value::build_text("héllo wörld 你好世界"); bencher.bench_local(|| black_box(black_box(&value).exec_length())); } #[divan::bench] fn length_integer(bencher: Bencher) { let value = Value::from_i64(123456789); bencher.bench_local(|| black_box(black_box(&value).exec_length())); } #[divan::bench] fn length_float(bencher: Bencher) { let value = Value::from_f64(123.456789); bencher.bench_local(|| black_box(black_box(&value).exec_length())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn length_blob(bencher: Bencher) { let value = Value::Blob(vec![0u8; 100]); bencher.bench_local(|| black_box(black_box(&value).exec_length())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn octet_length_text(bencher: Bencher) { let value = Value::build_text("héllo wörld"); bencher.bench_local(|| black_box(black_box(&value).exec_octet_length())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn octet_length_unicode(bencher: Bencher) { let value = Value::build_text("你好世界"); bencher.bench_local(|| black_box(black_box(&value).exec_octet_length())); } // ============================================================================= // Trim Functions // ============================================================================= #[divan::bench] fn trim_spaces(bencher: Bencher) { let value = Value::build_text(" hello world "); bencher.bench_local(|| black_box(black_box(&value).exec_trim(None))); } #[divan::bench] fn trim_with_pattern(bencher: Bencher) { let value = Value::build_text("xxxhello worldxxx"); let pattern = Value::build_text("x"); bencher.bench_local(|| black_box(black_box(&value).exec_trim(Some(black_box(&pattern))))); } #[divan::bench] fn ltrim_spaces(bencher: Bencher) { let value = Value::build_text(" hello world"); bencher.bench_local(|| black_box(black_box(&value).exec_ltrim(None))); } #[divan::bench] fn ltrim_with_pattern(bencher: Bencher) { let value = Value::build_text("xxxhello world"); let pattern = Value::build_text("x"); bencher.bench_local(|| black_box(black_box(&value).exec_ltrim(Some(black_box(&pattern))))); } #[divan::bench] fn rtrim_spaces(bencher: Bencher) { let value = Value::build_text("hello world "); bencher.bench_local(|| black_box(black_box(&value).exec_rtrim(None))); } #[divan::bench] fn rtrim_with_pattern(bencher: Bencher) { let value = Value::build_text("hello worldxxx"); let pattern = Value::build_text("x"); bencher.bench_local(|| black_box(black_box(&value).exec_rtrim(Some(black_box(&pattern))))); } // ============================================================================= // Substring Function // ============================================================================= #[divan::bench] fn substring_simple(bencher: Bencher) { let value = Value::build_text("hello world"); let start = Value::from_i64(1); let length = Value::from_i64(5); bencher.bench_local(|| { black_box(Value::exec_substring( black_box(&value), black_box(&start), Some(black_box(&length)), )) }); } #[divan::bench] fn substring_long_text(bencher: Bencher) { let value = Value::build_text("the quick brown fox jumps over the lazy dog"); let start = Value::from_i64(5); let length = Value::from_i64(15); bencher.bench_local(|| { black_box(Value::exec_substring( black_box(&value), black_box(&start), Some(black_box(&length)), )) }); } #[divan::bench] fn substring_unicode(bencher: Bencher) { let value = Value::build_text("héllo wörld 你好"); let start = Value::from_i64(1); let length = Value::from_i64(10); bencher.bench_local(|| { black_box(Value::exec_substring( black_box(&value), black_box(&start), Some(black_box(&length)), )) }); } #[divan::bench] fn substring_negative_start(bencher: Bencher) { let value = Value::build_text("hello world"); let start = Value::from_i64(-5); let length = Value::from_i64(5); bencher.bench_local(|| { black_box(Value::exec_substring( black_box(&value), black_box(&start), Some(black_box(&length)), )) }); } #[divan::bench] fn substring_blob(bencher: Bencher) { let value = Value::Blob(b"hello world".to_vec()); let start = Value::from_i64(1); let length = Value::from_i64(5); bencher.bench_local(|| { black_box(Value::exec_substring( black_box(&value), black_box(&start), Some(black_box(&length)), )) }); } // ============================================================================= // Instr Function // ============================================================================= #[divan::bench] fn instr_found_early(bencher: Bencher) { let value = Value::build_text("hello world"); let pattern = Value::build_text("ell"); bencher.bench_local(|| black_box(black_box(&value).exec_instr(black_box(&pattern)))); } #[divan::bench] fn instr_found_late(bencher: Bencher) { let value = Value::build_text("the quick brown fox jumps over the lazy dog"); let pattern = Value::build_text("dog"); bencher.bench_local(|| black_box(black_box(&value).exec_instr(black_box(&pattern)))); } #[divan::bench] fn instr_not_found(bencher: Bencher) { let value = Value::build_text("hello world"); let pattern = Value::build_text("xyz"); bencher.bench_local(|| black_box(black_box(&value).exec_instr(black_box(&pattern)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn instr_blob(bencher: Bencher) { let value = Value::Blob(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); let pattern = Value::Blob(vec![5, 6, 7]); bencher.bench_local(|| black_box(black_box(&value).exec_instr(black_box(&pattern)))); } // ============================================================================= // Replace Function // ============================================================================= #[divan::bench] fn replace_single_occurrence(bencher: Bencher) { let source = Value::build_text("hello world"); let pattern = Value::build_text("world"); let replacement = Value::build_text("there"); bencher.bench_local(|| { black_box(Value::exec_replace( black_box(&source), black_box(&pattern), black_box(&replacement), )) }); } #[divan::bench] fn replace_multiple_occurrences(bencher: Bencher) { let source = Value::build_text("banana banana banana"); let pattern = Value::build_text("banana"); let replacement = Value::build_text("apple"); bencher.bench_local(|| { black_box(Value::exec_replace( black_box(&source), black_box(&pattern), black_box(&replacement), )) }); } #[divan::bench] fn replace_empty_pattern(bencher: Bencher) { let source = Value::build_text("hello world"); let pattern = Value::build_text(""); let replacement = Value::build_text("x"); bencher.bench_local(|| { black_box(Value::exec_replace( black_box(&source), black_box(&pattern), black_box(&replacement), )) }); } // ============================================================================= // Quote Function // ============================================================================= #[divan::bench] fn quote_text(bencher: Bencher) { let value = Value::build_text("hello world"); bencher.bench_local(|| black_box(black_box(&value).exec_quote())); } #[divan::bench] fn quote_text_with_quotes(bencher: Bencher) { let value = Value::build_text("hello'world"); bencher.bench_local(|| black_box(black_box(&value).exec_quote())); } #[divan::bench] fn quote_integer(bencher: Bencher) { let value = Value::from_i64(12345); bencher.bench_local(|| black_box(black_box(&value).exec_quote())); } #[divan::bench] fn quote_blob(bencher: Bencher) { let value = Value::Blob(vec![0x01, 0x02, 0xAB, 0xCD, 0xEF]); bencher.bench_local(|| black_box(black_box(&value).exec_quote())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn quote_null(bencher: Bencher) { let value = Value::Null; bencher.bench_local(|| black_box(black_box(&value).exec_quote())); } // ============================================================================= // Soundex Function // ============================================================================= #[divan::bench] fn soundex_simple(bencher: Bencher) { let value = Value::build_text("Robert"); bencher.bench_local(|| black_box(black_box(&value).exec_soundex())); } #[divan::bench] fn soundex_complex(bencher: Bencher) { let value = Value::build_text("Ashcraft"); bencher.bench_local(|| black_box(black_box(&value).exec_soundex())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn soundex_non_ascii(bencher: Bencher) { let value = Value::build_text("闪电五连鞭"); bencher.bench_local(|| black_box(black_box(&value).exec_soundex())); } // ============================================================================= // Type Functions // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn typeof_integer(bencher: Bencher) { let value = Value::from_i64(12345); bencher.bench_local(|| black_box(black_box(&value).exec_typeof())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn typeof_float(bencher: Bencher) { let value = Value::from_f64(123.456); bencher.bench_local(|| black_box(black_box(&value).exec_typeof())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn typeof_text(bencher: Bencher) { let value = Value::build_text("hello"); bencher.bench_local(|| black_box(black_box(&value).exec_typeof())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn typeof_blob(bencher: Bencher) { let value = Value::Blob(vec![1, 2, 3]); bencher.bench_local(|| black_box(black_box(&value).exec_typeof())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn typeof_null(bencher: Bencher) { let value = Value::Null; bencher.bench_local(|| black_box(black_box(&value).exec_typeof())); } // ============================================================================= // Cast Function // ============================================================================= #[divan::bench] fn cast_integer_to_text(bencher: Bencher) { let value = Value::from_i64(12345); bencher.bench_local(|| black_box(black_box(&value).exec_cast("TEXT"))); } #[divan::bench] fn cast_float_to_integer(bencher: Bencher) { let value = Value::from_f64(123.456); bencher.bench_local(|| black_box(black_box(&value).exec_cast("INT"))); } #[divan::bench] fn cast_text_to_integer(bencher: Bencher) { let value = Value::build_text("12345"); bencher.bench_local(|| black_box(black_box(&value).exec_cast("INT"))); } #[divan::bench] fn cast_text_to_real(bencher: Bencher) { let value = Value::build_text("123.456"); bencher.bench_local(|| black_box(black_box(&value).exec_cast("REAL"))); } #[divan::bench] fn cast_text_to_blob(bencher: Bencher) { let value = Value::build_text("hello world"); bencher.bench_local(|| black_box(black_box(&value).exec_cast("BLOB"))); } #[divan::bench] fn cast_text_to_numeric(bencher: Bencher) { let value = Value::build_text("123.456"); bencher.bench_local(|| black_box(black_box(&value).exec_cast("NUMERIC"))); } // ============================================================================= // Hex/Unhex Functions // ============================================================================= #[divan::bench] fn hex_text(bencher: Bencher) { let value = Value::build_text("hello"); bencher.bench_local(|| black_box(black_box(&value).exec_hex())); } #[divan::bench] fn hex_blob(bencher: Bencher) { let value = Value::Blob(vec![0x01, 0x02, 0xAB, 0xCD, 0xEF]); bencher.bench_local(|| black_box(black_box(&value).exec_hex())); } #[divan::bench] fn hex_integer(bencher: Bencher) { let value = Value::from_i64(255); bencher.bench_local(|| black_box(black_box(&value).exec_hex())); } #[divan::bench] fn unhex_valid(bencher: Bencher) { let value = Value::build_text("48656C6C6F"); bencher.bench_local(|| black_box(black_box(&value).exec_unhex(None))); } #[divan::bench] fn unhex_with_ignored(bencher: Bencher) { let value = Value::build_text(" 48656C6C6F "); let ignore = Value::build_text(" "); bencher.bench_local(|| black_box(black_box(&value).exec_unhex(Some(black_box(&ignore))))); } // ============================================================================= // Unicode Function // ============================================================================= #[divan::bench] fn unicode_ascii(bencher: Bencher) { let value = Value::build_text("A"); bencher.bench_local(|| black_box(black_box(&value).exec_unicode())); } #[divan::bench] fn unicode_emoji(bencher: Bencher) { let value = Value::build_text("😊"); bencher.bench_local(|| black_box(black_box(&value).exec_unicode())); } #[divan::bench] fn unicode_cjk(bencher: Bencher) { let value = Value::build_text("你"); bencher.bench_local(|| black_box(black_box(&value).exec_unicode())); } // ============================================================================= // Numeric Functions // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn abs_positive_integer(bencher: Bencher) { let value = Value::from_i64(12345); bencher.bench_local(|| black_box(black_box(&value).exec_abs())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn abs_negative_integer(bencher: Bencher) { let value = Value::from_i64(-12345); bencher.bench_local(|| black_box(black_box(&value).exec_abs())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn abs_float(bencher: Bencher) { let value = Value::from_f64(-123.456); bencher.bench_local(|| black_box(black_box(&value).exec_abs())); } #[divan::bench] fn abs_text_numeric(bencher: Bencher) { let value = Value::build_text("-123.456"); bencher.bench_local(|| black_box(black_box(&value).exec_abs())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn sign_positive(bencher: Bencher) { let value = Value::from_i64(42); bencher.bench_local(|| black_box(black_box(&value).exec_sign())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn sign_negative(bencher: Bencher) { let value = Value::from_i64(-42); bencher.bench_local(|| black_box(black_box(&value).exec_sign())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn sign_zero(bencher: Bencher) { let value = Value::from_i64(0); bencher.bench_local(|| black_box(black_box(&value).exec_sign())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn sign_float(bencher: Bencher) { let value = Value::from_f64(-42.5); bencher.bench_local(|| black_box(black_box(&value).exec_sign())); } // ============================================================================= // Round Function // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn round_no_precision(bencher: Bencher) { let value = Value::from_f64(123.456); bencher.bench_local(|| black_box(black_box(&value).exec_round(None))); } #[divan::bench] fn round_with_precision(bencher: Bencher) { let value = Value::from_f64(123.456789); let precision = Value::from_i64(2); bencher.bench_local(|| black_box(black_box(&value).exec_round(Some(black_box(&precision))))); } #[divan::bench] fn round_high_precision(bencher: Bencher) { let value = Value::from_f64(std::f64::consts::PI); let precision = Value::from_i64(10); bencher.bench_local(|| black_box(black_box(&value).exec_round(Some(black_box(&precision))))); } // ============================================================================= // Log Function // ============================================================================= #[divan::bench] fn log_base_10(bencher: Bencher) { let value = Value::from_f64(100.0); bencher.bench_local(|| black_box(black_box(&value).exec_math_log(None))); } #[divan::bench] fn log_base_2(bencher: Bencher) { let value = Value::from_f64(8.0); let base = Value::from_f64(2.0); bencher.bench_local(|| black_box(black_box(&value).exec_math_log(Some(black_box(&base))))); } #[divan::bench] fn log_arbitrary_base(bencher: Bencher) { let value = Value::from_f64(100.0); let base = Value::from_f64(7.0); bencher.bench_local(|| black_box(black_box(&value).exec_math_log(Some(black_box(&base))))); } // ============================================================================= // Arithmetic Operations // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn add_integers(bencher: Bencher) { let a = Value::from_i64(1000); let b = Value::from_i64(2000); bencher.bench_local(|| black_box(black_box(&a).exec_add(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn add_floats(bencher: Bencher) { let a = Value::from_f64(100.5); let b = Value::from_f64(200.5); bencher.bench_local(|| black_box(black_box(&a).exec_add(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn add_mixed(bencher: Bencher) { let a = Value::from_i64(100); let b = Value::from_f64(200.5); bencher.bench_local(|| black_box(black_box(&a).exec_add(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn subtract_integers(bencher: Bencher) { let a = Value::from_i64(2000); let b = Value::from_i64(1000); bencher.bench_local(|| black_box(black_box(&a).exec_subtract(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn multiply_integers(bencher: Bencher) { let a = Value::from_i64(100); let b = Value::from_i64(200); bencher.bench_local(|| black_box(black_box(&a).exec_multiply(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn divide_integers(bencher: Bencher) { let a = Value::from_i64(1000); let b = Value::from_i64(10); bencher.bench_local(|| black_box(black_box(&a).exec_divide(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn remainder_integers(bencher: Bencher) { let a = Value::from_i64(17); let b = Value::from_i64(5); bencher.bench_local(|| black_box(black_box(&a).exec_remainder(black_box(&b)))); } // ============================================================================= // Bitwise Operations // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn bit_and(bencher: Bencher) { let a = Value::from_i64(0b11110000); let b = Value::from_i64(0b10101010); bencher.bench_local(|| black_box(black_box(&a).exec_bit_and(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn bit_or(bencher: Bencher) { let a = Value::from_i64(0b11110000); let b = Value::from_i64(0b00001111); bencher.bench_local(|| black_box(black_box(&a).exec_bit_or(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn bit_not(bencher: Bencher) { let a = Value::from_i64(0b11110000); bencher.bench_local(|| black_box(black_box(&a).exec_bit_not())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn shift_left(bencher: Bencher) { let a = Value::from_i64(1); let b = Value::from_i64(8); bencher.bench_local(|| black_box(black_box(&a).exec_shift_left(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn shift_right(bencher: Bencher) { let a = Value::from_i64(256); let b = Value::from_i64(4); bencher.bench_local(|| black_box(black_box(&a).exec_shift_right(black_box(&b)))); } // ============================================================================= // Boolean Operations // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn boolean_not_true(bencher: Bencher) { let value = Value::from_i64(1); bencher.bench_local(|| black_box(black_box(&value).exec_boolean_not())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn boolean_not_false(bencher: Bencher) { let value = Value::from_i64(0); bencher.bench_local(|| black_box(black_box(&value).exec_boolean_not())); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn and_true_true(bencher: Bencher) { let a = Value::from_i64(1); let b = Value::from_i64(1); bencher.bench_local(|| black_box(black_box(&a).exec_and(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn and_true_false(bencher: Bencher) { let a = Value::from_i64(1); let b = Value::from_i64(0); bencher.bench_local(|| black_box(black_box(&a).exec_and(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn or_false_false(bencher: Bencher) { let a = Value::from_i64(0); let b = Value::from_i64(0); bencher.bench_local(|| black_box(black_box(&a).exec_or(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn or_true_false(bencher: Bencher) { let a = Value::from_i64(1); let b = Value::from_i64(0); bencher.bench_local(|| black_box(black_box(&a).exec_or(black_box(&b)))); } // ============================================================================= // Concat Functions // ============================================================================= #[divan::bench] fn concat_two_strings(bencher: Bencher) { let a = Value::build_text("hello "); let b = Value::build_text("world"); bencher.bench_local(|| black_box(black_box(&a).exec_concat(black_box(&b)))); } #[divan::bench] fn concat_string_integer(bencher: Bencher) { let a = Value::build_text("count: "); let b = Value::from_i64(42); bencher.bench_local(|| black_box(black_box(&a).exec_concat(black_box(&b)))); } #[divan::bench] fn concat_blobs(bencher: Bencher) { let a = Value::Blob(b"hello ".to_vec()); let b = Value::Blob(b"world".to_vec()); bencher.bench_local(|| black_box(black_box(&a).exec_concat(black_box(&b)))); } #[divan::bench] fn concat_strings_multiple(bencher: Bencher) { let values = [ Value::build_text("the "), Value::build_text("quick "), Value::build_text("brown "), Value::build_text("fox"), ]; bencher.bench_local(|| black_box(Value::exec_concat_strings(black_box(values.iter())))); } #[divan::bench] fn concat_ws_strings(bencher: Bencher) { let values = [ Value::build_text(", "), Value::build_text("apple"), Value::build_text("banana"), Value::build_text("cherry"), ]; bencher.bench_local(|| black_box(Value::exec_concat_ws(black_box(values.iter())))); } // ============================================================================= // Char Function // ============================================================================= #[divan::bench] fn char_single(bencher: Bencher) { let values = [Value::from_i64(65)]; bencher.bench_local(|| black_box(Value::exec_char(black_box(values.iter())))); } #[divan::bench] fn char_multiple(bencher: Bencher) { let values = [ Value::from_i64(72), Value::from_i64(101), Value::from_i64(108), Value::from_i64(108), Value::from_i64(111), ]; bencher.bench_local(|| black_box(Value::exec_char(black_box(values.iter())))); } // ============================================================================= // Min/Max Functions // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn min_integers(bencher: Bencher) { let values = [ Value::from_i64(5), Value::from_i64(3), Value::from_i64(8), Value::from_i64(1), Value::from_i64(9), ]; bencher.bench_local(|| black_box(Value::exec_min(black_box(values.iter())))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn max_integers(bencher: Bencher) { let values = [ Value::from_i64(5), Value::from_i64(3), Value::from_i64(8), Value::from_i64(1), Value::from_i64(9), ]; bencher.bench_local(|| black_box(Value::exec_max(black_box(values.iter())))); } #[divan::bench] fn min_strings(bencher: Bencher) { let values = [ Value::build_text("banana"), Value::build_text("apple"), Value::build_text("cherry"), ]; bencher.bench_local(|| black_box(Value::exec_min(black_box(values.iter())))); } #[divan::bench] fn max_strings(bencher: Bencher) { let values = [ Value::build_text("banana"), Value::build_text("apple"), Value::build_text("cherry"), ]; bencher.bench_local(|| black_box(Value::exec_max(black_box(values.iter())))); } // ============================================================================= // Nullif Function // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn nullif_equal(bencher: Bencher) { let a = Value::from_i64(42); let b = Value::from_i64(42); bencher.bench_local(|| black_box(black_box(&a).exec_nullif(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn nullif_not_equal(bencher: Bencher) { let a = Value::from_i64(42); let b = Value::from_i64(100); bencher.bench_local(|| black_box(black_box(&a).exec_nullif(black_box(&b)))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn nullif_strings(bencher: Bencher) { let a = Value::build_text("hello"); let b = Value::build_text("hello"); bencher.bench_local(|| black_box(black_box(&a).exec_nullif(black_box(&b)))); } // ============================================================================= // Zeroblob Function // ============================================================================= #[divan::bench] fn zeroblob_small(bencher: Bencher) { let value = Value::from_i64(10); bencher.bench_local(|| black_box(black_box(&value).exec_zeroblob().unwrap())); } #[divan::bench] fn zeroblob_medium(bencher: Bencher) { let value = Value::from_i64(1000); bencher.bench_local(|| black_box(black_box(&value).exec_zeroblob().unwrap())); } #[divan::bench] fn zeroblob_large(bencher: Bencher) { let value = Value::from_i64(10000); bencher.bench_local(|| black_box(black_box(&value).exec_zeroblob().unwrap())); } // ============================================================================= // If/Conditional Function // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn exec_if_true(bencher: Bencher) { let value = Value::from_i64(1); bencher.bench_local(|| black_box(black_box(&value).exec_if(false, false))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn exec_if_false(bencher: Bencher) { let value = Value::from_i64(0); bencher.bench_local(|| black_box(black_box(&value).exec_if(false, false))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn exec_if_null(bencher: Bencher) { let value = Value::Null; bencher.bench_local(|| black_box(black_box(&value).exec_if(true, false))); } #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn exec_if_not(bencher: Bencher) { let value = Value::from_i64(1); bencher.bench_local(|| black_box(black_box(&value).exec_if(false, true))); } // ============================================================================= // LIKE Pattern // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn construct_like_exact(bencher: Bencher) { bencher.bench_local(|| { black_box(Value::exec_like( black_box("hello"), black_box("hello"), None, )) .unwrap() }); } #[divan::bench] fn construct_like_contains(bencher: Bencher) { bencher.bench_local(|| { black_box(Value::exec_like( black_box("%hello%"), black_box("hello"), None, )) .unwrap() }); } #[divan::bench] fn construct_like_with_single_wildcard(bencher: Bencher) { bencher.bench_local(|| { black_box(Value::exec_like( black_box("h_llo"), black_box("hello"), None, )) .unwrap() }); } #[divan::bench] fn construct_like_complex(bencher: Bencher) { bencher.bench_local(|| { black_box(Value::exec_like( black_box("%h_llo%w_rld%"), black_box("hello world"), None, )) .unwrap() }); } // ============================================================================= // Random Functions // ============================================================================= #[cfg(feature = "nanosecond-bench")] #[divan::bench] fn exec_random(bencher: Bencher) { bencher.bench_local(|| black_box(Value::exec_random(|| 42))); } #[divan::bench] fn exec_randomblob_small(bencher: Bencher) { let length = Value::from_i64(10); bencher.bench_local(|| { black_box( black_box(&length) .exec_randomblob(|buf| buf.fill(0)) .unwrap(), ) }); } #[divan::bench] fn exec_randomblob_medium(bencher: Bencher) { let length = Value::from_i64(100); bencher.bench_local(|| { black_box( black_box(&length) .exec_randomblob(|buf| buf.fill(0)) .unwrap(), ) }); } #[divan::bench] fn exec_randomblob_large(bencher: Bencher) { let length = Value::from_i64(1000); bencher.bench_local(|| { black_box( black_box(&length) .exec_randomblob(|buf| buf.fill(0)) .unwrap(), ) }); } ================================================ FILE: core/benches/tpc_h_benchmark.rs ================================================ use std::sync::Arc; #[cfg(not(feature = "codspeed"))] use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, SamplingMode}; #[cfg(not(feature = "codspeed"))] use pprof::criterion::{Output, PProfProfiler}; #[cfg(feature = "codspeed")] use codspeed_criterion_compat::{ black_box, criterion_group, criterion_main, BenchmarkId, Criterion, SamplingMode, }; use turso_core::{Database, PlatformIO}; const TPC_H_PATH: &str = "../perf/tpc-h/TPC-H.db"; macro_rules! tpc_query { ($num:literal) => { ( $num, include_str!(concat!("../../perf/tpc-h/queries/", $num, ".sql")), ) }; } fn rusqlite_open_tpc_h() -> rusqlite::Connection { let sqlite_conn = rusqlite::Connection::open(TPC_H_PATH).unwrap(); sqlite_conn .pragma_update(None, "locking_mode", "EXCLUSIVE") .unwrap(); sqlite_conn } fn bench_tpc_h_queries(criterion: &mut Criterion) { // https://github.com/tursodatabase/turso/issues/174 // The rusqlite benchmark crashes on Mac M1 when using the flamegraph features let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err(); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, TPC_H_PATH).unwrap(); let limbo_conn = db.connect().unwrap(); let queries = [ tpc_query!(1), // tpc_query!(2), // Skipped as subquery in bind_column references is todo! tpc_query!(3), // thread 'main' panicked at core/translate/planner.rs:256:28: // not yet implemented // tpc_query!(4), tpc_query!(5), tpc_query!(6), tpc_query!(7), tpc_query!(8), tpc_query!(9), tpc_query!(10), // tpc_query!(11), // Skipped not implemented tpc_query!(12), // thread 'main' panicked at core/storage/btree.rs:3233:26: // overflow cell with divider cell was not found // tpc_query!(13), tpc_query!(14), // thread 'main' panicked at core/benches/tpc_h_benchmark.rs:71:62: // called `Result::unwrap()` on an `Err` value: ParseError("CREATE VIEW not supported yet") // tpc_query!(15), // thread 'main' panicked at core/translate/planner.rs:267:34: // not yet implemented // tpc_query!(16), // thread 'main' panicked at core/translate/planner.rs:291:30: // not yet implemented // tpc_query!(17), // thread 'main' panicked at core/translate/planner.rs:267:34: // not yet implemented // tpc_query!(18), tpc_query!(19), // thread 'main' panicked at core/translate/planner.rs:267:34: // not yet implemented // tpc_query!(20), // thread 'main' panicked at core/translate/planner.rs:256:28: // not yet implemented // tpc_query!(21), // thread 'main' panicked at core/translate/planner.rs:291:30: // not yet implemented // tpc_query!(22), ]; for (idx, query) in queries.iter() { let mut group = criterion.benchmark_group(format!("Query `{idx}` ")); group.sampling_mode(SamplingMode::Flat); group.sample_size(10); group.bench_with_input( BenchmarkId::new("limbo_tpc_h_query", idx), query, |b, query| { b.iter(|| { let mut stmt = limbo_conn.prepare(query).unwrap(); loop { match stmt.step().unwrap() { turso_core::StepResult::Row => { black_box(stmt.row()); } turso_core::StepResult::IO => { db.io.step().unwrap(); } turso_core::StepResult::Done => { break; } turso_core::StepResult::Interrupt | turso_core::StepResult::Busy => { unreachable!(); } } } stmt.reset().unwrap(); }); }, ); if enable_rusqlite { let sqlite_conn = rusqlite_open_tpc_h(); group.bench_with_input( BenchmarkId::new("sqlite_tpc_h_query", idx), query, |b, query| { let mut stmt = sqlite_conn.prepare(query).unwrap(); b.iter(|| { let mut rows = stmt.raw_query(); while let Some(row) = rows.next().unwrap() { black_box(row); } }); }, ); } group.finish(); } } #[cfg(not(feature = "codspeed"))] criterion_group! { name = benches; config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); targets = bench_tpc_h_queries } #[cfg(feature = "codspeed")] criterion_group! { name = benches; config = Criterion::default(); targets = bench_tpc_h_queries } criterion_main!(benches); ================================================ FILE: core/benches/write_perf_benchmark.rs ================================================ //! Write Performance Benchmarks //! //! This module contains benchmarks specifically designed to measure and identify //! write/INSERT performance bottlenecks, including: //! - Index overhead impact //! - Transaction size impact //! - Sequential vs random key patterns //! - UPDATE vs INSERT performance //! //! Run with: cargo bench --bench write_perf_benchmark #[cfg(not(feature = "codspeed"))] use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; #[cfg(not(feature = "codspeed"))] use pprof::criterion::{Output, PProfProfiler}; #[cfg(feature = "codspeed")] use codspeed_criterion_compat::{ criterion_group, criterion_main, BenchmarkId, Criterion, Throughput, }; use std::sync::Arc; use tempfile::TempDir; use turso_core::{Database, PlatformIO, StepResult}; #[cfg(not(target_family = "wasm"))] #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; /// Helper to execute a statement to completion fn run_to_completion( stmt: &mut turso_core::Statement, db: &Arc, ) -> turso_core::Result<()> { loop { match stmt.step()? { StepResult::IO => { db.io.step()?; } StepResult::Done => break, StepResult::Row => {} StepResult::Interrupt | StepResult::Busy => { panic!("Unexpected step result"); } } } Ok(()) } /// Helper to setup a limbo database with the given schema fn setup_limbo(temp_dir: &TempDir, schema: &str) -> Arc { setup_limbo_with_sync(temp_dir, schema, true) } /// Helper to setup a limbo database with optional sync fn setup_limbo_with_sync(temp_dir: &TempDir, schema: &str, sync_on: bool) -> Arc { let db_path = temp_dir.path().join("bench.db"); #[allow(clippy::arc_with_non_send_sync)] let io = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io, db_path.to_str().unwrap()).unwrap(); let conn = db.connect().unwrap(); // Set synchronous mode let sync_mode = if sync_on { "FULL" } else { "OFF" }; let mut stmt = conn .query(format!("PRAGMA synchronous = {sync_mode}")) .unwrap() .unwrap(); run_to_completion(&mut stmt, &db).unwrap(); // Execute schema let mut stmt = conn.query(schema).unwrap().unwrap(); run_to_completion(&mut stmt, &db).unwrap(); db } /// Helper to setup rusqlite with the given schema fn setup_rusqlite(temp_dir: &TempDir, schema: &str) -> rusqlite::Connection { let db_path = temp_dir.path().join("bench.db"); let conn = rusqlite::Connection::open(db_path).unwrap(); conn.pragma_update(None, "synchronous", "FULL").unwrap(); conn.pragma_update(None, "journal_mode", "WAL").unwrap(); conn.pragma_update(None, "locking_mode", "EXCLUSIVE") .unwrap(); // Use execute_batch to handle multiple statements (e.g., CREATE TABLE + CREATE INDEX) conn.execute_batch(schema).unwrap(); conn } /// Benchmark: Impact of indexes on INSERT performance /// /// This benchmark measures how adding indexes affects INSERT throughput. /// Each index adds overhead due to: /// 1. Seek operations for uniqueness checks /// 2. Additional B-tree insertions /// 3. Page splits on index pages fn bench_index_impact(criterion: &mut Criterion) { let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err() && !cfg!(feature = "codspeed"); let batch_size = 100; let mut group = criterion.benchmark_group("Index Impact on INSERT"); group.throughput(Throughput::Elements(batch_size as u64)); // Test configurations: (name, schema, insert_sql) let configs = [ ( "0_indexes", "CREATE TABLE test (id INTEGER, val1 TEXT, val2 INTEGER, val3 REAL)", "INSERT INTO test VALUES ", ), ( "1_index_pk", "CREATE TABLE test (id INTEGER PRIMARY KEY, val1 TEXT, val2 INTEGER, val3 REAL)", "INSERT INTO test VALUES ", ), ( "2_indexes", "CREATE TABLE test (id INTEGER PRIMARY KEY, val1 TEXT, val2 INTEGER, val3 REAL); \ CREATE INDEX idx_val2 ON test(val2)", "INSERT INTO test VALUES ", ), ( "3_indexes", "CREATE TABLE test (id INTEGER PRIMARY KEY, val1 TEXT, val2 INTEGER, val3 REAL); \ CREATE INDEX idx_val2 ON test(val2); \ CREATE INDEX idx_val3 ON test(val3)", "INSERT INTO test VALUES ", ), ]; for (name, schema, insert_prefix) in configs { // Build multi-row insert statement let mut values = String::from(insert_prefix); for i in 0..batch_size { if i > 0 { values.push(','); } values.push_str(&format!("({}, 'value_{}', {}, {}.5)", i, i, i * 10, i)); } // Limbo benchmark let temp_dir = tempfile::tempdir().unwrap(); let db = setup_limbo(&temp_dir, schema); let conn = db.connect().unwrap(); group.bench_function(BenchmarkId::new("limbo", name), |b| { let mut insert_stmt = conn.prepare(&values).unwrap(); let mut delete_stmt = conn.query("DELETE FROM test").unwrap().unwrap(); b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); run_to_completion(&mut insert_stmt, &db).unwrap(); total += start.elapsed(); insert_stmt.reset().unwrap(); // Clear table for next iteration (not timed) run_to_completion(&mut delete_stmt, &db).unwrap(); delete_stmt.reset().unwrap(); } total }); }); // SQLite benchmark if enable_rusqlite { let temp_dir = tempfile::tempdir().unwrap(); let sqlite_conn = setup_rusqlite(&temp_dir, schema); group.bench_function(BenchmarkId::new("sqlite", name), |b| { let mut stmt = sqlite_conn.prepare(&values).unwrap(); b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); stmt.raw_execute().unwrap(); total += start.elapsed(); // Clear table for next iteration (not timed) sqlite_conn.execute("DELETE FROM test", []).unwrap(); } total }); }); } } group.finish(); } /// Benchmark: Transaction size impact on INSERT throughput /// /// Measures how the number of rows per transaction affects throughput. /// Larger transactions amortize commit overhead but increase memory pressure. fn bench_transaction_size(criterion: &mut Criterion) { let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err() && !cfg!(feature = "codspeed"); let mut group = criterion.benchmark_group("Transaction Size Impact"); // Different transaction sizes (rows per transaction) let tx_sizes = [1, 10, 50, 100, 500, 1000]; for tx_size in tx_sizes { group.throughput(Throughput::Elements(tx_size as u64)); // Build multi-row insert let mut values = String::from("INSERT INTO test VALUES "); for i in 0..tx_size { if i > 0 { values.push(','); } values.push_str(&format!("({i}, 'data_{i}')")); } // Limbo benchmark let temp_dir = tempfile::tempdir().unwrap(); let db = setup_limbo( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", ); let conn = db.connect().unwrap(); group.bench_function(BenchmarkId::new("limbo", format!("{tx_size}_rows")), |b| { let mut begin = conn.query("BEGIN").unwrap().unwrap(); let mut commit = conn.query("COMMIT").unwrap().unwrap(); let mut insert_stmt = conn.prepare(&values).unwrap(); let mut delete_stmt = conn.query("DELETE FROM test").unwrap().unwrap(); b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); run_to_completion(&mut begin, &db).unwrap(); begin.reset().unwrap(); run_to_completion(&mut insert_stmt, &db).unwrap(); insert_stmt.reset().unwrap(); run_to_completion(&mut commit, &db).unwrap(); commit.reset().unwrap(); total += start.elapsed(); // Clear for next iteration (not timed) run_to_completion(&mut delete_stmt, &db).unwrap(); delete_stmt.reset().unwrap(); } total }); }); // SQLite benchmark if enable_rusqlite { let temp_dir = tempfile::tempdir().unwrap(); let sqlite_conn = setup_rusqlite( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", ); group.bench_function(BenchmarkId::new("sqlite", format!("{tx_size}_rows")), |b| { b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); sqlite_conn.execute("BEGIN", []).unwrap(); sqlite_conn.execute(&values, []).unwrap(); sqlite_conn.execute("COMMIT", []).unwrap(); total += start.elapsed(); // Clear for next iteration (not timed) sqlite_conn.execute("DELETE FROM test", []).unwrap(); } total }); }); } } group.finish(); } /// Benchmark: Sequential vs Random key insertion patterns /// /// Sequential keys (monotonically increasing) are typically faster because: /// 1. Balance quick path can be used (appending to rightmost leaf) /// 2. Better cache locality /// 3. Fewer page splits fn bench_key_pattern(criterion: &mut Criterion) { let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err() && !cfg!(feature = "codspeed"); let batch_size = 100; let mut group = criterion.benchmark_group("Key Pattern Impact"); group.throughput(Throughput::Elements(batch_size as u64)); // Generate random keys (pre-computed for consistency) let random_keys: Vec = (0..batch_size) .map(|i| { // Simple LCG for reproducible "random" keys let mut x = (i as i64 * 1103515245 + 12345) % (1 << 31); x = x.abs(); x }) .collect(); // Sequential insert let mut seq_values = String::from("INSERT INTO test VALUES "); for i in 0..batch_size { if i > 0 { seq_values.push(','); } seq_values.push_str(&format!("({i}, 'data_{i}')")); } // Random insert let mut rand_values = String::from("INSERT INTO test VALUES "); for (i, key) in random_keys.iter().enumerate() { if i > 0 { rand_values.push(','); } rand_values.push_str(&format!("({key}, 'data_{i}')")); } // Limbo sequential let temp_dir = tempfile::tempdir().unwrap(); let db = setup_limbo( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", ); let conn = db.connect().unwrap(); group.bench_function(BenchmarkId::new("limbo", "sequential_keys"), |b| { let mut stmt = conn.prepare(&seq_values).unwrap(); let mut delete_stmt = conn.query("DELETE FROM test").unwrap().unwrap(); b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); run_to_completion(&mut stmt, &db).unwrap(); total += start.elapsed(); stmt.reset().unwrap(); run_to_completion(&mut delete_stmt, &db).unwrap(); delete_stmt.reset().unwrap(); } total }); }); // Limbo random let temp_dir = tempfile::tempdir().unwrap(); let db = setup_limbo( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", ); let conn = db.connect().unwrap(); group.bench_function(BenchmarkId::new("limbo", "random_keys"), |b| { let mut stmt = conn.prepare(&rand_values).unwrap(); let mut delete_stmt = conn.query("DELETE FROM test").unwrap().unwrap(); b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); run_to_completion(&mut stmt, &db).unwrap(); total += start.elapsed(); stmt.reset().unwrap(); run_to_completion(&mut delete_stmt, &db).unwrap(); delete_stmt.reset().unwrap(); } total }); }); if enable_rusqlite { // SQLite sequential let temp_dir = tempfile::tempdir().unwrap(); let sqlite_conn = setup_rusqlite( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", ); group.bench_function(BenchmarkId::new("sqlite", "sequential_keys"), |b| { let mut stmt = sqlite_conn.prepare(&seq_values).unwrap(); b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); stmt.raw_execute().unwrap(); total += start.elapsed(); sqlite_conn.execute("DELETE FROM test", []).unwrap(); } total }); }); // SQLite random let temp_dir = tempfile::tempdir().unwrap(); let sqlite_conn = setup_rusqlite( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", ); group.bench_function(BenchmarkId::new("sqlite", "random_keys"), |b| { let mut stmt = sqlite_conn.prepare(&rand_values).unwrap(); b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); stmt.raw_execute().unwrap(); total += start.elapsed(); sqlite_conn.execute("DELETE FROM test", []).unwrap(); } total }); }); } group.finish(); } /// Benchmark: UPDATE vs INSERT performance /// /// Updates may have different performance characteristics due to: /// 1. Required seek to find existing row /// 2. Potential in-place update vs delete+insert /// 3. Index maintenance on modified columns fn bench_update_performance(criterion: &mut Criterion) { let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err() && !cfg!(feature = "codspeed"); let mut group = criterion.benchmark_group("UPDATE Performance"); // Pre-populate table and measure update throughput let row_count = 1000; let update_count = 100; group.throughput(Throughput::Elements(update_count as u64)); // Build initial data insert let mut initial_insert = String::from("INSERT INTO test VALUES "); for i in 0..row_count { if i > 0 { initial_insert.push(','); } initial_insert.push_str(&format!("({i}, 'initial_{i}')")); } // Build batch update (updates middle rows) let mut batch_update = String::new(); let start = row_count / 2 - update_count / 2; for i in 0..update_count { if i > 0 { batch_update.push_str("; "); } batch_update.push_str(&format!( "UPDATE test SET data = 'updated_{}' WHERE id = {}", i, start + i )); } // Limbo benchmark let temp_dir = tempfile::tempdir().unwrap(); let db = setup_limbo( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", ); let conn = db.connect().unwrap(); // Insert initial data let mut stmt = conn.prepare(&initial_insert).unwrap(); run_to_completion(&mut stmt, &db).unwrap(); group.bench_function(BenchmarkId::new("limbo", "batch_update"), |b| { let mut stmt = conn.prepare(&batch_update).unwrap(); b.iter(|| { run_to_completion(&mut stmt, &db).unwrap(); stmt.reset().unwrap(); }); }); // SQLite benchmark if enable_rusqlite { let temp_dir = tempfile::tempdir().unwrap(); let sqlite_conn = setup_rusqlite( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", ); sqlite_conn.execute(&initial_insert, []).unwrap(); group.bench_function(BenchmarkId::new("sqlite", "batch_update"), |b| { b.iter(|| { sqlite_conn.execute_batch(&batch_update).unwrap(); }); }); } group.finish(); } /// Benchmark: DELETE performance /// /// Measures DELETE throughput with different patterns fn bench_delete_performance(criterion: &mut Criterion) { let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err() && !cfg!(feature = "codspeed"); let mut group = criterion.benchmark_group("DELETE Performance"); let row_count = 1000; let delete_count = 100; group.throughput(Throughput::Elements(delete_count as u64)); // Build initial data insert let mut initial_insert = String::from("INSERT INTO test VALUES "); for i in 0..row_count { if i > 0 { initial_insert.push(','); } initial_insert.push_str(&format!("({i}, 'data_{i}')")); } // Build range delete let start = row_count / 2 - delete_count / 2; let end = start + delete_count; let range_delete = format!("DELETE FROM test WHERE id >= {start} AND id < {end}"); // Limbo benchmark let temp_dir = tempfile::tempdir().unwrap(); let db = setup_limbo( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", ); let conn = db.connect().unwrap(); group.bench_function(BenchmarkId::new("limbo", "range_delete"), |b| { b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { // Re-insert data let mut stmt = conn.prepare(&initial_insert).unwrap(); run_to_completion(&mut stmt, &db).unwrap(); // Time only the delete let start = std::time::Instant::now(); let mut stmt = conn.prepare(&range_delete).unwrap(); run_to_completion(&mut stmt, &db).unwrap(); total += start.elapsed(); // Clean up for next iteration let mut stmt = conn.query("DELETE FROM test").unwrap().unwrap(); run_to_completion(&mut stmt, &db).unwrap(); } total }); }); // SQLite benchmark if enable_rusqlite { let temp_dir = tempfile::tempdir().unwrap(); let sqlite_conn = setup_rusqlite( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", ); group.bench_function(BenchmarkId::new("sqlite", "range_delete"), |b| { b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { // Re-insert data sqlite_conn.execute(&initial_insert, []).unwrap(); // Time only the delete let start = std::time::Instant::now(); sqlite_conn.execute(&range_delete, []).unwrap(); total += start.elapsed(); // Clean up for next iteration sqlite_conn.execute("DELETE FROM test", []).unwrap(); } total }); }); } group.finish(); } /// Benchmark: Large transaction commit (many dirty pages) /// /// Specifically targets the commit_dirty_pages path with many pages fn bench_large_transaction_commit(criterion: &mut Criterion) { let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err() && !cfg!(feature = "codspeed"); let mut group = criterion.benchmark_group("Large Transaction Commit"); // Insert enough rows to dirty many pages (assuming 4KB pages, ~100 rows per page for small rows) let row_counts = [100, 1000, 5000, 10000]; for row_count in row_counts { group.throughput(Throughput::Elements(row_count as u64)); // Build large insert let mut values = String::from("INSERT INTO test VALUES "); for i in 0..row_count { if i > 0 { values.push(','); } // Use larger row size to dirty more pages values.push_str(&format!( "({}, '{}', {})", i, "x".repeat(100), // 100 byte string per row i * 10 )); } // Limbo benchmark let temp_dir = tempfile::tempdir().unwrap(); let db = setup_limbo( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT, val INTEGER)", ); let conn = db.connect().unwrap(); group.bench_function( BenchmarkId::new("limbo", format!("{row_count}_rows")), |b| { b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { // BEGIN let mut stmt = conn.query("BEGIN").unwrap().unwrap(); run_to_completion(&mut stmt, &db).unwrap(); // INSERT (not timed separately - we want full transaction) let start = std::time::Instant::now(); let mut stmt = conn.prepare(&values).unwrap(); run_to_completion(&mut stmt, &db).unwrap(); // COMMIT let mut stmt = conn.query("COMMIT").unwrap().unwrap(); run_to_completion(&mut stmt, &db).unwrap(); total += start.elapsed(); // Clean up let mut stmt = conn.query("DELETE FROM test").unwrap().unwrap(); run_to_completion(&mut stmt, &db).unwrap(); } total }); }, ); // SQLite benchmark if enable_rusqlite { let temp_dir = tempfile::tempdir().unwrap(); let sqlite_conn = setup_rusqlite( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT, val INTEGER)", ); group.bench_function( BenchmarkId::new("sqlite", format!("{row_count}_rows")), |b| { b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { sqlite_conn.execute("BEGIN", []).unwrap(); let start = std::time::Instant::now(); sqlite_conn.execute(&values, []).unwrap(); sqlite_conn.execute("COMMIT", []).unwrap(); total += start.elapsed(); sqlite_conn.execute("DELETE FROM test", []).unwrap(); } total }); }, ); } } group.finish(); } /// Benchmark: Fsync overhead isolation /// /// Compares INSERT performance with sync=FULL vs sync=OFF to isolate fsync cost fn bench_fsync_overhead(criterion: &mut Criterion) { let enable_rusqlite = std::env::var("DISABLE_RUSQLITE_BENCHMARK").is_err() && !cfg!(feature = "codspeed"); let batch_size = 100; let mut group = criterion.benchmark_group("Fsync Overhead"); group.throughput(Throughput::Elements(batch_size as u64)); // Build insert statement let mut values = String::from("INSERT INTO test VALUES "); for i in 0..batch_size { if i > 0 { values.push(','); } values.push_str(&format!("({i}, 'data_{i}')")); } // Limbo with sync=FULL let temp_dir = tempfile::tempdir().unwrap(); let db = setup_limbo_with_sync( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", true, ); let conn = db.connect().unwrap(); group.bench_function(BenchmarkId::new("limbo", "sync_FULL"), |b| { let mut insert_stmt = conn.prepare(&values).unwrap(); let mut delete_stmt = conn.query("DELETE FROM test").unwrap().unwrap(); b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); run_to_completion(&mut insert_stmt, &db).unwrap(); total += start.elapsed(); insert_stmt.reset().unwrap(); run_to_completion(&mut delete_stmt, &db).unwrap(); delete_stmt.reset().unwrap(); } total }); }); // Limbo with sync=OFF let temp_dir = tempfile::tempdir().unwrap(); let db = setup_limbo_with_sync( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", false, ); let conn = db.connect().unwrap(); group.bench_function(BenchmarkId::new("limbo", "sync_OFF"), |b| { let mut insert_stmt = conn.prepare(&values).unwrap(); let mut delete_stmt = conn.query("DELETE FROM test").unwrap().unwrap(); b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); run_to_completion(&mut insert_stmt, &db).unwrap(); total += start.elapsed(); insert_stmt.reset().unwrap(); run_to_completion(&mut delete_stmt, &db).unwrap(); delete_stmt.reset().unwrap(); } total }); }); if enable_rusqlite { // SQLite with sync=FULL let temp_dir = tempfile::tempdir().unwrap(); let sqlite_conn = setup_rusqlite( &temp_dir, "CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", ); group.bench_function(BenchmarkId::new("sqlite", "sync_FULL"), |b| { let mut stmt = sqlite_conn.prepare(&values).unwrap(); b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); stmt.raw_execute().unwrap(); total += start.elapsed(); sqlite_conn.execute("DELETE FROM test", []).unwrap(); } total }); }); // SQLite with sync=OFF let temp_dir = tempfile::tempdir().unwrap(); let db_path = temp_dir.path().join("bench.db"); let sqlite_conn = rusqlite::Connection::open(db_path).unwrap(); sqlite_conn .pragma_update(None, "synchronous", "OFF") .unwrap(); sqlite_conn .pragma_update(None, "journal_mode", "WAL") .unwrap(); sqlite_conn .pragma_update(None, "locking_mode", "EXCLUSIVE") .unwrap(); sqlite_conn .execute("CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)", []) .unwrap(); group.bench_function(BenchmarkId::new("sqlite", "sync_OFF"), |b| { let mut stmt = sqlite_conn.prepare(&values).unwrap(); b.iter_custom(|iters| { let mut total = std::time::Duration::ZERO; for _ in 0..iters { let start = std::time::Instant::now(); stmt.raw_execute().unwrap(); total += start.elapsed(); sqlite_conn.execute("DELETE FROM test", []).unwrap(); } total }); }); } group.finish(); } #[cfg(not(feature = "codspeed"))] criterion_group! { name = write_perf_benches; config = Criterion::default() .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))) .sample_size(50); targets = bench_index_impact, bench_transaction_size, bench_key_pattern, bench_update_performance, bench_delete_performance, bench_large_transaction_commit, bench_fsync_overhead } #[cfg(feature = "codspeed")] criterion_group! { name = write_perf_benches; config = Criterion::default().sample_size(50); targets = bench_index_impact, bench_transaction_size, bench_key_pattern, bench_update_performance, bench_delete_performance, bench_large_transaction_commit, bench_fsync_overhead } criterion_main!(write_perf_benches); ================================================ FILE: core/btree_dump.rs ================================================ use crate::schema::Schema; use crate::storage::btree::BTreeCursor; use crate::storage::btree::CursorTrait; use crate::storage::pager::Pager; use crate::sync::Arc; use crate::sync::RwLock; use crate::util::IOExt; use crate::vtab::{InternalVirtualTable, InternalVirtualTableCursor}; use crate::{Connection, Result, Value}; use turso_ext::{ ConstraintInfo, ConstraintOp, ConstraintUsage, IndexInfo, OrderByInfo, ResultCode, }; #[derive(Debug)] pub struct BtreeDumpTable; impl Default for BtreeDumpTable { fn default() -> Self { Self::new() } } impl BtreeDumpTable { pub fn new() -> Self { Self } } impl InternalVirtualTable for BtreeDumpTable { fn name(&self) -> String { "btree_dump".to_string() } fn sql(&self) -> String { "CREATE TABLE btree_dump(record BLOB, name TEXT HIDDEN)".to_string() } fn open(&self, conn: Arc) -> Result>> { let pager = conn.get_pager(); let schema = conn.schema.read().clone(); let cursor = BtreeDumpCursor::new(pager, schema); Ok(Arc::new(RwLock::new(cursor))) } fn best_index( &self, constraints: &[ConstraintInfo], _order_by: &[OrderByInfo], ) -> std::result::Result { let mut usages = vec![ ConstraintUsage { argv_index: None, omit: false, }; constraints.len() ]; let mut name_idx: Option = None; for (i, c) in constraints.iter().enumerate() { if !c.usable || c.op != ConstraintOp::Eq { continue; } // column 1 is the hidden `name` column if c.column_index == 1 { name_idx = Some(i); } } if let Some(idx) = name_idx { usages[idx] = ConstraintUsage { argv_index: Some(1), omit: true, }; } let (cost, rows) = if name_idx.is_some() { (1.0, 100) } else { (f64::MAX, 100) }; Ok(IndexInfo { idx_num: if name_idx.is_some() { 1 } else { 0 }, idx_str: None, order_by_consumed: false, estimated_cost: cost, estimated_rows: rows, constraint_usages: usages, }) } } pub struct BtreeDumpCursor { pager: Arc, schema: Arc, cursor: Option, current_record: Option>, row_idx: i64, } impl BtreeDumpCursor { fn new(pager: Arc, schema: Arc) -> Self { Self { pager, schema, cursor: None, current_record: None, row_idx: 0, } } /// Look up the root page for a given name (index first, then table). fn find_root_page(&self, name: &str) -> Option { // Search indexes first (indexes are stored by table name, so iterate all) for indexes in self.schema.indexes.values() { for index in indexes { if index.name.eq_ignore_ascii_case(name) { return Some(index.root_page); } } } // Then search tables if let Some(table) = self.schema.get_table(name) { return table.get_root_page().ok(); } None } fn read_current_record(&mut self) -> Result<()> { self.current_record = None; if let Some(ref mut cursor) = self.cursor { let payload = self.pager.io.block(|| { let record = cursor.record()?; match record { crate::types::IOResult::Done(Some(rec)) => Ok(crate::types::IOResult::Done( Some(rec.get_payload().to_vec()), )), crate::types::IOResult::Done(None) => Ok(crate::types::IOResult::Done(None)), crate::types::IOResult::IO(io) => Ok(crate::types::IOResult::IO(io)), } })?; self.current_record = payload; } Ok(()) } } impl InternalVirtualTableCursor for BtreeDumpCursor { fn filter(&mut self, args: &[Value], _idx_str: Option, idx_num: i32) -> Result { self.cursor = None; self.current_record = None; self.row_idx = 0; if idx_num != 1 { // No name constraint provided — return no rows return Ok(false); } let name = match args.first() { Some(Value::Text(s)) => s.as_str(), _ => return Ok(false), }; let root_page = match self.find_root_page(name) { Some(rp) if rp > 0 => rp, Some(_) => { return Err(crate::LimboError::InternalError(format!( "btree_dump: '{name}' has no physical btree (MVCC non-checkpointed)", ))); } None => { return Err(crate::LimboError::InternalError(format!( "btree_dump: no such table or index: '{name}'", ))); } }; let mut btree_cursor = BTreeCursor::new(self.pager.clone(), root_page, 0); self.pager.io.block(|| btree_cursor.rewind())?; self.cursor = Some(btree_cursor); self.read_current_record()?; Ok(self.current_record.is_some()) } fn next(&mut self) -> Result { if let Some(ref mut cursor) = self.cursor { self.pager.io.block(|| cursor.next())?; self.row_idx += 1; self.read_current_record()?; Ok(self.current_record.is_some()) } else { Ok(false) } } fn column(&self, column: usize) -> Result { match column { 0 => match &self.current_record { Some(data) => Ok(Value::from_blob(data.clone())), None => Ok(Value::Null), }, _ => Ok(Value::Null), } } fn rowid(&self) -> i64 { self.row_idx } } ================================================ FILE: core/build.rs ================================================ use chrono::{TimeZone, Utc}; use std::path::PathBuf; use std::process::Command; use std::{env, fs}; fn main() { // Ensure Cargo reruns when this script or the reproducibility seed changes. println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-env-changed=SOURCE_DATE_EPOCH"); // Tell cargo to rebuild when git HEAD changes, so sqlite_source_id() stays current. // We use `git rev-parse --git-dir` instead of hardcoding ".git" to support worktrees, // where the git directory lives elsewhere (e.g., ../.git/worktrees/my-worktree). // Silently ignored if git unavailable (e.g., building from tarball). // Resolve git dir dynamically to support worktrees. let git_dir = run_git(&["rev-parse", "--git-dir"]).map(PathBuf::from); if let Some(git_dir) = git_dir.as_ref() { // Common dir holds refs for worktrees; fall back to git_dir if unavailable. let git_common_dir = run_git(&["rev-parse", "--git-common-dir"]).map(PathBuf::from); let head_path = git_dir.join("HEAD"); // HEAD changes on checkout/switch println!("cargo::rerun-if-changed={}", head_path.display()); // The ref file (e.g., refs/heads/main) changes on commit if let Ok(head_content) = fs::read_to_string(&head_path) { if let Some(ref_path) = head_content.strip_prefix("ref: ") { let ref_base = git_common_dir.as_deref().unwrap_or(git_dir.as_path()); let ref_path = ref_base.join(ref_path.trim()); println!("cargo::rerun-if-changed={}", ref_path.display()); if !ref_path.exists() { let packed_refs = ref_base.join("packed-refs"); println!("cargo::rerun-if-changed={}", packed_refs.display()); } } } } let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); // Write to a temp file first, then only update built.rs if contents changed. let built_file = out_dir.join("built.rs"); let temp_file = out_dir.join("built.rs.tmp"); // We shell out to git instead of using libgit2 (via the `built` crate's git2 feature) // because libgit2-sys adds ~18s to clean release builds. The git CLI is always available // in dev environments and CI. Falls back to None if git unavailable. // Commit hash is used for sqlite_source_id() and to derive a stable timestamp. let git_hash = run_git(&["rev-parse", "HEAD"]); let git_commit_epoch = run_git(&["show", "-s", "--format=%ct", "HEAD"]) .and_then(|epoch| epoch.parse::().ok()); let git_hash_code = match git_hash { Some(hash) => format!("pub const GIT_COMMIT_HASH: Option<&str> = Some(\"{hash}\");"), None => "pub const GIT_COMMIT_HASH: Option<&str> = None;".to_string(), }; // Honor reproducible-builds if set; otherwise seed it from git commit time. let source_date_epoch = env::var("SOURCE_DATE_EPOCH") .ok() .and_then(|epoch| epoch.parse::().ok()); if source_date_epoch.is_none() { if let Some(epoch) = git_commit_epoch { env::set_var("SOURCE_DATE_EPOCH", epoch.to_string()); } } // Pre-format the timestamp so sqlite_source_id() doesn't need chrono at runtime. // Prefer SOURCE_DATE_EPOCH (reproducible builds), then git commit time, and fall back to now. let sqlite_date = source_date_epoch .or(git_commit_epoch) .and_then(|epoch| Utc.timestamp_opt(epoch, 0).single()) .unwrap_or_else(Utc::now) .format("%Y-%m-%d %H:%M:%S") .to_string(); // Generate built metadata and append our extra constants. built::write_built_file_with_opts(&temp_file) .expect("Failed to acquire build-time information"); let built_contents = fs::read_to_string(&temp_file).expect("Failed to read built metadata"); let new_contents = format!( "{built_contents}\npub const BUILT_TIME_SQLITE: &str = \"{sqlite_date}\";\n{git_hash_code}\n" ); // Avoid touching built.rs when content is unchanged to prevent rebuild loops. let existing_contents = fs::read_to_string(&built_file).ok(); if existing_contents.as_deref() != Some(new_contents.as_str()) { fs::write(&built_file, new_contents).expect("Failed to write built file"); } let _ = fs::remove_file(&temp_file); } fn run_git(args: &[&str]) -> Option { let output = Command::new("git").args(args).output().ok()?; if !output.status.success() { return None; } let stdout = String::from_utf8(output.stdout).ok()?; let trimmed = stdout.trim(); if trimmed.is_empty() { None } else { Some(trimmed.to_string()) } } ================================================ FILE: core/busy.rs ================================================ use crate::MonotonicInstant; use std::time::Duration; /// Type alias for busy handler callback function. /// /// The callback receives: /// - `count`: The number of times the busy handler has been invoked for the same locking event /// /// Returns: /// - `0` to stop retrying and return SQLITE_BUSY to the application. /// - Non-zero to retry the database access. /// /// # Safety Notes (per SQLite spec) /// - The callback MUST NOT modify the database connection that invoked it. /// - The callback MUST NOT close the connection or any prepared statement. /// - The callback is NOT reentrant. pub type BusyHandlerCallback = Box i32 + Send + Sync>; #[derive(Default)] /// Represents the busy handler configuration for a connection. pub enum BusyHandler { #[default] /// No busy handler: return SQLITE_BUSY immediately on lock contention. None, /// Default timeout-based handler (implements sqliteDefaultBusyCallback) /// The duration is the maximum total time to wait before giving up Timeout(Duration), /// Custom user-defined callback handler Custom { callback: BusyHandlerCallback }, } impl std::fmt::Debug for BusyHandler { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { BusyHandler::None => write!(f, "BusyHandler::None"), BusyHandler::Timeout(d) => write!(f, "BusyHandler::Timeout({d:?}"), BusyHandler::Custom { .. } => write!(f, "BusyHandler::Custom"), } } } /// Tracks the state of busy handler invocations for a statement. /// /// This implements a yield-based busy handling mechanism that integrates with /// the async event loop. Instead of blocking with `thread::sleep`, the statement /// yields back to the caller with `StepResult::IO` and a timeout. When `step()` /// is called again after the timeout has passed, it retries the operation. /// /// Uses increasing delays. After 12 iterations, continues with 100ms delays until max duration is reached. #[derive(Debug)] pub struct BusyHandlerState { /// Number of times the busy handler has been invoked for this locking event invocation_count: i32, /// For timeout-based handlers: the next timeout instant to wait until timeout: MonotonicInstant, /// For timeout-based handlers: the current iteration index into DELAYS iteration: usize, } impl BusyHandlerState { /// Delay schedule for timeout-based busy handler (sqliteDefaultBusyCallback) const DELAYS: [Duration; 12] = [ Duration::from_millis(1), Duration::from_millis(2), Duration::from_millis(5), Duration::from_millis(10), Duration::from_millis(15), Duration::from_millis(20), Duration::from_millis(25), Duration::from_millis(25), Duration::from_millis(25), Duration::from_millis(50), Duration::from_millis(50), Duration::from_millis(100), ]; /// Cumulative totals for each iteration (for calculating remaining time) const TOTALS: [Duration; 12] = [ Duration::from_millis(0), Duration::from_millis(1), Duration::from_millis(3), Duration::from_millis(8), Duration::from_millis(18), Duration::from_millis(33), Duration::from_millis(53), Duration::from_millis(78), Duration::from_millis(103), Duration::from_millis(128), Duration::from_millis(178), Duration::from_millis(228), ]; /// Create a new busy handler state pub fn new(now: MonotonicInstant) -> Self { Self { invocation_count: 0, timeout: now, iteration: 0, } } /// Reset the state for a new locking event pub fn reset(&mut self, now: MonotonicInstant) { self.invocation_count = 0; self.timeout = now; self.iteration = 0; } /// Get the current timeout instant pub fn timeout(&self) -> MonotonicInstant { self.timeout } /// Invoke the busy handler and determine whether to retry. /// /// Returns `true` if the operation should be retried, `false` if SQLITE_BUSY /// should be returned to the application. /// /// For timeout-based handlers, this also updates the internal timeout instant. /// For custom handlers, this invokes the callback and respects its return value. pub fn invoke(&mut self, handler: &BusyHandler, now: MonotonicInstant) -> bool { match handler { BusyHandler::None => { // No handler: return BUSY immediately false } BusyHandler::Timeout(max_duration) => self.invoke_timeout_handler(*max_duration, now), BusyHandler::Custom { callback } => { let result = callback(self.invocation_count); self.invocation_count += 1; if result != 0 { // Retry with a small delay self.timeout = now + Duration::from_millis(1); true } else { false } } } } /// Implements sqliteDefaultBusyCallback logic for timeout-based handling. /// /// This uses an exponentially increasing delay schedule, capped at 100ms per iteration. fn invoke_timeout_handler(&mut self, max_duration: Duration, now: MonotonicInstant) -> bool { let idx = self.iteration.min(11); let mut delay = Self::DELAYS[idx]; let mut prior = Self::TOTALS[idx]; // After 12 iterations, each additional iteration adds 100ms if self.iteration >= 12 { prior += delay * (self.iteration as u32 - 11); } // Check if we've exceeded or would exceed the max duration if prior + delay > max_duration { delay = max_duration.saturating_sub(prior); if delay.is_zero() { return false; } } self.iteration = self.iteration.saturating_add(1); self.invocation_count += 1; self.timeout = now + delay; true } /// Get the delay duration that should be waited before the next retry. /// /// This returns the duration between `now` and the timeout instant. /// Returns `Duration::ZERO` if the timeout has already passed. pub fn get_delay(&self, now: MonotonicInstant) -> Duration { if now >= self.timeout { Duration::ZERO } else { self.timeout.duration_since(now) } } } #[cfg(test)] mod tests { use super::*; fn test_instant() -> MonotonicInstant { MonotonicInstant::now() } #[test] fn test_busy_handler_timeout_basic() { let handler = BusyHandler::Timeout(Duration::from_millis(100)); let now = test_instant(); let mut state = BusyHandlerState::new(now); // First invocation should return true (retry) assert!(state.invoke(&handler, now)); // Timeout should be set to 1ms from now assert_eq!(state.timeout(), now + Duration::from_millis(1)); } #[test] fn test_busy_handler_timeout_exhausted() { let handler = BusyHandler::Timeout(Duration::from_millis(0)); let now = test_instant(); let mut state = BusyHandlerState::new(now); // Zero timeout should return false immediately assert!(!state.invoke(&handler, now)); } #[test] fn test_busy_handler_custom_callback() { // Callback that retries 3 times then gives up let callback: BusyHandlerCallback = Box::new(|count| if count < 3 { 1 } else { 0 }); let handler = BusyHandler::Custom { callback }; let now = test_instant(); let mut state = BusyHandlerState::new(now); // First 3 invocations should retry assert!(state.invoke(&handler, now)); assert!(state.invoke(&handler, now)); assert!(state.invoke(&handler, now)); // 4th invocation should return false assert!(!state.invoke(&handler, now)); } #[test] fn test_busy_handler_none_returns_false_immediately() { let handler = BusyHandler::None; let now = test_instant(); let mut state = BusyHandlerState::new(now); // None handler should always return false (don't retry) assert!(!state.invoke(&handler, now)); // Even on subsequent invocations assert!(!state.invoke(&handler, now)); } #[test] fn test_custom_callback_receives_correct_count() { use std::sync::{Arc, Mutex}; // Track the counts passed to callback (using Arc+Mutex for Send+Sync) let counts = Arc::new(Mutex::new(Vec::new())); let counts_clone = counts.clone(); let callback: BusyHandlerCallback = Box::new(move |count| { counts_clone.lock().unwrap().push(count); if count < 5 { 1 } else { 0 } }); let handler = BusyHandler::Custom { callback }; let now = test_instant(); let mut state = BusyHandlerState::new(now); // Invoke 6 times for _ in 0..6 { state.invoke(&handler, now); } // Verify counts were 0, 1, 2, 3, 4, 5 assert_eq!(*counts.lock().unwrap(), vec![0, 1, 2, 3, 4, 5]); } #[test] fn test_custom_callback_always_retry() { // Callback that always retries let callback: BusyHandlerCallback = Box::new(|_| 1); let handler = BusyHandler::Custom { callback }; let now = test_instant(); let mut state = BusyHandlerState::new(now); // Should always return true for _ in 0..100 { assert!(state.invoke(&handler, now)); } } #[test] fn test_custom_callback_never_retry() { // Callback that never retries let callback: BusyHandlerCallback = Box::new(|_| 0); let handler = BusyHandler::Custom { callback }; let now = test_instant(); let mut state = BusyHandlerState::new(now); // First invocation should return false assert!(!state.invoke(&handler, now)); } #[test] fn test_custom_callback_sets_timeout() { let callback: BusyHandlerCallback = Box::new(|_| 1); let handler = BusyHandler::Custom { callback }; let now = test_instant(); let mut state = BusyHandlerState::new(now); assert!(state.invoke(&handler, now)); // Custom callback sets 1ms timeout assert_eq!(state.timeout(), now + Duration::from_millis(1)); } #[test] fn test_timeout_delay_schedule() { let handler = BusyHandler::Timeout(Duration::from_secs(10)); // Long timeout let now = test_instant(); let mut state = BusyHandlerState::new(now); // Expected delays per iteration: 1, 2, 5, 10, 15, 20, 25, 25, 25, 50, 50, 100ms // The timeout is set to `now + delay` each time, so we check individual delays let expected_delays_ms: [u64; 12] = [1, 2, 5, 10, 15, 20, 25, 25, 25, 50, 50, 100]; for (i, expected_ms) in expected_delays_ms.iter().enumerate() { assert!(state.invoke(&handler, now), "iteration {i} should retry"); let timeout = state.timeout(); assert_eq!( timeout, now + Duration::from_millis(*expected_ms), "iteration {i} should have delay of {expected_ms}ms" ); } } #[test] fn test_timeout_caps_at_max_duration() { // 5ms timeout - should only allow a few iterations let handler = BusyHandler::Timeout(Duration::from_millis(5)); let now = test_instant(); let mut state = BusyHandlerState::new(now); // First iteration: 1ms delay (total: 1ms) assert!(state.invoke(&handler, now)); // Second iteration: 2ms delay (total: 3ms) assert!(state.invoke(&handler, now)); // Third iteration: would be 5ms but only 2ms left (total would be 8ms > 5ms) // So delay is capped to 2ms assert!(state.invoke(&handler, now)); // Fourth iteration: no time left assert!(!state.invoke(&handler, now)); } #[test] fn test_state_reset() { let handler = BusyHandler::Timeout(Duration::from_millis(100)); let now = test_instant(); let mut state = BusyHandlerState::new(now); // Invoke a few times state.invoke(&handler, now); state.invoke(&handler, now); state.invoke(&handler, now); // Reset let later = MonotonicInstant::now(); state.reset(later); // Should be back to initial state assert_eq!(state.timeout(), later); assert!(state.invoke(&handler, later)); // First delay after reset should be 1ms assert_eq!(state.timeout(), later + Duration::from_millis(1)); } #[test] fn test_get_delay_when_timeout_passed() { let now = MonotonicInstant::now(); let state = BusyHandlerState::new(now); // Timeout is at `now`, so any time >= now should return zero delay assert_eq!(state.get_delay(now), Duration::ZERO); // A later time should also return zero std::thread::sleep(Duration::from_micros(10)); let later = MonotonicInstant::now(); assert_eq!(state.get_delay(later), Duration::ZERO); } #[test] fn test_get_delay_calculates_remaining_time() { let now = MonotonicInstant::now(); let mut state = BusyHandlerState::new(now); let handler = BusyHandler::Timeout(Duration::from_millis(100)); state.invoke(&handler, now); // Sets timeout to now + 1ms // Check delay from `now` - should be 1ms let delay = state.get_delay(now); assert_eq!(delay, Duration::from_millis(1)); } } ================================================ FILE: core/connection.rs ================================================ use crate::error::io_error; use crate::storage::journal_mode; use crate::sync::{ atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicIsize, AtomicU16, AtomicU64, Ordering}, Arc, RwLock, }; use crate::turso_assert; #[cfg(all(feature = "fs", feature = "conn_raw_api"))] use crate::types::{WalFrameInfo, WalState}; #[cfg(feature = "fs")] use crate::util::{OpenMode, OpenOptions}; #[cfg(all(feature = "fs", feature = "conn_raw_api"))] use crate::Page; use crate::MAIN_DB_ID; use crate::{ ast, function, io::{MemoryIO, IO}, parse_schema_rows, refresh_analyze_stats, translate, util::IOExt, vdbe, AllViewsTxState, AtomicCipherMode, AtomicSyncMode, AtomicTempStore, BusyHandler, BusyHandlerCallback, CaptureDataChangesInfo, CheckpointMode, CheckpointResult, CipherMode, Cmd, Completion, ConnectionMetrics, Database, DatabaseCatalog, DatabaseOpts, Duration, EncryptionKey, EncryptionOpts, IndexMethod, LimboError, MvStore, OpenFlags, PageSize, Pager, Parser, QueryMode, QueryRunner, Result, Schema, Statement, SyncMode, TransactionMode, Trigger, Value, VirtualTable, }; use arc_swap::ArcSwap; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use smallvec::SmallVec; use std::fmt::Display; use std::ops::Deref; use tracing::{instrument, Level}; use turso_macros::AtomicEnum; #[derive(Clone, AtomicEnum, Copy, PartialEq, Eq, Debug)] pub(crate) enum TransactionState { Write { schema_did_change: bool, }, Read, /// PendingUpgrade remembers what transaction state was before upgrade to write (has_read_txn is true if before transaction were in Read state) /// This is important, because if we failed to initialize write transaction immediatley - we need to end implicitly started read txn (e.g. for simiple INSERT INTO operation) /// But for late upgrade of transaction we should keep read transaction active (e.g. BEGIN; SELECT ...; INSERT INTO ...) PendingUpgrade { has_read_txn: bool, }, None, } /// Database connection handle. /// /// If you add a setting that affects SQL compilation or execution, call /// `bump_prepare_context_generation()` in its setter so cached prepared /// statements know they need to be reprepared. pub struct Connection { pub(crate) db: Arc, pub(crate) pager: ArcSwap, pub(crate) schema: RwLock>, /// Per-database schema cache (database_index -> schema) /// Loaded lazily to avoid copying all schemas on connection open pub(super) database_schemas: RwLock>>, /// Whether to automatically commit transaction pub(crate) auto_commit: AtomicBool, pub(super) transaction_state: AtomicTransactionState, pub(super) last_insert_rowid: AtomicI64, pub(crate) last_change: AtomicI64, pub(crate) total_changes: AtomicI64, pub(crate) syms: parking_lot::RwLock, pub(super) _shared_cache: bool, pub(super) cache_size: AtomicI32, /// page size used for an uninitialized database or the next vacuum command. /// it's not always equal to the current page size of the database pub(super) page_size: AtomicU16, /// Disable automatic checkpoint behaviour when DB is shutted down or WAL reach certain size /// Client still can manually execute PRAGMA wal_checkpoint(...) commands pub(super) wal_auto_checkpoint_disabled: AtomicBool, pub(super) capture_data_changes: RwLock>, /// CDC v2: transaction ID for grouping CDC records by transaction. /// -1 means unset (will be assigned on first CDC write in the transaction). pub(crate) cdc_transaction_id: AtomicI64, pub(super) closed: AtomicBool, /// Attached databases pub(super) attached_databases: RwLock, pub(super) query_only: AtomicBool, /// If enabled, the UPDATE/DELETE statements must have a WHERE clause pub(super) dml_require_where: AtomicBool, pub(crate) mv_tx: RwLock>, /// Per-attached-database MVCC transactions. /// Main DB uses `mv_tx` above for zero-cost hot path access. pub(crate) attached_mv_txs: RwLock>, /// Per-connection view transaction states for uncommitted changes. This represents /// one entry per view that was touched in the transaction. pub(crate) view_transaction_states: AllViewsTxState, /// Connection-level metrics aggregation pub metrics: RwLock, /// Greater than zero if connection executes a program within a program /// This is necessary in order for connection to not "finalize" transaction (commit/abort) when program ends /// (because parent program is still pending and it will handle "finalization" instead) /// /// The state is integer as we may want to spawn deep nested programs (e.g. Root -[run]-> S1 -[run]-> S2 -[run]-> ...) /// and we need to track current nestedness depth in order to properly understand when we will reach the root back again pub(super) nestedness: AtomicI32, /// Stack of currently compiling triggers to prevent recursive trigger subprogram compilation pub(super) compiling_triggers: RwLock>>, /// Stack of currently executing triggers to prevent recursive trigger execution /// Only prevents the same trigger from firing again, allowing different triggers on the same table to fire pub(super) executing_triggers: RwLock>>, pub(crate) encryption_key: RwLock>, pub(super) encryption_cipher_mode: AtomicCipherMode, pub(super) sync_mode: AtomicSyncMode, pub(super) temp_store: AtomicTempStore, pub(super) data_sync_retry: AtomicBool, /// Busy handler for lock contention /// Default is BusyHandler::None (return SQLITE_BUSY immediately) pub(super) busy_handler: RwLock, /// Whether this is an internal connection used for MVCC bootstrap pub(super) is_mvcc_bootstrap_connection: AtomicBool, /// Whether pragma foreign_keys=ON for this connection pub(super) fk_pragma: AtomicBool, pub(crate) fk_deferred_violations: AtomicIsize, /// Number of active write statements on this connection. pub(crate) n_active_writes: AtomicI32, /// Whether pragma ignore_check_constraints=ON for this connection pub(super) check_constraints_pragma: AtomicBool, /// Track when each virtual table instance is currently in transaction. pub(crate) vtab_txn_states: RwLock>, /// Generation counter bumped whenever any setting that affects PrepareContext /// changes. Allows prepared statements to cheaply detect when they need to be /// reprepared (single u64 comparison instead of rebuilding the full context). /// IMPORTANT: this is a bit of a regression landmine because the generation /// MUST be incremented whenever any setting that affects PrepareContext changes, /// and this is not currently centralized; each setter bumps the generation individually. pub(crate) prepare_context_generation: AtomicU64, } // SAFETY: This needs to be audited for thread safety. // See: https://github.com/tursodatabase/turso/issues/1552 crate::assert::assert_send_sync!(Connection); impl Drop for Connection { fn drop(&mut self) { if !self.is_closed() { // Roll back any active MVCC transactions so that MvStore entries // don't leak and block future checkpoints. The tx may have // already been committed/aborted externally (e.g. by tests that // manipulate MvStore directly), so only rollback if still active. if let Some(mv_store) = self.db.get_mv_store().as_ref() { if let Some(tx_id) = self.get_mv_tx_id() { let pager = self.pager.load(); if mv_store.is_tx_rollbackable(tx_id) { mv_store.rollback_tx(tx_id, pager.clone(), self, MAIN_DB_ID); } else { self.set_mv_tx(None); } pager.end_read_tx(); } } self.rollback_attached_mvcc_txs(false); // Release any WAL locks the connection might be holding. // This prevents deadlocks if a connection is dropped (e.g., due to a panic) // while holding a read or write lock. let pager = self.pager.load(); if let Some(wal) = &pager.wal { if wal.holds_write_lock() { wal.end_write_tx(); } if wal.holds_read_lock() { wal.end_read_tx(); } } // Also release WAL locks on all attached database pagers for attached_pager in self.get_all_attached_pagers() { if let Some(wal) = &attached_pager.wal { if wal.holds_write_lock() { wal.end_write_tx(); } if wal.holds_read_lock() { wal.end_read_tx(); } } } // if connection wasn't properly closed, decrement the connection counter self.db .n_connections .fetch_sub(1, crate::sync::atomic::Ordering::SeqCst); } } } impl Connection { /// Bump the prepare context generation counter. Must be called whenever any /// connection setting that is tracked in `PrepareContext` changes, so that /// prepared statements know they need to be reprepared. #[inline] pub(crate) fn bump_prepare_context_generation(&self) { self.prepare_context_generation .fetch_add(1, Ordering::Release); } #[inline] pub(crate) fn prepare_context_generation(&self) -> u64 { self.prepare_context_generation.load(Ordering::Acquire) } /// check if connection executes nested program (so it must not do any "finalization" work as parent program will handle it) pub fn is_nested_stmt(&self) -> bool { self.nestedness.load(Ordering::SeqCst) > 0 } /// starts nested program execution pub fn start_nested(&self) { self.nestedness.fetch_add(1, Ordering::SeqCst); } /// ends nested program execution pub fn end_nested(&self) { self.nestedness.fetch_add(-1, Ordering::SeqCst); } /// Check if a specific trigger is currently compiling (for recursive trigger prevention) pub fn trigger_is_compiling(&self, trigger: impl AsRef) -> bool { let compiling = self.compiling_triggers.read(); if let Some(trigger) = compiling.iter().find(|t| t.name == trigger.as_ref().name) { tracing::debug!("Trigger is already compiling: {}", trigger.name); return true; } false } pub fn start_trigger_compilation(&self, trigger: Arc) { tracing::debug!("Starting trigger compilation: {}", trigger.name); self.compiling_triggers.write().push(trigger); } pub fn end_trigger_compilation(&self) { tracing::debug!( "Ending trigger compilation: {:?}", self.compiling_triggers.read().last().map(|t| &t.name) ); self.compiling_triggers.write().pop(); } /// Check if a specific trigger is currently executing (for recursive trigger prevention) pub fn is_trigger_executing(&self, trigger: impl AsRef) -> bool { let executing = self.executing_triggers.read(); if let Some(trigger) = executing.iter().find(|t| t.name == trigger.as_ref().name) { tracing::debug!("Trigger is already executing: {}", trigger.name); return true; } false } pub fn start_trigger_execution(&self, trigger: Arc) { tracing::debug!("Starting trigger execution: {}", trigger.name); self.executing_triggers.write().push(trigger); } pub fn end_trigger_execution(&self) { tracing::debug!( "Ending trigger execution: {:?}", self.executing_triggers.read().last().map(|t| &t.name) ); self.executing_triggers.write().pop(); } pub fn prepare(self: &Arc, sql: impl AsRef) -> Result { self._prepare(sql) } #[instrument(skip_all, level = Level::INFO)] pub fn _prepare(self: &Arc, sql: impl AsRef) -> Result { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } if sql.as_ref().is_empty() { return Err(LimboError::InvalidArgument( "The supplied SQL string contains no statements".to_string(), )); } let sql = sql.as_ref(); tracing::debug!("Preparing: {}", sql); let mut parser = Parser::new(sql.as_bytes()); let cmd = match parser.next_cmd()? { Some(cmd) => cmd, None => { return Err(LimboError::InvalidArgument( "The supplied SQL string contains no statements".to_string(), )); } }; let syms = self.syms.read(); let byte_offset_end = parser.offset(); let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end]) .unwrap() .trim(); self.maybe_update_schema(); let pager = self.pager.load().clone(); let mode = QueryMode::new(&cmd); let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd; // Read lock + Arc::Clone the schema here to avoid a possible recursive read lock in `op_parse_schema`, // where we try to read the schema again there let schema = self.schema.read().clone(); let program = translate::translate( &schema, stmt, pager.clone(), self.clone(), &syms, mode, input, )?; Ok(Statement::new(program, pager, mode, byte_offset_end)) } /// Prepare a statement from an AST node directly, skipping SQL parsing. /// This is more efficient when AST is already available or constructed programmatically. pub fn prepare_stmt(self: &Arc, stmt: ast::Stmt) -> Result { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } self.maybe_update_schema(); let syms = self.syms.read(); let pager = self.pager.load().clone(); let mode = QueryMode::Normal; let schema = self.schema.read().clone(); let program = translate::translate( &schema, stmt, pager.clone(), self.clone(), &syms, mode, "", // No SQL input string available )?; Ok(Statement::new(program, pager, mode, 0)) } /// Whether this is an internal connection used for MVCC bootstrap pub fn is_mvcc_bootstrap_connection(&self) -> bool { self.is_mvcc_bootstrap_connection.load(Ordering::SeqCst) } /// Promote MVCC bootstrap connection to a regular connection so it reads from the MV store again. pub fn promote_to_regular_connection(&self) { assert!(self.is_mvcc_bootstrap_connection.load(Ordering::SeqCst)); self.is_mvcc_bootstrap_connection .store(false, Ordering::SeqCst); } /// Demote regular connection to MVCC bootstrap connection so it does not read from the MV store. pub fn demote_to_mvcc_connection(&self) { assert!(!self.is_mvcc_bootstrap_connection.load(Ordering::SeqCst)); self.is_mvcc_bootstrap_connection .store(true, Ordering::SeqCst); } /// Parse schema from scratch if version of schema for the connection differs from the schema cookie in the root page /// This function must be called outside of any transaction because internally it will start transaction session by itself #[allow(dead_code)] fn maybe_reparse_schema(self: &Arc) -> Result<()> { let pager = self.pager.load().clone(); // first, quickly read schema_version from the root page in order to check if schema changed pager.begin_read_tx()?; let on_disk_schema_version = pager .io .block(|| pager.with_header(|header| header.schema_cookie)); let on_disk_schema_version = match on_disk_schema_version { Ok(db_schema_version) => db_schema_version.get(), Err(LimboError::Page1NotAlloc) => { // this means this is a fresh db, so return a schema version of 0 0 } Err(err) => { pager.end_read_tx(); return Err(err); } }; pager.end_read_tx(); let db_schema_version = self.db.schema.lock().schema_version; tracing::debug!( "path: {}, db_schema_version={} vs on_disk_schema_version={}", self.db.path, db_schema_version, on_disk_schema_version ); // if schema_versions matches - exit early if db_schema_version == on_disk_schema_version { return Ok(()); } // maybe_reparse_schema must be called outside of any transaction turso_assert!( self.get_tx_state() == TransactionState::None, "unexpected start transaction" ); // start read transaction manually, because we will read schema cookie once again and // we must be sure that it will consistent with schema content // // from now on we must be very careful with errors propagation // in order to not accidentally keep read transaction opened pager.begin_read_tx()?; self.set_tx_state(TransactionState::Read); let reparse_result = self.reparse_schema(); let previous = self.transaction_state.swap(TransactionState::None); turso_assert!( matches!(previous, TransactionState::None | TransactionState::Read), "unexpected end transaction state" ); // close opened transaction if it was kept open // (in most cases, it will be automatically closed if stmt was executed properly) if previous == TransactionState::Read { pager.end_read_tx(); } reparse_result?; let schema = self.schema.read().clone(); self.db.update_schema_if_newer(schema); Ok(()) } pub(crate) fn reparse_schema(self: &Arc) -> Result<()> { let pager = self.pager.load().clone(); // read cookie before consuming statement program - otherwise we can end up reading cookie with closed transaction state let cookie = pager .io .block(|| pager.with_header(|header| header.schema_cookie))? .get(); // create fresh schema as some objects can be deleted let mut fresh = Schema::with_options(self.experimental_custom_types_enabled()); fresh.schema_version = cookie; // Preserve existing views to avoid expensive repopulation. // TODO: We may not need to do this if we materialize our views. let existing_views = self.schema.read().incremental_views.clone(); // TODO: this is hack to avoid a cyclical problem with schema reprepare // The problem here is that we prepare a statement here, but when the statement tries // to execute it, it first checks the schema cookie to see if it needs to reprepare the statement. // But in this occasion it will always reprepare, and we get an error. So we trick the statement by swapping our schema // with a new clean schema that has the same header cookie. self.with_schema_mut(|schema| { *schema = fresh.clone(); }); let stmt = self.prepare("SELECT * FROM sqlite_schema")?; // MVCC bootstrap connection gets the "baseline" from the DB file and ignores anything in MV store let mv_tx = if self.is_mvcc_bootstrap_connection() { None } else { self.get_mv_tx() }; // TODO: This function below is synchronous, make it async parse_schema_rows(stmt, &mut fresh, &self.syms.read(), mv_tx, existing_views)?; // Load custom types from __turso_internal_types if the table exists // and custom types are enabled. Type loading errors are non-fatal: we log // warnings and continue with whatever types loaded successfully. if self.experimental_custom_types_enabled() && fresh .tables .contains_key(crate::schema::TURSO_TYPES_TABLE_NAME) { // Temporarily install the schema so we can prepare a query against it self.with_schema_mut(|schema| { *schema = fresh.clone(); }); let load_result: Result<()> = (|| { let type_sqls = self.query_stored_type_definitions()?; fresh.load_type_definitions(&type_sqls)?; Ok(()) })(); if let Err(e) = load_result { tracing::warn!("Failed to load custom types: {}", e); } } // Best-effort load stats if sqlite_stat1 is present and DB is initialized. refresh_analyze_stats(self); tracing::debug!( "reparse_schema: schema_version={}, tables={:?}", fresh.schema_version, fresh.tables.keys() ); self.with_schema_mut(|schema| { *schema = fresh; }); Result::Ok(()) } #[instrument(skip_all, level = Level::INFO)] pub fn prepare_execute_batch(self: &Arc, sql: impl AsRef) -> Result<()> { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } if sql.as_ref().is_empty() { return Err(LimboError::InvalidArgument( "The supplied SQL string contains no statements".to_string(), )); } self.maybe_update_schema(); let sql = sql.as_ref(); tracing::trace!("Preparing and executing batch: {}", sql); let mut parser = Parser::new(sql.as_bytes()); while let Some(cmd) = parser.next_cmd()? { let syms = self.syms.read(); let pager = self.pager.load().clone(); let byte_offset_end = parser.offset(); let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end]) .unwrap() .trim(); let mode = QueryMode::new(&cmd); let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd; let schema = self.schema.read().clone(); let program = translate::translate( &schema, stmt, pager.clone(), self.clone(), &syms, mode, input, )?; Statement::new(program, pager.clone(), mode, 0).run_ignore_rows()?; } Ok(()) } #[instrument(skip_all, level = Level::INFO)] pub fn query(self: &Arc, sql: impl AsRef) -> Result> { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } let sql = sql.as_ref(); self.maybe_update_schema(); tracing::trace!("Querying: {}", sql); let mut parser = Parser::new(sql.as_bytes()); let cmd = parser.next_cmd()?; let byte_offset_end = parser.offset(); let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end]) .unwrap() .trim(); match cmd { Some(cmd) => self.run_cmd(cmd, input), None => Ok(None), } } #[instrument(skip_all, level = Level::INFO)] pub(crate) fn run_cmd( self: &Arc, cmd: Cmd, input: &str, ) -> Result> { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } let syms = self.syms.read(); let pager = self.pager.load().clone(); let mode = QueryMode::new(&cmd); let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd; let schema = self.schema.read().clone(); let program = translate::translate( &schema, stmt, pager.clone(), self.clone(), &syms, mode, input, )?; let stmt = Statement::new(program, pager, mode, 0); Ok(Some(stmt)) } pub fn query_runner<'a>(self: &'a Arc, sql: &'a [u8]) -> QueryRunner<'a> { QueryRunner::new(self, sql) } /// Execute will run a query from start to finish taking ownership of I/O because it will run pending I/Os if it didn't finish. /// TODO: make this api async #[instrument(skip_all, level = Level::INFO)] pub fn execute(self: &Arc, sql: impl AsRef) -> Result<()> { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } let sql = sql.as_ref(); self.maybe_update_schema(); let mut parser = Parser::new(sql.as_bytes()); while let Some(cmd) = parser.next_cmd()? { let syms = self.syms.read(); let pager = self.pager.load().clone(); let byte_offset_end = parser.offset(); let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end]) .unwrap() .trim(); let mode = QueryMode::new(&cmd); let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd; let schema = self.schema.read().clone(); let program = translate::translate( &schema, stmt, pager.clone(), self.clone(), &syms, mode, input, )?; Statement::new(program, pager.clone(), mode, 0).run_ignore_rows()?; } Ok(()) } #[instrument(skip_all, level = Level::INFO)] pub fn consume_stmt( self: &Arc, sql: impl AsRef, ) -> Result> { let mut parser = Parser::new(sql.as_ref().as_bytes()); let Some(cmd) = parser.next_cmd()? else { return Ok(None); }; let syms = self.syms.read(); let pager = self.pager.load().clone(); let byte_offset_end = parser.offset(); let input = str::from_utf8(&sql.as_ref().as_bytes()[..byte_offset_end]) .unwrap() .trim(); let mode = QueryMode::new(&cmd); let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd; let schema = self.schema.read().clone(); let program = translate::translate( &schema, stmt, pager.clone(), self.clone(), &syms, mode, input, )?; let stmt = Statement::new(program, pager, mode, 0); Ok(Some((stmt, parser.offset()))) } #[cfg(feature = "fs")] pub fn from_uri(uri: &str, db_opts: DatabaseOpts) -> Result<(Arc, Arc)> { use crate::util::MEMORY_PATH; let opts = OpenOptions::parse(uri)?; let flags = opts.get_flags()?; if opts.path == MEMORY_PATH || matches!(opts.mode, OpenMode::Memory) { let io = Arc::new(MemoryIO::new()); let db = Database::open_file_with_flags(io.clone(), MEMORY_PATH, flags, db_opts, None)?; let conn = db.connect()?; return Ok((io, conn)); } let encryption_opts = match (opts.cipher.clone(), opts.hexkey.clone()) { (Some(cipher), Some(hexkey)) => Some(EncryptionOpts { cipher, hexkey }), (Some(_), None) => { return Err(LimboError::InvalidArgument( "hexkey is required when cipher is provided".to_string(), )) } (None, Some(_)) => { return Err(LimboError::InvalidArgument( "cipher is required when hexkey is provided".to_string(), )) } (None, None) => None, }; let (io, db) = Database::open_new( &opts.path, opts.vfs.as_ref(), flags, db_opts, encryption_opts, )?; if let Some(modeof) = opts.modeof { let perms = std::fs::metadata(modeof).map_err(|e| io_error(e, "metadata"))?; std::fs::set_permissions(&opts.path, perms.permissions()) .map_err(|e| io_error(e, "set_permissions"))?; } let conn = db.connect()?; if let Some(cipher) = opts.cipher { let _ = conn.pragma_update("cipher", format!("'{cipher}'")); } if let Some(hexkey) = opts.hexkey { let _ = conn.pragma_update("hexkey", format!("'{hexkey}'")); } Ok((io, conn)) } #[cfg(feature = "fs")] fn from_uri_attached( uri: &str, mut db_opts: DatabaseOpts, main_db_flags: OpenFlags, io: Arc, ) -> Result<(Arc, Option)> { let opts = OpenOptions::parse(uri)?; let mut flags = opts.get_flags()?; if main_db_flags.contains(OpenFlags::ReadOnly) { flags |= OpenFlags::ReadOnly; } let encryption_opts = match (opts.cipher.clone(), opts.hexkey.clone()) { (Some(cipher), Some(hexkey)) => Some(EncryptionOpts { cipher, hexkey }), (Some(_), None) => { return Err(LimboError::InvalidArgument( "hexkey is required when cipher is provided".to_string(), )) } (None, Some(_)) => { return Err(LimboError::InvalidArgument( "cipher is required when hexkey is provided".to_string(), )) } (None, None) => None, }; if encryption_opts.is_some() { db_opts = db_opts.with_encryption(true); } let io = opts.vfs.map(Database::io_for_vfs).unwrap_or(Ok(io))?; let db = Database::open_file_with_flags( io.clone(), &opts.path, flags, db_opts, encryption_opts.clone(), )?; if let Some(modeof) = opts.modeof { let perms = std::fs::metadata(modeof).map_err(|e| io_error(e, "metadata"))?; std::fs::set_permissions(&opts.path, perms.permissions()) .map_err(|e| io_error(e, "set_permissions"))?; } Ok((db, encryption_opts)) } pub fn set_foreign_keys_enabled(&self, enable: bool) { self.fk_pragma.store(enable, Ordering::Release); self.bump_prepare_context_generation(); } pub fn foreign_keys_enabled(&self) -> bool { self.fk_pragma.load(Ordering::Acquire) } pub fn set_check_constraints_ignored(&self, ignore: bool) { self.check_constraints_pragma .store(ignore, Ordering::Release); } pub fn check_constraints_ignored(&self) -> bool { self.check_constraints_pragma.load(Ordering::Acquire) } pub(crate) fn clear_deferred_foreign_key_violations(&self) -> isize { self.fk_deferred_violations.swap(0, Ordering::Release) } pub(crate) fn get_deferred_foreign_key_violations(&self) -> isize { self.fk_deferred_violations.load(Ordering::Acquire) } pub(crate) fn increment_deferred_foreign_key_violations(&self, v: isize) { self.fk_deferred_violations.fetch_add(v, Ordering::AcqRel); } /// Query the CREATE TYPE SQL definitions stored in __turso_internal_types. /// The connection's schema must already contain the table definitions so /// that `prepare` can resolve the table name. Returns an empty Vec if the /// types table does not exist. pub(crate) fn query_stored_type_definitions(self: &Arc) -> Result> { let has_types_table = { let s = self.schema.read(); s.tables.contains_key(crate::schema::TURSO_TYPES_TABLE_NAME) }; if !has_types_table { return Ok(Vec::new()); } let mut type_stmt = self.prepare(format!( "SELECT name, sql FROM {}", crate::schema::TURSO_TYPES_TABLE_NAME ))?; let mut type_rows = Vec::new(); type_stmt.run_with_row_callback(|row| { type_rows.push(row.get::<&str>(1)?.to_string()); Ok(()) })?; Ok(type_rows) } pub fn maybe_update_schema(&self) { let current_schema_version = self.schema.read().schema_version; let schema = self.db.schema.lock(); if matches!(self.get_tx_state(), TransactionState::None) && current_schema_version != schema.schema_version { *self.schema.write() = schema.clone(); } } /// Read schema version at current transaction #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn read_schema_version(&self) -> Result { let pager = self.pager.load(); pager .io .block(|| pager.with_header(|header| header.schema_cookie)) .map(|version| version.get()) } /// Update schema version to the new value within opened write transaction /// /// New version of the schema must be strictly greater than previous one - otherwise method will panic /// Write transaction must be opened in advance - otherwise method will panic #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn write_schema_version(self: &Arc, version: u32) -> Result<()> { let TransactionState::Write { .. } = self.get_tx_state() else { return Err(LimboError::InternalError( "write_schema_version must be called from within Write transaction".to_string(), )); }; let pager = self.pager.load(); pager.io.block(|| { pager.with_header_mut(|header| { turso_assert!( header.schema_cookie.get() < version, "cookie can't go back in time" ); self.set_tx_state(TransactionState::Write { schema_did_change: true, }); self.with_schema_mut(|schema| schema.schema_version = version); header.schema_cookie = version.into(); }) })?; self.reparse_schema()?; Ok(()) } /// Try to read page with given ID with fixed WAL watermark position /// This method return false if page is not found (so, this is probably new page created after watermark position which wasn't checkpointed to the DB file yet) #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn try_wal_watermark_read_page( &self, page_idx: u32, page: &mut [u8], frame_watermark: Option, ) -> Result { let Some((page_ref, c)) = self.try_wal_watermark_read_page_begin(page_idx, frame_watermark)? else { return Ok(false); }; match self.get_pager().io.wait_for_completion(c) { #[cfg(all(target_os = "windows", feature = "experimental_win_iocp"))] Err(LimboError::CompletionError(crate::error::CompletionError::IOError( std::io::ErrorKind::UnexpectedEof, _, ))) => { return Ok(false); } Err(e) => return Err(e), _ => {} } self.try_wal_watermark_read_page_end(page, page_ref) } #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn try_wal_watermark_read_page_begin( &self, page_idx: u32, frame_watermark: Option, ) -> Result, Completion)>> { let pager = self.pager.load(); let (page_ref, c) = match pager.read_page_no_cache(page_idx as i64, frame_watermark, true) { Ok(result) => result, // on windows, zero read will trigger UnexpectedEof #[cfg(target_os = "windows")] Err(LimboError::CompletionError(crate::error::CompletionError::IOError( std::io::ErrorKind::UnexpectedEof, _, ))) => return Ok(None), Err(err) => return Err(err), }; Ok(Some((page_ref, c))) } #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn try_wal_watermark_read_page_end( &self, page: &mut [u8], page_ref: Arc, ) -> Result { let content = page_ref.get_contents(); // empty read - attempt to read absent page if content.buffer.as_ref().is_none_or(|b| b.is_empty()) { return Ok(false); } page.copy_from_slice(content.as_ptr()); Ok(true) } /// Return unique set of page numbers changes after WAL watermark position in the current WAL session /// (so, if concurrent connection wrote something to the WAL - this method will not see this change) #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn wal_changed_pages_after(&self, frame_watermark: u64) -> Result> { self.pager.load().wal_changed_pages_after(frame_watermark) } #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn wal_state(&self) -> Result { self.pager.load().wal_state() } #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn wal_get_frame(&self, frame_no: u64, frame: &mut [u8]) -> Result { use crate::storage::sqlite3_ondisk::parse_wal_frame_header; let c = self.pager.load().wal_get_frame(frame_no, frame)?; self.db.io.wait_for_completion(c)?; let (header, _) = parse_wal_frame_header(frame); Ok(WalFrameInfo { page_no: header.page_number, db_size: header.db_size, }) } /// Insert `frame` (header included) at the position `frame_no` in the WAL /// If WAL already has frame at that position - turso-db will compare content of the page and either report conflict or return OK /// If attempt to write frame at the position `frame_no` will create gap in the WAL - method will return error #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn wal_insert_frame(&self, frame_no: u64, frame: &[u8]) -> Result { self.pager.load().wal_insert_frame(frame_no, frame) } /// Start WAL session by initiating read+write transaction for this connection #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn wal_insert_begin(&self) -> Result<()> { let pager = self.pager.load(); pager.begin_read_tx()?; pager.io.block(|| pager.begin_write_tx()).inspect_err(|_| { pager.end_read_tx(); })?; // start write transaction and disable auto-commit mode as SQL can be executed within WAL session (at caller own risk) self.set_tx_state(TransactionState::Write { schema_did_change: false, }); self.auto_commit.store(false, Ordering::SeqCst); Ok(()) } /// Finish WAL session by ending read+write transaction taken in the [Self::wal_insert_begin] method /// All frames written after last commit frame (db_size > 0) within the session will be rolled back #[cfg(all(feature = "fs", feature = "conn_raw_api"))] pub fn wal_insert_end(self: &Arc, force_commit: bool) -> Result<()> { use crate::{return_if_io, types::IOResult}; { let pager = self.pager.load(); let Some(wal) = pager.wal.as_ref() else { return Err(LimboError::InternalError( "wal_insert_end called without a wal".to_string(), )); }; let commit_err = if force_commit { pager .io .block(|| { return_if_io!(pager.commit_dirty_pages( true, self.get_sync_mode(), self.get_data_sync_retry(), )); pager.commit_dirty_pages_end(); Ok(IOResult::Done(())) }) .err() } else { None }; self.auto_commit.store(true, Ordering::SeqCst); self.set_tx_state(TransactionState::None); wal.end_write_tx(); wal.end_read_tx(); if !force_commit { // remove all non-commited changes in case if WAL session left some suffix without commit frame if let Some(mv_store) = self.mv_store().as_ref() { if let Some(tx_id) = self.get_mv_tx_id() { mv_store.rollback_tx(tx_id, pager.clone(), self, MAIN_DB_ID); } } pager.rollback(false, self, true); } if let Some(err) = commit_err { return Err(err); } } // let's re-parse schema from scratch if schema cookie changed compared to the our in-memory view of schema self.maybe_reparse_schema()?; Ok(()) } /// Flush dirty pages to disk. pub fn cacheflush(&self) -> Result> { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } let pager = self.pager.load(); pager.io.block(|| pager.cacheflush()) } pub fn checkpoint(self: &Arc, mode: CheckpointMode) -> Result { use crate::mvcc::database::CheckpointStateMachine; use crate::state_machine::{StateTransition, TransitionResult}; if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } if let Some(mv_store) = self.mv_store().as_ref() { let pager = self.pager.load().clone(); let io = pager.io.clone(); let mut ckpt_sm = CheckpointStateMachine::new( pager, mv_store.clone(), self.clone(), true, self.get_sync_mode(), ); loop { match ckpt_sm.step(&()) { Ok(TransitionResult::Continue) => {} Ok(TransitionResult::Done(result)) => return Ok(result), Ok(TransitionResult::Io(iocompletions)) => { if let Err(err) = iocompletions.wait(io.as_ref()) { ckpt_sm.cleanup_after_external_io_error(); return Err(err); } } Err(err) => return Err(err), } } } else { self.pager .load() .blocking_checkpoint(mode, self.get_sync_mode()) } } /// Close a connection and checkpoint. pub fn close(&self) -> Result<()> { if self.is_closed() { return Ok(()); } self.closed.store(true, Ordering::SeqCst); let pager = self.pager.load(); match self.get_tx_state() { TransactionState::None => { // No active transaction } _ => { if self.mvcc_enabled() { if let Some(mv_store) = self.mv_store().as_ref() { if let Some(tx_id) = self.get_mv_tx_id() { mv_store.rollback_tx(tx_id, pager.clone(), self, MAIN_DB_ID); } } pager.end_read_tx(); } else { pager.rollback_tx(self); } // Roll back all attached DB transactions regardless of main // DB mode — a :memory: attached DB may use WAL even when the // main DB uses MVCC. self.rollback_attached_mvcc_txs(false); self.rollback_attached_wal_txns(); self.set_tx_state(TransactionState::None); } } if self.db.n_connections.fetch_sub(1, Ordering::SeqCst).eq(&1) && !self.db.is_readonly() { self.pager.load().checkpoint_shutdown( self.is_wal_auto_checkpoint_disabled(), self.get_sync_mode(), )?; }; Ok(()) } pub fn wal_auto_checkpoint_disable(&self) { self.wal_auto_checkpoint_disabled .store(true, Ordering::SeqCst); } pub fn is_wal_auto_checkpoint_disabled(&self) -> bool { self.wal_auto_checkpoint_disabled.load(Ordering::SeqCst) || self.db.get_mv_store().is_some() } pub fn last_insert_rowid(&self) -> i64 { self.last_insert_rowid.load(Ordering::SeqCst) } pub(crate) fn update_last_rowid(&self, rowid: i64) { self.last_insert_rowid.store(rowid, Ordering::SeqCst); } pub fn set_changes(&self, nchange: i64) { self.last_change.store(nchange, Ordering::SeqCst); self.total_changes.fetch_add(nchange, Ordering::SeqCst); } pub fn changes(&self) -> i64 { self.last_change.load(Ordering::SeqCst) } pub fn total_changes(&self) -> i64 { self.total_changes.load(Ordering::SeqCst) } pub fn get_cache_size(&self) -> i32 { self.cache_size.load(Ordering::SeqCst) } pub fn set_cache_size(&self, size: i32) { self.cache_size.store(size, Ordering::SeqCst); self.bump_prepare_context_generation(); } pub fn get_capture_data_changes_info( &self, ) -> crate::sync::RwLockReadGuard<'_, Option> { self.capture_data_changes.read() } pub fn set_capture_data_changes_info(&self, opts: Option) { *self.capture_data_changes.write() = opts; self.bump_prepare_context_generation(); } pub fn get_cdc_transaction_id(&self) -> i64 { self.cdc_transaction_id.load(Ordering::SeqCst) } pub fn set_cdc_transaction_id(&self, id: i64) { self.cdc_transaction_id.store(id, Ordering::SeqCst); } pub fn get_page_size(&self) -> PageSize { let value = self.page_size.load(Ordering::SeqCst); PageSize::new_from_header_u16(value).unwrap_or_default() } pub fn is_closed(&self) -> bool { self.closed.load(Ordering::SeqCst) } pub fn is_query_only(&self) -> bool { self.query_only.load(Ordering::SeqCst) } pub fn get_database_canonical_path(&self) -> String { if self.db.path == ":memory:" { // For in-memory databases, SQLite shows empty string String::new() } else { // For file databases, try show the full absolute path if that doesn't fail match std::fs::canonicalize(&self.db.path) { Ok(abs_path) => abs_path.to_string_lossy().to_string(), Err(_) => self.db.path.to_string(), } } } /// Check if a specific attached database is read only or not, by its index pub fn is_readonly(&self, index: usize) -> bool { if index <= 1 { self.db.is_readonly() } else { let db = self.attached_databases.read().get_database_by_index(index); db.expect("Should never have called this without being sure the database exists") .is_readonly() } } /// Reset the page size for the current connection. /// /// Specifying a new page size does not change the page size immediately. /// Instead, the new page size is remembered and is used to set the page size when the database /// is first created, if it does not already exist when the page_size pragma is issued, /// or at the next VACUUM command that is run on the same database connection while not in WAL mode. pub fn reset_page_size(&self, size: u32) -> Result<()> { if self.db.initialized() { return Ok(()); } let Some(size) = PageSize::new(size) else { return Ok(()); }; self.page_size.store(size.get_raw(), Ordering::SeqCst); self.pager.load().set_initial_page_size(size)?; self.bump_prepare_context_generation(); Ok(()) } #[cfg(feature = "fs")] pub fn open_new(&self, path: &str, vfs: &str) -> Result<(Arc, Arc)> { Database::open_with_vfs(&self.db, path, vfs) } pub fn list_vfs(&self) -> Vec { #[allow(unused_mut)] let mut all_vfs = vec![String::from("memory")]; #[cfg(feature = "fs")] { #[cfg(target_family = "unix")] { all_vfs.push("syscall".to_string()); } #[cfg(all(target_os = "linux", feature = "io_uring"))] { all_vfs.push("io_uring".to_string()); } #[cfg(all(target_os = "windows", feature = "experimental_win_iocp"))] { all_vfs.push("experimental_win_iocp".to_string()); } all_vfs.extend(crate::ext::list_vfs_modules()); } all_vfs } pub fn get_auto_commit(&self) -> bool { self.auto_commit.load(Ordering::SeqCst) } pub fn reparse_schema_after_extension_load(self: &Arc) -> Result<()> { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } // Collect row data from the Statement first, then drop the Statement // before taking the schema write lock. This prevents a deadlock in MVCC // mode where Statement::drop -> abort -> rollback_tx -> schema.read() // would deadlock against the schema write lock. let mut rows_data: Vec<(String, String, String, i64, Option)> = Vec::new(); { let mut rows = self .query("SELECT * FROM sqlite_schema")? .expect("query must be parsed to statement"); rows.run_with_row_callback(|row| { let ty = row.get::<&str>(0)?.to_string(); let name = row.get::<&str>(1)?.to_string(); let table_name = row.get::<&str>(2)?.to_string(); let root_page = row.get::(3)?; let sql = row.get::<&str>(4).ok().map(|s| s.to_string()); rows_data.push((ty, name, table_name, root_page, sql)); Ok(()) })?; } // Statement dropped here, before schema write lock let syms = self.syms.read(); self.with_schema_mut(|schema| -> Result<()> { // Incremental re-parse after extension loading. The schema already has // tables/indices/views from initial parse. We only need to pick up // entries that previously failed (e.g. virtual tables whose module // wasn't loaded yet). "Already exists" errors are expected and skipped. let mut from_sql_indexes = Vec::new(); let mut automatic_indices = HashMap::default(); let mut dbsp_state_roots = HashMap::default(); let mut dbsp_state_index_roots = HashMap::default(); let mut materialized_view_info = HashMap::default(); for (ty, name, table_name, root_page, sql) in &rows_data { match schema.handle_schema_row( ty, name, table_name, *root_page, sql.as_deref(), &syms, &mut from_sql_indexes, &mut automatic_indices, &mut dbsp_state_roots, &mut dbsp_state_index_roots, &mut materialized_view_info, ) { Ok(()) => {} Err(LimboError::ParseError(msg)) if msg.contains("already exists") => {} Err(LimboError::ExtensionError(msg)) => { eprintln!("Warning: {msg}"); } Err(e) => return Err(e), } } match schema.populate_indices(&syms, from_sql_indexes, automatic_indices, false) { Ok(()) => {} Err(LimboError::ParseError(msg)) if msg.contains("already exists") => {} Err(LimboError::ExtensionError(msg)) => eprintln!("Warning: {msg}"), Err(e) => return Err(e), } match schema.populate_materialized_views( materialized_view_info, dbsp_state_roots, dbsp_state_index_roots, ) { Ok(()) => {} Err(LimboError::ExtensionError(msg)) => eprintln!("Warning: {msg}"), Err(e) => return Err(e), } Ok(()) }) } // Clearly there is something to improve here, Vec> isn't a couple of tea /// Query the current rows/values of `pragma_name`. pub fn pragma_query(self: &Arc, pragma_name: &str) -> Result>> { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } let pragma = format!("PRAGMA {pragma_name}"); let mut stmt = self.prepare(pragma)?; stmt.run_collect_rows() } /// Set a new value to `pragma_name`. /// /// Some pragmas will return the updated value which cannot be retrieved /// with this method. pub fn pragma_update( self: &Arc, pragma_name: &str, pragma_value: V, ) -> Result>> { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } let pragma = format!("PRAGMA {pragma_name} = {pragma_value}"); let mut stmt = self.prepare(pragma)?; stmt.run_collect_rows() } pub fn experimental_views_enabled(&self) -> bool { self.db.experimental_views_enabled() } pub fn experimental_index_method_enabled(&self) -> bool { self.db.experimental_index_method_enabled() } pub fn experimental_custom_types_enabled(&self) -> bool { self.db.experimental_custom_types_enabled() } pub fn experimental_attach_enabled(&self) -> bool { self.db.experimental_attach_enabled() } pub fn mvcc_enabled(&self) -> bool { self.db.mvcc_enabled() } pub fn mv_store(&self) -> impl Deref>> { struct TransparentWrapper(T); impl Deref for TransparentWrapper { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } // Never use MV store for bootstrapping - we read state directly from sqlite_schema in the DB file. if !self.is_mvcc_bootstrap_connection() { either::Left(self.db.get_mv_store()) } else { either::Right(TransparentWrapper(None)) } } /// Query the current value(s) of `pragma_name` associated to /// `pragma_value`. /// /// This method can be used with query-only pragmas which need an argument /// (e.g. `table_info('one_tbl')`) or pragmas which returns value(s) /// (e.g. `integrity_check`). pub fn pragma( self: &Arc, pragma_name: &str, pragma_value: V, ) -> Result>> { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } let pragma = format!("PRAGMA {pragma_name}({pragma_value})"); let mut stmt = self.prepare(pragma)?; let mut results = Vec::new(); loop { match stmt.step()? { vdbe::StepResult::Row => { let row: Vec = stmt.row().unwrap().get_values().cloned().collect(); results.push(row); } vdbe::StepResult::Interrupt | vdbe::StepResult::Busy => { return Err(LimboError::Busy); } _ => break, } } Ok(results) } #[inline] pub fn with_schema_mut(&self, f: impl FnOnce(&mut Schema) -> T) -> T { let mut schema_ref = self.schema.write(); let schema = Arc::make_mut(&mut *schema_ref); f(schema) } /// Mutate the schema for a specific database (main or attached). pub(crate) fn with_database_schema_mut( &self, database_id: usize, f: impl FnOnce(&mut Schema) -> T, ) -> T { if !crate::is_attached_db(database_id) { self.with_schema_mut(f) } else { // For attached databases, update a connection-local copy of the schema. // We don't update the shared db.schema until after the WAL commit, so // other connections won't see uncommitted schema changes (which would // cause SchemaUpdated mismatches). let mut schemas = self.database_schemas.write(); let schema_arc = schemas.entry(database_id).or_insert_with(|| { // Lazily copy from the shared Database schema let attached_dbs = self.attached_databases.read(); let (db, _pager) = attached_dbs .index_to_data .get(&database_id) .expect("Database ID should be valid"); let schema = db.schema.lock().clone(); schema }); let schema = Arc::make_mut(schema_arc); f(schema) } } pub fn is_db_initialized(&self) -> bool { self.db.initialized() } pub(crate) fn get_pager_from_database_index(&self, index: &usize) -> Arc { if *index < 2 { self.pager.load().clone() } else { self.attached_databases.read().get_pager_by_index(index) } } /// Get the database name for a given database index. /// Returns "main" for index 0, "temp" for index 1, and the alias for attached databases. pub(crate) fn get_database_name_by_index(&self, index: usize) -> Option { match index { 0 => Some("main".to_string()), 1 => Some("temp".to_string()), _ => self.attached_databases.read().get_name_by_index(index), } } #[cfg(feature = "fs")] fn is_attached(&self, alias: &str) -> bool { self.attached_databases .read() .name_to_index .contains_key(alias) } /// Attach a database file with the given alias name #[cfg(not(feature = "fs"))] pub(crate) fn attach_database(&self, _path: &str, _alias: &str) -> Result<()> { return Err(LimboError::InvalidArgument(format!( "attach not available in this build (no-fs)" ))); } /// Attach a database file with the given alias name #[cfg(feature = "fs")] pub(crate) fn attach_database(&self, path: &str, alias: &str) -> Result<()> { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } if self.is_attached(alias) { return Err(LimboError::InvalidArgument(format!( "database {alias} is already in use" ))); } // Check for reserved database names if alias.eq_ignore_ascii_case("main") || alias.eq_ignore_ascii_case("temp") { return Err(LimboError::InvalidArgument(format!( "reserved name {alias} is already in use" ))); } let use_views = self.db.experimental_views_enabled(); let use_custom_types = self.db.experimental_custom_types_enabled(); let db_opts = DatabaseOpts::new() .with_views(use_views) .with_custom_types(use_custom_types); // Select the IO layer for the attached database: // - :memory: databases always get a fresh MemoryIO // - File-based databases reuse the parent's IO when the parent is also // file-based (important for simulator fault injection and WAL coordination) // - If the parent is :memory: (MemoryIO) but the attached DB is file-based, // we need a file-capable IO layer since MemoryIO can't read real files let is_memory_db = path == ":memory:" || path.starts_with("file::memory:") || path.is_empty(); let io: Arc = if is_memory_db { Arc::new(MemoryIO::new()) } else if self.db.path.starts_with(":memory:") { Database::io_for_path(path)? } else { self.db.io.clone() }; let main_db_flags = self.db.open_flags; let (db, encryption_opts) = Self::from_uri_attached(path, db_opts, main_db_flags, io)?; // Build encryption key from URI opts to pass to _init for decrypting page 1. let encryption_key = if let Some(ref enc) = encryption_opts { Some(EncryptionKey::from_hex_string(&enc.hexkey)?) } else { None }; let pager = Arc::new(db._init(encryption_key.as_ref())?); // In-memory attached databases inherit the main database's journal mode. // A fresh :memory: DB defaults to WAL, so when main is MVCC we need to // create an MvStore for the attached DB so it runs in the same mode. if is_memory_db && self.mvcc_enabled() && !db.mvcc_enabled() { // todo(v): pass required encryption ctx to enable encryption with mvcc let mv_store = journal_mode::open_mv_store( db.io.clone(), &db.path, db.open_flags, db.durable_storage.clone(), None, )?; db.mv_store.store(Some(mv_store.clone())); let bootstrap_conn = db._connect(true, Some(pager.clone()), encryption_key)?; mv_store.bootstrap(bootstrap_conn)?; } // Reject incompatible journal modes for file-based databases: we cannot // silently convert the header (the user may have attached read-only). if self.mvcc_enabled() != db.mvcc_enabled() { let main_mode = if self.mvcc_enabled() { "MVCC" } else { "WAL" }; let attached_mode = if db.mvcc_enabled() { "MVCC" } else { "WAL" }; return Err(LimboError::InvalidArgument(format!( "cannot attach database '{alias}': main database uses {main_mode} journal mode \ but attached database uses {attached_mode}. Both must use the same journal mode." ))); } // Reject mismatched page sizes: ephemeral tables and cross-database // operations assume a uniform page size across all attached databases. let main_pager = self.pager.load(); if let (Some(main_ps), Some(attached_ps)) = (main_pager.get_page_size(), pager.get_page_size()) { if main_ps != attached_ps { return Err(LimboError::InvalidArgument(format!( "cannot attach database '{alias}': page size mismatch \ (main={main_ps:?}, attached={attached_ps:?})" ))); } } self.attached_databases.write().insert(alias, (db, pager)); self.bump_prepare_context_generation(); Ok(()) } // Detach a database by alias name pub(crate) fn detach_database(&self, alias: &str) -> Result<()> { if self.is_closed() { return Err(LimboError::InternalError("Connection closed".to_string())); } if alias == "main" || alias == "temp" { return Err(LimboError::InvalidArgument(format!( "cannot detach database: {alias}" ))); } // Look up the database index first, then rollback any MVCC transaction // *before* removing the database from the catalog. mv_store_for_db // and get_pager_from_database_index read `attached_databases`, so we // must not hold the write lock during the rollback. let database_id = { let attached_dbs = self.attached_databases.read(); match attached_dbs.name_to_index.get(alias).copied() { Some(id) => id, None => { return Err(LimboError::InvalidArgument(format!( "no such database: {alias}" ))); } } }; // Rollback any active transaction on this database before detaching. // After the Database is removed from the catalog, the MvStore / Pager // become unreachable and the transaction would leak forever. let pager = self.get_pager_from_database_index(&database_id); if let Some((tx_id, _mode)) = self.get_mv_tx_for_db(database_id) { if let Some(mv_store) = self.mv_store_for_db(database_id) { mv_store.rollback_tx(tx_id, pager.clone(), self, database_id); pager.end_read_tx(); } self.set_mv_tx_for_db(database_id, None); } else { // Non-MVCC attached DB (e.g. :memory:) — rollback WAL state. pager.rollback_attached(); } // Remove from catalog. The write lock must be released before // acquiring database_schemas.write() to maintain consistent lock // ordering (attached_databases before database_schemas). { let mut attached_dbs = self.attached_databases.write(); attached_dbs.remove(alias); } // Invalidate the cached schema for this database index so that a future // ATTACH reusing the same index won't see stale schema entries. self.database_schemas.write().remove(&database_id); self.bump_prepare_context_generation(); Ok(()) } /// List all attached database aliases pub fn list_attached_databases(&self) -> Vec { self.attached_databases .read() .name_to_index .keys() .cloned() .collect() } /// Get all attached database pagers (excludes main/temp databases) pub fn get_all_attached_pagers(&self) -> Vec> { let catalog = self.attached_databases.read(); catalog .index_to_data .values() .map(|(_db, pager)| pager.clone()) .collect() } /// Get all attached database (index, pager) pairs (excludes main/temp databases) pub(crate) fn get_all_attached_pagers_with_index(&self) -> Vec<(usize, Arc)> { let catalog = self.attached_databases.read(); catalog .index_to_data .iter() .map(|(&idx, (_db, pager))| (idx, pager.clone())) .collect() } pub(crate) fn database_schemas(&self) -> &RwLock>> { &self.database_schemas } /// Publish a connection-local attached DB schema to the shared Database instance. /// Called after the attached pager's WAL commit succeeds, so other connections /// can now see the schema changes. pub(crate) fn publish_attached_schema(&self, database_id: usize) { let mut schemas = self.database_schemas.write(); if let Some(local_schema) = schemas.remove(&database_id) { let attached_dbs = self.attached_databases.read(); if let Some((db, _pager)) = attached_dbs.index_to_data.get(&database_id) { *db.schema.lock() = local_schema; } } } pub(crate) fn attached_databases(&self) -> &RwLock { &self.attached_databases } /// Access schema for a database using a closure pattern to avoid cloning pub(crate) fn with_schema(&self, database_id: usize, f: impl FnOnce(&Schema) -> T) -> T { match database_id { crate::MAIN_DB_ID | crate::TEMP_DB_ID => { // Main database - use connection's schema which should be kept in sync // NOTE: for Temp databases, for now they can use the connection-local schema // but this will change in the future let schema = self.schema.read(); f(&schema) } _ => { // Attached database: prefer the connection-local copy (which may contain // uncommitted schema changes from this connection's transaction), falling // back to the shared Database schema (last committed state). let schemas = self.database_schemas.read(); if let Some(local_schema) = schemas.get(&database_id) { return f(local_schema); } drop(schemas); let attached_dbs = self.attached_databases.read(); let (db, _pager) = attached_dbs .index_to_data .get(&database_id) .expect("Database ID should be valid after resolve_database_id"); let schema = db.schema.lock().clone(); f(&schema) } } } // Get the canonical path for a database given its Database object fn get_canonical_path_for_database(db: &Database) -> String { if db.path == ":memory:" { // For in-memory databases, SQLite shows empty string String::new() } else { // For file databases, try to show the full absolute path if that doesn't fail match std::fs::canonicalize(&db.path) { Ok(abs_path) => abs_path.to_string_lossy().to_string(), Err(_) => db.path.to_string(), } } } /// List all databases (main + attached) with their sequence numbers, names, and file paths /// Returns a vector of tuples: (seq_number, name, file_path) pub fn list_all_databases(&self) -> Vec<(usize, String, String)> { let mut databases = Vec::new(); // Add main database (always seq=0, name="main") let main_path = Self::get_canonical_path_for_database(&self.db); databases.push((0, "main".to_string(), main_path)); // Add attached databases let attached_dbs = self.attached_databases.read(); for (alias, &seq_number) in attached_dbs.name_to_index.iter() { let file_path = if let Some((db, _pager)) = attached_dbs.index_to_data.get(&seq_number) { Self::get_canonical_path_for_database(db) } else { String::new() }; databases.push((seq_number, alias.clone(), file_path)); } // Sort by sequence number to ensure consistent ordering databases.sort_by_key(|&(seq, _, _)| seq); databases } pub fn get_pager(&self) -> Arc { self.pager.load().clone() } pub fn get_query_only(&self) -> bool { self.is_query_only() } pub fn set_query_only(&self, value: bool) { self.query_only.store(value, Ordering::SeqCst); self.bump_prepare_context_generation(); } pub fn get_dml_require_where(&self) -> bool { self.dml_require_where.load(Ordering::SeqCst) } pub fn set_dml_require_where(&self, value: bool) { self.dml_require_where.store(value, Ordering::SeqCst); } pub fn get_sync_mode(&self) -> SyncMode { self.sync_mode.get() } pub fn set_sync_mode(&self, mode: SyncMode) { self.sync_mode.set(mode); self.bump_prepare_context_generation(); } pub fn get_temp_store(&self) -> crate::TempStore { self.temp_store.get() } pub fn set_temp_store(&self, value: crate::TempStore) { self.temp_store.set(value); } pub fn get_data_sync_retry(&self) -> bool { self.data_sync_retry .load(crate::sync::atomic::Ordering::SeqCst) } pub fn set_data_sync_retry(&self, value: bool) { self.data_sync_retry .store(value, crate::sync::atomic::Ordering::SeqCst); self.bump_prepare_context_generation(); } /// Get the sync type setting. pub fn get_sync_type(&self) -> crate::io::FileSyncType { self.pager.load().get_sync_type() } /// Set the sync type (for PRAGMA fullfsync). pub fn set_sync_type(&self, value: crate::io::FileSyncType) { self.pager.load().set_sync_type(value); } /// Creates a HashSet of modules that have been loaded pub fn get_syms_vtab_mods(&self) -> HashSet { self.syms.read().vtab_modules.keys().cloned().collect() } /// Returns external (extension) functions: (name, is_aggregate, argc) pub fn get_syms_functions(&self) -> Vec<(String, bool, i32)> { self.syms .read() .functions .values() .map(|f| { let is_agg = matches!(f.func, function::ExtFunc::Aggregate { .. }); let argc = match &f.func { function::ExtFunc::Aggregate { argc, .. } => *argc as i32, function::ExtFunc::Scalar(_) => -1, }; (f.name.clone(), is_agg, argc) }) .collect() } pub(crate) fn database_ptr(&self) -> usize { Arc::as_ptr(&self.db) as usize } pub fn set_encryption_key(&self, key: EncryptionKey) -> Result<()> { tracing::trace!("setting encryption key for connection"); *self.encryption_key.write() = Some(key); self.bump_prepare_context_generation(); self.set_encryption_context() } pub fn set_encryption_cipher(&self, cipher_mode: CipherMode) -> Result<()> { tracing::trace!("setting encryption cipher for connection"); self.encryption_cipher_mode.set(cipher_mode); self.bump_prepare_context_generation(); self.set_encryption_context() } pub fn set_reserved_bytes(&self, reserved_bytes: u8) -> Result<()> { let pager = self.pager.load(); pager.set_reserved_space_bytes(reserved_bytes); Ok(()) } /// Get the reserved bytes value from the pager cache. /// Returns None if not yet set (database not initialized). pub fn get_reserved_bytes(&self) -> Option { let pager = self.pager.load(); pager.get_reserved_space() } pub fn get_encryption_cipher_mode(&self) -> Option { match self.encryption_cipher_mode.get() { CipherMode::None => None, mode => Some(mode), } } // if both key and cipher are set, set encryption context on pager fn set_encryption_context(&self) -> Result<()> { let key_guard = self.encryption_key.read(); let Some(key) = key_guard.as_ref() else { return Ok(()); }; let cipher_mode = self.get_encryption_cipher_mode(); let Some(cipher_mode) = cipher_mode else { return Ok(()); }; tracing::trace!("setting encryption ctx for connection"); let pager = self.pager.load(); if pager.is_encryption_ctx_set() { return Err(LimboError::InvalidArgument( "cannot reset encryption attributes if already set in the session".to_string(), )); } pager.set_encryption_context(cipher_mode, key) } /// Sets a custom busy handler callback. pub fn set_busy_handler(&self, handler: Option) { *self.busy_handler.write() = match handler { Some(callback) => BusyHandler::Custom { callback }, None => BusyHandler::None, }; self.bump_prepare_context_generation(); } /// Sets maximum total accumulated timeout. If the duration is Zero, we unset the busy handler. pub fn set_busy_timeout(&self, duration: Duration) { *self.busy_handler.write() = if duration.is_zero() { BusyHandler::None } else { BusyHandler::Timeout(duration) }; self.bump_prepare_context_generation(); } /// Get the busy timeout duration. pub fn get_busy_timeout(&self) -> Duration { match &*self.busy_handler.read() { BusyHandler::Timeout(d) => *d, _ => Duration::ZERO, } } /// Get a reference to the busy handler. pub fn get_busy_handler(&self) -> crate::sync::RwLockReadGuard<'_, BusyHandler> { self.busy_handler.read() } pub(crate) fn set_tx_state(&self, state: TransactionState) { self.transaction_state.set(state); } pub(crate) fn get_tx_state(&self) -> TransactionState { self.transaction_state.get() } /// Returns true if the connection is currently in a write transaction. /// Used by index methods to determine if it's safe to flush writes. pub fn is_in_write_tx(&self) -> bool { matches!(self.get_tx_state(), TransactionState::Write { .. }) } pub(crate) fn get_mv_tx_id(&self) -> Option { self.mv_tx.read().map(|(tx_id, _)| tx_id) } pub(crate) fn get_mv_tx(&self) -> Option<(u64, TransactionMode)> { *self.mv_tx.read() } #[inline(always)] pub(crate) fn set_mv_tx(&self, tx_id_and_mode: Option<(u64, TransactionMode)>) { tracing::debug!("set_mv_tx: {:?}", tx_id_and_mode); *self.mv_tx.write() = tx_id_and_mode; } /// Get MVCC transaction ID for a specific database. /// Uses fast path for main DB, O(1) HashMap lookup for attached DBs. pub(crate) fn get_mv_tx_id_for_db(&self, db: usize) -> Option { if !crate::is_attached_db(db) { self.get_mv_tx_id() } else { self.attached_mv_txs .read() .get(&db) .map(|(tx_id, _)| *tx_id) } } /// Get MVCC transaction ID and mode for a specific database. pub(crate) fn get_mv_tx_for_db(&self, db: usize) -> Option<(u64, TransactionMode)> { if !crate::is_attached_db(db) { self.get_mv_tx() } else { self.attached_mv_txs.read().get(&db).copied() } } /// Set MVCC transaction for a specific database. pub(crate) fn set_mv_tx_for_db(&self, db: usize, val: Option<(u64, TransactionMode)>) { if !crate::is_attached_db(db) { self.set_mv_tx(val); } else { let mut txs = self.attached_mv_txs.write(); match val { Some(v) => { txs.insert(db, v); } None => { txs.remove(&db); } } } } /// Rollback MVCC transactions on all attached databases and clear the /// attached transaction list. When `clear_schemas` is true the /// connection-local schema cache for each attached DB is also removed so /// that post-rollback queries see the committed schema. /// /// This is the single source of truth for attached-MVCC rollback logic — /// callers in `close()`, `rollback_current_txn()`, and `op_auto_commit` /// should all delegate here. pub(crate) fn rollback_attached_mvcc_txs(&self, clear_schemas: bool) { let txs: HashMap = self.attached_mv_txs.read().clone(); for (&db_id, &(tx_id, _mode)) in &txs { if let Some(attached_mv_store) = self.mv_store_for_db(db_id) { let attached_pager = self.get_pager_from_database_index(&db_id); if attached_mv_store.is_tx_rollbackable(tx_id) { attached_mv_store.rollback_tx(tx_id, attached_pager.clone(), self, db_id); } else { self.set_mv_tx_for_db(db_id, None); } if clear_schemas { self.database_schemas().write().remove(&db_id); } attached_pager.end_read_tx(); } } self.attached_mv_txs.write().clear(); } /// Rollback WAL-mode transactions on all attached databases and discard /// their connection-local schema caches. MVCC-enabled attached databases /// are skipped — those are handled by `rollback_attached_mvcc_txs`. pub(crate) fn rollback_attached_wal_txns(&self) { let attached_pagers = self.get_all_attached_pagers_with_index(); // Collect WAL-mode db_ids first, then batch the schema removal under // a single write lock to avoid per-iteration lock contention. let wal_pagers: SmallVec<[(usize, Arc); 4]> = attached_pagers .into_iter() .filter(|(db_id, _)| self.mv_store_for_db(*db_id).is_none()) .collect(); if !wal_pagers.is_empty() { let mut schemas = self.database_schemas().write(); for (db_id, _) in &wal_pagers { schemas.remove(db_id); } } for (_, attached_pager) in &wal_pagers { attached_pager.rollback_attached(); } } /// Iterate over all attached MVCC transactions, calling `f(db_id, tx_id)` for each. pub(crate) fn for_each_attached_mv_tx(&self, mut f: impl FnMut(usize, u64)) { let txs = self.attached_mv_txs.read(); for (&db_id, &(tx_id, _)) in txs.iter() { f(db_id, tx_id); } } /// Get the next attached MVCC transaction. /// Returns an arbitrary entry from `attached_mv_txs`, or `None` if empty. pub(crate) fn next_attached_mv_tx(&self) -> Option<(usize, u64, TransactionMode)> { self.attached_mv_txs .read() .iter() .next() .map(|(&db_id, &(tx_id, mode))| (db_id, tx_id, mode)) } /// Get the MvStore for a specific database. /// Returns None for databases without MVCC or for bootstrap connections. pub(crate) fn mv_store_for_db(&self, db: usize) -> Option> { if self.is_mvcc_bootstrap_connection() { return None; } if !crate::is_attached_db(db) { self.db.get_mv_store().as_ref().cloned() } else { let catalog = self.attached_databases.read(); catalog .index_to_data .get(&db) .and_then(|(db, _)| db.get_mv_store().as_ref().cloned()) } } pub(crate) fn set_mvcc_checkpoint_threshold(&self, threshold: i64) -> Result<()> { match self.db.get_mv_store().as_ref() { Some(mv_store) => { mv_store.set_checkpoint_threshold(threshold); self.bump_prepare_context_generation(); Ok(()) } None => Err(LimboError::InternalError("MVCC not enabled".into())), } } pub(crate) fn mvcc_checkpoint_threshold(&self) -> Result { match self.db.get_mv_store().as_ref() { Some(mv_store) => Ok(mv_store.checkpoint_threshold()), None => Err(LimboError::InternalError("MVCC not enabled".into())), } } } pub type Row = vdbe::Row; pub type StepResult = vdbe::StepResult; #[derive(Default)] pub struct SymbolTable { pub functions: HashMap>, pub vtabs: HashMap>, pub vtab_modules: HashMap>, pub index_methods: HashMap>, } impl std::fmt::Debug for SymbolTable { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SymbolTable") .field("functions", &self.functions) .finish() } } fn is_shared_library(path: &std::path::Path) -> bool { path.extension() .is_some_and(|ext| ext == "so" || ext == "dylib" || ext == "dll") } pub fn resolve_ext_path(extpath: &str) -> Result { let path = std::path::Path::new(extpath); if !path.exists() { if is_shared_library(path) { return Err(LimboError::ExtensionError(format!( "Extension file not found: {extpath}" ))); }; let maybe = path.with_extension(std::env::consts::DLL_EXTENSION); maybe.exists().then_some(maybe).ok_or_else(|| { LimboError::ExtensionError(format!("Extension file not found: {extpath}")) }) } else { Ok(path.to_path_buf()) } } impl SymbolTable { pub fn new() -> Self { Self { functions: HashMap::default(), vtabs: HashMap::default(), vtab_modules: HashMap::default(), index_methods: HashMap::default(), } } pub fn resolve_function( &self, name: &str, _arg_count: usize, ) -> Option> { self.functions.get(name).cloned() } pub fn extend(&mut self, other: &SymbolTable) { for (name, func) in &other.functions { self.functions.insert(name.clone(), func.clone()); } for (name, vtab) in &other.vtabs { self.vtabs.insert(name.clone(), vtab.clone()); } for (name, module) in &other.vtab_modules { self.vtab_modules.insert(name.clone(), module.clone()); } for (name, module) in &other.index_methods { self.index_methods.insert(name.clone(), module.clone()); } } } ================================================ FILE: core/dbpage.rs ================================================ use crate::storage::pager::Pager; use crate::sync::Arc; use crate::sync::RwLock; use crate::util::IOExt; use crate::vtab::{InternalVirtualTable, InternalVirtualTableCursor}; use crate::{Connection, LimboError, Result, Value}; use turso_ext::{ ConstraintInfo, ConstraintOp, ConstraintUsage, IndexInfo, OrderByInfo, ResultCode, }; pub const DBPAGE_TABLE_NAME: &str = "sqlite_dbpage"; #[derive(Debug)] pub struct DbPageTable; impl Default for DbPageTable { fn default() -> Self { Self::new() } } impl DbPageTable { pub fn new() -> Self { Self } } impl InternalVirtualTable for DbPageTable { fn name(&self) -> String { DBPAGE_TABLE_NAME.to_string() } fn sql(&self) -> String { "CREATE TABLE sqlite_dbpage(pgno INTEGER PRIMARY KEY, data BLOB, schema HIDDEN)".to_string() } fn open(&self, conn: Arc) -> Result>> { let pager = conn.get_pager(); let cursor = DbPageCursor::new(pager); Ok(Arc::new(RwLock::new(cursor))) } /// TODO: sqlite does where_onerow optimization using idx_flag, we should do that eventually.. probably not needed for now. /// Analyzes query constraints and returns cost estimates to help pick the best query plan. /// /// We encode constraint info into `idx_num` as a bitmask, which `filter()` later uses: /// - Bit 0 (0x1): equality on `pgno` - enables single-page lookup /// - Bit 1 (0x2): equality on `schema` - we only support "main", so we let Turso handle filtering if the user provides a different schema. fn best_index( &self, constraints: &[ConstraintInfo], _order_by: &[OrderByInfo], ) -> std::result::Result { let mut idx_num = 0; let mut estimated_cost = 1_000_000.0; let constraint_usages = constraints .iter() .map(|constraint| { let mut usage = ConstraintUsage { argv_index: None, omit: false, }; if constraint.op == ConstraintOp::Eq { match constraint.column_index { // pgno column 0 => { idx_num |= 1; usage.argv_index = Some(1); usage.omit = true; estimated_cost = 1.0; } // schema column 2 => { idx_num |= 2; usage.argv_index = Some(if (idx_num & 1) != 0 { 2 } else { 1 }); usage.omit = true; } _ => {} } } usage }) .collect(); let index_info = IndexInfo { idx_num, idx_str: None, order_by_consumed: false, estimated_cost, estimated_rows: if (idx_num & 1) != 0 { 1 } else { 1_000_000 }, constraint_usages, }; Ok(index_info) } } pub struct DbPageCursor { pager: Arc, pgno: i64, mx_pgno: i64, /// If true, schema constraint was for non-"main" schema, so return no rows schema_mismatch: bool, } impl DbPageCursor { fn new(pager: Arc) -> Self { Self { pager, pgno: 1, mx_pgno: 0, schema_mismatch: false, } } } impl InternalVirtualTableCursor for DbPageCursor { /// iterates based on constraints identified by `best_index()`. /// /// When `idx_num` has bit 0 set, we do a single-page lookup using `args[0]` as the page number. /// If the requested page is out of range (≤0 or beyond db size), the scan returns empty. /// Otherwise, we do a full table scan over all pages starting from page 1. fn filter(&mut self, args: &[Value], _idx_str: Option, idx_num: i32) -> Result { self.schema_mismatch = false; let db_size = self .pager .io .block(|| self.pager.with_header(|header| header.database_size.get()))?; self.mx_pgno = db_size as i64; let mut arg_idx = 0; if (idx_num & 1) != 0 { let pgno = if let Some(Value::Numeric(crate::numeric::Numeric::Integer(val))) = args.get(arg_idx) { *val } else { 0 }; arg_idx += 1; if pgno > 0 && pgno <= self.mx_pgno { self.pgno = pgno; self.mx_pgno = pgno; } else { self.mx_pgno = 0; } } else { self.pgno = 1; } if (idx_num & 2) != 0 { if let Some(Value::Text(schema)) = args.get(arg_idx) { if schema.as_str() != "main" { self.schema_mismatch = true; return Ok(false); } } } Ok(self.pgno <= self.mx_pgno) } fn next(&mut self) -> Result { if self.schema_mismatch { return Ok(false); } self.pgno += 1; Ok(self.pgno <= self.mx_pgno) } fn column(&self, column: usize) -> Result { match column { 0 => Ok(Value::from_i64(self.pgno)), 1 => { // check for the pending byte page - this only needs when db is more than 1 gb. if let Some(pending_page) = self.pager.pending_byte_page_id() { if self.pgno == pending_page as i64 { let page_size = self.pager.usable_space() + self.pager.get_reserved_space().unwrap_or(0) as usize; return Ok(Value::from_blob(vec![0u8; page_size])); } } let (page_ref, completion) = self.pager.read_page(self.pgno)?; if let Some(c) = completion { self.pager.io.wait_for_completion(c)?; } let page_contents = page_ref.get_contents(); let data_slice = page_contents.as_ptr(); Ok(Value::from_blob(data_slice.to_vec())) } 2 => Ok(Value::from_text("main")), // we don't support multiple databases - todo when we do _ => Ok(Value::Null), } } fn rowid(&self) -> i64 { self.pgno } } fn parse_rowid(value: &Value) -> Result> { match value { Value::Null => Ok(None), Value::Numeric(crate::numeric::Numeric::Integer(i)) => Ok(Some(*i)), _ => Err(LimboError::InvalidArgument( "sqlite_dbpage rowid must be an integer".to_string(), )), } } fn ensure_main_schema(value: &Value) -> Result<()> { match value { Value::Null => Ok(()), Value::Text(schema) if schema.as_str() == "main" => Ok(()), Value::Text(schema) => Err(LimboError::InvalidArgument(format!( "sqlite_dbpage only supports main schema (got {})", schema.as_str() ))), _ => Err(LimboError::InvalidArgument( "sqlite_dbpage schema must be text or null".to_string(), )), } } pub(crate) fn update_dbpage(pager: &Arc, args: &[Value]) -> Result> { if args.len() < 2 { return Err(LimboError::InternalError( "sqlite_dbpage update expects at least 2 arguments".to_string(), )); } let old_rowid = parse_rowid(&args[0])?; let new_rowid = parse_rowid(&args[1])?; if old_rowid.is_some() && new_rowid.is_none() { return Err(LimboError::InvalidArgument( "sqlite_dbpage does not support DELETE".to_string(), )); } let columns = if args.len() > 2 { &args[2..] } else { &[] }; let column_pgno = columns.first().and_then(|value| value.as_int()); let column_data = columns.get(1); let column_schema = columns.get(2); let target_pgno = match (new_rowid, old_rowid, column_pgno) { (Some(rowid), _, Some(pgno)) if rowid != pgno => { return Err(LimboError::InvalidArgument( "sqlite_dbpage pgno does not match rowid".to_string(), )); } (Some(rowid), _, _) => rowid, (None, Some(rowid), Some(pgno)) if rowid != pgno => { return Err(LimboError::InvalidArgument( "sqlite_dbpage pgno does not match rowid".to_string(), )); } (None, Some(rowid), _) => rowid, (None, None, Some(pgno)) => pgno, _ => { return Err(LimboError::InvalidArgument( "sqlite_dbpage requires a target page number".to_string(), )) } }; if target_pgno <= 0 { return Err(LimboError::InvalidArgument( "sqlite_dbpage pgno must be positive".to_string(), )); } if let Some(schema) = column_schema { ensure_main_schema(schema)?; } let data = match column_data { Some(Value::Blob(blob)) => blob.as_slice(), Some(Value::Null) | None => { return Err(LimboError::InvalidArgument( "sqlite_dbpage requires data for updates".to_string(), )) } _ => { return Err(LimboError::InvalidArgument( "sqlite_dbpage data must be a blob".to_string(), )) } }; let db_size = pager .io .block(|| pager.with_header(|header| header.database_size.get()))?; if target_pgno as u64 > db_size as u64 { return Err(LimboError::InvalidArgument(format!( "sqlite_dbpage pgno {target_pgno} is out of range" ))); } if let Some(pending_page) = pager.pending_byte_page_id() { if target_pgno == pending_page as i64 { return Err(LimboError::InvalidArgument( "sqlite_dbpage cannot write the pending byte page".to_string(), )); } } let expected_len = pager.usable_space() + pager.get_reserved_space().unwrap_or(0) as usize; if data.len() != expected_len { return Err(LimboError::InvalidArgument(format!( "sqlite_dbpage data length must be {expected_len} bytes" ))); } let (page_ref, completion) = pager.read_page(target_pgno)?; if let Some(c) = completion { pager.io.wait_for_completion(c)?; } pager.add_dirty(&page_ref)?; let contents = page_ref.get_contents(); let buffer = contents .buffer .as_ref() .expect("sqlite_dbpage page buffer should be loaded"); buffer.as_mut_slice().copy_from_slice(data); let is_insert = old_rowid.is_none(); Ok(if is_insert { Some(target_pgno) } else { None }) } ================================================ FILE: core/error.rs ================================================ use thiserror::Error; use crate::storage::page_cache::CacheError; #[derive(Debug, Clone, Error, miette::Diagnostic)] pub enum LimboError { #[error("Corrupt database: {0}")] Corrupt(String), #[error("File is not a database")] NotADB, #[error("Internal error: {0}")] InternalError(String), #[error(transparent)] CacheError(#[from] CacheError), #[error("Database is full: {0}")] DatabaseFull(String), #[error("Parse error: {0}")] ParseError(String), #[error(transparent)] #[diagnostic(transparent)] LexerError(#[from] turso_parser::error::Error), #[error("Conversion error: {0}")] ConversionError(String), #[error("Env variable error: {0}")] EnvVarError(#[from] std::env::VarError), #[error("Transaction error: {0}")] TxError(String), #[error(transparent)] CompletionError(#[from] CompletionError), #[error("Locking error: {0}")] LockingError(String), #[error("Parse error: {0}")] ParseIntError(#[from] std::num::ParseIntError), #[error("Parse error: {0}")] ParseFloatError(#[from] std::num::ParseFloatError), #[error("Parse error: {0}")] InvalidDate(String), #[error("Parse error: {0}")] InvalidTime(String), #[error("Modifier parsing error: {0}")] InvalidModifier(String), #[error("Invalid argument supplied: {0}")] InvalidArgument(String), #[error("Invalid formatter supplied: {0}")] InvalidFormatter(String), #[error("Runtime error: {0}")] Constraint(String), #[error("Runtime error: {0}")] /// We need to specify for ROLLBACK|FAIL resolve types when to roll the tx back /// so instead of matching on the string, we introduce a specific ForeignKeyConstraint error ForeignKeyConstraint(String), #[error("Runtime error: {1}")] Raise(turso_parser::ast::ResolveType, String), #[error("RaiseIgnore")] RaiseIgnore, #[error("Extension error: {0}")] ExtensionError(String), #[error("Runtime error: integer overflow")] IntegerOverflow, #[error("Runtime error: string or blob too big")] TooBig, #[error("Runtime error: database table is locked")] TableLocked, #[error("Error: Resource is read-only")] ReadOnly, #[error("Database is busy")] Busy, #[error("interrupt")] Interrupt, #[error("Database snapshot is stale. You must rollback and retry the whole transaction.")] BusySnapshot, #[error("Conflict: {0}")] Conflict(String), #[error("Database schema changed")] SchemaUpdated, #[error("Database schema conflict")] SchemaConflict, #[error( "Database is empty, header does not exist - page 1 should've been allocated before this" )] Page1NotAlloc, #[error("Transaction terminated")] TxTerminated, #[error("Write-write conflict")] WriteWriteConflict, #[error("Commit dependency aborted")] CommitDependencyAborted, #[error("No such transaction ID: {0}")] NoSuchTransactionID(String), #[error("Null value")] NullValue, #[error("invalid column type")] InvalidColumnType, #[error("Invalid blob size, expected {0}")] InvalidBlobSize(usize), #[error("Planning error: {0}")] PlanningError(String), #[error("Checkpoint failed: {0}")] CheckpointFailed(String), #[error("Unsupported text encoding: {0}. Only UTF-8 is supported.")] UnsupportedEncoding(String), } #[cfg(target_family = "unix")] impl From for LimboError { fn from(value: rustix::io::Errno) -> Self { CompletionError::from(value).into() } } #[cfg(all(target_os = "linux", feature = "io_uring"))] impl From<&'static str> for LimboError { fn from(value: &'static str) -> Self { CompletionError::UringIOError(value).into() } } #[derive(Debug, Copy, Clone, PartialEq, Error)] pub enum CompletionError { #[error("I/O error ({1}): {0}")] IOError(std::io::ErrorKind, &'static str), #[cfg(target_family = "unix")] #[error("I/O error: {0}")] RustixIOError(#[from] rustix::io::Errno), #[cfg(all(target_os = "linux", feature = "io_uring"))] #[error("I/O error: {0}")] // TODO: if needed create an enum for IO Uring errors so that we don't have to pass strings around UringIOError(&'static str), #[error("Completion was aborted")] Aborted, #[error("Decryption failed for page={page_idx}")] DecryptionError { page_idx: usize }, #[error("I/O error: partial write")] ShortWrite, #[error("I/O error: short read on page {page_idx}: expected {expected} bytes, got {actual}")] ShortRead { page_idx: usize, expected: usize, actual: usize, }, #[error("I/O error: short read on WAL frame at offset {offset}: expected {expected} bytes, got {actual}")] ShortReadWalFrame { offset: u64, expected: usize, actual: usize, }, #[error("Checksum mismatch on page {page_id}: expected {expected}, got {actual}")] ChecksumMismatch { page_id: usize, expected: u64, actual: u64, }, #[error("tursodb not compiled with checksum feature")] ChecksumNotEnabled, } /// Convert a `std::io::Error` into a `LimboError` with an operation label. pub fn io_error(e: std::io::Error, op: &'static str) -> LimboError { LimboError::CompletionError(CompletionError::IOError(e.kind(), op)) } #[cold] // makes all branches that return errors marked as unlikely pub(crate) const fn cold_return(v: T) -> T { v } #[macro_export] macro_rules! bail_parse_error { ($($arg:tt)*) => { return $crate::error::cold_return(Err($crate::error::LimboError::ParseError(format!($($arg)*)))) }; } #[macro_export] macro_rules! bail_corrupt_error { ($($arg:tt)*) => { return $crate::error::cold_return(Err($crate::error::LimboError::Corrupt(format!($($arg)*)))) }; } /// Bounds-checked buffer slicing that returns `LimboError::Corrupt` on out-of-bounds. /// /// Accepts any range expression: `buf, pos..`, `buf, start..end`, etc. #[macro_export] macro_rules! slice_in_bounds_or_corrupt { ($buf:expr, $range:expr) => { $buf.get($range).ok_or_else(|| { $crate::error::cold_return($crate::error::LimboError::Corrupt(format!( "range {:?} out of bounds for buffer size {}", $range, $buf.len() ))) })? }; } /// Asserts a condition or bails with `LimboError::Corrupt`. /// /// Usage: /// `assert_or_bail_corrupt!(condition, "message {}", arg)` #[macro_export] macro_rules! assert_or_bail_corrupt { ($cond:expr, $($arg:tt)*) => { if !($cond) { $crate::bail_corrupt_error!($($arg)*); } }; } #[macro_export] macro_rules! bail_constraint_error { ($($arg:tt)*) => { return $crate::error::cold_return(Err($crate::error::LimboError::Constraint(format!($($arg)*)))) }; } impl From for LimboError { fn from(err: turso_ext::ResultCode) -> Self { cold_return(LimboError::ExtensionError(err.to_string())) } } pub const SQLITE_ERROR: usize = 1; pub const SQLITE_CONSTRAINT: usize = 19; pub const SQLITE_CONSTRAINT_CHECK: usize = SQLITE_CONSTRAINT | (1 << 8); pub const SQLITE_CONSTRAINT_PRIMARYKEY: usize = SQLITE_CONSTRAINT | (6 << 8); #[allow(dead_code)] pub const SQLITE_CONSTRAINT_FOREIGNKEY: usize = SQLITE_CONSTRAINT | (3 << 8); pub const SQLITE_CONSTRAINT_NOTNULL: usize = SQLITE_CONSTRAINT | (5 << 8); pub const SQLITE_CONSTRAINT_TRIGGER: usize = SQLITE_CONSTRAINT | (7 << 8); pub const SQLITE_FULL: usize = 13; // we want this in autoincrement - incase if user inserts max allowed int pub const SQLITE_CONSTRAINT_UNIQUE: usize = 2067; ================================================ FILE: core/ext/dynamic.rs ================================================ use crate::{ ext::{register_aggregate_function, register_scalar_function, register_vtab_module}, Connection, LimboError, }; #[cfg(not(target_family = "wasm"))] use libloading::{Library, Symbol}; use std::{ ffi::{c_char, CString}, sync::{Arc, Mutex, OnceLock}, }; use turso_ext::{ExtensionApi, ExtensionApiRef, ExtensionEntryPoint, ResultCode, VfsImpl}; #[cfg(not(target_family = "wasm"))] type ExtensionStore = Vec<(Arc, ExtensionApiRef)>; #[cfg(not(target_family = "wasm"))] static EXTENSIONS: OnceLock>> = OnceLock::new(); #[cfg(not(target_family = "wasm"))] pub fn get_extension_libraries() -> Arc> { EXTENSIONS .get_or_init(|| Arc::new(Mutex::new(Vec::new()))) .clone() } type Vfs = (String, Arc); static VFS_MODULES: OnceLock>> = OnceLock::new(); #[derive(Clone, Debug)] pub struct VfsMod { pub ctx: *const VfsImpl, } unsafe impl Send for VfsMod {} unsafe impl Sync for VfsMod {} crate::assert::assert_send_sync!(VfsMod); impl Connection { #[cfg(not(target_family = "wasm"))] pub fn load_extension>( self: &Arc, path: P, ) -> crate::Result<()> { use turso_ext::ExtensionApiRef; let api = Box::new(unsafe { self._build_turso_ext() }); let lib = unsafe { Library::new(path).map_err(|e| LimboError::ExtensionError(e.to_string()))? }; let entry: Symbol = unsafe { lib.get(b"register_extension") .map_err(|e| LimboError::ExtensionError(e.to_string()))? }; let api_ptr: *const ExtensionApi = Box::into_raw(api); let api_ref = ExtensionApiRef { api: api_ptr }; let result_code = unsafe { entry(api_ptr) }; if result_code.is_ok() { let extensions = get_extension_libraries(); extensions .lock() .map_err(|_| { LimboError::ExtensionError("Error locking extension libraries".to_string()) })? .push((Arc::new(lib), api_ref)); if self.is_db_initialized() { self.reparse_schema_after_extension_load()?; } Ok(()) } else { if !api_ptr.is_null() { let _ = unsafe { Box::from_raw(api_ptr.cast_mut()) }; } Err(LimboError::ExtensionError( "Extension registration failed".to_string(), )) } } } #[allow(clippy::arc_with_non_send_sync)] pub(crate) unsafe extern "C" fn register_vfs( name: *const c_char, vfs: *const VfsImpl, ) -> ResultCode { if name.is_null() || vfs.is_null() { return ResultCode::Error; } let c_str = unsafe { CString::from_raw(name as *mut _) }; let name_str = match c_str.to_str() { Ok(s) => s.to_string(), Err(_) => return ResultCode::Error, }; add_vfs_module(name_str, Arc::new(VfsMod { ctx: vfs })); ResultCode::OK } /// Get pointers to all the vfs extensions that need to be built in at compile time. /// any other types that are defined in the same extension will not be registered /// until the database file is opened and `register_builtins` is called. #[cfg(feature = "fs")] #[allow(clippy::arc_with_non_send_sync)] pub fn add_builtin_vfs_extensions( api: Option, ) -> crate::Result)>> { use turso_ext::VfsInterface; let mut vfslist: Vec<*const VfsImpl> = Vec::new(); let mut api = match api { None => ExtensionApi { ctx: std::ptr::null_mut(), register_scalar_function, register_aggregate_function, register_vtab_module, vfs_interface: VfsInterface { register_vfs, builtin_vfs: vfslist.as_mut_ptr(), builtin_vfs_count: 0, }, }, Some(mut api) => { api.vfs_interface.builtin_vfs = vfslist.as_mut_ptr(); api } }; register_static_vfs_modules(&mut api); let mut vfslist = Vec::with_capacity(api.vfs_interface.builtin_vfs_count as usize); let slice = unsafe { std::slice::from_raw_parts_mut( api.vfs_interface.builtin_vfs, api.vfs_interface.builtin_vfs_count as usize, ) }; for vfs in slice { if vfs.is_null() { continue; } let vfsimpl = unsafe { &**vfs }; let name = unsafe { CString::from_raw(vfsimpl.name as *mut _) .to_str() .map_err(|_| { LimboError::ExtensionError("unable to register vfs extension".to_string()) })? .to_string() }; vfslist.push(( name, Arc::new(VfsMod { ctx: vfsimpl as *const _, }), )); } Ok(vfslist) } #[allow(dead_code)] #[cfg(feature = "fs")] fn register_static_vfs_modules(_api: &mut ExtensionApi) { /* Placeholder for any VFS modules to build in at compile time */ } pub fn add_vfs_module(name: String, vfs: Arc) { let mut modules = VFS_MODULES .get_or_init(|| Mutex::new(Vec::new())) .lock() .unwrap(); if !modules.iter().any(|v| v.0 == name) { modules.push((name, vfs)); } } pub fn list_vfs_modules() -> Vec { VFS_MODULES .get_or_init(|| Mutex::new(Vec::new())) .lock() .unwrap() .iter() .map(|v| v.0.clone()) .collect() } pub fn get_vfs_modules() -> Vec { VFS_MODULES .get_or_init(|| Mutex::new(Vec::new())) .lock() .unwrap() .clone() } ================================================ FILE: core/ext/mod.rs ================================================ #[cfg(feature = "fs")] mod dynamic; mod vtab_xconnect; use crate::index_method::backing_btree::BackingBtreeIndexMethod; #[cfg(all(feature = "fts", not(target_family = "wasm")))] use crate::index_method::fts::{FtsIndexMethod, FTS_INDEX_METHOD_NAME}; use crate::index_method::toy_vector_sparse_ivf::VectorSparseInvertedIndexMethod; use crate::index_method::{ BACKING_BTREE_INDEX_METHOD_NAME, TOY_VECTOR_SPARSE_IVF_INDEX_METHOD_NAME, }; use crate::schema::{Schema, Table}; use crate::sync::atomic::{AtomicU64, Ordering}; use crate::sync::Mutex; #[cfg(all(target_os = "linux", feature = "io_uring", not(miri)))] use crate::UringIO; #[cfg(all(target_os = "windows", feature = "experimental_win_iocp", not(miri)))] use crate::WindowsIOCP; use crate::{function::ExternalFunc, Connection, Database}; use crate::{vtab::VirtualTable, SymbolTable}; #[cfg(feature = "fs")] use crate::{LimboError, IO}; #[cfg(feature = "fs")] pub use dynamic::{add_builtin_vfs_extensions, add_vfs_module, list_vfs_modules, VfsMod}; use std::{ ffi::{c_char, c_void, CStr, CString}, sync::Arc, }; use turso_ext::{ ExtensionApi, InitAggFunction, ResultCode, ScalarFunction, VTabKind, VTabModuleImpl, }; pub use turso_ext::{FinalizeFunction, StepFunction, Value as ExtValue, ValueType as ExtValueType}; pub use vtab_xconnect::{execute, prepare_stmt}; /// The context passed to extensions to register with Core /// along with the function pointers #[repr(C)] pub struct ExtensionCtx { syms: *mut SymbolTable, schema: *mut c_void, /// We must bump the prepare context generation so prepared statements /// know they need to be reprepared after extension registration. prepare_context_generation: *const AtomicU64, } pub(crate) unsafe extern "C" fn register_vtab_module( ctx: *mut c_void, name: *const c_char, module: VTabModuleImpl, kind: VTabKind, ) -> ResultCode { if name.is_null() || ctx.is_null() { return ResultCode::Error; } let c_str = unsafe { CString::from_raw(name as *mut c_char) }; let name_str = match c_str.to_str() { Ok(s) => s.to_string(), Err(_) => return ResultCode::Error, }; let ext_ctx = unsafe { &mut *(ctx as *mut ExtensionCtx) }; let module = Arc::new(module); let vmodule = VTabImpl { module_kind: kind, implementation: module, }; unsafe { let syms = &mut *ext_ctx.syms; syms.vtab_modules.insert(name_str.clone(), vmodule.into()); if !ext_ctx.prepare_context_generation.is_null() { (*ext_ctx.prepare_context_generation).fetch_add(1, Ordering::Release); } if kind == VTabKind::TableValuedFunction { if let Ok(vtab) = VirtualTable::function(&name_str, syms) { let table = Arc::new(Table::Virtual(vtab)); let mutex = &*(ext_ctx.schema as *mut Mutex>); let mut guard = mutex.lock(); let schema = Arc::make_mut(&mut *guard); schema.tables.insert(name_str, table); } else { return ResultCode::Error; } } } ResultCode::OK } #[derive(Clone)] pub struct VTabImpl { pub module_kind: VTabKind, pub implementation: Arc, } pub(crate) unsafe extern "C" fn register_scalar_function( ctx: *mut c_void, name: *const c_char, func: ScalarFunction, ) -> ResultCode { let c_str = unsafe { CStr::from_ptr(name) }; let name_str = match c_str.to_str() { Ok(s) => s.to_string(), Err(_) => return ResultCode::InvalidArgs, }; if ctx.is_null() { return ResultCode::Error; } let ext_ctx = unsafe { &mut *(ctx as *mut ExtensionCtx) }; unsafe { (*ext_ctx.syms).functions.insert( name_str.clone(), Arc::new(ExternalFunc::new_scalar(name_str, func)), ); if !ext_ctx.prepare_context_generation.is_null() { (*ext_ctx.prepare_context_generation).fetch_add(1, Ordering::Release); } } ResultCode::OK } pub(crate) unsafe extern "C" fn register_aggregate_function( ctx: *mut c_void, name: *const c_char, args: i32, init_func: InitAggFunction, step_func: StepFunction, finalize_func: FinalizeFunction, ) -> ResultCode { let c_str = unsafe { CStr::from_ptr(name) }; let name_str = match c_str.to_str() { Ok(s) => s.to_string(), Err(_) => return ResultCode::InvalidArgs, }; if ctx.is_null() { return ResultCode::Error; } let ext_ctx = unsafe { &mut *(ctx as *mut ExtensionCtx) }; unsafe { (*ext_ctx.syms).functions.insert( name_str.clone(), Arc::new(ExternalFunc::new_aggregate( name_str, args, (init_func, step_func, finalize_func), )), ); if !ext_ctx.prepare_context_generation.is_null() { (*ext_ctx.prepare_context_generation).fetch_add(1, Ordering::Release); } } ResultCode::OK } impl Database { #[cfg(feature = "fs")] #[allow(clippy::arc_with_non_send_sync, dead_code)] pub fn open_with_vfs( &self, path: &str, vfs: &str, ) -> crate::Result<(Arc, Arc)> { use crate::{MemoryIO, SyscallIO}; use dynamic::get_vfs_modules; let io: Arc = match vfs { "memory" => Arc::new(MemoryIO::new()), "syscall" => Arc::new(SyscallIO::new()?), #[cfg(all(target_os = "linux", feature = "io_uring", not(miri)))] "io_uring" => Arc::new(UringIO::new()?), #[cfg(all(target_os = "windows", feature = "experimental_win_iocp", not(miri)))] "experimental_win_iocp" => Arc::new(WindowsIOCP::new()?), other => match get_vfs_modules().iter().find(|v| v.0 == vfs) { Some((_, vfs)) => vfs.clone(), None => { return Err(LimboError::InvalidArgument(format!("no such VFS: {other}"))); } }, }; let db = Self::open_file(io.clone(), path)?; Ok((io, db)) } /// Register any built-in extensions that can be stored on the Database so we do not have /// to register these once-per-connection, and the connection can just extend its symbol table pub fn register_global_builtin_extensions(&self) -> Result<(), String> { { let mut syms = self.builtin_syms.write(); syms.index_methods.insert( TOY_VECTOR_SPARSE_IVF_INDEX_METHOD_NAME.to_string(), Arc::new(VectorSparseInvertedIndexMethod), ); syms.index_methods.insert( BACKING_BTREE_INDEX_METHOD_NAME.to_string(), Arc::new(BackingBtreeIndexMethod), ); #[cfg(all(feature = "fts", not(target_family = "wasm")))] syms.index_methods .insert(FTS_INDEX_METHOD_NAME.to_string(), Arc::new(FtsIndexMethod)); } let syms = self.builtin_syms.data_ptr(); // Pass the mutex pointer and the appropriate handler let schema_mutex_ptr = &*self.schema as *const Mutex> as *mut Mutex>; let ctx = Box::into_raw(Box::new(ExtensionCtx { syms, schema: schema_mutex_ptr as *mut c_void, prepare_context_generation: std::ptr::null(), })); #[allow(unused)] let mut ext_api = ExtensionApi { ctx: ctx as *mut c_void, register_scalar_function, register_aggregate_function, register_vtab_module, #[cfg(feature = "fs")] vfs_interface: turso_ext::VfsInterface { register_vfs: dynamic::register_vfs, builtin_vfs: std::ptr::null_mut(), builtin_vfs_count: 0, }, }; #[cfg(feature = "uuid")] crate::uuid::register_extension(&mut ext_api); #[cfg(feature = "series")] crate::series::register_extension(&mut ext_api); #[cfg(feature = "time")] crate::time::register_extension(&mut ext_api); crate::regexp::register_extension(&mut ext_api); #[cfg(feature = "fs")] { let vfslist = add_builtin_vfs_extensions(Some(ext_api)).map_err(|e| e.to_string())?; for (name, vfs) in vfslist { add_vfs_module(name, vfs); } } let _ = unsafe { Box::from_raw(ctx) }; Ok(()) } } impl Connection { /// Build the connection's extension api context for manually registering an extension. /// you probably want to use `Connection::load_extension(path)`. /// /// # Safety /// Only to be used when registering a staticly linked extension manually. /// You should only ever call this method on your applications startup, /// The caller is responsible for calling `_free_extension_ctx` after registering the /// extension. /// /// usage: /// ```ignore /// let ext_api = conn._build_turso_ext(); /// unsafe { /// my_extension::register_extension(&mut ext_api); /// conn._free_extension_ctx(ext_api); /// } ///``` pub unsafe fn _build_turso_ext(&self) -> ExtensionApi { let schema_mutex_ptr = &*self.db.schema as *const Mutex> as *mut Mutex>; let ctx = ExtensionCtx { syms: self.syms.data_ptr(), schema: schema_mutex_ptr as *mut c_void, prepare_context_generation: &self.prepare_context_generation as *const _, }; let ctx = Box::into_raw(Box::new(ctx)) as *mut c_void; ExtensionApi { ctx, register_scalar_function, register_aggregate_function, register_vtab_module, #[cfg(feature = "fs")] vfs_interface: turso_ext::VfsInterface { register_vfs: dynamic::register_vfs, builtin_vfs: std::ptr::null_mut(), builtin_vfs_count: 0, }, } } /// Free the connection's extension libary context after registering an extension manually. /// # Safety /// Only to be used if you have previously called Connection::build_turso_ext pub unsafe fn _free_extension_ctx(&self, api: ExtensionApi) { if api.ctx.is_null() { return; } let _ = unsafe { Box::from_raw(api.ctx as *mut ExtensionCtx) }; } } ================================================ FILE: core/ext/vtab_xconnect.rs ================================================ use crate::{types::Value, Connection, LimboError, Statement}; use std::{ boxed::Box, ffi::{c_char, c_void, CStr, CString}, num::NonZeroUsize, ptr, sync::Weak, }; use turso_ext::{Conn as ExtConn, ResultCode, Stmt, Value as ExtValue}; /// Wrapper around core Connection::execute with optional arguments to bind /// to the statment This function takes ownership of the optional turso_ext::Value array if provided pub unsafe extern "C" fn execute( ctx: *mut ExtConn, sql: *const c_char, args: *mut ExtValue, arg_count: i32, last_insert_rowid: *mut i64, ) -> ResultCode { let c_str = unsafe { CStr::from_ptr(sql as *mut c_char) }; let sql_str = match c_str.to_str() { Ok(s) => s.to_string(), Err(_) => { tracing::error!("query: failed to convert sql to string"); return ResultCode::Error; } }; let Ok(extcon) = ExtConn::from_ptr(ctx) else { tracing::error!("query: null connection"); return ResultCode::Error; }; if extcon._ctx.is_null() { tracing::error!("execute: connection ctx is null"); return ResultCode::Error; } let weak_box = extcon._ctx as *const Weak; let weak = unsafe { &*weak_box }; if let Some(conn) = weak.upgrade() { match conn.query(&sql_str) { Ok(Some(mut stmt)) => { if arg_count > 0 { let args_slice = &mut std::slice::from_raw_parts_mut(args, arg_count as usize); for (i, val) in args_slice.iter_mut().enumerate() { stmt.bind_at( NonZeroUsize::new(i + 1).unwrap(), Value::from_ffi(std::mem::take(val)).unwrap_or(Value::Null), ); } } let result = stmt.run_with_row_callback(|_| { Err(crate::LimboError::InternalError(String::from( "execute used for query returning a row", ))) }); let rc = match result { Ok(_) => { *last_insert_rowid = conn.last_insert_rowid(); ResultCode::OK } Err(err) => match err { crate::LimboError::Busy => ResultCode::Busy, crate::LimboError::Interrupt => ResultCode::Interrupt, _ => { tracing::error!("execute: failed to execute query: {:?}", err); ResultCode::Error } }, }; return rc; } Ok(None) => tracing::error!("query: no statement returned"), Err(e) => tracing::error!("query: failed to execute query: {:?}", e), }; } ResultCode::Error } /// Wraps core Connection::prepare with a custom Stmt object with the necessary function pointers. /// This object is boxed/leaked and the caller is responsible for freeing the memory. pub unsafe extern "C" fn prepare_stmt(ctx: *mut ExtConn, sql: *const c_char) -> *mut Stmt { let c_str = unsafe { CStr::from_ptr(sql as *mut c_char) }; let sql_str = match c_str.to_str() { Ok(s) => s.to_string(), Err(_) => { tracing::error!("prepare_stmt: failed to convert sql to string"); return ptr::null_mut(); } }; let Ok(extcon) = ExtConn::from_ptr(ctx) else { tracing::error!("prepare_stmt: null connection"); return ptr::null_mut(); }; if extcon._ctx.is_null() { tracing::error!("prepare_stmt: null connection ctx"); return ptr::null_mut(); } let weak_box = extcon._ctx as *const Weak; let weak = unsafe { &*weak_box }; if let Some(conn) = weak.upgrade() { match conn.prepare(&sql_str) { Ok(stmt) => { let raw_stmt = Box::into_raw(Box::new(stmt)) as *mut c_void; Box::into_raw(Box::new(Stmt::new( extcon._ctx, raw_stmt, stmt_bind_args_fn, stmt_step, stmt_get_row, stmt_get_column_names, stmt_free_current_row, stmt_close, ))) } Err(e) => { tracing::error!("prepare_stmt: failed to prepare statement: {:?}", e); ptr::null_mut() } } } else { tracing::error!("failed to upgrade stored connection on vtable module"); ptr::null_mut() } } /// This function expects 1 based indexing. Wraps core statement bind_at functionality /// this function does not take ownership of the provided arg value pub unsafe extern "C" fn stmt_bind_args_fn(ctx: *mut Stmt, idx: i32, arg: ExtValue) -> ResultCode { let Ok(stmt) = Stmt::from_ptr(ctx) else { tracing::error!("prepare_stmt: null stmt pointer"); return ResultCode::Error; }; let stmt_ctx: &mut Statement = unsafe { &mut *(stmt._ctx as *mut Statement) }; // from_ffi takes ownership let Ok(owned_val) = Value::from_ffi(arg) else { tracing::error!("stmt_bind_args_fn: failed to convert arg to Value"); return ResultCode::Error; }; let Some(idx) = NonZeroUsize::new(idx as usize) else { tracing::error!("stmt_bind_args_fn: invalid index"); return ResultCode::Error; }; stmt_ctx.bind_at(idx, owned_val); ResultCode::OK } /// Wraps the functionality of the core Statement::step function, /// preferring to handle the IO step result internally to prevent having to expose /// run_once. Returns the equivalent ResultCode which then maps to an external StepResult. /// This function is blocking pub unsafe extern "C" fn stmt_step(stmt: *mut Stmt) -> ResultCode { let Ok(stmt) = Stmt::from_ptr(stmt) else { tracing::error!("stmt_step: failed to convert stmt to Stmt"); return ResultCode::Error; }; if stmt._conn.is_null() || stmt._ctx.is_null() { tracing::error!("stmt_step: null connection or context"); return ResultCode::Error; } let stmt_ctx: &mut Statement = unsafe { &mut *(stmt._ctx as *mut Statement) }; let res = stmt_ctx.run_one_step_blocking(|| Ok(()), || Ok(())); match res { Ok(Some(_)) => ResultCode::Row, Ok(None) => { // Done ResultCode::EOF } Err(LimboError::Interrupt) => ResultCode::Interrupt, Err(LimboError::Busy) => ResultCode::Busy, Err(_) => ResultCode::Error, } } /// Instead of returning a pointer to the row, sets the Stmt's 'cursor'/current_row /// to the next result row, and then the caller can access the resulting value on the Stmt. pub unsafe extern "C" fn stmt_get_row(ctx: *mut Stmt) { let Ok(stmt) = Stmt::from_ptr(ctx) else { tracing::error!("stmt_get_row: failed to convert stmt to Stmt"); return; }; if !stmt.current_row.is_null() { stmt.free_current_row(); } let stmt_ctx: &mut Statement = unsafe { &mut *(stmt._ctx as *mut Statement) }; if let Some(row) = stmt_ctx.row() { let values = row.get_values(); let mut owned_values = Vec::with_capacity(row.len()); for value in values { owned_values.push(Value::to_ffi(value)); } stmt.current_row = Box::into_raw(owned_values.into_boxed_slice()) as *mut ExtValue; stmt.current_row_len = row.len() as i32; } else { stmt.current_row_len = 0; } } /// Free the memory of the current row/cursor of the Stmt object. pub unsafe extern "C" fn stmt_free_current_row(ctx: *mut Stmt) { let Ok(stmt) = Stmt::from_ptr(ctx) else { return; }; if !stmt.current_row.is_null() { let values: &mut [ExtValue] = std::slice::from_raw_parts_mut(stmt.current_row, stmt.current_row_len as usize); for value in values.iter_mut() { let owned_value = std::mem::take(value); owned_value.__free_internal_type(); } let _ = Box::from_raw(stmt.current_row); } } /// Provides an easier API to get all the result column names associated with /// the prepared Statement. The caller is responsible for freeing the memory pub unsafe extern "C" fn stmt_get_column_names( ctx: *mut Stmt, count: *mut i32, ) -> *mut *mut c_char { if !count.is_null() { *count = 0; } let Ok(stmt) = Stmt::from_ptr(ctx) else { tracing::error!("stmt_get_column_names: null Stmt pointer"); return ptr::null_mut(); }; let stmt_ctx: &mut Statement = unsafe { &mut *(stmt._ctx as *mut Statement) }; let num_cols = stmt_ctx.num_columns(); if num_cols == 0 { tracing::info!("stmt_get_column_names: no columns"); return ptr::null_mut(); } let mut names: Vec<*mut c_char> = Vec::with_capacity(num_cols); // collect all the column names and convert them to C strings to send back for i in 0..num_cols { let name = stmt_ctx.get_column_name(i); match CString::new(name.as_bytes()) { Ok(cstr) => names.push(cstr.into_raw()), Err(_) => { // fall-back: free what we allocated so far for p in names { let _ = CString::from_raw(p); } return std::ptr::null_mut(); } } } if !count.is_null() { *count = names.len() as i32; } Box::into_raw(names.into_boxed_slice()) as *mut *mut c_char } /// Ffi/extension wrapper around core Statement::reset and /// cleans up resources associated with the Statement pub unsafe extern "C" fn stmt_close(stmt: *mut Stmt) { if stmt.is_null() { return; } let mut wrapper = Box::from_raw(stmt); if wrapper._ctx.is_null() { // already closed return; } // clean up the current row if it exists if !wrapper.current_row.is_null() { wrapper.free_current_row(); } // free the managed internal context let mut internal = Box::::from_raw(wrapper._ctx.cast()); internal.reset_best_effort(); } ================================================ FILE: core/fast_lock.rs ================================================ use crate::sync::atomic::{AtomicBool, Ordering}; use crate::thread::spin_loop; use std::{ cell::UnsafeCell, ops::{Deref, DerefMut}, }; #[derive(Debug)] pub struct SpinLock { locked: AtomicBool, value: UnsafeCell, } pub struct SpinLockGuard<'a, T> { lock: &'a SpinLock, } impl Drop for SpinLockGuard<'_, T> { fn drop(&mut self) { self.lock.locked.store(false, Ordering::Release); } } impl Deref for SpinLockGuard<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { unsafe { &*self.lock.value.get() } } } impl DerefMut for SpinLockGuard<'_, T> { fn deref_mut(&mut self) -> &mut T { unsafe { &mut *self.lock.value.get() } } } unsafe impl Send for SpinLock {} unsafe impl Sync for SpinLock {} impl SpinLock { pub fn new(value: T) -> Self { Self { locked: AtomicBool::new(false), value: UnsafeCell::new(value), } } pub fn lock(&self) -> SpinLockGuard<'_, T> { while self.locked.swap(true, Ordering::Acquire) { spin_loop(); } SpinLockGuard { lock: self } } pub fn into_inner(self) -> UnsafeCell { self.value } } #[cfg(test)] mod tests { use crate::sync::Arc; use super::SpinLock; #[test] fn test_fast_lock_multiple_thread_sum() { let lock = Arc::new(SpinLock::new(0)); let mut threads = vec![]; const NTHREADS: usize = 1000; for _ in 0..NTHREADS { let lock = lock.clone(); threads.push(std::thread::spawn(move || { let mut guard = lock.lock(); *guard += 1; })); } for thread in threads { thread.join().unwrap(); } assert_eq!(*lock.lock(), NTHREADS); } } #[cfg(all(shuttle, test))] mod shuttle_tests { use super::*; use crate::sync::*; use crate::thread; /// Test basic mutual exclusion with counter increment #[test] fn shuttle_spinlock_counter() { shuttle::check_random( || { let lock = Arc::new(SpinLock::new(0)); let mut threads = vec![]; const NTHREADS: usize = 3; for _ in 0..NTHREADS { let lock = lock.clone(); threads.push(thread::spawn(move || { let mut guard = lock.lock(); *guard += 1; })); } for thread in threads { thread.join().unwrap(); } assert_eq!(*lock.lock(), NTHREADS); }, 1000, ); } /// Test that lock provides mutual exclusion - no two threads hold lock simultaneously #[test] fn shuttle_spinlock_mutual_exclusion() { shuttle::check_random( || { let lock = Arc::new(SpinLock::new(())); let in_critical_section = Arc::new(AtomicBool::new(false)); let mut threads = vec![]; for _ in 0..3 { let lock = lock.clone(); let in_cs = in_critical_section.clone(); threads.push(thread::spawn(move || { let _guard = lock.lock(); // If another thread is in critical section, this is a bug assert!( !in_cs.swap(true, Ordering::SeqCst), "Two threads in critical section!" ); // Simulate some work thread::yield_now(); in_cs.store(false, Ordering::SeqCst); })); } for thread in threads { thread.join().unwrap(); } }, 1000, ); } /// Test multiple lock/unlock cycles per thread #[test] fn shuttle_spinlock_multiple_cycles() { shuttle::check_random( || { let lock = Arc::new(SpinLock::new(0i32)); let mut threads = vec![]; for _ in 0..2 { let lock = lock.clone(); threads.push(thread::spawn(move || { for _ in 0..3 { let mut guard = lock.lock(); *guard += 1; // Guard dropped here, releasing lock } })); } for thread in threads { thread.join().unwrap(); } // 2 threads * 3 iterations = 6 assert_eq!(*lock.lock(), 6); }, 1000, ); } /// Test that guard properly releases lock on drop #[test] fn shuttle_spinlock_guard_drop() { shuttle::check_random( || { let lock = Arc::new(SpinLock::new(0)); let lock1 = lock.clone(); let t1 = thread::spawn(move || { { let mut guard = lock1.lock(); *guard = 1; // guard dropped here } // After drop, another thread should be able to acquire }); let lock2 = lock.clone(); let t2 = thread::spawn(move || { let mut guard = lock2.lock(); *guard = 2; }); t1.join().unwrap(); t2.join().unwrap(); // Value should be 1 or 2 depending on order, but lock should be acquirable let val = *lock.lock(); assert!(val == 1 || val == 2); }, 1000, ); } /// Test read-modify-write pattern under contention #[test] fn shuttle_spinlock_read_modify_write() { shuttle::check_random( || { let lock = Arc::new(SpinLock::new(vec![0i32; 3])); let mut threads = vec![]; for i in 0..3 { let lock = lock.clone(); threads.push(thread::spawn(move || { let mut guard = lock.lock(); // Read current value, modify, write back guard[i] += 1; })); } for thread in threads { thread.join().unwrap(); } let guard = lock.lock(); assert_eq!(*guard, vec![1, 1, 1]); }, 1000, ); } /// Test lock acquisition order doesn't cause starvation (probabilistic) #[test] fn shuttle_spinlock_no_starvation() { shuttle::check_random( || { let lock = Arc::new(SpinLock::new(Vec::::new())); let mut threads = vec![]; for id in 0..3 { let lock = lock.clone(); threads.push(thread::spawn(move || { let mut guard = lock.lock(); guard.push(id); })); } for thread in threads { thread.join().unwrap(); } // All threads should have acquired the lock exactly once let guard = lock.lock(); assert_eq!(guard.len(), 3); let mut sorted = guard.clone(); sorted.sort(); assert_eq!(sorted, vec![0, 1, 2]); }, 1000, ); } /// Test nested-style access pattern (reacquire after release) #[test] fn shuttle_spinlock_reacquire() { shuttle::check_random( || { let lock = Arc::new(SpinLock::new(0)); let lock1 = lock.clone(); let t1 = thread::spawn(move || { { let mut guard = lock1.lock(); *guard += 1; } // Release and reacquire { let mut guard = lock1.lock(); *guard += 1; } }); let lock2 = lock.clone(); let t2 = thread::spawn(move || { let mut guard = lock2.lock(); *guard += 10; }); t1.join().unwrap(); t2.join().unwrap(); // Should be 12 (1 + 1 + 10) assert_eq!(*lock.lock(), 12); }, 1000, ); } } ================================================ FILE: core/function.rs ================================================ use crate::sync::Arc; use std::fmt; use std::fmt::{Debug, Display}; use strum::IntoEnumIterator; use turso_ext::{FinalizeFunction, InitAggFunction, ScalarFunction, StepFunction}; use crate::LimboError; pub trait Deterministic: std::fmt::Display { fn is_deterministic(&self) -> bool; } pub struct ExternalFunc { pub name: String, pub func: ExtFunc, } impl Deterministic for ExternalFunc { fn is_deterministic(&self) -> bool { // external functions can be whatever so let's just default to false false } } #[derive(Debug, Clone)] pub enum ExtFunc { Scalar(ScalarFunction), Aggregate { argc: usize, init: InitAggFunction, step: StepFunction, finalize: FinalizeFunction, }, } impl ExtFunc { pub fn agg_args(&self) -> Result { if let ExtFunc::Aggregate { argc, .. } = self { return Ok(*argc); } Err(()) } } impl ExternalFunc { pub fn new_scalar(name: String, func: ScalarFunction) -> Self { Self { name, func: ExtFunc::Scalar(func), } } pub fn new_aggregate( name: String, argc: i32, func: (InitAggFunction, StepFunction, FinalizeFunction), ) -> Self { Self { name, func: ExtFunc::Aggregate { argc: argc as usize, init: func.0, step: func.1, finalize: func.2, }, } } } impl Debug for ExternalFunc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name) } } impl Display for ExternalFunc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name) } } #[cfg(feature = "json")] #[derive(Debug, Clone, PartialEq, strum::EnumIter)] pub enum JsonFunc { Json, Jsonb, JsonArray, JsonbArray, JsonArrayLength, JsonArrowExtract, JsonArrowShiftExtract, JsonExtract, JsonbExtract, JsonObject, JsonbObject, JsonType, JsonErrorPosition, JsonValid, JsonPatch, JsonbPatch, JsonRemove, JsonbRemove, JsonReplace, JsonbReplace, JsonInsert, JsonbInsert, JsonPretty, JsonSet, JsonbSet, JsonQuote, } #[cfg(feature = "json")] impl Deterministic for JsonFunc { fn is_deterministic(&self) -> bool { true } } #[cfg(feature = "json")] impl Display for JsonFunc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", match self { Self::Json => "json", Self::Jsonb => "jsonb", Self::JsonArray => "json_array", Self::JsonbArray => "jsonb_array", Self::JsonExtract => "json_extract", Self::JsonbExtract => "jsonb_extract", Self::JsonArrayLength => "json_array_length", Self::JsonArrowExtract => "->", Self::JsonArrowShiftExtract => "->>", Self::JsonObject => "json_object", Self::JsonbObject => "jsonb_object", Self::JsonType => "json_type", Self::JsonErrorPosition => "json_error_position", Self::JsonValid => "json_valid", Self::JsonPatch => "json_patch", Self::JsonbPatch => "jsonb_patch", Self::JsonRemove => "json_remove", Self::JsonbRemove => "jsonb_remove", Self::JsonReplace => "json_replace", Self::JsonbReplace => "jsonb_replace", Self::JsonInsert => "json_insert", Self::JsonbInsert => "jsonb_insert", Self::JsonPretty => "json_pretty", Self::JsonSet => "json_set", Self::JsonbSet => "jsonb_set", Self::JsonQuote => "json_quote", } ) } } #[cfg(feature = "json")] impl JsonFunc { /// Returns true for operator-style entries that should not appear in PRAGMA function_list. pub fn is_internal(&self) -> bool { matches!(self, Self::JsonArrowExtract | Self::JsonArrowShiftExtract) } pub fn arities(&self) -> &'static [i32] { match self { Self::Json | Self::Jsonb | Self::JsonQuote | Self::JsonErrorPosition | Self::JsonValid => &[1], Self::JsonPatch | Self::JsonbPatch => &[2], Self::JsonArrayLength | Self::JsonType => &[1, 2], // Operators — filtered out, arity doesn't matter Self::JsonArrowExtract | Self::JsonArrowShiftExtract => &[2], // Variable-arg _ => &[-1], } } } #[derive(Debug, Clone, strum::EnumIter)] pub enum VectorFunc { Vector, Vector32, Vector32Sparse, Vector64, Vector8, Vector1Bit, VectorExtract, VectorDistanceCos, VectorDistanceL2, VectorDistanceJaccard, VectorDistanceDot, VectorConcat, VectorSlice, } impl Deterministic for VectorFunc { fn is_deterministic(&self) -> bool { true } } impl Display for VectorFunc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let str = match self { Self::Vector => "vector", Self::Vector32 => "vector32", Self::Vector32Sparse => "vector32_sparse", Self::Vector64 => "vector64", Self::Vector8 => "vector8", Self::Vector1Bit => "vector1bit", Self::VectorExtract => "vector_extract", Self::VectorDistanceCos => "vector_distance_cos", Self::VectorDistanceL2 => "vector_distance_l2", Self::VectorDistanceJaccard => "vector_distance_jaccard", Self::VectorDistanceDot => "vector_distance_dot", Self::VectorConcat => "vector_concat", Self::VectorSlice => "vector_slice", }; write!(f, "{str}") } } impl VectorFunc { pub fn arities(&self) -> &'static [i32] { match self { Self::Vector | Self::Vector32 | Self::Vector32Sparse | Self::Vector64 | Self::Vector8 | Self::Vector1Bit | Self::VectorExtract => &[1], Self::VectorDistanceCos | Self::VectorDistanceL2 | Self::VectorDistanceJaccard | Self::VectorDistanceDot => &[2], Self::VectorSlice => &[3], Self::VectorConcat => &[-1], } } } /// Full-text search functions #[cfg(all(feature = "fts", not(target_family = "wasm")))] #[derive(Debug, Clone, PartialEq, strum::EnumIter)] pub enum FtsFunc { /// fts_score(col1, col2, ..., query): computes FTS relevance score /// When used with an FTS index, the optimizer routes through the index method Score, /// fts_match(col1, col2, ..., query): returns true if document matches query /// Used in WHERE clause for filtering rows by FTS match Match, /// fts_highlight(text, query, before_tag, after_tag): returns text with matching terms highlighted /// Wraps matching query terms in the text with before_tag and after_tag markers Highlight, } #[cfg(all(feature = "fts", not(target_family = "wasm")))] impl FtsFunc { pub fn is_deterministic(&self) -> bool { true } pub fn arities(&self) -> &'static [i32] { match self { Self::Highlight => &[4], // Score and Match take variable columns + query Self::Score | Self::Match => &[-1], } } } #[cfg(all(feature = "fts", not(target_family = "wasm")))] impl Display for FtsFunc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let str = match self { Self::Score => "fts_score", Self::Match => "fts_match", Self::Highlight => "fts_highlight", }; write!(f, "{str}") } } #[derive(Debug, Clone, strum::EnumIter)] pub enum AggFunc { Avg, Count, Count0, GroupConcat, Max, Min, StringAgg, Sum, Total, #[cfg(feature = "json")] JsonbGroupArray, #[cfg(feature = "json")] JsonGroupArray, #[cfg(feature = "json")] JsonbGroupObject, #[cfg(feature = "json")] JsonGroupObject, ArrayAgg, #[strum(disabled)] External(Arc), } #[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter)] pub enum WindowFunc { RowNumber, } impl WindowFunc { pub fn arities(&self) -> &'static [i32] { match self { Self::RowNumber => &[0], } } } impl Deterministic for WindowFunc { fn is_deterministic(&self) -> bool { true } } impl std::fmt::Display for WindowFunc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::RowNumber => write!(f, "row_number"), } } } impl PartialEq for AggFunc { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Avg, Self::Avg) | (Self::Count, Self::Count) | (Self::GroupConcat, Self::GroupConcat) | (Self::Max, Self::Max) | (Self::Min, Self::Min) | (Self::StringAgg, Self::StringAgg) | (Self::Sum, Self::Sum) | (Self::Total, Self::Total) | (Self::ArrayAgg, Self::ArrayAgg) => true, (Self::External(a), Self::External(b)) => Arc::ptr_eq(a, b), _ => false, } } } impl Deterministic for AggFunc { fn is_deterministic(&self) -> bool { false // consider aggregate functions nondeterministic since they depend on the number of rows, not only the input arguments } } impl std::fmt::Display for AggFunc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.as_str()) } } impl AggFunc { pub fn num_args(&self) -> usize { match self { Self::Avg => 1, Self::Count0 => 0, Self::Count => 1, Self::GroupConcat => 1, Self::Max => 1, Self::Min => 1, Self::StringAgg => 2, Self::Sum => 1, Self::Total => 1, Self::ArrayAgg => 1, #[cfg(feature = "json")] Self::JsonGroupArray | Self::JsonbGroupArray => 1, #[cfg(feature = "json")] Self::JsonGroupObject | Self::JsonbGroupObject => 2, Self::External(func) => func.agg_args().unwrap_or(0), } } /// Returns all valid arities for this aggregate function. /// Most aggregates have a single arity, but group_concat accepts 1 or 2 args. pub fn arities(&self) -> &'static [i32] { match self { Self::Avg => &[1], Self::Count0 => &[0], Self::Count => &[1], Self::GroupConcat => &[1, 2], Self::Max => &[1], Self::Min => &[1], Self::StringAgg => &[2], Self::Sum => &[1], Self::Total => &[1], Self::ArrayAgg => &[1], #[cfg(feature = "json")] Self::JsonGroupArray | Self::JsonbGroupArray => &[1], #[cfg(feature = "json")] Self::JsonGroupObject | Self::JsonbGroupObject => &[2], Self::External(_) => &[-1], } } pub fn as_str(&self) -> &'static str { match self { Self::Avg => "avg", Self::Count0 => "count", Self::Count => "count", Self::GroupConcat => "group_concat", Self::Max => "max", Self::Min => "min", Self::StringAgg => "string_agg", Self::Sum => "sum", Self::Total => "total", Self::ArrayAgg => "array_agg", #[cfg(feature = "json")] Self::JsonbGroupArray => "jsonb_group_array", #[cfg(feature = "json")] Self::JsonGroupArray => "json_group_array", #[cfg(feature = "json")] Self::JsonbGroupObject => "jsonb_group_object", #[cfg(feature = "json")] Self::JsonGroupObject => "json_group_object", Self::External(_) => "extension function", } } } #[derive(Debug, Clone, PartialEq, strum::EnumIter)] pub enum ScalarFunc { Cast, Changes, Char, Coalesce, Concat, ConcatWs, Glob, IfNull, Iif, Instr, Like, Abs, Upper, Lower, Random, RandomBlob, Trim, LTrim, RTrim, Round, Length, OctetLength, Min, Max, Nullif, Sign, Substr, Substring, Soundex, Date, Time, TotalChanges, DateTime, Typeof, Unicode, Quote, SqliteVersion, TursoVersion, SqliteSourceId, UnixEpoch, JulianDay, Hex, Unhex, ZeroBlob, LastInsertRowid, Replace, #[cfg(feature = "fs")] #[cfg(not(target_family = "wasm"))] LoadExtension, StrfTime, Printf, Likely, TimeDiff, Likelihood, TableColumnsJsonArray, BinRecordJsonObject, Attach, Detach, Unlikely, StatInit, StatPush, StatGet, ConnTxnId, IsAutocommit, // Test type functions (for custom type system testing) TestUintEncode, TestUintDecode, TestUintAdd, TestUintSub, TestUintMul, TestUintDiv, TestUintLt, TestUintEq, StringReverse, // Built-in type support functions BooleanToInt, IntToBoolean, ValidateIpAddr, // Numeric type functions NumericEncode, NumericDecode, NumericAdd, NumericSub, NumericMul, NumericDiv, NumericLt, NumericEq, // Array construction / element access (desugared from ARRAY[…] and expr[n] syntax) Array, ArrayElement, ArraySetElement, // Array utility functions ArrayLength, ArrayAppend, ArrayPrepend, ArrayCat, ArrayRemove, ArrayContains, ArrayPosition, ArraySlice, StringToArray, ArrayToString, ArrayOverlap, ArrayContainsAll, } impl Deterministic for ScalarFunc { fn is_deterministic(&self) -> bool { match self { ScalarFunc::Cast => true, ScalarFunc::Changes => false, // depends on DB state ScalarFunc::Char => true, ScalarFunc::Coalesce => true, ScalarFunc::Concat => true, ScalarFunc::ConcatWs => true, ScalarFunc::Glob => true, ScalarFunc::IfNull => true, ScalarFunc::Iif => true, ScalarFunc::Instr => true, ScalarFunc::Like => true, ScalarFunc::Abs => true, ScalarFunc::Upper => true, ScalarFunc::Lower => true, ScalarFunc::Random => false, // duh ScalarFunc::RandomBlob => false, // duh ScalarFunc::Trim => true, ScalarFunc::LTrim => true, ScalarFunc::RTrim => true, ScalarFunc::Round => true, ScalarFunc::Length => true, ScalarFunc::OctetLength => true, ScalarFunc::Min => true, ScalarFunc::Max => true, ScalarFunc::Nullif => true, ScalarFunc::Sign => true, ScalarFunc::Substr => true, ScalarFunc::Substring => true, ScalarFunc::Soundex => true, ScalarFunc::Date => false, ScalarFunc::Time => false, ScalarFunc::TotalChanges => false, ScalarFunc::DateTime => false, ScalarFunc::Typeof => true, ScalarFunc::Unicode => true, ScalarFunc::Quote => true, ScalarFunc::SqliteVersion => false, ScalarFunc::TursoVersion => false, ScalarFunc::SqliteSourceId => false, ScalarFunc::UnixEpoch => false, ScalarFunc::JulianDay => false, ScalarFunc::Hex => true, ScalarFunc::Unhex => true, ScalarFunc::ZeroBlob => true, ScalarFunc::LastInsertRowid => false, ScalarFunc::Replace => true, #[cfg(feature = "fs")] #[cfg(not(target_family = "wasm"))] ScalarFunc::LoadExtension => false, ScalarFunc::StrfTime => false, ScalarFunc::Printf => true, ScalarFunc::Likely => true, ScalarFunc::TimeDiff => false, ScalarFunc::Likelihood => true, ScalarFunc::TableColumnsJsonArray => true, // while columns of the table can change with DDL statements, within single query plan it's static ScalarFunc::BinRecordJsonObject => true, ScalarFunc::Attach => false, // changes database state ScalarFunc::Detach => false, // changes database state ScalarFunc::Unlikely => true, ScalarFunc::StatInit => false, // internal ANALYZE function ScalarFunc::StatPush => false, // internal ANALYZE function ScalarFunc::StatGet => false, // internal ANALYZE function ScalarFunc::ConnTxnId => false, // depends on connection state ScalarFunc::IsAutocommit => false, // depends on connection state ScalarFunc::TestUintEncode | ScalarFunc::TestUintDecode | ScalarFunc::TestUintAdd | ScalarFunc::TestUintSub | ScalarFunc::TestUintMul | ScalarFunc::TestUintDiv | ScalarFunc::TestUintLt | ScalarFunc::TestUintEq | ScalarFunc::StringReverse => true, ScalarFunc::BooleanToInt | ScalarFunc::IntToBoolean | ScalarFunc::ValidateIpAddr | ScalarFunc::NumericEncode | ScalarFunc::NumericDecode | ScalarFunc::NumericAdd | ScalarFunc::NumericSub | ScalarFunc::NumericMul | ScalarFunc::NumericDiv | ScalarFunc::NumericLt | ScalarFunc::NumericEq => true, ScalarFunc::Array | ScalarFunc::ArrayElement | ScalarFunc::ArraySetElement | ScalarFunc::ArrayLength | ScalarFunc::ArrayAppend | ScalarFunc::ArrayPrepend | ScalarFunc::ArrayCat | ScalarFunc::ArrayRemove | ScalarFunc::ArrayContains | ScalarFunc::ArrayPosition | ScalarFunc::ArraySlice | ScalarFunc::StringToArray | ScalarFunc::ArrayToString | ScalarFunc::ArrayOverlap | ScalarFunc::ArrayContainsAll => true, } } } impl ScalarFunc { /// Returns true if this function returns a record-format array blob /// that needs ArrayDecode for display. /// /// FIXME: ideally every function would declare its return type via a /// `return_type()` method, and this whitelist would be replaced by a /// generic check. Postponed for now — the set of array-returning /// functions is small and controlled by us. pub fn returns_array_blob(&self) -> bool { matches!( self, Self::Array | Self::ArraySetElement | Self::ArrayAppend | Self::ArrayPrepend | Self::ArrayCat | Self::ArrayRemove | Self::ArraySlice | Self::StringToArray ) } } impl Display for ScalarFunc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let str = match self { Self::Cast => "cast", Self::Changes => "changes", Self::Char => "char", Self::Coalesce => "coalesce", Self::Concat => "concat", Self::ConcatWs => "concat_ws", Self::Glob => "glob", Self::IfNull => "ifnull", Self::Iif => "iif", Self::Instr => "instr", Self::Like => "like", Self::Abs => "abs", Self::Upper => "upper", Self::Lower => "lower", Self::Random => "random", Self::RandomBlob => "randomblob", Self::Trim => "trim", Self::LTrim => "ltrim", Self::RTrim => "rtrim", Self::Round => "round", Self::Length => "length", Self::OctetLength => "octet_length", Self::Min => "min", Self::Max => "max", Self::Nullif => "nullif", Self::Sign => "sign", Self::Substr => "substr", Self::Substring => "substring", Self::Soundex => "soundex", Self::Date => "date", Self::Time => "time", Self::TotalChanges => "total_changes", Self::Typeof => "typeof", Self::Unicode => "unicode", Self::Quote => "quote", Self::SqliteVersion => "sqlite_version", Self::TursoVersion => "turso_version", Self::SqliteSourceId => "sqlite_source_id", Self::JulianDay => "julianday", Self::UnixEpoch => "unixepoch", Self::Hex => "hex", Self::Unhex => "unhex", Self::ZeroBlob => "zeroblob", Self::LastInsertRowid => "last_insert_rowid", Self::Replace => "replace", Self::DateTime => "datetime", #[cfg(feature = "fs")] #[cfg(not(target_family = "wasm"))] Self::LoadExtension => "load_extension", Self::StrfTime => "strftime", Self::Printf => "printf", Self::Likely => "likely", Self::TimeDiff => "timediff", Self::Likelihood => "likelihood", Self::TableColumnsJsonArray => "table_columns_json_array", Self::BinRecordJsonObject => "bin_record_json_object", Self::Attach => "attach", Self::Detach => "detach", Self::Unlikely => "unlikely", Self::StatInit => "stat_init", Self::StatPush => "stat_push", Self::StatGet => "stat_get", Self::ConnTxnId => "conn_txn_id", Self::IsAutocommit => "is_autocommit", Self::TestUintEncode => "test_uint_encode", Self::TestUintDecode => "test_uint_decode", Self::TestUintAdd => "test_uint_add", Self::TestUintSub => "test_uint_sub", Self::TestUintMul => "test_uint_mul", Self::TestUintDiv => "test_uint_div", Self::TestUintLt => "test_uint_lt", Self::TestUintEq => "test_uint_eq", Self::StringReverse => "string_reverse", Self::BooleanToInt => "boolean_to_int", Self::IntToBoolean => "int_to_boolean", Self::ValidateIpAddr => "validate_ipaddr", Self::NumericEncode => "numeric_encode", Self::NumericDecode => "numeric_decode", Self::NumericAdd => "numeric_add", Self::NumericSub => "numeric_sub", Self::NumericMul => "numeric_mul", Self::NumericDiv => "numeric_div", Self::NumericLt => "numeric_lt", Self::NumericEq => "numeric_eq", Self::Array => "array", Self::ArrayElement => "array_element", Self::ArraySetElement => "array_set_element", Self::ArrayLength => "array_length", Self::ArrayAppend => "array_append", Self::ArrayPrepend => "array_prepend", Self::ArrayCat => "array_cat", Self::ArrayRemove => "array_remove", Self::ArrayContains => "array_contains", Self::ArrayPosition => "array_position", Self::ArraySlice => "array_slice", Self::StringToArray => "string_to_array", Self::ArrayToString => "array_to_string", Self::ArrayOverlap => "array_overlap", Self::ArrayContainsAll => "array_contains_all", }; write!(f, "{str}") } } impl ScalarFunc { /// Returns true for internal functions that should not appear in PRAGMA function_list. pub fn is_internal(&self) -> bool { matches!( self, Self::Cast | Self::Array | Self::ArrayElement | Self::ArraySetElement | Self::StatInit | Self::StatPush | Self::StatGet | Self::Attach | Self::Detach | Self::TableColumnsJsonArray | Self::BinRecordJsonObject | Self::ConnTxnId | Self::IsAutocommit ) } /// Returns the valid arities for this function. /// Each value becomes a separate row in PRAGMA function_list. /// -1 means truly variable arguments (e.g. coalesce, printf). pub fn arities(&self) -> &'static [i32] { match self { // 0-arg Self::Changes | Self::LastInsertRowid | Self::Random | Self::SqliteVersion | Self::TursoVersion | Self::SqliteSourceId | Self::TotalChanges => &[0], // 1-arg Self::Abs | Self::Hex | Self::Length | Self::Lower | Self::OctetLength | Self::Quote | Self::RandomBlob | Self::Sign | Self::Soundex | Self::Typeof | Self::Unicode | Self::Upper | Self::ZeroBlob | Self::Likely | Self::Unlikely => &[1], // 2-arg Self::Glob | Self::Instr | Self::Nullif | Self::IfNull | Self::Likelihood | Self::TimeDiff => &[2], // 3-arg Self::Iif | Self::Replace => &[3], // Multi-arity (one row per valid arity) Self::Like => &[2, 3], Self::Trim | Self::LTrim | Self::RTrim | Self::Round | Self::Unhex => &[1, 2], Self::Substr | Self::Substring => &[2, 3], // Truly variable-arg Self::Char | Self::Coalesce | Self::Concat | Self::ConcatWs | Self::Date | Self::Time | Self::DateTime | Self::UnixEpoch | Self::JulianDay | Self::StrfTime | Self::Printf => &[-1], #[cfg(feature = "fs")] #[cfg(not(target_family = "wasm"))] Self::LoadExtension => &[-1], // Internal functions — arity doesn't matter since they're filtered out Self::Cast | Self::StatInit | Self::StatPush | Self::StatGet | Self::Attach | Self::Detach | Self::TableColumnsJsonArray | Self::BinRecordJsonObject | Self::ConnTxnId | Self::IsAutocommit => &[0], // Scalar max/min (multi-arg) Self::Max | Self::Min => &[-1], // Test functions for custom types (1-arg encode/decode, 2-arg operators) Self::TestUintEncode | Self::TestUintDecode | Self::StringReverse => &[1], Self::TestUintAdd | Self::TestUintSub | Self::TestUintMul | Self::TestUintDiv | Self::TestUintLt | Self::TestUintEq => &[2], // Built-in type functions Self::BooleanToInt | Self::IntToBoolean | Self::ValidateIpAddr | Self::NumericDecode => &[1], Self::NumericAdd | Self::NumericSub | Self::NumericMul | Self::NumericDiv | Self::NumericLt | Self::NumericEq => &[2], Self::NumericEncode => &[3], // Array construction / element access Self::Array => &[-1], // variable arity Self::ArrayElement => &[2], Self::ArraySetElement => &[3], // Array functions Self::ArrayLength => &[1, 2], Self::ArrayAppend | Self::ArrayPrepend | Self::ArrayCat | Self::ArrayRemove | Self::ArrayContains | Self::ArrayPosition | Self::ArrayOverlap | Self::ArrayContainsAll => &[2], Self::ArraySlice => &[3], Self::StringToArray => &[2, 3], Self::ArrayToString => &[2, 3], } } /// Returns true for functions that can turn NULL arguments into a non-NULL result. /// /// This is used by planner/optimizer logic that needs to reason about whether /// predicates are null-rejecting for outer-join simplification. pub fn can_mask_nulls(&self) -> bool { matches!(self, Self::Coalesce | Self::IfNull) } } #[derive(Debug, Clone, PartialEq, strum::EnumIter)] pub enum MathFunc { Acos, Acosh, Asin, Asinh, Atan, Atan2, Atanh, Ceil, Ceiling, Cos, Cosh, Degrees, Exp, Floor, Ln, Log, Log10, Log2, Mod, Pi, Pow, Power, Radians, Sin, Sinh, Sqrt, Tan, Tanh, Trunc, } pub enum MathFuncArity { Nullary, Unary, Binary, UnaryOrBinary, } impl Deterministic for MathFunc { fn is_deterministic(&self) -> bool { true } } impl MathFunc { pub fn arity(&self) -> MathFuncArity { match self { Self::Pi => MathFuncArity::Nullary, Self::Acos | Self::Acosh | Self::Asin | Self::Asinh | Self::Atan | Self::Atanh | Self::Ceil | Self::Ceiling | Self::Cos | Self::Cosh | Self::Degrees | Self::Exp | Self::Floor | Self::Ln | Self::Log10 | Self::Log2 | Self::Radians | Self::Sin | Self::Sinh | Self::Sqrt | Self::Tan | Self::Tanh | Self::Trunc => MathFuncArity::Unary, Self::Atan2 | Self::Mod | Self::Pow | Self::Power => MathFuncArity::Binary, Self::Log => MathFuncArity::UnaryOrBinary, } } pub fn arities(&self) -> &'static [i32] { match self.arity() { MathFuncArity::Nullary => &[0], MathFuncArity::Unary => &[1], MathFuncArity::Binary => &[2], MathFuncArity::UnaryOrBinary => &[1, 2], } } } impl Display for MathFunc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let str = match self { Self::Acos => "acos", Self::Acosh => "acosh", Self::Asin => "asin", Self::Asinh => "asinh", Self::Atan => "atan", Self::Atan2 => "atan2", Self::Atanh => "atanh", Self::Ceil => "ceil", Self::Ceiling => "ceiling", Self::Cos => "cos", Self::Cosh => "cosh", Self::Degrees => "degrees", Self::Exp => "exp", Self::Floor => "floor", Self::Ln => "ln", Self::Log => "log", Self::Log10 => "log10", Self::Log2 => "log2", Self::Mod => "mod", Self::Pi => "pi", Self::Pow => "pow", Self::Power => "power", Self::Radians => "radians", Self::Sin => "sin", Self::Sinh => "sinh", Self::Sqrt => "sqrt", Self::Tan => "tan", Self::Tanh => "tanh", Self::Trunc => "trunc", }; write!(f, "{str}") } } #[derive(Debug, Clone)] pub enum AlterTableFunc { RenameTable, AlterColumn, RenameColumn, } impl Display for AlterTableFunc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AlterTableFunc::RenameTable => write!(f, "limbo_rename_table"), AlterTableFunc::RenameColumn => write!(f, "limbo_rename_column"), AlterTableFunc::AlterColumn => write!(f, "limbo_alter_column"), } } } #[derive(Debug, Clone)] pub enum Func { Agg(AggFunc), Window(WindowFunc), Scalar(ScalarFunc), Math(MathFunc), Vector(VectorFunc), #[cfg(all(feature = "fts", not(target_family = "wasm")))] Fts(FtsFunc), #[cfg(feature = "json")] Json(JsonFunc), AlterTable(AlterTableFunc), External(Arc), } impl Display for Func { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Agg(agg_func) => write!(f, "{}", agg_func.as_str()), Self::Window(window_func) => write!(f, "{window_func}"), Self::Scalar(scalar_func) => write!(f, "{scalar_func}"), Self::Math(math_func) => write!(f, "{math_func}"), Self::Vector(vector_func) => write!(f, "{vector_func}"), #[cfg(all(feature = "fts", not(target_family = "wasm")))] Self::Fts(fts_func) => write!(f, "{fts_func}"), #[cfg(feature = "json")] Self::Json(json_func) => write!(f, "{json_func}"), Self::External(generic_func) => write!(f, "{generic_func}"), Self::AlterTable(alter_func) => write!(f, "{alter_func}"), } } } #[derive(Debug, Clone)] pub struct FuncCtx { pub func: Func, pub arg_count: usize, } impl Deterministic for Func { fn is_deterministic(&self) -> bool { match self { Self::Agg(agg_func) => agg_func.is_deterministic(), Self::Window(window_func) => window_func.is_deterministic(), Self::Scalar(scalar_func) => scalar_func.is_deterministic(), Self::Math(math_func) => math_func.is_deterministic(), Self::Vector(vector_func) => vector_func.is_deterministic(), #[cfg(all(feature = "fts", not(target_family = "wasm")))] Self::Fts(fts_func) => fts_func.is_deterministic(), #[cfg(feature = "json")] Self::Json(json_func) => json_func.is_deterministic(), Self::External(external_func) => external_func.is_deterministic(), Self::AlterTable(_) => true, } } } impl Func { pub fn supports_star_syntax(&self) -> bool { // Functions that need star expansion also support star syntax if self.needs_star_expansion() { return true; } match self { Self::Scalar(scalar_func) => { matches!( scalar_func, ScalarFunc::Changes | ScalarFunc::Random | ScalarFunc::TotalChanges | ScalarFunc::SqliteVersion | ScalarFunc::TursoVersion | ScalarFunc::SqliteSourceId | ScalarFunc::LastInsertRowid ) } Self::Math(math_func) => { matches!(math_func.arity(), MathFuncArity::Nullary) } // Aggregate functions with (*) syntax are handled separately in the planner Self::Agg(_) => false, Self::Window(_) => false, _ => false, } } /// Returns true for functions that can turn NULL arguments into a non-NULL result. /// /// This metadata is currently used by optimizer null-rejection analysis. pub fn can_mask_nulls(&self) -> bool { match self { Self::Scalar(scalar_func) => scalar_func.can_mask_nulls(), _ => false, } } /// Returns true if the function needs the `*` to be expanded to all columns /// from the referenced tables. This is used for functions like `json_object(*)` /// and `jsonb_object(*)` which create a JSON object with column names as keys /// and column values as values. #[cfg(feature = "json")] pub fn needs_star_expansion(&self) -> bool { matches!( self, Self::Json(JsonFunc::JsonObject) | Self::Json(JsonFunc::JsonbObject) ) } #[cfg(not(feature = "json"))] pub fn needs_star_expansion(&self) -> bool { false } pub fn resolve_function(name: &str, arg_count: usize) -> Result { let normalized_name = crate::util::normalize_ident(name); match normalized_name.as_str() { "avg" => { if arg_count != 1 { crate::bail_parse_error!("wrong number of arguments to function {}()", name) } Ok(Self::Agg(AggFunc::Avg)) } "count" => { // Handle both COUNT() and COUNT(expr) cases if arg_count == 0 { Ok(Self::Agg(AggFunc::Count0)) // COUNT() case } else if arg_count == 1 { Ok(Self::Agg(AggFunc::Count)) // COUNT(expr) case } else { crate::bail_parse_error!("wrong number of arguments to function {}()", name) } } "group_concat" => { if arg_count != 1 && arg_count != 2 { println!("{arg_count}"); crate::bail_parse_error!("wrong number of arguments to function {}()", name) } Ok(Self::Agg(AggFunc::GroupConcat)) } "max" if arg_count > 1 => Ok(Self::Scalar(ScalarFunc::Max)), "max" => { if arg_count < 1 { crate::bail_parse_error!("wrong number of arguments to function {}()", name) } Ok(Self::Agg(AggFunc::Max)) } "min" if arg_count > 1 => Ok(Self::Scalar(ScalarFunc::Min)), "min" => { if arg_count < 1 { crate::bail_parse_error!("wrong number of arguments to function {}()", name) } Ok(Self::Agg(AggFunc::Min)) } "nullif" if arg_count == 2 => Ok(Self::Scalar(ScalarFunc::Nullif)), "string_agg" => { if arg_count != 2 { crate::bail_parse_error!("wrong number of arguments to function {}()", name) } Ok(Self::Agg(AggFunc::StringAgg)) } "sum" => { if arg_count != 1 { crate::bail_parse_error!("wrong number of arguments to function {}()", name) } Ok(Self::Agg(AggFunc::Sum)) } "total" => { if arg_count != 1 { crate::bail_parse_error!("wrong number of arguments to function {}()", name) } Ok(Self::Agg(AggFunc::Total)) } "row_number" => { if arg_count != 0 { crate::bail_parse_error!("wrong number of arguments to function {}()", name) } Ok(Self::Window(WindowFunc::RowNumber)) } "timediff" => { if arg_count != 2 { crate::bail_parse_error!("wrong number of arguments to function {}()", name) } Ok(Self::Scalar(ScalarFunc::TimeDiff)) } "array_agg" => Ok(Self::Agg(AggFunc::ArrayAgg)), #[cfg(feature = "json")] "jsonb_group_array" => Ok(Self::Agg(AggFunc::JsonbGroupArray)), #[cfg(feature = "json")] "json_group_array" => Ok(Self::Agg(AggFunc::JsonGroupArray)), #[cfg(feature = "json")] "jsonb_group_object" => Ok(Self::Agg(AggFunc::JsonbGroupObject)), #[cfg(feature = "json")] "json_group_object" => Ok(Self::Agg(AggFunc::JsonGroupObject)), "char" => Ok(Self::Scalar(ScalarFunc::Char)), "coalesce" => Ok(Self::Scalar(ScalarFunc::Coalesce)), "concat" => Ok(Self::Scalar(ScalarFunc::Concat)), "concat_ws" => Ok(Self::Scalar(ScalarFunc::ConcatWs)), "changes" => Ok(Self::Scalar(ScalarFunc::Changes)), "total_changes" => Ok(Self::Scalar(ScalarFunc::TotalChanges)), "glob" => Ok(Self::Scalar(ScalarFunc::Glob)), "ifnull" => Ok(Self::Scalar(ScalarFunc::IfNull)), "if" | "iif" => Ok(Self::Scalar(ScalarFunc::Iif)), "instr" => Ok(Self::Scalar(ScalarFunc::Instr)), "like" => Ok(Self::Scalar(ScalarFunc::Like)), "abs" => Ok(Self::Scalar(ScalarFunc::Abs)), "upper" => Ok(Self::Scalar(ScalarFunc::Upper)), "lower" => Ok(Self::Scalar(ScalarFunc::Lower)), "random" => Ok(Self::Scalar(ScalarFunc::Random)), "randomblob" => Ok(Self::Scalar(ScalarFunc::RandomBlob)), "trim" => Ok(Self::Scalar(ScalarFunc::Trim)), "ltrim" => Ok(Self::Scalar(ScalarFunc::LTrim)), "rtrim" => Ok(Self::Scalar(ScalarFunc::RTrim)), "round" => Ok(Self::Scalar(ScalarFunc::Round)), "length" => Ok(Self::Scalar(ScalarFunc::Length)), "octet_length" => Ok(Self::Scalar(ScalarFunc::OctetLength)), "sign" => Ok(Self::Scalar(ScalarFunc::Sign)), "substr" => Ok(Self::Scalar(ScalarFunc::Substr)), "substring" => Ok(Self::Scalar(ScalarFunc::Substring)), "date" => Ok(Self::Scalar(ScalarFunc::Date)), "time" => Ok(Self::Scalar(ScalarFunc::Time)), "datetime" => Ok(Self::Scalar(ScalarFunc::DateTime)), "typeof" => Ok(Self::Scalar(ScalarFunc::Typeof)), "last_insert_rowid" => Ok(Self::Scalar(ScalarFunc::LastInsertRowid)), "unicode" => Ok(Self::Scalar(ScalarFunc::Unicode)), "quote" => Ok(Self::Scalar(ScalarFunc::Quote)), "sqlite_version" => Ok(Self::Scalar(ScalarFunc::SqliteVersion)), "turso_version" => Ok(Self::Scalar(ScalarFunc::TursoVersion)), "sqlite_source_id" => Ok(Self::Scalar(ScalarFunc::SqliteSourceId)), "replace" => Ok(Self::Scalar(ScalarFunc::Replace)), "likely" => Ok(Self::Scalar(ScalarFunc::Likely)), "likelihood" => Ok(Self::Scalar(ScalarFunc::Likelihood)), "unlikely" => Ok(Self::Scalar(ScalarFunc::Unlikely)), #[cfg(feature = "json")] "json" => Ok(Self::Json(JsonFunc::Json)), #[cfg(feature = "json")] "jsonb" => Ok(Self::Json(JsonFunc::Jsonb)), #[cfg(feature = "json")] "json_array_length" => Ok(Self::Json(JsonFunc::JsonArrayLength)), #[cfg(feature = "json")] "json_array" => Ok(Self::Json(JsonFunc::JsonArray)), #[cfg(feature = "json")] "jsonb_array" => Ok(Self::Json(JsonFunc::JsonbArray)), #[cfg(feature = "json")] "json_extract" => Ok(Func::Json(JsonFunc::JsonExtract)), #[cfg(feature = "json")] "jsonb_extract" => Ok(Func::Json(JsonFunc::JsonbExtract)), #[cfg(feature = "json")] "json_object" => Ok(Func::Json(JsonFunc::JsonObject)), #[cfg(feature = "json")] "jsonb_object" => Ok(Func::Json(JsonFunc::JsonbObject)), #[cfg(feature = "json")] "json_type" => Ok(Func::Json(JsonFunc::JsonType)), #[cfg(feature = "json")] "json_error_position" => Ok(Self::Json(JsonFunc::JsonErrorPosition)), #[cfg(feature = "json")] "json_valid" => Ok(Self::Json(JsonFunc::JsonValid)), #[cfg(feature = "json")] "json_patch" => Ok(Self::Json(JsonFunc::JsonPatch)), #[cfg(feature = "json")] "json_remove" => Ok(Self::Json(JsonFunc::JsonRemove)), #[cfg(feature = "json")] "jsonb_remove" => Ok(Self::Json(JsonFunc::JsonbRemove)), #[cfg(feature = "json")] "json_replace" => Ok(Self::Json(JsonFunc::JsonReplace)), #[cfg(feature = "json")] "json_insert" => Ok(Self::Json(JsonFunc::JsonInsert)), #[cfg(feature = "json")] "jsonb_insert" => Ok(Self::Json(JsonFunc::JsonbInsert)), #[cfg(feature = "json")] "jsonb_replace" => Ok(Self::Json(JsonFunc::JsonReplace)), #[cfg(feature = "json")] "json_pretty" => Ok(Self::Json(JsonFunc::JsonPretty)), #[cfg(feature = "json")] "json_set" => Ok(Self::Json(JsonFunc::JsonSet)), #[cfg(feature = "json")] "jsonb_set" => Ok(Self::Json(JsonFunc::JsonbSet)), #[cfg(feature = "json")] "json_quote" => Ok(Self::Json(JsonFunc::JsonQuote)), "unixepoch" => Ok(Self::Scalar(ScalarFunc::UnixEpoch)), "julianday" => Ok(Self::Scalar(ScalarFunc::JulianDay)), "hex" => Ok(Self::Scalar(ScalarFunc::Hex)), "unhex" => Ok(Self::Scalar(ScalarFunc::Unhex)), "zeroblob" => Ok(Self::Scalar(ScalarFunc::ZeroBlob)), "soundex" => Ok(Self::Scalar(ScalarFunc::Soundex)), "table_columns_json_array" => Ok(Self::Scalar(ScalarFunc::TableColumnsJsonArray)), "bin_record_json_object" => Ok(Self::Scalar(ScalarFunc::BinRecordJsonObject)), "conn_txn_id" => Ok(Self::Scalar(ScalarFunc::ConnTxnId)), "is_autocommit" => Ok(Self::Scalar(ScalarFunc::IsAutocommit)), "acos" => Ok(Self::Math(MathFunc::Acos)), "acosh" => Ok(Self::Math(MathFunc::Acosh)), "asin" => Ok(Self::Math(MathFunc::Asin)), "asinh" => Ok(Self::Math(MathFunc::Asinh)), "atan" => Ok(Self::Math(MathFunc::Atan)), "atan2" => Ok(Self::Math(MathFunc::Atan2)), "atanh" => Ok(Self::Math(MathFunc::Atanh)), "ceil" => Ok(Self::Math(MathFunc::Ceil)), "ceiling" => Ok(Self::Math(MathFunc::Ceiling)), "cos" => Ok(Self::Math(MathFunc::Cos)), "cosh" => Ok(Self::Math(MathFunc::Cosh)), "degrees" => Ok(Self::Math(MathFunc::Degrees)), "exp" => Ok(Self::Math(MathFunc::Exp)), "floor" => Ok(Self::Math(MathFunc::Floor)), "ln" => Ok(Self::Math(MathFunc::Ln)), "log" => Ok(Self::Math(MathFunc::Log)), "log10" => Ok(Self::Math(MathFunc::Log10)), "log2" => Ok(Self::Math(MathFunc::Log2)), "mod" => Ok(Self::Math(MathFunc::Mod)), "pi" => Ok(Self::Math(MathFunc::Pi)), "pow" => Ok(Self::Math(MathFunc::Pow)), "power" => Ok(Self::Math(MathFunc::Power)), "radians" => Ok(Self::Math(MathFunc::Radians)), "sin" => Ok(Self::Math(MathFunc::Sin)), "sinh" => Ok(Self::Math(MathFunc::Sinh)), "sqrt" => Ok(Self::Math(MathFunc::Sqrt)), "tan" => Ok(Self::Math(MathFunc::Tan)), "tanh" => Ok(Self::Math(MathFunc::Tanh)), "trunc" => Ok(Self::Math(MathFunc::Trunc)), #[cfg(feature = "fs")] #[cfg(not(target_family = "wasm"))] "load_extension" => Ok(Self::Scalar(ScalarFunc::LoadExtension)), "strftime" => Ok(Self::Scalar(ScalarFunc::StrfTime)), "printf" | "format" => Ok(Self::Scalar(ScalarFunc::Printf)), "vector" => Ok(Self::Vector(VectorFunc::Vector)), "vector32" => Ok(Self::Vector(VectorFunc::Vector32)), "vector32_sparse" => Ok(Self::Vector(VectorFunc::Vector32Sparse)), "vector64" => Ok(Self::Vector(VectorFunc::Vector64)), "vector8" => Ok(Self::Vector(VectorFunc::Vector8)), "vector1bit" => Ok(Self::Vector(VectorFunc::Vector1Bit)), "vector_extract" => Ok(Self::Vector(VectorFunc::VectorExtract)), "vector_distance_cos" => Ok(Self::Vector(VectorFunc::VectorDistanceCos)), "vector_distance_l2" => Ok(Self::Vector(VectorFunc::VectorDistanceL2)), "vector_distance_jaccard" => Ok(Self::Vector(VectorFunc::VectorDistanceJaccard)), "vector_distance_dot" => Ok(Self::Vector(VectorFunc::VectorDistanceDot)), "vector_concat" => Ok(Self::Vector(VectorFunc::VectorConcat)), "vector_slice" => Ok(Self::Vector(VectorFunc::VectorSlice)), // FTS functions #[cfg(all(feature = "fts", not(target_family = "wasm")))] "fts_score" => Ok(Self::Fts(FtsFunc::Score)), #[cfg(all(feature = "fts", not(target_family = "wasm")))] "fts_match" => Ok(Self::Fts(FtsFunc::Match)), #[cfg(all(feature = "fts", not(target_family = "wasm")))] "fts_highlight" => Ok(Self::Fts(FtsFunc::Highlight)), // Test type functions (for custom type system testing) "test_uint_encode" => Ok(Self::Scalar(ScalarFunc::TestUintEncode)), "test_uint_decode" => Ok(Self::Scalar(ScalarFunc::TestUintDecode)), "test_uint_add" => Ok(Self::Scalar(ScalarFunc::TestUintAdd)), "test_uint_sub" => Ok(Self::Scalar(ScalarFunc::TestUintSub)), "test_uint_mul" => Ok(Self::Scalar(ScalarFunc::TestUintMul)), "test_uint_div" => Ok(Self::Scalar(ScalarFunc::TestUintDiv)), "test_uint_lt" => Ok(Self::Scalar(ScalarFunc::TestUintLt)), "test_uint_eq" => Ok(Self::Scalar(ScalarFunc::TestUintEq)), "string_reverse" => Ok(Self::Scalar(ScalarFunc::StringReverse)), // Built-in type support functions "boolean_to_int" => Ok(Self::Scalar(ScalarFunc::BooleanToInt)), "int_to_boolean" => Ok(Self::Scalar(ScalarFunc::IntToBoolean)), "validate_ipaddr" => Ok(Self::Scalar(ScalarFunc::ValidateIpAddr)), "numeric_encode" => Ok(Self::Scalar(ScalarFunc::NumericEncode)), "numeric_decode" => Ok(Self::Scalar(ScalarFunc::NumericDecode)), "numeric_add" => Ok(Self::Scalar(ScalarFunc::NumericAdd)), "numeric_sub" => Ok(Self::Scalar(ScalarFunc::NumericSub)), "numeric_mul" => Ok(Self::Scalar(ScalarFunc::NumericMul)), "numeric_div" => Ok(Self::Scalar(ScalarFunc::NumericDiv)), "numeric_lt" => Ok(Self::Scalar(ScalarFunc::NumericLt)), "numeric_eq" => Ok(Self::Scalar(ScalarFunc::NumericEq)), // Array construction / element access (desugared from syntax) "array" => Ok(Self::Scalar(ScalarFunc::Array)), "array_element" => Ok(Self::Scalar(ScalarFunc::ArrayElement)), "array_set_element" => Ok(Self::Scalar(ScalarFunc::ArraySetElement)), // Array functions "array_length" => Ok(Self::Scalar(ScalarFunc::ArrayLength)), "array_append" => Ok(Self::Scalar(ScalarFunc::ArrayAppend)), "array_prepend" => Ok(Self::Scalar(ScalarFunc::ArrayPrepend)), "array_cat" => Ok(Self::Scalar(ScalarFunc::ArrayCat)), "array_remove" => Ok(Self::Scalar(ScalarFunc::ArrayRemove)), "array_contains" => Ok(Self::Scalar(ScalarFunc::ArrayContains)), "array_position" => Ok(Self::Scalar(ScalarFunc::ArrayPosition)), "array_slice" => Ok(Self::Scalar(ScalarFunc::ArraySlice)), "string_to_array" => Ok(Self::Scalar(ScalarFunc::StringToArray)), "array_to_string" => Ok(Self::Scalar(ScalarFunc::ArrayToString)), "array_overlap" | "array_overlaps" => Ok(Self::Scalar(ScalarFunc::ArrayOverlap)), "array_contains_all" => Ok(Self::Scalar(ScalarFunc::ArrayContainsAll)), _ => crate::bail_parse_error!("no such function: {}", name), } } /// Returns a list of all built-in functions for PRAGMA function_list. /// Derives the list from enum iteration so it stays in sync automatically. /// Functions with multiple valid arities get one row per arity. pub fn builtin_function_list() -> Vec { let mut funcs = Vec::new(); // Helper: push one entry per arity for a function let mut push = |name: String, func_type: &'static str, arities: &[i32], det: bool| { for &narg in arities { funcs.push(FunctionListEntry { name: name.clone(), func_type, narg, deterministic: det, }); } }; // Scalar functions (filter out internal-only variants) for f in ScalarFunc::iter() { if f.is_internal() { continue; } push(f.to_string(), "s", f.arities(), f.is_deterministic()); } // Aggregate functions (External is #[strum(disabled)], skipped automatically). // SQLite reports built-in aggregates as "w" (window-capable) since they // can all be used with OVER clauses. for f in AggFunc::iter() { push(f.to_string(), "w", f.arities(), f.is_deterministic()); } // Window functions. for f in WindowFunc::iter() { push(f.to_string(), "w", f.arities(), f.is_deterministic()); } // Math functions (all scalar) for f in MathFunc::iter() { push(f.to_string(), "s", f.arities(), f.is_deterministic()); } // Vector functions (all scalar) for f in VectorFunc::iter() { push(f.to_string(), "s", f.arities(), f.is_deterministic()); } // JSON functions (feature-gated, filter out operator-style entries) #[cfg(feature = "json")] for f in JsonFunc::iter() { if f.is_internal() { continue; } push(f.to_string(), "s", f.arities(), f.is_deterministic()); } // FTS functions (feature-gated) #[cfg(all(feature = "fts", not(target_family = "wasm")))] for f in FtsFunc::iter() { push(f.to_string(), "s", f.arities(), f.is_deterministic()); } // Aliases: functions callable under multiple names. // These are additional names that resolve_function() accepts // but that map to existing enum variants. funcs.push(FunctionListEntry { name: "format".into(), func_type: "s", narg: -1, deterministic: true, }); funcs.push(FunctionListEntry { name: "if".into(), func_type: "s", narg: 3, deterministic: true, }); funcs } } pub struct FunctionListEntry { pub name: String, pub func_type: &'static str, // "s" = scalar, "a" = aggregate, "w" = window pub narg: i32, // -1 = variable pub deterministic: bool, } ================================================ FILE: core/functions/datetime.rs ================================================ use crate::numeric::Numeric; use crate::types::AsValueRef; use crate::types::Value; use crate::LimboError::InvalidModifier; use crate::{Result, ValueRef}; // chrono isn't used more due to incompatibility with sqlite use chrono::{Local, Offset, TimeZone}; use std::borrow::Cow; use std::fmt::Write; const JD_TO_MS: i64 = 86_400_000; const MAX_JD: i64 = 464269060799999; // 9999-12-31 23:59:59.999 #[derive(Debug, Clone, Copy)] struct DateTime { i_jd: i64, // The julian day number times 86400000 y: i32, m: i32, d: i32, h: i32, min: i32, s: f64, tz: i32, // Timezone offset in minutes n_floor: i32, valid_jd: bool, valid_ymd: bool, valid_hms: bool, raw_s: bool, // Raw numeric value stored in s is_error: bool, use_subsec: bool, is_utc: bool, is_local: bool, } impl Default for DateTime { fn default() -> Self { DateTime { i_jd: 0, y: 2000, m: 1, d: 1, h: 0, min: 0, s: 0.0, tz: 0, n_floor: 0, valid_jd: false, valid_ymd: false, valid_hms: false, raw_s: false, is_error: false, use_subsec: false, is_utc: false, is_local: false, } } } impl DateTime { fn set_error(&mut self) { *self = DateTime::default(); self.is_error = true; } fn compute_jd(&mut self) { if self.valid_jd { return; } let mut y: i32; let mut m: i32; let d: i32; if self.valid_ymd { y = self.y; m = self.m; d = self.d; } else { y = 2000; m = 1; d = 1; } if !(-4713..=9999).contains(&y) || self.raw_s { self.set_error(); return; } if m <= 2 { y -= 1; m += 12; } let a = (y + 4800) / 100; let b = 38 - a + (a / 4); let x1 = 36525 * (y + 4716) / 100; let x2 = 306001 * (m + 1) / 10000; self.i_jd = (x1 as i64 + x2 as i64 + d as i64 + b as i64) * 86400000 - 131716800000; self.valid_jd = true; if self.valid_hms { self.i_jd += self.h as i64 * 3_600_000 + self.min as i64 * 60_000 + (self.s * 1000.0 + 0.5) as i64; if self.tz != 0 { self.i_jd -= self.tz as i64 * 60_000; self.valid_ymd = false; self.valid_hms = false; self.tz = 0; self.is_utc = true; self.is_local = false; } } } fn compute_ymd(&mut self) { if self.valid_ymd { return; } if !self.valid_jd { self.y = 2000; self.m = 1; self.d = 1; } else if self.i_jd < 0 || self.i_jd > MAX_JD { self.set_error(); return; } else { let z = ((self.i_jd + 43200000) / JD_TO_MS) as i32; let alpha = ((z as f64 + 32044.75) / 36524.25) as i32 - 52; let a = z + 1 + alpha - ((alpha + 100) / 4) + 25; let b = a + 1524; let c = ((b as f64 - 122.1) / 365.25) as i32; let d_calc = (36525 * (c & 32767)) / 100; let e = ((b - d_calc) as f64 / 30.6001) as i32; let x1 = (30.6001 * e as f64) as i32; self.d = b - d_calc - x1; self.m = if e < 14 { e - 1 } else { e - 13 }; self.y = if self.m > 2 { c - 4716 } else { c - 4715 }; } self.valid_ymd = true; } fn compute_hms(&mut self) { if self.valid_hms { return; } self.compute_jd(); let day_ms = ((self.i_jd + 43200000) % 86400000) as i32; self.s = (day_ms % 60000) as f64 / 1000.0; let day_min = day_ms / 60000; self.min = day_min % 60; self.h = day_min / 60; self.raw_s = false; self.valid_hms = true; } fn compute_ymd_hms(&mut self) { self.compute_ymd(); self.compute_hms(); } fn clear_ymd_hms_tz(&mut self) { self.valid_ymd = false; self.valid_hms = false; self.tz = 0; } fn compute_floor(&mut self) { assert!(self.valid_ymd || self.is_error); assert!(self.d >= 0 && self.d <= 31); assert!(self.m >= 0 && self.m <= 12); if self.d <= 28 || ((1 << self.m) & 0x15aa) != 0 { self.n_floor = 0; } else if self.m != 2 { self.n_floor = if self.d == 31 { 1 } else { 0 }; } else if self.y % 4 != 0 || (self.y % 100 == 0 && self.y % 400 != 0) { self.n_floor = self.d - 28; } else { self.n_floor = self.d - 29; } } } fn get_digits(z: &str, digits: usize, min_val: i32, max_val: i32) -> Option<(i32, &str)> { if z.len() < digits { return None; } if !z.is_char_boundary(digits) { return None; } let bytes = z.as_bytes(); if !bytes.iter().take(digits).all(|b| b.is_ascii_digit()) { return None; } let slice = &z[..digits]; let val = slice.parse::().ok()?; if val < min_val || val > max_val { return None; } Some((val, &z[digits..])) } fn set_to_current(p: &mut DateTime) { let now = std::time::SystemTime::now(); let duration = now .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default(); const UNIX_EPOCH_IJD: i64 = 210866760000000; p.i_jd = UNIX_EPOCH_IJD + duration.as_millis() as i64; p.valid_jd = true; p.is_utc = true; p.is_local = false; p.clear_ymd_hms_tz(); } fn parse_date_or_time(value: &str, p: &mut DateTime) -> Result<()> { if parse_yyyy_mm_dd(value, p) { return Ok(()); } if parse_hh_mm_ss(value, p) { return Ok(()); } if value.eq_ignore_ascii_case("now") { set_to_current(p); return Ok(()); } if let Ok(val) = value.parse::() { p.s = val; p.raw_s = true; if (0.0..5373484.5).contains(&val) { p.i_jd = (val * JD_TO_MS as f64 + 0.5) as i64; p.valid_jd = true; } return Ok(()); } if value.eq_ignore_ascii_case("subsec") || value.eq_ignore_ascii_case("subsecond") { p.use_subsec = true; set_to_current(p); return Ok(()); } Err(crate::LimboError::InvalidModifier("Parse Failed".into())) } fn parse_yyyy_mm_dd(mut z: &str, p: &mut DateTime) -> bool { let y: i32; let m: i32; let d: i32; let neg: bool; if z.starts_with('-') { z = &z[1..]; neg = true; } else { neg = false; } if let Some((val, rem)) = get_digits(z, 4, 0, 9999) { y = val; z = rem; } else { return false; } if !z.starts_with('-') { return false; } z = &z[1..]; if let Some((val, rem)) = get_digits(z, 2, 1, 12) { m = val; z = rem; } else { return false; } if !z.starts_with('-') { return false; } z = &z[1..]; if let Some((val, rem)) = get_digits(z, 2, 1, 31) { d = val; z = rem; } else { return false; } while !z.is_empty() { let c = z.as_bytes()[0] as char; if c.is_ascii_whitespace() || c == 'T' { z = &z[1..]; } else { break; } } if parse_hh_mm_ss(z, p) { } else if z.is_empty() { p.valid_hms = false; } else { return false; } p.valid_jd = false; p.valid_ymd = true; p.y = if neg { -y } else { y }; p.m = m; p.d = d; p.compute_floor(); if p.tz != 0 { p.compute_jd(); } true } fn parse_hh_mm_ss(mut z: &str, p: &mut DateTime) -> bool { let h: i32; let m: i32; let s: i32; let mut ms: f64 = 0.0; if let Some((val, rem)) = get_digits(z, 2, 0, 24) { h = val; z = rem; } else { return false; } if !z.starts_with(':') { return false; } z = &z[1..]; if let Some((val, rem)) = get_digits(z, 2, 0, 59) { m = val; z = rem; } else { return false; } if z.starts_with(':') { z = &z[1..]; if let Some((val, rem)) = get_digits(z, 2, 0, 59) { s = val; z = rem; } else { return false; } if z.starts_with('.') && z.len() > 1 && z.as_bytes()[1].is_ascii_digit() { let mut r_scale = 1.0; z = &z[1..]; // Skip '.' while !z.is_empty() && z.as_bytes()[0].is_ascii_digit() { let digit = (z.as_bytes()[0] - b'0') as f64; ms = ms * 10.0 + digit; r_scale *= 10.0; z = &z[1..]; } ms /= r_scale; if ms > 0.999 { ms = 0.999; } } } else { s = 0; } p.valid_jd = false; p.raw_s = false; p.valid_hms = true; p.h = h; p.min = m; p.s = s as f64 + ms; if parse_timezone(z, p) { return false; } true } fn parse_timezone(mut z: &str, p: &mut DateTime) -> bool { while !z.is_empty() { let c = z.as_bytes()[0] as char; if c.is_ascii_whitespace() { z = &z[1..]; } else { break; } } p.tz = 0; if z.is_empty() { return false; } let c = z.as_bytes()[0] as char; let sgn: i32; if c == '-' { sgn = -1; } else if c == '+' { sgn = 1; } else if c == 'Z' || c == 'z' { z = &z[1..]; p.is_local = false; p.is_utc = true; return check_trailing_garbage(z); } else { return true; } z = &z[1..]; let n_hr: i32; if let Some((val, rem)) = get_digits(z, 2, 0, 14) { n_hr = val; z = rem; } else { return true; } if !z.starts_with(':') { return true; } z = &z[1..]; let n_mn: i32; if let Some((val, rem)) = get_digits(z, 2, 0, 59) { n_mn = val; z = rem; } else { return true; } p.tz = sgn * (n_mn + n_hr * 60); if p.tz == 0 { p.is_local = false; p.is_utc = true; } check_trailing_garbage(z) } // Helper to mimic the "zulu_time" label logic: // while( sqlite3Isspace(*zDate) ){ zDate++; } // return *zDate!=0; fn check_trailing_garbage(mut z: &str) -> bool { while !z.is_empty() { let c = z.as_bytes()[0] as char; if c.is_ascii_whitespace() { z = &z[1..]; } else { break; } } // Return true if garbage remains (Error), false if empty (Success) !z.is_empty() } fn auto_adjust_date(p: &mut DateTime) { if !p.raw_s || p.valid_jd { p.raw_s = false; } else if p.s >= -210866760000.0 && p.s <= 253402300799.0 { let r = p.s * 1000.0 + 210866760000000.0; p.i_jd = (r + 0.5) as i64; p.valid_jd = true; p.raw_s = false; p.clear_ymd_hms_tz(); } } fn parse_modifier(p: &mut DateTime, z: &str, idx: usize) -> Result<()> { let mut chars = z.chars(); let first_char = match chars.next() { Some(c) => c.to_ascii_lowercase(), None => return Err(InvalidModifier(format!("Unknown modifier: {z}"))), }; match first_char { 'a' if z.eq_ignore_ascii_case("auto") => { if idx > 0 { return Err(InvalidModifier(format!( "Modifier 'auto' must be first: {z}" ))); } auto_adjust_date(p); Ok(()) } 'c' if z.eq_ignore_ascii_case("ceiling") => { p.compute_jd(); p.clear_ymd_hms_tz(); p.n_floor = 0; Ok(()) } 'f' if z.eq_ignore_ascii_case("floor") => { p.compute_jd(); if p.n_floor != 0 { p.i_jd -= p.n_floor as i64 * JD_TO_MS; p.n_floor = 0; } p.clear_ymd_hms_tz(); Ok(()) } 'j' if z.eq_ignore_ascii_case("julianday") => { if idx > 0 { return Err(InvalidModifier(format!( "Modifier 'julianday' must be first: {z}" ))); } if p.valid_jd && p.raw_s { p.raw_s = false; Ok(()) } else { Err(InvalidModifier(format!( "Invalid use of julianday modifier: {z}" ))) } } 'l' if z.eq_ignore_ascii_case("localtime") => { if !p.is_local { p.compute_jd(); let timestamp = (p.i_jd - 210866760000000) / 1000; let offset_sec = match Local.timestamp_opt(timestamp, 0) { chrono::LocalResult::Single(dt) => dt.offset().fix().local_minus_utc(), _ => 0, }; p.i_jd += (offset_sec as i64) * 1000; p.clear_ymd_hms_tz(); p.is_local = true; p.is_utc = false; } Ok(()) } 'u' if z.eq_ignore_ascii_case("unixepoch") => { if idx > 0 { return Err(InvalidModifier(format!( "Modifier 'unixepoch' must be first: {z}" ))); } if p.raw_s { let r = p.s * 1000.0 + 210866760000000.0; p.i_jd = (r + 0.5) as i64; p.valid_jd = true; p.raw_s = false; p.clear_ymd_hms_tz(); Ok(()) } else { Err(InvalidModifier(format!( "Invalid use of unixepoch modifier: {z}" ))) } } 'u' if z.eq_ignore_ascii_case("utc") => { if !p.is_utc { p.compute_jd(); let timestamp = (p.i_jd - 210866760000000) / 1000; let offset_sec = match Local.timestamp_opt(timestamp, 0) { chrono::LocalResult::Single(dt) => dt.offset().fix().local_minus_utc(), _ => 0, }; p.i_jd -= (offset_sec as i64) * 1000; p.clear_ymd_hms_tz(); p.is_utc = true; p.is_local = false; } Ok(()) } 'w' if z .get(..8) .is_some_and(|s| s.eq_ignore_ascii_case("weekday ")) => { if let Ok(val) = z[8..].trim().parse::() { if (0.0..7.0).contains(&val) && (val as i64 as f64) == val { let n = val as i64; p.compute_ymd_hms(); p.valid_jd = false; p.compute_jd(); let mut z = ((p.i_jd + 129600000) / 86400000) % 7; if z > n { z -= 7; } p.i_jd += (n - z) * 86400000; p.clear_ymd_hms_tz(); return Ok(()); } } Err(InvalidModifier(format!("Invalid weekday: {z}"))) } 's' => { if z.eq_ignore_ascii_case("subsec") || z.eq_ignore_ascii_case("subsecond") { p.use_subsec = true; Ok(()) } else if z .get(..9) .is_some_and(|s| s.eq_ignore_ascii_case("start of ")) { if !p.valid_jd && !p.valid_ymd && !p.valid_hms { return Err(InvalidModifier(format!("Invalid start of: {z}"))); } p.compute_ymd(); p.valid_hms = true; p.h = 0; p.min = 0; p.s = 0.0; p.raw_s = false; p.valid_jd = false; p.tz = 0; p.n_floor = 0; let suffix = &z[9..]; if suffix.eq_ignore_ascii_case("month") { p.d = 1; Ok(()) } else if suffix.eq_ignore_ascii_case("year") { p.m = 1; p.d = 1; Ok(()) } else if suffix.eq_ignore_ascii_case("day") { Ok(()) } else { Err(InvalidModifier(format!("Invalid start of: {z}"))) } } else { Err(InvalidModifier(format!("Unknown modifier: {z}"))) } } '+' | '-' | '0'..='9' => parse_arithmetic_modifier(p, z), _ => Err(InvalidModifier(format!("Unknown modifier: {z}"))), } } fn parse_arithmetic_modifier(p: &mut DateTime, z: &str) -> Result<()> { let z = z.trim(); let is_neg = z.starts_with('-'); let sign = if is_neg { -1 } else { 1 }; let clean_z = if z.starts_with('+') || z.starts_with('-') { &z[1..] } else { z }; // Case 1: YYYY-MM-DD Arithmetic if clean_z.len() >= 10 && clean_z.as_bytes().get(4) == Some(&b'-') && clean_z.as_bytes().get(7) == Some(&b'-') && clean_z.is_char_boundary(4) && clean_z.is_char_boundary(5) && clean_z.is_char_boundary(7) && clean_z.is_char_boundary(8) && clean_z.is_char_boundary(10) { let y_res = get_digits(&clean_z[0..4], 4, 0, 9999); let m_res = get_digits(&clean_z[5..7], 2, 0, 11); let d_res = get_digits(&clean_z[8..10], 2, 0, 30); if let (Some((y, _)), Some((m, _)), Some((d, _))) = (y_res, m_res, d_res) { let rem = &clean_z[10..]; let mut valid_format = true; let mut time_str = None; if !rem.is_empty() { if rem.starts_with(' ') { time_str = Some(rem.trim_start()); } else { valid_format = false; } } if valid_format { p.compute_ymd_hms(); p.valid_jd = false; let y_adj = y as i64; let m_adj = m as i64; let d_adj = d as i64; if is_neg { p.y = p.y.wrapping_sub(y_adj as i32); p.m = p.m.wrapping_sub(m_adj as i32); } else { p.y = p.y.wrapping_add(y_adj as i32); p.m = p.m.wrapping_add(m_adj as i32); } // Normalize months let m_current = p.m as i64; let x = if m_current > 0 { (m_current - 1) / 12 } else { (m_current - 12) / 12 }; p.y = p.y.wrapping_add(x as i32); p.m = (m_current - x * 12) as i32; p.compute_floor(); p.compute_jd(); // Apply day offset let day_diff = if is_neg { -d_adj } else { d_adj }; p.i_jd = p.i_jd.wrapping_add(day_diff.wrapping_mul(JD_TO_MS)); // Apply time offset if present if let Some(t_val) = time_str { let mut tx = DateTime::default(); if parse_hh_mm_ss(t_val, &mut tx) { tx.compute_jd(); let ms = (tx.h as i64 * 3600000) + (tx.min as i64 * 60000) + (tx.s * 1000.0) as i64; p.i_jd = p.i_jd.wrapping_add((sign as i64).wrapping_mul(ms)); } else { // If time parsing failed, the whole modifier is invalid return Err(InvalidModifier(format!( "Invalid time in arithmetic modifier: {z}" ))); } } p.clear_ymd_hms_tz(); return Ok(()); } } } // Case 2: HH:MM:SS Arithmetic if z.contains(':') { let mut tx = DateTime::default(); let time_str = if z.starts_with('+') || z.starts_with('-') { &z[1..] } else { z }; if parse_hh_mm_ss(time_str, &mut tx) { tx.compute_jd(); let ms = (tx.h as i64 * 3600000) + (tx.min as i64 * 60000) + (tx.s * 1000.0) as i64; p.compute_jd(); p.i_jd = p.i_jd.wrapping_add((sign as i64).wrapping_mul(ms)); p.clear_ymd_hms_tz(); return Ok(()); } } // Case 3: NNN Units let mut parts = z.split_whitespace(); if let Some(val_str) = parts.next() { if let Ok(val) = val_str.parse::() { if let Some(unit) = parts.next() { let limit_check = |v: f64, limit: f64| v.abs() < limit; if unit.eq_ignore_ascii_case("day") || unit.eq_ignore_ascii_case("days") { if !limit_check(val, 5373485.0) { return Err(InvalidModifier(format!("Modifier out of range: {z}"))); } p.compute_jd(); let ms = val * 86400000.0; let rounder = if ms < 0.0 { -0.5 } else { 0.5 }; p.i_jd = p.i_jd.wrapping_add((ms + rounder) as i64); p.n_floor = 0; p.clear_ymd_hms_tz(); return Ok(()); } else if unit.eq_ignore_ascii_case("hour") || unit.eq_ignore_ascii_case("hours") { if !limit_check(val, 1.2897e+11) { return Err(InvalidModifier(format!("Modifier out of range: {z}"))); } p.compute_jd(); let ms = val * 3600000.0; let rounder = if ms < 0.0 { -0.5 } else { 0.5 }; p.i_jd = p.i_jd.wrapping_add((ms + rounder) as i64); p.n_floor = 0; p.clear_ymd_hms_tz(); return Ok(()); } else if unit.eq_ignore_ascii_case("minute") || unit.eq_ignore_ascii_case("minutes") { if !limit_check(val, 7.7379e+12) { return Err(InvalidModifier(format!("Modifier out of range: {z}"))); } p.compute_jd(); let ms = val * 60000.0; let rounder = if ms < 0.0 { -0.5 } else { 0.5 }; p.i_jd = p.i_jd.wrapping_add((ms + rounder) as i64); p.n_floor = 0; p.clear_ymd_hms_tz(); return Ok(()); } else if unit.eq_ignore_ascii_case("second") || unit.eq_ignore_ascii_case("seconds") { if !limit_check(val, 4.6427e+14) { return Err(InvalidModifier(format!("Modifier out of range: {z}"))); } p.compute_jd(); let ms = val * 1000.0; let rounder = if ms < 0.0 { -0.5 } else { 0.5 }; p.i_jd = p.i_jd.wrapping_add((ms + rounder) as i64); p.n_floor = 0; p.clear_ymd_hms_tz(); return Ok(()); } else if unit.eq_ignore_ascii_case("month") || unit.eq_ignore_ascii_case("months") { if !limit_check(val, 176546.0) { return Err(InvalidModifier(format!("Modifier out of range: {z}"))); } p.compute_ymd_hms(); let int_months = val as i64; let frac_months = val - int_months as f64; let total_months = (p.m as i64) + int_months; let x = if total_months > 0 { (total_months - 1) / 12 } else { (total_months - 12) / 12 }; p.y = p.y.wrapping_add(x as i32); p.m = (total_months - x * 12) as i32; p.compute_floor(); p.valid_jd = false; p.compute_jd(); if frac_months.abs() > f64::EPSILON { let ms = frac_months * 30.0 * JD_TO_MS as f64; let rounder = if ms < 0.0 { -0.5 } else { 0.5 }; p.i_jd = p.i_jd.wrapping_add((ms + rounder) as i64); } p.clear_ymd_hms_tz(); return Ok(()); } else if unit.eq_ignore_ascii_case("year") || unit.eq_ignore_ascii_case("years") { if !limit_check(val, 14713.0) { return Err(InvalidModifier(format!("Modifier out of range: {z}"))); } p.compute_ymd_hms(); let int_years = val as i64; let frac_years = val - int_years as f64; p.y = p.y.wrapping_add(int_years as i32); p.compute_floor(); p.valid_jd = false; p.compute_jd(); if frac_years.abs() > f64::EPSILON { let ms = frac_years * 365.0 * JD_TO_MS as f64; let rounder = if ms < 0.0 { -0.5 } else { 0.5 }; p.i_jd = p.i_jd.wrapping_add((ms + rounder) as i64); } p.clear_ymd_hms_tz(); return Ok(()); } } } } Err(InvalidModifier(format!("Invalid arithmetic modifier: {z}"))) } pub fn exec_datetime_general(values: I, func_type: &str) -> Value where V: AsValueRef, E: ExactSizeIterator, I: IntoIterator, { let mut values = values.into_iter(); let mut p = DateTime::default(); let mut has_modifier = false; if values.len() == 0 { set_to_current(&mut p); } else { let first = values.next().unwrap(); match first.as_value_ref() { ValueRef::Text(s) => { if parse_date_or_time(s.as_str(), &mut p).is_err() { return Value::Null; } } ValueRef::Numeric(Numeric::Integer(i)) => { p.s = i as f64; p.raw_s = true; if p.s >= 0.0 && p.s < 5373484.5 { p.i_jd = (p.s * JD_TO_MS as f64 + 0.5) as i64; p.valid_jd = true; } } ValueRef::Numeric(Numeric::Float(f)) => { p.s = f64::from(f); p.raw_s = true; if p.s >= 0.0 && p.s < 5373484.5 { p.i_jd = (p.s * JD_TO_MS as f64 + 0.5) as i64; p.valid_jd = true; } } _ => return Value::Null, } } for (i, val) in values.enumerate() { has_modifier = true; if let ValueRef::Text(s) = val.as_value_ref() { if parse_modifier(&mut p, s.as_str(), i).is_err() { return Value::Null; } } else { return Value::Null; } } p.compute_jd(); if p.is_error || p.i_jd < 0 || p.i_jd > MAX_JD { return Value::Null; } if !has_modifier && p.valid_ymd && p.d > 28 { p.valid_ymd = false; } match func_type { "julianday" => Value::from_f64(p.i_jd as f64 / 86400000.0), "unixepoch" => { let unix = (p.i_jd - 210866760000000) / 1000; if p.use_subsec { let ms = (p.i_jd - 210866760000000) as f64 / 1000.0; Value::from_f64(ms) } else { Value::from_i64(unix) } } _ => { p.compute_ymd_hms(); if p.is_error { return Value::Null; } let mut res = String::new(); if func_type == "date" { if p.y < 0 { write!(res, "-{:04}-{:02}-{:02}", p.y.abs(), p.m, p.d).unwrap(); } else { write!(res, "{:04}-{:02}-{:02}", p.y, p.m, p.d).unwrap(); } } else if func_type == "time" { write!(res, "{:02}:{:02}", p.h, p.min).unwrap(); if p.use_subsec { write!(res, ":{:06.3}", p.s).unwrap(); } else { write!(res, ":{:02}", p.s as i32).unwrap(); } } else { if p.y < 0 { write!( res, "-{:04}-{:02}-{:02} {:02}:{:02}", p.y.abs(), p.m, p.d, p.h, p.min ) .unwrap(); } else { write!( res, "{:04}-{:02}-{:02} {:02}:{:02}", p.y, p.m, p.d, p.h, p.min ) .unwrap(); } if p.use_subsec { write!(res, ":{:06.3}", p.s).unwrap(); } else { write!(res, ":{:02}", p.s as i32).unwrap(); } } Value::from_text(res) } } } pub fn exec_date(values: I) -> Value where V: AsValueRef, E: ExactSizeIterator, I: IntoIterator, { exec_datetime_general(values, "date") } pub fn exec_time(values: I) -> Value where V: AsValueRef, E: ExactSizeIterator, I: IntoIterator, { exec_datetime_general(values, "time") } pub fn exec_datetime_full(values: I) -> Value where V: AsValueRef, E: ExactSizeIterator, I: IntoIterator, { exec_datetime_general(values, "datetime") } pub fn exec_julianday(values: I) -> Value where V: AsValueRef, E: ExactSizeIterator, I: IntoIterator, { exec_datetime_general(values, "julianday") } pub fn exec_unixepoch(values: I) -> Value where V: AsValueRef, E: ExactSizeIterator, I: IntoIterator, { exec_datetime_general(values, "unixepoch") } pub fn exec_timediff(values: I) -> Value where V: AsValueRef, E: ExactSizeIterator, I: IntoIterator, { let mut values = values.into_iter(); if values.len() < 2 { return Value::Null; } let mut d1 = DateTime::default(); let mut d2 = DateTime::default(); // Parse first argument (d1) let val1 = values.next().unwrap(); match val1.as_value_ref() { ValueRef::Text(s) => { if parse_date_or_time(s.as_str(), &mut d1).is_err() { return Value::Null; } } ValueRef::Numeric(Numeric::Integer(i)) => { d1.s = i as f64; d1.raw_s = true; if d1.s >= 0.0 && d1.s < 5373484.5 { d1.i_jd = (d1.s * JD_TO_MS as f64 + 0.5) as i64; d1.valid_jd = true; } } ValueRef::Numeric(Numeric::Float(f)) => { d1.s = f64::from(f); d1.raw_s = true; if d1.s >= 0.0 && d1.s < 5373484.5 { d1.i_jd = (d1.s * JD_TO_MS as f64 + 0.5) as i64; d1.valid_jd = true; } } _ => return Value::Null, } // Parse second argument (d2) let val2 = values.next().unwrap(); match val2.as_value_ref() { ValueRef::Text(s) => { if parse_date_or_time(s.as_str(), &mut d2).is_err() { return Value::Null; } } ValueRef::Numeric(Numeric::Integer(i)) => { d2.s = i as f64; d2.raw_s = true; if d2.s >= 0.0 && d2.s < 5373484.5 { d2.i_jd = (d2.s * JD_TO_MS as f64 + 0.5) as i64; d2.valid_jd = true; } } ValueRef::Numeric(Numeric::Float(f)) => { d2.s = f64::from(f); d2.raw_s = true; if d2.s >= 0.0 && d2.s < 5373484.5 { d2.i_jd = (d2.s * JD_TO_MS as f64 + 0.5) as i64; d2.valid_jd = true; } } _ => return Value::Null, } d1.compute_jd(); d2.compute_jd(); // Validate inputs after computation if d1.is_error || d2.is_error { return Value::Null; } d1.compute_ymd_hms(); d2.compute_ymd_hms(); let sign: char; if d1.i_jd >= d2.i_jd { sign = '+'; } else { sign = '-'; std::mem::swap(&mut d1, &mut d2); } let mut y = d1.y - d2.y; let mut m = d1.m - d2.m; if m < 0 { y -= 1; m += 12; } let mut temp = d2; temp.y += y; temp.m += m; // Normalize months while temp.m > 12 { temp.m -= 12; temp.y += 1; } while temp.m < 1 { temp.m += 12; temp.y -= 1; } temp.valid_jd = false; temp.compute_jd(); // Adjust if the Y/M shift overshot d1 while temp.i_jd > d1.i_jd { m -= 1; if m < 0 { m = 11; y -= 1; } temp = d2; temp.y += y; temp.m += m; while temp.m > 12 { temp.m -= 12; temp.y += 1; } while temp.m < 1 { temp.m += 12; temp.y -= 1; } temp.valid_jd = false; temp.compute_jd(); } let diff_ms = d1.i_jd - temp.i_jd; let days = diff_ms / 86400000; let rem_ms = diff_ms % 86400000; let hours = rem_ms / 3600000; let rem_ms = rem_ms % 3600000; let mins = rem_ms / 60000; let rem_ms = rem_ms % 60000; let secs = rem_ms as f64 / 1000.0; let mut res = String::new(); write!( res, "{sign}{y:04}-{m:02}-{days:02} {hours:02}:{mins:02}:{secs:06.3}" ) .unwrap(); Value::from_text(res) } pub fn exec_strftime(values: I) -> Value where V: AsValueRef, E: ExactSizeIterator, I: IntoIterator, { let mut values = values.into_iter(); if values.len() < 1 { return Value::Null; } let fmt_val = values.next().unwrap(); let fmt_str = match fmt_val.as_value_ref() { ValueRef::Text(s) => Cow::Borrowed(s.as_str()), ValueRef::Null => return Value::Null, val => Cow::Owned(val.to_string()), }; let mut p = DateTime::default(); if values.len() == 0 { set_to_current(&mut p); } else { let init_val = values.next().unwrap(); match init_val.as_value_ref() { ValueRef::Text(s) => { let s_str = s.as_str(); if s_str.eq_ignore_ascii_case("now") { set_to_current(&mut p); } else if let Ok(val) = s_str.parse::() { p.s = val; p.raw_s = true; if p.s >= 0.0 && p.s < 5373484.5 { p.i_jd = (p.s * JD_TO_MS as f64 + 0.5) as i64; p.valid_jd = true; } } else { let mut temp_p = DateTime::default(); if parse_date_or_time(s_str, &mut temp_p).is_ok() { p = temp_p; } else { return Value::Null; } } } ValueRef::Numeric(Numeric::Integer(i)) => { p.s = i as f64; p.raw_s = true; if p.s >= 0.0 && p.s < 5373484.5 { p.i_jd = (p.s * JD_TO_MS as f64 + 0.5) as i64; p.valid_jd = true; } } ValueRef::Numeric(Numeric::Float(f)) => { p.s = f64::from(f); p.raw_s = true; if p.s >= 0.0 && p.s < 5373484.5 { p.i_jd = (p.s * JD_TO_MS as f64 + 0.5) as i64; p.valid_jd = true; } } _ => return Value::Null, } for (i, val) in values.enumerate() { if let ValueRef::Text(s) = val.as_value_ref() { if parse_modifier(&mut p, s.as_str(), i).is_err() { return Value::Null; } } else { return Value::Null; } } } p.compute_jd(); if p.is_error { return Value::Null; } p.compute_ymd_hms(); let mut res = String::new(); let mut chars = fmt_str.chars().peekable(); let days_after_jan1 = |curr: &DateTime| -> i64 { let jan1 = DateTime { y: curr.y, m: 1, d: 1, valid_ymd: true, ..Default::default() }; let mut j1 = jan1; j1.compute_jd(); let curr_norm = DateTime { y: curr.y, m: curr.m, d: curr.d, valid_ymd: true, ..Default::default() }; let mut c1 = curr_norm; c1.compute_jd(); (c1.i_jd - j1.i_jd) / JD_TO_MS }; let days_after_mon = |curr: &DateTime| -> i64 { ((curr.i_jd + 43200000) / JD_TO_MS) % 7 }; let days_after_sun = |curr: &DateTime| -> i64 { ((curr.i_jd + 129600000) / JD_TO_MS) % 7 }; while let Some(c) = chars.next() { if c != '%' { res.push(c); continue; } match chars.next() { Some('d') => write!(res, "{:02}", p.d).unwrap(), Some('e') => write!(res, "{:2}", p.d).unwrap(), Some('F') => write!(res, "{:04}-{:02}-{:02}", p.y, p.m, p.d).unwrap(), Some('f') => { let mut s = p.s; if s > 59.999 { s = 59.999; } write!(res, "{s:06.3}").unwrap() } Some('g') => { let mut y_iso = p; y_iso.i_jd += (3 - days_after_mon(&p)) * 86400000; y_iso.valid_ymd = false; y_iso.compute_ymd(); write!(res, "{:02}", y_iso.y % 100).unwrap(); } Some('G') => { let mut y_iso = p; y_iso.i_jd += (3 - days_after_mon(&p)) * 86400000; y_iso.valid_ymd = false; y_iso.compute_ymd(); write!(res, "{:04}", y_iso.y).unwrap(); } Some('H') => write!(res, "{:02}", p.h).unwrap(), Some('I') => { let h = if p.h % 12 == 0 { 12 } else { p.h % 12 }; write!(res, "{h:02}").unwrap(); } Some('j') => { write!(res, "{:03}", days_after_jan1(&p) + 1).unwrap(); } Some('J') => { let val = p.i_jd as f64 / 86400000.0; if val.abs() >= 1_000_000.0 && val.abs() < 10_000_000.0 { let s = format!("{val:.9}"); let trimmed = s.trim_end_matches('0').trim_end_matches('.'); write!(res, "{trimmed}").unwrap(); } else { write!(res, "{val}").unwrap(); } } Some('k') => write!(res, "{:2}", p.h).unwrap(), Some('l') => { let h = if p.h % 12 == 0 { 12 } else { p.h % 12 }; write!(res, "{h:2}").unwrap(); } Some('m') => write!(res, "{:02}", p.m).unwrap(), Some('M') => write!(res, "{:02}", p.min).unwrap(), Some('p') => write!(res, "{}", if p.h >= 12 { "PM" } else { "AM" }).unwrap(), Some('P') => write!(res, "{}", if p.h >= 12 { "pm" } else { "am" }).unwrap(), Some('R') => write!(res, "{:02}:{:02}", p.h, p.min).unwrap(), Some('s') => { if p.use_subsec { write!(res, "{:.3}", (p.i_jd - 210866760000000) as f64 / 1000.0).unwrap(); } else { write!(res, "{}", (p.i_jd - 210866760000000) / 1000).unwrap(); } } Some('S') => write!(res, "{:02}", p.s as i32).unwrap(), Some('T') => write!(res, "{:02}:{:02}:{:02}", p.h, p.min, p.s as i32).unwrap(), Some('u') => { let mut w = days_after_sun(&p); if w == 0 { w = 7; } write!(res, "{w}").unwrap(); } Some('U') => { let w = (days_after_jan1(&p) - days_after_sun(&p) + 7) / 7; write!(res, "{w:02}").unwrap(); } Some('V') => { let mut temp = p; temp.i_jd += (3 - days_after_mon(&p)) * 86400000; temp.valid_ymd = false; temp.compute_ymd(); let w = days_after_jan1(&temp) / 7 + 1; write!(res, "{w:02}").unwrap(); } Some('w') => { write!(res, "{}", days_after_sun(&p)).unwrap(); } Some('W') => { let w = (days_after_jan1(&p) - days_after_mon(&p) + 7) / 7; write!(res, "{w:02}").unwrap(); } Some('Y') => write!(res, "{:04}", p.y).unwrap(), Some('%') => res.push('%'), _ => return Value::Null, } } Value::from_text(res) } #[cfg(test)] mod tests { use super::*; #[test] fn test_valid_get_date_from_time_value() { let now = chrono::Local::now().to_utc().format("%Y-%m-%d").to_string(); let prev_date_str = "2024-07-20"; let test_date_str = "2024-07-21"; let next_date_str = "2024-07-22"; let test_cases: Vec<(Value, &str)> = vec![ // Format 1: YYYY-MM-DD (no timezone applicable) (Value::build_text("2024-07-21"), test_date_str), // Format 2: YYYY-MM-DD HH:MM (Value::build_text("2024-07-21 22:30"), test_date_str), (Value::build_text("2024-07-21 22:30+02:00"), test_date_str), (Value::build_text("2024-07-21 22:30-05:00"), next_date_str), (Value::build_text("2024-07-21 01:30+05:00"), prev_date_str), (Value::build_text("2024-07-21 22:30Z"), test_date_str), // Format 3: YYYY-MM-DD HH:MM:SS (Value::build_text("2024-07-21 22:30:45"), test_date_str), ( Value::build_text("2024-07-21 22:30:45+02:00"), test_date_str, ), ( Value::build_text("2024-07-21 22:30:45-05:00"), next_date_str, ), ( Value::build_text("2024-07-21 01:30:45+05:00"), prev_date_str, ), (Value::build_text("2024-07-21 22:30:45Z"), test_date_str), // Format 4: YYYY-MM-DD HH:MM:SS.SSS (Value::build_text("2024-07-21 22:30:45.123"), test_date_str), ( Value::build_text("2024-07-21 22:30:45.123+02:00"), test_date_str, ), ( Value::build_text("2024-07-21 22:30:45.123-05:00"), next_date_str, ), ( Value::build_text("2024-07-21 01:30:45.123+05:00"), prev_date_str, ), (Value::build_text("2024-07-21 22:30:45.123Z"), test_date_str), // Format 5: YYYY-MM-DDTHH:MM (Value::build_text("2024-07-21T22:30"), test_date_str), (Value::build_text("2024-07-21T22:30+02:00"), test_date_str), (Value::build_text("2024-07-21T22:30-05:00"), next_date_str), (Value::build_text("2024-07-21T01:30+05:00"), prev_date_str), (Value::build_text("2024-07-21T22:30Z"), test_date_str), // Format 6: YYYY-MM-DDTHH:MM:SS (Value::build_text("2024-07-21T22:30:45"), test_date_str), ( Value::build_text("2024-07-21T22:30:45+02:00"), test_date_str, ), ( Value::build_text("2024-07-21T22:30:45-05:00"), next_date_str, ), ( Value::build_text("2024-07-21T01:30:45+05:00"), prev_date_str, ), (Value::build_text("2024-07-21T22:30:45Z"), test_date_str), // Format 7: YYYY-MM-DDTHH:MM:SS.SSS (Value::build_text("2024-07-21T22:30:45.123"), test_date_str), ( Value::build_text("2024-07-21T22:30:45.123+02:00"), test_date_str, ), ( Value::build_text("2024-07-21T22:30:45.123-05:00"), next_date_str, ), ( Value::build_text("2024-07-21T01:30:45.123+05:00"), prev_date_str, ), (Value::build_text("2024-07-21T22:30:45.123Z"), test_date_str), // Format 8: HH:MM (Value::build_text("22:30"), "2000-01-01"), (Value::build_text("22:30+02:00"), "2000-01-01"), (Value::build_text("22:30-05:00"), "2000-01-02"), (Value::build_text("01:30+05:00"), "1999-12-31"), (Value::build_text("22:30Z"), "2000-01-01"), // Format 9: HH:MM:SS (Value::build_text("22:30:45"), "2000-01-01"), (Value::build_text("22:30:45+02:00"), "2000-01-01"), (Value::build_text("22:30:45-05:00"), "2000-01-02"), (Value::build_text("01:30:45+05:00"), "1999-12-31"), (Value::build_text("22:30:45Z"), "2000-01-01"), // Format 10: HH:MM:SS.SSS (Value::build_text("22:30:45.123"), "2000-01-01"), (Value::build_text("22:30:45.123+02:00"), "2000-01-01"), (Value::build_text("22:30:45.123-05:00"), "2000-01-02"), (Value::build_text("01:30:45.123+05:00"), "1999-12-31"), (Value::build_text("22:30:45.123Z"), "2000-01-01"), // Test Format 11: 'now' (Value::build_text("now"), &now), // Format 12: DDDDDDDDDD (Julian date as float or integer) (Value::from_f64(2460512.5), test_date_str), (Value::from_i64(2460513), test_date_str), ]; for (input, expected) in test_cases { let result = exec_date(&[input.clone()]); assert_eq!( result, Value::build_text(expected.to_string()), "Failed for input: {input:?}" ); } } #[test] fn test_invalid_get_date_from_time_value() { let invalid_cases = vec![ Value::build_text("2024-07-21 25:00"), // Invalid hour Value::build_text("2024-07-21 25:00:00"), // Invalid hour Value::build_text("2024-07-21 23:60:00"), // Invalid minute Value::build_text("2024-07-21 22:58:60"), // Invalid second // Note: Invalid days now overflow like SQLite (2024-07-32 -> 2024-08-01) Value::build_text("2024-13-01"), // Invalid month Value::build_text("invalid_date"), // Completely invalid string Value::build_text(""), // Empty string Value::from_i64(i64::MAX), // Large Julian day Value::from_i64(-1), // Negative Julian day Value::from_f64(f64::MAX), // Large float Value::from_f64(-1.0), // Negative Julian day as float Value::from_f64(f64::NAN), // NaN Value::from_f64(f64::INFINITY), // Infinity Value::Null, // Null value Value::Blob(vec![1, 2, 3]), // Blob (unsupported type) // Invalid timezone tests Value::build_text("2024-07-21T12:00:00+24:00"), // Invalid timezone offset (too large) Value::build_text("2024-07-21T12:00:00-24:00"), // Invalid timezone offset (too small) Value::build_text("2024-07-21T12:00:00+00:60"), // Invalid timezone minutes Value::build_text("2024-07-21T12:00:00+00:00:00"), // Invalid timezone format (extra seconds) Value::build_text("2024-07-21T12:00:00+"), // Incomplete timezone Value::build_text("2024-07-21T12:00:00+Z"), // Invalid timezone format Value::build_text("2024-07-21T12:00:00+00:00Z"), // Mixing offset and Z Value::build_text("2024-07-21T12:00:00UTC"), // Named timezone (not supported) ]; for case in invalid_cases.iter() { let result = exec_date([case]); assert_eq!(result, Value::Null); } } #[test] fn test_valid_get_time_from_datetime_value() { let test_time_str = "22:30:45"; let prev_time_str = "20:30:45"; let next_time_str = "03:30:45"; let test_cases = vec![ // Format 1: YYYY-MM-DD (no timezone applicable) (Value::build_text("2024-07-21"), "00:00:00"), // Format 2: YYYY-MM-DD HH:MM (Value::build_text("2024-07-21 22:30"), "22:30:00"), (Value::build_text("2024-07-21 22:30+02:00"), "20:30:00"), (Value::build_text("2024-07-21 22:30-05:00"), "03:30:00"), (Value::build_text("2024-07-21 22:30Z"), "22:30:00"), // Format 3: YYYY-MM-DD HH:MM:SS (Value::build_text("2024-07-21 22:30:45"), test_time_str), ( Value::build_text("2024-07-21 22:30:45+02:00"), prev_time_str, ), ( Value::build_text("2024-07-21 22:30:45-05:00"), next_time_str, ), (Value::build_text("2024-07-21 22:30:45Z"), test_time_str), // Format 4: YYYY-MM-DD HH:MM:SS.SSS (Value::build_text("2024-07-21 22:30:45.123"), test_time_str), ( Value::build_text("2024-07-21 22:30:45.123+02:00"), prev_time_str, ), ( Value::build_text("2024-07-21 22:30:45.123-05:00"), next_time_str, ), (Value::build_text("2024-07-21 22:30:45.123Z"), test_time_str), // Format 5: YYYY-MM-DDTHH:MM (Value::build_text("2024-07-21T22:30"), "22:30:00"), (Value::build_text("2024-07-21T22:30+02:00"), "20:30:00"), (Value::build_text("2024-07-21T22:30-05:00"), "03:30:00"), (Value::build_text("2024-07-21T22:30Z"), "22:30:00"), // Format 6: YYYY-MM-DDTHH:MM:SS (Value::build_text("2024-07-21T22:30:45"), test_time_str), ( Value::build_text("2024-07-21T22:30:45+02:00"), prev_time_str, ), ( Value::build_text("2024-07-21T22:30:45-05:00"), next_time_str, ), (Value::build_text("2024-07-21T22:30:45Z"), test_time_str), // Format 7: YYYY-MM-DDTHH:MM:SS.SSS (Value::build_text("2024-07-21T22:30:45.123"), test_time_str), ( Value::build_text("2024-07-21T22:30:45.123+02:00"), prev_time_str, ), ( Value::build_text("2024-07-21T22:30:45.123-05:00"), next_time_str, ), (Value::build_text("2024-07-21T22:30:45.123Z"), test_time_str), // Format 8: HH:MM (Value::build_text("22:30"), "22:30:00"), (Value::build_text("22:30+02:00"), "20:30:00"), (Value::build_text("22:30-05:00"), "03:30:00"), (Value::build_text("22:30Z"), "22:30:00"), // Format 9: HH:MM:SS (Value::build_text("22:30:45"), test_time_str), (Value::build_text("22:30:45+02:00"), prev_time_str), (Value::build_text("22:30:45-05:00"), next_time_str), (Value::build_text("22:30:45Z"), test_time_str), // Format 10: HH:MM:SS.SSS (Value::build_text("22:30:45.123"), test_time_str), (Value::build_text("22:30:45.123+02:00"), prev_time_str), (Value::build_text("22:30:45.123-05:00"), next_time_str), (Value::build_text("22:30:45.123Z"), test_time_str), // Format 12: DDDDDDDDDD (Julian date as float or integer) (Value::from_f64(2460082.1), "14:24:00"), (Value::from_i64(2460082), "12:00:00"), ]; for (input, expected) in test_cases { let result = exec_time(&[input]); if let Value::Text(result_str) = result { assert_eq!(result_str.as_str(), expected); } else { panic!("Expected Value::Text, but got: {result:?}"); } } } #[test] fn test_invalid_get_time_from_datetime_value() { let invalid_cases = vec![ Value::build_text("2024-07-21 25:00"), // Invalid hour Value::build_text("2024-07-21 25:00:00"), // Invalid hour Value::build_text("2024-07-21 23:60:00"), // Invalid minute Value::build_text("2024-07-21 22:58:60"), // Invalid second // Note: Invalid days now overflow like SQLite (2024-07-32 -> 2024-08-01) Value::build_text("2024-13-01"), // Invalid month Value::build_text("invalid_date"), // Completely invalid string Value::build_text(""), // Empty string Value::from_i64(i64::MAX), // Large Julian day Value::from_i64(-1), // Negative Julian day Value::from_f64(f64::MAX), // Large float Value::from_f64(-1.0), // Negative Julian day as float Value::from_f64(f64::NAN), // NaN Value::from_f64(f64::INFINITY), // Infinity Value::Null, // Null value Value::Blob(vec![1, 2, 3]), // Blob (unsupported type) // Invalid timezone tests Value::build_text("2024-07-21T12:00:00+24:00"), // Invalid timezone offset (too large) Value::build_text("2024-07-21T12:00:00-24:00"), // Invalid timezone offset (too small) Value::build_text("2024-07-21T12:00:00+00:60"), // Invalid timezone minutes Value::build_text("2024-07-21T12:00:00+00:00:00"), // Invalid timezone format (extra seconds) Value::build_text("2024-07-21T12:00:00+"), // Incomplete timezone Value::build_text("2024-07-21T12:00:00+Z"), // Invalid timezone format Value::build_text("2024-07-21T12:00:00+00:00Z"), // Mixing offset and Z Value::build_text("2024-07-21T12:00:00UTC"), // Named timezone (not supported) ]; for case in invalid_cases { let result = exec_time(&[case.clone()]); assert_eq!(result, Value::Null); } } #[test] fn test_parse_days() { let get_days = |s: &str| -> f64 { let mut p = DateTime::default(); p.compute_jd(); let start_jd = p.i_jd; parse_modifier(&mut p, s, 1).expect("Failed to parse modifier"); (p.i_jd - start_jd) as f64 / 86_400_000.0 }; assert_eq!(get_days("5 days"), 5.0); assert_eq!(get_days("-3 days"), -3.0); assert_eq!(get_days("+2 days"), 2.0); assert_eq!(get_days("4 days"), 4.0); assert_eq!(get_days("6 DAYS"), 6.0); assert_eq!(get_days("+5 DAYS"), 5.0); // Fractional days assert_eq!(get_days("1.5 days"), 1.5); assert_eq!(get_days("-0.25 days"), -0.25); } #[test] fn test_parse_hours() { let get_hours = |s: &str| -> f64 { let mut p = DateTime::default(); p.compute_jd(); let start_jd = p.i_jd; parse_modifier(&mut p, s, 1).expect("Failed to parse modifier"); (p.i_jd - start_jd) as f64 / 3_600_000.0 }; assert_eq!(get_hours("12 hours"), 12.0); assert_eq!(get_hours("-2 hours"), -2.0); assert_eq!(get_hours("+3 HOURS"), 3.0); // Fractional hours assert_eq!(get_hours("0.5 hours"), 0.5); } #[test] fn test_parse_minutes() { let get_minutes = |s: &str| -> f64 { let mut p = DateTime::default(); p.compute_jd(); let start_jd = p.i_jd; parse_modifier(&mut p, s, 1).expect("Failed to parse modifier"); (p.i_jd - start_jd) as f64 / 60_000.0 }; assert_eq!(get_minutes("30 minutes"), 30.0); assert_eq!(get_minutes("-15 minutes"), -15.0); assert_eq!(get_minutes("+45 MINUTES"), 45.0); } #[test] fn test_parse_seconds() { let get_seconds = |s: &str| -> f64 { let mut p = DateTime::default(); p.compute_jd(); let start_jd = p.i_jd; parse_modifier(&mut p, s, 1).expect("Failed to parse modifier"); (p.i_jd - start_jd) as f64 / 1000.0 }; assert_eq!(get_seconds("45 seconds"), 45.0); assert_eq!(get_seconds("-10 seconds"), -10.0); assert_eq!(get_seconds("+20 SECONDS"), 20.0); } #[test] fn test_parse_months() { let get_months = |s: &str| -> f64 { let mut p = DateTime::default(); let start_y = p.y; let start_m = p.m; parse_modifier(&mut p, s, 1).expect("Failed to parse modifier"); ((p.y - start_y) * 12 + (p.m - start_m)) as f64 }; assert_eq!(get_months("3 months"), 3.0); assert_eq!(get_months("-1 months"), -1.0); assert_eq!(get_months("+6 MONTHS"), 6.0); } #[test] fn test_parse_years() { let get_years = |s: &str| -> f64 { let mut p = DateTime::default(); let start_y = p.y; parse_modifier(&mut p, s, 1).expect("Failed to parse modifier"); (p.y - start_y) as f64 }; assert_eq!(get_years("2 years"), 2.0); assert_eq!(get_years("-1 years"), -1.0); assert_eq!(get_years("+10 YEARS"), 10.0); } #[test] fn test_parse_time_offset() { let get_ms_change = |s: &str| -> i64 { let mut p = DateTime::default(); p.compute_jd(); let start_jd = p.i_jd; parse_modifier(&mut p, s, 1).expect("Failed to parse modifier"); p.i_jd - start_jd }; // +01:30 = 90 mins = 5,400,000 ms assert_eq!(get_ms_change("+01:30"), 5_400_000); // -00:45 = -45 mins = -2,700,000 ms assert_eq!(get_ms_change("-00:45"), -2_700_000); // +02:15:30 = 8,130,000 ms assert_eq!(get_ms_change("+02:15:30"), 8_130_000); // +02:15:30.250 = 8,130,250 ms assert_eq!(get_ms_change("+02:15:30.250"), 8_130_250); } #[test] fn test_parse_date_offset() { let run = |modifier: &str| -> String { let args = vec![ Value::build_text("2000-01-01 00:00:00".to_string()), Value::build_text(modifier.to_string()), ]; let val = exec_datetime_full(args); val.to_text().unwrap().to_string() }; assert_eq!(run("+2023-05-15"), "4023-06-16 00:00:00"); assert_eq!(run("-2023-05-15"), "-0024-07-17 00:00:00"); } #[test] fn test_parse_date_time_offset() { let run = |modifier: &str| -> String { let args = vec![ Value::build_text("2000-01-01 00:00:00".to_string()), Value::build_text(modifier.to_string()), ]; let val = exec_datetime_full(args); val.to_text().unwrap().to_string() }; assert_eq!(run("+2023-05-15 14:30"), "4023-06-16 14:30:00"); assert_eq!(run("-0001-05-15 14:30"), "1998-07-16 09:30:00"); } #[test] fn test_parse_start_of() { let run = |start: &str, modifier: &str| -> String { let args = vec![ Value::build_text(start.to_string()), Value::build_text(modifier.to_string()), ]; let val = exec_datetime_full(args); val.to_text().unwrap().to_string() }; let base = "2023-06-15 12:30:45"; assert_eq!(run(base, "start of month"), "2023-06-01 00:00:00"); assert_eq!(run(base, "START OF MONTH"), "2023-06-01 00:00:00"); assert_eq!(run(base, "start of year"), "2023-01-01 00:00:00"); assert_eq!(run(base, "START OF YEAR"), "2023-01-01 00:00:00"); assert_eq!(run(base, "start of day"), "2023-06-15 00:00:00"); assert_eq!(run(base, "START OF DAY"), "2023-06-15 00:00:00"); } #[test] fn test_parse_weekday() { let run = |start: &str, modifier: &str| -> String { let args = vec![ Value::build_text(start.to_string()), Value::build_text(modifier.to_string()), ]; let val = exec_date(args); val.to_text().unwrap().to_string() }; // 2023-01-01 was a Sunday (0) assert_eq!(run("2023-01-01", "weekday 0"), "2023-01-01"); // No change assert_eq!(run("2023-01-01", "weekday 1"), "2023-01-02"); // Next Monday assert_eq!(run("2023-01-01", "WEEKDAY 6"), "2023-01-07"); // Next Saturday } #[test] fn test_parse_ceiling_modifier() { let mut p = DateTime::default(); assert!(parse_modifier(&mut p, "ceiling", 1).is_ok()); assert!(parse_modifier(&mut p, "CEILING", 1).is_ok()); } #[test] fn test_parse_other_modifiers() { // Setup state for modifiers that require specific preconditions let mut p = DateTime { valid_jd: true, raw_s: true, ..DateTime::default() }; // Modifiers that should just parse OK assert!(parse_modifier(&mut p, "localtime", 1).is_ok()); assert!(parse_modifier(&mut p, "LOCALTIME", 1).is_ok()); assert!(parse_modifier(&mut p, "utc", 1).is_ok()); assert!(parse_modifier(&mut p, "UTC", 1).is_ok()); assert!(parse_modifier(&mut p, "subsec", 1).is_ok()); assert!(parse_modifier(&mut p, "SUBSEC", 1).is_ok()); assert!(parse_modifier(&mut p, "subsecond", 1).is_ok()); assert!(parse_modifier(&mut p, "SUBSECOND", 1).is_ok()); // These must be at index 0 to parse validly assert!(parse_modifier(&mut p, "unixepoch", 0).is_ok()); p.raw_s = true; assert!(parse_modifier(&mut p, "UNIXEPOCH", 0).is_ok()); p.raw_s = true; assert!(parse_modifier(&mut p, "julianday", 0).is_ok()); p.raw_s = true; assert!(parse_modifier(&mut p, "JULIANDAY", 0).is_ok()); p.raw_s = true; assert!(parse_modifier(&mut p, "auto", 0).is_ok()); p.raw_s = true; assert!(parse_modifier(&mut p, "AUTO", 0).is_ok()); } #[test] fn test_parse_invalid_modifier() { let mut p = DateTime::default(); assert!(parse_modifier(&mut p, "invalid modifier", 1).is_err()); assert!(parse_modifier(&mut p, "5", 1).is_err()); assert!(parse_modifier(&mut p, "days", 1).is_err()); assert!(parse_modifier(&mut p, "++5 days", 1).is_err()); assert!(parse_modifier(&mut p, "weekday 7", 1).is_err()); } #[test] fn test_apply_modifier_days() { let run = |mod_str: &str| -> String { let args = vec![ Value::build_text("2023-06-15 12:30:45".to_string()), Value::build_text(mod_str.to_string()), ]; exec_datetime_full(args).to_text().unwrap().to_string() }; assert_eq!(run("5 days"), "2023-06-20 12:30:45"); assert_eq!(run("-3 days"), "2023-06-12 12:30:45"); } #[test] fn test_apply_modifier_hours() { let run = |mod_str: &str| -> String { let args = vec![ Value::build_text("2023-06-15 12:30:45".to_string()), Value::build_text(mod_str.to_string()), ]; exec_datetime_full(args).to_text().unwrap().to_string() }; assert_eq!(run("6 hours"), "2023-06-15 18:30:45"); assert_eq!(run("-2 hours"), "2023-06-15 10:30:45"); } #[test] fn test_apply_modifier_minutes() { let run = |mod_str: &str| -> String { let args = vec![ Value::build_text("2023-06-15 12:30:45".to_string()), Value::build_text(mod_str.to_string()), ]; exec_datetime_full(args).to_text().unwrap().to_string() }; assert_eq!(run("45 minutes"), "2023-06-15 13:15:45"); assert_eq!(run("-15 minutes"), "2023-06-15 12:15:45"); } #[test] fn test_apply_modifier_seconds() { let run = |mod_str: &str| -> String { let args = vec![ Value::build_text("2023-06-15 12:30:45".to_string()), Value::build_text(mod_str.to_string()), ]; exec_datetime_full(args).to_text().unwrap().to_string() }; assert_eq!(run("30 seconds"), "2023-06-15 12:31:15"); assert_eq!(run("-20 seconds"), "2023-06-15 12:30:25"); } #[test] fn test_apply_modifier_time_offset() { let run = |mod_str: &str| -> String { let args = vec![ Value::build_text("2023-06-15 12:30:45".to_string()), Value::build_text(mod_str.to_string()), ]; exec_datetime_full(args).to_text().unwrap().to_string() }; assert_eq!(run("+01:30"), "2023-06-15 14:00:45"); assert_eq!(run("-00:45"), "2023-06-15 11:45:45"); } #[test] fn test_apply_modifier_date_time_offset() { let run = |mod_str: &str| -> String { let args = vec![ Value::build_text("2023-06-15 12:30:45".to_string()), Value::build_text(mod_str.to_string()), ]; exec_datetime_full(args).to_text().unwrap().to_string() }; assert_eq!(run("+0001-01-01 01:01"), "2024-07-16 13:31:45"); assert_eq!(run("-0001-01-01 01:01"), "2022-05-14 11:29:45"); assert_eq!(run("+0002-03-04 05:06"), "2025-09-19 17:36:45"); assert_eq!(run("-0002-03-04 05:06"), "2021-03-11 07:24:45"); } #[test] fn test_apply_modifier_start_of_year() { let res = exec_datetime_full(&[ Value::build_text("2023-06-15 12:30:45"), Value::build_text("start of year"), ]); assert_eq!(res.to_text().unwrap(), "2023-01-01 00:00:00"); } #[test] fn test_apply_modifier_start_of_day() { let res = exec_datetime_full(&[ Value::build_text("2023-06-15 12:30:45"), Value::build_text("start of day"), ]); assert_eq!(res.to_text().unwrap(), "2023-06-15 00:00:00"); } #[test] fn test_single_modifier() { let res = exec_datetime_full(&[ Value::build_text("2023-06-15 12:30:45"), Value::build_text("-1 day"), ]); assert_eq!(res.to_text().unwrap(), "2023-06-14 12:30:45"); } #[test] fn test_multiple_modifiers() { let res = exec_datetime_full(&[ Value::build_text("2023-06-15 12:30:45"), Value::build_text("-1 day"), Value::build_text("+3 hours"), ]); assert_eq!(res.to_text().unwrap(), "2023-06-14 15:30:45"); } #[test] fn test_subsec_modifier() { let res = exec_datetime_general( &[ Value::build_text("2023-06-15 12:30:45"), Value::build_text("subsec"), ], "time", ); assert_eq!(res.to_text().unwrap(), "12:30:45.000"); } #[test] fn test_start_of_day_modifier() { let res = exec_datetime_full(&[ Value::build_text("2023-06-15 12:30:45"), Value::build_text("start of day"), Value::build_text("-1 day"), ]); assert_eq!(res.to_text().unwrap(), "2023-06-14 00:00:00"); } #[test] fn test_start_of_month_modifier() { let res = exec_datetime_full(&[ Value::build_text("2023-06-15 12:30:45"), Value::build_text("start of month"), Value::build_text("+1 day"), ]); assert_eq!(res.to_text().unwrap(), "2023-06-02 00:00:00"); } #[test] fn test_start_of_year_modifier() { let res = exec_datetime_full(&[ Value::build_text("2023-06-15 12:30:45"), Value::build_text("start of year"), Value::build_text("+30 days"), Value::build_text("+5 hours"), ]); assert_eq!(res.to_text().unwrap(), "2023-01-31 05:00:00"); } #[test] fn test_timezone_modifiers() { let base_str = "2023-06-15 12:30:45"; let naive = chrono::NaiveDate::from_ymd_opt(2023, 6, 15) .unwrap() .and_hms_opt(12, 30, 45) .unwrap(); // 1. Test 'localtime' modifier: Input (assumed UTC) -> Output (Local) let args_local = vec![ Value::build_text(base_str.to_string()), Value::build_text("localtime".to_string()), ]; let res_local = exec_datetime_full(args_local); // Expected calculation: Treat naive as UTC, convert to Local let utc_dt = chrono::DateTime::::from_naive_utc_and_offset(naive, chrono::Utc); let expected_local = utc_dt .with_timezone(&chrono::Local) .format("%Y-%m-%d %H:%M:%S") .to_string(); assert_eq!( res_local.to_text().unwrap(), expected_local, "localtime modifier mismatch" ); // 2. Test 'utc' modifier: Input (assumed Local) -> Output (UTC) let args_utc = vec![ Value::build_text(base_str.to_string()), Value::build_text("utc".to_string()), ]; let res_utc = exec_datetime_full(args_utc); // Expected calculation: Treat naive as Local, convert to UTC // We handle potential Local ambiguities (though 2023-06-15 is typically safe) match chrono::Local.from_local_datetime(&naive) { chrono::LocalResult::Single(local_input) => { let expected_utc = local_input .with_timezone(&chrono::Utc) .format("%Y-%m-%d %H:%M:%S") .to_string(); assert_eq!( res_utc.to_text().unwrap(), expected_utc, "utc modifier mismatch" ); } _ => { // Fallback if local time is ambiguous/invalid in test environment // Ensure result is at least a valid string and not Null assert!(res_utc.to_text().is_some()); assert_ne!(res_utc, Value::Null); } } } #[test] fn test_combined_modifiers() { let args = vec![ Value::build_text("2000-01-01 00:00:00".to_string()), Value::build_text("-1 day".to_string()), Value::build_text("+5 hours".to_string()), Value::build_text("+30 minutes".to_string()), Value::build_text("+15 seconds".to_string()), Value::build_text("subsec".to_string()), ]; let result = exec_datetime_full(args); assert_eq!(result.to_text().unwrap(), "1999-12-31 05:30:15.000"); } #[test] fn test_max_datetime_limit() { let args = vec![Value::build_text("9999-12-31 23:59:59".to_string())]; let result = exec_datetime_full(args); assert_eq!(result.to_text().unwrap(), "9999-12-31 23:59:59"); } #[test] fn test_leap_second_ignored() { let args = vec![Value::build_text("2024-06-30 23:59:60".to_string())]; let result = exec_datetime_full(args); assert_eq!(result, Value::Null); } #[test] fn test_already_on_weekday_no_change() { let args = vec![ Value::build_text("2023-01-01 12:00:00".to_string()), Value::build_text("weekday 0".to_string()), ]; let result = exec_datetime_full(args); assert_eq!(result.to_text().unwrap(), "2023-01-01 12:00:00"); } #[test] fn test_move_forward_if_different() { let args1 = vec![ Value::build_text("2023-01-01 12:00:00".to_string()), Value::build_text("weekday 1".to_string()), ]; let res1 = exec_datetime_full(args1); assert_eq!(res1.to_text().unwrap(), "2023-01-02 12:00:00"); let args2 = vec![ Value::build_text("2023-01-03 12:00:00".to_string()), Value::build_text("weekday 5".to_string()), ]; let res2 = exec_datetime_full(args2); assert_eq!(res2.to_text().unwrap(), "2023-01-06 12:00:00"); } #[test] fn test_wrap_around_weekend() { let args1 = vec![ Value::build_text("2023-01-06 12:00:00".to_string()), Value::build_text("weekday 0".to_string()), ]; let res1 = exec_datetime_full(args1); assert_eq!(res1.to_text().unwrap(), "2023-01-08 12:00:00"); let args2 = vec![ Value::build_text("2023-01-08 12:00:00".to_string()), Value::build_text("weekday 0".to_string()), ]; let res2 = exec_datetime_full(args2); assert_eq!(res2.to_text().unwrap(), "2023-01-08 12:00:00"); } #[test] fn test_same_day_stays_put() { let args = vec![ Value::build_text("2023-01-05 12:00:00".to_string()), Value::build_text("weekday 4".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2023-01-05 12:00:00"); } #[test] fn test_already_on_friday_no_change() { let args = vec![ Value::build_text("2023-01-06 12:00:00".to_string()), Value::build_text("weekday 5".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2023-01-06 12:00:00"); } #[test] fn test_apply_modifier_julianday() { let jd_args = vec![Value::build_text("2000-01-01 12:00:00".to_string())]; let jd_val = exec_julianday(jd_args); let dt_args = vec![jd_val, Value::build_text("auto".to_string())]; let dt_res = exec_datetime_full(dt_args); assert_eq!(dt_res.to_text().unwrap(), "2000-01-01 12:00:00"); } #[test] fn test_apply_modifier_start_of_month() { let args = vec![ Value::build_text("2023-06-15 12:30:45".to_string()), Value::build_text("start of month".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2023-06-01 00:00:00"); } #[test] fn test_apply_modifier_subsec() { let args = vec![ Value::build_text("2023-06-15 12:30:45".to_string()), Value::build_text("subsec".to_string()), ]; let res = exec_datetime_general(args, "datetime"); assert_eq!(res.to_text().unwrap(), "2023-06-15 12:30:45.000"); } #[test] fn test_apply_modifier_floor_modifier_n_floor_gt_0() { let args = vec![ Value::build_text("2023-01-31".to_string()), Value::build_text("+1 month".to_string()), Value::build_text("floor".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2023-02-28 00:00:00"); } #[test] fn test_apply_modifier_floor_modifier_n_floor_le_0() { let args = vec![ Value::build_text("2023-01-15".to_string()), Value::build_text("+1 month".to_string()), Value::build_text("floor".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2023-02-15 00:00:00"); } #[test] fn test_apply_modifier_ceiling_modifier_sets_n_floor_to_zero() { let args = vec![ Value::build_text("2023-01-31".to_string()), Value::build_text("ceiling".to_string()), Value::build_text("+1 month".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2023-03-03 00:00:00"); } #[test] fn test_apply_modifier_start_of_month_basic() { let args = vec![ Value::build_text("2023-06-15 12:30:45".to_string()), Value::build_text("start of month".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2023-06-01 00:00:00"); } #[test] fn test_apply_modifier_start_of_month_already_at_first() { let args = vec![ Value::build_text("2023-06-01 00:00:00".to_string()), Value::build_text("start of month".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2023-06-01 00:00:00"); } #[test] fn test_apply_modifier_start_of_month_edge_case() { let args = vec![ Value::build_text("2023-07-31 23:59:59".to_string()), Value::build_text("start of month".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2023-07-01 00:00:00"); } #[test] fn test_apply_modifier_subsec_no_change() { let args = vec![ Value::build_text("2023-06-15 12:30:45.123".to_string()), Value::build_text("subsec".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2023-06-15 12:30:45.123"); } #[test] fn test_apply_modifier_subsec_preserves_fractional_seconds() { let args = vec![ Value::build_text("2025-01-02 04:12:21.891".to_string()), Value::build_text("subsec".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2025-01-02 04:12:21.891"); } #[test] fn test_apply_modifier_subsec_no_fractional_seconds() { let args = vec![ Value::build_text("2025-01-02 04:12:21".to_string()), Value::build_text("subsec".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2025-01-02 04:12:21.000"); } #[test] fn test_apply_modifier_subsec_truncate_to_milliseconds() { let args = vec![ Value::build_text("2025-01-02 04:12:21.891123456".to_string()), Value::build_text("subsec".to_string()), ]; let res = exec_datetime_full(args); assert_eq!(res.to_text().unwrap(), "2025-01-02 04:12:21.891"); } #[test] fn test_strftime() { let fmt = Value::build_text("%Y-%m-%d".to_string()); let date = Value::build_text("2023-10-25 14:30:00".to_string()); let expected = Value::build_text("2023-10-25".to_string()); assert_eq!(exec_strftime(&[fmt, date]), expected); let fmt = Value::build_text("%H:%M:%S".to_string()); let date = Value::build_text("2023-10-25 14:30:45".to_string()); let expected = Value::build_text("14:30:45".to_string()); assert_eq!(exec_strftime(&[fmt, date]), expected); let fmt = Value::build_text("Date: %Y-%m-%d, Time: %H:%M".to_string()); let date = Value::build_text("2023-10-25 14:30:45".to_string()); let expected = Value::build_text("Date: 2023-10-25, Time: 14:30".to_string()); assert_eq!(exec_strftime(&[fmt, date]), expected); let fmt = Value::build_text("%Y-%m-%d".to_string()); let date = Value::build_text("2023-10-25".to_string()); let mod1 = Value::build_text("start of month".to_string()); let expected = Value::build_text("2023-10-01".to_string()); assert_eq!(exec_strftime(&[fmt, date, mod1]), expected); let fmt = Value::build_text("%Y-%m-%d".to_string()); let date = Value::build_text("2023-10-25".to_string()); let mod1 = Value::build_text("+5 days".to_string()); let expected = Value::build_text("2023-10-30".to_string()); assert_eq!(exec_strftime(&[fmt, date, mod1]), expected); let fmt = Value::build_text("%J".to_string()); let date = Value::build_text("2023-01-01 12:00:00".to_string()); let expected = Value::build_text("2459946".to_string()); assert_eq!(exec_strftime(&[fmt, date]), expected); let fmt = Value::build_text("%s".to_string()); let date = Value::build_text("2023-01-01 00:00:00".to_string()); let expected = Value::build_text("1672531200".to_string()); assert_eq!(exec_strftime(&[fmt, date]), expected); let fmt = Value::build_text("%S.%f".to_string()); let date = Value::build_text("2023-01-01 12:00:05.123".to_string()); let expected = Value::build_text("05.05.123".to_string()); assert_eq!(exec_strftime(&[fmt, date]), expected); let fmt = Value::build_text("%w".to_string()); let date = Value::build_text("2023-01-01".to_string()); let expected = Value::build_text("0".to_string()); assert_eq!(exec_strftime(&[fmt, date]), expected); let fmt = Value::build_text("%j".to_string()); let date = Value::build_text("2023-02-01".to_string()); let expected = Value::build_text("032".to_string()); assert_eq!(exec_strftime(&[fmt, date]), expected); let fmt = Value::Null; let date = Value::build_text("now".to_string()); assert_eq!(exec_strftime(&[fmt, date]), Value::Null); let fmt = Value::build_text("%Y".to_string()); let date = Value::Null; let expected = Value::Null; assert_eq!(exec_strftime(&[fmt, date]), expected); let fmt = Value::build_text("%Y".to_string()); let date = Value::build_text("invalid-date".to_string()); assert_eq!(exec_strftime(&[fmt, date]), Value::Null); let fmt = Value::build_text("100%%".to_string()); let date = Value::build_text("2023-01-01".to_string()); let expected = Value::build_text("100%".to_string()); assert_eq!(exec_strftime(&[fmt, date]), expected); } #[test] fn test_exec_timediff() { let start = Value::build_text("12:00:00"); let end = Value::build_text("14:30:45"); let expected = Value::build_text("-0000-00-00 02:30:45.000"); assert_eq!(exec_timediff(&[start, end]), expected); let start = Value::build_text("14:30:45"); let end = Value::build_text("12:00:00"); let expected = Value::build_text("+0000-00-00 02:30:45.000"); assert_eq!(exec_timediff(&[start, end]), expected); let start = Value::build_text("12:00:01.300"); let end = Value::build_text("12:00:00.500"); let expected = Value::build_text("+0000-00-00 00:00:00.800"); assert_eq!(exec_timediff(&[start, end]), expected); let start = Value::build_text("13:30:00"); let end = Value::build_text("16:45:30"); let expected = Value::build_text("-0000-00-00 03:15:30.000"); assert_eq!(exec_timediff(&[start, end]), expected); let start = Value::build_text("2023-05-10 23:30:00"); let end = Value::build_text("2023-05-11 01:15:00"); let expected = Value::build_text("-0000-00-00 01:45:00.000"); assert_eq!(exec_timediff(&[start, end]), expected); let start = Value::Null; let end = Value::build_text("12:00:00"); let expected = Value::Null; assert_eq!(exec_timediff(&[start, end]), expected); let start = Value::build_text("not a time"); let end = Value::build_text("12:00:00"); let expected = Value::Null; assert_eq!(exec_timediff(&[start, end]), expected); // Test identical times - should return zero duration, not Null let start = Value::build_text("12:00:00"); let end = Value::build_text("12:00:00"); let expected = Value::build_text("+0000-00-00 00:00:00.000"); assert_eq!(exec_timediff(&[start, end]), expected); } #[test] fn test_subsec_fixed_time_expansion() { let args = vec![ Value::build_text("2024-01-01 12:00:00".to_string()), Value::build_text("subsec".to_string()), ]; let result = exec_datetime_full(args); assert_eq!(result.to_text().unwrap(), "2024-01-01 12:00:00.000"); } #[test] fn test_subsec_date_only_expansion() { let args = vec![ Value::build_text("2024-01-01".to_string()), Value::build_text("subsec".to_string()), ]; let result = exec_datetime_full(args); assert_eq!(result.to_text().unwrap(), "2024-01-01 00:00:00.000"); } #[test] fn test_subsec_iso_separator() { let args = vec![ Value::build_text("2024-01-01T15:30:00".to_string()), Value::build_text("subsec".to_string()), ]; let result = exec_datetime_full(args); assert_eq!(result.to_text().unwrap(), "2024-01-01 15:30:00.000"); } #[test] fn test_subsec_chaining_before_math() { let args = vec![ Value::build_text("2024-01-01 12:00:00".to_string()), Value::build_text("subsec".to_string()), Value::build_text("+1 hour".to_string()), ]; let result = exec_datetime_full(args); assert_eq!(result.to_text().unwrap(), "2024-01-01 13:00:00.000"); } #[test] fn test_subsec_chaining_after_math() { let args = vec![ Value::build_text("2024-01-01 12:00:00".to_string()), Value::build_text("+1 hour".to_string()), Value::build_text("subsec".to_string()), ]; let result = exec_datetime_full(args); assert_eq!(result.to_text().unwrap(), "2024-01-01 13:00:00.000"); } #[test] fn test_subsec_rollover_math() { let args = vec![ Value::build_text("2024-01-01 12:00:00.999".to_string()), Value::build_text("+1 second".to_string()), Value::build_text("subsec".to_string()), ]; let result = exec_datetime_full(args); assert_eq!(result.to_text().unwrap(), "2024-01-01 12:00:01.999"); } #[test] fn test_subsec_case_insensitivity() { let args = vec![ Value::build_text("2024-01-01 12:00:00".to_string()), Value::build_text("SuBsEc".to_string()), ]; let result = exec_datetime_full(args); assert_eq!(result.to_text().unwrap(), "2024-01-01 12:00:00.000"); } #[test] fn test_parse_modifier_unicode_no_panic() { let unicode_inputs = ["!*\u{ea37}", "\u{1F600}", "日本語", "中", "\u{0080}", ""]; for input in unicode_inputs { let args = vec![ Value::build_text("now".to_string()), Value::build_text(input.to_string()), ]; let result = exec_datetime_full(args); // Expect Null for invalid modifiers, but no panic assert_eq!(result, Value::Null); } } #[test] fn test_unixepoch_basic_usage() { let result = exec_unixepoch(vec![Value::build_text("1970-01-01 00:00:00".to_string())]); assert_eq!(result, Value::from_i64(0)); let result = exec_unixepoch(vec![Value::build_text("2023-01-01 00:00:00".to_string())]); assert_eq!(result, Value::from_i64(1672531200)); let result = exec_unixepoch(vec![Value::build_text("1969-12-31 23:59:59".to_string())]); assert_eq!(result, Value::from_i64(-1)); let result = exec_unixepoch(vec![Value::from_f64(2440587.5)]); assert_eq!(result, Value::from_i64(0)); } #[test] fn test_unixepoch_numeric_modifiers_unixepoch() { let res1 = exec_unixepoch(vec![ Value::from_i64(1672531200), Value::build_text("unixepoch".to_string()), ]); assert_eq!(res1, Value::from_i64(1672531200)); let res2 = exec_unixepoch(vec![ Value::from_i64(0), Value::build_text("unixepoch".to_string()), ]); assert_eq!(res2, Value::from_i64(0)); let res3 = exec_unixepoch(vec![ Value::from_i64(1672531200), Value::build_text("unixepoch".to_string()), Value::build_text("start of year".to_string()), ]); assert_eq!(res3, Value::from_i64(1672531200)); } #[test] fn test_unixepoch_numeric_modifiers_julianday() { let res1 = exec_unixepoch(vec![ Value::from_f64(2440587.5), Value::build_text("julianday".to_string()), ]); assert_eq!(res1, Value::from_i64(0)); let res2 = exec_unixepoch(vec![ Value::from_f64(2460311.5), Value::build_text("julianday".to_string()), ]); assert_eq!(res2, Value::from_i64(1704153600)); let res3 = exec_unixepoch(vec![ Value::from_f64(0.0), Value::build_text("julianday".to_string()), ]); match res3 { Value::Numeric(Numeric::Integer(i)) => assert_eq!(i, -210866760000), _ => panic!("Expected Integer result for JD 0"), } } #[test] fn test_unixepoch_numeric_modifiers_auto() { let res1 = exec_unixepoch(vec![ Value::from_f64(2440587.5), Value::build_text("auto".to_string()), ]); assert_eq!(res1, Value::from_i64(0)); let res2 = exec_unixepoch(vec![ Value::from_i64(1672531200), Value::build_text("auto".to_string()), ]); assert_eq!(res2, Value::from_i64(1672531200)); let res3 = exec_unixepoch(vec![ Value::from_f64(0.0), Value::build_text("auto".to_string()), ]); match res3 { Value::Numeric(Numeric::Integer(i)) => assert!(i < 0), _ => panic!("Expected Integer result"), } } #[test] fn test_unixepoch_invalid_usage() { let res1 = exec_unixepoch(vec![ Value::from_i64(0), Value::build_text("start of year".to_string()), Value::build_text("unixepoch".to_string()), ]); assert_eq!(res1, Value::Null); let res2 = exec_unixepoch(vec![ Value::build_text("2023-01-01".to_string()), Value::build_text("unixepoch".to_string()), ]); assert_eq!(res2, Value::Null); let res3 = exec_unixepoch(vec![ Value::from_i64(0), Value::build_text("unixepoch".to_string()), Value::build_text("julianday".to_string()), ]); assert_eq!(res3, Value::Null); } #[test] fn test_unixepoch_complex_calculations() { let res1 = exec_unixepoch(vec![ Value::from_f64(2440587.5), Value::build_text("julianday".to_string()), Value::build_text("+1 day".to_string()), ]); assert_eq!(res1, Value::from_i64(86400)); let res2 = exec_unixepoch(vec![ Value::from_f64(2460311.5), Value::build_text("auto".to_string()), Value::build_text("start of month".to_string()), Value::build_text("+1 month".to_string()), ]); assert_eq!(res2, Value::from_i64(1706745600)); } #[test] fn test_unixepoch_subsecond_precision() { let res1 = exec_unixepoch(vec![ Value::build_text("1970-01-01 00:00:00.0006".to_string()), Value::build_text("subsec".to_string()), ]); match res1 { Value::Numeric(Numeric::Float(f)) => { assert!((f64::from(f) - 0.001).abs() < f64::EPSILON) } _ => panic!("Expected Float result"), } let res2 = exec_unixepoch(vec![ Value::build_text("1970-01-01 00:00:00.9996".to_string()), Value::build_text("subsec".to_string()), ]); match res2 { Value::Numeric(Numeric::Float(f)) => { assert!((f64::from(f) - 0.999).abs() < f64::EPSILON) } _ => panic!("Expected Float result"), } } #[test] fn test_fast_path_date_only() { assert_eq!( exec_date(vec![Value::build_text("2024-01-01".to_string())]) .to_text() .unwrap(), "2024-01-01" ); assert_eq!( exec_date(vec![Value::build_text("0001-01-01".to_string())]) .to_text() .unwrap(), "0001-01-01" ); assert_eq!( exec_date(vec![Value::build_text("9999-12-31".to_string())]) .to_text() .unwrap(), "9999-12-31" ); assert_eq!( exec_date(vec![Value::build_text("2024-02-29".to_string())]) .to_text() .unwrap(), "2024-02-29" ); assert_eq!( exec_date(vec![Value::build_text("2023-02-29".to_string())]) .to_text() .unwrap(), "2023-03-01" ); assert_eq!( exec_date(vec![Value::build_text("2024-00-01".to_string())]), Value::Null ); assert_eq!( exec_date(vec![Value::build_text("2024-13-01".to_string())]), Value::Null ); assert_eq!( exec_date(vec![Value::build_text("2024-01-00".to_string())]), Value::Null ); assert_eq!( exec_date(vec![Value::build_text("2024-01-32".to_string())]), Value::Null ); assert_eq!( exec_date(vec![Value::build_text("2024/01/01".to_string())]), Value::Null ); assert_eq!( exec_date(vec![Value::build_text("2024.01.01".to_string())]), Value::Null ); assert_eq!( exec_date(vec![Value::build_text("202X-01-01".to_string())]), Value::Null ); assert_eq!( exec_date(vec![Value::build_text("2024-0a-01".to_string())]), Value::Null ); } #[test] fn test_fast_path_datetime_formats() { assert_eq!( exec_datetime_full(vec![Value::build_text("2024-01-15 10:30".to_string())]) .to_text() .unwrap(), "2024-01-15 10:30:00" ); assert_eq!( exec_datetime_full(vec![Value::build_text("2024-01-15T10:30".to_string())]) .to_text() .unwrap(), "2024-01-15 10:30:00" ); assert_eq!( exec_datetime_full(vec![Value::build_text("2024-01-15X10:30".to_string())]), Value::Null ); assert_eq!( exec_datetime_full(vec![Value::build_text("2024-01-15 10:30:45".to_string())]) .to_text() .unwrap(), "2024-01-15 10:30:45" ); assert_eq!( exec_datetime_full(vec![Value::build_text("2024-01-15T10:30:45".to_string())]) .to_text() .unwrap(), "2024-01-15 10:30:45" ); assert_eq!( exec_datetime_full(vec![Value::build_text("2024-01-15 25:30:45".to_string())]), Value::Null ); assert_eq!( exec_datetime_full(vec![Value::build_text("2024-01-15 10:60:45".to_string())]), Value::Null ); assert_eq!( exec_datetime_full(vec![Value::build_text("2024-01-15 10:30:60".to_string())]), Value::Null ); } #[test] fn test_fast_path_time_only() { assert_eq!( exec_time(vec![Value::build_text("10:30".to_string())]) .to_text() .unwrap(), "10:30:00" ); assert_eq!( exec_time(vec![Value::build_text("00:00".to_string())]) .to_text() .unwrap(), "00:00:00" ); assert_eq!( exec_time(vec![Value::build_text("23:59".to_string())]) .to_text() .unwrap(), "23:59:00" ); assert_eq!( exec_time(vec![Value::build_text("24:00".to_string())]) .to_text() .unwrap(), "24:00:00" ); assert_eq!( exec_time(vec![Value::build_text("10:60".to_string())]), Value::Null ); assert_eq!( exec_time(vec![Value::build_text("10:30:45".to_string())]) .to_text() .unwrap(), "10:30:45" ); assert_eq!( exec_time(vec![Value::build_text("00:00:00".to_string())]) .to_text() .unwrap(), "00:00:00" ); assert_eq!( exec_time(vec![Value::build_text("23:59:59".to_string())]) .to_text() .unwrap(), "23:59:59" ); let res1 = exec_datetime_general( vec![ Value::build_text("10:30:45.123".to_string()), Value::build_text("subsec".to_string()), ], "time", ); assert_eq!(res1.to_text().unwrap(), "10:30:45.123"); let res2 = exec_datetime_general( vec![ Value::build_text("10:30:45.1".to_string()), Value::build_text("subsec".to_string()), ], "time", ); assert_eq!(res2.to_text().unwrap(), "10:30:45.100"); } #[test] fn test_fast_path_skips_timezone_strings() { assert_eq!( exec_datetime_full(vec![Value::build_text("2024-01-15 10:30:45Z".to_string())]) .to_text() .unwrap(), "2024-01-15 10:30:45" ); assert_eq!( exec_datetime_full(vec![Value::build_text( "2024-01-15 10:30:45+02:00".to_string() )]) .to_text() .unwrap(), "2024-01-15 08:30:45" ); assert_eq!( exec_datetime_full(vec![Value::build_text( "2024-01-15 10:30:45-05:00".to_string() )]) .to_text() .unwrap(), "2024-01-15 15:30:45" ); assert_eq!( exec_time(vec![Value::build_text("10:30:45+02:00".to_string())]) .to_text() .unwrap(), "08:30:45" ); } #[test] fn test_fast_path_fractional_seconds_precision() { let args1 = vec![ Value::build_text("2024-01-15 10:30:45.123456789".to_string()), Value::build_text("subsec".to_string()), ]; assert_eq!( exec_datetime_full(args1).to_text().unwrap(), "2024-01-15 10:30:45.123" ); let args2 = vec![ Value::build_text("2024-01-15 10:30:45.1".to_string()), Value::build_text("subsec".to_string()), ]; assert_eq!( exec_datetime_full(args2).to_text().unwrap(), "2024-01-15 10:30:45.100" ); let args3 = vec![ Value::build_text("2024-01-15 10:30:45.100".to_string()), Value::build_text("subsec".to_string()), ]; assert_eq!( exec_datetime_full(args3).to_text().unwrap(), "2024-01-15 10:30:45.100" ); let args4 = vec![ Value::build_text("2024-01-15 10:30:45.000".to_string()), Value::build_text("subsec".to_string()), ]; assert_eq!( exec_datetime_full(args4).to_text().unwrap(), "2024-01-15 10:30:45.000" ); } #[test] fn test_fast_path_month_day_boundaries() { let run = |s: &str| -> String { exec_datetime_full(&[Value::build_text(s.to_string())]) .to_text() .unwrap() .to_string() }; assert_eq!(run("2024-04-30"), "2024-04-30 00:00:00"); assert_eq!(run("2024-04-31"), "2024-05-01 00:00:00"); assert_eq!(run("2024-01-31"), "2024-01-31 00:00:00"); assert_eq!(run("2024-03-31"), "2024-03-31 00:00:00"); assert_eq!(run("2024-02-29"), "2024-02-29 00:00:00"); assert_eq!(run("2023-02-29"), "2023-03-01 00:00:00"); assert_eq!(run("2024-02-28"), "2024-02-28 00:00:00"); } #[test] fn test_fast_path_edge_cases() { // Helper for cases expected to fail (return Null) let run = |s: &str| -> Value { exec_datetime_full(&[Value::build_text(s.to_string())]) }; // Helper for cases expected to succeed (return String) let run_str = |s: &str| -> String { run(s).to_text().unwrap().to_string() }; assert_eq!(run(""), Value::Null); assert_eq!(run("a"), Value::Null); assert_eq!(run("ab"), Value::Null); assert_eq!(run("abc"), Value::Null); assert_eq!(run("abcd"), Value::Null); assert_eq!(run_str("0000-01-01"), "0000-01-01 00:00:00"); assert_eq!(run(" 2024-01-01"), Value::Null); assert_eq!(run_str("2024-01-01 "), "2024-01-01 00:00:00"); assert_eq!(run_str("2024-01-15\t10:30:45"), "2024-01-15 10:30:45"); assert_eq!(run("2024-1-01"), Value::Null); assert_eq!(run("2024-01-1"), Value::Null); assert_eq!(run("aaaa-bb-cc"), Value::Null); assert_eq!(run("2024-01-01abc"), Value::Null); assert_eq!(run_str("-2024-01-01"), "-2024-01-01 00:00:00"); assert_eq!(run("10:30:45.12abc"), Value::Null); assert_eq!(run("2024-01-15 10:30:45.123xyz"), Value::Null); // Manual check for subsec since it requires 2 arguments let dt_args = &[ Value::build_text("10:30:45.12".to_string()), Value::build_text("subsec".to_string()), ]; assert_eq!( exec_datetime_general(dt_args, "time").to_text().unwrap(), "10:30:45.120" ); } // Regression test for fuzzing crash: strftime with non-char-boundary UTF-8 modifiers // The modifier "swww\0\u{1}\t\0\u{fffd}\u{fffd}\u{f}W" has multi-byte chars where // byte index 9 is not a valid char boundary, causing panic on slice. #[test] fn test_strftime_invalid_utf8_boundary_modifier() { // This modifier starts with 's' so it matches the 's' => branch, // but byte 9 falls inside a multi-byte character let modifier_with_multibyte = "swww\0\u{1}\t\0\u{fffd}\u{fffd}\u{f}W"; let args = &[ Value::build_text("".to_string()), Value::from_f64(-1.8041807844761696e230), Value::build_text(modifier_with_multibyte.to_string()), ]; // Should not panic, just return an error or null let _ = exec_strftime(args.iter()); // Also test the 'w' => weekday branch with similar input let weekday_modifier = "weekda\u{fffd}\u{fffd}"; let args2 = &[ Value::build_text("".to_string()), Value::from_f64(0.0), Value::build_text(weekday_modifier.to_string()), ]; let _ = exec_strftime(args2.iter()); } } ================================================ FILE: core/functions/mod.rs ================================================ pub mod datetime; pub mod printf; ================================================ FILE: core/functions/printf.rs ================================================ use std::fmt::Write; use std::iter::{repeat_n, Peekable}; use std::str; use std::str::Chars; use crate::numeric::{format_float, str_to_i64, Numeric}; use crate::types::Value; use crate::vdbe::Register; #[derive(Default, Clone)] struct FormatFlags { left_justify: bool, force_sign: bool, space_sign: bool, zero_pad: bool, alternate: bool, comma_sep: bool, alt_form_2: bool, } struct FormatSpec { flags: FormatFlags, width: Option, precision: Option, spec_type: char, } /// Consume flag characters after '%', returning the FormatFlags. /// In SQLite, later flags override earlier ones for +/space conflicts. fn parse_flags(chars: &mut Peekable) -> FormatFlags { let mut flags = FormatFlags::default(); while let Some(&c) = chars.peek() { match c { '-' => { flags.left_justify = true; } '+' => { flags.force_sign = true; flags.space_sign = false; } ' ' => { flags.space_sign = true; flags.force_sign = false; } '0' => { flags.zero_pad = true; } '#' => { flags.alternate = true; } ',' => { flags.comma_sep = true; } '!' => { flags.alt_form_2 = true; } _ => break, } chars.next(); } flags } /// Parse a decimal integer from the character stream. fn parse_number(chars: &mut Peekable) -> Option { let mut n: usize = 0; let mut found = false; while let Some(&c) = chars.peek() { if let Some(d) = c.to_digit(10) { n = n.saturating_mul(10).saturating_add(d as usize); found = true; chars.next(); } else { break; } } if found { Some(n) } else { None } } /// Maximum width/precision to prevent OOM from adversarial format strings. /// SQLite uses int for width and caps output at SQLITE_MAX_LENGTH (1 billion), /// but we use a tighter limit since f64 only has ~17 meaningful digits anyway. const MAX_WIDTH: usize = 1_000_000; /// Parse a full format specifier after the initial '%'. /// May consume arguments from `values` for `*` width/precision. fn parse_format_spec( chars: &mut Peekable, values: &[Register], args_index: &mut usize, ) -> FormatSpec { let mut flags = parse_flags(chars); // Width — SQLite casts * width to C int (int32) let width = if chars.peek() == Some(&'*') { chars.next(); let w = get_arg_i64(values, args_index) as i32; if w < 0 { // Negative width means left-justify flags.left_justify = true; // SQLite: width = width >= -2147483647 ? -width : 0 if w > i32::MIN { Some(((-w) as usize).min(MAX_WIDTH)) } else { Some(0) } } else { Some((w as usize).min(MAX_WIDTH)) } } else { parse_number(chars).map(|w| w.min(MAX_WIDTH)) }; // Precision — SQLite casts * precision to C int (int32), then negates if negative. // For i32::MIN, negation wraps back to i32::MIN (still negative), treated as 0. let precision = if chars.peek() == Some(&'.') { chars.next(); if chars.peek() == Some(&'*') { chars.next(); let p = get_arg_i64(values, args_index) as i32; let p = if p < 0 { p.wrapping_neg() } else { p }; Some((p.max(0) as usize).min(MAX_WIDTH)) } else { Some(parse_number(chars).unwrap_or(0).min(MAX_WIDTH)) } } else { None }; // Skip length modifiers (l, ll, h, hh) - ignored in SQL context while matches!(chars.peek(), Some(&'l') | Some(&'h')) { chars.next(); } // Type character let spec_type = chars.next().unwrap_or('\0'); FormatSpec { flags, width, precision, spec_type, } } // ── Coercion helpers ──────────────────────────────────────────── /// Get the next argument as i64 (for * width/precision), advancing args_index. fn get_arg_i64(values: &[Register], args_index: &mut usize) -> i64 { if *args_index >= values.len() { return 0; } let val = coerce_to_i64(values[*args_index].get_value()); *args_index += 1; val } /// Coerce a Value to i64 for integer specifiers. fn coerce_to_i64(value: &Value) -> i64 { match value { Value::Numeric(Numeric::Integer(i)) => *i, Value::Numeric(Numeric::Float(f)) => f64::from(*f) as i64, Value::Text(t) => str_to_i64(t.as_str()).unwrap_or(0), Value::Blob(b) => { let s = String::from_utf8_lossy(b); str_to_i64(s.as_ref()).unwrap_or(0) } Value::Null => 0, } } /// Coerce a Value to f64 for float specifiers. fn coerce_to_f64(value: &Value) -> f64 { match value { Value::Numeric(Numeric::Float(f)) => f64::from(*f), _ => match Option::::from(value) { Some(Numeric::Integer(i)) => i as f64, Some(Numeric::Float(f)) => f.into(), None => 0.0, }, } } /// Coerce a Value to String for string specifiers. fn coerce_to_string(value: &Value) -> String { match value { Value::Null => String::new(), Value::Numeric(Numeric::Integer(i)) => i.to_string(), Value::Numeric(Numeric::Float(f)) => format_float(f64::from(*f)), Value::Text(t) => t.as_str().to_string(), Value::Blob(b) => String::from_utf8_lossy(b).to_string(), } } // ── Formatting helpers ────────────────────────────────────────── /// Insert comma separators into a digit string (e.g. "1234567" → "1,234,567"). fn insert_commas(digits: &str) -> String { let bytes = digits.as_bytes(); let len = bytes.len(); if len <= 3 { return digits.to_string(); } let mut result = String::with_capacity(len + len / 3); let first_group = len % 3; if first_group > 0 { result.push_str(&digits[..first_group]); } for chunk in digits.as_bytes()[first_group..].chunks(3) { if !result.is_empty() { result.push(','); } // digits is always ASCII [0-9], guaranteed valid UTF-8 result.push_str(str::from_utf8(chunk).expect("digit string is ASCII")); } result } /// Apply width padding to content. `sign_prefix` is prepended before content /// and is part of the total width calculation. /// `zero_overrides_left`: when true, the `0` flag takes priority over `-` (SQLite /// integer specifiers %d/%u/%x/%o). For float specifiers, pass false so that `-` /// overrides `0` per standard C behavior. fn apply_width( output: &mut String, sign_prefix: &str, content: &str, width: Option, flags: &FormatFlags, zero_overrides_left: bool, ) { // Use character count, not byte count, for width — SQLite counts characters. let total_len = sign_prefix.chars().count() + content.chars().count(); let w = width.unwrap_or(0); if total_len >= w { output.push_str(sign_prefix); output.push_str(content); return; } let pad_len = w - total_len; if flags.zero_pad && (zero_overrides_left || !flags.left_justify) { output.push_str(sign_prefix); for _ in 0..pad_len { output.push('0'); } output.push_str(content); } else if flags.left_justify { output.push_str(sign_prefix); output.push_str(content); for _ in 0..pad_len { output.push(' '); } } else { for _ in 0..pad_len { output.push(' '); } output.push_str(sign_prefix); output.push_str(content); } } /// Compute sign prefix for a numeric value. fn sign_prefix(negative: bool, flags: &FormatFlags) -> &'static str { if negative { "-" } else if flags.force_sign { "+" } else if flags.space_sign { " " } else { "" } } /// Strip trailing zeros from a decimal number string, but always keep at least /// one digit after the decimal point. E.g. "3.140000" → "3.14", "1.000000" → "1.0". /// If there is no decimal point, appends ".0". fn ensure_decimal_strip_zeros(s: &str) -> String { if s.contains('.') { let trimmed = s.trim_end_matches('0'); // Keep at least one digit after '.' if trimmed.ends_with('.') { format!("{trimmed}0") } else { trimmed.to_string() } } else { format!("{s}.0") } } /// Build exponential mantissa from decoded digits: first digit, optional '.', /// then `precision` more digits (sqlite3.c:32581-32602 in etEXP path). /// Returns (mantissa_string, flag_dp). fn build_exp_mantissa(digits: &[u8], precision: usize, flags: &FormatFlags) -> (String, bool) { let mut mantissa = String::new(); mantissa.push((b'0' + digits.first().copied().unwrap_or(0)) as char); let flag_dp = precision > 0 || flags.alternate || flags.alt_form_2; if flag_dp { mantissa.push('.'); } let mut j = 1_usize; for _ in 0..precision { if j < digits.len() { mantissa.push((b'0' + digits[j]) as char); j += 1; } else { mantissa.push('0'); } } (mantissa, flag_dp) } /// SQLite RTZ (Remove Trailing Zeros) — strip trailing zeros after the decimal /// point. If `keep_dot_zero` is true (alt_form_2 / `!` flag), a bare "." becomes /// ".0"; otherwise the dot is also removed. (sqlite3.c:32604-32613) fn strip_trailing_zeros(s: &mut String, keep_dot_zero: bool) { let trimmed = s.trim_end_matches('0'); *s = if trimmed.ends_with('.') { if keep_dot_zero { format!("{trimmed}0") } else { trimmed.trim_end_matches('.').to_string() } } else { trimmed.to_string() }; } /// Pad a digit string to at least `min_digits` characters with leading zeros. fn pad_with_precision(digits: String, precision: Option) -> String { let min_digits = precision.unwrap_or(1); if digits.len() < min_digits { "0".repeat(min_digits - digits.len()) + &digits } else { digits } } /// Dekker-style double-double multiplication, ported from SQLite's `dekkerMul2` /// (sqlite3.c:36334). Multiplies the double-double number x = (x[0], x[1]) /// by the double-double constant (y, yy). fn dekker_mul2(x: &mut [f64; 2], y: f64, yy: f64) { let hx = f64::from_bits(x[0].to_bits() & 0xffff_ffff_fc00_0000); let tx = x[0] - hx; let hy = f64::from_bits(y.to_bits() & 0xffff_ffff_fc00_0000); let ty = y - hy; let p = hx * hy; let q = hx * ty + tx * hy; let c = p + q; let cc = p - c + q + tx * ty; let cc = x[0] * yy + x[1] * y + cc; x[0] = c + cc; x[1] = c - x[0] + cc; } /// Decode a positive finite float into significant decimal digits and a /// decimal point position, then round to `i_round` significant digits /// (capped at `max_round`). /// /// This is a faithful port of SQLite's `sqlite3FpDecode` (sqlite3.c:36884). /// It uses Dekker-style double-double arithmetic to scale the value into a /// u64-representable range, producing the same digit sequences as SQLite. /// /// * `i_round` – for `%f` pass `-(precision as i32)`, for `%e` pass /// `precision + 1`, for `%g` pass `precision`. /// * `max_round` – typically 16 (or 26 with `!` flag). /// /// Returns `(digits, iDP)` where `digits` is a non-empty vector of digit /// values 0–9 (trailing zeros stripped) and `iDP` is the number of digits /// before the decimal point. fn fp_decode(r: f64, i_round: i32, max_round: usize) -> (Vec, i32) { debug_assert!(r > 0.0); // SQLite (sqlite3.c:32502-32505): infinity with zero-pad is represented // as digit '9' at decimal position 1000 (i.e. 9 * 10^999). if r.is_infinite() { return (vec![9], 1000); } let mut rr = [r, 0.0_f64]; let mut exp: i32 = 0; // Scale r into [9.223_372_036_854_774_784e17, 9.223_372_036_854_774_784e18] // using Dekker multiplication with error-compensation constants. // Constants are copied verbatim from sqlite3.c:36930-36955. #[allow(clippy::excessive_precision)] if rr[0] > 9.223_372_036_854_774_784e+18 { while rr[0] > 9.223_372_036_854_774_784e+118 { exp += 100; dekker_mul2(&mut rr, 1.0e-100, -1.999_189_980_260_288_361_96e-117); } while rr[0] > 9.223_372_036_854_774_784e+28 { exp += 10; dekker_mul2(&mut rr, 1.0e-10, -3.643_219_731_549_774_157_9e-27); } while rr[0] > 9.223_372_036_854_774_784e+18 { exp += 1; dekker_mul2(&mut rr, 1.0e-01, -5.551_115_123_125_782_702_1e-18); } } else { while rr[0] < 9.223_372_036_854_774_784e-83 { exp -= 100; dekker_mul2(&mut rr, 1.0e+100, -1.590_289_110_975_991_804_6e+83); } while rr[0] < 9.223_372_036_854_774_784e+07 { exp -= 10; dekker_mul2(&mut rr, 1.0e+10, 0.0); } while rr[0] < 9.223_372_036_854_774_78e+17 { exp -= 1; dekker_mul2(&mut rr, 1.0e+01, 0.0); } } // Convert double-double to u64 let v: u64 = if rr[1] < 0.0 { (rr[0] as u64).wrapping_sub((-rr[1]) as u64) } else { (rr[0] as u64).wrapping_add(rr[1] as u64) }; // Extract decimal digits from u64 let mut buf = Vec::with_capacity(20); let mut temp = v; while temp > 0 { buf.push((temp % 10) as u8); temp /= 10; } buf.reverse(); let n = buf.len(); let mut dp = n as i32 + exp; // ── Rounding (sqlite3.c:36968-36997) ────────────────────────── let mut i_round = i_round; if i_round <= 0 { i_round = dp - i_round; if i_round == 0 && !buf.is_empty() && buf[0] >= 5 { buf.insert(0, 0); dp += 1; i_round = 1; } } let n = buf.len(); if i_round > 0 && ((i_round as usize) < n || n > max_round) { let i_round = if (i_round as usize) > max_round { max_round } else { i_round as usize }; let mut carried_past = false; if i_round < n && buf[i_round] >= 5 { let mut j = i_round; loop { if j == 0 { buf.insert(0, 1); dp += 1; carried_past = true; break; } j -= 1; buf[j] += 1; if buf[j] <= 9 { break; } buf[j] = 0; } } let keep = if carried_past { i_round + 1 } else { i_round }; buf.truncate(keep); } // Trim trailing zeros (sqlite3.c:37001-37003) while buf.len() > 1 && *buf.last().unwrap() == 0 { buf.pop(); } (buf, dp) } /// Build a fixed-point decimal string from a positive float, extracting /// significant digits via `fp_decode` (a faithful port of SQLite's /// `sqlite3FpDecode`) then placing them according to the `etFLOAT` layout. fn format_fixed_from_digits(abs_f: f64, precision: usize, max_sig: usize) -> String { if abs_f == 0.0 { return if precision == 0 { "0".to_string() } else { format!("0.{}", "0".repeat(precision)) }; } let i_round = -(precision as i32); let (digits, dp) = fp_decode(abs_f, i_round, max_sig); // ── Integer part (sqlite3.c:32581-32588) ─────────────────────── let mut result = String::new(); let mut j: usize = 0; if dp <= 0 { result.push('0'); } else { for _ in 0..dp { if j < digits.len() { result.push((b'0' + digits[j]) as char); j += 1; } else { result.push('0'); } } } if precision == 0 { return result; } // ── Fractional part (sqlite3.c:32591-32602) ──────────────────── result.push('.'); // Leading zeros for numbers < 1 (e2 < 0 in SQLite, dp <= 0 here) let mut e2 = dp - 1; let mut frac_remaining = precision; e2 += 1; // mirrors the for(e2++;...) in SQLite while e2 < 0 && frac_remaining > 0 { result.push('0'); frac_remaining -= 1; e2 += 1; } // Significant digits while frac_remaining > 0 { if j < digits.len() { result.push((b'0' + digits[j]) as char); j += 1; } else { result.push('0'); } frac_remaining -= 1; } result } /// Limit a formatted numeric string to `max_sig` significant digits, rounding /// at the boundary. This matches SQLite's behavior of not showing IEEE 754 /// mantissa noise beyond the float's representable precision. #[cfg(test)] fn limit_significant_digits(s: &str, max_sig: usize) -> String { let chars: Vec = s.chars().collect(); let mut result: Vec = chars.clone(); // Find positions of all digits and track significant digit count let mut digit_positions: Vec = Vec::new(); let mut sig_count = 0; let mut first_nonzero = false; for (i, &c) in chars.iter().enumerate() { if !c.is_ascii_digit() { continue; } if c != '0' || first_nonzero { first_nonzero = true; sig_count += 1; } digit_positions.push(i); } if sig_count <= max_sig { return s.to_string(); } // Find the index in digit_positions where the (max_sig+1)th significant digit is let mut sig_seen = 0; let mut round_pos = None; // position of the (max_sig+1)th sig digit let mut last_sig_pos = None; // position of the max_sig-th sig digit let mut first_nonzero2 = false; for &pos in &digit_positions { let c = chars[pos]; if c != '0' || first_nonzero2 { first_nonzero2 = true; sig_seen += 1; } if sig_seen == max_sig { last_sig_pos = Some(pos); } if sig_seen == max_sig + 1 { round_pos = Some(pos); break; } } let (Some(round_pos), Some(_last_sig_pos)) = (round_pos, last_sig_pos) else { return s.to_string(); }; // Check if we need to round up (digit at round_pos >= 5) let round_digit = chars[round_pos].to_digit(10).unwrap(); // Zero out all digits from round_pos onward for &pos in &digit_positions { if pos >= round_pos { result[pos] = '0'; } } // If round digit >= 5, propagate carry backwards if round_digit >= 5 { // Walk backwards through digit positions before round_pos let mut carry = true; for &pos in digit_positions.iter().rev() { if pos >= round_pos { continue; } if !carry { break; } let d = result[pos].to_digit(10).unwrap() + 1; if d >= 10 { result[pos] = '0'; } else { result[pos] = char::from_digit(d, 10).unwrap(); carry = false; } } // If carry propagated past all digits, insert a '1' before the first digit if carry { let first_digit_pos = digit_positions[0]; result.insert(first_digit_pos, '1'); } } result.into_iter().collect() } /// Handle NaN and non-zero_pad Infinity for float specifiers. /// NaN: zero_pad → "null", otherwise → "NaN" /// Infinity (non-zero_pad): "Inf"/"-Inf"/"+Inf" /// Infinity with zero_pad is NOT handled here — it falls through to normal /// formatting where fp_decode returns digits=[9], dp=1000 (sqlite3.c:32502-32505). fn format_special_float(output: &mut String, f: f64, spec: &FormatSpec) { // Width padding uses spaces only (SQLite breaks out before zero-pad code). let mut space_flags = spec.flags.clone(); space_flags.zero_pad = false; if f.is_nan() { let text = if spec.flags.zero_pad { "null" } else { "NaN" }; apply_width(output, "", text, spec.width, &space_flags, false); return; } // Non-zero_pad infinity let prefix = sign_prefix(f < 0.0, &spec.flags); apply_width(output, prefix, "Inf", spec.width, &space_flags, false); } // ── Per-specifier formatters ──────────────────────────────────── fn format_signed_int(output: &mut String, value: &Value, spec: &FormatSpec) { let i = coerce_to_i64(value); let negative = i < 0; let digits = i.unsigned_abs().to_string(); let mut padded = pad_with_precision(digits, spec.precision); let prefix = sign_prefix(negative, &spec.flags); if spec.flags.comma_sep && spec.flags.zero_pad { // SQLite: zero-pad digits to (width - prefix.len()), then insert commas. // Commas are not counted in the width. Left-justify is ignored when // both comma and zero-pad are set. let w = spec.width.unwrap_or(0); let digit_target = w.saturating_sub(prefix.len()); if padded.len() < digit_target { padded = "0".repeat(digit_target - padded.len()) + &padded; } output.push_str(prefix); output.push_str(&insert_commas(&padded)); } else if spec.flags.comma_sep { padded = insert_commas(&padded); apply_width(output, prefix, &padded, spec.width, &spec.flags, true); } else { apply_width(output, prefix, &padded, spec.width, &spec.flags, true); } } fn format_unsigned_int(output: &mut String, value: &Value, spec: &FormatSpec) { let i = coerce_to_i64(value); let u = i as u64; let digits = u.to_string(); let mut padded = pad_with_precision(digits, spec.precision); if spec.flags.comma_sep && spec.flags.zero_pad { // SQLite: zero-pad digits to width, then insert commas. // Commas are not counted in the width. Left-justify is ignored when // both comma and zero-pad are set. let w = spec.width.unwrap_or(0); if padded.len() < w { padded = "0".repeat(w - padded.len()) + &padded; } output.push_str(&insert_commas(&padded)); } else if spec.flags.comma_sep { padded = insert_commas(&padded); apply_width(output, "", &padded, spec.width, &spec.flags, true); } else { apply_width(output, "", &padded, spec.width, &spec.flags, true); } } fn format_hex(output: &mut String, value: &Value, spec: &FormatSpec, uppercase: bool) { let i = coerce_to_i64(value); let u = i as u64; let digits = if uppercase { format!("{u:X}") } else { format!("{u:x}") }; let padded = pad_with_precision(digits, spec.precision); let prefix = if spec.flags.alternate && u != 0 { // SQLite: %p always uses lowercase "0x" prefix even with uppercase digits. // %X uses "0X". Both from aPrefix[] in sqlite3.c:32037. if uppercase && spec.spec_type != 'p' { "0X" } else { "0x" } } else { "" }; // In SQLite, when # and 0 flags are both set, width applies to digits only // and the prefix is added on top (not counted in width). if spec.flags.alternate && spec.flags.zero_pad && !prefix.is_empty() { let w = spec.width.unwrap_or(0); let zero_padded = if padded.len() < w { "0".repeat(w - padded.len()) + &padded } else { padded }; output.push_str(prefix); output.push_str(&zero_padded); } else { apply_width(output, prefix, &padded, spec.width, &spec.flags, true); } } fn format_octal(output: &mut String, value: &Value, spec: &FormatSpec) { let i = coerce_to_i64(value); let u = i as u64; let digits = format!("{u:o}"); let padded = pad_with_precision(digits, spec.precision); // SQLite always adds "0" prefix for octal with # flag when value is non-zero, // even if precision padding already added leading zeros. let prefix = if spec.flags.alternate && u != 0 { "0" } else { "" }; // In SQLite, when # and 0 flags are both set, width applies to digits only // and the prefix is added on top (not counted in width). if spec.flags.alternate && spec.flags.zero_pad && !prefix.is_empty() { let w = spec.width.unwrap_or(0); let zero_padded = if padded.len() < w { "0".repeat(w - padded.len()) + &padded } else { padded }; output.push_str(prefix); output.push_str(&zero_padded); } else { apply_width(output, prefix, &padded, spec.width, &spec.flags, true); } } fn format_float_decimal(output: &mut String, value: &Value, spec: &FormatSpec) { let f = coerce_to_f64(value); // SQLite source (sqlite3.c:32497-32518): special float handling. // NaN and non-zero_pad Inf break out before normal formatting. // Inf with zero_pad falls through to normal code with digits=[9], dp=1000. if f.is_nan() || (f.is_infinite() && !spec.flags.zero_pad) { format_special_float(output, f, spec); return; } // Cap precision to avoid extreme allocations let precision = spec.precision.unwrap_or(6).min(1000); let negative = f < 0.0; let abs_f = f.abs(); // SQLite source: sqlite3FpDecode uses 16 sig digits, or 26 with ! flag let max_sig = if spec.flags.alt_form_2 { 26 } else { 16 }; // Build the base decimal string using digit extraction (matches SQLite's // sqlite3FpDecode + etFLOAT formatting). This replaces the previous // approach of Rust's format! + limit_significant_digits, which could // round the leading digit differently for very large numbers. let formatted = if spec.flags.alt_form_2 && precision == 0 { // %!.0f: force decimal point with one zero, e.g. "3.0" let mut s = format_fixed_from_digits(abs_f, 0, max_sig); s.push_str(".0"); s } else if precision == 0 { let mut s = format_fixed_from_digits(abs_f, 0, max_sig); if spec.flags.alternate { s.push('.'); } s } else { let s = format_fixed_from_digits(abs_f, precision, max_sig); if spec.flags.alt_form_2 { ensure_decimal_strip_zeros(&s) } else { s } }; // Apply comma separator to integer part let content = if spec.flags.comma_sep { if let Some(dot_pos) = formatted.find('.') { let int_part = &formatted[..dot_pos]; let frac_part = &formatted[dot_pos..]; insert_commas(int_part) + frac_part } else { insert_commas(&formatted) } } else { formatted }; // SQLite 3.51+ (sqlite3.c:32520-32532): Suppress minus sign for %f with # // when displayed value is zero and no +/space flag is set. let negative = if negative && spec.flags.alternate && !spec.flags.force_sign && !spec.flags.space_sign { !content.bytes().all(|b| b == b'0' || b == b'.' || b == b',') } else { negative }; let prefix = sign_prefix(negative, &spec.flags); apply_width(output, prefix, &content, spec.width, &spec.flags, false); } fn format_exponential(output: &mut String, value: &Value, spec: &FormatSpec, uppercase: bool) { let f = coerce_to_f64(value); if f.is_nan() || (f.is_infinite() && !spec.flags.zero_pad) { format_special_float(output, f, spec); return; } format_exponential_inner(output, f, spec, uppercase); } fn format_exponential_inner(output: &mut String, f: f64, spec: &FormatSpec, uppercase: bool) { let precision = spec.precision.unwrap_or(6).min(1000); let negative = f < 0.0; let abs_f = f.abs(); let e_char = if uppercase { 'E' } else { 'e' }; let max_sig = if spec.flags.alt_form_2 { 26 } else { 16 }; // Handle zero specially (fp_decode requires positive finite input) if abs_f == 0.0 { let flag_dp = precision > 0 || spec.flags.alternate || spec.flags.alt_form_2; let mut mantissa = "0".to_string(); if flag_dp { mantissa.push('.'); for _ in 0..precision { mantissa.push('0'); } } if spec.flags.alt_form_2 { mantissa = ensure_decimal_strip_zeros(&mantissa); } let content = format!("{mantissa}{e_char}+00"); let prefix = sign_prefix(negative, &spec.flags); apply_width(output, prefix, &content, spec.width, &spec.flags, false); return; } // Use fp_decode with iRound = precision+1 (total significant digits for %e) let i_round = (precision + 1) as i32; let (digits, dp) = fp_decode(abs_f, i_round, max_sig); let exp = dp - 1; let (mut mantissa, flag_dp) = build_exp_mantissa(&digits, precision, &spec.flags); // RTZ (remove trailing zeros): only with ! flag for %e (sqlite3.c:32557) if spec.flags.alt_form_2 && flag_dp { strip_trailing_zeros(&mut mantissa, true); } let content = format!("{mantissa}{e_char}{exp:+03}"); let prefix = sign_prefix(negative, &spec.flags); apply_width(output, prefix, &content, spec.width, &spec.flags, false); } fn format_general(output: &mut String, value: &Value, spec: &FormatSpec, uppercase: bool) { let f = coerce_to_f64(value); if f.is_nan() || (f.is_infinite() && !spec.flags.zero_pad) { format_special_float(output, f, spec); return; } format_general_inner(output, f, spec, uppercase); } fn format_general_inner(output: &mut String, f: f64, spec: &FormatSpec, uppercase: bool) { // SQLite: if precision == 0, set to 1 (line 32491) let precision = spec.precision.unwrap_or(6).clamp(1, 1000); let negative = f < 0.0; let abs_f = f.abs(); let e_char = if uppercase { 'E' } else { 'e' }; let max_sig = if spec.flags.alt_form_2 { 26 } else { 16 }; // Handle zero specially if abs_f == 0.0 { let flag_rtz = !spec.flags.alternate; let flag_dp = precision > 1 || spec.flags.alternate || spec.flags.alt_form_2; let mut s = "0".to_string(); if flag_dp { s.push('.'); for _ in 1..precision { s.push('0'); } } if flag_rtz && flag_dp { strip_trailing_zeros(&mut s, spec.flags.alt_form_2); } let prefix = sign_prefix(negative, &spec.flags); apply_width(output, prefix, &s, spec.width, &spec.flags, false); return; } // Call fp_decode with iRound = precision (significant digits for %g) let (digits, dp) = fp_decode(abs_f, precision as i32, max_sig); let exp = dp - 1; // SQLite: precision-- then check (lines 32547-32555) let precision = precision - 1; // flag_rtz for generic: ON unless # flag (line 32549) let flag_rtz = !spec.flags.alternate; let use_exp = exp < -4 || exp > precision as i32; let content = if use_exp { // ── Exponential notation ────────────────────────────────── let (mut mantissa, flag_dp) = build_exp_mantissa(&digits, precision, &spec.flags); if flag_rtz && flag_dp { strip_trailing_zeros(&mut mantissa, spec.flags.alt_form_2); } format!("{mantissa}{e_char}{exp:+03}") } else { // ── Fixed-point notation ────────────────────────────────── // SQLite: precision = precision - exp (line 32553), giving digits after decimal let frac_precision = if precision as i32 > exp { (precision as i32 - exp) as usize } else { 0 }; // Build fixed-point string from decoded digits let mut s = String::new(); let mut j: usize = 0; // Integer part if dp <= 0 { s.push('0'); } else { for _ in 0..dp { if j < digits.len() { s.push((b'0' + digits[j]) as char); j += 1; } else { s.push('0'); } } } let flag_dp = frac_precision > 0 || spec.flags.alternate || spec.flags.alt_form_2; if flag_dp { s.push('.'); } // Leading zeros for numbers < 1 let mut e2 = dp - 1; let mut frac_remaining = frac_precision; e2 += 1; while e2 < 0 && frac_remaining > 0 { s.push('0'); frac_remaining -= 1; e2 += 1; } // Significant digits in fractional part while frac_remaining > 0 { if j < digits.len() { s.push((b'0' + digits[j]) as char); j += 1; } else { s.push('0'); } frac_remaining -= 1; } if flag_rtz && flag_dp { strip_trailing_zeros(&mut s, spec.flags.alt_form_2); } s }; // Apply comma separator to integer part (only meaningful for fixed-point) let content = if spec.flags.comma_sep { if let Some(dot_pos) = content.find('.') { let int_part = &content[..dot_pos]; let frac_part = &content[dot_pos..]; insert_commas(int_part) + frac_part } else if !content.contains('e') && !content.contains('E') { insert_commas(&content) } else { content } } else { content }; let prefix = sign_prefix(negative, &spec.flags); apply_width(output, prefix, &content, spec.width, &spec.flags, false); } fn format_string(output: &mut String, value: &Value, spec: &FormatSpec) { // For blobs, truncate at first NUL byte (SQLite behavior) let s = match value { Value::Blob(b) => { let end = b.iter().position(|&byte| byte == 0).unwrap_or(b.len()); String::from_utf8_lossy(&b[..end]).to_string() } _ => coerce_to_string(value), }; let truncated = if let Some(prec) = spec.precision { // Truncate by character count (not bytes). SQLite uses bytes by default // and chars with !, but since blobs are already lossy-converted to UTF-8, // character-based truncation avoids mid-char splits. if let Some((byte_idx, _)) = s.char_indices().nth(prec) { &s[..byte_idx] } else { &s } } else { &s }; // Zero-pad flag is ignored for string specifiers let mut flags = spec.flags.clone(); flags.zero_pad = false; apply_width(output, "", truncated, spec.width, &flags, false); } fn format_char(output: &mut String, value: &Value, spec: &FormatSpec) { // In SQLite SQL context, %c takes the first character of the string representation let c = match value { Value::Text(t) => t.value.chars().next().unwrap_or('\0'), _ => { let s = coerce_to_string(value); s.chars().next().unwrap_or('\0') } }; // NUL character produces no output (matches SQLite behavior) if c == '\0' { return; } // Precision on %c repeats the character that many times (default 1) let repeat = spec.precision.unwrap_or(1).max(1); let s: String = repeat_n(c, repeat).collect(); // Zero-pad flag is ignored for char specifiers let mut flags = spec.flags.clone(); flags.zero_pad = false; apply_width(output, "", &s, spec.width, &flags, false); } /// Escape control characters (0x00-0x1f, 0x7f) as \uXXXX for %#q/%#Q. fn escape_control_chars(s: &str) -> String { let mut result = String::with_capacity(s.len()); for c in s.chars() { if c.is_ascii_control() { write!(result, "\\u{:04x}", c as u32).unwrap(); } else if c == '\\' { result.push_str("\\\\"); } else { result.push(c); } } result } fn format_sql_quote(output: &mut String, value: &Value, spec: &FormatSpec) { // %q: double single quotes; NULL → (NULL). Supports width/precision. // %#q: also escape control characters as \uXXXX. let mut flags = spec.flags.clone(); flags.zero_pad = false; match value { Value::Null => { let truncated = truncate_to_precision("(NULL)", spec.precision); apply_width(output, "", truncated, spec.width, &flags, false); } _ => { let s = coerce_to_string(value); let truncated = truncate_to_precision(&s, spec.precision); let mut escaped = truncated.replace('\'', "''"); if spec.flags.alternate { escaped = escape_control_chars(&escaped); } apply_width(output, "", &escaped, spec.width, &flags, false); } } } fn format_sql_quote_wrap(output: &mut String, value: &Value, spec: &FormatSpec) { // %Q: like %q but wrapped in quotes; NULL → unquoted NULL. Supports width/precision. // %#Q: also escape control characters as \uXXXX. let mut flags = spec.flags.clone(); flags.zero_pad = false; match value { Value::Null => { let truncated = truncate_to_precision("NULL", spec.precision); apply_width(output, "", truncated, spec.width, &flags, false); } _ => { let s = coerce_to_string(value); let truncated = truncate_to_precision(&s, spec.precision); let mut escaped = truncated.replace('\'', "''"); // %#Q: escape control chars and wrap with unistr('...') if any are present. let use_unistr = if spec.flags.alternate { let has_ctrl = escaped.bytes().any(|b| b <= 0x1f); if has_ctrl { escaped = escape_control_chars(&escaped); true } else { false } } else { false }; let mut quoted = String::with_capacity(escaped.len() + 10); if use_unistr { quoted.push_str("unistr('"); } else { quoted.push('\''); } quoted.push_str(&escaped); if use_unistr { quoted.push_str("')"); } else { quoted.push('\''); } apply_width(output, "", "ed, spec.width, &flags, false); } } } fn format_sql_identifier(output: &mut String, value: &Value, spec: &FormatSpec) { // %w: double double-quotes (no wrapping quotes in SQL context); NULL → (NULL). let mut flags = spec.flags.clone(); flags.zero_pad = false; match value { Value::Null => { let truncated = truncate_to_precision("(NULL)", spec.precision); apply_width(output, "", truncated, spec.width, &flags, false); } _ => { let s = coerce_to_string(value); let truncated = truncate_to_precision(&s, spec.precision); let escaped = truncated.replace('"', "\"\""); apply_width(output, "", &escaped, spec.width, &flags, false); } } } fn format_ordinal(output: &mut String, value: &Value, spec: &FormatSpec) { // %r: format integer as ordinal (1st, 2nd, 3rd, 4th, ...) let i = coerce_to_i64(value); let negative = i < 0; let abs = i.unsigned_abs(); let suffix = match (abs % 100, abs % 10) { (11..=13, _) => "th", (_, 1) => "st", (_, 2) => "nd", (_, 3) => "rd", _ => "th", }; let mut digits = abs.to_string(); // Precision is the total width of digits+suffix, not just digits. let digit_prec = spec.precision.map(|p| p.saturating_sub(suffix.len())); digits = pad_with_precision(digits, digit_prec); let prefix = sign_prefix(negative, &spec.flags); // Zero-pad: pad the digit portion to fill width (0 overrides - like integers) if spec.flags.zero_pad { let w = spec.width.unwrap_or(0); let content_chars = prefix.len() + digits.len() + suffix.len(); if content_chars < w { digits = "0".repeat(w - content_chars) + &digits; } output.push_str(prefix); output.push_str(&digits); output.push_str(suffix); } else { let content = format!("{digits}{suffix}"); apply_width(output, prefix, &content, spec.width, &spec.flags, false); } } /// Truncate a string to at most `precision` characters. fn truncate_to_precision(s: &str, precision: Option) -> &str { match precision { Some(prec) => { // Truncate by character count. SQLite uses byte count by default // and character count with the ! flag, but since Turso is UTF-8 only, // character-based truncation is always correct for our strings. if let Some((byte_idx, _)) = s.char_indices().nth(prec) { &s[..byte_idx] } else { s } } _ => s, } } // ── Main entry point ──────────────────────────────────────────── pub fn exec_printf(values: &[Register]) -> crate::Result { if values.is_empty() { return Ok(Value::Null); } // SQLite converts the format argument to text if not already text. let format_value = values[0].get_value(); let fmt_owned: String; let format_str = match &format_value { Value::Text(t) => t.as_str(), Value::Null => return Ok(Value::Null), Value::Numeric(Numeric::Integer(i)) => { fmt_owned = i.to_string(); fmt_owned.as_str() } Value::Numeric(Numeric::Float(f)) => { fmt_owned = format_float(f64::from(*f)); fmt_owned.as_str() } Value::Blob(b) => { fmt_owned = String::from_utf8_lossy(b).to_string(); fmt_owned.as_str() } }; let mut result = String::new(); let mut args_index = 1; let mut chars = format_str.chars().peekable(); // Track whether any output or specifier processing happened. SQLite's // internal StrAccum buffer stays NULL until something triggers allocation // (any literal text, %%, trailing %, or any specifier including %n). // When an unknown specifier triggers early return before any allocation, // the result is NULL. Otherwise it's the accumulated text (possibly ""). let mut touched = false; while let Some(c) = chars.next() { if c != '%' { touched = true; result.push(c); continue; } // Check for %% if chars.peek() == Some(&'%') { touched = true; chars.next(); result.push('%'); continue; } // Trailing '%' at end of format string is preserved if chars.peek().is_none() { result.push('%'); break; } // Parse the full format specifier let spec = parse_format_spec(&mut chars, values, &mut args_index); // Get the argument value (or use NULL if missing) let needs_arg = !matches!(spec.spec_type, 'n' | '\0'); let null_val = Value::Null; let arg = if needs_arg { if args_index < values.len() { let v = values[args_index].get_value(); args_index += 1; v } else { &null_val } } else { &null_val }; match spec.spec_type { 'd' | 'i' => format_signed_int(&mut result, arg, &spec), 'u' => format_unsigned_int(&mut result, arg, &spec), 'f' => format_float_decimal(&mut result, arg, &spec), 'e' => format_exponential(&mut result, arg, &spec, false), 'E' => format_exponential(&mut result, arg, &spec, true), 'g' => format_general(&mut result, arg, &spec, false), 'G' => format_general(&mut result, arg, &spec, true), 'x' => format_hex(&mut result, arg, &spec, false), 'X' => format_hex(&mut result, arg, &spec, true), 'o' => format_octal(&mut result, arg, &spec), 'p' => format_hex(&mut result, arg, &spec, true), 's' | 'z' => format_string(&mut result, arg, &spec), 'c' => format_char(&mut result, arg, &spec), 'q' => format_sql_quote(&mut result, arg, &spec), 'Q' => format_sql_quote_wrap(&mut result, arg, &spec), 'w' => format_sql_identifier(&mut result, arg, &spec), 'r' => format_ordinal(&mut result, arg, &spec), 'n' => { /* silently ignored, no arg consumed */ } _ => { // Unknown specifier: return NULL if nothing was processed // before this point, otherwise return accumulated text. // This matches SQLite where the internal buffer (zText) stays // NULL until any append is attempted. if !touched { return Ok(Value::Null); } break; } } touched = true; } Ok(Value::build_text(result)) } #[cfg(test)] mod tests { use super::*; fn text(value: &str) -> Register { Register::Value(Value::build_text(value.to_string())) } fn integer(value: i64) -> Register { Register::Value(Value::from_i64(value)) } fn float(value: f64) -> Register { Register::Value(Value::from_f64(value)) } #[test] fn test_printf_no_args() { assert_eq!(exec_printf(&[]).unwrap(), Value::Null); } #[test] fn test_printf_basic_string() { assert_eq!( exec_printf(&[text("Hello World")]).unwrap(), *text("Hello World").get_value() ); } #[test] fn test_printf_string_formatting() { let test_cases = vec![ ( vec![text("Hello, %s!"), text("World")], text("Hello, World!"), ), ( vec![text("%s %s!"), text("Hello"), text("World")], text("Hello World!"), ), ( vec![text("Hello, %s!"), Register::Value(Value::Null)], text("Hello, !"), ), (vec![text("Value: %s"), integer(42)], text("Value: 42")), (vec![text("100%% complete")], text("100% complete")), ]; for (input, output) in test_cases { assert_eq!(exec_printf(&input).unwrap(), *output.get_value()); } } #[test] fn test_printf_integer_formatting() { let test_cases = vec![ (vec![text("Number: %d"), integer(42)], text("Number: 42")), (vec![text("Number: %d"), integer(-42)], text("Number: -42")), ( vec![text("%d + %d = %d"), integer(2), integer(3), integer(5)], text("2 + 3 = 5"), ), ( vec![text("Number: %d"), text("not a number")], text("Number: 0"), ), ( vec![text("Truncated float: %d"), float(3.9)], text("Truncated float: 3"), ), (vec![text("Number: %i"), integer(42)], text("Number: 42")), ]; for (input, output) in test_cases { assert_eq!(exec_printf(&input).unwrap(), *output.get_value()); } } #[test] fn test_printf_unsigned_integer_formatting() { let test_cases = vec![ (vec![text("Number: %u"), integer(42)], text("Number: 42")), ( vec![text("Negative: %u"), integer(-1)], text("Negative: 18446744073709551615"), ), (vec![text("NaN: %u"), text("not a number")], text("NaN: 0")), ]; for (input, output) in test_cases { assert_eq!(exec_printf(&input).unwrap(), *output.get_value()); } } #[test] fn test_printf_float_formatting() { let test_cases = vec![ ( vec![text("Number: %f"), float(42.5)], text("Number: 42.500000"), ), ( vec![text("Number: %f"), float(-42.5)], text("Number: -42.500000"), ), ( vec![text("Number: %f"), integer(42)], text("Number: 42.000000"), ), ( vec![text("Number: %f"), text("not a number")], text("Number: 0.000000"), ), ]; // Huge finite float must not overflow rounding to produce "inf" let huge = exec_printf(&[text("%f"), float(1e308)]).unwrap(); let huge_str = match &huge { Value::Text(t) => t.as_str().to_string(), _ => panic!("expected text"), }; assert!(huge_str.starts_with("9999999999999999")); assert!(huge_str.ends_with(".000000")); assert!(!huge_str.contains("inf")); for (input, expected) in test_cases { assert_eq!(exec_printf(&input).unwrap(), *expected.get_value()); } } #[test] fn test_printf_width_precision() { let test_cases = vec![ (vec![text("%.2f"), float(4.002)], text("4.00")), (vec![text("%05d"), integer(42)], text("00042")), (vec![text("%.5d"), integer(42)], text("00042")), (vec![text("%+d"), integer(42)], text("+42")), (vec![text("%.3s"), text("hello")], text("hel")), (vec![text("%08x"), integer(255)], text("000000ff")), (vec![text("%#x"), integer(255)], text("0xff")), ]; for (input, expected) in test_cases { assert_eq!(exec_printf(&input).unwrap(), *expected.get_value()); } } #[test] fn test_printf_dynamic_width() { assert_eq!( exec_printf(&[text("%.*f"), integer(2), float(3.14258)]).unwrap(), *text("3.14").get_value() ); } #[test] fn test_printf_character_formatting() { let test_cases = vec![ (vec![text("character: %c"), text("a")], text("character: a")), ( vec![text("character: %c"), text("this is a test")], text("character: t"), ), ( vec![text("character: %c"), integer(123)], text("character: 1"), ), ( vec![text("character: %c"), float(42.5)], text("character: 4"), ), // Empty string → NUL char → no output (matches SQLite) (vec![text("character: %c"), text("")], text("character: ")), // NULL → coerces to empty string → NUL → no output ( vec![text("character: %c"), Register::Value(Value::Null)], text("character: "), ), ]; for (input, expected) in test_cases { assert_eq!(exec_printf(&input).unwrap(), *expected.get_value()); } } #[test] fn test_printf_exponential_formatting() { let test_cases = vec![ ( vec![text("Exp: %e"), float(23000000.0)], text("Exp: 2.300000e+07"), ), ( vec![text("Exp: %e"), float(-23000000.0)], text("Exp: -2.300000e+07"), ), (vec![text("Exp: %e"), float(0.0)], text("Exp: 0.000000e+00")), ]; for (input, expected) in test_cases { assert_eq!(exec_printf(&input).unwrap(), *expected.get_value()); } } #[test] fn test_printf_general_formatting() { let test_cases = vec![ (vec![text("%g"), float(100.0)], text("100")), (vec![text("%g"), float(0.00123)], text("0.00123")), (vec![text("%g"), float(1.0)], text("1")), (vec![text("%g"), float(1.5)], text("1.5")), (vec![text("%g"), float(0.0)], text("0")), (vec![text("%g"), integer(42)], text("42")), // Comma separator applies to %G decimal notation (vec![text("%,G"), integer(1000)], text("1,000")), ( vec![text("%,.20G"), float(1234567.89)], text("1,234,567.89"), ), ]; for (input, expected) in test_cases { assert_eq!(exec_printf(&input).unwrap(), *expected.get_value()); } } #[test] fn test_printf_sql_quoting() { assert_eq!( exec_printf(&[text("%q"), text("it's")]).unwrap(), *text("it''s").get_value() ); assert_eq!( exec_printf(&[text("%Q"), text("it's")]).unwrap(), *text("'it''s'").get_value() ); assert_eq!( exec_printf(&[text("%Q"), Register::Value(Value::Null)]).unwrap(), *text("NULL").get_value() ); assert_eq!( exec_printf(&[text("%q"), Register::Value(Value::Null)]).unwrap(), *text("(NULL)").get_value() ); } #[test] fn test_printf_comma_separator() { assert_eq!( exec_printf(&[text("%,d"), integer(1234567)]).unwrap(), *text("1,234,567").get_value() ); assert_eq!( exec_printf(&[text("%,d"), integer(-1234567)]).unwrap(), *text("-1,234,567").get_value() ); } #[test] fn test_printf_edge_cases() { let test_cases = vec![ (vec![text("")], text("")), (vec![text("%%%%")], text("%%")), (vec![text("No substitutions")], text("No substitutions")), ( vec![text("%d%d%d"), integer(1), integer(2), integer(3)], text("123"), ), // Trailing % is preserved (vec![text("test%")], text("test%")), // Unknown specifier: NULL if nothing processed before, else accumulated text (vec![text("%d%j"), integer(42)], text("42")), (vec![text("hello%j")], text("hello")), (vec![text("%n%j")], text("")), // Negative zero should not show minus sign (vec![text("%f"), float(-0.0)], text("0.000000")), ]; for (input, expected) in test_cases { assert_eq!(exec_printf(&input).unwrap(), *expected.get_value()); } } #[test] fn test_printf_hexadecimal_formatting() { let test_cases = vec![ (vec![text("hex: %x"), integer(4)], text("hex: 4")), ( vec![text("hex: %X"), integer(15565303546)], text("hex: 39FC3AEFA"), ), ( vec![text("hex: %x"), integer(-15565303546)], text("hex: fffffffc603c5106"), ), (vec![text("hex: %x"), float(42.5)], text("hex: 2a")), (vec![text("hex: %x"), text("42")], text("hex: 2a")), (vec![text("hex: %x"), text("")], text("hex: 0")), ]; for (input, expected) in test_cases { assert_eq!(exec_printf(&input).unwrap(), *expected.get_value()); } } #[test] fn test_printf_octal_formatting() { let test_cases = vec![ (vec![text("octal: %o"), integer(4)], text("octal: 4")), (vec![text("octal: %o"), float(42.5)], text("octal: 52")), (vec![text("octal: %o"), text("42")], text("octal: 52")), // # flag always adds "0" prefix when value is non-zero (vec![text("%#o"), integer(8)], text("010")), (vec![text("%#o"), integer(0)], text("0")), // # flag with precision: "0" prefix added even if precision pads with zeros (vec![text("%#.5o"), integer(8)], text("000010")), ( vec![text("%#.20o"), integer(1000)], text("000000000000000001750"), ), ]; for (input, expected) in test_cases { assert_eq!(exec_printf(&input).unwrap(), *expected.get_value()); } } // ── Bug fix regression tests ──────────────────────────────────── #[test] fn test_rounding_half_away_from_zero() { // Bug 1: SQLite uses half-away-from-zero, not half-to-even assert_eq!( exec_printf(&[text("%.0f"), float(0.5)]).unwrap(), *text("1").get_value() ); assert_eq!( exec_printf(&[text("%.0f"), float(2.5)]).unwrap(), *text("3").get_value() ); assert_eq!( exec_printf(&[text("%.0f"), float(-0.5)]).unwrap(), *text("-1").get_value() ); assert_eq!( exec_printf(&[text("%.0e"), float(2.5)]).unwrap(), *text("3e+00").get_value() ); } #[test] fn test_alt_hex_zero_pad_width() { // Bug 2: # flag with 0 flag - prefix not counted in width assert_eq!( exec_printf(&[text("%#08x"), integer(255)]).unwrap(), *text("0x000000ff").get_value() ); assert_eq!( exec_printf(&[text("%#04x"), integer(255)]).unwrap(), *text("0x00ff").get_value() ); assert_eq!( exec_printf(&[text("%#08o"), integer(8)]).unwrap(), *text("000000010").get_value() ); } #[test] fn test_alt_flag_forces_decimal_point() { // Bug 3: # flag forces decimal point on %e and %g assert_eq!( exec_printf(&[text("%#.0e"), float(1.0)]).unwrap(), *text("1.e+00").get_value() ); assert_eq!( exec_printf(&[text("%#.0g"), float(1.0)]).unwrap(), *text("1.").get_value() ); assert_eq!( exec_printf(&[text("%#g"), float(100000.0)]).unwrap(), *text("100000.").get_value() ); } #[test] fn test_g_threshold_rounding() { // Bug 4: %g pre-rounding changes the exponent threshold assert_eq!( exec_printf(&[text("%g"), float(999999.5)]).unwrap(), *text("1e+06").get_value() ); assert_eq!( exec_printf(&[text("%.1g"), float(9.5)]).unwrap(), *text("1e+01").get_value() ); } #[test] fn test_zero_pad_ignored_for_strings() { // Bug 5: 0 flag should be ignored for %s and %c assert_eq!( exec_printf(&[text("%05s"), text("hi")]).unwrap(), *text(" hi").get_value() ); assert_eq!( exec_printf(&[text("%05c"), text("A")]).unwrap(), *text(" A").get_value() ); } #[test] fn test_q_width_precision() { // Bug 6: %q/%Q/%w should respect width and precision assert_eq!( exec_printf(&[text("%.2q"), text("hello")]).unwrap(), *text("he").get_value() ); assert_eq!( exec_printf(&[text("%10q"), text("hi")]).unwrap(), *text(" hi").get_value() ); assert_eq!( exec_printf(&[text("%10Q"), text("hi")]).unwrap(), *text(" 'hi'").get_value() ); } #[test] fn test_infinity_handling() { // SQLite source (sqlite3.c:32502): infinity + flag_zeropad → 9-fill // infinity without flag_zeropad → "Inf" let inf_f = exec_printf(&[text("%020f"), float(f64::INFINITY)]).unwrap(); let inf_str = match &inf_f { Value::Text(t) => t.as_str().to_string(), _ => panic!("expected text"), }; assert!(inf_str.starts_with("9000")); assert_eq!(inf_str.len(), 1007); // 9 + 999 zeros + ".000000" assert_eq!( exec_printf(&[text("%020e"), float(f64::INFINITY)]).unwrap(), *text("00000009.000000e+999").get_value() ); assert_eq!( exec_printf(&[text("%020g"), float(f64::INFINITY)]).unwrap(), *text("000000000000009e+999").get_value() ); // Without zero-pad → "Inf" (not 9-fill) assert_eq!( exec_printf(&[text("%e"), float(f64::INFINITY)]).unwrap(), *text("Inf").get_value() ); assert_eq!( exec_printf(&[text("%G"), float(f64::INFINITY)]).unwrap(), *text("Inf").get_value() ); assert_eq!( exec_printf(&[text("%f"), float(f64::INFINITY)]).unwrap(), *text("Inf").get_value() ); // With zero-pad but no width still triggers 9-fill assert_eq!( exec_printf(&[text("%0G"), float(f64::INFINITY)]).unwrap(), *text("9E+999").get_value() ); assert_eq!( exec_printf(&[text("%0,G"), float(f64::INFINITY)]).unwrap(), *text("9E+999").get_value() ); // Negative infinity assert_eq!( exec_printf(&[text("%e"), float(f64::NEG_INFINITY)]).unwrap(), *text("-Inf").get_value() ); assert_eq!( exec_printf(&[text("%020e"), float(f64::NEG_INFINITY)]).unwrap(), *text("-0000009.000000e+999").get_value() ); // # flag with %g infinity: RTZ disabled, so trailing zeros remain assert_eq!( exec_printf(&[text("%#0g"), float(f64::INFINITY)]).unwrap(), *text("9.00000e+999").get_value() ); // ! flag with %e infinity: RTZ enabled, strips to .0 assert_eq!( exec_printf(&[text("%!0e"), float(f64::INFINITY)]).unwrap(), *text("9.0e+999").get_value() ); // ! flag with %f infinity: strips trailing fractional zeros let inf_bang_f = exec_printf(&[text("%!0f"), float(f64::INFINITY)]).unwrap(); let inf_bang_str = match &inf_bang_f { Value::Text(t) => t.as_str().to_string(), _ => panic!("expected text"), }; assert!( inf_bang_str.ends_with(".0"), "Infinity with %!0f should end with .0, got: ...{}", &inf_bang_str[inf_bang_str.len().saturating_sub(10)..] ); } #[test] fn test_significant_digits_limiting() { // Default: 16 significant digits (hide IEEE noise) assert_eq!( exec_printf(&[text("%.20f"), float(1.0 / 3.0)]).unwrap(), *text("0.33333333333333330000").get_value() ); assert_eq!( exec_printf(&[text("%.20e"), float(1.0 / 3.0)]).unwrap(), *text("3.33333333333333300000e-01").get_value() ); // ! flag: 26 significant digits max, trailing zeros stripped (sqlite3.c:32496). // fp_decode extracts 19 digits from the u64; the ! flag's RTZ then strips // the trailing '0', yielding 19 fractional characters. assert_eq!( exec_printf(&[text("%!.20f"), float(1.0 / 3.0)]).unwrap(), *text("0.3333333333333333148").get_value() ); } #[test] fn test_nan_handling() { // Value::from_f64(NaN) returns Value::Null (NonNan rejects NaN), // so NaN is treated as NULL which coerces to 0.0 for float formats. // The NaN-specific formatting code (NaN/null output) is defense-in-depth // that can't be triggered through the Value system. assert_eq!( exec_printf(&[text("%f"), float(f64::NAN)]).unwrap(), *text("0.000000").get_value() ); assert_eq!( exec_printf(&[text("%e"), float(f64::NAN)]).unwrap(), *text("0.000000e+00").get_value() ); assert_eq!( exec_printf(&[text("%g"), float(f64::NAN)]).unwrap(), *text("0").get_value() ); } #[test] fn test_blob_nul_truncation() { // Bug 9: %s on blobs truncates at first NUL byte let blob_val = Register::Value(Value::Blob(vec![0x48, 0x00, 0x4C])); // H\0L let result = exec_printf(&[text("%s"), blob_val]).unwrap(); assert_eq!(result, *text("H").get_value()); let blob_hello = Register::Value(Value::Blob(b"Hello".to_vec())); assert_eq!( exec_printf(&[text("%s"), blob_hello]).unwrap(), *text("Hello").get_value() ); } #[test] fn test_limit_significant_digits_rounding() { // Verify the rounding behavior of limit_significant_digits assert_eq!(limit_significant_digits("123456789", 5), "123460000"); assert_eq!(limit_significant_digits("1.23456789", 5), "1.23460000"); assert_eq!(limit_significant_digits("0.001234", 3), "0.001230"); assert_eq!(limit_significant_digits("9.9999", 3), "10.0000"); assert_eq!(limit_significant_digits("0.099999", 4), "0.100000"); } #[test] fn test_i32_star_precision_wrapping() { // i32::MIN as * precision wraps back to itself after negation → treated as 0 assert_eq!( exec_printf(&[text("%.*d"), integer(-2147483648), integer(42)]).unwrap(), *text("42").get_value() ); // 4294967295 as i64 → -1 as i32 → wrapping_neg → 1 assert_eq!( exec_printf(&[text("%.*d"), integer(4294967295), integer(42)]).unwrap(), *text("42").get_value() ); } #[test] fn test_comma_zero_pad_interaction() { // When comma + zero_pad: zero-pad digits to width, then insert commas // Width 15 = 15 digit positions, commas added on top assert_eq!( exec_printf(&[text("%0,15d"), integer(42)]).unwrap(), *text("000,000,000,000,042").get_value() ); assert_eq!( exec_printf(&[text("%0,15u"), integer(42)]).unwrap(), *text("000,000,000,000,042").get_value() ); // Left-justify is ignored when comma + zero-pad are both set assert_eq!( exec_printf(&[text("%-0,15d"), integer(42)]).unwrap(), *text("000,000,000,000,042").get_value() ); } #[test] fn test_ordinal_format() { assert_eq!( exec_printf(&[text("%r"), integer(1)]).unwrap(), *text("1st").get_value() ); assert_eq!( exec_printf(&[text("%r"), integer(2)]).unwrap(), *text("2nd").get_value() ); assert_eq!( exec_printf(&[text("%r"), integer(3)]).unwrap(), *text("3rd").get_value() ); assert_eq!( exec_printf(&[text("%r"), integer(11)]).unwrap(), *text("11th").get_value() ); assert_eq!( exec_printf(&[text("%r"), integer(112)]).unwrap(), *text("112th").get_value() ); assert_eq!( exec_printf(&[text("%.5r"), integer(-39)]).unwrap(), *text("-039th").get_value() ); assert_eq!( exec_printf(&[text("% r"), integer(42)]).unwrap(), *text(" 42nd").get_value() ); // Zero-pad pads the digits before the suffix assert_eq!( exec_printf(&[text("%010r"), integer(0)]).unwrap(), *text("00000000th").get_value() ); } #[test] fn test_q_null_precision_truncation() { // Precision truncates the NULL literal representation assert_eq!( exec_printf(&[text("%.0q"), Register::Value(Value::Null)]).unwrap(), *text("").get_value() ); assert_eq!( exec_printf(&[text("%.3q"), Register::Value(Value::Null)]).unwrap(), *text("(NU").get_value() ); assert_eq!( exec_printf(&[text("%.0Q"), Register::Value(Value::Null)]).unwrap(), *text("").get_value() ); } #[test] fn test_q_null_width_padding() { // Width applies to the NULL representation assert_eq!( exec_printf(&[text("%-10q"), Register::Value(Value::Null)]).unwrap(), *text("(NULL) ").get_value() ); } #[test] fn test_unknown_specifier_returns_early() { // Unknown specifier as first thing → NULL (SQLite's StrAccum never allocated) assert_eq!( exec_printf(&[text("%b"), integer(42)]).unwrap(), Value::Null, ); // Unknown specifier after literal text → accumulated text assert_eq!( exec_printf(&[text("hello%b"), integer(42)]).unwrap(), *text("hello").get_value() ); // Unknown specifier after %n → "" (StrAccum was allocated by %n processing) assert_eq!( exec_printf(&[text("%n%b"), integer(42)]).unwrap(), *text("").get_value(), ); } #[test] fn test_control_char_escaping_with_hash_q() { // %#q escapes control characters as \uXXXX and doubles backslashes assert_eq!( exec_printf(&[text("%#q"), text("a\nb")]).unwrap(), *text("a\\u000ab").get_value() ); assert_eq!( exec_printf(&[text("%#q"), text("a\tb")]).unwrap(), *text("a\\u0009b").get_value() ); // Backslash is doubled in escape mode assert_eq!( exec_printf(&[text("%#q"), text("a\\b")]).unwrap(), *text("a\\\\b").get_value() ); } #[test] fn test_hash_q_upper_unistr_wrapping() { // %#Q wraps with unistr('...') when control chars are present assert_eq!( exec_printf(&[text("%#Q"), text("a\nb")]).unwrap(), *text("unistr('a\\u000ab')").get_value() ); // %#Q without control chars — no unistr wrapping assert_eq!( exec_printf(&[text("%#Q"), text("hello")]).unwrap(), *text("'hello'").get_value() ); // %Q without # — no unistr wrapping even with control chars assert_eq!( exec_printf(&[text("%Q"), text("a\nb")]).unwrap(), *text("'a\nb'").get_value() ); } #[test] fn test_very_small_float_no_nan() { // 1e-300 with %G should not produce NaN — round_half_away_e must handle // subnormal scale values from 10^(-309+) without dividing by ~0. let result = exec_printf(&[text("%.*G"), integer(10), float(1e-300)]).unwrap(); assert_eq!(result, *text("1E-300").get_value()); // Also test with %e let result = exec_printf(&[text("%.10e"), float(1e-300)]).unwrap(); assert!( !result.to_string().contains("NaN"), "1e-300 with %e should not produce NaN" ); // And %g let result = exec_printf(&[text("%.10g"), float(1e-300)]).unwrap(); assert!( !result.to_string().contains("NaN"), "1e-300 with %g should not produce NaN" ); } #[test] fn test_large_float_f_format() { // 1e308 with %f must produce leading digits "9999..." (matching SQLite's // sqlite3FpDecode), NOT "1000..." (which Rust's format! produces). let result = exec_printf(&[text("%.0f"), float(1e308)]).unwrap(); let s = result.to_string(); assert!( s.starts_with("99999999999999990"), "1e308 with %.0f should start with 9999..., got: {}", &s[..s.len().min(40)] ); // With commas too let result = exec_printf(&[text("%,f"), float(1e308)]).unwrap(); let s = result.to_string(); assert!( s.starts_with("99,999,999,999,999,990"), "1e308 with %,f should start with 99,999..., got: {}", &s[..s.len().min(40)] ); } #[test] fn test_negative_zero_suppression() { // SQLite 3.51+ (sqlite3.c:32520-32532): With # flag (no + or space), // %f suppresses minus sign when displayed value rounds to zero. // -0.0000001 with %#f displays as 0.000000 — suppress minus let result = exec_printf(&[text("%#f"), float(-0.0000001)]).unwrap(); assert_eq!(result.to_string(), "0.000000"); // Same without # flag — keep minus let result = exec_printf(&[text("%f"), float(-0.0000001)]).unwrap(); assert_eq!(result.to_string(), "-0.000000"); // With + flag, # doesn't suppress (flag_prefix is set) let result = exec_printf(&[text("%#+f"), float(-0.0000001)]).unwrap(); assert_eq!(result.to_string(), "-0.000000"); // -0.5 rounds to -1, not zero — keep minus let result = exec_printf(&[text("%#.0f"), float(-0.5)]).unwrap(); assert_eq!(result.to_string(), "-1."); // -0.4 rounds to 0 — suppress let result = exec_printf(&[text("%#.0f"), float(-0.4)]).unwrap(); assert_eq!(result.to_string(), "0."); // -0.0000001 with comma separator let result = exec_printf(&[text("%#,f"), float(-0.0000001)]).unwrap(); assert_eq!(result.to_string(), "0.000000"); } } ================================================ FILE: core/incremental/aggregate_operator.rs ================================================ // Aggregate operator for DBSP-style incremental computation use crate::function::{AggFunc, Func}; use crate::incremental::dbsp::Hash128; use crate::incremental::dbsp::{Delta, DeltaPair, HashableRow}; use crate::incremental::operator::{ generate_storage_id, ComputationTracker, DbspStateCursors, EvalState, IncrementalOperator, }; use crate::incremental::persistence::{ReadRecord, WriteRow}; use crate::numeric::Numeric; use crate::storage::btree::CursorTrait; use crate::sync::Arc; use crate::sync::Mutex; use crate::types::{IOResult, ImmutableRecord, SeekKey, SeekOp, SeekResult, ValueRef}; use crate::{return_and_restore_if_io, return_if_io, LimboError, Result, Value}; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::collections::BTreeMap; use std::fmt::{self, Display}; // Architecture of the Aggregate Operator // ======================================== // // This operator implements SQL aggregations (GROUP BY, DISTINCT, COUNT, SUM, AVG, MIN, MAX) // using DBSP-style incremental computation. The key insight is that all these operations // can be expressed as operations on weighted sets (Z-sets) stored in persistent BTrees. // // ## Storage Strategy // // We use three different storage encodings (identified by 2-bit type codes in storage IDs): // - **Regular aggregates** (COUNT/SUM/AVG): Store accumulated state as a blob // - **MIN/MAX aggregates**: Store individual values; BTree ordering gives us min/max efficiently // - **DISTINCT tracking**: Store distinct values with weights (positive = present, zero = deleted) // // ## MIN/MAX Handling // // MIN/MAX are special because they're not fully incrementalizable: // - **Inserts**: Can be computed incrementally (new_min = min(old_min, new_value)) // - **Deletes**: Must recompute from the BTree when the current min/max is deleted // // Our approach: // 1. Store each value with its weight in a BTree (leveraging natural ordering) // 2. On insert: Simply compare with current min/max (incremental) // 3. On delete of current min/max: Scan the BTree to find the next min/max // - For MIN: scan forward from the beginning to find first value with positive weight // - For MAX: scan backward from the end to find last value with positive weight // // ## DISTINCT Handling // // DISTINCT operations (COUNT(DISTINCT), SUM(DISTINCT), etc.) are implemented using the // weighted set pattern: // - Each distinct value is stored with a weight (occurrence count) // - Weight > 0 means the value exists in the current dataset // - Weight = 0 means the value has been deleted (we may clean these up) // - We track transitions: when a value's weight crosses zero (appears/disappears) // // ## Plain DISTINCT (SELECT DISTINCT) // // A clever reuse of infrastructure: SELECT DISTINCT x, y, z is compiled to: // - GROUP BY x, y, z (making each unique row combination a group) // - Empty aggregates vector (no actual aggregations to compute) // - The groups themselves become the distinct rows // // This allows us to reuse all the incremental machinery for DISTINCT without special casing. // The `is_distinct_only` flag indicates this pattern, where the groups ARE the output rows. // // ## State Machines // // The operator uses async-ready state machines to handle I/O operations: // - **Eval state machine**: Fetches existing state, applies deltas, recomputes MIN/MAX // - **Commit state machine**: Persists updated state back to storage // - Each state represents a resumption point for when I/O operations yield /// Constants for aggregate type encoding in storage IDs (2 bits) pub const AGG_TYPE_REGULAR: u8 = 0b00; // COUNT/SUM/AVG pub const AGG_TYPE_MINMAX: u8 = 0b01; // MIN/MAX (BTree ordering gives both) pub const AGG_TYPE_DISTINCT: u8 = 0b10; // DISTINCT values tracking /// Hash a Value to generate an element_id for DISTINCT storage /// Uses HashableRow with column_idx as rowid for consistent hashing fn hash_value(value: &Value, column_idx: usize) -> Hash128 { // Use column_idx as rowid to ensure different columns with same value get different hashes let row = HashableRow::new(column_idx as i64, vec![value.clone()]); row.cached_hash() } // Serialization type codes for aggregate functions const AGG_FUNC_COUNT: i64 = 0; const AGG_FUNC_SUM: i64 = 1; const AGG_FUNC_AVG: i64 = 2; const AGG_FUNC_MIN: i64 = 3; const AGG_FUNC_MAX: i64 = 4; const AGG_FUNC_COUNT_DISTINCT: i64 = 5; const AGG_FUNC_SUM_DISTINCT: i64 = 6; const AGG_FUNC_AVG_DISTINCT: i64 = 7; #[derive(Debug, Clone, PartialEq)] pub enum AggregateFunction { Count, CountDistinct(usize), // COUNT(DISTINCT column_index) Sum(usize), // Column index SumDistinct(usize), // SUM(DISTINCT column_index) Avg(usize), // Column index AvgDistinct(usize), // AVG(DISTINCT column_index) Min(usize), // Column index Max(usize), // Column index } impl Display for AggregateFunction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AggregateFunction::Count => write!(f, "COUNT(*)"), AggregateFunction::CountDistinct(idx) => write!(f, "COUNT(DISTINCT col{idx})"), AggregateFunction::Sum(idx) => write!(f, "SUM(col{idx})"), AggregateFunction::SumDistinct(idx) => write!(f, "SUM(DISTINCT col{idx})"), AggregateFunction::Avg(idx) => write!(f, "AVG(col{idx})"), AggregateFunction::AvgDistinct(idx) => write!(f, "AVG(DISTINCT col{idx})"), AggregateFunction::Min(idx) => write!(f, "MIN(col{idx})"), AggregateFunction::Max(idx) => write!(f, "MAX(col{idx})"), } } } impl AggregateFunction { /// Get the default output column name for this aggregate function #[inline] pub fn default_output_name(&self) -> String { self.to_string() } /// Serialize this aggregate function to a Value /// Returns a vector of values: [type_code, optional_column_index] pub fn to_values(&self) -> Vec { match self { AggregateFunction::Count => vec![Value::Numeric(Numeric::Integer(AGG_FUNC_COUNT))], AggregateFunction::CountDistinct(idx) => { vec![ Value::Numeric(Numeric::Integer(AGG_FUNC_COUNT_DISTINCT)), Value::from_i64(*idx as i64), ] } AggregateFunction::Sum(idx) => { vec![ Value::Numeric(Numeric::Integer(AGG_FUNC_SUM)), Value::from_i64(*idx as i64), ] } AggregateFunction::SumDistinct(idx) => { vec![ Value::Numeric(Numeric::Integer(AGG_FUNC_SUM_DISTINCT)), Value::from_i64(*idx as i64), ] } AggregateFunction::Avg(idx) => { vec![ Value::Numeric(Numeric::Integer(AGG_FUNC_AVG)), Value::from_i64(*idx as i64), ] } AggregateFunction::AvgDistinct(idx) => { vec![ Value::Numeric(Numeric::Integer(AGG_FUNC_AVG_DISTINCT)), Value::from_i64(*idx as i64), ] } AggregateFunction::Min(idx) => { vec![ Value::Numeric(Numeric::Integer(AGG_FUNC_MIN)), Value::from_i64(*idx as i64), ] } AggregateFunction::Max(idx) => { vec![ Value::Numeric(Numeric::Integer(AGG_FUNC_MAX)), Value::from_i64(*idx as i64), ] } } } /// Deserialize an aggregate function from values /// Consumes values from the cursor and returns the aggregate function pub fn from_values(values: &[Value], cursor: &mut usize) -> Result { let type_code = values .get(*cursor) .ok_or_else(|| LimboError::InternalError("Missing aggregate type code".into()))?; let agg_fn = match type_code { Value::Numeric(Numeric::Integer(AGG_FUNC_COUNT)) => { *cursor += 1; AggregateFunction::Count } Value::Numeric(Numeric::Integer(AGG_FUNC_COUNT_DISTINCT)) => { *cursor += 1; let idx = values.get(*cursor).ok_or_else(|| { LimboError::InternalError("Missing COUNT(DISTINCT) column index".into()) })?; if let Value::Numeric(Numeric::Integer(idx)) = idx { *cursor += 1; AggregateFunction::CountDistinct(*idx as usize) } else { return Err(LimboError::InternalError(format!( "Expected Integer for COUNT(DISTINCT) column index, got {idx:?}" ))); } } Value::Numeric(Numeric::Integer(AGG_FUNC_SUM)) => { *cursor += 1; let idx = values .get(*cursor) .ok_or_else(|| LimboError::InternalError("Missing SUM column index".into()))?; if let Value::Numeric(Numeric::Integer(idx)) = idx { *cursor += 1; AggregateFunction::Sum(*idx as usize) } else { return Err(LimboError::InternalError(format!( "Expected Integer for SUM column index, got {idx:?}" ))); } } Value::Numeric(Numeric::Integer(AGG_FUNC_SUM_DISTINCT)) => { *cursor += 1; let idx = values.get(*cursor).ok_or_else(|| { LimboError::InternalError("Missing SUM(DISTINCT) column index".into()) })?; if let Value::Numeric(Numeric::Integer(idx)) = idx { *cursor += 1; AggregateFunction::SumDistinct(*idx as usize) } else { return Err(LimboError::InternalError(format!( "Expected Integer for SUM(DISTINCT) column index, got {idx:?}" ))); } } Value::Numeric(Numeric::Integer(AGG_FUNC_AVG)) => { *cursor += 1; let idx = values .get(*cursor) .ok_or_else(|| LimboError::InternalError("Missing AVG column index".into()))?; if let Value::Numeric(Numeric::Integer(idx)) = idx { *cursor += 1; AggregateFunction::Avg(*idx as usize) } else { return Err(LimboError::InternalError(format!( "Expected Integer for AVG column index, got {idx:?}" ))); } } Value::Numeric(Numeric::Integer(AGG_FUNC_AVG_DISTINCT)) => { *cursor += 1; let idx = values.get(*cursor).ok_or_else(|| { LimboError::InternalError("Missing AVG(DISTINCT) column index".into()) })?; if let Value::Numeric(Numeric::Integer(idx)) = idx { *cursor += 1; AggregateFunction::AvgDistinct(*idx as usize) } else { return Err(LimboError::InternalError(format!( "Expected Integer for AVG(DISTINCT) column index, got {idx:?}" ))); } } Value::Numeric(Numeric::Integer(AGG_FUNC_MIN)) => { *cursor += 1; let idx = values .get(*cursor) .ok_or_else(|| LimboError::InternalError("Missing MIN column index".into()))?; if let Value::Numeric(Numeric::Integer(idx)) = idx { *cursor += 1; AggregateFunction::Min(*idx as usize) } else { return Err(LimboError::InternalError(format!( "Expected Integer for MIN column index, got {idx:?}" ))); } } Value::Numeric(Numeric::Integer(AGG_FUNC_MAX)) => { *cursor += 1; let idx = values .get(*cursor) .ok_or_else(|| LimboError::InternalError("Missing MAX column index".into()))?; if let Value::Numeric(Numeric::Integer(idx)) = idx { *cursor += 1; AggregateFunction::Max(*idx as usize) } else { return Err(LimboError::InternalError(format!( "Expected Integer for MAX column index, got {idx:?}" ))); } } _ => { return Err(LimboError::InternalError(format!( "Unknown aggregate type code: {type_code:?}" ))) } }; Ok(agg_fn) } /// Create an AggregateFunction from a SQL function and its arguments /// Returns None if the function is not a supported aggregate pub fn from_sql_function( func: &crate::function::Func, input_column_idx: Option, ) -> Option { match func { Func::Agg(agg_func) => { match agg_func { AggFunc::Count | AggFunc::Count0 => Some(AggregateFunction::Count), AggFunc::Sum => input_column_idx.map(AggregateFunction::Sum), AggFunc::Avg => input_column_idx.map(AggregateFunction::Avg), AggFunc::Min => input_column_idx.map(AggregateFunction::Min), AggFunc::Max => input_column_idx.map(AggregateFunction::Max), _ => None, // Other aggregate functions not yet supported in DBSP } } _ => None, // Not an aggregate function } } } /// Information about a column that has MIN/MAX aggregations #[derive(Debug, Clone)] pub struct AggColumnInfo { /// Index used for storage key generation pub index: usize, /// Whether this column has a MIN aggregate pub has_min: bool, /// Whether this column has a MAX aggregate pub has_max: bool, } // group_key_str -> (group_key, state) type ComputedStates = HashMap, AggregateState)>; // group_key_str -> (column_index, value_as_hashable_row) -> accumulated_weight pub type MinMaxDeltas = HashMap>; /// Type for tracking distinct values within a batch /// Maps: group_key_str -> (column_idx, HashableRow) -> accumulated_weight /// HashableRow contains the value with column_idx as rowid for proper hashing type DistinctDeltas = HashMap>; /// Return type for merge_delta_with_existing function type MergeResult = (Delta, HashMap, AggregateState)>); /// Information about distinct value transitions for a single column #[derive(Debug, Clone)] pub struct DistinctTransition { pub transition_type: TransitionType, pub transitioned_value: Value, // The value that was added/removed } #[derive(Debug, Clone, PartialEq)] pub enum TransitionType { Added, // Value added to distinct set Removed, // Value removed from distinct set } #[derive(Debug)] enum AggregateCommitState { Idle, Eval { eval_state: EvalState, }, PersistDelta { delta: Delta, computed_states: ComputedStates, old_states: HashMap, // Track old counts for plain DISTINCT current_idx: usize, write_row: WriteRow, min_max_deltas: MinMaxDeltas, distinct_deltas: DistinctDeltas, input_delta: Delta, // Keep original input delta for distinct processing }, PersistMinMax { delta: Delta, min_max_persist_state: MinMaxPersistState, distinct_deltas: DistinctDeltas, }, PersistDistinctValues { delta: Delta, distinct_persist_state: DistinctPersistState, }, Done { delta: Delta, }, Invalid, } // Aggregate-specific eval states #[derive(Debug)] pub enum AggregateEvalState { FetchKey { delta: Delta, // Keep original delta for merge operation current_idx: usize, groups_to_read: Vec<(String, Vec)>, // Changed to Vec for index-based access existing_groups: HashMap, old_values: HashMap>, pre_existing_groups: HashSet, // Track groups that existed before this delta }, FetchAggregateState { delta: Delta, // Keep original delta for merge operation current_idx: usize, groups_to_read: Vec<(String, Vec)>, // Changed to Vec for index-based access existing_groups: HashMap, old_values: HashMap>, rowid: Option, // Rowid found by FetchKey (None if not found) read_record_state: Box, pre_existing_groups: HashSet, // Track groups that existed before this delta }, FetchDistinctValues { delta: Delta, // Keep original delta for merge operation current_idx: usize, groups_to_read: Vec<(String, Vec)>, // Changed to Vec for index-based access existing_groups: HashMap, old_values: HashMap>, fetch_distinct_state: Box, pre_existing_groups: HashSet, // Track groups that existed before this delta }, RecomputeMinMax { delta: Delta, existing_groups: HashMap, old_values: HashMap>, recompute_state: Box, pre_existing_groups: HashSet, // Track groups that existed before this delta }, Done { output: (Delta, ComputedStates), }, } /// Note that the AggregateOperator essentially implements a ZSet, even /// though the ZSet structure is never used explicitly. The on-disk btree /// plays the role of the set! #[derive(Debug)] pub struct AggregateOperator { // Unique operator ID for indexing in persistent storage pub operator_id: i64, // GROUP BY column indices group_by: Vec, // Aggregate functions to compute (including MIN/MAX) pub aggregates: Vec, // Column names from input pub input_column_names: Vec, // Map from column index to aggregate info for quick lookup pub column_min_max: HashMap, // Set of column indices that have distinct aggregates pub distinct_columns: HashSet, tracker: Option>>, // State machine for commit operation commit_state: AggregateCommitState, // SELECT DISTINCT x,y,z.... with no aggregations. is_distinct_only: bool, } /// State for a single group's aggregates #[derive(Debug, Clone, Default)] pub struct AggregateState { // For COUNT: just the count pub count: i64, // For SUM: column_index -> sum value pub sums: HashMap, // For AVG: column_index -> (sum, count) for computing average pub avgs: HashMap, // For MIN: column_index -> minimum value pub mins: HashMap, // For MAX: column_index -> maximum value pub maxs: HashMap, // For DISTINCT aggregates: column_index -> computed value // These are populated during eval when we scan the BTree (or in-memory map) pub distinct_counts: HashMap, pub distinct_sums: HashMap, // Weights of specific distinct values needed for current delta processing // (column_index, value) -> weight // Populated during FetchKey for values mentioned in the delta pub(crate) distinct_value_weights: HashMap<(usize, HashableRow), i64>, } impl AggregateEvalState { /// Process a delta through the aggregate state machine. /// /// Control flow is strictly linear for maintainability: /// 1. FetchKey → FetchAggregateState (always) /// 2. FetchAggregateState → FetchKey (always, loops until all groups processed) /// 3. FetchKey (when done) → FetchDistinctValues (always) /// 4. FetchDistinctValues → RecomputeMinMax (always) /// 5. RecomputeMinMax → Done (always) /// /// Some states may be no-ops depending on the operator configuration: /// - FetchAggregateState: For plain DISTINCT, skips reading aggregate blob (no aggregates to fetch) /// - FetchDistinctValues: No-op if no distinct columns exist (distinct_columns is empty) /// - RecomputeMinMax: No-op if no MIN/MAX aggregates exist (has_min_max() returns false) /// /// This deterministic flow ensures each state always transitions to the same next state, /// making the state machine easier to understand and debug. fn process_delta( &mut self, operator: &mut AggregateOperator, cursors: &mut DbspStateCursors, ) -> Result> { loop { match self { AggregateEvalState::FetchKey { delta, current_idx, groups_to_read, existing_groups, old_values, pre_existing_groups, } => { if *current_idx >= groups_to_read.len() { // All groups have been fetched, move to FetchDistinctValues // Create FetchDistinctState based on the delta and existing groups let fetch_distinct_state = FetchDistinctState::new( delta, &operator.distinct_columns, |values| operator.extract_group_key(values), AggregateOperator::group_key_to_string, existing_groups, operator.is_distinct_only, ); *self = AggregateEvalState::FetchDistinctValues { delta: std::mem::take(delta), current_idx: 0, groups_to_read: std::mem::take(groups_to_read), existing_groups: std::mem::take(existing_groups), old_values: std::mem::take(old_values), fetch_distinct_state: Box::new(fetch_distinct_state), pre_existing_groups: std::mem::take(pre_existing_groups), }; } else { // Get the current group to read let (group_key_str, _group_key) = &groups_to_read[*current_idx]; // For plain DISTINCT, we still need to transition to FetchAggregateState // to add the group to existing_groups, but we won't read any aggregate blob // Build the key for regular aggregate state: (operator_id, zset_hash, element_id=0) let operator_storage_id = generate_storage_id(operator.operator_id, 0, AGG_TYPE_REGULAR); let zset_hash = operator.generate_group_hash(group_key_str); let element_id = Hash128::new(0, 0); // Always zeros for aggregate state // Create index key values let index_key_values = vec![ Value::from_i64(operator_storage_id), zset_hash.to_value(), element_id.to_value(), ]; // Create an immutable record for the index key let index_record = ImmutableRecord::from_values(&index_key_values, index_key_values.len()); // Seek in the index to find if this row exists let seek_result = return_if_io!(cursors.index_cursor.seek( SeekKey::IndexKey(&index_record), SeekOp::GE { eq_only: true } )); let rowid = if matches!(seek_result, SeekResult::Found) { // Found in index, get the table rowid // The btree code handles extracting the rowid from the index record for has_rowid indexes return_if_io!(cursors.index_cursor.rowid()) } else { // Not found in index, no existing state None }; // Always transition to FetchAggregateState let taken_existing = std::mem::take(existing_groups); let taken_old_values = std::mem::take(old_values); let next_state = AggregateEvalState::FetchAggregateState { delta: std::mem::take(delta), current_idx: *current_idx, groups_to_read: std::mem::take(groups_to_read), existing_groups: taken_existing, old_values: taken_old_values, rowid, read_record_state: Box::new(ReadRecord::new()), pre_existing_groups: std::mem::take(pre_existing_groups), // Pass through existing }; *self = next_state; } } AggregateEvalState::FetchAggregateState { delta, current_idx, groups_to_read, existing_groups, old_values, rowid, read_record_state, pre_existing_groups, } => { // Get the current group to read let (group_key_str, group_key) = &groups_to_read[*current_idx]; // For plain DISTINCT, skip aggregate state fetch entirely // The distinct values are handled separately in FetchDistinctValues if operator.is_distinct_only { // Always insert the group key so FetchDistinctState will process it // The count will be set properly when we fetch distinct values existing_groups.insert(group_key_str.clone(), AggregateState::default()); } else if let Some(rowid) = rowid { let key = SeekKey::TableRowId(*rowid); // Regular aggregates - read the blob let state = return_if_io!( read_record_state.read_record(key, &mut cursors.table_cursor) ); // Process the fetched state if let Some(state) = state { let mut old_row = group_key.clone(); old_row.extend(state.to_values(&operator.aggregates)); old_values.insert(group_key_str.clone(), old_row); existing_groups.insert(group_key_str.clone(), state); // Track that this group exists in storage pre_existing_groups.insert(group_key_str.clone()); } } // If no rowid, there's no existing state for this group // Always move to next group via FetchKey let next_idx = *current_idx + 1; let taken_existing = std::mem::take(existing_groups); let taken_old_values = std::mem::take(old_values); let taken_pre_existing_groups = std::mem::take(pre_existing_groups); let next_state = AggregateEvalState::FetchKey { delta: std::mem::take(delta), current_idx: next_idx, groups_to_read: std::mem::take(groups_to_read), existing_groups: taken_existing, old_values: taken_old_values, pre_existing_groups: taken_pre_existing_groups, }; *self = next_state; } AggregateEvalState::FetchDistinctValues { delta, current_idx: _, groups_to_read: _, existing_groups, old_values, fetch_distinct_state, pre_existing_groups, } => { // Use FetchDistinctState to read distinct values from BTree storage return_if_io!(fetch_distinct_state.fetch_distinct_values( operator.operator_id, existing_groups, cursors, |group_key| operator.generate_group_hash(group_key), operator.is_distinct_only )); // For plain DISTINCT, mark groups as "from storage" if they have distinct values if operator.is_distinct_only { for (group_key_str, state) in existing_groups.iter() { // Check if this group has any distinct values with positive weight let has_values = state.distinct_value_weights.values().any(|&w| w > 0); if has_values { pre_existing_groups.insert(group_key_str.clone()); } } } // Extract MIN/MAX deltas for recomputation let min_max_deltas = operator.extract_min_max_deltas(delta); // Create RecomputeMinMax before moving existing_groups let recompute_state = Box::new(RecomputeMinMax::new( min_max_deltas, existing_groups, operator, )); // Transition to RecomputeMinMax let next_state = AggregateEvalState::RecomputeMinMax { delta: std::mem::take(delta), existing_groups: std::mem::take(existing_groups), old_values: std::mem::take(old_values), recompute_state, pre_existing_groups: std::mem::take(pre_existing_groups), }; *self = next_state; } AggregateEvalState::RecomputeMinMax { delta, existing_groups, old_values, recompute_state, pre_existing_groups, } => { if operator.has_min_max() { // Process MIN/MAX recomputation - this will update existing_groups with correct MIN/MAX return_if_io!(recompute_state.process(existing_groups, operator, cursors)); } // Now compute final output with updated MIN/MAX values let (output_delta, computed_states) = operator.merge_delta_with_existing( delta, existing_groups, old_values, pre_existing_groups, ); *self = AggregateEvalState::Done { output: (output_delta, computed_states), }; } AggregateEvalState::Done { output } => { let (delta, computed_states) = output.clone(); return Ok(IOResult::Done((delta, computed_states))); } } } } } impl AggregateState { pub fn new() -> Self { Self::default() } /// Convert the aggregate state to a vector of Values for unified serialization /// Format: [count, num_aggregates, (agg_metadata, agg_state)...] /// Each aggregate includes its type and column index for proper deserialization pub fn to_value_vector(&self, aggregates: &[AggregateFunction]) -> Vec { let mut values = Vec::new(); // Include count first values.push(Value::from_i64(self.count)); // Store number of aggregates values.push(Value::from_i64(aggregates.len() as i64)); // Add each aggregate's metadata and state for agg in aggregates { // First, add the aggregate function metadata (type and column index) values.extend(agg.to_values()); // Then add the state for this aggregate match agg { AggregateFunction::Count => { // Count state is already stored at the beginning } AggregateFunction::CountDistinct(col_idx) => { // Store the distinct count for this column let count = self.distinct_counts.get(col_idx).copied().unwrap_or(0); values.push(Value::from_i64(count)); } AggregateFunction::Sum(col_idx) => { let sum = self.sums.get(col_idx).copied().unwrap_or(0.0); values.push(Value::from_f64(sum)); } AggregateFunction::SumDistinct(col_idx) => { // Store both the distinct count and sum for this column let count = self.distinct_counts.get(col_idx).copied().unwrap_or(0); let sum = self.distinct_sums.get(col_idx).copied().unwrap_or(0.0); values.push(Value::from_i64(count)); values.push(Value::from_f64(sum)); } AggregateFunction::Avg(col_idx) => { let (sum, count) = self.avgs.get(col_idx).copied().unwrap_or((0.0, 0)); values.push(Value::from_f64(sum)); values.push(Value::from_i64(count)); } AggregateFunction::AvgDistinct(col_idx) => { // Store both the distinct count and sum for this column let count = self.distinct_counts.get(col_idx).copied().unwrap_or(0); let sum = self.distinct_sums.get(col_idx).copied().unwrap_or(0.0); values.push(Value::from_i64(count)); values.push(Value::from_f64(sum)); } AggregateFunction::Min(col_idx) => { if let Some(min_val) = self.mins.get(col_idx) { values.push(Value::from_i64(1)); // Has value values.push(min_val.clone()); } else { values.push(Value::from_i64(0)); // No value } } AggregateFunction::Max(col_idx) => { if let Some(max_val) = self.maxs.get(col_idx) { values.push(Value::from_i64(1)); // Has value values.push(max_val.clone()); } else { values.push(Value::from_i64(0)); // No value } } } } values } /// Reconstruct aggregate state from a vector of Values pub fn from_value_vector(values: &[Value]) -> Result { let mut cursor = 0; let mut state = Self::new(); // Read count let count = values .get(cursor) .ok_or_else(|| LimboError::InternalError("Aggregate state missing count".into()))?; if let Value::Numeric(Numeric::Integer(count)) = count { state.count = *count; cursor += 1; } else { return Err(LimboError::InternalError(format!( "Expected Integer for count, got {count:?}" ))); } // Read number of aggregates let num_aggregates = values .get(cursor) .ok_or_else(|| LimboError::InternalError("Missing number of aggregates".into()))?; let num_aggregates = match num_aggregates { Value::Numeric(Numeric::Integer(n)) => *n as usize, _ => { return Err(LimboError::InternalError(format!( "Expected Integer for aggregate count, got {num_aggregates:?}" ))) } }; cursor += 1; // Read each aggregate's state with type and column index for _ in 0..num_aggregates { // Deserialize the aggregate function metadata let agg_fn = AggregateFunction::from_values(values, &mut cursor)?; // Read the state for this aggregate match agg_fn { AggregateFunction::Count => { // Count state is already stored at the beginning } AggregateFunction::CountDistinct(col_idx) => { let count = values.get(cursor).ok_or_else(|| { LimboError::InternalError("Missing COUNT(DISTINCT) value".into()) })?; if let Value::Numeric(Numeric::Integer(count)) = count { state.distinct_counts.insert(col_idx, *count); cursor += 1; } else { return Err(LimboError::InternalError(format!( "Expected Integer for COUNT(DISTINCT) value, got {count:?}" ))); } } AggregateFunction::SumDistinct(col_idx) => { let count = values.get(cursor).ok_or_else(|| { LimboError::InternalError("Missing SUM(DISTINCT) count".into()) })?; if let Value::Numeric(Numeric::Integer(count)) = count { state.distinct_counts.insert(col_idx, *count); cursor += 1; } else { return Err(LimboError::InternalError(format!( "Expected Integer for SUM(DISTINCT) count, got {count:?}" ))); } let sum = values.get(cursor).ok_or_else(|| { LimboError::InternalError("Missing SUM(DISTINCT) sum".into()) })?; if let Value::Numeric(Numeric::Float(sum)) = sum { state.distinct_sums.insert(col_idx, f64::from(*sum)); cursor += 1; } else { return Err(LimboError::InternalError(format!( "Expected Float for SUM(DISTINCT) sum, got {sum:?}" ))); } } AggregateFunction::AvgDistinct(col_idx) => { let count = values.get(cursor).ok_or_else(|| { LimboError::InternalError("Missing AVG(DISTINCT) count".into()) })?; if let Value::Numeric(Numeric::Integer(count)) = count { state.distinct_counts.insert(col_idx, *count); cursor += 1; } else { return Err(LimboError::InternalError(format!( "Expected Integer for AVG(DISTINCT) count, got {count:?}" ))); } let sum = values.get(cursor).ok_or_else(|| { LimboError::InternalError("Missing AVG(DISTINCT) sum".into()) })?; if let Value::Numeric(Numeric::Float(sum)) = sum { state.distinct_sums.insert(col_idx, f64::from(*sum)); cursor += 1; } else { return Err(LimboError::InternalError(format!( "Expected Float for AVG(DISTINCT) sum, got {sum:?}" ))); } } AggregateFunction::Sum(col_idx) => { let sum = values .get(cursor) .ok_or_else(|| LimboError::InternalError("Missing SUM value".into()))?; if let Value::Numeric(Numeric::Float(sum)) = sum { state.sums.insert(col_idx, f64::from(*sum)); cursor += 1; } else { return Err(LimboError::InternalError(format!( "Expected Float for SUM value, got {sum:?}" ))); } } AggregateFunction::Avg(col_idx) => { let sum = values .get(cursor) .ok_or_else(|| LimboError::InternalError("Missing AVG sum value".into()))?; let sum = match sum { Value::Numeric(Numeric::Float(f)) => f64::from(*f), _ => { return Err(LimboError::InternalError(format!( "Expected Float for AVG sum, got {sum:?}" ))) } }; cursor += 1; let count = values.get(cursor).ok_or_else(|| { LimboError::InternalError("Missing AVG count value".into()) })?; let count = match count { Value::Numeric(Numeric::Integer(i)) => *i, _ => { return Err(LimboError::InternalError(format!( "Expected Integer for AVG count, got {count:?}" ))) } }; cursor += 1; state.avgs.insert(col_idx, (sum, count)); } AggregateFunction::Min(col_idx) => { let has_value = values.get(cursor).ok_or_else(|| { LimboError::InternalError("Missing MIN has_value flag".into()) })?; if let Value::Numeric(Numeric::Integer(has_value)) = has_value { cursor += 1; if *has_value == 1 { let min_val = values .get(cursor) .ok_or_else(|| { LimboError::InternalError("Missing MIN value".into()) })? .clone(); cursor += 1; state.mins.insert(col_idx, min_val); } } else { return Err(LimboError::InternalError(format!( "Expected Integer for MIN has_value flag, got {has_value:?}" ))); } } AggregateFunction::Max(col_idx) => { let has_value = values.get(cursor).ok_or_else(|| { LimboError::InternalError("Missing MAX has_value flag".into()) })?; if let Value::Numeric(Numeric::Integer(has_value)) = has_value { cursor += 1; if *has_value == 1 { let max_val = values .get(cursor) .ok_or_else(|| { LimboError::InternalError("Missing MAX value".into()) })? .clone(); cursor += 1; state.maxs.insert(col_idx, max_val); } } else { return Err(LimboError::InternalError(format!( "Expected Integer for MAX has_value flag, got {has_value:?}" ))); } } } } Ok(state) } fn to_blob(&self, aggregates: &[AggregateFunction], group_key: &[Value]) -> Vec { let mut all_values = Vec::new(); // Store the group key size first all_values.push(Value::from_i64(group_key.len() as i64)); all_values.extend_from_slice(group_key); all_values.extend(self.to_value_vector(aggregates)); let record = ImmutableRecord::from_values(&all_values, all_values.len()); record.as_blob().clone() } pub fn from_blob(blob: &[u8]) -> Result<(Self, Vec)> { let record = ImmutableRecord::from_bin_record(blob.to_vec()); let mut all_values: Vec = record.get_values_owned()?; if all_values.is_empty() { return Err(LimboError::InternalError( "Aggregate state blob is empty".into(), )); } // Read the group key size let group_key_count = match &all_values[0] { Value::Numeric(Numeric::Integer(n)) if *n >= 0 => *n as usize, Value::Numeric(Numeric::Integer(n)) => { return Err(LimboError::InternalError(format!( "Negative group key count: {n}" ))) } other => { return Err(LimboError::InternalError(format!( "Expected Integer for group key count, got {other:?}" ))) } }; // Remove the group key count from the values all_values.remove(0); if all_values.len() < group_key_count { return Err(LimboError::InternalError(format!( "Blob too short: expected at least {} values for group key, got {}", group_key_count, all_values.len() ))); } // Split into group key and state values let group_key = all_values[..group_key_count].to_vec(); let state_values = &all_values[group_key_count..]; // Reconstruct the aggregate state let state = Self::from_value_vector(state_values)?; Ok((state, group_key)) } /// Apply a delta to this aggregate state fn apply_delta( &mut self, values: &[Value], weight: isize, aggregates: &[AggregateFunction], _column_names: &[String], // No longer needed distinct_transitions: &HashMap, ) { // Update COUNT self.count += weight as i64; // Track which columns have had their distinct counts/sums updated // This prevents double-counting when multiple distinct aggregates // operate on the same column (e.g., COUNT(DISTINCT col), SUM(DISTINCT col), AVG(DISTINCT col)) let mut processed_counts: HashSet = HashSet::default(); let mut processed_sums: HashSet = HashSet::default(); // Update distinct aggregate state for agg in aggregates { match agg { AggregateFunction::Count => { // Already handled above } AggregateFunction::CountDistinct(col_idx) => { // Only update count if we haven't processed this column yet if !processed_counts.contains(col_idx) { if let Some(transition) = distinct_transitions.get(col_idx) { let current_count = self.distinct_counts.get(col_idx).copied().unwrap_or(0); let new_count = match transition.transition_type { TransitionType::Added => current_count + 1, TransitionType::Removed => current_count - 1, }; self.distinct_counts.insert(*col_idx, new_count); processed_counts.insert(*col_idx); } } } AggregateFunction::SumDistinct(col_idx) | AggregateFunction::AvgDistinct(col_idx) => { if let Some(transition) = distinct_transitions.get(col_idx) { // Update count if not already processed (needed for AVG) if !processed_counts.contains(col_idx) { let current_count = self.distinct_counts.get(col_idx).copied().unwrap_or(0); let new_count = match transition.transition_type { TransitionType::Added => current_count + 1, TransitionType::Removed => current_count - 1, }; self.distinct_counts.insert(*col_idx, new_count); processed_counts.insert(*col_idx); } // Update sum if not already processed if !processed_sums.contains(col_idx) { let current_sum = self.distinct_sums.get(col_idx).copied().unwrap_or(0.0); let value_as_float = match &transition.transitioned_value { Value::Numeric(Numeric::Integer(i)) => *i as f64, Value::Numeric(Numeric::Float(f)) => f64::from(*f), _ => 0.0, }; let new_sum = match transition.transition_type { TransitionType::Added => current_sum + value_as_float, TransitionType::Removed => current_sum - value_as_float, }; self.distinct_sums.insert(*col_idx, new_sum); processed_sums.insert(*col_idx); } } } AggregateFunction::Sum(col_idx) => { if let Some(val) = values.get(*col_idx) { let num_val = match val { Value::Numeric(Numeric::Integer(i)) => *i as f64, Value::Numeric(Numeric::Float(f)) => f64::from(*f), _ => 0.0, }; *self.sums.entry(*col_idx).or_insert(0.0) += num_val * weight as f64; } } AggregateFunction::Avg(col_idx) => { if let Some(val) = values.get(*col_idx) { let num_val = match val { Value::Numeric(Numeric::Integer(i)) => *i as f64, Value::Numeric(Numeric::Float(f)) => f64::from(*f), _ => 0.0, }; let (sum, count) = self.avgs.entry(*col_idx).or_insert((0.0, 0)); *sum += num_val * weight as f64; *count += weight as i64; } } AggregateFunction::Min(_col_name) | AggregateFunction::Max(_col_name) => { // MIN/MAX cannot be handled incrementally in apply_delta because: // // 1. For insertions: We can't just keep the minimum/maximum value. // We need to track ALL values to handle future deletions correctly. // // 2. For deletions (retractions): If we delete the current MIN/MAX, // we need to find the next best value, which requires knowing all // other values in the group. // // Example: Consider MIN(price) with values [10, 20, 30] // - Current MIN = 10 // - Delete 10 (weight = -1) // - New MIN should be 20, but we can't determine this without // having tracked all values [20, 30] // // Therefore, MIN/MAX processing is handled separately: // - All input values are persisted to the index via persist_min_max() // - When aggregates have MIN/MAX, we unconditionally transition to // the RecomputeMinMax state machine (see EvalState::RecomputeMinMax) // - RecomputeMinMax checks if the current MIN/MAX was deleted, and if so, // scans the index to find the new MIN/MAX from remaining values // // This ensures correctness for incremental computation at the cost of // additional I/O for MIN/MAX operations. } } } } /// Convert aggregate state to output values /// /// Note: SQLite returns INTEGER for SUM when all inputs are integers, and REAL when any input is REAL. /// However, in an incremental system like DBSP, we cannot track whether all current values are integers /// after deletions. For example: /// - Initial: SUM(10, 20, 30.5) = 60.5 (REAL) /// - After DELETE 30.5: SUM(10, 20) = 30 (SQLite returns INTEGER, but we only know the sum is 30.0) /// /// Therefore, we always return REAL for SUM operations. pub fn to_values(&self, aggregates: &[AggregateFunction]) -> Vec { let mut result = Vec::new(); for agg in aggregates { match agg { AggregateFunction::Count => { result.push(Value::from_i64(self.count)); } AggregateFunction::CountDistinct(col_idx) => { // Return the computed DISTINCT count let count = self.distinct_counts.get(col_idx).copied().unwrap_or(0); result.push(Value::from_i64(count)); } AggregateFunction::Sum(col_idx) => { let sum = self.sums.get(col_idx).copied().unwrap_or(0.0); result.push(Value::from_f64(sum)); } AggregateFunction::SumDistinct(col_idx) => { // Return the computed SUM(DISTINCT) let sum = self.distinct_sums.get(col_idx).copied().unwrap_or(0.0); result.push(Value::from_f64(sum)); } AggregateFunction::Avg(col_idx) => { if let Some((sum, count)) = self.avgs.get(col_idx) { if *count > 0 { result.push(Value::from_f64(sum / *count as f64)); } else { result.push(Value::Null); } } else { result.push(Value::Null); } } AggregateFunction::AvgDistinct(col_idx) => { // Compute AVG from SUM(DISTINCT) / COUNT(DISTINCT) let count = self.distinct_counts.get(col_idx).copied().unwrap_or(0); if count > 0 { let sum = self.distinct_sums.get(col_idx).copied().unwrap_or(0.0); let avg = sum / count as f64; // AVG always returns a float value for consistency with SQLite result.push(Value::from_f64(avg)); } else { result.push(Value::Null); } } AggregateFunction::Min(col_idx) => { // Return the MIN value from our state result.push(self.mins.get(col_idx).cloned().unwrap_or(Value::Null)); } AggregateFunction::Max(col_idx) => { // Return the MAX value from our state result.push(self.maxs.get(col_idx).cloned().unwrap_or(Value::Null)); } } } result } } impl AggregateOperator { /// Detect if a distinct value crosses the zero boundary (using pre-fetched weights and batch-accumulated weights) fn detect_distinct_value_transition( col_idx: usize, val: &Value, weight: isize, existing_state: &AggregateState, group_distinct_deltas: Option<&HashMap<(usize, HashableRow), isize>>, ) -> Option { let hashable_row = HashableRow::new(col_idx as i64, vec![val.clone()]); // Get the weight from storage (pre-fetched in AggregateState) let storage_count = existing_state .distinct_value_weights .get(&(col_idx, hashable_row.clone())) .copied() .unwrap_or(0); // Get the accumulated weight from the current batch (before this row) let batch_accumulated = if let Some(deltas) = group_distinct_deltas { deltas.get(&(col_idx, hashable_row)).copied().unwrap_or(0) } else { 0 }; // The old count is storage + batch accumulated so far (before this row) let old_count = storage_count + batch_accumulated as i64; // The new count includes the current weight let new_count = old_count + weight as i64; // Detect transitions if old_count <= 0 && new_count > 0 { // Value added to distinct set Some(DistinctTransition { transition_type: TransitionType::Added, transitioned_value: val.clone(), }) } else if old_count > 0 && new_count <= 0 { // Value removed from distinct set Some(DistinctTransition { transition_type: TransitionType::Removed, transitioned_value: val.clone(), }) } else { // No transition None } } /// Detect distinct value transitions for a single row fn detect_distinct_transitions( &self, row_values: &[Value], weight: isize, existing_state: &AggregateState, group_distinct_deltas: Option<&HashMap<(usize, HashableRow), isize>>, ) -> HashMap { let mut transitions = HashMap::default(); // Plain Distinct doesn't track individual values, so no transitions needed if self.is_distinct_only { // Distinct is handled by the count alone in apply_delta return transitions; } // Process each distinct column for &col_idx in &self.distinct_columns { let val = match row_values.get(col_idx) { Some(v) => v, None => continue, }; // Skip null values if val == &Value::Null { continue; } if let Some(transition) = Self::detect_distinct_value_transition( col_idx, val, weight, existing_state, group_distinct_deltas, ) { transitions.insert(col_idx, transition); } } transitions } pub fn new( operator_id: i64, group_by: Vec, aggregates: Vec, input_column_names: Vec, ) -> Result { // Precompute flags for runtime efficiency // Plain DISTINCT is indicated by empty aggregates vector let is_distinct_only = aggregates.is_empty(); // Build map of column indices to their MIN/MAX info let mut column_min_max = HashMap::default(); let mut storage_indices = HashMap::default(); let mut current_index = 0; // First pass: assign storage indices to unique MIN/MAX columns for agg in &aggregates { match agg { AggregateFunction::Min(col_idx) | AggregateFunction::Max(col_idx) => { storage_indices.entry(*col_idx).or_insert_with(|| { let idx = current_index; current_index += 1; idx }); } _ => {} } } // Second pass: build the column info map for MIN/MAX for agg in &aggregates { match agg { AggregateFunction::Min(col_idx) => { let storage_index = *storage_indices.get(col_idx).ok_or_else(|| { LimboError::InternalError( "storage index for MIN column should exist from first pass".to_string(), ) })?; let entry = column_min_max.entry(*col_idx).or_insert(AggColumnInfo { index: storage_index, has_min: false, has_max: false, }); entry.has_min = true; } AggregateFunction::Max(col_idx) => { let storage_index = *storage_indices.get(col_idx).ok_or_else(|| { LimboError::InternalError( "storage index for MAX column should exist from first pass".to_string(), ) })?; let entry = column_min_max.entry(*col_idx).or_insert(AggColumnInfo { index: storage_index, has_min: false, has_max: false, }); entry.has_max = true; } _ => {} } } // Build the distinct columns set let mut distinct_columns = HashSet::default(); for agg in &aggregates { match agg { AggregateFunction::CountDistinct(col_idx) | AggregateFunction::SumDistinct(col_idx) | AggregateFunction::AvgDistinct(col_idx) => { distinct_columns.insert(*col_idx); } _ => {} } } Ok(Self { operator_id, group_by, aggregates, input_column_names, column_min_max, distinct_columns, tracker: None, commit_state: AggregateCommitState::Idle, is_distinct_only, }) } pub fn has_min_max(&self) -> bool { !self.column_min_max.is_empty() } /// Check if this operator has any DISTINCT aggregates or plain DISTINCT pub fn has_distinct(&self) -> bool { !self.distinct_columns.is_empty() || self.is_distinct_only } fn eval_internal( &mut self, state: &mut EvalState, cursors: &mut DbspStateCursors, ) -> Result> { match state { EvalState::Uninitialized => { panic!("Cannot eval AggregateOperator with Uninitialized state"); } EvalState::Init { deltas } => { // Aggregate operators only use left_delta, right_delta must be empty assert!( deltas.right.is_empty(), "AggregateOperator expects right_delta to be empty" ); if deltas.left.changes.is_empty() { *state = EvalState::Done; return Ok(IOResult::Done((Delta::new(), HashMap::default()))); } let mut groups_to_read = BTreeMap::new(); for (row, _weight) in &deltas.left.changes { let group_key = self.extract_group_key(&row.values); let group_key_str = Self::group_key_to_string(&group_key); groups_to_read.insert(group_key_str, group_key); } let delta = std::mem::take(&mut deltas.left); *state = EvalState::Aggregate(Box::new(AggregateEvalState::FetchKey { delta, current_idx: 0, groups_to_read: groups_to_read.into_iter().collect(), existing_groups: HashMap::default(), old_values: HashMap::default(), pre_existing_groups: HashSet::default(), // Initialize empty })); } EvalState::Aggregate(_agg_state) => { // Already in progress, continue processing below. } EvalState::Done => { panic!("unreachable state! should have returned"); } EvalState::Join(_) => { panic!("Join state should not appear in aggregate operator"); } } // Process the delta through the aggregate state machine match state { EvalState::Aggregate(agg_state) => { let result = return_if_io!(agg_state.process_delta(self, cursors)); Ok(IOResult::Done(result)) } _ => panic!("Invalid state for aggregate processing"), } } fn merge_delta_with_existing( &mut self, delta: &Delta, existing_groups: &mut HashMap, old_values: &mut HashMap>, pre_existing_groups: &HashSet, ) -> MergeResult { let mut output_delta = Delta::new(); let mut temp_keys: HashMap> = HashMap::default(); // Track distinct value weights as we process the batch let mut batch_distinct_weights: HashMap> = HashMap::default(); // Process each change in the delta for (row, weight) in delta.changes.iter() { if let Some(tracker) = &self.tracker { tracker.lock().record_aggregation(); } // Extract group key let group_key = self.extract_group_key(&row.values); let group_key_str = Self::group_key_to_string(&group_key); // Get or create the state for this group let state = existing_groups.entry(group_key_str.clone()).or_default(); // Get batch weights for this group let group_batch_weights = batch_distinct_weights.get(&group_key_str); // Detect distinct transitions using the existing state and batch-accumulated weights let distinct_transitions = if self.has_distinct() { self.detect_distinct_transitions(&row.values, *weight, state, group_batch_weights) } else { HashMap::default() }; // Update batch weights after detecting transitions if self.has_distinct() { for &col_idx in &self.distinct_columns { if let Some(val) = row.values.get(col_idx) { if val != &Value::Null { let hashable_row = HashableRow::new(col_idx as i64, vec![val.clone()]); let group_entry = batch_distinct_weights .entry(group_key_str.clone()) .or_default(); let weight_entry = group_entry.entry((col_idx, hashable_row)).or_insert(0); *weight_entry += weight; } } } } temp_keys.insert(group_key_str.clone(), group_key.clone()); // Apply the delta to the state with pre-computed transitions state.apply_delta( &row.values, *weight, &self.aggregates, &self.input_column_names, &distinct_transitions, ); } // Generate output delta from temporary states and collect final states let mut final_states = HashMap::default(); for (group_key_str, state) in existing_groups.iter() { let group_key = if let Some(key) = temp_keys.get(group_key_str) { key.clone() } else if let Some(old_row) = old_values.get(group_key_str) { // Extract group key from old row (first N columns where N = group_by.len()) old_row[0..self.group_by.len()].to_vec() } else { vec![] }; // Generate synthetic rowid for this group let result_key = self.generate_group_rowid(group_key_str); // Always store the state for persistence (even if count=0, we need to delete it) final_states.insert(group_key_str.clone(), (group_key.clone(), state.clone())); // Check if we only have DISTINCT (no other aggregates) if self.is_distinct_only { // For plain DISTINCT, we output each distinct VALUE (not group) // state.count tells us how many distinct values have positive weight // Check if this group had any values before let old_existed = pre_existing_groups.contains(group_key_str); let new_exists = state.count > 0; if old_existed && !new_exists { // All distinct values removed: output deletion if let Some(old_row_values) = old_values.get(group_key_str) { let old_row = HashableRow::new(result_key, old_row_values.clone()); output_delta.changes.push((old_row, -1)); } else { // For plain DISTINCT, the old row is just the group key itself let old_row = HashableRow::new(result_key, group_key.clone()); output_delta.changes.push((old_row, -1)); } } else if !old_existed && new_exists { // First distinct value added: output insertion let output_values = group_key.clone(); // DISTINCT doesn't add aggregate values - just the group key let output_row = HashableRow::new(result_key, output_values.clone()); output_delta.changes.push((output_row, 1)); } // No output if staying positive or staying at zero } else { // Normal aggregates: output deletions and insertions as before if let Some(old_row_values) = old_values.get(group_key_str) { let old_row = HashableRow::new(result_key, old_row_values.clone()); output_delta.changes.push((old_row, -1)); } // Only include groups with count > 0 in the output delta if state.count > 0 { // Build output row: group_by columns + aggregate values let mut output_values = group_key.clone(); let aggregate_values = state.to_values(&self.aggregates); output_values.extend(aggregate_values); let output_row = HashableRow::new(result_key, output_values.clone()); output_delta.changes.push((output_row, 1)); } } } (output_delta, final_states) } /// Extract distinct values from delta changes for batch tracking fn extract_distinct_deltas(&self, delta: &Delta) -> DistinctDeltas { let mut distinct_deltas: DistinctDeltas = HashMap::default(); for (row, weight) in &delta.changes { let group_key = self.extract_group_key(&row.values); let group_key_str = Self::group_key_to_string(&group_key); // Get or create entry for this group let group_entry = distinct_deltas.entry(group_key_str.clone()).or_default(); if self.is_distinct_only { // For plain DISTINCT, the group itself is what we're tracking // We store a single entry that represents "this group exists N times" // Use column index 0 with the group_key_str as the value // For group key, use 0 as column index let key = ( 0, HashableRow::new(0, vec![Value::Text(group_key_str.clone().into())]), ); let value_entry = group_entry.entry(key).or_insert(0); *value_entry += weight; } else { // For DISTINCT aggregates, track individual column values for &col_idx in &self.distinct_columns { if let Some(val) = row.values.get(col_idx) { // Skip NULL values if val == &Value::Null { continue; } let key = (col_idx, HashableRow::new(col_idx as i64, vec![val.clone()])); let value_entry = group_entry.entry(key).or_insert(0); *value_entry += weight; } } } } distinct_deltas } /// Extract MIN/MAX values from delta changes for persistence to index fn extract_min_max_deltas(&self, delta: &Delta) -> MinMaxDeltas { let mut min_max_deltas: MinMaxDeltas = HashMap::default(); for (row, weight) in &delta.changes { let group_key = self.extract_group_key(&row.values); let group_key_str = Self::group_key_to_string(&group_key); for agg in &self.aggregates { match agg { AggregateFunction::Min(col_idx) | AggregateFunction::Max(col_idx) => { if let Some(val) = row.values.get(*col_idx) { // Skip NULL values - they don't participate in MIN/MAX if val == &Value::Null { continue; } // Create a HashableRow with just this value // Use 0 as rowid since we only care about the value for comparison let hashable_value = HashableRow::new(0, vec![val.clone()]); let key = (*col_idx, hashable_value); let group_entry = min_max_deltas.entry(group_key_str.clone()).or_default(); let value_entry = group_entry.entry(key).or_insert(0); // Accumulate the weight *value_entry += weight; } } _ => {} // Ignore non-MIN/MAX aggregates } } } min_max_deltas } pub fn set_tracker(&mut self, tracker: Arc>) { self.tracker = Some(tracker); } /// Generate a hash for a group /// For no GROUP BY: returns a zero hash /// For GROUP BY: returns a 128-bit hash of the group key string pub fn generate_group_hash(&self, group_key_str: &str) -> Hash128 { if self.group_by.is_empty() { Hash128::new(0, 0) } else { Hash128::hash_str(group_key_str) } } /// Generate a rowid for a group (for output rows) /// This is NOT the hash used for storage (that's generate_group_hash which returns full 128-bit). /// This is a synthetic rowid used in place of SQLite's rowid for aggregate output rows. /// We truncate the 128-bit hash to 64 bits for SQLite rowid compatibility. pub fn generate_group_rowid(&self, group_key_str: &str) -> i64 { let hash = self.generate_group_hash(group_key_str); hash.as_i64() } /// Extract group key values from a row pub fn extract_group_key(&self, values: &[Value]) -> Vec { let mut key = Vec::new(); for &idx in &self.group_by { if let Some(val) = values.get(idx) { key.push(val.clone()); } else { key.push(Value::Null); } } key } /// Convert group key to string for indexing (since Value doesn't implement Hash) pub fn group_key_to_string(key: &[Value]) -> String { key.iter() .map(|v| format!("{v:?}")) .collect::>() .join(",") } } impl IncrementalOperator for AggregateOperator { fn eval( &mut self, state: &mut EvalState, cursors: &mut DbspStateCursors, ) -> Result> { let (delta, _) = return_if_io!(self.eval_internal(state, cursors)); Ok(IOResult::Done(delta)) } fn commit( &mut self, mut deltas: DeltaPair, cursors: &mut DbspStateCursors, ) -> Result> { // Aggregate operator only uses left delta, right must be empty assert!( deltas.right.is_empty(), "AggregateOperator expects right delta to be empty in commit" ); let delta = std::mem::take(&mut deltas.left); loop { // Note: because we std::mem::replace here (without it, the borrow checker goes nuts, // because we call self.eval_interval, which requires a mutable borrow), we have to // restore the state if we return I/O. So we can't use return_if_io! let mut state = std::mem::replace(&mut self.commit_state, AggregateCommitState::Invalid); match &mut state { AggregateCommitState::Invalid => { panic!("Reached invalid state! State was replaced, and not replaced back"); } AggregateCommitState::Idle => { let eval_state = EvalState::from_delta(delta.clone()); self.commit_state = AggregateCommitState::Eval { eval_state }; } AggregateCommitState::Eval { ref mut eval_state } => { // Clone the delta for MIN/MAX processing before eval consumes it // We need to get the delta from the eval_state if it's still in Init let input_delta = match eval_state { EvalState::Init { deltas } => deltas.left.clone(), _ => Delta::new(), // Empty delta if already processed }; // Extract MIN/MAX and DISTINCT deltas before any I/O operations let min_max_deltas = self.extract_min_max_deltas(&input_delta); // For plain DISTINCT, we need to extract deltas too let distinct_deltas = if self.has_distinct() || self.is_distinct_only { self.extract_distinct_deltas(&input_delta) } else { HashMap::default() }; // Get old counts before eval modifies the states // We need to extract this from the eval_state before it's consumed let old_states = HashMap::default(); // TODO: Extract from eval_state let (output_delta, computed_states) = return_and_restore_if_io!( &mut self.commit_state, state, self.eval_internal(eval_state, cursors) ); self.commit_state = AggregateCommitState::PersistDelta { delta: output_delta, computed_states, old_states, current_idx: 0, write_row: WriteRow::new(), min_max_deltas, // Store for later use distinct_deltas, // Store for distinct processing input_delta, // Store original input }; } AggregateCommitState::PersistDelta { delta, computed_states, old_states, current_idx, write_row, min_max_deltas, distinct_deltas, input_delta, } => { let states_vec: Vec<_> = computed_states.iter().collect(); if *current_idx >= states_vec.len() { // Use the min_max_deltas we extracted earlier from the input delta self.commit_state = AggregateCommitState::PersistMinMax { delta: delta.clone(), min_max_persist_state: MinMaxPersistState::new(min_max_deltas.clone()), distinct_deltas: distinct_deltas.clone(), }; } else { let (group_key_str, (group_key, agg_state)) = states_vec[*current_idx]; // Skip aggregate state persistence for plain DISTINCT // Plain DISTINCT only uses the distinct value weights, not aggregate state if self.is_distinct_only { // Skip to next - distinct values are handled in PersistDistinctValues // We still need to transition states properly let next_idx = *current_idx + 1; if next_idx >= states_vec.len() { // Done with all groups, move to PersistMinMax self.commit_state = AggregateCommitState::PersistMinMax { delta: std::mem::take(delta), min_max_persist_state: MinMaxPersistState::new(std::mem::take( min_max_deltas, )), distinct_deltas: std::mem::take(distinct_deltas), }; } else { // Move to next group self.commit_state = AggregateCommitState::PersistDelta { delta: std::mem::take(delta), computed_states: std::mem::take(computed_states), old_states: std::mem::take(old_states), current_idx: next_idx, write_row: WriteRow::new(), min_max_deltas: std::mem::take(min_max_deltas), distinct_deltas: std::mem::take(distinct_deltas), input_delta: std::mem::take(input_delta), }; } continue; } // Build the key components for regular aggregates let operator_storage_id = generate_storage_id(self.operator_id, 0, AGG_TYPE_REGULAR); let zset_hash = self.generate_group_hash(group_key_str); let element_id = Hash128::new(0, 0); // Always zeros for regular aggregates // Determine weight: 1 if exists, -1 if deleted let weight = if agg_state.count == 0 { -1 } else { 1 }; // Serialize the aggregate state (only for regular aggregates, not plain DISTINCT) let state_blob = agg_state.to_blob(&self.aggregates, group_key); let blob_value = Value::Blob(state_blob); // Build the aggregate storage format: [operator_id, zset_hash, element_id, value, weight] let operator_id_val = Value::from_i64(operator_storage_id); let zset_hash_val = zset_hash.to_value(); let element_id_val = element_id.to_value(); let blob_val = blob_value.clone(); // Create index key - the first 3 columns of our primary key let index_key = vec![ operator_id_val.clone(), zset_hash_val.clone(), element_id_val.clone(), ]; // Record values (without weight) let record_values = vec![operator_id_val, zset_hash_val, element_id_val, blob_val]; return_and_restore_if_io!( &mut self.commit_state, state, write_row.write_row(cursors, index_key, record_values, weight) ); let delta = std::mem::take(delta); let computed_states = std::mem::take(computed_states); let min_max_deltas = std::mem::take(min_max_deltas); let distinct_deltas = std::mem::take(distinct_deltas); let input_delta = std::mem::take(input_delta); self.commit_state = AggregateCommitState::PersistDelta { delta, computed_states, old_states: std::mem::take(old_states), current_idx: *current_idx + 1, write_row: WriteRow::new(), // Reset for next write min_max_deltas, distinct_deltas, input_delta, }; } } AggregateCommitState::PersistMinMax { delta, min_max_persist_state, distinct_deltas, } => { if self.has_min_max() { return_and_restore_if_io!( &mut self.commit_state, state, min_max_persist_state.persist_min_max( self.operator_id, &self.column_min_max, cursors, |group_key_str| self.generate_group_hash(group_key_str) ) ); } // Transition to PersistDistinctValues let delta = std::mem::take(delta); let distinct_deltas = std::mem::take(distinct_deltas); let distinct_persist_state = DistinctPersistState::new(distinct_deltas); self.commit_state = AggregateCommitState::PersistDistinctValues { delta, distinct_persist_state, }; } AggregateCommitState::PersistDistinctValues { delta, distinct_persist_state, } => { if self.has_distinct() { // Use the state machine to persist distinct values to BTree return_and_restore_if_io!( &mut self.commit_state, state, distinct_persist_state.persist_distinct_values( self.operator_id, cursors, |group_key_str| self.generate_group_hash(group_key_str) ) ); } // Transition to Done let delta = std::mem::take(delta); self.commit_state = AggregateCommitState::Done { delta }; } AggregateCommitState::Done { delta } => { self.commit_state = AggregateCommitState::Idle; let delta = std::mem::take(delta); return Ok(IOResult::Done(delta)); } } } } fn set_tracker(&mut self, tracker: Arc>) { self.tracker = Some(tracker); } } /// State machine for recomputing MIN/MAX values after deletion #[derive(Debug)] pub enum RecomputeMinMax { ProcessElements { /// Current column being processed current_column_idx: usize, /// Columns to process (combined MIN and MAX) columns_to_process: Vec<(String, usize, bool)>, // (group_key, column_name, is_min) /// MIN/MAX deltas for checking values and weights min_max_deltas: MinMaxDeltas, }, Scan { /// Columns still to process columns_to_process: Vec<(String, usize, bool)>, /// Current index in columns_to_process (will resume from here) current_column_idx: usize, /// MIN/MAX deltas for checking values and weights min_max_deltas: MinMaxDeltas, /// Current group key being processed group_key: String, /// Current column name being processed column_name: usize, /// Whether we're looking for MIN (true) or MAX (false) is_min: bool, /// The scan state machine for finding the new MIN/MAX scan_state: Box, }, Done, } impl RecomputeMinMax { pub fn new( min_max_deltas: MinMaxDeltas, existing_groups: &HashMap, operator: &AggregateOperator, ) -> Self { let mut groups_to_check: HashSet<(String, usize, bool)> = HashSet::default(); // Remember the min_max_deltas are essentially just the only column that is affected by // this min/max, in delta (actually ZSet - consolidated delta) format. This makes it easier // for us to consume it in here. // // The most challenging case is the case where there is a retraction, since we need to go // back to the index. for (group_key_str, values) in &min_max_deltas { for ((col_name, hashable_row), weight) in values { let col_info = operator.column_min_max.get(col_name); let value = &hashable_row.values[0]; if *weight < 0 { // Deletion detected - check if it's the current MIN/MAX if let Some(state) = existing_groups.get(group_key_str) { // Check for MIN if let Some(current_min) = state.mins.get(col_name) { if current_min == value { groups_to_check.insert((group_key_str.clone(), *col_name, true)); } } // Check for MAX if let Some(current_max) = state.maxs.get(col_name) { if current_max == value { groups_to_check.insert((group_key_str.clone(), *col_name, false)); } } } } else if *weight > 0 { // If it is not found in the existing groups, then we only need to care // about this if this is a new record being inserted if let Some(info) = col_info { if info.has_min { groups_to_check.insert((group_key_str.clone(), *col_name, true)); } if info.has_max { groups_to_check.insert((group_key_str.clone(), *col_name, false)); } } } } } if groups_to_check.is_empty() { // No recomputation or initialization needed Self::Done } else { // Convert HashSet to Vec for indexed processing let groups_to_check_vec: Vec<_> = groups_to_check.into_iter().collect(); Self::ProcessElements { current_column_idx: 0, columns_to_process: groups_to_check_vec, min_max_deltas, } } } pub fn process( &mut self, existing_groups: &mut HashMap, operator: &AggregateOperator, cursors: &mut DbspStateCursors, ) -> Result> { loop { match self { RecomputeMinMax::ProcessElements { current_column_idx, columns_to_process, min_max_deltas, } => { if *current_column_idx >= columns_to_process.len() { *self = RecomputeMinMax::Done; return Ok(IOResult::Done(())); } let (group_key, column_name, is_min) = columns_to_process[*current_column_idx].clone(); // Column name is already the index // Get the storage index from column_min_max map let column_info = operator .column_min_max .get(&column_name) .expect("Column should exist in column_min_max map"); let storage_index = column_info.index; // Get current value from existing state let current_value = existing_groups.get(&group_key).and_then(|state| { if is_min { state.mins.get(&column_name).cloned() } else { state.maxs.get(&column_name).cloned() } }); // Create storage keys for index lookup let storage_id = generate_storage_id(operator.operator_id, storage_index, AGG_TYPE_MINMAX); let zset_hash = operator.generate_group_hash(&group_key); // Get the values for this group from min_max_deltas let group_values = min_max_deltas.get(&group_key).cloned().unwrap_or_default(); let columns_to_process = std::mem::take(columns_to_process); let min_max_deltas = std::mem::take(min_max_deltas); let scan_state = if is_min { Box::new(ScanState::new_for_min( current_value, group_key.clone(), column_name, storage_id, zset_hash, group_values, )) } else { Box::new(ScanState::new_for_max( current_value, group_key.clone(), column_name, storage_id, zset_hash, group_values, )) }; *self = RecomputeMinMax::Scan { columns_to_process, current_column_idx: *current_column_idx, min_max_deltas, group_key, column_name, is_min, scan_state, }; } RecomputeMinMax::Scan { columns_to_process, current_column_idx, min_max_deltas, group_key, column_name, is_min, scan_state, } => { // Find new value using the scan state machine let new_value = return_if_io!(scan_state.find_new_value(cursors)); // Update the state with new value (create if doesn't exist) let state = existing_groups.entry(group_key.clone()).or_default(); if *is_min { if let Some(min_val) = new_value { state.mins.insert(*column_name, min_val); } else { state.mins.remove(column_name); } } else if let Some(max_val) = new_value { state.maxs.insert(*column_name, max_val); } else { state.maxs.remove(column_name); } // Move to next column let min_max_deltas = std::mem::take(min_max_deltas); let columns_to_process = std::mem::take(columns_to_process); *self = RecomputeMinMax::ProcessElements { current_column_idx: *current_column_idx + 1, columns_to_process, min_max_deltas, }; } RecomputeMinMax::Done => { return Ok(IOResult::Done(())); } } } } } /// State machine for scanning through the index to find new MIN/MAX values #[derive(Debug)] pub enum ScanState { CheckCandidate { /// Current candidate value for MIN/MAX candidate: Option, /// Group key being processed group_key: String, /// Column name being processed column_name: usize, /// Storage ID for the index seek storage_id: i64, /// ZSet ID for the group zset_hash: Hash128, /// Group values from MinMaxDeltas: (column_name, HashableRow) -> weight group_values: HashMap<(usize, HashableRow), isize>, /// Whether we're looking for MIN (true) or MAX (false) is_min: bool, }, FetchNextCandidate { /// Current candidate to seek past current_candidate: Value, /// Group key being processed group_key: String, /// Column name being processed column_name: usize, /// Storage ID for the index seek storage_id: i64, /// ZSet ID for the group zset_hash: Hash128, /// Group values from MinMaxDeltas: (column_name, HashableRow) -> weight group_values: HashMap<(usize, HashableRow), isize>, /// Whether we're looking for MIN (true) or MAX (false) is_min: bool, }, Done { /// The final MIN/MAX value found result: Option, }, } impl ScanState { pub fn new_for_min( current_min: Option, group_key: String, column_name: usize, storage_id: i64, zset_hash: Hash128, group_values: HashMap<(usize, HashableRow), isize>, ) -> Self { Self::CheckCandidate { candidate: current_min, group_key, column_name, storage_id, zset_hash, group_values, is_min: true, } } // Extract a new candidate from the index. It is possible that, when searching, // we end up going into a different operator altogether. That means we have // exhausted this operator (or group) entirely, and no good candidate was found fn extract_new_candidate( cursors: &mut DbspStateCursors, index_record: &ImmutableRecord, seek_op: SeekOp, storage_id: i64, zset_hash: Hash128, ) -> Result>> { let seek_result = return_if_io!(cursors .index_cursor .seek(SeekKey::IndexKey(index_record), seek_op)); if !matches!(seek_result, SeekResult::Found) { return Ok(IOResult::Done(None)); } let record = return_if_io!(cursors.index_cursor.record()).ok_or_else(|| { LimboError::InternalError( "Record found on the cursor, but could not be read".to_string(), ) })?; let mut values = record.iter()?; let Some(rec_storage_id) = values.next() else { return Ok(IOResult::Done(None)); }; let Some(rec_zset_hash) = values.next() else { return Ok(IOResult::Done(None)); }; // Check if we're still in the same group if let ValueRef::Numeric(Numeric::Integer(rec_sid)) = rec_storage_id? { if rec_sid != storage_id { return Ok(IOResult::Done(None)); } } else { return Ok(IOResult::Done(None)); } // Compare zset_hash as blob if let ValueRef::Blob(rec_zset_blob) = rec_zset_hash? { if let Some(rec_hash) = Hash128::from_blob(rec_zset_blob) { if rec_hash != zset_hash { return Ok(IOResult::Done(None)); } } else { return Ok(IOResult::Done(None)); } } else { return Ok(IOResult::Done(None)); } let third = values.next(); let Some(third) = third else { return Ok(IOResult::Done(None)); }; // Get the value (3rd element) Ok(IOResult::Done(Some(third?.to_owned()))) } pub fn new_for_max( current_max: Option, group_key: String, column_name: usize, storage_id: i64, zset_hash: Hash128, group_values: HashMap<(usize, HashableRow), isize>, ) -> Self { Self::CheckCandidate { candidate: current_max, group_key, column_name, storage_id, zset_hash, group_values, is_min: false, } } pub fn find_new_value( &mut self, cursors: &mut DbspStateCursors, ) -> Result>> { loop { match self { ScanState::CheckCandidate { candidate, group_key, column_name, storage_id, zset_hash, group_values, is_min, } => { // First, check if we have a candidate if let Some(cand_val) = candidate { // Check if the candidate is retracted (weight <= 0) // Create a HashableRow to look up the weight let hashable_cand = HashableRow::new(0, vec![cand_val.clone()]); let key = (*column_name, hashable_cand); let is_retracted = group_values.get(&key).is_some_and(|weight| *weight <= 0); if is_retracted { // Candidate is retracted, need to fetch next from index *self = ScanState::FetchNextCandidate { current_candidate: cand_val.clone(), group_key: std::mem::take(group_key), column_name: std::mem::take(column_name), storage_id: *storage_id, zset_hash: *zset_hash, group_values: std::mem::take(group_values), is_min: *is_min, }; continue; } } // Candidate is valid or we have no candidate // Now find the best value from insertions in group_values let mut best_from_zset = None; for ((col, hashable_val), weight) in group_values.iter() { if col == column_name && *weight > 0 { let value = &hashable_val.values[0]; // Skip NULL values - they don't participate in MIN/MAX if value == &Value::Null { continue; } // This is an insertion for our column if let Some(ref current_best) = best_from_zset { if *is_min { if value.cmp(current_best) == std::cmp::Ordering::Less { best_from_zset = Some(value.clone()); } } else if value.cmp(current_best) == std::cmp::Ordering::Greater { best_from_zset = Some(value.clone()); } } else { best_from_zset = Some(value.clone()); } } } // Compare candidate with best from ZSet, filtering out NULLs let result = match (&candidate, &best_from_zset) { (Some(cand), Some(zset_val)) if cand != &Value::Null => { if *is_min { if zset_val.cmp(cand) == std::cmp::Ordering::Less { Some(zset_val.clone()) } else { Some(cand.clone()) } } else if zset_val.cmp(cand) == std::cmp::Ordering::Greater { Some(zset_val.clone()) } else { Some(cand.clone()) } } (Some(cand), None) if cand != &Value::Null => Some(cand.clone()), (None, Some(zset_val)) => Some(zset_val.clone()), (Some(cand), Some(_)) if cand == &Value::Null => best_from_zset, _ => None, }; *self = ScanState::Done { result }; } ScanState::FetchNextCandidate { current_candidate, group_key, column_name, storage_id, zset_hash, group_values, is_min, } => { // Seek to the next value in the index let index_key = vec![ Value::from_i64(*storage_id), zset_hash.to_value(), current_candidate.clone(), ]; let index_record = ImmutableRecord::from_values(&index_key, index_key.len()); let seek_op = if *is_min { SeekOp::GT // For MIN, seek greater than current } else { SeekOp::LT // For MAX, seek less than current }; let new_candidate = return_if_io!(Self::extract_new_candidate( cursors, &index_record, seek_op, *storage_id, *zset_hash )); *self = ScanState::CheckCandidate { candidate: new_candidate, group_key: std::mem::take(group_key), column_name: std::mem::take(column_name), storage_id: *storage_id, zset_hash: *zset_hash, group_values: std::mem::take(group_values), is_min: *is_min, }; } ScanState::Done { result } => { return Ok(IOResult::Done(result.clone())); } } } } } /// State machine for persisting Min/Max values to storage #[derive(Debug)] pub enum MinMaxPersistState { Init { min_max_deltas: MinMaxDeltas, group_keys: Vec, }, ProcessGroup { min_max_deltas: MinMaxDeltas, group_keys: Vec, group_idx: usize, value_idx: usize, }, WriteValue { min_max_deltas: MinMaxDeltas, group_keys: Vec, group_idx: usize, value_idx: usize, value: Value, column_name: usize, weight: isize, write_row: WriteRow, }, Done, } /// State machine for fetching distinct values from BTree storage #[derive(Debug)] pub enum FetchDistinctState { Init { groups_to_fetch: Vec<(String, HashMap>)>, }, FetchGroup { groups_to_fetch: Vec<(String, HashMap>)>, group_idx: usize, value_idx: usize, values_to_fetch: Vec<(usize, Value)>, }, ReadValue { groups_to_fetch: Vec<(String, HashMap>)>, group_idx: usize, value_idx: usize, values_to_fetch: Vec<(usize, Value)>, group_key: String, column_idx: usize, value: Value, }, Done, } impl FetchDistinctState { /// Add fetch entry for plain DISTINCT - the group itself is the distinct value fn add_plain_distinct_fetch( group_entry: &mut HashMap>, group_key_str: &str, ) { let group_value = Value::Text(group_key_str.to_string().into()); group_entry .entry(0) .or_default() .insert(HashableRow::new(0, vec![group_value])); } /// Add fetch entries for DISTINCT aggregates - individual column values fn add_aggregate_distinct_fetch( group_entry: &mut HashMap>, row_values: &[Value], distinct_columns: &HashSet, ) { for &col_idx in distinct_columns { if let Some(val) = row_values.get(col_idx) { if val != &Value::Null { group_entry .entry(col_idx) .or_default() .insert(HashableRow::new(col_idx as i64, vec![val.clone()])); } } } } pub fn new( delta: &Delta, distinct_columns: &HashSet, extract_group_key: impl Fn(&[Value]) -> Vec, group_key_to_string: impl Fn(&[Value]) -> String, existing_groups: &HashMap, is_plain_distinct: bool, ) -> Self { let mut groups_to_fetch: HashMap>> = HashMap::default(); for (row, _weight) in &delta.changes { let group_key = extract_group_key(&row.values); let group_key_str = group_key_to_string(&group_key); // Skip groups we don't need to fetch // For DISTINCT aggregates, only fetch for existing groups if !is_plain_distinct && !existing_groups.contains_key(&group_key_str) { continue; } let group_entry = groups_to_fetch.entry(group_key_str.clone()).or_default(); if is_plain_distinct { Self::add_plain_distinct_fetch(group_entry, &group_key_str); } else { Self::add_aggregate_distinct_fetch(group_entry, &row.values, distinct_columns); } } let groups_to_fetch: Vec<_> = groups_to_fetch.into_iter().collect(); if groups_to_fetch.is_empty() { Self::Done } else { Self::Init { groups_to_fetch } } } pub fn fetch_distinct_values( &mut self, operator_id: i64, existing_groups: &mut HashMap, cursors: &mut DbspStateCursors, generate_group_hash: impl Fn(&str) -> Hash128, is_plain_distinct: bool, ) -> Result> { loop { match self { FetchDistinctState::Init { groups_to_fetch } => { if groups_to_fetch.is_empty() { *self = FetchDistinctState::Done; continue; } let groups = std::mem::take(groups_to_fetch); *self = FetchDistinctState::FetchGroup { groups_to_fetch: groups, group_idx: 0, value_idx: 0, values_to_fetch: Vec::new(), }; } FetchDistinctState::FetchGroup { groups_to_fetch, group_idx, value_idx, values_to_fetch, } => { if *group_idx >= groups_to_fetch.len() { *self = FetchDistinctState::Done; continue; } // Build list of values to fetch for current group if not done if values_to_fetch.is_empty() && *group_idx < groups_to_fetch.len() { let (_group_key, cols_values) = &groups_to_fetch[*group_idx]; for (col_idx, values) in cols_values { for hashable_row in values { // Extract the value from HashableRow let value = hashable_row.values.first().ok_or_else(|| { LimboError::InternalError( "hashable_row should have at least one value".to_string(), ) })?; values_to_fetch.push((*col_idx, value.clone())); } } } if *value_idx >= values_to_fetch.len() { // Move to next group *group_idx += 1; *value_idx = 0; values_to_fetch.clear(); continue; } // Fetch current value let (group_key, _) = groups_to_fetch[*group_idx].clone(); let (column_idx, value) = values_to_fetch[*value_idx].clone(); let groups = std::mem::take(groups_to_fetch); let values = std::mem::take(values_to_fetch); *self = FetchDistinctState::ReadValue { groups_to_fetch: groups, group_idx: *group_idx, value_idx: *value_idx, values_to_fetch: values, group_key, column_idx, value, }; } FetchDistinctState::ReadValue { groups_to_fetch, group_idx, value_idx, values_to_fetch, group_key, column_idx, value, } => { // Read the record from BTree using the same pattern as WriteRow: // 1. Seek in index to find the entry // 2. Get rowid from index cursor // 3. Use rowid to read from table cursor let storage_id = generate_storage_id(operator_id, *column_idx, AGG_TYPE_DISTINCT); let zset_hash = generate_group_hash(group_key); let element_id = hash_value(value, *column_idx); // First, seek in the index cursor let index_key = vec![ Value::from_i64(storage_id), zset_hash.to_value(), element_id.to_value(), ]; let index_record = ImmutableRecord::from_values(&index_key, index_key.len()); let seek_result = return_if_io!(cursors.index_cursor.seek( SeekKey::IndexKey(&index_record), SeekOp::GE { eq_only: true } )); // Early exit if not found in index if !matches!(seek_result, SeekResult::Found) { let groups = std::mem::take(groups_to_fetch); let values = std::mem::take(values_to_fetch); *self = FetchDistinctState::FetchGroup { groups_to_fetch: groups, group_idx: *group_idx, value_idx: *value_idx + 1, values_to_fetch: values, }; continue; } // Get the rowid from the index cursor let rowid = return_if_io!(cursors.index_cursor.rowid()); // Early exit if no rowid let rowid = match rowid { Some(id) => id, None => { let groups = std::mem::take(groups_to_fetch); let values = std::mem::take(values_to_fetch); *self = FetchDistinctState::FetchGroup { groups_to_fetch: groups, group_idx: *group_idx, value_idx: *value_idx + 1, values_to_fetch: values, }; continue; } }; // Now seek in the table cursor using the rowid let table_result = return_if_io!(cursors .table_cursor .seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true })); // Early exit if not found in table if !matches!(table_result, SeekResult::Found) { let groups = std::mem::take(groups_to_fetch); let values = std::mem::take(values_to_fetch); *self = FetchDistinctState::FetchGroup { groups_to_fetch: groups, group_idx: *group_idx, value_idx: *value_idx + 1, values_to_fetch: values, }; continue; } // Read the actual record from the table cursor let record = return_if_io!(cursors.table_cursor.record()); if let Some(r) = record { // The table has 5 columns: storage_id, zset_hash, element_id, blob, weight // The weight is at index 4 if let Some(weight) = r.get_value_opt(4) { // Get the weight directly from column 5(index 4) let weight = match weight.to_owned() { Value::Numeric(Numeric::Integer(w)) => w, _ => 0, }; // Store the weight in the existing group's state let state = existing_groups.entry(group_key.clone()).or_default(); state.distinct_value_weights.insert( ( *column_idx, HashableRow::new(*column_idx as i64, vec![value.clone()]), ), weight, ); } } // Move to next value let groups = std::mem::take(groups_to_fetch); let values = std::mem::take(values_to_fetch); *self = FetchDistinctState::FetchGroup { groups_to_fetch: groups, group_idx: *group_idx, value_idx: *value_idx + 1, values_to_fetch: values, }; } FetchDistinctState::Done => { // For plain DISTINCT, construct AggregateState from the weights we fetched if is_plain_distinct { for (_group_key_str, state) in existing_groups.iter_mut() { // For plain DISTINCT, sum all the weights to get total count // Each weight represents how many times the distinct value appears let total_weight: i64 = state.distinct_value_weights.values().sum(); // Set the count based on total weight state.count = total_weight; } } return Ok(IOResult::Done(())); } } } } } /// State machine for persisting distinct values to BTree storage #[derive(Debug)] pub enum DistinctPersistState { Init { distinct_deltas: DistinctDeltas, group_keys: Vec, }, ProcessGroup { distinct_deltas: DistinctDeltas, group_keys: Vec, group_idx: usize, value_keys: Vec<(usize, HashableRow)>, // (col_idx, value) pairs for current group value_idx: usize, }, WriteValue { distinct_deltas: DistinctDeltas, group_keys: Vec, group_idx: usize, value_keys: Vec<(usize, HashableRow)>, value_idx: usize, group_key: String, col_idx: usize, value: Value, weight: isize, write_row: WriteRow, }, Done, } impl DistinctPersistState { pub fn new(distinct_deltas: DistinctDeltas) -> Self { let group_keys: Vec = distinct_deltas.keys().cloned().collect(); Self::Init { distinct_deltas, group_keys, } } pub fn persist_distinct_values( &mut self, operator_id: i64, cursors: &mut DbspStateCursors, generate_group_hash: impl Fn(&str) -> Hash128, ) -> Result> { loop { match self { DistinctPersistState::Init { distinct_deltas, group_keys, } => { let distinct_deltas = std::mem::take(distinct_deltas); let group_keys = std::mem::take(group_keys); *self = DistinctPersistState::ProcessGroup { distinct_deltas, group_keys, group_idx: 0, value_keys: Vec::new(), value_idx: 0, }; } DistinctPersistState::ProcessGroup { distinct_deltas, group_keys, group_idx, value_keys, value_idx, } => { // Check if we're past all groups if *group_idx >= group_keys.len() { *self = DistinctPersistState::Done; continue; } // Check if we need to get value_keys for current group if value_keys.is_empty() && *group_idx < group_keys.len() { let group_key_str = &group_keys[*group_idx]; if let Some(group_values) = distinct_deltas.get(group_key_str) { *value_keys = group_values.keys().cloned().collect(); } } // Check if we have more values in current group if *value_idx >= value_keys.len() { *group_idx += 1; *value_idx = 0; value_keys.clear(); continue; } // Process current value let group_key = group_keys[*group_idx].clone(); let (col_idx, hashable_row) = value_keys[*value_idx].clone(); let weight = distinct_deltas[&group_key][&(col_idx, hashable_row.clone())]; // Extract the value from HashableRow (it's the first element in values vector) let value = hashable_row .values .first() .ok_or_else(|| { LimboError::InternalError( "hashable_row should have at least one value".to_string(), ) })? .clone(); let distinct_deltas = std::mem::take(distinct_deltas); let group_keys = std::mem::take(group_keys); let value_keys = std::mem::take(value_keys); *self = DistinctPersistState::WriteValue { distinct_deltas, group_keys, group_idx: *group_idx, value_keys, value_idx: *value_idx, group_key, col_idx, value, weight, write_row: WriteRow::new(), }; } DistinctPersistState::WriteValue { distinct_deltas, group_keys, group_idx, value_keys, value_idx, group_key, col_idx, value, weight, write_row, } => { // Build the key components for DISTINCT storage let storage_id = generate_storage_id(operator_id, *col_idx, AGG_TYPE_DISTINCT); let zset_hash = generate_group_hash(group_key); // For DISTINCT, element_id is a hash of the value let element_id = hash_value(value, *col_idx); // Create index key let index_key = vec![ Value::from_i64(storage_id), zset_hash.to_value(), element_id.to_value(), ]; // Record values (operator_id, zset_hash, element_id, weight_blob) // Store weight as a minimal AggregateState blob so ReadRecord can parse it let weight_state = AggregateState { count: *weight as i64, ..Default::default() }; let weight_blob = weight_state.to_blob(&[], &[]); let record_values = vec![ Value::from_i64(storage_id), zset_hash.to_value(), element_id.to_value(), Value::Blob(weight_blob), ]; // Write to BTree return_if_io!(write_row.write_row(cursors, index_key, record_values, *weight)); // Move to next value let distinct_deltas = std::mem::take(distinct_deltas); let group_keys = std::mem::take(group_keys); let value_keys = std::mem::take(value_keys); *self = DistinctPersistState::ProcessGroup { distinct_deltas, group_keys, group_idx: *group_idx, value_keys, value_idx: *value_idx + 1, }; } DistinctPersistState::Done => { return Ok(IOResult::Done(())); } } } } } impl MinMaxPersistState { pub fn new(min_max_deltas: MinMaxDeltas) -> Self { let group_keys: Vec = min_max_deltas.keys().cloned().collect(); Self::Init { min_max_deltas, group_keys, } } pub fn persist_min_max( &mut self, operator_id: i64, column_min_max: &HashMap, cursors: &mut DbspStateCursors, generate_group_hash: impl Fn(&str) -> Hash128, ) -> Result> { loop { match self { MinMaxPersistState::Init { min_max_deltas, group_keys, } => { let min_max_deltas = std::mem::take(min_max_deltas); let group_keys = std::mem::take(group_keys); *self = MinMaxPersistState::ProcessGroup { min_max_deltas, group_keys, group_idx: 0, value_idx: 0, }; } MinMaxPersistState::ProcessGroup { min_max_deltas, group_keys, group_idx, value_idx, } => { // Check if we're past all groups if *group_idx >= group_keys.len() { *self = MinMaxPersistState::Done; continue; } let group_key_str = &group_keys[*group_idx]; let values = &min_max_deltas[group_key_str]; // This should always exist // Convert HashMap to Vec for indexed access let values_vec: Vec<_> = values.iter().collect(); // Check if we have more values in current group if *value_idx >= values_vec.len() { *group_idx += 1; *value_idx = 0; // Continue to check if we're past all groups now continue; } // Process current value and extract what we need before taking ownership let ((column_name, hashable_row), weight) = values_vec[*value_idx]; let column_name = *column_name; let value = hashable_row.values[0].clone(); // Extract the Value from HashableRow let weight = *weight; let min_max_deltas = std::mem::take(min_max_deltas); let group_keys = std::mem::take(group_keys); *self = MinMaxPersistState::WriteValue { min_max_deltas, group_keys, group_idx: *group_idx, value_idx: *value_idx, column_name, value, weight, write_row: WriteRow::new(), }; } MinMaxPersistState::WriteValue { min_max_deltas, group_keys, group_idx, value_idx, value, column_name, weight, write_row, } => { // Should have exited in the previous state assert!(*group_idx < group_keys.len()); let group_key_str = &group_keys[*group_idx]; // Get the column info from the pre-computed map let column_info = column_min_max .get(column_name) .expect("Column should exist in column_min_max map"); let column_index = column_info.index; // Build the key components for MinMax storage using new encoding let storage_id = generate_storage_id(operator_id, column_index, AGG_TYPE_MINMAX); let zset_hash = generate_group_hash(group_key_str); // element_id is the actual value for Min/Max let element_id_val = value.clone(); // Create index key let index_key = vec![ Value::from_i64(storage_id), zset_hash.to_value(), element_id_val.clone(), ]; // Record values (operator_id, zset_hash, element_id, unused_placeholder) // For MIN/MAX, the element_id IS the value, so we use NULL for the 4th column let record_values = vec![ Value::from_i64(storage_id), zset_hash.to_value(), element_id_val.clone(), Value::Null, // Placeholder - not used for MIN/MAX ]; return_if_io!(write_row.write_row( cursors, index_key.clone(), record_values, *weight )); // Move to next value let min_max_deltas = std::mem::take(min_max_deltas); let group_keys = std::mem::take(group_keys); *self = MinMaxPersistState::ProcessGroup { min_max_deltas, group_keys, group_idx: *group_idx, value_idx: *value_idx + 1, }; } MinMaxPersistState::Done => { return Ok(IOResult::Done(())); } } } } } ================================================ FILE: core/incremental/compiler.rs ================================================ //! DBSP Compiler: Converts Logical Plans to DBSP Circuits //! //! This module implements compilation from SQL logical plans to DBSP circuits. //! The initial version supports only filter and projection operators. //! //! Based on the DBSP paper: "DBSP: Automatic Incremental View Maintenance for Rich Query Languages" use crate::incremental::aggregate_operator::AggregateOperator; use crate::incremental::dbsp::{Delta, DeltaPair}; use crate::incremental::expr_compiler::CompiledExpression; use crate::incremental::operator::{ create_dbsp_state_index, DbspStateCursors, EvalState, FilterOperator, FilterPredicate, IncrementalOperator, InputOperator, JoinOperator, JoinType, ProjectOperator, }; use crate::schema::Type; use crate::storage::btree::{BTreeCursor, BTreeKey, CursorTrait}; // Note: logical module must be made pub(crate) in translate/mod.rs use crate::numeric::Numeric; use crate::sync::{atomic::Ordering, Arc}; use crate::translate::logical::{ BinaryOperator, Column, ColumnInfo, JoinType as LogicalJoinType, LogicalExpr, LogicalPlan, LogicalSchema, SchemaRef, }; use crate::types::{IOResult, ImmutableRecord, SeekKey, SeekOp, SeekResult, Value}; use crate::Pager; use crate::{return_and_restore_if_io, return_if_io, LimboError, Result}; use rustc_hash::FxHashMap as HashMap; use std::fmt::{self, Display, Formatter}; // The state table has 5 columns: operator_id, zset_id, element_id, value, weight const OPERATOR_COLUMNS: usize = 5; /// State machine for writing rows to simple materialized views (table-only, no index) #[derive(Debug, Default)] pub enum WriteRowView { #[default] GetRecord, Delete, Insert { final_weight: isize, }, Done, } impl WriteRowView { pub fn new() -> Self { Self::default() } /// Write a row with weight management for table-only storage. /// /// # Arguments /// * `cursor` - BTree cursor for the storage /// * `key` - The key to seek (TableRowId) /// * `build_record` - Function that builds the record values to insert. /// Takes the final_weight and returns the complete record values. /// * `weight` - The weight delta to apply pub fn write_row( &mut self, cursor: &mut BTreeCursor, key: SeekKey, build_record: impl Fn(isize) -> Vec, weight: isize, ) -> Result> { loop { match self { WriteRowView::GetRecord => { let res = return_if_io!(cursor.seek(key.clone(), SeekOp::GE { eq_only: true })); if !matches!(res, SeekResult::Found) { *self = WriteRowView::Insert { final_weight: weight, }; } else { let existing_record = return_if_io!(cursor.record()); let r = existing_record.ok_or_else(|| { LimboError::InternalError(format!( "Found key {key:?} in storage but could not read record" )) })?; let last = r.iter()?.last(); // Weight is always the last value let existing_weight = match last { Some(val) => match val?.to_owned() { Value::Numeric(Numeric::Integer(w)) => w as isize, _ => { return Err(LimboError::InternalError(format!( "Invalid weight value in storage for key {key:?}" ))) } }, None => { return Err(LimboError::InternalError(format!( "No weight value found in storage for key {key:?}" ))) } }; let final_weight = existing_weight + weight; if final_weight <= 0 { *self = WriteRowView::Delete } else { *self = WriteRowView::Insert { final_weight } } } } WriteRowView::Delete => { // Mark as Done before delete to avoid retry on I/O *self = WriteRowView::Done; return_if_io!(cursor.delete()); } WriteRowView::Insert { final_weight } => { return_if_io!(cursor.seek(key.clone(), SeekOp::GE { eq_only: true })); // Extract the row ID from the key let key_i64 = match key { SeekKey::TableRowId(id) => id, _ => { return Err(LimboError::InternalError( "Expected TableRowId for storage".to_string(), )) } }; // Build the record values using the provided function let record_values = build_record(*final_weight); // Create an ImmutableRecord from the values let immutable_record = ImmutableRecord::from_values(&record_values, record_values.len()); let btree_key = BTreeKey::new_table_rowid(key_i64, Some(&immutable_record)); // Mark as Done before insert to avoid retry on I/O *self = WriteRowView::Done; return_if_io!(cursor.insert(&btree_key)); } WriteRowView::Done => { return Ok(IOResult::Done(())); } } } } } /// State machine for commit operations pub enum CommitState { /// Initial state - ready to start commit Init, /// Running circuit with commit_operators flag set to true CommitOperators { /// Execute state for running the circuit execute_state: Box, /// Persistent cursors for operator state (table and index) state_cursors: Box, }, /// Updating the materialized view with the delta UpdateView { /// Delta to write to the view delta: Delta, /// Current index in delta.changes being processed current_index: usize, /// State for writing individual rows write_row_state: WriteRowView, /// Cursor for view data btree - created fresh for each row view_cursor: Box, }, } impl std::fmt::Debug for CommitState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Init => write!(f, "Init"), Self::CommitOperators { execute_state, .. } => f .debug_struct("CommitOperators") .field("execute_state", execute_state) .field("has_state_table_cursor", &true) .field("has_state_index_cursor", &true) .finish(), Self::UpdateView { delta, current_index, write_row_state, .. } => f .debug_struct("UpdateView") .field("delta", delta) .field("current_index", current_index) .field("write_row_state", write_row_state) .field("has_view_cursor", &true) .finish(), } } } /// State machine for circuit execution across I/O operations /// Similar to EvalState but for tracking execution state through the circuit #[derive(Debug)] pub enum ExecuteState { /// Empty state so we can allocate the space without executing Uninitialized, /// Initial state - starting circuit execution Init { /// Input deltas to process input_data: DeltaSet, }, /// Processing multiple inputs (for recursive node processing) ProcessingInputs { /// Collection of (node_id, state) pairs to process input_states: Vec<(i64, ExecuteState)>, /// Current index being processed current_index: usize, /// Collected deltas from processed inputs input_deltas: Vec, }, /// Processing a specific node in the circuit ProcessingNode { /// Node's evaluation state (includes the delta in its Init state) eval_state: Box, }, } /// A set of deltas for multiple tables/operators /// This provides a cleaner API for passing deltas through circuit execution #[derive(Debug, Clone, Default)] pub struct DeltaSet { /// Deltas keyed by table/operator name deltas: HashMap, } impl DeltaSet { /// Create a new empty delta set pub fn new() -> Self { Self { deltas: HashMap::default(), } } /// Create an empty delta set (more semantic for "no changes") pub fn empty() -> Self { Self { deltas: HashMap::default(), } } /// Create a DeltaSet from a HashMap pub fn from_map(deltas: HashMap) -> Self { Self { deltas } } /// Add a delta for a table pub fn insert(&mut self, table_name: String, delta: Delta) { self.deltas.insert(table_name, delta); } /// Get delta for a table, returns empty delta if not found pub fn get(&self, table_name: &str) -> Delta { self.deltas .get(table_name) .cloned() .unwrap_or_else(Delta::new) } /// Convert DeltaSet into the underlying HashMap pub fn into_map(self) -> HashMap { self.deltas } /// Check if all deltas in the set are empty pub fn is_empty(&self) -> bool { self.deltas.values().all(|d| d.is_empty()) } } /// Represents a DBSP operator in the compiled circuit #[derive(Debug, Clone, PartialEq)] pub enum DbspOperator { /// Filter operator (σ) - filters records based on a predicate Filter { predicate: DbspExpr }, /// Projection operator (π) - projects specific columns Projection { exprs: Vec, schema: SchemaRef, }, /// Aggregate operator (γ) - performs grouping and aggregation Aggregate { group_exprs: Vec, aggr_exprs: Vec, schema: SchemaRef, }, /// Join operator (⋈) - joins two relations Join { join_type: JoinType, on_exprs: Vec<(DbspExpr, DbspExpr)>, schema: SchemaRef, }, /// Input operator - source of data Input { name: String, schema: SchemaRef }, /// Merge operator for combining streams (used in recursive CTEs and UNION) Merge { schema: SchemaRef }, /// Distinct operator - removes duplicates Distinct { schema: SchemaRef }, } /// Represents an expression in DBSP #[derive(Debug, Clone, PartialEq)] pub enum DbspExpr { /// Column reference Column(String), /// Literal value Literal(Value), /// Binary expression BinaryExpr { left: Box, op: BinaryOperator, right: Box, }, } /// A node in the DBSP circuit DAG pub struct DbspNode { /// Unique identifier for this node pub id: i64, /// The operator metadata pub operator: DbspOperator, /// Input nodes (edges in the DAG) pub inputs: Vec, /// The actual executable operator pub executable: Box, } // SAFETY: This needs to be audited for thread safety. // See: https://github.com/tursodatabase/turso/issues/1552 unsafe impl Send for DbspNode {} unsafe impl Sync for DbspNode {} crate::assert::assert_send_sync!(DbspNode); impl std::fmt::Debug for DbspNode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DbspNode") .field("id", &self.id) .field("operator", &self.operator) .field("inputs", &self.inputs) .field("has_executable", &true) .finish() } } impl DbspNode { fn process_node( &mut self, eval_state: &mut EvalState, commit_operators: bool, cursors: &mut DbspStateCursors, ) -> Result> { // Process delta using the executable operator let op = &mut self.executable; let state = if commit_operators { // Clone the deltas from eval_state - don't extract them // in case we need to re-execute due to I/O let deltas = match eval_state { EvalState::Init { deltas } => deltas.clone(), _ => panic!("commit can only be called when eval_state is in Init state"), }; let result = return_if_io!(op.commit(deltas, cursors)); // After successful commit, move state to Done *eval_state = EvalState::Done; result } else { return_if_io!(op.eval(eval_state, cursors)) }; Ok(IOResult::Done(state)) } } /// Version number for the DBSP circuit format /// This should be incremented when the circuit structure changes pub const DBSP_CIRCUIT_VERSION: u32 = 1; /// Represents a complete DBSP circuit (DAG of operators) #[derive(Debug)] pub struct DbspCircuit { /// All nodes in the circuit, indexed by their ID pub(super) nodes: HashMap, /// Counter for generating unique node IDs next_id: i64, /// Root node ID (the final output) pub(super) root: Option, /// Output schema of the circuit (schema of the root node) pub(super) output_schema: SchemaRef, /// State machine for commit operation commit_state: CommitState, /// Root page for the main materialized view data pub(super) main_data_root: i64, /// Root page for internal DBSP state table pub(super) internal_state_root: i64, /// Root page for the DBSP state table's primary key index pub(super) internal_state_index_root: i64, } // SAFETY: This needs to be audited for thread safety. // See: https://github.com/tursodatabase/turso/issues/1552 unsafe impl Send for DbspCircuit {} unsafe impl Sync for DbspCircuit {} crate::assert::assert_send_sync!(DbspCircuit); impl DbspCircuit { /// Create a new empty circuit with initial empty schema /// The actual output schema will be set when the root node is established pub fn new( main_data_root: i64, internal_state_root: i64, internal_state_index_root: i64, ) -> Self { // Start with an empty schema - will be updated when root is set let empty_schema = Arc::new(LogicalSchema::new(vec![])); Self { nodes: HashMap::default(), next_id: 1, // Start from 1 to reserve 0 for metadata root: None, output_schema: empty_schema, commit_state: CommitState::Init, main_data_root, internal_state_root, internal_state_index_root, } } /// Set the root node and update the output schema fn set_root(&mut self, root_id: i64, schema: SchemaRef) { self.root = Some(root_id); self.output_schema = schema; } /// Get the current materialized state by reading from btree /// Add a node to the circuit fn add_node( &mut self, operator: DbspOperator, inputs: Vec, executable: Box, ) -> i64 { let id = self.next_id; self.next_id += 1; let node = DbspNode { id, operator, inputs, executable, }; self.nodes.insert(id, node); id } pub fn run_circuit( &mut self, execute_state: &mut ExecuteState, pager: &Arc, state_cursors: &mut DbspStateCursors, commit_operators: bool, ) -> Result> { if let Some(root_id) = self.root { self.execute_node( root_id, pager.clone(), execute_state, commit_operators, state_cursors, ) } else { Err(LimboError::ParseError( "Circuit has no root node".to_string(), )) } } /// Execute the circuit with incremental input data (deltas). /// /// # Arguments /// * `pager` - Pager for btree access /// * `context` - Execution context for tracking operator states /// * `execute_state` - State machine containing input deltas and tracking execution progress pub fn execute( &mut self, pager: Arc, execute_state: &mut ExecuteState, ) -> Result> { if let Some(root_id) = self.root { // Create temporary cursors for execute (non-commit) operations let table_cursor = BTreeCursor::new_table(pager.clone(), self.internal_state_root, OPERATOR_COLUMNS); let index_def = create_dbsp_state_index(self.internal_state_index_root); let index_cursor = BTreeCursor::new_index( pager.clone(), self.internal_state_index_root, &index_def, 3, ); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); self.execute_node(root_id, pager, execute_state, false, &mut cursors) } else { Err(LimboError::ParseError( "Circuit has no root node".to_string(), )) } } /// Commit deltas to the circuit, updating internal operator state and persisting to btree. /// This should be called after execute() when you want to make changes permanent. /// /// # Arguments /// * `input_data` - The deltas to commit (same as what was passed to execute) /// * `pager` - Pager for creating cursors to the btrees pub fn commit( &mut self, input_data: HashMap, pager: Arc, ) -> Result> { // No root means nothing to commit if self.root.is_none() { return Ok(IOResult::Done(Delta::new())); } // Get btree root pages let main_data_root = self.main_data_root; // Add 1 for the weight column that we store in the btree let num_columns = self.output_schema.columns.len() + 1; // Convert input_data to DeltaSet once, outside the loop let input_delta_set = DeltaSet::from_map(input_data); loop { // Take ownership of the state for processing, to avoid borrow checker issues (we have // to call run_circuit, which takes &mut self. Because of that, cannot use // return_if_io. We have to use the version that restores the state before returning. let mut state = std::mem::replace(&mut self.commit_state, CommitState::Init); match &mut state { CommitState::Init => { // Create state cursors when entering CommitOperators state let state_table_cursor = BTreeCursor::new_table( pager.clone(), self.internal_state_root, OPERATOR_COLUMNS, ); let index_def = create_dbsp_state_index(self.internal_state_index_root); let state_index_cursor = BTreeCursor::new_index( pager.clone(), self.internal_state_index_root, &index_def, 3, // Index on first 3 columns ); let state_cursors = Box::new(DbspStateCursors::new( state_table_cursor, state_index_cursor, )); self.commit_state = CommitState::CommitOperators { execute_state: Box::new(ExecuteState::Init { input_data: input_delta_set.clone(), }), state_cursors, }; } CommitState::CommitOperators { ref mut execute_state, ref mut state_cursors, } => { let delta = return_and_restore_if_io!( &mut self.commit_state, state, self.run_circuit(execute_state, &pager, state_cursors, true,) ); // Create view cursor when entering UpdateView state let view_cursor = Box::new(BTreeCursor::new_table( pager.clone(), main_data_root, num_columns, )); self.commit_state = CommitState::UpdateView { delta, current_index: 0, write_row_state: WriteRowView::new(), view_cursor, }; } CommitState::UpdateView { delta, current_index, write_row_state, view_cursor, } => { if *current_index >= delta.changes.len() { self.commit_state = CommitState::Init; let delta = std::mem::take(delta); return Ok(IOResult::Done(delta)); } else { let (row, weight) = delta.changes[*current_index].clone(); // If we're starting a new row (GetRecord state), we need a fresh cursor // due to btree cursor state machine limitations if matches!(write_row_state, WriteRowView::GetRecord) { *view_cursor = Box::new(BTreeCursor::new_table( pager.clone(), main_data_root, num_columns, )); } // Build the view row format: row values + weight let key = SeekKey::TableRowId(row.rowid); let row_values = row.values.clone(); let build_fn = move |final_weight: isize| -> Vec { let mut values = row_values.clone(); values.push(Value::from_i64(final_weight as i64)); values }; return_and_restore_if_io!( &mut self.commit_state, state, write_row_state.write_row(view_cursor, key, build_fn, weight) ); // Move to next row let delta = std::mem::take(delta); // Take ownership of view_cursor - we'll create a new one for next row if needed let view_cursor = std::mem::replace( view_cursor, Box::new(BTreeCursor::new_table( pager.clone(), main_data_root, num_columns, )), ); self.commit_state = CommitState::UpdateView { delta, current_index: *current_index + 1, write_row_state: WriteRowView::new(), view_cursor, }; } } } } } /// Execute a specific node in the circuit fn execute_node( &mut self, node_id: i64, pager: Arc, execute_state: &mut ExecuteState, commit_operators: bool, cursors: &mut DbspStateCursors, ) -> Result> { loop { match execute_state { ExecuteState::Uninitialized => { panic!("Trying to execute an uninitialized ExecuteState state machine"); } ExecuteState::Init { input_data } => { let node = self .nodes .get(&node_id) .ok_or_else(|| LimboError::ParseError("Node not found".to_string()))?; // Check if this is an Input node match &node.operator { DbspOperator::Input { name, .. } => { // Input nodes get their delta directly from input_data let delta = input_data.get(name); *execute_state = ExecuteState::ProcessingNode { eval_state: Box::new(EvalState::Init { deltas: delta.into(), }), }; } _ => { // Non-input nodes need to process their inputs let input_data = std::mem::take(input_data); let input_node_ids = node.inputs.clone(); let input_states: Vec<(i64, ExecuteState)> = input_node_ids .iter() .map(|&input_id| { ( input_id, ExecuteState::Init { input_data: input_data.clone(), }, ) }) .collect(); *execute_state = ExecuteState::ProcessingInputs { input_states, current_index: 0, input_deltas: Vec::new(), }; } } } ExecuteState::ProcessingInputs { input_states, current_index, input_deltas, } => { if *current_index >= input_states.len() { // All inputs processed let left_delta = input_deltas.first().cloned().unwrap_or_else(Delta::new); let right_delta = input_deltas.get(1).cloned().unwrap_or_else(Delta::new); *execute_state = ExecuteState::ProcessingNode { eval_state: Box::new(EvalState::Init { deltas: DeltaPair::new(left_delta, right_delta), }), }; } else { // Get the (node_id, state) pair for the current index let (input_node_id, input_state) = &mut input_states[*current_index]; // Create temporary cursors for the recursive call let temp_table_cursor = BTreeCursor::new_table( pager.clone(), self.internal_state_root, OPERATOR_COLUMNS, ); let index_def = create_dbsp_state_index(self.internal_state_index_root); let temp_index_cursor = BTreeCursor::new_index( pager.clone(), self.internal_state_index_root, &index_def, 3, ); let mut temp_cursors = DbspStateCursors::new(temp_table_cursor, temp_index_cursor); let delta = return_if_io!(self.execute_node( *input_node_id, pager.clone(), input_state, commit_operators, &mut temp_cursors )); input_deltas.push(delta); *current_index += 1; } } ExecuteState::ProcessingNode { eval_state } => { // Get mutable reference to node for eval let node = self .nodes .get_mut(&node_id) .ok_or_else(|| LimboError::ParseError("Node not found".to_string()))?; let output_delta = return_if_io!(node.process_node(eval_state, commit_operators, cursors)); return Ok(IOResult::Done(output_delta)); } } } } } impl Display for DbspCircuit { fn fmt(&self, f: &mut Formatter) -> fmt::Result { writeln!(f, "DBSP Circuit:")?; if let Some(root_id) = self.root { self.fmt_node(f, root_id, 0)?; } Ok(()) } } impl DbspCircuit { fn fmt_node(&self, f: &mut Formatter, node_id: i64, depth: usize) -> fmt::Result { let indent = " ".repeat(depth); if let Some(node) = self.nodes.get(&node_id) { match &node.operator { DbspOperator::Filter { predicate } => { writeln!(f, "{indent}Filter[{node_id}]: {predicate:?}")?; } DbspOperator::Projection { exprs, .. } => { writeln!(f, "{indent}Projection[{node_id}]: {exprs:?}")?; } DbspOperator::Aggregate { group_exprs, aggr_exprs, .. } => { writeln!( f, "{indent}Aggregate[{node_id}]: GROUP BY {group_exprs:?}, AGGR {aggr_exprs:?}" )?; } DbspOperator::Join { join_type, on_exprs, .. } => { writeln!(f, "{indent}Join[{node_id}]: {join_type:?} ON {on_exprs:?}")?; } DbspOperator::Input { name, .. } => { writeln!(f, "{indent}Input[{node_id}]: {name}")?; } DbspOperator::Merge { schema } => { writeln!( f, "{indent}Merge[{node_id}]: UNION/Recursive (schema: {} columns)", schema.columns.len() )?; } DbspOperator::Distinct { schema } => { writeln!( f, "{indent}Distinct[{node_id}]: (schema: {} columns)", schema.columns.len() )?; } } for input_id in &node.inputs { self.fmt_node(f, *input_id, depth + 1)?; } } Ok(()) } } /// Compiler from LogicalPlan to DBSP Circuit pub struct DbspCompiler { circuit: DbspCircuit, } impl DbspCompiler { /// Create a new DBSP compiler pub fn new( main_data_root: i64, internal_state_root: i64, internal_state_index_root: i64, ) -> Self { Self { circuit: DbspCircuit::new( main_data_root, internal_state_root, internal_state_index_root, ), } } /// Resolve join condition columns to determine which side each column belongs to. /// /// Returns (left_column, left_index, right_column, right_index) where: /// - left_column/right_column are the Column references /// - left_index/right_index are the column indices in their respective schemas /// /// Handles cases where: /// - Columns are in normal order (left table column = right table column) /// - Columns are swapped (right table column = left table column) /// - One or both columns have table qualifiers /// - Column names exist in both tables but are disambiguated by qualifiers fn resolve_join_columns( first_col: &Column, second_col: &Column, left_schema: &LogicalSchema, right_schema: &LogicalSchema, ) -> Result<(Column, usize, Column, usize)> { // Check all four possibilities to handle ambiguous column names let first_in_left = left_schema.find_column(&first_col.name, first_col.table.as_deref()); let first_in_right = right_schema.find_column(&first_col.name, first_col.table.as_deref()); let second_in_left = left_schema.find_column(&second_col.name, second_col.table.as_deref()); let second_in_right = right_schema.find_column(&second_col.name, second_col.table.as_deref()); // Determine the correct pairing: one column must be from left, one from right if first_in_left.is_some() && second_in_right.is_some() { // first is from left, second is from right let (left_idx, _) = first_in_left.ok_or_else(|| { LimboError::InternalError("first_in_left should exist".to_string()) })?; let (right_idx, _) = second_in_right.ok_or_else(|| { LimboError::InternalError("second_in_right should exist".to_string()) })?; Ok((first_col.clone(), left_idx, second_col.clone(), right_idx)) } else if first_in_right.is_some() && second_in_left.is_some() { // first is from right, second is from left let (left_idx, _) = second_in_left.ok_or_else(|| { LimboError::InternalError("second_in_left should exist".to_string()) })?; let (right_idx, _) = first_in_right.ok_or_else(|| { LimboError::InternalError("first_in_right should exist".to_string()) })?; Ok((second_col.clone(), left_idx, first_col.clone(), right_idx)) } else { // Provide specific error messages for different failure cases if first_in_left.is_none() && first_in_right.is_none() { Err(LimboError::ParseError(format!( "Join condition column '{}' not found in either input", first_col.name ))) } else if second_in_left.is_none() && second_in_right.is_none() { Err(LimboError::ParseError(format!( "Join condition column '{}' not found in either input", second_col.name ))) } else { Err(LimboError::ParseError(format!( "Join condition columns '{}' and '{}' must come from different input tables", first_col.name, second_col.name ))) } } } /// Compile a logical plan to a DBSP circuit pub fn compile(mut self, plan: &LogicalPlan) -> Result { let root_id = self.compile_plan(plan)?; let output_schema = plan.schema().clone(); self.circuit.set_root(root_id, output_schema); Ok(self.circuit) } /// Recursively compile a logical plan node fn compile_plan(&mut self, plan: &LogicalPlan) -> Result { match plan { LogicalPlan::Projection(proj) => { // Compile the input first let input_id = self.compile_plan(&proj.input)?; // Get input column names for the ProjectOperator let input_schema = proj.input.schema(); let input_column_names: Vec = input_schema.columns.iter() .map(|col| col.name.clone()) .collect(); // Convert logical expressions to DBSP expressions let dbsp_exprs = proj.exprs.iter() .map(Self::compile_expr) .collect::>>()?; // Compile logical expressions to CompiledExpressions let mut compiled_exprs = Vec::new(); let mut aliases = Vec::new(); for expr in &proj.exprs { let (compiled, alias) = Self::compile_expression(expr, input_schema)?; compiled_exprs.push(compiled); aliases.push(alias); } // Get output column names from the projection schema let output_column_names: Vec = proj.schema.columns.iter() .map(|col| col.name.clone()) .collect(); // Create the ProjectOperator let executable: Box = Box::new(ProjectOperator::from_compiled(compiled_exprs, aliases, input_column_names, output_column_names)?); // Create projection node let node_id = self.circuit.add_node( DbspOperator::Projection { exprs: dbsp_exprs, schema: proj.schema.clone(), }, vec![input_id], executable, ); Ok(node_id) } LogicalPlan::Filter(filter) => { // Compile the input first let input_id = self.compile_plan(&filter.input)?; // Get input schema for column resolution let input_schema = filter.input.schema(); // Check if the predicate contains expressions that need to be computed if Self::predicate_needs_projection(&filter.predicate) { // Complex expression in WHERE clause - need to add projection first // 1. Create projection that adds the computed expression as a new column // First, get all existing columns let mut projection_exprs = Vec::new(); let mut dbsp_exprs = Vec::new(); for col in &input_schema.columns { projection_exprs.push(LogicalExpr::Column(Column { name: col.name.clone(), table: None, })); dbsp_exprs.push(DbspExpr::Column(col.name.clone())); } // Now add the expression as a computed column let temp_column_name = "__temp_filter_expr"; let computed_expr = Self::extract_expression_from_predicate(&filter.predicate)?; projection_exprs.push(computed_expr); // Compile the projection expressions let mut compiled_exprs = Vec::new(); let mut aliases = Vec::new(); let mut output_names = Vec::new(); for (i, expr) in projection_exprs.iter().enumerate() { let (compiled, _alias) = Self::compile_expression(expr, input_schema)?; compiled_exprs.push(compiled); if i < input_schema.columns.len() { aliases.push(None); output_names.push(input_schema.columns[i].name.clone()); } else { aliases.push(Some(temp_column_name.to_string())); output_names.push(temp_column_name.to_string()); } } // Get input column names for ProjectOperator let input_column_names: Vec = input_schema.columns.iter() .map(|col| col.name.clone()) .collect(); // Create projection operator let proj_executable: Box = Box::new(ProjectOperator::from_compiled( compiled_exprs.clone(), aliases.clone(), input_column_names, output_names.clone() )?); // Create updated schema for the projection output let mut proj_schema_columns = input_schema.columns.clone(); proj_schema_columns.push(ColumnInfo { name: temp_column_name.to_string(), table: None, database: None, table_alias: None, ty: Type::Integer, // Computed expressions default to Integer }); let proj_schema = SchemaRef::new(LogicalSchema { columns: proj_schema_columns, }); // Add projection node let proj_id = self.circuit.add_node( DbspOperator::Projection { exprs: dbsp_exprs.clone(), schema: proj_schema.clone(), }, vec![input_id], proj_executable, ); // Now create a filter that replaces the complex expression with the temp column // but keeps all other conditions intact let replaced_predicate = Self::replace_complex_with_temp(&filter.predicate, temp_column_name)?; let filter_predicate = Self::compile_filter_predicate(&replaced_predicate, &proj_schema)?; let filter_executable: Box = Box::new(FilterOperator::new(filter_predicate)); // Create filter node let filter_id = self.circuit.add_node( DbspOperator::Filter { predicate: Self::compile_expr(&replaced_predicate)? }, vec![proj_id], filter_executable, ); // Finally, project again to remove the temporary column let mut final_exprs = Vec::new(); let mut final_aliases = Vec::new(); let mut final_names = Vec::new(); let mut final_dbsp_exprs = Vec::new(); for (i, column) in input_schema.columns.iter().enumerate() { let col_name = &column.name; final_exprs.push(compiled_exprs[i].clone()); final_aliases.push(None); final_names.push(col_name.clone()); final_dbsp_exprs.push(DbspExpr::Column(col_name.clone())); } // Input names for the final projection include the temp column let filter_output_names = output_names.clone(); let final_proj_executable: Box = Box::new(ProjectOperator::from_compiled( final_exprs, final_aliases, filter_output_names, final_names.clone() )?); let final_id = self.circuit.add_node( DbspOperator::Projection { exprs: final_dbsp_exprs, schema: input_schema.clone(), // Back to original schema }, vec![filter_id], final_proj_executable, ); Ok(final_id) } else { // Simple filter - use existing implementation // Convert predicate to DBSP expression let dbsp_predicate = Self::compile_expr(&filter.predicate)?; // Convert to FilterPredicate let filter_predicate = Self::compile_filter_predicate(&filter.predicate, input_schema)?; // Create executable operator let executable: Box = Box::new(FilterOperator::new(filter_predicate)); // Create filter node let node_id = self.circuit.add_node( DbspOperator::Filter { predicate: dbsp_predicate }, vec![input_id], executable, ); Ok(node_id) } } LogicalPlan::Aggregate(agg) => { // Compile the input first let input_id = self.compile_plan(&agg.input)?; // Get input column names let input_schema = agg.input.schema(); let input_column_names: Vec = input_schema.columns.iter() .map(|col| col.name.clone()) .collect(); // Compile group by expressions to column indices let mut group_by_indices = Vec::new(); let mut dbsp_group_exprs = Vec::new(); for expr in &agg.group_expr { // For now, only support simple column references in GROUP BY if let LogicalExpr::Column(col) = expr { // Find the column index in the input schema using qualified lookup let (col_idx, _) = input_schema.find_column(&col.name, col.table.as_deref()) .ok_or_else(|| LimboError::ParseError( format!("GROUP BY column '{}' not found in input", col.name) ))?; group_by_indices.push(col_idx); dbsp_group_exprs.push(DbspExpr::Column(col.name.clone())); } else { return Err(LimboError::ParseError( "Only column references are supported in GROUP BY for incremental views".to_string() )); } } // Compile aggregate expressions (both DISTINCT and regular) let mut aggregate_functions = Vec::new(); for expr in &agg.aggr_expr { if let LogicalExpr::AggregateFunction { fun, args, distinct } = expr { use crate::function::AggFunc; use crate::incremental::aggregate_operator::AggregateFunction; match fun { AggFunc::Count | AggFunc::Count0 => { if *distinct { // COUNT(DISTINCT col) if args.is_empty() { return Err(LimboError::ParseError("COUNT(DISTINCT) requires an argument".to_string())); } if let LogicalExpr::Column(col) = &args[0] { let (col_idx, _) = input_schema.find_column(&col.name, col.table.as_deref()) .ok_or_else(|| LimboError::ParseError( format!("COUNT(DISTINCT) column '{}' not found in input", col.name) ))?; aggregate_functions.push(AggregateFunction::CountDistinct(col_idx)); } else { return Err(LimboError::ParseError( "Only column references are supported in aggregate functions for incremental views".to_string() )); } } else { aggregate_functions.push(AggregateFunction::Count); } } AggFunc::Sum => { if args.is_empty() { return Err(LimboError::ParseError("SUM requires an argument".to_string())); } // Extract column index from the argument if let LogicalExpr::Column(col) = &args[0] { let (col_idx, _) = input_schema.find_column(&col.name, col.table.as_deref()) .ok_or_else(|| LimboError::ParseError( format!("SUM column '{}' not found in input", col.name) ))?; if *distinct { aggregate_functions.push(AggregateFunction::SumDistinct(col_idx)); } else { aggregate_functions.push(AggregateFunction::Sum(col_idx)); } } else { return Err(LimboError::ParseError( "Only column references are supported in aggregate functions for incremental views".to_string() )); } } AggFunc::Avg => { if args.is_empty() { return Err(LimboError::ParseError("AVG requires an argument".to_string())); } if let LogicalExpr::Column(col) = &args[0] { let (col_idx, _) = input_schema.find_column(&col.name, col.table.as_deref()) .ok_or_else(|| LimboError::ParseError( format!("AVG column '{}' not found in input", col.name) ))?; if *distinct { aggregate_functions.push(AggregateFunction::AvgDistinct(col_idx)); } else { aggregate_functions.push(AggregateFunction::Avg(col_idx)); } } else { return Err(LimboError::ParseError( "Only column references are supported in aggregate functions for incremental views".to_string() )); } } AggFunc::Min => { if args.is_empty() { return Err(LimboError::ParseError("MIN requires an argument".to_string())); } if let LogicalExpr::Column(col) = &args[0] { let (col_idx, _) = input_schema.find_column(&col.name, col.table.as_deref()) .ok_or_else(|| LimboError::ParseError( format!("MIN column '{}' not found in input", col.name) ))?; aggregate_functions.push(AggregateFunction::Min(col_idx)); } else { return Err(LimboError::ParseError( "Only column references are supported in MIN for incremental views".to_string() )); } } AggFunc::Max => { if args.is_empty() { return Err(LimboError::ParseError("MAX requires an argument".to_string())); } if let LogicalExpr::Column(col) = &args[0] { let (col_idx, _) = input_schema.find_column(&col.name, col.table.as_deref()) .ok_or_else(|| LimboError::ParseError( format!("MAX column '{}' not found in input", col.name) ))?; aggregate_functions.push(AggregateFunction::Max(col_idx)); } else { return Err(LimboError::ParseError( "Only column references are supported in MAX for incremental views".to_string() )); } } _ => { return Err(LimboError::ParseError( format!("Unsupported aggregate function in DBSP compiler: {fun:?}") )); } } } else { return Err(LimboError::ParseError( "Expected aggregate function in aggregate expressions".to_string() )); } } let operator_id = self.circuit.next_id; use crate::incremental::aggregate_operator::AggregateOperator; let executable: Box = Box::new(AggregateOperator::new( operator_id, group_by_indices.clone(), aggregate_functions.clone(), input_column_names, )?); let result_node_id = self.circuit.add_node( DbspOperator::Aggregate { group_exprs: dbsp_group_exprs, aggr_exprs: aggregate_functions, schema: agg.schema.clone(), }, vec![input_id], executable, ); Ok(result_node_id) } LogicalPlan::Join(join) => { // Compile left and right inputs let left_id = self.compile_plan(&join.left)?; let right_id = self.compile_plan(&join.right)?; // Get schemas from inputs let left_schema = join.left.schema(); let right_schema = join.right.schema(); // Get column names from left and right let left_columns: Vec = left_schema.columns.iter() .map(|col| col.name.clone()) .collect(); let right_columns: Vec = right_schema.columns.iter() .map(|col| col.name.clone()) .collect(); // Check if there are any non-equijoin conditions in the filter if join.filter.is_some() { return Err(LimboError::ParseError( "Non-equijoin conditions are not supported in materialized views. Only equality joins (=) are allowed.".to_string() )); } // Check if we have at least one equijoin condition if join.on.is_empty() { return Err(LimboError::ParseError( "Joins in materialized views must have at least one equality condition.".to_string() )); } // Extract join key indices from join conditions // For now, we only support equijoin conditions let mut left_key_indices = Vec::new(); let mut right_key_indices = Vec::new(); let mut dbsp_on_exprs = Vec::new(); for (left_expr, right_expr) in &join.on { // Extract column indices from join expressions // We expect simple column references in join conditions if let (LogicalExpr::Column(first_col), LogicalExpr::Column(second_col)) = (left_expr, right_expr) { let (actual_left_col, actual_left_idx, actual_right_col, actual_right_idx) = Self::resolve_join_columns(first_col, second_col, left_schema, right_schema)?; left_key_indices.push(actual_left_idx); right_key_indices.push(actual_right_idx); // Convert to DBSP expressions dbsp_on_exprs.push(( DbspExpr::Column(actual_left_col.name.clone()), DbspExpr::Column(actual_right_col.name.clone()) )); } else { return Err(LimboError::ParseError( "Only simple column references are supported in join conditions for incremental views".to_string() )); } } // Convert logical join type to operator join type let operator_join_type = match join.join_type { LogicalJoinType::Inner => JoinType::Inner, LogicalJoinType::Left => JoinType::Left, LogicalJoinType::Right => JoinType::Right, LogicalJoinType::Full => JoinType::Full, LogicalJoinType::Cross => JoinType::Cross, }; // Create JoinOperator let operator_id = self.circuit.next_id; let executable: Box = Box::new(JoinOperator::new( operator_id, operator_join_type.clone(), left_key_indices, right_key_indices, left_columns, right_columns, )?); // Create join node let node_id = self.circuit.add_node( DbspOperator::Join { join_type: operator_join_type, on_exprs: dbsp_on_exprs, schema: join.schema.clone(), }, vec![left_id, right_id], executable, ); Ok(node_id) } LogicalPlan::TableScan(scan) => { // Create input node with InputOperator for uniform handling let executable: Box = Box::new(InputOperator::new(scan.table_name.clone())); let node_id = self.circuit.add_node( DbspOperator::Input { name: scan.table_name.clone(), schema: scan.schema.clone(), }, vec![], executable, ); Ok(node_id) } LogicalPlan::Union(union) => { // Handle UNION and UNION ALL self.compile_union(union) } LogicalPlan::Distinct(distinct) => { // DISTINCT is implemented as GROUP BY all columns with a special aggregate let input_id = self.compile_plan(&distinct.input)?; let input_schema = distinct.input.schema(); // Create GROUP BY indices for all columns let group_by: Vec = (0..input_schema.columns.len()).collect(); // Column names for the operator let input_column_names: Vec = input_schema.columns.iter() .map(|col| col.name.clone()) .collect(); // Create the aggregate operator with DISTINCT mode let operator_id = self.circuit.next_id; let executable: Box = Box::new( AggregateOperator::new( operator_id, group_by, vec![], // Empty aggregates indicates plain DISTINCT input_column_names, )?, ); // Add the node to the circuit let node_id = self.circuit.add_node( DbspOperator::Distinct { schema: input_schema.clone(), }, vec![input_id], executable, ); Ok(node_id) } _ => Err(LimboError::ParseError( format!("Unsupported operator in DBSP compiler: only Filter, Projection, Join, Aggregate, and Union are supported, got: {:?}", match plan { LogicalPlan::Sort(_) => "Sort", LogicalPlan::Limit(_) => "Limit", LogicalPlan::Union(_) => "Union", LogicalPlan::EmptyRelation(_) => "EmptyRelation", LogicalPlan::Values(_) => "Values", LogicalPlan::WithCTE(_) => "WithCTE", LogicalPlan::CTERef(_) => "CTERef", _ => "Unknown", } ) )), } } /// Extract a representative table name from a logical plan (for UNION ALL identification) /// Returns a string that uniquely identifies the source of the data fn extract_source_identifier(plan: &LogicalPlan) -> String { match plan { LogicalPlan::TableScan(scan) => { // Direct table scan - use the table name scan.table_name.clone() } LogicalPlan::Projection(proj) => { // Pass through to input Self::extract_source_identifier(&proj.input) } LogicalPlan::Filter(filter) => { // Pass through to input Self::extract_source_identifier(&filter.input) } LogicalPlan::Aggregate(agg) => { // Aggregate of a table format!("agg_{}", Self::extract_source_identifier(&agg.input)) } LogicalPlan::Sort(sort) => { // Pass through to input Self::extract_source_identifier(&sort.input) } LogicalPlan::Limit(limit) => { // Pass through to input Self::extract_source_identifier(&limit.input) } LogicalPlan::Join(join) => { // Join of two sources - combine their identifiers let left_id = Self::extract_source_identifier(&join.left); let right_id = Self::extract_source_identifier(&join.right); format!("join_{left_id}_{right_id}") } LogicalPlan::Union(union) => { // Union of multiple sources if union.inputs.is_empty() { "union_empty".to_string() } else { let identifiers: Vec = union .inputs .iter() .map(|input| Self::extract_source_identifier(input)) .collect(); format!("union_{}", identifiers.join("_")) } } LogicalPlan::Distinct(distinct) => { // Distinct of a source format!( "distinct_{}", Self::extract_source_identifier(&distinct.input) ) } LogicalPlan::WithCTE(with_cte) => { // CTE body Self::extract_source_identifier(&with_cte.body) } LogicalPlan::CTERef(cte_ref) => { // CTE reference - use the CTE name format!("cte_{}", cte_ref.name) } LogicalPlan::EmptyRelation(_) => "empty".to_string(), LogicalPlan::Values(_) => "values".to_string(), } } /// Compile a UNION operator fn compile_union(&mut self, union: &crate::translate::logical::Union) -> Result { if union.inputs.len() != 2 { return Err(LimboError::ParseError(format!( "UNION requires exactly 2 inputs, got {}", union.inputs.len() ))); } // Extract source identifiers from each input (for UNION ALL) let left_source = Self::extract_source_identifier(&union.inputs[0]); let right_source = Self::extract_source_identifier(&union.inputs[1]); // Compile left and right inputs let left_id = self.compile_plan(&union.inputs[0])?; let right_id = self.compile_plan(&union.inputs[1])?; use crate::incremental::merge_operator::{MergeOperator, UnionMode}; // Create a merge operator that handles the rowid transformation let operator_id = self.circuit.next_id; let mode = if union.all { // For UNION ALL, pass the source identifiers UnionMode::All { left_table: left_source, right_table: right_source, } } else { UnionMode::Distinct }; let merge_operator = Box::new(MergeOperator::new(operator_id, mode)); let merge_id = self.circuit.add_node( DbspOperator::Merge { schema: union.schema.clone(), }, vec![left_id, right_id], merge_operator, ); Ok(merge_id) } /// Convert a logical expression to a DBSP expression fn compile_expr(expr: &LogicalExpr) -> Result { match expr { LogicalExpr::Column(col) => Ok(DbspExpr::Column(col.name.clone())), LogicalExpr::Literal(val) => Ok(DbspExpr::Literal(val.clone())), LogicalExpr::BinaryExpr { left, op, right } => { let left_expr = Self::compile_expr(left)?; let right_expr = Self::compile_expr(right)?; Ok(DbspExpr::BinaryExpr { left: Box::new(left_expr), op: *op, right: Box::new(right_expr), }) } LogicalExpr::Alias { expr, .. } => { // For aliases, compile the underlying expression Self::compile_expr(expr) } // For complex expressions (functions, etc), we can't represent them as DbspExpr // but that's OK - they'll be handled by the ProjectOperator's VDBE compilation // For now, just use a placeholder _ => { // Use a literal null as placeholder - the actual execution will use the compiled VDBE Ok(DbspExpr::Literal(Value::Null)) } } } /// Compile a logical expression to a CompiledExpression and optional alias fn compile_expression( expr: &LogicalExpr, input_schema: &LogicalSchema, ) -> Result<(CompiledExpression, Option)> { // Check for alias first if let LogicalExpr::Alias { expr, alias } = expr { // For aliases, compile the underlying expression and return with alias let (compiled, _) = Self::compile_expression(expr, input_schema)?; return Ok((compiled, Some(alias.clone()))); } // Convert LogicalExpr to AST Expr with proper column resolution let ast_expr = Self::logical_to_ast_expr_with_schema(expr, input_schema)?; // Extract column names from schema for CompiledExpression::compile let input_column_names: Vec = input_schema .columns .iter() .map(|col| col.name.clone()) .collect(); // For all expressions (simple or complex), use CompiledExpression::compile // This handles both trivial cases and complex VDBE compilation // We need to set up the necessary context use crate::sync::Arc; use crate::{Database, MemoryIO, SymbolTable}; // Create an internal connection for expression compilation let io = Arc::new(MemoryIO::new()); let db = Database::open_file(io, ":memory:")?; let internal_conn = db.connect()?; internal_conn.set_query_only(true); internal_conn.auto_commit.store(false, Ordering::SeqCst); // Create temporary symbol table let temp_syms = SymbolTable::new(); // Get a minimal schema for compilation (we don't need the full schema for expressions) let schema = crate::schema::Schema::new(); // Compile the expression using the existing CompiledExpression::compile let compiled = CompiledExpression::compile( &ast_expr, &input_column_names, &schema, &temp_syms, internal_conn, )?; Ok((compiled, None)) } /// Convert LogicalExpr to AST Expr with qualified column resolution fn logical_to_ast_expr_with_schema( expr: &LogicalExpr, schema: &LogicalSchema, ) -> Result { use turso_parser::ast; match expr { LogicalExpr::Column(col) => { // Find the column index using qualified lookup let (idx, _) = schema .find_column(&col.name, col.table.as_deref()) .ok_or_else(|| { LimboError::ParseError(format!( "Column '{}' with table {:?} not found in schema", col.name, col.table )) })?; // Return a Register expression with the correct index Ok(ast::Expr::Register(idx)) } LogicalExpr::Literal(val) => { let lit = match val { Value::Numeric(Numeric::Integer(i)) => ast::Literal::Numeric(i.to_string()), Value::Numeric(Numeric::Float(f)) => { ast::Literal::Numeric(f64::from(*f).to_string()) } Value::Text(t) => { // Add quotes for string literals as translate_expr expects them // Also escape any single quotes in the string let escaped = t.to_string().replace('\'', "''"); ast::Literal::String(format!("'{escaped}'")) } Value::Blob(b) => ast::Literal::Blob(format!("{b:?}")), Value::Null => ast::Literal::Null, }; Ok(ast::Expr::Literal(lit)) } LogicalExpr::BinaryExpr { left, op, right } => { let left_expr = Self::logical_to_ast_expr_with_schema(left, schema)?; let right_expr = Self::logical_to_ast_expr_with_schema(right, schema)?; Ok(ast::Expr::Binary( Box::new(left_expr), *op, Box::new(right_expr), )) } LogicalExpr::ScalarFunction { fun, args } => { let ast_args: Result> = args .iter() .map(|arg| Self::logical_to_ast_expr_with_schema(arg, schema)) .collect(); let ast_args: Vec> = ast_args?.into_iter().map(Box::new).collect(); Ok(ast::Expr::FunctionCall { name: ast::Name::exact(fun.clone()), distinctness: None, args: ast_args, order_by: Vec::new(), filter_over: ast::FunctionTail { filter_clause: None, over_clause: None, }, }) } LogicalExpr::Alias { expr, .. } => { // For conversion to AST, ignore the alias and convert the inner expression Self::logical_to_ast_expr_with_schema(expr, schema) } LogicalExpr::AggregateFunction { fun, args, distinct, } => { // Convert aggregate function to AST let ast_args: Result> = args .iter() .map(|arg| Self::logical_to_ast_expr_with_schema(arg, schema)) .collect(); let ast_args: Vec> = ast_args?.into_iter().map(Box::new).collect(); // Get the function name based on the aggregate type let func_name = match fun { crate::function::AggFunc::Count => "COUNT", crate::function::AggFunc::Sum => "SUM", crate::function::AggFunc::Avg => "AVG", crate::function::AggFunc::Min => "MIN", crate::function::AggFunc::Max => "MAX", _ => { return Err(LimboError::ParseError(format!( "Unsupported aggregate function: {fun:?}" ))) } }; Ok(ast::Expr::FunctionCall { name: ast::Name::exact(func_name.to_string()), distinctness: if *distinct { Some(ast::Distinctness::Distinct) } else { None }, args: ast_args, order_by: Vec::new(), filter_over: ast::FunctionTail { filter_clause: None, over_clause: None, }, }) } LogicalExpr::Between { expr, low, high, negated, } => { // BETWEEN x AND y is rewritten as (expr >= x AND expr <= y) // NOT BETWEEN x AND y is rewritten as (expr < x OR expr > y) let expr_ast = Self::logical_to_ast_expr_with_schema(expr, schema)?; let low_ast = Self::logical_to_ast_expr_with_schema(low, schema)?; let high_ast = Self::logical_to_ast_expr_with_schema(high, schema)?; if *negated { // NOT BETWEEN: (expr < low OR expr > high) Ok(ast::Expr::Binary( Box::new(ast::Expr::Binary( Box::new(expr_ast.clone()), ast::Operator::Less, Box::new(low_ast), )), ast::Operator::Or, Box::new(ast::Expr::Binary( Box::new(expr_ast), ast::Operator::Greater, Box::new(high_ast), )), )) } else { // BETWEEN: (expr >= low AND expr <= high) Ok(ast::Expr::Binary( Box::new(ast::Expr::Binary( Box::new(expr_ast.clone()), ast::Operator::GreaterEquals, Box::new(low_ast), )), ast::Operator::And, Box::new(ast::Expr::Binary( Box::new(expr_ast), ast::Operator::LessEquals, Box::new(high_ast), )), )) } } LogicalExpr::InList { expr, list, negated, } => { let lhs = Box::new(Self::logical_to_ast_expr_with_schema(expr, schema)?); let values: Result> = list .iter() .map(|item| { let ast_expr = Self::logical_to_ast_expr_with_schema(item, schema)?; Ok(Box::new(ast_expr)) }) .collect(); Ok(ast::Expr::InList { lhs, not: *negated, rhs: values?, }) } LogicalExpr::Like { expr, pattern, escape, negated, } => { let lhs = Box::new(Self::logical_to_ast_expr_with_schema(expr, schema)?); let rhs = Box::new(Self::logical_to_ast_expr_with_schema(pattern, schema)?); let escape_expr = escape .map(|c| Box::new(ast::Expr::Literal(ast::Literal::String(c.to_string())))); Ok(ast::Expr::Like { lhs, not: *negated, op: ast::LikeOperator::Like, rhs, escape: escape_expr, }) } LogicalExpr::IsNull { expr, negated } => { let inner_expr = Box::new(Self::logical_to_ast_expr_with_schema(expr, schema)?); if *negated { // IS NOT NULL needs to be represented differently Ok(ast::Expr::Unary( ast::UnaryOperator::Not, Box::new(ast::Expr::IsNull(inner_expr)), )) } else { Ok(ast::Expr::IsNull(inner_expr)) } } LogicalExpr::Cast { expr, type_name } => { let inner_expr = Box::new(Self::logical_to_ast_expr_with_schema(expr, schema)?); Ok(ast::Expr::Cast { expr: inner_expr, type_name: type_name.clone(), }) } _ => Err(LimboError::ParseError(format!( "Cannot convert LogicalExpr to AST Expr: {expr:?}" ))), } } /// Check if a predicate contains expressions that need projection fn predicate_needs_projection(expr: &LogicalExpr) -> bool { match expr { LogicalExpr::BinaryExpr { left, op, right } => { // Only these specific simple patterns DON'T need projection match (left.as_ref(), right.as_ref()) { // Simple column to literal comparisons (LogicalExpr::Column(_), LogicalExpr::Literal(_)) if matches!( op, BinaryOperator::Equals | BinaryOperator::NotEquals | BinaryOperator::Greater | BinaryOperator::GreaterEquals | BinaryOperator::Less | BinaryOperator::LessEquals ) => { false } // Simple column to column comparisons (LogicalExpr::Column(_), LogicalExpr::Column(_)) if matches!( op, BinaryOperator::Equals | BinaryOperator::NotEquals | BinaryOperator::Greater | BinaryOperator::GreaterEquals | BinaryOperator::Less | BinaryOperator::LessEquals ) => { false } // AND/OR of simple expressions - check recursively _ if matches!(op, BinaryOperator::And | BinaryOperator::Or) => { Self::predicate_needs_projection(left) || Self::predicate_needs_projection(right) } // Everything else needs projection _ => true, } } // These simple cases don't need projection LogicalExpr::Column(_) | LogicalExpr::Literal(_) => false, // Default: assume we need projection for safety // This includes: Between, InList, Like, IsNull, Cast, ScalarFunction, Case, // InSubquery, Exists, ScalarSubquery, and any future expression types _ => true, } } /// Extract the expression part from a predicate that needs to be computed fn extract_expression_from_predicate(expr: &LogicalExpr) -> Result { match expr { LogicalExpr::BinaryExpr { left, op, right } => { // Handle AND/OR - recursively find the complex expression if matches!(op, BinaryOperator::And | BinaryOperator::Or) { // Check left side first if Self::predicate_needs_projection(left) { return Self::extract_expression_from_predicate(left); } // Then check right side if Self::predicate_needs_projection(right) { return Self::extract_expression_from_predicate(right); } // Neither side needs projection (shouldn't happen if predicate_needs_projection was true) return Ok(expr.clone()); } // For comparison expressions, check if we need to extract a subexpression if matches!( op, BinaryOperator::Greater | BinaryOperator::GreaterEquals | BinaryOperator::Less | BinaryOperator::LessEquals | BinaryOperator::Equals | BinaryOperator::NotEquals ) { // If the left side is complex (not a column), extract it if !matches!( left.as_ref(), LogicalExpr::Column(_) | LogicalExpr::Literal(_) ) { return Ok((**left).clone()); } // If the right side is complex (not a literal), extract it if !matches!( right.as_ref(), LogicalExpr::Column(_) | LogicalExpr::Literal(_) ) { return Ok((**right).clone()); } // Both sides are simple but the expression as a whole might need projection // (e.g., for arithmetic operations) Ok(expr.clone()) } else { // For other binary operators (arithmetic, etc.), return the whole expression Ok(expr.clone()) } } // For non-binary expressions (BETWEEN, IN, LIKE, functions, etc.), // we need to compute the whole expression as a boolean _ => Ok(expr.clone()), } } /// Replace complex expressions in the predicate with references to the temp column fn replace_complex_with_temp( expr: &LogicalExpr, temp_column_name: &str, ) -> Result { match expr { LogicalExpr::BinaryExpr { left, op, right } => { // Handle AND/OR - recursively process both sides if matches!(op, BinaryOperator::And | BinaryOperator::Or) { let new_left = Self::replace_complex_with_temp(left, temp_column_name)?; let new_right = Self::replace_complex_with_temp(right, temp_column_name)?; return Ok(LogicalExpr::BinaryExpr { left: Box::new(new_left), op: *op, right: Box::new(new_right), }); } // Check if this is a complex comparison that needs replacement if Self::predicate_needs_projection(expr) { // Determine which side is complex and needs replacement let left_is_simple = matches!( left.as_ref(), LogicalExpr::Column(_) | LogicalExpr::Literal(_) ); let right_is_simple = matches!( right.as_ref(), LogicalExpr::Column(_) | LogicalExpr::Literal(_) ); if !left_is_simple { // Left side is complex - replace it with temp column return Ok(LogicalExpr::BinaryExpr { left: Box::new(LogicalExpr::Column(Column { name: temp_column_name.to_string(), table: None, })), op: *op, right: right.clone(), }); } else if !right_is_simple { // Right side is complex - replace it with temp column return Ok(LogicalExpr::BinaryExpr { left: left.clone(), op: *op, right: Box::new(LogicalExpr::Column(Column { name: temp_column_name.to_string(), table: None, })), }); } else { // Both sides are simple, but the expression as a whole needs projection // This shouldn't happen normally, but keep the expression as-is return Ok(expr.clone()); } } // Simple comparison - keep as is Ok(expr.clone()) } // For non-binary expressions that need projection (BETWEEN, IN, etc.), // replace the whole expression with a column reference to the temp column // The temp column will hold the boolean result of evaluating the expression _ if Self::predicate_needs_projection(expr) => { // The complex expression result is in the temp column // We need to check if it's true (non-zero) Ok(LogicalExpr::BinaryExpr { left: Box::new(LogicalExpr::Column(Column { name: temp_column_name.to_string(), table: None, })), op: BinaryOperator::Equals, right: Box::new(LogicalExpr::Literal(Value::from_i64(1))), // true = 1 in SQL }) } _ => Ok(expr.clone()), } } /// Compile a logical expression to a FilterPredicate for execution fn compile_filter_predicate( expr: &LogicalExpr, schema: &LogicalSchema, ) -> Result { match expr { LogicalExpr::BinaryExpr { left, op, right } => { // Extract column name and value for simple predicates // First check for column-to-column comparisons if let (LogicalExpr::Column(left_col), LogicalExpr::Column(right_col)) = (left.as_ref(), right.as_ref()) { // Resolve both column names to indices let left_idx = schema .columns .iter() .position(|c| c.name == left_col.name) .ok_or_else(|| { crate::LimboError::ParseError(format!( "Column '{}' not found in schema for filter", left_col.name )) })?; let right_idx = schema .columns .iter() .position(|c| c.name == right_col.name) .ok_or_else(|| { crate::LimboError::ParseError(format!( "Column '{}' not found in schema for filter", right_col.name )) })?; match op { BinaryOperator::Equals => Ok(FilterPredicate::ColumnEquals { left_idx, right_idx, }), BinaryOperator::NotEquals => Ok(FilterPredicate::ColumnNotEquals { left_idx, right_idx, }), BinaryOperator::Greater => Ok(FilterPredicate::ColumnGreaterThan { left_idx, right_idx, }), BinaryOperator::GreaterEquals => { Ok(FilterPredicate::ColumnGreaterThanOrEqual { left_idx, right_idx, }) } BinaryOperator::Less => Ok(FilterPredicate::ColumnLessThan { left_idx, right_idx, }), BinaryOperator::LessEquals => Ok(FilterPredicate::ColumnLessThanOrEqual { left_idx, right_idx, }), BinaryOperator::And | BinaryOperator::Or => { // Handle logical operators recursively let left_pred = Self::compile_filter_predicate(left, schema)?; let right_pred = Self::compile_filter_predicate(right, schema)?; match op { BinaryOperator::And => Ok(FilterPredicate::And( Box::new(left_pred), Box::new(right_pred), )), BinaryOperator::Or => Ok(FilterPredicate::Or( Box::new(left_pred), Box::new(right_pred), )), _ => unreachable!(), } } _ => Err(LimboError::ParseError(format!( "Unsupported operator in filter: {op:?}" ))), } } else if let (LogicalExpr::Column(col), LogicalExpr::Literal(val)) = (left.as_ref(), right.as_ref()) { // Column-to-literal comparisons let column_idx = schema .columns .iter() .position(|c| c.name == col.name) .ok_or_else(|| { crate::LimboError::ParseError(format!( "Column '{}' not found in schema for filter", col.name )) })?; match op { BinaryOperator::Equals => Ok(FilterPredicate::Equals { column_idx, value: val.clone(), }), BinaryOperator::NotEquals => Ok(FilterPredicate::NotEquals { column_idx, value: val.clone(), }), BinaryOperator::Greater => Ok(FilterPredicate::GreaterThan { column_idx, value: val.clone(), }), BinaryOperator::GreaterEquals => Ok(FilterPredicate::GreaterThanOrEqual { column_idx, value: val.clone(), }), BinaryOperator::Less => Ok(FilterPredicate::LessThan { column_idx, value: val.clone(), }), BinaryOperator::LessEquals => Ok(FilterPredicate::LessThanOrEqual { column_idx, value: val.clone(), }), BinaryOperator::And => { // Handle AND of two predicates let left_pred = Self::compile_filter_predicate(left, schema)?; let right_pred = Self::compile_filter_predicate(right, schema)?; Ok(FilterPredicate::And( Box::new(left_pred), Box::new(right_pred), )) } BinaryOperator::Or => { // Handle OR of two predicates let left_pred = Self::compile_filter_predicate(left, schema)?; let right_pred = Self::compile_filter_predicate(right, schema)?; Ok(FilterPredicate::Or( Box::new(left_pred), Box::new(right_pred), )) } _ => Err(LimboError::ParseError(format!( "Unsupported operator in filter: {op:?}" ))), } } else if matches!(op, BinaryOperator::And | BinaryOperator::Or) { // Handle logical operators let left_pred = Self::compile_filter_predicate(left, schema)?; let right_pred = Self::compile_filter_predicate(right, schema)?; match op { BinaryOperator::And => Ok(FilterPredicate::And( Box::new(left_pred), Box::new(right_pred), )), BinaryOperator::Or => Ok(FilterPredicate::Or( Box::new(left_pred), Box::new(right_pred), )), _ => unreachable!(), } } else { Err(LimboError::ParseError( "Filter predicate must be column op value or column op column".to_string(), )) } } LogicalExpr::IsNull { expr, negated } => { // Extract column index from the inner expression if let LogicalExpr::Column(col) = expr.as_ref() { let column_idx = schema .columns .iter() .position(|c| c.name == col.name) .ok_or_else(|| { LimboError::ParseError(format!( "Column '{}' not found in schema for IS NULL filter", col.name )) })?; if *negated { Ok(FilterPredicate::IsNotNull { column_idx }) } else { Ok(FilterPredicate::IsNull { column_idx }) } } else { Err(LimboError::ParseError( "IS NULL/IS NOT NULL expects a column reference".to_string(), )) } } _ => Err(LimboError::ParseError(format!( "Unsupported filter expression: {expr:?}" ))), } } } #[cfg(test)] mod tests { use super::*; use crate::incremental::dbsp::Delta; use crate::incremental::operator::{FilterOperator, FilterPredicate}; use crate::schema::{BTreeTable, ColDef, Column as SchemaColumn, Schema, Type}; use crate::storage::pager::CreateBTreeFlags; use crate::sync::Arc; use crate::translate::logical::{ColumnInfo, LogicalPlanBuilder, LogicalSchema}; use crate::util::IOExt; use crate::{Database, MemoryIO, Pager, IO}; use rustc_hash::FxHashSet as HashSet; use turso_parser::ast; use turso_parser::parser::Parser; // Macro to create a test schema with a users table macro_rules! test_schema { () => {{ let mut schema = Schema::new(); let users_table = BTreeTable { name: "users".to_string(), root_page: 2, primary_key_columns: vec![("id".to_string(), turso_parser::ast::SortOrder::Asc)], columns: vec![ SchemaColumn::new( Some("id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, ..Default::default() }, ), SchemaColumn::new_default_text( Some("name".to_string()), "TEXT".to_string(), None, ), SchemaColumn::new_default_integer( Some("age".to_string()), "INTEGER".to_string(), None, ), ], has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }; schema .add_btree_table(Arc::new(users_table)) .expect("Test setup: failed to add users table"); // Add products table for join tests let products_table = BTreeTable { name: "products".to_string(), root_page: 3, primary_key_columns: vec![( "product_id".to_string(), turso_parser::ast::SortOrder::Asc, )], columns: vec![ SchemaColumn::new( Some("product_id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, ..Default::default() }, ), SchemaColumn::new_default_text( Some("product_name".to_string()), "TEXT".to_string(), None, ), SchemaColumn::new_default_integer( Some("price".to_string()), "INTEGER".to_string(), None, ), ], has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }; schema .add_btree_table(Arc::new(products_table)) .expect("Test setup: failed to add products table"); // Add orders table for join tests let orders_table = BTreeTable { name: "orders".to_string(), root_page: 4, primary_key_columns: vec![( "order_id".to_string(), turso_parser::ast::SortOrder::Asc, )], columns: vec![ SchemaColumn::new( Some("order_id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, ..Default::default() }, ), SchemaColumn::new_default_integer( Some("user_id".to_string()), "INTEGER".to_string(), None, ), SchemaColumn::new_default_integer( Some("product_id".to_string()), "INTEGER".to_string(), None, ), SchemaColumn::new_default_integer( Some("quantity".to_string()), "INTEGER".to_string(), None, ), ], has_rowid: true, has_autoincrement: false, is_strict: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }; schema .add_btree_table(Arc::new(orders_table)) .expect("Test setup: failed to add orders table"); // Add customers table with id and name for testing column ambiguity let customers_table = BTreeTable { name: "customers".to_string(), root_page: 6, primary_key_columns: vec![("id".to_string(), turso_parser::ast::SortOrder::Asc)], columns: vec![ SchemaColumn::new( Some("id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, ..Default::default() }, ), SchemaColumn::new_default_text( Some("name".to_string()), "TEXT".to_string(), None, ), ], has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }; schema .add_btree_table(Arc::new(customers_table)) .expect("Test setup: failed to add customers table"); // Add purchases table (junction table for three-way join) let purchases_table = BTreeTable { name: "purchases".to_string(), root_page: 7, primary_key_columns: vec![("id".to_string(), turso_parser::ast::SortOrder::Asc)], columns: vec![ SchemaColumn::new( Some("id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, ..Default::default() }, ), SchemaColumn::new_default_integer( Some("customer_id".to_string()), "INTEGER".to_string(), None, ), SchemaColumn::new_default_integer( Some("vendor_id".to_string()), "INTEGER".to_string(), None, ), SchemaColumn::new_default_integer( Some("quantity".to_string()), "INTEGER".to_string(), None, ), ], has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }; schema .add_btree_table(Arc::new(purchases_table)) .expect("Test setup: failed to add purchases table"); // Add vendors table with id, name, and price (ambiguous columns with customers) let vendors_table = BTreeTable { name: "vendors".to_string(), root_page: 8, primary_key_columns: vec![("id".to_string(), turso_parser::ast::SortOrder::Asc)], columns: vec![ SchemaColumn::new( Some("id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, ..Default::default() }, ), SchemaColumn::new_default_text( Some("name".to_string()), "TEXT".to_string(), None, ), SchemaColumn::new_default_integer( Some("price".to_string()), "INTEGER".to_string(), None, ), ], has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }; schema .add_btree_table(Arc::new(vendors_table)) .expect("Test setup: failed to add vendors table"); let sales_table = BTreeTable { name: "sales".to_string(), root_page: 2, primary_key_columns: vec![], columns: vec![ SchemaColumn::new_default_integer( Some("product_id".to_string()), "INTEGER".to_string(), None, ), SchemaColumn::new_default_integer( Some("amount".to_string()), "INTEGER".to_string(), None, ), ], has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }; schema .add_btree_table(Arc::new(sales_table)) .expect("Test setup: failed to add sales table"); schema }}; } fn setup_btree_for_circuit() -> (Arc, i64, i64, i64) { let io: Arc = Arc::new(MemoryIO::new()); let db = Database::open_file(io.clone(), ":memory:").unwrap(); let conn = db.connect().unwrap(); let pager = conn.pager.load().clone(); let _ = pager.io.block(|| pager.allocate_page1()).unwrap(); let main_root_page = pager .io .block(|| pager.btree_create(&CreateBTreeFlags::new_table())) .unwrap() as i64; let dbsp_state_page = pager .io .block(|| pager.btree_create(&CreateBTreeFlags::new_table())) .unwrap() as i64; let dbsp_state_index_page = pager .io .block(|| pager.btree_create(&CreateBTreeFlags::new_index())) .unwrap() as i64; ( pager, main_root_page, dbsp_state_page, dbsp_state_index_page, ) } // Macro to compile SQL to DBSP circuit macro_rules! compile_sql { ($sql:expr) => {{ let (pager, main_root_page, dbsp_state_page, dbsp_state_index_page) = setup_btree_for_circuit(); let schema = test_schema!(); let mut parser = Parser::new($sql.as_bytes()); let cmd = parser .next() .unwrap() // This returns Option> .unwrap(); // This unwraps the Result match cmd { ast::Cmd::Stmt(stmt) => { let mut builder = LogicalPlanBuilder::new(&schema); let logical_plan = builder.build_statement(&stmt).unwrap(); ( DbspCompiler::new(main_root_page, dbsp_state_page, dbsp_state_index_page) .compile(&logical_plan) .unwrap(), pager, ) } _ => panic!("Only SQL statements are supported"), } }}; } // Macro to assert circuit structure macro_rules! assert_circuit { ($circuit:expr, depth: $depth:expr, root: $root_type:ident) => { assert_eq!($circuit.nodes.len(), $depth); let node = get_node_at_level(&$circuit, 0); assert!(matches!(node.operator, DbspOperator::$root_type { .. })); }; } // Macro to assert operator properties macro_rules! assert_operator { ($circuit:expr, $level:expr, Input { name: $name:expr }) => {{ let node = get_node_at_level(&$circuit, $level); match &node.operator { DbspOperator::Input { name, .. } => assert_eq!(name, $name), _ => panic!("Expected Input operator at level {}", $level), } }}; ($circuit:expr, $level:expr, Filter) => {{ let node = get_node_at_level(&$circuit, $level); assert!(matches!(node.operator, DbspOperator::Filter { .. })); }}; ($circuit:expr, $level:expr, Projection { columns: [$($col:expr),*] }) => {{ let node = get_node_at_level(&$circuit, $level); match &node.operator { DbspOperator::Projection { exprs, .. } => { let expected_cols = vec![$($col),*]; let actual_cols: Vec = exprs.iter().map(|e| { match e { DbspExpr::Column(name) => name.clone(), _ => "expr".to_string(), } }).collect(); assert_eq!(actual_cols, expected_cols); } _ => panic!("Expected Projection operator at level {}", $level), } }}; } // Macro to assert filter predicate macro_rules! assert_filter_predicate { ($circuit:expr, $level:expr, $col:literal > $val:literal) => {{ let node = get_node_at_level(&$circuit, $level); match &node.operator { DbspOperator::Filter { predicate } => match predicate { DbspExpr::BinaryExpr { left, op, right } => { assert!(matches!(op, ast::Operator::Greater)); assert!(matches!(&**left, DbspExpr::Column(name) if name == $col)); assert!(matches!(&**right, DbspExpr::Literal(Value::Numeric(Numeric::Integer($val))))); } _ => panic!("Expected binary expression in filter"), }, _ => panic!("Expected Filter operator at level {}", $level), } }}; ($circuit:expr, $level:expr, $col:literal < $val:literal) => {{ let node = get_node_at_level(&$circuit, $level); match &node.operator { DbspOperator::Filter { predicate } => match predicate { DbspExpr::BinaryExpr { left, op, right } => { assert!(matches!(op, ast::Operator::Less)); assert!(matches!(&**left, DbspExpr::Column(name) if name == $col)); assert!(matches!(&**right, DbspExpr::Literal(Value::Numeric(Numeric::Integer($val))))); } _ => panic!("Expected binary expression in filter"), }, _ => panic!("Expected Filter operator at level {}", $level), } }}; ($circuit:expr, $level:expr, $col:literal = $val:literal) => {{ let node = get_node_at_level(&$circuit, $level); match &node.operator { DbspOperator::Filter { predicate } => match predicate { DbspExpr::BinaryExpr { left, op, right } => { assert!(matches!(op, ast::Operator::Equals)); assert!(matches!(&**left, DbspExpr::Column(name) if name == $col)); assert!(matches!(&**right, DbspExpr::Literal(Value::Numeric(Numeric::Integer($val))))); } _ => panic!("Expected binary expression in filter"), }, _ => panic!("Expected Filter operator at level {}", $level), } }}; } // Helper to get node at specific level from root fn get_node_at_level(circuit: &DbspCircuit, level: usize) -> &DbspNode { let mut current_id = circuit.root.expect("Circuit has no root"); for _ in 0..level { let node = circuit.nodes.get(¤t_id).expect("Node not found"); if node.inputs.is_empty() { panic!("No more levels available, requested level {level}"); } current_id = node.inputs[0]; } circuit.nodes.get(¤t_id).expect("Node not found") } // Helper function for tests to execute circuit and extract the Delta result #[cfg(test)] fn test_execute( circuit: &mut DbspCircuit, inputs: HashMap, pager: Arc, ) -> Result { let mut execute_state = ExecuteState::Init { input_data: DeltaSet::from_map(inputs), }; match circuit.execute(pager, &mut execute_state)? { IOResult::Done(delta) => Ok(delta), IOResult::IO(_) => panic!("Unexpected I/O in test"), } } // Helper to get the committed BTree state from main_data_root // This reads the actual persisted data from the BTree #[cfg(test)] fn get_current_state(pager: Arc, circuit: &DbspCircuit) -> Result { use crate::storage::btree::CursorTrait; let mut delta = Delta::new(); let main_data_root = circuit.main_data_root; let num_columns = circuit.output_schema.columns.len() + 1; // Create a cursor to read the btree let mut btree_cursor = BTreeCursor::new_table(pager.clone(), main_data_root, num_columns); // Rewind to the beginning pager.io.block(|| btree_cursor.rewind())?; // Read all rows from the BTree loop { // Check if cursor is empty (no more rows) if btree_cursor.is_empty() { break; } // Get the rowid let rowid = pager.io.block(|| btree_cursor.rowid()).unwrap().unwrap(); // Get the record at this position let record = loop { match btree_cursor.record().unwrap() { IOResult::Done(r) => break r, IOResult::IO(io) => io.wait(&*pager.io).unwrap(), } } .unwrap() .to_owned(); let num_data_columns = record.column_count() - 1; let mut values = Vec::with_capacity(num_data_columns); let mut values_iter = record.iter()?; for _ in 0..num_data_columns { let value = values_iter.next().expect("we already checked bounds")?; values.push(value.to_owned()); } delta.insert(rowid, values); pager.io.block(|| btree_cursor.next()).unwrap(); } Ok(delta) } #[test] fn test_simple_projection() { let (circuit, _) = compile_sql!("SELECT name FROM users"); // Circuit has 2 nodes with Projection at root assert_circuit!(circuit, depth: 2, root: Projection); // Verify operators at each level assert_operator!(circuit, 0, Projection { columns: ["name"] }); assert_operator!(circuit, 1, Input { name: "users" }); } #[test] fn test_filter_with_projection() { let (circuit, _) = compile_sql!("SELECT name FROM users WHERE age > 18"); // Circuit has 3 nodes with Projection at root assert_circuit!(circuit, depth: 3, root: Projection); // Verify operators at each level assert_operator!(circuit, 0, Projection { columns: ["name"] }); assert_operator!(circuit, 1, Filter); assert_filter_predicate!(circuit, 1, "age" > 18); assert_operator!(circuit, 2, Input { name: "users" }); } #[test] fn test_select_star() { let (mut circuit, pager) = compile_sql!("SELECT * FROM users"); // Create test data let mut input_delta = Delta::new(); input_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); input_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(17), ], ); // Create input map let mut inputs = HashMap::default(); inputs.insert("users".to_string(), input_delta); let result = test_execute(&mut circuit, inputs.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(inputs.clone(), pager.clone())) .unwrap(); // Should have all rows with all columns assert_eq!(result.changes.len(), 2); // Verify both rows are present with all columns for (row, weight) in &result.changes { assert_eq!(*weight, 1); assert_eq!(row.values.len(), 3); // id, name, age } } #[test] fn test_execute_filter() { let (mut circuit, pager) = compile_sql!("SELECT * FROM users WHERE age > 18"); // Create test data let mut input_delta = Delta::new(); input_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); input_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(17), ], ); input_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(30), ], ); // Create input map let mut inputs = HashMap::default(); inputs.insert("users".to_string(), input_delta); let result = test_execute(&mut circuit, inputs.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(inputs.clone(), pager.clone())) .unwrap(); // Should only have Alice and Charlie (age > 18) assert_eq!( result.changes.len(), 2, "Expected 2 rows after filtering, got {}", result.changes.len() ); // Check that the filtered rows are correct let names: Vec = result .changes .iter() .filter_map(|(row, weight)| { if *weight > 0 && row.values.len() > 1 { if let Value::Text(name) = &row.values[1] { Some(name.to_string()) } else { None } } else { None } }) .collect(); assert!( names.contains(&"Alice".to_string()), "Alice should be in results" ); assert!( names.contains(&"Charlie".to_string()), "Charlie should be in results" ); assert!( !names.contains(&"Bob".to_string()), "Bob should not be in results" ); } #[test] fn test_simple_column_projection() { let (mut circuit, pager) = compile_sql!("SELECT name, age FROM users"); // Create test data let mut input_delta = Delta::new(); input_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); input_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(17), ], ); // Create input map let mut inputs = HashMap::default(); inputs.insert("users".to_string(), input_delta); let result = test_execute(&mut circuit, inputs.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(inputs.clone(), pager.clone())) .unwrap(); // Should have all rows but only 2 columns (name, age) assert_eq!(result.changes.len(), 2); for (row, _) in &result.changes { assert_eq!(row.values.len(), 2); // Only name and age // First value should be name (Text) assert!(matches!(&row.values[0], Value::Text(_))); // Second value should be age (Integer) assert!(matches!( &row.values[1], Value::Numeric(Numeric::Integer(_)) )); } } #[test] fn test_simple_aggregation() { // Test COUNT(*) with GROUP BY let (mut circuit, pager) = compile_sql!("SELECT age, COUNT(*) FROM users GROUP BY age"); // Create test data let mut input_delta = Delta::new(); input_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); input_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(25), ], ); input_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(30), ], ); // Create input map let mut inputs = HashMap::default(); inputs.insert("users".to_string(), input_delta); let result = test_execute(&mut circuit, inputs.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(inputs.clone(), pager.clone())) .unwrap(); // Should have 2 groups: age 25 with count 2, age 30 with count 1 assert_eq!(result.changes.len(), 2); // Check the results let mut found_25 = false; let mut found_30 = false; for (row, weight) in &result.changes { assert_eq!(*weight, 1); assert_eq!(row.values.len(), 2); // age, count if let ( Value::Numeric(Numeric::Integer(age)), Value::Numeric(Numeric::Integer(count)), ) = (&row.values[0], &row.values[1]) { if *age == 25 { assert_eq!(*count, 2, "Age 25 should have count 2"); found_25 = true; } else if *age == 30 { assert_eq!(*count, 1, "Age 30 should have count 1"); found_30 = true; } } } assert!(found_25, "Should have group for age 25"); assert!(found_30, "Should have group for age 30"); } #[test] fn test_sum_aggregation() { // Test SUM with GROUP BY let (mut circuit, pager) = compile_sql!("SELECT name, SUM(age) FROM users GROUP BY name"); // Create test data - some names appear multiple times let mut input_delta = Delta::new(); input_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); input_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Alice".into()), Value::from_i64(30), ], ); input_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Bob".into()), Value::from_i64(20), ], ); // Create input map let mut inputs = HashMap::default(); inputs.insert("users".to_string(), input_delta); let result = test_execute(&mut circuit, inputs.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(inputs.clone(), pager.clone())) .unwrap(); // Should have 2 groups: Alice with sum 55, Bob with sum 20 assert_eq!(result.changes.len(), 2); for (row, weight) in &result.changes { assert_eq!(*weight, 1); assert_eq!(row.values.len(), 2); // name, sum if let (Value::Text(name), Value::Numeric(Numeric::Float(sum))) = (&row.values[0], &row.values[1]) { if name.as_str() == "Alice" { assert_eq!(*sum, 55.0, "Alice should have sum 55"); } else if name.as_str() == "Bob" { assert_eq!(*sum, 20.0, "Bob should have sum 20"); } } } } #[test] fn test_aggregation_without_group_by() { // Test aggregation without GROUP BY - should produce a single row let (mut circuit, pager) = compile_sql!("SELECT COUNT(*), SUM(age), AVG(age) FROM users"); // Create test data let mut input_delta = Delta::new(); input_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); input_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); input_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(20), ], ); // Create input map let mut inputs = HashMap::default(); inputs.insert("users".to_string(), input_delta); let result = test_execute(&mut circuit, inputs.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(inputs.clone(), pager.clone())) .unwrap(); // Should have exactly 1 row with all aggregates assert_eq!( result.changes.len(), 1, "Should have exactly one result row" ); let (row, weight) = result.changes.first().unwrap(); assert_eq!(*weight, 1); assert_eq!(row.values.len(), 3); // count, sum, avg // Check aggregate results // COUNT should be Integer if let Value::Numeric(Numeric::Integer(count)) = &row.values[0] { assert_eq!(*count, 3, "COUNT(*) should be 3"); } else { panic!("COUNT should be Integer, got {:?}", row.values[0]); } // SUM can be Integer (if whole number) or Float match &row.values[1] { Value::Numeric(Numeric::Integer(sum)) => assert_eq!(*sum, 75, "SUM(age) should be 75"), Value::Numeric(Numeric::Float(sum)) => { assert_eq!(f64::from(*sum), 75.0, "SUM(age) should be 75.0") } other => panic!("SUM should be Integer or Float, got {other:?}"), } // AVG should be Float if let Value::Numeric(Numeric::Float(avg)) = &row.values[2] { assert_eq!(f64::from(*avg), 25.0, "AVG(age) should be 25.0"); } else { panic!("AVG should be Float, got {:?}", row.values[2]); } } #[test] fn test_expression_projection_execution() { // Test that complex expressions work through VDBE compilation let (mut circuit, pager) = compile_sql!("SELECT hex(id) FROM users"); // Create test data let mut input_delta = Delta::new(); input_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); input_delta.insert( 2, vec![ Value::from_i64(255), Value::Text("Bob".into()), Value::from_i64(17), ], ); // Create input map let mut inputs = HashMap::default(); inputs.insert("users".to_string(), input_delta); let result = test_execute(&mut circuit, inputs.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(inputs.clone(), pager.clone())) .unwrap(); assert_eq!(result.changes.len(), 2); let hex_values: HashMap = result .changes .iter() .map(|(row, _)| { let rowid = row.rowid; if let Value::Text(text) = &row.values[0] { (rowid, text.to_string()) } else { panic!("Expected Text value for hex() result"); } }) .collect(); assert_eq!( hex_values.get(&1).unwrap(), "31", "hex(1) should return '31' (hex of ASCII '1')" ); assert_eq!( hex_values.get(&2).unwrap(), "323535", "hex(255) should return '323535' (hex of ASCII '2', '5', '5')" ); } // TODO: This test currently fails on incremental updates. // The initial execution works correctly, but incremental updates produce // incorrect results (3 changes instead of 2, with wrong values). // This tests that the aggregate operator correctly handles incremental // updates when it's sandwiched between projection operators. #[test] fn test_projection_aggregation_projection_pattern() { // Test pattern: projection -> aggregation -> projection // Query: SELECT HEX(SUM(age + 2)) FROM users let (mut circuit, pager) = compile_sql!("SELECT HEX(SUM(age + 2)) FROM users"); // Initial input data let mut input_delta = Delta::new(); input_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".to_string().into()), Value::from_i64(25), ], ); input_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".to_string().into()), Value::from_i64(30), ], ); input_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".to_string().into()), Value::from_i64(35), ], ); let mut input_data = HashMap::default(); input_data.insert("users".to_string(), input_delta); let result = test_execute(&mut circuit, input_data.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(input_data.clone(), pager.clone())) .unwrap(); // Expected: SUM(age + 2) = (25+2) + (30+2) + (35+2) = 27 + 32 + 37 = 96 // HEX(96) should be the hex representation of the string "96" = "3936" assert_eq!(result.changes.len(), 1); let (row, _weight) = &result.changes[0]; assert_eq!(row.values.len(), 1); // The hex function converts the number to string first, then to hex // SUM now returns Float, so 96.0 as string is "96.0", which in hex is "39362E30" // (hex of ASCII '9', '6', '.', '0') assert_eq!( row.values[0], Value::Text("39362E30".to_string().into()), "HEX(SUM(age + 2)) should return '39362E30' for sum of 96.0" ); // Test incremental update: add a new user let mut input_delta = Delta::new(); input_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("David".to_string().into()), Value::from_i64(40), ], ); let mut input_data = HashMap::default(); input_data.insert("users".to_string(), input_delta); let result = test_execute(&mut circuit, input_data, pager).unwrap(); // Expected: new SUM(age + 2) = 96.0 + (40+2) = 138.0 // HEX(138.0) = hex of "138.0" = "3133382E30" assert_eq!(result.changes.len(), 2); // First change: remove old aggregate (96.0) let (row, weight) = &result.changes[0]; assert_eq!(*weight, -1); assert_eq!(row.values[0], Value::Text("39362E30".to_string().into())); // Second change: add new aggregate (138.0) let (row, weight) = &result.changes[1]; assert_eq!(*weight, 1); assert_eq!( row.values[0], Value::Text("3133382E30".to_string().into()), "HEX(SUM(age + 2)) should return '3133382E30' for sum of 138.0" ); } #[test] fn test_nested_projection_with_groupby() { // Test pattern: projection -> aggregation with GROUP BY -> projection // Query: SELECT name, HEX(SUM(age * 2)) FROM users GROUP BY name let (mut circuit, pager) = compile_sql!("SELECT name, HEX(SUM(age * 2)) FROM users GROUP BY name"); // Initial input data let mut input_delta = Delta::new(); input_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".to_string().into()), Value::from_i64(25), ], ); input_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".to_string().into()), Value::from_i64(30), ], ); input_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Alice".to_string().into()), Value::from_i64(35), ], ); let mut input_data = HashMap::default(); input_data.insert("users".to_string(), input_delta); let result = test_execute(&mut circuit, input_data.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(input_data.clone(), pager.clone())) .unwrap(); // Expected results: // Alice: SUM(25*2 + 35*2) = 50 + 70 = 120.0, HEX("120.0") = "3132302E30" // Bob: SUM(30*2) = 60.0, HEX("60.0") = "36302E30" assert_eq!(result.changes.len(), 2); let results: HashMap = result .changes .iter() .map(|(row, _weight)| { let name = match &row.values[0] { Value::Text(t) => t.to_string(), _ => panic!("Expected text for name"), }; let hex_sum = match &row.values[1] { Value::Text(t) => t.to_string(), _ => panic!("Expected text for hex value"), }; (name, hex_sum) }) .collect(); assert_eq!( results.get("Alice").unwrap(), "3132302E30", "Alice's HEX(SUM(age * 2)) should be '3132302E30' (120.0)" ); assert_eq!( results.get("Bob").unwrap(), "36302E30", "Bob's HEX(SUM(age * 2)) should be '36302E30' (60.0)" ); } #[test] fn test_transaction_context() { // Test that uncommitted changes are visible within a transaction // but don't affect the operator's internal state let (mut circuit, pager) = compile_sql!("SELECT * FROM users WHERE age > 18"); // Initialize with some data let mut init_data = HashMap::default(); let mut delta = Delta::new(); delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(17), ], ); init_data.insert("users".to_string(), delta); let _ = test_execute(&mut circuit, init_data.clone(), pager.clone()).unwrap(); let state = pager .io .block(|| circuit.commit(init_data.clone(), pager.clone())) .unwrap(); // Verify initial delta : only Alice (age > 18) assert_eq!(state.changes.len(), 1); assert_eq!(state.changes[0].0.values[1], Value::Text("Alice".into())); // Create uncommitted changes that would be visible in a transaction let mut uncommitted = HashMap::default(); let mut uncommitted_delta = Delta::new(); // Add Charlie (age 30) - should be visible in transaction uncommitted_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(30), ], ); // Add David (age 15) - should NOT be visible (filtered out) uncommitted_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("David".into()), Value::from_i64(15), ], ); uncommitted.insert("users".to_string(), uncommitted_delta); // Execute with uncommitted data - this simulates processing the uncommitted changes // through the circuit to see what would be visible let tx_result = test_execute(&mut circuit, uncommitted.clone(), pager.clone()).unwrap(); // The result should show Charlie being added (passes filter, age > 18) // David is filtered out (age 15 < 18) assert_eq!(tx_result.changes.len(), 1, "Should see Charlie added"); assert_eq!( tx_result.changes[0].0.values[1], Value::Text("Charlie".into()) ); // Now actually commit Charlie (without uncommitted context) let mut commit_data = HashMap::default(); let mut commit_delta = Delta::new(); commit_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(30), ], ); commit_data.insert("users".to_string(), commit_delta); let commit_result = test_execute(&mut circuit, commit_data.clone(), pager.clone()).unwrap(); // The commit result should show Charlie being added assert_eq!(commit_result.changes.len(), 1, "Should see Charlie added"); assert_eq!( commit_result.changes[0].0.values[1], Value::Text("Charlie".into()) ); // Commit the change to make it permanent pager .io .block(|| circuit.commit(commit_data.clone(), pager.clone())) .unwrap(); // Now if we execute again with no changes, we should see no delta let empty_result = test_execute(&mut circuit, HashMap::default(), pager).unwrap(); assert_eq!(empty_result.changes.len(), 0, "No changes when no new data"); } #[test] fn test_uncommitted_delete() { // Test that uncommitted deletes are handled correctly without affecting operator state let (mut circuit, pager) = compile_sql!("SELECT * FROM users WHERE age > 18"); // Initialize with some data let mut init_data = HashMap::default(); let mut delta = Delta::new(); delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(20), ], ); init_data.insert("users".to_string(), delta); let _ = test_execute(&mut circuit, init_data.clone(), pager.clone()).unwrap(); let state = pager .io .block(|| circuit.commit(init_data.clone(), pager.clone())) .unwrap(); // Verify initial delta: Alice, Bob, Charlie (all age > 18) assert_eq!(state.changes.len(), 3); // Create uncommitted delete for Bob let mut uncommitted = HashMap::default(); let mut uncommitted_delta = Delta::new(); uncommitted_delta.delete( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); uncommitted.insert("users".to_string(), uncommitted_delta); // Execute with uncommitted delete let tx_result = test_execute(&mut circuit, uncommitted.clone(), pager.clone()).unwrap(); // Result should show the deleted row that passed the filter assert_eq!( tx_result.changes.len(), 1, "Should see the uncommitted delete" ); // Verify operator's internal state is unchanged (still has all 3 users) let state_after = get_current_state(pager.clone(), &circuit).unwrap(); assert_eq!( state_after.changes.len(), 3, "Internal state should still have all 3 users" ); // Now actually commit the delete let mut commit_data = HashMap::default(); let mut commit_delta = Delta::new(); commit_delta.delete( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); commit_data.insert("users".to_string(), commit_delta); let commit_result = test_execute(&mut circuit, commit_data.clone(), pager.clone()).unwrap(); // Actually commit the delete to update operator state pager .io .block(|| circuit.commit(commit_data.clone(), pager.clone())) .unwrap(); // The commit result should show Bob being deleted assert_eq!(commit_result.changes.len(), 1, "Should see Bob deleted"); assert_eq!( commit_result.changes[0].1, -1, "Delete should have weight -1" ); assert_eq!( commit_result.changes[0].0.values[1], Value::Text("Bob".into()) ); // After commit, internal state should have only Alice and Charlie let final_state = get_current_state(pager, &circuit).unwrap(); assert_eq!( final_state.changes.len(), 2, "After commit, should have Alice and Charlie" ); let names: Vec = final_state .changes .iter() .map(|(row, _)| { if let Value::Text(name) = &row.values[1] { name.to_string() } else { panic!("Expected text value"); } }) .collect(); assert!(names.contains(&"Alice".to_string())); assert!(names.contains(&"Charlie".to_string())); assert!(!names.contains(&"Bob".to_string())); } #[test] fn test_uncommitted_update() { // Test that uncommitted updates (delete + insert) are handled correctly let (mut circuit, pager) = compile_sql!("SELECT * FROM users WHERE age > 18"); // Initialize with some data let mut init_data = HashMap::default(); let mut delta = Delta::new(); delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(17), ], ); // Bob is 17, filtered out init_data.insert("users".to_string(), delta); let _ = test_execute(&mut circuit, init_data.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(init_data.clone(), pager.clone())) .unwrap(); // Create uncommitted update: Bob turns 19 (update from 17 to 19) // This is modeled as delete + insert let mut uncommitted = HashMap::default(); let mut uncommitted_delta = Delta::new(); uncommitted_delta.delete( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(17), ], ); uncommitted_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(19), ], ); uncommitted.insert("users".to_string(), uncommitted_delta); // Execute with uncommitted update let tx_result = test_execute(&mut circuit, uncommitted.clone(), pager.clone()).unwrap(); // Bob should now appear in the result (age 19 > 18) // Consolidate to see the final state let mut final_result = tx_result; final_result.consolidate(); assert_eq!(final_result.changes.len(), 1, "Bob should now be in view"); assert_eq!( final_result.changes[0].0.values[1], Value::Text("Bob".into()) ); assert_eq!(final_result.changes[0].0.values[2], Value::from_i64(19)); // Now actually commit the update let mut commit_data = HashMap::default(); let mut commit_delta = Delta::new(); commit_delta.delete( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(17), ], ); commit_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(19), ], ); commit_data.insert("users".to_string(), commit_delta); // Commit the update pager .io .block(|| circuit.commit(commit_data.clone(), pager.clone())) .unwrap(); // After committing, Bob should be in the view's state let state = get_current_state(pager, &circuit).unwrap(); let mut consolidated_state = state; consolidated_state.consolidate(); // Should have both Alice and Bob now assert_eq!( consolidated_state.changes.len(), 2, "Should have Alice and Bob" ); let names: Vec = consolidated_state .changes .iter() .map(|(row, _)| { if let Value::Text(name) = &row.values[1] { name.as_str().to_string() } else { panic!("Expected text value"); } }) .collect(); assert!(names.contains(&"Alice".to_string())); assert!(names.contains(&"Bob".to_string())); } #[test] fn test_uncommitted_filtered_delete() { // Test deleting a row that doesn't pass the filter let (mut circuit, pager) = compile_sql!("SELECT * FROM users WHERE age > 18"); // Initialize with mixed data let mut init_data = HashMap::default(); let mut delta = Delta::new(); delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(15), ], ); // Bob doesn't pass filter init_data.insert("users".to_string(), delta); let _ = test_execute(&mut circuit, init_data.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(init_data.clone(), pager.clone())) .unwrap(); // Create uncommitted delete for Bob (who isn't in the view because age=15) let mut uncommitted = HashMap::default(); let mut uncommitted_delta = Delta::new(); uncommitted_delta.delete( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(15), ], ); uncommitted.insert("users".to_string(), uncommitted_delta); // Execute with uncommitted delete - should produce no output changes let tx_result = test_execute(&mut circuit, uncommitted, pager.clone()).unwrap(); // Bob wasn't in the view, so deleting him produces no output assert_eq!( tx_result.changes.len(), 0, "Deleting filtered row produces no changes" ); // The view state should still only have Alice let state = get_current_state(pager, &circuit).unwrap(); assert_eq!(state.changes.len(), 1, "View still has only Alice"); assert_eq!(state.changes[0].0.values[1], Value::Text("Alice".into())); } #[test] fn test_uncommitted_mixed_operations() { // Test multiple uncommitted operations together let (mut circuit, pager) = compile_sql!("SELECT * FROM users WHERE age > 18"); // Initialize with some data let mut init_data = HashMap::default(); let mut delta = Delta::new(); delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); init_data.insert("users".to_string(), delta); let _ = test_execute(&mut circuit, init_data.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(init_data.clone(), pager.clone())) .unwrap(); // Verify initial state let state = get_current_state(pager.clone(), &circuit).unwrap(); assert_eq!(state.changes.len(), 2); // Create uncommitted changes: // - Delete Alice // - Update Bob's age to 35 // - Insert Charlie (age 40) // - Insert David (age 16, filtered out) let mut uncommitted = HashMap::default(); let mut uncommitted_delta = Delta::new(); // Delete Alice uncommitted_delta.delete( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); // Update Bob (delete + insert) uncommitted_delta.delete( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); uncommitted_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(35), ], ); // Insert Charlie uncommitted_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(40), ], ); // Insert David (will be filtered) uncommitted_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("David".into()), Value::from_i64(16), ], ); uncommitted.insert("users".to_string(), uncommitted_delta); // Execute with uncommitted changes let tx_result = test_execute(&mut circuit, uncommitted.clone(), pager.clone()).unwrap(); // Result should show all changes: delete Alice, update Bob, insert Charlie and David assert_eq!( tx_result.changes.len(), 4, "Should see all uncommitted mixed operations" ); // Verify operator's internal state is unchanged let state_after = get_current_state(pager.clone(), &circuit).unwrap(); assert_eq!(state_after.changes.len(), 2, "Still has Alice and Bob"); // Commit all changes let mut commit_data = HashMap::default(); let mut commit_delta = Delta::new(); commit_delta.delete( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); commit_delta.delete( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); commit_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(35), ], ); commit_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(40), ], ); commit_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("David".into()), Value::from_i64(16), ], ); commit_data.insert("users".to_string(), commit_delta); let commit_result = test_execute(&mut circuit, commit_data.clone(), pager.clone()).unwrap(); // Should see: Alice deleted, Bob deleted, Bob inserted, Charlie inserted // (David filtered out) assert_eq!(commit_result.changes.len(), 4, "Should see 4 changes"); // Actually commit the changes to update operator state pager .io .block(|| circuit.commit(commit_data.clone(), pager.clone())) .unwrap(); // After all commits, execute with no changes should return empty delta let empty_result = test_execute(&mut circuit, HashMap::default(), pager).unwrap(); assert_eq!(empty_result.changes.len(), 0, "No changes when no new data"); } #[test] fn test_uncommitted_aggregation() { // Test that aggregations work correctly with uncommitted changes // This tests the specific scenario where a transaction adds new data // and we need to see correct aggregation results within the transaction // Create a sales table schema for testing let _ = test_schema!(); let (mut circuit, pager) = compile_sql!("SELECT product_id, SUM(amount) as total, COUNT(*) as cnt FROM sales GROUP BY product_id"); // Initialize with base data: (1, 100), (1, 200), (2, 150), (2, 250) let mut init_data = HashMap::default(); let mut delta = Delta::new(); delta.insert(1, vec![Value::from_i64(1), Value::from_i64(100)]); delta.insert(2, vec![Value::from_i64(1), Value::from_i64(200)]); delta.insert(3, vec![Value::from_i64(2), Value::from_i64(150)]); delta.insert(4, vec![Value::from_i64(2), Value::from_i64(250)]); init_data.insert("sales".to_string(), delta); let _ = test_execute(&mut circuit, init_data.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(init_data.clone(), pager.clone())) .unwrap(); // Verify initial state: product 1 total=300, product 2 total=400 let state = get_current_state(pager.clone(), &circuit).unwrap(); assert_eq!(state.changes.len(), 2, "Should have 2 product groups"); // Build a map of product_id -> (total, count) let initial_results: HashMap = state .changes .iter() .map(|(row, _)| { // SUM might return Integer or Float, COUNT returns Integer let product_id = match &row.values[0] { Value::Numeric(Numeric::Integer(id)) => *id, _ => panic!("Product ID should be Integer, got {:?}", row.values[0]), }; let total = match &row.values[1] { Value::Numeric(Numeric::Integer(t)) => *t, Value::Numeric(Numeric::Float(t)) => f64::from(*t) as i64, _ => panic!("Total should be numeric, got {:?}", row.values[1]), }; let count = match &row.values[2] { Value::Numeric(Numeric::Integer(c)) => *c, _ => panic!("Count should be Integer, got {:?}", row.values[2]), }; (product_id, (total, count)) }) .collect(); assert_eq!( initial_results.get(&1).unwrap(), &(300, 2), "Product 1 should have total=300, count=2" ); assert_eq!( initial_results.get(&2).unwrap(), &(400, 2), "Product 2 should have total=400, count=2" ); // Create uncommitted changes: INSERT (1, 50), (3, 300) let mut uncommitted = HashMap::default(); let mut uncommitted_delta = Delta::new(); uncommitted_delta.insert(5, vec![Value::from_i64(1), Value::from_i64(50)]); // Add to product 1 uncommitted_delta.insert(6, vec![Value::from_i64(3), Value::from_i64(300)]); // New product 3 uncommitted.insert("sales".to_string(), uncommitted_delta); // Execute with uncommitted data - simulating a read within transaction let tx_result = test_execute(&mut circuit, uncommitted.clone(), pager.clone()).unwrap(); // Result should show the aggregate changes from uncommitted data // Product 1: retraction of (300, 2) and insertion of (350, 3) // Product 3: insertion of (300, 1) - new product assert_eq!( tx_result.changes.len(), 3, "Should see aggregate changes from uncommitted data" ); // IMPORTANT: Verify operator's internal state is unchanged let state_after = get_current_state(pager.clone(), &circuit).unwrap(); assert_eq!( state_after.changes.len(), 2, "Internal state should still have 2 groups" ); // Verify the internal state still has original values let state_results: HashMap = state_after .changes .iter() .map(|(row, _)| { let product_id = match &row.values[0] { Value::Numeric(Numeric::Integer(id)) => *id, _ => panic!("Product ID should be Integer"), }; let total = match &row.values[1] { Value::Numeric(Numeric::Integer(t)) => *t, Value::Numeric(Numeric::Float(t)) => f64::from(*t) as i64, _ => panic!("Total should be numeric"), }; let count = match &row.values[2] { Value::Numeric(Numeric::Integer(c)) => *c, _ => panic!("Count should be Integer"), }; (product_id, (total, count)) }) .collect(); assert_eq!( state_results.get(&1).unwrap(), &(300, 2), "Product 1 unchanged" ); assert_eq!( state_results.get(&2).unwrap(), &(400, 2), "Product 2 unchanged" ); assert!( !state_results.contains_key(&3), "Product 3 should not be in committed state" ); // Now actually commit the changes let mut commit_data = HashMap::default(); let mut commit_delta = Delta::new(); commit_delta.insert(5, vec![Value::from_i64(1), Value::from_i64(50)]); commit_delta.insert(6, vec![Value::from_i64(3), Value::from_i64(300)]); commit_data.insert("sales".to_string(), commit_delta); let commit_result = test_execute(&mut circuit, commit_data.clone(), pager.clone()).unwrap(); // Should see changes for product 1 (updated) and product 3 (new) assert_eq!( commit_result.changes.len(), 3, "Should see 3 changes (delete old product 1, insert new product 1, insert product 3)" ); // Actually commit the changes to update operator state pager .io .block(|| circuit.commit(commit_data.clone(), pager.clone())) .unwrap(); // After commit, verify final state let final_state = get_current_state(pager, &circuit).unwrap(); assert_eq!( final_state.changes.len(), 3, "Should have 3 product groups after commit" ); let final_results: HashMap = final_state .changes .iter() .map(|(row, _)| { let product_id = match &row.values[0] { Value::Numeric(Numeric::Integer(id)) => *id, _ => panic!("Product ID should be Integer"), }; let total = match &row.values[1] { Value::Numeric(Numeric::Integer(t)) => *t, Value::Numeric(Numeric::Float(t)) => f64::from(*t) as i64, _ => panic!("Total should be numeric"), }; let count = match &row.values[2] { Value::Numeric(Numeric::Integer(c)) => *c, _ => panic!("Count should be Integer"), }; (product_id, (total, count)) }) .collect(); assert_eq!( final_results.get(&1).unwrap(), &(350, 3), "Product 1 should have total=350, count=3" ); assert_eq!( final_results.get(&2).unwrap(), &(400, 2), "Product 2 should have total=400, count=2" ); assert_eq!( final_results.get(&3).unwrap(), &(300, 1), "Product 3 should have total=300, count=1" ); } #[test] fn test_uncommitted_data_visible_in_transaction() { // Test that uncommitted INSERTs are visible within the same transaction // This simulates: BEGIN; INSERT ...; SELECT * FROM view; COMMIT; let (mut circuit, pager) = compile_sql!("SELECT * FROM users WHERE age > 18"); // Initialize with some data - need to match the schema (id, name, age) let mut init_data = HashMap::default(); let mut delta = Delta::new(); delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); init_data.insert("users".to_string(), delta); let _ = test_execute(&mut circuit, init_data.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(init_data.clone(), pager.clone())) .unwrap(); // Verify initial state let state = get_current_state(pager.clone(), &circuit).unwrap(); assert_eq!( state.len(), 2, "Should have 2 users initially (both pass age > 18 filter)" ); // Simulate a transaction: INSERT new users that pass the filter - match schema (id, name, age) let mut uncommitted = HashMap::default(); let mut tx_delta = Delta::new(); tx_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(35), ], ); tx_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("David".into()), Value::from_i64(20), ], ); uncommitted.insert("users".to_string(), tx_delta); // Execute with uncommitted data - this should return the uncommitted changes // that passed through the filter (age > 18) let tx_result = test_execute(&mut circuit, uncommitted.clone(), pager.clone()).unwrap(); // IMPORTANT: tx_result should contain the filtered uncommitted changes! // Both Charlie (35) and David (20) should pass the age > 18 filter assert_eq!( tx_result.len(), 2, "Should see 2 uncommitted rows that pass filter" ); // Verify the uncommitted results contain the expected rows let has_charlie = tx_result.changes.iter().any(|(row, _)| row.rowid == 3); assert!( has_charlie, "Should find Charlie (rowid=3) in uncommitted results" ); let has_david = tx_result.changes.iter().any(|(row, _)| row.rowid == 4); assert!( has_david, "Should find David (rowid=4) in uncommitted results" ); // CRITICAL: Verify the operator state wasn't modified by uncommitted execution let state_after_uncommitted = get_current_state(pager, &circuit).unwrap(); assert_eq!( state_after_uncommitted.len(), 2, "State should STILL be 2 after uncommitted execution - only Alice and Bob" ); // The state should not contain Charlie or David let has_charlie_in_state = state_after_uncommitted .changes .iter() .any(|(row, _)| row.rowid == 3); let has_david_in_state = state_after_uncommitted .changes .iter() .any(|(row, _)| row.rowid == 4); assert!( !has_charlie_in_state, "Charlie should NOT be in operator state (uncommitted)" ); assert!( !has_david_in_state, "David should NOT be in operator state (uncommitted)" ); } #[test] fn test_uncommitted_aggregation_with_rollback() { // Test that rollback properly discards uncommitted aggregation changes // Similar to test_uncommitted_aggregation but explicitly tests rollback semantics // Create a simple aggregation circuit let (mut circuit, pager) = compile_sql!("SELECT age, COUNT(*) as cnt FROM users GROUP BY age"); // Initialize with some data let mut init_data = HashMap::default(); let mut delta = Delta::new(); delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(25), ], ); delta.insert( 4, vec![ Value::from_i64(4), Value::Text("David".into()), Value::from_i64(30), ], ); init_data.insert("users".to_string(), delta); let _ = test_execute(&mut circuit, init_data.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(init_data.clone(), pager.clone())) .unwrap(); // Verify initial state: age 25 count=2, age 30 count=2 let state = get_current_state(pager.clone(), &circuit).unwrap(); assert_eq!(state.changes.len(), 2); let initial_counts: HashMap = state .changes .iter() .map(|(row, _)| { if let ( Value::Numeric(Numeric::Integer(age)), Value::Numeric(Numeric::Integer(count)), ) = (&row.values[0], &row.values[1]) { (*age, *count) } else { panic!("Unexpected value types"); } }) .collect(); assert_eq!(initial_counts.get(&25).unwrap(), &2); assert_eq!(initial_counts.get(&30).unwrap(), &2); // Create uncommitted changes that would affect aggregations let mut uncommitted = HashMap::default(); let mut uncommitted_delta = Delta::new(); // Add more people aged 25 uncommitted_delta.insert( 5, vec![ Value::from_i64(5), Value::Text("Eve".into()), Value::from_i64(25), ], ); uncommitted_delta.insert( 6, vec![ Value::from_i64(6), Value::Text("Frank".into()), Value::from_i64(25), ], ); // Add person aged 35 (new group) uncommitted_delta.insert( 7, vec![ Value::from_i64(7), Value::Text("Grace".into()), Value::from_i64(35), ], ); // Delete Bob (age 30) uncommitted_delta.delete( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); uncommitted.insert("users".to_string(), uncommitted_delta); // Execute with uncommitted changes let tx_result = test_execute(&mut circuit, uncommitted.clone(), pager.clone()).unwrap(); // Should see the aggregate changes from uncommitted data // Age 25: retraction of count 1 and insertion of count 2 // Age 30: insertion of count 1 (Bob is new for age 30) assert!( !tx_result.changes.is_empty(), "Should see aggregate changes from uncommitted data" ); // Verify internal state is unchanged (simulating rollback by not committing) let state_after_rollback = get_current_state(pager, &circuit).unwrap(); assert_eq!( state_after_rollback.changes.len(), 2, "Should still have 2 age groups" ); let rollback_counts: HashMap = state_after_rollback .changes .iter() .map(|(row, _)| { if let ( Value::Numeric(Numeric::Integer(age)), Value::Numeric(Numeric::Integer(count)), ) = (&row.values[0], &row.values[1]) { (*age, *count) } else { panic!("Unexpected value types"); } }) .collect(); // Verify counts are unchanged after rollback assert_eq!( rollback_counts.get(&25).unwrap(), &2, "Age 25 count unchanged" ); assert_eq!( rollback_counts.get(&30).unwrap(), &2, "Age 30 count unchanged" ); assert!( !rollback_counts.contains_key(&35), "Age 35 should not exist" ); } #[test] fn test_circuit_rowid_update_consolidation() { let (pager, p1, p2, p3) = setup_btree_for_circuit(); // Test that circuit properly consolidates state when rowid changes let mut circuit = DbspCircuit::new(p1, p2, p3); // Create a simple filter node let schema = Arc::new(LogicalSchema::new(vec![ ColumnInfo { name: "id".to_string(), ty: Type::Integer, database: None, table: None, table_alias: None, }, ColumnInfo { name: "value".to_string(), ty: Type::Integer, database: None, table: None, table_alias: None, }, ])); // First create an input node with InputOperator let input_id = circuit.add_node( DbspOperator::Input { name: "test".to_string(), schema: schema.clone(), }, vec![], Box::new(InputOperator::new("test".to_string())), ); let filter_op = FilterOperator::new(FilterPredicate::GreaterThan { column_idx: 1, // "value" is at index 1 value: Value::from_i64(10), }); // Create the filter predicate using DbspExpr let predicate = DbspExpr::BinaryExpr { left: Box::new(DbspExpr::Column("value".to_string())), op: ast::Operator::Greater, right: Box::new(DbspExpr::Literal(Value::from_i64(10))), }; let filter_id = circuit.add_node( DbspOperator::Filter { predicate }, vec![input_id], // Filter takes input from the input node Box::new(filter_op), ); circuit.set_root(filter_id, schema); // Initialize with a row let mut init_data = HashMap::default(); let mut delta = Delta::new(); delta.insert(5, vec![Value::from_i64(5), Value::from_i64(20)]); init_data.insert("test".to_string(), delta); let _ = test_execute(&mut circuit, init_data.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(init_data.clone(), pager.clone())) .unwrap(); // Verify initial state let state = get_current_state(pager.clone(), &circuit).unwrap(); assert_eq!(state.changes.len(), 1); assert_eq!(state.changes[0].0.rowid, 5); // Now update the rowid from 5 to 3 let mut update_data = HashMap::default(); let mut update_delta = Delta::new(); update_delta.delete(5, vec![Value::from_i64(5), Value::from_i64(20)]); update_delta.insert(3, vec![Value::from_i64(3), Value::from_i64(20)]); update_data.insert("test".to_string(), update_delta); test_execute(&mut circuit, update_data.clone(), pager.clone()).unwrap(); // Commit the changes to update operator state pager .io .block(|| circuit.commit(update_data.clone(), pager.clone())) .unwrap(); // The circuit should consolidate the state properly let final_state = get_current_state(pager, &circuit).unwrap(); assert_eq!( final_state.changes.len(), 1, "Circuit should consolidate to single row" ); assert_eq!(final_state.changes[0].0.rowid, 3); assert_eq!( final_state.changes[0].0.values, vec![Value::from_i64(3), Value::from_i64(20)] ); assert_eq!(final_state.changes[0].1, 1); } #[test] fn test_circuit_respects_multiplicities() { let (mut circuit, pager) = compile_sql!("SELECT * from users"); // Insert same row twice (multiplicity 2) let mut delta = Delta::new(); delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); let mut inputs = HashMap::default(); inputs.insert("users".to_string(), delta); test_execute(&mut circuit, inputs.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(inputs.clone(), pager.clone())) .unwrap(); // Delete once (should leave multiplicity 1) let mut delete_one = Delta::new(); delete_one.delete( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); let mut inputs = HashMap::default(); inputs.insert("users".to_string(), delete_one); test_execute(&mut circuit, inputs.clone(), pager.clone()).unwrap(); pager .io .block(|| circuit.commit(inputs.clone(), pager.clone())) .unwrap(); // With proper DBSP: row still exists (weight 2 - 1 = 1) let state = get_current_state(pager, &circuit).unwrap(); let mut consolidated = state; consolidated.consolidate(); assert_eq!( consolidated.len(), 1, "Row should still exist with multiplicity 1" ); } #[test] fn test_join_with_aggregation() { // Test join followed by aggregation - verifying actual output let (mut circuit, pager) = compile_sql!( "SELECT u.name, SUM(o.quantity) as total_quantity FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name" ); // Create test data for users let mut users_delta = Delta::new(); users_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(30), ], ); users_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(25), ], ); // Create test data for orders (order_id, user_id, product_id, quantity) let mut orders_delta = Delta::new(); orders_delta.insert( 1, vec![ Value::from_i64(1), Value::from_i64(1), Value::from_i64(101), Value::from_i64(5), ], ); // Alice: 5 orders_delta.insert( 2, vec![ Value::from_i64(2), Value::from_i64(1), Value::from_i64(102), Value::from_i64(3), ], ); // Alice: 3 orders_delta.insert( 3, vec![ Value::from_i64(3), Value::from_i64(2), Value::from_i64(101), Value::from_i64(7), ], ); // Bob: 7 orders_delta.insert( 4, vec![ Value::from_i64(4), Value::from_i64(1), Value::from_i64(103), Value::from_i64(2), ], ); // Alice: 2 let inputs = HashMap::from_iter([ ("users".to_string(), users_delta), ("orders".to_string(), orders_delta), ]); let result = test_execute(&mut circuit, inputs, pager).unwrap(); // Should have 2 results: Alice with total 10, Bob with total 7 assert_eq!( result.len(), 2, "Should have aggregated results for Alice and Bob" ); // Check the results let mut results_map: HashMap = HashMap::default(); for (row, weight) in result.changes { assert_eq!(weight, 1); assert_eq!(row.values.len(), 2); // name and total_quantity if let (Value::Text(name), Value::Numeric(Numeric::Float(total))) = (&row.values[0], &row.values[1]) { results_map.insert(name.to_string(), f64::from(*total)); } else { panic!("Unexpected value types in result"); } } assert_eq!( results_map.get("Alice"), Some(&10.0), "Alice should have total quantity 10" ); assert_eq!( results_map.get("Bob"), Some(&7.0), "Bob should have total quantity 7" ); } #[test] fn test_join_aggregate_with_filter() { // Test complex query with join, filter, and aggregation - verifying output let (mut circuit, pager) = compile_sql!( "SELECT u.name, SUM(o.quantity) as total FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 18 GROUP BY u.name" ); // Create test data for users let mut users_delta = Delta::new(); users_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(30), ], ); // age > 18 users_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(17), ], ); // age <= 18 users_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(25), ], ); // age > 18 // Create test data for orders (order_id, user_id, product_id, quantity) let mut orders_delta = Delta::new(); orders_delta.insert( 1, vec![ Value::from_i64(1), Value::from_i64(1), Value::from_i64(101), Value::from_i64(5), ], ); // Alice: 5 orders_delta.insert( 2, vec![ Value::from_i64(2), Value::from_i64(2), Value::from_i64(102), Value::from_i64(10), ], ); // Bob: 10 (should be filtered) orders_delta.insert( 3, vec![ Value::from_i64(3), Value::from_i64(3), Value::from_i64(101), Value::from_i64(7), ], ); // Charlie: 7 orders_delta.insert( 4, vec![ Value::from_i64(4), Value::from_i64(1), Value::from_i64(103), Value::from_i64(3), ], ); // Alice: 3 let inputs = HashMap::from_iter([ ("users".to_string(), users_delta), ("orders".to_string(), orders_delta), ]); let result = test_execute(&mut circuit, inputs, pager).unwrap(); // Should only have results for Alice and Charlie (Bob filtered out due to age <= 18) assert_eq!( result.len(), 2, "Should only have results for users with age > 18" ); // Check the results let mut results_map: HashMap = HashMap::default(); for (row, weight) in result.changes { assert_eq!(weight, 1); assert_eq!(row.values.len(), 2); // name and total if let (Value::Text(name), Value::Numeric(Numeric::Float(total))) = (&row.values[0], &row.values[1]) { results_map.insert(name.to_string(), f64::from(*total)); } } assert_eq!( results_map.get("Alice"), Some(&8.0), "Alice should have total 8" ); assert_eq!( results_map.get("Charlie"), Some(&7.0), "Charlie should have total 7" ); assert_eq!(results_map.get("Bob"), None, "Bob should be filtered out"); } #[test] fn test_three_way_join_execution() { // Test executing a 3-way join with aggregation let (mut circuit, pager) = compile_sql!( "SELECT u.name, p.product_name, SUM(o.quantity) as total FROM users u JOIN orders o ON u.id = o.user_id JOIN products p ON o.product_id = p.product_id GROUP BY u.name, p.product_name" ); // Create test data for users let mut users_delta = Delta::new(); users_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); users_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); // Create test data for products let mut products_delta = Delta::new(); products_delta.insert( 100, vec![ Value::from_i64(100), Value::Text("Widget".into()), Value::from_i64(50), ], ); products_delta.insert( 101, vec![ Value::from_i64(101), Value::Text("Gadget".into()), Value::from_i64(75), ], ); products_delta.insert( 102, vec![ Value::from_i64(102), Value::Text("Doohickey".into()), Value::from_i64(25), ], ); // Create test data for orders joining users and products let mut orders_delta = Delta::new(); // Alice orders 5 Widgets orders_delta.insert( 1, vec![ Value::from_i64(1), Value::from_i64(1), Value::from_i64(100), Value::from_i64(5), ], ); // Alice orders 3 Gadgets orders_delta.insert( 2, vec![ Value::from_i64(2), Value::from_i64(1), Value::from_i64(101), Value::from_i64(3), ], ); // Bob orders 7 Widgets orders_delta.insert( 3, vec![ Value::from_i64(3), Value::from_i64(2), Value::from_i64(100), Value::from_i64(7), ], ); // Bob orders 2 Doohickeys orders_delta.insert( 4, vec![ Value::from_i64(4), Value::from_i64(2), Value::from_i64(102), Value::from_i64(2), ], ); // Alice orders 4 more Widgets orders_delta.insert( 5, vec![ Value::from_i64(5), Value::from_i64(1), Value::from_i64(100), Value::from_i64(4), ], ); let mut inputs = HashMap::default(); inputs.insert("users".to_string(), users_delta); inputs.insert("products".to_string(), products_delta); inputs.insert("orders".to_string(), orders_delta); // Execute the 3-way join with aggregation let result = test_execute(&mut circuit, inputs.clone(), pager).unwrap(); // We should get aggregated results for each user-product combination // Expected results: // - Alice, Widget: 9 (5 + 4) // - Alice, Gadget: 3 // - Bob, Widget: 7 // - Bob, Doohickey: 2 assert_eq!(result.len(), 4, "Should have 4 aggregated results"); // Verify aggregation results let mut found_results = HashSet::default(); for (row, weight) in result.changes.iter() { assert_eq!(*weight, 1); // Row should have name, product_name, and sum columns assert_eq!(row.values.len(), 3); if let ( Value::Text(name), Value::Text(product), Value::Numeric(Numeric::Float(total)), ) = (&row.values[0], &row.values[1], &row.values[2]) { let key = format!("{}-{}", name.as_ref(), product.as_ref()); found_results.insert(key.clone()); match key.as_str() { "Alice-Widget" => { assert_eq!(*total, 9.0, "Alice should have ordered 9 Widgets total") } "Alice-Gadget" => { assert_eq!(*total, 3.0, "Alice should have ordered 3 Gadgets") } "Bob-Widget" => assert_eq!(*total, 7.0, "Bob should have ordered 7 Widgets"), "Bob-Doohickey" => { assert_eq!(*total, 2.0, "Bob should have ordered 2 Doohickeys") } _ => panic!("Unexpected result: {key}"), } } else { panic!("Unexpected value types in result"); } } // Ensure we found all expected combinations assert!(found_results.contains("Alice-Widget")); assert!(found_results.contains("Alice-Gadget")); assert!(found_results.contains("Bob-Widget")); assert!(found_results.contains("Bob-Doohickey")); } #[test] fn test_join_execution() { let (mut circuit, pager) = compile_sql!( "SELECT u.name, o.quantity FROM users u JOIN orders o ON u.id = o.user_id" ); // Create test data for users let mut users_delta = Delta::new(); users_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); users_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); // Create test data for orders let mut orders_delta = Delta::new(); orders_delta.insert( 1, vec![ Value::from_i64(1), Value::from_i64(1), Value::from_i64(100), Value::from_i64(5), ], ); orders_delta.insert( 2, vec![ Value::from_i64(2), Value::from_i64(1), Value::from_i64(101), Value::from_i64(3), ], ); orders_delta.insert( 3, vec![ Value::from_i64(3), Value::from_i64(2), Value::from_i64(102), Value::from_i64(7), ], ); let mut inputs = HashMap::default(); inputs.insert("users".to_string(), users_delta); inputs.insert("orders".to_string(), orders_delta); // Execute the join let result = test_execute(&mut circuit, inputs.clone(), pager).unwrap(); // We should get 3 results (2 orders for Alice, 1 for Bob) assert_eq!(result.len(), 3, "Should have 3 join results"); // Verify the join results contain the correct data let results: Vec<_> = result.changes.iter().collect(); // Check that we have the expected joined rows for (row, weight) in results { assert_eq!(*weight, 1); // All weights should be 1 for insertions // Row should have name and quantity columns assert_eq!(row.values.len(), 2); } } #[test] fn test_three_way_join_with_column_ambiguity() { // Test three-way join with aggregation where multiple tables have columns with the same name // Ensures that column references are correctly resolved to their respective tables // Tables: customers(id, name), purchases(id, customer_id, vendor_id, quantity), vendors(id, name, price) // Note: both customers and vendors have 'id' and 'name' columns which can cause ambiguity let sql = "SELECT c.name as customer_name, v.name as vendor_name, SUM(p.quantity) as total_quantity, SUM(p.quantity * v.price) as total_value FROM customers c JOIN purchases p ON c.id = p.customer_id JOIN vendors v ON p.vendor_id = v.id GROUP BY c.name, v.name"; let (mut circuit, pager) = compile_sql!(sql); // Create test data for customers (id, name) let mut customers_delta = Delta::new(); customers_delta.insert(1, vec![Value::from_i64(1), Value::Text("Alice".into())]); customers_delta.insert(2, vec![Value::from_i64(2), Value::Text("Bob".into())]); // Create test data for vendors (id, name, price) let mut vendors_delta = Delta::new(); vendors_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Widget Co".into()), Value::from_i64(10), ], ); vendors_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Gadget Inc".into()), Value::from_i64(20), ], ); // Create test data for purchases (id, customer_id, vendor_id, quantity) let mut purchases_delta = Delta::new(); // Alice purchases 5 units from Widget Co purchases_delta.insert( 1, vec![ Value::from_i64(1), Value::from_i64(1), // customer_id: Alice Value::from_i64(1), // vendor_id: Widget Co Value::from_i64(5), ], ); // Alice purchases 3 units from Gadget Inc purchases_delta.insert( 2, vec![ Value::from_i64(2), Value::from_i64(1), // customer_id: Alice Value::from_i64(2), // vendor_id: Gadget Inc Value::from_i64(3), ], ); // Bob purchases 2 units from Widget Co purchases_delta.insert( 3, vec![ Value::from_i64(3), Value::from_i64(2), // customer_id: Bob Value::from_i64(1), // vendor_id: Widget Co Value::from_i64(2), ], ); // Alice purchases 4 more units from Widget Co purchases_delta.insert( 4, vec![ Value::from_i64(4), Value::from_i64(1), // customer_id: Alice Value::from_i64(1), // vendor_id: Widget Co Value::from_i64(4), ], ); let inputs = HashMap::from_iter([ ("customers".to_string(), customers_delta), ("purchases".to_string(), purchases_delta), ("vendors".to_string(), vendors_delta), ]); let result = test_execute(&mut circuit, inputs, pager).unwrap(); // Expected results: // Alice|Gadget Inc|3|60 (3 units * 20 price = 60) // Alice|Widget Co|9|90 (9 units * 10 price = 90) // Bob|Widget Co|2|20 (2 units * 10 price = 20) assert_eq!(result.len(), 3, "Should have 3 aggregated results"); // Sort results for consistent testing let mut results: Vec<_> = result.changes.into_iter().collect(); results.sort_by(|a, b| { let a_cust = &a.0.values[0]; let a_vend = &a.0.values[1]; let b_cust = &b.0.values[0]; let b_vend = &b.0.values[1]; (a_cust, a_vend).cmp(&(b_cust, b_vend)) }); // Verify Alice's Gadget Inc purchases assert_eq!(results[0].0.values[0], Value::Text("Alice".into())); assert_eq!(results[0].0.values[1], Value::Text("Gadget Inc".into())); assert_eq!(results[0].0.values[2], Value::from_i64(3)); // total_quantity assert_eq!(results[0].0.values[3], Value::from_i64(60)); // total_value // Verify Alice's Widget Co purchases assert_eq!(results[1].0.values[0], Value::Text("Alice".into())); assert_eq!(results[1].0.values[1], Value::Text("Widget Co".into())); assert_eq!(results[1].0.values[2], Value::from_i64(9)); // total_quantity assert_eq!(results[1].0.values[3], Value::from_i64(90)); // total_value // Verify Bob's Widget Co purchases assert_eq!(results[2].0.values[0], Value::Text("Bob".into())); assert_eq!(results[2].0.values[1], Value::Text("Widget Co".into())); assert_eq!(results[2].0.values[2], Value::from_i64(2)); // total_quantity assert_eq!(results[2].0.values[3], Value::from_i64(20)); // total_value } #[test] fn test_projection_with_function_and_ambiguous_columns() { // Test projection with functions operating on potentially ambiguous columns // Uses HEX() function on sum of columns from different tables with same names // Tables: customers(id, name), vendors(id, name, price), purchases(id, customer_id, vendor_id, quantity) // This test ensures column references are correctly resolved to their respective tables let sql = "SELECT HEX(c.id + v.id) as hex_sum, UPPER(c.name) as customer_upper, LOWER(v.name) as vendor_lower, c.id * v.price as product_value FROM customers c JOIN vendors v ON c.id = v.id"; let (mut circuit, pager) = compile_sql!(sql); // Create test data for customers (id, name) let mut customers_delta = Delta::new(); customers_delta.insert(1, vec![Value::from_i64(1), Value::Text("Alice".into())]); customers_delta.insert(2, vec![Value::from_i64(2), Value::Text("Bob".into())]); customers_delta.insert(3, vec![Value::from_i64(3), Value::Text("Charlie".into())]); // Create test data for vendors (id, name, price) let mut vendors_delta = Delta::new(); vendors_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Widget Co".into()), Value::from_i64(10), ], ); vendors_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Gadget Inc".into()), Value::from_i64(20), ], ); vendors_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Tool Corp".into()), Value::from_i64(30), ], ); let inputs = HashMap::from_iter([ ("customers".to_string(), customers_delta), ("vendors".to_string(), vendors_delta), ]); let result = test_execute(&mut circuit, inputs, pager).unwrap(); // Expected results: // For customer 1 (Alice) + vendor 1: // - HEX(1 + 1) = HEX(2) = "32" // - UPPER("Alice") = "ALICE" // - LOWER("Widget Co") = "widget co" // - 1 * 10 = 10 assert_eq!(result.len(), 3, "Should have 3 join results"); let mut results = result.changes; results.sort_by_key(|(row, _)| { // Sort by the product_value column for predictable ordering match &row.values[3] { Value::Numeric(Numeric::Integer(n)) => *n, _ => 0, } }); // First result: Alice + Widget Co assert_eq!(results[0].0.values[0], Value::Text("32".into())); // HEX(2) assert_eq!(results[0].0.values[1], Value::Text("ALICE".into())); assert_eq!(results[0].0.values[2], Value::Text("widget co".into())); assert_eq!(results[0].0.values[3], Value::from_i64(10)); // 1 * 10 // Second result: Bob + Gadget Inc assert_eq!(results[1].0.values[0], Value::Text("34".into())); // HEX(4) assert_eq!(results[1].0.values[1], Value::Text("BOB".into())); assert_eq!(results[1].0.values[2], Value::Text("gadget inc".into())); assert_eq!(results[1].0.values[3], Value::from_i64(40)); // 2 * 20 // Third result: Charlie + Tool Corp assert_eq!(results[2].0.values[0], Value::Text("36".into())); // HEX(6) assert_eq!(results[2].0.values[1], Value::Text("CHARLIE".into())); assert_eq!(results[2].0.values[2], Value::Text("tool corp".into())); assert_eq!(results[2].0.values[3], Value::from_i64(90)); // 3 * 30 } #[test] fn test_projection_column_selection_after_join() { // Test selecting specific columns after a join, especially with overlapping column names // This ensures the projection correctly picks columns by their qualified references let sql = "SELECT c.id as customer_id, c.name as customer_name, o.order_id, o.quantity, p.product_name FROM users c JOIN orders o ON c.id = o.user_id JOIN products p ON o.product_id = p.product_id WHERE o.quantity > 2"; let (mut circuit, pager) = compile_sql!(sql); // Create test data for users (id, name, age) let mut users_delta = Delta::new(); users_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); users_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); // Create test data for orders (order_id, user_id, product_id, quantity) let mut orders_delta = Delta::new(); orders_delta.insert( 1, vec![ Value::from_i64(101), Value::from_i64(1), // Alice Value::from_i64(201), // Widget Value::from_i64(5), // quantity > 2 ], ); orders_delta.insert( 2, vec![ Value::from_i64(102), Value::from_i64(2), // Bob Value::from_i64(202), // Gadget Value::from_i64(1), // quantity <= 2, filtered out ], ); orders_delta.insert( 3, vec![ Value::from_i64(103), Value::from_i64(1), // Alice Value::from_i64(202), // Gadget Value::from_i64(3), // quantity > 2 ], ); // Create test data for products (product_id, product_name, price) let mut products_delta = Delta::new(); products_delta.insert( 201, vec![ Value::from_i64(201), Value::Text("Widget".into()), Value::from_i64(10), ], ); products_delta.insert( 202, vec![ Value::from_i64(202), Value::Text("Gadget".into()), Value::from_i64(20), ], ); let inputs = HashMap::from_iter([ ("users".to_string(), users_delta), ("orders".to_string(), orders_delta), ("products".to_string(), products_delta), ]); let result = test_execute(&mut circuit, inputs, pager).unwrap(); // Should have 2 results (orders with quantity > 2) assert_eq!(result.len(), 2, "Should have 2 results after filtering"); let mut results = result.changes; results.sort_by_key(|(row, _)| { match &row.values[2] { // Sort by order_id Value::Numeric(Numeric::Integer(n)) => *n, _ => 0, } }); // First result: Alice's order 101 for Widget assert_eq!(results[0].0.values[0], Value::from_i64(1)); // customer_id assert_eq!(results[0].0.values[1], Value::Text("Alice".into())); // customer_name assert_eq!(results[0].0.values[2], Value::from_i64(101)); // order_id assert_eq!(results[0].0.values[3], Value::from_i64(5)); // quantity assert_eq!(results[0].0.values[4], Value::Text("Widget".into())); // product_name // Second result: Alice's order 103 for Gadget assert_eq!(results[1].0.values[0], Value::from_i64(1)); // customer_id assert_eq!(results[1].0.values[1], Value::Text("Alice".into())); // customer_name assert_eq!(results[1].0.values[2], Value::from_i64(103)); // order_id assert_eq!(results[1].0.values[3], Value::from_i64(3)); // quantity assert_eq!(results[1].0.values[4], Value::Text("Gadget".into())); // product_name } #[test] fn test_projection_column_reordering_and_duplication() { // Test that projection can reorder columns and select the same column multiple times // This is important for views that need specific column arrangements let sql = "SELECT o.quantity, u.name, u.id, o.quantity * 2 as double_quantity, u.id as user_id_again FROM users u JOIN orders o ON u.id = o.user_id WHERE u.id = 1"; let (mut circuit, pager) = compile_sql!(sql); // Create test data for users let mut users_delta = Delta::new(); users_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); // Create test data for orders let mut orders_delta = Delta::new(); orders_delta.insert( 1, vec![ Value::from_i64(101), Value::from_i64(1), // user_id Value::from_i64(201), // product_id Value::from_i64(5), // quantity ], ); orders_delta.insert( 2, vec![ Value::from_i64(102), Value::from_i64(1), // user_id Value::from_i64(202), // product_id Value::from_i64(3), // quantity ], ); let inputs = HashMap::from_iter([ ("users".to_string(), users_delta), ("orders".to_string(), orders_delta), ]); let result = test_execute(&mut circuit, inputs, pager).unwrap(); assert_eq!(result.len(), 2, "Should have 2 results for user 1"); // Check that columns are in the right order and values are correct for (row, _) in &result.changes { // Column 0: o.quantity (5 or 3) assert!(matches!( row.values[0], Value::Numeric(Numeric::Integer(5)) | Value::Numeric(Numeric::Integer(3)) )); // Column 1: u.name assert_eq!(row.values[1], Value::Text("Alice".into())); // Column 2: u.id assert_eq!(row.values[2], Value::from_i64(1)); // Column 3: o.quantity * 2 (10 or 6) assert!(matches!( row.values[3], Value::Numeric(Numeric::Integer(10)) | Value::Numeric(Numeric::Integer(6)) )); // Column 4: u.id again assert_eq!(row.values[4], Value::from_i64(1)); } } #[test] fn test_join_with_aggregate_execution() { let (mut circuit, pager) = compile_sql!( "SELECT u.name, SUM(o.quantity) as total_quantity FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name" ); // Create test data for users let mut users_delta = Delta::new(); users_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(25), ], ); users_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(30), ], ); // Create test data for orders let mut orders_delta = Delta::new(); orders_delta.insert( 1, vec![ Value::from_i64(1), Value::from_i64(1), Value::from_i64(100), Value::from_i64(5), ], ); orders_delta.insert( 2, vec![ Value::from_i64(2), Value::from_i64(1), Value::from_i64(101), Value::from_i64(3), ], ); orders_delta.insert( 3, vec![ Value::from_i64(3), Value::from_i64(2), Value::from_i64(102), Value::from_i64(7), ], ); let mut inputs = HashMap::default(); inputs.insert("users".to_string(), users_delta); inputs.insert("orders".to_string(), orders_delta); // Execute the join with aggregation let result = test_execute(&mut circuit, inputs.clone(), pager).unwrap(); // We should get 2 aggregated results (one for Alice, one for Bob) assert_eq!(result.len(), 2, "Should have 2 aggregated results"); // Verify aggregation results for (row, weight) in result.changes.iter() { assert_eq!(*weight, 1); // Row should have name and sum columns assert_eq!(row.values.len(), 2); // Check the aggregated values if let Value::Text(name) = &row.values[0] { if name.as_ref() == "Alice" { // Alice should have total quantity of 8 (5 + 3) assert_eq!(row.values[1], Value::from_i64(8)); } else if name.as_ref() == "Bob" { // Bob should have total quantity of 7 assert_eq!(row.values[1], Value::from_i64(7)); } } } } #[test] fn test_filter_with_qualified_columns_in_join() { // Test that filters correctly handle qualified column names in joins // when multiple tables have columns with the SAME names. // Both users and customers tables have 'id' and 'name' columns which can be ambiguous. let (mut circuit, pager) = compile_sql!( "SELECT users.id, users.name, customers.id, customers.name FROM users JOIN customers ON users.id = customers.id WHERE users.id > 1 AND customers.id < 100" ); // Create test data let mut users_delta = Delta::new(); let mut customers_delta = Delta::new(); // Users data: (id, name, age) users_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(30), ], ); // id = 1 users_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(25), ], ); // id = 2 users_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(35), ], ); // id = 3 // Customers data: (id, name, email) customers_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Customer Alice".into()), Value::Text("alice@example.com".into()), ], ); // id = 1 customers_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Customer Bob".into()), Value::Text("bob@example.com".into()), ], ); // id = 2 customers_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Customer Charlie".into()), Value::Text("charlie@example.com".into()), ], ); // id = 3 let mut inputs = HashMap::default(); inputs.insert("users".to_string(), users_delta); inputs.insert("customers".to_string(), customers_delta); let result = test_execute(&mut circuit, inputs.clone(), pager).unwrap(); // Should get rows where users.id > 1 AND customers.id < 100 // - users.id=2 (> 1) AND customers.id=2 (< 100) ✓ // - users.id=3 (> 1) AND customers.id=3 (< 100) ✓ // Alice excluded: users.id=1 (NOT > 1) assert_eq!(result.len(), 2, "Should have 2 filtered results"); let (row, weight) = &result.changes[0]; assert_eq!(*weight, 1); assert_eq!(row.values.len(), 4, "Should have 4 columns"); // Verify the filter correctly used qualified columns for Bob assert_eq!(row.values[0], Value::from_i64(2), "users.id should be 2"); assert_eq!( row.values[1], Value::Text("Bob".into()), "users.name should be Bob" ); assert_eq!( row.values[2], Value::from_i64(2), "customers.id should be 2" ); assert_eq!( row.values[3], Value::Text("Customer Bob".into()), "customers.name should be Customer Bob" ); } #[test] fn test_expression_in_where_clause() { // Test expressions in WHERE clauses like (quantity * price) >= 400 let (mut circuit, pager) = compile_sql!("SELECT * FROM users WHERE (age * 2) > 30"); // Create test data let mut input_delta = Delta::new(); input_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(20), // age * 2 = 40 > 30, should pass ], ); input_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(10), // age * 2 = 20 <= 30, should be filtered out ], ); input_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(16), // age * 2 = 32 > 30, should pass ], ); // Create input map let mut inputs = HashMap::default(); inputs.insert("users".to_string(), input_delta); let result = test_execute(&mut circuit, inputs.clone(), pager).unwrap(); // Should only have Alice and Charlie (age * 2 > 30) assert_eq!( result.changes.len(), 2, "Should have 2 rows after filtering" ); // Check Alice let alice = result .changes .iter() .find(|(row, _)| row.values[0] == Value::from_i64(1)) .expect("Alice should be in result"); assert_eq!(alice.0.values[1], Value::Text("Alice".into())); assert_eq!(alice.0.values[2], Value::from_i64(20)); // Check Charlie let charlie = result .changes .iter() .find(|(row, _)| row.values[0] == Value::from_i64(3)) .expect("Charlie should be in result"); assert_eq!(charlie.0.values[1], Value::Text("Charlie".into())); assert_eq!(charlie.0.values[2], Value::from_i64(16)); // Bob should not be in result let bob = result .changes .iter() .find(|(row, _)| row.values[0] == Value::from_i64(2)); assert!(bob.is_none(), "Bob should be filtered out"); } fn make_column_info(name: &str, ty: Type, table: &str) -> ColumnInfo { ColumnInfo { name: name.to_string(), ty, database: None, table: Some(table.to_string()), table_alias: None, } } #[test] fn test_resolve_join_columns_normal_order() { // Normal case: left.id = right.id let left_schema = LogicalSchema::new(vec![ ColumnInfo { name: "id".to_string(), ty: Type::Integer, database: None, table: Some("left".to_string()), table_alias: None, }, ColumnInfo { name: "name".to_string(), ty: Type::Text, database: None, table: Some("left".to_string()), table_alias: None, }, ]); let right_schema = LogicalSchema::new(vec![ ColumnInfo { name: "id".to_string(), ty: Type::Integer, database: None, table: Some("right".to_string()), table_alias: None, }, ColumnInfo { name: "value".to_string(), ty: Type::Integer, database: None, table: Some("right".to_string()), table_alias: None, }, ]); let left_col = Column { name: "id".to_string(), table: Some("left".to_string()), }; let right_col = Column { name: "id".to_string(), table: Some("right".to_string()), }; let result = DbspCompiler::resolve_join_columns(&left_col, &right_col, &left_schema, &right_schema); assert!(result.is_ok()); let (actual_left, left_idx, actual_right, right_idx) = result.unwrap(); assert_eq!(actual_left.name, "id"); assert_eq!(actual_left.table, Some("left".to_string())); assert_eq!(left_idx, 0); assert_eq!(actual_right.name, "id"); assert_eq!(actual_right.table, Some("right".to_string())); assert_eq!(right_idx, 0); } #[test] fn test_resolve_join_columns_swapped_order() { // Swapped case: right.id = left.id let left_schema = LogicalSchema::new(vec![ make_column_info("id", Type::Integer, "left"), make_column_info("name", Type::Text, "left"), ]); let right_schema = LogicalSchema::new(vec![ make_column_info("id", Type::Integer, "right"), make_column_info("value", Type::Integer, "right"), ]); let right_col = Column { name: "id".to_string(), table: Some("right".to_string()), }; let left_col = Column { name: "id".to_string(), table: Some("left".to_string()), }; let result = DbspCompiler::resolve_join_columns(&right_col, &left_col, &left_schema, &right_schema); assert!(result.is_ok()); let (actual_left, left_idx, actual_right, right_idx) = result.unwrap(); assert_eq!(actual_left.name, "id"); assert_eq!(actual_left.table, Some("left".to_string())); assert_eq!(left_idx, 0); assert_eq!(actual_right.name, "id"); assert_eq!(actual_right.table, Some("right".to_string())); assert_eq!(right_idx, 0); } #[test] fn test_resolve_join_columns_one_ambiguous_one_not() { // Both tables have 'id', but only left has 'other_id' let left_schema = LogicalSchema::new(vec![ make_column_info("id", Type::Integer, "left"), make_column_info("other_id", Type::Integer, "left"), ]); let right_schema = LogicalSchema::new(vec![ make_column_info("id", Type::Integer, "right"), make_column_info("value", Type::Integer, "right"), ]); // Unqualified 'id' with qualified 'left.other_id' let id_col = Column { name: "id".to_string(), table: None, }; let other_id_col = Column { name: "other_id".to_string(), table: Some("left".to_string()), }; // id from right, other_id from left let result = DbspCompiler::resolve_join_columns(&id_col, &other_id_col, &left_schema, &right_schema); assert!(result.is_ok()); let (actual_left, left_idx, actual_right, right_idx) = result.unwrap(); assert_eq!(actual_left.name, "other_id"); assert_eq!(left_idx, 1); assert_eq!(actual_right.name, "id"); assert_eq!(right_idx, 0); } #[test] fn test_resolve_join_columns_mixed_qualified() { // One qualified, one unqualified, column exists on both sides let left_schema = LogicalSchema::new(vec![ make_column_info("id", Type::Integer, "left"), make_column_info("name", Type::Text, "left"), ]); let right_schema = LogicalSchema::new(vec![ make_column_info("id", Type::Integer, "right"), make_column_info("name", Type::Text, "right"), ]); // Qualified left.id with unqualified name let left_id = Column { name: "id".to_string(), table: Some("left".to_string()), }; let name_unqualified = Column { name: "name".to_string(), table: None, }; let result = DbspCompiler::resolve_join_columns( &left_id, &name_unqualified, &left_schema, &right_schema, ); // left.id is explicitly from left, so unqualified 'name' must be resolved from right assert!(result.is_ok()); let (actual_left, left_idx, actual_right, right_idx) = result.unwrap(); assert_eq!(actual_left.name, "id"); assert_eq!(left_idx, 0); assert_eq!(actual_right.name, "name"); assert_eq!(right_idx, 1); } #[test] fn test_resolve_join_columns_both_from_same_side() { // Both columns from left table - should fail let left_schema = LogicalSchema::new(vec![ make_column_info("id", Type::Integer, "left"), make_column_info("other_id", Type::Integer, "left"), ]); let right_schema = LogicalSchema::new(vec![make_column_info("value", Type::Integer, "right")]); let left_id = Column { name: "id".to_string(), table: Some("left".to_string()), }; let left_other_id = Column { name: "other_id".to_string(), table: Some("left".to_string()), }; let result = DbspCompiler::resolve_join_columns( &left_id, &left_other_id, &left_schema, &right_schema, ); assert!(result.is_err()); assert!(result .unwrap_err() .to_string() .contains("must come from different input tables")); } #[test] fn test_resolve_join_columns_nonexistent_column() { // Column doesn't exist in either table let left_schema = LogicalSchema::new(vec![make_column_info("id", Type::Integer, "left")]); let right_schema = LogicalSchema::new(vec![make_column_info("value", Type::Integer, "right")]); let id_col = Column { name: "id".to_string(), table: None, }; let nonexistent_col = Column { name: "does_not_exist".to_string(), table: None, }; let result = DbspCompiler::resolve_join_columns( &id_col, &nonexistent_col, &left_schema, &right_schema, ); assert!(result.is_err()); } #[test] fn test_resolve_join_columns_both_qualified() { // Both columns qualified - should work normally let left_schema = LogicalSchema::new(vec![ make_column_info("id", Type::Integer, "left"), make_column_info("name", Type::Text, "left"), ]); let right_schema = LogicalSchema::new(vec![ make_column_info("id", Type::Integer, "right"), make_column_info("value", Type::Integer, "right"), ]); let left_id = Column { name: "id".to_string(), table: Some("left".to_string()), }; let right_id = Column { name: "id".to_string(), table: Some("right".to_string()), }; let result = DbspCompiler::resolve_join_columns(&left_id, &right_id, &left_schema, &right_schema); assert!(result.is_ok()); let (actual_left, left_idx, actual_right, right_idx) = result.unwrap(); assert_eq!(actual_left.name, "id"); assert_eq!(left_idx, 0); assert_eq!(actual_right.name, "id"); assert_eq!(right_idx, 0); } #[test] fn test_resolve_join_columns_both_unqualified_same_name() { // Both columns unqualified with same name existing in both tables - should succeed // (first match wins based on order of checking) let left_schema = LogicalSchema::new(vec![make_column_info("id", Type::Integer, "left")]); let right_schema = LogicalSchema::new(vec![make_column_info("id", Type::Integer, "right")]); let id_col1 = Column { name: "id".to_string(), table: None, }; let id_col2 = Column { name: "id".to_string(), table: None, }; let result = DbspCompiler::resolve_join_columns(&id_col1, &id_col2, &left_schema, &right_schema); // Should succeed - unqualified 'id' matches in both schemas assert!(result.is_ok()); } #[test] fn test_resolve_join_columns_first_not_found() { // First column doesn't exist anywhere let left_schema = LogicalSchema::new(vec![make_column_info("id", Type::Integer, "left")]); let right_schema = LogicalSchema::new(vec![make_column_info("value", Type::Integer, "right")]); let missing_col = Column { name: "missing".to_string(), table: None, }; let value_col = Column { name: "value".to_string(), table: None, }; let result = DbspCompiler::resolve_join_columns( &missing_col, &value_col, &left_schema, &right_schema, ); assert!(result.is_err()); assert!(result .unwrap_err() .to_string() .contains("not found in either input")); } #[test] fn test_resolve_join_columns_both_unqualified_different_names() { // Both unqualified, each exists in only one table let left_schema = LogicalSchema::new(vec![make_column_info("left_id", Type::Integer, "left")]); let right_schema = LogicalSchema::new(vec![make_column_info("right_id", Type::Integer, "right")]); let left_col = Column { name: "left_id".to_string(), table: None, }; let right_col = Column { name: "right_id".to_string(), table: None, }; let result = DbspCompiler::resolve_join_columns(&left_col, &right_col, &left_schema, &right_schema); assert!(result.is_ok()); let (actual_left, left_idx, actual_right, right_idx) = result.unwrap(); assert_eq!(actual_left.name, "left_id"); assert_eq!(left_idx, 0); assert_eq!(actual_right.name, "right_id"); assert_eq!(right_idx, 0); } } ================================================ FILE: core/incremental/cursor.rs ================================================ use crate::numeric::Numeric; use crate::sync::Arc; use crate::sync::Mutex; use crate::{ incremental::{ compiler::{DeltaSet, ExecuteState}, dbsp::{Delta, HashableRow, RowKeyZSet}, view::{IncrementalView, ViewTransactionState}, }, return_if_io, storage::btree::CursorTrait, types::{IOResult, SeekKey, SeekOp, SeekResult, Value}, LimboError, Pager, Result, }; /// State machine for seek operations #[derive(Debug)] enum SeekState { /// Initial state before seeking Init, /// Actively seeking with btree and uncommitted iterators Seek { /// The row we are trying to find target: i64, }, /// Btree seek returned TryAdvance, now advancing with next()/prev() Advancing { /// The row we are trying to find target: i64, /// The seek operation (determines direction of advance) op: SeekOp, }, /// Seek completed successfully Done, } /// Cursor for reading materialized views that combines: /// 1. Persistent btree data (committed state) /// 2. Transaction-specific DBSP deltas (uncommitted changes) /// /// Works like a regular table cursor - reads from disk on-demand /// and overlays transaction changes as needed. pub struct MaterializedViewCursor { // Core components btree_cursor: Box, view: Arc>, pager: Arc, // Current changes that are uncommitted uncommitted: RowKeyZSet, // Reference to shared transaction state for this specific view - shared with Connection tx_state: Arc, // The transaction state always grows. It never gets reduced. That is in the very nature of // DBSP, because deletions are just appends with weight < 0. So we will use the length of the // state to check if we have to recompute the transaction state last_tx_state_len: usize, // Current row cache - only cache the current row we're looking at current_row: Option<(i64, Vec)>, // Execution state for circuit processing execute_state: ExecuteState, // State machine for seek operations seek_state: SeekState, } impl MaterializedViewCursor { pub fn new( btree_cursor: Box, view: Arc>, pager: Arc, tx_state: Arc, ) -> Result { Ok(Self { btree_cursor, view, pager, uncommitted: RowKeyZSet::new(), tx_state, last_tx_state_len: 0, current_row: None, execute_state: ExecuteState::Uninitialized, seek_state: SeekState::Init, }) } /// Compute transaction changes lazily on first access fn ensure_tx_changes_computed(&mut self) -> Result> { // Check if we've already processed the current state let current_len = self.tx_state.len(); if current_len == self.last_tx_state_len { return Ok(IOResult::Done(())); } // Get the view and the current transaction state let mut view_guard = self.view.lock(); let table_deltas = self.tx_state.get_table_deltas(); // Process the deltas through the circuit to get materialized changes let mut uncommitted = DeltaSet::new(); for (table_name, delta) in table_deltas { uncommitted.insert(table_name, delta); } let processed_delta = return_if_io!(view_guard.execute_with_uncommitted( uncommitted, self.pager.clone(), &mut self.execute_state )); self.uncommitted = RowKeyZSet::from_delta(&processed_delta); self.last_tx_state_len = current_len; Ok(IOResult::Done(())) } // Read the current btree entry as a vector (empty if no current position) fn read_btree_delta_entry(&mut self) -> Result>> { let btree_rowid = return_if_io!(self.btree_cursor.rowid()); let rowid = match btree_rowid { None => return Ok(IOResult::Done(Vec::new())), Some(rowid) => rowid, }; let btree_record = return_if_io!(self.btree_cursor.record()).ok_or_else(|| { crate::LimboError::InternalError( "Invalid data in materialized view: found a rowid, but not the row!".to_string(), ) })?; let mut btree_values = btree_record.get_values_owned()?; // The last column should be the weight let weight_value = btree_values.pop().ok_or_else(|| { crate::LimboError::InternalError( "Invalid data in materialized view: no weight column found".to_string(), ) })?; // Convert the Value to isize weight let weight = match weight_value { Value::Numeric(Numeric::Integer(w)) => w as isize, _ => { return Err(crate::LimboError::InternalError(format!( "Invalid data in materialized view: expected integer weight, found {weight_value:?}" ))) } }; if weight <= 0 { return Err(crate::LimboError::InternalError(format!( "Invalid data in materialized view: expected a positive weight, found {weight}" ))); } Ok(IOResult::Done(vec![( HashableRow::new(rowid, btree_values), weight, )])) } /// Process btree changes: merge with uncommitted, build zset, and determine result. /// Returns the next state action: either Done with a result, or updates seek_state for another iteration. fn process_btree_changes( &mut self, target: i64, target_rowid: i64, op: SeekOp, changes: Vec<(HashableRow, isize)>, ) -> Result> { let mut btree_entries = Delta { changes }; let changes = self.uncommitted.seek(target, op); let uncommitted_entries = Delta { changes }; btree_entries.merge(&uncommitted_entries); // if empty pre-zset, means nothing was found. Empty post-zset can mean that // we just canceled weights. if btree_entries.is_empty() { self.seek_state = SeekState::Done; return Ok(IOResult::Done(())); } let min_seen = btree_entries .changes .first() .expect("cannot be empty, we just tested for it") .0 .rowid; let max_seen = btree_entries .changes .last() .expect("cannot be empty, we just tested for it") .0 .rowid; let zset = RowKeyZSet::from_delta(&btree_entries); let ret = zset.seek(target_rowid, op); if !ret.is_empty() { let (row, _) = &ret[0]; self.current_row = Some((row.rowid, row.values.clone())); self.seek_state = SeekState::Done; return Ok(IOResult::Done(())); } let new_target = match op { SeekOp::GT => Some(max_seen), SeekOp::GE { eq_only: false } => Some(max_seen + 1), SeekOp::LT => Some(min_seen), SeekOp::LE { eq_only: false } => Some(min_seen - 1), SeekOp::LE { eq_only: true } | SeekOp::GE { eq_only: true } => None, }; if let Some(target) = new_target { self.seek_state = SeekState::Seek { target }; } else { self.seek_state = SeekState::Done; } Ok(IOResult::Done(())) } /// Internal seek implementation that doesn't check preconditions fn do_seek(&mut self, target_rowid: i64, op: SeekOp) -> Result> { loop { // Process state machine - need to handle mutable borrow carefully match &mut self.seek_state { SeekState::Init => { self.current_row = None; self.seek_state = SeekState::Seek { target: target_rowid, }; } SeekState::Seek { target } => { let target = *target; let btree_result = return_if_io!(self.btree_cursor.seek(SeekKey::TableRowId(target), op)); let changes = match btree_result { SeekResult::Found => return_if_io!(self.read_btree_delta_entry()), SeekResult::TryAdvance => { // Transition to Advancing state before calling next/prev. // This ensures that if next/prev returns IO, we resume in // Advancing state and don't redundantly call seek again. self.seek_state = SeekState::Advancing { target, op }; continue; } SeekResult::NotFound => Vec::new(), }; return_if_io!(self.process_btree_changes(target, target_rowid, op, changes)); // Check if we're done or need to continue seeking if matches!(self.seek_state, SeekState::Done) { let result = if self.current_row.is_some() { SeekResult::Found } else { SeekResult::NotFound }; return Ok(IOResult::Done(result)); } // Otherwise state is Seek with new target, loop continues } SeekState::Advancing { target, op } => { let target = *target; let op = *op; // Cursor is positioned at the leaf but current entry doesn't match. // Advance in the appropriate direction to find the next matching entry. match op { SeekOp::GT | SeekOp::GE { .. } => { return_if_io!(self.btree_cursor.next()) } SeekOp::LT | SeekOp::LE { .. } => { return_if_io!(self.btree_cursor.prev()) } }; // read_btree_delta_entry handles the case where cursor is at end let changes = return_if_io!(self.read_btree_delta_entry()); return_if_io!(self.process_btree_changes(target, target_rowid, op, changes)); // Check if we're done or need to continue seeking if matches!(self.seek_state, SeekState::Done) { let result = if self.current_row.is_some() { SeekResult::Found } else { SeekResult::NotFound }; return Ok(IOResult::Done(result)); } // Otherwise state is Seek with new target, loop continues } SeekState::Done => { // We always return before setting the state to done. Meaning if we got here, // this is a new seek. self.seek_state = SeekState::Init; } } } } pub fn seek(&mut self, key: SeekKey, op: SeekOp) -> Result> { // Ensure transaction changes are computed return_if_io!(self.ensure_tx_changes_computed()); let target_rowid = match &key { SeekKey::TableRowId(rowid) => *rowid, SeekKey::IndexKey(_) => { return Err(LimboError::ParseError( "Cannot search a materialized view with an index key".to_string(), )); } }; self.do_seek(target_rowid, op) } pub fn next(&mut self) -> Result> { // If there's a pending seek operation (due to IO), complete it first. // SeekState::Seek or SeekState::Advancing means IO was interrupted mid-seek and we need to resume. // SeekState::Init means cursor was never positioned - don't resume, fall through to check current_row. if matches!( self.seek_state, SeekState::Seek { .. } | SeekState::Advancing { .. } ) { // target is ignored when resuming let result = return_if_io!(self.do_seek(0, SeekOp::GT)); return Ok(IOResult::Done(result == SeekResult::Found)); } // If cursor is not positioned (no current_row), return false // This matches BTreeCursor behavior when valid_state == Invalid let Some((current_rowid, _)) = &self.current_row else { return Ok(IOResult::Done(false)); }; // Use GT to find the next row after current position let result = return_if_io!(self.do_seek(*current_rowid, SeekOp::GT)); Ok(IOResult::Done(result == SeekResult::Found)) } pub fn column(&mut self, col: usize) -> Result> { if let Some((_, ref values)) = self.current_row { Ok(IOResult::Done( values.get(col).cloned().unwrap_or(Value::Null), )) } else { Ok(IOResult::Done(Value::Null)) } } pub fn rowid(&self) -> Result>> { Ok(IOResult::Done(self.current_row.as_ref().map(|(id, _)| *id))) } pub fn rewind(&mut self) -> Result> { return_if_io!(self.ensure_tx_changes_computed()); // Seek GT from i64::MIN to find the first row using internal do_seek let _result = return_if_io!(self.do_seek(i64::MIN, SeekOp::GT)); Ok(IOResult::Done(())) } pub fn is_valid(&self) -> Result { Ok(self.current_row.is_some()) } } #[cfg(test)] mod tests { use super::*; use crate::storage::btree::BTreeCursor; use crate::sync::Arc; use crate::util::IOExt; use crate::{Connection, Database, OpenFlags}; /// Helper to create a test connection with a table and materialized view fn create_test_connection() -> Result> { // Create an in-memory database with experimental views enabled let io = Arc::new(crate::io::MemoryIO::new()); let db = Database::open_file_with_flags( io, ":memory:", OpenFlags::default(), crate::DatabaseOpts { enable_views: true, enable_custom_types: false, enable_load_extension: false, enable_encryption: false, enable_index_method: false, enable_autovacuum: false, enable_attach: false, unsafe_testing: false, }, None, )?; let conn = db.connect()?; // Create a test table conn.execute("CREATE TABLE test_table (id INTEGER PRIMARY KEY, value INTEGER)")?; // Create materialized view conn.execute("CREATE MATERIALIZED VIEW test_view AS SELECT id, value FROM test_table")?; Ok(conn) } /// Helper to create a test cursor for the materialized view fn create_test_cursor( conn: &Arc, ) -> Result<( MaterializedViewCursor, Arc, Arc, )> { // Get the schema and view let view_mutex = conn .schema .read() .get_materialized_view("test_view") .ok_or_else(|| crate::LimboError::InternalError("View not found".to_string()))?; // Get the view's root page let view = view_mutex.lock(); let root_page = view.get_root_page(); if root_page == 0 { return Err(crate::LimboError::InternalError( "View not materialized".to_string(), )); } let num_columns = view.column_schema.columns.len(); drop(view); // Create a btree cursor let pager = conn.get_pager(); let btree_cursor = Box::new(BTreeCursor::new(pager.clone(), root_page, num_columns)); // Get or create transaction state for this view let tx_state = conn.view_transaction_states.get_or_create("test_view"); // Create the materialized view cursor let cursor = MaterializedViewCursor::new( btree_cursor, view_mutex.clone(), pager.clone(), tx_state.clone(), )?; Ok((cursor, tx_state, pager)) } /// Helper to populate test table with data through SQL fn populate_test_table(conn: &Arc, rows: Vec<(i64, i64)>) -> Result<()> { for (id, value) in rows { let sql = format!("INSERT INTO test_table (id, value) VALUES ({id}, {value})"); conn.execute(&sql)?; } Ok(()) } /// Helper to apply changes through ViewTransactionState fn apply_changes_to_tx_state( tx_state: &ViewTransactionState, changes: Vec<(i64, Vec, isize)>, ) { for (rowid, values, weight) in changes { if weight > 0 { tx_state.insert("test_table", rowid, values); } else if weight < 0 { tx_state.delete("test_table", rowid, values); } } } #[test] fn test_seek_key_exists_in_btree() -> Result<()> { let conn = create_test_connection()?; // Populate table with test data: rows 1, 3, 5, 7 populate_test_table(&conn, vec![(1, 10), (3, 30), (5, 50), (7, 70)])?; // Create cursor for testing let (mut cursor, _tx_state, pager) = create_test_cursor(&conn)?; // No uncommitted changes - tx_state is already empty // Test 1: Seek exact match (row 3) let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(3), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); // Test 2: Seek GE (row 4 should find row 5) let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(4), SeekOp::GE { eq_only: false }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); // Test 3: Seek GT (row 3 should find row 5) let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(3), SeekOp::GT))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); // Test 4: Seek LE (row 4 should find row 3) let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(4), SeekOp::LE { eq_only: false }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); // Test 5: Seek LT (row 5 should find row 3) let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(5), SeekOp::LT))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); Ok(()) } #[test] fn test_seek_key_exists_only_uncommitted() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 5, 7 populate_test_table(&conn, vec![(1, 10), (5, 50), (7, 70)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Add uncommitted changes: insert rows 3 and 6 apply_changes_to_tx_state( &tx_state, vec![ (3, vec![Value::from_i64(3), Value::from_i64(30)], 1), // Insert row 3 (6, vec![Value::from_i64(6), Value::from_i64(60)], 1), // Insert row 6 ], ); // Test 1: Seek exact match for uncommitted row 3 let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(3), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(30)); // Test 2: Seek GE for row 2 should find uncommitted row 3 let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(2), SeekOp::GE { eq_only: false }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); // Test 3: Seek GT for row 5 should find uncommitted row 6 let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(5), SeekOp::GT))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(6)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(60)); // Test 4: Seek LE for row 6 should find uncommitted row 6 let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(6), SeekOp::LE { eq_only: false }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(6)); Ok(()) } #[test] fn test_seek_key_deleted_by_uncommitted() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 3, 5, 7 populate_test_table(&conn, vec![(1, 10), (3, 30), (5, 50), (7, 70)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Delete row 3 and 5 in uncommitted changes apply_changes_to_tx_state( &tx_state, vec![ (3, vec![Value::from_i64(3), Value::from_i64(30)], -1), // Delete row 3 (5, vec![Value::from_i64(5), Value::from_i64(50)], -1), // Delete row 5 ], ); // Test 1: Seek exact match for deleted row 3 should not find it let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(3), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::NotFound); // Test 2: Seek GE for row 2 should skip deleted row 3 and find row 7 let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(2), SeekOp::GE { eq_only: false }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(7)); // Test 3: Seek GT for row 1 should skip deleted rows and find row 7 let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(1), SeekOp::GT))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(7)); // Test 4: Seek LE for row 5 should find row 1 (skipping deleted 3 and 5) let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(5), SeekOp::LE { eq_only: false }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); Ok(()) } #[test] fn test_seek_with_updates() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 3, 5 populate_test_table(&conn, vec![(1, 10), (3, 30), (5, 50)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Update row 3 (delete old + insert new) apply_changes_to_tx_state( &tx_state, vec![ (3, vec![Value::from_i64(3), Value::from_i64(30)], -1), // Delete old row 3 (3, vec![Value::from_i64(3), Value::from_i64(35)], 1), // Insert new row 3 ], ); // Test: Seek for updated row 3 should find it let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(3), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); // The values should be from the uncommitted set (35 instead of 30) assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(35)); Ok(()) } #[test] fn test_seek_boundary_conditions() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 5, 10 populate_test_table(&conn, vec![(5, 50), (10, 100)])?; // Create cursor for testing let (mut cursor, _tx_state, pager) = create_test_cursor(&conn)?; // No uncommitted changes - tx_state is already empty // Test 1: Seek LT for minimum value (should find nothing) let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(1), SeekOp::LT))?; assert_eq!(result, SeekResult::NotFound); // Test 2: Seek GT for maximum value (should find nothing) let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(15), SeekOp::GT))?; assert_eq!(result, SeekResult::NotFound); // Test 3: Seek exact for non-existent key let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(7), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::NotFound); Ok(()) } #[test] fn test_seek_complex_uncommitted_weights() -> Result<()> { let conn = create_test_connection()?; // Populate table with row 5 populate_test_table(&conn, vec![(5, 50)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Complex uncommitted changes with multiple operations on same row apply_changes_to_tx_state( &tx_state, vec![ (5, vec![Value::from_i64(5), Value::from_i64(50)], -1), // Delete original (5, vec![Value::from_i64(5), Value::from_i64(51)], 1), // Insert update 1 (5, vec![Value::from_i64(5), Value::from_i64(51)], -1), // Delete update 1 (5, vec![Value::from_i64(5), Value::from_i64(52)], 1), // Insert update 2 // Net effect: row 5 exists with value 52 ], ); // Seek for row 5 should find it (net weight = 1 from btree + 0 from uncommitted = 1) let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(5), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); // The final value should be 52 from the last update assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(52)); Ok(()) } #[test] fn test_seek_affected_by_transaction_state_changes() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1 and 3 populate_test_table(&conn, vec![(1, 10), (3, 30)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Seek for row 2 - doesn't exist let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(2), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::NotFound); // Add row 2 to uncommitted tx_state.insert( "test_table", 2, vec![Value::from_i64(2), Value::from_i64(20)], ); // Now seek for row 2 finds it let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(2), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(2)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(20)); Ok(()) } #[test] fn test_rewind_btree_first_uncommitted_later() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 3, 5 populate_test_table(&conn, vec![(1, 10), (3, 30), (5, 50)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Add uncommitted rows 8, 10 (all larger than btree rows) apply_changes_to_tx_state( &tx_state, vec![ (8, vec![Value::from_i64(8), Value::from_i64(80)], 1), (10, vec![Value::from_i64(10), Value::from_i64(100)], 1), ], ); // Initially cursor is not positioned assert!(!cursor.is_valid()?); // Rewind should position at first btree row (1) since uncommitted are all larger pager.io.block(|| cursor.rewind())?; assert!(cursor.is_valid()?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); Ok(()) } #[test] fn test_rewind_with_uncommitted_first() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 5, 7 populate_test_table(&conn, vec![(5, 50), (7, 70)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Add uncommitted row 2 (smaller than any btree row) apply_changes_to_tx_state( &tx_state, vec![(2, vec![Value::from_i64(2), Value::from_i64(20)], 1)], ); // Rewind should position at row 2 (uncommitted) pager.io.block(|| cursor.rewind())?; assert!(cursor.is_valid()?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(2)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(20)); Ok(()) } #[test] fn test_rewind_skip_deleted_first() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 3, 5 populate_test_table(&conn, vec![(1, 10), (3, 30), (5, 50)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Delete row 1 in uncommitted apply_changes_to_tx_state( &tx_state, vec![(1, vec![Value::from_i64(1), Value::from_i64(10)], -1)], ); // Rewind should skip deleted row 1 and position at row 3 pager.io.block(|| cursor.rewind())?; assert!(cursor.is_valid()?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); Ok(()) } #[test] fn test_rewind_empty_btree_with_uncommitted() -> Result<()> { let conn = create_test_connection()?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Add uncommitted rows (no btree data) apply_changes_to_tx_state( &tx_state, vec![ (3, vec![Value::from_i64(3), Value::from_i64(30)], 1), (7, vec![Value::from_i64(7), Value::from_i64(70)], 1), ], ); // Rewind should find first uncommitted row pager.io.block(|| cursor.rewind())?; assert!(cursor.is_valid()?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(30)); Ok(()) } #[test] fn test_rewind_all_deleted() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 2, 4 populate_test_table(&conn, vec![(2, 20), (4, 40)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Delete all rows in uncommitted apply_changes_to_tx_state( &tx_state, vec![ (2, vec![Value::from_i64(2), Value::from_i64(20)], -1), (4, vec![Value::from_i64(4), Value::from_i64(40)], -1), ], ); // Rewind should find no valid rows pager.io.block(|| cursor.rewind())?; assert!(!cursor.is_valid()?); assert_eq!(pager.io.block(|| cursor.rowid())?, None); Ok(()) } #[test] fn test_rewind_with_updates() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 3 populate_test_table(&conn, vec![(1, 10), (3, 30)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Update row 1 (delete + insert with new value) apply_changes_to_tx_state( &tx_state, vec![ (1, vec![Value::from_i64(1), Value::from_i64(10)], -1), (1, vec![Value::from_i64(1), Value::from_i64(15)], 1), ], ); // Rewind should position at row 1 with updated value pager.io.block(|| cursor.rewind())?; assert!(cursor.is_valid()?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(15)); Ok(()) } // ===== NEXT() TEST SUITE ===== #[test] fn test_next_btree_only_sequential() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 3, 5, 7 populate_test_table(&conn, vec![(1, 10), (3, 30), (5, 50), (7, 70)])?; // Create cursor for testing let (mut cursor, _tx_state, pager) = create_test_cursor(&conn)?; // Start with rewind to position at first row pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); // Next should move to row 3 assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); // Next should move to row 5 assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); // Next should move to row 7 assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(7)); // Next should reach end assert!(!pager.io.block(|| cursor.next())?); assert!(!cursor.is_valid()?); Ok(()) } #[test] fn test_next_uncommitted_only() -> Result<()> { let conn = create_test_connection()?; // Create cursor for testing (no btree data) let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Add uncommitted rows 2, 4, 6 apply_changes_to_tx_state( &tx_state, vec![ (2, vec![Value::from_i64(2), Value::from_i64(20)], 1), (4, vec![Value::from_i64(4), Value::from_i64(40)], 1), (6, vec![Value::from_i64(6), Value::from_i64(60)], 1), ], ); // Start with rewind to position at first row pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(2)); // Next should move to row 4 assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(4)); // Next should move to row 6 assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(6)); // Next should reach end assert!(!pager.io.block(|| cursor.next())?); assert!(!cursor.is_valid()?); Ok(()) } #[test] fn test_next_mixed_btree_uncommitted() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 5, 9 populate_test_table(&conn, vec![(1, 10), (5, 50), (9, 90)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Add uncommitted rows 3, 7 apply_changes_to_tx_state( &tx_state, vec![ (3, vec![Value::from_i64(3), Value::from_i64(30)], 1), (7, vec![Value::from_i64(7), Value::from_i64(70)], 1), ], ); // Should iterate in order: 1, 3, 5, 7, 9 pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(7)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(9)); assert!(!pager.io.block(|| cursor.next())?); assert!(!cursor.is_valid()?); Ok(()) } #[test] fn test_next_skip_deleted_rows() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 2, 3, 4, 5 populate_test_table(&conn, vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Delete rows 2 and 4 in uncommitted apply_changes_to_tx_state( &tx_state, vec![ (2, vec![Value::from_i64(2), Value::from_i64(20)], -1), (4, vec![Value::from_i64(4), Value::from_i64(40)], -1), ], ); // Should iterate: 1, 3, 5 (skipping deleted 2 and 4) pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); assert!(!pager.io.block(|| cursor.next())?); assert!(!cursor.is_valid()?); Ok(()) } #[test] fn test_next_with_updates() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 3, 5 populate_test_table(&conn, vec![(1, 10), (3, 30), (5, 50)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Update row 3 (delete old + insert new) apply_changes_to_tx_state( &tx_state, vec![ (3, vec![Value::from_i64(3), Value::from_i64(30)], -1), (3, vec![Value::from_i64(3), Value::from_i64(35)], 1), ], ); // Should iterate all rows with updated values pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(35)); // Updated value assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); assert!(!pager.io.block(|| cursor.next())?); Ok(()) } #[test] fn test_next_from_uninitialized() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 2, 4 populate_test_table(&conn, vec![(2, 20), (4, 40)])?; // Create cursor for testing let (mut cursor, _tx_state, pager) = create_test_cursor(&conn)?; // Cursor not positioned initially assert!(!cursor.is_valid()?); // Next on uninitialized cursor should return false (matching BTreeCursor behavior) assert!(!pager.io.block(|| cursor.next())?); assert!(!cursor.is_valid()?); // Position cursor with rewind first pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(2)); // Now next should work assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(4)); assert!(!pager.io.block(|| cursor.next())?); Ok(()) } #[test] fn test_next_empty_table() -> Result<()> { let conn = create_test_connection()?; // Create cursor for testing (empty table) let (mut cursor, _tx_state, pager) = create_test_cursor(&conn)?; // Next on empty table should return false assert!(!pager.io.block(|| cursor.next())?); assert!(!cursor.is_valid()?); Ok(()) } #[test] fn test_next_all_deleted() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 2, 3 populate_test_table(&conn, vec![(1, 10), (2, 20), (3, 30)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Delete all rows apply_changes_to_tx_state( &tx_state, vec![ (1, vec![Value::from_i64(1), Value::from_i64(10)], -1), (2, vec![Value::from_i64(2), Value::from_i64(20)], -1), (3, vec![Value::from_i64(3), Value::from_i64(30)], -1), ], ); // Next should find nothing assert!(!pager.io.block(|| cursor.next())?); assert!(!cursor.is_valid()?); Ok(()) } #[test] fn test_next_complex_interleaving() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 2, 4, 6, 8 populate_test_table(&conn, vec![(2, 20), (4, 40), (6, 60), (8, 80)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Complex changes: // - Insert row 1 // - Delete row 2 // - Insert row 3 // - Update row 4 // - Insert row 5 // - Delete row 6 // - Insert row 7 // - Keep row 8 as-is // - Insert row 9 apply_changes_to_tx_state( &tx_state, vec![ (1, vec![Value::from_i64(1), Value::from_i64(10)], 1), // Insert 1 (2, vec![Value::from_i64(2), Value::from_i64(20)], -1), // Delete 2 (3, vec![Value::from_i64(3), Value::from_i64(30)], 1), // Insert 3 (4, vec![Value::from_i64(4), Value::from_i64(40)], -1), // Delete old 4 (4, vec![Value::from_i64(4), Value::from_i64(45)], 1), // Insert new 4 (5, vec![Value::from_i64(5), Value::from_i64(50)], 1), // Insert 5 (6, vec![Value::from_i64(6), Value::from_i64(60)], -1), // Delete 6 (7, vec![Value::from_i64(7), Value::from_i64(70)], 1), // Insert 7 (9, vec![Value::from_i64(9), Value::from_i64(90)], 1), // Insert 9 ], ); // Should iterate: 1, 3, 4(updated), 5, 7, 8, 9 pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(4)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(45)); // Updated value assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(7)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(8)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(9)); assert!(!pager.io.block(|| cursor.next())?); assert!(!cursor.is_valid()?); Ok(()) } #[test] fn test_next_after_seek() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 3, 5, 7, 9 populate_test_table(&conn, vec![(1, 10), (3, 30), (5, 50), (7, 70), (9, 90)])?; // Create cursor for testing let (mut cursor, _tx_state, pager) = create_test_cursor(&conn)?; // Seek to row 5 let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(5), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); // Next should move to row 7 assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(7)); // Next should move to row 9 assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(9)); // Next should reach end assert!(!pager.io.block(|| cursor.next())?); Ok(()) } #[test] fn test_next_multiple_weights_same_row() -> Result<()> { let conn = create_test_connection()?; // Populate table with row 1 populate_test_table(&conn, vec![(1, 10)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Multiple operations on same row: apply_changes_to_tx_state( &tx_state, vec![ (1, vec![Value::from_i64(1), Value::from_i64(10)], -1), // Delete original (1, vec![Value::from_i64(1), Value::from_i64(11)], 1), // Insert v1 (1, vec![Value::from_i64(1), Value::from_i64(11)], -1), // Delete v1 (1, vec![Value::from_i64(1), Value::from_i64(12)], 1), // Insert v2 (1, vec![Value::from_i64(1), Value::from_i64(12)], -1), // Delete v2 // Net weight: 1 (btree) - 1 + 1 - 1 + 1 - 1 = 0 (row deleted) ], ); // Row should be deleted assert!(!pager.io.block(|| cursor.next())?); assert!(!cursor.is_valid()?); Ok(()) } #[test] fn test_next_only_uncommitted_large_gaps() -> Result<()> { let conn = create_test_connection()?; // Create cursor for testing (no btree data) let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Add uncommitted rows with large gaps apply_changes_to_tx_state( &tx_state, vec![ (100, vec![Value::from_i64(100), Value::from_i64(1000)], 1), (500, vec![Value::from_i64(500), Value::from_i64(5000)], 1), (999, vec![Value::from_i64(999), Value::from_i64(9990)], 1), ], ); // Should iterate through all with large gaps pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(100)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(500)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(999)); assert!(!pager.io.block(|| cursor.next())?); Ok(()) } #[test] fn test_multiple_updates_same_row_single_transaction() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 2, 3 populate_test_table(&conn, vec![(1, 10), (2, 20), (3, 30)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Multiple successive updates to row 2 in the same transaction // 20 -> 25 -> 28 -> 32 (final value should be 32) apply_changes_to_tx_state( &tx_state, vec![ (2, vec![Value::from_i64(2), Value::from_i64(20)], -1), // Delete original (2, vec![Value::from_i64(2), Value::from_i64(25)], 1), // First update (2, vec![Value::from_i64(2), Value::from_i64(25)], -1), // Delete first update (2, vec![Value::from_i64(2), Value::from_i64(28)], 1), // Second update (2, vec![Value::from_i64(2), Value::from_i64(28)], -1), // Delete second update (2, vec![Value::from_i64(2), Value::from_i64(32)], 1), // Final update ], ); // Seek to row 2 should find the final value let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(2), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(2)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(32)); // Next through all rows to verify only final values are seen pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(10)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(2)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(32)); // Final value assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(30)); assert!(!pager.io.block(|| cursor.next())?); Ok(()) } #[test] fn test_empty_materialized_view_with_uncommitted() -> Result<()> { let conn = create_test_connection()?; // Don't populate any data - view is created but empty // This tests a materialized view that was never populated // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Add uncommitted rows to empty materialized view apply_changes_to_tx_state( &tx_state, vec![ (5, vec![Value::from_i64(5), Value::from_i64(50)], 1), (10, vec![Value::from_i64(10), Value::from_i64(100)], 1), (15, vec![Value::from_i64(15), Value::from_i64(150)], 1), ], ); // Test seek on empty materialized view with uncommitted data let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(10), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(10)); // Test GT seek let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(7), SeekOp::GT))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(10)); // Test rewind and next pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(10)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(15)); assert!(!pager.io.block(|| cursor.next())?); Ok(()) } #[test] fn test_exact_match_btree_uncommitted_same_rowid_different_values() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 3, 5 populate_test_table(&conn, vec![(1, 10), (3, 30), (5, 50)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Add uncommitted row 3 with different value (not a delete+insert, just insert) // This simulates a case where uncommitted has a new version of row 3 apply_changes_to_tx_state( &tx_state, vec![ (3, vec![Value::from_i64(3), Value::from_i64(35)], 1), // New version with positive weight ], ); // Exact match seek for row 3 should find the uncommitted version (35) // because when both exist with positive weight, uncommitted takes precedence let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(3), SeekOp::GE { eq_only: true }))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); // This test verifies which value we get when both btree and uncommitted // have the same rowid with positive weights // The expected behavior needs to be defined - typically uncommitted wins // or they get merged based on the DBSP semantics Ok(()) } #[test] fn test_boundary_value_seeks() -> Result<()> { let conn = create_test_connection()?; // Populate table with some normal values populate_test_table(&conn, vec![(100, 1000), (200, 2000)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Add uncommitted rows at extreme positions apply_changes_to_tx_state( &tx_state, vec![ ( i64::MIN + 1, vec![Value::from_i64(i64::MIN + 1), Value::from_i64(-999)], 1, ), ( i64::MAX - 1, vec![Value::from_i64(i64::MAX - 1), Value::from_i64(999)], 1, ), ], ); // Test 1: Seek GT with i64::MAX should find nothing let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(i64::MAX), SeekOp::GT))?; assert_eq!(result, SeekResult::NotFound); // Test 2: Seek LT with i64::MIN should find nothing let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(i64::MIN), SeekOp::LT))?; assert_eq!(result, SeekResult::NotFound); // Test 3: Seek GE with i64::MAX - 1 should find our extreme row let result = pager.io.block(|| { cursor.seek( SeekKey::TableRowId(i64::MAX - 1), SeekOp::GE { eq_only: false }, ) })?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(i64::MAX - 1)); // Test 4: Seek LE with i64::MIN + 1 should find our extreme low row let result = pager.io.block(|| { cursor.seek( SeekKey::TableRowId(i64::MIN + 1), SeekOp::LE { eq_only: false }, ) })?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(i64::MIN + 1)); // Test 5: Seek GT from i64::MIN should find the smallest row let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(i64::MIN), SeekOp::GT))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(i64::MIN + 1)); // Test 6: Seek LT from i64::MAX should find the largest row let result = pager .io .block(|| cursor.seek(SeekKey::TableRowId(i64::MAX), SeekOp::LT))?; assert_eq!(result, SeekResult::Found); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(i64::MAX - 1)); Ok(()) } #[test] fn test_next_concurrent_btree_uncommitted_advance() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 2, 3, 4, 5 populate_test_table(&conn, vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Delete some btree rows and add replacements in uncommitted apply_changes_to_tx_state( &tx_state, vec![ (2, vec![Value::from_i64(2), Value::from_i64(20)], -1), // Delete btree row 2 (2, vec![Value::from_i64(2), Value::from_i64(25)], 1), // Replace with new value (4, vec![Value::from_i64(4), Value::from_i64(40)], -1), // Delete btree row 4 ], ); // Should iterate: 1, 2(new), 3, 5 pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(2)); assert_eq!(pager.io.block(|| cursor.column(1))?, Value::from_i64(25)); // New value assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); assert!(!pager.io.block(|| cursor.next())?); Ok(()) } #[test] fn test_transaction_state_changes_mid_iteration() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 3, 5 populate_test_table(&conn, vec![(1, 10), (3, 30), (5, 50)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Start iteration pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); // Move to next row assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); // Now add new uncommitted changes mid-iteration apply_changes_to_tx_state( &tx_state, vec![ (2, vec![Value::from_i64(2), Value::from_i64(20)], 1), // Insert before current (4, vec![Value::from_i64(4), Value::from_i64(40)], 1), // Insert after current (6, vec![Value::from_i64(6), Value::from_i64(60)], 1), // Insert at end ], ); // Continue iteration - cursor continues from where it was, sees row 5 next // (new changes are only visible after rewind/seek) assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); // No more rows in original iteration assert!(!pager.io.block(|| cursor.next())?); // Rewind and verify we see all rows including the newly added ones pager.io.block(|| cursor.rewind())?; assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(2)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(4)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(6)); assert!(!pager.io.block(|| cursor.next())?); Ok(()) } #[test] fn test_rewind_after_failed_seek() -> Result<()> { let conn = create_test_connection()?; // Populate table with rows 1, 3, 5 populate_test_table(&conn, vec![(1, 10), (3, 30), (5, 50)])?; // Create cursor for testing let (mut cursor, tx_state, pager) = create_test_cursor(&conn)?; // Add uncommitted row 2 apply_changes_to_tx_state( &tx_state, vec![(2, vec![Value::from_i64(2), Value::from_i64(20)], 1)], ); // Seek to non-existent row 4 with exact match assert_eq!( pager .io .block(|| cursor.seek(SeekKey::TableRowId(4), SeekOp::GE { eq_only: true }))?, SeekResult::NotFound ); assert!(!cursor.is_valid()?); // Rewind should work correctly after failed seek pager.io.block(|| cursor.rewind())?; assert!(cursor.is_valid()?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); // Verify we can iterate through all rows assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(2)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(3)); assert!(pager.io.block(|| cursor.next())?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(5)); assert!(!pager.io.block(|| cursor.next())?); // Try another failed seek (GT on maximum value) assert_eq!( pager .io .block(|| cursor.seek(SeekKey::TableRowId(5), SeekOp::GT))?, SeekResult::NotFound ); assert!(!cursor.is_valid()?); // Rewind again pager.io.block(|| cursor.rewind())?; assert!(cursor.is_valid()?); assert_eq!(pager.io.block(|| cursor.rowid())?, Some(1)); Ok(()) } // ===== IO RESUMPTION TEST SUITE ===== // These tests verify correct behavior when btree operations return IO (pending) mod io_resumption_tests { use super::*; use crate::io::Completion; use crate::storage::btree::{BTreeKey, CursorTrait}; use crate::types::{IOCompletions, ImmutableRecord, IndexInfo}; use crate::Register; use std::sync::atomic::{AtomicUsize, Ordering}; /// Mock btree cursor that tracks calls and can simulate IO pending states. /// Used to verify that seek operations aren't redundantly repeated after IO resumption. struct MockBTreeCursor { /// Number of times seek() was called seek_count: AtomicUsize, /// Number of times next() was called next_count: AtomicUsize, /// Number of times prev() was called prev_count: AtomicUsize, /// Current rowid to return current_rowid: Option, /// Record to return (needs to live for the cursor lifetime) record: ImmutableRecord, /// Index info index_info: Arc, } impl MockBTreeCursor { fn new() -> Self { // Create a minimal record with rowid=1, value=10, weight=1 let record = Self::create_test_record(1, 10, 1); Self { seek_count: AtomicUsize::new(0), next_count: AtomicUsize::new(0), prev_count: AtomicUsize::new(0), current_rowid: Some(1), record, index_info: Arc::new(IndexInfo::default()), } } fn create_test_record(rowid: i64, value: i64, weight: i64) -> ImmutableRecord { // Build a binary record with format: [header_size, type1, type2, type3, rowid, value, weight] // For integers, type code is 1 for 1-byte int, 2 for 2-byte, etc. // Using type 6 (8-byte integer) for all values // Header: 4 bytes (header size byte + 3 type bytes) let mut payload = vec![ 4u8, // header size 6u8, // type for rowid (8-byte int) 6u8, // type for value (8-byte int) 6u8, // type for weight (8-byte int) ]; // Data: 3 x 8-byte integers payload.extend_from_slice(&rowid.to_be_bytes()); payload.extend_from_slice(&value.to_be_bytes()); payload.extend_from_slice(&weight.to_be_bytes()); ImmutableRecord::from_bin_record(payload) } fn get_seek_count(&self) -> usize { self.seek_count.load(Ordering::SeqCst) } fn get_prev_count(&self) -> usize { self.prev_count.load(Ordering::SeqCst) } } impl CursorTrait for MockBTreeCursor { fn seek(&mut self, _key: SeekKey<'_>, _op: SeekOp) -> Result> { let count = self.seek_count.fetch_add(1, Ordering::SeqCst); if count == 0 { // First seek returns TryAdvance Ok(IOResult::Done(SeekResult::TryAdvance)) } else { // Subsequent seeks return Found to avoid infinite loop // (The bug is that this second seek happens at all) Ok(IOResult::Done(SeekResult::Found)) } } fn seek_unpacked( &mut self, _registers: &[Register], _op: SeekOp, ) -> Result> { // Not used in these tests Ok(IOResult::Done(SeekResult::NotFound)) } fn next(&mut self) -> Result> { let count = self.next_count.fetch_add(1, Ordering::SeqCst); if count == 0 { // First call returns IO (pending) let completion = Completion::new_yield(); Ok(IOResult::IO(IOCompletions::Single(completion))) } else { // Subsequent calls return Done Ok(IOResult::Done(())) } } fn prev(&mut self) -> Result> { let count = self.prev_count.fetch_add(1, Ordering::SeqCst); if count == 0 { // First call returns IO (pending) let completion = Completion::new_yield(); Ok(IOResult::IO(IOCompletions::Single(completion))) } else { // Subsequent calls return Done Ok(IOResult::Done(())) } } fn rowid(&mut self) -> Result>> { Ok(IOResult::Done(self.current_rowid)) } fn record(&mut self) -> Result>> { Ok(IOResult::Done(Some(&self.record))) } fn last(&mut self) -> Result> { Ok(IOResult::Done(())) } fn insert(&mut self, _key: &BTreeKey) -> Result> { Ok(IOResult::Done(())) } fn delete(&mut self) -> Result> { Ok(IOResult::Done(())) } fn set_null_flag(&mut self, _flag: bool) {} fn get_null_flag(&self) -> bool { false } fn exists(&mut self, _key: &Value) -> Result> { Ok(IOResult::Done(false)) } fn clear_btree(&mut self) -> Result>> { Ok(IOResult::Done(None)) } fn btree_destroy(&mut self) -> Result>> { Ok(IOResult::Done(None)) } fn count(&mut self) -> Result> { Ok(IOResult::Done(0)) } fn is_empty(&self) -> bool { false } fn root_page(&self) -> i64 { 1 } fn rewind(&mut self) -> Result> { Ok(IOResult::Done(())) } fn has_record(&self) -> bool { true } fn set_has_record(&mut self, _has_record: bool) {} fn get_index_info(&self) -> &Arc { &self.index_info } fn seek_end(&mut self) -> Result> { Ok(IOResult::Done(())) } fn seek_to_last(&mut self, _always_seek: bool) -> Result> { Ok(IOResult::Done(())) } fn invalidate_record(&mut self) {} fn has_rowid(&self) -> bool { true } fn get_pager(&self) -> Arc { panic!("MockBTreeCursor::get_pager should not be called") } fn get_skip_advance(&self) -> bool { false } } /// Test that verifies the bug: when btree.next() returns IO after TryAdvance, /// resuming should NOT call btree.seek() again. /// /// Current behavior (BUG): seek is called twice /// Expected behavior: seek should only be called once #[test] fn test_seek_not_repeated_after_io_during_try_advance() -> Result<()> { let conn = create_test_connection()?; // Get the view for creating a cursor let view_mutex = conn .schema .read() .get_materialized_view("test_view") .ok_or_else(|| crate::LimboError::InternalError("View not found".to_string()))?; let pager = conn.get_pager(); let tx_state = conn.view_transaction_states.get_or_create("test_view"); // Create mock cursor that returns TryAdvance from seek and IO from next let mock_cursor = MockBTreeCursor::new(); let mock_cursor_box: Box = Box::new(mock_cursor); // Get a reference to the mock to check counts later // We need to use Any::downcast to access the mock's methods let mock_ptr = mock_cursor_box.as_ref() as *const dyn CursorTrait; let mut cursor = MaterializedViewCursor::new(mock_cursor_box, view_mutex, pager, tx_state)?; // Use LE so that rowid=1 satisfies the condition (1 <= 5) let seek_op = SeekOp::LE { eq_only: false }; // First call to do_seek - should call btree.seek() which returns TryAdvance, // then btree.prev() which returns IO let result = cursor.do_seek(5, seek_op); // Should return IO (pending) assert!( matches!(result, Ok(IOResult::IO(_))), "Expected IO result, got {result:?}" ); // Check seek was called once let mock_ref: &MockBTreeCursor = unsafe { &*(mock_ptr as *const MockBTreeCursor) }; assert_eq!( mock_ref.get_seek_count(), 1, "seek should be called exactly once before IO" ); // For LE, we call prev() not next() assert_eq!( mock_ref.get_prev_count(), 1, "prev should be called once (returned IO)" ); // Second call to do_seek (simulating resumption after IO completes) // BUG: This will call btree.seek() again, which is wasteful let result = cursor.do_seek(5, seek_op); // The result might be Found or some other result assert!( matches!(result, Ok(IOResult::Done(_))), "Expected Done result on resume, got {result:?}" ); // Check seek count - seek should only be called once // If this fails with seek_count=2, it means the bug exists: // seek is being redundantly called again after IO resumption let final_seek_count = mock_ref.get_seek_count(); assert_eq!( final_seek_count, 1, "seek should only be called once, but was called {final_seek_count} times (redundant seek after IO during TryAdvance)" ); Ok(()) } } } ================================================ FILE: core/incremental/dbsp.rs ================================================ // Simplified DBSP integration for incremental view maintenance // For now, we'll use a basic approach and can expand to full DBSP later use crate::numeric::Numeric; use crate::Value; use std::collections::{BTreeMap, HashMap}; use std::hash::{Hash, Hasher}; /// A 128-bit hash value implemented as a UUID /// We use UUID because it's a standard 128-bit type we already depend on #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Hash128 { // Store as UUID internally for efficient 128-bit representation uuid: uuid::Uuid, } impl Hash128 { /// Create a new 128-bit hash from high and low 64-bit parts pub fn new(high: u64, low: u64) -> Self { // Convert two u64 values to UUID bytes (big-endian) let mut bytes = [0u8; 16]; bytes[0..8].copy_from_slice(&high.to_be_bytes()); bytes[8..16].copy_from_slice(&low.to_be_bytes()); Self { uuid: uuid::Uuid::from_bytes(bytes), } } /// Get the low 64 bits as i64 (for when we need a rowid) pub fn as_i64(&self) -> i64 { let bytes = self.uuid.as_bytes(); let low = u64::from_be_bytes([ bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], ]); low as i64 } /// Compute a 128-bit hash of the given values /// We serialize values to a string representation and use UUID v5 (SHA-1 based) /// to get a deterministic 128-bit hash pub fn hash_values(values: &[Value]) -> Self { // Build a string representation of all values // Use a delimiter that won't appear in normal values let mut s = String::new(); for (i, value) in values.iter().enumerate() { if i > 0 { s.push('\x00'); // null byte as delimiter } // Add type prefix to distinguish between types match value { Value::Null => s.push_str("N:"), Value::Numeric(Numeric::Integer(n)) => { s.push_str("I:"); s.push_str(&n.to_string()); } Value::Numeric(Numeric::Float(f)) => { s.push_str("F:"); // Use to_bits to ensure consistent representation s.push_str(&f64::from(*f).to_bits().to_string()); } Value::Text(t) => { s.push_str("T:"); s.push_str(t.as_str()); } Value::Blob(b) => { s.push_str("B:"); s.push_str(&hex::encode(b)); } } } Self::hash_str(&s) } /// Hash a string value to 128 bits using UUID v5 pub fn hash_str(s: &str) -> Self { // Use UUID v5 with a fixed namespace to get deterministic 128-bit hashes // We use the DNS namespace as it's a standard choice let uuid = uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_DNS, s.as_bytes()); Self { uuid } } /// Convert to a big-endian byte array for storage pub fn to_blob(self) -> Vec { self.uuid.as_bytes().to_vec() } /// Create from a big-endian byte array pub fn from_blob(bytes: &[u8]) -> Option { if bytes.len() != 16 { return None; } let mut uuid_bytes = [0u8; 16]; uuid_bytes.copy_from_slice(bytes); Some(Self { uuid: uuid::Uuid::from_bytes(uuid_bytes), }) } /// Convert to a Value::Blob for storage pub fn to_value(self) -> Value { Value::Blob(self.to_blob()) } /// Try to extract a Hash128 from a Value pub fn from_value(value: &Value) -> Option { match value { Value::Blob(b) => Self::from_blob(b), _ => None, } } } impl std::fmt::Display for Hash128 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.uuid) } } // The DBSP paper uses as a key the whole record, with both the row key and the values. This is a // bit confuses for us in databases, because when you say "key", it is easy to understand that as // being the row key. // // Empirically speaking, using row keys as the ZSet keys will waste a competent but not brilliant // engineer around 82 and 88 hours, depending on how you count. Hours that are never coming back. // // One of the situations in which using row keys completely breaks are table updates. If the "key" // is the row key, let's say "5", then an update is a delete + insert. Imagine a table that had k = // 5, v = 5, and a view that filters v > 2. // // Now we will do an update that changes v => 1. If the "key" is 5, then inside the Delta set, we // will have (5, weight = -1), (5, weight = +1), and the whole thing just disappears. The Delta // set, therefore, has to contain ((5, 5), weight = -1), ((5, 1), weight = +1). // // It is theoretically possible to use the rowkey in the ZSet and then use a hash of key -> // Vec(changes) in the Delta set. But deviating from the paper here is just asking for trouble, as // I am sure it would break somewhere else. #[derive(Debug, Clone, PartialEq, Eq)] pub struct HashableRow { pub rowid: i64, pub values: Vec, // Pre-computed hash: DBSP rows are immutable and frequently hashed during joins, // making caching worthwhile despite the memory overhead cached_hash: Hash128, } impl HashableRow { pub fn new(rowid: i64, values: Vec) -> Self { let cached_hash = Self::compute_hash(rowid, &values); Self { rowid, values, cached_hash, } } fn compute_hash(rowid: i64, values: &[Value]) -> Hash128 { // Include rowid in the hash by prepending it to values let mut all_values = Vec::with_capacity(values.len() + 1); all_values.push(Value::from_i64(rowid)); all_values.extend_from_slice(values); Hash128::hash_values(&all_values) } pub fn cached_hash(&self) -> Hash128 { self.cached_hash } } impl Hash for HashableRow { fn hash(&self, state: &mut H) { // Hash the 128-bit value by hashing both parts self.cached_hash.to_blob().hash(state); } } impl PartialOrd for HashableRow { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for HashableRow { fn cmp(&self, other: &Self) -> std::cmp::Ordering { // First compare by rowid, then by values if rowids are equal // This ensures Ord is consistent with Eq (which compares all fields) match self.rowid.cmp(&other.rowid) { std::cmp::Ordering::Equal => { // If rowids are equal, compare values to maintain consistency with Eq self.values.cmp(&other.values) } other => other, } } } type DeltaEntry = (HashableRow, isize); /// A delta represents ordered changes to data #[derive(Debug, Clone, Default)] pub struct Delta { /// Ordered list of changes: (row, weight) where weight is +1 for insert, -1 for delete /// It is crucial that this is ordered. Imagine the case of an update, which becomes a delete + /// insert. If this is not ordered, it would be applied in arbitrary order and break the view. pub changes: Vec, } impl Delta { pub fn new() -> Self { Self { changes: Vec::new(), } } pub fn insert(&mut self, row_key: i64, values: Vec) { let row = HashableRow::new(row_key, values); self.changes.push((row, 1)); } pub fn delete(&mut self, row_key: i64, values: Vec) { let row = HashableRow::new(row_key, values); self.changes.push((row, -1)); } pub fn is_empty(&self) -> bool { self.changes.is_empty() } pub fn len(&self) -> usize { self.changes.len() } /// Merge another delta into this one /// This preserves the order of operations - no consolidation is done /// to maintain the full history of changes pub fn merge(&mut self, other: &Delta) { // Simply append all changes from other, preserving order self.changes.extend(other.changes.iter().cloned()); } /// Consolidate changes by combining entries with the same HashableRow pub fn consolidate(&mut self) { if self.changes.is_empty() { return; } // Use a HashMap to accumulate weights let mut consolidated: HashMap = HashMap::default(); for (row, weight) in self.changes.drain(..) { *consolidated.entry(row).or_insert(0) += weight; } // Convert back to vec, filtering out zero weights self.changes = consolidated .into_iter() .filter(|(_, weight)| *weight != 0) .collect(); } } /// A pair of deltas for operators that process two inputs #[derive(Debug, Clone, Default)] pub struct DeltaPair { pub left: Delta, pub right: Delta, } impl DeltaPair { /// Create a new delta pair pub fn new(left: Delta, right: Delta) -> Self { Self { left, right } } } impl From for DeltaPair { /// Convert a single delta into a delta pair with empty right delta fn from(delta: Delta) -> Self { Self { left: delta, right: Delta::new(), } } } impl From<&Delta> for DeltaPair { /// Convert a delta reference into a delta pair with empty right delta fn from(delta: &Delta) -> Self { Self { left: delta.clone(), right: Delta::new(), } } } /// A simplified ZSet for incremental computation /// Each element has a weight: positive for additions, negative for deletions #[derive(Clone, Debug, Default)] pub struct SimpleZSet { data: BTreeMap, } #[allow(dead_code)] impl SimpleZSet { pub fn new() -> Self { Self { data: BTreeMap::new(), } } pub fn insert(&mut self, item: T, weight: isize) { let current = self.data.get(&item).copied().unwrap_or(0); let new_weight = current + weight; if new_weight == 0 { self.data.remove(&item); } else { self.data.insert(item, new_weight); } } pub fn iter(&self) -> impl Iterator { self.data.iter().map(|(k, &v)| (k, v)) } /// Get all items with positive weights pub fn to_vec(&self) -> Vec { self.data .iter() .filter(|(_, &weight)| weight > 0) .map(|(item, _)| item.clone()) .collect() } pub fn merge(&mut self, other: &SimpleZSet) { for (item, weight) in other.iter() { self.insert(item.clone(), weight); } } /// Get the weight for a specific item (0 if not present) pub fn get(&self, item: &T) -> isize { self.data.get(item).copied().unwrap_or(0) } /// Get the first element (smallest key) in the Z-set pub fn first(&self) -> Option<(&T, isize)> { self.data.iter().next().map(|(k, &v)| (k, v)) } /// Get the last element (largest key) in the Z-set pub fn last(&self) -> Option<(&T, isize)> { self.data.iter().next_back().map(|(k, &v)| (k, v)) } /// Get a range of elements pub fn range(&self, range: R) -> impl Iterator + '_ where R: std::ops::RangeBounds, { self.data.range(range).map(|(k, &v)| (k, v)) } /// Check if empty pub fn is_empty(&self) -> bool { self.data.is_empty() } /// Get the number of elements pub fn len(&self) -> usize { self.data.len() } } // Type aliases for convenience pub type RowKey = HashableRow; pub type RowKeyZSet = SimpleZSet; impl RowKeyZSet { /// Create a Z-set from a Delta by consolidating all changes pub fn from_delta(delta: &Delta) -> Self { let mut zset = Self::new(); // Add all changes from the delta, consolidating as we go for (row, weight) in &delta.changes { zset.insert(row.clone(), *weight); } zset } /// Seek to find ALL entries for the best matching rowid /// For GT/GE: returns all entries for the smallest rowid that satisfies the condition /// For LT/LE: returns all entries for the largest rowid that satisfies the condition /// Returns empty vec if no match found pub fn seek(&self, target: i64, op: crate::types::SeekOp) -> Vec<(HashableRow, isize)> { use crate::types::SeekOp; // First find the best matching rowid let best_rowid = match op { SeekOp::GT => { // Find smallest rowid > target self.data .iter() .filter(|(row, _)| row.rowid > target) .map(|(row, _)| row.rowid) .min() } SeekOp::GE { eq_only: false } => { // Find smallest rowid >= target self.data .iter() .filter(|(row, _)| row.rowid >= target) .map(|(row, _)| row.rowid) .min() } SeekOp::GE { eq_only: true } | SeekOp::LE { eq_only: true } => { // Need exact match if self.data.iter().any(|(row, _)| row.rowid == target) { Some(target) } else { None } } SeekOp::LT => { // Find largest rowid < target self.data .iter() .filter(|(row, _)| row.rowid < target) .map(|(row, _)| row.rowid) .max() } SeekOp::LE { eq_only: false } => { // Find largest rowid <= target self.data .iter() .filter(|(row, _)| row.rowid <= target) .map(|(row, _)| row.rowid) .max() } }; // Now get ALL entries with that rowid match best_rowid { Some(rowid) => self .data .iter() .filter(|(row, _)| row.rowid == rowid) .map(|(k, &v)| (k.clone(), v)) .collect(), None => Vec::new(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_zset_merge_with_weights() { let mut zset1 = SimpleZSet::new(); zset1.insert(1, 1); // Row 1 with weight +1 zset1.insert(2, 1); // Row 2 with weight +1 let mut zset2 = SimpleZSet::new(); zset2.insert(2, -1); // Row 2 with weight -1 (delete) zset2.insert(3, 1); // Row 3 with weight +1 (insert) zset1.merge(&zset2); // Row 1: weight 1 (unchanged) // Row 2: weight 0 (deleted) // Row 3: weight 1 (inserted) assert_eq!(zset1.iter().count(), 2); // Only rows 1 and 3 assert!(zset1.iter().any(|(k, _)| *k == 1)); assert!(zset1.iter().any(|(k, _)| *k == 3)); assert!(!zset1.iter().any(|(k, _)| *k == 2)); // Row 2 removed } #[test] fn test_zset_represents_updates_as_delete_plus_insert() { let mut zset = SimpleZSet::new(); // Initial state zset.insert(1, 1); // Update row 1: delete old + insert new zset.insert(1, -1); // Delete old version zset.insert(1, 1); // Insert new version // Weight should be 1 (not 2) let weight = zset.iter().find(|(k, _)| **k == 1).map(|(_, w)| w); assert_eq!(weight, Some(1)); } #[test] fn test_hashable_row_delta_operations() { let mut delta = Delta::new(); // Test INSERT delta.insert(1, vec![Value::from_i64(1), Value::from_i64(100)]); assert_eq!(delta.len(), 1); // Test UPDATE (DELETE + INSERT) - order matters! delta.delete(1, vec![Value::from_i64(1), Value::from_i64(100)]); delta.insert(1, vec![Value::from_i64(1), Value::from_i64(200)]); assert_eq!(delta.len(), 3); // Should have 3 operations before consolidation // Verify order is preserved let ops: Vec<_> = delta.changes.iter().collect(); assert_eq!(ops[0].1, 1); // First insert assert_eq!(ops[1].1, -1); // Delete assert_eq!(ops[2].1, 1); // Second insert // Test consolidation delta.consolidate(); // After consolidation, the first insert and delete should cancel out // leaving only the second insert assert_eq!(delta.len(), 1); let final_row = &delta.changes[0]; assert_eq!(final_row.0.rowid, 1); assert_eq!( final_row.0.values, vec![Value::from_i64(1), Value::from_i64(200)] ); assert_eq!(final_row.1, 1); } #[test] fn test_duplicate_row_consolidation() { let mut delta = Delta::new(); // Insert same row twice delta.insert(2, vec![Value::from_i64(2), Value::from_i64(300)]); delta.insert(2, vec![Value::from_i64(2), Value::from_i64(300)]); assert_eq!(delta.len(), 2); delta.consolidate(); assert_eq!(delta.len(), 1); // Weight should be 2 (sum of both inserts) let final_row = &delta.changes[0]; assert_eq!(final_row.0.rowid, 2); assert_eq!(final_row.1, 2); } } ================================================ FILE: core/incremental/expr_compiler.rs ================================================ // Expression compilation for incremental operators // This module provides utilities to compile SQL expressions into VDBE subprograms // that can be executed efficiently in the incremental computation context. use crate::numeric::Numeric; use crate::schema::Schema; use crate::storage::pager::Pager; use crate::sync::Arc; use crate::translate::emitter::Resolver; use crate::translate::expr::translate_expr; use crate::types::Text; use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts}; use crate::vdbe::insn::Insn; use crate::vdbe::{Program, ProgramState, Register}; use crate::{Connection, QueryMode, Result, Value}; use crate::{DatabaseCatalog, RwLock, SymbolTable}; use rustc_hash::FxHashMap as HashMap; use turso_parser::ast::{Expr, Literal, Operator}; // Transform an expression to replace column references with Register expressions Why do we want to // do this? // // Imagine you have a view like: // // create materialized view hex(count(*) + 2). translate_expr will usually try to find match names // to either literals or columns. But "count(*)" is not a column in any sqlite table. // // We *could* theoretically have a table-representation of every DBSP-step, but it is a lot simpler // to just pass registers as parameters to the VDBE expression, and teach translate_expr to // recognize those. // // But because the expression compiler will not generate those register inputs, we have to // transform the expression. fn transform_expr_for_dbsp(expr: &Expr, input_column_names: &[String]) -> Expr { match expr { // Transform column references (represented as Id) to Register expressions Expr::Id(name) => { // Check if this is a column name from our input if let Some(idx) = input_column_names .iter() .position(|col| col == name.as_str()) { // Replace with a Register expression Expr::Register(idx) } else { // Not a column reference, keep as is expr.clone() } } // Recursively transform nested expressions Expr::Binary(lhs, op, rhs) => Expr::Binary( Box::new(transform_expr_for_dbsp(lhs, input_column_names)), *op, Box::new(transform_expr_for_dbsp(rhs, input_column_names)), ), Expr::Unary(op, operand) => Expr::Unary( *op, Box::new(transform_expr_for_dbsp(operand, input_column_names)), ), Expr::FunctionCall { name, distinctness, args, order_by, filter_over, } => Expr::FunctionCall { name: name.clone(), distinctness: *distinctness, args: args .iter() .map(|arg| Box::new(transform_expr_for_dbsp(arg, input_column_names))) .collect(), order_by: order_by.clone(), filter_over: filter_over.clone(), }, Expr::Parenthesized(exprs) => Expr::Parenthesized( exprs .iter() .map(|e| Box::new(transform_expr_for_dbsp(e, input_column_names))) .collect(), ), // For other expression types, keep as is _ => expr.clone(), } } /// Enum to represent either a trivial or compiled expression #[derive(Clone)] pub enum ExpressionExecutor { /// Trivial expression that can be evaluated inline Trivial(TrivialExpression), /// Compiled VDBE program for complex expressions Compiled(Arc), } /// Trivial expression that can be evaluated inline without VDBE /// Supports arithmetic operations with automatic type promotion (integer to float) #[derive(Clone, Debug)] pub enum TrivialExpression { /// Direct column reference Column(usize), /// Immediate value Immediate(Value), /// Binary operation on trivial expressions (supports type promotion) Binary { left: Box, op: Operator, right: Box, }, } impl TrivialExpression { /// Evaluate the trivial expression with the given input values /// Automatically promotes integers to floats when mixing types in arithmetic pub fn evaluate(&self, values: &[Value]) -> Value { match self { TrivialExpression::Column(idx) => values.get(*idx).cloned().unwrap_or(Value::Null), TrivialExpression::Immediate(val) => val.clone(), TrivialExpression::Binary { left, op, right } => { let left_val = left.evaluate(values); let right_val = right.evaluate(values); // Use Value's exec_* methods which handle all type coercion // (including Text → Numeric) consistently with SQLite semantics match op { Operator::Add => left_val.exec_add(&right_val), Operator::Subtract => left_val.exec_subtract(&right_val), Operator::Multiply => left_val.exec_multiply(&right_val), Operator::Divide => left_val.exec_divide(&right_val), _ => panic!("Unsupported operator in trivial expression: {op:?}"), } } } } } /// Compiled expression that can be executed on row values #[derive(Clone)] pub struct CompiledExpression { /// The expression executor (trivial or compiled) pub executor: ExpressionExecutor, /// Number of input values expected (columns from the row) pub input_count: usize, } impl std::fmt::Debug for CompiledExpression { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut s = f.debug_struct("CompiledExpression"); s.field("input_count", &self.input_count); match &self.executor { ExpressionExecutor::Trivial(t) => s.field("executor", &format!("Trivial({t:?})")), ExpressionExecutor::Compiled(p) => { s.field("executor", &format!("Compiled({} insns)", p.insns.len())) } }; s.finish() } } #[derive(PartialEq)] enum TrivialType { Integer, Float, Text, Null, } impl CompiledExpression { /// Get the "type" of a trivial expression for type checking /// Returns None if type can't be determined statically fn get_trivial_type(expr: &TrivialExpression) -> Option { match expr { TrivialExpression::Column(_) => None, // Can't know column type statically TrivialExpression::Immediate(val) => match val { Value::Numeric(Numeric::Integer(_)) => Some(TrivialType::Integer), Value::Numeric(Numeric::Float(_)) => Some(TrivialType::Float), Value::Text(_) => Some(TrivialType::Text), Value::Null => Some(TrivialType::Null), _ => None, }, TrivialExpression::Binary { left, right, .. } => { // For binary ops, both sides must have the same type let left_type = Self::get_trivial_type(left)?; let right_type = Self::get_trivial_type(right)?; if left_type == right_type { Some(left_type) } else { None // Type mismatch } } } } // Validates if an expression is trivial (columns, immediates, and simple arithmetic) // Only considers expressions trivial if they don't require type coercion fn try_get_trivial_expr( expr: &Expr, input_column_names: &[String], ) -> Option { match expr { // Column reference or register Expr::Id(name) => input_column_names .iter() .position(|col| col == name.as_str()) .map(TrivialExpression::Column), Expr::Register(idx) => Some(TrivialExpression::Column(*idx)), // Immediate values Expr::Literal(lit) => { let value = match lit { Literal::Numeric(n) => { if let Ok(i) = n.parse::() { Value::from_i64(i) } else if let Ok(f) = n.parse::() { Value::from_f64(f) } else { return None; } } Literal::String(s) => { let cleaned = s.trim_matches('\'').trim_matches('"').to_string(); Value::Text(Text::new(cleaned)) } Literal::Null => Value::Null, _ => return None, }; Some(TrivialExpression::Immediate(value)) } // Binary operations with simple operators Expr::Binary(left, op, right) => { // Only support simple arithmetic operators match op { Operator::Add | Operator::Subtract | Operator::Multiply | Operator::Divide => { // Both operands must be trivial let left_trivial = Self::try_get_trivial_expr(left, input_column_names)?; let right_trivial = Self::try_get_trivial_expr(right, input_column_names)?; // Check if we can determine types statically // For arithmetic operations, we allow mixing integers and floats // since we promote integers to floats as needed if let (Some(left_type), Some(right_type)) = ( Self::get_trivial_type(&left_trivial), Self::get_trivial_type(&right_trivial), ) { // Both types are known - check if they're numeric or null let numeric_types = matches!( left_type, TrivialType::Integer | TrivialType::Float | TrivialType::Null ) && matches!( right_type, TrivialType::Integer | TrivialType::Float | TrivialType::Null ); if !numeric_types { return None; // Non-numeric types - not trivial } } // If we can't determine types (columns involved), we optimistically // assume they'll be compatible at runtime Some(TrivialExpression::Binary { left: Box::new(left_trivial), op: *op, right: Box::new(right_trivial), }) } _ => None, } } // Parenthesized expressions with single element Expr::Parenthesized(exprs) if exprs.len() == 1 => { Self::try_get_trivial_expr(&exprs[0], input_column_names) } _ => None, } } /// Compile a SQL expression into either a trivial executor or VDBE program /// /// For trivial expressions (columns, immediates, simple same-type arithmetic), uses inline evaluation. /// For complex expressions or those requiring type coercion, compiles to VDBE bytecode. pub fn compile( expr: &Expr, input_column_names: &[String], schema: &Schema, syms: &SymbolTable, connection: Arc, ) -> Result { let input_count = input_column_names.len(); // First, check if this is a trivial expression if let Some(trivial) = Self::try_get_trivial_expr(expr, input_column_names) { return Ok(CompiledExpression { executor: ExpressionExecutor::Trivial(trivial), input_count, }); } // Fall back to VDBE compilation for complex expressions // Create a minimal program builder for expression compilation let mut builder = ProgramBuilder::new( QueryMode::Normal, None, ProgramBuilderOpts { num_cursors: 0, approx_num_insns: 5, // Most expressions are simple approx_num_labels: 0, // Expressions don't need labels }, ); // Allocate registers for input values let input_count = input_column_names.len(); // Allocate input registers for _ in 0..input_count { builder.alloc_register(); } // Allocate a temp register for computation let temp_result_register = builder.alloc_register(); // Transform the expression to replace column references with Register expressions let transformed_expr = transform_expr_for_dbsp(expr, input_column_names); // Create a resolver for translate_expr let database_schemas = RwLock::new(HashMap::default()); let attached_databases = RwLock::new(DatabaseCatalog::new()); let resolver = Resolver::new(schema, &database_schemas, &attached_databases, syms, true); // Translate the transformed expression to bytecode translate_expr( &mut builder, None, // No table references needed for pure expressions &transformed_expr, temp_result_register, &resolver, )?; // Copy the result to register 0 for return builder.emit_insn(Insn::Copy { src_reg: temp_result_register, dst_reg: 0, extra_amount: 0, }); // Add a Halt instruction to complete the subprogram builder.emit_insn(Insn::Halt { err_code: 0, description: String::new(), on_error: None, description_reg: None, }); // Build the program from the compiled expression bytecode let program = Arc::new(builder.build(connection, false, "")?); Ok(CompiledExpression { executor: ExpressionExecutor::Compiled(program), input_count, }) } /// Execute the compiled expression with the given input values pub fn execute(&self, values: &[Value], pager: Arc) -> Result { match &self.executor { ExpressionExecutor::Trivial(trivial) => { // Fast path: evaluate trivial expression inline Ok(trivial.evaluate(values)) } ExpressionExecutor::Compiled(program) => { // Slow path: execute VDBE program // Create a state with the input values loaded into registers let mut state = ProgramState::new(program.max_registers, 0); // Load input values into registers assert_eq!( values.len(), self.input_count, "Mismatch in number of registers! Got {}, expected {}", values.len(), self.input_count ); for (idx, value) in values.iter().enumerate() { state.set_register(idx, Register::Value(value.clone())); } // Execute the program let mut pc = 0usize; while pc < program.insns.len() { let (insn, _) = &program.insns[pc]; let insn_fn = insn.to_function(); state.pc = pc as u32; // Execute the instruction match insn_fn(program, &mut state, insn, &pager)? { crate::vdbe::execute::InsnFunctionStepResult::IO(_) => { return Err(crate::LimboError::InternalError( "Expression evaluation encountered unexpected I/O".to_string(), )); } crate::vdbe::execute::InsnFunctionStepResult::Done => { break; } crate::vdbe::execute::InsnFunctionStepResult::Row => { return Err(crate::LimboError::InternalError( "Expression evaluation produced unexpected row".to_string(), )); } crate::vdbe::execute::InsnFunctionStepResult::Step => { pc = state.pc as usize; } } } // The compiled expression puts the result in register 0 match state.get_register(0) { Register::Value(v) => Ok(v.clone()), _ => Ok(Value::Null), } } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_mixed_type_arithmetic() { // Test integer - float let expr = TrivialExpression::Binary { left: Box::new(TrivialExpression::Immediate(Value::from_i64(1))), op: Operator::Subtract, right: Box::new(TrivialExpression::Immediate(Value::from_f64(0.5))), }; let result = expr.evaluate(&[]); assert_eq!(result, Value::from_f64(0.5)); // Test float - integer let expr = TrivialExpression::Binary { left: Box::new(TrivialExpression::Immediate(Value::from_f64(2.5))), op: Operator::Subtract, right: Box::new(TrivialExpression::Immediate(Value::from_i64(1))), }; let result = expr.evaluate(&[]); assert_eq!(result, Value::from_f64(1.5)); // Test integer * float let expr = TrivialExpression::Binary { left: Box::new(TrivialExpression::Immediate(Value::from_i64(10))), op: Operator::Multiply, right: Box::new(TrivialExpression::Immediate(Value::from_f64(0.1))), }; let result = expr.evaluate(&[]); assert_eq!(result, Value::from_f64(1.0)); // Test integer / float let expr = TrivialExpression::Binary { left: Box::new(TrivialExpression::Immediate(Value::from_i64(1))), op: Operator::Divide, right: Box::new(TrivialExpression::Immediate(Value::from_f64(2.0))), }; let result = expr.evaluate(&[]); assert_eq!(result, Value::from_f64(0.5)); // Test integer + float let expr = TrivialExpression::Binary { left: Box::new(TrivialExpression::Immediate(Value::from_i64(1))), op: Operator::Add, right: Box::new(TrivialExpression::Immediate(Value::from_f64(0.5))), }; let result = expr.evaluate(&[]); assert_eq!(result, Value::from_f64(1.5)); } #[test] fn test_nested_mixed_type_expressions() { // Test nested expressions with mixed types: (1 - 0.04) let one_minus_float = TrivialExpression::Binary { left: Box::new(TrivialExpression::Immediate(Value::from_i64(1))), op: Operator::Subtract, right: Box::new(TrivialExpression::Immediate(Value::from_f64(0.04))), }; let result = one_minus_float.evaluate(&[]); assert_eq!(result, Value::from_f64(0.96)); // Test multiplication with nested mixed-type expression: 100.0 * (1 - 0.04) let nested_expr = TrivialExpression::Binary { left: Box::new(TrivialExpression::Immediate(Value::from_f64(100.0))), op: Operator::Multiply, right: Box::new(one_minus_float), }; let result = nested_expr.evaluate(&[]); assert_eq!(result, Value::from_f64(96.0)); } #[test] fn test_text_to_numeric_coercion_in_arithmetic() { // Non-numeric text should coerce to 0 (SQLite behavior) let values = vec![Value::Text(Text::new("hello".to_string()))]; // text - 1 => 0 - 1 = -1 let expr = TrivialExpression::Binary { left: Box::new(TrivialExpression::Column(0)), op: Operator::Subtract, right: Box::new(TrivialExpression::Immediate(Value::from_i64(1))), }; assert_eq!(expr.evaluate(&values), Value::from_i64(-1)); // text + 1 => 0 + 1 = 1 let expr = TrivialExpression::Binary { left: Box::new(TrivialExpression::Column(0)), op: Operator::Add, right: Box::new(TrivialExpression::Immediate(Value::from_i64(1))), }; assert_eq!(expr.evaluate(&values), Value::from_i64(1)); // text * 2 => 0 * 2 = 0 let expr = TrivialExpression::Binary { left: Box::new(TrivialExpression::Column(0)), op: Operator::Multiply, right: Box::new(TrivialExpression::Immediate(Value::from_i64(2))), }; assert_eq!(expr.evaluate(&values), Value::from_i64(0)); // text / 2 => 0 / 2 = 0 let expr = TrivialExpression::Binary { left: Box::new(TrivialExpression::Column(0)), op: Operator::Divide, right: Box::new(TrivialExpression::Immediate(Value::from_i64(2))), }; assert_eq!(expr.evaluate(&values), Value::from_i64(0)); // Numeric text "42" - 1 => 41 let numeric_text_values = vec![Value::Text(Text::new("42".to_string()))]; let expr = TrivialExpression::Binary { left: Box::new(TrivialExpression::Column(0)), op: Operator::Subtract, right: Box::new(TrivialExpression::Immediate(Value::from_i64(1))), }; assert_eq!(expr.evaluate(&numeric_text_values), Value::from_i64(41)); } } ================================================ FILE: core/incremental/filter_operator.rs ================================================ #![allow(dead_code)] // Filter operator for DBSP-style incremental computation // This operator filters rows based on predicates use crate::incremental::dbsp::{Delta, DeltaPair}; use crate::incremental::operator::{ ComputationTracker, DbspStateCursors, EvalState, IncrementalOperator, }; use crate::sync::Arc; use crate::sync::Mutex; use crate::types::IOResult; use crate::{Result, Value}; use std::cmp::Ordering; /// Filter predicate for filtering rows #[derive(Debug, Clone)] pub enum FilterPredicate { /// Column = value (using column index) Equals { column_idx: usize, value: Value }, /// Column != value (using column index) NotEquals { column_idx: usize, value: Value }, /// Column > value (using column index) GreaterThan { column_idx: usize, value: Value }, /// Column >= value (using column index) GreaterThanOrEqual { column_idx: usize, value: Value }, /// Column < value (using column index) LessThan { column_idx: usize, value: Value }, /// Column <= value (using column index) LessThanOrEqual { column_idx: usize, value: Value }, /// Column = Column comparisons ColumnEquals { left_idx: usize, right_idx: usize }, /// Column != Column comparisons ColumnNotEquals { left_idx: usize, right_idx: usize }, /// Column > Column comparisons ColumnGreaterThan { left_idx: usize, right_idx: usize }, /// Column >= Column comparisons ColumnGreaterThanOrEqual { left_idx: usize, right_idx: usize }, /// Column < Column comparisons ColumnLessThan { left_idx: usize, right_idx: usize }, /// Column <= Column comparisons ColumnLessThanOrEqual { left_idx: usize, right_idx: usize }, /// Column IS NULL check IsNull { column_idx: usize }, /// Column IS NOT NULL check IsNotNull { column_idx: usize }, /// Logical AND of two predicates And(Box, Box), /// Logical OR of two predicates Or(Box, Box), /// No predicate (accept all rows) None, } /// Filter operator - filters rows based on predicate #[derive(Debug)] pub struct FilterOperator { predicate: FilterPredicate, tracker: Option>>, } impl FilterOperator { pub fn new(predicate: FilterPredicate) -> Self { Self { predicate, tracker: None, } } /// Get the predicate for this filter pub fn predicate(&self) -> &FilterPredicate { &self.predicate } pub fn evaluate_predicate(&self, values: &[Value]) -> bool { match &self.predicate { FilterPredicate::None => true, FilterPredicate::Equals { column_idx, value } => { let v = &values[*column_idx]; v == value } FilterPredicate::NotEquals { column_idx, value } => { let v = &values[*column_idx]; v != value } FilterPredicate::GreaterThan { column_idx, value } => { let v = &values[*column_idx]; v.cmp(value) == Ordering::Greater } FilterPredicate::GreaterThanOrEqual { column_idx, value } => { let v = &values[*column_idx]; v.cmp(value) != Ordering::Less } FilterPredicate::LessThan { column_idx, value } => { let v = &values[*column_idx]; v.cmp(value) == Ordering::Less } FilterPredicate::LessThanOrEqual { column_idx, value } => { let v = &values[*column_idx]; v.cmp(value) != Ordering::Greater } FilterPredicate::And(left, right) => { // Temporarily create sub-filters to evaluate let left_filter = FilterOperator::new((**left).clone()); let right_filter = FilterOperator::new((**right).clone()); left_filter.evaluate_predicate(values) && right_filter.evaluate_predicate(values) } FilterPredicate::Or(left, right) => { let left_filter = FilterOperator::new((**left).clone()); let right_filter = FilterOperator::new((**right).clone()); left_filter.evaluate_predicate(values) || right_filter.evaluate_predicate(values) } FilterPredicate::ColumnEquals { left_idx, right_idx, } => { let left = &values[*left_idx]; let right = &values[*right_idx]; left == right } FilterPredicate::ColumnNotEquals { left_idx, right_idx, } => { let left = &values[*left_idx]; let right = &values[*right_idx]; left != right } FilterPredicate::ColumnGreaterThan { left_idx, right_idx, } => { let left = &values[*left_idx]; let right = &values[*right_idx]; left.cmp(right) == Ordering::Greater } FilterPredicate::ColumnGreaterThanOrEqual { left_idx, right_idx, } => { let left = &values[*left_idx]; let right = &values[*right_idx]; left.cmp(right) != Ordering::Less } FilterPredicate::ColumnLessThan { left_idx, right_idx, } => { let left = &values[*left_idx]; let right = &values[*right_idx]; left.cmp(right) == Ordering::Less } FilterPredicate::ColumnLessThanOrEqual { left_idx, right_idx, } => { let left = &values[*left_idx]; let right = &values[*right_idx]; left.cmp(right) != Ordering::Greater } FilterPredicate::IsNull { column_idx } => { matches!(values[*column_idx], Value::Null) } FilterPredicate::IsNotNull { column_idx } => { !matches!(values[*column_idx], Value::Null) } } } } impl IncrementalOperator for FilterOperator { fn eval( &mut self, state: &mut EvalState, _cursors: &mut DbspStateCursors, ) -> Result> { let delta = match state { EvalState::Init { deltas } => { // Filter operators only use left_delta, right_delta must be empty assert!( deltas.right.is_empty(), "FilterOperator expects right_delta to be empty" ); std::mem::take(&mut deltas.left) } _ => unreachable!( "FilterOperator doesn't execute the state machine. Should be in Init state" ), }; let mut output_delta = Delta::new(); // Process the delta through the filter for (row, weight) in delta.changes { if let Some(tracker) = &self.tracker { tracker.lock().record_filter(); } // Only pass through rows that satisfy the filter predicate // For deletes (weight < 0), we only pass them if the row values // would have passed the filter (meaning it was in the view) if self.evaluate_predicate(&row.values) { output_delta.changes.push((row, weight)); } } *state = EvalState::Done; Ok(IOResult::Done(output_delta)) } fn commit( &mut self, deltas: DeltaPair, _cursors: &mut DbspStateCursors, ) -> Result> { // Filter operator only uses left delta, right must be empty assert!( deltas.right.is_empty(), "FilterOperator expects right delta to be empty in commit" ); let mut output_delta = Delta::new(); // Commit the delta to our internal state // Only pass through and track rows that satisfy the filter predicate for (row, weight) in deltas.left.changes { if let Some(tracker) = &self.tracker { tracker.lock().record_filter(); } // Only track and output rows that pass the filter // For deletes, this means the row was in the view (its values pass the filter) // For inserts, this means the row should be in the view if self.evaluate_predicate(&row.values) { output_delta.changes.push((row, weight)); } } Ok(IOResult::Done(output_delta)) } fn set_tracker(&mut self, tracker: Arc>) { self.tracker = Some(tracker); } } #[cfg(test)] mod tests { use super::*; use crate::types::Text; #[test] fn test_is_null_predicate() { let predicate = FilterPredicate::IsNull { column_idx: 1 }; let filter = FilterOperator::new(predicate); // Test with NULL value let values_with_null = vec![ Value::from_i64(1), Value::Null, Value::Text(Text::from("test")), ]; assert!(filter.evaluate_predicate(&values_with_null)); // Test with non-NULL value let values_without_null = vec![ Value::from_i64(1), Value::from_i64(42), Value::Text(Text::from("test")), ]; assert!(!filter.evaluate_predicate(&values_without_null)); // Test with different non-NULL types let values_with_text = vec![ Value::from_i64(1), Value::Text(Text::from("not null")), Value::Text(Text::from("test")), ]; assert!(!filter.evaluate_predicate(&values_with_text)); let values_with_blob = vec![ Value::from_i64(1), Value::Blob(vec![1, 2, 3]), Value::Text(Text::from("test")), ]; assert!(!filter.evaluate_predicate(&values_with_blob)); } #[test] fn test_is_not_null_predicate() { let predicate = FilterPredicate::IsNotNull { column_idx: 1 }; let filter = FilterOperator::new(predicate); // Test with NULL value let values_with_null = vec![ Value::from_i64(1), Value::Null, Value::Text(Text::from("test")), ]; assert!(!filter.evaluate_predicate(&values_with_null)); // Test with non-NULL value (Integer) let values_with_integer = vec![ Value::from_i64(1), Value::from_i64(42), Value::Text(Text::from("test")), ]; assert!(filter.evaluate_predicate(&values_with_integer)); // Test with non-NULL value (Text) let values_with_text = vec![ Value::from_i64(1), Value::Text(Text::from("not null")), Value::Text(Text::from("test")), ]; assert!(filter.evaluate_predicate(&values_with_text)); // Test with non-NULL value (Blob) let values_with_blob = vec![ Value::from_i64(1), Value::Blob(vec![1, 2, 3]), Value::Text(Text::from("test")), ]; assert!(filter.evaluate_predicate(&values_with_blob)); } #[test] fn test_is_null_with_and() { // Test: column_0 = 1 AND column_1 IS NULL let predicate = FilterPredicate::And( Box::new(FilterPredicate::Equals { column_idx: 0, value: Value::from_i64(1), }), Box::new(FilterPredicate::IsNull { column_idx: 1 }), ); let filter = FilterOperator::new(predicate); // Should match: column_0 = 1 AND column_1 IS NULL let values_match = vec![ Value::from_i64(1), Value::Null, Value::Text(Text::from("test")), ]; assert!(filter.evaluate_predicate(&values_match)); // Should not match: column_0 = 2 AND column_1 IS NULL let values_wrong_first = vec![ Value::from_i64(2), Value::Null, Value::Text(Text::from("test")), ]; assert!(!filter.evaluate_predicate(&values_wrong_first)); // Should not match: column_0 = 1 AND column_1 IS NOT NULL let values_not_null = vec![ Value::from_i64(1), Value::from_i64(42), Value::Text(Text::from("test")), ]; assert!(!filter.evaluate_predicate(&values_not_null)); } #[test] fn test_is_not_null_with_or() { // Test: column_0 = 1 OR column_1 IS NOT NULL let predicate = FilterPredicate::Or( Box::new(FilterPredicate::Equals { column_idx: 0, value: Value::from_i64(1), }), Box::new(FilterPredicate::IsNotNull { column_idx: 1 }), ); let filter = FilterOperator::new(predicate); // Should match: column_0 = 1 (regardless of column_1) let values_first_matches = vec![ Value::from_i64(1), Value::Null, Value::Text(Text::from("test")), ]; assert!(filter.evaluate_predicate(&values_first_matches)); // Should match: column_1 IS NOT NULL (regardless of column_0) let values_second_matches = vec![ Value::from_i64(2), Value::from_i64(42), Value::Text(Text::from("test")), ]; assert!(filter.evaluate_predicate(&values_second_matches)); // Should not match: column_0 != 1 AND column_1 IS NULL let values_no_match = vec![ Value::from_i64(2), Value::Null, Value::Text(Text::from("test")), ]; assert!(!filter.evaluate_predicate(&values_no_match)); } #[test] fn test_complex_null_predicates() { // Test: (column_0 IS NULL OR column_1 IS NOT NULL) AND column_2 = 'test' let predicate = FilterPredicate::And( Box::new(FilterPredicate::Or( Box::new(FilterPredicate::IsNull { column_idx: 0 }), Box::new(FilterPredicate::IsNotNull { column_idx: 1 }), )), Box::new(FilterPredicate::Equals { column_idx: 2, value: Value::Text(Text::from("test")), }), ); let filter = FilterOperator::new(predicate); // Should match: column_0 IS NULL, column_2 = 'test' let values1 = vec![Value::Null, Value::Null, Value::Text(Text::from("test"))]; assert!(filter.evaluate_predicate(&values1)); // Should match: column_1 IS NOT NULL, column_2 = 'test' let values2 = vec![ Value::from_i64(1), Value::from_i64(42), Value::Text(Text::from("test")), ]; assert!(filter.evaluate_predicate(&values2)); // Should not match: column_2 != 'test' let values3 = vec![ Value::Null, Value::from_i64(42), Value::Text(Text::from("other")), ]; assert!(!filter.evaluate_predicate(&values3)); // Should not match: column_0 IS NOT NULL AND column_1 IS NULL AND column_2 = 'test' let values4 = vec![ Value::from_i64(1), Value::Null, Value::Text(Text::from("test")), ]; assert!(!filter.evaluate_predicate(&values4)); } #[test] fn test_cross_type_numeric_comparisons() { // GreaterThan: Integer > Float let predicate = FilterPredicate::GreaterThan { column_idx: 0, value: Value::from_f64(1.5), }; let filter = FilterOperator::new(predicate); assert!(filter.evaluate_predicate(&[Value::from_i64(2)])); // 2 > 1.5 assert!(!filter.evaluate_predicate(&[Value::from_i64(1)])); // 1 > 1.5 // GreaterThan: Float > Integer let predicate = FilterPredicate::GreaterThan { column_idx: 0, value: Value::from_i64(2), }; let filter = FilterOperator::new(predicate); assert!(filter.evaluate_predicate(&[Value::from_f64(2.5)])); // 2.5 > 2 assert!(!filter.evaluate_predicate(&[Value::from_f64(1.5)])); // 1.5 > 2 // GreaterThanOrEqual: Integer >= Float let predicate = FilterPredicate::GreaterThanOrEqual { column_idx: 0, value: Value::from_f64(2.0), }; let filter = FilterOperator::new(predicate); assert!(filter.evaluate_predicate(&[Value::from_i64(2)])); // 2 >= 2.0 assert!(filter.evaluate_predicate(&[Value::from_i64(3)])); // 3 >= 2.0 assert!(!filter.evaluate_predicate(&[Value::from_i64(1)])); // 1 >= 2.0 // GreaterThanOrEqual: Float >= Integer let predicate = FilterPredicate::GreaterThanOrEqual { column_idx: 0, value: Value::from_i64(2), }; let filter = FilterOperator::new(predicate); assert!(filter.evaluate_predicate(&[Value::from_f64(2.0)])); // 2.0 >= 2 assert!(!filter.evaluate_predicate(&[Value::from_f64(1.9)])); // 1.9 >= 2 // LessThan: Integer < Float let predicate = FilterPredicate::LessThan { column_idx: 0, value: Value::from_f64(1.5), }; let filter = FilterOperator::new(predicate); assert!(filter.evaluate_predicate(&[Value::from_i64(1)])); // 1 < 1.5 assert!(!filter.evaluate_predicate(&[Value::from_i64(2)])); // 2 < 1.5 // LessThan: Float < Integer let predicate = FilterPredicate::LessThan { column_idx: 0, value: Value::from_i64(2), }; let filter = FilterOperator::new(predicate); assert!(filter.evaluate_predicate(&[Value::from_f64(1.5)])); // 1.5 < 2 assert!(!filter.evaluate_predicate(&[Value::from_f64(2.5)])); // 2.5 < 2 // LessThanOrEqual: Integer <= Float let predicate = FilterPredicate::LessThanOrEqual { column_idx: 0, value: Value::from_f64(2.0), }; let filter = FilterOperator::new(predicate); assert!(filter.evaluate_predicate(&[Value::from_i64(2)])); // 2 <= 2.0 assert!(filter.evaluate_predicate(&[Value::from_i64(1)])); // 1 <= 2.0 assert!(!filter.evaluate_predicate(&[Value::from_i64(3)])); // 3 <= 2.0 // LessThanOrEqual: Float <= Integer let predicate = FilterPredicate::LessThanOrEqual { column_idx: 0, value: Value::from_i64(2), }; let filter = FilterOperator::new(predicate); assert!(filter.evaluate_predicate(&[Value::from_f64(2.0)])); // 2.0 <= 2 assert!(!filter.evaluate_predicate(&[Value::from_f64(2.1)])); // 2.1 <= 2 } } ================================================ FILE: core/incremental/input_operator.rs ================================================ // Input operator for DBSP-style incremental computation // This operator serves as the entry point for data into the incremental computation pipeline use crate::incremental::dbsp::{Delta, DeltaPair}; use crate::incremental::operator::{ ComputationTracker, DbspStateCursors, EvalState, IncrementalOperator, }; use crate::sync::Arc; use crate::sync::Mutex; use crate::types::IOResult; use crate::Result; /// Input operator - source of data for the circuit /// Represents base relations/tables that receive external updates #[derive(Debug)] pub struct InputOperator { #[allow(dead_code)] name: String, } impl InputOperator { pub fn new(name: String) -> Self { Self { name } } } impl IncrementalOperator for InputOperator { fn eval( &mut self, state: &mut EvalState, _cursors: &mut DbspStateCursors, ) -> Result> { match state { EvalState::Init { deltas } => { // Input operators only use left_delta, right_delta must be empty assert!( deltas.right.is_empty(), "InputOperator expects right_delta to be empty" ); let output = std::mem::take(&mut deltas.left); *state = EvalState::Done; Ok(IOResult::Done(output)) } _ => unreachable!( "InputOperator doesn't execute the state machine. Should be in Init state" ), } } fn commit( &mut self, deltas: DeltaPair, _cursors: &mut DbspStateCursors, ) -> Result> { // Input operator only uses left delta, right must be empty assert!( deltas.right.is_empty(), "InputOperator expects right delta to be empty in commit" ); // Input operator passes through the delta unchanged during commit Ok(IOResult::Done(deltas.left)) } fn set_tracker(&mut self, _tracker: Arc>) { // Input operator doesn't need tracking } } ================================================ FILE: core/incremental/join_operator.rs ================================================ #![allow(dead_code)] use crate::incremental::dbsp::Hash128; use crate::incremental::dbsp::{Delta, DeltaPair, HashableRow}; use crate::incremental::operator::{ generate_storage_id, ComputationTracker, DbspStateCursors, EvalState, IncrementalOperator, }; use crate::incremental::persistence::WriteRow; use crate::numeric::Numeric; use crate::storage::btree::CursorTrait; use crate::sync::Arc; use crate::sync::Mutex; use crate::types::{IOResult, ImmutableRecord, SeekKey, SeekOp, SeekResult}; use crate::{return_and_restore_if_io, return_if_io, Result, Value}; #[derive(Debug, Clone, PartialEq)] pub enum JoinType { Inner, Left, Right, Full, Cross, } // Helper function to read the next row from the BTree for joins fn read_next_join_row( storage_id: i64, join_key: &HashableRow, last_element_hash: Option, cursors: &mut DbspStateCursors, ) -> Result>> { // Build the index key: (storage_id, zset_id, element_id) // zset_id is the hash of the join key let zset_hash = join_key.cached_hash(); // For iteration, use the last element hash if we have one, or NULL to start let index_key_values = match last_element_hash { Some(last_hash) => vec![ Value::from_i64(storage_id), zset_hash.to_value(), last_hash.to_value(), ], None => vec![ Value::from_i64(storage_id), zset_hash.to_value(), Value::Null, // Start iteration from beginning ], }; let index_record = ImmutableRecord::from_values(&index_key_values, index_key_values.len()); // Use GE (>=) for initial seek with NULL, GT (>) for continuation let seek_op = if last_element_hash.is_none() { SeekOp::GE { eq_only: false } } else { SeekOp::GT }; let seek_result = return_if_io!(cursors .index_cursor .seek(SeekKey::IndexKey(&index_record), seek_op)); if !matches!(seek_result, SeekResult::Found) { return Ok(IOResult::Done(None)); } // Check if we're still in the same (storage_id, zset_id) range let current_record = return_if_io!(cursors.index_cursor.record()); // Extract all needed values from the record before dropping it let (found_storage_id, found_zset_hash, element_hash) = if let Some(rec) = current_record { let values = rec.get_three_values(0, 1, 2); // Index has 4 values: storage_id, zset_id, element_id, rowid (appended by WriteRow) if let Ok((v0, v1, v2)) = values { let found_storage_id = match &v0.to_owned() { Value::Numeric(Numeric::Integer(id)) => *id, _ => return Ok(IOResult::Done(None)), }; let found_zset_hash = match &v1.to_owned() { Value::Blob(blob) => Hash128::from_blob(blob).ok_or_else(|| { crate::LimboError::InternalError("Invalid zset_hash blob".to_string()) })?, _ => return Ok(IOResult::Done(None)), }; let element_hash = match &v2.to_owned() { Value::Blob(blob) => Hash128::from_blob(blob).ok_or_else(|| { crate::LimboError::InternalError("Invalid element_hash blob".to_string()) })?, _ => { return Ok(IOResult::Done(None)); } }; (found_storage_id, found_zset_hash, element_hash) } else { return Ok(IOResult::Done(None)); } } else { return Ok(IOResult::Done(None)); }; // Now we can safely check if we're in the right range // If we've moved to a different storage_id or zset_id, we're done if found_storage_id != storage_id || found_zset_hash != zset_hash { return Ok(IOResult::Done(None)); } // Now get the actual row from the table using the rowid from the index let rowid = return_if_io!(cursors.index_cursor.rowid()); if let Some(rowid) = rowid { return_if_io!(cursors .table_cursor .seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true })); let table_record = return_if_io!(cursors.table_cursor.record()); if let Some(rec) = table_record { let table_values = rec.get_two_values(3, 4); // Table format: [storage_id, zset_id, element_id, value_blob, weight] if let Ok((value_at_3, value_at_4)) = table_values { // Deserialize the row from the blob let value_at_3 = value_at_3.to_owned(); let blob = match value_at_3 { Value::Blob(ref b) => b, _ => return Ok(IOResult::Done(None)), }; // The blob contains the serialized HashableRow // For now, let's deserialize it simply let row = deserialize_hashable_row(blob)?; let weight = match &value_at_4.to_owned() { Value::Numeric(Numeric::Integer(w)) => *w as isize, _ => return Ok(IOResult::Done(None)), }; return Ok(IOResult::Done(Some((element_hash, row, weight)))); } } } Ok(IOResult::Done(None)) } // Join-specific eval states #[derive(Debug)] pub enum JoinEvalState { ProcessDeltaJoin { deltas: DeltaPair, output: Delta, }, ProcessLeftJoin { deltas: DeltaPair, output: Delta, current_idx: usize, last_row_scanned: Option, }, ProcessRightJoin { deltas: DeltaPair, output: Delta, current_idx: usize, last_row_scanned: Option, }, Done { output: Delta, }, } impl JoinEvalState { fn combine_rows( left_row: &HashableRow, left_weight: i64, right_row: &HashableRow, right_weight: i64, output: &mut Delta, ) { // Combine the rows let mut combined_values = left_row.values.clone(); combined_values.extend(right_row.values.clone()); // Use hash of combined values as synthetic rowid let temp_row = HashableRow::new(0, combined_values.clone()); let joined_rowid = temp_row.cached_hash().as_i64(); let joined_row = HashableRow::new(joined_rowid, combined_values); // Add to output with combined weight let combined_weight = left_weight * right_weight; output.changes.push((joined_row, combined_weight as isize)); } fn process_join_state( &mut self, cursors: &mut DbspStateCursors, left_key_indices: &[usize], right_key_indices: &[usize], left_storage_id: i64, right_storage_id: i64, ) -> Result> { loop { match self { JoinEvalState::ProcessDeltaJoin { deltas, output } => { // Move to ProcessLeftJoin *self = JoinEvalState::ProcessLeftJoin { deltas: std::mem::take(deltas), output: std::mem::take(output), current_idx: 0, last_row_scanned: None, }; } JoinEvalState::ProcessLeftJoin { deltas, output, current_idx, last_row_scanned, } => { if *current_idx >= deltas.left.changes.len() { *self = JoinEvalState::ProcessRightJoin { deltas: std::mem::take(deltas), output: std::mem::take(output), current_idx: 0, last_row_scanned: None, }; } else { let (left_row, left_weight) = &deltas.left.changes[*current_idx]; // Extract join key using provided indices let key_values: Vec = left_key_indices .iter() .map(|&idx| left_row.values.get(idx).cloned().unwrap_or(Value::Null)) .collect(); let left_key = HashableRow::new(0, key_values); let next_row = return_if_io!(read_next_join_row( right_storage_id, &left_key, *last_row_scanned, cursors )); match next_row { Some((element_hash, right_row, right_weight)) => { Self::combine_rows( left_row, (*left_weight) as i64, &right_row, right_weight as i64, output, ); // Continue scanning with this left row *self = JoinEvalState::ProcessLeftJoin { deltas: std::mem::take(deltas), output: std::mem::take(output), current_idx: *current_idx, last_row_scanned: Some(element_hash), }; } None => { // No more matches for this left row, move to next *self = JoinEvalState::ProcessLeftJoin { deltas: std::mem::take(deltas), output: std::mem::take(output), current_idx: *current_idx + 1, last_row_scanned: None, }; } } } } JoinEvalState::ProcessRightJoin { deltas, output, current_idx, last_row_scanned, } => { if *current_idx >= deltas.right.changes.len() { *self = JoinEvalState::Done { output: std::mem::take(output), }; } else { let (right_row, right_weight) = &deltas.right.changes[*current_idx]; // Extract join key using provided indices let key_values: Vec = right_key_indices .iter() .map(|&idx| right_row.values.get(idx).cloned().unwrap_or(Value::Null)) .collect(); let right_key = HashableRow::new(0, key_values); let next_row = return_if_io!(read_next_join_row( left_storage_id, &right_key, *last_row_scanned, cursors )); match next_row { Some((element_hash, left_row, left_weight)) => { Self::combine_rows( &left_row, left_weight as i64, right_row, (*right_weight) as i64, output, ); // Continue scanning with this right row *self = JoinEvalState::ProcessRightJoin { deltas: std::mem::take(deltas), output: std::mem::take(output), current_idx: *current_idx, last_row_scanned: Some(element_hash), }; } None => { // No more matches for this right row, move to next *self = JoinEvalState::ProcessRightJoin { deltas: std::mem::take(deltas), output: std::mem::take(output), current_idx: *current_idx + 1, last_row_scanned: None, }; } } } } JoinEvalState::Done { output } => { return Ok(IOResult::Done(std::mem::take(output))); } } } } } #[derive(Debug)] enum JoinCommitState { Idle, Eval { eval_state: EvalState, }, CommitLeftDelta { deltas: DeltaPair, output: Delta, current_idx: usize, write_row: WriteRow, }, CommitRightDelta { deltas: DeltaPair, output: Delta, current_idx: usize, write_row: WriteRow, }, Invalid, } /// Join operator - performs incremental join between two relations /// Implements the DBSP formula: δ(R ⋈ S) = (δR ⋈ S) ∪ (R ⋈ δS) ∪ (δR ⋈ δS) #[derive(Debug)] pub struct JoinOperator { /// Unique operator ID for indexing in persistent storage operator_id: i64, /// Type of join to perform join_type: JoinType, /// Column indices for extracting join keys from left input left_key_indices: Vec, /// Column indices for extracting join keys from right input right_key_indices: Vec, /// Column names from left input left_columns: Vec, /// Column names from right input right_columns: Vec, /// Tracker for computation statistics tracker: Option>>, commit_state: JoinCommitState, } impl JoinOperator { pub fn new( operator_id: i64, join_type: JoinType, left_key_indices: Vec, right_key_indices: Vec, left_columns: Vec, right_columns: Vec, ) -> Result { // Check for unsupported join types match join_type { JoinType::Left => { return Err(crate::LimboError::ParseError( "LEFT OUTER JOIN is not yet supported in incremental views".to_string(), )) } JoinType::Right => { return Err(crate::LimboError::ParseError( "RIGHT OUTER JOIN is not yet supported in incremental views".to_string(), )) } JoinType::Full => { return Err(crate::LimboError::ParseError( "FULL OUTER JOIN is not yet supported in incremental views".to_string(), )) } JoinType::Cross => { return Err(crate::LimboError::ParseError( "CROSS JOIN is not yet supported in incremental views".to_string(), )) } JoinType::Inner => {} // Inner join is supported } let result = Self { operator_id, join_type, left_key_indices, right_key_indices, left_columns, right_columns, tracker: None, commit_state: JoinCommitState::Idle, }; Ok(result) } /// Extract join key from row values using the specified indices fn extract_join_key(&self, values: &[Value], indices: &[usize]) -> HashableRow { let key_values: Vec = indices .iter() .map(|&idx| values.get(idx).cloned().unwrap_or(Value::Null)) .collect(); // Use 0 as a dummy rowid for join keys. They don't come from a table, // so they don't need a rowid. Their key will be the hash of the row values. HashableRow::new(0, key_values) } /// Generate storage ID for left table fn left_storage_id(&self) -> i64 { // Use column_index=0 for left side generate_storage_id(self.operator_id, 0, 0) } /// Generate storage ID for right table fn right_storage_id(&self) -> i64 { // Use column_index=1 for right side generate_storage_id(self.operator_id, 1, 0) } /// SQL-compliant comparison for join keys /// Returns true if keys match according to SQL semantics (NULL != NULL) fn sql_keys_equal(left_key: &HashableRow, right_key: &HashableRow) -> bool { if left_key.values.len() != right_key.values.len() { return false; } for (left_val, right_val) in left_key.values.iter().zip(right_key.values.iter()) { // In SQL, NULL never equals NULL if matches!(left_val, Value::Null) || matches!(right_val, Value::Null) { return false; } // For non-NULL values, use regular comparison if left_val != right_val { return false; } } true } fn process_join_state( &mut self, state: &mut EvalState, cursors: &mut DbspStateCursors, ) -> Result> { // Get the join state out of the enum match state { EvalState::Join(js) => js.process_join_state( cursors, &self.left_key_indices, &self.right_key_indices, self.left_storage_id(), self.right_storage_id(), ), _ => panic!("process_join_state called with non-join state"), } } fn eval_internal( &mut self, state: &mut EvalState, cursors: &mut DbspStateCursors, ) -> Result> { loop { let loop_state = std::mem::replace(state, EvalState::Uninitialized); match loop_state { EvalState::Uninitialized => { panic!("Cannot eval JoinOperator with Uninitialized state"); } EvalState::Init { deltas } => { let mut output = Delta::new(); // Component 3: δR ⋈ δS (left delta join right delta) for (left_row, left_weight) in &deltas.left.changes { let left_key = self.extract_join_key(&left_row.values, &self.left_key_indices); for (right_row, right_weight) in &deltas.right.changes { let right_key = self.extract_join_key(&right_row.values, &self.right_key_indices); if Self::sql_keys_equal(&left_key, &right_key) { if let Some(tracker) = &self.tracker { tracker.lock().record_join_lookup(); } // Combine the rows let mut combined_values = left_row.values.clone(); combined_values.extend(right_row.values.clone()); // Create the joined row with a unique rowid // Use hash of the combined values to ensure uniqueness // Use hash of combined values as synthetic rowid let temp_row = HashableRow::new(0, combined_values.clone()); let joined_rowid = temp_row.cached_hash().as_i64(); let joined_row = HashableRow::new(joined_rowid, combined_values.clone()); // Add to output with combined weight let combined_weight = left_weight * right_weight; output.changes.push((joined_row, combined_weight)); } } } *state = EvalState::Join(Box::new(JoinEvalState::ProcessDeltaJoin { deltas, output, })); } EvalState::Join(join_state) => { *state = EvalState::Join(join_state); let output = return_if_io!(self.process_join_state(state, cursors)); return Ok(IOResult::Done(output)); } EvalState::Done => { return Ok(IOResult::Done(Delta::new())); } EvalState::Aggregate(_) => { panic!("Aggregate state should not appear in join operator"); } } } } } fn deserialize_hashable_row(blob: &[u8]) -> Result { use crate::types::ImmutableRecord; let record = ImmutableRecord::from_bin_record(blob.to_vec()); let all_values: Vec = record.get_values_owned()?; if all_values.is_empty() { return Err(crate::LimboError::InternalError( "HashableRow blob must contain at least rowid".to_string(), )); } // First value is the rowid let rowid = match &all_values[0] { Value::Numeric(Numeric::Integer(i)) => *i, _ => { return Err(crate::LimboError::InternalError( "First value must be rowid (integer)".to_string(), )) } }; // Rest are the row values let values = all_values[1..].to_vec(); Ok(HashableRow::new(rowid, values)) } fn serialize_hashable_row(row: &HashableRow) -> Vec { use crate::types::ImmutableRecord; let mut all_values = Vec::with_capacity(row.values.len() + 1); all_values.push(Value::from_i64(row.rowid)); all_values.extend_from_slice(&row.values); let record = ImmutableRecord::from_values(&all_values, all_values.len()); record.as_blob().clone() } impl IncrementalOperator for JoinOperator { fn eval( &mut self, state: &mut EvalState, cursors: &mut DbspStateCursors, ) -> Result> { let delta = return_if_io!(self.eval_internal(state, cursors)); Ok(IOResult::Done(delta)) } fn commit( &mut self, deltas: DeltaPair, cursors: &mut DbspStateCursors, ) -> Result> { loop { let mut state = std::mem::replace(&mut self.commit_state, JoinCommitState::Invalid); match &mut state { JoinCommitState::Idle => { self.commit_state = JoinCommitState::Eval { eval_state: deltas.clone().into(), } } JoinCommitState::Eval { ref mut eval_state } => { let output = return_and_restore_if_io!( &mut self.commit_state, state, self.eval(eval_state, cursors) ); self.commit_state = JoinCommitState::CommitLeftDelta { deltas: deltas.clone(), output, current_idx: 0, write_row: WriteRow::new(), }; } JoinCommitState::CommitLeftDelta { deltas, output, current_idx, ref mut write_row, } => { if *current_idx >= deltas.left.changes.len() { self.commit_state = JoinCommitState::CommitRightDelta { deltas: std::mem::take(deltas), output: std::mem::take(output), current_idx: 0, write_row: WriteRow::new(), }; continue; } let (row, weight) = &deltas.left.changes[*current_idx]; // Extract join key from the left row let join_key = self.extract_join_key(&row.values, &self.left_key_indices); // The index key: (storage_id, zset_id, element_id) // zset_id is the hash of the join key, element_id is hash of the row let storage_id = self.left_storage_id(); let zset_hash = join_key.cached_hash(); let element_hash = row.cached_hash(); let index_key = vec![ Value::from_i64(storage_id), zset_hash.to_value(), element_hash.to_value(), ]; // The record values: we'll store the serialized row as a blob let row_blob = serialize_hashable_row(row); let record_values = vec![ Value::from_i64(self.left_storage_id()), zset_hash.to_value(), element_hash.to_value(), Value::Blob(row_blob), ]; // Use return_and_restore_if_io to handle I/O properly return_and_restore_if_io!( &mut self.commit_state, state, write_row.write_row(cursors, index_key, record_values, *weight) ); self.commit_state = JoinCommitState::CommitLeftDelta { deltas: deltas.clone(), output: output.clone(), current_idx: *current_idx + 1, write_row: WriteRow::new(), }; } JoinCommitState::CommitRightDelta { deltas, output, current_idx, ref mut write_row, } => { if *current_idx >= deltas.right.changes.len() { // Reset to Idle state for next commit self.commit_state = JoinCommitState::Idle; return Ok(IOResult::Done(output.clone())); } let (row, weight) = &deltas.right.changes[*current_idx]; // Extract join key from the right row let join_key = self.extract_join_key(&row.values, &self.right_key_indices); // The index key: (storage_id, zset_id, element_id) let zset_hash = join_key.cached_hash(); let element_hash = row.cached_hash(); let index_key = vec![ Value::from_i64(self.right_storage_id()), zset_hash.to_value(), element_hash.to_value(), ]; // The record values: we'll store the serialized row as a blob let row_blob = serialize_hashable_row(row); let record_values = vec![ Value::from_i64(self.right_storage_id()), zset_hash.to_value(), element_hash.to_value(), Value::Blob(row_blob), ]; // Use return_and_restore_if_io to handle I/O properly return_and_restore_if_io!( &mut self.commit_state, state, write_row.write_row(cursors, index_key, record_values, *weight) ); self.commit_state = JoinCommitState::CommitRightDelta { deltas: std::mem::take(deltas), output: std::mem::take(output), current_idx: *current_idx + 1, write_row: WriteRow::new(), }; } JoinCommitState::Invalid => { panic!("Invalid join commit state"); } } } } fn set_tracker(&mut self, tracker: Arc>) { self.tracker = Some(tracker); } } ================================================ FILE: core/incremental/merge_operator.rs ================================================ // Merge operator for DBSP - combines two delta streams // Used in recursive CTEs and UNION operations use crate::incremental::dbsp::{Delta, DeltaPair, HashableRow}; use crate::incremental::operator::{ ComputationTracker, DbspStateCursors, EvalState, IncrementalOperator, }; use crate::sync::Arc; use crate::sync::Mutex; use crate::types::IOResult; use crate::Result; use std::collections::{hash_map::DefaultHasher, HashMap}; use std::fmt::{self, Display}; use std::hash::{Hash, Hasher}; /// How the merge operator should handle rowids when combining deltas #[derive(Debug, Clone)] pub enum UnionMode { /// For UNION (distinct) - hash values only to merge duplicates Distinct, /// For UNION ALL - include source table name in hash to keep duplicates separate All { left_table: String, right_table: String, }, } /// Merge operator that combines two input deltas into one output delta /// Handles both recursive CTEs and UNION/UNION ALL operations #[derive(Debug)] pub struct MergeOperator { operator_id: i64, union_mode: UnionMode, /// For UNION: tracks seen value hashes with their assigned rowids /// For UNION ALL: tracks (source_id, original_rowid) -> assigned_rowid mappings seen_rows: HashMap, // hash -> assigned_rowid /// Next rowid to assign for new rows next_rowid: i64, } impl MergeOperator { /// Create a new merge operator with specified union mode pub fn new(operator_id: i64, mode: UnionMode) -> Self { Self { operator_id, union_mode: mode, seen_rows: HashMap::default(), next_rowid: 1, } } /// Transform a delta's rowids based on the union mode with state tracking fn transform_delta(&mut self, delta: Delta, is_left: bool) -> Delta { match &self.union_mode { UnionMode::Distinct => { // For UNION distinct, track seen values and deduplicate let mut output = Delta::new(); for (row, weight) in delta.changes { // Hash only the values (not rowid) for deduplication let temp_row = HashableRow::new(0, row.values.clone()); let value_hash = temp_row.cached_hash().as_i64() as u64; // Check if we've seen this value before let assigned_rowid = if let Some(&existing_rowid) = self.seen_rows.get(&value_hash) { // Value already seen - use existing rowid existing_rowid } else { // New value - assign new rowid and remember it let new_rowid = self.next_rowid; self.next_rowid += 1; self.seen_rows.insert(value_hash, new_rowid); new_rowid }; // Output the row with the assigned rowid let final_row = HashableRow::new(assigned_rowid, temp_row.values); output.changes.push((final_row, weight)); } output } UnionMode::All { left_table, right_table, } => { // For UNION ALL, maintain consistent rowid mapping per source let table = if is_left { left_table } else { right_table }; let mut source_hasher = DefaultHasher::new(); table.hash(&mut source_hasher); let source_id = source_hasher.finish(); let mut output = Delta::new(); for (row, weight) in delta.changes { // Create a unique key for this (source, rowid) pair let mut key_hasher = DefaultHasher::new(); source_id.hash(&mut key_hasher); row.rowid.hash(&mut key_hasher); let key_hash = key_hasher.finish(); // Check if we've seen this (source, rowid) before let assigned_rowid = if let Some(&existing_rowid) = self.seen_rows.get(&key_hash) { // Use existing rowid for this (source, rowid) pair existing_rowid } else { // New row - assign new rowid let new_rowid = self.next_rowid; self.next_rowid += 1; self.seen_rows.insert(key_hash, new_rowid); new_rowid }; // Create output row with consistent rowid let final_row = HashableRow::new(assigned_rowid, row.values.clone()); output.changes.push((final_row, weight)); } output } } } } impl Display for MergeOperator { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.union_mode { UnionMode::Distinct => write!(f, "MergeOperator({}, UNION)", self.operator_id), UnionMode::All { .. } => write!(f, "MergeOperator({}, UNION ALL)", self.operator_id), } } } impl IncrementalOperator for MergeOperator { fn eval( &mut self, input: &mut EvalState, _cursors: &mut DbspStateCursors, ) -> Result> { match input { EvalState::Init { deltas } => { // Extract deltas from the evaluation state let delta_pair = std::mem::take(deltas); // Transform deltas based on union mode (with state tracking) let left_transformed = self.transform_delta(delta_pair.left, true); let right_transformed = self.transform_delta(delta_pair.right, false); // Merge the transformed deltas let mut output = Delta::new(); output.merge(&left_transformed); output.merge(&right_transformed); // Move to Done state *input = EvalState::Done; Ok(IOResult::Done(output)) } EvalState::Aggregate(_) | EvalState::Join(_) | EvalState::Uninitialized => { // Merge operator only handles Init state unreachable!("MergeOperator only handles Init state") } EvalState::Done => { // Already evaluated Ok(IOResult::Done(Delta::new())) } } } fn commit( &mut self, deltas: DeltaPair, _cursors: &mut DbspStateCursors, ) -> Result> { // Transform deltas based on union mode let left_transformed = self.transform_delta(deltas.left, true); let right_transformed = self.transform_delta(deltas.right, false); // Merge the transformed deltas let mut output = Delta::new(); output.merge(&left_transformed); output.merge(&right_transformed); Ok(IOResult::Done(output)) } fn set_tracker(&mut self, _tracker: Arc>) { // Merge operator doesn't need tracking for now } } ================================================ FILE: core/incremental/mod.rs ================================================ pub mod aggregate_operator; pub mod compiler; pub mod cursor; pub mod dbsp; pub mod expr_compiler; pub mod filter_operator; pub mod input_operator; pub mod join_operator; pub mod merge_operator; pub mod operator; pub mod persistence; pub mod project_operator; pub mod view; ================================================ FILE: core/incremental/operator.rs ================================================ #![allow(dead_code)] // Operator DAG for DBSP-style incremental computation // Based on Feldera DBSP design but adapted for Turso's architecture pub use crate::incremental::aggregate_operator::{ AggregateEvalState, AggregateFunction, AggregateState, }; pub use crate::incremental::filter_operator::{FilterOperator, FilterPredicate}; pub use crate::incremental::input_operator::InputOperator; pub use crate::incremental::join_operator::{JoinEvalState, JoinOperator, JoinType}; pub use crate::incremental::project_operator::{ProjectColumn, ProjectOperator}; use crate::incremental::dbsp::{Delta, DeltaPair}; #[cfg(test)] use crate::numeric::Numeric; use crate::schema::{Index, IndexColumn}; use crate::storage::btree::BTreeCursor; use crate::sync::Arc; use crate::sync::Mutex; use crate::types::IOResult; use crate::Result; use std::fmt::Debug; /// Struct to hold both table and index cursors for DBSP state operations pub struct DbspStateCursors { /// Cursor for the DBSP state table pub table_cursor: BTreeCursor, /// Cursor for the DBSP state table's primary key index pub index_cursor: BTreeCursor, } impl DbspStateCursors { /// Create a new DbspStateCursors with both table and index cursors pub fn new(table_cursor: BTreeCursor, index_cursor: BTreeCursor) -> Self { Self { table_cursor, index_cursor, } } } /// Create an index definition for the DBSP state table /// This defines the primary key index on (operator_id, zset_id, element_id) pub fn create_dbsp_state_index(root_page: i64) -> Index { Index { name: "dbsp_state_pk".to_string(), table_name: "dbsp_state".to_string(), root_page, columns: vec![ IndexColumn { name: "operator_id".to_string(), order: turso_parser::ast::SortOrder::Asc, collation: None, pos_in_table: 0, default: None, expr: None, }, IndexColumn { name: "zset_id".to_string(), order: turso_parser::ast::SortOrder::Asc, collation: None, pos_in_table: 1, default: None, expr: None, }, IndexColumn { name: "element_id".to_string(), order: turso_parser::ast::SortOrder::Asc, collation: None, pos_in_table: 2, default: None, expr: None, }, ], unique: true, ephemeral: false, has_rowid: true, where_clause: None, index_method: None, on_conflict: None, } } /// Generate a storage ID with column index and operation type encoding /// Storage ID = (operator_id << 16) | (column_index << 2) | operation_type /// Bit layout (64-bit integer): /// - Bits 16-63 (48 bits): operator_id /// - Bits 2-15 (14 bits): column_index (supports up to 16,384 columns) /// - Bits 0-1 (2 bits): operation type (AGG_TYPE_REGULAR, AGG_TYPE_MINMAX, etc.) pub fn generate_storage_id(operator_id: i64, column_index: usize, op_type: u8) -> i64 { assert!(op_type <= 3, "Invalid operation type"); assert!(column_index < 16384, "Column index too large"); ((operator_id) << 16) | ((column_index as i64) << 2) | (op_type as i64) } // Generic eval state that delegates to operator-specific states #[derive(Debug)] pub enum EvalState { Uninitialized, Init { deltas: DeltaPair }, Aggregate(Box), Join(Box), Done, } impl From for EvalState { fn from(delta: Delta) -> Self { EvalState::Init { deltas: delta.into(), } } } impl From for EvalState { fn from(deltas: DeltaPair) -> Self { EvalState::Init { deltas } } } impl EvalState { pub fn from_delta(delta: Delta) -> Self { Self::Init { deltas: delta.into(), } } fn delta_ref(&self) -> &Delta { match self { EvalState::Init { deltas } => &deltas.left, _ => panic!("delta_ref() can only be called when in Init state",), } } pub fn extract_delta(&mut self) -> Delta { match self { EvalState::Init { deltas } => { let extracted = std::mem::take(&mut deltas.left); *self = EvalState::Uninitialized; extracted } _ => panic!("extract_delta() can only be called when in Init state"), } } } /// Tracks computation counts to verify incremental behavior (for tests now), and in the future /// should be used to provide statistics. #[derive(Debug, Default, Clone)] pub struct ComputationTracker { pub filter_evaluations: usize, pub project_operations: usize, pub join_lookups: usize, pub aggregation_updates: usize, pub full_scans: usize, } impl ComputationTracker { pub fn new() -> Self { Self::default() } pub fn record_filter(&mut self) { self.filter_evaluations += 1; } pub fn record_project(&mut self) { self.project_operations += 1; } pub fn record_join_lookup(&mut self) { self.join_lookups += 1; } pub fn record_aggregation(&mut self) { self.aggregation_updates += 1; } pub fn record_full_scan(&mut self) { self.full_scans += 1; } pub fn total_computations(&self) -> usize { self.filter_evaluations + self.project_operations + self.join_lookups + self.aggregation_updates } } /// Represents an operator in the dataflow graph #[derive(Debug, Clone)] pub enum QueryOperator { /// Table scan - source of data TableScan { table_name: String, column_names: Vec, }, /// Filter rows based on predicate Filter { predicate: FilterPredicate, input: usize, // Index of input operator }, /// Project columns (select specific columns) Project { columns: Vec, input: usize, }, /// Join two inputs Join { join_type: JoinType, on_column: String, left_input: usize, right_input: usize, }, /// Aggregate Aggregate { group_by: Vec, aggregates: Vec, input: usize, }, } /// Operator DAG (Directed Acyclic Graph) /// Base trait for incremental operators // SAFETY: This needs to be audited for thread safety. // See: https://github.com/tursodatabase/turso/issues/1552 pub trait IncrementalOperator: Debug + Send { /// Evaluate the operator with a state, without modifying internal state /// This is used during query execution to compute results /// May need to read from storage to get current state (e.g., for aggregates) /// /// # Arguments /// * `state` - The evaluation state (may be in progress from a previous I/O operation) /// * `cursors` - Cursors for reading operator state from storage (table and optional index) /// /// # Returns /// The output delta from the evaluation fn eval( &mut self, state: &mut EvalState, cursors: &mut DbspStateCursors, ) -> Result>; /// Commit deltas to the operator's internal state and return the output /// This is called when a transaction commits, making changes permanent /// Returns the output delta (what downstream operators should see) /// The cursors parameter is for operators that need to persist state fn commit( &mut self, deltas: DeltaPair, cursors: &mut DbspStateCursors, ) -> Result>; /// Set computation tracker fn set_tracker(&mut self, tracker: Arc>); } #[cfg(test)] mod tests { use rustc_hash::FxHashSet as HashSet; use super::*; use crate::incremental::aggregate_operator::{AggregateOperator, AGG_TYPE_REGULAR}; use crate::incremental::dbsp::HashableRow; use crate::storage::btree::CursorTrait; use crate::storage::pager::CreateBTreeFlags; use crate::sync::Arc; use crate::sync::Mutex; use crate::types::Text; use crate::util::IOExt; use crate::Value; use crate::{Database, MemoryIO, IO}; /// Create a test pager for operator tests with both table and index fn create_test_pager() -> (crate::sync::Arc, i64, i64) { let io: Arc = Arc::new(MemoryIO::new()); let db = Database::open_file(io.clone(), ":memory:").unwrap(); let conn = db.connect().unwrap(); let pager = conn.pager.load().clone(); // Allocate page 1 first (database header) let _ = pager.io.block(|| pager.allocate_page1()); // Create a BTree for the table let table_root_page_id = pager .io .block(|| pager.btree_create(&CreateBTreeFlags::new_table())) .expect("Failed to create BTree for aggregate state table") as i64; // Create a BTree for the index let index_root_page_id = pager .io .block(|| pager.btree_create(&CreateBTreeFlags::new_index())) .expect("Failed to create BTree for aggregate state index") as i64; (pager, table_root_page_id, index_root_page_id) } /// Read the current state from the BTree (for testing) /// Returns a Delta with all the current aggregate values fn get_current_state_from_btree( agg: &AggregateOperator, pager: &crate::sync::Arc, cursors: &mut DbspStateCursors, ) -> Delta { let mut result = Delta::new(); // Rewind to start of table pager.io.block(|| cursors.table_cursor.rewind()).unwrap(); loop { // Check if cursor is empty (no more rows) if cursors.table_cursor.is_empty() { break; } // Get the record at this position let record = loop { match cursors.table_cursor.record().unwrap() { IOResult::Done(r) => break r, IOResult::IO(io) => io.wait(&*pager.io).unwrap(), } } .unwrap() .to_owned(); let values: Vec = record.get_values_owned().unwrap(); // Parse the 5-column structure: operator_id, zset_id, element_id, value, weight if let Some(Value::Numeric(Numeric::Integer(op_id))) = values.first() { // For regular aggregates, use column_index=0 and AGG_TYPE_REGULAR let expected_op_id = generate_storage_id(agg.operator_id, 0, AGG_TYPE_REGULAR); // Skip if not our operator if *op_id != expected_op_id { pager.io.block(|| cursors.table_cursor.next()).unwrap(); continue; } // Get the blob data from column 3 (value column) if let Some(Value::Blob(blob)) = values.get(3) { // Deserialize the state match AggregateState::from_blob(blob) { Ok((state, group_key)) => { // Should not have made it this far. assert!(state.count != 0); // Build output row: group_by columns + aggregate values let mut output_values = group_key.clone(); output_values.extend(state.to_values(&agg.aggregates)); let group_key_str = AggregateOperator::group_key_to_string(&group_key); let rowid = agg.generate_group_rowid(&group_key_str); let output_row = HashableRow::new(rowid, output_values); result.changes.push((output_row, 1)); } Err(e) => { // Log or handle the deserialization error // For now, we'll skip this entry eprintln!("Failed to deserialize aggregate state: {e}"); } } } } pager.io.block(|| cursors.table_cursor.next()).unwrap(); } result.consolidate(); result } /// Assert that we're doing incremental work, not full recomputation fn assert_incremental(tracker: &ComputationTracker, expected_ops: usize, data_size: usize) { assert!( tracker.total_computations() <= expected_ops, "Expected <= {} operations for incremental update, got {}", expected_ops, tracker.total_computations() ); assert!( tracker.total_computations() < data_size, "Computation count {} suggests full recomputation (data size: {})", tracker.total_computations(), data_size ); assert_eq!( tracker.full_scans, 0, "Incremental computation should not perform full scans" ); } // Aggregate tests #[test] fn test_aggregate_incremental_update_emits_retraction() { // This test verifies that when an aggregate value changes, // the operator emits both a retraction (-1) of the old value // and an insertion (+1) of the new value. // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); // Create an aggregate operator for SUM(age) with no GROUP BY let mut agg = AggregateOperator::new( 1, // operator_id for testing vec![], // No GROUP BY vec![AggregateFunction::Sum(2)], // age is at index 2 vec!["id".to_string(), "name".to_string(), "age".to_string()], ) .unwrap(); // Initial data: 3 users let mut initial_delta = Delta::new(); initial_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".to_string().into()), Value::from_i64(25), ], ); initial_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".to_string().into()), Value::from_i64(30), ], ); initial_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".to_string().into()), Value::from_i64(35), ], ); // Initialize with initial data pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Verify initial state: SUM(age) = 25 + 30 + 35 = 90 let state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(state.changes.len(), 1, "Should have one aggregate row"); let (row, weight) = &state.changes[0]; assert_eq!(*weight, 1, "Aggregate row should have weight 1"); assert_eq!(row.values[0], Value::from_f64(90.0), "SUM should be 90"); // Now add a new user (incremental update) let mut update_delta = Delta::new(); update_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("David".to_string().into()), Value::from_i64(40), ], ); // Process the incremental update let output_delta = pager .io .block(|| agg.commit((&update_delta).into(), &mut cursors)) .unwrap(); // CRITICAL: The output delta should contain TWO changes: // 1. Retraction of old aggregate value (90) with weight -1 // 2. Insertion of new aggregate value (130) with weight +1 assert_eq!( output_delta.changes.len(), 2, "Expected 2 changes (retraction + insertion), got {}: {:?}", output_delta.changes.len(), output_delta.changes ); // Verify the retraction comes first let (retraction_row, retraction_weight) = &output_delta.changes[0]; assert_eq!( *retraction_weight, -1, "First change should be a retraction" ); assert_eq!( retraction_row.values[0], Value::from_f64(90.0), "Retracted value should be the old sum (90)" ); // Verify the insertion comes second let (insertion_row, insertion_weight) = &output_delta.changes[1]; assert_eq!(*insertion_weight, 1, "Second change should be an insertion"); assert_eq!( insertion_row.values[0], Value::from_f64(130.0), "Inserted value should be the new sum (130)" ); // Both changes should have the same row ID (since it's the same aggregate group) assert_eq!( retraction_row.rowid, insertion_row.rowid, "Retraction and insertion should have the same row ID" ); } #[test] fn test_aggregate_with_group_by_emits_retractions() { // This test verifies that when aggregate values change for grouped data, // the operator emits both retractions and insertions correctly for each group. // Create an aggregate operator for SUM(score) GROUP BY team // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing vec![1], // GROUP BY team (index 1) vec![AggregateFunction::Sum(3)], // score is at index 3 vec![ "id".to_string(), "team".to_string(), "player".to_string(), "score".to_string(), ], ) .unwrap(); // Initial data: players on different teams let mut initial_delta = Delta::new(); initial_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("red".to_string().into()), Value::Text("Alice".to_string().into()), Value::from_i64(10), ], ); initial_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("blue".to_string().into()), Value::Text("Bob".to_string().into()), Value::from_i64(15), ], ); initial_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("red".to_string().into()), Value::Text("Charlie".to_string().into()), Value::from_i64(20), ], ); // Initialize with initial data pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Verify initial state: red team = 30, blue team = 15 let state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(state.changes.len(), 2, "Should have two groups"); // Find the red and blue team aggregates let mut red_sum = None; let mut blue_sum = None; for (row, weight) in &state.changes { assert_eq!(*weight, 1); if let Value::Text(team) = &row.values[0] { if team.as_str() == "red" { red_sum = Some(&row.values[1]); } else if team.as_str() == "blue" { blue_sum = Some(&row.values[1]); } } } assert_eq!( red_sum, Some(&Value::from_f64(30.0)), "Red team sum should be 30" ); assert_eq!( blue_sum, Some(&Value::from_f64(15.0)), "Blue team sum should be 15" ); // Now add a new player to the red team (incremental update) let mut update_delta = Delta::new(); update_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("red".to_string().into()), Value::Text("David".to_string().into()), Value::from_i64(25), ], ); // Process the incremental update let output_delta = pager .io .block(|| agg.commit((&update_delta).into(), &mut cursors)) .unwrap(); // Should have 2 changes: retraction of old red team sum, insertion of new red team sum // Blue team should NOT be affected assert_eq!( output_delta.changes.len(), 2, "Expected 2 changes for red team only, got {}: {:?}", output_delta.changes.len(), output_delta.changes ); // Both changes should be for the red team let mut found_retraction = false; let mut found_insertion = false; for (row, weight) in &output_delta.changes { if let Value::Text(team) = &row.values[0] { assert_eq!(team.as_str(), "red", "Only red team should have changes"); if *weight == -1 { // Retraction of old value assert_eq!( row.values[1], Value::from_f64(30.0), "Should retract old sum of 30" ); found_retraction = true; } else if *weight == 1 { // Insertion of new value assert_eq!( row.values[1], Value::from_f64(55.0), "Should insert new sum of 55" ); found_insertion = true; } } } assert!(found_retraction, "Should have found retraction"); assert!(found_insertion, "Should have found insertion"); } // Aggregation tests #[test] fn test_count_increments_not_recounts() { let tracker = Arc::new(Mutex::new(ComputationTracker::new())); // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); // Create COUNT(*) GROUP BY category let mut agg = AggregateOperator::new( 1, // operator_id for testing vec![1], // category is at index 1 vec![AggregateFunction::Count], vec![ "item_id".to_string(), "category".to_string(), "price".to_string(), ], ) .unwrap(); agg.set_tracker(tracker.clone()); // Initial: 100 items in 10 categories (10 items each) let mut initial = Delta::new(); for i in 0..100 { let category = format!("cat_{}", i / 10); initial.insert( i, vec![ Value::from_i64(i), Value::Text(Text::new(category)), Value::from_i64(i * 10), ], ); } pager .io .block(|| agg.commit((&initial).into(), &mut cursors)) .unwrap(); // Reset tracker for delta processing tracker.lock().aggregation_updates = 0; // Add one item to category 'cat_0' let mut delta = Delta::new(); delta.insert( 100, vec![ Value::from_i64(100), Value::Text(Text::new("cat_0")), Value::from_i64(1000), ], ); pager .io .block(|| agg.commit((&delta).into(), &mut cursors)) .unwrap(); assert_eq!(tracker.lock().aggregation_updates, 1); // Check the final state - cat_0 should now have count 11 let final_state = get_current_state_from_btree(&agg, &pager, &mut cursors); let cat_0 = final_state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text(Text::new("cat_0"))) .unwrap(); assert_eq!(cat_0.0.values[1], Value::from_i64(11)); // Verify incremental behavior - we process the delta twice (eval + commit) let t = tracker.lock(); assert_incremental(&t, 2, 101); } #[test] fn test_sum_updates_incrementally() { let tracker = Arc::new(Mutex::new(ComputationTracker::new())); // Create SUM(amount) GROUP BY product // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing vec![1], // product is at index 1 vec![AggregateFunction::Sum(2)], // amount is at index 2 vec![ "sale_id".to_string(), "product".to_string(), "amount".to_string(), ], ) .unwrap(); agg.set_tracker(tracker.clone()); // Initial sales let mut initial = Delta::new(); initial.insert( 1, vec![ Value::from_i64(1), Value::Text(Text::new("Widget")), Value::from_i64(100), ], ); initial.insert( 2, vec![ Value::from_i64(2), Value::Text(Text::new("Gadget")), Value::from_i64(200), ], ); initial.insert( 3, vec![ Value::from_i64(3), Value::Text(Text::new("Widget")), Value::from_i64(150), ], ); pager .io .block(|| agg.commit((&initial).into(), &mut cursors)) .unwrap(); // Check initial state: Widget=250, Gadget=200 let state = get_current_state_from_btree(&agg, &pager, &mut cursors); let widget_sum = state .changes .iter() .find(|(c, _)| c.values[0] == Value::Text(Text::new("Widget"))) .map(|(c, _)| c) .unwrap(); assert_eq!(widget_sum.values[1], Value::from_i64(250)); // Reset tracker tracker.lock().aggregation_updates = 0; // Add sale of 50 for Widget let mut delta = Delta::new(); delta.insert( 4, vec![ Value::from_i64(4), Value::Text(Text::new("Widget")), Value::from_i64(50), ], ); pager .io .block(|| agg.commit((&delta).into(), &mut cursors)) .unwrap(); assert_eq!(tracker.lock().aggregation_updates, 1); // Check final state - Widget should now be 300 (250 + 50) let final_state = get_current_state_from_btree(&agg, &pager, &mut cursors); let widget = final_state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text(Text::new("Widget"))) .unwrap(); assert_eq!(widget.0.values[1], Value::from_i64(300)); } #[test] fn test_count_and_sum_together() { // Test the example from DBSP_ROADMAP: COUNT(*) and SUM(amount) GROUP BY user_id // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing vec![1], // user_id is at index 1 vec![ AggregateFunction::Count, AggregateFunction::Sum(2), // amount is at index 2 ], vec![ "order_id".to_string(), "user_id".to_string(), "amount".to_string(), ], ) .unwrap(); // Initial orders let mut initial = Delta::new(); initial.insert( 1, vec![Value::from_i64(1), Value::from_i64(1), Value::from_i64(100)], ); initial.insert( 2, vec![Value::from_i64(2), Value::from_i64(1), Value::from_i64(200)], ); initial.insert( 3, vec![Value::from_i64(3), Value::from_i64(2), Value::from_i64(150)], ); pager .io .block(|| agg.commit((&initial).into(), &mut cursors)) .unwrap(); // Check initial state // User 1: count=2, sum=300 // User 2: count=1, sum=150 let state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(state.changes.len(), 2); let user1 = state .changes .iter() .find(|(c, _)| c.values[0] == Value::from_i64(1)) .map(|(c, _)| c) .unwrap(); assert_eq!(user1.values[1], Value::from_i64(2)); // count assert_eq!(user1.values[2], Value::from_i64(300)); // sum let user2 = state .changes .iter() .find(|(c, _)| c.values[0] == Value::from_i64(2)) .map(|(c, _)| c) .unwrap(); assert_eq!(user2.values[1], Value::from_i64(1)); // count assert_eq!(user2.values[2], Value::from_i64(150)); // sum // Add order for user 1 let mut delta = Delta::new(); delta.insert( 4, vec![Value::from_i64(4), Value::from_i64(1), Value::from_i64(50)], ); pager .io .block(|| agg.commit((&delta).into(), &mut cursors)) .unwrap(); // Check final state - user 1 should have updated count and sum let final_state = get_current_state_from_btree(&agg, &pager, &mut cursors); let user1 = final_state .changes .iter() .find(|(row, _)| row.values[0] == Value::from_i64(1)) .unwrap(); assert_eq!(user1.0.values[1], Value::from_i64(3)); // count: 2 + 1 assert_eq!(user1.0.values[2], Value::from_i64(350)); // sum: 300 + 50 } #[test] fn test_avg_maintains_sum_and_count() { // Test AVG aggregation // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing vec![1], // category is at index 1 vec![AggregateFunction::Avg(2)], // value is at index 2 vec![ "id".to_string(), "category".to_string(), "value".to_string(), ], ) .unwrap(); // Initial data let mut initial = Delta::new(); initial.insert( 1, vec![ Value::from_i64(1), Value::Text(Text::new("A")), Value::from_i64(10), ], ); initial.insert( 2, vec![ Value::from_i64(2), Value::Text(Text::new("A")), Value::from_i64(20), ], ); initial.insert( 3, vec![ Value::from_i64(3), Value::Text(Text::new("B")), Value::from_i64(30), ], ); pager .io .block(|| agg.commit((&initial).into(), &mut cursors)) .unwrap(); // Check initial averages // Category A: avg = (10 + 20) / 2 = 15 // Category B: avg = 30 / 1 = 30 let state = get_current_state_from_btree(&agg, &pager, &mut cursors); let cat_a = state .changes .iter() .find(|(c, _)| c.values[0] == Value::Text(Text::new("A"))) .map(|(c, _)| c) .unwrap(); assert_eq!(cat_a.values[1], Value::from_f64(15.0)); let cat_b = state .changes .iter() .find(|(c, _)| c.values[0] == Value::Text(Text::new("B"))) .map(|(c, _)| c) .unwrap(); assert_eq!(cat_b.values[1], Value::from_f64(30.0)); // Add value to category A let mut delta = Delta::new(); delta.insert( 4, vec![ Value::from_i64(4), Value::Text(Text::new("A")), Value::from_i64(30), ], ); pager .io .block(|| agg.commit((&delta).into(), &mut cursors)) .unwrap(); // Check final state - Category A avg should now be (10 + 20 + 30) / 3 = 20 let final_state = get_current_state_from_btree(&agg, &pager, &mut cursors); let cat_a = final_state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text(Text::new("A"))) .unwrap(); assert_eq!(cat_a.0.values[1], Value::from_f64(20.0)); } #[test] fn test_delete_updates_aggregates() { // Test that deletes (negative weights) properly update aggregates // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing vec![1], // category is at index 1 vec![ AggregateFunction::Count, AggregateFunction::Sum(2), // value is at index 2 ], vec![ "id".to_string(), "category".to_string(), "value".to_string(), ], ) .unwrap(); // Initial data let mut initial = Delta::new(); initial.insert( 1, vec![ Value::from_i64(1), Value::Text(Text::new("A")), Value::from_i64(100), ], ); initial.insert( 2, vec![ Value::from_i64(2), Value::Text(Text::new("A")), Value::from_i64(200), ], ); pager .io .block(|| agg.commit((&initial).into(), &mut cursors)) .unwrap(); // Check initial state: count=2, sum=300 let state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert!(!state.changes.is_empty()); let (row, _weight) = &state.changes[0]; assert_eq!(row.values[1], Value::from_i64(2)); // count assert_eq!(row.values[2], Value::from_i64(300)); // sum // Delete one row let mut delta = Delta::new(); delta.delete( 1, vec![ Value::from_i64(1), Value::Text(Text::new("A")), Value::from_i64(100), ], ); pager .io .block(|| agg.commit((&delta).into(), &mut cursors)) .unwrap(); // Check final state - should update to count=1, sum=200 let final_state = get_current_state_from_btree(&agg, &pager, &mut cursors); let cat_a = final_state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text(Text::new("A"))) .unwrap(); assert_eq!(cat_a.0.values[1], Value::from_i64(1)); // count: 2 - 1 assert_eq!(cat_a.0.values[2], Value::from_i64(200)); // sum: 300 - 100 } #[test] fn test_count_aggregation_with_deletions() { let aggregates = vec![AggregateFunction::Count]; let group_by = vec![0]; // category is at index 0 let input_columns = vec!["category".to_string(), "value".to_string()]; // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing group_by, aggregates, input_columns, ) .unwrap(); // Initialize with data let mut init_data = Delta::new(); init_data.insert(1, vec![Value::Text("A".into()), Value::from_i64(10)]); init_data.insert(2, vec![Value::Text("A".into()), Value::from_i64(20)]); init_data.insert(3, vec![Value::Text("B".into()), Value::from_i64(30)]); pager .io .block(|| agg.commit((&init_data).into(), &mut cursors)) .unwrap(); // Check initial counts let state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(state.changes.len(), 2); // Find group A and B let group_a = state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("A".into())) .unwrap(); let group_b = state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("B".into())) .unwrap(); assert_eq!(group_a.0.values[1], Value::from_i64(2)); // COUNT = 2 for A assert_eq!(group_b.0.values[1], Value::from_i64(1)); // COUNT = 1 for B // Delete one row from group A let mut delete_delta = Delta::new(); delete_delta.delete(1, vec![Value::Text("A".into()), Value::from_i64(10)]); let output = pager .io .block(|| agg.commit((&delete_delta).into(), &mut cursors)) .unwrap(); // Should emit retraction for old count and insertion for new count assert_eq!(output.changes.len(), 2); // Check final state let final_state = get_current_state_from_btree(&agg, &pager, &mut cursors); let group_a_final = final_state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("A".into())) .unwrap(); assert_eq!(group_a_final.0.values[1], Value::from_i64(1)); // COUNT = 1 for A after deletion // Delete all rows from group B let mut delete_all_b = Delta::new(); delete_all_b.delete(3, vec![Value::Text("B".into()), Value::from_i64(30)]); let output_b = pager .io .block(|| agg.commit((&delete_all_b).into(), &mut cursors)) .unwrap(); assert_eq!(output_b.changes.len(), 1); // Only retraction, no new row assert_eq!(output_b.changes[0].1, -1); // Retraction // Final state should not have group B let final_state2 = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(final_state2.changes.len(), 1); // Only group A remains assert_eq!(final_state2.changes[0].0.values[0], Value::Text("A".into())); } #[test] fn test_sum_aggregation_with_deletions() { let aggregates = vec![AggregateFunction::Sum(1)]; // value is at index 1 let group_by = vec![0]; // category is at index 0 let input_columns = vec!["category".to_string(), "value".to_string()]; // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing group_by, aggregates, input_columns, ) .unwrap(); // Initialize with data let mut init_data = Delta::new(); init_data.insert(1, vec![Value::Text("A".into()), Value::from_i64(10)]); init_data.insert(2, vec![Value::Text("A".into()), Value::from_i64(20)]); init_data.insert(3, vec![Value::Text("B".into()), Value::from_i64(30)]); init_data.insert(4, vec![Value::Text("B".into()), Value::from_i64(15)]); pager .io .block(|| agg.commit((&init_data).into(), &mut cursors)) .unwrap(); // Check initial sums let state = get_current_state_from_btree(&agg, &pager, &mut cursors); let group_a = state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("A".into())) .unwrap(); let group_b = state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("B".into())) .unwrap(); assert_eq!(group_a.0.values[1], Value::from_i64(30)); // SUM = 30 for A (10+20) assert_eq!(group_b.0.values[1], Value::from_i64(45)); // SUM = 45 for B (30+15) // Delete one row from group A let mut delete_delta = Delta::new(); delete_delta.delete(2, vec![Value::Text("A".into()), Value::from_i64(20)]); pager .io .block(|| agg.commit((&delete_delta).into(), &mut cursors)) .unwrap(); // Check updated sum let state = get_current_state_from_btree(&agg, &pager, &mut cursors); let group_a = state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("A".into())) .unwrap(); assert_eq!(group_a.0.values[1], Value::from_i64(10)); // SUM = 10 for A after deletion // Delete all from group B let mut delete_all_b = Delta::new(); delete_all_b.delete(3, vec![Value::Text("B".into()), Value::from_i64(30)]); delete_all_b.delete(4, vec![Value::Text("B".into()), Value::from_i64(15)]); pager .io .block(|| agg.commit((&delete_all_b).into(), &mut cursors)) .unwrap(); // Group B should be gone let final_state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(final_state.changes.len(), 1); // Only group A remains assert_eq!(final_state.changes[0].0.values[0], Value::Text("A".into())); } #[test] fn test_avg_aggregation_with_deletions() { let aggregates = vec![AggregateFunction::Avg(1)]; // value is at index 1 let group_by = vec![0]; // category is at index 0 let input_columns = vec!["category".to_string(), "value".to_string()]; // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing group_by, aggregates, input_columns, ) .unwrap(); // Initialize with data let mut init_data = Delta::new(); init_data.insert(1, vec![Value::Text("A".into()), Value::from_i64(10)]); init_data.insert(2, vec![Value::Text("A".into()), Value::from_i64(20)]); init_data.insert(3, vec![Value::Text("A".into()), Value::from_i64(30)]); pager .io .block(|| agg.commit((&init_data).into(), &mut cursors)) .unwrap(); // Check initial average let state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(state.changes.len(), 1); assert_eq!(state.changes[0].0.values[1], Value::from_f64(20.0)); // AVG = (10+20+30)/3 = 20 // Delete the middle value let mut delete_delta = Delta::new(); delete_delta.delete(2, vec![Value::Text("A".into()), Value::from_i64(20)]); pager .io .block(|| agg.commit((&delete_delta).into(), &mut cursors)) .unwrap(); // Check updated average let state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(state.changes[0].0.values[1], Value::from_f64(20.0)); // AVG = (10+30)/2 = 20 (same!) // Delete another to change the average let mut delete_another = Delta::new(); delete_another.delete(3, vec![Value::Text("A".into()), Value::from_i64(30)]); pager .io .block(|| agg.commit((&delete_another).into(), &mut cursors)) .unwrap(); let state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(state.changes[0].0.values[1], Value::from_f64(10.0)); // AVG = 10/1 = 10 } #[test] fn test_multiple_aggregations_with_deletions() { // Test COUNT, SUM, and AVG together let aggregates = vec![ AggregateFunction::Count, AggregateFunction::Sum(1), // value is at index 1 AggregateFunction::Avg(1), // value is at index 1 ]; let group_by = vec![0]; // category is at index 0 let input_columns = vec!["category".to_string(), "value".to_string()]; // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing group_by, aggregates, input_columns, ) .unwrap(); // Initialize with data let mut init_data = Delta::new(); init_data.insert(1, vec![Value::Text("A".into()), Value::from_i64(100)]); init_data.insert(2, vec![Value::Text("A".into()), Value::from_i64(200)]); init_data.insert(3, vec![Value::Text("B".into()), Value::from_i64(50)]); pager .io .block(|| agg.commit((&init_data).into(), &mut cursors)) .unwrap(); // Check initial state let state = get_current_state_from_btree(&agg, &pager, &mut cursors); let group_a = state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("A".into())) .unwrap(); assert_eq!(group_a.0.values[1], Value::from_i64(2)); // COUNT = 2 assert_eq!(group_a.0.values[2], Value::from_i64(300)); // SUM = 300 assert_eq!(group_a.0.values[3], Value::from_f64(150.0)); // AVG = 150 // Delete one row from group A let mut delete_delta = Delta::new(); delete_delta.delete(1, vec![Value::Text("A".into()), Value::from_i64(100)]); pager .io .block(|| agg.commit((&delete_delta).into(), &mut cursors)) .unwrap(); // Check all aggregates updated correctly let state = get_current_state_from_btree(&agg, &pager, &mut cursors); let group_a = state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("A".into())) .unwrap(); assert_eq!(group_a.0.values[1], Value::from_i64(1)); // COUNT = 1 assert_eq!(group_a.0.values[2], Value::from_i64(200)); // SUM = 200 assert_eq!(group_a.0.values[3], Value::from_f64(200.0)); // AVG = 200 // Insert a new row with floating point value let mut insert_delta = Delta::new(); insert_delta.insert(4, vec![Value::Text("A".into()), Value::from_f64(50.5)]); pager .io .block(|| agg.commit((&insert_delta).into(), &mut cursors)) .unwrap(); let state = get_current_state_from_btree(&agg, &pager, &mut cursors); let group_a = state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("A".into())) .unwrap(); assert_eq!(group_a.0.values[1], Value::from_i64(2)); // COUNT = 2 assert_eq!(group_a.0.values[2], Value::from_f64(250.5)); // SUM = 250.5 assert_eq!(group_a.0.values[3], Value::from_f64(125.25)); // AVG = 125.25 } #[test] fn test_filter_operator_rowid_update() { // When a row's rowid changes (e.g., UPDATE t SET a=1 WHERE a=3 on INTEGER PRIMARY KEY), // the operator should properly consolidate the state // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut filter = FilterOperator::new(FilterPredicate::GreaterThan { column_idx: 1, // "b" is at index 1 value: Value::from_i64(2), }); // Initialize with a row (rowid=3, values=[3, 3]) let mut init_data = Delta::new(); init_data.insert(3, vec![Value::from_i64(3), Value::from_i64(3)]); let state = pager .io .block(|| filter.commit((&init_data).into(), &mut cursors)) .unwrap(); // Check initial state assert_eq!(state.changes.len(), 1); assert_eq!(state.changes[0].0.rowid, 3); assert_eq!( state.changes[0].0.values, vec![Value::from_i64(3), Value::from_i64(3)] ); // Simulate an UPDATE that changes rowid from 3 to 1 // This is sent as: delete(3) + insert(1) let mut update_delta = Delta::new(); update_delta.delete(3, vec![Value::from_i64(3), Value::from_i64(3)]); update_delta.insert(1, vec![Value::from_i64(1), Value::from_i64(3)]); let output = pager .io .block(|| filter.commit((&update_delta).into(), &mut cursors)) .unwrap(); // The output delta should have both changes (both pass the filter b > 2) assert_eq!(output.changes.len(), 2); assert_eq!(output.changes[0].1, -1); // delete weight assert_eq!(output.changes[1].1, 1); // insert weight } // ============================================================================ // EVAL/COMMIT PATTERN TESTS // These tests verify that the eval/commit pattern works correctly: // - eval() computes results without modifying state // - eval() with uncommitted data returns correct results // - commit() updates internal state // - State remains unchanged when eval() is called with uncommitted data // ============================================================================ #[test] fn test_filter_eval_with_uncommitted() { // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut filter = FilterOperator::new(FilterPredicate::GreaterThan { column_idx: 2, // "age" is at index 2 value: Value::from_i64(25), }); // Initialize with some data let mut init_data = Delta::new(); init_data.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(30), ], ); init_data.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(20), ], ); let state = pager .io .block(|| filter.commit((&init_data).into(), &mut cursors)) .unwrap(); // Verify initial state (only Alice passes filter) assert_eq!(state.changes.len(), 1); assert_eq!(state.changes[0].0.rowid, 1); // Create uncommitted changes let mut uncommitted = Delta::new(); uncommitted.insert( 3, vec![ Value::from_i64(3), Value::Text("Charlie".into()), Value::from_i64(35), ], ); uncommitted.insert( 4, vec![ Value::from_i64(4), Value::Text("David".into()), Value::from_i64(15), ], ); // Eval with uncommitted - should return filtered uncommitted rows let mut eval_state = uncommitted.clone().into(); let result = pager .io .block(|| filter.eval(&mut eval_state, &mut cursors)) .unwrap(); assert_eq!( result.changes.len(), 1, "Only Charlie (35) should pass filter" ); assert_eq!(result.changes[0].0.rowid, 3); // Now commit the changes let state = pager .io .block(|| filter.commit((&uncommitted).into(), &mut cursors)) .unwrap(); // State should now include Charlie (who passes filter) assert_eq!( state.changes.len(), 1, "State should now have Alice and Charlie" ); } #[test] fn test_aggregate_eval_with_uncommitted_preserves_state() { // This is the critical test - aggregations must not modify internal state during eval // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing vec![1], // category is at index 1 vec![ AggregateFunction::Count, AggregateFunction::Sum(2), // amount is at index 2 ], vec![ "id".to_string(), "category".to_string(), "amount".to_string(), ], ) .unwrap(); // Initialize with data let mut init_data = Delta::new(); init_data.insert( 1, vec![ Value::from_i64(1), Value::Text("A".into()), Value::from_i64(100), ], ); init_data.insert( 2, vec![ Value::from_i64(2), Value::Text("A".into()), Value::from_i64(200), ], ); init_data.insert( 3, vec![ Value::from_i64(3), Value::Text("B".into()), Value::from_i64(150), ], ); pager .io .block(|| agg.commit((&init_data).into(), &mut cursors)) .unwrap(); // Check initial state: A -> (count=2, sum=300), B -> (count=1, sum=150) let initial_state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(initial_state.changes.len(), 2); // Store initial state for comparison let initial_a = initial_state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("A".into())) .unwrap(); assert_eq!(initial_a.0.values[1], Value::from_i64(2)); // count assert_eq!(initial_a.0.values[2], Value::from_f64(300.0)); // sum // Create uncommitted changes let mut uncommitted = Delta::new(); uncommitted.insert( 4, vec![ Value::from_i64(4), Value::Text("A".into()), Value::from_i64(50), ], ); uncommitted.insert( 5, vec![ Value::from_i64(5), Value::Text("C".into()), Value::from_i64(75), ], ); // Eval with uncommitted should return the delta (changes to aggregates) let mut eval_state = uncommitted.clone().into(); let result = pager .io .block(|| agg.eval(&mut eval_state, &mut cursors)) .unwrap(); // Result should contain updates for A and new group C // For A: retraction of old (2, 300) and insertion of new (3, 350) // For C: insertion of (1, 75) assert!(!result.changes.is_empty(), "Should have aggregate changes"); // CRITICAL: Verify internal state hasn't changed let state_after_eval = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!( state_after_eval.changes.len(), 2, "State should still have only A and B" ); let a_after_eval = state_after_eval .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("A".into())) .unwrap(); assert_eq!( a_after_eval.0.values[1], Value::from_i64(2), "A count should still be 2" ); assert_eq!( a_after_eval.0.values[2], Value::from_f64(300.0), "A sum should still be 300" ); // Now commit the changes pager .io .block(|| agg.commit((&uncommitted).into(), &mut cursors)) .unwrap(); // State should now be updated let final_state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(final_state.changes.len(), 3, "Should now have A, B, and C"); let a_final = final_state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("A".into())) .unwrap(); assert_eq!( a_final.0.values[1], Value::from_i64(3), "A count should now be 3" ); assert_eq!( a_final.0.values[2], Value::from_f64(350.0), "A sum should now be 350" ); let c_final = final_state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("C".into())) .unwrap(); assert_eq!( c_final.0.values[1], Value::from_i64(1), "C count should be 1" ); assert_eq!( c_final.0.values[2], Value::from_f64(75.0), "C sum should be 75" ); } #[test] fn test_aggregate_eval_multiple_times_without_commit() { // Test that calling eval multiple times with different uncommitted data // doesn't pollute the internal state // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing vec![], // No GROUP BY vec![ AggregateFunction::Count, AggregateFunction::Sum(1), // value is at index 1 ], vec!["id".to_string(), "value".to_string()], ) .unwrap(); // Initialize let mut init_data = Delta::new(); init_data.insert(1, vec![Value::from_i64(1), Value::from_i64(100)]); init_data.insert(2, vec![Value::from_i64(2), Value::from_i64(200)]); pager .io .block(|| agg.commit((&init_data).into(), &mut cursors)) .unwrap(); // Initial state: count=2, sum=300 let initial_state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(initial_state.changes.len(), 1); assert_eq!(initial_state.changes[0].0.values[0], Value::from_i64(2)); assert_eq!(initial_state.changes[0].0.values[1], Value::from_f64(300.0)); // First eval with uncommitted let mut uncommitted1 = Delta::new(); uncommitted1.insert(3, vec![Value::from_i64(3), Value::from_i64(50)]); let mut eval_state1 = uncommitted1.clone().into(); let _ = pager .io .block(|| agg.eval(&mut eval_state1, &mut cursors)) .unwrap(); // State should be unchanged let state1 = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(state1.changes[0].0.values[0], Value::from_i64(2)); assert_eq!(state1.changes[0].0.values[1], Value::from_f64(300.0)); // Second eval with different uncommitted let mut uncommitted2 = Delta::new(); uncommitted2.insert(4, vec![Value::from_i64(4), Value::from_i64(75)]); uncommitted2.insert(5, vec![Value::from_i64(5), Value::from_i64(25)]); let mut eval_state2 = uncommitted2.clone().into(); let _ = pager .io .block(|| agg.eval(&mut eval_state2, &mut cursors)) .unwrap(); // State should STILL be unchanged let state2 = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(state2.changes[0].0.values[0], Value::from_i64(2)); assert_eq!(state2.changes[0].0.values[1], Value::from_f64(300.0)); // Third eval with deletion as uncommitted let mut uncommitted3 = Delta::new(); uncommitted3.delete(1, vec![Value::from_i64(1), Value::from_i64(100)]); let mut eval_state3 = uncommitted3.clone().into(); let _ = pager .io .block(|| agg.eval(&mut eval_state3, &mut cursors)) .unwrap(); // State should STILL be unchanged let state3 = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(state3.changes[0].0.values[0], Value::from_i64(2)); assert_eq!(state3.changes[0].0.values[1], Value::from_f64(300.0)); } #[test] fn test_aggregate_eval_with_mixed_committed_and_uncommitted() { // Test eval with both committed delta and uncommitted changes // Create a persistent pager for the test let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); // Create index cursor with proper index definition for DBSP state table let index_def = create_dbsp_state_index(index_root_page_id); // Index has 4 columns: operator_id, zset_id, element_id, rowid let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id for testing vec![1], // type is at index 1 vec![AggregateFunction::Count], vec!["id".to_string(), "type".to_string()], ) .unwrap(); // Initialize let mut init_data = Delta::new(); init_data.insert(1, vec![Value::from_i64(1), Value::Text("X".into())]); init_data.insert(2, vec![Value::from_i64(2), Value::Text("Y".into())]); pager .io .block(|| agg.commit((&init_data).into(), &mut cursors)) .unwrap(); // Create a committed delta (to be processed) let mut committed_delta = Delta::new(); committed_delta.insert(3, vec![Value::from_i64(3), Value::Text("X".into())]); // Create uncommitted changes let mut uncommitted = Delta::new(); uncommitted.insert(4, vec![Value::from_i64(4), Value::Text("Y".into())]); uncommitted.insert(5, vec![Value::from_i64(5), Value::Text("Z".into())]); // Eval with both - should process both but not commit let mut combined = committed_delta.clone(); combined.merge(&uncommitted); let mut eval_state = combined.clone().into(); let result = pager .io .block(|| agg.eval(&mut eval_state, &mut cursors)) .unwrap(); // Result should reflect changes from both assert!(!result.changes.is_empty(), "Result should not be empty"); // Verify the DBSP pattern: retraction (-1) followed by insertion (1) for updates, // and just insertion (1) for new groups // We expect exactly 5 changes: // - X: retraction + insertion (was 1, now 2) // - Y: retraction + insertion (was 1, now 2) // - Z: insertion only (new group with count 1) assert_eq!( result.changes.len(), 5, "Should have 5 changes (2 retractions + 3 insertions)" ); // Sort by group name then by weight to get predictable order let mut sorted_changes: Vec<_> = result.changes.iter().collect(); sorted_changes.sort_by(|a, b| { let a_group = &a.0.values[0]; let b_group = &b.0.values[0]; match a_group.partial_cmp(b_group).unwrap() { std::cmp::Ordering::Equal => a.1.cmp(&b.1), // Sort by weight if same group other => other, } }); // Check X group: should have retraction (-1) for count=1, then insertion (1) for count=2 assert_eq!(sorted_changes[0].0.values[0], Value::Text("X".into())); assert_eq!(sorted_changes[0].0.values[1], Value::from_i64(1)); // old count assert_eq!(sorted_changes[0].1, -1); // retraction assert_eq!(sorted_changes[1].0.values[0], Value::Text("X".into())); assert_eq!(sorted_changes[1].0.values[1], Value::from_i64(2)); // new count assert_eq!(sorted_changes[1].1, 1); // insertion // Check Y group: should have retraction (-1) for count=1, then insertion (1) for count=2 assert_eq!(sorted_changes[2].0.values[0], Value::Text("Y".into())); assert_eq!(sorted_changes[2].0.values[1], Value::from_i64(1)); // old count assert_eq!(sorted_changes[2].1, -1); // retraction assert_eq!(sorted_changes[3].0.values[0], Value::Text("Y".into())); assert_eq!(sorted_changes[3].0.values[1], Value::from_i64(2)); // new count assert_eq!(sorted_changes[3].1, 1); // insertion // Check Z group: should only have insertion (1) for count=1 (new group) assert_eq!(sorted_changes[4].0.values[0], Value::Text("Z".into())); assert_eq!(sorted_changes[4].0.values[1], Value::from_i64(1)); // new count assert_eq!(sorted_changes[4].1, 1); // insertion only (no retraction as it's new); // But internal state should be unchanged let state = get_current_state_from_btree(&agg, &pager, &mut cursors); assert_eq!(state.changes.len(), 2, "Should still have only X and Y"); // Now commit only the committed_delta pager .io .block(|| agg.commit((&committed_delta).into(), &mut cursors)) .unwrap(); // State should now have X count=2, Y count=1 let final_state = get_current_state_from_btree(&agg, &pager, &mut cursors); let x = final_state .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("X".into())) .unwrap(); assert_eq!(x.0.values[1], Value::from_i64(2)); } #[test] fn test_min_max_basic() { // Test basic MIN/MAX functionality let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![], // No GROUP BY vec![ AggregateFunction::Min(2), // price is at index 2 AggregateFunction::Max(2), // price is at index 2 ], vec!["id".to_string(), "name".to_string(), "price".to_string()], ) .unwrap(); // Initial data let mut initial_delta = Delta::new(); initial_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Apple".into()), Value::from_f64(1.50), ], ); initial_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Banana".into()), Value::from_f64(0.75), ], ); initial_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Orange".into()), Value::from_f64(2.00), ], ); initial_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("Grape".into()), Value::from_f64(3.50), ], ); let result = pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Verify MIN and MAX assert_eq!(result.changes.len(), 1); let (row, weight) = &result.changes[0]; assert_eq!(*weight, 1); assert_eq!(row.values[0], Value::from_f64(0.75)); // MIN assert_eq!(row.values[1], Value::from_f64(3.50)); // MAX } #[test] fn test_min_max_deletion_updates_min() { // Test that deleting the MIN value updates to the next lowest let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![], // No GROUP BY vec![ AggregateFunction::Min(2), // price is at index 2 AggregateFunction::Max(2), // price is at index 2 ], vec!["id".to_string(), "name".to_string(), "price".to_string()], ) .unwrap(); // Initial data let mut initial_delta = Delta::new(); initial_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Apple".into()), Value::from_f64(1.50), ], ); initial_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Banana".into()), Value::from_f64(0.75), ], ); initial_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Orange".into()), Value::from_f64(2.00), ], ); initial_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("Grape".into()), Value::from_f64(3.50), ], ); pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Delete the MIN value (Banana at 0.75) let mut delete_delta = Delta::new(); delete_delta.delete( 2, vec![ Value::from_i64(2), Value::Text("Banana".into()), Value::from_f64(0.75), ], ); let result = pager .io .block(|| agg.commit((&delete_delta).into(), &mut cursors)) .unwrap(); // Should emit retraction of old values and new values assert_eq!(result.changes.len(), 2); // Find the retraction (weight = -1) let retraction = result.changes.iter().find(|(_, w)| *w == -1).unwrap(); assert_eq!(retraction.0.values[0], Value::from_f64(0.75)); // Old MIN assert_eq!(retraction.0.values[1], Value::from_f64(3.50)); // Old MAX // Find the new values (weight = 1) let new_values = result.changes.iter().find(|(_, w)| *w == 1).unwrap(); assert_eq!(new_values.0.values[0], Value::from_f64(1.50)); // New MIN (Apple) assert_eq!(new_values.0.values[1], Value::from_f64(3.50)); // MAX unchanged } #[test] fn test_min_max_deletion_updates_max() { // Test that deleting the MAX value updates to the next highest let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![], // No GROUP BY vec![ AggregateFunction::Min(2), // price is at index 2 AggregateFunction::Max(2), // price is at index 2 ], vec!["id".to_string(), "name".to_string(), "price".to_string()], ) .unwrap(); // Initial data let mut initial_delta = Delta::new(); initial_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Apple".into()), Value::from_f64(1.50), ], ); initial_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Banana".into()), Value::from_f64(0.75), ], ); initial_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Orange".into()), Value::from_f64(2.00), ], ); initial_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("Grape".into()), Value::from_f64(3.50), ], ); pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Delete the MAX value (Grape at 3.50) let mut delete_delta = Delta::new(); delete_delta.delete( 4, vec![ Value::from_i64(4), Value::Text("Grape".into()), Value::from_f64(3.50), ], ); let result = pager .io .block(|| agg.commit((&delete_delta).into(), &mut cursors)) .unwrap(); // Should emit retraction of old values and new values assert_eq!(result.changes.len(), 2); // Find the retraction (weight = -1) let retraction = result.changes.iter().find(|(_, w)| *w == -1).unwrap(); assert_eq!(retraction.0.values[0], Value::from_f64(0.75)); // Old MIN assert_eq!(retraction.0.values[1], Value::from_f64(3.50)); // Old MAX // Find the new values (weight = 1) let new_values = result.changes.iter().find(|(_, w)| *w == 1).unwrap(); assert_eq!(new_values.0.values[0], Value::from_f64(0.75)); // MIN unchanged assert_eq!(new_values.0.values[1], Value::from_f64(2.00)); // New MAX (Orange) } #[test] fn test_min_max_insertion_updates_min() { // Test that inserting a new MIN value updates the aggregate let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![], // No GROUP BY vec![ AggregateFunction::Min(2), // price is at index 2 AggregateFunction::Max(2), // price is at index 2 ], vec!["id".to_string(), "name".to_string(), "price".to_string()], ) .unwrap(); // Initial data let mut initial_delta = Delta::new(); initial_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Apple".into()), Value::from_f64(1.50), ], ); initial_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Orange".into()), Value::from_f64(2.00), ], ); initial_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Grape".into()), Value::from_f64(3.50), ], ); pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Insert a new MIN value let mut insert_delta = Delta::new(); insert_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("Lemon".into()), Value::from_f64(0.50), ], ); let result = pager .io .block(|| agg.commit((&insert_delta).into(), &mut cursors)) .unwrap(); // Should emit retraction of old values and new values assert_eq!(result.changes.len(), 2); // Find the retraction (weight = -1) let retraction = result.changes.iter().find(|(_, w)| *w == -1).unwrap(); assert_eq!(retraction.0.values[0], Value::from_f64(1.50)); // Old MIN assert_eq!(retraction.0.values[1], Value::from_f64(3.50)); // Old MAX // Find the new values (weight = 1) let new_values = result.changes.iter().find(|(_, w)| *w == 1).unwrap(); assert_eq!(new_values.0.values[0], Value::from_f64(0.50)); // New MIN (Lemon) assert_eq!(new_values.0.values[1], Value::from_f64(3.50)); // MAX unchanged } #[test] fn test_min_max_insertion_updates_max() { // Test that inserting a new MAX value updates the aggregate let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![], // No GROUP BY vec![ AggregateFunction::Min(2), // price is at index 2 AggregateFunction::Max(2), // price is at index 2 ], vec!["id".to_string(), "name".to_string(), "price".to_string()], ) .unwrap(); // Initial data let mut initial_delta = Delta::new(); initial_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Apple".into()), Value::from_f64(1.50), ], ); initial_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Orange".into()), Value::from_f64(2.00), ], ); initial_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Grape".into()), Value::from_f64(3.50), ], ); pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Insert a new MAX value let mut insert_delta = Delta::new(); insert_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("Melon".into()), Value::from_f64(5.00), ], ); let result = pager .io .block(|| agg.commit((&insert_delta).into(), &mut cursors)) .unwrap(); // Should emit retraction of old values and new values assert_eq!(result.changes.len(), 2); // Find the retraction (weight = -1) let retraction = result.changes.iter().find(|(_, w)| *w == -1).unwrap(); assert_eq!(retraction.0.values[0], Value::from_f64(1.50)); // Old MIN assert_eq!(retraction.0.values[1], Value::from_f64(3.50)); // Old MAX // Find the new values (weight = 1) let new_values = result.changes.iter().find(|(_, w)| *w == 1).unwrap(); assert_eq!(new_values.0.values[0], Value::from_f64(1.50)); // MIN unchanged assert_eq!(new_values.0.values[1], Value::from_f64(5.00)); // New MAX (Melon) } #[test] fn test_min_max_update_changes_min() { // Test that updating a row to become the new MIN updates the aggregate let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![], // No GROUP BY vec![ AggregateFunction::Min(2), // price is at index 2 AggregateFunction::Max(2), // price is at index 2 ], vec!["id".to_string(), "name".to_string(), "price".to_string()], ) .unwrap(); // Initial data let mut initial_delta = Delta::new(); initial_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Apple".into()), Value::from_f64(1.50), ], ); initial_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Orange".into()), Value::from_f64(2.00), ], ); initial_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Grape".into()), Value::from_f64(3.50), ], ); pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Update Orange price to be the new MIN (update = delete + insert) let mut update_delta = Delta::new(); update_delta.delete( 2, vec![ Value::from_i64(2), Value::Text("Orange".into()), Value::from_f64(2.00), ], ); update_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Orange".into()), Value::from_f64(0.25), ], ); let result = pager .io .block(|| agg.commit((&update_delta).into(), &mut cursors)) .unwrap(); // Should emit retraction of old values and new values assert_eq!(result.changes.len(), 2); // Find the retraction (weight = -1) let retraction = result.changes.iter().find(|(_, w)| *w == -1).unwrap(); assert_eq!(retraction.0.values[0], Value::from_f64(1.50)); // Old MIN assert_eq!(retraction.0.values[1], Value::from_f64(3.50)); // Old MAX // Find the new values (weight = 1) let new_values = result.changes.iter().find(|(_, w)| *w == 1).unwrap(); assert_eq!(new_values.0.values[0], Value::from_f64(0.25)); // New MIN (updated Orange) assert_eq!(new_values.0.values[1], Value::from_f64(3.50)); // MAX unchanged } #[test] fn test_min_max_with_group_by() { // Test MIN/MAX with GROUP BY let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![1], // GROUP BY category (index 1) vec![ AggregateFunction::Min(3), // price is at index 3 AggregateFunction::Max(3), // price is at index 3 ], vec![ "id".to_string(), "category".to_string(), "name".to_string(), "price".to_string(), ], ) .unwrap(); // Initial data with two categories let mut initial_delta = Delta::new(); initial_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("fruit".into()), Value::Text("Apple".into()), Value::from_f64(1.50), ], ); initial_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("fruit".into()), Value::Text("Banana".into()), Value::from_f64(0.75), ], ); initial_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("fruit".into()), Value::Text("Orange".into()), Value::from_f64(2.00), ], ); initial_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("veggie".into()), Value::Text("Carrot".into()), Value::from_f64(0.50), ], ); initial_delta.insert( 5, vec![ Value::from_i64(5), Value::Text("veggie".into()), Value::Text("Lettuce".into()), Value::from_f64(1.25), ], ); let result = pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Should have two groups assert_eq!(result.changes.len(), 2); // Find fruit group let fruit = result .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("fruit".into())) .unwrap(); assert_eq!(fruit.1, 1); // weight assert_eq!(fruit.0.values[1], Value::from_f64(0.75)); // MIN (Banana) assert_eq!(fruit.0.values[2], Value::from_f64(2.00)); // MAX (Orange) // Find veggie group let veggie = result .changes .iter() .find(|(row, _)| row.values[0] == Value::Text("veggie".into())) .unwrap(); assert_eq!(veggie.1, 1); // weight assert_eq!(veggie.0.values[1], Value::from_f64(0.50)); // MIN (Carrot) assert_eq!(veggie.0.values[2], Value::from_f64(1.25)); // MAX (Lettuce) } #[test] fn test_min_max_with_nulls() { // Test that NULL values are ignored in MIN/MAX let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![], // No GROUP BY vec![ AggregateFunction::Min(2), // price is at index 2 AggregateFunction::Max(2), // price is at index 2 ], vec!["id".to_string(), "name".to_string(), "price".to_string()], ) .unwrap(); // Initial data with NULL values let mut initial_delta = Delta::new(); initial_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Apple".into()), Value::from_f64(1.50), ], ); initial_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Unknown1".into()), Value::Null, ], ); initial_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Orange".into()), Value::from_f64(2.00), ], ); initial_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("Unknown2".into()), Value::Null, ], ); initial_delta.insert( 5, vec![ Value::from_i64(5), Value::Text("Grape".into()), Value::from_f64(3.50), ], ); let result = pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Verify MIN and MAX ignore NULLs assert_eq!(result.changes.len(), 1); let (row, weight) = &result.changes[0]; assert_eq!(*weight, 1); assert_eq!(row.values[0], Value::from_f64(1.50)); // MIN (Apple, ignoring NULLs) assert_eq!(row.values[1], Value::from_f64(3.50)); // MAX (Grape, ignoring NULLs) } #[test] fn test_min_max_integer_values() { // Test MIN/MAX with integer values instead of floats let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![], // No GROUP BY vec![ AggregateFunction::Min(2), // score is at index 2 AggregateFunction::Max(2), // score is at index 2 ], vec!["id".to_string(), "name".to_string(), "score".to_string()], ) .unwrap(); // Initial data with integer scores let mut initial_delta = Delta::new(); initial_delta.insert( 1, vec![ Value::from_i64(1), Value::Text("Alice".into()), Value::from_i64(85), ], ); initial_delta.insert( 2, vec![ Value::from_i64(2), Value::Text("Bob".into()), Value::from_i64(92), ], ); initial_delta.insert( 3, vec![ Value::from_i64(3), Value::Text("Carol".into()), Value::from_i64(78), ], ); initial_delta.insert( 4, vec![ Value::from_i64(4), Value::Text("Dave".into()), Value::from_i64(95), ], ); let result = pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Verify MIN and MAX with integers assert_eq!(result.changes.len(), 1); let (row, weight) = &result.changes[0]; assert_eq!(*weight, 1); assert_eq!(row.values[0], Value::from_i64(78)); // MIN (Carol) assert_eq!(row.values[1], Value::from_i64(95)); // MAX (Dave) } #[test] fn test_min_max_text_values() { // Test MIN/MAX with text values (alphabetical ordering) let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![], // No GROUP BY vec![ AggregateFunction::Min(1), // name is at index 1 AggregateFunction::Max(1), // name is at index 1 ], vec!["id".to_string(), "name".to_string()], ) .unwrap(); // Initial data with text values let mut initial_delta = Delta::new(); initial_delta.insert(1, vec![Value::from_i64(1), Value::Text("Charlie".into())]); initial_delta.insert(2, vec![Value::from_i64(2), Value::Text("Alice".into())]); initial_delta.insert(3, vec![Value::from_i64(3), Value::Text("Bob".into())]); initial_delta.insert(4, vec![Value::from_i64(4), Value::Text("David".into())]); let result = pager .io .block(|| agg.commit((&initial_delta).into(), &mut cursors)) .unwrap(); // Verify MIN and MAX with text (alphabetical) assert_eq!(result.changes.len(), 1); let (row, weight) = &result.changes[0]; assert_eq!(*weight, 1); assert_eq!(row.values[0], Value::Text("Alice".into())); // MIN alphabetically assert_eq!(row.values[1], Value::Text("David".into())); // MAX alphabetically } #[test] fn test_min_max_with_other_aggregates() { let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![], // No GROUP BY vec![ AggregateFunction::Count, AggregateFunction::Sum(1), // value is at index 1 AggregateFunction::Min(1), // value is at index 1 AggregateFunction::Max(1), // value is at index 1 AggregateFunction::Avg(1), // value is at index 1 ], vec!["id".to_string(), "value".to_string()], ) .unwrap(); // Initial data let mut delta = Delta::new(); delta.insert(1, vec![Value::from_i64(1), Value::from_i64(10)]); delta.insert(2, vec![Value::from_i64(2), Value::from_i64(5)]); delta.insert(3, vec![Value::from_i64(3), Value::from_i64(15)]); delta.insert(4, vec![Value::from_i64(4), Value::from_i64(20)]); let result = pager .io .block(|| agg.commit((&delta).into(), &mut cursors)) .unwrap(); assert_eq!(result.changes.len(), 1); let (row, weight) = &result.changes[0]; assert_eq!(*weight, 1); assert_eq!(row.values[0], Value::from_i64(4)); // COUNT assert_eq!(row.values[1], Value::from_i64(50)); // SUM assert_eq!(row.values[2], Value::from_i64(5)); // MIN assert_eq!(row.values[3], Value::from_i64(20)); // MAX assert_eq!(row.values[4], Value::from_f64(12.5)); // AVG (50/4) // Delete the MIN value let mut delta2 = Delta::new(); delta2.delete(2, vec![Value::from_i64(2), Value::from_i64(5)]); let result2 = pager .io .block(|| agg.commit((&delta2).into(), &mut cursors)) .unwrap(); assert_eq!(result2.changes.len(), 2); let (row_del, weight_del) = &result2.changes[0]; assert_eq!(*weight_del, -1); assert_eq!(row_del.values[0], Value::from_i64(4)); // Old COUNT assert_eq!(row_del.values[1], Value::from_i64(50)); // Old SUM assert_eq!(row_del.values[2], Value::from_i64(5)); // Old MIN assert_eq!(row_del.values[3], Value::from_i64(20)); // Old MAX assert_eq!(row_del.values[4], Value::from_f64(12.5)); // Old AVG let (row_ins, weight_ins) = &result2.changes[1]; assert_eq!(*weight_ins, 1); assert_eq!(row_ins.values[0], Value::from_i64(3)); // New COUNT assert_eq!(row_ins.values[1], Value::from_i64(45)); // New SUM assert_eq!(row_ins.values[2], Value::from_i64(10)); // New MIN assert_eq!(row_ins.values[3], Value::from_i64(20)); // MAX unchanged assert_eq!(row_ins.values[4], Value::from_f64(15.0)); // New AVG (45/3) // Now delete the MAX value let mut delta3 = Delta::new(); delta3.delete(4, vec![Value::from_i64(4), Value::from_i64(20)]); let result3 = pager .io .block(|| agg.commit((&delta3).into(), &mut cursors)) .unwrap(); assert_eq!(result3.changes.len(), 2); let (row_del2, weight_del2) = &result3.changes[0]; assert_eq!(*weight_del2, -1); assert_eq!(row_del2.values[3], Value::from_i64(20)); // Old MAX let (row_ins2, weight_ins2) = &result3.changes[1]; assert_eq!(*weight_ins2, 1); assert_eq!(row_ins2.values[0], Value::from_i64(2)); // COUNT assert_eq!(row_ins2.values[1], Value::from_i64(25)); // SUM assert_eq!(row_ins2.values[2], Value::from_i64(10)); // MIN unchanged assert_eq!(row_ins2.values[3], Value::from_i64(15)); // New MAX assert_eq!(row_ins2.values[4], Value::from_f64(12.5)); // AVG (25/2) } #[test] fn test_min_max_multiple_columns() { let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut agg = AggregateOperator::new( 1, // operator_id vec![], // No GROUP BY vec![ AggregateFunction::Min(0), // col1 is at index 0 AggregateFunction::Max(1), // col2 is at index 1 AggregateFunction::Min(2), // col3 is at index 2 ], vec!["col1".to_string(), "col2".to_string(), "col3".to_string()], ) .unwrap(); // Initial data let mut delta = Delta::new(); delta.insert( 1, vec![ Value::from_i64(10), Value::from_i64(100), Value::from_i64(1000), ], ); delta.insert( 2, vec![ Value::from_i64(5), Value::from_i64(200), Value::from_i64(2000), ], ); delta.insert( 3, vec![ Value::from_i64(15), Value::from_i64(150), Value::from_i64(500), ], ); let result = pager .io .block(|| agg.commit((&delta).into(), &mut cursors)) .unwrap(); assert_eq!(result.changes.len(), 1); let (row, weight) = &result.changes[0]; assert_eq!(*weight, 1); assert_eq!(row.values[0], Value::from_i64(5)); // MIN(col1) assert_eq!(row.values[1], Value::from_i64(200)); // MAX(col2) assert_eq!(row.values[2], Value::from_i64(500)); // MIN(col3) // Delete the row with MIN(col1) and MAX(col2) let mut delta2 = Delta::new(); delta2.delete( 2, vec![ Value::from_i64(5), Value::from_i64(200), Value::from_i64(2000), ], ); let result2 = pager .io .block(|| agg.commit((&delta2).into(), &mut cursors)) .unwrap(); assert_eq!(result2.changes.len(), 2); // Should emit delete of old state and insert of new state let (row_del, weight_del) = &result2.changes[0]; assert_eq!(*weight_del, -1); assert_eq!(row_del.values[0], Value::from_i64(5)); // Old MIN(col1) assert_eq!(row_del.values[1], Value::from_i64(200)); // Old MAX(col2) assert_eq!(row_del.values[2], Value::from_i64(500)); // Old MIN(col3) let (row_ins, weight_ins) = &result2.changes[1]; assert_eq!(*weight_ins, 1); assert_eq!(row_ins.values[0], Value::from_i64(10)); // New MIN(col1) assert_eq!(row_ins.values[1], Value::from_i64(150)); // New MAX(col2) assert_eq!(row_ins.values[2], Value::from_i64(500)); // MIN(col3) unchanged } #[test] fn test_join_operator_inner() { // Test INNER JOIN with incremental updates let (pager, table_page_id, index_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_page_id, 10); let index_def = create_dbsp_state_index(index_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_page_id, &index_def, 10); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut join = JoinOperator::new( 1, // operator_id JoinType::Inner, vec![0], // Join on first column vec![0], vec!["customer_id".to_string(), "amount".to_string()], vec!["id".to_string(), "name".to_string()], ) .unwrap(); // FIRST COMMIT: Initialize with data let mut left_delta = Delta::new(); left_delta.insert(1, vec![Value::from_i64(1), Value::from_f64(100.0)]); left_delta.insert(2, vec![Value::from_i64(2), Value::from_f64(200.0)]); left_delta.insert(3, vec![Value::from_i64(3), Value::from_f64(300.0)]); // No match initially let mut right_delta = Delta::new(); right_delta.insert(1, vec![Value::from_i64(1), Value::Text("Alice".into())]); right_delta.insert(2, vec![Value::from_i64(2), Value::Text("Bob".into())]); right_delta.insert(4, vec![Value::from_i64(4), Value::Text("David".into())]); // No match initially let delta_pair = DeltaPair::new(left_delta, right_delta); let result = pager .io .block(|| join.commit(delta_pair.clone(), &mut cursors)) .unwrap(); // Should have 2 matches (customer 1 and 2) assert_eq!( result.changes.len(), 2, "First commit should produce 2 matches" ); let mut results: Vec<_> = result.changes; results.sort_by_key(|r| r.0.values[0].clone()); assert_eq!(results[0].0.values[0], Value::from_i64(1)); assert_eq!(results[0].0.values[3], Value::Text("Alice".into())); assert_eq!(results[1].0.values[0], Value::from_i64(2)); assert_eq!(results[1].0.values[3], Value::Text("Bob".into())); // SECOND COMMIT: Add incremental data that should join with persisted state // Add a new left row that should match existing right row (customer 4) let mut left_delta2 = Delta::new(); left_delta2.insert(5, vec![Value::from_i64(4), Value::from_f64(400.0)]); // Should match David from persisted state // Add a new right row that should match existing left row (customer 3) let mut right_delta2 = Delta::new(); right_delta2.insert(6, vec![Value::from_i64(3), Value::Text("Charlie".into())]); // Should match customer 3 from persisted state let delta_pair2 = DeltaPair::new(left_delta2, right_delta2); let result2 = pager .io .block(|| join.commit(delta_pair2.clone(), &mut cursors)) .unwrap(); // The second commit should produce: // 1. New left (customer_id=4) joins with persisted right (id=4, David) // 2. Persisted left (customer_id=3) joins with new right (id=3, Charlie) assert_eq!( result2.changes.len(), 2, "Second commit should produce 2 new matches from incremental join. Got: {:?}", result2.changes ); // Verify the incremental results let mut results2: Vec<_> = result2.changes; results2.sort_by_key(|r| r.0.values[0].clone()); // Check for customer 3 joined with Charlie (existing left + new right) let charlie_match = results2 .iter() .find(|(row, _)| row.values[0] == Value::from_i64(3)) .expect("Should find customer 3 joined with new Charlie"); assert_eq!(charlie_match.0.values[2], Value::from_i64(3)); assert_eq!(charlie_match.0.values[3], Value::Text("Charlie".into())); // Check for customer 4 joined with David (new left + existing right) let david_match = results2 .iter() .find(|(row, _)| row.values[0] == Value::from_i64(4)) .expect("Should find new customer 4 joined with existing David"); assert_eq!(david_match.0.values[0], Value::from_i64(4)); assert_eq!(david_match.0.values[3], Value::Text("David".into())); } #[test] fn test_join_operator_with_deletions() { // Test INNER JOIN with deletions (negative weights) let (pager, table_page_id, index_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_page_id, 10); let index_def = create_dbsp_state_index(index_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_page_id, &index_def, 10); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut join = JoinOperator::new( 1, // operator_id JoinType::Inner, vec![0], // Join on first column vec![0], vec!["customer_id".to_string(), "amount".to_string()], vec!["id".to_string(), "name".to_string()], ) .unwrap(); // FIRST COMMIT: Add initial data let mut left_delta = Delta::new(); left_delta.insert(1, vec![Value::from_i64(1), Value::from_f64(100.0)]); left_delta.insert(2, vec![Value::from_i64(2), Value::from_f64(200.0)]); left_delta.insert(3, vec![Value::from_i64(3), Value::from_f64(300.0)]); let mut right_delta = Delta::new(); right_delta.insert(1, vec![Value::from_i64(1), Value::Text("Alice".into())]); right_delta.insert(2, vec![Value::from_i64(2), Value::Text("Bob".into())]); right_delta.insert(3, vec![Value::from_i64(3), Value::Text("Charlie".into())]); let delta_pair = DeltaPair::new(left_delta, right_delta); let result = pager .io .block(|| join.commit(delta_pair.clone(), &mut cursors)) .unwrap(); assert_eq!(result.changes.len(), 3, "Should have 3 initial joins"); // SECOND COMMIT: Delete customer 2 from left side let mut left_delta2 = Delta::new(); left_delta2.delete(2, vec![Value::from_i64(2), Value::from_f64(200.0)]); let empty_right = Delta::new(); let delta_pair2 = DeltaPair::new(left_delta2, empty_right); let result2 = pager .io .block(|| join.commit(delta_pair2.clone(), &mut cursors)) .unwrap(); // Should produce 1 deletion (retraction) of the join for customer 2 assert_eq!( result2.changes.len(), 1, "Should produce 1 retraction for deleted customer 2" ); assert_eq!( result2.changes[0].1, -1, "Should have weight -1 for deletion" ); assert_eq!(result2.changes[0].0.values[0], Value::from_i64(2)); assert_eq!(result2.changes[0].0.values[3], Value::Text("Bob".into())); // THIRD COMMIT: Delete customer 3 from right side let empty_left = Delta::new(); let mut right_delta3 = Delta::new(); right_delta3.delete(3, vec![Value::from_i64(3), Value::Text("Charlie".into())]); let delta_pair3 = DeltaPair::new(empty_left, right_delta3); let result3 = pager .io .block(|| join.commit(delta_pair3.clone(), &mut cursors)) .unwrap(); // Should produce 1 deletion (retraction) of the join for customer 3 assert_eq!( result3.changes.len(), 1, "Should produce 1 retraction for deleted customer 3" ); assert_eq!( result3.changes[0].1, -1, "Should have weight -1 for deletion" ); assert_eq!(result3.changes[0].0.values[0], Value::from_i64(3)); assert_eq!(result3.changes[0].0.values[2], Value::from_i64(3)); } #[test] fn test_join_operator_one_to_many() { // Test one-to-many relationship: one customer with multiple orders let (pager, table_page_id, index_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_page_id, 10); let index_def = create_dbsp_state_index(index_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_page_id, &index_def, 10); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut join = JoinOperator::new( 1, // operator_id JoinType::Inner, vec![0], // Join on first column (customer_id for orders) vec![0], // Join on first column (id for customers) vec![ "customer_id".to_string(), "order_id".to_string(), "amount".to_string(), ], vec!["id".to_string(), "name".to_string()], ) .unwrap(); // FIRST COMMIT: Add one customer let left_delta = Delta::new(); // Empty orders initially let mut right_delta = Delta::new(); right_delta.insert(1, vec![Value::from_i64(100), Value::Text("Alice".into())]); let delta_pair = DeltaPair::new(left_delta, right_delta); let result = pager .io .block(|| join.commit(delta_pair.clone(), &mut cursors)) .unwrap(); // No joins yet (customer exists but no orders) assert_eq!( result.changes.len(), 0, "Should have no joins with customer but no orders" ); // SECOND COMMIT: Add multiple orders for the same customer let mut left_delta2 = Delta::new(); left_delta2.insert( 1, vec![ Value::from_i64(100), Value::from_i64(1001), Value::from_f64(50.0), ], ); // order 1001 left_delta2.insert( 2, vec![ Value::from_i64(100), Value::from_i64(1002), Value::from_f64(75.0), ], ); // order 1002 left_delta2.insert( 3, vec![ Value::from_i64(100), Value::from_i64(1003), Value::from_f64(100.0), ], ); // order 1003 let right_delta2 = Delta::new(); // No new customers let delta_pair2 = DeltaPair::new(left_delta2, right_delta2); let result2 = pager .io .block(|| join.commit(delta_pair2.clone(), &mut cursors)) .unwrap(); // Should produce 3 joins (3 orders × 1 customer) assert_eq!( result2.changes.len(), 3, "Should produce 3 joins for 3 orders with same customer. Got: {:?}", result2.changes ); // Verify all three joins have the same customer but different orders for (row, weight) in &result2.changes { assert_eq!(*weight, 1, "Weight should be 1 for insertion"); assert_eq!( row.values[0], Value::from_i64(100), "Customer ID should be 100" ); assert_eq!( row.values[4], Value::Text("Alice".into()), "Customer name should be Alice" ); // Check order IDs are different let order_id = match &row.values[1] { Value::Numeric(Numeric::Integer(id)) => *id, _ => panic!("Expected integer order ID"), }; assert!( (1001..=1003).contains(&order_id), "Order ID {order_id} should be between 1001 and 1003" ); } // THIRD COMMIT: Delete one order let mut left_delta3 = Delta::new(); left_delta3.delete( 2, vec![ Value::from_i64(100), Value::from_i64(1002), Value::from_f64(75.0), ], ); let delta_pair3 = DeltaPair::new(left_delta3, Delta::new()); let result3 = pager .io .block(|| join.commit(delta_pair3.clone(), &mut cursors)) .unwrap(); // Should produce 1 retraction for the deleted order assert_eq!(result3.changes.len(), 1, "Should produce 1 retraction"); assert_eq!(result3.changes[0].1, -1, "Should be a deletion"); assert_eq!( result3.changes[0].0.values[1], Value::from_i64(1002), "Should delete order 1002" ); } #[test] fn test_join_operator_many_to_many() { // Test many-to-many: multiple rows with same key on both sides let (pager, table_page_id, index_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_page_id, 10); let index_def = create_dbsp_state_index(index_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_page_id, &index_def, 10); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut join = JoinOperator::new( 1, // operator_id JoinType::Inner, vec![0], // Join on category_id vec![0], // Join on id vec![ "category_id".to_string(), "product_name".to_string(), "price".to_string(), ], vec!["id".to_string(), "category_name".to_string()], ) .unwrap(); // FIRST COMMIT: Add multiple products in same category let mut left_delta = Delta::new(); left_delta.insert( 1, vec![ Value::from_i64(10), Value::Text("Laptop".into()), Value::from_f64(1000.0), ], ); left_delta.insert( 2, vec![ Value::from_i64(10), Value::Text("Mouse".into()), Value::from_f64(50.0), ], ); left_delta.insert( 3, vec![ Value::from_i64(10), Value::Text("Keyboard".into()), Value::from_f64(100.0), ], ); // Add multiple categories with same ID (simulating denormalized data or versioning) let mut right_delta = Delta::new(); right_delta.insert( 1, vec![Value::from_i64(10), Value::Text("Electronics".into())], ); right_delta.insert( 2, vec![Value::from_i64(10), Value::Text("Computers".into())], ); // Same category ID, different name let delta_pair = DeltaPair::new(left_delta, right_delta); let result = pager .io .block(|| join.commit(delta_pair.clone(), &mut cursors)) .unwrap(); // Should produce 3 products × 2 categories = 6 joins assert_eq!( result.changes.len(), 6, "Should produce 6 joins (3 products × 2 category records). Got: {:?}", result.changes ); // Verify we have all combinations let mut found_combinations = HashSet::default(); for (row, weight) in &result.changes { assert_eq!(*weight, 1); let product = row.values[1].to_string(); let category = row.values[4].to_string(); found_combinations.insert((product, category)); } assert_eq!( found_combinations.len(), 6, "Should have 6 unique combinations" ); // SECOND COMMIT: Add one more product in the same category let mut left_delta2 = Delta::new(); left_delta2.insert( 4, vec![ Value::from_i64(10), Value::Text("Monitor".into()), Value::from_f64(500.0), ], ); let delta_pair2 = DeltaPair::new(left_delta2, Delta::new()); let result2 = pager .io .block(|| join.commit(delta_pair2.clone(), &mut cursors)) .unwrap(); // New product should join with both existing category records assert_eq!( result2.changes.len(), 2, "New product should join with 2 existing category records" ); for (row, _) in &result2.changes { assert_eq!(row.values[1], Value::Text("Monitor".into())); } } #[test] fn test_join_operator_update_in_one_to_many() { // Test updates in one-to-many scenarios let (pager, table_page_id, index_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_page_id, 10); let index_def = create_dbsp_state_index(index_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_page_id, &index_def, 10); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut join = JoinOperator::new( 1, // operator_id JoinType::Inner, vec![0], // Join on customer_id vec![0], // Join on id vec![ "customer_id".to_string(), "order_id".to_string(), "amount".to_string(), ], vec!["id".to_string(), "name".to_string()], ) .unwrap(); // FIRST COMMIT: Setup one customer with multiple orders let mut left_delta = Delta::new(); left_delta.insert( 1, vec![ Value::from_i64(100), Value::from_i64(1001), Value::from_f64(50.0), ], ); left_delta.insert( 2, vec![ Value::from_i64(100), Value::from_i64(1002), Value::from_f64(75.0), ], ); left_delta.insert( 3, vec![ Value::from_i64(100), Value::from_i64(1003), Value::from_f64(100.0), ], ); let mut right_delta = Delta::new(); right_delta.insert(1, vec![Value::from_i64(100), Value::Text("Alice".into())]); let delta_pair = DeltaPair::new(left_delta, right_delta); let result = pager .io .block(|| join.commit(delta_pair.clone(), &mut cursors)) .unwrap(); assert_eq!(result.changes.len(), 3, "Should have 3 initial joins"); // SECOND COMMIT: Update the customer name (affects all 3 joins) let mut right_delta2 = Delta::new(); // Delete old customer record right_delta2.delete(1, vec![Value::from_i64(100), Value::Text("Alice".into())]); // Insert updated customer record right_delta2.insert( 1, vec![Value::from_i64(100), Value::Text("Alice Smith".into())], ); let delta_pair2 = DeltaPair::new(Delta::new(), right_delta2); let result2 = pager .io .block(|| join.commit(delta_pair2.clone(), &mut cursors)) .unwrap(); // Should produce 3 deletions and 3 insertions (one for each order) assert_eq!(result2.changes.len(), 6, "Should produce 6 changes (3 deletions + 3 insertions) when updating customer with 3 orders"); let deletions: Vec<_> = result2.changes.iter().filter(|(_, w)| *w == -1).collect(); let insertions: Vec<_> = result2.changes.iter().filter(|(_, w)| *w == 1).collect(); assert_eq!(deletions.len(), 3, "Should have 3 deletions"); assert_eq!(insertions.len(), 3, "Should have 3 insertions"); // Check all deletions have old name for (row, _) in &deletions { assert_eq!( row.values[4], Value::Text("Alice".into()), "Deletions should have old name" ); } // Check all insertions have new name for (row, _) in &insertions { assert_eq!( row.values[4], Value::Text("Alice Smith".into()), "Insertions should have new name" ); } // Verify we still have all three order IDs in the insertions let mut order_ids = HashSet::default(); for (row, _) in &insertions { if let Value::Numeric(Numeric::Integer(order_id)) = &row.values[1] { order_ids.insert(*order_id); } } assert_eq!( order_ids.len(), 3, "Should still have all 3 order IDs after update" ); assert!(order_ids.contains(&1001)); assert!(order_ids.contains(&1002)); assert!(order_ids.contains(&1003)); } #[test] fn test_join_operator_weight_accumulation_complex() { // Test complex weight accumulation with multiple identical rows let (pager, table_page_id, index_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_page_id, 10); let index_def = create_dbsp_state_index(index_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_page_id, &index_def, 10); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut join = JoinOperator::new( 1, // operator_id JoinType::Inner, vec![0], // Join on first column vec![0], vec!["key".to_string(), "val_left".to_string()], vec!["key".to_string(), "val_right".to_string()], ) .unwrap(); // FIRST COMMIT: Add identical rows multiple times (simulating duplicates) let mut left_delta = Delta::new(); // Same key-value pair inserted 3 times with different rowids left_delta.insert(1, vec![Value::from_i64(10), Value::Text("A".into())]); left_delta.insert(2, vec![Value::from_i64(10), Value::Text("A".into())]); left_delta.insert(3, vec![Value::from_i64(10), Value::Text("A".into())]); let mut right_delta = Delta::new(); // Same key-value pair inserted 2 times right_delta.insert(4, vec![Value::from_i64(10), Value::Text("B".into())]); right_delta.insert(5, vec![Value::from_i64(10), Value::Text("B".into())]); let delta_pair = DeltaPair::new(left_delta, right_delta); let result = pager .io .block(|| join.commit(delta_pair.clone(), &mut cursors)) .unwrap(); // Should produce 3 × 2 = 6 join results (cartesian product) assert_eq!( result.changes.len(), 6, "Should produce 6 joins (3 left rows × 2 right rows)" ); // All should have weight 1 for (_, weight) in &result.changes { assert_eq!(*weight, 1); } // SECOND COMMIT: Delete one instance from left let mut left_delta2 = Delta::new(); left_delta2.delete(2, vec![Value::from_i64(10), Value::Text("A".into())]); let delta_pair2 = DeltaPair::new(left_delta2, Delta::new()); let result2 = pager .io .block(|| join.commit(delta_pair2.clone(), &mut cursors)) .unwrap(); // Should produce 2 retractions (1 deleted left row × 2 right rows) assert_eq!( result2.changes.len(), 2, "Should produce 2 retractions when deleting 1 of 3 identical left rows" ); for (_, weight) in &result2.changes { assert_eq!(*weight, -1, "Should be retractions"); } } #[test] fn test_join_produces_all_expected_results() { // Test that a join produces ALL expected output rows // This reproduces the issue where only 1 of 3 expected rows appears in the final result // Create a join operator similar to: SELECT u.name, o.quantity FROM users u JOIN orders o ON u.id = o.user_id let mut join = JoinOperator::new( 0, JoinType::Inner, vec![0], // Join on first column (id) vec![0], // Join on first column (user_id) vec!["id".to_string(), "name".to_string()], vec![ "user_id".to_string(), "product_id".to_string(), "quantity".to_string(), ], ) .unwrap(); // Create test data matching the example that fails: // users: (1, 'Alice'), (2, 'Bob') // orders: (1, 5), (1, 3), (2, 7) -- user_id, quantity let left_delta = Delta { changes: vec![ ( HashableRow::new( 1, vec![Value::from_i64(1), Value::Text(Text::from("Alice"))], ), 1, ), ( HashableRow::new(2, vec![Value::from_i64(2), Value::Text(Text::from("Bob"))]), 1, ), ], }; // Orders: Alice has 2 orders, Bob has 1 let right_delta = Delta { changes: vec![ ( HashableRow::new( 1, vec![Value::from_i64(1), Value::from_i64(100), Value::from_i64(5)], ), 1, ), ( HashableRow::new( 2, vec![Value::from_i64(1), Value::from_i64(101), Value::from_i64(3)], ), 1, ), ( HashableRow::new( 3, vec![Value::from_i64(2), Value::from_i64(100), Value::from_i64(7)], ), 1, ), ], }; // Evaluate the join let delta_pair = DeltaPair::new(left_delta, right_delta); let mut state = EvalState::Init { deltas: delta_pair }; let (pager, table_root, index_root) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root, 5); let index_def = create_dbsp_state_index(index_root); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let result = pager .io .block(|| join.eval(&mut state, &mut cursors)) .unwrap(); // Should produce 3 results: Alice with 2 orders, Bob with 1 order assert_eq!( result.changes.len(), 3, "Should produce 3 joined rows (Alice×2 + Bob×1)" ); // Verify the actual content of the results let mut expected_results = HashSet::default(); // Expected: (Alice, 5), (Alice, 3), (Bob, 7) expected_results.insert(("Alice".to_string(), 5)); expected_results.insert(("Alice".to_string(), 3)); expected_results.insert(("Bob".to_string(), 7)); let mut actual_results = HashSet::default(); for (row, weight) in &result.changes { assert_eq!(*weight, 1, "All results should have weight 1"); // Extract name (column 1 from left) and quantity (column 3 from right) let name = match &row.values[1] { Value::Text(t) => t.as_str().to_string(), _ => panic!("Expected text value for name"), }; let quantity = match &row.values[4] { Value::Numeric(Numeric::Integer(q)) => *q, _ => panic!("Expected integer value for quantity"), }; actual_results.insert((name, quantity)); } assert_eq!( expected_results, actual_results, "Join should produce all expected results. Expected: {expected_results:?}, Got: {actual_results:?}", ); // Also verify that rowids are unique (this is important for btree storage) let mut seen_rowids = HashSet::default(); for (row, _) in &result.changes { let was_new = seen_rowids.insert(row.rowid); assert!(was_new, "Duplicate rowid found: {}. This would cause rows to overwrite each other in btree storage!", row.rowid); } } // Merge operator tests use crate::incremental::merge_operator::{MergeOperator, UnionMode}; #[test] fn test_merge_operator_basic() { let (_pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(_pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(_pager, index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut merge_op = MergeOperator::new( 1, UnionMode::All { left_table: "table1".to_string(), right_table: "table2".to_string(), }, ); // Create two deltas let mut left_delta = Delta::new(); left_delta.insert(1, vec![Value::from_i64(1)]); left_delta.insert(2, vec![Value::from_i64(2)]); let mut right_delta = Delta::new(); right_delta.insert(3, vec![Value::from_i64(3)]); right_delta.insert(4, vec![Value::from_i64(4)]); let delta_pair = DeltaPair::new(left_delta, right_delta); // Evaluate merge let result = merge_op.commit(delta_pair, &mut cursors).unwrap(); if let IOResult::Done(merged) = result { // Should have all 4 entries assert_eq!(merged.len(), 4); // Check that all values are present let values: Vec = merged .changes .iter() .filter_map(|(row, weight)| { if *weight > 0 && !row.values.is_empty() { if let Value::Numeric(Numeric::Integer(n)) = &row.values[0] { Some(*n) } else { None } } else { None } }) .collect(); assert!(values.contains(&1)); assert!(values.contains(&2)); assert!(values.contains(&3)); assert!(values.contains(&4)); } else { panic!("Expected Done result"); } } #[test] fn test_merge_operator_stateful_distinct() { let (_pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(_pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(_pager, index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); // Test that UNION (distinct) properly deduplicates across multiple operations let mut merge_op = MergeOperator::new(7, UnionMode::Distinct); // First operation: insert values 1, 2, 3 from left and 2, 3, 4 from right let mut left_delta1 = Delta::new(); left_delta1.insert(1, vec![Value::from_i64(1)]); left_delta1.insert(2, vec![Value::from_i64(2)]); left_delta1.insert(3, vec![Value::from_i64(3)]); let mut right_delta1 = Delta::new(); right_delta1.insert(4, vec![Value::from_i64(2)]); // Duplicate value 2 right_delta1.insert(5, vec![Value::from_i64(3)]); // Duplicate value 3 right_delta1.insert(6, vec![Value::from_i64(4)]); let result1 = merge_op .commit(DeltaPair::new(left_delta1, right_delta1), &mut cursors) .unwrap(); if let IOResult::Done(merged1) = result1 { // Should have 4 unique values (1, 2, 3, 4) // But 6 total entries (3 from left + 3 from right) assert_eq!(merged1.len(), 6); // Collect unique rowids - should be 4 let unique_rowids: HashSet = merged1.changes.iter().map(|(row, _)| row.rowid).collect(); assert_eq!( unique_rowids.len(), 4, "Should have 4 unique rowids for 4 unique values" ); } else { panic!("Expected Done result"); } // Second operation: insert value 2 again from left, and value 5 from right let mut left_delta2 = Delta::new(); left_delta2.insert(7, vec![Value::from_i64(2)]); // Duplicate of existing value let mut right_delta2 = Delta::new(); right_delta2.insert(8, vec![Value::from_i64(5)]); // New value let result2 = merge_op .commit(DeltaPair::new(left_delta2, right_delta2), &mut cursors) .unwrap(); if let IOResult::Done(merged2) = result2 { assert_eq!(merged2.len(), 2, "Should have 2 entries in delta"); // Check that value 2 got the same rowid as before let has_existing_rowid = merged2 .changes .iter() .any(|(row, _)| row.values == vec![Value::from_i64(2)] && row.rowid <= 4); assert!(has_existing_rowid, "Value 2 should reuse existing rowid"); // Check that value 5 got a new rowid let has_new_rowid = merged2 .changes .iter() .any(|(row, _)| row.values == vec![Value::from_i64(5)] && row.rowid > 4); assert!(has_new_rowid, "Value 5 should get a new rowid"); } else { panic!("Expected Done result"); } } #[test] fn test_merge_operator_single_sided_inputs_union_all() { let (_pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(_pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(_pager, index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); // Test UNION ALL with inputs coming from only one side at a time let mut merge_op = MergeOperator::new( 10, UnionMode::All { left_table: "orders".to_string(), right_table: "archived_orders".to_string(), }, ); // First: only left side (orders) has data let mut left_delta1 = Delta::new(); left_delta1.insert(100, vec![Value::from_i64(1001)]); left_delta1.insert(101, vec![Value::from_i64(1002)]); let right_delta1 = Delta::new(); // Empty right side let result1 = merge_op .commit(DeltaPair::new(left_delta1, right_delta1), &mut cursors) .unwrap(); let first_rowids = if let IOResult::Done(ref merged1) = result1 { assert_eq!(merged1.len(), 2, "Should have 2 entries from left only"); merged1 .changes .iter() .map(|(row, _)| row.rowid) .collect::>() } else { panic!("Expected Done result"); }; // Second: only right side (archived_orders) has data let left_delta2 = Delta::new(); // Empty left side let mut right_delta2 = Delta::new(); right_delta2.insert(100, vec![Value::from_i64(2001)]); // Same rowid as left, different table right_delta2.insert(102, vec![Value::from_i64(2002)]); let result2 = merge_op .commit(DeltaPair::new(left_delta2, right_delta2), &mut cursors) .unwrap(); let second_result_rowid_100 = if let IOResult::Done(ref merged2) = result2 { assert_eq!(merged2.len(), 2, "Should have 2 entries from right only"); // Rowids should be different from the left side even though original rowid 100 is the same let second_rowids: Vec = merged2.changes.iter().map(|(row, _)| row.rowid).collect(); for rowid in &second_rowids { assert!( !first_rowids.contains(rowid), "Right side rowids should be different from left side rowids" ); } // Save rowid for archived_orders.100 merged2 .changes .iter() .find(|(row, _)| row.values == vec![Value::from_i64(2001)]) .map(|(row, _)| row.rowid) .unwrap() } else { panic!("Expected Done result"); }; // Third: left side again with same rowids as before let mut left_delta3 = Delta::new(); left_delta3.insert(100, vec![Value::from_i64(1003)]); // Same rowid 100 from orders left_delta3.insert(101, vec![Value::from_i64(1004)]); // Same rowid 101 from orders let right_delta3 = Delta::new(); // Empty right side let result3 = merge_op .commit(DeltaPair::new(left_delta3, right_delta3), &mut cursors) .unwrap(); if let IOResult::Done(merged3) = result3 { assert_eq!(merged3.len(), 2, "Should have 2 entries from left"); // Should get the same assigned rowids as the first operation let third_rowids: Vec = merged3.changes.iter().map(|(row, _)| row.rowid).collect(); assert_eq!( first_rowids, third_rowids, "Same (table, rowid) pairs should get same assigned rowids" ); } else { panic!("Expected Done result"); } // Fourth: right side again with rowid 100 let left_delta4 = Delta::new(); // Empty left side let mut right_delta4 = Delta::new(); right_delta4.insert(100, vec![Value::from_i64(2003)]); // Same rowid 100 from archived_orders let result4 = merge_op .commit(DeltaPair::new(left_delta4, right_delta4), &mut cursors) .unwrap(); if let IOResult::Done(merged4) = result4 { assert_eq!(merged4.len(), 1, "Should have 1 entry from right"); // Should get same assigned rowid as second operation for archived_orders.100 let fourth_rowid = merged4.changes[0].0.rowid; assert_eq!( fourth_rowid, second_result_rowid_100, "archived_orders rowid 100 should consistently map to same assigned rowid" ); } else { panic!("Expected Done result"); } } #[test] fn test_merge_operator_both_sides_empty() { let (_pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(_pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(_pager, index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); // Test that both sides being empty works correctly let mut merge_op = MergeOperator::new( 12, UnionMode::All { left_table: "t1".to_string(), right_table: "t2".to_string(), }, ); // First: insert some data to establish state let mut left_delta1 = Delta::new(); left_delta1.insert(1, vec![Value::from_i64(100)]); let mut right_delta1 = Delta::new(); right_delta1.insert(1, vec![Value::from_i64(200)]); let result1 = merge_op .commit(DeltaPair::new(left_delta1, right_delta1), &mut cursors) .unwrap(); let original_t1_rowid = if let IOResult::Done(ref merged1) = result1 { assert_eq!(merged1.len(), 2, "Should have 2 entries initially"); // Save the rowid for t1.rowid=1 merged1 .changes .iter() .find(|(row, _)| row.values == vec![Value::from_i64(100)]) .map(|(row, _)| row.rowid) .unwrap() } else { panic!("Expected Done result"); }; // Second: both sides empty - should produce empty output let empty_left = Delta::new(); let empty_right = Delta::new(); let result2 = merge_op .commit(DeltaPair::new(empty_left, empty_right), &mut cursors) .unwrap(); if let IOResult::Done(merged2) = result2 { assert_eq!( merged2.len(), 0, "Both empty sides should produce empty output" ); } else { panic!("Expected Done result"); } // Third: add more data to verify state is still intact let mut left_delta3 = Delta::new(); left_delta3.insert(1, vec![Value::from_i64(101)]); // Same rowid as before let right_delta3 = Delta::new(); let result3 = merge_op .commit(DeltaPair::new(left_delta3, right_delta3), &mut cursors) .unwrap(); if let IOResult::Done(merged3) = result3 { assert_eq!(merged3.len(), 1, "Should have 1 entry"); // Should reuse the same assigned rowid for t1.rowid=1 let rowid = merged3.changes[0].0.rowid; assert_eq!( rowid, original_t1_rowid, "Should maintain consistent rowid mapping after empty operation" ); } else { panic!("Expected Done result"); } } #[test] fn test_aggregate_serialization_with_different_column_indices() { // Test that aggregate state serialization correctly preserves column indices // when multiple aggregates operate on different columns let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); // Create first operator with SUM(col1), MIN(col3) GROUP BY col0 let mut agg1 = AggregateOperator::new( 1, vec![0], vec![AggregateFunction::Sum(1), AggregateFunction::Min(3)], vec![ "group".to_string(), "val1".to_string(), "val2".to_string(), "val3".to_string(), ], ) .unwrap(); // Add initial data let mut delta = Delta::new(); delta.insert( 1, vec![ Value::Text("A".into()), Value::from_i64(10), Value::from_i64(100), Value::from_i64(5), ], ); delta.insert( 2, vec![ Value::Text("A".into()), Value::from_i64(15), Value::from_i64(200), Value::from_i64(3), ], ); let result1 = pager .io .block(|| agg1.commit((&delta).into(), &mut cursors)) .unwrap(); assert_eq!(result1.changes.len(), 1); let (row1, _) = &result1.changes[0]; assert_eq!(row1.values[0], Value::Text("A".into())); assert_eq!(row1.values[1], Value::from_i64(25)); // SUM(val1) = 10 + 15 assert_eq!(row1.values[2], Value::from_i64(3)); // MIN(val3) = min(5, 3) // Create operator with same ID but different column mappings: SUM(col3), MIN(col1) let mut agg2 = AggregateOperator::new( 1, // Same operator_id vec![0], vec![AggregateFunction::Sum(3), AggregateFunction::Min(1)], vec![ "group".to_string(), "val1".to_string(), "val2".to_string(), "val3".to_string(), ], ) .unwrap(); // Process new data let mut delta2 = Delta::new(); delta2.insert( 3, vec![ Value::Text("A".into()), Value::from_i64(20), Value::from_i64(300), Value::from_i64(4), ], ); let result2 = pager .io .block(|| agg2.commit((&delta2).into(), &mut cursors)) .unwrap(); // Find the positive weight row for group A (the updated aggregate) let row2 = result2 .changes .iter() .find(|(row, weight)| row.values[0] == Value::Text("A".into()) && *weight > 0) .expect("Should have a positive weight row for group A"); let (row2, _) = row2; // Verify that column indices are preserved correctly in serialization // When agg2 processes the data with different column mappings: // - It reads the existing state which has SUM(col1)=25 and MIN(col3)=3 // - For SUM(col3), there's no existing state, so it starts fresh: 4 // - For MIN(col1), there's no existing state, so it starts fresh: 20 assert_eq!( row2.values[1], Value::from_i64(4), "SUM(col3) should be 4 (new data only)" ); assert_eq!( row2.values[2], Value::from_i64(20), "MIN(col1) should be 20 (new data only)" ); } #[test] fn test_distinct_removes_duplicates() { let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); // Create a DISTINCT operator that groups by all columns let mut operator = AggregateOperator::new( 0, // operator_id vec![0], // group by column 0 (value) vec![], // Empty aggregates for plain DISTINCT vec!["value".to_string()], ) .unwrap(); // Create input with duplicates let mut input = Delta::new(); input.insert(1, vec![Value::from_i64(100)]); // First 100 input.insert(2, vec![Value::from_i64(200)]); // First 200 input.insert(3, vec![Value::from_i64(100)]); // Duplicate 100 input.insert(4, vec![Value::from_i64(300)]); // First 300 input.insert(5, vec![Value::from_i64(200)]); // Duplicate 200 input.insert(6, vec![Value::from_i64(100)]); // Another duplicate 100 // Execute commit (for materialized views) instead of eval let result = pager .io .block(|| operator.commit((&input).into(), &mut cursors)) .unwrap(); // Should have exactly 3 distinct values (100, 200, 300) let distinct_values: HashSet = result .changes .iter() .map(|(row, _weight)| match &row.values[0] { Value::Numeric(Numeric::Integer(i)) => *i, _ => panic!("Expected integer value"), }) .collect(); assert_eq!( distinct_values.len(), 3, "Should have exactly 3 distinct values" ); assert!(distinct_values.contains(&100)); assert!(distinct_values.contains(&200)); assert!(distinct_values.contains(&300)); // All weights should be 1 (distinct normalizes weights) for (_row, weight) in &result.changes { assert_eq!(*weight, 1, "DISTINCT should output weight 1 for all groups"); } } #[test] fn test_distinct_incremental_updates() { let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut operator = AggregateOperator::new( 0, vec![0, 1], // group by both columns vec![], // Empty aggregates for plain DISTINCT vec!["category".to_string(), "value".to_string()], ) .unwrap(); // First batch: insert some values let mut delta1 = Delta::new(); delta1.insert(1, vec![Value::Text("A".into()), Value::from_i64(100)]); delta1.insert(2, vec![Value::Text("B".into()), Value::from_i64(200)]); delta1.insert(3, vec![Value::Text("A".into()), Value::from_i64(100)]); // Duplicate // Commit first batch let result1 = pager .io .block(|| operator.commit((&delta1).into(), &mut cursors)) .unwrap(); // Should have 2 distinct groups: (A,100) and (B,200) assert_eq!( result1.changes.len(), 2, "First commit should output 2 distinct groups" ); // Verify each group appears with weight +1 for (_row, weight) in &result1.changes { assert_eq!(*weight, 1, "New groups should have weight +1"); } // Second batch: delete one instance of (A,100) and add new group let mut delta2 = Delta::new(); delta2.delete(1, vec![Value::Text("A".into()), Value::from_i64(100)]); delta2.insert(4, vec![Value::Text("C".into()), Value::from_i64(300)]); let result2 = pager .io .block(|| operator.commit((&delta2).into(), &mut cursors)) .unwrap(); // Should only output the new group (C,300) with weight +1 // (A,100) still exists (weight went from 2 to 1), so no output for it assert_eq!( result2.changes.len(), 1, "Second commit should only output new group" ); let (row, weight) = &result2.changes[0]; assert_eq!(*weight, 1); assert_eq!(row.values[0], Value::Text("C".into())); assert_eq!(row.values[1], Value::from_i64(300)); // Third batch: delete last instance of (A,100) let mut delta3 = Delta::new(); delta3.delete(3, vec![Value::Text("A".into()), Value::from_i64(100)]); let result3 = pager .io .block(|| operator.commit((&delta3).into(), &mut cursors)) .unwrap(); // Should output (A,100) with weight -1 (group disappeared) assert_eq!( result3.changes.len(), 1, "Third commit should output disappeared group" ); let (row, weight) = &result3.changes[0]; assert_eq!(*weight, -1, "Disappeared group should have weight -1"); assert_eq!(row.values[0], Value::Text("A".into())); assert_eq!(row.values[1], Value::from_i64(100)) } #[test] fn test_distinct_state_transitions() { let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); // Test that DISTINCT correctly tracks state transitions (0 ↔ positive) let mut operator = AggregateOperator::new( 0, vec![0], vec![], // Empty aggregates for plain DISTINCT vec!["value".to_string()], ) .unwrap(); // Insert value with weight 3 let mut delta1 = Delta::new(); for i in 1..=3 { delta1.insert(i, vec![Value::from_i64(100)]); } let result1 = pager .io .block(|| operator.commit((&delta1).into(), &mut cursors)) .unwrap(); assert_eq!(result1.changes.len(), 1); assert_eq!(result1.changes[0].1, 1, "First appearance should output +1"); // Remove 2 instances (weight goes from 3 to 1, still positive) let mut delta2 = Delta::new(); for i in 1..=2 { delta2.delete(i, vec![Value::from_i64(100)]); } let result2 = pager .io .block(|| operator.commit((&delta2).into(), &mut cursors)) .unwrap(); assert_eq!(result2.changes.len(), 0, "No transition, no output"); // Remove last instance (weight goes from 1 to 0) let mut delta3 = Delta::new(); delta3.delete(3, vec![Value::from_i64(100)]); let result3 = pager .io .block(|| operator.commit((&delta3).into(), &mut cursors)) .unwrap(); assert_eq!(result3.changes.len(), 1); assert_eq!(result3.changes[0].1, -1, "Disappearance should output -1"); // Re-add the value (weight goes from 0 to 1) let mut delta4 = Delta::new(); delta4.insert(4, vec![Value::from_i64(100)]); let result4 = pager .io .block(|| operator.commit((&delta4).into(), &mut cursors)) .unwrap(); assert_eq!(result4.changes.len(), 1); assert_eq!(result4.changes[0].1, 1, "Reappearance should output +1") } #[test] fn test_distinct_persistence() { let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); // First operator instance let mut operator1 = AggregateOperator::new( 0, vec![0], vec![], // Empty aggregates for plain DISTINCT vec!["value".to_string()], ) .unwrap(); // Insert some values let mut delta1 = Delta::new(); delta1.insert(1, vec![Value::from_i64(100)]); delta1.insert(2, vec![Value::from_i64(100)]); // Duplicate delta1.insert(3, vec![Value::from_i64(200)]); let result1 = pager .io .block(|| operator1.commit((&delta1).into(), &mut cursors)) .unwrap(); // Should have 2 distinct values assert_eq!(result1.changes.len(), 2, "Should output 2 distinct values"); // Create new operator instance with same ID (simulates restart) // Create new cursors for the second operator let table_cursor2 = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_cursor2 = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors2 = DbspStateCursors::new(table_cursor2, index_cursor2); let mut operator2 = AggregateOperator::new( 0, // Same operator_id vec![0], vec![], // Empty aggregates for plain DISTINCT vec!["value".to_string()], ) .unwrap(); // Add new value and delete existing (100 has weight 2, so it stays) let mut delta2 = Delta::new(); delta2.insert(4, vec![Value::from_i64(300)]); delta2.delete(1, vec![Value::from_i64(100)]); // Remove one of the 100s let result2 = pager .io .block(|| operator2.commit((&delta2).into(), &mut cursors2)) .unwrap(); // Should only output the new value (300) // 100 still exists (went from weight 2 to 1) assert_eq!(result2.changes.len(), 1, "Should only output new value"); assert_eq!(result2.changes[0].1, 1, "Should be insertion"); assert_eq!(result2.changes[0].0.values[0], Value::from_i64(300)); // Now delete the last instance of 100 let mut delta3 = Delta::new(); delta3.delete(2, vec![Value::from_i64(100)]); let result3 = pager .io .block(|| operator2.commit((&delta3).into(), &mut cursors2)) .unwrap(); // Should output deletion of 100 assert_eq!(result3.changes.len(), 1, "Should output deletion"); assert_eq!(result3.changes[0].1, -1, "Should be deletion"); assert_eq!(result3.changes[0].0.values[0], Value::from_i64(100)); } #[test] fn test_distinct_batch_with_multiple_groups() { let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut operator = AggregateOperator::new( 0, vec![0, 1], // group by category and value vec![], // Empty aggregates for plain DISTINCT vec!["category".to_string(), "value".to_string()], ) .unwrap(); // Create a complex batch with multiple groups and duplicates within each group let mut delta = Delta::new(); // Group (A, 100): 3 instances delta.insert(1, vec![Value::Text("A".into()), Value::from_i64(100)]); delta.insert(2, vec![Value::Text("A".into()), Value::from_i64(100)]); delta.insert(3, vec![Value::Text("A".into()), Value::from_i64(100)]); // Group (B, 200): 2 instances delta.insert(4, vec![Value::Text("B".into()), Value::from_i64(200)]); delta.insert(5, vec![Value::Text("B".into()), Value::from_i64(200)]); // Group (A, 200): 1 instance delta.insert(6, vec![Value::Text("A".into()), Value::from_i64(200)]); // Group (C, 100): 2 instances delta.insert(7, vec![Value::Text("C".into()), Value::from_i64(100)]); delta.insert(8, vec![Value::Text("C".into()), Value::from_i64(100)]); // More instances of Group (A, 100) delta.insert(9, vec![Value::Text("A".into()), Value::from_i64(100)]); delta.insert(10, vec![Value::Text("A".into()), Value::from_i64(100)]); // Group (B, 100): 1 instance delta.insert(11, vec![Value::Text("B".into()), Value::from_i64(100)]); let result = pager .io .block(|| operator.commit((&delta).into(), &mut cursors)) .unwrap(); // Should have exactly 5 distinct groups: // (A, 100), (A, 200), (B, 100), (B, 200), (C, 100) assert_eq!( result.changes.len(), 5, "Should have exactly 5 distinct groups" ); // All should have weight +1 (new groups appearing) for (_row, weight) in &result.changes { assert_eq!(*weight, 1, "All groups should have weight +1"); } // Verify the distinct groups let groups: HashSet<(String, i64)> = result .changes .iter() .map(|(row, _)| { let category = match &row.values[0] { Value::Text(s) => s.value.clone().into_owned(), _ => panic!("Expected text for category"), }; let value = match &row.values[1] { Value::Numeric(Numeric::Integer(i)) => *i, _ => panic!("Expected integer for value"), }; (category, value) }) .collect(); assert!(groups.contains(&("A".to_string(), 100))); assert!(groups.contains(&("A".to_string(), 200))); assert!(groups.contains(&("B".to_string(), 100))); assert!(groups.contains(&("B".to_string(), 200))); assert!(groups.contains(&("C".to_string(), 100))); } #[test] fn test_multiple_distinct_aggregates_same_column() { // Test that multiple DISTINCT aggregates on the same column don't interfere let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); // Create operator with COUNT(DISTINCT value), SUM(DISTINCT value), AVG(DISTINCT value) // all on the same column let mut operator = AggregateOperator::new( 0, vec![], // No group by - single group vec![ AggregateFunction::CountDistinct(0), // COUNT(DISTINCT value) AggregateFunction::SumDistinct(0), // SUM(DISTINCT value) AggregateFunction::AvgDistinct(0), // AVG(DISTINCT value) ], vec!["value".to_string()], ) .unwrap(); // Insert distinct values: 10, 20, 30 (each appearing multiple times) let mut input = Delta::new(); input.insert(1, vec![Value::from_i64(10)]); input.insert(2, vec![Value::from_i64(10)]); // duplicate input.insert(3, vec![Value::from_i64(20)]); input.insert(4, vec![Value::from_i64(20)]); // duplicate input.insert(5, vec![Value::from_i64(30)]); input.insert(6, vec![Value::from_i64(10)]); // duplicate let output = pager .io .block(|| operator.commit((&input).into(), &mut cursors)) .unwrap(); // Should have exactly one output row (no group by) assert_eq!(output.changes.len(), 1); let (row, weight) = &output.changes[0]; assert_eq!(*weight, 1); // Extract the aggregate values let values = &row.values; assert_eq!(values.len(), 3); // 3 aggregate values // COUNT(DISTINCT value) should be 3 (distinct values: 10, 20, 30) assert_eq!(values[0], Value::from_i64(3)); // SUM(DISTINCT value) should be 60 (10 + 20 + 30) assert_eq!(values[1], Value::from_i64(60)); // AVG(DISTINCT value) should be 20.0 (60 / 3) assert_eq!(values[2], Value::from_f64(20.0)); } #[test] fn test_count_distinct_with_deletions() { let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut operator = AggregateOperator::new( 1, vec![], // No GROUP BY vec![AggregateFunction::CountDistinct(1)], vec!["id".to_string(), "value".to_string()], ) .unwrap(); // Insert 3 distinct values let mut delta1 = Delta::new(); delta1.insert(1, vec![Value::from_i64(1), Value::from_i64(100)]); delta1.insert(2, vec![Value::from_i64(2), Value::from_i64(200)]); delta1.insert(3, vec![Value::from_i64(3), Value::from_i64(300)]); let result1 = pager .io .block(|| operator.commit((&delta1).into(), &mut cursors)) .unwrap(); assert_eq!(result1.changes.len(), 1); assert_eq!(result1.changes[0].1, 1); assert_eq!(result1.changes[0].0.values[0], Value::from_i64(3)); // Delete one value let mut delta2 = Delta::new(); delta2.delete(2, vec![Value::from_i64(2), Value::from_i64(200)]); let result2 = pager .io .block(|| operator.commit((&delta2).into(), &mut cursors)) .unwrap(); assert_eq!(result2.changes.len(), 2); let new_row = result2.changes.iter().find(|(_, w)| *w == 1).unwrap(); assert_eq!(new_row.0.values[0], Value::from_i64(2)); } #[test] fn test_sum_distinct_with_deletions() { let (pager, table_root_page_id, index_root_page_id) = create_test_pager(); let table_cursor = BTreeCursor::new_table(pager.clone(), table_root_page_id, 5); let index_def = create_dbsp_state_index(index_root_page_id); let index_cursor = BTreeCursor::new_index(pager.clone(), index_root_page_id, &index_def, 4); let mut cursors = DbspStateCursors::new(table_cursor, index_cursor); let mut operator = AggregateOperator::new( 1, vec![], vec![AggregateFunction::SumDistinct(1)], vec!["id".to_string(), "value".to_string()], ) .unwrap(); // Insert values including a duplicate let mut delta1 = Delta::new(); delta1.insert(1, vec![Value::from_i64(1), Value::from_i64(100)]); delta1.insert(2, vec![Value::from_i64(2), Value::from_i64(200)]); delta1.insert(3, vec![Value::from_i64(3), Value::from_i64(100)]); // Duplicate delta1.insert(4, vec![Value::from_i64(4), Value::from_i64(300)]); let result1 = pager .io .block(|| operator.commit((&delta1).into(), &mut cursors)) .unwrap(); assert_eq!(result1.changes.len(), 1); assert_eq!(result1.changes[0].1, 1); assert_eq!(result1.changes[0].0.values[0], Value::from_f64(600.0)); // 100 + 200 + 300 // Delete value 200 let mut delta2 = Delta::new(); delta2.delete(2, vec![Value::from_i64(2), Value::from_i64(200)]); let result2 = pager .io .block(|| operator.commit((&delta2).into(), &mut cursors)) .unwrap(); assert_eq!(result2.changes.len(), 2); let new_row = result2.changes.iter().find(|(_, w)| *w == 1).unwrap(); assert_eq!(new_row.0.values[0], Value::from_f64(400.0)); // 100 + 300 } } ================================================ FILE: core/incremental/persistence.rs ================================================ use crate::incremental::operator::{AggregateState, DbspStateCursors}; use crate::numeric::Numeric; use crate::storage::btree::{BTreeCursor, BTreeKey, CursorTrait}; use crate::types::{IOResult, ImmutableRecord, SeekKey, SeekOp, SeekResult}; use crate::{return_if_io, LimboError, Result, Value}; #[derive(Debug, Default)] pub enum ReadRecord { #[default] GetRecord, Done { state: Box>, }, } impl ReadRecord { pub fn new() -> Self { Self::default() } pub fn read_record( &mut self, key: SeekKey, cursor: &mut BTreeCursor, ) -> Result>> { loop { match self { ReadRecord::GetRecord => { let res = return_if_io!(cursor.seek(key.clone(), SeekOp::GE { eq_only: true })); if !matches!(res, SeekResult::Found) { *self = ReadRecord::Done { state: Box::new(None), }; } else { let record = return_if_io!(cursor.record()); let r = record.ok_or_else(|| { LimboError::InternalError(format!( "Found key {key:?} in aggregate storage but could not read record" )) })?; // The blob is in column 3: operator_id, zset_id, element_id, value, weight let blob = r.get_value(3)?.to_owned(); let (state, _group_key) = match blob { Value::Blob(blob) => AggregateState::from_blob(&blob), Value::Null => { // For plain DISTINCT, we store null value and just track weight // Return a minimal state indicating existence Ok((AggregateState::default(), vec![])) } _ => Err(LimboError::ParseError( "Value in aggregator not blob or null".to_string(), )), }?; *self = ReadRecord::Done { state: Box::new(Some(state)), } } } ReadRecord::Done { state } => return Ok(IOResult::Done((**state).clone())), } } } } #[derive(Debug, Default)] pub enum WriteRow { #[default] GetRecord, Delete { rowid: i64, }, DeleteIndex, ComputeNewRowId { final_weight: isize, }, InsertNew { rowid: i64, final_weight: isize, }, InsertIndex { rowid: i64, }, UpdateExisting { rowid: i64, final_weight: isize, }, Done, } impl WriteRow { pub fn new() -> Self { Self::default() } /// Write a row with weight management using index for lookups. /// /// # Arguments /// * `cursors` - DBSP state cursors (table and index) /// * `index_key` - The key to seek in the index /// * `record_values` - The record values (without weight) to insert /// * `weight` - The weight delta to apply pub fn write_row( &mut self, cursors: &mut DbspStateCursors, index_key: Vec, record_values: Vec, weight: isize, ) -> Result> { loop { match self { WriteRow::GetRecord => { // First, seek in the index to find if the row exists let index_values = index_key.clone(); let index_record = ImmutableRecord::from_values(&index_values, index_values.len()); let res = return_if_io!(cursors.index_cursor.seek( SeekKey::IndexKey(&index_record), SeekOp::GE { eq_only: true } )); if !matches!(res, SeekResult::Found) { // Row doesn't exist, we'll insert a new one *self = WriteRow::ComputeNewRowId { final_weight: weight, }; } else { // Found in index, get the rowid it points to let rowid = return_if_io!(cursors.index_cursor.rowid()); let rowid = rowid.ok_or_else(|| { LimboError::InternalError( "Index cursor does not have a valid rowid".to_string(), ) })?; // Now seek in the table using the rowid let table_res = return_if_io!(cursors .table_cursor .seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true })); if !matches!(table_res, SeekResult::Found) { return Err(LimboError::InternalError( "Index points to non-existent table row".to_string(), )); } let existing_record = return_if_io!(cursors.table_cursor.record()); let r = existing_record.ok_or_else(|| { LimboError::InternalError( "Found rowid in table but could not read record".to_string(), ) })?; let weight_opt = r.get_value_opt(4); // Weight is always the last value (column 4 in our 5-column structure) let existing_weight = match weight_opt { Some(val) => match val.to_owned() { Value::Numeric(Numeric::Integer(w)) => w as isize, _ => { return Err(LimboError::InternalError( "Invalid weight value in storage".to_string(), )) } }, None => { return Err(LimboError::InternalError( "No weight value found in storage".to_string(), )) } }; let final_weight = existing_weight + weight; if final_weight <= 0 { // Store index_key for later deletion of index entry *self = WriteRow::Delete { rowid } } else { // Store the rowid for update *self = WriteRow::UpdateExisting { rowid, final_weight, } } } } WriteRow::Delete { rowid } => { // Seek to the row and delete it return_if_io!(cursors .table_cursor .seek(SeekKey::TableRowId(*rowid), SeekOp::GE { eq_only: true })); // Transition to DeleteIndex to also delete the index entry *self = WriteRow::DeleteIndex; return_if_io!(cursors.table_cursor.delete()); } WriteRow::DeleteIndex => { // Mark as Done before delete to avoid retry on I/O *self = WriteRow::Done; return_if_io!(cursors.index_cursor.delete()); } WriteRow::ComputeNewRowId { final_weight } => { // Find the last rowid to compute the next one return_if_io!(cursors.table_cursor.last()); let rowid = if cursors.table_cursor.is_empty() { 1 } else { match return_if_io!(cursors.table_cursor.rowid()) { Some(id) => id + 1, None => { return Err(LimboError::InternalError( "Table cursor has rows but no valid rowid".to_string(), )) } } }; // Transition to InsertNew with the computed rowid *self = WriteRow::InsertNew { rowid, final_weight: *final_weight, }; } WriteRow::InsertNew { rowid, final_weight, } => { let rowid_val = *rowid; let final_weight_val = *final_weight; // Seek to where we want to insert // The insert will position the cursor correctly return_if_io!(cursors.table_cursor.seek( SeekKey::TableRowId(rowid_val), SeekOp::GE { eq_only: false } )); // Build the complete record with weight // Use the function parameter record_values directly let mut complete_record = record_values.clone(); complete_record.push(Value::from_i64(final_weight_val as i64)); // Create an ImmutableRecord from the values let immutable_record = ImmutableRecord::from_values(&complete_record, complete_record.len()); let btree_key = BTreeKey::new_table_rowid(rowid_val, Some(&immutable_record)); // Transition to InsertIndex state after table insertion *self = WriteRow::InsertIndex { rowid: rowid_val }; return_if_io!(cursors.table_cursor.insert(&btree_key)); } WriteRow::InsertIndex { rowid } => { // For has_rowid indexes, we need to append the rowid to the index key // Use the function parameter index_key directly let mut index_values = index_key.clone(); index_values.push(Value::from_i64(*rowid)); // Create the index record with the rowid appended let index_record = ImmutableRecord::from_values(&index_values, index_values.len()); let index_btree_key = BTreeKey::new_index_key(&index_record); // Mark as Done before index insert to avoid retry on I/O *self = WriteRow::Done; return_if_io!(cursors.index_cursor.insert(&index_btree_key)); } WriteRow::UpdateExisting { rowid, final_weight, } => { // Build the complete record with weight let mut complete_record = record_values.clone(); complete_record.push(Value::from_i64(*final_weight as i64)); // Create an ImmutableRecord from the values let immutable_record = ImmutableRecord::from_values(&complete_record, complete_record.len()); let btree_key = BTreeKey::new_table_rowid(*rowid, Some(&immutable_record)); // Mark as Done before insert to avoid retry on I/O *self = WriteRow::Done; // BTree insert with existing key will replace the old value return_if_io!(cursors.table_cursor.insert(&btree_key)); } WriteRow::Done => { return Ok(IOResult::Done(())); } } } } } ================================================ FILE: core/incremental/project_operator.rs ================================================ // Project operator for DBSP-style incremental computation // This operator projects/transforms columns in a relational stream use crate::incremental::dbsp::{Delta, DeltaPair, HashableRow}; use crate::incremental::expr_compiler::CompiledExpression; use crate::incremental::operator::{ ComputationTracker, DbspStateCursors, EvalState, IncrementalOperator, }; use crate::sync::Mutex; use crate::sync::{atomic::Ordering, Arc}; use crate::types::IOResult; use crate::{Connection, Database, Result, Value}; #[derive(Debug, Clone)] pub struct ProjectColumn { /// Compiled expression (handles both trivial columns and complex expressions) pub compiled: CompiledExpression, } /// Project operator - selects/transforms columns #[derive(Clone)] pub struct ProjectOperator { columns: Vec, input_column_names: Vec, output_column_names: Vec, tracker: Option>>, // Internal in-memory connection for expression evaluation // Programs are very dependent on having a connection, so give it one. // // We could in theory pass the current connection, but there are a host of problems with that. // For example: during a write transaction, where views are usually updated, we have autocommit // on. When the program we are executing calls Halt, it will try to commit the current // transaction, which is absolutely incorrect. // // There are other ways to solve this, but a read-only connection to an empty in-memory // database gives us the closest environment we need to execute expressions. internal_conn: Arc, } impl std::fmt::Debug for ProjectOperator { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ProjectOperator") .field("columns", &self.columns) .field("input_column_names", &self.input_column_names) .field("output_column_names", &self.output_column_names) .finish() } } impl ProjectOperator { /// Create a ProjectOperator from pre-compiled expressions pub fn from_compiled( compiled_exprs: Vec, aliases: Vec>, input_column_names: Vec, output_column_names: Vec, ) -> crate::Result { // Set up internal connection for expression evaluation let io = Arc::new(crate::MemoryIO::new()); let db = Database::open_file(io, ":memory:")?; let internal_conn = db.connect()?; // Set to read-only mode and disable auto-commit since we're only evaluating expressions internal_conn.set_query_only(true); internal_conn.auto_commit.store(false, Ordering::SeqCst); // Create ProjectColumn structs from compiled expressions let columns: Vec = compiled_exprs .into_iter() .zip(aliases) .map(|(compiled, _alias)| ProjectColumn { compiled }) .collect(); Ok(Self { columns, input_column_names, output_column_names, tracker: None, internal_conn, }) } fn project_values(&self, values: &[Value]) -> Vec { let mut output = Vec::new(); for col in &self.columns { // Use the internal connection's pager for expression evaluation let internal_pager = self.internal_conn.pager.load().clone(); // Execute the compiled expression (handles both columns and complex expressions) let result = col .compiled .execute(values, internal_pager) .expect("Failed to execute compiled expression for the Project operator"); output.push(result); } output } } impl IncrementalOperator for ProjectOperator { fn eval( &mut self, state: &mut EvalState, _cursors: &mut DbspStateCursors, ) -> Result> { let delta = match state { EvalState::Init { deltas } => { // Project operators only use left_delta, right_delta must be empty assert!( deltas.right.is_empty(), "ProjectOperator expects right_delta to be empty" ); std::mem::take(&mut deltas.left) } _ => unreachable!( "ProjectOperator doesn't execute the state machine. Should be in Init state" ), }; let mut output_delta = Delta::new(); for (row, weight) in delta.changes { if let Some(tracker) = &self.tracker { tracker.lock().record_project(); } let projected = self.project_values(&row.values); let projected_row = HashableRow::new(row.rowid, projected); output_delta.changes.push((projected_row, weight)); } *state = EvalState::Done; Ok(IOResult::Done(output_delta)) } fn commit( &mut self, deltas: DeltaPair, _cursors: &mut DbspStateCursors, ) -> Result> { // Project operator only uses left delta, right must be empty assert!( deltas.right.is_empty(), "ProjectOperator expects right delta to be empty in commit" ); let mut output_delta = Delta::new(); // Commit the delta to our internal state and build output for (row, weight) in &deltas.left.changes { if let Some(tracker) = &self.tracker { tracker.lock().record_project(); } let projected = self.project_values(&row.values); let projected_row = HashableRow::new(row.rowid, projected); output_delta.changes.push((projected_row, *weight)); } Ok(crate::types::IOResult::Done(output_delta)) } fn set_tracker(&mut self, tracker: Arc>) { self.tracker = Some(tracker); } } ================================================ FILE: core/incremental/view.rs ================================================ use super::compiler::{DbspCircuit, DbspCompiler, DeltaSet}; use super::dbsp::Delta; use super::operator::ComputationTracker; use crate::numeric::Numeric; use crate::schema::{BTreeTable, Schema}; use crate::storage::btree::CursorTrait; use crate::sync::Arc; use crate::sync::Mutex; use crate::translate::logical::LogicalPlanBuilder; use crate::types::{IOResult, Value}; use crate::util::{extract_view_columns, ViewColumnSchema}; use crate::{return_if_io, LimboError, Pager, Result, Statement}; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::cell::RefCell; use std::fmt; use std::rc::Rc; use turso_parser::ast; use turso_parser::{ ast::{Cmd, Stmt}, parser::Parser, }; /// State machine for populating a view from its source table pub enum PopulateState { /// Initial state - need to prepare the query Start, /// All tables that need to be populated ProcessingAllTables { queries: Vec, current_idx: usize, }, /// Actively processing rows from the query ProcessingOneTable { queries: Vec, current_idx: usize, stmt: Box, rows_processed: usize, /// If we're in the middle of processing a row (merge_delta returned I/O) pending_row: Option<(i64, Vec)>, // (rowid, values) }, /// Population complete Done, } // SAFETY: This needs to be audited for thread safety. // See: https://github.com/tursodatabase/turso/issues/1552 unsafe impl Send for PopulateState {} unsafe impl Sync for PopulateState {} crate::assert::assert_send_sync!(PopulateState); /// State machine for merge_delta to handle I/O operations impl fmt::Debug for PopulateState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PopulateState::Start => write!(f, "Start"), PopulateState::ProcessingAllTables { current_idx, queries, } => f .debug_struct("ProcessingAllTables") .field("current_idx", current_idx) .field("num_queries", &queries.len()) .finish(), PopulateState::ProcessingOneTable { current_idx, rows_processed, pending_row, queries, .. } => f .debug_struct("ProcessingOneTable") .field("current_idx", current_idx) .field("rows_processed", rows_processed) .field("has_pending", &pending_row.is_some()) .field("total_queries", &queries.len()) .finish(), PopulateState::Done => write!(f, "Done"), } } } /// Per-connection transaction state for incremental views #[derive(Debug, Clone, Default)] pub struct ViewTransactionState { // Per-table deltas for uncommitted changes // Maps table_name -> Delta for that table // Using RefCell for interior mutability table_deltas: RefCell>, } impl ViewTransactionState { /// Create a new transaction state pub fn new() -> Self { Self { table_deltas: RefCell::new(HashMap::default()), } } /// Insert a row into the delta for a specific table pub fn insert(&self, table_name: &str, key: i64, values: Vec) { let mut deltas = self.table_deltas.borrow_mut(); let delta = deltas.entry(table_name.to_string()).or_default(); delta.insert(key, values); } /// Delete a row from the delta for a specific table pub fn delete(&self, table_name: &str, key: i64, values: Vec) { let mut deltas = self.table_deltas.borrow_mut(); let delta = deltas.entry(table_name.to_string()).or_default(); delta.delete(key, values); } /// Clear all changes in the delta pub fn clear(&self) { self.table_deltas.borrow_mut().clear(); } /// Get deltas organized by table pub fn get_table_deltas(&self) -> HashMap { self.table_deltas.borrow().clone() } /// Check if the delta is empty pub fn is_empty(&self) -> bool { self.table_deltas.borrow().values().all(|d| d.is_empty()) } /// Returns how many elements exist in the delta. pub fn len(&self) -> usize { self.table_deltas.borrow().values().map(|d| d.len()).sum() } } /// Container for all view transaction states within a connection /// Provides interior mutability for the map of view states #[derive(Debug, Clone, Default)] pub struct AllViewsTxState { states: Rc>>>, } // SAFETY: This needs to be audited for thread safety. // See: https://github.com/tursodatabase/turso/issues/1552 unsafe impl Send for AllViewsTxState {} unsafe impl Sync for AllViewsTxState {} crate::assert::assert_send_sync!(AllViewsTxState); impl AllViewsTxState { /// Create a new container for view transaction states pub fn new() -> Self { Self { states: Rc::new(RefCell::new(HashMap::default())), } } /// Get or create a transaction state for a view #[allow(clippy::arc_with_non_send_sync)] pub fn get_or_create(&self, view_name: &str) -> Arc { let mut states = self.states.borrow_mut(); // ViewTransactionState uses RefCell (not Sync), but AllViewsTxState is // single-threaded (Rc-based). Arc is used for shared ownership, not // cross-thread sharing. states .entry(view_name.to_string()) .or_insert_with(|| Arc::new(ViewTransactionState::new())) .clone() } /// Get a transaction state for a view if it exists pub fn get(&self, view_name: &str) -> Option> { self.states.borrow().get(view_name).cloned() } /// Clear all transaction states pub fn clear(&self) { self.states.borrow_mut().clear(); } /// Check if there are no transaction states pub fn is_empty(&self) -> bool { self.states.borrow().is_empty() } /// Get all view names that have transaction states pub fn get_view_names(&self) -> Vec { self.states.borrow().keys().cloned().collect() } } /// Incremental view that maintains its state through a DBSP circuit /// /// This version keeps everything in-memory. This is acceptable for small views, since DBSP /// doesn't have to track the history of changes. Still for very large views (think of the result /// of create view v as select * from tbl where x > 1; and that having 1B values. /// /// We should have a version of this that materializes the results. Materializing will also be good /// for large aggregations, because then we don't have to re-compute when opening the database /// again. /// /// Uses DBSP circuits for incremental computation. #[derive(Debug)] pub struct IncrementalView { name: String, // The SELECT statement that defines how to transform input data pub select_stmt: ast::Select, // DBSP circuit that encapsulates the computation circuit: DbspCircuit, // All tables referenced by this view (from FROM clause and JOINs) referenced_tables: Vec>, // Mapping from table aliases to actual table names (e.g., "c" -> "customers") table_aliases: HashMap, // Mapping from table name to fully qualified name (e.g., "customers" -> "main.customers") // This preserves database qualification from the original query qualified_table_names: HashMap, // WHERE conditions for each table (accumulated from all occurrences) // Multiple conditions from UNION branches or duplicate references are stored as a vector table_conditions: HashMap>>, // The view's column schema with table relationships pub column_schema: ViewColumnSchema, // State machine for population populate_state: PopulateState, // Computation tracker for statistics // We will use this one day to export rows_read, but for now, will just test that we're doing the expected amount of compute #[cfg_attr(not(test), allow(dead_code))] pub tracker: Arc>, // Root page of the btree storing the materialized state (0 for unmaterialized) root_page: i64, } // SAFETY: This needs to be audited for thread safety. // See: https://github.com/tursodatabase/turso/issues/1552 unsafe impl Send for IncrementalView {} unsafe impl Sync for IncrementalView {} crate::assert::assert_send_sync!(IncrementalView); impl IncrementalView { /// Try to compile the SELECT statement into a DBSP circuit fn try_compile_circuit( select: &ast::Select, schema: &Schema, main_data_root: i64, internal_state_root: i64, internal_state_index_root: i64, ) -> Result { // Build the logical plan from the SELECT statement let mut builder = LogicalPlanBuilder::new(schema); // Convert Select to a Stmt for the builder let stmt = ast::Stmt::Select(select.clone()); let logical_plan = builder.build_statement(&stmt)?; // Compile the logical plan to a DBSP circuit with the storage roots let compiler = DbspCompiler::new( main_data_root, internal_state_root, internal_state_index_root, ); let circuit = compiler.compile(&logical_plan)?; Ok(circuit) } /// Get an iterator over column names, using enumerated naming for unnamed columns pub fn column_names(&self) -> impl Iterator + '_ { self.column_schema .columns .iter() .enumerate() .map(|(i, vc)| { vc.column .name .clone() .unwrap_or_else(|| format!("column{}", i + 1)) }) } /// Check if this view has the same SQL definition as the provided SQL string pub fn has_same_sql(&self, sql: &str) -> bool { // Parse the SQL to extract just the SELECT statement if let Ok(Some(Cmd::Stmt(Stmt::CreateMaterializedView { select, .. }))) = Parser::new(sql.as_bytes()).next_cmd() { // Compare the SELECT statements as SQL strings return self.select_stmt == select; } false } /// Validate a SELECT statement and extract the columns it would produce /// This is used during CREATE MATERIALIZED VIEW to validate the view before storing it pub fn validate_and_extract_columns( select: &ast::Select, schema: &Schema, ) -> Result { crate::util::validate_select_for_unsupported_features(select)?; // Use the shared function to extract columns with full table context extract_view_columns(select, schema) } pub fn from_sql( sql: &str, schema: &Schema, main_data_root: i64, internal_state_root: i64, internal_state_index_root: i64, ) -> Result { let mut parser = Parser::new(sql.as_bytes()); let cmd = parser.next_cmd()?; let cmd = cmd.expect("View is an empty statement"); match cmd { Cmd::Stmt(Stmt::CreateMaterializedView { if_not_exists: _, view_name, columns: _, select, }) => IncrementalView::from_stmt( view_name, select, schema, main_data_root, internal_state_root, internal_state_index_root, ), _ => Err(LimboError::ParseError(format!( "View is not a CREATE MATERIALIZED VIEW statement: {sql}" ))), } } pub fn from_stmt( view_name: ast::QualifiedName, select: ast::Select, schema: &Schema, main_data_root: i64, internal_state_root: i64, internal_state_index_root: i64, ) -> Result { let name = view_name.name.as_str().to_string(); // Extract output columns using the shared function let column_schema = extract_view_columns(&select, schema)?; let mut referenced_tables = Vec::new(); let mut table_aliases = HashMap::default(); let mut qualified_table_names = HashMap::default(); let mut table_conditions = HashMap::default(); Self::extract_all_tables( &select, schema, &mut referenced_tables, &mut table_aliases, &mut qualified_table_names, &mut table_conditions, )?; Self::new( name, select.clone(), referenced_tables, table_aliases, qualified_table_names, table_conditions, column_schema, schema, main_data_root, internal_state_root, internal_state_index_root, ) } #[allow(clippy::too_many_arguments)] pub fn new( name: String, select_stmt: ast::Select, referenced_tables: Vec>, table_aliases: HashMap, qualified_table_names: HashMap, table_conditions: HashMap>>, column_schema: ViewColumnSchema, schema: &Schema, main_data_root: i64, internal_state_root: i64, internal_state_index_root: i64, ) -> Result { // Create the tracker that will be shared by all operators let tracker = Arc::new(Mutex::new(ComputationTracker::new())); // Compile the SELECT statement into a DBSP circuit let circuit = Self::try_compile_circuit( &select_stmt, schema, main_data_root, internal_state_root, internal_state_index_root, )?; Ok(Self { name, select_stmt, circuit, referenced_tables, table_aliases, qualified_table_names, table_conditions, column_schema, populate_state: PopulateState::Start, tracker, root_page: main_data_root, }) } pub fn name(&self) -> &str { &self.name } /// Execute the circuit with uncommitted changes to get processed delta pub fn execute_with_uncommitted( &mut self, uncommitted: DeltaSet, pager: Arc, execute_state: &mut crate::incremental::compiler::ExecuteState, ) -> crate::Result> { // Initialize execute_state with the input data *execute_state = crate::incremental::compiler::ExecuteState::Init { input_data: uncommitted, }; self.circuit.execute(pager, execute_state) } /// Get the root page for this materialized view's btree pub fn get_root_page(&self) -> i64 { self.root_page } /// Get all table names referenced by this view pub fn get_referenced_table_names(&self) -> Vec { self.referenced_tables .iter() .map(|t| t.name.clone()) .collect() } /// Get all tables referenced by this view pub fn get_referenced_tables(&self) -> Vec> { self.referenced_tables.clone() } /// Process a single table reference from a FROM or JOIN clause fn process_table_reference( name: &ast::QualifiedName, alias: &Option, schema: &Schema, table_map: &mut HashMap>, aliases: &mut HashMap, qualified_names: &mut HashMap, cte_names: &HashSet, ) -> Result<()> { let table_name = name.name.as_str(); // Build the fully qualified name let qualified_name = if let Some(ref db) = name.db_name { format!("{db}.{table_name}") } else { table_name.to_string() }; // Skip CTEs - they're not real tables if !cte_names.contains(table_name) { if let Some(table) = schema.get_btree_table(table_name) { table_map.insert(table_name.to_string(), table); qualified_names.insert(table_name.to_string(), qualified_name); // Store the alias mapping if there is an alias if let Some(alias_enum) = alias { let alias_name = match alias_enum { ast::As::As(name) | ast::As::Elided(name) => name.as_str(), }; aliases.insert(alias_name.to_string(), table_name.to_string()); } } else { return Err(LimboError::ParseError(format!( "Table '{table_name}' not found in schema" ))); } } Ok(()) } fn extract_one_statement( select: &ast::OneSelect, schema: &Schema, table_map: &mut HashMap>, aliases: &mut HashMap, qualified_names: &mut HashMap, table_conditions: &mut HashMap>>, cte_names: &HashSet, ) -> Result<()> { if let ast::OneSelect::Select { from: Some(ref from), .. } = select { // Get the main table from FROM clause if let ast::SelectTable::Table(name, alias, _) = from.select.as_ref() { Self::process_table_reference( name, alias, schema, table_map, aliases, qualified_names, cte_names, )?; } // Get all tables from JOIN clauses for join in &from.joins { if let ast::SelectTable::Table(name, alias, _) = join.table.as_ref() { Self::process_table_reference( name, alias, schema, table_map, aliases, qualified_names, cte_names, )?; } } } // Extract WHERE conditions for this SELECT let where_expr = if let ast::OneSelect::Select { where_clause: Some(ref where_expr), .. } = select { Some(where_expr.as_ref().clone()) } else { None }; // Ensure all tables have an entry in table_conditions (even if empty) for table_name in table_map.keys() { table_conditions.entry(table_name.clone()).or_default(); } // Extract and store table-specific conditions from the WHERE clause if let Some(ref where_expr) = where_expr { for table_name in table_map.keys() { let all_tables: Vec = table_map.keys().cloned().collect(); let table_specific_condition = Self::extract_conditions_for_table( where_expr, table_name, aliases, &all_tables, schema, ); // Only add if there's actually a condition for this table if let Some(condition) = table_specific_condition { let conditions = table_conditions.get_mut(table_name).ok_or_else(|| { LimboError::InternalError( "table_conditions should have entry for table_name".to_string(), ) })?; conditions.push(Some(condition)); } } } else { // No WHERE clause - push None for all tables in this SELECT. It is a way // of signaling that we need all rows in the table. It is important we signal this // explicitly, because the same table may appear in many conditions - some of which // have filters that would otherwise be applied. for table_name in table_map.keys() { let conditions = table_conditions.get_mut(table_name).ok_or_else(|| { LimboError::InternalError( "table_conditions should have entry for table_name".to_string(), ) })?; conditions.push(None); } } Ok(()) } /// Extract all tables and their aliases from the SELECT statement, handling CTEs /// Deduplicates tables and accumulates WHERE conditions fn extract_all_tables( select: &ast::Select, schema: &Schema, tables: &mut Vec>, aliases: &mut HashMap, qualified_names: &mut HashMap, table_conditions: &mut HashMap>>, ) -> Result<()> { let mut table_map = HashMap::default(); Self::extract_all_tables_inner( select, schema, &mut table_map, aliases, qualified_names, table_conditions, &HashSet::default(), )?; // Convert deduplicated table map to vector for (_name, table) in table_map { tables.push(table); } Ok(()) } fn extract_all_tables_inner( select: &ast::Select, schema: &Schema, table_map: &mut HashMap>, aliases: &mut HashMap, qualified_names: &mut HashMap, table_conditions: &mut HashMap>>, parent_cte_names: &HashSet, ) -> Result<()> { let mut cte_names = parent_cte_names.clone(); // First, collect CTE names and process any CTEs (WITH clauses) if let Some(ref with) = select.with { // First pass: collect all CTE names (needed for recursive CTEs) for cte in &with.ctes { cte_names.insert(cte.tbl_name.as_str().to_string()); } // Second pass: extract tables from each CTE's SELECT statement for cte in &with.ctes { // Recursively extract tables from each CTE's SELECT statement Self::extract_all_tables_inner( &cte.select, schema, table_map, aliases, qualified_names, table_conditions, &cte_names, )?; } } // Then process the main SELECT body Self::extract_one_statement( &select.body.select, schema, table_map, aliases, qualified_names, table_conditions, &cte_names, )?; // Process any compound selects (UNION, etc.) for c in &select.body.compounds { let ast::CompoundSelect { select, .. } = c; Self::extract_one_statement( select, schema, table_map, aliases, qualified_names, table_conditions, &cte_names, )?; } Ok(()) } /// Generate SQL queries for populating the view from each source table /// Returns a vector of SQL statements, one for each referenced table /// Each query includes the WHERE conditions accumulated from all occurrences fn sql_for_populate(&self) -> crate::Result> { Self::generate_populate_queries( &self.select_stmt, &self.referenced_tables, &self.table_aliases, &self.qualified_table_names, &self.table_conditions, ) } pub fn generate_populate_queries( select_stmt: &ast::Select, referenced_tables: &[Arc], table_aliases: &HashMap, qualified_table_names: &HashMap, table_conditions: &HashMap>>, ) -> crate::Result> { if referenced_tables.is_empty() { return Err(LimboError::ParseError( "No tables to populate from".to_string(), )); } let mut queries = Vec::new(); for table in referenced_tables { // Check if the table has a rowid alias (INTEGER PRIMARY KEY column) let has_rowid_alias = table.columns.iter().any(|col| col.is_rowid_alias()); // Select all columns. The circuit will handle filtering and projection // If there's a rowid alias, we don't need to select rowid separately let select_clause = if has_rowid_alias { "*".to_string() } else { "*, rowid".to_string() }; // Get accumulated WHERE conditions for this table let where_clause = if let Some(conditions) = table_conditions.get(&table.name) { // Combine multiple conditions with OR if there are multiple occurrences Self::combine_conditions( select_stmt, conditions, &table.name, referenced_tables, table_aliases, )? } else { String::new() }; // Use the qualified table name if available, otherwise just the table name let table_name = qualified_table_names .get(&table.name) .cloned() .unwrap_or_else(|| table.name.clone()); // Construct the query for this table let query = if where_clause.is_empty() { format!("SELECT {select_clause} FROM {table_name}") } else { format!("SELECT {select_clause} FROM {table_name} WHERE {where_clause}") }; tracing::debug!("populating materialized view with `{query}`"); queries.push(query); } Ok(queries) } fn combine_conditions( _select_stmt: &ast::Select, conditions: &[Option], table_name: &str, _referenced_tables: &[Arc], table_aliases: &HashMap, ) -> crate::Result { // Check if any conditions are None (SELECTs without WHERE) let has_none = conditions.iter().any(|c| c.is_none()); let non_empty: Vec<_> = conditions.iter().filter_map(|c| c.as_ref()).collect(); // If we have both Some and None conditions, that means in some of the expressions where // this table appear we want all rows. So we need to fetch all rows. if has_none && !non_empty.is_empty() { return Ok(String::new()); } if non_empty.is_empty() { return Ok(String::new()); } if non_empty.len() == 1 { // Unqualify the expression before converting to string let unqualified = Self::unqualify_expression(non_empty[0], table_name, table_aliases); return Ok(unqualified.to_string()); } // Multiple conditions - combine with OR // This happens in UNION ALL when the same table appears multiple times let mut combined_parts = Vec::new(); for condition in non_empty { let unqualified = Self::unqualify_expression(condition, table_name, table_aliases); // Wrap each condition in parentheses to preserve precedence combined_parts.push(format!("({unqualified})")); } // Join all conditions with OR Ok(combined_parts.join(" OR ")) } /// Resolve a table alias to the actual table name /// Check if an expression is a simple comparison that can be safely extracted /// This excludes subqueries, CASE expressions, function calls, etc. fn is_simple_comparison(expr: &ast::Expr) -> bool { match expr { // Simple column references and literals are OK ast::Expr::Column { .. } | ast::Expr::Literal(_) => true, // Simple binary operations between simple expressions are OK ast::Expr::Binary(left, op, right) => { match op { // Logical operators ast::Operator::And | ast::Operator::Or => { Self::is_simple_comparison(left) && Self::is_simple_comparison(right) } // Comparison operators ast::Operator::Equals | ast::Operator::NotEquals | ast::Operator::Less | ast::Operator::LessEquals | ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Is | ast::Operator::IsNot => { Self::is_simple_comparison(left) && Self::is_simple_comparison(right) } // String concatenation and other operations are NOT simple ast::Operator::Concat => false, // Arithmetic might be OK if operands are simple ast::Operator::Add | ast::Operator::Subtract | ast::Operator::Multiply | ast::Operator::Divide | ast::Operator::Modulus => { Self::is_simple_comparison(left) && Self::is_simple_comparison(right) } _ => false, } } // Unary operations might be OK ast::Expr::Unary( ast::UnaryOperator::Not | ast::UnaryOperator::Negative | ast::UnaryOperator::Positive, inner, ) => Self::is_simple_comparison(inner), ast::Expr::Unary(_, _) => false, // Complex expressions are NOT simple ast::Expr::Case { .. } => false, ast::Expr::Cast { .. } => false, ast::Expr::Collate { .. } => false, ast::Expr::Exists(_) => false, ast::Expr::FunctionCall { .. } => false, ast::Expr::InList { .. } => false, ast::Expr::InSelect { .. } => false, ast::Expr::Like { .. } => false, ast::Expr::NotNull(_) => true, // IS NOT NULL is simple enough ast::Expr::Parenthesized(exprs) => { // Parenthesized expression can contain multiple expressions // Only consider it simple if it has exactly one simple expression exprs.len() == 1 && Self::is_simple_comparison(&exprs[0]) } ast::Expr::Subquery(_) => false, // BETWEEN might be OK if all operands are simple ast::Expr::Between { .. } => { // BETWEEN has a different structure, for safety just exclude it false } // Qualified references are simple ast::Expr::DoublyQualified(..) => true, ast::Expr::Qualified(_, _) => true, // These are simple ast::Expr::Id(_) => true, ast::Expr::Name(_) => true, // Anything else is not simple _ => false, } } /// Extract conditions from a WHERE clause that apply to a specific table fn extract_conditions_for_table( expr: &ast::Expr, table_name: &str, aliases: &HashMap, all_tables: &[String], schema: &Schema, ) -> Option { match expr { ast::Expr::Binary(left, op, right) => { match op { ast::Operator::And => { // For AND, we can extract conditions independently let left_cond = Self::extract_conditions_for_table( left, table_name, aliases, all_tables, schema, ); let right_cond = Self::extract_conditions_for_table( right, table_name, aliases, all_tables, schema, ); match (left_cond, right_cond) { (Some(l), Some(r)) => Some(ast::Expr::Binary( Box::new(l), ast::Operator::And, Box::new(r), )), (Some(l), None) => Some(l), (None, Some(r)) => Some(r), (None, None) => None, } } ast::Operator::Or => { // For OR, both sides must reference only our table let left_tables = Self::get_tables_in_expr(left, aliases, all_tables, schema); let right_tables = Self::get_tables_in_expr(right, aliases, all_tables, schema); if left_tables.len() == 1 && left_tables.contains(&table_name.to_string()) && right_tables.len() == 1 && right_tables.contains(&table_name.to_string()) && Self::is_simple_comparison(expr) { Some(expr.clone()) } else { None } } _ => { // For comparison operators, check if this condition only references our table let referenced_tables = Self::get_tables_in_expr(expr, aliases, all_tables, schema); if referenced_tables.len() == 1 && referenced_tables.contains(&table_name.to_string()) && Self::is_simple_comparison(expr) { Some(expr.clone()) } else { None } } } } _ => { // For other expressions, check if they only reference our table let referenced_tables = Self::get_tables_in_expr(expr, aliases, all_tables, schema); if referenced_tables.len() == 1 && referenced_tables.contains(&table_name.to_string()) && Self::is_simple_comparison(expr) { Some(expr.clone()) } else { None } } } } /// Unqualify column references in an expression /// Removes table/alias prefixes from qualified column names fn unqualify_expression( expr: &ast::Expr, table_name: &str, aliases: &HashMap, ) -> ast::Expr { match expr { ast::Expr::Binary(left, op, right) => ast::Expr::Binary( Box::new(Self::unqualify_expression(left, table_name, aliases)), *op, Box::new(Self::unqualify_expression(right, table_name, aliases)), ), ast::Expr::Qualified(table_or_alias, column) => { // Check if this qualification refers to our table let table_str = table_or_alias.as_str(); let actual_table = if let Some(actual) = aliases.get(table_str) { actual.clone() } else if table_str.contains('.') { // Handle database.table format table_str .split('.') .next_back() .unwrap_or(table_str) .to_string() } else { table_str.to_string() }; if actual_table == table_name { // Remove the qualification ast::Expr::Id(column.clone()) } else { // Keep the qualification (shouldn't happen if extraction worked correctly) expr.clone() } } ast::Expr::DoublyQualified(_database, table, column) => { // Check if this refers to our table if table.as_str() == table_name { // Remove the qualification, keep just the column ast::Expr::Id(column.clone()) } else { // Keep the qualification (shouldn't happen if extraction worked correctly) expr.clone() } } ast::Expr::Unary(op, inner) => ast::Expr::Unary( *op, Box::new(Self::unqualify_expression(inner, table_name, aliases)), ), ast::Expr::FunctionCall { name, args, distinctness, filter_over, order_by, } => ast::Expr::FunctionCall { name: name.clone(), args: args .iter() .map(|arg| Box::new(Self::unqualify_expression(arg, table_name, aliases))) .collect(), distinctness: *distinctness, filter_over: filter_over.clone(), order_by: order_by.clone(), }, ast::Expr::InList { lhs, not, rhs } => ast::Expr::InList { lhs: Box::new(Self::unqualify_expression(lhs, table_name, aliases)), not: *not, rhs: rhs .iter() .map(|item| Box::new(Self::unqualify_expression(item, table_name, aliases))) .collect(), }, ast::Expr::Between { lhs, not, start, end, } => ast::Expr::Between { lhs: Box::new(Self::unqualify_expression(lhs, table_name, aliases)), not: *not, start: Box::new(Self::unqualify_expression(start, table_name, aliases)), end: Box::new(Self::unqualify_expression(end, table_name, aliases)), }, _ => expr.clone(), } } /// Get all tables referenced in an expression fn get_tables_in_expr( expr: &ast::Expr, aliases: &HashMap, all_tables: &[String], schema: &Schema, ) -> Vec { let mut tables = Vec::new(); Self::collect_tables_in_expr(expr, aliases, all_tables, schema, &mut tables); tables.sort(); tables.dedup(); tables } /// Recursively collect table references from an expression fn collect_tables_in_expr( expr: &ast::Expr, aliases: &HashMap, all_tables: &[String], schema: &Schema, tables: &mut Vec, ) { match expr { ast::Expr::Binary(left, _, right) => { Self::collect_tables_in_expr(left, aliases, all_tables, schema, tables); Self::collect_tables_in_expr(right, aliases, all_tables, schema, tables); } ast::Expr::Qualified(table_or_alias, _) => { // Handle database.table or just table/alias let table_str = table_or_alias.as_str(); let table_name = if let Some(actual_table) = aliases.get(table_str) { // It's an alias actual_table.clone() } else if table_str.contains('.') { // It might be database.table format, extract just the table name table_str .split('.') .next_back() .unwrap_or(table_str) .to_string() } else { // It's a direct table name table_str.to_string() }; tables.push(table_name); } ast::Expr::DoublyQualified(_database, table, _column) => { // For database.table.column, extract the table name tables.push(table.to_string()); } ast::Expr::Id(column) => { // Unqualified column - try to find which table has this column if all_tables.len() == 1 { tables.push(all_tables[0].clone()); } else { // Check which table has this column for table_name in all_tables { if let Some(table) = schema.get_btree_table(table_name) { if table .columns .iter() .any(|col| col.name.as_deref() == Some(column.as_str())) { tables.push(table_name.clone()); break; // Found the table, stop looking } } } } } ast::Expr::FunctionCall { args, .. } => { for arg in args { Self::collect_tables_in_expr(arg, aliases, all_tables, schema, tables); } } ast::Expr::InList { lhs, rhs, .. } => { Self::collect_tables_in_expr(lhs, aliases, all_tables, schema, tables); for item in rhs { Self::collect_tables_in_expr(item, aliases, all_tables, schema, tables); } } ast::Expr::InSelect { lhs, .. } => { Self::collect_tables_in_expr(lhs, aliases, all_tables, schema, tables); } ast::Expr::Between { lhs, start, end, .. } => { Self::collect_tables_in_expr(lhs, aliases, all_tables, schema, tables); Self::collect_tables_in_expr(start, aliases, all_tables, schema, tables); Self::collect_tables_in_expr(end, aliases, all_tables, schema, tables); } ast::Expr::Unary(_, expr) => { Self::collect_tables_in_expr(expr, aliases, all_tables, schema, tables); } _ => { // Literals, etc. don't reference tables } } } /// Populate the view by scanning the source table using a state machine /// This can be called multiple times and will resume from where it left off /// This method is only for materialized views and will persist data to the btree pub fn populate_from_table( &mut self, conn: &crate::sync::Arc, pager: &crate::sync::Arc, _btree_cursor: &mut dyn CursorTrait, ) -> crate::Result> { // Assert that this is a materialized view with a root page assert!( self.root_page != 0, "populate_from_table should only be called for materialized views with root_page" ); // Mark as nested for the duration of this call to prevent inner queries from // committing the outer transaction's dirty pages. We increment on every entry // and decrement on every exit (including IO yields and errors) so re-entrant // calls keep the counter balanced. conn.start_nested(); let result = self.populate_from_table_inner(conn, pager, _btree_cursor); conn.end_nested(); result } fn populate_from_table_inner( &mut self, conn: &crate::sync::Arc, pager: &crate::sync::Arc, _btree_cursor: &mut dyn CursorTrait, ) -> crate::Result> { 'outer: loop { match std::mem::replace(&mut self.populate_state, PopulateState::Done) { PopulateState::Start => { // Generate the SQL query for populating the view // It is best to use a standard query than a cursor for two reasons: // 1) Using a sql query will allow us to be much more efficient in cases where we only want // some rows, in particular for indexed filters // 2) There are two types of cursors: index and table. In some situations (like for example // if the table has an integer primary key), the key will be exclusively in the index // btree and not in the table btree. Using cursors would force us to be aware of this // distinction (and others), and ultimately lead to reimplementing the whole query // machinery (next step is which index is best to use, etc) let queries = self.sql_for_populate()?; self.populate_state = PopulateState::ProcessingAllTables { queries, current_idx: 0, }; } PopulateState::ProcessingAllTables { queries, current_idx, } => { if current_idx >= queries.len() { self.populate_state = PopulateState::Done; return Ok(IOResult::Done(())); } let query = queries[current_idx].clone(); // Use the parent connection directly for reading. // We need to use the same connection that has the uncommitted schema changes. // Creating a new connection would cause schema version mismatch issues because // the new connection's schema cookie check would fail (database file has old version). // Prepare the statement using the parent connection let stmt = conn.prepare(&query)?; self.populate_state = PopulateState::ProcessingOneTable { queries, current_idx, stmt: Box::new(stmt), rows_processed: 0, pending_row: None, }; } PopulateState::ProcessingOneTable { queries, current_idx, mut stmt, mut rows_processed, pending_row, } => { // If we have a pending row from a previous I/O interruption, process it first if let Some((rowid, values)) = pending_row { match self.process_one_row( rowid, values.clone(), current_idx, pager.clone(), )? { IOResult::Done(_) => { // Row processed successfully, continue to next row rows_processed += 1; } IOResult::IO(io) => { // Still not done, restore state with pending row and return self.populate_state = PopulateState::ProcessingOneTable { queries, current_idx, stmt, rows_processed, pending_row: Some((rowid, values)), }; return Ok(IOResult::IO(io)); } } } // Process rows one at a time - no batching loop { // This step() call resumes from where the statement left off match stmt.step()? { crate::vdbe::StepResult::Row => { // Get the row let row = stmt.row().ok_or_else(|| { LimboError::InternalError( "row should exist after StepResult::Row".to_string(), ) })?; // Extract values from the row let all_values: Vec = row.get_values().cloned().collect(); // Extract rowid and values using helper let (rowid, values) = match self.extract_rowid_and_values(all_values, current_idx) { Some(result) => result, None => { // Invalid rowid, skip this row rows_processed += 1; continue; } }; // Process this row match self.process_one_row( rowid, values.clone(), current_idx, pager.clone(), )? { IOResult::Done(_) => { // Row processed successfully, continue to next row rows_processed += 1; } IOResult::IO(io) => { // Save state and return I/O // We'll resume at the SAME row when called again (don't increment rows_processed) // The circuit still has unfinished work for this row self.populate_state = PopulateState::ProcessingOneTable { queries, current_idx, stmt, rows_processed, // Don't increment - row not done yet! pending_row: Some((rowid, values)), // Save the row for resumption }; return Ok(IOResult::IO(io)); } } } crate::vdbe::StepResult::Done => { // All rows processed from this table // Move to next table self.populate_state = PopulateState::ProcessingAllTables { queries, current_idx: current_idx + 1, }; continue 'outer; } crate::vdbe::StepResult::Interrupt | crate::vdbe::StepResult::Busy => { // Save state before returning error self.populate_state = PopulateState::ProcessingOneTable { queries, current_idx, stmt, rows_processed, pending_row: None, // No pending row when interrupted between rows }; return Err(LimboError::Busy); } crate::vdbe::StepResult::IO => { // Statement needs I/O - save state and return self.populate_state = PopulateState::ProcessingOneTable { queries, current_idx, stmt, rows_processed, pending_row: None, // No pending row when interrupted between rows }; // TODO: Get the actual I/O completion from the statement let completion = crate::io::Completion::new_yield(); return Ok(IOResult::IO(crate::types::IOCompletions::Single( completion, ))); } } } } PopulateState::Done => { return Ok(IOResult::Done(())); } } } } /// Process a single row through the circuit fn process_one_row( &mut self, rowid: i64, values: Vec, table_idx: usize, pager: Arc, ) -> crate::Result> { // Create a single-row delta let mut single_row_delta = Delta::new(); single_row_delta.insert(rowid, values); // Create a DeltaSet with this delta for the current table let mut delta_set = DeltaSet::new(); let table_name = self.referenced_tables[table_idx].name.clone(); delta_set.insert(table_name, single_row_delta); // Process through merge_delta self.merge_delta(delta_set, pager) } /// Extract rowid and values from a row fn extract_rowid_and_values( &self, all_values: Vec, table_idx: usize, ) -> Option<(i64, Vec)> { if let Some((idx, _)) = self.referenced_tables[table_idx].get_rowid_alias_column() { // The rowid is the value at the rowid alias column index let rowid = match all_values.get(idx) { Some(Value::Numeric(Numeric::Integer(id))) => *id, _ => return None, // Invalid rowid }; // All values are table columns (no separate rowid was selected) Some((rowid, all_values)) } else { // The last value is the explicitly selected rowid let rowid = match all_values.last() { Some(Value::Numeric(Numeric::Integer(id))) => *id, _ => return None, // Invalid rowid }; // Get all values except the rowid let values = all_values[..all_values.len() - 1].to_vec(); Some((rowid, values)) } } /// Merge a delta set of changes into the view's current state pub fn merge_delta( &mut self, delta_set: DeltaSet, pager: Arc, ) -> crate::Result> { // Early return if all deltas are empty if delta_set.is_empty() { return Ok(IOResult::Done(())); } // Use the circuit to process the deltas and write to btree let input_data = delta_set.into_map(); // The circuit now handles all btree I/O internally with the provided pager let _delta = return_if_io!(self.circuit.commit(input_data, pager)); Ok(IOResult::Done(())) } } #[cfg(test)] mod tests { use super::*; use crate::schema::{BTreeTable, ColDef, Column as SchemaColumn, Schema, Type}; use crate::sync::Arc; use turso_parser::ast; use turso_parser::parser::Parser; // Helper function to create a test schema with multiple tables fn create_test_schema() -> Schema { let mut schema = Schema::new(); // Create customers table let customers_table = BTreeTable { name: "customers".to_string(), root_page: 2, primary_key_columns: vec![("id".to_string(), ast::SortOrder::Asc)], columns: vec![ SchemaColumn::new( Some("id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, unique: false, hidden: false, notnull_conflict_clause: None, }, ), SchemaColumn::new_default_text(Some("name".to_string()), "TEXT".to_string(), None), ], has_rowid: true, is_strict: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, has_autoincrement: false, }; // Create orders table let orders_table = BTreeTable { name: "orders".to_string(), root_page: 3, primary_key_columns: vec![("id".to_string(), ast::SortOrder::Asc)], columns: vec![ SchemaColumn::new( Some("id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, unique: false, hidden: false, notnull_conflict_clause: None, }, ), SchemaColumn::new( Some("customer_id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef::default(), ), SchemaColumn::new_default_integer( Some("total".to_string()), "INTEGER".to_string(), None, ), ], has_rowid: true, is_strict: false, has_autoincrement: false, foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, unique_sets: vec![], }; // Create products table let products_table = BTreeTable { name: "products".to_string(), root_page: 4, primary_key_columns: vec![("id".to_string(), ast::SortOrder::Asc)], columns: vec![ SchemaColumn::new( Some("id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, unique: false, hidden: false, notnull_conflict_clause: None, }, ), SchemaColumn::new_default_text(Some("name".to_string()), "TEXT".to_string(), None), SchemaColumn::new( Some("price".to_string()), "REAL".to_string(), None, None, Type::Real, None, ColDef::default(), ), ], has_rowid: true, is_strict: false, has_autoincrement: false, foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, unique_sets: vec![], }; // Create logs table - without a rowid alias (no INTEGER PRIMARY KEY) let logs_table = BTreeTable { name: "logs".to_string(), root_page: 5, primary_key_columns: vec![], // No primary key, so no rowid alias columns: vec![ SchemaColumn::new( Some("message".to_string()), "TEXT".to_string(), None, None, Type::Text, None, ColDef::default(), ), SchemaColumn::new_default_integer( Some("level".to_string()), "INTEGER".to_string(), None, ), SchemaColumn::new_default_integer( Some("timestamp".to_string()), "INTEGER".to_string(), None, ), ], has_rowid: true, // Has implicit rowid but no alias is_strict: false, has_autoincrement: false, foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, unique_sets: vec![], }; schema .add_btree_table(Arc::new(customers_table)) .expect("Test setup: failed to add customers table"); schema .add_btree_table(Arc::new(orders_table)) .expect("Test setup: failed to add orders table"); schema .add_btree_table(Arc::new(products_table)) .expect("Test setup: failed to add products table"); schema .add_btree_table(Arc::new(logs_table)) .expect("Test setup: failed to add logs table"); schema } // Helper to parse SQL and extract the SELECT statement fn parse_select(sql: &str) -> ast::Select { let mut parser = Parser::new(sql.as_bytes()); let cmd = parser.next().unwrap().unwrap(); match cmd { ast::Cmd::Stmt(ast::Stmt::Select(select)) => select, _ => panic!("Expected SELECT statement"), } } // Type alias for the complex return type of extract_all_tables type ExtractedTableInfo = ( Vec>, HashMap, HashMap, HashMap>>, ); fn extract_all_tables(select: &ast::Select, schema: &Schema) -> Result { let mut referenced_tables = Vec::new(); let mut table_aliases = HashMap::default(); let mut qualified_table_names = HashMap::default(); let mut table_conditions = HashMap::default(); IncrementalView::extract_all_tables( select, schema, &mut referenced_tables, &mut table_aliases, &mut qualified_table_names, &mut table_conditions, )?; Ok(( referenced_tables, table_aliases, qualified_table_names, table_conditions, )) } #[test] fn test_extract_single_table() { let schema = create_test_schema(); let select = parse_select("SELECT * FROM customers"); let (tables, _, _, _table_conditions) = extract_all_tables(&select, &schema).unwrap(); assert_eq!(tables.len(), 1); assert_eq!(tables[0].name, "customers"); } #[test] fn test_tables_from_union() { let schema = create_test_schema(); let select = parse_select("SELECT name FROM customers union SELECT name from products"); let (tables, _, _, table_conditions) = extract_all_tables(&select, &schema).unwrap(); assert_eq!(tables.len(), 2); assert!(table_conditions.contains_key("customers")); assert!(table_conditions.contains_key("products")); } #[test] fn test_extract_tables_from_inner_join() { let schema = create_test_schema(); let select = parse_select( "SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id", ); let (tables, _, _, table_conditions) = extract_all_tables(&select, &schema).unwrap(); assert_eq!(tables.len(), 2); assert!(table_conditions.contains_key("customers")); assert!(table_conditions.contains_key("orders")); } #[test] fn test_extract_tables_from_multiple_joins() { let schema = create_test_schema(); let select = parse_select( "SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id INNER JOIN products ON orders.id = products.id", ); let (tables, _, _, table_conditions) = extract_all_tables(&select, &schema).unwrap(); assert_eq!(tables.len(), 3); assert!(table_conditions.contains_key("customers")); assert!(table_conditions.contains_key("orders")); assert!(table_conditions.contains_key("products")); } #[test] fn test_extract_tables_from_left_join() { let schema = create_test_schema(); let select = parse_select( "SELECT * FROM customers LEFT JOIN orders ON customers.id = orders.customer_id", ); let (tables, _, _, table_conditions) = extract_all_tables(&select, &schema).unwrap(); assert_eq!(tables.len(), 2); assert!(table_conditions.contains_key("customers")); assert!(table_conditions.contains_key("orders")); } #[test] fn test_extract_tables_from_cross_join() { let schema = create_test_schema(); let select = parse_select("SELECT * FROM customers CROSS JOIN orders"); let (tables, _, _, table_conditions) = extract_all_tables(&select, &schema).unwrap(); assert_eq!(tables.len(), 2); assert!(table_conditions.contains_key("customers")); assert!(table_conditions.contains_key("orders")); } #[test] fn test_extract_tables_with_aliases() { let schema = create_test_schema(); let select = parse_select("SELECT * FROM customers c INNER JOIN orders o ON c.id = o.customer_id"); let (tables, aliases, _, _table_conditions) = extract_all_tables(&select, &schema).unwrap(); // Should still extract the actual table names, not aliases assert_eq!(tables.len(), 2); let table_names: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect(); assert!(table_names.contains(&"customers")); assert!(table_names.contains(&"orders")); // Check that aliases are correctly mapped assert_eq!(aliases.get("c"), Some(&"customers".to_string())); assert_eq!(aliases.get("o"), Some(&"orders".to_string())); } #[test] fn test_extract_tables_nonexistent_table_error() { let schema = create_test_schema(); let select = parse_select("SELECT * FROM nonexistent"); let result = extract_all_tables(&select, &schema).map(|(tables, _, _, _)| tables); assert!(result.is_err()); assert!(result .unwrap_err() .to_string() .contains("Table 'nonexistent' not found")); } #[test] fn test_extract_tables_nonexistent_join_table_error() { let schema = create_test_schema(); let select = parse_select( "SELECT * FROM customers INNER JOIN nonexistent ON customers.id = nonexistent.id", ); let result = extract_all_tables(&select, &schema).map(|(tables, _, _, _)| tables); assert!(result.is_err()); assert!(result .unwrap_err() .to_string() .contains("Table 'nonexistent' not found")); } #[test] fn test_sql_for_populate_simple_query_no_where() { // Test simple query with no WHERE clause let schema = create_test_schema(); let select = parse_select("SELECT * FROM customers"); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); let view = IncrementalView::new( "test_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 1); // customers has id as rowid alias, so no need for explicit rowid assert_eq!(queries[0], "SELECT * FROM customers"); } #[test] fn test_sql_for_populate_simple_query_with_where() { // Test simple query with WHERE clause let schema = create_test_schema(); let select = parse_select("SELECT * FROM customers WHERE id > 10"); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); let view = IncrementalView::new( "test_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 1); // For single-table queries, we should get the full WHERE clause assert_eq!(queries[0], "SELECT * FROM customers WHERE id > 10"); } #[test] fn test_sql_for_populate_join_with_where_on_both_tables() { // Test JOIN query with WHERE conditions on both tables let schema = create_test_schema(); let select = parse_select( "SELECT * FROM customers c \ JOIN orders o ON c.id = o.customer_id \ WHERE c.id > 10 AND o.total > 100", ); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); let view = IncrementalView::new( "test_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 2); // With per-table WHERE extraction: // - customers table gets: c.id > 10 // - orders table gets: o.total > 100 assert!(queries .iter() .any(|q| q == "SELECT * FROM customers WHERE id > 10")); assert!(queries .iter() .any(|q| q == "SELECT * FROM orders WHERE total > 100")); } #[test] fn test_sql_for_populate_complex_join_with_mixed_conditions() { // Test complex JOIN with WHERE conditions mixing both tables let schema = create_test_schema(); let select = parse_select( "SELECT * FROM customers c \ JOIN orders o ON c.id = o.customer_id \ WHERE c.id > 10 AND o.total > 100 AND c.name = 'John' \ AND o.customer_id = 5 AND (c.id = 15 OR o.total = 200)", ); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); let view = IncrementalView::new( "test_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 2); // With per-table WHERE extraction: // - customers gets: c.id > 10 AND c.name = 'John' // - orders gets: o.total > 100 AND o.customer_id = 5 // Note: The OR condition (c.id = 15 OR o.total = 200) involves both tables, // so it cannot be extracted to either table individually // Check both queries exist (order doesn't matter) assert!(queries .contains(&"SELECT * FROM customers WHERE id > 10 AND name = 'John'".to_string())); assert!(queries .contains(&"SELECT * FROM orders WHERE total > 100 AND customer_id = 5".to_string())); } #[test] fn test_sql_for_populate_table_without_rowid_alias() { let schema = create_test_schema(); let select = parse_select("SELECT * FROM logs WHERE level > 2"); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); let view = IncrementalView::new( "test_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 1); // logs table has no rowid alias, so we need to explicitly select rowid assert_eq!(queries[0], "SELECT *, rowid FROM logs WHERE level > 2"); } #[test] fn test_sql_for_populate_join_with_and_without_rowid_alias() { // Test JOIN between a table with rowid alias and one without let schema = create_test_schema(); let select = parse_select( "SELECT * FROM customers c \ JOIN logs l ON c.id = l.level \ WHERE c.id > 10 AND l.level > 2", ); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); let view = IncrementalView::new( "test_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 2); // customers has rowid alias (id), logs doesn't assert!(queries.contains(&"SELECT * FROM customers WHERE id > 10".to_string())); assert!(queries.contains(&"SELECT *, rowid FROM logs WHERE level > 2".to_string())); } #[test] fn test_sql_for_populate_with_database_qualified_names() { // Test that database.table.column references are handled correctly // The table name in FROM should keep the database prefix, // but column names in WHERE should be unqualified let schema = create_test_schema(); // Test with single table using database qualification let select = parse_select("SELECT * FROM main.customers WHERE main.customers.id > 10"); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); let view = IncrementalView::new( "test_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 1); // The FROM clause should preserve the database qualification, // but the WHERE clause should have unqualified column names assert_eq!(queries[0], "SELECT * FROM main.customers WHERE id > 10"); } #[test] fn test_sql_for_populate_join_with_database_qualified_names() { // Test JOIN with database-qualified table and column references let schema = create_test_schema(); let select = parse_select( "SELECT * FROM main.customers c \ JOIN main.orders o ON c.id = o.customer_id \ WHERE main.customers.id > 10 AND main.orders.total > 100", ); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); let view = IncrementalView::new( "test_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 2); // The FROM clauses should preserve database qualification, // but WHERE clauses should have unqualified column names assert!(queries.contains(&"SELECT * FROM main.customers WHERE id > 10".to_string())); assert!(queries.contains(&"SELECT * FROM main.orders WHERE total > 100".to_string())); } #[test] fn test_where_extraction_for_three_tables_with_aliases() { // Test that WHERE clause extraction correctly separates conditions for 3+ tables // This addresses the concern about conditions "piling up" as joins increase let schema = create_test_schema(); let select = parse_select( "SELECT * FROM customers c JOIN orders o ON c.id = o.customer_id JOIN products p ON p.id = o.product_id WHERE c.id > 10 AND o.total > 100 AND p.price > 50", ); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); // Verify we extracted all three tables assert_eq!(tables.len(), 3); let table_names: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect(); assert!(table_names.contains(&"customers")); assert!(table_names.contains(&"orders")); assert!(table_names.contains(&"products")); // Verify aliases are correctly mapped assert_eq!(aliases.get("c"), Some(&"customers".to_string())); assert_eq!(aliases.get("o"), Some(&"orders".to_string())); assert_eq!(aliases.get("p"), Some(&"products".to_string())); // Generate populate queries to verify each table gets its own conditions let queries = IncrementalView::generate_populate_queries( &select, &tables, &aliases, &qualified_names, &table_conditions, ) .unwrap(); assert_eq!(queries.len(), 3); // Verify the exact queries generated for each table // The order might vary, so check all possibilities let expected_queries = vec![ "SELECT * FROM customers WHERE id > 10", "SELECT * FROM orders WHERE total > 100", "SELECT * FROM products WHERE price > 50", ]; for expected in &expected_queries { assert!( queries.contains(&expected.to_string()), "Missing expected query: {expected}. Got: {queries:?}" ); } } #[test] fn test_sql_for_populate_complex_expressions_not_included() { // Test that complex expressions (subqueries, CASE, string concat) are NOT included in populate queries let schema = create_test_schema(); let select = parse_select( "SELECT * FROM customers WHERE id > (SELECT MAX(customer_id) FROM orders) AND name || ' Customer' = 'John Customer' AND CASE WHEN id > 10 THEN 1 ELSE 0 END = 1 AND EXISTS (SELECT 1 FROM orders WHERE customer_id = customers.id)", ); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); let queries = IncrementalView::generate_populate_queries( &select, &tables, &aliases, &qualified_names, &table_conditions, ) .unwrap(); assert_eq!(queries.len(), 1); // Since customers table has an INTEGER PRIMARY KEY (id), we should get SELECT * // without rowid and without WHERE clause (all conditions are complex) assert_eq!(queries[0], "SELECT * FROM customers"); } #[test] fn test_sql_for_populate_unambiguous_unqualified_column() { // Test that unambiguous unqualified columns ARE extracted let schema = create_test_schema(); let select = parse_select( "SELECT * FROM customers c \ JOIN orders o ON c.id = o.customer_id \ WHERE total > 100", // 'total' only exists in orders table ); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); let view = IncrementalView::new( "test_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 2); // 'total' is unambiguous (only in orders), so it should be extracted assert!(queries.contains(&"SELECT * FROM customers".to_string())); assert!(queries.contains(&"SELECT * FROM orders WHERE total > 100".to_string())); } #[test] fn test_database_qualified_table_names() { let schema = create_test_schema(); // Test with database-qualified table names let select = parse_select( "SELECT c.id, c.name, o.id, o.total FROM main.customers c JOIN main.orders o ON c.id = o.customer_id WHERE c.id > 10", ); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); // Check that qualified names are preserved assert!(qualified_names.contains_key("customers")); assert_eq!(qualified_names.get("customers").unwrap(), "main.customers"); assert!(qualified_names.contains_key("orders")); assert_eq!(qualified_names.get("orders").unwrap(), "main.orders"); let view = IncrementalView::new( "test_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 2); // The FROM clause should contain the database-qualified name // But the WHERE clause should use unqualified column names assert!(queries.contains(&"SELECT * FROM main.customers WHERE id > 10".to_string())); assert!(queries.contains(&"SELECT * FROM main.orders".to_string())); } #[test] fn test_mixed_qualified_unqualified_tables() { let schema = create_test_schema(); // Test with a mix of qualified and unqualified table names let select = parse_select( "SELECT c.id, c.name, o.id, o.total FROM main.customers c JOIN orders o ON c.id = o.customer_id WHERE c.id > 10 AND o.total < 1000", ); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); // Check that qualified names are preserved where specified assert_eq!(qualified_names.get("customers").unwrap(), "main.customers"); // Unqualified tables should not have an entry (or have the bare name) assert!( !qualified_names.contains_key("orders") || qualified_names.get("orders").unwrap() == "orders" ); let view = IncrementalView::new( "test_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 2); // The FROM clause should preserve qualification where specified assert!(queries.contains(&"SELECT * FROM main.customers WHERE id > 10".to_string())); assert!(queries.contains(&"SELECT * FROM orders WHERE total < 1000".to_string())); } #[test] fn test_extract_tables_with_simple_cte() { let schema = create_test_schema(); let select = parse_select( "WITH customer_totals AS ( SELECT c.id, c.name, SUM(o.total) as total_spent FROM customers c JOIN orders o ON c.id = o.customer_id GROUP BY c.id, c.name ) SELECT * FROM customer_totals WHERE total_spent > 1000", ); let (tables, aliases, _qualified_names, _table_conditions) = extract_all_tables(&select, &schema).unwrap(); // Check that we found both tables from the CTE assert_eq!(tables.len(), 2); let table_names: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect(); assert!(table_names.contains(&"customers")); assert!(table_names.contains(&"orders")); // Check aliases from the CTE assert_eq!(aliases.get("c"), Some(&"customers".to_string())); assert_eq!(aliases.get("o"), Some(&"orders".to_string())); } #[test] fn test_extract_tables_with_multiple_ctes() { let schema = create_test_schema(); let select = parse_select( "WITH high_value_customers AS ( SELECT id, name FROM customers WHERE id IN (SELECT customer_id FROM orders WHERE total > 500) ), recent_orders AS ( SELECT id, customer_id, total FROM orders WHERE id > 100 ) SELECT hvc.name, ro.total FROM high_value_customers hvc JOIN recent_orders ro ON hvc.id = ro.customer_id", ); let (tables, _aliases, _qualified_names, _table_conditions) = extract_all_tables(&select, &schema).unwrap(); // Check that we found both tables from both CTEs assert_eq!(tables.len(), 2); let table_names: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect(); assert!(table_names.contains(&"customers")); assert!(table_names.contains(&"orders")); } #[test] fn test_sql_for_populate_union_mixed_conditions() { // Test UNION where same table appears with and without WHERE clause // This should drop ALL conditions to ensure we get all rows let schema = create_test_schema(); let select = parse_select( "SELECT * FROM customers WHERE id > 10 UNION ALL SELECT * FROM customers", ); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); let view = IncrementalView::new( "union_view".to_string(), select.clone(), tables, aliases, qualified_names, table_conditions, extract_view_columns(&select, &schema).unwrap(), &schema, 1, // main_data_root 2, // internal_state_root 3, // internal_state_index_root ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 1); // When the same table appears with and without WHERE conditions in a UNION, // we must fetch ALL rows (no WHERE clause) because the conditions are incompatible assert_eq!( queries[0], "SELECT * FROM customers", "UNION with mixed conditions (some with WHERE, some without) should fetch ALL rows" ); } #[test] fn test_extract_tables_with_nested_cte() { let schema = create_test_schema(); let select = parse_select( "WITH RECURSIVE customer_hierarchy AS ( SELECT id, name, 0 as level FROM customers WHERE id = 1 UNION ALL SELECT c.id, c.name, ch.level + 1 FROM customers c JOIN orders o ON c.id = o.customer_id JOIN customer_hierarchy ch ON o.customer_id = ch.id WHERE ch.level < 3 ) SELECT * FROM customer_hierarchy", ); let (tables, _aliases, _qualified_names, _table_conditions) = extract_all_tables(&select, &schema).unwrap(); // Check that we found the tables referenced in the recursive CTE let table_names: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect(); // We're finding duplicates because "customers" appears twice in the recursive CTE // Let's deduplicate let unique_tables: HashSet<&str> = table_names.iter().cloned().collect(); assert_eq!(unique_tables.len(), 2); assert!(unique_tables.contains("customers")); assert!(unique_tables.contains("orders")); } #[test] fn test_extract_tables_with_cte_and_main_query() { let schema = create_test_schema(); let select = parse_select( "WITH customer_stats AS ( SELECT customer_id, COUNT(*) as order_count FROM orders GROUP BY customer_id ) SELECT c.name, cs.order_count, p.name as product_name FROM customers c JOIN customer_stats cs ON c.id = cs.customer_id JOIN products p ON p.id = 1", ); let (tables, aliases, _qualified_names, _table_conditions) = extract_all_tables(&select, &schema).unwrap(); // Check that we found tables from both the CTE and the main query assert_eq!(tables.len(), 3); let table_names: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect(); assert!(table_names.contains(&"customers")); assert!(table_names.contains(&"orders")); assert!(table_names.contains(&"products")); // Check aliases from main query assert_eq!(aliases.get("c"), Some(&"customers".to_string())); assert_eq!(aliases.get("p"), Some(&"products".to_string())); } #[test] fn test_sql_for_populate_simple_union() { let schema = create_test_schema(); let select = parse_select( "SELECT * FROM orders WHERE total > 1000 UNION ALL SELECT * FROM orders WHERE total < 100", ); let (tables, aliases, qualified_names, table_conditions) = extract_all_tables(&select, &schema).unwrap(); // Generate populate queries let queries = IncrementalView::generate_populate_queries( &select, &tables, &aliases, &qualified_names, &table_conditions, ) .unwrap(); // We should have deduplicated to a single table assert_eq!(tables.len(), 1, "Should have one unique table"); assert_eq!(tables[0].name, "orders"); // Single table, order doesn't matter // Should have collected two conditions assert_eq!(table_conditions.get("orders").unwrap().len(), 2); // Should combine multiple conditions with OR assert_eq!(queries.len(), 1); // Conditions are combined with OR assert_eq!( queries[0], "SELECT * FROM orders WHERE (total > 1000) OR (total < 100)" ); } #[test] fn test_sql_for_populate_with_union_and_filters() { let schema = create_test_schema(); // Test UNION with different WHERE conditions on the same table let select = parse_select( "SELECT * FROM orders WHERE total > 1000 UNION ALL SELECT * FROM orders WHERE total < 100", ); let view = IncrementalView::from_stmt( ast::QualifiedName { db_name: None, name: ast::Name::exact("test_view".to_string()), alias: None, }, select, &schema, 1, 2, 3, ) .unwrap(); let queries = view.sql_for_populate().unwrap(); // We deduplicate tables, so we get 1 query for orders assert_eq!(queries.len(), 1); // Multiple conditions on the same table are combined with OR assert_eq!( queries[0], "SELECT * FROM orders WHERE (total > 1000) OR (total < 100)" ); } #[test] fn test_sql_for_populate_with_union_mixed_tables() { let schema = create_test_schema(); // Test UNION with different tables let select = parse_select( "SELECT id, name FROM customers WHERE id > 10 UNION ALL SELECT customer_id as id, 'Order' as name FROM orders WHERE total > 500", ); let view = IncrementalView::from_stmt( ast::QualifiedName { db_name: None, name: ast::Name::exact("test_view".to_string()), alias: None, }, select, &schema, 1, 2, 3, ) .unwrap(); let queries = view.sql_for_populate().unwrap(); assert_eq!(queries.len(), 2, "Should have one query per table"); // Check that each table gets its appropriate WHERE clause let customers_query = queries .iter() .find(|q| q.contains("FROM customers")) .unwrap(); let orders_query = queries.iter().find(|q| q.contains("FROM orders")).unwrap(); assert!(customers_query.contains("WHERE id > 10")); assert!(orders_query.contains("WHERE total > 500")); } #[test] fn test_sql_for_populate_duplicate_tables_conflicting_filters() { // This tests what happens when we have duplicate table references with different filters // We need to manually construct a view to simulate what would happen with CTEs let schema = create_test_schema(); // Get the orders table twice (simulating what would happen with CTEs) let orders_table = schema.get_btree_table("orders").unwrap(); let referenced_tables = vec![orders_table.clone(), orders_table]; // Create a SELECT that would have conflicting WHERE conditions let select = parse_select( "SELECT * FROM orders WHERE total > 1000", // This is just for the AST ); let view = IncrementalView::new( "test_view".to_string(), select.clone(), referenced_tables, HashMap::default(), HashMap::default(), HashMap::default(), extract_view_columns(&select, &schema).unwrap(), &schema, 1, 2, 3, ) .unwrap(); let queries = view.sql_for_populate().unwrap(); // With duplicates, we should get 2 identical queries assert_eq!(queries.len(), 2); // Both should be the same since they're from the same table reference assert_eq!(queries[0], queries[1]); } #[test] fn test_table_extraction_with_nested_ctes_complex_conditions() { let schema = create_test_schema(); let select = parse_select( "WITH customer_orders AS ( SELECT c.*, o.total FROM customers c JOIN orders o ON c.id = o.customer_id WHERE c.name LIKE 'A%' AND o.total > 100 ), top_customers AS ( SELECT * FROM customer_orders WHERE total > 500 ) SELECT * FROM top_customers", ); // Test table extraction directly without creating a view let mut tables = Vec::new(); let mut aliases = HashMap::default(); let mut qualified_names = HashMap::default(); let mut table_conditions = HashMap::default(); IncrementalView::extract_all_tables( &select, &schema, &mut tables, &mut aliases, &mut qualified_names, &mut table_conditions, ) .unwrap(); let table_names: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect(); // Should have one reference to each table assert_eq!(table_names.len(), 2, "Should have 2 table references"); assert!(table_names.contains(&"customers")); assert!(table_names.contains(&"orders")); // Check aliases assert_eq!(aliases.get("c"), Some(&"customers".to_string())); assert_eq!(aliases.get("o"), Some(&"orders".to_string())); } #[test] fn test_union_all_populate_queries() { // Test that UNION ALL generates correct populate queries let schema = create_test_schema(); // Create a UNION ALL query that references the same table twice with different WHERE conditions let sql = " SELECT id, name FROM customers WHERE id < 5 UNION ALL SELECT id, name FROM customers WHERE id > 10 "; let mut parser = Parser::new(sql.as_bytes()); let cmd = parser.next_cmd().unwrap(); let select_stmt = match cmd.unwrap() { turso_parser::ast::Cmd::Stmt(ast::Stmt::Select(select)) => select, _ => panic!("Expected SELECT statement"), }; // Extract tables and conditions let (tables, aliases, qualified_names, conditions) = extract_all_tables(&select_stmt, &schema).unwrap(); // Generate populate queries let queries = IncrementalView::generate_populate_queries( &select_stmt, &tables, &aliases, &qualified_names, &conditions, ) .unwrap(); // Expected query - assuming customers table has INTEGER PRIMARY KEY // so we don't need to select rowid separately let expected = "SELECT * FROM customers WHERE (id < 5) OR (id > 10)"; assert_eq!( queries.len(), 1, "Should generate exactly 1 query for UNION ALL with same table" ); assert_eq!(queries[0], expected, "Query should match expected format"); } #[test] fn test_union_all_different_tables_populate_queries() { // Test UNION ALL with different tables let schema = create_test_schema(); let sql = " SELECT id, name FROM customers WHERE id < 5 UNION ALL SELECT id, product_name FROM orders WHERE amount > 100 "; let mut parser = Parser::new(sql.as_bytes()); let cmd = parser.next_cmd().unwrap(); let select_stmt = match cmd.unwrap() { turso_parser::ast::Cmd::Stmt(ast::Stmt::Select(select)) => select, _ => panic!("Expected SELECT statement"), }; // Extract tables and conditions let (tables, aliases, qualified_names, conditions) = extract_all_tables(&select_stmt, &schema).unwrap(); // Generate populate queries let queries = IncrementalView::generate_populate_queries( &select_stmt, &tables, &aliases, &qualified_names, &conditions, ) .unwrap(); // Should generate separate queries for each table assert_eq!( queries.len(), 2, "Should generate 2 queries for different tables" ); // Check we have queries for both tables let has_customers = queries.iter().any(|q| q.contains("customers")); let has_orders = queries.iter().any(|q| q.contains("orders")); assert!(has_customers, "Should have a query for customers table"); assert!(has_orders, "Should have a query for orders table"); // Verify the customers query has its WHERE clause let customers_query = queries .iter() .find(|q| q.contains("customers")) .expect("Should have customers query"); assert!( customers_query.contains("WHERE"), "Customers query should have WHERE clause" ); } } ================================================ FILE: core/index_method/backing_btree.rs ================================================ use crate::sync::Arc; use crate::{ index_method::{ IndexMethod, IndexMethodAttachment, IndexMethodConfiguration, IndexMethodCursor, IndexMethodDefinition, BACKING_BTREE_INDEX_METHOD_NAME, }, LimboError, Result, }; /// Special 'backing_btree' index method which can be used by other custom index methods /// /// Under the hood, it's marked as 'treat_as_btree' which recognized by the tursodb core as a special index method /// which should be translated to ordinary btree but also do not explicitly managed by the core #[derive(Debug)] pub struct BackingBtreeIndexMethod; #[derive(Debug)] pub struct BackingBTreeIndexMethodAttachment(String); impl IndexMethod for BackingBtreeIndexMethod { fn attach( &self, configuration: &IndexMethodConfiguration, ) -> Result> { Ok(Arc::new(BackingBTreeIndexMethodAttachment( configuration.index_name.clone(), ))) } } impl IndexMethodAttachment for BackingBTreeIndexMethodAttachment { fn definition<'a>(&'a self) -> IndexMethodDefinition<'a> { IndexMethodDefinition { method_name: BACKING_BTREE_INDEX_METHOD_NAME, index_name: &self.0, patterns: &[], backing_btree: true, results_materialized: false, } } fn init(&self) -> Result> { Err(LimboError::InternalError( "init is not supported for backing_btree index method".to_string(), )) } } ================================================ FILE: core/index_method/fts.rs ================================================ use crate::sync::Arc; use crate::turso_assert; use crate::turso_debug_assert; use crate::{ index_method::{ parse_patterns, IndexMethod, IndexMethodAttachment, IndexMethodConfiguration, IndexMethodCursor, IndexMethodDefinition, }, return_if_io, schema::IndexColumn, storage::{ btree::{BTreeCursor, BTreeKey, CursorTrait}, pager::Pager, }, translate::collate::CollationSeq, types::{IOResult, ImmutableRecord, IndexInfo, KeyInfo, SeekKey, SeekOp, SeekResult, Text}, vdbe::Register, Connection, LimboError, Result, Value, }; use parking_lot::RwLock; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::io::{BufWriter, Write}; use std::ops::Range; use std::path::{Path, PathBuf}; use std::{cell::RefCell, sync::atomic::Ordering}; use tantivy::{ directory::{ error::{DeleteError, OpenReadError, OpenWriteError}, Directory, FileHandle, OwnedBytes, TerminatingWrite, WatchCallback, WatchHandle, }, merge_policy::NoMergePolicy, schema::{Field, Schema}, tokenizer::{ NgramTokenizer, RawTokenizer, SimpleTokenizer, TextAnalyzer, TokenStream, WhitespaceTokenizer, }, DocAddress, HasLen, Index, IndexReader, IndexSettings, IndexWriter, Searcher, TantivyDocument, }; use turso_parser::ast::{self, Select, SortOrder}; /// Name identifier for the FTS index method, used in `CREATE INDEX ... USING fts`. pub const FTS_INDEX_METHOD_NAME: &str = "fts"; /// Default memory budget (64MB) for Tantivy's IndexWriter. /// Controls how much memory Tantivy uses for in-memory indexing before flushing to disk. pub const DEFAULT_MEMORY_BUDGET_BYTES: usize = 64 * 1024 * 1024; /// Default chunk size (152KB) for splitting large files when storing in BTree. /// Files larger than this are split into multiple chunks for efficient storage and retrieval. pub const DEFAULT_CHUNK_SIZE: usize = 512 * 1024; /// Number of documents to batch before committing to Tantivy. /// Higher values improve throughput but increase memory usage and latency. pub const BATCH_COMMIT_SIZE: usize = 1000; /// Default memory budget (64MB) for hot cache (metadata + term dictionaries). /// Hot files are frequently accessed and kept in an LRU cache. pub const DEFAULT_HOT_CACHE_BYTES: usize = 64 * 1024 * 1024; /// Default memory budget (128MB) for chunk LRU cache. /// Caches segment data chunks loaded on-demand from the BTree. pub const DEFAULT_CHUNK_CACHE_BYTES: usize = 128 * 1024 * 1024; const ROWID_FIELD: &str = "rowid"; // Thread-local tokenizer cache to avoid creating a new tokenizer for each call. // TextAnalyzer is not Send/Sync, so we use thread_local storage. crate::thread::thread_local! { static FTS_TOKENIZER: RefCell = RefCell::new( TextAnalyzer::builder(SimpleTokenizer::default()) .filter(tantivy::tokenizer::LowerCaser) .build() ); } /// Highlight matching terms in text by wrapping them with tags. /// /// Standalone function that can be used without an FTS index. /// It tokenizes both the query and text using Tantivy's default tokenizer, /// finds matching terms, and wraps them with the specified tags. pub fn fts_highlight(text: &str, query: &str, before_tag: &str, after_tag: &str) -> String { if text.is_empty() || query.is_empty() { return text.to_string(); } FTS_TOKENIZER.with(|tokenizer| { let mut tokenizer = tokenizer.borrow_mut(); // Extract query terms (lowercased) let query_terms: HashSet = { let mut terms = HashSet::default(); let mut query_stream = tokenizer.token_stream(query); while let Some(token) = query_stream.next() { terms.insert(token.text.to_string()); } terms }; if query_terms.is_empty() { return text.to_string(); } // Tokenize the text and track positions of matching tokens let match_ranges: Vec<(usize, usize)> = { let mut ranges = Vec::new(); let mut text_stream = tokenizer.token_stream(text); while let Some(token) = text_stream.next() { if query_terms.contains(&token.text) { ranges.push((token.offset_from, token.offset_to)); } } ranges }; if match_ranges.is_empty() { return text.to_string(); } // Optimized string building: pre-calculate size and build forward let extra_len = match_ranges.len() * (before_tag.len() + after_tag.len()); let mut result = String::with_capacity(text.len() + extra_len); let mut last_end = 0; for (start, end) in &match_ranges { // Validate UTF-8 boundaries if *start > text.len() || *end > text.len() || !text.is_char_boundary(*start) || !text.is_char_boundary(*end) { continue; } // Append text before this match if *start > last_end { result.push_str(&text[last_end..*start]); } // Append highlighted match result.push_str(before_tag); result.push_str(&text[*start..*end]); result.push_str(after_tag); last_end = *end; } // Append remaining text after last match if last_end < text.len() { result.push_str(&text[last_end..]); } result }) } /// Check if text matches a query by testing for any common terms. /// /// Standalone function that can be used without an FTS index. /// It tokenizes both the query and text using Tantivy's default tokenizer, /// and returns true if any query terms appear in the text. pub fn fts_match(text: &str, query: &str) -> bool { if text.is_empty() || query.is_empty() { return false; } FTS_TOKENIZER.with(|tokenizer| { let mut tokenizer = tokenizer.borrow_mut(); // Extract query terms (lowercased) let query_terms: HashSet = { let mut terms = HashSet::default(); let mut query_stream = tokenizer.token_stream(query); while let Some(token) = query_stream.next() { terms.insert(token.text.to_string()); } terms }; if query_terms.is_empty() { return false; } // Tokenize the text and check if any query terms appear let mut text_stream = tokenizer.token_stream(text); while let Some(token) = text_stream.next() { if query_terms.contains(&token.text) { return true; } } false }) } /// File classification for hybrid caching strategy. /// Determines which files are kept hot in memory vs lazy-loaded on demand. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum FileCategory { /// Always in memory: meta.json, .managed.json, .lock (typically < 64KB) Metadata, /// Hot files: .term dictionaries - loaded on first access, kept in LRU TermDictionary, /// Fast fields and field norms - small, frequently accessed FastFields, /// Cold files: .idx, .pos, .store - lazy-loaded on demand SegmentData, } impl FileCategory { const METADATA_FILES: [&'static str; 3] = [TANTIVY_META_FILE, ".managed.json", ".lock"]; /// Classify a file based on its path/extension. /// https://fulmicoton.gitbooks.io/tantivy-doc/content/index-files.html fn from_path(path: &Path) -> Self { let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); // Check for known Tantivy metadata files first if Self::METADATA_FILES.contains(&name) { return FileCategory::Metadata; } match ext { // Term dictionary - hot for queries "term" => FileCategory::TermDictionary, // Fast fields and field norms - small, frequently accessed "fast" | "fieldnorm" => FileCategory::FastFields, // Segment data - large, lazy-loaded "idx" | "pos" | "store" => FileCategory::SegmentData, "lock" | "info" => FileCategory::Metadata, // Default to segment data (lazy-loaded) _ => FileCategory::SegmentData, } } /// Returns true if files in this category should be preloaded at startup. const fn should_preload(&self) -> bool { matches!(self, FileCategory::Metadata) } /// Returns true if files in this category should be kept in the hot cache. const fn is_hot(&self) -> bool { matches!( self, FileCategory::Metadata | FileCategory::TermDictionary | FileCategory::FastFields ) } } /// Metadata about a file stored in the FTS directory. /// Used for catalog-first loading where we build file metadata without loading content. #[derive(Debug, Clone)] struct FileMetadata { /// Total file size in bytes size: usize, /// Number of chunks this file is split into num_chunks: usize, /// File category for caching decisions category: FileCategory, } impl FileMetadata { fn new(path: &Path, size: usize, num_chunks: usize) -> Self { Self { size, num_chunks, category: FileCategory::from_path(path), } } } type ChunkKey = (PathBuf, i64); /// Eviction samples per put const EVICTION_SAMPLES: usize = 8; /// Generic bounded LRU cache with sampling-based eviction. pub struct LruCache { capacity: usize, inner: RwLock>, } #[derive(Debug)] struct LruCacheInner { current_size: usize, clock: u64, entries: HashMap, } #[derive(Debug)] struct LruCacheEntry { data: Arc<[u8]>, accessed: u64, } impl std::fmt::Debug for LruCache { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let inner = self.inner.read(); f.debug_struct("LruCache") .field("capacity", &self.capacity) .field("current_size", &inner.current_size) .field("entries", &inner.entries.len()) .finish() } } impl LruCache { /// Creates a new empty cache with the specified capacity in bytes. fn new(capacity: usize) -> Self { Self { capacity, inner: RwLock::new(LruCacheInner { current_size: 0, clock: 0, entries: HashMap::default(), }), } } /// Lookup entry, updating access timestamp. Returns Arc-cloned data. fn get(&self, key: &Q) -> Option> where K: std::borrow::Borrow, Q: Eq + std::hash::Hash + ?Sized, { let mut inner = self.inner.write(); inner.clock += 1; let ts = inner.clock; if let Some(entry) = inner.entries.get_mut(key) { entry.accessed = ts; Some(Arc::clone(&entry.data)) } else { None } } /// Insert entry, evicting stale entries if over capacity. /// /// Eviction uses sampling: examines K entries and evicts the one with /// the oldest access timestamp. Repeat until under capacity. fn put(&self, key: K, value: Vec) { let arc_value: Arc<[u8]> = Arc::from(value); let size = arc_value.len(); let mut inner = self.inner.write(); // Check for existing entry - get old size if present let old_size = inner.entries.get(&key).map(|e| e.data.len()); if let Some(old) = old_size { // Update existing entry inner.clock += 1; let ts = inner.clock; let entry = inner.entries.get_mut(&key).expect("entry must exist"); entry.data = arc_value; entry.accessed = ts; inner.current_size = inner.current_size - old + size; return; } // Evict until under capacity while inner.current_size + size > self.capacity && !inner.entries.is_empty() { let victim = { inner .entries .iter() .take(EVICTION_SAMPLES) .min_by_key(|(_, e)| e.accessed) .map(|(k, _)| k.clone()) }; match victim { Some(k) => { if let Some(e) = inner.entries.remove(&k) { inner.current_size -= e.data.len(); } } None => break, } } inner.clock += 1; let ts = inner.clock; inner.entries.insert( key, LruCacheEntry { data: arc_value, accessed: ts, }, ); inner.current_size += size; } /// Remove an entry from the cache. fn remove(&self, key: &Q) where K: std::borrow::Borrow, Q: Eq + std::hash::Hash + ?Sized, { let mut inner = self.inner.write(); if let Some(e) = inner.entries.remove(key) { inner.current_size -= e.data.len(); } } /// Current memory usage in bytes. fn size(&self) -> usize { self.inner.read().current_size } /// Number of entries in the cache. fn len(&self) -> usize { self.inner.read().entries.len() } /// Check if key exists in cache. fn contains(&self, key: &Q) -> bool where K: std::borrow::Borrow, Q: Eq + std::hash::Hash + ?Sized, { self.inner.read().entries.contains_key(key) } } /// Specialized methods for ChunkKey (PathBuf, i64) caches. impl LruCache { /// Invalidate all chunks for a file path. /// Called when a file is deleted or overwritten. fn invalidate(&self, path: &Path) { let mut inner = self.inner.write(); let mut freed = 0usize; inner.entries.retain(|(p, _), e| { if p == path { freed += e.data.len(); false } else { true } }); inner.current_size -= freed; } } /// Specialized methods for PathBuf caches (hot files). impl LruCache { /// Create from preloaded files (used during initialization). fn with_preloaded(capacity: usize, files: HashMap>) -> Self { let current_size: usize = files.values().map(|v| v.len()).sum(); let entries: HashMap = files .into_iter() .enumerate() .map(|(i, (path, data))| { ( path, LruCacheEntry { data: Arc::from(data), accessed: i as u64, }, ) }) .collect(); Self { capacity, inner: RwLock::new(LruCacheInner { current_size, clock: entries.len() as u64, entries, }), } } } /// Type aliases to please the almighty clippy type Catalog = HashMap; type PendingWrites = HashMap>; /// Tantivy Directory implementation backed by Turso's BTree storage. /// /// Tantivy stores its index as a collection of files (segments, metadata, term dictionaries, etc.). /// The `Directory` trait is Tantivy's storage abstraction for reading, writing, and managing /// these files. Tantivy's Directory methods are synchronous, so we must do blocking IO to back /// these operations and cache data in memory for performance. /// /// FTS index files are stored in a BTree with the schema `(path TEXT, chunk_no INTEGER, bytes BLOB)`. /// Large files are split into chunks of `DEFAULT_CHUNK_SIZE` (1MB) to enable efficient /// partial reads and bounded memory usage during loading. /// /// We use a two-tier caching strategy to optimize for Tantivy's access patterns: /// /// 1. `hot_cache` (keyed by `PathBuf`): Caches entire files for small, /// frequently-accessed files that benefit from being fully resident in memory: /// - Metadata files (meta.json, .managed.json, .lock) /// - Term dictionaries (.term) - critical for query performance /// - Fast fields and field norms (.fast, .fieldnorm) /// /// 2. `chunk_cache` (keyed by `(PathBuf, chunk_no)`): Caches individual 1MB chunks /// of large segment files. Large files like posting lists (.idx), positions (.pos), /// and document store (.store) are split into 1MB chunks when stored in the BTree. /// When Tantivy reads a byte range, we load only the chunks covering that range, /// allowing partial file access without loading entire multi-MB files into memory. /// /// Writes are buffered in memory (`pending_writes`) and flushed to the BTree when: /// - A Tantivy commit occurs (via `commit_and_flush`) /// - The cursor is dropped with pending documents /// - The transaction is about to commit (via `pre_commit`) /// /// During flush, writes are moved to `flushing_writes` so they remain readable while /// the async BTree write completes. #[derive(Clone)] struct HybridBTreeDirectory { /// File catalog: path -> metadata (always in memory, no content) catalog: Arc>, /// Hot cache: LRU cache for frequently accessed files (metadata, term dictionaries) /// Bounded to DEFAULT_HOT_CACHE_BYTES (64MB) to prevent unbounded memory growth hot_cache: Arc>, /// Chunk cache: LRU cache for lazy-loaded segment chunks chunk_cache: Arc>, /// Pending writes to be flushed to BTree pending_writes: Arc>, /// Writes currently being flushed to BTree (still readable during flush) /// This preserves data for reads during async flush operations flushing_writes: Arc>>>, /// Pending deletes to be flushed to BTree pending_deletes: Arc>>, /// Reference to pager for IO pager: Arc, /// BTree root page for the FTS directory index btree_root_page: i64, } impl std::fmt::Debug for HybridBTreeDirectory { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("HybridBTreeDirectory") .field("catalog_size", &self.catalog.read().len()) .field("hot_cache_size", &self.hot_cache.len()) .field("hot_cache_bytes", &self.hot_cache.size()) .field("chunk_cache_size", &self.chunk_cache.size()) .field("btree_root_page", &self.btree_root_page) .finish() } } impl HybridBTreeDirectory { /// Create a clone with fresh (empty) pending state. /// This is used when creating a new cursor from a cached directory to ensure /// each cursor has its own isolated pending_writes/pending_deletes. /// This prevents the bug where writes from one cursor affect the Drop behavior /// of another cursor. fn clone_with_fresh_pending(&self) -> Self { Self { catalog: Arc::clone(&self.catalog), hot_cache: Arc::clone(&self.hot_cache), chunk_cache: Arc::clone(&self.chunk_cache), // Fresh pending state - not shared with cache pending_writes: Arc::new(RwLock::new(HashMap::default())), flushing_writes: Arc::new(RwLock::new(HashMap::default())), pending_deletes: Arc::new(RwLock::new(Vec::new())), pager: Arc::clone(&self.pager), btree_root_page: self.btree_root_page, } } } fn io_not_found>>(msg: M) -> std::io::Error { std::io::Error::new(std::io::ErrorKind::NotFound, msg) } impl HybridBTreeDirectory { /// Create from preloaded catalog and hot cache files. fn with_preloaded( pager: Arc, btree_root_page: i64, catalog: HashMap, hot_files: HashMap>, hot_cache_capacity: usize, chunk_cache_capacity: usize, ) -> Self { Self { catalog: Arc::new(RwLock::new(catalog)), hot_cache: Arc::new(LruCache::::with_preloaded( hot_cache_capacity, hot_files, )), chunk_cache: Arc::new(LruCache::::new(chunk_cache_capacity)), pending_writes: Arc::new(RwLock::new(HashMap::default())), flushing_writes: Arc::new(RwLock::new(HashMap::default())), pending_deletes: Arc::new(RwLock::new(Vec::new())), pager, btree_root_page, } } /// Get pending writes for flushing. /// With HashMap, writes are automatically deduplicated (only latest write per path is kept). /// The writes are also copied to flushing_writes so they remain readable during async flush. fn take_pending_writes(&self) -> Vec<(PathBuf, Vec)> { let mut pending = self.pending_writes.write(); let writes_map = std::mem::take(&mut *pending); // Convert HashMap to Vec for the state machine let writes: Vec<(PathBuf, Vec)> = writes_map.into_iter().collect(); // Copy to flushing_writes so data remains readable during async flush { let mut flushing = self.flushing_writes.write(); for (path, data) in &writes { flushing.insert(path.clone(), data.clone()); } } tracing::debug!("FTS take_pending_writes: {} entries", writes.len()); writes } /// Clear flushing_writes after flush completes successfully. /// Call this after all writes have been persisted to BTree. fn complete_flush(&self) { let mut flushing = self.flushing_writes.write(); tracing::debug!( "FTS complete_flush: clearing {} entries from flushing_writes", flushing.len() ); flushing.clear(); } /// Find file data in pending writes or flushing writes. /// Checks pending_writes first (O(1) HashMap lookup), then flushing_writes. fn find_in_pending_writes(&self, path: &Path) -> Option> { // Check pending_writes first (most recent) - O(1) lookup { let pending = self.pending_writes.read(); if let Some(data) = pending.get(path) { return Some(data.clone()); } } // Check flushing_writes (data being flushed but not yet in BTree) { let flushing = self.flushing_writes.read(); if let Some(data) = flushing.get(path) { return Some(data.clone()); } } None } const CHUNK_LEN: usize = 3; /// Blocking read of a range of chunks from BTree using a single cursor. /// Efficient for both single and multiple chunk reads, as it only seeks once /// and advances sequentially. fn get_chunks_range_blocking( &self, path: &Path, start_chunk: usize, end_chunk: usize, ) -> std::io::Result>> { if start_chunk > end_chunk { return Ok(Vec::new()); } let mut chunks = Vec::with_capacity(end_chunk - start_chunk + 1); let path_str = path.to_string_lossy().to_string(); // Check cache for all requested chunks first let mut uncached_start = None; for chunk_no in start_chunk..=end_chunk { let cache_key = (path.to_path_buf(), chunk_no as i64); if let Some(chunk) = self.chunk_cache.get(&cache_key) { chunks.push(chunk); } else { // Found first uncached chunk uncached_start = Some(chunk_no); break; } } // If all chunks were cached, return them if uncached_start.is_none() { return Ok(chunks); } let uncached_start = uncached_start.unwrap(); // Create cursor and seek to first uncached chunk let mut cursor = BTreeCursor::new(self.pager.clone(), self.btree_root_page, Self::CHUNK_LEN); cursor.index_info = Some(Arc::new(IndexInfo { has_rowid: false, num_cols: Self::CHUNK_LEN, key_info: vec![key_info(), key_info(), key_info()], is_unique: false, })); let seek_key = ImmutableRecord::from_values( &[ Value::Text(Text::new(path_str.clone())), Value::from_i64(uncached_start as i64), Value::Blob(vec![]), ], Self::CHUNK_LEN, ); // Blocking seek to first chunk loop { match cursor.seek(SeekKey::IndexKey(&seek_key), SeekOp::GE { eq_only: false }) { Ok(IOResult::Done(SeekResult::Found)) => break, Ok(IOResult::Done(SeekResult::TryAdvance)) => { loop { match cursor.next() { Ok(IOResult::Done(_)) => { if !cursor.has_record() { return Err(io_not_found(format!( "chunk {}:{} not found", path.display(), uncached_start ))); } break; } Ok(IOResult::IO(_)) => { self.pager .io .step() .map_err(|e| std::io::Error::other(e.to_string()))?; } Err(e) => return Err(std::io::Error::other(e.to_string())), } } break; } Ok(IOResult::Done(SeekResult::NotFound)) => { return Err(io_not_found(format!( "chunk {}:{} not found", path.display(), uncached_start ))); } Ok(IOResult::IO(_)) => { self.pager .io .step() .map_err(|e| std::io::Error::other(e.to_string()))?; } Err(e) => return Err(std::io::Error::other(e.to_string())), } } // Read remaining chunks sequentially for expected_chunk_no in uncached_start..=end_chunk { // Check if cursor has a record if !cursor.has_record() { return Err(io_not_found(format!( "chunk {}:{} not found (cursor exhausted)", path.display(), expected_chunk_no ))); } // Read current record let record = loop { match cursor.record() { Ok(IOResult::Done(r)) => break r, Ok(IOResult::IO(_)) => { self.pager .io .step() .map_err(|e| std::io::Error::other(e.to_string()))?; } Err(e) => return Err(std::io::Error::other(e.to_string())), } }; let record = record.ok_or_else(|| io_not_found("no record at cursor"))?; // Extract and validate let found_path = record.get_value_opt(0).and_then(|v| match v { crate::types::ValueRef::Text(t) => Some(t.value.to_string()), _ => None, }); let found_chunk = record.get_value_opt(1).and_then(|v| match v { crate::types::ValueRef::Numeric(crate::numeric::Numeric::Integer(i)) => Some(i), _ => None, }); let bytes = record.get_value_opt(2).and_then(|v| match v { crate::types::ValueRef::Blob(b) => Some(b.to_vec()), _ => None, }); let (found_path_str, found_chunk_no, bytes) = match (found_path, found_chunk, bytes) { (Some(p), Some(c), Some(b)) => (p, c, b), _ => { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "malformed chunk record", )) } }; if found_path_str != path_str || found_chunk_no != expected_chunk_no as i64 { return Err(io_not_found(format!( "wrong chunk: expected {path_str}:{expected_chunk_no}, got {found_path_str}:{found_chunk_no}", ))); } // Cache and collect the chunk if can_cache_chunks(path) { let cache_key = (path.to_path_buf(), expected_chunk_no as i64); self.chunk_cache.put(cache_key, bytes.clone()); } chunks.push(Arc::from(bytes)); // Advance cursor to next record (unless this is the last chunk we need) if expected_chunk_no < end_chunk { loop { match cursor.next() { Ok(IOResult::Done(_)) => break, Ok(IOResult::IO(_)) => { self.pager .io .step() .map_err(|e| std::io::Error::other(e.to_string()))?; } Err(e) => return Err(std::io::Error::other(e.to_string())), } } } } Ok(chunks) } /// Load an entire file by concatenating all its chunks (blocking). /// Uses efficient bulk read with a single cursor seek. fn load_file_blocking(&self, path: &Path) -> std::io::Result> { let catalog = self.catalog.read(); let metadata = catalog .get(path) .ok_or_else(|| io_not_found(format!("file not in catalog: {}", path.display())))?; if metadata.num_chunks == 0 { return Ok(Vec::new()); } let chunks = self.get_chunks_range_blocking(path, 0, metadata.num_chunks.saturating_sub(1))?; let mut result = Vec::with_capacity(metadata.size); for chunk in chunks { result.extend_from_slice(&chunk); } Ok(result) } /// Add a file to the hot cache. fn add_to_hot_cache(&self, path: PathBuf, data: Vec) { self.hot_cache.put(path, data); } /// Update the catalog with file metadata. fn update_catalog(&self, path: PathBuf, metadata: FileMetadata) { let mut catalog = self.catalog.write(); catalog.insert(path, metadata); } } /// Simple in-memory file handle for data already loaded (hot cache, pending writes). /// Use `Arc<[u8]>` for zero-copy reads when backed by the hot cache. struct InMemoryFileHandle { data: Arc<[u8]>, } impl std::fmt::Debug for InMemoryFileHandle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("InMemoryFileHandle") .field("len", &self.data.len()) .finish() } } impl HasLen for InMemoryFileHandle { fn len(&self) -> usize { self.data.len() } } impl FileHandle for InMemoryFileHandle { fn read_bytes(&self, range: Range) -> std::io::Result { if range.end > self.data.len() { return Err(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, "range exceeds file length", )); } if range.start >= range.end { return Ok(OwnedBytes::empty()); } Ok(OwnedBytes::new(Arc::clone(&self.data)).slice(range)) } } /// Lazy file handle that fetches chunks on demand. struct LazyFileHandle { path: PathBuf, size: usize, directory: HybridBTreeDirectory, } impl std::fmt::Debug for LazyFileHandle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LazyFileHandle") .field("path", &self.path) .field("size", &self.size) .finish() } } impl HasLen for LazyFileHandle { fn len(&self) -> usize { self.size } } impl FileHandle for LazyFileHandle { fn read_bytes(&self, range: Range) -> std::io::Result { if range.end > self.size { return Err(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, format!( "range {:?} exceeds file size {} for {}", range, self.size, self.path.display() ), )); } if range.start >= range.end { return Ok(OwnedBytes::new(Vec::new())); } // Check hot cache first if let Some(data) = self.directory.hot_cache.get(&self.path) { return Ok(OwnedBytes::new(data).slice(range)); } // Check pending/flushing writes (data not yet persisted to BTree) if let Some(data) = self.directory.find_in_pending_writes(&self.path) { return Ok(OwnedBytes::new(data[range].to_vec())); } // Calculate required chunks let chunk_size = DEFAULT_CHUNK_SIZE; let start_chunk = range.start / chunk_size; let end_chunk = range.end.saturating_sub(1) / chunk_size; // Use efficient bulk read when multiple chunks are needed let chunks = self.directory .get_chunks_range_blocking(&self.path, start_chunk, end_chunk)?; // Collect result from chunks let mut result = Vec::with_capacity(range.len()); for (i, chunk) in chunks.into_iter().enumerate() { let chunk_no = start_chunk + i; let chunk_start = chunk_no * chunk_size; // Calculate slice within this chunk let local_start = if chunk_no == start_chunk { range.start - chunk_start } else { 0 }; let local_end = if chunk_no == end_chunk { range.end - chunk_start } else { chunk.len() }; // Defensive bounds check - should not be needed if logic is correct turso_debug_assert!( local_start <= chunk.len() && local_end <= chunk.len(), "chunk slice out of bounds", { "local_start": local_start, "local_end": local_end, "chunk_len": chunk.len() } ); let local_end = local_end.min(chunk.len()); let local_start = local_start.min(local_end); result.extend_from_slice(&chunk[local_start..local_end]); } Ok(OwnedBytes::new(result)) } } /// In-memory writer for HybridBTreeDirectory. struct HybridWriter { path: PathBuf, buffer: Vec, directory: HybridBTreeDirectory, } impl Write for HybridWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.buffer.extend_from_slice(buf); Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } impl Drop for HybridWriter { fn drop(&mut self) { // Commit the write to the directory let data = std::mem::take(&mut self.buffer); if !data.is_empty() { // Update catalog let num_chunks = data.len().div_ceil(DEFAULT_CHUNK_SIZE); let metadata = FileMetadata::new(&self.path, data.len(), num_chunks); self.directory .update_catalog(self.path.clone(), metadata.clone()); // If it's a hot file category, add to hot cache if metadata.category.is_hot() { self.directory .add_to_hot_cache(self.path.clone(), data.clone()); } // Queue for BTree flush (HashMap auto-deduplicates by path) let mut pending = self.directory.pending_writes.write(); pending.insert(self.path.clone(), data); } } } impl TerminatingWrite for HybridWriter { fn terminate_ref(&mut self, _: tantivy::directory::AntiCallToken) -> std::io::Result<()> { let data = std::mem::take(&mut self.buffer); // Calculate chunks (0 for empty files, consistent with Drop impl) let num_chunks = data.len().div_ceil(DEFAULT_CHUNK_SIZE); // Update catalog - even empty files should exist in the catalog let metadata = FileMetadata::new(&self.path, data.len(), num_chunks); self.directory .update_catalog(self.path.clone(), metadata.clone()); // If it's a hot file category, add to hot cache (even if empty) if metadata.category.is_hot() { self.directory .add_to_hot_cache(self.path.clone(), data.clone()); } // Queue for BTree flush (HashMap auto-deduplicates by path) // Empty files are still queued to ensure they can be read back from pending writes let mut pending = self.directory.pending_writes.write(); pending.insert(self.path.clone(), data); Ok(()) } } impl Directory for HybridBTreeDirectory { fn get_file_handle( &self, path: &Path, ) -> std::result::Result, OpenReadError> { if let Some(data) = self.hot_cache.get(path) { return Ok(Arc::new(InMemoryFileHandle { data })); } // Check pending writes (files written but not yet flushed to BTree) // This is critical for cold files that are immediately read back by Tantivy if let Some(data) = self.find_in_pending_writes(path) { return Ok(Arc::new(InMemoryFileHandle { data: Arc::from(data), })); } // Check catalog for file metadata let catalog = self.catalog.read(); let metadata = catalog .get(path) .ok_or_else(|| OpenReadError::FileDoesNotExist(path.to_path_buf()))?; Ok(Arc::new(LazyFileHandle { path: path.to_path_buf(), size: metadata.size, directory: self.clone(), })) } fn exists(&self, path: &Path) -> std::result::Result { // Check hot cache if self.hot_cache.contains(path) { return Ok(true); } // Check catalog let catalog = self.catalog.read(); Ok(catalog.contains_key(path)) } fn delete(&self, path: &Path) -> std::result::Result<(), DeleteError> { // Remove from hot cache self.hot_cache.remove(path); // Remove from catalog { let mut catalog = self.catalog.write(); catalog.remove(path); } if can_cache_chunks(path) { // Invalidate chunk cache self.chunk_cache.invalidate(path); } // Queue for BTree deletion { let mut pending = self.pending_deletes.write(); pending.push(path.to_path_buf()); } Ok(()) } fn open_write( &self, path: &Path, ) -> std::result::Result>, OpenWriteError> { // Tantivy's Directory trait documentation states files "may not previously exist", // and the standard MmapDirectory implementation uses OpenOptions::create_new(true) // which fails with FileAlreadyExists if the file is present. // However, Tantivy may call open_write on existing files during operations like // segment merging or metadata updates. To handle this gracefully, we delete any // existing file first. The error is ignored because: // 1. If the file doesn't exist, delete() succeeds (no-op on missing files) // 2. Our delete() implementation always returns Ok(()) - it only removes entries // from in-memory structures (hot_cache, catalog, chunk_cache) and queues the // BTree deletion, none of which can fail. // // Skip delete for the meta lock file: Tantivy calls open_write on it for every // search query, but it's never cached (can_cache_chunks returns false), never in // hot_cache, and doesn't need BTree deletion, so delete() is pure overhead. if path != Path::new(TANTIVY_META_LOCK_FILE) { let _ = self.delete(path); } let writer: Box = Box::new(HybridWriter { path: path.to_path_buf(), buffer: Vec::new(), directory: self.clone(), }); Ok(BufWriter::new(writer)) } fn atomic_read(&self, path: &Path) -> std::result::Result, OpenReadError> { // Check hot cache first (includes recently written files) if let Some(data) = self.hot_cache.get(path) { return Ok(data.to_vec()); } // Check pending writes (files written but not yet flushed to BTree) if let Some(data) = self.find_in_pending_writes(path) { return Ok(data); } // Check if file exists in catalog { let catalog = self.catalog.read(); if !catalog.contains_key(path) { return Err(OpenReadError::FileDoesNotExist(path.to_path_buf())); } } // Load file blocking from BTree self.load_file_blocking(path) .map_err(|e| OpenReadError::IoError { io_error: Arc::new(e), filepath: path.to_path_buf(), }) } fn atomic_write(&self, path: &Path, data: &[u8]) -> std::io::Result<()> { // Update catalog let num_chunks = data.len().div_ceil(DEFAULT_CHUNK_SIZE).max(1); let metadata = FileMetadata::new(path, data.len(), num_chunks); self.update_catalog(path.to_path_buf(), metadata.clone()); // If it's a hot file category, add to hot cache if metadata.category.is_hot() { self.add_to_hot_cache(path.to_path_buf(), data.to_vec()); } // Queue for BTree flush (HashMap auto-deduplicates by path) let mut pending = self.pending_writes.write(); pending.insert(path.to_path_buf(), data.to_vec()); Ok(()) } fn sync_directory(&self) -> std::io::Result<()> { Ok(()) } fn watch(&self, _cb: WatchCallback) -> std::result::Result { Ok(WatchHandle::empty()) } } /// Creates default `KeyInfo` for BTree index columns. fn key_info() -> KeyInfo { KeyInfo { sort_order: SortOrder::Asc, collation: CollationSeq::Binary, } } /// Creates an AST `Name` node from a string. fn name(name: impl ToString) -> ast::Name { ast::Name::exact(name.to_string()) } /// Parse field weights from a string like "body=2.0,title=1.0" /// Returns a HashMap mapping column names to tantivy 'boost factors' fn parse_field_weights(weights_str: &str, columns: &[IndexColumn]) -> Result> { let mut weights = HashMap::default(); if weights_str.is_empty() { return Ok(weights); } // Get valid column names for validation let valid_columns: HashSet<&str> = columns.iter().map(|c| c.name.as_str()).collect(); // Parse format: "col1=1.5,col2=2.0" for part in weights_str.split(',') { let part = part.trim(); if part.is_empty() { continue; } let (col_name, weight_str) = part.split_once('=').ok_or_else(|| { LimboError::ParseError(format!( "invalid weight format '{part}'. Expected 'column=weight' (e.g., 'title=2.0')", )) })?; let col_name = col_name.trim(); let weight_str = weight_str.trim(); // Validate column exists in index if !valid_columns.contains(col_name) { return Err(LimboError::ParseError(format!( "unknown column '{}' in weights. Valid columns: {}", col_name, columns .iter() .map(|c| c.name.as_str()) .collect::>() .join(", ") ))); } let weight: f32 = weight_str.parse().map_err(|_| { LimboError::ParseError(format!( "invalid weight value '{weight_str}' for column '{col_name}'. Expected a number (e.g., 2.0)", )) })?; if weight <= 0.0 { return Err(LimboError::ParseError(format!( "weight for column '{col_name}' must be positive, got {weight}", ))); } weights.insert(col_name.to_string(), weight); } Ok(weights) } /// Factory for creating FTS index attachments. /// /// Implements the `IndexMethod` trait to integrate with turso's index method system. /// When a user creates an FTS index with `CREATE INDEX ... USING fts (...)`, /// this factory creates an `FtsIndexAttachment` with the specified configuration. #[derive(Debug)] pub struct FtsIndexMethod; impl IndexMethod for FtsIndexMethod { fn attach(&self, cfg: &IndexMethodConfiguration) -> Result> { let attachment = FtsIndexAttachment::new(cfg.clone())?; Ok(Arc::new(attachment)) } } /// Cached FTS directory shared across cursors to avoid expensive catalog reloads. /// /// Contains a `HybridBTreeDirectory` with its catalog already loaded from the BTree. /// Only the directory is cached, not the Tantivy Index/Reader, because each cursor /// needs its own Index instance to handle writes correctly. pub struct CachedFtsDirectory { directory: HybridBTreeDirectory, } impl std::fmt::Debug for CachedFtsDirectory { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CachedFtsDirectory") .field("directory", &"HybridBTreeDirectory") .finish() } } /// FTS index attachment that holds configuration and creates cursors for queries. /// /// Created by `FtsIndexMethod::attach()` and implements `IndexMethodAttachment`. /// Stores the Tantivy schema, field mappings, query patterns, and a shared /// directory cache to optimize repeated queries. #[derive(Debug)] pub struct FtsIndexAttachment { /// Internal configuration cfg: IndexMethodConfiguration, /// Tantivy schema for the FTS index schema: Schema, /// Tantivy field for the rowid column rowid_field: Field, /// Schema fields for each indexed text column text_fields: Vec<(IndexColumn, Field)>, /// Parsed query patterns for FTS queries patterns: Vec
, sql: String) { let name = normalize_ident(view.name()); // Add to tables (so it appears as a regular table) self.tables.insert(name.clone(), table); // Track that this is a materialized view self.materialized_view_names.insert(name.clone()); self.materialized_view_sql.insert(name.clone(), sql); // Store the incremental view (DBSP circuit) self.incremental_views .insert(name, Arc::new(Mutex::new(view))); } pub fn get_materialized_view(&self, name: &str) -> Option>> { let name = normalize_ident(name); self.incremental_views.get(&name).cloned() } /// Check if DBSP state table exists with the current version pub fn has_compatible_dbsp_state_table(&self, view_name: &str) -> bool { use crate::incremental::compiler::DBSP_CIRCUIT_VERSION; let view_name = normalize_ident(view_name); let expected_table_name = format!("{DBSP_TABLE_PREFIX}{DBSP_CIRCUIT_VERSION}_{view_name}"); // Check if a table with the expected versioned name exists self.tables.contains_key(&expected_table_name) } pub fn is_materialized_view(&self, name: &str) -> bool { let name = normalize_ident(name); self.materialized_view_names.contains(&name) } /// Check if a table has any incompatible dependent materialized views pub fn has_incompatible_dependent_views(&self, table_name: &str) -> Vec { let table_name = normalize_ident(table_name); // Get all materialized views that depend on this table let dependent_views = self .table_to_materialized_views .get(&table_name) .cloned() .unwrap_or_default(); // Filter to only incompatible views dependent_views .into_iter() .filter(|view_name| self.incompatible_views.contains(view_name)) .collect() } pub fn remove_view(&mut self, name: &str) -> Result<()> { let name = normalize_ident(name); if self.views.contains_key(&name) { self.views.remove(&name); Ok(()) } else if self.materialized_view_names.contains(&name) { // Remove from tables self.tables.remove(&name); // Remove DBSP state table and its indexes from in-memory schema use crate::incremental::compiler::DBSP_CIRCUIT_VERSION; let dbsp_table_name = format!("{DBSP_TABLE_PREFIX}{DBSP_CIRCUIT_VERSION}_{name}"); self.tables.remove(&dbsp_table_name); self.remove_indices_for_table(&dbsp_table_name); // Remove from materialized view tracking self.materialized_view_names.remove(&name); self.materialized_view_sql.remove(&name); self.incremental_views.remove(&name); // Remove from table_to_materialized_views dependencies for views in self.table_to_materialized_views.values_mut() { views.retain(|v| v != &name); } Ok(()) } else { Err(crate::LimboError::ParseError(format!( "no such view: {name}" ))) } } /// Register that a materialized view depends on a table pub fn add_materialized_view_dependency(&mut self, table_name: &str, view_name: &str) { let table_name = normalize_ident(table_name); let view_name = normalize_ident(view_name); self.table_to_materialized_views .entry(table_name) .or_default() .push(view_name); } /// Get all materialized views that depend on a given table pub fn get_dependent_materialized_views(&self, table_name: &str) -> Vec { if self.table_to_materialized_views.is_empty() { return Vec::new(); } let table_name = normalize_ident(table_name); self.table_to_materialized_views .get(&table_name) .cloned() .unwrap_or_default() } /// Add a regular (non-materialized) view pub fn add_view(&mut self, view: View) -> Result<()> { self.check_object_name_conflict(&view.name)?; let name = normalize_ident(&view.name); self.views.insert(name, Arc::new(view)); Ok(()) } /// Get a regular view by name pub fn get_view(&self, name: &str) -> Option> { let name = normalize_ident(name); self.views.get(&name).cloned() } pub fn add_trigger(&mut self, trigger: Trigger, table_name: &str) -> Result<()> { self.check_object_name_conflict(&trigger.name)?; let table_name = normalize_ident(table_name); // See [Schema::add_index] for why we push to the front of the deque. self.triggers .entry(table_name) .or_default() .push_front(Arc::new(trigger)); Ok(()) } pub fn remove_trigger(&mut self, name: &str) -> Result<()> { let name = normalize_ident(name); let mut removed = false; for triggers_list in self.triggers.values_mut() { for i in 0..triggers_list.len() { let trigger = &triggers_list[i]; if normalize_ident(&trigger.name) == name { removed = true; triggers_list.remove(i); break; } } if removed { break; } } if !removed { return Err(crate::LimboError::ParseError(format!( "no such trigger: {name}" ))); } Ok(()) } pub fn remove_triggers_for_table(&mut self, table_name: &str) { let table_name = normalize_ident(table_name); self.triggers.remove(&table_name); } pub fn get_trigger_for_table(&self, table_name: &str, name: &str) -> Option> { let table_name = normalize_ident(table_name); let name = normalize_ident(name); self.triggers .get(&table_name) .and_then(|triggers| triggers.iter().find(|t| t.name == name).cloned()) } pub fn get_triggers_for_table( &self, table_name: &str, ) -> impl Iterator> + Clone { let table_name = normalize_ident(table_name); self.triggers .get(&table_name) .map(|triggers| triggers.iter()) .unwrap_or_default() } pub fn get_trigger(&self, name: &str) -> Option> { let name = normalize_ident(name); self.triggers .values() .flatten() .find(|t| t.name == name) .cloned() } pub fn add_btree_table(&mut self, table: Arc) -> Result<()> { self.check_object_name_conflict(&table.name)?; let name = normalize_ident(&table.name); self.tables.insert(name, Table::BTree(table).into()); Ok(()) } pub fn add_virtual_table(&mut self, table: Arc) -> Result<()> { self.check_object_name_conflict(&table.name)?; let name = normalize_ident(&table.name); self.tables.insert(name, Table::Virtual(table).into()); Ok(()) } pub fn get_table(&self, name: &str) -> Option> { let name = normalize_ident(name); let name = if name.eq_ignore_ascii_case(SCHEMA_TABLE_NAME_ALT) { SCHEMA_TABLE_NAME } else { &name }; self.tables.get(name).cloned() } pub fn remove_table(&mut self, table_name: &str) { let name = normalize_ident(table_name); self.tables.remove(&name); self.analyze_stats.remove_table(&name); // If this was a materialized view, also clean up the metadata if self.materialized_view_names.remove(&name) { self.incremental_views.remove(&name); self.materialized_view_sql.remove(&name); } } pub fn get_btree_table(&self, name: &str) -> Option> { let name = normalize_ident(name); if let Some(table) = self.tables.get(&name) { table.btree() } else { None } } pub fn add_index(&mut self, index: Arc) -> Result<()> { self.check_object_name_conflict(&index.name)?; let table_name = normalize_ident(&index.table_name); // We must add the new index to the front of the deque, because SQLite stores index definitions as a linked list // where the newest parsed index entry is at the head of list. If we would add it to the back of a regular Vec for example, // then we would evaluate ON CONFLICT DO UPDATE clauses in the wrong index iteration order and UPDATE the wrong row. // Additionally, REPLACE indexes must go after all the non-REPLACE indexes so that // non-mutating conflict resolutions all happen before mutating ones, ensuring that // no half-committed state is left behind. let is_replace = index.on_conflict == Some(ResolveType::Replace); let indexes_for_table = self.indexes.entry(table_name).or_default(); if is_replace { // REPLACE indexes sort newest-first among themselves. let first_replace = indexes_for_table .iter() .position(|idx| idx.on_conflict == Some(ResolveType::Replace)); let pos = first_replace.unwrap_or(indexes_for_table.len()); indexes_for_table.insert(pos, index); } else { // Non-REPLACE indexes go at the front, newest first. indexes_for_table.push_front(index); } turso_debug_assert!( indexes_for_table .iter() .position(|idx| idx.on_conflict == Some(ResolveType::Replace)) .is_none_or(|first_replace| { indexes_for_table .iter() .skip(first_replace) .all(|idx| idx.on_conflict == Some(ResolveType::Replace)) }), "REPLACE indexes must form a contiguous suffix" ); Ok(()) } pub fn get_indices(&self, table_name: &str) -> impl Iterator> { let name = normalize_ident(table_name); self.indexes .get(&name) .map(|v| v.iter()) .unwrap_or_default() .filter(|i| !i.is_backing_btree_index()) } #[cfg(all(feature = "fts", not(target_family = "wasm")))] pub fn has_fts_index(&self, table_name: &str) -> bool { use crate::index_method::fts::FTS_INDEX_METHOD_NAME; self.get_indices(table_name).any(|idx| { idx.index_method .as_ref() .is_some_and(|m| m.definition().method_name == FTS_INDEX_METHOD_NAME) }) } pub fn get_index(&self, table_name: &str, index_name: &str) -> Option<&Arc> { let name = normalize_ident(table_name); self.indexes .get(&name)? .iter() .find(|index| index.name == index_name) } pub fn remove_indices_for_table(&mut self, table_name: &str) { let name = normalize_ident(table_name); self.indexes.remove(&name); self.analyze_stats.remove_table(&name); } pub fn remove_index(&mut self, idx: &Index) { let name = normalize_ident(&idx.table_name); self.indexes .get_mut(&name) .expect("Must have the index") .retain_mut(|other_idx| other_idx.name != idx.name); self.analyze_stats.remove_index(&name, &idx.name); } pub fn table_has_indexes(&self, table_name: &str) -> bool { let name = normalize_ident(table_name); self.has_indexes.contains(&name) } pub fn table_set_has_index(&mut self, table_name: &str) { self.has_indexes.insert(table_name.to_string()); } /// Update [Schema] by scanning the first root page (sqlite_schema) /// Returns Result> to allow async operation with external IO loop pub fn make_from_btree( &mut self, state: &mut MakeFromBtreeState, mv_cursor: Option>>, pager: &Arc, syms: &SymbolTable, ) -> Result> { let result = self.make_from_btree_internal(state, mv_cursor, pager, syms); if result.is_err() { state.cleanup(pager); } else if let Ok(IOResult::Done(..)) = result { turso_assert!( !state.read_tx_active, "make_from_btree must properly cleanup internal state in case of success" ); } result } fn make_from_btree_internal( &mut self, state: &mut MakeFromBtreeState, mv_cursor: Option>>, pager: &Arc, syms: &SymbolTable, ) -> Result> { loop { tracing::debug!("make_from_btree: state.phase={:?}", state.phase); match &state.phase { MakeFromBtreePhase::Init => { if mv_cursor.is_some() { return Err(crate::LimboError::ParseError( "MVCC is not supported for make_from_btree schema recovery".to_string(), )); } state.cursor = Some(BTreeCursor::new_table(Arc::clone(pager), 1, 10)); pager.begin_read_tx()?; state.read_tx_active = true; state.accumulators = Some(MakeFromBtreeAccumulators { from_sql_indexes: Vec::with_capacity(10), automatic_indices: HashMap::with_capacity_and_hasher(10, FxBuildHasher), dbsp_state_roots: HashMap::default(), dbsp_state_index_roots: HashMap::default(), materialized_view_info: HashMap::default(), }); state.phase = MakeFromBtreePhase::Rewinding; } MakeFromBtreePhase::Rewinding => { let cursor = state .cursor .as_mut() .expect("cursor must be initialized in Init phase"); return_if_io!(cursor.rewind()); state.phase = MakeFromBtreePhase::FetchingRecord; } MakeFromBtreePhase::FetchingRecord => { let cursor = state .cursor .as_mut() .expect("cursor must be initialized in Init phase"); let row = return_if_io!(cursor.record()); let Some(row) = row else { // EOF - finalize pager.end_read_tx(); state.read_tx_active = false; let acc = state .accumulators .take() .expect("accumulators must be initialized in Init phase"); self.populate_indices( syms, acc.from_sql_indexes, acc.automatic_indices, mv_cursor.is_some(), )?; self.populate_materialized_views( acc.materialized_view_info, acc.dbsp_state_roots, acc.dbsp_state_index_roots, )?; state.cursor = None; state.phase = MakeFromBtreePhase::Done; return Ok(IOResult::Done(())); }; // Process the row (no IO - CPU only) // sqlite schema table has 5 columns: type, name, tbl_name, rootpage, sql let ty_value = row.get_value(0)?; let ValueRef::Text(ty) = ty_value else { return Err(LimboError::ConversionError("Expected text value".into())); }; let ValueRef::Text(name) = row.get_value(1)? else { return Err(LimboError::ConversionError("Expected text value".into())); }; let table_name_value = row.get_value(2)?; let ValueRef::Text(table_name) = table_name_value else { return Err(LimboError::ConversionError("Expected text value".into())); }; let root_page_value = row.get_value(3)?; let ValueRef::Numeric(crate::numeric::Numeric::Integer(root_page)) = root_page_value else { return Err(LimboError::ConversionError("Expected integer value".into())); }; let sql_value = row.get_value(4)?; let sql_textref = match sql_value { ValueRef::Text(sql) => Some(sql), _ => None, }; let sql = sql_textref.map(|s| s.as_str()); let acc = state .accumulators .as_mut() .expect("accumulators must be initialized in Init phase"); self.handle_schema_row( &ty, &name, &table_name, root_page, sql, syms, &mut acc.from_sql_indexes, &mut acc.automatic_indices, &mut acc.dbsp_state_roots, &mut acc.dbsp_state_index_roots, &mut acc.materialized_view_info, )?; state.phase = MakeFromBtreePhase::Advancing; } MakeFromBtreePhase::Advancing => { let cursor = state .cursor .as_mut() .expect("cursor must be initialized in Init phase"); return_if_io!(cursor.next()); state.phase = MakeFromBtreePhase::FetchingRecord; } MakeFromBtreePhase::Done => { return Ok(IOResult::Done(())); } } } } /// Populate indices parsed from the schema. /// from_sql_indexes: indices explicitly created with CREATE INDEX /// automatic_indices: indices created automatically for primary key and unique constraints pub fn populate_indices( &mut self, syms: &SymbolTable, from_sql_indexes: Vec, automatic_indices: HashMap>, mvcc_enabled: bool, ) -> Result<()> { for unparsed_sql_from_index in from_sql_indexes { let table = self .get_btree_table(&unparsed_sql_from_index.table_name) .unwrap(); let index = Index::from_sql( syms, &unparsed_sql_from_index.sql, unparsed_sql_from_index.root_page, table.as_ref(), )?; if mvcc_enabled && index.index_method.is_some() { crate::bail_parse_error!("Custom index modules are not supported with MVCC"); } self.add_index(Arc::new(index))?; } for automatic_index in automatic_indices { // Autoindexes must be parsed in definition order. // The SQL statement parser enforces that the column definitions come first, and compounds are defined after that, // e.g. CREATE TABLE t (a, b, UNIQUE(a, b)), and you can't do something like CREATE TABLE t (a, b, UNIQUE(a, b), c); // Hence, we can process the singles first (unique_set.columns.len() == 1), and then the compounds (unique_set.columns.len() > 1). let table = self.get_btree_table(&automatic_index.0).unwrap(); let mut automatic_indexes = automatic_index.1; automatic_indexes.reverse(); // reverse so we can pop() without shifting array elements, while still processing in left-to-right order // we must process unique_sets in this exact order in order to emit automatic indices schema entries in the same order let mut pk_index_added = false; for unique_set in &table.unique_sets { if unique_set.is_primary_key { assert!(table.primary_key_columns.len() == unique_set.columns.len(), "trying to add a {}-column primary key index for table {}, but the table has {} primary key columns", unique_set.columns.len(), table.name, table.primary_key_columns.len()); // Add composite primary key index assert!( !pk_index_added, "trying to add a second primary key index for table {}", table.name ); pk_index_added = true; if unique_set.columns.len() == 1 { let col_name = &unique_set.columns.first().unwrap().0; let Some((_, column)) = table.get_column(col_name) else { return Err(LimboError::ParseError(format!( "Column {col_name} not found in table {}", table.name ))); }; if column.is_rowid_alias() { // rowid alias, no index needed continue; } } if let Some(index_entry) = automatic_indexes.pop() { self.add_index(Arc::new(Index::automatic_from_primary_key( table.as_ref(), index_entry, unique_set.columns.len(), unique_set.conflict_clause, )?))?; } else if mvcc_enabled { // In MVCC mode, automatic indices might not be fully populated yet during recovery // Skip creating this index - it will be added later when its schema row is processed continue; } else { return Err(LimboError::InternalError(format!( "Missing automatic index entry for primary key on table {}", table.name ))); } } else { // Add composite unique index let mut column_indices_and_sort_orders = Vec::with_capacity(unique_set.columns.len()); for (col_name, sort_order) in unique_set.columns.iter() { let Some((pos_in_table, _)) = table.get_column(col_name) else { return Err(crate::LimboError::ParseError(format!( "Column {} not found in table {}", col_name, table.name ))); }; column_indices_and_sort_orders.push((pos_in_table, *sort_order)); } if let Some(index_entry) = automatic_indexes.pop() { self.add_index(Arc::new(Index::automatic_from_unique( table.as_ref(), index_entry, column_indices_and_sort_orders, unique_set.conflict_clause, )?))?; } else if mvcc_enabled { // In MVCC mode, automatic indices might not be fully populated yet during recovery // Skip creating this index - it will be added later when its schema row is processed continue; } else { return Err(LimboError::InternalError(format!( "Missing automatic index entry for UNIQUE constraint on table {}", table.name ))); } } } // In MVCC mode during recovery, not all automatic index schema rows might be visible yet // during incremental schema reparsing, so we may have extra entries if !mvcc_enabled { assert!(automatic_indexes.is_empty(), "all automatic indexes parsed from sqlite_schema should have been consumed, but {} remain", automatic_indexes.len()); } } Ok(()) } /// Populate materialized views parsed from the schema. pub fn populate_materialized_views( &mut self, materialized_view_info: HashMap, dbsp_state_roots: HashMap, dbsp_state_index_roots: HashMap, ) -> Result<()> { for (view_name, (sql, main_root)) in materialized_view_info { // Look up the DBSP state root for this view // If missing, it means version mismatch - skip this view // Check if we have a compatible DBSP state root let dbsp_state_root = if let Some(&root) = dbsp_state_roots.get(&view_name) { root } else { tracing::warn!( "Materialized view '{}' has incompatible version or missing DBSP state table", view_name ); // Track this as an incompatible view self.incompatible_views.insert(view_name.clone()); // Use a dummy root page - the view won't be usable anyway 0 }; // Look up the DBSP state index root (may not exist for older schemas) let dbsp_state_index_root = dbsp_state_index_roots.get(&view_name).copied().unwrap_or(0); // Register the DBSP state index so integrity check can account for its pages. if dbsp_state_index_root > 0 && dbsp_state_root > 0 { use crate::incremental::compiler::DBSP_CIRCUIT_VERSION; use crate::incremental::operator::create_dbsp_state_index; let mut index = create_dbsp_state_index(dbsp_state_index_root); let dbsp_table_name = format!("{DBSP_TABLE_PREFIX}{DBSP_CIRCUIT_VERSION}_{view_name}"); index.name = format!("sqlite_autoindex_{dbsp_table_name}_1"); index.table_name = dbsp_table_name; if let Err(e) = self.add_index(std::sync::Arc::new(index)) { if !e.to_string().contains("already exists") { return Err(e); } } } // Create the IncrementalView with all root pages let incremental_view = IncrementalView::from_sql( &sql, self, main_root, dbsp_state_root, dbsp_state_index_root, )?; let referenced_tables = incremental_view.get_referenced_table_names(); // Create a BTreeTable for the materialized view let table = Arc::new(Table::BTree(Arc::new(BTreeTable { name: view_name.clone(), root_page: main_root, columns: incremental_view.column_schema.flat_columns(), primary_key_columns: Vec::new(), has_rowid: true, is_strict: false, has_autoincrement: false, foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, unique_sets: vec![], }))); // Only add to schema if compatible if !self.incompatible_views.contains(&view_name) { self.add_materialized_view(incremental_view, table, sql); } // Register dependencies regardless of compatibility for table_name in referenced_tables { self.add_materialized_view_dependency(&table_name, &view_name); } } Ok(()) } #[allow(clippy::too_many_arguments)] pub fn handle_schema_row( &mut self, ty: &str, name: &str, table_name: &str, root_page: i64, maybe_sql: Option<&str>, syms: &SymbolTable, from_sql_indexes: &mut Vec, automatic_indices: &mut HashMap>, dbsp_state_roots: &mut HashMap, dbsp_state_index_roots: &mut HashMap, materialized_view_info: &mut HashMap, ) -> Result<()> { match ty { "table" => { let sql = maybe_sql.expect("sql should be present for table"); let sql_bytes = sql.as_bytes(); if root_page == 0 && contains_ignore_ascii_case!(sql_bytes, b"create virtual") { // a virtual table is found in the sqlite_schema, but it's no // longer in the in-memory schema. We need to recreate it if // the module is loaded in the symbol table. let vtab = if let Some(vtab) = syms.vtabs.get(name) { vtab.clone() } else { let mod_name = module_name_from_sql(sql)?; crate::VirtualTable::table( Some(name), mod_name, module_args_from_sql(sql)?, syms, )? }; self.add_virtual_table(vtab)?; } else { let table = BTreeTable::from_sql(sql, root_page)?; // Check if this is a DBSP state table if table.name.starts_with(DBSP_TABLE_PREFIX) { // Extract version and view name from __turso_internal_dbsp_state_v_ let suffix = table.name.strip_prefix(DBSP_TABLE_PREFIX).unwrap(); // Parse version and view name (format: "_") if let Some(underscore_pos) = suffix.find('_') { let version_str = &suffix[..underscore_pos]; let view_name = &suffix[underscore_pos + 1..]; // Check version compatibility if let Ok(stored_version) = version_str.parse::() { use crate::incremental::compiler::DBSP_CIRCUIT_VERSION; if stored_version == DBSP_CIRCUIT_VERSION { // Version matches, store the root page dbsp_state_roots.insert(view_name.to_string(), root_page); } else { // Version mismatch - DO NOT insert into dbsp_state_roots // This will cause populate_materialized_views to skip this view tracing::warn!( "Skipping materialized view '{}' - has version {} but current version is {}. DROP and recreate the view to use it.", view_name, stored_version, DBSP_CIRCUIT_VERSION ); // We can't track incompatible views here since we're in handle_schema_row // which doesn't have mutable access to self } } } } let mut table = table; table.resolve_custom_type_affinities(self); self.add_btree_table(Arc::new(table))?; } } "index" => { match maybe_sql { Some(sql) => { from_sql_indexes.push(UnparsedFromSqlIndex { table_name: table_name.to_string(), root_page, sql: sql.to_string(), }); } None => { // Automatic index on primary key and/or unique constraint, e.g. // table|foo|foo|2|CREATE TABLE foo (a text PRIMARY KEY, b) // index|sqlite_autoindex_foo_1|foo|3| let index_name = name.to_string(); let table_name = table_name.to_string(); // Check if this is an index for a DBSP state table if table_name.starts_with(DBSP_TABLE_PREFIX) { // Extract version and view name from __turso_internal_dbsp_state_v_ let suffix = table_name.strip_prefix(DBSP_TABLE_PREFIX).unwrap(); // Parse version and view name (format: "_") if let Some(underscore_pos) = suffix.find('_') { let version_str = &suffix[..underscore_pos]; let view_name = &suffix[underscore_pos + 1..]; // Only store index root if version matches if let Ok(stored_version) = version_str.parse::() { use crate::incremental::compiler::DBSP_CIRCUIT_VERSION; if stored_version == DBSP_CIRCUIT_VERSION { dbsp_state_index_roots .insert(view_name.to_string(), root_page); } } } } else { match automatic_indices.entry(table_name) { std::collections::hash_map::Entry::Vacant(e) => { e.insert(vec![(index_name, root_page)]); } std::collections::hash_map::Entry::Occupied(mut e) => { e.get_mut().push((index_name, root_page)); } } } } } } "view" => { use crate::schema::View; use turso_parser::ast::{Cmd, Stmt}; use turso_parser::parser::Parser; let sql = maybe_sql.expect("sql should be present for view"); let view_name = name.to_string(); // Parse the SQL to determine if it's a regular or materialized view let mut parser = Parser::new(sql.as_bytes()); if let Ok(Some(Cmd::Stmt(stmt))) = parser.next_cmd() { match stmt { Stmt::CreateMaterializedView { .. } => { // Store materialized view info for later creation // We'll handle reuse logic and create the actual IncrementalView // in a later pass when we have both the main root page and DBSP state root materialized_view_info .insert(view_name.clone(), (sql.to_string(), root_page)); // Mark the existing view for potential reuse if self.incremental_views.contains_key(&view_name) { // We'll check for reuse in the third pass } } Stmt::CreateView { view_name: _, columns: column_names, select, .. } => { crate::util::validate_select_for_unsupported_features(&select)?; // Extract actual columns from the SELECT statement let view_column_schema = crate::util::extract_view_columns(&select, self)?; // If column names were provided in CREATE VIEW (col1, col2, ...), // use them to rename the columns let mut final_columns = view_column_schema.flat_columns(); for (i, indexed_col) in column_names.iter().enumerate() { if let Some(col) = final_columns.get_mut(i) { col.name = Some(indexed_col.col_name.to_string()); } } // Create regular view let view = View::new(name.to_string(), sql.to_string(), select, final_columns); self.add_view(view)?; } _ => {} } } } "trigger" => { use turso_parser::ast::{Cmd, Stmt}; use turso_parser::parser::Parser; let sql = maybe_sql.expect("sql should be present for trigger"); let trigger_name = name.to_string(); let mut parser = Parser::new(sql.as_bytes()); let Ok(Some(Cmd::Stmt(Stmt::CreateTrigger { temporary, if_not_exists: _, trigger_name: _, time, event, tbl_name, for_each_row, when_clause, commands, }))) = parser.next_cmd() else { return Err(crate::LimboError::ParseError(format!( "invalid trigger sql: {sql}" ))); }; self.add_trigger( Trigger::new( trigger_name, sql.to_string(), tbl_name.name.to_string(), time, event, for_each_row, when_clause.map(|e| *e), commands, temporary, ), tbl_name.name.as_str(), )?; } // Types are stored in sqlite_turso_types, not sqlite_schema _ => {} }; Ok(()) } /// Compute all resolved FKs *referencing* `table_name` (arg: `table_name` is the parent). /// Each item contains the child table, normalized columns/positions, and the parent lookup /// strategy (rowid vs. UNIQUE index or PK). pub fn resolved_fks_referencing(&self, table_name: &str) -> Result> { let fk_mismatch_err = |child: &str, parent: &str| -> crate::LimboError { crate::LimboError::ForeignKeyConstraint(format!( "foreign key mismatch - \"{child}\" referencing \"{parent}\"" )) }; let target = normalize_ident(table_name); let mut out = Vec::with_capacity(4); // arbitrary estimate let parent_tbl = self .get_btree_table(&target) .ok_or_else(|| fk_mismatch_err("", &target))?; // Precompute helper to find parent unique index, if it's not the rowid let find_parent_unique = |cols: &Vec| -> Option> { self.get_indices(&parent_tbl.name) .find(|idx| { idx.unique && idx.columns.len() == cols.len() && idx .columns .iter() .zip(cols.iter()) .all(|(ic, pc)| ic.name.eq_ignore_ascii_case(pc)) }) .cloned() }; for t in self.tables.values() { let Some(child) = t.btree() else { continue; }; for fk in &child.foreign_keys { if !fk.parent_table.eq_ignore_ascii_case(&target) { continue; } if fk.child_columns.is_empty() { // SQLite requires an explicit child column list unless the table has a single-column PK that return Err(fk_mismatch_err(&child.name, &parent_tbl.name)); } let child_cols: Vec = fk.child_columns.clone(); let mut child_pos = Vec::with_capacity(child_cols.len()); for cname in &child_cols { let (i, _) = child .get_column(cname) .ok_or_else(|| fk_mismatch_err(&child.name, &parent_tbl.name))?; child_pos.push(i); } let parent_cols: Vec = if fk.parent_columns.is_empty() { if !parent_tbl.primary_key_columns.is_empty() { parent_tbl .primary_key_columns .iter() .map(|(col, _)| col) .cloned() .collect() } else { return Err(fk_mismatch_err(&child.name, &parent_tbl.name)); } } else { fk.parent_columns.clone() }; // Same length required if parent_cols.len() != child_cols.len() { return Err(fk_mismatch_err(&child.name, &parent_tbl.name)); } let mut parent_pos = Vec::with_capacity(parent_cols.len()); for pc in &parent_cols { let pos = parent_tbl.get_column(pc).map(|(i, _)| i).or_else(|| { ROWID_STRS .iter() .any(|s| pc.eq_ignore_ascii_case(s)) .then_some(0) }); let Some(p) = pos else { return Err(fk_mismatch_err(&child.name, &parent_tbl.name)); }; parent_pos.push(p); } // Determine if the FK's parent key is the ROWID or a rowid alias. let parent_uses_rowid = if parent_cols.len() == 1 { let pc = &parent_cols[0]; ROWID_STRS.iter().any(|&r| r.eq_ignore_ascii_case(pc)) || parent_tbl.columns.iter().any(|c| { c.is_rowid_alias() && c.name .as_deref() .is_some_and(|n| n.eq_ignore_ascii_case(pc)) }) } else { false }; // If not rowid, there must be a non-partial UNIQUE exactly on parent_cols let parent_unique_index = if parent_uses_rowid { None } else { find_parent_unique(&parent_cols) }; fk.validate()?; out.push(ResolvedFkRef { child_table: Arc::clone(&child), fk: Arc::clone(fk), child_cols, child_pos, parent_pos, parent_uses_rowid, parent_unique_index, }); } } Ok(out) } /// Compute all resolved FKs *declared by* `child_table` pub fn resolved_fks_for_child(&self, child_table: &str) -> crate::Result> { let fk_mismatch_err = |child: &str, parent: &str| -> crate::LimboError { crate::LimboError::ForeignKeyConstraint(format!( "foreign key mismatch - \"{child}\" referencing \"{parent}\"" )) }; let child_name = normalize_ident(child_table); let child = self .get_btree_table(&child_name) .ok_or_else(|| fk_mismatch_err(&child_name, ""))?; let mut out = Vec::with_capacity(child.foreign_keys.len()); for fk in &child.foreign_keys { let parent_name = normalize_ident(&fk.parent_table); let parent_tbl = self .get_btree_table(&parent_name) .ok_or_else(|| fk_mismatch_err(&child.name, &parent_name))?; let child_cols: Vec = fk.child_columns.clone(); if child_cols.is_empty() { return Err(fk_mismatch_err(&child.name, &parent_tbl.name)); } // Child positions exist let mut child_pos = Vec::with_capacity(child_cols.len()); for cname in &child_cols { let (i, _) = child .get_column(cname) .ok_or_else(|| fk_mismatch_err(&child.name, &parent_tbl.name))?; child_pos.push(i); } let parent_cols: Vec = if fk.parent_columns.is_empty() { if !parent_tbl.primary_key_columns.is_empty() { parent_tbl .primary_key_columns .iter() .map(|(col, _)| col) .cloned() .collect() } else { return Err(fk_mismatch_err(&child.name, &parent_tbl.name)); } } else { fk.parent_columns.clone() }; if parent_cols.len() != child_cols.len() { return Err(fk_mismatch_err(&child.name, &parent_tbl.name)); } // Parent positions exist, or rowid sentinel let mut parent_pos = Vec::with_capacity(parent_cols.len()); for pc in &parent_cols { let pos = parent_tbl.get_column(pc).map(|(i, _)| i).or_else(|| { ROWID_STRS .iter() .any(|&r| r.eq_ignore_ascii_case(pc)) .then_some(0) }); let Some(p) = pos else { return Err(fk_mismatch_err(&child.name, &parent_tbl.name)); }; parent_pos.push(p); } let parent_uses_rowid = parent_cols.len().eq(&1) && { let c = parent_cols[0].as_str(); ROWID_STRS.iter().any(|&r| r.eq_ignore_ascii_case(c)) || parent_tbl.columns.iter().any(|col| { col.is_rowid_alias() && col .name .as_deref() .is_some_and(|n| n.eq_ignore_ascii_case(c)) }) }; // Must be PK or a non-partial UNIQUE on exactly those columns. let parent_unique_index = if parent_uses_rowid { None } else { self.get_indices(&parent_tbl.name) .find(|idx| { idx.unique && idx.where_clause.is_none() && idx.columns.len() == parent_cols.len() && idx .columns .iter() .zip(parent_cols.iter()) .all(|(ic, pc)| ic.name.eq_ignore_ascii_case(pc)) }) .cloned() .ok_or_else(|| fk_mismatch_err(&child.name, &parent_tbl.name))? .into() }; fk.validate()?; out.push(ResolvedFkRef { child_table: Arc::clone(&child), fk: Arc::clone(fk), child_cols, child_pos, parent_pos, parent_uses_rowid, parent_unique_index, }); } Ok(out) } /// Returns if any table declares a FOREIGN KEY whose parent is `table_name`. pub fn any_resolved_fks_referencing(&self, table_name: &str) -> bool { self.tables.values().any(|t| { let Some(bt) = t.btree() else { return false; }; bt.foreign_keys .iter() .any(|fk| fk.parent_table == table_name) }) } /// Returns true if `table_name` declares any FOREIGN KEYs pub fn has_child_fks(&self, table_name: &str) -> bool { self.get_table(table_name) .and_then(|t| t.btree()) .is_some_and(|t| !t.foreign_keys.is_empty()) } fn check_object_name_conflict(&self, name: &str) -> Result<()> { if let Some(object_type) = self.get_object_type(name) { let type_str = match object_type { SchemaObjectType::Table => "table", SchemaObjectType::View => "view", SchemaObjectType::Index => "index", }; return Err(crate::LimboError::ParseError(format!( "{type_str} \"{name}\" already exists" ))); } Ok(()) } /// Returns the type of schema object with the given name, if one exists. /// Checks tables, views, and indexes. pub fn get_object_type(&self, name: &str) -> Option { let normalized_name = normalize_ident(name); if self.tables.contains_key(&normalized_name) { return Some(SchemaObjectType::Table); } if self.views.contains_key(&normalized_name) { return Some(SchemaObjectType::View); } for index_list in self.indexes.values() { if index_list.iter().any(|i| i.name.eq_ignore_ascii_case(name)) { return Some(SchemaObjectType::Index); } } None } } impl Clone for Schema { /// Cloning a `Schema` requires deep cloning of all internal tables and indexes, even though they are wrapped in `Arc`. /// Simply copying the `Arc` pointers would result in multiple `Schema` instances sharing the same underlying tables and indexes, /// which could lead to panics or data races if any instance attempts to modify them. /// To ensure each `Schema` is independent and safe to modify, we clone the underlying data for all tables and indexes. fn clone(&self) -> Self { let tables = self .tables .iter() .map(|(name, table)| match table.deref() { Table::BTree(table) => { let table = Arc::deref(table); ( name.clone(), Arc::new(Table::BTree(Arc::new(table.clone()))), ) } Table::Virtual(table) => { let table = Arc::deref(table); ( name.clone(), Arc::new(Table::Virtual(Arc::new(table.clone()))), ) } Table::FromClauseSubquery(from_clause_subquery) => ( name.clone(), Arc::new(Table::FromClauseSubquery(Arc::new( (**from_clause_subquery).clone(), ))), ), }) .collect(); let indexes = self .indexes .iter() .map(|(name, indexes)| { let indexes = indexes .iter() .map(|index| Arc::new((**index).clone())) .collect(); (name.clone(), indexes) }) .collect(); let materialized_view_names = self.materialized_view_names.clone(); let materialized_view_sql = self.materialized_view_sql.clone(); let incremental_views = self .incremental_views .iter() .map(|(name, view)| (name.clone(), view.clone())) .collect(); let views = self .views .iter() .map(|(name, view)| (name.clone(), Arc::new((**view).clone()))) .collect(); let triggers = self .triggers .iter() .map(|(table_name, triggers)| { ( table_name.clone(), triggers.iter().map(|t| Arc::new((**t).clone())).collect(), ) }) .collect(); let incompatible_views = self.incompatible_views.clone(); Self { tables, materialized_view_names, materialized_view_sql, incremental_views, views, triggers, indexes, has_indexes: self.has_indexes.clone(), schema_version: self.schema_version, analyze_stats: self.analyze_stats.clone(), table_to_materialized_views: self.table_to_materialized_views.clone(), incompatible_views, dropped_root_pages: self.dropped_root_pages.clone(), type_registry: self.type_registry.clone(), } } } #[derive(Clone, Debug)] pub enum Table { BTree(Arc), Virtual(Arc), FromClauseSubquery(Arc), } impl Table { pub fn get_root_page(&self) -> crate::Result { match self { Table::BTree(table) => Ok(table.root_page), Table::Virtual(_) => Err(crate::LimboError::InternalError( "Virtual tables do not have a root page".to_string(), )), Table::FromClauseSubquery(_) => Err(crate::LimboError::InternalError( "FROM clause subqueries do not have a root page".to_string(), )), } } pub fn get_name(&self) -> &str { match self { Self::BTree(table) => &table.name, Self::Virtual(table) => &table.name, Self::FromClauseSubquery(from_clause_subquery) => &from_clause_subquery.name, } } pub fn get_column_at(&self, index: usize) -> Option<&Column> { match self { Self::BTree(table) => table.columns.get(index), Self::Virtual(table) => table.columns.get(index), Self::FromClauseSubquery(from_clause_subquery) => { from_clause_subquery.columns.get(index) } } } /// Returns the column position and column for a given column name. pub fn get_column_by_name(&self, name: &str) -> Option<(usize, &Column)> { match self { Self::BTree(table) => table.get_column(name), Self::Virtual(table) => table.columns.iter().enumerate().find(|(_, col)| { col.name .as_ref() .is_some_and(|n| n.eq_ignore_ascii_case(name)) }), Self::FromClauseSubquery(from_clause_subquery) => from_clause_subquery .columns .iter() .enumerate() .find(|(_, col)| { col.name .as_ref() .is_some_and(|n| n.eq_ignore_ascii_case(name)) }), } } pub fn columns(&self) -> &Vec { match self { Self::BTree(table) => &table.columns, Self::Virtual(table) => &table.columns, Self::FromClauseSubquery(from_clause_subquery) => &from_clause_subquery.columns, } } pub fn is_strict(&self) -> bool { match self { Self::BTree(table) => table.is_strict, Self::Virtual(_) => false, Self::FromClauseSubquery(_) => false, } } pub fn btree(&self) -> Option> { match self { Self::BTree(table) => Some(table.clone()), Self::Virtual(_) => None, Self::FromClauseSubquery(_) => None, } } pub fn btree_mut(&mut self) -> Option<&mut Arc> { match self { Self::BTree(table) => Some(table), Self::Virtual(_) => None, Self::FromClauseSubquery(_) => None, } } pub fn virtual_table(&self) -> Option> { match self { Self::Virtual(table) => Some(table.clone()), _ => None, } } } impl PartialEq for Table { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::BTree(a), Self::BTree(b)) => Arc::ptr_eq(a, b), (Self::Virtual(a), Self::Virtual(b)) => Arc::ptr_eq(a, b), _ => false, } } } #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct UniqueSet { pub columns: Vec<(String, SortOrder)>, pub is_primary_key: bool, pub conflict_clause: Option, } #[derive(Clone, Debug)] pub struct CheckConstraint { /// Optional constraint name pub name: Option, /// CHECK expression pub expr: ast::Expr, /// Column name if this is a column-level CHECK constraint (defined inline with the column). /// None if this is a table-level CHECK constraint. pub column: Option, } impl CheckConstraint { pub fn new(name: Option<&ast::Name>, expr: &ast::Expr, column: Option<&str>) -> Self { Self { name: name.map(|n| n.as_str().to_string()), expr: expr.clone(), column: column.map(|s| s.to_string()), } } /// Returns the SQL representation of this CHECK constraint (e.g. `CHECK(x > 0)`). pub fn sql(&self) -> String { format!("CHECK({})", self.expr) } } #[derive(Clone, Debug)] pub struct BTreeTable { pub root_page: i64, pub name: String, pub primary_key_columns: Vec<(String, SortOrder)>, pub columns: Vec, pub has_rowid: bool, pub is_strict: bool, pub has_autoincrement: bool, pub unique_sets: Vec, pub foreign_keys: Vec>, pub check_constraints: Vec, /// ON CONFLICT clause for the INTEGER PRIMARY KEY constraint. /// Stored here because rowid-alias PKs have their UniqueSet removed. pub rowid_alias_conflict_clause: Option, } impl BTreeTable { /// Create a table reference for TypeCheck where custom type columns have /// their `ty_str` replaced with the base type name. This ensures TypeCheck /// validates the encoded value against the correct base type (e.g., BLOB) /// rather than accepting any STRICT type via the wildcard arm. pub fn type_check_table_ref(table: &Arc, schema: &Schema) -> Arc { let has_custom = table .columns .iter() .any(|c| c.is_array() || schema.get_type_def(&c.ty_str, table.is_strict).is_some()); if !has_custom { return Arc::clone(table); } let mut modified = (**table).clone(); for col in &mut modified.columns { if col.is_array() { // Arrays are stored as record-format blobs. col.ty_str = "BLOB".to_string(); } else if let Some(type_def) = schema.get_type_def(&col.ty_str, table.is_strict) { col.ty_str = type_def.base.to_uppercase(); } } Arc::new(modified) } /// Create a table ref for pre-encode TypeCheck that validates user input /// against the type's declared `value` input type (or base if not declared). /// For UPDATE, `only_columns` limits which columns are checked — non-SET /// columns hold encoded values and must be skipped (set to ANY). pub fn input_type_check_table_ref( table: &Arc, schema: &Schema, only_columns: Option<&std::collections::HashSet>, ) -> Arc { let has_custom = table .columns .iter() .any(|c| c.is_array() || schema.get_type_def(&c.ty_str, table.is_strict).is_some()); if !has_custom { return Arc::clone(table); } let mut modified = (**table).clone(); for (i, col) in modified.columns.iter_mut().enumerate() { if let Some(only) = only_columns { if !only.contains(&i) { // Non-SET column in UPDATE: holds encoded value, skip check col.ty_str = "ANY".to_string(); continue; } } if col.is_array() { // Pre-encode: user input can be text ('[1,2]') or blob (ARRAY[]), // so accept ANY here; the encoder handles conversion. col.ty_str = "ANY".to_string(); } else if let Some(type_def) = schema.get_type_def(&col.ty_str, table.is_strict) { col.ty_str = type_def.value_input_type().to_uppercase(); } } Arc::new(modified) } /// Override column type metadata for custom type columns so that /// SQLite's name-based type/affinity rules use the BASE type /// instead of the custom type name (e.g. "doubled" contains "DOUB" /// which would incorrectly map to REAL instead of INTEGER). pub fn resolve_custom_type_affinities(&mut self, schema: &Schema) { if !self.is_strict { return; } for col in &mut self.columns { if col.is_array() { // Arrays are stored as record-format blobs regardless of element type. col.set_ty(Type::Blob); col.set_base_affinity(Affinity::Blob); continue; } if let Some(type_def) = schema.get_type_def_unchecked(&col.ty_str) { let (base_ty, _) = type_from_name(&type_def.base); col.set_ty(base_ty); col.set_base_affinity(Affinity::affinity(&type_def.base)); } } } pub fn get_rowid_alias_column(&self) -> Option<(usize, &Column)> { self.columns .iter() .enumerate() .find(|(_, column)| column.is_rowid_alias()) } /// Returns the column position and column for a given column name. /// Returns None if the column name is not found. /// E.g. if table is CREATE TABLE t (a, b, c) /// then get_column("b") returns (1, &Column { .. }) pub fn get_column(&self, name: &str) -> Option<(usize, &Column)> { self.columns.iter().enumerate().find(|(_, column)| { column .name .as_ref() .is_some_and(|n| n.eq_ignore_ascii_case(name)) }) } pub fn from_sql(sql: &str, root_page: i64) -> Result { let mut parser = Parser::new(sql.as_bytes()); let cmd = parser.next_cmd()?; match cmd { Some(Cmd::Stmt(Stmt::CreateTable { tbl_name, body, .. })) => { create_table(tbl_name.name.as_str(), &body, root_page) } _ => unreachable!("Expected CREATE TABLE statement"), } } /// Reconstruct the SQL for the table. /// FIXME: this makes us incompatible with SQLite since sqlite stores the user-provided SQL as is in /// `sqlite_schema.sql` /// For example, if a user creates a table like: `CREATE TABLE t (x)`, we store it as /// `CREATE TABLE t (x)`, whereas sqlite stores it with the original extra whitespace. pub fn to_sql(&self) -> String { let mut sql = format!("CREATE TABLE {} (", quote_ident(&self.name)); let needs_pk_inline = self.primary_key_columns.len() == 1; // Add columns for (i, column) in self.columns.iter().enumerate() { if i > 0 { sql.push_str(", "); } // we need to wrap the column name in square brackets if it contains special characters let column_name = column.name.as_ref().expect("column name is None"); if identifier_contains_special_chars(column_name) { sql.push('['); sql.push_str(column_name); sql.push(']'); } else { sql.push_str(column_name); } if !column.ty_str.is_empty() { sql.push(' '); sql.push_str(&column.ty_str); if column.is_array() { sql.push_str("[]"); } } if column.notnull() { sql.push_str(" NOT NULL"); } if column.unique() { sql.push_str(" UNIQUE"); } if needs_pk_inline && column.primary_key() { sql.push_str(" PRIMARY KEY"); } if let Some(default) = &column.default { sql.push_str(" DEFAULT "); sql.push_str(&default.to_string()); } if let Some(generated) = &column.generated { sql.push_str(" AS ("); sql.push_str(&generated.to_string()); sql.push(')'); } // Add column-level CHECK constraints inline for check_constraint in &self.check_constraints { if check_constraint.column.as_deref() == Some(column_name) { sql.push(' '); if let Some(name) = &check_constraint.name { sql.push_str("CONSTRAINT "); sql.push_str(&Name::exact(name.clone()).as_ident()); sql.push(' '); } sql.push_str(&check_constraint.sql()); } } } let has_table_pk = !self.primary_key_columns.is_empty(); // Add table-level PRIMARY KEY constraint if exists if !needs_pk_inline && has_table_pk { sql.push_str(", PRIMARY KEY ("); for (i, col) in self.primary_key_columns.iter().enumerate() { if i > 0 { sql.push_str(", "); } sql.push_str(&col.0); } sql.push(')'); } for fk in &self.foreign_keys { sql.push_str(", FOREIGN KEY ("); for (i, col) in fk.child_columns.iter().enumerate() { if i > 0 { sql.push_str(", "); } sql.push_str(col); } sql.push_str(") REFERENCES "); sql.push_str(&fk.parent_table); sql.push('('); for (i, col) in fk.parent_columns.iter().enumerate() { if i > 0 { sql.push_str(", "); } sql.push_str(col); } sql.push(')'); // Add ON DELETE/UPDATE actions, NoAction is default so just make empty in that case if fk.on_delete != RefAct::NoAction { sql.push_str(" ON DELETE "); sql.push_str(match fk.on_delete { RefAct::SetNull => "SET NULL", RefAct::SetDefault => "SET DEFAULT", RefAct::Cascade => "CASCADE", RefAct::Restrict => "RESTRICT", _ => "", }); } if fk.on_update != RefAct::NoAction { sql.push_str(" ON UPDATE "); sql.push_str(match fk.on_update { RefAct::SetNull => "SET NULL", RefAct::SetDefault => "SET DEFAULT", RefAct::Cascade => "CASCADE", RefAct::Restrict => "RESTRICT", _ => "", }); } if fk.deferred { sql.push_str(" DEFERRABLE INITIALLY DEFERRED"); } } // Add table-level CHECK constraints (column-level ones were emitted inline above) for check_constraint in &self.check_constraints { if check_constraint.column.is_some() { continue; } sql.push_str(", "); if let Some(name) = &check_constraint.name { sql.push_str("CONSTRAINT "); sql.push_str(&Name::exact(name.clone()).as_ident()); sql.push(' '); } sql.push_str(&check_constraint.sql()); } // Add table-level UNIQUE constraints for unique_set in &self.unique_sets { // Skip primary key (handled above) if unique_set.is_primary_key { continue; } // Skip single-column unique constraints that were already emitted inline if unique_set.columns.len() == 1 { let col_name = &unique_set.columns[0].0; if let Some((_, col)) = self.get_column(col_name) { if col.unique() { continue; } } } sql.push_str(", UNIQUE ("); for (i, (col_name, _)) in unique_set.columns.iter().enumerate() { if i > 0 { sql.push_str(", "); } if identifier_contains_special_chars(col_name) { sql.push('['); sql.push_str(col_name); sql.push(']'); } else { sql.push_str(col_name); } } sql.push(')'); } sql.push(')'); // Add STRICT keyword if this is a STRICT table if self.is_strict { sql.push_str(" STRICT"); } sql } pub fn column_collations(&self) -> Vec { self.columns .iter() .map(|column| column.collation()) .collect() } } fn identifier_contains_special_chars(name: &str) -> bool { name.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') } #[derive(Debug, Default, Clone, Copy)] pub struct PseudoCursorType { pub column_count: usize, } impl PseudoCursorType { pub fn new() -> Self { Self { column_count: 0 } } pub fn new_with_columns(columns: impl AsRef<[Column]>) -> Self { Self { column_count: columns.as_ref().len(), } } } /// A derived table from a FROM clause subquery. #[derive(Debug, Clone)] pub struct FromClauseSubquery { /// The name of the derived table; uses the alias if available. pub name: String, /// The query plan for the derived table. Can be either a simple SelectPlan /// or a compound select (UNION/INTERSECT/EXCEPT). pub plan: Box, /// The columns of the derived table. pub columns: Vec, /// The start register for the result columns of the derived table; /// must be set before data is read from it. pub result_columns_start_reg: Option, /// The table cursor backing a materialized EphemeralTable representation of /// this subquery, if one was emitted. pub materialized_cursor_id: Option, /// CTE-specific materialization metadata, when this FROM-subquery is a CTE /// reference rather than an inline derived table. pub cte: Option, } #[derive(Debug, Clone, Copy)] pub struct FromClauseSubqueryCteMetadata { /// Identity shared by all references to the same CTE definition. pub id: usize, /// True when this CTE is referenced more than once inside the enclosing /// query tree and therefore must be materialized once and shared. pub shared_materialization: bool, /// True for explicit WITH ... AS MATERIALIZED. pub materialize_hint: bool, } impl FromClauseSubquery { pub fn cte_id(&self) -> Option { self.cte.map(|cte| cte.id) } pub fn materialize_hint(&self) -> bool { self.cte.is_some_and(|cte| cte.materialize_hint) } pub fn shared_materialization(&self) -> bool { self.cte.is_some_and(|cte| cte.shared_materialization) } pub fn set_shared_materialization(&mut self, shared: bool) { if let Some(cte) = &mut self.cte { cte.shared_materialization = shared; } } /// Shared CTE references and explicit MATERIALIZED hints both force a /// table-backed materialization that can be scanned or probed later. pub fn requires_table_materialization(&self) -> bool { self.shared_materialization() || self.materialize_hint() } /// Only simple single-reference SELECT subqueries can safely use their /// synthesized seek index as the storage target directly. Compound /// subqueries still need table-backed storage so their set-operation /// semantics are preserved before any later SEARCH shape is chosen. pub fn supports_direct_index_materialization(&self) -> bool { matches!(self.plan.as_ref(), Plan::Select(_)) && !self.requires_table_materialization() } } pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> Result { let table_name = normalize_ident(tbl_name); trace!("Creating table {}", table_name); let mut has_rowid = true; let mut has_autoincrement = false; let mut primary_key_columns = vec![]; let mut foreign_keys = vec![]; let mut check_constraints = vec![]; let mut cols = vec![]; let is_strict: bool; let mut unique_sets_columns: Vec = vec![]; let mut unique_sets_constraints: Vec = vec![]; match body { CreateTableBody::ColumnsAndConstraints { columns, constraints, options, } => { is_strict = options.contains_strict(); // we need to preserve order of unique sets definition // but also, we analyze constraints first in order to check PRIMARY KEY constraint and recognize rowid alias properly // that's why we maintain 2 unique_set sequences and merge them together in the end for c in constraints { if let ast::TableConstraint::PrimaryKey { columns, auto_increment, conflict_clause, } = &c.constraint { if !primary_key_columns.is_empty() { crate::bail_parse_error!( "table \"{}\" has more than one primary key", tbl_name ); } if *auto_increment { has_autoincrement = true; } for column in columns { let col_name = match column.expr.as_ref() { Expr::Id(id) => normalize_ident(id.as_str()), Expr::Literal(Literal::String(value)) => { value.trim_matches('\'').to_owned() } expr => { bail_parse_error!("unsupported primary key expression: {}", expr) } }; primary_key_columns .push((col_name, column.order.unwrap_or(SortOrder::Asc))); } unique_sets_constraints.push(UniqueSet { columns: primary_key_columns.clone(), is_primary_key: true, conflict_clause: *conflict_clause, }); } else if let ast::TableConstraint::Unique { columns, conflict_clause, } = &c.constraint { let mut unique_columns = Vec::with_capacity(columns.len()); for column in columns { match column.expr.as_ref() { Expr::Id(id) => unique_columns.push(( id.as_str().to_string(), column.order.unwrap_or(SortOrder::Asc), )), Expr::Literal(Literal::String(value)) => unique_columns.push(( value.trim_matches('\'').to_owned(), column.order.unwrap_or(SortOrder::Asc), )), expr => { bail_parse_error!("unsupported unique key expression: {}", expr) } } } let unique_set = UniqueSet { columns: unique_columns, is_primary_key: false, conflict_clause: *conflict_clause, }; unique_sets_constraints.push(unique_set); } else if let ast::TableConstraint::ForeignKey { columns, clause, defer_clause, } = &c.constraint { let child_columns: Vec = columns .iter() .map(|ic| normalize_ident(ic.col_name.as_str())) .collect(); // derive parent columns: explicit or default to parent PK let parent_table = normalize_ident(clause.tbl_name.as_str()); let parent_columns: Vec = clause .columns .iter() .map(|ic| normalize_ident(ic.col_name.as_str())) .collect(); // Only check arity if parent columns were explicitly listed if !parent_columns.is_empty() && child_columns.len() != parent_columns.len() { crate::bail_parse_error!( "foreign key on \"{}\" has {} child column(s) but {} parent column(s)", tbl_name, child_columns.len(), parent_columns.len() ); } // deferrable semantics let deferred = match defer_clause { Some(d) => { d.deferrable && matches!( d.init_deferred, Some(InitDeferredPred::InitiallyDeferred) ) } None => false, // NOT DEFERRABLE INITIALLY IMMEDIATE by default }; let fk = ForeignKey { parent_table, parent_columns, child_columns, on_delete: clause .args .iter() .find_map(|a| { if let ast::RefArg::OnDelete(x) = a { Some(*x) } else { None } }) .unwrap_or(RefAct::NoAction), on_update: clause .args .iter() .find_map(|a| { if let ast::RefArg::OnUpdate(x) = a { Some(*x) } else { None } }) .unwrap_or(RefAct::NoAction), deferred, }; foreign_keys.push(Arc::new(fk)); } else if let ast::TableConstraint::Check(expr) = &c.constraint { check_constraints.push(CheckConstraint::new(c.name.as_ref(), expr, None)); } } // Due to a bug in SQLite, this check is needed to maintain backwards compatibility with rowid alias // SQLite docs: https://sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key // Issue: https://github.com/tursodatabase/turso/issues/3665 let mut primary_key_desc_columns_constraint = false; for ast::ColumnDefinition { col_name, col_type, constraints, } in columns { let name = col_name.as_str().to_string(); // Regular sqlite tables have an integer rowid that uniquely identifies a row. // Even if you create a table with a column e.g. 'id INT PRIMARY KEY', there will still // be a separate hidden rowid, and the 'id' column will have a separate index built for it. // // However: // A column defined as exactly INTEGER PRIMARY KEY is a rowid alias, meaning that the rowid // and the value of this column are the same. // https://www.sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key let ty_str = col_type .as_ref() .cloned() .map(|ast::Type { name, .. }| name) .unwrap_or_default(); let ty_params: Vec> = match col_type { Some(ast::Type { size: Some(ast::TypeSize::MaxSize(ref expr)), .. }) => vec![expr.clone()], Some(ast::Type { size: Some(ast::TypeSize::TypeSize(ref e1, ref e2)), .. }) => vec![e1.clone(), e2.clone()], _ => Vec::new(), }; let mut typename_exactly_integer = false; let ty = match col_type { Some(data_type) => { let (ty, ei) = type_from_name(&data_type.name); typename_exactly_integer = ei; ty } None => Type::Null, }; let mut default = None; let mut primary_key = false; let mut notnull = false; let mut notnull_conflict_clause = None; let mut order = SortOrder::Asc; let mut unique = false; let mut collation = None; for c_def in constraints { match &c_def.constraint { ast::ColumnConstraint::Check(expr) => { check_constraints.push(CheckConstraint::new( c_def.name.as_ref(), expr, Some(&name), )); } ast::ColumnConstraint::Generated { .. } => { // todo(sivukhin): table_xinfo must be updated when generated columns will be supported in order to properly emit "hidden" column value crate::bail_parse_error!("GENERATED columns are not yet supported"); } ast::ColumnConstraint::PrimaryKey { order: o, auto_increment, conflict_clause, .. } => { if !primary_key_columns.is_empty() { crate::bail_parse_error!( "table \"{}\" has more than one primary key", tbl_name ); } primary_key = true; if *auto_increment { has_autoincrement = true; } if let Some(o) = o { order = *o; } unique_sets_columns.push(UniqueSet { columns: vec![(name.clone(), order)], is_primary_key: true, conflict_clause: *conflict_clause, }); } ast::ColumnConstraint::NotNull { nullable, conflict_clause, .. } => { notnull = !nullable; notnull_conflict_clause = *conflict_clause; } ast::ColumnConstraint::Default(ref expr) => { default = Some( translate_ident_to_string_literal(expr) .unwrap_or_else(|| expr.clone()), ); } ast::ColumnConstraint::Unique(conflict) => { unique = true; unique_sets_columns.push(UniqueSet { columns: vec![(name.clone(), order)], is_primary_key: false, conflict_clause: *conflict, }); } ast::ColumnConstraint::Collate { ref collation_name } => { collation = Some(CollationSeq::new(collation_name.as_str())?); } ast::ColumnConstraint::ForeignKey { clause, defer_clause, } => { if clause.columns.len() > 1 { crate::bail_parse_error!( "foreign key on {} should reference only one column of table {}", name, clause.tbl_name.as_str() ); } let fk = ForeignKey { parent_table: normalize_ident(clause.tbl_name.as_str()), parent_columns: clause .columns .iter() .map(|c| normalize_ident(c.col_name.as_str())) .collect(), on_delete: clause .args .iter() .find_map(|arg| { if let ast::RefArg::OnDelete(act) = arg { Some(*act) } else { None } }) .unwrap_or(RefAct::NoAction), on_update: clause .args .iter() .find_map(|arg| { if let ast::RefArg::OnUpdate(act) = arg { Some(*act) } else { None } }) .unwrap_or(RefAct::NoAction), child_columns: vec![name.clone()], deferred: match defer_clause { Some(d) => { d.deferrable && matches!( d.init_deferred, Some(InitDeferredPred::InitiallyDeferred) ) } None => false, }, }; foreign_keys.push(Arc::new(fk)); } } } if primary_key { primary_key_columns.push((name.clone(), order)); if order == SortOrder::Desc { primary_key_desc_columns_constraint = true; } } else if primary_key_columns .iter() .any(|(col_name, _)| col_name.eq_ignore_ascii_case(&name)) { primary_key = true; } let mut col = Column::new( Some(name), ty_str, default, None, ty, collation, ColDef { primary_key, rowid_alias: typename_exactly_integer && primary_key && !primary_key_desc_columns_constraint, notnull, unique, hidden: false, notnull_conflict_clause, }, ); col.ty_params = ty_params; if let Some(t) = col_type.as_ref() { if t.is_array() { col.set_array_dimensions(t.array_dimensions); } } cols.push(col); } if options.contains_without_rowid() { has_rowid = false; } } CreateTableBody::AsSelect(_) => { crate::bail_parse_error!("CREATE TABLE AS SELECT is not supported") } }; // flip is_rowid_alias back to false if the table has multiple primary key columns // or if the table has no rowid if !has_rowid || primary_key_columns.len() > 1 { for col in cols.iter_mut() { col.set_rowid_alias(false); } } if has_autoincrement { // only allow integers if primary_key_columns.len() != 1 { crate::bail_parse_error!("AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY"); } let pk_col_name = &primary_key_columns[0].0; let pk_col = cols.iter().find(|c| { c.name .as_deref() .is_some_and(|n| n.eq_ignore_ascii_case(pk_col_name)) }); if let Some(col) = pk_col { if col.ty() != Type::Integer { crate::bail_parse_error!("AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY"); } } } // concat unqiue_sets collected from column definitions and constraints in correct order let mut unique_sets = unique_sets_columns .into_iter() .chain(unique_sets_constraints) .collect::>(); // Capture PK conflict clause before the rowid-alias UniqueSet is removed. let rowid_alias_conflict_clause = unique_sets .iter() .find(|us| us.is_primary_key) .and_then(|us| us.conflict_clause); for col in cols.iter() { if col.is_rowid_alias() { // Unique sets are used for creating automatic indexes. An index is not created for a rowid alias PRIMARY KEY. // However, an index IS created for a rowid alias UNIQUE, e.g. CREATE TABLE t(x INTEGER PRIMARY KEY, UNIQUE(x)) let unique_set_w_only_rowid_alias = unique_sets.iter().position(|us| { us.is_primary_key && us.columns.len() == 1 && us .columns .first() .unwrap() .0 .eq_ignore_ascii_case(col.name.as_ref().unwrap()) }); if let Some(u) = unique_set_w_only_rowid_alias { unique_sets.remove(u); } } } Ok(BTreeTable { root_page, name: table_name, has_rowid, primary_key_columns, has_autoincrement, columns: cols, is_strict, foreign_keys, unique_sets: { // If there are any unique sets that have identical column names in the same order (even if they are PRIMARY KEY and UNIQUE and have different sort orders), remove the duplicates. // Examples: // PRIMARY KEY (a, b) and UNIQUE (a desc, b) are the same // PRIMARY KEY (a, b) and UNIQUE (b, a) are not the same // Using a n^2 monkey algorithm here because n is small, CPUs are fast, life is short, and most importantly: // we want to preserve the order of the sets -- automatic index names in sqlite_schema must be in definition order. let mut i = 0; while i < unique_sets.len() { let mut j = i + 1; while j < unique_sets.len() { let lengths_equal = unique_sets[i].columns.len() == unique_sets[j].columns.len(); if lengths_equal && unique_sets[i] .columns .iter() .zip(unique_sets[j].columns.iter()) .all(|((a_name, _), (b_name, _))| a_name.eq_ignore_ascii_case(b_name)) { // SQLite rejects duplicate constraints on the same columns when both // specify ON CONFLICT with different resolve types. if let (Some(a), Some(b)) = ( unique_sets[i].conflict_clause, unique_sets[j].conflict_clause, ) { if a != b { crate::bail_parse_error!( "conflicting ON CONFLICT clauses specified" ); } } unique_sets.remove(j); } else { j += 1; } } i += 1; } unique_sets }, check_constraints, rowid_alias_conflict_clause, }) } /// SQLite treats bare identifiers in DEFAULT clauses as string literals. /// E.g., `DEFAULT hello` becomes the string "hello", not a column reference. pub fn translate_ident_to_string_literal(expr: &Expr) -> Option> { match expr { Expr::Name(name) | Expr::Id(name) => { Some(Box::new(Expr::Literal(Literal::String(name.as_literal())))) } _ => None, } } pub fn _build_pseudo_table(columns: &[ResultColumn]) -> PseudoCursorType { let table = PseudoCursorType::new(); for column in columns { match column { ResultColumn::Expr(expr, _as_name) => { todo!("unsupported expression {:?}", expr); } ResultColumn::Star => { todo!(); } ResultColumn::TableStar(_) => { todo!(); } } } table } #[derive(Debug, Clone)] pub struct ForeignKey { /// Columns in this table (child side) pub child_columns: Vec, /// Referenced (parent) table pub parent_table: String, /// Parent-side referenced columns pub parent_columns: Vec, pub on_delete: RefAct, pub on_update: RefAct, /// DEFERRABLE INITIALLY DEFERRED pub deferred: bool, } impl ForeignKey { fn validate(&self) -> Result<()> { if self .parent_columns .iter() .any(|c| ROWID_STRS.iter().any(|&r| r.eq_ignore_ascii_case(c))) { return Err(crate::LimboError::ForeignKeyConstraint(format!( "foreign key mismatch referencing \"{}\"", self.parent_table ))); } Ok(()) } } /// A single resolved foreign key where `parent_table == target`. #[derive(Clone, Debug)] pub struct ResolvedFkRef { /// Child table that owns the FK. pub child_table: Arc, /// The FK as declared on the child table. pub fk: Arc, /// Resolved, normalized column names. pub child_cols: Vec, /// Column positions in the child/parent tables (pos_in_table) pub child_pos: Vec, pub parent_pos: Vec, /// If the parent key is rowid or a rowid-alias (single-column only) pub parent_uses_rowid: bool, /// For non-rowid parents: the UNIQUE index that enforces the parent key. /// (None when `parent_uses_rowid == true`.) pub parent_unique_index: Option>, } impl ResolvedFkRef { /// Returns if any referenced parent column can change when these column positions are updated. pub fn parent_key_may_change( &self, updated_parent_positions: &HashSet, parent_tbl: &BTreeTable, ) -> bool { if self.parent_uses_rowid { // parent rowid changes if the parent's rowid or alias is updated if let Some((idx, _)) = parent_tbl .columns .iter() .enumerate() .find(|(_, c)| c.is_rowid_alias()) { return updated_parent_positions.contains(&idx); } // Without a rowid alias, a direct rowid update is represented separately with ROWID_SENTINEL return true; } self.parent_pos .iter() .any(|p| updated_parent_positions.contains(p)) } /// Returns if any child column of this FK is in `updated_child_positions` pub fn child_key_changed( &self, updated_child_positions: &HashSet, child_tbl: &BTreeTable, ) -> bool { if self .child_pos .iter() .any(|p| updated_child_positions.contains(p)) { return true; } // special case: if FK uses a rowid alias on child, and rowid changed if self.child_cols.len() == 1 { let (i, col) = child_tbl.get_column(&self.child_cols[0]).unwrap(); if col.is_rowid_alias() && updated_child_positions.contains(&i) { return true; } } false } } #[derive(Debug, Clone)] pub struct Column { pub name: Option, pub ty_str: String, pub ty_params: Vec>, pub default: Option>, pub generated: Option>, raw: u16, /// ON CONFLICT clause for NOT NULL constraint on this column. pub notnull_conflict_clause: Option, } #[derive(Default)] pub struct ColDef { pub primary_key: bool, pub rowid_alias: bool, pub notnull: bool, pub unique: bool, pub hidden: bool, pub notnull_conflict_clause: Option, } // flags const F_PRIMARY_KEY: u16 = 1; const F_ROWID_ALIAS: u16 = 2; const F_NOTNULL: u16 = 4; const F_UNIQUE: u16 = 8; const F_HIDDEN: u16 = 16; // pack Type and Collation in the remaining bits const TYPE_SHIFT: u16 = 5; const TYPE_MASK: u16 = 0b111 << TYPE_SHIFT; const COLL_SHIFT: u16 = TYPE_SHIFT + 3; const COLL_MASK: u16 = 0b11 << COLL_SHIFT; // Bits 10-12: base type affinity override for custom type columns. // 0 = not set (use ty_str-based affinity), 1-5 = Affinity value + 1 const BASE_AFF_SHIFT: u16 = COLL_SHIFT + 2; const BASE_AFF_MASK: u16 = 0b111 << BASE_AFF_SHIFT; // Bits 13-15: array dimensions (0 = scalar, 1-7 = number of [] dimensions) const ARRAY_DIM_SHIFT: u16 = 13; const ARRAY_DIM_MASK: u16 = 0b111 << ARRAY_DIM_SHIFT; impl Column { pub fn affinity(&self) -> Affinity { let v = ((self.raw & BASE_AFF_MASK) >> BASE_AFF_SHIFT) as u8; if v > 0 { // Custom type column: use the base type's affinity match v { 1 => Affinity::Integer, 2 => Affinity::Text, 3 => Affinity::Blob, 4 => Affinity::Real, _ => Affinity::Numeric, } } else { Affinity::affinity(&self.ty_str) } } /// Set the base type affinity override for a custom type column. /// This ensures affinity rules use the custom type's BASE type /// rather than applying SQLite name-based rules to the type name. pub fn set_base_affinity(&mut self, affinity: Affinity) { let v: u16 = match affinity { Affinity::Integer => 1, Affinity::Text => 2, Affinity::Blob => 3, Affinity::Real => 4, Affinity::Numeric => 5, }; self.raw = (self.raw & !BASE_AFF_MASK) | ((v << BASE_AFF_SHIFT) & BASE_AFF_MASK); } pub fn affinity_with_strict(&self, is_strict: bool) -> Affinity { if is_strict && self.ty_str.eq_ignore_ascii_case("ANY") { Affinity::Blob } else { self.affinity() } } pub fn new_default_text( name: Option, ty_str: String, default: Option>, ) -> Self { Self::new( name, ty_str, default, None, Type::Text, None, ColDef::default(), ) } pub fn new_default_integer( name: Option, ty_str: String, default: Option>, ) -> Self { Self::new( name, ty_str, default, None, Type::Integer, None, ColDef::default(), ) } #[inline] pub fn new( name: Option, ty_str: String, default: Option>, generated: Option>, ty: Type, col: Option, coldef: ColDef, ) -> Self { let mut raw = 0u16; raw |= (ty as u16) << TYPE_SHIFT; if let Some(c) = col { raw |= (c as u16) << COLL_SHIFT; } if coldef.primary_key { raw |= F_PRIMARY_KEY } if coldef.rowid_alias { raw |= F_ROWID_ALIAS } if coldef.notnull { raw |= F_NOTNULL } if coldef.unique { raw |= F_UNIQUE } if coldef.hidden { raw |= F_HIDDEN } Self { name, ty_str, ty_params: Vec::new(), default, generated, raw, notnull_conflict_clause: coldef.notnull_conflict_clause, } } #[inline] pub const fn ty(&self) -> Type { let v = ((self.raw & TYPE_MASK) >> TYPE_SHIFT) as u8; Type::from_bits(v) } #[inline] pub const fn set_ty(&mut self, ty: Type) { self.raw = (self.raw & !TYPE_MASK) | (((ty as u16) << TYPE_SHIFT) & TYPE_MASK); } #[inline] pub const fn collation_opt(&self) -> Option { if self.has_explicit_collation() { Some(self.collation()) } else { None } } #[inline] pub const fn collation(&self) -> CollationSeq { let v = ((self.raw & COLL_MASK) >> COLL_SHIFT) as u8; CollationSeq::from_bits(v) } #[inline] pub const fn has_explicit_collation(&self) -> bool { let v = ((self.raw & COLL_MASK) >> COLL_SHIFT) as u8; v != CollationSeq::Unset as u8 } #[inline] pub const fn set_collation(&mut self, c: Option) { if let Some(c) = c { self.raw = (self.raw & !COLL_MASK) | (((c as u16) << COLL_SHIFT) & COLL_MASK); } } #[inline] pub fn primary_key(&self) -> bool { self.raw & F_PRIMARY_KEY != 0 } #[inline] pub const fn is_rowid_alias(&self) -> bool { self.raw & F_ROWID_ALIAS != 0 } #[inline] pub const fn notnull(&self) -> bool { self.raw & F_NOTNULL != 0 } #[inline] pub const fn unique(&self) -> bool { self.raw & F_UNIQUE != 0 } #[inline] pub const fn hidden(&self) -> bool { self.raw & F_HIDDEN != 0 } #[inline] pub const fn set_primary_key(&mut self, v: bool) { self.set_flag(F_PRIMARY_KEY, v); } #[inline] pub const fn set_rowid_alias(&mut self, v: bool) { self.set_flag(F_ROWID_ALIAS, v); } #[inline] pub const fn set_notnull(&mut self, v: bool) { self.set_flag(F_NOTNULL, v); } #[inline] pub const fn set_unique(&mut self, v: bool) { self.set_flag(F_UNIQUE, v); } #[inline] pub const fn set_hidden(&mut self, v: bool) { self.set_flag(F_HIDDEN, v); } #[inline] pub const fn is_array(&self) -> bool { (self.raw & ARRAY_DIM_MASK) != 0 } /// Number of array dimensions (0 = scalar, 1 = `[]`, 2 = `[][]`, etc.) #[inline] pub const fn array_dimensions(&self) -> u32 { ((self.raw & ARRAY_DIM_MASK) >> ARRAY_DIM_SHIFT) as u32 } #[inline] pub fn set_array_dimensions(&mut self, dims: u32) { assert!(dims <= 7, "array dimensions must be <= 7"); self.raw = (self.raw & !ARRAY_DIM_MASK) | ((dims as u16) << ARRAY_DIM_SHIFT); } #[inline] const fn set_flag(&mut self, mask: u16, val: bool) { if val { self.raw |= mask } else { self.raw &= !mask } } } // TODO: This might replace some of util::columns_from_create_table_body impl TryFrom<&ColumnDefinition> for Column { type Error = crate::LimboError; fn try_from(value: &ColumnDefinition) -> crate::Result { let name = value.col_name.as_str(); let mut default = None; let mut generated = None; let mut notnull = false; let mut notnull_conflict_clause = None; let mut primary_key = false; let mut unique = false; let mut collation = None; for ast::NamedColumnConstraint { constraint, .. } in &value.constraints { match constraint { ast::ColumnConstraint::PrimaryKey { .. } => primary_key = true, ast::ColumnConstraint::NotNull { conflict_clause, .. } => { notnull = true; notnull_conflict_clause = *conflict_clause; } ast::ColumnConstraint::Unique(..) => unique = true, ast::ColumnConstraint::Default(expr) => { default.replace( translate_ident_to_string_literal(expr).unwrap_or_else(|| expr.clone()), ); } ast::ColumnConstraint::Collate { collation_name } => { collation.replace(CollationSeq::new(collation_name.as_str())?); } ast::ColumnConstraint::Generated { expr, .. } => { generated = Some(expr.clone()); } _ => {} }; } let ty = match value.col_type { Some(ref data_type) => type_from_name(&data_type.name).0, None => Type::Null, }; let ty_str = value .col_type .as_ref() .map(|t| t.name.to_string()) .unwrap_or_default(); let ty_params: Vec> = match &value.col_type { Some(ast::Type { size: Some(ast::TypeSize::MaxSize(ref expr)), .. }) => vec![expr.clone()], Some(ast::Type { size: Some(ast::TypeSize::TypeSize(ref e1, ref e2)), .. }) => vec![e1.clone(), e2.clone()], _ => Vec::new(), }; let hidden = ty_str.contains("HIDDEN"); let mut col = Column::new( Some(name.to_string()), ty_str, default, generated, ty, collation, ColDef { primary_key, rowid_alias: primary_key && matches!(ty, Type::Integer), notnull, unique, hidden, notnull_conflict_clause, }, ); col.ty_params = ty_params; if let Some(t) = value.col_type.as_ref() { if t.is_array() { col.set_array_dimensions(t.array_dimensions); } } Ok(col) } } #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq)] pub enum Type { Null = 0, Text = 1, Numeric = 2, Integer = 3, Real = 4, Blob = 5, } impl Type { #[inline] const fn from_bits(bits: u8) -> Self { match bits { 0 => Type::Null, 1 => Type::Text, 2 => Type::Numeric, 3 => Type::Integer, 4 => Type::Real, 5 => Type::Blob, _ => Type::Null, } } } impl fmt::Display for Type { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { Self::Null => "", Self::Text => "TEXT", Self::Numeric => "NUMERIC", Self::Integer => "INTEGER", Self::Real => "REAL", Self::Blob => "BLOB", }; write!(f, "{s}") } } pub fn sqlite_schema_table() -> BTreeTable { BTreeTable { root_page: 1, name: "sqlite_schema".to_string(), has_rowid: true, is_strict: false, has_autoincrement: false, primary_key_columns: vec![], columns: vec![ Column::new_default_text(Some("type".to_string()), "TEXT".to_string(), None), Column::new_default_text(Some("name".to_string()), "TEXT".to_string(), None), Column::new_default_text(Some("tbl_name".to_string()), "TEXT".to_string(), None), Column::new_default_integer(Some("rootpage".to_string()), "INT".to_string(), None), Column::new_default_text(Some("sql".to_string()), "TEXT".to_string(), None), ], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, unique_sets: vec![], } } #[allow(dead_code)] #[derive(Debug, Clone)] pub struct Index { pub name: String, pub table_name: String, pub root_page: i64, pub columns: Vec, pub unique: bool, pub ephemeral: bool, /// Does the index have a rowid as the last column? /// This is the case for btree indexes (persistent or ephemeral) that /// have been created based on a table with a rowid. /// For example, WITHOUT ROWID tables (not supported in Limbo yet), /// and SELECT DISTINCT ephemeral indexes will not have a rowid. pub has_rowid: bool, pub where_clause: Option>, pub index_method: Option>, /// ON CONFLICT clause from the constraint definition (PRIMARY KEY or UNIQUE). pub on_conflict: Option, } #[allow(dead_code)] #[derive(Debug, Clone)] pub struct IndexColumn { pub name: String, pub order: SortOrder, /// the position of the column in the source table. /// for example: /// CREATE TABLE t (a,b,c) /// CREATE INDEX idx ON t(b) /// b.pos_in_table == 1 pub pos_in_table: usize, pub collation: Option, pub default: Option>, /// Expression for expression indexes. None for simple column indexes. pub expr: Option>, } impl Index { pub fn from_sql( syms: &SymbolTable, sql: &str, root_page: i64, table: &BTreeTable, ) -> Result { let mut parser = Parser::new(sql.as_bytes()); let cmd = parser.next_cmd()?; match cmd { Some(Cmd::Stmt(Stmt::CreateIndex { idx_name, tbl_name, columns, unique, where_clause, using, with_clause, .. })) => { let index_name = normalize_ident(idx_name.name.as_str()); let index_columns = resolve_sorted_columns(table, &columns)?; if let Some(using) = using { if where_clause.is_some() { bail_parse_error!("custom index module do not support partial indices"); } if unique { bail_parse_error!("custom index module do not support UNIQUE indices"); } let parameters = resolve_index_method_parameters(with_clause)?; let Some(module) = syms.index_methods.get(using.as_str()) else { bail_parse_error!("unknown module name: '{}'", using); }; let configuration = IndexMethodConfiguration { table_name: table.name.clone(), index_name: index_name.clone(), columns: index_columns.clone(), parameters, }; let descriptor = module.attach(&configuration)?; Ok(Index { name: index_name, table_name: normalize_ident(tbl_name.as_str()), root_page, columns: index_columns, unique: false, ephemeral: false, has_rowid: table.has_rowid, where_clause: None, index_method: Some(descriptor), on_conflict: None, }) } else { Ok(Index { name: index_name, table_name: normalize_ident(tbl_name.as_str()), root_page, columns: index_columns, unique, ephemeral: false, has_rowid: table.has_rowid, where_clause, index_method: None, on_conflict: None, }) } } _ => todo!("Expected create index statement"), } } /// Check if this is an expression index. pub fn is_expression_index(&self) -> bool { self.columns.iter().any(|c| c.expr.is_some()) } /// check if this is special backing_btree index created and managed by custom index_method pub fn is_backing_btree_index(&self) -> bool { self.index_method .as_ref() .is_some_and(|x| x.definition().backing_btree) } pub fn automatic_from_primary_key( table: &BTreeTable, auto_index: (String, i64), // name, root_page column_count: usize, conflict_clause: Option, ) -> Result { let has_primary_key_index = table.get_rowid_alias_column().is_none() && !table.primary_key_columns.is_empty(); assert!(has_primary_key_index); let (index_name, root_page) = auto_index; let mut primary_keys = Vec::with_capacity(column_count); for (col_name, order) in table.primary_key_columns.iter() { let Some((pos_in_table, _)) = table.get_column(col_name) else { return Err(crate::LimboError::ParseError(format!( "Column {} not found in table {}", col_name, table.name ))); }; let (_, column) = table.get_column(col_name).unwrap(); primary_keys.push(IndexColumn { name: normalize_ident(col_name), order: *order, pos_in_table, collation: column.collation_opt(), default: column.default.clone(), expr: None, }); } assert!(primary_keys.len() == column_count); Ok(Index { name: normalize_ident(index_name.as_str()), table_name: table.name.clone(), root_page, columns: primary_keys, unique: true, ephemeral: false, has_rowid: table.has_rowid, where_clause: None, index_method: None, on_conflict: conflict_clause, }) } pub fn automatic_from_unique( table: &BTreeTable, auto_index: (String, i64), // name, root_page column_indices_and_sort_orders: Vec<(usize, SortOrder)>, conflict_clause: Option, ) -> Result { let (index_name, root_page) = auto_index; let mut unique_cols = Vec::with_capacity(column_indices_and_sort_orders.len()); for (pos, sort_order) in &column_indices_and_sort_orders { let Some((pos_in_table, col)) = table .columns .iter() .enumerate() .find(|(pos_in_table, _)| pos == pos_in_table) else { return Err(crate::LimboError::ParseError(format!( "Unique constraint column not found in table {}", table.name ))); }; unique_cols.push(IndexColumn { name: normalize_ident(col.name.as_ref().unwrap()), order: *sort_order, pos_in_table, collation: col.collation_opt(), default: col.default.clone(), expr: None, }); } Ok(Index { name: normalize_ident(index_name.as_str()), table_name: table.name.clone(), root_page, columns: unique_cols, unique: true, ephemeral: false, has_rowid: table.has_rowid, where_clause: None, index_method: None, on_conflict: conflict_clause, }) } /// Given a column position in the table, return the position in the index. /// Returns None if the column is not found in the index. /// For example, given: /// CREATE TABLE t (a, b, c) /// CREATE INDEX idx ON t(b) /// then column_table_pos_to_index_pos(1) returns Some(0) pub fn column_table_pos_to_index_pos(&self, table_pos: usize) -> Option { self.columns .iter() .position(|c| c.pos_in_table == table_pos) } /// Given an expression, return the position in the index if it matches an expression index column. /// Expression index matching is textual (after binding), so the caller should normalize the query /// expression to resemble the stored index expression (e.g. unqualified column names). pub fn expression_to_index_pos(&self, expr: &Expr) -> Option { self.columns.iter().position(|c| { c.expr .as_ref() .is_some_and(|e| exprs_are_equivalent(e, expr)) }) } /// Walk the where_clause Expr of a partial index and validate that it doesn't reference any other /// tables or use any disallowed constructs. pub fn validate_where_expr(&self, table: &Table, _resolver: &Resolver) -> bool { let Some(where_clause) = &self.where_clause else { return true; }; let tbl_norm = self.table_name.as_str(); let has_col = |name: &str| { table.columns().iter().any(|c| { c.name .as_ref() .is_some_and(|cn| cn.eq_ignore_ascii_case(name)) }) }; let is_tbl = |ns: &str| normalize_ident(ns) == tbl_norm; let is_deterministic_fn = |name: &str, argc: usize| { let n = normalize_ident(name); Func::resolve_function(&n, argc).is_ok_and(|f| f.is_deterministic()) }; let mut ok = true; let _ = walk_expr(where_clause.as_ref(), &mut |e: &Expr| -> crate::Result< WalkControl, > { if !ok { return Ok(WalkControl::SkipChildren); } match e { Expr::Literal(_) | Expr::RowId { .. } => {} // Unqualified identifier: must be a column of the target table or ROWID Expr::Id(n) => { let n = n.as_str(); if !ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(n)) && !has_col(n) { ok = false; } } // Qualified: qualifier must match this index's table; column must exist Expr::Qualified(ns, col) | Expr::DoublyQualified(_, ns, col) => { if !is_tbl(ns.as_str()) || !has_col(col.as_str()) { ok = false; } } Expr::FunctionCall { name, filter_over, .. } | Expr::FunctionCallStar { name, filter_over, .. } => { // reject windowed if filter_over.over_clause.is_some() { ok = false; } else { let argc = match e { Expr::FunctionCall { args, .. } => args.len(), Expr::FunctionCallStar { .. } => 0, _ => unreachable!(), }; // Reject non-deterministic functions. Function arguments can reference // columns of the indexed table (e.g., LENGTH(t0.c0)), which will be // validated by the Expr::Id and Expr::Qualified cases during the walk. if !is_deterministic_fn(name.as_str(), argc) { ok = false; } } } // Explicitly disallowed constructs Expr::Exists(_) | Expr::InSelect { .. } | Expr::Subquery(_) | Expr::Raise { .. } | Expr::Variable(_) => { ok = false; } _ => {} } Ok(if ok { WalkControl::Continue } else { WalkControl::SkipChildren }) }); ok } pub fn bind_where_expr( &self, table_refs: Option<&mut TableReferences>, resolver: &Resolver, ) -> Option { let Some(where_clause) = &self.where_clause else { return None; }; let mut expr = where_clause.clone(); bind_and_rewrite_expr( &mut expr, table_refs, None, resolver, BindingBehavior::ResultColumnsNotAllowed, ) .ok()?; Some(*expr) } } #[cfg(test)] mod tests { use super::*; #[test] pub fn test_has_rowid_true() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT);"#; let table = BTreeTable::from_sql(sql, 0)?; assert!(table.has_rowid, "has_rowid should be set to true"); Ok(()) } #[test] pub fn test_has_rowid_false() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT) WITHOUT ROWID;"#; let table = BTreeTable::from_sql(sql, 0)?; assert!(!table.has_rowid, "has_rowid should be set to false"); Ok(()) } #[test] pub fn test_column_is_rowid_alias_single_text() -> Result<()> { let sql = r#"CREATE TABLE t1 (a TEXT PRIMARY KEY, b TEXT);"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!( !column.is_rowid_alias(), "column 'a´ has type different than INTEGER so can't be a rowid alias" ); Ok(()) } #[test] pub fn test_column_is_rowid_alias_single_integer() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT);"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!( column.is_rowid_alias(), "column 'a´ should be a rowid alias" ); Ok(()) } #[test] pub fn test_column_is_rowid_alias_single_integer_separate_primary_key_definition() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, PRIMARY KEY(a));"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!( column.is_rowid_alias(), "column 'a´ should be a rowid alias" ); Ok(()) } #[test] pub fn test_column_is_rowid_alias_single_integer_separate_primary_key_definition_without_rowid( ) -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, PRIMARY KEY(a)) WITHOUT ROWID;"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!( !column.is_rowid_alias(), "column 'a´ shouldn't be a rowid alias because table has no rowid" ); Ok(()) } #[test] pub fn test_column_is_rowid_alias_single_integer_without_rowid() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT) WITHOUT ROWID;"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!( !column.is_rowid_alias(), "column 'a´ shouldn't be a rowid alias because table has no rowid" ); Ok(()) } #[test] pub fn test_multiple_pk_forbidden() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT PRIMARY KEY);"#; let table = BTreeTable::from_sql(sql, 0); let error = table.unwrap_err(); assert!( matches!(error, LimboError::ParseError(e) if e.contains("table \"t1\" has more than one primary key")) ); Ok(()) } #[test] pub fn test_column_is_rowid_alias_separate_composite_primary_key_definition() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, PRIMARY KEY(a, b));"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!( !column.is_rowid_alias(), "column 'a´ shouldn't be a rowid alias because table has composite primary key" ); Ok(()) } #[test] pub fn test_primary_key_inline_single() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT, c REAL);"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!(column.primary_key(), "column 'a' should be a primary key"); let column = table.get_column("b").unwrap().1; assert!( !column.primary_key(), "column 'b' shouldn't be a primary key" ); let column = table.get_column("c").unwrap().1; assert!( !column.primary_key(), "column 'c' shouldn't be a primary key" ); assert_eq!( vec![("a".to_string(), SortOrder::Asc)], table.primary_key_columns, "primary key column names should be ['a']" ); Ok(()) } #[test] pub fn test_primary_key_inline_multiple_forbidden() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT PRIMARY KEY, c REAL);"#; let table = BTreeTable::from_sql(sql, 0); let error = table.unwrap_err(); assert!( matches!(error, LimboError::ParseError(e) if e.contains("table \"t1\" has more than one primary key")) ); Ok(()) } #[test] pub fn test_conflicting_on_conflict_unique_rejected() -> Result<()> { let sql = r#"CREATE TABLE t1 (a UNIQUE ON CONFLICT FAIL, b, UNIQUE(a) ON CONFLICT IGNORE);"#; let table = BTreeTable::from_sql(sql, 0); let error = table.unwrap_err(); assert!( matches!(error, LimboError::ParseError(e) if e.contains("conflicting ON CONFLICT clauses")) ); Ok(()) } #[test] pub fn test_conflicting_on_conflict_composite_unique_rejected() -> Result<()> { let sql = r#"CREATE TABLE t1 (a, b, UNIQUE(a, b) ON CONFLICT FAIL, UNIQUE(a, b) ON CONFLICT IGNORE);"#; let table = BTreeTable::from_sql(sql, 0); let error = table.unwrap_err(); assert!( matches!(error, LimboError::ParseError(e) if e.contains("conflicting ON CONFLICT clauses")) ); Ok(()) } #[test] pub fn test_same_on_conflict_unique_allowed() -> Result<()> { let sql = r#"CREATE TABLE t1 (a UNIQUE ON CONFLICT FAIL, b, UNIQUE(a) ON CONFLICT FAIL);"#; assert!(BTreeTable::from_sql(sql, 0).is_ok()); Ok(()) } #[test] pub fn test_one_on_conflict_unique_allowed() -> Result<()> { let sql = r#"CREATE TABLE t1 (a UNIQUE ON CONFLICT FAIL, b, UNIQUE(a));"#; assert!(BTreeTable::from_sql(sql, 0).is_ok()); Ok(()) } #[test] pub fn test_primary_key_separate_single() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a desc));"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!(column.primary_key(), "column 'a' should be a primary key"); let column = table.get_column("b").unwrap().1; assert!( !column.primary_key(), "column 'b' shouldn't be a primary key" ); let column = table.get_column("c").unwrap().1; assert!( !column.primary_key(), "column 'c' shouldn't be a primary key" ); assert_eq!( vec![("a".to_string(), SortOrder::Desc)], table.primary_key_columns, "primary key column names should be ['a']" ); Ok(()) } #[test] pub fn test_primary_key_separate_multiple() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a, b desc));"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!(column.primary_key(), "column 'a' should be a primary key"); let column = table.get_column("b").unwrap().1; assert!(column.primary_key(), "column 'b' shouldn be a primary key"); let column = table.get_column("c").unwrap().1; assert!( !column.primary_key(), "column 'c' shouldn't be a primary key" ); assert_eq!( vec![ ("a".to_string(), SortOrder::Asc), ("b".to_string(), SortOrder::Desc) ], table.primary_key_columns, "primary key column names should be ['a', 'b']" ); Ok(()) } #[test] pub fn test_primary_key_separate_single_quoted() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY('a'));"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!(column.primary_key(), "column 'a' should be a primary key"); let column = table.get_column("b").unwrap().1; assert!( !column.primary_key(), "column 'b' shouldn't be a primary key" ); let column = table.get_column("c").unwrap().1; assert!( !column.primary_key(), "column 'c' shouldn't be a primary key" ); assert_eq!( vec![("a".to_string(), SortOrder::Asc)], table.primary_key_columns, "primary key column names should be ['a']" ); Ok(()) } #[test] pub fn test_primary_key_separate_single_doubly_quoted() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY("a"));"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!(column.primary_key(), "column 'a' should be a primary key"); let column = table.get_column("b").unwrap().1; assert!( !column.primary_key(), "column 'b' shouldn't be a primary key" ); let column = table.get_column("c").unwrap().1; assert!( !column.primary_key(), "column 'c' shouldn't be a primary key" ); assert_eq!( vec![("a".to_string(), SortOrder::Asc)], table.primary_key_columns, "primary key column names should be ['a']" ); Ok(()) } #[test] pub fn test_default_value() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER DEFAULT 23);"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; let default = column.default.clone().unwrap(); assert_eq!(default.to_string(), "23"); Ok(()) } #[test] pub fn test_col_notnull() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER NOT NULL);"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!(column.notnull()); Ok(()) } #[test] pub fn test_col_notnull_negative() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER);"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert!(!column.notnull()); Ok(()) } #[test] pub fn test_col_type_string_integer() -> Result<()> { let sql = r#"CREATE TABLE t1 (a InTeGeR);"#; let table = BTreeTable::from_sql(sql, 0)?; let column = table.get_column("a").unwrap().1; assert_eq!(column.ty_str, "InTeGeR"); Ok(()) } #[test] pub fn test_sqlite_schema() { let expected = r#"CREATE TABLE sqlite_schema (type TEXT, name TEXT, tbl_name TEXT, rootpage INT, sql TEXT)"#; let actual = sqlite_schema_table().to_sql(); assert_eq!(expected, actual); } #[test] pub fn test_special_column_names() -> Result<()> { let tests = [ ("foobar", "CREATE TABLE t (foobar TEXT)"), ("_table_name3", "CREATE TABLE t (_table_name3 TEXT)"), ("special name", "CREATE TABLE t ([special name] TEXT)"), ("foo&bar", "CREATE TABLE t ([foo&bar] TEXT)"), (" name", "CREATE TABLE t ([ name] TEXT)"), ]; for (input_column_name, expected_sql) in tests { let sql = format!("CREATE TABLE t ([{input_column_name}] TEXT)"); let actual = BTreeTable::from_sql(&sql, 0)?.to_sql(); assert_eq!(expected_sql, actual); } Ok(()) } #[test] fn test_special_table_names_are_quoted_in_to_sql() -> Result<()> { let tests = [ ( r#"CREATE TABLE "t t" (x TEXT)"#, r#"CREATE TABLE "t t" (x TEXT)"#, ), ( r#"CREATE TABLE "123table" (x TEXT)"#, r#"CREATE TABLE "123table" (x TEXT)"#, ), ( r#"CREATE TABLE "t""t" (x TEXT)"#, r#"CREATE TABLE "t""t" (x TEXT)"#, ), ]; for (input_sql, expected_sql) in tests { let actual = BTreeTable::from_sql(input_sql, 0)?.to_sql(); assert_eq!(actual, expected_sql); } Ok(()) } #[test] #[should_panic] fn test_automatic_index_single_column() { // Without composite primary keys, we should not have an automatic index on a primary key that is a rowid alias let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT);"#; let table = BTreeTable::from_sql(sql, 0).unwrap(); let _index = Index::automatic_from_primary_key( &table, ("sqlite_autoindex_t1_1".to_string(), 2), 1, None, ) .unwrap(); } #[test] fn test_automatic_index_composite_key() -> Result<()> { let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, PRIMARY KEY(a, b));"#; let table = BTreeTable::from_sql(sql, 0)?; let index = Index::automatic_from_primary_key( &table, ("sqlite_autoindex_t1_1".to_string(), 2), 2, None, )?; assert_eq!(index.name, "sqlite_autoindex_t1_1"); assert_eq!(index.table_name, "t1"); assert_eq!(index.root_page, 2); assert!(index.unique); assert_eq!(index.columns.len(), 2); assert_eq!(index.columns[0].name, "a"); assert_eq!(index.columns[1].name, "b"); assert!(matches!(index.columns[0].order, SortOrder::Asc)); assert!(matches!(index.columns[1].order, SortOrder::Asc)); Ok(()) } #[test] #[should_panic] fn test_automatic_index_no_primary_key() { let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT);"#; let table = BTreeTable::from_sql(sql, 0).unwrap(); Index::automatic_from_primary_key( &table, ("sqlite_autoindex_t1_1".to_string(), 2), 1, None, ) .unwrap(); } #[test] fn test_automatic_index_nonexistent_column() { // Create a table with a primary key column that doesn't exist in the table let table = BTreeTable { root_page: 0, name: "t1".to_string(), has_rowid: true, is_strict: false, has_autoincrement: false, primary_key_columns: vec![("nonexistent".to_string(), SortOrder::Asc)], columns: vec![Column::new_default_integer( Some("a".to_string()), "INT".to_string(), None, )], unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }; let result = Index::automatic_from_primary_key( &table, ("sqlite_autoindex_t1_1".to_string(), 2), 1, None, ); assert!(result.is_err()); } #[test] fn test_automatic_index_unique_column() -> Result<()> { let sql = r#"CREATE table t1 (x INTEGER, y INTEGER UNIQUE);"#; let table = BTreeTable::from_sql(sql, 0)?; let index = Index::automatic_from_unique( &table, ("sqlite_autoindex_t1_1".to_string(), 2), vec![(1, SortOrder::Asc)], None, )?; assert_eq!(index.name, "sqlite_autoindex_t1_1"); assert_eq!(index.table_name, "t1"); assert_eq!(index.root_page, 2); assert!(index.unique); assert_eq!(index.columns.len(), 1); assert_eq!(index.columns[0].name, "y"); assert!(matches!(index.columns[0].order, SortOrder::Asc)); Ok(()) } #[test] fn test_automatic_index_pkey_unique_column() -> Result<()> { let sql = r#"CREATE TABLE t1 (x PRIMARY KEY, y UNIQUE);"#; let table = BTreeTable::from_sql(sql, 0)?; let indices = [ Index::automatic_from_primary_key( &table, ("sqlite_autoindex_t1_1".to_string(), 2), 1, None, )?, Index::automatic_from_unique( &table, ("sqlite_autoindex_t1_2".to_string(), 3), vec![(1, SortOrder::Asc)], None, )?, ]; assert_eq!(indices[0].name, "sqlite_autoindex_t1_1"); assert_eq!(indices[0].table_name, "t1"); assert_eq!(indices[0].root_page, 2); assert!(indices[0].unique); assert_eq!(indices[0].columns.len(), 1); assert_eq!(indices[0].columns[0].name, "x"); assert!(matches!(indices[0].columns[0].order, SortOrder::Asc)); assert_eq!(indices[1].name, "sqlite_autoindex_t1_2"); assert_eq!(indices[1].table_name, "t1"); assert_eq!(indices[1].root_page, 3); assert!(indices[1].unique); assert_eq!(indices[1].columns.len(), 1); assert_eq!(indices[1].columns[0].name, "y"); assert!(matches!(indices[1].columns[0].order, SortOrder::Asc)); Ok(()) } #[test] fn test_automatic_index_pkey_many_unique_columns() -> Result<()> { let sql = r#"CREATE TABLE t1 (a PRIMARY KEY, b UNIQUE, c, d, UNIQUE(c, d));"#; let table = BTreeTable::from_sql(sql, 0)?; let auto_indices = [ ("sqlite_autoindex_t1_1".to_string(), 2), ("sqlite_autoindex_t1_2".to_string(), 3), ("sqlite_autoindex_t1_3".to_string(), 4), ]; let indices = vec![ Index::automatic_from_primary_key( &table, ("sqlite_autoindex_t1_1".to_string(), 2), 1, None, )?, Index::automatic_from_unique( &table, ("sqlite_autoindex_t1_2".to_string(), 3), vec![(1, SortOrder::Asc)], None, )?, Index::automatic_from_unique( &table, ("sqlite_autoindex_t1_3".to_string(), 4), vec![(2, SortOrder::Asc), (3, SortOrder::Asc)], None, )?, ]; assert!(indices.len() == auto_indices.len()); for (pos, index) in indices.iter().enumerate() { let (index_name, root_page) = &auto_indices[pos]; assert_eq!(index.name, *index_name); assert_eq!(index.table_name, "t1"); assert_eq!(index.root_page, *root_page); assert!(index.unique); if pos == 0 { assert_eq!(index.columns.len(), 1); assert_eq!(index.columns[0].name, "a"); } else if pos == 1 { assert_eq!(index.columns.len(), 1); assert_eq!(index.columns[0].name, "b"); } else if pos == 2 { assert_eq!(index.columns.len(), 2); assert_eq!(index.columns[0].name, "c"); assert_eq!(index.columns[1].name, "d"); } assert!(matches!(index.columns[0].order, SortOrder::Asc)); } Ok(()) } #[test] fn test_automatic_index_unique_set_dedup() -> Result<()> { let sql = r#"CREATE TABLE t1 (a, b, UNIQUE(a, b), UNIQUE(a, b));"#; let table = BTreeTable::from_sql(sql, 0)?; let index = Index::automatic_from_unique( &table, ("sqlite_autoindex_t1_1".to_string(), 2), vec![(0, SortOrder::Asc), (1, SortOrder::Asc)], None, )?; assert_eq!(index.name, "sqlite_autoindex_t1_1"); assert_eq!(index.table_name, "t1"); assert_eq!(index.root_page, 2); assert!(index.unique); assert_eq!(index.columns.len(), 2); assert_eq!(index.columns[0].name, "a"); assert!(matches!(index.columns[0].order, SortOrder::Asc)); assert_eq!(index.columns[1].name, "b"); assert!(matches!(index.columns[1].order, SortOrder::Asc)); Ok(()) } #[test] fn test_automatic_index_primary_key_is_unique() -> Result<()> { let sql = r#"CREATE TABLE t1 (a primary key unique);"#; let table = BTreeTable::from_sql(sql, 0)?; let index = Index::automatic_from_primary_key( &table, ("sqlite_autoindex_t1_1".to_string(), 2), 1, None, )?; assert_eq!(index.name, "sqlite_autoindex_t1_1"); assert_eq!(index.table_name, "t1"); assert_eq!(index.root_page, 2); assert!(index.unique); assert_eq!(index.columns.len(), 1); assert_eq!(index.columns[0].name, "a"); assert!(matches!(index.columns[0].order, SortOrder::Asc)); Ok(()) } #[test] fn test_automatic_index_primary_key_is_unique_and_composite() -> Result<()> { let sql = r#"CREATE TABLE t1 (a, b, PRIMARY KEY(a, b), UNIQUE(a, b));"#; let table = BTreeTable::from_sql(sql, 0)?; let index = Index::automatic_from_primary_key( &table, ("sqlite_autoindex_t1_1".to_string(), 2), 2, None, )?; assert_eq!(index.name, "sqlite_autoindex_t1_1"); assert_eq!(index.table_name, "t1"); assert_eq!(index.root_page, 2); assert!(index.unique); assert_eq!(index.columns.len(), 2); assert_eq!(index.columns[0].name, "a"); assert_eq!(index.columns[1].name, "b"); assert!(matches!(index.columns[0].order, SortOrder::Asc)); Ok(()) } #[test] fn test_strict_table_to_sql() -> Result<()> { let sql = r#"CREATE TABLE test_strict (id INTEGER, name TEXT) STRICT"#; let table = BTreeTable::from_sql(sql, 0)?; // Verify the table is marked as strict assert!(table.is_strict); // Verify that to_sql() includes the STRICT keyword let reconstructed_sql = table.to_sql(); assert!( reconstructed_sql.contains("STRICT"), "Reconstructed SQL should contain STRICT keyword: {reconstructed_sql}" ); assert_eq!( reconstructed_sql, "CREATE TABLE test_strict (id INTEGER, name TEXT) STRICT" ); Ok(()) } #[test] fn test_non_strict_table_to_sql() -> Result<()> { let sql = r#"CREATE TABLE test_normal (id INTEGER, name TEXT)"#; let table = BTreeTable::from_sql(sql, 0)?; // Verify the table is NOT marked as strict assert!(!table.is_strict); // Verify that to_sql() does NOT include the STRICT keyword let reconstructed_sql = table.to_sql(); assert!( !reconstructed_sql.contains("STRICT"), "Non-strict table SQL should not contain STRICT keyword: {reconstructed_sql}" ); assert_eq!( reconstructed_sql, "CREATE TABLE test_normal (id INTEGER, name TEXT)" ); Ok(()) } #[test] fn test_automatic_index_unique_and_a_pk() -> Result<()> { let sql = r#"CREATE TABLE t1 (a NUMERIC UNIQUE UNIQUE, b TEXT PRIMARY KEY)"#; let table = BTreeTable::from_sql(sql, 0)?; let mut indexes = vec![ Index::automatic_from_unique( &table, ("sqlite_autoindex_t1_1".to_string(), 2), vec![(0, SortOrder::Asc)], None, )?, Index::automatic_from_primary_key( &table, ("sqlite_autoindex_t1_2".to_string(), 3), 1, None, )?, ]; assert!(indexes.len() == 2); let index = indexes.pop().unwrap(); assert_eq!(index.name, "sqlite_autoindex_t1_2"); assert_eq!(index.table_name, "t1"); assert_eq!(index.root_page, 3); assert!(index.unique); assert_eq!(index.columns.len(), 1); assert_eq!(index.columns[0].name, "b"); assert!(matches!(index.columns[0].order, SortOrder::Asc)); let index = indexes.pop().unwrap(); assert_eq!(index.name, "sqlite_autoindex_t1_1"); assert_eq!(index.table_name, "t1"); assert_eq!(index.root_page, 2); assert!(index.unique); assert_eq!(index.columns.len(), 1); assert_eq!(index.columns[0].name, "a"); assert!(matches!(index.columns[0].order, SortOrder::Asc)); Ok(()) } } ================================================ FILE: core/series.rs ================================================ use crate::sync::Arc; use turso_ext::{ Connection, ConstraintInfo, ConstraintOp, ConstraintUsage, ExtensionApi, IndexInfo, OrderByInfo, ResultCode, VTabCursor, VTabKind, VTabModule, VTabModuleDerive, VTable, Value, }; pub fn register_extension(ext_api: &mut ExtensionApi) { // FIXME: Add macro magic to register functions automatically. unsafe { GenerateSeriesVTabModule::register_GenerateSeriesVTabModule(ext_api); } } macro_rules! extract_arg_integer { ($args:expr, $idx:expr) => { $args.get($idx).and_then(|v| v.to_integer()) }; } /// A virtual table that generates a sequence of integers #[derive(Debug, VTabModuleDerive, Default)] struct GenerateSeriesVTabModule; impl VTabModule for GenerateSeriesVTabModule { type Table = GenerateSeriesTable; const NAME: &'static str = "generate_series"; const VTAB_KIND: VTabKind = VTabKind::TableValuedFunction; fn create(_args: &[Value]) -> Result<(String, Self::Table), ResultCode> { let schema = "CREATE TABLE generate_series ( value INTEGER, start INTEGER HIDDEN, stop INTEGER HIDDEN, step INTEGER HIDDEN )" .into(); Ok((schema, GenerateSeriesTable {})) } } struct GenerateSeriesTable {} impl VTable for GenerateSeriesTable { type Cursor = GenerateSeriesCursor; type Error = ResultCode; fn open(&self, _conn: Option>) -> Result { Ok(GenerateSeriesCursor { start: 0, stop: 0, step: 0, current: 0, }) } fn best_index( constraints: &[ConstraintInfo], _order_by: &[OrderByInfo], ) -> Result { const START_COLUMN_INDEX: u32 = 1; const STEP_COLUMN_INDEX: u32 = 3; // The bits of `idx_num` are used to indicate which arguments are available to the filter method: // - Bit 0 set -> 'start' is available // - Bit 1 set -> 'stop' is available // - Bit 2 set -> 'step' is available let mut idx_num = 0; let mut positions = [None; 4]; // maps column index to constraint position let mut start_exists = false; let mut usable = true; for (i, c) in constraints.iter().enumerate() { if c.column_index == START_COLUMN_INDEX && c.op == ConstraintOp::Eq { start_exists = true; } if c.column_index >= START_COLUMN_INDEX && c.column_index <= STEP_COLUMN_INDEX { if !c.usable { usable = false; } else if c.op == ConstraintOp::Eq { let bit = 1 << (c.column_index - 1); idx_num |= bit; positions[c.column_index as usize] = Some(i); } } } if !start_exists { return Err(ResultCode::InvalidArgs); } if !usable { return Err(ResultCode::ConstraintViolation); } // Assign argv indexes contiguously let mut argv_idx = 1; let mut argv_indexes = [None; 4]; for (i, pos) in positions.iter().enumerate() { if pos.is_some() { argv_indexes[i] = Some(argv_idx); argv_idx += 1; } } let constraint_usages = constraints .iter() .enumerate() .map(|(idx, c)| { let argv_index = positions.get(c.column_index as usize).and_then(|&pos| { pos.filter(|&i| i == idx) .and_then(|_| argv_indexes[c.column_index as usize]) }); ConstraintUsage { argv_index, omit: argv_index.is_some(), } }) .collect(); Ok(IndexInfo { idx_num, idx_str: Some(idx_num.to_string()), constraint_usages, ..Default::default() }) } } /// The cursor for iterating over the generated sequence #[derive(Debug)] struct GenerateSeriesCursor { start: i64, stop: i64, step: i64, current: i64, } impl GenerateSeriesCursor { /// Returns true if this is an ascending series (positive step) but start > stop fn is_invalid_ascending_series(&self) -> bool { self.step > 0 && self.start > self.stop } /// Returns true if this is a descending series (negative step) but start < stop fn is_invalid_descending_series(&self) -> bool { self.step < 0 && self.start < self.stop } /// Returns true if this is an invalid range that should produce an empty series fn is_invalid_range(&self) -> bool { self.is_invalid_ascending_series() || self.is_invalid_descending_series() } /// Returns true if we would exceed the stop value in the current direction fn would_exceed(&self) -> bool { (self.step > 0 && self.current.saturating_add(self.step) > self.stop) || (self.step < 0 && self.current.saturating_add(self.step) < self.stop) } } impl VTabCursor for GenerateSeriesCursor { type Error = ResultCode; fn filter(&mut self, args: &[Value], idx_info: Option<(&str, i32)>) -> ResultCode { let mut start: Option = None; let mut stop: Option = None; let mut step = 1; // SQLite default for stop when it is omitted const DEFAULT_STOP_OMITTED: Option = Some(u32::MAX as i64); if let Some((_, idx_num)) = idx_info { let mut arg_idx = 0; // For the semantics of `idx_num`, see the comment in the `best_index` method. if idx_num & 1 != 0 { start = extract_arg_integer!(args, arg_idx); arg_idx += 1; } if idx_num & 2 != 0 { stop = extract_arg_integer!(args, arg_idx); arg_idx += 1; } else { stop = DEFAULT_STOP_OMITTED; } if idx_num & 4 != 0 { step = args .get(arg_idx) .map(|v| v.to_integer().unwrap_or(1)) .unwrap_or(1); } } if start.is_none() { return ResultCode::InvalidArgs; } if stop.is_none() { return ResultCode::EOF; // Sqlite returns an empty series for wacky args } // Convert zero step to 1, matching SQLite behavior if step == 0 { step = 1; } self.start = start.unwrap(); self.step = step; self.stop = stop.unwrap(); // Set initial value based on range validity // For invalid input SQLite returns an empty series self.current = if self.is_invalid_range() { return ResultCode::EOF; } else { self.start }; ResultCode::OK } fn next(&mut self) -> ResultCode { if self.eof() { return ResultCode::EOF; } self.current = match self.current.checked_add(self.step) { Some(val) => val, None => { return ResultCode::EOF; } }; ResultCode::OK } fn eof(&self) -> bool { // Check for invalid ranges (empty series) first if self.is_invalid_range() { return true; } // Check if we would exceed the stop value in the current direction if self.would_exceed() { return true; } if self.current == i64::MAX && self.step > 0 { return true; } if self.current == i64::MIN && self.step < 0 { return true; } false } fn column(&self, idx: u32) -> Result { Ok(match idx { 0 => Value::from_integer(self.current), 1 => Value::from_integer(self.start), 2 => Value::from_integer(self.stop), 3 => Value::from_integer(self.step), _ => Value::null(), }) } fn rowid(&self) -> i64 { let sub = self.current.saturating_sub(self.start); // Handle overflow in rowid calculation by capping at MAX/MIN match sub.checked_div(self.step) { Some(val) => val.saturating_add(1), None => { if self.step > 0 { i64::MAX } else { i64::MIN } } } } } #[cfg(test)] mod tests { use super::*; use quickcheck::{Arbitrary, Gen}; use quickcheck_macros::quickcheck; #[derive(Debug, Clone)] struct Series { start: i64, stop: i64, step: i64, } impl Arbitrary for Series { fn arbitrary(g: &mut Gen) -> Self { let mut start = i64::arbitrary(g); let mut stop = i64::arbitrary(g); let mut iters = 0; while stop.checked_sub(start).is_none() { start = i64::arbitrary(g); stop = i64::arbitrary(g); iters += 1; if iters > 1000 { panic!("Failed to generate valid range after 1000 attempts"); } } // step should be a reasonable value proportional to the range let mut divisor = i8::arbitrary(g); if divisor == 0 { divisor = 1; } let step = (stop - start).saturating_abs() / divisor as i64; Series { start, stop, step } } } // Helper function to collect all values from a cursor, returns Result with error code fn collect_series(series: Series) -> Result, ResultCode> { let tbl = GenerateSeriesTable {}; let mut cursor = tbl.open(None)?; // Create args array for filter let args = vec![ Value::from_integer(series.start), Value::from_integer(series.stop), Value::from_integer(series.step), ]; // Initialize cursor through filter match cursor.filter(&args, Some(("idx", 1 | 2 | 4))) { ResultCode::OK => (), ResultCode::EOF => return Ok(vec![]), err => return Err(err), } let mut values = Vec::new(); loop { values.push(cursor.column(0)?.to_integer().unwrap()); if values.len() > 1000 { panic!( "Generated more than 1000 values, expected this many: {:?}", (series.stop - series.start) / series.step + 1 ); } match cursor.next() { ResultCode::OK => (), ResultCode::EOF => break, err => return Err(err), } } Ok(values) } #[quickcheck] /// Test that the series length is correct /// Example: /// start = 1, stop = 10, step = 1 /// expected length = 10 fn prop_series_length(series: Series) { let start = series.start; let stop = series.stop; let step = series.step; let values = collect_series(series.clone()).unwrap_or_else(|e| { panic!("Failed to generate series for start={start}, stop={stop}, step={step}: {e:?}") }); if series_is_invalid_or_empty(&series) { assert!( values.is_empty(), "Series should be empty for invalid range: start={start}, stop={stop}, step={step}, got {values:?}" ); } else { let expected_len = series_expected_length(&series); assert_eq!( values.len(), expected_len, "Series length mismatch for start={}, stop={}, step={}: expected {}, got {}, values: {:?}", start, stop, step, expected_len, values.len(), values ); } } #[quickcheck] /// Test that the series is monotonically increasing /// Example: /// start = 1, stop = 10, step = 1 /// expected series = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] fn prop_series_monotonic_increasing_or_decreasing(series: Series) { let start = series.start; let stop = series.stop; let step = series.step; let values = collect_series(series.clone()).unwrap_or_else(|e| { panic!("Failed to generate series for start={start}, stop={stop}, step={step}: {e:?}") }); if series_is_invalid_or_empty(&series) { assert!( values.is_empty(), "Series should be empty for invalid range: start={start}, stop={stop}, step={step}" ); } else { assert!( values .windows(2) .all(|w| if step > 0 { w[0] < w[1] } else { w[0] > w[1] }), "Series not monotonically {}: {:?} (start={}, stop={}, step={})", if step > 0 { "increasing" } else { "decreasing" }, values, start, stop, step ); } } #[quickcheck] /// Test that the series step size is consistent /// Example: /// start = 1, stop = 10, step = 1 /// expected step size = 1 fn prop_series_step_size(series: Series) { let start = series.start; let stop = series.stop; let step = series.step; let values = collect_series(series.clone()).unwrap_or_else(|e| { panic!("Failed to generate series for start={start}, stop={stop}, step={step}: {e:?}") }); if series_is_invalid_or_empty(&series) { assert!( values.is_empty(), "Series should be empty for invalid range: start={start}, stop={stop}, step={step}" ); } else if !values.is_empty() { assert!( values .windows(2) .all(|w| (w[1].saturating_sub(w[0])).abs() == step.abs()), "Step size not consistent: {:?} (expected step size: {})", values .windows(2) .map(|w| w[1].saturating_sub(w[0])) .collect::>(), step.abs() ); } } #[quickcheck] /// Test that the series bounds are correct /// Example: /// start = 1, stop = 10, step = 1 /// expected bounds = [1, 10] fn prop_series_bounds(series: Series) { let start = series.start; let stop = series.stop; let step = series.step; let values = collect_series(series.clone()).unwrap_or_else(|e| { panic!("Failed to generate series for start={start}, stop={stop}, step={step}: {e:?}") }); if series_is_invalid_or_empty(&series) { assert!( values.is_empty(), "Series should be empty for invalid range: start={start}, stop={stop}, step={step}" ); } else if !values.is_empty() { assert_eq!( values.first(), Some(&start), "Series doesn't start with start value: {values:?} (expected start: {start})" ); assert!( values.last().is_none_or(|&last| if step > 0 { last <= stop } else { last >= stop }), "Series exceeds stop value: {values:?} (stop: {stop})" ); } } #[test] fn test_series_empty_positive_step() { let values = collect_series(Series { start: 10, stop: 5, step: 1, }) .expect("Failed to generate series"); assert!( values.is_empty(), "Series should be empty when start > stop with positive step" ); } #[test] fn test_series_empty_negative_step() { let values = collect_series(Series { start: 5, stop: 10, step: -1, }) .expect("Failed to generate series"); assert!( values.is_empty(), "Series should be empty when start < stop with negative step" ); } #[test] fn test_series_single_element() { let values = collect_series(Series { start: 5, stop: 5, step: 1, }) .expect("Failed to generate single element series"); assert_eq!( values, vec![5], "Single element series should contain only the start value" ); } #[test] fn test_zero_step_is_interpreted_as_1() { let values = collect_series(Series { start: 1, stop: 10, step: 0, }) .expect("Failed to generate series"); assert_eq!( values, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "Zero step should be interpreted as 1" ); } #[test] fn test_invalid_inputs() { // Test that invalid ranges return empty series instead of errors let values = collect_series(Series { start: 10, stop: 1, step: 1, }) .expect("Failed to generate series"); assert!( values.is_empty(), "Invalid positive range should return empty series, got {values:?}" ); let values = collect_series(Series { start: 1, stop: 10, step: -1, }) .expect("Failed to generate series"); assert!( values.is_empty(), "Invalid negative range should return empty series" ); // Test that extreme ranges return empty series let values = collect_series(Series { start: i64::MAX, stop: i64::MIN, step: 1, }) .expect("Failed to generate series"); assert!( values.is_empty(), "Extreme range (MAX to MIN) should return empty series" ); let values = collect_series(Series { start: i64::MIN, stop: i64::MAX, step: -1, }) .expect("Failed to generate series"); assert!( values.is_empty(), "Extreme range (MIN to MAX) should return empty series" ); } #[quickcheck] /// Test that rowid is always monotonically increasing regardless of step direction fn prop_series_rowid_monotonic(series: Series) { let start = series.start; let stop = series.stop; let step = series.step; let tbl = GenerateSeriesTable {}; let mut cursor = tbl.open(None).unwrap(); let args = vec![ Value::from_integer(start), Value::from_integer(stop), Value::from_integer(step), ]; // Initialize cursor through filter cursor.filter(&args, Some(("idx", 1 | 2 | 4))); let mut rowids = vec![]; while !cursor.eof() { let cur_rowid = cursor.rowid(); match cursor.next() { ResultCode::OK => rowids.push(cur_rowid), ResultCode::EOF => break, err => { panic!("Unexpected error {err:?} for start={start}, stop={stop}, step={step}") } } } assert!( rowids.windows(2).all(|w| w[1] == w[0] + 1), "Rowids not monotonically increasing: {rowids:?} (start={start}, stop={stop}, step={step})" ); } #[quickcheck] /// Test that empty series are handled consistently fn prop_series_empty(series: Series) { let start = series.start; let stop = series.stop; let step = series.step; let values = collect_series(series.clone()).unwrap_or_else(|e| { panic!("Failed to generate series for start={start}, stop={stop}, step={step}: {e:?}") }); if series_is_invalid_or_empty(&series) { assert!( values.is_empty(), "Series should be empty for invalid range: start={start}, stop={stop}, step={step}" ); } else if start == stop { assert_eq!( values, vec![start], "Series with start==stop should contain exactly one element" ); } } fn series_is_invalid_or_empty(series: &Series) -> bool { let start = series.start; let stop = series.stop; let step = series.step; (start > stop && step > 0) || (start < stop && step < 0) || (step == 0 && start != stop) } fn series_expected_length(series: &Series) -> usize { let start = series.start; let stop = series.stop; let step = series.step; if step == 0 { if start == stop { 1 } else { 0 } } else { ((stop.saturating_sub(start)).saturating_div(step)).saturating_add(1) as usize } } #[test] fn test_best_index_argv_order_all_constraints() { // Test when start, stop, and step constraints are present let constraints = vec![ usable_constraint(1), // start usable_constraint(2), // stop usable_constraint(3), // step ]; let index_info = GenerateSeriesTable::best_index(&constraints, &[]).unwrap(); // Verify start gets argv_index 1, stop gets 2, step gets 3 assert_eq!(index_info.constraint_usages[0].argv_index, Some(1)); // start assert_eq!(index_info.constraint_usages[1].argv_index, Some(2)); // stop assert_eq!(index_info.constraint_usages[2].argv_index, Some(3)); // step assert_eq!(index_info.idx_num, 7); // All bits set (1 | 2 | 4) } #[test] fn test_best_index_argv_order_start_stop_only() { let constraints = vec![ usable_constraint(1), // start usable_constraint(2), // stop ]; let index_info = GenerateSeriesTable::best_index(&constraints, &[]).unwrap(); // Verify start gets argv_index 1, stop gets 2 assert_eq!(index_info.constraint_usages[0].argv_index, Some(1)); // start assert_eq!(index_info.constraint_usages[1].argv_index, Some(2)); // stop assert_eq!(index_info.idx_num, 3); // Bits 0 and 1 set (1 | 2) } #[test] fn test_best_index_argv_order_only_start() { let constraints = vec![ usable_constraint(1), // start ]; let index_info = GenerateSeriesTable::best_index(&constraints, &[]).unwrap(); // Verify start gets argv_index 1 assert_eq!(index_info.constraint_usages[0].argv_index, Some(1)); // start assert_eq!(index_info.idx_num, 1); // Only bit 0 set } #[test] fn test_best_index_argv_order_reverse_constraint_order() { // Test when constraints are provided in reverse order (step, stop, start) let constraints = vec![ usable_constraint(3), // step usable_constraint(2), // stop usable_constraint(1), // start ]; let index_info = GenerateSeriesTable::best_index(&constraints, &[]).unwrap(); // Verify start still gets argv_index 1, stop gets 2, step gets 3 regardless of constraint order assert_eq!(index_info.constraint_usages[0].argv_index, Some(3)); // step assert_eq!(index_info.constraint_usages[1].argv_index, Some(2)); // stop assert_eq!(index_info.constraint_usages[2].argv_index, Some(1)); // start assert_eq!(index_info.idx_num, 7); // All bits set (1 | 2 | 4) } #[test] fn test_best_index_argv_order_missing_start() { // Test when start constraint is missing but stop and step are present let constraints = vec![ usable_constraint(2), // stop usable_constraint(3), // step ]; let result = GenerateSeriesTable::best_index(&constraints, &[]); assert!(matches!(result, Err(ResultCode::InvalidArgs))); } #[test] fn test_best_index_no_usable_constraints() { let constraints = vec![ConstraintInfo { column_index: 1, op: ConstraintOp::Eq, usable: false, index: 0, }]; let result = GenerateSeriesTable::best_index(&constraints, &[]); assert!(matches!(result, Err(ResultCode::ConstraintViolation))); } fn usable_constraint(column_index: u32) -> ConstraintInfo { ConstraintInfo { column_index, op: ConstraintOp::Eq, usable: true, index: 0, } } } ================================================ FILE: core/state_machine.rs ================================================ use crate::{ types::{IOCompletions, IOResult}, Result, }; pub enum TransitionResult { Io(IOCompletions), Continue, Done(Result), } /// A generic trait for state machines. pub trait StateTransition { type Context; type SMResult; /// Transition the state machine to the next state. /// /// Returns `TransitionResult::Io` if the state machine needs to perform an IO operation. /// Returns `TransitionResult::Continue` if the state machine needs to continue. /// Returns `TransitionResult::Done` if the state machine is done. fn step(&mut self, context: &Self::Context) -> Result>; /// Finalize the state machine. /// /// This is called when the state machine is done. fn finalize(&mut self, context: &Self::Context) -> Result<()>; /// Check if the state machine is finalized. fn is_finalized(&self) -> bool; } #[derive(Debug)] pub struct StateMachine { state: State, is_finalized: bool, } /// A generic state machine that loops calling `transition` until it returns `TransitionResult::Done` or `TransitionResult::Io`. impl StateMachine { pub fn new(state: State) -> Self { Self { state, is_finalized: false, } } pub fn step(&mut self, context: &State::Context) -> Result> { loop { if self.is_finalized { unreachable!("StateMachine::transition: state machine is finalized"); } match self.state.step(context)? { TransitionResult::Io(io) => { return Ok(IOResult::IO(io)); } TransitionResult::Continue => { continue; } TransitionResult::Done(result) => { assert!(self.state.is_finalized()); self.is_finalized = true; return Ok(IOResult::Done(result)); } } } } pub fn finalize(&mut self, context: &State::Context) -> Result<()> { self.state.finalize(context)?; self.is_finalized = true; Ok(()) } pub fn is_finalized(&self) -> bool { self.is_finalized } } ================================================ FILE: core/statement.rs ================================================ use crate::turso_assert_eq; use std::{ borrow::Cow, num::NonZero, ops::Deref, sync::{atomic::Ordering, Arc}, task::Waker, }; use tracing::{instrument, Level}; use turso_parser::{ ast::{fmt::ToTokens, Cmd}, parser::Parser, }; use crate::{ busy::BusyHandlerState, parameters, schema::Trigger, stats::refresh_analyze_stats, translate::{self, display::PlanContext, emitter::TransactionMode}, vdbe::{ self, explain::{EXPLAIN_COLUMNS_TYPE, EXPLAIN_QUERY_PLAN_COLUMNS_TYPE}, }, LimboError, MvStore, Pager, QueryMode, Result, Value, EXPLAIN_COLUMNS, EXPLAIN_QUERY_PLAN_COLUMNS, }; type ProgramExecutionState = vdbe::ProgramExecutionState; type Row = vdbe::Row; type StepResult = vdbe::StepResult; pub struct Statement { pub(crate) program: vdbe::Program, state: vdbe::ProgramState, pager: Arc, /// indicates if the statement is a NORMAL/EXPLAIN/EXPLAIN QUERY PLAN query_mode: QueryMode, /// Flag to show if the statement was busy busy: bool, /// Busy handler state for tracking invocations and timeouts busy_handler_state: Option, /// True once step() has returned Row for a write statement (INSERT/UPDATE/DELETE /// with RETURNING). With ephemeral-buffered RETURNING, the first Row proves all /// DML completed — only the scan-back remains. Used by reset_internal to decide /// commit vs rollback when a statement is abandoned. has_returned_row: bool, /// Byte offset in the original SQL string where this statement ends. /// Used by sqlite3_prepare_v2 to set the *pzTail output parameter. tail_offset: usize, } crate::assert::assert_send_sync!(Statement); impl std::fmt::Debug for Statement { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Statement").finish() } } impl Drop for Statement { fn drop(&mut self) { self.reset_best_effort(); } } impl Statement { pub fn new( program: vdbe::Program, pager: Arc, query_mode: QueryMode, tail_offset: usize, ) -> Self { let (max_registers, cursor_count) = match query_mode { QueryMode::Normal => (program.max_registers, program.cursor_ref.len()), QueryMode::Explain => (EXPLAIN_COLUMNS.len(), 0), QueryMode::ExplainQueryPlan => (EXPLAIN_QUERY_PLAN_COLUMNS.len(), 0), }; let state = vdbe::ProgramState::new(max_registers, cursor_count); Self { program, state, pager, query_mode, busy: false, busy_handler_state: None, has_returned_row: false, tail_offset, } } pub fn tail_offset(&self) -> usize { self.tail_offset } pub fn get_trigger(&self) -> Option> { self.program.trigger.clone() } pub fn get_query_mode(&self) -> QueryMode { self.query_mode } pub fn get_program(&self) -> &vdbe::Program { &self.program } pub fn get_pager(&self) -> &Arc { &self.pager } pub fn n_change(&self) -> i64 { self.state .n_change .load(crate::sync::atomic::Ordering::SeqCst) } pub fn set_mv_tx(&mut self, mv_tx: Option<(u64, TransactionMode)>) { self.program.connection.set_mv_tx(mv_tx); } pub fn interrupt(&mut self) { self.state.interrupt(); } pub fn execution_state(&self) -> ProgramExecutionState { self.state.execution_state } /// Current statement-level metrics (rows read, VM steps, etc.). pub fn metrics(&self) -> &vdbe::metrics::StatementMetrics { &self.state.metrics } pub fn mv_store(&self) -> impl Deref>> { self.program.connection.mv_store() } /// Take the pending IO completions from this statement. /// Returns None if no IO is pending. /// This is used by async state machines that need to yield the completions. pub fn take_io_completions(&mut self) -> Option { self.state.io_completions.take() } fn _step(&mut self, waker: Option<&Waker>) -> Result { if matches!(self.state.execution_state, ProgramExecutionState::Init) && !self .program .prepare_context .matches_connection(&self.program.connection) { self.reprepare()?; } // If we're waiting for a busy handler timeout, check if we can proceed if let Some(busy_state) = self.busy_handler_state.as_ref() { if self.pager.io.current_time_monotonic() < busy_state.timeout() { // Yield the query as the timeout has not been reached yet if let Some(waker) = waker { waker.wake_by_ref(); } return Ok(StepResult::IO); } } const MAX_SCHEMA_RETRY: usize = 50; let mut res = self .program .step(&mut self.state, &self.pager, self.query_mode, waker); for attempt in 0..MAX_SCHEMA_RETRY { // Only reprepare if we still need to update schema if !matches!(res, Err(LimboError::SchemaUpdated)) { break; } tracing::debug!("reprepare: attempt={}", attempt); self.reprepare()?; res = self .program .step(&mut self.state, &self.pager, self.query_mode, waker); } // Aggregate metrics when statement completes if matches!(res, Ok(StepResult::Done)) { self.program .connection .metrics .write() .record_statement(&self.state.metrics); self.busy = false; self.busy_handler_state = None; // Reset busy state on completion // After ANALYZE completes, refresh in-memory stats so planners can use them. let sql = self.program.sql.trim_start().as_bytes(); if sql.len() >= 7 && sql[..7].eq_ignore_ascii_case(b"ANALYZE") { refresh_analyze_stats(&self.program.connection); } } else { self.busy = true; } // Handle busy result by invoking the busy handler if matches!(res, Ok(StepResult::Busy)) { let now = self.pager.io.current_time_monotonic(); let handler = self.program.connection.get_busy_handler(); // Initialize or get existing busy handler state let busy_state = self .busy_handler_state .get_or_insert_with(|| BusyHandlerState::new(now)); // Invoke the busy handler to determine if we should retry if busy_state.invoke(&handler, now) { // Handler says retry, yield with IO to wait for timeout if let Some(waker) = waker { waker.wake_by_ref(); } res = Ok(StepResult::IO); #[cfg(shuttle)] crate::thread::spin_loop(); } // else: Handler says stop, res stays as Busy } // Track when a write statement yields its first Row. With ephemeral-buffered // RETURNING, this proves all DML completed — only the scan-back remains. if matches!(res, Ok(StepResult::Row)) && self.query_mode == QueryMode::Normal && self.program.change_cnt_on && !self.program.result_columns.is_empty() { self.has_returned_row = true; } res } #[inline] pub fn step(&mut self) -> Result { self._step(None) } #[inline] pub fn step_with_waker(&mut self, waker: &Waker) -> Result { self._step(Some(waker)) } pub fn run_ignore_rows(&mut self) -> Result<()> { loop { match self.step()? { vdbe::StepResult::Done => return Ok(()), vdbe::StepResult::IO => self.pager.io.step()?, vdbe::StepResult::Row => continue, vdbe::StepResult::Interrupt | vdbe::StepResult::Busy => { return Err(LimboError::Busy) } } } } pub fn run_collect_rows(&mut self) -> Result>> { let mut values = Vec::new(); loop { match self.step()? { vdbe::StepResult::Done => return Ok(values), vdbe::StepResult::IO => self.pager.io.step()?, vdbe::StepResult::Row => { values.push(self.row().unwrap().get_values().cloned().collect()); continue; } vdbe::StepResult::Interrupt | vdbe::StepResult::Busy => { return Err(LimboError::Busy) } } } } /// Blocks execution, advances IO, and runs to completion of the statement pub fn run_with_row_callback( &mut self, mut func: impl FnMut(&Row) -> Result<()>, ) -> Result<()> { loop { match self.step()? { vdbe::StepResult::Done => break, vdbe::StepResult::IO => self.pager.io.step()?, vdbe::StepResult::Row => { func(self.row().expect("row should be present"))?; } vdbe::StepResult::Interrupt => return Err(LimboError::Interrupt), vdbe::StepResult::Busy => return Err(LimboError::Busy), } } Ok(()) } /// Blocks execution, advances IO, and stops at any StepResult except IO /// You can optionally pass a handler to run after IO is advanced pub fn run_one_step_blocking( &mut self, mut pre_io_func: impl FnMut() -> Result<()>, mut post_io_func: impl FnMut() -> Result<()>, ) -> Result> { let result = loop { match self.step()? { vdbe::StepResult::Done => break None, vdbe::StepResult::IO => { pre_io_func()?; self.pager.io.step()?; post_io_func()?; } vdbe::StepResult::Row => break Some(self.row().expect("row should be present")), vdbe::StepResult::Interrupt => return Err(LimboError::Interrupt), vdbe::StepResult::Busy => return Err(LimboError::Busy), } }; Ok(result) } #[instrument(skip_all, level = Level::DEBUG)] fn reprepare(&mut self) -> Result<()> { tracing::trace!("repreparing statement"); let conn = self.program.connection.clone(); // End transactions on attached database pagers so they get a fresh view // of the database. Without this, the pager would still see the old page 1 // with the stale schema cookie, causing an infinite SchemaUpdated loop. // SchemaUpdated can occur at different points in the Transaction opcode, // so the attached pager may or may not hold locks at this point. let attached_db_ids: Vec = self .program .prepared .write_databases .iter() .chain(self.program.prepared.read_databases.iter()) .filter(|&&id| crate::is_attached_db(id)) .copied() .collect(); for db_id in attached_db_ids { let pager = conn.get_pager_from_database_index(&db_id); // Discard any connection-local schema changes for this attached DB // so the re-translate reads the committed schema. conn.database_schemas().write().remove(&db_id); if pager.holds_read_lock() { pager.rollback_attached(); } } *conn.schema.write() = conn.db.clone_schema(); self.program = { let mut parser = Parser::new(self.program.sql.as_bytes()); let cmd = parser.next_cmd()?; let cmd = cmd.expect("Same SQL string should be able to be parsed"); let syms = conn.syms.read(); let mode = self.query_mode; #[cfg(debug_assertions)] turso_assert_eq!(QueryMode::new(&cmd), mode); let (Cmd::Stmt(stmt) | Cmd::Explain(stmt) | Cmd::ExplainQueryPlan(stmt)) = cmd; let schema = conn.schema.read().clone(); translate::translate( &schema, stmt, self.pager.clone(), conn.clone(), &syms, mode, &self.program.sql, )? }; // Save parameters before they are reset let parameters = std::mem::take(&mut self.state.parameters); let (max_registers, cursor_count) = match self.query_mode { QueryMode::Normal => (self.program.max_registers, self.program.cursor_ref.len()), QueryMode::Explain => (EXPLAIN_COLUMNS.len(), 0), QueryMode::ExplainQueryPlan => (EXPLAIN_QUERY_PLAN_COLUMNS.len(), 0), }; self.reset_internal(Some(max_registers), Some(cursor_count))?; // Load the parameters back into the state self.state.parameters = parameters; Ok(()) } pub fn num_columns(&self) -> usize { match self.query_mode { QueryMode::Normal => self.program.result_columns.len(), QueryMode::Explain => EXPLAIN_COLUMNS.len(), QueryMode::ExplainQueryPlan => EXPLAIN_QUERY_PLAN_COLUMNS.len(), } } pub fn get_column_name(&self, idx: usize) -> Cow<'_, str> { if self.query_mode == QueryMode::Explain { return Cow::Owned(EXPLAIN_COLUMNS.get(idx).expect("No column").to_string()); } if self.query_mode == QueryMode::ExplainQueryPlan { return Cow::Owned( EXPLAIN_QUERY_PLAN_COLUMNS .get(idx) .expect("No column") .to_string(), ); } match self.query_mode { QueryMode::Normal => { let column = &self.program.result_columns.get(idx).expect("No column"); match column.name(&self.program.table_references) { Some(name) => Cow::Borrowed(name), None => { let tables = [&self.program.table_references]; let ctx = PlanContext(&tables); Cow::Owned(column.expr.displayer(&ctx).to_string()) } } } QueryMode::Explain => Cow::Borrowed(EXPLAIN_COLUMNS[idx]), QueryMode::ExplainQueryPlan => Cow::Borrowed(EXPLAIN_QUERY_PLAN_COLUMNS[idx]), } } pub fn get_column_table_name(&self, idx: usize) -> Option> { if self.query_mode == QueryMode::Explain || self.query_mode == QueryMode::ExplainQueryPlan { return None; } let column = &self.program.result_columns.get(idx).expect("No column"); match &column.expr { turso_parser::ast::Expr::Column { table, .. } => self .program .table_references .find_table_by_internal_id(*table) .map(|(_, table_ref)| Cow::Borrowed(table_ref.get_name())), _ => None, } } /// Returns the declared type of a result column. /// /// This behaves similarly to SQLite's `sqlite3_column_decltype()`: /// If the Nth column of the returned result set of a SELECT is a table column /// (not an expression or subquery) then the declared type of the table column /// is returned. If the Nth column of the result set is an expression or subquery, /// then None is returned. The returned string is always UTF-8 encoded. /// /// See: pub fn get_column_decltype(&self, idx: usize) -> Option { if self.query_mode == QueryMode::Explain { return Some( EXPLAIN_COLUMNS_TYPE .get(idx) .expect("No column") .to_string(), ); } if self.query_mode == QueryMode::ExplainQueryPlan { return Some( EXPLAIN_QUERY_PLAN_COLUMNS_TYPE .get(idx) .expect("No column") .to_string(), ); } let column = &self.program.result_columns.get(idx).expect("No column"); match &column.expr { turso_parser::ast::Expr::Column { table, column: column_idx, .. } => { let (_, table_ref) = self .program .table_references .find_table_by_internal_id(*table)?; let table_column = table_ref.get_column_at(*column_idx)?; let ty_str = &table_column.ty_str; if ty_str.is_empty() { None } else { Some(ty_str.clone()) } } _ => None, } } /// Returns the type affinity name of a result column (e.g., "INTEGER", "TEXT", "REAL", "BLOB", "NUMERIC"). /// /// Unlike `get_column_decltype` which returns the original declared type string, /// this method returns the normalized SQLite type affinity name. pub fn get_column_type_name(&self, idx: usize) -> Option { if self.query_mode == QueryMode::Explain { return Some( EXPLAIN_COLUMNS_TYPE .get(idx) .expect("No column") .to_string(), ); } if self.query_mode == QueryMode::ExplainQueryPlan { return Some( EXPLAIN_QUERY_PLAN_COLUMNS_TYPE .get(idx) .expect("No column") .to_string(), ); } let column = &self.program.result_columns.get(idx).expect("No column"); match &column.expr { turso_parser::ast::Expr::Column { table, column: column_idx, .. } => { let (_, table_ref) = self .program .table_references .find_table_by_internal_id(*table)?; let table_column = table_ref.get_column_at(*column_idx)?; match &table_column.ty() { crate::schema::Type::Integer => Some("INTEGER".to_string()), crate::schema::Type::Real => Some("REAL".to_string()), crate::schema::Type::Text => Some("TEXT".to_string()), crate::schema::Type::Blob => Some("BLOB".to_string()), crate::schema::Type::Numeric => Some("NUMERIC".to_string()), crate::schema::Type::Null => None, } } _ => None, } } pub fn parameters(&self) -> ¶meters::Parameters { &self.program.parameters } pub fn parameters_count(&self) -> usize { self.program.parameters.count() } pub fn parameter_index(&self, name: &str) -> Option> { self.program.parameters.index(name) } pub fn bind_at(&mut self, index: NonZero, value: Value) { self.state.bind_at(index, value); } pub fn clear_bindings(&mut self) { self.state.clear_bindings(); } pub fn reset(&mut self) -> Result<()> { self.reset_internal(None, None) } pub fn reset_best_effort(&mut self) { match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.reset())) { Ok(Ok(())) => {} Ok(Err(err)) => { tracing::error!("Statement reset failed during best-effort cleanup: {err}"); } Err(_) => { tracing::error!("Statement reset panicked during best-effort cleanup"); } } } fn reset_internal( &mut self, max_registers: Option, max_cursors: Option, ) -> Result<()> { fn capture_reset_error( reset_error: &mut Option, err: LimboError, context: &str, ) { tracing::error!("{context}: {err}"); if reset_error.is_none() { *reset_error = Some(err); } } let mut reset_error: Option = None; if let Some(io) = self.state.io_completions.take() { if let Err(err) = io.wait(self.pager.io.as_ref()) { capture_reset_error( &mut reset_error, err, "Error while draining pending IO during statement reset", ); } } if self.state.execution_state.is_running() { if self.query_mode == QueryMode::Normal && self.program.change_cnt_on && self.has_returned_row { // Write statement with RETURNING, user got at least one Row. // With ephemeral-buffered RETURNING, ALL DML completed before any // rows were yielded. The remaining work is just the scan-back // (in-memory) + Halt. Commit the transaction via halt(). let mut halt_completed = false; loop { match vdbe::execute::halt( &self.program, &mut self.state, &self.pager, 0, "", None, ) { Ok(vdbe::execute::InsnFunctionStepResult::Done) => { halt_completed = true; break; } Ok(vdbe::execute::InsnFunctionStepResult::IO(_)) => { if let Err(e) = self.pager.io.step() { capture_reset_error( &mut reset_error, e, "Error committing during statement reset", ); break; } } Err(e) => { capture_reset_error( &mut reset_error, e, "Error halting statement during reset", ); break; } Ok(vdbe::execute::InsnFunctionStepResult::Row) | Ok(vdbe::execute::InsnFunctionStepResult::Step) => { capture_reset_error( &mut reset_error, LimboError::InternalError( "Unexpected halt result during reset".to_string(), ), "Statement reset encountered unexpected halt result", ); break; } } } if !halt_completed { if let Err(abort_err) = self.program .abort(&self.pager, reset_error.as_ref(), &mut self.state) { capture_reset_error( &mut reset_error, abort_err, "Abort failed during statement reset", ); } } } else { // Either a read-only statement, a write statement that never // yielded a Row (DML still in progress or hit Busy/error), or a // write statement without RETURNING. Rollback to avoid committing // partial DML or silently retrying after transient errors (Busy). if let Err(abort_err) = self.program.abort(&self.pager, None, &mut self.state) { capture_reset_error( &mut reset_error, abort_err, "Abort failed during statement reset", ); } } } else { // Statement not running (Done/Failed/Init) — cleanup only. if let Err(abort_err) = self.program.abort(&self.pager, None, &mut self.state) { capture_reset_error( &mut reset_error, abort_err, "Abort failed during statement reset", ); } } // Safety net: if end_statement wasn't reached (e.g. statement dropped // mid-execution), ensure n_active_writes is decremented before reset // clears the flag. if self.state.is_active_write { self.program .connection .n_active_writes .fetch_sub(1, Ordering::SeqCst); self.state.is_active_write = false; } self.state.reset(max_registers, max_cursors); self.state.n_change.store(0, Ordering::SeqCst); self.busy = false; self.busy_handler_state = None; self.has_returned_row = false; if let Some(err) = reset_error { return Err(err); } Ok(()) } pub fn row(&self) -> Option<&Row> { self.state.result_row.as_ref() } pub fn get_sql(&self) -> &str { &self.program.sql } pub fn is_busy(&self) -> bool { self.busy } /// Internal method to get IO from a statement. /// Used by select internal crate /// /// Avoid using this method for advancing IO while iteration over `step`. /// Prefer to use helper methods instead such as [Self::run_with_row_callback] pub fn _io(&self) -> &dyn crate::IO { self.pager.io.as_ref() } } ================================================ FILE: core/stats.rs ================================================ use crate::sync::Arc; use rustc_hash::FxHashMap as HashMap; use crate::schema::Schema; use crate::translate::emitter::TransactionMode; use crate::util::normalize_ident; use crate::{Connection, Result, Statement, TransactionState, Value}; pub const STATS_TABLE: &str = "sqlite_stat1"; const STATS_QUERY: &str = "SELECT tbl, idx, stat FROM sqlite_stat1"; /// Statistics produced by ANALYZE for a single index. #[derive(Clone, Debug, Default)] pub struct IndexStat { /// Estimated total number of rows in the table/index when the stat was collected. pub total_rows: Option, /// Average number of rows per distinct key prefix, for each leftmost prefix /// of the index columns. /// /// These values come directly from sqlite_stat1's stat column (after the /// first number which is total_rows). For a stat string "1000 100 10 1": /// - total_rows = 1000 /// - avg_rows_per_distinct_prefix = [100, 10, 1] /// /// Entry at position `i` means: on average, this many rows share the same /// values in the first `i + 1` columns of the index. Lower values indicate /// higher selectivity (more distinct prefixes). /// /// To compute number of distinct values (NDV) for prefix i: /// ndv = total_rows / avg_rows_per_distinct_prefix[i] pub avg_rows_per_distinct_prefix: Vec, } /// Statistics produced by ANALYZE for a single BTree table. #[derive(Clone, Debug, Default)] pub struct TableStat { /// Estimated row count for the table (sqlite_stat1 entry with a NULL index name). pub row_count: Option, /// Per-index statistics keyed by normalized index name. pub index_stats: HashMap, } impl TableStat { /// Get or create the per-index statistics bucket for the given index name. pub fn index_stats_mut(&mut self, index_name: &str) -> &mut IndexStat { let index_name = normalize_ident(index_name); self.index_stats.entry(index_name).or_default() } } /// Container for ANALYZE statistics across the schema. #[derive(Clone, Debug, Default)] pub struct AnalyzeStats { /// Per-table statistics keyed by normalized table name. pub tables: HashMap, } impl AnalyzeStats { pub fn needs_refresh(&self) -> bool { self.tables.is_empty() } /// Get the statistics for a table, if present. pub fn table_stats(&self, table_name: &str) -> Option<&TableStat> { let table_name = normalize_ident(table_name); self.tables.get(&table_name) } /// Get or create the statistics bucket for a table. pub fn table_stats_mut(&mut self, table_name: &str) -> &mut TableStat { let table_name = normalize_ident(table_name); self.tables.entry(table_name).or_default() } /// Remove all statistics for a table. pub fn remove_table(&mut self, table_name: &str) { let table_name = normalize_ident(table_name); self.tables.remove(&table_name); } /// Remove statistics for a specific index on a table. pub fn remove_index(&mut self, table_name: &str, index_name: &str) { let table_name = normalize_ident(table_name); let index_name = normalize_ident(index_name); if let Some(table_stats) = self.tables.get_mut(&table_name) { table_stats.index_stats.remove(&index_name); } } } /// Read sqlite_stat1 contents into an AnalyzeStats map without mutating schema. /// /// Only regular B-tree tables and indexes are considered. Virtual and ephemeral /// tables are ignored. pub fn gather_sqlite_stat1( conn: &Arc, schema: &Schema, mv_tx: Option<(u64, TransactionMode)>, ) -> Result { let mut stats = AnalyzeStats::default(); let mut stmt = conn.prepare(STATS_QUERY)?; stmt.set_mv_tx(mv_tx); load_sqlite_stat1_from_stmt(stmt, schema, &mut stats)?; Ok(stats) } /// Best-effort refresh analyze_stats on the connection's schema. pub fn refresh_analyze_stats(conn: &Arc) { if !conn.is_db_initialized() || conn.is_nested_stmt() { return; } if matches!(conn.get_tx_state(), TransactionState::Write { .. }) { return; } // Need a snapshot of the current schema to validate tables/indexes. let schema_snapshot = { conn.schema.read().clone() }; if schema_snapshot.get_btree_table(STATS_TABLE).is_none() { return; } let mv_tx = conn.get_mv_tx(); if let Ok(stats) = gather_sqlite_stat1(conn, &schema_snapshot, mv_tx) { conn.with_schema_mut(|schema| { schema.analyze_stats = stats; }); } } fn load_sqlite_stat1_from_stmt( mut stmt: Statement, schema: &Schema, stats: &mut AnalyzeStats, ) -> Result<()> { stmt.run_with_row_callback(|row| { let table_name = row.get::<&str>(0)?; let idx_value = row.get::<&Value>(1)?; let stat_value = row.get::<&Value>(2)?; let idx_name = match idx_value { Value::Null => None, Value::Text(s) => Some(s.as_str()), _ => None, }; let stat = match stat_value { Value::Text(s) => s.as_str(), _ => return Ok(()), }; // Skip if table is not a regular B-tree. if schema.get_btree_table(table_name).is_none() { return Ok(()); } let Some(numbers) = parse_stat_numbers(stat) else { return Ok(()); }; if numbers.is_empty() { return Ok(()); } if idx_name.is_none() { if let Some(total_rows) = numbers.first().copied() { stats.table_stats_mut(table_name).row_count = Some(total_rows); } return Ok(()); } // Index-level entry: only keep if the index exists on this table. let idx_name = normalize_ident(idx_name.unwrap()); if schema.get_index(table_name, &idx_name).is_none() { return Ok(()); } let total_rows = numbers.first().copied(); { let idx_stats = stats.table_stats_mut(table_name).index_stats_mut(&idx_name); idx_stats.total_rows = total_rows; idx_stats.avg_rows_per_distinct_prefix = numbers.iter().skip(1).copied().collect(); } // If we didn't see a table-level row yet, seed row_count from index stats. if let Some(total_rows) = total_rows { let table_stats = stats.table_stats_mut(table_name); if table_stats.row_count.is_none() { table_stats.row_count = Some(total_rows); } } Ok(()) })?; Ok(()) } fn parse_stat_numbers(stat: &str) -> Option> { stat.split_whitespace() .map(|s| s.parse::().ok()) .collect() } /// Statistics accumulator for ANALYZE. #[derive(Debug, Clone)] pub struct StatAccum { /// Number of columns in the index (not including rowid) pub n_col: usize, /// Total number of rows seen pub n_row: u64, /// Distinct counts for each column prefix. /// distinct[i] = number of distinct values for columns 0..=i pub distinct: Vec, } impl StatAccum { pub fn new(n_col: usize) -> Self { Self { n_col, n_row: 0, distinct: vec![0; n_col], } } /// Push a row, indicating which column (0-indexed) is the first to differ /// from the previous row. If this is the first row, pass 0. /// /// i_chng is the index of the leftmost column that changed: /// - 0 means column 0 changed (or first row) /// - 1 means columns 0 was same, column 1 changed /// - n_col means all columns were the same (duplicate row) pub fn push(&mut self, i_chng: usize) { self.n_row += 1; // Increment distinct counts for columns i_chng and onwards // because if column i changed, then prefixes (0..=i), (0..=i+1), etc. all have a new distinct value for i in i_chng..self.n_col { self.distinct[i] += 1; } } /// Get the stat1 string: "total avg1 avg2 ..." /// where avgN = ceil(total / distinctN) pub fn get_stat1(&self) -> String { if self.n_row == 0 { return String::new(); } let mut parts = vec![self.n_row.to_string()]; for &d in &self.distinct { let avg = if d > 0 { self.n_row.div_ceil(d) } else { self.n_row }; parts.push(avg.to_string()); } parts.join(" ") } /// Serialize to bytes for storage in a blob register. pub fn to_bytes(&self) -> Vec { let mut bytes = Vec::with_capacity(8 + 8 + 8 * self.n_col); bytes.extend_from_slice(&(self.n_col as u64).to_le_bytes()); bytes.extend_from_slice(&self.n_row.to_le_bytes()); for &d in &self.distinct { bytes.extend_from_slice(&d.to_le_bytes()); } bytes } /// Deserialize from bytes. pub fn from_bytes(bytes: &[u8]) -> Option { if bytes.len() < 16 { return None; } let n_col = u64::from_le_bytes(bytes[0..8].try_into().ok()?) as usize; let n_row = u64::from_le_bytes(bytes[8..16].try_into().ok()?); if bytes.len() < 16 + 8 * n_col { return None; } let mut distinct = Vec::with_capacity(n_col); for i in 0..n_col { let start = 16 + i * 8; let d = u64::from_le_bytes(bytes[start..start + 8].try_into().ok()?); distinct.push(d); } Some(Self { n_col, n_row, distinct, }) } } #[cfg(test)] mod tests { use super::parse_stat_numbers; #[test] fn parse_stat_numbers_basic() { assert_eq!(parse_stat_numbers("10 5 3 1").unwrap(), vec![10, 5, 3, 1]); assert_eq!(parse_stat_numbers(" 42\t7 ").unwrap(), vec![42, 7]); assert!(parse_stat_numbers("abc 1").is_none()); } } ================================================ FILE: core/storage/btree.rs ================================================ use branches::{mark_unlikely, unlikely}; use rustc_hash::FxHashMap as HashMap; #[cfg(debug_assertions)] use rustc_hash::FxHashSet as HashSet; use smallvec::SmallVec; use tracing::{instrument, Level}; use super::{ pager::PageRef, sqlite3_ondisk::{ write_varint_to_vec, IndexInteriorCell, IndexLeafCell, OverflowCell, MINIMUM_CELL_SIZE, }, }; use crate::{ io::CompletionGroup, io_yield_one, schema::Index, storage::{ pager::{BtreePageAllocMode, Pager}, sqlite3_ondisk::{ payload_overflows, read_u32, read_varint, write_varint, BTreeCell, DatabaseHeader, PageContent, PageSize, PageType, TableInteriorCell, TableLeafCell, CELL_PTR_SIZE_BYTES, FREELIST_LEAF_PTR_SIZE, FREELIST_TRUNK_HEADER_SIZE, FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR, FREELIST_TRUNK_OFFSET_LEAF_COUNT, FREELIST_TRUNK_OFFSET_NEXT_TRUNK_PTR, INTERIOR_PAGE_HEADER_SIZE_BYTES, LEAF_PAGE_HEADER_SIZE_BYTES, LEFT_CHILD_PTR_SIZE_BYTES, }, state_machines::{ AdvanceState, CountState, EmptyTableState, MoveToRightState, MoveToState, RewindState, SeekEndState, SeekToLastState, }, }, translate::plan::IterationDirection, turso_assert, types::{ find_compare, get_tie_breaker_from_seek_op, IOCompletions, IndexInfo, RecordCompare, SeekResult, }, util::IOExt, vdbe::Register, Completion, MvStore, }; use crate::{ numeric::Numeric, return_corrupt, return_if_io, types::{ compare_immutable_iter, AsValueRef, IOResult, ImmutableRecord, SeekKey, SeekOp, Value, ValueRef, }, LimboError, Result, }; use crate::{ turso_assert_eq, turso_assert_greater_than, turso_assert_greater_than_or_equal, turso_assert_less_than, turso_assert_less_than_or_equal, }; use std::{ any::Any, cmp::{Ordering, Reverse}, collections::BinaryHeap, fmt::Debug, ops::ControlFlow, pin::Pin, sync::Arc, }; /// Maximum number of key values to store on the stack when converting registers to ValueRefs /// during seeking. Since we use a SmallVec it'll gracefully fall back to heap allocating beyond /// this threshold. const STACK_ALLOC_KEY_VALS_MAX: usize = 16; /// The B-Tree page header is 12 bytes for interior pages and 8 bytes for leaf pages. /// /// +--------+-----------------+-----------------+-----------------+--------+----- ..... ----+ /// | Page | First Freeblock | Cell Count | Cell Content | Frag. | Right-most | /// | Type | Offset | | Area Start | Bytes | pointer | /// +--------+-----------------+-----------------+-----------------+--------+----- ..... ----+ /// 0 1 2 3 4 5 6 7 8 11 /// pub mod offset { /// Type of the B-Tree page (u8). pub const BTREE_PAGE_TYPE: usize = 0; /// A pointer to the first freeblock (u16). /// /// This field of the B-Tree page header is an offset to the first freeblock, or zero if /// there are no freeblocks on the page. A freeblock is a structure used to identify /// unallocated space within a B-Tree page, organized as a chain. /// /// Please note that freeblocks do not mean the regular unallocated free space to the left /// of the cell content area pointer, but instead blocks of at least 4 /// bytes WITHIN the cell content area that are not in use due to e.g. /// deletions. pub const BTREE_FIRST_FREEBLOCK: usize = 1; /// The number of cells in the page (u16). pub const BTREE_CELL_COUNT: usize = 3; /// A pointer to the first byte of cell allocated content from top (u16). /// /// A zero value for this integer is interpreted as 65,536. /// If a page contains no cells (which is only possible for a root page of a table that /// contains no rows) then the offset to the cell content area will equal the page size minus /// the bytes of reserved space. If the database uses a 65536-byte page size and the /// reserved space is zero (the usual value for reserved space) then the cell content offset of /// an empty page wants to be 6,5536 /// /// SQLite strives to place cells as far toward the end of the b-tree page as it can, in /// order to leave space for future growth of the cell pointer array. This means that the /// cell content area pointer moves leftward as cells are added to the page. pub const BTREE_CELL_CONTENT_AREA: usize = 5; /// The number of fragmented bytes (u8). /// /// Fragments are isolated groups of 1, 2, or 3 unused bytes within the cell content area. pub const BTREE_FRAGMENTED_BYTES_COUNT: usize = 7; /// The right-most pointer (saved separately from cells) (u32) pub const BTREE_RIGHTMOST_PTR: usize = 8; } /// Maximum depth of an SQLite B-Tree structure. Any B-Tree deeper than /// this will be declared corrupt. This value is calculated based on a /// maximum database size of 2^31 pages a minimum fanout of 2 for a /// root-node and 3 for all other internal nodes. /// /// If a tree that appears to be taller than this is encountered, it is /// assumed that the database is corrupt. pub const BTCURSOR_MAX_DEPTH: usize = 20; /// Maximum number of sibling pages that balancing is performed on. pub const MAX_SIBLING_PAGES_TO_BALANCE: usize = 3; /// We only need maximum 5 pages to balance 3 pages, because we can guarantee that cells from 3 pages will fit in 5 pages. pub const MAX_NEW_SIBLING_PAGES_AFTER_BALANCE: usize = 5; /// Validate cells in a page are in a valid state. Only in debug mode. macro_rules! debug_validate_cells { ($page_contents:expr, $usable_space:expr) => { #[cfg(debug_assertions)] { debug_validate_cells_core($page_contents, $usable_space); } }; } /// State machine of destroy operations /// Keep track of traversal so that it can be resumed when IO is encountered #[derive(Debug, Clone)] enum DestroyState { Start, LoadPage, ProcessPage, ClearOverflowPages { cell: BTreeCell }, FreePage, } struct DestroyInfo { state: DestroyState, } #[derive(Debug)] enum DeleteState { Start, DeterminePostBalancingSeekKey, LoadPage { post_balancing_seek_key: Option, }, FindCell { post_balancing_seek_key: Option, }, ClearOverflowPages { cell_idx: usize, cell: BTreeCell, original_child_pointer: Option, post_balancing_seek_key: Option, }, InteriorNodeReplacement { page: PageRef, /// the btree level of the page where the cell replacement happened. /// if the replacement causes the page to overflow/underflow, we need to remember it and balance it /// after the deletion process is otherwise complete. btree_depth: usize, cell_idx: usize, original_child_pointer: Option, post_balancing_seek_key: Option, }, CheckNeedsBalancing { /// same as `InteriorNodeReplacement::btree_depth` btree_depth: usize, post_balancing_seek_key: Option, interior_node_was_replaced: bool, }, /// If an interior node was replaced, we need to move back up from the subtree to the interior cell /// that now has the replaced content, so that the next invocation of BTreeCursor::next() does not /// stop at that cell. /// The reason it is important to land here is that the replaced cell was smaller (LT) than the deleted cell, /// so we must ensure we skip over it. I.e., when BTreeCursor::next() is called, it will move past the cell /// that holds the replaced content. /// See: https://github.com/tursodatabase/turso/issues/3045 PostInteriorNodeReplacement, Balancing { /// If provided, will also balance an ancestor page at depth `balance_ancestor_at_depth`. /// If not provided, balancing will stop as soon as a level is encountered where no balancing is required. balance_ancestor_at_depth: Option, }, RestoreContextAfterBalancing, } #[derive(Debug)] pub enum OverwriteCellState { /// Allocate a new payload for the cell. AllocatePayload, /// Fill the cell payload with the new payload. FillPayload { new_payload: Vec, rowid: Option, fill_cell_payload_state: FillCellPayloadState, }, /// Clear the old cell's overflow pages and add them to the freelist. /// Overwrite the cell with the new payload. ClearOverflowPagesAndOverwrite { new_payload: Vec, old_offset: usize, old_local_size: usize, }, } struct BalanceContext { pages_to_balance_new: [Option; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE], sibling_count_new: usize, cell_array: CellArray, old_cell_count_per_page_cumulative: [u16; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE], #[cfg(debug_assertions)] cells_debug: Vec>, } impl std::fmt::Debug for BalanceContext { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BalanceContext") .field("pages_to_balance_new", &self.pages_to_balance_new) .field("sibling_count_new", &self.sibling_count_new) .field("cell_array", &self.cell_array) .field( "old_cell_count_per_page_cumulative", &self.old_cell_count_per_page_cumulative, ) .finish() } } #[derive(Debug, Default)] /// State machine of a btree rebalancing operation. enum BalanceSubState { #[default] Start, BalanceRoot, Decide, Quick, /// Choose which sibling pages to balance (max 3). /// Generally, the siblings involved will be the page that triggered the balancing and its left and right siblings. /// The exceptions are: /// 1. If the leftmost page triggered balancing, up to 3 leftmost pages will be balanced. /// 2. If the rightmost page triggered balancing, up to 3 rightmost pages will be balanced. NonRootPickSiblings, /// Perform the actual balancing. This will result in 1-5 pages depending on the number of total cells to be distributed /// from the source pages. NonRootDoBalancing, NonRootDoBalancingAllocate { i: usize, context: Option, }, NonRootDoBalancingFinish { context: BalanceContext, }, /// Free pages that are not used anymore after balancing. FreePages { curr_page: usize, sibling_count_new: usize, }, } #[derive(Debug, Default)] struct BalanceState { sub_state: BalanceSubState, balance_info: Option, /// Reusable buffers for divider cell payloads. /// These persist across balance operations to avoid repeated allocations. /// We use Vec with clear/resize instead of allocating new each time. reusable_divider_buffers: [Vec; MAX_SIBLING_PAGES_TO_BALANCE - 1], /// Reusable Vec for CellArray cell_payloads to avoid per-balance allocation. /// Cleared before each use; grows as needed and retains capacity across operations. reusable_cell_payloads: Vec<&'static mut [u8]>, } /// State machine of a write operation. /// May involve balancing due to overflow. #[derive(Debug)] enum WriteState { Start, /// Overwrite an existing cell. /// In addition to deleting the old cell and writing a new one, /// we may also need to clear the old cell's overflow pages /// and add them to the freelist. Overwrite { page: PageRef, cell_idx: usize, // This is an Option although it's not optional; we `take` it as owned for [BTreeCursor::overwrite_cell] // to work around the borrow checker, and then insert it back if overwriting returns IO. state: Option, }, /// Insert a new cell. This path is taken when inserting a new row. Insert { page: PageRef, cell_idx: usize, new_payload: Vec, fill_cell_payload_state: FillCellPayloadState, }, Balancing, Finish, } struct ReadPayloadOverflow { payload: Vec, next_page: u32, remaining_to_read: usize, page: PageRef, } #[derive(Debug)] pub struct PinGuard(PageRef); impl PinGuard { pub fn new(p: PageRef) -> Self { p.pin(); Self(p) } } // Since every Drop will unpin, every clone // needs to add to the pin count impl Clone for PinGuard { fn clone(&self) -> Self { self.0.pin(); Self(self.0.clone()) } } impl PinGuard { pub fn to_page(&self) -> PageRef { self.0.clone() } } impl Drop for PinGuard { fn drop(&mut self) { self.0.try_unpin(); } } impl std::ops::Deref for PinGuard { type Target = PageRef; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(Clone, Debug)] pub enum BTreeKey<'a> { TableRowId((i64, Option<&'a ImmutableRecord>)), IndexKey(&'a ImmutableRecord), } impl BTreeKey<'_> { /// Create a new table rowid key from a rowid and an optional immutable record. /// The record is optional because it may not be available when the key is created. pub fn new_table_rowid(rowid: i64, record: Option<&ImmutableRecord>) -> BTreeKey<'_> { BTreeKey::TableRowId((rowid, record)) } /// Create a new index key from an immutable record. pub fn new_index_key(record: &ImmutableRecord) -> BTreeKey<'_> { BTreeKey::IndexKey(record) } /// Get the record, if present. Index will always be present, pub fn get_record(&self) -> Option<&'_ ImmutableRecord> { match self { BTreeKey::TableRowId((_, record)) => *record, BTreeKey::IndexKey(record) => Some(record), } } /// Get the rowid, if present. Index will never be present. pub fn maybe_rowid(&self) -> Option { match self { BTreeKey::TableRowId((rowid, _)) => Some(*rowid), BTreeKey::IndexKey(_) => None, } } /// Assert that the key is an integer rowid and return it. fn to_rowid(&self) -> i64 { match self { BTreeKey::TableRowId((rowid, _)) => *rowid, BTreeKey::IndexKey(_) => panic!("BTreeKey::to_rowid called on IndexKey"), } } } #[derive(Debug, Clone)] struct BalanceInfo { /// Old pages being balanced. We can have maximum 3 pages being balanced at the same time. pages_to_balance: [Option; MAX_SIBLING_PAGES_TO_BALANCE], /// Bookkeeping of the rightmost pointer so the offset::BTREE_RIGHTMOST_PTR can be updated. rightmost_pointer: *mut u8, /// Number of siblings being used to balance sibling_count: usize, /// First divider cell to remove that marks the first sibling first_divider_cell: usize, /// Reusable buffer for constructing new divider cells during balance. /// Avoids allocating a new Vec for each sibling during balance_non_root. reusable_divider_cell: Vec, } // SAFETY: Need to guarantee during balancing that we do not modify the rightmost pointer on the pointee `PageContent` // safe as long as the Balance Algorithm does not modify the pointer unsafe impl Send for BalanceInfo {} unsafe impl Sync for BalanceInfo {} /// Holds the state machine for the operation that was in flight when the cursor /// was suspended due to IO. enum CursorState { None, /// The cursor is in a write operation. Write(WriteState), Destroy(DestroyInfo), Delete(DeleteState), } impl CursorState { fn destroy_info(&self) -> Option<&DestroyInfo> { match self { CursorState::Destroy(x) => Some(x), _ => None, } } fn mut_destroy_info(&mut self) -> Option<&mut DestroyInfo> { match self { CursorState::Destroy(x) => Some(x), _ => None, } } } impl Debug for CursorState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Delete(..) => write!(f, "Delete"), Self::Destroy(..) => write!(f, "Destroy"), Self::None => write!(f, "None"), Self::Write(..) => write!(f, "Write"), } } } #[derive(Debug, Clone)] enum OverflowState { Start, ProcessPage { next_page: PageRef }, Done, } /// Holds a Record or RowId, so that these can be transformed into a SeekKey to restore /// cursor position to its previous location. #[derive(Debug)] pub enum CursorContextKey { TableRowId(i64), /// If we are in an index tree we can then reuse this field to save /// our cursor information IndexKeyRowId(ImmutableRecord), } #[derive(Debug)] pub struct CursorContext { pub key: CursorContextKey, pub seek_op: SeekOp, } impl CursorContext { fn seek_eq_only(key: &BTreeKey<'_>) -> Self { Self { key: key.into(), seek_op: SeekOp::GE { eq_only: true }, } } } impl From<&BTreeKey<'_>> for CursorContextKey { fn from(key: &BTreeKey<'_>) -> Self { match key { BTreeKey::TableRowId((rowid, _)) => CursorContextKey::TableRowId(*rowid), BTreeKey::IndexKey(record) => CursorContextKey::IndexKeyRowId((*record).clone()), } } } /// In the future, we may expand these general validity states #[derive(Debug, PartialEq, Eq)] pub enum CursorValidState { /// Cursor does not point to a valid entry, and Btree will never yield a record. Invalid, /// Cursor is pointing a to an existing location/cell in the Btree Valid, /// Cursor may be pointing to a non-existent location/cell. This can happen after balancing operations RequireSeek, /// Cursor requires an advance after a seek RequireAdvance(IterationDirection), } #[derive(Debug, Clone, Copy)] pub struct InteriorPageBinarySearchState { min_cell_idx: isize, max_cell_idx: isize, nearest_matching_cell: Option, eq_seen: bool, } #[derive(Debug, Clone, Copy)] pub struct LeafPageBinarySearchState { min_cell_idx: isize, max_cell_idx: isize, nearest_matching_cell: Option, /// Indicates if we have seen an exact match during the downwards traversal of the btree. /// This is only needed in index seeks, in cases where we need to determine whether we call /// an additional next()/prev() to fetch a matching record from an interior node. We will not /// do that if both are true: /// 1. We have not seen an EQ during the traversal /// 2. We are looking for an exact match ([SeekOp::GE] or [SeekOp::LE] with eq_only: true) eq_seen: bool, /// In multiple places, we do a seek that checks for an exact match (SeekOp::EQ) in the tree. /// In those cases, we need to know where to land if we don't find an exact match in the leaf page. /// For non-eq-only conditions (GT, LT, GE, LE), this is pretty simple: /// - If we are looking for GT/GE and don't find a match, we should end up beyond the end of the page (idx=cell count). /// - If we are looking for LT/LE and don't find a match, we should end up before the beginning of the page (idx=-1). /// /// For eq-only conditions (GE { eq_only: true } or LE { eq_only: true }), we need to know where to land if we don't find an exact match. /// For GE, we want to land at the first cell that is greater than the seek key. /// For LE, we want to land at the last cell that is less than the seek key. /// This is because e.g. when we attempt to insert rowid 666, we first check if it exists. /// If it doesn't, we want to land in the place where rowid 666 WOULD be inserted. target_cell_when_not_found: i32, } #[derive(Debug)] /// State used for seeking pub enum CursorSeekState { Start, MovingBetweenPages { eq_seen: bool, }, InteriorPageBinarySearch { state: InteriorPageBinarySearchState, }, FoundLeaf { eq_seen: bool, }, LeafPageBinarySearch { state: LeafPageBinarySearchState, }, } pub trait CursorTrait: Any + Send + Sync { /// Move cursor to last entry. fn last(&mut self) -> Result>; /// Move cursor to next entry. fn next(&mut self) -> Result>; /// Move cursor to previous entry. fn prev(&mut self) -> Result>; /// Get the rowid of the entry the cursor is poiting to if any fn rowid(&mut self) -> Result>>; /// Get the record of the entry the cursor is poiting to if any fn record(&mut self) -> Result>>; /// Move the cursor based on the key and the type of operation (op). fn seek(&mut self, key: SeekKey<'_>, op: SeekOp) -> Result>; /// Seek using registers directly without serializing them into an ImmutableRecord first. /// This avoids heap allocation and serialization overhead in hot paths like index lookups. fn seek_unpacked(&mut self, registers: &[Register], op: SeekOp) -> Result>; /// Insert a record in the position the cursor is at. fn insert(&mut self, key: &BTreeKey) -> Result>; /// Delete a record in the position the cursor is at. fn delete(&mut self) -> Result>; fn set_null_flag(&mut self, flag: bool); fn get_null_flag(&self) -> bool; /// Check if a key exists. fn exists(&mut self, key: &Value) -> Result>; fn clear_btree(&mut self) -> Result>>; fn btree_destroy(&mut self) -> Result>>; /// Count the number of entries in the b-tree /// /// Only supposed to be used in the context of a simple Count Select Statement fn count(&mut self) -> Result>; fn is_empty(&self) -> bool; fn root_page(&self) -> i64; /// Move cursor at the start. fn rewind(&mut self) -> Result>; /// Check if cursor is poiting at a valid entry with a record. fn has_record(&self) -> bool; fn set_has_record(&mut self, has_record: bool); fn get_index_info(&self) -> &Arc; fn seek_end(&mut self) -> Result>; fn seek_to_last(&mut self, always_seek: bool) -> Result>; /// Returns true if this cursor operates in MVCC mode. fn is_mvcc(&self) -> bool { false } // --- start: BTreeCursor specific functions ---- fn invalidate_record(&mut self); fn has_rowid(&self) -> bool; fn get_pager(&self) -> Arc; fn get_skip_advance(&self) -> bool; /// Invalidate cached navigation state. Must be called on cursors that /// share a btree (e.g. OpenDup cursors) when the btree structure is /// modified by another cursor (e.g. clear_btree via ResetSorter). fn invalidate_btree_cache(&mut self) {} // --- end: BTreeCursor specific functions ---- } pub struct BTreeCursor { /// The pager that is used to read and write to the database file. pub pager: Arc, /// Cached value of the usable space of a BTree page, since it is very expensive to call in a hot loop via pager.usable_space(). /// This is OK to cache because both 'PRAGMA page_size' and '.filectrl reserve_bytes' only have an effect on: /// 1. an uninitialized database, /// 2. an initialized database when the command is immediately followed by VACUUM. usable_space_cached: usize, /// Page id of the root page used to go back up fast. root_page: i64, /// Rowid and record are stored before being consumed. pub has_record: bool, null_flag: bool, /// Index internal pages are consumed on the way up, so we store going upwards flag in case /// we just moved to a parent page and the parent page is an internal index page which requires /// to be consumed. going_upwards: bool, /// Information maintained across execution attempts when an operation yields due to I/O. state: CursorState, /// State machine for balancing. balance_state: BalanceState, /// Information maintained while freeing overflow pages. Maintained separately from cursor state since /// any method could require freeing overflow pages overflow_state: OverflowState, /// Page stack used to traverse the btree. /// Each cursor has a stack because each cursor traverses the btree independently. stack: PageStack, /// Reusable immutable record, used to allow better allocation strategy. reusable_immutable_record: Option, /// Information about the index key structure (sort order, collation, etc) pub index_info: Option>, /// Maintain count of the number of records in the btree. Used for the `Count` opcode count: usize, /// Stores the cursor context before rebalancing so that a seek can be done later context: Option, /// Store whether the Cursor is in a valid state. Meaning if it is pointing to a valid cell index or not pub valid_state: CursorValidState, seek_state: CursorSeekState, /// Separate state to read a record with overflow pages. This separation from `state` is necessary as /// we can be in a function that relies on `state`, but also needs to process overflow pages read_overflow_state: Option, /// State machine for [BTreeCursor::is_empty_table] is_empty_table_state: EmptyTableState, /// State machine for [BTreeCursor::move_to_rightmost] and, optionally, the id of the rightmost page in the btree. /// If we know the rightmost page id and are already on that page, we can skip a seek. move_to_right_state: (MoveToRightState, Option), /// State machine for [BTreeCursor::seek_to_last] seek_to_last_state: SeekToLastState, /// State machine for [BTreeCursor::rewind] rewind_state: RewindState, /// State machine for [BTreeCursor::next] and [BTreeCursor::prev] advance_state: AdvanceState, /// State machine for [BTreeCursor::count] count_state: CountState, /// State machine for [BTreeCursor::seek_end] seek_end_state: SeekEndState, /// State machine for [BTreeCursor::move_to] move_to_state: MoveToState, /// Whether the next call to [BTreeCursor::next()] should be a no-op. /// This is currently only used after a delete operation causes a rebalancing. /// Advancing is only skipped if the cursor is currently pointing to a valid record /// when next() is called. pub skip_advance: bool, /// Reusable buffer for cell payloads during insert/update operations. /// This avoids allocating a new Vec for each write operation. reusable_cell_payload: Vec, } crate::assert::assert_send!(BTreeCursor); crate::assert::assert_sync!(BTreeCursor); /// We store the cell index and cell count for each page in the stack. /// The reason we store the cell count is because we need to know when we are at the end of the page, /// without having to perform IO to get the ancestor pages. #[derive(Debug, Clone, Copy, Default)] struct BTreeNodeState { cell_idx: i32, cell_count: Option, } impl BTreeNodeState { /// Check if the current cell index is at the end of the page. /// This information is used to determine whether a child page should move up to its parent. /// If the child page is the rightmost leaf page and it has reached the end, this means all of its ancestors have /// already reached the end, so it should not go up because there are no more records to traverse. fn is_at_end(&self) -> bool { let cell_count = self.cell_count.expect("cell_count is not set"); // cell_idx == cell_count means: we will traverse to the rightmost pointer next. // cell_idx == cell_count + 1 means: we have already gone down to the rightmost pointer. self.cell_idx == cell_count + 1 } } impl BTreeCursor { pub fn new(pager: Arc, root_page: i64, _num_columns: usize) -> Self { let valid_state = if root_page == 1 && !pager.db_initialized() { CursorValidState::Invalid } else { CursorValidState::Valid }; let usable_space = pager.usable_space(); Self { pager, root_page, usable_space_cached: usable_space, has_record: false, null_flag: false, going_upwards: false, state: CursorState::None, balance_state: BalanceState::default(), overflow_state: OverflowState::Start, stack: PageStack { current_page: -1, node_states: [BTreeNodeState::default(); BTCURSOR_MAX_DEPTH + 1], stack: [const { None }; BTCURSOR_MAX_DEPTH + 1], }, reusable_immutable_record: None, index_info: None, count: 0, context: None, valid_state, seek_state: CursorSeekState::Start, read_overflow_state: None, is_empty_table_state: EmptyTableState::Start, move_to_right_state: (MoveToRightState::Start, None), seek_to_last_state: SeekToLastState::Start, rewind_state: RewindState::Start, advance_state: AdvanceState::Start, count_state: CountState::Start, seek_end_state: SeekEndState::Start, move_to_state: MoveToState::Start, skip_advance: false, reusable_cell_payload: Vec::new(), } } pub fn new_table(pager: Arc, root_page: i64, num_columns: usize) -> Self { Self::new(pager, root_page, num_columns) } pub fn new_index(pager: Arc, root_page: i64, index: &Index, num_columns: usize) -> Self { let mut cursor = Self::new(pager, root_page, num_columns); cursor.index_info = Some(Arc::new(IndexInfo::new_from_index(index))); cursor } /// Resets the cached count state so the next `count()` call re-traverses the /// btree. Must be called after any mutation (insert, delete, clear) that may /// change the number of rows in the tree. fn invalidate_count_cache(&mut self) { self.count_state = CountState::Start; self.count = 0; } pub fn get_index_rowid_from_record(&self) -> Option { if !self.has_rowid() { return None; } let rowid = match self.get_immutable_record().as_ref().unwrap().last_value() { Some(Ok(ValueRef::Numeric(Numeric::Integer(rowid)))) => rowid, _ => unreachable!( "index where has_rowid() is true should have an integer rowid as the last value" ), }; Some(rowid) } /// Check if the table is empty. /// This is done by checking if the root page has no cells. #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn is_empty_table(&mut self) -> Result> { loop { let state = self.is_empty_table_state.clone(); match state { EmptyTableState::Start => { let (page, c) = self.pager.read_page(self.root_page)?; self.is_empty_table_state = EmptyTableState::ReadPage { page }; if let Some(c) = c { io_yield_one!(c); } } EmptyTableState::ReadPage { page } => { turso_assert!(page.is_loaded(), "page should be loaded"); let cell_count = page.get_contents().cell_count(); break Ok(IOResult::Done(cell_count == 0)); } } } } /// Move the cursor to the previous record and return it. /// Used in backwards iteration. #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG, name = "prev"))] pub fn get_prev_record(&mut self) -> Result> { let mut inner = || { loop { let (old_top_idx, page_type, is_index, is_leaf, cell_count) = { let page = self.stack.top_ref(); let contents = page.get_contents(); ( self.stack.current(), contents.page_type()?, page.is_index()?, contents.is_leaf(), contents.cell_count(), ) }; let cell_idx = self.stack.current_cell_index(); // If we are at the end of the page and we haven't just come back from the right child, // we now need to move to the rightmost child. if cell_idx == i32::MAX && !self.going_upwards { let rightmost_pointer = self.stack.top_ref().get_contents().rightmost_pointer()?; if let Some(rightmost_pointer) = rightmost_pointer { let past_rightmost_pointer = cell_count as i32 + 1; self.stack.set_cell_index(past_rightmost_pointer); let (page, c) = self.read_page(rightmost_pointer as i64)?; self.descend_backwards(page); if let Some(c) = c { io_yield_one!(c); } continue; } } if cell_idx >= cell_count as i32 { self.stack.set_cell_index(cell_count as i32 - 1); } else if !self.stack.current_cell_index_less_than_min() { // skip retreat in case we still haven't visited this cell in index let should_visit_internal_node = is_index && self.going_upwards; // we are going upwards, this means we still need to visit divider cell in an index if should_visit_internal_node { self.going_upwards = false; return Ok(IOResult::Done(true)); } else if matches!( page_type, PageType::IndexLeaf | PageType::TableLeaf | PageType::TableInterior ) { self.stack.retreat(); } } // moved to beginning of current page // todo: find a better way to flag moved to end or begin of page if self.stack.current_cell_index_less_than_min() { loop { if self.stack.current_cell_index() >= 0 { break; } if self.stack.has_parent() { self.pop_upwards(); } else { // moved to begin of btree return Ok(IOResult::Done(false)); } } // continue to next loop to get record from the new page continue; } if is_leaf { return Ok(IOResult::Done(true)); } if is_index && self.going_upwards { // If we are going upwards, we need to visit the divider cell before going back to another child page. // This is because index interior cells have payloads, so unless we do this we will be skipping an entry when traversing the tree. self.going_upwards = false; return Ok(IOResult::Done(true)); } let cell_idx = self.stack.current_cell_index() as usize; let left_child_page = self .stack .get_page_contents_at_level(old_top_idx) .unwrap() .cell_interior_read_left_child_page(cell_idx)?; if page_type == PageType::IndexInterior { // In backwards iteration, if we haven't just moved to this interior node from the // right child, but instead are about to move to the left child, we need to retreat // so that we don't come back to this node again. // For example: // this parent: key 666 // left child has: key 663, key 664, key 665 // we need to move to the previous parent (with e.g. key 662) when iterating backwards. self.stack.retreat(); } let (mem_page, c) = self.read_page(left_child_page as i64)?; self.descend_backwards(mem_page); if let Some(c) = c { io_yield_one!(c); } } }; let has_record = return_if_io!(inner()); self.invalidate_record(); self.set_has_record(has_record); Ok(IOResult::Done(())) } /// Reads the record of a cell that has overflow pages. This is a state machine that requires to be called until completion so everything /// that calls this function should be reentrant. #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn process_overflow_read( &mut self, payload: &'static [u8], start_next_page: u32, payload_size: u64, ) -> Result> { loop { if self.read_overflow_state.is_none() { let (page, c) = self.read_page(start_next_page as i64)?; self.read_overflow_state.replace(ReadPayloadOverflow { payload: payload.to_vec(), next_page: start_next_page, remaining_to_read: payload_size as usize - payload.len(), page, }); if let Some(c) = c { io_yield_one!(c); } continue; } let ReadPayloadOverflow { payload, remaining_to_read, next_page, page, .. } = self.read_overflow_state.as_mut().unwrap(); turso_assert!(page.is_loaded(), "page should be loaded"); tracing::debug!(next_page, remaining_to_read, "reading overflow page"); let contents = page.get_contents(); // The first four bytes of each overflow page are a big-endian integer which is the page number of the next page in the chain, or zero for the final page in the chain. let next = contents.read_u32_no_offset(0); let buf = contents.as_ptr(); let usable_space = self.pager.usable_space(); let to_read = (*remaining_to_read).min(usable_space - 4); payload.extend_from_slice(&buf[4..4 + to_read]); *remaining_to_read -= to_read; if *remaining_to_read != 0 && next != 0 { let (new_page, c) = self.read_page(next as i64)?; let ReadPayloadOverflow { next_page, page, .. } = self.read_overflow_state.as_mut().unwrap(); *page = new_page; *next_page = next; if let Some(c) = c { io_yield_one!(c); } continue; } turso_assert!( *remaining_to_read == 0 && next == 0, "we can't have more pages to read while also have read everything" ); let payload_swap = std::mem::take(payload); let mut reuse_immutable = self.get_immutable_record_or_create(); reuse_immutable.as_mut().unwrap().invalidate(); reuse_immutable .as_mut() .unwrap() .start_serialization(&payload_swap); let _ = self.read_overflow_state.take(); break Ok(IOResult::Done(())); } } /// Check if any ancestor pages still have cells to iterate. /// If not, traversing back up to parent is of no use because we are at the end of the tree. fn ancestor_pages_have_more_children(&self) -> bool { let node_states = self.stack.node_states; (0..self.stack.current()) .rev() .any(|idx| !node_states[idx].is_at_end()) } /// Move the cursor to the next record and return it. /// Used in forwards iteration, which is the default. #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG, name = "next"))] pub fn get_next_record(&mut self) -> Result> { let mut inner = || { if self.stack.current_page == -1 { // This can happen in nested left joins. See: // https://github.com/tursodatabase/turso/issues/2924 return Ok(IOResult::Done(false)); } loop { let mem_page = self.stack.top_ref(); let contents = mem_page.get_contents(); let cell_idx = self.stack.current_cell_index(); let cell_count = contents.cell_count(); let is_leaf = contents.is_leaf(); if cell_idx != -1 && is_leaf && cell_idx as usize + 1 < cell_count { self.stack.advance(); return Ok(IOResult::Done(true)); } let mem_page = mem_page.clone(); let contents = mem_page.get_contents(); tracing::debug!( id = mem_page.get().id, cell = self.stack.current_cell_index(), cell_count, "current_before_advance", ); let is_index = mem_page.is_index()?; let should_skip_advance = is_index && self.going_upwards // we are going upwards, this means we still need to visit divider cell in an index && self.stack.current_cell_index() >= 0 && self.stack.current_cell_index() < cell_count as i32; // if we weren't on a // valid cell then it means we will have to move upwards again or move to right page, // anyways, we won't visit this invalid cell index if should_skip_advance { tracing::debug!( going_upwards = self.going_upwards, page = mem_page.get().id, cell_idx = self.stack.current_cell_index(), "skipping advance", ); self.going_upwards = false; return Ok(IOResult::Done(true)); } // Important to advance only after loading the page in order to not advance > 1 times self.stack.advance(); let cell_idx = self.stack.current_cell_index() as usize; tracing::debug!(id = mem_page.get().id, cell = cell_idx, "current"); if cell_idx >= cell_count { let rightmost_already_traversed = cell_idx > cell_count; match (contents.rightmost_pointer()?, rightmost_already_traversed) { (Some(right_most_pointer), false) => { // do rightmost self.stack.advance(); let (mem_page, c) = self.read_page(right_most_pointer as i64)?; self.descend(mem_page); if let Some(c) = c { io_yield_one!(c); } continue; } _ => { if self.ancestor_pages_have_more_children() { tracing::trace!("moving simple upwards"); self.pop_upwards(); continue; } else { // If none of the ancestor pages have more children to iterate, that means we are at the end of the btree and should stop iterating. return Ok(IOResult::Done(false)); } } } } turso_assert!( cell_idx < cell_count, "cell index out of bounds", { "cell_idx": cell_idx, "cell_count": cell_count, "page_type": contents.page_type().ok(), "page_id": mem_page.get().id } ); if is_leaf { return Ok(IOResult::Done(true)); } if is_index && self.going_upwards { // This means we just came up from a child, so now we need to visit the divider cell before going back to another child page. // This is because index interior cells have payloads, so unless we do this we will be skipping an entry when traversing the tree. self.going_upwards = false; return Ok(IOResult::Done(true)); } let left_child_page = contents.cell_interior_read_left_child_page(cell_idx)?; let (mem_page, c) = self.read_page(left_child_page as i64)?; self.descend(mem_page); if let Some(c) = c { io_yield_one!(c); } } }; let has_record = return_if_io!(inner()); self.invalidate_record(); self.set_has_record(has_record); Ok(IOResult::Done(())) } /// Move the cursor to the record that matches the seek key and seek operation. /// This may be used to seek to a specific record in a point query (e.g. SELECT * FROM table WHERE col = 10) /// or e.g. find the first record greater than the seek key in a range query (e.g. SELECT * FROM table WHERE col > 10). /// We don't include the rowid in the comparison and that's why the last value from the record is not included. fn do_seek(&mut self, key: SeekKey<'_>, op: SeekOp) -> Result> { let ret = return_if_io!(match key { SeekKey::TableRowId(rowid) => { self.tablebtree_seek(rowid, op) } SeekKey::IndexKey(index_key) => { self.indexbtree_seek(index_key, op) } }); self.valid_state = CursorValidState::Valid; Ok(IOResult::Done(ret)) } fn do_seek_unpacked( &mut self, registers: &[Register], op: SeekOp, ) -> Result> { let ret = return_if_io!(self.indexbtree_seek_unpacked(registers, op)); self.valid_state = CursorValidState::Valid; Ok(IOResult::Done(ret)) } /// Pop the stack and mark that we are going upwards in the B-tree. /// This is the only place where `going_upwards` should be set to `true`. fn pop_upwards(&mut self) { self.going_upwards = true; self.stack.pop(); } /// Descend into a child page during forward iteration. /// Clears the `going_upwards` flag — once we descend, we are no longer going upwards. fn descend(&mut self, page: PageRef) { self.going_upwards = false; self.stack.push(page); } /// Descend into a child page during backward iteration. /// Clears the `going_upwards` flag — once we descend, we are no longer going upwards. fn descend_backwards(&mut self, page: PageRef) { self.going_upwards = false; self.stack.push_backwards(page); } /// Move the cursor to the root page of the btree. #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn move_to_root(&mut self) -> Result> { self.seek_state = CursorSeekState::Start; self.going_upwards = false; tracing::trace!(root_page = self.root_page); let (mem_page, c) = self.read_page(self.root_page)?; self.stack.clear(); self.stack.push(mem_page); Ok(c) } /// Move the cursor to the rightmost record in the btree. #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG))] fn move_to_rightmost(&mut self, always_seek: bool) -> Result> { loop { let (move_to_right_state, rightmost_page_id) = &self.move_to_right_state; match *move_to_right_state { MoveToRightState::Start => { if !always_seek { if let Some(rightmost_page_id) = rightmost_page_id { // If we know the rightmost page and are already on it, we can skip a seek. // This optimization is never performed if always_seek = true. always_seek is used // in cases where we cannot be sure that the btree wasn't modified from under us // e.g. by a trigger subprogram. let current_page = self.stack.top_ref(); if current_page.get().id == *rightmost_page_id { let contents = current_page.get_contents(); let cell_count = contents.cell_count(); self.stack.set_cell_index(cell_count as i32 - 1); return Ok(IOResult::Done(cell_count > 0)); } } } let rightmost_page_id = *rightmost_page_id; let c = self.move_to_root()?; self.move_to_right_state = (MoveToRightState::ProcessPage, rightmost_page_id); if let Some(c) = c { io_yield_one!(c); } } MoveToRightState::ProcessPage => { let mem_page = self.stack.top_ref(); let page_idx = mem_page.get().id; let contents = mem_page.get_contents(); if contents.is_leaf() { self.move_to_right_state = (MoveToRightState::Start, Some(page_idx)); if contents.cell_count() > 0 { self.stack.set_cell_index(contents.cell_count() as i32 - 1); return Ok(IOResult::Done(true)); } return Ok(IOResult::Done(false)); } match contents.rightmost_pointer()? { Some(right_most_pointer) => { self.stack.set_cell_index(contents.cell_count() as i32 + 1); let (mem_page, c) = self.read_page(right_most_pointer as i64)?; self.stack.push(mem_page); if let Some(c) = c { io_yield_one!(c); } } None => { unreachable!("interior page should have a rightmost pointer"); } } } } } } /// Specialized version of move_to() for table btrees. #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG))] fn tablebtree_move_to(&mut self, rowid: i64, seek_op: SeekOp) -> Result> { loop { let (old_top_idx, is_leaf, cell_count) = { let page = self.stack.top_ref(); let contents = page.get_contents(); ( self.stack.current(), contents.is_leaf(), contents.cell_count(), ) }; if is_leaf { self.seek_state = CursorSeekState::FoundLeaf { eq_seen: false }; return Ok(IOResult::Done(())); } if matches!( self.seek_state, CursorSeekState::Start | CursorSeekState::MovingBetweenPages { .. } ) { let eq_seen = match &self.seek_state { CursorSeekState::MovingBetweenPages { eq_seen } => *eq_seen, _ => false, }; let min_cell_idx = 0; let max_cell_idx = cell_count as isize - 1; let nearest_matching_cell = None; self.seek_state = CursorSeekState::InteriorPageBinarySearch { state: InteriorPageBinarySearchState { min_cell_idx, max_cell_idx, nearest_matching_cell, eq_seen, }, }; } let CursorSeekState::InteriorPageBinarySearch { state } = &self.seek_state else { unreachable!("we must be in an interior binary search state"); }; let mut state = *state; let control = self.tablebtree_move_inner(rowid, seek_op, old_top_idx, cell_count, &mut state)?; // Persist state if inner function didn't change seek_state to something else (e.g., MovingBetweenPages) if matches!( self.seek_state, CursorSeekState::InteriorPageBinarySearch { .. } ) { self.seek_state = CursorSeekState::InteriorPageBinarySearch { state }; } match control { ControlFlow::Continue(_) => {} ControlFlow::Break(result) => { return Ok(result); } } } } fn tablebtree_move_inner( &mut self, rowid: i64, seek_op: SeekOp, old_top_idx: usize, cell_count: usize, state: &mut InteriorPageBinarySearchState, ) -> Result>> { let min = state.min_cell_idx; let max = state.max_cell_idx; if min > max { if let Some(nearest_matching_cell) = state.nearest_matching_cell { let left_child_page = self .stack .get_page_contents_at_level(old_top_idx) .unwrap() .cell_interior_read_left_child_page(nearest_matching_cell)?; self.stack.set_cell_index(nearest_matching_cell as i32); let (mem_page, c) = self.read_page(left_child_page as i64)?; self.stack.push(mem_page); self.seek_state = CursorSeekState::MovingBetweenPages { eq_seen: state.eq_seen, }; if let Some(c) = c { return Ok(ControlFlow::Break(IOResult::IO(IOCompletions::Single(c)))); } return Ok(ControlFlow::Continue(())); } self.stack.set_cell_index(cell_count as i32 + 1); match self .stack .get_page_contents_at_level(old_top_idx) .unwrap() .rightmost_pointer()? { Some(right_most_pointer) => { let (mem_page, c) = self.read_page(right_most_pointer as i64)?; self.stack.push(mem_page); self.seek_state = CursorSeekState::MovingBetweenPages { eq_seen: state.eq_seen, }; if let Some(c) = c { return Ok(ControlFlow::Break(IOResult::IO(IOCompletions::Single(c)))); } return Ok(ControlFlow::Continue(())); } None => { unreachable!("we shall not go back up! The only way is down the slope"); } } } let cur_cell_idx = (min + max) >> 1; // rustc generates extra insns for (min+max)/2 due to them being isize. we know min&max are >=0 here. let cell_rowid = self .stack .get_page_contents_at_level(old_top_idx) .unwrap() .cell_table_interior_read_rowid(cur_cell_idx as usize)?; // in sqlite btrees left child pages have <= keys. // table btrees can have a duplicate rowid in the interior cell, so for example if we are looking for rowid=10, // and we find an interior cell with rowid=10, we need to move to the left page since (due to the <= rule of sqlite btrees) // the left page may have a rowid=10. // Logic table for determining if target leaf page is in left subtree // // Forwards iteration (looking for first match in tree): // OP | Current Cell vs Seek Key | Action? | Explanation // GT | > | go left | First > key is in left subtree // GT | = or < | go right | First > key is in right subtree // GE | > or = | go left | First >= key is in left subtree // GE | < | go right | First >= key is in right subtree // // Backwards iteration (looking for last match in tree): // OP | Current Cell vs Seek Key | Action? | Explanation // LE | > or = | go left | Last <= key is in left subtree // LE | < | go right | Last <= key is in right subtree // LT | > or = | go left | Last < key is in left subtree // LT | < | go right?| Last < key is in right subtree, except if cell rowid is exactly 1 less // // No iteration (point query): // EQ | > or = | go left | Last = key is in left subtree // EQ | < | go right | Last = key is in right subtree let is_on_left = match seek_op { SeekOp::GT => cell_rowid > rowid, SeekOp::GE { .. } => cell_rowid >= rowid, SeekOp::LE { .. } => cell_rowid >= rowid, SeekOp::LT => cell_rowid + 1 >= rowid, }; if is_on_left { state.nearest_matching_cell.replace(cur_cell_idx as usize); state.max_cell_idx = cur_cell_idx - 1; } else { state.min_cell_idx = cur_cell_idx + 1; } Ok(ControlFlow::Continue(())) } /// Specialized version of move_to() for index btrees. #[cfg_attr(debug_assertions, instrument(skip(self, index_key), level = Level::DEBUG))] fn indexbtree_move_to( &mut self, index_key: &ImmutableRecord, cmp: SeekOp, ) -> Result> { let key_values = index_key.get_values()?; let record_comparer = { let index_info = self .index_info .as_ref() .expect("indexbtree_move_to: index_info required"); find_compare(key_values.iter().peekable(), index_info) }; self.indexbtree_move_to_internal(cmp, record_comparer, &key_values) } /// Move cursor to position using registers directly, avoiding record serialization. /// See `seek_unpacked` for rationale. #[instrument(skip(self, registers), level = Level::DEBUG)] fn indexbtree_move_to_unpacked( &mut self, registers: &[Register], cmp: SeekOp, ) -> Result> { if matches!( self.seek_state, CursorSeekState::LeafPageBinarySearch { .. } | CursorSeekState::FoundLeaf { .. } ) { self.seek_state = CursorSeekState::Start; } if matches!(self.seek_state, CursorSeekState::Start) { if let Some(c) = self.move_to_root()? { return Ok(IOResult::IO(IOCompletions::Single(c))); } } let index_info = self .index_info .as_ref() .expect("indexbtree_move_to_unpacked: index_info required"); let key_values: SmallVec<[ValueRef<'_>; STACK_ALLOC_KEY_VALS_MAX]> = registers .iter() .map(|r| r.get_value().as_value_ref()) .collect(); let record_comparer = find_compare(key_values.iter().peekable(), index_info); self.indexbtree_move_to_internal(cmp, record_comparer, &key_values) } fn indexbtree_move_to_internal( &mut self, cmp: SeekOp, record_comparer: RecordCompare, key_values: &[ValueRef<'_>], ) -> Result> { tracing::debug!("Using record comparison strategy: {:?}", record_comparer); let tie_breaker = get_tie_breaker_from_seek_op(cmp); loop { let (old_top_idx, is_leaf, cell_count) = { let page = self.stack.top_ref(); let contents = page.get_contents(); ( self.stack.current(), contents.is_leaf(), contents.cell_count(), ) }; if is_leaf { let eq_seen = match &self.seek_state { CursorSeekState::MovingBetweenPages { eq_seen } => *eq_seen, _ => false, }; self.seek_state = CursorSeekState::FoundLeaf { eq_seen }; return Ok(IOResult::Done(())); } if matches!( self.seek_state, CursorSeekState::Start | CursorSeekState::MovingBetweenPages { .. } ) { let eq_seen = match &self.seek_state { CursorSeekState::MovingBetweenPages { eq_seen } => *eq_seen, _ => false, }; let min_cell_idx = 0; let max_cell_idx = cell_count as isize - 1; let nearest_matching_cell = None; self.seek_state = CursorSeekState::InteriorPageBinarySearch { state: InteriorPageBinarySearchState { min_cell_idx, max_cell_idx, nearest_matching_cell, eq_seen, }, }; } let CursorSeekState::InteriorPageBinarySearch { state } = &self.seek_state else { unreachable!( "we must be in an interior binary search state, got {:?}", self.seek_state ); }; let mut state = *state; let control = self.indexbtree_move_to_inner( cmp, old_top_idx, cell_count, record_comparer, key_values, tie_breaker, &mut state, )?; // Persist state if inner function didn't change seek_state to something else (e.g., MovingBetweenPages) if matches!( self.seek_state, CursorSeekState::InteriorPageBinarySearch { .. } ) { self.seek_state = CursorSeekState::InteriorPageBinarySearch { state }; } match control { ControlFlow::Continue(_) => {} ControlFlow::Break(result) => { return Ok(result); } } } } #[expect(clippy::too_many_arguments)] fn indexbtree_move_to_inner( &mut self, cmp: SeekOp, old_top_idx: usize, cell_count: usize, record_comparer: RecordCompare, key_values: &[ValueRef<'_>], tie_breaker: Ordering, state: &mut InteriorPageBinarySearchState, ) -> Result>> { let iter_dir = cmp.iteration_direction(); let min = state.min_cell_idx; let max = state.max_cell_idx; if min > max { let Some(leftmost_matching_cell) = state.nearest_matching_cell else { self.stack.set_cell_index(cell_count as i32 + 1); match self .stack .get_page_contents_at_level(old_top_idx) .unwrap() .rightmost_pointer()? { Some(right_most_pointer) => { let (mem_page, c) = self.read_page(right_most_pointer as i64)?; self.stack.push(mem_page); self.seek_state = CursorSeekState::MovingBetweenPages { eq_seen: state.eq_seen, }; if let Some(c) = c { return Ok(ControlFlow::Break(IOResult::IO(IOCompletions::Single(c)))); } return Ok(ControlFlow::Continue(())); } None => { unreachable!("we shall not go back up! The only way is down the slope"); } } }; let matching_cell = self .stack .get_page_contents_at_level(old_top_idx) .unwrap() .cell_get(leftmost_matching_cell, self.usable_space())?; self.stack.set_cell_index(leftmost_matching_cell as i32); // we don't advance in case of forward iteration and index tree internal nodes because we will visit this node going up. // in backwards iteration, we must retreat because otherwise we would unnecessarily visit this node again. // Example: // this parent: key 666, and we found the target key in the left child. // left child has: key 663, key 664, key 665 // we need to move to the previous parent (with e.g. key 662) when iterating backwards so that we don't end up back here again. if iter_dir == IterationDirection::Backwards { self.stack.retreat(); } let BTreeCell::IndexInteriorCell(IndexInteriorCell { left_child_page, .. }) = &matching_cell else { unreachable!("unexpected cell type: {:?}", matching_cell); }; { let page = self.stack.get_page_at_level(old_top_idx).unwrap(); turso_assert!( page.get().id != *left_child_page as usize, "corrupt: current page and left child page are the same", { "cell": leftmost_matching_cell, "page_id": page.get().id } ); } let (mem_page, c) = self.read_page(*left_child_page as i64)?; self.stack.push(mem_page); self.seek_state = CursorSeekState::MovingBetweenPages { eq_seen: state.eq_seen, }; if let Some(c) = c { return Ok(ControlFlow::Break(IOResult::IO(IOCompletions::Single(c)))); } return Ok(ControlFlow::Continue(())); } let cur_cell_idx = (min + max) >> 1; // rustc generates extra insns for (min+max)/2 due to them being isize. we know min&max are >=0 here. self.stack.set_cell_index(cur_cell_idx as i32); let (payload, payload_size, first_overflow_page) = self .stack .get_page_contents_at_level(old_top_idx) .unwrap() .cell_index_read_payload_ptr(cur_cell_idx as usize, self.usable_space())?; if let Some(next_page) = first_overflow_page { let res = self.process_overflow_read(payload, next_page, payload_size)?; if res.is_io() { return Ok(ControlFlow::Break(res)); } } else { self.get_immutable_record_or_create() .as_mut() .unwrap() .invalidate(); self.get_immutable_record_or_create() .as_mut() .unwrap() .start_serialization(payload); }; let (target_leaf_page_is_in_left_subtree, is_eq) = { let record = self.get_immutable_record(); let record = record.as_ref().unwrap(); let interior_cell_vs_index_key = record_comparer .compare( record, key_values, self.index_info .as_ref() .expect("indexbtree_move_to: index_info required"), 0, tie_breaker, ) .unwrap(); // in sqlite btrees left child pages have <= keys. // in general, in forwards iteration we want to find the first key that matches the seek condition. // in backwards iteration we want to find the last key that matches the seek condition. // // Logic table for determining if target leaf page is in left subtree. // For index b-trees this is a bit more complicated since the interior cells contain payloads (the key is the payload). // and for non-unique indexes there might be several cells with the same key. // // Forwards iteration (looking for first match in tree): // OP | Current Cell vs Seek Key | Action? | Explanation // GT | > | go left | First > key could be exactly this one, or in left subtree // GT | = or < | go right | First > key must be in right subtree // GE | > | go left | First >= key could be exactly this one, or in left subtree // GE | = | go left | First >= key could be exactly this one, or in left subtree // GE | < | go right | First >= key must be in right subtree // // Backwards iteration (looking for last match in tree): // OP | Current Cell vs Seek Key | Action? | Explanation // LE | > | go left | Last <= key must be in left subtree // LE | = | go right | Last <= key is either this one, or somewhere to the right of this one. So we need to go right to make sure // LE | < | go right | Last <= key must be in right subtree // LT | > | go left | Last < key must be in left subtree // LT | = | go left | Last < key must be in left subtree since we want strictly less than // LT | < | go right | Last < key could be exactly this one, or in right subtree // // No iteration (point query): // EQ | > | go left | First = key must be in left subtree // EQ | = | go left | First = key could be exactly this one, or in left subtree // EQ | < | go right | First = key must be in right subtree ( match cmp { SeekOp::GT => interior_cell_vs_index_key.is_gt(), SeekOp::GE { .. } => interior_cell_vs_index_key.is_ge(), SeekOp::LE { .. } => interior_cell_vs_index_key.is_gt(), SeekOp::LT => interior_cell_vs_index_key.is_ge(), }, interior_cell_vs_index_key.is_eq(), ) }; if is_eq { state.eq_seen = true; } if target_leaf_page_is_in_left_subtree { state.nearest_matching_cell = Some(cur_cell_idx as usize); state.max_cell_idx = cur_cell_idx - 1; } else { state.min_cell_idx = cur_cell_idx + 1; } Ok(ControlFlow::Continue(())) } /// Specialized version of do_seek() for table btrees that uses binary search instead /// of iterating cells in order. #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn tablebtree_seek(&mut self, rowid: i64, seek_op: SeekOp) -> Result> { if matches!( self.seek_state, CursorSeekState::Start | CursorSeekState::MovingBetweenPages { .. } | CursorSeekState::InteriorPageBinarySearch { .. } ) { // No need for another move_to_root. Move_to already moves to root return_if_io!(self.move_to(SeekKey::TableRowId(rowid), seek_op)); let page = self.stack.top_ref(); let contents = page.get_contents(); turso_assert!( contents.is_leaf(), "tablebtree_seek() called on non-leaf page" ); let cell_count = contents.cell_count(); if cell_count == 0 { self.stack.set_cell_index(0); return Ok(IOResult::Done(SeekResult::NotFound)); } let min_cell_idx = 0; let max_cell_idx = cell_count as isize - 1; // If iter dir is forwards, we want the first cell that matches; // If iter dir is backwards, we want the last cell that matches. let nearest_matching_cell = None; self.seek_state = CursorSeekState::LeafPageBinarySearch { state: LeafPageBinarySearchState { min_cell_idx, max_cell_idx, nearest_matching_cell, eq_seen: false, // not relevant for table btrees target_cell_when_not_found: match seek_op.iteration_direction() { IterationDirection::Forwards => cell_count as i32, IterationDirection::Backwards => -1, }, }, }; } let CursorSeekState::LeafPageBinarySearch { state } = &self.seek_state else { unreachable!("we must be in a leaf binary search state"); }; let page = self.stack.top_ref().clone(); let contents = page.get_contents(); let mut state = *state; loop { let control = self.tablebtree_seek_inner(rowid, seek_op, contents, &mut state)?; // Persist state after each iteration since inner function modifies it if matches!( self.seek_state, CursorSeekState::LeafPageBinarySearch { .. } ) { self.seek_state = CursorSeekState::LeafPageBinarySearch { state }; } match control { ControlFlow::Continue(_) => {} ControlFlow::Break(res) => { return Ok(res); } } } } fn tablebtree_seek_inner( &mut self, rowid: i64, seek_op: SeekOp, contents: &mut PageContent, state: &mut LeafPageBinarySearchState, ) -> Result>> { let iter_dir = seek_op.iteration_direction(); let min = state.min_cell_idx; let max = state.max_cell_idx; let target_cell_when_not_found = state.target_cell_when_not_found; if min > max { if let Some(nearest_matching_cell) = state.nearest_matching_cell { self.stack.set_cell_index(nearest_matching_cell as i32); self.set_has_record(true); return Ok(ControlFlow::Break(IOResult::Done(SeekResult::Found))); } else { // if !eq_only - matching entry can exist in neighbour leaf page // this can happen if key in the interiour page was deleted - but divider kept untouched // in such case BTree can navigate to the leaf which no longer has matching key for seek_op // in this case, caller must advance cursor if necessary return Ok(ControlFlow::Break(IOResult::Done(if seek_op.eq_only() { let has_record = target_cell_when_not_found >= 0 && target_cell_when_not_found < contents.cell_count() as i32; self.has_record = has_record; self.stack.set_cell_index(target_cell_when_not_found); SeekResult::NotFound } else { // set cursor to the position where which would hold the op-boundary if it were present self.stack.set_cell_index(target_cell_when_not_found); SeekResult::TryAdvance }))); }; } let cur_cell_idx = (min + max) >> 1; // rustc generates extra insns for (min+max)/2 due to them being isize. we know min&max are >=0 here. let cell_rowid = contents.cell_table_leaf_read_rowid(cur_cell_idx as usize)?; let cmp = cell_rowid.cmp(&rowid); let found = match seek_op { SeekOp::GT => cmp.is_gt(), SeekOp::GE { eq_only: true } => cmp.is_eq(), SeekOp::GE { eq_only: false } => cmp.is_ge(), SeekOp::LE { eq_only: true } => cmp.is_eq(), SeekOp::LE { eq_only: false } => cmp.is_le(), SeekOp::LT => cmp.is_lt(), }; // rowids are unique, so we can return the rowid immediately if found && seek_op.eq_only() { self.stack.set_cell_index(cur_cell_idx as i32); self.set_has_record(true); return Ok(ControlFlow::Break(IOResult::Done(SeekResult::Found))); } if found { state.nearest_matching_cell = Some(cur_cell_idx as usize); match iter_dir { IterationDirection::Forwards => { state.max_cell_idx = cur_cell_idx - 1; } IterationDirection::Backwards => { state.min_cell_idx = cur_cell_idx + 1; } } } else if cmp.is_gt() { if matches!(seek_op, SeekOp::GE { eq_only: true }) { state.target_cell_when_not_found = target_cell_when_not_found.min(cur_cell_idx as i32); } state.max_cell_idx = cur_cell_idx - 1; } else if cmp.is_lt() { if matches!(seek_op, SeekOp::LE { eq_only: true }) { state.target_cell_when_not_found = target_cell_when_not_found.max(cur_cell_idx as i32); } state.min_cell_idx = cur_cell_idx + 1; } else { match iter_dir { IterationDirection::Forwards => { state.min_cell_idx = cur_cell_idx + 1; } IterationDirection::Backwards => { state.max_cell_idx = cur_cell_idx - 1; } } } Ok(ControlFlow::Continue(())) } #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn indexbtree_seek( &mut self, key: &ImmutableRecord, seek_op: SeekOp, ) -> Result> { let key_values = key.get_values()?; let record_comparer = { let index_info = self .index_info .as_ref() .expect("indexbtree_seek: index_info required"); find_compare(key_values.iter().peekable(), index_info) }; tracing::debug!( "Using record comparison strategy for seek: {:?}", record_comparer ); self.indexbtree_seek_internal(seek_op, record_comparer, &key_values) } /// Seek using registers directly, avoiding record serialization overhead. /// See `seek_unpacked` trait method for rationale. #[instrument(skip_all, level = Level::DEBUG)] fn indexbtree_seek_unpacked( &mut self, registers: &[Register], seek_op: SeekOp, ) -> Result> { let index_info = self .index_info .as_ref() .expect("indexbtree_seek_unpacked: index_info required"); // SmallVec stores up to MAX_STACK_KEY_VALUES on the stack, spilling to heap only if exceeded let key_values: SmallVec<[ValueRef<'_>; STACK_ALLOC_KEY_VALS_MAX]> = registers .iter() .map(|r| r.get_value().as_value_ref()) .collect(); let record_comparer = find_compare(key_values.iter().peekable(), index_info); tracing::debug!( "Using record comparison strategy for seek: {:?}", record_comparer ); self.indexbtree_seek_internal(seek_op, record_comparer, &key_values) } fn indexbtree_seek_internal( &mut self, seek_op: SeekOp, record_comparer: RecordCompare, key_values: &[ValueRef<'_>], ) -> Result> { if matches!( self.seek_state, CursorSeekState::Start | CursorSeekState::MovingBetweenPages { .. } | CursorSeekState::InteriorPageBinarySearch { .. } ) { if matches!(self.seek_state, CursorSeekState::Start) { if let Some(c) = self.move_to_root()? { return Ok(IOResult::IO(IOCompletions::Single(c))); } } return_if_io!(self.indexbtree_move_to_internal(seek_op, record_comparer, key_values)); let CursorSeekState::FoundLeaf { eq_seen } = &self.seek_state else { unreachable!( "We must still be in FoundLeaf state after indexbtree_move_to_internal, got: {:?}", self.seek_state ); }; let eq_seen = *eq_seen; let page = self.stack.top_ref(); let contents = page.get_contents(); let cell_count = contents.cell_count(); if cell_count == 0 { return Ok(IOResult::Done(SeekResult::NotFound)); } let min = 0; let max = cell_count as isize - 1; // If iter dir is forwards, we want the first cell that matches; // If iter dir is backwards, we want the last cell that matches. let nearest_matching_cell = None; self.seek_state = CursorSeekState::LeafPageBinarySearch { state: LeafPageBinarySearchState { min_cell_idx: min, max_cell_idx: max, nearest_matching_cell, eq_seen, target_cell_when_not_found: match seek_op.iteration_direction() { IterationDirection::Forwards => cell_count as i32, IterationDirection::Backwards => -1, }, }, }; } let CursorSeekState::LeafPageBinarySearch { state } = &self.seek_state else { unreachable!( "we must be in a leaf binary search state, got: {:?}", self.seek_state ); }; let old_top_idx = self.stack.current(); let mut state = *state; loop { let control = self.indexbtree_seek_inner( seek_op, old_top_idx, key_values, record_comparer, &mut state, )?; // Persist state after each iteration since inner function modifies it if matches!( self.seek_state, CursorSeekState::LeafPageBinarySearch { .. } ) { self.seek_state = CursorSeekState::LeafPageBinarySearch { state }; } match control { ControlFlow::Continue(_) => {} ControlFlow::Break(res) => { return Ok(res); } } } } fn indexbtree_seek_inner( &mut self, seek_op: SeekOp, old_top_idx: usize, key_values: &[ValueRef<'_>], record_comparer: RecordCompare, state: &mut LeafPageBinarySearchState, ) -> Result>> { let iter_dir = seek_op.iteration_direction(); let min = state.min_cell_idx; let max = state.max_cell_idx; let eq_seen = state.eq_seen; if min > max { if let Some(nearest_matching_cell) = state.nearest_matching_cell { self.stack.set_cell_index(nearest_matching_cell as i32); self.set_has_record(true); return Ok(ControlFlow::Break(IOResult::Done(SeekResult::Found))); } else { // set cursor to the position where which would hold the op-boundary if it were present let target_cell = state.target_cell_when_not_found; self.stack.set_cell_index(target_cell); let has_record = target_cell >= 0 && target_cell < self .stack .get_page_contents_at_level(old_top_idx) .unwrap() .cell_count() as i32; self.has_record = has_record; // Similar logic as in tablebtree_seek(), but for indexes. // The difference is that since index keys are not necessarily unique, we need to TryAdvance // even when eq_only=true and we have seen an EQ match up in the tree in an interior node. if seek_op.eq_only() && !eq_seen { return Ok(ControlFlow::Break(IOResult::Done(SeekResult::NotFound))); } return Ok(ControlFlow::Break(IOResult::Done(SeekResult::TryAdvance))); }; } let cur_cell_idx = (min + max) >> 1; // rustc generates extra insns for (min+max)/2 due to them being isize. we know min&max are >=0 here. self.stack.set_cell_index(cur_cell_idx as i32); let (payload, payload_size, first_overflow_page) = self .stack .get_page_contents_at_level(old_top_idx) .unwrap() .cell_index_read_payload_ptr(cur_cell_idx as usize, self.usable_space())?; if let Some(next_page) = first_overflow_page { let res = self.process_overflow_read(payload, next_page, payload_size)?; if let IOResult::IO(io) = res { return Ok(ControlFlow::Break(IOResult::IO(io))); } } else { self.get_immutable_record_or_create() .as_mut() .unwrap() .invalidate(); self.get_immutable_record_or_create() .as_mut() .unwrap() .start_serialization(payload); }; let (cmp, found) = self.compare_with_current_record( key_values, seek_op, &record_comparer, self.index_info .as_ref() .expect("indexbtree_seek: index_info required"), ); if found { state.nearest_matching_cell.replace(cur_cell_idx as usize); match iter_dir { IterationDirection::Forwards => { state.max_cell_idx = cur_cell_idx - 1; } IterationDirection::Backwards => { state.min_cell_idx = cur_cell_idx + 1; } } } else if cmp.is_gt() { if matches!(seek_op, SeekOp::GE { eq_only: true }) { state.target_cell_when_not_found = state.target_cell_when_not_found.min(cur_cell_idx as i32); } state.max_cell_idx = cur_cell_idx - 1; } else if cmp.is_lt() { if matches!(seek_op, SeekOp::LE { eq_only: true }) { state.target_cell_when_not_found = state.target_cell_when_not_found.max(cur_cell_idx as i32); } state.min_cell_idx = cur_cell_idx + 1; } else { match iter_dir { IterationDirection::Forwards => { state.min_cell_idx = cur_cell_idx + 1; } IterationDirection::Backwards => { state.max_cell_idx = cur_cell_idx - 1; } } } Ok(ControlFlow::Continue(())) } fn compare_with_current_record( &self, key_values: &[ValueRef], seek_op: SeekOp, record_comparer: &RecordCompare, index_info: &IndexInfo, ) -> (Ordering, bool) { let record = self.get_immutable_record(); let record = record.as_ref().unwrap(); let tie_breaker = get_tie_breaker_from_seek_op(seek_op); let cmp = record_comparer .compare(record, key_values, index_info, 0, tie_breaker) .unwrap(); let found = match seek_op { SeekOp::GT => cmp.is_gt(), SeekOp::GE { eq_only: true } => cmp.is_eq(), SeekOp::GE { eq_only: false } => cmp.is_ge(), SeekOp::LE { eq_only: true } => cmp.is_eq(), SeekOp::LE { eq_only: false } => cmp.is_le(), SeekOp::LT => cmp.is_lt(), }; (cmp, found) } #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] pub fn move_to(&mut self, key: SeekKey<'_>, cmp: SeekOp) -> Result> { tracing::trace!(?key, ?cmp); // For a table with N rows, we can find any row by row id in O(log(N)) time by starting at the root page and following the B-tree pointers. // B-trees consist of interior pages and leaf pages. Interior pages contain pointers to other pages, while leaf pages contain the actual row data. // // Conceptually, each Interior Cell in a interior page has a rowid and a left child node, and the page itself has a right-most child node. // Example: consider an interior page that contains cells C1(rowid=10), C2(rowid=20), C3(rowid=30). // - All rows with rowids <= 10 are in the left child node of C1. // - All rows with rowids > 10 and <= 20 are in the left child node of C2. // - All rows with rowids > 20 and <= 30 are in the left child node of C3. // - All rows with rowids > 30 are in the right-most child node of the page. // // There will generally be multiple levels of interior pages before we reach a leaf page, // so we need to follow the interior page pointers until we reach the leaf page that contains the row we are looking for (if it exists). // // Here's a high-level overview of the algorithm: // 1. Since we start at the root page, its cells are all interior cells. // 2. We scan the interior cells until we find a cell whose rowid is greater than or equal to the rowid we are looking for. // 3. Follow the left child pointer of the cell we found in step 2. // a. In case none of the cells in the page have a rowid greater than or equal to the rowid we are looking for, // we follow the right-most child pointer of the page instead (since all rows with rowids greater than the rowid we are looking for are in the right-most child node). // 4. We are now at a new page. If it's another interior page, we repeat the process from step 2. If it's a leaf page, we continue to step 5. // 5. We scan the leaf cells in the leaf page until we find the cell whose rowid is equal to the rowid we are looking for. // This cell contains the actual data we are looking for. // 6. If we find the cell, we return the record. Otherwise, we return an empty result. // If we are at the beginning/end of seek state, start a new move from the root. if matches!( self.seek_state, // these are stages that happen at the leaf page, so we can consider that the previous seek finished and we can start a new one. CursorSeekState::LeafPageBinarySearch { .. } | CursorSeekState::FoundLeaf { .. } ) { self.seek_state = CursorSeekState::Start; } loop { match self.move_to_state { MoveToState::Start => { self.move_to_state = MoveToState::MoveToPage; if matches!(self.seek_state, CursorSeekState::Start) { let c = self.move_to_root()?; if let Some(c) = c { io_yield_one!(c); } } } MoveToState::MoveToPage => { let ret = match key { SeekKey::TableRowId(rowid_key) => self.tablebtree_move_to(rowid_key, cmp), SeekKey::IndexKey(index_key) => self.indexbtree_move_to(index_key, cmp), }; return_if_io!(ret); self.move_to_state = MoveToState::Start; return Ok(IOResult::Done(())); } } } } /// Insert a record into the btree. /// If the insert operation overflows the page, it will be split and the btree will be balanced. #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn insert_into_page(&mut self, bkey: &BTreeKey) -> Result> { let record = bkey .get_record() .expect("expected record present on insert"); if let CursorState::None = &self.state { self.state = CursorState::Write(WriteState::Start); } let usable_space = self.usable_space(); let ret = loop { let CursorState::Write(write_state) = &mut self.state else { panic!("expected write state"); }; match write_state { WriteState::Start => { let page = self.stack.top(); // get page and find cell let cell_idx = { self.pager.add_dirty(&page)?; self.stack.current_cell_index() }; if cell_idx == -1 { // This might be a brand new table and the cursor hasn't moved yet. Let's advance it to the first slot. self.stack.set_cell_index(0); } let cell_idx = self.stack.current_cell_index() as usize; tracing::debug!(cell_idx); // if the cell index is less than the total cells, check: if its an existing // rowid, we are going to update / overwrite the cell if cell_idx < page.get_contents().cell_count() { let cell = page.get_contents().cell_get(cell_idx, usable_space)?; match cell { BTreeCell::TableLeafCell(tbl_leaf) => { if tbl_leaf.rowid == bkey.to_rowid() { tracing::debug!("TableLeafCell: found exact match with cell_idx={cell_idx}, overwriting"); self.has_record = true; *write_state = WriteState::Overwrite { page, cell_idx, state: Some(OverwriteCellState::AllocatePayload), }; continue; } } BTreeCell::IndexLeafCell(..) | BTreeCell::IndexInteriorCell(..) => { return_if_io!(self.record()); let cmp = compare_immutable_iter( record.iter()?, self.get_immutable_record() .as_ref() .unwrap() .iter()?, &self.index_info.as_ref().unwrap().key_info, )?; if cmp == Ordering::Equal { tracing::debug!("IndexLeafCell: found exact match with cell_idx={cell_idx}, overwriting"); self.set_has_record(true); let CursorState::Write(write_state) = &mut self.state else { panic!("expected write state"); }; *write_state = WriteState::Overwrite { page, cell_idx, state: Some(OverwriteCellState::AllocatePayload), }; continue; } else { turso_assert!( !matches!(cell, BTreeCell::IndexInteriorCell(..)), "we should not be inserting a new index interior cell. the only valid operation on an index interior cell is an overwrite!" ); } } other => panic!("unexpected cell type, expected TableLeaf or IndexLeaf, found: {other:?}"), } } let CursorState::Write(write_state) = &mut self.state else { panic!("expected write state"); }; // Reuse the cell payload buffer to avoid allocations let mut payload = std::mem::take(&mut self.reusable_cell_payload); payload.clear(); // Reserve capacity if needed (typical cell is small) // child pointer (4) + payload size varint (up to 9) + rowid varint (up to 9) const MAX_CELL_HEADER: usize = 22; let needed_capacity = record.get_payload().len() + MAX_CELL_HEADER; if payload.capacity() < needed_capacity { payload.reserve(needed_capacity - payload.capacity()); } *write_state = WriteState::Insert { page, cell_idx, new_payload: payload, fill_cell_payload_state: FillCellPayloadState::Start, }; continue; } WriteState::Insert { page, cell_idx, new_payload, ref mut fill_cell_payload_state, } => { return_if_io!(fill_cell_payload( &PinGuard::new(page.clone()), bkey.maybe_rowid(), new_payload, *cell_idx, record, usable_space, self.pager.clone(), fill_cell_payload_state, )); { let contents = page.get_contents(); tracing::debug!(name: "overflow", cell_count = contents.cell_count()); insert_into_cell( contents, new_payload.as_slice(), *cell_idx, usable_space, )?; }; self.stack.set_cell_index(*cell_idx as i32); let overflows = !page.get_contents().overflow_cells.is_empty(); // Recover the reusable buffer before transitioning state let recovered_payload = std::mem::take(new_payload); self.reusable_cell_payload = recovered_payload; if overflows { *write_state = WriteState::Balancing; turso_assert!(matches!(self.balance_state.sub_state, BalanceSubState::Start), "no balancing operation should be in progress during insert", { "state": self.state, "sub_state": self.balance_state.sub_state }); // If we balance, we must save the cursor position and seek to it later. self.save_context(CursorContext::seek_eq_only(bkey)); } else { *write_state = WriteState::Finish; } continue; } WriteState::Overwrite { page, cell_idx, ref mut state, } => { turso_assert!(page.is_loaded(), "page is not loaded", { "page_id": page.get().id }); let page = page.clone(); // Currently it's necessary to .take() here to prevent double-borrow of `self` in `overwrite_cell`. // We insert the state back if overwriting returns IO. let mut state = state.take().expect("state should be present"); let cell_idx = *cell_idx; if let IOResult::IO(io) = self.overwrite_cell(&page, cell_idx, record, &mut state)? { let CursorState::Write(write_state) = &mut self.state else { panic!("expected write state"); }; *write_state = WriteState::Overwrite { page, cell_idx, state: Some(state), }; return Ok(IOResult::IO(io)); } let overflows = !page.get_contents().overflow_cells.is_empty(); let underflows = !overflows && { let free_space = compute_free_space(page.get_contents(), usable_space)?; free_space * 3 > usable_space * 2 }; let CursorState::Write(write_state) = &mut self.state else { panic!("expected write state"); }; if overflows || underflows { *write_state = WriteState::Balancing; turso_assert!(matches!(self.balance_state.sub_state, BalanceSubState::Start), "no balancing operation should be in progress during overwrite", { "state": self.state, "sub_state": self.balance_state.sub_state }); // If we balance, we must save the cursor position and seek to it later. self.save_context(CursorContext::seek_eq_only(bkey)); } else { *write_state = WriteState::Finish; } continue; } WriteState::Balancing => { return_if_io!(self.balance(None)); let CursorState::Write(write_state) = &mut self.state else { panic!("expected write state"); }; *write_state = WriteState::Finish; } WriteState::Finish => { break Ok(IOResult::Done(())); } }; }; if matches!(self.state, CursorState::Write(WriteState::Finish)) { // if there was a balance triggered, the cursor position is invalid. // it's probably not the greatest idea in the world to do this eagerly here, // but at least it works. return_if_io!(self.restore_context()); } self.state = CursorState::None; ret } /// Balance a leaf page. /// Balancing is done when a page overflows. /// see e.g. https://en.wikipedia.org/wiki/B-tree /// /// This is a naive algorithm that doesn't try to distribute cells evenly by content. /// It will try to split the page in half by keys not by content. /// Sqlite tries to have a page at least 40% full. /// /// `balance_ancestor_at_depth` specifies whether to balance an ancestor page at a specific depth. /// If `None`, balancing stops when a level is encountered that doesn't need balancing. /// If `Some(depth)`, the page on the stack at depth `depth` will be rebalanced after balancing the current page. #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG))] fn balance(&mut self, balance_ancestor_at_depth: Option) -> Result> { loop { let usable_space = self.usable_space(); let BalanceState { sub_state, balance_info, .. } = &mut self.balance_state; match sub_state { BalanceSubState::Start => { turso_assert!( balance_info.is_none(), "BalanceInfo should be empty on start" ); let current_page = self.stack.top_ref(); let next_balance_depth = balance_ancestor_at_depth.unwrap_or_else(|| self.stack.current()); { // check if we don't need to balance // don't continue if: // - current page is not overfull root // OR // - current page is not overfull and the amount of free space on the page // is less than 2/3rds of the total usable space on the page // // https://github.com/sqlite/sqlite/blob/0aa95099f5003dc99f599ab77ac0004950b281ef/src/btree.c#L9064-L9071 let page = current_page.get_contents(); let free_space = compute_free_space(page, usable_space)?; let this_level_is_already_balanced = page.overflow_cells.is_empty() && (!self.stack.has_parent() || free_space * 3 <= usable_space * 2); if this_level_is_already_balanced { if self.stack.current() > next_balance_depth { while self.stack.current() > next_balance_depth { // Even though this level is already balanced, we know there's an upper level that needs balancing. // So we pop the stack and continue. self.stack.pop(); } continue; } // Otherwise, we're done. *sub_state = BalanceSubState::Start; return Ok(IOResult::Done(())); } } if !self.stack.has_parent() { *sub_state = BalanceSubState::BalanceRoot; } else { *sub_state = BalanceSubState::Decide; } } BalanceSubState::BalanceRoot => { return_if_io!(self.balance_root()); let BalanceState { sub_state, .. } = &mut self.balance_state; *sub_state = BalanceSubState::Decide; } BalanceSubState::Decide => { let cur_page = self.stack.top_ref(); let cur_page_contents = cur_page.get_contents(); // Check if we can use the balance_quick() fast path. let mut do_quick = false; if cur_page_contents.page_type()? == PageType::TableLeaf && cur_page_contents.overflow_cells.len() == 1 { let overflow_cell_is_last = cur_page_contents.overflow_cells.first().unwrap().index == cur_page_contents.cell_count(); if overflow_cell_is_last { let parent = self .stack .get_page_at_level(self.stack.current() - 1) .expect("parent page should be on the stack"); let parent_contents = parent.get_contents(); let parent_rightmost = parent_contents.rightmost_pointer()?.ok_or_else(|| { mark_unlikely(); LimboError::Corrupt(format!( "parent page {} is a leaf page, expected interior page", parent.get().id )) })?; if parent.get().id != 1 && parent_rightmost == cur_page.get().id as u32 { // If all of the following are true, we can use the balance_quick() fast path: // - The page is a table leaf page // - The overflow cell would be the last cell on the leaf page // - The parent page is not page 1 // - The leaf page is the rightmost page in the subtree do_quick = true; } } } let BalanceState { sub_state, .. } = &mut self.balance_state; if do_quick { *sub_state = BalanceSubState::Quick; } else { *sub_state = BalanceSubState::NonRootPickSiblings; self.stack.pop(); } } BalanceSubState::Quick => { return_if_io!(self.balance_quick()); } BalanceSubState::NonRootPickSiblings | BalanceSubState::NonRootDoBalancing | BalanceSubState::NonRootDoBalancingAllocate { .. } | BalanceSubState::NonRootDoBalancingFinish { .. } | BalanceSubState::FreePages { .. } => { return_if_io!(self.balance_non_root()); } } } } /// Fast balancing routine for the common special case where the rightmost leaf page of a given subtree overflows (= an append). /// In this case we just add a new leaf page as the right sibling of that page, and insert a new divider cell into the parent. /// The high level steps are: /// 1. Allocate a new leaf page and insert the overflow cell payload in it. /// 2. Create a new divider cell in the parent - it contains the page number of the old rightmost leaf, plus the largest rowid on that page. /// 3. Update the rightmost pointer of the parent to point to the new leaf page. /// 4. Continue balance from the parent page (inserting the new divider cell may have overflowed the parent) #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG))] fn balance_quick(&mut self) -> Result> { // Since we are going to change the btree structure, let's forget our cached knowledge of the rightmost page. let _ = self.move_to_right_state.1.take(); // Allocate a new leaf page and insert the overflow cell payload in it. let new_rightmost_leaf = return_if_io!(self.pager.do_allocate_page( PageType::TableLeaf, 0, BtreePageAllocMode::Any )); self.pager.add_dirty(&new_rightmost_leaf)?; let usable_space = self.usable_space(); let old_rightmost_leaf = self.stack.top_ref(); let old_rightmost_leaf_contents = old_rightmost_leaf.get_contents(); turso_assert!( old_rightmost_leaf_contents.overflow_cells.len() == 1, "expected 1 overflow cell", { "overflow_cell_count": old_rightmost_leaf_contents.overflow_cells.len() } ); let parent = self .stack .get_page_at_level(self.stack.current() - 1) .expect("parent page should be on the stack"); self.pager.add_dirty(parent)?; let parent_contents = parent.get_contents(); let rightmost_pointer = parent_contents .rightmost_pointer()? .expect("parent should have a rightmost pointer"); turso_assert!( rightmost_pointer == old_rightmost_leaf.get().id as u32, "leaf should be the rightmost page in the subtree" ); let overflow_cell = old_rightmost_leaf_contents .overflow_cells .pop() .expect("overflow cell should be present"); turso_assert!( overflow_cell.index == old_rightmost_leaf_contents.cell_count(), "overflow cell must be the last cell in the leaf" ); let new_rightmost_leaf_contents = new_rightmost_leaf.get_contents(); insert_into_cell( new_rightmost_leaf_contents, &overflow_cell.payload.as_ref(), 0, usable_space, )?; // Create a new divider cell in the parent - it contains the page number of the old rightmost leaf, plus the largest rowid on that page. let mut new_divider: [u8; 13] = [0; 13]; // 4 bytes for page number, max 9 bytes for rowid (varint) new_divider[0..4].copy_from_slice(&(old_rightmost_leaf.get().id as u32).to_be_bytes()); let largest_rowid = old_rightmost_leaf_contents .cell_table_leaf_read_rowid(old_rightmost_leaf_contents.cell_count() - 1)?; let n = write_varint(&mut new_divider[4..], largest_rowid as u64); let divider_length = 4 + n; // Insert the new divider cell into the parent. insert_into_cell( parent_contents, &new_divider[..divider_length], parent_contents.cell_count(), usable_space, )?; parent_contents.write_rightmost_ptr(new_rightmost_leaf.get().id as u32); // Continue balance from the parent page (inserting the new divider cell may have overflowed the parent) self.stack.pop(); let BalanceState { sub_state, .. } = &mut self.balance_state; *sub_state = BalanceSubState::Start; Ok(IOResult::Done(())) } /// Balance a non root page by trying to balance cells between a maximum of 3 siblings that should be neighboring the page that overflowed/underflowed. #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG))] fn balance_non_root(&mut self) -> Result> { loop { let usable_space = self.usable_space(); let BalanceState { sub_state, balance_info, reusable_divider_buffers, reusable_cell_payloads, } = &mut self.balance_state; tracing::debug!(?sub_state); match sub_state { BalanceSubState::Start | BalanceSubState::BalanceRoot | BalanceSubState::Decide | BalanceSubState::Quick => { panic!("balance_non_root: unexpected state {sub_state:?}") } BalanceSubState::NonRootPickSiblings => { // Since we are going to change the btree structure, let's forget our cached knowledge of the rightmost page. let _ = self.move_to_right_state.1.take(); let (parent_page_idx, page_type, cell_count, over_cell_count) = { let parent_page = self.stack.top_ref(); let parent_contents = parent_page.get_contents(); ( self.stack.current(), parent_contents.page_type()?, parent_contents.cell_count(), parent_contents.overflow_cells.len(), ) }; turso_assert!( matches!(page_type, PageType::IndexInterior | PageType::TableInterior), "expected index or table interior page" ); let number_of_cells_in_parent = cell_count + over_cell_count; // If `seek` moved to rightmost page, cell index will be out of bounds. Meaning cell_count+1. // In any other case, `seek` will stay in the correct index. let past_rightmost_pointer = self.stack.current_cell_index() as usize == number_of_cells_in_parent + 1; if past_rightmost_pointer { self.stack.retreat(); } let parent_page = self.stack.get_page_at_level(parent_page_idx).unwrap(); let parent_contents = parent_page.get_contents(); if !past_rightmost_pointer && over_cell_count > 0 { // The ONLY way we can have an overflow cell in the parent is if we replaced an interior cell from a cell in the child, and that replacement did not fit. // This can only happen on index btrees. if matches!(page_type, PageType::IndexInterior) { turso_assert!(parent_contents.overflow_cells.len() == 1, "index interior page must have no more than 1 overflow cell, as a result of InteriorNodeReplacement"); } else { turso_assert!(false, "page type must have no overflow cells", { "page_type": page_type }); } let overflow_cell = parent_contents.overflow_cells.first().unwrap(); let parent_page_cell_idx = self.stack.current_cell_index() as usize; // Parent page must be positioned at the divider cell that overflowed due to the replacement. turso_assert!( overflow_cell.index == parent_page_cell_idx, "overflow cell index must be the result of InteriorNodeReplacement that leaves both child and parent unbalanced, and hence parent page's position must equal overflow_cell.index", { "parent_page_id": parent_page.get().id, "parent_page_cell_idx": parent_page_cell_idx, "overflow_cell_index": overflow_cell.index } ); } self.pager.add_dirty(parent_page)?; let parent_contents = parent_page.get_contents(); let page_to_balance_idx = self.stack.current_cell_index() as usize; tracing::debug!( "balance_non_root(parent_id={} page_to_balance_idx={})", parent_page.get().id, page_to_balance_idx ); // Part 1: Find the sibling pages to balance let mut pages_to_balance: [Option; MAX_SIBLING_PAGES_TO_BALANCE] = [const { None }; MAX_SIBLING_PAGES_TO_BALANCE]; turso_assert!( page_to_balance_idx <= parent_contents.cell_count(), "page_to_balance_idx={page_to_balance_idx} is out of bounds for parent cell count {number_of_cells_in_parent}" ); // As there will be at maximum 3 pages used to balance: // sibling_pointer is the index represeneting one of those 3 pages, and we initialize it to the last possible page. // next_divider is the first divider that contains the first page of the 3 pages. let (sibling_pointer, first_cell_divider) = match number_of_cells_in_parent { n if n < 2 => (number_of_cells_in_parent, 0), 2 => (2, 0), // Here we will have at lest 2 cells and one right pointer, therefore we can get 3 siblings. // In case of 2 we will have all pages to balance. _ => { // In case of > 3 we have to check which ones to get let next_divider = if page_to_balance_idx == 0 { // first cell, take first 3 0 } else if page_to_balance_idx == number_of_cells_in_parent { // Page corresponds to right pointer, so take last 3 number_of_cells_in_parent - 2 } else { // Some cell in the middle, so we want to take sibling on left and right. page_to_balance_idx - 1 }; (2, next_divider) } }; let sibling_count = sibling_pointer + 1; let last_sibling_is_right_pointer = sibling_pointer + first_cell_divider - parent_contents.overflow_cells.len() == parent_contents.cell_count(); // Get the right page pointer that we will need to update later let right_pointer = if last_sibling_is_right_pointer { parent_contents.rightmost_pointer_raw()?.unwrap() } else { let max_overflow_cells = if matches!(page_type, PageType::IndexInterior) { 1 } else { 0 }; turso_assert!( parent_contents.overflow_cells.len() <= max_overflow_cells, "must have at most {max_overflow_cells} overflow cell in the parent" ); // OVERFLOW CELL ADJUSTMENT: // Let there be parent with cells [0,1,2,3,4]. // Let's imagine the cell at idx 2 gets replaced with a new payload that causes it to overflow. // See handling of InteriorNodeReplacement in btree.rs. // // In this case the rightmost divider is going to be 3 (2 is the middle one and we pick neighbors 1-3). // drop_cell(): [0,1,2,3,4] -> [0,1,3,4] <-- cells on right side get shifted left! // insert_into_cell(): [0,1,3,4] -> [0,1,3,4] + overflow cell (2) <-- crucially, no physical shifting happens, overflow cell is stored separately // // This means '3' is actually physically located at index '2'. // So IF the parent has an overflow cell, we need to subtract 1 to get the actual rightmost divider cell idx to physically read from. // The formula for the actual cell idx is: // first_cell_divider + sibling_pointer - parent_contents.overflow_cells.len() // so in the above case: // actual_cell_idx = 1 + 2 - 1 = 2 // // In the case where the last divider cell is the overflow cell, there would be no left-shifting of cells in drop_cell(), // because they are still positioned correctly (imagine .pop() from a vector). // However, note that we are always looking for the _rightmost_ child page pointer between the (max 2) dividers, and for any case where the last divider cell is the overflow cell, // the 'last_sibling_is_right_pointer' condition will also be true (since the overflow cell's left child will be the middle page), so we won't enter this code branch. // // Hence: when we enter this branch with overflow_cells.len() == 1, we know that left-shifting has happened and we need to subtract 1. let actual_cell_idx = first_cell_divider + sibling_pointer - parent_contents.overflow_cells.len(); let start_of_cell = parent_contents.cell_get_raw_start_offset(actual_cell_idx); let buf = parent_contents.as_ptr().as_mut_ptr(); unsafe { buf.add(start_of_cell) } }; // load sibling pages // start loading right page first let mut pgno: u32 = unsafe { right_pointer.cast::().read_unaligned().swap_bytes() }; let current_sibling = sibling_pointer; let mut group = CompletionGroup::new(|_| {}); for i in (0..=current_sibling).rev() { match btree_read_page(&self.pager, pgno as i64) { Err(e) => { mark_unlikely(); tracing::error!("error reading page {}: {}", pgno, e); group.cancel(); self.pager.io.drain()?; return Err(e); } Ok((page, c)) => { pages_to_balance[i].replace(PinGuard::new(page)); if let Some(c) = c { group.add(&c); } } } if i == 0 { break; } let next_cell_divider = i + first_cell_divider - 1; let divider_is_overflow_cell = parent_contents .overflow_cells .first() .is_some_and(|overflow_cell| overflow_cell.index == next_cell_divider); if divider_is_overflow_cell { turso_assert!( matches!( parent_contents.page_type().ok(), Some(PageType::IndexInterior) ), "expected index interior page", { "page_type": parent_contents.page_type().ok() } ); turso_assert!( parent_contents.overflow_cells.len() == 1, "must have a single overflow cell in the parent, as a result of InteriorNodeReplacement" ); let overflow_cell = parent_contents.overflow_cells.first().unwrap(); pgno = u32::from_be_bytes(overflow_cell.payload[0..4].try_into().unwrap()); } else { // grep for 'OVERFLOW CELL ADJUSTMENT' for explanation. // here we only subtract 1 if the divider cell has been shifted left, i.e. the overflow cell was placed to the left // this cell. let actual_cell_idx = if let Some(overflow_cell) = parent_contents.overflow_cells.first() { if next_cell_divider < overflow_cell.index { next_cell_divider } else { next_cell_divider - 1 } } else { next_cell_divider }; pgno = match parent_contents.cell_get(actual_cell_idx, usable_space)? { BTreeCell::TableInteriorCell(TableInteriorCell { left_child_page, .. }) | BTreeCell::IndexInteriorCell(IndexInteriorCell { left_child_page, .. }) => left_child_page, other => { mark_unlikely(); crate::bail_corrupt_error!( "expected interior cell, got {:?}", other ) } }; } } balance_info.replace(BalanceInfo { pages_to_balance, rightmost_pointer: right_pointer, sibling_count, first_divider_cell: first_cell_divider, reusable_divider_cell: Vec::new(), }); *sub_state = BalanceSubState::NonRootDoBalancing; let completion = group.build(); if !completion.finished() { io_yield_one!(completion); } } BalanceSubState::NonRootDoBalancing => { // Ensure all involved pages are in memory. let balance_info = balance_info.as_mut().unwrap(); for page in balance_info .pages_to_balance .iter() .take(balance_info.sibling_count) { let page = page.as_ref().unwrap(); self.pager.add_dirty(page)?; #[cfg(debug_assertions)] let page_type_of_siblings = balance_info.pages_to_balance[0] .as_ref() .unwrap() .get_contents() .page_type() .ok(); #[cfg(debug_assertions)] { let contents = page.get_contents(); debug_validate_cells!(&contents, usable_space); turso_assert_eq!(contents.page_type().ok(), page_type_of_siblings); } } // Start balancing. let parent_page = PinGuard::new(self.stack.top_ref().clone()); let parent_contents = parent_page.get_contents(); // Pre-compute parent page parameters for faster cell region lookups. // Note: cell_count cannot be pre-computed as it changes during the loop via drop_cell. let parent_page_type = parent_contents.page_type()?; let parent_max_local = payload_overflow_threshold_max(parent_page_type, usable_space); let parent_min_local = payload_overflow_threshold_min(parent_page_type, usable_space); // 1. Collect cell data from divider cells, and count the total number of cells to be distributed. // The count includes: all cells and overflow cells from the sibling pages, and divider cells from the parent page, // excluding the rightmost divider, which will not be dropped from the parent; instead it will be updated at the end. let mut total_cells_to_redistribute = 0; let pages_to_balance_new: [Option; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE] = [const { None }; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE]; for i in (0..balance_info.sibling_count).rev() { let sibling_page = balance_info.pages_to_balance[i].as_ref().unwrap(); turso_assert!(sibling_page.is_loaded(), "sibling page is not loaded"); let sibling_contents = sibling_page.get_contents(); total_cells_to_redistribute += sibling_contents.cell_count(); total_cells_to_redistribute += sibling_contents.overflow_cells.len(); // Right pointer is not dropped, we simply update it at the end. This could be a divider cell that points // to the last page in the list of pages to balance or this could be the rightmost pointer that points to a page. let is_last_sibling = i == balance_info.sibling_count - 1; if is_last_sibling { continue; } // Since we know we have a left sibling, take the divider that points to left sibling of this page let cell_idx = balance_info.first_divider_cell + i; let divider_is_overflow_cell = parent_contents .overflow_cells .first() .is_some_and(|overflow_cell| overflow_cell.index == cell_idx); let cell_buf = if divider_is_overflow_cell { turso_assert!( matches!( parent_contents.page_type().ok(), Some(PageType::IndexInterior) ), "expected index interior page", { "page_type": parent_contents.page_type().ok() } ); turso_assert!( parent_contents.overflow_cells.len() == 1, "must have a single overflow cell in the parent, as a result of InteriorNodeReplacement" ); let overflow_cell = parent_contents.overflow_cells.first().unwrap(); &overflow_cell.payload } else { // grep for 'OVERFLOW CELL ADJUSTMENT' for explanation. // here we can subtract overflow_cells.len() every time, because we are iterating right-to-left, // so if we are to the left of the overflow cell, it has already been cleared from the parent and overflow_cells.len() is 0. let actual_cell_idx = cell_idx - parent_contents.overflow_cells.len(); // Use pre-computed page parameters for faster lookup. // Note: cell_count must be fresh as it changes during the loop. let (cell_start, cell_len) = parent_contents ._cell_get_raw_region_faster( actual_cell_idx, usable_space, parent_contents.cell_count(), parent_max_local, parent_min_local, parent_page_type, )?; let buf = parent_contents.as_ptr(); &buf[cell_start..cell_start + cell_len] }; // Count the divider cell itself (which will be dropped from the parent) total_cells_to_redistribute += 1; tracing::debug!( "balance_non_root(drop_divider_cell, first_divider_cell={}, divider_cell={}, left_pointer={})", balance_info.first_divider_cell, i, read_u32(cell_buf, 0) ); // Reuse the divider buffer to avoid allocation per balance operation. // The buffer is cleared and filled with the new cell data. reusable_divider_buffers[i].clear(); reusable_divider_buffers[i].extend_from_slice(cell_buf); if divider_is_overflow_cell { tracing::debug!( "clearing overflow cells from parent cell_idx={}", cell_idx ); parent_contents.overflow_cells.clear(); } else { // grep for 'OVERFLOW CELL ADJUSTMENT' for explanation. // here we can subtract overflow_cells.len() every time, because we are iterating right-to-left, // so if we are to the left of the overflow cell, it has already been cleared from the parent and overflow_cells.len() is 0. let actual_cell_idx = cell_idx - parent_contents.overflow_cells.len(); tracing::trace!( "dropping divider cell from parent cell_idx={} count={}", actual_cell_idx, parent_contents.cell_count() ); drop_cell(parent_contents, actual_cell_idx, usable_space)?; } } /* 2. Initialize CellArray with all the cells used for distribution, this includes divider cells if !leaf. */ // Reuse the cell_payloads Vec from previous balance operations to avoid allocation. let mut cell_payloads_vec = std::mem::take(reusable_cell_payloads); cell_payloads_vec.clear(); // Ensure we have at least total_cells_to_redistribute capacity. // Since len=0 after clear, reserve(n) ensures capacity >= n. cell_payloads_vec.reserve(total_cells_to_redistribute); let mut cell_array = CellArray { cell_payloads: cell_payloads_vec, cell_count_per_page_cumulative: [0; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE], }; let cells_capacity_start = cell_array.cell_payloads.capacity(); let mut total_cells_inserted = 0; // This is otherwise identical to CellArray.cell_count_per_page_cumulative, // but we exclusively track what the prefix sums were _before_ we started redistributing cells. let mut old_cell_count_per_page_cumulative: [u16; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE] = [0; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE]; let page_type = balance_info.pages_to_balance[0] .as_ref() .unwrap() .get_contents() .page_type()?; tracing::debug!("balance_non_root(page_type={:?})", page_type); let is_table_leaf = matches!(page_type, PageType::TableLeaf); let is_leaf = matches!(page_type, PageType::TableLeaf | PageType::IndexLeaf); for (i, old_page) in balance_info .pages_to_balance .iter() .take(balance_info.sibling_count) .enumerate() { let old_page = old_page.as_ref().unwrap(); let old_page_contents = old_page.get_contents(); let page_type = old_page_contents.page_type()?; let max_local = payload_overflow_threshold_max(page_type, usable_space); let min_local = payload_overflow_threshold_min(page_type, usable_space); let cell_count = old_page_contents.cell_count(); debug_validate_cells!(&old_page_contents, usable_space); for cell_idx in 0..cell_count { let (cell_start, cell_len) = old_page_contents ._cell_get_raw_region_faster( cell_idx, usable_space, cell_count, max_local, min_local, page_type, )?; let buf = old_page_contents.as_ptr(); let cell_buf = &mut buf[cell_start..cell_start + cell_len]; // TODO(pere): make this reference and not copy cell_array.cell_payloads.push(to_static_buf(cell_buf)); } // Insert overflow cells into correct place let offset = total_cells_inserted; for overflow_cell in old_page_contents.overflow_cells.iter_mut() { cell_array.cell_payloads.insert( offset + overflow_cell.index, to_static_buf(&mut Pin::as_mut(&mut overflow_cell.payload)), ); } old_cell_count_per_page_cumulative[i] = cell_array.cell_payloads.len() as u16; let mut cells_inserted = old_page_contents.cell_count() + old_page_contents.overflow_cells.len(); let is_last_sibling = i == balance_info.sibling_count - 1; if !is_last_sibling && !is_table_leaf { // If we are a index page or a interior table page we need to take the divider cell too. // But we don't need the last divider as it will remain the same. let mut divider_cell = reusable_divider_buffers[i].as_mut_slice(); // TODO(pere): in case of old pages are leaf pages, so index leaf page, we need to strip page pointers // from divider cells in index interior pages (parent) because those should not be included. cells_inserted += 1; if !is_leaf { // This divider cell needs to be updated with new left pointer, let right_pointer = old_page_contents.rightmost_pointer()?.unwrap(); divider_cell[..LEFT_CHILD_PTR_SIZE_BYTES] .copy_from_slice(&right_pointer.to_be_bytes()); } else { // index leaf turso_assert!( divider_cell.len() >= LEFT_CHILD_PTR_SIZE_BYTES, "divider cell is too short" ); // let's strip the page pointer divider_cell = &mut divider_cell[LEFT_CHILD_PTR_SIZE_BYTES..]; } cell_array.cell_payloads.push(to_static_buf(divider_cell)); } total_cells_inserted += cells_inserted; } turso_assert!( cell_array.cell_payloads.capacity() == cells_capacity_start, "calculation of max cells was wrong" ); // Verify that all cells were collected correctly. // Note: For table leaf pages, dividers are counted in total_cells_to_redistribute // but are NOT included in cell_array (they stay in parent as bookkeeping). // For index/interior pages, dividers ARE included in cell_array. let dividers_in_parent_only = if is_table_leaf { // Table leaf: dividers are NOT added to cell_array balance_info.sibling_count.saturating_sub(1) } else { // Index/interior: dividers ARE added to cell_array 0 }; let expected_cells_in_array = total_cells_to_redistribute - dividers_in_parent_only; turso_assert!( cell_array.cell_payloads.len() == expected_cells_in_array, "cell count mismatch after collection", { "collected": cell_array.cell_payloads.len(), "expected": expected_cells_in_array, "total_cells_to_redistribute": total_cells_to_redistribute, "dividers_in_parent_only": dividers_in_parent_only, "is_table_leaf": is_table_leaf } ); turso_assert!( total_cells_inserted == expected_cells_in_array, "cell count mismatch between total cells inserted and expected", { "total_cells_inserted": total_cells_inserted, "expected_cells_in_array": expected_cells_in_array, "total_cells_to_redistribute": total_cells_to_redistribute, "dividers_in_parent_only": dividers_in_parent_only } ); // Let's copy all cells for later checks #[cfg(debug_assertions)] let mut cells_debug = Vec::new(); #[cfg(debug_assertions)] { for cell in &cell_array.cell_payloads { cells_debug.push(cell.to_vec()); if is_leaf { crate::turso_assert_ne!(cell[0], 0); } } } #[cfg(debug_assertions)] validate_cells_after_insertion(&cell_array, is_table_leaf); /* 3. Initiliaze current size of every page including overflow cells and divider cells that might be included. */ let mut new_page_sizes: [i64; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE] = [0; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE]; let header_size = if is_leaf { LEAF_PAGE_HEADER_SIZE_BYTES } else { INTERIOR_PAGE_HEADER_SIZE_BYTES }; // number of bytes beyond header, different from global usableSapce which includes // header let usable_space_without_header = usable_space - header_size; for i in 0..balance_info.sibling_count { cell_array.cell_count_per_page_cumulative[i] = old_cell_count_per_page_cumulative[i]; let page = &balance_info.pages_to_balance[i].as_ref().unwrap(); let page_contents = page.get_contents(); let free_space = compute_free_space(page_contents, usable_space)?; new_page_sizes[i] = usable_space_without_header as i64 - free_space as i64; for overflow in &page_contents.overflow_cells { // 2 to account of pointer new_page_sizes[i] += 2 + overflow.payload.len() as i64; } let is_last_sibling = i == balance_info.sibling_count - 1; if !is_leaf && !is_last_sibling { // Account for divider cell which is included in this page. new_page_sizes[i] += cell_array.cell_payloads [cell_array.cell_count_up_to_page(i)] .len() as i64; } } /* 4. Now let's try to move cells to the left trying to stack them without exceeding the maximum size of a page. There are two cases: * If current page has too many cells, it will move them to the next page. * If it still has space, and it can take a cell from the right it will take them. Here there is a caveat. Taking a cell from the right might take cells from page i+1, i+2, i+3, so not necessarily adjacent. But we decrease the size of the adjacent page if we move from the right. This might cause a intermitent state where page can have size <0. This will also calculate how many pages are required to balance the cells and store in sibling_count_new. */ // Try to pack as many cells to the left let mut sibling_count_new = balance_info.sibling_count; let mut i = 0; while i < sibling_count_new { // First try to move cells to the right if they do not fit while new_page_sizes[i] > usable_space_without_header as i64 { let needs_new_page = i + 1 >= sibling_count_new; if needs_new_page { sibling_count_new = i + 2; turso_assert!( sibling_count_new <= 5, "it is corrupt to require more than 5 pages to balance 3 siblings" ); new_page_sizes[sibling_count_new - 1] = 0; cell_array.cell_count_per_page_cumulative[sibling_count_new - 1] = cell_array.cell_payloads.len() as u16; } let size_of_cell_to_remove_from_left = 2 + cell_array.cell_payloads [cell_array.cell_count_up_to_page(i) - 1] .len() as i64; new_page_sizes[i] -= size_of_cell_to_remove_from_left; let size_of_cell_to_move_right = if !is_table_leaf { if cell_array.cell_count_per_page_cumulative[i] < cell_array.cell_payloads.len() as u16 { // This means we move to the right page the divider cell and we // promote left cell to divider CELL_PTR_SIZE_BYTES as i64 + cell_array.cell_payloads [cell_array.cell_count_up_to_page(i)] .len() as i64 } else { 0 } } else { size_of_cell_to_remove_from_left }; new_page_sizes[i + 1] += size_of_cell_to_move_right; cell_array.cell_count_per_page_cumulative[i] -= 1; } // Now try to take from the right if we didn't have enough while cell_array.cell_count_per_page_cumulative[i] < cell_array.cell_payloads.len() as u16 { let size_of_cell_to_remove_from_right = CELL_PTR_SIZE_BYTES as i64 + cell_array.cell_payloads[cell_array.cell_count_up_to_page(i)] .len() as i64; let can_take = new_page_sizes[i] + size_of_cell_to_remove_from_right > usable_space_without_header as i64; if can_take { break; } new_page_sizes[i] += size_of_cell_to_remove_from_right; cell_array.cell_count_per_page_cumulative[i] += 1; let size_of_cell_to_remove_from_right = if !is_table_leaf { if cell_array.cell_count_per_page_cumulative[i] < cell_array.cell_payloads.len() as u16 { CELL_PTR_SIZE_BYTES as i64 + cell_array.cell_payloads [cell_array.cell_count_up_to_page(i)] .len() as i64 } else { 0 } } else { size_of_cell_to_remove_from_right }; new_page_sizes[i + 1] -= size_of_cell_to_remove_from_right; } // Check if this page contains up to the last cell. If this happens it means we really just need up to this page. // Let's update the number of new pages to be up to this page (i+1) let page_completes_all_cells = cell_array.cell_count_per_page_cumulative[i] >= cell_array.cell_payloads.len() as u16; if page_completes_all_cells { sibling_count_new = i + 1; break; } i += 1; if i >= sibling_count_new { break; } } tracing::debug!( "balance_non_root(sibling_count={}, sibling_count_new={}, cells={})", balance_info.sibling_count, sibling_count_new, cell_array.cell_payloads.len() ); /* 5. Balance pages starting from a left stacked cell state and move them to right trying to maintain a balanced state where we only move from left to right if it will not unbalance both pages, meaning moving left to right won't make right page bigger than left page. */ // Comment borrowed from SQLite src/btree.c // The packing computed by the previous block is biased toward the siblings // on the left side (siblings with smaller keys). The left siblings are // always nearly full, while the right-most sibling might be nearly empty. // The next block of code attempts to adjust the packing of siblings to // get a better balance. // // This adjustment is more than an optimization. The packing above might // be so out of balance as to be illegal. For example, the right-most // sibling might be completely empty. This adjustment is not optional. for i in (1..sibling_count_new).rev() { let mut size_right_page = new_page_sizes[i]; let mut size_left_page = new_page_sizes[i - 1]; let mut cell_left = cell_array.cell_count_per_page_cumulative[i - 1] - 1; // When table leaves are being balanced, divider cells are not part of the balancing, // because table dividers don't have payloads unlike index dividers. // Hence: // - For table leaves: the same cell that is removed from left is added to right. // - For all other page types: the divider cell is added to right, and the last non-divider cell is removed from left; // the cell removed from the left will later become a new divider cell in the parent page. // TABLE LEAVES BALANCING: // ======================= // Before balancing: // LEFT RIGHT // +-----+-----+-----+-----+ +-----+-----+ // | C1 | C2 | C3 | C4 | | C5 | C6 | // +-----+-----+-----+-----+ +-----+-----+ // ^ ^ // (too full) (has space) // After balancing: // LEFT RIGHT // +-----+-----+-----+ +-----+-----+-----+ // | C1 | C2 | C3 | | C4 | C5 | C6 | // +-----+-----+-----+ +-----+-----+-----+ // ^ // (C4 moved directly) // // (C3's rowid also becomes the divider cell's rowid in the parent page // // OTHER PAGE TYPES BALANCING: // =========================== // Before balancing: // PARENT: [...|D1|...] // | // LEFT RIGHT // +-----+-----+-----+-----+ +-----+-----+ // | K1 | K2 | K3 | K4 | | K5 | K6 | // +-----+-----+-----+-----+ +-----+-----+ // ^ ^ // (too full) (has space) // After balancing: // PARENT: [...|K4|...] <-- K4 becomes new divider // | // LEFT RIGHT // +-----+-----+-----+ +-----+-----+-----+ // | K1 | K2 | K3 | | D1 | K5 | K6 | // +-----+-----+-----+ +-----+-----+-----+ // ^ // (old divider D1 added to right) // Legend: // - C# = Cell (table leaf) // - K# = Key cell (index/internal node) // - D# = Divider cell let mut cell_right = if is_table_leaf { cell_left } else { cell_left + 1 }; loop { let cell_left_size = cell_array.cell_size_bytes(cell_left as usize) as i64; let cell_right_size = cell_array.cell_size_bytes(cell_right as usize) as i64; // TODO: add assert nMaxCells let is_last_sibling = i == sibling_count_new - 1; let pointer_size = if is_last_sibling { 0 } else { CELL_PTR_SIZE_BYTES as i64 }; // As mentioned, this step rebalances the siblings so that cells are moved from left to right, since the previous step just // packed as much as possible to the left. However, if the right-hand-side page would become larger than the left-hand-side page, // we stop. let would_not_improve_balance = size_right_page + cell_right_size + (CELL_PTR_SIZE_BYTES as i64) > size_left_page - (cell_left_size + pointer_size); if size_right_page != 0 && would_not_improve_balance { break; } size_left_page -= cell_left_size + (CELL_PTR_SIZE_BYTES as i64); size_right_page += cell_right_size + (CELL_PTR_SIZE_BYTES as i64); cell_array.cell_count_per_page_cumulative[i - 1] = cell_left; if cell_left == 0 { break; } cell_left -= 1; cell_right -= 1; } new_page_sizes[i] = size_right_page; new_page_sizes[i - 1] = size_left_page; turso_assert_greater_than!( cell_array.cell_count_per_page_cumulative[i - 1], if i > 1 { cell_array.cell_count_per_page_cumulative[i - 2] } else { 0 } ); } *sub_state = BalanceSubState::NonRootDoBalancingAllocate { i: 0, context: Some(BalanceContext { pages_to_balance_new, sibling_count_new, cell_array, old_cell_count_per_page_cumulative, #[cfg(debug_assertions)] cells_debug, }), }; } BalanceSubState::NonRootDoBalancingAllocate { i, context } => { let BalanceContext { pages_to_balance_new, old_cell_count_per_page_cumulative, cell_array, sibling_count_new, .. } = context.as_mut().unwrap(); let pager = self.pager.clone(); let balance_info = balance_info.as_mut().unwrap(); let page_type = balance_info.pages_to_balance[0] .as_ref() .unwrap() .get_contents() .page_type()?; // Allocate pages or set dirty if not needed if *i < balance_info.sibling_count { let page = balance_info.pages_to_balance[*i].as_ref().unwrap(); turso_assert!(page.is_dirty(), "sibling page must be already marked dirty"); pages_to_balance_new[*i].replace(page.clone()); } else { let page = return_if_io!(pager.do_allocate_page( page_type, 0, BtreePageAllocMode::Any )); pages_to_balance_new[*i].replace(PinGuard::new(page)); // Since this page didn't exist before, we can set it to cells length as it // marks them as empty since it is a prefix sum of cells. old_cell_count_per_page_cumulative[*i] = cell_array.cell_payloads.len() as u16; } if *i + 1 < *sibling_count_new { *i += 1; continue; } else { *sub_state = BalanceSubState::NonRootDoBalancingFinish { context: context.take().unwrap(), }; } } BalanceSubState::NonRootDoBalancingFinish { context: BalanceContext { pages_to_balance_new, sibling_count_new, cell_array, old_cell_count_per_page_cumulative, #[cfg(debug_assertions)] cells_debug, }, } => { let balance_info = balance_info.as_mut().unwrap(); let page_type = balance_info.pages_to_balance[0] .as_ref() .unwrap() .get_contents() .page_type()?; let parent_is_root = !self.stack.has_parent(); let parent_page = PinGuard::new(self.stack.top_ref().clone()); let parent_contents = parent_page.get_contents(); let mut sibling_count_new = *sibling_count_new; let is_table_leaf = matches!(page_type, PageType::TableLeaf); // Reassign page numbers in increasing order { let mut page_numbers: [usize; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE] = [0; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE]; for (i, page) in pages_to_balance_new .iter() .take(sibling_count_new) .enumerate() { page_numbers[i] = page.as_ref().unwrap().get().id; } page_numbers.sort_unstable(); for (page, new_id) in pages_to_balance_new .iter() .take(sibling_count_new) .rev() .zip(page_numbers.iter().rev().take(sibling_count_new)) { let page = page.as_ref().unwrap(); if *new_id != page.get().id { page.get().id = *new_id; self.pager .upsert_page_in_cache(*new_id, page.0.clone(), true)?; } } #[cfg(debug_assertions)] { tracing::debug!( "balance_non_root(parent page_id={})", parent_page.get().id ); for page in pages_to_balance_new.iter().take(sibling_count_new) { tracing::debug!( "balance_non_root(new_sibling page_id={})", page.as_ref().unwrap().get().id ); } } } // pages_pointed_to helps us debug we did in fact create divider cells to all the new pages and the rightmost pointer, // also points to the last page. #[cfg(debug_assertions)] let mut pages_pointed_to = HashSet::default(); // Write right pointer in parent page to point to new rightmost page. keep in mind // we update rightmost pointer first because inserting cells could defragment parent page, // therfore invalidating the pointer. let right_page_id = pages_to_balance_new[sibling_count_new - 1] .as_ref() .unwrap() .get() .id as u32; let rightmost_pointer = balance_info.rightmost_pointer; let rightmost_pointer = unsafe { std::slice::from_raw_parts_mut(rightmost_pointer, 4) }; rightmost_pointer[0..4].copy_from_slice(&right_page_id.to_be_bytes()); #[cfg(debug_assertions)] pages_pointed_to.insert(right_page_id); tracing::debug!( "balance_non_root(rightmost_pointer_update, rightmost_pointer={})", right_page_id ); /* 6. Update parent pointers. Update right pointer and insert divider cells with newly created distribution of cells */ // Ensure right-child pointer of the right-most new sibling pge points to the page // that was originally on that place. let is_leaf_page = matches!(page_type, PageType::TableLeaf | PageType::IndexLeaf); if !is_leaf_page { let last_sibling_idx = balance_info.sibling_count - 1; let last_page = balance_info.pages_to_balance[last_sibling_idx] .as_ref() .unwrap(); let right_pointer = last_page.get_contents().rightmost_pointer()?.unwrap(); let new_last_page = pages_to_balance_new[sibling_count_new - 1] .as_ref() .unwrap(); new_last_page .get_contents() .write_rightmost_ptr(right_pointer); } turso_assert!( parent_contents.overflow_cells.is_empty(), "parent page overflow cells should be empty before divider cell reinsertion" ); // TODO: pointer map update (vacuum support) // Update divider cells in parent // Cache first_divider_cell to allow mutable access to reusable_divider_cell let first_divider_cell_cached = balance_info.first_divider_cell; for (sibling_page_idx, page) in pages_to_balance_new .iter() .enumerate() .take(sibling_count_new - 1) /* do not take last page */ { let page = page.as_ref().unwrap(); // e.g. if we have 3 pages and the leftmost child page has 3 cells, // then the divider cell idx is 3 in the flat cell array. let divider_cell_idx = cell_array.cell_count_up_to_page(sibling_page_idx); let mut divider_cell = &mut cell_array.cell_payloads[divider_cell_idx]; // Reuse the buffer for constructing new divider cell to avoid allocation per iteration balance_info.reusable_divider_cell.clear(); if !is_leaf_page { // Interior // Make this page's rightmost pointer point to pointer of divider cell before modification let previous_pointer_divider = read_u32(divider_cell, 0); page.get_contents() .write_rightmost_ptr(previous_pointer_divider); // divider cell now points to this page balance_info .reusable_divider_cell .extend_from_slice(&(page.get().id as u32).to_be_bytes()); // now copy the rest of the divider cell: // Table Interior page: // * varint rowid // Index Interior page: // * varint payload size // * payload // * first overflow page (u32 optional) balance_info .reusable_divider_cell .extend_from_slice(÷r_cell[4..]); } else if is_table_leaf { // For table leaves, divider_cell_idx effectively points to the last cell of the old left page. // The new divider cell's rowid becomes the second-to-last cell's rowid. // i.e. in the diagram above, the new divider cell's rowid becomes the rowid of C3. // FIXME: not needed conversion // FIXME: need to update cell size in order to free correctly? // insert into cell with correct range should be enough divider_cell = &mut cell_array.cell_payloads[divider_cell_idx - 1]; let (_, n_bytes_payload) = read_varint(divider_cell)?; let (rowid, _) = read_varint(÷r_cell[n_bytes_payload..])?; balance_info .reusable_divider_cell .extend_from_slice(&(page.get().id as u32).to_be_bytes()); write_varint_to_vec(rowid, &mut balance_info.reusable_divider_cell); } else { // Leaf index balance_info .reusable_divider_cell .extend_from_slice(&(page.get().id as u32).to_be_bytes()); balance_info .reusable_divider_cell .extend_from_slice(divider_cell); } let left_pointer = read_u32( &balance_info.reusable_divider_cell[..LEFT_CHILD_PTR_SIZE_BYTES], 0, ); turso_assert!( left_pointer != parent_page.get().id as u32, "left pointer is the same as parent page id" ); #[cfg(debug_assertions)] { pages_pointed_to.insert(left_pointer); tracing::debug!( "balance_non_root(insert_divider_cell, first_divider_cell={}, divider_cell={}, left_pointer={})", first_divider_cell_cached, sibling_page_idx, left_pointer ); } turso_assert!( left_pointer == page.get().id as u32, "left pointer is not the same as page id" ); // FIXME: remove this lock let database_size = self .pager .io .block(|| self.pager.with_header(|header| header.database_size))? .get(); turso_assert!( left_pointer <= database_size, "invalid page number divider left pointer exceeds database number of pages", { "left_pointer": left_pointer, "database_size": database_size } ); let divider_cell_insert_idx_in_parent = first_divider_cell_cached + sibling_page_idx; #[cfg(debug_assertions)] let overflow_cell_count_before = parent_contents.overflow_cells.len(); insert_into_cell( parent_contents, &balance_info.reusable_divider_cell, divider_cell_insert_idx_in_parent, usable_space, )?; #[cfg(debug_assertions)] { let overflow_cell_count_after = parent_contents.overflow_cells.len(); let divider_cell_is_overflow_cell = overflow_cell_count_after > overflow_cell_count_before; BTreeCursor::validate_balance_non_root_divider_cell_insertion( balance_info, parent_contents, divider_cell_insert_idx_in_parent, divider_cell_is_overflow_cell, page, usable_space, ); } } tracing::debug!( "balance_non_root(parent_overflow={})", parent_contents.overflow_cells.len() ); #[cfg(debug_assertions)] { // Let's ensure every page is pointed to by the divider cell or the rightmost pointer. for page in pages_to_balance_new.iter().take(sibling_count_new) { let page = page.as_ref().unwrap(); turso_assert!( pages_pointed_to.contains(&(page.get().id as u32)), "page not pointed to by divider cell or rightmost pointer", { "page_id": page.get().id } ); } } /* 7. Start real movement of cells. Next comment is borrowed from SQLite: */ /* Now update the actual sibling pages. The order in which they are updated ** is important, as this code needs to avoid disrupting any page from which ** cells may still to be read. In practice, this means: ** ** (1) If cells are moving left (from apNew[iPg] to apNew[iPg-1]) ** then it is not safe to update page apNew[iPg] until after ** the left-hand sibling apNew[iPg-1] has been updated. ** ** (2) If cells are moving right (from apNew[iPg] to apNew[iPg+1]) ** then it is not safe to update page apNew[iPg] until after ** the right-hand sibling apNew[iPg+1] has been updated. ** ** If neither of the above apply, the page is safe to update. ** ** The iPg value in the following loop starts at nNew-1 goes down ** to 0, then back up to nNew-1 again, thus making two passes over ** the pages. On the initial downward pass, only condition (1) above ** needs to be tested because (2) will always be true from the previous ** step. On the upward pass, both conditions are always true, so the ** upwards pass simply processes pages that were missed on the downward ** pass. */ let mut done = [false; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE]; let rightmost_page_negative_idx = 1 - sibling_count_new as i64; let rightmost_page_positive_idx = sibling_count_new as i64 - 1; for i in rightmost_page_negative_idx..=rightmost_page_positive_idx { // As mentioned above, we do two passes over the pages: // 1. Downward pass: Process pages in decreasing order // 2. Upward pass: Process pages in increasing order // Hence if we have 3 siblings: // the order of 'i' will be: -2, -1, 0, 1, 2. // and the page processing order is: 2, 1, 0, 1, 2. let page_idx = i.unsigned_abs() as usize; if done[page_idx] { continue; } // As outlined above, this condition ensures we process pages in the correct order to avoid disrupting cells that still need to be read. // 1. i >= 0 handles the upward pass where we process any pages not processed in the downward pass. // - condition (1) is not violated: if cells are moving right-to-left, righthand sibling has not been updated yet. // - condition (2) is not violated: if cells are moving left-to-right, righthand sibling has already been updated in the downward pass. // 2. The second condition checks if it's safe to process a page during the downward pass. // - condition (1) is not violated: if cells are moving right-to-left, we do nothing. // - condition (2) is not violated: if cells are moving left-to-right, we are allowed to update. if i >= 0 || old_cell_count_per_page_cumulative[page_idx - 1] >= cell_array.cell_count_per_page_cumulative[page_idx - 1] { let (start_old_cells, start_new_cells, number_new_cells) = if page_idx == 0 { (0, 0, cell_array.cell_count_up_to_page(0)) } else { let this_was_old_page = page_idx < balance_info.sibling_count; // We add !is_table_leaf because we want to skip 1 in case of divider cell which is encountared between pages assigned let start_old_cells = if this_was_old_page { old_cell_count_per_page_cumulative[page_idx - 1] as usize + (!is_table_leaf) as usize } else { cell_array.cell_payloads.len() }; let start_new_cells = cell_array .cell_count_up_to_page(page_idx - 1) + (!is_table_leaf) as usize; ( start_old_cells, start_new_cells, cell_array.cell_count_up_to_page(page_idx) - start_new_cells, ) }; let page = pages_to_balance_new[page_idx].as_ref().unwrap(); tracing::debug!("pre_edit_page(page={})", page.get().id); let page_contents = page.get_contents(); edit_page( page_contents, start_old_cells, start_new_cells, number_new_cells, cell_array, usable_space, )?; debug_validate_cells!(page_contents, usable_space); tracing::trace!( "edit_page page={} cells={}", page.get().id, page_contents.cell_count() ); page_contents.overflow_cells.clear(); done[page_idx] = true; } } // TODO: vacuum support let first_child_page = pages_to_balance_new[0].as_ref().unwrap(); let first_child_contents = first_child_page.get_contents(); if parent_is_root && parent_contents.cell_count() == 0 // this check to make sure we are not having negative free space && parent_contents.offset() <= compute_free_space(first_child_contents, usable_space)? { // From SQLite: // The root page of the b-tree now contains no cells. The only sibling // page is the right-child of the parent. Copy the contents of the // child page into the parent, decreasing the overall height of the // b-tree structure by one. This is described as the "balance-shallower" // sub-algorithm in some documentation. turso_assert_eq!(sibling_count_new, 1); let parent_offset = if parent_page.get().id == 1 { DatabaseHeader::SIZE } else { 0 }; #[cfg(debug_assertions)] turso_assert_eq!(parent_offset, parent_contents.offset()); // From SQLite: // It is critical that the child page be defragmented before being // copied into the parent, because if the parent is page 1 then it will // by smaller than the child due to the database header, and so // all the free space needs to be up front. defragment_page_full(first_child_contents, usable_space)?; let child_top = first_child_contents.cell_content_area() as usize; let parent_buf = parent_contents.as_ptr(); let child_buf = first_child_contents.as_ptr(); let content_size = usable_space - child_top; // Copy cell contents parent_buf[child_top..child_top + content_size] .copy_from_slice(&child_buf[child_top..child_top + content_size]); // Copy header and pointer // NOTE: don't use .cell_pointer_array_offset_and_size() because of different // header size let header_and_pointer_size = first_child_contents.header_size() + first_child_contents.cell_pointer_array_size(); let first_child_offset = first_child_contents.offset(); parent_buf[parent_offset..parent_offset + header_and_pointer_size] .copy_from_slice( &child_buf[first_child_offset ..first_child_offset + header_and_pointer_size], ); sibling_count_new -= 1; // decrease sibling count for debugging and free at the end turso_assert_less_than!(sibling_count_new, balance_info.sibling_count); } #[cfg(debug_assertions)] BTreeCursor::post_balance_non_root_validation( &parent_page, balance_info, parent_contents, pages_to_balance_new, page_type, is_table_leaf, cells_debug, sibling_count_new, right_page_id, usable_space, ); // Balance-shallower case if sibling_count_new == 0 { self.stack.set_cell_index(0); // reset cell index, top is already parent } // Restore the cell_payloads Vec to BalanceState for reuse in future operations. // This avoids allocation on subsequent balance operations. let mut recovered_vec = std::mem::take(&mut cell_array.cell_payloads); recovered_vec.clear(); *reusable_cell_payloads = recovered_vec; *sub_state = BalanceSubState::FreePages { curr_page: sibling_count_new, sibling_count_new, }; } BalanceSubState::FreePages { curr_page, sibling_count_new, } => { let sibling_count = { balance_info .as_ref() .expect("must be balancing") .sibling_count }; // We have to free pages that are not used anymore if !((*sibling_count_new..sibling_count).contains(curr_page)) { *sub_state = BalanceSubState::Start; let _ = balance_info.take(); return Ok(IOResult::Done(())); } else { let balance_info = balance_info.as_ref().expect("must be balancing"); let page = balance_info.pages_to_balance[*curr_page].as_ref().unwrap(); return_if_io!(self.pager.free_page(Some(page.0.clone()), page.get().id)); *sub_state = BalanceSubState::FreePages { curr_page: *curr_page + 1, sibling_count_new: *sibling_count_new, }; } } } } } /// Validates that a divider cell was correctly inserted into the parent page /// during B-tree balancing and that it points to the correct child page. #[cfg(debug_assertions)] fn validate_balance_non_root_divider_cell_insertion( balance_info: &BalanceInfo, parent_contents: &mut PageContent, divider_cell_insert_idx_in_parent: usize, divider_cell_is_overflow_cell: bool, child_page: &PageRef, usable_space: usize, ) { let left_pointer = if divider_cell_is_overflow_cell { parent_contents.overflow_cells .iter() .find(|cell| cell.index == divider_cell_insert_idx_in_parent) .map(|cell| read_u32(&cell.payload, 0)) .unwrap_or_else(|| { panic!( "overflow cell with divider cell was not found (divider_cell_idx={}, balance_info.first_divider_cell={}, overflow_cells.len={})", divider_cell_insert_idx_in_parent, balance_info.first_divider_cell, parent_contents.overflow_cells.len(), ) }) } else if divider_cell_insert_idx_in_parent < parent_contents.cell_count() { let (cell_start, cell_len) = parent_contents .cell_get_raw_region(divider_cell_insert_idx_in_parent, usable_space) .unwrap(); read_u32( &parent_contents.as_ptr()[cell_start..cell_start + cell_len], 0, ) } else { panic!( "divider cell is not in the parent page (divider_cell_idx={}, balance_info.first_divider_cell={}, overflow_cells.len={})", divider_cell_insert_idx_in_parent, balance_info.first_divider_cell, parent_contents.overflow_cells.len(), ) }; // Verify the left pointer points to the correct page turso_assert_eq!( left_pointer, child_page.get().id as u32, "inserted cell doesn't point to correct page", { "left_pointer": left_pointer, "child_page_id": child_page.get().id } ); } #[cfg(debug_assertions)] #[allow(clippy::too_many_arguments)] fn post_balance_non_root_validation( parent_page: &PageRef, balance_info: &BalanceInfo, parent_contents: &mut PageContent, pages_to_balance_new: &[Option; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE], page_type: PageType, is_table_leaf: bool, cells_debug: &mut [Vec], sibling_count_new: usize, right_page_id: u32, usable_space: usize, ) { let mut valid = true; let mut current_index_cell = 0; for cell_idx in 0..parent_contents.cell_count() { let cell = parent_contents.cell_get(cell_idx, usable_space).unwrap(); match cell { BTreeCell::TableInteriorCell(table_interior_cell) => { let left_child_page = table_interior_cell.left_child_page; if left_child_page == parent_page.get().id as u32 { tracing::error!("balance_non_root(parent_divider_points_to_same_page, page_id={}, cell_left_child_page={})", parent_page.get().id, left_child_page, ); valid = false; } } BTreeCell::IndexInteriorCell(index_interior_cell) => { let left_child_page = index_interior_cell.left_child_page; if left_child_page == parent_page.get().id as u32 { tracing::error!("balance_non_root(parent_divider_points_to_same_page, page_id={}, cell_left_child_page={})", parent_page.get().id, left_child_page, ); valid = false; } } _ => {} } } // Let's now make a in depth check that we in fact added all possible cells somewhere and they are not lost for (page_idx, page) in pages_to_balance_new .iter() .take(sibling_count_new) .enumerate() { let page = page.as_ref().unwrap(); let contents = page.get_contents(); debug_validate_cells!(contents, usable_space); // Cells are distributed in order for cell_idx in 0..contents.cell_count() { let (cell_start, cell_len) = contents .cell_get_raw_region(cell_idx, usable_space) .unwrap(); let buf = contents.as_ptr(); let cell_buf = to_static_buf(&mut buf[cell_start..cell_start + cell_len]); let cell_buf_in_array = &cells_debug[current_index_cell]; if cell_buf != cell_buf_in_array { tracing::error!("balance_non_root(cell_not_found_debug, page_id={}, cell_in_cell_array_idx={})", page.get().id, current_index_cell, ); valid = false; } let cell = crate::storage::sqlite3_ondisk::read_btree_cell( cell_buf, contents, 0, usable_space, ) .unwrap(); match &cell { BTreeCell::TableInteriorCell(table_interior_cell) => { let left_child_page = table_interior_cell.left_child_page; if left_child_page == page.get().id as u32 { tracing::error!("balance_non_root(child_page_points_same_page, page_id={}, cell_left_child_page={}, page_idx={})", page.get().id, left_child_page, page_idx ); valid = false; } if left_child_page == parent_page.get().id as u32 { tracing::error!("balance_non_root(child_page_points_parent_of_child, page_id={}, cell_left_child_page={}, page_idx={})", page.get().id, left_child_page, page_idx ); valid = false; } } BTreeCell::IndexInteriorCell(index_interior_cell) => { let left_child_page = index_interior_cell.left_child_page; if left_child_page == page.get().id as u32 { tracing::error!("balance_non_root(child_page_points_same_page, page_id={}, cell_left_child_page={}, page_idx={})", page.get().id, left_child_page, page_idx ); valid = false; } if left_child_page == parent_page.get().id as u32 { tracing::error!("balance_non_root(child_page_points_parent_of_child, page_id={}, cell_left_child_page={}, page_idx={})", page.get().id, left_child_page, page_idx ); valid = false; } } _ => {} } current_index_cell += 1; } // Now check divider cells and their pointers. let parent_buf = parent_contents.as_ptr(); let cell_divider_idx = balance_info.first_divider_cell + page_idx; if sibling_count_new == 0 { // Balance-shallower case // We need to check data in parent page debug_validate_cells!(parent_contents, usable_space); if pages_to_balance_new[0].is_none() { tracing::error!( "balance_non_root(balance_shallower_incorrect_page, page_idx={})", 0 ); valid = false; } for (i, value) in pages_to_balance_new .iter() .enumerate() .take(sibling_count_new) .skip(1) { if value.is_some() { tracing::error!( "balance_non_root(balance_shallower_incorrect_page, page_idx={})", i ); valid = false; } } if current_index_cell != cells_debug.len() || cells_debug.len() != contents.cell_count() || contents.cell_count() != parent_contents.cell_count() { tracing::error!("balance_non_root(balance_shallower_incorrect_cell_count, current_index_cell={}, cells_debug={}, cell_count={}, parent_cell_count={})", current_index_cell, cells_debug.len(), contents.cell_count(), parent_contents.cell_count() ); valid = false; } if right_page_id == page.get().id as u32 || right_page_id == parent_page.get().id as u32 { tracing::error!("balance_non_root(balance_shallower_rightmost_pointer, page_id={}, parent_page_id={}, rightmost={})", page.get().id, parent_page.get().id, right_page_id, ); valid = false; } if let Some(rm) = contents.rightmost_pointer().ok().flatten() { if rm != right_page_id { tracing::error!("balance_non_root(balance_shallower_rightmost_pointer, page_rightmost={}, rightmost={})", rm, right_page_id, ); valid = false; } } if let Some(rm) = parent_contents.rightmost_pointer().ok().flatten() { if rm != right_page_id { tracing::error!("balance_non_root(balance_shallower_rightmost_pointer, parent_rightmost={}, rightmost={})", rm, right_page_id, ); valid = false; } } if parent_contents.page_type().ok() != Some(page_type) { tracing::error!("balance_non_root(balance_shallower_parent_page_type, page_type={:?}, parent_page_type={:?})", page_type, parent_contents.page_type().ok() ); valid = false } for (parent_cell_idx, cell_buf_in_array) in cells_debug.iter().enumerate().take(contents.cell_count()) { let (parent_cell_start, parent_cell_len) = parent_contents .cell_get_raw_region(parent_cell_idx, usable_space) .unwrap(); let (cell_start, cell_len) = contents .cell_get_raw_region(parent_cell_idx, usable_space) .unwrap(); let buf = contents.as_ptr(); let cell_buf = to_static_buf(&mut buf[cell_start..cell_start + cell_len]); let parent_cell_buf = to_static_buf( &mut parent_buf[parent_cell_start..parent_cell_start + parent_cell_len], ); if cell_buf != cell_buf_in_array || cell_buf != parent_cell_buf { tracing::error!("balance_non_root(balance_shallower_cell_not_found_debug, page_id={}, cell_in_cell_array_idx={})", page.get().id, parent_cell_idx, ); valid = false; } } } else if page_idx == sibling_count_new - 1 { // We will only validate rightmost pointer of parent page, we will not validate rightmost if it's a cell and not the last pointer because, // insert cell could've defragmented the page and invalidated the pointer. // right pointer, we just check right pointer points to this page. if cell_divider_idx == parent_contents.cell_count() && right_page_id != page.get().id as u32 { tracing::error!("balance_non_root(cell_divider_right_pointer, should point to {}, but points to {})", page.get().id, right_page_id ); valid = false; } } else { // divider cell might be an overflow cell let mut was_overflow = false; for overflow_cell in &parent_contents.overflow_cells { if overflow_cell.index == cell_divider_idx { let left_pointer = read_u32(&overflow_cell.payload, 0); if left_pointer != page.get().id as u32 { tracing::error!("balance_non_root(cell_divider_left_pointer_overflow, should point to page_id={}, but points to {}, divider_cell={}, overflow_cells_parent={})", page.get().id, left_pointer, page_idx, parent_contents.overflow_cells.len() ); valid = false; } was_overflow = true; break; } } if was_overflow { if !is_table_leaf { // remember to increase cell if this cell was moved to parent current_index_cell += 1; } continue; } // check if overflow // check if right pointer, this is the last page. Do we update rightmost pointer and defragment moves it? let (cell_start, cell_len) = parent_contents .cell_get_raw_region(cell_divider_idx, usable_space) .unwrap(); let cell_left_pointer = read_u32(&parent_buf[cell_start..cell_start + cell_len], 0); if cell_left_pointer != page.get().id as u32 { tracing::error!("balance_non_root(cell_divider_left_pointer, should point to page_id={}, but points to {}, divider_cell={}, overflow_cells_parent={})", page.get().id, cell_left_pointer, page_idx, parent_contents.overflow_cells.len() ); valid = false; } if is_table_leaf { // If we are in a table leaf page, we just need to check that this cell that should be a divider cell is in the parent // This means we already check cell in leaf pages but not on parent so we don't advance current_index_cell let last_sibling_idx = balance_info.sibling_count - 1; if page_idx >= last_sibling_idx { // This means we are in the last page and we don't need to check anything continue; } let cell_buf: &'static mut [u8] = to_static_buf(&mut cells_debug[current_index_cell - 1]); let cell = crate::storage::sqlite3_ondisk::read_btree_cell( cell_buf, contents, 0, usable_space, ) .unwrap(); let parent_cell = parent_contents .cell_get(cell_divider_idx, usable_space) .unwrap(); let rowid = match cell { BTreeCell::TableLeafCell(table_leaf_cell) => table_leaf_cell.rowid, _ => unreachable!(), }; let rowid_parent = match parent_cell { BTreeCell::TableInteriorCell(table_interior_cell) => { table_interior_cell.rowid } _ => unreachable!(), }; if rowid_parent != rowid { tracing::error!("balance_non_root(cell_divider_rowid, page_id={}, cell_divider_idx={}, rowid_parent={}, rowid={})", page.get().id, cell_divider_idx, rowid_parent, rowid ); valid = false; } } else { // In any other case, we need to check that this cell was moved to parent as divider cell let mut was_overflow = false; for overflow_cell in &parent_contents.overflow_cells { if overflow_cell.index == cell_divider_idx { let left_pointer = read_u32(&overflow_cell.payload, 0); if left_pointer != page.get().id as u32 { tracing::error!("balance_non_root(cell_divider_divider_cell_overflow should point to page_id={}, but points to {}, divider_cell={}, overflow_cells_parent={})", page.get().id, left_pointer, page_idx, parent_contents.overflow_cells.len() ); valid = false; } was_overflow = true; break; } } if was_overflow { if !is_table_leaf { // remember to increase cell if this cell was moved to parent current_index_cell += 1; } continue; } let (parent_cell_start, parent_cell_len) = parent_contents .cell_get_raw_region(cell_divider_idx, usable_space) .unwrap(); let cell_buf_in_array = &cells_debug[current_index_cell]; let left_pointer = read_u32( &parent_buf[parent_cell_start..parent_cell_start + parent_cell_len], 0, ); if left_pointer != page.get().id as u32 { tracing::error!("balance_non_root(divider_cell_left_pointer_interior should point to page_id={}, but points to {}, divider_cell={}, overflow_cells_parent={})", page.get().id, left_pointer, page_idx, parent_contents.overflow_cells.len() ); valid = false; } match page_type { PageType::TableInterior | PageType::IndexInterior => { let parent_cell_buf = &parent_buf[parent_cell_start..parent_cell_start + parent_cell_len]; if parent_cell_buf[4..] != cell_buf_in_array[4..] { tracing::error!("balance_non_root(cell_divider_cell, page_id={}, cell_divider_idx={})", page.get().id, cell_divider_idx, ); valid = false; } } PageType::IndexLeaf => { let parent_cell_buf = &parent_buf[parent_cell_start..parent_cell_start + parent_cell_len]; if parent_cell_buf[4..] != cell_buf_in_array[..] { tracing::error!("balance_non_root(cell_divider_cell_index_leaf, page_id={}, cell_divider_idx={})", page.get().id, cell_divider_idx, ); valid = false; } } _ => { unreachable!() } } current_index_cell += 1; } } } // Verify all cells were accounted for (non-shallower case) if sibling_count_new > 0 && current_index_cell != cells_debug.len() { tracing::error!( "balance_non_root(cell_count_mismatch, current_index_cell={}, cells_debug_len={}, sibling_count_new={})", current_index_cell, cells_debug.len(), sibling_count_new ); valid = false; } turso_assert!( valid, "corrupted database, cells were not balanced properly" ); } /// Balance the root page. /// This is done when the root page overflows, and we need to create a new root page. /// See e.g. https://en.wikipedia.org/wiki/B-tree fn balance_root(&mut self) -> Result> { /* todo: balance deeper, create child and copy contents of root there. Then split root */ /* if we are in root page then we just need to create a new root and push key there */ // Since we are going to change the btree structure, let's forget our cached knowledge of the rightmost page. let _ = self.move_to_right_state.1.take(); let root = self.stack.top(); let root_contents = root.get_contents(); let child = return_if_io!(self.pager.do_allocate_page( root_contents.page_type()?, 0, BtreePageAllocMode::Any )); let is_page_1 = root.get().id == 1; let offset = if is_page_1 { DatabaseHeader::SIZE } else { 0 }; #[cfg(debug_assertions)] turso_assert_eq!(offset, root_contents.offset()); tracing::debug!( "balance_root(root={}, rightmost={}, page_type={:?})", root.get().id, child.get().id, root_contents.page_type().ok() ); turso_assert!(root.is_dirty(), "root must be marked dirty"); turso_assert!( child.is_dirty(), "child must be marked dirty as freshly allocated page" ); let root_buf = root_contents.as_ptr(); let child_contents = child.get_contents(); let child_buf = child_contents.as_ptr(); let (root_pointer_start, root_pointer_len) = root_contents.cell_pointer_array_offset_and_size(); let (child_pointer_start, _) = child.get_contents().cell_pointer_array_offset_and_size(); let top = root_contents.cell_content_area() as usize; // 1. Modify child // Copy pointers child_buf[child_pointer_start..child_pointer_start + root_pointer_len] .copy_from_slice(&root_buf[root_pointer_start..root_pointer_start + root_pointer_len]); // Copy cell contents child_buf[top..].copy_from_slice(&root_buf[top..]); // Copy header child_buf[0..root_contents.header_size()] .copy_from_slice(&root_buf[offset..offset + root_contents.header_size()]); // Copy overflow cells std::mem::swap( &mut child_contents.overflow_cells, &mut root_contents.overflow_cells, ); root_contents.overflow_cells.clear(); // 2. Modify root let new_root_page_type = match root_contents.page_type()? { PageType::IndexLeaf => PageType::IndexInterior, PageType::TableLeaf => PageType::TableInterior, other => other, } as u8; // set new page type root_contents.write_page_type(new_root_page_type); root_contents.write_rightmost_ptr(child.get().id as u32); root_contents.write_cell_content_area(self.usable_space()); root_contents.write_cell_count(0); root_contents.write_first_freeblock(0); root_contents.write_fragmented_bytes_count(0); root_contents.overflow_cells.clear(); self.root_page = root.get().id as i64; self.stack.clear(); self.stack.push(root); self.stack.set_cell_index(0); // leave parent pointing at the rightmost pointer (in this case 0, as there are no cells), since we will be balancing the rightmost child page. self.stack.push(child); Ok(IOResult::Done(())) } #[inline(always)] /// Returns the usable space of the current page (which is computed as: page_size - reserved_bytes). /// This is cached to avoid calling `pager.usable_space()` in a hot loop. fn usable_space(&self) -> usize { self.usable_space_cached } /// Clear the overflow pages linked to a specific page provided by the leaf cell /// Uses a state machine to keep track of it's operations so that traversal can be /// resumed from last point after IO interruption #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn clear_overflow_pages(&mut self, cell: &BTreeCell) -> Result> { loop { match self.overflow_state.clone() { OverflowState::Start => { let first_overflow_page = match cell { BTreeCell::TableLeafCell(leaf_cell) => leaf_cell.first_overflow_page, BTreeCell::IndexLeafCell(leaf_cell) => leaf_cell.first_overflow_page, BTreeCell::IndexInteriorCell(interior_cell) => { interior_cell.first_overflow_page } BTreeCell::TableInteriorCell(_) => return Ok(IOResult::Done(())), // No overflow pages }; if let Some(next_page) = first_overflow_page { if unlikely( next_page < 2 || next_page > self .pager .io .block(|| { self.pager.with_header(|header| header.database_size) })? .get(), ) { self.overflow_state = OverflowState::Start; return Err(LimboError::Corrupt("Invalid overflow page number".into())); } let (page, c) = self.read_page(next_page as i64)?; self.overflow_state = OverflowState::ProcessPage { next_page: page }; if let Some(c) = c { io_yield_one!(c); } } else { self.overflow_state = OverflowState::Done; } } OverflowState::ProcessPage { next_page: page } => { turso_assert!(page.is_loaded(), "page should be loaded"); let contents = page.get_contents(); let next = contents.read_u32_no_offset(0); let next_page_id = page.get().id; return_if_io!(self.pager.free_page(Some(page), next_page_id)); if next != 0 { if unlikely( next < 2 || next > self .pager .io .block(|| { self.pager.with_header(|header| header.database_size) })? .get(), ) { self.overflow_state = OverflowState::Start; return Err(LimboError::Corrupt("Invalid overflow page number".into())); } let (page, c) = self.read_page(next as i64)?; self.overflow_state = OverflowState::ProcessPage { next_page: page }; if let Some(c) = c { io_yield_one!(c); } } else { self.overflow_state = OverflowState::Done; } } OverflowState::Done => { self.overflow_state = OverflowState::Start; return Ok(IOResult::Done(())); } }; } } /// Deletes all contents of the B-tree by freeing all its pages in an iterative depth-first order. /// This ensures child pages are freed before their parents /// Uses a state machine to keep track of the operation to ensure IO doesn't cause repeated traversals /// /// Depending on the caller, the root page may either be freed as well or left allocated but emptied. /// /// # Example /// For a B-tree with this structure (where 4' is an overflow page): /// ```text /// 1 (root) /// / \ /// 2 3 /// / \ / \ /// 4' <- 4 5 6 7 /// ``` /// /// The destruction order would be: [4',4,5,2,6,7,3,1] fn destroy_btree_contents(&mut self, keep_root: bool) -> Result>> { if let CursorState::None = &self.state { let c = self.move_to_root()?; self.state = CursorState::Destroy(DestroyInfo { state: DestroyState::Start, }); if let Some(c) = c { io_yield_one!(c); } } loop { let destroy_state = { let destroy_info = self .state .destroy_info() .expect("unable to get a mut reference to destroy state in cursor"); destroy_info.state.clone() }; match destroy_state { DestroyState::Start => { let destroy_info = self .state .mut_destroy_info() .expect("unable to get a mut reference to destroy state in cursor"); destroy_info.state = DestroyState::LoadPage; } DestroyState::LoadPage => { let _page = self.stack.top_ref(); let destroy_info = self .state .mut_destroy_info() .expect("unable to get a mut reference to destroy state in cursor"); destroy_info.state = DestroyState::ProcessPage; } DestroyState::ProcessPage => { self.stack.advance(); let page = self.stack.top_ref(); let contents = page.get_contents(); let cell_idx = self.stack.current_cell_index(); // If we've processed all cells in this page, figure out what to do with this page if cell_idx >= contents.cell_count() as i32 { match (contents.is_leaf(), cell_idx) { // Leaf pages with all cells processed (true, n) if n >= contents.cell_count() as i32 => { let destroy_info = self.state.mut_destroy_info().expect( "unable to get a mut reference to destroy state in cursor", ); destroy_info.state = DestroyState::FreePage; continue; } // Non-leaf page which has processed all children but not it's potential right child (false, n) if n == contents.cell_count() as i32 => { if let Some(rightmost) = contents.rightmost_pointer()? { let (rightmost_page, c) = self.read_page(rightmost as i64)?; self.stack.push(rightmost_page); let destroy_info = self.state.mut_destroy_info().expect( "unable to get a mut reference to destroy state in cursor", ); destroy_info.state = DestroyState::LoadPage; if let Some(c) = c { io_yield_one!(c); } } else { let destroy_info = self.state.mut_destroy_info().expect( "unable to get a mut reference to destroy state in cursor", ); destroy_info.state = DestroyState::FreePage; } continue; } // Non-leaf page which has processed all children and it's right child (false, n) if n > contents.cell_count() as i32 => { let destroy_info = self.state.mut_destroy_info().expect( "unable to get a mut reference to destroy state in cursor", ); destroy_info.state = DestroyState::FreePage; continue; } _ => unreachable!("Invalid cell idx state"), } } // We have not yet processed all cells in this page // Get the current cell let cell = contents.cell_get(cell_idx as usize, self.usable_space())?; match contents.is_leaf() { // For a leaf cell, clear the overflow pages associated with this cell true => { let destroy_info = self .state .mut_destroy_info() .expect("unable to get a mut reference to destroy state in cursor"); destroy_info.state = DestroyState::ClearOverflowPages { cell }; continue; } // For interior cells, check the type of cell to determine what to do false => match &cell { // For index interior cells, remove the overflow pages BTreeCell::IndexInteriorCell(_) => { let destroy_info = self.state.mut_destroy_info().expect( "unable to get a mut reference to destroy state in cursor", ); destroy_info.state = DestroyState::ClearOverflowPages { cell }; continue; } // For all other interior cells, load the left child page _ => { let child_page_id = match &cell { BTreeCell::TableInteriorCell(cell) => cell.left_child_page, BTreeCell::IndexInteriorCell(cell) => cell.left_child_page, _ => panic!("expected interior cell"), }; let (child_page, c) = self.read_page(child_page_id as i64)?; self.stack.push(child_page); let destroy_info = self.state.mut_destroy_info().expect( "unable to get a mut reference to destroy state in cursor", ); destroy_info.state = DestroyState::LoadPage; if let Some(c) = c { io_yield_one!(c); } } }, } } DestroyState::ClearOverflowPages { cell } => { return_if_io!(self.clear_overflow_pages(&cell)); match cell { // For an index interior cell, clear the left child page now that overflow pages have been cleared BTreeCell::IndexInteriorCell(index_int_cell) => { let (child_page, c) = self.read_page(index_int_cell.left_child_page as i64)?; self.stack.push(child_page); let destroy_info = self .state .mut_destroy_info() .expect("unable to get a mut reference to destroy state in cursor"); destroy_info.state = DestroyState::LoadPage; if let Some(c) = c { io_yield_one!(c); } } // For any leaf cell, advance the index now that overflow pages have been cleared BTreeCell::TableLeafCell(_) | BTreeCell::IndexLeafCell(_) => { let destroy_info = self .state .mut_destroy_info() .expect("unable to get a mut reference to destroy state in cursor"); destroy_info.state = DestroyState::LoadPage; } _ => panic!("unexpected cell type"), } } DestroyState::FreePage => { let page = self.stack.top(); let page_id = page.get().id; if self.stack.has_parent() { return_if_io!(self.pager.free_page(Some(page), page_id)); self.stack.pop(); let destroy_info = self .state .mut_destroy_info() .expect("unable to get a mut reference to destroy state in cursor"); destroy_info.state = DestroyState::ProcessPage; } else { if keep_root { self.clear_root(&page)?; } else { return_if_io!(self.pager.free_page(Some(page), page_id)); } self.state = CursorState::None; // TODO: For now, no-op the result return None always. This will change once [AUTO_VACUUM](https://www.sqlite.org/lang_vacuum.html) is introduced // At that point, the last root page(call this x) will be moved into the position of the root page of this table and the value returned will be x return Ok(IOResult::Done(None)); } } } } } fn clear_root(&mut self, root_page: &PageRef) -> Result<()> { let contents = root_page.get_contents(); let page_type = match contents.page_type()? { PageType::TableLeaf | PageType::TableInterior => PageType::TableLeaf, PageType::IndexLeaf | PageType::IndexInterior => PageType::IndexLeaf, }; self.pager.add_dirty(root_page)?; btree_init_page(root_page, page_type, 0, self.pager.usable_space()); Ok(()) } pub fn overwrite_cell( &mut self, page: &PageRef, cell_idx: usize, record: &ImmutableRecord, state: &mut OverwriteCellState, ) -> Result> { loop { turso_assert!(page.is_loaded(), "page is not loaded", { "page_id": page.get().id }); match state { OverwriteCellState::AllocatePayload => { let serial_types_len = record.column_count(); // Reuse the cell payload buffer to avoid allocations let mut new_payload = std::mem::take(&mut self.reusable_cell_payload); new_payload.clear(); if new_payload.capacity() < serial_types_len { new_payload.reserve(serial_types_len - new_payload.capacity()); } let rowid = return_if_io!(self.rowid()); *state = OverwriteCellState::FillPayload { new_payload, rowid, fill_cell_payload_state: FillCellPayloadState::Start, }; continue; } OverwriteCellState::FillPayload { new_payload, rowid, fill_cell_payload_state, } => { { return_if_io!(fill_cell_payload( &PinGuard::new(page.clone()), *rowid, new_payload, cell_idx, record, self.usable_space(), self.pager.clone(), fill_cell_payload_state, )); } // figure out old cell offset & size let (old_offset, old_local_size) = { let contents = page.get_contents(); contents.cell_get_raw_region(cell_idx, self.usable_space())? }; *state = OverwriteCellState::ClearOverflowPagesAndOverwrite { new_payload: std::mem::take(new_payload), old_offset, old_local_size, }; continue; } OverwriteCellState::ClearOverflowPagesAndOverwrite { new_payload, old_offset, old_local_size, } => { let contents = page.get_contents(); let cell = contents.cell_get(cell_idx, self.usable_space())?; return_if_io!(self.clear_overflow_pages(&cell)); // if it all fits in local space and old_local_size is enough, do an in-place overwrite if new_payload.len() == *old_local_size { Self::overwrite_content(page, *old_offset, new_payload)?; // Recover the reusable buffer self.reusable_cell_payload = std::mem::take(new_payload); return Ok(IOResult::Done(())); } drop_cell(contents, cell_idx, self.usable_space())?; insert_into_cell(contents, new_payload, cell_idx, self.usable_space())?; // Recover the reusable buffer self.reusable_cell_payload = std::mem::take(new_payload); return Ok(IOResult::Done(())); } } } } pub fn overwrite_content(page: &PageRef, dest_offset: usize, new_payload: &[u8]) -> Result<()> { turso_assert!(page.is_loaded(), "page should be loaded"); let buf = page.get_contents().as_ptr(); buf[dest_offset..dest_offset + new_payload.len()].copy_from_slice(new_payload); Ok(()) } fn get_immutable_record_or_create(&mut self) -> Option<&mut ImmutableRecord> { let reusable_immutable_record = &mut self.reusable_immutable_record; if reusable_immutable_record.is_none() { let page_size = self.pager.get_page_size_unchecked().get(); let record = ImmutableRecord::new(page_size as usize); reusable_immutable_record.replace(record); } reusable_immutable_record.as_mut() } fn get_immutable_record(&self) -> Option<&ImmutableRecord> { self.reusable_immutable_record.as_ref() } pub fn is_write_in_progress(&self) -> bool { matches!(self.state, CursorState::Write(_)) } // Save cursor context, to be restored later pub fn save_context(&mut self, cursor_context: CursorContext) { self.valid_state = CursorValidState::RequireSeek; self.context = Some(cursor_context); } /// If context is defined, restore it and set it None on success #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn restore_context(&mut self) -> Result> { if self.context.is_none() || matches!(self.valid_state, CursorValidState::Valid) { return Ok(IOResult::Done(())); } if let CursorValidState::RequireAdvance(direction) = self.valid_state { return_if_io!(match direction { // Avoid calling next()/prev() directly because they immediately call restore_context() IterationDirection::Forwards => self.get_next_record(), IterationDirection::Backwards => self.get_prev_record(), }); self.context = None; self.valid_state = CursorValidState::Valid; return Ok(IOResult::Done(())); } let ctx = self.context.take().unwrap(); let seek_key = match ctx.key { CursorContextKey::TableRowId(rowid) => SeekKey::TableRowId(rowid), CursorContextKey::IndexKeyRowId(ref record) => SeekKey::IndexKey(record), }; let res = self.seek(seek_key, ctx.seek_op)?; match res { IOResult::Done(res) => { if let SeekResult::TryAdvance = res { self.valid_state = CursorValidState::RequireAdvance(ctx.seek_op.iteration_direction()); self.context = Some(ctx); io_yield_one!(Completion::new_yield()); } self.valid_state = CursorValidState::Valid; Ok(IOResult::Done(())) } IOResult::IO(io) => { self.context = Some(ctx); Ok(IOResult::IO(io)) } } } pub fn read_page(&self, page_idx: i64) -> Result<(PageRef, Option)> { btree_read_page(&self.pager, page_idx) } pub fn allocate_page(&self, page_type: PageType, offset: usize) -> Result> { self.pager .do_allocate_page(page_type, offset, BtreePageAllocMode::Any) } } impl CursorTrait for BTreeCursor { #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn next(&mut self) -> Result> { if self.valid_state == CursorValidState::Invalid { return Ok(IOResult::Done(())); } if self.skip_advance { // See DeleteState::RestoreContextAfterBalancing self.skip_advance = false; let mem_page = self.stack.top_ref(); let contents = mem_page.get_contents(); let cell_idx = self.stack.current_cell_index(); let cell_count = contents.cell_count(); let has_record = cell_idx >= 0 && cell_idx < cell_count as i32; if has_record { self.set_has_record(has_record); // If we are positioned at a record, we stop here without advancing. self.read_overflow_state = None; return Ok(IOResult::Done(())); } // But: if we aren't currently positioned at a record (for example, we are at the end of a page), // we need to advance despite the skip_advance flag // because the intent is to find the next record immediately after the one we just deleted. } loop { match self.advance_state { AdvanceState::Start => { return_if_io!(self.restore_context()); self.advance_state = AdvanceState::Advance; } AdvanceState::Advance => { return_if_io!(self.get_next_record()); self.advance_state = AdvanceState::Start; self.read_overflow_state = None; return Ok(IOResult::Done(())); } } } } #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn last(&mut self) -> Result> { self.set_null_flag(false); let always_seek = false; let cursor_has_record = return_if_io!(self.move_to_rightmost(always_seek)); self.set_has_record(cursor_has_record); self.invalidate_record(); self.read_overflow_state = None; Ok(IOResult::Done(())) } #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn prev(&mut self) -> Result> { loop { match self.advance_state { AdvanceState::Start => { return_if_io!(self.restore_context()); self.advance_state = AdvanceState::Advance; } AdvanceState::Advance => { return_if_io!(self.get_prev_record()); self.advance_state = AdvanceState::Start; self.read_overflow_state = None; return Ok(IOResult::Done(())); } } } } #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG))] fn rowid(&mut self) -> Result>> { if self.get_null_flag() { return Ok(IOResult::Done(None)); } if self.has_record() { let page = self.stack.top_ref(); let contents = page.get_contents(); let page_type = contents.page_type()?; if page_type.is_table() { let cell_idx = self.stack.current_cell_index(); let rowid = contents.cell_table_leaf_read_rowid(cell_idx as usize)?; Ok(IOResult::Done(Some(rowid))) } else { let _ = return_if_io!(self.record()); Ok(IOResult::Done(self.get_index_rowid_from_record())) } } else { Ok(IOResult::Done(None)) } } #[cfg_attr(debug_assertions, instrument(skip(self, key), level = Level::DEBUG))] fn seek(&mut self, key: SeekKey<'_>, op: SeekOp) -> Result> { self.skip_advance = false; // Empty trace to capture the span information tracing::trace!(""); // We need to clear the null flag for the table cursor before seeking, // because it might have been set to false by an unmatched left-join row during the previous iteration // on the outer loop. self.set_null_flag(false); let seek_result = return_if_io!(self.do_seek(key, op)); self.invalidate_record(); // Reset seek state self.seek_state = CursorSeekState::Start; self.valid_state = CursorValidState::Valid; self.read_overflow_state = None; Ok(IOResult::Done(seek_result)) } #[cfg_attr(debug_assertions, instrument(skip(self, registers), level = Level::DEBUG))] fn seek_unpacked( &mut self, registers: &[Register], op: SeekOp, ) -> Result> { self.skip_advance = false; // Empty trace to capture the span information tracing::trace!(""); // We need to clear the null flag for the table cursor before seeking, // because it might have been set to false by an unmatched left-join row during the previous iteration // on the outer loop. self.set_null_flag(false); let seek_result = return_if_io!(self.do_seek_unpacked(registers, op)); self.invalidate_record(); // Reset seek state self.seek_state = CursorSeekState::Start; self.valid_state = CursorValidState::Valid; Ok(IOResult::Done(seek_result)) } #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG))] fn record(&mut self) -> Result>> { if !self.has_record() { return Ok(IOResult::Done(None)); } let invalidated = self .reusable_immutable_record .as_ref() .is_none_or(|record| record.is_invalidated()); if !invalidated { return Ok(IOResult::Done(self.reusable_immutable_record.as_ref())); } let page = self.stack.top_ref(); let contents = page.get_contents(); let cell_idx = self.stack.current_cell_index(); let cell = contents.cell_get(cell_idx as usize, self.usable_space())?; let (payload, payload_size, first_overflow_page) = match cell { BTreeCell::TableLeafCell(TableLeafCell { payload, payload_size, first_overflow_page, .. }) => (payload, payload_size, first_overflow_page), BTreeCell::IndexInteriorCell(IndexInteriorCell { payload, payload_size, first_overflow_page, .. }) => (payload, payload_size, first_overflow_page), BTreeCell::IndexLeafCell(IndexLeafCell { payload, first_overflow_page, payload_size, }) => (payload, payload_size, first_overflow_page), _ => unreachable!("unexpected page_type"), }; if let Some(next_page) = first_overflow_page { return_if_io!(self.process_overflow_read(payload, next_page, payload_size)) } else { self.get_immutable_record_or_create() .as_mut() .unwrap() .invalidate(); self.get_immutable_record_or_create() .as_mut() .unwrap() .start_serialization(payload); }; Ok(IOResult::Done(self.reusable_immutable_record.as_ref())) } #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn insert(&mut self, key: &BTreeKey) -> Result> { tracing::debug!(valid_state = ?self.valid_state, cursor_state = ?self.state, is_write_in_progress = self.is_write_in_progress()); return_if_io!(self.insert_into_page(key)); self.invalidate_count_cache(); if key.maybe_rowid().is_some() { self.set_has_record(true); } Ok(IOResult::Done(())) } #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG))] /// Delete state machine flow: /// 1. Start -> check if the rowid to be delete is present in the page or not. If not we early return /// 2. DeterminePostBalancingSeekKey -> determine the key to seek to after balancing. /// 3. LoadPage -> load the page. /// 4. FindCell -> find the cell to be deleted in the page. /// 5. ClearOverflowPages -> Clear the overflow pages if there are any before dropping the cell, then if we are in a leaf page we just drop the cell in place. /// if we are in interior page, we need to rotate keys in order to replace current cell (InteriorNodeReplacement). /// 6. InteriorNodeReplacement -> we copy the left subtree leaf node into the deleted interior node's place. /// 7. Balancing -> perform balancing /// 8. PostInteriorNodeReplacement -> if an interior node was replaced, we need to advance the cursor once. /// 9. SeekAfterBalancing -> adjust the cursor to a node that is closer to the deleted value. go to Finish /// 10. Finish -> Delete operation is done. Return CursorResult(Ok()) fn delete(&mut self) -> Result> { if let CursorState::None = &self.state { self.invalidate_count_cache(); self.state = CursorState::Delete(DeleteState::Start); } loop { let usable_space = self.usable_space(); let delete_state = match &mut self.state { CursorState::Delete(x) => x, _ => unreachable!("expected delete state"), }; tracing::debug!(?delete_state); match delete_state { DeleteState::Start => { let page = self.stack.top_ref(); self.pager.add_dirty(page)?; if matches!( page.get_contents().page_type()?, PageType::TableLeaf | PageType::TableInterior ) { if return_if_io!(self.rowid()).is_none() { self.state = CursorState::None; return Ok(IOResult::Done(())); } } else if !self.has_record() { self.state = CursorState::None; return Ok(IOResult::Done(())); } self.state = CursorState::Delete(DeleteState::DeterminePostBalancingSeekKey); } DeleteState::DeterminePostBalancingSeekKey => { // FIXME: skip this work if we determine deletion wont result in balancing // Right now we calculate the key every time for simplicity/debugging // since it won't affect correctness which is more important let page = self.stack.top_ref(); let target_key = if page.is_index()? { let record = match return_if_io!(self.record()) { Some(record) => record.clone(), None => unreachable!("there should've been a record"), }; CursorContext { key: CursorContextKey::IndexKeyRowId(record), seek_op: SeekOp::GE { eq_only: true }, } } else { let Some(rowid) = return_if_io!(self.rowid()) else { panic!("cursor should be pointing to a record with a rowid"); }; CursorContext { key: CursorContextKey::TableRowId(rowid), seek_op: SeekOp::GE { eq_only: true }, } }; self.state = CursorState::Delete(DeleteState::LoadPage { post_balancing_seek_key: Some(target_key), }); } DeleteState::LoadPage { post_balancing_seek_key, } => { self.state = CursorState::Delete(DeleteState::FindCell { post_balancing_seek_key: post_balancing_seek_key.take(), }); } DeleteState::FindCell { post_balancing_seek_key, } => { let page = self.stack.top_ref(); let cell_idx = self.stack.current_cell_index() as usize; let contents = page.get_contents(); if unlikely(cell_idx >= contents.cell_count()) { return_corrupt!( "Corrupted page: cell index {} is out of bounds for page with {} cells", cell_idx, contents.cell_count() ); } tracing::debug!( "DeleteState::FindCell: page_id: {}, cell_idx: {}", page.get().id, cell_idx ); let cell = contents.cell_get(cell_idx, usable_space)?; let original_child_pointer = match &cell { BTreeCell::TableInteriorCell(interior) => Some(interior.left_child_page), BTreeCell::IndexInteriorCell(interior) => Some(interior.left_child_page), _ => None, }; self.state = CursorState::Delete(DeleteState::ClearOverflowPages { cell_idx, cell, original_child_pointer, post_balancing_seek_key: post_balancing_seek_key.take(), }); } DeleteState::ClearOverflowPages { cell, .. } => { let cell = cell.clone(); return_if_io!(self.clear_overflow_pages(&cell)); let CursorState::Delete(DeleteState::ClearOverflowPages { cell_idx, original_child_pointer, ref mut post_balancing_seek_key, .. }) = self.state else { unreachable!("expected clear overflow pages state"); }; let page = self.stack.top_ref(); let contents = page.get_contents(); if !contents.is_leaf() { self.state = CursorState::Delete(DeleteState::InteriorNodeReplacement { page: page.clone(), btree_depth: self.stack.current(), cell_idx, original_child_pointer, post_balancing_seek_key: post_balancing_seek_key.take(), }); } else { drop_cell(contents, cell_idx, usable_space)?; self.state = CursorState::Delete(DeleteState::CheckNeedsBalancing { btree_depth: self.stack.current(), post_balancing_seek_key: post_balancing_seek_key.take(), interior_node_was_replaced: false, }); } } DeleteState::InteriorNodeReplacement { .. } => { // This is an interior node, we need to handle deletion differently. // 1. Move cursor to the largest key in the left subtree. // 2. Replace the cell in the interior (parent) node with that key. // 3. Delete that key from the child page. // Step 1: Move cursor to the largest key in the left subtree. // The largest key is always in a leaf, and so this traversal may involvegoing multiple pages downwards, // so we store the page we are currently on. // avoid calling prev() because it internally calls restore_context() which may cause unintended behavior. return_if_io!(self.get_prev_record()); let CursorState::Delete(DeleteState::InteriorNodeReplacement { ref page, btree_depth, cell_idx, original_child_pointer, ref mut post_balancing_seek_key, .. }) = self.state else { unreachable!("expected interior node replacement state"); }; // Ensure we keep the parent page at the same position as before the replacement. self.stack .node_states .get_mut(btree_depth) .expect("parent page should be on the stack") .cell_idx = cell_idx as i32; let (cell_payload, leaf_cell_idx) = { let leaf_page = self.stack.top_ref(); let leaf_contents = leaf_page.get_contents(); turso_assert!(leaf_contents.is_leaf()); turso_assert_greater_than!(leaf_contents.cell_count(), 0); let leaf_cell_idx = leaf_contents.cell_count() - 1; let last_cell_on_child_page = leaf_contents.cell_get(leaf_cell_idx, usable_space)?; let mut cell_payload: Vec = Vec::new(); let child_pointer = original_child_pointer.expect("there should be a pointer"); // Rewrite the old leaf cell as an interior cell depending on type. match last_cell_on_child_page { BTreeCell::TableLeafCell(leaf_cell) => { // Table interior cells contain the left child pointer and the rowid as varint. cell_payload.extend_from_slice(&child_pointer.to_be_bytes()); write_varint_to_vec(leaf_cell.rowid as u64, &mut cell_payload); } BTreeCell::IndexLeafCell(leaf_cell) => { // Index interior cells contain: // 1. The left child pointer // 2. The payload size as varint // 3. The payload // 4. The first overflow page as varint, omitted if no overflow. cell_payload.extend_from_slice(&child_pointer.to_be_bytes()); write_varint_to_vec(leaf_cell.payload_size, &mut cell_payload); cell_payload.extend_from_slice(leaf_cell.payload); if let Some(first_overflow_page) = leaf_cell.first_overflow_page { cell_payload .extend_from_slice(&first_overflow_page.to_be_bytes()); } } _ => unreachable!("Expected table leaf cell"), } (cell_payload, leaf_cell_idx) }; let leaf_page = self.stack.top_ref(); self.pager.add_dirty(page)?; self.pager.add_dirty(leaf_page)?; // Step 2: Replace the cell in the parent (interior) page. { let parent_contents = page.get_contents(); let parent_page_id = page.get().id; let left_child_page = u32::from_be_bytes( cell_payload[..4].try_into().expect("invalid cell payload"), ); turso_assert!( left_child_page as usize != parent_page_id, "corrupt: current page and left child page are the same", { "left_child_page": left_child_page, "parent_page_id": parent_page_id } ); // First, drop the old cell that is being replaced. drop_cell(parent_contents, cell_idx, usable_space)?; // Then, insert the new cell (the predecessor) in its place. insert_into_cell(parent_contents, &cell_payload, cell_idx, usable_space)?; } // Step 3: Delete the predecessor cell from the leaf page. { let leaf_contents = leaf_page.get_contents(); drop_cell(leaf_contents, leaf_cell_idx, usable_space)?; } self.state = CursorState::Delete(DeleteState::CheckNeedsBalancing { btree_depth, post_balancing_seek_key: post_balancing_seek_key.take(), interior_node_was_replaced: true, }); } DeleteState::CheckNeedsBalancing { btree_depth, .. } => { let page = self.stack.top_ref(); // Check if either the leaf page we took the replacement cell from underflows, or if the interior page we inserted it into overflows OR underflows. // If the latter is true, we must always balance that level regardless of whether the leaf page (or any ancestor pages in between) need balancing. let leaf_underflows = { let leaf_contents = page.get_contents(); let free_space = compute_free_space(leaf_contents, usable_space)?; free_space * 3 > usable_space * 2 }; let interior_overflows_or_underflows = { // Invariant: ancestor pages on the stack are pinned to the page cache, // so we don't need return_if_locked_maybe_load! any ancestor, // and we already loaded the current page above. let interior_page = self .stack .get_page_at_level(*btree_depth) .expect("ancestor page should be on the stack"); let interior_contents = interior_page.get_contents(); let overflows = !interior_contents.overflow_cells.is_empty(); if overflows { true } else { let free_space = compute_free_space(interior_contents, usable_space)?; free_space * 3 > usable_space * 2 } }; let needs_balancing = leaf_underflows || interior_overflows_or_underflows; let CursorState::Delete(DeleteState::CheckNeedsBalancing { btree_depth, ref mut post_balancing_seek_key, interior_node_was_replaced, .. }) = self.state else { unreachable!("expected check needs balancing state"); }; if needs_balancing { let balance_only_ancestor = !leaf_underflows && interior_overflows_or_underflows; if balance_only_ancestor { // Only need to balance the ancestor page; move there immediately. while self.stack.current() > btree_depth { self.stack.pop(); } } let balance_both = leaf_underflows && interior_overflows_or_underflows; turso_assert!(matches!(self.balance_state.sub_state, BalanceSubState::Start), "no balancing operation should be in progress during delete", { "sub_state": self.balance_state.sub_state }); let post_balancing_seek_key = post_balancing_seek_key .take() .expect("post_balancing_seek_key should be Some"); self.save_context(post_balancing_seek_key); self.state = CursorState::Delete(DeleteState::Balancing { balance_ancestor_at_depth: if balance_both { Some(btree_depth) } else { None }, }); } else { // No balancing needed. if interior_node_was_replaced { // If we did replace an interior node, we need to advance the cursor once to // get back at the interior node that now has the replaced content. // The reason it is important to land here is that the replaced cell was smaller (LT) than the deleted cell, // so we must ensure we skip over it. I.e., when BTreeCursor::next() is called, it will move past the cell // that holds the replaced content. self.state = CursorState::Delete(DeleteState::PostInteriorNodeReplacement); } else { // If we didn't replace an interior node, we are done, // except we need to retreat, so that the next call to BTreeCursor::next() lands at the next record (because we deleted the current one) self.stack.retreat(); self.state = CursorState::None; return Ok(IOResult::Done(())); } } } DeleteState::PostInteriorNodeReplacement => { return_if_io!(self.get_next_record()); self.state = CursorState::None; return Ok(IOResult::Done(())); } DeleteState::Balancing { balance_ancestor_at_depth, } => { let balance_ancestor_at_depth = *balance_ancestor_at_depth; return_if_io!(self.balance(balance_ancestor_at_depth)); self.state = CursorState::Delete(DeleteState::RestoreContextAfterBalancing); } DeleteState::RestoreContextAfterBalancing => { return_if_io!(self.restore_context()); // We deleted key K, and performed a seek to: GE { eq_only: true } K. // This means that the cursor is now pointing to the next key after K. // We need to make the next call to BTreeCursor::next() a no-op so that we don't skip over // a row when deleting rows in a loop. self.skip_advance = true; self.state = CursorState::None; return Ok(IOResult::Done(())); } } } } #[inline(always)] /// In outer joins, whenever the right-side table has no matching row, the query must still return a row /// for each left-side row. In order to achieve this, we set the null flag on the right-side table cursor /// so that it returns NULL for all columns until cleared. fn set_null_flag(&mut self, flag: bool) { self.null_flag = flag; } #[inline(always)] fn get_null_flag(&self) -> bool { self.null_flag } #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn exists(&mut self, key: &Value) -> Result> { let int_key = match key { Value::Numeric(Numeric::Integer(i)) => i, _ => unreachable!("btree tables are indexed by integers!"), }; let seek_result = return_if_io!(self.seek(SeekKey::TableRowId(*int_key), SeekOp::GE { eq_only: true })); let exists = matches!(seek_result, SeekResult::Found); self.invalidate_record(); Ok(IOResult::Done(exists)) } /// Deletes all content from the B-Tree but preserves the root page. /// /// Unlike [`btree_destroy`], which frees all pages including the root, /// this method only clears the tree’s contents. The root page remains /// allocated and is reset to an empty leaf page. fn clear_btree(&mut self) -> Result>> { self.invalidate_count_cache(); self.destroy_btree_contents(true) } /// Destroys the entire B-Tree, including the root page. /// /// All pages belonging to the tree are freed, leaving no trace of the B-Tree. /// Use this when the structure itself is no longer needed. /// /// For cases where the B-Tree should remain allocated but emptied, see [`btree_clear`]. #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG))] fn btree_destroy(&mut self) -> Result>> { self.destroy_btree_contents(false) } #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG))] /// Count the number of entries in the b-tree /// /// Only supposed to be used in the context of a simple Count Select Statement fn count(&mut self) -> Result> { let mut mem_page; let mut contents; 'outer: loop { let state = self.count_state; match state { CountState::Start => { let c = self.move_to_root()?; self.count_state = CountState::Loop; if let Some(c) = c { io_yield_one!(c); } } CountState::Loop => { self.stack.advance(); mem_page = self.stack.top_ref(); contents = mem_page.get_contents(); /* If this is a leaf page or the tree is not an int-key tree, then ** this page contains countable entries. Increment the entry counter ** accordingly. */ if !matches!(contents.page_type()?, PageType::TableInterior) { self.count += contents.cell_count(); } let cell_idx = self.stack.current_cell_index() as usize; // Second condition is necessary in case we return if the page is locked in the loop below if contents.is_leaf() || cell_idx > contents.cell_count() { loop { if !self.stack.has_parent() { // All pages of the b-tree have been visited. Return successfully let c = self.move_to_root()?; self.count_state = CountState::Finish; if let Some(c) = c { io_yield_one!(c); } continue 'outer; } // Move to parent self.stack.pop(); mem_page = self.stack.top_ref(); turso_assert!(mem_page.is_loaded(), "page should be loaded"); contents = mem_page.get_contents(); let cell_idx = self.stack.current_cell_index() as usize; if cell_idx <= contents.cell_count() { break; } } } let cell_idx = self.stack.current_cell_index() as usize; turso_assert_less_than_or_equal!(cell_idx, contents.cell_count()); turso_assert!(!contents.is_leaf()); if cell_idx == contents.cell_count() { // Move to right child // should be safe as contents is not a leaf page let right_most_pointer = contents.rightmost_pointer()?.unwrap(); self.stack.advance(); let (child, c) = self.read_page(right_most_pointer as i64)?; self.stack.push(child); if let Some(c) = c { io_yield_one!(c); } } else { // Move to child left page let cell = contents.cell_get(cell_idx, self.usable_space())?; match cell { BTreeCell::TableInteriorCell(TableInteriorCell { left_child_page, .. }) | BTreeCell::IndexInteriorCell(IndexInteriorCell { left_child_page, .. }) => { self.stack.advance(); let (child, c) = self.read_page(left_child_page as i64)?; self.stack.push(child); if let Some(c) = c { io_yield_one!(c); } } _ => unreachable!(), } } } CountState::Finish => { return Ok(IOResult::Done(self.count)); } } } } #[inline] fn is_empty(&self) -> bool { !self.has_record } #[inline] fn root_page(&self) -> i64 { self.root_page } #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn rewind(&mut self) -> Result> { self.set_null_flag(false); if self.valid_state == CursorValidState::Invalid { return Ok(IOResult::Done(())); } self.skip_advance = false; loop { match self.rewind_state { RewindState::Start => { self.rewind_state = RewindState::NextRecord; let c = self.move_to_root()?; if let Some(c) = c { io_yield_one!(c); } } RewindState::NextRecord => { return_if_io!(self.get_next_record()); self.rewind_state = RewindState::Start; self.read_overflow_state = None; return Ok(IOResult::Done(())); } } } } #[inline] fn has_rowid(&self) -> bool { match &self.index_info { Some(index_key_info) => index_key_info.has_rowid, None => true, // currently we don't support WITHOUT ROWID tables } } #[inline] fn invalidate_record(&mut self) { self.get_immutable_record_or_create() .as_mut() .unwrap() .invalidate(); } #[inline] fn get_pager(&self) -> Arc { self.pager.clone() } #[inline] fn get_skip_advance(&self) -> bool { self.skip_advance } fn invalidate_btree_cache(&mut self) { self.move_to_right_state.1 = None; self.invalidate_count_cache(); } #[inline] fn has_record(&self) -> bool { self.has_record } #[inline] fn set_has_record(&mut self, has_record: bool) { self.has_record = has_record } #[inline] fn get_index_info(&self) -> &Arc { self.index_info.as_ref().unwrap() } fn seek_end(&mut self) -> Result> { loop { match self.seek_end_state { SeekEndState::Start => { let c = self.move_to_root()?; self.seek_end_state = SeekEndState::ProcessPage; if let Some(c) = c { io_yield_one!(c); } } SeekEndState::ProcessPage => { let mem_page = self.stack.top_ref(); let contents = mem_page.get_contents(); if contents.is_leaf() { // set cursor just past the last cell to append self.stack.set_cell_index(contents.cell_count() as i32); self.seek_end_state = SeekEndState::Start; return Ok(IOResult::Done(())); } match contents.rightmost_pointer()? { Some(right_most_pointer) => { self.stack.set_cell_index(contents.cell_count() as i32 + 1); // invalid on interior let (child, c) = self.read_page(right_most_pointer as i64)?; self.stack.push(child); if let Some(c) = c { io_yield_one!(c); } } None => unreachable!("interior page must have rightmost pointer"), } } } } } #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG))] fn seek_to_last(&mut self, always_seek: bool) -> Result> { loop { match self.seek_to_last_state { SeekToLastState::Start => { let has_record = return_if_io!(self.move_to_rightmost(always_seek)); self.invalidate_record(); self.set_has_record(has_record); if !has_record { self.seek_to_last_state = SeekToLastState::IsEmpty; continue; } return Ok(IOResult::Done(())); } SeekToLastState::IsEmpty => { let is_empty = return_if_io!(self.is_empty_table()); turso_assert!(is_empty); self.seek_to_last_state = SeekToLastState::Start; return Ok(IOResult::Done(())); } } } } } #[derive(Debug, thiserror::Error)] pub enum IntegrityCheckError { #[error("Cell {cell_idx} in page {page_id} is out of range. cell_range={cell_start}..{cell_end}, content_area={content_area}, usable_space={usable_space}")] CellOutOfRange { cell_idx: usize, page_id: i64, cell_start: usize, cell_end: usize, content_area: usize, usable_space: usize, }, #[error("Cell {cell_idx} in page {page_id} extends out of page. cell_range={cell_start}..{cell_end}, content_area={content_area}, usable_space={usable_space}")] CellOverflowsPage { cell_idx: usize, page_id: i64, cell_start: usize, cell_end: usize, content_area: usize, usable_space: usize, }, #[error("Page {page_id} ({page_category:?}) cell {cell_idx} has rowid={rowid} in wrong order. Parent cell has parent_rowid={max_intkey} and next_rowid={next_rowid}")] CellRowidOutOfRange { page_id: i64, page_category: PageCategory, cell_idx: usize, rowid: i64, max_intkey: i64, next_rowid: i64, }, #[error("Page {page_id} is at different depth from another leaf page this_page_depth={this_page_depth}, other_page_depth={other_page_depth} ")] LeafDepthMismatch { page_id: i64, this_page_depth: usize, other_page_depth: usize, }, #[error("Page {page_id} detected freeblock that extends page start={start} end={end}")] FreeBlockOutOfRange { page_id: i64, start: usize, end: usize, }, #[error("Page {page_id} cell overlap detected at position={start} with previous_end={prev_end}. content_area={content_area}, is_free_block={is_free_block}")] CellOverlap { page_id: i64, start: usize, prev_end: usize, content_area: usize, is_free_block: bool, }, #[error("Page {page_id} unexpected fragmentation got={got}, expected={expected}")] UnexpectedFragmentation { page_id: i64, got: usize, expected: usize, }, #[error("Page {page_id} referenced multiple times (references={references:?}, page_category={page_category:?})")] PageReferencedMultipleTimes { page_id: i64, references: Vec, page_category: PageCategory, }, #[error("Freelist: size is {actual_count} but should be {expected_count}")] FreelistCountMismatch { actual_count: usize, expected_count: usize, }, #[error("Page {page_id}: never used")] PageNeverUsed { page_id: i64 }, #[error("Pending byte page {page_id} is being used")] PendingBytePageUsed { page_id: i64 }, #[error("Freelist: freelist leaf count too big on page {page_id}")] FreelistTrunkCorrupt { page_id: i64, page_pointers: u32, max_pointers: usize, }, #[error("Freelist: invalid page number {pointer}")] FreelistPointerOutOfRange { page_id: i64, pointer: i64 }, #[error("overflow list length is {got} but should be {expected}")] OverflowListLengthMismatch { got: usize, expected: usize }, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum PageCategory { Normal, Overflow, FreeListTrunk, FreePage, } #[derive(Clone)] pub struct CheckFreelist { pub expected_count: usize, pub actual_count: usize, } #[derive(Clone)] struct IntegrityCheckPageEntry { page_idx: i64, level: usize, max_intkey: i64, page_category: PageCategory, overflow_pages_expected: Option, overflow_pages_seen: usize, } pub struct IntegrityCheckState { page_stack: Vec, pub db_size: usize, first_leaf_level: Option, pub page_reference: HashMap, page: Option, pub freelist_count: CheckFreelist, } impl IntegrityCheckState { pub fn new(db_size: usize) -> Self { Self { page_stack: Vec::new(), db_size, page_reference: HashMap::default(), first_leaf_level: None, page: None, freelist_count: CheckFreelist { expected_count: 0, actual_count: 0, }, } } pub fn set_expected_freelist_count(&mut self, count: usize) { self.freelist_count.expected_count = count; } pub fn start( &mut self, page_idx: i64, page_category: PageCategory, errors: &mut Vec, ) { turso_assert!( self.page_stack.is_empty(), "stack should be empty before integrity check for new root" ); self.first_leaf_level = None; let _ = self.page.take(); // root can't be referenced from anywhere - so we insert "zero entry" for it self.push_page( IntegrityCheckPageEntry { page_idx, level: 0, max_intkey: i64::MAX, page_category, overflow_pages_expected: None, overflow_pages_seen: 0, }, 0, errors, ); } fn push_page( &mut self, entry: IntegrityCheckPageEntry, referenced_by: i64, errors: &mut Vec, ) { let page_id = entry.page_idx; let Some(previous) = self.page_reference.insert(page_id, referenced_by) else { self.page_stack.push(entry); return; }; errors.push(IntegrityCheckError::PageReferencedMultipleTimes { page_id, page_category: entry.page_category, references: vec![previous, referenced_by], }); } } impl std::fmt::Debug for IntegrityCheckState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("IntegrityCheckState") .field("first_leaf_level", &self.first_leaf_level) .finish() } } fn overflow_pages_expected_for_cell( payload_size: u64, local_payload_size: usize, usable_space: usize, ) -> usize { let payload_size = usize::try_from(payload_size).unwrap_or(usize::MAX); let remaining_payload = payload_size.saturating_sub(local_payload_size); if remaining_payload == 0 { return 0; } let overflow_page_payload = usable_space.saturating_sub(4).max(1); remaining_payload.div_ceil(overflow_page_payload) } /// Perform integrity check on a whole table/index. We check for: /// 1. Correct order of keys in case of rowids. /// 2. There are no overlap between cells. /// 3. Cells do not scape outside expected range. /// 4. Depth of leaf pages are equal. /// 5. Overflow pages are correct (TODO) /// /// In order to keep this reentrant, we keep a stack of pages we need to check. Ideally, like in /// SQLlite, we would have implemented a recursive solution which would make it easier to check the /// depth. pub fn integrity_check( state: &mut IntegrityCheckState, errors: &mut Vec, pager: &Arc, mv_store: Option<&Arc>, ) -> Result> { if let Some(mv_store) = mv_store { let Some(IntegrityCheckPageEntry { page_idx: root_page, .. }) = state.page_stack.last().cloned() else { panic!("Page stack is empty on integrity_check start"); }; if root_page < 0 { let table_id = mv_store.get_table_id_from_root_page(root_page); turso_assert!( !mv_store.is_btree_allocated(&table_id), "we got a negative page index that is reported as allocated" ); state.page_stack.pop(); return Ok(IOResult::Done(())); } } if state.db_size == 0 { state.page_stack.pop(); return Ok(IOResult::Done(())); } loop { let Some(IntegrityCheckPageEntry { page_idx, page_category, level, max_intkey, overflow_pages_expected, overflow_pages_seen, }) = state.page_stack.last().cloned() else { return Ok(IOResult::Done(())); }; turso_assert!( page_idx >= 0, "pages should be positive during integrity check" ); let page = match state.page.take() { Some(page) => page, None => { let (page, c) = btree_read_page(pager, page_idx)?; state.page = Some(page); if let Some(c) = c { io_yield_one!(c); } state.page.take().expect("page should be present") } }; turso_assert!(page.is_loaded(), "page should be loaded"); state.page_stack.pop(); let contents = page.get_contents(); if page_category == PageCategory::FreeListTrunk { state.freelist_count.actual_count += 1; let next_freelist_trunk_page = contents.read_u32_no_offset(FREELIST_TRUNK_OFFSET_NEXT_TRUNK_PTR); if next_freelist_trunk_page != 0 { if next_freelist_trunk_page as usize > state.db_size { tracing::error!( "integrity_check: freelist trunk page {} has invalid next pointer {}. header_bytes={:02x?}", page.get().id, next_freelist_trunk_page, &contents.as_ptr()[0..16] ); errors.push(IntegrityCheckError::FreelistPointerOutOfRange { page_id: page.get().id as i64, pointer: next_freelist_trunk_page as i64, }); continue; } state.push_page( IntegrityCheckPageEntry { page_idx: next_freelist_trunk_page as i64, level, max_intkey, page_category: PageCategory::FreeListTrunk, overflow_pages_expected: None, overflow_pages_seen: 0, }, page.get().id as i64, errors, ); } let page_pointers = contents.read_u32_no_offset(FREELIST_TRUNK_OFFSET_LEAF_COUNT); let page_size = contents.as_ptr().len(); let max_pointers = page_size.saturating_sub(FREELIST_TRUNK_HEADER_SIZE) / FREELIST_LEAF_PTR_SIZE; if unlikely(page_pointers as usize > max_pointers) { tracing::error!( "integrity_check: freelist trunk page {} has invalid leaf count {} (max {}). header_bytes={:02x?}", page.get().id, page_pointers, max_pointers, &contents.as_ptr()[0..16] ); errors.push(IntegrityCheckError::FreelistTrunkCorrupt { page_id: page.get().id as i64, page_pointers, max_pointers, }); continue; } for i in 0..page_pointers { let offset = FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR + FREELIST_LEAF_PTR_SIZE * i as usize; if unlikely(offset + FREELIST_LEAF_PTR_SIZE > page_size) { tracing::error!( "integrity_check: freelist trunk page {} has invalid leaf offset {}. header_bytes={:02x?}", page.get().id, offset, &contents.as_ptr()[0..16] ); errors.push(IntegrityCheckError::FreelistTrunkCorrupt { page_id: page.get().id as i64, page_pointers, max_pointers, }); break; } let page_pointer = contents.read_u32_no_offset(offset); if page_pointer as usize > state.db_size { tracing::error!( "integrity_check: freelist trunk page {} has invalid leaf pointer {}. header_bytes={:02x?}", page.get().id, page_pointer, &contents.as_ptr()[0..16] ); errors.push(IntegrityCheckError::FreelistPointerOutOfRange { page_id: page.get().id as i64, pointer: page_pointer as i64, }); continue; } state.push_page( IntegrityCheckPageEntry { page_idx: page_pointer as i64, level, max_intkey, page_category: PageCategory::FreePage, overflow_pages_expected: None, overflow_pages_seen: 0, }, page.get().id as i64, errors, ); } continue; } if page_category == PageCategory::FreePage { state.freelist_count.actual_count += 1; continue; } if page_category == PageCategory::Overflow { let overflow_pages_seen = overflow_pages_seen.saturating_add(1); let next_overflow_page = contents.read_u32_no_offset(0); if next_overflow_page != 0 { state.push_page( IntegrityCheckPageEntry { page_idx: next_overflow_page as i64, level, max_intkey, page_category: PageCategory::Overflow, overflow_pages_expected, overflow_pages_seen, }, page.get().id as i64, errors, ); } else if let Some(expected) = overflow_pages_expected { if overflow_pages_seen != expected { errors.push(IntegrityCheckError::OverflowListLengthMismatch { got: overflow_pages_seen, expected, }); } } continue; } let usable_space = pager.usable_space(); let mut coverage_checker = CoverageChecker::new(page.get().id as i64); // Now we check every cell for few things: // 1. Check cell is in correct range. Not exceeds page and not starts before we have marked // (cell content area). // 2. We add the cell to coverage checker in order to check if cells do not overlap. // 3. We check order of rowids in case of table pages. We iterate backwards in order to check // if current cell's rowid is less than the next cell. We also check rowid is less than the // parent's divider cell. In case of this page being root page max rowid will be i64::MAX. // 4. We append pages to the stack to check later. // 5. In case of leaf page, check if the current level(depth) is equal to other leaf pages we // have seen. let mut next_rowid = max_intkey; for cell_idx in (0..contents.cell_count()).rev() { let (cell_start, cell_length) = contents.cell_get_raw_region(cell_idx, usable_space)?; if cell_start < contents.cell_content_area() as usize || cell_start > usable_space - 4 { errors.push(IntegrityCheckError::CellOutOfRange { cell_idx, page_id: page.get().id as i64, cell_start, cell_end: cell_start + cell_length, content_area: contents.cell_content_area() as usize, usable_space, }); } if cell_start + cell_length > usable_space { errors.push(IntegrityCheckError::CellOverflowsPage { cell_idx, page_id: page.get().id as i64, cell_start, cell_end: cell_start + cell_length, content_area: contents.cell_content_area() as usize, usable_space, }); } coverage_checker.add_cell(cell_start, cell_start + cell_length); let cell = contents.cell_get(cell_idx, usable_space)?; match cell { BTreeCell::TableInteriorCell(table_interior_cell) => { state.push_page( IntegrityCheckPageEntry { page_idx: table_interior_cell.left_child_page as i64, level: level + 1, max_intkey: table_interior_cell.rowid, page_category: PageCategory::Normal, overflow_pages_expected: None, overflow_pages_seen: 0, }, page.get().id as i64, errors, ); let rowid = table_interior_cell.rowid; if rowid > max_intkey || rowid > next_rowid { errors.push(IntegrityCheckError::CellRowidOutOfRange { page_id: page.get().id as i64, page_category, cell_idx, rowid, max_intkey, next_rowid, }); } next_rowid = rowid; } BTreeCell::TableLeafCell(table_leaf_cell) => { // check depth of leaf pages are equal if let Some(expected_leaf_level) = state.first_leaf_level { if expected_leaf_level != level { errors.push(IntegrityCheckError::LeafDepthMismatch { page_id: page.get().id as i64, this_page_depth: level, other_page_depth: expected_leaf_level, }); } } else { state.first_leaf_level = Some(level); } let rowid = table_leaf_cell.rowid; if rowid > max_intkey || rowid > next_rowid { errors.push(IntegrityCheckError::CellRowidOutOfRange { page_id: page.get().id as i64, page_category, cell_idx, rowid, max_intkey, next_rowid, }); } next_rowid = rowid; if let Some(first_overflow_page) = table_leaf_cell.first_overflow_page { let expected_pages = overflow_pages_expected_for_cell( table_leaf_cell.payload_size, table_leaf_cell.payload.len(), usable_space, ); state.push_page( IntegrityCheckPageEntry { page_idx: first_overflow_page as i64, level, max_intkey, page_category: PageCategory::Overflow, overflow_pages_expected: Some(expected_pages), overflow_pages_seen: 0, }, page.get().id as i64, errors, ); } } BTreeCell::IndexInteriorCell(index_interior_cell) => { state.push_page( IntegrityCheckPageEntry { page_idx: index_interior_cell.left_child_page as i64, level: level + 1, max_intkey, // we don't care about intkey in non-table pages page_category: PageCategory::Normal, overflow_pages_expected: None, overflow_pages_seen: 0, }, page.get().id as i64, errors, ); if let Some(first_overflow_page) = index_interior_cell.first_overflow_page { let expected_pages = overflow_pages_expected_for_cell( index_interior_cell.payload_size, index_interior_cell.payload.len(), usable_space, ); state.push_page( IntegrityCheckPageEntry { page_idx: first_overflow_page as i64, level, max_intkey, page_category: PageCategory::Overflow, overflow_pages_expected: Some(expected_pages), overflow_pages_seen: 0, }, page.get().id as i64, errors, ); } } BTreeCell::IndexLeafCell(index_leaf_cell) => { // check depth of leaf pages are equal if let Some(expected_leaf_level) = state.first_leaf_level { if expected_leaf_level != level { errors.push(IntegrityCheckError::LeafDepthMismatch { page_id: page.get().id as i64, this_page_depth: level, other_page_depth: expected_leaf_level, }); } } else { state.first_leaf_level = Some(level); } if let Some(first_overflow_page) = index_leaf_cell.first_overflow_page { let expected_pages = overflow_pages_expected_for_cell( index_leaf_cell.payload_size, index_leaf_cell.payload.len(), usable_space, ); state.push_page( IntegrityCheckPageEntry { page_idx: first_overflow_page as i64, level, max_intkey, page_category: PageCategory::Overflow, overflow_pages_expected: Some(expected_pages), overflow_pages_seen: 0, }, page.get().id as i64, errors, ); } } } } if let Some(rightmost) = contents.rightmost_pointer()? { state.push_page( IntegrityCheckPageEntry { page_idx: rightmost as i64, level: level + 1, max_intkey, page_category: PageCategory::Normal, overflow_pages_expected: None, overflow_pages_seen: 0, }, page.get().id as i64, errors, ); } // Now we add free blocks to the coverage checker let first_freeblock = contents.first_freeblock() as usize; if first_freeblock > 0 { let mut pc = first_freeblock; while pc > 0 { let next = contents.read_u16_no_offset(pc as usize) as usize; let size = contents.read_u16_no_offset(pc as usize + 2) as usize; // check it doesn't go out of range if pc > usable_space - 4 { errors.push(IntegrityCheckError::FreeBlockOutOfRange { page_id: page.get().id as i64, start: pc, end: pc + size, }); break; } coverage_checker.add_free_block(pc, pc + size); pc = next; } } // Let's check the overlap of freeblocks and cells now that we have collected them all. coverage_checker.analyze( usable_space, contents.cell_content_area() as usize, errors, contents.num_frag_free_bytes() as usize, ); } } pub fn btree_read_page(pager: &Arc, page_idx: i64) -> Result<(PageRef, Option)> { pager.read_page(page_idx) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct IntegrityCheckCellRange { start: usize, end: usize, is_free_block: bool, } // Implement ordering for min-heap (smallest start address first) impl Ord for IntegrityCheckCellRange { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.start.cmp(&other.start) } } impl PartialOrd for IntegrityCheckCellRange { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } #[cfg(debug_assertions)] fn validate_cells_after_insertion(cell_array: &CellArray, leaf_data: bool) { for cell in &cell_array.cell_payloads { turso_assert_greater_than_or_equal!(cell.len(), 4); if leaf_data { turso_assert!(cell[0] != 0); } } } pub struct CoverageChecker { /// Min-heap ordered by cell start heap: BinaryHeap>, page_idx: i64, } impl CoverageChecker { pub fn new(page_idx: i64) -> Self { Self { heap: BinaryHeap::new(), page_idx, } } fn add_range(&mut self, cell_start: usize, cell_end: usize, is_free_block: bool) { self.heap.push(Reverse(IntegrityCheckCellRange { start: cell_start, end: cell_end, is_free_block, })); } pub fn add_cell(&mut self, cell_start: usize, cell_end: usize) { self.add_range(cell_start, cell_end, false); } pub fn add_free_block(&mut self, cell_start: usize, cell_end: usize) { self.add_range(cell_start, cell_end, true); } pub fn analyze( &mut self, usable_space: usize, content_area: usize, errors: &mut Vec, expected_fragmentation: usize, ) { let mut fragmentation = 0; let mut prev_end = content_area; while let Some(cell) = self.heap.pop() { let start = cell.0.start; if prev_end > start { errors.push(IntegrityCheckError::CellOverlap { page_id: self.page_idx, start, prev_end, content_area, is_free_block: cell.0.is_free_block, }); break; } else { fragmentation += start - prev_end; prev_end = cell.0.end; } } fragmentation += usable_space - prev_end; if fragmentation != expected_fragmentation { errors.push(IntegrityCheckError::UnexpectedFragmentation { page_id: self.page_idx, got: fragmentation, expected: expected_fragmentation, }); } } } /// Stack of pages representing the tree traversal order. /// current_page represents the current page being used in the tree and current_page - 1 would be /// the parent. Using current_page + 1 or higher is undefined behaviour. struct PageStack { /// Pointer to the current page being consumed current_page: i32, /// List of pages in the stack. Root page will be in index 0 pub stack: [Option; BTCURSOR_MAX_DEPTH + 1], /// List of cell indices in the stack. /// node_states[current_page] is the current cell index being consumed. Similarly /// node_states[current_page-1] is the cell index of the parent of the current page /// that we save in case of going back up. /// There are two points that need special attention: /// If node_states[current_page] = -1, it indicates that the current iteration has reached the start of the current_page /// If node_states[current_page] = `cell_count`, it means that the current iteration has reached the end of the current_page node_states: [BTreeNodeState; BTCURSOR_MAX_DEPTH + 1], } impl PageStack { /// Push a new page onto the stack. /// This effectively means traversing to a child page. #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG, name = "pagestack::push"))] fn _push(&mut self, page: PageRef, starting_cell_idx: i32) { tracing::trace!(current = self.current_page, new_page_id = page.get().id,); 'validate: { let current = self.current_page; if current == -1 { break 'validate; } let current_top = self.stack[current as usize].as_ref(); if let Some(current_top) = current_top { turso_assert!( current_top.get().id != page.get().id, "about to push page twice", { "page_id": page.get().id } ); } } self.populate_parent_cell_count(); self.current_page += 1; turso_assert_greater_than_or_equal!(self.current_page, 0); let current = self.current_page as usize; turso_assert_less_than!( current, BTCURSOR_MAX_DEPTH, "corrupted database, stack is bigger than expected" ); // Pin the page to prevent it from being evicted while on the stack page.pin(); self.stack[current] = Some(page); self.node_states[current] = BTreeNodeState { cell_idx: starting_cell_idx, cell_count: None, // we don't know the cell count yet, so we set it to None. any code pushing a child page onto the stack MUST set the parent page's cell_count. }; } /// Populate the parent page's cell count. /// This is needed so that we can, from a child page, check of ancestor pages' position relative to its cell index /// without having to perform IO to get the ancestor page contents. /// /// This rests on the assumption that the parent page is already in memory whenever a child is pushed onto the stack. /// We currently ensure this by pinning all the pages on [PageStack] to the page cache so that they cannot be evicted. fn populate_parent_cell_count(&mut self) { let stack_empty = self.current_page == -1; if stack_empty { return; } let current = self.current(); let page = self.stack[current].as_ref().unwrap(); turso_assert!( page.is_pinned(), "parent page is not pinned", { "page_id": page.get().id } ); turso_assert!( page.is_loaded(), "parent page is not loaded", { "page_id": page.get().id } ); let contents = page.get_contents(); let cell_count = contents.cell_count() as i32; self.node_states[current].cell_count = Some(cell_count); } fn push(&mut self, page: PageRef) { self._push(page, -1); } fn push_backwards(&mut self, page: PageRef) { self._push(page, i32::MAX); } /// Pop a page off the stack. /// This effectively means traversing back up to a parent page. #[cfg_attr(debug_assertions, instrument(skip_all, level = Level::DEBUG, name = "pagestack::pop"))] fn pop(&mut self) { let current = self.current_page; turso_assert_greater_than_or_equal!(current, 0); tracing::trace!(current); let current = current as usize; // Unpin the page before removing it from the stack if let Some(page) = &self.stack[current] { page.unpin(); } turso_assert_greater_than!(current, 0); self.node_states[current] = BTreeNodeState::default(); self.stack[current] = None; self.current_page -= 1; } /// Get the top page on the stack. /// This is the page that is currently being traversed. fn top(&self) -> PageRef { let current = self.current(); let page = self.stack[current].clone().unwrap(); turso_assert!(page.is_loaded(), "page should be loaded"); page } fn top_ref(&self) -> &PageRef { let current = self.current(); let page = self.stack[current].as_ref().unwrap(); turso_assert!(page.is_loaded(), "page should be loaded"); page } /// Current page pointer being used #[inline(always)] fn current(&self) -> usize { turso_assert_greater_than_or_equal!(self.current_page, 0); self.current_page as usize } /// Cell index of the current page fn current_cell_index(&self) -> i32 { let current = self.current(); self.node_states[current].cell_idx } /// Check if the current cell index is less than 0. /// This means we have been iterating backwards and have reached the start of the page. fn current_cell_index_less_than_min(&self) -> bool { let cell_idx = self.current_cell_index(); cell_idx < 0 } /// Advance the current cell index of the current page to the next cell. /// We usually advance after going traversing a new page #[inline(always)] fn advance(&mut self) { let current = self.current(); self.node_states[current].cell_idx += 1; } #[cfg_attr(debug_assertions, instrument(skip(self), level = Level::DEBUG, name = "pagestack::retreat"))] fn retreat(&mut self) { let current = self.current(); #[cfg(debug_assertions)] { tracing::trace!( curr_cell_index = self.node_states[current].cell_idx, node_states = ?self.node_states.iter().map(|state| state.cell_idx).collect::>(), ); } self.node_states[current].cell_idx -= 1; } fn set_cell_index(&mut self, idx: i32) { let current = self.current(); self.node_states[current].cell_idx = idx; } fn has_parent(&self) -> bool { self.current_page > 0 } /// Get a page at a specific level in the stack (0 = root, 1 = first child, etc.) fn get_page_at_level(&self, level: usize) -> Option<&PageRef> { if level < self.stack.len() { self.stack[level].as_ref() } else { None } } fn get_page_contents_at_level(&self, level: usize) -> Option<&mut PageContent> { self.get_page_at_level(level) .map(|page| page.get_contents()) } fn unpin_all_if_pinned(&mut self) { self.stack.iter_mut().flatten().for_each(|page| { let _ = page.try_unpin(); }); } fn clear(&mut self) { self.unpin_all_if_pinned(); self.current_page = -1; } } impl Drop for PageStack { fn drop(&mut self) { self.unpin_all_if_pinned(); } } /// Used for redistributing cells during a balance operation. struct CellArray { /// The actual cell data. /// For all other page types except table leaves, this will also contain the associated divider cell from the parent page. cell_payloads: Vec<&'static mut [u8]>, /// Prefix sum of cells in each page. /// For example, if three pages have 1, 2, and 3 cells, respectively, /// then cell_count_per_page_cumulative will be [1, 3, 6]. cell_count_per_page_cumulative: [u16; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE], } impl std::fmt::Debug for CellArray { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CellArray").finish() } } impl CellArray { pub fn cell_size_bytes(&self, cell_idx: usize) -> u16 { self.cell_payloads[cell_idx].len() as u16 } /// Returns the number of cells up to and including the given page. pub fn cell_count_up_to_page(&self, page_idx: usize) -> usize { self.cell_count_per_page_cumulative[page_idx] as usize } } /// Try to find a freeblock inside the cell content area that is large enough to fit the given amount of bytes. /// Used to check if a cell can be inserted into a freeblock to reduce fragmentation. /// Returns the absolute byte offset of the freeblock if found. fn find_free_slot( page_ref: &PageContent, usable_space: usize, amount: usize, ) -> Result> { const CELL_SIZE_MIN: usize = 4; // NOTE: freelist is in ascending order of keys and pc // unuse_space is reserved bytes at the end of page, therefore we must substract from maxpc let mut prev_block = None; let mut cur_block = match page_ref.first_freeblock() { 0 => None, first_block => Some(first_block as usize), }; let max_start_offset = usable_space - amount; while let Some(cur) = cur_block { if unlikely(cur + CELL_SIZE_MIN > usable_space) { return_corrupt!("Free block header extends beyond page"); } let (next, size) = { let cur_u16: u16 = cur .try_into() .unwrap_or_else(|_| panic!("cur={cur} is too large to fit in a u16")); let (next, size) = page_ref.read_freeblock(cur_u16); (next as usize, size as usize) }; // Doesn't fit in this freeblock, try the next one. if amount > size { if next == 0 { // No next -> can't fit. return Ok(None); } prev_block = cur_block; if unlikely(next <= cur) { return_corrupt!("Free list not in ascending order"); } cur_block = Some(next); continue; } let new_size = size - amount; // If the freeblock's new size is < CELL_SIZE_MIN, the freeblock is deleted and the remaining bytes // become fragmented free bytes. if new_size < CELL_SIZE_MIN { if page_ref.num_frag_free_bytes() > 57 { // SQLite has a fragmentation limit of 60 bytes. // check sqlite docs https://www.sqlite.org/fileformat.html#:~:text=A%20freeblock%20requires,not%20exceed%2060 return Ok(None); } // Delete the slot from freelist and update the page's fragment count. match prev_block { Some(prev) => { let prev_u16: u16 = prev .try_into() .unwrap_or_else(|_| panic!("prev={prev} is too large to fit in a u16")); let next_u16: u16 = next .try_into() .unwrap_or_else(|_| panic!("next={next} is too large to fit in a u16")); page_ref.write_freeblock_next_ptr(prev_u16, next_u16); } None => { let next_u16: u16 = next .try_into() .unwrap_or_else(|_| panic!("next={next} is too large to fit in a u16")); page_ref.write_first_freeblock(next_u16); } } let new_size_u8: u8 = new_size .try_into() .unwrap_or_else(|_| panic!("new_size={new_size} is too large to fit in a u8")); let frag = page_ref.num_frag_free_bytes() + new_size_u8; page_ref.write_fragmented_bytes_count(frag); return Ok(cur_block); } else if unlikely(new_size + cur > max_start_offset) { return_corrupt!("Free block extends beyond page end"); } else { // Requested amount fits inside the current free slot so we reduce its size // to account for newly allocated space. let cur_u16: u16 = cur .try_into() .unwrap_or_else(|_| panic!("cur={cur} is too large to fit in a u16")); let new_size_u16: u16 = new_size .try_into() .unwrap_or_else(|_| panic!("new_size={new_size} is too large to fit in a u16")); page_ref.write_freeblock_size(cur_u16, new_size_u16); // Return the offset immediately after the shrunk freeblock. return Ok(Some(cur + new_size)); } } Ok(None) } pub fn btree_init_page(page: &PageRef, page_type: PageType, offset: usize, usable_space: usize) { // setup btree page let contents = page.get_contents(); tracing::debug!( "btree_init_page(id={}, offset={}, usable_space={})", page.get().id, offset, usable_space ); #[cfg(debug_assertions)] //TODO restore format args (as the "details" last arg) turso_assert_eq!( offset, contents.offset(), "offset doesn't match computed offset for page" ); let id = page_type as u8; contents.write_page_type(id); contents.write_first_freeblock(0); contents.write_cell_count(0); contents.write_cell_content_area(usable_space); contents.write_fragmented_bytes_count(0); contents.write_rightmost_ptr(0); #[cfg(debug_assertions)] { // we might get already used page from the pool. generally this is not a problem because // b tree access is very controlled. However, for encrypted pages (and also checksums) we want // to ensure that there are no reserved bytes that contain old data. let buf = contents.as_ptr(); let buffer_len = buf.len(); turso_assert!( usable_space <= buffer_len, "usable_space must be <= buffer_len" ); // this is no op if usable_space == buffer_len buf[usable_space..buffer_len].fill(0); } } fn to_static_buf(buf: &mut [u8]) -> &'static mut [u8] { unsafe { std::mem::transmute::<&mut [u8], &'static mut [u8]>(buf) } } fn edit_page( page: &mut PageContent, start_old_cells: usize, start_new_cells: usize, number_new_cells: usize, cell_array: &CellArray, usable_space: usize, ) -> Result<()> { tracing::debug!( "edit_page start_old_cells={} start_new_cells={} number_new_cells={} cell_array={}", start_old_cells, start_new_cells, number_new_cells, cell_array.cell_payloads.len() ); let end_old_cells = start_old_cells + page.cell_count() + page.overflow_cells.len(); let end_new_cells = start_new_cells + number_new_cells; let mut count_cells = page.cell_count(); if start_old_cells < start_new_cells { debug_validate_cells!(page, usable_space); let number_to_shift = page_free_array( page, start_old_cells, start_new_cells - start_old_cells, cell_array, usable_space, )?; // shift pointers left shift_cells_left(page, count_cells, number_to_shift); count_cells -= number_to_shift; debug_validate_cells!(page, usable_space); } if end_new_cells < end_old_cells { debug_validate_cells!(page, usable_space); let number_tail_removed = page_free_array( page, end_new_cells, end_old_cells - end_new_cells, cell_array, usable_space, )?; turso_assert_greater_than_or_equal!(count_cells, number_tail_removed); count_cells -= number_tail_removed; debug_validate_cells!(page, usable_space); } // TODO: make page_free_array defragment, for now I'm lazy so this will work for now. let mut defragmented_page = defragment_page_for_insert(page, usable_space, 0)?; // TODO: add to start if start_new_cells < start_old_cells { let count = number_new_cells.min(start_old_cells - start_new_cells); page_insert_array( &mut defragmented_page, start_new_cells, count, cell_array, 0, usable_space, )?; count_cells += count; } // TODO: overflow cells debug_validate_cells!(defragmented_page.0, usable_space); for i in 0..defragmented_page.0.overflow_cells.len() { let overflow_cell = &defragmented_page.0.overflow_cells[i]; // cell index in context of new list of cells that should be in the page if start_old_cells + overflow_cell.index >= start_new_cells { let cell_idx = start_old_cells + overflow_cell.index - start_new_cells; if cell_idx < number_new_cells { count_cells += 1; page_insert_array( &mut defragmented_page, start_new_cells + cell_idx, 1, cell_array, cell_idx, usable_space, )?; } } } debug_validate_cells!(defragmented_page.0, usable_space); // TODO: append cells to end page_insert_array( &mut defragmented_page, start_new_cells + count_cells, number_new_cells - count_cells, cell_array, count_cells, usable_space, )?; debug_validate_cells!(defragmented_page.0, usable_space); // TODO: noverflow page.write_cell_count(number_new_cells as u16); Ok(()) } /// Shifts the cell pointers in the B-tree page to the left by a specified number of positions. /// /// # Parameters /// - `page`: A mutable reference to the `PageContent` representing the B-tree page. /// - `count_cells`: The total number of cells currently in the page. /// - `number_to_shift`: The number of cell pointers to shift to the left. /// /// # Behavior /// This function modifies the cell pointer array within the page by copying memory regions. /// It shifts the pointers starting from `number_to_shift` to the beginning of the array, /// effectively removing the first `number_to_shift` pointers. fn shift_cells_left(page: &mut PageContent, count_cells: usize, number_to_shift: usize) { let buf = page.as_ptr(); let (start, _) = page.cell_pointer_array_offset_and_size(); buf.copy_within( start + (number_to_shift * 2)..start + (count_cells * 2), start, ); } fn page_free_array( page: &mut PageContent, first: usize, count: usize, cell_array: &CellArray, usable_space: usize, ) -> Result { tracing::debug!("page_free_array {}..{}", first, first + count); let buf = &mut page.as_ptr()[page.offset()..usable_space]; let buf_range = buf.as_ptr_range(); let mut number_of_cells_removed = 0; let mut number_of_cells_buffered = 0; let mut buffered_cells_offsets: [usize; 10] = [0; 10]; let mut buffered_cells_ends: [usize; 10] = [0; 10]; for i in first..first + count { let cell = &cell_array.cell_payloads[i]; let cell_pointer = cell.as_ptr_range(); // check if not overflow cell if cell_pointer.start >= buf_range.start && cell_pointer.start < buf_range.end { turso_assert!( cell_pointer.end >= buf_range.start && cell_pointer.end <= buf_range.end, "whole cell should be inside the page" ); // TODO: remove pointer too let offset = cell_pointer.start as usize - buf_range.start as usize; let len = cell_pointer.end as usize - cell_pointer.start as usize; turso_assert_greater_than!(len, 0, "cell size should be greater than 0"); let end = offset + len; /* Try to merge the current cell with a contiguous buffered cell to reduce the number of * `free_cell_range()` operations. Break on the first merge to avoid consuming too much time, * `free_cell_range()` will try to merge contiguous cells anyway. */ let mut j = 0; while j < number_of_cells_buffered { // If the buffered cell is immediately after the current cell if buffered_cells_offsets[j] == end { // Merge them by updating the buffered cell's offset to the current cell's offset buffered_cells_offsets[j] = offset; break; // If the buffered cell is immediately before the current cell } else if buffered_cells_ends[j] == offset { // Merge them by updating the buffered cell's end offset to the current cell's end offset buffered_cells_ends[j] = end; break; } j += 1; } // If no cells were merged if j >= number_of_cells_buffered { // If the buffered cells array is full, flush the buffered cells using `free_cell_range()` to empty the array if number_of_cells_buffered >= buffered_cells_offsets.len() { for j in 0..number_of_cells_buffered { free_cell_range( page, buffered_cells_offsets[j], buffered_cells_ends[j] - buffered_cells_offsets[j], usable_space, )?; } number_of_cells_buffered = 0; // Reset array counter } // Buffer the current cell buffered_cells_offsets[number_of_cells_buffered] = offset; buffered_cells_ends[number_of_cells_buffered] = end; number_of_cells_buffered += 1; } number_of_cells_removed += 1; } } for j in 0..number_of_cells_buffered { free_cell_range( page, buffered_cells_offsets[j], buffered_cells_ends[j] - buffered_cells_offsets[j], usable_space, )?; } page.write_cell_count(page.cell_count() as u16 - number_of_cells_removed as u16); Ok(number_of_cells_removed) } /// A proof type that guarantees a page has been defragmented. /// /// This type can only be constructed by calling [`defragment_page_for_insert`], /// which ensures the page has been defragmented before any insert operations. /// Functions like [`page_insert_array`] require this type to enforce at compile-time /// that defragmentation has occurred. pub struct DefragmentedPage<'a>(&'a mut PageContent); impl std::ops::Deref for DefragmentedPage<'_> { type Target = PageContent; #[inline] fn deref(&self) -> &Self::Target { self.0 } } impl std::ops::DerefMut for DefragmentedPage<'_> { #[inline] fn deref_mut(&mut self) -> &mut Self::Target { self.0 } } /// Insert multiple cells into a page in a single batch operation. /// /// This is an optimized version that avoids O(N²) complexity by: /// 1. Computing total space needed upfront /// 2. Allocating all space at once /// 3. Copying all cell payloads sequentially /// 4. Shifting existing cell pointers once /// 5. Writing all new cell pointers in one pass /// 6. Updating cell count once fn page_insert_array( page: &mut DefragmentedPage, first: usize, count: usize, cell_array: &CellArray, start_insert: usize, _usable_space: usize, ) -> Result<()> { if count == 0 { return Ok(()); } tracing::debug!( "page_insert_array(first={}, count={}, start_insert={}, cell_count={}, page_type={:?})", first, count, start_insert, page.cell_count(), page.page_type().ok() ); turso_assert!(first <= cell_array.cell_payloads.len(), "first OOB"); turso_assert!( count <= cell_array.cell_payloads.len().saturating_sub(first), "first+count OOB" ); // Calculate total space needed for all cell payloads // We read from cell_array at indices [first, first+count) let mut total_payload_size: usize = 0; for i in 0..count { let payload = &cell_array.cell_payloads[first + i]; let cell_size = payload.len().max(MINIMUM_CELL_SIZE); total_payload_size += cell_size; } // Total space needed includes cell pointers let total_ptr_space = count.checked_mul(CELL_PTR_SIZE_BYTES).ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("page_insert_array: ptr space overflow".into()) })?; // After defragmentation, all free space is in the unallocated region // between the cell pointer array and the cell content area. let current_cell_count = page.cell_count(); let mut cell_content_area = page.cell_content_area() as usize; let unallocated_start = page.unallocated_region_start(); turso_assert!( start_insert <= current_cell_count, "start_insert beyond cell_count" ); turso_assert!( // we cast to u16 later so assert no overflow current_cell_count + count <= u16::MAX as usize, "cell_count overflow" ); // Verify we have enough space // The new cell pointers will extend the cell pointer array by `total_ptr_space` // The new cell content will reduce cell_content_area by `total_payload_size` let new_unallocated_start = unallocated_start .checked_add(total_ptr_space) .ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("page_insert_array: unalloc start overflow".into()) })?; let new_cell_content_area = cell_content_area .checked_sub(total_payload_size) .ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("page_insert_array: payload underflow".to_string()) })?; turso_assert!( new_unallocated_start <= new_cell_content_area, "page_insert_array: not enough space for pointers and payloads in unallocated region", { "total_ptr_space": total_ptr_space, "total_payload_size": total_payload_size, "unallocated_start": unallocated_start, "cell_content_area": cell_content_area, "unallocated_region_size": cell_content_area - unallocated_start } ); let buf = page.as_ptr(); let (cell_pointer_array_start, _) = page.cell_pointer_array_offset_and_size(); // Shift existing cell pointers to make room for new ones // We're inserting `count` cells at position `start_insert`, so we need to shift // all cell pointers from position `start_insert` onwards to the right by `count * 2` bytes if start_insert < current_cell_count { let cells_to_shift = current_cell_count - start_insert; let shift_src_start = cell_pointer_array_start + (start_insert * CELL_PTR_SIZE_BYTES); let shift_dst_start = shift_src_start + total_ptr_space; let shift_size = cells_to_shift * CELL_PTR_SIZE_BYTES; buf.copy_within( shift_src_start..shift_src_start + shift_size, shift_dst_start, ); } // Allocate space for all cells and write payloads + pointers // We allocate space from the content area (which grows downward) // Read from cell_array[first..first+count], insert at page positions [start_insert..start_insert+count] for i in 0..count { let payload = &cell_array.cell_payloads[first + i]; let cell_size = payload.len().max(MINIMUM_CELL_SIZE); // Allocate space for this cell (grow content area downward) cell_content_area = cell_content_area.checked_sub(cell_size).ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("page_insert_array: cell allocation underflow".to_string()) })?; // Copy cell payload buf[cell_content_area..cell_content_area + payload.len()].copy_from_slice(payload); // Write cell pointer at position (start_insert + i) let ptr_offset = cell_pointer_array_start + ((start_insert + i) * CELL_PTR_SIZE_BYTES); page.write_u16_no_offset(ptr_offset, cell_content_area as u16); } // Update page header page.write_cell_content_area(cell_content_area); page.write_cell_count((current_cell_count + count) as u16); debug_validate_cells!(page, _usable_space); Ok(()) } /// Free the range of bytes that a cell occupies. /// This function also updates the freeblock list in the page. /// Freeblocks are used to keep track of free space in the page, /// and are organized as a linked list. /// /// This function may merge the freed cell range into either the next freeblock, /// previous freeblock, or both. fn free_cell_range( page: &mut PageContent, mut offset: usize, len: usize, usable_space: usize, ) -> Result<()> { const CELL_SIZE_MIN: usize = 4; if unlikely(len < CELL_SIZE_MIN) { return_corrupt!("free_cell_range: minimum cell size is {CELL_SIZE_MIN}"); } if unlikely(offset > usable_space.saturating_sub(CELL_SIZE_MIN)) { return_corrupt!("free_cell_range: start offset beyond usable space: offset={offset} usable_space={usable_space}"); } let mut size = len; let mut end = offset + len; if unlikely(end > usable_space) { return_corrupt!("free_cell_range: freed range extends beyond usable space: offset={offset} len={len} end={end} usable_space={usable_space}"); } let cur_content_area = page.cell_content_area() as usize; let first_block = page.first_freeblock() as usize; if first_block == 0 { if unlikely(offset < cur_content_area) { return_corrupt!("free_cell_range: free block before content area: offset={offset} cell_content_area={cur_content_area}"); } if offset == cur_content_area { // if the freeblock list is empty and the freed range is exactly at the beginning of the content area, // we are not creating a freeblock; instead we are just extending the unallocated region. page.write_cell_content_area(end); } else { // otherwise we set it as the first freeblock in the page header. let offset_u16: u16 = offset .try_into() .unwrap_or_else(|_| panic!("offset={offset} is too large to fit in a u16")); page.write_first_freeblock(offset_u16); let size_u16: u16 = size .try_into() .unwrap_or_else(|_| panic!("size={size} is too large to fit in a u16")); page.write_freeblock(offset_u16, size_u16, None); } return Ok(()); } // if the freeblock list is not empty, we need to find the correct position to insert the new freeblock // resulting from the freeing of this cell range; we may be also able to merge the freed range into existing freeblocks. let mut prev_block = None; let mut next_block = Some(first_block); while let Some(next) = next_block { if unlikely(prev_block.is_some_and(|prev| next <= prev)) { return_corrupt!("free_cell_range: freeblocks not in ascending order: next_block={next} prev_block={prev_block:?}"); } if next >= offset { break; } prev_block = Some(next); next_block = match page.read_u16_no_offset(next) { // Freed range extends beyond the last freeblock, so we are creating a new freeblock. 0 => None, next => Some(next as usize), }; } if let Some(next) = next_block { if unlikely(next + CELL_SIZE_MIN > usable_space) { return_corrupt!("free_cell_range: free block beyond usable space: next_block={next} usable_space={usable_space}"); } } let mut removed_fragmentation = 0; const SINGLE_FRAGMENT_SIZE_MAX: usize = CELL_SIZE_MIN - 1; // If the freed range extends into the next freeblock, we will merge the freed range into it. // If there is a 1-3 byte gap between the freed range and the next freeblock, we are effectively // clearing that amount of fragmented bytes, since a 1-3 byte range cannot be a valid cell. if let Some(next) = next_block { if end + SINGLE_FRAGMENT_SIZE_MAX >= next { removed_fragmentation = (next - end) as u8; let next_size = page.read_u16_no_offset(next + 2) as usize; end = next + next_size; if unlikely(end > usable_space) { return_corrupt!("free_cell_range: coalesced block extends beyond page: offset={offset} len={len} end={end} usable_space={usable_space}"); } size = end - offset; // Since we merged the two freeblocks, we need to update the next_block to the next freeblock in the list. next_block = match page.read_u16_no_offset(next) { 0 => None, next => Some(next as usize), }; } } // If the freed range extends into the previous freeblock, we will merge them similarly as above. if let Some(prev) = prev_block { let prev_size = page.read_u16_no_offset(prev + 2) as usize; let prev_end = prev + prev_size; if unlikely(prev_end > offset) { return_corrupt!( "free_cell_range: previous block overlap: prev_end={prev_end} offset={offset}" ); } // If the previous freeblock extends into the freed range, we will merge the freed range into the // previous freeblock and clear any 1-3 byte fragmentation in between, similarly as above if prev_end + SINGLE_FRAGMENT_SIZE_MAX >= offset { removed_fragmentation += (offset - prev_end) as u8; size = end - prev; offset = prev; } } let cur_frag_free_bytes = page.num_frag_free_bytes(); if unlikely(removed_fragmentation > cur_frag_free_bytes) { return_corrupt!("free_cell_range: invalid fragmentation count: removed_fragmentation={removed_fragmentation} num_frag_free_bytes={cur_frag_free_bytes}"); } let frag = cur_frag_free_bytes - removed_fragmentation; page.write_fragmented_bytes_count(frag); if unlikely(offset < cur_content_area) { return_corrupt!("free_cell_range: free block before content area: offset={offset} cell_content_area={cur_content_area}"); } // As above, if the freed range is exactly at the beginning of the content area, we are not creating a freeblock; // instead we are just extending the unallocated region. if offset == cur_content_area { if unlikely(prev_block.is_some_and(|prev| prev != first_block)) { return_corrupt!("free_cell_range: invalid content area merge - freed range should have been merged with previous freeblock: prev={prev_block:?} first_block={first_block}"); } // If we get here, we are freeing data from the left end of the content area, // so we are extending the unallocated region instead of creating a freeblock. // We update the first freeblock to be the next one, and shrink the content area to start from the end // of the freed range. match next_block { Some(next) => { if unlikely(next <= end) { return_corrupt!("free_cell_range: invalid content area merge - first freeblock should either be 0 or greater than the content area start: next_block={next} end={end}"); } let next_u16: u16 = next .try_into() .unwrap_or_else(|_| panic!("next={next} is too large to fit in a u16")); page.write_first_freeblock(next_u16); } None => { page.write_first_freeblock(0); } } page.write_cell_content_area(end); } else { // If we are creating a new freeblock: // a) if it's the first one, we update the header to indicate so, // b) if it's not the first one, we update the previous freeblock to point to the new one, // and the new one to point to the next one. let offset_u16: u16 = offset .try_into() .unwrap_or_else(|_| panic!("offset={offset} is too large to fit in a u16")); if let Some(prev) = prev_block { page.write_u16_no_offset(prev, offset_u16); } else { page.write_first_freeblock(offset_u16); } let size_u16: u16 = size .try_into() .unwrap_or_else(|_| panic!("size={size} is too large to fit in a u16")); let next_block_u16 = next_block.map(|b| { b.try_into() .unwrap_or_else(|_| panic!("next_block={b} is too large to fit in a u16")) }); page.write_freeblock(offset_u16, size_u16, next_block_u16); } Ok(()) } /// This function handles pages with two or fewer freeblocks and max_frag_bytes (parameter to defragment_page()) /// or fewer fragmented bytes. In this case it is faster to move the two (or one) /// blocks of cells using memmove() and add the required offsets to each pointer /// in the cell-pointer array than it is to reconstruct the entire page. /// Note that this function will leave max_frag_bytes as is, it will not try to reduce it. fn defragment_page_fast( page: &PageContent, usable_space: usize, freeblock_1st: usize, freeblock_2nd: usize, ) -> Result<()> { if unlikely(freeblock_1st == 0) { return_corrupt!("defragment_page_fast: expected at least one freeblock"); } if unlikely(freeblock_2nd > 0 && freeblock_1st >= freeblock_2nd) { return_corrupt!( "defragment_page_fast: first freeblock must be before second freeblock: freeblock_1st={freeblock_1st} freeblock_2nd={freeblock_2nd}" ); } const FREEBLOCK_SIZE_MIN: usize = 4; if unlikely(freeblock_1st > usable_space - FREEBLOCK_SIZE_MIN) { return_corrupt!( "defragment_page_fast: first freeblock beyond usable space: freeblock_1st={freeblock_1st} usable_space={usable_space}" ); } if unlikely(freeblock_2nd > usable_space - FREEBLOCK_SIZE_MIN) { return_corrupt!( "defragment_page_fast: second freeblock beyond usable space: freeblock_2nd={freeblock_2nd} usable_space={usable_space}" ); } let freeblock_1st_size = page.read_u16_no_offset(freeblock_1st + 2) as usize; let freeblock_2nd_size = if freeblock_2nd > 0 { page.read_u16_no_offset(freeblock_2nd + 2) as usize } else { 0 }; let freeblocks_total_size = freeblock_1st_size + freeblock_2nd_size; let cell_content_area = page.cell_content_area() as usize; if freeblock_2nd > 0 { // If there's 2 freeblocks, merge them into one first. if unlikely(freeblock_1st + freeblock_1st_size > freeblock_2nd) { return_corrupt!( "defragment_page_fast: overlapping freeblocks: freeblock_1st={freeblock_1st} freeblock_1st_size={freeblock_1st_size} freeblock_2nd={freeblock_2nd}" ); } if unlikely(freeblock_2nd + freeblock_2nd_size > usable_space) { return_corrupt!( "defragment_page_fast: second freeblock extends beyond usable space: freeblock_2nd={freeblock_2nd} freeblock_2nd_size={freeblock_2nd_size} usable_space={usable_space}" ); } let buf = page.as_ptr(); // Effectively moves everything in between the two freeblocks rightwards by the length of the 2nd freeblock, // so that the first freeblock size becomes `freeblocks_total_size` (merging the two freeblocks) // and the second freeblock gets overwritten by non-free cell data. // Illustrative doodle: // | content area start |--cell content A--| 1st free |--cell content B--| 2nd free |--cell content C--| // -> // | content area start |--cell content A--| merged free |--cell content B--|--cell content C--| let after_first_freeblock = freeblock_1st + freeblock_1st_size; let copy_amount = freeblock_2nd - after_first_freeblock; buf.copy_within( after_first_freeblock..after_first_freeblock + copy_amount, freeblock_1st + freeblocks_total_size, ); } else if unlikely(freeblock_1st + freeblock_1st_size > usable_space) { return_corrupt!( "defragment_page_fast: first freeblock extends beyond usable space: freeblock_1st={freeblock_1st} freeblock_1st_size={freeblock_1st_size} usable_space={usable_space}" ); } // Now we have one freeblock somewhere in the middle of the content area, e.g.: // content area start |-----------| merged freeblock |-----------| // By moving the cells from the left of the merged free block to where the merged freeblock was, we effectively move the freeblock to the very left end of the content area, // meaning, it's no longer a freeblock, it's just plain old free space. // content area start | free space | ----------- cells ----------| let new_cell_content_area = cell_content_area + freeblocks_total_size; if unlikely(new_cell_content_area + (freeblock_1st - cell_content_area) > usable_space) { return_corrupt!( "defragment_page_fast: new cell content area extends beyond usable space: new_cell_content_area={new_cell_content_area} freeblock_1st={freeblock_1st} cell_content_area={cell_content_area} usable_space={usable_space}" ); } let copy_amount = freeblock_1st - cell_content_area; // cells to the left of the first freeblock let buf = page.as_ptr(); buf.copy_within( cell_content_area..cell_content_area + copy_amount, new_cell_content_area, ); // Freeblocks are now erased since the free space is at the beginning, but we must update the cell pointer array to point to the right locations. let cell_count = page.cell_count(); let cell_pointer_array_offset = page.cell_pointer_array_offset_and_size().0; for i in 0..cell_count { let ptr_offset = cell_pointer_array_offset + (i * CELL_PTR_SIZE_BYTES); let cell_ptr = page.read_u16_no_offset(ptr_offset) as usize; if cell_ptr < freeblock_1st { // If the cell pointer was located before the first freeblock, we need to shift it right by the size of the merged freeblock // since the space occupied by both the 1st and 2nd freeblocks was now moved to its left. let new_offset = cell_ptr + freeblocks_total_size; if unlikely(new_offset > usable_space) { return_corrupt!( "defragment_page_fast: shifted cell pointer beyond usable space: new_offset={new_offset} usable_space={usable_space}" ); } page.write_u16_no_offset(ptr_offset, (cell_ptr + freeblocks_total_size) as u16); } else if freeblock_2nd > 0 && cell_ptr < freeblock_2nd { // If the cell pointer was located between the first and second freeblock, we need to shift it right by the size of only the second freeblock, // since the first one was already on its left. let new_offset = cell_ptr + freeblock_2nd_size; if unlikely(new_offset > usable_space) { return_corrupt!( "defragment_page_fast: shifted cell pointer beyond usable space: new_offset={new_offset} usable_space={usable_space}" ); } page.write_u16_no_offset(ptr_offset, (cell_ptr + freeblock_2nd_size) as u16); } } // Update page header page.write_cell_content_area(new_cell_content_area); page.write_first_freeblock(0); debug_validate_cells!(page, usable_space); Ok(()) } /// Defragment a page, and never use the fast-path algorithm. fn defragment_page_full(page: &PageContent, usable_space: usize) -> Result<()> { defragment_page(page, usable_space, -1) } /// Defragment a page and return a proof that can be used with [`page_insert_array`]. /// /// This is the entry point for defragmentation when you need to perform insert /// operations afterward. The returned [`DefragmentedPage`] proves at compile-time /// that defragmentation has occurred. /// /// For defragmentation without the type-state proof (e.g., in `allocate_cell_space`), /// use [`defragment_page`] directly. #[inline] fn defragment_page_for_insert( page: &mut PageContent, usable_space: usize, max_frag_bytes: isize, ) -> Result> { defragment_page(page, usable_space, max_frag_bytes)?; Ok(DefragmentedPage(page)) } /// Defragment a page. This means packing all the cells to the end of the page. fn defragment_page(page: &PageContent, usable_space: usize, max_frag_bytes: isize) -> Result<()> { debug_validate_cells!(page, usable_space); tracing::debug!("defragment_page (optimized in-place)"); let cell_count = page.cell_count(); if cell_count == 0 { page.write_cell_content_area(usable_space); page.write_first_freeblock(0); page.write_fragmented_bytes_count(0); debug_validate_cells!(page, usable_space); return Ok(()); } // Use fast algorithm if there are at most 2 freeblocks and the total fragmented free space is less than max_frag_bytes. if page.num_frag_free_bytes() as isize <= max_frag_bytes { let freeblock_1st = page.first_freeblock() as usize; if freeblock_1st == 0 { // No freeblocks and very little if any fragmented free bytes -> no need to defragment. return Ok(()); } let freeblock_2nd = page.read_u16_no_offset(freeblock_1st) as usize; if freeblock_2nd == 0 { return defragment_page_fast(page, usable_space, freeblock_1st, 0); } let freeblock_3rd = page.read_u16_no_offset(freeblock_2nd) as usize; if freeblock_3rd == 0 { return defragment_page_fast(page, usable_space, freeblock_1st, freeblock_2nd); } } // A small struct to hold cell metadata for sorting. // Size: 2 + 2 + 8 = 12 bytes, with alignment likely 16 bytes. #[derive(Clone, Copy)] struct CellInfo { old_offset: u16, size: u16, pointer_index: usize, } // Use stack allocation for the common case (most pages have <256 cells). // This avoids heap allocation in the hot path. // MAX_STACK_CELLS * 16 bytes = 4KB of stack space. const MAX_STACK_CELLS: usize = 256; // Helper function to process cells and defragment the page. // This is generic over the slice type to work with both stack and heap storage. #[inline] fn process_cells( page: &PageContent, usable_space: usize, cells: &mut [CellInfo], is_physically_sorted: bool, ) -> Result<()> { if !is_physically_sorted { // Sort cells by old physical offset in descending order. // Using unstable sort is fine as the original order doesn't matter. cells.sort_unstable_by(|a, b| b.old_offset.cmp(&a.old_offset)); } // Get direct mutable access to the page buffer. let buffer = page.as_ptr(); let cell_pointer_area_offset = page.cell_pointer_array_offset(); let first_cell_content_byte = page.unallocated_region_start(); // Move data and update pointers. let mut cbrk = usable_space; for cell in cells.iter() { cbrk -= cell.size as usize; let new_offset = cbrk; let old_offset = cell.old_offset as usize; // Basic corruption check turso_assert!( new_offset >= first_cell_content_byte && old_offset + cell.size as usize <= usable_space, "corrupt page detected during defragmentation", { "new_offset": new_offset, "first_cell_content_byte": first_cell_content_byte, "old_offset": old_offset, "cell_size": cell.size, "usable_space": usable_space } ); // Move the cell data. `copy_within` is the idiomatic and safe // way to perform a `memmove` operation on a slice. if new_offset != old_offset { let src_range = old_offset..(old_offset + cell.size as usize); buffer.copy_within(src_range, new_offset); } // Update the pointer in the cell pointer array to the new offset. let pointer_location = cell_pointer_area_offset + (cell.pointer_index * 2); turso_assert!( new_offset < PageSize::MAX as usize, "new_offset exceeds PageSize::MAX", { "new_offset": new_offset, "page_size_max": PageSize::MAX } ); page.write_u16_no_offset(pointer_location, new_offset as u16); } page.write_cell_content_area(cbrk); page.write_first_freeblock(0); page.write_fragmented_bytes_count(0); Ok(()) } // Gather cell metadata. let cell_offset = page.cell_pointer_array_offset(); let mut is_physically_sorted = true; let mut last_offset = u16::MAX; // Pre-compute page-level constants for cell_get_raw_region_faster. // These are the same for all cells on the page, so computing them once // avoids redundant work in the loop. let page_type = page.page_type()?; let max_local = payload_overflow_threshold_max(page_type, usable_space); let min_local = payload_overflow_threshold_min(page_type, usable_space); let mut cells = SmallVec::<[CellInfo; MAX_STACK_CELLS]>::with_capacity(cell_count); for i in 0..cell_count { let pc = page.read_u16_no_offset(cell_offset + (i * 2)); let (_, size) = page._cell_get_raw_region_faster( i, usable_space, cell_count, max_local, min_local, page_type, )?; if pc > last_offset { is_physically_sorted = false; } last_offset = pc; cells.push(CellInfo { old_offset: pc, size: size as u16, pointer_index: i, }); } process_cells(page, usable_space, &mut cells, is_physically_sorted)?; debug_validate_cells!(page, usable_space); Ok(()) } #[cfg(debug_assertions)] /// Only enabled in debug mode, where we ensure that all cells are valid. fn debug_validate_cells_core(page: &PageContent, usable_space: usize) { for i in 0..page.cell_count() { let (offset, size) = page.cell_get_raw_region(i, usable_space).unwrap(); let _buf = &page.as_ptr()[offset..offset + size]; // E.g. the following table btree cell may just have two bytes: // Payload size 0 (stored as SerialTypeKind::ConstInt0) // Rowid 1 (stored as SerialTypeKind::ConstInt1) turso_assert_greater_than_or_equal!( size, 2, "cell size should be at least 2 bytes", { "idx": i, "offset": offset, "buf": _buf } ); if page.is_leaf() { turso_assert!(page.as_ptr()[offset] != 0); } turso_assert_less_than_or_equal!( offset + size, usable_space, "cell spans out of usable space" ); } } /// Insert a record into a cell. /// If the cell overflows, an overflow cell is created. /// insert_into_cell() is called from insert_into_page(), /// and the overflow cell count is used to determine if the page overflows, /// i.e. whether we need to balance the btree after the insert. fn _insert_into_cell( page: &mut PageContent, payload: &[u8], cell_idx: usize, usable_space: usize, allow_regular_insert_despite_overflow: bool, // used during balancing to allow regular insert despite overflow cells ) -> Result<()> { turso_assert_less_than_or_equal!( cell_idx, page.cell_count() + page.overflow_cells.len(), "attempting to add cell to incorrect place", { "cell_idx": cell_idx, "cell_count": page.cell_count(), "overflow_count": page.overflow_cells.len(), "page_type": format!("{:?}", page.page_type()) } ); let already_has_overflow = !page.overflow_cells.is_empty(); let free = compute_free_space(page, usable_space)?; let enough_space = if already_has_overflow && !allow_regular_insert_despite_overflow { false } else { // otherwise, we need to check if we have enough space payload.len() + CELL_PTR_SIZE_BYTES <= free }; if !enough_space { #[cfg(debug_assertions)] { if let Some(overflow_cell) = page.overflow_cells.last() { turso_assert!(overflow_cell.index + 1 == cell_idx, "multiple overflow cells can only occur when a parent overflows during balancing as divider cells are inserted into it. those cells should always be in-order and sequential"); } } page.overflow_cells.push(OverflowCell { index: cell_idx, payload: Pin::new(Vec::from(payload)), }); return Ok(()); } turso_assert_less_than_or_equal!( cell_idx, page.cell_count(), "cell_idx > cell_count without overflow cells" ); let new_cell_data_pointer = allocate_cell_space(page, payload.len(), usable_space, free)?; tracing::debug!( "insert_into_cell(idx={}, pc={}, size={})", cell_idx, new_cell_data_pointer, payload.len() ); turso_assert_less_than_or_equal!(new_cell_data_pointer as usize + payload.len(), usable_space); let buf = page.as_ptr(); // copy data buf[new_cell_data_pointer as usize..new_cell_data_pointer as usize + payload.len()] .copy_from_slice(payload); // memmove(pIns+2, pIns, 2*(pPage->nCell - i)); let (cell_pointer_array_start, _) = page.cell_pointer_array_offset_and_size(); let cell_pointer_cur_idx = cell_pointer_array_start + (CELL_PTR_SIZE_BYTES * cell_idx); // move existing pointers forward by CELL_PTR_SIZE_BYTES... let n_cells_forward = page.cell_count() - cell_idx; let n_bytes_forward = CELL_PTR_SIZE_BYTES * n_cells_forward; if n_bytes_forward > 0 { buf.copy_within( cell_pointer_cur_idx..cell_pointer_cur_idx + n_bytes_forward, cell_pointer_cur_idx + CELL_PTR_SIZE_BYTES, ); } // ...and insert new cell pointer at the current index page.write_u16_no_offset(cell_pointer_cur_idx, new_cell_data_pointer); // update cell count let new_n_cells = (page.cell_count() + 1) as u16; page.write_cell_count(new_n_cells); debug_validate_cells!(page, usable_space); Ok(()) } fn insert_into_cell( page: &mut PageContent, payload: &[u8], cell_idx: usize, usable_space: usize, ) -> Result<()> { _insert_into_cell(page, payload, cell_idx, usable_space, false) } /// Normally in [insert_into_cell()], if a page already has overflow cells, all /// new insertions are also added to the overflow cells vector. /// The amount of free space is the sum of: /// #1. The size of the unallocated region /// #2. Fragments (isolated 1-3 byte chunks of free space within the cell content area) /// #3. freeblocks (linked list of blocks of at least 4 bytes within the cell content area that /// are not in use due to e.g. deletions) /// Free blocks can be zero, meaning the "real free space" that can be used to allocate is expected /// to be between first cell byte and end of cell pointer area. #[allow(unused_assignments)] #[inline] fn compute_free_space(page: &PageContent, usable_space: usize) -> Result { // TODO(pere): maybe free space is not calculated correctly with offset // Usable space, not the same as free space, simply means: // space that is not reserved for extensions by sqlite. Usually reserved_space is 0. let first_cell = page.offset() + page.header_size() + (2 * page.cell_count()); if unlikely(first_cell > usable_space) { return_corrupt!( "compute_free_space: first_cell beyond usable space: first_cell={first_cell} usable_space={usable_space}" ); } let cell_content_area_start = page.cell_content_area() as usize; if unlikely(cell_content_area_start > usable_space) { return_corrupt!( "compute_free_space: cell content area beyond usable space: cell_content_area_start={cell_content_area_start} usable_space={usable_space}" ); } let mut free_space_bytes = cell_content_area_start + page.num_frag_free_bytes() as usize; // #3 is computed by iterating over the freeblocks linked list let mut cur_freeblock_ptr = page.first_freeblock() as usize; if cur_freeblock_ptr > 0 { if unlikely(cur_freeblock_ptr < cell_content_area_start) { return_corrupt!( "compute_free_space: first freeblock before content area: first_freeblock={cur_freeblock_ptr} cell_content_area_start={cell_content_area_start}" ); } let mut next = 0usize; let mut size = 0usize; loop { if unlikely(cur_freeblock_ptr + 4 > usable_space) { return_corrupt!( "compute_free_space: freeblock header out of bounds: cur_freeblock_ptr={cur_freeblock_ptr} usable_space={usable_space}" ); } next = page.read_u16_no_offset(cur_freeblock_ptr) as usize; // first 2 bytes in freeblock = next freeblock pointer size = page.read_u16_no_offset(cur_freeblock_ptr + 2) as usize; // next 2 bytes in freeblock = size of current freeblock if unlikely(size < 4) { return_corrupt!( "compute_free_space: freeblock too small: cur_freeblock_ptr={cur_freeblock_ptr} size={size}" ); } if unlikely(cur_freeblock_ptr + size > usable_space) { return_corrupt!( "compute_free_space: freeblock extends beyond page: cur_freeblock_ptr={cur_freeblock_ptr} size={size} usable_space={usable_space}" ); } free_space_bytes += size; if next == 0 { break; } // Freeblocks are in order from left to right on the page. if unlikely(next <= cur_freeblock_ptr + size + 3) { return_corrupt!( "compute_free_space: freeblocks list not in ascending order: cur_freeblock_ptr={cur_freeblock_ptr} size={size} next={next}" ); } cur_freeblock_ptr = next; } } if unlikely(free_space_bytes > usable_space) { return_corrupt!( "compute_free_space: free space greater than usable space: free_space_bytes={free_space_bytes} usable_space={usable_space}" ); } if unlikely(free_space_bytes < first_cell) { return_corrupt!( "compute_free_space: free space underflow: free_space_bytes={free_space_bytes} first_cell={first_cell} usable_space={usable_space}" ); } Ok(free_space_bytes - first_cell) } /// Allocate space for a cell on a page. #[inline] fn allocate_cell_space( page_ref: &PageContent, mut amount: usize, usable_space: usize, free_space: usize, ) -> Result { if amount < MINIMUM_CELL_SIZE { amount = MINIMUM_CELL_SIZE; } let unallocated_region_start = page_ref.unallocated_region_start(); let mut cell_content_area_start = page_ref.cell_content_area() as usize; // there are free blocks and enough space to fit a new 2-byte cell pointer if page_ref.first_freeblock() != 0 && unallocated_region_start + CELL_PTR_SIZE_BYTES <= cell_content_area_start { // find slot if let Some(pc) = find_free_slot(page_ref, usable_space, amount)? { // we can fit the cell in a freeblock. return Ok(pc as u16); } /* fall through, we might need to defragment */ } // We know at this point that we have no freeblocks in the middle of the cell content area // that can fit the cell, but we do know we have enough space to _somehow_ fit it. // The check below sees whether we can just put the cell in the unallocated region. if unallocated_region_start + CELL_PTR_SIZE_BYTES + amount > cell_content_area_start { // There's no room in the unallocated region, so we need to defragment. // max_frag_bytes is a parameter to defragment_page() that controls whether we are able to use // the fast-path defragmentation. The calculation here is done to see whether we can merge 1-2 freeblocks // and move them to the unallocated region and fit the cell that way. // Basically: if we have exactly enough space for the cell and the cell pointer on the page, // we cannot have any fragmented space because then the freeblocks would not fit the cell. let max_frag_bytes = 4.min(free_space as isize - (CELL_PTR_SIZE_BYTES + amount) as isize); defragment_page(page_ref, usable_space, max_frag_bytes)?; cell_content_area_start = page_ref.cell_content_area() as usize; } // insert the cell -> content area start moves left by that amount. cell_content_area_start -= amount; page_ref.write_cell_content_area(cell_content_area_start); turso_assert_less_than_or_equal!(cell_content_area_start + amount, usable_space); // we can just return the start of the cell content area, since the cell is inserted to the very left of the cell content area. Ok(cell_content_area_start as u16) } #[derive(Debug, Clone)] pub enum FillCellPayloadState { /// Determine whether we can fit the record on the current page. /// If yes, return immediately after copying the data. /// Otherwise move to [CopyData] state. Start, /// Copy the next chunk of data from the record buffer to the cell payload. /// If we can't fit all of the remaining data on the current page, /// move the internal state to [CopyDataState::AllocateOverflowPage] CopyData { /// Internal state of the copy data operation. /// We can either be copying data or allocating an overflow page. state: CopyDataState, /// Track how much space we have left on the current page we are copying data into. /// This is reset whenever a new overflow page is allocated. space_left_on_cur_page: usize, /// Offset into the record buffer to copy from. src_data_offset: usize, /// Offset into the destination buffer we are copying data into. /// This is either: /// - an offset in the btree page where the cell is, or /// - an offset in an overflow page dst_data_offset: usize, /// If this is Some, we will copy data into this overflow page. /// If this is None, we will copy data into the cell payload on the btree page. /// Also: to safely form a chain of overflow pages, the current page must be pinned to the page cache /// so that e.g. a spilling operation does not evict it to disk. current_overflow_page: Option, }, } #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub enum CopyDataState { /// Copy the next chunk of data from the record buffer to the cell payload. Copy, /// Allocate a new overflow page if we couldn't fit all data to the current page. AllocateOverflowPage, } /// Fill in the cell payload with the record. /// If the record is too large to fit in the cell, it will spill onto overflow pages. /// This function needs a separate [FillCellPayloadState] because allocating overflow pages /// may require I/O. #[allow(clippy::too_many_arguments)] fn fill_cell_payload( page: &PinGuard, int_key: Option, cell_payload: &mut Vec, cell_idx: usize, record: &ImmutableRecord, usable_space: usize, pager: Arc, fill_cell_payload_state: &mut FillCellPayloadState, ) -> Result> { let overflow_page_pointer_size = 4; let overflow_page_data_size = usable_space - overflow_page_pointer_size; let result = loop { let record_buf = record.get_payload(); match fill_cell_payload_state { FillCellPayloadState::Start => { let page_contents = page.get_contents(); let page_type = page_contents.page_type()?; // fill in header if matches!(page_type, PageType::IndexInterior) { // if a write happened on an index interior page, it is always an overwrite. // we must copy the left child pointer of the replaced cell to the new cell. let left_child_page = page_contents.cell_interior_read_left_child_page(cell_idx)?; cell_payload.extend_from_slice(&left_child_page.to_be_bytes()); } if matches!(page_type, PageType::TableLeaf) { let int_key = int_key.unwrap(); write_varint_to_vec(record_buf.len() as u64, cell_payload); write_varint_to_vec(int_key as u64, cell_payload); } else { write_varint_to_vec(record_buf.len() as u64, cell_payload); } let max_local = payload_overflow_threshold_max(page_type, usable_space); let min_local = payload_overflow_threshold_min(page_type, usable_space); let (overflows, local_size_if_overflow) = payload_overflows(record_buf.len(), max_local, min_local, usable_space); if !overflows { // enough allowed space to fit inside a btree page cell_payload.extend_from_slice(record_buf.as_ref()); break Ok(IOResult::Done(())); } // so far we've written any of: left child page, rowid, payload size (depending on page type) let cell_non_payload_elems_size = cell_payload.len(); let new_total_local_size = cell_non_payload_elems_size + local_size_if_overflow; cell_payload.resize(new_total_local_size, 0); *fill_cell_payload_state = FillCellPayloadState::CopyData { state: CopyDataState::Copy, space_left_on_cur_page: local_size_if_overflow - overflow_page_pointer_size, // local_size_if_overflow includes the overflow page pointer, but we don't want to write payload data there. src_data_offset: 0, dst_data_offset: cell_non_payload_elems_size, current_overflow_page: None, }; continue; } FillCellPayloadState::CopyData { state, src_data_offset, space_left_on_cur_page, dst_data_offset, current_overflow_page, } => { match state { CopyDataState::Copy => { turso_assert!(*src_data_offset < record_buf.len(), "trying to read past end of record buffer", { "src_data_offset": src_data_offset, "record_buf_len": record_buf.len() }); let record_offset_slice = &record_buf[*src_data_offset..]; let amount_to_copy = (*space_left_on_cur_page).min(record_offset_slice.len()); let record_offset_slice_to_copy = &record_offset_slice[..amount_to_copy]; if let Some(cur_page) = current_overflow_page { // Copy data into the current overflow page. turso_assert!( cur_page.is_loaded(), "current overflow page is not loaded" ); turso_assert!(*dst_data_offset == overflow_page_pointer_size, "data must be copied to overflow page pointer offset on overflow pages", { "dst_data_offset": dst_data_offset, "overflow_page_pointer_size": overflow_page_pointer_size }); let contents = cur_page.get_contents(); let buf = &mut contents.as_ptr() [*dst_data_offset..*dst_data_offset + amount_to_copy]; buf.copy_from_slice(record_offset_slice_to_copy); } else { // Copy data into the cell payload on the btree page. let buf = &mut cell_payload [*dst_data_offset..*dst_data_offset + amount_to_copy]; buf.copy_from_slice(record_offset_slice_to_copy); } if record_offset_slice.len() - amount_to_copy == 0 { break Ok(IOResult::Done(())); } *state = CopyDataState::AllocateOverflowPage; *src_data_offset += amount_to_copy; } CopyDataState::AllocateOverflowPage => { let new_overflow_page = match pager.allocate_overflow_page() { Ok(IOResult::Done(new_overflow_page)) => { PinGuard::new(new_overflow_page) } Ok(IOResult::IO(io_result)) => return Ok(IOResult::IO(io_result)), Err(e) => { mark_unlikely(); break Err(e); } }; turso_assert!( new_overflow_page.is_loaded(), "new overflow page is not loaded" ); let new_overflow_page_id = new_overflow_page.get().id as u32; if let Some(prev_page) = current_overflow_page { // Update the previous overflow page's "next overflow page" pointer to point to the new overflow page. turso_assert!( prev_page.is_loaded(), "previous overflow page is not loaded" ); let contents = prev_page.get_contents(); let buf = &mut contents.as_ptr()[..overflow_page_pointer_size]; buf.copy_from_slice(&new_overflow_page_id.to_be_bytes()); } else { // Update the cell payload's "next overflow page" pointer to point to the new overflow page. let first_overflow_page_ptr_offset = cell_payload.len() - overflow_page_pointer_size; let buf = &mut cell_payload[first_overflow_page_ptr_offset ..first_overflow_page_ptr_offset + overflow_page_pointer_size]; buf.copy_from_slice(&new_overflow_page_id.to_be_bytes()); } *dst_data_offset = overflow_page_pointer_size; *space_left_on_cur_page = overflow_page_data_size; *current_overflow_page = Some(new_overflow_page.clone()); *state = CopyDataState::Copy; } } } } }; result } /// Returns the maximum payload size (X) that can be stored directly on a b-tree page without spilling to overflow pages. /// /// For table leaf pages: X = usable_size - 35 /// For index pages: X = ((usable_size - 12) * 64/255) - 23 /// /// The usable size is the total page size less the reserved space at the end of each page. /// These thresholds are designed to: /// - Give a minimum fanout of 4 for index b-trees /// - Ensure enough payload is on the b-tree page that the record header can usually be accessed /// without consulting an overflow page #[inline] pub fn payload_overflow_threshold_max(page_type: PageType, usable_space: usize) -> usize { match page_type { PageType::IndexInterior | PageType::IndexLeaf => { ((usable_space - 12) * 64 / 255) - 23 // Index page formula } PageType::TableInterior | PageType::TableLeaf => { usable_space - 35 // Table leaf page formula } } } /// Returns the minimum payload size (M) that must be stored on the b-tree page before spilling to overflow pages is allowed. /// /// For all page types: M = ((usable_size - 12) * 32/255) - 23 /// /// When payload size P exceeds max_local(): /// - If K = M + ((P-M) % (usable_size-4)) <= max_local(): store K bytes on page /// - Otherwise: store M bytes on page /// /// The remaining bytes are stored on overflow pages in both cases. #[inline] pub fn payload_overflow_threshold_min(_page_type: PageType, usable_space: usize) -> usize { // Same formula for all page types ((usable_space - 12) * 32 / 255) - 23 } /// Drop a cell from a page. /// This is done by freeing the range of bytes that the cell occupies. #[inline] fn drop_cell(page: &mut PageContent, cell_idx: usize, usable_space: usize) -> Result<()> { let (cell_start, cell_len) = page.cell_get_raw_region(cell_idx, usable_space)?; free_cell_range(page, cell_start, cell_len, usable_space)?; if page.cell_count() > 1 { shift_pointers_left(page, cell_idx); } else { page.write_cell_content_area(usable_space); page.write_first_freeblock(0); page.write_fragmented_bytes_count(0); } page.write_cell_count(page.cell_count() as u16 - 1); debug_validate_cells!(page, usable_space); Ok(()) } /// Shift pointers to the left once starting from a cell position /// This is useful when we remove a cell and we want to move left the cells from the right to fill /// the empty space that's not needed #[inline] fn shift_pointers_left(page: &mut PageContent, cell_idx: usize) { turso_assert_greater_than!(page.cell_count(), 0); let buf = page.as_ptr(); let (start, _) = page.cell_pointer_array_offset_and_size(); let start = start + (cell_idx * 2) + 2; let right_cells = page.cell_count() - cell_idx - 1; let amount_to_shift = right_cells * 2; buf.copy_within(start..start + amount_to_shift, start - 2); } #[cfg(test)] mod tests { use rand::{rng, Rng}; use rand_chacha::{ rand_core::{RngCore, SeedableRng}, ChaCha8Rng, }; use sorted_vec::SortedVec; use test_log::test; use turso_parser::ast::SortOrder; use super::*; use crate::{ io::{Buffer, MemoryIO, OpenFlags, IO}, schema::IndexColumn, storage::{ database::DatabaseFile, page_cache::PageCache, pager::default_page1, sqlite3_ondisk::PageSize, }, types::Text, vdbe::Register, BufferPool, Completion, Connection, IOContext, StepResult, Wal, WalFile, WalFileShared, }; use arc_swap::ArcSwapOption; use std::{mem::transmute, ops::Deref, sync::Arc}; use tempfile::TempDir; use crate::{ storage::{ btree::{compute_free_space, fill_cell_payload, payload_overflow_threshold_max}, sqlite3_ondisk::{BTreeCell, PageContent, PageType}, }, types::Value, Database, Page, Pager, PlatformIO, }; use super::{btree_init_page, defragment_page, drop_cell, insert_into_cell}; #[allow(clippy::arc_with_non_send_sync)] fn get_page(id: usize) -> PageRef { let page = Arc::new(Page::new(id as i64)); { let inner = page.get(); inner.buffer = Some(Arc::new(Buffer::new_temporary(4096))); } page.set_loaded(); btree_init_page(&page, PageType::TableLeaf, 0, 4096); page } #[allow(clippy::arc_with_non_send_sync)] fn get_database() -> Arc { let mut path = TempDir::new().unwrap().keep(); path.push("test.db"); { let connection = rusqlite::Connection::open(&path).unwrap(); connection .pragma_update(None, "journal_mode", "wal") .unwrap(); } let io: Arc = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io.clone(), path.to_str().unwrap()).unwrap(); db } fn ensure_cell(page: &mut PageContent, cell_idx: usize, payload: &Vec) { let cell = page.cell_get_raw_region(cell_idx, 4096).unwrap(); tracing::trace!("cell idx={} start={} len={}", cell_idx, cell.0, cell.1); let buf = &page.as_ptr()[cell.0..cell.0 + cell.1]; assert_eq!(buf.len(), payload.len()); assert_eq!(buf, payload); } fn add_record( id: usize, pos: usize, page: PageRef, record: ImmutableRecord, conn: &Arc, ) -> Vec { let mut payload: Vec = Vec::new(); let mut fill_cell_payload_state = FillCellPayloadState::Start; run_until_done( || { fill_cell_payload( &PinGuard::new(page.clone()), Some(id as i64), &mut payload, pos, &record, 4096, conn.pager.load().clone(), &mut fill_cell_payload_state, ) }, &conn.pager.load().clone(), ) .unwrap(); insert_into_cell(page.get_contents(), &payload, pos, 4096).unwrap(); payload } fn insert_record( cursor: &mut BTreeCursor, pager: &Arc, rowid: i64, val: Value, ) -> Result<(), LimboError> { let regs = &[Register::Value(val)]; let record = ImmutableRecord::from_registers(regs, regs.len()); run_until_done( || { let key = SeekKey::TableRowId(rowid); cursor.seek(key, SeekOp::GE { eq_only: true }) }, pager.deref(), )?; run_until_done( || cursor.insert(&BTreeKey::new_table_rowid(rowid, Some(&record))), pager.deref(), )?; Ok(()) } fn assert_btree_empty(cursor: &mut BTreeCursor, pager: &Pager) -> Result<()> { let _c = cursor.move_to_root()?; run_until_done(|| cursor.next(), pager)?; let empty = !cursor.has_record; assert!(empty, "expected B-tree to be empty"); Ok(()) } #[test] fn test_insert_cell() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let header_size = 8; let regs = &[Register::Value(Value::from_i64(1))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let payload = add_record(1, 0, page.clone(), record, &conn); let page_contents = page.get_contents(); assert_eq!(page_contents.cell_count(), 1); let free = compute_free_space(page_contents, 4096).unwrap(); assert_eq!(free, 4096 - payload.len() - 2 - header_size); let cell_idx = 0; ensure_cell(page_contents, cell_idx, &payload); } struct Cell { pos: usize, payload: Vec, } #[test] fn test_drop_1() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let header_size = 8; let mut total_size = 0; let mut cells = Vec::new(); let usable_space = 4096; for i in 0..3 { let regs = &[Register::Value(Value::from_i64(i as i64))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let payload = add_record(i, i, page.clone(), record, &conn); assert_eq!(page_contents.cell_count(), i + 1); let free = compute_free_space(page_contents, usable_space).unwrap(); total_size += payload.len() + 2; assert_eq!(free, 4096 - total_size - header_size); cells.push(Cell { pos: i, payload }); } for (i, cell) in cells.iter().enumerate() { ensure_cell(page_contents, i, &cell.payload); } cells.remove(1); drop_cell(page_contents, 1, usable_space).unwrap(); for (i, cell) in cells.iter().enumerate() { ensure_cell(page_contents, i, &cell.payload); } } fn validate_btree(pager: Arc, page_idx: i64) -> (usize, bool) { let num_columns = 5; let cursor = BTreeCursor::new_table(pager.clone(), page_idx, num_columns); let (page, _c) = cursor.read_page(page_idx).unwrap(); while page.is_locked() { pager.io.step().unwrap(); } // Pin page in order to not drop it in between page.set_dirty(); let contents = page.get_contents(); let mut previous_key = None; let mut valid = true; let mut depth = None; debug_validate_cells!(contents, pager.usable_space()); let mut child_pages = Vec::new(); for cell_idx in 0..contents.cell_count() { let cell = contents.cell_get(cell_idx, cursor.usable_space()).unwrap(); let current_depth = match cell { BTreeCell::TableLeafCell(..) => 1, BTreeCell::TableInteriorCell(TableInteriorCell { left_child_page, .. }) => { let (child_page, _c) = cursor.read_page(left_child_page as i64).unwrap(); while child_page.is_locked() { pager.io.step().unwrap(); } child_pages.push(child_page); if left_child_page == page.get().id as u32 { valid = false; tracing::error!( "left child page is the same as parent {}", left_child_page ); continue; } let (child_depth, child_valid) = validate_btree(pager.clone(), left_child_page as i64); valid &= child_valid; child_depth } _ => panic!("unsupported btree cell: {cell:?}"), }; if current_depth >= 100 { tracing::error!("depth is too big"); page.clear_dirty(); return (100, false); } depth = Some(depth.unwrap_or(current_depth + 1)); if depth != Some(current_depth + 1) { tracing::error!("depth is different for child of page {}", page_idx); valid = false; } match cell { BTreeCell::TableInteriorCell(TableInteriorCell { rowid, .. }) | BTreeCell::TableLeafCell(TableLeafCell { rowid, .. }) => { if previous_key.is_some() && previous_key.unwrap() >= rowid { tracing::error!( "keys are in bad order: prev={:?}, current={}", previous_key, rowid ); valid = false; } previous_key = Some(rowid); } _ => panic!("unsupported btree cell: {cell:?}"), } } if let Some(right) = contents.rightmost_pointer().ok().flatten() { let (right_depth, right_valid) = validate_btree(pager.clone(), right as i64); valid &= right_valid; depth = Some(depth.unwrap_or(right_depth + 1)); if depth != Some(right_depth + 1) { tracing::error!("depth is different for child of page {}", page_idx); valid = false; } } let first_page_type = child_pages.first_mut().map(|p| { if !p.is_loaded() { let (new_page, _c) = pager.read_page(p.get().id as i64).unwrap(); *p = new_page; } while p.is_locked() { pager.io.step().unwrap(); } p.get_contents().page_type().ok() }); if let Some(child_type) = first_page_type { for page in child_pages.iter_mut().skip(1) { if !page.is_loaded() { let (new_page, _c) = pager.read_page(page.get().id as i64).unwrap(); *page = new_page; } while page.is_locked() { pager.io.step().unwrap(); } if page.get_contents().page_type().ok() != child_type { tracing::error!("child pages have different types"); valid = false; } } } if contents.rightmost_pointer().ok().flatten().is_none() && contents.cell_count() == 0 { valid = false; } page.clear_dirty(); (depth.unwrap(), valid) } fn format_btree(pager: Arc, page_idx: i64, depth: usize) -> String { let num_columns = 5; let cursor = BTreeCursor::new_table(pager.clone(), page_idx, num_columns); let (page, _c) = cursor.read_page(page_idx).unwrap(); while page.is_locked() { pager.io.step().unwrap(); } // Pin page in order to not drop it in between loading of different pages. If not contents will be a dangling reference. page.set_dirty(); let contents = page.get_contents(); let mut current = Vec::new(); let mut child = Vec::new(); for cell_idx in 0..contents.cell_count() { let cell = contents.cell_get(cell_idx, cursor.usable_space()).unwrap(); match cell { BTreeCell::TableInteriorCell(cell) => { current.push(format!( "node[rowid:{}, ptr(<=):{}]", cell.rowid, cell.left_child_page )); child.push(format_btree( pager.clone(), cell.left_child_page as i64, depth + 2, )); } BTreeCell::TableLeafCell(cell) => { current.push(format!( "leaf[rowid:{}, len(payload):{}, overflow:{}]", cell.rowid, cell.payload.len(), cell.first_overflow_page.is_some() )); } _ => panic!("unsupported btree cell: {cell:?}"), } } if let Some(rightmost) = contents.rightmost_pointer().ok().flatten() { child.push(format_btree(pager, rightmost as i64, depth + 2)); } let current = format!( "{}-page:{}, ptr(right):{:?}\n{}+cells:{}", " ".repeat(depth), page_idx, contents.rightmost_pointer().ok().flatten(), " ".repeat(depth), current.join(", ") ); page.clear_dirty(); if child.is_empty() { current } else { current + "\n" + &child.join("\n") } } fn empty_btree() -> (Arc, i64, Arc, Arc) { #[allow(clippy::arc_with_non_send_sync)] let io: Arc = Arc::new(MemoryIO::new()); let db = Database::open_file(io.clone(), ":memory:").unwrap(); let conn = db.connect().unwrap(); let pager = conn.pager.load().clone(); // FIXME: handle page cache is full // force allocate page1 with a transaction pager.begin_read_tx().unwrap(); run_until_done(|| pager.begin_write_tx(), &pager).unwrap(); run_until_done(|| pager.commit_tx(&conn, true), &pager).unwrap(); let page2 = run_until_done(|| pager.allocate_page(), &pager).unwrap(); btree_init_page(&page2, PageType::TableLeaf, 0, pager.usable_space()); (pager, page2.get().id as i64, db, conn) } #[test] fn btree_with_virtual_page_1() -> Result<()> { #[allow(clippy::arc_with_non_send_sync)] let io: Arc = Arc::new(MemoryIO::new()); let db = Database::open_file(io.clone(), ":memory:").unwrap(); let conn = db.connect().unwrap(); let pager = conn.pager.load().clone(); let mut cursor = BTreeCursor::new(pager, 1, 5); let result = cursor.rewind()?; assert!(matches!(result, IOResult::Done(_))); let result = cursor.next()?; assert!(matches!(result, IOResult::Done(_))); assert!(!cursor.has_record); let result = cursor.record()?; assert!(matches!(result, IOResult::Done(record) if record.is_none())); Ok(()) } #[test] pub fn btree_test_overflow_pages_are_cleared_on_overwrite() { // Create a database with a table let (pager, root_page, _, _) = empty_btree(); let num_columns = 5; // Get the maximum local payload size for table leaf pages let max_local = payload_overflow_threshold_max(PageType::TableLeaf, 4096); let usable_size = 4096; // Create a payload that is definitely larger than the maximum local size // This will force the creation of overflow pages let large_payload_size = max_local + usable_size * 2; let large_payload = vec![b'X'; large_payload_size]; // Create a record with the large payload let regs = &[Register::Value(Value::Blob(large_payload))]; let large_record = ImmutableRecord::from_registers(regs, regs.len()); // Create cursor for the table let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); let cursor = &mut cursor; let initial_pagecount = pager .io .block(|| pager.with_header(|header| header.database_size.get())) .unwrap(); assert_eq!( initial_pagecount, 2, "Page count should be 2 after initial insert, was {initial_pagecount}" ); // Insert the large record with rowid 1 run_until_done( || { let key = SeekKey::TableRowId(1); cursor.seek(key, SeekOp::GE { eq_only: true }) }, pager.deref(), ) .unwrap(); let key = BTreeKey::new_table_rowid(1, Some(&large_record)); run_until_done(|| cursor.insert(&key), pager.deref()).unwrap(); // Verify that overflow pages were created by checking freelist count // The freelist count should be 0 initially, and after inserting a large record, // some pages should be allocated for overflow, but they won't be in freelist yet let freelist_after_insert = pager .io .block(|| pager.with_header(|header| header.freelist_pages.get())) .unwrap(); assert_eq!( freelist_after_insert, 0, "Freelist count should be 0 after insert, was {freelist_after_insert}" ); let pagecount_after_insert = pager .io .block(|| pager.with_header(|header| header.database_size.get())) .unwrap(); const EXPECTED_OVERFLOW_PAGES: u32 = 3; assert_eq!( pagecount_after_insert, initial_pagecount + EXPECTED_OVERFLOW_PAGES, "Page count should be {} after insert, was {pagecount_after_insert}", initial_pagecount + EXPECTED_OVERFLOW_PAGES ); // Create a smaller record to overwrite with let small_payload = vec![b'Y'; 100]; // Much smaller payload let regs = &[Register::Value(Value::Blob(small_payload.clone()))]; let small_record = ImmutableRecord::from_registers(regs, regs.len()); // Seek to the existing record run_until_done( || { let key = SeekKey::TableRowId(1); cursor.seek(key, SeekOp::GE { eq_only: true }) }, pager.deref(), ) .unwrap(); // Overwrite the record with the same rowid let key = BTreeKey::new_table_rowid(1, Some(&small_record)); run_until_done(|| cursor.insert(&key), pager.deref()).unwrap(); // Check that the freelist count has increased, indicating overflow pages were cleared let freelist_after_overwrite = pager .io .block(|| pager.with_header(|header| header.freelist_pages.get())) .unwrap(); assert_eq!(freelist_after_overwrite, EXPECTED_OVERFLOW_PAGES, "Freelist count should be {EXPECTED_OVERFLOW_PAGES} after overwrite, was {freelist_after_overwrite}"); // Verify the record was actually overwritten by reading it back run_until_done( || { let key = SeekKey::TableRowId(1); cursor.seek(key, SeekOp::GE { eq_only: true }) }, pager.deref(), ) .unwrap(); let record = loop { match cursor.record().unwrap() { IOResult::Done(r) => break r, IOResult::IO(io) => io.wait(&*pager.io).unwrap(), } }; let record = record.unwrap(); // The record should now contain the smaller payload let record_payload = record.get_payload(); const RECORD_HEADER_SIZE: usize = 1; const ROWID_VARINT_SIZE: usize = 1; const ROWID_PAYLOAD_SIZE: usize = 0; // const int 1 doesn't take any space const BLOB_PAYLOAD_SIZE: usize = 1; // the size '100 bytes' can be expressed as 1 byte assert_eq!( record_payload.len(), RECORD_HEADER_SIZE + ROWID_VARINT_SIZE + ROWID_PAYLOAD_SIZE + BLOB_PAYLOAD_SIZE + small_payload.len(), "Record should now contain smaller payload after overwrite" ); } #[test] #[ignore] pub fn btree_insert_fuzz_ex() { for sequence in [ &[ (777548915, 3364), (639157228, 3796), (709175417, 1214), (390824637, 210), (906124785, 1481), (197677875, 1305), (457946262, 3734), (956825466, 592), (835875722, 1334), (649214013, 1250), (531143011, 1788), (765057993, 2351), (510007766, 1349), (884516059, 822), (81604840, 2545), ] .as_slice(), &[ (293471650, 2452), (163608869, 627), (544576229, 464), (705823748, 3441), ] .as_slice(), &[ (987283511, 2924), (261851260, 1766), (343847101, 1657), (315844794, 572), ] .as_slice(), &[ (987283511, 2924), (261851260, 1766), (343847101, 1657), (315844794, 572), (649272840, 1632), (723398505, 3140), (334416967, 3874), ] .as_slice(), ] { let (pager, root_page, _, _) = empty_btree(); let num_columns = 5; let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); for (key, size) in sequence.iter() { run_until_done( || { let key = SeekKey::TableRowId(*key); cursor.seek(key, SeekOp::GE { eq_only: true }) }, pager.deref(), ) .unwrap(); let regs = &[Register::Value(Value::Blob(vec![0; *size]))]; let value = ImmutableRecord::from_registers(regs, regs.len()); tracing::info!("insert key:{}", key); run_until_done( || cursor.insert(&BTreeKey::new_table_rowid(*key, Some(&value))), pager.deref(), ) .unwrap(); tracing::info!( "=========== btree ===========\n{}\n\n", format_btree(pager.clone(), root_page, 0) ); } for (key, _) in sequence.iter() { let seek_key = SeekKey::TableRowId(*key); assert!( matches!( cursor.seek(seek_key, SeekOp::GE { eq_only: true }).unwrap(), IOResult::Done(SeekResult::Found) ), "key {key} is not found" ); } } } fn rng_from_time_or_env() -> (ChaCha8Rng, u64) { let seed = std::env::var("SEED").map_or( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis(), |v| { v.parse() .expect("Failed to parse SEED environment variable as u64") }, ); let rng = ChaCha8Rng::seed_from_u64(seed as u64); (rng, seed as u64) } fn btree_insert_fuzz_run( attempts: usize, inserts: usize, size: impl Fn(&mut ChaCha8Rng) -> usize, ) { const VALIDATE_INTERVAL: usize = 1000; let do_validate_btree = std::env::var("VALIDATE_BTREE") .is_ok_and(|v| v.parse().expect("validate should be bool")); let (mut rng, seed) = rng_from_time_or_env(); let mut seen = crate::HashSet::default(); tracing::info!("super seed: {}", seed); let num_columns = 5; for _ in 0..attempts { let (pager, root_page, _db, conn) = empty_btree(); let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); let mut keys = SortedVec::new(); tracing::info!("seed: {seed}"); for insert_id in 0..inserts { let do_validate = do_validate_btree || (insert_id % VALIDATE_INTERVAL == 0); pager.begin_read_tx().unwrap(); run_until_done(|| pager.begin_write_tx(), &pager).unwrap(); let size = size(&mut rng); let key = { let result; loop { let key = (rng.next_u64() % (1 << 30)) as i64; if seen.contains(&key) { continue; } else { seen.insert(key); } result = key; break; } result }; keys.push(key); tracing::info!( "INSERT INTO t VALUES ({}, randomblob({})); -- {}", key, size, insert_id ); run_until_done( || { let key = SeekKey::TableRowId(key); cursor.seek(key, SeekOp::GE { eq_only: true }) }, pager.deref(), ) .unwrap(); let regs = &[Register::Value(Value::Blob(vec![0; size]))]; let value = ImmutableRecord::from_registers(regs, regs.len()); let btree_before = if do_validate { format_btree(pager.clone(), root_page, 0) } else { "".to_string() }; run_until_done( || cursor.insert(&BTreeKey::new_table_rowid(key, Some(&value))), pager.deref(), ) .unwrap(); pager.io.block(|| pager.commit_tx(&conn, true)).unwrap(); pager.begin_read_tx().unwrap(); // FIXME: add sorted vector instead, should be okay for small amounts of keys for now :P, too lazy to fix right now let _c = cursor.move_to_root().unwrap(); let mut valid = true; if do_validate { let _c = cursor.move_to_root().unwrap(); for key in keys.iter() { tracing::trace!("seeking key: {}", key); run_until_done(|| cursor.next(), pager.deref()).unwrap(); let cursor_rowid = run_until_done(|| cursor.rowid(), pager.deref()) .unwrap() .unwrap(); if *key != cursor_rowid { valid = false; println!("key {key} is not found, got {cursor_rowid}"); break; } } } // let's validate btree too so that we undertsand where the btree failed if do_validate && (!valid || matches!(validate_btree(pager.clone(), root_page), (_, false))) { let btree_after = format_btree(pager, root_page, 0); println!("btree before:\n{btree_before}"); println!("btree after:\n{btree_after}"); panic!("invalid btree"); } pager.end_read_tx(); } pager.begin_read_tx().unwrap(); tracing::info!( "=========== btree ===========\n{}\n\n", format_btree(pager.clone(), root_page, 0) ); if matches!(validate_btree(pager.clone(), root_page), (_, false)) { panic!("invalid btree"); } let _c = cursor.move_to_root().unwrap(); for key in keys.iter() { tracing::trace!("seeking key: {}", key); run_until_done(|| cursor.next(), pager.deref()).unwrap(); let cursor_rowid = run_until_done(|| cursor.rowid(), pager.deref()) .unwrap() .unwrap(); assert_eq!( *key, cursor_rowid, "key {key} is not found, got {cursor_rowid}" ); } pager.end_read_tx(); } } fn btree_index_insert_fuzz_run(attempts: usize, inserts: usize) { use crate::storage::pager::CreateBTreeFlags; let (mut rng, seed) = if std::env::var("SEED").is_ok() { let seed = std::env::var("SEED").unwrap(); let seed = seed.parse::().unwrap(); let rng = ChaCha8Rng::seed_from_u64(seed); (rng, seed) } else { rng_from_time_or_env() }; let mut seen = crate::HashSet::default(); tracing::info!("super seed: {}", seed); for _ in 0..attempts { let (pager, _, _db, conn) = empty_btree(); let index_root_page = pager .io .block(|| pager.btree_create(&CreateBTreeFlags::new_index())) .unwrap() as i64; let index_def = Index { name: "testindex".to_string(), where_clause: None, columns: (0..10) .map(|i| IndexColumn { name: format!("test{i}"), order: SortOrder::Asc, collation: None, pos_in_table: i, default: None, expr: None, }) .collect(), table_name: "test".to_string(), root_page: index_root_page, unique: false, ephemeral: false, has_rowid: false, index_method: None, on_conflict: None, }; let num_columns = index_def.columns.len(); let mut cursor = BTreeCursor::new_index(pager.clone(), index_root_page, &index_def, num_columns); let mut keys = SortedVec::new(); tracing::info!("seed: {seed}"); for i in 0..inserts { pager.begin_read_tx().unwrap(); pager.io.block(|| pager.begin_write_tx()).unwrap(); let key = { let result; loop { let cols = (0..num_columns) .map(|_| (rng.next_u64() % (1 << 30)) as i64) .collect::>(); if seen.contains(&cols) { continue; } else { seen.insert(cols.clone()); } result = cols; break; } result }; tracing::info!("insert {}/{}: {:?}", i + 1, inserts, key); keys.push(key.clone()); let regs = key .iter() .map(|col| Register::Value(Value::from_i64(*col))) .collect::>(); let value = ImmutableRecord::from_registers(®s, regs.len()); run_until_done( || { let record = ImmutableRecord::from_registers(®s, regs.len()); let key = SeekKey::IndexKey(&record); cursor.seek(key, SeekOp::GE { eq_only: true }) }, pager.deref(), ) .unwrap(); run_until_done( || cursor.insert(&BTreeKey::new_index_key(&value)), pager.deref(), ) .unwrap(); let c = cursor.move_to_root().unwrap(); if let Some(c) = c { pager.io.wait_for_completion(c).unwrap(); } pager.io.block(|| pager.commit_tx(&conn, true)).unwrap(); } // Check that all keys can be found by seeking pager.begin_read_tx().unwrap(); let _c = cursor.move_to_root().unwrap(); for (i, key) in keys.iter().enumerate() { tracing::info!("seeking key {}/{}: {:?}", i + 1, keys.len(), key); let exists = run_until_done( || { let regs = key .iter() .map(|col| Register::Value(Value::from_i64(*col))) .collect::>(); cursor.seek( SeekKey::IndexKey(&ImmutableRecord::from_registers(®s, regs.len())), SeekOp::GE { eq_only: true }, ) }, pager.deref(), ) .unwrap(); let mut found = matches!(exists, SeekResult::Found); if matches!(exists, SeekResult::TryAdvance) { run_until_done(|| cursor.next(), pager.deref()).unwrap(); found = cursor.has_record(); } assert!(found, "key {key:?} is not found"); } // Check that key count is right let _c = cursor.move_to_root().unwrap(); let mut count = 0; while { run_until_done(|| cursor.next(), pager.deref()).unwrap(); cursor.has_record } { count += 1; } assert_eq!( count, keys.len(), "key count is not right, got {}, expected {}", count, keys.len() ); // Check that all keys can be found in-order, by iterating the btree let _c = cursor.move_to_root().unwrap(); let mut prev = None; for (i, key) in keys.iter().enumerate() { tracing::info!("iterating key {}/{}: {:?}", i + 1, keys.len(), key); run_until_done(|| cursor.next(), pager.deref()).unwrap(); let record = loop { match cursor.record().unwrap() { IOResult::Done(r) => break r, IOResult::IO(io) => io.wait(&*pager.io).unwrap(), } }; let record = record.as_ref().unwrap(); let cur = record .get_values() .unwrap() .iter() .map(ValueRef::to_owned) .collect::>(); if let Some(prev) = prev { if prev >= cur { println!("Seed: {seed}"); } assert!( prev < cur, "keys are not in ascending order: {prev:?} < {cur:?}", ); } prev = Some(cur); } pager.end_read_tx(); } } fn btree_index_insert_delete_fuzz_run( attempts: usize, operations: usize, size: impl Fn(&mut ChaCha8Rng) -> usize, insert_chance: f64, ) { use crate::storage::pager::CreateBTreeFlags; let (mut rng, seed) = if std::env::var("SEED").is_ok() { let seed = std::env::var("SEED").unwrap(); let seed = seed.parse::().unwrap(); let rng = ChaCha8Rng::seed_from_u64(seed); (rng, seed) } else { rng_from_time_or_env() }; let mut seen = crate::HashSet::default(); tracing::info!("super seed: {}", seed); for _ in 0..attempts { let (pager, _, _db, conn) = empty_btree(); let index_root_page = pager .io .block(|| pager.btree_create(&CreateBTreeFlags::new_index())) .unwrap() as i64; let index_def = Index { name: "testindex".to_string(), where_clause: None, columns: vec![IndexColumn { name: "testcol".to_string(), order: SortOrder::Asc, collation: None, pos_in_table: 0, default: None, expr: None, }], table_name: "test".to_string(), root_page: index_root_page, unique: false, ephemeral: false, has_rowid: false, index_method: None, on_conflict: None, }; let mut cursor = BTreeCursor::new_index(pager.clone(), index_root_page, &index_def, 1); // Track expected keys that should be present in the tree let mut expected_keys = Vec::new(); tracing::info!("seed: {seed}"); for i in 0..operations { let print_progress = i % 100 == 0; pager.begin_read_tx().unwrap(); pager.io.block(|| pager.begin_write_tx()).unwrap(); // Decide whether to insert or delete (80% chance of insert) let is_insert = rng.next_u64() % 100 < (insert_chance * 100.0) as u64; if is_insert { // Generate a unique key for insertion let key = { let result; loop { let sizeof_blob = size(&mut rng); let blob = (0..sizeof_blob) .map(|_| (rng.next_u64() % 256) as u8) .collect::>(); if seen.contains(&blob) { continue; } else { seen.insert(blob.clone()); } result = blob; break; } result }; if print_progress { tracing::info!("insert {}/{}, seed: {seed}", i + 1, operations); } expected_keys.push(key.clone()); let regs = vec![Register::Value(Value::Blob(key))]; let value = ImmutableRecord::from_registers(®s, regs.len()); let seek_result = run_until_done( || { let record = ImmutableRecord::from_registers(®s, regs.len()); let key = SeekKey::IndexKey(&record); cursor.seek(key, SeekOp::GE { eq_only: true }) }, pager.deref(), ) .unwrap(); if let SeekResult::TryAdvance = seek_result { run_until_done(|| cursor.next(), pager.deref()).unwrap(); } run_until_done( || cursor.insert(&BTreeKey::new_index_key(&value)), pager.deref(), ) .unwrap(); } else { // Delete a random existing key if !expected_keys.is_empty() { let delete_idx = rng.next_u64() as usize % expected_keys.len(); let key_to_delete = expected_keys[delete_idx].clone(); if print_progress { tracing::info!("delete {}/{}, seed: {seed}", i + 1, operations); } let regs = vec![Register::Value(Value::Blob(key_to_delete.clone()))]; let record = ImmutableRecord::from_registers(®s, regs.len()); // Seek to the key to delete let seek_result = run_until_done( || { cursor .seek(SeekKey::IndexKey(&record), SeekOp::GE { eq_only: true }) }, pager.deref(), ) .unwrap(); let mut found = matches!(seek_result, SeekResult::Found); if matches!(seek_result, SeekResult::TryAdvance) { run_until_done(|| cursor.next(), pager.deref()).unwrap(); found = cursor.has_record() } assert!(found, "expected key {key_to_delete:?} is not found"); // Delete the key run_until_done(|| cursor.delete(), pager.deref()).unwrap(); // Remove from expected keys expected_keys.remove(delete_idx); } } let c = cursor.move_to_root().unwrap(); if let Some(c) = c { pager.io.wait_for_completion(c).unwrap(); } pager.io.block(|| pager.commit_tx(&conn, true)).unwrap(); } // Final validation let mut sorted_keys = expected_keys.clone(); sorted_keys.sort(); validate_expected_keys(&pager, &mut cursor, &sorted_keys, seed); pager.end_read_tx(); } } fn validate_expected_keys( pager: &Arc, cursor: &mut BTreeCursor, expected_keys: &[Vec], seed: u64, ) { // Check that all expected keys can be found by seeking pager.begin_read_tx().unwrap(); let _c = cursor.move_to_root().unwrap(); for (i, key) in expected_keys.iter().enumerate() { tracing::info!( "validating key {}/{}, seed: {seed}", i + 1, expected_keys.len() ); let exists = run_until_done( || { let regs = vec![Register::Value(Value::Blob(key.clone()))]; cursor.seek( SeekKey::IndexKey(&ImmutableRecord::from_registers(®s, regs.len())), SeekOp::GE { eq_only: true }, ) }, pager.deref(), ) .unwrap(); let mut found = matches!(exists, SeekResult::Found); if matches!(exists, SeekResult::TryAdvance) { run_until_done(|| cursor.next(), pager.deref()).unwrap(); found = cursor.has_record(); } assert!(found, "expected key {key:?} is not found"); } // Check key count let _c = cursor.move_to_root().unwrap(); run_until_done(|| cursor.rewind(), pager.deref()).unwrap(); if !cursor.has_record() { panic!("no keys in tree"); } let mut count = 1; loop { run_until_done(|| cursor.next(), pager.deref()).unwrap(); if !cursor.has_record() { break; } count += 1; } assert_eq!( count, expected_keys.len(), "key count is not right, got {}, expected {}, seed: {seed}", count, expected_keys.len() ); // Check that all keys can be found in-order, by iterating the btree let _c = cursor.move_to_root().unwrap(); for (i, key) in expected_keys.iter().enumerate() { run_until_done(|| cursor.next(), pager.deref()).unwrap(); tracing::info!( "iterating key {}/{}, cursor stack cur idx: {:?}, cursor stack depth: {:?}, seed: {seed}", i + 1, expected_keys.len(), cursor.stack.current_cell_index(), cursor.stack.current() ); let record = loop { match cursor.record().unwrap() { IOResult::Done(r) => break r, IOResult::IO(io) => io.wait(&*pager.io).unwrap(), } }; let record = record.as_ref().unwrap(); let cur = record.get_value(0).expect("expected at least one column"); let ValueRef::Blob(ref cur) = cur else { panic!("expected blob, got {cur:?}"); }; assert_eq!(cur, key, "key {key:?} is not found, seed: {seed}"); } pager.end_read_tx(); } #[test] pub fn test_drop_odd() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let header_size = 8; let mut total_size = 0; let mut cells = Vec::new(); let usable_space = 4096; let total_cells = 10; for i in 0..total_cells { let regs = &[Register::Value(Value::from_i64(i as i64))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let payload = add_record(i, i, page.clone(), record, &conn); assert_eq!(page_contents.cell_count(), i + 1); let free = compute_free_space(page_contents, usable_space).unwrap(); total_size += payload.len() + 2; assert_eq!(free, 4096 - total_size - header_size); cells.push(Cell { pos: i, payload }); } let mut removed = 0; let mut new_cells = Vec::new(); for cell in cells { if cell.pos % 2 == 1 { drop_cell(page_contents, cell.pos - removed, usable_space).unwrap(); removed += 1; } else { new_cells.push(cell); } } let cells = new_cells; for (i, cell) in cells.iter().enumerate() { ensure_cell(page_contents, i, &cell.payload); } for (i, cell) in cells.iter().enumerate() { ensure_cell(page_contents, i, &cell.payload); } } #[test] pub fn btree_insert_fuzz_run_equal_size() { for size in 1..8 { tracing::info!("======= size:{} =======", size); btree_insert_fuzz_run(2, 1024, |_| size); } } #[test] pub fn btree_index_insert_fuzz_run_equal_size() { btree_index_insert_fuzz_run(2, 1024); } #[test] pub fn btree_index_insert_delete_fuzz_run_test() { btree_index_insert_delete_fuzz_run( 2, 2000, |rng| { let min: u32 = 4; let size = min + rng.next_u32() % (1024 - min); size as usize }, 0.65, ); } #[test] pub fn btree_insert_fuzz_run_random() { btree_insert_fuzz_run(128, 16, |rng| (rng.next_u32() % 4096) as usize); } #[test] pub fn btree_insert_fuzz_run_small() { btree_insert_fuzz_run(1, 100, |rng| (rng.next_u32() % 128) as usize); } #[test] pub fn btree_insert_fuzz_run_big() { btree_insert_fuzz_run(64, 32, |rng| 3 * 1024 + (rng.next_u32() % 1024) as usize); } #[test] pub fn btree_insert_fuzz_run_overflow() { btree_insert_fuzz_run(64, 32, |rng| (rng.next_u32() % 32 * 1024) as usize); } #[test] #[ignore] pub fn fuzz_long_btree_insert_fuzz_run_equal_size() { for size in 1..8 { tracing::info!("======= size:{} =======", size); btree_insert_fuzz_run(2, 10_000, |_| size); } } #[test] #[ignore] pub fn fuzz_long_btree_index_insert_fuzz_run_equal_size() { btree_index_insert_fuzz_run(2, 10_000); } #[test] #[ignore] pub fn fuzz_long_btree_index_insert_delete_fuzz_run() { btree_index_insert_delete_fuzz_run( 2, 10000, |rng| { let min: u32 = 4; let size = min + rng.next_u32() % (1024 - min); size as usize }, 0.65, ); } #[test] #[ignore] pub fn fuzz_long_btree_insert_fuzz_run_random() { btree_insert_fuzz_run(2, 10_000, |rng| (rng.next_u32() % 4096) as usize); } #[test] #[ignore] pub fn fuzz_long_btree_insert_fuzz_run_small() { btree_insert_fuzz_run(2, 10_000, |rng| (rng.next_u32() % 128) as usize); } #[test] #[ignore] pub fn fuzz_long_btree_insert_fuzz_run_big() { btree_insert_fuzz_run(2, 10_000, |rng| 3 * 1024 + (rng.next_u32() % 1024) as usize); } #[test] #[ignore] pub fn fuzz_long_btree_insert_fuzz_run_overflow() { btree_insert_fuzz_run(2, 5_000, |rng| (rng.next_u32() % 32 * 1024) as usize); } #[allow(clippy::arc_with_non_send_sync)] fn setup_test_env(database_size: u32) -> Arc { let page_size = 512; let io: Arc = Arc::new(MemoryIO::new()); let buffer_pool = BufferPool::begin_init(&io, page_size * 128); let db_file = Arc::new(DatabaseFile::new( io.open_file(":memory:", OpenFlags::Create, false).unwrap(), )); let wal_file = io.open_file("test.wal", OpenFlags::Create, false).unwrap(); let wal_shared = WalFileShared::new_shared(wal_file).unwrap(); let last_checksum_and_max_frame = wal_shared.read().last_checksum_and_max_frame(); let wal: Arc = Arc::new(WalFile::new( io.clone(), wal_shared, last_checksum_and_max_frame, buffer_pool.clone(), )); // For new empty databases, init_page_1 must be Some(page) so allocate_page1() can be called let init_page_1 = Arc::new(ArcSwapOption::new(Some(default_page1(None)))); let pager = Arc::new( Pager::new( db_file, Some(wal), io, PageCache::new(10), buffer_pool, Arc::new(crate::sync::Mutex::new(())), init_page_1, ) .unwrap(), ); pager.io.step().unwrap(); let _ = run_until_done(|| pager.allocate_page1(), &pager); for _ in 0..(database_size - 1) { let _res = pager.allocate_page().unwrap(); } pager .io .block(|| { pager.with_header_mut(|header| { header.page_size = PageSize::new(page_size as u32).unwrap() }) }) .unwrap(); pager } #[test] pub fn test_clear_overflow_pages() -> Result<()> { let pager = setup_test_env(5); let num_columns = 5; let mut cursor = BTreeCursor::new_table(pager.clone(), 1, num_columns); let max_local = payload_overflow_threshold_max(PageType::TableLeaf, 4096); let usable_size = cursor.usable_space(); // Create a large payload that will definitely trigger overflow let large_payload = vec![b'A'; max_local + usable_size]; // Setup overflow pages (2, 3, 4) with linking let mut current_page = 2_usize; while current_page <= 4 { #[allow(clippy::arc_with_non_send_sync)] let buf = Arc::new(Buffer::new_temporary( pager .io .block(|| pager.with_header(|header| header.page_size))? .get() as usize, )); let _buf = buf.clone(); let c = Completion::new_write(move |_| { let _ = _buf.clone(); }); let _c = pager .db_file .write_page(current_page, buf.clone(), &IOContext::default(), c)?; pager.io.step()?; let (page, _c) = cursor.read_page(current_page as i64)?; while page.is_locked() { cursor.pager.io.step()?; } { let contents = page.get_contents(); let next_page = if current_page < 4 { current_page + 1 } else { 0 }; contents.write_u32_no_offset(0, next_page as u32); // Write pointer to next overflow page let buf = contents.as_ptr(); buf[4..].fill(b'A'); } current_page += 1; } pager.io.step()?; // Create leaf cell pointing to start of overflow chain let leaf_cell = BTreeCell::TableLeafCell(TableLeafCell { rowid: 1, payload: unsafe { transmute::<&[u8], &'static [u8]>(large_payload.as_slice()) }, first_overflow_page: Some(2), // Point to first overflow page payload_size: large_payload.len() as u64, }); let initial_freelist_pages = pager .io .block(|| pager.with_header(|header| header.freelist_pages))? .get(); // Clear overflow pages pager.io.block(|| cursor.clear_overflow_pages(&leaf_cell))?; let (freelist_pages, freelist_trunk_page) = pager .io .block(|| { pager.with_header(|header| { ( header.freelist_pages.get(), header.freelist_trunk_page.get(), ) }) }) .unwrap(); // Verify proper number of pages were added to freelist assert_eq!( freelist_pages, initial_freelist_pages + 3, "Expected 3 pages to be added to freelist" ); // If this is first trunk page let trunk_page_id = freelist_trunk_page; if trunk_page_id > 0 { // Verify trunk page structure let (trunk_page, _c) = cursor.read_page(trunk_page_id as i64)?; let contents = trunk_page.get_contents(); // Read number of leaf pages in trunk let n_leaf = contents.read_u32_no_offset(4); assert!(n_leaf > 0, "Trunk page should have leaf entries"); for i in 0..n_leaf { let leaf_page_id = contents.read_u32_no_offset(8 + (i as usize * 4)); assert!( (2..=4).contains(&leaf_page_id), "Leaf page ID {leaf_page_id} should be in range 2-4" ); } } Ok(()) } #[test] pub fn test_clear_overflow_pages_no_overflow() -> Result<()> { let pager = setup_test_env(5); let num_columns = 5; let mut cursor = BTreeCursor::new_table(pager.clone(), 1, num_columns); let small_payload = vec![b'A'; 10]; // Create leaf cell with no overflow pages let leaf_cell = BTreeCell::TableLeafCell(TableLeafCell { rowid: 1, payload: unsafe { transmute::<&[u8], &'static [u8]>(small_payload.as_slice()) }, first_overflow_page: None, payload_size: small_payload.len() as u64, }); let initial_freelist_pages = pager .io .block(|| pager.with_header(|header| header.freelist_pages))? .get() as usize; // Try to clear non-existent overflow pages pager.io.block(|| cursor.clear_overflow_pages(&leaf_cell))?; let (freelist_pages, freelist_trunk_page) = pager.io.block(|| { pager.with_header(|header| { ( header.freelist_pages.get(), header.freelist_trunk_page.get(), ) }) })?; // Verify freelist was not modified assert_eq!( freelist_pages as usize, initial_freelist_pages, "Freelist should not change when no overflow pages exist" ); // Verify trunk page wasn't created assert_eq!( freelist_trunk_page, 0, "No trunk page should be created when no overflow pages exist" ); Ok(()) } #[test] fn test_btree_destroy() -> Result<()> { let initial_size = 1; let pager = setup_test_env(initial_size); let num_columns = 5; let mut cursor = BTreeCursor::new_table(pager.clone(), 2, num_columns); // Initialize page 2 as a root page (interior) let root_page = run_until_done( || cursor.allocate_page(PageType::TableInterior, 0), &cursor.pager, )?; // Allocate two leaf pages let page3 = run_until_done( || cursor.allocate_page(PageType::TableLeaf, 0), &cursor.pager, )?; let page4 = run_until_done( || cursor.allocate_page(PageType::TableLeaf, 0), &cursor.pager, )?; // Configure the root page to point to the two leaf pages { let contents = root_page.get_contents(); // Set rightmost pointer to page4 contents.write_rightmost_ptr(page4.get().id as u32); // Create a cell with pointer to page3 let cell_content = vec![ // First 4 bytes: left child pointer (page3) (page3.get().id >> 24) as u8, (page3.get().id >> 16) as u8, (page3.get().id >> 8) as u8, page3.get().id as u8, // Next byte: rowid as varint (simple value 100) 100, ]; // Insert the cell insert_into_cell(contents, &cell_content, 0, 512)?; } // Add a simple record to each leaf page for page in [&page3, &page4] { let contents = page.get_contents(); // Simple record with just a rowid and payload let record_bytes = vec![ 5, // Payload length (varint) page.get().id as u8, // Rowid (varint) b'h', b'e', b'l', b'l', b'o', // Payload ]; insert_into_cell(contents, &record_bytes, 0, 512)?; } // Verify structure before destruction assert_eq!( pager .io .block(|| pager.with_header(|header| header.database_size))? .get(), 4, // We should have pages 1-4 "Database should have 4 pages total" ); // Track freelist state before destruction let initial_free_pages = pager .io .block(|| pager.with_header(|header| header.freelist_pages))? .get(); assert_eq!(initial_free_pages, 0, "should start with no free pages"); run_until_done(|| cursor.btree_destroy(), pager.deref())?; let pages_freed = pager .io .block(|| pager.with_header(|header| header.freelist_pages))? .get() - initial_free_pages; assert_eq!(pages_freed, 3, "should free 3 pages (root + 2 leaves)"); Ok(()) } #[test] pub fn test_clear_btree_with_single_page() -> Result<()> { let (pager, root_page, _, _) = empty_btree(); let num_columns = 5; let record_count = 10; let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); for rowid in 1..=record_count { insert_record(&mut cursor, &pager, rowid, Value::from_i64(rowid))?; } let page_count = pager .io .block(|| pager.with_header(|header| header.database_size.get()))?; assert_eq!( page_count, 2, "expected two pages (header + root), got {page_count}" ); run_until_done(|| cursor.clear_btree(), &pager)?; assert_btree_empty(&mut cursor, pager.deref()) } #[test] pub fn test_clear_btree_with_multiple_pages() -> Result<()> { let (pager, root_page, _, _) = empty_btree(); let num_columns = 5; let record_count = 1000; let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); for rowid in 1..=record_count { insert_record(&mut cursor, &pager, rowid, Value::from_i64(rowid))?; } // Ensure enough records were created so the tree spans multiple pages. let page_count = pager .io .block(|| pager.with_header(|header| header.database_size.get()))?; assert!( page_count > 2, "expected more pages than just header + root, got {page_count}" ); run_until_done(|| cursor.clear_btree(), &pager)?; assert_btree_empty(&mut cursor, pager.deref()) } #[test] pub fn test_clear_btree_reinsertion() -> Result<()> { let (pager, root_page, _, _) = empty_btree(); let num_columns = 5; let record_count = 1000; let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); for rowid in 1..=record_count { insert_record(&mut cursor, &pager, rowid, Value::from_i64(rowid))?; } run_until_done(|| cursor.clear_btree(), &pager)?; // Reinsert into cleared B-tree to ensure it’s still functional for rowid in 1..=record_count { insert_record(&mut cursor, &pager, rowid, Value::from_i64(rowid))?; } if let (_, false) = validate_btree(pager.clone(), root_page) { panic!("Invalid B-tree after reinsertion"); } let _c = cursor.move_to_root()?; for i in 1..=record_count { run_until_done(|| cursor.next(), &pager)?; let exists = cursor.has_record(); assert!(exists, "Record {i} not found"); let record = loop { match cursor.record()? { IOResult::Done(r) => break r, IOResult::IO(io) => io.wait(&*pager.io)?, } } .unwrap(); let value = record.get_value(0)?; assert_eq!( value, ValueRef::Numeric(Numeric::Integer(i)), "Unexpected value for record {i}", ); } Ok(()) } #[test] pub fn test_clear_btree_multiple_cursors() -> Result<()> { let (pager, root_page, _, _) = empty_btree(); let num_columns = 5; let record_count = 1000; let mut cursor1 = BTreeCursor::new_table(pager.clone(), root_page, num_columns); let mut cursor2 = BTreeCursor::new_table(pager.clone(), root_page, num_columns); // Use cursor1 to insert records for rowid in 1..=record_count { insert_record(&mut cursor1, &pager, rowid, Value::from_i64(rowid))?; } // Use cursor1 to clear the btree run_until_done(|| cursor1.clear_btree(), &pager)?; // Verify that cursor2 works correctly assert_btree_empty(&mut cursor2, pager.deref())?; // Insert using cursor2 insert_record(&mut cursor1, &pager, 1, Value::from_i64(123))?; if let (_, false) = validate_btree(pager.clone(), root_page) { panic!("Invalid B-tree after insertion"); } let key = Value::from_i64(1); let exists = run_until_done(|| cursor2.exists(&key), pager.deref())?; assert!(exists, "key not found {key}"); Ok(()) } /// Regression test: after clear_btree() on one cursor and invalidate_btree_cache() /// on a sibling cursor sharing the same btree (e.g. OpenDup), the count cache must /// be reset. Otherwise count() returns the stale value from before the clear. /// /// This is the mechanism behind stale partition counts in window functions: /// ResetSorter calls clear_btree on the main cursor and invalidate_btree_cache on /// OpenDup cursors. If count_state/count are not reset, the Count instruction on the /// dup cursor returns the previous partition's row count. #[test] pub fn test_clear_btree_resets_count_cache() -> Result<()> { let (pager, root_page, _, _) = empty_btree(); let num_columns = 1; let mut cursor_main = BTreeCursor::new_table(pager.clone(), root_page, num_columns); let mut cursor_dup = BTreeCursor::new_table(pager.clone(), root_page, num_columns); // Insert 5 records (simulating partition 'a' with 5 rows) for rowid in 1..=5 { insert_record(&mut cursor_main, &pager, rowid, Value::from_i64(rowid))?; } // Count via the dup cursor -- should be 5 and caches the result let count1 = run_until_done(|| cursor_dup.count(), pager.deref())?; assert_eq!(count1, 5, "first count should be 5"); // Simulate ResetSorter: clear the btree via the main cursor run_until_done(|| cursor_main.clear_btree(), &pager)?; // Invalidate sibling cursor's cache (as op_reset_sorter does) cursor_dup.invalidate_btree_cache(); // Insert only 2 records (simulating partition 'b' with 2 rows) for rowid in 1..=2 { insert_record(&mut cursor_main, &pager, rowid, Value::from_i64(rowid + 10))?; } // Count via the dup cursor again -- must be 2, not the stale 5 let count2 = run_until_done(|| cursor_dup.count(), pager.deref())?; assert_eq!( count2, 2, "count after clear + re-insert should be 2, got stale count if cache was not reset" ); Ok(()) } /// Verify that clear_btree() resets its own count cache, not just sibling cursors. #[test] pub fn test_clear_btree_resets_own_count_cache() -> Result<()> { let (pager, root_page, _, _) = empty_btree(); let num_columns = 1; let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); // Insert 5 records and count for rowid in 1..=5 { insert_record(&mut cursor, &pager, rowid, Value::from_i64(rowid))?; } let count1 = run_until_done(|| cursor.count(), pager.deref())?; assert_eq!(count1, 5); // Clear and re-insert 3 records run_until_done(|| cursor.clear_btree(), &pager)?; for rowid in 1..=3 { insert_record(&mut cursor, &pager, rowid, Value::from_i64(rowid + 10))?; } // Count should reflect the new 3 records, not the stale 5 let count2 = run_until_done(|| cursor.count(), pager.deref())?; assert_eq!( count2, 3, "count after clear_btree + re-insert should be 3, not stale 5" ); Ok(()) } /// Verify that insert() invalidates the count cache so a subsequent count() /// re-traverses the btree instead of returning the stale cached value. #[test] pub fn test_insert_invalidates_count_cache() -> Result<()> { let (pager, root_page, _, _) = empty_btree(); let num_columns = 1; let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); // Insert 3 records and count for rowid in 1..=3 { insert_record(&mut cursor, &pager, rowid, Value::from_i64(rowid))?; } let count1 = run_until_done(|| cursor.count(), pager.deref())?; assert_eq!(count1, 3, "initial count should be 3"); // Insert 2 more records for rowid in 4..=5 { insert_record(&mut cursor, &pager, rowid, Value::from_i64(rowid))?; } // Count should reflect all 5 records, not the stale 3 let count2 = run_until_done(|| cursor.count(), pager.deref())?; assert_eq!( count2, 5, "count after additional inserts should be 5, not stale 3" ); Ok(()) } #[test] pub fn test_clear_btree_with_overflow_pages() -> Result<()> { let (pager, root_page, _, _) = empty_btree(); let num_columns = 5; let record_count = 100; let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); let initial_page_count = pager .io .block(|| pager.with_header(|header| header.database_size.get()))?; for rowid in 1..=record_count { let large_blob = vec![b'A'; 8192]; insert_record(&mut cursor, &pager, rowid, Value::Blob(large_blob))?; } let page_count_after_inserts = pager .io .block(|| pager.with_header(|header| header.database_size.get()))?; let created_pages = page_count_after_inserts - initial_page_count; assert!( created_pages > record_count as u32, "expected more pages to be created than records, got {created_pages}" ); run_until_done(|| cursor.clear_btree(), &pager)?; assert_btree_empty(&mut cursor, pager.deref()) } #[test] pub fn test_defragment() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let header_size = 8; let mut total_size = 0; let mut cells = Vec::new(); let usable_space = 4096; for i in 0..3 { let regs = &[Register::Value(Value::from_i64(i as i64))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let payload = add_record(i, i, page.clone(), record, &conn); assert_eq!(page_contents.cell_count(), i + 1); let free = compute_free_space(page_contents, usable_space).unwrap(); total_size += payload.len() + 2; assert_eq!(free, 4096 - total_size - header_size); cells.push(Cell { pos: i, payload }); } for (i, cell) in cells.iter().enumerate() { ensure_cell(page_contents, i, &cell.payload); } cells.remove(1); drop_cell(page_contents, 1, usable_space).unwrap(); for (i, cell) in cells.iter().enumerate() { ensure_cell(page_contents, i, &cell.payload); } defragment_page(page_contents, usable_space, 4).unwrap(); for (i, cell) in cells.iter().enumerate() { ensure_cell(page_contents, i, &cell.payload); } } #[test] pub fn test_drop_odd_with_defragment() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let header_size = 8; let mut total_size = 0; let mut cells = Vec::new(); let usable_space = 4096; let total_cells = 10; for i in 0..total_cells { let regs = &[Register::Value(Value::from_i64(i as i64))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let payload = add_record(i, i, page.clone(), record, &conn); assert_eq!(page_contents.cell_count(), i + 1); let free = compute_free_space(page_contents, usable_space).unwrap(); total_size += payload.len() + 2; assert_eq!(free, 4096 - total_size - header_size); cells.push(Cell { pos: i, payload }); } let mut removed = 0; let mut new_cells = Vec::new(); for cell in cells { if cell.pos % 2 == 1 { drop_cell(page_contents, cell.pos - removed, usable_space).unwrap(); removed += 1; } else { new_cells.push(cell); } } let cells = new_cells; for (i, cell) in cells.iter().enumerate() { ensure_cell(page_contents, i, &cell.payload); } defragment_page(page_contents, usable_space, 4).unwrap(); for (i, cell) in cells.iter().enumerate() { ensure_cell(page_contents, i, &cell.payload); } } #[test] pub fn test_fuzz_drop_defragment_insert() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let header_size = 8; let mut total_size = 0; let mut cells = Vec::new(); let usable_space = 4096; let mut i = 100000; let seed = rng().random(); tracing::info!("seed {}", seed); let mut rng = ChaCha8Rng::seed_from_u64(seed); while i > 0 { i -= 1; match rng.next_u64() % 4 { 0 => { // allow appends with extra place to insert let cell_idx = rng.next_u64() as usize % (page_contents.cell_count() + 1); let free = compute_free_space(page_contents, usable_space).unwrap(); let regs = &[Register::Value(Value::from_i64(i as i64))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let mut payload: Vec = Vec::new(); let mut fill_cell_payload_state = FillCellPayloadState::Start; run_until_done( || { fill_cell_payload( &PinGuard::new(page.clone()), Some(i as i64), &mut payload, cell_idx, &record, 4096, conn.pager.load().clone(), &mut fill_cell_payload_state, ) }, &conn.pager.load().clone(), ) .unwrap(); if (free as usize) < payload.len() + 2 { // do not try to insert overflow pages because they require balancing continue; } insert_into_cell(page_contents, &payload, cell_idx, 4096).unwrap(); assert!(page_contents.overflow_cells.is_empty()); total_size += payload.len() + 2; cells.insert(cell_idx, Cell { pos: i, payload }); } 1 => { if page_contents.cell_count() == 0 { continue; } let cell_idx = rng.next_u64() as usize % page_contents.cell_count(); let (_, len) = page_contents .cell_get_raw_region(cell_idx, usable_space) .unwrap(); drop_cell(page_contents, cell_idx, usable_space).unwrap(); total_size -= len + 2; cells.remove(cell_idx); } 2 => { defragment_page(page_contents, usable_space, 4).unwrap(); } 3 => { // check cells for (i, cell) in cells.iter().enumerate() { ensure_cell(page_contents, i, &cell.payload); } assert_eq!(page_contents.cell_count(), cells.len()); } _ => unreachable!(), } let free = compute_free_space(page_contents, usable_space).unwrap(); assert_eq!(free, 4096 - total_size - header_size); } } #[test] pub fn test_fuzz_drop_defragment_insert_issue_1085() { // This test is used to demonstrate that issue at https://github.com/tursodatabase/turso/issues/1085 // is FIXED. let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let header_size = 8; let mut total_size = 0; let usable_space = 4096; let mut i = 1000; for seed in [15292777653676891381, 9261043168681395159] { tracing::info!("seed {}", seed); let mut rng = ChaCha8Rng::seed_from_u64(seed); while i > 0 { i -= 1; match rng.next_u64() % 3 { 0 => { // allow appends with extra place to insert let cell_idx = rng.next_u64() as usize % (page_contents.cell_count() + 1); let free = compute_free_space(page_contents, usable_space).unwrap(); let regs = &[Register::Value(Value::from_i64(i))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let mut payload: Vec = Vec::new(); let mut fill_cell_payload_state = FillCellPayloadState::Start; run_until_done( || { fill_cell_payload( &PinGuard::new(page.clone()), Some(i), &mut payload, cell_idx, &record, 4096, conn.pager.load().clone(), &mut fill_cell_payload_state, ) }, &conn.pager.load().clone(), ) .unwrap(); if (free as usize) < payload.len() - 2 { // do not try to insert overflow pages because they require balancing continue; } insert_into_cell(page_contents, &payload, cell_idx, 4096).unwrap(); assert!(page_contents.overflow_cells.is_empty()); total_size += payload.len() + 2; } 1 => { if page_contents.cell_count() == 0 { continue; } let cell_idx = rng.next_u64() as usize % page_contents.cell_count(); let (_, len) = page_contents .cell_get_raw_region(cell_idx, usable_space) .unwrap(); drop_cell(page_contents, cell_idx, usable_space).unwrap(); total_size -= len + 2; } 2 => { defragment_page(page_contents, usable_space, 4).unwrap(); } _ => unreachable!(), } let free = compute_free_space(page_contents, usable_space).unwrap(); assert_eq!(free, 4096 - total_size - header_size); } } } // this test will create a tree like this: // -page:2, ptr(right):4 // +cells:node[rowid:14, ptr(<=):3] // -page:3, ptr(right):0 // +cells:leaf[rowid:11, len(payload):137, overflow:false] // -page:4, ptr(right):0 // +cells: #[test] pub fn test_drop_page_in_balancing_issue_1203() { let db = get_database(); let conn = db.connect().unwrap(); let queries = vec![ "CREATE TABLE lustrous_petit (awesome_nomous TEXT,ambitious_amargi TEXT,fantastic_daniels BLOB,stupendous_highleyman TEXT,relaxed_crane TEXT,elegant_bromma INTEGER,proficient_castro BLOB,ambitious_liman TEXT,responsible_lusbert BLOB);", "INSERT INTO lustrous_petit VALUES ('funny_sarambi', 'hardworking_naoumov', X'666561726C6573735F68696C6C', 'elegant_iafd', 'rousing_flag', 681399778772406122, X'706572736F6E61626C655F676F6477696E6772696D6D', 'insightful_anonymous', X'706F77657266756C5F726F636861'), ('personable_holmes', 'diligent_pera', X'686F6E6573745F64696D656E73696F6E', 'energetic_raskin', 'gleaming_federasyon', -2778469859573362611, X'656666696369656E745F6769617A', 'sensible_skirda', X'66616E7461737469635F6B656174696E67'), ('inquisitive_baedan', 'brave_sphinx', X'67656E65726F75735F6D6F6E7473656E79', 'inquisitive_syndicate', 'amiable_room', 6954857961525890638, X'7374756E6E696E675F6E6965747A73636865', 'glowing_coordinator', X'64617A7A6C696E675F7365766572696E65'), ('upbeat_foxtale', 'engaging_aktimon', X'63726561746976655F6875746368696E6773', 'ample_locura', 'creative_barrett', 6413352509911171593, X'6772697070696E675F6D696E7969', 'competitive_parissi', X'72656D61726B61626C655F77696E7374616E6C6579');", "INSERT INTO lustrous_petit VALUES ('ambitious_berry', 'devoted_marshall', X'696E7175697369746976655F6C6172657661', 'flexible_pramen', 'outstanding_stauch', 6936508362673228293, X'6C6F76696E675F6261756572', 'charming_anonymous', X'68617264776F726B696E675F616E6E6973'), ('enchanting_cohen', 'engaging_rubel', X'686F6E6573745F70726F766F63617A696F6E65', 'humorous_robin', 'imaginative_shuzo', 4762266264295288131, X'726F7573696E675F6261796572', 'vivid_bolling', X'6F7267616E697A65645F7275696E73'), ('affectionate_resistance', 'gripping_rustamova', X'6B696E645F6C61726B696E', 'bright_boulanger', 'upbeat_ashirov', -1726815435854320541, X'61646570745F66646361', 'dazzling_tashjian', X'68617264776F726B696E675F6D6F72656C'), ('zestful_ewald', 'favorable_lewis', X'73747570656E646F75735F7368616C6966', 'bright_combustion', 'blithesome_harding', 8408539013935554176, X'62726176655F737079726F706F756C6F75', 'hilarious_finnegan', X'676976696E675F6F7267616E697A696E67'), ('blithesome_picqueray', 'sincere_william', X'636F75726167656F75735F6D69746368656C6C', 'rousing_atan', 'mirthful_katie', -429232313453215091, X'6C6F76656C795F776174616E616265', 'stupendous_mcmillan', X'666F63757365645F6B61666568'), ('incredible_kid', 'friendly_yvetot', X'706572666563745F617A697A', 'helpful_manhattan', 'shining_horrox', -4318061095860308846, X'616D626974696F75735F726F7765', 'twinkling_anarkiya', X'696D6167696E61746976655F73756D6E6572');", "INSERT INTO lustrous_petit VALUES ('sleek_graeber', 'approachable_ghazzawi', X'62726176655F6865776974747768697465', 'adaptable_zimmer', 'polite_cohn', -5464225138957223865, X'68756D6F726F75735F736E72', 'adaptable_igualada', X'6C6F76656C795F7A686F75'), ('imaginative_rautiainen', 'magnificent_ellul', X'73706C656E6469645F726F6361', 'responsible_brown', 'upbeat_uruguaya', -1185340834321792223, X'616D706C655F6D6470', 'philosophical_kelly', X'676976696E675F6461676865726D6172676F7369616E'), ('blithesome_darkness', 'creative_newell', X'6C757374726F75735F61706174726973', 'engaging_kids', 'charming_wark', -1752453819873942466, X'76697669645F6162657273', 'independent_barricadas', X'676C697374656E696E675F64686F6E6474'), ('productive_chardronnet', 'optimistic_karnage', X'64696C6967656E745F666F72657374', 'engaging_beggar', 'sensible_wolke', 784341549042407442, X'656E676167696E675F6265726B6F7769637A', 'blithesome_zuzenko', X'6E6963655F70726F766F63617A696F6E65');", "INSERT INTO lustrous_petit VALUES ('shining_sagris', 'considerate_mother', X'6F70656E5F6D696E6465645F72696F74', 'polite_laufer', 'patient_mink', 2240393952789100851, X'636F75726167656F75735F6D636D696C6C616E', 'glowing_robertson', X'68656C7066756C5F73796D6F6E6473'), ('dazzling_glug', 'stupendous_poznan', X'706572736F6E61626C655F6672616E6B73', 'open_minded_ruins', 'qualified_manes', 2937238916206423261, X'696E736967687466756C5F68616B69656C', 'passionate_borl', X'616D6961626C655F6B7570656E647561'), ('wondrous_parry', 'knowledgeable_giovanni', X'6D6F76696E675F77696E6E', 'shimmering_aberlin', 'affectionate_calhoun', 702116954493913499, X'7265736F7572636566756C5F62726F6D6D61', 'propitious_mezzagarcia', X'746563686E6F6C6F676963616C5F6E6973686974616E69');", "INSERT INTO lustrous_petit VALUES ('kind_room', 'hilarious_crow', X'6F70656E5F6D696E6465645F6B6F74616E7969', 'hardworking_petit', 'adaptable_zarrow', 2491343172109894986, X'70726F647563746976655F646563616C6F677565', 'willing_sindikalis', X'62726561746874616B696E675F6A6F7264616E');", "INSERT INTO lustrous_petit VALUES ('confident_etrebilal', 'agreeable_shifu', X'726F6D616E7469635F7363687765697A6572', 'loving_debs', 'gripping_spooner', -3136910055229112693, X'677265676172696F75735F736B726F7A6974736B79', 'ample_ontiveros', X'7175616C69666965645F726F6D616E69656E6B6F'), ('competitive_call', 'technological_egoumenides', X'6469706C6F6D617469635F6D6F6E616768616E', 'willing_stew', 'frank_neal', -5973720171570031332, X'6C6F76696E675F6465737461', 'dazzling_gambone', X'70726F647563746976655F6D656E64656C676C6565736F6E'), ('favorable_delesalle', 'sensible_atterbury', X'666169746866756C5F64617861', 'bountiful_aldred', 'marvelous_malgraith', 5330463874397264493, X'706572666563745F7765726265', 'lustrous_anti', X'6C6F79616C5F626F6F6B6368696E'), ('stellar_corlu', 'loyal_espana', X'6D6F76696E675F7A6167', 'efficient_nelson', 'qualified_shepard', 1015518116803600464, X'737061726B6C696E675F76616E6469766572', 'loving_scoffer', X'686F6E6573745F756C72696368'), ('adaptable_taylor', 'shining_yasushi', X'696D6167696E61746976655F776974746967', 'alluring_blackmore', 'zestful_coeurderoy', -7094136731216188999, X'696D6167696E61746976655F757A63617465677569', 'gleaming_hernandez', X'6672616E6B5F646F6D696E69636B'), ('competitive_luis', 'stellar_fredericks', X'616772656561626C655F6D696368656C', 'optimistic_navarro', 'funny_hamilton', 4003895682491323194, X'6F70656E5F6D696E6465645F62656C6D6173', 'incredible_thorndycraft', X'656C6567616E745F746F6C6B69656E'), ('remarkable_parsons', 'sparkling_ulrich', X'737061726B6C696E675F6D6172696E636561', 'technological_leighlais', 'warmhearted_konok', -5789111414354869563, X'676976696E675F68657272696E67', 'adept_dabtara', X'667269656E646C795F72617070');", "INSERT INTO lustrous_petit VALUES ('hardworking_norberg', 'approachable_winter', X'62726176655F68617474696E6768', 'imaginative_james', 'open_minded_capital', -5950508516718821688, X'6C757374726F75735F72616E7473', 'warmhearted_limanov', X'696E736967687466756C5F646F637472696E65'), ('generous_shatz', 'generous_finley', X'726176697368696E675F6B757A6E6574736F76', 'stunning_arrigoni', 'favorable_volcano', -8442328990977069526, X'6D6972746866756C5F616C7467656C64', 'thoughtful_zurbrugg', X'6D6972746866756C5F6D6F6E726F65'), ('frank_kerr', 'splendid_swain', X'70617373696F6E6174655F6D6470', 'flexible_dubey', 'sensible_tj', 6352949260574274181, X'656666696369656E745F6B656D736B79', 'vibrant_ege', X'736C65656B5F6272696768746F6E'), ('organized_neal', 'glistening_sugar', X'656E676167696E675F6A6F72616D', 'romantic_krieger', 'qualified_corr', -4774868512022958085, X'706572666563745F6B6F7A6172656B', 'bountiful_zaikowska', X'74686F7567687466756C5F6C6F6767616E73'), ('excellent_lydiettcarrion', 'diligent_denslow', X'666162756C6F75735F6D616E68617474616E', 'confident_tomar', 'glistening_ligt', -1134906665439009896, X'7175616C69666965645F6F6E6B656E', 'remarkable_anarkiya', X'6C6F79616C5F696E64616261'), ('passionate_melis', 'loyal_xsilent', X'68617264776F726B696E675F73637564', 'lustrous_barnes', 'nice_sugako', -4097897163377829983, X'726F6D616E7469635F6461686572', 'bright_imrie', X'73656E7369626C655F6D61726B'), ('giving_mlb', 'breathtaking_fourier', X'736C65656B5F616E61726368697374', 'glittering_malet', 'brilliant_crew', 8791228049111405793, X'626F756E746966756C5F626576656E736565', 'lovely_swords', X'70726F706974696F75735F696E656469746173'), ('honest_wright', 'qualified_rabble', X'736C65656B5F6D6172656368616C', 'shimmering_marius', 'blithesome_mckelvie', -1330737263592370654, X'6F70656E5F6D696E6465645F736D616C6C', 'energetic_gorman', X'70726F706974696F75735F6B6F74616E7969');", "DELETE FROM lustrous_petit WHERE (ambitious_liman > 'adept_dabtaqu');", "INSERT INTO lustrous_petit VALUES ('technological_dewey', 'fabulous_st', X'6F7074696D69737469635F73687562', 'considerate_levy', 'adaptable_kernis', 4195134012457716562, X'61646570745F736F6C6964617269646164', 'vibrant_crump', X'6C6F79616C5F72796E6572'), ('super_marjan', 'awesome_gethin', X'736C65656B5F6F737465727765696C', 'diplomatic_loidl', 'qualified_bokani', -2822676417968234733, X'6272696768745F64756E6C6170', 'creative_en', X'6D6972746866756C5F656C6F6666'), ('philosophical_malet', 'unique_garcia', X'76697669645F6E6F7262657267', 'spellbinding_fire', 'faithful_barringtonbush', -7293711848773657758, X'6272696C6C69616E745F6F6B65656665', 'gripping_guillon', X'706572736F6E61626C655F6D61726C696E7370696B65'), ('thoughtful_morefus', 'lustrous_rodriguez', X'636F6E666964656E745F67726F73736D616E726F73686368696E', 'devoted_jackson', 'propitious_karnage', -7802999054396485709, X'63617061626C655F64', 'enchanting_orwell', X'7477696E6B6C696E675F64616C616B6F676C6F75'), ('alluring_guillon', 'brilliant_pinotnoir', X'706572736F6E61626C655F6A6165636B6C65', 'open_minded_azeez', 'courageous_romania', 2126962403055072268, X'746563686E6F6C6F676963616C5F6962616E657A', 'open_minded_rosa', X'6C757374726F75735F6575726F7065'), ('courageous_kolokotronis', 'inquisitive_gahman', X'677265676172696F75735F626172726574', 'ambitious_shakur', 'fantastic_apatris', -1232732971861520864, X'737061726B6C696E675F7761746368', 'captivating_clover', X'636F6E666964656E745F736574686E65737363617374726F'), ('charming_sullivan', 'focused_congress', X'7368696D6D6572696E675F636C7562', 'wondrous_skrbina', 'giving_mendanlioglu', -6837337053772308333, X'636861726D696E675F73616C696E6173', 'rousing_hedva', X'6469706C6F6D617469635F7061796E');", ]; for query in queries { let mut stmt = conn.query(query).unwrap().unwrap(); loop { let row = stmt.step().expect("step"); match row { StepResult::Done => { break; } _ => { tracing::debug!("row {:?}", row); } } } } } // this test will create a tree like this: // -page:2, ptr(right):3 // +cells: // -page:3, ptr(right):0 // +cells: #[test] pub fn test_drop_page_in_balancing_issue_1203_2() { let db = get_database(); let conn = db.connect().unwrap(); let queries = vec![ "CREATE TABLE super_becky (engrossing_berger BLOB,plucky_chai BLOB,mirthful_asbo REAL,bountiful_jon REAL,competitive_petit REAL,engrossing_rexroth REAL);", "INSERT INTO super_becky VALUES (X'636861726D696E675F6261796572', X'70726F647563746976655F70617269737369', 6847793643.408741, 7330361375.924953, -6586051582.891455, -6921021872.711397), (X'657863656C6C656E745F6F7267616E697A696E67', X'6C757374726F75735F73696E64696B616C6973', 9905774996.48619, 570325205.2246342, 5852346465.53047, 728566012.1968269), (X'7570626561745F73656174746C65', X'62726176655F6661756E', -2202725836.424899, 5424554426.388281, 2625872085.917082, -6657362503.808359), (X'676C6F77696E675F6D617877656C6C', X'7761726D686561727465645F726F77616E', -9610936969.793116, 4886606277.093559, -3414536174.7928505, 6898267795.317778), (X'64796E616D69635F616D616E', X'7374656C6C61725F7374657073', 3918935692.153696, 151068445.947237, 4582065669.356403, -3312668220.4789667), (X'64696C6967656E745F64757272757469', X'7175616C69666965645F6D726163686E696B', 5527271629.262201, 6068855126.044355, 289904657.13490677, 2975774820.0877323), (X'6469706C6F6D617469635F726F76657363696F', X'616C6C7572696E675F626F7474696369', 9844748192.66119, -6180276383.305578, -4137330511.025565, -478754566.79494476), (X'776F6E64726F75735F6173686572', X'6465766F7465645F6176657273696F6E', 2310211470.114773, -6129166761.628184, -2865371645.3145514, 7542428654.8645935), (X'617070726F61636861626C655F6B686F6C61', X'6C757374726F75735F6C696E6E656C6C', -4993113161.458349, 7356727284.362968, -3228937035.568404, -1779334005.5067253);", "INSERT INTO super_becky VALUES (X'74686F7567687466756C5F726576696577', X'617765736F6D655F63726F73736579', 9401977997.012783, 8428201961.643898, 2822821303.052643, 4555601220.718847), (X'73706563746163756C61725F6B686179617469', X'616772656561626C655F61646F6E696465', 7414547022.041355, 365016845.73330307, 50682963.055828094, -9258802584.962656), (X'6C6F79616C5F656D6572736F6E', X'676C6F77696E675F626174616C6F', -5522070106.765736, 2712536599.6384163, 6631385631.869345, 1242757880.7583427), (X'68617264776F726B696E675F6F6B656C6C79', X'666162756C6F75735F66696C697373', 6682622809.9778805, 4233900041.917185, 9017477903.795563, -756846353.6034946), (X'68617264776F726B696E675F626C61756D616368656E', X'616666656374696F6E6174655F6B6F736D616E', -1146438175.3174362, -7545123696.438596, -6799494012.403366, 5646913977.971333), (X'66616E7461737469635F726F77616E', X'74686F7567687466756C5F7465727269746F72696573', -4414529784.916277, -6209371635.279242, 4491104121.288605, 2590223842.117277);", "INSERT INTO super_becky VALUES (X'676C697374656E696E675F706F72746572', X'696E7175697369746976655F656D', 2986144164.3676434, 3495899172.5935287, -849280584.9386635, 6869709150.2699375), (X'696D6167696E61746976655F6D65726C696E6F', X'676C6F77696E675F616B74696D6F6E', 8733490615.829357, 6782649864.719433, 6926744218.74107, 1532081022.4379768), (X'6E6963655F726F73736574', X'626C69746865736F6D655F66696C697373', -839304300.0706863, 6155504968.705227, -2951592321.950267, -6254186334.572437), (X'636F6E666964656E745F6C69626574', X'676C696D6D6572696E675F6B6F74616E7969', -5344675223.37533, -8703794729.211002, 3987472096.020382, -7678989974.961197), (X'696D6167696E61746976655F6B61726162756C7574', X'64796E616D69635F6D6367697272', 2028227065.6995697, -7435689525.030833, 7011220815.569796, 5526665697.213846), (X'696E7175697369746976655F636C61726B', X'616666656374696F6E6174655F636C6561766572', 3016598350.546356, -3686782925.383732, 9671422351.958004, 9099319829.078941), (X'63617061626C655F746174616E6B61', X'696E6372656469626C655F6F746F6E6F6D61', 6339989259.432795, -8888997534.102034, 6855868409.475763, -2565348887.290493), (X'676F7267656F75735F6265726E657269', X'65647563617465645F6F6D6F77616C69', 6992467657.527826, -3538089391.748543, -7103111660.146708, 4019283237.3740463), (X'616772656561626C655F63756C74757265', X'73706563746163756C61725F657370616E61', 189387871.06959534, 6211851191.361202, 1786455196.9768047, 7966404387.318119);", "INSERT INTO super_becky VALUES (X'7068696C6F736F70686963616C5F6C656967686C616973', X'666162756C6F75735F73656D696E61746F7265', 8688321500.141502, -7855144036.024546, -5234949709.573349, -9937638367.366447), (X'617070726F61636861626C655F726F677565', X'676C65616D696E675F6D7574696E79', -5351540099.744092, -3614025150.9013805, -2327775310.276925, 2223379997.077526), (X'676C696D6D6572696E675F63617263686961', X'696D6167696E61746976655F61737379616E6E', 4104832554.8371887, -5531434716.627781, 1652773397.4099865, 3884980522.1830273);", "DELETE FROM super_becky WHERE (plucky_chai != X'7761726D686561727465645F6877616E67' AND mirthful_asbo != 9537234687.183533 AND bountiful_jon = -3538089391.748543);", "INSERT INTO super_becky VALUES (X'706C75636B795F6D617263616E74656C', X'696D6167696E61746976655F73696D73', 9535651632.375484, 92270815.0720501, 1299048084.6248207, 6460855331.572151), (X'726F6D616E7469635F706F746C61746368', X'68756D6F726F75735F63686165686F', 9345375719.265533, 7825332230.247925, -7133157299.39028, -6939677879.6597), (X'656666696369656E745F6261676E696E69', X'63726561746976655F67726168616D', -2615470560.1954746, 6790849074.977201, -8081732985.448849, -8133707792.312794), (X'677265676172696F75735F73637564', X'7368696E696E675F67726F7570', -7996394978.2610035, -9734939565.228964, 1108439333.8481388, -5420483517.169478), (X'6C696B61626C655F6B616E6176616C6368796B', X'636F75726167656F75735F7761726669656C64', -1959869609.656724, 4176668769.239971, -8423220404.063669, 9987687878.685959), (X'657863656C6C656E745F68696C6473646F74746572', X'676C6974746572696E675F7472616D7564616E61', -5220160777.908238, 3892402687.8826714, 9803857762.617172, -1065043714.0265541), (X'6D61676E69666963656E745F717565657273', X'73757065725F717565657273', -700932053.2006226, -4706306995.253335, -5286045811.046467, 1954345265.5250092), (X'676976696E675F6275636B65726D616E6E', X'667269656E646C795F70697A7A6F6C61746F', -2186859620.9089565, -6098492099.446075, -7456845586.405931, 8796967674.444252);", "DELETE FROM super_becky WHERE TRUE;", "INSERT INTO super_becky VALUES (X'6F7074696D69737469635F6368616E69616C', X'656E657267657469635F6E65677261', 1683345860.4208698, 4163199322.9289455, -4192968616.7868404, -7253371206.571701), (X'616C6C7572696E675F686176656C', X'7477696E6B6C696E675F626965627579636B', -9947019174.287437, 5975899640.893995, 3844707723.8570194, -9699970750.513876), (X'6F7074696D69737469635F7A686F75', X'616D626974696F75735F636F6E6772657373', 4143738484.1081524, -2138255286.170598, 9960750454.03466, 5840575852.80299), (X'73706563746163756C61725F6A6F6E67', X'73656E7369626C655F616269646F72', -1767611042.9716015, -7684260477.580351, 4570634429.188147, -9222640121.140202), (X'706F6C6974655F6B657272', X'696E736967687466756C5F63686F646F726B6F6666', -635016769.5123329, -4359901288.494518, -7531565119.905825, -1180410948.6572971), (X'666C657869626C655F636F6D756E69656C6C6F', X'6E6963655F6172636F73', 8708423014.802425, -6276712625.559328, -771680766.2485523, 8639486874.113342);", "DELETE FROM super_becky WHERE (mirthful_asbo < 9730384310.536528 AND plucky_chai < X'6E6963655F61726370B2');", "DELETE FROM super_becky WHERE (mirthful_asbo > 6248699554.426553 AND bountiful_jon > 4124481472.333034);", "INSERT INTO super_becky VALUES (X'676C696D6D6572696E675F77656C7368', X'64696C6967656E745F636F7262696E', 8217054003.369003, 8745594518.77864, 1928172803.2261295, -8375115534.050233), (X'616772656561626C655F6463', X'6C6F76696E675F666F72656D616E', -5483889804.871533, -8264576639.127487, 4770567289.404846, -3409172927.2573576), (X'6D617276656C6F75735F6173696D616B6F706F756C6F73', X'746563686E6F6C6F676963616C5F6A61637175696572', 2694858779.206814, -1703227425.3442516, -4504989231.263319, -3097265869.5230227), (X'73747570656E646F75735F64757075697364657269', X'68696C6172696F75735F6D75697268656164', 568174708.66469, -4878260547.265669, -9579691520.956625, 73507727.8100338), (X'626C69746865736F6D655F626C6F6B', X'61646570745F6C65696572', 7772117077.916897, 4590608571.321514, -881713470.657032, -9158405774.647465);", "INSERT INTO super_becky VALUES (X'6772697070696E675F6573736578', X'67656E65726F75735F636875726368696C6C', -4180431825.598956, 7277443000.677654, 2499796052.7878246, -2858339306.235305), (X'756E697175655F6D6172656368616C', X'62726561746874616B696E675F636875726368696C6C', 1401354536.7625294, -611427440.2796707, -4621650430.463729, 1531473111.7482872), (X'657863656C6C656E745F66696E6C6579', X'666169746866756C5F62726F636B', -4020697828.0073624, -2833530733.19637, -7766170050.654022, 8661820959.434689);", "INSERT INTO super_becky VALUES (X'756E697175655F6C617061797265', X'6C6F76696E675F7374617465', 7063237787.258968, -5425712581.365798, -7750509440.0141945, -7570954710.892544), (X'62726561746874616B696E675F6E65616C', X'636F75726167656F75735F61727269676F6E69', 289862394.2028198, 9690362375.014446, -4712463267.033899, 2474917855.0973473), (X'7477696E6B6C696E675F7368616B7572', X'636F75726167656F75735F636F6D6D6974746565', 5449035403.229155, -2159678989.597906, 3625606019.1150894, -3752010405.4475393);", "INSERT INTO super_becky VALUES (X'70617373696F6E6174655F73686970776179', X'686F6E6573745F7363687765697A6572', 4193384746.165228, -2232151704.896323, 8615245520.962444, -9789090953.995636);", "INSERT INTO super_becky VALUES (X'6C696B61626C655F69', X'6661766F7261626C655F6D626168', 6581403690.769894, 3260059398.9544716, -407118859.046051, -3155853965.2700634), (X'73696E636572655F6F72', X'616772656561626C655F617070656C6261756D', 9402938544.308651, -7595112171.758331, -7005316716.211025, -8368210960.419411);", "INSERT INTO super_becky VALUES (X'6D617276656C6F75735F6B61736864616E', X'6E6963655F636F7272', -5976459640.85817, -3177550476.2092276, 2073318650.736992, -1363247319.9978447);", "INSERT INTO super_becky VALUES (X'73706C656E6469645F6C616D656E646F6C61', X'677265676172696F75735F766F6E6E65677574', 6898259773.050102, 8973519699.707073, -25070632.280548096, -1845922497.9676847), (X'617765736F6D655F7365766572', X'656E657267657469635F706F746C61746368', -8750678407.717808, 5130907533.668898, -6778425327.111566, 3718982135.202587);", "INSERT INTO super_becky VALUES (X'70726F706974696F75735F6D616C617465737461', X'657863656C6C656E745F65766572657474', -8846855772.62094, -6168969732.697067, -8796372709.125793, 9983557891.544613), (X'73696E636572655F6C6177', X'696E7175697369746976655F73616E647374726F6D', -6366985697.975358, 3838628702.6652164, 3680621713.3371124, -786796486.8049564), (X'706F6C6974655F676C6561736F6E', X'706C75636B795F677579616E61', -3987946379.104308, -2119148244.413993, -1448660343.6888638, -1264195510.1611118), (X'676C6974746572696E675F6C6975', X'70657273697374656E745F6F6C6976696572', 6741779968.943846, -3239809989.227495, -1026074003.5506897, 4654600514.871752);", "DELETE FROM super_becky WHERE (engrossing_berger < X'6566651A3C70278D4E200657551D8071A1' AND competitive_petit > 1236742147.9451914);", "INSERT INTO super_becky VALUES (X'6661766F7261626C655F726569746D616E', X'64657465726D696E65645F726974746572', -7412553243.829927, -7572665195.290464, 7879603411.222157, 3706943306.5691853), (X'70657273697374656E745F6E6F6C616E', X'676C6974746572696E675F73686570617264', 7028261282.277422, -2064164782.3494844, -5244048504.507779, -2399526243.005843), (X'6B6E6F776C6564676561626C655F70617474656E', X'70726F66696369656E745F726F7365627261756768', 3713056763.583538, 3919834206.566164, -6306779387.430006, -9939464323.995546), (X'616461707461626C655F7172757A', X'696E7175697369746976655F68617261776179', 6519349690.299835, -9977624623.820414, 7500579325.440605, -8118341251.362242);", "INSERT INTO super_becky VALUES (X'636F6E73696465726174655F756E696F6E', X'6E6963655F6573736578', -1497385534.8720198, 9957688503.242973, 9191804202.566128, -179015615.7117195), (X'666169746866756C5F626F776C656773', X'6361707469766174696E675F6D6367697272', 893707300.1576138, 3381656294.246702, 6884723724.381908, 6248331214.701559), (X'6B6E6F776C6564676561626C655F70656E6E61', X'6B696E645F616A697468', -3335162603.6574974, 1812878172.8505402, 5115606679.658335, -5690100280.808182), (X'617765736F6D655F77696E7374616E6C6579', X'70726F706974696F75735F6361726173736F', -7395576292.503981, 4956546102.029215, -1468521769.7486448, -2968223925.60355), (X'636F75726167656F75735F77617266617265', X'74686F7567687466756C5F7361707068697265', 7052982930.566017, -9806098174.104418, -6910398936.377775, -4041963031.766964), (X'657863656C6C656E745F6B62', X'626C69746865736F6D655F666F75747A6F706F756C6F73', 6142173202.994768, 5193126957.544125, -7522202722.983735, -1659088056.594862), (X'7374756E6E696E675F6E6576616461', X'626F756E746966756C5F627572746F6E', -3822097036.7628613, -3458840259.240303, 2544472236.86788, 6928890176.466003);", "INSERT INTO super_becky VALUES (X'706572736F6E61626C655F646D69747269', X'776F6E64726F75735F6133796F', 2651932559.0077076, 811299402.3174248, -8271909238.671928, 6761098864.189909);", "INSERT INTO super_becky VALUES (X'726F7573696E675F6B6C6166657461', X'64617A7A6C696E675F6B6E617070', 9370628891.439335, -5923332007.253168, -2763161830.5880013, -9156194881.875952), (X'656666696369656E745F6C6576656C6C6572', X'616C6C7572696E675F706561636F7474', 3102641409.8314342, 2838360181.628153, 2466271662.169607, 1015942181.844162), (X'6469706C6F6D617469635F7065726B696E73', X'726F7573696E675F6172616269', -1551071129.022499, -8079487600.186886, 7832984580.070087, -6785993247.895652), (X'626F756E746966756C5F6D656D62657273', X'706F77657266756C5F70617269737369', 9226031830.72445, 7012021503.536997, -2297349030.108919, -2738320055.4710903), (X'676F7267656F75735F616E6172636F7469636F', X'68656C7066756C5F7765696C616E64', -8394163480.676959, -2978605095.699134, -6439355448.021704, 9137308022.281273), (X'616666656374696F6E6174655F70726F6C65696E666F', X'706C75636B795F73616E7A', 3546758708.3524914, -1870964264.9353771, 338752565.3643894, -3908023657.299715), (X'66756E6E795F706F70756C61697265', X'6F75747374616E64696E675F626576696E67746F6E', -1533858145.408224, 6164225076.710373, 8419445987.622173, 584555253.6852646), (X'76697669645F6D7474', X'7368696D6D6572696E675F70616F6E65737361', 5512251366.193035, -8680583180.123213, -4445968638.153208, -3274009935.4229546);", "INSERT INTO super_becky VALUES (X'7068696C6F736F70686963616C5F686F7264', X'657863656C6C656E745F67757373656C7370726F757473', -816909447.0240917, -3614686681.8786583, 7701617524.26067, -4541962047.183721), (X'616D6961626C655F69676E6174696576', X'6D61676E69666963656E745F70726F76696E6369616C69', -1318532883.847702, -4918966075.976474, -7601723171.33518, -3515747704.3847466), (X'70726F66696369656E745F32303137', X'66756E6E795F6E77', -1264540201.518032, 8227396547.578808, 6245093925.183641, -8368355328.110817);", "INSERT INTO super_becky VALUES (X'77696C6C696E675F6E6F6B6B65', X'726F6D616E7469635F677579616E61', 6618610796.3707695, -3814565359.1524105, 1663106272.4565296, -4175107840.768817), (X'72656C617865645F7061766C6F76', X'64657465726D696E65645F63686F646F726B6F6666', -3350029338.034504, -3520837855.4619064, 3375167499.631817, -8866806483.714607), (X'616D706C655F67696464696E6773', X'667269656E646C795F6A6F686E', 1458864959.9942684, 1344208968.0486107, 9335156635.91314, -6180643697.918882), (X'72656C617865645F6C65726F79', X'636F75726167656F75735F6E6F72646772656E', -5164986537.499656, 8820065797.720875, 6146530425.891005, 6949241471.958189), (X'666F63757365645F656D6D61', X'696D6167696E61746976655F6C6F6E67', -9587619060.80035, 6128068142.184402, 6765196076.956905, 800226302.7983418);", "INSERT INTO super_becky VALUES (X'616D626974696F75735F736F6E67', X'706572666563745F6761686D616E', 4989979180.706432, -9374266591.537058, 314459621.2820797, -3200029490.9553604), (X'666561726C6573735F626C6174', X'676C697374656E696E675F616374696F6E', -8512203612.903147, -7625581186.013805, -9711122307.234787, -301590929.32751083), (X'617765736F6D655F6669646573', X'666169746866756C5F63756E6E696E6768616D', -1428228887.9205084, 7669883854.400173, 5604446195.905277, -1509311057.9653416), (X'68756D6F726F75735F77697468647261776E', X'62726561746874616B696E675F7472617562656C', -7292778713.676636, -6728132503.529593, 2805341768.7252483, 330416975.2300949);", "INSERT INTO super_becky VALUES (X'677265676172696F75735F696873616E', X'7374656C6C61725F686172746D616E', 8819210651.1988, 5298459883.813452, 7293544377.958424, 460475869.72971725), (X'696E736967687466756C5F62657765726E69747A', X'676C65616D696E675F64656E736C6F77', -6911957282.193239, 1754196756.2193146, -6316860403.693853, -3094020672.236368), (X'6D6972746866756C5F616D6265727261656B656C6C79', X'68756D6F726F75735F6772617665', 1785574023.0269203, -372056983.82761574, 4133719439.9538956, 9374053482.066044), (X'76697669645F736169747461', X'7761726D686561727465645F696E656469746173', 2787071361.6099434, 9663839418.553448, -5934098589.901047, -9774745509.608858), (X'61646570745F6F6375727279', X'6C696B61626C655F726569746D616E', -3098540915.1310825, 5460848322.672174, -6012867197.519758, 6769770087.661135), (X'696E646570656E64656E745F6F', X'656C6567616E745F726F6F726461', 1462542860.3143978, 3360904654.2464733, 5458876201.665213, -5522844849.529962), (X'72656D61726B61626C655F626F6B616E69', X'6F70656E5F6D696E6465645F686F72726F78', 7589481760.867031, 7970075121.546291, 7513467575.5213585, 9663061478.289227), (X'636F6E666964656E745F6C616479', X'70617373696F6E6174655F736B726F7A6974736B79', 8266917234.53915, -7172933478.625412, 309854059.94031143, -8309837814.497616);", "DELETE FROM super_becky WHERE (competitive_petit != 8725256604.165474 OR engrossing_rexroth > -3607424615.7839313 OR plucky_chai < X'726F7573696E675F6216E20375');", "INSERT INTO super_becky VALUES (X'7368696E696E675F736F6C69646169726573', X'666561726C6573735F63617264616E', -170727879.20838165, 2744601113.384678, 5676912434.941502, 6757573601.657997), (X'636F75726167656F75735F706C616E636865', X'696E646570656E64656E745F636172736F6E', -6271723086.761938, -180566679.7470188, -1285774632.134449, 1359665735.7842407), (X'677265676172696F75735F7374616D61746F76', X'7374756E6E696E675F77696C64726F6F7473', -6210238866.953484, 2492683045.8287067, -9688894361.68205, 5420275482.048567), (X'696E646570656E64656E745F6F7267616E697A6572', X'676C6974746572696E675F736F72656C', 9291163783.3073, -6843003475.769236, -1320245894.772686, -5023483808.044955), (X'676C6F77696E675F6E65736963', X'676C65616D696E675F746F726D6579', 829526382.8027191, 9365690945.1316, 4761505764.826195, -4149154965.0024815), (X'616C6C7572696E675F646F637472696E65', X'6E6963655F636C6561766572', 3896644979.981762, -288600448.8016701, 9462856570.130062, -909633752.5993862);", ]; for query in queries { let mut stmt = conn.query(query).unwrap().unwrap(); loop { let row = stmt.step().expect("step"); match row { StepResult::Done => { break; } _ => { tracing::debug!("row {:?}", row); } } } } } #[test] pub fn test_free_space() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let header_size = 8; let usable_space = 4096; let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let payload = add_record(0, 0, page.clone(), record, &conn); let free = compute_free_space(page_contents, usable_space).unwrap(); assert_eq!(free, 4096 - payload.len() - 2 - header_size); } #[test] pub fn test_defragment_1() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let usable_space = 4096; let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let payload = add_record(0, 0, page.clone(), record, &conn); assert_eq!(page_contents.cell_count(), 1); defragment_page(page_contents, usable_space, 4).unwrap(); assert_eq!(page_contents.cell_count(), 1); let (start, len) = page_contents.cell_get_raw_region(0, usable_space).unwrap(); let buf = page_contents.as_ptr(); assert_eq!(&payload, &buf[start..start + len]); } #[test] pub fn test_insert_drop_insert() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let usable_space = 4096; let regs = &[ Register::Value(Value::from_i64(0)), Register::Value(Value::Text(Text::new("aaaaaaaa"))), ]; let record = ImmutableRecord::from_registers(regs, regs.len()); let _ = add_record(0, 0, page.clone(), record, &conn); assert_eq!(page_contents.cell_count(), 1); drop_cell(page_contents, 0, usable_space).unwrap(); assert_eq!(page_contents.cell_count(), 0); let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let payload = add_record(0, 0, page.clone(), record, &conn); assert_eq!(page_contents.cell_count(), 1); let (start, len) = page_contents.cell_get_raw_region(0, usable_space).unwrap(); let buf = page_contents.as_ptr(); assert_eq!(&payload, &buf[start..start + len]); } #[test] pub fn test_insert_drop_insert_multiple() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let usable_space = 4096; let regs = &[ Register::Value(Value::from_i64(0)), Register::Value(Value::Text(Text::new("aaaaaaaa"))), ]; let record = ImmutableRecord::from_registers(regs, regs.len()); let _ = add_record(0, 0, page.clone(), record, &conn); for _ in 0..100 { assert_eq!(page_contents.cell_count(), 1); drop_cell(page_contents, 0, usable_space).unwrap(); assert_eq!(page_contents.cell_count(), 0); let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let payload = add_record(0, 0, page.clone(), record, &conn); assert_eq!(page_contents.cell_count(), 1); let (start, len) = page_contents.cell_get_raw_region(0, usable_space).unwrap(); let buf = page_contents.as_ptr(); assert_eq!(&payload, &buf[start..start + len]); } } #[test] pub fn test_drop_a_few_insert() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let usable_space = 4096; let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let payload = add_record(0, 0, page.clone(), record, &conn); let regs = &[Register::Value(Value::from_i64(1))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let _ = add_record(1, 1, page.clone(), record, &conn); let regs = &[Register::Value(Value::from_i64(2))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let _ = add_record(2, 2, page.clone(), record, &conn); drop_cell(page_contents, 1, usable_space).unwrap(); drop_cell(page_contents, 1, usable_space).unwrap(); ensure_cell(page_contents, 0, &payload); } #[test] pub fn test_fuzz_victim_1() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let page_contents = page.get_contents(); let usable_space = 4096; let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let _ = add_record(0, 0, page.clone(), record, &conn); let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let _ = add_record(0, 0, page.clone(), record, &conn); drop_cell(page_contents, 0, usable_space).unwrap(); defragment_page(page_contents, usable_space, 4).unwrap(); let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let _ = add_record(0, 1, page.clone(), record, &conn); drop_cell(page_contents, 0, usable_space).unwrap(); let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let _ = add_record(0, 1, page.clone(), record, &conn); } #[test] pub fn test_fuzz_victim_2() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let usable_space = 4096; let insert = |pos, page| { let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let _ = add_record(0, pos, page, record, &conn); }; let drop = |pos, page| { drop_cell(page, pos, usable_space).unwrap(); }; let defragment = |page| { defragment_page(page, usable_space, 4).unwrap(); }; defragment(page.get_contents()); defragment(page.get_contents()); insert(0, page.clone()); drop(0, page.get_contents()); insert(0, page.clone()); drop(0, page.get_contents()); insert(0, page.clone()); defragment(page.get_contents()); defragment(page.get_contents()); drop(0, page.get_contents()); defragment(page.get_contents()); insert(0, page.clone()); drop(0, page.get_contents()); insert(0, page.clone()); insert(1, page.clone()); insert(1, page.clone()); insert(0, page.clone()); drop(3, page.get_contents()); drop(2, page.get_contents()); compute_free_space(page.get_contents(), usable_space).unwrap(); } #[test] pub fn test_fuzz_victim_3() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let usable_space = 4096; let insert = |pos, page| { let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let _ = add_record(0, pos, page, record, &conn); }; let drop = |pos, page| { drop_cell(page, pos, usable_space).unwrap(); }; let defragment = |page| { defragment_page(page, usable_space, 4).unwrap(); }; let regs = &[Register::Value(Value::from_i64(0))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let mut payload: Vec = Vec::new(); let mut fill_cell_payload_state = FillCellPayloadState::Start; run_until_done( || { fill_cell_payload( &PinGuard::new(page.clone()), Some(0), &mut payload, 0, &record, 4096, conn.pager.load().clone(), &mut fill_cell_payload_state, ) }, &conn.pager.load().clone(), ) .unwrap(); insert(0, page.clone()); defragment(page.get_contents()); insert(0, page.clone()); defragment(page.get_contents()); insert(0, page.clone()); drop(2, page.get_contents()); drop(0, page.get_contents()); let free = compute_free_space(page.get_contents(), usable_space).unwrap(); let total_size = payload.len() + 2; assert_eq!( free, usable_space - page.get_contents().header_size() - total_size ); dbg!(free); } #[test] pub fn btree_insert_sequential() { let (pager, root_page, _, _) = empty_btree(); let mut keys = Vec::new(); let num_columns = 5; for i in 0..10000 { let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); tracing::info!("INSERT INTO t VALUES ({});", i,); let regs = &[Register::Value(Value::from_i64(i))]; let value = ImmutableRecord::from_registers(regs, regs.len()); tracing::trace!("before insert {}", i); run_until_done( || { let key = SeekKey::TableRowId(i); cursor.seek(key, SeekOp::GE { eq_only: true }) }, pager.deref(), ) .unwrap(); run_until_done( || cursor.insert(&BTreeKey::new_table_rowid(i, Some(&value))), pager.deref(), ) .unwrap(); keys.push(i); } if matches!(validate_btree(pager.clone(), root_page), (_, false)) { panic!("invalid btree"); } tracing::trace!( "=========== btree ===========\n{}\n\n", format_btree(pager.clone(), root_page, 0) ); for key in keys.iter() { let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); let key = Value::from_i64(*key); let exists = run_until_done(|| cursor.exists(&key), pager.deref()).unwrap(); assert!(exists, "key not found {key}"); } } #[test] pub fn test_big_payload_compute_free() { let db = get_database(); let conn = db.connect().unwrap(); let page = get_page(2); let usable_space = 4096; let regs = &[Register::Value(Value::Blob(vec![0; 3600]))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let mut payload: Vec = Vec::new(); let mut fill_cell_payload_state = FillCellPayloadState::Start; run_until_done( || { fill_cell_payload( &PinGuard::new(page.clone()), Some(0), &mut payload, 0, &record, 4096, conn.pager.load().clone(), &mut fill_cell_payload_state, ) }, &conn.pager.load().clone(), ) .unwrap(); insert_into_cell(page.get_contents(), &payload, 0, 4096).unwrap(); let free = compute_free_space(page.get_contents(), usable_space).unwrap(); let total_size = payload.len() + 2; assert_eq!( free, usable_space - page.get_contents().header_size() - total_size ); dbg!(free); } #[test] pub fn test_delete_balancing() { // What does this test do: // 1. Insert 10,000 rows of ~15 byte payload each. This creates // nearly 40 pages (10,000 * 15 / 4096) and 240 rows per page. // 2. Delete enough rows to create empty/ nearly empty pages to trigger balancing // (verified this in SQLite). // 3. Verify validity/integrity of btree after deleting and also verify that these // values are actually deleted. let (pager, root_page, _, _) = empty_btree(); let num_columns = 5; // Insert 10,000 records in to the BTree. for i in 1..=10000 { let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); let regs = &[Register::Value(Value::Text(Text::new("hello world")))]; let value = ImmutableRecord::from_registers(regs, regs.len()); run_until_done( || { let key = SeekKey::TableRowId(i); cursor.seek(key, SeekOp::GE { eq_only: true }) }, pager.deref(), ) .unwrap(); run_until_done( || cursor.insert(&BTreeKey::new_table_rowid(i, Some(&value))), pager.deref(), ) .unwrap(); } if let (_, false) = validate_btree(pager.clone(), root_page) { panic!("Invalid B-tree after insertion"); } let num_columns = 5; // Delete records with 500 <= key <= 3500 for i in 500..=3500 { let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); let seek_key = SeekKey::TableRowId(i); let seek_result = run_until_done( || cursor.seek(seek_key.clone(), SeekOp::GE { eq_only: true }), pager.deref(), ) .unwrap(); if matches!(seek_result, SeekResult::Found) { run_until_done(|| cursor.delete(), pager.deref()).unwrap(); } } // Verify that records with key < 500 and key > 3500 still exist in the BTree. for i in 1..=10000 { if (500..=3500).contains(&i) { continue; } let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); let key = Value::from_i64(i); let exists = run_until_done(|| cursor.exists(&key), pager.deref()).unwrap(); assert!(exists, "Key {i} should exist but doesn't"); } // Verify the deleted records don't exist. for i in 500..=3500 { let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); let key = Value::from_i64(i); let exists = run_until_done(|| cursor.exists(&key), pager.deref()).unwrap(); assert!(!exists, "Deleted key {i} still exists"); } } #[test] pub fn test_overflow_cells() { let iterations = 10_usize; let mut huge_texts = Vec::new(); for i in 0..iterations { let mut huge_text = String::new(); for _j in 0..8192 { huge_text.push((b'A' + i as u8) as char); } huge_texts.push(huge_text); } let (pager, root_page, _, _) = empty_btree(); let num_columns = 5; for (i, huge_text) in huge_texts.iter().enumerate().take(iterations) { let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); tracing::info!("INSERT INTO t VALUES ({});", i,); let regs = &[Register::Value(Value::Text(Text::new(huge_text.clone())))]; let value = ImmutableRecord::from_registers(regs, regs.len()); tracing::trace!("before insert {}", i); tracing::debug!( "=========== btree before ===========\n{}\n\n", format_btree(pager.clone(), root_page, 0) ); run_until_done( || { let key = SeekKey::TableRowId(i as i64); cursor.seek(key, SeekOp::GE { eq_only: true }) }, pager.deref(), ) .unwrap(); run_until_done( || cursor.insert(&BTreeKey::new_table_rowid(i as i64, Some(&value))), pager.deref(), ) .unwrap(); tracing::debug!( "=========== btree after ===========\n{}\n\n", format_btree(pager.clone(), root_page, 0) ); } let mut cursor = BTreeCursor::new_table(pager.clone(), root_page, num_columns); let _c = cursor.move_to_root().unwrap(); for i in 0..iterations { run_until_done(|| cursor.next(), pager.deref()).unwrap(); let has_next = cursor.has_record(); if !has_next { panic!("expected Some(rowid) but got {:?}", cursor.has_record()); }; let rowid = run_until_done(|| cursor.rowid(), pager.deref()) .unwrap() .unwrap(); assert_eq!(rowid, i as i64, "got!=expected"); } } fn run_until_done(action: impl FnMut() -> Result>, pager: &Pager) -> Result { pager.io.block(action) } #[test] fn test_free_array() { let (mut rng, seed) = rng_from_time_or_env(); tracing::info!("seed={}", seed); const ITERATIONS: usize = 10000; for _ in 0..ITERATIONS { let mut cell_array = CellArray { cell_payloads: Vec::new(), cell_count_per_page_cumulative: [0; MAX_NEW_SIBLING_PAGES_AFTER_BALANCE], }; let mut cells_cloned = Vec::new(); let (pager, _, _, _) = empty_btree(); let page_type = PageType::TableLeaf; let page = run_until_done(|| pager.allocate_page(), &pager).unwrap(); btree_init_page(&page, page_type, 0, pager.usable_space()); let mut size = (rng.next_u64() % 100) as u16; let mut i = 0; // add a bunch of cells while compute_free_space(page.get_contents(), pager.usable_space()).unwrap() >= size as usize + 10 { insert_cell(i, size, page.clone(), pager.clone()); i += 1; size = (rng.next_u64() % 1024) as u16; } // Create cell array with references to cells inserted let contents = page.get_contents(); for cell_idx in 0..contents.cell_count() { let buf = contents.as_ptr(); let (start, len) = contents .cell_get_raw_region(cell_idx, pager.usable_space()) .unwrap(); cell_array .cell_payloads .push(to_static_buf(&mut buf[start..start + len])); cells_cloned.push(buf[start..start + len].to_vec()); } debug_validate_cells!(contents, pager.usable_space()); // now free a prefix or suffix of cells added let cells_before_free = contents.cell_count(); let size = rng.next_u64() as usize % cells_before_free; let prefix = rng.next_u64() % 2 == 0; let start = if prefix { 0 } else { contents.cell_count() - size }; let removed = page_free_array(contents, start, size, &cell_array, pager.usable_space()).unwrap(); // shift if needed if prefix { shift_cells_left(contents, cells_before_free, removed); } assert_eq!(removed, size); assert_eq!(contents.cell_count(), cells_before_free - size); #[cfg(debug_assertions)] debug_validate_cells_core(contents, pager.usable_space()); // check cells are correct let mut cell_idx_cloned = if prefix { size } else { 0 }; for cell_idx in 0..contents.cell_count() { let buf = contents.as_ptr(); let (start, len) = contents .cell_get_raw_region(cell_idx, pager.usable_space()) .unwrap(); let cell_in_page = &buf[start..start + len]; let cell_in_array = &cells_cloned[cell_idx_cloned]; assert_eq!(cell_in_page, cell_in_array); cell_idx_cloned += 1; } } } fn insert_cell(cell_idx: u64, size: u16, page: PageRef, pager: Arc) { let mut payload = Vec::new(); let regs = &[Register::Value(Value::Blob(vec![0; size as usize]))]; let record = ImmutableRecord::from_registers(regs, regs.len()); let mut fill_cell_payload_state = FillCellPayloadState::Start; let contents = page.get_contents(); run_until_done( || { fill_cell_payload( &PinGuard::new(page.clone()), Some(cell_idx as i64), &mut payload, cell_idx as usize, &record, pager.usable_space(), pager.clone(), &mut fill_cell_payload_state, ) }, &pager, ) .unwrap(); insert_into_cell(contents, &payload, cell_idx as usize, pager.usable_space()).unwrap(); } /// Strict property tests for page-level btree mutations. /// /// These tests model expected cell bytes and check that every mutation /// preserves both byte-level payload contents and page-layout invariants. mod property_tests { use std::collections::HashSet; use quickcheck::{quickcheck, TestResult}; use crate::storage::btree::{ compute_free_space, defragment_page, drop_cell, insert_into_cell, }; use crate::storage::sqlite3_ondisk::{write_varint, PageContent, CELL_PTR_SIZE_BYTES}; use crate::PageRef; use super::get_page; const PAGE_SIZE: usize = 4096; const MIN_INSERTED_CELLS: usize = 6; struct FillOutcome { expected: Vec>, had_middle_insert: bool, } /// Convert arbitrary fuzz bytes into bounded payload sizes that are small enough /// to produce many cells and varied freeblock behavior in a single page. fn normalize_sizes(raw: &[u8]) -> Vec { raw.iter() .take(300) .map(|v| ((*v as usize) % 220) + 1) .collect() } /// Validate strict page invariants after each mutation. /// /// Checks: /// - pointer array/header consistency: /// `unallocated_region_start` must equal /// `cell_pointer_array_offset + (cell_count * 2)`, so the header and pointer-array /// metadata agree on where unallocated space begins. /// - structural bounds: /// every cell and freeblock must lie fully within `[cell_content_area, usable_space)`. /// - pointer uniqueness: /// no two cell-pointer entries may reference the same cell start offset. /// - interval non-overlap: /// cell byte ranges and freeblock ranges must be disjoint; overlap means corruption. /// - freeblock chain validity: /// the linked list must be strictly ascending by offset and must not contain cycles. /// - accounting equality: /// independently computed free space from layout pieces must exactly equal /// `compute_free_space(page, usable_space)`. /// - logical data preservation (optional): /// when an expected model is provided, each on-page cell must match expected bytes /// at the same logical index. fn strict_validate_page( page: &PageContent, usable_space: usize, expected_cells: Option<&[Vec]>, ) { let cell_count = page.cell_count(); let cell_content_area = page.cell_content_area() as usize; let unallocated_start = page.unallocated_region_start(); let ptr_start = page.cell_pointer_array_offset(); let expected_unallocated_start = ptr_start + (cell_count * CELL_PTR_SIZE_BYTES); assert_eq!( unallocated_start, expected_unallocated_start, "unallocated region start inconsistent with cell pointer array" ); assert!( unallocated_start <= cell_content_area, "cell pointer array overlaps cell content area" ); assert!( cell_content_area <= usable_space, "cell content area beyond usable space" ); assert!( page.num_frag_free_bytes() <= 60, "fragmented free bytes exceed SQLite limit" ); let mut intervals = Vec::<(usize, usize, &'static str)>::new(); let mut ptrs = HashSet::new(); for i in 0..cell_count { let ptr_offset = ptr_start + (i * CELL_PTR_SIZE_BYTES); let raw_ptr = page.read_u16_no_offset(ptr_offset) as usize; let (start, len) = page.cell_get_raw_region(i, usable_space).unwrap(); assert_eq!( raw_ptr, start, "cell pointer does not match parsed cell start" ); assert!(len >= 2, "cell too small"); assert!( start >= cell_content_area, "cell starts before cell content area" ); assert!( start + len <= usable_space, "cell extends beyond usable space" ); assert!(ptrs.insert(raw_ptr), "duplicate cell pointer"); intervals.push((start, start + len, "cell")); } let mut freeblock_total = 0usize; let mut seen_freeblocks = HashSet::new(); let mut cur = page.first_freeblock() as usize; let mut prev = 0usize; while cur != 0 { assert!( seen_freeblocks.insert(cur), "freeblock cycle detected at offset {cur}" ); assert!( cur >= cell_content_area, "freeblock before cell content area" ); assert!(cur + 4 <= usable_space, "freeblock header out of bounds"); let (next, size_u16) = page.read_freeblock(cur as u16); let size = size_u16 as usize; assert!(size >= 4, "freeblock size too small"); assert!( cur + size <= usable_space, "freeblock extends beyond usable space" ); if prev != 0 { assert!(cur > prev, "freeblocks must be strictly ascending"); } let next_usize = next as usize; if next_usize != 0 { assert!(next_usize > cur, "freeblock next pointer not ascending"); } intervals.push((cur, cur + size, "freeblock")); freeblock_total += size; prev = cur; cur = next_usize; } intervals.sort_by_key(|(start, _, _)| *start); for pair in intervals.windows(2) { let (a_start, a_end, a_kind) = pair[0]; let (b_start, _b_end, b_kind) = pair[1]; assert!( a_end <= b_start, "interval overlap: {a_kind}@{a_start}..{a_end} overlaps {b_kind}@{b_start}" ); } let computed = compute_free_space(page, usable_space).unwrap(); let expected_free = (cell_content_area - unallocated_start) + page.num_frag_free_bytes() as usize + freeblock_total; assert_eq!( computed, expected_free, "compute_free_space mismatch: computed={computed}, expected={expected_free}" ); if let Some(expected_cells) = expected_cells { assert_eq!( cell_count, expected_cells.len(), "cell count mismatch against expected model" ); for (i, expected) in expected_cells.iter().enumerate() { let (start, len) = page.cell_get_raw_region(i, usable_space).unwrap(); let actual = &page.as_ptr()[start..start + len]; assert_eq!( actual, expected.as_slice(), "cell bytes mismatch at idx {i}" ); } } } /// Build a valid table-leaf cell: /// [payload_size varint][rowid varint][record(header + blob data)]. /// /// The body is synthetic but stable, so byte-level equality checks are deterministic. fn make_table_leaf_cell(rowid: u64, data_size: usize) -> Vec { let mut cell = Vec::new(); let serial_type = (data_size as u64) * 2 + 12; let mut header_buf = [0u8; 9]; let mut serial_buf = [0u8; 9]; let serial_len = write_varint(&mut serial_buf, serial_type); let header_size = 1 + serial_len; let header_size_len = write_varint(&mut header_buf, header_size as u64); let mut record = Vec::new(); record.extend_from_slice(&header_buf[..header_size_len]); record.extend_from_slice(&serial_buf[..serial_len]); record.extend(vec![0xAB; data_size]); let payload_size = record.len() as u64; let mut payload_size_buf = [0u8; 9]; let payload_size_len = write_varint(&mut payload_size_buf, payload_size); cell.extend_from_slice(&payload_size_buf[..payload_size_len]); let mut rowid_buf = [0u8; 9]; let rowid_len = write_varint(&mut rowid_buf, rowid); cell.extend_from_slice(&rowid_buf[..rowid_len]); cell.extend_from_slice(&record); cell } /// Execute a modeled insertion workload against one page. /// /// For each insert that fits, mutate both: /// - the real page (via `insert_into_cell`), and /// - the expected model vector at the same index. /// /// `had_middle_insert` ensures we exercised pointer-shift paths, not only appends. fn fill_page_with_model( page: &PageRef, cell_sizes: &[usize], insert_hints: &[u8], ) -> FillOutcome { let mut expected = Vec::new(); let mut had_middle_insert = false; let contents = page.get_contents(); for (i, size) in cell_sizes.iter().copied().enumerate() { let cell = make_table_leaf_cell(i as u64, size); let free = compute_free_space(contents, PAGE_SIZE).unwrap(); if cell.len() + CELL_PTR_SIZE_BYTES > free { continue; } let idx = if expected.is_empty() { 0 } else { insert_hints.get(i).copied().unwrap_or(i as u8) as usize % (expected.len() + 1) }; if idx < expected.len() { had_middle_insert = true; } insert_into_cell(contents, &cell, idx, PAGE_SIZE).unwrap(); expected.insert(idx, cell); strict_validate_page(contents, PAGE_SIZE, Some(&expected)); } FillOutcome { expected, had_middle_insert, } } quickcheck! { // Invariant: arbitrary insert sequences (including middle inserts) preserve exact cell bytes // and keep page layout/accounting valid after every insertion. fn prop_insertions_preserve_exact_cell_bytes( raw_sizes: Vec, insert_hints: Vec ) -> TestResult { // Build many small payload sizes from random bytes so one page gets many edits. let cell_sizes = normalize_sizes(&raw_sizes); if cell_sizes.len() < MIN_INSERTED_CELLS { return TestResult::discard(); } let page = get_page(2); // Mutate both the real page and the expected-model vector in lock-step. let outcome = fill_page_with_model(&page, &cell_sizes, &insert_hints); // Require enough inserts and at least one middle insert (not append-only). if outcome.expected.len() < MIN_INSERTED_CELLS || !outcome.had_middle_insert { return TestResult::discard(); } // Final strict check: metadata + free-space accounting + exact cell bytes. strict_validate_page(page.get_contents(), PAGE_SIZE, Some(&outcome.expected)); TestResult::passed() } } quickcheck! { // Invariant: every drop operation removes exactly one modeled cell, never mutates surviving // cell bytes, and always preserves freeblock/pointer/free-space structural validity. fn prop_drop_sequence_preserves_model_and_layout( raw_sizes: Vec, insert_hints: Vec, drop_ops: Vec ) -> TestResult { if drop_ops.is_empty() { return TestResult::discard(); } // Start from a non-trivial page state built with randomized inserts. let page = get_page(2); let cell_sizes = normalize_sizes(&raw_sizes); let mut outcome = fill_page_with_model(&page, &cell_sizes, &insert_hints); if outcome.expected.len() < MIN_INSERTED_CELLS || !outcome.had_middle_insert { return TestResult::discard(); } let contents = page.get_contents(); let mut drops_executed = 0usize; for op in drop_ops.iter().take(200) { // Stop once the model is empty; there is nothing left to drop. if outcome.expected.is_empty() { break; } // Drop same logical index in model and real page. let idx = (*op as usize) % outcome.expected.len(); outcome.expected.remove(idx); drop_cell(contents, idx, PAGE_SIZE).unwrap(); // After each mutation, validate structure and surviving bytes immediately. strict_validate_page(contents, PAGE_SIZE, Some(&outcome.expected)); drops_executed += 1; } if drops_executed == 0 { // Require at least one real mutation, otherwise this run is not informative. return TestResult::discard(); } TestResult::passed() } } quickcheck! { // Invariant: after creating holes via drops, inserting new cells back into the page // preserves all existing bytes and keeps freeblock reuse/allocation safe. fn prop_insert_drop_insert_reuses_space_safely( raw_sizes: Vec, insert_hints: Vec, drop_ops: Vec, new_sizes: Vec, new_insert_hints: Vec ) -> TestResult { if drop_ops.is_empty() || new_sizes.is_empty() { return TestResult::discard(); } let page = get_page(2); let cell_sizes = normalize_sizes(&raw_sizes); let mut outcome = fill_page_with_model(&page, &cell_sizes, &insert_hints); if outcome.expected.len() < MIN_INSERTED_CELLS { return TestResult::discard(); } let contents = page.get_contents(); let mut drops_executed = 0usize; // Phase 1: create holes and freeblocks by dropping cells in random positions. for op in drop_ops.iter().take(16) { if outcome.expected.len() <= 2 { break; } let idx = (*op as usize) % outcome.expected.len(); outcome.expected.remove(idx); drop_cell(contents, idx, PAGE_SIZE).unwrap(); strict_validate_page(contents, PAGE_SIZE, Some(&outcome.expected)); drops_executed += 1; } if drops_executed == 0 { return TestResult::discard(); } let base_rowid = 1_000_000u64 + outcome.expected.len() as u64; let mut inserted = 0usize; // Phase 2: insert new cells back, forcing allocator/freeblock reuse paths. for (i, raw) in new_sizes.iter().take(32).enumerate() { let size = ((*raw as usize) % 220) + 1; let cell = make_table_leaf_cell(base_rowid + i as u64, size); let free = compute_free_space(contents, PAGE_SIZE).unwrap(); if cell.len() + CELL_PTR_SIZE_BYTES > free { continue; } let idx = if outcome.expected.is_empty() { 0 } else { new_insert_hints .get(i) .copied() .unwrap_or(i as u8) as usize % (outcome.expected.len() + 1) }; insert_into_cell(contents, &cell, idx, PAGE_SIZE).unwrap(); outcome.expected.insert(idx, cell); strict_validate_page(contents, PAGE_SIZE, Some(&outcome.expected)); inserted += 1; } if inserted == 0 { // Require at least one successful re-insert to exercise the target path. return TestResult::discard(); } TestResult::passed() } } quickcheck! { // Invariant: full defragmentation is lossless (all live cell bytes unchanged), reaches canonical // no-freeblock/no-fragment state, and is idempotent when applied repeatedly. fn prop_defragment_is_lossless_and_idempotent( raw_sizes: Vec, insert_hints: Vec, drop_ops: Vec ) -> TestResult { if drop_ops.is_empty() { return TestResult::discard(); } let page = get_page(2); let cell_sizes = normalize_sizes(&raw_sizes); let mut outcome = fill_page_with_model(&page, &cell_sizes, &insert_hints); if outcome.expected.len() < MIN_INSERTED_CELLS { // Need enough cells so one drop still leaves a meaningful page state. return TestResult::discard(); } let contents = page.get_contents(); let mut drops_executed = 0usize; for op in drop_ops.iter().take(40) { if outcome.expected.len() <= 1 { break; } // Create realistic holes/freeblocks before defragmenting. let idx = (*op as usize) % outcome.expected.len(); outcome.expected.remove(idx); drop_cell(contents, idx, PAGE_SIZE).unwrap(); strict_validate_page(contents, PAGE_SIZE, Some(&outcome.expected)); drops_executed += 1; } if drops_executed == 0 || outcome.expected.is_empty() { return TestResult::discard(); } // First defrag: must preserve live cells and clean freeblock/fragment metadata. defragment_page(contents, PAGE_SIZE, -1).unwrap(); strict_validate_page(contents, PAGE_SIZE, Some(&outcome.expected)); assert_eq!(contents.first_freeblock(), 0, "freeblocks remain after defrag"); assert_eq!(contents.num_frag_free_bytes(), 0, "fragments remain after defrag"); // Second defrag should be a no-op on bytes (idempotence). let snapshot_after_first = contents.as_ptr().to_vec(); defragment_page(contents, PAGE_SIZE, -1).unwrap(); strict_validate_page(contents, PAGE_SIZE, Some(&outcome.expected)); assert_eq!( contents.as_ptr().to_vec(), snapshot_after_first, "defragmentation is not idempotent" ); TestResult::passed() } } quickcheck! { // Invariant: for simple freeblock layouts where fast-path is applicable, fast defrag and // full defrag produce the same logical page state and identical serialized cell bytes. fn prop_defragment_fast_matches_full( raw_sizes: Vec, insert_hints: Vec, drop_op: u8 ) -> TestResult { let cell_sizes = normalize_sizes(&raw_sizes); if cell_sizes.len() < MIN_INSERTED_CELLS { return TestResult::discard(); } let page_fast = get_page(2); let mut outcome = fill_page_with_model(&page_fast, &cell_sizes, &insert_hints); if outcome.expected.len() < MIN_INSERTED_CELLS { return TestResult::discard(); } // Clone logical state to second page so both start identical. let page_full = get_page(3); let full_contents = page_full.get_contents(); for (i, cell) in outcome.expected.iter().enumerate() { insert_into_cell(full_contents, cell, i, PAGE_SIZE).unwrap(); } strict_validate_page(full_contents, PAGE_SIZE, Some(&outcome.expected)); // Create a single hole => one freeblock, making fast-path eligibility likely. let idx = drop_op as usize % outcome.expected.len(); let fast_contents = page_fast.get_contents(); outcome.expected.remove(idx); drop_cell(fast_contents, idx, PAGE_SIZE).unwrap(); drop_cell(full_contents, idx, PAGE_SIZE).unwrap(); strict_validate_page(fast_contents, PAGE_SIZE, Some(&outcome.expected)); strict_validate_page(full_contents, PAGE_SIZE, Some(&outcome.expected)); // Try fast-path on one page and force full-path on the other. defragment_page(fast_contents, PAGE_SIZE, 4).unwrap(); defragment_page(full_contents, PAGE_SIZE, -1).unwrap(); // Both algorithms must preserve the exact same logical model. strict_validate_page(fast_contents, PAGE_SIZE, Some(&outcome.expected)); strict_validate_page(full_contents, PAGE_SIZE, Some(&outcome.expected)); assert_eq!(fast_contents.cell_count(), full_contents.cell_count()); assert_eq!( compute_free_space(fast_contents, PAGE_SIZE).unwrap(), compute_free_space(full_contents, PAGE_SIZE).unwrap() ); assert_eq!(fast_contents.first_freeblock(), full_contents.first_freeblock()); assert_eq!( fast_contents.num_frag_free_bytes(), full_contents.num_frag_free_bytes() ); for i in 0..fast_contents.cell_count() { let (s1, l1) = fast_contents.cell_get_raw_region(i, PAGE_SIZE).unwrap(); let (s2, l2) = full_contents.cell_get_raw_region(i, PAGE_SIZE).unwrap(); assert_eq!(l1, l2, "cell {i} length mismatch after defragmentation"); assert_eq!( &fast_contents.as_ptr()[s1..s1 + l1], &full_contents.as_ptr()[s2..s2 + l2], "cell {i} bytes mismatch between fast and full defrag" ); } TestResult::passed() } } quickcheck! { // Invariant: dropping all cells and defragmenting returns the page to a canonical empty state // (zero cells, no fragments/freeblocks, content area at end, exact free-space accounting). fn prop_drop_all_then_defrag_returns_canonical_empty_page( raw_sizes: Vec, insert_hints: Vec ) -> TestResult { let page = get_page(2); let cell_sizes = normalize_sizes(&raw_sizes); let mut outcome = fill_page_with_model(&page, &cell_sizes, &insert_hints); if outcome.expected.len() < MIN_INSERTED_CELLS { return TestResult::discard(); } let contents = page.get_contents(); while !outcome.expected.is_empty() { // Repeatedly drop from the logical front so model and page stay aligned. outcome.expected.remove(0); drop_cell(contents, 0, PAGE_SIZE).unwrap(); strict_validate_page(contents, PAGE_SIZE, Some(&outcome.expected)); } // After all cells are gone, defrag should normalize page to canonical empty form. defragment_page(contents, PAGE_SIZE, -1).unwrap(); strict_validate_page(contents, PAGE_SIZE, Some(&[])); assert_eq!(contents.cell_count(), 0); assert_eq!(contents.first_freeblock(), 0); assert_eq!(contents.num_frag_free_bytes(), 0); assert_eq!(contents.cell_content_area() as usize, PAGE_SIZE); assert_eq!( compute_free_space(contents, PAGE_SIZE).unwrap(), PAGE_SIZE - contents.header_size(), "empty page must expose full free space minus header" ); TestResult::passed() } } } /// Corruption-handling properties. /// /// These tests verify that malformed on-page metadata is rejected with /// corruption errors, instead of silently succeeding or panicking. mod corruption_properties { use quickcheck::quickcheck; use crate::storage::btree::{compute_free_space, defragment_page, insert_into_cell}; use crate::storage::sqlite3_ondisk::write_varint; use super::get_page; const PAGE_SIZE: usize = 4096; fn make_table_leaf_cell(rowid: u64, data_size: usize) -> Vec { let mut cell = Vec::new(); let serial_type = (data_size as u64) * 2 + 12; let mut header_buf = [0u8; 9]; let mut serial_buf = [0u8; 9]; let serial_len = write_varint(&mut serial_buf, serial_type); let header_size = 1 + serial_len; let header_size_len = write_varint(&mut header_buf, header_size as u64); let mut record = Vec::new(); record.extend_from_slice(&header_buf[..header_size_len]); record.extend_from_slice(&serial_buf[..serial_len]); record.extend(vec![0xCC; data_size]); let payload_size = record.len() as u64; let mut payload_size_buf = [0u8; 9]; let payload_size_len = write_varint(&mut payload_size_buf, payload_size); cell.extend_from_slice(&payload_size_buf[..payload_size_len]); let mut rowid_buf = [0u8; 9]; let rowid_len = write_varint(&mut rowid_buf, rowid); cell.extend_from_slice(&rowid_buf[..rowid_len]); cell.extend_from_slice(&record); cell } quickcheck! { // Desired invariant: malformed freeblock pointer values should return Corrupt errors. fn prop_compute_free_space_returns_err_when_first_freeblock_is_invalid(seed: u16) -> bool { let page = get_page(2); let contents = page.get_contents(); let bad_ptr = ((seed as usize % (PAGE_SIZE - 1)) + 1) as u16; // 1..=4095, always < initial cell_content_area (4096) contents.write_first_freeblock(bad_ptr); compute_free_space(contents, PAGE_SIZE).is_err() } } quickcheck! { // Desired invariant: malformed freeblock chain ordering should return Corrupt errors. fn prop_compute_free_space_returns_err_on_malformed_freeblock_chain(seed: u16) -> bool { let page = get_page(2); let contents = page.get_contents(); // Move content area left so freeblocks can exist "inside content area". contents.write_cell_content_area(64); // Create one freeblock whose "next" pointer violates ordering assumptions. let base = 128 + (seed as usize % (PAGE_SIZE - 256)); let cur = base as u16; let next = (base + 1) as u16; // intentionally invalid relative to size constraints contents.write_first_freeblock(cur); contents.write_freeblock(cur, 8, Some(next)); compute_free_space(contents, PAGE_SIZE).is_err() } } quickcheck! { // Desired invariant: malformed freeblock metadata should return Corrupt errors. fn prop_defragment_returns_err_on_malformed_freeblock_chain(seed: u8) -> bool { let page = get_page(2); let contents = page.get_contents(); // Ensure page is non-empty so defragmentation doesn't early-return. let cell = make_table_leaf_cell(1, (seed as usize % 24) + 1); if insert_into_cell(contents, &cell, 0, PAGE_SIZE).is_err() { return true; } // Construct malformed chain: first freeblock points "backwards". contents.write_first_freeblock(100); contents.write_freeblock(100, 8, Some(90)); defragment_page(contents, PAGE_SIZE, 4).is_err() } } } } ================================================ FILE: core/storage/buffer_pool.rs ================================================ use branches::unlikely; use super::{slot_bitmap::AtomicSlotBitmap, sqlite3_ondisk::WAL_FRAME_HEADER_SIZE}; use crate::io::TEMP_BUFFER_CACHE; use crate::sync::atomic::{AtomicUsize, Ordering}; use crate::sync::Arc; use crate::turso_assert; use crate::{Buffer, LimboError, IO}; use std::cell::UnsafeCell; use std::ptr::NonNull; use std::sync::OnceLock; #[derive(Debug)] /// A buffer allocated from an arena from `[BufferPool]` pub struct ArenaBuffer { /// The `Arena` the buffer came from arena: Arc, /// Pointer to the start of the buffer ptr: NonNull, /// Identifier for the `[Arena]` the buffer came from arena_id: u32, /// The index of the first slot making up the buffer slot_idx: u32, /// The requested length of the allocation. /// For pooled buffers, `len` is always `<= Arena::slot_size` and occupies exactly one slot. len: usize, } // Unsound: write and read from different threads can be dangerous with current ArenaBuffer implementation without some additional explicit synchronization unsafe impl Sync for ArenaBuffer {} unsafe impl Send for ArenaBuffer {} crate::assert::assert_send_sync!(ArenaBuffer); impl ArenaBuffer { fn new(arena: Arc, ptr: NonNull, len: usize, arena_id: u32, slot_idx: u32) -> Self { ArenaBuffer { arena, ptr, arena_id, slot_idx, len, } } #[inline(always)] /// Returns the `id` of the underlying arena, only if it was registered with `io_uring` pub const fn fixed_id(&self) -> Option { // Arenas which are not registered will have `id`s <= UNREGISTERED_START if self.arena_id < UNREGISTERED_START { Some(self.arena_id) } else { None } } /// The requested size of the allocation, the actual size of the underlying buffer is rounded up to /// the arena's slot_size (and in practice is always `<= slot_size` for pooled buffers). pub const fn logical_len(&self) -> usize { self.len } pub fn as_slice(&self) -> &[u8] { unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.logical_len()) } } pub fn as_mut_slice(&mut self) -> &mut [u8] { unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.logical_len()) } } } impl Drop for ArenaBuffer { fn drop(&mut self) { self.arena.free(self.slot_idx, self.logical_len()); } } impl std::ops::Deref for ArenaBuffer { type Target = [u8]; fn deref(&self) -> &Self::Target { self.as_slice() } } impl std::ops::DerefMut for ArenaBuffer { fn deref_mut(&mut self) -> &mut Self::Target { self.as_mut_slice() } } /// Static Buffer pool managing multiple memory arenas /// of which `[ArenaBuffer]`s are returned for requested allocations pub struct BufferPool { inner: UnsafeCell, } unsafe impl Sync for BufferPool {} unsafe impl Send for BufferPool {} crate::assert::assert_send_sync!(BufferPool); struct PoolInner { /// An instance of the program's IO, used for registering /// Arena's with io_uring. io: Option>, /// An Arena which returns `ArenaBuffer`s of size `db_page_size`. page_arena: Option>, /// An Arena which returns `ArenaBuffer`s of size `db_page_size` /// plus 24 byte `WAL_FRAME_HEADER_SIZE`, preventing the fragmentation /// or complex book-keeping needed to use the same arena for both sizes. wal_frame_arena: Option>, /// The size of each `Arena`, in bytes. arena_size: usize, /// The `[Database::page_size]`, which the `page_arena` will use to /// return buffers from `Self::get_page`. db_page_size: OnceLock, } unsafe impl Sync for PoolInner {} unsafe impl Send for PoolInner {} crate::assert::assert_send_sync!(PoolInner); impl Default for BufferPool { fn default() -> Self { Self::new(Self::DEFAULT_ARENA_SIZE) } } impl BufferPool { /// 3MB Default size for each `Arena`. Any higher and /// it will fail to register the second arena with io_uring due /// to `RL_MEMLOCK` limit for un-privileged processes being 8MB total. pub const DEFAULT_ARENA_SIZE: usize = 3 * 1024 * 1024; /// 1MB size For testing/CI pub const TEST_ARENA_SIZE: usize = 1024 * 1024; /// 4KB default page_size pub const DEFAULT_PAGE_SIZE: usize = 4096; /// Maximum size for each Arena (64MB total) const MAX_ARENA_SIZE: usize = 32 * 1024 * 1024; /// 64kb Minimum arena size const MIN_ARENA_SIZE: usize = 1024 * 64; fn new(arena_size: usize) -> Self { turso_assert!( (Self::MIN_ARENA_SIZE..Self::MAX_ARENA_SIZE).contains(&arena_size), "Arena size out of valid range", { "arena_size": arena_size, "min": Self::MIN_ARENA_SIZE, "max": Self::MAX_ARENA_SIZE } ); Self { inner: UnsafeCell::new(PoolInner { page_arena: None, wal_frame_arena: None, arena_size, db_page_size: OnceLock::new(), io: None, }), } } /// Request a `Buffer` of size `len` #[inline] pub fn allocate(&self, len: usize) -> Buffer { self.inner().allocate(len) } /// Request a `Buffer` the size of the `db_page_size` the `BufferPool` was initialized with. #[inline] pub fn get_page(&self) -> Buffer { let inner = self.inner_mut(); inner.get_db_page_buffer() } /// Request a `Buffer` for use with a WAL frame, /// `[Database::page_size] + `WAL_FRAME_HEADER_SIZE` #[inline] pub fn get_wal_frame(&self) -> Buffer { let inner = self.inner_mut(); inner.get_wal_frame_buffer() } #[inline] fn inner(&self) -> &PoolInner { unsafe { &*self.inner.get() } } #[inline] #[allow(clippy::mut_from_ref)] fn inner_mut(&self) -> &mut PoolInner { unsafe { &mut *self.inner.get() } } /// Create a static `BufferPool` initialize the pool to the default page size, **without** /// populating the Arenas. Arenas will not be created until `[BufferPool::finalize_page_size]`, /// and the pool will temporarily return temporary buffers to prevent reallocation of the /// arena if the page size is set to something other than the default value. pub fn begin_init(io: &Arc, arena_size: usize) -> Arc { let pool = Arc::new(BufferPool::new(arena_size)); let inner = pool.inner_mut(); // Just store the IO handle, don't create arena yet if inner.io.is_none() { inner.io = Some(Arc::clone(io)); } pool } /// Call when `[Database::db_state]` is initialized, providing the `page_size` to allocate /// an arena for the pool. Before this call, the pool will use temporary buffers which are /// cached in thread local storage. pub fn finalize_with_page_size(&self, page_size: usize) -> crate::Result<()> { let inner = self.inner_mut(); tracing::trace!("finalize page size called with size {page_size}"); if page_size != BufferPool::DEFAULT_PAGE_SIZE { // so far we have handed out some temporary buffers, since the page size is not // default, we need to clear the cache so they aren't reused for other operations. TEMP_BUFFER_CACHE.with(|cache| { cache.borrow_mut().reinit_cache(page_size); }); } if inner.page_arena.is_some() { tracing::trace!("Buffer pool already initialized, skipping finalize"); return Ok(()); } // Tries to atomically (guarenteed by the OnceLock) initialize the page size for the inner pool. // If it succeeds, we now have to initialize the arenas. // If the initialization fails, this means the arenas have already been initialized by a previous thread // This avoids a potential TOCTOU race, where 2 threads could try to initalize the arena at the same time // after checking the `db_page_size` if inner.db_page_size.set(page_size).is_ok() { inner.init_arenas()?; }; Ok(()) } } impl PoolInner { #[inline] pub fn get_db_page_size(&self) -> usize { *(self .db_page_size .get() .unwrap_or(&BufferPool::DEFAULT_PAGE_SIZE)) } /// Allocate a buffer of the given length from the pool, falling back to /// temporary thread local buffers if the pool is not initialized or is full. pub fn allocate(&self, len: usize) -> Buffer { turso_assert!(len > 0, "Cannot allocate zero-length buffer"); let db_page_size = self.get_db_page_size(); let wal_frame_size = db_page_size + WAL_FRAME_HEADER_SIZE; // Check if this is exactly a WAL frame size allocation if len == wal_frame_size { return self .wal_frame_arena .as_ref() .and_then(|wal_arena| Arena::try_alloc(wal_arena, len)) .unwrap_or_else(|| Buffer::new_temporary(len)); } // For all other sizes, use regular arena self.page_arena .as_ref() .and_then(|arena| Arena::try_alloc(arena, len)) .unwrap_or_else(|| Buffer::new_temporary(len)) } fn get_db_page_buffer(&mut self) -> Buffer { let db_page_size = self.get_db_page_size(); self.page_arena .as_ref() .and_then(|arena| Arena::try_alloc(arena, db_page_size)) .unwrap_or_else(|| Buffer::new_temporary(db_page_size)) } fn get_wal_frame_buffer(&mut self) -> Buffer { let len = self.get_db_page_size() + WAL_FRAME_HEADER_SIZE; self.wal_frame_arena .as_ref() .and_then(|wal_arena| Arena::try_alloc(wal_arena, len)) .unwrap_or_else(|| Buffer::new_temporary(len)) } /// Allocate a new arena for the pool to use fn init_arenas(&mut self) -> crate::Result<()> { let db_page_size = self.get_db_page_size(); let arena_size = self.arena_size; let io = self.io.as_ref().expect("Pool not initialized").clone(); // Create regular page arena match Arena::new(db_page_size, arena_size, &io) { Ok(arena) => { tracing::trace!( "added arena {} with size {} MB and slot size {}", arena.id, arena_size / (1024 * 1024), db_page_size ); self.page_arena = Some(Arc::new(arena)); } Err(e) => { tracing::error!("Failed to create arena: {:?}", e); return Err(LimboError::InternalError(format!( "Failed to create arena: {e}", ))); } } // Create WAL frame arena let wal_frame_size = db_page_size + WAL_FRAME_HEADER_SIZE; match Arena::new(wal_frame_size, arena_size, &io) { Ok(arena) => { tracing::trace!( "added WAL frame arena {} with size {} MB and slot size {}", arena.id, arena_size / (1024 * 1024), wal_frame_size ); self.wal_frame_arena = Some(Arc::new(arena)); } Err(e) => { tracing::error!("Failed to create WAL frame arena: {:?}", e); return Err(LimboError::InternalError(format!( "Failed to create WAL frame arena: {e}", ))); } } Ok(()) } } /// Preallocated block of memory used by the pool to distribute `ArenaBuffer`s #[derive(Debug)] struct Arena { /// Identifier to tie allocations back to the arena. If the arena is registerd /// with `io_uring`, then the ID represents the index of the arena into the ring's /// sparse registered buffer array created on the ring's initialization. id: u32, /// Base pointer to the arena returned by `mmap` base: NonNull, /// Total number of slots currently allocated/in use. allocated_slots: AtomicUsize, /// Currently free slots (lock-free atomic bitmap). free_slots: AtomicSlotBitmap, /// Total size of the arena in bytes arena_size: usize, /// Slot size the total arena is divided into. slot_size: usize, } // SAFETY: Arena's base pointer comes from mmap and is never aliased. All mutable // state is behind atomics (AtomicUsize, AtomicSlotBitmap), so concurrent access is safe. unsafe impl Send for Arena {} unsafe impl Sync for Arena {} impl Drop for Arena { fn drop(&mut self) { unsafe { arena::dealloc(self.base.as_ptr(), self.arena_size) }; } } /// Slots 0 and 1 will be reserved for Arenas which are registered buffers /// with io_uring. const UNREGISTERED_START: u32 = 2; /// ID's for an Arena which is not registered with `io_uring` /// registered arena will always have id = 0..=1 /// we purposely use std::sync::AtomicU32 instead of core::sync::AtomicU32 because /// this is a global static variable and can mess with shuttle tests static NEXT_ID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(UNREGISTERED_START); impl Arena { /// Create a new arena with the given size and page size. /// NOTE: Minimum arena size is slot_size * 64 fn new(slot_size: usize, arena_size: usize, io: &Arc) -> Result { let min_slots = arena_size.div_ceil(slot_size); let rounded_slots = (min_slots.max(64) + 63) & !63; let rounded_bytes = rounded_slots * slot_size; // Guard against the global cap if unlikely(rounded_bytes > BufferPool::MAX_ARENA_SIZE) { return Err(format!( "arena size {} B exceeds hard limit of {} B", rounded_bytes, BufferPool::MAX_ARENA_SIZE )); } let ptr = unsafe { arena::alloc(rounded_bytes) }; let base = NonNull::new(ptr).ok_or("Failed to allocate arena")?; let id = io .register_fixed_buffer(base, rounded_bytes) .unwrap_or_else(|_| { // Register with io_uring if possible, otherwise use next available ID let next_id = NEXT_ID.fetch_add(1, Ordering::AcqRel); tracing::trace!("Allocating arena with id {}", next_id); next_id }); let map = AtomicSlotBitmap::new(rounded_slots as u32); Ok(Self { id, base, free_slots: map, allocated_slots: AtomicUsize::new(0), slot_size, arena_size: rounded_bytes, }) } /// Allocate a `Buffer` large enough for logical length `size`. pub fn try_alloc(arena: &Arc, size: usize) -> Option { if size > arena.slot_size { // The buffer pool only supports single-slot allocations. Larger requests fall back to // temporary heap buffers via the caller. return None; } let first_idx = arena.free_slots.alloc_one()?; arena.allocated_slots.fetch_add(1, Ordering::AcqRel); let offset = first_idx as usize * arena.slot_size; let ptr = unsafe { NonNull::new_unchecked(arena.base.as_ptr().add(offset)) }; Some(Buffer::new_pooled(ArenaBuffer::new( Arc::clone(arena), ptr, size, arena.id, first_idx, ))) } /// Mark all relevant slots that include `size` starting at `slot_idx` as free. pub fn free(&self, slot_idx: u32, size: usize) { turso_assert!( size <= self.slot_size, "pooled buffers must not exceed one slot" ); turso_assert!( !self.free_slots.is_free(slot_idx), "must not already be marked free" ); self.free_slots.free_one(slot_idx); self.allocated_slots.fetch_sub(1, Ordering::AcqRel); } } #[cfg(all(unix, not(miri)))] mod arena { use libc::MAP_ANONYMOUS; use libc::{mmap, munmap, MAP_PRIVATE, PROT_READ, PROT_WRITE}; use std::ffi::c_void; pub unsafe fn alloc(len: usize) -> *mut u8 { let ptr = mmap( std::ptr::null_mut(), len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0, ); if ptr == libc::MAP_FAILED { panic!("mmap failed: {}", std::io::Error::last_os_error()); } #[cfg(target_os = "linux")] { libc::madvise(ptr, len, libc::MADV_HUGEPAGE); } ptr as *mut u8 } pub unsafe fn dealloc(ptr: *mut u8, len: usize) { let result = munmap(ptr as *mut c_void, len); if result != 0 { panic!("munmap failed: {}", std::io::Error::last_os_error()); } } } #[cfg(any(not(unix), miri))] mod arena { pub fn alloc(len: usize) -> *mut u8 { let layout = std::alloc::Layout::from_size_align(len, std::mem::size_of::()).unwrap(); unsafe { std::alloc::alloc_zeroed(layout) } } pub fn dealloc(ptr: *mut u8, len: usize) { let layout = std::alloc::Layout::from_size_align(len, std::mem::size_of::()).unwrap(); unsafe { std::alloc::dealloc(ptr, layout) }; } } /// Shuttle tests for concurrent buffer pool operations. /// /// These tests target the `unsafe impl Sync/Send` on: /// - `ArenaBuffer`: Raw pointer access across threads /// - `BufferPool`: UnsafeCell-based interior mutability /// - `PoolInner`: Shared mutable state /// #[cfg(all(shuttle, test))] mod shuttle_tests { use super::*; use crate::io::MemoryIO; use crate::sync::*; use crate::thread; use rustc_hash::FxHashSet as HashSet; fn create_test_pool() -> Arc { let io: Arc = Arc::new(MemoryIO::new()); let pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE); pool.finalize_with_page_size(4096).unwrap(); pool } /// Test concurrent allocations from BufferPool. /// Verifies that multiple threads can safely call get_page() simultaneously. #[test] fn shuttle_concurrent_page_allocation() { shuttle::check_random( || { let pool = create_test_pool(); let mut handles = vec![]; for _ in 0..3 { let pool = Arc::clone(&pool); let h = thread::spawn(move || { let buf = pool.get_page(); assert_eq!(buf.len(), 4096); buf }); handles.push(h); } let buffers: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect(); // Verify all buffers are valid and distinct (no double allocation) assert_eq!(buffers.len(), 3); }, 1000, ); } /// Test concurrent allocation and deallocation. /// Buffers are dropped in different threads than they were allocated. #[test] fn shuttle_concurrent_alloc_and_drop() { shuttle::check_random( || { let pool = create_test_pool(); let pool2 = Arc::clone(&pool); // Thread 1: allocate and send buffer to be dropped elsewhere let h1 = thread::spawn(move || { let buf = pool.get_page(); buf.len() // return length, buffer dropped here }); // Thread 2: allocate concurrently let h2 = thread::spawn(move || { let buf = pool2.get_page(); buf.len() }); assert_eq!(h1.join().unwrap(), 4096); assert_eq!(h2.join().unwrap(), 4096); }, 1000, ); } /// Test that ArenaBuffer can be safely sent between threads and written to. /// This tests the `unsafe impl Send + Sync for ArenaBuffer`. #[test] fn shuttle_arena_buffer_send_and_write() { shuttle::check_random( || { let pool = create_test_pool(); let buf = pool.get_page(); // Write some data buf.as_mut_slice()[0] = 42; // Send to another thread for reading let h = thread::spawn(move || { assert_eq!(buf.as_slice()[0], 42); buf.as_slice()[0] }); assert_eq!(h.join().unwrap(), 42); }, 1000, ); } /// Test concurrent WAL frame and page allocations. /// Both arena types are exercised simultaneously. #[test] fn shuttle_concurrent_mixed_allocations() { shuttle::check_random( || { let pool = create_test_pool(); let pool2 = Arc::clone(&pool); let pool3 = Arc::clone(&pool); let h1 = thread::spawn(move || { let buf = pool.get_page(); assert_eq!(buf.len(), 4096); }); let h2 = thread::spawn(move || { let buf = pool2.get_wal_frame(); // WAL frame = page_size + WAL_FRAME_HEADER_SIZE (24) assert_eq!(buf.len(), 4096 + WAL_FRAME_HEADER_SIZE); }); let h3 = thread::spawn(move || { let buf = pool3.allocate(1024); assert_eq!(buf.len(), 1024); }); h1.join().unwrap(); h2.join().unwrap(); h3.join().unwrap(); }, 1000, ); } /// Stress test: many threads allocating and dropping buffers rapidly. /// This helps find race conditions in the slot bitmap and arena management. #[test] fn shuttle_stress_concurrent_alloc_drop() { shuttle::check_random( || { let pool = create_test_pool(); let mut handles = vec![]; for i in 0..4 { let pool = Arc::clone(&pool); let h = thread::spawn(move || { // Each thread does multiple alloc/drop cycles for _ in 0..2 { let buf = pool.get_page(); // Write thread-specific data buf.as_mut_slice()[0] = i as u8; assert_eq!(buf.as_slice()[0], i as u8); // buf dropped here, returning slot to arena } }); handles.push(h); } for h in handles { h.join().unwrap(); } }, 1000, ); } /// Test that buffers allocated by one thread can be safely read by another. /// Uses a channel-like pattern with Arc to share buffers. #[test] fn shuttle_buffer_shared_read() { shuttle::check_random( || { let pool = create_test_pool(); // Allocate and write in main thread let buf = pool.get_page(); for (i, byte) in buf.as_mut_slice().iter_mut().enumerate().take(100) { *byte = (i % 256) as u8; } // Wrap in Arc for shared access (Buffer itself doesn't impl Clone) let buf = Arc::new(buf); let buf2 = Arc::clone(&buf); // Reader thread let h = thread::spawn(move || { for i in 0..100 { assert_eq!(buf2.as_slice()[i], (i % 256) as u8); } }); // Main thread also reads for i in 0..100 { assert_eq!(buf.as_slice()[i], (i % 256) as u8); } h.join().unwrap(); }, 1000, ); } /// Test pool initialization race (though guarded by init_lock). /// Multiple threads trying to finalize should be safe. #[test] fn shuttle_concurrent_finalize() { let test = || { let io: Arc = Arc::new(MemoryIO::new()); let pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE); let pool2 = Arc::clone(&pool); let pool3 = Arc::clone(&pool); let h1 = thread::spawn(move || { let _ = pool.finalize_with_page_size(4096).unwrap(); }); let h2 = thread::spawn(move || { let _ = pool2.finalize_with_page_size(4096).unwrap(); }); // Also try to allocate while finalizing let h3 = thread::spawn(move || { // This may get a temporary buffer if arena isn't ready let buf = pool3.allocate(4096); assert_eq!(buf.len(), 4096); }); h1.join().unwrap(); h2.join().unwrap(); h3.join().unwrap(); }; shuttle::check_random(test, 1000); } /// Test concurrent writes to the same ArenaBuffer at the SAME offsets. /// This exercises the `unsafe impl Sync for ArenaBuffer` which is documented as potentially unsound. /// Each thread writes a distinct pattern to all 4096 bytes, and we verify no torn writes occurred /// (all bytes must have the same pattern value, not a mix from different threads). #[test] fn shuttle_concurrent_write_same_buffer_same_offset() { shuttle::check_random( || { let pool = create_test_pool(); let buf = pool.get_page(); // Three distinct byte patterns that threads will race to write const PATTERN_A: u8 = 0xAA; const PATTERN_B: u8 = 0xBB; const PATTERN_C: u8 = 0xCC; // Use scoped threads so buf can be borrowed by multiple threads thread::scope(|scope| { // Thread A writes PATTERN_A to all 4096 bytes scope.spawn(|| { buf.as_mut_slice().fill(PATTERN_A); }); // Thread B writes PATTERN_B to all 4096 bytes scope.spawn(|| { buf.as_mut_slice().fill(PATTERN_B); }); // Thread C writes PATTERN_C to all 4096 bytes scope.spawn(|| { buf.as_mut_slice().fill(PATTERN_C); }); }); // After all writes complete, verify no torn writes across the entire buffer // All 4096 bytes should have the same pattern (whichever thread won) let slice = buf.as_slice(); let first_byte = slice[0]; // First byte must be one of our patterns assert!( first_byte == PATTERN_A || first_byte == PATTERN_B || first_byte == PATTERN_C, "Invalid pattern in buffer: 0x{:02X}", first_byte ); // All bytes must match the first byte (no partial/torn writes) for (i, &byte) in slice.iter().enumerate() { assert!( byte == first_byte, "Torn write at offset {}: got 0x{:02X}, expected 0xAA, 0xBB, or 0xCC", i, byte ); } }, 1000, ); } /// Test concurrent writes to different offsets of the same buffer (non-overlapping). /// This tests that writes to different parts of the buffer don't interfere. #[test] fn shuttle_concurrent_write_different_offsets() { shuttle::check_random( || { let pool = create_test_pool(); let buf = pool.get_page(); let ptr = buf.as_ptr() as usize; let len = buf.len(); let _buf = buf; let h1 = thread::spawn(move || { let slice = unsafe { std::slice::from_raw_parts_mut(ptr as *mut u8, len) }; for i in 0..100 { slice[i] = 0xAA; } }); let h2 = thread::spawn(move || { let slice = unsafe { std::slice::from_raw_parts_mut(ptr as *mut u8, len) }; for i in 100..200 { slice[i] = 0xBB; } }); let h3 = thread::spawn(move || { let slice = unsafe { std::slice::from_raw_parts_mut(ptr as *mut u8, len) }; for i in 200..300 { slice[i] = 0xCC; } }); h1.join().unwrap(); h2.join().unwrap(); h3.join().unwrap(); // Verify each section has the correct pattern let final_slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, len) }; for i in 0..100 { assert_eq!( final_slice[i], 0xAA, "Section 1 corrupted at offset {}: expected 0xAA, got 0x{:02X}", i, final_slice[i] ); } for i in 100..200 { assert_eq!( final_slice[i], 0xBB, "Section 2 corrupted at offset {}: expected 0xBB, got 0x{:02X}", i, final_slice[i] ); } for i in 200..300 { assert_eq!( final_slice[i], 0xCC, "Section 3 corrupted at offset {}: expected 0xCC, got 0x{:02X}", i, final_slice[i] ); } }, 1000, ); } /// Test allocation racing with deallocation (slot recycling). /// Verifies that when a buffer is dropped and its slot is freed, /// concurrent allocations correctly handle the recycled slot. #[test] fn shuttle_alloc_during_drop_slot_recycling() { shuttle::check_random( || { let pool = create_test_pool(); // Pre-allocate some buffers and write identifying data let mut initial_bufs: Vec<_> = (0..5).map(|_| pool.get_page()).collect(); let initial_ptrs: Vec = initial_bufs.iter().map(|b| b.as_ptr() as usize).collect(); // Write unique patterns to initial buffers for (i, buf) in initial_bufs.iter_mut().enumerate() { buf.as_mut_slice()[0] = 0xDE; buf.as_mut_slice()[1] = i as u8; } let pool2 = Arc::clone(&pool); let pool3 = Arc::clone(&pool); // Thread 1: drops buffers, freeing slots let h1 = thread::spawn(move || { drop(initial_bufs); }); // Thread 2: allocates while slots are being freed let h2 = thread::spawn(move || { let mut bufs = Vec::new(); for i in 0..3 { let buf = pool2.get_page(); assert_eq!(buf.len(), 4096, "Buffer {} has wrong length", i); // Write identifying pattern buf.as_mut_slice()[0] = 0xAA; buf.as_mut_slice()[1] = i as u8; bufs.push(buf); } bufs }); // Thread 3: also allocates concurrently let h3 = thread::spawn(move || { let mut bufs = Vec::new(); for i in 0..3 { let buf = pool3.get_page(); assert_eq!(buf.len(), 4096, "Buffer {} has wrong length", i); // Write different identifying pattern buf.as_mut_slice()[0] = 0xBB; buf.as_mut_slice()[1] = i as u8; bufs.push(buf); } bufs }); h1.join().unwrap(); let bufs2 = h2.join().unwrap(); let bufs3 = h3.join().unwrap(); // Verify buffers within each thread don't overlap let ptrs2: HashSet<_> = bufs2.iter().map(|b| b.as_ptr() as usize).collect(); assert_eq!( ptrs2.len(), bufs2.len(), "Thread 2 got duplicate buffer pointers" ); let ptrs3: HashSet<_> = bufs3.iter().map(|b| b.as_ptr() as usize).collect(); assert_eq!( ptrs3.len(), bufs3.len(), "Thread 3 got duplicate buffer pointers" ); // Verify no overlap between buffers from different threads for ptr in &ptrs2 { assert!( !ptrs3.contains(ptr), "Slot double-allocation: same memory 0x{:X} returned to both threads", ptr ); } // Verify each buffer has correct identifying data (not corrupted) for (i, buf) in bufs2.iter().enumerate() { assert_eq!( buf.as_slice()[0], 0xAA, "Thread 2 buffer {} header corrupted", i ); assert_eq!( buf.as_slice()[1], i as u8, "Thread 2 buffer {} index corrupted", i ); } for (i, buf) in bufs3.iter().enumerate() { assert_eq!( buf.as_slice()[0], 0xBB, "Thread 3 buffer {} header corrupted", i ); assert_eq!( buf.as_slice()[1], i as u8, "Thread 3 buffer {} index corrupted", i ); } // Verify we can still allocate after all this let final_buf = pool.get_page(); assert_eq!(final_buf.len(), 4096, "Final allocation failed"); // Keep initial_ptrs to suppress unused warning let _ = initial_ptrs; }, 1000, ); } /// Test arena exhaustion and recovery. /// Allocates until the arena is full (falls back to temporary buffers), /// then frees and verifies slots are correctly recycled. #[test] fn shuttle_arena_exhaustion_and_recovery() { shuttle::check_random( || { let pool = create_test_pool(); // Allocate many buffers to exhaust the arena // TEST_ARENA_SIZE = 1MB, page_size = 4KB, so ~256 slots max let mut buffers: Vec = Vec::new(); let mut pooled_count = 0; let mut temp_count = 0; for i in 0..300 { let buf = pool.get_page(); assert_eq!(buf.len(), 4096, "Buffer {} has wrong length", i); // Write identifying data buf.as_mut_slice()[0] = (i & 0xFF) as u8; buf.as_mut_slice()[1] = ((i >> 8) & 0xFF) as u8; if buf.is_pooled() { pooled_count += 1; } else { temp_count += 1; } buffers.push(buf); } assert!(temp_count > 0); // We should have some pooled and some temporary buffers assert!(pooled_count > 0, "Expected some pooled buffers, got none"); // With 1MB arena and 4KB pages, we have ~256 slots // So with 300 allocations, we should have some temporary assert!( pooled_count <= 256, "Got {} pooled buffers, but arena should only have ~256 slots", pooled_count ); assert!(pooled_count + temp_count >= 256); // Verify all buffers have correct identifying data for (i, buf) in buffers.iter().enumerate() { assert_eq!( buf.as_slice()[0], (i & 0xFF) as u8, "Buffer {} low byte corrupted", i ); assert_eq!( buf.as_slice()[1], ((i >> 8) & 0xFF) as u8, "Buffer {} high byte corrupted", i ); } // Drop half the buffers to free slots let dropped_count = buffers.len() - 150; buffers.truncate(150); // Allocate again - should get recycled slots let pool2 = Arc::clone(&pool); let h = thread::spawn(move || { let mut new_bufs = Vec::new(); for i in 0..50 { let buf = pool2.get_page(); assert_eq!(buf.len(), 4096, "New buffer {} has wrong length", i); // Write new pattern buf.as_mut_slice()[0] = 0xFF; buf.as_mut_slice()[1] = i as u8; new_bufs.push(buf); } new_bufs }); let new_bufs = h.join().unwrap(); // Verify new buffers for (i, buf) in new_bufs.iter().enumerate() { assert_eq!(buf.len(), 4096, "New buffer {} length check failed", i); assert_eq!(buf.as_slice()[0], 0xFF, "New buffer {} header corrupted", i); assert_eq!( buf.as_slice()[1], i as u8, "New buffer {} index corrupted", i ); } // Verify remaining original buffers still have correct data for (i, buf) in buffers.iter().enumerate() { assert_eq!( buf.as_slice()[0], (i & 0xFF) as u8, "Original buffer {} corrupted after recycling", i ); } let _ = (temp_count, dropped_count); // suppress warnings }, 1000, ); } /// Test that allocated buffers never overlap (slot double-allocation detection). /// Multiple threads allocate concurrently and we verify all pointers are unique. #[test] fn shuttle_slot_overlap_verification() { shuttle::check_random( || { let pool = create_test_pool(); let mut handles = vec![]; for thread_id in 0..4u8 { let pool = Arc::clone(&pool); let h = thread::spawn(move || { let mut bufs = Vec::new(); for buf_id in 0..10u8 { let buf = pool.get_page(); assert_eq!(buf.len(), 4096, "Buffer has wrong length"); // Write thread and buffer identifying data buf.as_mut_slice()[0] = thread_id; buf.as_mut_slice()[1] = buf_id; // Write a checksum pattern buf.as_mut_slice()[2] = thread_id ^ buf_id; bufs.push(buf); } bufs }); handles.push(h); } let all_bufs: Vec> = handles.into_iter().map(|h| h.join().unwrap()).collect(); // Verify each thread got the expected number of buffers for (thread_id, thread_bufs) in all_bufs.iter().enumerate() { assert_eq!( thread_bufs.len(), 10, "Thread {} got {} buffers instead of 10", thread_id, thread_bufs.len() ); } // Collect all pointers and verify uniqueness let mut all_ptrs: Vec = Vec::new(); for thread_bufs in &all_bufs { for buf in thread_bufs { all_ptrs.push(buf.as_ptr() as usize); } } let unique_ptrs: HashSet<_> = all_ptrs.iter().copied().collect(); assert_eq!( all_ptrs.len(), unique_ptrs.len(), "Slot double-allocation detected: {} total buffers but only {} unique pointers", all_ptrs.len(), unique_ptrs.len() ); // Verify each buffer still has correct identifying data (no cross-thread corruption) for (thread_id, thread_bufs) in all_bufs.iter().enumerate() { for (buf_id, buf) in thread_bufs.iter().enumerate() { assert_eq!( buf.as_slice()[0], thread_id as u8, "Buffer [{},{}] thread_id corrupted: expected {}, got {}", thread_id, buf_id, thread_id, buf.as_slice()[0] ); assert_eq!( buf.as_slice()[1], buf_id as u8, "Buffer [{},{}] buf_id corrupted: expected {}, got {}", thread_id, buf_id, buf_id, buf.as_slice()[1] ); let expected_checksum = (thread_id as u8) ^ (buf_id as u8); assert_eq!( buf.as_slice()[2], expected_checksum, "Buffer [{},{}] checksum corrupted: expected {}, got {}", thread_id, buf_id, expected_checksum, buf.as_slice()[2] ); } } // Verify buffers don't overlap by checking memory ranges let page_size = 4096usize; for (i, ptr_i) in all_ptrs.iter().enumerate() { for (j, ptr_j) in all_ptrs.iter().enumerate() { if i != j { let range_i = *ptr_i..(*ptr_i + page_size); // Check if ptr_j falls within range_i assert!( !range_i.contains(ptr_j), "Buffer {} (0x{:X}) overlaps with buffer {} (0x{:X})", i, ptr_i, j, ptr_j ); } } } }, 1000, ); } /// Test buffer content integrity under concurrent operations. /// Each thread writes a unique pattern and verifies it's not corrupted /// by other threads' operations. #[test] fn shuttle_buffer_content_integrity() { shuttle::check_random( || { let pool = create_test_pool(); let mut handles = vec![]; for thread_id in 0u8..4 { let pool = Arc::clone(&pool); let h = thread::spawn(move || { let buf = pool.get_page(); // Write thread-specific pattern let pattern = thread_id.wrapping_mul(37); for byte in buf.as_mut_slice().iter_mut() { *byte = pattern; } // Yield to allow other threads to run thread::yield_now(); // Verify pattern is intact for (i, byte) in buf.as_slice().iter().enumerate() { assert_eq!( *byte, pattern, "Buffer corruption at offset {}: expected {}, got {}", i, pattern, *byte ); } buf }); handles.push(h); } // All threads should complete without corruption for h in handles { h.join().unwrap(); } }, 1000, ); } /// Test the race between ArenaBuffer::drop upgrading Weak while /// the Arena might be getting dropped. This exercises the weak reference /// pattern used for buffer deallocation. #[test] fn shuttle_weak_reference_upgrade_during_drop() { shuttle::check_random( || { let pool = create_test_pool(); // Allocate multiple buffers and write identifying data let buf1 = pool.get_page(); let buf2 = pool.get_page(); let buf3 = pool.get_page(); buf1.as_mut_slice()[0] = 0x11; buf2.as_mut_slice()[0] = 0x22; buf3.as_mut_slice()[0] = 0x33; let buf1_ptr = buf1.as_ptr() as usize; let buf2_ptr = buf2.as_ptr() as usize; // Clone pool references let pool2 = Arc::clone(&pool); let pool3 = Arc::clone(&pool); // Thread 1: drop buffer1, freeing its slot let h1 = thread::spawn(move || { // Buffer drop will try to upgrade Weak and call free() drop(buf1); }); // Thread 2: drop pool reference and buffer2 let h2 = thread::spawn(move || { drop(buf2); drop(pool2); }); // Thread 3: allocate while others are dropping let h3 = thread::spawn(move || { let new_buf = pool3.get_page(); assert_eq!(new_buf.len(), 4096, "New buffer has wrong length"); new_buf.as_mut_slice()[0] = 0x44; new_buf }); h1.join().unwrap(); h2.join().unwrap(); let new_buf = h3.join().unwrap(); // Verify buf3 is still intact assert_eq!(buf3.as_slice()[0], 0x33, "buf3 was corrupted"); // Verify new_buf has correct data assert_eq!(new_buf.as_slice()[0], 0x44, "new_buf was corrupted"); // Original pool reference keeps arena alive // Allocate more to verify arena is still functional let final_buf = pool.get_page(); assert_eq!(final_buf.len(), 4096, "Final allocation failed"); final_buf.as_mut_slice()[0] = 0x55; assert_eq!(final_buf.as_slice()[0], 0x55, "Final buffer write failed"); // The recycled slot might be one of the dropped buffers let final_ptr = final_buf.as_ptr() as usize; // This is valid - we might get a recycled slot let _ = (buf1_ptr, buf2_ptr, final_ptr); }, 1000, ); } /// Test bitmap consistency: after many concurrent operations, /// verify that allocated_slots matches actual allocations. #[test] fn shuttle_bitmap_consistency() { shuttle::check_random( || { let pool = create_test_pool(); let mut handles = vec![]; // Many threads doing alloc/free cycles for thread_id in 0..4u8 { let pool = Arc::clone(&pool); let h = thread::spawn(move || { let mut bufs = Vec::new(); // Allocate 5 buffers for i in 0..5u8 { let buf = pool.get_page(); assert_eq!( buf.len(), 4096, "Thread {} buf {} wrong length", thread_id, i ); // Mark with identifying data buf.as_mut_slice()[0] = thread_id; buf.as_mut_slice()[1] = i; buf.as_mut_slice()[2] = 0xAA; // Initial marker bufs.push(buf); } // Verify all 5 before truncation for (i, buf) in bufs.iter().enumerate() { assert_eq!( buf.as_slice()[0], thread_id, "Pre-truncate thread_id mismatch" ); assert_eq!(buf.as_slice()[1], i as u8, "Pre-truncate index mismatch"); } // Free 3 buffers (keep first 2) bufs.truncate(2); // Allocate 3 more for i in 0..3u8 { let buf = pool.get_page(); assert_eq!( buf.len(), 4096, "Thread {} new buf {} wrong length", thread_id, i ); buf.as_mut_slice()[0] = thread_id; buf.as_mut_slice()[1] = 10 + i; // Different index range buf.as_mut_slice()[2] = 0xBB; // New marker bufs.push(buf); } // Should have 5 buffers now (2 original + 3 new) assert_eq!(bufs.len(), 5, "Thread {} should have 5 buffers", thread_id); bufs }); handles.push(h); } let all_bufs: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect(); // Verify each thread returned 5 buffers for (thread_id, thread_bufs) in all_bufs.iter().enumerate() { assert_eq!( thread_bufs.len(), 5, "Thread {} returned {} buffers instead of 5", thread_id, thread_bufs.len() ); } // Count pooled vs temporary buffers let mut pooled_count = 0; let mut temp_count = 0; for thread_bufs in &all_bufs { for buf in thread_bufs { if buf.fixed_id().is_some() { pooled_count += 1; } else { temp_count += 1; } } } // Total should be 20 (4 threads * 5 buffers) assert_eq!( pooled_count + temp_count, 20, "Total buffer count mismatch: {} pooled + {} temp != 20", pooled_count, temp_count ); // Verify all buffers have valid identifying data for (thread_id, thread_bufs) in all_bufs.iter().enumerate() { for (i, buf) in thread_bufs.iter().enumerate() { assert_eq!( buf.as_slice()[0], thread_id as u8, "Buffer [{},{}] thread_id corrupted", thread_id, i ); let marker = buf.as_slice()[2]; assert!( marker == 0xAA || marker == 0xBB, "Buffer [{},{}] has invalid marker 0x{:02X}", thread_id, i, marker ); } } // Collect all pointers to verify no duplicates let all_ptrs: HashSet<_> = all_bufs .iter() .flat_map(|bufs| bufs.iter().map(|b| b.as_ptr() as usize)) .collect(); assert_eq!( all_ptrs.len(), 20, "Found duplicate pointers: {} unique out of 20", all_ptrs.len() ); // Try allocating more to verify arena is consistent let mut final_bufs = Vec::new(); for i in 0..10 { let buf = pool.get_page(); assert_eq!(buf.len(), 4096, "Final buf {} wrong length", i); buf.as_mut_slice()[0] = 0xFF; buf.as_mut_slice()[1] = i as u8; final_bufs.push(buf); } // Verify final buffers don't overlap with existing ones for buf in &final_bufs { let ptr = buf.as_ptr() as usize; assert!( !all_ptrs.contains(&ptr), "Final buffer overlaps with existing at 0x{:X}", ptr ); } // Keep buffers alive until end drop(all_bufs); drop(final_bufs); }, 1000, ); } /// Test concurrent access through inner_mut(). /// Multiple threads calling get_page() and get_wal_frame() simultaneously /// access different PoolInner fields through the unsynchronized inner_mut(). #[test] fn shuttle_concurrent_inner_mut_access() { shuttle::check_random( || { let pool = create_test_pool(); let mut handles = vec![]; // Threads calling get_page (accesses page_arena through inner_mut) for page_thread_id in 0..2u8 { let pool = Arc::clone(&pool); let h = thread::spawn(move || { let mut bufs = Vec::new(); for i in 0..5u8 { let buf = pool.get_page(); assert_eq!(buf.len(), 4096, "Page buffer has wrong length"); // Mark as page buffer with identifying data buf.as_mut_slice()[0] = 0xAA; // Page marker buf.as_mut_slice()[1] = page_thread_id; buf.as_mut_slice()[2] = i; bufs.push(buf); } bufs }); handles.push(h); } // Threads calling get_wal_frame (accesses wal_frame_arena through inner_mut) for wal_thread_id in 0..2u8 { let pool = Arc::clone(&pool); let h = thread::spawn(move || { let mut bufs = Vec::new(); for i in 0..5u8 { let buf = pool.get_wal_frame(); assert_eq!( buf.len(), 4096 + WAL_FRAME_HEADER_SIZE, "WAL frame buffer has wrong length" ); // Mark as WAL buffer with identifying data buf.as_mut_slice()[0] = 0xBB; // WAL marker buf.as_mut_slice()[1] = wal_thread_id; buf.as_mut_slice()[2] = i; bufs.push(buf); } bufs }); handles.push(h); } // Thread calling allocate with various sizes { let pool = Arc::clone(&pool); let h = thread::spawn(move || { let mut bufs = Vec::new(); for i in 0..5u8 { let buf = pool.allocate(2048); assert_eq!(buf.len(), 2048, "Allocated buffer has wrong length"); // Mark as generic allocation buf.as_mut_slice()[0] = 0xCC; // Allocate marker buf.as_mut_slice()[1] = i; bufs.push(buf); } bufs }); handles.push(h); } let results: Vec> = handles.into_iter().map(|h| h.join().unwrap()).collect(); // Verify we got expected number of buffers from each type // 2 page threads * 5 + 2 wal threads * 5 + 1 allocate thread * 5 = 25 total let total_bufs: usize = results.iter().map(|v| v.len()).sum(); assert_eq!( total_bufs, 25, "Expected 25 total buffers, got {}", total_bufs ); // Verify each buffer has correct marker and data for (thread_idx, thread_bufs) in results.iter().enumerate() { for (buf_idx, buf) in thread_bufs.iter().enumerate() { let marker = buf.as_slice()[0]; assert!( marker == 0xAA || marker == 0xBB || marker == 0xCC, "Buffer [{},{}] has invalid marker 0x{:02X}", thread_idx, buf_idx, marker ); // Verify length matches marker type match marker { 0xAA => assert_eq!(buf.len(), 4096, "Page buffer wrong length"), 0xBB => assert_eq!( buf.len(), 4096 + WAL_FRAME_HEADER_SIZE, "WAL buffer wrong length" ), 0xCC => assert_eq!(buf.len(), 2048, "Allocate buffer wrong length"), _ => unreachable!(), } } } // Collect all pointers and verify no duplicates let all_ptrs: HashSet<_> = results .iter() .flat_map(|bufs| bufs.iter().map(|b| b.as_ptr() as usize)) .collect(); assert_eq!( all_ptrs.len(), total_bufs, "Found duplicate pointers: {} unique out of {}", all_ptrs.len(), total_bufs ); }, 1000, ); } /// Stress test with higher iteration count and more threads. /// This provides better coverage for subtle race conditions. #[test] fn shuttle_high_contention_stress() { shuttle::check_random( || { let pool = create_test_pool(); let mut handles = vec![]; for thread_id in 0..6u8 { let pool = Arc::clone(&pool); let h = thread::spawn(move || { let mut bufs = Vec::new(); let mut dropped_count = 0u8; for iter in 0..4u8 { // Alternate between page and WAL frame allocations let is_page = (thread_id + iter) % 2 == 0; let buf = if is_page { pool.get_page() } else { pool.get_wal_frame() }; // Verify length matches allocation type let expected_len = if is_page { 4096 } else { 4096 + WAL_FRAME_HEADER_SIZE }; assert_eq!( buf.len(), expected_len, "Thread {} iter {} got wrong buffer length", thread_id, iter ); // Write identifying data with checksum buf.as_mut_slice()[0] = thread_id; buf.as_mut_slice()[1] = iter; buf.as_mut_slice()[2] = if is_page { 0xAA } else { 0xBB }; buf.as_mut_slice()[3] = thread_id ^ iter; // Checksum bufs.push(buf); // Occasionally drop a buffer to test recycling if bufs.len() > 2 && iter % 2 == 1 { let dropped = bufs.pop().unwrap(); // Verify the dropped buffer still had valid data assert_eq!( dropped.as_slice()[0], thread_id, "Dropped buffer thread_id corrupted" ); dropped_count += 1; } } // Verify all remaining buffers before returning for (i, buf) in bufs.iter().enumerate() { assert_eq!( buf.as_slice()[0], thread_id, "Pre-return buffer {} thread_id corrupted", i ); let checksum = buf.as_slice()[0] ^ buf.as_slice()[1]; assert_eq!( buf.as_slice()[3], checksum, "Pre-return buffer {} checksum invalid", i ); } (bufs, dropped_count) }); handles.push(h); } let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect(); // Verify results from all threads let mut total_remaining = 0; let mut total_dropped = 0u8; for (thread_id, (thread_bufs, dropped)) in results.iter().enumerate() { total_remaining += thread_bufs.len(); total_dropped = total_dropped.saturating_add(*dropped); // Verify each buffer has correct identifying data for (buf_idx, buf) in thread_bufs.iter().enumerate() { assert_eq!( buf.as_slice()[0], thread_id as u8, "Thread {} buffer {} thread_id corrupted: expected {}, got {}", thread_id, buf_idx, thread_id, buf.as_slice()[0] ); let marker = buf.as_slice()[2]; assert!( marker == 0xAA || marker == 0xBB, "Thread {} buffer {} has invalid marker 0x{:02X}", thread_id, buf_idx, marker ); // Verify checksum let expected_checksum = buf.as_slice()[0] ^ buf.as_slice()[1]; assert_eq!( buf.as_slice()[3], expected_checksum, "Thread {} buffer {} checksum mismatch", thread_id, buf_idx ); // Verify length matches marker let expected_len = if marker == 0xAA { 4096 } else { 4096 + WAL_FRAME_HEADER_SIZE }; assert_eq!( buf.len(), expected_len, "Thread {} buffer {} length mismatch for marker", thread_id, buf_idx ); } } // Sanity check: we should have some buffers remaining assert!( total_remaining > 0, "No buffers remaining after stress test" ); // Collect all pointers and verify no duplicates among remaining buffers let all_ptrs: HashSet<_> = results .iter() .flat_map(|(bufs, _)| bufs.iter().map(|b| b.as_ptr() as usize)) .collect(); assert_eq!( all_ptrs.len(), total_remaining, "Found duplicate pointers: {} unique out of {} remaining", all_ptrs.len(), total_remaining ); // Final allocation to verify pool is still healthy let final_buf = pool.get_page(); assert_eq!(final_buf.len(), 4096, "Final allocation failed"); final_buf.as_mut_slice()[0] = 0xFF; assert_eq!(final_buf.as_slice()[0], 0xFF, "Final buffer write failed"); let _ = total_dropped; // suppress warning }, 1000, ); } } ================================================ FILE: core/storage/checksum.rs ================================================ #![allow(unused_variables, dead_code)] use crate::{CompletionError, Result}; const CHECKSUM_PAGE_SIZE: usize = 4096; const CHECKSUM_SIZE: usize = 8; pub(crate) const CHECKSUM_REQUIRED_RESERVED_BYTES: u8 = CHECKSUM_SIZE as u8; #[derive(Debug, Clone)] pub struct ChecksumContext {} impl ChecksumContext { pub fn new() -> Self { ChecksumContext {} } #[cfg(not(feature = "checksum"))] pub fn add_checksum_to_page(&self, _page: &mut [u8], _page_id: usize) -> Result<()> { use crate::LimboError; Err(LimboError::InternalError( "tursodb must be recompiled with checksum feature in order to use checksums" .to_string(), )) } #[cfg(not(feature = "checksum"))] pub fn verify_checksum( &self, _page: &mut [u8], _page_id: usize, ) -> std::result::Result<(), CompletionError> { Err(CompletionError::ChecksumNotEnabled) } #[cfg(feature = "checksum")] pub fn add_checksum_to_page(&self, page: &mut [u8], _page_id: usize) -> Result<()> { if page.len() != CHECKSUM_PAGE_SIZE { return Ok(()); } // compute checksum on the actual page data (excluding the reserved checksum area) let actual_page = &page[..CHECKSUM_PAGE_SIZE - CHECKSUM_SIZE]; let checksum = self.compute_checksum(actual_page); let checksum_bytes = checksum.to_le_bytes(); assert_eq!(checksum_bytes.len(), CHECKSUM_SIZE); page[CHECKSUM_PAGE_SIZE - CHECKSUM_SIZE..].copy_from_slice(&checksum_bytes); Ok(()) } #[cfg(feature = "checksum")] pub fn verify_checksum( &self, page: &mut [u8], page_id: usize, ) -> std::result::Result<(), CompletionError> { if page.len() != CHECKSUM_PAGE_SIZE { return Ok(()); } let actual_page = &page[..CHECKSUM_PAGE_SIZE - CHECKSUM_SIZE]; let stored_checksum_bytes = &page[CHECKSUM_PAGE_SIZE - CHECKSUM_SIZE..]; let stored_checksum = u64::from_le_bytes(stored_checksum_bytes.try_into().unwrap()); let computed_checksum = self.compute_checksum(actual_page); if stored_checksum != computed_checksum { tracing::error!( "Checksum mismatch on page {}: expected {:x}, got {:x}", page_id, stored_checksum, computed_checksum ); return Err(CompletionError::ChecksumMismatch { page_id, expected: stored_checksum, actual: computed_checksum, }); } Ok(()) } fn compute_checksum(&self, data: &[u8]) -> u64 { twox_hash::XxHash3_64::oneshot(data) } pub fn required_reserved_bytes(&self) -> u8 { CHECKSUM_REQUIRED_RESERVED_BYTES } } impl Default for ChecksumContext { fn default() -> Self { Self::new() } } #[cfg(test)] #[cfg(feature = "checksum")] mod tests { use super::*; fn get_random_page() -> [u8; CHECKSUM_PAGE_SIZE] { let mut page = [0u8; CHECKSUM_PAGE_SIZE]; for (i, byte) in page .iter_mut() .enumerate() .take(CHECKSUM_PAGE_SIZE - CHECKSUM_SIZE) { *byte = (i % 256) as u8; } page } #[test] fn test_add_checksum_to_page() { let ctx = ChecksumContext::new(); let mut page = get_random_page(); let result = ctx.add_checksum_to_page(&mut page, 2); assert!(result.is_ok()); let checksum_bytes = &page[CHECKSUM_PAGE_SIZE - CHECKSUM_SIZE..]; let stored_checksum = u64::from_le_bytes(checksum_bytes.try_into().unwrap()); let actual_page = &page[..CHECKSUM_PAGE_SIZE - CHECKSUM_SIZE]; let expected_checksum = ctx.compute_checksum(actual_page); assert_eq!(stored_checksum, expected_checksum); } #[test] fn test_verify_checksum_valid() { let ctx = ChecksumContext::new(); let mut page = get_random_page(); ctx.add_checksum_to_page(&mut page, 2).unwrap(); let result = ctx.verify_checksum(&mut page, 2); assert!(result.is_ok()); } #[test] fn test_verify_checksum_mismatch() { let ctx = ChecksumContext::new(); let mut page = get_random_page(); ctx.add_checksum_to_page(&mut page, 2).unwrap(); // corrupt the data to cause checksum mismatch page[0] = 255; let result = ctx.verify_checksum(&mut page, 2); assert!(result.is_err()); match result.unwrap_err() { CompletionError::ChecksumMismatch { page_id, expected, actual, } => { assert_eq!(page_id, 2); assert_ne!(expected, actual); } _ => panic!("Expected ChecksumMismatch error"), } } #[test] fn test_verify_checksum_corrupted_checksum() { let ctx = ChecksumContext::new(); let mut page = get_random_page(); ctx.add_checksum_to_page(&mut page, 2).unwrap(); // corrupt the checksum itself page[CHECKSUM_PAGE_SIZE - 1] = 255; let result = ctx.verify_checksum(&mut page, 2); assert!(result.is_err()); match result.unwrap_err() { CompletionError::ChecksumMismatch { page_id, expected, actual, } => { assert_eq!(page_id, 2); assert_ne!(expected, actual); } _ => panic!("Expected ChecksumMismatch error"), } } } ================================================ FILE: core/storage/database.rs ================================================ use crate::io::FileSyncType; use crate::storage::checksum::ChecksumContext; use crate::storage::encryption::EncryptionContext; use crate::sync::Arc; use crate::{io::Completion, Buffer, CompletionError, LimboError, Result}; use crate::{ turso_assert, turso_assert_eq, turso_assert_greater_than, turso_assert_greater_than_or_equal, turso_assert_less_than_or_equal, }; use tracing::{instrument, Level}; #[derive(Debug, Clone)] pub enum EncryptionOrChecksum { Encryption(EncryptionContext), Checksum(ChecksumContext), None, } #[derive(Debug, Clone)] pub struct IOContext { encryption_or_checksum: EncryptionOrChecksum, } impl IOContext { pub fn encryption_context(&self) -> Option<&EncryptionContext> { match &self.encryption_or_checksum { EncryptionOrChecksum::Encryption(ctx) => Some(ctx), _ => None, } } pub fn get_reserved_space_bytes(&self) -> u8 { match &self.encryption_or_checksum { EncryptionOrChecksum::Encryption(ctx) => ctx.required_reserved_bytes(), EncryptionOrChecksum::Checksum(ctx) => ctx.required_reserved_bytes(), EncryptionOrChecksum::None => Default::default(), } } pub fn set_encryption(&mut self, encryption_ctx: EncryptionContext) { self.encryption_or_checksum = EncryptionOrChecksum::Encryption(encryption_ctx); } pub fn encryption_or_checksum(&self) -> &EncryptionOrChecksum { &self.encryption_or_checksum } pub fn reset_checksum(&mut self) { self.encryption_or_checksum = EncryptionOrChecksum::None; } } impl Default for IOContext { fn default() -> Self { #[cfg(feature = "checksum")] let encryption_or_checksum = EncryptionOrChecksum::Checksum(ChecksumContext::default()); #[cfg(not(feature = "checksum"))] let encryption_or_checksum = EncryptionOrChecksum::None; Self { encryption_or_checksum, } } } /// DatabaseStorage is an interface a database file that consists of pages. /// /// The purpose of this trait is to abstract the upper layers of Limbo from /// the storage medium. A database can either be a file on disk, like in SQLite, /// or something like a remote page server service. pub trait DatabaseStorage: Send + Sync { fn read_header(&self, c: Completion) -> Result; fn read_page(&self, page_idx: usize, io_ctx: &IOContext, c: Completion) -> Result; fn write_page( &self, page_idx: usize, buffer: Arc, io_ctx: &IOContext, c: Completion, ) -> Result; fn write_pages( &self, first_page_idx: usize, page_size: usize, buffers: Vec>, io_ctx: &IOContext, c: Completion, ) -> Result; fn sync(&self, c: Completion, sync_type: FileSyncType) -> Result; fn size(&self) -> Result; fn truncate(&self, len: usize, c: Completion) -> Result; } #[derive(Clone)] pub struct DatabaseFile { file: Arc, } impl DatabaseStorage for DatabaseFile { #[instrument(skip_all, level = Level::DEBUG)] fn read_header(&self, c: Completion) -> Result { self.file.pread(0, c) } #[instrument(skip_all, level = Level::DEBUG)] fn read_page(&self, page_idx: usize, io_ctx: &IOContext, c: Completion) -> Result { // casting to i64 to check some weird casting that could've happened before. This should be // okay since page numbers should be u32 turso_assert_greater_than_or_equal!(page_idx as i64, 0); let r = c.as_read(); let size = r.buf().len(); turso_assert_greater_than!(page_idx, 0); if !(512..=65536).contains(&size) || size & (size - 1) != 0 { return Err(LimboError::NotADB); } let Some(pos) = (page_idx as u64 - 1).checked_mul(size as u64) else { return Err(LimboError::IntegerOverflow); }; match &io_ctx.encryption_or_checksum { EncryptionOrChecksum::Encryption(ctx) => { let encryption_ctx = ctx.clone(); let read_buffer = r.buf_arc(); let original_c = c.clone(); let decrypt_complete = Box::new(move |res: Result<(Arc, i32), CompletionError>| { let (buf, bytes_read) = match res { Ok((buf, bytes_read)) => (buf, bytes_read), Err(err) => { tracing::error!(err = ?err); original_c.error(err); return original_c.get_error(); } }; turso_assert_greater_than!( bytes_read, 0, "database: expected to read data on success for encrypted page", { "page_idx": page_idx } ); match encryption_ctx.decrypt_page(buf.as_slice(), page_idx) { Ok(decrypted_data) => { let original_buf = original_c.as_read().buf(); original_buf.as_mut_slice().copy_from_slice(&decrypted_data); original_c.complete(bytes_read); original_c.get_error() } Err(e) => { tracing::error!( "Failed to decrypt page data for page_id={page_idx}: {e}" ); turso_assert!( !original_c.failed(), "Original completion already has an error" ); original_c.error(CompletionError::DecryptionError { page_idx }); original_c.get_error() } } }); let wrapped_completion = Completion::new_read(read_buffer, decrypt_complete); self.file.pread(pos, wrapped_completion) } EncryptionOrChecksum::Checksum(ctx) => { let checksum_ctx = ctx.clone(); let read_buffer = r.buf_arc(); let original_c = c.clone(); let verify_complete = Box::new(move |res: Result<(Arc, i32), CompletionError>| { let (buf, bytes_read) = match res { Ok((buf, bytes_read)) => (buf, bytes_read), Err(err) => { original_c.error(err); return original_c.get_error(); } }; if bytes_read <= 0 { tracing::trace!("Read page {page_idx} with {} bytes", bytes_read); original_c.complete(bytes_read); return original_c.get_error(); } match checksum_ctx.verify_checksum(buf.as_mut_slice(), page_idx) { Ok(_) => { original_c.complete(bytes_read); original_c.get_error() } Err(e) => { tracing::error!( "Failed to verify checksum for page_id={page_idx}: {e}" ); turso_assert!( !original_c.failed(), "Original completion already has an error" ); original_c.error(e); original_c.get_error() } } }); let wrapped_completion = Completion::new_read(read_buffer, verify_complete); self.file.pread(pos, wrapped_completion) } EncryptionOrChecksum::None => self.file.pread(pos, c), } } #[instrument(skip_all, level = Level::DEBUG)] fn write_page( &self, page_idx: usize, buffer: Arc, io_ctx: &IOContext, c: Completion, ) -> Result { let buffer_size = buffer.len(); turso_assert_greater_than!(page_idx, 0); turso_assert_greater_than_or_equal!(buffer_size, 512); turso_assert_less_than_or_equal!(buffer_size, 65536); turso_assert_eq!(buffer_size & (buffer_size - 1), 0); let Some(pos) = (page_idx as u64 - 1).checked_mul(buffer_size as u64) else { return Err(LimboError::IntegerOverflow); }; let buffer = match &io_ctx.encryption_or_checksum { EncryptionOrChecksum::Encryption(ctx) => encrypt_buffer(page_idx, buffer, ctx), EncryptionOrChecksum::Checksum(ctx) => checksum_buffer(page_idx, buffer, ctx), EncryptionOrChecksum::None => buffer, }; self.file.pwrite(pos, buffer, c) } fn write_pages( &self, first_page_idx: usize, page_size: usize, buffers: Vec>, io_ctx: &IOContext, c: Completion, ) -> Result { turso_assert_greater_than!(first_page_idx, 0); turso_assert_greater_than_or_equal!(page_size, 512); turso_assert_less_than_or_equal!(page_size, 65536); turso_assert_eq!(page_size & (page_size - 1), 0); let Some(pos) = (first_page_idx as u64 - 1).checked_mul(page_size as u64) else { return Err(LimboError::IntegerOverflow); }; let buffers = match &io_ctx.encryption_or_checksum() { EncryptionOrChecksum::Encryption(ctx) => buffers .into_iter() .enumerate() .map(|(i, buffer)| encrypt_buffer(first_page_idx + i, buffer, ctx)) .collect::>(), EncryptionOrChecksum::Checksum(ctx) => buffers .into_iter() .enumerate() .map(|(i, buffer)| checksum_buffer(first_page_idx + i, buffer, ctx)) .collect::>(), EncryptionOrChecksum::None => buffers, }; let c = self.file.pwritev(pos, buffers, c)?; Ok(c) } #[instrument(skip_all, level = Level::DEBUG)] fn sync(&self, c: Completion, sync_type: FileSyncType) -> Result { self.file.sync(c, sync_type) } #[instrument(skip_all, level = Level::DEBUG)] fn size(&self) -> Result { self.file.size() } #[instrument(skip_all, level = Level::INFO)] fn truncate(&self, len: usize, c: Completion) -> Result { let c = self.file.truncate(len as u64, c)?; Ok(c) } } #[cfg(feature = "fs")] impl DatabaseFile { pub fn new(file: Arc) -> Self { Self { file } } } fn encrypt_buffer(page_idx: usize, buffer: Arc, ctx: &EncryptionContext) -> Arc { let encrypted_data = ctx.encrypt_page(buffer.as_slice(), page_idx).unwrap(); Arc::new(Buffer::new(encrypted_data.to_vec())) } fn checksum_buffer(page_idx: usize, buffer: Arc, ctx: &ChecksumContext) -> Arc { ctx.add_checksum_to_page(buffer.as_mut_slice(), page_idx) .unwrap(); buffer } #[cfg(test)] mod tests { use super::*; use crate::{File, MemoryIO, IO}; struct MockFile { read_result: std::result::Result, } impl File for MockFile { fn lock_file(&self, _exclusive: bool) -> Result<()> { Ok(()) } fn unlock_file(&self) -> Result<()> { Ok(()) } fn pread(&self, _pos: u64, c: Completion) -> Result { match self.read_result { Ok(bytes_read) => c.complete(bytes_read), Err(err) => c.error(err), } Ok(c) } fn pwrite(&self, _pos: u64, _buffer: Arc, c: Completion) -> Result { c.complete(0); Ok(c) } fn sync(&self, c: Completion, _sync_type: FileSyncType) -> Result { c.complete(0); Ok(c) } fn size(&self) -> Result { Ok(0) } fn truncate(&self, _len: u64, c: Completion) -> Result { c.complete(0); Ok(c) } } #[cfg(feature = "checksum")] #[test] fn checksum_read_wrapper_propagates_callback_errors() { let db_file = DatabaseFile { file: Arc::new(MockFile { read_result: Ok(0) }), }; let io_ctx = IOContext::default(); let page_idx = 1usize; let expected = 4096usize; let buf = Arc::new(Buffer::new_temporary(expected)); let original = Completion::new_read(buf, move |res| { let (_, bytes_read) = res.expect("mock read should complete"); if bytes_read == 0 { Some(CompletionError::ShortRead { page_idx, expected, actual: 0, }) } else { None } }); let wrapped = db_file .read_page(page_idx, &io_ctx, original.clone()) .unwrap(); let io = MemoryIO::new(); let err = io .wait_for_completion(wrapped) .expect_err("wrapped completion must fail"); assert!(matches!( err, LimboError::CompletionError(CompletionError::ShortRead { .. }) )); assert!(matches!( original.get_error(), Some(CompletionError::ShortRead { .. }) )); } #[cfg(feature = "checksum")] #[test] fn checksum_read_wrapper_propagates_transport_errors_to_original_completion() { let db_file = DatabaseFile { file: Arc::new(MockFile { read_result: Err(CompletionError::Aborted), }), }; let io_ctx = IOContext::default(); let page_idx = 1usize; let buf = Arc::new(Buffer::new_temporary(4096)); let original = Completion::new_read(buf, |_res| None); let wrapped = db_file .read_page(page_idx, &io_ctx, original.clone()) .unwrap(); let io = MemoryIO::new(); let err = io .wait_for_completion(wrapped) .expect_err("wrapped completion must fail"); assert!(matches!( err, LimboError::CompletionError(CompletionError::Aborted) )); assert_eq!(original.get_error(), Some(CompletionError::Aborted)); } } ================================================ FILE: core/storage/encryption.rs ================================================ #![allow(unused_variables, dead_code)] use crate::turso_assert; use crate::{LimboError, Result}; use aegis::aegis128l::Aegis128L; use aegis::aegis128x2::Aegis128X2; use aegis::aegis128x4::Aegis128X4; use aegis::aegis256::Aegis256; use aegis::aegis256x2::Aegis256X2; use aegis::aegis256x4::Aegis256X4; use aes_gcm::{ aead::{Aead, AeadCore, KeyInit, OsRng}, Aes128Gcm, Aes256Gcm, Key, Nonce, }; use turso_macros::{match_ignore_ascii_case, AtomicEnum}; /// Encryption Scheme /// We support two major algorithms: AEGIS, AES GCM. These algorithms picked so that they also do /// verification of the ciphertext, so we don't need to implement. That is if the page is corrupted /// (or tampered), then we will know if we got garbage bytes post decryption. /// /// We perform encryption at the page level, i.e., each page is encrypted and decrypted individually. /// We store the nonce and tag (or the verification bits) in the page itself. We also generate a /// random nonce every time we encrypt a page. /// /// Example: Assume the page size is 4096 bytes and we use AEGIS 256. So we reserve the last 48 bytes /// for the nonce (32 bytes) and tag (16 bytes). /// /// ```ignore /// Unencrypted Page Encrypted Page /// ┌───────────────┐ ┌───────────────┐ /// │ │ │ │ /// │ Page Content │ │ Encrypted │ /// │ (4048 bytes) │ ────────► │ Content │ /// │ │ │ (4048 bytes) │ /// ├───────────────┤ ├───────────────┤ /// │ Reserved │ │ Tag (16) │ /// │ (48 bytes) │ ├───────────────┤ /// │ [empty] │ │ Nonce (32) │ /// └───────────────┘ └───────────────┘ /// 4096 bytes 4096 bytes /// ``` /// /// The above applies to all the pages except Page 1. The page 1 contains the SQLite header (the /// first 100 bytes). Specifically, the bytes 16 to 24 contain metadata which is required to /// initialise the connection, which happens before we can setup the encryption context. So, we /// don't encrypt the header but instead use the header data as additional data (AD) for the /// encryption of the rest of the page. This provides us protection against tampering and /// corruption for the unencrypted portion. /// /// On disk, the encrypted page 1 contains special bytes replacing the SQLite's magic bytes (the /// first 16 bytes): /// /// ```ignore /// Turso Header (16 bytes) /// ┌─────────┬───────┬────────┬──────────────────┐ /// │ │ │ │ │ /// │ Turso │Version│ Cipher │ Unused │ /// │ (5) │ (1) │ (1) │ (9 bytes) │ /// │ │ │ │ │ /// └─────────┴───────┴────────┴──────────────────┘ /// 0-4 5 6 7-15 /// /// Standard SQLite Header: "SQLite format 3\0" (16 bytes) /// ↓ /// Turso Encrypted Header: "Turso" + Version + Cipher ID + Unused /// ``` /// /// constants used for the Turso page header in the encrypted dbs. pub const TURSO_HEADER_PREFIX: &[u8] = b"Turso"; pub const SQLITE_HEADER: &[u8] = b"SQLite format 3\0"; const TURSO_VERSION: u8 = 0x00; const VERSION_OFFSET: usize = 5; const CIPHER_OFFSET: usize = 6; const TURSO_HEADER_SIZE: usize = 16; #[derive(Clone)] pub enum EncryptionKey { Key128([u8; 16]), Key256([u8; 32]), } impl EncryptionKey { pub fn new_256(key: [u8; 32]) -> Self { Self::Key256(key) } pub fn new_128(key: [u8; 16]) -> Self { Self::Key128(key) } pub fn from_hex_string(s: &str) -> Result { let hex_str = s.trim(); let bytes = hex::decode(hex_str) .map_err(|e| LimboError::InvalidArgument(format!("Invalid hex string: {e}")))?; match bytes.len() { 16 => { let key: [u8; 16] = bytes.try_into().unwrap(); Ok(Self::Key128(key)) } 32 => { let key: [u8; 32] = bytes.try_into().unwrap(); Ok(Self::Key256(key)) } _ => Err(LimboError::InvalidArgument(format!( "Hex string must decode to exactly 16 or 32 bytes, got {}", bytes.len() ))), } } pub fn as_slice(&self) -> &[u8] { match self { Self::Key128(key) => key, Self::Key256(key) => key, } } #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { match self { Self::Key128(_) => 16, Self::Key256(_) => 32, } } pub fn as_128(&self) -> Option<&[u8; 16]> { match self { Self::Key128(key) => Some(key), _ => None, } } pub fn as_256(&self) -> Option<&[u8; 32]> { match self { Self::Key256(key) => Some(key), _ => None, } } } impl std::fmt::Debug for EncryptionKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("EncryptionKey") .field("key", &"") .finish() } } impl Drop for EncryptionKey { fn drop(&mut self) { // securely zero out the key bytes before dropping match self { Self::Key128(key) => { for byte in key.iter_mut() { unsafe { std::ptr::write_volatile(byte, 0); } } } Self::Key256(key) => { for byte in key.iter_mut() { unsafe { std::ptr::write_volatile(byte, 0); } } } } } } macro_rules! define_aegis_cipher { ($struct_name:ident, $cipher_type:ty, key128, $nonce_size:literal, $name:literal) => { define_aegis_cipher!(@impl $struct_name, $cipher_type, $nonce_size, $name, 16, as_128); }; ($struct_name:ident, $cipher_type:ty, key256, $nonce_size:literal, $name:literal) => { define_aegis_cipher!(@impl $struct_name, $cipher_type, $nonce_size, $name, 32, as_256); }; (@impl $struct_name:ident, $cipher_type:ty, $nonce_size:literal, $name:literal, $key_size:literal, $key_method:ident) => { #[derive(Clone)] pub struct $struct_name { key: EncryptionKey, } impl $struct_name { const TAG_SIZE: usize = 16; fn new(key: &EncryptionKey) -> Self { Self { key: key.clone() } } fn encrypt(&self, plaintext: &[u8], ad: &[u8]) -> Result<(Vec, [u8; $nonce_size])> { let nonce = generate_secure_nonce::<$nonce_size>(); let key_bytes = self.key.$key_method() .ok_or_else(|| -> LimboError { CipherError::InvalidKeySize { cipher: $name, expected: $key_size }.into() })?; let (ciphertext, tag) = <$cipher_type>::new(key_bytes, &nonce).encrypt(plaintext, ad); let mut result = ciphertext; result.extend_from_slice(&tag); Ok((result, nonce)) } fn decrypt(&self, ciphertext: &[u8], nonce: &[u8; $nonce_size], ad: &[u8]) -> Result> { if ciphertext.len() < Self::TAG_SIZE { return Err(LimboError::from(CipherError::CiphertextTooShort { cipher: $name })); } let (ct, tag) = ciphertext.split_at(ciphertext.len() - Self::TAG_SIZE); let tag_array: [u8; 16] = tag.try_into().map_err(|_| -> LimboError { CipherError::InvalidTagSize { cipher: $name }.into() })?; let key_bytes = self.key.$key_method() .ok_or_else(|| -> LimboError { CipherError::InvalidKeySize { cipher: $name, expected: $key_size }.into() })?; <$cipher_type>::new(key_bytes, nonce) .decrypt(ct, &tag_array, ad) .map_err(|_| -> LimboError { CipherError::DecryptionFailed { cipher: $name }.into() }) } } impl std::fmt::Debug for $struct_name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct(stringify!($struct_name)) .field("key", &"") .finish() } } }; } macro_rules! define_aes_gcm_cipher { ($struct_name:ident, $cipher_type:ty, key128, $name:literal) => { define_aes_gcm_cipher!(@impl $struct_name, $cipher_type, $name, 16, as_128); }; ($struct_name:ident, $cipher_type:ty, key256, $name:literal) => { define_aes_gcm_cipher!(@impl $struct_name, $cipher_type, $name, 32, as_256); }; (@impl $struct_name:ident, $cipher_type:ty, $name:literal, $key_size:literal, $key_method:ident) => { #[derive(Clone)] pub struct $struct_name { cipher: $cipher_type, } impl $struct_name { const TAG_SIZE: usize = 16; const NONCE_SIZE: usize = 12; fn new(key: &EncryptionKey) -> Result { let key_bytes = key.$key_method() .ok_or_else(|| -> LimboError { CipherError::InvalidKeySize { cipher: $name, expected: $key_size }.into() })?; let cipher_key: &Key<$cipher_type> = key_bytes.into(); Ok(Self { cipher: <$cipher_type>::new(cipher_key), }) } fn encrypt(&self, plaintext: &[u8], ad: &[u8]) -> Result<(Vec, [u8; 12])> { let nonce = <$cipher_type>::generate_nonce(&mut OsRng); let ciphertext = self.cipher.encrypt(&nonce, aes_gcm::aead::Payload { msg: plaintext, aad: ad, }).map_err(|e| { LimboError::InternalError(format!("{} encryption failed: {e:?}", $name)) })?; let mut nonce_array = [0u8; 12]; nonce_array.copy_from_slice(&nonce); Ok((ciphertext, nonce_array)) } fn decrypt(&self, ciphertext: &[u8], nonce: &[u8; 12], ad: &[u8]) -> Result> { let nonce = Nonce::from_slice(nonce); self.cipher .decrypt(nonce, aes_gcm::aead::Payload { msg: ciphertext, aad: ad, }) .map_err(|_| -> LimboError { CipherError::DecryptionFailed { cipher: $name }.into() }) } } impl std::fmt::Debug for $struct_name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct(stringify!($struct_name)) .field("key", &"") .finish() } } }; } // AES-GCM ciphers define_aes_gcm_cipher!(Aes128GcmCipher, Aes128Gcm, key128, "AES-128-GCM"); define_aes_gcm_cipher!(Aes256GcmCipher, Aes256Gcm, key256, "AES-256-GCM"); // AEGIS ciphers define_aegis_cipher!(Aegis256Cipher, Aegis256::<16>, key256, 32, "AEGIS-256"); define_aegis_cipher!( Aegis256X2Cipher, Aegis256X2::<16>, key256, 32, "AEGIS-256X2" ); define_aegis_cipher!( Aegis256X4Cipher, Aegis256X4::<16>, key256, 32, "AEGIS-256X4" ); define_aegis_cipher!( Aegis128X2Cipher, Aegis128X2::<16>, key128, 16, "AEGIS-128X2" ); define_aegis_cipher!(Aegis128LCipher, Aegis128L::<16>, key128, 16, "AEGIS-128L"); define_aegis_cipher!( Aegis128X4Cipher, Aegis128X4::<16>, key128, 16, "AEGIS-128X4" ); #[derive(Debug, AtomicEnum, Clone, Copy, PartialEq, Eq)] pub enum CipherMode { None, Aes128Gcm, Aes256Gcm, Aegis256, Aegis128L, Aegis128X2, Aegis128X4, Aegis256X2, Aegis256X4, } impl TryFrom<&str> for CipherMode { type Error = LimboError; fn try_from(s: &str) -> Result { let s_bytes = s.as_bytes(); match_ignore_ascii_case!(match s_bytes { b"aes128gcm" | b"aes-128-gcm" | b"aes_128_gcm" => Ok(CipherMode::Aes128Gcm), b"aes256gcm" | b"aes-256-gcm" | b"aes_256_gcm" => Ok(CipherMode::Aes256Gcm), b"aegis256" | b"aegis-256" | b"aegis_256" => Ok(CipherMode::Aegis256), b"aegis128l" | b"aegis-128l" | b"aegis_128l" => Ok(CipherMode::Aegis128L), b"aegis128x2" | b"aegis-128x2" | b"aegis_128x2" => Ok(CipherMode::Aegis128X2), b"aegis128x4" | b"aegis-128x4" | b"aegis_128x4" => Ok(CipherMode::Aegis128X4), b"aegis256x2" | b"aegis-256x2" | b"aegis_256x2" => Ok(CipherMode::Aegis256X2), b"aegis256x4" | b"aegis-256x4" | b"aegis_256x4" => Ok(CipherMode::Aegis256X4), _ => Err(LimboError::InvalidArgument(format!( "Unknown cipher name: {s}" ))), }) } } impl std::fmt::Display for CipherMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CipherMode::Aes128Gcm => write!(f, "aes128gcm"), CipherMode::Aes256Gcm => write!(f, "aes256gcm"), CipherMode::Aegis256 => write!(f, "aegis256"), CipherMode::Aegis128L => write!(f, "aegis128l"), CipherMode::Aegis128X2 => write!(f, "aegis128x2"), CipherMode::Aegis128X4 => write!(f, "aegis128x4"), CipherMode::Aegis256X2 => write!(f, "aegis256x2"), CipherMode::Aegis256X4 => write!(f, "aegis256x4"), CipherMode::None => write!(f, "None"), } } } impl CipherMode { /// Every cipher requires a specific key size. For 256-bit algorithms, this is 32 bytes. /// For 128-bit algorithms, it would be 16 bytes, etc. pub fn required_key_size(&self) -> usize { match self { CipherMode::Aes128Gcm => 16, CipherMode::Aes256Gcm => 32, CipherMode::Aegis256 => 32, CipherMode::Aegis256X2 => 32, CipherMode::Aegis256X4 => 32, CipherMode::Aegis128L => 16, CipherMode::Aegis128X2 => 16, CipherMode::Aegis128X4 => 16, CipherMode::None => 0, } } /// Returns the nonce size for this cipher mode. pub fn nonce_size(&self) -> usize { match self { CipherMode::Aes128Gcm => 12, CipherMode::Aes256Gcm => 12, CipherMode::Aegis256 => 32, CipherMode::Aegis256X2 => 32, CipherMode::Aegis256X4 => 32, CipherMode::Aegis128L => 16, CipherMode::Aegis128X2 => 16, CipherMode::Aegis128X4 => 16, CipherMode::None => 0, } } /// Returns the authentication tag size for this cipher mode. pub fn tag_size(&self) -> usize { match self { CipherMode::Aes128Gcm => 16, CipherMode::Aes256Gcm => 16, CipherMode::Aegis256 => 16, CipherMode::Aegis256X2 => 16, CipherMode::Aegis256X4 => 16, CipherMode::Aegis128L => 16, CipherMode::Aegis128X2 => 16, CipherMode::Aegis128X4 => 16, CipherMode::None => 0, } } /// Returns the total metadata size (nonce + tag) for this cipher mode. pub fn metadata_size(&self) -> usize { self.nonce_size() + self.tag_size() } /// Returns the cipher identifier byte for Turso header pub fn cipher_id(&self) -> u8 { match self { CipherMode::Aes128Gcm => 1, CipherMode::Aes256Gcm => 2, CipherMode::Aegis256 => 3, CipherMode::Aegis256X2 => 4, CipherMode::Aegis256X4 => 5, CipherMode::Aegis128L => 6, CipherMode::Aegis128X2 => 7, CipherMode::Aegis128X4 => 8, CipherMode::None => 0, } } /// Creates a CipherMode from cipher identifier byte. This is used when read from Turso header. pub fn from_cipher_id(id: u8) -> Result { match id { 1 => Ok(CipherMode::Aes128Gcm), 2 => Ok(CipherMode::Aes256Gcm), 3 => Ok(CipherMode::Aegis256), 4 => Ok(CipherMode::Aegis256X2), 5 => Ok(CipherMode::Aegis256X4), 6 => Ok(CipherMode::Aegis128L), 7 => Ok(CipherMode::Aegis128X2), 8 => Ok(CipherMode::Aegis128X4), _ => Err(LimboError::InvalidArgument(format!( "Unknown cipher ID: {id}" ))), } } } #[derive(Clone)] pub enum Cipher { Aes128Gcm(Box), Aes256Gcm(Box), Aegis256(Box), Aegis256X2(Box), Aegis256X4(Box), Aegis128L(Box), Aegis128X2(Box), Aegis128X4(Box), } impl std::fmt::Debug for Cipher { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Cipher::Aes128Gcm(_) => write!(f, "Cipher::Aes128Gcm"), Cipher::Aes256Gcm(_) => write!(f, "Cipher::Aes256Gcm"), Cipher::Aegis256(_) => write!(f, "Cipher::Aegis256"), Cipher::Aegis256X2(_) => write!(f, "Cipher::Aegis256X2"), Cipher::Aegis256X4(_) => write!(f, "Cipher::Aegis256X4"), Cipher::Aegis128L(_) => write!(f, "Cipher::Aegis128L"), Cipher::Aegis128X2(_) => write!(f, "Cipher::Aegis128X2"), Cipher::Aegis128X4(_) => write!(f, "Cipher::Aegis128X4"), } } } #[derive(Debug, Clone)] pub struct EncryptionContext { cipher_mode: CipherMode, cipher: Cipher, page_size: usize, } impl EncryptionContext { pub fn new(cipher_mode: CipherMode, key: &EncryptionKey, page_size: usize) -> Result { let required_size = cipher_mode.required_key_size(); if key.len() != required_size { return Err(crate::LimboError::InvalidArgument(format!( "Invalid key size for {:?}: expected {} bytes, got {}", cipher_mode, required_size, key.len() ))); } let cipher = match cipher_mode { CipherMode::Aes128Gcm => Cipher::Aes128Gcm(Box::new(Aes128GcmCipher::new(key)?)), CipherMode::Aes256Gcm => Cipher::Aes256Gcm(Box::new(Aes256GcmCipher::new(key)?)), CipherMode::Aegis256 => Cipher::Aegis256(Box::new(Aegis256Cipher::new(key))), CipherMode::Aegis256X2 => Cipher::Aegis256X2(Box::new(Aegis256X2Cipher::new(key))), CipherMode::Aegis256X4 => Cipher::Aegis256X4(Box::new(Aegis256X4Cipher::new(key))), CipherMode::Aegis128L => Cipher::Aegis128L(Box::new(Aegis128LCipher::new(key))), CipherMode::Aegis128X2 => Cipher::Aegis128X2(Box::new(Aegis128X2Cipher::new(key))), CipherMode::Aegis128X4 => Cipher::Aegis128X4(Box::new(Aegis128X4Cipher::new(key))), CipherMode::None => { return Err(LimboError::InvalidArgument( "must select valid CipherMode".into(), )) } }; Ok(Self { cipher_mode, cipher, page_size, }) } pub fn cipher_mode(&self) -> CipherMode { self.cipher_mode } /// Returns the number of reserved bytes required at the end of each page for encryption metadata. pub fn required_reserved_bytes(&self) -> u8 { self.cipher_mode.metadata_size() as u8 } pub fn encrypt_chunk(&self, plaintext: &[u8], aad: &[u8]) -> Result<(Vec, Vec)> { self.encrypt_raw_with_ad(plaintext, aad) } pub fn decrypt_chunk(&self, ciphertext: &[u8], nonce: &[u8], aad: &[u8]) -> Result> { self.decrypt_raw_with_ad(ciphertext, nonce, aad) } pub fn nonce_size(&self) -> usize { self.cipher_mode.nonce_size() } pub fn tag_size(&self) -> usize { self.cipher_mode.tag_size() } /// Creates Turso header for encrypted page 1 fn create_turso_header(&self) -> [u8; TURSO_HEADER_SIZE] { let mut header = [0u8; TURSO_HEADER_SIZE]; // "Turso" prefix (5 bytes) header[..TURSO_HEADER_PREFIX.len()].copy_from_slice(TURSO_HEADER_PREFIX); // version byte (1 byte) header[VERSION_OFFSET] = TURSO_VERSION; // cipher identifier (1 byte) header[CIPHER_OFFSET] = self.cipher_mode.cipher_id(); // remaining unused 9 bytes header } /// Validates and extracts cipher mode from Turso header fn validate_turso_header(&self, header: &[u8]) -> Result<()> { if header.len() < TURSO_HEADER_SIZE { return Err(LimboError::InternalError( "Header too short for encrypted Turso db".into(), )); } if &header[..TURSO_HEADER_PREFIX.len()] != TURSO_HEADER_PREFIX { return Err(LimboError::InternalError( "Invalid Turso header: prefix mismatch".into(), )); } let version = header[VERSION_OFFSET]; if version != TURSO_VERSION { return Err(LimboError::InternalError(format!( "Unsupported Turso header version: expected {TURSO_VERSION}, got {version}" ))); } let cipher_id = header[CIPHER_OFFSET]; let header_cipher = CipherMode::from_cipher_id(cipher_id)?; if header_cipher != self.cipher_mode { return Err(LimboError::InternalError(format!( "Cipher mode mismatch: expected {:?} (ID {}), got {:?} (ID {})", self.cipher_mode, self.cipher_mode.cipher_id(), header_cipher, cipher_id ))); } if header[CIPHER_OFFSET + 1..TURSO_HEADER_SIZE] .iter() .any(|&b| b != 0) { return Err(LimboError::InternalError( "Invalid Turso header: unused bytes must be zero".into(), )); } Ok(()) } #[cfg(feature = "encryption")] pub fn encrypt_page(&self, page: &[u8], page_id: usize) -> Result> { use crate::storage::sqlite3_ondisk::DatabaseHeader; if page_id == DatabaseHeader::PAGE_ID { return self.encrypt_page_1(page); } tracing::debug!("encrypting page {}", page_id); assert_eq!( page.len(), self.page_size, "Page data must be exactly {} bytes", self.page_size ); let metadata_size = self.cipher_mode.metadata_size(); let reserved_bytes = &page[self.page_size - metadata_size..]; #[cfg(debug_assertions)] { let reserved_bytes_zeroed = reserved_bytes.iter().all(|&b| b == 0); turso_assert!( reserved_bytes_zeroed, "last reserved bytes must be empty/zero, but found non-zero bytes" ); } let payload = &page[..self.page_size - metadata_size]; let (encrypted, nonce) = self.encrypt_raw(payload)?; let nonce_size = self.cipher_mode.nonce_size(); assert_eq!( encrypted.len(), self.page_size - nonce_size, "Encrypted page must be exactly {} bytes", self.page_size - nonce_size ); let mut result = Vec::with_capacity(self.page_size); result.extend_from_slice(&encrypted); result.extend_from_slice(&nonce); assert_eq!( result.len(), self.page_size, "Encrypted page must be exactly {} bytes", self.page_size ); Ok(result) } #[cfg(feature = "encryption")] pub fn decrypt_page(&self, encrypted_page: &[u8], page_id: usize) -> Result> { use crate::storage::sqlite3_ondisk::DatabaseHeader; if page_id == DatabaseHeader::PAGE_ID { return self.decrypt_page_1(encrypted_page); } tracing::debug!("decrypting page {}", page_id); assert_eq!( encrypted_page.len(), self.page_size, "Encrypted page data must be exactly {} bytes", self.page_size ); let nonce_size = self.cipher_mode.nonce_size(); let nonce_offset = encrypted_page.len() - nonce_size; let payload = &encrypted_page[..nonce_offset]; let nonce = &encrypted_page[nonce_offset..]; let decrypted_data = self.decrypt_raw(payload, nonce)?; let metadata_size = self.cipher_mode.metadata_size(); assert_eq!( decrypted_data.len(), self.page_size - metadata_size, "Decrypted page data must be exactly {} bytes", self.page_size - metadata_size ); let mut result = Vec::with_capacity(self.page_size); result.extend_from_slice(&decrypted_data); result.resize(self.page_size, 0); assert_eq!( result.len(), self.page_size, "Decrypted page data must be exactly {} bytes", self.page_size ); Ok(result) } #[cfg(feature = "encryption")] fn encrypt_page_1(&self, page: &[u8]) -> Result> { use crate::storage::sqlite3_ondisk::DatabaseHeader; tracing::debug!("encrypting page 1"); assert_eq!( page.len(), self.page_size, "Page data must be exactly {} bytes", self.page_size ); // since this is page 1, this must have header turso_assert!( page.starts_with(SQLITE_HEADER), "Page 1 must start with SQLite header" ); let metadata_size = self.cipher_mode.metadata_size(); let reserved_bytes = &page[self.page_size - metadata_size..]; #[cfg(debug_assertions)] { // In debug builds, ensure that the reserved bytes are zeroed out. So even when we are // reusing a page from buffer pool, we zero out in debug build so that we can be // sure that b tree layer is not writing any data into the reserved space. // We avoid calling `memset` in release builds for performance reasons. let reserved_bytes_zeroed = reserved_bytes.iter().all(|&b| b == 0); turso_assert!( reserved_bytes_zeroed, "last reserved bytes must be empty/zero, but found non-zero bytes" ); } // page 1 encryption: // 1. First 16 bytes are replaced with Turso magic bytes // 2. Next 84 bytes (16-100) are kept as-is (not encrypted) // 3. Remaining bytes (100-end) are encrypted // 4. The header (the first 100 bytes) as associated data let turso_header = self.create_turso_header(); let mut new_header = Vec::with_capacity(DatabaseHeader::SIZE); new_header.extend_from_slice(&turso_header); new_header.extend_from_slice(&page[TURSO_HEADER_SIZE..DatabaseHeader::SIZE]); let payload = &page[DatabaseHeader::SIZE..self.page_size - metadata_size]; let (encrypted, nonce) = self.encrypt_raw_with_ad(payload, &new_header)?; let nonce_size = self.cipher_mode.nonce_size(); assert_eq!( encrypted.len(), self.page_size - nonce_size - DatabaseHeader::SIZE, "Encrypted page must be exactly {} bytes", self.page_size - nonce_size - DatabaseHeader::SIZE ); let mut result = Vec::with_capacity(self.page_size); // 1. copy the header result.append(&mut new_header); // 2. copy the encrypted payload result.extend_from_slice(&encrypted); // 3. now add the nonce result.extend_from_slice(&nonce); assert_eq!( result.len(), self.page_size, "Encrypted page must be exactly {} bytes", self.page_size ); Ok(result) } #[cfg(feature = "encryption")] fn decrypt_page_1(&self, encrypted_page: &[u8]) -> Result> { use crate::storage::sqlite3_ondisk::DatabaseHeader; tracing::debug!("decrypting page 1"); assert_eq!( encrypted_page.len(), self.page_size, "Encrypted page data must be exactly {} bytes", self.page_size ); self.validate_turso_header(&encrypted_page[..TURSO_HEADER_SIZE])?; let nonce_size = self.cipher_mode.nonce_size(); let nonce_offset = encrypted_page.len() - nonce_size; let payload = &encrypted_page[DatabaseHeader::SIZE..nonce_offset]; let nonce = &encrypted_page[nonce_offset..]; // it's important to use the header on disk (with Turso magic bytes) as associated data // for protection against tampering the header let header = &encrypted_page[..DatabaseHeader::SIZE]; let decrypted_data = self.decrypt_raw_with_ad(payload, nonce, header)?; let metadata_size = self.cipher_mode.metadata_size(); assert_eq!( decrypted_data.len(), self.page_size - metadata_size - DatabaseHeader::SIZE, "Decrypted page data must be exactly {} bytes", self.page_size - metadata_size - DatabaseHeader::SIZE ); // reconstruct the page with the appropriate SQLite header let mut result = Vec::with_capacity(self.page_size); result.extend_from_slice(SQLITE_HEADER); result.extend_from_slice(&encrypted_page[TURSO_HEADER_SIZE..DatabaseHeader::SIZE]); result.extend_from_slice(&decrypted_data); result.resize(self.page_size, 0); assert_eq!( result.len(), self.page_size, "Decrypted page data must be exactly {} bytes", self.page_size ); Ok(result) } /// encrypts raw data using the configured cipher, returns ciphertext and nonce fn encrypt_raw(&self, plaintext: &[u8]) -> Result<(Vec, Vec)> { const AD: &[u8] = b""; self.encrypt_raw_with_ad(plaintext, AD) } /// encrypts raw data with associated data using the configured cipher fn encrypt_raw_with_ad(&self, plaintext: &[u8], ad: &[u8]) -> Result<(Vec, Vec)> { macro_rules! encrypt_cipher { ($cipher:expr) => {{ let (ciphertext, nonce) = $cipher.encrypt(plaintext, ad)?; Ok((ciphertext, nonce.to_vec())) }}; } match &self.cipher { Cipher::Aes128Gcm(cipher) => encrypt_cipher!(cipher), Cipher::Aes256Gcm(cipher) => encrypt_cipher!(cipher), Cipher::Aegis256(cipher) => encrypt_cipher!(cipher), Cipher::Aegis256X2(cipher) => encrypt_cipher!(cipher), Cipher::Aegis256X4(cipher) => encrypt_cipher!(cipher), Cipher::Aegis128L(cipher) => encrypt_cipher!(cipher), Cipher::Aegis128X2(cipher) => encrypt_cipher!(cipher), Cipher::Aegis128X4(cipher) => encrypt_cipher!(cipher), } } fn decrypt_raw(&self, ciphertext: &[u8], nonce: &[u8]) -> Result> { const AD: &[u8] = b""; self.decrypt_raw_with_ad(ciphertext, nonce, AD) } fn decrypt_raw_with_ad(&self, ciphertext: &[u8], nonce: &[u8], ad: &[u8]) -> Result> { macro_rules! decrypt_with_nonce { ($cipher:expr, $nonce_size:literal, $name:literal) => {{ let nonce_array: [u8; $nonce_size] = nonce.try_into().map_err(|_| { LimboError::InternalError(format!( "Invalid nonce size for {}: expected {}, got {}", $name, $nonce_size, nonce.len() )) })?; $cipher.decrypt(ciphertext, &nonce_array, ad) }}; } match &self.cipher { Cipher::Aes128Gcm(cipher) => decrypt_with_nonce!(cipher, 12, "AES-128-GCM"), Cipher::Aes256Gcm(cipher) => decrypt_with_nonce!(cipher, 12, "AES-256-GCM"), Cipher::Aegis256(cipher) => decrypt_with_nonce!(cipher, 32, "AEGIS-256"), Cipher::Aegis256X2(cipher) => decrypt_with_nonce!(cipher, 32, "AEGIS-256X2"), Cipher::Aegis256X4(cipher) => decrypt_with_nonce!(cipher, 32, "AEGIS-256X4"), Cipher::Aegis128L(cipher) => decrypt_with_nonce!(cipher, 16, "AEGIS-128L"), Cipher::Aegis128X2(cipher) => decrypt_with_nonce!(cipher, 16, "AEGIS-128X2"), Cipher::Aegis128X4(cipher) => decrypt_with_nonce!(cipher, 16, "AEGIS-128X4"), } } #[cfg(not(feature = "encryption"))] pub fn encrypt_page(&self, _page: &[u8], _page_id: usize) -> Result> { Err(LimboError::InvalidArgument( "encryption is not enabled, cannot encrypt page. enable via passing `--features encryption`".into(), )) } #[cfg(not(feature = "encryption"))] pub fn decrypt_page(&self, _encrypted_page: &[u8], _page_id: usize) -> Result> { Err(LimboError::InvalidArgument( "encryption is not enabled, cannot decrypt page. enable via passing `--features encryption`".into(), )) } } fn generate_secure_nonce() -> [u8; N] { // use OsRng directly to fill bytes, generic over nonce size use aes_gcm::aead::rand_core::RngCore; let mut nonce = [0u8; N]; OsRng.fill_bytes(&mut nonce); nonce } // Helper functions for consistent error messages enum CipherError { InvalidKeySize { cipher: &'static str, expected: usize, }, InvalidTagSize { cipher: &'static str, }, DecryptionFailed { cipher: &'static str, }, CiphertextTooShort { cipher: &'static str, }, } impl From for LimboError { fn from(err: CipherError) -> Self { let msg = match err { CipherError::InvalidKeySize { cipher, expected } => { format!("{cipher} requires {expected}-byte key") } CipherError::InvalidTagSize { cipher } => format!("Invalid tag size for {cipher}"), CipherError::DecryptionFailed { cipher } => { format!("{cipher} decryption failed: invalid tag") } CipherError::CiphertextTooShort { cipher } => { format!("Ciphertext too short for {cipher}") } }; LimboError::InternalError(msg) } } #[cfg(test)] #[cfg(feature = "encryption")] mod tests { use crate::storage::sqlite3_ondisk::DatabaseHeader; use super::*; use rand::Rng; const DEFAULT_ENCRYPTED_PAGE_SIZE: usize = 4096; macro_rules! test_cipher_wrapper { ($test_name:ident, $cipher_type:ty, $key_gen:expr, $nonce_size:literal, $message:literal) => { #[test] fn $test_name() { let key = EncryptionKey::from_hex_string(&$key_gen()).unwrap(); let cipher = <$cipher_type>::new(&key); let plaintext = $message.as_bytes(); let ad = b"additional data"; let (ciphertext, nonce) = cipher.encrypt(plaintext, ad).unwrap(); assert_eq!(nonce.len(), $nonce_size); assert_ne!(ciphertext[..plaintext.len()], plaintext[..]); let decrypted = cipher.decrypt(&ciphertext, &nonce, ad).unwrap(); assert_eq!(decrypted, plaintext); } }; } macro_rules! test_aes_cipher_wrapper { ($test_name:ident, $cipher_type:ty, $key_gen:expr, $nonce_size:literal, $message:literal) => { #[test] fn $test_name() { let key = EncryptionKey::from_hex_string(&$key_gen()).unwrap(); let cipher = <$cipher_type>::new(&key).unwrap(); let plaintext = $message.as_bytes(); let ad = b"additional data"; let (ciphertext, nonce) = cipher.encrypt(plaintext, ad).unwrap(); assert_eq!(nonce.len(), $nonce_size); assert_ne!(ciphertext[..plaintext.len()], plaintext[..]); let decrypted = cipher.decrypt(&ciphertext, &nonce, ad).unwrap(); assert_eq!(decrypted, plaintext); } }; } macro_rules! test_raw_encryption { ($test_name:ident, $cipher_mode:expr, $key_gen:expr, $nonce_size:literal, $message:literal) => { #[test] fn $test_name() { let key = EncryptionKey::from_hex_string(&$key_gen()).unwrap(); let ctx = EncryptionContext::new($cipher_mode, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let plaintext = $message.as_bytes(); let (ciphertext, nonce) = ctx.encrypt_raw(plaintext).unwrap(); assert_eq!(nonce.len(), $nonce_size); assert_ne!(ciphertext[..plaintext.len()], plaintext[..]); let decrypted = ctx.decrypt_raw(&ciphertext, &nonce).unwrap(); assert_eq!(decrypted, plaintext); } }; } fn generate_random_hex_key() -> String { let mut rng = rand::rng(); let mut bytes = [0u8; 32]; rng.fill(&mut bytes); hex::encode(bytes) } fn generate_random_hex_key_128() -> String { let mut rng = rand::rng(); let mut bytes = [0u8; 16]; rng.fill(&mut bytes); hex::encode(bytes) } fn create_test_page_1() -> Vec { let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE]; page[..SQLITE_HEADER.len()].copy_from_slice(SQLITE_HEADER); let mut rng = rand::rng(); // 48 is the max reserved bytes we might need for metadata with any cipher rng.fill(&mut page[SQLITE_HEADER.len()..DEFAULT_ENCRYPTED_PAGE_SIZE - 48]); page } test_aes_cipher_wrapper!( test_aes128gcm_cipher_wrapper, Aes128GcmCipher, generate_random_hex_key_128, 12, "Hello, AES-128-GCM!" ); test_raw_encryption!( test_aes128gcm_raw_encryption, CipherMode::Aes128Gcm, generate_random_hex_key_128, 12, "Hello, AES-128-GCM!" ); #[test] fn test_page_1_encrypt_decrypt_round_trip_with_ad() { let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_data = create_test_page_1(); let encrypted = ctx.encrypt_page(&page_data, 1).unwrap(); assert_ne!( &page_data[0..DatabaseHeader::SIZE], &encrypted[0..DatabaseHeader::SIZE], "Encrypted data should be different from the page data" ); assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); // check that header is readable directly from disk (not encrypted) assert_eq!(&encrypted[..5], b"Turso"); assert_eq!(encrypted[5], TURSO_VERSION); assert_eq!(encrypted[6], CipherMode::Aegis256.cipher_id()); // header should be unencrypted, but data after DatabaseHeader::SIZE should be different assert_eq!(&encrypted[16..100], &page_data[16..100]); // header portion assert_ne!(&encrypted[100..200], &page_data[100..200]); // some encrypted portion // decrypt page 1 let decrypted = ctx.decrypt_page(&encrypted, 1).unwrap(); assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); // check that SQLite header was restored assert_eq!(&decrypted[..SQLITE_HEADER.len()], SQLITE_HEADER); assert_eq!(decrypted, page_data); } #[test] fn test_turso_header_validation() { let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); // test cipher_id conversion assert_eq!(CipherMode::Aes128Gcm.cipher_id(), 1); assert_eq!(CipherMode::Aes256Gcm.cipher_id(), 2); assert_eq!(CipherMode::Aegis256.cipher_id(), 3); assert_eq!(CipherMode::Aegis128L.cipher_id(), 6); // test from_cipher_id conversion assert_eq!( CipherMode::from_cipher_id(1).unwrap(), CipherMode::Aes128Gcm ); assert_eq!(CipherMode::from_cipher_id(3).unwrap(), CipherMode::Aegis256); assert!(CipherMode::from_cipher_id(99).is_err()); // test header creation let header = ctx.create_turso_header(); assert_eq!(&header[..5], b"Turso"); assert_eq!(header[5], TURSO_VERSION); assert_eq!(header[6], 3); // AEGIS-256 assert_eq!(&header[7..], &[0u8; 9]); // unused bytes are zero } #[test] fn test_invalid_turso_header_fails_decrypt() { let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_data = create_test_page_1(); let encrypted = ctx.encrypt_page(&page_data, 1).unwrap(); // corrupt the header prefix let mut corrupted = encrypted.clone(); corrupted[0] = b'V'; // make `Turso` to `Vurso` assert!(ctx.decrypt_page(&corrupted, 1).is_err()); // test with wrong cipher ID let mut wrong_cipher = encrypted; wrong_cipher[6] = 99; // invalid cipher ID assert!(ctx.decrypt_page(&wrong_cipher, 1).is_err()); } #[test] fn test_associated_data_validation() { let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_data = create_test_page_1(); let encrypted = ctx.encrypt_page(&page_data, 1).unwrap(); // modify a byte in the preserved header portion (bytes 16-100) let mut corrupted_ad = encrypted; corrupted_ad[50] ^= 1; // flip one bit in the associated data portion // this should fail decryption because associated data doesn't match let decrypt_result = ctx.decrypt_page(&corrupted_ad, 1); assert!( decrypt_result.is_err(), "Decryption should fail with corrupted associated data" ); } #[test] fn test_turso_header_corruption_detection() { let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_data = create_test_page_1(); let encrypted = ctx.encrypt_page(&page_data, 1).unwrap(); let mut corrupted_turso_header = encrypted; corrupted_turso_header[7] ^= 1; let decrypt_result = ctx.decrypt_page(&corrupted_turso_header, 1); assert!( decrypt_result.is_err(), "Decryption should fail with corrupted Turso header" ); } #[test] fn test_aes128gcm_encrypt_decrypt_round_trip() { let mut rng = rand::rng(); let cipher_mode = CipherMode::Aes128Gcm; let metadata_size = cipher_mode.metadata_size(); let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size; let page_data = { let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE]; page.iter_mut() .take(data_size) .for_each(|byte| *byte = rng.random()); page }; let key = EncryptionKey::from_hex_string(&generate_random_hex_key_128()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aes128Gcm, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_id = 42; let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap(); assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_ne!(&encrypted[..data_size], &page_data[..data_size]); assert_ne!(&encrypted[..], &page_data[..]); let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap(); assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_eq!(decrypted, page_data); } #[test] fn test_aes_encrypt_decrypt_round_trip() { let mut rng = rand::rng(); let cipher_mode = CipherMode::Aes256Gcm; let metadata_size = cipher_mode.metadata_size(); let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size; let page_data = { let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE]; page.iter_mut() .take(data_size) .for_each(|byte| *byte = rng.random()); page }; let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aes256Gcm, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_id = 42; let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap(); assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_ne!(&encrypted[..data_size], &page_data[..data_size]); assert_ne!(&encrypted[..], &page_data[..]); let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap(); assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_eq!(decrypted, page_data); } test_cipher_wrapper!( test_aegis256_cipher_wrapper, Aegis256Cipher, generate_random_hex_key, 32, "Hello, AEGIS-256!" ); test_raw_encryption!( test_aegis256_raw_encryption, CipherMode::Aegis256, generate_random_hex_key, 32, "Hello, AEGIS-256!" ); #[test] fn test_aegis256_encrypt_decrypt_round_trip() { let mut rng = rand::rng(); let cipher_mode = CipherMode::Aegis256; let metadata_size = cipher_mode.metadata_size(); let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size; let page_data = { let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE]; page.iter_mut() .take(data_size) .for_each(|byte| *byte = rng.random()); page }; let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aegis256, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_id = 42; let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap(); assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_ne!(&encrypted[..data_size], &page_data[..data_size]); let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap(); assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_eq!(decrypted, page_data); } test_cipher_wrapper!( test_aegis128x2_cipher_wrapper, Aegis128X2Cipher, generate_random_hex_key_128, 16, "Hello, AEGIS-128X2!" ); test_raw_encryption!( test_aegis128x2_raw_encryption, CipherMode::Aegis128X2, generate_random_hex_key_128, 16, "Hello, AEGIS-128X2!" ); #[test] fn test_aegis128x2_encrypt_decrypt_round_trip() { let mut rng = rand::rng(); let cipher_mode = CipherMode::Aegis128X2; let metadata_size = cipher_mode.metadata_size(); let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size; let page_data = { let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE]; page.iter_mut() .take(data_size) .for_each(|byte| *byte = rng.random()); page }; let key = EncryptionKey::from_hex_string(&generate_random_hex_key_128()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aegis128X2, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_id = 42; let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap(); assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_ne!(&encrypted[..data_size], &page_data[..data_size]); let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap(); assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_eq!(decrypted, page_data); } test_cipher_wrapper!( test_aegis128l_cipher_wrapper, Aegis128LCipher, generate_random_hex_key_128, 16, "Hello, AEGIS-128L!" ); test_raw_encryption!( test_aegis128l_raw_encryption, CipherMode::Aegis128L, generate_random_hex_key_128, 16, "Hello, AEGIS-128L!" ); #[test] fn test_aegis128l_encrypt_decrypt_round_trip() { let mut rng = rand::rng(); let cipher_mode = CipherMode::Aegis128L; let metadata_size = cipher_mode.metadata_size(); let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size; let page_data = { let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE]; page.iter_mut() .take(data_size) .for_each(|byte| *byte = rng.random()); page }; let key = EncryptionKey::from_hex_string(&generate_random_hex_key_128()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aegis128L, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_id = 42; let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap(); assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_ne!(&encrypted[..data_size], &page_data[..data_size]); let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap(); assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_eq!(decrypted, page_data); } test_cipher_wrapper!( test_aegis128x4_cipher_wrapper, Aegis128X4Cipher, generate_random_hex_key_128, 16, "Hello, AEGIS-128X4!" ); test_raw_encryption!( test_aegis128x4_raw_encryption, CipherMode::Aegis128X4, generate_random_hex_key_128, 16, "Hello, AEGIS-128X4!" ); #[test] fn test_aegis128x4_encrypt_decrypt_round_trip() { let mut rng = rand::rng(); let cipher_mode = CipherMode::Aegis128X4; let metadata_size = cipher_mode.metadata_size(); let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size; let page_data = { let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE]; page.iter_mut() .take(data_size) .for_each(|byte| *byte = rng.random()); page }; let key = EncryptionKey::from_hex_string(&generate_random_hex_key_128()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aegis128X4, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_id = 42; let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap(); assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_ne!(&encrypted[..data_size], &page_data[..data_size]); let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap(); assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_eq!(decrypted, page_data); } test_cipher_wrapper!( test_aegis256x2_cipher_wrapper, Aegis256X2Cipher, generate_random_hex_key, 32, "Hello, AEGIS-256X2!" ); test_raw_encryption!( test_aegis256x2_raw_encryption, CipherMode::Aegis256X2, generate_random_hex_key, 32, "Hello, AEGIS-256X2!" ); #[test] fn test_aegis256x2_encrypt_decrypt_round_trip() { let mut rng = rand::rng(); let cipher_mode = CipherMode::Aegis256X2; let metadata_size = cipher_mode.metadata_size(); let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size; let page_data = { let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE]; page.iter_mut() .take(data_size) .for_each(|byte| *byte = rng.random()); page }; let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aegis256X2, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_id = 42; let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap(); assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_ne!(&encrypted[..data_size], &page_data[..data_size]); let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap(); assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_eq!(decrypted, page_data); } test_cipher_wrapper!( test_aegis256x4_cipher_wrapper, Aegis256X4Cipher, generate_random_hex_key, 32, "Hello, AEGIS-256X4!" ); test_raw_encryption!( test_aegis256x4_raw_encryption, CipherMode::Aegis256X4, generate_random_hex_key, 32, "Hello, AEGIS-256X4!" ); #[test] fn test_aegis256x4_encrypt_decrypt_round_trip() { let mut rng = rand::rng(); let cipher_mode = CipherMode::Aegis256X4; let metadata_size = cipher_mode.metadata_size(); let data_size = DEFAULT_ENCRYPTED_PAGE_SIZE - metadata_size; let page_data = { let mut page = vec![0u8; DEFAULT_ENCRYPTED_PAGE_SIZE]; page.iter_mut() .take(data_size) .for_each(|byte| *byte = rng.random()); page }; let key = EncryptionKey::from_hex_string(&generate_random_hex_key()).unwrap(); let ctx = EncryptionContext::new(CipherMode::Aegis256X4, &key, DEFAULT_ENCRYPTED_PAGE_SIZE) .unwrap(); let page_id = 42; let encrypted = ctx.encrypt_page(&page_data, page_id).unwrap(); assert_eq!(encrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_ne!(&encrypted[..data_size], &page_data[..data_size]); let decrypted = ctx.decrypt_page(&encrypted, page_id).unwrap(); assert_eq!(decrypted.len(), DEFAULT_ENCRYPTED_PAGE_SIZE); assert_eq!(decrypted, page_data); } #[test] fn test_cipher_mode_string_parsing() { // Test AES-128-GCM let mode = CipherMode::try_from("aes128gcm").unwrap(); assert_eq!(mode, CipherMode::Aes128Gcm); assert_eq!(mode.to_string(), "aes128gcm"); assert_eq!(mode.required_key_size(), 16); assert_eq!(mode.nonce_size(), 12); assert_eq!(mode.tag_size(), 16); let mode = CipherMode::try_from("aes-128-gcm").unwrap(); assert_eq!(mode, CipherMode::Aes128Gcm); let mode = CipherMode::try_from("aes_128_gcm").unwrap(); assert_eq!(mode, CipherMode::Aes128Gcm); // Test AES-256-GCM let mode = CipherMode::try_from("aes256gcm").unwrap(); assert_eq!(mode, CipherMode::Aes256Gcm); assert_eq!(mode.to_string(), "aes256gcm"); assert_eq!(mode.required_key_size(), 32); assert_eq!(mode.nonce_size(), 12); // Test that all AEGIS variants can be parsed from strings let mode = CipherMode::try_from("aegis128x2").unwrap(); assert_eq!(mode, CipherMode::Aegis128X2); assert_eq!(mode.to_string(), "aegis128x2"); assert_eq!(mode.required_key_size(), 16); assert_eq!(mode.nonce_size(), 16); assert_eq!(mode.tag_size(), 16); let mode = CipherMode::try_from("aegis-128x2").unwrap(); assert_eq!(mode, CipherMode::Aegis128X2); let mode = CipherMode::try_from("aegis_128x2").unwrap(); assert_eq!(mode, CipherMode::Aegis128X2); // Test AEGIS-128L let mode = CipherMode::try_from("aegis128l").unwrap(); assert_eq!(mode, CipherMode::Aegis128L); assert_eq!(mode.to_string(), "aegis128l"); assert_eq!(mode.required_key_size(), 16); assert_eq!(mode.nonce_size(), 16); // Test AEGIS-128X4 let mode = CipherMode::try_from("aegis128x4").unwrap(); assert_eq!(mode, CipherMode::Aegis128X4); assert_eq!(mode.to_string(), "aegis128x4"); assert_eq!(mode.required_key_size(), 16); assert_eq!(mode.nonce_size(), 16); // Test AEGIS-256X2 let mode = CipherMode::try_from("aegis256x2").unwrap(); assert_eq!(mode, CipherMode::Aegis256X2); assert_eq!(mode.to_string(), "aegis256x2"); assert_eq!(mode.required_key_size(), 32); assert_eq!(mode.nonce_size(), 32); // Test AEGIS-256X4 let mode = CipherMode::try_from("aegis256x4").unwrap(); assert_eq!(mode, CipherMode::Aegis256X4); assert_eq!(mode.to_string(), "aegis256x4"); assert_eq!(mode.required_key_size(), 32); assert_eq!(mode.nonce_size(), 32); } } ================================================ FILE: core/storage/journal_mode.rs ================================================ use crate::sync::Arc; use crate::storage::sqlite3_ondisk::Version; use crate::{mvcc, MvStore, OpenFlags, Result, IO}; #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, strum_macros::EnumString, strum_macros::Display, strum_macros::IntoStaticStr, )] #[strum(ascii_case_insensitive, serialize_all = "snake_case")] pub enum JournalMode { Delete, Truncate, Persist, Memory, Wal, #[strum(to_string = "mvcc", serialize = "experimental_mvcc")] Mvcc, Off, } impl JournalMode { /// Modes that are supported #[inline] pub fn supported(&self) -> bool { matches!(self, JournalMode::Wal | JournalMode::Mvcc) } /// As the header file version #[inline] pub fn as_version(&self) -> Option { match self { JournalMode::Wal => Some(Version::Wal), JournalMode::Mvcc => Some(Version::Mvcc), _ => None, } } } impl From for JournalMode { fn from(value: Version) -> Self { match value { Version::Legacy => Self::Delete, Version::Wal => Self::Wal, Version::Mvcc => Self::Mvcc, } } } pub fn logical_log_exists(db_path: impl AsRef) -> bool { let db_path = db_path.as_ref(); let log_path = db_path.with_extension("db-log"); std::path::Path::exists(log_path.as_path()) && log_path.as_path().metadata().unwrap().len() > 0 } pub fn open_mv_store( io: Arc, db_path: impl AsRef, flags: OpenFlags, durable_storage: Option>, encryption_ctx: Option, ) -> Result> { let storage: Arc = if let Some(storage) = durable_storage { storage } else { let db_path = db_path.as_ref(); let log_path = db_path.with_extension("db-log"); let string_path = log_path .as_os_str() .to_str() .expect("path should be valid string"); let file = io.open_file(string_path, flags, false)?; Arc::new(mvcc::persistent_storage::Storage::new( file, io, encryption_ctx, )) }; let mv_store = MvStore::new(mvcc::MvccClock::new(), storage); let mv_store = Arc::new(mv_store); Ok(mv_store) } ================================================ FILE: core/storage/mod.rs ================================================ //! The storage layer. //! //! This module contains the storage layer for Limbo. The storage layer is //! responsible for managing access to the database and its pages. The main //! interface to the storage layer is the `Pager` struct, which is //! responsible for managing the database file and the pages it contains. //! //! Pages in a database are stored in one of the following to data structures: //! `DatabaseStorage` or `Wal`. The `DatabaseStorage` trait is responsible //! for reading and writing pages to the database file, either local or //! remote. The `Wal` struct is responsible for managing the write-ahead log //! for the database, also either local or remote. pub(crate) mod btree; pub(crate) mod buffer_pool; pub(crate) mod checksum; pub mod database; pub(crate) mod encryption; pub(crate) mod journal_mode; pub(crate) mod page_cache; #[allow(clippy::arc_with_non_send_sync)] pub(crate) mod pager; #[allow(dead_code)] pub(super) mod slot_bitmap; pub mod sqlite3_ondisk; mod state_machines; pub(crate) mod subjournal; #[allow(clippy::arc_with_non_send_sync)] pub(crate) mod wal; #[macro_export] macro_rules! return_corrupt { ($($arg:tt)*) => { return Err(LimboError::Corrupt(format!($($arg)*))); }; } ================================================ FILE: core/storage/page_cache.rs ================================================ use crate::sync::{atomic::Ordering, Arc}; use intrusive_collections::{intrusive_adapter, LinkedList, LinkedListLink}; use rustc_hash::FxHashMap as HashMap; use tracing::trace; use crate::{ storage::{btree::PinGuard, sqlite3_ondisk::DatabaseHeader}, turso_assert, }; use super::pager::PageRef; #[cfg(not(target_family = "wasm"))] const DEFAULT_PAGE_CACHE_SIZE_IN_PAGES: usize = 2000; #[cfg(target_family = "wasm")] const DEFAULT_PAGE_CACHE_SIZE_IN_PAGES: usize = 100000; /// Minimum safe cache size in pages. /// This accounts for: /// - Btree cursor stack (up to BTCURSOR_MAX_DEPTH = 20 pages) /// - Balance operations (MAX_SIBLING_PAGES_TO_BALANCE = 5 new pages) /// - State machine pages (freelist operations, header refs, etc.) /// - Some buffer for concurrent operations pub const MINIMUM_PAGE_CACHE_SIZE_IN_PAGES: usize = 200; /// The spill threshold as a fraction of capacity. const DEFAULT_SPILL_THRESHOLD_PERCENT: usize = 90; #[derive(Debug, Copy, Eq, Hash, PartialEq, Clone)] #[repr(transparent)] pub struct PageCacheKey(usize); const CLEAR: u8 = 0; const REF_MAX: u8 = 3; /// An entry in the page cache. /// /// The entry is stored in the intrusive linked list in PageCache::list`. struct PageCacheEntry { /// Key identifying this page key: PageCacheKey, /// The cached page page: PageRef, /// Reference counter (SIEVE/GClock): starts at zero, bumped on access, /// decremented during eviction, only pages at 0 are evicted. ref_bit: u8, /// Intrusive link for SIEVE queue link: LinkedListLink, } intrusive_adapter!(EntryAdapter = Box: PageCacheEntry { link: LinkedListLink }); impl PageCacheEntry { fn new(key: PageCacheKey, page: PageRef) -> Box { Box::new(Self { key, page, ref_bit: CLEAR, link: LinkedListLink::new(), }) } #[inline] fn bump_ref(&mut self) { self.ref_bit = std::cmp::min(self.ref_bit + 1, REF_MAX); } #[inline] /// Returns the old value fn decrement_ref(&mut self) -> u8 { let old = self.ref_bit; self.ref_bit = old.saturating_sub(1); old } } /// Result returned when attempting to spill dirty pages from the cache. #[derive(Debug)] pub enum SpillResult { /// No spilling was needed (cache is below threshold) NotNeeded, /// Spilling is needed but disabled Disabled, /// Successfully collected dirty pages to spill PagesToSpill(Vec), /// Cache is at capacity with only unevictable pages CacheFull, } /// PageCache implements a variation of the SIEVE algorithm that maintains an intrusive linked list queue of /// pages which keep a 'reference_bit' to determine how recently/frequently the page has been accessed. /// The bit is set to `Clear` on initial insertion and then bumped on each access and decremented /// during eviction scans. /// /// The ring is circular. `clock_hand` points at the tail (LRU). /// Sweep order follows next: tail (LRU) -> head (MRU) -> .. -> tail /// New pages are inserted after the clock hand in the `next` direction, /// which places them at head (MRU) (i.e. `tail.next` is the head). pub struct PageCache { /// Capacity in pages capacity: usize, /// Map of Key -> pointer to entry in the queue map: HashMap, /// The eviction queue (intrusive doubly-linked list) queue: LinkedList, /// Clock hand cursor for SIEVE eviction (pointer to an entry in the queue, or null) clock_hand: *mut PageCacheEntry, /// Threshold number of pages at which we start spilling dirty pages. spill_threshold: usize, spill_enabled: bool, /// Conservative estimation of pages that are evictable based on dirty/spilled state. evictable_count: usize, } unsafe impl Send for PageCache {} unsafe impl Sync for PageCache {} crate::assert::assert_send_sync!(PageCache); #[derive(Debug, Clone, PartialEq, thiserror::Error)] pub enum CacheError { #[error("{0}")] InternalError(String), #[error("page {pgno} is locked")] Locked { pgno: usize }, #[error("page {pgno} is dirty")] Dirty { pgno: usize }, #[error("page {pgno} is pinned")] Pinned { pgno: usize }, #[error("cache active refs")] ActiveRefs, #[error("Page cache is full")] Full, #[error("key already exists")] KeyExists, } #[derive(Debug, PartialEq)] pub enum CacheResizeResult { Done, PendingEvictions, } impl PageCacheKey { pub fn new(pgno: usize) -> Self { Self(pgno) } } impl PageCache { #[cfg(not(target_family = "wasm"))] pub fn new(capacity: usize) -> Self { Self::new_with_spill(capacity, true) } #[cfg(target_family = "wasm")] pub fn new(capacity: usize) -> Self { Self::new_with_spill(capacity, false) } /// Create a new PageCache with explicit spill control. pub fn new_with_spill(capacity: usize, spill_enabled: bool) -> Self { let spill_threshold = (capacity * DEFAULT_SPILL_THRESHOLD_PERCENT) / 100; Self { capacity, map: HashMap::default(), queue: LinkedList::new(EntryAdapter::new()), clock_hand: std::ptr::null_mut(), spill_threshold: spill_threshold.max(1), spill_enabled, evictable_count: 0, } } /// Advances the clock hand to the next entry in the circular queue. /// Follows the "next" direction: from tail/LRU through the list back to tail. /// With our insertion-after-hand strategy, this moves through entries in age order. fn advance_clock_hand(&mut self) { if self.clock_hand.is_null() { return; } unsafe { let mut cursor = self.queue.cursor_mut_from_ptr(self.clock_hand); cursor.move_next(); if cursor.get().is_some() { self.clock_hand = cursor.as_cursor().get().unwrap() as *const _ as *mut PageCacheEntry; } else { // Reached end, wrap to front let front_cursor = self.queue.front_mut(); if front_cursor.get().is_some() { self.clock_hand = front_cursor.as_cursor().get().unwrap() as *const _ as *mut PageCacheEntry; } else { self.clock_hand = std::ptr::null_mut(); } } } } pub fn contains_key(&self, key: &PageCacheKey) -> bool { self.map.contains_key(key) } #[inline] pub fn insert(&mut self, key: PageCacheKey, value: PageRef) -> Result<(), CacheError> { self._insert(key, value, false) } #[inline] pub fn upsert_page(&mut self, key: PageCacheKey, value: PageRef) -> Result<(), CacheError> { self._insert(key, value, true) } pub fn _insert( &mut self, key: PageCacheKey, value: PageRef, update_in_place: bool, ) -> Result<(), CacheError> { trace!("insert(key={:?})", key); if let Some(&entry_ptr) = self.map.get(&key) { let entry = unsafe { &mut *entry_ptr }; let p = &entry.page; if !p.is_loaded() && !p.is_locked() { // evict, then continue with fresh insert self._delete(key, true)?; // Proceed to insert new entry } else { entry.bump_ref(); if update_in_place { // Track evictable count change if page state differs let old_evictable = Self::counted_as_evictable(&entry.page); let new_evictable = Self::counted_as_evictable(&value); if old_evictable && !new_evictable { self.evictable_count = self.evictable_count.saturating_sub(1); } else if !old_evictable && new_evictable { self.evictable_count += 1; } entry.page = value; return Ok(()); } else { turso_assert!( Arc::ptr_eq(&entry.page, &value), "Attempted to insert different page with same key: {key:?}" ); return Err(CacheError::KeyExists); } } } // Key doesn't exist, proceed with new entry self.make_room_for(1)?; // Track evictable count for the new page let is_evictable = Self::counted_as_evictable(&value); let entry = PageCacheEntry::new(key, value); if self.clock_hand.is_null() { // First entry - just push it self.queue.push_back(entry); let entry_ptr = self.queue.back().get().unwrap() as *const _ as *mut PageCacheEntry; self.map.insert(key, entry_ptr); self.clock_hand = entry_ptr; } else { // Insert after clock hand (in circular list semantics, this makes it the new head/MRU) unsafe { let mut cursor = self.queue.cursor_mut_from_ptr(self.clock_hand); cursor.insert_after(entry); // The inserted entry is now at the next position after clock hand cursor.move_next(); let entry_ptr = cursor.get().ok_or_else(|| { CacheError::InternalError("Failed to get inserted entry pointer".into()) })? as *const PageCacheEntry as *mut PageCacheEntry; self.map.insert(key, entry_ptr); } } // Update evictable count after successful insertion if is_evictable { self.evictable_count += 1; } Ok(()) } fn _delete(&mut self, key: PageCacheKey, clean_page: bool) -> Result<(), CacheError> { let Some(&entry_ptr) = self.map.get(&key) else { return Ok(()); }; let entry = unsafe { &mut *entry_ptr }; let page = &entry.page; if page.is_locked() { return Err(CacheError::Locked { pgno: page.get().id, }); } if page.is_dirty() { return Err(CacheError::Dirty { pgno: page.get().id, }); } if page.is_pinned() { return Err(CacheError::Pinned { pgno: page.get().id, }); } // Track evictable count before removing let was_evictable = Self::counted_as_evictable(page); if clean_page { page.clear_loaded(); let _ = page.get().buffer.take(); } // Remove from map first self.map.remove(&key); // If clock hand points to this entry, advance it before removing if self.clock_hand == entry_ptr { self.advance_clock_hand(); // If hand is still pointing to the same entry after advance, we're removing the last entry if self.clock_hand == entry_ptr { self.clock_hand = std::ptr::null_mut(); } } // Remove the entry from the queue unsafe { let mut cursor = self.queue.cursor_mut_from_ptr(entry_ptr); cursor.remove(); } // Update evictable count after successful removal if was_evictable { self.evictable_count = self.evictable_count.saturating_sub(1); } Ok(()) } #[inline] /// Deletes a page from the cache pub fn delete(&mut self, key: PageCacheKey) -> Result<(), CacheError> { trace!("cache_delete(key={:?})", key); self._delete(key, true) } #[inline] pub fn get(&mut self, key: &PageCacheKey) -> crate::Result> { let Some(&entry_ptr) = self.map.get(key) else { return Ok(None); }; let entry = unsafe { &mut *entry_ptr }; let page = entry.page.clone(); // Because we can abort a read_page completion, this means a page can be in the cache but be unloaded and unlocked. // However, if we do not evict that page from the page cache, we will return an unloaded page later which will trigger // assertions later on. This is worsened by the fact that page cache is not per `Statement`, so you can abort a completion // in one Statement, and trigger some error in the next one if we don't evict the page here. if !page.is_loaded() && !page.is_locked() { self.delete(*key)?; return Ok(None); } entry.bump_ref(); Ok(Some(page)) } #[inline] pub fn peek(&mut self, key: &PageCacheKey, touch: bool) -> Option { let &entry_ptr = self.map.get(key)?; let entry = unsafe { &mut *entry_ptr }; let page = entry.page.clone(); if touch { entry.bump_ref(); } Some(page) } /// Resizes the cache to a new capacity. /// If shrinking, attempts to evict pages. if growing, increases capacity. pub fn resize(&mut self, new_cap: usize) -> CacheResizeResult { if new_cap == self.capacity { return CacheResizeResult::Done; } // Evict entries one by one until we're at new capacity while new_cap < self.len() { if self.evict_one().is_err() { return CacheResizeResult::PendingEvictions; } } self.capacity = new_cap; self.spill_threshold = ((new_cap * DEFAULT_SPILL_THRESHOLD_PERCENT) / 100).max(1); CacheResizeResult::Done } /// Returns true if the cache is at or above the spill threshold and spilling is enabled. /// This indicates that dirty pages should be flushed to make room for new pages. #[inline] pub fn needs_spill(&self) -> bool { let len = self.len(); if len < self.spill_threshold || !self.spill_enabled { return false; } let needed_evictable = len.saturating_sub(self.spill_threshold); // Fast path: use tracked evictable_count to avoid O(n) scan: // evictable_count is a conservative upper bound on evictable pages, // Empty slots also count as available room since make_room_for uses them first. // Calculate if we have enough room (evictable + empty slots) and won't need to spill. let empty_slots = self.capacity.saturating_sub(len); let available_room = self.evictable_count.saturating_add(empty_slots); if available_room >= needed_evictable { return false; } // Slow path: do the full count since our estimate suggests we might need to spill. // The actual count may be lower than evictable_count due to locked/pinned pages. self.count_evictable_pages() < needed_evictable } #[inline] /// Count pages that can be evicted without spilling. fn count_evictable_pages(&self) -> usize { self.map .values() .filter(|&&entry_ptr| { let entry = unsafe { &*entry_ptr }; Self::evictable(&entry.page) }) .count() } /// Check if spilling is enabled for this cache. #[inline] pub fn is_spill_enabled(&self) -> bool { self.spill_enabled } /// Enable or disable spilling for this cache. pub fn set_spill_enabled(&mut self, enabled: bool) { self.spill_enabled = enabled; } /// Get the current spill threshold (number of pages). #[inline] pub fn spill_threshold(&self) -> usize { self.spill_threshold } /// Set a custom spill threshold (number of pages). /// The threshold will be clamped to be at least 1 and at most capacity. pub fn set_spill_threshold(&mut self, threshold: usize) { self.spill_threshold = threshold.clamp(1, self.capacity); } #[inline] fn spillable(page: &PageRef) -> bool { page.is_dirty() && !page.is_spilled() && !page.is_locked() && !page.is_pinned() && Arc::strong_count(page) == 1 && page.get().id.ne(&DatabaseHeader::PAGE_ID) && page.get().overflow_cells.is_empty() } #[inline] /// Check if a page should be counted as evictable for tracking purposes. /// This is a conservative check that ignores locked/pinned/strong_count state /// since those are typically short-lived. We track based on dirty/spilled state. fn counted_as_evictable(page: &PageRef) -> bool { // Page 1 is never evictable if page.get().id == DatabaseHeader::PAGE_ID { return false; } // A page is evictable if it's clean OR spilled !page.is_dirty() || page.is_spilled() } /// Notify the cache that a page has become dirty. /// This should be called when a page transitions from clean/spilled to dirty. /// The page must already be in the cache. pub fn notify_page_dirty(&mut self, key: PageCacheKey) { if let Some(&entry_ptr) = self.map.get(&key) { let entry = unsafe { &*entry_ptr }; let page = &entry.page; // Page was evictable (clean or spilled) before becoming dirty, // now it's dirty && !spilled, so not evictable if page.get().id != DatabaseHeader::PAGE_ID { // Only decrement if we were counting it as evictable // (it was clean or spilled before this call) self.evictable_count = self.evictable_count.saturating_sub(1); } } } /// Notify the cache that a page has been spilled. /// This should be called when a page transitions from dirty to spilled. /// The page must already be in the cache. pub fn notify_page_spilled(&mut self, key: PageCacheKey) { if let Some(&entry_ptr) = self.map.get(&key) { let entry = unsafe { &*entry_ptr }; let page = &entry.page; // Page was dirty && !spilled (not evictable), now it's spilled (evictable) if page.get().id != DatabaseHeader::PAGE_ID { self.evictable_count += 1; } } } /// Get the current evictable page count (for diagnostics/testing). #[cfg(test)] pub fn evictable_count(&self) -> usize { self.evictable_count } /// Collect dirty pages that can be spilled to make room in the cache. /// Pages that are locked or pinned are skipped. pub fn collect_spillable_pages(&self, max_pages: usize) -> Vec { if !self.spill_enabled || max_pages == 0 { return Vec::new(); } const EST_SPILL: usize = 128; let mut spillable: Vec = Vec::with_capacity(EST_SPILL); for (_, &entry_ptr) in self.map.iter() { let entry = unsafe { &*entry_ptr }; let page = &entry.page; if Self::spillable(page) { spillable.push(PinGuard::new(page.clone())); } if spillable.len() >= max_pages { break; } } spillable.sort_by_key(|pg| pg.get().id); spillable } /// Returns the number of dirty pages currently in the cache. pub fn dirty_count(&self) -> usize { self.map .values() .filter(|&&entry_ptr| { let entry = unsafe { &*entry_ptr }; entry.page.is_dirty() }) .count() } /// Check if the cache needs spilling and return appropriate result. /// This is the main entry point for the spilling check during insertion. pub fn check_spill(&self, max_pages: usize) -> SpillResult { if !self.needs_spill() { return SpillResult::NotNeeded; } let pages = self.collect_spillable_pages(max_pages); if pages.is_empty() { SpillResult::CacheFull } else { SpillResult::PagesToSpill(pages) } } /// Ensures at least `n` free slots are available /// /// Uses the SIEVE algorithm to evict pages if necessary: /// Start at clock hand position /// If page ref_bit > 0, decrement and continue /// If page ref_bit == 0 and evictable, evict it /// If page is unevictable (dirty/locked/pinned), continue sweep /// On sweep, pages with ref_bit > 0 are given a second chance by decrementing /// their ref_bit and leaving them in place; only pages with ref_bit == 0 are evicted. /// /// Returns `CacheError::Full` if not enough pages can be evicted pub fn make_room_for(&mut self, n: usize) -> Result<(), CacheError> { if n > self.capacity { return Err(CacheError::Full); } let available = self.capacity - self.len(); if n <= available { return Ok(()); } let need = n - available; for _ in 0..need { self.evict_one()?; } Ok(()) } #[inline] fn evictable(page: &PageRef) -> bool { (!page.is_dirty() || page.is_spilled()) && !page.is_locked() && !page.is_pinned() && page.get().id.ne(&DatabaseHeader::PAGE_ID) && Arc::strong_count(page) == 1 } /// Evicts a single page using the SIEVE algorithm fn evict_one(&mut self) -> Result<(), CacheError> { if self.len() == 0 { return Err(CacheError::InternalError( "Cannot evict from empty cache".into(), )); } let mut examined = 0usize; let max_examinations = self.len().saturating_mul(REF_MAX as usize + 1); while examined < max_examinations { // Clock hand should never be null here since we checked len() > 0 turso_assert!( !self.clock_hand.is_null(), "page_cache: clock hand is null during eviction", { "entries": self.len() } ); let entry_ptr = self.clock_hand; let entry = unsafe { &mut *entry_ptr }; let key = entry.key; let page = &entry.page; let evictable = Self::evictable(page); if evictable && entry.ref_bit == CLEAR { turso_assert!( Self::counted_as_evictable(page), "mismatched evictable count state" ); // Evict this entry self.advance_clock_hand(); // Check if clock hand wrapped back to the same entry (meaning this is the only/last entry) if self.clock_hand == entry_ptr { self.clock_hand = std::ptr::null_mut(); } self.map.remove(&key); // Clean the page page.clear_loaded(); let _ = page.get().buffer.take(); // Remove from queue unsafe { let mut cursor = self.queue.cursor_mut_from_ptr(entry_ptr); cursor.remove(); } // Update evictable count after successful eviction self.evictable_count = self.evictable_count.saturating_sub(1); return Ok(()); } else if evictable { // Decrement ref bit and continue entry.decrement_ref(); self.advance_clock_hand(); examined += 1; } else { // Skip unevictable page self.advance_clock_hand(); examined += 1; } } Err(CacheError::Full) } pub fn clear(&mut self, clear_dirty: bool) -> Result<(), CacheError> { // Check all pages are clean for &entry_ptr in self.map.values() { let entry = unsafe { &*entry_ptr }; if entry.page.is_dirty() && !clear_dirty { return Err(CacheError::Dirty { pgno: entry.page.get().id, }); } } // Clean all pages for &entry_ptr in self.map.values() { let entry = unsafe { &*entry_ptr }; entry.page.clear_loaded(); let _ = entry.page.get().buffer.take(); } self.map.clear(); self.queue.clear(); self.clock_hand = std::ptr::null_mut(); self.evictable_count = 0; Ok(()) } /// Removes all pages from the cache with pgno greater than max_page_num pub fn truncate(&mut self, max_page_num: usize) -> Result<(), CacheError> { for key in self .map .keys() .filter(|k| k.0 > max_page_num) .copied() .collect::>() { self.delete(key)?; } Ok(()) } pub fn print(&self) { tracing::debug!("page_cache_len={}", self.map.len()); let mut cursor = self.queue.front(); let mut i = 0; while let Some(entry) = cursor.get() { let page = &entry.page; tracing::debug!( "slot={}, page={:?}, flags={}, pin_count={}, ref_bit={:?}", i, entry.key, page.get().flags.load(Ordering::SeqCst), page.get().pin_count.load(Ordering::SeqCst), entry.ref_bit, ); cursor.move_next(); i += 1; } } #[cfg(test)] pub fn keys(&mut self) -> Vec { self.map.keys().copied().collect() } pub fn len(&self) -> usize { self.map.len() } pub fn capacity(&self) -> usize { self.capacity } #[cfg(test)] fn verify_cache_integrity(&self) { use rustc_hash::FxHashSet as HashSet; let map_len = self.map.len(); // Count entries in queue let mut queue_len = 0; let mut cursor = self.queue.front(); let mut seen_keys = HashSet::default(); while let Some(entry) = cursor.get() { queue_len += 1; seen_keys.insert(entry.key); cursor.move_next(); } assert_eq!(map_len, queue_len, "map and queue length mismatch"); assert_eq!(map_len, seen_keys.len(), "duplicate keys in queue"); // Verify all map entries are in queue for &key in self.map.keys() { assert!(seen_keys.contains(&key), "map key not in queue"); } // Verify clock hand if !self.clock_hand.is_null() { assert!(map_len > 0, "clock hand set but map is empty"); let hand_key = unsafe { (*self.clock_hand).key }; assert!( self.map.contains_key(&hand_key), "clock hand points to non-existent entry" ); } else { assert_eq!(map_len, 0, "clock hand null but map not empty"); } } #[cfg(test)] fn ref_of(&self, key: &PageCacheKey) -> Option { self.map.get(key).map(|&ptr| unsafe { (*ptr).ref_bit }) } } impl Default for PageCache { fn default() -> Self { PageCache::new(DEFAULT_PAGE_CACHE_SIZE_IN_PAGES) } } #[cfg(test)] mod tests { use super::*; use crate::storage::page_cache::CacheError; use crate::storage::pager::{Page, PageRef}; use crate::sync::Arc; use rand_chacha::{ rand_core::{RngCore, SeedableRng}, ChaCha8Rng, }; fn create_key(id: usize) -> PageCacheKey { PageCacheKey::new(id) } pub fn page_with_content(page_id: usize) -> PageRef { let page = Arc::new(Page::new(page_id as i64)); { let inner = page.get(); inner.buffer = Some(Arc::new(crate::Buffer::new_temporary(4096))); } page.set_loaded(); page } fn insert_page(cache: &mut PageCache, id: usize) -> PageCacheKey { let key = create_key(id); let page = page_with_content(id); cache .insert(key, page) .unwrap_or_else(|e| panic!("Failed to insert page {id}: {e:?}")); key } #[test] fn test_delete_only_element() { let mut cache = PageCache::default(); let key1 = insert_page(&mut cache, 1); cache.verify_cache_integrity(); assert_eq!(cache.len(), 1); assert!(cache.delete(key1).is_ok()); assert_eq!( cache.len(), 0, "Length should be 0 after deleting only element" ); assert!( !cache.contains_key(&key1), "Cache should not contain key after delete" ); cache.verify_cache_integrity(); } #[test] fn test_detach_tail() { let mut cache = PageCache::default(); let key1 = insert_page(&mut cache, 1); // tail let _key2 = insert_page(&mut cache, 2); // middle let _key3 = insert_page(&mut cache, 3); // head cache.verify_cache_integrity(); assert_eq!(cache.len(), 3); // Delete tail assert!(cache.delete(key1).is_ok()); assert_eq!(cache.len(), 2, "Length should be 2 after deleting tail"); assert!( !cache.contains_key(&key1), "Cache should not contain deleted tail key" ); cache.verify_cache_integrity(); } #[test] fn test_insert_existing_key_updates_in_place() { let mut cache = PageCache::default(); let key1 = create_key(1); let page1_v1 = page_with_content(1); let page1_v2 = page1_v1.clone(); // Same Arc instance assert!(cache.insert(key1, page1_v1).is_ok()); assert_eq!(cache.len(), 1); // Inserting same page instance should return KeyExists error let result = cache.insert(key1, page1_v2); assert_eq!(result, Err(CacheError::KeyExists)); assert_eq!(cache.len(), 1); // Verify the page is still accessible assert!(cache.get(&key1).unwrap().is_some()); cache.verify_cache_integrity(); } #[test] #[should_panic(expected = "Attempted to insert different page with same key")] fn test_insert_different_page_same_key_panics() { let mut cache = PageCache::default(); let key1 = create_key(1); let page1_v1 = page_with_content(1); let page1_v2 = page_with_content(1); // Different Arc instance assert!(cache.insert(key1, page1_v1).is_ok()); assert_eq!(cache.len(), 1); cache.verify_cache_integrity(); // This should panic because it's a different page instance let _ = cache.insert(key1, page1_v2); } #[test] fn test_delete_nonexistent_key() { let mut cache = PageCache::default(); let key_nonexist = create_key(99); // Deleting non-existent key should be a no-op (returns Ok) assert!(cache.delete(key_nonexist).is_ok()); assert_eq!(cache.len(), 0); cache.verify_cache_integrity(); } #[test] fn test_page_cache_evict() { // Note: page 1 is DatabaseHeader and is never evictable, so use page ids >= 2 let mut cache = PageCache::new_with_spill(1, true); let key2 = insert_page(&mut cache, 2); let key3 = insert_page(&mut cache, 3); // With capacity=1, inserting key3 should evict key2 assert_eq!(cache.get(&key3).unwrap().unwrap().get().id, 3); assert!( cache.get(&key2).unwrap().is_none(), "key2 should be evicted" ); // key3 should still be accessible assert_eq!(cache.get(&key3).unwrap().unwrap().get().id, 3); assert!( cache.get(&key2).unwrap().is_none(), "capacity=1 should have evicted the older page" ); cache.verify_cache_integrity(); } #[test] fn test_sieve_touch_non_tail_does_not_affect_immediate_eviction() { // SIEVE algorithm: touching a non-tail page marks it but doesn't move it. // The tail (if unmarked) will still be the first eviction candidate. // Note: page 1 is DatabaseHeader and is never evictable, so use page ids >= 2 // Insert 2,3,4 -> order [4,3,2] with tail=2 let mut cache = PageCache::new_with_spill(3, true); let key2 = insert_page(&mut cache, 2); let key3 = insert_page(&mut cache, 3); let key4 = insert_page(&mut cache, 4); // Touch key3 (middle) to mark it with reference bit assert!(cache.get(&key3).unwrap().is_some()); // Insert 5: SIEVE examines tail (key2, unmarked) -> evict key2 let key5 = insert_page(&mut cache, 5); assert!( cache.get(&key3).unwrap().is_some(), "marked non-tail (key3) should remain" ); assert!(cache.get(&key4).unwrap().is_some(), "key4 should remain"); assert!( cache.get(&key5).unwrap().is_some(), "key5 was just inserted" ); assert!( cache.get(&key2).unwrap().is_none(), "unmarked tail (key2) should be evicted first" ); cache.verify_cache_integrity(); } #[test] fn clock_second_chance_decrements_tail_then_evicts_next() { let mut cache = PageCache::new_with_spill(3, true); let key1 = insert_page(&mut cache, 1); let key2 = insert_page(&mut cache, 2); let key3 = insert_page(&mut cache, 3); assert_eq!(cache.len(), 3); assert!(cache.get(&key1).unwrap().is_some()); let key4 = insert_page(&mut cache, 4); assert!(cache.get(&key1).unwrap().is_some(), "key1 should survive"); assert!(cache.get(&key2).unwrap().is_some(), "key2 remains"); assert!(cache.get(&key4).unwrap().is_some(), "key4 inserted"); assert!( cache.get(&key3).unwrap().is_none(), "key3 (next after tail) evicted" ); assert_eq!(cache.len(), 3); cache.verify_cache_integrity(); } #[test] fn test_delete_locked_page() { let mut cache = PageCache::default(); let key = insert_page(&mut cache, 1); let page = cache.get(&key).unwrap().unwrap(); page.set_locked(); assert_eq!(cache.delete(key), Err(CacheError::Locked { pgno: 1 })); assert_eq!(cache.len(), 1, "Locked page should not be deleted"); cache.verify_cache_integrity(); } #[test] fn test_delete_dirty_page() { let mut cache = PageCache::default(); let key = insert_page(&mut cache, 1); let page = cache.get(&key).unwrap().unwrap(); page.set_dirty(); assert_eq!(cache.delete(key), Err(CacheError::Dirty { pgno: 1 })); assert_eq!(cache.len(), 1, "Dirty page should not be deleted"); cache.verify_cache_integrity(); } #[test] fn test_delete_pinned_page() { let mut cache = PageCache::default(); let key = insert_page(&mut cache, 1); let page = cache.get(&key).unwrap().unwrap(); page.pin(); assert_eq!(cache.delete(key), Err(CacheError::Pinned { pgno: 1 })); assert_eq!(cache.len(), 1, "Pinned page should not be deleted"); cache.verify_cache_integrity(); } #[test] fn test_make_room_for_with_dirty_pages() { let mut cache = PageCache::new_with_spill(2, true); let key1 = insert_page(&mut cache, 1); let key2 = insert_page(&mut cache, 2); // Make both pages dirty (unevictable) cache.get(&key1).unwrap().unwrap().set_dirty(); cache.get(&key2).unwrap().unwrap().set_dirty(); // Try to insert a third page, should fail because can't evict dirty pages let key3 = create_key(3); let page3 = page_with_content(3); let result = cache.insert(key3, page3); assert_eq!(result, Err(CacheError::Full)); assert_eq!(cache.len(), 2); cache.verify_cache_integrity(); } #[test] fn test_page_cache_insert_and_get() { let mut cache = PageCache::default(); let key1 = insert_page(&mut cache, 1); let key2 = insert_page(&mut cache, 2); assert_eq!(cache.get(&key1).unwrap().unwrap().get().id, 1); assert_eq!(cache.get(&key2).unwrap().unwrap().get().id, 2); cache.verify_cache_integrity(); } #[test] fn test_page_cache_over_capacity() { // Test SIEVE eviction when exceeding capacity // Note: page 1 is DatabaseHeader and is never evictable, so use page ids >= 2 let mut cache = PageCache::new_with_spill(2, true); let key2 = insert_page(&mut cache, 2); let key3 = insert_page(&mut cache, 3); // Insert 4: tail (key2, unmarked) should be evicted let key4 = insert_page(&mut cache, 4); assert_eq!(cache.len(), 2); assert!(cache.get(&key3).unwrap().is_some(), "key3 should remain"); assert!(cache.get(&key4).unwrap().is_some(), "key4 just inserted"); assert!( cache.get(&key2).unwrap().is_none(), "key2 (oldest, unmarked) should be evicted" ); cache.verify_cache_integrity(); } #[test] fn test_page_cache_delete() { let mut cache = PageCache::default(); let key1 = insert_page(&mut cache, 1); assert!(cache.delete(key1).is_ok()); assert!(cache.get(&key1).unwrap().is_none()); assert_eq!(cache.len(), 0); cache.verify_cache_integrity(); } #[test] fn test_page_cache_clear() { let mut cache = PageCache::default(); let key1 = insert_page(&mut cache, 1); let key2 = insert_page(&mut cache, 2); assert!(cache.clear(false).is_ok()); assert!(cache.get(&key1).unwrap().is_none()); assert!(cache.get(&key2).unwrap().is_none()); assert_eq!(cache.len(), 0); cache.verify_cache_integrity(); } #[test] fn test_resize_smaller_success() { let mut cache = PageCache::default(); for i in 1..=5 { let _ = insert_page(&mut cache, i); } assert_eq!(cache.len(), 5); let result = cache.resize(3); assert_eq!(result, CacheResizeResult::Done); assert_eq!(cache.len(), 3); assert_eq!(cache.capacity(), 3); // Should still be able to insert after resize assert!(cache.insert(create_key(6), page_with_content(6)).is_ok()); assert_eq!(cache.len(), 3); // One was evicted to make room cache.verify_cache_integrity(); } #[test] fn test_detach_with_multiple_pages() { let mut cache = PageCache::default(); let _key1 = insert_page(&mut cache, 1); let key2 = insert_page(&mut cache, 2); let _key3 = insert_page(&mut cache, 3); // Delete middle element (key2) assert!(cache.delete(key2).is_ok()); // Verify structure after deletion assert_eq!(cache.len(), 2); assert!(!cache.contains_key(&key2)); cache.verify_cache_integrity(); } #[test] fn test_delete_multiple_elements() { let mut cache = PageCache::default(); let key1 = insert_page(&mut cache, 1); let key2 = insert_page(&mut cache, 2); let key3 = insert_page(&mut cache, 3); cache.verify_cache_integrity(); assert_eq!(cache.len(), 3); // Delete head (key3) assert!(cache.delete(key3).is_ok()); assert_eq!(cache.len(), 2, "Length should be 2 after deleting head"); assert!( !cache.contains_key(&key3), "Cache should not contain deleted head key" ); cache.verify_cache_integrity(); // Delete tail (key1) assert!(cache.delete(key1).is_ok()); assert_eq!(cache.len(), 1, "Length should be 1 after deleting two"); cache.verify_cache_integrity(); // Delete last element (key2) assert!(cache.delete(key2).is_ok()); assert_eq!(cache.len(), 0, "Length should be 0 after deleting all"); cache.verify_cache_integrity(); } #[test] fn test_resize_larger() { let mut cache = PageCache::new_with_spill(2, true); let key1 = insert_page(&mut cache, 1); let key2 = insert_page(&mut cache, 2); assert_eq!(cache.len(), 2); let result = cache.resize(5); assert_eq!(result, CacheResizeResult::Done); assert_eq!(cache.len(), 2); assert_eq!(cache.capacity(), 5); // Existing pages should still be accessible assert!(cache.get(&key1).is_ok_and(|p| p.is_some())); assert!(cache.get(&key2).is_ok_and(|p| p.is_some())); // Now we should be able to add 3 more without eviction for i in 3..=5 { let _ = insert_page(&mut cache, i); } assert_eq!(cache.len(), 5); cache.verify_cache_integrity(); } #[test] fn test_resize_same_capacity() { let mut cache = PageCache::new_with_spill(3, true); for i in 1..=3 { let _ = insert_page(&mut cache, i); } let result = cache.resize(3); assert_eq!(result, CacheResizeResult::Done); assert_eq!(cache.len(), 3); assert_eq!(cache.capacity(), 3); cache.verify_cache_integrity(); } #[test] fn test_truncate_page_cache() { let mut cache = PageCache::new_with_spill(10, true); let _ = insert_page(&mut cache, 1); let _ = insert_page(&mut cache, 4); let _ = insert_page(&mut cache, 8); let _ = insert_page(&mut cache, 10); // Truncate to keep only pages <= 4 cache.truncate(4).unwrap(); assert!(cache.contains_key(&PageCacheKey(1))); assert!(cache.contains_key(&PageCacheKey(4))); assert!(!cache.contains_key(&PageCacheKey(8))); assert!(!cache.contains_key(&PageCacheKey(10))); assert_eq!(cache.len(), 2); assert_eq!(cache.capacity(), 10); cache.verify_cache_integrity(); } #[test] fn test_truncate_page_cache_remove_all() { let mut cache = PageCache::new_with_spill(10, true); let _ = insert_page(&mut cache, 8); let _ = insert_page(&mut cache, 10); // Truncate to 4 (removes all pages since they're > 4) cache.truncate(4).unwrap(); assert!(!cache.contains_key(&PageCacheKey(8))); assert!(!cache.contains_key(&PageCacheKey(10))); assert_eq!(cache.len(), 0); assert_eq!(cache.capacity(), 10); cache.verify_cache_integrity(); } #[test] fn test_page_cache_fuzz() { let seed = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); let mut rng = ChaCha8Rng::seed_from_u64(seed); tracing::info!("fuzz test seed: {}", seed); let max_pages = 10; let mut cache = PageCache::new_with_spill(10, true); let mut reference_map = HashMap::default(); for _ in 0..10000 { cache.print(); match rng.next_u64() % 2 { 0 => { // Insert operation let id_page = rng.next_u64() % max_pages; let key = PageCacheKey::new(id_page as usize); #[allow(clippy::arc_with_non_send_sync)] let page = Arc::new(Page::new(id_page as i64)); if cache.peek(&key, false).is_some() { continue; // Skip duplicate page ids } tracing::debug!("inserting page {:?}", key); match cache.insert(key, page.clone()) { Err(CacheError::Full | CacheError::ActiveRefs) => {} // Expected, ignore Err(err) => { panic!("Cache insertion failed unexpectedly: {err:?}"); } Ok(_) => { reference_map.insert(key, page); // Clean up reference_map if cache evicted something if cache.len() < reference_map.len() { reference_map.retain(|k, _| cache.contains_key(k)); } } } assert!(cache.len() <= 10, "Cache size exceeded capacity"); } 1 => { // Delete operation let random = rng.next_u64() % 2 == 0; let key = if random || reference_map.is_empty() { let id_page: u64 = rng.next_u64() % max_pages; PageCacheKey::new(id_page as usize) } else { let i = rng.next_u64() as usize % reference_map.len(); *reference_map.keys().nth(i).unwrap() }; tracing::debug!("removing page {:?}", key); reference_map.remove(&key); assert!(cache.delete(key).is_ok()); } _ => unreachable!(), } cache.verify_cache_integrity(); // Verify all pages in reference_map are in cache for (key, page) in &reference_map { let cached_page = cache.peek(key, false).expect("Page should be in cache"); assert_eq!(cached_page.get().id, key.0); assert_eq!(page.get().id, key.0); } } } #[test] fn test_peek_without_touch() { // Test that peek with touch=false doesn't mark pages // Note: page 1 is DatabaseHeader and is never evictable, so use page ids >= 2 let mut cache = PageCache::new_with_spill(2, true); let key2 = insert_page(&mut cache, 2); let key3 = insert_page(&mut cache, 3); // Peek key2 without touching (no ref bit set) assert!(cache.peek(&key2, false).is_some()); // Insert 4: should evict unmarked tail (key2) let key4 = insert_page(&mut cache, 4); assert!(cache.get(&key3).unwrap().is_some(), "key3 should remain"); assert!( cache.get(&key4).unwrap().is_some(), "key4 was just inserted" ); assert!( cache.get(&key2).unwrap().is_none(), "key2 should be evicted since peek(false) didn't mark it" ); assert_eq!(cache.len(), 2); cache.verify_cache_integrity(); } #[test] fn test_peek_with_touch() { // Test that peek with touch=true marks pages for SIEVE let mut cache = PageCache::new_with_spill(2, true); let key1 = insert_page(&mut cache, 1); let key2 = insert_page(&mut cache, 2); // Peek key1 WITH touching (sets ref bit) assert!(cache.peek(&key1, true).is_some()); // Insert 3: key1 is marked, so it gets second chance // key2 becomes new tail and gets evicted let key3 = insert_page(&mut cache, 3); assert!( cache.get(&key1).unwrap().is_some(), "key1 should survive (was marked)" ); assert!( cache.get(&key3).unwrap().is_some(), "key3 was just inserted" ); assert!( cache.get(&key2).unwrap().is_none(), "key2 should be evicted after key1's second chance" ); assert_eq!(cache.len(), 2); cache.verify_cache_integrity(); } #[test] #[ignore = "long running test, remove ignore to verify memory stability"] fn test_clear_memory_stability() { let initial_memory = memory_stats::memory_stats().unwrap().physical_mem; for _ in 0..100000 { let mut cache = PageCache::new(1000); for i in 0..1000 { let key = create_key(i); let page = page_with_content(i); cache.insert(key, page).unwrap(); } cache.clear(false).unwrap(); drop(cache); } let final_memory = memory_stats::memory_stats().unwrap().physical_mem; let growth = final_memory.saturating_sub(initial_memory); println!("Memory growth: {growth} bytes"); assert!( growth < 10_000_000, "Memory grew by {growth} bytes over test cycles (limit: 10MB)", ); } #[test] fn clock_drains_hot_page_within_single_sweep_when_others_are_unevictable() { // Note: page 1 is DatabaseHeader and is never evictable, so use page ids >= 2 // capacity 3: [4(head), 3, 2(tail)] let mut c = PageCache::new_with_spill(3, true); let k2 = insert_page(&mut c, 2); let k3 = insert_page(&mut c, 3); let k4 = insert_page(&mut c, 4); // Make k2 hot: bump to Max for _ in 0..3 { assert!(c.get(&k2).unwrap().is_some()); } assert!(matches!(c.ref_of(&k2), Some(REF_MAX))); // Make other pages unevictable; clock must keep revisiting k2. c.get(&k3).unwrap().unwrap().set_dirty(); c.get(&k4).unwrap().unwrap().set_dirty(); // Insert 5 -> sweep rotates as needed, draining k2 and evicting it. let k5 = insert_page(&mut c, 5); assert!( c.get(&k2).unwrap().is_none(), "k2 should be evicted after its credit drains" ); assert!(c.get(&k3).unwrap().is_some(), "k3 is dirty (unevictable)"); assert!(c.get(&k4).unwrap().is_some(), "k4 is dirty (unevictable)"); assert!(c.get(&k5).unwrap().is_some(), "k5 just inserted"); c.verify_cache_integrity(); } #[test] fn gclock_hot_survives_scan_pages() { let mut c = PageCache::new_with_spill(4, true); let _k1 = insert_page(&mut c, 1); let k2 = insert_page(&mut c, 2); let _k3 = insert_page(&mut c, 3); let _k4 = insert_page(&mut c, 4); // Make k2 truly hot: three real touches for _ in 0..3 { assert!(c.get(&k2).unwrap().is_some()); } assert!(matches!(c.ref_of(&k2), Some(REF_MAX))); // Now simulate a scan inserting new pages 5..10 (one-hit wonders). for id in 5..=10 { let _ = insert_page(&mut c, id); } // Hot k2 should still be present; most single-hit scan pages should churn. assert!( c.get(&k2).unwrap().is_some(), "hot page should survive scan" ); // The earliest single-hit page should be gone. assert!(c.get(&create_key(5)).unwrap().is_none()); c.verify_cache_integrity(); } #[test] fn hand_stays_valid_after_deleting_only_element() { let mut c = PageCache::new_with_spill(2, true); let k = insert_page(&mut c, 1); assert!(c.delete(k).is_ok()); // Inserting again should not panic and should succeed let _ = insert_page(&mut c, 2); c.verify_cache_integrity(); } #[test] fn hand_is_reset_after_clear_and_resize() { let mut c = PageCache::new_with_spill(3, true); for i in 1..=3 { let _ = insert_page(&mut c, i); } c.clear(false).unwrap(); // No elements; insert should not rely on stale hand let _ = insert_page(&mut c, 10); // Resize from 1 -> 4 and back should not OOB the hand assert_eq!(c.resize(4), CacheResizeResult::Done); assert_eq!(c.resize(1), CacheResizeResult::Done); let _ = insert_page(&mut c, 11); c.verify_cache_integrity(); } #[test] fn resize_preserves_ref_and_recency() { let mut c = PageCache::new_with_spill(4, true); let _k1 = insert_page(&mut c, 1); let k2 = insert_page(&mut c, 2); let _k3 = insert_page(&mut c, 3); let _k4 = insert_page(&mut c, 4); // Make k2 hot. for _ in 0..3 { assert!(c.get(&k2).unwrap().is_some()); } let _r_before = c.ref_of(&k2); // Shrink to 3 (one page will be evicted during repack/next insert) assert_eq!(c.resize(3), CacheResizeResult::Done); assert!(matches!(c.ref_of(&k2), _r_before)); // Force an eviction; hot k2 should survive more passes. let _ = insert_page(&mut c, 5); assert!(c.get(&k2).unwrap().is_some()); c.verify_cache_integrity(); } #[test] fn test_sieve_second_chance_preserves_marked_page() { let mut cache = PageCache::new_with_spill(3, true); let key1 = insert_page(&mut cache, 1); let key2 = insert_page(&mut cache, 2); let key3 = insert_page(&mut cache, 3); // Mark key1 for second chance assert!(cache.get(&key1).unwrap().is_some()); let key4 = insert_page(&mut cache, 4); // CLOCK sweep from hand: // - key1 marked -> decrement, continue // - key3 (MRU) unmarked -> evict assert!( cache.get(&key1).unwrap().is_some(), "key1 had ref bit set, got second chance" ); assert!( cache.get(&key3).unwrap().is_none(), "key3 (MRU) should be evicted" ); assert!(cache.get(&key4).unwrap().is_some(), "key4 just inserted"); assert!( cache.get(&key2).unwrap().is_some(), "key2 (middle) should remain" ); cache.verify_cache_integrity(); } #[test] fn test_clock_sweep_wraps_around() { // Test that clock hand properly wraps around the circular list let mut cache = PageCache::new_with_spill(3, true); let key1 = insert_page(&mut cache, 1); let key2 = insert_page(&mut cache, 2); let key3 = insert_page(&mut cache, 3); // Mark all pages assert!(cache.get(&key1).unwrap().is_some()); assert!(cache.get(&key2).unwrap().is_some()); assert!(cache.get(&key3).unwrap().is_some()); // Insert 4: hand will sweep full circle, decrementing all refs // then sweep again and evict first unmarked page let key4 = insert_page(&mut cache, 4); // One page was evicted after full sweep assert_eq!(cache.len(), 3); assert!(cache.get(&key4).unwrap().is_some()); // Verify exactly one of the original pages was evicted let survivors = [key1, key2, key3] .iter() .filter(|k| cache.get(k).unwrap().is_some()) .count(); assert_eq!(survivors, 2, "Should have 2 survivors from original 3"); cache.verify_cache_integrity(); } #[test] fn test_circular_list_single_element() { let mut cache = PageCache::new_with_spill(3, true); let key1 = insert_page(&mut cache, 1); // Single element exists assert_eq!(cache.len(), 1); assert!(cache.contains_key(&key1)); // Delete single element assert!(cache.delete(key1).is_ok()); assert!(cache.clock_hand.is_null()); // Insert after empty should work let key2 = insert_page(&mut cache, 2); assert_eq!(cache.len(), 1); assert!(cache.contains_key(&key2)); cache.verify_cache_integrity(); } #[test] fn test_hand_advances_on_eviction() { // Note: page 1 is DatabaseHeader and is never evictable, so use page ids >= 2 let mut cache = PageCache::new_with_spill(2, true); let _key2 = insert_page(&mut cache, 2); let _key3 = insert_page(&mut cache, 3); // Note initial hand position let initial_hand = cache.clock_hand; // Force eviction let _key4 = insert_page(&mut cache, 4); // Hand should exist (not null) let new_hand = cache.clock_hand; assert!(!new_hand.is_null()); // Hand moved during sweep (exact position depends on eviction) assert!(initial_hand.is_null() || new_hand != initial_hand || cache.len() < 2); cache.verify_cache_integrity(); } #[test] fn test_multi_level_ref_counting() { let mut cache = PageCache::new_with_spill(2, true); let key1 = insert_page(&mut cache, 1); let _key2 = insert_page(&mut cache, 2); // Bump key1 to MAX (3 accesses) for _ in 0..3 { assert!(cache.get(&key1).unwrap().is_some()); } assert_eq!(cache.ref_of(&key1), Some(REF_MAX)); // Insert multiple new pages - key1 should survive longer for i in 3..6 { let _ = insert_page(&mut cache, i); } // key1 might still be there due to high ref count // (depends on exact sweep pattern, but it got multiple chances) cache.verify_cache_integrity(); } #[test] fn test_resize_maintains_circular_structure() { let mut cache = PageCache::new_with_spill(5, true); for i in 1..=4 { let _ = insert_page(&mut cache, i); } // Resize smaller assert_eq!(cache.resize(2), CacheResizeResult::Done); assert_eq!(cache.len(), 2); // Verify structure via integrity check cache.verify_cache_integrity(); } #[test] fn test_link_after_correctness() { let mut cache = PageCache::new_with_spill(4, true); let key1 = insert_page(&mut cache, 1); let key2 = insert_page(&mut cache, 2); let key3 = insert_page(&mut cache, 3); // Verify all keys are in cache assert!(cache.contains_key(&key1)); assert!(cache.contains_key(&key2)); assert!(cache.contains_key(&key3)); assert_eq!(cache.len(), 3); cache.verify_cache_integrity(); } #[test] fn test_evictable_count_tracking() { // Test that evictable_count is tracked correctly for fast-path spill check // Note: page 1 is DatabaseHeader and is never evictable let mut cache = PageCache::new_with_spill(10, true); // Insert clean pages (all evictable except page 1) let key1 = insert_page(&mut cache, 1); // page 1 is never evictable let key2 = insert_page(&mut cache, 2); let key3 = insert_page(&mut cache, 3); // Page 1 is not counted, pages 2 and 3 are evictable assert_eq!(cache.evictable_count(), 2); // Make page 2 dirty - it becomes non-evictable cache.notify_page_dirty(key2); assert_eq!(cache.evictable_count(), 1); // Make page 2 spilled - it becomes evictable again cache.notify_page_spilled(key2); assert_eq!(cache.evictable_count(), 2); // Delete page 3 - evictable count decreases assert!(cache.delete(key3).is_ok()); assert_eq!(cache.evictable_count(), 1); // Delete page 1 (which wasn't counted) - no change assert!(cache.delete(key1).is_ok()); assert_eq!(cache.evictable_count(), 1); // Clear cache assert!(cache.clear(true).is_ok()); assert_eq!(cache.evictable_count(), 0); cache.verify_cache_integrity(); } #[test] fn test_needs_spill_fast_path() { // Test that needs_spill uses the fast path when we have enough evictable pages // Capacity 10, threshold 90% = 9, so when len > 9 we need some evictable pages let mut cache = PageCache::new_with_spill(10, true); // Insert 10 clean pages (all evictable except page 1) for i in 1..=10 { let _ = insert_page(&mut cache, i); } // len=10, threshold=9, needed_evictable = 10-9 = 1 // evictable_count = 9 (pages 2-10, page 1 not counted) // Fast path: 9 >= 1, so no spill needed assert!(!cache.needs_spill()); assert_eq!(cache.evictable_count(), 9); // Make all pages dirty for i in 2..=10 { let key = create_key(i); cache.notify_page_dirty(key); cache.peek(&key, false).unwrap().set_dirty(); } // Now evictable_count = 0 and pages are actually dirty // needed_evictable = 1, but we have 0 evictable pages assert_eq!(cache.evictable_count(), 0); // needs_spill should return true because we need 1 evictable page but have 0 assert!(cache.needs_spill()); // Mark pages as spilled for i in 2..=10 { let key = create_key(i); cache.notify_page_spilled(key); cache.peek(&key, false).unwrap().set_spilled(); } // Now evictable_count = 9 and pages are spilled (evictable) assert_eq!(cache.evictable_count(), 9); // Fast path: 9 >= 1, so no spill needed assert!(!cache.needs_spill()); cache.verify_cache_integrity(); } } ================================================ FILE: core/storage/pager.rs ================================================ use crate::assert::assert_send_sync; #[cfg(target_vendor = "apple")] use crate::io::AtomicFileSyncType; use crate::io::FileSyncType; use crate::io::WriteBatch; use crate::storage::btree::PinGuard; use crate::storage::subjournal::Subjournal; use crate::storage::wal::PreparedFrames; use crate::storage::{ buffer_pool::BufferPool, database::DatabaseStorage, sqlite3_ondisk::{ self, parse_wal_frame_header, DatabaseHeader, OverflowCell, PageSize, PageType, CELL_PTR_SIZE_BYTES, INTERIOR_PAGE_HEADER_SIZE_BYTES, LEAF_PAGE_HEADER_SIZE_BYTES, MINIMUM_CELL_SIZE, }, wal::{CheckpointResult, RollbackTo, Wal, IOV_MAX}, }; use crate::sync::atomic::{ AtomicBool, AtomicIsize, AtomicU16, AtomicU32, AtomicU64, AtomicU8, AtomicUsize, Ordering, }; use crate::sync::Arc; use crate::sync::{Mutex, RwLock}; use crate::types::{IOCompletions, WalState}; use crate::util::IOExt as _; use crate::{ io::CompletionGroup, return_if_io, types::WalFrameInfo, Completion, Connection, IOResult, LimboError, Result, TransactionState, }; use crate::{io_yield_one, Buffer, CompletionError, IOContext, OpenFlags, SyncMode, IO}; #[allow(unused_imports)] use crate::{ turso_assert, turso_assert_eq, turso_assert_greater_than, turso_assert_greater_than_or_equal, turso_assert_less_than, turso_assert_ne, turso_debug_assert, turso_soft_unreachable, }; use arc_swap::ArcSwapOption; use roaring::RoaringBitmap; use std::cell::UnsafeCell; use tracing::{instrument, trace, Level}; use super::btree::offset::{ BTREE_CELL_CONTENT_AREA, BTREE_CELL_COUNT, BTREE_FIRST_FREEBLOCK, BTREE_FRAGMENTED_BYTES_COUNT, BTREE_PAGE_TYPE, BTREE_RIGHTMOST_PTR, }; use super::btree::{ btree_init_page, payload_overflow_threshold_max, payload_overflow_threshold_min, }; use super::page_cache::{CacheError, CacheResizeResult, PageCache, PageCacheKey, SpillResult}; use super::sqlite3_ondisk::read_varint; use super::sqlite3_ondisk::{ begin_write_btree_page, read_btree_cell, read_u32, BTreeCell, FREELIST_LEAF_PTR_SIZE, FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR, FREELIST_TRUNK_OFFSET_LEAF_COUNT, FREELIST_TRUNK_OFFSET_NEXT_TRUNK_PTR, }; use super::wal::CheckpointMode; use crate::storage::encryption::{CipherMode, EncryptionContext, EncryptionKey}; /// SQLite's default maximum page count const DEFAULT_MAX_PAGE_COUNT: u32 = 0xfffffffe; const RESERVED_SPACE_NOT_SET: u16 = u16::MAX; #[cfg(feature = "test_helper")] /// Used for testing purposes to change the position of the PENDING BYTE static PENDING_BYTE: AtomicU32 = AtomicU32::new(0x40000000); #[cfg(not(feature = "test_helper"))] /// Byte offset that signifies the start of the ignored page - 1 GB mark const PENDING_BYTE: u32 = 0x40000000; #[cfg(not(feature = "omit_autovacuum"))] use ptrmap::*; #[derive(Debug, Clone)] pub struct HeaderRef(PageRef); impl HeaderRef { pub fn from_pager(pager: &Pager) -> Result> { let page = return_if_io!(pager.read_header_page()); Ok(IOResult::Done(Self(page))) } pub fn borrow(&self) -> &DatabaseHeader { // TODO: Instead of erasing mutability, implement `get_mut_contents` and return a shared reference. let content = self.0.get_contents(); bytemuck::from_bytes::(&content.as_ptr()[0..DatabaseHeader::SIZE]) } } #[derive(Debug, Clone)] pub struct HeaderRefMut(PageRef); impl HeaderRefMut { pub fn from_pager(pager: &Pager) -> Result> { let page = return_if_io!(pager.read_header_page()); pager.add_dirty(&page)?; Ok(IOResult::Done(Self(page))) } pub fn borrow_mut(&self) -> &mut DatabaseHeader { let content = self.0.get_contents(); bytemuck::from_bytes_mut::(&mut content.as_ptr()[0..DatabaseHeader::SIZE]) } /// Get a reference to the underlying page pub fn page(&self) -> &PageRef { &self.0 } } pub struct PageInner { pub flags: AtomicUsize, pub id: usize, /// If >0, the page is pinned and not eligible for eviction from the page cache. /// The reason this is a counter is that multiple nested code paths may signal that /// a page must not be evicted from the page cache, so even if an inner code path /// requests unpinning via [Page::unpin], the pin count will still be >0 if the outer /// code path has not yet requested to unpin the page as well. /// /// Note that [PageCache::clear] evicts the pages even if pinned, so as long as /// we clear the page cache on errors, pins will not 'leak'. pub pin_count: AtomicUsize, /// The WAL frame number this page was loaded from (0 if loaded from main DB file) /// This tracks which version of the page we have in memory pub wal_tag: AtomicU64, /// The actual page data buffer. None if not loaded. pub buffer: Option>, /// Overflow cells during btree operations pub overflow_cells: Vec, } // Methods moved from PageContent - these provide btree page access impl PageInner { /// Creates a new PageInner from an Arc. pub fn new(buffer: Arc) -> Self { Self { flags: AtomicUsize::new(0), id: 0, pin_count: AtomicUsize::new(0), wal_tag: AtomicU64::new(TAG_UNSET), buffer: Some(buffer), overflow_cells: Vec::new(), } } /// Creates a new PageInner with an owned buffer. pub fn from_buffer(buffer: Buffer) -> Self { Self { flags: AtomicUsize::new(0), id: 0, pin_count: AtomicUsize::new(0), wal_tag: AtomicU64::new(TAG_UNSET), buffer: Some(Arc::new(buffer)), overflow_cells: Vec::new(), } } /// Get the page buffer as a mutable slice. Panics if buffer not loaded. #[inline] #[allow(clippy::mut_from_ref)] pub fn as_ptr(&self) -> &mut [u8] { self.buffer .as_ref() .expect("buffer not loaded") .as_mut_slice() } /// The position where page content starts. It's 100 for page 1 (database file header is 100 bytes), /// 0 for all other pages. #[inline] pub fn offset(&self) -> usize { if self.id == 1 { DatabaseHeader::SIZE } else { 0 } } /// Read a u8 from the page content at the given offset, taking account the possible db header on page 1. #[inline] fn read_u8(&self, pos: usize) -> u8 { let buf = self.as_ptr(); buf[self.offset() + pos] } /// Read a u16 from the page content at the given offset, taking account the possible db header on page 1. #[inline] fn read_u16(&self, pos: usize) -> u16 { let buf = self.as_ptr(); let offset = self.offset(); u16::from_be_bytes([buf[offset + pos], buf[offset + pos + 1]]) } /// Read a u32 from the page content at the given offset, taking account the possible db header on page 1. #[inline] fn read_u32(&self, pos: usize) -> u32 { let buf = self.as_ptr(); read_u32(buf, self.offset() + pos) } /// Write a u8 to the page content at the given offset, taking account the possible db header on page 1. #[inline] fn write_u8(&self, pos: usize, value: u8) { tracing::trace!("write_u8(pos={}, value={})", pos, value); let buf = self.as_ptr(); buf[self.offset() + pos] = value; } /// Write a u16 to the page content at the given offset, taking account the possible db header on page 1. #[inline] fn write_u16(&self, pos: usize, value: u16) { tracing::trace!("write_u16(pos={}, value={})", pos, value); let buf = self.as_ptr(); let offset = self.offset(); buf[offset + pos..offset + pos + 2].copy_from_slice(&value.to_be_bytes()); } /// Write a u32 to the page content at the given offset, taking account the possible db header on page 1. #[inline] fn write_u32(&self, pos: usize, value: u32) { tracing::trace!("write_u32(pos={}, value={})", pos, value); let buf = self.as_ptr(); let offset = self.offset(); buf[offset + pos..offset + pos + 4].copy_from_slice(&value.to_be_bytes()); } #[inline] pub fn page_type(&self) -> crate::Result { self.read_u8(BTREE_PAGE_TYPE).try_into() } /// Read a u16 from the page content at the given absolute offset (no db header offset). #[inline] pub fn read_u16_no_offset(&self, pos: usize) -> u16 { let buf = self.as_ptr(); u16::from_be_bytes([buf[pos], buf[pos + 1]]) } /// Read a u32 from the page content at the given absolute offset (no db header offset). #[inline] pub fn read_u32_no_offset(&self, pos: usize) -> u32 { let buf = self.as_ptr(); u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]) } /// Write a u16 at the given absolute offset (no db header offset). pub fn write_u16_no_offset(&self, pos: usize, value: u16) { tracing::trace!("write_u16_no_offset(pos={}, value={})", pos, value); let buf = self.as_ptr(); buf[pos..pos + 2].copy_from_slice(&value.to_be_bytes()); } /// Write a u32 at the given absolute offset (no db header offset). pub fn write_u32_no_offset(&self, pos: usize, value: u32) { tracing::trace!("write_u32_no_offset(pos={}, value={})", pos, value); let buf = self.as_ptr(); buf[pos..pos + 4].copy_from_slice(&value.to_be_bytes()); } pub fn write_page_type(&self, value: u8) { self.write_u8(BTREE_PAGE_TYPE, value); } pub fn write_rightmost_ptr(&self, value: u32) { self.write_u32(BTREE_RIGHTMOST_PTR, value); } pub fn write_first_freeblock(&self, value: u16) { self.write_u16(BTREE_FIRST_FREEBLOCK, value); } pub fn write_freeblock(&self, offset: u16, size: u16, next_block: Option) { self.write_freeblock_next_ptr(offset, next_block.unwrap_or(0)); self.write_freeblock_size(offset, size); } pub fn write_freeblock_size(&self, offset: u16, size: u16) { self.write_u16_no_offset(offset as usize + 2, size); } pub fn write_freeblock_next_ptr(&self, offset: u16, next_block: u16) { self.write_u16_no_offset(offset as usize, next_block); } pub fn read_freeblock(&self, offset: u16) -> (u16, u16) { ( self.read_u16_no_offset(offset as usize), self.read_u16_no_offset(offset as usize + 2), ) } pub fn write_cell_count(&self, value: u16) { self.write_u16(BTREE_CELL_COUNT, value); } pub fn write_cell_content_area(&self, value: usize) { turso_debug_assert!(value <= PageSize::MAX as usize); let value = value as u16; self.write_u16(BTREE_CELL_CONTENT_AREA, value); } pub fn write_fragmented_bytes_count(&self, value: u8) { self.write_u8(BTREE_FRAGMENTED_BYTES_COUNT, value); } #[inline] pub fn first_freeblock(&self) -> u16 { self.read_u16(BTREE_FIRST_FREEBLOCK) } #[inline] pub fn cell_count(&self) -> usize { self.read_u16(BTREE_CELL_COUNT) as usize } #[inline] pub fn cell_pointer_array_size(&self) -> usize { self.cell_count() * CELL_PTR_SIZE_BYTES } #[inline] pub fn unallocated_region_start(&self) -> usize { let (cell_ptr_array_start, cell_ptr_array_size) = self.cell_pointer_array_offset_and_size(); cell_ptr_array_start + cell_ptr_array_size } #[inline] pub fn unallocated_region_size(&self) -> usize { self.cell_content_area() as usize - self.unallocated_region_start() } #[inline] pub fn cell_content_area(&self) -> u32 { let offset = self.read_u16(BTREE_CELL_CONTENT_AREA); if offset == 0 { PageSize::MAX } else { offset as u32 } } #[inline] pub fn header_size(&self) -> usize { let is_interior = self.read_u8(BTREE_PAGE_TYPE) <= PageType::TableInterior as u8; (!is_interior as usize) * LEAF_PAGE_HEADER_SIZE_BYTES + (is_interior as usize) * INTERIOR_PAGE_HEADER_SIZE_BYTES } #[inline] pub fn num_frag_free_bytes(&self) -> u8 { self.read_u8(BTREE_FRAGMENTED_BYTES_COUNT) } #[inline] pub fn rightmost_pointer(&self) -> crate::Result> { match self.page_type()? { PageType::IndexInterior | PageType::TableInterior => { Ok(Some(self.read_u32(BTREE_RIGHTMOST_PTR))) } PageType::IndexLeaf | PageType::TableLeaf => Ok(None), } } #[inline] pub fn rightmost_pointer_raw(&self) -> crate::Result> { match self.page_type()? { PageType::IndexInterior | PageType::TableInterior => Ok(Some(unsafe { self.as_ptr() .as_mut_ptr() .add(self.offset() + BTREE_RIGHTMOST_PTR) })), PageType::IndexLeaf | PageType::TableLeaf => Ok(None), } } #[inline] pub fn cell_get(&self, idx: usize, usable_size: usize) -> crate::Result { tracing::trace!("cell_get(idx={})", idx); let buf = self.as_ptr(); let ncells = self.cell_count(); turso_assert_less_than!(idx, ncells, "cell_get: idx out of bounds", {"idx": idx, "ncells": ncells} ); let cell_pointer_array_start = self.header_size(); let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES); let cell_pointer = self.read_u16(cell_pointer) as usize; let static_buf: &'static [u8] = unsafe { std::mem::transmute::<&[u8], &'static [u8]>(buf) }; read_btree_cell(static_buf, self, cell_pointer, usable_size) } #[inline(always)] pub fn cell_table_interior_read_rowid(&self, idx: usize) -> crate::Result { turso_debug_assert!(matches!(self.page_type(), Ok(PageType::TableInterior))); let buf = self.as_ptr(); let cell_pointer_array_start = self.header_size(); let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES); let cell_pointer = self.read_u16(cell_pointer) as usize; const LEFT_CHILD_PAGE_SIZE_BYTES: usize = 4; let (rowid, _) = read_varint(crate::slice_in_bounds_or_corrupt!( buf, cell_pointer + LEFT_CHILD_PAGE_SIZE_BYTES.. ))?; Ok(rowid as i64) } #[inline(always)] pub fn cell_interior_read_left_child_page(&self, idx: usize) -> crate::Result { turso_debug_assert!(matches!( self.page_type(), Ok(PageType::TableInterior) | Ok(PageType::IndexInterior) )); let buf = self.as_ptr(); let cell_pointer_array_start = self.header_size(); let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES); let cell_pointer = self.read_u16(cell_pointer) as usize; crate::assert_or_bail_corrupt!( cell_pointer + 4 <= buf.len(), "cell pointer {} out of bounds for page size {}", cell_pointer, buf.len() ); Ok(u32::from_be_bytes([ buf[cell_pointer], buf[cell_pointer + 1], buf[cell_pointer + 2], buf[cell_pointer + 3], ])) } #[inline(always)] pub fn cell_table_leaf_read_rowid(&self, idx: usize) -> crate::Result { turso_debug_assert!(matches!(self.page_type(), Ok(PageType::TableLeaf))); let buf = self.as_ptr(); let cell_pointer_array_start = self.header_size(); let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES); let cell_pointer = self.read_u16(cell_pointer) as usize; let mut pos = cell_pointer; let (_, nr) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, pos..))?; pos += nr; let (rowid, _) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, pos..))?; Ok(rowid as i64) } /// Fast path for index cells: returns payload slice and overflow info without constructing BTreeCell. /// /// This bypasses the full `cell_get()` to `read_btree_cell()` path for binary search hot loops. /// The returned slice is valid as long as the page is alive. /// /// Returns: (payload_slice, payload_size, first_overflow_page) #[inline(always)] pub fn cell_index_read_payload_ptr( &self, idx: usize, usable_size: usize, ) -> crate::Result<(&'static [u8], u64, Option)> { let buf = self.as_ptr(); let cell_pointer_array_start = self.header_size(); let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES); let cell_offset = self.read_u16(cell_pointer) as usize; let page_type = self.page_type()?; let (payload_size, varint_len, header_skip) = match page_type { PageType::IndexInterior => { let (size, len) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, cell_offset + 4..))?; (size, len, 4usize) } PageType::IndexLeaf => { let (size, len) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, cell_offset..))?; (size, len, 0usize) } _ => unreachable!("cell_index_read_payload_ptr called on non-index page"), }; let payload_start = cell_offset + header_skip + varint_len; let max_local = payload_overflow_threshold_max(page_type, usable_size); let min_local = payload_overflow_threshold_min(page_type, usable_size); let (overflows, local_size) = sqlite3_ondisk::payload_overflows( payload_size as usize, max_local, min_local, usable_size, ); let (payload_slice, first_overflow) = if overflows { let overflow_ptr_offset = payload_start + local_size - 4; crate::assert_or_bail_corrupt!( overflow_ptr_offset + 4 <= buf.len(), "overflow pointer offset {} out of bounds for page size {}", overflow_ptr_offset, buf.len() ); let first_overflow_page = u32::from_be_bytes([ buf[overflow_ptr_offset], buf[overflow_ptr_offset + 1], buf[overflow_ptr_offset + 2], buf[overflow_ptr_offset + 3], ]); let payload_end = payload_start + local_size - 4; crate::assert_or_bail_corrupt!( payload_start < payload_end && payload_end <= buf.len(), "payload range {}..{} out of bounds for page size {}", payload_start, payload_end, buf.len() ); // SAFETY: valid as long as page is alive let slice = unsafe { std::mem::transmute::<&[u8], &'static [u8]>(&buf[payload_start..payload_end]) }; (slice, Some(first_overflow_page)) } else { let payload_end = payload_start + payload_size as usize; crate::assert_or_bail_corrupt!( payload_end <= buf.len(), "payload range {}..{} out of bounds for page size {}", payload_start, payload_end, buf.len() ); // SAFETY: valid as long as page is alive let slice = unsafe { std::mem::transmute::<&[u8], &'static [u8]>(&buf[payload_start..payload_end]) }; (slice, None) }; Ok((payload_slice, payload_size, first_overflow)) } #[inline] pub fn cell_pointer_array_offset_and_size(&self) -> (usize, usize) { ( self.cell_pointer_array_offset(), self.cell_pointer_array_size(), ) } #[inline] pub fn cell_pointer_array_offset(&self) -> usize { self.offset() + self.header_size() } #[inline] pub fn cell_get_raw_start_offset(&self, idx: usize) -> usize { let cell_pointer_array_start = self.cell_pointer_array_offset(); let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES); self.read_u16_no_offset(cell_pointer) as usize } #[inline] pub fn cell_get_raw_region( &self, idx: usize, usable_size: usize, ) -> crate::Result<(usize, usize)> { let page_type = self.page_type()?; let max_local = payload_overflow_threshold_max(page_type, usable_size); let min_local = payload_overflow_threshold_min(page_type, usable_size); let cell_count = self.cell_count(); self._cell_get_raw_region_faster( idx, usable_size, cell_count, max_local, min_local, page_type, ) } #[inline] pub fn _cell_get_raw_region_faster( &self, idx: usize, usable_size: usize, cell_count: usize, max_local: usize, min_local: usize, page_type: PageType, ) -> crate::Result<(usize, usize)> { let buf = self.as_ptr(); turso_assert_less_than!(idx, cell_count); let start = self.cell_get_raw_start_offset(idx); let len = match page_type { PageType::IndexInterior => { let (len_payload, n_payload) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, start + 4..))?; let (overflows, to_read) = sqlite3_ondisk::payload_overflows( len_payload as usize, max_local, min_local, usable_size, ); if overflows { 4 + to_read + n_payload } else { 4 + len_payload as usize + n_payload } } PageType::TableInterior => { let (_, n_rowid) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, start + 4..))?; 4 + n_rowid } PageType::IndexLeaf => { let (len_payload, n_payload) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, start..))?; let (overflows, to_read) = sqlite3_ondisk::payload_overflows( len_payload as usize, max_local, min_local, usable_size, ); if overflows { to_read + n_payload } else { let mut size = len_payload as usize + n_payload; if size < MINIMUM_CELL_SIZE { size = MINIMUM_CELL_SIZE; } size } } PageType::TableLeaf => { let (len_payload, n_payload) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, start..))?; let (_, n_rowid) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, start + n_payload..))?; let (overflows, to_read) = sqlite3_ondisk::payload_overflows( len_payload as usize, max_local, min_local, usable_size, ); if overflows { to_read + n_payload + n_rowid } else { let mut size = len_payload as usize + n_payload + n_rowid; if size < MINIMUM_CELL_SIZE { size = MINIMUM_CELL_SIZE; } size } } }; crate::assert_or_bail_corrupt!( start + len <= buf.len(), "cell region {}..{} out of bounds for page size {}", start, start + len, buf.len() ); Ok((start, len)) } pub fn is_leaf(&self) -> bool { self.read_u8(BTREE_PAGE_TYPE) > PageType::TableInterior as u8 } pub fn write_database_header(&self, header: &DatabaseHeader) { let buf = self.as_ptr(); buf[0..DatabaseHeader::SIZE].copy_from_slice(bytemuck::bytes_of(header)); } pub fn debug_print_freelist(&self, usable_space: usize) { let mut pc = self.first_freeblock() as usize; let mut block_num = 0; println!("---- Free List Blocks ----"); println!("first freeblock pointer: {pc}"); println!("cell content area: {}", self.cell_content_area()); println!("fragmented bytes: {}", self.num_frag_free_bytes()); while pc != 0 && pc <= usable_space { let next = self.read_u16_no_offset(pc); let size = self.read_u16_no_offset(pc + 2); println!("block {block_num}: position={pc}, size={size}, next={next}"); pc = next as usize; block_num += 1; } println!("--------------"); } } /// Type alias for backward compatibility - PageContent is now PageInner pub type PageContent = PageInner; /// WAL tag not set pub const TAG_UNSET: u64 = u64::MAX; /// WAL write in progress, sentinel value set before starting a WAL write /// so we can detect if page was modified during the write pub const TAG_WRITE_PENDING: u64 = u64::MAX - 1; /// Bit layout: /// epoch: 20 /// frame: 44 const EPOCH_BITS: u32 = 20; const FRAME_BITS: u32 = 64 - EPOCH_BITS; const EPOCH_SHIFT: u32 = FRAME_BITS; const EPOCH_MAX: u32 = (1u32 << EPOCH_BITS) - 1; const FRAME_MAX: u64 = (1u64 << FRAME_BITS) - 1; #[inline] pub fn pack_tag_pair(frame: u64, seq: u32) -> u64 { ((seq as u64) << EPOCH_SHIFT) | (frame & FRAME_MAX) } #[inline] pub fn unpack_tag_pair(tag: u64) -> (u64, u32) { let epoch = ((tag >> EPOCH_SHIFT) & (EPOCH_MAX as u64)) as u32; let frame = tag & FRAME_MAX; (frame, epoch) } #[derive(Debug)] pub struct Page { pub inner: UnsafeCell, } // SAFETY: Page is thread-safe because we use atomic page flags to serialize // concurrent modifications. unsafe impl Send for Page {} unsafe impl Sync for Page {} crate::assert::assert_send_sync!(Page); // Concurrency control of pages will be handled by the pager, we won't wrap Page with RwLock // because that is bad bad. pub type PageRef = Arc; /// Page is locked for I/O to prevent concurrent access. const PAGE_LOCKED: usize = 0b010; /// Page is dirty. Flush needed. const PAGE_DIRTY: usize = 0b1000; /// Page's contents are loaded in memory. const PAGE_LOADED: usize = 0b10000; /// Page has been spilled to WAL (can be evicted even though dirty). const PAGE_SPILLED: usize = 0b100000; impl Page { pub fn new(id: i64) -> Self { turso_assert_greater_than_or_equal!(id, 0); Self { inner: UnsafeCell::new(PageInner { flags: AtomicUsize::new(0), id: id as usize, pin_count: AtomicUsize::new(0), wal_tag: AtomicU64::new(TAG_UNSET), buffer: None, overflow_cells: Vec::new(), }), } } #[allow(clippy::mut_from_ref)] pub fn get(&self) -> &mut PageInner { unsafe { &mut *self.inner.get() } } /// Returns a mutable reference to PageInner for accessing page contents. /// Panics if the page buffer is not loaded. pub fn get_contents(&self) -> &mut PageInner { let inner = self.get(); turso_debug_assert!( inner.buffer.is_some(), "page buffer not loaded", { "page_id": inner.id } ); inner } #[inline] pub fn is_locked(&self) -> bool { self.get().flags.load(Ordering::Acquire) & PAGE_LOCKED != 0 } #[inline] pub fn set_locked(&self) { self.get().flags.fetch_or(PAGE_LOCKED, Ordering::Acquire); } #[inline] pub fn clear_locked(&self) { self.get().flags.fetch_and(!PAGE_LOCKED, Ordering::Release); } #[inline] pub fn is_dirty(&self) -> bool { self.get().flags.load(Ordering::Acquire) & PAGE_DIRTY != 0 } #[inline] /// almost never should be called explicitly - instead [Pager::add_dirty] method must be used pub fn set_dirty(&self) { tracing::debug!("set_dirty(page={})", self.get().id); self.clear_wal_tag(); // Clear spilled flag since page is being modified again self.get().flags.fetch_and(!PAGE_SPILLED, Ordering::Release); self.get().flags.fetch_or(PAGE_DIRTY, Ordering::Release); } #[inline] /// caller must ensure that [Pager::dirty_pages] will be updated accordingly pub fn clear_dirty(&self) { tracing::debug!("clear_dirty(page={})", self.get().id); self.get().flags.fetch_and(!PAGE_DIRTY, Ordering::Release); self.clear_wal_tag(); } /// Clear the dirty flag without touching wal_tag. /// Used when a WAL frame has been durably written and the tag already encodes it. #[inline] pub fn clear_dirty_keep_wal_tag(&self) { tracing::debug!("clear_dirty_keep_wal_tag(page={})", self.get().id); self.get().flags.fetch_and(!PAGE_DIRTY, Ordering::Release); } /// Returns true if the page has been spilled to WAL and is safe to evict even while dirty. #[inline] pub fn is_spilled(&self) -> bool { self.get().flags.load(Ordering::Acquire) & PAGE_SPILLED != 0 } /// Mark the page as spilled to WAL. Spilled pages remain dirty but may be evicted from cache. #[inline] pub fn set_spilled(&self) { tracing::debug!("set_spilled(page={})", self.get().id); self.get().flags.fetch_or(PAGE_SPILLED, Ordering::Release); } /// Clear the spilled flag. This is also done implicitly on set_dirty(). #[inline] pub fn clear_spilled(&self) { self.get().flags.fetch_and(!PAGE_SPILLED, Ordering::Release); } #[inline] pub fn is_loaded(&self) -> bool { self.get().flags.load(Ordering::Acquire) & PAGE_LOADED != 0 } #[inline] pub fn set_loaded(&self) { self.get().flags.fetch_or(PAGE_LOADED, Ordering::Release); } #[inline] pub fn clear_loaded(&self) { tracing::debug!("clear loaded {}", self.get().id); self.get().flags.fetch_and(!PAGE_LOADED, Ordering::Release); } #[inline] pub fn is_index(&self) -> crate::Result { Ok(match self.get_contents().page_type()? { PageType::IndexLeaf | PageType::IndexInterior => true, PageType::TableLeaf | PageType::TableInterior => false, }) } /// Increment the pin count by 1. A pin count >0 means the page is pinned and not eligible for eviction from the page cache. #[inline] pub fn pin(&self) { self.get().pin_count.fetch_add(1, Ordering::SeqCst); } /// Decrement the pin count by 1. If the count reaches 0, the page is no longer /// pinned and is eligible for eviction from the page cache. #[inline] pub fn unpin(&self) { let was_pinned = self.try_unpin(); turso_assert!( was_pinned, "Attempted to unpin page that was not pinned", { "page_id": self.get().id } ); } /// Try to decrement the pin count by 1, but do nothing if it was already 0. /// Returns true if the pin count was decremented. #[inline] pub fn try_unpin(&self) -> bool { self.get() .pin_count .fetch_update(Ordering::Release, Ordering::SeqCst, |current| { if current == 0 { None } else { Some(current - 1) } }) .is_ok() } /// Returns true if the page is pinned and thus not eligible for eviction from the page cache. #[inline] pub fn is_pinned(&self) -> bool { self.get().pin_count.load(Ordering::Acquire) > 0 } #[inline] /// Set the WAL tag from a (frame, epoch) pair. /// If inputs are invalid, stores TAG_UNSET, which will prevent /// the cached page from being used during checkpoint. pub fn set_wal_tag(&self, frame: u64, epoch: u32) { // use only first 20 bits for seq (max: 1048576) let e = epoch & EPOCH_MAX; self.get() .wal_tag .store(pack_tag_pair(frame, e), Ordering::Release); } #[inline] /// Load the (frame, seq) pair from the packed tag. pub fn wal_tag_pair(&self) -> (u64, u32) { unpack_tag_pair(self.get().wal_tag.load(Ordering::Acquire)) } #[inline] pub fn clear_wal_tag(&self) { self.get().wal_tag.store(TAG_UNSET, Ordering::Release) } #[inline] /// Returns true if the page has a valid WAL tag (i.e., was written to WAL and not modified since). /// Returns false if the wal_tag is TAG_UNSET (page was modified since last WAL write). pub fn has_wal_tag(&self) -> bool { let tag = self.get().wal_tag.load(Ordering::Acquire); let result = tag != TAG_UNSET && tag != TAG_WRITE_PENDING; tracing::debug!( "has_wal_tag(page={}) = {} (tag={:x})", self.get().id, result, tag ); result } #[inline] /// Mark page as having a WAL write in progress. /// This is set before starting a spill/cacheflush so we can detect /// if the page was modified during the write. pub fn set_write_pending(&self) { tracing::debug!("set_write_pending(page={})", self.get().id); self.get() .wal_tag .store(TAG_WRITE_PENDING, Ordering::Release); } #[inline] /// Try to set the WAL tag, but only if the page wasn't modified during the write. /// Returns true if the tag was set, false if the page was modified (wal_tag became TAG_UNSET). pub fn try_set_wal_tag(&self, frame: u64, epoch: u32) -> bool { let new_tag = pack_tag_pair(frame, epoch); let page_id = self.get().id; let current = self.get().wal_tag.load(Ordering::Acquire); // Only set if current tag is not TAG_UNSET (meaning page wasn't modified during write) // TAG_WRITE_PENDING is fine, it means the write was in progress and page wasn't modified if current == TAG_UNSET { tracing::debug!( "try_set_wal_tag(page={}, frame={}) SKIPPED: wal_tag is TAG_UNSET (page was modified)", page_id, frame ); return false; } tracing::debug!( "try_set_wal_tag(page={}, frame={}) SUCCESS: current={:x}", page_id, frame, current ); self.get().wal_tag.store(new_tag, Ordering::Release); true } #[inline] pub fn is_valid_for_checkpoint(&self, target_frame: u64, epoch: u32) -> bool { let (f, s) = self.wal_tag_pair(); f == target_frame && s == epoch && !self.is_dirty() && self.is_loaded() && !self.is_locked() } } #[derive(Clone, Copy, Debug, PartialEq)] /// The state of the current pager cache commit. enum CommitState { /// Prepare WAL header for commit if needed PrepareWal, /// Sync WAL header after prepare PrepareWalSync, /// Get DB size (mostly from page cache - but in rare cases we can read it from disk) GetDbSize, /// Scan all dirty pages and issue concurrent reads for evicted (spilled) pages. ScanAndIssueReads { db_size: u32 }, /// Wait for all batched reads of evicted pages to complete. WaitBatchedReads { db_size: u32 }, /// Collect pages (now all available) and prepare WAL frames. PrepareFrames { db_size: u32 }, /// All frames prepared, writes are in flight WaitWrites, /// Writes are complete, wait for WAL sync to complete WaitSync, /// Wait for WAL sync to complete and finalize the WAL commit. /// After this state, the write transaction is durable. /// If autocheckpoint is enabled and the autocheckpoint threshold is reached, checkpoint will be attempted. WalCommitDone, /// Checkpoint the WAL to the database file (if needed). /// This is decoupled from commit - checkpoint failure does not affect commit durability. AutoCheckpoint, } #[derive(Debug, Default)] struct CheckpointState { phase: CheckpointPhase, /// The checkpoint result, set after WAL checkpoint completes result: Option, /// The checkpoint mode, used to determine if WAL truncation is needed mode: Option, } #[derive(Clone, Debug, Default, PartialEq)] enum CheckpointPhase { #[default] NotCheckpointing, Checkpoint { mode: CheckpointMode, sync_mode: crate::SyncMode, clear_page_cache: bool, }, /// Truncate the database file if everything was backfilled and file is larger than expected. TruncateDbFile { sync_mode: crate::SyncMode, clear_page_cache: bool, /// Whether we've invalidated page 1 from cache (needed because checkpoint may write /// pages directly from WALto DB file, so cached page 1 of the checkpointer connection may have stale database_size) page1_invalidated: bool, }, /// Sync the database file after checkpoint (if sync_mode != Off and we backfilled any frames from the WAL). SyncDbFile { clear_page_cache: bool }, /// Truncate the WAL file after DB file is safely synced (only for TRUNCATE checkpoint mode). /// This must happen AFTER SyncDbFile to ensure data durability. TruncateWalFile { clear_page_cache: bool }, /// Finalize: release guard and optionally clear page cache. Finalize { clear_page_cache: bool }, } /// The mode of allocating a btree page. /// SQLite defines the following: /// #define BTALLOC_ANY 0 /* Allocate any page */ /// #define BTALLOC_EXACT 1 /* Allocate exact page if possible */ /// #define BTALLOC_LE 2 /* Allocate any page <= the parameter */ pub enum BtreePageAllocMode { /// Allocate any btree page Any, /// Allocate a specific page number, typically used for root page allocation Exact(u32), /// Allocate a page number less than or equal to the parameter Le(u32), } /// This will keep track of the state of current cache commit in order to not repeat work struct CommitInfo { completions: Vec, completion_group: Option, state: CommitState, collected_pages: Vec, page_sources: Vec, page_source_cursor: usize, prepared_frames: Vec, } /// Represents a dirty page that will be committed to the log. enum PageSource { /// Cache resident page Cached(usize), /// A page read from disk because it was spilled/evicted from cache Evicted(PageRef), } impl CommitInfo { fn reset(&mut self) { self.completions.clear(); self.completion_group = None; self.state = CommitState::PrepareWal; self.collected_pages.clear(); self.page_sources.clear(); self.prepared_frames.clear(); self.page_source_cursor = 0; } /// Clear and reserve space for n pages in each vector. fn initialize(&mut self, n: usize) { self.page_sources.clear(); self.page_sources.reserve(n.min(IOV_MAX)); self.completions.clear(); self.completions.reserve(n / 4); self.completion_group = None; self.collected_pages.reserve(n.min(IOV_MAX)); } } /// Track the state of the auto-vacuum mode. #[derive(Clone, Copy, Debug, PartialEq)] pub enum AutoVacuumMode { None, Full, Incremental, } impl From for u8 { fn from(mode: AutoVacuumMode) -> u8 { match mode { AutoVacuumMode::None => 0, AutoVacuumMode::Full => 1, AutoVacuumMode::Incremental => 2, } } } impl From for AutoVacuumMode { fn from(value: u8) -> AutoVacuumMode { match value { 0 => AutoVacuumMode::None, 1 => AutoVacuumMode::Full, 2 => AutoVacuumMode::Incremental, _ => unreachable!("Invalid AutoVacuumMode value: {}", value), } } } #[derive(Debug, Clone)] #[cfg(not(feature = "omit_autovacuum"))] enum PtrMapGetState { Start, Deserialize { ptrmap_page: PageRef, offset_in_ptrmap_page: usize, }, } #[derive(Debug, Clone)] #[cfg(not(feature = "omit_autovacuum"))] enum PtrMapPutState { Start, Deserialize { ptrmap_page: PageRef, offset_in_ptrmap_page: usize, }, } #[derive(Debug, Clone)] enum HeaderRefState { Start, CreateHeader { page: PageRef, completion: Option, }, } #[cfg(not(feature = "omit_autovacuum"))] #[derive(Debug, Clone, Copy)] enum BtreeCreateVacuumFullState { Start, AllocatePage { root_page_num: u32 }, PtrMapPut { allocated_page_id: u32 }, } #[derive(Debug, Clone)] enum SavepointKind { Statement, Named { name: String, starts_transaction: bool, }, } #[derive(Clone, Copy, Debug)] pub enum SavepointResult { /// Releasing the named savepoint should commit the surrounding transaction. Commit, /// The named savepoint was released without committing the transaction. Release, /// No matching named savepoint exists. NotFound, } #[derive(Debug, Clone)] struct SavepointSnapshot { kind: SavepointKind, start_offset: u64, db_size: u32, wal_max_frame: u64, wal_checksum: (u32, u32), deferred_fk_violations: isize, } struct Savepoint { kind: SavepointKind, /// Start offset of this savepoint in the subjournal. start_offset: AtomicU64, /// Current write offset in the subjournal. write_offset: AtomicU64, /// Bitmap of page numbers that are dirty in the savepoint. page_bitmap: RwLock, /// Database size at the start of the savepoint. /// If the database grows during the savepoint and a rollback to the savepoint is performed, /// the pages exceeding the database size at the start of the savepoint will be ignored. db_size: AtomicU32, /// We might want to rollback. /// WAL max frame at the start of the savepoint. wal_max_frame: AtomicU64, /// WAL checksum at the start of the savepoint. wal_checksum: RwLock<(u32, u32)>, /// Deferred FK counter value at the start of this savepoint. deferred_fk_violations: AtomicIsize, } impl Savepoint { fn new( kind: SavepointKind, subjournal_offset: u64, db_size: u32, wal_max_frame: u64, wal_checksum: (u32, u32), deferred_fk_violations: isize, ) -> Self { Self { kind, start_offset: AtomicU64::new(subjournal_offset), write_offset: AtomicU64::new(subjournal_offset), page_bitmap: RwLock::new(RoaringBitmap::new()), db_size: AtomicU32::new(db_size), wal_max_frame: AtomicU64::new(wal_max_frame), wal_checksum: RwLock::new(wal_checksum), deferred_fk_violations: AtomicIsize::new(deferred_fk_violations), } } pub fn add_dirty_page(&self, page_num: u32) { self.page_bitmap.write().insert(page_num); } pub fn has_dirty_page(&self, page_num: u32) -> bool { self.page_bitmap.read().contains(page_num) } fn start_offset(&self) -> u64 { self.start_offset.load(Ordering::Acquire) } fn write_offset(&self) -> u64 { self.write_offset.load(Ordering::Acquire) } fn set_write_offset(&self, offset: u64) { self.write_offset.store(offset, Ordering::Release); } fn snapshot(&self) -> SavepointSnapshot { SavepointSnapshot { kind: self.kind.clone(), start_offset: self.start_offset(), db_size: self.db_size.load(Ordering::Acquire), wal_max_frame: self.wal_max_frame.load(Ordering::Acquire), wal_checksum: *self.wal_checksum.read(), deferred_fk_violations: self.deferred_fk_violations.load(Ordering::Acquire), } } fn from_snapshot(snapshot: SavepointSnapshot) -> Self { Self { kind: snapshot.kind, start_offset: AtomicU64::new(snapshot.start_offset), write_offset: AtomicU64::new(snapshot.start_offset), page_bitmap: RwLock::new(RoaringBitmap::new()), db_size: AtomicU32::new(snapshot.db_size), wal_max_frame: AtomicU64::new(snapshot.wal_max_frame), wal_checksum: RwLock::new(snapshot.wal_checksum), deferred_fk_violations: AtomicIsize::new(snapshot.deferred_fk_violations), } } } /// The pager interface implements the persistence layer by providing access /// to pages of the database file, including caching, concurrency control, and /// transaction management. pub struct Pager { /// Source of the database pages. pub db_file: Arc, /// The write-ahead log (WAL) for the database. /// in-memory databases, ephemeral tables and ephemeral indexes do not have a WAL. pub(crate) wal: Option>, /// A page cache for the database. page_cache: Arc>, /// Buffer pool for temporary data storage. pub buffer_pool: Arc, /// I/O interface for input/output operations. pub io: Arc, /// Dirty pages as a bitmap, naturally sorted by page number. dirty_pages: Arc>, subjournal: RwLock>, savepoints: Arc>>, commit_info: RwLock, checkpoint_state: RwLock, syncing: Arc, auto_vacuum_mode: AtomicU8, /// Mutex for synchronizing database initialization to prevent race conditions init_lock: Arc>, /// The state of the current allocate page operation. allocate_page_state: RwLock, /// The state of the current allocate page1 operation. allocate_page1_state: RwLock, /// Cache page_size and reserved_space at Pager init and reuse for subsequent /// `usable_space` calls. TODO: Invalidate reserved_space when we add the functionality /// to change it. pub(crate) page_size: AtomicU32, reserved_space: AtomicU16, /// Schema cookie cache. /// /// Note that schema cookie is 32-bits, but we use 64-bit field so we can /// represent case where value is not set. schema_cookie: AtomicU64, free_page_state: RwLock, /// State machine for async cache spilling. spill_state: RwLock, /// State machine for async cacheflush operation. cacheflush_state: RwLock, /// Maximum number of pages allowed in the database. Default is 1073741823 (SQLite default). max_page_count: AtomicU32, header_ref_state: RwLock, #[cfg(not(feature = "omit_autovacuum"))] vacuum_state: RwLock, pub(crate) io_ctx: RwLock, /// encryption is an opt-in feature. we will enable it only if the flag is passed enable_encryption: AtomicBool, /// In Memory Page 1 for Empty Dbs init_page_1: Arc>, /// Sync type for durability. FullFsync uses F_FULLFSYNC on macOS (PRAGMA fullfsync). /// Only stored on Apple platforms; on others, always returns Fsync. #[cfg(target_vendor = "apple")] sync_type: AtomicFileSyncType, } assert_send_sync!(Pager); #[cfg(not(feature = "omit_autovacuum"))] pub struct VacuumState { /// State machine for [Pager::ptrmap_get] ptrmap_get_state: PtrMapGetState, /// State machine for [Pager::ptrmap_put] ptrmap_put_state: PtrMapPutState, btree_create_vacuum_full_state: BtreeCreateVacuumFullState, } #[derive(Debug, Clone)] enum AllocatePageState { Start, /// Search the trunk page for an available free list leaf. /// If none are found, there are two options: /// - If there are no more trunk pages, the freelist is empty, so allocate a new page. /// - If there are more trunk pages, use the current first trunk page as the new allocation, /// and set the next trunk page as the database's "first freelist trunk page". SearchAvailableFreeListLeaf { trunk_page: PageRef, }, /// If a freelist leaf is found, reuse it for the page allocation and remove it from the trunk page. ReuseFreelistLeaf { trunk_page: PageRef, leaf_page: PageRef, number_of_freelist_leaves: u32, }, /// If a suitable freelist leaf is not found, allocate an entirely new page. AllocateNewPage { current_db_size: u32, }, } #[derive(Clone)] enum AllocatePage1State { Start, Writing { page: PageRef }, Done, } #[derive(Debug, Clone)] enum FreePageState { Start, AddToTrunk { page: Arc }, NewTrunk { page: Arc }, } /// State machine for async cache spilling. /// Tracks progress of writing dirty pages to WAL or disk. #[derive(Debug, Default, Clone)] enum SpillState { #[default] /// No spill operation in progress Idle, /// WAL spill in progress, waiting for write completions WritingToWal { /// Pinned pages being spilled pages: Vec, /// Completions to wait for completions: Vec, }, /// Writing ephemeral tables pages directly to disk WritingToDisk { /// Pages being spilled pages: Vec, /// Completions to wait for completions: Vec, }, } enum CacheFlushStep { /// Yield to caller with pending I/O, resume with given phase Yield(CacheFlushState, IOCompletions), /// Continue immediately to next phase (no I/O wait) Continue(CacheFlushState), /// Flush complete, return accumulated completions Done(Vec), } #[derive(Default)] pub enum CacheFlushState { #[default] Init, WalPrepareStart { dirty_ids: Vec, completion: Completion, }, WalPrepareFinish { dirty_ids: Vec, completion: Completion, }, Collecting(CollectingState), WaitingForRead { state: CollectingState, page_id: usize, page: PageRef, completion: Completion, }, } #[derive(Default)] pub struct CollectingState { pub dirty_ids: Vec, pub current_idx: usize, pub collected_pages: Vec, pub completions: Vec, } impl Pager { pub fn new( db_file: Arc, wal: Option>, io: Arc, page_cache: PageCache, buffer_pool: Arc, init_lock: Arc>, init_page_1: Arc>, ) -> Result { let allocate_page1_state = if init_page_1.load().is_some() { RwLock::new(AllocatePage1State::Start) } else { RwLock::new(AllocatePage1State::Done) }; Ok(Self { db_file, wal, page_cache: Arc::new(RwLock::new(page_cache)), io, dirty_pages: Arc::new(RwLock::new(RoaringBitmap::new())), subjournal: RwLock::new(None), savepoints: Arc::new(RwLock::new(Vec::new())), commit_info: RwLock::new(CommitInfo { completions: Vec::new(), completion_group: None, state: CommitState::PrepareWal, collected_pages: Vec::new(), prepared_frames: Vec::new(), page_sources: Vec::new(), page_source_cursor: 0, }), syncing: Arc::new(AtomicBool::new(false)), checkpoint_state: RwLock::new(CheckpointState::default()), buffer_pool, auto_vacuum_mode: AtomicU8::new(AutoVacuumMode::None.into()), init_lock, allocate_page1_state, page_size: AtomicU32::new(0), // 0 means not set reserved_space: AtomicU16::new(RESERVED_SPACE_NOT_SET), schema_cookie: AtomicU64::new(Self::SCHEMA_COOKIE_NOT_SET), free_page_state: RwLock::new(FreePageState::Start), spill_state: RwLock::new(SpillState::Idle), cacheflush_state: RwLock::new(CacheFlushState::default()), allocate_page_state: RwLock::new(AllocatePageState::Start), max_page_count: AtomicU32::new(DEFAULT_MAX_PAGE_COUNT), header_ref_state: RwLock::new(HeaderRefState::Start), #[cfg(not(feature = "omit_autovacuum"))] vacuum_state: RwLock::new(VacuumState { ptrmap_get_state: PtrMapGetState::Start, ptrmap_put_state: PtrMapPutState::Start, btree_create_vacuum_full_state: BtreeCreateVacuumFullState::Start, }), io_ctx: RwLock::new(IOContext::default()), enable_encryption: AtomicBool::new(false), init_page_1, #[cfg(target_vendor = "apple")] sync_type: AtomicFileSyncType::new(FileSyncType::Fsync), }) } /// Get the sync type setting. /// On non-Apple platforms, always returns Fsync (compile-time constant). #[cfg(target_vendor = "apple")] #[inline] pub fn get_sync_type(&self) -> FileSyncType { self.sync_type.get() } /// Get the sync type setting. /// On non-Apple platforms, always returns Fsync (compile-time constant). #[cfg(not(target_vendor = "apple"))] #[inline] pub fn get_sync_type(&self) -> FileSyncType { FileSyncType::Fsync } /// Set the sync type (for PRAGMA fullfsync). Only effective on Apple platforms. #[cfg(target_vendor = "apple")] pub fn set_sync_type(&self, value: FileSyncType) { self.sync_type.set(value); } /// Set the sync type. No-op on non-Apple platforms. #[cfg(not(target_vendor = "apple"))] pub fn set_sync_type(&self, _value: FileSyncType) { // No-op: FullFsync only has effect on Apple platforms } pub fn init_page_1(&self) -> Arc> { self.init_page_1.clone() } /// Read page 1 (the database header page) using the header_ref_state state machine. /// Used by HeaderRef and HeaderRefMut to avoid duplicating the page-loading logic. fn read_header_page(&self) -> Result> { loop { let state = self.header_ref_state.read().clone(); tracing::trace!("read_header_page - {:?}", state); match state { HeaderRefState::Start => { // If db is not initialized, return the in-memory page if let Some(page1) = self.init_page_1.load_full() { return Ok(IOResult::Done(page1)); } let (page, c) = self.read_page(DatabaseHeader::PAGE_ID as i64)?; *self.header_ref_state.write() = HeaderRefState::CreateHeader { page, completion: c.clone(), }; if let Some(c) = c { io_yield_one!(c); } } HeaderRefState::CreateHeader { page, completion } => { // Check if the read failed (e.g., due to checksum/decryption error) if let Some(ref c) = completion { if let Some(err) = c.get_error() { *self.header_ref_state.write() = HeaderRefState::Start; return Err(err.into()); } } turso_assert!(page.is_loaded(), "page should be loaded"); turso_assert!( page.get().id == DatabaseHeader::PAGE_ID, "incorrect header page id" ); *self.header_ref_state.write() = HeaderRefState::Start; return Ok(IOResult::Done(page)); } } } } /// Set whether cache spilling is enabled. pub fn set_spill_enabled(&self, enabled: bool) { self.page_cache.write().set_spill_enabled(enabled); } /// Get whether cache spilling is enabled. pub fn get_spill_enabled(&self) -> bool { self.page_cache.read().is_spill_enabled() } /// Open the subjournal if not yet open. /// The subjournal is a file that is used to store the "before images" of pages for the /// current savepoint. If the savepoint is rolled back, the pages can be restored from the subjournal. /// /// Currently uses MemoryIO, but should eventually be backed by temporary on-disk files. pub fn open_subjournal(&self) -> Result<()> { if self.subjournal.read().is_some() { return Ok(()); } use crate::MemoryIO; let db_file_io = Arc::new(MemoryIO::new()); let file = db_file_io.open_file("subjournal", OpenFlags::Create, false)?; let db_file = Subjournal::new(file); *self.subjournal.write() = Some(db_file); Ok(()) } /// Write page to subjournal if the current savepoint does not currently /// contain an an entry for it. In case of a statement-level rollback, /// the page image can be restored from the subjournal. /// /// A buffer of length page_size + 4 bytes is allocated and the page id /// is written to the beginning of the buffer. The rest of the buffer is filled with the page contents. pub fn subjournal_page_if_required(&self, page: &Page) -> Result<()> { if self.subjournal.read().is_none() { return Ok(()); } let write_offset = { let savepoints = self.savepoints.read(); let Some(cur_savepoint) = savepoints.last() else { return Ok(()); }; // Skip subjournaling for pages that didn't exist when the savepoint was opened. // New pages (allocated during this statement) can be "rolled back" by simply // truncating back to the original db_size. This matches SQLite's subjRequiresPage() // which checks: p->nOrig >= pgno. let page_id_u32 = page.get().id as u32; if page_id_u32 > cur_savepoint.db_size.load(Ordering::Acquire) { return Ok(()); } if cur_savepoint.has_dirty_page(page_id_u32) { return Ok(()); } cur_savepoint.write_offset.load(Ordering::SeqCst) }; let page_id = page.get().id; let page_size = self.page_size.load(Ordering::SeqCst) as usize; let buffer = { let page_id = page.get().id as u32; let contents = page.get_contents(); let buffer = self.buffer_pool.allocate(page_size + 4); let contents_buffer = contents.as_ptr(); turso_assert!( contents_buffer.len() == page_size, "contents buffer length should be equal to page size" ); buffer.as_mut_slice()[0..4].copy_from_slice(&page_id.to_be_bytes()); buffer.as_mut_slice()[4..4 + page_size].copy_from_slice(contents_buffer); Arc::new(buffer) }; let savepoints = self.savepoints.clone(); let write_complete = { let buf_copy = buffer.clone(); Box::new(move |res: Result| { let Ok(bytes_written) = res else { return; }; let buf_copy = buf_copy.clone(); let buf_len = buf_copy.len(); turso_assert!( bytes_written == buf_len as i32, "wrote({bytes_written}) != expected({buf_len})" ); let savepoints = savepoints.read(); let cur_savepoint = savepoints.last().unwrap(); cur_savepoint.add_dirty_page(page_id as u32); cur_savepoint .write_offset .fetch_add(page_size as u64 + 4, Ordering::SeqCst); }) }; let c = Completion::new_write(write_complete); let subjournal = self.subjournal.read(); let subjournal = subjournal.as_ref().unwrap(); let c = subjournal.write_page(write_offset, page_size, buffer, c)?; turso_assert!(c.succeeded(), "memory IO should complete immediately"); Ok(()) } /// try to "acquire" ownership on the subjournal of the connection-scoped pager /// if another statement owns the subjournal - return Busy error and let the caller retry attempt later pub fn try_use_subjournal(&self) -> Result<()> { let subjournal = self.subjournal.read(); let subjournal = subjournal.as_ref().expect("subjournal must be opened"); subjournal.try_use() } /// release ownership of the subjournal /// caller must guarantee that [Self::stop_use_subjournal] is called only after successful call to the [Self::try_use_subjournal] pub fn stop_use_subjournal(&self) { let subjournal = self.subjournal.read(); let subjournal = subjournal.as_ref().expect("subjournal must be opened"); subjournal.stop_use() } /// check if subjournal is in use for some statement pub fn subjournal_in_use(&self) -> bool { let subjournal = self.subjournal.read(); let Some(subjournal) = subjournal.as_ref() else { return false; }; subjournal.in_use() } pub fn open_savepoint(&self, db_size: u32) -> Result<()> { self.open_savepoint_with_kind(SavepointKind::Statement, db_size, 0) } /// Release i.e. commit the current savepoint. This basically just means removing it. pub fn release_savepoint(&self) -> Result<()> { let mut savepoints = self.savepoints.write(); if !matches!( savepoints.last().map(|savepoint| &savepoint.kind), Some(SavepointKind::Statement) ) { return Ok(()); }; let savepoint = savepoints.pop().expect("savepoint must exist"); if let Some(parent) = savepoints.last() { parent.set_write_offset(savepoint.write_offset()); } else { let subjournal = self.subjournal.read(); let Some(subjournal) = subjournal.as_ref() else { return Ok(()); }; let c = subjournal.truncate(0)?; turso_assert!(c.succeeded(), "memory IO should complete immediately"); } Ok(()) } /// Opens a named savepoint and captures rollback metadata for the current transaction state. /// /// If `starts_transaction` is true, releasing this savepoint at the root depth commits the /// transaction. pub fn open_named_savepoint( &self, name: String, db_size: u32, starts_transaction: bool, deferred_fk_violations: isize, ) -> Result<()> { self.open_savepoint_with_kind( SavepointKind::Named { name, starts_transaction, }, db_size, deferred_fk_violations, ) } /// Releases the newest matching named savepoint and all nested savepoints opened after it. pub fn release_named_savepoint(&self, name: &str) -> Result { let mut savepoints = self.savepoints.write(); let Some(target_idx) = savepoints.iter().rposition(|savepoint| { matches!( savepoint.kind, SavepointKind::Named { name: ref savepoint_name, .. } if savepoint_name == name ) }) else { return Ok(SavepointResult::NotFound); }; let result = if matches!( savepoints[target_idx].kind, SavepointKind::Named { starts_transaction: true, .. } ) && target_idx == 0 { SavepointResult::Commit } else { SavepointResult::Release }; if matches!(result, SavepointResult::Commit) { // Defer mutation until transaction commit succeeds. If commit fails // (e.g. deferred FK violation), savepoints must remain intact. return Ok(result); } let journal_end_offset = savepoints .last() .map(|savepoint| savepoint.write_offset()) .unwrap_or(0); savepoints.truncate(target_idx); if let Some(parent) = savepoints.last() { parent.set_write_offset(journal_end_offset); } else { let subjournal = self.subjournal.read(); let Some(subjournal) = subjournal.as_ref() else { return Ok(result); }; let c = subjournal.truncate(0)?; assert!(c.succeeded(), "memory IO should complete immediately"); } Ok(result) } pub fn clear_savepoints(&self) -> Result<()> { *self.savepoints.write() = Vec::new(); let subjournal = self.subjournal.read(); let Some(subjournal) = subjournal.as_ref() else { return Ok(()); }; let c = subjournal.truncate(0)?; turso_assert!(c.succeeded(), "memory IO should complete immediately"); Ok(()) } /// Rollback to the newest savepoint. This basically just means reading the subjournal from the start offset /// of the savepoint to the end of the subjournal and restoring the page images to the page cache. pub fn rollback_to_newest_savepoint(&self) -> Result { let mut savepoints = self.savepoints.write(); if !matches!( savepoints.last().map(|savepoint| &savepoint.kind), Some(SavepointKind::Statement) ) { return Ok(false); } let savepoint = savepoints.pop().expect("savepoint must exist"); let journal_end_offset = savepoint.write_offset(); let savepoint = savepoint.snapshot(); self.rollback_to_snapshot(&savepoint, journal_end_offset)?; if let Some(parent) = savepoints.last() { parent.set_write_offset(savepoint.start_offset); } Ok(true) } /// Rollback to the newest matching named savepoint while keeping the named savepoint active. /// /// Returns deferred FK counter snapshot for the rolled-back savepoint. pub fn rollback_to_named_savepoint(&self, name: &str) -> Result> { let target = { let savepoints = self.savepoints.read(); let Some(target_idx) = savepoints.iter().rposition(|savepoint| { matches!( savepoint.kind, SavepointKind::Named { name: ref savepoint_name, .. } if savepoint_name == name ) }) else { return Ok(None); }; let journal_end_offset = savepoints .last() .map(|savepoint| savepoint.write_offset()) .unwrap_or_else(|| savepoints[target_idx].write_offset()); ( target_idx, savepoints[target_idx].snapshot(), journal_end_offset, ) }; self.rollback_to_snapshot(&target.1, target.2)?; let mut savepoints = self.savepoints.write(); let deferred_fk_violations = target.1.deferred_fk_violations; savepoints.truncate(target.0); if let Some(parent) = savepoints.last() { parent.set_write_offset(target.1.start_offset); } savepoints.push(Savepoint::from_snapshot(target.1)); Ok(Some(deferred_fk_violations)) } fn open_savepoint_with_kind( &self, kind: SavepointKind, db_size: u32, deferred_fk_violations: isize, ) -> Result<()> { let subjournal_offset = self .savepoints .read() .last() .map(|savepoint| savepoint.write_offset()) .unwrap_or(0); let (wal_max_frame, wal_checksum) = if let Some(wal) = &self.wal { (wal.get_max_frame(), wal.get_last_checksum()) } else { (0, (0, 0)) }; let savepoint = Savepoint::new( kind, subjournal_offset, db_size, wal_max_frame, wal_checksum, deferred_fk_violations, ); self.savepoints.write().push(savepoint); Ok(()) } fn rollback_to_snapshot( &self, savepoint: &SavepointSnapshot, journal_end_offset: u64, ) -> Result<()> { let subjournal = self.subjournal.read(); let Some(subjournal) = subjournal.as_ref() else { return Ok(()); }; let journal_start_offset = savepoint.start_offset; let db_size = savepoint.db_size; let mut rollback_bitset = RoaringBitmap::new(); let mut current_offset = journal_start_offset; let page_size = self.page_size.load(Ordering::SeqCst) as u64; let mut dirty_pages = self.dirty_pages.write(); while current_offset < journal_end_offset { let page_id_buffer = Arc::new(self.buffer_pool.allocate(4)); let c = subjournal.read_page_number(current_offset, page_id_buffer.clone())?; turso_assert!(c.succeeded(), "memory IO should complete immediately"); let page_id = u32::from_be_bytes(page_id_buffer.as_slice()[0..4].try_into().unwrap()); current_offset += 4; if rollback_bitset.contains(page_id) { current_offset += page_size; continue; } if page_id > db_size { dirty_pages.remove(page_id); if let Some(page) = self .page_cache .write() .get(&PageCacheKey::new(page_id as usize))? { page.clear_dirty(); page.try_unpin(); } current_offset += page_size; rollback_bitset.insert(page_id); continue; } let page_buffer = Arc::new(self.buffer_pool.allocate(page_size as usize)); let page = Arc::new(Page::new(page_id as i64)); let c = subjournal.read_page( current_offset, page_buffer, page.clone(), page_size as usize, )?; turso_assert!(c.succeeded(), "memory IO should complete immediately"); current_offset += page_size; rollback_bitset.insert(page_id); self.upsert_page_in_cache(page_id as usize, page, false)?; } let truncate_completion = subjournal.truncate(journal_start_offset)?; turso_assert!( truncate_completion.succeeded(), "memory IO should complete immediately" ); self.page_cache.write().truncate(db_size as usize)?; if let Some(wal) = &self.wal { wal.rollback(Some(RollbackTo { frame: savepoint.wal_max_frame, checksum: savepoint.wal_checksum, })); } Ok(()) } #[cfg(feature = "test_helper")] pub fn get_pending_byte() -> u32 { PENDING_BYTE.load(Ordering::Relaxed) } #[cfg(feature = "test_helper")] /// Used in testing to allow for pending byte pages in smaller dbs pub fn set_pending_byte(val: u32) { PENDING_BYTE.store(val, Ordering::Relaxed); } #[cfg(not(feature = "test_helper"))] pub const fn get_pending_byte() -> u32 { PENDING_BYTE } /// From SQLITE: https://github.com/sqlite/sqlite/blob/7e38287da43ea3b661da3d8c1f431aa907d648c9/src/btreeInt.h#L608 \ /// The database page the [PENDING_BYTE] occupies. This page is never used. pub fn pending_byte_page_id(&self) -> Option { // PENDING_BYTE_PAGE(pBt) ((Pgno)((PENDING_BYTE/((pBt)->pageSize))+1)) let page_size = self.page_size.load(Ordering::SeqCst); Self::get_pending_byte() .checked_div(page_size) .map(|val| val + 1) } /// Get the maximum page count for this database pub fn get_max_page_count(&self) -> u32 { self.max_page_count.load(Ordering::SeqCst) } /// Set the maximum page count for this database /// Returns the new maximum page count (may be clamped to current database size) pub fn set_max_page_count(&self, new_max: u32) -> crate::Result> { // Get current database size let current_page_count = return_if_io!(self.with_header(|header| header.database_size.get())); // Clamp new_max to be at least the current database size let clamped_max = std::cmp::max(new_max, current_page_count); self.max_page_count.store(clamped_max, Ordering::SeqCst); Ok(IOResult::Done(clamped_max)) } pub fn set_wal(&mut self, wal: Arc) { wal.set_io_context(self.io_ctx.read().clone()); self.wal = Some(wal); } pub fn get_auto_vacuum_mode(&self) -> AutoVacuumMode { self.auto_vacuum_mode.load(Ordering::SeqCst).into() } pub fn set_auto_vacuum_mode(&self, mode: AutoVacuumMode) { self.auto_vacuum_mode.store(mode.into(), Ordering::SeqCst); } /// Retrieves the pointer map entry for a given database page. /// `target_page_num` (1-indexed) is the page whose entry is sought. /// Returns `Ok(None)` if the page is not supposed to have a ptrmap entry (e.g. header, or a ptrmap page itself). #[cfg(not(feature = "omit_autovacuum"))] pub fn ptrmap_get(&self, target_page_num: u32) -> Result>> { loop { let ptrmap_get_state = { let vacuum_state = self.vacuum_state.read(); vacuum_state.ptrmap_get_state.clone() }; match ptrmap_get_state { PtrMapGetState::Start => { tracing::trace!("ptrmap_get(page_idx = {})", target_page_num); let configured_page_size = return_if_io!(self.with_header(|header| header.page_size)).get() as usize; if target_page_num < FIRST_PTRMAP_PAGE_NO || is_ptrmap_page(target_page_num, configured_page_size) { return Ok(IOResult::Done(None)); } let ptrmap_pg_no = get_ptrmap_page_no_for_db_page(target_page_num, configured_page_size); let offset_in_ptrmap_page = get_ptrmap_offset_in_page( target_page_num, ptrmap_pg_no, configured_page_size, )?; tracing::trace!( "ptrmap_get(page_idx = {}) = ptrmap_pg_no = {}", target_page_num, ptrmap_pg_no ); let (ptrmap_page, c) = self.read_page(ptrmap_pg_no as i64)?; self.vacuum_state.write().ptrmap_get_state = PtrMapGetState::Deserialize { ptrmap_page, offset_in_ptrmap_page, }; if let Some(c) = c { io_yield_one!(c); } } PtrMapGetState::Deserialize { ptrmap_page, offset_in_ptrmap_page, } => { turso_assert!(ptrmap_page.is_loaded(), "ptrmap_page should be loaded"); let page_content = ptrmap_page.get_contents(); let ptrmap_pg_no = page_content.id; let full_buffer_slice: &[u8] = page_content.as_ptr(); // Ptrmap pages are not page 1, so their internal offset within their buffer should be 0. // The actual page data starts at page_content.offset() within the full_buffer_slice. if ptrmap_pg_no != 1 && page_content.offset() != 0 { return Err(LimboError::Corrupt(format!( "Ptrmap page {} has unexpected internal offset {}", ptrmap_pg_no, page_content.offset() ))); } let ptrmap_page_data_slice: &[u8] = &full_buffer_slice[page_content.offset()..]; let actual_data_length = ptrmap_page_data_slice.len(); // Check if the calculated offset for the entry is within the bounds of the actual page data length. if offset_in_ptrmap_page + PTRMAP_ENTRY_SIZE > actual_data_length { return Err(LimboError::InternalError(format!( "Ptrmap offset {offset_in_ptrmap_page} + entry size {PTRMAP_ENTRY_SIZE} out of bounds for page {ptrmap_pg_no} (actual data len {actual_data_length})" ))); } let entry_slice = &ptrmap_page_data_slice [offset_in_ptrmap_page..offset_in_ptrmap_page + PTRMAP_ENTRY_SIZE]; self.vacuum_state.write().ptrmap_get_state = PtrMapGetState::Start; break match PtrmapEntry::deserialize(entry_slice) { Some(entry) => Ok(IOResult::Done(Some(entry))), None => Err(LimboError::Corrupt(format!( "Failed to deserialize ptrmap entry for page {target_page_num} from ptrmap page {ptrmap_pg_no}" ))), }; } } } } /// Writes or updates the pointer map entry for a given database page. /// `db_page_no_to_update` (1-indexed) is the page whose entry is to be set. /// `entry_type` and `parent_page_no` define the new entry. #[cfg(not(feature = "omit_autovacuum"))] pub fn ptrmap_put( &self, db_page_no_to_update: u32, entry_type: PtrmapType, parent_page_no: u32, ) -> Result> { tracing::trace!( "ptrmap_put(page_idx = {}, entry_type = {:?}, parent_page_no = {})", db_page_no_to_update, entry_type, parent_page_no ); loop { let ptrmap_put_state = { let vacuum_state = self.vacuum_state.read(); vacuum_state.ptrmap_put_state.clone() }; match ptrmap_put_state { PtrMapPutState::Start => { let page_size = return_if_io!(self.with_header(|header| header.page_size)).get() as usize; if db_page_no_to_update < FIRST_PTRMAP_PAGE_NO || is_ptrmap_page(db_page_no_to_update, page_size) { turso_soft_unreachable!("Cannot set ptrmap entry for header/ptrmap page or invalid page", { "page": db_page_no_to_update }); return Err(LimboError::InternalError(format!( "Cannot set ptrmap entry for page {db_page_no_to_update}: it's a header/ptrmap page or invalid." ))); } let ptrmap_pg_no = get_ptrmap_page_no_for_db_page(db_page_no_to_update, page_size); let offset_in_ptrmap_page = get_ptrmap_offset_in_page(db_page_no_to_update, ptrmap_pg_no, page_size)?; tracing::trace!( "ptrmap_put(page_idx = {}, entry_type = {:?}, parent_page_no = {}) = ptrmap_pg_no = {}, offset_in_ptrmap_page = {}", db_page_no_to_update, entry_type, parent_page_no, ptrmap_pg_no, offset_in_ptrmap_page ); let (ptrmap_page, c) = self.read_page(ptrmap_pg_no as i64)?; self.vacuum_state.write().ptrmap_put_state = PtrMapPutState::Deserialize { ptrmap_page, offset_in_ptrmap_page, }; if let Some(c) = c { io_yield_one!(c); } } PtrMapPutState::Deserialize { ptrmap_page, offset_in_ptrmap_page, } => { turso_assert!(ptrmap_page.is_loaded(), "page should be loaded"); self.add_dirty(&ptrmap_page)?; let page_content = ptrmap_page.get_contents(); let ptrmap_pg_no = page_content.id; let full_buffer_slice = page_content.as_ptr(); if offset_in_ptrmap_page + PTRMAP_ENTRY_SIZE > full_buffer_slice.len() { return Err(LimboError::InternalError(format!( "Ptrmap offset {} + entry size {} out of bounds for page {} (actual data len {})", offset_in_ptrmap_page, PTRMAP_ENTRY_SIZE, ptrmap_pg_no, full_buffer_slice.len() ))); } let entry = PtrmapEntry { entry_type, parent_page_no, }; entry.serialize( &mut full_buffer_slice [offset_in_ptrmap_page..offset_in_ptrmap_page + PTRMAP_ENTRY_SIZE], )?; turso_assert!( ptrmap_page.get().id == ptrmap_pg_no, "ptrmap page has unexpected number" ); self.vacuum_state.write().ptrmap_put_state = PtrMapPutState::Start; break Ok(IOResult::Done(())); } } } } /// This method is used to allocate a new root page for a btree, both for tables and indexes /// FIXME: handle no room in page cache #[instrument(skip_all, level = Level::DEBUG)] pub fn btree_create(&self, flags: &CreateBTreeFlags) -> Result> { let page_type = match flags { _ if flags.is_table() => PageType::TableLeaf, _ if flags.is_index() => PageType::IndexLeaf, _ => unreachable!("Invalid flags state"), }; #[cfg(feature = "omit_autovacuum")] { let page = return_if_io!(self.do_allocate_page(page_type, 0, BtreePageAllocMode::Any)); Ok(IOResult::Done(page.get().id as u32)) } // If autovacuum is enabled, we need to allocate a new page number that is greater than the largest root page number #[cfg(not(feature = "omit_autovacuum"))] { let auto_vacuum_mode = AutoVacuumMode::from(self.auto_vacuum_mode.load(Ordering::SeqCst)); match auto_vacuum_mode { AutoVacuumMode::None => { let page = return_if_io!(self.do_allocate_page(page_type, 0, BtreePageAllocMode::Any)); Ok(IOResult::Done(page.get().id as u32)) } AutoVacuumMode::Full => { loop { let btree_create_vacuum_full_state = { let vacuum_state = self.vacuum_state.read(); vacuum_state.btree_create_vacuum_full_state }; match btree_create_vacuum_full_state { BtreeCreateVacuumFullState::Start => { let (mut root_page_num, page_size) = return_if_io!(self .with_header(|header| { ( header.vacuum_mode_largest_root_page.get(), header.page_size.get(), ) })); turso_assert_greater_than!(root_page_num, 0, "Largest root page number cannot be 0 because that is set to 1 when creating the database with autovacuum enabled"); root_page_num += 1; turso_assert_greater_than_or_equal!( root_page_num, FIRST_PTRMAP_PAGE_NO, "can never be less than 2 because we have already incremented" ); while is_ptrmap_page(root_page_num, page_size as usize) { root_page_num += 1; } turso_assert_greater_than_or_equal!( root_page_num, 3, "root page must be >= 3 (number of the first root page)" ); self.vacuum_state.write().btree_create_vacuum_full_state = BtreeCreateVacuumFullState::AllocatePage { root_page_num }; } BtreeCreateVacuumFullState::AllocatePage { root_page_num } => { // root_page_num here is the desired root page let page = return_if_io!(self.do_allocate_page( page_type, 0, BtreePageAllocMode::Exact(root_page_num), )); let allocated_page_id = page.get().id as u32; return_if_io!(self.with_header_mut(|header| { if allocated_page_id > header.vacuum_mode_largest_root_page.get() { tracing::debug!( "Updating largest root page in header from {} to {}", header.vacuum_mode_largest_root_page.get(), allocated_page_id ); header.vacuum_mode_largest_root_page = allocated_page_id.into(); } })); if allocated_page_id != root_page_num { // TODO(Zaid): Handle swapping the allocated page with the desired root page } // TODO(Zaid): Update the header metadata to reflect the new root page number self.vacuum_state.write().btree_create_vacuum_full_state = BtreeCreateVacuumFullState::PtrMapPut { allocated_page_id }; } BtreeCreateVacuumFullState::PtrMapPut { allocated_page_id } => { // For now map allocated_page_id since we are not swapping it with root_page_num return_if_io!(self.ptrmap_put( allocated_page_id, PtrmapType::RootPage, 0, )); self.vacuum_state.write().btree_create_vacuum_full_state = BtreeCreateVacuumFullState::Start; return Ok(IOResult::Done(allocated_page_id)); } } } } AutoVacuumMode::Incremental => { return Err(LimboError::InternalError( "Incremental auto-vacuum is not supported".to_string(), )); } } } } /// Allocate a new overflow page. /// This is done when a cell overflows and new space is needed. // FIXME: handle no room in page cache pub fn allocate_overflow_page(&self) -> Result> { let page = return_if_io!(self.allocate_page()); tracing::debug!("Pager::allocate_overflow_page(id={})", page.get().id); // setup overflow page let contents = page.get_contents(); let buf = contents.as_ptr(); buf.fill(0); Ok(IOResult::Done(page)) } /// Allocate a new page to the btree via the pager. /// This marks the page as dirty and writes the page header. // FIXME: handle no room in page cache pub fn do_allocate_page( &self, page_type: PageType, offset: usize, _alloc_mode: BtreePageAllocMode, ) -> Result> { let page = return_if_io!(self.allocate_page()); #[cfg(debug_assertions)] turso_assert_eq!( offset, page.get_contents().offset(), "offset doesn't match computed offset for page" ); btree_init_page(&page, page_type, offset, self.usable_space()); tracing::debug!( "do_allocate_page(id={}, page_type={:?})", page.get().id, page.get_contents().page_type().ok() ); Ok(IOResult::Done(page)) } /// The "usable size" of a database page is the page size specified by the 2-byte integer at offset 16 /// in the header, minus the "reserved" space size recorded in the 1-byte integer at offset 20 in the header. /// The usable size of a page might be an odd number. However, the usable size is not allowed to be less than 480. /// In other words, if the page size is 512, then the reserved space size cannot exceed 32. pub fn usable_space(&self) -> usize { let page_size = self.get_page_size().unwrap_or_else(|| { let size = self .io .block(|| self.with_header(|header| header.page_size)) .unwrap_or_default(); self.page_size.store(size.get(), Ordering::SeqCst); size }); let reserved_space = self.get_reserved_space().unwrap_or_else(|| { let space = if self.db_initialized() { self.io .block(|| self.with_header(|header| header.reserved_space)) .unwrap_or_default() } else { // Before page 1 is allocated, the in-memory bootstrap header may still carry // reserved_space=0. Use IOContext so checksum/encryption-required tail bytes are // respected when computing usable space for first writes. self.io_ctx.read().get_reserved_space_bytes() }; self.set_reserved_space(space); space }); (page_size.get() as usize) - (reserved_space as usize) } pub fn db_initialized(&self) -> bool { self.init_page_1.load().is_none() } /// Set the initial page size for the database. Should only be called before the database is initialized pub fn set_initial_page_size(&self, size: PageSize) -> Result<()> { turso_assert!(!self.db_initialized()); let IOResult::Done(_) = self.with_header_mut(|header| { header.page_size = size; })? else { panic!("DB should not be initialized and should not do any IO"); }; self.page_size.store(size.get(), Ordering::SeqCst); // Clear dirty pages since this is pre-initialization setup, not a real write transaction. // with_header_mut marks page 1 dirty as a side effect, but no transaction is active. self.dirty_pages.write().clear(); Ok(()) } /// Get the current page size. Returns None if not set yet. pub fn get_page_size(&self) -> Option { let value = self.page_size.load(Ordering::SeqCst); if value == 0 { None } else { PageSize::new(value) } } /// Get the current page size, panicking if not set. pub fn get_page_size_unchecked(&self) -> PageSize { let value = self.page_size.load(Ordering::SeqCst); turso_assert_ne!(value, 0); PageSize::new(value).expect("invalid page size stored") } /// Set the page size. Used internally when page size is determined. pub fn set_page_size(&self, size: PageSize) { self.page_size.store(size.get(), Ordering::SeqCst); } /// Get the current reserved space. Returns None if not set yet. pub fn get_reserved_space(&self) -> Option { let value = self.reserved_space.load(Ordering::SeqCst); if value == RESERVED_SPACE_NOT_SET { None } else { Some(value as u8) } } /// Set the reserved space. Must fit in u8. pub fn set_reserved_space(&self, space: u8) { self.reserved_space.store(space as u16, Ordering::SeqCst); } /// Schema cookie sentinel value that represents value not set. const SCHEMA_COOKIE_NOT_SET: u64 = u64::MAX; /// Get the cached schema cookie. Returns None if not set yet. pub fn get_schema_cookie_cached(&self) -> Option { let value = self.schema_cookie.load(Ordering::SeqCst); if value == Self::SCHEMA_COOKIE_NOT_SET { None } else { Some(value as u32) } } /// Set the schema cookie cache. pub fn set_schema_cookie(&self, cookie: Option) { let value = cookie.map_or(Self::SCHEMA_COOKIE_NOT_SET, |v| v as u64); self.schema_cookie.store(value, Ordering::SeqCst); } /// Get the schema cookie, using the cached value if available to avoid reading page 1. pub fn get_schema_cookie(&self) -> Result> { // Try to use cached value first if let Some(cookie) = self.get_schema_cookie_cached() { return Ok(IOResult::Done(cookie)); } // If not cached, read from header and cache it self.with_header(|header| header.schema_cookie.get()) } #[inline(always)] #[instrument(skip_all, level = Level::DEBUG)] pub fn begin_read_tx(&self) -> Result<()> { let Some(wal) = self.wal.as_ref() else { return Ok(()); }; let changed = wal.begin_read_tx()?; if changed { // Someone else changed the database -> assume our page cache is invalid (this is default SQLite behavior, we can probably do better with more granular invalidation) self.clear_page_cache(false); // Invalidate cached schema cookie to force re-read on next access self.set_schema_cookie(None); } Ok(()) } /// MVCC-only: refresh connection-private WAL change counters without starting a read tx and invalidate cache if needed. pub fn mvcc_refresh_if_db_changed(&self) { let Some(wal) = self.wal.as_ref() else { return; }; if wal.mvcc_refresh_if_db_changed() { // Prevents stale page cache reads after MVCC checkpoints update the DB file. self.clear_page_cache(false); self.set_schema_cookie(None); } } #[instrument(skip_all, level = Level::DEBUG)] pub fn maybe_allocate_page1(&self) -> Result> { if !self.db_initialized() { if let Some(_lock) = self.init_lock.try_lock() { return Ok(self.allocate_page1()?.map(|_| ())); } // Give a chance for the allocation to happen elsewhere io_yield_one!(Completion::new_yield()); } Ok(IOResult::Done(())) } #[inline(always)] #[instrument(skip_all, level = Level::DEBUG)] pub fn begin_write_tx(&self) -> Result> { // TODO(Diego): The only possibly allocate page1 here is because OpenEphemeral needs a write transaction // we should have a unique API to begin transactions, something like sqlite3BtreeBeginTrans return_if_io!(self.maybe_allocate_page1()); let Some(wal) = self.wal.as_ref() else { return Ok(IOResult::Done(())); }; Ok(IOResult::Done(wal.begin_write_tx()?)) } /// commit dirty pages from current transaction in WAL mode if this is not nested statement (for nested statements, parent will do the commit) /// if update_transaction_state set to false, then [Connection::transaction_state] left unchanged /// if update_transaction_state set to true, then [Connection::transaction_state] reset to [TransactionState::None] in case when method completes without error #[instrument(skip_all, level = Level::DEBUG)] pub fn commit_tx( &self, connection: &Connection, update_transaction_state: bool, ) -> Result> { if connection.is_nested_stmt() { // Parent statement will handle the transaction commit. return Ok(IOResult::Done(())); } let Some(wal) = self.wal.as_ref() else { // TODO: Unsure what the semantics of "end_tx" is for in-memory databases, ephemeral tables and ephemeral indexes. self.clear_savepoints()?; return Ok(IOResult::Done(())); }; let complete_commit = || { if update_transaction_state { connection.set_tx_state(TransactionState::None); } self.commit_dirty_pages_end(); }; loop { let commit_state = self.commit_info.read().state; tracing::debug!("commit_state: {:?}", commit_state); // we separate auto-checkpoint from the commit in order for checkpoint to be able to backfill WAL till the end // (including new frames from current transaction) // otherwise, we will be unable to do WAL restart match commit_state { CommitState::AutoCheckpoint => { let checkpoint_result = self.checkpoint( CheckpointMode::Passive { upper_bound_inclusive: None, }, connection.get_sync_mode(), false, ); match checkpoint_result { Ok(IOResult::IO(io)) => return Ok(IOResult::IO(io)), Ok(IOResult::Done(_)) => complete_commit(), Err(err) => { tracing::info!("auto-checkpoint failed: {err}"); complete_commit(); self.cleanup_after_auto_checkpoint_failure(); } } self.clear_savepoints()?; return Ok(IOResult::Done(())); } _ => { return_if_io!(self.commit_dirty_pages( connection.is_wal_auto_checkpoint_disabled(), connection.get_sync_mode(), connection.get_data_sync_retry(), )); let schema_did_change = match connection.get_tx_state() { TransactionState::Write { schema_did_change } => schema_did_change, _ => false, }; wal.end_write_tx(); wal.end_read_tx(); // we do not set TransactionState::None here - because caller can decide that nothing should be done for this connection // and skip next calls of the commit_tx methods after IO tracing::debug!("commit_tx: schema_did_change={schema_did_change}"); if schema_did_change { let schema = connection.schema.read().clone(); connection.db.update_schema_if_newer(schema); } if self.commit_info.read().state != CommitState::AutoCheckpoint { complete_commit(); self.clear_savepoints()?; return Ok(IOResult::Done(())); } } } } } #[instrument(skip_all, level = Level::DEBUG)] pub fn rollback_tx(&self, connection: &Connection) { if connection.is_nested_stmt() { // Parent statement will handle the transaction rollback. return; } let Some(wal) = self.wal.as_ref() else { // TODO: Unsure what the semantics of "end_tx" is for in-memory databases, ephemeral tables and ephemeral indexes. return; }; let (is_write, schema_did_change) = match connection.get_tx_state() { TransactionState::Write { schema_did_change } => (true, schema_did_change), _ => (false, false), }; tracing::trace!("rollback_tx(schema_did_change={})", schema_did_change); if is_write { self.clear_savepoints() .expect("in practice, clear_savepoints() should never fail as it uses memory IO"); // IMPORTANT: rollback() must be called BEFORE end_write_tx() releases the write_lock. // Otherwise, another thread could commit new frames to frame_cache between // end_write_tx() and rollback(), and rollback() would incorrectly remove them. self.rollback(schema_did_change, connection, is_write); wal.end_write_tx(); } else { self.rollback(schema_did_change, connection, is_write); } wal.end_read_tx(); } #[instrument(skip_all, level = Level::DEBUG)] pub fn end_read_tx(&self) { let Some(wal) = self.wal.as_ref() else { return; }; wal.end_read_tx(); } /// End just the write transaction on the WAL, without affecting the read lock. pub fn end_write_tx(&self) { let Some(wal) = self.wal.as_ref() else { return; }; wal.end_write_tx(); } /// Returns true if this pager's WAL currently holds a read lock. pub fn holds_read_lock(&self) -> bool { let Some(wal) = self.wal.as_ref() else { return false; }; wal.holds_read_lock() } pub fn holds_write_lock(&self) -> bool { let Some(wal) = self.wal.as_ref() else { return false; }; wal.holds_write_lock() } /// Rollback and clean up an attached database pager's transaction. /// Unlike rollback_tx, this doesn't modify connection-level state. pub fn rollback_attached(&self) { let Some(wal) = self.wal.as_ref() else { return; }; let is_write = wal.holds_write_lock(); if is_write { self.clear_savepoints() .expect("clear_savepoints should not fail for attached DB"); // Clear dirty pages and page cache before releasing the write lock self.clear_page_cache(true); self.dirty_pages.write().clear(); self.reset_internal_states(); self.set_schema_cookie(None); wal.rollback(None); wal.end_write_tx(); } else { // For read-only transactions, pager state machines (e.g. header_ref_state) // can be left in intermediate states if an IO completion was aborted. // Reset them so the next query on this attached DB starts clean. self.reset_internal_states(); } if wal.holds_read_lock() { wal.end_read_tx(); } } /// Reads a page from disk (either WAL or DB file) bypassing page-cache #[tracing::instrument(skip_all, level = Level::DEBUG)] pub fn read_page_no_cache( &self, page_idx: i64, frame_watermark: Option, allow_empty_read: bool, ) -> Result<(PageRef, Completion)> { turso_assert_greater_than_or_equal!(page_idx, 0); tracing::debug!("read_page_no_cache(page_idx = {})", page_idx); let page = Arc::new(Page::new(page_idx)); let io_ctx = self.io_ctx.read(); let Some(wal) = self.wal.as_ref() else { turso_assert!( matches!(frame_watermark, Some(0) | None), "frame_watermark must be either None or Some(0) because DB has no WAL and read with other watermark is invalid" ); page.set_locked(); let c = self.begin_read_disk_page( page_idx as usize, page.clone(), allow_empty_read, &io_ctx, )?; return Ok((page, c)); }; if let Some(frame_id) = wal.find_frame(page_idx as u64, frame_watermark)? { let c = wal.read_frame(frame_id, page.clone(), self.buffer_pool.clone())?; // TODO(pere) should probably first insert to page cache, and if successful, // read frame or page return Ok((page, c)); } let c = self.begin_read_disk_page(page_idx as usize, page.clone(), allow_empty_read, &io_ctx)?; Ok((page, c)) } /// Reads a page from the database. #[tracing::instrument(skip_all, level = Level::TRACE)] pub fn read_page(&self, page_idx: i64) -> Result<(PageRef, Option)> { turso_assert_greater_than_or_equal!(page_idx, 0, "pages in pager should be positive, negative might indicate unallocated pages from mvcc or any other nasty bug"); tracing::debug!("read_page(page_idx = {})", page_idx); // First check if page is in cache { let mut page_cache = self.page_cache.write(); let page_key = PageCacheKey::new(page_idx as usize); if let Some(page) = page_cache.get(&page_key)? { turso_assert!( page_idx as usize == page.get().id, "attempted to read page but got different page", { "expected_page": page_idx, "actual_page": page.get().id } ); return Ok((page, None)); } } tracing::debug!("read_page(page_idx = {page_idx}) = reading page from disk"); // Page not in cache, read from disk let (page, c) = self.read_page_no_cache(page_idx, None, false)?; loop { match self.cache_insert(page_idx as usize, page.clone())? { IOResult::Done(()) => { return Ok((page, Some(c))); } IOResult::IO(IOCompletions::Single(spill_c)) => { // NOTE: Because `cache_insert` can return completions as *multiple* different states, we cannot // simply create a new CompletionGroup and return it here without inserting the // page into the cache. In order to do this, we would need to make read_page // re-entrant so it continues to call cache_insert and have every caller // propogate the IOResult. For now, we will wait syncronously for spilling IO // on cache insertion on read_page. self.io.wait_for_completion(spill_c)?; } } } } fn begin_read_disk_page( &self, page_idx: usize, page: PageRef, allow_empty_read: bool, io_ctx: &IOContext, ) -> Result { sqlite3_ondisk::begin_read_page( self.db_file.as_ref(), self.buffer_pool.clone(), page, page_idx, allow_empty_read, io_ctx, ) } /// Insert a page into the cache, with spilling support. /// This handles cache full conditions by spilling dirty pages and retrying. fn cache_insert(&self, page_idx: usize, page: PageRef) -> Result> { { let mut page_cache = self.page_cache.write(); let page_key = PageCacheKey::new(page_idx); match page_cache.insert(page_key, page.clone()) { Ok(_) => return Ok(IOResult::Done(())), Err(CacheError::KeyExists) => { unreachable!("Page should not exist in cache after get() miss"); } Err(CacheError::Full) => { // Fall through to spilling } Err(e) => return Err(e.into()), } } match self.try_spill_dirty_pages()? { IOResult::Done(true) => { let mut page_cache = self.page_cache.write(); let page_key = PageCacheKey::new(page_idx); match page_cache.insert(page_key, page) { Ok(_) => Ok(IOResult::Done(())), Err(CacheError::KeyExists) => Ok(IOResult::Done(())), Err(e) => Err(e.into()), } } IOResult::Done(false) => Err(LimboError::Busy), IOResult::IO(c) => Ok(IOResult::IO(c)), } } // Get a page from the cache, if it exists. pub fn cache_get(&self, page_idx: usize) -> Result> { tracing::trace!("read_page(page_idx = {})", page_idx); let mut page_cache = self.page_cache.write(); let page_key = PageCacheKey::new(page_idx); page_cache.get(&page_key) } /// Get a page from cache only if it matches the target frame pub fn cache_get_for_checkpoint( &self, page_idx: usize, target_frame: u64, seq: u32, ) -> Result> { let mut page_cache = self.page_cache.write(); let page_key = PageCacheKey::new(page_idx); let page = page_cache.get(&page_key)?.and_then(|page| { if page.is_valid_for_checkpoint(target_frame, seq) { tracing::debug!( "cache_get_for_checkpoint: page {page_idx} frame {target_frame} is valid", ); Some(page) } else { tracing::trace!( "cache_get_for_checkpoint: page {} has frame/tag {:?}: (dirty={}), need frame {} and seq {seq}", page_idx, page.wal_tag_pair(), page.is_dirty(), target_frame ); None } }); Ok(page) } /// Changes the size of the page cache. pub fn change_page_cache_size(&self, capacity: usize) -> Result { let mut page_cache = self.page_cache.write(); Ok(page_cache.resize(capacity)) } pub fn add_dirty(&self, page: &Page) -> Result<()> { turso_assert!( page.is_loaded(), "page must be loaded in add_dirty() so its contents can be subjournaled", { "page_id": page.get().id } ); self.subjournal_page_if_required(page)?; let mut dirty_pages = self.dirty_pages.write(); dirty_pages.insert(page.get().id as u32); // Notify cache before marking dirty (page was evictable, now it won't be) // Only notify if page wasn't already dirty if !page.is_dirty() { let key = PageCacheKey::new(page.get().id); self.page_cache.write().notify_page_dirty(key); } page.set_dirty(); Ok(()) } pub fn wal_state(&self) -> Result { let Some(wal) = self.wal.as_ref() else { turso_soft_unreachable!("wal_state() called on database without WAL"); return Err(LimboError::InternalError( "wal_state() called on database without WAL".to_string(), )); }; Ok(WalState { checkpoint_seq_no: wal.get_checkpoint_seq(), max_frame: wal.get_max_frame(), }) } /// Flush all dirty pages to disk (async/re-entrant). /// Unlike commit_dirty_pages, this function does not commit, checkpoint nor sync the WAL/Database. #[instrument(skip_all, level = Level::INFO)] pub fn cacheflush(&self) -> Result>> { let wal = self .wal .as_ref() .ok_or_else(|| LimboError::InternalError("cacheflush() called without WAL".into()))?; let page_sz = self.get_page_size().unwrap_or_default(); loop { let phase = std::mem::take(&mut *self.cacheflush_state.write()); match self.cacheflush_step(wal, page_sz, phase)? { CacheFlushStep::Yield(next_phase, io) => { *self.cacheflush_state.write() = next_phase; return Ok(IOResult::IO(io)); } CacheFlushStep::Continue(next_phase) => { *self.cacheflush_state.write() = next_phase; } CacheFlushStep::Done(completions) => { *self.cacheflush_state.write() = CacheFlushState::Init; return Ok(IOResult::Done(completions)); } } } } /// Executes one step of the cache flush state machine. #[inline] fn cacheflush_step( &self, wal: &Arc, page_sz: PageSize, phase: CacheFlushState, ) -> Result { match phase { CacheFlushState::Init => self.cacheflush_init(wal, page_sz), CacheFlushState::WalPrepareStart { dirty_ids, completion, } => self.cacheflush_wal_prepare_start(wal, dirty_ids, completion), CacheFlushState::WalPrepareFinish { dirty_ids, completion, } => self.cacheflush_wal_prepare_finish(dirty_ids, completion), CacheFlushState::Collecting(state) => self.cacheflush_collect(wal, page_sz, state), CacheFlushState::WaitingForRead { state, page_id, page, completion, } => self.cacheflush_handle_read(wal, page_sz, state, page_id, page, completion), } } /// Init phase: gather dirty page IDs and begin WAL preparation. fn cacheflush_init(&self, wal: &Arc, page_sz: PageSize) -> Result { let dirty_ids: Vec = self.dirty_pages.read().iter().map(|x| x as usize).collect(); if dirty_ids.is_empty() { return Ok(CacheFlushStep::Done(Vec::new())); } // Start WAL preparation match wal.prepare_wal_start(page_sz)? { Some(completion) => Ok(CacheFlushStep::Yield( CacheFlushState::WalPrepareStart { dirty_ids, completion: completion.clone(), }, IOCompletions::Single(completion), )), None => { // No async prep needed, go straight to finish let completion = wal.prepare_wal_finish(self.get_sync_type())?; Ok(CacheFlushStep::Yield( CacheFlushState::WalPrepareFinish { dirty_ids, completion: completion.clone(), }, IOCompletions::Single(completion), )) } } } #[inline] /// Wait for WAL prepare_start, then call prepare_finish. fn cacheflush_wal_prepare_start( &self, wal: &Arc, dirty_ids: Vec, completion: Completion, ) -> Result { if !completion.succeeded() { return Ok(CacheFlushStep::Yield( CacheFlushState::WalPrepareStart { dirty_ids, completion: completion.clone(), }, IOCompletions::Single(completion), )); } let finish_completion = wal.prepare_wal_finish(self.get_sync_type())?; Ok(CacheFlushStep::Yield( CacheFlushState::WalPrepareFinish { dirty_ids, completion: finish_completion.clone(), }, IOCompletions::Single(finish_completion), )) } #[inline] /// Wait for WAL prepare_finish, then start collecting pages. fn cacheflush_wal_prepare_finish( &self, dirty_ids: Vec, completion: Completion, ) -> Result { if !completion.succeeded() { return Ok(CacheFlushStep::Yield( CacheFlushState::WalPrepareFinish { dirty_ids, completion: completion.clone(), }, IOCompletions::Single(completion), )); } Ok(CacheFlushStep::Continue(CacheFlushState::Collecting( CollectingState { dirty_ids, current_idx: 0, collected_pages: Vec::new(), completions: Vec::new(), }, ))) } #[inline] /// Main collection loop: fetch pages from cache, handle evictions, write batches. fn cacheflush_collect( &self, wal: &Arc, page_sz: PageSize, mut state: CollectingState, ) -> Result { while state.current_idx < state.dirty_ids.len() { let page_id = state.dirty_ids[state.current_idx]; let cache_result = self.page_cache.write().get(&PageCacheKey::new(page_id))?; match cache_result { Some(page) => { trace!( "cacheflush(page={}, page_type={:?})", page_id, page.get_contents().page_type().ok() ); state.collected_pages.push(page); state.current_idx += 1; } None => { // Page evicted, need async read from WAL trace!("cacheflush: page {} evicted, reading from WAL", page_id); let (page, completion) = self.read_page_no_cache(page_id as i64, None, false)?; if !completion.succeeded() { return Ok(CacheFlushStep::Yield( CacheFlushState::WaitingForRead { state, page_id, page, completion: completion.clone(), }, IOCompletions::Single(completion), )); } // Sync read completed immediately trace!( "cacheflush(page={}, page_type={:?}) [re-read sync]", page_id, page.get_contents().page_type().ok() ); state.collected_pages.push(page); state.current_idx += 1; } } if Self::should_flush_batch(&state) { self.flush_page_batch(wal, page_sz, &mut state)?; } } // All pages collected and written Ok(CacheFlushStep::Done(state.completions)) } /// Handle completion of async page read for evicted page. fn cacheflush_handle_read( &self, wal: &Arc, page_sz: PageSize, mut state: CollectingState, page_id: usize, page: PageRef, completion: Completion, ) -> Result { if !completion.succeeded() { return Ok(CacheFlushStep::Yield( CacheFlushState::WaitingForRead { state, page_id, page, completion: completion.clone(), }, IOCompletions::Single(completion), )); } trace!( "cacheflush(page={}, page_type={:?}) [re-read complete]", page_id, page.get_contents().page_type().ok() ); state.collected_pages.push(page); state.current_idx += 1; if Self::should_flush_batch(&state) { self.flush_page_batch(wal, page_sz, &mut state)?; } Ok(CacheFlushStep::Continue(CacheFlushState::Collecting(state))) } #[inline] fn should_flush_batch(state: &CollectingState) -> bool { let at_capacity = state.collected_pages.len() == IOV_MAX; let at_end = state.current_idx >= state.dirty_ids.len(); !state.collected_pages.is_empty() && (at_capacity || at_end) } /// Writes accumulated pages to WAL as a single vectored append. #[inline] fn flush_page_batch( &self, wal: &Arc, page_sz: PageSize, state: &mut CollectingState, ) -> Result<()> { let pages = std::mem::take(&mut state.collected_pages); // Mark pages as write-pending to detect concurrent modifications for page in &pages { page.set_write_pending(); } match wal.append_frames_vectored(pages, page_sz) { Ok(completion) => { state.completions.push(completion); Ok(()) } Err(e) => { self.io.cancel(&state.completions)?; self.io.drain()?; Err(e) } } } /// Attempt to spill dirty pages from the cache to make room for new pages. /// This is called when the cache reaches its spill threshold. /// /// For databases with a WAL: write only spillable dirty pages to WAL, /// then mark them as spilled so they can be evicted even while dirty. /// For ephemeral tables: writes pages directly to the temp database file. #[instrument(skip_all, level = Level::DEBUG)] pub fn try_spill_dirty_pages(&self) -> Result> { let state = self.spill_state.read().clone(); match state { SpillState::Idle => { // Check if spilling is needed let spill_result = { let cache = self.page_cache.read(); cache.check_spill(IOV_MAX) }; match spill_result { SpillResult::NotNeeded | SpillResult::Disabled => { return Ok(IOResult::Done(false)); } SpillResult::CacheFull => { tracing::debug!("try_spill_dirty_pages: cache full, no spillable pages"); return Ok(IOResult::Done(false)); } SpillResult::PagesToSpill(pages) => { if pages.is_empty() { return Ok(IOResult::Done(false)); } let page_count = pages.len(); tracing::debug!("try_spill_dirty_pages: spilling {} pages", page_count); if let Some(wal) = self.wal.as_ref() { let page_sz = self.get_page_size().unwrap_or_default(); // Ensure WAL is initialized. Most of the time this is a no-op. let prepare = wal.prepare_wal_start(page_sz)?; if let Some(c) = prepare { self.io.wait_for_completion(c)?; let c = wal.prepare_wal_finish(self.get_sync_type())?; self.io.wait_for_completion(c)?; } let wal_pages: Vec = pages .iter() .map(|p| { // Set write_pending on all pages before WAL write so callback can // detect mid-write modifications. p.set_write_pending(); p.to_page() }) .collect(); let c = wal.append_frames_vectored(wal_pages, page_sz)?; if c.succeeded() { // Synchronous completion, WAL tags already set by callback. { let mut cache = self.page_cache.write(); for page in &pages { if page.has_wal_tag() { let key = PageCacheKey::new(page.get().id); cache.notify_page_spilled(key); page.set_spilled(); } } } *self.spill_state.write() = SpillState::Idle; return Ok(IOResult::Done(true)); } *self.spill_state.write() = SpillState::WritingToWal { pages, completions: vec![c.clone()], }; io_yield_one!(c); } else { let mut group = CompletionGroup::new(|_| {}); // Ephemeral table case: write directly to temp file for page in &pages { page.set_write_pending(); } let completions = self.spill_pages_to_disk(&pages)?; if completions.is_empty() { self.finish_ephemeral_spill(&pages); return Ok(IOResult::Done(true)); } for completion in &completions { group.add(completion); } *self.spill_state.write() = SpillState::WritingToDisk { pages, completions: completions.clone(), }; io_yield_one!(group.build()); } } } } SpillState::WritingToWal { pages, completions } => { for c in &completions { if !c.succeeded() { io_yield_one!(c.clone()); } } // All I/O complete, pages are now in WAL. // Mark spilled pages so they can be evicted while dirty. // Only do so if page wasn't modified since write started (each page has valid wal_tag). let mut spilled_count = 0; { let mut cache = self.page_cache.write(); for page in &pages { if page.has_wal_tag() { let key = PageCacheKey::new(page.get().id); cache.notify_page_spilled(key); page.set_spilled(); spilled_count += 1; } else { // Page was modified during write, it will need to be re-spilled tracing::debug!( "try_spill_dirty_pages: page {} modified during write, not marking as spilled", page.get().id ); } } } if spilled_count == 0 && !pages.is_empty() { tracing::warn!( "try_spill_dirty_pages: no pages marked as spilled out of {}, all were modified during write", pages.len() ); } *self.spill_state.write() = SpillState::Idle; trace!( "try_spill_dirty_pages: successfully spilled {} / {} pages to WAL", spilled_count, pages.len(), ); return Ok(IOResult::Done(true)); } SpillState::WritingToDisk { pages, completions } => { let all_done = completions.iter().all(|c| c.succeeded()); if !all_done { for c in &completions { if !c.succeeded() { io_yield_one!(c.clone()); } } } // All I/O complete, finish ephemeral spill self.finish_ephemeral_spill(&pages); *self.spill_state.write() = SpillState::Idle; trace!( "try_spill_dirty_pages: successfully spilled {} pages to disk", pages.len() ); return Ok(IOResult::Done(true)); } } } /// Wait for any in-flight spill writes to finish. /// This prevents publishing WAL metadata that references frames that are not yet durable. fn wait_for_spill_completions(&self) -> Result> { loop { let state = self.spill_state.read().clone(); if matches!(state, SpillState::Idle) { return Ok(IOResult::Done(())); } match self.try_spill_dirty_pages()? { IOResult::Done(_) => continue, IOResult::IO(c) => return Ok(IOResult::IO(c)), } } } /// Finish a spill operation for ephemeral tables fn finish_ephemeral_spill(&self, pages: &[PinGuard]) { for page in pages { let tag = page.get().wal_tag.load(Ordering::Acquire); // wal tag is set to TAG_UNSET when adding to dirty_pages, meaning that this // page was dirtied after the spill started, so we don't clear the dirty flag in that case if tag != TAG_UNSET { page.clear_dirty(); } } } /// Write a set of pages directly to the database file (for ephemeral tables without WAL). /// This is used by try_spill_dirty_pages for ephemeral tables/indexes. fn spill_pages_to_disk(&self, pages: &[PinGuard]) -> Result> { let mut completions: Vec = Vec::with_capacity(pages.len()); for page in pages { match begin_write_btree_page(self, &page.to_page()) { Ok(c) => completions.push(c), Err(e) => { self.io.cancel(&completions)?; self.io.drain()?; return Err(e); } } } Ok(completions) } /// Check if the cache needs spilling and attempt to spill if necessary. /// This should be called before inserting new pages into the cache. pub fn ensure_cache_space(&self) -> Result> { let needs_spill = { let cache = self.page_cache.read(); cache.needs_spill() }; if needs_spill { match self.try_spill_dirty_pages()? { IOResult::Done(spilled) => { if spilled { // After spilling, try to evict clean pages to make room in the cache let mut cache = self.page_cache.write(); if let Err(e) = cache.make_room_for(1) { // Cache is completely full with unevictable pages tracing::error!( "ensure_cache_space: {e} cache full, could not make room" ); return Err(LimboError::CacheError(CacheError::Full)); } } } IOResult::IO(completion) => { return Ok(IOResult::IO(completion)); } } } Ok(IOResult::Done(())) } /// Flush all dirty pages to disk. /// In the base case, it will write the dirty pages to the WAL and then fsync the WAL. /// If the WAL size is over the checkpoint threshold, it will checkpoint the WAL to /// the database file and then fsync the database file. #[instrument(skip_all, level = Level::DEBUG)] pub fn commit_dirty_pages( &self, wal_auto_checkpoint_disabled: bool, sync_mode: SyncMode, data_sync_retry: bool, ) -> Result> { { let mut commit_info = self.commit_info.write(); if commit_info.state == CommitState::PrepareWal { commit_info.reset(); } } // Wait for spill writes before publishing frames if let IOResult::IO(c) = self.wait_for_spill_completions()? { return Ok(IOResult::IO(c)); } let result = self.commit_dirty_pages_inner(wal_auto_checkpoint_disabled, sync_mode, data_sync_retry); if result.is_err() { self.commit_info.write().reset(); } result } pub fn commit_dirty_pages_end(&self) { self.commit_info.write().reset(); } #[instrument(skip_all, level = Level::DEBUG)] fn commit_dirty_pages_inner( &self, wal_auto_checkpoint_disabled: bool, sync_mode: SyncMode, data_sync_retry: bool, ) -> Result> { let Some(wal) = self.wal.as_ref() else { turso_soft_unreachable!("commit_dirty_pages() called without WAL"); return Err(LimboError::InternalError( "commit_dirty_pages() called without WAL".into(), )); }; loop { let state = self.commit_info.read().state; trace!(?state); match state { CommitState::PrepareWal => { let page_sz = self.get_page_size_unchecked(); let c = wal.prepare_wal_start(page_sz)?; let Some(c) = c else { self.commit_info.write().state = CommitState::GetDbSize; continue; }; self.commit_info.write().state = CommitState::PrepareWalSync; if !c.succeeded() { io_yield_one!(c); } } CommitState::PrepareWalSync => { let c = wal.prepare_wal_finish(self.get_sync_type())?; self.commit_info.write().state = CommitState::GetDbSize; if !c.succeeded() { io_yield_one!(c); } } CommitState::GetDbSize => { let db_size = return_if_io!(self.with_header(|h| h.database_size)); self.commit_info.write().state = CommitState::ScanAndIssueReads { db_size: db_size.get(), }; } CommitState::ScanAndIssueReads { db_size } => { let mut commit_info = self.commit_info.write(); let dirty_pages = self.dirty_pages.read(); if dirty_pages.is_empty() { return Ok(IOResult::Done(())); } commit_info.initialize(dirty_pages.len() as usize); let mut cache = self.page_cache.write(); for page_id in dirty_pages.iter() { let page_id = page_id as usize; let page_key = PageCacheKey::new(page_id); if cache.peek(&page_key, false).is_some() { commit_info.page_sources.push(PageSource::Cached(page_id)); } else { let (page, completion) = self.read_page_no_cache(page_id as i64, None, false)?; commit_info.page_sources.push(PageSource::Evicted(page)); if !completion.finished() { commit_info.completions.push(completion); } } } drop(cache); drop(dirty_pages); if !commit_info.completions.is_empty() { commit_info.state = CommitState::WaitBatchedReads { db_size }; drop(commit_info); io_yield_one!(self.commit_completion()); } commit_info.state = CommitState::PrepareFrames { db_size }; } CommitState::WaitBatchedReads { db_size } => { let all_done = self .commit_info .read() .completions .iter() .all(|c| c.finished()); if !all_done { io_yield_one!(self.commit_completion()); } // Check for any read errors let mut commit_info = self.commit_info.write(); let failed = commit_info .completions .iter() .find(|c| !c.succeeded()) .cloned(); if let Some(_failed) = failed { return Err(LimboError::CompletionError(CompletionError::IOError( std::io::ErrorKind::Other, "read", ))); } // All reads complete and successful, proceed to frame preparation commit_info.completions.clear(); commit_info.completion_group = None; commit_info.state = CommitState::PrepareFrames { db_size }; } CommitState::PrepareFrames { db_size } => { let page_sz = self.get_page_size_unchecked(); let mut commit_info = self.commit_info.write(); let mut cache = self.page_cache.write(); 'inner: loop { let cursor = commit_info.page_source_cursor; if cursor >= commit_info.page_sources.len() { break 'inner; } let total = commit_info.page_sources.len(); let is_last = cursor + 1 >= total; // Linear consumption, no lookup required let page = match &commit_info.page_sources[cursor] { PageSource::Cached(page_id) => { let page_key = PageCacheKey::new(*page_id); cache .get(&page_key)? .expect("page evicted between scan and prepare") } PageSource::Evicted(page) => page.clone(), }; commit_info.page_source_cursor += 1; commit_info.collected_pages.push(page); if commit_info.collected_pages.len() == IOV_MAX || is_last { self.prepare_collected_frames( &mut commit_info, wal, page_sz, db_size, is_last, )?; } } drop(cache); if commit_info.prepared_frames.is_empty() { turso_assert!( self.dirty_pages.read().is_empty(), "dirty pages must be empty if no frames prepared" ); return Ok(IOResult::Done(())); } // Submit all WAL writes let wal_file = wal.wal_file()?; let mut batch = WriteBatch::new(wal_file); for prepared in &commit_info.prepared_frames { batch.writev(prepared.offset, &prepared.bufs); } commit_info.completions = batch.submit()?; commit_info.completion_group = None; commit_info.state = CommitState::WaitWrites; } CommitState::WaitWrites => { if !self .commit_info .read() .completions .iter() .all(|c| c.finished()) { io_yield_one!(self.commit_completion()); } // Check for any write errors let failed = self .commit_info .read() .completions .iter() .find(|c| !c.succeeded()) .cloned(); let mut commit_info = self.commit_info.write(); if let Some(_failed) = failed { commit_info.completions.clear(); commit_info.completion_group = None; commit_info.prepared_frames.clear(); return Err(LimboError::CompletionError(CompletionError::IOError( std::io::ErrorKind::Other, "write", ))); } commit_info.completions.clear(); commit_info.completion_group = None; // Writes done, submit fsync if needed. // NORMAL mode skips fsync on WAL commit (but still fsyncs on checkpoint and wal restart). if sync_mode == SyncMode::Full { let sync_c = wal.sync(self.get_sync_type())?; // Reuse the existing Vec instead of allocating a new one commit_info.completions.push(sync_c); commit_info.state = CommitState::WaitSync; } else { commit_info.state = CommitState::WalCommitDone; } } // To protect against partial writes, we MUST ensure that all write Completions // finish before submitting the fsync. It is possible that a partial write will // cause an IO backend to resubmit the write (particularly with io_uring) and we // cannot have the fsync submitted before all writes are fully done, even if // they are IO_LINK'd together or we submit the fsync with IO_DRAIN, the only way // to ensure durability in the case of partial writes is to ensure the pwritev // completes before the fsync is submitted. CommitState::WaitSync => { let sync_c = self.commit_info.read().completions[0].clone(); // Wait for fsync to complete if !sync_c.finished() { io_yield_one!(sync_c); } // Check for fsync error as we might need to panic on data_sync_retry=off let mut commit_info = self.commit_info.write(); if !sync_c.succeeded() { commit_info.completions.clear(); commit_info.prepared_frames.clear(); if !data_sync_retry { panic!( "fsync error (data_sync_retry=off): {:?}", sync_c.get_error() ); } return Err(LimboError::CompletionError(CompletionError::IOError( std::io::ErrorKind::Other, "sync", ))); } commit_info.completions.clear(); commit_info.state = CommitState::WalCommitDone; } CommitState::WalCommitDone => { // all I/O complete, NOW it's safe to advance WAL state let mut commit_info = self.commit_info.write(); wal.commit_prepared_frames(&commit_info.prepared_frames); wal.finalize_committed_pages(&commit_info.prepared_frames); wal.finish_append_frames_commit()?; self.dirty_pages.write().clear(); commit_info.prepared_frames.clear(); let need_checkpoint = !wal_auto_checkpoint_disabled && wal.should_checkpoint(); if need_checkpoint { commit_info.state = CommitState::AutoCheckpoint; } return Ok(IOResult::Done(())); } CommitState::AutoCheckpoint => panic!("checkpoint must be handled externally"), } } } /// Prepare collected pages as WAL frames without submitting I/O. fn prepare_collected_frames( &self, commit_info: &mut CommitInfo, wal: &Arc, page_sz: PageSize, db_size: u32, is_commit_frame: bool, ) -> Result<()> { let pages = std::mem::take(&mut commit_info.collected_pages); if pages.is_empty() { return Ok(()); } let commit_flag = if is_commit_frame { Some(db_size) } else { None }; for page in &pages { page.set_write_pending(); } // Chain from previous batch if any let prev = commit_info.prepared_frames.last(); let prepared = wal.prepare_frames(&pages, page_sz, commit_flag, prev)?; tracing::debug!("prepare_collected_frames: offset={}", prepared.offset); commit_info.prepared_frames.push(prepared); Ok(()) } fn commit_completion(&self) -> Completion { let mut commit_info = self.commit_info.write(); if let Some(group) = &commit_info.completion_group { return group.clone(); } let mut group = CompletionGroup::new(|_| {}); for c in commit_info.completions.iter() { group.add(c); } let result = group.build(); commit_info.completion_group = Some(result.clone()); result } #[instrument(skip_all, level = Level::DEBUG)] pub fn wal_changed_pages_after(&self, frame_watermark: u64) -> Result> { let wal = self.wal.as_ref().unwrap(); wal.changed_pages_after(frame_watermark) } #[instrument(skip_all, level = Level::DEBUG)] pub fn wal_get_frame(&self, frame_no: u64, frame: &mut [u8]) -> Result { let Some(wal) = self.wal.as_ref() else { turso_soft_unreachable!("wal_get_frame() called on database without WAL"); return Err(LimboError::InternalError( "wal_get_frame() called on database without WAL".to_string(), )); }; wal.read_frame_raw(frame_no, frame) } #[instrument(skip_all, level = Level::DEBUG)] pub fn wal_insert_frame(&self, frame_no: u64, frame: &[u8]) -> Result { let Some(wal) = self.wal.as_ref() else { turso_soft_unreachable!("wal_insert_frame() called on database without WAL"); return Err(LimboError::InternalError( "wal_insert_frame() called on database without WAL".to_string(), )); }; let (header, raw_page) = parse_wal_frame_header(frame); wal.write_frame_raw( self.buffer_pool.clone(), frame_no, header.page_number as u64, header.db_size as u64, raw_page, self.get_sync_type(), )?; if let Some(page) = self.cache_get(header.page_number as usize)? { let content = page.get_contents(); content.as_ptr().copy_from_slice(raw_page); turso_assert!( page.get().id == header.page_number as usize, "page has unexpected id" ); } if header.page_number == 1 { let db_size = self .io .block(|| self.with_header(|header| header.database_size))?; tracing::debug!("truncate page_cache as first page was written: {}", db_size); let mut page_cache = self.page_cache.write(); page_cache.truncate(db_size.get() as usize).map_err(|e| { LimboError::InternalError(format!("Failed to truncate page cache: {e:?}")) })?; } if header.is_commit_frame() { let mut dirty_pages = self.dirty_pages.write(); tracing::debug!( "wal_callback: commit frame, clearing {} dirty pages", dirty_pages.len() ); let mut cache = self.page_cache.write(); for page_id in dirty_pages.iter() { let page_key = PageCacheKey::new(page_id as usize); // Page may have been evicted from cache after spilling to WAL if let Some(page) = cache.get(&page_key)? { page.clear_dirty(); } } dirty_pages.clear(); } Ok(WalFrameInfo { page_no: header.page_number, db_size: header.db_size, }) } pub fn is_checkpointing(&self) -> bool { self.checkpoint_state.read().phase != CheckpointPhase::NotCheckpointing } fn reset_checkpoint_state(&self) { self.clear_checkpoint_state(); self.commit_info.write().state = CommitState::PrepareWal; } /// Reset checkpoint state machine to initial state. /// Use this to clean up after a failed explicit checkpoint (PRAGMA wal_checkpoint). pub fn clear_checkpoint_state(&self) { let mut state = self.checkpoint_state.write(); state.phase = CheckpointPhase::NotCheckpointing; state.result = None; state.mode = None; } /// Clean up after a auto-checkpoint failure. /// Auto-checkpoint executed outside of the main transaction - so WAL transaction was already finalized pub fn cleanup_after_auto_checkpoint_failure(&self) { self.reset_checkpoint_state(); if let Some(wal) = self.wal.as_ref() { wal.abort_checkpoint(); } } #[instrument(skip_all, level = Level::DEBUG, name = "pager_checkpoint",)] /// Checkpoint the WAL to the database file (if needed). /// Args: /// - mode: The checkpoint mode to use (PASSIVE, FULL, RESTART, TRUNCATE) /// - sync_mode: The fsync mode to use (OFF, NORMAL, FULL) /// - clear_page_cache: Whether to clear the page cache after checkpointing pub fn checkpoint( &self, mode: CheckpointMode, sync_mode: crate::SyncMode, clear_page_cache: bool, ) -> Result> { let Some(wal) = self.wal.as_ref() else { turso_soft_unreachable!("checkpoint() called on database without WAL"); return Err(LimboError::InternalError( "checkpoint() called on database without WAL".to_string(), )); }; loop { // Clone the phase to check what state we're in, but keep result in place // This is important because we need to be careful not to e.g. clone and drop the checkpoint result which // causes a drop of CheckpointLocks prematurely and results in a panic. let phase = self.checkpoint_state.read().phase.clone(); match phase { CheckpointPhase::NotCheckpointing => { let mut state = self.checkpoint_state.write(); state.phase = CheckpointPhase::Checkpoint { mode, sync_mode, clear_page_cache, }; state.mode = Some(mode); } CheckpointPhase::Checkpoint { mode, sync_mode, clear_page_cache, } => { let res = return_if_io!(wal.checkpoint(self, mode)); let mut state = self.checkpoint_state.write(); if matches!(mode, CheckpointMode::Truncate { .. }) // `should_truncate` will be true for successful truncate checkpoint && res.should_truncate() { state.phase = CheckpointPhase::TruncateDbFile { sync_mode, clear_page_cache, page1_invalidated: false, }; } else if res.wal_checkpoint_backfilled == 0 || sync_mode == crate::SyncMode::Off { state.phase = CheckpointPhase::Finalize { clear_page_cache }; } else { state.phase = CheckpointPhase::SyncDbFile { clear_page_cache }; } state.result = Some(res); } CheckpointPhase::TruncateDbFile { sync_mode, clear_page_cache, page1_invalidated, } => { let should_skip_truncate_db_file = { let state = self.checkpoint_state.read(); turso_assert!( matches!(state.mode, Some(CheckpointMode::Truncate { .. })), "mode should be truncate in CheckpointPhase::TruncateDbFile" ); let result = state.result.as_ref().expect("result should be set"); // Skip if we already sent truncate result.db_truncate_sent }; if should_skip_truncate_db_file { let mut state = self.checkpoint_state.write(); if sync_mode == crate::SyncMode::Off { // Skip DB sync, proceed to WAL truncation state.phase = CheckpointPhase::TruncateWalFile { clear_page_cache }; } else { // Sync DB first, then SyncDbFile will transition to TruncateWalFile state.phase = CheckpointPhase::SyncDbFile { clear_page_cache }; } continue; } // Invalidate page 1 (header) in cache before reading - checkpoint potentially wrote pages // directly to DB file from the WAL, so the checkpointer connections' page 1 may have stale database_size. if !page1_invalidated { let page1_key = PageCacheKey::new(DatabaseHeader::PAGE_ID); self.page_cache.write().delete(page1_key)?; let mut state = self.checkpoint_state.write(); state.phase = CheckpointPhase::TruncateDbFile { sync_mode, clear_page_cache, page1_invalidated: true, }; } // Truncate the database file unless already at correct size let db_size = return_if_io!(self.with_header(|header| header.database_size)).get(); let page_size = self.get_page_size().unwrap_or_default(); let expected = db_size as u64 * page_size.get() as u64; let should_skip_db_truncate = match self.db_file.size() { Ok(current_size) => expected >= current_size, Err(err) => { // e.g. file.size() is not supported in web worker environment, so we should // skip the truncate if we can't check the size. tracing::debug!( "checkpoint(TRUNCATE): db_file.size unavailable, skipping db truncate pre-check: {err}" ); true } }; if should_skip_db_truncate { // No DB truncation needed (or unsupported size pre-check), move to next phase. let mut state = self.checkpoint_state.write(); if sync_mode == crate::SyncMode::Off { // Skip DB sync, proceed to WAL truncation state.phase = CheckpointPhase::TruncateWalFile { clear_page_cache }; } else { // Sync DB first, then SyncDbFile will transition to TruncateWalFile state.phase = CheckpointPhase::SyncDbFile { clear_page_cache }; } continue; } let c = self.db_file.truncate( expected as usize, Completion::new_trunc(move |_| { tracing::trace!( "Database file truncated to expected size: {} bytes", expected ); }), )?; self.checkpoint_state .write() .result .as_mut() .expect("result should be set") .db_truncate_sent = true; io_yield_one!(c); } CheckpointPhase::SyncDbFile { clear_page_cache } => { let need_sync_db_file = { let state = self.checkpoint_state.read(); let result = state.result.as_ref().expect("result should be set"); !result.db_sync_sent }; if !need_sync_db_file { turso_assert!( !self.syncing.load(Ordering::SeqCst), "syncing should be done" ); // After DB is synced, truncate WAL if in TRUNCATE mode let is_truncate_mode = { let state = self.checkpoint_state.read(); matches!(state.mode, Some(CheckpointMode::Truncate { .. })) }; if is_truncate_mode { self.checkpoint_state.write().phase = CheckpointPhase::TruncateWalFile { clear_page_cache }; } else { self.checkpoint_state.write().phase = CheckpointPhase::Finalize { clear_page_cache }; } continue; } let c = sqlite3_ondisk::begin_sync( self.db_file.as_ref(), self.syncing.clone(), self.get_sync_type(), )?; self.checkpoint_state .write() .result .as_mut() .expect("result should be set") .db_sync_sent = true; io_yield_one!(c); } CheckpointPhase::TruncateWalFile { clear_page_cache } => { // Truncate WAL file after DB is safely synced - this ensures data durability. // If crash occurred after WAL truncate but before DB sync, data would be lost. let need_wal_truncate = { let state = self.checkpoint_state.read(); turso_assert!( matches!(state.mode, Some(CheckpointMode::Truncate { .. })), "mode should be truncate in CheckpointPhase::TruncateWalFile" ); let result = state.result.as_ref().expect("result should be set"); !result.wal_truncate_sent || !result.wal_sync_sent }; if !need_wal_truncate { self.checkpoint_state.write().phase = CheckpointPhase::Finalize { clear_page_cache }; continue; } // Call WAL truncate return_if_io!(wal.truncate_wal( self.checkpoint_state .write() .result .as_mut() .expect("result should be set"), self.get_sync_type(), )); } CheckpointPhase::Finalize { clear_page_cache } => { let mut state = self.checkpoint_state.write(); let mut res = state.result.take().expect("result should be set"); state.phase = CheckpointPhase::NotCheckpointing; state.mode = None; // Clear page cache only if requested (explicit checkpoints do this, auto-checkpoint does not) if clear_page_cache { self.page_cache.write().clear(false).map_err(|e| { res.release_guard(); LimboError::InternalError(format!("Failed to clear page cache: {e:?}")) })?; } // Release checkpoint guard res.release_guard(); return Ok(IOResult::Done(res)); } } } } /// Invalidates entire page cache by removing all dirty and clean pages. Usually used in case /// of a rollback or in case we want to invalidate page cache after starting a read transaction /// right after new writes happened which would invalidate current page cache. pub fn clear_page_cache(&self, clear_dirty: bool) { let dirty_pages = self.dirty_pages.write(); let mut cache = self.page_cache.write(); for page_id in dirty_pages.iter() { let page_key = PageCacheKey::new(page_id as usize); if let Some(page) = cache.get(&page_key).unwrap_or(None) { page.clear_dirty(); } } cache .clear(clear_dirty) .expect("Failed to clear page cache"); if clear_dirty { drop(dirty_pages); self.dirty_pages.write().clear(); } } /// Checkpoint in Truncate mode and delete the WAL file. This method is _only_ to be called /// for shutting down the last remaining connection to a database. /// /// sqlite3.h /// Usually, when a database in [WAL mode] is closed or detached from a /// database handle, SQLite checks if if there are other connections to the /// same database, and if there are no other database connection (if the /// connection being closed is the last open connection to the database), /// then SQLite performs a [checkpoint] before closing the connection and /// deletes the WAL file. pub fn checkpoint_shutdown( &self, wal_auto_checkpoint_disabled: bool, sync_mode: crate::SyncMode, ) -> Result<()> { let mut attempts = 0; { let Some(wal) = self.wal.as_ref() else { turso_soft_unreachable!("checkpoint_shutdown() called on database without WAL"); return Err(LimboError::InternalError( "checkpoint_shutdown() called on database without WAL".to_string(), )); }; // fsync the wal syncronously before beginning checkpoint let c = wal.sync(self.get_sync_type())?; self.io.wait_for_completion(c)?; } if !wal_auto_checkpoint_disabled { while let Err(LimboError::Busy) = self.blocking_checkpoint( CheckpointMode::Truncate { upper_bound_inclusive: None, }, sync_mode, ) { if attempts == 3 { // don't return error on `close` if we are unable to checkpoint, we can silently fail tracing::warn!( "Failed to checkpoint WAL on shutdown after 3 attempts, giving up" ); return Ok(()); } attempts += 1; } } // TODO: delete the WAL file here after truncate checkpoint, but *only* if we are sure that // no other connections have opened since. Ok(()) } /// Perform a blocking checkpoint with the specified mode. /// This is a convenience wrapper around `checkpoint()` that blocks until completion. /// Explicit checkpoints clear the page cache after completion. #[instrument(skip_all, level = Level::DEBUG)] pub fn blocking_checkpoint( &self, mode: CheckpointMode, sync_mode: crate::SyncMode, ) -> Result { self.io.block(|| self.checkpoint(mode, sync_mode, true)) } pub fn freepage_list(&self) -> u32 { self.io .block(|| HeaderRef::from_pager(self)) .map(|header_ref| header_ref.borrow().freelist_pages.get()) .unwrap_or(0) } // Providing a page is optional, if provided it will be used to avoid reading the page from disk. // This is implemented in accordance with sqlite freepage2() function. #[instrument(skip_all, level = Level::DEBUG)] pub fn free_page(&self, mut page: Option, page_id: usize) -> Result> { tracing::trace!("free_page(page_id={})", page_id); // Number of reserved slots in trunk header (next pointer + leaf count) const RESERVED_SLOTS: usize = 2; let header_ref = self.io.block(|| HeaderRefMut::from_pager(self))?; let header = header_ref.borrow_mut(); let mut state = self.free_page_state.write(); tracing::debug!(?state); loop { match &mut *state { FreePageState::Start => { if page_id < 2 || page_id > header.database_size.get() as usize { return Err(LimboError::Corrupt(format!( "Invalid page number {page_id} for free operation" ))); } let (page, c) = match page.take() { Some(page) => { turso_assert_eq!( page.get().id, page_id, "free_page page id mismatch", { "expected": page_id, "actual": page.get().id } ); if page.is_loaded() { let page_contents = page.get_contents(); page_contents.overflow_cells.clear(); } (page, None) } None => self.read_page(page_id as i64)?, }; header.freelist_pages = (header.freelist_pages.get() + 1).into(); let trunk_page_id = header.freelist_trunk_page.get(); // Pin page to prevent eviction while stored in state machine page.pin(); if trunk_page_id != 0 { *state = FreePageState::AddToTrunk { page }; } else { *state = FreePageState::NewTrunk { page }; } if let Some(c) = c { if !c.succeeded() { io_yield_one!(c); } } } FreePageState::AddToTrunk { page } => { let trunk_page_id = header.freelist_trunk_page.get(); let (trunk_page, c) = self.read_page(trunk_page_id as i64)?; if let Some(c) = c { if !c.succeeded() { io_yield_one!(c); } } turso_assert!(trunk_page.is_loaded(), "trunk_page should be loaded"); let trunk_page_contents = trunk_page.get_contents(); let number_of_leaf_pages = trunk_page_contents.read_u32_no_offset(FREELIST_TRUNK_OFFSET_LEAF_COUNT); let max_free_list_entries = (header.usable_space() / FREELIST_LEAF_PTR_SIZE) - RESERVED_SLOTS; if number_of_leaf_pages < max_free_list_entries as u32 { turso_assert!( trunk_page.get().id == trunk_page_id as usize, "trunk page has unexpected id" ); self.add_dirty(&trunk_page)?; trunk_page_contents.write_u32_no_offset( FREELIST_TRUNK_OFFSET_LEAF_COUNT, number_of_leaf_pages + 1, ); trunk_page_contents.write_u32_no_offset( FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR + (number_of_leaf_pages as usize * FREELIST_LEAF_PTR_SIZE), page_id as u32, ); // Unpin page before finishing - it's added to freelist page.unpin(); break; } // page remains pinned as it transitions to NewTrunk state *state = FreePageState::NewTrunk { page: page.clone() }; } FreePageState::NewTrunk { page } => { turso_assert!(page.is_loaded(), "page should be loaded"); // If we get here, need to make this page a new trunk turso_assert!(page.get().id == page_id, "page has unexpected id"); self.add_dirty(page)?; let trunk_page_id = header.freelist_trunk_page.get(); let contents = page.get_contents(); // Point to previous trunk contents .write_u32_no_offset(FREELIST_TRUNK_OFFSET_NEXT_TRUNK_PTR, trunk_page_id); // Zero leaf count contents.write_u32_no_offset(FREELIST_TRUNK_OFFSET_LEAF_COUNT, 0); // Update page 1 to point to new trunk header.freelist_trunk_page = (page_id as u32).into(); // Unpin page before finishing - it's now a trunk page page.unpin(); break; } } } *state = FreePageState::Start; Ok(IOResult::Done(())) } #[instrument(skip_all, level = Level::DEBUG)] pub fn allocate_page1(&self) -> Result> { let state = self.allocate_page1_state.read().clone(); match state { AllocatePage1State::Start => { turso_assert!(!self.db_initialized()); tracing::trace!("allocate_page1(Start)"); let IOResult::Done(mut default_header) = self.with_header(|header| *header)? else { panic!("DB should not be initialized and should not do any IO"); }; turso_assert_eq!(default_header.database_size.get(), 0); default_header.database_size = 1.into(); // Use cached reserved_space if set (e.g., by sync engine before page allocation), // otherwise fall back to IOContext's encryption/checksum requirements. let reserved_space_bytes = self.get_reserved_space().unwrap_or_else(|| { let io_ctx = self.io_ctx.read(); io_ctx.get_reserved_space_bytes() }); default_header.reserved_space = reserved_space_bytes; self.set_reserved_space(reserved_space_bytes); if let Some(size) = self.get_page_size() { default_header.page_size = size; } tracing::debug!( "allocate_page1(Start) page_size = {:?}, reserved_space = {}", default_header.page_size, default_header.reserved_space ); self.buffer_pool .finalize_with_page_size(default_header.page_size.get() as usize)?; let page = allocate_new_page(1, &self.buffer_pool); let contents = page.get_contents(); contents.write_database_header(&default_header); let page1 = page; // Create the sqlite_schema table, for this we just need to create the btree page // for the first page of the database which is basically like any other btree page // but with a 100 byte offset, so we just init the page so that sqlite understands // this is a correct page. btree_init_page( &page1, PageType::TableLeaf, DatabaseHeader::SIZE, (default_header.page_size.get() - default_header.reserved_space as u32) as usize, ); let c = begin_write_btree_page(self, &page1)?; // Pin page1 to prevent eviction while stored in state machine page1.pin(); *self.allocate_page1_state.write() = AllocatePage1State::Writing { page: page1 }; io_yield_one!(c); } AllocatePage1State::Writing { page } => { turso_assert!(page.is_loaded(), "page should be loaded"); tracing::trace!("allocate_page1(Writing done)"); let page_key = PageCacheKey::new(page.get().id); let mut cache = self.page_cache.write(); cache.insert(page_key, page.clone()).map_err(|e| { LimboError::InternalError(format!("Failed to insert page 1 into cache: {e:?}")) })?; // After we wrote the header page, we may now set this None, to signify we initialized self.init_page_1.store(None); page.unpin(); *self.allocate_page1_state.write() = AllocatePage1State::Done; Ok(IOResult::Done(page)) } AllocatePage1State::Done => unreachable!("cannot try to allocate page 1 again"), } } pub fn allocating_page1(&self) -> bool { matches!( *self.allocate_page1_state.read(), AllocatePage1State::Writing { .. } ) } /// Tries to reuse a page from the freelist if available. /// If not, allocates a new page which increases the database size. /// /// FIXME: implement sqlite's 'nearby' parameter and use AllocMode. /// SQLite's allocate_page() equivalent has a parameter 'nearby' which is a hint about the page number we want to have for the allocated page. /// We should use this parameter to allocate the page in the same way as SQLite does; instead now we just either take the first available freelist page /// or allocate a new page. #[allow(clippy::readonly_write_lock)] #[instrument(skip_all, level = Level::DEBUG)] pub fn allocate_page(&self) -> Result> { // Ensure cache has room before allocating (we may spill dirty pages first) return_if_io!(self.ensure_cache_space()); let header_ref = self.io.block(|| HeaderRefMut::from_pager(self))?; let header = header_ref.borrow_mut(); loop { let mut state = self.allocate_page_state.write(); tracing::debug!("allocate_page(state={:?})", state); match &mut *state { AllocatePageState::Start => { let old_db_size = header.database_size.get(); #[cfg(not(feature = "omit_autovacuum"))] let mut new_db_size = old_db_size; #[cfg(feature = "omit_autovacuum")] let new_db_size = old_db_size; tracing::debug!("allocate_page(database_size={})", new_db_size); #[cfg(not(feature = "omit_autovacuum"))] { // If the following conditions are met, allocate a pointer map page, add to cache and increment the database size // - autovacuum is enabled // - the last page is a pointer map page if matches!( AutoVacuumMode::from(self.auto_vacuum_mode.load(Ordering::SeqCst)), AutoVacuumMode::Full ) && is_ptrmap_page(new_db_size + 1, header.page_size.get() as usize) { // we will allocate a ptrmap page, so increment size new_db_size += 1; let page = allocate_new_page(new_db_size as i64, &self.buffer_pool); self.add_dirty(&page)?; let page_key = PageCacheKey::new(page.get().id as usize); let mut cache = self.page_cache.write(); cache.insert(page_key, page)?; } } let first_freelist_trunk_page_id = header.freelist_trunk_page.get(); if first_freelist_trunk_page_id == 0 { *state = AllocatePageState::AllocateNewPage { current_db_size: new_db_size, }; continue; } let (trunk_page, c) = self.read_page(first_freelist_trunk_page_id as i64)?; // Pin trunk_page to prevent eviction while stored in state machine trunk_page.pin(); *state = AllocatePageState::SearchAvailableFreeListLeaf { trunk_page }; if let Some(c) = c { io_yield_one!(c); } } AllocatePageState::SearchAvailableFreeListLeaf { trunk_page } => { turso_assert!( trunk_page.is_loaded(), "Freelist trunk page is not loaded", { "page_id": trunk_page.get().id } ); let page_contents = trunk_page.get_contents(); let next_trunk_page_id = page_contents.read_u32_no_offset(FREELIST_TRUNK_OFFSET_NEXT_TRUNK_PTR); let number_of_freelist_leaves = page_contents.read_u32_no_offset(FREELIST_TRUNK_OFFSET_LEAF_COUNT); // There are leaf pointers on this trunk page, so we can reuse one of the pages // for the allocation. if number_of_freelist_leaves != 0 { let page_contents = trunk_page.get_contents(); let next_leaf_page_id = page_contents.read_u32_no_offset(FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR); let (leaf_page, c) = self.read_page(next_leaf_page_id as i64)?; turso_assert!( number_of_freelist_leaves > 0, "Freelist trunk page has no leaves", { "page_id": trunk_page.get().id } ); // Pin leaf_page to prevent eviction while stored in state machine // trunk_page is already pinned from previous state leaf_page.pin(); *state = AllocatePageState::ReuseFreelistLeaf { trunk_page: trunk_page.clone(), leaf_page, number_of_freelist_leaves, }; if let Some(c) = c { io_yield_one!(c); } continue; } // No freelist leaves on this trunk page. // Reuse the trunk page itself (even if this is the last trunk). // Update the database's first freelist trunk page to the next trunk page (may be 0 if there are no more trunk pages). header.freelist_trunk_page = next_trunk_page_id.into(); header.freelist_pages = (header.freelist_pages.get() - 1).into(); self.add_dirty(trunk_page)?; // zero out the page turso_assert!( trunk_page.get_contents().overflow_cells.is_empty(), "Freelist trunk page has overflow cells", { "page_id": trunk_page.get().id } ); trunk_page.get_contents().as_ptr().fill(0); let page_key = PageCacheKey::new(trunk_page.get().id); { let page_cache = self.page_cache.read(); turso_assert!( page_cache.contains_key(&page_key), "page is not in cache", { "page_id": trunk_page.get().id } ); } // Unpin trunk_page before returning - caller takes ownership trunk_page.unpin(); let trunk_page = trunk_page.clone(); *state = AllocatePageState::Start; return Ok(IOResult::Done(trunk_page)); } AllocatePageState::ReuseFreelistLeaf { trunk_page, leaf_page, number_of_freelist_leaves, } => { turso_assert!( leaf_page.is_loaded(), "Leaf page is not loaded", { "page_id": leaf_page.get().id } ); let page_contents = trunk_page.get_contents(); self.add_dirty(leaf_page)?; // zero out the page turso_assert!( leaf_page.get_contents().overflow_cells.is_empty(), "Freelist leaf page has overflow cells", { "page_id": leaf_page.get().id } ); leaf_page.get_contents().as_ptr().fill(0); let page_key = PageCacheKey::new(leaf_page.get().id); { let page_cache = self.page_cache.read(); turso_assert!( page_cache.contains_key(&page_key), "page is not in cache", { "page_id": leaf_page.get().id } ); } // Mark trunk page dirty BEFORE modifying it so subjournal captures original content self.add_dirty(trunk_page)?; // Shift left all the other leaf pages in the trunk page and subtract 1 from the leaf count let remaining_leaves_count = (*number_of_freelist_leaves - 1) as usize; { let buf = page_contents.as_ptr(); // use copy within the same page let offset_remaining_leaves_start = FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR + FREELIST_LEAF_PTR_SIZE; let offset_remaining_leaves_end = offset_remaining_leaves_start + remaining_leaves_count * FREELIST_LEAF_PTR_SIZE; buf.copy_within( offset_remaining_leaves_start..offset_remaining_leaves_end, FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR, ); } // write the new leaf count page_contents.write_u32_no_offset( FREELIST_TRUNK_OFFSET_LEAF_COUNT, remaining_leaves_count as u32, ); header.freelist_pages = (header.freelist_pages.get() - 1).into(); // Unpin both pages before returning - caller takes ownership of leaf_page trunk_page.unpin(); leaf_page.unpin(); let leaf_page = leaf_page.clone(); *state = AllocatePageState::Start; return Ok(IOResult::Done(leaf_page)); } AllocatePageState::AllocateNewPage { current_db_size } => { let mut new_db_size = *current_db_size + 1; // if new_db_size reaches the pending page, we need to allocate a new one if Some(new_db_size) == self.pending_byte_page_id() { let richard_hipp_special_page = allocate_new_page(new_db_size as i64, &self.buffer_pool); self.add_dirty(&richard_hipp_special_page)?; let page_key = PageCacheKey::new(richard_hipp_special_page.get().id); { let mut cache = self.page_cache.write(); cache.insert(page_key, richard_hipp_special_page).unwrap(); } // HIPP special page is assumed to zeroed and should never be read or written to by the BTREE new_db_size += 1; } // Check if allocating a new page would exceed the maximum page count let max_page_count = self.get_max_page_count(); if new_db_size > max_page_count { return Err(LimboError::DatabaseFull( "database or disk is full".to_string(), )); } // FIXME: should reserve page cache entry before modifying the database let page = allocate_new_page(new_db_size as i64, &self.buffer_pool); { // setup page and add to cache self.add_dirty(&page)?; let page_key = PageCacheKey::new(page.get().id as usize); { // Run in separate block to avoid deadlock on page cache write lock let mut cache = self.page_cache.write(); cache.insert(page_key, page.clone())?; } header.database_size = new_db_size.into(); *state = AllocatePageState::Start; return Ok(IOResult::Done(page)); } } } } } pub fn upsert_page_in_cache( &self, id: usize, page: PageRef, dirty_page_must_exist: bool, ) -> Result<(), LimboError> { let mut cache = self.page_cache.write(); let page_key = PageCacheKey::new(id); // FIXME: use specific page key for writer instead of max frame, this will make readers not conflict if dirty_page_must_exist { turso_assert!(page.is_dirty(), "page must be dirty for upsert", { "page_id": id }); } cache.upsert_page(page_key, page.clone()).map_err(|e| { LimboError::InternalError(format!( "Failed to insert loaded page {id} into cache: {e:?}" )) })?; page.set_loaded(); page.clear_wal_tag(); Ok(()) } #[instrument(skip_all, level = Level::DEBUG)] pub fn rollback(&self, schema_did_change: bool, connection: &Connection, is_write: bool) { tracing::debug!(schema_did_change); if is_write { let clear_dirty = true; // The page cache only needs to be cleared if we are rolling back a write transaction. // If a read transaction rolls back, and the next read transaction detects that the // database has changed in between (see db_changed() in wal.rs), then the page cache // will be cleared. Since the read transaction itself has not modified anything, it can proceed // with its cached pages in case the database has NOT changed in between. // // Even in the case of a write transaction, clearing the entire page cache is overkill, // since we only need to clear the dirty pages that were modified by the write transaction. self.clear_page_cache(clear_dirty); self.dirty_pages.write().clear(); } else { turso_assert!( self.dirty_pages.read().is_empty(), "dirty pages should be empty for read txn" ); } self.reset_internal_states(); // Invalidate cached schema cookie since rollback may have restored the database schema cookie self.set_schema_cookie(None); if schema_did_change { *connection.schema.write() = connection.db.clone_schema(); } if is_write { if let Some(wal) = self.wal.as_ref() { wal.rollback(None); } } } fn reset_internal_states(&self) { *self.checkpoint_state.write() = CheckpointState::default(); self.syncing.store(false, Ordering::SeqCst); self.commit_info.write().reset(); *self.allocate_page_state.write() = AllocatePageState::Start; *self.free_page_state.write() = FreePageState::Start; *self.spill_state.write() = SpillState::Idle; #[cfg(not(feature = "omit_autovacuum"))] { let mut vacuum_state = self.vacuum_state.write(); vacuum_state.ptrmap_get_state = PtrMapGetState::Start; vacuum_state.ptrmap_put_state = PtrMapPutState::Start; vacuum_state.btree_create_vacuum_full_state = BtreeCreateVacuumFullState::Start; } *self.header_ref_state.write() = HeaderRefState::Start; } pub fn with_header(&self, f: impl Fn(&DatabaseHeader) -> T) -> Result> { let header_ref = return_if_io!(HeaderRef::from_pager(self)); let header = header_ref.borrow(); // Update cached schema cookie when reading header self.set_schema_cookie(Some(header.schema_cookie.get())); Ok(IOResult::Done(f(header))) } pub fn with_header_mut(&self, f: impl Fn(&mut DatabaseHeader) -> T) -> Result> { let header_ref = return_if_io!(HeaderRefMut::from_pager(self)); let header = header_ref.borrow_mut(); let result = f(header); // Update cached schema cookie after modification self.set_schema_cookie(Some(header.schema_cookie.get())); Ok(IOResult::Done(result)) } pub fn is_encryption_ctx_set(&self) -> bool { self.io_ctx.write().encryption_context().is_some() } pub fn is_encryption_enabled(&self) -> bool { self.enable_encryption.load(Ordering::SeqCst) } pub fn set_encryption_context( &self, cipher_mode: CipherMode, key: &EncryptionKey, ) -> Result<()> { // we will set the encryption context only if the encryption is opted-in. if !self.enable_encryption.load(Ordering::SeqCst) { return Err(LimboError::InvalidArgument( "encryption is an opt in feature. enable it via passing `--experimental-encryption`" .into(), )); } let page_size = self.get_page_size_unchecked().get() as usize; let encryption_ctx = EncryptionContext::new(cipher_mode, key, page_size)?; { let mut io_ctx = self.io_ctx.write(); io_ctx.set_encryption(encryption_ctx); } let Some(wal) = self.wal.as_ref() else { return Ok(()); }; wal.set_io_context(self.io_ctx.read().clone()); // whenever we set the encryption context, lets reset the page cache. The page cache // might have been loaded with page 1 to initialise the connection. During initialisation, // we only read the header which is unencrypted, but the rest of the page is. If so, lets // clear the cache. self.clear_page_cache(false); // Also invalidate cached schema cookie to force re-read of page 1 with encryption self.set_schema_cookie(None); Ok(()) } pub fn reset_checksum_context(&self) { { let mut io_ctx = self.io_ctx.write(); io_ctx.reset_checksum(); } let Some(wal) = self.wal.as_ref() else { return }; wal.set_io_context(self.io_ctx.read().clone()) } pub fn set_reserved_space_bytes(&self, value: u8) { self.set_reserved_space(value); } /// Encryption is an opt-in feature. If the flag is passed, then enable the encryption on /// pager, which is then used to set it on the IOContext. pub fn enable_encryption(&self, enable: bool) { self.enable_encryption.store(enable, Ordering::SeqCst); } } pub fn allocate_new_page(page_id: i64, buffer_pool: &Arc) -> PageRef { let page = Arc::new(Page::new(page_id)); { let buffer = buffer_pool.get_page(); let inner = page.get(); inner.buffer = Some(Arc::new(buffer)); page.set_loaded(); page.clear_wal_tag(); } page } pub fn default_page1(cipher: Option<&CipherMode>) -> PageRef { // New Database header for empty Database let mut default_header = DatabaseHeader::default(); if let Some(cipher) = cipher { // we will set the reserved space bytes as required by either the encryption let reserved_space_bytes = cipher.metadata_size() as u8; default_header.reserved_space = reserved_space_bytes; } let page = Arc::new(Page::new(DatabaseHeader::PAGE_ID as i64)); { let inner = page.get(); inner.buffer = Some(Arc::new(Buffer::new_temporary( default_header.page_size.get() as usize, ))); } page.get_contents().write_database_header(&default_header); page.set_loaded(); page.clear_wal_tag(); btree_init_page( &page, PageType::TableLeaf, DatabaseHeader::SIZE, // offset of 100 bytes (default_header.page_size.get() - default_header.reserved_space as u32) as usize, ); page } #[derive(Debug, Clone, Copy)] pub struct CreateBTreeFlags(pub u8); impl CreateBTreeFlags { pub const TABLE: u8 = 0b0001; pub const INDEX: u8 = 0b0010; pub fn new_table() -> Self { Self(CreateBTreeFlags::TABLE) } pub fn new_index() -> Self { Self(CreateBTreeFlags::INDEX) } pub fn is_table(&self) -> bool { (self.0 & CreateBTreeFlags::TABLE) != 0 } pub fn is_index(&self) -> bool { (self.0 & CreateBTreeFlags::INDEX) != 0 } pub fn get_flags(&self) -> u8 { self.0 } } /* ** The pointer map is a lookup table that identifies the parent page for ** each child page in the database file. The parent page is the page that ** contains a pointer to the child. Every page in the database contains ** 0 or 1 parent pages. Each pointer map entry consists of a single byte 'type' ** and a 4 byte parent page number. ** ** The PTRMAP_XXX identifiers below are the valid types. ** ** The purpose of the pointer map is to facilitate moving pages from one ** position in the file to another as part of autovacuum. When a page ** is moved, the pointer in its parent must be updated to point to the ** new location. The pointer map is used to locate the parent page quickly. ** ** PTRMAP_ROOTPAGE: The database page is a root-page. The page-number is not ** used in this case. ** ** PTRMAP_FREEPAGE: The database page is an unused (free) page. The page-number ** is not used in this case. ** ** PTRMAP_OVERFLOW1: The database page is the first page in a list of ** overflow pages. The page number identifies the page that ** contains the cell with a pointer to this overflow page. ** ** PTRMAP_OVERFLOW2: The database page is the second or later page in a list of ** overflow pages. The page-number identifies the previous ** page in the overflow page list. ** ** PTRMAP_BTREE: The database page is a non-root btree page. The page number ** identifies the parent page in the btree. */ #[cfg(not(feature = "omit_autovacuum"))] pub(crate) mod ptrmap { #[allow(unused_imports)] use crate::{storage::sqlite3_ondisk::PageSize, LimboError, Result}; use crate::{turso_assert_greater_than_or_equal, turso_soft_unreachable}; // Constants pub const PTRMAP_ENTRY_SIZE: usize = 5; /// Page 1 is the schema page which contains the database header. /// Page 2 is the first pointer map page if the database has any pointer map pages. pub const FIRST_PTRMAP_PAGE_NO: u32 = 2; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum PtrmapType { RootPage = 1, FreePage = 2, Overflow1 = 3, Overflow2 = 4, BTreeNode = 5, } impl PtrmapType { pub fn from_u8(value: u8) -> Option { match value { 1 => Some(PtrmapType::RootPage), 2 => Some(PtrmapType::FreePage), 3 => Some(PtrmapType::Overflow1), 4 => Some(PtrmapType::Overflow2), 5 => Some(PtrmapType::BTreeNode), _ => None, } } } #[derive(Debug, Clone, Copy)] pub struct PtrmapEntry { pub entry_type: PtrmapType, pub parent_page_no: u32, } impl PtrmapEntry { pub fn serialize(&self, buffer: &mut [u8]) -> Result<()> { if buffer.len() < PTRMAP_ENTRY_SIZE { return Err(LimboError::InternalError(format!( "Buffer too small to serialize ptrmap entry. Expected at least {} bytes, got {}", PTRMAP_ENTRY_SIZE, buffer.len() ))); } buffer[0] = self.entry_type as u8; buffer[1..5].copy_from_slice(&self.parent_page_no.to_be_bytes()); Ok(()) } pub fn deserialize(buffer: &[u8]) -> Option { if buffer.len() < PTRMAP_ENTRY_SIZE { return None; } let entry_type_u8 = buffer[0]; let parent_bytes_slice = buffer.get(1..5)?; let parent_page_no = u32::from_be_bytes(parent_bytes_slice.try_into().ok()?); PtrmapType::from_u8(entry_type_u8).map(|entry_type| PtrmapEntry { entry_type, parent_page_no, }) } } /// Calculates how many database pages are mapped by a single pointer map page. /// This is based on the total page size, as ptrmap pages are filled with entries. pub fn entries_per_ptrmap_page(page_size: usize) -> usize { turso_assert_greater_than_or_equal!(page_size, PageSize::MIN as usize); page_size / PTRMAP_ENTRY_SIZE } /// Calculates the cycle length of pointer map pages /// The cycle length is the number of database pages that are mapped by a single pointer map page. pub fn ptrmap_page_cycle_length(page_size: usize) -> usize { turso_assert_greater_than_or_equal!(page_size, PageSize::MIN as usize); (page_size / PTRMAP_ENTRY_SIZE) + 1 } /// Determines if a given page number `db_page_no` (1-indexed) is a pointer map page in a database with autovacuum enabled pub fn is_ptrmap_page(db_page_no: u32, page_size: usize) -> bool { // The first page cannot be a ptrmap page because its for the schema if db_page_no == 1 { return false; } if db_page_no == FIRST_PTRMAP_PAGE_NO { return true; } get_ptrmap_page_no_for_db_page(db_page_no, page_size) == db_page_no } /// Calculates which pointer map page (1-indexed) contains the entry for `db_page_no_to_query` (1-indexed). /// `db_page_no_to_query` is the page whose ptrmap entry we are interested in. pub fn get_ptrmap_page_no_for_db_page(db_page_no_to_query: u32, page_size: usize) -> u32 { let group_size = ptrmap_page_cycle_length(page_size) as u32; if group_size == 0 { panic!("Page size too small, a ptrmap page cannot map any db pages."); } let effective_page_index = db_page_no_to_query - FIRST_PTRMAP_PAGE_NO; let group_idx = effective_page_index / group_size; (group_idx * group_size) + FIRST_PTRMAP_PAGE_NO } /// Calculates the byte offset of the entry for `db_page_no_to_query` (1-indexed) /// within its pointer map page (`ptrmap_page_no`, 1-indexed). pub fn get_ptrmap_offset_in_page( db_page_no_to_query: u32, ptrmap_page_no: u32, page_size: usize, ) -> Result { // The data pages mapped by `ptrmap_page_no` are: // `ptrmap_page_no + 1`, `ptrmap_page_no + 2`, ..., up to `ptrmap_page_no + n_data_pages_per_group`. // `db_page_no_to_query` must be one of these. // The 0-indexed position of `db_page_no_to_query` within this sequence of data pages is: // `db_page_no_to_query - (ptrmap_page_no + 1)`. let n_data_pages_per_group = entries_per_ptrmap_page(page_size); let first_data_page_mapped = ptrmap_page_no + 1; let last_data_page_mapped = ptrmap_page_no + n_data_pages_per_group as u32; if db_page_no_to_query < first_data_page_mapped || db_page_no_to_query > last_data_page_mapped { turso_soft_unreachable!("Page is not mapped by ptrmap data range", { "page": db_page_no_to_query, "range_start": first_data_page_mapped, "range_end": last_data_page_mapped, "ptrmap_page": ptrmap_page_no }); return Err(LimboError::InternalError(format!( "Page {db_page_no_to_query} is not mapped by the data page range [{first_data_page_mapped}, {last_data_page_mapped}] of ptrmap page {ptrmap_page_no}" ))); } if is_ptrmap_page(db_page_no_to_query, page_size) { turso_soft_unreachable!("Page is a pointer map page and should not have an entry calculated this way", { "page": db_page_no_to_query }); return Err(LimboError::InternalError(format!( "Page {db_page_no_to_query} is a pointer map page and should not have an entry calculated this way." ))); } let entry_index_on_page = (db_page_no_to_query - first_data_page_mapped) as usize; Ok(entry_index_on_page * PTRMAP_ENTRY_SIZE) } } #[cfg(test)] mod tests { use crate::sync::Arc; use crate::sync::RwLock; use crate::storage::page_cache::{PageCache, PageCacheKey}; use super::Page; #[test] fn test_shared_cache() { // ensure cache can be shared between threads let cache = Arc::new(RwLock::new(PageCache::new(10))); let thread = { let cache = cache.clone(); std::thread::spawn(move || { let mut cache = cache.write(); let page_key = PageCacheKey::new(1); let page = Page::new(1); // Set loaded so that we avoid eviction, as we evict the page from cache if it is not locked and not loaded page.set_loaded(); cache.insert(page_key, Arc::new(page)).unwrap(); }) }; let _ = thread.join(); let mut cache = cache.write(); let page_key = PageCacheKey::new(1); let page = cache.get(&page_key).unwrap(); assert_eq!(page.unwrap().get().id, 1); } } #[cfg(test)] #[cfg(not(feature = "omit_autovacuum"))] mod ptrmap_tests { use crate::sync::Arc; use super::ptrmap::*; use super::*; use crate::io::{MemoryIO, OpenFlags, IO}; use crate::storage::buffer_pool::BufferPool; use crate::storage::database::DatabaseFile; use crate::storage::page_cache::PageCache; use crate::storage::pager::{default_page1, Pager}; use crate::storage::sqlite3_ondisk::PageSize; use crate::storage::wal::{WalFile, WalFileShared}; use arc_swap::ArcSwapOption; pub fn run_until_done( mut action: impl FnMut() -> Result>, pager: &Pager, ) -> Result { loop { match action()? { IOResult::Done(res) => { return Ok(res); } IOResult::IO(io) => io.wait(pager.io.as_ref())?, } } } // Helper to create a Pager for testing fn test_pager_setup(page_size: u32, initial_db_pages: u32) -> Pager { let io: Arc = Arc::new(MemoryIO::new()); let db_file: Arc = Arc::new(DatabaseFile::new( io.open_file("test.db", OpenFlags::Create, true).unwrap(), )); // Construct interfaces for the pager let pages = initial_db_pages + 10; let sz = std::cmp::max(std::cmp::min(pages, 64), pages); let buffer_pool = BufferPool::begin_init(&io, (sz * page_size) as usize); let wal_shared = WalFileShared::new_shared( io.open_file("test.db-wal", OpenFlags::Create, false) .unwrap(), ) .unwrap(); let last_checksum_and_max_frame = wal_shared.read().last_checksum_and_max_frame(); let wal: Arc = Arc::new(WalFile::new( io.clone(), wal_shared, last_checksum_and_max_frame, buffer_pool.clone(), )); // For new empty databases, init_page_1 must be Some(page) so allocate_page1() can be called let init_page_1 = Arc::new(ArcSwapOption::new(Some(default_page1(None)))); let pager = Pager::new( db_file, Some(wal), io, PageCache::new(sz as usize), buffer_pool, Arc::new(Mutex::new(())), init_page_1, ) .unwrap(); run_until_done(|| pager.allocate_page1(), &pager).unwrap(); { let page_cache = pager.page_cache.read(); println!( "Cache Len: {} Cap: {}", page_cache.len(), page_cache.capacity() ); } pager .io .block(|| { pager.with_header_mut(|header| header.vacuum_mode_largest_root_page = 1.into()) }) .unwrap(); pager.set_auto_vacuum_mode(AutoVacuumMode::Full); // Allocate all the pages as btree root pages const EXPECTED_FIRST_ROOT_PAGE_ID: u32 = 3; // page1 = 1, first ptrmap page = 2, root page = 3 for i in 0..initial_db_pages { let res = run_until_done( || pager.btree_create(&CreateBTreeFlags::new_table()), &pager, ); { let page_cache = pager.page_cache.read(); println!( "i: {} Cache Len: {} Cap: {}", i, page_cache.len(), page_cache.capacity() ); } match res { Ok(root_page_id) => { assert_eq!(root_page_id, EXPECTED_FIRST_ROOT_PAGE_ID + i); } Err(e) => { panic!("test_pager_setup: btree_create failed: {e:?}"); } } } pager } #[test] fn test_ptrmap_page_allocation() { let page_size = 4096; let initial_db_pages = 10; let pager = test_pager_setup(page_size, initial_db_pages); // Page 5 should be mapped by ptrmap page 2. let db_page_to_update: u32 = 5; let expected_ptrmap_pg_no = get_ptrmap_page_no_for_db_page(db_page_to_update, page_size as usize); assert_eq!(expected_ptrmap_pg_no, FIRST_PTRMAP_PAGE_NO); // Ensure the pointer map page ref is created and loadable via the pager let ptrmap_page_ref = pager.read_page(expected_ptrmap_pg_no as i64); assert!(ptrmap_page_ref.is_ok()); // Ensure that the database header size is correctly reflected assert_eq!( pager .io .block(|| pager.with_header(|header| header.database_size)) .unwrap() .get(), initial_db_pages + 2 ); // (1+1) -> (header + ptrmap) // Read the entry from the ptrmap page and verify it let entry = pager .io .block(|| pager.ptrmap_get(db_page_to_update)) .unwrap() .unwrap(); assert_eq!(entry.entry_type, PtrmapType::RootPage); assert_eq!(entry.parent_page_no, 0); } #[test] fn test_is_ptrmap_page_logic() { let page_size = PageSize::MIN as usize; let n_data_pages = entries_per_ptrmap_page(page_size); assert_eq!(n_data_pages, 102); // 512/5 = 102 assert!(!is_ptrmap_page(1, page_size)); // Header assert!(is_ptrmap_page(2, page_size)); // P0 assert!(!is_ptrmap_page(3, page_size)); // D0_1 assert!(!is_ptrmap_page(4, page_size)); // D0_2 assert!(!is_ptrmap_page(5, page_size)); // D0_3 assert!(is_ptrmap_page(105, page_size)); // P1 assert!(!is_ptrmap_page(106, page_size)); // D1_1 assert!(!is_ptrmap_page(107, page_size)); // D1_2 assert!(!is_ptrmap_page(108, page_size)); // D1_3 assert!(is_ptrmap_page(208, page_size)); // P2 } #[test] fn test_get_ptrmap_page_no() { let page_size = PageSize::MIN as usize; // Maps 103 data pages // Test pages mapped by P0 (page 2) assert_eq!(get_ptrmap_page_no_for_db_page(3, page_size), 2); // D(3) -> P0(2) assert_eq!(get_ptrmap_page_no_for_db_page(4, page_size), 2); // D(4) -> P0(2) assert_eq!(get_ptrmap_page_no_for_db_page(5, page_size), 2); // D(5) -> P0(2) assert_eq!(get_ptrmap_page_no_for_db_page(104, page_size), 2); // D(104) -> P0(2) assert_eq!(get_ptrmap_page_no_for_db_page(105, page_size), 105); // Page 105 is a pointer map page. // Test pages mapped by P1 (page 6) assert_eq!(get_ptrmap_page_no_for_db_page(106, page_size), 105); // D(106) -> P1(105) assert_eq!(get_ptrmap_page_no_for_db_page(107, page_size), 105); // D(107) -> P1(105) assert_eq!(get_ptrmap_page_no_for_db_page(108, page_size), 105); // D(108) -> P1(105) assert_eq!(get_ptrmap_page_no_for_db_page(208, page_size), 208); // Page 208 is a pointer map page. } #[test] fn test_get_ptrmap_offset() { let page_size = PageSize::MIN as usize; // Maps 103 data pages assert_eq!(get_ptrmap_offset_in_page(3, 2, page_size).unwrap(), 0); assert_eq!( get_ptrmap_offset_in_page(4, 2, page_size).unwrap(), PTRMAP_ENTRY_SIZE ); assert_eq!( get_ptrmap_offset_in_page(5, 2, page_size).unwrap(), 2 * PTRMAP_ENTRY_SIZE ); // P1 (page 105) maps D(106)...D(207) // D(106) is index 0 on P1. Offset 0. // D(107) is index 1 on P1. Offset 5. // D(108) is index 2 on P1. Offset 10. assert_eq!(get_ptrmap_offset_in_page(106, 105, page_size).unwrap(), 0); assert_eq!( get_ptrmap_offset_in_page(107, 105, page_size).unwrap(), PTRMAP_ENTRY_SIZE ); assert_eq!( get_ptrmap_offset_in_page(108, 105, page_size).unwrap(), 2 * PTRMAP_ENTRY_SIZE ); } } ================================================ FILE: core/storage/slot_bitmap.rs ================================================ use crate::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use crate::turso_assert; /// Shared constants for bitmap operations. const WORD_SHIFT: u32 = 6; const WORD_BITS: u32 = 64; const WORD_MASK: u32 = 63; const ALL_FREE: u64 = u64::MAX; const ALL_ALLOCATED: u64 = 0u64; #[inline] const fn word_and_bit_to_slot(word_idx: usize, bit: u32) -> u32 { (word_idx as u32) << WORD_SHIFT | bit } #[inline] const fn slot_to_word_and_bit(slot_idx: u32) -> (usize, u32) { ((slot_idx >> WORD_SHIFT) as usize, slot_idx & WORD_MASK) } /// Lock-free atomic bitmap for tracking allocated slots in an arena. /// /// Bit meaning: /// - 1 = free /// - 0 = allocated /// /// `alloc_one` is lock-free (CAS retry bounded by contention, not blocking). /// `free_one` is wait-free (single `fetch_or`). pub(super) struct AtomicSlotBitmap { words: Box<[AtomicU64]>, n_slots: u32, /// Performance hint for where to start scanning. Not correctness-critical. next_word_hint: AtomicUsize, } // SAFETY: All fields are atomics or immutable after construction. unsafe impl Send for AtomicSlotBitmap {} unsafe impl Sync for AtomicSlotBitmap {} impl std::fmt::Debug for AtomicSlotBitmap { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AtomicSlotBitmap") .field("n_slots", &self.n_slots) .finish() } } impl AtomicSlotBitmap { /// Creates a new `AtomicSlotBitmap` capable of tracking `n_slots` slots. /// All slots start as free. pub fn new(n_slots: u32) -> Self { turso_assert!(n_slots > 0, "number of slots must be non-zero"); turso_assert!( n_slots % WORD_BITS == 0, "number of slots in map must be a multiple of 64" ); let n_words = (n_slots / WORD_BITS) as usize; let words: Vec = (0..n_words).map(|_| AtomicU64::new(ALL_FREE)).collect(); Self { words: words.into_boxed_slice(), n_slots, next_word_hint: AtomicUsize::new(0), } } /// Returns whether a slot is currently free (snapshot, may be stale). pub fn is_free(&self, slot: u32) -> bool { if slot >= self.n_slots { return false; } let (word_idx, bit) = slot_to_word_and_bit(slot); (self.words[word_idx].load(Ordering::Acquire) & (1u64 << bit)) != 0 } /// Allocates a single free slot from the bitmap. Lock-free. /// /// Returns `Some(slot_index)` on success, `None` if all slots are allocated. pub fn alloc_one(&self) -> Option { let n_words = self.words.len(); if n_words == 0 { return None; } let hint = self.next_word_hint.load(Ordering::Acquire).min(n_words - 1); // Scan from hint to end, then wrap around from 0 to hint. for offset in 0..n_words { let word_idx = (hint + offset) % n_words; let mut word = self.words[word_idx].load(Ordering::Acquire); while word != ALL_ALLOCATED { let bit = word.trailing_zeros(); let new_word = word & !(1u64 << bit); match self.words[word_idx].compare_exchange_weak( word, new_word, Ordering::AcqRel, Ordering::Acquire, ) { Ok(_) => { // Update hint: if word is now fully allocated, advance past it. let new_hint = if new_word == ALL_ALLOCATED { (word_idx + 1) % n_words } else { word_idx }; self.next_word_hint.store(new_hint, Ordering::Release); return Some(word_and_bit_to_slot(word_idx, bit)); } Err(actual) => { // CAS failed — another thread changed this word. Retry with fresh value. word = actual; } } } } None } /// Frees a previously allocated slot. Wait-free (single atomic op). pub fn free_one(&self, slot: u32) { turso_assert!(slot < self.n_slots, "free_one out of bounds"); let (word_idx, bit) = slot_to_word_and_bit(slot); let mask = 1u64 << bit; let old = self.words[word_idx].fetch_or(mask, Ordering::Release); debug_assert!((old & mask) == 0, "double-free detected for slot {slot}"); // If this word is before the current hint, pull the hint back. let hint = self.next_word_hint.load(Ordering::Acquire); if word_idx < hint { self.next_word_hint.store(word_idx, Ordering::Release); } } } #[cfg(test)] pub mod tests { use super::*; use rand::{rngs::StdRng, Rng, SeedableRng}; fn atomic_free_vec(ab: &AtomicSlotBitmap) -> Vec { (0..ab.n_slots).map(|i| ab.is_free(i)).collect() } fn assert_equivalent(ab: &AtomicSlotBitmap, model: &[bool]) { let av = atomic_free_vec(ab); assert_eq!(av, model, "bitmap bits disagree with reference model"); } #[test] fn alloc_one_exhausts_all() { let ab = AtomicSlotBitmap::new(256); let mut model = vec![true; 256]; let mut count = 0; while let Some(idx) = ab.alloc_one() { assert!(model[idx as usize], "must be free in model"); model[idx as usize] = false; count += 1; } assert_eq!(count, 256, "should allocate all slots once"); assert!(ab.alloc_one().is_none(), "no slots left"); assert_equivalent(&ab, &model); } #[test] fn free_one_allows_reuse() { let ab = AtomicSlotBitmap::new(128); let mut model = vec![true; 128]; let a = ab.alloc_one().unwrap(); let b = ab.alloc_one().unwrap(); model[a as usize] = false; model[b as usize] = false; ab.free_one(a); model[a as usize] = true; assert_equivalent(&ab, &model); let c = ab.alloc_one().unwrap(); model[c as usize] = false; assert_equivalent(&ab, &model); } #[test] fn freeing_earlier_slot_updates_hint() { let ab = AtomicSlotBitmap::new(64); let mut allocated = Vec::new(); while let Some(s) = ab.alloc_one() { allocated.push(s); } let freed = allocated[0]; ab.free_one(freed); assert_eq!(ab.alloc_one(), Some(freed)); } #[test] fn fuzz_alloc_free_compare_with_reference_model() { let seeds: &[u64] = &[ std::time::SystemTime::UNIX_EPOCH .elapsed() .unwrap_or_default() .as_secs(), 1234567890, 0x69420, 94822, 165029, ]; for &seed in seeds { let mut rng = StdRng::seed_from_u64(seed); let n_slots = rng.random_range(1..10) * 64; let ab = AtomicSlotBitmap::new(n_slots); let mut model = vec![true; n_slots as usize]; for _ in 0..2000usize { match rng.random_range(0..100) { 0..=59 => { let got = ab.alloc_one(); if let Some(i) = got { assert!(i < n_slots, "index in range"); assert!(model[i as usize], "bit must be free"); model[i as usize] = false; } else { assert!( !model.iter().any(|&b| b), "allocator returned None but a free slot exists" ); } } _ => { let idx = rng.random_range(0..model.len()); if !model[idx] { ab.free_one(idx as u32); model[idx] = true; } } } assert_equivalent(&ab, &model); } } } } ================================================ FILE: core/storage/sqlite3_ondisk.rs ================================================ //! SQLite on-disk file format. //! //! SQLite stores data in a single database file, which is divided into fixed-size //! pages: //! //! ```text //! +----------+----------+----------+-----------------------------+----------+ //! | | | | | | //! | Page 1 | Page 2 | Page 3 | ... | Page N | //! | | | | | | //! +----------+----------+----------+-----------------------------+----------+ //! ``` //! //! The first page is special because it contains a 100 byte header at the beginning. //! //! Each page consists of a page header and N cells, which contain the records. //! //! ```text //! +-----------------+----------------+---------------------+----------------+ //! | | | | | //! | Page header | Cell pointer | Unallocated | Cell content | //! | (8 or 12 bytes) | array | space | area | //! | | | | | //! +-----------------+----------------+---------------------+----------------+ //! ``` //! //! The write-ahead log (WAL) is a separate file that contains the physical //! log of changes to a database file. The file starts with a WAL header and //! is followed by a sequence of WAL frames, which are database pages with //! additional metadata. //! //! ```text //! +-----------------+-----------------+-----------------+-----------------+ //! | | | | | //! | WAL header | WAL frame 1 | WAL frame 2 | WAL frame N | //! | | | | | //! +-----------------+-----------------+-----------------+-----------------+ //! ``` //! //! For more information, see the SQLite file format specification: //! //! https://www.sqlite.org/fileformat.html #![allow(clippy::arc_with_non_send_sync)] use crate::{turso_assert, turso_assert_eq, turso_assert_greater_than}; use branches::{mark_unlikely, unlikely}; use bytemuck::{Pod, Zeroable}; use pack1::{I32BE, U16BE, U32BE}; use tracing::{instrument, Level}; use super::pager::PageRef; pub use super::pager::{PageContent, PageInner}; use super::wal::TursoRwLock; use crate::error::LimboError; use crate::fast_lock::SpinLock; use crate::io::{Buffer, Completion, FileSyncType, ReadComplete}; use crate::numeric::Numeric; use crate::storage::btree::{payload_overflow_threshold_max, payload_overflow_threshold_min}; use crate::storage::buffer_pool::BufferPool; use crate::storage::database::{DatabaseStorage, EncryptionOrChecksum}; use crate::storage::pager::Pager; use crate::storage::wal::READMARK_NOT_USED; use crate::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; use crate::sync::Arc; use crate::sync::RwLock; use crate::types::{SerialType, SerialTypeKind, TextRef, TextSubtype, ValueRef}; use crate::{bail_corrupt_error, CompletionError, File, IOContext, Result, WalFileShared}; use rustc_hash::FxHashMap; use std::collections::BTreeMap; use std::pin::Pin; /// The minimum size of a cell in bytes. pub const MINIMUM_CELL_SIZE: usize = 4; pub const CELL_PTR_SIZE_BYTES: usize = 2; pub const INTERIOR_PAGE_HEADER_SIZE_BYTES: usize = 12; pub const LEAF_PAGE_HEADER_SIZE_BYTES: usize = 8; pub const LEFT_CHILD_PTR_SIZE_BYTES: usize = 4; // Freelist trunk page layout: // - Bytes 0-3: Page number of next freelist trunk page (0 if none) // - Bytes 4-7: Number of leaf page pointers on this trunk page // - Bytes 8+: Array of 4-byte leaf page pointers pub const FREELIST_TRUNK_OFFSET_NEXT_TRUNK_PTR: usize = 0; pub const FREELIST_TRUNK_OFFSET_LEAF_COUNT: usize = 4; pub const FREELIST_TRUNK_OFFSET_FIRST_LEAF_PTR: usize = 8; pub const FREELIST_TRUNK_HEADER_SIZE: usize = 8; pub const FREELIST_LEAF_PTR_SIZE: usize = 4; #[derive(PartialEq, Eq, Zeroable, Pod, Clone, Copy, Debug)] #[repr(transparent)] /// Read/Write file format version. pub struct PageSize(U16BE); impl PageSize { pub const MIN: u32 = 512; pub const MAX: u32 = 65536; pub const DEFAULT: u16 = 4096; /// Interpret a user-provided u32 as either a valid page size or None. pub const fn new(size: u32) -> Option { if size < PageSize::MIN || size > PageSize::MAX { return None; } // Page size must be a power of two. if size.count_ones() != 1 { return None; } if size == PageSize::MAX { // Internally, the value 1 represents 65536, since the on-disk value of the page size in the DB header is 2 bytes. return Some(Self(U16BE::new(1))); } Some(Self(U16BE::new(size as u16))) } /// Interpret a u16 on disk (DB file header) as either a valid page size or /// return a corrupt error. pub fn new_from_header_u16(value: u16) -> Result { match value { 1 => Ok(Self(U16BE::new(1))), n => { let Some(size) = Self::new(n as u32) else { bail_corrupt_error!("invalid page size in database header: {n}"); }; Ok(size) } } } pub const fn get(self) -> u32 { match self.0.get() { 1 => Self::MAX, v => v as u32, } } /// Get the raw u16 value stored internally pub const fn get_raw(self) -> u16 { self.0.get() } } impl Default for PageSize { fn default() -> Self { Self(U16BE::new(Self::DEFAULT)) } } #[derive(PartialEq, Eq, Zeroable, Pod, Clone, Copy, Debug)] #[repr(transparent)] /// Read/Write file format version. pub struct CacheSize(I32BE); impl CacheSize { // The negative value means that we store the amount of pages a XKiB of memory can hold. // We can calculate "real" cache size by diving by page size. pub const DEFAULT: i32 = -2000; // Minimum number of pages that cache can hold. pub const MIN: i64 = super::page_cache::MINIMUM_PAGE_CACHE_SIZE_IN_PAGES as i64; // SQLite uses this value as threshold for maximum cache size pub const MAX_SAFE: i64 = 2147450880; pub const fn new(size: i32) -> Self { match size { Self::DEFAULT => Self(I32BE::new(0)), v => Self(I32BE::new(v)), } } pub const fn get(self) -> i32 { match self.0.get() { 0 => Self::DEFAULT, v => v, } } } impl Default for CacheSize { fn default() -> Self { Self(I32BE::new(Self::DEFAULT)) } } /// Read/Write file format version. #[derive(PartialEq, Eq, Clone, Copy, Debug)] #[repr(u8)] pub enum Version { Legacy = 1, Wal = 2, Mvcc = 255, } impl Version { #[inline] pub fn wal(&self) -> bool { matches!(self, Self::Wal) } #[inline] pub fn mvcc(&self) -> bool { matches!(self, Self::Mvcc) } #[inline] pub fn legacy(&self) -> bool { matches!(self, Self::Legacy) } } impl TryFrom for Version { type Error = u8; fn try_from(value: u8) -> std::result::Result { match value { 1 => Ok(Version::Legacy), 2 => Ok(Version::Wal), 255 => Ok(Version::Mvcc), v => Err(v), } } } /// Raw version byte for use in DatabaseHeader where Pod is required. /// Use `Version::try_from(raw.0)` to convert to the validated enum. #[derive(PartialEq, Eq, Zeroable, Pod, Clone, Copy)] #[repr(transparent)] pub struct RawVersion(pub u8); impl RawVersion { pub fn to_version(self) -> std::result::Result { Version::try_from(self.0) } } impl From for RawVersion { fn from(v: Version) -> Self { Self(v as u8) } } impl std::fmt::Debug for RawVersion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.to_version() { Ok(v) => write!(f, "{v:?}"), Err(v) => write!(f, "RawVersion::Invalid({v})"), } } } #[derive(PartialEq, Eq, Zeroable, Pod, Clone, Copy)] #[repr(transparent)] /// Text encoding. pub struct TextEncoding(U32BE); impl TextEncoding { #![allow(non_upper_case_globals)] // SQLite doesn't write the text encoding bytes until the first table is written, so when // opening an empty SQLite file, the encoding bytes will be 0. SQLite considers this to mean UTF-8. pub const Unset: Self = Self(U32BE::new(0)); pub const Utf8: Self = Self(U32BE::new(1)); pub const Utf16Le: Self = Self(U32BE::new(2)); pub const Utf16Be: Self = Self(U32BE::new(3)); pub fn is_utf8(&self) -> bool { self == &Self::Utf8 || self == &Self::Unset } } impl std::fmt::Display for TextEncoding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { Self::Utf8 => f.write_str("UTF-8"), Self::Utf16Le => f.write_str("UTF-16le"), Self::Utf16Be => f.write_str("UTF-16be"), Self(v) => write!(f, "TextEncoding::Invalid({})", v.get()), } } } impl std::fmt::Debug for TextEncoding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { Self::Utf8 => f.write_str("TextEncoding::Utf8"), Self::Utf16Le => f.write_str("TextEncoding::Utf16Le"), Self::Utf16Be => f.write_str("TextEncoding::Utf16Be"), Self(v) => write!(f, "TextEncoding::Invalid({})", v.get()), } } } impl Default for TextEncoding { fn default() -> Self { Self::Utf8 } } #[derive(Pod, Zeroable, Clone, Copy, Debug)] #[repr(C, packed)] /// Database Header Format pub struct DatabaseHeader { /// b"SQLite format 3\0" pub magic: [u8; 16], /// Page size in bytes. Must be a power of two between 512 and 32768 inclusive, or the value 1 representing a page size of 65536. pub page_size: PageSize, /// File format write version. 1 for legacy; 2 for WAL. pub write_version: RawVersion, /// File format read version. 1 for legacy; 2 for WAL. pub read_version: RawVersion, /// Bytes of unused "reserved" space at the end of each page. Usually 0. pub reserved_space: u8, /// Maximum embedded payload fraction. Must be 64. pub max_embed_frac: u8, /// Minimum embedded payload fraction. Must be 32. pub min_embed_frac: u8, /// Leaf payload fraction. Must be 32. pub leaf_frac: u8, /// File change counter. pub change_counter: U32BE, /// Size of the database file in pages. The "in-header database size". pub database_size: U32BE, /// Page number of the first freelist trunk page. pub freelist_trunk_page: U32BE, /// Total number of freelist pages. pub freelist_pages: U32BE, /// The schema cookie. pub schema_cookie: U32BE, /// The schema format number. Supported schema formats are 1, 2, 3, and 4. pub schema_format: U32BE, /// Default page cache size. pub default_page_cache_size: CacheSize, /// The page number of the largest root b-tree page when in auto-vacuum or incremental-vacuum modes, or zero otherwise. pub vacuum_mode_largest_root_page: U32BE, /// Text encoding. pub text_encoding: TextEncoding, /// The "user version" as read and set by the user_version pragma. pub user_version: I32BE, /// True (non-zero) for incremental-vacuum mode. False (zero) otherwise. pub incremental_vacuum_enabled: U32BE, /// The "Application ID" set by PRAGMA application_id. pub application_id: I32BE, /// Reserved for expansion. Must be zero. _padding: [u8; 20], /// The version-valid-for number. pub version_valid_for: U32BE, /// SQLITE_VERSION_NUMBER pub version_number: U32BE, } impl DatabaseHeader { pub const PAGE_ID: usize = 1; pub const SIZE: usize = size_of::(); const _CHECK: () = { assert!(Self::SIZE == 100); }; pub fn usable_space(self) -> usize { (self.page_size.get() as usize) - (self.reserved_space as usize) } } impl Default for DatabaseHeader { fn default() -> Self { Self { magic: *b"SQLite format 3\0", page_size: Default::default(), write_version: RawVersion::from(Version::Wal), read_version: RawVersion::from(Version::Wal), reserved_space: 0, max_embed_frac: 64, min_embed_frac: 32, leaf_frac: 32, change_counter: U32BE::new(1), database_size: U32BE::new(0), freelist_trunk_page: U32BE::new(0), freelist_pages: U32BE::new(0), schema_cookie: U32BE::new(0), schema_format: U32BE::new(4), // latest format, new sqlite3 databases use this format default_page_cache_size: Default::default(), vacuum_mode_largest_root_page: U32BE::new(0), text_encoding: TextEncoding::Utf8, user_version: I32BE::new(0), incremental_vacuum_enabled: U32BE::new(0), application_id: I32BE::new(0), _padding: [0; 20], version_valid_for: U32BE::new(3047000), version_number: U32BE::new(3047000), } } } pub const WAL_HEADER_SIZE: usize = 32; pub const WAL_FRAME_HEADER_SIZE: usize = 24; // magic is a single number represented as WAL_MAGIC_LE but the big endian // counterpart is just the same number with LSB set to 1. pub const WAL_MAGIC_LE: u32 = 0x377f0682; pub const WAL_MAGIC_BE: u32 = 0x377f0683; /// The Write-Ahead Log (WAL) header. /// The first 32 bytes of a WAL file comprise the WAL header. /// The WAL header is divided into the following fields stored in big-endian order. #[derive(Debug, Clone, Copy)] #[repr(C)] // This helps with encoding because rust does not respect the order in structs, so in // this case we want to keep the order pub struct WalHeader { /// Magic number. 0x377f0682 or 0x377f0683 /// If the LSB is 0, checksums are native byte order, else checksums are serialized pub magic: u32, /// WAL format version. Currently 3007000 pub file_format: u32, /// Database page size in bytes. Power of two between 512 and 65536 inclusive pub page_size: u32, /// Checkpoint sequence number. Increases with each checkpoint pub checkpoint_seq: u32, /// Random value used for the first salt in checksum calculations /// TODO: Incremented with each checkpoint pub salt_1: u32, /// Random value used for the second salt in checksum calculations. /// TODO: A different random value for each checkpoint pub salt_2: u32, /// First checksum value in the wal-header pub checksum_1: u32, /// Second checksum value in the wal-header pub checksum_2: u32, } impl WalHeader { pub const fn new() -> Self { let magic = if cfg!(target_endian = "big") { WAL_MAGIC_BE } else { WAL_MAGIC_LE }; WalHeader { magic, file_format: 3007000, page_size: 0, // Signifies WAL header that is not persistent on disk yet. checkpoint_seq: 0, // TODO implement sequence number salt_1: 0, salt_2: 0, checksum_1: 0, checksum_2: 0, } } } impl Default for WalHeader { fn default() -> Self { Self::new() } } /// Immediately following the wal-header are zero or more frames. /// Each frame consists of a 24-byte frame-header followed by bytes of page data. /// The frame-header is six big-endian 32-bit unsigned integer values, as follows: #[allow(dead_code)] #[derive(Debug, Default, Copy, Clone)] pub struct WalFrameHeader { /// Page number pub(crate) page_number: u32, /// For commit records, the size of the database file in pages after the commit. /// For all other records, zero. pub(crate) db_size: u32, /// Salt-1 copied from the WAL header pub(crate) salt_1: u32, /// Salt-2 copied from the WAL header pub(crate) salt_2: u32, /// Checksum-1: Cumulative checksum up through and including this page pub(crate) checksum_1: u32, /// Checksum-2: Second half of the cumulative checksum pub(crate) checksum_2: u32, } impl WalFrameHeader { pub fn is_commit_frame(&self) -> bool { self.db_size > 0 } } #[repr(u8)] #[derive(Debug, PartialEq, Clone, Copy)] pub enum PageType { IndexInterior = 2, TableInterior = 5, IndexLeaf = 10, TableLeaf = 13, } impl PageType { pub fn is_table(&self) -> bool { match self { PageType::IndexInterior | PageType::IndexLeaf => false, PageType::TableInterior | PageType::TableLeaf => true, } } } impl TryFrom for PageType { type Error = LimboError; fn try_from(value: u8) -> Result { match value { 2 => Ok(Self::IndexInterior), 5 => Ok(Self::TableInterior), 10 => Ok(Self::IndexLeaf), 13 => Ok(Self::TableLeaf), _ => { mark_unlikely(); Err(LimboError::Corrupt(format!("Invalid page type: {value}"))) } } } } #[derive(Debug, Clone)] pub struct OverflowCell { pub index: usize, pub payload: Pin>, } /// Send read request for DB page read to the IO /// if allow_empty_read is set, than empty read will be raise error for the page, but will not panic #[instrument(skip_all, level = Level::DEBUG)] pub fn begin_read_page( db_file: &dyn DatabaseStorage, buffer_pool: Arc, page: PageRef, page_idx: usize, allow_empty_read: bool, io_ctx: &IOContext, ) -> Result { tracing::trace!("begin_read_btree_page(page_idx = {})", page_idx); let buf = buffer_pool.get_page(); #[allow(clippy::arc_with_non_send_sync)] let buf = Arc::new(buf); let complete = Box::new(move |res: Result<(Arc, i32), CompletionError>| { let Ok((buf, bytes_read)) = res else { page.clear_locked(); return None; // IO error already captured in completion }; let buf_len = buf.len(); // Handle truncated database files: if we read fewer bytes than expected // (and it's not an intentional empty read), return a ShortRead error. if bytes_read == 0 { if !allow_empty_read { tracing::error!("short read on page {page_idx}: expected {buf_len} bytes, got 0"); page.clear_locked(); return Some(CompletionError::ShortRead { page_idx, expected: buf_len, actual: 0, }); } } else if bytes_read != buf_len as i32 { tracing::error!( "short read on page {page_idx}: expected {buf_len} bytes, got {bytes_read}" ); page.clear_locked(); return Some(CompletionError::ShortRead { page_idx, expected: buf_len, actual: bytes_read as usize, }); } let page = page.clone(); let buffer = if bytes_read == 0 { Arc::new(Buffer::new_temporary(0)) } else { buf }; finish_read_page(page_idx, buffer, page); None }); let c = Completion::new_read(buf, complete); db_file.read_page(page_idx, io_ctx, c) } #[instrument(skip_all, level = Level::DEBUG)] pub fn finish_read_page(page_idx: usize, buffer: Arc, page: PageRef) { tracing::trace!("finish_read_page(page_idx = {page_idx})"); { let inner = page.get(); inner.buffer = Some(buffer); page.clear_locked(); page.set_loaded(); // we set the wal tag only when reading page from log, or in allocate_page, // we clear it here for safety in case page is being re-loaded. page.clear_wal_tag(); } } #[instrument(skip_all, level = Level::DEBUG)] pub fn begin_write_btree_page(pager: &Pager, page: &PageRef) -> Result { tracing::trace!("begin_write_btree_page(page={})", page.get().id); let page_source = &pager.db_file; let page_finish = page.clone(); let page_id = page.get().id; tracing::trace!("begin_write_btree_page(page_id={})", page_id); let buffer = page.get().buffer.clone().expect("buffer not loaded"); let buf_len = buffer.len(); let write_complete = { Box::new(move |res: Result| { let Ok(bytes_written) = res else { return; }; tracing::trace!("finish_write_btree_page"); page_finish.clear_dirty(); turso_assert!( bytes_written == buf_len as i32, "wrote({bytes_written}) != expected({buf_len})" ); }) }; let c = Completion::new_write(write_complete); let io_ctx = pager.io_ctx.read(); page_source.write_page(page_id, buffer, &io_ctx, c) } #[instrument(skip_all, level = Level::DEBUG)] /// Write a batch of pages to the database file. /// /// we have a batch of pages to write, lets say the following: /// (they are already sorted by id thanks to BTreeMap) /// [1,2,3,6,7,9,10,11,12] // /// we want to collect this into runs of: /// [1,2,3], [6,7], [9,10,11,12] /// and submit each run as a `writev` call, /// for 3 total syscalls instead of 9. pub fn write_pages_vectored( pager: &Pager, batch: BTreeMap>, done_flag: Arc, err: Arc>, ) -> Result> { if batch.is_empty() { done_flag.store(true, Ordering::Release); return Ok(Vec::new()); } let page_sz = pager.get_page_size_unchecked().get() as usize; let mut run_count = 0; let mut prev_id = None; for &id in batch.keys() { if let Some(prev) = prev_id { if id != prev + 1 { run_count += 1; } } else { run_count = 1; } prev_id = Some(id); } let runs_left = Arc::new(AtomicUsize::new(run_count)); const EST_BUFF_CAPACITY: usize = 32; let mut run_bufs = Vec::with_capacity(EST_BUFF_CAPACITY); let mut run_start_id: Option = None; let mut completions = Vec::with_capacity(run_count); let mut iter = batch.iter().peekable(); while let Some((id, buffer)) = iter.next() { if run_start_id.is_none() { run_start_id = Some(*id); } run_bufs.push(buffer.clone()); let is_end_of_run = iter.peek().is_none_or(|(next_id, _)| **next_id != id + 1); if !is_end_of_run { continue; } let start_id = run_start_id.take().expect("start id"); let runs_left_cl = runs_left.clone(); let done_cl = done_flag.clone(); let err_cl = err.clone(); let expected_bytes = (page_sz * run_bufs.len()) as i32; let cmp = Completion::new_write(move |res| { // Record error/mismatch, but always resolve the batch progress. match res { Ok(n) => { if n != expected_bytes { let _ = err_cl.set(CompletionError::ShortWrite); tracing::error!( "write_pages_vectored: short write: wrote({n}) != expected({expected_bytes})" ); } } Err(e) => { tracing::error!("write_pages_vectored: write error: {:?}", e); let _ = err_cl.set(e); } } // we have to decrement runs_left on both paths if runs_left_cl.fetch_sub(1, Ordering::AcqRel) == 1 { tracing::debug!("write_pages_vectored: run complete"); done_cl.store(true, Ordering::Release); } }); let io_ctx = pager.io_ctx.read(); let bufs = std::mem::replace(&mut run_bufs, Vec::with_capacity(EST_BUFF_CAPACITY)); match pager .db_file .write_pages(start_id, page_sz, bufs, &io_ctx, cmp) { Ok(c) => completions.push(c), Err(e) => { // We failed to submit this run at all. Mark batch failed+done and cancel already-submitted. let _ = err.set(CompletionError::Aborted); done_flag.store(true, Ordering::Release); pager.io.cancel(&completions)?; pager.io.drain()?; return Err(e); } } } Ok(completions) } #[instrument(skip_all, level = Level::DEBUG)] pub fn begin_sync( db_file: &dyn DatabaseStorage, syncing: Arc, sync_type: FileSyncType, ) -> Result { turso_assert!(!syncing.load(Ordering::SeqCst)); syncing.store(true, Ordering::SeqCst); let completion = Completion::new_sync(move |_| { syncing.store(false, Ordering::SeqCst); }); #[allow(clippy::arc_with_non_send_sync)] db_file.sync(completion, sync_type) } #[allow(clippy::enum_variant_names)] #[derive(Debug, Clone)] pub enum BTreeCell { TableInteriorCell(TableInteriorCell), TableLeafCell(TableLeafCell), IndexInteriorCell(IndexInteriorCell), IndexLeafCell(IndexLeafCell), } #[derive(Debug, Clone)] pub struct TableInteriorCell { pub left_child_page: u32, pub rowid: i64, } #[derive(Debug, Clone)] pub struct TableLeafCell { pub rowid: i64, /// Payload of cell, if it overflows it won't include overflowed payload. pub payload: &'static [u8], /// This is the complete payload size including overflow pages. pub payload_size: u64, pub first_overflow_page: Option, } #[derive(Debug, Clone)] pub struct IndexInteriorCell { pub left_child_page: u32, pub payload: &'static [u8], /// This is the complete payload size including overflow pages. pub payload_size: u64, pub first_overflow_page: Option, } #[derive(Debug, Clone)] pub struct IndexLeafCell { pub payload: &'static [u8], /// This is the complete payload size including overflow pages. pub payload_size: u64, pub first_overflow_page: Option, } /// read_btree_cell contructs a BTreeCell which is basically a wrapper around pointer to the payload of a cell. /// buffer input "page" is static because we want the cell to point to the data in the page in case it has any payload. pub fn read_btree_cell( page: &'static [u8], page_content: &PageContent, pos: usize, usable_size: usize, ) -> Result { let page_type = page_content.page_type()?; let max_local = payload_overflow_threshold_max(page_type, usable_size); let min_local = payload_overflow_threshold_min(page_type, usable_size); match page_type { PageType::IndexInterior => { let mut pos = pos; crate::assert_or_bail_corrupt!( pos + 4 <= page.len(), "cell offset {} out of bounds for page size {}", pos, page.len() ); let left_child_page = u32::from_be_bytes([page[pos], page[pos + 1], page[pos + 2], page[pos + 3]]); pos += 4; let (payload_size, nr) = read_varint(crate::slice_in_bounds_or_corrupt!(page, pos..))?; pos += nr; let (overflows, to_read) = payload_overflows(payload_size as usize, max_local, min_local, usable_size); let to_read = if overflows { to_read } else { page.len() - pos }; crate::assert_or_bail_corrupt!( pos + to_read <= page.len(), "payload range {}..{} out of bounds for page size {}", pos, pos + to_read, page.len() ); let (payload, first_overflow_page) = read_payload(&page[pos..pos + to_read], payload_size as usize)?; Ok(BTreeCell::IndexInteriorCell(IndexInteriorCell { left_child_page, payload, first_overflow_page, payload_size, })) } PageType::TableInterior => { let mut pos = pos; crate::assert_or_bail_corrupt!( pos + 4 <= page.len(), "cell offset {} out of bounds for page size {}", pos, page.len() ); let left_child_page = u32::from_be_bytes([page[pos], page[pos + 1], page[pos + 2], page[pos + 3]]); pos += 4; let (rowid, _) = read_varint(crate::slice_in_bounds_or_corrupt!(page, pos..))?; Ok(BTreeCell::TableInteriorCell(TableInteriorCell { left_child_page, rowid: rowid as i64, })) } PageType::IndexLeaf => { let mut pos = pos; let (payload_size, nr) = read_varint(crate::slice_in_bounds_or_corrupt!(page, pos..))?; pos += nr; let (overflows, to_read) = payload_overflows(payload_size as usize, max_local, min_local, usable_size); let to_read = if overflows { to_read } else { page.len() - pos }; crate::assert_or_bail_corrupt!( pos + to_read <= page.len(), "payload range {}..{} out of bounds for page size {}", pos, pos + to_read, page.len() ); let (payload, first_overflow_page) = read_payload(&page[pos..pos + to_read], payload_size as usize)?; Ok(BTreeCell::IndexLeafCell(IndexLeafCell { payload, first_overflow_page, payload_size, })) } PageType::TableLeaf => { let mut pos = pos; let (payload_size, nr) = read_varint(crate::slice_in_bounds_or_corrupt!(page, pos..))?; pos += nr; let (rowid, nr) = read_varint(crate::slice_in_bounds_or_corrupt!(page, pos..))?; pos += nr; let (overflows, to_read) = payload_overflows(payload_size as usize, max_local, min_local, usable_size); let to_read = if overflows { to_read } else { page.len() - pos }; crate::assert_or_bail_corrupt!( pos + to_read <= page.len(), "payload range {}..{} out of bounds for page size {}", pos, pos + to_read, page.len() ); let (payload, first_overflow_page) = read_payload(&page[pos..pos + to_read], payload_size as usize)?; Ok(BTreeCell::TableLeafCell(TableLeafCell { rowid: rowid as i64, payload, first_overflow_page, payload_size, })) } } } /// read_payload takes in the unread bytearray with the payload size /// and returns the payload on the page, and optionally the first overflow page number. #[allow(clippy::readonly_write_lock)] fn read_payload( unread: &'static [u8], payload_size: usize, ) -> Result<(&'static [u8], Option)> { let cell_len = unread.len(); // We will let overflow be constructed back if needed or requested. if payload_size <= cell_len { // fit within 1 page Ok((&unread[..payload_size], None)) } else { // overflow if cell_len < 4 { bail_corrupt_error!( "overflow cell too small: {} bytes, need at least 4", cell_len ); } let first_overflow_page = u32::from_be_bytes([ unread[cell_len - 4], unread[cell_len - 3], unread[cell_len - 2], unread[cell_len - 1], ]); Ok((&unread[..cell_len - 4], Some(first_overflow_page))) } } #[inline(always)] #[allow(dead_code)] pub fn validate_serial_type(value: u64) -> Result<()> { if !SerialType::u64_is_valid_serial_type(value) { crate::bail_corrupt_error!("Invalid serial type: {}", value); } Ok(()) } /// Reads a value that might reference the buffer it is reading from. Be sure to store RefValue with the buffer /// always. #[inline(always)] pub fn read_value<'a>(buf: &'a [u8], serial_type: SerialType) -> Result<(ValueRef<'a>, usize)> { match serial_type.kind() { SerialTypeKind::Null => Ok((ValueRef::Null, 0)), SerialTypeKind::I8 => { let val = *buf.first().ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("Invalid UInt8 value".into()) })?; Ok((ValueRef::Numeric(Numeric::Integer(val as i8 as i64)), 1)) } SerialTypeKind::I16 => { let bytes: &[u8; 2] = buf.get(..2) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("Invalid BEInt16 value".into()) })?; Ok(( ValueRef::Numeric(Numeric::Integer(i16::from_be_bytes(*bytes) as i64)), 2, )) } SerialTypeKind::I24 => { let bytes: &[u8; 3] = buf.get(..3) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("Invalid BEInt24 value".into()) })?; let sign_extension = (bytes[0] as i8 >> 7) as u8; Ok(( ValueRef::Numeric(Numeric::Integer(i32::from_be_bytes([ sign_extension, bytes[0], bytes[1], bytes[2], ]) as i64)), 3, )) } SerialTypeKind::I32 => { let bytes: &[u8; 4] = buf.get(..4) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("Invalid BEInt32 value".into()) })?; Ok(( ValueRef::Numeric(Numeric::Integer(i32::from_be_bytes(*bytes) as i64)), 4, )) } SerialTypeKind::I48 => { let bytes: &[u8; 6] = buf.get(..6) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("Invalid BEInt48 value".into()) })?; let sign_extension = (bytes[0] as i8 >> 7) as u8; Ok(( ValueRef::Numeric(Numeric::Integer(i64::from_be_bytes([ sign_extension, sign_extension, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], ]))), 6, )) } SerialTypeKind::I64 => { let bytes: &[u8; 8] = buf.get(..8) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("Invalid BEInt64 value".into()) })?; Ok(( ValueRef::Numeric(Numeric::Integer(i64::from_be_bytes(*bytes))), 8, )) } SerialTypeKind::F64 => { let bytes: &[u8; 8] = buf .get(..8) .and_then(|s| s.try_into().ok()) .ok_or_else(|| LimboError::Corrupt("Invalid BEFloat64 value".into()))?; Ok((ValueRef::from_f64(f64::from_be_bytes(*bytes)), 8)) } SerialTypeKind::ConstInt0 => Ok((ValueRef::Numeric(Numeric::Integer(0)), 0)), SerialTypeKind::ConstInt1 => Ok((ValueRef::Numeric(Numeric::Integer(1)), 0)), SerialTypeKind::Blob => { let content_size = serial_type.size(); let data = buf.get(..content_size).ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("Invalid Blob value".into()) })?; Ok((ValueRef::Blob(data), content_size)) } SerialTypeKind::Text => { let content_size = serial_type.size(); let data = buf.get(..content_size).ok_or_else(|| { mark_unlikely(); LimboError::Corrupt(format!( "Invalid String value, length {} < expected length {}", buf.len(), content_size )) })?; // SAFETY: SerialTypeKind is Text so this buffer is a valid string let val = unsafe { std::str::from_utf8_unchecked(data) }; Ok(( ValueRef::Text(TextRef::new(val, TextSubtype::Text)), content_size, )) } } } pub fn read_value_serial_type<'a>( buf: &'a [u8], serial_type: u64, ) -> Result<(ValueRef<'a>, usize)> { match serial_type { 0 => Ok((ValueRef::Null, 0)), 1 => { if buf.is_empty() { mark_unlikely(); crate::bail_corrupt_error!("Invalid 1-byte int"); } Ok((ValueRef::Numeric(Numeric::Integer(buf[0] as i8 as i64)), 1)) } 2 => { if buf.len() < 2 { mark_unlikely(); crate::bail_corrupt_error!("Invalid 2-byte int"); } Ok(( ValueRef::Numeric(Numeric::Integer(i16::from_be_bytes([buf[0], buf[1]]) as i64)), 2, )) } 3 => { if buf.len() < 3 { mark_unlikely(); crate::bail_corrupt_error!("Invalid 3-byte int"); } let sign_extension = if buf[0] <= 0x7F { 0 } else { 0xFF }; Ok(( ValueRef::Numeric(Numeric::Integer(i32::from_be_bytes([ sign_extension, buf[0], buf[1], buf[2], ]) as i64)), 3, )) } 4 => { if buf.len() < 4 { mark_unlikely(); crate::bail_corrupt_error!("Invalid 4-byte int"); } Ok(( ValueRef::Numeric(Numeric::Integer(i32::from_be_bytes([ buf[0], buf[1], buf[2], buf[3], ]) as i64)), 4, )) } 5 => { if buf.len() < 6 { mark_unlikely(); crate::bail_corrupt_error!("Invalid 6-byte int"); } let sign_extension = if buf[0] <= 0x7F { 0 } else { 0xFF }; Ok(( ValueRef::Numeric(Numeric::Integer(i64::from_be_bytes([ sign_extension, sign_extension, buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], ]))), 6, )) } 6 => { if buf.len() < 8 { mark_unlikely(); crate::bail_corrupt_error!("Invalid 8-byte int"); } Ok(( ValueRef::Numeric(Numeric::Integer(i64::from_be_bytes([ buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], ]))), 8, )) } 7 => { if buf.len() < 8 { mark_unlikely(); crate::bail_corrupt_error!("Invalid 8-byte float"); } Ok(( ValueRef::from_f64(f64::from_be_bytes([ buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], ])), 8, )) } 8 => Ok((ValueRef::Numeric(Numeric::Integer(0)), 0)), 9 => Ok((ValueRef::Numeric(Numeric::Integer(1)), 0)), n if n >= 12 => match n % 2 { 0 => { // Blob let content_size = ((n - 12) / 2) as usize; let data = buf.get(..content_size).ok_or_else(|| { mark_unlikely(); LimboError::Corrupt("Invalid Blob value".into()) })?; Ok((ValueRef::Blob(data), content_size)) } 1 => { // Text let content_size = ((n - 13) / 2) as usize; let data = buf.get(..content_size).ok_or_else(|| { mark_unlikely(); LimboError::Corrupt(format!( "Invalid String value, length {} < expected length {}", buf.len(), content_size )) })?; // SAFETY: SerialTypeKind is Text so this buffer is a valid string let val = unsafe { std::str::from_utf8_unchecked(data) }; Ok(( ValueRef::Text(TextRef::new(val, TextSubtype::Text)), content_size, )) } _ => unreachable!(), }, _ => { mark_unlikely(); crate::bail_corrupt_error!("Invalid serial type for integer") } } } #[inline(always)] pub fn read_integer(buf: &[u8], serial_type: u8) -> Result { match serial_type { 1 => { if buf.is_empty() { mark_unlikely(); crate::bail_corrupt_error!("Invalid 1-byte int"); } Ok(buf[0] as i8 as i64) } 2 => { if buf.len() < 2 { mark_unlikely(); crate::bail_corrupt_error!("Invalid 2-byte int"); } Ok(i16::from_be_bytes([buf[0], buf[1]]) as i64) } 3 => { if buf.len() < 3 { mark_unlikely(); crate::bail_corrupt_error!("Invalid 3-byte int"); } let sign_extension = if buf[0] <= 0x7F { 0 } else { 0xFF }; Ok(i32::from_be_bytes([sign_extension, buf[0], buf[1], buf[2]]) as i64) } 4 => { if buf.len() < 4 { mark_unlikely(); crate::bail_corrupt_error!("Invalid 4-byte int"); } Ok(i32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as i64) } 5 => { if buf.len() < 6 { mark_unlikely(); crate::bail_corrupt_error!("Invalid 6-byte int"); } let sign_extension = if buf[0] <= 0x7F { 0 } else { 0xFF }; Ok(i64::from_be_bytes([ sign_extension, sign_extension, buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], ])) } 6 => { if buf.len() < 8 { crate::bail_corrupt_error!("Invalid 8-byte int"); } Ok(i64::from_be_bytes([ buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], ])) } 8 => Ok(0), 9 => Ok(1), _ => { mark_unlikely(); crate::bail_corrupt_error!("Invalid serial type for integer") } } } /// Reads varint integer from the buffer. /// This function is similar to `sqlite3GetVarint32` #[inline(always)] pub fn read_varint(buf: &[u8]) -> Result<(u64, usize)> { let mut v: u64 = 0; for i in 0..8 { match buf.get(i) { Some(c) => { v = (v << 7) + (c & 0x7f) as u64; if (c & 0x80) == 0 { return Ok((v, i + 1)); } } None => { mark_unlikely(); crate::bail_corrupt_error!("Invalid varint"); } } } match buf.get(8) { Some(&c) => { // Values requiring 9 bytes must have non-zero in the top 8 bits (value >= 1<<56). // Since the final value is `(v<<8) + c`, the top 8 bits (v >> 48) must not be 0. // If those are zero, this should be treated as corrupt. // Perf? the comparison + branching happens only in parsing 9-byte varint which is rare. if unlikely((v >> 48) == 0) { bail_corrupt_error!("Invalid varint"); } v = (v << 8) + c as u64; Ok((v, 9)) } None => { mark_unlikely(); bail_corrupt_error!("Invalid varint"); } } } #[inline(always)] /// Reads a varint from the buffer, returning None if more data is needed. pub fn read_varint_partial(buf: &[u8]) -> Result> { let mut v: u64 = 0; for i in 0..8 { let Some(&c) = buf.get(i) else { return Ok(None); }; v = (v << 7) + (c & 0x7f) as u64; if (c & 0x80) == 0 { return Ok(Some((v, i + 1))); } } let Some(&c) = buf.get(8) else { return Ok(None); }; if unlikely((v >> 48) == 0) { bail_corrupt_error!("Invalid varint"); } v = (v << 8) + c as u64; Ok(Some((v, 9))) } /// Compute the length of a varint encoding for a given u64 value. /// /// SQLite varint: bytes 1-8 each carry 7 payload bits (56 total). /// The optional 9th byte carries a full 8 bits (no continuation bit), /// giving 64 bits total. So values needing >56 bits always take 9 bytes. #[inline(always)] pub fn varint_len(value: u64) -> usize { if value <= 0x7f { 1 } else if value > (1u64 << 56) - 1 { 9 } else { let bits = 64 - value.leading_zeros() as usize; bits.div_ceil(7) } } pub fn write_varint(buf: &mut [u8], value: u64) -> usize { if value <= 0x7f { buf[0] = (value & 0x7f) as u8; return 1; } if value <= 0x3fff { buf[0] = (((value >> 7) & 0x7f) | 0x80) as u8; buf[1] = (value & 0x7f) as u8; return 2; } let mut value = value; if (value & ((0xff000000_u64) << 32)) > 0 { buf[8] = value as u8; value >>= 8; for i in (0..8).rev() { buf[i] = ((value & 0x7f) | 0x80) as u8; value >>= 7; } return 9; } let mut encoded: [u8; 9] = [0; 9]; let mut bytes = value; let mut n = 0; while bytes != 0 { let v = 0x80 | (bytes & 0x7f); encoded[n] = v as u8; bytes >>= 7; n += 1; } encoded[0] &= 0x7f; for i in 0..n { buf[i] = encoded[n - 1 - i]; } n } pub fn write_varint_to_vec(value: u64, payload: &mut Vec) { let mut varint = [0u8; 9]; let n = write_varint(&mut varint, value); payload.extend_from_slice(&varint[0..n]); } /// Stream through frames in chunks, building frame_cache incrementally /// Track last valid commit frame for consistency pub fn build_shared_wal( file: &Arc, io: &Arc, ) -> Result>> { let size = file.size()?; let header = Arc::new(SpinLock::new(WalHeader::default())); let read_locks = std::array::from_fn(|_| TursoRwLock::new()); for (i, l) in read_locks.iter().enumerate() { l.write(); l.set_value_exclusive(if i < 2 { 0 } else { READMARK_NOT_USED }); l.unlock(); } let wal_file_shared = Arc::new(RwLock::new(WalFileShared { enabled: AtomicBool::new(true), wal_header: header.clone(), min_frame: AtomicU64::new(0), max_frame: AtomicU64::new(0), nbackfills: AtomicU64::new(0), transaction_count: AtomicU64::new(0), frame_cache: Arc::new(SpinLock::new(FxHashMap::default())), last_checksum: (0, 0), file: Some(file.clone()), read_locks, write_lock: TursoRwLock::new(), loaded: AtomicBool::new(false), checkpoint_lock: TursoRwLock::new(), initialized: AtomicBool::new(false), epoch: AtomicU32::new(0), })); if size < WAL_HEADER_SIZE as u64 { wal_file_shared.write().loaded.store(true, Ordering::SeqCst); return Ok(wal_file_shared); } let reader = Arc::new(StreamingWalReader::new( file.clone(), wal_file_shared.clone(), header, size, )); let h = reader.clone().read_header()?; io.wait_for_completion(h)?; loop { if reader.done.load(Ordering::Acquire) { break; } let offset = reader.off_atomic.load(Ordering::Acquire); if offset >= size { reader.finalize_loading(); break; } let (_read_size, c) = reader.clone().submit_one_chunk(offset)?; io.wait_for_completion(c)?; let new_off = reader.off_atomic.load(Ordering::Acquire); if new_off <= offset { reader.finalize_loading(); break; } } Ok(wal_file_shared) } pub(super) struct StreamingWalReader { file: Arc, wal_shared: Arc>, header: Arc>, file_size: u64, state: RwLock, off_atomic: AtomicU64, page_atomic: AtomicU64, pub(super) done: AtomicBool, } /// Mutable state for streaming reader struct StreamingState { frame_idx: u64, cumulative_checksum: (u32, u32), /// checksum of the last valid commit frame last_valid_checksum: (u32, u32), last_valid_frame: u64, pending_frames: FxHashMap>, page_size: usize, use_native_endian: bool, header_valid: bool, } impl StreamingWalReader { fn new( file: Arc, wal_shared: Arc>, header: Arc>, file_size: u64, ) -> Self { Self { file, wal_shared, header, file_size, off_atomic: AtomicU64::new(0), page_atomic: AtomicU64::new(0), done: AtomicBool::new(false), state: RwLock::new(StreamingState { frame_idx: 1, cumulative_checksum: (0, 0), last_valid_checksum: (0, 0), last_valid_frame: 0, pending_frames: FxHashMap::default(), page_size: 0, use_native_endian: false, header_valid: false, }), } } fn read_header(self: Arc) -> crate::Result { let header_buf = Arc::new(Buffer::new_temporary(WAL_HEADER_SIZE)); let reader = self.clone(); let completion: Box = Box::new(move |res| { let _reader = reader.clone(); _reader.handle_header_read(res); None }); let c = Completion::new_read(header_buf, completion); self.file.pread(0, c) } fn submit_one_chunk(self: Arc, offset: u64) -> crate::Result<(usize, Completion)> { let page_size = self.page_atomic.load(Ordering::Acquire) as usize; if page_size == 0 { return Err(crate::LimboError::InternalError( "page size not initialized".into(), )); } let frame_size = WAL_FRAME_HEADER_SIZE + page_size; if frame_size == 0 { return Err(crate::LimboError::InternalError( "invalid frame size".into(), )); } const BASE: usize = 16 * 1024 * 1024; let aligned = (BASE / frame_size) * frame_size; let read_size = aligned .max(frame_size) .min((self.file_size - offset) as usize); if read_size == 0 { // end-of-file; let caller finalize return Ok((0, Completion::new_yield())); } let buf = Arc::new(Buffer::new_temporary(read_size)); let me = self.clone(); let completion: Box = Box::new(move |res| { tracing::debug!("WAL chunk read complete"); let reader = me.clone(); reader.handle_chunk_read(res); None }); let c = Completion::new_read(buf, completion); let guard = self.file.pread(offset, c)?; Ok((read_size, guard)) } fn handle_header_read(self: Arc, res: Result<(Arc, i32), CompletionError>) { let Ok((buf, bytes_read)) = res else { self.finalize_loading(); return; }; if bytes_read != WAL_HEADER_SIZE as i32 { self.finalize_loading(); return; } let (page_sz, c1, c2, use_native, ok) = { let mut h = self.header.lock(); let s = buf.as_slice(); h.magic = u32::from_be_bytes(s[0..4].try_into().unwrap()); h.file_format = u32::from_be_bytes(s[4..8].try_into().unwrap()); h.page_size = u32::from_be_bytes(s[8..12].try_into().unwrap()); h.checkpoint_seq = u32::from_be_bytes(s[12..16].try_into().unwrap()); h.salt_1 = u32::from_be_bytes(s[16..20].try_into().unwrap()); h.salt_2 = u32::from_be_bytes(s[20..24].try_into().unwrap()); h.checksum_1 = u32::from_be_bytes(s[24..28].try_into().unwrap()); h.checksum_2 = u32::from_be_bytes(s[28..32].try_into().unwrap()); tracing::debug!("WAL header: {:?}", *h); let use_native = cfg!(target_endian = "big") == ((h.magic & 1) != 0); let calc = checksum_wal(&s[0..24], &h, (0, 0), use_native); ( h.page_size, h.checksum_1, h.checksum_2, use_native, calc == (h.checksum_1, h.checksum_2), ) }; if PageSize::new(page_sz).is_none() || !ok { self.finalize_loading(); return; } { let mut st = self.state.write(); st.page_size = page_sz as usize; st.use_native_endian = use_native; st.cumulative_checksum = (c1, c2); st.last_valid_checksum = (c1, c2); st.header_valid = true; } self.off_atomic .store(WAL_HEADER_SIZE as u64, Ordering::Release); self.page_atomic.store(page_sz as u64, Ordering::Release); } fn handle_chunk_read(self: Arc, res: Result<(Arc, i32), CompletionError>) { let Ok((buf, bytes_read)) = res else { self.finalize_loading(); return; }; let buf_slice = &buf.as_slice()[..bytes_read as usize]; // Snapshot salts/endianness once to avoid per-frame header locks let (header_copy, use_native) = { let st = self.state.read(); let h = self.header.lock(); (*h, st.use_native_endian) }; let consumed = self.process_frames(buf_slice, &header_copy, use_native); self.off_atomic.fetch_add(consumed as u64, Ordering::AcqRel); // If we didn’t consume the full chunk, we hit a stop condition if consumed < buf_slice.len() || self.off_atomic.load(Ordering::Acquire) >= self.file_size { self.finalize_loading(); } } // Processes frames from a buffer, returns bytes processed fn process_frames(&self, buf: &[u8], header: &WalHeader, use_native: bool) -> usize { let mut st = self.state.write(); let page_size = st.page_size; let frame_size = WAL_FRAME_HEADER_SIZE + page_size; let mut pos = 0; while pos + frame_size <= buf.len() { let fh = &buf[pos..pos + WAL_FRAME_HEADER_SIZE]; let page = &buf[pos + WAL_FRAME_HEADER_SIZE..pos + frame_size]; let page_no = u32::from_be_bytes(fh[0..4].try_into().unwrap()); let db_size = u32::from_be_bytes(fh[4..8].try_into().unwrap()); let s1 = u32::from_be_bytes(fh[8..12].try_into().unwrap()); let s2 = u32::from_be_bytes(fh[12..16].try_into().unwrap()); let c1 = u32::from_be_bytes(fh[16..20].try_into().unwrap()); let c2 = u32::from_be_bytes(fh[20..24].try_into().unwrap()); tracing::debug!("process_frames: page_no={page_no}, db_size={db_size}, s1={s1}, s2={s2}, c1={c1}, c2={c2}"); if page_no == 0 { tracing::debug!( "process_frames: unexpected page_no, stop reading WAL at initialization phase" ); break; } if s1 != header.salt_1 || s2 != header.salt_2 { tracing::debug!( "process_frames: salt mismatch, stop reading WAL at initialization phase" ); break; } let seed = checksum_wal(&fh[0..8], header, st.cumulative_checksum, use_native); let calc = checksum_wal(page, header, seed, use_native); if calc != (c1, c2) { tracing::debug!( "process_frames: checksum mismatch, stop reading WAL at initialization phase" ); break; } st.cumulative_checksum = calc; let frame_idx = st.frame_idx; st.pending_frames .entry(page_no as u64) .or_default() .push(frame_idx); if db_size > 0 { st.last_valid_frame = st.frame_idx; st.last_valid_checksum = calc; self.flush_pending_frames(&mut st); } st.frame_idx += 1; pos += frame_size; } pos } fn flush_pending_frames(&self, state: &mut StreamingState) { if state.pending_frames.is_empty() { return; } let wfs = self.wal_shared.read(); { let mut frame_cache = wfs.frame_cache.lock(); for (page, mut frames) in state.pending_frames.drain() { // Only include frames up to last valid commit frames.retain(|&f| f <= state.last_valid_frame); if !frames.is_empty() { frame_cache.entry(page).or_default().extend(frames); } } } wfs.max_frame .store(state.last_valid_frame, Ordering::Release); } /// Finalizes the loading process fn finalize_loading(&self) { let mut wfs = self.wal_shared.write(); let st = self.state.read(); let max_frame = st.last_valid_frame; if max_frame > 0 { let mut frame_cache = wfs.frame_cache.lock(); for frames in frame_cache.values_mut() { frames.retain(|&f| f <= max_frame); } frame_cache.retain(|_, frames| !frames.is_empty()); } wfs.max_frame.store(max_frame, Ordering::SeqCst); // use checksum of last valid commit frame, not necessarily the last frame wfs.last_checksum = st.last_valid_checksum; if st.header_valid { wfs.initialized.store(true, Ordering::SeqCst); } wfs.nbackfills.store(0, Ordering::SeqCst); wfs.loaded.store(true, Ordering::SeqCst); self.done.store(true, Ordering::Release); tracing::debug!( "WAL loading complete: {} frames processed, last commit at frame {}", st.frame_idx - 1, max_frame ); } } pub fn begin_read_wal_frame_raw( buffer_pool: &Arc, io: &F, offset: u64, complete: Box, ) -> Result { tracing::trace!("begin_read_wal_frame_raw(offset={})", offset); let buf = Arc::new(buffer_pool.get_wal_frame()); let c = Completion::new_read(buf, complete); let c = io.pread(offset, c)?; Ok(c) } pub fn begin_read_wal_frame( io: &F, offset: u64, buffer_pool: Arc, complete: Box, page_idx: usize, io_ctx: &IOContext, ) -> Result { tracing::trace!( "begin_read_wal_frame(offset={}, page_idx={})", offset, page_idx ); let buf = buffer_pool.get_page(); let buf = Arc::new(buf); match io_ctx.encryption_or_checksum() { EncryptionOrChecksum::Encryption(ctx) => { let encryption_ctx = ctx.clone(); let original_complete = complete; let decrypt_complete = Box::new(move |res: Result<(Arc, i32), CompletionError>| { let Ok((encrypted_buf, bytes_read)) = res else { return original_complete(res); }; turso_assert_greater_than!( bytes_read, 0, "expected to read data for encrypted page", { "page_idx": page_idx } ); match encryption_ctx.decrypt_page(encrypted_buf.as_slice(), page_idx) { Ok(decrypted_data) => { encrypted_buf .as_mut_slice() .copy_from_slice(&decrypted_data); original_complete(Ok((encrypted_buf, bytes_read))) } Err(e) => { tracing::error!( "Failed to decrypt WAL frame data for page_idx={page_idx}: {e}" ); let err = CompletionError::DecryptionError { page_idx }; original_complete(Err(err)); Some(err) } } }); let new_completion = Completion::new_read(buf, decrypt_complete); io.pread(offset, new_completion) } EncryptionOrChecksum::Checksum(ctx) => { let checksum_ctx = ctx.clone(); let original_c = complete; let verify_complete = Box::new(move |res: Result<(Arc, i32), CompletionError>| { let Ok((buf, bytes_read)) = res else { return original_c(res); }; if bytes_read <= 0 { tracing::trace!("Read page {page_idx} with {} bytes", bytes_read); return original_c(Ok((buf, bytes_read))); } match checksum_ctx.verify_checksum(buf.as_mut_slice(), page_idx) { Ok(_) => original_c(Ok((buf, bytes_read))), Err(e) => { mark_unlikely(); tracing::error!( "Failed to verify checksum for page_id={page_idx}: {e}" ); original_c(Err(e)); Some(e) } } }); let c = Completion::new_read(buf, verify_complete); io.pread(offset, c) } EncryptionOrChecksum::None => { let c = Completion::new_read(buf, complete); io.pread(offset, c) } } } pub fn parse_wal_frame_header(frame: &[u8]) -> (WalFrameHeader, &[u8]) { let page_number = u32::from_be_bytes(frame[0..4].try_into().unwrap()); let db_size = u32::from_be_bytes(frame[4..8].try_into().unwrap()); let salt_1 = u32::from_be_bytes(frame[8..12].try_into().unwrap()); let salt_2 = u32::from_be_bytes(frame[12..16].try_into().unwrap()); let checksum_1 = u32::from_be_bytes(frame[16..20].try_into().unwrap()); let checksum_2 = u32::from_be_bytes(frame[20..24].try_into().unwrap()); let header = WalFrameHeader { page_number, db_size, salt_1, salt_2, checksum_1, checksum_2, }; let page = &frame[WAL_FRAME_HEADER_SIZE..]; (header, page) } pub fn prepare_wal_frame( buffer_pool: &Arc, wal_header: &WalHeader, prev_checksums: (u32, u32), page_size: u32, page_number: u32, db_size: u32, page: &[u8], ) -> ((u32, u32), Arc) { tracing::trace!(page_number); let buffer = buffer_pool.get_wal_frame(); let frame = buffer.as_mut_slice(); frame[WAL_FRAME_HEADER_SIZE..].copy_from_slice(page); frame[0..4].copy_from_slice(&page_number.to_be_bytes()); frame[4..8].copy_from_slice(&db_size.to_be_bytes()); frame[8..12].copy_from_slice(&wal_header.salt_1.to_be_bytes()); frame[12..16].copy_from_slice(&wal_header.salt_2.to_be_bytes()); let expects_be = wal_header.magic & 1; let use_native_endian = cfg!(target_endian = "big") as u32 == expects_be; let header_checksum = checksum_wal(&frame[0..8], wal_header, prev_checksums, use_native_endian); let final_checksum = checksum_wal( &frame[WAL_FRAME_HEADER_SIZE..WAL_FRAME_HEADER_SIZE + page_size as usize], wal_header, header_checksum, use_native_endian, ); frame[16..20].copy_from_slice(&final_checksum.0.to_be_bytes()); frame[20..24].copy_from_slice(&final_checksum.1.to_be_bytes()); (final_checksum, Arc::new(buffer)) } pub fn begin_write_wal_header(io: &F, header: &WalHeader) -> Result { tracing::trace!("begin_write_wal_header"); let buffer = { let buffer = Buffer::new_temporary(WAL_HEADER_SIZE); let buf = buffer.as_mut_slice(); buf[0..4].copy_from_slice(&header.magic.to_be_bytes()); buf[4..8].copy_from_slice(&header.file_format.to_be_bytes()); buf[8..12].copy_from_slice(&header.page_size.to_be_bytes()); buf[12..16].copy_from_slice(&header.checkpoint_seq.to_be_bytes()); buf[16..20].copy_from_slice(&header.salt_1.to_be_bytes()); buf[20..24].copy_from_slice(&header.salt_2.to_be_bytes()); buf[24..28].copy_from_slice(&header.checksum_1.to_be_bytes()); buf[28..32].copy_from_slice(&header.checksum_2.to_be_bytes()); #[allow(clippy::arc_with_non_send_sync)] Arc::new(buffer) }; let write_complete = move |res: Result| { let Ok(bytes_written) = res else { return; }; turso_assert!( bytes_written == WAL_HEADER_SIZE as i32, "wal header wrote({bytes_written}) != expected({WAL_HEADER_SIZE})" ); }; #[allow(clippy::arc_with_non_send_sync)] let c = Completion::new_write(write_complete); let c = io.pwrite(0, buffer, c)?; Ok(c) } /// Checks if payload will overflow a cell based on the maximum allowed size. /// It will return the min size that will be stored in that case, /// including overflow pointer /// see e.g. https://github.com/sqlite/sqlite/blob/9591d3fe93936533c8c3b0dc4d025ac999539e11/src/dbstat.c#L371 #[inline] pub fn payload_overflows( payload_size: usize, payload_overflow_threshold_max: usize, payload_overflow_threshold_min: usize, usable_size: usize, ) -> (bool, usize) { if payload_size <= payload_overflow_threshold_max { return (false, 0); } let mut space_left = payload_overflow_threshold_min + (payload_size - payload_overflow_threshold_min) % (usable_size - 4); if space_left > payload_overflow_threshold_max { space_left = payload_overflow_threshold_min; } (true, space_left + 4) } /// The checksum is computed by interpreting the input as an even number of unsigned 32-bit integers: x(0) through x(N). /// The 32-bit integers are big-endian if the magic number in the first 4 bytes of the WAL header is 0x377f0683 /// and the integers are little-endian if the magic number is 0x377f0682. /// The checksum values are always stored in the frame header in a big-endian format regardless of which byte order is used to compute the checksum. /// /// The checksum algorithm only works for content which is a multiple of 8 bytes in length. /// In other words, if the inputs are x(0) through x(N) then N must be odd. /// The checksum algorithm is as follows: /// /// s0 = s1 = 0 /// for i from 0 to n-1 step 2: /// s0 += x(i) + s1; /// s1 += x(i+1) + s0; /// endfor /// /// The outputs s0 and s1 are both weighted checksums using Fibonacci weights in reverse order. /// (The largest Fibonacci weight occurs on the first element of the sequence being summed.) /// The s1 value spans all 32-bit integer terms of the sequence whereas s0 omits the final term. #[inline] pub fn checksum_wal( buf: &[u8], _wal_header: &WalHeader, input: (u32, u32), native_endian: bool, // Sqlite interprets big endian as "native" ) -> (u32, u32) { turso_assert_eq!(buf.len() % 8, 0, "buffer must be a multiple of 8"); let mut s0: u32 = input.0; let mut s1: u32 = input.1; let mut i = 0; if native_endian { while i < buf.len() { let v0 = u32::from_ne_bytes(buf[i..i + 4].try_into().unwrap()); let v1 = u32::from_ne_bytes(buf[i + 4..i + 8].try_into().unwrap()); s0 = s0.wrapping_add(v0.wrapping_add(s1)); s1 = s1.wrapping_add(v1.wrapping_add(s0)); i += 8; } } else { while i < buf.len() { let v0 = u32::from_ne_bytes(buf[i..i + 4].try_into().unwrap()).swap_bytes(); let v1 = u32::from_ne_bytes(buf[i + 4..i + 8].try_into().unwrap()).swap_bytes(); s0 = s0.wrapping_add(v0.wrapping_add(s1)); s1 = s1.wrapping_add(v1.wrapping_add(s0)); i += 8; } } (s0, s1) } impl WalHeader { pub fn as_bytes(&self) -> &[u8] { unsafe { std::mem::transmute::<&WalHeader, &[u8; size_of::()]>(self) } } } #[inline] pub fn read_u32(buf: &[u8], pos: usize) -> u32 { u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]) } #[cfg(test)] mod tests { use crate::Value; use super::*; use rstest::rstest; #[rstest] #[case(&[], SerialType::null(), Value::Null)] #[case(&[255], SerialType::i8(), Value::from_i64(-1))] #[case(&[0x12, 0x34], SerialType::i16(), Value::from_i64(0x1234))] #[case(&[0xFE], SerialType::i8(), Value::from_i64(-2))] #[case(&[0x12, 0x34, 0x56], SerialType::i24(), Value::from_i64(0x123456))] #[case(&[0x12, 0x34, 0x56, 0x78], SerialType::i32(), Value::from_i64(0x12345678))] #[case(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC], SerialType::i48(), Value::from_i64(0x123456789ABC))] #[case(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xFF], SerialType::i64(), Value::from_i64(0x123456789ABCDEFF))] #[case(&[0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18], SerialType::f64(), Value::from_f64(std::f64::consts::PI))] #[case(&[1, 2], SerialType::const_int0(), Value::from_i64(0))] #[case(&[65, 66], SerialType::const_int1(), Value::from_i64(1))] #[case(&[1, 2, 3], SerialType::blob(3), Value::Blob(vec![1, 2, 3]))] #[case(&[], SerialType::blob(0), Value::Blob(vec![]))] // empty blob #[case(&[65, 66, 67], SerialType::text(3), Value::build_text("ABC"))] #[case(&[0x80], SerialType::i8(), Value::from_i64(-128))] #[case(&[0x80, 0], SerialType::i16(), Value::from_i64(-32768))] #[case(&[0x80, 0, 0], SerialType::i24(), Value::from_i64(-8388608))] #[case(&[0x80, 0, 0, 0], SerialType::i32(), Value::from_i64(-2147483648))] #[case(&[0x80, 0, 0, 0, 0, 0], SerialType::i48(), Value::from_i64(-140737488355328))] #[case(&[0x80, 0, 0, 0, 0, 0, 0, 0], SerialType::i64(), Value::from_i64(-9223372036854775808))] #[case(&[0x7f], SerialType::i8(), Value::from_i64(127))] #[case(&[0x7f, 0xff], SerialType::i16(), Value::from_i64(32767))] #[case(&[0x7f, 0xff, 0xff], SerialType::i24(), Value::from_i64(8388607))] #[case(&[0x7f, 0xff, 0xff, 0xff], SerialType::i32(), Value::from_i64(2147483647))] #[case(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff], SerialType::i48(), Value::from_i64(140737488355327))] #[case(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], SerialType::i64(), Value::from_i64(9223372036854775807))] fn test_read_value( #[case] buf: &[u8], #[case] serial_type: SerialType, #[case] expected: Value, ) { let result = read_value(buf, serial_type).unwrap(); assert_eq!(result.0.to_owned(), expected); } #[test] fn test_serial_type_helpers() { assert_eq!( TryInto::::try_into(12u64).unwrap(), SerialType::blob(0) ); assert_eq!( TryInto::::try_into(14u64).unwrap(), SerialType::blob(1) ); assert_eq!( TryInto::::try_into(13u64).unwrap(), SerialType::text(0) ); assert_eq!( TryInto::::try_into(15u64).unwrap(), SerialType::text(1) ); assert_eq!( TryInto::::try_into(16u64).unwrap(), SerialType::blob(2) ); assert_eq!( TryInto::::try_into(17u64).unwrap(), SerialType::text(2) ); } #[rstest] #[case(0, SerialType::null())] #[case(1, SerialType::i8())] #[case(2, SerialType::i16())] #[case(3, SerialType::i24())] #[case(4, SerialType::i32())] #[case(5, SerialType::i48())] #[case(6, SerialType::i64())] #[case(7, SerialType::f64())] #[case(8, SerialType::const_int0())] #[case(9, SerialType::const_int1())] #[case(12, SerialType::blob(0))] #[case(13, SerialType::text(0))] #[case(14, SerialType::blob(1))] #[case(15, SerialType::text(1))] fn test_parse_serial_type(#[case] input: u64, #[case] expected: SerialType) { let result = SerialType::try_from(input).unwrap(); assert_eq!(result, expected); } #[test] fn test_validate_serial_type() { for i in 0..=9 { let result = validate_serial_type(i); assert!(result.is_ok()); } for i in 10..=11 { let result = validate_serial_type(i); assert!(result.is_err()); } for i in 12..=1000 { let result = validate_serial_type(i); assert!(result.is_ok()); } } #[rstest] #[case(&[])] // empty buffer #[case(&[0x80])] // truncated 1-byte with continuation #[case(&[0x80, 0x80])] // truncated 2-byte #[case(&[0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80])] // 9-byte truncated to 8 #[case(&[0x80; 9])] // bits set without end fn test_read_varint_malformed_inputs(#[case] buf: &[u8]) { assert!(read_varint(buf).is_err()); } #[test] fn streaming_reader_ignores_uncommitted_checksums() { let io: Arc = Arc::new(crate::MemoryIO::new()); let file = io .open_file("streaming-reader-wal", crate::OpenFlags::Create, false) .unwrap(); let page_size: usize = 1024; let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE); buffer_pool .finalize_with_page_size(page_size) .expect("initialize buffer pool"); let mut wal_header = WalHeader { magic: WAL_MAGIC_LE, file_format: 3007000, page_size: page_size as u32, checkpoint_seq: 0, salt_1: 0x1234_5678, salt_2: 0x9abc_def0, checksum_1: 0, checksum_2: 0, }; let header_prefix = &wal_header.as_bytes()[..WAL_HEADER_SIZE - 8]; let use_native = (wal_header.magic & 1) != 0; let (c1, c2) = checksum_wal(header_prefix, &wal_header, (0, 0), use_native); wal_header.checksum_1 = c1; wal_header.checksum_2 = c2; io.wait_for_completion(begin_write_wal_header(file.as_ref(), &wal_header).unwrap()) .unwrap(); let page = vec![0xAB; page_size]; let frame_size = WAL_FRAME_HEADER_SIZE + page_size; let mut offset = WAL_HEADER_SIZE as u64; let (commit_checksum, commit_frame) = prepare_wal_frame( &buffer_pool, &wal_header, (wal_header.checksum_1, wal_header.checksum_2), wal_header.page_size, 1, 1, &page, ); let commit_frame_clone = commit_frame.clone(); let c = file .pwrite( offset, commit_frame, Completion::new_write(move |res| { assert_eq!(res.unwrap() as usize, frame_size); let _keep = commit_frame_clone.clone(); }), ) .unwrap(); io.wait_for_completion(c).unwrap(); offset += frame_size as u64; let (after_frame2_checksum, frame2) = prepare_wal_frame( &buffer_pool, &wal_header, commit_checksum, wal_header.page_size, 2, 0, &page, ); let frame2_clone = frame2.clone(); let c = file .pwrite( offset, frame2, Completion::new_write(move |res| { assert_eq!(res.unwrap() as usize, frame_size); let _keep = frame2_clone.clone(); }), ) .unwrap(); io.wait_for_completion(c).unwrap(); offset += frame_size as u64; let (after_frame3_checksum, frame3) = prepare_wal_frame( &buffer_pool, &wal_header, after_frame2_checksum, wal_header.page_size, 3, 0, &page, ); let frame3_clone = frame3.clone(); let c = file .pwrite( offset, frame3, Completion::new_write(move |res| { assert_eq!(res.unwrap() as usize, frame_size); let _keep = frame3_clone.clone(); }), ) .unwrap(); io.wait_for_completion(c).unwrap(); let shared = build_shared_wal(&file, &io).unwrap(); let guard = shared.read(); assert_eq!(guard.max_frame.load(Ordering::Acquire), 1); assert_eq!(guard.last_checksum, commit_checksum); // checksum should only include committed frame. assert_ne!(guard.last_checksum, after_frame3_checksum); let frame_cache = guard.frame_cache.lock(); assert_eq!(frame_cache.get(&1), Some(&vec![1u64])); assert!(frame_cache.get(&2).is_none()); } #[quickcheck_macros::quickcheck] fn varint_len_matches_write_varint(value: u64) -> bool { let mut buf = [0u8; 9]; let written = write_varint(&mut buf, value); varint_len(value) == written } } ================================================ FILE: core/storage/state_machines.rs ================================================ use crate::PageRef; #[derive(Debug, Clone)] pub enum EmptyTableState { Start, ReadPage { page: PageRef }, } #[derive(Debug, Clone, Copy)] pub enum MoveToRightState { Start, ProcessPage, } #[derive(Debug, Clone, Copy)] pub enum SeekToLastState { Start, IsEmpty, } #[derive(Debug, Clone, Copy)] pub enum RewindState { Start, NextRecord, } #[derive(Debug, Clone, Copy)] pub enum AdvanceState { Start, Advance, } #[derive(Debug, Clone, Copy)] pub enum CountState { Start, Loop, Finish, } #[derive(Debug, Clone, Copy)] pub enum SeekEndState { Start, ProcessPage, } #[derive(Debug, Clone, Copy)] pub enum MoveToState { Start, MoveToPage, } ================================================ FILE: core/storage/subjournal.rs ================================================ use crate::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; use crate::{turso_assert, turso_assert_eq}; use crate::{ storage::sqlite3_ondisk::finish_read_page, Buffer, Completion, CompletionError, PageRef, Result, }; #[derive(Clone)] pub struct Subjournal { file: Arc, in_use: Arc, } impl Subjournal { pub fn new(file: Arc) -> Self { Self { file, in_use: Arc::new(AtomicBool::new(false)), } } pub fn try_use(&self) -> Result<()> { let result = self .in_use .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst); if result.is_err() { return Err(crate::LimboError::Busy); } Ok(()) } pub fn stop_use(&self) { let result = self .in_use .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst); turso_assert!( result.is_ok(), "try_start_use must succeed before stop_use call" ); } pub fn in_use(&self) -> bool { self.in_use.load(Ordering::SeqCst) } pub fn write_page( &self, offset: u64, page_size: usize, buffer: Arc, c: Completion, ) -> Result { turso_assert_eq!( buffer.len(), page_size + 4, "buffer length should be page_size + 4 bytes for page id" ); self.file.pwrite(offset, buffer, c) } pub fn read_page_number(&self, offset: u64, page_id_buffer: Arc) -> Result { turso_assert_eq!( page_id_buffer.len(), 4, "page_id_buffer length should be 4 bytes" ); let c = Completion::new_read( page_id_buffer, move |res: Result<(Arc, i32), CompletionError>| { let Ok((buf, bytes_read)) = res else { return None; }; let expected = buf.len(); if bytes_read != expected as i32 { tracing::error!( "subjournal short read: expected {expected} bytes, got {bytes_read}" ); return Some(CompletionError::ShortRead { page_idx: 0, // reading page number header, not a page expected, actual: bytes_read as usize, }); } None }, ); let c = self.file.pread(offset, c)?; Ok(c) } pub fn read_page( &self, offset: u64, buffer: Arc, page: PageRef, page_size: usize, ) -> Result { turso_assert_eq!(buffer.len(), page_size, "buffer length should be page_size"); let c = Completion::new_read( buffer, move |res: Result<(Arc, i32), CompletionError>| { let Ok((buf, bytes_read)) = res else { return None; }; let page_idx = page.get().id; if bytes_read != page_size as i32 { tracing::error!( "subjournal short read on page {page_idx}: expected {page_size} bytes, got {bytes_read}" ); return Some(CompletionError::ShortRead { page_idx, expected: page_size, actual: bytes_read as usize, }); } finish_read_page(page_idx, buf, page.clone()); None }, ); let c = self.file.pread(offset, c)?; Ok(c) } pub fn truncate(&self, offset: u64) -> Result { let c = Completion::new_trunc(move |res: Result| { let Ok(_) = res else { return; }; }); self.file.truncate(offset, c) } } ================================================ FILE: core/storage/wal.rs ================================================ #![allow(clippy::not_unsafe_ptr_arg_deref)] use crate::io::FileSyncType; use crate::sync::Mutex; use crate::sync::OnceLock; use crate::{turso_assert, turso_assert_greater_than, turso_debug_assert}; use branches::mark_unlikely; use rustc_hash::{FxHashMap, FxHashSet}; use std::array; use std::borrow::Cow; use std::collections::BTreeMap; use strum::EnumString; use tracing::{instrument, Level}; use crate::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; use crate::sync::RwLock; use std::fmt::{Debug, Formatter}; use std::{fmt, sync::Arc}; use super::buffer_pool::BufferPool; use super::pager::{PageRef, Pager}; use super::sqlite3_ondisk::{self, checksum_wal, WalHeader, WAL_MAGIC_BE, WAL_MAGIC_LE}; use crate::fast_lock::SpinLock; use crate::io::clock::MonotonicInstant; use crate::io::CompletionGroup; use crate::io::{File, IO}; use crate::storage::database::EncryptionOrChecksum; use crate::storage::sqlite3_ondisk::{ begin_read_wal_frame, begin_read_wal_frame_raw, finish_read_page, prepare_wal_frame, write_pages_vectored, PageSize, WAL_FRAME_HEADER_SIZE, WAL_HEADER_SIZE, }; use crate::types::{IOCompletions, IOResult}; use crate::{ bail_corrupt_error, io_yield_one, Buffer, Completion, CompletionError, IOContext, LimboError, Result, }; /// this contains the frame to rollback to and its associated checksum. #[derive(Debug, Clone)] pub struct RollbackTo { pub frame: u64, pub checksum: (u32, u32), } #[derive(Debug, Clone, Default)] pub struct CheckpointResult { /// max frame in the WAL after checkpoint /// note, that as we TRUNCATE wal outside of the main checkpoint routine - this field will be set to non-zero number even for TRUNCATE mode pub wal_max_frame: u64, /// total amount of frames backfilled to the DB file after checkpoint pub wal_total_backfilled: u64, /// amount of new frames backfilled to the DB file during this checkpoint procedure pub wal_checkpoint_backfilled: u64, /// In the case of everything backfilled, we need to hold the locks until the db /// file is truncated. maybe_guard: Option, pub db_truncate_sent: bool, pub db_sync_sent: bool, /// Whether WAL truncation I/O has been submitted (for TRUNCATE checkpoint mode) pub wal_truncate_sent: bool, /// Whether WAL sync I/O has been submitted after truncation pub wal_sync_sent: bool, } impl Drop for CheckpointResult { fn drop(&mut self) { let _ = self.maybe_guard.take(); } } impl CheckpointResult { pub fn new( wal_max_frame: u64, wal_total_backfilled: u64, wal_checkpoint_backfilled: u64, ) -> Self { Self { wal_max_frame, wal_total_backfilled, wal_checkpoint_backfilled, maybe_guard: None, db_sync_sent: false, db_truncate_sent: false, wal_truncate_sent: false, wal_sync_sent: false, } } pub const fn everything_backfilled(&self) -> bool { self.wal_max_frame == self.wal_total_backfilled } pub fn should_truncate(&self) -> bool { // TRUNCATE should also clear any stale WAL bytes when the log was restarted // (wal_max_frame=0) but the file still contains old frames. self.everything_backfilled() } pub fn release_guard(&mut self) { let _ = self.maybe_guard.take(); } } #[derive(Debug, Copy, Clone, PartialEq, EnumString)] #[strum(ascii_case_insensitive)] pub enum CheckpointMode { /// Checkpoint as many frames as possible without waiting for any database readers or writers to finish, then sync the database file if all frames in the log were checkpointed. /// Passive never blocks readers or writers, only ensures (like all modes do) that there are no other checkpointers. /// /// Optional upper_bound_inclusive parameter can be set in order to checkpoint frames with number no larger than the parameter Passive { upper_bound_inclusive: Option }, /// This mode blocks until there is no database writer and all readers are reading from the most recent database snapshot. It then checkpoints all frames in the log file and syncs the database file. This mode blocks new database writers while it is pending, but new database readers are allowed to continue unimpeded. Full, /// This mode works the same way as `Full` with the addition that after checkpointing the log file it blocks (calls the busy-handler callback) until all readers are reading from the database file only. This ensures that the next writer will restart the log file from the beginning. Like `Full`, this mode blocks new database writer attempts while it is pending, but does not impede readers. Restart, /// This mode works the same way as `Restart` with the addition that it also truncates the log file to zero bytes just prior to a successful return. /// /// Extra parameter can be set in order to perform conditional TRUNCATE: database will be checkpointed and truncated only if max_frames equals to the parameter value /// this behaviour used by sync-engine which consolidate WAL before checkpoint and needs to be sure that no frames will be missed Truncate { upper_bound_inclusive: Option }, } impl CheckpointMode { fn should_restart_log(&self) -> bool { matches!( self, CheckpointMode::Truncate { .. } | CheckpointMode::Restart ) } /// All modes other than Passive require a complete backfilling of all available frames /// from `shared.nbackfills + 1 -> shared.max_frame` fn require_all_backfilled(&self) -> bool { !matches!(self, CheckpointMode::Passive { .. }) } } /// Immutable view of the WAL metadata a connection snapshots from shared state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct WalSnapshot { max_frame: u64, nbackfills: u64, last_checksum: (u32, u32), checkpoint_seq: u32, transaction_count: u64, } impl WalSnapshot { /// First frame that is still visible in the WAL after checkpoint backfill. const fn min_frame(self) -> u64 { self.nbackfills + 1 } } /// Which read-mark, if any, currently protects this connection's snapshot. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ReadGuardKind { None, DbFile, ReadMark(usize), } impl ReadGuardKind { /// Convert the lock index stored on `WalFile` into a semantic guard kind. const fn from_lock_index(lock_index: usize) -> Self { match lock_index { NO_LOCK_HELD => Self::None, 0 => Self::DbFile, idx => Self::ReadMark(idx), } } /// Convert the semantic guard kind back into the legacy lock index representation. const fn lock_index(self) -> usize { match self { Self::None => NO_LOCK_HELD, Self::DbFile => 0, Self::ReadMark(idx) => idx, } } } /// Connection-local WAL state derived from a shared snapshot plus a held read guard. #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct WalConnectionState { snapshot: WalSnapshot, read_guard: ReadGuardKind, } impl WalConnectionState { /// Build a new connection-local WAL state bundle. const fn new(snapshot: WalSnapshot, read_guard: ReadGuardKind) -> Self { Self { snapshot, read_guard, } } /// Replace just the shared snapshot while preserving the current read guard. const fn with_snapshot(self, snapshot: WalSnapshot) -> Self { Self { snapshot, read_guard: self.read_guard, } } } #[repr(transparent)] #[derive(Debug, Default)] /// A 64-bit read-write lock with embedded 32-bit value storage. /// Using a single Atomic allows the reader count and lock state are updated /// atomically together while sitting in a single cpu cache line. /// /// # Memory Layout: /// ```ignore /// [63:32] Value bits - 32 bits for stored value /// [31:1] Reader count - 31 bits for reader count /// [0] Writer bit - 1 bit indicating exclusive write lock /// ``` /// /// # Synchronization Guarantees: /// - Acquire semantics on lock acquisition ensure visibility of all writes /// made by the previous lock holder /// - Release semantics on unlock ensure all writes made while holding the /// lock are visible to the next acquirer /// - The embedded value can be atomically read without holding any lock pub struct TursoRwLock(AtomicU64); pub const READMARK_NOT_USED: u32 = 0xffffffff; const NO_LOCK_HELD: usize = usize::MAX; impl TursoRwLock { /// Bit 0: Writer flag const WRITER: u64 = 0b1; /// Reader increment value (bit 1) const READER_INC: u64 = 0b10; /// Reader count starts at bit 1 const READER_SHIFT: u32 = 1; /// Mask for 31 reader bits [31:1] const READER_COUNT_MASK: u64 = 0x7fff_ffffu64 << Self::READER_SHIFT; /// Value starts at bit 32 const VALUE_SHIFT: u32 = 32; /// Mask for 32 value bits [63:32] const VALUE_MASK: u64 = 0xffff_ffffu64 << Self::VALUE_SHIFT; #[inline] pub const fn new() -> Self { Self(AtomicU64::new(0)) } const fn has_writer(val: u64) -> bool { val & Self::WRITER != 0 } const fn has_readers(val: u64) -> bool { val & Self::READER_COUNT_MASK != 0 } #[inline] /// Try to acquire a shared read lock. pub fn read(&self) -> bool { let mut count = 0; // Bounded loop to avoid infinite loops // Retry on Reader contention (should hopefully be spurious) while count < 1_000_000 { let cur = self.0.load(Ordering::Acquire); // If a writer is present we cannot proceed. if Self::has_writer(cur) { return false; } // 2 billion readers is a high enough number where we will skip the branch // and assume that we are not overflowing :) let desired = cur.wrapping_add(Self::READER_INC); // for success, Acquire establishes happens-before relationship with the previous Release from unlock // for failure we only care about reading it for the next iteration so we can use Relaxed. let res = self .0 .compare_exchange(cur, desired, Ordering::Acquire, Ordering::Relaxed); if res.is_err() { count += 1; crate::thread::spin_loop(); continue; } return true; } // Too much reader contention return Busy false } /// Try to take an exclusive lock. Succeeds if no readers and no writer. #[inline] pub fn write(&self) -> bool { let cur = self.0.load(Ordering::Acquire); // exclusive lock, so require no readers and no writer if Self::has_writer(cur) || Self::has_readers(cur) { return false; } let desired = cur | Self::WRITER; self.0 // Safety: Failure here can be Relaxed as we will read again on next iteration. .compare_exchange(cur, desired, Ordering::Acquire, Ordering::Relaxed) .is_ok() } /// upgrade read lock to the write lock /// only possible if there is exactly single reader at the moment /// return true if lock was upgraded succesfully - and false otherwise #[inline] pub fn upgrade(&self) -> bool { let cur = self.0.load(Ordering::Acquire); // Check for single reader: exactly one reader, any value if (cur & !Self::VALUE_MASK) != Self::READER_INC { return false; } // Preserve value bits, replace reader with writer let desired = (cur & Self::VALUE_MASK) | Self::WRITER; self.0 .compare_exchange(cur, desired, Ordering::Acquire, Ordering::Relaxed) .is_ok() } /// downgrade write lock to the read lock /// MUST be called for a lock acquired by the writer #[inline] pub fn downgrade(&self) { let cur = self.0.load(Ordering::Acquire); turso_debug_assert!(Self::has_writer(cur)); // Preserve value bits, replace writer with one reader let desired = (cur & Self::VALUE_MASK) | Self::READER_INC; self.0.store(desired, Ordering::Release); } #[inline] /// Unlock whatever lock is currently held. /// For write lock: clear writer bit /// For read lock: decrement reader count pub fn unlock(&self) { let cur = self.0.load(Ordering::Acquire); if (cur & Self::WRITER) != 0 { // Clear writer bit, preserve everything else (including value) // Release ordering ensures all our writes are visible to next acquirer let cur = self.0.fetch_and(!Self::WRITER, Ordering::Release); turso_assert!(!Self::has_readers(cur), "write lock was held with readers"); } else { turso_assert!( Self::has_readers(cur), "unlock called with no readers or writers" ); self.0.fetch_sub(Self::READER_INC, Ordering::Release); } } #[inline] /// Read the embedded 32-bit value atomically regardless of slot occupancy. pub fn get_value(&self) -> u32 { (self.0.load(Ordering::Acquire) >> Self::VALUE_SHIFT) as u32 } #[inline] /// Set the embedded value while holding the write lock. pub fn set_value_exclusive(&self, v: u32) { // Must be called only while WRITER bit is set let cur = self.0.load(Ordering::Acquire); turso_assert!(Self::has_writer(cur), "must hold exclusive lock"); let desired = (cur & !Self::VALUE_MASK) | ((v as u64) << Self::VALUE_SHIFT); self.0.store(desired, Ordering::Release); } } /// Represents a batch of WAL frames which will be appended to the log /// with a `pwritev` call and then sync'd to disk. pub struct PreparedFrames { /// File offset for the first frame pub offset: u64, /// Serialized frame buffers pub bufs: Vec>, /// Per-frame metadata: (page_ref, frame_id, cumulative_checksum) pub metadata: Vec<(PageRef, u64, (u32, u32))>, /// Checksum after all frames in this batch pub final_checksum: (u32, u32), /// Max frame ID after this batch pub final_max_frame: u64, /// Epoch at preparation time pub epoch: u32, } /// Write-ahead log (WAL). pub trait Wal: Debug + Send + Sync { /// Begin a read transaction. /// Returns whether the database state has changed since the last read transaction. fn begin_read_tx(&self) -> Result; /// MVCC helper: check if WAL state changed without starting a read tx. fn mvcc_refresh_if_db_changed(&self) -> bool; /// Begin a write transaction. fn begin_write_tx(&self) -> Result<()>; /// End a read transaction. fn end_read_tx(&self); /// End a write transaction. fn end_write_tx(&self); /// Returns true if this WAL instance currently holds a read lock. fn holds_read_lock(&self) -> bool; /// Returns true if this WAL instance currently holds the write lock. fn holds_write_lock(&self) -> bool; /// Find the latest frame containing a page. /// /// optional frame_watermark parameter can be passed to force WAL to find frame not larger than watermark value /// caller must guarantee, that frame_watermark must be greater than last checkpointed frame, otherwise method will panic fn find_frame(&self, page_id: u64, frame_watermark: Option) -> Result>; /// Read a frame from the WAL. fn read_frame( &self, frame_id: u64, page: PageRef, buffer_pool: Arc, ) -> Result; /// Read a raw frame (header included) from the WAL. fn read_frame_raw(&self, frame_id: u64, frame: &mut [u8]) -> Result; /// Write a raw frame (header included) from the WAL. /// Note, that turso-db will use page_no and size_after fields from the header, but will overwrite checksum with proper value fn write_frame_raw( &self, buffer_pool: Arc, frame_id: u64, page_id: u64, db_size: u64, page: &[u8], sync_type: FileSyncType, ) -> Result<()>; /// Prepare WAL header for the future append /// Most of the time this method will return Ok(None) fn prepare_wal_start(&self, page_sz: PageSize) -> Result>; fn prepare_wal_finish(&self, sync_type: FileSyncType) -> Result; /// Prepare a batch of WAL frames for durable commit/append to the log. fn prepare_frames( &self, pages: &[PageRef], page_sz: PageSize, db_size_on_commit: Option, prev: Option<&PreparedFrames>, ) -> Result; /// For each prepared frame, update in-memory WAL index and rolling checksum /// and advance max_frame to make committed frames visible to readers. fn commit_prepared_frames(&self, prepared: &[PreparedFrames]); /// Mark in-memory pages clean and set WAL tags after durable commit. fn finalize_committed_pages(&self, prepared: &[PreparedFrames]); /// Return a handle to the underlying File. fn wal_file(&self) -> Result>; /// Write a bunch of frames to the WAL. /// db_size is the database size in pages after the transaction finishes. /// db_size is set -> last frame written in transaction /// db_size is none -> non-last frame written in transaction fn append_frames_vectored(&self, pages: Vec, page_sz: PageSize) -> Result; /// Complete append of frames by updating shared wal state. Before this /// all changes were stored locally. fn finish_append_frames_commit(&self) -> Result<()>; fn should_checkpoint(&self) -> bool; fn checkpoint(&self, pager: &Pager, mode: CheckpointMode) -> Result>; fn sync(&self, sync_type: FileSyncType) -> Result; fn is_syncing(&self) -> bool; fn get_max_frame_in_wal(&self) -> u64; fn get_checkpoint_seq(&self) -> u32; fn get_max_frame(&self) -> u64; fn get_min_frame(&self) -> u64; fn rollback(&self, rollback_to: Option); fn abort_checkpoint(&self); fn get_last_checksum(&self) -> (u32, u32); /// Return unique set of pages changed **after** frame_watermark position and until current WAL session max_frame_no fn changed_pages_after(&self, frame_watermark: u64) -> Result>; fn set_io_context(&self, ctx: IOContext); /// Update the max frame to the current shared max frame. /// Currently this is only used for MVCC as it takes care of write conflicts on its own. /// This should't be used with regular WAL mode. fn update_max_frame(&self); /// Truncate WAL file to zero and sync it. This is called AFTER the DB file has been /// synced during TRUNCATE checkpoint mode, ensuring data durability. /// The result parameter is used to track I/O progress (wal_truncate_sent, wal_sync_sent). fn truncate_wal( &self, result: &mut CheckpointResult, sync_type: FileSyncType, ) -> Result>; #[cfg(debug_assertions)] fn as_any(&self) -> &dyn std::any::Any; } #[derive(Debug, Clone)] pub enum CheckpointState { Start, Processing, /// Determine the checkpoint result: update nBackfills, restart log if needed. DetermineResult, /// Final cleanup: release locks, clear internal state, return result. /// WAL truncation (if needed) is handled by pager.rs via truncate_wal() AFTER the DB is synced. Finalize { checkpoint_result: Option, }, } /// IOV_MAX is 1024 on most systems, lets use 512 to be safe pub const CKPT_BATCH_PAGES: usize = 512; /// TODO: *ALL* of these need to be tuned for perf. It is tricky /// trying to figure out the ideal numbers here to work together concurrently const MIN_AVG_RUN_FOR_FLUSH: f32 = 32.0; const MIN_BATCH_LEN_FOR_FLUSH: usize = 512; const MAX_INFLIGHT_WRITES: usize = 64; pub const MAX_INFLIGHT_READS: usize = 512; pub const IOV_MAX: usize = 1024; type PageId = usize; struct InflightRead { completion: Completion, page_id: PageId, /// Buffer slot to contain the page content from the WAL read. buf: Arc>>>, } /// WriteBatch is a collection of pages that are being checkpointed together. It is used to /// aggregate contiguous pages into a single write operation to the database file. #[derive(Default)] struct WriteBatch { /// BTreeMap for sorting during insertion, helps create more efficient `writev` operations. items: BTreeMap>, /// total number of `runs`, each representing a contiguous group of `PageId`s run_count: usize, } impl WriteBatch { fn new() -> Self { Self { items: BTreeMap::new(), run_count: 0, } } #[inline] /// Add a pageId + Buffer to the batch of Writes to be submitted. fn insert(&mut self, page_id: PageId, buf: Arc) { if let std::collections::btree_map::Entry::Occupied(mut e) = self.items.entry(page_id) { e.insert(buf); return; } // Single range query to check neighbors let start = page_id.saturating_sub(1); let end = page_id.saturating_add(1); let mut has_left = false; let mut has_right = false; for (k, _) in self.items.range(start..=end) { if *k == page_id.wrapping_sub(1) { has_left = true; } if *k == page_id.wrapping_add(1) { has_right = true; } } match (has_left, has_right) { (false, false) => self.run_count += 1, (true, true) => self.run_count = self.run_count.saturating_sub(1), _ => {} } self.items.insert(page_id, buf); } #[inline] fn len(&self) -> usize { self.items.len() } #[inline] fn is_empty(&self) -> bool { self.items.is_empty() } #[inline] fn is_full(&self) -> bool { self.items.len() >= CKPT_BATCH_PAGES } #[inline] fn avg_run_len(&self) -> f32 { if self.run_count == 0 { 0.0 } else { self.items.len() as f32 / self.run_count as f32 } } #[inline] fn take(&mut self) -> BTreeMap> { self.run_count = 0; std::mem::take(&mut self.items) } #[inline] fn clear(&mut self) { self.items.clear(); self.run_count = 0; } } impl std::ops::Deref for WriteBatch { type Target = BTreeMap>; fn deref(&self) -> &Self::Target { &self.items } } impl std::ops::DerefMut for WriteBatch { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items } } /// Information and structures for processing a checkpoint operation. struct OngoingCheckpoint { /// Used for benchmarking/debugging a checkpoint operation. time: MonotonicInstant, /// minimum frame number to be backfilled by this checkpoint operation. min_frame: u64, /// maximum safe frame number that will be backfilled by this checkpoint operation. max_frame: u64, /// cursor used to iterate through all the pages that might have a frame in the safe range current_page: u64, /// State of the checkpoint state: CheckpointState, /// Batch repreesnts a collection of pages to be backfilled to the DB file. pending_writes: WriteBatch, /// Read operations currently ongoing. inflight_reads: Vec, /// Array of atomic counters representing write operations that are currently in flight. inflight_writes: Vec, /// List of all page_id + frame_id combinations to be backfilled pages_to_checkpoint: Vec<(u64, u64)>, } struct InflightWriteBatch { done: Arc, err: Arc>, } impl OngoingCheckpoint { fn reset(&mut self) { self.min_frame = 0; self.max_frame = 0; self.current_page = 0; self.pages_to_checkpoint.clear(); self.pending_writes.clear(); self.inflight_reads.clear(); self.inflight_writes.clear(); self.state = CheckpointState::Start; } #[inline] /// Whether or not new reads should be issued during checkpoint processing. fn should_issue_reads(&self) -> bool { (self.current_page as usize) < self.pages_to_checkpoint.len() && !self.pending_writes.is_full() && self.inflight_reads.len() < MAX_INFLIGHT_READS } #[inline] /// Whether the backfilling/IO process is entirely completed during checkpoint processing. fn complete(&self) -> bool { (self.current_page as usize) >= self.pages_to_checkpoint.len() && self.inflight_reads.is_empty() && self.pending_writes.is_empty() && self.inflight_writes.is_empty() } #[inline] /// Whether we should flush an exisitng batch of writes and begin concurrently aggregating a new one. fn should_flush_batch(&self) -> bool { self.pending_writes.is_full() || (self.pending_writes.len() >= MIN_BATCH_LEN_FOR_FLUSH && self.pending_writes.avg_run_len() >= MIN_AVG_RUN_FOR_FLUSH) || ((self.current_page as usize) >= self.pages_to_checkpoint.len() && self.inflight_reads.is_empty() && !self.pending_writes.is_empty()) } #[inline] /// Remove any completed write operations from `inflight_writes`, /// returns whether any progress was made. fn process_inflight_writes(&mut self) -> bool { let before_len = self.inflight_writes.len(); self.inflight_writes .retain(|w| !w.done.load(Ordering::Acquire)); before_len > self.inflight_writes.len() } #[inline] /// Remove any completed read operations from `inflight_reads` /// returns whether any progress was made. fn process_pending_reads(&mut self) -> Result { let mut moved = false; let mut err: Option = None; self.inflight_reads.retain(|slot| { if !slot.completion.finished() { return true; } if slot.completion.succeeded() { if let Some(buf) = slot.buf.lock().take() { self.pending_writes.insert(slot.page_id, buf); moved = true; } else { err = Some(CompletionError::IOError(std::io::ErrorKind::Other, "read")); } } else { err = Some( slot.completion .get_error() .unwrap_or(CompletionError::IOError(std::io::ErrorKind::Other, "read")), ); } false }); if let Some(e) = err { return Err(LimboError::CompletionError(e)); } Ok(moved) } fn first_write_error(&self) -> Option where CompletionError: Clone, { self.inflight_writes .iter() .find_map(|w| w.err.get().cloned()) } } impl InflightWriteBatch { #[inline] fn new() -> InflightWriteBatch { InflightWriteBatch { done: Arc::new(AtomicBool::new(false)), err: Arc::new(OnceLock::new()), } } } impl fmt::Debug for OngoingCheckpoint { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("OngoingCheckpoint") .field("state", &self.state) .field("min_frame", &self.min_frame) .field("max_frame", &self.max_frame) .field("current_page", &self.current_page) .finish() } } pub struct WalFile { io: Arc, buffer_pool: Arc, syncing: Arc, write_lock_held: AtomicBool, shared: Arc>, ongoing_checkpoint: RwLock, checkpoint_threshold: usize, /// This is the index to the read_lock in WalFileShared that we are holding. This lock contains /// the max frame for this connection. max_frame_read_lock_index: AtomicUsize, /// Max frame allowed to lookup range=(minframe..max_frame) max_frame: AtomicU64, /// Start of range to look for frames range=(minframe..max_frame) min_frame: AtomicU64, /// Check of last frame in WAL, this is a cumulative checksum over all frames in the WAL last_checksum: RwLock<(u32, u32)>, checkpoint_seq: AtomicU32, transaction_count: AtomicU64, /// Manages locks needed for checkpointing checkpoint_guard: RwLock>, io_ctx: RwLock, } impl fmt::Debug for WalFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("WalFile") .field("syncing", &self.syncing.load(Ordering::Relaxed)) .field("page_size", &self.page_size()) .field("shared", &self.shared) .field("ongoing_checkpoint", &*self.ongoing_checkpoint.read()) .field("checkpoint_threshold", &self.checkpoint_threshold) .field("max_frame_read_lock_index", &self.max_frame_read_lock_index) .field("max_frame", &self.max_frame) .field("min_frame", &self.min_frame) // Excluding other fields .finish() } } /* * sqlite3/src/wal.c * ** nBackfill is the number of frames in the WAL that have been written ** back into the database. (We call the act of moving content from WAL to ** database "backfilling".) The nBackfill number is never greater than ** WalIndexHdr.mxFrame. nBackfill can only be increased by threads ** holding the WAL_CKPT_LOCK lock (which includes a recovery thread). ** However, a WAL_WRITE_LOCK thread can move the value of nBackfill from ** mxFrame back to zero when the WAL is reset. ** ** nBackfillAttempted is the largest value of nBackfill that a checkpoint ** has attempted to achieve. Normally nBackfill==nBackfillAtempted, however ** the nBackfillAttempted is set before any backfilling is done and the ** nBackfill is only set after all backfilling completes. So if a checkpoint ** crashes, nBackfillAttempted might be larger than nBackfill. The ** WalIndexHdr.mxFrame must never be less than nBackfillAttempted. ** ** The aLock[] field is a set of bytes used for locking. These bytes should ** never be read or written. ** ** There is one entry in aReadMark[] for each reader lock. If a reader ** holds read-lock K, then the value in aReadMark[K] is no greater than ** the mxFrame for that reader. The value READMARK_NOT_USED (0xffffffff) ** for any aReadMark[] means that entry is unused. aReadMark[0] is ** a special case; its value is never used and it exists as a place-holder ** to avoid having to offset aReadMark[] indexes by one. Readers holding ** WAL_READ_LOCK(0) always ignore the entire WAL and read all content ** directly from the database. ** ** The value of aReadMark[K] may only be changed by a thread that ** is holding an exclusive lock on WAL_READ_LOCK(K). Thus, the value of ** aReadMark[K] cannot changed while there is a reader is using that mark ** since the reader will be holding a shared lock on WAL_READ_LOCK(K). ** ** The checkpointer may only transfer frames from WAL to database where ** the frame numbers are less than or equal to every aReadMark[] that is ** in use (that is, every aReadMark[j] for which there is a corresponding ** WAL_READ_LOCK(j)). New readers (usually) pick the aReadMark[] with the ** largest value and will increase an unused aReadMark[] to mxFrame if there ** is not already an aReadMark[] equal to mxFrame. The exception to the ** previous sentence is when nBackfill equals mxFrame (meaning that everything ** in the WAL has been backfilled into the database) then new readers ** will choose aReadMark[0] which has value 0 and hence such reader will ** get all their all content directly from the database file and ignore ** the WAL. ** ** Writers normally append new frames to the end of the WAL. However, ** if nBackfill equals mxFrame (meaning that all WAL content has been ** written back into the database) and if no readers are using the WAL ** (in other words, if there are no WAL_READ_LOCK(i) where i>0) then ** the writer will first "reset" the WAL back to the beginning and start ** writing new content beginning at frame 1. */ // TODO(pere): lock only important parts + pin WalFileShared /// WalFileShared is the part of a WAL that will be shared between threads. A wal has information /// that needs to be communicated between threads so this struct does the job. pub struct WalFileShared { pub enabled: AtomicBool, pub wal_header: Arc>, pub min_frame: AtomicU64, pub max_frame: AtomicU64, pub nbackfills: AtomicU64, pub transaction_count: AtomicU64, // Frame cache maps a Page to all the frames it has stored in WAL in ascending order. // This is to easily find the frame it must checkpoint each connection if a checkpoint is // necessary. // One difference between SQLite and limbo is that we will never support multi process, meaning // we don't need WAL's index file. So we can do stuff like this without shared memory. // TODO: this will need refactoring because this is incredible memory inefficient. pub frame_cache: Arc>>>, pub last_checksum: (u32, u32), // Check of last frame in WAL, this is a cumulative checksum over all frames in the WAL pub file: Option>, /// Read locks advertise the maximum WAL frame a reader may access. /// Slot 0 is special, when it is held (shared) the reader bypasses the WAL and uses the main DB file. /// When checkpointing, we must acquire the exclusive read lock 0 to ensure that no readers read /// from a partially checkpointed db file. /// Slots 1‑4 carry a frame‑number in value and may be shared by many readers. Slot 1 is the /// default read lock and is to contain the max_frame in WAL. pub read_locks: [TursoRwLock; 5], /// There is only one write allowed in WAL mode. This lock takes care of ensuring there is only /// one used. pub write_lock: TursoRwLock, /// Serialises checkpointer threads, only one checkpoint can be in flight at any time. Blocking and exclusive only pub checkpoint_lock: TursoRwLock, pub loaded: AtomicBool, pub initialized: AtomicBool, /// Increments on each checkpoint, used to prevent stale cached pages being used for /// backfilling. pub epoch: AtomicU32, } impl fmt::Debug for WalFileShared { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("WalFileShared") .field("enabled", &self.enabled.load(Ordering::Relaxed)) .field("wal_header", &self.wal_header) .field("min_frame", &self.min_frame) .field("max_frame", &self.max_frame) .field("nbackfills", &self.nbackfills) .field("frame_cache", &self.frame_cache) .field("last_checksum", &self.last_checksum) // Excluding `file`, `read_locks`, and `write_lock` .finish() } } #[derive(Clone, Debug)] /// To manage and ensure that no locks are leaked during checkpointing in /// the case of errors. It is held by the WalFile while checkpoint is ongoing /// then transferred to the CheckpointResult if necessary. enum CheckpointLocks { Writer { ptr: Arc> }, Read0 { ptr: Arc> }, } /// Database checkpointers takes the following locks, in order: /// The exclusive CHECKPOINTER lock. /// The exclusive WRITER lock (FULL, RESTART and TRUNCATE only). /// Exclusive lock on read-mark slots 1-N. These are immediately released after being taken. /// Exclusive lock on read-mark 0. /// Exclusive lock on read-mark slots 1-N again. These are immediately released after being taken (RESTART and TRUNCATE only). /// All of the above use blocking locks. impl CheckpointLocks { fn new(ptr: Arc>, mode: CheckpointMode) -> Result { let ptr_clone = ptr.clone(); { let shared = ptr.write(); if !shared.checkpoint_lock.write() { tracing::trace!("CheckpointGuard::new: checkpoint lock failed, returning Busy"); return Err(LimboError::Busy); } match mode { CheckpointMode::Passive { .. } => { if !shared.read_locks[0].write() { shared.checkpoint_lock.unlock(); tracing::trace!("CheckpointGuard: read0 lock failed, returning Busy"); return Err(LimboError::Busy); } } CheckpointMode::Full => { if !shared.read_locks[0].write() { shared.checkpoint_lock.unlock(); tracing::trace!("CheckpointGuard: read0 lock failed (Full), Busy"); return Err(LimboError::Busy); } if !shared.write_lock.write() { shared.read_locks[0].unlock(); shared.checkpoint_lock.unlock(); tracing::trace!("CheckpointGuard: write lock failed (Full), Busy"); return Err(LimboError::Busy); } } CheckpointMode::Restart | CheckpointMode::Truncate { .. } => { if !shared.read_locks[0].write() { shared.checkpoint_lock.unlock(); tracing::trace!("CheckpointGuard: read0 lock failed, returning Busy"); return Err(LimboError::Busy); } if !shared.write_lock.write() { shared.checkpoint_lock.unlock(); shared.read_locks[0].unlock(); tracing::trace!("CheckpointGuard: write lock failed, returning Busy"); return Err(LimboError::Busy); } } } } match mode { CheckpointMode::Passive { .. } => Ok(Self::Read0 { ptr: ptr_clone }), CheckpointMode::Full | CheckpointMode::Restart | CheckpointMode::Truncate { .. } => { Ok(Self::Writer { ptr: ptr_clone }) } } } } impl Drop for CheckpointLocks { fn drop(&mut self) { match self { CheckpointLocks::Writer { ptr: shared } => { let guard = shared.write(); guard.write_lock.unlock(); guard.read_locks[0].unlock(); guard.checkpoint_lock.unlock(); } CheckpointLocks::Read0 { ptr: shared } => { let guard = shared.write(); guard.read_locks[0].unlock(); guard.checkpoint_lock.unlock(); } } } } /// Result of try_begin_read_tx - either success or a retriable condition. enum TryBeginReadResult { /// Successfully started read transaction, returns whether DB changed Ok(bool), /// Transient condition, caller should retry immediately (like SQLite's WAL_RETRY) Retry, } impl WalFile { /// Read the shared WAL metadata that defines a connection snapshot. fn load_shared_snapshot(shared: &WalFileShared) -> WalSnapshot { WalSnapshot { max_frame: shared.max_frame.load(Ordering::Acquire), nbackfills: shared.nbackfills.load(Ordering::Acquire), last_checksum: shared.last_checksum, checkpoint_seq: shared.wal_header.lock().checkpoint_seq, transaction_count: shared.transaction_count.load(Ordering::Acquire), } } /// Reconstruct the connection-local WAL state stored on this `WalFile`. fn connection_state(&self) -> WalConnectionState { WalConnectionState::new( WalSnapshot { max_frame: self.max_frame.load(Ordering::Acquire), nbackfills: self.min_frame.load(Ordering::Acquire).saturating_sub(1), last_checksum: *self.last_checksum.read(), checkpoint_seq: self.checkpoint_seq.load(Ordering::Acquire), transaction_count: self.transaction_count.load(Ordering::Acquire), }, ReadGuardKind::from_lock_index(self.max_frame_read_lock_index.load(Ordering::Acquire)), ) } /// Persist a connection-local WAL snapshot bundle back into the legacy fields on `WalFile`. fn install_connection_state(&self, state: WalConnectionState) { self.max_frame .store(state.snapshot.max_frame, Ordering::Release); self.min_frame .store(state.snapshot.min_frame(), Ordering::Release); *self.last_checksum.write() = state.snapshot.last_checksum; self.checkpoint_seq .store(state.snapshot.checkpoint_seq, Ordering::Release); self.transaction_count .store(state.snapshot.transaction_count, Ordering::Release); self.max_frame_read_lock_index .store(state.read_guard.lock_index(), Ordering::Release); } /// Compare a freshly loaded shared snapshot against the connection's current snapshot. fn db_changed_against(&self, snapshot: WalSnapshot, local_state: WalConnectionState) -> bool { snapshot != local_state.snapshot } /// Try to begin a read transaction. Returns Retry for transient conditions /// that should be retried immediately, Ok for success. fn try_begin_read_tx(&self) -> TryBeginReadResult { turso_assert!( self.max_frame_read_lock_index .load(Ordering::Acquire) .eq(&NO_LOCK_HELD), "cannot start a new read tx without ending an existing one", { "lock_value": self.max_frame_read_lock_index.load(Ordering::Acquire), "expected": NO_LOCK_HELD } ); // Snapshot the shared WAL state. We haven't taken a read lock yet, so we need // to validate these values later. let shared_snapshot = self.with_shared(Self::load_shared_snapshot); tracing::debug!( "try_begin_read_tx: shared_max={}, nbackfills={}, last_checksum={:?}, checkpoint_seq={:?}, transaction_count={}", shared_snapshot.max_frame, shared_snapshot.nbackfills, shared_snapshot.last_checksum, shared_snapshot.checkpoint_seq, shared_snapshot.transaction_count ); // Check if database changed since this connection's last read transaction. // If it has, the connection will invalidate its page cache. let db_changed = self.db_changed_against(shared_snapshot, self.connection_state()); tracing::debug!("try_begin_read_tx: db_changed={}", db_changed); // If WAL is fully checkpointed (shared_max == nbackfills), readers can ignore // the WAL and read directly from the DB file by holding read_locks[0]. if shared_snapshot.max_frame == shared_snapshot.nbackfills { tracing::debug!( "begin_read_tx: WAL fully checkpointed, shared_max={}, nbackfills={}", shared_snapshot.max_frame, shared_snapshot.nbackfills ); if !self.with_shared(|shared| shared.read_locks[0].read()) { tracing::debug!("begin_read_tx: unable to acquire read-0 lock slot, retrying"); return TryBeginReadResult::Retry; } // Re-validate: a writer could have appended frames between our snapshot // and lock acquisition. If so, we cannot proceed because we'd not be reading // up to date committed content from the WAL. let snapshot_after_lock = self.with_shared(Self::load_shared_snapshot); if snapshot_after_lock != shared_snapshot { tracing::debug!( "begin_read_tx: shared data changed ({}, {}, {:?}, {}, {}) != ({}, {}, {:?}, {}, {}), retrying", shared_snapshot.max_frame, shared_snapshot.nbackfills, shared_snapshot.last_checksum, shared_snapshot.checkpoint_seq, shared_snapshot.transaction_count, snapshot_after_lock.max_frame, snapshot_after_lock.nbackfills, snapshot_after_lock.last_checksum, snapshot_after_lock.checkpoint_seq, snapshot_after_lock.transaction_count ); self.with_shared(|shared| shared.read_locks[0].unlock()); return TryBeginReadResult::Retry; } self.install_connection_state(WalConnectionState::new( shared_snapshot, ReadGuardKind::DbFile, )); return TryBeginReadResult::Ok(db_changed); } // If we get this far, it means that the reader will want to use // the WAL to get at content from recent commits. The job now is // to select one of the aReadMark[] entries that is closest to // but not exceeding pWal->hdr.mxFrame and lock that entry. // Find largest mark <= mx among slots 1..N let mut best_idx: i64 = -1; let mut best_mark: u32 = 0; self.with_shared(|shared| { for (idx, lock) in shared.read_locks.iter().enumerate().skip(1) { let m = lock.get_value(); if m != READMARK_NOT_USED && m <= shared_snapshot.max_frame as u32 && m > best_mark { best_mark = m; best_idx = idx as i64; } } }); tracing::debug!( "try_begin_read_tx: best_idx={}, best_mark={}", best_idx, best_mark ); // If none found or lagging, try to claim/update a slot if best_idx == -1 || (best_mark as u64) < shared_snapshot.max_frame { self.with_shared(|shared| { for (idx, lock) in shared.read_locks.iter().enumerate().skip(1) { if !lock.write() { continue; // busy slot } // claim or bump this slot lock.set_value_exclusive(shared_snapshot.max_frame as u32); best_idx = idx as i64; best_mark = shared_snapshot.max_frame as u32; lock.unlock(); break; } }) } // SQLite only requires finding SOME slot (mxI != 0), not that the mark equals mxFrame. // A stale mark is fine - the reader uses shared_max for reading, // and the mark just tells the checkpointer what frames are protected. if best_idx == -1 { return TryBeginReadResult::Retry; } // Now acquire shared read lock on the chosen slot. let read_result = self.with_shared(|shared| { if !shared.read_locks[best_idx as usize].read() { return None; } Some(( Self::load_shared_snapshot(shared), shared.read_locks[best_idx as usize].get_value(), )) }); tracing::debug!("try_begin_read_tx: read_result={:?}", read_result); let Some((snapshot_after_lock, current_slot_mark)) = read_result else { return TryBeginReadResult::Retry; }; // Re-validate state after acquiring the lock. Each check prevents a correctness violation: // // - current_slot_mark != best_mark: Between releasing the exclusive lock (after updating // the slot) and acquiring this shared lock, another thread can exclusively lock and // modify the slot. The checkpointer uses the slot's value to decide how far it can // checkpoint. If the slot now says 700 but we recorded 500, the checkpointer may // overwrite DB pages for frames 501-700 that we expect to read from the WAL. // // - mx2 != shared_max: A writer appended frames. We must retry to see them. // // - nb2 != nbackfills: A checkpointer advanced. We'd set min_frame wrong, potentially // trying to read frames from WAL that were already overwritten. // // - cksm2 != last_checksum: WAL content changed (e.g., rollback reused frame slots). // // - ckpt_seq2 != checkpoint_seq: WAL was reset. Frame numbers are now meaningless. if current_slot_mark != best_mark || snapshot_after_lock != shared_snapshot { self.with_shared(|shared| shared.read_locks[best_idx as usize].unlock()); return TryBeginReadResult::Retry; } self.install_connection_state(WalConnectionState::new( shared_snapshot, ReadGuardKind::ReadMark(best_idx as usize), )); tracing::debug!( "begin_read_tx(min={}, max={}, slot={}, max_frame_in_wal={})", self.min_frame.load(Ordering::Acquire), self.max_frame.load(Ordering::Acquire), best_idx, shared_snapshot.max_frame ); TryBeginReadResult::Ok(db_changed) } } impl Wal for WalFile { fn begin_read_tx(&self) -> Result { // Implement progressive backoff because transient lock contention // should resolve quickly, but under heavy contention busy-spinning wastes // CPU. SQLite uses quadratic backoff after 5 retries, with total delay // up to ~10 seconds before giving up, so we just mirror SQLite's implementation // here. let mut cnt = 0u32; loop { tracing::trace!("begin_read_tx: cnt={cnt}"); match self.try_begin_read_tx() { TryBeginReadResult::Ok(changed) => return Ok(changed), TryBeginReadResult::Retry => { cnt += 1; if cnt > 100 { return Err(LimboError::Busy); } // Progressive backoff: first 5 retries are immediate, then we // start yielding/sleeping with increasing delays. if cnt > 5 { if cnt < 10 { // Retries 6-9: yield to scheduler (minimal delay) self.io.yield_now(); } else { // Retries 10+: quadratic backoff in microseconds // Formula matches SQLite: (cnt-9)^2 * 39 microseconds let delay_us = ((cnt - 9) * (cnt - 9) * 39) as u64; self.io.sleep(std::time::Duration::from_micros(delay_us)); } } continue; } } } } fn mvcc_refresh_if_db_changed(&self) -> bool { WalFile::mvcc_refresh_if_db_changed(self) } /// End a read transaction. #[inline(always)] #[instrument(skip_all, level = Level::DEBUG)] fn end_read_tx(&self) { let slot = self.max_frame_read_lock_index.load(Ordering::Acquire); if slot != NO_LOCK_HELD { self.with_shared(|shared| shared.read_locks[slot].unlock()); self.max_frame_read_lock_index .store(NO_LOCK_HELD, Ordering::Release); tracing::debug!("end_read_tx(slot={slot})"); } else { tracing::debug!("end_read_tx(slot=no_lock)"); } } /// Begin a write transaction #[instrument(skip_all, level = Level::DEBUG)] fn begin_write_tx(&self) -> Result<()> { tracing::debug!("begin_write_tx"); self.with_shared(|shared| { // sqlite/src/wal.c 3702 // Cannot start a write transaction without first holding a read // transaction. // assert(pWal->readLock >= 0); // assert(pWal->writeLock == 0 && pWal->iReCksum == 0); turso_assert!( self.max_frame_read_lock_index.load(Ordering::Acquire) != NO_LOCK_HELD, "must have a read transaction to begin a write transaction" ); turso_assert!( !self.holds_write_lock(), "write lock already held by this connection" ); if !shared.write_lock.write() { return Err(LimboError::Busy); } let db_changed = self.db_changed(shared); if db_changed { // Snapshot is stale, give up and let caller retry from scratch. // Return BusySnapshot instead of Busy so the caller knows it must // restart the read transaction to get a fresh snapshot. // Retrying with busy_timeout will NEVER HELP. tracing::debug!("unable to upgrade transaction from read to write: snapshot is stale, give up and let caller retry from scratch, self.max_frame={}, shared_max={}", self.max_frame.load(Ordering::Acquire), shared.max_frame.load(Ordering::Acquire)); shared.write_lock.unlock(); return Err(LimboError::BusySnapshot); } Ok(()) })?; if self .write_lock_held .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .is_err() { self.with_shared(|shared| shared.write_lock.unlock()); turso_assert!( false, "begin_write_tx called while write lock already held according to connection state" ); } let result = self.try_restart_log_before_write(); if let Err(LimboError::Busy) | Ok(()) = &result { // it's fine if we were unable to restart WAL file due to Busy errors return Ok(()); } // don't forget to release the write-lock if self.with_shared(|shared| { shared.write_lock.unlock(); }); turso_assert!( self.write_lock_held .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire) .is_ok(), "end_write_tx called while write lock not held according to connection state" ); Err(result.expect_err("Ok case handled above")) } /// End a write transaction #[instrument(skip_all, level = Level::DEBUG)] fn end_write_tx(&self) { turso_assert!( self.write_lock_held .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire) .is_ok(), "end_write_tx called while write lock not held according to connection state" ); self.with_shared(|shared| shared.write_lock.unlock()); } /// Returns true if this WAL instance currently holds a read lock. fn holds_read_lock(&self) -> bool { self.max_frame_read_lock_index.load(Ordering::Acquire) != NO_LOCK_HELD } /// Returns true if this WAL instance currently holds the write lock. fn holds_write_lock(&self) -> bool { self.write_lock_held.load(Ordering::Acquire) } /// Find the latest frame containing a page. #[instrument(skip_all, level = Level::DEBUG)] fn find_frame(&self, page_id: u64, frame_watermark: Option) -> Result> { #[cfg(not(feature = "conn_raw_api"))] turso_assert!( frame_watermark.is_none(), "unexpected use of frame_watermark optional argument" ); turso_assert!( frame_watermark.unwrap_or(0) <= self.max_frame.load(Ordering::Acquire), "frame_watermark must be <= than current WAL max_frame value" ); // we can guarantee correctness of the method, only if frame_watermark is strictly after the current checkpointed prefix // // if it's not, than pages from WAL range [frame_watermark..nBackfill] are already in the DB file, // and in case if page first occurrence in WAL was after frame_watermark - we will be unable to read proper previous version of the page self.with_shared(|shared| { let nbackfills = shared.nbackfills.load(Ordering::Acquire); turso_assert!( frame_watermark.is_none() || frame_watermark.unwrap() >= nbackfills, "frame_watermark must be >= than current WAL backfill amount", { "frame_watermark": frame_watermark, "nbackfills": nbackfills } ); }); // if we are holding read_lock 0 and didn't write anything to the WAL, skip and read right from db file. // // note, that max_frame_read_lock_index is set to 0 only when shared_max_frame == nbackfill in which case // min_frame is set to nbackfill + 1 and max_frame is set to shared_max_frame // // by default, SQLite tries to restart log file in this case - but for now let's keep it simple in the turso-db if self.max_frame_read_lock_index.load(Ordering::Acquire) == 0 && self.max_frame.load(Ordering::Acquire) < self.min_frame.load(Ordering::Acquire) { tracing::debug!( "find_frame(page_id={}, frame_watermark={:?}): max_frame is 0 - read from DB file", page_id, frame_watermark, ); return Ok(None); } self.with_shared(|shared| { let frames = shared.frame_cache.lock(); let range = frame_watermark.map(|x| 0..=x).unwrap_or_else(|| { self.min_frame.load(Ordering::Acquire)..=self.max_frame.load(Ordering::Acquire) }); tracing::debug!( "find_frame(page_id={}, frame_watermark={:?}): min_frame={}, max_frame={}", page_id, frame_watermark, self.min_frame.load(Ordering::Acquire), self.max_frame.load(Ordering::Acquire) ); if let Some(list) = frames.get(&page_id) { if let Some(f) = list.iter().rfind(|&&f| range.contains(&f)) { tracing::debug!( "find_frame(page_id={}, frame_watermark={:?}): found frame={}", page_id, frame_watermark, *f ); return Ok(Some(*f)); } } Ok(None) }) } /// Read a frame from the WAL. #[instrument(skip_all, level = Level::DEBUG)] fn read_frame( &self, frame_id: u64, page: PageRef, buffer_pool: Arc, ) -> Result { tracing::debug!( "read_frame(page_idx = {}, frame_id = {})", page.get().id, frame_id ); let offset = self.frame_offset(frame_id); page.set_locked(); let frame = page.clone(); let page_idx = page.get().id; let shared_file = self.shared.clone(); let complete = Box::new(move |res: Result<(Arc, i32), CompletionError>| { let Ok((buf, bytes_read)) = res else { tracing::debug!(err = ?res.unwrap_err()); page.clear_locked(); page.clear_wal_tag(); return None; // IO error already captured in completion }; let buf_len = buf.len(); if bytes_read != buf_len as i32 { tracing::debug!( "WAL short read at offset {offset}, page {page_idx}, frame_id={frame_id}: expected {buf_len} bytes, got {bytes_read}" ); page.clear_locked(); page.clear_wal_tag(); return Some(CompletionError::ShortReadWalFrame { offset, expected: buf_len, actual: bytes_read as usize, }); } let cloned = frame.clone(); finish_read_page(page.get().id, buf, cloned); let epoch = shared_file.read().epoch.load(Ordering::Acquire); frame.set_wal_tag(frame_id, epoch); None }); let file = self.with_shared(|shared| { turso_assert!( shared.enabled.load(Ordering::Relaxed), "WAL must be enabled" ); // important not to hold shared lock beyond this point to avoid deadlock scenario where: // thread 1: takes readlock here, passes reference to shared.file to begin_read_wal_frame // thread 2: tries to acquire write lock elsewhere // thread 1: tries to re-acquire read lock in the completion (see 'complete' above) // // this causes a deadlock due to the locking policy in parking_lot: // from https://docs.rs/parking_lot/latest/parking_lot/type.RwLock.html: // "This lock uses a task-fair locking policy which avoids both reader and writer starvation. // This means that readers trying to acquire the lock will block even if the lock is unlocked // when there are writers waiting to acquire the lock. // Because of this, attempts to recursively acquire a read lock within a single thread may result in a deadlock." shared.file.as_ref().unwrap().clone() }); begin_read_wal_frame( file.as_ref(), offset + WAL_FRAME_HEADER_SIZE as u64, buffer_pool, complete, page_idx, &self.io_ctx.read(), ) } #[instrument(skip_all, level = Level::DEBUG)] // todo(sivukhin): change API to accept Buffer or some other owned type // this method involves IO and cross "async" boundary - so juggling with references is bad and dangerous fn read_frame_raw(&self, frame_id: u64, frame: &mut [u8]) -> Result { tracing::debug!("read_frame_raw({})", frame_id); let offset = self.frame_offset(frame_id); // HACK: *mut u8 can't be Sent between threads safely, cast it to usize then // for the time of writing this comment - this is *safe* as all callers immediately call synchronous method wait_for_completion and hold necessary references let (frame_ptr, frame_len) = (frame.as_mut_ptr() as usize, frame.len()); let encryption_ctx = { let io_ctx = self.io_ctx.read(); io_ctx.encryption_context().cloned() }; let complete = Box::new(move |res: Result<(Arc, i32), CompletionError>| { let Ok((buf, bytes_read)) = res else { return None; // IO error already captured in completion }; let buf_len = buf.len(); if bytes_read != buf_len as i32 { tracing::debug!( "short read on WAL frame {frame_id} at offset {offset}: expected {buf_len} bytes, got {bytes_read}" ); return Some(CompletionError::ShortReadWalFrame { offset, expected: buf_len, actual: bytes_read as usize, }); } let buf_ptr = buf.as_ptr(); let frame_ptr = frame_ptr as *mut u8; let frame_ref: &mut [u8] = unsafe { std::slice::from_raw_parts_mut(frame_ptr, frame_len) }; // Copy the just-read WAL frame into the destination buffer unsafe { std::ptr::copy_nonoverlapping(buf_ptr, frame_ptr, frame_len); } // Now parse the header from the freshly-copied data let (header, raw_page) = sqlite3_ondisk::parse_wal_frame_header(frame_ref); if let Some(ctx) = encryption_ctx.clone() { match ctx.decrypt_page(raw_page, header.page_number as usize) { Ok(decrypted_data) => { turso_assert!( (frame_len - WAL_FRAME_HEADER_SIZE) == decrypted_data.len(), "frame_len minus header_size does not equal expected decrypted data length", { "frame_len_minus_header": frame_len - WAL_FRAME_HEADER_SIZE, "decrypted_data_len": decrypted_data.len() } ); frame_ref[WAL_FRAME_HEADER_SIZE..].copy_from_slice(&decrypted_data); } Err(_) => { tracing::debug!("Failed to decrypt page data for frame_id={frame_id}"); } } } None }); let file = self.with_shared(|shared| { turso_assert!( shared.enabled.load(Ordering::Relaxed), "WAL must be enabled" ); shared.file.as_ref().unwrap().clone() }); let c = begin_read_wal_frame_raw(&self.buffer_pool, file.as_ref(), offset, complete)?; Ok(c) } #[instrument(skip_all, level = Level::DEBUG)] // todo(sivukhin): change API to accept Buffer or some other owned type // this method involves IO and cross "async" boundary - so juggling with references is bad and dangerous fn write_frame_raw( &self, buffer_pool: Arc, frame_id: u64, page_id: u64, db_size: u64, page: &[u8], sync_type: FileSyncType, ) -> Result<()> { let Some(page_size) = PageSize::new(page.len() as u32) else { bail_corrupt_error!("invalid page size: {}", page.len()); }; self.ensure_header_if_needed(page_size, sync_type)?; tracing::debug!("write_raw_frame({})", frame_id); // if page_size wasn't initialized before - we will initialize it during that raw write if self.page_size() != 0 && page.len() != self.page_size() as usize { return Err(LimboError::InvalidArgument(format!( "unexpected page size in frame: got={}, expected={}", page.len(), self.page_size(), ))); } if frame_id > self.max_frame.load(Ordering::Acquire) + 1 { // attempt to write frame out of sequential order - error out return Err(LimboError::InvalidArgument(format!( "frame_id is beyond next frame in the WAL: frame_id={}, max_frame={}", frame_id, self.max_frame.load(Ordering::Acquire) ))); } if frame_id <= self.max_frame.load(Ordering::Acquire) { // just validate if page content from the frame matches frame in the WAL let offset = self.frame_offset(frame_id); let conflict = Arc::new(Mutex::new(false)); // HACK: *mut u8 can't be shared between threads safely, cast it to usize then // for the time of writing this comment - this is *safe* as the function immediately call synchronous method wait_for_completion and hold necessary references let (page_ptr, page_len) = (page.as_ptr() as usize, page.len()); let complete = Box::new({ let conflict = conflict.clone(); move |res: Result<(Arc, i32), CompletionError>| { let Ok((buf, bytes_read)) = res else { return None; // IO error already captured in completion }; let buf_len = buf.len(); if bytes_read != buf_len as i32 { tracing::debug!( "short read on WAL frame validation at offset {offset}, page_id={page_id}: expected {buf_len} bytes, got {bytes_read}" ); return Some(CompletionError::ShortReadWalFrame { offset, expected: buf_len, actual: bytes_read as usize, }); } let page = unsafe { std::slice::from_raw_parts(page_ptr as *mut u8, page_len) }; if buf.as_slice() != page { *conflict.lock() = true; } None } }); let file = self.with_shared(|shared| { turso_assert!( shared.enabled.load(Ordering::Relaxed), "WAL must be enabled" ); shared.file.as_ref().unwrap().clone() }); let c = begin_read_wal_frame( file.as_ref(), offset + WAL_FRAME_HEADER_SIZE as u64, buffer_pool, complete, page_id as usize, &self.io_ctx.read(), )?; self.io.wait_for_completion(c)?; return if *conflict.lock() { Err(LimboError::Conflict(format!( "frame content differs from the WAL: frame_id={frame_id}" ))) } else { Ok(()) }; } // perform actual write let offset = self.frame_offset(frame_id); let (header, file) = self.with_shared(|shared| { let header = shared.wal_header.clone(); turso_assert!( shared.enabled.load(Ordering::Relaxed), "WAL must be enabled" ); let file = shared.file.as_ref().unwrap().clone(); (header, file) }); let header = header.lock(); let checksums = *self.last_checksum.read(); let (checksums, frame_bytes) = prepare_wal_frame( &self.buffer_pool, &header, checksums, header.page_size, page_id as u32, db_size as u32, page, ); let c = Completion::new_write(|_| {}); let c = file.pwrite(offset, frame_bytes, c)?; self.io.wait_for_completion(c)?; self.complete_append_frame(page_id, frame_id, checksums); if db_size > 0 { self.finish_append_frames_commit()?; } Ok(()) } #[instrument(skip_all, level = Level::DEBUG)] fn should_checkpoint(&self) -> bool { self.with_shared(|shared| { let frame_id = shared.max_frame.load(Ordering::Acquire) as usize; let nbackfills = shared.nbackfills.load(Ordering::Acquire) as usize; frame_id > self.checkpoint_threshold + nbackfills }) } #[instrument(skip_all, level = Level::DEBUG)] fn checkpoint( &self, pager: &Pager, mode: CheckpointMode, ) -> Result> { self.checkpoint_inner(pager, mode).inspect_err(|e| { tracing::debug!("Wal Checkpoint failed: {e}"); let _ = self.checkpoint_guard.write().take(); self.ongoing_checkpoint.write().state = CheckpointState::Start; }) } #[instrument(err, skip_all, level = Level::DEBUG)] fn sync(&self, sync_type: FileSyncType) -> Result { tracing::debug!("wal_sync"); let syncing = self.syncing.clone(); let completion = Completion::new_sync(move |result| { tracing::debug!("wal_sync finish"); if let Err(err) = result { tracing::info!("wal_sync failed: {err}"); } syncing.store(false, Ordering::Release); }); let file = self.with_shared(|shared| { turso_assert!( shared.enabled.load(Ordering::Relaxed), "WAL must be enabled" ); shared.file.as_ref().unwrap().clone() }); self.syncing.store(true, Ordering::Release); let c = file.sync(completion, sync_type)?; Ok(c) } // Currently used for assertion purposes fn is_syncing(&self) -> bool { self.syncing.load(Ordering::Acquire) } fn get_max_frame_in_wal(&self) -> u64 { self.with_shared(|shared| shared.max_frame.load(Ordering::Acquire)) } fn get_checkpoint_seq(&self) -> u32 { self.with_shared(|shared| shared.wal_header.lock().checkpoint_seq) } fn get_max_frame(&self) -> u64 { self.max_frame.load(Ordering::Acquire) } fn get_min_frame(&self) -> u64 { self.min_frame.load(Ordering::Acquire) } fn get_last_checksum(&self) -> (u32, u32) { *self.last_checksum.read() } #[instrument(skip_all, level = Level::DEBUG)] fn rollback(&self, rollback_to: Option) { let is_savepoint = rollback_to.is_some(); let (max_frame, last_checksum) = self.with_shared(|shared| { let max_frame = rollback_to .as_ref() .map(|r| r.frame) .unwrap_or_else(|| shared.max_frame.load(Ordering::Acquire)); let last_checksum = rollback_to .as_ref() .map(|r| r.checksum) .unwrap_or(shared.last_checksum); let mut frame_cache = shared.frame_cache.lock(); frame_cache.retain(|_page_id, frames| { // keep frames <= max_frame while frames.last().is_some_and(|&f| f > max_frame) { frames.pop(); } !frames.is_empty() }); (max_frame, last_checksum) }); *self.last_checksum.write() = last_checksum; self.max_frame.store(max_frame, Ordering::Release); if !is_savepoint { self.reset_internal_states(); } } fn abort_checkpoint(&self) { let _ = self.checkpoint_guard.write().take(); self.reset_internal_states(); } #[instrument(skip_all, level = Level::DEBUG)] fn finish_append_frames_commit(&self) -> Result<()> { self.with_shared_mut_dangerous(|shared| { shared .max_frame .store(self.max_frame.load(Ordering::Acquire), Ordering::Release); let last_checksum = *self.last_checksum.read(); tracing::trace!( max_frame = self.max_frame.load(Ordering::Acquire), ?last_checksum ); shared.last_checksum = last_checksum; let new_count = self.transaction_count.fetch_add(1, Ordering::AcqRel) + 1; shared.transaction_count.store(new_count, Ordering::Release); Ok(()) }) } fn changed_pages_after(&self, frame_watermark: u64) -> Result> { let frame_count = self.get_max_frame(); let page_size = self.page_size(); let mut frame = vec![0u8; page_size as usize + WAL_FRAME_HEADER_SIZE]; let mut seen = FxHashSet::default(); turso_assert!( frame_count >= frame_watermark, "frame_count must be not less than frame_watermark", { "frame_count": frame_count, "frame_watermark": frame_watermark } ); let mut pages = Vec::with_capacity((frame_count - frame_watermark) as usize); for frame_no in frame_watermark + 1..=frame_count { let c = self.read_frame_raw(frame_no, &mut frame)?; self.io.wait_for_completion(c)?; let (header, _) = sqlite3_ondisk::parse_wal_frame_header(&frame); if seen.insert(header.page_number) { pages.push(header.page_number); } } Ok(pages) } fn prepare_wal_start(&self, page_size: PageSize) -> Result> { if self.with_shared(|shared| shared.is_initialized())? { return Ok(None); } tracing::debug!("ensure_header_if_needed"); *self.last_checksum.write() = self.with_shared_mut_dangerous(|shared| { let checksum = { let mut hdr = shared.wal_header.lock(); hdr.magic = if cfg!(target_endian = "big") { WAL_MAGIC_BE } else { WAL_MAGIC_LE }; if hdr.page_size == 0 { hdr.page_size = page_size.get(); } if hdr.salt_1 == 0 && hdr.salt_2 == 0 { hdr.salt_1 = self.io.generate_random_number() as u32; hdr.salt_2 = self.io.generate_random_number() as u32; } // recompute header checksum let prefix = &hdr.as_bytes()[..WAL_HEADER_SIZE - 8]; let use_native = (hdr.magic & 1) != 0; let (c1, c2) = checksum_wal(prefix, &hdr, (0, 0), use_native); hdr.checksum_1 = c1; hdr.checksum_2 = c2; (c1, c2) }; shared.last_checksum = checksum; checksum }); self.max_frame.store(0, Ordering::Release); let (header, file) = self.with_shared(|shared| { turso_assert!( shared.enabled.load(Ordering::Relaxed), "WAL must be enabled" ); ( *shared.wal_header.lock(), shared.file.as_ref().unwrap().clone(), ) }); let c = sqlite3_ondisk::begin_write_wal_header(file.as_ref(), &header)?; Ok(Some(c)) } fn prepare_wal_finish(&self, sync_type: FileSyncType) -> Result { let file = self.with_shared(|shared| { turso_assert!( shared.enabled.load(Ordering::Relaxed), "WAL must be enabled" ); shared.file.as_ref().unwrap().clone() }); let shared = self.shared.clone(); let c = file.sync( Completion::new_sync(move |_| { shared.read().initialized.store(true, Ordering::Release); }), sync_type, )?; Ok(c) } /// Prepares a batch of dirty pages as WAL frames without modifying WAL state. /// /// This is the first phase of a three-phase commit protocol: /// 1. prepare (`prepare_frames`) - serialize frames, compute checksums /// 2. write + fsync - caller submits I/O and waits for durability /// 3. commit/finalize (`commit_prepared_frames`) - update WAL index and page metadata /// /// WAL frames form a checksum chain for corruption detection. When writing /// multiple batches in a single transaction, pass the previous batch via `prev` /// to continue the chain. For the first batch, pass `None` to start from /// the committed WAL state. fn prepare_frames( &self, pages: &[PageRef], page_sz: PageSize, db_size_on_commit: Option, prev: Option<&PreparedFrames>, ) -> Result { turso_assert!( pages.len() <= IOV_MAX, "supported up to IOV_MAX pages at once" ); turso_assert!( self.with_shared(|shared| shared.is_initialized())?, "WAL must be initialized" ); let (header, epoch) = self.with_shared(|shared| { let hdr = *shared.wal_header.lock(); let epoch = shared.epoch.load(Ordering::Acquire); (hdr, epoch) }); turso_assert!( header.page_size == page_sz.get(), "page size mismatch between header and requested", { "header_page_size": header.page_size, "requested_page_size": page_sz.get() } ); // Either chain from previous batch of PreparedFrames or use committed WAL state let (mut rolling_checksum, mut next_frame_id) = match prev { Some(p) => (p.final_checksum, p.final_max_frame + 1), None => ( *self.last_checksum.read(), self.max_frame.load(Ordering::Acquire) + 1, ), }; let first_frame_id = next_frame_id; let mut bufs: Vec> = Vec::with_capacity(pages.len()); let mut metadata = Vec::with_capacity(pages.len()); for (idx, page) in pages.iter().enumerate() { let page_id = page.get().id; let plain = page.get_contents().as_ptr(); let data: Cow<[u8]> = { let io_ctx = self.io_ctx.read(); match io_ctx.encryption_or_checksum() { EncryptionOrChecksum::Encryption(ctx) => { Cow::Owned(ctx.encrypt_page(plain, page_id)?) } EncryptionOrChecksum::Checksum(ctx) => { ctx.add_checksum_to_page(plain, page_id)?; Cow::Borrowed(plain) } EncryptionOrChecksum::None => Cow::Borrowed(plain), } }; // if DB size is included for commit frame, it will need to be included only in the last frame of the batch. // however it might not be present in this batch so we cannot assert its presence let frame_db_size = if idx + 1 == pages.len() { db_size_on_commit.unwrap_or(0) } else { 0 }; let (checksum, frame_buf) = prepare_wal_frame( &self.buffer_pool, &header, rolling_checksum, header.page_size, page_id as u32, frame_db_size, &data, ); bufs.push(frame_buf); metadata.push((page.clone(), next_frame_id, checksum)); rolling_checksum = checksum; next_frame_id += 1; } let offset = self.frame_offset(first_frame_id); Ok(PreparedFrames { offset, bufs, metadata, final_checksum: rolling_checksum, final_max_frame: next_frame_id - 1, epoch, }) } /// For each prepared frame, update in-memory WAL index and rolling checksum. /// and advance max_frame to make frames visible to readers. fn commit_prepared_frames(&self, batches: &[PreparedFrames]) { for batch in batches { for (page, frame_id, checksum) in &batch.metadata { // Update WAL index mapping page -> frame self.complete_append_frame(page.get().id as u64, *frame_id, *checksum); } // Update rolling checksum *self.last_checksum.write() = batch.final_checksum; // Advance max_frame and make frames visible to readers self.max_frame .store(batch.final_max_frame, Ordering::Release); } } /// Mark pages clean and set WAL tags after durable commit. fn finalize_committed_pages(&self, prepared: &[PreparedFrames]) { for batch in prepared { for (page, frame_id, _) in &batch.metadata { page.clear_dirty(); page.set_wal_tag(*frame_id, batch.epoch); } } } /// Get WAL file for durable writes. fn wal_file(&self) -> Result> { self.with_shared(|shared| { turso_assert!( shared.enabled.load(Ordering::Relaxed), "WAL must be enabled" ); shared.file.as_ref().cloned().ok_or_else(|| { mark_unlikely(); LimboError::InternalError("WAL file not open".into()) }) }) } /// Use pwritev to append many frames to the log at once. /// /// # Safety: /// this method should only be used for cacheflush/spilling, /// the commit path should use prepare_frames + commit_prepared_frames instead, /// as it prevents prematurely modifing WAL state before durability is ensured. fn append_frames_vectored(&self, pages: Vec, page_sz: PageSize) -> Result { turso_assert!( pages.len() <= IOV_MAX, "we limit number of iovecs to IOV_MAX" ); turso_assert!( self.with_shared(|shared| shared.is_initialized())?, "WAL must be prepared with prepare_wal_start/prepare_wal_finish method" ); let (header, shared_page_size, epoch) = self.with_shared(|shared| { let hdr_guard = shared.wal_header.lock(); let header: WalHeader = *hdr_guard; let shared_page_size = header.page_size; let epoch = shared.epoch.load(Ordering::Acquire); (header, shared_page_size, epoch) }); turso_assert!( shared_page_size == page_sz.get(), "page size mismatch, tried to change page size after WAL header was already initialized", { "shared_page_size": shared_page_size, "page_size": page_sz.get() } ); // Prepare write buffers and bookkeeping let mut iovecs: Vec> = Vec::with_capacity(pages.len()); let mut page_frame_and_checksum: Vec<(PageRef, u64, (u32, u32))> = Vec::with_capacity(pages.len()); // Rolling checksum input to each frame build let mut rolling_checksum: (u32, u32) = *self.last_checksum.read(); let mut next_frame_id = self.max_frame.load(Ordering::Acquire) + 1; // Build every frame in order, updating the rolling checksum for page in pages.iter() { tracing::debug!("append_frames_vectored: page_id={}", page.get().id); let page_id = page.get().id; let plain = page.get_contents().as_ptr(); let data_to_write: std::borrow::Cow<[u8]> = { let io_ctx = self.io_ctx.read(); match &io_ctx.encryption_or_checksum() { EncryptionOrChecksum::Encryption(ctx) => { Cow::Owned(ctx.encrypt_page(plain, page_id)?) } EncryptionOrChecksum::Checksum(ctx) => { ctx.add_checksum_to_page(plain, page_id)?; Cow::Borrowed(plain) } EncryptionOrChecksum::None => Cow::Borrowed(plain), } }; let frame_db_size = 0; // this method is not used for the commit path let (new_checksum, frame_bytes) = prepare_wal_frame( &self.buffer_pool, &header, rolling_checksum, shared_page_size, page_id as u32, frame_db_size, &data_to_write, ); iovecs.push(frame_bytes); // (page, assigned_frame_id, cumulative_checksum_at_this_frame) page_frame_and_checksum.push((page.clone(), next_frame_id, new_checksum)); // Advance for the next frame rolling_checksum = new_checksum; next_frame_id += 1; } let first_frame_id = self.max_frame.load(Ordering::Acquire) + 1; let start_off = self.frame_offset(first_frame_id); // single completion for the whole batch let total_len: i32 = iovecs.iter().map(|b| b.len() as i32).sum(); let page_frame_for_cb = page_frame_and_checksum.clone(); let cmp = move |res: Result| { let Ok(bytes_written) = res else { return; }; turso_assert!( bytes_written == total_len, "pwritev wrote unexpected number of bytes", { "bytes_written": bytes_written, "expected": total_len } ); for (page, fid, _csum) in &page_frame_for_cb { page.clear_dirty(); page.set_wal_tag(*fid, epoch); } }; let c = Completion::new_write(cmp); let file = self.with_shared(|shared| { turso_assert!( shared.enabled.load(Ordering::Relaxed), "WAL must be enabled" ); shared.file.as_ref().unwrap().clone() }); let c = file.pwritev(start_off, iovecs, c)?; self.io.drain()?; for (page, fid, csum) in &page_frame_and_checksum { self.complete_append_frame(page.get().id as u64, *fid, *csum); } Ok(c) } #[cfg(debug_assertions)] fn as_any(&self) -> &dyn std::any::Any { self } fn set_io_context(&self, ctx: IOContext) { *self.io_ctx.write() = ctx; } fn update_max_frame(&self) { self.with_shared(|shared| { let new_max_frame = shared.max_frame.load(Ordering::Acquire); self.max_frame.store(new_max_frame, Ordering::Release); }) } fn truncate_wal( &self, result: &mut CheckpointResult, sync_type: FileSyncType, ) -> Result> { self.truncate_log(result, sync_type) } } impl WalFile { pub fn new( io: Arc, shared: Arc>, (last_checksum, max_frame): ((u32, u32), u64), buffer_pool: Arc, ) -> Self { let now = io.current_time_monotonic(); Self { io, // default to max frame in WAL, so that when we read schema we can read from WAL too if it's there. max_frame: AtomicU64::new(max_frame), shared, ongoing_checkpoint: RwLock::new(OngoingCheckpoint { time: now, pending_writes: WriteBatch::new(), inflight_writes: Vec::new(), state: CheckpointState::Start, min_frame: 0, max_frame: 0, current_page: 0, pages_to_checkpoint: Vec::new(), inflight_reads: Vec::with_capacity(MAX_INFLIGHT_READS), }), checkpoint_threshold: 1000, buffer_pool, checkpoint_seq: AtomicU32::new(0), syncing: Arc::new(AtomicBool::new(false)), write_lock_held: AtomicBool::new(false), min_frame: AtomicU64::new(0), transaction_count: AtomicU64::new(0), max_frame_read_lock_index: AtomicUsize::new(NO_LOCK_HELD), last_checksum: RwLock::new(last_checksum), checkpoint_guard: RwLock::new(None), io_ctx: RwLock::new(IOContext::default()), } } fn page_size(&self) -> u32 { self.with_shared(|shared| shared.wal_header.lock().page_size) } fn frame_offset(&self, frame_id: u64) -> u64 { turso_assert_greater_than!(frame_id, 0, "Frame ID must be 1-based"); let page_offset = (frame_id - 1) * (self.page_size() + WAL_FRAME_HEADER_SIZE as u32) as u64; WAL_HEADER_SIZE as u64 + page_offset } fn _get_shared_mut(&self) -> crate::sync::RwLockWriteGuard<'_, WalFileShared> { // WASM in browser main thread doesn't have a way to "park" a thread // so, we spin way here instead of calling blocking lock #[cfg(target_family = "wasm")] { loop { let Some(lock) = self.shared.try_write() else { std::hint::spin_loop(); continue; }; return lock; } } #[cfg(not(target_family = "wasm"))] { self.shared.write() } } fn _get_shared(&self) -> crate::sync::RwLockReadGuard<'_, WalFileShared> { // WASM in browser main thread doesn't have a way to "park" a thread // so, we spin way here instead of calling blocking lock #[cfg(target_family = "wasm")] { loop { let Some(lock) = self.shared.try_read() else { std::hint::spin_loop(); continue; }; return lock; } } #[cfg(not(target_family = "wasm"))] { self.shared.read() } } #[inline] /// Get a mutable shared lock on the WAL file shared state. /// Be very intentional about when you need this because it can easily cause a deadlock. /// If you're modifying e.g. the WAL locks, all of those operations are atomic and do not /// need shared_mut. fn with_shared_mut_dangerous(&self, func: F) -> R where F: FnOnce(&mut WalFileShared) -> R, { let mut shared = self._get_shared_mut(); func(&mut shared) } #[inline] fn with_shared(&self, func: F) -> R where F: FnOnce(&WalFileShared) -> R, { let shared = self._get_shared(); func(&shared) } fn increment_checkpoint_epoch(&self) { self.with_shared(|shared| { let prev = shared.epoch.fetch_add(1, Ordering::Release); tracing::debug!("increment checkpoint epoch: prev={}", prev); }); } fn complete_append_frame(&self, page_id: u64, frame_id: u64, checksums: (u32, u32)) { *self.last_checksum.write() = checksums; self.max_frame.store(frame_id, Ordering::Release); self.with_shared(|shared| { let mut frame_cache = shared.frame_cache.lock(); match frame_cache.get_mut(&page_id) { Some(frames) => { frames.push(frame_id); } None => { frame_cache.insert(page_id, vec![frame_id]); } } }) } /// Reset connection-private WAL state. fn reset_internal_states(&self) { self.ongoing_checkpoint.write().reset(); self.syncing.store(false, Ordering::Release); } /// the WAL file has been truncated and we are writing the first /// frame since then. We need to ensure that the header is initialized. fn ensure_header_if_needed(&self, page_size: PageSize, sync_type: FileSyncType) -> Result<()> { let Some(c) = self.prepare_wal_start(page_size)? else { return Ok(()); }; self.io.wait_for_completion(c)?; let c = self.prepare_wal_finish(sync_type)?; self.io.wait_for_completion(c)?; Ok(()) } fn checkpoint_inner( &self, pager: &Pager, mode: CheckpointMode, ) -> Result> { loop { let state = self.ongoing_checkpoint.read().state.clone(); tracing::debug!(?state); match state { // Acquire the relevant exclusive locks and checkpoint_lock // so no other checkpointer can run. fsync WAL if there are unapplied frames. // Decide the largest frame we are allowed to back‑fill. CheckpointState::Start => { let (max_frame, nbackfills) = self.with_shared(|shared| { let max_frame = shared.max_frame.load(Ordering::Acquire); let n_backfills = shared.nbackfills.load(Ordering::Acquire); (max_frame, n_backfills) }); tracing::debug!("shared_wal: max_frame={max_frame}, nbackfills={nbackfills}"); let needs_backfill = max_frame > nbackfills; if !needs_backfill && !mode.should_restart_log() { // there are no frames to copy over and we don't need to reset // the log so we can return early success. return Ok(IOResult::Done(CheckpointResult::new( max_frame, nbackfills, 0, ))); } // acquire the appropriate exclusive locks depending on the checkpoint mode self.acquire_proper_checkpoint_guard(mode)?; let mut max_frame = self.determine_max_safe_checkpoint_frame(); if let CheckpointMode::Truncate { upper_bound_inclusive: Some(upper_bound), } = mode { if max_frame > upper_bound { tracing::info!("abort checkpoint because latest frame in WAL is greater than upper_bound in TRUNCATE mode: {max_frame} != {upper_bound}"); return Err(LimboError::Busy); } } if let CheckpointMode::Passive { upper_bound_inclusive: Some(upper_bound), } = mode { max_frame = max_frame.min(upper_bound); } { let mut oc = self.ongoing_checkpoint.write(); oc.max_frame = max_frame; oc.min_frame = nbackfills + 1; } let (oc_min_frame, oc_max_frame) = { let oc = self.ongoing_checkpoint.read(); (oc.min_frame, oc.max_frame) }; tracing::debug!("checkpoint_inner::Start: min_frame={oc_min_frame}, max_frame={oc_max_frame}"); let to_checkpoint = self.with_shared(|shared| { let frame_cache = shared.frame_cache.lock(); let mut list = Vec::with_capacity( oc_max_frame.checked_sub(nbackfills).unwrap_or_default() as usize, ); for (&page_id, frames) in frame_cache.iter() { // for each page in the frame cache, grab the last (latest) frame for // that page that falls in the range of our safe min..max frame if let Some(&frame) = frames .iter() .rev() .find(|&&f| f >= oc_min_frame && f <= oc_max_frame) { list.push((page_id, frame)); } } // sort by frame_id for read locality list.sort_unstable_by(|a, b| (a.1, a.0).cmp(&(b.1, b.0))); list }); { let mut oc = self.ongoing_checkpoint.write(); oc.pages_to_checkpoint = to_checkpoint; oc.current_page = 0; oc.inflight_writes.clear(); oc.inflight_reads.clear(); oc.state = CheckpointState::Processing; oc.time = self.io.current_time_monotonic(); } tracing::trace!( "checkpoint_start(min_frame={}, max_frame={})", oc_min_frame, oc_max_frame, ); } // For locality, reading is ordered by frame ID, and writing ordered by page ID. // the more consecutive page ID's that we submit together, the fewer overall // write/writev syscalls made. All I/O during checkpointing is now in a single step // to prevent serialization, and we try to issue reads and flush batches concurrently // if at all possible, at the cost of some batching potential. CheckpointState::Processing => { // Gather I/O completions using a completion group let mut nr_completions = 0; let mut group = CompletionGroup::new(|_| {}); let mut ongoing_chkpt = self.ongoing_checkpoint.write(); // Check and clean any completed writes from pending flush if ongoing_chkpt.process_inflight_writes() { tracing::trace!("Completed a write batch"); } // Process completed reads into current batch if ongoing_chkpt.process_pending_reads()? { tracing::trace!("Drained reads into batch"); } if let Some(e) = ongoing_chkpt.first_write_error() { mark_unlikely(); // cancel everything still in-flight to avoid leaks let to_cancel: Vec = ongoing_chkpt .inflight_reads .iter() .map(|r| r.completion.clone()) .collect(); pager.io.cancel(&to_cancel)?; pager.io.drain()?; return Err(LimboError::CompletionError(e)); } let epoch = self.with_shared(|shared| shared.epoch.load(Ordering::Acquire)); // Issue reads until we hit limits 'inner: while ongoing_chkpt.should_issue_reads() { let (page_id, target_frame) = { ongoing_chkpt.pages_to_checkpoint[ongoing_chkpt.current_page as usize] }; if let Some(cached_page) = pager.cache_get_for_checkpoint(page_id as usize, target_frame, epoch)? { let buffer = cached_page .get_contents() .buffer .as_ref() .expect("buffer missing") .clone(); // We debug assert that the cached page has the // exact contents as one read from the WAL. #[cfg(debug_assertions)] { let mut raw = vec![0u8; self.page_size() as usize + WAL_FRAME_HEADER_SIZE]; self.io.wait_for_completion( self.read_frame_raw(target_frame, &mut raw)?, )?; let (_, wal_page) = sqlite3_ondisk::parse_wal_frame_header(&raw); let cached = buffer.as_slice(); turso_assert!(wal_page == cached, "cached page content differs from WAL read", { "page_id": page_id, "frame_id": target_frame }); } { ongoing_chkpt .pending_writes .insert(page_id as usize, buffer); // signify that a cached page was used, so it can be unpinned let current = ongoing_chkpt.current_page as usize; ongoing_chkpt.pages_to_checkpoint[current] = (page_id, target_frame); ongoing_chkpt.current_page += 1; } continue 'inner; } // Issue read if page wasn't found in the page cache or doesnt meet // the frame requirements let inflight = self.issue_wal_read_into_buffer(page_id as usize, target_frame)?; group.add(&inflight.completion); nr_completions += 1; ongoing_chkpt.inflight_reads.push(inflight); ongoing_chkpt.current_page += 1; } // Start a write if batch is ready and we're not at write limit let should_flush = ongoing_chkpt.inflight_writes.len() < MAX_INFLIGHT_WRITES && ongoing_chkpt.should_flush_batch(); if should_flush { let batch_map = ongoing_chkpt.pending_writes.take(); if !batch_map.is_empty() { let new_write = InflightWriteBatch::new(); for c in write_pages_vectored( pager, batch_map, new_write.done.clone(), new_write.err.clone(), )? { group.add(&c); nr_completions += 1; } ongoing_chkpt.inflight_writes.push(new_write); } } if nr_completions > 0 { io_yield_one!(group.build()); } else if ongoing_chkpt.complete() { ongoing_chkpt.state = CheckpointState::DetermineResult; } else { // This should be impossible now so we treat it as logic error. mark_unlikely(); return Err(LimboError::InternalError( "checkpoint stuck: no inflight completions but not complete".into(), )); } } // All eligible frames copied to the db file. // Compute checkpoint result, update nBackfills, restart log if needed. CheckpointState::DetermineResult => { let mut ongoing_chkpt = self.ongoing_checkpoint.write(); turso_assert!( ongoing_chkpt.complete(), "checkpoint pending flush must have finished" ); let checkpoint_result = self.with_shared(|shared| { let wal_max_frame = shared.max_frame.load(Ordering::Acquire); let wal_total_backfilled = ongoing_chkpt.max_frame; // Record two num pages fields to return as checkpoint result to caller. // Ref: pnLog, pnCkpt on https://www.sqlite.org/c3ref/wal_checkpoint_v2.html // the total # of frames we actually backfilled let wal_checkpoint_backfilled = wal_total_backfilled.saturating_sub(ongoing_chkpt.min_frame - 1); tracing::debug!("checkpoint: wal_max_frame={wal_max_frame}, wal_total_backfilled={wal_total_backfilled}, wal_checkpoint_backfilled={wal_checkpoint_backfilled}"); CheckpointResult::new(wal_max_frame, wal_total_backfilled, wal_checkpoint_backfilled) }); tracing::debug!("checkpoint_result={:?}, mode={:?}", checkpoint_result, mode); // store the max frame we were able to successfully checkpoint. // NOTE: we don't have a .shm file yet, so it's safe to update nbackfills here // before we sync, because if we crash and then recover, we will checkpoint the entire db anyway. self.with_shared(|shared| { shared .nbackfills .store(ongoing_chkpt.max_frame, Ordering::Release) }); if mode.require_all_backfilled() && !checkpoint_result.everything_backfilled() { return Err(LimboError::Busy); } if mode.should_restart_log() { turso_assert!( matches!( *self.checkpoint_guard.read(), Some(CheckpointLocks::Writer { .. }) ), "We must hold writer and checkpoint locks to restart the log", { "checkpoint_guard": *self.checkpoint_guard.read() } ); self.restart_log()?; } ongoing_chkpt.state = CheckpointState::Finalize { checkpoint_result: Some(checkpoint_result), }; } CheckpointState::Finalize { .. } => { // NOTE: For TRUNCATE mode, WAL truncation is NOT done here. // It is deferred to pager.rs after the DB file has been synced, // at which point it calls truncate_wal(). // This ensures data durability: if a crash occurs after WAL truncation // but before DB sync, the data would be lost. By truncating the WAL // only after the DB is safely synced, we guarantee recoverability. if mode.should_restart_log() { Self::unlock_after_restart(&self.shared, None); } let mut checkpoint_result = { let mut oc = self.ongoing_checkpoint.write(); let CheckpointState::Finalize { checkpoint_result, .. } = &mut oc.state else { panic!("unexpected state"); }; checkpoint_result.take().unwrap() }; // increment wal epoch to ensure no stale pages are used for backfilling self.increment_checkpoint_epoch(); tracing::debug!("checkpoint_result={:?}", checkpoint_result); // we cannot truncate the db file here because we are currently inside a // mut borrow of pager.wal, and accessing the header will attempt a borrow // during 'read_page', so the caller will use the result to determine if: // a. the max frame == num wal frames (everything backfilled) // b. the max frame > 0 (we have something to truncate) if checkpoint_result.should_truncate() { checkpoint_result.maybe_guard = self.checkpoint_guard.write().take(); } else { let _ = self.checkpoint_guard.write().take(); } { let mut oc = self.ongoing_checkpoint.write(); oc.inflight_writes.clear(); oc.pending_writes.clear(); oc.pages_to_checkpoint.clear(); oc.current_page = 0; } let oc_time = self.ongoing_checkpoint.read().time; tracing::debug!( "total time spent checkpointing: {:?}", self.io .current_time_monotonic() .duration_since(oc_time) .as_millis() ); self.ongoing_checkpoint.write().state = CheckpointState::Start; return Ok(IOResult::Done(checkpoint_result)); } } } } /// Coordinate what the maximum safe frame is for us to backfill when checkpointing. /// We can never backfill a frame with a higher number than any reader's read mark, /// because we might overwrite content the reader is reading from the database file. /// /// A checkpoint must never overwrite a page in the main DB file if some /// active reader might still need to read that page from the WAL. /// Concretely: the checkpoint may only copy frames `<= aReadMark[k]` for /// every in-use reader slot `k > 0`. /// /// `read_locks[0]` is special: readers holding slot 0 ignore the WAL entirely /// (they read only the DB file). Its value is a placeholder and does not /// constrain `mxSafeFrame`. /// /// For each slot 1..N: /// - If we can acquire the write lock (slot is free): /// - Slot 1: Set to mxSafeFrame (allowing new readers to see up to this point) /// - Slots 2+: Set to READMARK_NOT_USED (freeing the slot) /// - If we cannot acquire the lock (SQLITE_BUSY): /// - Lower mxSafeFrame to that reader's mark /// - In PASSIVE mode: Already have no busy handler, continue scanning /// - In FULL/RESTART/TRUNCATE: Disable busy handler for remaining slots /// /// Locking behavior: /// - PASSIVE: Never waits, no busy handler (xBusy==NULL) /// - FULL/RESTART/TRUNCATE: May wait via busy handler, but after first BUSY, /// switches to non-blocking for remaining slots /// /// We never modify slot values while a reader holds that slot's lock. /// TOOD: implement proper BUSY handling behavior fn determine_max_safe_checkpoint_frame(&self) -> u64 { self.with_shared(|shared| { let shared_max = shared.max_frame.load(Ordering::Acquire); let mut max_safe_frame = shared_max; for (read_lock_idx, read_lock) in shared.read_locks.iter().enumerate().skip(1) { let this_mark = read_lock.get_value(); if this_mark < max_safe_frame as u32 { let busy = !read_lock.write(); if !busy { let val = if read_lock_idx == 1 { // store the max_frame for the default read slot 1 max_safe_frame as u32 } else { READMARK_NOT_USED }; read_lock.set_value_exclusive(val); read_lock.unlock(); } else { max_safe_frame = this_mark as u64; } } } max_safe_frame }) } /// attempt to restart WAL header before write in order to keep WAL file size under the control /// The conditions for WAL restart are following: /// 1. we can do that only under write transaction /// 2. max_frame_read_lock_index == 0 - this means that transaction was initiated to read data from DB file /// 3. nbackfills > 0 - otherwise nothing was backfilled and there is no reason to truncate header /// 4. max_frame == nbackfills - otherwise there are some non-checkpointed frames in the WAL and we can't truncate the log pub fn try_restart_log_before_write(&self) -> Result<()> { let max_frame_read_lock_index = self.max_frame_read_lock_index.load(Ordering::Acquire); if max_frame_read_lock_index != 0 { tracing::debug!("try_restart_log_before_write: max_frame_read_lock_index={max_frame_read_lock_index}, writer use WAL - can't restart the log"); return Ok(()); } let (max_frame, nbackfills) = self.with_shared(|s| { ( s.max_frame.load(Ordering::Acquire), s.nbackfills.load(Ordering::Acquire), ) }); if nbackfills == 0 { tracing::debug!("try_restart_log_before_write: nbackfills={nbackfills}, nothing were backfilled - can't restart the log"); return Ok(()); } turso_assert!( max_frame >= nbackfills, "backfills can't be more than max_frame" ); if max_frame != nbackfills { tracing::debug!( "try_restart_log_before_write: max_frame={max_frame}, nbackfills={nbackfills}, not everything is backfilled to the DB file - can't restart the log" ); return Ok(()); } let read_lock_0 = self.with_shared(|s| s.read_locks[0].upgrade()); if !read_lock_0 { return Ok(()); } let result = self.restart_log(); if result.is_ok() { self.increment_checkpoint_epoch(); let shared = self.shared.clone(); Self::unlock_after_restart(&shared, result.as_ref().err()); } self.with_shared(|s| s.read_locks[0].downgrade()); tracing::debug!("try_restart_log_before_write: result={:?}", result); result } fn restart_log(&self) -> Result<()> { tracing::debug!("restart_log"); self.with_shared(|shared| { // Block all readers for idx in 1..shared.read_locks.len() { let lock = &shared.read_locks[idx]; if !lock.write() { // release everything we got so far for j in 1..idx { shared.read_locks[j].unlock(); } // Reader is active, cannot proceed return Err(LimboError::Busy); } // after the log is reset, we must set all secondary marks to READMARK_NOT_USED so the next reader selects a fresh slot lock.set_value_exclusive(READMARK_NOT_USED); } Ok(()) })?; // reinitialize in‑memory state self.with_shared_mut_dangerous(|shared| shared.restart_wal_header(&self.io)); let cksm = self.with_shared(|shared| shared.last_checksum); *self.last_checksum.write() = cksm; self.max_frame.store(0, Ordering::Release); self.min_frame.store(0, Ordering::Release); self.checkpoint_seq.fetch_add(1, Ordering::Release); Ok(()) } /// Truncate WAL file to zero and sync it. Called by pager AFTER DB file is synced. fn truncate_log( &self, result: &mut CheckpointResult, sync_type: FileSyncType, ) -> Result> { let file = self.with_shared(|shared| { turso_assert!( shared.enabled.load(Ordering::Relaxed), "WAL must be enabled" ); shared.initialized.store(false, Ordering::Release); shared.file.as_ref().unwrap().clone() }); if !result.wal_truncate_sent { let c = Completion::new_trunc({ move |res| { if let Err(err) = res { tracing::info!("WAL truncate failed: {err}") } else { tracing::trace!("WAL file truncated to 0 B"); } } }); let c = file.truncate(0, c)?; result.wal_truncate_sent = true; // after truncation - there will be nothing in the WAL result.wal_max_frame = 0; result.wal_total_backfilled = 0; io_yield_one!(c); } else if !result.wal_sync_sent { let c = file.sync( Completion::new_sync(move |res| { if let Err(err) = res { tracing::info!("WAL sync failed: {err}") } else { tracing::trace!("WAL file synced after truncation"); } }), sync_type, )?; result.wal_sync_sent = true; io_yield_one!(c); } Ok(IOResult::Done(())) } // unlock shared read locks taken by RESTART/TRUNCATE checkpoint modes fn unlock_after_restart(shared: &Arc>, e: Option<&LimboError>) { // release all read locks we just acquired, the caller will take care of the others let shared = shared.write(); for idx in 1..shared.read_locks.len() { shared.read_locks[idx].unlock(); } if let Some(e) = e { mark_unlikely(); tracing::debug!( "Failed to restart WAL header: {:?}, releasing read locks", e ); } } fn acquire_proper_checkpoint_guard(&self, mode: CheckpointMode) -> Result<()> { let needs_new_guard = { let guard = self.checkpoint_guard.read(); !matches!( (&*guard, mode), ( Some(CheckpointLocks::Read0 { .. }), CheckpointMode::Passive { .. }, ) | ( Some(CheckpointLocks::Writer { .. }), CheckpointMode::Restart | CheckpointMode::Truncate { .. }, ), ) }; if needs_new_guard { // Drop any existing guard if self.checkpoint_guard.read().is_some() { let _ = self.checkpoint_guard.write().take(); } let guard = CheckpointLocks::new(self.shared.clone(), mode)?; *self.checkpoint_guard.write() = Some(guard); } Ok(()) } fn issue_wal_read_into_buffer(&self, page_id: usize, frame_id: u64) -> Result { let offset = self.frame_offset(frame_id); let buf_slot = Arc::new(SpinLock::new(None)); tracing::debug!( "Issuing WAL read: page_id={}, frame_id={}, offset={}", page_id, frame_id, offset ); let complete = { let buf_slot = buf_slot.clone(); Box::new(move |res: Result<(Arc, i32), CompletionError>| { let Ok((buf, read)) = res else { return None; }; let buf_len = buf.len(); turso_assert!( read == buf_len as i32, "read bytes does not match expected buffer length", { "read": read, "expected": buf_len, "frame_id": frame_id } ); *buf_slot.lock() = Some(buf); None }) }; // schedule read of the page payload let file = self.with_shared(|shared| { turso_assert!( shared.enabled.load(Ordering::Relaxed), "WAL must be enabled" ); shared.file.as_ref().unwrap().clone() }); let c = begin_read_wal_frame( file.as_ref(), offset + WAL_FRAME_HEADER_SIZE as u64, self.buffer_pool.clone(), complete, page_id, &self.io_ctx.read(), )?; Ok(InflightRead { completion: c, page_id, buf: buf_slot, }) } /// Check if database changed since this connection's last read transaction. fn db_changed(&self, shared: &WalFileShared) -> bool { self.db_changed_against(Self::load_shared_snapshot(shared), self.connection_state()) } /// MVCC helper: check if WAL state changed and refresh local snapshot without starting a read tx. /// FIXME: this isn't TOCTOU safe because we're not taking WAL read locks. /// /// This is only used to invalidate page cache, so false positives are sort of acceptable since /// MVCC reads currently don't read from WAL frames ever. /// FIXME: MVCC should start using pager read transactions anyway so that we can get rid of /// the stop-the-world MVCC checkpoint that blocks all reads. pub fn mvcc_refresh_if_db_changed(&self) -> bool { self.with_shared(|shared| { let snapshot = Self::load_shared_snapshot(shared); let local_state = self.connection_state(); let changed = self.db_changed_against(snapshot, local_state); if changed { self.install_connection_state(local_state.with_snapshot(snapshot)); } changed }) } } impl WalFileShared { pub fn last_checksum_and_max_frame(&self) -> ((u32, u32), u64) { (self.last_checksum, self.max_frame.load(Ordering::Acquire)) } pub fn open_shared_if_exists( io: &Arc, path: &str, flags: crate::OpenFlags, ) -> Result>> { let file = match io.open_file(path, flags, false) { Ok(file) => file, Err(LimboError::CompletionError(CompletionError::IOError( std::io::ErrorKind::NotFound, _, ))) if flags.contains(crate::OpenFlags::ReadOnly) => { // In readonly mode, if the WAL file doesn't exist, we just return a noop WAL // since there's nothing to read from. return Ok(WalFileShared::new_noop()); } Err(e) => return Err(e), }; let wal_file_shared = sqlite3_ondisk::build_shared_wal(&file, io)?; turso_assert!( wal_file_shared .try_read() .is_some_and(|wfs| wfs.loaded.load(Ordering::Acquire)), "Unable to read WAL shared state" ); Ok(wal_file_shared) } pub fn is_initialized(&self) -> Result { Ok(self.initialized.load(Ordering::Acquire)) } pub fn new_noop() -> Arc> { let wal_header = WalHeader::new(); let read_locks = array::from_fn(|_| TursoRwLock::new()); for (i, lock) in read_locks.iter().enumerate() { lock.write(); lock.set_value_exclusive(if i < 2 { 0 } else { READMARK_NOT_USED }); lock.unlock(); } let shared = WalFileShared { enabled: AtomicBool::new(false), wal_header: Arc::new(SpinLock::new(wal_header)), min_frame: AtomicU64::new(0), max_frame: AtomicU64::new(0), nbackfills: AtomicU64::new(0), transaction_count: AtomicU64::new(0), frame_cache: Arc::new(SpinLock::new(FxHashMap::default())), last_checksum: (0, 0), file: None, read_locks, write_lock: TursoRwLock::new(), checkpoint_lock: TursoRwLock::new(), loaded: AtomicBool::new(true), initialized: AtomicBool::new(false), epoch: AtomicU32::new(0), }; Arc::new(RwLock::new(shared)) } #[cfg(test)] pub(super) fn new_shared(file: Arc) -> Result>> { let wal_header = WalHeader::new(); let read_locks = array::from_fn(|_| TursoRwLock::new()); // slot zero is always zero as it signifies that reads can be done from the db file // directly, and slot 1 is the default read mark containing the max frame. in this case // our max frame is zero so both slots 0 and 1 begin at 0 for (i, lock) in read_locks.iter().enumerate() { lock.write(); lock.set_value_exclusive(if i < 2 { 0 } else { READMARK_NOT_USED }); lock.unlock(); } let shared = WalFileShared { enabled: AtomicBool::new(true), wal_header: Arc::new(SpinLock::new(wal_header)), min_frame: AtomicU64::new(0), max_frame: AtomicU64::new(0), nbackfills: AtomicU64::new(0), transaction_count: AtomicU64::new(0), frame_cache: Arc::new(SpinLock::new(FxHashMap::default())), last_checksum: (0, 0), file: Some(file), read_locks, write_lock: TursoRwLock::new(), checkpoint_lock: TursoRwLock::new(), loaded: AtomicBool::new(true), initialized: AtomicBool::new(false), epoch: AtomicU32::new(0), }; Ok(Arc::new(RwLock::new(shared))) } pub fn page_size(&self) -> u32 { self.wal_header.lock().page_size } /// Called after a successful RESTART/TRUNCATE mode checkpoint /// when all frames are back‑filled. /// /// sqlite3/src/wal.c /// The following is guaranteed when this function is called: /// /// a) the WRITER lock is held, /// b) the entire log file has been checkpointed, and /// c) any existing readers are reading exclusively from the database /// file - there are no readers that may attempt to read a frame from /// the log file. /// /// This function updates the shared-memory structures so that the next /// client to write to the database (which may be this one) does so by /// writing frames into the start of the log file. fn restart_wal_header(&mut self, io: &Arc) { { let mut hdr = self.wal_header.lock(); hdr.checkpoint_seq = hdr.checkpoint_seq.wrapping_add(1); // keep hdr.magic, hdr.file_format, hdr.page_size as-is hdr.salt_1 = hdr.salt_1.wrapping_add(1); hdr.salt_2 = io.generate_random_number() as u32; self.max_frame.store(0, Ordering::Release); self.nbackfills.store(0, Ordering::Release); self.last_checksum = (hdr.checksum_1, hdr.checksum_2); // `prepare_wal_start` (used in the `commit_dirty_pages_inner`) do the work only if WAL is not initialized yet (so, self.initialized is false) // we change WAL state here, so on next write attempt `prepare_wal_start` will update WAL header self.initialized.store(false, Ordering::Release); } self.frame_cache.lock().clear(); // read-marks self.read_locks[0].set_value_exclusive(0); self.read_locks[1].set_value_exclusive(0); for lock in &self.read_locks[2..] { lock.set_value_exclusive(READMARK_NOT_USED); } } } #[cfg(test)] pub mod test { use super::{ReadGuardKind, WalConnectionState, WalFile, WalSnapshot}; use crate::sync::{atomic::Ordering, Arc}; use crate::sync::{Mutex, RwLock}; use crate::{ storage::{ buffer_pool::BufferPool, sqlite3_ondisk::{self, WAL_HEADER_SIZE}, wal::READMARK_NOT_USED, }, types::IOResult, util::IOExt, CheckpointMode, CheckpointResult, Completion, Connection, Database, LimboError, PlatformIO, WalFileShared, IO, }; #[cfg(unix)] use std::os::unix::fs::MetadataExt; #[allow(clippy::arc_with_non_send_sync)] pub(crate) fn get_database() -> (Arc, std::path::PathBuf) { let mut path = tempfile::tempdir().unwrap().keep(); let dbpath = path.clone(); path.push("test.db"); { let connection = rusqlite::Connection::open(&path).unwrap(); connection .pragma_update(None, "journal_mode", "wal") .unwrap(); } let io: Arc = Arc::new(PlatformIO::new().unwrap()); let db = Database::open_file(io.clone(), path.to_str().unwrap()).unwrap(); // db + tmp directory (db, dbpath) } #[test] fn test_truncate_file() { let (db, _path) = get_database(); let conn = db.connect().unwrap(); conn.execute("create table test (id integer primary key, value text)") .unwrap(); let _ = conn.execute("insert into test (value) values ('test1'), ('test2'), ('test3')"); let wal = db.shared_wal.write(); let wal_file = wal.file.as_ref().unwrap().clone(); let done = Arc::new(Mutex::new(false)); let _done = done.clone(); let _ = wal_file.truncate( WAL_HEADER_SIZE as u64, Completion::new_trunc(move |_| { *_done.lock() = true; }), ); assert!(wal_file.size().unwrap() == WAL_HEADER_SIZE as u64); assert!(*done.lock()); } #[test] fn test_wal_truncate_checkpoint() { let (db, path) = get_database(); let mut walpath = path.clone().into_os_string().into_string().unwrap(); walpath.push_str("/test.db-wal"); let walpath = std::path::PathBuf::from(walpath); let conn = db.connect().unwrap(); conn.execute("create table test (id integer primary key, value text)") .unwrap(); for _i in 0..25 { let _ = conn.execute("insert into test (value) values (randomblob(1024)), (randomblob(1024)), (randomblob(1024))"); } let pager = conn.pager.load(); let _ = pager.cacheflush(); let stat = std::fs::metadata(&walpath).unwrap(); let meta_before = std::fs::metadata(&walpath).unwrap(); let bytes_before = meta_before.len(); run_checkpoint_until_done( &pager, CheckpointMode::Truncate { upper_bound_inclusive: None, }, ); assert_eq!(pager.wal_state().unwrap().max_frame, 0); tracing::info!("wal filepath: {walpath:?}, size: {}", stat.len()); let meta_after = std::fs::metadata(&walpath).unwrap(); let bytes_after = meta_after.len(); assert_ne!( bytes_before, bytes_after, "WAL file should not have been empty before checkpoint" ); assert_eq!( bytes_after, 0, "WAL file should be truncated to 0 bytes, but is {bytes_after} bytes", ); std::fs::remove_dir_all(path).unwrap(); } #[test] fn test_shutdown_checkpoint_truncates_after_restart() { let (db, path) = get_database(); let mut walpath = path.clone().into_os_string().into_string().unwrap(); walpath.push_str("/test.db-wal"); let walpath = std::path::PathBuf::from(walpath); let conn = db.connect().unwrap(); conn.execute("create table test (id integer primary key, value text)") .unwrap(); conn.execute("insert into test (value) values ('v1'), ('v2')") .unwrap(); let pager = conn.pager.load(); run_checkpoint_until_done(&pager, CheckpointMode::Restart); let bytes_before = std::fs::metadata(&walpath).unwrap().len(); assert!( bytes_before > 0, "WAL should still have data after RESTART checkpoint" ); conn.close().unwrap(); let bytes_after = std::fs::metadata(&walpath).unwrap().len(); assert_eq!( bytes_after, 0, "Shutdown checkpoint should truncate WAL after RESTART, but WAL is {bytes_after} bytes", ); std::fs::remove_dir_all(path).unwrap(); } fn bulk_inserts(conn: &Arc, n_txns: usize, rows_per_txn: usize) { for _ in 0..n_txns { conn.execute("begin transaction").unwrap(); for i in 0..rows_per_txn { conn.execute(format!("insert into test(value) values ('v{i}')")) .unwrap(); } conn.execute("commit").unwrap(); } } fn count_test_table(conn: &Arc) -> i64 { let mut stmt = conn.prepare("select count(*) from test").unwrap(); let mut count: i64 = 0; stmt.run_with_row_callback(|row| { count = row.get(0).unwrap(); Ok(()) }) .unwrap(); count } fn run_checkpoint_until_done(pager: &crate::Pager, mode: CheckpointMode) -> CheckpointResult { // Use pager.checkpoint() instead of wal.checkpoint() directly because // WAL truncation (for TRUNCATE mode) now happens in pager's TruncateWalFile phase. pager .io .block(|| pager.checkpoint(mode, crate::SyncMode::Full, true)) .unwrap() } fn make_test_wal() -> (Arc>, WalFile) { let io: Arc = Arc::new(PlatformIO::new().unwrap()); let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE); let shared = WalFileShared::new_noop(); let wal = WalFile::new(io, shared.clone(), ((0, 0), 0), buffer_pool); (shared, wal) } fn set_shared_snapshot(shared: &Arc>, snapshot: WalSnapshot) { let mut guard = shared.write(); guard.max_frame.store(snapshot.max_frame, Ordering::Release); guard .nbackfills .store(snapshot.nbackfills, Ordering::Release); guard.last_checksum = snapshot.last_checksum; guard.wal_header.lock().checkpoint_seq = snapshot.checkpoint_seq; guard .transaction_count .store(snapshot.transaction_count, Ordering::Release); } #[cfg(test)] fn read_slots_with_readers(shared: &WalFileShared) -> Vec { shared .read_locks .iter() .enumerate() .filter_map(|(slot, lock)| { let state = lock.0.load(Ordering::Acquire); let has_readers = (state & super::TursoRwLock::READER_COUNT_MASK) != 0; has_readers.then_some(slot) }) .collect() } fn wal_header_snapshot(shared: &Arc>) -> (u32, u32, u32, u32) { // (checkpoint_seq, salt1, salt2, page_size) let shared_guard = shared.read(); let hdr = shared_guard.wal_header.lock(); (hdr.checkpoint_seq, hdr.salt_1, hdr.salt_2, hdr.page_size) } #[test] fn test_wal_connection_state_round_trip() { let (_shared, wal) = make_test_wal(); let state = WalConnectionState::new( WalSnapshot { max_frame: 11, nbackfills: 7, last_checksum: (31, 47), checkpoint_seq: 5, transaction_count: 13, }, ReadGuardKind::ReadMark(3), ); wal.install_connection_state(state); assert_eq!(wal.connection_state(), state); assert_eq!(wal.connection_state().snapshot.min_frame(), 8); } #[test] fn test_mvcc_refresh_updates_snapshot_without_changing_read_guard() { let (shared, wal) = make_test_wal(); let initial = WalSnapshot { max_frame: 4, nbackfills: 2, last_checksum: (9, 10), checkpoint_seq: 1, transaction_count: 3, }; set_shared_snapshot(&shared, initial); wal.install_connection_state(WalConnectionState::new(initial, ReadGuardKind::ReadMark(2))); assert!(!wal.mvcc_refresh_if_db_changed()); let updated = WalSnapshot { max_frame: 8, nbackfills: 5, last_checksum: (21, 34), checkpoint_seq: 7, transaction_count: 4, }; set_shared_snapshot(&shared, updated); assert!(wal.mvcc_refresh_if_db_changed()); assert_eq!( wal.connection_state(), WalConnectionState::new(updated, ReadGuardKind::ReadMark(2)) ); } #[test] fn restart_checkpoint_reset_wal_state_handling() { let (db, path) = get_database(); let walpath = { let mut p = path.clone().into_os_string().into_string().unwrap(); p.push_str("/test.db-wal"); std::path::PathBuf::from(p) }; let conn = db.connect().unwrap(); conn.execute("create table test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn, 20, 3); let IOResult::Done(completions) = conn.pager.load().cacheflush().unwrap() else { panic!() }; for c in completions { db.io.wait_for_completion(c).unwrap(); } // Snapshot header & counters before the RESTART checkpoint. let wal_shared = db.shared_wal.clone(); let (seq_before, salt1_before, salt2_before, _ps_before) = wal_header_snapshot(&wal_shared); let (mx_before, backfill_before) = { let s = wal_shared.read(); ( s.max_frame.load(Ordering::SeqCst), s.nbackfills.load(Ordering::SeqCst), ) }; assert!(mx_before > 0); assert_eq!(backfill_before, 0); let meta_before = std::fs::metadata(&walpath).unwrap(); #[cfg(unix)] let size_before = meta_before.blocks(); #[cfg(not(unix))] let size_before = meta_before.len(); // Run a RESTART checkpoint, should backfill everything and reset WAL counters, // but NOT truncate the file. { let pager = conn.pager.load(); let res = run_checkpoint_until_done(&pager, CheckpointMode::Restart); assert_eq!(res.wal_max_frame, mx_before); assert_eq!(res.wal_total_backfilled, mx_before); assert_eq!(res.wal_checkpoint_backfilled, mx_before); } // Validate post‑RESTART header & counters. let (seq_after, salt1_after, salt2_after, _ps_after) = wal_header_snapshot(&wal_shared); assert_eq!( seq_after, seq_before.wrapping_add(1), "checkpoint_seq must increment on RESTART" ); assert_eq!( salt1_after, salt1_before.wrapping_add(1), "salt_1 is incremented" ); assert_ne!(salt2_after, salt2_before, "salt_2 is randomized"); let (mx_after, backfill_after) = { let s = wal_shared.read(); ( s.max_frame.load(Ordering::SeqCst), s.nbackfills.load(Ordering::SeqCst), ) }; assert_eq!(mx_after, 0, "mxFrame reset to 0 after RESTART"); assert_eq!(backfill_after, 0, "nBackfill reset to 0 after RESTART"); // File size should be unchanged for RESTART (no truncate). let meta_after = std::fs::metadata(&walpath).unwrap(); #[cfg(unix)] let size_after = meta_after.blocks(); #[cfg(not(unix))] let size_after = meta_after.len(); assert_eq!( size_before, size_after, "RESTART must not change WAL file size" ); // Next write should start a new sequence at frame 1. conn.execute("insert into test(value) values ('post_restart')") .unwrap(); conn.pager .load() .wal .as_ref() .unwrap() .finish_append_frames_commit() .unwrap(); let new_max = wal_shared.read().max_frame.load(Ordering::SeqCst); assert_eq!(new_max, 1, "first append after RESTART starts at frame 1"); std::fs::remove_dir_all(path).unwrap(); } #[test] fn test_wal_passive_partial_then_complete() { let (db, _tmp) = get_database(); let conn1 = db.connect().unwrap(); let conn2 = db.connect().unwrap(); conn1 .execute("create table test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn1, 15, 2); let IOResult::Done(completions) = conn1.pager.load().cacheflush().unwrap() else { panic!() }; for c in completions { db.io.wait_for_completion(c).unwrap(); } // Force a read transaction that will freeze a lower read mark let readmark = { let pager = conn2.pager.load(); let wal2 = pager.wal.as_ref().unwrap(); wal2.begin_read_tx().unwrap(); wal2.get_max_frame() }; // generate more frames that the reader will not see. bulk_inserts(&conn1, 15, 2); let IOResult::Done(completions) = conn1.pager.load().cacheflush().unwrap() else { panic!() }; for c in completions { db.io.wait_for_completion(c).unwrap(); } // Run passive checkpoint, expect partial let (res1, max_before) = { let pager = conn1.pager.load(); let res = run_checkpoint_until_done( &pager, CheckpointMode::Passive { upper_bound_inclusive: None, }, ); let maxf = db.shared_wal.read().max_frame.load(Ordering::SeqCst); (res, maxf) }; assert_eq!(res1.wal_max_frame, max_before); assert!( res1.wal_total_backfilled < res1.wal_max_frame, "Partial backfill expected, {} : {}", res1.wal_total_backfilled, res1.wal_max_frame ); assert_eq!( res1.wal_total_backfilled, readmark, "Checkpointed frames should match read mark" ); // Release reader { let pager = conn2.pager.load(); let wal2 = pager.wal.as_ref().unwrap(); wal2.end_read_tx(); } // Second passive checkpoint should finish let pager = conn1.pager.load(); let res2 = run_checkpoint_until_done( &pager, CheckpointMode::Passive { upper_bound_inclusive: None, }, ); assert_eq!( res2.wal_total_backfilled, res2.wal_max_frame, "Second checkpoint completes remaining frames" ); } #[test] fn test_wal_restart_blocks_readers() { let (db, _) = get_database(); let conn1 = db.connect().unwrap(); let conn2 = db.connect().unwrap(); // Start a read transaction conn2 .pager .load() .wal .as_ref() .unwrap() .begin_read_tx() .unwrap(); // checkpoint should succeed here because the wal is fully checkpointed (empty) // so the reader is using readmark0 to read directly from the db file. let p = conn1.pager.load(); let w = p.wal.as_ref().unwrap(); loop { match w.checkpoint(&p, CheckpointMode::Restart) { Ok(IOResult::IO(io)) => { io.wait(db.io.as_ref()).unwrap(); } e => { assert!( matches!(e, Err(LimboError::Busy)), "reader is holding readmark0 we should return Busy" ); break; } } } conn2.pager.load().end_read_tx(); conn1 .execute("create table test(id integer primary key, value text)") .unwrap(); for i in 0..10 { conn1 .execute(format!("insert into test(value) values ('value{i}')")) .unwrap(); } // now that we have some frames to checkpoint, try again conn2.pager.load().begin_read_tx().unwrap(); let p = conn1.pager.load(); let w = p.wal.as_ref().unwrap(); loop { match w.checkpoint(&p, CheckpointMode::Restart) { Ok(IOResult::IO(io)) => { io.wait(db.io.as_ref()).unwrap(); } Ok(IOResult::Done(_)) => { panic!("Checkpoint should not have succeeded"); } Err(e) => { assert!( matches!(e, LimboError::Busy), "should return busy if we have readers" ); break; } } } } #[test] fn test_wal_read_marks_after_restart() { let (db, _path) = get_database(); let wal_shared = db.shared_wal.clone(); let conn = db.connect().unwrap(); conn.execute("create table test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn, 10, 5); // Checkpoint with restart { let pager = conn.pager.load(); let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart); assert!(result.everything_backfilled()); } // Verify read marks after restart let read_marks_after: Vec<_> = { let s = wal_shared.read(); (0..5).map(|i| s.read_locks[i].get_value()).collect() }; assert_eq!(read_marks_after[0], 0, "Slot 0 should remain 0"); assert_eq!( read_marks_after[1], 0, "Slot 1 (default reader) should be reset to 0" ); for (i, item) in read_marks_after.iter().take(5).skip(2).enumerate() { assert_eq!( *item, READMARK_NOT_USED, "Slot {i} should be READMARK_NOT_USED after restart", ); } } #[test] fn test_wal_concurrent_readers_during_checkpoint() { let (db, _path) = get_database(); let conn_writer = db.connect().unwrap(); conn_writer .execute("create table test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn_writer, 5, 10); // Start multiple readers at different points let conn_r1 = db.connect().unwrap(); let conn_r2 = db.connect().unwrap(); // R1 starts reading let r1_max_frame = { let pager = conn_r1.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.begin_read_tx().unwrap(); wal.get_max_frame() }; bulk_inserts(&conn_writer, 5, 10); // R2 starts reading, sees more frames than R1 let r2_max_frame = { let pager = conn_r2.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.begin_read_tx().unwrap(); wal.get_max_frame() }; // try passive checkpoint, should only checkpoint up to R1's position let checkpoint_result = { let pager = conn_writer.pager.load(); run_checkpoint_until_done( &pager, CheckpointMode::Passive { upper_bound_inclusive: None, }, ) }; assert!( checkpoint_result.wal_total_backfilled < checkpoint_result.wal_max_frame, "Should not checkpoint all frames when readers are active" ); assert_eq!( checkpoint_result.wal_total_backfilled, r1_max_frame, "Should have checkpointed up to R1's max frame" ); // Verify R2 still sees its frames assert_eq!( conn_r2.pager.load().wal.as_ref().unwrap().get_max_frame(), r2_max_frame, "Reader should maintain its snapshot" ); } #[test] fn test_wal_checkpoint_updates_read_marks() { let (db, _path) = get_database(); let wal_shared = db.shared_wal.clone(); let conn = db.connect().unwrap(); conn.execute("create table test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn, 10, 5); // get max frame before checkpoint let max_frame_before = wal_shared.read().max_frame.load(Ordering::SeqCst); { let pager = conn.pager.load(); let _result = run_checkpoint_until_done( &pager, CheckpointMode::Passive { upper_bound_inclusive: None, }, ); } // check that read mark 1 (default reader) was updated to max_frame let read_mark_1 = wal_shared.read().read_locks[1].get_value(); assert_eq!( read_mark_1 as u64, max_frame_before, "Read mark 1 should be updated to max frame during checkpoint" ); } #[test] fn test_wal_writer_blocks_restart_checkpoint() { let (db, _path) = get_database(); let conn1 = db.connect().unwrap(); let conn2 = db.connect().unwrap(); conn1 .execute("create table test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn1, 5, 5); // start a write transaction { let pager = conn2.pager.load(); let wal = pager.wal.as_ref().unwrap(); let _ = wal.begin_read_tx().unwrap(); wal.begin_write_tx().unwrap(); } // should fail because writer lock is held let result = { let pager = conn1.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.checkpoint(&pager, CheckpointMode::Restart) }; assert!( matches!(result, Err(LimboError::Busy)), "Restart checkpoint should fail when write lock is held" ); conn2.pager.load().wal.as_ref().unwrap().end_read_tx(); // release write lock conn2.pager.load().wal.as_ref().unwrap().end_write_tx(); // now restart should succeed let result = { let pager = conn1.pager.load(); run_checkpoint_until_done(&pager, CheckpointMode::Restart) }; assert!(result.everything_backfilled()); } #[test] #[should_panic(expected = "must have a read transaction to begin a write transaction")] fn test_wal_read_transaction_required_before_write() { let (db, _path) = get_database(); let conn = db.connect().unwrap(); conn.execute("create table test(id integer primary key, value text)") .unwrap(); // Attempt to start a write transaction without a read transaction let pager = conn.pager.load(); let wal = pager.wal.as_ref().unwrap(); let _ = wal.begin_write_tx(); } fn check_read_lock_slot(conn: &Arc, _expected_slot: usize) -> bool { let pager = conn.pager.load(); let _wal = pager.wal.as_ref().unwrap(); #[cfg(debug_assertions)] { let wal_any = _wal.as_any(); if let Some(wal_file) = wal_any.downcast_ref::() { return wal_file.max_frame_read_lock_index.load(Ordering::Acquire) == _expected_slot; } } false } #[test] fn test_wal_multiple_readers_at_different_frames() { let (db, _path) = get_database(); let conn_writer = db.connect().unwrap(); conn_writer .execute("CREATE TABLE test(id INTEGER PRIMARY KEY, value TEXT)") .unwrap(); fn start_reader(conn: &Arc) -> (u64, crate::Statement) { conn.execute("BEGIN").unwrap(); let mut stmt = conn.prepare("SELECT * FROM test").unwrap(); stmt.step().unwrap(); let frame = conn.pager.load().wal.as_ref().unwrap().get_max_frame(); (frame, stmt) } bulk_inserts(&conn_writer, 3, 5); let conn1 = &db.connect().unwrap(); let (r1_frame, _stmt) = start_reader(conn1); // reader 1 bulk_inserts(&conn_writer, 3, 5); let conn_r2 = db.connect().unwrap(); let (r2_frame, _stmt2) = start_reader(&conn_r2); // reader 2 bulk_inserts(&conn_writer, 3, 5); let conn_r3 = db.connect().unwrap(); let (r3_frame, _stmt3) = start_reader(&conn_r3); // reader 3 assert!(r1_frame < r2_frame && r2_frame < r3_frame); // passive checkpoint #1 let result1 = { let pager = conn_writer.pager.load(); run_checkpoint_until_done( &pager, CheckpointMode::Passive { upper_bound_inclusive: None, }, ) }; assert_eq!(result1.wal_total_backfilled, r1_frame); // finish reader‑1 conn1.execute("COMMIT").unwrap(); // passive checkpoint #2 let result2 = { let pager = conn_writer.pager.load(); run_checkpoint_until_done( &pager, CheckpointMode::Passive { upper_bound_inclusive: None, }, ) }; assert_eq!( result1.wal_checkpoint_backfilled + result2.wal_checkpoint_backfilled, r2_frame ); // verify visible rows let r2_cnt = count_test_table(&conn_r2); let r3_cnt = count_test_table(&conn_r3); assert_eq!(r2_cnt, 30); assert_eq!(r3_cnt, 45); } #[test] fn test_checkpoint_truncate_reset_handling() { let (db, path) = get_database(); let conn = db.connect().unwrap(); let walpath = { let mut p = path.clone().into_os_string().into_string().unwrap(); p.push_str("/test.db-wal"); std::path::PathBuf::from(p) }; conn.execute("create table test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn, 10, 10); // Get size before checkpoint let size_before = std::fs::metadata(&walpath).unwrap().len(); assert!(size_before > 0, "WAL file should have content"); // Do a TRUNCATE checkpoint { let pager = conn.pager.load(); run_checkpoint_until_done( &pager, CheckpointMode::Truncate { upper_bound_inclusive: None, }, ); } // Check file size after truncate let size_after = std::fs::metadata(&walpath).unwrap().len(); assert_eq!(size_after, 0, "WAL file should be truncated to 0 bytes"); // Verify we can still write to the database conn.execute("INSERT INTO test VALUES (1001, 'after-truncate')") .unwrap(); // Check WAL has new content let new_size = std::fs::metadata(&walpath).unwrap().len(); assert!(new_size >= 32, "WAL file too small"); let hdr = read_wal_header(&walpath); let expected_magic = if cfg!(target_endian = "big") { sqlite3_ondisk::WAL_MAGIC_BE } else { sqlite3_ondisk::WAL_MAGIC_LE }; assert!( hdr.magic == expected_magic, "bad WAL magic: {:#X}, expected: {:#X}", hdr.magic, sqlite3_ondisk::WAL_MAGIC_BE ); assert_eq!(hdr.file_format, 3007000); assert_eq!(hdr.page_size, 4096, "invalid page size"); assert_eq!(hdr.checkpoint_seq, 1, "invalid checkpoint_seq"); std::fs::remove_dir_all(path).unwrap(); } #[test] fn test_wal_checkpoint_truncate_db_file_contains_data() { let (db, path) = get_database(); let conn = db.connect().unwrap(); let walpath = { let mut p = path.clone().into_os_string().into_string().unwrap(); p.push_str("/test.db-wal"); std::path::PathBuf::from(p) }; conn.execute("create table test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn, 10, 100); // Get size before checkpoint let size_before = std::fs::metadata(&walpath).unwrap().len(); assert!(size_before > 0, "WAL file should have content"); // Do a TRUNCATE checkpoint { let pager = conn.pager.load(); run_checkpoint_until_done( &pager, CheckpointMode::Truncate { upper_bound_inclusive: None, }, ); } // Check file size after truncate let size_after = std::fs::metadata(&walpath).unwrap().len(); assert_eq!(size_after, 0, "WAL file should be truncated to 0 bytes"); // Verify we can still write to the database conn.execute("INSERT INTO test VALUES (1001, 'after-truncate')") .unwrap(); // Check WAL has new content let new_size = std::fs::metadata(&walpath).unwrap().len(); assert!(new_size >= 32, "WAL file too small"); let hdr = read_wal_header(&walpath); let expected_magic = if cfg!(target_endian = "big") { sqlite3_ondisk::WAL_MAGIC_BE } else { sqlite3_ondisk::WAL_MAGIC_LE }; assert!( hdr.magic == expected_magic, "bad WAL magic: {:#X}, expected: {:#X}", hdr.magic, sqlite3_ondisk::WAL_MAGIC_BE ); assert_eq!(hdr.file_format, 3007000); assert_eq!(hdr.page_size, 4096, "invalid page size"); assert_eq!(hdr.checkpoint_seq, 1, "invalid checkpoint_seq"); { let pager = conn.pager.load(); run_checkpoint_until_done( &pager, CheckpointMode::Passive { upper_bound_inclusive: None, }, ); } // delete the WAL file so we can read right from db and assert // that everything was backfilled properly std::fs::remove_file(&walpath).unwrap(); let count = count_test_table(&conn); assert_eq!( count, 1001, "we should have 1001 rows in the table all together" ); std::fs::remove_dir_all(path).unwrap(); } fn read_wal_header(path: &std::path::Path) -> sqlite3_ondisk::WalHeader { use std::{fs::File, io::Read}; let mut hdr = [0u8; 32]; File::open(path).unwrap().read_exact(&mut hdr).unwrap(); let be = |i| u32::from_be_bytes(hdr[i..i + 4].try_into().unwrap()); sqlite3_ondisk::WalHeader { magic: be(0x00), file_format: be(0x04), page_size: be(0x08), checkpoint_seq: be(0x0C), salt_1: be(0x10), salt_2: be(0x14), checksum_1: be(0x18), checksum_2: be(0x1C), } } #[test] fn test_wal_stale_snapshot_in_write_transaction() { let (db, _path) = get_database(); let conn1 = db.connect().unwrap(); let conn2 = db.connect().unwrap(); conn1 .execute("create table test(id integer primary key, value text)") .unwrap(); // Start a read transaction on conn2 { let pager = conn2.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.begin_read_tx().unwrap(); } // Make changes using conn1 bulk_inserts(&conn1, 5, 5); // Try to start a write transaction on conn2 with a stale snapshot let result = { let pager = conn2.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.begin_write_tx() }; // Should get BusySnapShot due to stale snapshot assert!(matches!(result, Err(LimboError::BusySnapshot))); // End read transaction and start a fresh one { let pager = conn2.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.end_read_tx(); wal.begin_read_tx().unwrap(); } // Now write transaction should work let result = { let pager = conn2.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.begin_write_tx() }; assert!(matches!(result, Ok(()))); } #[test] fn test_wal_readlock0_optimization_behavior() { let (db, _path) = get_database(); let conn1 = db.connect().unwrap(); let conn2 = db.connect().unwrap(); conn1 .execute("create table test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn1, 5, 5); // Do a full checkpoint to move all data to DB file { let pager = conn1.pager.load(); run_checkpoint_until_done( &pager, CheckpointMode::Passive { upper_bound_inclusive: None, }, ); } // Start a read transaction on conn2 { let pager = conn2.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.begin_read_tx().unwrap(); } // should use slot 0, as everything is backfilled assert!(check_read_lock_slot(&conn2, 0)); { let pager = conn1.pager.load(); let wal = pager.wal.as_ref().unwrap(); let frame = wal.find_frame(5, None); // since we hold readlock0, we should ignore the db file and find_frame should return none assert!(frame.is_ok_and(|f| f.is_none())); } // Try checkpoint, should fail because reader has slot 0 { let pager = conn1.pager.load(); let wal = pager.wal.as_ref().unwrap(); let result = wal.checkpoint(&pager, CheckpointMode::Restart); assert!( matches!(result, Err(LimboError::Busy)), "RESTART checkpoint should fail when a reader is using slot 0" ); } // End the read transaction { let pager = conn2.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.end_read_tx(); } { let pager = conn1.pager.load(); let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart); assert!( result.everything_backfilled(), "RESTART checkpoint should succeed after reader releases slot 0" ); } } #[test] fn test_wal_full_backfills_all() { let (db, _tmp) = get_database(); let conn = db.connect().unwrap(); // Write some data to put frames in the WAL conn.execute("create table test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn, 8, 4); // Ensure frames are flushed to the WAL let IOResult::Done(completions) = conn.pager.load().cacheflush().unwrap() else { panic!() }; for c in completions { db.io.wait_for_completion(c).unwrap(); } // Snapshot the current mxFrame before running FULL let wal_shared = db.shared_wal.clone(); let mx_before = wal_shared.read().max_frame.load(Ordering::SeqCst); assert!(mx_before > 0, "expected frames in WAL before FULL"); // Run FULL checkpoint - must backfill *all* frames up to mx_before let result = { let pager = conn.pager.load(); run_checkpoint_until_done(&pager, CheckpointMode::Full) }; assert_eq!(result.wal_checkpoint_backfilled, mx_before); assert_eq!(result.wal_total_backfilled, mx_before); } #[test] fn test_wal_full_waits_for_old_reader_then_succeeds() { let (db, _tmp) = get_database(); let writer = db.connect().unwrap(); let reader = db.connect().unwrap(); writer .execute("create table test(id integer primary key, value text)") .unwrap(); // First commit some data and flush (reader will snapshot here) bulk_inserts(&writer, 2, 3); let IOResult::Done(completions) = writer.pager.load().cacheflush().unwrap() else { panic!() }; for c in completions { db.io.wait_for_completion(c).unwrap(); } // Start a read transaction pinned at the current snapshot { let pager = reader.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.begin_read_tx().unwrap(); } let r_snapshot = { let pager = reader.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.get_max_frame() }; // Advance WAL beyond the reader's snapshot bulk_inserts(&writer, 3, 4); let IOResult::Done(completions) = writer.pager.load().cacheflush().unwrap() else { panic!() }; for c in completions { db.io.wait_for_completion(c).unwrap(); } let mx_now = db.shared_wal.read().max_frame.load(Ordering::SeqCst); assert!(mx_now > r_snapshot); // FULL must return Busy while a reader is stuck behind { let pager = writer.pager.load(); let wal = pager.wal.as_ref().unwrap(); loop { match wal.checkpoint(&pager, CheckpointMode::Full) { Ok(IOResult::IO(io)) => { // Drive any pending IO (should quickly become Busy or Done) io.wait(db.io.as_ref()).unwrap(); } Err(LimboError::Busy) => { break; } other => panic!("expected Busy from FULL with old reader, got {other:?}"), } } } // Release the reader, now full mode should succeed and backfill everything { let pager = reader.pager.load(); let wal = pager.wal.as_ref().unwrap(); wal.end_read_tx(); } let result = { let pager = writer.pager.load(); run_checkpoint_until_done(&pager, CheckpointMode::Full) }; assert_eq!(result.wal_checkpoint_backfilled, mx_now - r_snapshot); assert!(result.everything_backfilled()); } #[test] fn test_rollback_releases_read_lock() { let (db, _path) = get_database(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t(x)").unwrap(); conn.execute("BEGIN").unwrap(); conn.execute("INSERT INTO t VALUES(1)").unwrap(); { let pager = conn.pager.load(); let wal = pager.wal.as_ref().unwrap(); assert!( wal.holds_read_lock(), "read lock must be held during write tx" ); } conn.execute("ROLLBACK").unwrap(); { let pager = conn.pager.load(); let wal = pager.wal.as_ref().unwrap(); assert!( !wal.holds_read_lock(), "read lock must be released after ROLLBACK" ); } } #[test] fn test_rollback_releases_shared_read_lock_slot() { let (db, _path) = get_database(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t(x)").unwrap(); conn.execute("BEGIN").unwrap(); conn.execute("INSERT INTO t VALUES(1)").unwrap(); let locked_slots_before = { let shared = db.shared_wal.read(); read_slots_with_readers(&shared) }; assert_eq!( locked_slots_before.len(), 1, "expected exactly one shared read-lock slot while transaction is active" ); conn.execute("ROLLBACK").unwrap(); let locked_slots_after = { let shared = db.shared_wal.read(); read_slots_with_readers(&shared) }; assert!( locked_slots_after.is_empty(), "ROLLBACK must release the shared read-lock slot" ); } #[test] fn test_rollback_releases_slot_zero_read_lock() { let (db, _path) = get_database(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn, 3, 3); { let pager = conn.pager.load(); let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart); assert!( result.everything_backfilled(), "restart checkpoint setup must fully backfill WAL" ); } conn.execute("BEGIN").unwrap(); conn.execute("INSERT INTO test(value) VALUES('slot0')") .unwrap(); let locked_slots_before = { let shared = db.shared_wal.read(); read_slots_with_readers(&shared) }; assert_eq!( locked_slots_before, vec![0], "writer should use slot 0 when WAL is fully checkpointed" ); conn.execute("ROLLBACK").unwrap(); let locked_slots_after = { let shared = db.shared_wal.read(); read_slots_with_readers(&shared) }; assert!( locked_slots_after.is_empty(), "ROLLBACK must release slot 0 shared read-lock as well" ); } #[test] fn test_savepoint_rollback_preserves_read_lock() { let (db, _path) = get_database(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t(x INTEGER PRIMARY KEY)") .unwrap(); conn.execute("BEGIN").unwrap(); conn.execute("INSERT INTO t VALUES(1)").unwrap(); // Trigger a statement failure that causes savepoint rollback. // A duplicate primary key on the second INSERT will fail the // statement, rolling back to the anonymous savepoint while // keeping the write transaction open. let res = conn.execute("INSERT INTO t VALUES(1)"); assert!(res.is_err(), "duplicate PK insert must fail"); { let pager = conn.pager.load(); let wal = pager.wal.as_ref().unwrap(); assert!( wal.holds_read_lock(), "read lock must still be held after savepoint rollback" ); assert!( wal.holds_write_lock(), "write lock must still be held after savepoint rollback" ); } // The transaction should still be usable: commit succeeds and // the first insert is preserved. conn.execute("COMMIT").unwrap(); let mut stmt = conn.prepare("SELECT count(*) FROM t").unwrap(); let mut count: i64 = 0; stmt.run_with_row_callback(|row| { count = row.get(0).unwrap(); Ok(()) }) .unwrap(); assert_eq!(count, 1, "first insert should survive savepoint rollback"); } #[test] fn test_savepoint_then_tx_rollback_allows_restart_checkpoint_from_other_connection() { let (db, _path) = get_database(); let conn1 = db.connect().unwrap(); let conn2 = db.connect().unwrap(); conn1 .execute("CREATE TABLE test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn1, 2, 2); let count_before = count_test_table(&conn1); conn1.execute("BEGIN").unwrap(); conn1 .execute("INSERT INTO test(id, value) VALUES(1000, 'first')") .unwrap(); let duplicate = conn1.execute("INSERT INTO test(id, value) VALUES(1000, 'dup')"); assert!(duplicate.is_err(), "duplicate PK insert must fail"); { let pager = conn1.pager.load(); let wal = pager.wal.as_ref().unwrap(); assert!( wal.holds_read_lock(), "read lock must still be held after savepoint rollback" ); assert!( wal.holds_write_lock(), "write lock must still be held after savepoint rollback" ); } conn1.execute("ROLLBACK").unwrap(); { let pager = conn1.pager.load(); let wal = pager.wal.as_ref().unwrap(); assert!( !wal.holds_read_lock(), "read lock must be released after transaction rollback" ); assert!( !wal.holds_write_lock(), "write lock must be released after transaction rollback" ); } let locked_slots_after_rollback = { let shared = db.shared_wal.read(); read_slots_with_readers(&shared) }; assert!( locked_slots_after_rollback.is_empty(), "transaction rollback after savepoint failure must not leak shared read locks" ); assert_eq!( count_test_table(&conn1), count_before, "transaction rollback should remove writes made before savepoint failure" ); let result = { let pager = conn2.pager.load(); run_checkpoint_until_done(&pager, CheckpointMode::Restart) }; assert!( result.everything_backfilled(), "restart checkpoint from another connection must succeed after full rollback" ); } #[test] fn test_checkpoint_succeeds_after_rollback() { let (db, _path) = get_database(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE test(id integer primary key, value text)") .unwrap(); bulk_inserts(&conn, 5, 3); conn.execute("BEGIN").unwrap(); conn.execute("INSERT INTO test(value) VALUES('rollback_me')") .unwrap(); conn.execute("ROLLBACK").unwrap(); let pager = conn.pager.load(); let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart); assert!( result.everything_backfilled(), "checkpoint must succeed after rollback, not return Busy" ); } } ================================================ FILE: core/sync.rs ================================================ #[cfg(shuttle)] pub(crate) use shuttle_adapter::*; #[cfg(not(shuttle))] pub(crate) use std_adapter::*; #[cfg(shuttle)] mod shuttle_adapter { pub use shuttle::sync::atomic; pub use shuttle::sync::{Arc, Weak}; pub use std::sync::{LazyLock, OnceLock}; use std::fmt::{self, Debug}; use std::ops::{Deref, DerefMut}; pub struct Mutex(shuttle::sync::Mutex); impl fmt::Debug for Mutex { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut d = f.debug_struct("Mutex"); match self.try_lock() { Some(guard) => d.field("data", &&*guard), None => d.field("data", &format_args!("")), }; d.finish() } } impl Mutex { pub fn new(val: T) -> Self { Self(shuttle::sync::Mutex::new(val)) } } impl Mutex { #[allow(dead_code)] pub fn lock(&self) -> MutexGuard<'_, T> { MutexGuard(self.0.lock().unwrap()) } pub fn try_lock(&self) -> Option> { self.0.try_lock().ok().map(MutexGuard) } /// Lock the mutex through an Arc, returning an owned guard that can be stored pub fn lock_arc(self: &Arc) -> ArcMutexGuard where T: 'static, { // We need to lock the mutex and keep the Arc alive // Safety: We hold the Arc which keeps the Mutex alive let arc = Arc::clone(self); let guard = arc.0.lock().unwrap(); // Transmute the guard to have 'static lifetime since Arc keeps it alive let guard: shuttle::sync::MutexGuard<'static, T> = unsafe { std::mem::transmute(guard) }; ArcMutexGuard { _arc: arc, guard } } } impl Default for Mutex { fn default() -> Self { Self::new(T::default()) } } pub struct MutexGuard<'a, T: ?Sized>(shuttle::sync::MutexGuard<'a, T>); impl Deref for MutexGuard<'_, T> { type Target = T; fn deref(&self) -> &T { &self.0 } } impl DerefMut for MutexGuard<'_, T> { fn deref_mut(&mut self) -> &mut T { &mut self.0 } } /// An owned mutex guard that holds an Arc to the Mutex. /// This allows the guard to be stored across async yield points. pub struct ArcMutexGuard { _arc: Arc>, guard: shuttle::sync::MutexGuard<'static, T>, } unsafe impl Send for ArcMutexGuard {} unsafe impl Sync for ArcMutexGuard {} impl Deref for ArcMutexGuard { type Target = T; fn deref(&self) -> &T { &self.guard } } impl DerefMut for ArcMutexGuard { fn deref_mut(&mut self) -> &mut T { &mut self.guard } } pub struct RwLock(shuttle::sync::RwLock); impl fmt::Debug for RwLock { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut d = f.debug_struct("RwLock"); match self.try_read() { Some(guard) => d.field("data", &&*guard), None => d.field("data", &format_args!("")), }; d.finish() } } impl RwLock { pub fn new(val: T) -> Self { Self(shuttle::sync::RwLock::new(val)) } pub fn into_inner(self) -> T { self.0.into_inner().unwrap() } } impl RwLock { pub fn read(&self) -> RwLockReadGuard<'_, T> { RwLockReadGuard(self.0.read().unwrap()) } pub fn write(&self) -> RwLockWriteGuard<'_, T> { RwLockWriteGuard(self.0.write().unwrap()) } pub fn try_read(&self) -> Option> { self.0.try_read().ok().map(RwLockReadGuard) } pub fn try_write(&self) -> Option> { self.0.try_write().ok().map(RwLockWriteGuard) } pub fn get_mut(&mut self) -> &mut T { self.0.get_mut().unwrap() } } impl Default for RwLock { fn default() -> Self { Self::new(T::default()) } } pub struct RwLockReadGuard<'a, T: ?Sized>(shuttle::sync::RwLockReadGuard<'a, T>); impl Debug for RwLockReadGuard<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { Debug::fmt(&self.0, f) } } impl<'a, T: fmt::Display + ?Sized + 'a> fmt::Display for RwLockReadGuard<'a, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { (**self).fmt(f) } } impl Deref for RwLockReadGuard<'_, T> { type Target = T; fn deref(&self) -> &T { &self.0 } } pub struct RwLockWriteGuard<'a, T: ?Sized>(shuttle::sync::RwLockWriteGuard<'a, T>); impl Debug for RwLockWriteGuard<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { Debug::fmt(&self.0, f) } } impl<'a, T: fmt::Display + ?Sized + 'a> fmt::Display for RwLockWriteGuard<'a, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { (**self).fmt(f) } } impl Deref for RwLockWriteGuard<'_, T> { type Target = T; fn deref(&self) -> &T { &self.0 } } impl DerefMut for RwLockWriteGuard<'_, T> { fn deref_mut(&mut self) -> &mut T { &mut self.0 } } } #[cfg(not(shuttle))] mod std_adapter { pub use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; pub use std::sync::{atomic, Arc, LazyLock, OnceLock, Weak}; /// Type alias for ArcMutexGuard that hides the RawMutex type parameter pub type ArcMutexGuard = parking_lot::ArcMutexGuard; } ================================================ FILE: core/thread.rs ================================================ #[cfg(shuttle)] pub(crate) use shuttle_adapter::*; #[cfg(not(shuttle))] pub(crate) use std_adapter::*; #[expect(unused_imports)] #[cfg(shuttle)] mod shuttle_adapter { pub use shuttle::hint::spin_loop; pub use shuttle::thread::{ current, panicking, park, scope, sleep, spawn, yield_now, Builder, JoinHandle, Scope, ScopedJoinHandle, Thread, ThreadId, }; pub use shuttle::thread_local; } #[expect(unused_imports)] #[cfg(not(shuttle))] mod std_adapter { pub use std::hint::spin_loop; pub use std::thread::{ current, panicking, park, scope, sleep, spawn, yield_now, Builder, JoinHandle, Scope, ScopedJoinHandle, Thread, ThreadId, }; pub use std::thread_local; } ================================================ FILE: core/time/internal.rs ================================================ use std::ops::{Deref, Sub}; use chrono::{self, DateTime, Timelike, Utc}; use chrono::{prelude::*, DurationRound}; use turso_ext::Value; use crate::time::{Result, TimeError}; const DAYS_BEFORE_EPOCH: i64 = 719162; const TIME_BLOB_SIZE: usize = 13; const VERSION: u8 = 1; #[derive(Debug, PartialEq, PartialOrd, Eq)] pub struct Time { inner: DateTime, } #[derive(Debug, PartialEq, Eq, PartialOrd)] pub struct Duration { inner: chrono::Duration, } #[derive(strum_macros::Display, strum_macros::EnumString)] pub enum TimeField { #[strum(to_string = "millennium")] Millennium, #[strum(to_string = "century")] Century, #[strum(to_string = "decade")] Decade, #[strum(to_string = "year")] Year, #[strum(to_string = "quarter")] Quarter, #[strum(to_string = "month")] Month, #[strum(to_string = "day")] Day, #[strum(to_string = "hour")] Hour, #[strum(to_string = "minute")] Minute, #[strum(to_string = "second")] Second, #[strum(to_string = "millisecond")] MilliSecond, #[strum(to_string = "milli")] Milli, #[strum(to_string = "microsecond")] MicroSecond, #[strum(to_string = "micro")] Micro, #[strum(to_string = "nanosecond")] NanoSecond, #[strum(to_string = "nano")] Nano, #[strum(to_string = "isoyear")] IsoYear, #[strum(to_string = "isoweek")] IsoWeek, #[strum(to_string = "isodow")] IsoDow, #[strum(to_string = "yearday")] YearDay, #[strum(to_string = "weekday")] WeekDay, #[strum(to_string = "epoch")] Epoch, } #[derive(strum_macros::Display, strum_macros::EnumString)] pub enum TimeRoundField { #[strum(to_string = "millennium")] Millennium, #[strum(to_string = "century")] Century, #[strum(to_string = "decade")] Decade, #[strum(to_string = "year")] Year, #[strum(to_string = "quarter")] Quarter, #[strum(to_string = "month")] Month, #[strum(to_string = "week")] Week, #[strum(to_string = "day")] Day, #[strum(to_string = "hour")] Hour, #[strum(to_string = "minute")] Minute, #[strum(to_string = "second")] Second, #[strum(to_string = "millisecond")] MilliSecond, #[strum(to_string = "milli")] Milli, #[strum(to_string = "microsecond")] MicroSecond, #[strum(to_string = "micro")] Micro, } impl Default for Time { fn default() -> Self { Self::new() } } impl Time { /// Returns a new instance of Time with tracking UTC::now pub fn new() -> Self { Self { inner: Utc::now() } } pub fn into_blob(self) -> Value { let blob: [u8; 13] = self.into(); Value::from_blob(blob.to_vec()) } pub fn fmt_iso(&self, offset_sec: i32) -> Result { if offset_sec == 0 { if self.inner.nanosecond() == 0 { return Ok(self.inner.format("%FT%TZ").to_string()); } else { return Ok(self.inner.format("%FT%T%.9fZ").to_string()); } } // I do not see how this can error let offset = &FixedOffset::east_opt(offset_sec).ok_or(TimeError::InvalidFormat)?; let timezone_date = self.inner.with_timezone(offset); if timezone_date.nanosecond() == 0 { Ok(timezone_date.format("%FT%T%:z").to_string()) } else { Ok(timezone_date.format("%FT%T%.9f%:z").to_string()) } } pub fn fmt_datetime(&self, offset_sec: i32) -> Result { let fmt = "%F %T"; if offset_sec == 0 { return Ok(self.inner.format(fmt).to_string()); } // I do not see how this can error let offset = &FixedOffset::east_opt(offset_sec).ok_or(TimeError::InvalidFormat)?; let timezone_date = self.inner.with_timezone(offset); Ok(timezone_date.format(fmt).to_string()) } pub fn fmt_date(&self, offset_sec: i32) -> Result { let fmt = "%F"; if offset_sec == 0 { return Ok(self.inner.format(fmt).to_string()); } // I do not see how this can error let offset = &FixedOffset::east_opt(offset_sec).ok_or(TimeError::InvalidFormat)?; let timezone_date = self.inner.with_timezone(offset); Ok(timezone_date.format(fmt).to_string()) } pub fn fmt_time(&self, offset_sec: i32) -> Result { let fmt = "%T"; if offset_sec == 0 { return Ok(self.inner.format(fmt).to_string()); } // I do not see how this can error let offset = &FixedOffset::east_opt(offset_sec).ok_or(TimeError::InvalidFormat)?; let timezone_date = self.inner.with_timezone(offset); Ok(timezone_date.format(fmt).to_string()) } /// Adjust the datetime to the offset pub fn from_datetime(dt: DateTime) -> Self { Self { inner: dt } } #[allow(clippy::too_many_arguments)] pub fn time_date( year: i32, month: i32, day: i64, hour: i64, minutes: i64, seconds: i64, nano_secs: i64, offset: FixedOffset, ) -> Result> { let mut dt: NaiveDateTime = NaiveDate::from_ymd_opt(1, 1, 1) .unwrap() .and_hms_opt(0, 0, 0) .unwrap(); match year.cmp(&0) { std::cmp::Ordering::Greater => { let months = match (year - 1).unsigned_abs().checked_mul(12) { Some(m) => m, None => return Ok(None), }; dt = match dt.checked_add_months(chrono::Months::new(months)) { Some(d) => d, None => return Ok(None), }; } std::cmp::Ordering::Less => { let months = match (year - 1).unsigned_abs().checked_mul(12) { Some(m) => m, None => return Ok(None), }; dt = match dt.checked_sub_months(chrono::Months::new(months)) { Some(d) => d, None => return Ok(None), }; } std::cmp::Ordering::Equal => (), }; match month.cmp(&0) { std::cmp::Ordering::Greater => { dt = match dt.checked_add_months(chrono::Months::new((month - 1).unsigned_abs())) { Some(d) => d, None => return Ok(None), }; } std::cmp::Ordering::Less => { dt = match dt.checked_sub_months(chrono::Months::new((month - 1).unsigned_abs())) { Some(d) => d, None => return Ok(None), }; } std::cmp::Ordering::Equal => (), }; if let Some(d) = chrono::Duration::try_days(day - 1) { dt += d; } else { return Ok(None); } if let Some(d) = chrono::Duration::try_hours(hour) { dt += d; } else { return Ok(None); } if let Some(d) = chrono::Duration::try_minutes(minutes) { dt += d; } else { return Ok(None); } if let Some(d) = chrono::Duration::try_seconds(seconds) { dt += d; } else { return Ok(None); } dt += chrono::Duration::nanoseconds(nano_secs); let dt = dt .and_local_timezone(offset) .single() .map(|d| d.naive_utc()); match dt { Some(valid_dt) => Ok(Some(valid_dt.into())), None => Ok(None), } } pub fn time_add_date(self, years: i32, months: i32, days: i64) -> Result { let mut dt: NaiveDateTime = self.into(); match years.cmp(&0) { std::cmp::Ordering::Greater => { dt = dt .checked_add_months(chrono::Months::new(years.unsigned_abs() * 12)) .ok_or(TimeError::CreationError)?; } std::cmp::Ordering::Less => { dt = dt .checked_sub_months(chrono::Months::new(years.unsigned_abs() * 12)) .ok_or(TimeError::CreationError)?; } std::cmp::Ordering::Equal => (), }; match months.cmp(&0) { std::cmp::Ordering::Greater => { dt = dt .checked_add_months(chrono::Months::new(months.unsigned_abs())) .ok_or(TimeError::CreationError)? } std::cmp::Ordering::Less => { dt = dt .checked_sub_months(chrono::Months::new(months.unsigned_abs())) .ok_or(TimeError::CreationError)? } std::cmp::Ordering::Equal => (), }; dt += chrono::Duration::try_days(days).ok_or(TimeError::CreationError)?; Ok(dt.into()) } pub fn get_second(&self) -> i64 { self.inner.second() as i64 } pub fn get_nanosecond(&self) -> i64 { self.inner.timestamp_subsec_nanos() as i64 } pub fn to_unix(&self) -> i64 { self.inner.timestamp() } pub fn to_unix_milli(&self) -> i64 { self.inner.timestamp_millis() } pub fn to_unix_micro(&self) -> i64 { self.inner.timestamp_micros() } pub fn to_unix_nano(&self) -> Option { self.inner.timestamp_nanos_opt() } pub fn add_duration(&self, d: Duration) -> Self { Self { inner: self.inner + d.inner, } } pub fn sub_duration(&self, d: Duration) -> Self { Self { inner: self.inner - d.inner, } } pub fn trunc_duration(&self, d: Duration) -> Result { Ok(Self { inner: self.inner.duration_trunc(d.inner)?, }) } pub fn trunc_field(&self, field: TimeRoundField) -> Result> { use TimeRoundField::*; let year: i32; let mut month: i32 = 1; let mut week: i32 = 0; let mut day: i64 = 1; let mut hour: i64 = 0; let mut minutes: i64 = 0; let mut seconds: i64 = 0; let mut nano_secs: i64 = 0; let offset = FixedOffset::east_opt(0).unwrap(); // UTC match field { Millennium => { let millennium = (self.inner.year() / 1000) * 1000; year = millennium; } Century => { let century = (self.inner.year() / 100) * 100; year = century; } Decade => { let decade = (self.inner.year() / 10) * 10; year = decade; } Year => { year = self.inner.year(); } Quarter => { let quarter = ((self.inner.month() - 1) / 3) as i32; year = self.inner.year(); month = (quarter * 3) + 1; } Month => { year = self.inner.year(); month = self.inner.month() as i32; } Week => { let isoweek = self.inner.iso_week(); year = isoweek.year(); week = isoweek.week() as i32; } Day => { year = self.inner.year(); month = self.inner.month() as i32; day = self.inner.day() as i64; } Hour => { year = self.inner.year(); month = self.inner.month() as i32; day = self.inner.day() as i64; hour = self.inner.hour() as i64; } Minute => { year = self.inner.year(); month = self.inner.month() as i32; day = self.inner.day() as i64; hour = self.inner.hour() as i64; minutes = self.inner.minute() as i64; } Second => { year = self.inner.year(); month = self.inner.month() as i32; day = self.inner.day() as i64; hour = self.inner.hour() as i64; minutes = self.inner.minute() as i64; seconds = self.inner.second() as i64; } MilliSecond | Milli => { year = self.inner.year(); month = self.inner.month() as i32; day = self.inner.day() as i64; hour = self.inner.hour() as i64; minutes = self.inner.minute() as i64; seconds = self.inner.second() as i64; nano_secs = (self.inner.nanosecond() / 1_000_000 * 1_000_000) as i64; } MicroSecond | Micro => { year = self.inner.year(); month = self.inner.month() as i32; day = self.inner.day() as i64; hour = self.inner.hour() as i64; minutes = self.inner.minute() as i64; seconds = self.inner.second() as i64; nano_secs = (self.inner.nanosecond() / 1_000 * 1_000) as i64; } }; let ret = Self::time_date(year, month, day, hour, minutes, seconds, nano_secs, offset)?; let mut ret = match ret { Some(t) => t, None => return Ok(None), }; if week != 0 { ret = ret.time_add_date(0, 0, ((week - 1) * 7) as i64)?; } Ok(Some(ret)) } pub fn round_duration(&self, d: Duration) -> Result { Ok(Self { inner: self.inner.duration_round(d.inner)?, }) } pub fn time_get(&self, field: TimeField) -> Value { use TimeField::*; match field { Millennium => Value::from_integer((self.inner.year() / 1000) as i64), Century => Value::from_integer((self.inner.year() / 100) as i64), Decade => Value::from_integer((self.inner.year() / 10) as i64), Year => Value::from_integer(self.inner.year() as i64), Quarter => Value::from_integer(self.inner.month().div_ceil(3) as i64), Month => Value::from_integer(self.inner.month() as i64), Day => Value::from_integer(self.inner.day() as i64), Hour => Value::from_integer(self.inner.hour() as i64), Minute => Value::from_integer(self.inner.minute() as i64), Second => Value::from_float( self.inner.second() as f64 + (self.inner.nanosecond() as f64) / (1_000_000_000_f64), ), MilliSecond | Milli => { Value::from_integer((self.inner.nanosecond() / 1_000_000 % 1_000) as i64) } MicroSecond | Micro => { Value::from_integer((self.inner.nanosecond() / 1_000 % 1_000_000) as i64) } NanoSecond | Nano => { Value::from_integer((self.inner.nanosecond() % 1_000_000_000) as i64) } IsoYear => Value::from_integer(self.inner.iso_week().year() as i64), IsoWeek => Value::from_integer(self.inner.iso_week().week() as i64), IsoDow => Value::from_integer(self.inner.weekday().days_since(Weekday::Sun) as i64), YearDay => Value::from_integer(self.inner.ordinal() as i64), WeekDay => Value::from_integer(self.inner.weekday().num_days_from_sunday() as i64), Epoch => Value::from_float( self.inner.timestamp() as f64 + self.inner.nanosecond() as f64 / 1_000_000_000_f64, ), } } } impl From
VALUES (...) without compounds. // Note: values.is_empty() check ensures we use the multi-row path when // single-row VALUES contains subqueries (values extraction was skipped). if !values.is_empty() && select.body.compounds.is_empty() && matches!(&select.body.select, OneSelect::Values(values) if values.len() <= 1) { ( values.len(), program.alloc_cursor_id_keyed( CursorKey::table(table_references.joined_tables()[0].internal_id), CursorType::BTreeTable(ctx.table.clone()), ), ) } else { // Multiple rows - use coroutine for value population let yield_reg = program.alloc_register(); let jump_on_definition_label = program.allocate_label(); let start_offset_label = program.allocate_label(); program.emit_insn(Insn::InitCoroutine { yield_reg, jump_on_definition: jump_on_definition_label, start_offset: start_offset_label, }); program.preassign_label_to_next_insn(start_offset_label); let query_destination = QueryDestination::CoroutineYield { yield_reg, coroutine_implementation_start: ctx.halt_label, }; let num_result_cols = program.nested(|program| { translate_select(select, resolver, program, query_destination, connection) })?; if num_result_cols != required_column_count { crate::bail_parse_error!( "{} values for {required_column_count} columns", num_result_cols, ); } program.emit_insn(Insn::EndCoroutine { yield_reg }); program.preassign_label_to_next_insn(jump_on_definition_label); let cursor_id = program.alloc_cursor_id_keyed( CursorKey::table(table_references.joined_tables()[0].internal_id), CursorType::BTreeTable(ctx.table.clone()), ); // From SQLite /* Set useTempTable to TRUE if the result of the SELECT statement ** should be written into a temporary table (template 4). Set to ** FALSE if each output row of the SELECT can be written directly into ** the destination table (template 3). ** ** A temp table must be used if the table being updated is also one ** of the tables being read by the SELECT statement. Also use a ** temp table in the case of row triggers. */ if program.is_table_open(table) || has_insert_triggers { let temp_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(ctx.table.clone())); ctx.temp_table_ctx = Some(TempTableCtx { cursor_id: temp_cursor_id, loop_start_label: program.allocate_label(), loop_end_label: program.allocate_label(), }); program.emit_insn(Insn::OpenEphemeral { cursor_id: temp_cursor_id, is_table: true, }); // Main loop program.preassign_label_to_next_insn(ctx.loop_labels.loop_start); let yield_label = program.allocate_label(); program.emit_insn(Insn::Yield { yield_reg, end_offset: yield_label, // stays local, we’ll route at loop end subtype_clear_start_reg: 0, subtype_clear_count: 0, }); let record_reg = program.alloc_register(); let affinity_str = if columns.is_empty() { ctx.table .columns .iter() .filter(|col| !col.hidden()) .map(|col| col.affinity_with_strict(ctx.table.is_strict).aff_mask()) .collect::() } else { columns .iter() .map(|col_name| { let column_name = normalize_ident(col_name.as_str()); if ROWID_STRS .iter() .any(|s| s.eq_ignore_ascii_case(&column_name)) { return Ok(Affinity::Integer.aff_mask()); } table .get_column_by_name(&column_name) .map(|(_, col)| { col.affinity_with_strict(ctx.table.is_strict).aff_mask() }) .ok_or_else(|| { crate::error::LimboError::ParseError(format!( "table {} has no column named {}", table.get_name(), column_name )) }) }) .collect::>()? }; program.emit_insn(Insn::MakeRecord { start_reg: to_u16(program.reg_result_cols_start.unwrap_or(yield_reg + 1)), count: to_u16(num_result_cols), dest_reg: to_u16(record_reg), index_name: None, affinity_str: Some(affinity_str), }); let rowid_reg = program.alloc_register(); program.emit_insn(Insn::NewRowid { cursor: temp_cursor_id, rowid_reg, prev_largest_reg: 0, }); program.emit_insn(Insn::Insert { cursor: temp_cursor_id, key_reg: rowid_reg, record_reg, // since we are not doing an Insn::NewRowid or an Insn::NotExists here, we need to seek to ensure the insertion happens in the correct place. flag: InsertFlags::new().require_seek(), table_name: "".to_string(), }); // loop back program.emit_insn(Insn::Goto { target_pc: ctx.loop_labels.loop_start, }); program.preassign_label_to_next_insn(yield_label); program.emit_insn(Insn::OpenWrite { cursor_id, root_page: RegisterOrLiteral::Literal(ctx.table.root_page), db: ctx.database_id, }); } else { program.emit_insn(Insn::OpenWrite { cursor_id, root_page: RegisterOrLiteral::Literal(ctx.table.root_page), db: ctx.database_id, }); program.preassign_label_to_next_insn(ctx.loop_labels.loop_start); // on EOF, jump to select_exhausted to check FK constraints let select_exhausted = program.allocate_label(); ctx.loop_labels.select_exhausted = Some(select_exhausted); program.emit_insn(Insn::Yield { yield_reg, end_offset: select_exhausted, subtype_clear_start_reg: 0, subtype_clear_count: 0, }); } ctx.yield_reg_opt = Some(yield_reg); (num_result_cols, cursor_id) } } InsertBody::DefaultValues => { let num_values = table.columns().len(); let is_strict = table.is_strict(); values.extend(table.columns().iter().map(|c| { c.default.clone().unwrap_or_else(|| { if let Some(type_def) = resolver.schema().get_type_def(&c.ty_str, is_strict) { if let Some(ref default_expr) = type_def.default { return default_expr.clone(); } } Box::new(ast::Expr::Literal(ast::Literal::Null)) }) })); ( num_values, program.alloc_cursor_id_keyed( CursorKey::table(table_references.joined_tables()[0].internal_id), CursorType::BTreeTable(ctx.table.clone()), ), ) } }; ctx.num_values = num_values; ctx.cursor_id = cursor_id; Ok(()) } #[derive(Clone, Copy)] pub struct AutoincMeta { seq_cursor_id: usize, r_seq: usize, r_seq_rowid: usize, table_name_reg: usize, } pub static ROWID_COLUMN: std::sync::LazyLock = std::sync::LazyLock::new(|| { Column::new( None, // name String::new(), // type string None, // default None, // generated schema::Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, hidden: false, unique: false, notnull_conflict_clause: None, }, ) }); /// Represents how a table should be populated during an INSERT. #[derive(Debug)] pub struct Insertion<'a> { /// The integer key ("rowid") provided to the VDBE. key: InsertionKey<'a>, /// The column values that will be fed to the MakeRecord instruction to insert the row. /// If the table has a rowid alias column, it will also be included in this record, /// but a NULL will be stored for it. col_mappings: Vec>, /// The register that will contain the record built using the MakeRecord instruction. record_reg: usize, } impl<'a> Insertion<'a> { /// Return the register that contains the rowid. pub fn key_register(&self) -> usize { self.key.register() } /// Return the first register of the values that used to build the record /// for the main table insert. pub fn first_col_register(&self) -> usize { self.col_mappings .first() .expect("columns must be present") .register } /// Return the register that contains the record built using the MakeRecord instruction. pub fn record_register(&self) -> usize { self.record_reg } /// Returns the column mapping for a given column name. pub fn get_col_mapping_by_name(&self, name: &str) -> Option<&ColMapping<'a>> { if let InsertionKey::RowidAlias(mapping) = &self.key { // If the key is a rowid alias, a NULL is emitted as the column value, // so we need to return the key mapping instead so that the non-NULL rowid is used // for the index insert. if mapping .column .name .as_ref() .is_some_and(|n| n.eq_ignore_ascii_case(name)) { return Some(mapping); } } self.col_mappings.iter().find(|col| { col.column .name .as_ref() .is_some_and(|n| n.eq_ignore_ascii_case(name)) }) } } #[derive(Debug)] enum InsertionKey<'a> { /// Rowid is not provided by user and will be autogenerated. Autogenerated { register: usize }, /// Rowid is provided via the 'rowid' keyword. LiteralRowid { value_index: Option, register: usize, }, /// Rowid is provided via a rowid alias column. RowidAlias(ColMapping<'a>), } impl InsertionKey<'_> { fn register(&self) -> usize { match self { InsertionKey::Autogenerated { register } => *register, InsertionKey::LiteralRowid { register, .. } => *register, InsertionKey::RowidAlias(x) => x.register, } } fn is_provided_by_user(&self) -> bool { !matches!(self, InsertionKey::Autogenerated { .. }) } fn column_name(&self) -> &str { match self { InsertionKey::RowidAlias(x) => x .column .name .as_ref() .expect("rowid alias column must be present") .as_str(), InsertionKey::LiteralRowid { .. } => ROWID_STRS[0], InsertionKey::Autogenerated { .. } => ROWID_STRS[0], } } } /// Represents how a column in a table should be populated during an INSERT. /// In a vector of [ColMapping], the index of a given [ColMapping] is /// the position of the column in the table. #[derive(Debug)] pub struct ColMapping<'a> { /// Column definition pub column: &'a Column, /// Index of the value to use from a tuple in the insert statement. /// This is needed because the values in the insert statement are not necessarily /// in the same order as the columns in the table, nor do they necessarily contain /// all of the columns in the table. /// If None, a NULL will be emitted for the column, unless it has a default value. /// A NULL rowid alias column's value will be autogenerated. pub value_index: Option, /// Register where the value will be stored for insertion into the table. pub register: usize, } /// Resolves how each column in a table should be populated during an INSERT. /// Returns an [Insertion] struct that contains the key and record for the insertion. fn build_insertion<'a>( program: &mut ProgramBuilder, table: &'a Table, columns: &'a [ast::Name], num_values: usize, ) -> Result> { let table_columns = table.columns(); let rowid_register = program.alloc_register(); let mut insertion_key = InsertionKey::Autogenerated { register: rowid_register, }; let mut column_mappings = table .columns() .iter() .map(|c| ColMapping { column: c, value_index: None, register: program.alloc_register(), }) .collect::>(); if columns.is_empty() { // Case 1: No columns specified - map values to columns in order if num_values != table_columns.iter().filter(|c| !c.hidden()).count() { crate::bail_parse_error!( "table {} has {} columns but {} values were supplied", &table.get_name(), table_columns.len(), num_values ); } let mut value_idx = 0; for (i, col) in table_columns.iter().enumerate() { if col.hidden() { // Hidden columns are not taken into account. continue; } if col.is_rowid_alias() { insertion_key = InsertionKey::RowidAlias(ColMapping { column: col, value_index: Some(value_idx), register: rowid_register, }); } else { column_mappings[i].value_index = Some(value_idx); } value_idx += 1; } } else { // Case 2: Columns specified - map named columns to their values // Map each named column to its value index for (value_index, column_name) in columns.iter().enumerate() { let column_name = normalize_ident(column_name.as_str()); if let Some((idx_in_table, col_in_table)) = table.get_column_by_name(&column_name) { // Named column if col_in_table.is_rowid_alias() { insertion_key = InsertionKey::RowidAlias(ColMapping { column: col_in_table, value_index: Some(value_index), register: rowid_register, }); } else if column_mappings[idx_in_table].value_index.is_none() { column_mappings[idx_in_table].value_index = Some(value_index); } } else if ROWID_STRS .iter() .any(|s| s.eq_ignore_ascii_case(&column_name)) { // Explicit use of the 'rowid' keyword if let Some(col_in_table) = table.columns().iter().find(|c| c.is_rowid_alias()) { insertion_key = InsertionKey::RowidAlias(ColMapping { column: col_in_table, value_index: Some(value_index), register: rowid_register, }); } else { insertion_key = InsertionKey::LiteralRowid { value_index: Some(value_index), register: rowid_register, }; } } else { crate::bail_parse_error!( "table {} has no column named {}", &table.get_name(), column_name ); } } } Ok(Insertion { key: insertion_key, col_mappings: column_mappings, record_reg: program.alloc_register(), }) } /// Populates the column registers with values for multiple rows. /// This is used for INSERT INTO
VALUES (...), (...), ... or INSERT INTO
SELECT ... /// which use either a coroutine or an ephemeral table as the value source. fn translate_rows_multiple<'short, 'long: 'short>( program: &mut ProgramBuilder, insertion: &'short Insertion<'long>, yield_reg: usize, resolver: &Resolver, temp_table_ctx: &Option, is_strict: bool, ) -> Result<()> { if let Some(ref temp_table_ctx) = temp_table_ctx { // Rewind loop to read from ephemeral table program.emit_insn(Insn::Rewind { cursor_id: temp_table_ctx.cursor_id, pc_if_empty: temp_table_ctx.loop_end_label, }); program.preassign_label_to_next_insn(temp_table_ctx.loop_start_label); } let translate_value_fn = |prg: &mut ProgramBuilder, value_index: usize, column_register: usize| { if let Some(temp_table_ctx) = temp_table_ctx { prg.emit_insn(Insn::Column { cursor_id: temp_table_ctx.cursor_id, column: value_index, dest: column_register, default: None, }); } else { prg.emit_insn(Insn::Copy { src_reg: yield_reg + value_index, dst_reg: column_register, extra_amount: 0, }); } Ok(()) }; translate_rows_base(program, insertion, translate_value_fn, resolver, is_strict) } /// Populates the column registers with values for a single row fn translate_rows_single( program: &mut ProgramBuilder, value: &[Box], insertion: &Insertion, resolver: &Resolver, is_strict: bool, ) -> Result<()> { let translate_value_fn = |prg: &mut ProgramBuilder, value_index: usize, column_register: usize| -> Result<()> { translate_expr_no_constant_opt( prg, None, value.get(value_index).unwrap_or_else(|| { panic!("value index out of bounds: {value_index} for value: {value:?}") }), column_register, resolver, NoConstantOptReason::RegisterReuse, )?; Ok(()) }; translate_rows_base(program, insertion, translate_value_fn, resolver, is_strict) } /// Translate the key and the columns of the insertion. /// This function is called by both [translate_rows_single] and [translate_rows_multiple], /// each providing a different [translate_value_fn] implementation, because for multiple rows /// we need to emit the values in a loop, from either an ephemeral table or a coroutine, /// whereas for the single row the translation happens in a single pass without looping. fn translate_rows_base<'short, 'long: 'short>( program: &mut ProgramBuilder, insertion: &'short Insertion<'long>, mut translate_value_fn: impl FnMut(&mut ProgramBuilder, usize, usize) -> Result<()>, resolver: &Resolver, is_strict: bool, ) -> Result<()> { translate_key( program, insertion, &mut translate_value_fn, resolver, is_strict, )?; for col in insertion.col_mappings.iter() { translate_column( program, col.column, col.register, col.value_index, &mut translate_value_fn, resolver, is_strict, )?; } Ok(()) } /// Translate the [InsertionKey]. fn translate_key( program: &mut ProgramBuilder, insertion: &Insertion, mut translate_value_fn: impl FnMut(&mut ProgramBuilder, usize, usize) -> Result<()>, resolver: &Resolver, is_strict: bool, ) -> Result<()> { match &insertion.key { InsertionKey::RowidAlias(rowid_alias_column) => translate_column( program, rowid_alias_column.column, rowid_alias_column.register, rowid_alias_column.value_index, &mut translate_value_fn, resolver, is_strict, ), InsertionKey::LiteralRowid { value_index, register, } => translate_column( program, &ROWID_COLUMN, *register, *value_index, &mut translate_value_fn, resolver, is_strict, ), InsertionKey::Autogenerated { .. } => Ok(()), // will be populated later } } fn translate_column( program: &mut ProgramBuilder, column: &Column, column_register: usize, value_index: Option, translate_value_fn: &mut impl FnMut(&mut ProgramBuilder, usize, usize) -> Result<()>, resolver: &Resolver, is_strict: bool, ) -> Result<()> { if let Some(value_index) = value_index { translate_value_fn(program, value_index, column_register)?; } else if column.is_rowid_alias() { // Although a non-NULL integer key is used for the insertion key, // the rowid alias column is emitted as NULL. program.emit_insn(Insn::SoftNull { reg: column_register, }); } else if column.hidden() { // Emit NULL for not-explicitly-mentioned hidden columns, even ignoring DEFAULT. program.emit_insn(Insn::Null { dest: column_register, dest_end: None, }); } else if let Some(default_expr) = column.default.as_ref() { translate_expr(program, None, default_expr, column_register, resolver)?; } else if let Some(type_def) = resolver.schema().get_type_def(&column.ty_str, is_strict) { if let Some(ref default_expr) = type_def.default { translate_expr(program, None, default_expr, column_register, resolver)?; } else { program.emit_insn(Insn::Null { dest: column_register, dest_end: None, }); } } else { let nullable = !column.notnull() && !column.is_rowid_alias(); if !nullable { crate::bail_parse_error!( "column {} is not nullable", column .name .as_ref() .expect("column name must be present") .as_str() ); } program.emit_insn(Insn::Null { dest: column_register, dest_end: None, }); } Ok(()) } /// Emit bytecode to check PRIMARY KEY uniqueness constraint. /// Handles ON REPLACE (delete conflicting row) and UPSERT routing. fn emit_pk_uniqueness_check( program: &mut ProgramBuilder, ctx: &mut InsertEmitCtx, resolver: &mut Resolver, insertion: &Insertion, position: Option, upsert_catch_all: Option, preflight: &mut PreflightCtx, ) -> Result<()> { let make_record_label = program.allocate_label(); program.emit_insn(Insn::NotExists { cursor: ctx.cursor_id, rowid_reg: insertion.key_register(), target_pc: make_record_label, }); let rowid_column_name = insertion.key.column_name(); // Conflict on rowid: attempt to route through UPSERT if it targets the PK, otherwise raise constraint. // emit Halt for every case *except* when upsert handles the conflict 'emit_halt: { if preflight.on_replace { // copy the conflicting rowid into the key register and delete the existing row inline program.emit_insn(Insn::Copy { src_reg: insertion.key_register(), dst_reg: ctx.conflict_rowid_reg, extra_amount: 0, }); emit_replace_delete_conflicting_row( program, resolver, preflight.connection, ctx, preflight.table_references, )?; program.emit_insn(Insn::Goto { target_pc: make_record_label, }); break 'emit_halt; } if matches!(preflight.effective_on_conflict, ResolveType::Ignore) { // IGNORE: skip this row entirely on PK conflict program.emit_insn(Insn::Goto { target_pc: ctx.loop_labels.row_done, }); break 'emit_halt; } if let Some(position) = position.or(upsert_catch_all) { // PK conflict: the conflicting rowid is exactly the attempted key program.emit_insn(Insn::Copy { src_reg: insertion.key_register(), dst_reg: ctx.conflict_rowid_reg, extra_amount: 0, }); program.emit_insn(Insn::Goto { target_pc: preflight.upsert_actions[position].1, }); break 'emit_halt; } let raw_desc = format!("{}.{}", ctx.table.name, rowid_column_name); let (description, on_error) = halt_desc_and_on_error( &raw_desc, preflight.effective_on_conflict, program.has_statement_conflict, ); program.emit_insn(Insn::Halt { err_code: SQLITE_CONSTRAINT_PRIMARYKEY, description, on_error, description_reg: None, }); } program.preassign_label_to_next_insn(make_record_label); Ok(()) } /// Emit bytecode to check index uniqueness constraint. /// Handles partial index predicates, ON REPLACE, UPSERT routing, and non-unique indexes. #[allow(clippy::too_many_arguments)] fn emit_index_uniqueness_check( program: &mut ProgramBuilder, ctx: &mut InsertEmitCtx, resolver: &mut Resolver, insertion: &Insertion, index: &Index, position: Option, upsert_catch_all: Option, preflight: &mut PreflightCtx, ) -> Result<()> { // find which cursor we opened earlier for this index let idx_cursor_id = ctx .idx_cursors .iter() .find(|(name, _, _)| name == &index.name) .map(|(_, _, c_id)| *c_id) .expect("no cursor found for index"); // For partial indexes, evaluate the WHERE clause and skip if false let maybe_skip_probe_label = emit_partial_index_check(program, resolver, index, insertion)?; let num_cols = index.columns.len(); // allocate scratch registers for the index columns plus rowid let idx_start_reg = program.alloc_registers(num_cols + 1); // build unpacked key [idx_start_reg .. idx_start_reg+num_cols-1], and rowid in last reg, // copy each index column from the table's column registers into these scratch regs for (i, idx_col) in index.columns.iter().enumerate() { emit_index_column_value_for_insert( program, resolver, insertion, ctx.table, idx_col, idx_start_reg + i, )?; } // last register is the rowid program.emit_insn(Insn::Copy { src_reg: insertion.key_register(), dst_reg: idx_start_reg + num_cols, extra_amount: 0, }); if index.unique { emit_unique_index_check( program, ctx, resolver, index, idx_cursor_id, idx_start_reg, num_cols, position, upsert_catch_all, preflight, )?; } else { // Non-unique index: insert eagerly only for REPLACE (which doesn't use commit phase). // For UPSERT and ABORT/FAIL/IGNORE/ROLLBACK, defer to commit phase. if preflight.on_replace { let record_reg = program.alloc_register(); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(idx_start_reg), count: to_u16(num_cols + 1), dest_reg: to_u16(record_reg), index_name: Some(index.name.clone()), affinity_str: None, }); program.emit_insn(Insn::IdxInsert { cursor_id: idx_cursor_id, record_reg, unpacked_start: Some(idx_start_reg), unpacked_count: Some((num_cols + 1) as u16), flags: IdxInsertFlags::new().nchange(true), }); } } // Close the partial-index skip (preflight) if let Some(lbl) = maybe_skip_probe_label { program.resolve_label(lbl, program.offset()); } Ok(()) } /// Emit bytecode for unique index conflict detection and handling. #[allow(clippy::too_many_arguments)] fn emit_unique_index_check( program: &mut ProgramBuilder, ctx: &mut InsertEmitCtx, resolver: &mut Resolver, index: &Index, idx_cursor_id: usize, idx_start_reg: usize, num_cols: usize, position: Option, upsert_catch_all: Option, preflight: &mut PreflightCtx, ) -> Result<()> { let aff = index .columns .iter() .map(|ic| { if ic.expr.is_some() { Affinity::Blob.aff_mask() } else { ctx.table.columns[ic.pos_in_table] .affinity_with_strict(ctx.table.is_strict) .aff_mask() } }) .collect::(); program.emit_insn(Insn::Affinity { start_reg: idx_start_reg, count: NonZeroUsize::new(num_cols).expect("nonzero col count"), affinities: aff, }); if !preflight.upsert_actions.is_empty() { let next_check = program.allocate_label(); program.emit_insn(Insn::NoConflict { cursor_id: idx_cursor_id, target_pc: next_check, record_reg: idx_start_reg, num_regs: num_cols, }); // Conflict detected, figure out if this UPSERT handles the conflict if let Some(position) = position.or(upsert_catch_all) { match &preflight.upsert_actions[position].2.do_clause { UpsertDo::Nothing => { // Bail out without writing anything program.emit_insn(Insn::Goto { target_pc: ctx.loop_labels.row_done, }); } UpsertDo::Set { .. } => { // Route to DO UPDATE: capture conflicting rowid then jump program.emit_insn(Insn::IdxRowId { cursor_id: idx_cursor_id, dest: ctx.conflict_rowid_reg, }); program.emit_insn(Insn::Goto { target_pc: preflight.upsert_actions[position].1, }); } } } // No matching UPSERT handler so we emit constraint error // (if conflict clause matched - VM will jump to later instructions and skip halt) let raw_desc = format_unique_violation_desc(ctx.table.name.as_str(), index); let (description, on_error) = halt_desc_and_on_error( &raw_desc, preflight.effective_on_conflict, program.has_statement_conflict, ); program.emit_insn(Insn::Halt { err_code: SQLITE_CONSTRAINT_UNIQUE, description, on_error, description_reg: None, }); // continue preflight with next constraint program.preassign_label_to_next_insn(next_check); } else { // No UPSERT: probe for conflicts. let ok = program.allocate_label(); program.emit_insn(Insn::NoConflict { cursor_id: idx_cursor_id, target_pc: ok, record_reg: idx_start_reg, num_regs: num_cols, }); if preflight.on_replace { // REPLACE: delete conflicting row immediately, then insert eagerly. program.emit_insn(Insn::IdxRowId { cursor_id: idx_cursor_id, dest: ctx.conflict_rowid_reg, }); emit_replace_delete_conflicting_row( program, resolver, preflight.connection, ctx, preflight.table_references, )?; program.emit_insn(Insn::Goto { target_pc: ok }); } else if matches!(preflight.effective_on_conflict, ResolveType::Ignore) { // IGNORE: skip this row entirely on unique conflict. program.emit_insn(Insn::Goto { target_pc: ctx.loop_labels.row_done, }); } else { // ABORT/FAIL/ROLLBACK: halt on conflict. let raw_desc = format_unique_violation_desc(ctx.table.name.as_str(), index); let (description, on_error) = halt_desc_and_on_error( &raw_desc, preflight.effective_on_conflict, program.has_statement_conflict, ); program.emit_insn(Insn::Halt { err_code: SQLITE_CONSTRAINT_UNIQUE, description, on_error, description_reg: None, }); } program.preassign_label_to_next_insn(ok); if preflight.on_replace { // REPLACE: insert index entry eagerly (right after delete). // IdxDelete repositions the cursor, so we must NOT use USE_SEEK. let record_reg = program.alloc_register(); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(idx_start_reg), count: to_u16(num_cols + 1), dest_reg: to_u16(record_reg), index_name: Some(index.name.clone()), affinity_str: None, }); program.emit_insn(Insn::IdxInsert { cursor_id: idx_cursor_id, record_reg, unpacked_start: Some(idx_start_reg), unpacked_count: Some((num_cols + 1) as u16), flags: IdxInsertFlags::new().nchange(true), }); } // For non-REPLACE cases (ABORT/FAIL/IGNORE/ROLLBACK), index inserts are // deferred to the commit phase after all constraint checks pass. // This prevents stale index entries when a later constraint check fails. } Ok(()) } // Preflight phase: evaluate each applicable UNIQUE constraint and probe with NoConflict. // If any probe hits: // DO NOTHING -> jump to row_done_label. // // DO UPDATE (matching target) -> fetch conflicting rowid and jump to `upsert_entry`. // // otherwise, raise SQLITE_CONSTRAINT_UNIQUE fn emit_preflight_constraint_checks( program: &mut ProgramBuilder, ctx: &mut InsertEmitCtx, resolver: &mut Resolver, insertion: &Insertion, constraints: &ConstraintsToCheck, preflight: &mut PreflightCtx, ) -> Result<()> { let mut seen_replace = false; for (constraint, position) in &constraints.constraints_to_check { // Compute per-constraint effective conflict resolution: // Statement-level OR clause overrides; otherwise use constraint's clause. let effective = if ctx.statement_on_conflict.is_some() { ctx.on_conflict } else { match constraint { ResolvedUpsertTarget::PrimaryKey => { // Use rowid_alias_conflict_clause from BTreeTable, which is preserved // even for rowid-alias PKs (whose UniqueSet is removed). ctx.table .rowid_alias_conflict_clause .unwrap_or(ResolveType::Abort) } ResolvedUpsertTarget::Index(index) => { index.on_conflict.unwrap_or(ResolveType::Abort) } ResolvedUpsertTarget::CatchAll => unreachable!(), } }; // REPLACE constraints must sort after all non-REPLACE ones // (schema.rs:add_index + IPK deferral ensure this). if effective == ResolveType::Replace { seen_replace = true; } else { turso_assert!( !seen_replace, "non-REPLACE constraint after REPLACE constraint — sort order invariant violated" ); } let effective_on_replace = matches!(effective, ResolveType::Replace) && preflight.upsert_actions.is_empty(); preflight.on_replace = effective_on_replace; preflight.effective_on_conflict = effective; match constraint { ResolvedUpsertTarget::PrimaryKey => { emit_pk_uniqueness_check( program, ctx, resolver, insertion, *position, constraints.upsert_catch_all_position, preflight, )?; } ResolvedUpsertTarget::Index(index) => { emit_index_uniqueness_check( program, ctx, resolver, insertion, index, *position, constraints.upsert_catch_all_position, preflight, )?; } ResolvedUpsertTarget::CatchAll => unreachable!(), } } Ok(()) } // TODO: comeback here later to apply the same improvements on select fn translate_virtual_table_insert( program: &mut ProgramBuilder, virtual_table: Arc, columns: Vec, mut body: InsertBody, on_conflict: Option, resolver: &Resolver, connection: &Arc, ) -> Result<()> { #[cfg(not(feature = "cli_only"))] let _ = connection; let allow_dbpage_write = { #[cfg(feature = "cli_only")] { virtual_table.name == crate::dbpage::DBPAGE_TABLE_NAME && connection.db.opts.unsafe_testing } #[cfg(not(feature = "cli_only"))] { false } }; if virtual_table.readonly() && !allow_dbpage_write { crate::bail_constraint_error!("Table is read-only: {}", virtual_table.name); } let (num_values, value) = match &mut body { InsertBody::Select(select, None) => match &mut select.body.select { OneSelect::Values(values) => (values[0].len(), values.pop().unwrap()), _ => crate::bail_parse_error!("Virtual tables only support VALUES clause in INSERT"), }, InsertBody::DefaultValues => (0, vec![]), _ => crate::bail_parse_error!("Unsupported INSERT body for virtual tables"), }; let table = Table::Virtual(virtual_table.clone()); let cursor_id = program.alloc_cursor_id(CursorType::VirtualTable(virtual_table)); program.emit_insn(Insn::VOpen { cursor_id }); if !allow_dbpage_write { program.emit_insn(Insn::VBegin { cursor_id }); } /* * * Inserts for virtual tables are done in a single step. * argv[0] = (NULL for insert) */ let registers_start = program.alloc_register(); program.emit_insn(Insn::Null { dest: registers_start, dest_end: None, }); /* * * argv[1] = (rowid for insert - NULL in most cases) * argv[2..] = column values * */ let insertion = build_insertion(program, &table, &columns, num_values)?; translate_rows_single(program, &value, &insertion, resolver, false)?; let conflict_action = on_conflict.as_ref().map(|c| c.bit_value()).unwrap_or(0) as u16; program.emit_insn(Insn::VUpdate { cursor_id, arg_count: insertion.col_mappings.len() + 2, // +1 for NULL, +1 for rowid start_reg: registers_start, conflict_action, }); program.emit_insn(Insn::Close { cursor_id }); let halt_label = program.allocate_label(); program.resolve_label(halt_label, program.offset()); Ok(()) } /// makes sure that an AUTOINCREMENT table has a sequence row in `sqlite_sequence`, inserting one with 0 if missing. fn ensure_sequence_initialized( program: &mut ProgramBuilder, resolver: &Resolver, table: &schema::BTreeTable, database_id: usize, ) -> Result<()> { let seq_table = get_valid_sqlite_sequence_table(resolver, database_id)?; let seq_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(seq_table.clone())); program.emit_insn(Insn::OpenWrite { cursor_id: seq_cursor_id, root_page: seq_table.root_page.into(), db: database_id, }); let table_name_reg = program.emit_string8_new_reg(table.name.clone()); let loop_start_label = program.allocate_label(); let entry_exists_label = program.allocate_label(); let insert_new_label = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: seq_cursor_id, pc_if_empty: insert_new_label, }); program.preassign_label_to_next_insn(loop_start_label); let name_col_reg = program.alloc_register(); program.emit_column_or_rowid(seq_cursor_id, 0, name_col_reg); program.emit_insn(Insn::Eq { lhs: table_name_reg, rhs: name_col_reg, target_pc: entry_exists_label, flags: Default::default(), collation: None, }); program.emit_insn(Insn::Next { cursor_id: seq_cursor_id, pc_if_next: loop_start_label, }); program.preassign_label_to_next_insn(insert_new_label); let record_reg = program.alloc_register(); let record_start_reg = program.alloc_registers(2); let zero_reg = program.alloc_register(); program.emit_insn(Insn::Integer { dest: zero_reg, value: 0, }); program.emit_insn(Insn::Copy { src_reg: table_name_reg, dst_reg: record_start_reg, extra_amount: 0, }); program.emit_insn(Insn::Copy { src_reg: zero_reg, dst_reg: record_start_reg + 1, extra_amount: 0, }); let affinity_str = seq_table .columns .iter() .map(|c| c.affinity().aff_mask()) .collect(); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(record_start_reg), count: to_u16(2), dest_reg: to_u16(record_reg), index_name: None, affinity_str: Some(affinity_str), }); let new_rowid_reg = program.alloc_register(); program.emit_insn(Insn::NewRowid { cursor: seq_cursor_id, rowid_reg: new_rowid_reg, prev_largest_reg: 0, }); program.emit_insn(Insn::Insert { cursor: seq_cursor_id, key_reg: new_rowid_reg, record_reg, flag: InsertFlags::new(), table_name: SQLITE_SEQUENCE_TABLE_NAME.to_string(), }); program.preassign_label_to_next_insn(entry_exists_label); program.emit_insn(Insn::Close { cursor_id: seq_cursor_id, }); Ok(()) } #[inline] /// Build the UNIQUE constraint error description to match sqlite /// single column: `t.c1` /// multi-column: `t.(k, c1)` /// For constraint-level FAIL/ROLLBACK ON CONFLICT, pre-format the description /// and set on_error so the VM's halt() produces Raise(rt, msg) with correct semantics. /// `has_statement_conflict` should be true when the statement has its own OR clause /// (e.g. INSERT OR FAIL), in which case program.resolve_type already handles it. pub(crate) fn halt_desc_and_on_error( raw_desc: &str, effective: ResolveType, has_statement_conflict: bool, ) -> (String, Option) { if has_statement_conflict { return (raw_desc.to_string(), None); } match effective { ResolveType::Fail | ResolveType::Rollback => ( format!("UNIQUE constraint failed: {raw_desc} (19)"), Some(effective), ), _ => (raw_desc.to_string(), None), } } pub fn format_unique_violation_desc(table_name: &str, index: &Index) -> String { if index.columns.len() == 1 { let mut s = String::with_capacity(table_name.len() + 1 + index.columns[0].name.len()); s.push_str(table_name); s.push('.'); s.push_str(&index.columns[0].name); s } else { let mut s = String::with_capacity(table_name.len() + 3 + 4 * index.columns.len()); s.push_str(table_name); s.push_str(".("); s.push_str( &index .columns .iter() .map(|c| c.name.as_str()) .collect::>() .join(", "), ); s.push(')'); s } } /// Rewrite WHERE clause for partial index to reference insertion registers pub fn rewrite_partial_index_where( expr: &mut ast::Expr, insertion: &Insertion, ) -> crate::Result { let col_reg = |name: &str| -> Option { if ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(name)) { Some(insertion.key_register()) } else if let Some(c) = insertion.get_col_mapping_by_name(name) { if c.column.is_rowid_alias() { Some(insertion.key_register()) } else { Some(c.register) } } else { None } }; walk_expr_mut( expr, &mut |e: &mut ast::Expr| -> crate::Result { match e { // NOTE: should not have ANY Expr::Columns bound to the expr Expr::Id(name) => { let normalized = normalize_ident(name.as_str()); if let Some(reg) = col_reg(&normalized) { *e = Expr::Register(reg); } } Expr::Qualified(_, col) | Expr::DoublyQualified(_, _, col) => { let normalized = normalize_ident(col.as_str()); if let Some(reg) = col_reg(&normalized) { *e = Expr::Register(reg); } } _ => {} } Ok(WalkControl::Continue) }, ) } /// For an index expression, rewrite column references to use the insertion registers. fn rewrite_index_expr_for_insertion(expr: &mut ast::Expr, insertion: &Insertion) -> Result<()> { let mut missing_column = None; let col_reg = |name: &str| -> Option { if ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(name)) { Some(insertion.key_register()) } else if let Some(c) = insertion.get_col_mapping_by_name(name) { if c.column.is_rowid_alias() { Some(insertion.key_register()) } else { Some(c.register) } } else { None } }; walk_expr_mut( expr, &mut |e: &mut ast::Expr| -> crate::Result { match e { Expr::Id(name) | Expr::Name(name) => { let normalized = normalize_ident(name.as_str()); if let Some(reg) = col_reg(&normalized) { *e = Expr::Register(reg); } else { missing_column = Some(normalized); } } Expr::Qualified(_, col) | Expr::DoublyQualified(_, _, col) => { let normalized = normalize_ident(col.as_str()); if let Some(reg) = col_reg(&normalized) { *e = Expr::Register(reg); } else { missing_column = Some(normalized); } } _ => {} } Ok(if missing_column.is_some() { WalkControl::SkipChildren } else { WalkControl::Continue }) }, )?; if let Some(col) = missing_column { return Err(LimboError::PlanningError(format!( "Column not found in INSERT: {col}" ))); } Ok(()) } fn emit_index_column_value_for_insert( program: &mut ProgramBuilder, resolver: &Resolver, insertion: &Insertion, table: &BTreeTable, idx_col: &IndexColumn, dest_reg: usize, ) -> Result<()> { if let Some(expr) = &idx_col.expr { let mut expr = expr.as_ref().clone(); rewrite_index_expr_for_insertion(&mut expr, insertion)?; // After rewrite, Expr::Register nodes reference encoded column registers. // Decode custom type registers so the expression evaluates on user-facing // values, matching what SELECT / CREATE INDEX see. crate::translate::expr::decode_custom_type_registers_in_expr( program, resolver, &mut expr, &table.columns, insertion.first_col_register(), Some(insertion.key_register()), table.is_strict, )?; translate_expr_no_constant_opt( program, Some(&TableReferences::new_empty()), &expr, dest_reg, resolver, NoConstantOptReason::RegisterReuse, )?; } else { let Some(cm) = insertion.get_col_mapping_by_name(&idx_col.name) else { return Err(LimboError::PlanningError( "Column not found in INSERT".to_string(), )); }; // For rowid alias columns (INTEGER PRIMARY KEY), the actual value lives // in the key register, not the column register (which may hold NULL, // e.g. when the rowid is auto-generated). let src_reg = if cm.column.is_rowid_alias() { insertion.key_register() } else { cm.register }; program.emit_insn(Insn::Copy { src_reg, dst_reg: dest_reg, extra_amount: 0, }); } Ok(()) } struct ConstraintsToCheck { constraints_to_check: Vec<(ResolvedUpsertTarget, Option)>, upsert_catch_all_position: Option, } /// Context for preflight constraint checks struct PreflightCtx<'a, 'b> { /// UPSERT action handlers (target, label, upsert clause) upsert_actions: &'a [(ResolvedUpsertTarget, BranchOffset, Box)], /// Whether ON CONFLICT REPLACE is active globally (without UPSERT). /// This is true when the statement has INSERT OR REPLACE (applies to all constraints). on_replace: bool, /// The effective conflict resolution for the current constraint being checked. /// Updated per-constraint in emit_preflight_constraint_checks. effective_on_conflict: ResolveType, /// Database connection for FK checks connection: &'a Arc, /// Table references for expression evaluation table_references: &'b mut TableReferences, } #[allow(clippy::too_many_arguments)] fn build_constraints_to_check( table_name: &str, upsert_actions: &[(ResolvedUpsertTarget, BranchOffset, Box)], has_user_provided_rowid: bool, resolver: &Resolver, _connection: &Arc, database_id: usize, rowid_alias_conflict_clause: Option, has_statement_conflict: bool, ) -> ConstraintsToCheck { let mut constraints_to_check = Vec::new(); if has_user_provided_rowid { // Check uniqueness constraint for rowid if it was provided by user. // When the DB allocates it there are no need for separate uniqueness checks. let position = upsert_actions .iter() .position(|(target, ..)| matches!(target, ResolvedUpsertTarget::PrimaryKey)); constraints_to_check.push((ResolvedUpsertTarget::PrimaryKey, position)); } let indices: Vec<_> = resolver.with_schema(database_id, |s| { s.get_indices(table_name).cloned().collect() }); for index in &indices { let position = upsert_actions .iter() .position(|(target, ..)| matches!(target, ResolvedUpsertTarget::Index(x) if Arc::ptr_eq(x, index))); constraints_to_check.push((ResolvedUpsertTarget::Index(index.clone()), position)); } constraints_to_check.sort_by(|(_, p1), (_, p2)| match (p1, p2) { (Some(p1), Some(p2)) => p1.cmp(p2), (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => std::cmp::Ordering::Equal, }); // Defer INTEGER PRIMARY KEY REPLACE to run after all other constraint checks. // When IPK has REPLACE and other // constraints exist with potentially different modes, the IPK check // runs last to avoid premature row deletion before FAIL/IGNORE fires. let defer_ipk_replace_after_other_checks = !has_statement_conflict && rowid_alias_conflict_clause == Some(ResolveType::Replace) && constraints_to_check.len() > 1 && !upsert_actions .iter() .any(|(t, ..)| matches!(t, ResolvedUpsertTarget::PrimaryKey)); if defer_ipk_replace_after_other_checks { if let Some(pos) = constraints_to_check .iter() .position(|(c, _)| matches!(c, ResolvedUpsertTarget::PrimaryKey)) { let pk = constraints_to_check.remove(pos); constraints_to_check.push(pk); } } // Post-condition: when no statement-level override exists, all REPLACE // constraints (by DDL mode) must form a contiguous suffix. When a statement // override exists, all constraints get the same effective mode, so the DDL // ordering is irrelevant. turso_debug_assert!( has_statement_conflict || { let mut saw_replace = false; constraints_to_check.iter().all(|(c, _)| { let mode = match c { ResolvedUpsertTarget::PrimaryKey => { rowid_alias_conflict_clause.unwrap_or(ResolveType::Abort) } ResolvedUpsertTarget::Index(idx) => { idx.on_conflict.unwrap_or(ResolveType::Abort) } ResolvedUpsertTarget::CatchAll => return true, }; if mode == ResolveType::Replace { saw_replace = true; true } else { !saw_replace } }) }, "constraints must have all REPLACE entries at the end" ); let upsert_catch_all_position = if let Some((ResolvedUpsertTarget::CatchAll, ..)) = upsert_actions.last() { Some(upsert_actions.len() - 1) } else { None }; ConstraintsToCheck { constraints_to_check, upsert_catch_all_position, } } fn emit_update_sqlite_sequence( program: &mut ProgramBuilder, resolver: &Resolver, database_id: usize, seq_cursor_id: usize, r_seq_rowid: usize, table_name_reg: usize, new_key_reg: usize, ) -> Result<()> { let record_reg = program.alloc_register(); let record_start_reg = program.alloc_registers(2); program.emit_insn(Insn::Copy { src_reg: table_name_reg, dst_reg: record_start_reg, extra_amount: 0, }); program.emit_insn(Insn::Copy { src_reg: new_key_reg, dst_reg: record_start_reg + 1, extra_amount: 0, }); let seq_table = get_valid_sqlite_sequence_table(resolver, database_id)?; let affinity_str = seq_table .columns .iter() .map(|col| col.affinity().aff_mask()) .collect::(); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(record_start_reg), count: to_u16(2), dest_reg: to_u16(record_reg), index_name: None, affinity_str: Some(affinity_str), }); let update_existing_label = program.allocate_label(); let end_update_label = program.allocate_label(); program.emit_insn(Insn::NotNull { reg: r_seq_rowid, target_pc: update_existing_label, }); program.emit_insn(Insn::NewRowid { cursor: seq_cursor_id, rowid_reg: r_seq_rowid, prev_largest_reg: 0, }); program.emit_insn(Insn::Insert { cursor: seq_cursor_id, key_reg: r_seq_rowid, record_reg, flag: InsertFlags::new(), table_name: SQLITE_SEQUENCE_TABLE_NAME.to_string(), }); program.emit_insn(Insn::Goto { target_pc: end_update_label, }); program.preassign_label_to_next_insn(update_existing_label); program.emit_insn(Insn::Insert { cursor: seq_cursor_id, key_reg: r_seq_rowid, record_reg, flag: InsertFlags(turso_parser::ast::ResolveType::Replace.bit_value() as u8), table_name: SQLITE_SEQUENCE_TABLE_NAME.to_string(), }); program.preassign_label_to_next_insn(end_update_label); Ok(()) } fn emit_replace_delete_conflicting_row( program: &mut ProgramBuilder, resolver: &mut Resolver, connection: &Arc, ctx: &mut InsertEmitCtx, table_references: &mut TableReferences, ) -> Result<()> { program.emit_insn(Insn::SeekRowid { cursor_id: ctx.cursor_id, src_reg: ctx.conflict_rowid_reg, target_pc: ctx.halt_label, }); // Phase 1: Before Delete - build parent key registers and handle NoAction/Restrict. // CASCADE/SetNull/SetDefault actions are prepared but deferred until after Delete. let prepared_fk_actions = if connection.foreign_keys_enabled() { let prepared = ForeignKeyActions::prepare_fk_delete_actions( program, resolver, ctx.table.name.as_str(), ctx.cursor_id, ctx.conflict_rowid_reg, ctx.database_id, )?; if resolver.schema().has_child_fks(ctx.table.name.as_str()) { emit_fk_child_decrement_on_delete( program, ctx.table.as_ref(), ctx.table.name.as_str(), ctx.cursor_id, ctx.conflict_rowid_reg, ctx.database_id, resolver, )?; } prepared } else { ForeignKeyActions::default() }; let table = &ctx.table; let table_name = table.name.as_str(); let main_cursor_id = ctx.cursor_id; for (name, _, index_cursor_id) in ctx.idx_cursors.iter() { let index = resolver .schema() .get_index(table_name, name) .expect("index to exist"); let skip_delete_label = if index.where_clause.is_some() { let where_copy = index .bind_where_expr(Some(table_references), resolver) .expect("where clause to exist"); let skip_label = program.allocate_label(); let reg = program.alloc_register(); translate_expr_no_constant_opt( program, Some(table_references), &where_copy, reg, resolver, NoConstantOptReason::RegisterReuse, )?; program.emit_insn(Insn::IfNot { reg, jump_if_null: true, target_pc: skip_label, }); Some(skip_label) } else { None }; let num_regs = index.columns.len() + 1; let start_reg = program.alloc_registers(num_regs); for (reg_offset, column_index) in index.columns.iter().enumerate() { if let Some(expr) = &column_index.expr { let mut expr = expr.as_ref().clone(); bind_and_rewrite_expr( &mut expr, Some(table_references), None, resolver, BindingBehavior::ResultColumnsNotAllowed, )?; translate_expr_no_constant_opt( program, Some(table_references), &expr, start_reg + reg_offset, resolver, NoConstantOptReason::RegisterReuse, )?; } else { program.emit_column_or_rowid( main_cursor_id, column_index.pos_in_table, start_reg + reg_offset, ); } } program.emit_insn(Insn::Copy { src_reg: ctx.conflict_rowid_reg, dst_reg: start_reg + num_regs - 1, extra_amount: 0, }); program.emit_insn(Insn::IdxDelete { start_reg, num_regs, cursor_id: *index_cursor_id, raise_error_if_no_matching_entry: index.where_clause.is_none(), }); if let Some(label) = skip_delete_label { program.resolve_label(label, program.offset()); } } // CDC BEFORE, using rowid_reg if let Some(cdc_cursor_id) = ctx.cdc_table.as_ref().map(|(id, _tbl)| *id) { let cdc_has_before = program.capture_data_changes_info().has_before(); let before_record_reg = if cdc_has_before { Some(emit_cdc_full_record( program, &table.columns, main_cursor_id, ctx.conflict_rowid_reg, table.is_strict, )) } else { None }; emit_cdc_insns( program, resolver, OperationMode::DELETE, cdc_cursor_id, ctx.conflict_rowid_reg, before_record_reg, None, None, table_name, )?; } program.emit_insn(Insn::Delete { cursor_id: main_cursor_id, table_name: table_name.to_string(), is_part_of_update: true, }); // Phase 2: After Delete - fire CASCADE/SetNull/SetDefault FK actions. prepared_fk_actions.fire_prepared_fk_delete_actions( program, resolver, connection, ctx.database_id, )?; Ok(()) } /// Child-side FK checks for INSERT of a single row: /// For each outgoing FK on `child_tbl`, if the NEW tuple's FK columns are all non-NULL, /// verify that the referenced parent key exists. pub fn emit_fk_child_insert_checks( program: &mut ProgramBuilder, child_tbl: &BTreeTable, new_start_reg: usize, new_rowid_reg: usize, resolver: &Resolver, database_id: usize, ) -> crate::Result<()> { for fk_ref in resolver.with_schema(database_id, |s| s.resolved_fks_for_child(&child_tbl.name))? { let is_self_ref = fk_ref.fk.parent_table.eq_ignore_ascii_case(&child_tbl.name); // Short-circuit if any NEW component is NULL let fk_ok = program.allocate_label(); for cname in &fk_ref.child_cols { let (i, col) = child_tbl.get_column(cname).unwrap(); let src = if col.is_rowid_alias() { new_rowid_reg } else { new_start_reg + i }; program.emit_insn(Insn::IsNull { reg: src, target_pc: fk_ok, }); } let parent_tbl = resolver .with_schema(database_id, |s| s.get_btree_table(&fk_ref.fk.parent_table)) .expect("parent btree"); if fk_ref.parent_uses_rowid { let pcur = open_read_table(program, &parent_tbl, database_id); // first child col carries rowid let (i_child, col_child) = child_tbl.get_column(&fk_ref.child_cols[0]).unwrap(); let val_reg = if col_child.is_rowid_alias() { new_rowid_reg } else { new_start_reg + i_child }; // Normalize rowid to integer for both the probe and the same-row fast path. let tmp = program.alloc_register(); program.emit_insn(Insn::Copy { src_reg: val_reg, dst_reg: tmp, extra_amount: 0, }); program.emit_insn(Insn::MustBeInt { reg: tmp }); // If this is a self-reference *and* the child FK equals NEW rowid, // the constraint will be satisfied once this row is inserted if is_self_ref { program.emit_insn(Insn::Eq { lhs: tmp, rhs: new_rowid_reg, target_pc: fk_ok, flags: CmpInsFlags::default(), collation: None, }); } let violation = program.allocate_label(); program.emit_insn(Insn::NotExists { cursor: pcur, rowid_reg: tmp, target_pc: violation, }); program.emit_insn(Insn::Close { cursor_id: pcur }); program.emit_insn(Insn::Goto { target_pc: fk_ok }); // Missing parent: immediate → Halt before Insert; deferred → counter program.preassign_label_to_next_insn(violation); program.emit_insn(Insn::Close { cursor_id: pcur }); if fk_ref.fk.deferred { emit_fk_violation(program, &fk_ref.fk)?; } else { emit_fk_restrict_halt(program)?; } program.preassign_label_to_next_insn(fk_ok); } else { let idx = fk_ref .parent_unique_index .as_ref() .expect("parent unique index required"); let icur = open_read_index(program, idx, database_id); let ncols = fk_ref.child_cols.len(); // Build NEW child probe from child NEW values, apply parent-index affinities. let probe = { let start = program.alloc_registers(ncols); for (k, cname) in fk_ref.child_cols.iter().enumerate() { let (i, col) = child_tbl.get_column(cname).unwrap(); program.emit_insn(Insn::Copy { src_reg: if col.is_rowid_alias() { new_rowid_reg } else { new_start_reg + i }, dst_reg: start + k, extra_amount: 0, }); } if let Some(cnt) = NonZeroUsize::new(ncols) { program.emit_insn(Insn::Affinity { start_reg: start, count: cnt, affinities: build_index_affinity_string(idx, &parent_tbl), }); } start }; if is_self_ref { // Determine the parent column order to compare against: let parent_cols: Vec<&str> = idx.columns.iter().map(|ic| ic.name.as_str()).collect(); // Build new parent-key image from this same row’s new values, in the index order. let parent_new = program.alloc_registers(ncols); for (i, pname) in parent_cols.iter().enumerate() { let (pos, col) = child_tbl.get_column(pname).unwrap(); program.emit_insn(Insn::Copy { src_reg: if col.is_rowid_alias() { new_rowid_reg } else { new_start_reg + pos }, dst_reg: parent_new + i, extra_amount: 0, }); } if let Some(cnt) = NonZeroUsize::new(ncols) { program.emit_insn(Insn::Affinity { start_reg: parent_new, count: cnt, affinities: build_index_affinity_string(idx, &parent_tbl), }); } // Compare child probe to NEW parent image column-by-column. let mismatch = program.allocate_label(); for i in 0..ncols { let cont = program.allocate_label(); program.emit_insn(Insn::Eq { lhs: probe + i, rhs: parent_new + i, target_pc: cont, flags: CmpInsFlags::default().jump_if_null(), collation: Some(super::collate::CollationSeq::Binary), }); program.emit_insn(Insn::Goto { target_pc: mismatch, }); program.preassign_label_to_next_insn(cont); } // All equal: same-row OK program.emit_insn(Insn::Goto { target_pc: fk_ok }); program.preassign_label_to_next_insn(mismatch); } index_probe( program, icur, probe, ncols, // on_found: parent exists, FK satisfied |_p| Ok(()), // on_not_found: immediate → Halt; deferred → counter |p| { if fk_ref.fk.deferred { emit_fk_violation(p, &fk_ref.fk)?; } else { emit_fk_restrict_halt(p)?; } Ok(()) }, )?; program.emit_insn(Insn::Goto { target_pc: fk_ok }); program.preassign_label_to_next_insn(fk_ok); } } Ok(()) } /// Build NEW parent key image in FK parent-column order into a contiguous register block. /// Handles 3 shapes: /// - parent_uses_rowid: single "rowid" component /// - explicit fk.parent_columns /// - fk.parent_columns empty => use parent's declared PK columns (order-preserving) fn build_parent_key_image_for_insert( program: &mut ProgramBuilder, parent_table: &BTreeTable, pref: &ResolvedFkRef, insertion: &Insertion, ) -> crate::Result<(usize, usize)> { // Decide column list let parent_cols: Vec = if pref.parent_uses_rowid { vec!["rowid".to_string()] } else if !pref.fk.parent_columns.is_empty() { pref.fk.parent_columns.clone() } else { // fall back to the declared PK of the parent table, in schema order parent_table .primary_key_columns .iter() .map(|(n, _)| n.clone()) .collect() }; let ncols = parent_cols.len(); let start = program.alloc_registers(ncols); // Copy from the would-be parent insertion for (i, pname) in parent_cols.iter().enumerate() { let src = if pname.eq_ignore_ascii_case("rowid") { insertion.key_register() } else { // For rowid-alias parents, get_col_mapping_by_name will return the key mapping, // not the NULL placeholder in col_mappings. insertion .get_col_mapping_by_name(pname) .ok_or_else(|| { crate::LimboError::PlanningError(format!( "Column '{}' not present in INSERT image for parent {}", pname, parent_table.name )) })? .register }; program.emit_insn(Insn::Copy { src_reg: src, dst_reg: start + i, extra_amount: 0, }); } // Apply affinities of the parent columns (or integer for rowid) let aff: String = if pref.parent_uses_rowid { "i".to_string() } else { parent_cols .iter() .map(|name| { let (_, col) = parent_table.get_column(name).ok_or_else(|| { crate::LimboError::InternalError(format!("parent col {name} missing")) })?; Ok::<_, crate::LimboError>( col.affinity_with_strict(parent_table.is_strict).aff_mask(), ) }) .collect::>()? }; if let Some(count) = NonZeroUsize::new(ncols) { program.emit_insn(Insn::Affinity { start_reg: start, count, affinities: aff, }); } Ok((start, ncols)) } /// Parent-side: when inserting into the parent, decrement the counter /// if any child rows reference the NEW parent key. /// We *always* do this for deferred FKs, and we *also* do it for /// self-referential FKs (even if immediate) because the insert can /// “repair” a prior child-insert count recorded earlier in the same statement. pub fn emit_parent_side_fk_decrement_on_insert( program: &mut ProgramBuilder, parent_table: &BTreeTable, insertion: &Insertion, force_immediate: bool, resolver: &Resolver, database_id: usize, ) -> crate::Result<()> { for pref in resolver.with_schema(database_id, |s| { s.resolved_fks_referencing(&parent_table.name) })? { let is_self_ref = pref .child_table .name .eq_ignore_ascii_case(&parent_table.name); // Skip only when it cannot repair anything: non-deferred and not self-referencing if !force_immediate && !pref.fk.deferred && !is_self_ref { continue; } let (new_pk_start, n_cols) = build_parent_key_image_for_insert(program, parent_table, &pref, insertion)?; let child_tbl = &pref.child_table; let child_cols = &pref.fk.child_columns; let indices: Vec<_> = resolver.with_schema(database_id, |s| { s.get_indices(&child_tbl.name).cloned().collect() }); let idx = indices.iter().find(|ix| { ix.columns.len() == child_cols.len() && ix .columns .iter() .zip(child_cols.iter()) .all(|(ic, cc)| ic.name.eq_ignore_ascii_case(cc)) }); if let Some(ix) = idx { let icur = open_read_index(program, ix, database_id); // Copy key into probe regs and apply child-index affinities let probe_start = program.alloc_registers(n_cols); for i in 0..n_cols { program.emit_insn(Insn::Copy { src_reg: new_pk_start + i, dst_reg: probe_start + i, extra_amount: 0, }); } if let Some(count) = NonZeroUsize::new(n_cols) { program.emit_insn(Insn::Affinity { start_reg: probe_start, count, affinities: build_index_affinity_string(ix, child_tbl), }); } let found = program.allocate_label(); program.emit_insn(Insn::Found { cursor_id: icur, target_pc: found, record_reg: probe_start, num_regs: n_cols, }); // Not found, nothing to decrement program.emit_insn(Insn::Close { cursor_id: icur }); let skip = program.allocate_label(); program.emit_insn(Insn::Goto { target_pc: skip }); // Found: guarded counter decrement program.resolve_label(found, program.offset()); program.emit_insn(Insn::Close { cursor_id: icur }); emit_guarded_fk_decrement(program, skip, pref.fk.deferred); program.resolve_label(skip, program.offset()); } else { // fallback scan :( let ccur = open_read_table(program, child_tbl, database_id); let done = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: ccur, pc_if_empty: done, }); let loop_top = program.allocate_label(); let next_row = program.allocate_label(); program.resolve_label(loop_top, program.offset()); for (i, child_name) in child_cols.iter().enumerate() { let (pos, _) = child_tbl.get_column(child_name).ok_or_else(|| { crate::LimboError::InternalError(format!("child col {child_name} missing")) })?; let tmp = program.alloc_register(); program.emit_insn(Insn::Column { cursor_id: ccur, column: pos, dest: tmp, default: None, }); program.emit_insn(Insn::IsNull { reg: tmp, target_pc: next_row, }); let cont = program.allocate_label(); program.emit_insn(Insn::Eq { lhs: tmp, rhs: new_pk_start + i, target_pc: cont, flags: CmpInsFlags::default().jump_if_null(), collation: Some(super::collate::CollationSeq::Binary), }); program.emit_insn(Insn::Goto { target_pc: next_row, }); program.resolve_label(cont, program.offset()); } // Matched one child row: guarded decrement of counter emit_guarded_fk_decrement(program, next_row, pref.fk.deferred); program.resolve_label(next_row, program.offset()); program.emit_insn(Insn::Next { cursor_id: ccur, pc_if_next: loop_top, }); program.resolve_label(done, program.offset()); program.emit_insn(Insn::Close { cursor_id: ccur }); } } Ok(()) } /// Emit encode expressions for columns with custom types. /// For each column that has a custom type with an encode expression, /// evaluates the expression with `value` bound to the column register. fn emit_custom_type_encode( program: &mut ProgramBuilder, resolver: &Resolver, insertion: &Insertion, table_name: &str, ) -> Result<()> { let columns: Vec<_> = insertion .col_mappings .iter() .map(|m| m.column.clone()) .collect(); crate::translate::expr::emit_custom_type_encode_columns( program, resolver, &columns, insertion.first_col_register(), None, // INSERT: encode all columns table_name, ) } ================================================ FILE: core/translate/integrity_check.rs ================================================ use crate::{ schema::{Index, Schema, Table}, translate::{ emitter::Resolver, expr::{ bind_and_rewrite_expr, translate_condition_expr, translate_expr_no_constant_opt, BindingBehavior, ConditionMetadata, NoConstantOptReason, }, plan::{ColumnUsedMask, IterationDirection, JoinedTable, Operation, Scan, TableReferences}, }, vdbe::{ builder::{CursorKey, CursorType, ProgramBuilder}, insn::{CmpInsFlags, Insn}, }, }; use turso_parser::ast; /// Maximum number of errors to report with integrity check. If we exceed this number we will /// short circuit the procedure and return early to not waste time. SQLite uses 100 as default. pub const MAX_INTEGRITY_CHECK_ERRORS: usize = 100; enum BoundIndexColumn { Column(usize), Expr(Box), } struct BoundIntegrityIndex { index: crate::sync::Arc, cursor_id: usize, expected_count_reg: usize, where_expr: Option, columns: Vec, unique_nullable: Vec, } /// Translate PRAGMA integrity_check. pub fn translate_integrity_check( schema: &Schema, program: &mut ProgramBuilder, resolver: &Resolver, database_id: usize, max_errors: usize, ) -> crate::Result<()> { translate_integrity_check_impl(schema, program, resolver, database_id, max_errors, false) } /// Translate PRAGMA quick_check. pub fn translate_quick_check( schema: &Schema, program: &mut ProgramBuilder, resolver: &Resolver, database_id: usize, max_errors: usize, ) -> crate::Result<()> { translate_integrity_check_impl(schema, program, resolver, database_id, max_errors, true) } fn emit_integrity_result_row( program: &mut ProgramBuilder, remaining_errors_reg: usize, message_reg: usize, had_error_reg: usize, ) { program.emit_int(1, had_error_reg); program.emit_result_row(message_reg, 1); let continue_label = program.allocate_label(); program.emit_insn(Insn::IfPos { reg: remaining_errors_reg, target_pc: continue_label, decrement_by: 1, }); program.emit_insn(Insn::Halt { err_code: 0, on_error: None, description_reg: None, description: String::new(), }); program.preassign_label_to_next_insn(continue_label); } fn emit_row_missing_from_index_error( program: &mut ProgramBuilder, row_number_reg: usize, scratch_reg: usize, message_reg: usize, index_name: &str, remaining_errors_reg: usize, had_error_reg: usize, ) { program.emit_string8("row ".to_string(), message_reg); program.emit_insn(Insn::Concat { lhs: message_reg, rhs: row_number_reg, dest: message_reg, }); program.emit_string8(" missing from index ".to_string(), scratch_reg); program.emit_insn(Insn::Concat { lhs: message_reg, rhs: scratch_reg, dest: message_reg, }); program.emit_string8(index_name.to_string(), scratch_reg); program.emit_insn(Insn::Concat { lhs: message_reg, rhs: scratch_reg, dest: message_reg, }); emit_integrity_result_row(program, remaining_errors_reg, message_reg, had_error_reg); } fn bind_expr_for_table( expr: &ast::Expr, table_references: &mut TableReferences, resolver: &Resolver, ) -> crate::Result { let mut out = expr.clone(); bind_and_rewrite_expr( &mut out, Some(table_references), None, resolver, BindingBehavior::ResultColumnsNotAllowed, )?; Ok(out) } fn translate_integrity_check_impl( schema: &Schema, program: &mut ProgramBuilder, resolver: &Resolver, database_id: usize, max_errors: usize, quick: bool, ) -> crate::Result<()> { // 1) Run low-level btree/freelist/overflow verification first. This mirrors // SQLite's OP_IntegrityCk front-pass and can already emit corruption errors // before any row-by-row semantic checks run. let mut root_pages = Vec::with_capacity(schema.tables.len() + schema.indexes.len()); for table in schema.tables.values() { if let Table::BTree(btree_table) = table.as_ref() { if btree_table.root_page < 0 { continue; } root_pages.push(btree_table.root_page); if let Some(indexes) = schema.indexes.get(btree_table.name.as_str()) { for index in indexes { if index.root_page > 0 { root_pages.push(index.root_page); } } } } } for &dropped_root in &schema.dropped_root_pages { root_pages.push(dropped_root); } let remaining_errors_reg = program.alloc_register(); program.emit_int((max_errors.saturating_sub(1)) as i64, remaining_errors_reg); let had_error_reg = program.alloc_register(); program.emit_int(0, had_error_reg); let message_reg = program.alloc_register(); let scratch_reg = program.alloc_register(); program.emit_insn(Insn::IntegrityCk { db: database_id, max_errors, roots: root_pages, message_register: message_reg, }); let no_structural_error_label = program.allocate_label(); program.emit_insn(Insn::IsNull { reg: message_reg, target_pc: no_structural_error_label, }); program.emit_string8("*** in database main ***\n".to_string(), scratch_reg); program.emit_insn(Insn::Concat { lhs: scratch_reg, rhs: message_reg, dest: message_reg, }); emit_integrity_result_row(program, remaining_errors_reg, message_reg, had_error_reg); program.preassign_label_to_next_insn(no_structural_error_label); // 2) For each ordinary btree table, scan every row and validate: // - NOT NULL constraints // - CHECK constraints // - index membership/uniqueness (integrity_check only) // - index cardinality cross-checks for table in schema.tables.values() { let Table::BTree(btree_table) = table.as_ref() else { continue; }; if btree_table.root_page <= 0 { continue; } let table_ref_id = program.table_reference_counter.next(); let table_cursor_id = program.alloc_cursor_id_keyed( CursorKey::table(table_ref_id), CursorType::BTreeTable(btree_table.clone()), ); program.emit_insn(Insn::OpenRead { cursor_id: table_cursor_id, root_page: btree_table.root_page, db: database_id, }); let mut table_references = TableReferences::new( vec![JoinedTable { op: Operation::Scan(Scan::BTreeTable { iter_dir: IterationDirection::Forwards, index: None, }), table: Table::BTree(btree_table.clone()), identifier: btree_table.name.clone(), internal_id: table_ref_id, join_info: None, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id, indexed: None, }], vec![], ); let mut bound_indexes = Vec::new(); if let Some(indexes) = schema.indexes.get(btree_table.name.as_str()) { for index in indexes { if index.root_page <= 0 { continue; } let cursor_id = program.alloc_cursor_index(None, index)?; program.emit_insn(Insn::OpenRead { cursor_id, root_page: index.root_page, db: database_id, }); let expected_count_reg = program.alloc_register(); program.emit_int(0, expected_count_reg); let mut where_expr = None; if let Some(pred) = index.where_clause.as_deref() { where_expr = Some(bind_expr_for_table(pred, &mut table_references, resolver)?); } let mut columns = Vec::with_capacity(index.columns.len()); let mut unique_nullable = Vec::with_capacity(index.columns.len()); for col in &index.columns { if let Some(expr) = col.expr.as_deref() { columns.push(BoundIndexColumn::Expr(Box::new(bind_expr_for_table( expr, &mut table_references, resolver, )?))); unique_nullable.push(true); } else { columns.push(BoundIndexColumn::Column(col.pos_in_table)); unique_nullable.push(!btree_table.columns[col.pos_in_table].notnull()); } } bound_indexes.push(BoundIntegrityIndex { index: index.clone(), cursor_id, expected_count_reg, where_expr, columns, unique_nullable, }); } } let mut bound_checks = Vec::with_capacity(btree_table.check_constraints.len()); for check in &btree_table.check_constraints { bound_checks.push(bind_expr_for_table( &check.expr, &mut table_references, resolver, )?); } let not_null_columns: Vec<(usize, String)> = btree_table .columns .iter() .enumerate() .filter_map(|(idx, col)| { if col.notnull() && !col.is_rowid_alias() { Some(( idx, col.name.clone().unwrap_or_else(|| format!("column{idx}")), )) } else { None } }) .collect(); let row_number_reg = program.alloc_register(); program.emit_int(0, row_number_reg); let table_empty_label = program.allocate_label(); let loop_start_label = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: table_cursor_id, pc_if_empty: table_empty_label, }); program.preassign_label_to_next_insn(loop_start_label); program.emit_insn(Insn::AddImm { register: row_number_reg, value: 1, }); for (col_idx, col_name) in ¬_null_columns { let col_value_reg = program.alloc_register(); program.emit_column_or_rowid(table_cursor_id, *col_idx, col_value_reg); let not_null_ok = program.allocate_label(); program.emit_insn(Insn::NotNull { reg: col_value_reg, target_pc: not_null_ok, }); program.emit_string8( format!("NULL value in {}.{}", btree_table.name, col_name), message_reg, ); emit_integrity_result_row(program, remaining_errors_reg, message_reg, had_error_reg); program.preassign_label_to_next_insn(not_null_ok); } for check_expr in &bound_checks { let check_ok = program.allocate_label(); let check_fail = program.allocate_label(); translate_condition_expr( program, &table_references, check_expr, ConditionMetadata { jump_if_condition_is_true: true, jump_target_when_true: check_ok, jump_target_when_false: check_fail, jump_target_when_null: check_ok, }, resolver, )?; program.preassign_label_to_next_insn(check_fail); program.emit_string8( format!("CHECK constraint failed in {}", btree_table.name), message_reg, ); emit_integrity_result_row(program, remaining_errors_reg, message_reg, had_error_reg); program.preassign_label_to_next_insn(check_ok); } for bound_index in &bound_indexes { let skip_current_index = program.allocate_label(); if let Some(where_expr) = bound_index.where_expr.as_ref() { let where_failed = skip_current_index; let where_true_fallthrough = program.allocate_label(); translate_condition_expr( program, &table_references, where_expr, ConditionMetadata { // For partial indexes, rows that evaluate predicate to FALSE/NULL // are not part of the index and must be skipped. jump_if_condition_is_true: false, jump_target_when_true: where_true_fallthrough, jump_target_when_false: where_failed, jump_target_when_null: where_failed, }, resolver, )?; program.preassign_label_to_next_insn(where_true_fallthrough); } // Count rows that are expected to appear in this index. For partial // indexes this is only rows where the predicate is true. program.emit_insn(Insn::AddImm { register: bound_index.expected_count_reg, value: 1, }); let key_start_reg = program.alloc_registers(bound_index.columns.len() + 1); for (i, col) in bound_index.columns.iter().enumerate() { let target = key_start_reg + i; match col { BoundIndexColumn::Column(pos) => { program.emit_column_or_rowid(table_cursor_id, *pos, target); } BoundIndexColumn::Expr(expr) => { translate_expr_no_constant_opt( program, Some(&table_references), expr, target, resolver, NoConstantOptReason::RegisterReuse, )?; } } } let rowid_reg = key_start_reg + bound_index.columns.len(); program.emit_insn(Insn::RowId { cursor_id: table_cursor_id, dest: rowid_reg, }); if !quick { let found_label = program.allocate_label(); // Verify the table row has a matching index entry (key columns + rowid). program.emit_insn(Insn::Found { cursor_id: bound_index.cursor_id, target_pc: found_label, record_reg: key_start_reg, num_regs: bound_index.columns.len() + 1, }); emit_row_missing_from_index_error( program, row_number_reg, scratch_reg, message_reg, &bound_index.index.name, remaining_errors_reg, had_error_reg, ); program.preassign_label_to_next_insn(found_label); if bound_index.index.unique { // This intentionally runs even after a "missing from index" // report above. SQLite does the same: a single corrupt row // can violate multiple invariants and each should be // independently reportable. // // Uniqueness rule matches SQLite: // unique key is valid if any key column is NULL, OR // the next index entry is strictly greater on key columns. let unique_ok = program.allocate_label(); for (i, is_nullable) in bound_index.unique_nullable.iter().enumerate() { if *is_nullable { program.emit_insn(Insn::IsNull { reg: key_start_reg + i, target_pc: unique_ok, }); } } let next_exists = program.allocate_label(); program.emit_insn(Insn::Next { cursor_id: bound_index.cursor_id, pc_if_next: next_exists, }); program.emit_insn(Insn::Goto { target_pc: unique_ok, }); program.preassign_label_to_next_insn(next_exists); program.emit_insn(Insn::IdxGT { cursor_id: bound_index.cursor_id, start_reg: key_start_reg, num_regs: bound_index.columns.len(), target_pc: unique_ok, }); program.emit_string8( format!("non-unique entry in index {}", bound_index.index.name), message_reg, ); emit_integrity_result_row( program, remaining_errors_reg, message_reg, had_error_reg, ); program.preassign_label_to_next_insn(unique_ok); } } program.preassign_label_to_next_insn(skip_current_index); } program.emit_insn(Insn::Next { cursor_id: table_cursor_id, pc_if_next: loop_start_label, }); program.preassign_label_to_next_insn(table_empty_label); for bound_index in &bound_indexes { if bound_index.where_expr.is_none() { let actual_count_reg = program.alloc_register(); program.emit_insn(Insn::Count { cursor_id: bound_index.cursor_id, target_reg: actual_count_reg, exact: true, }); let counts_match = program.allocate_label(); program.emit_insn(Insn::Eq { lhs: actual_count_reg, rhs: bound_index.expected_count_reg, target_pc: counts_match, flags: CmpInsFlags::default(), collation: None, }); program.emit_string8( format!("wrong # of entries in index {}", bound_index.index.name), message_reg, ); emit_integrity_result_row( program, remaining_errors_reg, message_reg, had_error_reg, ); program.preassign_label_to_next_insn(counts_match); } program.emit_insn(Insn::Close { cursor_id: bound_index.cursor_id, }); } program.emit_insn(Insn::Close { cursor_id: table_cursor_id, }); } let has_errors_label = program.allocate_label(); program.emit_insn(Insn::If { reg: had_error_reg, target_pc: has_errors_label, jump_if_null: false, }); program.emit_string8("ok".to_string(), message_reg); program.emit_result_row(message_reg, 1); program.preassign_label_to_next_insn(has_errors_label); let column_name = if quick { "quick_check" } else { "integrity_check" }; program.add_pragma_result_column(column_name.into()); Ok(()) } ================================================ FILE: core/translate/logical.rs ================================================ //! Logical plan representation for SQL queries //! //! This module provides a platform-independent intermediate representation //! for SQL queries. The logical plan is a DAG (Directed Acyclic Graph) that //! supports CTEs and can be used for query optimization before being compiled //! to an execution plan (e.g., DBSP circuits). //! //! The main entry point is `LogicalPlanBuilder` which constructs logical plans //! from SQL AST nodes. use crate::function::AggFunc; use crate::numeric::Numeric; use crate::schema::{Schema, Type}; use crate::sync::Arc; use crate::turso_assert_ne; use crate::types::Value; use crate::{LimboError, Result}; use rustc_hash::FxHashMap as HashMap; use std::fmt::{self, Display, Formatter}; use turso_macros::match_ignore_ascii_case; use turso_parser::ast; /// Result type for preprocessing aggregate expressions type PreprocessAggregateResult = ( bool, // needs_pre_projection Vec, // pre_projection_exprs Vec, // pre_projection_schema Vec, // modified_aggr_exprs Vec, // modified_group_exprs ); /// Result type for parsing join conditions type JoinConditionsResult = (Vec<(LogicalExpr, LogicalExpr)>, Option); /// Information about a column in a logical schema #[derive(Debug, Clone, PartialEq)] pub struct ColumnInfo { pub name: String, pub ty: Type, pub database: Option, pub table: Option, pub table_alias: Option, } /// Schema information for logical plan nodes #[derive(Debug, Clone, PartialEq)] pub struct LogicalSchema { pub columns: Vec, } /// A reference to a schema that can be shared between nodes pub type SchemaRef = Arc; impl LogicalSchema { pub fn new(columns: Vec) -> Self { Self { columns } } pub fn empty() -> Self { Self { columns: Vec::new(), } } pub fn column_count(&self) -> usize { self.columns.len() } pub fn find_column(&self, name: &str, table: Option<&str>) -> Option<(usize, &ColumnInfo)> { if let Some(table_ref) = table { // Check if it's a database.table format if table_ref.contains('.') { let parts: Vec<&str> = table_ref.splitn(2, '.').collect(); if parts.len() == 2 { let db = parts[0]; let tbl = parts[1]; return self .columns .iter() .position(|c| { c.name == name && c.database.as_deref() == Some(db) && c.table.as_deref() == Some(tbl) }) .map(|idx| (idx, &self.columns[idx])); } } // Try to match against table alias first, then table name self.columns .iter() .position(|c| { c.name == name && (c.table_alias.as_deref() == Some(table_ref) || c.table.as_deref() == Some(table_ref)) }) .map(|idx| (idx, &self.columns[idx])) } else { // Unqualified lookup - just match by name self.columns .iter() .position(|c| c.name == name) .map(|idx| (idx, &self.columns[idx])) } } } /// Logical representation of a SQL query plan #[derive(Debug, Clone, PartialEq)] pub enum LogicalPlan { /// Projection - SELECT expressions Projection(Projection), /// Filter - WHERE/HAVING clause Filter(Filter), /// Aggregate - GROUP BY with aggregate functions Aggregate(Aggregate), /// Join - combining two relations Join(Join), /// Sort - ORDER BY clause Sort(Sort), /// Limit - LIMIT/OFFSET clause Limit(Limit), /// Table scan - reading from a base table TableScan(TableScan), /// Union - UNION/UNION ALL/INTERSECT/EXCEPT Union(Union), /// Distinct - remove duplicates Distinct(Distinct), /// Empty relation - no rows EmptyRelation(EmptyRelation), /// Values - literal rows (VALUES clause) Values(Values), /// CTE support - WITH clause WithCTE(WithCTE), /// Reference to a CTE CTERef(CTERef), } impl LogicalPlan { /// Get the schema of this plan node pub fn schema(&self) -> &SchemaRef { match self { LogicalPlan::Projection(p) => &p.schema, LogicalPlan::Filter(f) => f.input.schema(), LogicalPlan::Aggregate(a) => &a.schema, LogicalPlan::Join(j) => &j.schema, LogicalPlan::Sort(s) => s.input.schema(), LogicalPlan::Limit(l) => l.input.schema(), LogicalPlan::TableScan(t) => &t.schema, LogicalPlan::Union(u) => &u.schema, LogicalPlan::Distinct(d) => d.input.schema(), LogicalPlan::EmptyRelation(e) => &e.schema, LogicalPlan::Values(v) => &v.schema, LogicalPlan::WithCTE(w) => w.body.schema(), LogicalPlan::CTERef(c) => &c.schema, } } } /// Projection operator - SELECT expressions #[derive(Debug, Clone, PartialEq)] pub struct Projection { pub input: Arc, pub exprs: Vec, pub schema: SchemaRef, } /// Filter operator - WHERE/HAVING predicates #[derive(Debug, Clone, PartialEq)] pub struct Filter { pub input: Arc, pub predicate: LogicalExpr, } /// Aggregate operator - GROUP BY with aggregations #[derive(Debug, Clone, PartialEq)] pub struct Aggregate { pub input: Arc, pub group_expr: Vec, pub aggr_expr: Vec, pub schema: SchemaRef, } /// Types of joins #[derive(Debug, Clone, Copy, PartialEq)] pub enum JoinType { Inner, Left, Right, Full, Cross, } /// Join operator - combines two relations #[derive(Debug, Clone, PartialEq)] pub struct Join { pub left: Arc, pub right: Arc, pub join_type: JoinType, pub on: Vec<(LogicalExpr, LogicalExpr)>, // Equijoin conditions (left_expr, right_expr) pub filter: Option, // Additional filter conditions pub schema: SchemaRef, } /// Sort operator - ORDER BY #[derive(Debug, Clone, PartialEq)] pub struct Sort { pub input: Arc, pub exprs: Vec, } /// Sort expression with direction #[derive(Debug, Clone, PartialEq)] pub struct SortExpr { pub expr: LogicalExpr, pub asc: bool, pub nulls_first: bool, } /// Limit operator - LIMIT/OFFSET #[derive(Debug, Clone, PartialEq)] pub struct Limit { pub input: Arc, pub skip: Option, pub fetch: Option, } /// Table scan operator #[derive(Debug, Clone, PartialEq)] pub struct TableScan { pub table_name: String, pub alias: Option, pub schema: SchemaRef, pub projection: Option>, // Column indices to project } /// Union operator #[derive(Debug, Clone, PartialEq)] pub struct Union { pub inputs: Vec>, pub all: bool, // true for UNION ALL, false for UNION pub schema: SchemaRef, } /// Distinct operator #[derive(Debug, Clone, PartialEq)] pub struct Distinct { pub input: Arc, } /// Empty relation - produces no rows #[derive(Debug, Clone, PartialEq)] pub struct EmptyRelation { pub produce_one_row: bool, pub schema: SchemaRef, } /// Values operator - literal rows #[derive(Debug, Clone, PartialEq)] pub struct Values { pub rows: Vec>, pub schema: SchemaRef, } /// WITH clause - CTEs #[derive(Debug, Clone, PartialEq)] pub struct WithCTE { pub ctes: HashMap>, pub body: Arc, } /// Reference to a CTE #[derive(Debug, Clone, PartialEq)] pub struct CTERef { pub name: String, pub schema: SchemaRef, } /// Logical expression representation #[derive(Debug, Clone, PartialEq)] pub enum LogicalExpr { /// Column reference Column(Column), /// Literal value Literal(Value), /// Binary expression BinaryExpr { left: Box, op: BinaryOperator, right: Box, }, /// Unary expression UnaryExpr { op: UnaryOperator, expr: Box, }, /// Aggregate function AggregateFunction { fun: AggregateFunction, args: Vec, distinct: bool, }, /// Scalar function call ScalarFunction { fun: String, args: Vec }, /// CASE expression Case { expr: Option>, when_then: Vec<(LogicalExpr, LogicalExpr)>, else_expr: Option>, }, /// IN list InList { expr: Box, list: Vec, negated: bool, }, /// IN subquery InSubquery { expr: Box, subquery: Arc, negated: bool, }, /// EXISTS subquery Exists { subquery: Arc, negated: bool, }, /// Scalar subquery ScalarSubquery(Arc), /// Alias for an expression Alias { expr: Box, alias: String, }, /// IS NULL / IS NOT NULL IsNull { expr: Box, negated: bool, }, /// BETWEEN Between { expr: Box, low: Box, high: Box, negated: bool, }, /// LIKE pattern matching Like { expr: Box, pattern: Box, escape: Option, negated: bool, }, /// CAST expression Cast { expr: Box, type_name: Option, }, } /// Column reference #[derive(Debug, Clone, PartialEq)] pub struct Column { pub name: String, pub table: Option, } impl Column { pub fn new(name: impl Into) -> Self { Self { name: name.into(), table: None, } } pub fn with_table(name: impl Into, table: impl Into) -> Self { Self { name: name.into(), table: Some(table.into()), } } } impl Display for Column { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match &self.table { Some(t) => write!(f, "{}.{}", t, self.name), None => write!(f, "{}", self.name), } } } /// Strip alias wrapper from an expression, returning the underlying expression. /// This is useful when comparing expressions where one might be aliased and the other not, /// such as when matching SELECT expressions with GROUP BY expressions. pub fn strip_alias(expr: &LogicalExpr) -> &LogicalExpr { match expr { LogicalExpr::Alias { expr, .. } => expr, _ => expr, } } /// Type alias for binary operators pub type BinaryOperator = ast::Operator; /// Type alias for unary operators pub type UnaryOperator = ast::UnaryOperator; /// Type alias for aggregate functions pub type AggregateFunction = AggFunc; /// Compiler from AST to LogicalPlan pub struct LogicalPlanBuilder<'a> { schema: &'a Schema, ctes: HashMap>, } impl<'a> LogicalPlanBuilder<'a> { pub fn new(schema: &'a Schema) -> Self { Self { schema, ctes: HashMap::default(), } } /// Main entry point: compile a statement to a logical plan pub fn build_statement(&mut self, stmt: &ast::Stmt) -> Result { match stmt { ast::Stmt::Select(select) => self.build_select(select), _ => Err(LimboError::ParseError( "Only SELECT statements are currently supported in logical plans".to_string(), )), } } // Convert Name to String fn name_to_string(name: &ast::Name) -> String { name.as_str().to_string() } // Build a SELECT statement // Build a logical plan from a SELECT statement fn build_select(&mut self, select: &ast::Select) -> Result { // Handle WITH clause if present if let Some(with) = &select.with { return self.build_with_cte(with, select); } // Build the main query body let order_by = &select.order_by; let limit = &select.limit; self.build_select_body(&select.body, order_by, limit) } // Build WITH CTE fn build_with_cte(&mut self, with: &ast::With, select: &ast::Select) -> Result { let mut cte_plans = HashMap::default(); // Build each CTE for cte in &with.ctes { let cte_plan = self.build_select(&cte.select)?; let cte_name = Self::name_to_string(&cte.tbl_name); cte_plans.insert(cte_name.clone(), Arc::new(cte_plan)); self.ctes .insert(cte_name.clone(), cte_plans[&cte_name].clone()); } // Build the main body with CTEs available let order_by = &select.order_by; let limit = &select.limit; let body = self.build_select_body(&select.body, order_by, limit)?; // Clear CTEs from builder context for cte in &with.ctes { self.ctes.remove(&Self::name_to_string(&cte.tbl_name)); } Ok(LogicalPlan::WithCTE(WithCTE { ctes: cte_plans, body: Arc::new(body), })) } // Build SELECT body fn build_select_body( &mut self, body: &ast::SelectBody, order_by: &[ast::SortedColumn], limit: &Option, ) -> Result { let mut plan = self.build_one_select(&body.select)?; // Handle compound operators (UNION, INTERSECT, EXCEPT) if !body.compounds.is_empty() { for compound in &body.compounds { let right = self.build_one_select(&compound.select)?; plan = Self::build_compound(plan, right, &compound.operator)?; } } // Apply ORDER BY if !order_by.is_empty() { plan = self.build_sort(plan, order_by)?; } // Apply LIMIT if let Some(limit) = limit { plan = Self::build_limit(plan, limit)?; } Ok(plan) } // Build a single SELECT (without compounds) fn build_one_select(&mut self, select: &ast::OneSelect) -> Result { match select { ast::OneSelect::Select { distinctness, columns, from, where_clause, group_by, window_clause: _, } => { // Start with FROM clause let mut plan = if let Some(from) = from { self.build_from(from)? } else { // No FROM clause - single row LogicalPlan::EmptyRelation(EmptyRelation { produce_one_row: true, schema: Arc::new(LogicalSchema::empty()), }) }; // Apply WHERE if let Some(where_expr) = where_clause { let predicate = self.build_expr(where_expr, plan.schema())?; plan = LogicalPlan::Filter(Filter { input: Arc::new(plan), predicate, }); } // Apply GROUP BY and aggregations if let Some(group_by) = group_by { plan = self.build_aggregate(plan, group_by, columns)?; } else if Self::has_aggregates(columns) { // Aggregation without GROUP BY plan = self.build_aggregate_no_group(plan, columns)?; } else { // Regular projection plan = self.build_projection(plan, columns)?; } // Apply HAVING (part of GROUP BY) if let Some(ref group_by) = group_by { if let Some(ref having_expr) = group_by.having { let predicate = self.build_expr(having_expr, plan.schema())?; plan = LogicalPlan::Filter(Filter { input: Arc::new(plan), predicate, }); } } // Apply DISTINCT if distinctness.is_some() { plan = LogicalPlan::Distinct(Distinct { input: Arc::new(plan), }); } Ok(plan) } ast::OneSelect::Values(values) => self.build_values(values), } } // Build FROM clause fn build_from(&mut self, from: &ast::FromClause) -> Result { let mut plan = { self.build_select_table(&from.select)? }; // Handle JOINs if !from.joins.is_empty() { for join in &from.joins { let right = self.build_select_table(&join.table)?; plan = self.build_join(plan, right, &join.operator, &join.constraint)?; } } Ok(plan) } // Build a table reference fn build_select_table(&mut self, table: &ast::SelectTable) -> Result { match table { ast::SelectTable::Table(name, alias, _indexed) => { let table_name = Self::name_to_string(&name.name); // Check if it's a CTE reference if let Some(cte_plan) = self.ctes.get(&table_name) { return Ok(LogicalPlan::CTERef(CTERef { name: table_name.clone(), schema: cte_plan.schema().clone(), })); } // Regular table scan let table_alias = alias.as_ref().map(|a| match a { ast::As::As(name) => Self::name_to_string(name), ast::As::Elided(name) => Self::name_to_string(name), }); let table_schema = self.get_table_schema(&table_name, table_alias.as_deref())?; Ok(LogicalPlan::TableScan(TableScan { table_name, alias: table_alias, schema: table_schema, projection: None, })) } ast::SelectTable::Select(subquery, _alias) => self.build_select(subquery), ast::SelectTable::TableCall(_, _, _) => Err(LimboError::ParseError( "Table-valued functions are not supported in logical plans".to_string(), )), ast::SelectTable::Sub(_, _) => Err(LimboError::ParseError( "Subquery in FROM clause not yet supported".to_string(), )), } } // Build JOIN fn build_join( &mut self, left: LogicalPlan, right: LogicalPlan, op: &ast::JoinOperator, constraint: &Option, ) -> Result { // Determine join type let join_type = match op { ast::JoinOperator::Comma => JoinType::Cross, // Comma is essentially a cross join ast::JoinOperator::TypedJoin(Some(jt)) => { // Check the join type flags // Note: JoinType can have multiple flags set if jt.contains(ast::JoinType::NATURAL) { // Natural joins need special handling - find common columns return self.build_natural_join(left, right, JoinType::Inner); } else if jt.contains(ast::JoinType::LEFT) && jt.contains(ast::JoinType::RIGHT) && jt.contains(ast::JoinType::OUTER) { // FULL OUTER JOIN (has LEFT, RIGHT, and OUTER) JoinType::Full } else if jt.contains(ast::JoinType::LEFT) && jt.contains(ast::JoinType::OUTER) { JoinType::Left } else if jt.contains(ast::JoinType::RIGHT) && jt.contains(ast::JoinType::OUTER) { JoinType::Right } else if jt.contains(ast::JoinType::OUTER) && !jt.contains(ast::JoinType::LEFT) && !jt.contains(ast::JoinType::RIGHT) { // Plain OUTER JOIN should also be FULL JoinType::Full } else if jt.contains(ast::JoinType::LEFT) { JoinType::Left } else if jt.contains(ast::JoinType::RIGHT) { JoinType::Right } else if jt.contains(ast::JoinType::CROSS) || (jt.contains(ast::JoinType::INNER) && jt.contains(ast::JoinType::CROSS)) { JoinType::Cross } else { JoinType::Inner // Default to inner } } ast::JoinOperator::TypedJoin(None) => JoinType::Inner, // Default JOIN is INNER JOIN }; // Build join conditions let (on_conditions, filter) = match constraint { Some(ast::JoinConstraint::On(expr)) => { // Parse ON clause into equijoin conditions and filters self.parse_join_conditions(expr, left.schema(), right.schema())? } Some(ast::JoinConstraint::Using(columns)) => { // Build equijoin conditions from USING clause let on = self.build_using_conditions(columns, left.schema(), right.schema())?; (on, None) } None => { // Cross join or natural join (Vec::new(), None) } }; // Build combined schema let schema = self.build_join_schema(&left, &right, &join_type)?; Ok(LogicalPlan::Join(Join { left: Arc::new(left), right: Arc::new(right), join_type, on: on_conditions, filter, schema, })) } // Helper: Parse join conditions into equijoins and filters fn parse_join_conditions( &mut self, expr: &ast::Expr, left_schema: &SchemaRef, right_schema: &SchemaRef, ) -> Result { // For now, we'll handle simple equality conditions // More complex conditions will go into the filter let mut equijoins = Vec::new(); let mut filters = Vec::new(); // Try to extract equijoin conditions from the expression self.extract_equijoin_conditions( expr, left_schema, right_schema, &mut equijoins, &mut filters, )?; let filter = if filters.is_empty() { None } else { // Combine multiple filters with AND Some( filters .into_iter() .reduce(|acc, e| LogicalExpr::BinaryExpr { left: Box::new(acc), op: BinaryOperator::And, right: Box::new(e), }) .unwrap(), ) }; Ok((equijoins, filter)) } // Helper: Extract equijoin conditions from expression fn extract_equijoin_conditions( &mut self, expr: &ast::Expr, left_schema: &SchemaRef, right_schema: &SchemaRef, equijoins: &mut Vec<(LogicalExpr, LogicalExpr)>, filters: &mut Vec, ) -> Result<()> { match expr { ast::Expr::Binary(lhs, ast::Operator::Equals, rhs) => { // Check if this is an equijoin condition (left.col = right.col) let left_expr = self.build_expr(lhs, left_schema)?; let right_expr = self.build_expr(rhs, right_schema)?; // For simplicity, we'll check if one references left and one references right // In a real implementation, we'd need more sophisticated column resolution equijoins.push((left_expr, right_expr)); } ast::Expr::Binary(lhs, ast::Operator::And, rhs) => { // Recursively extract from AND conditions self.extract_equijoin_conditions( lhs, left_schema, right_schema, equijoins, filters, )?; self.extract_equijoin_conditions( rhs, left_schema, right_schema, equijoins, filters, )?; } _ => { // Other conditions go into the filter // We need a combined schema to build the expression let combined_schema = self.combine_schemas(left_schema, right_schema)?; let filter_expr = self.build_expr(expr, &combined_schema)?; filters.push(filter_expr); } } Ok(()) } // Helper: Build equijoin conditions from USING clause fn build_using_conditions( &mut self, columns: &[ast::Name], left_schema: &SchemaRef, right_schema: &SchemaRef, ) -> Result> { let mut conditions = Vec::new(); for col_name in columns { let name = Self::name_to_string(col_name); // Find the column in both schemas let _left_idx = left_schema .columns .iter() .position(|col| col.name == name) .ok_or_else(|| { LimboError::ParseError(format!("Column {name} not found in left table")) })?; let _right_idx = right_schema .columns .iter() .position(|col| col.name == name) .ok_or_else(|| { LimboError::ParseError(format!("Column {name} not found in right table")) })?; conditions.push(( LogicalExpr::Column(Column { name: name.clone(), table: None, // Will be resolved later }), LogicalExpr::Column(Column { name, table: None, // Will be resolved later }), )); } Ok(conditions) } // Helper: Build natural join by finding common columns fn build_natural_join( &mut self, left: LogicalPlan, right: LogicalPlan, join_type: JoinType, ) -> Result { let left_schema = left.schema(); let right_schema = right.schema(); // Find common column names let mut common_columns = Vec::new(); for left_col in &left_schema.columns { if right_schema .columns .iter() .any(|col| col.name == left_col.name) { common_columns.push(ast::Name::exact(left_col.name.clone())); } } if common_columns.is_empty() { // Natural join with no common columns becomes a cross join let schema = self.build_join_schema(&left, &right, &JoinType::Cross)?; return Ok(LogicalPlan::Join(Join { left: Arc::new(left), right: Arc::new(right), join_type: JoinType::Cross, on: Vec::new(), filter: None, schema, })); } // Build equijoin conditions for common columns let on = self.build_using_conditions(&common_columns, left_schema, right_schema)?; let schema = self.build_join_schema(&left, &right, &join_type)?; Ok(LogicalPlan::Join(Join { left: Arc::new(left), right: Arc::new(right), join_type, on, filter: None, schema, })) } // Helper: Build schema for join result fn build_join_schema( &self, left: &LogicalPlan, right: &LogicalPlan, _join_type: &JoinType, ) -> Result { let left_schema = left.schema(); let right_schema = right.schema(); // Concatenate the schemas, preserving all column information let mut columns = Vec::new(); // Keep all columns from left with their table info for col in &left_schema.columns { columns.push(col.clone()); } // Keep all columns from right with their table info for col in &right_schema.columns { columns.push(col.clone()); } Ok(Arc::new(LogicalSchema::new(columns))) } // Helper: Combine two schemas for expression building fn combine_schemas(&self, left: &SchemaRef, right: &SchemaRef) -> Result { let mut columns = left.columns.clone(); columns.extend(right.columns.clone()); Ok(Arc::new(LogicalSchema::new(columns))) } // Build projection fn build_projection( &mut self, input: LogicalPlan, columns: &[ast::ResultColumn], ) -> Result { let input_schema = input.schema(); let mut proj_exprs = Vec::new(); let mut schema_columns = Vec::new(); for col in columns { match col { ast::ResultColumn::Expr(expr, alias) => { let logical_expr = self.build_expr(expr, input_schema)?; let col_name = match alias { Some(as_alias) => match as_alias { ast::As::As(name) | ast::As::Elided(name) => Self::name_to_string(name), }, None => Self::expr_to_column_name(expr), }; let col_type = Self::infer_expr_type(&logical_expr, input_schema)?; schema_columns.push(ColumnInfo { name: col_name.clone(), ty: col_type, database: None, table: None, table_alias: None, }); if let Some(as_alias) = alias { let alias_name = match as_alias { ast::As::As(name) | ast::As::Elided(name) => Self::name_to_string(name), }; proj_exprs.push(LogicalExpr::Alias { expr: Box::new(logical_expr), alias: alias_name, }); } else { proj_exprs.push(logical_expr); } } ast::ResultColumn::Star => { // Expand * to all columns for col in &input_schema.columns { proj_exprs.push(LogicalExpr::Column(Column::new(col.name.clone()))); schema_columns.push(col.clone()); } } ast::ResultColumn::TableStar(table) => { // Expand table.* to all columns from that table let table_name = Self::name_to_string(table); for col in &input_schema.columns { // Simple check - would need proper table tracking in real implementation proj_exprs.push(LogicalExpr::Column(Column::with_table( col.name.clone(), table_name.clone(), ))); schema_columns.push(col.clone()); } } } } Ok(LogicalPlan::Projection(Projection { input: Arc::new(input), exprs: proj_exprs, schema: Arc::new(LogicalSchema::new(schema_columns)), })) } // Helper function to preprocess aggregate expressions that contain complex arguments // Returns: (needs_pre_projection, pre_projection_exprs, pre_projection_schema, modified_aggr_exprs) // // This will be used in expressions like select sum(hex(a + 2)) from tbl => hex(a + 2) is a // pre-projection. // // Another alternative is to always generate a projection together with an aggregation, and // just have "a" be the identity projection if we don't have a complex case. But that's quite // wasteful. fn preprocess_aggregate_expressions( aggr_exprs: &[LogicalExpr], group_exprs: &[LogicalExpr], input_schema: &SchemaRef, ) -> Result { let mut needs_pre_projection = false; let mut pre_projection_exprs = Vec::new(); let mut pre_projection_schema = Vec::new(); let mut modified_aggr_exprs = Vec::new(); let mut modified_group_exprs = Vec::new(); let mut projected_col_counter = 0; // First, add all group by expressions to the pre-projection for expr in group_exprs { if let LogicalExpr::Column(col) = expr { pre_projection_exprs.push(expr.clone()); let col_type = Self::infer_expr_type(expr, input_schema)?; pre_projection_schema.push(ColumnInfo { name: col.name.clone(), ty: col_type, database: None, table: col.table.clone(), table_alias: None, }); // Column references stay as-is in the modified group expressions modified_group_exprs.push(expr.clone()); } else { // Complex group by expression - project it needs_pre_projection = true; let proj_col_name = format!("__group_proj_{projected_col_counter}"); projected_col_counter += 1; pre_projection_exprs.push(expr.clone()); let col_type = Self::infer_expr_type(expr, input_schema)?; pre_projection_schema.push(ColumnInfo { name: proj_col_name.clone(), ty: col_type, database: None, table: None, table_alias: None, }); // Replace complex expression with reference to projected column modified_group_exprs.push(LogicalExpr::Column(Column { name: proj_col_name, table: None, })); } } // Check each aggregate expression for agg_expr in aggr_exprs { if let LogicalExpr::AggregateFunction { fun, args, distinct, } = agg_expr { let mut modified_args = Vec::new(); for arg in args { // Check if the argument is a simple column reference or a complex expression match arg { LogicalExpr::Column(_) => { // Simple column - just use it modified_args.push(arg.clone()); // Make sure the column is in the pre-projection if !pre_projection_exprs.iter().any(|e| e == arg) { pre_projection_exprs.push(arg.clone()); let col_type = Self::infer_expr_type(arg, input_schema)?; if let LogicalExpr::Column(col) = arg { pre_projection_schema.push(ColumnInfo { name: col.name.clone(), ty: col_type, database: None, table: col.table.clone(), table_alias: None, }); } } } _ => { // Complex expression - we need to project it first needs_pre_projection = true; let proj_col_name = format!("__agg_arg_proj_{projected_col_counter}"); projected_col_counter += 1; // Add the expression to the pre-projection pre_projection_exprs.push(arg.clone()); let col_type = Self::infer_expr_type(arg, input_schema)?; pre_projection_schema.push(ColumnInfo { name: proj_col_name.clone(), ty: col_type, database: None, table: None, table_alias: None, }); // In the aggregate, reference the projected column modified_args.push(LogicalExpr::Column(Column::new(proj_col_name))); } } } // Create the modified aggregate expression modified_aggr_exprs.push(LogicalExpr::AggregateFunction { fun: fun.clone(), args: modified_args, distinct: *distinct, }); } else { modified_aggr_exprs.push(agg_expr.clone()); } } Ok(( needs_pre_projection, pre_projection_exprs, pre_projection_schema, modified_aggr_exprs, modified_group_exprs, )) } // Build aggregate with GROUP BY fn build_aggregate( &mut self, input: LogicalPlan, group_by: &ast::GroupBy, columns: &[ast::ResultColumn], ) -> Result { let input_schema = input.schema(); // Build grouping expressions let mut group_exprs = Vec::new(); for expr in &group_by.exprs { group_exprs.push(self.build_expr(expr, input_schema)?); } // Use the unified aggregate builder self.build_aggregate_internal(input, group_exprs, columns) } // Build aggregate without GROUP BY fn build_aggregate_no_group( &mut self, input: LogicalPlan, columns: &[ast::ResultColumn], ) -> Result { // Use the unified aggregate builder with empty group expressions self.build_aggregate_internal(input, vec![], columns) } // Unified internal aggregate builder that handles both GROUP BY and non-GROUP BY cases fn build_aggregate_internal( &mut self, input: LogicalPlan, group_exprs: Vec, columns: &[ast::ResultColumn], ) -> Result { let input_schema = input.schema(); let has_group_by = !group_exprs.is_empty(); // First pass: build a map of aliases to expressions from the SELECT list // and a vector of SELECT expressions for positional references // This allows GROUP BY to reference SELECT aliases (e.g., GROUP BY year) // or positions (e.g., GROUP BY 1) let mut alias_to_expr = HashMap::default(); let mut select_exprs = Vec::new(); for col in columns { if let ast::ResultColumn::Expr(expr, alias) = col { let logical_expr = self.build_expr(expr, input_schema)?; select_exprs.push(logical_expr.clone()); if let Some(alias) = alias { let alias_name = match alias { ast::As::As(name) | ast::As::Elided(name) => Self::name_to_string(name), }; alias_to_expr.insert(alias_name, logical_expr); } } } // Resolve GROUP BY expressions: replace column references that match SELECT aliases // or integer literals that represent positions let group_exprs = group_exprs .into_iter() .map(|expr| { // Check for positional reference (integer literal) if let LogicalExpr::Literal(crate::types::Value::Numeric( crate::Numeric::Integer(pos), )) = &expr { // SQLite uses 1-based indexing if *pos > 0 && (*pos as usize) <= select_exprs.len() { return select_exprs[(*pos as usize) - 1].clone(); } } // Check for alias reference (unqualified column name) if let LogicalExpr::Column(col) = &expr { if col.table.is_none() { // Unqualified column - check if it matches an alias if let Some(aliased_expr) = alias_to_expr.get(&col.name) { return aliased_expr.clone(); } } } expr }) .collect::>(); // Build aggregate expressions and projection expressions let mut aggr_exprs = Vec::new(); let mut projection_exprs = Vec::new(); let mut aggregate_schema_columns = Vec::new(); // First, add GROUP BY columns to the aggregate output schema // These are always part of the aggregate operator's output for group_expr in &group_exprs { match group_expr { LogicalExpr::Column(col) => { // For column references in GROUP BY, preserve the original column info if let Some((_, col_info)) = input_schema.find_column(&col.name, col.table.as_deref()) { // Preserve the column with all its table information aggregate_schema_columns.push(col_info.clone()); } else { // Fallback if column not found (shouldn't happen) let col_type = Self::infer_expr_type(group_expr, input_schema)?; aggregate_schema_columns.push(ColumnInfo { name: col.name.clone(), ty: col_type, database: None, table: col.table.clone(), table_alias: None, }); } } _ => { // For complex GROUP BY expressions, generate a name let col_name = format!("__group_{}", aggregate_schema_columns.len()); let col_type = Self::infer_expr_type(group_expr, input_schema)?; aggregate_schema_columns.push(ColumnInfo { name: col_name, ty: col_type, database: None, table: None, table_alias: None, }); } } } // Track aggregates we've already seen to avoid duplicates let mut aggregate_map: HashMap = HashMap::default(); for col in columns { match col { ast::ResultColumn::Expr(expr, alias) => { let logical_expr = self.build_expr(expr, input_schema)?; // Determine the column name for this expression let col_name = match alias { Some(as_alias) => match as_alias { ast::As::As(name) | ast::As::Elided(name) => Self::name_to_string(name), }, None => Self::expr_to_column_name(expr), }; // Check if the TOP-LEVEL expression is an aggregate // We only care about immediate aggregates, not nested ones if Self::is_aggregate_expr(&logical_expr) { // Pure aggregate function - check if we've seen it before let agg_key = format!("{logical_expr:?}"); let agg_col_name = if let Some(existing_name) = aggregate_map.get(&agg_key) { // Reuse existing aggregate existing_name.clone() } else { // New aggregate - add it let col_type = Self::infer_expr_type(&logical_expr, input_schema)?; aggregate_schema_columns.push(ColumnInfo { name: col_name.clone(), ty: col_type, database: None, table: None, table_alias: None, }); aggr_exprs.push(logical_expr); aggregate_map.insert(agg_key, col_name.clone()); col_name.clone() }; // In the projection, reference this aggregate by name projection_exprs.push(LogicalExpr::Column(Column { name: agg_col_name, table: None, })); } else if Self::contains_aggregate(&logical_expr) { // This is an expression that contains an aggregate somewhere // (e.g., sum(a + 2) * 2) // We need to extract aggregates and replace them with column references let (processed_expr, extracted_aggs) = Self::extract_and_replace_aggregates_with_dedup( logical_expr, &mut aggregate_map, )?; // Add only new aggregates for (agg_expr, agg_name) in extracted_aggs { let agg_type = Self::infer_expr_type(&agg_expr, input_schema)?; aggregate_schema_columns.push(ColumnInfo { name: agg_name, ty: agg_type, database: None, table: None, table_alias: None, }); aggr_exprs.push(agg_expr); } // Add the processed expression (with column refs) to projection projection_exprs.push(processed_expr); } else { // Non-aggregate expression - validation depends on GROUP BY presence if has_group_by { // With GROUP BY: only allow constants and grouped columns // TODO: SQLite actually allows any column here and returns the value from // the first row encountered in each group. We should support this in the // future for full SQLite compatibility, but for now we're stricter to // simplify the DBSP compilation. if !Self::is_constant_expr(&logical_expr) && !Self::is_valid_in_group_by(&logical_expr, &group_exprs) { return Err(LimboError::ParseError(format!( "Column '{col_name}' must appear in the GROUP BY clause or be used in an aggregate function" ))); } // If this expression matches a GROUP BY expression, replace it with a reference // to the corresponding column in the aggregate output let logical_expr_stripped = strip_alias(&logical_expr); if let Some(group_idx) = group_exprs .iter() .position(|g| logical_expr_stripped == strip_alias(g)) { // Reference the GROUP BY column in the aggregate output by its name let group_col_name = &aggregate_schema_columns[group_idx].name; projection_exprs.push(LogicalExpr::Column(Column { name: group_col_name.clone(), table: None, })); } else { projection_exprs.push(logical_expr); } } else { // Without GROUP BY: only allow constant expressions // TODO: SQLite allows any column here and returns a value from an // arbitrary row. We should support this for full compatibility, // but for now we're stricter to simplify DBSP compilation. if !Self::is_constant_expr(&logical_expr) { return Err(LimboError::ParseError(format!( "Column '{col_name}' must be used in an aggregate function when using aggregates without GROUP BY" ))); } projection_exprs.push(logical_expr); } } } _ => { let error_msg = if has_group_by { "* not supported with GROUP BY".to_string() } else { "* not supported with aggregate functions".to_string() }; return Err(LimboError::ParseError(error_msg)); } } } // Check if any aggregate functions have complex expressions as arguments // or if GROUP BY has complex expressions // If so, we need to insert a projection before the aggregate let ( needs_pre_projection, pre_projection_exprs, pre_projection_schema, modified_aggr_exprs, modified_group_exprs, ) = Self::preprocess_aggregate_expressions(&aggr_exprs, &group_exprs, input_schema)?; // Build the final schema for the projection let mut projection_schema_columns = Vec::new(); for (i, expr) in projection_exprs.iter().enumerate() { let col_name = if i < columns.len() { match &columns[i] { ast::ResultColumn::Expr(e, alias) => match alias { Some(as_alias) => match as_alias { ast::As::As(name) | ast::As::Elided(name) => Self::name_to_string(name), }, None => Self::expr_to_column_name(e), }, _ => format!("col_{i}"), } } else { format!("col_{i}") }; // For type inference, we need the aggregate schema for column references let aggregate_schema = LogicalSchema::new(aggregate_schema_columns.clone()); let col_type = Self::infer_expr_type(expr, &Arc::new(aggregate_schema))?; projection_schema_columns.push(ColumnInfo { name: col_name, ty: col_type, database: None, table: None, table_alias: None, }); } // Create the input plan (with pre-projection if needed) let aggregate_input = if needs_pre_projection { Arc::new(LogicalPlan::Projection(Projection { input: Arc::new(input), exprs: pre_projection_exprs, schema: Arc::new(LogicalSchema::new(pre_projection_schema)), })) } else { Arc::new(input) }; // Use modified aggregate and group expressions if we inserted a pre-projection let final_aggr_exprs = if needs_pre_projection { modified_aggr_exprs } else { aggr_exprs }; let final_group_exprs = if needs_pre_projection { modified_group_exprs } else { group_exprs }; // Check if we need the outer projection // We need a projection if: // 1. We have expressions that compute new values (e.g., SUM(x) * 2) // 2. We're selecting a different set of columns than GROUP BY + aggregates // 3. We're reordering columns from their natural aggregate output order let needs_outer_projection = { // Check for complex expressions let has_complex_exprs = projection_exprs .iter() .any(|expr| !matches!(expr, LogicalExpr::Column(_))); if has_complex_exprs { true } else { // Check if we're selecting exactly what aggregate outputs in the same order // The aggregate outputs: all GROUP BY columns, then all aggregate expressions // The projection might select a subset or reorder these if projection_exprs.len() != aggregate_schema_columns.len() { // Different number of columns true } else { // Check if columns match in order and name !projection_exprs.iter().zip(&aggregate_schema_columns).all( |(expr, agg_col)| { if let LogicalExpr::Column(col) = expr { col.name == agg_col.name } else { false } }, ) } } }; // Create the aggregate node with its natural schema let aggregate_plan = LogicalPlan::Aggregate(Aggregate { input: aggregate_input, group_expr: final_group_exprs, aggr_expr: final_aggr_exprs, schema: Arc::new(LogicalSchema::new(aggregate_schema_columns)), }); if needs_outer_projection { Ok(LogicalPlan::Projection(Projection { input: Arc::new(aggregate_plan), exprs: projection_exprs, schema: Arc::new(LogicalSchema::new(projection_schema_columns)), })) } else { // No projection needed - aggregate output matches what we want Ok(aggregate_plan) } } /// Build VALUES clause #[allow(clippy::vec_box)] fn build_values(&mut self, values: &[Vec>]) -> Result { if values.is_empty() { return Err(LimboError::ParseError("Empty VALUES clause".to_string())); } let mut rows = Vec::new(); let first_row_len = values[0].len(); // Infer schema from first row let mut schema_columns = Vec::new(); for (i, _) in values[0].iter().enumerate() { schema_columns.push(ColumnInfo { name: format!("column{}", i + 1), ty: Type::Text, database: None, table: None, table_alias: None, }); } for row in values { if row.len() != first_row_len { return Err(LimboError::ParseError( "All rows in VALUES must have the same number of columns".to_string(), )); } let mut logical_row = Vec::new(); for expr in row { // VALUES doesn't have input schema let empty_schema = Arc::new(LogicalSchema::empty()); logical_row.push(self.build_expr(expr, &empty_schema)?); } rows.push(logical_row); } Ok(LogicalPlan::Values(Values { rows, schema: Arc::new(LogicalSchema::new(schema_columns)), })) } // Build SORT fn build_sort( &mut self, input: LogicalPlan, exprs: &[ast::SortedColumn], ) -> Result { let input_schema = input.schema(); let mut sort_exprs = Vec::new(); for sorted_col in exprs { let expr = self.build_expr(&sorted_col.expr, input_schema)?; sort_exprs.push(SortExpr { expr, asc: sorted_col.order != Some(ast::SortOrder::Desc), nulls_first: sorted_col.nulls == Some(ast::NullsOrder::First), }); } Ok(LogicalPlan::Sort(Sort { input: Arc::new(input), exprs: sort_exprs, })) } // Build LIMIT fn build_limit(input: LogicalPlan, limit: &ast::Limit) -> Result { let fetch = match limit.expr.as_ref() { ast::Expr::Literal(ast::Literal::Numeric(s)) => s.parse::().ok(), _ => { return Err(LimboError::ParseError( "LIMIT must be a literal integer".to_string(), )); } }; let skip = if let Some(offset) = &limit.offset { match offset.as_ref() { ast::Expr::Literal(ast::Literal::Numeric(s)) => s.parse::().ok(), _ => { return Err(LimboError::ParseError( "OFFSET must be a literal integer".to_string(), )); } } } else { None }; Ok(LogicalPlan::Limit(Limit { input: Arc::new(input), skip, fetch, })) } // Build compound operator (UNION, INTERSECT, EXCEPT) fn build_compound( left: LogicalPlan, right: LogicalPlan, op: &ast::CompoundOperator, ) -> Result { // Check schema compatibility if left.schema().column_count() != right.schema().column_count() { return Err(LimboError::ParseError( "UNION/INTERSECT/EXCEPT requires same number of columns".to_string(), )); } let all = matches!(op, ast::CompoundOperator::UnionAll); match op { ast::CompoundOperator::Union | ast::CompoundOperator::UnionAll => { let schema = left.schema().clone(); Ok(LogicalPlan::Union(Union { inputs: vec![Arc::new(left), Arc::new(right)], all, schema, })) } _ => Err(LimboError::ParseError( "INTERSECT and EXCEPT not yet supported in logical plans".to_string(), )), } } // Build expression from AST fn build_expr(&mut self, expr: &ast::Expr, _schema: &SchemaRef) -> Result { match expr { ast::Expr::Id(name) => Ok(LogicalExpr::Column(Column::new(Self::name_to_string(name)))), ast::Expr::DoublyQualified(db, table, col) => { Ok(LogicalExpr::Column(Column::with_table( Self::name_to_string(col), format!( "{}.{}", Self::name_to_string(db), Self::name_to_string(table) ), ))) } ast::Expr::Qualified(table, col) => Ok(LogicalExpr::Column(Column::with_table( Self::name_to_string(col), Self::name_to_string(table), ))), ast::Expr::Literal(lit) => Ok(LogicalExpr::Literal(Self::build_literal(lit)?)), ast::Expr::Binary(lhs, op, rhs) => { // Special case: IS NULL and IS NOT NULL if matches!(op, ast::Operator::Is | ast::Operator::IsNot) { if let ast::Expr::Literal(ast::Literal::Null) = rhs.as_ref() { let expr = Box::new(self.build_expr(lhs, _schema)?); return Ok(LogicalExpr::IsNull { expr, negated: matches!(op, ast::Operator::IsNot), }); } } let left = Box::new(self.build_expr(lhs, _schema)?); let right = Box::new(self.build_expr(rhs, _schema)?); Ok(LogicalExpr::BinaryExpr { left, op: *op, right, }) } ast::Expr::Unary(op, expr) => { let inner = Box::new(self.build_expr(expr, _schema)?); Ok(LogicalExpr::UnaryExpr { op: *op, expr: inner, }) } ast::Expr::FunctionCall { name, distinctness, args, filter_over, .. } => { // Check for window functions (OVER clause) if filter_over.over_clause.is_some() { return Err(LimboError::ParseError( "Unsupported expression type: window functions are not yet supported" .to_string(), )); } let func_name = Self::name_to_string(name); let arg_count = args.len(); // Check if it's an aggregate function (considering argument count for min/max) if let Some(agg_fun) = Self::parse_aggregate_function(&func_name, arg_count) { let distinct = distinctness.is_some(); let arg_exprs = args .iter() .map(|e| self.build_expr(e, _schema)) .collect::>>()?; Ok(LogicalExpr::AggregateFunction { fun: agg_fun, args: arg_exprs, distinct, }) } else { // Regular scalar function let arg_exprs = args .iter() .map(|e| self.build_expr(e, _schema)) .collect::>>()?; Ok(LogicalExpr::ScalarFunction { fun: func_name, args: arg_exprs, }) } } ast::Expr::FunctionCallStar { name, .. } => { // Handle COUNT(*) and similar let func_name = Self::name_to_string(name); // FunctionCallStar always has 0 args (it's the * form) if let Some(agg_fun) = Self::parse_aggregate_function(&func_name, 0) { Ok(LogicalExpr::AggregateFunction { fun: agg_fun, args: vec![], distinct: false, }) } else if let Ok(func) = crate::function::Func::resolve_function(&func_name, 0) { // Check if this function supports star expansion (e.g., json_object, jsonb_object) if func.needs_star_expansion() { // Expand * to all columns as alternating key-value pairs let mut args = Vec::new(); for col in &_schema.columns { // Add column name as string literal args.push(LogicalExpr::Literal(crate::types::Value::Text( col.name.clone().into(), ))); // Add column reference args.push(LogicalExpr::Column(Column::new(col.name.clone()))); } Ok(LogicalExpr::ScalarFunction { fun: func_name, args, }) } else { Err(LimboError::ParseError(format!( "Function {func_name}(*) is not supported" ))) } } else { Err(LimboError::ParseError(format!( "Function {func_name}(*) is not supported" ))) } } ast::Expr::Case { base, when_then_pairs, else_expr, } => { let case_expr = if let Some(e) = base { Some(Box::new(self.build_expr(e, _schema)?)) } else { None }; let when_then_exprs = when_then_pairs .iter() .map(|(when, then)| { Ok(( self.build_expr(when, _schema)?, self.build_expr(then, _schema)?, )) }) .collect::>>()?; let else_result = if let Some(e) = else_expr { Some(Box::new(self.build_expr(e, _schema)?)) } else { None }; Ok(LogicalExpr::Case { expr: case_expr, when_then: when_then_exprs, else_expr: else_result, }) } ast::Expr::InList { lhs, not, rhs } => { let expr = Box::new(self.build_expr(lhs, _schema)?); let list = rhs .iter() .map(|e| self.build_expr(e, _schema)) .collect::>>()?; Ok(LogicalExpr::InList { expr, list, negated: *not, }) } ast::Expr::InSelect { lhs, not, rhs } => { let expr = Box::new(self.build_expr(lhs, _schema)?); let subquery = Arc::new(self.build_select(rhs)?); Ok(LogicalExpr::InSubquery { expr, subquery, negated: *not, }) } ast::Expr::Exists(select) => { let subquery = Arc::new(self.build_select(select)?); Ok(LogicalExpr::Exists { subquery, negated: false, }) } ast::Expr::Subquery(select) => { let subquery = Arc::new(self.build_select(select)?); Ok(LogicalExpr::ScalarSubquery(subquery)) } ast::Expr::IsNull(lhs) => { let expr = Box::new(self.build_expr(lhs, _schema)?); Ok(LogicalExpr::IsNull { expr, negated: false, }) } ast::Expr::NotNull(lhs) => { let expr = Box::new(self.build_expr(lhs, _schema)?); Ok(LogicalExpr::IsNull { expr, negated: true, }) } ast::Expr::Between { lhs, not, start, end, } => { let expr = Box::new(self.build_expr(lhs, _schema)?); let low = Box::new(self.build_expr(start, _schema)?); let high = Box::new(self.build_expr(end, _schema)?); Ok(LogicalExpr::Between { expr, low, high, negated: *not, }) } ast::Expr::Like { lhs, not, op: _, rhs, escape, } => { let expr = Box::new(self.build_expr(lhs, _schema)?); let pattern = Box::new(self.build_expr(rhs, _schema)?); let escape_char = escape.as_ref().and_then(|e| { if let ast::Expr::Literal(ast::Literal::String(s)) = e.as_ref() { s.chars().next() } else { None } }); Ok(LogicalExpr::Like { expr, pattern, escape: escape_char, negated: *not, }) } ast::Expr::Parenthesized(exprs) => { // the assumption is that there is at least one parenthesis here. // If this is not true, then I don't understand this code and can't be trusted. turso_assert_ne!(exprs.len(), 0); // Multiple expressions in parentheses is unusual but handle it // by building the first one (SQLite behavior) self.build_expr(&exprs[0], _schema) } ast::Expr::Cast { expr, type_name } => { let inner = self.build_expr(expr, _schema)?; Ok(LogicalExpr::Cast { expr: Box::new(inner), type_name: type_name.clone(), }) } _ => Err(LimboError::ParseError(format!( "Unsupported expression type in logical plan: {expr:?}" ))), } } /// Build literal value fn build_literal(lit: &ast::Literal) -> Result { match lit { ast::Literal::Null => Ok(Value::Null), ast::Literal::True => Ok(Value::from_i64(1)), ast::Literal::False => Ok(Value::from_i64(0)), ast::Literal::Keyword(k) => { let k_bytes = k.as_bytes(); match_ignore_ascii_case!(match k_bytes { b"true" => Ok(Value::from_i64(1)), // SQLite uses int for bool b"false" => Ok(Value::from_i64(0)), // SQLite uses int for bool _ => Ok(Value::Text(k.clone().into())), }) } ast::Literal::Numeric(s) => { if let Ok(i) = s.parse::() { Ok(Value::from_i64(i)) } else if let Ok(f) = s.parse::() { Ok(Value::from_f64(f)) } else { Ok(Value::Text(s.clone().into())) } } ast::Literal::String(s) => { // Strip surrounding quotes from the SQL literal // The parser includes quotes in the string value let unquoted = if s.starts_with('\'') && s.ends_with('\'') && s.len() > 1 { &s[1..s.len() - 1] } else { s.as_str() }; Ok(Value::Text(unquoted.to_string().into())) } ast::Literal::Blob(b) => Ok(Value::Blob(b.clone().into())), ast::Literal::CurrentDate | ast::Literal::CurrentTime | ast::Literal::CurrentTimestamp => Err(LimboError::ParseError( "Temporal literals not yet supported".to_string(), )), } } /// Parse aggregate function name (considering argument count for min/max) fn parse_aggregate_function(name: &str, arg_count: usize) -> Option { let name_bytes = name.as_bytes(); match_ignore_ascii_case!(match name_bytes { b"COUNT" => Some(AggFunc::Count), b"SUM" => Some(AggFunc::Sum), b"AVG" => Some(AggFunc::Avg), // MIN and MAX are only aggregates with 1 argument // With 2+ arguments, they're scalar functions b"MIN" if arg_count == 1 => Some(AggFunc::Min), b"MAX" if arg_count == 1 => Some(AggFunc::Max), b"GROUP_CONCAT" => Some(AggFunc::GroupConcat), b"STRING_AGG" => Some(AggFunc::StringAgg), b"TOTAL" => Some(AggFunc::Total), b"ARRAY_AGG" => Some(AggFunc::ArrayAgg), _ => None, }) } // Check if expression contains aggregates fn has_aggregates(columns: &[ast::ResultColumn]) -> bool { for col in columns { if let ast::ResultColumn::Expr(expr, _) = col { if Self::expr_has_aggregate(expr) { return true; } } } false } // Check if AST expression contains aggregates fn expr_has_aggregate(expr: &ast::Expr) -> bool { match expr { ast::Expr::FunctionCall { name, args, .. } => { // Check if the function itself is an aggregate (considering arg count for min/max) let arg_count = args.len(); if Self::parse_aggregate_function(&Self::name_to_string(name), arg_count).is_some() { return true; } // Also check if any arguments contain aggregates (for nested functions like HEX(SUM(...))) args.iter().any(|arg| Self::expr_has_aggregate(arg)) } ast::Expr::FunctionCallStar { name, .. } => { // FunctionCallStar always has 0 args Self::parse_aggregate_function(&Self::name_to_string(name), 0).is_some() } ast::Expr::Binary(lhs, _, rhs) => { Self::expr_has_aggregate(lhs) || Self::expr_has_aggregate(rhs) } ast::Expr::Unary(_, e) => Self::expr_has_aggregate(e), ast::Expr::Case { when_then_pairs, else_expr, .. } => { when_then_pairs .iter() .any(|(w, t)| Self::expr_has_aggregate(w) || Self::expr_has_aggregate(t)) || else_expr .as_ref() .is_some_and(|e| Self::expr_has_aggregate(e)) } ast::Expr::Parenthesized(exprs) => { // Check if any parenthesized expression contains an aggregate exprs.iter().any(|e| Self::expr_has_aggregate(e)) } _ => false, } } // Check if logical expression is an aggregate fn is_aggregate_expr(expr: &LogicalExpr) -> bool { match expr { LogicalExpr::AggregateFunction { .. } => true, LogicalExpr::Alias { expr, .. } => Self::is_aggregate_expr(expr), _ => false, } } // Check if logical expression contains an aggregate anywhere fn contains_aggregate(expr: &LogicalExpr) -> bool { match expr { LogicalExpr::AggregateFunction { .. } => true, LogicalExpr::Alias { expr, .. } => Self::contains_aggregate(expr), LogicalExpr::BinaryExpr { left, right, .. } => { Self::contains_aggregate(left) || Self::contains_aggregate(right) } LogicalExpr::UnaryExpr { expr, .. } => Self::contains_aggregate(expr), LogicalExpr::ScalarFunction { args, .. } => args.iter().any(Self::contains_aggregate), LogicalExpr::Case { when_then, else_expr, .. } => { when_then .iter() .any(|(w, t)| Self::contains_aggregate(w) || Self::contains_aggregate(t)) || else_expr .as_ref() .is_some_and(|e| Self::contains_aggregate(e)) } _ => false, } } // Check if an expression is a constant (contains only literals) fn is_constant_expr(expr: &LogicalExpr) -> bool { match expr { LogicalExpr::Literal(_) => true, LogicalExpr::BinaryExpr { left, right, .. } => { Self::is_constant_expr(left) && Self::is_constant_expr(right) } LogicalExpr::UnaryExpr { expr, .. } => Self::is_constant_expr(expr), LogicalExpr::ScalarFunction { args, .. } => args.iter().all(Self::is_constant_expr), LogicalExpr::Alias { expr, .. } => Self::is_constant_expr(expr), _ => false, } } // Check if an expression is valid in GROUP BY context // An expression is valid if it's: // 1. A constant literal // 2. An aggregate function // 3. A grouping column (or expression involving only grouping columns) fn is_valid_in_group_by(expr: &LogicalExpr, group_exprs: &[LogicalExpr]) -> bool { // First check if the entire expression appears in GROUP BY // Strip aliases before comparing since SELECT might have aliases but GROUP BY might not let expr_stripped = strip_alias(expr); if group_exprs.iter().any(|g| expr_stripped == strip_alias(g)) { return true; } // If not, check recursively based on expression type match expr { LogicalExpr::Literal(_) => true, // Constants are always valid LogicalExpr::AggregateFunction { .. } => true, // Aggregates are valid LogicalExpr::Column(col) => { // Check if this column is in the GROUP BY group_exprs.iter().any(|g| match g { LogicalExpr::Column(gcol) => gcol.name == col.name, _ => false, }) } LogicalExpr::BinaryExpr { left, right, .. } => { // Both sides must be valid Self::is_valid_in_group_by(left, group_exprs) && Self::is_valid_in_group_by(right, group_exprs) } LogicalExpr::UnaryExpr { expr, .. } => Self::is_valid_in_group_by(expr, group_exprs), LogicalExpr::ScalarFunction { args, .. } => { // All arguments must be valid args.iter() .all(|arg| Self::is_valid_in_group_by(arg, group_exprs)) } LogicalExpr::Alias { expr, .. } => Self::is_valid_in_group_by(expr, group_exprs), _ => false, // Other expressions are not valid } } // Extract aggregates from an expression and replace them with column references, with deduplication // Returns the modified expression and a list of NEW (aggregate_expr, column_name) pairs fn extract_and_replace_aggregates_with_dedup( expr: LogicalExpr, aggregate_map: &mut HashMap, ) -> Result<(LogicalExpr, Vec<(LogicalExpr, String)>)> { let mut new_aggregates = Vec::new(); let mut counter = aggregate_map.len(); let new_expr = Self::replace_aggregates_with_columns_dedup( expr, &mut new_aggregates, aggregate_map, &mut counter, )?; Ok((new_expr, new_aggregates)) } // Recursively replace aggregate functions with column references, with deduplication fn replace_aggregates_with_columns_dedup( expr: LogicalExpr, new_aggregates: &mut Vec<(LogicalExpr, String)>, aggregate_map: &mut HashMap, counter: &mut usize, ) -> Result { match expr { LogicalExpr::AggregateFunction { .. } => { // Found an aggregate - check if we've seen it before let agg_key = format!("{expr:?}"); let col_name = if let Some(existing_name) = aggregate_map.get(&agg_key) { // Reuse existing aggregate existing_name.clone() } else { // New aggregate let col_name = format!("__agg_{}", *counter); *counter += 1; aggregate_map.insert(agg_key, col_name.clone()); new_aggregates.push((expr, col_name.clone())); col_name }; Ok(LogicalExpr::Column(Column { name: col_name, table: None, })) } LogicalExpr::BinaryExpr { left, op, right } => { let new_left = Self::replace_aggregates_with_columns_dedup( *left, new_aggregates, aggregate_map, counter, )?; let new_right = Self::replace_aggregates_with_columns_dedup( *right, new_aggregates, aggregate_map, counter, )?; Ok(LogicalExpr::BinaryExpr { left: Box::new(new_left), op, right: Box::new(new_right), }) } LogicalExpr::UnaryExpr { op, expr } => { let new_expr = Self::replace_aggregates_with_columns_dedup( *expr, new_aggregates, aggregate_map, counter, )?; Ok(LogicalExpr::UnaryExpr { op, expr: Box::new(new_expr), }) } LogicalExpr::ScalarFunction { fun, args } => { let mut new_args = Vec::new(); for arg in args { new_args.push(Self::replace_aggregates_with_columns_dedup( arg, new_aggregates, aggregate_map, counter, )?); } Ok(LogicalExpr::ScalarFunction { fun, args: new_args, }) } LogicalExpr::Case { expr: case_expr, when_then, else_expr, } => { let new_case_expr = if let Some(e) = case_expr { Some(Box::new(Self::replace_aggregates_with_columns_dedup( *e, new_aggregates, aggregate_map, counter, )?)) } else { None }; let mut new_when_then = Vec::new(); for (when, then) in when_then { let new_when = Self::replace_aggregates_with_columns_dedup( when, new_aggregates, aggregate_map, counter, )?; let new_then = Self::replace_aggregates_with_columns_dedup( then, new_aggregates, aggregate_map, counter, )?; new_when_then.push((new_when, new_then)); } let new_else = if let Some(e) = else_expr { Some(Box::new(Self::replace_aggregates_with_columns_dedup( *e, new_aggregates, aggregate_map, counter, )?)) } else { None }; Ok(LogicalExpr::Case { expr: new_case_expr, when_then: new_when_then, else_expr: new_else, }) } LogicalExpr::Alias { expr, alias } => { let new_expr = Self::replace_aggregates_with_columns_dedup( *expr, new_aggregates, aggregate_map, counter, )?; Ok(LogicalExpr::Alias { expr: Box::new(new_expr), alias, }) } // Other expressions - keep as is _ => Ok(expr), } } // Get column name from expression fn expr_to_column_name(expr: &ast::Expr) -> String { match expr { ast::Expr::Id(name) => Self::name_to_string(name), ast::Expr::Qualified(_, col) => Self::name_to_string(col), ast::Expr::FunctionCall { name, .. } => Self::name_to_string(name), ast::Expr::FunctionCallStar { name, .. } => { format!("{}(*)", Self::name_to_string(name)) } _ => "expr".to_string(), } } // Get table schema fn get_table_schema(&self, table_name: &str, alias: Option<&str>) -> Result { // Look up table in schema let table = self .schema .get_table(table_name) .ok_or_else(|| LimboError::ParseError(format!("Table '{table_name}' not found")))?; // Parse table_name which might be "db.table" for attached databases let (database, actual_table) = if table_name.contains('.') { let parts: Vec<&str> = table_name.splitn(2, '.').collect(); (Some(parts[0].to_string()), parts[1].to_string()) } else { (None, table_name.to_string()) }; let mut columns = Vec::new(); for col in table.columns() { if let Some(ref name) = col.name { columns.push(ColumnInfo { name: name.clone(), ty: col.ty(), database: database.clone(), table: Some(actual_table.clone()), table_alias: alias.map(|s| s.to_string()), }); } } Ok(Arc::new(LogicalSchema::new(columns))) } // Infer expression type fn infer_expr_type(expr: &LogicalExpr, schema: &SchemaRef) -> Result { match expr { LogicalExpr::Column(col) => { if let Some((_, col_info)) = schema.find_column(&col.name, col.table.as_deref()) { Ok(col_info.ty) } else { Ok(Type::Text) } } LogicalExpr::Literal(Value::Numeric(Numeric::Integer(_))) => Ok(Type::Integer), LogicalExpr::Literal(Value::Numeric(Numeric::Float(_))) => Ok(Type::Real), LogicalExpr::Literal(Value::Text(_)) => Ok(Type::Text), LogicalExpr::Literal(Value::Null) => Ok(Type::Null), LogicalExpr::Literal(Value::Blob(_)) => Ok(Type::Blob), LogicalExpr::BinaryExpr { op, left, right } => { match op { ast::Operator::Add | ast::Operator::Subtract | ast::Operator::Multiply => { // Infer types of operands to match SQLite/Numeric behavior let left_type = Self::infer_expr_type(left, schema)?; let right_type = Self::infer_expr_type(right, schema)?; // Integer op Integer = Integer (matching core/numeric/mod.rs behavior) // Any operation with Real = Real match (left_type, right_type) { (Type::Integer, Type::Integer) => Ok(Type::Integer), (Type::Integer, Type::Real) | (Type::Real, Type::Integer) | (Type::Real, Type::Real) => Ok(Type::Real), (Type::Null, _) | (_, Type::Null) => Ok(Type::Null), // For Text/Blob, SQLite coerces to numeric, defaulting to Real _ => Ok(Type::Real), } } ast::Operator::Divide => { // Division always produces Real in SQLite Ok(Type::Real) } ast::Operator::Modulus => { // Modulus follows same rules as other arithmetic ops let left_type = Self::infer_expr_type(left, schema)?; let right_type = Self::infer_expr_type(right, schema)?; match (left_type, right_type) { (Type::Integer, Type::Integer) => Ok(Type::Integer), _ => Ok(Type::Real), } } ast::Operator::Equals | ast::Operator::NotEquals | ast::Operator::Less | ast::Operator::LessEquals | ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::And | ast::Operator::Or | ast::Operator::Is | ast::Operator::IsNot => Ok(Type::Integer), ast::Operator::Concat => Ok(Type::Text), _ => Ok(Type::Text), // Default for other operators } } LogicalExpr::UnaryExpr { op, expr } => match op { ast::UnaryOperator::Not => Ok(Type::Integer), ast::UnaryOperator::Negative | ast::UnaryOperator::Positive => { Self::infer_expr_type(expr, schema) } ast::UnaryOperator::BitwiseNot => Ok(Type::Integer), }, LogicalExpr::AggregateFunction { fun, .. } => match fun { AggFunc::Count | AggFunc::Count0 => Ok(Type::Integer), AggFunc::Sum | AggFunc::Avg | AggFunc::Total => Ok(Type::Real), AggFunc::Min | AggFunc::Max => Ok(Type::Text), AggFunc::GroupConcat | AggFunc::StringAgg => Ok(Type::Text), AggFunc::ArrayAgg => Ok(Type::Blob), #[cfg(feature = "json")] AggFunc::JsonbGroupArray | AggFunc::JsonGroupArray | AggFunc::JsonbGroupObject | AggFunc::JsonGroupObject => Ok(Type::Text), AggFunc::External(_) => Ok(Type::Text), // Default for external }, LogicalExpr::Alias { expr, .. } => Self::infer_expr_type(expr, schema), LogicalExpr::IsNull { .. } => Ok(Type::Integer), LogicalExpr::InList { .. } | LogicalExpr::InSubquery { .. } => Ok(Type::Integer), LogicalExpr::Exists { .. } => Ok(Type::Integer), LogicalExpr::Between { .. } => Ok(Type::Integer), LogicalExpr::Like { .. } => Ok(Type::Integer), _ => Ok(Type::Text), } } } #[cfg(test)] mod tests { use super::*; use crate::schema::{BTreeTable, ColDef, Column as SchemaColumn, Schema, Type}; use turso_parser::parser::Parser; fn create_test_schema() -> Schema { let mut schema = Schema::new(); // Create users table let users_table = BTreeTable { name: "users".to_string(), root_page: 2, primary_key_columns: vec![("id".to_string(), turso_parser::ast::SortOrder::Asc)], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, columns: vec![ SchemaColumn::new( Some("id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, ..Default::default() }, ), SchemaColumn::new_default_text(Some("name".to_string()), "TEXT".to_string(), None), SchemaColumn::new_default_integer( Some("age".to_string()), "INTEGER".to_string(), None, ), SchemaColumn::new_default_text(Some("email".to_string()), "TEXT".to_string(), None), ], has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], }; schema .add_btree_table(Arc::new(users_table)) .expect("Test setup: failed to add users table"); // Create orders table let orders_table = BTreeTable { name: "orders".to_string(), root_page: 3, primary_key_columns: vec![("id".to_string(), turso_parser::ast::SortOrder::Asc)], columns: vec![ SchemaColumn::new( Some("id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, ..Default::default() }, ), SchemaColumn::new_default_integer( Some("user_id".to_string()), "INTEGER".to_string(), None, ), SchemaColumn::new_default_text( Some("product".to_string()), "TEXT".to_string(), None, ), SchemaColumn::new( Some("amount".to_string()), "REAL".to_string(), None, None, Type::Real, None, ColDef::default(), ), ], has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }; schema .add_btree_table(Arc::new(orders_table)) .expect("Test setup: failed to add orders table"); // Create products table let products_table = BTreeTable { name: "products".to_string(), root_page: 4, primary_key_columns: vec![("id".to_string(), turso_parser::ast::SortOrder::Asc)], columns: vec![ SchemaColumn::new( Some("id".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef { primary_key: true, rowid_alias: true, notnull: true, ..Default::default() }, ), SchemaColumn::new_default_text(Some("name".to_string()), "TEXT".to_string(), None), SchemaColumn::new( Some("price".to_string()), "REAL".to_string(), None, None, Type::Real, None, ColDef::default(), ), SchemaColumn::new_default_integer( Some("product_id".to_string()), "INTEGER".to_string(), None, ), ], has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }; schema .add_btree_table(Arc::new(products_table)) .expect("Test setup: failed to add products table"); schema } fn parse_and_build(sql: &str, schema: &Schema) -> Result { let mut parser = Parser::new(sql.as_bytes()); let cmd = parser .next() .ok_or_else(|| LimboError::ParseError("Empty statement".to_string()))? .map_err(|e| LimboError::ParseError(e.to_string()))?; match cmd { ast::Cmd::Stmt(stmt) => { let mut builder = LogicalPlanBuilder::new(schema); builder.build_statement(&stmt) } _ => Err(LimboError::ParseError( "Only SQL statements are supported".to_string(), )), } } #[test] fn test_simple_select() { let schema = create_test_schema(); let sql = "SELECT id, name FROM users"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 2); assert!(matches!(proj.exprs[0], LogicalExpr::Column(_))); assert!(matches!(proj.exprs[1], LogicalExpr::Column(_))); match &*proj.input { LogicalPlan::TableScan(scan) => { assert_eq!(scan.table_name, "users"); } _ => panic!("Expected TableScan"), } } _ => panic!("Expected Projection"), } } #[test] fn test_select_with_filter() { let schema = create_test_schema(); let sql = "SELECT name FROM users WHERE age > 18"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 1); match &*proj.input { LogicalPlan::Filter(filter) => { assert!(matches!( filter.predicate, LogicalExpr::BinaryExpr { op: ast::Operator::Greater, .. } )); match &*filter.input { LogicalPlan::TableScan(scan) => { assert_eq!(scan.table_name, "users"); } _ => panic!("Expected TableScan"), } } _ => panic!("Expected Filter"), } } _ => panic!("Expected Projection"), } } #[test] fn test_aggregate_with_group_by() { let schema = create_test_schema(); let sql = "SELECT user_id, SUM(amount) FROM orders GROUP BY user_id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.group_expr.len(), 1); assert_eq!(agg.aggr_expr.len(), 1); assert_eq!(agg.schema.column_count(), 2); assert!(matches!( agg.aggr_expr[0], LogicalExpr::AggregateFunction { fun: AggFunc::Sum, .. } )); match &*agg.input { LogicalPlan::TableScan(scan) => { assert_eq!(scan.table_name, "orders"); } _ => panic!("Expected TableScan"), } } _ => panic!("Expected Aggregate (no projection)"), } } #[test] fn test_aggregate_without_group_by() { let schema = create_test_schema(); let sql = "SELECT COUNT(*), MAX(age) FROM users"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.group_expr.len(), 0); assert_eq!(agg.aggr_expr.len(), 2); assert_eq!(agg.schema.column_count(), 2); assert!(matches!( agg.aggr_expr[0], LogicalExpr::AggregateFunction { fun: AggFunc::Count, .. } )); assert!(matches!( agg.aggr_expr[1], LogicalExpr::AggregateFunction { fun: AggFunc::Max, .. } )); } _ => panic!("Expected Aggregate (no projection)"), } } #[test] fn test_order_by() { let schema = create_test_schema(); let sql = "SELECT name FROM users ORDER BY age DESC, name ASC"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Sort(sort) => { assert_eq!(sort.exprs.len(), 2); assert!(!sort.exprs[0].asc); // DESC assert!(sort.exprs[1].asc); // ASC match &*sort.input { LogicalPlan::Projection(_) => {} _ => panic!("Expected Projection"), } } _ => panic!("Expected Sort"), } } #[test] fn test_limit_offset() { let schema = create_test_schema(); let sql = "SELECT * FROM users LIMIT 10 OFFSET 5"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Limit(limit) => { assert_eq!(limit.fetch, Some(10)); assert_eq!(limit.skip, Some(5)); } _ => panic!("Expected Limit"), } } #[test] fn test_order_by_with_limit() { let schema = create_test_schema(); let sql = "SELECT name FROM users ORDER BY age DESC LIMIT 5"; let plan = parse_and_build(sql, &schema).unwrap(); // Should produce: Limit -> Sort -> Projection -> TableScan match plan { LogicalPlan::Limit(limit) => { assert_eq!(limit.fetch, Some(5)); assert_eq!(limit.skip, None); match &*limit.input { LogicalPlan::Sort(sort) => { assert_eq!(sort.exprs.len(), 1); assert!(!sort.exprs[0].asc); // DESC match &*sort.input { LogicalPlan::Projection(_) => {} _ => panic!("Expected Projection under Sort"), } } _ => panic!("Expected Sort under Limit"), } } _ => panic!("Expected Limit at top level"), } } #[test] fn test_distinct() { let schema = create_test_schema(); let sql = "SELECT DISTINCT name FROM users"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Distinct(distinct) => match &*distinct.input { LogicalPlan::Projection(_) => {} _ => panic!("Expected Projection"), }, _ => panic!("Expected Distinct"), } } #[test] fn test_union() { let schema = create_test_schema(); let sql = "SELECT id FROM users UNION SELECT user_id FROM orders"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Union(union) => { assert!(!union.all); assert_eq!(union.inputs.len(), 2); } _ => panic!("Expected Union"), } } #[test] fn test_union_all() { let schema = create_test_schema(); let sql = "SELECT id FROM users UNION ALL SELECT user_id FROM orders"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Union(union) => { assert!(union.all); assert_eq!(union.inputs.len(), 2); } _ => panic!("Expected Union"), } } #[test] fn test_union_with_order_by() { let schema = create_test_schema(); let sql = "SELECT id, name FROM users UNION SELECT user_id, name FROM orders ORDER BY id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Sort(sort) => { assert_eq!(sort.exprs.len(), 1); assert!(sort.exprs[0].asc); // Default ASC match &*sort.input { LogicalPlan::Union(union) => { assert!(!union.all); // UNION (not UNION ALL) assert_eq!(union.inputs.len(), 2); } _ => panic!("Expected Union under Sort"), } } _ => panic!("Expected Sort at top level"), } } #[test] fn test_with_cte() { let schema = create_test_schema(); let sql = "WITH active_users AS (SELECT * FROM users WHERE age > 18) SELECT name FROM active_users"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::WithCTE(with) => { assert_eq!(with.ctes.len(), 1); assert!(with.ctes.contains_key("active_users")); let cte = &with.ctes["active_users"]; match &**cte { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Filter(_) => {} _ => panic!("Expected Filter in CTE"), }, _ => panic!("Expected Projection in CTE"), } match &*with.body { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::CTERef(cte_ref) => { assert_eq!(cte_ref.name, "active_users"); } _ => panic!("Expected CTERef"), }, _ => panic!("Expected Projection in body"), } } _ => panic!("Expected WithCTE"), } } #[test] fn test_case_expression() { let schema = create_test_schema(); let sql = "SELECT CASE WHEN age < 18 THEN 'minor' WHEN age < 65 THEN 'adult' ELSE 'senior' END FROM users"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 1); assert!(matches!(proj.exprs[0], LogicalExpr::Case { .. })); } _ => panic!("Expected Projection"), } } #[test] fn test_in_list() { let schema = create_test_schema(); let sql = "SELECT * FROM users WHERE id IN (1, 2, 3)"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Filter(filter) => match &filter.predicate { LogicalExpr::InList { list, negated, .. } => { assert!(!negated); assert_eq!(list.len(), 3); } _ => panic!("Expected InList"), }, _ => panic!("Expected Filter"), }, _ => panic!("Expected Projection"), } } #[test] fn test_in_subquery() { let schema = create_test_schema(); let sql = "SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Filter(filter) => { assert!(matches!(filter.predicate, LogicalExpr::InSubquery { .. })); } _ => panic!("Expected Filter"), }, _ => panic!("Expected Projection"), } } #[test] fn test_exists_subquery() { let schema = create_test_schema(); let sql = "SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id)"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Filter(filter) => { assert!(matches!(filter.predicate, LogicalExpr::Exists { .. })); } _ => panic!("Expected Filter"), }, _ => panic!("Expected Projection"), } } #[test] fn test_between() { let schema = create_test_schema(); let sql = "SELECT * FROM users WHERE age BETWEEN 18 AND 65"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Filter(filter) => match &filter.predicate { LogicalExpr::Between { negated, .. } => { assert!(!negated); } _ => panic!("Expected Between"), }, _ => panic!("Expected Filter"), }, _ => panic!("Expected Projection"), } } #[test] fn test_like() { let schema = create_test_schema(); let sql = "SELECT * FROM users WHERE name LIKE 'John%'"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Filter(filter) => match &filter.predicate { LogicalExpr::Like { negated, escape, .. } => { assert!(!negated); assert!(escape.is_none()); } _ => panic!("Expected Like"), }, _ => panic!("Expected Filter"), }, _ => panic!("Expected Projection"), } } #[test] fn test_is_null() { let schema = create_test_schema(); let sql = "SELECT * FROM users WHERE email IS NULL"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Filter(filter) => match &filter.predicate { LogicalExpr::IsNull { negated, .. } => { assert!(!negated); } _ => panic!("Expected IsNull, got: {:?}", filter.predicate), }, _ => panic!("Expected Filter"), }, _ => panic!("Expected Projection"), } } #[test] fn test_is_not_null() { let schema = create_test_schema(); let sql = "SELECT * FROM users WHERE email IS NOT NULL"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Filter(filter) => match &filter.predicate { LogicalExpr::IsNull { negated, .. } => { assert!(negated); } _ => panic!("Expected IsNull"), }, _ => panic!("Expected Filter"), }, _ => panic!("Expected Projection"), } } #[test] fn test_values_clause() { let schema = create_test_schema(); let sql = "SELECT * FROM (VALUES (1, 'a'), (2, 'b'))"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Values(values) => { assert_eq!(values.rows.len(), 2); assert_eq!(values.rows[0].len(), 2); } _ => panic!("Expected Values"), }, _ => panic!("Expected Projection"), } } #[test] fn test_complex_expression_with_aggregation() { // Test: SELECT sum(id + 2) * 2 FROM orders GROUP BY user_id let schema = create_test_schema(); // Test the complex case: sum((id + 2)) * 2 with parentheses let sql = "SELECT sum((id + 2)) * 2 FROM orders GROUP BY user_id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 1); match &proj.exprs[0] { LogicalExpr::BinaryExpr { left, op, right } => { assert_eq!(*op, BinaryOperator::Multiply); assert!(matches!(**left, LogicalExpr::Column(_))); assert!(matches!(**right, LogicalExpr::Literal(_))); } _ => panic!("Expected BinaryExpr in projection"), } match &*proj.input { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.group_expr.len(), 1); assert_eq!(agg.aggr_expr.len(), 1); match &agg.aggr_expr[0] { LogicalExpr::AggregateFunction { fun, args, .. } => { assert_eq!(*fun, AggregateFunction::Sum); assert_eq!(args.len(), 1); match &args[0] { LogicalExpr::Column(col) => { assert!(col.name.starts_with("__agg_arg_proj_")); } _ => panic!( "Expected Column reference to projected expression in aggregate args, got {:?}", args[0] ), } } _ => panic!("Expected AggregateFunction"), } match &*agg.input { LogicalPlan::Projection(inner_proj) => { assert!(inner_proj.exprs.len() >= 2); let has_binary_add = inner_proj.exprs.iter().any(|e| { matches!( e, LogicalExpr::BinaryExpr { op: BinaryOperator::Add, .. } ) }); assert!( has_binary_add, "Should have id + 2 expression in inner projection" ); } _ => panic!("Expected Projection as input to Aggregate"), } } _ => panic!("Expected Aggregate under Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_function_on_aggregate_result() { let schema = create_test_schema(); let sql = "SELECT abs(sum(id)) FROM orders GROUP BY user_id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 1); match &proj.exprs[0] { LogicalExpr::ScalarFunction { fun, args } => { assert_eq!(fun, "abs"); assert_eq!(args.len(), 1); assert!(matches!(args[0], LogicalExpr::Column(_))); } _ => panic!("Expected ScalarFunction in projection"), } } _ => panic!("Expected Projection"), } } #[test] fn test_multiple_aggregates_with_arithmetic() { let schema = create_test_schema(); let sql = "SELECT sum(id) * 2 + count(*) FROM orders GROUP BY user_id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 1); match &proj.exprs[0] { LogicalExpr::BinaryExpr { op, .. } => { assert_eq!(*op, BinaryOperator::Add); } _ => panic!("Expected BinaryExpr"), } match &*proj.input { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.aggr_expr.len(), 2); } _ => panic!("Expected Aggregate"), } } _ => panic!("Expected Projection"), } } #[test] fn test_projection_aggregation_projection() { let schema = create_test_schema(); // This tests: projection -> aggregation -> projection // The inner projection computes (id + 2), then we aggregate sum(), then apply abs() let sql = "SELECT abs(sum(id + 2)) FROM orders GROUP BY user_id"; let plan = parse_and_build(sql, &schema).unwrap(); // Should produce: Projection(abs) -> Aggregate(sum) -> Projection(id + 2) -> TableScan match plan { LogicalPlan::Projection(outer_proj) => { assert_eq!(outer_proj.exprs.len(), 1); // Outer projection should apply abs() function match &outer_proj.exprs[0] { LogicalExpr::ScalarFunction { fun, args } => { assert_eq!(fun, "abs"); assert_eq!(args.len(), 1); assert!(matches!(args[0], LogicalExpr::Column(_))); } _ => panic!("Expected abs() function in outer projection"), } // Next should be the Aggregate match &*outer_proj.input { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.group_expr.len(), 1); assert_eq!(agg.aggr_expr.len(), 1); // The aggregate should be summing a column reference match &agg.aggr_expr[0] { LogicalExpr::AggregateFunction { fun, args, .. } => { assert_eq!(*fun, AggregateFunction::Sum); assert_eq!(args.len(), 1); // Should reference the projected column match &args[0] { LogicalExpr::Column(col) => { assert!(col.name.starts_with("__agg_arg_proj_")); } _ => panic!("Expected column reference in aggregate"), } } _ => panic!("Expected AggregateFunction"), } // Input to aggregate should be a projection computing id + 2 match &*agg.input { LogicalPlan::Projection(inner_proj) => { // Should have at least the group column and the computed expression assert!(inner_proj.exprs.len() >= 2); // Check for the id + 2 expression let has_add_expr = inner_proj.exprs.iter().any(|e| { matches!( e, LogicalExpr::BinaryExpr { op: BinaryOperator::Add, .. } ) }); assert!( has_add_expr, "Should have id + 2 expression in inner projection" ); } _ => panic!("Expected inner Projection under Aggregate"), } } _ => panic!("Expected Aggregate under outer Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_group_by_validation_allow_grouped_column() { let schema = create_test_schema(); // Test that grouped columns are allowed let sql = "SELECT user_id, COUNT(*) FROM orders GROUP BY user_id"; let result = parse_and_build(sql, &schema); assert!(result.is_ok(), "Should allow grouped column in SELECT"); } #[test] fn test_group_by_validation_allow_constants() { let schema = create_test_schema(); // Test that simple constants are allowed even when not grouped let sql = "SELECT user_id, 42, COUNT(*) FROM orders GROUP BY user_id"; let result = parse_and_build(sql, &schema); assert!( result.is_ok(), "Should allow simple constants in SELECT with GROUP BY" ); let sql_complex = "SELECT user_id, (100 + 50) * 2, COUNT(*) FROM orders GROUP BY user_id"; let result_complex = parse_and_build(sql_complex, &schema); assert!( result_complex.is_ok(), "Should allow complex constant expressions in SELECT with GROUP BY" ); } #[test] fn test_parenthesized_aggregate_expressions() { let schema = create_test_schema(); let sql = "SELECT 25, (MAX(id) / 3), 39 FROM orders"; let result = parse_and_build(sql, &schema); assert!( result.is_ok(), "Should handle parenthesized aggregate expressions" ); let plan = result.unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 3); assert!(matches!( proj.exprs[0], LogicalExpr::Literal(Value::Numeric(Numeric::Integer(25))) )); match &proj.exprs[1] { LogicalExpr::BinaryExpr { left, op, right } => { assert_eq!(*op, BinaryOperator::Divide); assert!(matches!(&**left, LogicalExpr::Column(_))); assert!(matches!( &**right, LogicalExpr::Literal(Value::Numeric(Numeric::Integer(3))) )); } _ => panic!("Expected BinaryExpr for (MAX(id) / 3)"), } assert!(matches!( proj.exprs[2], LogicalExpr::Literal(Value::Numeric(Numeric::Integer(39))) )); match &*proj.input { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.aggr_expr.len(), 1); assert!(matches!( agg.aggr_expr[0], LogicalExpr::AggregateFunction { fun: AggFunc::Max, .. } )); } _ => panic!("Expected Aggregate node under Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_duplicate_aggregate_reuse() { let schema = create_test_schema(); let sql = "SELECT (COUNT(*) - 225), 30, COUNT(*) FROM orders"; let result = parse_and_build(sql, &schema); assert!(result.is_ok(), "Should handle duplicate aggregates"); let plan = result.unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 3); match &proj.exprs[0] { LogicalExpr::BinaryExpr { left, op, right } => { assert_eq!(*op, BinaryOperator::Subtract); match &**left { LogicalExpr::Column(col) => { assert!(col.name.starts_with("__agg_") || col.name == "COUNT(*)"); } _ => panic!("Expected Column reference for COUNT(*)"), } assert!(matches!( &**right, LogicalExpr::Literal(Value::Numeric(Numeric::Integer(225))) )); } _ => panic!("Expected BinaryExpr for (COUNT(*) - 225)"), } assert!(matches!( proj.exprs[1], LogicalExpr::Literal(Value::Numeric(Numeric::Integer(30))) )); match &proj.exprs[2] { LogicalExpr::Column(col) => { assert!(col.name.starts_with("__agg_") || col.name == "COUNT(*)"); } _ => panic!("Expected Column reference for COUNT(*)"), } match &*proj.input { LogicalPlan::Aggregate(agg) => { assert_eq!( agg.aggr_expr.len(), 1, "Should have only one COUNT(*) aggregate" ); assert!(matches!( agg.aggr_expr[0], LogicalExpr::AggregateFunction { fun: AggFunc::Count, .. } )); } _ => panic!("Expected Aggregate node under Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_aggregate_without_group_by_allow_constants() { let schema = create_test_schema(); // Test that constants are allowed with aggregates even without GROUP BY let sql = "SELECT 42, COUNT(*), MAX(amount) FROM orders"; let result = parse_and_build(sql, &schema); assert!( result.is_ok(), "Should allow simple constants with aggregates without GROUP BY" ); // Test complex constant expressions let sql_complex = "SELECT (9 / 6) % 5, COUNT(*), MAX(amount) FROM orders"; let result_complex = parse_and_build(sql_complex, &schema); assert!( result_complex.is_ok(), "Should allow complex constant expressions with aggregates without GROUP BY" ); } #[test] fn test_aggregate_without_group_by_creates_aggregate_node() { let schema = create_test_schema(); // Test that aggregate without GROUP BY creates proper Aggregate node let sql = "SELECT MAX(amount) FROM orders"; let plan = parse_and_build(sql, &schema).unwrap(); // Should be: Aggregate -> TableScan (no projection needed for simple aggregate) match plan { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.group_expr.len(), 0, "Should have no group expressions"); assert_eq!( agg.aggr_expr.len(), 1, "Should have one aggregate expression" ); assert_eq!( agg.schema.column_count(), 1, "Schema should have one column" ); } _ => panic!("Expected Aggregate at top level (no projection)"), } } #[test] fn test_scalar_vs_aggregate_function_classification() { let schema = create_test_schema(); // Test MIN/MAX with 1 argument - should be aggregate let sql = "SELECT MIN(amount) FROM orders"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.aggr_expr.len(), 1, "MIN(x) should be an aggregate"); match &agg.aggr_expr[0] { LogicalExpr::AggregateFunction { fun, args, .. } => { assert!(matches!(fun, AggFunc::Min)); assert_eq!(args.len(), 1); } _ => panic!("Expected AggregateFunction"), } } _ => panic!("Expected Aggregate node for MIN(x)"), } // Test MIN/MAX with 2 arguments - should be scalar in projection let sql = "SELECT MIN(amount, user_id) FROM orders"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 1, "Should have one projection expression"); match &proj.exprs[0] { LogicalExpr::ScalarFunction { fun, args } => { assert_eq!( fun.to_lowercase(), "min", "MIN(x,y) should be a scalar function" ); assert_eq!(args.len(), 2); } _ => panic!("Expected ScalarFunction for MIN(x,y)"), } } _ => panic!("Expected Projection node for scalar MIN(x,y)"), } // Test MAX with 3 arguments - should be scalar let sql = "SELECT MAX(amount, user_id, id) FROM orders"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 1); match &proj.exprs[0] { LogicalExpr::ScalarFunction { fun, args } => { assert_eq!( fun.to_lowercase(), "max", "MAX(x,y,z) should be a scalar function" ); assert_eq!(args.len(), 3); } _ => panic!("Expected ScalarFunction for MAX(x,y,z)"), } } _ => panic!("Expected Projection node for scalar MAX(x,y,z)"), } // Test that MIN with 0 args is treated as scalar (will fail later in execution) let sql = "SELECT MIN() FROM orders"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &proj.exprs[0] { LogicalExpr::ScalarFunction { fun, args } => { assert_eq!(fun.to_lowercase(), "min"); assert_eq!(args.len(), 0, "MIN() should be scalar with 0 args"); } _ => panic!("Expected ScalarFunction for MIN()"), }, _ => panic!("Expected Projection for MIN()"), } // Test other functions that are always aggregate (COUNT, SUM, AVG) let sql = "SELECT COUNT(*), SUM(amount), AVG(amount) FROM orders"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.aggr_expr.len(), 3, "Should have 3 aggregate functions"); for expr in &agg.aggr_expr { assert!(matches!(expr, LogicalExpr::AggregateFunction { .. })); } } _ => panic!("Expected Aggregate node"), } // Test scalar functions that are never aggregates (ABS, ROUND, etc.) let sql = "SELECT ABS(amount), ROUND(amount), LENGTH(product) FROM orders"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 3, "Should have 3 scalar functions"); for expr in &proj.exprs { match expr { LogicalExpr::ScalarFunction { .. } => {} _ => panic!("Expected all ScalarFunctions"), } } } _ => panic!("Expected Projection node for scalar functions"), } } #[test] fn test_mixed_aggregate_and_group_columns() { let schema = create_test_schema(); // When selecting both aggregate and grouping columns let sql = "SELECT user_id, sum(id) FROM orders GROUP BY user_id"; let plan = parse_and_build(sql, &schema).unwrap(); // No projection needed - aggregate outputs exactly what we select match plan { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.group_expr.len(), 1); assert_eq!(agg.aggr_expr.len(), 1); assert_eq!(agg.schema.column_count(), 2); } _ => panic!("Expected Aggregate (no projection)"), } } #[test] fn test_scalar_function_wrapping_aggregate_no_group_by() { // Test: SELECT HEX(SUM(age + 2)) FROM users // Expected structure: // Projection { exprs: [ScalarFunction(HEX, [Column])] } // -> Aggregate { aggr_expr: [Sum(BinaryExpr(age + 2))], group_expr: [] } // -> Projection { exprs: [BinaryExpr(age + 2)] } // -> TableScan("users") let schema = create_test_schema(); let sql = "SELECT HEX(SUM(age + 2)) FROM users"; let mut parser = Parser::new(sql.as_bytes()); let stmt = parser.next().unwrap().unwrap(); let plan = match stmt { ast::Cmd::Stmt(stmt) => { let mut builder = LogicalPlanBuilder::new(&schema); builder.build_statement(&stmt).unwrap() } _ => panic!("Expected SQL statement"), }; match &plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 1, "Should have one expression"); match &proj.exprs[0] { LogicalExpr::ScalarFunction { fun, args } => { assert_eq!(fun, "HEX", "Outer function should be HEX"); assert_eq!(args.len(), 1, "HEX should have one argument"); match &args[0] { LogicalExpr::Column(_) => {} LogicalExpr::AggregateFunction { .. } => { panic!( "Aggregate function should not be embedded in projection! It should be in a separate Aggregate operator" ); } _ => panic!( "Expected column reference as argument to HEX, got: {:?}", args[0] ), } } _ => panic!("Expected ScalarFunction (HEX), got: {:?}", proj.exprs[0]), } match &*proj.input { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.group_expr.len(), 0, "Should have no GROUP BY"); assert_eq!( agg.aggr_expr.len(), 1, "Should have one aggregate expression" ); match &agg.aggr_expr[0] { LogicalExpr::AggregateFunction { fun, args, distinct, } => { assert_eq!(*fun, crate::function::AggFunc::Sum, "Should be SUM"); assert!(!distinct, "Should not be DISTINCT"); assert_eq!(args.len(), 1, "SUM should have one argument"); match &args[0] { LogicalExpr::Column(col) => { // When aggregate arguments are complex, they get pre-projected assert!( col.name.starts_with("__agg_arg_proj_"), "Should reference pre-projected column, got: {}", col.name ); } LogicalExpr::BinaryExpr { left, op, right } => { // Simple case without pre-projection (shouldn't happen with current implementation) assert_eq!(*op, ast::Operator::Add, "Should be addition"); match (&**left, &**right) { ( LogicalExpr::Column(col), LogicalExpr::Literal(val), ) => { assert_eq!( col.name, "age", "Should reference age column" ); assert_eq!( *val, Value::from_i64(2), "Should add 2" ); } _ => panic!("Expected age + 2"), } } _ => panic!( "Expected Column reference or BinaryExpr for aggregate argument, got: {:?}", args[0] ), } } _ => panic!("Expected AggregateFunction"), } match &*agg.input { LogicalPlan::TableScan(scan) => { assert_eq!(scan.table_name, "users"); } LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::TableScan(scan) => { assert_eq!(scan.table_name, "users"); } _ => panic!("Expected TableScan under projection"), }, _ => panic!("Expected TableScan or Projection under Aggregate"), } } _ => panic!( "Expected Aggregate operator under Projection, got: {:?}", proj.input ), } } _ => panic!("Expected Projection as top-level operator, got: {plan:?}"), } } // ===== JOIN TESTS ===== #[test] fn test_inner_join() { let schema = create_test_schema(); let sql = "SELECT * FROM users u INNER JOIN orders o ON u.id = o.user_id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { match &*proj.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Inner); assert!(!join.on.is_empty(), "Should have join conditions"); // Check left input is users match &*join.left { LogicalPlan::TableScan(scan) => { assert_eq!(scan.table_name, "users"); } _ => panic!("Expected TableScan for left input"), } // Check right input is orders match &*join.right { LogicalPlan::TableScan(scan) => { assert_eq!(scan.table_name, "orders"); } _ => panic!("Expected TableScan for right input"), } } _ => panic!("Expected Join under Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_left_join() { let schema = create_test_schema(); let sql = "SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 2); // name and amount match &*proj.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Left); assert!(!join.on.is_empty(), "Should have join conditions"); } _ => panic!("Expected Join under Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_right_join() { let schema = create_test_schema(); let sql = "SELECT * FROM orders o RIGHT JOIN users u ON o.user_id = u.id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Right); assert!(!join.on.is_empty(), "Should have join conditions"); } _ => panic!("Expected Join under Projection"), }, _ => panic!("Expected Projection at top level"), } } #[test] fn test_full_outer_join() { let schema = create_test_schema(); let sql = "SELECT * FROM users u FULL OUTER JOIN orders o ON u.id = o.user_id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Full); assert!(!join.on.is_empty(), "Should have join conditions"); } _ => panic!("Expected Join under Projection"), }, _ => panic!("Expected Projection at top level"), } } #[test] fn test_cross_join() { let schema = create_test_schema(); let sql = "SELECT * FROM users CROSS JOIN orders"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Cross); assert!(join.on.is_empty(), "Cross join should have no conditions"); assert!(join.filter.is_none(), "Cross join should have no filter"); } _ => panic!("Expected Join under Projection"), }, _ => panic!("Expected Projection at top level"), } } #[test] fn test_join_with_multiple_conditions() { let schema = create_test_schema(); let sql = "SELECT * FROM users u JOIN orders o ON u.id = o.user_id AND u.age > 18"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { match &*proj.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Inner); // Should have at least one equijoin condition assert!(!join.on.is_empty(), "Should have join conditions"); // Additional conditions may be in filter // The exact distribution depends on our implementation } _ => panic!("Expected Join under Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_join_using_clause() { let schema = create_test_schema(); // Note: Both tables should have an 'id' column for this to work let sql = "SELECT * FROM users JOIN orders USING (id)"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Inner); assert!( !join.on.is_empty(), "USING clause should create join conditions" ); } _ => panic!("Expected Join under Projection"), }, _ => panic!("Expected Projection at top level"), } } #[test] fn test_natural_join() { let schema = create_test_schema(); let sql = "SELECT * FROM users NATURAL JOIN orders"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { match &*proj.input { LogicalPlan::Join(join) => { // Natural join finds common columns (id in this case) // If no common columns, it becomes a cross join assert!( !join.on.is_empty() || join.join_type == JoinType::Cross, "Natural join should either find common columns or become cross join" ); } _ => panic!("Expected Join under Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_three_way_join() { let schema = create_test_schema(); let sql = "SELECT * FROM users u JOIN orders o ON u.id = o.user_id JOIN products p ON o.product_id = p.id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { match &*proj.input { LogicalPlan::Join(join2) => { // Second join (with products) assert_eq!(join2.join_type, JoinType::Inner); match &*join2.left { LogicalPlan::Join(join1) => { // First join (users with orders) assert_eq!(join1.join_type, JoinType::Inner); } _ => panic!("Expected nested Join for three-way join"), } } _ => panic!("Expected Join under Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_mixed_join_types() { let schema = create_test_schema(); let sql = "SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id INNER JOIN products p ON o.product_id = p.id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { match &*proj.input { LogicalPlan::Join(join2) => { // Second join should be INNER assert_eq!(join2.join_type, JoinType::Inner); match &*join2.left { LogicalPlan::Join(join1) => { // First join should be LEFT assert_eq!(join1.join_type, JoinType::Left); } _ => panic!("Expected nested Join"), } } _ => panic!("Expected Join under Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_join_with_filter() { let schema = create_test_schema(); let sql = "SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE o.amount > 100"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { match &*proj.input { LogicalPlan::Filter(filter) => { // WHERE clause creates a Filter above the Join match &*filter.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Inner); } _ => panic!("Expected Join under Filter"), } } _ => panic!("Expected Filter under Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_join_with_projection() { let schema = create_test_schema(); let sql = "SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(proj) => { assert_eq!(proj.exprs.len(), 2); // u.name and o.amount match &*proj.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Inner); } _ => panic!("Expected Join under Projection"), } } _ => panic!("Expected Projection at top level"), } } #[test] fn test_join_with_aggregation() { let schema = create_test_schema(); let sql = "SELECT u.name, SUM(o.amount) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Aggregate(agg) => { assert_eq!(agg.group_expr.len(), 1); // GROUP BY u.name assert_eq!(agg.aggr_expr.len(), 1); // SUM(o.amount) match &*agg.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Inner); } _ => panic!("Expected Join under Aggregate"), } } _ => panic!("Expected Aggregate"), } } #[test] fn test_join_with_order_by() { let schema = create_test_schema(); let sql = "SELECT * FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.amount DESC"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Sort(sort) => { assert_eq!(sort.exprs.len(), 1); assert!(!sort.exprs[0].asc); // DESC match &*sort.input { LogicalPlan::Projection(proj) => match &*proj.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Inner); } _ => panic!("Expected Join under Projection"), }, _ => panic!("Expected Projection under Sort"), } } _ => panic!("Expected Sort at top level"), } } #[test] fn test_join_in_subquery() { let schema = create_test_schema(); let sql = "SELECT * FROM ( SELECT u.id, u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id ) WHERE amount > 100"; let plan = parse_and_build(sql, &schema).unwrap(); match plan { LogicalPlan::Projection(outer_proj) => match &*outer_proj.input { LogicalPlan::Filter(filter) => match &*filter.input { LogicalPlan::Projection(inner_proj) => match &*inner_proj.input { LogicalPlan::Join(join) => { assert_eq!(join.join_type, JoinType::Inner); } _ => panic!("Expected Join in subquery"), }, _ => panic!("Expected Projection for subquery"), }, _ => panic!("Expected Filter"), }, _ => panic!("Expected Projection at top level"), } } #[test] fn test_join_ambiguous_column() { let schema = create_test_schema(); // Both users and orders have an 'id' column let sql = "SELECT id FROM users JOIN orders ON users.id = orders.user_id"; let result = parse_and_build(sql, &schema); // This might error or succeed depending on how we handle ambiguous columns // For now, just check that parsing completes match result { Ok(_) => { // If successful, the implementation handles ambiguous columns somehow } Err(_) => { // If error, the implementation rejects ambiguous columns } } } // Tests for strip_alias function #[test] fn test_strip_alias_with_alias() { let inner_expr = LogicalExpr::Column(Column::new("test")); let aliased = LogicalExpr::Alias { expr: Box::new(inner_expr.clone()), alias: "my_alias".to_string(), }; let stripped = strip_alias(&aliased); assert_eq!(stripped, &inner_expr); } #[test] fn test_strip_alias_without_alias() { let expr = LogicalExpr::Column(Column::new("test")); let stripped = strip_alias(&expr); assert_eq!(stripped, &expr); } #[test] fn test_strip_alias_literal() { let expr = LogicalExpr::Literal(Value::from_i64(42)); let stripped = strip_alias(&expr); assert_eq!(stripped, &expr); } #[test] fn test_strip_alias_scalar_function() { let expr = LogicalExpr::ScalarFunction { fun: "substr".to_string(), args: vec![ LogicalExpr::Column(Column::new("name")), LogicalExpr::Literal(Value::from_i64(1)), LogicalExpr::Literal(Value::from_i64(4)), ], }; let stripped = strip_alias(&expr); assert_eq!(stripped, &expr); } #[test] fn test_strip_alias_nested_alias() { // Test that strip_alias only removes the outermost alias let inner_expr = LogicalExpr::Column(Column::new("test")); let inner_alias = LogicalExpr::Alias { expr: Box::new(inner_expr.clone()), alias: "inner_alias".to_string(), }; let outer_alias = LogicalExpr::Alias { expr: Box::new(inner_alias.clone()), alias: "outer_alias".to_string(), }; let stripped = strip_alias(&outer_alias); assert_eq!(stripped, &inner_alias); // Stripping again should give us the inner expression let double_stripped = strip_alias(stripped); assert_eq!(double_stripped, &inner_expr); } #[test] fn test_strip_alias_comparison_with_alias() { // Test that two expressions match when one has an alias and one doesn't let base_expr = LogicalExpr::ScalarFunction { fun: "substr".to_string(), args: vec![ LogicalExpr::Column(Column::new("orderdate")), LogicalExpr::Literal(Value::from_i64(1)), LogicalExpr::Literal(Value::from_i64(4)), ], }; let aliased_expr = LogicalExpr::Alias { expr: Box::new(base_expr.clone()), alias: "year".to_string(), }; // Without strip_alias, they wouldn't match assert_ne!(&aliased_expr, &base_expr); // With strip_alias, they should match assert_eq!(strip_alias(&aliased_expr), &base_expr); assert_eq!(strip_alias(&base_expr), &base_expr); } #[test] fn test_strip_alias_binary_expr() { let expr = LogicalExpr::BinaryExpr { left: Box::new(LogicalExpr::Column(Column::new("a"))), op: BinaryOperator::Add, right: Box::new(LogicalExpr::Literal(Value::from_i64(1))), }; let stripped = strip_alias(&expr); assert_eq!(stripped, &expr); } #[test] fn test_strip_alias_aggregate_function() { let expr = LogicalExpr::AggregateFunction { fun: AggFunc::Sum, args: vec![LogicalExpr::Column(Column::new("amount"))], distinct: false, }; let stripped = strip_alias(&expr); assert_eq!(stripped, &expr); } #[test] fn test_strip_alias_comparison_multiple_expressions() { // Test comparing a list of expressions with and without aliases let expr1 = LogicalExpr::Column(Column::new("a")); let expr2 = LogicalExpr::ScalarFunction { fun: "substr".to_string(), args: vec![ LogicalExpr::Column(Column::new("b")), LogicalExpr::Literal(Value::from_i64(1)), LogicalExpr::Literal(Value::from_i64(4)), ], }; let aliased1 = LogicalExpr::Alias { expr: Box::new(expr1.clone()), alias: "col_a".to_string(), }; let aliased2 = LogicalExpr::Alias { expr: Box::new(expr2.clone()), alias: "year".to_string(), }; let select_exprs = [aliased1, aliased2]; let group_exprs = [expr1, expr2]; // Verify that stripping aliases allows matching for (select_expr, group_expr) in select_exprs.iter().zip(group_exprs.iter()) { assert_eq!(strip_alias(select_expr), group_expr); } } } ================================================ FILE: core/translate/main_loop/body.rs ================================================ use crate::translate::plan::SimpleAggregate; use crate::translate::{ aggregation::emit_collseq_if_needed, order_by::{custom_type_comparator, EmitOrderBy}, window::EmitWindow, }; use super::*; /// SQLite (and so Turso) processes joins as a nested loop. /// The loop may emit rows to various destinations depending on the query: /// - a GROUP BY sorter (grouping is done by sorting based on the GROUP BY keys and aggregating while the GROUP BY keys match) /// - a GROUP BY phase with no sorting (when the rows are already in the order required by the GROUP BY keys) /// - an AggStep (the columns are collected for aggregation, which is finished later) /// - a Window (rows are buffered and returned according to the rules of the window definition) /// - an ORDER BY sorter (when there is none of the above, but there is an ORDER BY) /// - a QueryResult (there is none of the above, so the loop either emits a ResultRow, or if it's a subquery, yields to the parent query) enum LoopEmitTarget { GroupBy, OrderBySorter, AggStep, Window, QueryResult, } /// Emits the bytecode for the inner loop of a query. /// At this point the cursors for all tables have been opened and rewound. pub fn emit_loop<'a>( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx<'a>, plan: &'a SelectPlan, ) -> Result<()> { LoopBodyEmitter::emit(program, t_ctx, plan) } /// Emits the select-loop body. pub struct LoopBodyEmitter; /// Internal state for loop-body emission. /// /// The body has one non-obvious ordering rule: anti-join body entry must be /// resolved before any body instructions are emitted, otherwise relocated /// constants can make the backward jump land incorrectly. struct LoopBody<'prog, 'ctx, 'plan> { program: &'prog mut ProgramBuilder, t_ctx: &'ctx mut TranslateCtx<'plan>, plan: &'plan SelectPlan, } impl LoopBodyEmitter { pub fn emit<'a>( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx<'a>, plan: &'a SelectPlan, ) -> Result<()> { LoopBody::new(program, t_ctx, plan).emit() } } impl<'prog, 'ctx, 'plan> LoopBody<'prog, 'ctx, 'plan> { const fn new( program: &'prog mut ProgramBuilder, t_ctx: &'ctx mut TranslateCtx<'plan>, plan: &'plan SelectPlan, ) -> Self { Self { program, t_ctx, plan, } } /// Resolve the final anti-join body target before any row-emission logic runs. fn resolve_anti_join_entry(&mut self) { // The innermost anti-join body entry must be resolved before any row // emission target is chosen, otherwise the late jump back into the body // can land on the wrong relocated instruction. if let Some(last_join) = self.plan.join_order.last() { let last_idx = last_join.original_idx; let is_anti = self.plan.table_references.joined_tables()[last_idx] .join_info .as_ref() .is_some_and(|ji| ji.is_anti()); if is_anti { if let Some(sa_meta) = self.t_ctx.meta_semi_anti_joins[last_idx].as_ref() { self.program .preassign_label_to_next_insn(sa_meta.label_body); } } } } /// Choose the row-consumption target for the already-open main loop. fn select_emit_target(&self) -> LoopEmitTarget { if self .plan .group_by .as_ref() .is_some_and(|gb| !gb.exprs.is_empty()) { return LoopEmitTarget::GroupBy; } if !self.plan.aggregates.is_empty() { return LoopEmitTarget::AggStep; } if self.plan.window.is_some() { return LoopEmitTarget::Window; } if !self.plan.order_by.is_empty() { return LoopEmitTarget::OrderBySorter; } LoopEmitTarget::QueryResult } /// Emit the loop body once all required entry labels are fixed. fn emit(mut self) -> Result<()> { self.resolve_anti_join_entry(); emit_loop_source( self.program, self.t_ctx, self.plan, self.select_emit_target(), ) } } /// This is a helper function for inner_loop_emit, /// which does a different thing depending on the emit target. /// See the InnerLoopEmitTarget enum for more details. fn emit_loop_source<'a>( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx<'a>, plan: &'a SelectPlan, emit_target: LoopEmitTarget, ) -> Result<()> { match emit_target { LoopEmitTarget::GroupBy => { let GroupByMetadata { row_source, registers, .. } = t_ctx.meta_group_by.as_ref().unwrap(); let start_reg = registers.reg_group_by_source_cols_start; let mut cur_reg = start_reg; // Collect all non-aggregate expressions in the following order: // 1. GROUP BY expressions. These serve as sort keys. // 2. Remaining non-aggregate expressions that are not in GROUP BY. // // Example: // SELECT col1, col2, SUM(col3) FROM table GROUP BY col1 // - col1 is added first (from GROUP BY) // - col2 is added second (non-aggregate, in SELECT, not in GROUP BY) for (expr, _) in t_ctx.non_aggregate_expressions.iter() { let key_reg = cur_reg; cur_reg += 1; translate_expr( program, Some(&plan.table_references), expr, key_reg, &t_ctx.resolver, )?; } match row_source { GroupByRowSource::Sorter { sort_cursor, sorter_column_count, reg_sorter_key, .. } => { // Sorter path: store only unique leaf columns from aggregate args. // Full expressions are re-evaluated from the pseudo cursor during aggregation. for leaf_expr in t_ctx.agg_leaf_columns.iter() { let reg = cur_reg; cur_reg += 1; translate_expr( program, Some(&plan.table_references), leaf_expr, reg, &t_ctx.resolver, )?; } sorter_insert( program, start_reg, *sorter_column_count, *sort_cursor, *reg_sorter_key, ); } GroupByRowSource::MainLoop { .. } => { for agg in plan.aggregates.iter() { for expr in agg.args.iter() { let agg_reg = cur_reg; cur_reg += 1; translate_expr( program, Some(&plan.table_references), expr, agg_reg, &t_ctx.resolver, )?; } } group_by_agg_phase(program, t_ctx, plan)?; } } Ok(()) } LoopEmitTarget::OrderBySorter => { EmitOrderBy::sorter_insert(program, t_ctx, plan)?; if let Distinctness::Distinct { ctx } = &plan.distinctness { let distinct_ctx = ctx.as_ref().expect("distinct context must exist"); program.preassign_label_to_next_insn(distinct_ctx.label_on_conflict); } Ok(()) } LoopEmitTarget::AggStep => { let start_reg = t_ctx .reg_agg_start .expect("aggregate registers must be initialized"); if let Some(SimpleAggregate::MinMax(min_max)) = &plan.simple_aggregate { let expr_reg = program.alloc_register(); translate_expr( program, Some(&plan.table_references), &min_max.argument, expr_reg, &t_ctx.resolver, )?; let loop_end = t_ctx .label_main_loop_end .expect("simple min/max requires the main-loop end label"); let label_on_null = if matches!(min_max.func, crate::function::AggFunc::Min) { // Ascending index order places NULLs first. Keep scanning until // the first non-NULL value, then jump straight to AggFinal. let label_on_null = program.allocate_label(); program.emit_insn(Insn::IsNull { reg: expr_reg, target_pc: label_on_null, }); Some(label_on_null) } else { None }; emit_collseq_if_needed(program, &plan.table_references, &min_max.argument); let comparator = custom_type_comparator( &min_max.argument, &plan.table_references, t_ctx.resolver.schema(), ); program.emit_insn(Insn::AggStep { acc_reg: start_reg, col: expr_reg, delimiter: 0, func: min_max.func.clone(), comparator, }); program.emit_insn(Insn::Goto { target_pc: loop_end, }); if let Some(label_on_null) = label_on_null { program.preassign_label_to_next_insn(label_on_null); } return Ok(()); } // In planner.rs, we have collected all aggregates from the SELECT clause, including ones where the aggregate is embedded inside // a more complex expression. Some examples: length(sum(x)), sum(x) + avg(y), sum(x) + 1, etc. // The result of those more complex expressions depends on the final result of the aggregate, so we don't translate the complete expressions here. // Instead, we accumulate the intermediate results of all aggreagates, and evaluate any expressions that do not contain aggregates. for (i, agg) in plan.aggregates.iter().enumerate() { let reg = start_reg + i; translate_aggregation_step( program, &plan.table_references, AggArgumentSource::new_from_expression(&agg.func, &agg.args, &agg.distinctness), reg, &t_ctx.resolver, )?; if let Distinctness::Distinct { ctx } = &agg.distinctness { let ctx = ctx .as_ref() .expect("distinct aggregate context not populated"); program.preassign_label_to_next_insn(ctx.label_on_conflict); } } let label_emit_nonagg_only_once = if let Some(flag) = t_ctx.reg_nonagg_emit_once_flag { let if_label = program.allocate_label(); program.emit_insn(Insn::If { reg: flag, target_pc: if_label, jump_if_null: false, }); Some(if_label) } else { None }; let col_start = t_ctx.reg_result_cols_start.unwrap(); // Process only non-aggregate columns let non_agg_columns = plan .result_columns .iter() .enumerate() .filter(|(_, rc)| !rc.contains_aggregates); for (i, rc) in non_agg_columns { let reg = col_start + i; // Must use no_constant_opt to prevent constant hoisting: in compound // selects (UNION ALL), all branches share the same result registers, // so hoisted constants from the last branch overwrite earlier branches. translate_expr_no_constant_opt( program, Some(&plan.table_references), &rc.expr, reg, &t_ctx.resolver, NoConstantOptReason::RegisterReuse, )?; } // For result columns that contain aggregates but also reference // non-aggregate columns (e.g. CASE WHEN SUM(1) THEN a ELSE b END), // pre-read those column references while the cursor is still valid. // They are cached in expr_to_reg_cache so that when the full // expression is evaluated after AggFinal, translate_expr finds // the cached values instead of reading from the exhausted cursor. for rc in plan .result_columns .iter() .filter(|rc| rc.contains_aggregates) { walk_expr(&rc.expr, &mut |expr: &Expr| -> Result { match expr { Expr::Column { .. } | Expr::RowId { .. } => { let reg = program.alloc_register(); translate_expr( program, Some(&plan.table_references), expr, reg, &t_ctx.resolver, )?; t_ctx.resolver.cache_scalar_expr_reg( Cow::Owned(expr.clone()), reg, false, &plan.table_references, )?; Ok(WalkControl::SkipChildren) } _ => { if plan.aggregates.iter().any(|a| a.original_expr == *expr) { return Ok(WalkControl::SkipChildren); } Ok(WalkControl::Continue) } } })?; } if let Some(label) = label_emit_nonagg_only_once { program.resolve_label(label, program.offset()); let flag = t_ctx.reg_nonagg_emit_once_flag.unwrap(); program.emit_int(1, flag); } Ok(()) } LoopEmitTarget::QueryResult => { turso_assert!( plan.aggregates.is_empty(), "QueryResult target should not have aggregates" ); let offset_jump_to = t_ctx .labels_main_loop .first() .map(|l| l.next) .or(t_ctx.label_main_loop_end); emit_select_result( program, &t_ctx.resolver, plan, t_ctx.label_main_loop_end, offset_jump_to, t_ctx.reg_nonagg_emit_once_flag, t_ctx.reg_offset, t_ctx.reg_result_cols_start.unwrap(), t_ctx.limit_ctx, )?; if let Distinctness::Distinct { ctx } = &plan.distinctness { let distinct_ctx = ctx.as_ref().expect("distinct context must exist"); program.preassign_label_to_next_insn(distinct_ctx.label_on_conflict); } Ok(()) } LoopEmitTarget::Window => { EmitWindow::emit_window_loop_source(program, t_ctx, plan)?; Ok(()) } } } /// Emit WHERE conditions and inner-loop entry for an unmatched outer hash join row. /// /// Filters applicable WHERE terms (non-ON, non-consumed), optionally restricted to /// `build_table_idx` / `probe_table_idx` when a Gosub wraps inner tables. Then either /// enters the inner-loop subroutine via Gosub or calls `emit_loop` directly. pub(super) fn emit_unmatched_row_conditions_and_loop<'a>( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx<'a>, plan: &'a SelectPlan, build_table_idx: usize, probe_table_idx: usize, skip_label: BranchOffset, gosub: Option<(usize, BranchOffset)>, ) -> Result<()> { let has_gosub = gosub.is_some(); let allowed_tables = { let mut m = TableMask::new(); m.add_table(build_table_idx); m.add_table(probe_table_idx); // When there's a gosub wrapping inner tables, we must also allow // conditions that reference outer tables (those appearing before the // hash join probe in the join order), since their cursors are valid // at the unmatched-scan point. if has_gosub { let probe_pos = plan .join_order .iter() .position(|j| j.original_idx == probe_table_idx) .expect("probe table must be in join order"); for join in &plan.join_order[..probe_pos] { m.add_table(join.original_idx); } } m }; for cond in plan .where_clause .iter() .filter(|c| !c.consumed && c.from_outer_join.is_none()) .filter(|c| { !has_gosub || expr_tables_subset_of(&c.expr, &plan.table_references, &allowed_tables) }) { let jump_target_when_true = program.allocate_label(); let condition_metadata = ConditionMetadata { jump_if_condition_is_true: false, jump_target_when_true, jump_target_when_false: skip_label, jump_target_when_null: skip_label, }; translate_condition_expr( program, &plan.table_references, &cond.expr, condition_metadata, &t_ctx.resolver, )?; program.preassign_label_to_next_insn(jump_target_when_true); } if let Some((reg, label)) = gosub { program.emit_insn(Insn::Gosub { target_pc: label, return_reg: reg, }); } else { emit_loop(program, t_ctx, plan)?; } Ok(()) } ================================================ FILE: core/translate/main_loop/close.rs ================================================ use super::*; use crate::translate::main_loop::hash::{ emit_hash_join_unmatched_build_rows, GraceHashLoop, HashProbeCloseEmitter, }; /// Represents final step of Loop emission pub struct CloseLoop; impl CloseLoop { pub fn emit<'a>( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx<'a>, tables: &TableReferences, join_order: &[JoinOrderMember], mode: OperationMode, select_plan: Option<&'a SelectPlan>, ) -> Result<()> { // We close the loops for all tables in reverse order, i.e. innermost first. // OPEN t1 // OPEN t2 // OPEN t3 // // CLOSE t3 // CLOSE t2 // CLOSE t1 for join in join_order.iter().rev() { let table_index = join.original_idx; let table = &tables.joined_tables()[table_index]; let loop_labels = *t_ctx .labels_main_loop .get(table_index) .expect("source has no loop labels"); // SEMI/ANTI-JOIN: emit Goto -> outer_next right after the body. // For semi-join: after body runs (one match found), skip inner's Next. // For anti-join: after body runs (inner exhausted), move to next outer row. let is_semi_or_anti = table .join_info .as_ref() .is_some_and(|ji| ji.is_semi_or_anti()); if is_semi_or_anti { let sa_meta = t_ctx.meta_semi_anti_joins[table_index] .as_ref() .expect("semi/anti-join must have SemiAntiJoinMetadata"); let comment = if table.join_info.as_ref().unwrap().is_semi() { "semi-join: early out after first match" } else { "anti-join: exit body, next outer row" }; program.add_comment(program.offset(), comment); program.emit_insn(Insn::Goto { target_pc: sa_meta.label_next_outer, }); } let (table_cursor_id, index_cursor_id) = table.resolve_cursors(program, mode.clone())?; // Track the "next iteration" offset for semi/anti-join label resolution. // For most operations this equals the loop_labels.next resolution offset; // HashJoin overrides it to point at the Gosub Return or HashNext instead. let mut semi_anti_next_pc = None; // Helper: resolve loop_labels.next and record its offset for semi/anti-join. let mut resolve_next = |program: &mut ProgramBuilder| { let pc = program.offset(); program.resolve_label(loop_labels.next, pc); semi_anti_next_pc = Some(pc); }; match &table.op { Operation::Scan(scan) => { resolve_next(program); match scan { Scan::BTreeTable { iter_dir, .. } => { let iteration_cursor_id = if let OperationMode::UPDATE( UpdateRowSource::PrebuiltEphemeralTable { ephemeral_table_cursor_id, .. }, ) = &mode { *ephemeral_table_cursor_id } else { index_cursor_id.unwrap_or_else(|| { table_cursor_id.expect( "Either ephemeral or index or table cursor must be opened", ) }) }; if *iter_dir == IterationDirection::Backwards { program.emit_insn(Insn::Prev { cursor_id: iteration_cursor_id, pc_if_prev: loop_labels.loop_start, }); } else { program.emit_insn(Insn::Next { cursor_id: iteration_cursor_id, pc_if_next: loop_labels.loop_start, }); } } Scan::VirtualTable { .. } => { program.emit_insn(Insn::VNext { cursor_id: table_cursor_id .expect("Virtual tables do not support covering indexes"), pc_if_next: loop_labels.loop_start, }); } Scan::Subquery { iter_dir } => { // Check if this is a materialized CTE (EphemeralTable) or coroutine if let Table::FromClauseSubquery(subquery) = &table.table { if let Some(QueryDestination::EphemeralTable { cursor_id, .. }) = subquery.plan.select_query_destination() { if *iter_dir == IterationDirection::Backwards { program.emit_insn(Insn::Prev { cursor_id: *cursor_id, pc_if_prev: loop_labels.loop_start, }); } else { program.emit_insn(Insn::Next { cursor_id: *cursor_id, pc_if_next: loop_labels.loop_start, }); } } else { turso_assert_eq!( *iter_dir, IterationDirection::Forwards, "coroutine-backed subqueries cannot scan backwards" ); // Coroutine-based subquery - use Goto to Yield program.emit_insn(Insn::Goto { target_pc: loop_labels.loop_start, }); } } else { // A subquery has no cursor to call Next on, so it just emits a Goto // to the Yield instruction, which in turn jumps back to the main loop of the subquery, // so that the next row from the subquery can be read. program.emit_insn(Insn::Goto { target_pc: loop_labels.loop_start, }); } } } program.preassign_label_to_next_insn(loop_labels.loop_end); } Operation::Search(search) => { // Materialized subqueries with ephemeral indexes are allowed let is_materialized_subquery = matches!( &table.table, Table::FromClauseSubquery(_) ) && matches!(search, Search::Seek { index: Some(idx), .. } if idx.ephemeral); turso_assert_some!( { is_from_clause: !matches!(table.table, Table::FromClauseSubquery(_)), is_materialized_subquery: is_materialized_subquery }, "Subqueries do not support index seeks unless materialized" ); resolve_next(program); let iteration_cursor_id = if let OperationMode::UPDATE(UpdateRowSource::PrebuiltEphemeralTable { ephemeral_table_cursor_id, .. }) = &mode { *ephemeral_table_cursor_id } else if is_materialized_subquery { // Table-backed materialized subquery seeks iterate the // auxiliary ephemeral index cursor. index_cursor_id.expect("materialized subquery must have index cursor") } else { index_cursor_id.unwrap_or_else(|| { table_cursor_id.expect( "Either ephemeral or index or table cursor must be opened", ) }) }; // Rowid equality point lookups are handled with a SeekRowid instruction which does not loop, so there is no need to emit a Next instruction. match search { Search::RowidEq { .. } => {} Search::Seek { seek_def, .. } => { if seek_def.iter_dir == IterationDirection::Backwards { program.emit_insn(Insn::Prev { cursor_id: iteration_cursor_id, pc_if_prev: loop_labels.loop_start, }); } else { program.emit_insn(Insn::Next { cursor_id: iteration_cursor_id, pc_if_next: loop_labels.loop_start, }); } } Search::InSeek { index, .. } => { let meta = t_ctx.meta_in_seeks[table_index] .as_ref() .expect("InSeek must have metadata"); let ephemeral_cursor_id = meta.ephemeral_cursor_id; let outer_loop_start = meta.outer_loop_start; let next_val_label = meta.next_val_label; let can_have_multiple_matches = index.is_some(); if can_have_multiple_matches { // Rowid InSeek uses SeekRowid, so one RHS key can produce at // most one row. Index-backed InSeek can hit duplicates, so // keep scanning the current key's match range before advancing // the ephemeral cursor to the next IN value. program.emit_insn(Insn::Next { cursor_id: iteration_cursor_id, pc_if_next: loop_labels.loop_start, }); } // Once the current key is exhausted (or a seek found nothing), // advance the outer ephemeral cursor and restart the equality seek. program.resolve_label(next_val_label, program.offset()); program.emit_insn(Insn::Next { cursor_id: ephemeral_cursor_id, pc_if_next: outer_loop_start, }); } } program.preassign_label_to_next_insn(loop_labels.loop_end); } Operation::IndexMethodQuery(_) => { resolve_next(program); program.emit_insn(Insn::Next { cursor_id: index_cursor_id.unwrap(), pc_if_next: loop_labels.loop_start, }); program.preassign_label_to_next_insn(loop_labels.loop_end); } Operation::HashJoin(ref hash_join_op) => { if let Some(hash_ctx) = t_ctx .hash_table_contexts .get(&hash_join_op.build_table_idx) .cloned() { // Emit the close-loop teardown for a hash-join probe table. semi_anti_next_pc = HashProbeCloseEmitter::new( program, t_ctx, hash_join_op, hash_ctx, select_plan, table_index, ) .emit()? .semi_anti_next_pc; } // Advance probe cursor. program.resolve_label(loop_labels.next, program.offset()); let probe_cursor_id = table_cursor_id.expect("Probe table must have a cursor"); program.emit_insn(Insn::Next { cursor_id: probe_cursor_id, pc_if_next: loop_labels.loop_start, }); program.preassign_label_to_next_insn(loop_labels.loop_end); // Outer joins: emit unmatched build rows with NULLs for the probe side. // This runs BEFORE grace so that in-memory partitions (with valid // matched_bits from the main probe) are scanned while still available. // At runtime, the scan skips spilled partitions — those are handled // per-partition inside the grace loop where matched_bits are still live. if matches!( hash_join_op.join_type, HashJoinType::LeftOuter | HashJoinType::FullOuter ) { if let Some(hash_ctx) = t_ctx .hash_table_contexts .get(&hash_join_op.build_table_idx) .cloned() { emit_hash_join_unmatched_build_rows( program, t_ctx, hash_join_op, &hash_ctx, select_plan, table_index, probe_cursor_id, )?; } } // Grace hash join processing: process spilled partition pairs. // At runtime, this is a no-op if the build side didn't spill. // For LEFT/FULL OUTER, each grace partition gets its own unmatched // scan before eviction (so matched_bits are still live). if let Some(hash_ctx) = t_ctx .hash_table_contexts .get(&hash_join_op.build_table_idx) .cloned() { // emit grace processing loop after the probe cursor is exhausted. GraceHashLoop::emit( program, t_ctx, hash_join_op, &hash_ctx, select_plan, table_index, probe_cursor_id, )?; } } Operation::MultiIndexScan(_) => { // MultiIndexScan uses RowSetRead for iteration - the next is handled // at the end of the RowSet read loop in emit_multi_index_scan_loop resolve_next(program); program.emit_insn(Insn::Goto { target_pc: loop_labels.loop_start, }); program.preassign_label_to_next_insn(loop_labels.loop_end); } } // Resolve any semi/anti-join "outer next" labels targeting this table. if let Some(pc) = semi_anti_next_pc { for meta in t_ctx.meta_semi_anti_joins.iter().flatten() { if meta.outer_table_idx == table_index { program.resolve_label(meta.label_next_outer, pc); } } } // SEMI/ANTI-JOIN: after loop_end (inner loop exhausted). // Semi-join: no match found -> skip outer row (Goto -> next_outer). // Anti-join: no match found -> run body (Goto -> label_body, jumps backward). if is_semi_or_anti { let sa_meta = t_ctx.meta_semi_anti_joins[table_index] .as_ref() .expect("semi/anti-join must have SemiAntiJoinMetadata"); let join_info = table.join_info.as_ref().unwrap(); if join_info.is_semi() { program.add_comment(program.offset(), "semi-join: no match, skip outer row"); program.emit_insn(Insn::Goto { target_pc: sa_meta.label_next_outer, }); } else { // Anti-join: inner exhausted without match -> run body program.add_comment(program.offset(), "anti-join: no match, emit outer row"); program.emit_insn(Insn::Goto { target_pc: sa_meta.label_body, }); } } // OUTER JOIN: may still need to emit NULLs for the right table. // Outer hash join probes are handled above via check_outer / unmatched scan. let is_outer_hash_join_probe = matches!( table.op, Operation::HashJoin(ref hj) if matches!( hj.join_type, HashJoinType::LeftOuter | HashJoinType::FullOuter ) ); if let Some(join_info) = table.join_info.as_ref() { if join_info.is_outer() && !is_outer_hash_join_probe { let lj_meta = t_ctx.meta_left_joins[table_index].as_ref().unwrap(); // The left join match flag is set to 1 when there is any match on the right table // (e.g. SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a). // If the left join match flag has been set to 1, we jump to the next row on the outer table, // i.e. continue to the next row of t1 in our example. program.resolve_label(lj_meta.label_match_flag_check_value, program.offset()); let label_when_right_table_notnull = program.allocate_label(); program.emit_insn(Insn::IfPos { reg: lj_meta.reg_match_flag, target_pc: label_when_right_table_notnull, decrement_by: 0, }); // If the left join match flag is still 0, it means there was no match on the right table, // but since it's a LEFT JOIN, we still need to emit a row with NULLs for the right table. // In that case, we now enter the routine that does exactly that. // First we set the right table cursor's "pseudo null bit" on, which means any Insn::Column will return NULL. // This needs to be set for both the table and the index cursor, if present, // since even if the iteration cursor is the index cursor, it might fetch values from the table cursor. [table_cursor_id, index_cursor_id] .iter() .filter_map(|maybe_cursor_id| maybe_cursor_id.as_ref()) .for_each(|cursor_id| { program.emit_insn(Insn::NullRow { cursor_id: *cursor_id, }); }); if let Table::FromClauseSubquery(from_clause_subquery) = &table.table { if let Some(start_reg) = from_clause_subquery.result_columns_start_reg { let column_count = from_clause_subquery.columns.len(); if column_count > 0 { // Subqueries materialize their row into registers rather than being read back // through a cursor. NullRow only affects cursor reads, so we also have to // explicitly null out the cached registers or stale values would be re-emitted. program.emit_insn(Insn::Null { dest: start_reg, dest_end: Some(start_reg + column_count - 1), }); } } } // Re-enter the loop body at match-flag set so // post-join predicates are re-evaluated with right-table NULLs. program.emit_insn(Insn::Goto { target_pc: lj_meta.label_match_flag_set_true, }); program.preassign_label_to_next_insn(label_when_right_table_notnull); } } } // After ALL loops are closed, emit HashClose for any hash tables that were built. // This must happen at the very end because hash join probe loops may be nested // inside outer loops that re-enter them. Hash tables used by materialization // subplans can be kept open and are skipped here. // // When inside a nested subquery (correlated or non-correlated), skip HashClose // because the hash build is protected by Once and must persist across subquery // re-invocations. The hash table will be cleaned up by ProgramState::reset(). if !program.is_nested() { for join in join_order.iter() { let table_index = join.original_idx; let table = &tables.joined_tables()[table_index]; if let Operation::HashJoin(hash_join_op) = &table.op { let build_table = &tables.joined_tables()[hash_join_op.build_table_idx]; let hash_table_reg: usize = build_table.internal_id.into(); if !program.should_keep_hash_table_open(hash_table_reg) { program.emit_insn(Insn::HashClose { hash_table_id: hash_table_reg, }); program.clear_hash_build_signature(hash_table_reg); } } } } Ok(()) } } pub(super) struct AutoIndexResult { pub(super) use_bloom_filter: bool, } pub(super) struct AutoIndexBuild<'a> { pub(super) index: &'a Arc, pub(super) table_cursor_id: CursorID, pub(super) index_cursor_id: CursorID, pub(super) table_has_rowid: bool, pub(super) num_seek_keys: usize, pub(super) seek_def: &'a SeekDef, pub(super) affinity_str: Option<&'a Arc>, } /// Open an ephemeral index cursor and build an automatic index on a table. /// This is used as a last-resort to avoid a nested full table scan /// Returns the cursor id of the ephemeral index cursor. pub(super) fn emit_autoindex( program: &mut ProgramBuilder, build: AutoIndexBuild<'_>, ) -> Result { let AutoIndexBuild { index, table_cursor_id, index_cursor_id, table_has_rowid, num_seek_keys, seek_def, affinity_str, } = build; turso_assert!(index.ephemeral, "index must be ephemeral", { "index_name": &index.name }); let label_ephemeral_build_end = program.allocate_label(); // Since this typically happens in an inner loop, we only build it once. program.emit_insn(Insn::Once { target_pc_when_reentered: label_ephemeral_build_end, }); program.emit_insn(Insn::OpenAutoindex { cursor_id: index_cursor_id, }); // Rewind source table let label_ephemeral_build_loop_start = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: table_cursor_id, pc_if_empty: label_ephemeral_build_loop_start, }); program.preassign_label_to_next_insn(label_ephemeral_build_loop_start); // Emit all columns from source table that are needed in the ephemeral index. // Also reserve a register for the rowid if the source table has rowids. let num_regs_to_reserve = index.columns.len() + table_has_rowid as usize; let ephemeral_cols_start_reg = program.alloc_registers(num_regs_to_reserve); for (i, col) in index.columns.iter().enumerate() { let reg = ephemeral_cols_start_reg + i; program.emit_column_or_rowid(table_cursor_id, col.pos_in_table, reg); } if table_has_rowid { program.emit_insn(Insn::RowId { cursor_id: table_cursor_id, dest: ephemeral_cols_start_reg + index.columns.len(), }); } let record_reg = program.alloc_register(); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(ephemeral_cols_start_reg), count: to_u16(num_regs_to_reserve), dest_reg: to_u16(record_reg), index_name: Some(index.name.clone()), affinity_str: affinity_str.map(|s| (**s).clone()), }); // Skip bloom filter for non-binary collations since it uses binary hashing. let use_bloom_filter = index.columns.iter().take(num_seek_keys).all(|col| { col.collation .is_none_or(|coll| matches!(coll, CollationSeq::Binary | CollationSeq::Unset)) }) && seek_def.start.op.eq_only(); if use_bloom_filter { program.emit_insn(Insn::FilterAdd { cursor_id: index_cursor_id, key_reg: ephemeral_cols_start_reg, num_keys: num_seek_keys, }); } program.emit_insn(Insn::IdxInsert { cursor_id: index_cursor_id, record_reg, unpacked_start: Some(ephemeral_cols_start_reg), unpacked_count: Some(num_regs_to_reserve as u16), flags: IdxInsertFlags::new().use_seek(false), }); program.emit_insn(Insn::Next { cursor_id: table_cursor_id, pc_if_next: label_ephemeral_build_loop_start, }); program.preassign_label_to_next_insn(label_ephemeral_build_end); Ok(AutoIndexResult { use_bloom_filter }) } ================================================ FILE: core/translate/main_loop/conditions.rs ================================================ use super::*; use crate::translate::subquery::emit_non_from_clause_subqueries_for_eval_at; fn condition_references_subquery(expr: &Expr, subqueries: &[NonFromClauseSubquery]) -> bool { subqueries .iter() .any(|s| expr_references_subquery_id(expr, s.internal_id)) } fn subquery_referenced_in_predicates( predicates: &[WhereTerm], from_outer_join: bool, subquery_id: TableInternalId, ) -> bool { predicates .iter() .filter(|cond| cond.from_outer_join.is_some() == from_outer_join) .any(|cond| expr_references_subquery_id(&cond.expr, subquery_id)) } #[allow(clippy::too_many_arguments)] fn emit_correlated_subqueries( program: &mut ProgramBuilder, resolver: &Resolver<'_>, table_references: &TableReferences, join_order: &[JoinOrderMember], join_index: usize, predicates: &[WhereTerm], subqueries: &mut [NonFromClauseSubquery], on_only: bool, ) -> Result<()> { emit_non_from_clause_subqueries_for_eval_at( program, resolver, subqueries, join_order, Some(table_references), EvalAt::Loop(join_index), |subquery| { subquery.correlated && (!on_only || subquery_referenced_in_predicates(predicates, true, subquery.internal_id)) }, ) } #[derive(Clone, Copy, PartialEq, Eq)] enum SubqueryRefFilter { WithoutSubqueryRefs, WithSubqueryRefs, } #[allow(clippy::too_many_arguments)] fn emit_conditions( program: &mut ProgramBuilder, t_ctx: &TranslateCtx<'_>, table_references: &TableReferences, join_order: &[JoinOrderMember], predicates: &[WhereTerm], join_index: usize, next: BranchOffset, from_outer_join: bool, subqueries: &[NonFromClauseSubquery], subquery_ref_filter: SubqueryRefFilter, ) -> Result<()> { for cond in predicates .iter() .filter(|cond| cond.from_outer_join.is_some() == from_outer_join) .filter(|cond| { cond.should_eval_at_loop(join_index, join_order, subqueries, Some(table_references)) }) .filter(|cond| match subquery_ref_filter { SubqueryRefFilter::WithoutSubqueryRefs => { !condition_references_subquery(&cond.expr, subqueries) } SubqueryRefFilter::WithSubqueryRefs => { condition_references_subquery(&cond.expr, subqueries) } }) { let jump_target_when_true = program.allocate_label(); let condition_metadata = ConditionMetadata { jump_if_condition_is_true: false, jump_target_when_true, jump_target_when_false: next, jump_target_when_null: next, }; translate_condition_expr( program, table_references, &cond.expr, condition_metadata, &t_ctx.resolver, )?; program.preassign_label_to_next_insn(jump_target_when_true); } Ok(()) } /// Per-loop predicate emission. /// /// Conditions that reference subquery results cannot be emitted until their /// correlated subqueries have run, so emission proceeds in three ordered steps. pub(super) struct LoopConditionEmitter<'a, 'ctx> { program: &'a mut ProgramBuilder, t_ctx: &'a TranslateCtx<'ctx>, table_references: &'a TableReferences, join_order: &'a [JoinOrderMember], predicates: &'a [WhereTerm], join_index: usize, condition_fail_target: BranchOffset, from_outer_join: bool, subqueries: &'a mut [NonFromClauseSubquery], } impl<'a, 'ctx> LoopConditionEmitter<'a, 'ctx> { #[allow(clippy::too_many_arguments)] pub(super) const fn new( program: &'a mut ProgramBuilder, t_ctx: &'a TranslateCtx<'ctx>, table_references: &'a TableReferences, join_order: &'a [JoinOrderMember], predicates: &'a [WhereTerm], join_index: usize, condition_fail_target: BranchOffset, from_outer_join: bool, subqueries: &'a mut [NonFromClauseSubquery], ) -> Self { Self { program, t_ctx, table_references, join_order, predicates, join_index, condition_fail_target, from_outer_join, subqueries, } } /// Emit predicates that do not depend on subquery result registers. fn emit_early_conditions(&mut self) -> Result<()> { emit_conditions( self.program, self.t_ctx, self.table_references, self.join_order, self.predicates, self.join_index, self.condition_fail_target, self.from_outer_join, self.subqueries, SubqueryRefFilter::WithoutSubqueryRefs, ) } /// Materialize correlated subqueries that become valid at this loop depth. fn emit_correlated_subqueries(&mut self) -> Result<()> { emit_correlated_subqueries( self.program, &self.t_ctx.resolver, self.table_references, self.join_order, self.join_index, self.predicates, self.subqueries, self.from_outer_join, ) } /// Emit predicates that read registers populated by correlated subqueries. fn emit_late_conditions(&mut self) -> Result<()> { emit_conditions( self.program, self.t_ctx, self.table_references, self.join_order, self.predicates, self.join_index, self.condition_fail_target, self.from_outer_join, self.subqueries, SubqueryRefFilter::WithSubqueryRefs, ) } pub(super) fn emit(mut self) -> Result<()> { self.emit_early_conditions()?; self.emit_correlated_subqueries()?; self.emit_late_conditions() } } ================================================ FILE: core/translate/main_loop/hash.rs ================================================ use crate::translate::emitter::HashLabels; use super::*; #[derive(Debug, Clone)] /// Payload layout metadata recorded during hash-build planning or reuse. pub(super) struct HashBuildPayloadInfo { pub payload_columns: Vec, pub key_affinities: String, pub use_bloom_filter: bool, pub bloom_filter_cursor_id: CursorID, pub allow_seek: bool, } fn expr_references_outer_query(expr: &Expr, table_references: &TableReferences) -> bool { let mut has_outer_ref = false; let _ = walk_expr(expr, &mut |e: &Expr| -> Result { match e { Expr::Column { table, .. } | Expr::RowId { table, .. } => { if table_references .find_outer_query_ref_by_internal_id(*table) .is_some() { has_outer_ref = true; } } _ => {} } Ok(WalkControl::Continue) }); has_outer_ref } /// Static configuration for a fresh hash-table build. struct HashBuildConfig { payload_columns: Vec, payload_signature_columns: Vec, key_affinities: String, collations: Vec, use_bloom_filter: bool, bloom_filter_cursor_id: CursorID, materialized_cursor_id: Option, use_materialized_keys: bool, allow_seek: bool, signature: HashBuildSignature, } /// Typestate entry point for hash-build planning. /// /// Planning decides whether an existing hash build can be reused and, if not, /// captures all configuration needed to emit a fresh build deterministically. pub(crate) struct HashBuildPlanner<'a, 'plan> { program: &'a mut ProgramBuilder, t_ctx: &'a mut TranslateCtx<'plan>, table_references: &'a TableReferences, non_from_clause_subqueries: &'a [NonFromClauseSubquery], predicates: &'a [WhereTerm], hash_join_op: &'a HashJoinOp, hash_build_cursor_id: CursorID, hash_table_id: usize, } /// A planned hash build whose signature check has already completed. pub(super) struct PreparedHashBuild<'a, 'plan> { planner: HashBuildPlanner<'a, 'plan>, config: HashBuildConfig, } /// Result of hash-build planning. /// /// Reuse means the caller can immediately probe an existing compatible hash /// table. Build means the caller must execute the prepared build before probing. pub(super) enum HashBuildPlan<'a, 'plan> { Reuse(HashBuildPayloadInfo), Build(Box>), } impl<'a, 'plan> HashBuildPlanner<'a, 'plan> { #[allow(clippy::too_many_arguments)] /// Capture the immutable inputs needed to decide whether to reuse or build. pub(super) const fn new( program: &'a mut ProgramBuilder, t_ctx: &'a mut TranslateCtx<'plan>, table_references: &'a TableReferences, non_from_clause_subqueries: &'a [NonFromClauseSubquery], predicates: &'a [WhereTerm], hash_join_op: &'a HashJoinOp, hash_build_cursor_id: CursorID, hash_table_id: usize, ) -> Self { Self { program, t_ctx, table_references, non_from_clause_subqueries, predicates, hash_join_op, hash_build_cursor_id, hash_table_id, } } /// Decide whether the hash table can be reused or must be rebuilt. pub(super) fn prepare(self) -> Result> { let materialized_input = self .t_ctx .materialized_build_inputs .get(&self.hash_join_op.build_table_idx); let materialized_cursor_id = materialized_input.map(|input| input.cursor_id); let num_keys = self.hash_join_op.join_keys.len(); let mut key_affinities = String::new(); for join_key in &self.hash_join_op.join_keys { let build_expr = join_key.get_build_expr(self.predicates); let probe_expr = join_key.get_probe_expr(self.predicates); let affinity = comparison_affinity(build_expr, probe_expr, Some(self.table_references), None); key_affinities.push(affinity.aff_mask()); } let collations: Vec = self .hash_join_op .join_keys .iter() .map(|join_key| { let (original_lhs, original_rhs) = match join_key.build_side { BinaryExprSide::Lhs => ( join_key.get_build_expr(self.predicates), join_key.get_probe_expr(self.predicates), ), BinaryExprSide::Rhs => ( join_key.get_probe_expr(self.predicates), join_key.get_build_expr(self.predicates), ), }; resolve_comparison_collseq(original_lhs, original_rhs, self.table_references) .unwrap_or(CollationSeq::Binary) }) .collect(); let use_bloom_filter = self.hash_join_op.use_bloom_filter && collations .iter() .all(|c| matches!(c, CollationSeq::Binary | CollationSeq::Unset)); let build_table = &self.table_references.joined_tables()[self.hash_join_op.build_table_idx]; let (payload_columns, payload_signature_columns, use_materialized_keys, allow_seek) = match materialized_input.map(|input| &input.mode) { Some(MaterializedBuildInputMode::KeyPayload { num_keys: payload_num_keys, payload_columns, }) => { turso_assert!( *payload_num_keys == num_keys, "materialized hash build input key count mismatch" ); let payload_signature_columns = (0..payload_columns.len()) .map(|i| *payload_num_keys + i) .collect(); ( payload_columns.clone(), payload_signature_columns, true, false, ) } _ => { let payload_signature_columns: Vec = build_table.col_used_mask.iter().collect(); let payload_columns = payload_signature_columns .iter() .map(|col_idx| { let column = build_table .columns() .get(*col_idx) .expect("build table column missing"); MaterializedColumnRef::Column { table_id: build_table.internal_id, column_idx: *col_idx, is_rowid_alias: column.is_rowid_alias(), } }) .collect(); (payload_columns, payload_signature_columns, false, true) } }; let bloom_filter_cursor_id = if use_materialized_keys { materialized_cursor_id.expect("materialized input cursor is required") } else { self.hash_build_cursor_id }; let join_key_indices = self .hash_join_op .join_keys .iter() .map(|key| key.where_clause_idx) .collect::>(); let signature = HashBuildSignature { join_key_indices, payload_refs: payload_columns.clone(), key_affinities: key_affinities.clone(), use_bloom_filter, materialized_input_cursor: materialized_cursor_id, materialized_mode: materialized_input.as_ref().map(|input| match input.mode { MaterializedBuildInputMode::RowidOnly => MaterializedBuildInputModeTag::RowidOnly, MaterializedBuildInputMode::KeyPayload { .. } => { MaterializedBuildInputModeTag::Payload } }), }; if self .program .hash_build_signature_matches(self.hash_table_id, &signature) { return Ok(HashBuildPlan::Reuse(HashBuildPayloadInfo { payload_columns, key_affinities, use_bloom_filter, bloom_filter_cursor_id, allow_seek, })); } if self.program.has_hash_build_signature(self.hash_table_id) { self.program.emit_insn(Insn::HashClose { hash_table_id: self.hash_table_id, }); self.program.clear_hash_build_signature(self.hash_table_id); } Ok(HashBuildPlan::Build(Box::new(PreparedHashBuild { planner: self, config: HashBuildConfig { payload_columns, payload_signature_columns, key_affinities, collations, use_bloom_filter, bloom_filter_cursor_id, materialized_cursor_id, use_materialized_keys, allow_seek, signature, }, }))) } } impl<'a, 'plan> PreparedHashBuild<'a, 'plan> { /// Emit the fresh hash build after planning has fixed its configuration. pub(super) fn emit(self) -> Result { let Self { planner, config } = self; let build_table = &planner.table_references.joined_tables()[planner.hash_join_op.build_table_idx]; let btree = build_table .btree() .expect("Hash join build table must be a BTree table"); let num_keys = planner.hash_join_op.join_keys.len(); let build_key_start_reg = planner.program.alloc_registers(num_keys); let mut build_rowid_reg = None; let mut build_iter_cursor_id = planner.hash_build_cursor_id; let materialized_input = planner .t_ctx .materialized_build_inputs .get(&planner.hash_join_op.build_table_idx); if let Some(input) = materialized_input { match &input.mode { MaterializedBuildInputMode::RowidOnly => { build_rowid_reg = Some(planner.program.alloc_register()); build_iter_cursor_id = input.cursor_id; } MaterializedBuildInputMode::KeyPayload { .. } => { build_iter_cursor_id = input.cursor_id; } } } let (key_source_cursor_id, payload_source_cursor_id, hash_build_rowid_cursor_id) = if config.use_materialized_keys { ( build_iter_cursor_id, build_iter_cursor_id, build_iter_cursor_id, ) } else { ( planner.hash_build_cursor_id, planner.hash_build_cursor_id, planner.hash_build_cursor_id, ) }; let build_loop_start = planner.program.allocate_label(); let build_loop_end = planner.program.allocate_label(); let skip_to_next = planner.program.allocate_label(); let label_hash_build_end = planner.program.allocate_label(); planner.program.emit_insn(Insn::Once { target_pc_when_reentered: label_hash_build_end, }); if !config.use_materialized_keys { planner.program.emit_insn(Insn::OpenRead { cursor_id: planner.hash_build_cursor_id, root_page: btree.root_page, db: build_table.database_id, }); } planner.program.emit_insn(Insn::Rewind { cursor_id: build_iter_cursor_id, pc_if_empty: build_loop_end, }); if !config.use_materialized_keys { planner .program .set_cursor_override(build_table.internal_id, planner.hash_build_cursor_id); } planner .program .preassign_label_to_next_insn(build_loop_start); if let (Some(rowid_reg), Some(input_cursor_id)) = (build_rowid_reg, config.materialized_cursor_id) { planner .program .emit_column_or_rowid(input_cursor_id, 0, rowid_reg); planner.program.emit_insn(Insn::SeekRowid { cursor_id: planner.hash_build_cursor_id, src_reg: rowid_reg, target_pc: skip_to_next, }); } if !config.use_materialized_keys { let build_only_mask = TableMask::from_table_number_iter( [planner.hash_join_op.build_table_idx].into_iter(), ); for cond in planner.predicates.iter() { if cond.from_outer_join.is_some() { // OUTER JOIN predicates must stay on the right-table loop // recorded in `from_outer_join`; applying them while // building the hash table would drop unmatched build rows // before null-extension. continue; } let mask = table_mask_from_expr( &cond.expr, planner.table_references, planner.non_from_clause_subqueries, )?; if !mask.contains_table(planner.hash_join_op.build_table_idx) || !build_only_mask.contains_all(&mask) { continue; } if expr_references_outer_query(&cond.expr, planner.table_references) { continue; } let jump_target_when_true = planner.program.allocate_label(); let condition_metadata = ConditionMetadata { jump_if_condition_is_true: false, jump_target_when_true, jump_target_when_false: skip_to_next, jump_target_when_null: skip_to_next, }; translate_condition_expr( planner.program, planner.table_references, &cond.expr, condition_metadata, &planner.t_ctx.resolver, )?; planner .program .preassign_label_to_next_insn(jump_target_when_true); } } if config.use_materialized_keys { for idx in 0..num_keys { planner.program.emit_column_or_rowid( key_source_cursor_id, idx, build_key_start_reg + idx, ); } } else { for (idx, join_key) in planner.hash_join_op.join_keys.iter().enumerate() { let build_expr = join_key.get_build_expr(planner.predicates); translate_expr( planner.program, Some(planner.table_references), build_expr, build_key_start_reg + idx, &planner.t_ctx.resolver, )?; } } if let Some(count) = std::num::NonZeroUsize::new(num_keys) { planner.program.emit_insn(Insn::Affinity { start_reg: build_key_start_reg, count, affinities: config.key_affinities.clone(), }); } let num_payload = config.payload_columns.len(); let (payload_start_reg, mut payload_info) = if num_payload > 0 { let payload_reg = planner.program.alloc_registers(num_payload); for (i, &col_idx) in config.payload_signature_columns.iter().enumerate() { planner.program.emit_column_or_rowid( payload_source_cursor_id, col_idx, payload_reg + i, ); } ( Some(payload_reg), HashBuildPayloadInfo { payload_columns: config.payload_columns.clone(), key_affinities: config.key_affinities.clone(), use_bloom_filter: false, bloom_filter_cursor_id: config.bloom_filter_cursor_id, allow_seek: config.allow_seek, }, ) } else { ( None, HashBuildPayloadInfo { payload_columns: vec![], key_affinities: config.key_affinities.clone(), use_bloom_filter: false, bloom_filter_cursor_id: config.bloom_filter_cursor_id, allow_seek: config.allow_seek, }, ) }; if !config.use_materialized_keys { planner .program .clear_cursor_override(build_table.internal_id); } planner.program.emit_insn(Insn::HashBuild { data: Box::new(HashBuildData { cursor_id: hash_build_rowid_cursor_id, key_start_reg: build_key_start_reg, num_keys, hash_table_id: planner.hash_table_id, mem_budget: planner.hash_join_op.mem_budget, collations: config.collations, payload_start_reg, num_payload, track_matched: matches!( planner.hash_join_op.join_type, HashJoinType::LeftOuter | HashJoinType::FullOuter ), }), }); if config.use_bloom_filter { planner.program.emit_insn(Insn::FilterAdd { cursor_id: config.bloom_filter_cursor_id, key_reg: build_key_start_reg, num_keys, }); payload_info.use_bloom_filter = true; } planner.program.preassign_label_to_next_insn(skip_to_next); planner.program.emit_insn(Insn::Next { cursor_id: build_iter_cursor_id, pc_if_next: build_loop_start, }); planner.program.preassign_label_to_next_insn(build_loop_end); planner.program.emit_insn(Insn::HashBuildFinalize { hash_table_id: planner.hash_table_id, }); planner .program .record_hash_build_signature(planner.hash_table_id, config.signature); planner .program .preassign_label_to_next_insn(label_hash_build_end); Ok(payload_info) } } struct PreparedProbeBuild { build_cursor_id: CursorID, payload_info: HashBuildPayloadInfo, } struct ProbeSetupState { build_cursor_id: CursorID, payload_info: HashBuildPayloadInfo, payload_dest_reg: Option, match_reg: usize, hash_probe_miss_label: BranchOffset, match_found_label: BranchOffset, hash_next_label: BranchOffset, probe_rowid_reg: Option, key_start_reg: usize, num_keys: usize, grace_flag_reg: Option, } /// Hash-join probe setup in `open_loop`. /// /// Setup still runs in three ordered phases, but plain helper methods are enough: /// build or reuse the hash table, emit probe instructions, then install `HashCtx`. pub(super) struct HashProbeSetupEmitter<'a, 'plan> { program: &'a mut ProgramBuilder, t_ctx: &'a mut TranslateCtx<'plan>, table_references: &'a TableReferences, subqueries: &'a [NonFromClauseSubquery], predicates: &'a [WhereTerm], hash_join_op: &'a HashJoinOp, mode: &'a OperationMode, probe_cursor_id: CursorID, loop_start: BranchOffset, loop_end: BranchOffset, next: BranchOffset, live_table_ids: &'a HashSet, } impl<'a, 'plan> HashProbeSetupEmitter<'a, 'plan> { #[allow(clippy::too_many_arguments)] pub(super) const fn new( program: &'a mut ProgramBuilder, t_ctx: &'a mut TranslateCtx<'plan>, table_references: &'a TableReferences, subqueries: &'a [NonFromClauseSubquery], predicates: &'a [WhereTerm], hash_join_op: &'a HashJoinOp, mode: &'a OperationMode, probe_cursor_id: CursorID, loop_start: BranchOffset, loop_end: BranchOffset, next: BranchOffset, live_table_ids: &'a HashSet, ) -> Self { Self { program, t_ctx, table_references, subqueries, predicates, hash_join_op, mode, probe_cursor_id, loop_start, loop_end, next, live_table_ids, } } /// Ensure the build cursor exists and the hash table is ready for probing. fn prepare_build(&mut self) -> Result { let build_table = &self.table_references.joined_tables()[self.hash_join_op.build_table_idx]; let (build_cursor_id, _) = build_table.resolve_cursors(self.program, self.mode.clone())?; let build_cursor_id = if let Some(cursor_id) = build_cursor_id { cursor_id } else { let btree = build_table .btree() .expect("Hash join build table must be a BTree table"); let cursor_id = self.program.alloc_cursor_id_keyed_if_not_exists( CursorKey::table(build_table.internal_id), CursorType::BTreeTable(btree.clone()), ); self.program.emit_insn(Insn::OpenRead { cursor_id, root_page: btree.root_page, db: build_table.database_id, }); cursor_id }; let hash_table_id: usize = build_table.internal_id.into(); let btree = build_table .btree() .expect("Hash join build table must be a BTree table"); let hash_build_cursor_id = self.program.alloc_cursor_id_keyed_if_not_exists( CursorKey::hash_build(build_table.internal_id), CursorType::BTreeTable(btree), ); let payload_info = match HashBuildPlanner::new( self.program, self.t_ctx, self.table_references, self.subqueries, self.predicates, self.hash_join_op, hash_build_cursor_id, hash_table_id, ) .prepare()? { HashBuildPlan::Reuse(info) => Ok(info), HashBuildPlan::Build(prepared) => prepared.emit(), }?; Ok(PreparedProbeBuild { build_cursor_id, payload_info, }) } /// Emit the probe-side cursor positioning, key loading, and `HashProbe`. Advance /// to the state needed to install the resulting `HashCtx`. fn emit_probe(&mut self, prepared: PreparedProbeBuild) -> Result { let PreparedProbeBuild { build_cursor_id, payload_info, } = prepared; let build_table = &self.table_references.joined_tables()[self.hash_join_op.build_table_idx]; let hash_table_id: usize = build_table.internal_id.into(); let num_keys = self.hash_join_op.join_keys.len(); // For LEFT/FULL OUTER hash joins, reset matched_bits at the start of // each outer-loop iteration so marks from a previous probe pass don't // suppress NULL-fill rows in the current one. if matches!( self.hash_join_op.join_type, HashJoinType::LeftOuter | HashJoinType::FullOuter ) { self.program .emit_insn(Insn::HashResetMatched { hash_table_id }); } self.program.emit_insn(Insn::Rewind { cursor_id: self.probe_cursor_id, pc_if_empty: self.loop_end, }); self.program.preassign_label_to_next_insn(self.loop_start); let probe_key_start_reg = self.program.alloc_registers(num_keys); for (idx, join_key) in self.hash_join_op.join_keys.iter().enumerate() { let probe_expr = join_key.get_probe_expr(self.predicates); translate_expr( self.program, Some(self.table_references), probe_expr, probe_key_start_reg + idx, &self.t_ctx.resolver, )?; } if let Some(count) = std::num::NonZeroUsize::new(num_keys) { self.program.emit_insn(Insn::Affinity { start_reg: probe_key_start_reg, count, affinities: payload_info.key_affinities.clone(), }); } if payload_info.use_bloom_filter && self.hash_join_op.join_type != HashJoinType::FullOuter { self.program.emit_insn(Insn::Filter { cursor_id: payload_info.bloom_filter_cursor_id, target_pc: self.next, key_reg: probe_key_start_reg, num_keys, }); } let num_payload = payload_info.payload_columns.len(); let payload_dest_reg = if num_payload > 0 { Some(self.program.alloc_registers(num_payload)) } else { None }; if matches!(self.hash_join_op.join_type, HashJoinType::FullOuter) { let probe_table_idx = self.hash_join_op.probe_table_idx; if let Some(lj_meta) = self.t_ctx.meta_left_joins[probe_table_idx].as_ref() { self.program.emit_insn(Insn::Integer { value: 0, dest: lj_meta.reg_match_flag, }); } } let hash_probe_miss_label = if self.hash_join_op.join_type == HashJoinType::FullOuter { self.program.allocate_label() } else { self.next }; let (probe_rowid_reg, grace_flag_reg) = { let rowid_reg = self.program.alloc_register(); self.program.emit_insn(Insn::RowId { cursor_id: self.probe_cursor_id, dest: rowid_reg, }); // grace_flag_reg: 0 during main probe loop, 1 during grace loop let flag_reg = self.program.alloc_register(); self.program.emit_insn(Insn::Integer { value: 0, dest: flag_reg, }); (Some(rowid_reg), Some(flag_reg)) }; let match_reg = self.program.alloc_register(); self.program.emit_insn(Insn::HashProbe { hash_table_id: to_u16(hash_table_id), key_start_reg: to_u16(probe_key_start_reg), num_keys: to_u16(num_keys), dest_reg: to_u16(match_reg), target_pc: hash_probe_miss_label, payload_dest_reg: payload_dest_reg.map(to_u16), num_payload: to_u16(num_payload), // Main probe loop always carries the probe rowid so spilled build // partitions are deferred to grace processing instead of loaded here. probe_rowid_reg: probe_rowid_reg.map(to_u16), }); let match_found_label = self.program.allocate_label(); self.program.preassign_label_to_next_insn(match_found_label); let hash_next_label = self.program.allocate_label(); Ok(ProbeSetupState { build_cursor_id, payload_info, payload_dest_reg, match_reg, hash_probe_miss_label, match_found_label, hash_next_label, probe_rowid_reg, key_start_reg: probe_key_start_reg, num_keys, grace_flag_reg, }) } /// Install `HashCtx` and cache any payload-backed expressions for later reads. fn install_context(&mut self, state: ProbeSetupState) -> Result<()> { let ProbeSetupState { build_cursor_id, payload_info, payload_dest_reg, match_reg, hash_probe_miss_label, match_found_label, hash_next_label, probe_rowid_reg, key_start_reg, num_keys, grace_flag_reg, } = state; let build_table = &self.table_references.joined_tables()[self.hash_join_op.build_table_idx]; let hash_table_id: usize = build_table.internal_id.into(); let payload_columns = payload_info.payload_columns.clone(); let mut labels = HashLabels::new(match_found_label, hash_next_label); if self.hash_join_op.join_type == HashJoinType::FullOuter { labels.check_outer = Some(hash_probe_miss_label); }; self.t_ctx.hash_table_contexts.insert( self.hash_join_op.build_table_idx, HashCtx { labels, hash_table_reg: hash_table_id, match_reg, payload_start_reg: payload_dest_reg, payload_columns: payload_info.payload_columns, build_cursor_id: if payload_info.allow_seek { Some(build_cursor_id) } else { None }, join_type: self.hash_join_op.join_type, inner_loop_gosub_reg: None, probe_rowid_reg, key_start_reg, num_keys, grace_flag_reg, }, ); self.t_ctx.resolver.enable_expr_to_reg_cache(); let rowid_expr = Expr::RowId { database: None, table: build_table.internal_id, }; let payload_has_build_rowid = payload_columns.iter().any(|payload| { matches!( payload, MaterializedColumnRef::RowId { table_id } if *table_id == build_table.internal_id ) }); let build_table_is_live = self.live_table_ids.contains(&build_table.internal_id); if payload_info.allow_seek && !payload_has_build_rowid && !build_table_is_live { self.t_ctx .resolver .cache_expr_reg(Cow::Owned(rowid_expr), match_reg, false, None); } if let Some(payload_reg) = payload_dest_reg { for (i, payload) in payload_columns.iter().enumerate() { let (payload_table_id, expr, is_column) = match payload { MaterializedColumnRef::Column { table_id, column_idx, is_rowid_alias, } => ( *table_id, Expr::Column { database: None, table: *table_id, column: *column_idx, is_rowid_alias: *is_rowid_alias, }, true, ), MaterializedColumnRef::RowId { table_id } => ( *table_id, Expr::RowId { database: None, table: *table_id, }, false, ), }; if self.live_table_ids.contains(&payload_table_id) { continue; } if is_column { self.t_ctx.resolver.cache_scalar_expr_reg( Cow::Owned(expr), payload_reg + i, true, self.table_references, )?; } else { self.t_ctx.resolver.cache_expr_reg( Cow::Owned(expr), payload_reg + i, false, None, ); } } } else if payload_info.allow_seek && !build_table_is_live { self.program.emit_insn(Insn::SeekRowid { cursor_id: build_cursor_id, src_reg: match_reg, target_pc: hash_next_label, }); } Ok(()) } pub(super) fn emit(mut self) -> Result<()> { let prepared = self.prepare_build()?; let state = self.emit_probe(prepared)?; self.install_context(state) } } struct ProbeCloseState { label_next_probe_row: BranchOffset, semi_anti_next_pc: Option, } /// Result of emitting hash-join probe teardown. pub(super) struct HashProbeCloseOutcome { pub semi_anti_next_pc: Option, } /// Close-loop path of a hash-join probe. /// /// The teardown remains ordered: emit `HashNext`, optionally emit FULL OUTER /// unmatched probe rows, then return the probe-row advance state. pub(super) struct HashProbeCloseEmitter<'a, 'plan> { program: &'a mut ProgramBuilder, t_ctx: &'a mut TranslateCtx<'plan>, hash_join_op: &'a HashJoinOp, hash_ctx: HashCtx, select_plan: Option<&'plan SelectPlan>, table_index: usize, } impl<'a, 'plan> HashProbeCloseEmitter<'a, 'plan> { /// Capture the mutable close-loop inputs for a single hash probe table. pub(super) fn new( program: &'a mut ProgramBuilder, t_ctx: &'a mut TranslateCtx<'plan>, hash_join_op: &'a HashJoinOp, hash_ctx: HashCtx, select_plan: Option<&'plan SelectPlan>, table_index: usize, ) -> Self { Self { program, t_ctx, hash_join_op, hash_ctx, select_plan, table_index, } } /// Emit `HashNext` and loop back into the existing match-processing path. fn emit_matched_iteration(&mut self) -> Result { let hash_table_reg = self.hash_ctx.hash_table_reg; let match_reg = self.hash_ctx.match_reg; let match_found_label = self.hash_ctx.labels.match_found; let hash_next_label = self.hash_ctx.labels.next; let payload_dest_reg = self.hash_ctx.payload_start_reg; let num_payload = self.hash_ctx.payload_columns.len(); let check_outer_label = self.hash_ctx.labels.check_outer; let join_type = self.hash_ctx.join_type; let inner_loop_gosub_reg = self.hash_ctx.inner_loop_gosub_reg; let inner_loop_skip_label = self.hash_ctx.labels.inner_loop_skip; let label_next_probe_row = self.program.allocate_label(); let mut semi_anti_next_pc = None; if let Some(gosub_reg) = inner_loop_gosub_reg { semi_anti_next_pc = Some(self.program.offset()); self.program.emit_insn(Insn::Return { return_reg: gosub_reg, can_fallthrough: false, }); if let Some(skip_label) = inner_loop_skip_label { self.program.preassign_label_to_next_insn(skip_label); } } let hash_next_target = if join_type == HashJoinType::FullOuter { check_outer_label.unwrap_or(label_next_probe_row) } else { label_next_probe_row }; if semi_anti_next_pc.is_none() { semi_anti_next_pc = Some(self.program.offset()); } self.program .resolve_label(hash_next_label, self.program.offset()); // Grace dispatch: if grace_flag_reg > 0, jump to the grace loop's own // HashNext (which has a different miss target). This lets the inner body // be shared between the main probe loop and the grace loop. if let Some(grace_flag_reg) = self.hash_ctx.grace_flag_reg { let grace_hash_next_label = self.program.allocate_label(); // Store in hash_ctx for the grace loop emitter to resolve later. let build_table_idx = self.hash_join_op.build_table_idx; if let Some(ctx) = self.t_ctx.hash_table_contexts.get_mut(&build_table_idx) { ctx.labels.grace_hash_next = Some(grace_hash_next_label); } self.program.emit_insn(Insn::IfPos { reg: grace_flag_reg, target_pc: grace_hash_next_label, decrement_by: 0, }); } self.program.emit_insn(Insn::HashNext { hash_table_id: hash_table_reg, dest_reg: match_reg, target_pc: hash_next_target, payload_dest_reg, num_payload, }); self.program.emit_insn(Insn::Goto { target_pc: match_found_label, }); Ok(ProbeCloseState { label_next_probe_row, semi_anti_next_pc, }) } /// Emit FULL OUTER unmatched probe rows before advancing to the next probe row. fn emit_probe_miss_rows(&mut self, state: ProbeCloseState) -> Result { let ProbeCloseState { label_next_probe_row, semi_anti_next_pc, } = state; if matches!(self.hash_ctx.join_type, HashJoinType::FullOuter) { let probe_table_idx = self.hash_join_op.probe_table_idx; let lj_meta = self.t_ctx.meta_left_joins[probe_table_idx] .as_ref() .expect("FULL OUTER probe table must have left join metadata"); let reg_match_flag = lj_meta.reg_match_flag; if let Some(check_outer_label) = self.hash_ctx.labels.check_outer { self.program .resolve_label(check_outer_label, self.program.offset()); } self.program .resolve_label(lj_meta.label_match_flag_check_value, self.program.offset()); self.program.emit_insn(Insn::IfPos { reg: reg_match_flag, target_pc: label_next_probe_row, decrement_by: 0, }); if let Some(cursor_id) = self.hash_ctx.build_cursor_id { self.program.emit_insn(Insn::NullRow { cursor_id }); } if let Some(payload_reg) = self.hash_ctx.payload_start_reg { let num_payload = self.hash_ctx.payload_columns.len(); if num_payload > 0 { self.program.emit_insn(Insn::Null { dest: payload_reg, dest_end: Some(payload_reg + num_payload - 1), }); } } if let Some(plan) = self.select_plan { emit_unmatched_row_conditions_and_loop( self.program, self.t_ctx, plan, self.hash_join_op.build_table_idx, self.table_index, label_next_probe_row, self.hash_ctx .inner_loop_gosub_reg .zip(self.hash_ctx.labels.inner_loop_gosub), )?; } } Ok(ProbeCloseState { label_next_probe_row, semi_anti_next_pc, }) } /// Anchor the next probe-row label and return the close-loop control-flow state. fn finish(&mut self, state: ProbeCloseState) -> HashProbeCloseOutcome { let ProbeCloseState { label_next_probe_row, semi_anti_next_pc, } = state; self.program .preassign_label_to_next_insn(label_next_probe_row); HashProbeCloseOutcome { semi_anti_next_pc } } pub(super) fn emit(mut self) -> Result { let state = self.emit_matched_iteration()?; let state = self.emit_probe_miss_rows(state)?; Ok(self.finish(state)) } } /// Emit unmatched build rows after the probe cursor has been exhausted. pub(super) fn emit_hash_join_unmatched_build_rows<'a>( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx<'a>, hash_join_op: &HashJoinOp, hash_ctx: &HashCtx, select_plan: Option<&'a SelectPlan>, table_index: usize, probe_cursor_id: CursorID, ) -> Result<()> { if !matches!( hash_join_op.join_type, HashJoinType::LeftOuter | HashJoinType::FullOuter ) { return Ok(()); } let Some(plan) = select_plan else { return Ok(()); }; let hash_table_reg = hash_ctx.hash_table_reg; let match_reg = hash_ctx.match_reg; let payload_dest_reg = hash_ctx.payload_start_reg; let num_payload = hash_ctx.payload_columns.len(); let build_cursor_id = hash_ctx.build_cursor_id; let done_unmatched = program.allocate_label(); program.emit_insn(Insn::NullRow { cursor_id: probe_cursor_id, }); program.emit_insn(Insn::HashScanUnmatched { hash_table_id: hash_table_reg, dest_reg: match_reg, target_pc: done_unmatched, payload_dest_reg, num_payload, }); let unmatched_loop = program.allocate_label(); let label_next_unmatched = program.allocate_label(); program.preassign_label_to_next_insn(unmatched_loop); if let Some(cursor_id) = build_cursor_id { program.emit_insn(Insn::SeekRowid { cursor_id, src_reg: match_reg, target_pc: done_unmatched, }); } emit_unmatched_row_conditions_and_loop( program, t_ctx, plan, hash_join_op.build_table_idx, table_index, label_next_unmatched, hash_ctx .inner_loop_gosub_reg .zip(hash_ctx.labels.inner_loop_gosub), )?; program.resolve_label(label_next_unmatched, program.offset()); program.emit_insn(Insn::HashNextUnmatched { hash_table_id: hash_table_reg, dest_reg: match_reg, target_pc: done_unmatched, payload_dest_reg, num_payload, }); program.emit_insn(Insn::Goto { target_pc: unmatched_loop, }); program.preassign_label_to_next_insn(done_unmatched); Ok(()) } /// Grace Hash Join processing loop after the probe cursor is exhausted. pub(crate) struct GraceHashLoop; impl GraceHashLoop { /// Emit VDBE-driven grace hash join processing loop. /// Uses the shared inner body via `Goto match_found_label` and `grace_flag_reg` /// dispatch so that aggregates, LIMIT, ORDER BY, etc. all work naturally. /// At runtime, HashGraceInit is a no-op if the build side didn't spill. pub fn emit<'a>( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx<'a>, hash_join_op: &HashJoinOp, hash_ctx: &HashCtx, select_plan: Option<&'a SelectPlan>, table_index: usize, probe_cursor_id: CursorID, ) -> Result<()> { // Need grace_flag_reg + probe_rowid_reg for grace processing let Some(probe_rowid_reg) = hash_ctx.probe_rowid_reg else { return Ok(()); }; let Some(grace_flag_reg) = hash_ctx.grace_flag_reg else { return Ok(()); }; let hash_table_reg = hash_ctx.hash_table_reg; let match_reg = hash_ctx.match_reg; let match_found_label = hash_ctx.labels.match_found; let payload_dest_reg = hash_ctx.payload_start_reg; let num_payload = hash_ctx.payload_columns.len(); let is_full_outer = hash_join_op.join_type == HashJoinType::FullOuter; let grace_done = program.allocate_label(); let grace_partition_top = program.allocate_label(); let grace_probe_top = program.allocate_label(); let grace_advance = program.allocate_label(); let grace_cleanup = program.allocate_label(); // HashGraceInit: finalize probe spill + grace_begin program.emit_insn(Insn::HashGraceInit { hash_table_id: to_u16(hash_table_reg), target_pc: grace_done, }); // Set grace mode flag = 1 program.emit_insn(Insn::Integer { value: 1, dest: grace_flag_reg, }); // grace_partition_top: load build partition + first probe chunk program.preassign_label_to_next_insn(grace_partition_top); program.emit_insn(Insn::HashGraceLoadPartition { hash_table_id: to_u16(hash_table_reg), target_pc: grace_cleanup, }); // grace_probe_top: get next probe entry (writes keys + rowid to registers) program.preassign_label_to_next_insn(grace_probe_top); // FULL OUTER: reset match flag before each probe entry so we can detect misses if is_full_outer { let probe_table_idx = hash_join_op.probe_table_idx; if let Some(lj_meta) = t_ctx.meta_left_joins[probe_table_idx].as_ref() { program.emit_insn(Insn::Integer { value: 0, dest: lj_meta.reg_match_flag, }); } } program.emit_insn(Insn::HashGraceNextProbe { hash_table_id: to_u16(hash_table_reg), key_start_reg: to_u16(hash_ctx.key_start_reg), num_keys: to_u16(hash_ctx.num_keys), probe_rowid_dest: to_u16(probe_rowid_reg), target_pc: grace_advance, }); // Re-position probe cursor via SeekRowid program.emit_insn(Insn::SeekRowid { cursor_id: probe_cursor_id, src_reg: probe_rowid_reg, target_pc: grace_probe_top, }); // For FULL OUTER, HashProbe miss needs to go to the outer-check path // (emit unmatched probe row with NULL build columns). // For INNER/LEFT OUTER, miss just advances to next probe entry. let grace_outer_check = if is_full_outer { program.allocate_label() } else { grace_probe_top }; // HashProbe the loaded build partition with the probe keys program.emit_insn(Insn::HashProbe { hash_table_id: to_u16(hash_table_reg), key_start_reg: to_u16(hash_ctx.key_start_reg), num_keys: to_u16(hash_ctx.num_keys), dest_reg: to_u16(match_reg), target_pc: grace_outer_check, payload_dest_reg: payload_dest_reg.map(to_u16), num_payload: to_u16(num_payload), probe_rowid_reg: None, // grace-only: HashGraceLoadPartition already loaded this partition }); // Jump INTO the shared inner body (conditions, result columns, aggregation). // The IfPos dispatch before the main loop's HashNext will route back here. program.emit_insn(Insn::Goto { target_pc: match_found_label, }); // grace_hash_next: the grace loop's own HashNext, reached via IfPos dispatch // from the shared body. if let Some(grace_hash_next_label) = hash_ctx.labels.grace_hash_next { program.resolve_label(grace_hash_next_label, program.offset()); } // For FULL OUTER, HashNext miss goes to outer check (unmatched probe row). // For INNER/LEFT OUTER, miss advances to next probe entry. program.emit_insn(Insn::HashNext { hash_table_id: hash_table_reg, dest_reg: match_reg, target_pc: grace_outer_check, payload_dest_reg, num_payload, }); // Another match found, loop back to shared body program.emit_insn(Insn::Goto { target_pc: match_found_label, }); // FULL OUTER: unmatched probe row path. // If match_flag is still 0, emit the probe row with NULL build columns. if is_full_outer { program.resolve_label(grace_outer_check, program.offset()); let probe_table_idx = hash_join_op.probe_table_idx; if let Some(lj_meta) = t_ctx.meta_left_joins[probe_table_idx].as_ref() { // If match_flag > 0, a match was found, skip to next probe entry program.emit_insn(Insn::IfPos { reg: lj_meta.reg_match_flag, target_pc: grace_probe_top, decrement_by: 0, }); } // Set build cursor to NULL row if let Some(cursor_id) = hash_ctx.build_cursor_id { program.emit_insn(Insn::NullRow { cursor_id }); } // NULL out payload registers if let Some(payload_reg) = hash_ctx.payload_start_reg { if num_payload > 0 { program.emit_insn(Insn::Null { dest: payload_reg, dest_end: Some(payload_reg + num_payload - 1), }); } } // Emit the unmatched row through the shared body if let Some(plan) = select_plan { emit_unmatched_row_conditions_and_loop( program, t_ctx, plan, hash_join_op.build_table_idx, table_index, grace_probe_top, hash_ctx .inner_loop_gosub_reg .zip(hash_ctx.labels.inner_loop_gosub), )?; } // Advance to next probe entry program.emit_insn(Insn::Goto { target_pc: grace_probe_top, }); } // grace_advance: probe entries exhausted for this partition. program.resolve_label(grace_advance, program.offset()); // LEFT/FULL OUTER: emit unmatched build rows for this partition BEFORE evicting. // After eviction, matched_bits are lost, so the global unmatched scan can't // see which build rows were matched during grace probing. if matches!( hash_join_op.join_type, HashJoinType::LeftOuter | HashJoinType::FullOuter ) { if let Some(plan) = select_plan { let done_grace_unmatched = program.allocate_label(); let grace_unmatched_loop = program.allocate_label(); let grace_next_unmatched = program.allocate_label(); // Set probe cursor to NULL row (unmatched build rows have no probe match) program.emit_insn(Insn::NullRow { cursor_id: probe_cursor_id, }); program.emit_insn(Insn::HashScanUnmatched { hash_table_id: hash_table_reg, dest_reg: match_reg, target_pc: done_grace_unmatched, payload_dest_reg, num_payload, }); program.preassign_label_to_next_insn(grace_unmatched_loop); if let Some(cursor_id) = hash_ctx.build_cursor_id { program.emit_insn(Insn::SeekRowid { cursor_id, src_reg: match_reg, target_pc: done_grace_unmatched, }); } emit_unmatched_row_conditions_and_loop( program, t_ctx, plan, hash_join_op.build_table_idx, table_index, grace_next_unmatched, hash_ctx .inner_loop_gosub_reg .zip(hash_ctx.labels.inner_loop_gosub), )?; program.resolve_label(grace_next_unmatched, program.offset()); program.emit_insn(Insn::HashNextUnmatched { hash_table_id: hash_table_reg, dest_reg: match_reg, target_pc: done_grace_unmatched, payload_dest_reg, num_payload, }); program.emit_insn(Insn::Goto { target_pc: grace_unmatched_loop, }); program.preassign_label_to_next_insn(done_grace_unmatched); } } // Evict current partition, advance to next program.emit_insn(Insn::HashGraceAdvancePartition { hash_table_id: to_u16(hash_table_reg), target_pc: grace_cleanup, }); program.emit_insn(Insn::Goto { target_pc: grace_partition_top, }); // grace_cleanup: clear grace mode flag program.resolve_label(grace_cleanup, program.offset()); program.emit_insn(Insn::Integer { value: 0, dest: grace_flag_reg, }); // grace_done program.preassign_label_to_next_insn(grace_done); Ok(()) } } ================================================ FILE: core/translate/main_loop/in_seek.rs ================================================ use super::*; /// Open or reuse the ephemeral cursor that supplies RHS values for an IN-seek. /// /// Literal lists are materialized once into a unique ephemeral index so both /// ordinary `Search::InSeek` and multi-index OR branches can drive repeated /// equality seeks from the same bytecode pattern. IN-subqueries already have an /// ephemeral cursor from subquery translation, so they are reused directly. pub(super) fn open_in_seek_source_cursor( program: &mut ProgramBuilder, table_references: &TableReferences, resolver: &Resolver<'_>, index: Option<&Arc>, source: &InSeekSource, ) -> Result { match source { InSeekSource::LiteralList { values, affinity } => { let label_once_end = program.allocate_label(); program.emit_insn(Insn::Once { target_pc_when_reentered: label_once_end, }); let collation = index .as_ref() .and_then(|idx| idx.columns.first()) .and_then(|c| c.collation); let ephemeral_index = Arc::new(Index { name: String::new(), table_name: String::new(), root_page: 0, columns: vec![IndexColumn { name: String::new(), order: SortOrder::Asc, pos_in_table: 0, collation, default: None, expr: None, }], unique: true, ephemeral: true, has_rowid: false, where_clause: None, index_method: None, on_conflict: None, }); let eph_cursor = program.alloc_cursor_id(CursorType::BTreeIndex(ephemeral_index)); program.emit_insn(Insn::OpenEphemeral { cursor_id: eph_cursor, is_table: false, }); let val_reg = program.alloc_register(); let record_reg = program.alloc_register(); let affinity_str = affinity.aff_mask().to_string(); for value in values.iter() { translate_expr_no_constant_opt( program, Some(table_references), value, val_reg, resolver, NoConstantOptReason::InListEphemeral, )?; program.emit_insn(Insn::MakeRecord { start_reg: to_u16(val_reg), count: 1, dest_reg: to_u16(record_reg), index_name: None, affinity_str: Some(affinity_str.clone()), }); program.emit_insn(Insn::IdxInsert { cursor_id: eph_cursor, record_reg, unpacked_start: Some(val_reg), unpacked_count: Some(1), flags: IdxInsertFlags::new().no_op_duplicate(), }); } program.preassign_label_to_next_insn(label_once_end); Ok(eph_cursor) } InSeekSource::Subquery { cursor_id } => Ok(*cursor_id), } } ================================================ FILE: core/translate/main_loop/init.rs ================================================ use super::*; pub fn init_distinct(program: &mut ProgramBuilder, plan: &SelectPlan) -> Result { let collations = plan .result_columns .iter() .map(|col| { get_collseq_from_expr(&col.expr, &plan.table_references) .map(|c| c.unwrap_or(CollationSeq::Binary)) }) .collect::>>()?; let hash_table_id = program.alloc_hash_table_id(); let ctx = DistinctCtx { hash_table_id, collations, label_on_conflict: program.allocate_label(), }; Ok(ctx) } /// First step of Loop emission, opens cursors for all tables and initializes distinct aggregate /// hash tables. Also emits condition checks for any WHERE clause terms that need to be evaluated /// before the loop (e.g. those that reference only tables that are on the outermost level of the /// join order). pub struct InitLoop; impl InitLoop { #[allow(clippy::too_many_arguments)] pub fn emit<'a>( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx<'a>, tables: &TableReferences, aggregates: &mut [Aggregate], mode: &OperationMode, where_clause: &[WhereTerm], join_order: &[JoinOrderMember], subqueries: &mut [NonFromClauseSubquery], ) -> Result<()> { turso_assert_eq!( t_ctx.meta_left_joins.len(), tables.joined_tables().len(), "meta_left_joins length must match tables length" ); if matches!( &mode, OperationMode::INSERT | OperationMode::UPDATE { .. } | OperationMode::DELETE ) { turso_assert_eq!(tables.joined_tables().len(), 1); let changed_table = &tables.joined_tables()[0].table; let prepared = prepare_cdc_if_necessary( program, t_ctx.resolver.schema(), changed_table.get_name(), )?; if let Some((cdc_cursor_id, _)) = prepared { t_ctx.cdc_cursor_id = Some(cdc_cursor_id); } } // Initialize distinct aggregates using hash tables for agg in aggregates.iter_mut().filter(|agg| agg.is_distinct()) { turso_assert_eq!( agg.args.len(), 1, "DISTINCT aggregate functions must have exactly one argument" ); let collations = vec![get_collseq_from_expr(&agg.original_expr, tables)? .unwrap_or(CollationSeq::Binary)]; let hash_table_id = program.alloc_hash_table_id(); agg.distinctness = Distinctness::Distinct { ctx: Some(DistinctCtx { hash_table_id, collations, label_on_conflict: program.allocate_label(), }), }; // DISTINCT aggregate hash tables live in ProgramState, so a correlated // subquery can re-enter with rows from the previous invocation still // recorded unless we clear the seen-set here. program.emit_insn(Insn::HashClear { hash_table_id }); emit_explain!( program, false, format!("USE HASH TABLE FOR {}(DISTINCT)", agg.func) ); } // Include hash-join build tables so their cursors are opened for hash build. let mut required_tables: HashSet = join_order .iter() .map(|member| member.original_idx) .collect(); for table in tables.joined_tables().iter() { if let Operation::HashJoin(hash_join_op) = &table.op { required_tables.insert(hash_join_op.build_table_idx); } } for (table_index, table) in tables.joined_tables().iter().enumerate() { if !required_tables.contains(&table_index) { continue; } // Ensure attached databases have a Transaction instruction for read access if crate::is_attached_db(table.database_id) { let schema_cookie = t_ctx .resolver .with_schema(table.database_id, |s| s.schema_version); program.begin_read_on_database(table.database_id, schema_cookie); } // Initialize bookkeeping for OUTER JOIN if let Some(join_info) = table.join_info.as_ref() { if join_info.is_outer() { let lj_metadata = LeftJoinMetadata { reg_match_flag: program.alloc_register(), label_match_flag_set_true: program.allocate_label(), label_match_flag_check_value: program.allocate_label(), }; t_ctx.meta_left_joins[table_index] = Some(lj_metadata); } if join_info.is_semi_or_anti() { let join_idx = join_order .iter() .position(|m| m.original_idx == table_index) .expect("table must be in join_order"); let outer_table_idx = find_non_semi_anti_ancestor(join_order, tables.joined_tables(), join_idx); // For hash join probe tables, loop_labels.next points to the probe // cursor's Next (which advances to the next outer row), but we need // to jump to the HashNext (which advances to the next hash match // for the current outer row). We allocate a fresh label here and // resolve it in close_loop at the right point. let sa_metadata = SemiAntiJoinMetadata { label_body: program.allocate_label(), label_next_outer: program.allocate_label(), outer_table_idx, }; t_ctx.meta_semi_anti_joins[table_index] = Some(sa_metadata); } } let (table_cursor_id, index_cursor_id) = table.open_cursors(program, mode.clone(), t_ctx.resolver.schema())?; match &table.op { Operation::Scan(Scan::BTreeTable { index, .. }) => match (&mode, &table.table) { (OperationMode::SELECT, Table::BTree(btree)) => { let root_page = btree.root_page; if let Some(cursor_id) = table_cursor_id { program.emit_insn(Insn::OpenRead { cursor_id, root_page, db: table.database_id, }); } if let Some(index_cursor_id) = index_cursor_id { program.emit_insn(Insn::OpenRead { cursor_id: index_cursor_id, root_page: index.as_ref().unwrap().root_page, db: table.database_id, }); } } (OperationMode::DELETE, Table::BTree(btree)) => { let root_page = btree.root_page; program.emit_insn(Insn::OpenWrite { cursor_id: table_cursor_id .expect("table cursor is always opened in OperationMode::DELETE"), root_page: root_page.into(), db: table.database_id, }); if let Some(index_cursor_id) = index_cursor_id { program.emit_insn(Insn::OpenWrite { cursor_id: index_cursor_id, root_page: index.as_ref().unwrap().root_page.into(), db: table.database_id, }); } // For delete, we need to open all the other indexes too for writing let indices: Vec<_> = t_ctx.resolver.with_schema(table.database_id, |s| { s.get_indices(table.table.get_name()).cloned().collect() }); for index in &indices { if table .op .index() .is_some_and(|table_index| table_index.name == index.name) { continue; } let cursor_id = program.alloc_cursor_index( Some(CursorKey::index(table.internal_id, index.clone())), index, )?; program.emit_insn(Insn::OpenWrite { cursor_id, root_page: index.root_page.into(), db: table.database_id, }); } } (OperationMode::UPDATE(update_mode), Table::BTree(btree)) => { let root_page = btree.root_page; match &update_mode { UpdateRowSource::Normal => { program.emit_insn(Insn::OpenWrite { cursor_id: table_cursor_id.expect( "table cursor is always opened in OperationMode::UPDATE", ), root_page: root_page.into(), db: table.database_id, }); } UpdateRowSource::PrebuiltEphemeralTable { target_table, .. } => { let target_table_cursor_id = program .resolve_cursor_id(&CursorKey::table(target_table.internal_id)); program.emit_insn(Insn::OpenWrite { cursor_id: target_table_cursor_id, root_page: target_table.btree().unwrap().root_page.into(), db: target_table.database_id, }); } } let write_db_id = match &update_mode { UpdateRowSource::PrebuiltEphemeralTable { target_table, .. } => { target_table.database_id } _ => table.database_id, }; if let Some(index_cursor_id) = index_cursor_id { program.emit_insn(Insn::OpenWrite { cursor_id: index_cursor_id, root_page: index.as_ref().unwrap().root_page.into(), db: write_db_id, }); } } _ => {} }, Operation::Scan(Scan::VirtualTable { .. }) => { if let Table::Virtual(tbl) = &table.table { let is_write = matches!( mode, OperationMode::INSERT | OperationMode::UPDATE { .. } | OperationMode::DELETE ); let allow_dbpage_write = { #[cfg(feature = "cli_only")] { t_ctx.unsafe_testing && tbl.name == crate::dbpage::DBPAGE_TABLE_NAME } #[cfg(not(feature = "cli_only"))] { false } }; if is_write && tbl.readonly() && !allow_dbpage_write { return Err(crate::LimboError::ReadOnly); } if let Some(cursor_id) = table_cursor_id { program.emit_insn(Insn::VOpen { cursor_id }); if is_write && !allow_dbpage_write { program.emit_insn(Insn::VBegin { cursor_id }); } } } } Operation::Scan(_) => {} Operation::Search(search) => { match mode { OperationMode::SELECT => { if let Some(table_cursor_id) = table_cursor_id { program.emit_insn(Insn::OpenRead { cursor_id: table_cursor_id, root_page: table.table.get_root_page()?, db: table.database_id, }); } } OperationMode::DELETE | OperationMode::UPDATE { .. } => { let table_cursor_id = table_cursor_id.expect( "table cursor is always opened in OperationMode::DELETE or OperationMode::UPDATE", ); program.emit_insn(Insn::OpenWrite { cursor_id: table_cursor_id, root_page: table.table.get_root_page()?.into(), db: table.database_id, }); // For DELETE, we need to open all the indexes for writing // UPDATE opens these in emit_program_for_update() separately if matches!(mode, OperationMode::DELETE) { let indices: Vec<_> = t_ctx.resolver.with_schema(table.database_id, |s| { s.get_indices(table.table.get_name()).cloned().collect() }); for index in &indices { if table .op .index() .is_some_and(|table_index| table_index.name == index.name) { continue; } let cursor_id = program.alloc_cursor_index( Some(CursorKey::index(table.internal_id, index.clone())), index, )?; program.emit_insn(Insn::OpenWrite { cursor_id, root_page: index.root_page.into(), db: table.database_id, }); } } } _ => { return Err(crate::LimboError::InternalError( "INSERT mode is not supported for Search operations".to_string(), )); } } let search_index = match search { Search::Seek { index: Some(index), .. } | Search::InSeek { index: Some(index), .. } => Some(index), _ => None, }; if let Some(index) = search_index { // Ephemeral index cursor are opened ad-hoc when needed. if !index.ephemeral { match mode { OperationMode::SELECT => { program.emit_insn(Insn::OpenRead { cursor_id: index_cursor_id.expect( "index cursor is always opened in Seek with index", ), root_page: index.root_page, db: table.database_id, }); } OperationMode::UPDATE { .. } | OperationMode::DELETE => { program.emit_insn(Insn::OpenWrite { cursor_id: index_cursor_id.expect( "index cursor is always opened in Seek with index", ), root_page: index.root_page.into(), db: table.database_id, }); } _ => { return Err(crate::LimboError::InternalError( "INSERT mode is not supported for indexed Search operations" .to_string(), )); } } } } } Operation::IndexMethodQuery(_) => match mode { OperationMode::SELECT => { if let Some(table_cursor_id) = table_cursor_id { program.emit_insn(Insn::OpenRead { cursor_id: table_cursor_id, root_page: table.table.get_root_page()?, db: table.database_id, }); } let index_cursor_id = index_cursor_id.unwrap(); program.emit_insn(Insn::OpenRead { cursor_id: index_cursor_id, root_page: table.op.index().unwrap().root_page, db: table.database_id, }); } OperationMode::DELETE => { if let Some(table_cursor_id) = table_cursor_id { program.emit_insn(Insn::OpenWrite { cursor_id: table_cursor_id, root_page: table.table.get_root_page()?.into(), db: table.database_id, }); } let index_cursor_id = index_cursor_id.expect("index cursor is always opened in OperationMode::DELETE for IndexMethodQuery"); program.emit_insn(Insn::OpenWrite { cursor_id: index_cursor_id, root_page: table.op.index().expect("index to exist").root_page.into(), db: table.database_id, }); let indices: Vec<_> = t_ctx.resolver.with_schema(table.database_id, |s| { s.get_indices(table.table.get_name()).cloned().collect() }); for index in &indices { if table .op .index() .is_some_and(|table_index| table_index.name == index.name) { continue; } let cursor_id = program.alloc_cursor_index( Some(CursorKey::index(table.internal_id, index.clone())), index, )?; program.emit_insn(Insn::OpenWrite { cursor_id, root_page: index.root_page.into(), db: table.database_id, }); } } OperationMode::UPDATE { .. } => { let table_cursor_id = table_cursor_id.expect( "table cursor is always opened in OperationMode::UPDATE for IndexMethodQuery", ); program.emit_insn(Insn::OpenWrite { cursor_id: table_cursor_id, root_page: table.table.get_root_page()?.into(), db: table.database_id, }); let index_cursor_id = index_cursor_id.unwrap(); program.emit_insn(Insn::OpenWrite { cursor_id: index_cursor_id, root_page: table.op.index().expect("index to exist").root_page.into(), db: table.database_id, }); } _ => panic!("Unsupported operation mode for index method"), }, Operation::HashJoin(_) => { match mode { OperationMode::SELECT => { // Open probe table cursor, the build table cursor should already be open from a previous iteration. if let Some(table_cursor_id) = table_cursor_id { let Table::BTree(btree) = &table.table else { panic!("Expected hash join probe table to be a BTree table"); }; program.emit_insn(Insn::OpenRead { cursor_id: table_cursor_id, root_page: btree.root_page, db: table.database_id, }); } } _ => unreachable!("Hash joins should only occur in SELECT operations"), } } Operation::MultiIndexScan(multi_idx_op) => { match mode { OperationMode::SELECT => { let Table::BTree(btree) = &table.table else { panic!("Expected multi-index scan table to be a BTree table"); }; // Open the table cursor if let Some(table_cursor_id) = table_cursor_id { program.emit_insn(Insn::OpenRead { cursor_id: table_cursor_id, root_page: btree.root_page, db: table.database_id, }); } // Open cursors for each index branch for branch in &multi_idx_op.branches { if let Some(index) = &branch.index { let branch_cursor_id = program.alloc_cursor_index( Some(CursorKey::index(table.internal_id, index.clone())), index, )?; program.emit_insn(Insn::OpenRead { cursor_id: branch_cursor_id, root_page: index.root_page, db: table.database_id, }); } } } _ => { unreachable!("Multi-index scans should only occur in SELECT operations") } } } } } for cond in where_clause .iter() .filter(|c| c.should_eval_before_loop(join_order, subqueries, Some(tables))) { let jump_target = program.allocate_label(); let meta = ConditionMetadata { jump_if_condition_is_true: false, jump_target_when_true: jump_target, jump_target_when_false: t_ctx.label_main_loop_end.expect( "main_loop_end label should be set before emitting condition expressions", ), jump_target_when_null: t_ctx.label_main_loop_end.expect( "main_loop_end label should be set before emitting condition expressions", ), }; translate_condition_expr(program, tables, &cond.expr, meta, &t_ctx.resolver)?; program.preassign_label_to_next_insn(jump_target); } Ok(()) } } ================================================ FILE: core/translate/main_loop/mod.rs ================================================ use turso_parser::ast::{Expr, SortOrder, TableInternalId}; use super::{ aggregation::{translate_aggregation_step, AggArgumentSource}, emitter::{ InSeekMetadata, MaterializedBuildInputMode, MaterializedColumnRef, OperationMode, Resolver, TranslateCtx, UpdateRowSource, }, expr::{ expr_references_subquery_id, translate_condition_expr, translate_expr, translate_expr_no_constant_opt, walk_expr, ConditionMetadata, NoConstantOptReason, WalkControl, }, group_by::{group_by_agg_phase, GroupByMetadata, GroupByRowSource}, optimizer::{constraints::BinaryExprSide, Optimizable}, order_by::sorter_insert, plan::{ Aggregate, DistinctCtx, Distinctness, EvalAt, HashJoinOp, HashJoinType, InSeekSource, IterationDirection, JoinOrderMember, JoinedTable, MultiIndexScanOp, NonFromClauseSubquery, Operation, QueryDestination, Scan, Search, SeekDef, SeekKey, SeekKeyComponent, SelectPlan, SetOperation, TableReferences, WhereTerm, }, }; use crate::{ emit_explain, schema::{Index, IndexColumn, Table}, translate::{ collate::{get_collseq_from_expr, resolve_comparison_collseq, CollationSeq}, emitter::{prepare_cdc_if_necessary, HashCtx}, expr::comparison_affinity, planner::{table_mask_from_expr, TableMask}, result_row::emit_select_result, }, turso_assert, turso_assert_eq, types::SeekOp, util::expr_tables_subset_of, vdbe::{ affinity::{self, Affinity}, builder::{ CursorKey, CursorType, HashBuildSignature, MaterializedBuildInputModeTag, ProgramBuilder, }, insn::{to_u16, CmpInsFlags, HashBuildData, IdxInsertFlags, Insn}, BranchOffset, CursorID, }, Result, }; use std::{borrow::Cow, collections::HashSet, sync::Arc}; use turso_macros::turso_assert_some; mod body; mod close; mod conditions; mod hash; mod in_seek; mod init; mod multi_index; mod open; mod seek; use body::emit_unmatched_row_conditions_and_loop; pub(crate) use body::LoopBodyEmitter; pub(crate) use close::CloseLoop; use close::{emit_autoindex, AutoIndexResult}; use in_seek::open_in_seek_source_cursor; pub(crate) use init::{init_distinct, InitLoop}; use multi_index::emit_multi_index_scan_loop; pub(crate) use open::OpenLoop; use seek::SeekEmitter; #[derive(Debug)] pub struct LeftJoinMetadata { pub reg_match_flag: usize, pub label_match_flag_set_true: BranchOffset, pub label_match_flag_check_value: BranchOffset, } #[derive(Debug)] pub struct SemiAntiJoinMetadata { pub label_body: BranchOffset, pub label_next_outer: BranchOffset, pub outer_table_idx: usize, } #[derive(Debug, Clone, Copy)] pub struct LoopLabels { pub loop_start: BranchOffset, pub next: BranchOffset, pub loop_end: BranchOffset, } impl LoopLabels { pub fn new(program: &mut ProgramBuilder) -> Self { Self { loop_start: program.allocate_label(), next: program.allocate_label(), loop_end: program.allocate_label(), } } } fn find_non_semi_anti_ancestor( join_order: &[JoinOrderMember], tables: &[JoinedTable], join_idx: usize, ) -> usize { assert!(join_idx > 0, "semi/anti-join cannot be the first table"); let mut idx = join_idx - 1; while idx > 0 { let prev = &tables[join_order[idx].original_idx]; if !prev .join_info .as_ref() .is_some_and(|ji| ji.is_semi_or_anti()) { break; } idx -= 1; } join_order[idx].original_idx } ================================================ FILE: core/translate/main_loop/multi_index.rs ================================================ use super::*; use crate::translate::plan::{MultiIndexBranch, MultiIndexBranchAccess}; #[expect(clippy::too_many_arguments)] fn emit_multi_index_rowset_update( program: &mut ProgramBuilder, is_intersection: bool, branch_idx: usize, rowid_reg: usize, rowset1_reg: usize, current_read_rowset: usize, current_write_rowset: usize, skip_row_label: BranchOffset, found_in_prev_label: Option, ) { if is_intersection { if branch_idx == 0 { program.emit_insn(Insn::RowSetAdd { rowset_reg: rowset1_reg, value_reg: rowid_reg, }); } else { program.emit_insn(Insn::RowSetTest { rowset_reg: current_read_rowset, pc_if_found: found_in_prev_label .expect("intersection branch must have found label"), value_reg: rowid_reg, batch: -1, }); program.emit_insn(Insn::Goto { target_pc: skip_row_label, }); program.preassign_label_to_next_insn( found_in_prev_label.expect("intersection branch must have found label"), ); program.emit_insn(Insn::RowSetAdd { rowset_reg: current_write_rowset, value_reg: rowid_reg, }); } } else { program.emit_insn(Insn::RowSetAdd { rowset_reg: rowset1_reg, value_reg: rowid_reg, }); } } #[expect(clippy::too_many_arguments)] fn emit_multi_index_or_residual_filters( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, table_references: &TableReferences, residual_exprs: &[Expr], jump_target: BranchOffset, index_cursor_id: Option, table_cursor_id: CursorID, requires_table_cursor: bool, ) -> Result<()> { if residual_exprs.is_empty() { return Ok(()); } if requires_table_cursor { if let Some(index_cursor_id) = index_cursor_id { program.emit_insn(Insn::DeferredSeek { index_cursor_id, table_cursor_id, }); } } for residual_expr in residual_exprs { let jump_target_when_true = program.allocate_label(); let condition_metadata = ConditionMetadata { jump_if_condition_is_true: false, jump_target_when_true, jump_target_when_false: jump_target, jump_target_when_null: jump_target, }; translate_condition_expr( program, table_references, residual_expr, condition_metadata, &t_ctx.resolver, )?; program.preassign_label_to_next_insn(jump_target_when_true); } Ok(()) } #[allow(clippy::too_many_arguments)] fn emit_seek_multi_index_branch( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, table: &JoinedTable, table_references: &TableReferences, branch: &MultiIndexBranch, table_cursor_id: CursorID, rowid_reg: usize, is_intersection: bool, branch_idx: usize, rowset1_reg: usize, current_read_rowset: usize, current_write_rowset: usize, found_in_prev_label: Option, ) -> Result<()> { let MultiIndexBranchAccess::Seek { seek_def } = &branch.access else { unreachable!("seek branch helper called for non-seek branch"); }; let branch_loop_start = program.allocate_label(); let branch_loop_end = program.allocate_label(); let branch_next = program.allocate_label(); let is_index = branch.index.is_some(); let branch_cursor_id = if let Some(index) = &branch.index { program.resolve_cursor_id(&CursorKey::index(table.internal_id, index.clone())) } else { table_cursor_id }; if let Some(r) = &branch.union_residuals { emit_multi_index_or_residual_filters( program, t_ctx, table_references, &r.pre_filter_exprs, branch_loop_end, None, table_cursor_id, false, )?; } let max_key_regs = seek_def .size(&seek_def.start) .max(seek_def.size(&seek_def.end)) .max(1); let key_start_reg = program.alloc_registers(max_key_regs); SeekEmitter::new( program, table_references, seek_def, t_ctx, branch_cursor_id, key_start_reg, branch_loop_end, branch.index.as_ref(), ) .emit(branch_loop_start, false)?; if is_index { program.emit_insn(Insn::IdxRowId { cursor_id: branch_cursor_id, dest: rowid_reg, }); } else { program.emit_insn(Insn::RowId { cursor_id: branch_cursor_id, dest: rowid_reg, }); } if let Some(r) = &branch.union_residuals { emit_multi_index_or_residual_filters( program, t_ctx, table_references, &r.post_filter_exprs, branch_next, is_index.then_some(branch_cursor_id), table_cursor_id, r.requires_table_cursor, )?; } emit_multi_index_rowset_update( program, is_intersection, branch_idx, rowid_reg, rowset1_reg, current_read_rowset, current_write_rowset, branch_next, found_in_prev_label, ); program.preassign_label_to_next_insn(branch_next); match seek_def.iter_dir { IterationDirection::Forwards => program.emit_insn(Insn::Next { cursor_id: branch_cursor_id, pc_if_next: branch_loop_start, }), IterationDirection::Backwards => program.emit_insn(Insn::Prev { cursor_id: branch_cursor_id, pc_if_prev: branch_loop_start, }), } program.preassign_label_to_next_insn(branch_loop_end); Ok(()) } #[allow(clippy::too_many_arguments)] fn emit_in_seek_multi_index_branch( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, table: &JoinedTable, table_references: &TableReferences, branch: &MultiIndexBranch, table_cursor_id: CursorID, rowid_reg: usize, is_intersection: bool, branch_idx: usize, rowset1_reg: usize, current_read_rowset: usize, current_write_rowset: usize, found_in_prev_label: Option, ) -> Result<()> { let MultiIndexBranchAccess::InSeek { source } = &branch.access else { unreachable!("IN-seek branch helper called for non-IN branch"); }; let branch_cursor_id = branch.index.as_ref().map(|index| { program.resolve_cursor_id(&CursorKey::index(table.internal_id, index.clone())) }); let ephemeral_cursor_id = open_in_seek_source_cursor( program, table_references, &t_ctx.resolver, branch.index.as_ref(), source, )?; let branch_loop_end = program.allocate_label(); if let Some(r) = &branch.union_residuals { emit_multi_index_or_residual_filters( program, t_ctx, table_references, &r.pre_filter_exprs, branch_loop_end, None, table_cursor_id, false, )?; } program.emit_insn(Insn::NullRow { cursor_id: ephemeral_cursor_id, }); program.emit_insn(Insn::Rewind { cursor_id: ephemeral_cursor_id, pc_if_empty: branch_loop_end, }); let outer_loop_start = program.allocate_label(); program.preassign_label_to_next_insn(outer_loop_start); let seek_reg = program.alloc_register(); program.emit_insn(Insn::Column { cursor_id: ephemeral_cursor_id, column: 0, dest: seek_reg, default: None, }); let next_value_label = program.allocate_label(); program.emit_insn(Insn::IsNull { reg: seek_reg, target_pc: next_value_label, }); if let Some(branch_cursor_id) = branch_cursor_id { let branch_loop_start = program.allocate_label(); let branch_next = program.allocate_label(); program.emit_insn(Insn::SeekGE { cursor_id: branch_cursor_id, start_reg: seek_reg, num_regs: 1, target_pc: next_value_label, is_index: true, eq_only: false, }); program.preassign_label_to_next_insn(branch_loop_start); program.emit_insn(Insn::IdxGT { cursor_id: branch_cursor_id, start_reg: seek_reg, num_regs: 1, target_pc: next_value_label, }); program.emit_insn(Insn::IdxRowId { cursor_id: branch_cursor_id, dest: rowid_reg, }); if let Some(r) = &branch.union_residuals { emit_multi_index_or_residual_filters( program, t_ctx, table_references, &r.post_filter_exprs, branch_next, Some(branch_cursor_id), table_cursor_id, r.requires_table_cursor, )?; } emit_multi_index_rowset_update( program, is_intersection, branch_idx, rowid_reg, rowset1_reg, current_read_rowset, current_write_rowset, branch_next, found_in_prev_label, ); program.preassign_label_to_next_insn(branch_next); program.emit_insn(Insn::Next { cursor_id: branch_cursor_id, pc_if_next: branch_loop_start, }); } else { program.emit_insn(Insn::SeekRowid { cursor_id: table_cursor_id, src_reg: seek_reg, target_pc: next_value_label, }); program.emit_insn(Insn::RowId { cursor_id: table_cursor_id, dest: rowid_reg, }); if let Some(r) = &branch.union_residuals { emit_multi_index_or_residual_filters( program, t_ctx, table_references, &r.post_filter_exprs, next_value_label, None, table_cursor_id, r.requires_table_cursor, )?; } emit_multi_index_rowset_update( program, is_intersection, branch_idx, rowid_reg, rowset1_reg, current_read_rowset, current_write_rowset, next_value_label, found_in_prev_label, ); } program.preassign_label_to_next_insn(next_value_label); program.emit_insn(Insn::Next { cursor_id: ephemeral_cursor_id, pc_if_next: outer_loop_start, }); program.preassign_label_to_next_insn(branch_loop_end); Ok(()) } #[allow(clippy::too_many_arguments)] pub(super) fn emit_multi_index_scan_loop( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, table: &JoinedTable, table_references: &TableReferences, multi_idx_op: &MultiIndexScanOp, loop_start: BranchOffset, loop_end: BranchOffset, ) -> Result<()> { let table_cursor_id = program.resolve_cursor_id(&CursorKey::table(table.internal_id)); let rowid_reg = program.alloc_register(); let is_intersection = matches!(multi_idx_op.set_op, SetOperation::Intersection { .. }); let rowset1_reg = program.alloc_register(); let rowset2_reg = if is_intersection { program.alloc_register() } else { rowset1_reg }; program.emit_insn(Insn::Null { dest: rowset1_reg, dest_end: None, }); if is_intersection { program.emit_insn(Insn::Null { dest: rowset2_reg, dest_end: None, }); } let mut current_read_rowset = rowset1_reg; let mut current_write_rowset = if is_intersection { rowset2_reg } else { rowset1_reg }; for (branch_idx, branch) in multi_idx_op.branches.iter().enumerate() { let found_in_prev_label = if is_intersection && branch_idx > 0 { Some(program.allocate_label()) } else { None }; match &branch.access { MultiIndexBranchAccess::Seek { .. } => emit_seek_multi_index_branch( program, t_ctx, table, table_references, branch, table_cursor_id, rowid_reg, is_intersection, branch_idx, rowset1_reg, current_read_rowset, current_write_rowset, found_in_prev_label, )?, MultiIndexBranchAccess::InSeek { .. } => emit_in_seek_multi_index_branch( program, t_ctx, table, table_references, branch, table_cursor_id, rowid_reg, is_intersection, branch_idx, rowset1_reg, current_read_rowset, current_write_rowset, found_in_prev_label, )?, } if is_intersection && branch_idx > 0 && branch_idx < multi_idx_op.branches.len() - 1 { std::mem::swap(&mut current_read_rowset, &mut current_write_rowset); program.emit_insn(Insn::Null { dest: current_write_rowset, dest_end: None, }); } } let final_rowset = if is_intersection && multi_idx_op.branches.len() > 1 { let num_swaps = multi_idx_op.branches.len().saturating_sub(2); if num_swaps % 2 == 0 { rowset2_reg } else { rowset1_reg } } else { rowset1_reg }; program.preassign_label_to_next_insn(loop_start); program.emit_insn(Insn::RowSetRead { rowset_reg: final_rowset, pc_if_empty: loop_end, dest_reg: rowid_reg, }); let skip_label = program.allocate_label(); program.emit_insn(Insn::SeekRowid { cursor_id: table_cursor_id, src_reg: rowid_reg, target_pc: skip_label, }); let rowid_expr = Expr::RowId { database: None, table: table.internal_id, }; t_ctx .resolver .cache_expr_reg(Cow::Owned(rowid_expr), rowid_reg, false, None); program.preassign_label_to_next_insn(skip_label); Ok(()) } ================================================ FILE: core/translate/main_loop/open.rs ================================================ use super::*; use crate::translate::main_loop::{conditions::LoopConditionEmitter, hash::HashProbeSetupEmitter}; use crate::translate::{ main_loop::close::AutoIndexBuild, plan::{self, SubqueryEvalPhase}, subquery::{materialized_from_clause_subquery_storage, MaterializedFromClauseSubqueryStorage}, }; fn emit_materialized_subquery_result_columns( program: &mut ProgramBuilder, from_clause_subquery: &crate::schema::FromClauseSubquery, cursor_id: CursorID, index: Option<&Index>, ) { let Some(start_reg) = from_clause_subquery.result_columns_start_reg else { return; }; let index_to_table = index.map(|index| { let mut source_cols = vec![None; from_clause_subquery.columns.len()]; for (source_col, idx_col) in index.columns.iter().enumerate() { source_cols[idx_col.pos_in_table] = Some(source_col); } source_cols }); for col_idx in 0..from_clause_subquery.columns.len() { let source_col = index_to_table .as_ref() .map(|source_cols| { source_cols[col_idx] .expect("direct materialized subquery index must cover every result column") }) .unwrap_or(col_idx); program.emit_insn(Insn::Column { cursor_id, column: source_col, dest: start_reg + col_idx, default: None, }); } } /// Opens the main loop for each table in the join order, emitting instructions to initialize /// cursors and perform index seeks as necessary. pub struct OpenLoop; impl OpenLoop { #[allow(clippy::too_many_arguments)] pub fn emit( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, table_references: &TableReferences, join_order: &[JoinOrderMember], predicates: &[WhereTerm], temp_cursor_id: Option, mode: OperationMode, subqueries: &mut [NonFromClauseSubquery], ) -> Result<()> { let live_table_ids: HashSet<_> = join_order.iter().map(|member| member.table_id).collect(); for (join_index, join) in join_order.iter().enumerate() { let joined_table_index = join.original_idx; let table = &table_references.joined_tables()[joined_table_index]; let LoopLabels { loop_start, loop_end, next, } = *t_ctx .labels_main_loop .get(joined_table_index) .expect("table has no loop labels"); // For chained anti-joins (e.g. NOT EXISTS t2 AND NOT EXISTS t3), // when anti-join N exhausts without a match, execution should continue // to anti-join N+1's open_loop (not jump to the body). Resolve the // previous anti-join's label_body to the current program offset. if join_index > 0 { let prev_table_idx = join_order[join_index - 1].original_idx; let prev_is_anti = table_references.joined_tables()[prev_table_idx] .join_info .as_ref() .is_some_and(|ji| ji.is_anti()); if prev_is_anti { if let Some(prev_sa_meta) = t_ctx.meta_semi_anti_joins[prev_table_idx].as_ref() { program.resolve_label(prev_sa_meta.label_body, program.offset()); } } } // Each OUTER JOIN has a "match flag" that is initially set to false, // and is set to true when a match is found for the OUTER JOIN. // This is used to determine whether to emit actual columns or NULLs for the columns of the right table. if let Some(join_info) = table.join_info.as_ref() { if join_info.is_outer() { let lj_meta = t_ctx.meta_left_joins[joined_table_index].as_ref().unwrap(); program.emit_insn(Insn::Integer { value: 0, dest: lj_meta.reg_match_flag, }); } } let (table_cursor_id, index_cursor_id) = table.resolve_cursors(program, mode.clone())?; match &table.op { Operation::Scan(scan) => { match (scan, &table.table) { (Scan::BTreeTable { iter_dir, .. }, Table::BTree(_)) => { let iteration_cursor_id = temp_cursor_id.unwrap_or_else(|| { index_cursor_id.unwrap_or_else(|| { table_cursor_id.expect( "Either ephemeral or index or table cursor must be opened", ) }) }); if *iter_dir == IterationDirection::Backwards { program.emit_insn(Insn::Last { cursor_id: iteration_cursor_id, pc_if_empty: loop_end, }); } else { program.emit_insn(Insn::Rewind { cursor_id: iteration_cursor_id, pc_if_empty: loop_end, }); } program.preassign_label_to_next_insn(loop_start); } ( Scan::VirtualTable { idx_num, idx_str, constraints, }, Table::Virtual(_), ) => { let (start_reg, count, maybe_idx_str, maybe_idx_int) = { let args_needed = constraints.len(); let start_reg = program.alloc_registers(args_needed); for (argv_index, expr) in constraints.iter().enumerate() { let target_reg = start_reg + argv_index; translate_expr( program, Some(table_references), expr, target_reg, &t_ctx.resolver, )?; } // If best_index provided an idx_str, translate it. let maybe_idx_str = if let Some(idx_str) = idx_str { let reg = program.alloc_register(); program.emit_insn(Insn::String8 { dest: reg, value: idx_str.to_owned(), }); Some(reg) } else { None }; (start_reg, args_needed, maybe_idx_str, Some(*idx_num)) }; // Emit VFilter with the computed arguments. program.emit_insn(Insn::VFilter { cursor_id: table_cursor_id .expect("Virtual tables do not support covering indexes"), arg_count: count, args_reg: start_reg, idx_str: maybe_idx_str, idx_num: maybe_idx_int.unwrap_or(0) as usize, pc_if_empty: loop_end, }); program.preassign_label_to_next_insn(loop_start); } ( Scan::Subquery { iter_dir }, Table::FromClauseSubquery(from_clause_subquery), ) => { match from_clause_subquery.plan.select_query_destination() { Some(QueryDestination::CoroutineYield { yield_reg, coroutine_implementation_start, }) => { turso_assert_eq!( *iter_dir, IterationDirection::Forwards, "coroutine-backed subqueries cannot scan backwards" ); // Coroutine-based subquery execution // In case the subquery is an inner loop, it needs to be reinitialized on each iteration of the outer loop. program.emit_insn(Insn::InitCoroutine { yield_reg: *yield_reg, jump_on_definition: BranchOffset::Offset(0), start_offset: *coroutine_implementation_start, }); program.preassign_label_to_next_insn(loop_start); // A subquery within the main loop of a parent query has no cursor, so instead of advancing the cursor, // it emits a Yield which jumps back to the main loop of the subquery itself to retrieve the next row. // When the subquery coroutine completes, this instruction jumps to the label at the top of the termination_label_stack, // which in this case is the end of the Yield-Goto loop in the parent query. program.emit_insn(Insn::Yield { yield_reg: *yield_reg, end_offset: loop_end, subtype_clear_start_reg: 0, subtype_clear_count: 0, }); } Some(QueryDestination::EphemeralTable { cursor_id, .. }) => { // Materialized CTE - scan the ephemeral table with Rewind/Next if *iter_dir == IterationDirection::Backwards { program.emit_insn(Insn::Last { cursor_id: *cursor_id, pc_if_empty: loop_end, }); } else { program.emit_insn(Insn::Rewind { cursor_id: *cursor_id, pc_if_empty: loop_end, }); } program.preassign_label_to_next_insn(loop_start); emit_materialized_subquery_result_columns( program, from_clause_subquery, *cursor_id, None, ); } _ => { unreachable!("Subquery table with unexpected query destination") } } } _ => unreachable!( "{:?} scan cannot be used with {:?} table", scan, table.table ), } if let Some(table_cursor_id) = table_cursor_id { if let Some(index_cursor_id) = index_cursor_id { program.emit_insn(Insn::DeferredSeek { index_cursor_id, table_cursor_id, }); } } } Operation::Search(search) => { let materialized_subquery_storage = match (&table.table, search) { ( Table::FromClauseSubquery(from_clause_subquery), Search::Seek { index: Some(index), .. }, ) if index.ephemeral => { materialized_from_clause_subquery_storage(from_clause_subquery) } _ => None, }; // Open the loop for the index search. // Rowid equality point lookups are handled with a SeekRowid instruction which does not loop, since it is a single row lookup. match search { Search::RowidEq { cmp_expr } => { assert!( !matches!(table.table, Table::FromClauseSubquery(_)), "Subqueries do not support rowid seeks" ); let src_reg = program.alloc_register(); translate_expr( program, Some(table_references), cmp_expr, src_reg, &t_ctx.resolver, )?; program.emit_insn(Insn::SeekRowid { cursor_id: table_cursor_id .expect("Search::RowidEq requires a table cursor"), src_reg, target_pc: next, }); } Search::Seek { index, seek_def, .. } => { // Otherwise, it's an index/rowid scan, i.e. first a seek is performed and then a scan until the comparison expression is not satisfied anymore. let mut bloom_filter = false; if let Some(index) = index { if index.ephemeral && !matches!( materialized_subquery_storage, Some(MaterializedFromClauseSubqueryStorage::DirectIndex) ) { // Build auxiliary ephemeral indexes lazily from the row source, // whether it is a base table or a table-backed materialized subquery. let table_has_rowid = if let Table::BTree(btree) = &table.table { btree.has_rowid } else { matches!(&table.table, Table::FromClauseSubquery(_)) }; let num_seek_keys = seek_def.size(&seek_def.start); let AutoIndexResult { use_bloom_filter, .. } = emit_autoindex( program, AutoIndexBuild { index, table_cursor_id: table_cursor_id.expect( "an ephemeral index must have a source table cursor", ), index_cursor_id: index_cursor_id.expect( "an ephemeral index must have an index cursor", ), table_has_rowid, num_seek_keys, seek_def, affinity_str: plan::synthesized_seek_affinity_str( index, seek_def, ) .as_ref(), }, )?; bloom_filter = use_bloom_filter; } } let seek_cursor_id = if materialized_subquery_storage.is_some() { index_cursor_id .expect("materialized subquery must have index cursor") } else { temp_cursor_id.unwrap_or_else(|| { index_cursor_id.unwrap_or_else(|| { table_cursor_id.expect( "Either ephemeral or index or table cursor must be opened", ) }) }) }; let max_registers = seek_def .size(&seek_def.start) .max(seek_def.size(&seek_def.end)); let start_reg = program.alloc_registers(max_registers); SeekEmitter::new( program, table_references, seek_def, t_ctx, seek_cursor_id, start_reg, loop_end, index.as_ref(), ) .emit(loop_start, bloom_filter)?; if let Some(materialized_subquery_storage) = materialized_subquery_storage { let index_cursor_id = index_cursor_id .expect("materialized subquery seek requires index cursor"); let Table::FromClauseSubquery(from_clause_subquery) = &table.table else { unreachable!("materialized subquery seek requires subquery") }; match materialized_subquery_storage { MaterializedFromClauseSubqueryStorage::TableBacked => { let table_cursor_id = table_cursor_id .expect("materialized subquery must have table cursor"); program.emit_insn(Insn::DeferredSeek { index_cursor_id, table_cursor_id, }); emit_materialized_subquery_result_columns( program, from_clause_subquery, table_cursor_id, None, ); } MaterializedFromClauseSubqueryStorage::DirectIndex => { let index = index.as_ref().expect( "direct-index materialized subquery requires index", ); emit_materialized_subquery_result_columns( program, from_clause_subquery, index_cursor_id, Some(index.as_ref()), ); } } } else { // Only emit DeferredSeek for non-subquery tables if let Some(index_cursor_id) = index_cursor_id { if let Some(table_cursor_id) = table_cursor_id { // Don't do a btree table seek until it's actually necessary to read from the table. program.emit_insn(Insn::DeferredSeek { index_cursor_id, table_cursor_id, }); } } } } Search::InSeek { index, source } => { let is_rowid = index.is_none(); let ephemeral_cursor_id = open_in_seek_source_cursor( program, table_references, &t_ctx.resolver, index.as_ref(), source, )?; program.emit_insn(Insn::NullRow { cursor_id: ephemeral_cursor_id, }); program.emit_insn(Insn::Rewind { cursor_id: ephemeral_cursor_id, pc_if_empty: loop_end, }); let outer_loop_start = program.allocate_label(); program.preassign_label_to_next_insn(outer_loop_start); let seek_reg = program.alloc_register(); // The emitted loop is: // for each RHS key in the ephemeral cursor // seek table/index to that key // scan all matching rows for that key program.emit_insn(Insn::Column { cursor_id: ephemeral_cursor_id, column: 0, dest: seek_reg, default: None, }); let next_val_label = program.allocate_label(); program.emit_insn(Insn::IsNull { reg: seek_reg, target_pc: next_val_label, }); if is_rowid { program.emit_insn(Insn::SeekRowid { cursor_id: table_cursor_id .expect("InSeek rowid requires table cursor"), src_reg: seek_reg, target_pc: next_val_label, }); } else { let idx_cursor = index_cursor_id .expect("InSeek with index requires index cursor"); program.emit_insn(Insn::SeekGE { cursor_id: idx_cursor, start_reg: seek_reg, num_regs: 1, target_pc: next_val_label, is_index: true, eq_only: false, }); program.preassign_label_to_next_insn(loop_start); program.emit_insn(Insn::IdxGT { cursor_id: idx_cursor, start_reg: seek_reg, num_regs: 1, target_pc: next_val_label, }); if let Some(table_cursor_id) = table_cursor_id { program.emit_insn(Insn::DeferredSeek { index_cursor_id: idx_cursor, table_cursor_id, }); } } // `close_loop` uses this metadata to stitch together the outer // ephemeral-value loop and the inner scan over matches for the // current value. t_ctx.meta_in_seeks[joined_table_index] = Some(InSeekMetadata { ephemeral_cursor_id, outer_loop_start, next_val_label, }); } } } Operation::IndexMethodQuery(query) => { let start_reg = program.alloc_registers(query.arguments.len() + 1); program.emit_int(query.pattern_idx as i64, start_reg); for i in 0..query.arguments.len() { translate_expr( program, Some(table_references), &query.arguments[i], start_reg + 1 + i, &t_ctx.resolver, )?; } program.emit_insn(Insn::IndexMethodQuery { db: crate::MAIN_DB_ID, cursor_id: index_cursor_id.expect("IndexMethod requires a index cursor"), start_reg, count_reg: query.arguments.len() + 1, pc_if_empty: loop_end, }); program.preassign_label_to_next_insn(loop_start); if let Some(table_cursor_id) = table_cursor_id { if let Some(index_cursor_id) = index_cursor_id { program.emit_insn(Insn::DeferredSeek { index_cursor_id, table_cursor_id, }); } } } Operation::HashJoin(hash_join_op) => { HashProbeSetupEmitter::new( program, t_ctx, table_references, subqueries, predicates, hash_join_op, &mode, table_cursor_id.expect("Probe table must have a cursor"), loop_start, loop_end, next, &live_table_ids, ) .emit()?; } Operation::MultiIndexScan(multi_idx_op) => { emit_multi_index_scan_loop( program, t_ctx, table, table_references, multi_idx_op, loop_start, loop_end, )?; } } let condition_fail_target = if let Operation::HashJoin(ref hj) = table.op { t_ctx .hash_table_contexts .get(&hj.build_table_idx) .map(|ctx| ctx.labels.next) .expect("should have hash context for build table") } else { next }; let is_outer_hj_probe = matches!(table.op, Operation::HashJoin(ref hj) if matches!( hj.join_type, HashJoinType::LeftOuter | HashJoinType::FullOuter )); // Emit OUTER JOIN conditions (must run before setting match flags). LoopConditionEmitter::new( program, t_ctx, table_references, join_order, predicates, join_index, condition_fail_target, true, subqueries, ) .emit()?; // Set the LEFT JOIN match flag. Skip outer hash join probes - they use // HashMarkMatched / check_outer instead. if let Some(join_info) = table.join_info.as_ref() { if join_info.is_outer() && !is_outer_hj_probe { let lj_meta = t_ctx.meta_left_joins[joined_table_index].as_ref().unwrap(); program.resolve_label(lj_meta.label_match_flag_set_true, program.offset()); program.emit_insn(Insn::Integer { value: 1, dest: lj_meta.reg_match_flag, }); } } // Outer hash joins: mark the build entry as matched. if let Operation::HashJoin(ref hj) = table.op { if matches!( hj.join_type, HashJoinType::LeftOuter | HashJoinType::FullOuter ) { let build_table = &table_references.joined_tables()[hj.build_table_idx]; let hash_table_id: usize = build_table.internal_id.into(); program.emit_insn(Insn::HashMarkMatched { hash_table_id }); // FULL OUTER: also set the probe-side match flag. if matches!(hj.join_type, HashJoinType::FullOuter) { let probe_idx = hj.probe_table_idx; if let Some(lj_meta) = t_ctx.meta_left_joins[probe_idx].as_ref() { program .resolve_label(lj_meta.label_match_flag_set_true, program.offset()); program.emit_insn(Insn::Integer { value: 1, dest: lj_meta.reg_match_flag, }); } } } } // Emit non-OUTER JOIN conditions. let from_outer_join = false; LoopConditionEmitter::new( program, t_ctx, table_references, join_order, predicates, join_index, condition_fail_target, from_outer_join, subqueries, ) .emit()?; // ANTI-JOIN: all conditions passed means a match was found. // Skip the outer row by jumping to the outer loop's Next. // label_body is resolved later in emit_loop, right before the body is emitted. if let Some(join_info) = table.join_info.as_ref() { if join_info.is_anti() { let sa_meta = t_ctx.meta_semi_anti_joins[joined_table_index] .as_ref() .expect("anti-join must have SemiAntiJoinMetadata"); program.add_comment(program.offset(), "anti-join: match found, skip outer row"); program.emit_insn(Insn::Goto { target_pc: sa_meta.label_next_outer, }); } } // Outer hash joins wrap inner loops in a Gosub subroutine so that // unmatched-row emission paths can re-enter them (cursors get Rewind'd). if let Operation::HashJoin(ref hj) = table.op { if matches!( hj.join_type, HashJoinType::LeftOuter | HashJoinType::FullOuter ) { let return_reg = program.alloc_register(); let gosub_label = program.allocate_label(); let skip_label = program.allocate_label(); program.emit_insn(Insn::Gosub { target_pc: gosub_label, return_reg, }); program.emit_insn(Insn::Goto { target_pc: skip_label, }); // Subroutine body starts here (inner loops follow) program.preassign_label_to_next_insn(gosub_label); if let Some(hash_ctx) = t_ctx.hash_table_contexts.get_mut(&hj.build_table_idx) { hash_ctx.inner_loop_gosub_reg = Some(return_reg); hash_ctx.labels.inner_loop_gosub = Some(gosub_label); hash_ctx.labels.inner_loop_skip = Some(skip_label); } } } } if subqueries.iter().any(|s| { !s.has_been_evaluated() && matches!(s.eval_phase, SubqueryEvalPhase::BeforeLoop) }) { crate::bail_parse_error!( "all before-loop subqueries should have already been emitted, but found {} unevaluated subqueries", subqueries .iter() .filter(|s| { !s.has_been_evaluated() && matches!(s.eval_phase, SubqueryEvalPhase::BeforeLoop) }) .count() ); } Ok(()) } } ================================================ FILE: core/translate/main_loop/seek.rs ================================================ use super::*; fn index_seek_affinities( idx: &Index, tables: &TableReferences, seek_def: &SeekDef, seek_key: &SeekKey, ) -> String { let table = tables .joined_tables() .iter() .find(|jt| jt.table.get_name() == idx.table_name) .expect("index source table not found in table references"); idx.columns .iter() .zip(seek_def.iter(seek_key)) .map(|(ic, key_component)| { let col_aff = if ic.expr.is_some() { Affinity::Blob } else { table .table .get_column_at(ic.pos_in_table) .expect("index column position out of bounds") .affinity() }; match key_component { SeekKeyComponent::Expr(expr) if col_aff.expr_needs_no_affinity_change(expr) => { affinity::SQLITE_AFF_NONE } _ => col_aff.aff_mask(), } }) .collect() } fn encode_seek_keys_for_custom_types( program: &mut ProgramBuilder, tables: &TableReferences, seek_index: &Arc, start_reg: usize, num_keys: usize, idx_col_offset: usize, resolver: &Resolver<'_>, ) -> crate::Result<()> { let table = tables .find_table_by_identifier(&seek_index.table_name) .or_else(|| tables.find_table_by_table_name(&seek_index.table_name)); let table = match table { Some(t) => t, None => return Ok(()), }; let columns = table.columns(); for i in 0..num_keys { let idx_col_pos = idx_col_offset + i; if idx_col_pos >= seek_index.columns.len() { break; } let idx_col = &seek_index.columns[idx_col_pos]; let table_col = match columns.get(idx_col.pos_in_table) { Some(c) => c, None => continue, }; let type_def = match resolver .schema() .get_type_def(&table_col.ty_str, table.is_strict()) { Some(td) => td, None => continue, }; let encode_expr = match &type_def.encode { Some(e) => e, None => continue, }; let reg = start_reg + i; let skip_label = program.allocate_label(); program.emit_insn(Insn::IsNull { reg, target_pc: skip_label, }); crate::translate::expr::emit_type_expr( program, encode_expr, reg, reg, table_col, type_def, resolver, )?; program.resolve_label(skip_label, program.offset()); } Ok(()) } /// Seek-based loop setup. /// /// A seek loop has a real two-phase contract: /// 1. Emit and position using the start bound. /// 2. Emit the termination bound and anchor `loop_start`. pub(super) struct SeekEmitter<'a, 'plan> { program: &'a mut ProgramBuilder, tables: &'a TableReferences, seek_def: &'a SeekDef, t_ctx: &'a mut TranslateCtx<'plan>, seek_cursor_id: usize, start_reg: usize, loop_end: BranchOffset, seek_index: Option<&'a Arc>, is_index: bool, } impl<'a, 'plan> SeekEmitter<'a, 'plan> { #[allow(clippy::too_many_arguments)] pub(super) fn new( program: &'a mut ProgramBuilder, tables: &'a TableReferences, seek_def: &'a SeekDef, t_ctx: &'a mut TranslateCtx<'plan>, seek_cursor_id: usize, start_reg: usize, loop_end: BranchOffset, seek_index: Option<&'a Arc>, ) -> Self { Self { program, tables, seek_def, t_ctx, seek_cursor_id, start_reg, loop_end, seek_index, is_index: seek_index.is_some(), } } /// Emit the start bound and position the cursor at the first candidate row. fn emit_start_bound(&mut self, use_bloom_filter: bool) -> Result<()> { if self.seek_def.prefix.is_empty() && matches!(self.seek_def.start.last_component, SeekKeyComponent::None) { match self.seek_def.iter_dir { IterationDirection::Forwards => { if self .seek_index .is_some_and(|index| index.columns[0].order == SortOrder::Asc) { self.program.emit_null(self.start_reg, None); self.program.emit_insn(Insn::SeekGT { is_index: self.is_index, cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs: 1, target_pc: self.loop_end, }); } else { self.program.emit_insn(Insn::Rewind { cursor_id: self.seek_cursor_id, pc_if_empty: self.loop_end, }); } } IterationDirection::Backwards => { if self .seek_index .is_some_and(|index| index.columns[0].order == SortOrder::Desc) { self.program.emit_null(self.start_reg, None); self.program.emit_insn(Insn::SeekLT { is_index: self.is_index, cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs: 1, target_pc: self.loop_end, }); } else { self.program.emit_insn(Insn::Last { cursor_id: self.seek_cursor_id, pc_if_empty: self.loop_end, }); } } } return Ok(()); } for (i, key) in self.seek_def.iter(&self.seek_def.start).enumerate() { let reg = self.start_reg + i; match key { SeekKeyComponent::Expr(expr) => { translate_expr_no_constant_opt( self.program, Some(self.tables), expr, reg, &self.t_ctx.resolver, NoConstantOptReason::RegisterReuse, )?; if !expr.is_nonnull(self.tables) { self.program.emit_insn(Insn::IsNull { reg, target_pc: self.loop_end, }); } } SeekKeyComponent::Null => self.program.emit_null(reg, None), SeekKeyComponent::None => { unreachable!("None component is not possible in iterator") } } } let num_regs = self.seek_def.size(&self.seek_def.start); if let Some(idx) = self.seek_index { encode_seek_keys_for_custom_types( self.program, self.tables, idx, self.start_reg, num_regs, 0, &self.t_ctx.resolver, )?; let affinities = index_seek_affinities(idx, self.tables, self.seek_def, &self.seek_def.start); if affinities.chars().any(|c| c != affinity::SQLITE_AFF_NONE) { self.program.emit_insn(Insn::Affinity { start_reg: self.start_reg, count: std::num::NonZeroUsize::new(num_regs).unwrap(), affinities, }); } if use_bloom_filter { turso_assert!( idx.ephemeral, "bloom filter can only be used with ephemeral indexes" ); self.program.emit_insn(Insn::Filter { cursor_id: self.seek_cursor_id, key_reg: self.start_reg, num_keys: num_regs, target_pc: self.loop_end, }); } } match self.seek_def.start.op { SeekOp::GE { eq_only } => self.program.emit_insn(Insn::SeekGE { is_index: self.is_index, cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs, target_pc: self.loop_end, eq_only, }), SeekOp::GT => self.program.emit_insn(Insn::SeekGT { is_index: self.is_index, cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs, target_pc: self.loop_end, }), SeekOp::LE { eq_only } => self.program.emit_insn(Insn::SeekLE { is_index: self.is_index, cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs, target_pc: self.loop_end, eq_only, }), SeekOp::LT => self.program.emit_insn(Insn::SeekLT { is_index: self.is_index, cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs, target_pc: self.loop_end, }), }; Ok(()) } /// Emit the end bound check and anchor the loop-start label. fn emit_termination(&mut self, loop_start: BranchOffset) -> Result<()> { if self.seek_def.prefix.is_empty() && matches!(self.seek_def.end.last_component, SeekKeyComponent::None) { self.program.preassign_label_to_next_insn(loop_start); match self.seek_def.iter_dir { IterationDirection::Forwards => { if self .seek_index .is_some_and(|index| index.columns[0].order == SortOrder::Desc) { self.program.emit_null(self.start_reg, None); self.program.emit_insn(Insn::IdxGE { cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs: 1, target_pc: self.loop_end, }); } } IterationDirection::Backwards => { if self .seek_index .is_some_and(|index| index.columns[0].order == SortOrder::Asc) { self.program.emit_null(self.start_reg, None); self.program.emit_insn(Insn::IdxLE { cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs: 1, target_pc: self.loop_end, }); } } } return Ok(()); } let num_regs = self.seek_def.size(&self.seek_def.end); let last_reg = self.start_reg + self.seek_def.prefix.len(); match &self.seek_def.end.last_component { SeekKeyComponent::Expr(expr) => { translate_expr_no_constant_opt( self.program, Some(self.tables), expr, last_reg, &self.t_ctx.resolver, NoConstantOptReason::RegisterReuse, )?; if let Some(idx) = self.seek_index { encode_seek_keys_for_custom_types( self.program, self.tables, idx, last_reg, 1, self.seek_def.prefix.len(), &self.t_ctx.resolver, )?; let affinities = index_seek_affinities(idx, self.tables, self.seek_def, &self.seek_def.end); if affinities.chars().any(|c| c != affinity::SQLITE_AFF_NONE) { self.program.emit_insn(Insn::Affinity { start_reg: self.start_reg, count: std::num::NonZeroUsize::new(num_regs).unwrap(), affinities, }); } } if !expr.is_nonnull(self.tables) { self.program.emit_insn(Insn::IsNull { reg: last_reg, target_pc: self.loop_end, }); } } SeekKeyComponent::Null => self.program.emit_null(last_reg, None), SeekKeyComponent::None => {} } self.program.preassign_label_to_next_insn(loop_start); let mut rowid_reg = None; let mut affinity = None; if !self.is_index { rowid_reg = Some(self.program.alloc_register()); self.program.emit_insn(Insn::RowId { cursor_id: self.seek_cursor_id, dest: rowid_reg.unwrap(), }); affinity = if let Some(table_ref) = self .tables .joined_tables() .iter() .find(|t| t.columns().iter().any(|c| c.is_rowid_alias())) { if let Some(rowid_col_idx) = table_ref.columns().iter().position(|c| c.is_rowid_alias()) { Some(table_ref.columns()[rowid_col_idx].affinity()) } else { Some(Affinity::Numeric) } } else { Some(Affinity::Numeric) }; } match (self.is_index, self.seek_def.end.op) { (true, SeekOp::GE { .. }) => self.program.emit_insn(Insn::IdxGE { cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs, target_pc: self.loop_end, }), (true, SeekOp::GT) => self.program.emit_insn(Insn::IdxGT { cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs, target_pc: self.loop_end, }), (true, SeekOp::LE { .. }) => self.program.emit_insn(Insn::IdxLE { cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs, target_pc: self.loop_end, }), (true, SeekOp::LT) => self.program.emit_insn(Insn::IdxLT { cursor_id: self.seek_cursor_id, start_reg: self.start_reg, num_regs, target_pc: self.loop_end, }), (false, SeekOp::GE { .. }) => self.program.emit_insn(Insn::Ge { lhs: rowid_reg.unwrap(), rhs: self.start_reg, target_pc: self.loop_end, flags: CmpInsFlags::default() .jump_if_null() .with_affinity(affinity.unwrap()), collation: self.program.curr_collation(), }), (false, SeekOp::GT) => self.program.emit_insn(Insn::Gt { lhs: rowid_reg.unwrap(), rhs: self.start_reg, target_pc: self.loop_end, flags: CmpInsFlags::default() .jump_if_null() .with_affinity(affinity.unwrap()), collation: self.program.curr_collation(), }), (false, SeekOp::LE { .. }) => self.program.emit_insn(Insn::Le { lhs: rowid_reg.unwrap(), rhs: self.start_reg, target_pc: self.loop_end, flags: CmpInsFlags::default() .jump_if_null() .with_affinity(affinity.unwrap()), collation: self.program.curr_collation(), }), (false, SeekOp::LT) => self.program.emit_insn(Insn::Lt { lhs: rowid_reg.unwrap(), rhs: self.start_reg, target_pc: self.loop_end, flags: CmpInsFlags::default() .jump_if_null() .with_affinity(affinity.unwrap()), collation: self.program.curr_collation(), }), } Ok(()) } pub(super) fn emit(mut self, loop_start: BranchOffset, use_bloom_filter: bool) -> Result<()> { self.emit_start_bound(use_bloom_filter)?; self.emit_termination(loop_start) } } ================================================ FILE: core/translate/mod.rs ================================================ //! The VDBE bytecode code generator. //! //! This module is responsible for translating the SQL AST into a sequence of //! instructions for the VDBE. The VDBE is a register-based virtual machine that //! executes bytecode instructions. This code generator is responsible for taking //! the SQL AST and generating the corresponding VDBE instructions. For example, //! a SELECT statement will be translated into a sequence of instructions that //! will read rows from the database and filter them according to a WHERE clause. pub(crate) mod aggregation; pub(crate) mod alter; pub(crate) mod analyze; pub(crate) mod attach; pub(crate) mod collate; mod compound_select; pub(crate) mod delete; pub(crate) mod display; pub(crate) mod emitter; pub(crate) mod expr; pub(crate) mod expression_index; pub(crate) mod fkeys; pub(crate) mod group_by; pub(crate) mod index; pub(crate) mod insert; pub(crate) mod integrity_check; pub(crate) mod logical; pub(crate) mod main_loop; pub(crate) mod optimizer; pub(crate) mod order_by; pub(crate) mod plan; pub(crate) mod planner; pub(crate) mod pragma; pub(crate) mod result_row; pub(crate) mod rollback; pub(crate) mod schema; pub(crate) mod select; pub(crate) mod stmt_journal; pub(crate) mod subquery; pub(crate) mod transaction; pub(crate) mod trigger; pub(crate) mod trigger_exec; pub(crate) mod update; pub(crate) mod upsert; pub(crate) mod vacuum; mod values; pub(crate) mod view; mod window; use crate::schema::Schema; use crate::storage::pager::Pager; use crate::sync::Arc; use crate::translate::delete::translate_delete; use crate::translate::emitter::Resolver; use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts, QueryMode}; use crate::vdbe::Program; use crate::{bail_parse_error, Connection, Result, SymbolTable}; use alter::translate_alter_table; use analyze::translate_analyze; use index::{translate_create_index, translate_drop_index, translate_optimize}; use insert::translate_insert; use rollback::{translate_release, translate_rollback, translate_savepoint}; use schema::{translate_create_table, translate_create_virtual_table, translate_drop_table}; use select::translate_select; use tracing::{instrument, Level}; use transaction::{translate_tx_begin, translate_tx_commit}; use turso_parser::ast; use update::translate_update; #[instrument(skip_all, level = Level::DEBUG)] #[allow(clippy::too_many_arguments)] pub fn translate( schema: &Schema, stmt: ast::Stmt, pager: Arc, connection: Arc, syms: &SymbolTable, query_mode: QueryMode, input: &str, ) -> Result { tracing::trace!("querying {}", input); let change_cnt_on = matches!( stmt, ast::Stmt::CreateIndex { .. } | ast::Stmt::Delete { .. } | ast::Stmt::Insert { .. } | ast::Stmt::Update { .. } ); let mut program = ProgramBuilder::new( query_mode, connection.get_capture_data_changes_info().clone(), // These options will be extended whithin each translate program ProgramBuilderOpts { num_cursors: 1, approx_num_insns: 32, approx_num_labels: 2, }, ); program.prologue(); let mut resolver = Resolver::new( schema, connection.database_schemas(), connection.attached_databases(), syms, connection.experimental_custom_types_enabled(), ); match stmt { // There can be no nesting with pragma, so lift it up here ast::Stmt::Pragma { name, body } => { pragma::translate_pragma( &resolver, &name, body, pager, connection.clone(), &mut program, )?; } stmt => translate_inner(stmt, &mut resolver, &mut program, &connection, input)?, }; program.epilogue(schema); program.build(connection, change_cnt_on, input) } // TODO: for now leaving the return value as a Program. But ideally to support nested parsing of arbitraty // statements, we would have to return a program builder instead /// Translate SQL statement into bytecode program. pub fn translate_inner( stmt: ast::Stmt, resolver: &mut Resolver, program: &mut ProgramBuilder, connection: &Arc, input: &str, ) -> Result<()> { let is_write = matches!( stmt, ast::Stmt::AlterTable { .. } | ast::Stmt::Analyze { .. } | ast::Stmt::CreateIndex { .. } | ast::Stmt::CreateTable { .. } | ast::Stmt::CreateTrigger { .. } | ast::Stmt::CreateView { .. } | ast::Stmt::CreateMaterializedView { .. } | ast::Stmt::CreateVirtualTable(..) | ast::Stmt::CreateType { .. } | ast::Stmt::Delete { .. } | ast::Stmt::DropIndex { .. } | ast::Stmt::DropTable { .. } | ast::Stmt::DropType { .. } | ast::Stmt::DropView { .. } | ast::Stmt::Reindex { .. } | ast::Stmt::Optimize { .. } | ast::Stmt::Update { .. } | ast::Stmt::Insert { .. } ); if is_write && connection.get_query_only() { bail_parse_error!("Cannot execute write statement in query_only mode") } let is_select = matches!(stmt, ast::Stmt::Select { .. }); match stmt { ast::Stmt::AlterTable(alter) => { translate_alter_table(alter, resolver, program, connection, input)?; } ast::Stmt::Analyze { name } => translate_analyze(name, resolver, program)?, ast::Stmt::Attach { expr, db_name, key } => { attach::translate_attach(&expr, resolver, &db_name, &key, program, connection.clone())?; } ast::Stmt::Begin { typ, name } => { translate_tx_begin(typ, name, resolver.schema(), program)? } ast::Stmt::Commit { name } => { translate_tx_commit(name, resolver.schema(), resolver, program)? } ast::Stmt::CreateIndex { .. } => { translate_create_index(program, connection, resolver, stmt)?; } ast::Stmt::CreateTable { temporary, if_not_exists, tbl_name, body, } => translate_create_table( tbl_name, resolver, temporary, if_not_exists, body, program, connection, )?, ast::Stmt::CreateTrigger { temporary, if_not_exists, trigger_name, time, event, tbl_name, for_each_row, when_clause, commands, } => { // Reconstruct SQL for storage let sql = trigger::create_trigger_to_sql( temporary, if_not_exists, &trigger_name, time, &event, &tbl_name, for_each_row, when_clause.as_deref(), &commands, ); trigger::translate_create_trigger( trigger_name, resolver, temporary, if_not_exists, time, tbl_name, program, sql, &commands, when_clause.as_deref(), )? } ast::Stmt::CreateView { view_name, select, columns, .. } => view::translate_create_view(&view_name, resolver, &select, &columns, program)?, ast::Stmt::CreateMaterializedView { view_name, select, .. } => view::translate_create_materialized_view( &view_name, resolver, &select, connection.clone(), program, )?, ast::Stmt::CreateVirtualTable(vtab) => { translate_create_virtual_table(vtab, resolver, program, connection)? } ast::Stmt::Delete { tbl_name, where_clause, limit, returning, indexed, order_by, with, } => { if !order_by.is_empty() { bail_parse_error!("ORDER BY clause is not supported in DELETE"); } if where_clause.is_none() && connection.get_dml_require_where() { bail_parse_error!( "DELETE without a WHERE clause is not allowed when require_where (or i_am_a_dummy) is enabled" ); } translate_delete( &tbl_name, resolver, where_clause, limit, returning, indexed, with, program, connection, )? } ast::Stmt::Detach { name } => { attach::translate_detach(&name, resolver, program, connection.clone())? } ast::Stmt::DropIndex { if_exists, idx_name, } => translate_drop_index(&idx_name, resolver, if_exists, program)?, ast::Stmt::DropTable { if_exists, tbl_name, } => translate_drop_table(tbl_name, resolver, if_exists, program, connection)?, ast::Stmt::DropTrigger { if_exists, trigger_name, } => trigger::translate_drop_trigger(resolver, &trigger_name, if_exists, program)?, ast::Stmt::DropView { if_exists, view_name, } => view::translate_drop_view(resolver, &view_name, if_exists, program)?, ast::Stmt::CreateType { if_not_exists, type_name, body, } => { if !connection.experimental_custom_types_enabled() { bail_parse_error!("Custom types require --experimental-custom-types flag"); } schema::translate_create_type(&type_name, &body, if_not_exists, resolver, program)? } ast::Stmt::DropType { if_exists, type_name, } => { if !connection.experimental_custom_types_enabled() { bail_parse_error!("Custom types require --experimental-custom-types flag"); } schema::translate_drop_type(&type_name, if_exists, resolver, program)? } ast::Stmt::Pragma { .. } => { bail_parse_error!("PRAGMA statement cannot be evaluated in a nested context") } ast::Stmt::Reindex { .. } => bail_parse_error!("REINDEX not supported yet"), ast::Stmt::Optimize { idx_name } => { translate_optimize(idx_name, resolver, program, connection)? } ast::Stmt::Release { name } => translate_release(program, name)?, ast::Stmt::Rollback { tx_name, savepoint_name, } => translate_rollback(program, tx_name, savepoint_name)?, ast::Stmt::Savepoint { name } => translate_savepoint(program, name)?, ast::Stmt::Select(select) => { translate_select( select, resolver, program, plan::QueryDestination::ResultRows, connection, )?; } ast::Stmt::Update(update) => { if update.where_clause.is_none() && connection.get_dml_require_where() { bail_parse_error!( "UPDATE without a WHERE clause is not allowed when require_where (or i_am_a_dummy) is enabled" ); } translate_update(update, resolver, program, connection)? } ast::Stmt::Vacuum { name, into } => { vacuum::translate_vacuum(program, name.as_ref(), into.as_deref())? } ast::Stmt::Insert { with, or_conflict, tbl_name, columns, body, returning, } => translate_insert( resolver, or_conflict, tbl_name, columns, body, returning, with, program, connection, )?, }; // Indicate write operations so that in the epilogue we can emit the correct type of transaction if is_write { program.begin_write_operation(); } // Indicate read operations so that in the epilogue we can emit the correct type of transaction if is_select && !program.table_references.is_empty() { program.begin_read_operation(); } Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::io::MemoryIO; use crate::schema::{BTreeTable, Table, SQLITE_SEQUENCE_TABLE_NAME}; use crate::Database; /// Verify that REGEXP produces the correct error when no regexp function is registered. #[test] fn test_regexp_no_function_registered() { let io = Arc::new(MemoryIO::new()); let db = Database::open_file(io, ":memory:").unwrap(); let conn = db.connect().unwrap(); let schema = db.schema.lock().clone(); let pager = conn.pager.load().clone(); // Use an empty SymbolTable so regexp() is not available. let empty_syms = SymbolTable::new(); let mut parser = turso_parser::parser::Parser::new(b"SELECT 'x' REGEXP 'y'"); let cmd = parser.next().unwrap().unwrap(); let stmt = match cmd { ast::Cmd::Stmt(s) => s, _ => panic!("expected statement"), }; let result = translate( &schema, stmt, pager, conn, &empty_syms, QueryMode::Normal, "", ); let err = result.unwrap_err().to_string(); assert!( err.contains("no such function: regexp"), "expected 'no such function: regexp', got: {err}" ); } #[test] fn test_insert_autoincrement_with_malformed_sqlite_sequence_is_corrupt() { let io = Arc::new(MemoryIO::new()); let db = Database::open_file(io, ":memory:").unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT, v TEXT)") .unwrap(); let mut schema = db.schema.lock().as_ref().clone(); let seq_root_page = schema .get_btree_table(SQLITE_SEQUENCE_TABLE_NAME) .expect("sqlite_sequence should exist after creating AUTOINCREMENT table") .root_page; let malformed_seq = BTreeTable::from_sql("CREATE TABLE sqlite_sequence(name)", seq_root_page) .expect("malformed sqlite_sequence SQL should parse"); schema.tables.insert( SQLITE_SEQUENCE_TABLE_NAME.to_string(), Arc::new(Table::BTree(Arc::new(malformed_seq))), ); let pager = conn.pager.load().clone(); let syms = SymbolTable::new(); let mut parser = turso_parser::parser::Parser::new(b"INSERT INTO t(v) VALUES('x')"); let cmd = parser.next().unwrap().unwrap(); let stmt = match cmd { ast::Cmd::Stmt(s) => s, _ => panic!("expected statement"), }; let err = translate(&schema, stmt, pager, conn, &syms, QueryMode::Normal, "") .expect_err("translation should fail with malformed sqlite_sequence"); match err { crate::LimboError::Corrupt(msg) => { assert!( msg.contains("sqlite_sequence"), "expected sqlite_sequence corruption error, got: {msg}" ); } other => panic!("expected LimboError::Corrupt, got: {other}"), } } #[test] fn test_insert_autoincrement_with_missing_sqlite_sequence_is_corrupt() { let io = Arc::new(MemoryIO::new()); let db = Database::open_file(io, ":memory:").unwrap(); let conn = db.connect().unwrap(); conn.execute("CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT, v TEXT)") .unwrap(); let mut schema = db.schema.lock().as_ref().clone(); schema.tables.remove(SQLITE_SEQUENCE_TABLE_NAME); let pager = conn.pager.load().clone(); let syms = SymbolTable::new(); let mut parser = turso_parser::parser::Parser::new(b"INSERT INTO t(v) VALUES('x')"); let cmd = parser.next().unwrap().unwrap(); let stmt = match cmd { ast::Cmd::Stmt(s) => s, _ => panic!("expected statement"), }; let err = translate(&schema, stmt, pager, conn, &syms, QueryMode::Normal, "") .expect_err("translation should fail with missing sqlite_sequence"); match err { crate::LimboError::Corrupt(msg) => { assert!( msg.contains("missing sqlite_sequence"), "expected missing sqlite_sequence error, got: {msg}" ); } other => panic!("expected LimboError::Corrupt, got: {other}"), } } } ================================================ FILE: core/translate/optimizer/OPTIMIZER.md ================================================ # Overview of the current state of the query optimizer in Limbo Query optimization is obviously an important part of any SQL-based database engine. This document is an overview of what we currently do. ## Structure of the optimizer directory 1. `mod.rs` - Provides the high-level optimization interface through `optimize_plan()` 2. `access_method.rs` - Determines what is the best index to use when joining a table to a set of preceding tables 3. `constraints.rs` - Manages query constraints: - Extracts constraints from the WHERE clause - Determines which constraints are usable with indexes 4. `cost.rs` - Calculates the cost of doing a seek vs a scan, for example 5. `join.rs` - Implements the System R style dynamic programming join ordering algorithm 6. `order.rs` - Determines if sort operations can be eliminated based on the chosen access methods and join order ## Join reordering and optimal index selection **The goals of query optimization are at least the following:** 1. Do as little page I/O as possible 2. Do as little CPU work as possible 3. Retain query correctness. **The most important ways to achieve no. 1 and no. 2 are:** 1. Choose the optimal access method for each table (e.g. an index or a rowid-based seek, or a full table scan if all else fails). 2. Choose the best or near-best way to reorder the tables in the query so that those optimal access methods can be used. 3. Also factor in whether the chosen join order and indexes allow removal of any sort operations that are necessary for query correctness. ## Limbo's optimizer Limbo's optimizer is an implementation of an extremely traditional [IBM System R](https://www.cs.cmu.edu/~15721-f24/slides/02-Selinger-SystemR-opt.pdf) style optimizer, i.e. straight from the 70s! The DP algorithm is explained below. ### Current high level flow of the optimizer 1. **SQL rewriting** - Rewrite certain SQL expressions to another form (not a lot currently; e.g. rewrite BETWEEN as two comparisons) - Eliminate constant conditions: e.g. `WHERE 1` is removed, `WHERE 0` short-circuits the whole query because it is trivially false. 2. **Check whether there is an "interesting order"** that we should consider when evaluating indexes and join orders - Is there a GROUP BY? an ORDER BY? Both? 3. **Convert WHERE clause conjucts to Constraints** - E.g. in `WHERE t.x = 5`, the expression `5` _constrains_ table `t` to values of `x` that are exactly `5`. - E.g. in `Where t.x = u.x`, the expression `u.x` constrains `t`, AND `t.x` constrains `u`. - Per table, each constraint has an estimated _selectivity_ (how much it filters the result set); this affects join order calculations, see the paragraph on _Estimation_ below. - Per table, constraints are also analyzed for whether one or multiple of them can be used as an index seek key to avoid a full scan. 4. **Compute the best join order using a dynamic programming algorithm:** - `n` = number of tables considered - `n=1`: find the lowest _cost_ way to access each single table, given the constraints of the query. Memoize the result. - `n=2`: for each table found in the `n=1` step, find the best way to join that table with each other table. Memoize the result. - `n=3`: for each 2-table subset found, find the best way to join that result to each other table. Memoize the result. - `n=m`: for each `m-1` table subset found, find the best way to join that result to the `m'th` table - **Use pruning to reduce search space:** - Compute the literal query order first, and store its _cost_ as an upper threshold. In some cases it is not possible to compute this upper threshold from the literal order—for example, when table-valued functions are involved and their arguments reference tables that appear to the right in the join order. In such situations, the literal order cannot be executed directly, so no meaningful _cost_ can be assigned. In these cases, the threshold is set to infinity, ensuring that valid plans are still considered. - If at any point a considered join order exceeds the upper threshold, discard that search path since it cannot be better than the current best. - For example, we have `SELECT * FROM a JOIN b JOIN c JOIN d`. Compute `JOIN(a,b,c,d)` first. If `JOIN (b,a)` is already worse than `JOIN(a,b,c,d)`, we don't have to even try `JOIN(b,a,c)`. - Also keep track of the best plan per _subset_: - If we find that `JOIN(b,a,c)` is better than any other permutation of the same tables, e.g. `JOIN(a,b,c)`, then we can discard _ALL_ of the other permutations for that subset. For example, we don't need to consider `JOIN(a,b,c,d)` because we know it's worse than `JOIN(b,a,c,d)`. - This is possible due to the associativity and commutativity of INNER JOINs. - Also keep track of the best _ordered plan_ , i.e. one that provides the "interesting order" mentioned above. - At the end, apply a cost penalty to the best overall plan - If it is now worse than the best sorted plan, then choose the sorted plan as the best plan for the query. - This allows us to eliminate a sorting operation. - If the best overall plan is still best even with the sorting penalty, then keep it. A sorting operation is later applied to sort the rows according to the desired order. 5. **Mutate the plan's `join_order` and `Operation`s to match the computed best plan.** ### Estimation of cost and cardinalities + a note on table statistics Currently, in the absence of `ANALYZE`, `sqlite_stat1` etc. we assume the following: 1. Each table has `1,000,000` rows. 2. Each equality (`=`) filter will filter out some percentage of the result set. 3. Each nonequality (e.g. `>`) will filter out some smaller percentage of the result set. 4. Each `4096` byte database page holds `50` rows, i.e. roughly `80` bytes per row 5. Sort operations have some CPU cost dependent on the number of input rows to the sort operation. From the above, we derive the following formula for estimating the cost of joining `t1` with `t2` ``` JOIN_COST = PAGE_IO(t1.rows) + t1.rows * PAGE_IO(t2.rows) ``` For example, let's take the query `SELECT * FROM t1 JOIN t2 USING(foo) WHERE t2.foo > 10`. Let's assume the following: - `t1` has `6400` rows and `t2` has `8000` rows - there are no indexes at all - let's ignore the CPU cost from the equation for simplicity. The best access method for both is a full table scan. The output cardinality of `t1` is the full table, because nothing is filtering it. Hence, the cost of `t1 JOIN t2` becomes: ``` JOIN_COST = PAGE_IO(t1.input_rows) + t1.output_rows * PAGE_IO(t2.input_rows) // plugging in the values: JOIN_COST = PAGE_IO(6400) + 6400 * PAGE_IO(8000) JOIN_COST = 80 + 6400 * 100 = 640080 ``` Now let's consider `t2 JOIN t1`. The best access method for both is still a full scan, but since we can filter on `t2.foo > 10`, its output cardinality decreases. Let's assume only 1/4 of the rows of `t2` match the condition `t2.foo > 10`. Hence, the cost of `t2 join t1` becomes: ``` JOIN_COST = PAGE_IO(t2.input_rows) + t2.output_rows * PAGE_IO(t1.input_rows) // plugging in the values: JOIN_COST = PAGE_IO(8000) + 1/4 * 8000 * PAGE_IO(6400) JOIN_COST = 100 + 2000 * 80 = 160100 ``` Even though `t2` is a larger table, because we were able to reduce the input set to the join operation, it's dramatically cheaper. #### Statistics Since we don't support `ANALYZE`, nor can we assume that users will call `ANALYZE` anyway, we use simple magic constants to estimate the selectivity of join predicates, row count of tables, and so on. When we have support for `ANALYZE`, we should plug the statistics from `sqlite_stat1` and friends into the optimizer to make more informed decisions. ### Estimating the output cardinality of a join The output cardinality (output row count) of an operation is as follows: ``` OUTPUT_CARDINALITY_JOIN = INPUT_CARDINALITY_RHS * OUTPUT_CARDINALITY_RHS where INPUT_CARDINALITY_RHS = OUTPUT_CARDINALITY_LHS ``` example: ``` SELECT * FROM products p JOIN order_lines o ON p.id = o.product_id ``` Assuming there are 100 products, i.e. just selecting all products would yield 100 rows: ``` OUTPUT_CARDINALITY_LHS = 100 INPUT_CARDINALITY_RHS = 100 ``` Assuming p.id = o.product_id will return three orders per each product: ``` OUTPUT_CARDINALITY_RHS = 3 OUTPUT_CARDINALITY_JOIN = 100 * 3 = 300 ``` i.e. the join is estimated to return 300 rows, 3 for each product. Again, in the absence of statistics, we use magic constants to estimate these cardinalities. Estimating them is important because in multi-way joins the output cardinality of the previous join becomes the input cardinality of the next one. ================================================ FILE: core/translate/optimizer/access_method.rs ================================================ use crate::sync::Arc; use rustc_hash::FxHashMap as HashMap; use smallvec::SmallVec; use std::collections::VecDeque; use turso_ext::{ConstraintInfo, ConstraintUsage, ResultCode}; use turso_parser::ast::{self, SortOrder, TableInternalId}; use crate::schema::Schema; use crate::stats::AnalyzeStats; use crate::translate::expr::{as_binary_components, walk_expr, WalkControl}; use crate::translate::optimizer::constraints::{ convert_to_vtab_constraint, ordered_materialized_key_columns, BinaryExprSide, Constraint, ConstraintOperator, RangeConstraintRef, }; use crate::translate::optimizer::cost::{rows_per_leaf_page_for_index, RowCountEstimate}; use crate::translate::optimizer::cost_params::CostModelParams; use crate::translate::plan::{ plan_has_outer_scope_dependency, HashJoinKey, HashJoinType, NonFromClauseSubquery, SetOperation, SubqueryState, TableReferences, WhereTerm, }; use crate::vdbe::affinity::Affinity; use crate::vdbe::hash_table::DEFAULT_MEM_BUDGET; use crate::{ schema::{FromClauseSubquery, Index, IndexColumn, Table}, translate::plan::{IndexMethodQuery, IterationDirection, JoinOrderMember, JoinedTable}, vtab::VirtualTable, LimboError, Result, }; use super::{ constraints::{ usable_constraints_for_join_order, usable_constraints_for_lhs_mask, TableConstraints, }, cost::{ estimate_cost_for_scan_or_seek, estimate_index_cost, estimate_rows_per_seek, AnalyzeCtx, Cost, IndexInfo, }, join::JoinPlanningContext, multi_index::{ consider_multi_index_intersection, consider_multi_index_union, MultiIndexBranchParams, }, order::{ btree_access_order_consumed, subquery_intrinsic_order_consumed, ColumnTarget, EqualityPrefixScope, OrderTarget, }, }; use crate::translate::planner::TableMask; #[derive(Debug, Clone)] /// Represents a way to access a table. pub struct AccessMethod { /// The estimated number of page fetches. /// CPU costs are folded into the same scalar cost model. pub cost: Cost, /// Estimated rows produced per outer row before applying remaining filters. pub estimated_rows_per_outer_row: f64, /// Whether join cardinality should still apply planner-side selectivity after /// using this access path's own row estimate. pub residual_constraints: ResidualConstraintMode, /// WHERE-term indices already accounted for by this access path's row estimate. pub consumed_where_terms: SmallVec<[usize; 4]>, /// Table-type specific access method details. pub params: AccessMethodParams, } /// Describes whether join planning should still apply residual WHERE-term /// selectivity after choosing an access path. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ResidualConstraintMode { /// Apply the selectivity of all relevant WHERE terms that this access path /// did not already consume. ApplyUnconsumed, /// The access path already provided its own final row estimate; do not /// multiply any planner-side residual selectivity on top. None, } /// Table‑specific details of how an [`AccessMethod`] operates. #[derive(Debug, Clone)] pub enum AccessMethodParams { BTreeTable { /// The direction of iteration for the access method. /// Typically this is backwards only if it helps satisfy an [OrderTarget]. iter_dir: IterationDirection, /// The index that is being used, if any. For rowid based searches (and full table scans), this is None. index: Option>, /// The constraint references that are being used, if any. /// An empty list of constraint refs means a scan (full table or index); /// a non-empty list means a search. constraint_refs: Vec, }, VirtualTable { /// Index identifier returned by the table's `best_index` method. idx_num: i32, /// Optional index string returned by the table's `best_index` method. idx_str: Option, /// Constraint descriptors passed to the virtual table’s `filter` method. /// Each corresponds to a column/operator pair from the WHERE clause. constraints: Vec, /// Information returned by the virtual table's `best_index` method /// describing how each constraint will be used. constraint_usages: Vec, }, /// FROM-subquery scan. Coroutine-backed scans run forwards; materialized /// subqueries may also be scanned backwards when their intrinsic order /// matches the requested extremum order. Subquery { iter_dir: IterationDirection }, /// Materialized subquery with an ephemeral index for seeking. /// The subquery results are materialized once into an ephemeral index, /// which can then be seeked using join conditions. MaterializedSubquery { /// The ephemeral index to build and seek into. index: Arc, /// The constraint references used for seeking. constraint_refs: Vec, /// The direction to iterate the ephemeral index once positioned. iter_dir: IterationDirection, }, HashJoin { /// The table to build the hash table from. build_table_idx: usize, /// The table to probe the hash table with. probe_table_idx: usize, /// Join key references - each entry contains the where_clause index and which side /// of the equality belongs to the build table. Supports expression-based join keys. join_keys: Vec, /// Memory budget for the hash table in bytes. mem_budget: usize, /// Whether the build input should be materialized as a rowid list before hash build. materialize_build_input: bool, /// Whether to use a bloom filter on the probe side. use_bloom_filter: bool, /// Join semantics: Inner, LeftOuter, or FullOuter. join_type: HashJoinType, }, /// Custom index method access (e.g., FTS). /// This variant is used when the optimizer determines that a custom index method /// should be used for table access in a join query. IndexMethod { /// The fully constructed IndexMethodQuery operation to apply to this table. query: IndexMethodQuery, /// Index in WHERE clause that was covered by this index method (if any). where_covered: Option, }, /// Multi-index scan for OR-by-union or AND-by-intersection optimization. /// Used when a WHERE clause has OR/AND terms that can each use a different index. /// Example: WHERE a = 1 AND|OR b = 2 with separate indexes on a and b. MultiIndexScan { /// Each branch represents one term with its own index access. branches: Vec, /// Index of the primary WHERE term. where_term_idx: usize, /// The set operation (Union for OR, Intersection for AND). set_op: SetOperation, }, /// IN-list driven index seek. InSeek { index: Option>, affinity: Affinity, where_term_idx: usize, }, } /// Result of generic btree candidate selection before it is wrapped into a full /// [`AccessMethod`]. pub(super) struct ChosenBtreeCandidate { pub(super) iter_dir: IterationDirection, pub(super) index: Option>, pub(super) constraint_refs: Vec, pub(super) cost: Cost, } #[derive(Debug, Clone)] pub(super) struct ChosenInSeekCandidate { pub(super) index: Option>, pub(super) affinity: Affinity, pub(super) constraint_idx: usize, pub(super) cost: Cost, pub(super) estimated_rows_per_outer_row: f64, } /// Describes what a caller needs to read from a branch-local scan. /// /// Ordinary table access needs the scanned rows themselves, but multi-index /// branches only harvest rowids into a RowSet and fetch full rows later. /// Making this explicit avoids threading "mystery bool" flags through the cost /// model. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum BranchReadMode { /// Cost the branch as if it only needs rowids from the scan. RowIdOnly, /// Cost the branch as a normal table/index access that may need full row data. FullRow, } #[allow(clippy::too_many_arguments)] /// Choose the best ordinary btree lookup candidate for one table under the /// current join-order prefix. pub(super) fn choose_best_btree_candidate( rhs_table: &JoinedTable, rhs_constraints: &TableConstraints, lhs_mask: &TableMask, rhs_table_idx: usize, maybe_order_target: Option<&OrderTarget>, schema: &Schema, analyze_stats: &AnalyzeStats, input_cardinality: f64, base_row_count: RowCountEstimate, params: &CostModelParams, ) -> Option { // Seed the baseline with a table scan only if a rowid candidate exists // (i.e. no INDEXED BY has removed it). Otherwise start at infinite cost // so the forced index candidate always wins. let has_rowid_candidate = rhs_constraints.candidates.iter().any(|c| c.index.is_none()); let mut best_cost = if has_rowid_candidate { estimate_cost_for_scan_or_seek( None, &[], &[], input_cardinality, base_row_count, false, params, None, ) } else { Cost(f64::MAX) }; let mut best_choice = ChosenBtreeCandidate { iter_dir: IterationDirection::Forwards, index: None, constraint_refs: vec![], cost: best_cost, }; let mut best_adjusted_output = f64::MAX; let mut best_is_ordered = false; // Build a mask for the rhs table itself. let mut rhs_table_mask = TableMask::new(); rhs_table_mask.add_table(rhs_table_idx); // Estimate cost for each candidate index (including the rowid index) and // keep the best candidate. for candidate in rhs_constraints.candidates.iter() { let usable_constraint_refs = usable_constraints_for_lhs_mask( &rhs_constraints.constraints, &candidate.refs, lhs_mask, rhs_table_idx, ); let index_info = match candidate.index.as_ref() { Some(index) => IndexInfo { unique: index.unique, covering: rhs_table.index_is_covering(index), column_count: index.columns.len(), rows_per_leaf_page: rows_per_leaf_page_for_index( index.columns.len(), rhs_table, params.rows_per_table_page, ), }, None => IndexInfo { unique: true, covering: !usable_constraint_refs.is_empty(), column_count: 1, rows_per_leaf_page: params.rows_per_table_page, }, }; let (iter_dir, is_index_ordered, order_satisfiability_bonus) = if let Some(order_target) = maybe_order_target { // Reuse the same index-vs-order matching logic as final plan // validation, but allow any equality-constrained seek prefix to // be skipped here. Candidate scoring only needs to know whether // this specific access path can emit rows ordered after its seek // key; final global ORDER BY validation is stricter and only // skips globally constant prefixes. let all_same_direction = btree_access_order_consumed( rhs_table, IterationDirection::Forwards, candidate.index.as_deref(), &usable_constraint_refs, &order_target.columns, schema, EqualityPrefixScope::AnyEquality, ) == order_target.columns.len(); let all_opposite_direction = btree_access_order_consumed( rhs_table, IterationDirection::Backwards, candidate.index.as_deref(), &usable_constraint_refs, &order_target.columns, schema, EqualityPrefixScope::AnyEquality, ) == order_target.columns.len(); let satisfies_order = all_same_direction || all_opposite_direction; if satisfies_order { // Bonus = estimated sort cost saved. Sorting is O(n log n). let n = *base_row_count; let sort_cost_saved = Cost(n * (n.max(1.0).log2()) * params.sort_cpu_per_row); ( if all_same_direction { IterationDirection::Forwards } else { IterationDirection::Backwards }, true, sort_cost_saved, ) } else { (IterationDirection::Forwards, false, Cost(0.0)) } } else { (IterationDirection::Forwards, false, Cost(0.0)) }; let analyze_ctx = AnalyzeCtx { rhs_table, index: candidate.index.as_ref(), stats: analyze_stats, }; let cost = estimate_cost_for_scan_or_seek( Some(index_info), &rhs_constraints.constraints, &usable_constraint_refs, input_cardinality, base_row_count, is_index_ordered, params, Some(&analyze_ctx), ); // Residual filter output adjustment (mirrors SQLite's whereLoopOutputAdjust). // // When two indexes have the same seek cost, the one whose seek // prerequisites already cover more residual WHERE constraints will // produce fewer output rows (because those residual filters can be // accounted for). This breaks ties correctly: a join-driven seek // like fromId=e1.toId (prereqs={e1}) can claim credit for the // constant residual label='requires', but a constant seek like // label='requires' (prereqs={}) cannot claim credit for the // join-dependent residual fromId=e1.toId. let loop_prereq_mask = { let mut mask = TableMask::new(); for ucref in usable_constraint_refs.iter() { for idx in [ ucref.eq.as_ref().map(|e| e.constraint_pos), ucref.lower_bound, ucref.upper_bound, ] .into_iter() .flatten() { let c = &rhs_constraints.constraints[idx]; mask = TableMask::from_table_number_iter( mask.tables_iter().chain(c.lhs_mask.tables_iter()), ); } } mask }; // Tables whose constraints this loop can account for: the loop's own // prerequisite tables plus the current table itself. let allowed_mask = TableMask::from_table_number_iter( loop_prereq_mask .tables_iter() .chain(rhs_table_mask.tables_iter()), ); // Collect which constraint positions are consumed by the index seek. let consumed: SmallVec<[usize; 8]> = usable_constraint_refs .iter() .flat_map(|ucref| { [ ucref.eq.as_ref().map(|e| e.constraint_pos), ucref.lower_bound, ucref.upper_bound, ] .into_iter() .flatten() }) .collect(); // Multiply selectivities of residual constraints whose prerequisites // are within the allowed mask (i.e. already satisfied by this loop). let residual_selectivity: f64 = rhs_constraints .constraints .iter() .enumerate() .filter(|(i, c)| { !consumed.contains(i) && c.usable && allowed_mask.contains_all(&c.lhs_mask) && matches!( c.operator, ConstraintOperator::AstNativeOperator(ast::Operator::Equals) | ConstraintOperator::AstNativeOperator(ast::Operator::Greater) | ConstraintOperator::AstNativeOperator(ast::Operator::GreaterEquals) | ConstraintOperator::AstNativeOperator(ast::Operator::Less) | ConstraintOperator::AstNativeOperator(ast::Operator::LessEquals) ) }) .map(|(_, c)| c.selectivity) .product(); // Adjusted output: lower means the loop delivers fewer rows downstream. let adjusted_output = residual_selectivity; // Only apply the order bonus when this candidate satisfies order but // the current best does not. When both satisfy order, switching saves // no additional sort cost. let effective_bonus = if is_index_ordered && !best_is_ordered { order_satisfiability_bonus } else { Cost(0.0) }; let adjusted_best = best_cost + effective_bonus; let costs_equal = (cost.0 - adjusted_best.0).abs() < 1e-9; if cost < adjusted_best || (costs_equal && adjusted_output < best_adjusted_output - 1e-12) { best_cost = cost; best_adjusted_output = adjusted_output; best_is_ordered = is_index_ordered; best_choice = ChosenBtreeCandidate { iter_dir, index: candidate.index.clone(), constraint_refs: usable_constraint_refs.clone(), cost, }; } } Some(best_choice) } fn consumed_where_terms_from_constraint_refs( constraints: &[Constraint], constraint_refs: &[RangeConstraintRef], ) -> SmallVec<[usize; 4]> { let mut consumed = SmallVec::new(); for cref in constraint_refs { for constraint_idx in [ cref.eq.as_ref().map(|eq| eq.constraint_pos), cref.lower_bound, cref.upper_bound, ] .into_iter() .flatten() { let where_term_idx = constraints[constraint_idx].where_clause_pos.0; if !consumed.contains(&where_term_idx) { consumed.push(where_term_idx); } } } consumed } #[allow(clippy::too_many_arguments)] /// Evaluate whether an `IN (...)` predicate should replace the ordinary btree /// access path with repeated equality seeks. /// /// This is intentionally separate from `choose_best_btree_candidate()`: the /// generic btree chooser reasons about a single continuous scan/seek over one /// candidate, while `InSeek` emits a two-level loop that materializes the RHS /// into an ephemeral cursor and performs one equality seek per RHS value. /// Because of that execution shape, only rowid or the first column of an index /// can drive `InSeek`, and the comparison collation must match the chosen /// index's first-key collation. pub(super) fn choose_best_in_seek_candidate( rhs_table: &JoinedTable, rhs_constraints: &TableConstraints, lhs_mask: &TableMask, input_cardinality: f64, base_row_count: RowCountEstimate, params: &CostModelParams, best_cost: Cost, read_mode: BranchReadMode, ) -> Result> { let Table::BTree(btree) = &rhs_table.table else { return Err(LimboError::InternalError( "consider_in_seek_access_method called on non-BTree table".into(), )); }; let base = *base_row_count; let tree_depth = if base <= 1.0 { 1.0 } else { (base.ln() / params.rows_per_table_page.ln()) .ceil() .max(1.0) }; let mut best_in_seek = None; let mut best_in_seek_cost = best_cost; for candidate in rhs_constraints.candidates.iter() { let first_col_pos = candidate .index .as_ref() .and_then(|idx| idx.columns.first().map(|c| c.pos_in_table)); let rowid_only = matches!(read_mode, BranchReadMode::RowIdOnly); let index_info = match candidate.index.as_ref() { Some(index) => IndexInfo { unique: index.unique, covering: rowid_only || rhs_table.index_is_covering(index), column_count: index.columns.len(), rows_per_leaf_page: rows_per_leaf_page_for_index( index.columns.len(), rhs_table, params.rows_per_table_page, ), }, None => IndexInfo { unique: true, covering: false, column_count: 1, rows_per_leaf_page: params.rows_per_table_page, }, }; for constraint in &rhs_constraints.constraints { let ConstraintOperator::In { not, estimated_values, } = constraint.operator else { continue; }; if not || !lhs_mask.contains_all(&constraint.lhs_mask) { continue; } let matches = if candidate.index.is_none() { constraint.is_rowid } else { !constraint.is_rowid && constraint.table_col_pos.is_some() && constraint.table_col_pos == first_col_pos }; if !matches { continue; } // `open_loop` copies the chosen index collation onto the ephemeral // IN cursor. Reject mismatches here so a BINARY `IN` comparison // cannot silently become `NOCASE`/`RTRIM` just because the index is. if let (Some(index), Some(col_pos)) = (&candidate.index, constraint.table_col_pos) { let constrained_column = &rhs_table.table.columns()[col_pos]; let table_collation = constrained_column.collation(); let index_collation = index.columns[0].collation.unwrap_or_default(); if table_collation != index_collation { continue; } } let rows_per_seek = if (index_info.unique && index_info.column_count == 1) || candidate.index.is_none() { 1.0 } else { (base * params.sel_eq_indexed).sqrt().max(1.0) }; let in_cost = estimate_index_cost( base, tree_depth, index_info, estimated_values * input_cardinality, rows_per_seek, params, ); if in_cost >= best_in_seek_cost { continue; } let affinity = if let Some(col_pos) = constraint.table_col_pos { btree .columns .get(col_pos) .map(|col| col.affinity()) .unwrap_or(Affinity::Blob) } else { Affinity::Integer }; best_in_seek_cost = in_cost; best_in_seek = Some(ChosenInSeekCandidate { index: candidate.index.clone(), affinity, constraint_idx: constraint.where_clause_pos.0, cost: in_cost, estimated_rows_per_outer_row: (constraint.selectivity * base).max(1.0), }); } } Ok(best_in_seek) } fn consider_in_seek_access_method( rhs_table: &JoinedTable, rhs_constraints: &TableConstraints, lhs_mask: &TableMask, input_cardinality: f64, base_row_count: RowCountEstimate, params: &CostModelParams, best_cost: Cost, ) -> Result> { Ok(choose_best_in_seek_candidate( rhs_table, rhs_constraints, lhs_mask, input_cardinality, base_row_count, params, best_cost, BranchReadMode::FullRow, )? .map(|chosen| AccessMethod { cost: chosen.cost, estimated_rows_per_outer_row: chosen.estimated_rows_per_outer_row, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms: smallvec::smallvec![chosen.constraint_idx], params: AccessMethodParams::InSeek { index: chosen.index, affinity: chosen.affinity, where_term_idx: chosen.constraint_idx, }, })) } /// Return the best [AccessMethod] for a given join order. #[allow(clippy::too_many_arguments)] pub fn find_best_access_method_for_join_order( rhs_table: &JoinedTable, rhs_constraints: &TableConstraints, join_order: &[JoinOrderMember], planning_context: JoinPlanningContext<'_>, where_clause: &[WhereTerm], available_indexes: &HashMap>>, table_references: &TableReferences, subqueries: &[NonFromClauseSubquery], schema: &Schema, analyze_stats: &AnalyzeStats, input_cardinality: f64, base_row_count: RowCountEstimate, params: &CostModelParams, ) -> Result> { match &rhs_table.table { Table::BTree(_) => find_best_access_method_for_btree( rhs_table, rhs_constraints, join_order, planning_context.maybe_order_target, where_clause, available_indexes, table_references, subqueries, schema, analyze_stats, input_cardinality, base_row_count, params, ), Table::Virtual(vtab) => find_best_access_method_for_vtab( vtab, &rhs_constraints.constraints, join_order, input_cardinality, base_row_count, params, ), Table::FromClauseSubquery(subquery) => find_best_access_method_for_subquery( rhs_table, subquery, rhs_constraints, join_order, planning_context, schema, input_cardinality, base_row_count, params, ), } } #[allow(clippy::too_many_arguments)] fn find_best_access_method_for_btree( rhs_table: &JoinedTable, rhs_constraints: &TableConstraints, join_order: &[JoinOrderMember], maybe_order_target: Option<&OrderTarget>, where_clause: &[WhereTerm], available_indexes: &HashMap>>, table_references: &TableReferences, subqueries: &[NonFromClauseSubquery], schema: &Schema, analyze_stats: &AnalyzeStats, input_cardinality: f64, base_row_count: RowCountEstimate, params: &CostModelParams, ) -> Result> { let rhs_table_idx = join_order.last().unwrap().original_idx; let lhs_mask = TableMask::from_table_number_iter( join_order .iter() .take(join_order.len() - 1) .map(|member| member.original_idx), ); let best = choose_best_btree_candidate( rhs_table, rhs_constraints, &lhs_mask, rhs_table_idx, maybe_order_target, schema, analyze_stats, input_cardinality, base_row_count, params, ) .expect("btree candidate selection must always consider the rowid candidate"); let estimated_rows_per_outer_row = if best.constraint_refs.is_empty() { *base_row_count } else { let index_info = match best.index.as_ref() { Some(index) => IndexInfo { unique: index.unique, covering: rhs_table.index_is_covering(index), column_count: index.columns.len(), rows_per_leaf_page: rows_per_leaf_page_for_index( index.columns.len(), rhs_table, params.rows_per_table_page, ), }, None => IndexInfo { unique: true, covering: true, column_count: 1, rows_per_leaf_page: params.rows_per_table_page, }, }; let analyze_ctx = AnalyzeCtx { rhs_table, index: best.index.as_ref(), stats: analyze_stats, }; estimate_rows_per_seek( index_info, &rhs_constraints.constraints, &best.constraint_refs, base_row_count, Some(&analyze_ctx), ) }; let mut best_access_method = AccessMethod { cost: best.cost, estimated_rows_per_outer_row, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms: consumed_where_terms_from_constraint_refs( &rhs_constraints.constraints, &best.constraint_refs, ), params: AccessMethodParams::BTreeTable { iter_dir: best.iter_dir, index: best.index, constraint_refs: best.constraint_refs, }, }; // Skip alternative access methods (in-seek, multi-index) when INDEXED BY or NOT INDEXED // is specified — the user explicitly requested a specific index or no index. if rhs_table.indexed.is_none() && rhs_table.btree().is_some_and(|b| b.has_rowid) { if let Some(in_seek_method) = consider_in_seek_access_method( rhs_table, rhs_constraints, &lhs_mask, input_cardinality, base_row_count, params, best_access_method.cost, )? { best_access_method = in_seek_method; } if let Some(multi_idx_method) = consider_multi_index_union( rhs_table, where_clause, available_indexes, table_references, subqueries, schema, input_cardinality, base_row_count, params, best_access_method.cost, &lhs_mask, analyze_stats, ) { best_access_method = multi_idx_method; } if let Some(multi_idx_and_method) = consider_multi_index_intersection( rhs_table, where_clause, available_indexes, table_references, subqueries, schema, input_cardinality, base_row_count, params, best_access_method.cost, &lhs_mask, analyze_stats, ) { best_access_method = multi_idx_and_method; } } Ok(Some(best_access_method)) } fn find_best_access_method_for_vtab( vtab: &VirtualTable, constraints: &[Constraint], join_order: &[JoinOrderMember], input_cardinality: f64, base_row_count: RowCountEstimate, params: &CostModelParams, ) -> Result> { let vtab_constraints = convert_to_vtab_constraint(constraints, join_order); // TODO: get proper order_by information to pass to the vtab. // maybe encode more info on t_ctx? we need: [col_idx , is_descending] let best_index_result = vtab.best_index(&vtab_constraints, &[]); match best_index_result { Ok(index_info) => { Ok(Some(AccessMethod { // TODO: Base cost on `IndexInfo::estimated_cost` and output cardinality on `IndexInfo::estimated_rows` cost: estimate_cost_for_scan_or_seek( None, &[], &[], input_cardinality, base_row_count, false, params, None, ), estimated_rows_per_outer_row: *base_row_count, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms: SmallVec::new(), params: AccessMethodParams::VirtualTable { idx_num: index_info.idx_num, idx_str: index_info.idx_str, constraints: vtab_constraints, constraint_usages: index_info.constraint_usages, }, })) } Err(ResultCode::ConstraintViolation) => Ok(None), Err(e) => Err(LimboError::from(e)), } } /// Collect all table IDs referenced in an expression. fn collect_table_refs(expr: &ast::Expr) -> Option> { let mut tables = Vec::new(); let result = walk_expr(expr, &mut |e| { match e { ast::Expr::Column { table, .. } | ast::Expr::RowId { table, .. } => { if !tables.contains(table) { tables.push(*table); } } _ => {} } Ok(WalkControl::Continue) }); result.ok().map(|_| tables) } /// Detect equi-join conditions between exactly two tables for hash join. /// /// Returns `HashJoinKey` entries pointing at `WHERE` terms of the form: /// = /// or /// = /// /// Both sides may be arbitrary expressions (e.g. `lower(t1.a) = substr(t2.b,1,3)`), /// but each side must reference columns from exactly one table: /// - the build side must reference only `build_table_id` /// - the probe side must reference only `probe_table_id` /// /// This function does *not* mark any terms as consumed; the caller is responsible /// for doing so if a hash join is selected. pub fn find_equijoin_conditions( build_table_id: TableInternalId, probe_table_id: TableInternalId, where_clause: &[WhereTerm], ) -> Vec { let mut join_keys = Vec::new(); for (where_idx, where_term) in where_clause.iter().enumerate() { if where_term.consumed { continue; } let Ok(Some((lhs, op, rhs))) = as_binary_components(&where_term.expr) else { continue; }; if !matches!(op.as_ast_operator(), Some(ast::Operator::Equals)) { continue; } let Some(lhs_tables) = collect_table_refs(lhs) else { continue; }; let Some(rhs_tables) = collect_table_refs(rhs) else { continue; }; // Require each side to reference exactly one table. This prevents // constants or multi-table expressions from being considered join keys. if lhs_tables.len() != 1 || rhs_tables.len() != 1 { continue; } let lhs_tid = lhs_tables[0]; let rhs_tid = rhs_tables[0]; // Accept either orientation: build=probe or probe=build. let build_side = if lhs_tid == build_table_id && rhs_tid == probe_table_id { Some(BinaryExprSide::Lhs) } else if rhs_tid == build_table_id && lhs_tid == probe_table_id { Some(BinaryExprSide::Rhs) } else { None }; if let Some(build_side) = build_side { join_keys.push(HashJoinKey { where_clause_idx: where_idx, build_side, }); } } join_keys } /// Estimate the cost of a hash join between two tables. /// /// The cost model accounts for: /// - Build phase: Creating the hash table from the build side (one-time cost) /// - Probe phase: Looking up each probe row in the hash table (one scan of probe table) /// - Memory pressure: Additional IO cost if the hash table spills to disk pub fn estimate_hash_join_cost( build_cardinality: f64, probe_cardinality: f64, mem_budget: usize, probe_multiplier: f64, params: &CostModelParams, ) -> Cost { // Estimate if the hash table will fit in memory based on actual row counts let estimated_hash_table_size = (build_cardinality as usize).saturating_mul(params.hash_bytes_per_row as usize); let will_spill = estimated_hash_table_size > mem_budget; // Build phase: hash and insert all rows from build table (one-time cost) // With real ANALYZE stats, this accurately reflects the actual build table size let build_cost = build_cardinality * (params.hash_cpu_cost + params.hash_insert_cost); // Probe phase: scan probe table, hash each row and lookup in hash table. // If the hash-join probe loop is nested under prior tables, the probe // scan repeats per outer row, so scale by probe_multiplier. let probe_cost = probe_cardinality * (params.hash_cpu_cost + params.hash_lookup_cost) * probe_multiplier; // Spill cost: if hash table exceeds memory budget, we need to write/read partitions to disk. // Grace hash join writes partitions and reads them back, so it's 2x the page IO. // Use page-based IO cost (rows / rows_per_page) rather than per-row IO. let spill_cost = if will_spill { let build_pages = (build_cardinality / params.rows_per_table_page).ceil(); let probe_pages = (probe_cardinality / params.rows_per_table_page).ceil(); // Write both sides to partitions, then read back: 2 * (build_pages + probe_pages) (build_pages + probe_pages) * 2.0 * probe_multiplier } else { 0.0 }; Cost(build_cost + probe_cost + spill_cost) } /// Try to create a hash join access method for joining two tables. #[allow(clippy::too_many_arguments)] pub fn try_hash_join_access_method( build_table: &JoinedTable, probe_table: &JoinedTable, build_table_idx: usize, probe_table_idx: usize, build_constraints: &TableConstraints, probe_constraints: &TableConstraints, where_clause: &mut [WhereTerm], build_cardinality: f64, probe_cardinality: f64, probe_multiplier: f64, subqueries: &[NonFromClauseSubquery], params: &CostModelParams, ) -> Option { // Only works for B-tree tables if !matches!(build_table.table, Table::BTree(_)) || !matches!(probe_table.table, Table::BTree(_)) { return None; } // Avoid hash join on self-joins over the same underlying table. The current // implementation assumes distinct build/probe sources; sharing storage can // lead to incorrect matches. let probe_root_page = probe_table.table.btree().expect("table is BTree").root_page; let build_root_page = build_table.table.btree().expect("table is BTree").root_page; if build_root_page == probe_root_page { return None; } // No hash join for semi/anti-joins (nested loop with index seek is preferred). if probe_table .join_info .as_ref() .is_some_and(|ji| ji.is_semi_or_anti()) || build_table .join_info .as_ref() .is_some_and(|ji| ji.is_semi_or_anti()) { return None; } // Determine join type from the probe table's join_info. let hash_join_type = if probe_table .join_info .as_ref() .is_some_and(|ji| ji.is_full_outer()) { HashJoinType::FullOuter } else if probe_table .join_info .as_ref() .is_some_and(|ji| ji.is_outer()) { HashJoinType::LeftOuter } else { HashJoinType::Inner }; // Can't build from a NullRow'd table — the hash table would hold real data // even when the cursor is in NullRow mode. if build_table .join_info .as_ref() .is_some_and(|ji| ji.is_outer()) { return None; } // Skip hash join on USING/NATURAL joins. if build_table .join_info .as_ref() .is_some_and(|ji| !ji.using.is_empty()) || probe_table .join_info .as_ref() .is_some_and(|ji| !ji.using.is_empty()) { return None; } // Avoid hash joins when there are correlated subqueries that reference the joined tables. for subquery in subqueries { if !subquery.correlated { continue; } // Check if the subquery references the build or probe table if let SubqueryState::Unevaluated { plan } = &subquery.state { if let Some(plan) = plan.as_ref() { let outer_refs = plan.table_references.outer_query_refs(); for outer_ref in outer_refs { if outer_ref.internal_id == build_table.internal_id || outer_ref.internal_id == probe_table.internal_id { return None; } } } } } let join_keys = find_equijoin_conditions( build_table.internal_id, probe_table.internal_id, where_clause, ) .into_iter() .filter(|join_key| { let probe_expr = join_key.get_probe_expr(where_clause); let Some(probe_tables) = collect_table_refs(probe_expr) else { return false; }; probe_tables.len() == 1 && probe_tables[0] == probe_table.internal_id }) .collect::>(); tracing::debug!( build_table = build_table.table.get_name(), probe_table = probe_table.table.get_name(), join_key_count = join_keys.len(), "hash-join equi-join keys" ); // Need at least one equi-join condition if join_keys.is_empty() { return None; } // Prefer nested-loop with index lookup when an index exists on join columns. // FULL OUTER must use hash join (needed for the unmatched-build scan). // Check both tables because we could potentially use a different // join order where the indexed table becomes the probe/inner table. if hash_join_type != HashJoinType::FullOuter { for join_key in &join_keys { let probe_expr = join_key.get_probe_expr(where_clause); let probe_tables = collect_table_refs(probe_expr).unwrap_or_default(); let probe_is_single_table = probe_tables.len() == 1 && probe_tables[0] == probe_table.internal_id; let probe_is_simple_column = expr_is_simple_column_from_table(probe_expr, probe_table.internal_id); let build_expr = join_key.get_build_expr(where_clause); let build_is_simple_column = expr_is_simple_column_from_table(build_expr, build_table.internal_id); // Check probe table constraints for index on join column, only when the probe side // references the probe table alone and is a simple column/rowid reference. if probe_is_single_table && probe_is_simple_column { if let Some(constraint) = probe_constraints .constraints .iter() .find(|c| c.where_clause_pos.0 == join_key.where_clause_idx) { if let Some(col_pos) = constraint.table_col_pos { // Check if the join column is a rowid alias directly from the table schema if let Some(column) = probe_table.columns().get(col_pos) { if column.is_rowid_alias() { return None; } } // Also check regular indexes for candidate in &probe_constraints.candidates { if let Some(index) = &candidate.index { if index.column_table_pos_to_index_pos(col_pos).is_some() { return None; } } } } } } // Check build table constraints for index on join column, only when the build side // is a simple column/rowid reference. if build_is_simple_column { if let Some(constraint) = build_constraints .constraints .iter() .find(|c| c.where_clause_pos.0 == join_key.where_clause_idx) { if let Some(col_pos) = constraint.table_col_pos { // Check if the join column is a rowid alias directly from the table schema if let Some(column) = build_table.columns().get(col_pos) { if column.is_rowid_alias() { return None; } } // Also check regular indexes for candidate in &build_constraints.candidates { if let Some(index) = &candidate.index { if index.column_table_pos_to_index_pos(col_pos).is_some() { return None; } } } } } } } } let cost = estimate_hash_join_cost( build_cardinality, probe_cardinality, DEFAULT_MEM_BUDGET, probe_multiplier, params, ); Some(AccessMethod { cost, estimated_rows_per_outer_row: probe_cardinality, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms: join_keys.iter().map(|key| key.where_clause_idx).collect(), params: AccessMethodParams::HashJoin { build_table_idx, probe_table_idx, join_keys, mem_budget: DEFAULT_MEM_BUDGET, materialize_build_input: false, use_bloom_filter: false, join_type: hash_join_type, }, }) } /// Returns true when the expression is a simple column/rowid reference to the table. /// Used to decide if an index seek could replace a hash join. fn expr_is_simple_column_from_table(expr: &ast::Expr, table_id: TableInternalId) -> bool { matches!( expr, ast::Expr::Column { table, .. } | ast::Expr::RowId { table, .. } if *table == table_id ) } /// Check whether a subquery's intrinsic row order (from its ORDER BY or /// finalized inner scan) already satisfies the outer order target, and if so /// in which direction. /// /// Backwards iteration (`Last`/`Prev`) is only possible when /// `table_materialization_required` is true — coroutine scans cannot be /// reversed at runtime. fn intrinsic_subquery_scan_direction( rhs_table: &JoinedTable, subquery: &FromClauseSubquery, maybe_order_target: Option<&OrderTarget>, table_materialization_required: bool, schema: &Schema, ) -> Option { let order_target = maybe_order_target?; let cols = &order_target.columns; let matches_forwards = subquery_intrinsic_order_consumed( rhs_table.internal_id, subquery, IterationDirection::Forwards, cols, schema, ) == cols.len(); if matches_forwards { return Some(IterationDirection::Forwards); } let matches_backwards = table_materialization_required && subquery_intrinsic_order_consumed( rhs_table.internal_id, subquery, IterationDirection::Backwards, cols, schema, ) == cols.len(); matches_backwards.then_some(IterationDirection::Backwards) } /// Find the best access method for a FROM clause subquery. /// /// Uncorrelated FROM-subqueries can either stay as coroutine scans or be treated /// like a table-backed row source with a synthesized ephemeral probe index. When /// the latter is worthwhile, we materialize the subquery into an EphemeralTable /// and later build the probe index lazily in the main-loop open phase. #[expect(clippy::too_many_arguments)] fn find_best_access_method_for_subquery( rhs_table: &JoinedTable, subquery: &FromClauseSubquery, rhs_constraints: &TableConstraints, join_order: &[JoinOrderMember], planning_context: JoinPlanningContext<'_>, schema: &Schema, input_cardinality: f64, base_row_count: RowCountEstimate, params: &CostModelParams, ) -> Result> { use super::constraints::ConstraintRef; let maybe_order_target = planning_context.maybe_order_target; let table_materialization_required = subquery.requires_table_materialization(); let can_direct_materialize_index = subquery.supports_direct_index_materialization(); let coroutine_scan_cost = estimate_cost_for_scan_or_seek( None, &[], &[], input_cardinality, base_row_count, false, params, None, ); let coroutine_reexecution_overhead = Cost((input_cardinality - 1.0).max(0.0) * *base_row_count * params.cpu_cost_per_seek); let coroutine_cost = coroutine_scan_cost + coroutine_reexecution_overhead; let scan_cost = if table_materialization_required { // Explicit MATERIALIZED hints and shared CTEs already produce a table-backed // row source. Scanning them behaves like rescanning cached rows, not rerunning // a coroutine body for each outer probe. coroutine_scan_cost } else { // The generic scan model treats repeated probes like cached rescans of a // row source. A coroutine-backed subquery is slightly more expensive: each // extra outer row reruns the subquery program instead of probing a // materialized result. Charge that extra work explicitly here. coroutine_cost }; // Plans with outer-scope dependencies cannot be materialized once - // they must re-execute for each outer row. Use coroutine for these. // This check must come first because correlated CTEs should NOT share materialized data. if plan_has_outer_scope_dependency(&subquery.plan) { return Ok(Some(AccessMethod { // Correlated subqueries always rerun for each outer row, even if the // enclosing CTE/subquery might otherwise be shareable. cost: coroutine_cost, estimated_rows_per_outer_row: *base_row_count, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms: SmallVec::new(), params: AccessMethodParams::Subquery { iter_dir: IterationDirection::Forwards, }, })); } // Build synthetic index columns from the constraints this materialized // subquery could probe. Because the index is ephemeral, we can order its // key columns to fit the chosen seek shape: equality columns first, then // range-only columns, then the remaining payload columns. let usable: Vec<(usize, &Constraint)> = rhs_constraints .constraints .iter() .enumerate() .filter(|(_, c)| { c.usable && c.table_col_pos.is_some() && matches!( c.operator.as_ast_operator(), Some( ast::Operator::Equals | ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals ) ) }) .collect(); // For extremum (MIN/MAX) targets we can reuse the subquery's intrinsic // order as a plain scan when every usable constraint is on the extremum // column itself. Once other key columns participate, the direct table // scan no longer has a simple "walk from one end" shape. let extremum_constraints_compatible = maybe_order_target.is_some_and(|ot| ot.is_extremum()) && match maybe_order_target .and_then(|ot| ot.columns.first()) .map(|c| &c.target) { Some(ColumnTarget::Column(pos)) => { usable.iter().all(|(_, c)| c.table_col_pos == Some(*pos)) } _ => false, }; // Try to reuse the subquery's intrinsic row order (from its ORDER BY or // finalized inner scan) to satisfy the outer order target directly. // For non-extremum targets this only applies when there are no seek // constraints — with constraints, we fall through to the materialized // index path which has its own order-satisfaction logic. if extremum_constraints_compatible || usable.is_empty() { if let Some(iter_dir) = intrinsic_subquery_scan_direction( rhs_table, subquery, maybe_order_target, table_materialization_required, schema, ) { return Ok(Some(AccessMethod { cost: scan_cost, estimated_rows_per_outer_row: *base_row_count, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms: SmallVec::new(), params: AccessMethodParams::Subquery { iter_dir }, })); } } if usable.is_empty() { return Ok(Some(AccessMethod { cost: scan_cost, estimated_rows_per_outer_row: *base_row_count, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms: SmallVec::new(), params: AccessMethodParams::Subquery { iter_dir: IterationDirection::Forwards, }, })); } let usable_constraints: Vec<&Constraint> = usable.iter().map(|(_, c)| *c).collect(); let key_col_positions = ordered_materialized_key_columns(&usable_constraints); if key_col_positions.is_empty() { return Ok(Some(AccessMethod { cost: scan_cost, estimated_rows_per_outer_row: *base_row_count, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms: SmallVec::new(), params: AccessMethodParams::Subquery { iter_dir: IterationDirection::Forwards, }, })); } let key_col_pos_to_index_pos: HashMap = key_col_positions .iter() .enumerate() .map(|(index_col_pos, table_col_pos)| (*table_col_pos, index_col_pos)) .collect(); // Map each usable constraint to a ConstraintRef let mut temp_constraint_refs: Vec = usable .iter() .map(|(orig_idx, c)| { let table_col_pos = c.table_col_pos.expect("table_col_pos was Some above"); let index_col_pos = *key_col_pos_to_index_pos .get(&table_col_pos) .expect("table_col_pos must exist in key_col_positions"); ConstraintRef { constraint_vec_pos: *orig_idx, index_col_pos, sort_order: SortOrder::Asc, } }) .collect(); temp_constraint_refs.sort_by_key(|x| x.index_col_pos); // Filter to only constraints that can be used given the current join order let usable_constraint_refs = usable_constraints_for_join_order( &rhs_constraints.constraints, &temp_constraint_refs, join_order, ); let has_search_constraints = !usable_constraint_refs.is_empty(); if !has_search_constraints { tracing::trace!( table = rhs_table.table.get_name(), cost = ?scan_cost, "using coroutine subquery access because no usable seek constraints remain" ); return Ok(Some(AccessMethod { cost: scan_cost, estimated_rows_per_outer_row: *base_row_count, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms: SmallVec::new(), params: AccessMethodParams::Subquery { iter_dir: IterationDirection::Forwards, }, })); } let ephemeral_index = materialized_subquery_ephemeral_index(rhs_table, subquery, &key_col_positions); let (iter_dir, _is_index_ordered, order_satisfiability_bonus) = materialized_subquery_order_properties( rhs_table, &ephemeral_index, &usable_constraint_refs, maybe_order_target, schema, base_row_count, params, ); let estimated_rows_per_outer_row = estimate_rows_per_seek( IndexInfo { unique: false, column_count: key_col_positions.len(), covering: true, rows_per_leaf_page: params.rows_per_table_page, }, &rhs_constraints.constraints, &usable_constraint_refs, base_row_count, None, ); let one_pass_scan_cost = estimate_cost_for_scan_or_seek(None, &[], &[], 1.0, base_row_count, false, params, None); let append_build_cost = Cost(*base_row_count * params.cpu_cost_per_seek); let seek_setup_cost = if table_materialization_required || can_direct_materialize_index { // Both table-backed materialization and direct-index materialization avoid // the extra "scan table into probe index" pass. They differ in storage, // not in setup work. one_pass_scan_cost + append_build_cost } else { // Compound SELECTs and other table-backed materializations need two passes: // first produce the ephemeral table, then scan it once to build the probe // index used by SEARCH. one_pass_scan_cost + one_pass_scan_cost + append_build_cost }; let seek_cost = Cost( input_cardinality * params.cpu_cost_per_seek + input_cardinality * estimated_rows_per_outer_row * params.cpu_cost_per_row, ); let total_cost = seek_setup_cost + seek_cost; if total_cost >= scan_cost + order_satisfiability_bonus { return Ok(Some(AccessMethod { cost: scan_cost, estimated_rows_per_outer_row: *base_row_count, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms: SmallVec::new(), params: AccessMethodParams::Subquery { iter_dir: IterationDirection::Forwards, }, })); } Ok(Some(AccessMethod { cost: total_cost, estimated_rows_per_outer_row, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms: consumed_where_terms_from_constraint_refs( &rhs_constraints.constraints, &usable_constraint_refs, ), params: AccessMethodParams::MaterializedSubquery { index: ephemeral_index, constraint_refs: usable_constraint_refs, iter_dir, }, })) } /// Describe the temporary index layout we would build on top of a materialized /// subquery if the planner chooses a seekable access path. /// /// This is planner metadata, not the runtime build step itself. The optimizer /// needs this shape up front so it can reason about seek prefixes, order /// coverage, and result-column remapping before any bytecode is emitted. fn materialized_subquery_ephemeral_index( rhs_table: &JoinedTable, subquery: &FromClauseSubquery, key_col_positions: &[usize], ) -> Arc { let mut index_columns: Vec = Vec::new(); let mut seen_col_positions = std::collections::HashSet::new(); for &col_pos in key_col_positions { let column = subquery .columns .get(col_pos) .expect("key column position out of bounds for materialized subquery"); if !seen_col_positions.insert(col_pos) { continue; } index_columns.push(IndexColumn { name: column.name.clone().unwrap_or_default(), order: SortOrder::Asc, pos_in_table: col_pos, collation: column.collation_opt(), default: column.default.clone(), expr: None, }); } for (col_pos, column) in subquery.columns.iter().enumerate() { if seen_col_positions.contains(&col_pos) { continue; } index_columns.push(IndexColumn { name: column.name.clone().unwrap_or_default(), order: SortOrder::Asc, pos_in_table: col_pos, collation: column.collation_opt(), default: column.default.clone(), expr: None, }); } Arc::new(Index { // Match the runtime autoindex naming so EQP and bytecode make it clear // that this is a synthetic probe/index-on-temp-table path. name: format!("ephemeral_subquery_{}", rhs_table.internal_id), columns: index_columns, unique: false, ephemeral: true, table_name: subquery.name.clone(), root_page: 0, where_clause: None, has_rowid: true, index_method: None, on_conflict: None, }) } /// Decide whether the synthetic materialized-subquery index would also satisfy /// the requested order target, and if so in which direction. /// /// The returned bonus is the estimated sorter work avoided by getting rows in /// the right order directly from the temporary index. fn materialized_subquery_order_properties( rhs_table: &JoinedTable, index: &Arc, constraint_refs: &[RangeConstraintRef], maybe_order_target: Option<&OrderTarget>, schema: &Schema, base_row_count: RowCountEstimate, params: &CostModelParams, ) -> (IterationDirection, bool, Cost) { let Some(order_target) = maybe_order_target else { return (IterationDirection::Forwards, false, Cost(0.0)); }; // Candidate scoring may ignore any equality-constrained prefix because a // seek fixes those columns to one value before iteration begins. let all_same_direction = btree_access_order_consumed( rhs_table, IterationDirection::Forwards, Some(index.as_ref()), constraint_refs, &order_target.columns, schema, EqualityPrefixScope::AnyEquality, ) == order_target.columns.len(); let all_opposite_direction = btree_access_order_consumed( rhs_table, IterationDirection::Backwards, Some(index.as_ref()), constraint_refs, &order_target.columns, schema, EqualityPrefixScope::AnyEquality, ) == order_target.columns.len(); if !(all_same_direction || all_opposite_direction) { return (IterationDirection::Forwards, false, Cost(0.0)); } // Reuse the same rough sorter cost model as ordinary ORDER BY planning: // if this index yields the needed order, we avoid an O(n log n) sort. let n = *base_row_count; let order_bonus = Cost(n * n.max(1.0).log2() * params.sort_cpu_per_row); ( if all_same_direction { IterationDirection::Forwards } else { IterationDirection::Backwards }, true, order_bonus, ) } ================================================ FILE: core/translate/optimizer/constraints.rs ================================================ use crate::{ schema::{Column, Index, Schema}, translate::{ collate::get_collseq_from_expr, expr::{as_binary_components, comparison_affinity}, expression_index::normalize_expr_for_index_matching, plan::{JoinOrderMember, JoinedTable, NonFromClauseSubquery, TableReferences, WhereTerm}, planner::{table_mask_from_expr, TableMask}, }, util::exprs_are_equivalent, vdbe::affinity::Affinity, Result, }; use crate::{turso_assert, turso_debug_assert}; use rustc_hash::FxHashMap as HashMap; use std::{cmp::Ordering, collections::VecDeque, sync::Arc}; use turso_ext::{ConstraintInfo, ConstraintOp}; use turso_parser::ast::{self, SortOrder, TableInternalId}; use super::cost_params::CostModelParams; /// Represents a single condition derived from a `WHERE` clause term /// that constrains a specific column of a table. /// /// Constraints are precomputed for each table involved in a query. They are used /// during query optimization to estimate the cost of different access paths (e.g., using an index) /// and to determine the optimal join order. A constraint can only be applied if all tables /// referenced in its expression (other than the constrained table itself) are already /// available in the current join context, i.e. on the left side in the join order /// relative to the table. Expression indexes are represented by leaving `table_col_pos` empty /// and storing the indexed expression in `expr`. #[derive(Debug, Clone)] pub struct Constraint { /// The position of the original `WHERE` clause term this constraint derives from, /// and which side of the [ast::Expr::Binary] comparison contains the expression /// that constrains the column. /// E.g. in SELECT * FROM t WHERE t.x = 10, the constraint is (0, BinaryExprSide::Rhs) /// because the RHS '10' is the constraining expression. /// /// This is tracked so we can: /// /// 1. Extract the constraining expression for use in an index seek key, and /// 2. Remove the relevant binary expression from the WHERE clause, if used as an index seek key. pub where_clause_pos: (usize, BinaryExprSide), /// The comparison operator (e.g., `=`, `>`, `<`) used in the constraint. pub operator: ConstraintOperator, /// The zero-based index of the constrained column within the table's schema. /// None for expression-index constraints. pub table_col_pos: Option, /// The expression constrained by this constraint, if it is not a simple column reference. pub expr: Option, /// For multi-index scan branches: the constraining expression and its affinity. /// When set, `get_constraining_expr` uses this instead of looking up in where_clause. /// This is needed because multi-index branches come from sub-expressions of an OR/AND, /// not directly from a top-level WHERE term. pub constraining_expr: Option<(ast::Operator, ast::Expr, Affinity)>, /// A bitmask representing the set of tables that appear on the *constraining* side /// of the comparison expression. For example, in SELECT * FROM t1,t2,t3 WHERE t1.x = t2.x + t3.x, /// the lhs_mask contains t2 and t3. Thus, this constraint can only be used if t2 and t3 /// have already been joined (i.e. are on the left side of the join order relative to t1). pub lhs_mask: TableMask, /// An estimated selectivity factor (0.0 to 1.0) indicating the fraction of rows /// expected to satisfy this constraint. Used for cost and cardinality estimation. pub selectivity: f64, /// Whether the constraint can participate in range-seek index matching /// (the eq/lower_bound/upper_bound model in RangeConstraintRef). /// False for IN constraints (which use a separate multi-value seek path) /// and for collation mismatches. pub usable: bool, /// Whether this constraint references the implicit rowid (tables without an INTEGER PRIMARY KEY alias). /// When true and `table_col_pos` is None, this constraint targets the rowid pseudo-column. pub is_rowid: bool, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum ConstraintOperator { AstNativeOperator(ast::Operator), Like { not: bool }, In { not: bool, estimated_values: f64 }, } impl ConstraintOperator { pub fn as_ast_operator(&self) -> Option { let ConstraintOperator::AstNativeOperator(op) = self else { return None; }; Some(*op) } } impl From for ConstraintOperator { fn from(op: ast::Operator) -> Self { ConstraintOperator::AstNativeOperator(op) } } #[derive(Debug, Clone, Copy, PartialEq)] pub enum BinaryExprSide { Lhs, Rhs, } impl Constraint { /// Get the constraining expression and operator, e.g. ('>=', '2+3') from 't.x >= 2+3' pub fn get_constraining_expr( &self, where_clause: &[WhereTerm], referenced_tables: Option<&TableReferences>, ) -> (ast::Operator, ast::Expr, Affinity) { // For multi-index branches, use the pre-computed constraining expression if let Some(constraining) = &self.constraining_expr { return constraining.clone(); } let (idx, side) = self.where_clause_pos; let where_term = &where_clause[idx]; let Ok(Some((lhs, op, rhs))) = as_binary_components(&where_term.expr) else { panic!("Expected a valid binary expression"); }; let mut affinity = Affinity::Blob; if op.as_ast_operator().is_some_and(|op| op.is_comparison()) && self.table_col_pos.is_some() { affinity = comparison_affinity(lhs, rhs, referenced_tables, None); } if side == BinaryExprSide::Lhs { if affinity.expr_needs_no_affinity_change(lhs) { affinity = Affinity::Blob; } ( self.operator .as_ast_operator() .expect("expected an ast operator because as_binary_components returned Some"), lhs.clone(), affinity, ) } else { if affinity.expr_needs_no_affinity_change(rhs) { affinity = Affinity::Blob; } ( self.operator .as_ast_operator() .expect("expected an ast operator because as_binary_components returned Some"), rhs.clone(), affinity, ) } } pub fn get_constraining_expr_ref<'a>(&self, where_clause: &'a [WhereTerm]) -> &'a ast::Expr { let (idx, side) = self.where_clause_pos; let where_term = &where_clause[idx]; let Ok(Some((lhs, _, rhs))) = as_binary_components(&where_term.expr) else { panic!("Expected a valid binary expression"); }; if side == BinaryExprSide::Lhs { lhs } else { rhs } } } #[derive(Debug, Clone)] /// A reference to a [Constraint] in a [TableConstraints]. /// /// This is used to track which constraints may be used as an index seek key. pub struct ConstraintRef { /// The position of the constraint in the [TableConstraints::constraints] vector. pub constraint_vec_pos: usize, /// The position of the constrained column in the index. Always 0 for rowid indices. pub index_col_pos: usize, /// The sort order of the constrained column in the index. Always ascending for rowid indices. pub sort_order: SortOrder, } /// A collection of [ConstraintRef]s for a given index, or if index is None, for the table's rowid index. /// For example, given a table `T (x,y,z)` with an index `T_I (y desc,z)`, take the following query: /// ```sql /// SELECT * FROM T WHERE y = 10 AND z = 20; /// ``` /// /// This will produce the following [ConstraintUseCandidate]: /// /// ConstraintUseCandidate { /// index: Some(T_I) /// refs: [ /// ConstraintRef { /// constraint_vec_pos: 0, // y = 10 /// index_col_pos: 0, // y /// sort_order: SortOrder::Desc, /// }, /// ConstraintRef { /// constraint_vec_pos: 1, // z = 20 /// index_col_pos: 1, // z /// sort_order: SortOrder::Asc, /// }, /// ], /// } /// #[derive(Debug)] pub struct ConstraintUseCandidate { /// The index that may be used to satisfy the constraints. If none, the table's rowid index is used. pub index: Option>, /// References to the constraints that may be used as an access path for the index. /// Refs are sorted by [ConstraintRef::index_col_pos] pub refs: Vec, } #[derive(Debug)] /// A collection of [Constraint]s and their potential [ConstraintUseCandidate]s for a given table. pub struct TableConstraints { /// The internal ID of the [TableReference] that these constraints are for. pub table_id: TableInternalId, /// The constraints for the table, i.e. any [WhereTerm]s that reference columns from this table. pub constraints: Vec, /// Candidates for indexes that may use the constraints to perform a lookup. pub candidates: Vec, } /// Estimate selectivity for IN expressions given the number of values and table row count. fn estimate_in_selectivity(in_list_len: f64, row_count: f64, not: bool) -> f64 { if not { // NOT IN: each value in the list excludes roughly 1/ndv of the rows. // Without ANALYZE stats we don't know ndv, so we use the equality // selectivity heuristic (sel_eq_unindexed = 0.1) per excluded value. // This gives NOT IN (v1,v2,v3) ≈ (1 - 0.1)^3 ≈ 0.729, which is a // reasonable estimate that the filter does meaningful work. let per_value_sel = 0.1_f64; // matches sel_eq_unindexed default (1.0 - per_value_sel).powf(in_list_len).max(0.01) } else { (in_list_len / row_count).min(1.0) } } /// Estimate the selectivity of a constraint based on the operator, column type, and ANALYZE stats. /// /// When ANALYZE stats are available, we use: /// - For unique/PK columns: 1 / row_count (one row expected per lookup) /// - For non-unique indexed columns: uses index stats to find avg rows per distinct value /// /// The sqlite_stat1 format stores: total_rows, avg_rows_per_key_col1, avg_rows_per_key_col1_col2, ... /// So selectivity = avg_rows_per_key / total_rows /// /// Falls back to hardcoded estimates when stats are unavailable. #[allow(clippy::too_many_arguments)] fn estimate_selectivity( schema: &Schema, table_name: &str, column: Option<&Column>, column_pos: Option, available_indexes: &HashMap>>, op: ConstraintOperator, params: &CostModelParams, is_rowid: bool, ) -> f64 { // Get ANALYZE stats for this table if available let table_stats = schema.analyze_stats.table_stats(table_name); let row_count = table_stats.and_then(|s| s.row_count).unwrap_or(0); match op { ConstraintOperator::AstNativeOperator(ast::Operator::Equals) => { let is_pk_or_rowid_alias = is_rowid || column.is_some_and(|c| c.is_rowid_alias() || c.primary_key()); let selectivity_when_unique = if row_count > 0 { 1.0 / row_count as f64 } else { // Fallback: use hardcoded estimate based on expected table size 1.0 / params.rows_per_table_fallback }; if is_pk_or_rowid_alias { selectivity_when_unique } else if let Some(col_pos) = column_pos { // For non-unique columns, find an index containing this column and use its stats if let Some(indexes) = available_indexes.get(table_name) { for index in indexes { // Check if this index has our column as its first column // (selectivity is most accurate when column is leftmost in index) if let Some(idx_col_pos) = index.column_table_pos_to_index_pos(col_pos) { // Only use stats if column is first in index (idx_col_pos == 0) // because that's when the distinct count is most useful if idx_col_pos == 0 { // Only use unique selectivity for single-column unique indexes. // For composite unique indexes like tpc-h (l_orderkey, l_linenumber), // the first column alone is NOT unique. if index.unique && index.columns.len() == 1 { return selectivity_when_unique; } if let Some(stats) = table_stats { if let Some(idx_stat) = stats.index_stats.get(&index.name) { if let (Some(total), Some(&avg_rows)) = ( idx_stat.total_rows, idx_stat.avg_rows_per_distinct_prefix.first(), ) { if total > 0 && avg_rows > 0 { // selectivity = avg_rows_per_key / total_rows return avg_rows as f64 / total as f64; } } } } else { return params.sel_eq_indexed; } } } } } // Fallback: use hardcoded selectivity for non-indexed columns // Don't scale by row_count - keep it distinct from PK selectivity params.sel_eq_unindexed } else { params.sel_eq_unindexed } } ConstraintOperator::AstNativeOperator(ast::Operator::Greater) | ConstraintOperator::AstNativeOperator(ast::Operator::GreaterEquals) | ConstraintOperator::AstNativeOperator(ast::Operator::Less) | ConstraintOperator::AstNativeOperator(ast::Operator::LessEquals) => params.sel_range, ConstraintOperator::AstNativeOperator(ast::Operator::Is) => params.sel_is_null, ConstraintOperator::AstNativeOperator(ast::Operator::IsNot) => params.sel_is_not_null, ConstraintOperator::Like { not: false } => params.sel_like, ConstraintOperator::Like { not: true } => params.sel_not_like, ConstraintOperator::In { not, estimated_values, } => estimate_in_selectivity(estimated_values, row_count as f64, not), _ => params.sel_other, } } #[allow(clippy::too_many_arguments)] /// Estimate selectivity for a single WHERE/ON constraint applied to `table_reference`. fn estimate_constraint_selectivity( schema: &Schema, table_reference: &JoinedTable, column: Option<&Column>, column_pos: Option, operator: ConstraintOperator, available_indexes: &HashMap>>, params: &CostModelParams, is_rowid: bool, ) -> f64 { estimate_selectivity( schema, table_reference.table.get_name(), column, column_pos, available_indexes, operator, params, is_rowid, ) } fn expression_matches_table( expr: &ast::Expr, table_reference: &JoinedTable, table_references: &TableReferences, subqueries: &[NonFromClauseSubquery], ) -> bool { match table_mask_from_expr(expr, table_references, subqueries) { Ok(mask) => table_references .joined_tables() .iter() .position(|t| t.internal_id == table_reference.internal_id) .is_some_and(|idx| mask.contains_table(idx) && mask.table_count() == 1), Err(_) => false, } } /// Precompute all potentially usable [Constraints] from a WHERE clause. /// The resulting list of [TableConstraints] is then used to evaluate the best access methods for various join orders. /// /// This method do not perform much filtering of constraints and delegate this tasks to the consumers of the method /// Consumers must inspect [TableConstraints] and its candidates and pick best constraints for optimized access pub fn constraints_from_where_clause( where_clause: &[WhereTerm], table_references: &TableReferences, available_indexes: &HashMap>>, subqueries: &[NonFromClauseSubquery], schema: &Schema, params: &CostModelParams, ) -> Result> { let mut constraints = Vec::new(); // For each table, collect all the Constraints and all potential index candidates that may use them. for table_reference in table_references.joined_tables() { let rowid_alias_column = table_reference .columns() .iter() .position(|c| c.is_rowid_alias()); let mut cs = TableConstraints { table_id: table_reference.internal_id, constraints: Vec::new(), candidates: available_indexes .get(table_reference.table.get_name()) .map_or(Vec::new(), |indexes| { indexes .iter() // Skip IndexMethod-based indexes (FTS, vector, etc.) - they use // pattern matching rather than btree index scans .filter(|index| index.index_method.is_none()) .map(|index| ConstraintUseCandidate { index: Some(index.clone()), refs: Vec::new(), }) .collect() }), }; // Add a candidate for the rowid index, which is always available when the table has a rowid alias. cs.candidates.push(ConstraintUseCandidate { index: None, refs: Vec::new(), }); for (i, term) in where_clause.iter().enumerate() { // Constraints originating from a LEFT JOIN must always be evaluated in that join's RHS table's loop, // regardless of which tables the constraint references. if let Some(outer_join_tbl) = term.from_outer_join { if outer_join_tbl != table_reference.internal_id { continue; } } // Try to extract as binary expression first if let Some((lhs, operator, rhs)) = as_binary_components(&term.expr)? { // If either the LHS or RHS of the constraint is a column from the table, add the constraint. match lhs { ast::Expr::Column { table, column, .. } => { if *table == table_reference.internal_id { let table_column = &table_reference.table.columns()[*column]; cs.constraints.push(Constraint { where_clause_pos: (i, BinaryExprSide::Rhs), operator, table_col_pos: Some(*column), expr: None, constraining_expr: None, lhs_mask: table_mask_from_expr(rhs, table_references, subqueries)?, selectivity: estimate_constraint_selectivity( schema, table_reference, Some(table_column), Some(*column), operator, available_indexes, params, false, ), usable: true, is_rowid: false, }); } } ast::Expr::RowId { table, .. } => { if *table == table_reference.internal_id { let (col, col_pos) = if let Some(alias) = rowid_alias_column { (Some(&table_reference.table.columns()[alias]), Some(alias)) } else { (None, None) }; cs.constraints.push(Constraint { where_clause_pos: (i, BinaryExprSide::Rhs), operator, table_col_pos: col_pos, expr: None, constraining_expr: None, lhs_mask: table_mask_from_expr(rhs, table_references, subqueries)?, selectivity: estimate_constraint_selectivity( schema, table_reference, col, col_pos, operator, available_indexes, params, true, ), usable: true, is_rowid: true, }); } } _ if expression_matches_table( lhs, table_reference, table_references, subqueries, ) => { let selectivity = estimate_constraint_selectivity( schema, table_reference, None, None, operator, available_indexes, params, false, ); tracing::debug!( table = table_reference.table.get_name(), where_clause_pos = i, operator = ?operator, lhs_mask = ?table_mask_from_expr(rhs, table_references, subqueries)?, selectivity, "expr constraint (lhs matches table)" ); cs.constraints.push(Constraint { where_clause_pos: (i, BinaryExprSide::Rhs), operator, table_col_pos: None, expr: Some(lhs.clone()), constraining_expr: None, lhs_mask: table_mask_from_expr(rhs, table_references, subqueries)?, selectivity, usable: true, is_rowid: false, }); } _ => {} }; match rhs { ast::Expr::Column { table, column, .. } => { if *table == table_reference.internal_id { let table_column = &table_reference.table.columns()[*column]; cs.constraints.push(Constraint { where_clause_pos: (i, BinaryExprSide::Lhs), operator: opposite_cmp_op(operator), table_col_pos: Some(*column), expr: None, constraining_expr: None, lhs_mask: table_mask_from_expr(lhs, table_references, subqueries)?, selectivity: estimate_constraint_selectivity( schema, table_reference, Some(table_column), Some(*column), operator, available_indexes, params, false, ), usable: true, is_rowid: false, }); } } ast::Expr::RowId { table, .. } => { if *table == table_reference.internal_id { let (col, col_pos) = if let Some(alias) = rowid_alias_column { (Some(&table_reference.table.columns()[alias]), Some(alias)) } else { (None, None) }; cs.constraints.push(Constraint { where_clause_pos: (i, BinaryExprSide::Lhs), operator: opposite_cmp_op(operator), table_col_pos: col_pos, expr: None, constraining_expr: None, lhs_mask: table_mask_from_expr(lhs, table_references, subqueries)?, selectivity: estimate_constraint_selectivity( schema, table_reference, col, col_pos, operator, available_indexes, params, true, ), usable: true, is_rowid: true, }); } } _ if expression_matches_table( rhs, table_reference, table_references, subqueries, ) => { let selectivity = estimate_constraint_selectivity( schema, table_reference, None, None, operator, available_indexes, params, false, ); tracing::debug!( table = table_reference.table.get_name(), where_clause_pos = i, operator = ?operator, lhs_mask = ?table_mask_from_expr(lhs, table_references, subqueries)?, selectivity, "expr constraint (rhs matches table)" ); cs.constraints.push(Constraint { where_clause_pos: (i, BinaryExprSide::Lhs), operator: opposite_cmp_op(operator), table_col_pos: None, expr: Some(rhs.clone()), constraining_expr: None, lhs_mask: table_mask_from_expr(lhs, table_references, subqueries)?, selectivity, usable: true, is_rowid: false, }); } _ => {} }; } // IN expressions are handled separately from binary expressions above because: // - as_binary_components returns (&Expr, ConstraintOperator, &Expr) - a single RHS // - InList has Vec as RHS, SubqueryResult has a different structure entirely // - They don't fit the binary expression abstraction without a more complex return type // Handle IN list: col IN (val1, val2, ...) if let ast::Expr::InList { lhs, not, rhs } = &term.expr { let estimated_values = rhs.len() as f64; let mut rhs_mask = TableMask::new(); for rhs_expr in rhs.iter() { rhs_mask |= table_mask_from_expr(rhs_expr, table_references, subqueries)?; } let table_stats = schema .analyze_stats .table_stats(table_reference.table.get_name()); let row_count = table_stats .and_then(|s| s.row_count) .unwrap_or(params.rows_per_table_fallback as u64) as f64; let selectivity = estimate_in_selectivity(estimated_values, row_count, *not); match lhs.as_ref() { ast::Expr::Column { table, column, .. } if *table == table_reference.internal_id => { let is_rowid = rowid_alias_column == Some(*column); cs.constraints.push(Constraint { where_clause_pos: (i, BinaryExprSide::Rhs), operator: ConstraintOperator::In { not: *not, estimated_values, }, table_col_pos: Some(*column), expr: None, constraining_expr: None, lhs_mask: rhs_mask, selectivity, usable: false, // IN uses a separate seek path, not the range-seek model is_rowid, }); } ast::Expr::RowId { table, .. } if *table == table_reference.internal_id => { cs.constraints.push(Constraint { where_clause_pos: (i, BinaryExprSide::Rhs), operator: ConstraintOperator::In { not: *not, estimated_values, }, table_col_pos: rowid_alias_column, expr: None, constraining_expr: None, lhs_mask: rhs_mask, selectivity, usable: false, is_rowid: true, }); } _ => {} } } // Handle IN subquery: col IN (SELECT ...) if let ast::Expr::SubqueryResult { subquery_id, lhs: Some(lhs_expr), not_in, query_type: ast::SubqueryType::In { .. }, } = &term.expr { // Find the subquery to check if it's correlated let subquery = subqueries .iter() .find(|s| s.internal_id == *subquery_id) .expect("subquery not found"); // Only use as constraint if NOT correlated if !subquery.correlated { let estimated_values = params.in_subquery_rows; let table_stats = schema .analyze_stats .table_stats(table_reference.table.get_name()); let row_count = table_stats .and_then(|s| s.row_count) .unwrap_or(params.rows_per_table_fallback as u64) as f64; let selectivity = estimate_in_selectivity(estimated_values, row_count, *not_in); match lhs_expr.as_ref() { ast::Expr::Column { table, column, .. } if *table == table_reference.internal_id => { let is_rowid = rowid_alias_column == Some(*column); cs.constraints.push(Constraint { where_clause_pos: (i, BinaryExprSide::Rhs), operator: ConstraintOperator::In { not: *not_in, estimated_values, }, table_col_pos: Some(*column), expr: None, constraining_expr: None, lhs_mask: TableMask::new(), // non-correlated = no dependencies selectivity, usable: false, // IN uses a separate seek path (consider_in_list_seek) is_rowid, }); } ast::Expr::RowId { table, .. } if *table == table_reference.internal_id => { cs.constraints.push(Constraint { where_clause_pos: (i, BinaryExprSide::Rhs), operator: ConstraintOperator::In { not: *not_in, estimated_values, }, table_col_pos: rowid_alias_column, expr: None, constraining_expr: None, lhs_mask: TableMask::new(), selectivity, usable: false, is_rowid: true, }); } _ => {} } } } } // sort equalities first so that index keys will be properly constructed. // see e.g.: https://www.solarwinds.com/blog/the-left-prefix-index-rule cs.constraints.sort_by(|a, b| { if a.operator == ast::Operator::Equals.into() { Ordering::Less } else if b.operator == ast::Operator::Equals.into() { Ordering::Greater } else { Ordering::Equal } }); // For each constraint we found, add a reference to it for each index that may be able to use it. for (i, constraint) in cs.constraints.iter_mut().enumerate() { // Skip constraints that don't participate in range-seek matching (IN, collation mismatches) if !constraint.usable { continue; } let constrained_column = constraint .table_col_pos .and_then(|pos| table_reference.table.columns().get(pos)); let column_collation = constrained_column.map(|c| c.collation()); let constraining_expr = constraint.get_constraining_expr_ref(where_clause); // Index seek keys must use the same collation as the constrained column. match ( get_collseq_from_expr(constraining_expr, table_references)?, column_collation, ) { (Some(collation), Some(column_collation)) if collation != column_collation => { constraint.usable = false; continue; } _ => {} } if constraint.is_rowid || rowid_alias_column.is_some_and(|p| constraint.table_col_pos == Some(p)) { let rowid_candidate = cs .candidates .iter_mut() .find_map(|candidate| { if candidate.index.is_none() { Some(candidate) } else { None } }) .unwrap(); rowid_candidate.refs.push(ConstraintRef { constraint_vec_pos: i, index_col_pos: 0, sort_order: SortOrder::Asc, }); } for index in available_indexes .get(table_reference.table.get_name()) .unwrap_or(&VecDeque::new()) .iter() .filter(|idx| idx.index_method.is_none()) { if let Some(position_in_index) = match constraint.table_col_pos { Some(pos) => index.column_table_pos_to_index_pos(pos), None => constraint.expr.as_ref().and_then(|e| { let normalized = normalize_expr_for_index_matching(e, table_reference, table_references); index.expression_to_index_pos(&normalized) }), } { turso_assert!( constraint.usable, "constraint collation must match table column collation" ); if let Some(table_col_pos) = constraint.table_col_pos { let constrained_column = &table_reference.table.columns()[table_col_pos]; let table_collation = constrained_column.collation(); let index_collation = index.columns[position_in_index] .collation .unwrap_or_default(); if table_collation != index_collation { continue; } // Custom type columns encode values as blobs. Blob ordering (memcmp) // doesn't necessarily match the custom type's semantic ordering, so // range constraints (>, <, >=, <=) can't use the index. Equality (=) // still works because encoded(A) == encoded(B) iff A == B. if schema .get_type_def( &constrained_column.ty_str, table_reference.table.is_strict(), ) .is_some() && constraint.operator != ast::Operator::Equals.into() { continue; } } if let Some(index_candidate) = cs.candidates.iter_mut().find_map(|candidate| { if candidate.index.as_ref().is_some_and(|i| { Arc::ptr_eq(index, i) && can_use_partial_index(index, where_clause) }) { Some(candidate) } else { None } }) { index_candidate.refs.push(ConstraintRef { constraint_vec_pos: i, index_col_pos: position_in_index, sort_order: index.columns[position_in_index].order, }); } } } } for candidate in cs.candidates.iter_mut() { // Sort by index_col_pos, ascending -- index columns must be consumed in contiguous order. candidate.refs.sort_by_key(|cref| cref.index_col_pos); } cs.candidates.retain(|c| { if let Some(idx) = &c.index { if idx.where_clause.is_some() && c.refs.is_empty() { // prevent a partial index from even being considered as a scan driver. return false; } } true }); constraints.push(cs); } Ok(constraints) } /// A reference to a [Constraint]s in a [TableConstraints] for single column. /// /// This is specialized version of [ConstraintRef] which specifically holds range-like constraints: /// - x = 10 (eq is set) /// - x >= 10, x > 10 (lower_bound is set) /// - x <= 10, x < 10 (upper_bound is set) /// - x > 10 AND x < 20 (both lower_bound and upper_bound are set) /// /// eq, lower_bound and upper_bound holds None or position of the constraint in the [Constraint] array #[derive(Debug, Clone)] pub struct EqConstraintRef { /// Position of the constraint in the [Constraint] array. pub constraint_pos: usize, /// Whether this equality constrains the column to a single value for the /// entire query (true for `col = 5`, false for `t2.x = t1.b` where the /// value changes per outer row in a nested-loop join). pub is_const: bool, } #[derive(Debug, Clone)] pub struct RangeConstraintRef { /// position of the column in the table definition pub table_col_pos: Option, /// position of the column in the index definition pub index_col_pos: usize, /// sort order for the column in the index definition pub sort_order: SortOrder, /// equality constraint pub eq: Option, /// lower bound constraint (either > or >=) pub lower_bound: Option, /// upper bound constraint (either < or <=) pub upper_bound: Option, } #[derive(Debug, Clone)] /// Represent seek range which can be used in query planning to emit range scan over table or index pub struct SeekRangeConstraint { pub sort_order: SortOrder, pub eq: Option<(ast::Operator, ast::Expr, Affinity)>, pub lower_bound: Option<(ast::Operator, ast::Expr, Affinity)>, pub upper_bound: Option<(ast::Operator, ast::Expr, Affinity)>, } impl SeekRangeConstraint { pub fn new_eq(sort_order: SortOrder, eq: (ast::Operator, ast::Expr, Affinity)) -> Self { Self { sort_order, eq: Some(eq), lower_bound: None, upper_bound: None, } } pub fn new_range( sort_order: SortOrder, lower_bound: Option<(ast::Operator, ast::Expr, Affinity)>, upper_bound: Option<(ast::Operator, ast::Expr, Affinity)>, ) -> Self { turso_assert!(lower_bound.is_some() || upper_bound.is_some()); Self { sort_order, eq: None, lower_bound, upper_bound, } } } impl RangeConstraintRef { /// Convert the [RangeConstraintRef] to a [SeekRangeConstraint] usable in a [crate::translate::plan::SeekDef::key]. pub fn as_seek_range_constraint( &self, constraints: &[Constraint], where_clause: &[WhereTerm], referenced_tables: Option<&TableReferences>, ) -> SeekRangeConstraint { if let Some(ref eq) = self.eq { return SeekRangeConstraint::new_eq( self.sort_order, constraints[eq.constraint_pos] .get_constraining_expr(where_clause, referenced_tables), ); } SeekRangeConstraint::new_range( self.sort_order, self.lower_bound .map(|x| constraints[x].get_constraining_expr(where_clause, referenced_tables)), self.upper_bound .map(|x| constraints[x].get_constraining_expr(where_clause, referenced_tables)), ) } } /// Find which [Constraint]s are usable for a given join order. /// Returns a slice of the references to the constraints that are usable. /// A constraint is considered usable for a given table if all of the other tables referenced by the constraint /// are on the left side in the join order relative to the table. /// /// This enforces the normal B-tree prefix rules: /// - usable index columns must form a contiguous prefix starting at column 0 /// - once a prefix column has no usable constraint, later columns cannot be used /// - once a prefix column uses a range constraint, later columns cannot be used /// /// Multiple constraints on the same index column are merged into a single /// [RangeConstraintRef]. Equality wins over range constraints; otherwise we keep /// at most one lower bound and one upper bound for that column. pub fn usable_constraints_for_lhs_mask( constraints: &[Constraint], refs: &[ConstraintRef], lhs_mask: &TableMask, table_idx: usize, ) -> Vec { turso_debug_assert!(refs.is_sorted_by_key(|x| x.index_col_pos)); let mut usable: Vec = Vec::new(); let mut current_required_column_pos = 0; for cref in refs.iter() { let constraint = &constraints[cref.constraint_vec_pos]; let other_side_refers_to_self = constraint.lhs_mask.contains_table(table_idx); if other_side_refers_to_self { // Self-referential constraints cannot seed a lookup, but if they are // on a later index column they also terminate the usable prefix. if cref.index_col_pos != current_required_column_pos { break; } continue; } if !lhs_mask.contains_all(&constraint.lhs_mask) { // Join-dependent constraints are only usable when every referenced // outer table is already on the left side of the join order. As // above, a missing earlier prefix column terminates the prefix. if cref.index_col_pos != current_required_column_pos { break; } continue; } if Some(cref.index_col_pos) == usable.last().map(|x| x.index_col_pos) { // Merge multiple usable constraints for the same index column into a // single equality-or-range group. assert_eq!(cref.sort_order, usable.last().unwrap().sort_order); assert_eq!(cref.index_col_pos, usable.last().unwrap().index_col_pos); assert_eq!( constraints[cref.constraint_vec_pos].table_col_pos, usable.last().unwrap().table_col_pos ); if usable.last().unwrap().eq.is_some() { // An equality already fixes this column exactly, so extra // constraints on the same column do not change the seek shape. continue; } match constraints[cref.constraint_vec_pos] .operator .as_ast_operator() { Some(ast::Operator::Greater) | Some(ast::Operator::GreaterEquals) => { usable.last_mut().unwrap().lower_bound = Some(cref.constraint_vec_pos); } Some(ast::Operator::Less) | Some(ast::Operator::LessEquals) => { usable.last_mut().unwrap().upper_bound = Some(cref.constraint_vec_pos); } _ => {} } continue; } if cref.index_col_pos != current_required_column_pos { // We found a gap in the usable prefix, so later index columns are // not usable for the lookup. break; } if usable.last().is_some_and(|x| x.eq.is_none()) { // The previous prefix column is already a range, so no later column // can participate in the seek key. break; } let operator = constraints[cref.constraint_vec_pos].operator; let table_col_pos = constraints[cref.constraint_vec_pos].table_col_pos; if operator == ast::Operator::Equals.into() && usable .last() .is_some_and(|x| x.table_col_pos == table_col_pos) { // Duplicate equalities on the same column do not expand the usable // prefix or change the seek shape. continue; } let constraint_group = match operator.as_ast_operator() { Some(ast::Operator::Equals) => RangeConstraintRef { table_col_pos, index_col_pos: cref.index_col_pos, sort_order: cref.sort_order, eq: Some(EqConstraintRef { constraint_pos: cref.constraint_vec_pos, is_const: constraints[cref.constraint_vec_pos].lhs_mask.is_empty(), }), lower_bound: None, upper_bound: None, }, Some(ast::Operator::Greater) | Some(ast::Operator::GreaterEquals) => { RangeConstraintRef { table_col_pos, index_col_pos: cref.index_col_pos, sort_order: cref.sort_order, eq: None, lower_bound: Some(cref.constraint_vec_pos), upper_bound: None, } } Some(ast::Operator::Less) | Some(ast::Operator::LessEquals) => RangeConstraintRef { table_col_pos, index_col_pos: cref.index_col_pos, sort_order: cref.sort_order, eq: None, lower_bound: None, upper_bound: Some(cref.constraint_vec_pos), }, _ => continue, }; usable.push(constraint_group); current_required_column_pos += 1; } usable } pub fn usable_constraints_for_join_order<'a>( constraints: &'a [Constraint], refs: &'a [ConstraintRef], join_order: &[JoinOrderMember], ) -> Vec { turso_debug_assert!(refs.is_sorted_by_key(|x| x.index_col_pos)); let table_idx = join_order.last().unwrap().original_idx; let lhs_mask = TableMask::from_table_number_iter( join_order .iter() .take(join_order.len() - 1) .map(|j| j.original_idx), ); usable_constraints_for_lhs_mask(constraints, refs, &lhs_mask, table_idx) } /// Order synthetic key columns for a materialized subquery seek index. /// /// Unlike ordinary index analysis, the ephemeral index does not have a fixed /// on-disk column order, so we can choose one that matches the intended probe /// shape. Equalities come first, followed by columns that are constrained only /// by ranges. Columns that have both equality and range predicates stay in the /// equality prefix; the range side is redundant for key ordering. pub fn ordered_materialized_key_columns(constraints: &[&Constraint]) -> Vec { let mut equality_cols = Vec::new(); let mut range_only_cols = Vec::new(); for constraint in constraints { let Some(col_pos) = constraint.table_col_pos else { continue; }; match constraint.operator.as_ast_operator() { Some(ast::Operator::Equals) => equality_cols.push(col_pos), Some( ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals, ) => range_only_cols.push(col_pos), _ => {} } } equality_cols.sort_unstable(); equality_cols.dedup(); range_only_cols.sort_unstable(); range_only_cols.dedup(); range_only_cols.retain(|col_pos| !equality_cols.contains(col_pos)); let mut ordered = equality_cols; ordered.extend(range_only_cols); ordered } fn can_use_partial_index(index: &Index, query_where_clause: &[WhereTerm]) -> bool { let Some(index_where) = &index.where_clause else { // Full index, always usable return true; }; // Check if query WHERE contains the exact same predicate for term in query_where_clause { if exprs_are_equivalent(&term.expr, index_where.as_ref()) { return true; } } // TODO: do better to determine if we should use partial index false } pub fn convert_to_vtab_constraint( constraints: &[Constraint], join_order: &[JoinOrderMember], ) -> Vec { let table_idx = join_order.last().unwrap().original_idx; let lhs_mask = TableMask::from_table_number_iter( join_order .iter() .take(join_order.len() - 1) .map(|j| j.original_idx), ); constraints .iter() .enumerate() .filter_map(|(i, constraint)| { let table_col_pos = constraint.table_col_pos?; let other_side_refers_to_self = constraint.lhs_mask.contains_table(table_idx); if other_side_refers_to_self { return None; } let all_required_tables_are_on_left_side = lhs_mask.contains_all(&constraint.lhs_mask); to_ext_constraint_op(&constraint.operator).map(|op| ConstraintInfo { column_index: table_col_pos as u32, op, usable: all_required_tables_are_on_left_side, index: i, }) }) .collect() } fn to_ext_constraint_op(op: &ConstraintOperator) -> Option { let ConstraintOperator::AstNativeOperator(op) = op else { return None; }; match op { ast::Operator::Equals => Some(ConstraintOp::Eq), ast::Operator::Less => Some(ConstraintOp::Lt), ast::Operator::LessEquals => Some(ConstraintOp::Le), ast::Operator::Greater => Some(ConstraintOp::Gt), ast::Operator::GreaterEquals => Some(ConstraintOp::Ge), ast::Operator::NotEquals => Some(ConstraintOp::Ne), _ => None, } } fn opposite_cmp_op(op: ConstraintOperator) -> ConstraintOperator { let ConstraintOperator::AstNativeOperator(op_inner) = &op else { return op; }; match op_inner { ast::Operator::Equals => ast::Operator::Equals, ast::Operator::Greater => ast::Operator::Less, ast::Operator::GreaterEquals => ast::Operator::LessEquals, ast::Operator::Less => ast::Operator::Greater, ast::Operator::LessEquals => ast::Operator::GreaterEquals, ast::Operator::NotEquals => ast::Operator::NotEquals, ast::Operator::Is => ast::Operator::Is, ast::Operator::IsNot => ast::Operator::IsNot, _ => panic!("unexpected operator: {op:?}"), } .into() } /// Result of analyzing a single term for multi-index scan potential. /// This is a shared intermediate structure used by both OR and AND analysis. #[derive(Debug)] pub struct AnalyzedTerm { /// The constraint derived from this term. pub constraint: Constraint, /// The best index for this term, if any. pub best_index: Option>, /// Constraint references for this term. pub constraint_refs: Vec, } /// Analyzes a single binary expression to determine if it can use an index. /// /// This is a shared helper for both OR and AND multi-index analysis. /// Returns `Some(AnalyzedTerm)` if the expression is a usable indexed constraint, /// `None` otherwise. #[allow(clippy::too_many_arguments)] pub(crate) fn analyze_binary_term_for_index( expr: &ast::Expr, where_term_idx: usize, table_id: TableInternalId, table_reference: &JoinedTable, indexes: Option<&VecDeque>>, rowid_alias_column: Option, available_indexes: &HashMap>>, table_references: &TableReferences, subqueries: &[NonFromClauseSubquery], schema: &Schema, params: &CostModelParams, ) -> Option { // Try to extract a binary comparison let (lhs, operator, rhs) = as_binary_components(expr).ok().flatten()?; // Check if the operator is usable for index seeks let is_usable_op = matches!( operator.as_ast_operator(), Some( ast::Operator::Equals | ast::Operator::Greater | ast::Operator::GreaterEquals | ast::Operator::Less | ast::Operator::LessEquals ) ); if !is_usable_op { return None; } // Check if this is an indexable constraint on our table let (table_col_pos, constraining_expr, side, is_rowid) = match lhs { ast::Expr::Column { table, column, .. } if *table == table_id => { (Some(*column), rhs.clone(), BinaryExprSide::Rhs, false) } ast::Expr::RowId { table, .. } if *table == table_id => { (rowid_alias_column, rhs.clone(), BinaryExprSide::Rhs, true) } _ => match rhs { ast::Expr::Column { table, column, .. } if *table == table_id => { (Some(*column), lhs.clone(), BinaryExprSide::Lhs, false) } ast::Expr::RowId { table, .. } if *table == table_id => { (rowid_alias_column, lhs.clone(), BinaryExprSide::Lhs, true) } _ => return None, // Doesn't reference our table }, }; // Normalize operator direction so it matches the constrained table column. // Example: `1 > t.b` constrains `t.b < 1`. let operator = if side == BinaryExprSide::Lhs { opposite_cmp_op(operator) } else { operator }; // Find the best index for this constraint let (best_index, constraint_refs) = find_best_index_for_constraint( table_col_pos, operator, indexes, rowid_alias_column, is_rowid, ); // If no index can be used, this term is not indexable if constraint_refs.is_empty() { return None; } let table_column = table_col_pos.and_then(|pos| table_reference.table.columns().get(pos)); let selectivity = estimate_constraint_selectivity( schema, table_reference, table_column, table_col_pos, operator, available_indexes, params, is_rowid, ); let lhs_mask = table_mask_from_expr(&constraining_expr, table_references, subqueries) .unwrap_or_else(|_| TableMask::new()); // Cannot use index seek if the constraining expression references the same table // being scanned, since the expression value varies per row and cannot be evaluated // before the scan (e.g. TYPEOF(b) NOT BETWEEN a AND a where both columns are from // the same table). if let Some(table_pos) = table_references .joined_tables() .iter() .position(|t| t.internal_id == table_id) { if lhs_mask.contains_table(table_pos) { return None; } } // Compute the affinity for the constraining expression let affinity = if let Some(ast_op) = operator.as_ast_operator() { if ast_op.is_comparison() && table_col_pos.is_some() { comparison_affinity(lhs, rhs, Some(table_references), None) } else { Affinity::Blob } } else { Affinity::Blob }; // Store the pre-computed constraining expression for multi-index branches let stored_constraining_expr = operator .as_ast_operator() .map(|ast_op| (ast_op, constraining_expr.clone(), affinity)); let constraint = Constraint { where_clause_pos: (where_term_idx, side), operator, table_col_pos, expr: None, constraining_expr: stored_constraining_expr, lhs_mask, selectivity, usable: true, is_rowid, }; Some(AnalyzedTerm { constraint, best_index, constraint_refs, }) } /// Find the best index for a single constraint. fn find_best_index_for_constraint( table_col_pos: Option, operator: ConstraintOperator, indexes: Option<&VecDeque>>, rowid_alias_column: Option, is_rowid: bool, ) -> (Option>, Vec) { // Handle implicit rowid (no alias column, table_col_pos is None) if is_rowid && table_col_pos.is_none() { let constraint_ref = RangeConstraintRef { table_col_pos: None, index_col_pos: 0, sort_order: SortOrder::Asc, eq: if operator.as_ast_operator() == Some(ast::Operator::Equals) { Some(EqConstraintRef { constraint_pos: 0, is_const: false, }) } else { None }, lower_bound: match operator.as_ast_operator() { Some(ast::Operator::Greater | ast::Operator::GreaterEquals) => Some(0), _ => None, }, upper_bound: match operator.as_ast_operator() { Some(ast::Operator::Less | ast::Operator::LessEquals) => Some(0), _ => None, }, }; return (None, vec![constraint_ref]); } let Some(col_pos) = table_col_pos else { return (None, vec![]); }; // Check rowid index first if this is a rowid constraint if rowid_alias_column == Some(col_pos) { let constraint_ref = RangeConstraintRef { table_col_pos: Some(col_pos), index_col_pos: 0, sort_order: SortOrder::Asc, eq: if operator.as_ast_operator() == Some(ast::Operator::Equals) { Some(EqConstraintRef { constraint_pos: 0, is_const: false, }) } else { None }, lower_bound: match operator.as_ast_operator() { Some(ast::Operator::Greater | ast::Operator::GreaterEquals) => Some(0), _ => None, }, upper_bound: match operator.as_ast_operator() { Some(ast::Operator::Less | ast::Operator::LessEquals) => Some(0), _ => None, }, }; return (None, vec![constraint_ref]); } // Find the best index that has this column as its first column if let Some(indexes) = indexes { for index in indexes.iter().filter(|idx| idx.index_method.is_none()) { if let Some(idx_col_pos) = index.column_table_pos_to_index_pos(col_pos) { // For multi-index OR, we prefer indexes where the constraint column // is the first column (leftmost prefix) if idx_col_pos == 0 { let constraint_ref = RangeConstraintRef { table_col_pos: Some(col_pos), index_col_pos: 0, sort_order: index.columns[0].order, eq: if operator.as_ast_operator() == Some(ast::Operator::Equals) { Some(EqConstraintRef { constraint_pos: 0, is_const: false, }) } else { None }, lower_bound: match operator.as_ast_operator() { Some(ast::Operator::Greater | ast::Operator::GreaterEquals) => Some(0), _ => None, }, upper_bound: match operator.as_ast_operator() { Some(ast::Operator::Less | ast::Operator::LessEquals) => Some(0), _ => None, }, }; return (Some(index.clone()), vec![constraint_ref]); } } } } (None, vec![]) } ================================================ FILE: core/translate/optimizer/cost.rs ================================================ use crate::schema::Index; use crate::stats::AnalyzeStats; use crate::sync::Arc; use crate::translate::optimizer::constraints::RangeConstraintRef; use crate::translate::plan::JoinedTable; use super::constraints::Constraint; use super::cost_params::CostModelParams; /// A simple newtype wrapper over a f64 that represents the cost of an operation. /// /// This is used to estimate the cost of scans, seeks, and joins. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Cost(pub f64); impl std::ops::Add for Cost { type Output = Cost; fn add(self, other: Cost) -> Cost { Cost(self.0 + other.0) } } impl std::ops::Deref for Cost { type Target = f64; fn deref(&self) -> &f64 { &self.0 } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct IndexInfo { pub unique: bool, pub column_count: usize, /// Whether the index satisfies the query without table lookups. /// True for genuinely covering indexes and for multi-index branches /// that only harvest rowids into a RowSet. pub covering: bool, /// Estimated rows per index leaf page, derived from the column count /// ratio between the index and its parent table. pub rows_per_leaf_page: f64, } /// Estimate rows per index leaf page based on the index/table width ratio. /// Narrower indexes have smaller entries, fitting more rows per page. pub fn index_leaf_rows_per_page( index_column_count: usize, table_column_count: usize, has_rowid_alias: bool, rows_per_table_page: f64, ) -> f64 { // Table width: all columns + implicit rowid (unless one column IS the rowid alias). let table_width = table_column_count as f64 + if has_rowid_alias { 0.0 } else { 1.0 }; // Index width: indexed columns + rowid suffix (always present). let index_width = index_column_count as f64 + 1.0; (rows_per_table_page * table_width / index_width).max(1.0) } /// Compute `rows_per_leaf_page` for an index on the given table. pub fn rows_per_leaf_page_for_index( index_column_count: usize, rhs_table: &JoinedTable, rows_per_table_page: f64, ) -> f64 { let table_column_count = rhs_table.columns().len(); let has_rowid_alias = rhs_table .btree() .is_some_and(|bt| bt.get_rowid_alias_column().is_some()); index_leaf_rows_per_page( index_column_count, table_column_count, has_rowid_alias, rows_per_table_page, ) } /// Estimate IO and CPU cost for a full table scan. /// /// # Arguments /// * `base_row_count` - Total rows in the table /// * `num_scans` - Number of times we scan the table (e.g., from outer loop in nested loop join) /// * `params` - Cost model parameters fn estimate_scan_cost(base_row_count: f64, num_scans: f64, params: &CostModelParams) -> Cost { let table_pages = (base_row_count / params.rows_per_table_page).max(1.0); // First scan reads all pages; subsequent scans benefit from caching let io_cost = if num_scans <= 1.0 { table_pages } else { // First scan + discounted cost for subsequent scans table_pages + (num_scans - 1.0) * table_pages * params.cache_reuse_factor }; // CPU cost for processing all rows on each scan let cpu_cost = num_scans * base_row_count * params.cpu_cost_per_row; Cost(io_cost + cpu_cost) } /// Estimate IO and CPU cost for index-based access. /// /// This properly separates the number of B-tree seeks from the number of rows /// returned per seek. A range scan does ONE seek followed by sequential leaf /// page reads, not one seek per row. /// /// # Arguments /// * `base_row_count` - Total rows in the table (for estimating tree depth and page counts) /// * `tree_depth` - B-tree depth (number of pages to traverse per seek) /// * `index_info` - Index properties (covering, unique, etc.) /// * `num_seeks` - Number of B-tree traversals (typically = outer cardinality for joins) /// * `rows_per_seek` - Expected rows returned per seek (1 for point lookup, more for range) /// * `params` - Cost model parameters pub fn estimate_index_cost( base_row_count: f64, tree_depth: f64, index_info: IndexInfo, input_cardinality: f64, rows_per_seek: f64, params: &CostModelParams, ) -> Cost { // Detect full index scan: when rows_per_seek equals base_row_count, we're scanning // the entire index, not seeking to specific positions. let is_full_scan = (rows_per_seek - base_row_count).abs() < 1.0; // Cost of B-tree traversals: each seek traverses tree_depth pages. let seek_cost = if is_full_scan { // Full scan: one seek to start, then sequential reads. // When re-scanned (nested loop inner), first scan is cold, rest are cached. if input_cardinality <= 1.0 { tree_depth } else { tree_depth + (input_cardinality - 1.0) * tree_depth * params.cache_reuse_factor } } else { input_cardinality * tree_depth }; let index_leaf_pages_count = (rows_per_seek / index_info.rows_per_leaf_page).max(1.0); let leaf_scan_cost = if is_full_scan { // Full scan of all leaf pages. Repeated scans benefit from caching. if input_cardinality <= 1.0 { index_leaf_pages_count } else { index_leaf_pages_count + (input_cardinality - 1.0) * index_leaf_pages_count * params.cache_reuse_factor } } else if rows_per_seek <= 1.0 { // Point lookup: the leaf page is the last page of the B-tree traversal, // already counted in seek_cost. 0.0 } else if input_cardinality <= 1.0 { index_leaf_pages_count } else { // Range scan in a nested-loop join: after the first iteration the inner // table's pages are largely in the buffer pool. Apply the same caching // discount as full scans. index_leaf_pages_count + (input_cardinality - 1.0) * index_leaf_pages_count * params.cache_reuse_factor }; // For non-covering indexes, we need to fetch from the table for each row. let table_lookup_cost = if index_info.covering { 0.0 } else { let table_pages_count = (base_row_count / params.rows_per_table_page).max(1.0); let selectivity = rows_per_seek / base_row_count.max(1.0); input_cardinality * selectivity * table_pages_count }; let io_cost = seek_cost + leaf_scan_cost + table_lookup_cost; // CPU cost: key comparisons during seeks + row processing let total_rows = input_cardinality * rows_per_seek; let cpu_cost = input_cardinality * params.cpu_cost_per_seek + total_rows * params.cpu_cost_per_row; Cost((io_cost + cpu_cost - params.index_bonus).max(0.001)) } pub(crate) fn is_unique_point_lookup( index_info: IndexInfo, usable_constraint_refs: &[RangeConstraintRef], ) -> bool { let eq_count = usable_constraint_refs .iter() .take_while(|cref| cref.eq.is_some()) .count(); index_info.unique && eq_count >= index_info.column_count } #[derive(Debug, Clone, Copy, PartialEq)] pub enum RowCountEstimate { HardcodedFallback(f64), AnalyzeStats(f64), } impl RowCountEstimate { /// Create a hardcoded fallback using the given params. pub fn hardcoded_fallback(params: &CostModelParams) -> Self { RowCountEstimate::HardcodedFallback(params.rows_per_table_fallback) } } impl std::ops::Deref for RowCountEstimate { type Target = f64; fn deref(&self) -> &f64 { match self { RowCountEstimate::HardcodedFallback(val) => val, RowCountEstimate::AnalyzeStats(val) => val, } } } /// ANALYZE-based context for cost estimation. /// Uses sqlite_stat1 histogram data for row estimates when available, /// otherwise falls through to heuristic selectivity multipliers. pub struct AnalyzeCtx<'a> { pub rhs_table: &'a JoinedTable, pub index: Option<&'a Arc>, pub stats: &'a AnalyzeStats, } pub(crate) fn estimate_rows_per_seek( index_info: IndexInfo, constraints: &[Constraint], usable_constraint_refs: &[RangeConstraintRef], base_row_count: RowCountEstimate, analyze_ctx: Option<&AnalyzeCtx>, ) -> f64 { if is_unique_point_lookup(index_info, usable_constraint_refs) { return 1.0; } if let Some(ctx) = analyze_ctx { if !usable_constraint_refs.is_empty() { if let Some(eq_prefix_rows) = estimate_rows_from_analyze_stats(ctx, usable_constraint_refs) { // Apply range selectivity for any trailing non-equality constraints // beyond the equality prefix. SQLite does this via whereRangeAdjust // which divides by ~4 per range bound. let eq_prefix_len = usable_constraint_refs .iter() .take_while(|cref| cref.eq.is_some()) .count(); let range_selectivity: f64 = usable_constraint_refs[eq_prefix_len..] .iter() .map(|cref| { let mut sel = 1.0; if let Some(lb) = cref.lower_bound { sel *= constraints[lb].selectivity; } if let Some(ub) = cref.upper_bound { sel *= constraints[ub].selectivity; } sel }) .product(); return (eq_prefix_rows * range_selectivity).max(1.0); } } } let selectivity_multiplier: f64 = usable_constraint_refs .iter() .map(|cref| { if let Some(ref eq) = cref.eq { return constraints[eq.constraint_pos].selectivity; } let mut selectivity = 1.0; if let Some(lower_bound) = cref.lower_bound { selectivity *= constraints[lower_bound].selectivity; } if let Some(upper_bound) = cref.upper_bound { selectivity *= constraints[upper_bound].selectivity; } selectivity }) .product(); (selectivity_multiplier * *base_row_count).max(1.0) } /// Estimate rows per seek using ANALYZE stats (sqlite_stat1 histogram data). /// Returns `None` when no actual stats exist for this index, signaling the /// caller to fall back to selectivity-based estimation. fn estimate_rows_from_analyze_stats( ctx: &AnalyzeCtx, constraint_refs: &[RangeConstraintRef], ) -> Option { let index = ctx.index?; // Only count leading equality-constrained columns. ANALYZE stats give // avg rows per distinct prefix value, which is only meaningful for // equality lookups. A range constraint (>, <, >=, <=) scans a portion // of the index rather than fixing a prefix value, so it should not // consume a prefix position in the stats lookup. let eq_prefix_len = constraint_refs .iter() .take_while(|cref| cref.eq.is_some()) .count(); if eq_prefix_len == 0 { // Pure range scan — ANALYZE per-distinct-value stats don't apply. return None; } let table_name = ctx.rhs_table.table.get_name(); let table_stats = ctx.stats.table_stats(table_name)?; let idx_stats = table_stats.index_stats.get(&index.name)?; if eq_prefix_len <= idx_stats.avg_rows_per_distinct_prefix.len() { Some(idx_stats.avg_rows_per_distinct_prefix[eq_prefix_len - 1] as f64) } else { // Stats exist for this index but don't cover the equality prefix length. // Return None to fall through to selectivity-based estimation. None } } /// Estimate the cost of a scan or seek operation. #[expect(clippy::too_many_arguments)] pub fn estimate_cost_for_scan_or_seek( index_info: Option, constraints: &[Constraint], usable_constraint_refs: &[RangeConstraintRef], input_cardinality: f64, base_row_count: RowCountEstimate, is_index_ordered: bool, params: &CostModelParams, analyze_ctx: Option<&AnalyzeCtx>, ) -> Cost { let base_row_count = *base_row_count; let tree_depth = if base_row_count <= 1.0 { 1.0 } else { (base_row_count.ln() / params.rows_per_table_page.ln()) .ceil() .max(1.0) }; let Some(index_info) = index_info else { // Full table scan (no index) return estimate_scan_cost(base_row_count, input_cardinality, params); }; if is_unique_point_lookup(index_info, usable_constraint_refs) { // Unique point lookup: 1 seek per input row, 1 row returned per seek return estimate_index_cost( base_row_count, tree_depth, index_info, input_cardinality, // num_seeks = outer cardinality 1.0, // rows_per_seek = 1 for unique point lookup params, ); } let rows_per_seek = estimate_rows_per_seek( index_info, constraints, usable_constraint_refs, RowCountEstimate::AnalyzeStats(base_row_count), analyze_ctx, ); let base_cost = estimate_index_cost( base_row_count, tree_depth, index_info, input_cardinality, // num_seeks = outer cardinality rows_per_seek, params, ); let is_full_scan = usable_constraint_refs.is_empty(); // Penalize non-covering indexes doing full scans when not ordered by the index. // Without ordering benefit, a full scan on a non-covering index requires random // table lookups for each row, which is expensive. if !index_info.covering && is_full_scan && !is_index_ordered { // Full index scan without ordering benefit - prefer table scan instead Cost(base_cost.0 * 2.0) } else { base_cost } } ================================================ FILE: core/translate/optimizer/cost_params.rs ================================================ /// Cost model parameters for query optimization. /// /// These parameters control the heuristics used by the query optimizer for /// cost estimation. They can be tuned to improve plan selection for specific /// workloads (e.g., TPC-H). /// /// # JSON Loading (requires `optimizer_params` feature) /// /// When the `optimizer_params` feature is enabled, parameters can be loaded /// from a JSON file via the `TURSO_OPTIMIZER_PARAMS` environment variable. /// The JSON file does not need to specify all fields, and unspecified fields will use the default values. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct CostModelParams { // === Cardinality Fallbacks (when no ANALYZE stats) === /// Assumed rows per table when statistics unavailable. pub rows_per_table_fallback: f64, /// Estimated rows per table B-tree page. pub rows_per_table_page: f64, // === Selectivity Fallbacks === /// Selectivity for equality predicate on unindexed column (e.g., `x = 5`). pub sel_eq_unindexed: f64, /// Selectivity for equality predicate on indexed column. /// Should be <= sel_eq_unindexed since indexes imply higher selectivity. pub sel_eq_indexed: f64, /// Selectivity for range predicates (>, >=, <, <=). pub sel_range: f64, /// Selectivity for IS NULL predicate. pub sel_is_null: f64, /// Selectivity for IS NOT NULL predicate. pub sel_is_not_null: f64, /// Selectivity for LIKE predicate. pub sel_like: f64, /// Selectivity for NOT LIKE predicate. pub sel_not_like: f64, /// Selectivity for other/unknown predicates. pub sel_other: f64, /// Estimated rows from IN subquery when actual count unknown. /// Matches SQLite's estimate (where.c line 3230). pub in_subquery_rows: f64, // === Scan/Seek Cost Weights === /// Discount factor for repeated scans (cache benefit). /// Range: [0, 1). Higher = more cache benefit assumed. pub cache_reuse_factor: f64, /// CPU cost per row processed (relative to page IO = 1.0). pub cpu_cost_per_row: f64, /// CPU cost per index seek (key comparisons). pub cpu_cost_per_seek: f64, /// Bonus subtracted from cost when using an index (encourages index usage). pub index_bonus: f64, // === Sort Cost === /// CPU cost per row for sorting (used in O(n log n) estimate). /// This is used when estimating the cost saved by using an ordered index. pub sort_cpu_per_row: f64, // === Hash Join Cost === /// CPU cost to compute hash of a row. pub hash_cpu_cost: f64, /// CPU cost to insert row into hash table. pub hash_insert_cost: f64, /// CPU cost to probe hash table. pub hash_lookup_cost: f64, /// Estimated bytes per row for hash table spill estimation. pub hash_bytes_per_row: f64, /// Selectivity threshold for hash join build-side materialization. /// Below this threshold, materialization may be beneficial. pub hash_materialize_selectivity_threshold: f64, /// Stricter selectivity threshold for nested hash probe operations. pub hash_nested_probe_selectivity_threshold: f64, // === Join Optimization === /// Selectivity heuristic factor for closed ranges (e.g., `x > 5 AND x < 10`). /// Applied when both lower and upper bounds exist on an index column. pub closed_range_selectivity_factor: f64, } impl CostModelParams { /// Create default parameters as a const fn (for compile-time static). pub const fn new() -> Self { Self { // Cardinality fallbacks rows_per_table_fallback: 1_000_000.0, rows_per_table_page: 50.0, // Selectivity fallbacks sel_eq_unindexed: 0.1, sel_eq_indexed: 0.001, sel_range: 0.4, sel_is_null: 0.1, sel_is_not_null: 0.9, sel_like: 0.2, sel_not_like: 0.2, sel_other: 0.9, in_subquery_rows: 25.0, // Scan/Seek costs cache_reuse_factor: 0.2, cpu_cost_per_row: 0.003, cpu_cost_per_seek: 0.01, index_bonus: 0.5, // Sort costs sort_cpu_per_row: 0.002, // Hash join specific costs and thresholds hash_cpu_cost: 0.001, hash_insert_cost: 0.002, hash_lookup_cost: 0.003, hash_bytes_per_row: 100.0, hash_materialize_selectivity_threshold: 0.5, hash_nested_probe_selectivity_threshold: 0.15, // Join optimization closed_range_selectivity_factor: 0.2, } } } /// Compile-time static default parameters (zero runtime overhead). #[cfg(any( not(feature = "optimizer_params"), all(test, feature = "optimizer_params") ))] pub static DEFAULT_PARAMS: CostModelParams = CostModelParams::new(); impl Default for CostModelParams { fn default() -> Self { Self::new() } } impl CostModelParams { /// Load parameters from a JSON file. /// /// Returns default parameters if the file cannot be read, parsed, or validated. #[cfg(feature = "optimizer_params")] pub fn load_from_file(path: &std::path::Path) -> Self { match std::fs::read_to_string(path) { Ok(contents) => match serde_json::from_str::(&contents) { Ok(params) => { if let Err(e) = params.validate() { tracing::warn!(?path, error = %e, "Invalid cost params, using defaults"); return Self::default(); } tracing::info!(?path, "Loaded optimizer cost parameters from file"); params } Err(e) => { tracing::warn!(?path, error = %e, "Failed to parse cost params JSON, using defaults"); Self::default() } }, Err(e) => { tracing::warn!(?path, error = %e, "Failed to read cost params file, using defaults"); Self::default() } } } /// Load parameters from the `TURSO_OPTIMIZER_PARAMS` environment variable path, /// or return defaults if not set or loading fails. #[cfg(feature = "optimizer_params")] fn from_env_or_default() -> Self { match std::env::var("TURSO_OPTIMIZER_PARAMS") { Ok(path) => Self::load_from_file(std::path::Path::new(&path)), Err(_) => Self::default(), } } } /// Lazily-loaded parameters from `TURSO_OPTIMIZER_PARAMS` env var (cached process-wide). /// Falls back to defaults if env var not set or loading fails. #[cfg(feature = "optimizer_params")] pub static LOADED_PARAMS: std::sync::LazyLock = std::sync::LazyLock::new(CostModelParams::from_env_or_default); #[cfg(feature = "optimizer_params")] impl CostModelParams { /// Validate that parameters are within sensible bounds. /// /// Returns an error message if any parameter is invalid. #[cfg(feature = "optimizer_params")] pub fn validate(&self) -> Result<(), String> { // Selectivity must be in (0, 1] let selectivity_params = [ ("sel_eq_unindexed", self.sel_eq_unindexed), ("sel_eq_indexed", self.sel_eq_indexed), ("sel_range", self.sel_range), ("sel_is_null", self.sel_is_null), ("sel_is_not_null", self.sel_is_not_null), ("sel_like", self.sel_like), ("sel_not_like", self.sel_not_like), ("sel_other", self.sel_other), ]; for (name, val) in selectivity_params { if val <= 0.0 || val > 1.0 { return Err(format!("{name} must be in (0, 1], got {val}")); } } // Indexed selectivity should be <= unindexed (indexes are more selective) if self.sel_eq_indexed > self.sel_eq_unindexed { return Err(format!( "sel_eq_indexed ({}) should be <= sel_eq_unindexed ({})", self.sel_eq_indexed, self.sel_eq_unindexed )); } // Positive value checks if self.rows_per_table_fallback <= 0.0 { return Err("rows_per_table_fallback must be positive".into()); } if self.rows_per_table_page <= 0.0 { return Err("rows_per_table_page must be positive".into()); } if self.in_subquery_rows <= 0.0 { return Err("in_subquery_rows must be positive".into()); } // Cache reuse factor must be in [0, 1) if self.cache_reuse_factor < 0.0 || self.cache_reuse_factor >= 1.0 { return Err(format!( "cache_reuse_factor must be in [0, 1), got {}", self.cache_reuse_factor )); } // Cost multipliers must be non-negative let cost_params = [ ("cpu_cost_per_row", self.cpu_cost_per_row), ("cpu_cost_per_seek", self.cpu_cost_per_seek), ("sort_cpu_per_row", self.sort_cpu_per_row), ("hash_cpu_cost", self.hash_cpu_cost), ("hash_insert_cost", self.hash_insert_cost), ("hash_lookup_cost", self.hash_lookup_cost), ]; for (name, val) in cost_params { if val < 0.0 { return Err(format!("{name} must be non-negative, got {val}")); } } Ok(()) } } #[cfg(all(test, feature = "optimizer_params"))] mod tests { use super::*; #[test] fn test_default_params_are_valid() { let params = CostModelParams::default(); assert!(params.validate().is_ok()); } #[test] fn test_invalid_selectivity_rejected() { let mut params = CostModelParams { sel_eq_unindexed: 1.5, ..Default::default() }; assert!(params.validate().is_err()); params = CostModelParams { sel_range: 0.0, ..Default::default() }; assert!(params.validate().is_err()); params = CostModelParams { sel_is_null: -0.1, ..Default::default() }; assert!(params.validate().is_err()); } #[test] fn test_indexed_selectivity_constraint() { let params = CostModelParams { sel_eq_indexed: 0.5, sel_eq_unindexed: 0.1, ..Default::default() }; assert!(params.validate().is_err()); } #[test] fn test_cache_reuse_bounds() { let mut params = CostModelParams { cache_reuse_factor: 1.0, ..Default::default() }; assert!(params.validate().is_err()); params.cache_reuse_factor = -0.1; assert!(params.validate().is_err()); params.cache_reuse_factor = 0.99; assert!(params.validate().is_ok()); } #[cfg(feature = "serde")] #[test] fn test_serde_roundtrip() { let params = CostModelParams::default(); let json = serde_json::to_string(¶ms).unwrap(); let parsed: CostModelParams = serde_json::from_str(&json).unwrap(); assert!((params.sel_eq_unindexed - parsed.sel_eq_unindexed).abs() < f64::EPSILON); } #[cfg(feature = "serde")] #[test] fn test_partial_json_uses_defaults() { let defaults = CostModelParams::new(); let json = r#"{"sel_eq_unindexed": 0.05}"#; let params: CostModelParams = serde_json::from_str(json).unwrap(); assert!((params.sel_eq_unindexed - 0.05).abs() < f64::EPSILON); // Other fields should be defaults assert!((params.sel_range - defaults.sel_range).abs() < f64::EPSILON); } #[test] fn test_load_from_file() { let dir = std::env::temp_dir(); let path = dir.join("test_cost_params.json"); let defaults = CostModelParams::new(); // Write a partial JSON file - unspecified fields should use defaults let json = r#"{ "sel_eq_unindexed": 0.15, "sel_eq_indexed": 0.005, "rows_per_table_fallback": 500000.0 }"#; std::fs::write(&path, json).unwrap(); let params = CostModelParams::load_from_file(&path); // Specified values should be loaded assert!((params.sel_eq_unindexed - 0.15).abs() < f64::EPSILON); assert!((params.sel_eq_indexed - 0.005).abs() < f64::EPSILON); assert!((params.rows_per_table_fallback - 500000.0).abs() < f64::EPSILON); // Unspecified values should be defaults assert!((params.sel_range - defaults.sel_range).abs() < f64::EPSILON); assert!((params.rows_per_table_page - defaults.rows_per_table_page).abs() < f64::EPSILON); std::fs::remove_file(&path).ok(); } #[test] fn test_load_from_file_invalid_json_returns_defaults() { let dir = std::env::temp_dir(); let path = dir.join("test_invalid_cost_params.json"); let defaults = CostModelParams::new(); std::fs::write(&path, "not valid json {{{").unwrap(); let params = CostModelParams::load_from_file(&path); // Should return defaults on parse error assert!((params.sel_eq_unindexed - defaults.sel_eq_unindexed).abs() < f64::EPSILON); assert!( (params.rows_per_table_fallback - defaults.rows_per_table_fallback).abs() < f64::EPSILON ); std::fs::remove_file(&path).ok(); } #[test] fn test_load_from_file_missing_returns_defaults() { let path = std::path::Path::new("/nonexistent/path/to/params.json"); let defaults = CostModelParams::new(); let params = CostModelParams::load_from_file(path); // Should return defaults when file doesn't exist assert!((params.sel_eq_unindexed - defaults.sel_eq_unindexed).abs() < f64::EPSILON); assert!( (params.rows_per_table_fallback - defaults.rows_per_table_fallback).abs() < f64::EPSILON ); } } ================================================ FILE: core/translate/optimizer/join.rs ================================================ use std::collections::VecDeque; use std::sync::Arc; use crate::{turso_assert_eq, turso_assert_greater_than}; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use smallvec::SmallVec; use turso_parser::ast::{Expr, Operator, TableInternalId}; use super::{ access_method::{find_best_access_method_for_join_order, AccessMethod}, constraints::TableConstraints, cost_params::CostModelParams, order::OrderTarget, IndexMethodCandidate, }; use crate::{ schema::{Index, Schema}, stats::AnalyzeStats, translate::{ expr::{walk_expr, WalkControl}, optimizer::{ access_method::{ estimate_hash_join_cost, try_hash_join_access_method, AccessMethodParams, ResidualConstraintMode, }, cost::{Cost, RowCountEstimate}, order::plan_satisfies_order_target, }, plan::{ HashJoinKey, HashJoinType, JoinOrderMember, JoinedTable, NonFromClauseSubquery, TableReferences, WhereTerm, }, planner::TableMask, }, LimboError, Result, }; #[derive(Debug, Clone, Copy)] /// Small bag of planner context that needs to flow through join enumeration. /// /// Keeping this as a struct avoids threading more ad-hoc parameters through the /// join planner as we add order-aware access path choices. pub(crate) struct JoinPlanningContext<'a> { pub maybe_order_target: Option<&'a OrderTarget>, } impl<'a> JoinPlanningContext<'a> { /// Convenience constructor used by the default planner entrypoints and tests. #[cfg_attr(not(test), allow(dead_code))] fn default_with_order_target(maybe_order_target: Option<&'a OrderTarget>) -> Self { Self { maybe_order_target } } } // Upper bound on rowids to materialize for a hash build input. // This is a safety limit, not a cost tuning parameter. const MAX_MATERIALIZED_BUILD_ROWS: f64 = 200_000.0; fn constraint_output_multipliers( rhs_constraints: &TableConstraints, lhs_mask: &TableMask, rhs_self_mask: TableMask, consumed_where_terms: &[usize], params: &CostModelParams, ) -> f64 { let mut multiplier = 1.0; let mut bounds: SmallVec<[(Option, bool, bool); 4]> = SmallVec::new(); let record_bound = |bounds: &mut SmallVec<[(Option, bool, bool); 4]>, dominated_col: Option, is_lower: bool, is_upper: bool| { if !(is_lower || is_upper) { return; } if let Some(entry) = bounds.iter_mut().find(|(col, _, _)| *col == dominated_col) { entry.1 |= is_lower; entry.2 |= is_upper; } else { bounds.push((dominated_col, is_lower, is_upper)); } }; for constraint in rhs_constraints.constraints.iter().filter(|constraint| { (lhs_mask.contains_all(&constraint.lhs_mask) || constraint.lhs_mask == rhs_self_mask || constraint.lhs_mask.is_empty()) && !consumed_where_terms.contains(&constraint.where_clause_pos.0) }) { multiplier *= constraint.selectivity; let dominated_col = constraint.table_col_pos; let is_lower = matches!( constraint.operator.as_ast_operator(), Some(Operator::Greater | Operator::GreaterEquals) ); let is_upper = matches!( constraint.operator.as_ast_operator(), Some(Operator::Less | Operator::LessEquals) ); record_bound(&mut bounds, dominated_col, is_lower, is_upper); } for (_, has_lower, has_upper) in &bounds { if *has_lower && *has_upper { multiplier *= params.closed_range_selectivity_factor; } } multiplier } /// Represents an n-ary join, anywhere from 1 table to N tables. #[derive(Debug, Clone)] pub struct JoinN { /// Tuple: (table_number, access_method_index) pub data: Vec<(usize, usize)>, /// The estimated number of rows returned by joining these n tables together. pub output_cardinality: f64, /// Estimated execution cost of this N-ary join. pub cost: Cost, } impl JoinN { pub fn table_numbers(&self) -> impl Iterator + use<'_> { self.data.iter().map(|(table_number, _)| *table_number) } pub fn best_access_methods(&self) -> impl Iterator + use<'_> { self.data .iter() .map(|(_, access_method_index)| *access_method_index) } } /// Join n-1 tables with the n'th table. /// Returns None if the plan is worse than the provided cost upper bound or if no valid access method is found. /// /// Hash-joins: /// - We only consider hash join once there is a non-empty LHS. /// - The build side is the most recently joined table (left-deep hash join); the RHS is the probe. /// - We avoid hash-join shapes that would drop build-side filters unless we can preserve them /// via materialized build rowids. /// - Probe->build chaining is only allowed when the build input is materialized from the /// join prefix; rebuilding from the full table would ignore prior join filters. #[allow(clippy::too_many_arguments)] pub fn join_lhs_and_rhs<'a>( lhs: Option<&JoinN>, initial_input_cardinality: f64, rhs_table_reference: &JoinedTable, rhs_constraints: &'a TableConstraints, all_constraints: &'a [TableConstraints], base_table_rows: &[RowCountEstimate], join_order: &[JoinOrderMember], planning_context: JoinPlanningContext<'_>, access_methods_arena: &'a mut Vec, cost_upper_bound: Cost, joined_tables: &[JoinedTable], where_clause: &mut [WhereTerm], where_term_table_ids: &[HashSet], subqueries: &[NonFromClauseSubquery], index_method_candidates: &[IndexMethodCandidate], params: &CostModelParams, analyze_stats: &AnalyzeStats, available_indexes: &HashMap>>, table_references: &TableReferences, schema: &Schema, ) -> Result> { // The input cardinality for this join is the output cardinality of the previous join. // For example, in a 2-way join, if the left table has 1000 rows, and the right table will return 2 rows for each of the left table's rows, // then the output cardinality of the join will be 2000. let input_cardinality = lhs.map_or(initial_input_cardinality, |l| l.output_cardinality); let rhs_table_number = join_order.last().unwrap().original_idx; let rhs_base_rows = base_table_rows .get(rhs_table_number) .copied() .unwrap_or_else(|| RowCountEstimate::hardcoded_fallback(params)); let Some(mut method) = find_best_access_method_for_join_order( rhs_table_reference, rhs_constraints, join_order, planning_context, where_clause, available_indexes, table_references, subqueries, schema, analyze_stats, input_cardinality, rhs_base_rows, params, )? else { return Ok(None); }; // Check if this access method will trigger ephemeral index creation. if let AccessMethodParams::BTreeTable { index: None, constraint_refs, .. } = &method.params { if constraint_refs.is_empty() { // Check if there are usable constraints that will create an ephemeral index let lhs_mask_for_ephemeral = lhs.map_or_else(TableMask::new, |l| { TableMask::from_table_number_iter(l.table_numbers()) }); let has_usable_constraints = rhs_constraints.constraints.iter().any(|c| { c.usable && c.table_col_pos.is_some() && lhs_mask_for_ephemeral.contains_all(&c.lhs_mask) }); if has_usable_constraints && lhs.is_some() { // Add ephemeral index build cost: scan the table once to build the index // This is similar to the build phase of a hash join let ephemeral_build_cost = *rhs_base_rows * 0.003; method.cost = method.cost + Cost(ephemeral_build_cost); } } } let lhs_cost = lhs.map_or(Cost(0.0), |l| l.cost); // If we have a previous table, consider hash join as an alternative let mut best_access_method = method; // Reuse for hash cost and output cardinality computation let lhs_mask = lhs.map_or_else(TableMask::new, |l| { TableMask::from_table_number_iter(l.table_numbers()) }); // Self-constraints are conditions comparing columns within the same table // (e.g., t.col1 < t.col2). Include them in selectivity since they filter rows. let rhs_self_mask = { let mut m = TableMask::new(); m.add_table(rhs_table_number); m }; let rhs_internal_id = rhs_table_reference.internal_id; let lhs_internal_ids: HashSet = lhs .map(|l| { l.table_numbers() .map(|table_no| joined_tables[table_no].internal_id) .collect() }) .unwrap_or_default(); let has_join_constraint = lhs.is_some() && where_term_table_ids.iter().any(|table_ids| { table_ids.contains(&rhs_internal_id) && table_ids.iter().any(|id| lhs_internal_ids.contains(id)) }); if lhs.is_some() && !has_join_constraint { let rhs_self_constraint_selectivity = build_self_constraint_selectivity(rhs_constraints, rhs_table_number); // Penalize cross products so we don't introduce a table before it can join. let effective_rhs_rows = (*rhs_base_rows) * rhs_self_constraint_selectivity; let cross_cost = (input_cardinality) * effective_rhs_rows; best_access_method.cost = best_access_method.cost + Cost(cross_cost); } // If we already have a non-empty LHS (at least one table has been joined), // consider a hash-join alternative for the current RHS. This is a left-deep // join: the last table in the LHS becomes the build side, and the new RHS // table is the probe side. We only allow hash joins when: // // - The would-be build table is accessed via a scan, or we can preserve its // filters by materializing rowids. // - The probe table is not using a selective index seek we’d prefer to keep. // - The build table has no remaining constraints from prior tables that are // not already consumed as hash-join keys in earlier hash joins. if let Some(lhs) = lhs { let rhs_table_idx = join_order.last().unwrap().original_idx; let last_lhs_table_idx = join_order[join_order.len() - 2].original_idx; let lhs_table_numbers: Vec = lhs.table_numbers().collect(); let rhs_has_selective_seek = matches!( best_access_method.params, AccessMethodParams::BTreeTable { ref constraint_refs, .. } if !constraint_refs.is_empty() ); // The probe table must NOT be the build table of any earlier hash join, // otherwise we would need to re-probe a table that is already being // produced by a hash build. let arena = &access_methods_arena; let probe_table_is_prior_build = lhs.data.iter().any(|(_, am_idx)| { arena.get(*am_idx).is_some_and(|am| { if let AccessMethodParams::HashJoin { build_table_idx, .. } = &am.params { *build_table_idx == rhs_table_idx } else { false } }) }); for build_table_idx in lhs_table_numbers { if build_table_idx != last_lhs_table_idx { continue; } let build_table = &joined_tables[build_table_idx]; let build_has_rowid = build_table.btree().is_some_and(|btree| btree.has_rowid); // If the chosen access method for the build table already uses constraints, // skip hash join to avoid dropping those filters (unless we later decide // to materialize the filtered rowids). let build_access_method_uses_constraints = lhs .data .iter() .find(|(table_no, _)| *table_no == build_table_idx) .map(|(_, am_idx)| *am_idx) .map(|am_idx| { let arena = &access_methods_arena; arena.get(am_idx).is_some_and(|am| { if let AccessMethodParams::BTreeTable { constraint_refs, .. } = &am.params { !constraint_refs.is_empty() } else { false } }) }) .unwrap_or(false); let build_constraints = &all_constraints[build_table_idx]; let build_base_rows = base_table_rows .get(build_table_idx) .copied() .unwrap_or_else(|| RowCountEstimate::hardcoded_fallback(params)); let build_self_selectivity = build_self_constraint_selectivity(build_constraints, build_table_idx); let build_cardinality = (*build_base_rows) * build_self_selectivity; let probe_cardinality = *rhs_base_rows; let prior_mask = lhs_mask.without_table(build_table_idx); let prior_constraint_selectivity = build_prior_constraint_selectivity(build_constraints, &prior_mask); let probe_multiplier = if lhs.data.len() == 1 && lhs.data[0].0 == build_table_idx { 1.0 } else { let join_selectivity = prior_constraint_selectivity.clamp(0.0, 1.0); let denom = (build_cardinality * join_selectivity).max(1.0); (input_cardinality / denom).max(1.0) }; // The build table must NOT have any constraints from prior tables that won't be // consumed as hash-join keys. When a table becomes a hash build table, its // cursor is exhausted after building. If there are constraints like // `prior.x = build.x` that aren't part of the build & probe hash join, they // can't be evaluated because the build cursor is no longer positioned. // // HOWEVER: If the constraint references only tables that are the BUILD side // of earlier hash joins where this proposed build table was the PROBE, then // that equality is already used as a hash-join key. In that case, when we // later SeekRowid into the build table for that earlier join, the cursor is // correctly positioned and the constraint is effectively "consumed". // // Example: // `SELECT... items JOIN products ON items.name = products.name JOIN order_items ON products.price = order_items.price`: // - First hash join: items(build) - products(probe) // - When considering: products(build) - order_items(probe) // - products has constraint from items, BUT items is build of an earlier hash join where products was probe // - So the constraint IS consumed, and products cursor IS positioned via SeekRowid // Get the set of tables that are build tables for hash joins where build_table_idx was probe let tables_already_hash_joined_as_build: Vec = { let arena = &access_methods_arena; lhs.data .iter() .filter_map(|(_, am_idx)| { arena.get(*am_idx).and_then(|am| { if let AccessMethodParams::HashJoin { build_table_idx: prior_build_table_idx, probe_table_idx, .. } = &am.params { if *probe_table_idx == build_table_idx { Some(*prior_build_table_idx) } else { None } } else { None } }) }) .collect() }; let prior_hash_build_mask = TableMask::from_table_number_iter( tables_already_hash_joined_as_build.iter().copied(), ); let build_has_prior_constraints = { build_constraints.constraints.iter().any(|c| { // Check if this constraint references prior tables that are NOT already // handled by a hash join where we were the probe table if !c.lhs_mask.intersects(&prior_mask) { return false; // Constraint doesn't reference prior tables } // Check if ALL referenced prior tables are already hash-joined with us as probe // If so, the constraint is consumed and we're OK for table_idx in 0..64 { if c.lhs_mask.contains_table(table_idx) && prior_mask.contains_table(table_idx) && !tables_already_hash_joined_as_build.contains(&table_idx) { // This prior table is NOT handled by a hash join, constraint not consumed return true; } } false // All referenced prior tables are hash-joined }) }; // If this build table was already a probe in a prior hash join, scanning it again // for a hash build would ignore the prior join filters. The planner disallows // probe-to-build chaining, even with materialization. let build_table_is_prior_probe = lhs.data.iter().any(|(_, am_idx)| { let arena = &access_methods_arena; arena.get(*am_idx).is_some_and(|am| { if let AccessMethodParams::HashJoin { probe_table_idx, .. } = &am.params { *probe_table_idx == build_table_idx } else { false } }) }); // Avoid probe->build chaining across outer-join boundaries. let prefix_has_outer = join_order .iter() .take(join_order.len().saturating_sub(1)) .any(|member| member.is_outer); let chaining_across_outer = build_table_is_prior_probe && prefix_has_outer; // Hash joins are safe only if we won't drop build-side filters: // - If the build scan is unconstrained, we can build directly. // - If there are prior/self constraints, we need materialization to preserve them. // Full index scans are treated as unconstrained for hash-join eligibility. // // We intentionally do NOT (yet) allow a table that is already the probe side of // a hash join to become the build side of another hash join; the second hash join // would rebuild from ALL rows of the middle table, not just the matching rows from the first. let build_am_is_plain_table_scan = lhs .data .iter() .find(|(table_no, _)| *table_no == build_table_idx) .map(|(_, am_idx)| { let arena = &access_methods_arena; arena.get(*am_idx).is_some_and(|am| { matches!( &am.params, AccessMethodParams::BTreeTable { constraint_refs, .. } if constraint_refs.is_empty() ) }) }) .unwrap_or(false); let build_table_is_last = build_table_idx == last_lhs_table_idx; // Eligibility gate: prefer nested-loop when uses a selective probe seek. // Probe->build chaining is only allowed when the // build input is materialized from the join prefix. let allow_hash_join = !rhs_has_selective_seek && !probe_table_is_prior_build && (!build_has_prior_constraints || build_has_rowid) && !chaining_across_outer; tracing::debug!( lhs_table = build_table.table.get_name(), rhs_table = rhs_table_reference.table.get_name(), allow_hash_join, rhs_has_selective_seek, probe_table_is_prior_build, build_table_is_prior_probe, chaining_across_outer, build_am_is_plain_table_scan, build_has_rowid, "hash-join eligibility check" ); if allow_hash_join { let lhs_constraints = build_constraints; if let Some(hash_join_method) = try_hash_join_access_method( build_table, rhs_table_reference, build_table_idx, rhs_table_idx, lhs_constraints, rhs_constraints, where_clause, build_cardinality, probe_cardinality, probe_multiplier, subqueries, params, ) { let mut hash_join_method = hash_join_method; let mut hash_join_allowed = true; let mem_budget = match &hash_join_method.params { AccessMethodParams::HashJoin { mem_budget, .. } => *mem_budget, _ => unreachable!("hash join params expected"), }; if let AccessMethodParams::HashJoin { materialize_build_input, use_bloom_filter, join_keys, .. } = &mut hash_join_method.params { let needs_materialization = build_has_uncovered_prior_constraints( lhs_constraints, join_keys, &prior_mask, &prior_hash_build_mask, ) || build_table_is_prior_probe || !build_table_is_last; let estimated_filtered_rows = (*build_base_rows) * build_self_selectivity * prior_constraint_selectivity; // Hard cap: avoid materializing huge lists when materialization is required. let materialization_too_large = needs_materialization && estimated_filtered_rows > MAX_MATERIALIZED_BUILD_ROWS; let can_materialize = build_has_indexable_prior_constraints(lhs_constraints, &prior_mask); let selectivity_threshold = if probe_multiplier > 1.0 { params.hash_nested_probe_selectivity_threshold } else { params.hash_materialize_selectivity_threshold }; // When probe is nested under prior loops, require stricter selectivity // to justify materialization. let wants_materialization = needs_materialization || (build_access_method_uses_constraints && prior_constraint_selectivity < selectivity_threshold); let optional_materialization_too_large = !needs_materialization && wants_materialization && estimated_filtered_rows > MAX_MATERIALIZED_BUILD_ROWS; // Build eligibility: a plain scan is always safe; otherwise we need // materialization or existing constraints that make the scan selective. let build_is_eligible = build_am_is_plain_table_scan || needs_materialization || build_access_method_uses_constraints; hash_join_allowed = build_is_eligible && (!needs_materialization || build_has_rowid) && !materialization_too_large; if hash_join_allowed { let should_materialize = if needs_materialization { build_has_rowid } else { wants_materialization && build_has_rowid && can_materialize && !optional_materialization_too_large }; let hash_probe_multiplier = if should_materialize { 1.0 } else { probe_multiplier }; let effective_build_cardinality = if should_materialize { estimated_filtered_rows } else { build_cardinality }; // Estimate probe filters that apply only to the probe table itself // (not join predicates) to inform the bloom filter heuristic. let probe_self_selectivity = rhs_constraints .constraints .iter() .filter(|c| c.lhs_mask.is_empty()) .map(|c| c.selectivity) .product::(); let probe_filtered_rows = (*rhs_base_rows) * probe_self_selectivity.clamp(0.0, 1.0); if probe_filtered_rows > 0.0 { let build_filtered_rows = if build_is_eligible { effective_build_cardinality } else { build_cardinality }; // Bloom filters help when the probe side is much larger than the build. *use_bloom_filter = build_filtered_rows > 0.0 && probe_filtered_rows / build_filtered_rows >= 2.0; } else { *use_bloom_filter = false; } if should_materialize { hash_join_method.cost = estimate_hash_join_cost( effective_build_cardinality, probe_cardinality, mem_budget, hash_probe_multiplier, params, ); } if should_materialize { // Materialize build-side rowids so the hash build only includes // rows that already match prior join constraints. *materialize_build_input = true; let materialize_cost = effective_build_cardinality * 0.003; hash_join_method.cost = hash_join_method.cost + Cost(materialize_cost); // When two materialized hash-join plans have equal cost, // prefer the one that filters by earlier prior tables. // // This is a deterministic tie-breaker that nudges the planner // toward chaining a more selective prefix without changing // the primary cost model. let tie_breaker = prior_mask.tables_iter().min().unwrap_or(0) as f64 * 1.0e-6; hash_join_method.cost = hash_join_method.cost + Cost(tie_breaker); } else { *materialize_build_input = false; } tracing::debug!( lhs_table = build_table.table.get_name(), rhs_table = rhs_table_reference.table.get_name(), materialize_build_input = *materialize_build_input, needs_materialization, estimated_filtered_rows, prior_constraint_selectivity, materialization_too_large, can_materialize, build_cardinality, effective_build_cardinality, probe_cardinality, probe_multiplier, hash_probe_multiplier, prior_mask = ?prior_mask, lhs_mask = ?lhs_mask, hash_join_cost = ?hash_join_method.cost, "hash-join candidate" ); } } // FULL OUTER requires hash join for the unmatched-build scan. let is_full_outer = matches!( &hash_join_method.params, AccessMethodParams::HashJoin { join_type: HashJoinType::FullOuter, .. } ); if hash_join_allowed && (is_full_outer || hash_join_method.cost < best_access_method.cost) { best_access_method = hash_join_method; } } } } } // Check if there's an index method candidate for this table (e.g., FTS) // and compare its cost against the current best access method. if let Some(candidate) = index_method_candidates .iter() .find(|c| c.table_idx == rhs_table_number) { if let Some(cost_estimate) = &candidate.cost_estimate { // FTS cost depends on whether it's the outer table (no LHS) or inner table let fts_cost = if lhs.is_none() { // Outer table: FTS cost is fixed Cost(cost_estimate.estimated_cost) } else { // Inner table: FTS cost is multiplied by input cardinality Cost(cost_estimate.estimated_cost * input_cardinality) }; if fts_cost < best_access_method.cost { best_access_method = AccessMethod { cost: fts_cost, estimated_rows_per_outer_row: cost_estimate.estimated_rows as f64, residual_constraints: ResidualConstraintMode::None, consumed_where_terms: candidate.where_covered.into_iter().collect(), params: AccessMethodParams::IndexMethod { query: candidate.to_query(), where_covered: candidate.where_covered, }, }; } } } // FULL OUTER needs a hash join. If the optimizer couldn't pick one, bail. if lhs.is_some() { let is_full_outer = rhs_table_reference .join_info .as_ref() .is_some_and(|ji| ji.is_full_outer()); if is_full_outer && !matches!( best_access_method.params, AccessMethodParams::HashJoin { join_type: HashJoinType::FullOuter, .. } ) { // This ordering can't satisfy FULL OUTER. Let the planner try others. return Ok(None); } } let cost = lhs_cost + best_access_method.cost; if cost > cost_upper_bound { return Ok(None); } // ============================================================================ // OUTPUT CARDINALITY CALCULATION // ============================================================================ // // Formula: output_rows = input_rows × rows_from_access_path × remaining_filter_selectivity. // // Each access method provides its own per-outer-row row estimate for: // - full BTree scans and full index scans // - rowid seeks // - ordinary secondary-index seeks // - multi-index OR/AND scans // - index-method access such as FTS // // Join planning only applies the selectivity of WHERE terms that the chosen // access path did not already consume. // let unconsumed_constraint_multiplier = constraint_output_multipliers( rhs_constraints, &lhs_mask, rhs_self_mask, &best_access_method.consumed_where_terms, params, ); let residual_multiplier = match best_access_method.residual_constraints { ResidualConstraintMode::ApplyUnconsumed => unconsumed_constraint_multiplier, ResidualConstraintMode::None => 1.0, }; let output_cardinality = input_cardinality * best_access_method.estimated_rows_per_outer_row * residual_multiplier; access_methods_arena.push(best_access_method); let mut best_access_methods = Vec::with_capacity(join_order.len()); best_access_methods.extend(lhs.map_or(vec![], |l| l.data.clone())); best_access_methods.push((rhs_table_number, access_methods_arena.len() - 1)); Ok(Some(JoinN { data: best_access_methods, output_cardinality, cost, })) } /// Returns true when build-side constraints reference prior tables in ways that /// are not already consumed by hash-join keys. fn build_has_uncovered_prior_constraints( build_constraints: &TableConstraints, join_keys: &[HashJoinKey], prior_mask: &TableMask, prior_hash_build_mask: &TableMask, ) -> bool { let mut join_key_indices = HashSet::default(); for join_key in join_keys { join_key_indices.insert(join_key.where_clause_idx); } build_constraints.constraints.iter().any(|constraint| { if !constraint.lhs_mask.intersects(prior_mask) { return false; } if join_key_indices.contains(&constraint.where_clause_pos.0) { return false; } if constraint.operator != Operator::Equals.into() { return true; } if !constraint.lhs_mask.intersects(prior_hash_build_mask) { return true; } for table_idx in prior_mask.tables_iter() { if constraint.lhs_mask.contains_table(table_idx) && !prior_hash_build_mask.contains_table(table_idx) { return true; } } false }) } /// Estimates selectivity from prior equality constraints on the build side. fn build_prior_constraint_selectivity( build_constraints: &TableConstraints, prior_mask: &TableMask, ) -> f64 { let mut selectivity = 1.0; let mut saw_constraint = false; for constraint in build_constraints.constraints.iter() { if constraint.operator == Operator::Equals.into() && constraint.lhs_mask.intersects(prior_mask) { tracing::debug!( where_clause_pos = ?constraint.where_clause_pos, lhs_mask = ?constraint.lhs_mask, prior_mask = ?prior_mask, selectivity = constraint.selectivity, "prior constraint selectivity contributor" ); selectivity *= constraint.selectivity; saw_constraint = true; } } if !saw_constraint { return 1.0; } selectivity.clamp(0.0, 1.0) } /// Estimates selectivity from build-side constraints that reference only the build table. fn build_self_constraint_selectivity( build_constraints: &TableConstraints, build_table_idx: usize, ) -> f64 { let build_only_mask = TableMask::from_table_number_iter([build_table_idx].into_iter()); let mut selectivity = 1.0; let mut saw_constraint = false; for constraint in build_constraints.constraints.iter() { if !build_only_mask.contains_all(&constraint.lhs_mask) { continue; } selectivity *= constraint.selectivity; saw_constraint = true; } if !saw_constraint { return 1.0; } selectivity.clamp(0.0, 1.0) } /// Returns true if any prior constraints can be turned into an index lookup. fn build_has_indexable_prior_constraints( build_constraints: &TableConstraints, prior_mask: &TableMask, ) -> bool { build_constraints.candidates.iter().any(|candidate| { candidate.refs.iter().any(|constraint_ref| { let constraint = &build_constraints.constraints[constraint_ref.constraint_vec_pos]; constraint.usable && constraint.lhs_mask.intersects(prior_mask) }) }) } /// The result of [compute_best_join_order]. #[derive(Debug)] pub struct BestJoinOrderResult { /// The best plan overall. pub best_plan: JoinN, /// The best plan for the given order target, if it isn't the overall best. pub best_ordered_plan: Option, } /// Compute the best way to join a given set of tables. /// Returns the best [JoinN] if one exists, otherwise returns None. #[allow(clippy::too_many_arguments)] #[cfg_attr(not(test), allow(dead_code))] pub fn compute_best_join_order<'a>( joined_tables: &[JoinedTable], initial_input_cardinality: f64, maybe_order_target: Option<&OrderTarget>, constraints: &'a [TableConstraints], base_table_rows: &[RowCountEstimate], access_methods_arena: &'a mut Vec, where_clause: &mut [WhereTerm], subqueries: &[NonFromClauseSubquery], index_method_candidates: &[IndexMethodCandidate], params: &CostModelParams, analyze_stats: &AnalyzeStats, available_indexes: &HashMap>>, table_references: &TableReferences, schema: &Schema, ) -> Result> { compute_best_join_order_with_context( joined_tables, initial_input_cardinality, JoinPlanningContext::default_with_order_target(maybe_order_target), constraints, base_table_rows, access_methods_arena, where_clause, subqueries, index_method_candidates, params, analyze_stats, available_indexes, table_references, schema, ) } /// Enumerate join orders while carrying a small amount of planner context that /// influences access-path scoring, such as an order target for sort elimination /// or simple MIN/MAX planning. #[expect(clippy::too_many_arguments)] pub(crate) fn compute_best_join_order_with_context<'a>( joined_tables: &[JoinedTable], initial_input_cardinality: f64, planning_context: JoinPlanningContext<'_>, constraints: &'a [TableConstraints], base_table_rows: &[RowCountEstimate], access_methods_arena: &'a mut Vec, where_clause: &mut [WhereTerm], subqueries: &[NonFromClauseSubquery], index_method_candidates: &[IndexMethodCandidate], params: &CostModelParams, analyze_stats: &AnalyzeStats, available_indexes: &HashMap>>, table_references: &TableReferences, schema: &Schema, ) -> Result> { // Skip work if we have no tables to consider. if joined_tables.is_empty() { return Ok(None); } let num_tables = joined_tables.len(); // For large queries, use greedy join ordering instead of exhaustive DP. // The DP algorithm has O(2^n) complexity which becomes prohibitively slow // beyond ~12 tables. The greedy algorithm is O(n²) and produces good // (though not always optimal) plans. let where_term_table_ids = build_where_term_table_ids(where_clause, joined_tables); if num_tables > GREEDY_JOIN_THRESHOLD { return compute_greedy_join_order( joined_tables, initial_input_cardinality, planning_context, constraints, base_table_rows, access_methods_arena, where_clause, &where_term_table_ids, subqueries, index_method_candidates, params, analyze_stats, available_indexes, table_references, schema, ); } // Compute naive left-to-right plan to use as pruning threshold let naive_plan = compute_naive_left_deep_plan( joined_tables, initial_input_cardinality, planning_context, base_table_rows, access_methods_arena, constraints, where_clause, &where_term_table_ids, subqueries, index_method_candidates, params, analyze_stats, available_indexes, table_references, schema, )?; // Keep track of both 1. the best plan overall (not considering sorting), and 2. the best ordered plan (which might not be the same). // We assign Some Cost (tm) to any required sort operation, so the best ordered plan may end up being // the one we choose, if the cost reduction from avoiding sorting brings it below the cost of the overall best one. let mut best_ordered_plan: Option = None; let mut best_plan_is_also_ordered = match (naive_plan.as_ref(), planning_context.maybe_order_target) { (Some(plan), Some(order_target)) => plan_satisfies_order_target( plan, access_methods_arena, joined_tables, order_target, schema, ), _ => false, }; // If we have one table, then the "naive left-to-right plan" is always the best. if joined_tables.len() == 1 { return match naive_plan { Some(plan) => Ok(Some(BestJoinOrderResult { best_plan: plan, best_ordered_plan: None, })), None => Err(LimboError::PlanningError( "No valid query plan found".to_string(), )), }; } let mut best_plan = naive_plan; // Reuse a single mutable join order to avoid allocating join orders per permutation. let mut join_order = Vec::with_capacity(num_tables); join_order.push(JoinOrderMember { table_id: TableInternalId::default(), original_idx: 0, is_outer: false, }); // Keep track of the current best cost so we can short-circuit planning for subplans // that already exceed the cost of the current best plan. let cost_upper_bound = best_plan.as_ref().map_or(Cost(f64::MAX), |plan| plan.cost); // Keep track of the best plan for a given subset of tables. // Consider this example: we have tables a,b,c,d to join. // if we find that 'b JOIN a' is better than 'a JOIN b', then we don't need to even try // to do 'a JOIN b JOIN c', because we know 'b JOIN a JOIN c' is going to be better. // This is due to the commutativity and associativity of inner joins. // Memo table keyed by a subset mask, then by the last table in the join order. // // We keep multiple plans per subset instead of only the cheapest one. The cheapest // subset plan is not always the best foundation for the next join. Keeping variants // lets the planner choose a better join order later (e.g. for hash-join chaining). let mut best_plan_memo: HashMap> = HashMap::with_capacity_and_hasher(2usize.pow(num_tables as u32 - 1), Default::default()); // Dynamic programming base case: calculate the best way to access each single table, as if // there were no other tables. for i in 0..num_tables { let mut mask = TableMask::new(); mask.add_table(i); let table_ref = &joined_tables[i]; join_order[0] = JoinOrderMember { table_id: table_ref.internal_id, original_idx: i, is_outer: false, }; turso_assert_eq!(join_order.len(), 1); let rel = join_lhs_and_rhs( None, initial_input_cardinality, table_ref, &constraints[i], constraints, base_table_rows, &join_order, planning_context, access_methods_arena, cost_upper_bound, joined_tables, where_clause, &where_term_table_ids, subqueries, index_method_candidates, params, analyze_stats, available_indexes, table_references, schema, )?; if let Some(rel) = rel { best_plan_memo.entry(mask).or_default().insert(i, rel); } } join_order.clear(); // As mentioned, inner joins are commutative. Outer joins are NOT. // Example: // "a LEFT JOIN b" can NOT be reordered as "b LEFT JOIN a". // If there are outer joins in the plan, ensure correct ordering. let left_join_illegal_map = { let ordering_constrained_count = joined_tables .iter() .filter(|t| { t.join_info .as_ref() .is_some_and(|j| j.is_ordering_constrained()) }) .count(); if ordering_constrained_count == 0 { None } else { // map from rhs table index to lhs table index let mut left_join_illegal_map: HashMap = HashMap::with_capacity_and_hasher(ordering_constrained_count, Default::default()); for (i, _) in joined_tables.iter().enumerate() { for (j, joined_table) in joined_tables.iter().enumerate().skip(i + 1) { // LEFT/FULL OUTER, SEMI, and ANTI joins all require the RHS table // to appear after the LHS table in the join order. if joined_table .join_info .as_ref() .is_some_and(|j| j.is_ordering_constrained()) { // bitwise OR the masks if let Some(illegal_lhs) = left_join_illegal_map.get_mut(&i) { illegal_lhs.add_table(j); } else { let mut mask = TableMask::new(); mask.add_table(j); left_join_illegal_map.insert(i, mask); } } } } Some(left_join_illegal_map) } }; // Now that we have our single-table base cases, we can start considering join subsets of 2 tables and more. // Try to join each single table to each other table. for subset_size in 2..=num_tables { for mask in generate_join_bitmasks(num_tables, subset_size) { // Keep track of the best way to join this subset of tables per possible last table. // This preserves alternative join orders that may be more expensive for the subset // but enable cheaper joins when adding more tables. let mut best_for_mask_by_last: HashMap = HashMap::default(); // Also keep track of the best plan for this subset that orders the rows in an // Interesting Way (tm), i.e. allows us to eliminate sort operations downstream. let mut best_ordered_for_mask: Option = None; // Try to join all subsets (masks) with all other tables. // In this block, LHS is always (n-1) tables, and RHS is a single table. for rhs_idx in 0..num_tables { // If the RHS table isn't a member of this join subset, skip. if !mask.contains_table(rhs_idx) { continue; } // If there are no other tables except RHS, skip. let lhs_mask = mask.without_table(rhs_idx); if lhs_mask.is_empty() { continue; } // If this join ordering would violate LEFT JOIN ordering restrictions, skip. if let Some(illegal_lhs) = left_join_illegal_map .as_ref() .and_then(|deps| deps.get(&rhs_idx)) { let legal = !lhs_mask.intersects(illegal_lhs); if !legal { continue; // Don't allow RHS before its LEFT in LEFT JOIN } } let Some(lhs_variants) = best_plan_memo.get(&lhs_mask) else { continue; }; // Stable iteration keeps tie-breaks consistent across runs. let mut lhs_keys: Vec = lhs_variants.keys().copied().collect(); lhs_keys.sort_unstable(); for lhs_key in lhs_keys { let lhs = &lhs_variants[&lhs_key]; // Build a JoinOrder out of the table bitmask under consideration. for table_no in lhs.table_numbers() { join_order.push(JoinOrderMember { table_id: joined_tables[table_no].internal_id, original_idx: table_no, is_outer: joined_tables[table_no] .join_info .as_ref() .is_some_and(|j| j.is_outer()), }); } join_order.push(JoinOrderMember { table_id: joined_tables[rhs_idx].internal_id, original_idx: rhs_idx, is_outer: joined_tables[rhs_idx] .join_info .as_ref() .is_some_and(|j| j.is_outer()), }); turso_assert_eq!(join_order.len(), subset_size); // Calculate the best way to join LHS with RHS. let rel = join_lhs_and_rhs( Some(lhs), initial_input_cardinality, &joined_tables[rhs_idx], &constraints[rhs_idx], constraints, base_table_rows, &join_order, planning_context, access_methods_arena, cost_upper_bound, joined_tables, where_clause, &where_term_table_ids, subqueries, index_method_candidates, params, analyze_stats, available_indexes, table_references, schema, )?; join_order.clear(); let Some(rel) = rel else { continue; }; let satisfies_order_target = if let Some(order_target) = planning_context.maybe_order_target { plan_satisfies_order_target( &rel, access_methods_arena, joined_tables, order_target, schema, ) } else { false }; // If this plan is worse than our overall best, it might still be the best ordered plan. if rel.cost >= cost_upper_bound { // But if it isn't, skip. if !satisfies_order_target { continue; } let existing_ordered_cost: Cost = best_ordered_for_mask .as_ref() .map_or(Cost(f64::MAX), |p: &JoinN| p.cost); if rel.cost < existing_ordered_cost { best_ordered_for_mask = Some(rel); } continue; } let should_replace = match best_for_mask_by_last.get(&rhs_idx) { Some(existing) => rel.cost < existing.cost, None => true, }; if should_replace { best_for_mask_by_last.insert(rhs_idx, rel); } } } let has_all_tables = mask.table_count() == num_tables; if has_all_tables { for rel in best_for_mask_by_last.into_values() { if cost_upper_bound <= rel.cost { continue; } let satisfies_order_target = if let Some(order_target) = planning_context.maybe_order_target { plan_satisfies_order_target( &rel, access_methods_arena, joined_tables, order_target, schema, ) } else { false }; if best_plan.as_ref().is_none_or(|plan| rel.cost < plan.cost) { best_plan = Some(rel); best_plan_is_also_ordered = satisfies_order_target; } } if let Some(rel) = best_ordered_for_mask.take() { let cost = rel.cost; if cost_upper_bound > cost { best_ordered_plan = Some(rel); } } } else if !best_for_mask_by_last.is_empty() { best_plan_memo.insert(mask, best_for_mask_by_last); } } } match best_plan { Some(best_plan) => Ok(Some(BestJoinOrderResult { best_plan, best_ordered_plan: if best_plan_is_also_ordered { None } else { best_ordered_plan }, })), None => { // Give a targeted error for FULL OUTER when no plan was found. let has_full_outer = joined_tables .iter() .any(|t| t.join_info.as_ref().is_some_and(|ji| ji.is_full_outer())); if has_full_outer { // Distinguish chaining from a missing equi-join condition. let build_is_outer = joined_tables.iter().any(|t| { let is_full = t.join_info.as_ref().is_some_and(|ji| ji.is_full_outer()); if !is_full { return false; } // Check if any earlier table (potential build) is also outer. joined_tables.iter().any(|other| { !std::ptr::eq(t, other) && other.join_info.as_ref().is_some_and(|ji| ji.is_outer()) }) }); let has_correlated_subquery = subqueries.iter().any(|sq| sq.correlated); let msg = if build_is_outer { "FULL OUTER JOIN chaining is not yet supported" } else if has_correlated_subquery { "FULL OUTER JOIN is not supported with correlated subqueries that reference the joined tables" } else { "FULL OUTER JOIN requires an equality condition in the ON clause" }; Err(LimboError::ParseError(msg.to_string())) } else { Err(LimboError::PlanningError( "No valid query plan found".to_string(), )) } } } } /// Above this threshold, use greedy O(n²) ordering instead of exhaustive O(2^n) DP. pub const GREEDY_JOIN_THRESHOLD: usize = 12; /// Greedy Operator Ordering (GOO) for join optimization. O(n²) time, O(n) space. /// /// Builds a left-deep join tree by: /// 1. Starting with the table that has best hub score (enables most index lookups) /// 2. Greedily adding the remaining table with lowest marginal cost /// /// Respects outer join ordering constraints. #[allow(clippy::too_many_arguments)] pub fn compute_greedy_join_order<'a>( joined_tables: &[JoinedTable], initial_input_cardinality: f64, planning_context: JoinPlanningContext<'_>, constraints: &'a [TableConstraints], base_table_rows: &[RowCountEstimate], access_methods_arena: &'a mut Vec, where_clause: &mut [WhereTerm], where_term_table_ids: &[HashSet], subqueries: &[NonFromClauseSubquery], index_method_candidates: &[IndexMethodCandidate], params: &CostModelParams, analyze_stats: &AnalyzeStats, available_indexes: &HashMap>>, table_references: &TableReferences, schema: &Schema, ) -> Result> { let num_tables = joined_tables.len(); if num_tables == 0 { return Ok(None); } // Outer join RHS tables require all preceding tables to be joined first. let left_join_deps: HashMap = joined_tables .iter() .enumerate() .filter(|(_, t)| { t.join_info .as_ref() .is_some_and(|ji| ji.is_ordering_constrained()) }) .map(|(j, _)| { let mut required = TableMask::new(); for k in 0..j { required.add_table(k); } (j, required) }) .collect(); let mut remaining: Vec = (0..num_tables).collect(); let mut join_order: Vec = Vec::with_capacity(num_tables); // Pick starting table: prefer tables with high "hub score" (referenced by many constraints). let first_idx = find_best_starting_table(num_tables, constraints, base_table_rows, &left_join_deps); let first_table = &joined_tables[first_idx]; join_order.push(JoinOrderMember { table_id: first_table.internal_id, original_idx: first_idx, is_outer: false, // First table cannot be outer join RHS }); remaining.retain(|&x| x != first_idx); let mut current_plan: Option = join_lhs_and_rhs( None, initial_input_cardinality, first_table, &constraints[first_idx], constraints, base_table_rows, &join_order, planning_context, access_methods_arena, Cost(f64::MAX), joined_tables, where_clause, where_term_table_ids, subqueries, index_method_candidates, params, analyze_stats, available_indexes, table_references, schema, )?; if current_plan.is_none() { return Err(LimboError::PlanningError( "No valid query plan found for first table".to_string(), )); } // Greedily add remaining tables, always picking lowest marginal cost. while !remaining.is_empty() { let current_mask = TableMask::from_table_number_iter(join_order.iter().map(|m| m.original_idx)); // Placeholder for candidate evaluation (avoids cloning) join_order.push(JoinOrderMember::default()); let mut best: Option<(usize, JoinN)> = None; let mut has_connected_candidate = false; for &idx in &remaining { // Outer join RHS requires all preceding tables joined first if let Some(required) = left_join_deps.get(&idx) { if !current_mask.contains_all(required) { continue; } } let connected = where_term_table_ids.iter().any(|table_ids| { let table_id = joined_tables[idx].internal_id; table_ids.contains(&table_id) && current_mask .tables_iter() .map(|table_no| joined_tables[table_no].internal_id) .any(|id| table_ids.contains(&id)) }); if connected { has_connected_candidate = true; break; } } for &idx in &remaining { // Outer join RHS requires all preceding tables joined first if let Some(required) = left_join_deps.get(&idx) { if !current_mask.contains_all(required) { continue; } } if has_connected_candidate { let connected = where_term_table_ids.iter().any(|table_ids| { let table_id = joined_tables[idx].internal_id; table_ids.contains(&table_id) && current_mask .tables_iter() .map(|table_no| joined_tables[table_no].internal_id) .any(|id| table_ids.contains(&id)) }); if !connected { continue; } } let table = &joined_tables[idx]; let last = join_order.last_mut().unwrap(); last.table_id = table.internal_id; last.original_idx = idx; last.is_outer = table.join_info.as_ref().is_some_and(|ji| ji.is_outer()); if let Some(plan) = join_lhs_and_rhs( current_plan.as_ref(), initial_input_cardinality, table, &constraints[idx], constraints, base_table_rows, &join_order, planning_context, access_methods_arena, Cost(f64::MAX), joined_tables, where_clause, where_term_table_ids, subqueries, index_method_candidates, params, analyze_stats, available_indexes, table_references, schema, )? { if best.as_ref().is_none_or(|(_, b)| plan.cost < b.cost) { best = Some((idx, plan)); } } } join_order.pop(); let (next_idx, next_plan) = best.ok_or_else(|| { LimboError::PlanningError("Greedy join ordering: no valid next table".to_string()) })?; let next_table = &joined_tables[next_idx]; join_order.push(JoinOrderMember { table_id: next_table.internal_id, original_idx: next_idx, is_outer: next_table .join_info .as_ref() .is_some_and(|ji| ji.is_outer()), }); remaining.retain(|&x| x != next_idx); current_plan = Some(next_plan); } Ok(Some(BestJoinOrderResult { best_plan: current_plan.expect("loop invariant: current_plan always Some"), best_ordered_plan: None, // Greedy doesn't track ordered variants })) } /// Select starting table for greedy join ordering. /// /// Prefers tables with high "hub score": tables referenced by many other tables' usable /// constraints. Starting with such tables enables index lookups on subsequent joins. /// E.g., in a star schema, the fact table is referenced by all dimension FKs, so /// starting there allows all dimensions to use their PK indexes. /// /// Score = (base_rows * filter_selectivity) / (1 + hub_score) /// Lower score wins. Outer join RHS tables are excluded (have ordering dependencies). fn find_best_starting_table( num_tables: usize, constraints: &[TableConstraints], base_table_rows: &[RowCountEstimate], left_join_deps: &HashMap, ) -> usize { // hub_score[t] = count of usable constraints on OTHER tables that reference t. // If we join t first, each such constraint becomes usable for an index lookup. let mut hub_score = vec![0usize; num_tables]; for (t, tc) in constraints.iter().enumerate() { for c in &tc.constraints { if c.usable && c.table_col_pos.is_some() { for other in (0..num_tables).filter(|&x| x != t && c.lhs_mask.contains_table(x)) { hub_score[other] += 1; } } } } let mut best: Option<(usize, f64)> = None; for t in 0..num_tables { if left_join_deps.contains_key(&t) { continue; // Outer join RHS - cannot be first } let base_rows = *base_table_rows[t]; // Self-constraints compare columns within the same table (e.g., t.col1 < t.col2). let self_mask = { let mut m = TableMask::new(); m.add_table(t); m }; // Include literal constraints (lhs_mask empty) and self-constraints in selectivity let selectivity: f64 = constraints[t] .constraints .iter() .filter(|c| c.lhs_mask.is_empty() || c.lhs_mask == self_mask) .map(|c| c.selectivity) .product(); let score = base_rows * selectivity / (1.0 + hub_score[t] as f64); if best.is_none_or(|(_, s)| score < s) { best = Some((t, score)); } } // Table 0 can never be outer join RHS, so best is always Some. best.expect("no valid starting table").0 } /// Specialized version of [compute_best_join_order] that just joins tables in the order they are given /// in the SQL query. This is used as an upper bound for any other plans -- we can give up enumerating /// permutations if they exceed this cost during enumeration. #[allow(clippy::too_many_arguments)] pub fn compute_naive_left_deep_plan<'a>( joined_tables: &[JoinedTable], initial_input_cardinality: f64, planning_context: JoinPlanningContext<'_>, base_table_rows: &[RowCountEstimate], access_methods_arena: &'a mut Vec, constraints: &'a [TableConstraints], where_clause: &mut [WhereTerm], where_term_table_ids: &[HashSet], subqueries: &[NonFromClauseSubquery], index_method_candidates: &[IndexMethodCandidate], params: &CostModelParams, analyze_stats: &AnalyzeStats, available_indexes: &HashMap>>, table_references: &TableReferences, schema: &Schema, ) -> Result> { let n = joined_tables.len(); turso_assert_greater_than!(n, 0); let join_order = joined_tables .iter() .enumerate() .map(|(i, t)| JoinOrderMember { table_id: t.internal_id, original_idx: i, is_outer: t.join_info.as_ref().is_some_and(|j| j.is_outer()), }) .collect::>(); // Start with first table let mut best_plan = join_lhs_and_rhs( None, initial_input_cardinality, &joined_tables[0], &constraints[0], constraints, base_table_rows, &join_order[..1], planning_context, access_methods_arena, Cost(f64::MAX), joined_tables, where_clause, where_term_table_ids, subqueries, index_method_candidates, params, analyze_stats, available_indexes, table_references, schema, )?; if best_plan.is_none() { return Ok(None); } // Add remaining tables one at a time from left to right for i in 1..n { best_plan = join_lhs_and_rhs( best_plan.as_ref(), initial_input_cardinality, &joined_tables[i], &constraints[i], constraints, base_table_rows, &join_order[..=i], planning_context, access_methods_arena, Cost(f64::MAX), joined_tables, where_clause, where_term_table_ids, subqueries, index_method_candidates, params, analyze_stats, available_indexes, table_references, schema, )?; if best_plan.is_none() { return Ok(None); } } Ok(best_plan) } /// Precompute table IDs referenced by each WHERE term for join-order decisions. fn build_where_term_table_ids( where_clause: &[WhereTerm], joined_tables: &[JoinedTable], ) -> Vec> { let joined_ids: HashSet = joined_tables.iter().map(|t| t.internal_id).collect(); where_clause .iter() .map(|term| expr_table_ids_filtered(&term.expr, &joined_ids)) .collect() } /// Collect table IDs from an expression that belong to the joined tables set. fn expr_table_ids_filtered( expr: &Expr, joined_ids: &HashSet, ) -> HashSet { let mut tables = HashSet::default(); let _ = walk_expr(expr, &mut |node| { match node { Expr::Column { table, .. } | Expr::RowId { table, .. } => { if joined_ids.contains(table) { tables.insert(*table); } } _ => {} } Ok(WalkControl::Continue) }); tables } /// Iterator that generates all possible size k bitmasks for a given number of tables. /// For example, given: 3 tables and k=2, the bitmasks are: /// - 0b011 (tables 0, 1) /// - 0b101 (tables 0, 2) /// - 0b110 (tables 1, 2) /// /// This is used in the dynamic programming approach to finding the best way to join a subset of N tables. struct JoinBitmaskIter { current: u128, max_exclusive: u128, } impl JoinBitmaskIter { fn new(table_number_max_exclusive: usize, how_many: usize) -> Self { Self { current: (1 << how_many) - 1, // Start with smallest k-bit number (e.g., 000111 for k=3) max_exclusive: 1 << table_number_max_exclusive, } } } impl Iterator for JoinBitmaskIter { type Item = TableMask; fn next(&mut self) -> Option { if self.current >= self.max_exclusive { return None; } let result = TableMask::from_bits(self.current); // Gosper's hack: compute next k-bit combination in lexicographic order let c = self.current & (!self.current + 1); // rightmost set bit let r = self.current + c; // add it to get a carry let ones = self.current ^ r; // changed bits let ones = (ones >> 2) / c; // right-adjust shifted bits self.current = r | ones; // form the next combination Some(result) } } /// Generate all possible bitmasks of size `how_many` for a given number of tables. fn generate_join_bitmasks(table_number_max_exclusive: usize, how_many: usize) -> JoinBitmaskIter { JoinBitmaskIter::new(table_number_max_exclusive, how_many) } #[cfg(test)] mod tests { use std::{collections::VecDeque, sync::Arc}; use turso_parser::ast::{self, Expr, Operator, SortOrder, TableInternalId}; use super::*; use crate::{ schema::{BTreeTable, ColDef, Column, Index, IndexColumn, Schema, Table, Type}, stats::AnalyzeStats, translate::{ optimizer::{ access_method::AccessMethodParams, constraints::{constraints_from_where_clause, BinaryExprSide, RangeConstraintRef}, cost_params::DEFAULT_PARAMS, }, plan::{ ColumnUsedMask, IterationDirection, JoinInfo, JoinType, Operation, TableReferences, WhereTerm, }, planner::TableMask, }, vdbe::builder::TableRefIdCounter, }; fn default_base_rows(n: usize) -> Vec { vec![RowCountEstimate::hardcoded_fallback(&DEFAULT_PARAMS); n] } fn empty_schema() -> Schema { Schema::default() } #[test] fn test_generate_bitmasks() { let bitmasks = generate_join_bitmasks(4, 2).collect::>(); assert!(bitmasks.contains(&TableMask(0b110))); // {0,1} -- first bit is always set to 0 so that a Mask with value 0 means "no tables are referenced". assert!(bitmasks.contains(&TableMask(0b1010))); // {0,2} assert!(bitmasks.contains(&TableMask(0b1100))); // {1,2} assert!(bitmasks.contains(&TableMask(0b10010))); // {0,3} assert!(bitmasks.contains(&TableMask(0b10100))); // {1,3} assert!(bitmasks.contains(&TableMask(0b11000))); // {2,3} } #[test] /// Test that [compute_best_join_order] returns None when there are no table references. fn test_compute_best_join_order_empty() { let table_references = TableReferences::new(vec![], vec![]); let available_indexes = HashMap::default(); let mut where_clause = vec![]; let mut access_methods_arena = Vec::new(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let result = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap(); assert!(result.is_none()); } #[test] /// Test that [compute_best_join_order] returns a table scan access method when the where clause is empty. fn test_compute_best_join_order_single_table_no_indexes() { let t1 = _create_btree_table("test_table", _create_column_list(&["id"], Type::Integer)); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = vec![_create_table_reference(t1, None, table_id_counter.next())]; let table_references = TableReferences::new(joined_tables, vec![]); let available_indexes = HashMap::default(); let mut where_clause = vec![]; let mut access_methods_arena = Vec::new(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); // SELECT * from test_table // expecting best_best_plan() not to do any work due to empty where clause. let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let BestJoinOrderResult { best_plan, .. } = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap() .unwrap(); // Should just be a table scan access method let access_method = &access_methods_arena[best_plan.data[0].1]; let (iter_dir, _, constraint_refs) = _as_btree(access_method); assert!(constraint_refs.is_empty()); assert!(iter_dir == IterationDirection::Forwards); } #[test] /// Test that [compute_best_join_order] returns a RowidEq access method when the where clause has an EQ constraint on the rowid alias. fn test_compute_best_join_order_single_table_rowid_eq() { let t1 = _create_btree_table("test_table", vec![_create_column_rowid_alias("id")]); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = vec![_create_table_reference(t1, None, table_id_counter.next())]; let mut where_clause = vec![_create_binary_expr( _create_column_expr(joined_tables[0].internal_id, 0, true), // table 0, column 0 (rowid) ast::Operator::Equals, _create_numeric_literal("42"), )]; let table_references = TableReferences::new(joined_tables, vec![]); let mut access_methods_arena = Vec::new(); let available_indexes = HashMap::default(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); // SELECT * FROM test_table WHERE id = 42 // expecting a RowidEq access method because id is a rowid alias. let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let result = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap(); assert!(result.is_some()); let BestJoinOrderResult { best_plan, .. } = result.unwrap(); assert_eq!(best_plan.table_numbers().collect::>(), vec![0]); let access_method = &access_methods_arena[best_plan.data[0].1]; let (iter_dir, _, constraint_refs) = _as_btree(access_method); assert!(!constraint_refs.is_empty()); assert!(iter_dir == IterationDirection::Forwards); assert!(constraint_refs.len() == 1); assert!( table_constraints[0].constraints [constraint_refs[0].eq.as_ref().unwrap().constraint_pos] .where_clause_pos == (0, BinaryExprSide::Rhs) ); } #[test] /// Test that [compute_best_join_order] returns an IndexScan access method when the where clause has an EQ constraint on a primary key. fn test_compute_best_join_order_single_table_pk_eq() { let t1 = _create_btree_table( "test_table", vec![_create_column_of_type("id", Type::Integer)], ); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = vec![_create_table_reference(t1, None, table_id_counter.next())]; let mut where_clause = vec![_create_binary_expr( _create_column_expr(joined_tables[0].internal_id, 0, false), // table 0, column 0 (id) ast::Operator::Equals, _create_numeric_literal("42"), )]; let table_references = TableReferences::new(joined_tables, vec![]); let mut access_methods_arena = Vec::new(); let mut available_indexes = HashMap::default(); let index = Arc::new(Index { name: "sqlite_autoindex_test_table_1".to_string(), table_name: "test_table".to_string(), where_clause: None, columns: vec![IndexColumn { name: "id".to_string(), order: SortOrder::Asc, pos_in_table: 0, collation: None, default: None, expr: None, }], unique: true, ephemeral: false, root_page: 1, has_rowid: true, index_method: None, on_conflict: None, }); available_indexes.insert("test_table".to_string(), VecDeque::from([index])); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); // SELECT * FROM test_table WHERE id = 42 // expecting an IndexScan access method because id is a primary key with an index let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let result = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap(); assert!(result.is_some()); let BestJoinOrderResult { best_plan, .. } = result.unwrap(); assert_eq!(best_plan.table_numbers().collect::>(), vec![0]); let access_method = &access_methods_arena[best_plan.data[0].1]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(!constraint_refs.is_empty()); assert!(iter_dir == IterationDirection::Forwards); assert!(index.as_ref().unwrap().name == "sqlite_autoindex_test_table_1"); assert!(constraint_refs.len() == 1); assert!( table_constraints[0].constraints [constraint_refs[0].eq.as_ref().unwrap().constraint_pos] .where_clause_pos == (0, BinaryExprSide::Rhs) ); } #[test] /// Test that [compute_best_join_order] moves the outer table to the inner position when an index can be used on it, but not the original inner table. fn test_compute_best_join_order_two_tables() { let t1 = _create_btree_table("table1", _create_column_list(&["id"], Type::Integer)); let t2 = _create_btree_table("table2", _create_column_list(&["id"], Type::Integer)); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = vec![ _create_table_reference(t1, None, table_id_counter.next()), _create_table_reference( t2, Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), ), ]; const TABLE1: usize = 0; const TABLE2: usize = 1; let mut available_indexes = HashMap::default(); // Index on the outer table (table1) let index1 = Arc::new(Index { name: "index1".to_string(), table_name: "table1".to_string(), where_clause: None, columns: vec![IndexColumn { name: "id".to_string(), order: SortOrder::Asc, pos_in_table: 0, collation: None, default: None, expr: None, }], unique: true, ephemeral: false, root_page: 1, has_rowid: true, index_method: None, on_conflict: None, }); available_indexes.insert("table1".to_string(), VecDeque::from([index1])); // SELECT * FROM table1 JOIN table2 WHERE table1.id = table2.id // expecting table2 to be chosen first due to the index on table1.id let mut where_clause = vec![_create_binary_expr( _create_column_expr(joined_tables[TABLE1].internal_id, 0, false), // table1.id ast::Operator::Equals, _create_column_expr(joined_tables[TABLE2].internal_id, 0, false), // table2.id )]; let table_references = TableReferences::new(joined_tables, vec![]); let mut access_methods_arena = Vec::new(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let result = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap(); assert!(result.is_some()); let BestJoinOrderResult { best_plan, .. } = result.unwrap(); assert_eq!(best_plan.table_numbers().collect::>(), vec![1, 0]); let access_method = &access_methods_arena[best_plan.data[0].1]; let (iter_dir, _, constraint_refs) = _as_btree(access_method); assert!(constraint_refs.is_empty()); assert!(iter_dir == IterationDirection::Forwards); let access_method = &access_methods_arena[best_plan.data[1].1]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(!constraint_refs.is_empty()); assert!(iter_dir == IterationDirection::Forwards); assert!(index.as_ref().unwrap().name == "index1"); assert!(constraint_refs.len() == 1); assert!( table_constraints[TABLE1].constraints [constraint_refs[0].eq.as_ref().unwrap().constraint_pos] .where_clause_pos == (0, BinaryExprSide::Rhs) ); } #[test] /// Test that [compute_best_join_order] returns a sensible order and plan for three tables, each with indexes. fn test_compute_best_join_order_three_tables_indexed() { let table_orders = _create_btree_table( "orders", vec![ _create_column_of_type("id", Type::Integer), _create_column_of_type("customer_id", Type::Integer), _create_column_of_type("total", Type::Integer), ], ); let table_customers = _create_btree_table( "customers", vec![ _create_column_of_type("id", Type::Integer), _create_column_of_type("name", Type::Integer), ], ); let table_order_items = _create_btree_table( "order_items", vec![ _create_column_of_type("id", Type::Integer), _create_column_of_type("order_id", Type::Integer), _create_column_of_type("product_id", Type::Integer), _create_column_of_type("quantity", Type::Integer), ], ); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = vec![ _create_table_reference(table_orders, None, table_id_counter.next()), _create_table_reference( table_customers, Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), ), _create_table_reference( table_order_items, Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), ), ]; const TABLE_NO_ORDERS: usize = 0; const TABLE_NO_CUSTOMERS: usize = 1; const TABLE_NO_ORDER_ITEMS: usize = 2; let mut available_indexes = HashMap::default(); ["orders", "customers", "order_items"] .iter() .for_each(|table_name| { // add primary key index called sqlite_autoindex__1 let index_name = format!("sqlite_autoindex_{table_name}_1"); let index = Arc::new(Index { name: index_name, where_clause: None, table_name: table_name.to_string(), columns: vec![IndexColumn { name: "id".to_string(), order: SortOrder::Asc, pos_in_table: 0, collation: None, default: None, expr: None, }], unique: true, ephemeral: false, root_page: 1, has_rowid: true, index_method: None, on_conflict: None, }); available_indexes.insert(table_name.to_string(), VecDeque::from([index])); }); let customer_id_idx = Arc::new(Index { name: "orders_customer_id_idx".to_string(), table_name: "orders".to_string(), where_clause: None, columns: vec![IndexColumn { name: "customer_id".to_string(), order: SortOrder::Asc, pos_in_table: 1, collation: None, default: None, expr: None, }], unique: false, ephemeral: false, root_page: 1, has_rowid: true, index_method: None, on_conflict: None, }); let order_id_idx = Arc::new(Index { name: "order_items_order_id_idx".to_string(), table_name: "order_items".to_string(), where_clause: None, columns: vec![IndexColumn { name: "order_id".to_string(), order: SortOrder::Asc, pos_in_table: 1, collation: None, default: None, expr: None, }], unique: false, ephemeral: false, root_page: 1, has_rowid: true, index_method: None, on_conflict: None, }); available_indexes .entry("orders".to_string()) .and_modify(|v| v.push_front(customer_id_idx)); available_indexes .entry("order_items".to_string()) .and_modify(|v| v.push_front(order_id_idx)); // SELECT * FROM orders JOIN customers JOIN order_items // WHERE orders.customer_id = customers.id AND orders.id = order_items.order_id AND customers.id = 42 // expecting customers to be chosen first due to the index on customers.id and it having a selective filter (=42) // then orders to be chosen next due to the index on orders.customer_id // then order_items to be chosen last due to the index on order_items.order_id let mut where_clause = vec![ // orders.customer_id = customers.id _create_binary_expr( _create_column_expr(joined_tables[TABLE_NO_ORDERS].internal_id, 1, false), // orders.customer_id ast::Operator::Equals, _create_column_expr(joined_tables[TABLE_NO_CUSTOMERS].internal_id, 0, false), // customers.id ), // orders.id = order_items.order_id _create_binary_expr( _create_column_expr(joined_tables[TABLE_NO_ORDERS].internal_id, 0, false), // orders.id ast::Operator::Equals, _create_column_expr(joined_tables[TABLE_NO_ORDER_ITEMS].internal_id, 1, false), // order_items.order_id ), // customers.id = 42 _create_binary_expr( _create_column_expr(joined_tables[TABLE_NO_CUSTOMERS].internal_id, 0, false), // customers.id ast::Operator::Equals, _create_numeric_literal("42"), ), ]; let table_references = TableReferences::new(joined_tables, vec![]); let mut access_methods_arena = Vec::new(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let result = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap(); assert!(result.is_some()); let BestJoinOrderResult { best_plan, .. } = result.unwrap(); // Customers (due to =42 filter) -> Orders (due to index on customer_id) -> Order_items (due to index on order_id) assert_eq!( best_plan.table_numbers().collect::>(), vec![TABLE_NO_CUSTOMERS, TABLE_NO_ORDERS, TABLE_NO_ORDER_ITEMS] ); let access_method = &access_methods_arena[best_plan.data[0].1]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(iter_dir == IterationDirection::Forwards); assert!(index.as_ref().unwrap().name == "sqlite_autoindex_customers_1"); assert!(constraint_refs.len() == 1); let constraint = &table_constraints[TABLE_NO_CUSTOMERS].constraints [constraint_refs[0].eq.as_ref().unwrap().constraint_pos]; assert!(constraint.lhs_mask.is_empty()); let access_method = &access_methods_arena[best_plan.data[1].1]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(iter_dir == IterationDirection::Forwards); assert!(index.as_ref().unwrap().name == "orders_customer_id_idx"); assert!(constraint_refs.len() == 1); let constraint = &table_constraints[TABLE_NO_ORDERS].constraints [constraint_refs[0].eq.as_ref().unwrap().constraint_pos]; assert!(constraint.lhs_mask.contains_table(TABLE_NO_CUSTOMERS)); let access_method = &access_methods_arena[best_plan.data[2].1]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(iter_dir == IterationDirection::Forwards); assert!(index.as_ref().unwrap().name == "order_items_order_id_idx"); assert!(constraint_refs.len() == 1); let constraint = &table_constraints[TABLE_NO_ORDER_ITEMS].constraints [constraint_refs[0].eq.as_ref().unwrap().constraint_pos]; assert!(constraint.lhs_mask.contains_table(TABLE_NO_ORDERS)); } struct TestColumn { name: String, ty: Type, is_rowid_alias: bool, } impl Default for TestColumn { fn default() -> Self { Self { name: "a".to_string(), ty: Type::Integer, is_rowid_alias: false, } } } #[test] fn test_join_order_three_tables_no_indexes() { let t1 = _create_btree_table("t1", _create_column_list(&["id", "foo"], Type::Integer)); let t2 = _create_btree_table("t2", _create_column_list(&["id", "foo"], Type::Integer)); let t3 = _create_btree_table("t3", _create_column_list(&["id", "foo"], Type::Integer)); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = vec![ _create_table_reference(t1, None, table_id_counter.next()), _create_table_reference( t2, Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), ), _create_table_reference( t3, Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), ), ]; let mut where_clause = vec![ // t2.foo = 42 (equality filter, more selective) _create_binary_expr( _create_column_expr(joined_tables[1].internal_id, 1, false), // table 1, column 1 (foo) ast::Operator::Equals, _create_numeric_literal("42"), ), // t1.foo > 10 (inequality filter, less selective) _create_binary_expr( _create_column_expr(joined_tables[0].internal_id, 1, false), // table 0, column 1 (foo) ast::Operator::Greater, _create_numeric_literal("10"), ), ]; let table_references = TableReferences::new(joined_tables, vec![]); let available_indexes = HashMap::default(); let mut access_methods_arena = Vec::new(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let BestJoinOrderResult { best_plan, .. } = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap() .unwrap(); // Verify that t2 is chosen first due to its equality filter assert_eq!(best_plan.table_numbers().next().unwrap(), 1); // Verify table scan is used since there are no indexes let access_method = &access_methods_arena[best_plan.data[0].1]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(constraint_refs.is_empty()); assert!(iter_dir == IterationDirection::Forwards); assert!(index.is_none()); // Verify that t1 is chosen next due to its inequality filter let access_method = &access_methods_arena[best_plan.data[1].1]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(constraint_refs.is_empty()); assert!(iter_dir == IterationDirection::Forwards); assert!(index.is_none()); // Verify that t3 is chosen last due to no filters let access_method = &access_methods_arena[best_plan.data[2].1]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(constraint_refs.is_empty()); assert!(iter_dir == IterationDirection::Forwards); assert!(index.is_none()); } #[test] /// Test that [compute_best_join_order] chooses a "fact table" as the outer table, /// when it has a foreign key to all dimension tables. fn test_compute_best_join_order_star_schema() { const NUM_DIM_TABLES: usize = 9; const FACT_TABLE_IDX: usize = 9; // Create fact table with foreign keys to all dimension tables let mut fact_columns = vec![_create_column_rowid_alias("id")]; for i in 0..NUM_DIM_TABLES { fact_columns.push(_create_column_of_type(&format!("dim{i}_id"), Type::Integer)); } let fact_table = _create_btree_table("fact", fact_columns); // Create dimension tables, each with an id and value column let dim_tables: Vec<_> = (0..NUM_DIM_TABLES) .map(|i| { _create_btree_table( &format!("dim{i}"), vec![ _create_column_rowid_alias("id"), _create_column_of_type("value", Type::Integer), ], ) }) .collect(); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = { let mut refs = vec![_create_table_reference( dim_tables[0].clone(), None, table_id_counter.next(), )]; refs.extend(dim_tables.iter().skip(1).map(|t| { _create_table_reference( t.clone(), Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), ) })); refs.push(_create_table_reference( fact_table, Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), )); refs }; let mut where_clause = vec![]; // Add join conditions between fact and each dimension table for i in 0..NUM_DIM_TABLES { let internal_id_fact = joined_tables[FACT_TABLE_IDX].internal_id; let internal_id_other = joined_tables[i].internal_id; where_clause.push(_create_binary_expr( _create_column_expr(internal_id_fact, i + 1, false), // fact.dimX_id ast::Operator::Equals, _create_column_expr(internal_id_other, 0, true), // dimX.id )); } let table_references = TableReferences::new(joined_tables, vec![]); let mut access_methods_arena = Vec::new(); let available_indexes = HashMap::default(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let result = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap(); assert!(result.is_some()); let BestJoinOrderResult { best_plan, .. } = result.unwrap(); // Expected optimal order: fact table as outer, with rowid seeks in any order on each dimension table // Verify fact table is selected as the outer table as all the other tables can use SeekRowid assert_eq!( best_plan.table_numbers().next().unwrap(), FACT_TABLE_IDX, "First table should be fact (table {}) due to available index, got table {} instead", FACT_TABLE_IDX, best_plan.table_numbers().next().unwrap() ); // Verify access methods let access_method = &access_methods_arena[best_plan.data[0].1]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(iter_dir == IterationDirection::Forwards); assert!(index.is_none()); assert!(constraint_refs.is_empty()); for (table_number, access_method_index) in best_plan.data.iter().skip(1) { let access_method = &access_methods_arena[*access_method_index]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(iter_dir == IterationDirection::Forwards); assert!(index.is_none()); assert!(constraint_refs.len() == 1); let constraint = &table_constraints[*table_number].constraints [constraint_refs[0].eq.as_ref().unwrap().constraint_pos]; assert!(constraint.lhs_mask.contains_table(FACT_TABLE_IDX)); assert!(constraint.operator.as_ast_operator() == Some(ast::Operator::Equals)); } } #[test] /// Test that [compute_best_join_order] figures out that the tables form a "linked list" pattern /// where a column in each table points to an indexed column in the next table, /// and chooses the best order based on that. fn test_compute_best_join_order_linked_list() { const NUM_TABLES: usize = 5; // Create tables t1 -> t2 -> t3 -> t4 -> t5 where there is a foreign key from each table to the next let mut tables = Vec::with_capacity(NUM_TABLES); for i in 0..NUM_TABLES { let mut columns = vec![_create_column_rowid_alias("id")]; if i < NUM_TABLES - 1 { columns.push(_create_column_of_type("next_id", Type::Integer)); } tables.push(_create_btree_table(&format!("t{}", i + 1), columns)); } let available_indexes = HashMap::default(); let mut table_id_counter = TableRefIdCounter::new(); // Create table references let joined_tables: Vec<_> = tables .iter() .map(|t| _create_table_reference(t.clone(), None, table_id_counter.next())) .collect(); // Create where clause linking each table to the next let mut where_clause = Vec::new(); for i in 0..NUM_TABLES - 1 { let internal_id_left = joined_tables[i].internal_id; let internal_id_right = joined_tables[i + 1].internal_id; where_clause.push(_create_binary_expr( _create_column_expr(internal_id_left, 1, false), // ti.next_id ast::Operator::Equals, _create_column_expr(internal_id_right, 0, true), // t(i+1).id )); } let table_references = TableReferences::new(joined_tables, vec![]); let mut access_methods_arena = Vec::new(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); // Run the optimizer let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let BestJoinOrderResult { best_plan, .. } = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap() .unwrap(); // Verify the join order is exactly t1 -> t2 -> t3 -> t4 -> t5 for i in 0..NUM_TABLES { assert_eq!( best_plan.table_numbers().nth(i).unwrap(), i, "Expected table {} at position {}, got table {} instead", i, i, best_plan.table_numbers().nth(i).unwrap() ); } // Verify access methods: // - First table should use Table scan let access_method = &access_methods_arena[best_plan.data[0].1]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(iter_dir == IterationDirection::Forwards); assert!(index.is_none()); assert!(constraint_refs.is_empty()); // all of the rest should use rowid equality for (i, table_constraints) in table_constraints .iter() .enumerate() .take(NUM_TABLES) .skip(1) { let access_method = &access_methods_arena[best_plan.data[i].1]; let (iter_dir, index, constraint_refs) = _as_btree(access_method); assert!(iter_dir == IterationDirection::Forwards); assert!(index.is_none()); assert!(constraint_refs.len() == 1); let constraint = &table_constraints.constraints [constraint_refs[0].eq.as_ref().unwrap().constraint_pos]; assert!(constraint.lhs_mask.contains_table(i - 1)); assert!(constraint.operator.as_ast_operator() == Some(ast::Operator::Equals)); } } #[test] /// Test that [compute_best_join_order] figures out that the index can't be used when only the second column is referenced fn test_index_second_column_only() { let mut joined_tables = Vec::new(); let mut table_id_counter = TableRefIdCounter::new(); // Create a table with two columns let table = _create_btree_table("t1", _create_column_list(&["x", "y"], Type::Integer)); // Create a two-column index on (x,y) let index = Arc::new(Index { name: "idx_xy".to_string(), table_name: "t1".to_string(), where_clause: None, columns: vec![ IndexColumn { name: "x".to_string(), order: SortOrder::Asc, pos_in_table: 0, collation: None, default: None, expr: None, }, IndexColumn { name: "y".to_string(), order: SortOrder::Asc, pos_in_table: 1, collation: None, default: None, expr: None, }, ], unique: false, root_page: 2, ephemeral: false, has_rowid: true, index_method: None, on_conflict: None, }); let mut available_indexes = HashMap::default(); available_indexes.insert("t1".to_string(), VecDeque::from([index])); let table = Table::BTree(table); joined_tables.push(JoinedTable { op: Operation::default_scan_for(&table), table, internal_id: table_id_counter.next(), identifier: "t1".to_string(), join_info: None, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id: 0, indexed: None, }); // Create where clause that only references second column let mut where_clause = vec![WhereTerm { expr: Expr::Binary( Box::new(Expr::Column { database: None, table: joined_tables[0].internal_id, column: 1, is_rowid_alias: false, }), ast::Operator::Equals, Box::new(Expr::Literal(ast::Literal::Numeric(5.to_string()))), ), from_outer_join: None, consumed: false, }]; let table_references = TableReferences::new(joined_tables, vec![]); let mut access_methods_arena = Vec::new(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let BestJoinOrderResult { best_plan, .. } = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap() .unwrap(); // Verify access method is a scan, not a seek, because the index can't be used when only the second column is referenced let access_method = &access_methods_arena[best_plan.data[0].1]; let (_, _, constraint_refs) = _as_btree(access_method); assert!(constraint_refs.is_empty()); } #[test] /// Test that an index with a gap in referenced columns (e.g. index on (a,b,c), where clause on a and c) /// only uses the prefix before the gap. fn test_index_skips_middle_column() { let mut table_id_counter = TableRefIdCounter::new(); let mut joined_tables = Vec::new(); let mut available_indexes = HashMap::default(); let columns = _create_column_list(&["c1", "c2", "c3"], Type::Integer); let table = _create_btree_table("t1", columns); let index = Arc::new(Index { name: "idx1".to_string(), table_name: "t1".to_string(), where_clause: None, columns: vec![ IndexColumn { name: "c1".to_string(), order: SortOrder::Asc, pos_in_table: 0, collation: None, default: None, expr: None, }, IndexColumn { name: "c2".to_string(), order: SortOrder::Asc, pos_in_table: 1, collation: None, default: None, expr: None, }, IndexColumn { name: "c3".to_string(), order: SortOrder::Asc, pos_in_table: 2, collation: None, default: None, expr: None, }, ], unique: false, root_page: 2, ephemeral: false, has_rowid: true, index_method: None, on_conflict: None, }); available_indexes.insert("t1".to_string(), VecDeque::from([index])); let table = Table::BTree(table); joined_tables.push(JoinedTable { op: Operation::default_scan_for(&table), table, internal_id: table_id_counter.next(), identifier: "t1".to_string(), join_info: None, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id: 0, indexed: None, }); // Create where clause that references first and third columns let mut where_clause = vec![ WhereTerm { expr: Expr::Binary( Box::new(Expr::Column { database: None, table: joined_tables[0].internal_id, column: 0, // c1 is_rowid_alias: false, }), ast::Operator::Equals, Box::new(Expr::Literal(ast::Literal::Numeric(5.to_string()))), ), from_outer_join: None, consumed: false, }, WhereTerm { expr: Expr::Binary( Box::new(Expr::Column { database: None, table: joined_tables[0].internal_id, column: 2, // c3 is_rowid_alias: false, }), ast::Operator::Equals, Box::new(Expr::Literal(ast::Literal::Numeric(7.to_string()))), ), from_outer_join: None, consumed: false, }, ]; let table_references = TableReferences::new(joined_tables, vec![]); let mut access_methods_arena = Vec::new(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let BestJoinOrderResult { best_plan, .. } = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap() .unwrap(); // Verify access method is a seek, and only uses the first column of the index let access_method = &access_methods_arena[best_plan.data[0].1]; let (_, index, constraint_refs) = _as_btree(access_method); assert!(index.as_ref().is_some_and(|i| i.name == "idx1")); assert!(constraint_refs.len() == 1); let constraint = &table_constraints[0].constraints [constraint_refs[0].eq.as_ref().unwrap().constraint_pos]; assert!(constraint.operator.as_ast_operator() == Some(ast::Operator::Equals)); assert!(constraint.table_col_pos == Some(0)); // c1 } #[test] /// Test that an index seek stops after a range operator. /// e.g. index on (a,b,c), where clause a=1, b>2, c=3. Only a and b should be used for seek. fn test_index_stops_at_range_operator() { let mut table_id_counter = TableRefIdCounter::new(); let mut joined_tables = Vec::new(); let mut available_indexes = HashMap::default(); let columns = _create_column_list(&["c1", "c2", "c3"], Type::Integer); let table = _create_btree_table("t1", columns); let index = Arc::new(Index { name: "idx1".to_string(), table_name: "t1".to_string(), where_clause: None, columns: vec![ IndexColumn { name: "c1".to_string(), order: SortOrder::Asc, pos_in_table: 0, collation: None, default: None, expr: None, }, IndexColumn { name: "c2".to_string(), order: SortOrder::Asc, pos_in_table: 1, collation: None, default: None, expr: None, }, IndexColumn { name: "c3".to_string(), order: SortOrder::Asc, pos_in_table: 2, collation: None, default: None, expr: None, }, ], root_page: 2, ephemeral: false, has_rowid: true, unique: false, index_method: None, on_conflict: None, }); available_indexes.insert("t1".to_string(), VecDeque::from([index])); let table = Table::BTree(table); joined_tables.push(JoinedTable { op: Operation::default_scan_for(&table), table, internal_id: table_id_counter.next(), identifier: "t1".to_string(), join_info: None, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id: 0, indexed: None, }); // Create where clause: c1 = 5 AND c2 > 10 AND c3 = 7 let mut where_clause = vec![ WhereTerm { expr: Expr::Binary( Box::new(Expr::Column { database: None, table: joined_tables[0].internal_id, column: 0, // c1 is_rowid_alias: false, }), ast::Operator::Equals, Box::new(Expr::Literal(ast::Literal::Numeric(5.to_string()))), ), from_outer_join: None, consumed: false, }, WhereTerm { expr: Expr::Binary( Box::new(Expr::Column { database: None, table: joined_tables[0].internal_id, column: 1, // c2 is_rowid_alias: false, }), ast::Operator::Greater, Box::new(Expr::Literal(ast::Literal::Numeric(10.to_string()))), ), from_outer_join: None, consumed: false, }, WhereTerm { expr: Expr::Binary( Box::new(Expr::Column { database: None, table: joined_tables[0].internal_id, column: 2, // c3 is_rowid_alias: false, }), ast::Operator::Equals, Box::new(Expr::Literal(ast::Literal::Numeric(7.to_string()))), ), from_outer_join: None, consumed: false, }, ]; let table_references = TableReferences::new(joined_tables, vec![]); let mut access_methods_arena = Vec::new(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let BestJoinOrderResult { best_plan, .. } = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap() .unwrap(); // Verify access method is a seek, and uses the first two columns of the index. // The third column can't be used because the second is a range query. let access_method = &access_methods_arena[best_plan.data[0].1]; let (_, index, constraint_refs) = _as_btree(access_method); assert!(index.as_ref().is_some_and(|i| i.name == "idx1")); assert!(constraint_refs.len() == 2); let constraint = &table_constraints[0].constraints [constraint_refs[0].eq.as_ref().unwrap().constraint_pos]; assert!(constraint.operator.as_ast_operator() == Some(ast::Operator::Equals)); assert!(constraint.table_col_pos == Some(0)); // c1 let constraint = &table_constraints[0].constraints[constraint_refs[1].lower_bound.unwrap()]; assert!(constraint.operator.as_ast_operator() == Some(ast::Operator::Greater)); assert!(constraint.table_col_pos == Some(1)); // c2 } fn _create_column(c: &TestColumn) -> Column { Column::new( Some(c.name.clone()), c.ty.to_string(), None, None, c.ty, None, ColDef { primary_key: false, rowid_alias: c.is_rowid_alias, ..Default::default() }, ) } fn _create_column_of_type(name: &str, ty: Type) -> Column { _create_column(&TestColumn { name: name.to_string(), ty, is_rowid_alias: false, }) } fn _create_column_list(names: &[&str], ty: Type) -> Vec { names .iter() .map(|name| _create_column_of_type(name, ty)) .collect() } fn _create_column_rowid_alias(name: &str) -> Column { _create_column(&TestColumn { name: name.to_string(), ty: Type::Integer, is_rowid_alias: true, }) } /// Creates a BTreeTable with the given name and columns fn _create_btree_table(name: &str, columns: Vec) -> Arc { Arc::new(BTreeTable { root_page: 1, // Page number doesn't matter for tests name: name.to_string(), has_autoincrement: false, primary_key_columns: vec![], columns, has_rowid: true, is_strict: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }) } /// Creates a TableReference for a BTreeTable fn _create_table_reference( table: Arc, join_info: Option, internal_id: TableInternalId, ) -> JoinedTable { let name = table.name.clone(); let table = Table::BTree(table); JoinedTable { op: Operation::default_scan_for(&table), table, identifier: name, internal_id, join_info, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id: 0, indexed: None, } } /// Creates a column expression fn _create_column_expr(table: TableInternalId, column: usize, is_rowid_alias: bool) -> Expr { Expr::Column { database: None, table, column, is_rowid_alias, } } /// Creates a binary expression for a WHERE clause fn _create_binary_expr(lhs: Expr, op: Operator, rhs: Expr) -> WhereTerm { WhereTerm { expr: Expr::Binary(Box::new(lhs), op, Box::new(rhs)), from_outer_join: None, consumed: false, } } /// Creates a numeric literal expression fn _create_numeric_literal(value: &str) -> Expr { Expr::Literal(ast::Literal::Numeric(value.to_string())) } fn _as_btree( access_method: &AccessMethod, ) -> ( IterationDirection, Option>, &'_ [RangeConstraintRef], ) { match &access_method.params { AccessMethodParams::BTreeTable { iter_dir, index, constraint_refs, } => (*iter_dir, index.clone(), constraint_refs), _ => panic!("expected BTreeTable access method"), } } #[test] /// Test that when an index is available on the join column, the optimizer prefers /// index lookup over hash join. fn test_prefer_index_lookup_over_hash_join() { // CREATE TABLE t1(a,b,c); // CREATE TABLE t2(a,b,c); // CREATE INDEX idx_t2_a ON t2(a); // SELECT * FROM t1 JOIN t2 ON t1.a = t2.a; // Expected: SCAN t1, SEARCH t2 USING INDEX idx_t2_a (a=?) // Not: HASH JOIN let t1 = _create_btree_table("t1", _create_column_list(&["a", "b", "c"], Type::Integer)); let t2 = _create_btree_table("t2", _create_column_list(&["a", "b", "c"], Type::Integer)); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = vec![ _create_table_reference(t1, None, table_id_counter.next()), _create_table_reference( t2, Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), ), ]; const TABLE1: usize = 0; const TABLE2: usize = 1; // Index on t2.a let mut available_indexes = HashMap::default(); let index_t2_a = Arc::new(Index { name: "idx_t2_a".to_string(), table_name: "t2".to_string(), where_clause: None, columns: vec![IndexColumn { name: "a".to_string(), order: SortOrder::Asc, pos_in_table: 0, collation: None, default: None, expr: None, }], unique: false, // Non-unique index ephemeral: false, root_page: 2, has_rowid: true, index_method: None, on_conflict: None, }); available_indexes.insert("t2".to_string(), VecDeque::from([index_t2_a])); // WHERE t1.a = t2.a let mut where_clause = vec![_create_binary_expr( _create_column_expr(joined_tables[TABLE1].internal_id, 0, false), // t1.a ast::Operator::Equals, _create_column_expr(joined_tables[TABLE2].internal_id, 0, false), // t2.a )]; let table_references = TableReferences::new(joined_tables, vec![]); let mut access_methods_arena = Vec::new(); let table_constraints = constraints_from_where_clause( &where_clause, &table_references, &available_indexes, &[], &empty_schema(), &DEFAULT_PARAMS, ) .unwrap(); let base_table_rows = default_base_rows(table_references.joined_tables().len()); let schema = empty_schema(); let result = compute_best_join_order( table_references.joined_tables(), 1.0, None, &table_constraints, &base_table_rows, &mut access_methods_arena, &mut where_clause, &[], &[], &DEFAULT_PARAMS, &AnalyzeStats::default(), &available_indexes, &table_references, &schema, ) .unwrap(); assert!(result.is_some()); let BestJoinOrderResult { best_plan, .. } = result.unwrap(); // Expected: t1 first (scan), t2 second (index seek) assert_eq!( best_plan.table_numbers().collect::>(), vec![TABLE1, TABLE2], "Expected join order [t1, t2] to use index on t2.a" ); // t1 should use table scan (no constraints) let access_method_t1 = &access_methods_arena[best_plan.data[0].1]; let (_, _, constraint_refs_t1) = _as_btree(access_method_t1); assert!( constraint_refs_t1.is_empty(), "t1 should use table scan with no constraints" ); // t2 should use index seek, NOT hash join let access_method_t2 = &access_methods_arena[best_plan.data[1].1]; match &access_method_t2.params { AccessMethodParams::BTreeTable { index, constraint_refs, .. } => { assert!( index.is_some(), "t2 should use index idx_t2_a, not a hash join" ); assert_eq!( index.as_ref().unwrap().name, "idx_t2_a", "t2 should use index idx_t2_a" ); assert!( !constraint_refs.is_empty(), "t2 should have constraints for index seek" ); } AccessMethodParams::HashJoin { .. } => { panic!("Expected index lookup on t2, but got hash join instead"); } _ => panic!("Unexpected access method for t2"), } } } ================================================ FILE: core/translate/optimizer/lift_common_subexpressions.rs ================================================ use crate::{turso_assert, turso_assert_greater_than}; use turso_parser::ast::{Expr, Operator}; use crate::{ translate::{expr::unwrap_parens_owned, plan::WhereTerm}, util::exprs_are_equivalent, Result, }; /// Lifts shared conjuncts (ANDs) from sibling OR terms. /// For example, given: /// (a AND b AND c AND d) /// OR /// (a AND b AND e AND f) /// Notice that both OR terms contain the same conjuncts (a AND b). /// /// This function will lift the common conjuncts (a AND b) to the top level, /// resulting in a Vec of three [WhereTerm]s like: /// 1. (c AND d) OR (e AND f) /// 2. a, /// 3. b, /// /// where `a` and `b` become separate WhereTerms, and the original WhereTerm /// is updated to `(c AND d) OR (e AND f)`. /// /// This optimization is important because we rely on individual [WhereTerm]s /// for index selection. Imagine an index on (a,b) -- with our current optimizer /// we wouldn't be able to use the index based on the original [WhereTerm]s, but /// if we can lift [a,b] to the top level, we can use the index. /// /// This function is horribly inefficient atm, but it at least makes certain /// less trivial queries (e.g. perf/tpc-h/queries/19.sql) finish reasonably fast. pub(crate) fn lift_common_subexpressions_from_binary_or_terms( where_clause: &mut Vec, ) -> Result<()> { let mut i = 0; while i < where_clause.len() { if !matches!(where_clause[i].expr, Expr::Binary(_, Operator::Or, _)) { // Not an OR term, skip. i += 1; continue; } let term_expr_owned = where_clause[i].expr.clone(); // Own the expression for flattening let term_from_outer_join = where_clause[i].from_outer_join; // This needs to be remembered for the new WhereTerms // e.g. a OR b OR c becomes effectively OR [a,b,c]. let or_operands = flatten_or_expr_owned(term_expr_owned)?; turso_assert!(or_operands.len() > 1); // Each OR operand is potentially an AND chain, e.g. // (a AND b) OR (c AND d). // Flatten them. // It's safe to remove parentheses with `unwrap_parens_owned` because // we will add them back once we reconstruct the OR term's child expressions. // e.g. (a AND b) OR (c AND d) becomes effectively AND [[a,b], [c,d]]. let all_or_operands_conjunct_lists: Vec<(Vec, usize)> = or_operands .into_iter() .map(|expr| { let (expr, paren_count) = unwrap_parens_owned(expr)?; Ok((flatten_and_expr_owned(expr)?, paren_count)) }) .collect::>>()?; // Find common conjuncts across all OR branches. // Initialize with conjuncts from the first OR branch. // We clone because `common_conjuncts_accumulator` will be modified. let mut common_conjuncts_accumulator = all_or_operands_conjunct_lists[0].0.clone(); for (other_conjunct_list, _) in all_or_operands_conjunct_lists.iter().skip(1) { // Retain only those expressions in `common_conjuncts_accumulator` // that are also present in `other_conjunct_list`. common_conjuncts_accumulator.retain(|common_expr| { other_conjunct_list .iter() .any(|expr| exprs_are_equivalent(common_expr, expr)) }); } // If no common conjuncts were found, move to the next WhereTerm. if common_conjuncts_accumulator.is_empty() { i += 1; continue; } // We found common conjuncts. Let's remove the common ones and rebuild the OR branches. // E.g. (a AND b) OR (a AND c) -> (b OR c) AND a. let mut new_or_operands_for_original_term = Vec::new(); let mut found_non_empty_or_branches = false; for (mut conjunct_list_for_or_branch, mut num_unwrapped_parens) in all_or_operands_conjunct_lists.into_iter() { // Remove the common conjuncts from this specific OR branch's list of conjuncts. conjunct_list_for_or_branch .retain(|expr_in_list| !common_conjuncts_accumulator.contains(expr_in_list)); if conjunct_list_for_or_branch.is_empty() { // If any of the OR branches are empty, we can remove the entire OR term. // E.g. (a AND b) OR (a) OR (a AND c) just becomes a. found_non_empty_or_branches = true; break; } // Rebuild this OR branch from its remaining (non-common) conjuncts. // If we unwrapped parentheses before, let's add them back. let mut top_level_expr = rebuild_and_expr_from_list(conjunct_list_for_or_branch); while num_unwrapped_parens > 0 { top_level_expr = Expr::Parenthesized(vec![top_level_expr.into()]); num_unwrapped_parens -= 1; } new_or_operands_for_original_term.push(top_level_expr); } if found_non_empty_or_branches { // If we found an empty OR branch, we can remove the entire OR term. // E.g. (a AND b) OR (a) OR (a AND c) just becomes a. where_clause[i].consumed = true; } else { turso_assert_greater_than!(new_or_operands_for_original_term.len(), 1); // Update the original WhereTerm's expression with the new OR structure (without common parts). where_clause[i].expr = rebuild_or_expr_from_list(new_or_operands_for_original_term); } // Add the lifted common conjuncts as new, separate WhereTerms. for common_expr_to_add in common_conjuncts_accumulator { where_clause.push(WhereTerm { expr: common_expr_to_add, from_outer_join: term_from_outer_join, consumed: false, }); } // Simply incrementing i is correct because we added new WhereTerms at the end. i += 1; } Ok(()) } /// Flatten an ast::Expr::Binary(lhs, OR, rhs) into a list of disjuncts. fn flatten_or_expr_owned(expr: Expr) -> Result> { let Expr::Binary(lhs, Operator::Or, rhs) = expr else { return Ok(vec![expr]); }; let mut flattened = flatten_or_expr_owned(*lhs)?; flattened.extend(flatten_or_expr_owned(*rhs)?); Ok(flattened) } /// Flatten an ast::Expr::Binary(lhs, AND, rhs) into a list of conjuncts. fn flatten_and_expr_owned(expr: Expr) -> Result> { let Expr::Binary(lhs, Operator::And, rhs) = expr else { return Ok(vec![expr]); }; let mut flattened = flatten_and_expr_owned(*lhs)?; flattened.extend(flatten_and_expr_owned(*rhs)?); Ok(flattened) } /// Rebuild an ast::Expr::Binary(lhs, AND, rhs) for a list of conjuncts. fn rebuild_and_expr_from_list(mut conjuncts: Vec) -> Expr { turso_assert!(!conjuncts.is_empty()); if conjuncts.len() == 1 { return conjuncts.pop().unwrap(); } let mut current_expr = conjuncts.remove(0); for next_expr in conjuncts { current_expr = Expr::Binary(Box::new(current_expr), Operator::And, Box::new(next_expr)); } current_expr } /// Rebuild an ast::Expr::Binary(lhs, OR, rhs) for a list of operands. fn rebuild_or_expr_from_list(mut operands: Vec) -> Expr { turso_assert!(!operands.is_empty()); if operands.len() == 1 { return operands.pop().unwrap(); } let mut current_expr = operands.remove(0); for next_expr in operands { current_expr = Expr::Binary(Box::new(current_expr), Operator::Or, Box::new(next_expr)); } current_expr } #[cfg(test)] mod tests { use super::*; use crate::translate::plan::WhereTerm; use turso_parser::ast::{self, Expr, Literal, Operator, TableInternalId}; #[test] fn test_lift_common_subexpressions() -> Result<()> { // SELECT * FROM t WHERE (a = 1 and x = 1 and b = 1) OR (a = 1 and y = 1 and b = 1) // should be rewritten to: // SELECT * FROM t WHERE (x = 1 OR y = 1) and a = 1 and b = 1 // assume the table has 4 columns: a, b, x, y let a_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 0, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let b_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 1, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let x_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 2, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let y_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 3, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); // Create (a = 1 AND x = 1 AND b = 1) OR (a = 1 AND y = 1 AND b = 1) let or_expr = Expr::Binary( Box::new(ast::Expr::Parenthesized(vec![rebuild_and_expr_from_list( vec![a_expr.clone(), x_expr.clone(), b_expr.clone()], ) .into()])), Operator::Or, Box::new(ast::Expr::Parenthesized(vec![rebuild_and_expr_from_list( vec![a_expr.clone(), y_expr.clone(), b_expr.clone()], ) .into()])), ); let mut where_clause = vec![WhereTerm { expr: or_expr, from_outer_join: None, consumed: false, }]; lift_common_subexpressions_from_binary_or_terms(&mut where_clause)?; // Should now have 3 terms: // 1. (x = 1) OR (y = 1) // 2. a = 1 // 3. b = 1 let nonconsumed_terms = where_clause .iter() .filter(|term| !term.consumed) .collect::>(); assert_eq!(nonconsumed_terms.len(), 3); assert_eq!( nonconsumed_terms[0].expr, Expr::Binary( Box::new(ast::Expr::Parenthesized(vec![x_expr.into()])), Operator::Or, Box::new(ast::Expr::Parenthesized(vec![y_expr.into()])) ) ); assert_eq!(nonconsumed_terms[1].expr, a_expr); assert_eq!(nonconsumed_terms[2].expr, b_expr); Ok(()) } #[test] fn test_lift_common_subexpressions_three_branches() -> Result<()> { // Test case with three OR branches and one common term: // (a = 1 AND x = 1) OR (a = 1 AND y = 1) OR (a = 1 AND z = 1) // Should become: // (x = 1 OR y = 1 OR z = 1) AND a = 1 let a_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 0, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let x_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 1, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let y_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 2, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let z_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 3, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); // Create (a = 1 AND x = 1) OR (a = 1 AND y = 1) OR (a = 1 AND z = 1) let or_expr = Expr::Binary( Box::new(Expr::Binary( Box::new(ast::Expr::Parenthesized(vec![rebuild_and_expr_from_list( vec![a_expr.clone(), x_expr.clone()], ) .into()])), Operator::Or, Box::new(ast::Expr::Parenthesized(vec![rebuild_and_expr_from_list( vec![a_expr.clone(), y_expr.clone()], ) .into()])), )), Operator::Or, Box::new(ast::Expr::Parenthesized(vec![rebuild_and_expr_from_list( vec![a_expr.clone(), z_expr.clone()], ) .into()])), ); let mut where_clause = vec![WhereTerm { expr: or_expr, from_outer_join: None, consumed: false, }]; lift_common_subexpressions_from_binary_or_terms(&mut where_clause)?; // Should now have 2 terms: // 1. (x = 1) OR (y = 1) OR (z = 1) // 2. a = 1 let nonconsumed_terms = where_clause .iter() .filter(|term| !term.consumed) .collect::>(); assert_eq!(nonconsumed_terms.len(), 2); assert_eq!( nonconsumed_terms[0].expr, Expr::Binary( Box::new(Expr::Binary( Box::new(ast::Expr::Parenthesized(vec![x_expr.into()])), Operator::Or, Box::new(ast::Expr::Parenthesized(vec![y_expr.into()])), )), Operator::Or, Box::new(ast::Expr::Parenthesized(vec![z_expr.into()])), ) ); assert_eq!(nonconsumed_terms[1].expr, a_expr); Ok(()) } #[test] fn test_lift_common_subexpressions_no_common_terms() -> Result<()> { // Test case where there are no common terms between OR branches: // SELECT * FROM t WHERE (x = 1) OR (y = 1) // should remain unchanged. let x_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 0, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let y_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 1, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let or_expr = Expr::Binary( Box::new(ast::Expr::Parenthesized(vec![x_expr.into()])), Operator::Or, Box::new(ast::Expr::Parenthesized(vec![y_expr.into()])), ); let mut where_clause = vec![WhereTerm { expr: or_expr.clone(), from_outer_join: None, consumed: false, }]; lift_common_subexpressions_from_binary_or_terms(&mut where_clause)?; // Should remain unchanged since no common terms let nonconsumed_terms = where_clause .iter() .filter(|term| !term.consumed) .collect::>(); assert_eq!(nonconsumed_terms.len(), 1); assert_eq!(nonconsumed_terms[0].expr, or_expr); Ok(()) } #[test] fn test_lift_common_subexpressions_from_outer_join() -> Result<()> { // Test case with from_outer_join flag set; // it should be retained in the new WhereTerms, for outer join correctness. let a_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 0, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let x_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 1, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let y_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 2, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let or_expr = Expr::Binary( Box::new(ast::Expr::Parenthesized(vec![rebuild_and_expr_from_list( vec![a_expr.clone(), x_expr.clone()], ) .into()])), Operator::Or, Box::new(ast::Expr::Parenthesized(vec![rebuild_and_expr_from_list( vec![a_expr.clone(), y_expr.clone()], ) .into()])), ); let mut where_clause = vec![WhereTerm { expr: or_expr, from_outer_join: Some(TableInternalId::default()), // Set from_outer_join consumed: false, }]; lift_common_subexpressions_from_binary_or_terms(&mut where_clause)?; // Should have 2 terms, both with from_outer_join set let nonconsumed_terms = where_clause .iter() .filter(|term| !term.consumed) .collect::>(); assert_eq!(nonconsumed_terms.len(), 2); assert_eq!( nonconsumed_terms[0].expr, Expr::Binary( Box::new(ast::Expr::Parenthesized(vec![x_expr.into()])), Operator::Or, Box::new(ast::Expr::Parenthesized(vec![y_expr.into()])) ) ); assert_eq!( nonconsumed_terms[0].from_outer_join, Some(TableInternalId::default()) ); assert_eq!(nonconsumed_terms[1].expr, a_expr); assert_eq!( nonconsumed_terms[1].from_outer_join, Some(TableInternalId::default()) ); Ok(()) } #[test] fn test_lift_common_subexpressions_single_term() -> Result<()> { // Test case with a single non-OR term: // SELECT * FROM t WHERE a = 1 // should remain unchanged. let single_expr = Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: 0, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ); let mut where_clause = vec![WhereTerm { expr: single_expr.clone(), from_outer_join: None, consumed: false, }]; lift_common_subexpressions_from_binary_or_terms(&mut where_clause)?; // Should remain unchanged let nonconsumed_terms = where_clause .iter() .filter(|term| !term.consumed) .collect::>(); assert_eq!(nonconsumed_terms.len(), 1); assert_eq!(nonconsumed_terms[0].expr, single_expr); Ok(()) } #[test] fn test_lift_common_subexpressions_empty_or_branch() -> Result<()> { // Test case where OR becomes redundant: // (a = 1 AND b = 1) OR (a = 1) becomes -> a = 1. let exprs = (0..=1) .map(|i| { Expr::Binary( Box::new(Expr::Column { database: None, table: TableInternalId::default(), column: i, is_rowid_alias: false, }), Operator::Equals, Box::new(Expr::Literal(Literal::Numeric("1".to_string()))), ) }) .collect::>(); let a_expr = exprs[0].clone(); let b_expr = exprs[1].clone(); let a_and_b_expr = Expr::Binary(Box::new(a_expr.clone()), Operator::And, Box::new(b_expr)); let or_expr = Expr::Binary( Box::new(a_and_b_expr), Operator::Or, Box::new(a_expr.clone()), ); let mut where_clause = vec![WhereTerm { expr: or_expr, from_outer_join: None, consumed: false, }]; lift_common_subexpressions_from_binary_or_terms(&mut where_clause)?; let nonconsumed_terms = where_clause .iter() .filter(|term| !term.consumed) .collect::>(); assert_eq!(nonconsumed_terms.len(), 1); assert_eq!(nonconsumed_terms[0].expr, a_expr); Ok(()) } } ================================================ FILE: core/translate/optimizer/mod.rs ================================================ use crate::translate::expression_index::expression_index_column_usage; use crate::translate::plan::MultiIndexBranchAccess; use crate::{ function::{AggFunc, Deterministic}, index_method::IndexMethodCostEstimate, numeric::Numeric, schema::{BTreeTable, Index, IndexColumn, Schema, Table, ROWID_SENTINEL}, translate::{ insert::ROWID_COLUMN, optimizer::{ access_method::AccessMethodParams, constraints::{ ConstraintUseCandidate, RangeConstraintRef, SeekRangeConstraint, TableConstraints, }, cost::RowCountEstimate, multi_index::MultiIndexBranchAccessParams, order::{ColumnTarget, OrderTarget}, }, plan::{ ColumnUsedMask, DmlSafetyReason, EphemeralRowidMode, HashJoinOp, IndexMethodQuery, NonFromClauseSubquery, OuterQueryReference, QueryDestination, ResultSetColumn, Scan, SeekKeyComponent, SubqueryState, }, trigger_exec::has_relevant_triggers_type_only, }, types::SeekOp, util::{ count_fts_column_args, exprs_are_equivalent, simple_bind_expr, try_capture_parameters, try_capture_parameters_column_agnostic, try_substitute_parameters, }, vdbe::{ affinity::Affinity, builder::{CursorKey, CursorType, ProgramBuilder}, }, LimboError, Result, }; use crate::{turso_assert, turso_assert_eq, turso_debug_assert, turso_soft_unreachable}; use constraints::{ constraints_from_where_clause, usable_constraints_for_join_order, Constraint, ConstraintOperator, ConstraintRef, }; use cost::Cost; use join::{compute_best_join_order_with_context, BestJoinOrderResult, JoinPlanningContext}; use lift_common_subexpressions::lift_common_subexpressions_from_binary_or_terms; use order::{ compute_order_target, plan_satisfies_order_target, simple_aggregate_order_target, EliminatesSortBy, OrderTargetPurpose, }; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::{cmp::Ordering, collections::VecDeque, sync::Arc}; use turso_ext::{ConstraintInfo, ConstraintUsage}; use turso_parser::ast::{self, Expr, SortOrder, SubqueryType, TriggerEvent}; use super::{ collate::get_collseq_from_expr, emitter::Resolver, plan::{ DeletePlan, GroupBy, InSeekSource, IterationDirection, JoinOrderMember, JoinType, JoinedTable, MinMaxDef, MultiIndexBranch, MultiIndexScanOp, Operation, Plan, Search, SeekDef, SeekKey, SelectPlan, SetOperation, SimpleAggregate, TableReferences, UpdatePlan, WhereTerm, }, planner::TableMask, }; pub(crate) mod access_method; pub(crate) mod constraints; pub(crate) mod cost; mod cost_params; pub(crate) mod join; pub(crate) mod lift_common_subexpressions; pub(crate) mod multi_index; pub(crate) mod order; pub(crate) mod unnest; /// A candidate index method that could be used for table access in a join query. /// This struct captures all information needed to construct an IndexMethodQuery /// operation, allowing the DP join ordering algorithm to consider custom index /// methods alongside BTree indexes. #[derive(Debug, Clone)] pub struct IndexMethodCandidate { /// Index of the table in the joined_tables list pub table_idx: usize, /// The index that defines this index method pub index: Arc, /// Pattern index from the index method definition that matched pub pattern_idx: usize, /// Arguments captured from pattern matching pub arguments: Vec, /// Mapping from synthetic column IDs to pattern column IDs for covered columns pub covered_columns: HashMap, /// Index in WHERE clause that was covered by this pattern (if any) pub where_covered: Option, /// Cost estimate from the index method pub cost_estimate: Option, } impl IndexMethodCandidate { /// Build the IndexMethodQuery operation from this candidate pub fn to_query(&self) -> IndexMethodQuery { IndexMethodQuery { index: self.index.clone(), pattern_idx: self.pattern_idx, arguments: self.arguments.clone(), covered_columns: self.covered_columns.clone(), } } } /// Result of successfully matching an index method pattern against a query. /// This intermediate struct allows both `collect_index_method_candidates` and /// `optimize_table_access_with_custom_modules` to share pattern matching logic. #[derive(Debug, Clone)] struct IndexMethodPatternMatch { /// Pattern index from the index method definition that matched pattern_idx: usize, /// Parameters captured from pattern matching (positional placeholders) parameters: HashMap, /// Index in WHERE clause that was covered by this pattern (if any) where_covered: Option, /// Whether the pattern explicitly handles ORDER BY pattern_has_order_by: bool, /// Whether the pattern explicitly handles LIMIT pattern_has_limit: bool, /// Pattern result columns (needed for covered columns calculation) pattern_columns: Vec, } /// Try to match an index method pattern against a query's clauses. #[allow(clippy::too_many_arguments)] fn try_match_index_method_pattern( pattern: &ast::Select, table: &JoinedTable, query_where_terms: &[WhereTerm], order_by: &[(Box, SortOrder)], limit: &Option>, offset: &Option>, pattern_idx: usize, soft_bind_errors: bool, ) -> Option { let mut pattern = pattern.clone(); if pattern.with.is_some() || !pattern.body.compounds.is_empty() { return None; } let ast::OneSelect::Select { columns, from: Some(ast::FromClause { select, joins }), distinctness: None, where_clause: ref mut pattern_where_clause, group_by: None, window_clause, } = &mut pattern.body.select else { if soft_bind_errors { return None; } panic!("unexpected select pattern body"); }; if !window_clause.is_empty() || !joins.is_empty() { return None; } let ast::SelectTable::Table(name, _, _) = select.as_ref() else { if soft_bind_errors { return None; } panic!("unexpected from clause"); }; // Bind expressions to this table for column in columns.iter_mut() { if let ast::ResultColumn::Expr(e, _) = column { if soft_bind_errors { if simple_bind_expr(table, &[], e).is_err() { return None; } } else { simple_bind_expr(table, &[], e).ok()?; } } } for column in pattern.order_by.iter_mut() { if soft_bind_errors { if simple_bind_expr(table, columns, &mut column.expr).is_err() { return None; } } else { simple_bind_expr(table, columns, &mut column.expr).ok()?; } } if let Some(pattern_where) = pattern_where_clause { if soft_bind_errors { if simple_bind_expr(table, columns, pattern_where).is_err() { return None; } } else { simple_bind_expr(table, columns, pattern_where).ok()?; } } if name.name.as_str() != table.table.get_name() { return None; } let pattern_has_order_by = !pattern.order_by.is_empty(); let pattern_has_limit = pattern.limit.is_some(); // If pattern has ORDER BY, it must match exactly if pattern_has_order_by && order_by.len() != pattern.order_by.len() { return None; } let mut where_query_covered: Option = None; let mut parameters = HashMap::default(); // Match ORDER BY if pattern has it if pattern_has_order_by { for (pattern_column, (query_column, query_order)) in pattern.order_by.iter().zip(order_by.iter()) { if *query_order != pattern_column.order.unwrap_or(SortOrder::Asc) { return None; } let num_col_args = count_fts_column_args(&pattern_column.expr); let captured = if num_col_args > 0 { try_capture_parameters_column_agnostic( &pattern_column.expr, query_column, num_col_args, ) } else { try_capture_parameters(&pattern_column.expr, query_column) }; parameters.extend(captured?); } } // Match LIMIT if pattern has it match (pattern.limit.as_ref().map(|x| &x.expr), limit) { (Some(_), None) => return None, (Some(pattern_limit), Some(query_limit)) => { let captured = try_capture_parameters(pattern_limit, query_limit)?; parameters.extend(captured); } (None, Some(_)) | (None, None) => {} } // Match OFFSET if pattern has it match ( pattern.limit.as_ref().and_then(|x| x.offset.as_ref()), offset, ) { (Some(_), None) => return None, (Some(pattern_off), Some(query_off)) => { let captured = try_capture_parameters(pattern_off, query_off)?; parameters.extend(captured); } (None, Some(_)) | (None, None) => {} } // Match WHERE clause if let Some(pattern_where) = pattern_where_clause { for (i, query_where) in query_where_terms.iter().enumerate() { let num_col_args = count_fts_column_args(pattern_where); let captured = if num_col_args > 0 { try_capture_parameters_column_agnostic( pattern_where, &query_where.expr, num_col_args, ) } else { try_capture_parameters(pattern_where, &query_where.expr) }; let Some(captured) = captured else { continue; }; parameters.extend(captured); where_query_covered = Some(i); break; } } // Pattern requires WHERE but we didn't match any if pattern_where_clause.is_some() && where_query_covered.is_none() { return None; } let where_covered_completely = query_where_terms.is_empty() || (where_query_covered.is_some() && query_where_terms.len() == 1); // When WHERE is not completely covered, skip patterns with ORDER BY/LIMIT // because post-filtering would disrupt the order or apply limits incorrectly if !where_covered_completely && (pattern_has_order_by || pattern_has_limit) { return None; } Some(IndexMethodPatternMatch { pattern_idx, parameters, where_covered: where_query_covered, pattern_has_order_by, pattern_has_limit, pattern_columns: columns.clone(), }) } /// Build covered columns mapping from pattern columns. /// Returns a HashMap mapping synthetic column IDs to pattern column IDs. fn build_covered_columns_mapping( pattern_columns: &[ast::ResultColumn], parameters: &HashMap, ) -> HashMap { let mut covered_column_id = 1_000_000; let mut covered_columns = HashMap::default(); for (pattern_column_id, pattern_column) in pattern_columns.iter().enumerate() { let ast::ResultColumn::Expr(pattern_expr, _) = pattern_column else { continue; }; let Some(_substituted) = try_substitute_parameters(pattern_expr, parameters) else { continue; }; covered_columns.insert(covered_column_id, pattern_column_id); covered_column_id += 1; } covered_columns } /// Sort parameters by key and extract just the expressions as a Vec. fn sorted_arguments_from_parameters(parameters: &HashMap) -> Vec { let mut arguments: Vec<_> = parameters.iter().collect(); arguments.sort_by_key(|(&i, _)| i); arguments.iter().map(|(_, e)| (*e).clone()).collect() } /// Collect index method candidates for all tables that have custom index methods. /// This function performs pattern matching but does NOT apply the operations, /// allowing the DP join ordering algorithm to consider index methods as candidates. #[allow(clippy::too_many_arguments)] fn collect_index_method_candidates( table_references: &TableReferences, available_indexes: &HashMap>>, where_clause: &[WhereTerm], order_by: &[(Box, SortOrder)], group_by: &Option, limit: &Option>, offset: &Option>, base_table_rows: &[RowCountEstimate], params: &cost_params::CostModelParams, ) -> Result> { let mut candidates = Vec::new(); // Group by is not supported for index methods if group_by.is_some() { return Ok(candidates); } let tables = table_references.joined_tables(); for (table_idx, table) in tables.iter().enumerate() { let Some(indexes) = available_indexes.get(table.table.get_name()) else { continue; }; for index in indexes { let Some(module) = &index.index_method else { continue; }; if index.is_backing_btree_index() { continue; } let definition = module.definition(); for (pattern_idx, pattern) in definition.patterns.iter().enumerate() { // Use shared helper for pattern matching let Some(pattern_match) = try_match_index_method_pattern( pattern, table, where_clause, order_by, limit, offset, pattern_idx, true, // continue on binding failures ) else { continue; }; // Build covered columns mapping from pattern match let covered_columns = build_covered_columns_mapping( &pattern_match.pattern_columns, &pattern_match.parameters, ); // Get cost estimate from the index method let cost_estimate = module.init().ok().and_then(|cursor| { let base_rows = base_table_rows .get(table_idx) .map(|r| **r) .unwrap_or(params.rows_per_table_fallback); cursor.estimate_cost(pattern_match.pattern_idx, base_rows) }); // Sort and collect arguments let arguments = sorted_arguments_from_parameters(&pattern_match.parameters); candidates.push(IndexMethodCandidate { table_idx, index: index.clone(), pattern_idx: pattern_match.pattern_idx, arguments, covered_columns, where_covered: pattern_match.where_covered, cost_estimate, }); // Found a match for this table+index, try next index break; } } } Ok(candidates) } #[tracing::instrument(skip_all, level = tracing::Level::DEBUG)] pub fn optimize_plan( program: &mut ProgramBuilder, plan: &mut Plan, resolver: &Resolver, ) -> Result<()> { let schema = resolver.schema(); match plan { Plan::Select(plan) => optimize_select_plan(plan, schema)?, Plan::Delete(plan) => optimize_delete_plan(plan, schema)?, Plan::Update(plan) => optimize_update_plan(program, plan, resolver)?, Plan::CompoundSelect { left, right_most, .. } => { optimize_select_plan(right_most, schema)?; for (plan, _) in left { optimize_select_plan(plan, schema)?; } } } // When debug tracing is enabled, print the optimized plan as a SQL string for debugging tracing::debug!(plan_sql = plan.to_string()); Ok(()) } #[cfg(all(feature = "fts", not(target_family = "wasm")))] /// Transform MATCH expressions to fts_match() function calls. fn transform_match_to_fts_match( where_clause: &mut [WhereTerm], schema: &Schema, table_references: &TableReferences, ) -> Result<()> { use super::ast::{FunctionTail, LikeOperator, Name, TableInternalId}; use super::expr::{walk_expr_mut, WalkControl}; // Helper to extract table ID from a column expression fn get_table_id_from_expr(expr: &Expr) -> Option { match expr { Expr::Column { table, .. } => Some(*table), Expr::Parenthesized(exprs) if !exprs.is_empty() => get_table_id_from_expr(&exprs[0]), _ => None, } } // Helper to check if a table has an FTS index by its internal ID let table_has_fts_index = |table_id: TableInternalId| -> bool { table_references .joined_tables() .iter() .find(|t| t.internal_id == table_id) .and_then(|t| { if let Table::BTree(btree) = &t.table { Some(schema.has_fts_index(&btree.name)) } else { None } }) .unwrap_or(false) }; let mut match_without_fts = false; for term in where_clause.iter_mut() { let _ = walk_expr_mut(&mut term.expr, &mut |e: &mut Expr| -> Result { match e { Expr::Like { lhs, not, op: LikeOperator::Match, rhs, escape: _, } => { // Check if the specific table referenced by this MATCH has an FTS index let has_fts = get_table_id_from_expr(lhs).is_some_and(table_has_fts_index); if !has_fts { match_without_fts = true; // Don't transform, we'll error after the walk return Ok(WalkControl::SkipChildren); } // Transform MATCH to fts_match(): // - `col MATCH 'query'` -> `fts_match(col, 'query')` // - `(col1, col2) MATCH 'query'` -> `fts_match(col1, col2, 'query')` let mut args: Vec> = match lhs.as_ref() { Expr::Parenthesized(cols) => cols.clone(), _ => vec![lhs.clone()], }; args.push(rhs.clone()); let func_call = Expr::FunctionCall { name: Name::exact("fts_match".to_string()), distinctness: None, args, order_by: vec![], filter_over: FunctionTail { filter_clause: None, over_clause: None, }, }; if *not { // For NOT MATCH, just wrap the whole thing in a unary NOT *e = Expr::Unary(ast::UnaryOperator::Not, Box::new(func_call)); } else { *e = func_call; } Ok(WalkControl::Continue) } _ => Ok(WalkControl::Continue), } }); } if match_without_fts { return Err(LimboError::ParseError( "unable to use function MATCH in the requested context".to_string(), )); } Ok(()) } /// Detect whether this plan qualifies for the simple-aggregate fast path. /// /// Analogous to SQLite's `isSimpleCount()` + `minMaxQuery()`. /// Must be called before `optimize_table_access` so the order target is available. fn detect_simple_aggregate(plan: &SelectPlan) -> Option { // Common preconditions shared by count(*) and min/max. if plan.aggregates.len() != 1 || plan.table_references.joined_tables().len() != 1 || plan.result_columns.len() != 1 || plan.group_by.is_some() || plan.contains_constant_false_condition { return None; } let table_ref = plan.table_references.joined_tables().first().unwrap(); let agg = plan.aggregates.first().unwrap(); let result_expr = &plan.result_columns.first().unwrap().expr; // The result column must be exactly the aggregate expression (not wrapped in // something like `length(count(*))`). if !exprs_are_equivalent(result_expr, &agg.original_expr) { return None; } match agg.func { AggFunc::Count0 if matches!(table_ref.table, Table::BTree(..)) && plan.table_references.outer_query_refs().is_empty() && plan.where_clause.is_empty() && plan.limit.is_none() && plan.offset.is_none() => { Some(SimpleAggregate::Count) } AggFunc::Min | AggFunc::Max if agg.args.len() == 1 && matches!( table_ref.table, Table::BTree(..) | Table::FromClauseSubquery(..) ) => { // Unlike COUNT(*), MIN/MAX may still use the fast path with a // WHERE clause as long as the chosen access path can walk directly // to the first qualifying extremum row. let argument = agg.args[0].clone(); let order = if matches!(agg.func, AggFunc::Min) { SortOrder::Asc } else { SortOrder::Desc }; let collation = get_collseq_from_expr(&argument, &plan.table_references) .ok() .flatten(); Some(SimpleAggregate::MinMax(Box::new(MinMaxDef { func: agg.func.clone(), argument, order, collation, }))) } _ => None, } } struct OptimizeTableAccessResult { join_order: Vec, output_rows: f64, min_max_fast_path: bool, } /** * Make a few passes over the plan to optimize it. * TODO: these could probably be done in less passes, * but having them separate makes them easier to understand */ pub fn optimize_select_plan(plan: &mut SelectPlan, schema: &Schema) -> Result<()> { // Transform MATCH expressions to fts_match() for FTS optimizer recognition #[cfg(all(feature = "fts", not(target_family = "wasm")))] transform_match_to_fts_match(&mut plan.where_clause, schema, &plan.table_references)?; unnest::unnest_exists_subqueries(plan)?; // EXISTS only needs 1 row. Add LIMIT 1 to surviving (non-unnested) EXISTS // subqueries. This is done here rather than in the subquery planner so that // unnesting sees the plan without an artificial LIMIT. for sub in &mut plan.non_from_clause_subqueries { if matches!(sub.query_type, ast::SubqueryType::Exists { .. }) { if let SubqueryState::Unevaluated { plan: Some(inner) } = &mut sub.state { if inner.limit.is_none() { inner.limit = Some(Box::new(Expr::Literal(ast::Literal::Numeric( "1".to_string(), )))); } } } } optimize_subqueries(plan, schema)?; lift_common_subexpressions_from_binary_or_terms(&mut plan.where_clause)?; if let ConstantConditionEliminationResult::ImpossibleCondition = eliminate_constant_conditions(&mut plan.where_clause)? { plan.contains_constant_false_condition = true; return Ok(()); } plan.simple_aggregate = detect_simple_aggregate(plan); let best_join_order = optimize_table_access( schema, &mut plan.result_columns, &mut plan.table_references, &schema.indexes, &mut plan.where_clause, &mut plan.order_by, &mut plan.group_by, plan.simple_aggregate.as_ref(), &plan.non_from_clause_subqueries, &mut plan.limit, &mut plan.offset, plan.input_cardinality_hint.unwrap_or(1.0), )?; if matches!(plan.simple_aggregate, Some(SimpleAggregate::MinMax(_))) && !best_join_order .as_ref() .is_some_and(|result| result.min_max_fast_path) { plan.simple_aggregate = None; } if let Some(OptimizeTableAccessResult { join_order, output_rows, .. }) = best_join_order { plan.join_order = join_order; let mut est = output_rows; // Clamp to LIMIT when it's a literal non-negative number. // Negative LIMIT means "no limit" in SQLite, so we skip those. if let Some(limit_expr) = &plan.limit { if let Ok(val) = crate::util::parse_signed_number(limit_expr) { let limit_f64 = match val { crate::types::Value::Numeric(Numeric::Integer(i)) if i >= 0 => Some(i as f64), crate::types::Value::Numeric(Numeric::Float(f)) => { let f: f64 = f.into(); if f >= 0.0 { Some(f) } else { None } } _ => None, }; if let Some(limit_val) = limit_f64 { est = est.min(limit_val); } } } plan.estimated_output_rows = Some(est); } reoptimize_correlated_subqueries(plan, schema)?; Ok(()) } fn optimize_delete_plan(plan: &mut DeletePlan, schema: &Schema) -> Result<()> { #[cfg(all(feature = "fts", not(target_family = "wasm")))] transform_match_to_fts_match(&mut plan.where_clause, schema, &plan.table_references)?; lift_common_subexpressions_from_binary_or_terms(&mut plan.where_clause)?; if let ConstantConditionEliminationResult::ImpossibleCondition = eliminate_constant_conditions(&mut plan.where_clause)? { plan.contains_constant_false_condition = true; return Ok(()); } if let Some(rowset_plan) = plan.rowset_plan.as_mut() { optimize_select_plan(rowset_plan, schema)?; } let _ = optimize_table_access( schema, &mut plan.result_columns, &mut plan.table_references, &schema.indexes, &mut plan.where_clause, &mut plan.order_by, &mut None, None, &plan.non_from_clause_subqueries, &mut plan.limit, &mut plan.offset, 1.0, )?; Ok(()) } fn optimize_update_plan( program: &mut ProgramBuilder, plan: &mut UpdatePlan, resolver: &Resolver, ) -> Result<()> { let schema = resolver.schema(); #[cfg(all(feature = "fts", not(target_family = "wasm")))] transform_match_to_fts_match(&mut plan.where_clause, schema, &plan.table_references)?; lift_common_subexpressions_from_binary_or_terms(&mut plan.where_clause)?; if let ConstantConditionEliminationResult::ImpossibleCondition = eliminate_constant_conditions(&mut plan.where_clause)? { plan.contains_constant_false_condition = true; return Ok(()); } let _ = optimize_table_access( schema, &mut [], &mut plan.table_references, &schema.indexes, &mut plan.where_clause, &mut plan.order_by, &mut None, None, &plan.non_from_clause_subqueries, &mut plan.limit, &mut plan.offset, 1.0, )?; if let Some(reason) = first_update_safety_reason(plan, resolver)? { plan.safety.require(reason); } if !plan.safety.requires_stable_write_set() { return Ok(()); } add_ephemeral_table_to_update_plan(program, plan) } fn first_update_safety_reason( plan: &UpdatePlan, resolver: &Resolver, ) -> Result> { let table_ref = &plan.table_references.joined_tables()[0]; let reason = 'requires: { let Some(btree_table_arc) = table_ref.table.btree() else { break 'requires None; }; let btree_table = btree_table_arc.as_ref(); // Multi-index scans gather rowids from multiple index branches. // For UPDATE, we always use the prebuilt ephemeral-table path so writes run against // that fixed rowid list (no surprises from branch/index overlap). if matches!(table_ref.op, Operation::MultiIndexScan(_)) { break 'requires Some(DmlSafetyReason::MultiIndexScan); } // Index method cursors that stream lazily need rowids collected first. if let Operation::IndexMethodQuery(query) = &table_ref.op { let attachment = query .index .index_method .as_ref() .expect("IndexMethodQuery always has an index_method attachment"); if !attachment.definition().results_materialized { break 'requires Some(DmlSafetyReason::IndexMethodNotMaterialized); } } // Check if there are UPDATE triggers let updated_cols: HashSet = plan.set_clauses.iter().map(|(i, _)| *i).collect(); let database_id = table_ref.database_id; if resolver.with_schema(database_id, |s| { has_relevant_triggers_type_only( s, TriggerEvent::Update, Some(&updated_cols), btree_table, ) }) { break 'requires Some(DmlSafetyReason::Trigger); } // REPLACE mode requires ephemeral table because REPLACE deletes conflicting rows, // which can corrupt the iteration order when iterating via an index. if matches!( plan.or_conflict, Some(turso_parser::ast::ResolveType::Replace) ) { break 'requires Some(DmlSafetyReason::ReplaceMode); } let Some(index) = table_ref.op.index() else { let rowid_alias_used = plan.set_clauses.iter().fold(false, |accum, (idx, _)| { accum || (*idx != ROWID_SENTINEL && btree_table.columns[*idx].is_rowid_alias()) }); if rowid_alias_used { break 'requires Some(DmlSafetyReason::KeyMutation); } let direct_rowid_update = plan .set_clauses .iter() .any(|(idx, _)| *idx == ROWID_SENTINEL); if direct_rowid_update { break 'requires Some(DmlSafetyReason::KeyMutation); } break 'requires None; }; for (set_clause_col_idx, _) in plan.set_clauses.iter() { for c in index.columns.iter() { if let Some(ref expr) = c.expr { let expr_idx_cols_mask = expression_index_column_usage(expr.as_ref(), table_ref, resolver)?; if expr_idx_cols_mask.get(*set_clause_col_idx) { break 'requires Some(DmlSafetyReason::KeyMutation); } } else if c.pos_in_table == *set_clause_col_idx { break 'requires Some(DmlSafetyReason::KeyMutation); } } } break 'requires None; }; Ok(reason) } /// Collect SubqueryResult IDs referenced in SET clause and RETURNING expressions. /// These subqueries must stay in the main update plan (evaluated during the update phase), /// not be moved to the ephemeral plan (which only collects rowids). fn collect_update_phase_subquery_ids( plan: &UpdatePlan, ) -> HashSet { use crate::translate::expr::walk_expr; use crate::translate::expr::WalkControl; let mut ids = HashSet::default(); let mut collector = |e: &ast::Expr| -> Result { if let ast::Expr::SubqueryResult { subquery_id, .. } = e { ids.insert(*subquery_id); } Ok(WalkControl::Continue) }; for (_, expr) in plan.set_clauses.iter() { let _ = walk_expr(expr, &mut collector); } if let Some(returning) = &plan.returning { for rc in returning { let _ = walk_expr(&rc.expr, &mut collector); } } ids } /// An ephemeral table is required if: /// 1. The UPDATE modifies any column that is present in the key of the btree used to iterate over the table. /// For regular table scans or seeks, the key is the rowid or the rowid alias column (INTEGER PRIMARY KEY). /// For index scans and seeks, the key is any column in the index used. /// 2. There are UPDATE triggers on the table (SQLite always uses ephemeral tables when triggers exist). /// /// The ephemeral table will accumulate all the rowids of the rows that are affected by the UPDATE, /// and then the temp table will be iterated over and the actual row updates performed. /// /// This is necessary because an UPDATE is implemented as a DELETE-then-INSERT operation, which could /// mess up the iteration order of the rows by changing the keys in the table/index that the iteration /// is performed over. The ephemeral table ensures stable iteration because it is not modified during /// the UPDATE loop. fn add_ephemeral_table_to_update_plan( program: &mut ProgramBuilder, plan: &mut UpdatePlan, ) -> Result<()> { let internal_id = program.table_reference_counter.next(); let ephemeral_table = Arc::new(BTreeTable { root_page: 0, // Not relevant for ephemeral table definition name: "ephemeral_scratch".to_string(), has_rowid: true, has_autoincrement: false, primary_key_columns: vec![], columns: vec![(*ROWID_COLUMN).clone()], is_strict: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }); let temp_cursor_id = program.alloc_cursor_id_keyed( CursorKey::table(internal_id), CursorType::BTreeTable(ephemeral_table.clone()), ); // The actual update loop will use the ephemeral table as the single [JoinedTable] which it then loops over. let table_references_update = TableReferences::new( vec![JoinedTable { table: Table::BTree(ephemeral_table.clone()), identifier: "ephemeral_scratch".to_string(), internal_id, op: Operation::Scan(Scan::BTreeTable { iter_dir: IterationDirection::Forwards, index: None, }), join_info: None, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id: 0, indexed: None, }], vec![], ); // Building the ephemeral table will use the TableReferences from the original plan -- i.e. if we chose an index scan originally, // we will build the ephemeral table by using the same index scan and using the same WHERE filters. let table_references_ephemeral_select = std::mem::replace(&mut plan.table_references, table_references_update); for table in table_references_ephemeral_select.joined_tables() { // The update loop needs to reference columns from the original source table, so we add it as an outer query reference. plan.table_references .add_outer_query_reference(OuterQueryReference { identifier: table.identifier.clone(), internal_id: table.internal_id, table: table.table.clone(), col_used_mask: table.col_used_mask.clone(), cte_select: None, cte_explicit_columns: vec![], cte_id: None, cte_definition_only: false, rowid_referenced: false, }); } // Preserve outer query references (e.g. CTEs) from the original plan. for outer_ref in table_references_ephemeral_select.outer_query_refs() { plan.table_references .add_outer_query_reference(outer_ref.clone()); } let join_order = table_references_ephemeral_select .joined_tables() .iter() .enumerate() .map(|(i, t)| JoinOrderMember { table_id: t.internal_id, original_idx: i, is_outer: t .join_info .as_ref() .is_some_and(|join_info| join_info.is_outer()), }) .collect(); let rowid_internal_id = table_references_ephemeral_select .joined_tables() .first() .unwrap() .internal_id; let ephemeral_plan = SelectPlan { table_references: table_references_ephemeral_select, result_columns: vec![ResultSetColumn { expr: Expr::RowId { database: None, table: rowid_internal_id, }, alias: None, contains_aggregates: false, }], where_clause: plan.where_clause.drain(..).collect(), group_by: None, // N/A order_by: vec![], // N/A aggregates: vec![], // N/A limit: None, // N/A query_destination: QueryDestination::EphemeralTable { cursor_id: temp_cursor_id, table: ephemeral_table, rowid_mode: EphemeralRowidMode::FromResultColumns, }, join_order, offset: None, contains_constant_false_condition: false, distinctness: super::plan::Distinctness::NonDistinct, values: vec![], window: None, input_cardinality_hint: None, estimated_output_rows: None, // Only move WHERE clause subqueries to the ephemeral plan. // SET clause and RETURNING clause subqueries must remain in the main update plan // because they compute new column values during the update phase (second pass), // not during row collection (first pass). Moving them here would cause correlated // subqueries in SET to evaluate with wrong cursor positions. non_from_clause_subqueries: { let update_phase_ids = collect_update_phase_subquery_ids(plan); let mut ephemeral_subs = Vec::new(); let mut remaining = Vec::new(); for sq in plan.non_from_clause_subqueries.drain(..) { if update_phase_ids.contains(&sq.internal_id) { remaining.push(sq); } else { ephemeral_subs.push(sq); } } plan.non_from_clause_subqueries = remaining; ephemeral_subs }, simple_aggregate: None, }; plan.ephemeral_plan = Some(ephemeral_plan); Ok(()) } fn optimize_subqueries(plan: &mut SelectPlan, schema: &Schema) -> Result<()> { for table in plan.table_references.joined_tables_mut() { if let Table::FromClauseSubquery(from_clause_subquery) = &mut table.table { let from_clause_subquery = Arc::make_mut(from_clause_subquery); // Use match to handle both SelectPlan and CompoundSelect variants match from_clause_subquery.plan.as_mut() { Plan::Select(select_plan) => optimize_select_plan(select_plan, schema)?, Plan::CompoundSelect { left, right_most, .. } => { optimize_select_plan(right_most, schema)?; for (select_plan, _) in left { optimize_select_plan(select_plan, schema)?; } } Plan::Delete(_) | Plan::Update(_) => { turso_soft_unreachable!( "DELETE/UPDATE plans should not appear in FROM clause subqueries" ); return Err(LimboError::InternalError( "DELETE/UPDATE plans should not appear in FROM clause subqueries" .to_string(), )); } } } } Ok(()) } /// Re-run correlated subqueries once the enclosing plan has learned a better /// estimate for how many times they will be invoked. /// /// This converges because `input_cardinality_hint` is monotonic within one /// optimization pass: once a plan receives a hint, later re-entry compares /// against that stored hint and skips re-optimization unless the new hint is /// strictly larger. The recursive call therefore only propagates larger hints /// down the subquery tree; it does not oscillate based on newly estimated row /// counts. fn reoptimize_correlated_subqueries(plan: &mut SelectPlan, schema: &Schema) -> Result<()> { let Some(invocation_hint) = plan .input_cardinality_hint .or(plan.estimated_output_rows) .filter(|hint| *hint > 1.0) else { return Ok(()); }; for subquery in &mut plan.non_from_clause_subqueries { if !subquery.correlated { continue; } let SubqueryState::Unevaluated { plan: Some(inner_plan), } = &mut subquery.state else { continue; }; if !select_plan_contains_cte_from_clause_subquery(inner_plan) { continue; } if inner_plan .input_cardinality_hint .is_some_and(|hint| hint >= invocation_hint) { continue; } inner_plan.input_cardinality_hint = Some(invocation_hint); optimize_select_plan(inner_plan, schema)?; } Ok(()) } /// Return whether this plan contains any FROM-clause CTE reference whose /// access-path choice may change when the enclosing invocation count grows. fn select_plan_contains_cte_from_clause_subquery(plan: &SelectPlan) -> bool { plan.table_references .joined_tables() .iter() .any(|table| match &table.table { Table::FromClauseSubquery(subquery) => { subquery.cte_id().is_some() || match subquery.plan.as_ref() { Plan::Select(select_plan) => { select_plan_contains_cte_from_clause_subquery(select_plan) } Plan::CompoundSelect { left, right_most, .. } => { left.iter().any(|(select_plan, _)| { select_plan_contains_cte_from_clause_subquery(select_plan) }) || select_plan_contains_cte_from_clause_subquery(right_most) } Plan::Delete(_) | Plan::Update(_) => false, } } _ => false, }) } #[allow(clippy::too_many_arguments)] fn optimize_table_access_with_custom_modules( result_columns: &mut [ResultSetColumn], table_references: &mut TableReferences, available_indexes: &HashMap>>, where_query: &mut [WhereTerm], order_by: &mut Vec<(Box, SortOrder)>, group_by: &mut Option, limit: &mut Option>, offset: &mut Option>, ) -> Result { let tables = table_references.joined_tables_mut(); if tables.is_empty() { return Ok(false); } // group by is not supported for now if group_by.is_some() { return Ok(false); } // Only optimize the first table with custom index methods. // This allows FTS to be used as the driving table in joins. let table = &mut tables[0]; let Some(indexes) = available_indexes.get(table.table.get_name()) else { return Ok(false); }; for index in indexes { let Some(module) = &index.index_method else { continue; }; if index.is_backing_btree_index() { continue; } let definition = module.definition(); for (pattern_idx, pattern) in definition.patterns.iter().enumerate() { let Some(pattern_match) = try_match_index_method_pattern( pattern, table, where_query, order_by, limit, offset, pattern_idx, false, // panic on binding failures ) else { continue; }; // Mark WHERE clause as consumed if let Some(where_covered) = pattern_match.where_covered { where_query[where_covered].consumed = true; } // Build covered columns mapping and update result_columns. // This differs from collect_index_method_candidates: we modify result_columns // and increment covered_column_id per matching query column, not per pattern column. let mut covered_column_id = 1_000_000; let mut covered_columns = HashMap::default(); for (pattern_column_id, pattern_column) in pattern_match.pattern_columns.iter().enumerate() { let ast::ResultColumn::Expr(pattern_expr, _) = pattern_column else { continue; }; let Some(substituted) = try_substitute_parameters(pattern_expr, &pattern_match.parameters) else { continue; }; for query_column in result_columns.iter_mut() { if !exprs_are_equivalent(&query_column.expr, &substituted) { continue; } query_column.expr = ast::Expr::Column { database: None, table: table.internal_id, column: covered_column_id, is_rowid_alias: false, }; covered_columns.insert(covered_column_id, pattern_column_id); covered_column_id += 1; } } // Calculate whether WHERE is completely covered for ORDER BY/LIMIT clearing let where_covered_completely = where_query.is_empty() || (pattern_match.where_covered.is_some() && where_query.len() == 1); // Only clear ORDER BY/LIMIT/OFFSET if: // 1. The pattern explicitly handles them (has ORDER BY/LIMIT), AND // 2. WHERE is completely covered (no post-filtering needed) // Otherwise, keep them so they're applied after post-filtering if pattern_match.pattern_has_order_by && where_covered_completely { let _ = order_by.drain(..); } if pattern_match.pattern_has_limit && where_covered_completely { let _ = limit.take(); let _ = offset.take(); } // Sort and collect arguments let arguments = sorted_arguments_from_parameters(&pattern_match.parameters); table.op = Operation::IndexMethodQuery(IndexMethodQuery { index: index.clone(), pattern_idx: pattern_match.pattern_idx, covered_columns, arguments, }); return Ok(true); } } Ok(false) } /// We do a single pass over projected, grouping, and ordering expressions to /// capture every expression that could be served directly from an expression index. /// Example: /// CREATE INDEX idx ON t(lower(a)); /// SELECT lower(a) FROM t ORDER BY lower(a); /// Both the SELECT list and ORDER BY can be covered by idx, avoiding a /// table cursor entirely. Recording them upfront lets both the cost model /// and covering checks reuse the same facts. fn register_expression_index_usages_for_plan( table_references: &mut TableReferences, result_columns: &[ResultSetColumn], order_by: &[(Box, SortOrder)], group_by: Option<&GroupBy>, ) { table_references.reset_expression_index_usages(); for rc in result_columns { table_references.register_expression_index_usage(&rc.expr); } for (expr, _) in order_by { table_references.register_expression_index_usage(expr); } if let Some(group_by) = group_by { for expr in &group_by.exprs { table_references.register_expression_index_usage(expr); } if let Some(having) = &group_by.having { for expr in having { table_references.register_expression_index_usage(expr); } } } } /// Derive a base row-count estimate for a table, preferring ANALYZE stats. fn base_row_estimate( schema: &Schema, table: &JoinedTable, params: &cost_params::CostModelParams, ) -> RowCountEstimate { match &table.table { Table::BTree(btree) => { if let Some(stats) = schema.analyze_stats.table_stats(&btree.name) { if let Some(rows) = stats.row_count.or_else(|| { stats .index_stats .values() .find_map(|idx_stat| idx_stat.total_rows) }) { return RowCountEstimate::AnalyzeStats(rows as f64); } } RowCountEstimate::hardcoded_fallback(params) } Table::FromClauseSubquery(subquery) => match subquery.plan.as_ref() { Plan::Select(plan) => { if let Some(rows) = plan.estimated_output_rows { return RowCountEstimate::AnalyzeStats(rows.max(1.0)); } RowCountEstimate::hardcoded_fallback(params) } Plan::CompoundSelect { left, right_most, .. } => { // Combine estimates from all branches according to set operation semantics. // left = [(A, op1), (B, op2)], right_most = C // represents: (A op1 B) op2 C // We fold left-to-right: seed with A, then apply op_i with plan_{i+1}. let fallback = *RowCountEstimate::hardcoded_fallback(params); let est = |p: &SelectPlan| p.estimated_output_rows.unwrap_or(fallback); let mut combined = left.first().map_or( right_most.estimated_output_rows.unwrap_or(fallback), |(p, _)| est(p), ); // The estimates to the right of each operator: left[1..].est, then right_most.est let rhs_estimates = left .iter() .skip(1) .map(|(p, _)| est(p)) .chain(std::iter::once(est(right_most))); for ((_, op), rhs) in left.iter().zip(rhs_estimates) { combined = match op { ast::CompoundOperator::UnionAll | ast::CompoundOperator::Union => { combined + rhs } ast::CompoundOperator::Intersect => combined.min(rhs), ast::CompoundOperator::Except => combined, }; } RowCountEstimate::AnalyzeStats(combined.max(1.0)) } _ => RowCountEstimate::hardcoded_fallback(params), }, _ => RowCountEstimate::hardcoded_fallback(params), } } /// Returns true if a WHERE-term predicate is null-rejecting for a table. /// /// Non-rejecting cases include: /// - `IS`/`IS NOT` comparisons, which can evaluate true for NULL-containing inputs. /// - Expressions that route the table's values through NULL-masking functions /// like `ifnull`/`coalesce`. fn where_term_is_null_rejecting_for_table( expr: &ast::Expr, operator: ConstraintOperator, table_id: ast::TableInternalId, ) -> bool { if matches!( operator, ConstraintOperator::AstNativeOperator(ast::Operator::Is | ast::Operator::IsNot) ) { return false; } !expr_has_null_masking_for_table(expr, table_id) } /// Returns true if an expression references a column from `table_id`. fn expr_references_table(expr: &ast::Expr, table_id: ast::TableInternalId) -> bool { use crate::translate::expr::{walk_expr, WalkControl}; let mut found = false; let _ = walk_expr(expr, &mut |inner: &ast::Expr| -> Result { match inner { ast::Expr::Column { table, .. } | ast::Expr::RowId { table, .. } if *table == table_id => { found = true; return Ok(WalkControl::SkipChildren); } _ => {} } Ok(WalkControl::Continue) }); found } /// Returns true if an expression is a NULL check on a column from `table_id`. /// Matches patterns like `col IS NULL`, `col IS NOT NULL`, `IsNull(col)`, `NotNull(col)`. fn is_null_check_on_table(expr: &ast::Expr, table_id: ast::TableInternalId) -> bool { match expr { ast::Expr::IsNull(inner) | ast::Expr::NotNull(inner) => { expr_references_table(inner, table_id) } ast::Expr::Binary(lhs, ast::Operator::Is | ast::Operator::IsNot, rhs) => { (matches!(rhs.as_ref(), ast::Expr::Literal(ast::Literal::Null)) && expr_references_table(lhs, table_id)) || (matches!(lhs.as_ref(), ast::Expr::Literal(ast::Literal::Null)) && expr_references_table(rhs, table_id)) } _ => false, } } /// Returns true if an expression uses a NULL-masking construct over columns from `table_id`. /// This includes NULL-masking functions (COALESCE, IFNULL) and CASE/IIF expressions /// that explicitly handle the NULL case for columns from the target table. fn expr_has_null_masking_for_table(expr: &ast::Expr, table_id: ast::TableInternalId) -> bool { use crate::translate::expr::{walk_expr, WalkControl}; let mut found = false; let _ = walk_expr(expr, &mut |e: &ast::Expr| -> Result { match e { ast::Expr::FunctionCall { name, args, .. } => { if let Ok(func) = crate::function::Func::resolve_function(name.as_str(), args.len()) { // IIF(cond, then, else) is like CASE WHEN cond THEN then ELSE else END. // If the condition is a null check on the target table, IIF masks nulls. if matches!( func, crate::function::Func::Scalar(crate::function::ScalarFunc::Iif) ) { if let Some(cond) = args.first() { if is_null_check_on_table(cond, table_id) { found = true; return Ok(WalkControl::SkipChildren); } } return Ok(WalkControl::Continue); } if !func.can_mask_nulls() { return Ok(WalkControl::Continue); } for arg in args { if expr_references_table(arg, table_id) { found = true; return Ok(WalkControl::SkipChildren); } } } } // CASE WHEN THEN ... ELSE ... END // If any WHEN condition checks for NULL on a column from the target table, // the CASE explicitly handles NULLs and can produce non-NULL results. ast::Expr::Case { when_then_pairs, .. } => { for (when_expr, _) in when_then_pairs { if is_null_check_on_table(when_expr, table_id) { found = true; return Ok(WalkControl::SkipChildren); } } } _ => {} } Ok(WalkControl::Continue) }); found } /// Enforce INDEXED BY / NOT INDEXED hints by validating index existence and /// filtering constraint candidates accordingly. fn enforce_indexed_by_hints( table_references: &TableReferences, available_indexes: &HashMap>>, constraints_per_table: &mut [TableConstraints], ) -> Result<()> { for (i, table_ref) in table_references.joined_tables().iter().enumerate() { let Some(ref indexed) = table_ref.indexed else { continue; }; let Some(btree) = table_ref.btree() else { continue; }; let Some(cs) = constraints_per_table.get_mut(i) else { continue; }; match indexed { ast::Indexed::IndexedBy(name) => { let idx_name = name.as_str(); // Verify the index exists and belongs to this table. let forced_index = available_indexes.get(&btree.name).and_then(|indexes| { indexes.iter().find(|idx| { idx.name.eq_ignore_ascii_case(idx_name) && idx.index_method.is_none() }) }); let Some(forced_index) = forced_index else { crate::bail_parse_error!("no such index: {}", idx_name); }; // Keep only the candidate for the forced index. let forced_index = forced_index.clone(); cs.candidates.retain(|c| { c.index .as_ref() .is_some_and(|idx| Arc::ptr_eq(idx, &forced_index)) }); // If no candidate survived (no WHERE constraints matched), add an empty one // so the optimizer can still scan the index. if cs.candidates.is_empty() { cs.candidates.push(ConstraintUseCandidate { index: Some(forced_index), refs: Vec::new(), }); } } ast::Indexed::NotIndexed => { // Remove all secondary index candidates, keep only rowid. cs.candidates.retain(|c| c.index.is_none()); } } } Ok(()) } /// Optimize the join order and index selection for a query. /// /// This function does the following: /// - Computes a set of [Constraint]s for each table. /// - Using those constraints, computes the best join order for the list of [TableReference]s /// and selects the best [crate::translate::optimizer::access_method::AccessMethod] for each table in the join order. /// - Mutates the [Operation]s in `joined_tables` to use the selected access methods. /// - Removes predicates from the `where_clause` that are now redundant due to the selected access methods. /// - Removes sorting operations if the selected join order and access methods satisfy the [crate::translate::optimizer::order::OrderTarget]. /// /// Returns the join order if it was optimized, or None if the default join order was considered best. #[allow(clippy::too_many_arguments)] fn optimize_table_access( schema: &Schema, result_columns: &mut [ResultSetColumn], table_references: &mut TableReferences, available_indexes: &HashMap>>, where_clause: &mut [WhereTerm], order_by: &mut Vec<(Box, SortOrder)>, group_by: &mut Option, simple_aggregate: Option<&SimpleAggregate>, subqueries: &[NonFromClauseSubquery], limit: &mut Option>, offset: &mut Option>, initial_input_cardinality: f64, ) -> Result> { // When optimizer_params feature is enabled, use lazily-loaded params (cached process-wide). // Otherwise, use the compile-time static for zero overhead. #[cfg(feature = "optimizer_params")] let params: &cost_params::CostModelParams = &cost_params::LOADED_PARAMS; #[cfg(not(feature = "optimizer_params"))] let params: &cost_params::CostModelParams = &cost_params::DEFAULT_PARAMS; if table_references.joined_tables().is_empty() { return Ok(None); } if table_references.joined_tables().len() > TableReferences::MAX_JOINED_TABLES { crate::bail_parse_error!( "Only up to {} tables can be joined", TableReferences::MAX_JOINED_TABLES ); } let has_expression_index = table_references.joined_tables().iter().any(|t| { matches!(&t.table, Table::BTree(btree) if available_indexes .get(&btree.name) .is_some_and(|indexes| indexes.iter().any(|index| index.is_expression_index()))) }); if has_expression_index { register_expression_index_usages_for_plan( table_references, result_columns, order_by.as_slice(), group_by.as_ref(), ); } // For single-table queries, try to optimize with custom index methods directly. // This is the fast path that preserves the original behavior. // Skip when INDEXED BY / NOT INDEXED is specified — those force a specific btree index or table scan. let is_single_table = table_references.joined_tables().len() == 1; let has_indexed_by_hint = table_references .joined_tables() .iter() .any(|t| t.indexed.is_some()); if is_single_table && !has_indexed_by_hint { let optimized = optimize_table_access_with_custom_modules( result_columns, table_references, available_indexes, where_clause, order_by, group_by, limit, offset, )?; if optimized { return Ok(None); } } let mut access_methods_arena = Vec::new(); // For multi-table queries, collect index method candidates to pass to the DP algorithm. // This allows the optimizer to consider index methods at any position in the join order. let base_table_rows_for_candidates = table_references .joined_tables() .iter() .map(|t| base_row_estimate(schema, t, params)) .collect::>(); let index_method_candidates = if !is_single_table { collect_index_method_candidates( table_references, available_indexes, where_clause, order_by, group_by, limit, offset, &base_table_rows_for_candidates, params, )? } else { Vec::new() }; let maybe_order_target = simple_aggregate .and_then(|sa| simple_aggregate_order_target(sa, table_references)) .or_else(|| compute_order_target(order_by, group_by.as_mut(), table_references)); let mut constraints_per_table = constraints_from_where_clause( where_clause, table_references, available_indexes, subqueries, schema, params, )?; // Enforce INDEXED BY / NOT INDEXED hints on constraint candidates. enforce_indexed_by_hints( table_references, available_indexes, &mut constraints_per_table, )?; let base_table_rows = table_references .joined_tables() .iter() .map(|t| base_row_estimate(schema, t, params)) .collect::>(); // Currently the expressions we evaluate as constraints are binary comparisons that (except for IS/IS NOT) // will never be true for a NULL operand. // If there are any constraints on the right hand side table of an outer join that are not part of the outer join condition, // the outer join can be converted into an inner join. // for example: // - SELECT * FROM t1 LEFT JOIN t2 ON false WHERE t2.id = 5 // there can never be a situation where null columns are emitted for t2 because t2.id = 5 will never be true in that case. // hence: we can convert the outer join into an inner join. // // Converting a LEFT JOIN into an INNER JOIN is an optimization opportunity: // it can enable join reordering and let more predicates participate in key selection. // -> recompute constraints if we rewrote a LEFT JOIN into an INNER JOIN. loop { let mut outer_join_rewritten = false; for (i, t) in table_references .joined_tables_mut() .iter_mut() .enumerate() .filter(|(_, t)| { t.join_info .as_ref() // Skip FULL OUTER JOIN tables: removing `outer` would suppress // unmatched-probe-row emission and prevent LeftJoinMetadata // allocation needed by the hash join. .is_some_and(|join_info| join_info.is_outer() && !join_info.is_full_outer()) }) { // Check if there's a constraint that would filter out NULL rows, // allowing us to convert the LEFT JOIN into an INNER JOIN for join reordering purposes. // Most binary ops like x = foo filter out NULL rows, but // IS NULL constraints do NOT - they specifically KEEP them. // So we should not convert LEFT JOIN to INNER JOIN based on IS NULL constraints. // Also, expressions wrapped in ifnull()/coalesce() are NOT null-rejecting because // they explicitly handle NULLs and can produce non-NULL values for NULL inputs. if constraints_per_table[i].constraints.iter().any(|c| { let is_from_where = where_clause[c.where_clause_pos.0].from_outer_join.is_none(); let is_null_rejecting = where_term_is_null_rejecting_for_table( &where_clause[c.where_clause_pos.0].expr, c.operator, t.internal_id, ); is_from_where && is_null_rejecting }) { t.join_info.as_mut().unwrap().join_type = JoinType::Inner; for term in where_clause.iter_mut() { if let Some(from_outer_join) = term.from_outer_join { if from_outer_join == t.internal_id { term.from_outer_join = None; } } } outer_join_rewritten = true; } } if !outer_join_rewritten { break; } constraints_per_table = constraints_from_where_clause( where_clause, table_references, available_indexes, subqueries, schema, params, )?; enforce_indexed_by_hints( table_references, available_indexes, &mut constraints_per_table, )?; } let planning_context = JoinPlanningContext { maybe_order_target: maybe_order_target.as_ref(), }; let Some(best_join_order_result) = compute_best_join_order_with_context( table_references.joined_tables(), initial_input_cardinality, planning_context, &constraints_per_table, &base_table_rows, &mut access_methods_arena, where_clause, subqueries, &index_method_candidates, params, &schema.analyze_stats, available_indexes, table_references, schema, )? else { return Ok(None); }; let BestJoinOrderResult { best_plan, best_ordered_plan, } = best_join_order_result; // See if best_ordered_plan is better than the overall best_plan if we add a sorting penalty // to the unordered plan's cost. let best_plan = if let Some(best_ordered_plan) = best_ordered_plan { let best_unordered_plan_cost = best_plan.cost; let best_ordered_plan_cost = best_ordered_plan.cost; const SORT_COST_PER_ROW_MULTIPLIER: f64 = 0.001; let sorting_penalty = Cost(best_plan.output_cardinality * SORT_COST_PER_ROW_MULTIPLIER); if best_unordered_plan_cost + sorting_penalty > best_ordered_plan_cost { best_ordered_plan } else { best_plan } } else { best_plan }; let final_output_cardinality = best_plan.output_cardinality; let mut sort_eliminated = false; // Eliminate sorting if possible. if let Some(order_target) = maybe_order_target.as_ref() { let satisfies_order_target = plan_satisfies_order_target( &best_plan, &access_methods_arena, table_references.joined_tables_mut(), order_target, schema, ); if satisfies_order_target { match &order_target.purpose { OrderTargetPurpose::EliminatesSort(EliminatesSortBy::Group) => { if let Some(g) = group_by.as_mut() { g.sort_elided = true; } } OrderTargetPurpose::EliminatesSort(EliminatesSortBy::Order) => { order_by.clear(); } OrderTargetPurpose::EliminatesSort(EliminatesSortBy::GroupByAndOrder) => { if let Some(g) = group_by.as_mut() { g.sort_elided = true; } order_by.clear(); } OrderTargetPurpose::Extremum => {} } } sort_eliminated = satisfies_order_target; } let (best_access_methods, best_table_numbers) = ( best_plan.best_access_methods().collect::>(), best_plan.table_numbers().collect::>(), ); // Collect hash join build/probe table indices. Build tables are excluded from the main // join order because they are consumed during hash build. A table may appear as both // probe and build (probe->build chaining) only when the build input is materialized. let (hash_join_build_tables, hash_join_probe_tables): (Vec, Vec) = best_access_methods .iter() .filter_map(|&am_idx| { let arena = &access_methods_arena; arena.get(am_idx).and_then(|am| { if let AccessMethodParams::HashJoin { build_table_idx, probe_table_idx, .. } = &am.params { Some((*build_table_idx, *probe_table_idx)) } else { None } }) }) .unzip(); #[cfg(debug_assertions)] { let mut probe_tables: HashSet = HashSet::default(); let mut build_tables: HashMap = HashMap::default(); let mut pos_by_table: Vec> = vec![None; table_references.joined_tables().len()]; for (pos, table_idx) in best_table_numbers.iter().enumerate() { pos_by_table[*table_idx] = Some(pos); } for &am_idx in best_access_methods.iter() { let arena = &access_methods_arena; let Some(am) = arena.get(am_idx) else { continue; }; if let AccessMethodParams::HashJoin { build_table_idx, probe_table_idx, materialize_build_input, .. } = &am.params { if let (Some(build_pos), Some(probe_pos)) = ( pos_by_table[*build_table_idx], pos_by_table[*probe_table_idx], ) { turso_assert!( probe_pos == build_pos + 1, "hash join build/probe tables are not adjacent in join order" ); } probe_tables.insert(*probe_table_idx); build_tables.insert(*build_table_idx, *materialize_build_input); } } for (build_table_idx, materialize_build_input) in build_tables { if probe_tables.contains(&build_table_idx) { turso_assert!( materialize_build_input, "probe->build chaining requires materialized build input" ); } } } let hash_join_build_only_tables: HashSet = hash_join_build_tables .iter() .copied() .filter(|table_idx| !hash_join_probe_tables.contains(table_idx)) .collect(); let best_join_order: Vec = best_table_numbers .iter() .filter(|table_number| { !hash_join_build_tables.contains(table_number) || hash_join_probe_tables.contains(table_number) }) .map(|&table_number| JoinOrderMember { table_id: table_references.joined_tables_mut()[table_number].internal_id, original_idx: table_number, is_outer: table_references.joined_tables_mut()[table_number] .join_info .as_ref() .is_some_and(|join_info| join_info.is_outer()), }) .collect(); // Mutate the Operations in `joined_tables` to use the selected access methods. // We iterate over ALL tables (including hash join build tables) to set their operations, // even though build tables are not in best_join_order. for (i, &table_idx) in best_table_numbers.iter().enumerate() { // Skip tables that already have an IndexMethodQuery operation set. // This happens when the first table was optimized with a custom index (e.g., FTS) // and we're continuing to optimize remaining tables in a multi-table query. if matches!( table_references.joined_tables()[table_idx].op, Operation::IndexMethodQuery(_) ) { continue; } let access_method = &mut access_methods_arena[best_access_methods[i]]; match &mut access_method.params { AccessMethodParams::BTreeTable { iter_dir, index, constraint_refs, } => { maybe_remove_index_candidate( index, &table_references.joined_tables()[table_idx], maybe_order_target.as_ref(), sort_eliminated, ); if constraint_refs.is_empty() { let is_leftmost_table = i == 0; let uses_index = index.is_some(); let try_to_build_ephemeral_index = !is_leftmost_table && !uses_index; if !try_to_build_ephemeral_index { table_references.joined_tables_mut()[table_idx].op = Operation::Scan(Scan::BTreeTable { iter_dir: *iter_dir, index: index.clone(), }); continue; } // This branch means we have a full table scan for a non-outermost table. // Try to construct an ephemeral index since it's going to be better than a scan. let table_id = table_references.joined_tables()[table_idx].internal_id; let table_constraints = constraints_per_table .iter() .find(|c| c.table_id == table_id); let Some(table_constraints) = table_constraints else { table_references.joined_tables_mut()[table_idx].op = Operation::Scan(Scan::BTreeTable { iter_dir: *iter_dir, index: index.clone(), }); continue; }; // Ephemeral indexes mirror rowid/column lookups. If the constraint targets an // expression (table_col_pos == None) we cannot derive a seek key that matches // the row layout, so fall back to a scan in that situation. let usable: Vec<(usize, &Constraint)> = table_constraints .constraints .iter() .enumerate() .filter(|(_, c)| c.usable && c.table_col_pos.is_some()) .collect(); // Find this table's position in best_join_order (which excludes build tables) let join_order_pos = best_join_order .iter() .position(|m| m.original_idx == table_idx) .unwrap_or_else(|| best_join_order.len().saturating_sub(1)); // Build a mapping from table_col_pos to index_col_pos. // Multiple constraints on the same column should share the same index_col_pos. // // This is important when a column appears in multiple constraints. // For example, in: // SELECT * FROM t1 LEFT JOIN t2 ON t1.a = t2.a AND t1.c = t2.c WHERE t2.a = 17 // // The constraints on t2 are: // t2.a = t1.a (from ON clause) // t2.c = t1.c (from ON clause) // t2.a = 17 (from WHERE clause) // // Both t2.a constraints must map to index_col_pos=0. If we incorrectly // assigned sequential index positions (0, 1, 2), the seek key would include // 3 components but the ephemeral index only has 2 key columns (t2.a, t2.c), // causing the seek to compare against the wrong columns and return no results. let mut unique_col_positions: Vec = usable .iter() .map(|(_, c)| c.table_col_pos.expect("table_col_pos was Some above")) .collect(); unique_col_positions.sort_unstable(); unique_col_positions.dedup(); // Map each usable constraint to a ConstraintRef. // Multiple constraints with the same table_col_pos share the same index_col_pos. let mut temp_constraint_refs: Vec = usable .iter() .map(|(orig_idx, c)| { let table_col_pos = c.table_col_pos.expect("table_col_pos was Some above"); let index_col_pos = unique_col_positions .binary_search(&table_col_pos) .expect("table_col_pos must exist in unique_col_positions"); ConstraintRef { constraint_vec_pos: *orig_idx, // index in the original constraints vec index_col_pos, sort_order: SortOrder::Asc, } }) .collect(); temp_constraint_refs.sort_by_key(|x| x.index_col_pos); let usable_constraint_refs = usable_constraints_for_join_order( &table_constraints.constraints, &temp_constraint_refs, &best_join_order[..=join_order_pos], ); if usable_constraint_refs.is_empty() { table_references.joined_tables_mut()[table_idx].op = Operation::Scan(Scan::BTreeTable { iter_dir: *iter_dir, index: index.clone(), }); continue; } let ephemeral_index = ephemeral_index_build( &table_references.joined_tables_mut()[table_idx], &usable_constraint_refs, ); mark_seek_constraints_consumed( &table_constraints.constraints, &usable_constraint_refs, where_clause, table_references.joined_tables()[table_idx] .join_info .as_ref() .is_some_and(|ji| ji.is_outer()), hash_join_build_only_tables.contains(&table_idx), ); let ephemeral_index = Arc::new(ephemeral_index); table_references.joined_tables_mut()[table_idx].op = Operation::Search(Search::Seek { index: Some(ephemeral_index), seek_def: build_seek_def_from_constraints( &table_constraints.constraints, &usable_constraint_refs, *iter_dir, where_clause, Some(table_references), )?, }); } else { let is_outer_join = table_references.joined_tables_mut()[table_idx] .join_info .as_ref() .is_some_and(|join_info| join_info.is_outer()); let defer_cross_table_constraints = hash_join_build_only_tables.contains(&table_idx); mark_seek_constraints_consumed( &constraints_per_table[table_idx].constraints, constraint_refs, where_clause, is_outer_join, defer_cross_table_constraints, ); if let Some(index) = &index { table_references.joined_tables_mut()[table_idx].op = Operation::Search(Search::Seek { index: Some(index.clone()), seek_def: build_seek_def_from_constraints( &constraints_per_table[table_idx].constraints, constraint_refs, *iter_dir, where_clause, Some(table_references), )?, }); continue; } turso_assert_eq!( constraint_refs.len(), 1, "expected exactly one constraint for rowid seek", {"constraint_refs": format!("{constraint_refs:?}")} ); table_references.joined_tables_mut()[table_idx].op = if let Some(ref eq) = constraint_refs[0].eq { Operation::Search(Search::RowidEq { cmp_expr: constraints_per_table[table_idx].constraints [eq.constraint_pos] .get_constraining_expr(where_clause, Some(table_references)) .1, }) } else { Operation::Search(Search::Seek { index: None, seek_def: build_seek_def_from_constraints( &constraints_per_table[table_idx].constraints, constraint_refs, *iter_dir, where_clause, Some(table_references), )?, }) }; } } AccessMethodParams::VirtualTable { idx_num, idx_str, constraints, constraint_usages, } => { table_references.joined_tables_mut()[table_idx].op = build_vtab_scan_op( where_clause, &constraints_per_table[table_idx], idx_num, idx_str, constraints, constraint_usages, Some(table_references), )?; } AccessMethodParams::Subquery { iter_dir } => { table_references.joined_tables_mut()[table_idx].op = Operation::Scan(Scan::Subquery { iter_dir: *iter_dir, }); } AccessMethodParams::MaterializedSubquery { index, constraint_refs, iter_dir, } => { let table_constraints = constraints_per_table .iter() .find(|c| c.table_id == table_references.joined_tables()[table_idx].internal_id) .expect("should have constraints for this table"); mark_seek_constraints_consumed( &table_constraints.constraints, constraint_refs, where_clause, false, false, ); // Build seek definition from the constraints let seek_def = build_seek_def_from_constraints( &table_constraints.constraints, constraint_refs, *iter_dir, where_clause, Some(table_references), )?; table_references.joined_tables_mut()[table_idx].op = Operation::Search(Search::Seek { index: Some(index.clone()), seek_def, }); } AccessMethodParams::HashJoin { build_table_idx, probe_table_idx, join_keys, mem_budget, materialize_build_input, use_bloom_filter, join_type, } => { // Mark WHERE clause terms as consumed since we're using hash join for join_key in join_keys.iter() { where_clause[join_key.where_clause_idx].consumed = true; } // Set up hash join operation on the probe table table_references.joined_tables_mut()[table_idx].op = Operation::HashJoin(HashJoinOp { build_table_idx: *build_table_idx, probe_table_idx: *probe_table_idx, join_keys: join_keys.clone(), mem_budget: *mem_budget, materialize_build_input: *materialize_build_input, use_bloom_filter: *use_bloom_filter, join_type: *join_type, }); } AccessMethodParams::IndexMethod { query, where_covered, } => { // Mark WHERE clause term as consumed if the index method covered it if let Some(idx) = where_covered { where_clause[*idx].consumed = true; } // Set up the index method query operation table_references.joined_tables_mut()[table_idx].op = Operation::IndexMethodQuery(query.clone()); } AccessMethodParams::MultiIndexScan { branches, where_term_idx, set_op, } => { // Mark the primary WHERE clause term as consumed where_clause[*where_term_idx].consumed = true; // For intersection, also mark additional consumed terms if let SetOperation::Intersection { additional_consumed_terms, } = set_op { for term_idx in additional_consumed_terms.iter() { where_clause[*term_idx].consumed = true; } } let w_idx = *where_term_idx; let s_op = set_op.clone(); // Build the MultiIndexScanOp from the branch parameters let mut multi_idx_branches = Vec::with_capacity(branches.len()); for branch in std::mem::take(branches) { let access = match branch.access { MultiIndexBranchAccessParams::Seek { constraints, constraint_refs, } => MultiIndexBranchAccess::Seek { seek_def: build_seek_def_from_constraints( &constraints, &constraint_refs, IterationDirection::Forwards, // Multi-index always scans forward where_clause, Some(table_references), )?, }, MultiIndexBranchAccessParams::InSeek { source } => { MultiIndexBranchAccess::InSeek { source } } }; multi_idx_branches.push(MultiIndexBranch { index: branch.index, access, estimated_rows: branch.estimated_rows, union_residuals: branch.residuals, }); } table_references.joined_tables_mut()[table_idx].op = Operation::MultiIndexScan(MultiIndexScanOp { branches: multi_idx_branches, where_term_idx: w_idx, set_op: s_op, }); } AccessMethodParams::InSeek { index, affinity, where_term_idx, } => { let source = match &where_clause[*where_term_idx].expr { Expr::InList { rhs, .. } => { let in_values: Vec = rhs.iter().map(|e| *e.clone()).collect(); InSeekSource::LiteralList { values: in_values, affinity: *affinity, } } Expr::SubqueryResult { query_type: SubqueryType::In { cursor_id, .. }, .. } => InSeekSource::Subquery { cursor_id: *cursor_id, }, _ => { return Err(crate::LimboError::InternalError( "InSeek where term is not an InList or SubqueryResult expression" .into(), )); } }; where_clause[*where_term_idx].consumed = true; table_references.joined_tables_mut()[table_idx].op = Operation::Search(Search::InSeek { index: index.clone(), source, }); } } } let mut probe_pos_by_table: Vec> = vec![None; table_references.joined_tables().len()]; for (pos, member) in best_join_order.iter().enumerate() { let table = &table_references.joined_tables()[member.original_idx]; if matches!(table.op, Operation::HashJoin(_)) { probe_pos_by_table[member.original_idx] = Some(pos); } } // If hash-join build constraints are still evaluated later (not consumed), // avoid materializing the build input to reduce redundant scans. for table in table_references.joined_tables_mut().iter_mut() { let Operation::HashJoin(hash_join_op) = &mut table.op else { continue; }; if !hash_join_op.materialize_build_input { continue; } let Some(probe_pos) = best_join_order .iter() .position(|member| member.original_idx == hash_join_op.probe_table_idx) else { continue; }; let build_table_was_prior_probe = probe_pos_by_table .get(hash_join_op.build_table_idx) .copied() .flatten() .is_some_and(|pos| pos < probe_pos); if build_table_was_prior_probe { continue; } let prior_mask = TableMask::from_table_number_iter( best_join_order[..probe_pos] .iter() .map(|member| member.original_idx), ); let join_key_indices: HashSet = hash_join_op .join_keys .iter() .map(|key| key.where_clause_idx) .collect(); let build_constraints = &constraints_per_table[hash_join_op.build_table_idx]; let mut has_prior_constraints = false; for constraint in build_constraints.constraints.iter() { if !constraint.lhs_mask.intersects(&prior_mask) { continue; } if join_key_indices.contains(&constraint.where_clause_pos.0) { continue; } has_prior_constraints = true; break; } if !has_prior_constraints { hash_join_op.materialize_build_input = false; } } Ok(Some(OptimizeTableAccessResult { join_order: best_join_order, output_rows: final_output_cardinality, min_max_fast_path: matches!(simple_aggregate, Some(SimpleAggregate::MinMax(_))) && sort_eliminated, })) } fn build_vtab_scan_op( where_clause: &mut [WhereTerm], table_constraints: &TableConstraints, idx_num: &i32, idx_str: &Option, vtab_constraints: &[ConstraintInfo], constraint_usages: &[ConstraintUsage], referenced_tables: Option<&TableReferences>, ) -> Result { if constraint_usages.len() != vtab_constraints.len() { return Err(LimboError::ExtensionError(format!( "Constraint usage count mismatch (expected {}, got {})", vtab_constraints.len(), constraint_usages.len() ))); } let mut constraints = vec![None; constraint_usages.len()]; let mut arg_count = 0; for (i, vtab_constraint) in vtab_constraints.iter().enumerate() { let usage = constraint_usages[i]; let argv_index = match usage.argv_index { Some(idx) if idx >= 1 && (idx as usize) <= constraint_usages.len() => idx, Some(idx) => { return Err(LimboError::ExtensionError(format!( "argv_index {} is out of valid range [1..{}]", idx, constraint_usages.len() ))); } None => continue, }; let zero_based_argv_index = (argv_index - 1) as usize; if constraints[zero_based_argv_index].is_some() { return Err(LimboError::ExtensionError(format!( "duplicate argv_index {argv_index}" ))); } let constraint = &table_constraints.constraints[vtab_constraint.index]; if usage.omit { where_clause[constraint.where_clause_pos.0].consumed = true; } let (_, expr, _) = constraint.get_constraining_expr(where_clause, referenced_tables); constraints[zero_based_argv_index] = Some(expr); arg_count += 1; } // Verify that used indices form a contiguous sequence starting from 1 let constraints = constraints .into_iter() .take(arg_count) .enumerate() .map(|(i, c)| { c.ok_or_else(|| { LimboError::ExtensionError(format!( "argv_index values must form contiguous sequence starting from 1, missing index {}", i + 1 )) }) }) .collect::>>()?; Ok(Operation::Scan(Scan::VirtualTable { idx_num: *idx_num, idx_str: idx_str.clone(), constraints, })) } /// Mark WHERE clause terms as consumed when they are covered by a seek /// (index seek, ephemeral auto-index seek, or rowid seek). /// /// `is_outer_join`: skip consuming non-ON WHERE terms for outer joins, because /// the cursor may land on a NULL-extended row that the WHERE filter must still /// reject (e.g. `SELECT * FROM t1 LEFT JOIN t2 ON false WHERE t2.id = 5`). /// /// `defer_cross_table`: skip cross-table constraints for hash-join build-only /// tables that lack a main-loop cursor — the probe side will evaluate them. fn mark_seek_constraints_consumed( constraints: &[Constraint], constraint_refs: &[RangeConstraintRef], where_clause: &mut [WhereTerm], is_outer_join: bool, defer_cross_table: bool, ) { for cref in constraint_refs.iter() { for pos in [ cref.eq.as_ref().map(|e| e.constraint_pos), cref.lower_bound, cref.upper_bound, ] { let Some(pos) = pos else { continue }; let constraint = &constraints[pos]; let where_term = &mut where_clause[constraint.where_clause_pos.0]; if where_term.consumed { continue; } if is_outer_join && where_term.from_outer_join.is_none() { continue; } if defer_cross_table && !constraint.lhs_mask.is_empty() { continue; } where_term.consumed = true; } } } #[derive(Debug, PartialEq, Clone)] enum ConstantConditionEliminationResult { Continue, ImpossibleCondition, } /// Removes predicates that are always true. /// Returns a ConstantEliminationResult indicating whether any predicates are always false. /// This is used to determine whether the query can be aborted early. fn eliminate_constant_conditions( where_clause: &mut [WhereTerm], ) -> Result { let mut i = 0; while i < where_clause.len() { let predicate = &where_clause[i]; if predicate.expr.is_always_true()? { // true predicates can be removed since they don't affect the result where_clause[i].consumed = true; i += 1; } else if predicate.expr.is_always_false()? { // any false predicate in a list of conjuncts (AND-ed predicates) will make the whole list false, // except an outer join condition, because that just results in NULLs, not skipping the whole loop if predicate.from_outer_join.is_some() { i += 1; continue; } where_clause .iter_mut() .for_each(|term| term.consumed = true); return Ok(ConstantConditionEliminationResult::ImpossibleCondition); } else { i += 1; } } Ok(ConstantConditionEliminationResult::Continue) } /// Check if the order target collation matches index column collations. /// Only remove the index when sort elimination selected this plan. fn maybe_remove_index_candidate( index: &mut Option>, table_reference: &JoinedTable, order_target: Option<&OrderTarget>, sort_eliminated: bool, ) { if !sort_eliminated { return; } if let Some((idx, order_target)) = index.as_mut().zip(order_target) { for col_order in &order_target.columns { // Only check columns from this table if col_order.table_id != table_reference.internal_id { continue; } // Find matching index column let matching_idx_col = match &col_order.target { ColumnTarget::Column(col_no) => { idx.columns.iter().find(|ic| ic.pos_in_table == *col_no) } ColumnTarget::RowId => { continue; } ColumnTarget::Expr(_expr) => { continue; } }; if let Some(idx_col) = matching_idx_col { // Index columns without explicit COLLATE use BINARY. // Treat them as BINARY for ordering compatibility checks. if col_order.collation != idx_col.collation.unwrap_or_default() { *index = None; return; } } } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AlwaysTrueOrFalse { AlwaysTrue, AlwaysFalse, } /** Helper trait for expressions that can be optimized Implemented for ast::Expr */ pub trait Optimizable { // if the expression is a constant expression that, when evaluated as a condition, is always true or false // return a [ConstantPredicate]. fn check_always_true_or_false(&self) -> Result>; fn is_always_true(&self) -> Result { Ok(self.check_always_true_or_false()? == Some(AlwaysTrueOrFalse::AlwaysTrue)) } fn is_always_false(&self) -> Result { Ok(self.check_always_true_or_false()? == Some(AlwaysTrueOrFalse::AlwaysFalse)) } fn is_constant(&self, resolver: &Resolver<'_>) -> bool; fn is_nonnull(&self, tables: &TableReferences) -> bool; } impl Optimizable for ast::Expr { /// Returns true if the expressions is (verifiably) non-NULL. /// It might still be non-NULL even if we return false; we just /// weren't able to prove it. /// This function is currently very conservative, and will return false /// for any expression where we aren't sure and didn't bother to find out /// by writing more complex code. fn is_nonnull(&self, tables: &TableReferences) -> bool { match self { Expr::SubqueryResult { .. } => false, Expr::Between { lhs, start, end, .. } => lhs.is_nonnull(tables) && start.is_nonnull(tables) && end.is_nonnull(tables), Expr::Binary(_, ast::Operator::Modulus | ast::Operator::Divide, _) => false, // 1 % 0, 1 / 0 Expr::Binary(expr, _, expr1) => expr.is_nonnull(tables) && expr1.is_nonnull(tables), Expr::Case { base, when_then_pairs, else_expr, .. } => { base.as_ref().is_none_or(|base| base.is_nonnull(tables)) && when_then_pairs .iter() .all(|(_, then)| then.is_nonnull(tables)) && else_expr .as_ref() .is_none_or(|else_expr| else_expr.is_nonnull(tables)) } Expr::Cast { expr, .. } => expr.is_nonnull(tables), Expr::Collate(expr, _) => expr.is_nonnull(tables), Expr::DoublyQualified(..) => { panic!("Do not call is_nonnull before DoublyQualified has been rewritten as Column") } Expr::Exists(..) => false, Expr::FunctionCall { .. } => false, Expr::FunctionCallStar { .. } => false, Expr::Id(..) => panic!("Do not call is_nonnull before Id has been rewritten as Column"), Expr::Column { table, column, is_rowid_alias, .. } => { if *is_rowid_alias { return true; } let (_, table_ref) = tables .find_table_by_internal_id(*table) .expect("table not found"); let columns = table_ref.columns(); let column = &columns[*column]; // Only INTEGER PRIMARY KEY (rowid alias) is implicitly NOT NULL. // Other PRIMARY KEY types (e.g., TEXT PRIMARY KEY) can contain NULL. column.is_rowid_alias() || column.notnull() } Expr::RowId { .. } => true, Expr::InList { lhs, rhs, .. } => { lhs.is_nonnull(tables) && (rhs.is_empty() || rhs.iter().all(|v| v.is_nonnull(tables))) } Expr::InSelect { .. } => false, Expr::InTable { .. } => false, Expr::IsNull(..) => true, Expr::Like { lhs, rhs, .. } => lhs.is_nonnull(tables) && rhs.is_nonnull(tables), Expr::Literal(literal) => match literal { ast::Literal::Numeric(_) => true, ast::Literal::String(_) => true, ast::Literal::Blob(_) => true, ast::Literal::Keyword(_) => true, ast::Literal::Null => false, ast::Literal::True => true, ast::Literal::False => true, ast::Literal::CurrentDate => true, ast::Literal::CurrentTime => true, ast::Literal::CurrentTimestamp => true, }, Expr::Name(..) => false, Expr::NotNull(..) => true, Expr::Parenthesized(exprs) => exprs.iter().all(|expr| expr.is_nonnull(tables)), Expr::Qualified(..) => { panic!("Do not call is_nonnull before Qualified has been rewritten as Column") } Expr::Raise(..) => false, Expr::Subquery(..) => false, Expr::Unary(_, expr) => expr.is_nonnull(tables), Expr::Variable(..) => false, Expr::Register(..) => false, // Register values can be null Expr::Array { .. } | Expr::Subscript { .. } => { unreachable!("Array and Subscript are desugared into function calls by the parser") } } } /// Returns true if the expression is a constant i.e. does not depend on columns and can be evaluated only once during the execution fn is_constant(&self, resolver: &Resolver<'_>) -> bool { match self { Expr::SubqueryResult { .. } => false, Expr::Between { lhs, start, end, .. } => { lhs.is_constant(resolver) && start.is_constant(resolver) && end.is_constant(resolver) } Expr::Binary(expr, _, expr1) => { expr.is_constant(resolver) && expr1.is_constant(resolver) } Expr::Case { base, when_then_pairs, else_expr, } => { base.as_ref().is_none_or(|base| base.is_constant(resolver)) && when_then_pairs.iter().all(|(when, then)| { when.is_constant(resolver) && then.is_constant(resolver) }) && else_expr .as_ref() .is_none_or(|else_expr| else_expr.is_constant(resolver)) } Expr::Cast { expr, .. } => expr.is_constant(resolver), Expr::Collate(expr, _) => expr.is_constant(resolver), // Not constant. Normally rewritten to Expr::Column by the optimizer, // but CHECK constraints bypass the rewrite pass and legitimately // contain DoublyQualified nodes. Expr::DoublyQualified(_, _, _) => false, Expr::Exists(_) => false, Expr::FunctionCall { args, name, filter_over, .. } => { if filter_over.over_clause.is_some() { return false; } let Some(func) = resolver.resolve_function(name.as_str(), args.len()) else { return false; }; func.is_deterministic() && args.iter().all(|arg| arg.is_constant(resolver)) } Expr::FunctionCallStar { .. } => false, Expr::Id(_) => true, Expr::Column { .. } => false, Expr::RowId { .. } => false, Expr::InList { lhs, rhs, .. } => { lhs.is_constant(resolver) && (rhs.is_empty() || rhs.iter().all(|v| v.is_constant(resolver))) } Expr::InSelect { .. } => { false // might be constant, too annoying to check subqueries etc. implement later } Expr::InTable { .. } => false, Expr::IsNull(expr) => expr.is_constant(resolver), Expr::Like { lhs, rhs, escape, .. } => { lhs.is_constant(resolver) && rhs.is_constant(resolver) && escape .as_ref() .is_none_or(|escape| escape.is_constant(resolver)) } Expr::Literal(_) => true, Expr::Name(_) => false, Expr::NotNull(expr) => expr.is_constant(resolver), Expr::Parenthesized(exprs) => exprs.iter().all(|expr| expr.is_constant(resolver)), // Not constant. Normally rewritten to Expr::Column by the optimizer, // but CHECK constraints bypass the rewrite pass and legitimately // contain Qualified nodes. Expr::Qualified(_, _) => false, Expr::Raise(_, expr) => expr.as_ref().is_none_or(|expr| expr.is_constant(resolver)), Expr::Subquery(_) => false, Expr::Unary(_, expr) => expr.is_constant(resolver), Expr::Variable(_) => true, Expr::Register(_) => false, Expr::Array { .. } | Expr::Subscript { .. } => { unreachable!("Array and Subscript are desugared into function calls by the parser") } } } /// Returns true if the expression is a constant expression that, when evaluated as a condition, is always true or false fn check_always_true_or_false(&self) -> Result> { match self { Self::Literal(lit) => match lit { ast::Literal::Numeric(b) => { if let Ok(int_value) = b.parse::() { return Ok(Some(if int_value == 0 { AlwaysTrueOrFalse::AlwaysFalse } else { AlwaysTrueOrFalse::AlwaysTrue })); } if let Ok(float_value) = b.parse::() { return Ok(Some(if float_value == 0.0 { AlwaysTrueOrFalse::AlwaysFalse } else { AlwaysTrueOrFalse::AlwaysTrue })); } Ok(None) } ast::Literal::String(s) => { // Use Numeric::from to match SQLite's string-to-numeric conversion, // which extracts leading numeric prefixes (e.g., '9S' -> 9, 'abc' -> 0) let without_quotes = s.trim_matches('\''); let numeric = Numeric::from(without_quotes); match numeric.to_bool() { true => Ok(Some(AlwaysTrueOrFalse::AlwaysTrue)), false => Ok(Some(AlwaysTrueOrFalse::AlwaysFalse)), } } _ => Ok(None), }, Self::Unary(op, expr) => { if *op == ast::UnaryOperator::Not { let trivial = expr.check_always_true_or_false()?; return Ok(trivial.map(|t| match t { AlwaysTrueOrFalse::AlwaysTrue => AlwaysTrueOrFalse::AlwaysFalse, AlwaysTrueOrFalse::AlwaysFalse => AlwaysTrueOrFalse::AlwaysTrue, })); } if *op == ast::UnaryOperator::Negative { let trivial = expr.check_always_true_or_false()?; return Ok(trivial); } Ok(None) } Self::InList { lhs: _, not, rhs } => { if rhs.is_empty() { return Ok(Some(if *not { AlwaysTrueOrFalse::AlwaysTrue } else { AlwaysTrueOrFalse::AlwaysFalse })); } Ok(None) } Self::Binary(lhs, op, rhs) => { let lhs_trivial = lhs.check_always_true_or_false()?; let rhs_trivial = rhs.check_always_true_or_false()?; match op { ast::Operator::And => { if lhs_trivial == Some(AlwaysTrueOrFalse::AlwaysFalse) || rhs_trivial == Some(AlwaysTrueOrFalse::AlwaysFalse) { return Ok(Some(AlwaysTrueOrFalse::AlwaysFalse)); } if lhs_trivial == Some(AlwaysTrueOrFalse::AlwaysTrue) && rhs_trivial == Some(AlwaysTrueOrFalse::AlwaysTrue) { return Ok(Some(AlwaysTrueOrFalse::AlwaysTrue)); } Ok(None) } ast::Operator::Or => { if lhs_trivial == Some(AlwaysTrueOrFalse::AlwaysTrue) || rhs_trivial == Some(AlwaysTrueOrFalse::AlwaysTrue) { return Ok(Some(AlwaysTrueOrFalse::AlwaysTrue)); } if lhs_trivial == Some(AlwaysTrueOrFalse::AlwaysFalse) && rhs_trivial == Some(AlwaysTrueOrFalse::AlwaysFalse) { return Ok(Some(AlwaysTrueOrFalse::AlwaysFalse)); } Ok(None) } _ => Ok(None), } } _ => Ok(None), } } } fn ephemeral_index_build( table_reference: &JoinedTable, constraint_refs: &[RangeConstraintRef], ) -> Index { let mut ephemeral_columns: Vec = table_reference .columns() .iter() .enumerate() .map(|(i, c)| IndexColumn { name: c.name.clone().unwrap(), order: SortOrder::Asc, pos_in_table: i, collation: c.collation_opt(), default: c.default.clone(), expr: None, }) // only include columns that are used in the query .filter(|c| table_reference.column_is_used(c.pos_in_table)) .collect(); // sort so that constraints first, then rest in whatever order they were in in the table ephemeral_columns.sort_by(|a, b| { let a_constraint = constraint_refs .iter() .enumerate() .find(|(_, c)| c.table_col_pos == Some(a.pos_in_table)); let b_constraint = constraint_refs .iter() .enumerate() .find(|(_, c)| c.table_col_pos == Some(b.pos_in_table)); match (a_constraint, b_constraint) { (Some(_), None) => Ordering::Less, (None, Some(_)) => Ordering::Greater, (Some((a_idx, _)), Some((b_idx, _))) => a_idx.cmp(&b_idx), (None, None) => Ordering::Equal, } }); let ephemeral_index = Index { name: format!( "ephemeral_{}_{}", table_reference.table.get_name(), table_reference.internal_id ), columns: ephemeral_columns, unique: false, ephemeral: true, table_name: table_reference.table.get_name().to_string(), root_page: 0, where_clause: None, has_rowid: table_reference .table .btree() .is_some_and(|btree| btree.has_rowid), index_method: None, on_conflict: None, }; ephemeral_index } /// Build a [SeekDef] for a given list of [Constraint]s pub fn build_seek_def_from_constraints( constraints: &[Constraint], constraint_refs: &[RangeConstraintRef], iter_dir: IterationDirection, where_clause: &[WhereTerm], referenced_tables: Option<&TableReferences>, ) -> Result { if constraint_refs.is_empty() { // Zero-prefix seeks are used for extremum scans over an already ordered // source: start at one end of the cursor and stop after the first // qualifying row. let (start_op, end_op) = match iter_dir { IterationDirection::Forwards => (SeekOp::GE { eq_only: true }, SeekOp::GT), IterationDirection::Backwards => (SeekOp::LE { eq_only: true }, SeekOp::LT), }; return Ok(SeekDef { prefix: Vec::new(), iter_dir, start: SeekKey { last_component: SeekKeyComponent::None, op: start_op, affinity: Affinity::Blob, }, end: SeekKey { last_component: SeekKeyComponent::None, op: end_op, affinity: Affinity::Blob, }, }); } // Extract the key values and operators let key = constraint_refs .iter() .map(|cref| cref.as_seek_range_constraint(constraints, where_clause, referenced_tables)) .collect(); let seek_def = build_seek_def(iter_dir, key)?; Ok(seek_def) } /// Build a [SeekDef] for a given [SeekRangeConstraint] and [IterationDirection]. /// To be usable as a seek key, all but potentially the last term must be equalities. /// The last term can be a nonequality (range with potentially one unbounded range). /// /// There are two parts to the seek definition: /// 1. start [SeekKey], which specifies the key that we will use to seek to the first row that matches the index key. /// 2. end [SeekKey], which specifies the key that we will use to terminate the index scan that follows the seek. /// /// There are some nuances to how, and which parts of, the index key can be used in the start and end [SeekKey]s, /// depending on the operator and iteration order. This function explains those nuances inline when dealing with /// each case. /// /// But to illustrate the general idea, consider the following examples: /// /// 1. For example, having two conditions like (x>10 AND y>20) cannot be used as a valid [SeekKey] GT(x:10, y:20) /// because the first row greater than (x:10, y:20) might be (x:11, y:19), which does not satisfy the where clause. /// In this case, only GT(x:10) must be used as the [SeekKey], and rows with y <= 20 must be filtered as a regular condition expression for each value of x. /// /// 2. In contrast, having (x=10 AND y>20) forms a valid index key GT(x:10, y:20) because after the seek, we can simply terminate as soon as x > 10, /// i.e. use GT(x:10, y:20) as the start [SeekKey] and GT(x:10) as the end. /// /// The preceding examples are for an ascending index. The logic is similar for descending indexes, but an important distinction is that /// since a descending index is laid out in reverse order, the comparison operators are reversed, e.g. LT becomes GT, LE becomes GE, etc. /// So when you see e.g. a SeekOp::GT below for a descending index, it actually means that we are seeking the first row where the index key is LESS than the seek key. /// fn build_seek_def( iter_dir: IterationDirection, mut key: Vec, ) -> Result { turso_assert!(!key.is_empty()); let last = key.last().unwrap(); // if we searching for exact key - emit definition immediately with prefix as a full key if last.eq.is_some() { let (start_op, end_op) = match iter_dir { IterationDirection::Forwards => (SeekOp::GE { eq_only: true }, SeekOp::GT), IterationDirection::Backwards => (SeekOp::LE { eq_only: true }, SeekOp::LT), }; return Ok(SeekDef { prefix: key, iter_dir, start: SeekKey { last_component: SeekKeyComponent::None, op: start_op, affinity: Affinity::Blob, }, end: SeekKey { last_component: SeekKeyComponent::None, op: end_op, affinity: Affinity::Blob, }, }); } turso_assert!(last.lower_bound.is_some() || last.upper_bound.is_some()); // pop last key as we will do some form of range search let last = key.pop().unwrap(); let has_upper_bound_only = last.upper_bound.is_some() && last.lower_bound.is_none(); let upper_bound_is_lt_or_le = matches!( last.upper_bound.as_ref().map(|(op, _, _)| op), Some(ast::Operator::Less | ast::Operator::LessEquals) ); // after that all key components must be equality constraints turso_debug_assert!(key.iter().all(|k| k.eq.is_some())); let has_prefix = !key.is_empty(); let apply_null_boundaries = |start: &mut SeekKey, end: &mut SeekKey| { // Sometimes we must add an extra NULL to the key on purpose. // We do this so scans over composite indexes match SQLite exactly. let start_is_prefix_only = matches!(start.last_component, SeekKeyComponent::None); let start_has_range_component = matches!(start.last_component, SeekKeyComponent::Expr(_)); let end_is_prefix_only = matches!(end.last_component, SeekKeyComponent::None); let end_has_range_component = matches!(end.last_component, SeekKeyComponent::Expr(_)); let start_is_forward_ge = matches!(start.op, SeekOp::GE { .. }) && matches!(iter_dir, IterationDirection::Forwards); let start_is_backward_le = matches!(start.op, SeekOp::LE { .. }) && matches!(iter_dir, IterationDirection::Backwards); let end_is_forward_gt = matches!(end.op, SeekOp::GT) && matches!(iter_dir, IterationDirection::Forwards); let end_is_backward_lt = matches!(end.op, SeekOp::LT) && matches!(iter_dir, IterationDirection::Backwards); // 1) Choose a better starting point. // // Example: // INDEX(c1, c2 ASC) // WHERE c1='a' AND c2<=999 // // If we start from key [c1='a'], we hit rows where c2 is NULL first. // For this case we want to start right after that NULL boundary. // So we: // - use start key [c1='a', NULL] // - change start op from GE to GT // - for backward scans in the symmetric shape, change LE to LT // // We only do this for "< / <=" ranges that do not also have a lower bound. if has_prefix && start_is_prefix_only && end_has_range_component && start_is_forward_ge && has_upper_bound_only && upper_bound_is_lt_or_le { start.last_component = SeekKeyComponent::Null; start.op = SeekOp::GT; } else if has_prefix && start_is_prefix_only && end_has_range_component && start_is_backward_le && has_upper_bound_only && upper_bound_is_lt_or_le { start.last_component = SeekKeyComponent::Null; start.op = SeekOp::LT; } // 2) Choose a better stopping point. // // Example: // INDEX(c1, c2 DESC) // WHERE c1='a' AND c2<=999 // // The stop check must also respect the NULL boundary for c2. // So we: // - use stop key [c1='a', NULL] // - change end op from GT to GE // - for backward scans, change LT to LE // // Same limit as above: only "< / <=" ranges with no lower bound. if has_prefix && end_is_prefix_only && start_has_range_component && end_is_forward_gt && has_upper_bound_only && upper_bound_is_lt_or_le { end.last_component = SeekKeyComponent::Null; end.op = SeekOp::GE { eq_only: false }; } else if has_prefix && end_is_prefix_only && start_has_range_component && end_is_backward_lt && has_upper_bound_only && upper_bound_is_lt_or_le { end.last_component = SeekKeyComponent::Null; end.op = SeekOp::LE { eq_only: false }; } }; // For the commented examples below, keep in mind that since a descending index is laid out in reverse order, the comparison operators are reversed, e.g. LT becomes GT, LE becomes GE, etc. // Also keep in mind that index keys are compared based on the number of columns given, so for example: // - if key is GT(x:10), then (x=10, y=usize::MAX) is not GT because only X is compared. (x=11, y=) is GT. // - if key is GT(x:10, y:20), then (x=10, y=21) is GT because both X and Y are compared. // - if key is GT(x:10, y:NULL), then (x=10, y=0) is GT because NULL is always LT in index key comparisons. Ok(match iter_dir { IterationDirection::Forwards => { let (mut start, mut end) = match last.sort_order { SortOrder::Asc => { let start = match last.lower_bound { // Forwards, Asc, GT: (x=10 AND y>20) // Start key: start from the first GT(x:10, y:20) Some((ast::Operator::Greater, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GT, affinity, }, // Forwards, Asc, GE: (x=10 AND y>=20) // Start key: start from the first GE(x:10, y:20) Some((ast::Operator::GreaterEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GE { eq_only: false }, affinity, }, // Forwards, Asc, None, (x=10 AND y<30) // Start key: start from the first GE(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::GE { eq_only: false }, affinity: Affinity::Blob, }, Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; let end = match last.upper_bound { // Forwards, Asc, LT, (x=10 AND y<30) // End key: end at first GE(x:10, y:30) Some((ast::Operator::Less, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GE { eq_only: false }, affinity, }, // Forwards, Asc, LE, (x=10 AND y<=30) // End key: end at first GT(x:10, y:30) Some((ast::Operator::LessEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GT, affinity, }, // Forwards, Asc, None, (x=10 AND y>20) // End key: end at first GT(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::GT, affinity: Affinity::Blob, }, Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; (start, end) } SortOrder::Desc => { let start = match last.upper_bound { // Forwards, Desc, LT: (x=10 AND y<30) // Start key: start from the first GT(x:10, y:30) Some((ast::Operator::Less, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GT, affinity, }, // Forwards, Desc, LE: (x=10 AND y<=30) // Start key: start from the first GE(x:10, y:30) Some((ast::Operator::LessEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GE { eq_only: false }, affinity, }, // Forwards, Desc, None: (x=10 AND y>20) // Start key: start from the first GE(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::GE { eq_only: false }, affinity: Affinity::Blob, }, Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; let end = match last.lower_bound { // Forwards, Asc, GT, (x=10 AND y>20) // End key: end at first GE(x:10, y:20) Some((ast::Operator::Greater, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GE { eq_only: false }, affinity, }, // Forwards, Asc, GE, (x=10 AND y>=20) // End key: end at first GT(x:10, y:20) Some((ast::Operator::GreaterEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::GT, affinity, }, // Forwards, Asc, None, (x=10 AND y<30) // End key: end at first GT(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::GT, affinity: Affinity::Blob, }, Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; (start, end) } }; apply_null_boundaries(&mut start, &mut end); SeekDef { prefix: key, iter_dir, start, end, } } IterationDirection::Backwards => { let (mut start, mut end) = match last.sort_order { SortOrder::Asc => { let start = match last.upper_bound { // Backwards, Asc, LT: (x=10 AND y<30) // Start key: start from the first LT(x:10, y:30) Some((ast::Operator::Less, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LT, affinity, }, // Backwards, Asc, LT: (x=10 AND y<=30) // Start key: start from the first LE(x:10, y:30) Some((ast::Operator::LessEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LE { eq_only: false }, affinity, }, // Backwards, Asc, None: (x=10 AND y>20) // Start key: start from the first LE(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::LE { eq_only: false }, affinity: Affinity::Blob, }, Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op) } }; let end = match last.lower_bound { // Backwards, Asc, GT, (x=10 AND y>20) // End key: end at first LE(x:10, y:20) Some((ast::Operator::Greater, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LE { eq_only: false }, affinity, }, // Backwards, Asc, GT, (x=10 AND y>=20) // End key: end at first LT(x:10, y:20) Some((ast::Operator::GreaterEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LT, affinity, }, // Backwards, Asc, None, (x=10 AND y<30) // End key: end at first LT(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::LT, affinity: Affinity::Blob, }, Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; (start, end) } SortOrder::Desc => { let start = match last.lower_bound { // Backwards, Desc, LT: (x=10 AND y>20) // Start key: start from the first LT(x:10, y:20) Some((ast::Operator::Greater, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LT, affinity, }, // Backwards, Desc, LE: (x=10 AND y>=20) // Start key: start from the first LE(x:10, y:20) Some((ast::Operator::GreaterEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LE { eq_only: false }, affinity, }, // Backwards, Desc, LE: (x=10 AND y<30) // Start key: start from the first LE(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::LE { eq_only: false }, affinity: Affinity::Blob, }, Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; let end = match last.upper_bound { // Backwards, Desc, LT, (x=10 AND y<30) // End key: end at first LE(x:10, y:30) Some((ast::Operator::Less, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LE { eq_only: false }, affinity, }, // Backwards, Desc, LT, (x=10 AND y<=30) // End key: end at first LT(x:10, y:30) Some((ast::Operator::LessEquals, bound, affinity)) => SeekKey { last_component: SeekKeyComponent::Expr(bound), op: SeekOp::LT, affinity, }, // Backwards, Desc, LT, (x=10 AND y>20) // End key: end at first LT(x:10) None => SeekKey { last_component: SeekKeyComponent::None, op: SeekOp::LT, affinity: Affinity::Blob, }, Some((op, _, _)) => { crate::bail_parse_error!("build_seek_def: invalid operator: {:?}", op,) } }; (start, end) } }; apply_null_boundaries(&mut start, &mut end); SeekDef { prefix: key, iter_dir, start, end, } } }) } #[cfg(test)] mod tests { use super::{where_term_is_null_rejecting_for_table, Optimizable}; use crate::translate::emitter::Resolver; use crate::{schema::Schema, DatabaseCatalog, RwLock, SymbolTable}; use rustc_hash::FxHashMap as HashMap; use turso_parser::ast::{self, Expr, FunctionTail, Name, TableInternalId}; fn empty_resolver<'a>( schema: &'a Schema, database_schemas: &'a RwLock>>, attached_databases: &'a RwLock, syms: &'a SymbolTable, ) -> Resolver<'a> { Resolver::new(schema, database_schemas, attached_databases, syms, true) } fn no_tail() -> FunctionTail { FunctionTail { filter_clause: None, over_clause: None, } } fn fn_call(name: &str, args: Vec) -> Expr { Expr::FunctionCall { name: Name::exact(name.to_string()), distinctness: None, args: args.into_iter().map(Box::new).collect(), order_by: vec![], filter_over: no_tail(), } } #[test] fn constant_classifier_for_coalesce_with_in_list() { let schema = Schema::new(); let syms = SymbolTable::new(); let database_schemas = RwLock::new(HashMap::default()); let attached_databases = RwLock::new(DatabaseCatalog::new()); let resolver = empty_resolver(&schema, &database_schemas, &attached_databases, &syms); let expr = fn_call( "coalesce", vec![ fn_call( "length", vec![Expr::Literal(ast::Literal::String("a".into()))], ), Expr::InList { lhs: Box::new(fn_call( "hex", vec![Expr::Literal(ast::Literal::Blob("01".into()))], )), not: false, rhs: vec![Box::new(Expr::Literal(ast::Literal::Blob("02".into())))], }, ], ); assert!(expr.is_constant(&resolver)); } #[test] fn constant_classifier_for_quote_of_column() { let schema = Schema::new(); let syms = SymbolTable::new(); let database_schemas = RwLock::new(HashMap::default()); let attached_databases = RwLock::new(DatabaseCatalog::new()); let resolver = empty_resolver(&schema, &database_schemas, &attached_databases, &syms); let expr = fn_call( "quote", vec![Expr::Column { database: None, table: TableInternalId::default(), column: 0, is_rowid_alias: false, }], ); assert!(!expr.is_constant(&resolver)); } #[test] fn null_rejection_detection_uses_function_resolution() { let table = TableInternalId::from(42); let expr = Expr::Binary( Box::new(fn_call( "IFNULL", vec![ Expr::Column { database: None, table, column: 0, is_rowid_alias: false, }, Expr::Literal(ast::Literal::Numeric("2147483647".into())), ], )), ast::Operator::GreaterEquals, Box::new(Expr::Literal(ast::Literal::Numeric("127".into()))), ); assert!(!where_term_is_null_rejecting_for_table( &expr, ast::Operator::GreaterEquals.into(), table )); } #[test] fn null_rejection_detection_requires_target_table_reference() { let target_table = TableInternalId::from(7); let other_table = TableInternalId::from(8); let expr = Expr::Binary( Box::new(fn_call( "coalesce", vec![ Expr::Column { database: None, table: other_table, column: 0, is_rowid_alias: false, }, Expr::Literal(ast::Literal::Numeric("0".into())), ], )), ast::Operator::Greater, Box::new(Expr::Literal(ast::Literal::Numeric("1".into()))), ); assert!(where_term_is_null_rejecting_for_table( &expr, ast::Operator::Greater.into(), target_table )); } #[test] fn null_rejection_detection_handles_nested_null_masking_functions() { let table = TableInternalId::from(9); let expr = Expr::Binary( Box::new(fn_call( "coalesce", vec![ fn_call( "ifnull", vec![ Expr::Column { database: None, table, column: 1, is_rowid_alias: false, }, Expr::Literal(ast::Literal::Numeric("0".into())), ], ), Expr::Literal(ast::Literal::Numeric("2".into())), ], )), ast::Operator::Equals, Box::new(Expr::Literal(ast::Literal::Numeric("2".into()))), ); assert!(!where_term_is_null_rejecting_for_table( &expr, ast::Operator::Equals.into(), table )); } #[test] fn null_rejection_detection_treats_is_operator_as_non_rejecting() { let table = TableInternalId::from(11); let expr = Expr::Binary( Box::new(Expr::Column { database: None, table, column: 0, is_rowid_alias: false, }), ast::Operator::Is, Box::new(Expr::Literal(ast::Literal::Null)), ); assert!(!where_term_is_null_rejecting_for_table( &expr, ast::Operator::Is.into(), table )); } #[test] fn null_rejection_detection_treats_is_between_columns_as_non_rejecting() { let table = TableInternalId::from(12); let expr = Expr::Binary( Box::new(Expr::Column { database: None, table, column: 0, is_rowid_alias: false, }), ast::Operator::Is, Box::new(Expr::Column { database: None, table, column: 1, is_rowid_alias: false, }), ); assert!(!where_term_is_null_rejecting_for_table( &expr, ast::Operator::Is.into(), table )); } #[test] fn null_rejection_detection_treats_is_with_non_null_literal_as_non_rejecting() { let table = TableInternalId::from(13); let expr = Expr::Binary( Box::new(Expr::Column { database: None, table, column: 0, is_rowid_alias: false, }), ast::Operator::Is, Box::new(Expr::Literal(ast::Literal::Numeric("5".into()))), ); assert!(!where_term_is_null_rejecting_for_table( &expr, ast::Operator::Is.into(), table )); } #[test] fn null_rejection_detection_treats_is_not_with_non_null_literal_as_non_rejecting() { let table = TableInternalId::from(14); let expr = Expr::Binary( Box::new(Expr::Column { database: None, table, column: 0, is_rowid_alias: false, }), ast::Operator::IsNot, Box::new(Expr::Literal(ast::Literal::Numeric("5".into()))), ); assert!(!where_term_is_null_rejecting_for_table( &expr, ast::Operator::IsNot.into(), table )); } #[test] fn null_rejection_detection_case_with_is_null_check_not_rejecting() { let table = TableInternalId::from(15); // CASE WHEN t.col IS NULL THEN 1 ELSE t.col END > 0 let expr = Expr::Binary( Box::new(Expr::Case { base: None, when_then_pairs: vec![( Box::new(Expr::IsNull(Box::new(Expr::Column { database: None, table, column: 0, is_rowid_alias: false, }))), Box::new(Expr::Literal(ast::Literal::Numeric("1".into()))), )], else_expr: Some(Box::new(Expr::Column { database: None, table, column: 0, is_rowid_alias: false, })), }), ast::Operator::Greater, Box::new(Expr::Literal(ast::Literal::Numeric("0".into()))), ); assert!(!where_term_is_null_rejecting_for_table( &expr, ast::Operator::Greater.into(), table )); } #[test] fn null_rejection_detection_case_without_null_check_is_rejecting() { let table = TableInternalId::from(16); // CASE WHEN t.col > 5 THEN t.col ELSE 0 END > 0 let expr = Expr::Binary( Box::new(Expr::Case { base: None, when_then_pairs: vec![( Box::new(Expr::Binary( Box::new(Expr::Column { database: None, table, column: 0, is_rowid_alias: false, }), ast::Operator::Greater, Box::new(Expr::Literal(ast::Literal::Numeric("5".into()))), )), Box::new(Expr::Column { database: None, table, column: 0, is_rowid_alias: false, }), )], else_expr: Some(Box::new(Expr::Literal(ast::Literal::Numeric("0".into())))), }), ast::Operator::Greater, Box::new(Expr::Literal(ast::Literal::Numeric("0".into()))), ); // CASE without IS NULL check doesn't mask nulls, so it IS null-rejecting assert!(where_term_is_null_rejecting_for_table( &expr, ast::Operator::Greater.into(), table )); } #[test] fn null_rejection_detection_iif_with_is_null_check_not_rejecting() { let table = TableInternalId::from(17); // IIF(t.col IS NULL, 1, t.col) > 0 let expr = Expr::Binary( Box::new(fn_call( "iif", vec![ Expr::IsNull(Box::new(Expr::Column { database: None, table, column: 0, is_rowid_alias: false, })), Expr::Literal(ast::Literal::Numeric("1".into())), Expr::Column { database: None, table, column: 0, is_rowid_alias: false, }, ], )), ast::Operator::Greater, Box::new(Expr::Literal(ast::Literal::Numeric("0".into()))), ); assert!(!where_term_is_null_rejecting_for_table( &expr, ast::Operator::Greater.into(), table )); } } ================================================ FILE: core/translate/optimizer/multi_index.rs ================================================ //! Multi-index-specific planning for OR-by-union and AND-by-intersection. //! //! This module owns the parts of planning that are unique to combining several //! index probes for the same table. It reuses the generic btree candidate //! chooser from `access_method.rs` for each individual branch, then layers the //! union/intersection-specific decomposition, costing, and residual handling on //! top. use crate::schema::{Index, Schema}; use crate::stats::AnalyzeStats; use crate::translate::expr::expr_references_any_subquery; use crate::translate::optimizer::access_method::{ choose_best_btree_candidate, choose_best_in_seek_candidate, AccessMethod, AccessMethodParams, BranchReadMode, ChosenInSeekCandidate, ResidualConstraintMode, }; use crate::translate::optimizer::constraints::{ analyze_binary_term_for_index, constraints_from_where_clause, Constraint, RangeConstraintRef, TableConstraints, }; use crate::translate::optimizer::cost::{ estimate_cost_for_scan_or_seek, estimate_rows_per_seek, rows_per_leaf_page_for_index, AnalyzeCtx, Cost, IndexInfo, RowCountEstimate, }; use crate::translate::optimizer::cost_params::CostModelParams; use crate::translate::plan::{ InSeekSource, JoinedTable, NonFromClauseSubquery, SetOperation, TableReferences, UnionBranchPrePostFilters, WhereTerm, }; use crate::translate::planner::{table_mask_from_expr, TableMask}; use rustc_hash::FxHashMap as HashMap; use smallvec::SmallVec; use std::{collections::VecDeque, sync::Arc}; use turso_parser::ast::{self, TableInternalId}; #[derive(Debug, Clone)] /// Parameters for a single branch of a multi-index scan. pub struct MultiIndexBranchParams { /// The index to use for this branch, or None for rowid access. pub index: Option>, /// How this branch probes the table/index. pub access: MultiIndexBranchAccessParams, /// Estimated number of rows from this branch. pub estimated_rows: f64, /// Residual filters for union (OR) branches. `None` for intersection branches. pub residuals: Option, } #[derive(Debug, Clone)] pub enum MultiIndexBranchAccessParams { Seek { constraints: Vec, constraint_refs: Vec, }, InSeek { source: InSeekSource, }, } /// Internal decomposition of an AND clause into intersection branches. #[derive(Debug)] struct AndClauseDecomposition { term_indices: Vec, branches: Vec, } /// One term that can participate in an AND-by-intersection plan. #[derive(Debug)] struct AndBranch { where_term_idx: usize, constraint: Constraint, index: Option>, constraint_refs: Vec, } /// Internal branch representation while evaluating a candidate multi-index plan. struct MultiIdxBranch { index: Option>, access: MultiIdxBranchAccess, cost: Cost, estimated_rows: f64, union_prepost_filters: Option, } enum MultiIdxBranchAccess { Seek { constraints: Vec, constraint_refs: Vec, }, InSeek { source: InSeekSource, constraint_idx: usize, }, } /// Flattens nested OR expressions into a list of disjuncts. /// /// For example, `(a OR b) OR c` becomes `[a, b, c]`. fn flatten_or_expr(expr: &ast::Expr) -> Vec<&ast::Expr> { match expr { ast::Expr::Binary(lhs, ast::Operator::Or, rhs) => { let mut result = flatten_or_expr(lhs); result.extend(flatten_or_expr(rhs)); result } _ => vec![expr], } } /// Flattens nested AND expressions into a list of conjuncts. /// /// For example, `(a AND b) AND c` becomes `[a, b, c]`. fn flatten_and_expr(expr: &ast::Expr) -> Vec<&ast::Expr> { match expr { ast::Expr::Binary(lhs, ast::Operator::And, rhs) => { let mut result = flatten_and_expr(lhs); result.extend(flatten_and_expr(rhs)); result } _ => vec![expr], } } /// Build temporary `WhereTerm`s from branch-local expressions and extract the /// constraints for exactly one target table. /// /// This is narrower than `constraints_from_where_clause()`: /// - `exprs` are synthetic planner inputs, not the query's real top-level /// `WHERE` terms. /// - The returned `WhereTerm`s are only suitable for branch-local planning /// and constraint bookkeeping for `table_reference`; they must not be reused /// for global predicate consumption or join rewrites. /// /// FIXME: stop synthesizing `WhereTerm`s here just to reuse /// `constraints_from_where_clause()`. Branch-local planning should have a /// direct constraint-extraction path that does not fabricate top-level planner /// terms. #[expect(clippy::too_many_arguments)] fn get_table_local_constraints_for_branch( exprs: &[ast::Expr], from_outer_join: Option, table_reference: &JoinedTable, table_references: &TableReferences, available_indexes: &HashMap>>, subqueries: &[NonFromClauseSubquery], schema: &Schema, params: &CostModelParams, ) -> crate::Result<(Vec, TableConstraints)> { let synthetic_where_terms = exprs .iter() .cloned() .map(|expr| WhereTerm { expr, from_outer_join, consumed: false, }) .collect::>(); let table_constraints = constraints_from_where_clause( &synthetic_where_terms, table_references, available_indexes, subqueries, schema, params, )? .into_iter() .find(|constraints| constraints.table_id == table_reference.internal_id) .expect("constraints_from_where_clause must return constraints for every joined table"); let mut table_constraints = table_constraints; // Branch-local constraints originate from synthetic `WhereTerm`s, so copy // out their constraining expressions while those temporary terms still // exist. for constraint in table_constraints.constraints.iter_mut() { if constraint.constraining_expr.is_some() || constraint.operator.as_ast_operator().is_none() { continue; } constraint.constraining_expr = Some(constraint.get_constraining_expr(&synthetic_where_terms, Some(table_references))); } Ok((synthetic_where_terms, table_constraints)) } /// Estimate the cost of a multi-index union scan (OR-by-union optimization). /// /// The cost model accounts for: /// 1. Cost of each branch scan /// 2. RowSet insert/test work needed to deduplicate rowids /// 3. Table fetches after deduplication /// 4. Overlap between branches, approximated from independent selectivities fn estimate_multi_index_scan_cost( branch_costs: &[Cost], branch_rows: &[f64], base_row_count: RowCountEstimate, input_cardinality: f64, params: &CostModelParams, ) -> (Cost, f64) { let base_row_count = *base_row_count; // Total cost of all branch scans. let branch_scan_cost: f64 = branch_costs.iter().map(|c| c.0).sum(); // Sum of branch row counts before RowSet deduplication. let total_rows_before_dedup: f64 = branch_rows.iter().sum(); // Estimate overlap between branches. For independent predicates: // P(A OR B) = 1 - (1 - P(A)) * (1 - P(B)) let mut unique_row_ratio = 1.0f64; for rows in branch_rows.iter() { let branch_selectivity = (*rows / base_row_count).min(1.0); unique_row_ratio *= 1.0 - branch_selectivity; } let estimated_unique_rows = base_row_count * (1.0 - unique_row_ratio); // RowSet operations do an insert and membership test per candidate rowid. let rowset_ops_cost = total_rows_before_dedup * params.cpu_cost_per_row * 2.0; // Table fetch cost mirrors single-index lookup costing, assuming some // locality benefit from rowid-ordered access after RowSet deduplication. let table_pages = (base_row_count / params.rows_per_table_page).max(1.0); let selectivity = estimated_unique_rows / base_row_count.max(1.0); let table_fetch_cost = selectivity * table_pages; let total_cost = (branch_scan_cost + rowset_ops_cost + table_fetch_cost) * input_cardinality; (Cost(total_cost), estimated_unique_rows) } /// Estimate the cost of a multi-index intersection (AND-by-intersection). /// /// The cost model accounts for: /// 1. Cost of each branch scan /// 2. RowSet test work while intersecting rowids /// 3. Table fetches for the surviving rowids /// 4. Final result size as the product of branch selectivities fn estimate_multi_index_intersection_cost( branch_costs: &[Cost], branch_rows: &[f64], base_row_count: RowCountEstimate, input_cardinality: f64, params: &CostModelParams, ) -> (Cost, f64) { let base_row_count = *base_row_count; // Total cost of all branch scans. let branch_scan_cost: f64 = branch_costs.iter().map(|c| c.0).sum(); // Estimate intersection result as the product of selectivities: // P(A AND B) = P(A) * P(B) let mut intersection_selectivity = 1.0f64; for rows in branch_rows.iter() { let branch_selectivity = (*rows / base_row_count).min(1.0); intersection_selectivity *= branch_selectivity; } let estimated_intersection_rows = (base_row_count * intersection_selectivity).max(1.0); // First branch inserts rowids; later branches test against the RowSet. let first_branch_rows = branch_rows.first().copied().unwrap_or(0.0); let subsequent_branch_rows: f64 = branch_rows.iter().skip(1).sum(); let rowset_ops_cost = (first_branch_rows + subsequent_branch_rows) * params.cpu_cost_per_row * 1.5; // Table fetch cost mirrors single-index lookup costing, assuming some // locality benefit from rowid-ordered access after intersection. let table_pages = (base_row_count / params.rows_per_table_page).max(1.0); let selectivity = estimated_intersection_rows / base_row_count.max(1.0); let table_fetch_cost = selectivity * table_pages; let total_cost = (branch_scan_cost + rowset_ops_cost + table_fetch_cost) * input_cardinality; (Cost(total_cost), estimated_intersection_rows) } /// Compute [`IndexInfo`] for a multi-index branch. /// /// RowSet-building branches only need rowids from the scan, so an index can be /// treated as covering even if it does not contain all later table columns. fn index_info_for_branch( index: Option<&Index>, rhs_table: &JoinedTable, read_mode: BranchReadMode, rows_per_table_page: f64, ) -> Option { let rowid_only = matches!(read_mode, BranchReadMode::RowIdOnly); match index { Some(index) => Some(IndexInfo { unique: index.unique, covering: rowid_only || rhs_table.index_is_covering(index), column_count: index.columns.len(), rows_per_leaf_page: rows_per_leaf_page_for_index( index.columns.len(), rhs_table, rows_per_table_page, ), }), None => Some(IndexInfo { unique: true, covering: true, column_count: 1, rows_per_leaf_page: rows_per_table_page, }), } } fn in_seek_source_from_expr( expr: &ast::Expr, chosen: &ChosenInSeekCandidate, ) -> Option { match expr { ast::Expr::InList { rhs, .. } => Some(InSeekSource::LiteralList { values: rhs.iter().map(|e| *e.clone()).collect(), affinity: chosen.affinity, }), ast::Expr::SubqueryResult { query_type: ast::SubqueryType::In { cursor_id, .. }, .. } => Some(InSeekSource::Subquery { cursor_id: *cursor_id, }), _ => None, } } #[allow(clippy::too_many_arguments)] fn choose_multi_index_branch_access( rhs_table: &JoinedTable, table_constraints: &TableConstraints, branch_terms: &[WhereTerm], lhs_mask: &TableMask, rhs_idx: usize, schema: &Schema, base_row_count: RowCountEstimate, analyze_stats: &AnalyzeStats, params: &CostModelParams, ) -> crate::Result> { let chosen_seek = choose_best_btree_candidate( rhs_table, table_constraints, lhs_mask, rhs_idx, None, schema, analyze_stats, 1.0, base_row_count, params, ); let mut best_branch = chosen_seek .as_ref() .filter(|chosen| !chosen.constraint_refs.is_empty()) .map(|chosen| { let index_info = index_info_for_branch( chosen.index.as_deref(), rhs_table, BranchReadMode::RowIdOnly, params.rows_per_table_page, ) .expect("multi-index branches always have costable access"); let analyze_ctx = AnalyzeCtx { rhs_table, index: chosen.index.as_ref(), stats: analyze_stats, }; let branch_cost = estimate_cost_for_scan_or_seek( Some(index_info), &table_constraints.constraints, &chosen.constraint_refs, 1.0, base_row_count, false, params, Some(&analyze_ctx), ); MultiIdxBranch { index: chosen.index.clone(), access: MultiIdxBranchAccess::Seek { constraints: table_constraints.constraints.clone(), constraint_refs: chosen.constraint_refs.clone(), }, cost: branch_cost, estimated_rows: estimate_rows_per_seek( index_info, &table_constraints.constraints, &chosen.constraint_refs, base_row_count, Some(&analyze_ctx), ), union_prepost_filters: None, } }); let in_seek_threshold = best_branch .as_ref() .map(|branch| branch.cost) .unwrap_or(Cost(f64::INFINITY)); if let Some(chosen_in_seek) = choose_best_in_seek_candidate( rhs_table, table_constraints, lhs_mask, 1.0, base_row_count, params, in_seek_threshold, BranchReadMode::RowIdOnly, )? { let Some(source) = in_seek_source_from_expr( &branch_terms[chosen_in_seek.constraint_idx].expr, &chosen_in_seek, ) else { return Ok(None); }; best_branch = Some(MultiIdxBranch { index: chosen_in_seek.index, access: MultiIdxBranchAccess::InSeek { source, constraint_idx: chosen_in_seek.constraint_idx, }, cost: chosen_in_seek.cost, estimated_rows: chosen_in_seek.estimated_rows_per_outer_row, union_prepost_filters: None, }); } Ok(best_branch) } /// Residual output from [`partition_residual_multi_or_exprs`]. struct MultiOrResidualPrePostFilters { pre_filter_exprs: Vec, post_filter_exprs: Vec, /// Combined table mask for `post_filter_exprs`. post_mask: TableMask, } /// Classify unconsumed branch conjuncts into pre-filters (outer-table-only, /// evaluated before the index seek) and post-filters (evaluated after the seek). /// /// Returns `None` if any residual contains a subquery or has an unresolvable /// table mask—matching the old `residual_tables_mask` rejection. fn partition_residual_multi_or_exprs( branch_terms: &[WhereTerm], access: &MultiIdxBranchAccess, lhs_mask: &TableMask, table_references: &TableReferences, subqueries: &[NonFromClauseSubquery], ) -> Option { let mut consumed = vec![false; branch_terms.len()]; match access { MultiIdxBranchAccess::Seek { constraints, constraint_refs, } => { for cref in constraint_refs.iter() { for idx in [ cref.eq.as_ref().map(|e| e.constraint_pos), cref.lower_bound, cref.upper_bound, ] .into_iter() .flatten() { consumed[constraints[idx].where_clause_pos.0] = true; } } } MultiIdxBranchAccess::InSeek { constraint_idx, .. } => consumed[*constraint_idx] = true, } let mut pre_filter_exprs = Vec::new(); let mut post_filter_exprs = Vec::new(); let mut post_mask = TableMask::new(); for (idx, term) in branch_terms.iter().enumerate() { if consumed[idx] { continue; } let expr = &term.expr; if expr_references_any_subquery(expr) { return None; } let mask = table_mask_from_expr(expr, table_references, subqueries).ok()?; if lhs_mask.contains_all(&mask) { pre_filter_exprs.push(expr.clone()); } else { post_mask |= mask; post_filter_exprs.push(expr.clone()); } } Some(MultiOrResidualPrePostFilters { pre_filter_exprs, post_filter_exprs, post_mask, }) } /// Estimate selectivity for a residual predicate that remains after a branch /// seek is chosen. /// /// We keep this intentionally heuristic: recurse through boolean structure and, /// for leaf predicates, reuse normal constraint selectivity analysis when the /// expression can be recognized as a single-table constraint. #[allow(clippy::too_many_arguments)] fn estimate_residual_expr_selectivity( expr: &ast::Expr, rhs_table: &JoinedTable, table_references: &TableReferences, available_indexes: &HashMap>>, subqueries: &[NonFromClauseSubquery], schema: &Schema, params: &CostModelParams, ) -> f64 { let Ok(expr) = crate::translate::expr::unwrap_parens(expr) else { return params.sel_other; }; match expr { ast::Expr::Binary(lhs, ast::Operator::And, rhs) => { estimate_residual_expr_selectivity( lhs, rhs_table, table_references, available_indexes, subqueries, schema, params, ) * estimate_residual_expr_selectivity( rhs, rhs_table, table_references, available_indexes, subqueries, schema, params, ) } ast::Expr::Binary(lhs, ast::Operator::Or, rhs) => { let lhs_selectivity = estimate_residual_expr_selectivity( lhs, rhs_table, table_references, available_indexes, subqueries, schema, params, ); let rhs_selectivity = estimate_residual_expr_selectivity( rhs, rhs_table, table_references, available_indexes, subqueries, schema, params, ); 1.0 - (1.0 - lhs_selectivity) * (1.0 - rhs_selectivity) } ast::Expr::Unary(ast::UnaryOperator::Not, inner) => { 1.0 - estimate_residual_expr_selectivity( inner, rhs_table, table_references, available_indexes, subqueries, schema, params, ) } _ => { let Ok((_, table_constraints)) = get_table_local_constraints_for_branch( &[expr.clone()], None, rhs_table, table_references, available_indexes, subqueries, schema, params, ) else { return params.sel_other; }; table_constraints .constraints .iter() .filter(|constraint| constraint.where_clause_pos.0 == 0) .map(|constraint| constraint.selectivity) // A single residual expression can sometimes yield multiple // derived constraints (for example, self-comparisons). Use the // strongest single estimate instead of multiplying duplicates. .reduce(f64::min) .unwrap_or(params.sel_other) } } .clamp(0.0, 1.0) } #[allow(clippy::too_many_arguments)] fn estimate_multi_or_residual_selectivity( residual_exprs: &[ast::Expr], rhs_table: &JoinedTable, table_references: &TableReferences, available_indexes: &HashMap>>, subqueries: &[NonFromClauseSubquery], schema: &Schema, params: &CostModelParams, ) -> f64 { residual_exprs .iter() .map(|expr| { estimate_residual_expr_selectivity( expr, rhs_table, table_references, available_indexes, subqueries, schema, params, ) }) .product::() .clamp(0.0, 1.0) } #[allow(clippy::too_many_arguments)] /// Evaluate a fully decomposed multi-index plan and return it if it beats the /// current best non-multi-index access cost. fn evaluate_multi_index_branches( branches: Vec, set_op: SetOperation, where_term_idx: usize, rhs_table: &JoinedTable, table_references: &TableReferences, available_indexes: &HashMap>>, subqueries: &[NonFromClauseSubquery], schema: &Schema, base_row_count: RowCountEstimate, input_cardinality: f64, params: &CostModelParams, best_cost: Cost, ) -> Option { let mut branch_costs = Vec::with_capacity(branches.len()); let mut branch_rows = Vec::with_capacity(branches.len()); let mut branch_params = Vec::with_capacity(branches.len()); for branch in branches { let post_filter_exprs = branch .union_prepost_filters .as_ref() .map(|r| &r.post_filter_exprs); let selectivity = if let Some(post_filter_exprs) = post_filter_exprs { estimate_multi_or_residual_selectivity( post_filter_exprs, rhs_table, table_references, available_indexes, subqueries, schema, params, ) } else { 1.0 }; let estimated_rows = branch.estimated_rows * selectivity; let params_for_branch = MultiIndexBranchParams { index: branch.index.clone(), access: match branch.access { MultiIdxBranchAccess::Seek { constraints, constraint_refs, } => MultiIndexBranchAccessParams::Seek { constraints, constraint_refs, }, MultiIdxBranchAccess::InSeek { source, .. } => { MultiIndexBranchAccessParams::InSeek { source } } }, estimated_rows, residuals: branch.union_prepost_filters, }; branch_costs.push(branch.cost); branch_rows.push(params_for_branch.estimated_rows); branch_params.push(params_for_branch); } let (multi_index_cost, estimated_rows) = match &set_op { SetOperation::Union => estimate_multi_index_scan_cost( &branch_costs, &branch_rows, base_row_count, input_cardinality, params, ), SetOperation::Intersection { .. } => estimate_multi_index_intersection_cost( &branch_costs, &branch_rows, base_row_count, input_cardinality, params, ), }; if multi_index_cost < best_cost { let mut consumed_where_terms = SmallVec::<[usize; 4]>::new(); consumed_where_terms.push(where_term_idx); if let SetOperation::Intersection { additional_consumed_terms, } = &set_op { for term_idx in additional_consumed_terms.iter().copied() { if !consumed_where_terms.contains(&term_idx) { consumed_where_terms.push(term_idx); } } } for branch in &branch_params { if let MultiIndexBranchAccessParams::Seek { constraints, .. } = &branch.access { for constraint in constraints { let where_term_idx = constraint.where_clause_pos.0; if !consumed_where_terms.contains(&where_term_idx) { consumed_where_terms.push(where_term_idx); } } } } Some(AccessMethod { cost: multi_index_cost, estimated_rows_per_outer_row: estimated_rows, residual_constraints: ResidualConstraintMode::ApplyUnconsumed, consumed_where_terms, params: AccessMethodParams::MultiIndexScan { branches: branch_params, where_term_idx, set_op, }, }) } else { None } } #[allow(clippy::too_many_arguments)] /// Analyze top-level AND terms to determine whether they can be executed as an /// AND-by-intersection plan. /// /// Returns `Some(...)` only when: /// 1. Multiple terms constrain the same table /// 2. Each term is individually indexable /// 3. No single composite index already covers multiple terms more directly /// 4. At least two distinct indexes participate in the final branch set fn analyze_and_terms_for_multi_index( table_reference: &JoinedTable, where_clause: &[WhereTerm], available_indexes: &HashMap>>, table_references: &TableReferences, subqueries: &[NonFromClauseSubquery], schema: &Schema, params: &CostModelParams, ) -> Option { let table_id = table_reference.internal_id; let table_name = table_reference.table.get_name(); let indexes = available_indexes.get(table_name); let rowid_alias_column = table_reference .columns() .iter() .position(|c| c.is_rowid_alias()); // Collect AND terms that: // 1. Reference this table // 2. Are simple binary comparisons // 3. Can use an index // 4. Are not already consumed // 5. Are local constraints rather than cross-table join conditions let mut candidate_branches: Vec = Vec::new(); let mut columns_used: Vec> = Vec::new(); for (where_term_idx, term) in where_clause.iter().enumerate() { if term.consumed { continue; } if matches!(&term.expr, ast::Expr::Binary(_, ast::Operator::Or, _)) { continue; } let Some(analyzed) = analyze_binary_term_for_index( &term.expr, where_term_idx, table_id, table_reference, indexes, rowid_alias_column, available_indexes, table_references, subqueries, schema, params, ) else { continue; }; if !analyzed.constraint.lhs_mask.is_empty() { continue; } columns_used.push(analyzed.constraint.table_col_pos); candidate_branches.push(AndBranch { where_term_idx, constraint: analyzed.constraint, index: analyzed.best_index, constraint_refs: analyzed.constraint_refs, }); } if candidate_branches.len() < 2 { return None; } // If a composite index already covers multiple constrained columns, prefer // that single lookup path over intersection. if let Some(indexes) = indexes { for index in indexes.iter().filter(|idx| idx.index_method.is_none()) { let mut columns_covered = 0; for (i, col_pos) in columns_used.iter().enumerate() { if let Some(col_pos) = col_pos { if let Some(idx_pos) = index.column_table_pos_to_index_pos(*col_pos) { if idx_pos < index.columns.len() { let earlier_covered = columns_used[..i].iter().filter_map(|c| *c).any(|c| { index .column_table_pos_to_index_pos(c) .is_some_and(|p| p < idx_pos) }); if idx_pos == 0 || earlier_covered { columns_covered += 1; } } } } } if columns_covered >= 2 { return None; } } } // Keep only branches that use distinct named indexes. Rowid (`None`) may // still appear more than once because it is not tied to a named index. let mut unique_branches: Vec = Vec::new(); let mut seen_indexes: Vec> = Vec::new(); for branch in candidate_branches { let index_name = branch.index.as_ref().map(|idx| idx.name.clone()); if index_name.is_some() && seen_indexes.contains(&index_name) { continue; } seen_indexes.push(index_name); unique_branches.push(branch); } if unique_branches.len() < 2 { return None; } Some(AndClauseDecomposition { term_indices: unique_branches.iter().map(|b| b.where_term_idx).collect(), branches: unique_branches, }) } #[allow(clippy::too_many_arguments)] /// Analyze OR clauses for OR-by-union optimization. /// /// Returns a `MultiIndexScan` access method when every disjunct can be planned /// as an individual lookup branch and the combined cost beats the current best /// non-multi-index alternative. pub fn consider_multi_index_union( rhs_table: &JoinedTable, where_clause: &[WhereTerm], available_indexes: &HashMap>>, table_references: &TableReferences, subqueries: &[NonFromClauseSubquery], schema: &Schema, input_cardinality: f64, base_row_count: RowCountEstimate, params: &CostModelParams, best_cost: Cost, lhs_mask: &TableMask, analyze_stats: &AnalyzeStats, ) -> Option { for (where_term_idx, term) in where_clause.iter().enumerate() { if term.consumed { continue; } let ast::Expr::Binary(_, ast::Operator::Or, _) = &term.expr else { continue; }; let disjuncts = flatten_or_expr(&term.expr); if disjuncts.len() < 2 { continue; } let mut allowed_mask = *lhs_mask; let Some(rhs_idx) = table_references .joined_tables() .iter() .position(|t| t.internal_id == rhs_table.internal_id) else { continue; }; allowed_mask.add_table(rhs_idx); // Each disjunct is replanned with branch-local `TableConstraints`, so // compound conjuncts can reuse the same compound-seek analysis as // ordinary btree access. let branches: Option> = disjuncts .into_iter() .map(|disjunct_expr| { let Ok(disjunct_expr) = crate::translate::expr::unwrap_parens(disjunct_expr) else { return None; }; let conjuncts = flatten_and_expr(disjunct_expr) .into_iter() .cloned() .collect::>(); let (synthetic_where_terms, table_constraints) = get_table_local_constraints_for_branch( &conjuncts, term.from_outer_join, rhs_table, table_references, available_indexes, subqueries, schema, params, ) .ok()?; let mut chosen = choose_multi_index_branch_access( rhs_table, &table_constraints, &synthetic_where_terms, lhs_mask, rhs_idx, schema, base_row_count, analyze_stats, params, ) .ok()??; // Partition residuals in a single pass: pre-filters reference // only outer (lhs) tables and can short-circuit the branch // before the index seek; post-filters reference the target // table and are evaluated after the seek. let partitioned_pre_post = partition_residual_multi_or_exprs( &synthetic_where_terms, &chosen.access, lhs_mask, table_references, subqueries, )?; if !allowed_mask.contains_all(&partitioned_pre_post.post_mask) { return None; } chosen.union_prepost_filters = Some(UnionBranchPrePostFilters { requires_table_cursor: partitioned_pre_post.post_mask.contains_table(rhs_idx), pre_filter_exprs: partitioned_pre_post.pre_filter_exprs, post_filter_exprs: partitioned_pre_post.post_filter_exprs, }); Some(chosen) }) .collect(); let Some(branches) = branches else { continue; }; if let Some(access_method) = evaluate_multi_index_branches( branches, SetOperation::Union, where_term_idx, rhs_table, table_references, available_indexes, subqueries, schema, base_row_count, input_cardinality, params, best_cost, ) { return Some(access_method); } } None } /// Analyze top-level AND terms for AND-by-intersection optimization. /// /// This is more restrictive than OR-by-union because every branch must be a /// local term on the current table, and the final plan only survives if it /// beats the best ordinary access path. #[expect(clippy::too_many_arguments)] pub fn consider_multi_index_intersection( rhs_table: &JoinedTable, where_clause: &[WhereTerm], available_indexes: &HashMap>>, table_references: &TableReferences, subqueries: &[NonFromClauseSubquery], schema: &Schema, input_cardinality: f64, base_row_count: RowCountEstimate, params: &CostModelParams, best_cost: Cost, lhs_mask: &TableMask, analyze_stats: &AnalyzeStats, ) -> Option { let decomposition = analyze_and_terms_for_multi_index( rhs_table, where_clause, available_indexes, table_references, subqueries, schema, params, )?; if decomposition.branches.len() < 2 { return None; } let all_usable = decomposition .branches .iter() .all(|b| lhs_mask.contains_all(&b.constraint.lhs_mask)); if !all_usable { return None; } let branches: Vec<_> = decomposition .branches .iter() .map(|b| { let constraints = vec![b.constraint.clone()]; let index_info = index_info_for_branch( b.index.as_deref(), rhs_table, BranchReadMode::RowIdOnly, params.rows_per_table_page, ) .expect("intersection branches always have costable access"); let analyze_ctx = AnalyzeCtx { rhs_table, index: b.index.as_ref(), stats: analyze_stats, }; MultiIdxBranch { index: b.index.clone(), access: MultiIdxBranchAccess::Seek { constraints: constraints.clone(), constraint_refs: b.constraint_refs.clone(), }, cost: estimate_cost_for_scan_or_seek( Some(index_info), &constraints, &b.constraint_refs, 1.0, base_row_count, false, params, Some(&analyze_ctx), ), estimated_rows: estimate_rows_per_seek( index_info, &constraints, &b.constraint_refs, base_row_count, Some(&analyze_ctx), ), union_prepost_filters: None, } }) .collect(); let where_term_idx = decomposition.term_indices[0]; let additional_consumed_terms: Vec = decomposition.term_indices.iter().skip(1).copied().collect(); evaluate_multi_index_branches( branches, SetOperation::Intersection { additional_consumed_terms, }, where_term_idx, rhs_table, table_references, available_indexes, subqueries, schema, base_row_count, input_cardinality, params, best_cost, ) } #[cfg(test)] mod tests { use super::{ consider_multi_index_intersection, consider_multi_index_union, AnalyzeStats, MultiIndexBranchParams, }; use crate::{ schema::{BTreeTable, ColDef, Column, Index, IndexColumn, Schema, Table, Type}, translate::{ optimizer::{ access_method::AccessMethodParams, cost::{Cost, RowCountEstimate}, cost_params::DEFAULT_PARAMS, }, plan::{ ColumnUsedMask, JoinInfo, JoinType, JoinedTable, Operation, TableReferences, WhereTerm, }, planner::TableMask, }, vdbe::builder::TableRefIdCounter, }; use rustc_hash::FxHashMap as HashMap; use std::{collections::VecDeque, sync::Arc}; use turso_parser::ast::{self, Expr, Operator, SortOrder, TableInternalId}; struct TestColumn { name: String, ty: Type, is_rowid_alias: bool, } fn empty_schema() -> Schema { Schema::default() } fn create_column(c: &TestColumn) -> Column { Column::new( Some(c.name.clone()), c.ty.to_string(), None, None, c.ty, None, ColDef { primary_key: false, rowid_alias: c.is_rowid_alias, ..Default::default() }, ) } fn create_column_of_type(name: &str, ty: Type) -> Column { create_column(&TestColumn { name: name.to_string(), ty, is_rowid_alias: false, }) } fn create_btree_table(name: &str, columns: Vec) -> Arc { Arc::new(BTreeTable { root_page: 1, name: name.to_string(), has_autoincrement: false, primary_key_columns: vec![], columns, has_rowid: true, is_strict: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }) } fn create_table_reference( table: Arc, join_info: Option, internal_id: TableInternalId, ) -> JoinedTable { let name = table.name.clone(); let table = Table::BTree(table); JoinedTable { op: Operation::default_scan_for(&table), table, identifier: name, internal_id, join_info, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id: 0, indexed: None, } } fn create_column_expr(table: TableInternalId, column: usize, is_rowid_alias: bool) -> Expr { Expr::Column { database: None, table, column, is_rowid_alias, } } fn create_numeric_literal(value: &str) -> Expr { Expr::Literal(ast::Literal::Numeric(value.to_string())) } fn create_string_literal(value: &str) -> Expr { Expr::Literal(ast::Literal::String(value.to_string())) } fn assert_is_multi_index( access_method: &crate::translate::optimizer::access_method::AccessMethod, ) -> &Vec { let AccessMethodParams::MultiIndexScan { branches, .. } = &access_method.params else { panic!("expected multi-index scan access method"); }; branches } #[test] fn test_multi_index_union_rejects_residuals_on_future_tables() { let link = create_btree_table( "link", vec![ create_column_of_type("src", Type::Integer), create_column_of_type("dst", Type::Integer), ], ); let item = create_btree_table( "item", vec![ create_column_of_type("id", Type::Integer), create_column_of_type("kind", Type::Text), ], ); let meta = create_btree_table( "meta", vec![ create_column_of_type("id", Type::Integer), create_column_of_type("kind", Type::Text), ], ); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = vec![ create_table_reference(link, None, table_id_counter.next()), create_table_reference( item, Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), ), create_table_reference( meta, Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), ), ]; const LINK: usize = 0; const ITEM: usize = 1; const META: usize = 2; let mut available_indexes = HashMap::default(); available_indexes.insert( "item".to_string(), VecDeque::from([Arc::new(Index { name: "idx_item_id".to_string(), table_name: "item".to_string(), where_clause: None, columns: vec![IndexColumn { name: "id".to_string(), order: SortOrder::Asc, pos_in_table: 0, collation: None, default: None, expr: None, }], unique: false, ephemeral: false, root_page: 2, has_rowid: true, index_method: None, on_conflict: None, })]), ); let lhs_link_src = Expr::Binary( Box::new(create_column_expr( joined_tables[LINK].internal_id, 0, false, )), Operator::Equals, Box::new(create_numeric_literal("1")), ); let lhs_link_dst_item_id = Expr::Binary( Box::new(create_column_expr( joined_tables[LINK].internal_id, 1, false, )), Operator::Equals, Box::new(create_column_expr( joined_tables[ITEM].internal_id, 0, false, )), ); let rhs_link_dst = Expr::Binary( Box::new(create_column_expr( joined_tables[LINK].internal_id, 1, false, )), Operator::Equals, Box::new(create_numeric_literal("1")), ); let rhs_link_src_item_id = Expr::Binary( Box::new(create_column_expr( joined_tables[LINK].internal_id, 0, false, )), Operator::Equals, Box::new(create_column_expr( joined_tables[ITEM].internal_id, 0, false, )), ); let future_meta_kind = Expr::Binary( Box::new(create_column_expr( joined_tables[META].internal_id, 1, false, )), Operator::Equals, Box::new(create_string_literal("entity")), ); let left_disjunct = Expr::Binary( Box::new(Expr::Binary( Box::new(lhs_link_src), Operator::And, Box::new(lhs_link_dst_item_id), )), Operator::And, Box::new(future_meta_kind.clone()), ); let right_disjunct = Expr::Binary( Box::new(Expr::Binary( Box::new(rhs_link_dst), Operator::And, Box::new(rhs_link_src_item_id), )), Operator::And, Box::new(future_meta_kind), ); let where_clause = vec![WhereTerm { expr: Expr::Binary( Box::new(left_disjunct), Operator::Or, Box::new(right_disjunct), ), from_outer_join: None, consumed: false, }]; let table_references = TableReferences::new(joined_tables, vec![]); let base_row_count = RowCountEstimate::hardcoded_fallback(&DEFAULT_PARAMS); let lhs_mask = TableMask::from_table_number_iter([LINK].into_iter()); let access_method = consider_multi_index_union( &table_references.joined_tables()[ITEM], &where_clause, &available_indexes, &table_references, &[], &empty_schema(), 1.0, base_row_count, &DEFAULT_PARAMS, Cost(f64::INFINITY), &lhs_mask, &AnalyzeStats::default(), ); assert!( access_method.is_none(), "future-table residuals must not produce a multi-index OR access method" ); } #[test] fn test_multi_index_intersection_supports_rowid_and_secondary_index_branches() { let item = create_btree_table( "item", vec![ create_column(&TestColumn { name: "id".to_string(), ty: Type::Integer, is_rowid_alias: true, }), create_column_of_type("a", Type::Integer), ], ); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = vec![create_table_reference(item, None, table_id_counter.next())]; let item_id = joined_tables[0].internal_id; let mut available_indexes = HashMap::default(); available_indexes.insert( "item".to_string(), VecDeque::from([Arc::new(Index { name: "idx_item_a".to_string(), table_name: "item".to_string(), where_clause: None, columns: vec![IndexColumn { name: "a".to_string(), order: SortOrder::Asc, pos_in_table: 1, collation: None, default: None, expr: None, }], unique: false, ephemeral: false, root_page: 2, has_rowid: true, index_method: None, on_conflict: None, })]), ); let where_clause = vec![ WhereTerm { expr: Expr::Binary( Box::new(create_column_expr(item_id, 0, true)), Operator::Greater, Box::new(create_numeric_literal("10")), ), from_outer_join: None, consumed: false, }, WhereTerm { expr: Expr::Binary( Box::new(create_column_expr(item_id, 1, false)), Operator::Equals, Box::new(create_numeric_literal("7")), ), from_outer_join: None, consumed: false, }, ]; let table_references = TableReferences::new(joined_tables, vec![]); let base_row_count = RowCountEstimate::hardcoded_fallback(&DEFAULT_PARAMS); let access_method = consider_multi_index_intersection( &table_references.joined_tables()[0], &where_clause, &available_indexes, &table_references, &[], &empty_schema(), 1.0, base_row_count, &DEFAULT_PARAMS, Cost(f64::INFINITY), &TableMask::new(), &AnalyzeStats::default(), ) .expect("rowid and secondary-index terms should be eligible for intersection"); let branches = assert_is_multi_index(&access_method); assert_eq!(branches.len(), 2); assert!( branches.iter().any(|branch| branch.index.is_none()), "expected one rowid branch" ); assert!( branches .iter() .any(|branch| branch.index.as_ref().map(|idx| idx.name.as_str()) == Some("idx_item_a")), "expected one secondary-index branch" ); } #[test] fn test_multi_index_union_branch_reuses_compound_seek_analysis() { let link = create_btree_table( "link", vec![ create_column_of_type("src", Type::Integer), create_column_of_type("dst", Type::Integer), ], ); let item = create_btree_table( "item", vec![ create_column_of_type("id", Type::Integer), create_column_of_type("kind", Type::Integer), ], ); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = vec![ create_table_reference(link, None, table_id_counter.next()), create_table_reference( item, Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), ), ]; const LINK: usize = 0; const ITEM: usize = 1; let mut available_indexes = HashMap::default(); available_indexes.insert( "item".to_string(), VecDeque::from([Arc::new(Index { name: "idx_item_id_kind".to_string(), table_name: "item".to_string(), where_clause: None, columns: vec![ IndexColumn { name: "id".to_string(), order: SortOrder::Asc, pos_in_table: 0, collation: None, default: None, expr: None, }, IndexColumn { name: "kind".to_string(), order: SortOrder::Asc, pos_in_table: 1, collation: None, default: None, expr: None, }, ], unique: false, ephemeral: false, root_page: 2, has_rowid: true, index_method: None, on_conflict: None, })]), ); let left_disjunct = Expr::Binary( Box::new(Expr::Binary( Box::new(Expr::Binary( Box::new(create_column_expr( joined_tables[LINK].internal_id, 0, false, )), Operator::Equals, Box::new(create_numeric_literal("1")), )), Operator::And, Box::new(Expr::Binary( Box::new(create_column_expr( joined_tables[ITEM].internal_id, 0, false, )), Operator::Equals, Box::new(create_column_expr( joined_tables[LINK].internal_id, 1, false, )), )), )), Operator::And, Box::new(Expr::Binary( Box::new(create_column_expr( joined_tables[ITEM].internal_id, 1, false, )), Operator::Equals, Box::new(create_numeric_literal("7")), )), ); let right_disjunct = Expr::Binary( Box::new(Expr::Binary( Box::new(Expr::Binary( Box::new(create_column_expr( joined_tables[LINK].internal_id, 1, false, )), Operator::Equals, Box::new(create_numeric_literal("1")), )), Operator::And, Box::new(Expr::Binary( Box::new(create_column_expr( joined_tables[ITEM].internal_id, 0, false, )), Operator::Equals, Box::new(create_column_expr( joined_tables[LINK].internal_id, 0, false, )), )), )), Operator::And, Box::new(Expr::Binary( Box::new(create_column_expr( joined_tables[ITEM].internal_id, 1, false, )), Operator::Equals, Box::new(create_numeric_literal("7")), )), ); let where_clause = vec![WhereTerm { expr: Expr::Binary( Box::new(left_disjunct), Operator::Or, Box::new(right_disjunct), ), from_outer_join: None, consumed: false, }]; let table_references = TableReferences::new(joined_tables, vec![]); let lhs_mask = TableMask::from_table_number_iter([LINK].into_iter()); let base_row_count = RowCountEstimate::hardcoded_fallback(&DEFAULT_PARAMS); let access_method = consider_multi_index_union( &table_references.joined_tables()[ITEM], &where_clause, &available_indexes, &table_references, &[], &empty_schema(), 1.0, base_row_count, &DEFAULT_PARAMS, Cost(f64::INFINITY), &lhs_mask, &AnalyzeStats::default(), ) .expect("compound OR branches should produce a multi-index union"); let branches = assert_is_multi_index(&access_method); assert_eq!(branches.len(), 2); for branch in branches { assert_eq!( branch.index.as_ref().map(|idx| idx.name.as_str()), Some("idx_item_id_kind") ); let super::MultiIndexBranchAccessParams::Seek { constraint_refs, .. } = &branch.access else { panic!("compound OR test should choose ordinary seek branches"); }; assert_eq!( constraint_refs.len(), 2, "branch should use both id and kind in the compound seek" ); } } #[test] fn test_multi_index_union_residual_selectivity_reduces_row_estimate() { let link = create_btree_table( "link", vec![ create_column_of_type("src", Type::Integer), create_column_of_type("dst", Type::Integer), ], ); let item = create_btree_table( "item", vec![ create_column_of_type("id", Type::Integer), create_column_of_type("kind", Type::Integer), ], ); let mut table_id_counter = TableRefIdCounter::new(); let joined_tables = vec![ create_table_reference(link, None, table_id_counter.next()), create_table_reference( item, Some(JoinInfo { join_type: JoinType::Inner, using: vec![], no_reorder: false, }), table_id_counter.next(), ), ]; const LINK: usize = 0; const ITEM: usize = 1; let link_id = joined_tables[LINK].internal_id; let item_id = joined_tables[ITEM].internal_id; let mut available_indexes = HashMap::default(); available_indexes.insert( "item".to_string(), VecDeque::from([Arc::new(Index { name: "idx_item_id".to_string(), table_name: "item".to_string(), where_clause: None, columns: vec![IndexColumn { name: "id".to_string(), order: SortOrder::Asc, pos_in_table: 0, collation: None, default: None, expr: None, }], unique: false, ephemeral: false, root_page: 2, has_rowid: true, index_method: None, on_conflict: None, })]), ); let make_branch = |literal_col, join_col, item_kind: Option<&str>| { let branch = Expr::Binary( Box::new(Expr::Binary( Box::new(create_column_expr(link_id, literal_col, false)), Operator::Equals, Box::new(create_numeric_literal("1")), )), Operator::And, Box::new(Expr::Binary( Box::new(create_column_expr(item_id, 0, false)), Operator::Equals, Box::new(create_column_expr(link_id, join_col, false)), )), ); if let Some(kind) = item_kind { Expr::Binary( Box::new(branch), Operator::And, Box::new(Expr::Binary( Box::new(create_column_expr(item_id, 1, false)), Operator::Equals, Box::new(create_numeric_literal(kind)), )), ) } else { branch } }; let make_join_expr = |item_kind: Option<&str>| { vec![WhereTerm { expr: Expr::Binary( Box::new(make_branch(0, 1, item_kind)), Operator::Or, Box::new(make_branch(1, 0, item_kind)), ), from_outer_join: None, consumed: false, }] }; let table_references = TableReferences::new(joined_tables, vec![]); let lhs_mask = TableMask::from_table_number_iter([LINK].into_iter()); let base_row_count = RowCountEstimate::hardcoded_fallback(&DEFAULT_PARAMS); let without_residual = consider_multi_index_union( &table_references.joined_tables()[ITEM], &make_join_expr(None), &available_indexes, &table_references, &[], &empty_schema(), 1.0, base_row_count, &DEFAULT_PARAMS, Cost(f64::INFINITY), &lhs_mask, &AnalyzeStats::default(), ) .expect("plain OR branches should produce a multi-index union"); let with_residual = consider_multi_index_union( &table_references.joined_tables()[ITEM], &make_join_expr(Some("7")), &available_indexes, &table_references, &[], &empty_schema(), 1.0, base_row_count, &DEFAULT_PARAMS, Cost(f64::INFINITY), &lhs_mask, &AnalyzeStats::default(), ) .expect("residual-filtered OR branches should still produce a multi-index union"); assert!( with_residual.estimated_rows_per_outer_row < without_residual.estimated_rows_per_outer_row, "branch-local residual filters must reduce the multi-index row estimate" ); } } ================================================ FILE: core/translate/optimizer/order.rs ================================================ use crate::schema::Table; use crate::turso_assert_greater_than_or_equal; use crate::{ schema::{FromClauseSubquery, Index, Schema}, translate::{ collate::{get_collseq_from_expr, CollationSeq}, expression_index::normalize_expr_for_index_matching, optimizer::access_method::AccessMethodParams, optimizer::constraints::RangeConstraintRef, plan::{ GroupBy, HashJoinType, IterationDirection, JoinedTable, Operation, Plan, Scan, SimpleAggregate, TableReferences, }, planner::table_mask_from_expr, }, util::exprs_are_equivalent, }; use turso_parser::ast::{self, SortOrder, TableInternalId}; use super::{ access_method::AccessMethod, cost::{is_unique_point_lookup, IndexInfo}, join::JoinN, }; /// Target component in an ORDER BY/GROUP BY that may be a plain column or an expression. #[derive(Debug, PartialEq, Clone)] pub enum ColumnTarget { Column(usize), RowId, /// We know that the ast lives at least as long as the Statement/Program, /// so we store a raw pointer here to avoid cloning yet another ast::Expr Expr(*const ast::Expr), } /// A convenience struct for representing a (table_no, column_target, [SortOrder]) tuple. #[derive(Debug, PartialEq, Clone)] pub struct ColumnOrder { pub table_id: TableInternalId, pub target: ColumnTarget, pub order: SortOrder, pub collation: CollationSeq, } #[derive(Debug, PartialEq, Clone)] /// If an [OrderTarget] is satisfied, then [EliminatesSort] describes which part /// of the query no longer requires sorting. pub enum EliminatesSortBy { Group, Order, GroupByAndOrder, } #[derive(Debug, PartialEq, Clone)] pub enum OrderTargetPurpose { /// Matching this target lets the planner eliminate a later ORDER BY and/or /// GROUP BY sort step. EliminatesSort(EliminatesSortBy), /// Matching this target enables an extremum fast path, analogous to /// SQLite's WHERE_ORDERBY_MIN/MAX planning mode. Extremum, } #[derive(Debug, PartialEq, Clone)] /// An [OrderTarget] is considered in join optimization and index selection, /// so that if a given join ordering and its access methods satisfy the [OrderTarget], /// then the join ordering and its access methods are preferred, all other things being equal. pub struct OrderTarget { pub columns: Vec, pub purpose: OrderTargetPurpose, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EqualityPrefixScope { /// Candidate scoring may skip any equality-constrained seek prefix because /// it only reasons about the order within that specific seek. AnyEquality, /// Final ORDER BY / GROUP BY elimination may only skip globally constant /// equality prefixes. Join-dependent equalities vary per outer row and do /// not guarantee a globally ordered concatenation of inner scans. ConstantEquality, } impl OrderTarget { /// Build an `OrderTarget` from a list of expressions if they can all be /// satisfied by a single-table ordering (needed for index satisfaction). fn maybe_from_iterator<'a>( list: impl Iterator + Clone, tables: &crate::translate::plan::TableReferences, purpose: OrderTargetPurpose, ) -> Option { if list.clone().count() == 0 { return None; } let mut cols = Vec::new(); for (expr, order) in list { let col = expr_to_column_order(expr, order, tables)?; cols.push(col); } Some(OrderTarget { columns: cols, purpose, }) } pub fn is_extremum(&self) -> bool { matches!(self.purpose, OrderTargetPurpose::Extremum) } } /// Build the synthetic ordering requirement used by simple MIN/MAX aggregation. pub fn simple_aggregate_order_target( simple_aggregate: &SimpleAggregate, tables: &TableReferences, ) -> Option { let SimpleAggregate::MinMax(min_max) = simple_aggregate else { return None; }; let mut target = OrderTarget::maybe_from_iterator( std::iter::once((&min_max.argument, min_max.order)), tables, OrderTargetPurpose::Extremum, )?; if let Some(coll) = min_max.collation { target.columns[0].collation = coll; } Some(target) } /// Compute an [OrderTarget] for the join optimizer to use. /// Ideally, a join order is both efficient in joining the tables /// but also returns the results in an order that minimizes the amount of /// sorting that needs to be done later (either in GROUP BY, ORDER BY, or both). /// /// TODO: this does not currently handle the case where we definitely cannot eliminate /// the ORDER BY sorter, but we could still eliminate the GROUP BY sorter. pub fn compute_order_target( order_by: &mut Vec<(Box, SortOrder)>, group_by_opt: Option<&mut GroupBy>, tables: &TableReferences, ) -> Option { match (order_by.is_empty(), group_by_opt) { // No ordering demands - we don't care what order the joined result rows are in (true, None) => None, // Only ORDER BY - we would like the joined result rows to be in the order specified by the ORDER BY (false, None) => OrderTarget::maybe_from_iterator( order_by.iter().map(|(expr, order)| (expr.as_ref(), *order)), tables, OrderTargetPurpose::EliminatesSort(EliminatesSortBy::Order), ), // Only GROUP BY - we would like the joined result rows to be in the order specified by the GROUP BY (true, Some(group_by)) => OrderTarget::maybe_from_iterator( group_by.exprs.iter().map(|expr| (expr, SortOrder::Asc)), tables, OrderTargetPurpose::EliminatesSort(EliminatesSortBy::Group), ), // Both ORDER BY and GROUP BY: // If the GROUP BY does not contain all the expressions in the ORDER BY, // then we must separately sort the result rows for ORDER BY anyway. // However, in that case we can use the GROUP BY expressions as the target order for the join, // so that we don't have to sort twice. // // If the GROUP BY contains all the expressions in the ORDER BY, // then we again can use the GROUP BY expressions as the target order for the join; // however in this case we must take the ASC/DESC from ORDER BY into account. (false, Some(group_by)) => { // Does the group by contain all expressions in the order by? let group_by_contains_all = order_by.iter().all(|(expr, _)| { group_by .exprs .iter() .any(|group_by_expr| exprs_are_equivalent(expr, group_by_expr)) }); // If not, let's try to target an ordering that matches the group by -- we don't care about ASC/DESC if !group_by_contains_all { return OrderTarget::maybe_from_iterator( group_by.exprs.iter().map(|expr| (expr, SortOrder::Asc)), tables, OrderTargetPurpose::EliminatesSort(EliminatesSortBy::Group), ); } // If yes, let's try to target an ordering that matches the GROUP BY columns, // but the ORDER BY orderings. First, we need to reorder the GROUP BY columns to match the ORDER BY columns. group_by.exprs.sort_by_key(|expr| { order_by .iter() .position(|(order_by_expr, _)| exprs_are_equivalent(expr, order_by_expr)) .map_or(usize::MAX, |i| i) }); // Now, regardless of whether we can eventually eliminate the sorting entirely in the optimizer, // we know that we don't need ORDER BY sorting anyway, because the GROUP BY will sort the result since // it contains all the necessary columns required for the ORDER BY, and the GROUP BY columns are now in the correct order. // First, however, we need to make sure the GROUP BY sorter's column sort directions match the ORDER BY requirements. turso_assert_greater_than_or_equal!(group_by.exprs.len(), order_by.len()); let sort_order = &mut group_by.sort_order; for (i, (_, order_by_dir)) in order_by.iter().enumerate() { sort_order[i] = *order_by_dir; } // The sort_by_key above reordered group_by.exprs but not sort_order, // so remaining positions may have stale values. GROUP BY columns not // in ORDER BY should default to ASC (matching SQLite's tie-breaking). for s in &mut sort_order[order_by.len()..] { *s = SortOrder::Asc; } // Now we can remove the ORDER BY from the query. order_by.clear(); OrderTarget::maybe_from_iterator( group_by .exprs .iter() .zip(group_by.sort_order.iter()) .map(|(expr, dir)| (expr, *dir)), tables, OrderTargetPurpose::EliminatesSort(EliminatesSortBy::GroupByAndOrder), ) } } } /// Check if the plan's row iteration order matches the [OrderTarget]'s column order. /// If yes, and this plan is selected, then a sort operation can be eliminated. pub fn plan_satisfies_order_target( plan: &JoinN, access_methods_arena: &[AccessMethod], joined_tables: &[JoinedTable], order_target: &OrderTarget, schema: &Schema, ) -> bool { // Outer hash joins emit unmatched rows in hash-bucket order, not scan order. for (_, access_method_index) in plan.data.iter() { let access_method = &access_methods_arena[*access_method_index]; if let AccessMethodParams::HashJoin { join_type, .. } = &access_method.params { if matches!(join_type, HashJoinType::LeftOuter | HashJoinType::FullOuter) { return false; } } } let mut target_col_idx = 0; let num_cols_in_order_target = order_target.columns.len(); for (loop_pos, (table_index, access_method_index)) in plan.data.iter().enumerate() { let access_method = &access_methods_arena[*access_method_index]; let table_ref = &joined_tables[*table_index]; // Outer joins can emit an extra row with NULLs on the right-hand side // when no match is found. Because that row is produced after the scan or // seek, we cannot rely on the right-hand table's access order to satisfy // ORDER BY / GROUP BY terms that reference that table. if table_ref .join_info .as_ref() .is_some_and(|join_info| join_info.is_outer()) && order_target.columns[target_col_idx..] .iter() .any(|target_col| target_col.table_id == table_ref.internal_id) { return false; } // Check if this table has an access method that provides the right ordering. let consumed = match &access_method.params { AccessMethodParams::BTreeTable { iter_dir, index: index_opt, constraint_refs, } => btree_access_order_consumed( table_ref, *iter_dir, index_opt.as_deref(), constraint_refs, &order_target.columns[target_col_idx..], schema, EqualityPrefixScope::ConstantEquality, ), AccessMethodParams::MaterializedSubquery { index, constraint_refs, iter_dir, } => btree_access_order_consumed( table_ref, *iter_dir, Some(index.as_ref()), constraint_refs, &order_target.columns[target_col_idx..], schema, EqualityPrefixScope::ConstantEquality, ), AccessMethodParams::Subquery { iter_dir } => { let Table::FromClauseSubquery(from_clause_subquery) = &table_ref.table else { unreachable!( "access_method.params::Subquery must be for a FromClauseSubquery table" ); }; subquery_intrinsic_order_consumed( table_ref.internal_id, from_clause_subquery, *iter_dir, &order_target.columns[target_col_idx..], schema, ) } _ => return false, }; if consumed == 0 { return false; } target_col_idx += consumed; if target_col_idx == num_cols_in_order_target { return true; } // The next ORDER BY column can only come from a deeper loop if the rows // output by this loop are unique for the columns so far. If they're not unique, // the inner loop would repeat the same values for each duplicate, resulting in // an output like `A B C ... A B C ...` instead of the correct fully sorted order `A A B B ...`. let next_term_comes_from_later_loop = order_target .columns .get(target_col_idx) .is_some_and(|target_col| { plan.data[loop_pos + 1..] .iter() .any(|(later_table_index, _)| { joined_tables[*later_table_index].internal_id == target_col.table_id }) }); if next_term_comes_from_later_loop && !access_method_emits_unique_order_prefix(access_method, consumed) { return false; } } target_col_idx == num_cols_in_order_target } fn access_method_emits_unique_order_prefix( access_method: &AccessMethod, consumed_order_terms: usize, ) -> bool { match &access_method.params { AccessMethodParams::BTreeTable { index, constraint_refs, .. } => access_path_makes_consumed_prefix_unique( index.as_deref(), constraint_refs, consumed_order_terms, ), AccessMethodParams::MaterializedSubquery { index, constraint_refs, .. } => access_path_makes_consumed_prefix_unique( Some(index.as_ref()), constraint_refs, consumed_order_terms, ), AccessMethodParams::Subquery { .. } | AccessMethodParams::HashJoin { .. } | AccessMethodParams::VirtualTable { .. } | AccessMethodParams::IndexMethod { .. } | AccessMethodParams::MultiIndexScan { .. } | AccessMethodParams::InSeek { .. } => false, } } fn access_path_makes_consumed_prefix_unique( index: Option<&Index>, constraint_refs: &[RangeConstraintRef], consumed_order_terms: usize, ) -> bool { if is_unique_point_lookup(index_info_for_access(index), constraint_refs) { return true; } match index { // Table scans only provide rowid order. If that rowid term was consumed, // the prefix is unique even though the scan obviously returns many rows. None => consumed_order_terms >= 1, Some(index) => { let eq_prefix_len = constraint_refs .iter() .take_while(|constraint| constraint.eq.is_some()) .count(); let unique_prefix_terms = eq_prefix_len + consumed_order_terms; // Unique indexes become prefix-unique once all key columns are either // fixed by equality or consumed as ORDER BY terms. if index.unique && unique_prefix_terms >= index.columns.len() { return true; } // Rowid tables keep duplicate secondary-index keys ordered by rowid. // If the consumed ORDER BY terms already include that implicit rowid // suffix, the emitted prefix is unique too. index.has_rowid && unique_prefix_terms > index.columns.len() } } } fn index_info_for_access(index: Option<&Index>) -> IndexInfo { match index { Some(index) => IndexInfo { unique: index.unique, column_count: index.columns.len(), covering: false, rows_per_leaf_page: 0.0, // unused here — only unique/column_count matter }, None => IndexInfo { unique: true, column_count: 1, covering: false, rows_per_leaf_page: 0.0, // unused here — only unique/column_count matter }, } } /// Return how many leading target columns a FROM-subquery can provide from its /// own output order, without fabricating an extra probe index. /// /// We recognize three sources of intrinsic order: /// 1. An explicit final `ORDER BY` on the subquery. /// 2. GROUP BY keys (we always use a sorter, never hashing - FOR NOW). /// 3. A simple single-source finalized scan whose output order is already known. pub fn subquery_intrinsic_order_consumed( table_id: TableInternalId, subquery: &FromClauseSubquery, iter_dir: IterationDirection, target: &[ColumnOrder], schema: &Schema, ) -> usize { let Plan::Select(select_plan) = subquery.plan.as_ref() else { // Don't consider sort elision for compound selects return 0; }; // Explicit ORDER BY takes priority. if !select_plan.order_by.is_empty() { let intrinsic = build_intrinsic_order( table_id, select_plan, select_plan .order_by .iter() .map(|(expr, order)| (expr.as_ref(), *order)), ); return match_intrinsic_order(&intrinsic, iter_dir, target); } // When ORDER BY was merged into GROUP BY and cleared, the GROUP BY // sort_order still describes the output row order. if let Some(group_by) = &select_plan.group_by { let intrinsic = build_intrinsic_order( table_id, select_plan, group_by .exprs .iter() .zip(group_by.sort_order.iter().copied()), ); let consumed = match_intrinsic_order(&intrinsic, iter_dir, target); if consumed > 0 { return consumed; } } finalized_scan_subquery_order_consumed(table_id, select_plan, iter_dir, target, schema) } /// Build a `ColumnOrder` list from expressions and sort directions by mapping /// each expression to a result column position. fn build_intrinsic_order( table_id: TableInternalId, select_plan: &crate::translate::plan::SelectPlan, exprs: impl Iterator, SortOrder)>, ) -> Vec { let mut intrinsic = Vec::new(); for (expr, order) in exprs { let expr = expr.borrow(); let Some((col_idx, result_col)) = select_plan .result_columns .iter() .enumerate() .find(|(_, result_col)| exprs_are_equivalent(expr, &result_col.expr)) else { break; }; let Ok(collation) = get_collseq_from_expr(expr, &select_plan.table_references) else { break; }; intrinsic.push(ColumnOrder { table_id, target: ColumnTarget::Column(col_idx), order, collation: collation.unwrap_or_else(|| { get_collseq_from_expr(&result_col.expr, &select_plan.table_references) .ok() .flatten() .unwrap_or_default() }), }); } intrinsic } /// Compare a subquery's intrinsic column order against an outer order target, /// accounting for iteration direction. Returns how many leading target columns /// are satisfied. fn match_intrinsic_order( intrinsic: &[ColumnOrder], iter_dir: IterationDirection, target: &[ColumnOrder], ) -> usize { let target_len = target.len().min(intrinsic.len()); for (intrinsic_col, target_col) in intrinsic.iter().zip(target.iter()).take(target_len) { if intrinsic_col.table_id != target_col.table_id || intrinsic_col.target != target_col.target || intrinsic_col.collation != target_col.collation { return 0; } let expected_order = match iter_dir { IterationDirection::Forwards => intrinsic_col.order, IterationDirection::Backwards => match intrinsic_col.order { SortOrder::Asc => SortOrder::Desc, SortOrder::Desc => SortOrder::Asc, }, }; if expected_order != target_col.order { return 0; } } target_len } /// Derive subquery output order from the finalized inner scan when there is no /// explicit `ORDER BY`. /// /// This intentionally starts narrow: single-source, non-aggregate, /// non-window, non-distinct SELECTs only. Those are the cases where insertion /// order into the materialized table is just the underlying scan order. fn finalized_scan_subquery_order_consumed( table_id: TableInternalId, select_plan: &crate::translate::plan::SelectPlan, iter_dir: IterationDirection, target: &[ColumnOrder], schema: &Schema, ) -> usize { if select_plan.group_by.is_some() || !select_plan.aggregates.is_empty() || select_plan.limit.is_some() || select_plan.offset.is_some() || select_plan.window.is_some() || select_plan.distinctness.is_distinct() || !select_plan.values.is_empty() || select_plan.join_order.len() != 1 || select_plan.joined_tables().len() != 1 { return 0; } let joined_table = &select_plan.joined_tables()[select_plan.join_order[0].original_idx]; // Extract inner iteration direction from the scan operation. let inner_iter_dir = match &joined_table.op { Operation::Scan(Scan::BTreeTable { iter_dir, .. }) | Operation::Scan(Scan::Subquery { iter_dir }) => *iter_dir, _ => return 0, }; // The outer scan direction composes with the direction used to populate the // materialized table. Reversing a backwards-populated table restores the // original key order. let effective_iter_dir = match (inner_iter_dir, iter_dir) { (IterationDirection::Forwards, IterationDirection::Forwards) | (IterationDirection::Backwards, IterationDirection::Backwards) => { IterationDirection::Forwards } (IterationDirection::Forwards, IterationDirection::Backwards) | (IterationDirection::Backwards, IterationDirection::Forwards) => { IterationDirection::Backwards } }; // Map outer target columns to inner scan columns through result column expressions. let mut mapped_target = Vec::with_capacity(target.len()); for target_col in target { if target_col.table_id != table_id { return 0; } let ColumnTarget::Column(result_col_idx) = target_col.target else { return 0; }; let Some(result_col) = select_plan.result_columns.get(result_col_idx) else { return 0; }; // The outer query sees result columns of the materialized subquery, but // the ordering proof has to be checked against the inner scan columns. let Some(mut inner_target_col) = expr_to_column_order( &result_col.expr, target_col.order, &select_plan.table_references, ) else { return 0; }; if inner_target_col.table_id != joined_table.internal_id || inner_target_col.collation != target_col.collation { return 0; } inner_target_col.order = target_col.order; mapped_target.push(inner_target_col); } match &joined_table.op { Operation::Scan(Scan::BTreeTable { index, .. }) => btree_access_order_consumed( joined_table, effective_iter_dir, index.as_deref(), &[], &mapped_target, schema, EqualityPrefixScope::ConstantEquality, ), Operation::Scan(Scan::Subquery { .. }) => { let Table::FromClauseSubquery(from_clause_subquery) = &joined_table.table else { return 0; }; subquery_intrinsic_order_consumed( joined_table.internal_id, from_clause_subquery, effective_iter_dir, &mapped_target, schema, ) } _ => 0, } } fn expr_to_column_order( expr: &ast::Expr, order: SortOrder, tables: &TableReferences, ) -> Option { match expr { ast::Expr::Column { table: table_id, column, .. } => { let table = tables.find_joined_table_by_internal_id(*table_id)?; let col = table.columns().get(*column)?; return Some(ColumnOrder { table_id: *table_id, target: ColumnTarget::Column(*column), order, collation: col.collation(), }); } ast::Expr::Collate(expr, collation) => { if let ast::Expr::Column { table: table_id, column, .. } = expr.as_ref() { let collation = CollationSeq::new(collation.as_str()).unwrap_or_default(); return Some(ColumnOrder { table_id: *table_id, target: ColumnTarget::Column(*column), order, collation, }); }; } ast::Expr::RowId { table, .. } => { return Some(ColumnOrder { table_id: *table, target: ColumnTarget::RowId, order, collation: CollationSeq::default(), }); } _ => {} } let mask = table_mask_from_expr(expr, tables, &[]).ok()?; if mask.table_count() != 1 { return None; } let collation = get_collseq_from_expr(expr, tables) .ok()? .unwrap_or_default(); let table_no = tables .joined_tables() .iter() .enumerate() .find_map(|(i, _)| mask.contains_table(i).then_some(i))?; let table_id = tables.joined_tables()[table_no].internal_id; Some(ColumnOrder { table_id, target: ColumnTarget::Expr(expr as *const ast::Expr), order, collation, }) } fn target_matches_index_column( target_col: &ColumnOrder, idx_col: &crate::schema::IndexColumn, table_ref: &JoinedTable, ) -> bool { if target_col.table_id != table_ref.internal_id { return false; } match (&target_col.target, &idx_col.expr) { (ColumnTarget::Column(col_no), None) => idx_col.pos_in_table == *col_no, (ColumnTarget::Expr(expr), Some(idx_expr)) => { let target_expr = unsafe { &**expr }; if exprs_are_equivalent(target_expr, idx_expr) { return true; } // Expression indexes are compared against the normalized form that // was stored in the schema. A query may write the same expression in // a slightly different but equivalent way, so normalize before the // final comparison. let refs = TableReferences::new(vec![table_ref.clone()], Vec::new()); let normalized = normalize_expr_for_index_matching(target_expr, table_ref, &refs); exprs_are_equivalent(&normalized, idx_expr) } _ => false, } } /// Return how many leading `order_target` columns this single-table btree /// access path can satisfy. /// /// This is shared by both candidate scoring and final ORDER BY / GROUP BY /// elimination so they use the same column-matching, collation, custom-type, /// and hidden-rowid-suffix rules. The caller supplies /// [`EqualityPrefixScope`] because candidate scoring may skip any equality /// prefix in the chosen seek key, while final global ordering proof may only /// skip prefixes that are constant across all output rows. pub(super) fn btree_access_order_consumed( table_ref: &JoinedTable, iter_dir: IterationDirection, index: Option<&Index>, constraint_refs: &[RangeConstraintRef], order_target: &[ColumnOrder], schema: &Schema, equality_prefix_scope: EqualityPrefixScope, ) -> usize { let Some(first_target_col) = order_target.first() else { return 0; }; let rowid_alias_col = table_ref .table .columns() .iter() .position(|c| c.is_rowid_alias()); match index { None => { // Without an index, only rowid order is available. if first_target_col.table_id != table_ref.internal_id { return 0; } match first_target_col.target { ColumnTarget::RowId => {} ColumnTarget::Column(col_no) => { let Some(rowid_alias_col) = rowid_alias_col else { return 0; }; if col_no != rowid_alias_col { return 0; } } ColumnTarget::Expr(_) => return 0, } let correct_order = if iter_dir == IterationDirection::Forwards { first_target_col.order == SortOrder::Asc } else { first_target_col.order == SortOrder::Desc }; usize::from(correct_order) } Some(index) => { let mut col_idx = 0; let mut idx_pos = 0; while col_idx < order_target.len() && idx_pos < index.columns.len() { let target_col = &order_target[col_idx]; if target_col.table_id != table_ref.internal_id { break; } let idx_col = &index.columns[idx_pos]; let eq_prefix_usable = constraint_refs.iter().any(|constraint| { constraint.index_col_pos == idx_pos && constraint.eq.as_ref().is_some_and(|eq| { equality_prefix_scope == EqualityPrefixScope::AnyEquality || eq.is_const }) }); if eq_prefix_usable { // Equality-constrained prefix columns produce a single value // per seek, so they do not disturb the ordering of the // remaining suffix. If the ORDER BY / GROUP BY also mentions // the same column with the same collation, that target term // is satisfied trivially and can be consumed here too. if target_matches_index_column(target_col, idx_col, table_ref) { let same_collation = target_col.collation == idx_col.collation.unwrap_or_default(); if !same_collation { break; } col_idx += 1; } idx_pos += 1; continue; } if !target_matches_index_column(target_col, idx_col, table_ref) { break; } // Custom type columns store encoded blobs. The B-tree's bytewise // ordering does not match the custom type's semantic ordering, so // the index cannot satisfy ORDER BY for those columns. if let ColumnTarget::Column(col_no) = &target_col.target { if let Some(col) = table_ref.table.columns().get(*col_no) { if schema .get_type_def(&col.ty_str, table_ref.table.is_strict()) .is_some() { break; } } } if target_col.collation != idx_col.collation.unwrap_or_default() { break; } let correct_order = if iter_dir == IterationDirection::Forwards { target_col.order == idx_col.order } else { target_col.order != idx_col.order }; if !correct_order { break; } col_idx += 1; idx_pos += 1; } // SQLite-style rowid tables keep equal secondary-index keys ordered // by rowid. That implicit suffix can satisfy one extra ORDER BY term. if col_idx < order_target.len() && idx_pos == index.columns.len() && index.has_rowid { let target_col = &order_target[col_idx]; let rowid_matches = match target_col.target { ColumnTarget::RowId => true, ColumnTarget::Column(col_no) => { rowid_alias_col.is_some_and(|alias| alias == col_no) } ColumnTarget::Expr(_) => false, }; let correct_order = if iter_dir == IterationDirection::Forwards { target_col.order == SortOrder::Asc } else { target_col.order == SortOrder::Desc }; if target_col.table_id == table_ref.internal_id && rowid_matches && correct_order { col_idx += 1; } } col_idx } } } ================================================ FILE: core/translate/optimizer/unnest.rs ================================================ //! Unnesting pass: rewrites EXISTS/NOT EXISTS correlated subqueries into semi/anti-joins. //! //! A correlated EXISTS subquery: //! SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.a = t1.a) //! is rewritten into a semi-join: //! SELECT * FROM t1 SEMI JOIN t2 ON t2.a = t1.a //! //! Similarly, NOT EXISTS becomes an anti-join. //! //! Base intuition for correctness: //! - `EXISTS(subquery)` is a yes/no test: did we find at least one inner row? //! - A semi-join is the same yes/no test, just run as a join loop: //! keep the outer row once a matching inner row is found. //! - For correlated equality predicates (for example `inner.k = outer.k`), the //! answer depends only on the key value `k`, not on outer row identity. If two //! outer rows have the same `k`, they both either match or do not match. //! That is why precomputing/joining by `k` preserves `EXISTS` truth values. //! - `NOT EXISTS(subquery)` is the opposite yes/no test. //! - An anti-join does exactly that: //! keep the outer row only if no matching inner row is found. //! - The same key-value argument applies to anti-join: for a given `k`, either //! every outer row with `k` survives (no inner match) or none survive. //! //! So the rewrite is semantics-preserving when we keep the same notion of //! "matching row" and do not move predicates across boundaries that change //! row existence (for example OUTER JOIN null-extension or inner-independent //! gates under `NOT EXISTS`, e.g. `NOT EXISTS (... WHERE corr AND 0)` is TRUE for every outer row). //! //! Canonical references used for blocker rationale in this module: //! - [SQLITE-EXISTS] https://sqlite.org/lang_expr.html#the_exists_operator //! - [PG-SUBQUERY] https://www.postgresql.org/docs/current/functions-subquery.html //! - [PG-JOIN-ORDER] https://www.postgresql.org/docs/current/queries-table-expressions.html //! - [MYSQL-SEMIJOIN] https://dev.mysql.com/doc/refman/8.4/en/semijoins-antijoins.html use smallvec::SmallVec; use turso_parser::ast::{self, Expr, TableInternalId, UnaryOperator}; use crate::function::{Deterministic, Func}; use crate::translate::{ expr::{walk_expr, WalkControl}, plan::{JoinInfo, JoinType, SelectPlan, SubqueryState, WhereTerm}, }; use crate::Result; /// Attempt to unnest EXISTS/NOT EXISTS correlated subqueries into semi/anti-joins. /// This is called during the optimizer pipeline, after constant condition elimination /// and before table access optimization. pub fn unnest_exists_subqueries(plan: &mut SelectPlan) -> Result<()> { let mut i = 0; while i < plan.non_from_clause_subqueries.len() { let subquery = &plan.non_from_clause_subqueries[i]; // Only consider unevaluated, correlated EXISTS subqueries. if !subquery.correlated { i += 1; continue; } let is_exists = matches!(subquery.query_type, ast::SubqueryType::Exists { .. }); if !is_exists { i += 1; continue; } if try_unnest_exists(plan, i) { // Subquery was removed from the vec; don't increment i. continue; } i += 1; } Ok(()) } /// Try to unnest a single EXISTS subquery at index `subquery_idx`. /// Returns true if the subquery was successfully unnested and removed. fn try_unnest_exists(plan: &mut SelectPlan, subquery_idx: usize) -> bool { // 1. Extract the inner plan (if available). let inner_plan = { let subquery = &plan.non_from_clause_subqueries[subquery_idx]; let SubqueryState::Unevaluated { plan: inner } = &subquery.state else { return false; }; let Some(inner) = inner.as_ref() else { return false; }; inner.clone() }; let subquery_id = plan.non_from_clause_subqueries[subquery_idx].internal_id; // 2. Check blockers: the inner plan must be simple enough to unnest. if !can_unnest_inner_plan(&inner_plan) { return false; } // 3. Determine if this is EXISTS or NOT EXISTS by scanning the outer WHERE clause. let Some(where_info) = find_exists_in_where(&plan.where_clause, subquery_id) else { return false; }; let join_type = if where_info.negated { JoinType::Anti } else { JoinType::Semi }; // 4. Extract correlation predicates from the inner WHERE clause. // These are predicates of the form `inner_col = outer_col` where one side // references an outer query ref and the other side references an inner table. let outer_table_ids: Vec = inner_plan .table_references .outer_query_refs() .iter() .map(|r| r.internal_id) .collect(); let inner_table_ids: Vec = inner_plan .table_references .joined_tables() .iter() .map(|t| t.internal_id) .collect(); // All inner WHERE terms must be expressible as join predicates or filters // on inner tables only. If any term references outer tables in a non-equality // context, we bail out. for term in &inner_plan.where_clause { if !is_valid_unnesting_predicate(&term.expr, &outer_table_ids, &inner_table_ids) { return false; } } // For anti-join rewrites, every inner WHERE term must reference an inner table. // Principle ([SQLITE-EXISTS], [PG-SUBQUERY]): NOT EXISTS depends on inner-row // emptiness, so inner-independent gates must stay under the quantifier. // Example: `NOT EXISTS (... WHERE corr AND 0)` is TRUE for every outer row. // Hoisting `0` to outer WHERE would reject all rows, so this rewrite is unsafe. if join_type == JoinType::Anti { for term in &inner_plan.where_clause { let refs = collect_table_refs(&term.expr); if !refs.iter().any(|t| inner_table_ids.contains(t)) { return false; } } } // 4b. Block unnesting if any correlation predicate references a table that // is on the nullable side of a LEFT/FULL OUTER JOIN in the outer plan. // Principle ([PG-JOIN-ORDER]): OUTER JOIN null-extension is defined before // WHERE filtering; moving such predicates across the boundary is not safe. // Example: correlating to nullable RHS columns can drop rows that should // survive as NULL-extended rows. let mut nullable_outer_table_ids: Vec = Vec::new(); let joined = plan.table_references.joined_tables(); for (i, t) in joined.iter().enumerate() { if let Some(ji) = &t.join_info { if ji.is_outer() || ji.is_full_outer() { // Right-side table of LEFT/FULL OUTER JOIN is nullable. nullable_outer_table_ids.push(t.internal_id); } if ji.is_full_outer() && i > 0 { // Left-side table of FULL OUTER JOIN is also nullable. nullable_outer_table_ids.push(joined[i - 1].internal_id); } } } if !nullable_outer_table_ids.is_empty() { // Check if any correlation predicate touches a nullable outer table. for term in &inner_plan.where_clause { let refs = collect_table_refs(&term.expr); if refs.iter().any(|t| nullable_outer_table_ids.contains(t)) { return false; } } } // 5. Perform the rewrite. // Move inner tables into the outer plan as semi/anti-joined tables. let mut inner_plan = inner_plan; let inner_tables = std::mem::take(inner_plan.table_references.joined_tables_mut()); for (idx, mut table) in inner_tables.into_iter().enumerate() { if idx == 0 { // First inner table gets the semi/anti-join annotation. table.join_info = Some(JoinInfo { join_type, using: vec![], no_reorder: false, }); } plan.table_references.add_joined_table(table); } // Move inner WHERE terms to the outer plan's WHERE clause. // The outer_query_ref column references in these terms already point to the // correct table IDs (they were set up during subquery planning), so they // work correctly in the outer scope. // Reset `consumed` since the inner optimizer may have marked terms consumed // during its own optimization pass; in the outer plan they need re-evaluation. for mut term in inner_plan.where_clause { term.consumed = false; plan.where_clause.push(term); } // Move any inner non-FROM subqueries to the outer plan. for inner_subquery in inner_plan.non_from_clause_subqueries { plan.non_from_clause_subqueries.push(inner_subquery); } // Replace the EXISTS/NOT EXISTS expression in the outer WHERE with a no-op (true). // The semi/anti-join handles the filtering. replace_exists_with_true(&mut plan.where_clause, where_info.where_term_idx); // Remove the subquery from the outer plan's subquery list. // Note: subquery_idx may have shifted if we inserted inner subqueries above, // but we inserted at the END, so the original index is still valid. plan.non_from_clause_subqueries.remove(subquery_idx); true } /// Check if the inner plan is simple enough to unnest. fn can_unnest_inner_plan(plan: &SelectPlan) -> bool { // Blocker ([MYSQL-SEMIJOIN]): only rewrite simple single-source subqueries. // Principle: current VM early-out is loop-local; multi-table inners would // need additional state to preserve existential semantics. // Example: EXISTS over `i1 JOIN i2` can match only at deeper loop levels. if plan.table_references.joined_tables().len() != 1 { return false; } // Blocker ([PG-SUBQUERY], [SQLITE-EXISTS]): LIMIT can change emptiness. // Example: `EXISTS(... LIMIT 0)` is always FALSE. if plan.limit.is_some() { return false; } // Blocker ([MYSQL-SEMIJOIN]): grouped subqueries require grouped rewrite. // Example: GROUP BY/HAVING subquery is not equivalent to row-level semi-join. if plan.group_by.is_some() { return false; } // Blocker ([MYSQL-SEMIJOIN]): ORDER BY on a plain EXISTS is semantically // irrelevant, but in practice appears with other complex constructs we // don't decorrelate here (keep this pass intentionally conservative). if !plan.order_by.is_empty() { return false; } // Blocker ([MYSQL-SEMIJOIN]): DISTINCT + existential checks may require // duplicate-elimination-aware planning. // Example: DISTINCT in inner subquery should not change outer cardinality. if !matches!( plan.distinctness, crate::translate::plan::Distinctness::NonDistinct ) { return false; } // Blocker ([MYSQL-SEMIJOIN]): window frames are not row-local filters. // Example: window function values depend on partition context. if plan.window.is_some() { return false; } // Blocker ([PG-SUBQUERY]): OFFSET changes emptiness independently of joins. // Example: `EXISTS(... OFFSET 1000)` may become FALSE even with matches. if plan.offset.is_some() { return false; } // Blocker ([MYSQL-SEMIJOIN]): VALUES-based inners are not handled by this // table-based rewrite path. if !plan.values.is_empty() { return false; } // Blocker ([PG-SUBQUERY]): aggregate subqueries can produce a row even when // no base rows match, which breaks existential rewrite assumptions. // Example: `EXISTS(SELECT count(*) FROM i WHERE false)` is TRUE. if !plan.aggregates.is_empty() { return false; } // Blocker ([MYSQL-SEMIJOIN]): nested correlated subqueries need layered // decorrelation ordering not implemented in this pass. // Example: inner WHERE contains `EXISTS (SELECT ... correlated to inner)`. if plan.non_from_clause_subqueries.iter().any(|s| s.correlated) { return false; } // Blocker ([PG-SUBQUERY]): side-effecting/volatile expressions may be // evaluated a different number of times after rewrite. // Example: `random()` under EXISTS should keep original evaluation behavior. for term in &plan.where_clause { if contains_nondeterministic_function(&term.expr) { return false; } } true } /// Information about where an EXISTS/NOT EXISTS expression appears in the WHERE clause. struct ExistsWhereInfo { /// Index into the WHERE clause vector. where_term_idx: usize, /// Whether the EXISTS is negated (NOT EXISTS). negated: bool, } /// Find the WHERE term that references the EXISTS subquery with the given ID. /// Returns None if the subquery is referenced in a context we can't unnest /// (e.g., inside OR, or referenced multiple times). fn find_exists_in_where( where_clause: &[WhereTerm], subquery_id: TableInternalId, ) -> Option { for (idx, term) in where_clause.iter().enumerate() { // Blocker ([PG-JOIN-ORDER]): OUTER JOIN ON terms cannot be rewritten as // normal WHERE terms without changing null-extension behavior. // Example: `LEFT JOIN ... ON EXISTS(...)` must still emit unmatched rows. if term.from_outer_join.is_some() { continue; } // Check for direct EXISTS reference: SubqueryResult { Exists } if let Expr::SubqueryResult { subquery_id: sid, query_type: ast::SubqueryType::Exists { .. }, .. } = &term.expr { if *sid == subquery_id { return Some(ExistsWhereInfo { where_term_idx: idx, negated: false, }); } } // Check for NOT EXISTS: Unary(Not, SubqueryResult { Exists }) if let Expr::Unary(UnaryOperator::Not, inner) = &term.expr { if let Expr::SubqueryResult { subquery_id: sid, query_type: ast::SubqueryType::Exists { .. }, .. } = inner.as_ref() { if *sid == subquery_id { return Some(ExistsWhereInfo { where_term_idx: idx, negated: true, }); } } } } None } /// Check if a predicate expression is valid for unnesting. /// Valid predicates are: /// - Pure inner-table predicates (no outer refs) /// - Equality predicates between outer and inner columns (correlation predicates) /// - Any expression that doesn't reference outer tables in non-equality positions fn is_valid_unnesting_predicate( expr: &Expr, outer_table_ids: &[TableInternalId], inner_table_ids: &[TableInternalId], ) -> bool { // Check if the expression references any outer tables. let mut has_outer_ref = false; let _ = walk_expr(expr, &mut |e: &Expr| -> Result { if let Expr::Column { table, .. } = e { if outer_table_ids.contains(table) { has_outer_ref = true; } } Ok(WalkControl::Continue) }); if !has_outer_ref { // Pure inner predicate: always valid. return true; } // For predicates with outer refs, we only support simple equality: // inner_col = outer_col or outer_col = inner_col is_correlation_equality(expr, outer_table_ids, inner_table_ids) } /// Check if an expression is a simple equality between an outer and inner column reference. fn is_correlation_equality( expr: &Expr, outer_table_ids: &[TableInternalId], inner_table_ids: &[TableInternalId], ) -> bool { if let Expr::Binary(lhs, ast::Operator::Equals, rhs) = expr { let lhs_tables = collect_table_refs(lhs); let rhs_tables = collect_table_refs(rhs); // One side references only outer tables, the other only inner tables. let lhs_is_outer = lhs_tables.iter().all(|t| outer_table_ids.contains(t)) && !lhs_tables.is_empty(); let lhs_is_inner = lhs_tables.iter().all(|t| inner_table_ids.contains(t)) && !lhs_tables.is_empty(); let rhs_is_outer = rhs_tables.iter().all(|t| outer_table_ids.contains(t)) && !rhs_tables.is_empty(); let rhs_is_inner = rhs_tables.iter().all(|t| inner_table_ids.contains(t)) && !rhs_tables.is_empty(); (lhs_is_outer && rhs_is_inner) || (lhs_is_inner && rhs_is_outer) } else { false } } /// Collect all table IDs referenced by column expressions in an expression tree. fn collect_table_refs(expr: &Expr) -> SmallVec<[TableInternalId; 2]> { let mut refs = SmallVec::new(); let _ = walk_expr(expr, &mut |e: &Expr| -> Result { if let Expr::Column { table, .. } = e { if !refs.contains(table) { refs.push(*table); } } Ok(WalkControl::Continue) }); refs } /// Check if an expression tree contains any non-deterministic function calls /// (e.g. random(), changes(), last_insert_rowid()). fn contains_nondeterministic_function(expr: &Expr) -> bool { let mut found = false; let _ = walk_expr(expr, &mut |e: &Expr| -> Result { match e { Expr::FunctionCall { name, args, .. } => { if let Ok(func) = Func::resolve_function(name.as_str(), args.len()) { if !func.is_deterministic() { found = true; } } } Expr::FunctionCallStar { name, .. } => { // Star functions like count(*) — resolve with 0 args if let Ok(func) = Func::resolve_function(name.as_str(), 0) { if !func.is_deterministic() { found = true; } } } _ => {} } Ok(WalkControl::Continue) }); found } /// Replace the WHERE term at the given index with a trivially-true expression. fn replace_exists_with_true(where_clause: &mut [WhereTerm], idx: usize) { where_clause[idx].expr = Expr::Literal(ast::Literal::Numeric("1".to_string())); } ================================================ FILE: core/translate/order_by.rs ================================================ use crate::sync::Arc; use turso_parser::ast::{self, SortOrder}; use crate::{ emit_explain, schema::{Index, IndexColumn, PseudoCursorType, Schema}, translate::{ collate::{get_collseq_from_expr, CollationSeq}, group_by::is_orderby_agg_or_const, plan::Aggregate, }, util::exprs_are_equivalent, vdbe::{ builder::{CursorType, ProgramBuilder}, insn::{to_u16, IdxInsertFlags, Insn}, }, Result, }; use super::{ emitter::TranslateCtx, expr::translate_expr, plan::{Distinctness, ResultSetColumn, SelectPlan, TableReferences}, result_row::{emit_offset, emit_result_row_and_limit}, }; use crate::vdbe::insn::SortComparatorType; /// Maps a custom type `<` operator function name to a SortComparatorType. /// Returns None if the function name is not recognized. fn sort_comparator_from_func_name(func_name: &str) -> Option { match func_name { "numeric_lt" => Some(SortComparatorType::NumericLt), "test_uint_lt" => Some(SortComparatorType::TestUintLt), "string_reverse" => Some(SortComparatorType::StringReverse), "array_lt" => Some(SortComparatorType::ArrayLt), _ => None, } } /// For an ORDER BY expression that is a column reference to a custom type, /// returns the SortComparatorType if the type has a `<` operator with a known /// comparator. Returns None otherwise, which causes the sorter to use encoded /// blob ordering instead of silently wrong results. pub(crate) fn custom_type_comparator( expr: &ast::Expr, referenced_tables: &TableReferences, schema: &Schema, ) -> Option { if let ast::Expr::Column { table: table_ref_id, column, .. } = expr { let (_, table) = referenced_tables.find_table_by_internal_id(*table_ref_id)?; let col = table.get_column_at(*column)?; // Array columns use element-wise comparison if col.is_array() { return Some(SortComparatorType::ArrayLt); } let type_def = schema.get_type_def(&col.ty_str, table.is_strict())?; type_def .operators .iter() .find(|op| op.op == "<") .and_then(|op| op.func_name.as_ref()) .and_then(|func_name| sort_comparator_from_func_name(func_name)) } else if super::expr::expr_is_array(expr, Some(referenced_tables)) { Some(SortComparatorType::ArrayLt) } else { None } } /// For a result column expression that is a column reference to a custom type, /// returns the column definition and type definition. fn result_column_custom_type_info<'a>( expr: &ast::Expr, referenced_tables: &'a TableReferences, schema: &'a Schema, ) -> Option<( &'a crate::schema::Column, std::sync::Arc, )> { if let ast::Expr::Column { table: table_ref_id, column, .. } = expr { let (_, table) = referenced_tables.find_table_by_internal_id(*table_ref_id)?; let col = table.get_column_at(*column)?; let type_def = schema.get_type_def(&col.ty_str, table.is_strict())?.clone(); Some((col, type_def)) } else { None } } /// Returns true if the expression is a column reference to a custom type /// (with encode/decode) that does NOT have a `<` operator with a known /// sort comparator. This includes types with no `<` operator at all, and /// types whose `<` function is not recognized by the sorter. fn is_custom_type_without_lt( expr: &ast::Expr, referenced_tables: &TableReferences, schema: &Schema, ) -> bool { if let ast::Expr::Column { table: table_ref_id, column, .. } = expr { if let Some((_, table)) = referenced_tables.find_table_by_internal_id(*table_ref_id) { if let Some(col) = table.get_column_at(*column) { if let Some(type_def) = schema.get_type_def(&col.ty_str, table.is_strict()) { if type_def.decode.is_some() { // No `<` operator at all (naked or with function) return !type_def.operators.iter().any(|op| op.op == "<"); } } } } } false } // Metadata for handling ORDER BY operations #[derive(Debug)] pub struct SortMetadata { // cursor id for the Sorter table where the sorted rows are stored pub sort_cursor: usize, // register where the sorter data is inserted and later retrieved from pub reg_sorter_data: usize, // We need to emit result columns in the order they are present in the SELECT, but they may not be in the same order in the ORDER BY sorter. // This vector holds the indexes of the result columns in the ORDER BY sorter. // This vector must be the same length as the result columns. pub remappings: Vec, /// Whether we append an extra ascending "Sequence" key to the ORDER BY sort keys. /// This is used *only* when a GROUP BY is present *and* ORDER BY is not purely /// aggregates/constants, so that rows that tie on ORDER BY terms are output in /// the same relative order the underlying row stream produced them. pub has_sequence: bool, /// Whether to use heap-sort with BTreeIndex instead of full-collection sort through Sorter pub use_heap_sort: bool, } pub struct EmitOrderBy; impl EmitOrderBy { /// Initialize resources needed for ORDER BY processing #[allow(clippy::too_many_arguments)] pub fn init( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, result_columns: &[ResultSetColumn], order_by: &[(Box, SortOrder)], referenced_tables: &TableReferences, has_group_by: bool, has_distinct: bool, aggregates: &[Aggregate], ) -> Result<()> { // Block ORDER BY on custom type columns without OPERATOR '<' for (expr, _) in order_by.iter() { if is_custom_type_without_lt(expr, referenced_tables, t_ctx.resolver.schema()) { if let Some((col, type_def)) = result_column_custom_type_info(expr, referenced_tables, t_ctx.resolver.schema()) { let col_name = col.name.as_deref().unwrap_or("?"); crate::bail_parse_error!( "cannot ORDER BY column '{}' of type '{}': type does not declare OPERATOR '<'", col_name, type_def.name ); } crate::bail_parse_error!( "cannot ORDER BY a custom type column that does not declare OPERATOR '<'" ); } } let only_aggs = order_by .iter() .all(|(e, _)| is_orderby_agg_or_const(&t_ctx.resolver, e, aggregates)); let use_heap_sort = !has_distinct && !has_group_by && t_ctx.limit_ctx.is_some(); // only emit sequence column if (we have GROUP BY and ORDER BY is not only aggregates or constants) OR (we decided to use heap-sort) let has_sequence = (has_group_by && !only_aggs) || use_heap_sort; let remappings = order_by_deduplicate_result_columns(order_by, result_columns, has_sequence); let sort_cursor = if use_heap_sort { let index_name = format!("heap_sort_{}", program.offset().as_offset_int()); // we don't really care about the name that much, just enough that we don't get name collisions let mut index_columns = Vec::with_capacity(order_by.len() + result_columns.len()); for (column, order) in order_by { let collation = get_collseq_from_expr(column, referenced_tables)?; let pos_in_table = index_columns.len(); index_columns.push(IndexColumn { name: pos_in_table.to_string(), order: *order, pos_in_table, collation, default: None, expr: None, }) } let pos_in_table = index_columns.len(); // add sequence number between ORDER BY columns and result column index_columns.push(IndexColumn { name: pos_in_table.to_string(), order: SortOrder::Asc, pos_in_table, collation: None, default: None, expr: None, }); for _ in remappings.iter().filter(|r| !r.deduplicated) { let pos_in_table = index_columns.len(); index_columns.push(IndexColumn { name: pos_in_table.to_string(), order: SortOrder::Asc, pos_in_table, collation: None, default: None, expr: None, }) } let index = Arc::new(Index { name: index_name, table_name: String::new(), ephemeral: true, root_page: 0, columns: index_columns, unique: false, has_rowid: false, where_clause: None, index_method: None, on_conflict: None, }); program.alloc_cursor_id(CursorType::BTreeIndex(index)) } else { program.alloc_cursor_id(CursorType::Sorter) }; t_ctx.meta_sort = Some(SortMetadata { sort_cursor, reg_sorter_data: program.alloc_register(), remappings, has_sequence, use_heap_sort, }); if use_heap_sort { program.emit_insn(Insn::OpenEphemeral { cursor_id: sort_cursor, is_table: false, }); } else { /* * Terms of the ORDER BY clause that is part of a SELECT statement may be assigned a collating sequence using the COLLATE operator, * in which case the specified collating function is used for sorting. * Otherwise, if the expression sorted by an ORDER BY clause is a column, * then the collating sequence of the column is used to determine sort order. * If the expression is not a column and has no COLLATE clause, then the BINARY collating sequence is used. */ let mut order_and_collations: Vec<(SortOrder, Option)> = order_by .iter() .map(|(expr, dir)| { let collation = get_collseq_from_expr(expr, referenced_tables)?; Ok((*dir, collation)) }) .collect::>>()?; // Resolve custom type comparators for ORDER BY columns. // For types with a `<` operator, the comparator is used for correct sort ordering. let mut comparators: Vec> = order_by .iter() .map(|(expr, _)| { custom_type_comparator(expr, referenced_tables, t_ctx.resolver.schema()) }) .collect(); if has_sequence { // sequence column: ascending with BINARY collation, no comparator order_and_collations.push((SortOrder::Asc, Some(CollationSeq::default()))); comparators.push(None); } let key_len = order_and_collations.len(); program.emit_insn(Insn::SorterOpen { cursor_id: sort_cursor, columns: key_len, order_and_collations, comparators, }); } Ok(()) } /// Emits the bytecode for outputting rows from an ORDER BY sorter. /// This is called when the main query execution loop has finished processing, /// and we can now emit rows from the ORDER BY sorter. pub fn emit( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, plan: &SelectPlan, ) -> Result<()> { let order_by = &plan.order_by; let result_columns = &plan.result_columns; let sort_loop_start_label = program.allocate_label(); let sort_loop_next_label = program.allocate_label(); let sort_loop_end_label = program.allocate_label(); let SortMetadata { sort_cursor, reg_sorter_data, ref remappings, has_sequence, use_heap_sort, } = *t_ctx.meta_sort.as_ref().unwrap(); let sorter_column_count = order_by.len() + if has_sequence { 1 } else { 0 } + remappings.iter().filter(|r| !r.deduplicated).count(); if use_heap_sort { emit_explain!(program, false, "USE TEMP B-TREE FOR ORDER BY".to_owned()); } else { emit_explain!(program, false, "USE SORTER FOR ORDER BY".to_owned()); } let cursor_id = if !use_heap_sort { let pseudo_cursor = program.alloc_cursor_id(CursorType::Pseudo(PseudoCursorType { column_count: sorter_column_count, })); program.emit_insn(Insn::OpenPseudo { cursor_id: pseudo_cursor, content_reg: reg_sorter_data, num_fields: sorter_column_count, }); program.emit_insn(Insn::SorterSort { cursor_id: sort_cursor, pc_if_empty: sort_loop_end_label, }); pseudo_cursor } else { program.emit_insn(Insn::Rewind { cursor_id: sort_cursor, pc_if_empty: sort_loop_end_label, }); sort_cursor }; program.preassign_label_to_next_insn(sort_loop_start_label); emit_offset(program, sort_loop_next_label, t_ctx.reg_offset); if !use_heap_sort { program.emit_insn(Insn::SorterData { cursor_id: sort_cursor, dest_reg: reg_sorter_data, pseudo_cursor: cursor_id, }); } // We emit the columns in SELECT order, not sorter order (sorter always has the sort keys first). // This is tracked in sort_metadata.remappings. let start_reg = t_ctx.reg_result_cols_start.unwrap(); for (i, rc) in result_columns.iter().enumerate() { let reg = start_reg + i; let remapping = remappings .get(i) .expect("remapping must exist for all result columns"); let column_idx = remapping.orderby_sorter_idx; program.emit_column_or_rowid(cursor_id, column_idx, reg); // Deduplicated columns share a sort key slot, which stores the encoded // (on-disk) value (decode was suppressed during sorter insert). Apply // DECODE now so the result set contains human-readable values. if remapping.deduplicated { if let Some((col, type_def)) = result_column_custom_type_info( &rc.expr, &plan.table_references, t_ctx.resolver.schema(), ) { if let Some(ref decode_expr) = type_def.decode { let skip_label = program.allocate_label(); program.emit_insn(Insn::IsNull { reg, target_pc: skip_label, }); super::expr::emit_type_expr( program, decode_expr, reg, reg, col, &type_def, &t_ctx.resolver, )?; program.resolve_label(skip_label, program.offset()); } } } } // Decode array blobs to JSON text for display, after extracting from sorter super::result_row::emit_array_decode_for_results( program, result_columns, &plan.table_references, start_reg, &t_ctx.resolver, )?; emit_result_row_and_limit( program, plan, start_reg, t_ctx.limit_ctx, if !use_heap_sort { Some(sort_loop_end_label) } else { None }, )?; program.resolve_label(sort_loop_next_label, program.offset()); if !use_heap_sort { program.emit_insn(Insn::SorterNext { cursor_id: sort_cursor, pc_if_next: sort_loop_start_label, }); } else { program.emit_insn(Insn::Next { cursor_id: sort_cursor, pc_if_next: sort_loop_start_label, }); } program.preassign_label_to_next_insn(sort_loop_end_label); Ok(()) } /// Emits the bytecode for inserting a row into an ORDER BY sorter. pub fn sorter_insert( program: &mut ProgramBuilder, t_ctx: &TranslateCtx, plan: &SelectPlan, ) -> Result<()> { let resolver = &t_ctx.resolver; let sort_metadata = t_ctx.meta_sort.as_ref().expect("sort metadata must exist"); let order_by = &plan.order_by; let order_by_len = order_by.len(); let result_columns = &plan.result_columns; let result_columns_to_skip_len = sort_metadata .remappings .iter() .filter(|r| r.deduplicated) .count(); // The ORDER BY sorter has the sort keys first, then the result columns. let orderby_sorter_column_count = order_by_len + if sort_metadata.has_sequence { 1 } else { 0 } + result_columns.len() - result_columns_to_skip_len; let start_reg = program.alloc_registers(orderby_sorter_column_count); for (i, (expr, _)) in order_by.iter().enumerate() { let key_reg = start_reg + i; // Check if this ORDER BY expression matches a finalized aggregate if let Some(agg_idx) = plan .aggregates .iter() .position(|agg| exprs_are_equivalent(&agg.original_expr, expr)) { // This ORDER BY expression is an aggregate, so copy from register let agg_start_reg = t_ctx .reg_agg_start .expect("aggregate registers must be initialized"); let src_reg = agg_start_reg + agg_idx; program.emit_insn(Insn::Copy { src_reg, dst_reg: key_reg, extra_amount: 0, }); } else { // Sort keys must be encoded (on-disk) values. Suppress decode so the // sorter compares encoded representations, using either the base type's // built-in comparison (naked OPERATOR '<') or a custom comparator function. let is_custom = result_column_custom_type_info(expr, &plan.table_references, resolver.schema()) .is_some_and(|(_, td)| td.decode.is_some()); if is_custom { program.suppress_custom_type_decode = true; } let result = translate_expr( program, Some(&plan.table_references), expr, key_reg, resolver, ); if is_custom { program.suppress_custom_type_decode = false; } result?; } } let SortMetadata { sort_cursor, reg_sorter_data, use_heap_sort, .. } = sort_metadata; let skip_label = if *use_heap_sort { // skip records which greater than current top-k maintained in a separate BTreeIndex let insert_label = program.allocate_label(); let skip_label = program.allocate_label(); let limit = t_ctx.limit_ctx.as_ref().expect("limit must be set"); let limit_reg = t_ctx.reg_limit_offset_sum.unwrap_or(limit.reg_limit); program.emit_insn(Insn::IfPos { reg: limit_reg, target_pc: insert_label, decrement_by: 1, }); program.emit_insn(Insn::Last { cursor_id: *sort_cursor, pc_if_empty: insert_label, }); program.emit_insn(Insn::IdxLE { cursor_id: *sort_cursor, start_reg, num_regs: orderby_sorter_column_count, target_pc: skip_label, }); program.emit_insn(Insn::Delete { cursor_id: *sort_cursor, table_name: "".to_string(), is_part_of_update: false, }); program.preassign_label_to_next_insn(insert_label); Some(skip_label) } else { None }; let mut cur_reg = start_reg + order_by_len; if sort_metadata.has_sequence { program.emit_insn(Insn::Sequence { cursor_id: sort_metadata.sort_cursor, target_reg: cur_reg, }); cur_reg += 1; } for (i, rc) in result_columns.iter().enumerate() { // If the result column is an exact duplicate of a sort key, we skip it. if sort_metadata .remappings .get(i) .expect("remapping must exist for all result columns") .deduplicated { continue; } translate_expr( program, Some(&plan.table_references), &rc.expr, cur_reg, resolver, )?; cur_reg += 1; } // Handle SELECT DISTINCT deduplication if let Distinctness::Distinct { ctx } = &plan.distinctness { let distinct_ctx = ctx.as_ref().expect("distinct context must exist"); // For distinctness checking with Insn::Found, we need a contiguous run of registers containing all the result columns. // The emitted columns are in the ORDER BY sorter order, which may be different from the SELECT order, and obviously the // ORDER BY clause may not have all the result columns. // Hence, we need to allocate new registers and Copy from the existing ones to make a contiguous run of registers. let mut needs_reordering = false; // Check if result columns in sorter are in SELECT order let mut prev = None; for (select_idx, _rc) in result_columns.iter().enumerate() { let sorter_idx = sort_metadata .remappings .get(select_idx) .expect("remapping must exist for all result columns") .orderby_sorter_idx; if prev.is_some_and(|p| sorter_idx != p + 1) { needs_reordering = true; break; } prev = Some(sorter_idx); } if needs_reordering { // Allocate registers for reordered result columns. // TODO: it may be possible to optimize this to minimize the number of Insn::Copy we do, but for now // we will just allocate a new reg for every result column. let reordered_start_reg = program.alloc_registers(result_columns.len()); for (select_idx, _rc) in result_columns.iter().enumerate() { let remapping = sort_metadata .remappings .get(select_idx) .expect("remapping must exist for all result columns"); let src_reg = start_reg + remapping.orderby_sorter_idx; let dst_reg = reordered_start_reg + select_idx; program.emit_insn(Insn::Copy { src_reg, dst_reg, extra_amount: 0, }); } distinct_ctx.emit_deduplication_insns( program, result_columns.len(), reordered_start_reg, ); } else { // Result columns are already in SELECT order, use them directly let start_reg = sort_metadata .remappings .first() .map(|r| start_reg + r.orderby_sorter_idx) .expect("remapping must exist for all result columns"); distinct_ctx.emit_deduplication_insns(program, result_columns.len(), start_reg); } } if *use_heap_sort { program.emit_insn(Insn::MakeRecord { start_reg: to_u16(start_reg), count: to_u16(orderby_sorter_column_count), dest_reg: to_u16(*reg_sorter_data), index_name: None, affinity_str: None, }); program.emit_insn(Insn::IdxInsert { cursor_id: *sort_cursor, record_reg: *reg_sorter_data, unpacked_start: None, unpacked_count: None, flags: IdxInsertFlags::new(), }); program.preassign_label_to_next_insn(skip_label.unwrap()); } else { sorter_insert( program, start_reg, orderby_sorter_column_count, *sort_cursor, *reg_sorter_data, ); } Ok(()) } } /// Emits the bytecode for inserting a row into a sorter. /// This can be either a GROUP BY sorter or an ORDER BY sorter. pub fn sorter_insert( program: &mut ProgramBuilder, start_reg: usize, column_count: usize, cursor_id: usize, record_reg: usize, ) { program.emit_insn(Insn::MakeRecord { start_reg: to_u16(start_reg), count: to_u16(column_count), dest_reg: to_u16(record_reg), index_name: None, affinity_str: None, }); program.emit_insn(Insn::SorterInsert { cursor_id, record_reg, }); } #[derive(Debug)] /// A mapping between a result column and its index in the ORDER BY sorter. /// ORDER BY columns are emitted first, then the result columns. /// If a result column is an exact duplicate of a sort key, we skip it. /// If we skip a result column, we need to keep track which ORDER BY column it matches. pub struct OrderByRemapping { pub orderby_sorter_idx: usize, pub deduplicated: bool, } /// In case any of the ORDER BY sort keys are exactly equal to a result column, we can skip emitting that result column. /// If we skip a result column, we need to keep track what index in the ORDER BY sorter the result columns have, /// because the result columns should be emitted in the SELECT clause order, not the ORDER BY clause order. pub fn order_by_deduplicate_result_columns( order_by: &[(Box, SortOrder)], result_columns: &[ResultSetColumn], has_sequence: bool, ) -> Vec { let mut result_column_remapping: Vec = Vec::new(); let order_by_len = order_by.len(); // `sequence_offset` shifts the base index where non-deduped SELECT columns begin, // because Sequence sits after ORDER BY keys but before result columns. let sequence_offset = if has_sequence { 1 } else { 0 }; let mut i = 0; for rc in result_columns.iter() { let found = order_by .iter() .enumerate() .find(|(_, (expr, _))| exprs_are_equivalent(expr, &rc.expr)); if let Some((j, _)) = found { result_column_remapping.push(OrderByRemapping { orderby_sorter_idx: j, deduplicated: true, }); } else { // This result column is not a duplicate of any ORDER BY key, so its sorter // index comes after all ORDER BY entries (hence the +order_by_len). The // counter `i` tracks how many such non-duplicate result columns we've seen. result_column_remapping.push(OrderByRemapping { orderby_sorter_idx: order_by_len + sequence_offset + i, deduplicated: false, }); i += 1; } } result_column_remapping } ================================================ FILE: core/translate/plan.rs ================================================ use rustc_hash::FxHashMap as HashMap; use smallvec::SmallVec; use std::{cmp::Ordering, marker::PhantomData, sync::Arc}; use turso_parser::ast::{ self, FrameBound, FrameClause, FrameExclude, FrameMode, ResolveType, SortOrder, SubqueryType, }; use crate::{ function::{AggFunc, WindowFunc}, schema::{BTreeTable, ColDef, Column, FromClauseSubquery, Index, Schema, Table}, translate::{ collate::{get_collseq_from_expr, CollationSeq}, emitter::UpdateRowSource, expr::{as_binary_components, get_expr_affinity}, expression_index::{normalize_expr_for_index_matching, single_table_column_usage}, optimizer::constraints::{BinaryExprSide, SeekRangeConstraint}, planner::determine_where_to_eval_term, }, vdbe::{ affinity::{self, Affinity}, builder::{CursorKey, CursorType, ProgramBuilder}, insn::{HashDistinctData, Insn}, BranchOffset, CursorID, }, Result, VirtualTable, }; use crate::{schema::Type, types::SeekOp}; use turso_parser::ast::TableInternalId; use super::emitter::OperationMode; /// Infer the Type and type name from an expression's affinity. /// /// Used for subquery result columns. SQLite derives column affinity from: /// - Column references: the declared column type /// - CAST expressions: the cast target type /// - Subqueries: recursively from the subquery's result expression /// - Literals: BLOB affinity (no affinity) /// /// The affinity determines comparison behavior in IN expressions, etc. fn infer_type_from_expr( expr: &ast::Expr, tables: Option<&TableReferences>, ) -> (Type, &'static str) { let affinity = get_expr_affinity(expr, tables, None); match affinity { Affinity::Integer => (Type::Integer, "INTEGER"), Affinity::Real => (Type::Real, "REAL"), Affinity::Text => (Type::Text, "TEXT"), Affinity::Numeric => (Type::Numeric, "NUMERIC"), Affinity::Blob => (Type::Blob, "BLOB"), } } #[derive(Debug, Clone)] pub struct ResultSetColumn { pub expr: ast::Expr, pub alias: Option, // TODO: encode which aggregates (e.g. index bitmask of plan.aggregates) are present in this column pub contains_aggregates: bool, } impl ResultSetColumn { pub fn name<'a>(&'a self, tables: &'a TableReferences) -> Option<&'a str> { if let Some(alias) = &self.alias { return Some(alias); } match &self.expr { ast::Expr::Column { table, column, .. } => { if let Some(joined_table_ref) = tables.find_joined_table_by_internal_id(*table) { if let Operation::IndexMethodQuery(module) = &joined_table_ref.op { if module.covered_columns.contains_key(column) { return None; } } joined_table_ref .table .get_column_at(*column) .unwrap() .name .as_deref() } else { // Column references an outer query table (correlated subquery). let (_, table_ref) = tables.find_table_by_internal_id(*table)?; table_ref.get_column_at(*column)?.name.as_deref() } } ast::Expr::RowId { table, .. } => { // If there is a rowid alias column, use its name let (_, table_ref) = tables.find_table_by_internal_id(*table)?; if let Table::BTree(table) = &table_ref { if let Some(rowid_alias_column) = table.get_rowid_alias_column() { if let Some(name) = &rowid_alias_column.1.name { return Some(name); } } } // If there is no rowid alias, use "rowid". Some("rowid") } _ => None, } } } #[derive(Debug, Clone)] pub struct GroupBy { pub exprs: Vec, /// Sort direction for each GROUP BY key column. Always present once /// `compute_group_by_sort_order` has run; the outer optimizer reads /// this to derive the materialized CTE's output order. pub sort_order: Vec, /// When true the scan already provides the GROUP BY order and no /// sorter is emitted. The `sort_order` is kept so that the outer /// query can still read the effective output order. pub sort_elided: bool, /// having clause split into a vec at 'AND' boundaries. pub having: Option>, } /// In a query plan, WHERE clause conditions and JOIN conditions are all folded into a vector of WhereTerm. /// This is done so that we can evaluate the conditions at the correct loop depth. /// We also need to keep track of whether the condition came from an OUTER JOIN. Take this example: /// SELECT * FROM users u LEFT JOIN products p ON u.id = 5. /// Even though the condition only refers to 'u', we CANNOT evaluate it at the users loop, because we need to emit NULL /// values for the columns of 'p', for EVERY row in 'u', instead of completely skipping any rows in 'u' where the condition is false. #[derive(Debug, Clone)] pub struct WhereTerm { /// The original condition expression. pub expr: ast::Expr, /// For normal JOIN conditions (ON or WHERE clauses), we break them up into individual [WhereTerm] conditions /// and let the optimizer determine when each should be evaluated based on the tables they reference. /// See e.g. [EvalAt]. /// For example, in "SELECT * FROM x JOIN y WHERE x.a = 2", we want to evaluate x.a = 2 right after opening x /// since it only depends on x. /// /// However, OUTER JOIN conditions require special handling. Consider: /// SELECT * FROM t LEFT JOIN s ON t.a = 2 /// /// Even though t.a = 2 only references t, we cannot evaluate it during t's loop and skip rows where t.a != 2. /// Instead, we must: /// 1. Process ALL rows from t /// 2. For each t row where t.a != 2, emit NULL values for s's columns /// 3. For each t row where t.a = 2, emit the actual s values /// /// This means the condition must be evaluated during s's loop, regardless of which tables it references. /// We track this requirement using [WhereTerm::from_outer_join], which contains the [TableInternalId] of the /// right-side table of the OUTER JOIN (in this case, s). When evaluating conditions, if [WhereTerm::from_outer_join] /// is set, we force evaluation to happen during that table's loop. pub from_outer_join: Option, /// Whether the condition has been consumed by the optimizer in some way, and it should not be evaluated /// in the normal place where WHERE terms are evaluated. /// A term may have been consumed e.g. if: /// - it has been converted into a constraint in a seek key /// - it has been removed due to being trivially true or false pub consumed: bool, } impl WhereTerm { pub fn should_eval_before_loop( &self, join_order: &[JoinOrderMember], subqueries: &[NonFromClauseSubquery], table_references: Option<&TableReferences>, ) -> bool { if self.consumed { return false; } let Ok(eval_at) = self.eval_at(join_order, subqueries, table_references) else { return false; }; eval_at == EvalAt::BeforeLoop } pub fn should_eval_at_loop( &self, loop_idx: usize, join_order: &[JoinOrderMember], subqueries: &[NonFromClauseSubquery], table_references: Option<&TableReferences>, ) -> bool { if self.consumed { return false; } let Ok(eval_at) = self.eval_at(join_order, subqueries, table_references) else { return false; }; eval_at == EvalAt::Loop(loop_idx) } fn eval_at( &self, join_order: &[JoinOrderMember], subqueries: &[NonFromClauseSubquery], table_references: Option<&TableReferences>, ) -> Result { determine_where_to_eval_term(self, join_order, subqueries, table_references) } } impl From for WhereTerm { fn from(value: Expr) -> Self { Self { expr: value, from_outer_join: None, consumed: false, } } } use crate::ast::Expr; use crate::util::exprs_are_equivalent; /// The loop index where to evaluate the condition. /// For example, in `SELECT * FROM u JOIN p WHERE u.id = 5`, the condition can already be evaluated at the first loop (idx 0), /// because that is the rightmost table that it references. /// /// Conditions like 1=2 can be evaluated before the main loop is opened, because they are constant. /// In theory we should be able to statically analyze them all and reduce them to a single boolean value, /// but that is not implemented yet. #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub enum EvalAt { Loop(usize), BeforeLoop, } #[allow(clippy::non_canonical_partial_ord_impl)] impl PartialOrd for EvalAt { fn partial_cmp(&self, other: &Self) -> Option { match (self, other) { (EvalAt::Loop(a), EvalAt::Loop(b)) => a.partial_cmp(b), (EvalAt::BeforeLoop, EvalAt::BeforeLoop) => Some(Ordering::Equal), (EvalAt::BeforeLoop, _) => Some(Ordering::Less), (_, EvalAt::BeforeLoop) => Some(Ordering::Greater), } } } impl Ord for EvalAt { fn cmp(&self, other: &Self) -> Ordering { self.partial_cmp(other) .expect("total ordering not implemented for EvalAt") } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SubqueryEvalPhase { BeforeLoop, Loop(usize), GroupedOutput, UngroupedAggregateOutput, WindowOutput, PreWrite, PostWriteReturning, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SubqueryOrigin { SelectList, SelectWhere, SelectGroupBy, SelectHaving, SelectOrderBy, SelectLimitOffset, DmlWhere, DmlSet, DmlReturning, TriggerWhen, } impl SubqueryOrigin { pub fn phase_floor(self) -> SubqueryEvalPhase { match self { SubqueryOrigin::SelectList | SubqueryOrigin::SelectWhere | SubqueryOrigin::SelectGroupBy | SubqueryOrigin::SelectHaving | SubqueryOrigin::SelectOrderBy | SubqueryOrigin::SelectLimitOffset | SubqueryOrigin::TriggerWhen => SubqueryEvalPhase::BeforeLoop, SubqueryOrigin::DmlWhere => SubqueryEvalPhase::BeforeLoop, SubqueryOrigin::DmlSet => SubqueryEvalPhase::PreWrite, SubqueryOrigin::DmlReturning => SubqueryEvalPhase::PostWriteReturning, } } pub fn is_post_write_returning(self) -> bool { matches!(self, SubqueryOrigin::DmlReturning) } } /// A query plan is either a SELECT or a DELETE (for now) #[derive(Debug, Clone)] pub enum Plan { Select(SelectPlan), CompoundSelect { left: Vec<(SelectPlan, ast::CompoundOperator)>, right_most: SelectPlan, limit: Option>, offset: Option>, order_by: Option>, }, Delete(DeletePlan), Update(UpdatePlan), } impl Plan { /// Returns true if this SELECT plan contains a reference to the given table. /// For compound selects, checks all component selects. /// Returns false for Delete/Update plans. pub fn select_contains_table(&self, table: &Table) -> bool { match self { Plan::Select(select_plan) => select_plan.table_references.contains_table(table), Plan::CompoundSelect { left, right_most, .. } => { right_most.table_references.contains_table(table) || left .iter() .any(|(plan, _)| plan.table_references.contains_table(table)) } Plan::Delete(_) | Plan::Update(_) => false, } } /// Returns the query destination for Select/CompoundSelect plans. /// Returns None for Delete/Update plans. pub fn select_query_destination(&self) -> Option<&QueryDestination> { match self { Plan::Select(select_plan) => Some(&select_plan.query_destination), Plan::CompoundSelect { right_most, .. } => Some(&right_most.query_destination), Plan::Delete(_) | Plan::Update(_) => None, } } /// Returns a mutable reference to the query destination for Select/CompoundSelect plans. /// Returns None for Delete/Update plans. pub fn select_query_destination_mut(&mut self) -> Option<&mut QueryDestination> { match self { Plan::Select(select_plan) => Some(&mut select_plan.query_destination), Plan::CompoundSelect { right_most, .. } => Some(&mut right_most.query_destination), Plan::Delete(_) | Plan::Update(_) => None, } } /// Returns the result columns for Select/CompoundSelect plans. /// Returns None for Delete/Update plans. pub fn select_result_columns(&self) -> Option<&[ResultSetColumn]> { match self { Plan::Select(select_plan) => Some(&select_plan.result_columns), Plan::CompoundSelect { right_most, .. } => Some(&right_most.result_columns), Plan::Delete(_) | Plan::Update(_) => None, } } /// Returns the table references for Select/CompoundSelect plans. /// Returns None for Delete/Update plans. pub fn select_table_references(&self) -> Option<&TableReferences> { match self { Plan::Select(select_plan) => Some(&select_plan.table_references), Plan::CompoundSelect { right_most, .. } => Some(&right_most.table_references), Plan::Delete(_) | Plan::Update(_) => None, } } /// Returns true if this plan or any of its subplans read from the given table. /// (Not for Delete/Update plans) fn reads_table(&self, database_id: usize, table_name: &str) -> bool { match self { Plan::Select(select_plan) => select_plan.reads_table(database_id, table_name), Plan::CompoundSelect { left, right_most, .. } => { left.iter() .any(|(select_plan, _)| select_plan.reads_table(database_id, table_name)) || right_most.reads_table(database_id, table_name) } Plan::Delete(_) | Plan::Update(_) => false, } } } /// The destination of the results of a query. /// Typically, the results of a query are returned to the caller. /// However, there are some cases where the results are not returned to the caller, /// but rather are yielded to a parent query via coroutine, or stored in a temp table, /// later used by the parent query. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EphemeralRowidMode { /// The last result column is used as the rowid key. FromResultColumns, /// Generate a fresh rowid for each inserted row. Auto, } #[derive(Debug, Clone)] pub enum QueryDestination { /// The results of the query are returned to the caller. ResultRows, /// The results of the query are yielded to a parent query via coroutine. CoroutineYield { /// The register that holds the program offset that handles jumping to/from the coroutine. yield_reg: usize, /// The index of the first instruction in the bytecode that implements the coroutine. coroutine_implementation_start: BranchOffset, }, /// The results of the query are stored in an ephemeral index, /// later used by the parent query. EphemeralIndex { /// The cursor ID of the ephemeral index that will be used to store the results. cursor_id: CursorID, /// The index that will be used to store the results. index: Arc, /// Optional MakeRecord affinity string to apply before inserting keys. /// For `IN (SELECT ...)` this must match the left-hand side expression affinity. affinity_str: Option>, /// Whether this is a delete operation that will remove the index entries is_delete: bool, }, /// The results of the query are stored in an ephemeral table, /// later used by the parent query. EphemeralTable { /// The cursor ID of the ephemeral table that will be used to store the results. cursor_id: CursorID, /// The table that will be used to store the results. table: Arc, /// How to determine the rowid key for inserts. rowid_mode: EphemeralRowidMode, }, /// The result of an EXISTS subquery are stored in a single register. ExistsSubqueryResult { /// The register that holds the result of the EXISTS subquery. result_reg: usize, }, /// The results of a subquery that is neither 'EXISTS' nor 'IN' are stored in a range of registers. RowValueSubqueryResult { /// The start register of the range that holds the result of the subquery. result_reg_start: usize, /// The number of registers that hold the result of the subquery. num_regs: usize, }, /// The results of the query are stored in a RowSet (for DELETE operations with triggers). /// Rowids are added to the RowSet using RowSetAdd, then read back using RowSetRead. RowSet { /// The register that holds the RowSet object. rowset_reg: usize, }, /// Decision made at some point after query plan construction. Unset, } impl QueryDestination { pub fn placeholder_for_subquery() -> Self { QueryDestination::CoroutineYield { yield_reg: usize::MAX, // will be set later in bytecode emission coroutine_implementation_start: BranchOffset::Placeholder, // will be set later in bytecode emission } } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct JoinOrderMember { /// The internal ID of the[TableReference] pub table_id: TableInternalId, /// The index of the table in the original join order. /// This is used to index into e.g. [TableReferences::joined_tables()] pub original_idx: usize, /// Whether this member is the right side of an OUTER JOIN pub is_outer: bool, } #[derive(Debug, Clone, PartialEq)] /// Whether a column is DISTINCT or not. pub enum Distinctness { /// The column is not a DISTINCT column. NonDistinct, /// The column is a DISTINCT column, /// and includes a translation context for handling duplicates. Distinct { ctx: Option }, } impl Distinctness { pub fn from_ast(distinctness: Option<&ast::Distinctness>) -> Self { match distinctness { Some(ast::Distinctness::Distinct) => Self::Distinct { ctx: None }, Some(ast::Distinctness::All) => Self::NonDistinct, None => Self::NonDistinct, } } pub fn is_distinct(&self) -> bool { matches!(self, Distinctness::Distinct { .. }) } } /// Translation context for handling DISTINCT columns. #[derive(Debug, Clone, PartialEq)] pub struct DistinctCtx { /// Hash table id used to deduplicate results. pub hash_table_id: usize, /// Collations for each distinct key column. pub collations: Vec, /// The label for the on conflict branch. /// When a duplicate is found, the program will jump to the offset this label points to. pub label_on_conflict: BranchOffset, } impl DistinctCtx { pub fn emit_deduplication_insns( &self, program: &mut ProgramBuilder, num_regs: usize, start_reg: usize, ) { program.emit_insn(Insn::HashDistinct { data: Box::new(HashDistinctData { hash_table_id: self.hash_table_id, key_start_reg: start_reg, num_keys: num_regs, collations: self.collations.clone(), target_pc: self.label_on_conflict, }), }); } } /// Detected simple-aggregate optimization. /// /// Analogous to SQLite's `isSimpleCount()` / `minMaxQuery()`. When set on a /// `SelectPlan`, the emitter can use a specialised fast path instead of a full /// scan + accumulate loop. #[derive(Debug, Clone)] pub struct MinMaxDef { pub func: AggFunc, pub argument: ast::Expr, pub order: SortOrder, /// Explicit COLLATE override, if any. `None` means use the column default. pub collation: Option, } #[derive(Debug, Clone)] pub enum SimpleAggregate { /// `SELECT count(*) FROM ` — uses the `Insn::Count` opcode directly. Count, /// `SELECT min(expr) FROM …` or `SELECT max(expr) FROM …` — the optimizer /// will pick an index that delivers rows in the right order so the emitter /// only needs to read the first (non-NULL for MIN) row. MinMax(Box), } #[derive(Debug, Clone)] pub struct SelectPlan { pub table_references: TableReferences, /// The order in which the tables are joined. Tables have usize Ids (their index in joined_tables) pub join_order: Vec, /// the columns inside SELECT ... FROM pub result_columns: Vec, /// where clause split into a vec at 'AND' boundaries. all join conditions also get shoved in here, /// and we keep track of which join they came from (mainly for OUTER JOIN processing) pub where_clause: Vec, /// group by clause pub group_by: Option, /// order by clause pub order_by: Vec<(Box, SortOrder)>, /// all the aggregates collected from the result columns, order by, and (TODO) having clauses pub aggregates: Vec, /// limit clause pub limit: Option>, /// offset clause pub offset: Option>, /// query contains a constant condition that is always false pub contains_constant_false_condition: bool, /// the destination of the resulting rows from this plan. pub query_destination: QueryDestination, /// whether the query is DISTINCT pub distinctness: Distinctness, /// values: https://sqlite.org/syntax/select-core.html pub values: Vec>, /// The window definition and all window functions associated with it. There is at most one /// window per SELECT. If the original query contains more, they are pushed down into subqueries. pub window: Option, /// Subqueries that appear in any part of the query apart from the FROM clause pub non_from_clause_subqueries: Vec, /// Estimated number of times this SELECT will be invoked by its parent scope. /// /// Top-level queries and standalone FROM-subqueries default to 1. Correlated /// non-FROM subqueries may be re-optimized after their parent join order is /// known so their inner FROM-subqueries can cost repeated probes correctly. pub input_cardinality_hint: Option, /// Estimated output rows from the optimizer's join order computation. /// Used to propagate cardinality estimates for CTE/subquery tables. pub estimated_output_rows: Option, /// When set, this query is a simple aggregate (COUNT(*), MIN, or MAX) /// that can be satisfied without a full table scan. pub simple_aggregate: Option, } impl SelectPlan { pub fn joined_tables(&self) -> &[JoinedTable] { self.table_references.joined_tables() } pub fn agg_args_count(&self) -> usize { self.aggregates.iter().map(|agg| agg.args.len()).sum() } /// Whether this query or any of its subqueries reference columns from the outer query. pub fn is_correlated(&self) -> bool { self.table_references .outer_query_refs() .iter() .any(|t| t.is_used()) || self.non_from_clause_subqueries.iter().any(|s| s.correlated) || self .table_references .joined_tables() .iter() .any(|t| match &t.table { Table::FromClauseSubquery(subquery) => plan_is_correlated(&subquery.plan), _ => false, }) } fn reads_table(&self, database_id: usize, table_name: &str) -> bool { self.table_references.joined_tables().iter().any(|table| { table.matches(database_id, table_name) || match &table.table { Table::FromClauseSubquery(subquery) => { subquery.plan.reads_table(database_id, table_name) } Table::BTree(_) | Table::Virtual(_) => false, } }) || self .non_from_clause_subqueries .iter() .any(|subquery| subquery.reads_table(database_id, table_name)) } } /// Why an UPDATE/DELETE must gather target rowids first, then apply writes. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DmlSafetyReason { /// Triggers exist, so we lock in target rows before writing. Trigger, /// WHERE has a subquery, so we lock in target rows before writing. SubqueryInWhere, /// The plan reads rowids from multiple index branches (multi-index scan). MultiIndexScan, /// REPLACE may delete conflicting rows while we are scanning. ReplaceMode, /// The statement updates key columns used by the scan itself. KeyMutation, /// The index method cursor does not materialize results up front, /// so writes could invalidate the live iterator. IndexMethodNotMaterialized, } /// Safety decisions made while planning UPDATE/DELETE. #[derive(Debug, Clone, Default)] pub struct DmlSafety { /// Why the safer "collect first, write later" mode was enabled. pub reasons: SmallVec<[DmlSafetyReason; 2]>, } impl DmlSafety { pub fn requires_stable_write_set(&self) -> bool { !self.reasons.is_empty() } pub fn require(&mut self, reason: DmlSafetyReason) { if !self.reasons.contains(&reason) { self.reasons.push(reason); } } } #[allow(dead_code)] #[derive(Debug, Clone)] pub struct DeletePlan { pub table_references: TableReferences, /// the columns inside SELECT ... FROM pub result_columns: Vec, /// where clause split into a vec at 'AND' boundaries. pub where_clause: Vec, /// order by clause pub order_by: Vec<(Box, SortOrder)>, /// limit clause pub limit: Option>, /// offset clause pub offset: Option>, /// query contains a constant condition that is always false pub contains_constant_false_condition: bool, /// Indexes that must be updated by the delete operation. pub indexes: Vec>, /// When DELETE cannot safely write while scanning, we first collect rowids into a RowSet. pub rowset_plan: Option, /// Register ID for the RowSet (if rowset_plan is Some) pub rowset_reg: Option, /// Subqueries that appear in the WHERE clause (for non-rowset path) pub non_from_clause_subqueries: Vec, /// Whether this DELETE plan uses the safer pre-materialization path, and why. pub safety: DmlSafety, } #[derive(Debug, Clone)] pub struct UpdatePlan { pub table_references: TableReferences, /// Conflict resolution strategy (e.g., OR IGNORE, OR REPLACE) pub or_conflict: Option, // (column index, new value) pairs pub set_clauses: Vec<(usize, Box)>, pub where_clause: Vec, pub order_by: Vec<(Box, SortOrder)>, pub limit: Option>, pub offset: Option>, // TODO: optional RETURNING clause pub returning: Option>, // whether the WHERE clause is always false pub contains_constant_false_condition: bool, pub indexes_to_update: Vec>, // If the UPDATE modifies any column that is present in the key of the btree used to iterate over the table (either the table itself or an index), // gather all the target rowids into an ephemeral table, and then use that table as the single JoinedTable for the actual UPDATE loop. // This ensures the keys of the btree used to iterate cannot be changed during the UPDATE loop itself, ensuring all the intended rows actually get // updated and none are skipped. pub ephemeral_plan: Option, // For ALTER TABLE turso-db emits appropriate DDL statement in the "updates" cell of CDC table // This field is present only for update plan created for ALTER TABLE when CDC mode has "updates" values pub cdc_update_alter_statement: Option, /// Subqueries that appear in the WHERE clause (for non-ephemeral path) pub non_from_clause_subqueries: Vec, /// Whether this UPDATE plan uses the safer pre-materialization path, and why. pub safety: DmlSafety, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum IterationDirection { Forwards, Backwards, } pub fn select_star( tables: &[JoinedTable], out_columns: &mut Vec, right_join_swapped: bool, ) { // RIGHT JOIN swapped tables; iterate in reverse to restore original column order. let table_iter: Vec<&JoinedTable> = if right_join_swapped { tables.iter().rev().collect() } else { tables.iter().collect() }; for table in table_iter { // Semi/anti-join tables are internal (from EXISTS/NOT EXISTS unnesting) // and should not contribute columns to SELECT *. if table .join_info .as_ref() .is_some_and(|ji| ji.is_semi_or_anti()) { continue; } out_columns.extend( table .columns() .iter() .enumerate() .filter(|(_, col)| !col.hidden()) .filter(|(_, col)| { // If we are joining with USING, we need to deduplicate the columns from the right table // that are also present in the USING clause. if let Some(join_info) = &table.join_info { !join_info.using.iter().any(|using_col| { col.name .as_ref() .is_some_and(|name| name.eq_ignore_ascii_case(using_col.as_str())) }) } else { true } }) .map(|(i, col)| ResultSetColumn { alias: None, expr: ast::Expr::Column { database: None, table: table.internal_id, column: i, is_rowid_alias: col.is_rowid_alias(), }, contains_aggregates: false, }), ); } } /// The type of join between two tables. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum JoinType { Inner, LeftOuter, FullOuter, /// Semi-join: keep outer row if inner match found (EXISTS). Semi, /// Anti-join: keep outer row if NO inner match found (NOT EXISTS). Anti, } /// Join information for a table reference. #[derive(Debug, Clone)] pub struct JoinInfo { /// The type of join. pub join_type: JoinType, /// The USING clause for the join, if any. NATURAL JOIN is transformed into USING (col1, col2, ...). pub using: Vec, /// When true, the optimizer must not reorder this table relative to its /// neighbors. Set for CROSS JOIN to match SQLite semantics. pub no_reorder: bool, } impl JoinInfo { /// Whether this is an OUTER JOIN (LEFT OUTER or FULL OUTER). pub fn is_outer(&self) -> bool { matches!(self.join_type, JoinType::LeftOuter | JoinType::FullOuter) } /// Whether this is a FULL OUTER JOIN. pub fn is_full_outer(&self) -> bool { self.join_type == JoinType::FullOuter } /// Whether this is a semi-join (EXISTS). pub fn is_semi(&self) -> bool { self.join_type == JoinType::Semi } /// Whether this is an anti-join (NOT EXISTS). pub fn is_anti(&self) -> bool { self.join_type == JoinType::Anti } /// Whether this is a semi-join or anti-join (EXISTS/NOT EXISTS). pub fn is_semi_or_anti(&self) -> bool { matches!(self.join_type, JoinType::Semi | JoinType::Anti) } /// Whether the optimizer must preserve this table's position in the join order. pub fn is_ordering_constrained(&self) -> bool { self.is_outer() || self.is_semi_or_anti() || self.no_reorder } } /// A joined table in the query plan. /// For example, /// ```sql /// SELECT * FROM users u JOIN products p JOIN (SELECT * FROM users) sub; /// ``` /// has three table references where /// - all have [Operation::Scan] /// - identifiers are `t`, `p`, `sub` /// - `t` and `p` are [Table::BTree] while `sub` is [Table::FromClauseSubquery] /// - join_info is None for the first table reference, and Some(JoinInfo { join_type: JoinType::Inner, using: vec![] }) for the second and third table references #[derive(Debug, Clone)] pub struct JoinedTable { /// The operation that this table reference performs. pub op: Operation, /// Table object, which contains metadata about the table, e.g. columns. pub table: Table, /// The name of the table as referred to in the query, either the literal name or an alias e.g. "users" or "u" pub identifier: String, /// Internal ID of the table reference, used in e.g. [Expr::Column] to refer to this table. pub internal_id: TableInternalId, /// The join info for this table reference, if it is the right side of a join (which all except the first table reference have) pub join_info: Option, /// Bitmask of columns that are referenced in the query. /// Used to decide whether a covering index can be used. pub col_used_mask: ColumnUsedMask, /// Count of how many times each column is referenced. /// /// Expression indexes can satisfy a column requirement if the column is /// only used to build the expression itself. Tracking counts lets us /// subtract a column from the covering set only when every usage is /// accounted for by an expression index. pub column_use_counts: Vec, /// Expressions referencing this table that may be satisfied by an expression index. /// /// Each entry stores the normalized expression text and the columns it /// needs. During covering checks we ask: does an index contain this /// expression? If yes, all columns that *only* feed this expression can be /// removed from the required-column set. pub expression_index_usages: Vec, /// The index of the database. "main" is always zero. pub database_id: usize, /// INDEXED BY / NOT INDEXED hint from the SQL statement. pub indexed: Option, } #[derive(Debug, Clone)] pub struct OuterQueryReference { /// The name of the table as referred to in the query, either the literal name or an alias e.g. "users" or "u" pub identifier: String, /// Internal ID of the table reference, used in e.g. [Expr::Column] to refer to this table. pub internal_id: TableInternalId, /// Table object, which contains metadata about the table, e.g. columns. pub table: Table, /// Bitmask of columns that are referenced in the query. /// Used to track dependencies, so that it can be resolved /// when a WHERE clause subquery should be evaluated; /// i.e., if the subquery depends on tables T and U, /// then both T and U need to be in scope for the subquery to be evaluated. pub col_used_mask: ColumnUsedMask, /// Original CTE SELECT AST for re-planning. When a CTE is referenced /// multiple times, each reference needs a fresh plan with unique /// internal_ids to avoid cursor key collisions. pub cte_select: Option, /// Explicit column names from WITH t(a, b) AS (...) syntax. pub cte_explicit_columns: Vec, /// CTE ID if this is a CTE reference. Used to track CTE reference counts /// for materialization decisions. pub cte_id: Option, /// When true, this entry is only for CTE definition lookup in subquery /// FROM clauses, not for column resolution. This is set when the CTE /// has been consumed by a FROM clause (with or without an alias), so /// column resolution goes through the joined_table instead. pub cte_definition_only: bool, /// Whether the rowid of this table is referenced. Tracked separately from /// col_used_mask because rowid is not a real column and setting a fake /// column index in col_used_mask could mislead covering index decisions. pub rowid_referenced: bool, } impl OuterQueryReference { /// Returns the columns of the table that this outer query reference refers to. pub fn columns(&self) -> &[Column] { self.table.columns() } /// Marks a column as used; used means that the column is referenced in the query. pub fn mark_column_used(&mut self, column_index: usize) { self.col_used_mask.set(column_index); } /// Whether the OuterQueryReference is used by the current query scope. /// This is used primarily to determine at what loop depth a subquery should be evaluated. pub fn is_used(&self) -> bool { !self.col_used_mask.is_empty() || self.rowid_referenced } } #[derive(Debug, Clone)] /// A collection of table references in a given SQL statement. /// /// `TableReferences::joined_tables` is the list of tables that are joined together. /// Example: SELECT * FROM t JOIN u JOIN v -- the joined tables are t, u and v. /// /// `TableReferences::outer_query_refs` are references to tables outside the current scope. /// Example: SELECT * FROM t WHERE EXISTS (SELECT * FROM u WHERE u.foo = t.foo) /// -- here, 'u' is an outer query reference for the subquery (SELECT * FROM u WHERE u.foo = t.foo), /// since that query does not declare 't' in its FROM clause. /// /// /// Typically a query will only have joined tables, but the following may have outer query references: /// - CTEs that refer to other preceding CTEs /// - Correlated subqueries, i.e. subqueries that depend on the outer scope pub struct TableReferences { /// Tables that are joined together in this query scope. joined_tables: Vec, /// Tables from outer scopes that are referenced in this query scope. outer_query_refs: Vec, /// Set when a RIGHT JOIN is rewritten as LEFT JOIN by swapping the two tables, /// so `select_star` emits columns in the original user-visible order. right_join_swapped: bool, } impl Default for TableReferences { fn default() -> Self { Self::new_empty() } } impl TableReferences { /// The maximum number of tables that can be joined together in a query. /// This limit is arbitrary, although we currently use a u128 to represent the [crate::translate::planner::TableMask], /// which can represent up to 128 tables. /// Even at 63 tables we currently cannot handle the optimization performantly, hence the arbitrary cap. pub const MAX_JOINED_TABLES: usize = 63; pub fn new( joined_tables: Vec, outer_query_refs: Vec, ) -> Self { Self { joined_tables, outer_query_refs, right_join_swapped: false, } } pub fn new_empty() -> Self { Self { joined_tables: Vec::new(), outer_query_refs: Vec::new(), right_join_swapped: false, } } pub fn is_empty(&self) -> bool { self.joined_tables.is_empty() && self.outer_query_refs.is_empty() } /// Mark that tables were swapped for a RIGHT-to-LEFT JOIN rewrite. pub fn set_right_join_swapped(&mut self) { self.right_join_swapped = true; } /// Whether tables were swapped for a RIGHT JOIN rewrite. pub fn right_join_swapped(&self) -> bool { self.right_join_swapped } /// Add a new [JoinedTable] to the query plan. pub fn add_joined_table(&mut self, joined_table: JoinedTable) { self.joined_tables.push(joined_table); } /// Add a new [OuterQueryReference] to the query plan. pub fn add_outer_query_reference(&mut self, outer_query_reference: OuterQueryReference) { self.outer_query_refs.push(outer_query_reference); } /// Returns an immutable reference to the [JoinedTable]s in the query plan. pub fn joined_tables(&self) -> &[JoinedTable] { &self.joined_tables } /// Returns a mutable reference to the [JoinedTable]s in the query plan. pub fn joined_tables_mut(&mut self) -> &mut Vec { &mut self.joined_tables } /// Resets the expression index usages for all joined tables. pub fn reset_expression_index_usages(&mut self) { for table in self.joined_tables.iter_mut() { table.clear_expression_index_usages(); } } /// Called before optimization so we can reuse the same registration /// for result columns, ORDER BY, and GROUP BY expressions. If a /// SELECT lists `LOWER(name)` and an index exists on `LOWER(name)`, we /// can plan a covering scan because the expression value lives inside /// the index key. pub fn register_expression_index_usage(&mut self, expr: &ast::Expr) { let Some((table_id, columns_mask)) = single_table_column_usage(expr) else { return; }; let Some(table_ref) = self .joined_tables() .iter() .find(|t| t.internal_id == table_id) else { return; }; let normalized = normalize_expr_for_index_matching(expr, table_ref, self); if let Some(table_ref_mut) = self .joined_tables_mut() .iter_mut() .find(|t| t.internal_id == table_id) { table_ref_mut.register_expression_index_usage(normalized, columns_mask); } } /// Returns an immutable reference to the [OuterQueryReference]s in the query plan. pub fn outer_query_refs(&self) -> &[OuterQueryReference] { &self.outer_query_refs } /// Returns an immutable reference to the [OuterQueryReference] with the given internal ID. pub fn find_outer_query_ref_by_internal_id( &self, internal_id: TableInternalId, ) -> Option<&OuterQueryReference> { self.outer_query_refs .iter() .find(|t| t.internal_id == internal_id) } /// Returns a mutable reference to the [OuterQueryReference] with the given internal ID. pub fn find_outer_query_ref_by_internal_id_mut( &mut self, internal_id: TableInternalId, ) -> Option<&mut OuterQueryReference> { self.outer_query_refs .iter_mut() .find(|t| t.internal_id == internal_id) } /// Returns an immutable reference to the [Table] with the given internal ID, /// plus a boolean indicating whether the table is a joined table from the current query scope (false), /// or an outer query reference (true). pub fn find_table_by_internal_id( &self, internal_id: TableInternalId, ) -> Option<(bool, &Table)> { self.joined_tables .iter() .find(|t| t.internal_id == internal_id) .map(|t| (false, &t.table)) .or_else(|| { self.outer_query_refs .iter() .find(|t| t.internal_id == internal_id) .map(|t| (true, &t.table)) }) } /// Returns an immutable reference to the [Table] with the given identifier, /// where identifier is either the literal name of the table or an alias. pub fn find_table_by_identifier(&self, identifier: &str) -> Option<&Table> { self.joined_tables .iter() .find(|t| t.identifier == identifier) .map(|t| &t.table) .or_else(|| { self.outer_query_refs .iter() .find(|t| t.identifier == identifier) .map(|t| &t.table) }) } /// Returns an immutable reference to the first [Table] whose underlying /// table name matches `name`. Unlike [find_table_by_identifier], this /// searches by the base table name (e.g. "t1") rather than the alias /// (e.g. "a"). This is needed when looking up column metadata for /// ephemeral auto-indexes, whose `table_name` field stores the base name /// while the table reference may be aliased. pub fn find_table_by_table_name(&self, name: &str) -> Option<&Table> { self.joined_tables .iter() .find(|t| t.table.get_name() == name) .map(|t| &t.table) .or_else(|| { self.outer_query_refs .iter() .find(|t| t.table.get_name() == name) .map(|t| &t.table) }) } /// Returns an immutable reference to the [OuterQueryReference] with the given identifier, /// where identifier is either the literal name of the table or an alias. pub fn find_outer_query_ref_by_identifier( &self, identifier: &str, ) -> Option<&OuterQueryReference> { self.outer_query_refs .iter() .find(|t| t.identifier == identifier) } /// Marks the pre-planned [OuterQueryReference] with the given identifier as /// "CTE definition only". This prevents it from being used for column /// resolution while still allowing CTE definition lookup in subquery FROM /// clauses. Called when a CTE is consumed by a FROM clause, since column /// resolution is then handled by the joined_table entry instead. pub fn mark_outer_query_ref_cte_definition_only(&mut self, identifier: &str) { if let Some(outer_ref) = self .outer_query_refs .iter_mut() .find(|t| t.identifier == identifier) { outer_ref.cte_definition_only = true; } } /// Returns the internal ID and immutable reference to the [Table] with the given identifier, pub fn find_table_and_internal_id_by_identifier( &self, identifier: &str, ) -> Option<(TableInternalId, &Table)> { self.joined_tables .iter() .find(|t| t.identifier == identifier) .map(|t| (t.internal_id, &t.table)) .or_else(|| { self.outer_query_refs .iter() .find(|t| t.identifier == identifier && !t.cte_definition_only) .map(|t| (t.internal_id, &t.table)) }) } /// Returns an immutable reference to the [JoinedTable] with the given internal ID. pub fn find_joined_table_by_internal_id( &self, internal_id: TableInternalId, ) -> Option<&JoinedTable> { self.joined_tables .iter() .find(|t| t.internal_id == internal_id) } /// Returns a mutable reference to the [JoinedTable] with the given internal ID. pub fn find_joined_table_by_internal_id_mut( &mut self, internal_id: TableInternalId, ) -> Option<&mut JoinedTable> { self.joined_tables .iter_mut() .find(|t| t.internal_id == internal_id) } /// Marks a column as used; used means that the column is referenced in the query. pub fn mark_column_used(&mut self, internal_id: TableInternalId, column_index: usize) { if let Some(joined_table) = self.find_joined_table_by_internal_id_mut(internal_id) { joined_table.mark_column_used(column_index); } else if let Some(outer_query_ref) = self.find_outer_query_ref_by_internal_id_mut(internal_id) { outer_query_ref.mark_column_used(column_index); } else { panic!("table with internal id {internal_id} not found in table references"); } } /// Marks the rowid of a table as referenced. This is tracked separately /// from column usage because rowid is not a real column. pub fn mark_rowid_referenced(&mut self, internal_id: TableInternalId) { if let Some(outer_query_ref) = self.find_outer_query_ref_by_internal_id_mut(internal_id) { outer_query_ref.rowid_referenced = true; } // For joined tables, rowid references don't need special tracking // since correlated subquery detection only looks at outer_query_refs. } pub fn contains_table(&self, table: &Table) -> bool { self.joined_tables .iter() .map(|t| &t.table) .chain(self.outer_query_refs.iter().map(|t| &t.table)) .any(|t| match t { Table::FromClauseSubquery(subquery_table) => { subquery_table.plan.select_contains_table(table) } _ => t == table, }) } pub fn extend(&mut self, other: TableReferences) { self.joined_tables.extend(other.joined_tables); self.outer_query_refs.extend(other.outer_query_refs); } } /// Tracks which columns are used in a query. Optimized for the common case /// of ≤64 columns (single u64), with heap-allocated overflow #[derive(Clone, Debug, Default, PartialEq)] pub struct ColumnUsedMask { inline: u64, overflow: Option>, } impl ColumnUsedMask { const INLINE_BITS: usize = 64; pub fn set(&mut self, index: usize) { if index < Self::INLINE_BITS { self.inline |= 1 << index; } else { let overflow_idx = (index - Self::INLINE_BITS) / 64; let bit = (index - Self::INLINE_BITS) % 64; let overflow = self.overflow.get_or_insert_with(Vec::new); if overflow_idx >= overflow.len() { overflow.resize(overflow_idx + 1, 0); } overflow[overflow_idx] |= 1 << bit; } } pub fn get(&self, index: usize) -> bool { if index < Self::INLINE_BITS { (self.inline >> index) & 1 != 0 } else { let Some(overflow) = &self.overflow else { return false; }; let overflow_idx = (index - Self::INLINE_BITS) / 64; let bit = (index - Self::INLINE_BITS) % 64; overflow .get(overflow_idx) .is_some_and(|word| (word >> bit) & 1 != 0) } } pub fn clear(&mut self, index: usize) { if index < Self::INLINE_BITS { self.inline &= !(1 << index); } else if let Some(overflow) = &mut self.overflow { let overflow_idx = (index - Self::INLINE_BITS) / 64; let bit = (index - Self::INLINE_BITS) % 64; if let Some(word) = overflow.get_mut(overflow_idx) { *word &= !(1 << bit); } } } pub fn contains_all_set_bits_of(&self, other: &Self) -> bool { if (self.inline & other.inline) != other.inline { return false; } match (&self.overflow, &other.overflow) { (None, None) => true, (None, Some(other_ov)) => other_ov.iter().all(|&w| w == 0), (Some(_), None) => true, (Some(self_ov), Some(other_ov)) => other_ov.iter().enumerate().all(|(i, &other_w)| { let self_w = self_ov.get(i).copied().unwrap_or(0); (self_w & other_w) == other_w }), } } pub fn is_empty(&self) -> bool { self.inline == 0 && self .overflow .as_ref() .is_none_or(|ov| ov.iter().all(|&w| w == 0)) } pub fn is_only(&self, index: usize) -> bool { if index < Self::INLINE_BITS { self.inline == (1 << index) && self .overflow .as_ref() .is_none_or(|ov| ov.iter().all(|&w| w == 0)) } else { if self.inline != 0 { return false; } let Some(overflow) = &self.overflow else { return false; }; let overflow_idx = (index - Self::INLINE_BITS) / 64; let bit = (index - Self::INLINE_BITS) % 64; // The overflow vector must be long enough to contain the target index if overflow_idx >= overflow.len() { return false; } overflow.iter().enumerate().all(|(i, &w)| { if i == overflow_idx { w == (1 << bit) } else { w == 0 } }) } } pub fn subtract(&mut self, other: &Self) { self.inline &= !other.inline; if let (Some(self_ov), Some(other_ov)) = (&mut self.overflow, &other.overflow) { for (i, other_w) in other_ov.iter().enumerate() { if let Some(self_w) = self_ov.get_mut(i) { *self_w &= !other_w; } } } } pub fn iter(&self) -> impl Iterator + '_ { let inline_iter = (0..Self::INLINE_BITS).filter(|&i| (self.inline >> i) & 1 != 0); let overflow_iter = self .overflow .iter() .flat_map(|ov| ov.iter().enumerate()) .flat_map(|(word_idx, &word)| { (0..64).filter_map(move |bit| { if (word >> bit) & 1 != 0 { Some(Self::INLINE_BITS + word_idx * 64 + bit) } else { None } }) }); inline_iter.chain(overflow_iter) } } impl std::ops::BitOrAssign<&Self> for ColumnUsedMask { fn bitor_assign(&mut self, rhs: &Self) { self.inline |= rhs.inline; if let Some(rhs_ov) = &rhs.overflow { let self_ov = self.overflow.get_or_insert_with(Vec::new); if self_ov.len() < rhs_ov.len() { self_ov.resize(rhs_ov.len(), 0); } for (i, &rhs_w) in rhs_ov.iter().enumerate() { self_ov[i] |= rhs_w; } } } } #[derive(Clone, Debug)] pub struct ExpressionIndexUsage { /// Normalized (non-bound) ast of the expression as stored on an index column. /// Example: `lower(name)` for INDEX ON t(lower(name)). pub normalized_expr: Box, /// Columns required to compute the expression. Helps decide whether using /// the expression value from the index fully covers those column reads. pub columns_mask: ColumnUsedMask, } /// Represents one key pair in a hash join equality condition. /// For `expr1 = expr2`, this tracks which WHERE term contains the equality /// and which side of the equality belongs to the build table. #[derive(Debug, Clone, Copy)] pub struct HashJoinKey { /// Index into the where_clause vector pub where_clause_idx: usize, /// Which side of the binary equality expression belongs to the build table. /// The other side belongs to the probe table. pub build_side: BinaryExprSide, } impl HashJoinKey { /// Get the build table's expression from the WHERE clause. pub fn get_build_expr<'a>(&self, where_clause: &'a [WhereTerm]) -> &'a ast::Expr { let where_term = &where_clause[self.where_clause_idx]; let Ok(Some((lhs, _, rhs))) = as_binary_components(&where_term.expr) else { panic!("HashJoinKey: expected a valid binary expression"); }; if self.build_side == BinaryExprSide::Lhs { lhs } else { rhs } } /// Get the probe table's expression from the WHERE clause. pub fn get_probe_expr<'a>(&self, where_clause: &'a [WhereTerm]) -> &'a ast::Expr { let where_term = &where_clause[self.where_clause_idx]; let Ok(Some((lhs, _, rhs))) = as_binary_components(&where_term.expr) else { panic!("HashJoinKey: expected a valid binary expression"); }; if self.build_side == BinaryExprSide::Lhs { rhs // probe is the opposite side } else { lhs } } } /// Hash join semantics. Build = LHS (populates hash table), Probe = RHS (scanned). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HashJoinType { /// Only matching rows emitted. Inner, /// All build rows appear; unmatched build rows get NULLs for the probe side. LeftOuter, /// Like LeftOuter, plus unmatched probe rows get NULLs for the build side. FullOuter, } /// Hash join operation metadata #[derive(Debug, Clone)] pub struct HashJoinOp { /// Index of the build table in the join order pub build_table_idx: usize, /// Index of the probe table in the join order pub probe_table_idx: usize, /// Join key references, each entry points to an equality condition in the [WhereTerm] /// and indicates which side of the equality belongs to the build table. pub join_keys: Vec, /// Memory budget for hash table pub mem_budget: usize, /// Whether the build input should be materialized as a rowid list before hash build. pub materialize_build_input: bool, /// Whether to use a bloom filter on the probe side. pub use_bloom_filter: bool, /// Join semantics (inner, left outer, or full outer). pub join_type: HashJoinType, } /// Distinguishes union (OR) from intersection (AND) operations for multi-index scans. #[derive(Debug, Clone, PartialEq, Eq)] pub enum SetOperation { /// Union: rowid appears in result if it's in ANY branch (OR) Union, /// Intersection: rowid appears in result only if it's in ALL branches (AND). /// Carries the indices of additional WHERE terms consumed beyond the primary one. Intersection { additional_consumed_terms: Vec, }, } /// Multi-index scan operation metadata for OR-by-union or AND-by-intersection optimization. /// /// When a WHERE clause contains an OR of terms that can each use a different index, /// we can scan each index separately and combine the results using a RowSet for deduplication. /// For example: `WHERE a = 1 OR b = 2` with indexes on `a` and `b`. /// /// Similarly, when a WHERE clause contains AND terms on different indexed columns, /// we can scan each index and intersect the results to reduce the number of table fetches. /// For example: `WHERE a = 1 AND b = 2` with separate indexes on `a` and `b`. #[derive(Debug, Clone)] pub struct MultiIndexScanOp { /// Each branch represents one term with its own index access pub branches: Vec, /// Index of the primary WHERE term. /// For Union: the index of the OR expression. /// For Intersection: the index of the first AND term consumed. pub where_term_idx: usize, /// The set operation to perform when combining branches pub set_op: SetOperation, } /// Residual filters that apply only to union (OR) branches. /// /// Each OR disjunct may be a compound expression (e.g. `a = 1 AND c > 5`), so /// after the index seek satisfies the indexable part, these residuals filter /// the remaining conditions. #[derive(Debug, Clone)] pub struct UnionBranchPrePostFilters { /// Outer-table-only residuals evaluated before the branch's index seek. /// These reference only tables from earlier (outer) loops, so they can /// short-circuit the entire branch without touching the index. pub pre_filter_exprs: Vec, /// Residual filter expressions that could not be satisfied by the index seek. /// Applied within the branch loop after positioning on the table row. pub post_filter_exprs: Vec, /// Whether residual evaluation needs the scanned table cursor positioned. pub requires_table_cursor: bool, } /// A single branch of a multi-index scan, representing one disjunct of an OR expression. #[derive(Debug, Clone)] pub struct MultiIndexBranch { /// The index to use for this branch, or None for rowid access pub index: Option>, /// How this branch probes the table/index. pub access: MultiIndexBranchAccess, /// Estimated number of rows from this branch pub estimated_rows: f64, /// Residual filters for union (OR) branches. `None` for intersection branches. pub union_residuals: Option, } /// Access shape for a single multi-index branch. #[derive(Debug, Clone)] #[expect(clippy::large_enum_variant)] pub enum MultiIndexBranchAccess { /// Ordinary seek/range scan on either the rowid btree or a secondary index. Seek { seek_def: SeekDef }, /// Repeated equality seeks driven by an IN-list or IN-subquery RHS. InSeek { source: InSeekSource }, } #[derive(Clone, Debug)] #[allow(clippy::large_enum_variant)] pub enum Operation { // Scan operation // This operation is used to scan a table. Scan(Scan), // Search operation // This operation is used to search for a row in a table using an index // (i.e. a primary key or a secondary index) Search(Search), // Access through custom index method query IndexMethodQuery(IndexMethodQuery), // Hash join operation // This operation is used on the probe side of a hash join. // The build table is accessed normally (via Scan), and the probe table // uses this operation to indicate it should probe the hash table. HashJoin(HashJoinOp), // Multi-index scan operation for OR-by-union optimization. // This operation scans multiple indexes (one per OR branch) and combines // results using RowSet deduplication. MultiIndexScan(MultiIndexScanOp), } impl Operation { pub fn default_scan_for(table: &Table) -> Self { match table { Table::BTree(_) => Operation::Scan(Scan::BTreeTable { iter_dir: IterationDirection::Forwards, index: None, }), Table::Virtual(_) => Operation::Scan(Scan::VirtualTable { idx_num: -1, idx_str: None, constraints: Vec::new(), }), Table::FromClauseSubquery(_) => Operation::Scan(Scan::Subquery { iter_dir: IterationDirection::Forwards, }), } } pub fn index(&self) -> Option<&Arc> { match self { Operation::Scan(Scan::BTreeTable { index, .. }) => index.as_ref(), Operation::Search(Search::Seek { index, .. }) | Operation::Search(Search::InSeek { index, .. }) => index.as_ref(), Operation::IndexMethodQuery(IndexMethodQuery { index, .. }) => Some(index), Operation::Scan(_) => None, Operation::Search(Search::RowidEq { .. }) => None, Operation::HashJoin(_) => None, // Multi-index scan uses multiple indexes; return None as there's no single index Operation::MultiIndexScan(_) => None, } } /// Returns true if this operation is guaranteed to access at most one row. /// Used to determine whether UPDATE/DELETE is single-write. /// /// Conservative: returns false when unsure (e.g. table scans, range seeks, /// non-unique index seeks). pub fn affects_max_1_row(&self) -> bool { match self { // RowidEq is always a single-row point lookup. Operation::Search(Search::RowidEq { .. }) => true, // Seek on a unique index with all columns equality-constrained. Operation::Search(Search::Seek { index, seek_def }) => { let Some(idx) = index else { // Seek on rowid (no index): check if the seek is an equality // point lookup. This happens when prefix has one eq constraint // and no range component. return seek_def.prefix.len() == 1 && seek_def.prefix[0].eq.is_some() && matches!(seek_def.start.last_component, SeekKeyComponent::None); }; if !idx.unique { return false; } // All index columns must have equality constraints. let num_index_cols = idx.columns.len(); let num_eq_prefix = seek_def.prefix.iter().filter(|c| c.eq.is_some()).count(); num_eq_prefix == num_index_cols } // Table scans, hash joins, multi-index scans, etc. are not single-row. _ => false, } } } impl JoinedTable { /// Returns the btree table for this table reference, if it is a BTreeTable. pub fn btree(&self) -> Option> { match &self.table { Table::BTree(_) => self.table.btree(), _ => None, } } pub fn virtual_table(&self) -> Option> { match &self.table { Table::Virtual(_) => self.table.virtual_table(), _ => None, } } fn matches(&self, database_id: usize, table_name: &str) -> bool { self.database_id == database_id && matches!(self.table, Table::BTree(_) | Table::Virtual(_)) && self.table.get_name().eq_ignore_ascii_case(table_name) } /// Creates a new TableReference for a subquery from a SelectPlan. pub fn new_subquery( identifier: String, plan: SelectPlan, join_info: Option, internal_id: TableInternalId, ) -> Result { let mut columns = plan .result_columns .iter() .map(|rc| { let (col_type, type_name) = infer_type_from_expr(&rc.expr, Some(&plan.table_references)); Column::new( rc.name(&plan.table_references).map(String::from), type_name.to_string(), None, None, col_type, None, ColDef::default(), ) }) .collect::>(); for (i, column) in columns.iter_mut().enumerate() { if super::expr::expr_is_array( &plan.result_columns[i].expr, Some(&plan.table_references), ) { column.set_array_dimensions(1); } column.set_collation(get_collseq_from_expr( &plan.result_columns[i].expr, &plan.table_references, )?); } let table = Table::FromClauseSubquery(Arc::new(FromClauseSubquery { name: identifier.clone(), plan: Box::new(Plan::Select(plan)), columns, result_columns_start_reg: None, materialized_cursor_id: None, cte: None, })); Ok(Self { op: Operation::default_scan_for(&table), table, identifier, internal_id, join_info, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id: 0, indexed: None, }) } /// Creates a new TableReference for a subquery from a Plan (either SelectPlan or CompoundSelect). /// If `explicit_columns` is provided, those names override the derived column names from the SELECT. /// If `cte_id` is provided, this subquery is a CTE reference that can share materialized data. /// If `materialize_hint` is true, the CTE was declared with AS MATERIALIZED and should always /// be materialized regardless of reference count. pub fn new_subquery_from_plan( identifier: String, plan: Plan, join_info: Option, internal_id: TableInternalId, explicit_columns: Option<&[String]>, cte_id: Option, materialize_hint: bool, ) -> Result { // Get result columns and table references from the plan let (result_columns, table_references) = match &plan { Plan::Select(select_plan) => { (&select_plan.result_columns, &select_plan.table_references) } Plan::CompoundSelect { left, right_most, .. } => { // For compound selects, SQLite uses the leftmost select's column names. // The leftmost select is left[0] if the vec is not empty, otherwise right_most. if !left.is_empty() { (&left[0].0.result_columns, &left[0].0.table_references) } else { (&right_most.result_columns, &right_most.table_references) } } Plan::Delete(_) | Plan::Update(_) => { unreachable!("DELETE/UPDATE plans cannot be subqueries") } }; // Note: column count validation (explicit_columns.len() vs result_columns.len()) // is intentionally NOT done here. SQLite defers this check until the CTE is // actually referenced. Callers that represent actual CTE references should // validate the count before calling this method. let mut columns = result_columns .iter() .enumerate() .map(|(i, rc)| { // Use explicit column name if provided, otherwise derive from result column let col_name = explicit_columns .and_then(|cols| cols.get(i).cloned()) .or_else(|| rc.name(table_references).map(String::from)); let (col_type, type_name) = infer_type_from_expr(&rc.expr, Some(table_references)); Column::new( col_name, type_name.to_string(), None, None, col_type, None, ColDef::default(), ) }) .collect::>(); for (i, column) in columns.iter_mut().enumerate() { if super::expr::expr_is_array(&result_columns[i].expr, Some(table_references)) { column.set_array_dimensions(1); } column.set_collation(get_collseq_from_expr( &result_columns[i].expr, table_references, )?); } // materialize_hint is set true for explicit WITH ... AS MATERIALIZED hint. // Multi-reference CTEs are also detected at emission time via reference counting, // and they may be materialized regardless of explicit keyword usage. let cte = cte_id.map(|id| crate::schema::FromClauseSubqueryCteMetadata { id, shared_materialization: false, materialize_hint, }); let table = Table::FromClauseSubquery(Arc::new(FromClauseSubquery { name: identifier.clone(), plan: Box::new(plan), columns, result_columns_start_reg: None, materialized_cursor_id: None, cte, })); Ok(Self { op: Operation::default_scan_for(&table), table, identifier, internal_id, join_info, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id: 0, indexed: None, }) } pub fn columns(&self) -> &[Column] { self.table.columns() } /// Mark a column as used in the query. /// This is used to determine whether a covering index can be used. pub fn mark_column_used(&mut self, index: usize) { if index >= self.column_use_counts.len() { self.column_use_counts.resize(index + 1, 0); } self.column_use_counts[index] += 1; self.col_used_mask.set(index); } /// Clear any previously registered expression index usages. pub fn clear_expression_index_usages(&mut self) { self.expression_index_usages.clear(); } /// Example: SELECT a+b FROM t WHERE a+b=5 with INDEX ON t(a+b) /// We want to remember that (a+b) is available on an index key and that /// columns a and b are only needed to produce that expression. Later we /// can avoid opening the table cursor if all column references are /// covered by expression keys. pub fn register_expression_index_usage( &mut self, normalized_expr: ast::Expr, columns_mask: ColumnUsedMask, ) { if columns_mask.is_empty() { return; } if self .expression_index_usages .iter() .any(|usage| exprs_are_equivalent(&usage.normalized_expr, &normalized_expr)) { return; } self.expression_index_usages.push(ExpressionIndexUsage { normalized_expr: Box::new(normalized_expr), columns_mask, }); } /// Provided an index that may contain expression keys, remove any /// columns from `required_columns` that are fully covered by expression index values. fn apply_expression_index_coverage( &self, index: &Index, required_columns: &mut ColumnUsedMask, ) { let mut coverage_counts = vec![0usize; self.column_use_counts.len()]; let mut any_covered = false; for usage in &self.expression_index_usages { // If the index stores the expression (e.g. idx on lower(name)), all // columns needed *solely* for that expression can be treated as // covered by the index key. Example: // CREATE INDEX idx ON t(lower(name)); // SELECT lower(name) FROM t; // Column `name` is not otherwise needed, so we can rely on the // expression value from the index and drop the table cursor. if index .expression_to_index_pos(&usage.normalized_expr) .is_some() { any_covered = true; for col_idx in usage.columns_mask.iter() { if col_idx >= coverage_counts.len() { coverage_counts.resize(col_idx + 1, 0); } coverage_counts[col_idx] += 1; } } } if !any_covered { return; } for (col_idx, &covered) in coverage_counts.iter().enumerate() { if covered == 0 { continue; } // Only drop the requirement if *all* references to this column are // satisfied by expression-index values. If the column is also // selected or filtered directly, the table data is still needed. if self.column_use_counts.get(col_idx).copied().unwrap_or(0) == covered { required_columns.clear(col_idx); } } } /// Open the necessary cursors for this table reference. /// Generally a table cursor is always opened unless a SELECT query can use a covering index. /// An index cursor is opened if an index is used in any way for reading data from the table. pub fn open_cursors( &self, program: &mut ProgramBuilder, mode: OperationMode, schema: &Schema, ) -> Result<(Option, Option)> { let index = self.op.index(); match &self.table { Table::BTree(btree) => { let use_covering_index = self.utilizes_covering_index(); let index_is_ephemeral = index.is_some_and(|index| index.ephemeral); let table_not_required = matches!(mode, OperationMode::SELECT) && use_covering_index && !index_is_ephemeral; let table_cursor_id = if table_not_required { None } else if let OperationMode::UPDATE(UpdateRowSource::PrebuiltEphemeralTable { target_table, .. }) = &mode { // The cursor for the ephemeral table was already allocated earlier. Let's allocate one for the target table, // in case it wasn't already allocated when populating the ephemeral table. Some(program.alloc_cursor_id_keyed_if_not_exists( CursorKey::table(target_table.internal_id), match &target_table.table { Table::BTree(btree) => CursorType::BTreeTable(btree.clone()), Table::Virtual(virtual_table) => { CursorType::VirtualTable(virtual_table.clone()) } _ => unreachable!("target table must be a btree or virtual table"), }, )) } else { // Check if this is a materialized view let cursor_type = if let Some(view_mutex) = schema.get_materialized_view(&btree.name) { CursorType::MaterializedView(btree.clone(), view_mutex) } else { CursorType::BTreeTable(btree.clone()) }; Some(program.alloc_cursor_id_keyed_if_not_exists( CursorKey::table(self.internal_id), cursor_type, )) }; let index_cursor_id = index .map(|index| { program.alloc_cursor_index_if_not_exists( CursorKey::index(self.internal_id, index.clone()), index, ) }) .transpose()?; Ok((table_cursor_id, index_cursor_id)) } Table::Virtual(virtual_table) => { let table_cursor_id = Some(program.alloc_cursor_id_keyed( CursorKey::table(self.internal_id), CursorType::VirtualTable(virtual_table.clone()), )); let index_cursor_id = None; Ok((table_cursor_id, index_cursor_id)) } Table::FromClauseSubquery(..) => { let index_cursor_id = index .map(|index| { program.alloc_cursor_index_if_not_exists( CursorKey::index(self.internal_id, index.clone()), index, ) }) .transpose()?; Ok((None, index_cursor_id)) } } } /// Resolve the already opened cursors for this table reference. pub fn resolve_cursors( &self, program: &mut ProgramBuilder, mode: OperationMode, ) -> Result<(Option, Option)> { let index = self.op.index(); let table_cursor_id = if let Table::FromClauseSubquery(from_clause_subquery) = &self.table { from_clause_subquery.materialized_cursor_id } else if let OperationMode::UPDATE(UpdateRowSource::PrebuiltEphemeralTable { target_table, .. }) = &mode { program.resolve_cursor_id_safe(&CursorKey::table(target_table.internal_id)) } else { program.resolve_cursor_id_safe(&CursorKey::table(self.internal_id)) }; let index_cursor_id = index.map(|index| { program.resolve_cursor_id(&CursorKey::index(self.internal_id, index.clone())) }); Ok((table_cursor_id, index_cursor_id)) } /// Returns true if a given index is a covering index for this [TableReference]. pub fn index_is_covering(&self, index: &Index) -> bool { let Table::BTree(btree) = &self.table else { return false; }; if self.col_used_mask.is_empty() { return false; } if index.index_method.is_some() { return false; } if self.expression_index_usages.is_empty() { Self::index_covers_columns(index, btree, &self.col_used_mask) } else { let mut required_columns = self.col_used_mask.clone(); self.apply_expression_index_coverage(index, &mut required_columns); if required_columns.is_empty() { return true; } Self::index_covers_columns(index, btree, &required_columns) } } fn index_covers_columns( index: &Index, btree: &BTreeTable, required_columns: &ColumnUsedMask, ) -> bool { // If a table has a rowid, the index is guaranteed to contain it as well. let rowid_alias_pos = if btree.has_rowid { btree.get_rowid_alias_column().map(|(pos, _)| pos) } else { None }; if let Some(pos) = rowid_alias_pos { if required_columns.is_only(pos) { // If the index would be ONLY used for the rowid, don't bother. // Example: SELECT id FROM t where id is a rowid alias - just scan the table. return false; } } // Check that every required column is covered by the index for required_col in required_columns.iter() { if rowid_alias_pos == Some(required_col) { // rowid is always implicitly covered by the index continue; } let covered_by_index = index .columns .iter() .any(|c| c.expr.is_none() && c.pos_in_table == required_col); if !covered_by_index { return false; } } true } /// Returns true if the index selected for use with this [TableReference] is a covering index, /// meaning that it contains all the columns that are referenced in the query. pub fn utilizes_covering_index(&self) -> bool { let Some(index) = self.op.index() else { return false; }; self.index_is_covering(index.as_ref()) } pub fn column_is_used(&self, index: usize) -> bool { self.col_used_mask.get(index) } } /// A definition of a rowid/index search. /// /// [SeekKey] is the condition that is used to seek to a specific row in a table/index. /// [SeekKey] also used to represent range scan termination condition. #[derive(Debug, Clone)] pub struct SeekDef { /// Common prefix of the key which is shared between start/end fields /// For example, given: /// - CREATE INDEX i ON t (x, y desc) /// - SELECT * FROM t WHERE x = 1 AND y >= 30 /// /// Then, prefix=[(eq=1, ASC)], start=Some((ge, Expr(30))), end=Some((gt, Sentinel)) pub prefix: Vec, /// The condition to use when seeking. See [SeekKey] for more details. pub start: SeekKey, /// The condition to use when terminating the scan that follows the seek. See [SeekKey] for more details. pub end: SeekKey, /// The direction of the scan that follows the seek. pub iter_dir: IterationDirection, } pub struct SeekDefKeyIterator<'a, T> { seek_def: &'a SeekDef, seek_key: &'a SeekKey, pos: usize, _t: PhantomData, } impl<'a> Iterator for SeekDefKeyIterator<'a, SeekKeyComponent<&'a ast::Expr>> { type Item = SeekKeyComponent<&'a ast::Expr>; fn next(&mut self) -> Option { let result = if self.pos < self.seek_def.prefix.len() { Some(SeekKeyComponent::Expr( &self.seek_def.prefix[self.pos].eq.as_ref().unwrap().1, )) } else if self.pos == self.seek_def.prefix.len() { match &self.seek_key.last_component { SeekKeyComponent::Expr(expr) => Some(SeekKeyComponent::Expr(expr)), SeekKeyComponent::Null => Some(SeekKeyComponent::Null), SeekKeyComponent::None => None, } } else { None }; self.pos += 1; result } } impl<'a> Iterator for SeekDefKeyIterator<'a, Affinity> { type Item = Affinity; fn next(&mut self) -> Option { let result = if self.pos < self.seek_def.prefix.len() { Some(self.seek_def.prefix[self.pos].eq.as_ref().unwrap().2) } else if self.pos == self.seek_def.prefix.len() { match &self.seek_key.last_component { SeekKeyComponent::Expr(..) => Some(self.seek_key.affinity), // NULL sentinel does not require conversion; use NONE affinity so width matches. SeekKeyComponent::Null => Some(Affinity::Blob), SeekKeyComponent::None => None, } } else { None }; self.pos += 1; result } } impl SeekDef { /// returns amount of values in the given seek key /// - so, for SELECT * FROM t WHERE x = 10 AND y = 20 AND y >= 30 there will be 3 values (10, 20, 30) pub fn size(&self, key: &SeekKey) -> usize { self.prefix.len() + match key.last_component { SeekKeyComponent::Expr(_) => 1, SeekKeyComponent::Null => 1, SeekKeyComponent::None => 0, } } /// iterate over value expressions in the given seek key pub fn iter<'a>( &'a self, key: &'a SeekKey, ) -> SeekDefKeyIterator<'a, SeekKeyComponent<&'a ast::Expr>> { SeekDefKeyIterator { seek_def: self, seek_key: key, pos: 0, _t: PhantomData, } } /// iterate over affinity in the given seek key pub fn iter_affinity<'a>(&'a self, key: &'a SeekKey) -> SeekDefKeyIterator<'a, Affinity> { SeekDefKeyIterator { seek_def: self, seek_key: key, pos: 0, _t: PhantomData, } } } /// Build the affinity string for a synthesized ephemeral seek index. /// /// The seek key only constrains the leading key prefix, but the backing record /// stored in the ephemeral index still includes the remaining payload columns /// (and possibly a synthetic rowid). Pad those trailing slots with NONE affinity /// so MakeRecord sees the same layout the index insert path produced. pub fn synthesized_seek_affinity_str(index: &Index, seek_def: &SeekDef) -> Option> { let num_key_cols = seek_def.size(&seek_def.start); let total_cols = index.columns.len() + if index.has_rowid { 1 } else { 0 }; let mut aff: String = seek_def .iter_affinity(&seek_def.start) .map(|a| a.aff_mask()) .collect(); for _ in num_key_cols..total_cols { aff.push(affinity::SQLITE_AFF_NONE); } aff.chars() .any(|c| c != affinity::SQLITE_AFF_NONE) .then(|| Arc::new(aff)) } /// [SeekKeyComponent] represents the optional trailing component of a seek key. /// Besides user-provided expressions, planner logic may inject a synthetic NULL sentinel /// to encode SQLite-compatible boundary behavior on composite indexes. /// This enum accepts generic argument E so we can use both /// SeekKeyComponent and SeekKeyComponent<&ast::Expr>. #[derive(Debug, Clone)] pub enum SeekKeyComponent { Expr(E), Null, None, } /// A condition to use when seeking. #[derive(Debug, Clone)] pub struct SeekKey { /// Complete key must be constructed from common [SeekDef::prefix] and optional last_component pub last_component: SeekKeyComponent, /// The comparison operator to use when seeking. pub op: SeekOp, /// Affinity of the comparison pub affinity: Affinity, } /// Represents the type of table scan performed during query execution. #[derive(Clone, Debug)] pub enum Scan { /// A scan of a B-tree–backed table, optionally using an index, and with an iteration direction. BTreeTable { /// The iter_dir is used to indicate the direction of the iterator. iter_dir: IterationDirection, /// The index that we are using to scan the table, if any. index: Option>, }, /// A scan of a virtual table, delegated to the table’s `filter` and related methods. VirtualTable { /// Index identifier returned by the table's `best_index` method. idx_num: i32, /// Optional index name returned by the table’s `best_index` method. idx_str: Option, /// Constraining expressions to be passed to the table’s `filter` method. /// The order of expressions matches the argument order expected by the virtual table. constraints: Vec, }, /// A scan of a subquery in the `FROM` clause. Subquery { /// Coroutine-backed scans run forwards. Materialized subqueries may /// also be scanned backwards when the planner relies on intrinsic /// subquery order for an extremum fast path. iter_dir: IterationDirection, }, } /// An enum that represents a search operation that can be used to search for a row in a table using an index /// (i.e. a primary key or a secondary index) #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug)] pub enum Search { /// A rowid equality point lookup. This is a special case that uses the SeekRowid bytecode instruction and does not loop. RowidEq { cmp_expr: ast::Expr }, /// A search on a table btree (via `rowid`) or a secondary index search. Uses bytecode instructions like SeekGE, SeekGT etc. Seek { index: Option>, seek_def: SeekDef, }, /// An IN-driven index seek. Iterates an ephemeral B-tree of IN values and /// for each value seeks into the real index (or table, if seek by rowid). InSeek { index: Option>, source: InSeekSource, }, } /// Where IN-seek values come from. #[derive(Clone, Debug)] pub enum InSeekSource { /// Literal values to materialize into a new ephemeral index at open_loop time. LiteralList { values: Vec, affinity: Affinity, }, /// Subquery already materialized by emit_non_from_clause_subquery; /// open_loop reuses the existing ephemeral cursor. Subquery { cursor_id: CursorID }, } #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug)] pub struct IndexMethodQuery { /// index method to use pub index: Arc, /// idx of the pattern from [crate::index_method::IndexMethodAttachment::definition] which planner chose to use for the access pub pattern_idx: usize, /// captured arguments for the pattern chosen by the planner pub arguments: Vec, /// mapping from index of [ast::Expr::Column] to the column index of IndexMethod response pub covered_columns: HashMap, } #[derive(Debug, Clone, PartialEq)] pub struct Aggregate { pub func: AggFunc, pub args: Vec, pub original_expr: ast::Expr, pub distinctness: Distinctness, } impl Aggregate { pub fn new(func: AggFunc, args: &[Box], expr: &Expr, distinctness: Distinctness) -> Self { Aggregate { func, args: args.iter().map(|arg| *arg.clone()).collect(), original_expr: expr.clone(), distinctness, } } pub fn is_distinct(&self) -> bool { self.distinctness.is_distinct() } } /// Represents the window definition and all window functions associated with a single SELECT. #[derive(Debug, Clone)] pub struct Window { /// The window name, either provided in the original statement or synthetically generated by /// the planner. This is optional because it can be assigned at different stages of query /// processing, but it should eventually always be set. pub name: Option, /// Expressions from the PARTITION BY clause. pub partition_by: Vec, /// The number of unique expressions in the PARTITION BY clause. This determines how many of /// the leftmost columns in the subquery output make up the partition key. pub deduplicated_partition_by_len: Option, /// Expressions from the ORDER BY clause. pub order_by: Vec<(Expr, SortOrder)>, /// All window functions associated with this window. pub functions: Vec, } impl Window { const DEFAULT_SORT_ORDER: SortOrder = SortOrder::Asc; pub fn new(name: Option, ast: &ast::Window) -> Result { if !Self::is_default_frame_spec(&ast.frame_clause) { crate::bail_parse_error!("Custom frame specifications are not supported yet"); } Ok(Window { name, partition_by: ast.partition_by.iter().map(|arg| *arg.clone()).collect(), deduplicated_partition_by_len: None, order_by: ast .order_by .iter() .map(|col| { ( *col.expr.clone(), col.order.unwrap_or(Self::DEFAULT_SORT_ORDER), ) }) .collect(), functions: vec![], }) } pub fn is_equivalent(&self, ast: &ast::Window) -> bool { if !Self::is_default_frame_spec(&ast.frame_clause) { return false; } if self.partition_by.len() != ast.partition_by.len() { return false; } if !self .partition_by .iter() .zip(&ast.partition_by) .all(|(a, b)| exprs_are_equivalent(a, b)) { return false; } if self.order_by.len() != ast.order_by.len() { return false; } self.order_by .iter() .zip(&ast.order_by) .all(|((expr_a, order_a), col_b)| { exprs_are_equivalent(expr_a, &col_b.expr) && *order_a == col_b.order.unwrap_or(Self::DEFAULT_SORT_ORDER) }) } fn is_default_frame_spec(frame: &Option) -> bool { if let Some(frame_clause) = frame { let FrameClause { mode, start, end, exclude, } = frame_clause; if *mode != FrameMode::Range { return false; } if *start != FrameBound::UnboundedPreceding { return false; } if *end != Some(FrameBound::CurrentRow) { return false; } if let Some(exclude) = exclude { if *exclude != FrameExclude::NoOthers { return false; } } } true } } #[derive(Debug, Clone)] pub enum WindowFunctionKind { Agg(AggFunc), Window(WindowFunc), } #[derive(Debug, Clone)] pub struct WindowFunction { /// The resolved function. Aggregate window functions and specialized window /// functions such as ROW_NUMBER() are supported. pub func: WindowFunctionKind, /// The expression from which the function was resolved. pub original_expr: Expr, } #[derive(Debug, Clone)] pub enum SubqueryState { /// The subquery has not been evaluated yet. /// The 'plan' field is only optional because it is .take()'d when the the subquery /// is translated into bytecode. Unevaluated { plan: Option> }, /// The subquery has been evaluated. /// The [evaluated_at] field contains the loop index where the subquery was evaluated. /// The query plan struct no longer exists because translating the plan currently /// requires an ownership transfer. We retain the outer table references so /// later masking/evaluation logic can still reason about dependencies. Evaluated { /// Join-loop position where the subquery was emitted into bytecode. evaluated_at: EvalAt, /// Outer table ids referenced by the subquery when it was planned. /// We keep these so later analysis can still understand dependencies /// even after the plan is consumed. outer_ref_ids: Vec, }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SubqueryPosition { ResultColumn, Where, GroupBy, Having, OrderBy, LimitOffset, } impl SubqueryPosition { /// Returns true if a subquery in this position of the SELECT can be correlated, i.e. if it can reference columns from the outer query. pub fn allow_correlated(&self) -> bool { matches!( self, SubqueryPosition::ResultColumn | SubqueryPosition::Where | SubqueryPosition::GroupBy | SubqueryPosition::OrderBy ) } pub fn name(&self) -> &'static str { match self { SubqueryPosition::ResultColumn => "SELECT list", SubqueryPosition::Where => "WHERE", SubqueryPosition::GroupBy => "GROUP BY", SubqueryPosition::Having => "HAVING", SubqueryPosition::OrderBy => "ORDER BY", SubqueryPosition::LimitOffset => "LIMIT/OFFSET", } } } #[derive(Debug, Clone)] /// A subquery that is not part of the `FROM` clause. /// This is used for subqueries in the WHERE clause, HAVING clause, ORDER BY clause, LIMIT clause, OFFSET clause, etc. /// Currently only subqueries in the WHERE clause are supported. pub struct NonFromClauseSubquery { pub internal_id: TableInternalId, pub query_type: SubqueryType, pub state: SubqueryState, pub correlated: bool, pub origin: SubqueryOrigin, pub eval_phase: SubqueryEvalPhase, } impl NonFromClauseSubquery { /// Returns true if the subquery has been evaluated (translated into bytecode). pub fn has_been_evaluated(&self) -> bool { matches!(self.state, SubqueryState::Evaluated { .. }) } pub fn is_post_write_returning(&self) -> bool { self.origin.is_post_write_returning() && matches!(self.eval_phase, SubqueryEvalPhase::PostWriteReturning) } pub fn reads_table(&self, database_id: usize, table_name: &str) -> bool { match &self.state { SubqueryState::Unevaluated { plan: Some(plan) } => { plan.reads_table(database_id, table_name) } _ => false, } } /// Returns the loop index where the subquery should be evaluated in this join order. /// /// If the subquery references tables from the parent query, it is evaluated at /// the right-most loop that makes those tables available. For hash joins, this /// may map a build-table reference to the probe loop where its rows are produced. pub fn get_eval_at( &self, join_order: &[JoinOrderMember], table_references: Option<&TableReferences>, ) -> Result { let plan = match &self.state { SubqueryState::Unevaluated { plan } => plan.as_ref().unwrap(), SubqueryState::Evaluated { evaluated_at, .. } => { return Ok(*evaluated_at); } }; eval_at_for_select_plan(plan, join_order, table_references) } /// Consumes the plan and returns it, and sets the subquery to the evaluated state. /// /// This captures any outer references before the plan is moved so later /// phases can still reason about dependencies. pub fn consume_plan(&mut self, evaluated_at: EvalAt) -> Box { match &mut self.state { SubqueryState::Unevaluated { plan } => { let outer_ref_ids = plan .as_ref() .map(|plan| { plan.table_references .outer_query_refs() .iter() .filter(|t| t.is_used()) .map(|t| t.internal_id) .collect::>() }) .unwrap_or_default(); let plan = plan.take().unwrap(); self.state = SubqueryState::Evaluated { evaluated_at, outer_ref_ids, }; plan } SubqueryState::Evaluated { .. } => { panic!("subquery has already been evaluated"); } } } } /// Determine the earliest evaluation point for a nested plan by walking all SELECT components. fn eval_at_for_plan( plan: &Plan, join_order: &[JoinOrderMember], table_references: Option<&TableReferences>, ) -> Result { match plan { Plan::Select(select_plan) => { eval_at_for_select_plan(select_plan, join_order, table_references) } Plan::CompoundSelect { left, right_most, .. } => { let mut eval_at = EvalAt::BeforeLoop; for (select_plan, _) in left.iter() { eval_at = eval_at.max(eval_at_for_select_plan( select_plan, join_order, table_references, )?); } eval_at = eval_at.max(eval_at_for_select_plan( right_most, join_order, table_references, )?); Ok(eval_at) } Plan::Delete(_) | Plan::Update(_) => Ok(EvalAt::BeforeLoop), } } /// Returns true if a plan (including compound SELECTs) references outer-scope tables. pub fn plan_is_correlated(plan: &Plan) -> bool { match plan { Plan::Select(select_plan) => select_plan.is_correlated(), Plan::CompoundSelect { left, right_most, .. } => left.iter().any(|(plan, _)| plan.is_correlated()) || right_most.is_correlated(), Plan::Delete(_) | Plan::Update(_) => false, } } /// Returns true when evaluating this plan depends on table values from an /// enclosing query scope outside the plan itself. /// /// This is narrower than [`plan_is_correlated()`]: a plan may contain /// internally correlated scalar subqueries (for example, a scalar subquery that /// references another table in the same CTE) without depending on an enclosing /// query row. Those plans are still safe to materialize once and reuse. pub fn plan_has_outer_scope_dependency(plan: &Plan) -> bool { fn select_plan_has_outer_scope_dependency( plan: &SelectPlan, accessible_table_ids: &mut Vec, ) -> bool { let outer_scope_base_len = accessible_table_ids.len(); accessible_table_ids.extend( plan.table_references .joined_tables() .iter() .map(|table| table.internal_id), ); let has_outer_scope_dependency = plan.table_references .outer_query_refs() .iter() .any(|outer_ref| { outer_ref.is_used() && !accessible_table_ids.contains(&outer_ref.internal_id) }) || plan .non_from_clause_subqueries .iter() .any(|subquery| match &subquery.state { SubqueryState::Unevaluated { plan: Some(subquery_plan), } => select_plan_has_outer_scope_dependency( subquery_plan, accessible_table_ids, ), SubqueryState::Unevaluated { plan: None } => false, SubqueryState::Evaluated { outer_ref_ids, .. } => outer_ref_ids .iter() .any(|outer_ref_id| !accessible_table_ids.contains(outer_ref_id)), }) || plan .table_references .joined_tables() .iter() .any(|table| match &table.table { Table::FromClauseSubquery(subquery) => { plan_has_outer_scope_dependency_with_tables( subquery.plan.as_ref(), accessible_table_ids, ) } _ => false, }); accessible_table_ids.truncate(outer_scope_base_len); has_outer_scope_dependency } fn plan_has_outer_scope_dependency_with_tables( plan: &Plan, accessible_table_ids: &mut Vec, ) -> bool { match plan { Plan::Select(select_plan) => { select_plan_has_outer_scope_dependency(select_plan, accessible_table_ids) } Plan::CompoundSelect { left, right_most, .. } => { left.iter().any(|(select_plan, _)| { select_plan_has_outer_scope_dependency(select_plan, accessible_table_ids) }) || select_plan_has_outer_scope_dependency(right_most, accessible_table_ids) } Plan::Delete(_) | Plan::Update(_) => false, } } plan_has_outer_scope_dependency_with_tables(plan, &mut Vec::new()) } /// Determine when a SELECT plan can be evaluated, including nested non-FROM and FROM-clause subqueries. fn eval_at_for_select_plan( plan: &SelectPlan, join_order: &[JoinOrderMember], table_references: Option<&TableReferences>, ) -> Result { let mut eval_at = EvalAt::BeforeLoop; let used_outer_refs = plan .table_references .outer_query_refs() .iter() .filter(|t| t.is_used()); for outer_ref in used_outer_refs { if let Some(loop_idx) = resolve_outer_ref_loop(outer_ref.internal_id, join_order, table_references) { eval_at = eval_at.max(EvalAt::Loop(loop_idx)); } } for subquery in plan.non_from_clause_subqueries.iter() { let eval_at_inner = subquery.get_eval_at(join_order, table_references)?; eval_at = eval_at.max(eval_at_inner); } for joined_table in plan.table_references.joined_tables().iter() { if let Table::FromClauseSubquery(from_clause_subquery) = &joined_table.table { eval_at = eval_at.max(eval_at_for_plan( from_clause_subquery.plan.as_ref(), join_order, table_references, )?); } } Ok(eval_at) } /// Resolves the loop index for an outer-table reference. /// /// If the table is not present in the join order, we look for a hash join /// where that table is the build side and map it to the probe loop. fn resolve_outer_ref_loop( table_id: TableInternalId, join_order: &[JoinOrderMember], table_references: Option<&TableReferences>, ) -> Option { if let Some(loop_idx) = join_order.iter().position(|t| t.table_id == table_id) { return Some(loop_idx); } let tables = table_references?; for (probe_idx, member) in join_order.iter().enumerate() { let probe_table = &tables.joined_tables()[member.original_idx]; if let Operation::HashJoin(ref hj) = probe_table.op { let build_table = &tables.joined_tables()[hj.build_table_idx]; if build_table.internal_id == table_id { return Some(probe_idx); } } } None } #[cfg(test)] mod tests { use super::*; use rand_chacha::{ rand_core::{RngCore, SeedableRng}, ChaCha8Rng, }; #[test] fn test_column_used_mask_empty() { let mask = ColumnUsedMask::default(); assert!(mask.is_empty()); let mut mask2 = ColumnUsedMask::default(); mask2.set(0); assert!(!mask2.is_empty()); } #[test] fn test_column_used_mask_set_and_get() { let mut mask = ColumnUsedMask::default(); let max_columns = 10000; let mut set_indices = Vec::new(); let mut rng = ChaCha8Rng::seed_from_u64( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(), ); for i in 0..max_columns { if rng.next_u32() % 3 == 0 { set_indices.push(i); mask.set(i); } } // Verify set bits are present for &i in &set_indices { assert!(mask.get(i), "Expected bit {i} to be set"); } // Verify unset bits are not present for i in 0..max_columns { if !set_indices.contains(&i) { assert!(!mask.get(i), "Expected bit {i} to not be set"); } } } #[test] fn test_column_used_mask_subset_relationship() { let mut full_mask = ColumnUsedMask::default(); let mut subset_mask = ColumnUsedMask::default(); let max_columns = 5000; let mut rng = ChaCha8Rng::seed_from_u64( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(), ); // Create a pattern where subset has fewer bits for i in 0..max_columns { if rng.next_u32() % 5 == 0 { full_mask.set(i); if i % 2 == 0 { subset_mask.set(i); } } } // full_mask contains all bits of subset_mask assert!(full_mask.contains_all_set_bits_of(&subset_mask)); // subset_mask does not contain all bits of full_mask assert!(!subset_mask.contains_all_set_bits_of(&full_mask)); // A mask contains itself assert!(full_mask.contains_all_set_bits_of(&full_mask)); assert!(subset_mask.contains_all_set_bits_of(&subset_mask)); } #[test] fn test_column_used_mask_empty_subset() { let mut mask = ColumnUsedMask::default(); for i in (0..1000).step_by(7) { mask.set(i); } let empty_mask = ColumnUsedMask::default(); // Empty mask is subset of everything assert!(mask.contains_all_set_bits_of(&empty_mask)); assert!(empty_mask.contains_all_set_bits_of(&empty_mask)); } #[test] fn test_column_used_mask_sparse_indices() { let mut sparse_mask = ColumnUsedMask::default(); // Test with very sparse, large indices let sparse_indices = vec![0, 137, 1042, 5389, 10000, 50000, 100000, 500000, 1000000]; for &idx in &sparse_indices { sparse_mask.set(idx); } for &idx in &sparse_indices { assert!(sparse_mask.get(idx), "Expected bit {idx} to be set"); } // Check some indices that shouldn't be set let unset_indices = vec![1, 100, 1000, 5000, 25000, 75000, 250000, 750000]; for &idx in &unset_indices { assert!(!sparse_mask.get(idx), "Expected bit {idx} to not be set"); } assert!(!sparse_mask.is_empty()); } #[test] fn test_column_used_mask_clear() { let mut mask = ColumnUsedMask::default(); // Test inline clear mask.set(5); mask.set(10); assert!(mask.get(5)); mask.clear(5); assert!(!mask.get(5)); assert!(mask.get(10)); // Test overflow clear mask.set(100); mask.set(200); assert!(mask.get(100)); mask.clear(100); assert!(!mask.get(100)); assert!(mask.get(200)); // Clear non-existent bit should be no-op mask.clear(999); assert!(!mask.get(999)); } #[test] fn test_column_used_mask_is_only() { // Test inline is_only let mut mask = ColumnUsedMask::default(); mask.set(5); assert!(mask.is_only(5)); assert!(!mask.is_only(0)); assert!(!mask.is_only(100)); mask.set(10); assert!(!mask.is_only(5)); assert!(!mask.is_only(10)); // Test overflow is_only let mut mask2 = ColumnUsedMask::default(); mask2.set(100); assert!(mask2.is_only(100)); assert!(!mask2.is_only(0)); assert!(!mask2.is_only(50)); mask2.set(200); assert!(!mask2.is_only(100)); // Test empty mask let empty = ColumnUsedMask::default(); assert!(!empty.is_only(0)); assert!(!empty.is_only(100)); } #[test] fn test_column_used_mask_subtract() { let mut mask1 = ColumnUsedMask::default(); let mut mask2 = ColumnUsedMask::default(); // Set up mask1 with inline and overflow bits for i in [1, 5, 10, 63, 64, 100, 200] { mask1.set(i); } // Set up mask2 with some overlapping bits for i in [5, 10, 100] { mask2.set(i); } mask1.subtract(&mask2); // Should remain assert!(mask1.get(1)); assert!(mask1.get(63)); assert!(mask1.get(64)); assert!(mask1.get(200)); // Should be cleared assert!(!mask1.get(5)); assert!(!mask1.get(10)); assert!(!mask1.get(100)); } #[test] fn test_column_used_mask_iter() { let mut mask = ColumnUsedMask::default(); let indices = vec![0, 5, 63, 64, 65, 127, 128, 200, 1000]; for &i in &indices { mask.set(i); } let collected: Vec = mask.iter().collect(); assert_eq!(collected, indices); // Empty mask iter let empty = ColumnUsedMask::default(); assert_eq!(empty.iter().count(), 0); } #[test] fn test_column_used_mask_bitor_assign() { let mut mask1 = ColumnUsedMask::default(); let mut mask2 = ColumnUsedMask::default(); // Inline bits mask1.set(1); mask1.set(5); mask2.set(5); mask2.set(10); // Overflow bits mask1.set(100); mask2.set(200); mask1 |= &mask2; assert!(mask1.get(1)); assert!(mask1.get(5)); assert!(mask1.get(10)); assert!(mask1.get(100)); assert!(mask1.get(200)); // mask2 should be unchanged assert!(!mask2.get(1)); assert!(mask2.get(5)); assert!(mask2.get(10)); assert!(!mask2.get(100)); assert!(mask2.get(200)); } #[test] fn test_column_used_mask_boundary_conditions() { let mut mask = ColumnUsedMask::default(); // Test at inline/overflow boundary mask.set(63); // last inline bit mask.set(64); // first overflow bit assert!(mask.get(63)); assert!(mask.get(64)); assert!(!mask.get(62)); assert!(!mask.get(65)); // Test is_only at boundary let mut mask2 = ColumnUsedMask::default(); mask2.set(63); assert!(mask2.is_only(63)); let mut mask3 = ColumnUsedMask::default(); mask3.set(64); assert!(mask3.is_only(64)); } fn rng_from_env_or_time() -> (ChaCha8Rng, u64) { let seed = std::env::var("TEST_SEED") .ok() .and_then(|s| s.parse().ok()) .unwrap_or_else(|| { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() as u64 }); (ChaCha8Rng::seed_from_u64(seed), seed) } /// Reference implementation using BTreeSet for correctness comparison struct ReferenceMask(std::collections::BTreeSet); impl ReferenceMask { fn new() -> Self { Self(std::collections::BTreeSet::new()) } fn set(&mut self, index: usize) { self.0.insert(index); } fn get(&self, index: usize) -> bool { self.0.contains(&index) } fn clear(&mut self, index: usize) { self.0.remove(&index); } fn is_empty(&self) -> bool { self.0.is_empty() } fn is_only(&self, index: usize) -> bool { self.0.len() == 1 && self.0.contains(&index) } fn contains_all_set_bits_of(&self, other: &Self) -> bool { other.0.is_subset(&self.0) } fn subtract(&mut self, other: &Self) { for &idx in &other.0 { self.0.remove(&idx); } } fn bitor_assign(&mut self, other: &Self) { for &idx in &other.0 { self.0.insert(idx); } } } #[test] fn test_column_used_mask_fuzz() { let (mut rng, seed) = rng_from_env_or_time(); eprintln!("test_column_used_mask_random_ops seed: {seed}"); let mut mask = ColumnUsedMask::default(); let mut reference = ReferenceMask::new(); let num_ops = 100000; let max_index = 4096; for _ in 0..num_ops { let op = rng.next_u32() % 10; let idx = (rng.next_u32() % max_index) as usize; match op { 0..=2 => { // Set (more frequent) mask.set(idx); reference.set(idx); } 3 => { // Get assert_eq!( mask.get(idx), reference.get(idx), "get({idx}) mismatch, seed={seed}" ); } 4 => { // Clear mask.clear(idx); reference.clear(idx); } 5 => { // IsEmpty assert_eq!( mask.is_empty(), reference.is_empty(), "is_empty mismatch, seed={seed}" ); } 6 => { // IsOnly assert_eq!( mask.is_only(idx), reference.is_only(idx), "is_only({idx}) mismatch, seed={seed}" ); } 7 => { // ContainsAllSetBitsOf with random other mask let mut other_mask = ColumnUsedMask::default(); let mut other_ref = ReferenceMask::new(); for _ in 0..(rng.next_u32() % 20) { let other_idx = (rng.next_u32() % max_index) as usize; other_mask.set(other_idx); other_ref.set(other_idx); } assert_eq!( mask.contains_all_set_bits_of(&other_mask), reference.contains_all_set_bits_of(&other_ref), "contains_all_set_bits_of mismatch, seed={seed}" ); } 8 => { // BitOrAssign with random other mask let mut other_mask = ColumnUsedMask::default(); let mut other_ref = ReferenceMask::new(); for _ in 0..(rng.next_u32() % 20) { let other_idx = (rng.next_u32() % max_index) as usize; other_mask.set(other_idx); other_ref.set(other_idx); } mask |= &other_mask; reference.bitor_assign(&other_ref); } 9 => { // Subtract with random other mask let mut other_mask = ColumnUsedMask::default(); let mut other_ref = ReferenceMask::new(); for _ in 0..(rng.next_u32() % 20) { let other_idx = (rng.next_u32() % max_index) as usize; other_mask.set(other_idx); other_ref.set(other_idx); } mask.subtract(&other_mask); reference.subtract(&other_ref); } _ => unreachable!(), } } // Final verification: iter should produce same results let mask_set: std::collections::BTreeSet = mask.iter().collect(); assert_eq!(mask_set, reference.0, "final iter mismatch, seed={seed}"); } } ================================================ FILE: core/translate/planner.rs ================================================ use crate::sync::Arc; use crate::{turso_assert, turso_assert_greater_than_or_equal, turso_assert_less_than}; use std::cmp::PartialEq; use super::{ expr::{walk_expr, walk_expr_mut}, plan::{ Aggregate, ColumnUsedMask, Distinctness, EvalAt, IterationDirection, JoinInfo, JoinOrderMember, JoinType as PlanJoinType, JoinedTable, Operation, OuterQueryReference, Plan, QueryDestination, ResultSetColumn, Scan, TableReferences, WhereTerm, }, select::prepare_select_plan, }; use crate::translate::{ emitter::Resolver, expr::{expr_vector_size, unwrap_parens, BindingBehavior, WalkControl}, plan::{NonFromClauseSubquery, SubqueryState}, }; use crate::{ ast::Limit, function::Func, schema::Table, util::{exprs_are_equivalent, normalize_ident, validate_aggregate_function_tail}, Result, }; use crate::{ function::{AggFunc, ExtFunc}, translate::expr::bind_and_rewrite_expr, }; use crate::{ translate::plan::{Window, WindowFunction, WindowFunctionKind}, vdbe::builder::ProgramBuilder, }; use smallvec::SmallVec; use turso_parser::ast::Literal::Null; use turso_parser::ast::{ self, As, Expr, FromClause, JoinType, Materialized, Over, QualifiedName, Select, TableInternalId, With, }; /// A CTE definition stored for deferred planning. /// Instead of planning CTEs once and cloning the result, we store the AST and /// re-plan each time the CTE is referenced. This ensures each reference gets /// truly unique internal_ids and cursor IDs. struct CteDefinition { /// Globally unique CTE identity for sharing materialized data. /// Multiple references to this CTE will use this ID to look up shared cursors. cte_id: usize, /// Normalized CTE name name: String, /// The original AST SELECT statement (cloned for each reference) select: Select, /// Explicit column names from WITH t(a, b) AS (...) syntax explicit_columns: Vec, /// Indexes of CTEs that this CTE directly references. /// Only includes CTEs that appear in this CTE's FROM clause, /// avoiding exponential re-planning when CTEs have transitive dependencies. referenced_cte_indices: SmallVec<[usize; 2]>, /// True if WITH ... AS MATERIALIZED was specified, forcing materialization materialize_hint: bool, } /// Collect all table names referenced in a SELECT's FROM clause. /// Used to determine which earlier CTEs a CTE directly depends on. fn collect_from_clause_table_refs(select: &Select, out: &mut Vec) { collect_from_select_body(&select.body, out); collect_subquery_table_refs_in_select_exprs(select, out); } fn collect_from_select_body(body: &ast::SelectBody, out: &mut Vec) { collect_from_one_select(&body.select, out); for compound in &body.compounds { collect_from_one_select(&compound.select, out); } } fn collect_from_one_select(one: &ast::OneSelect, out: &mut Vec) { match one { ast::OneSelect::Select { from, .. } => { if let Some(from_clause) = from { collect_from_select_table(&from_clause.select, out); for join in &from_clause.joins { collect_from_select_table(&join.table, out); } } } ast::OneSelect::Values(_) => {} } } fn collect_from_select_table(table: &ast::SelectTable, out: &mut Vec) { match table { ast::SelectTable::Table(qualified_name, _, _) | ast::SelectTable::TableCall(qualified_name, _, _) => { out.push(normalize_ident(qualified_name.name.as_str())); } ast::SelectTable::Select(subselect, _) => { collect_from_clause_table_refs(subselect, out); } ast::SelectTable::Sub(from_clause, _) => { collect_from_select_table(&from_clause.select, out); for join in &from_clause.joins { collect_from_select_table(&join.table, out); } } } } /// Collect table references from subqueries embedded in expressions. fn collect_subquery_table_refs_in_select_exprs(select: &Select, out: &mut Vec) { collect_subquery_table_refs_in_one_select(&select.body.select, out); for compound in &select.body.compounds { collect_subquery_table_refs_in_one_select(&compound.select, out); } for sorted in &select.order_by { collect_subquery_table_refs_in_expr(&sorted.expr, out); } if let Some(limit) = &select.limit { collect_subquery_table_refs_in_expr(&limit.expr, out); if let Some(offset) = &limit.offset { collect_subquery_table_refs_in_expr(offset, out); } } } fn collect_subquery_table_refs_in_one_select(one: &ast::OneSelect, out: &mut Vec) { match one { ast::OneSelect::Select { columns, where_clause, group_by, .. } => { for column in columns { if let ast::ResultColumn::Expr(expr, _) = column { collect_subquery_table_refs_in_expr(expr, out); } } if let Some(expr) = where_clause { collect_subquery_table_refs_in_expr(expr, out); } if let Some(group_by) = group_by { for expr in &group_by.exprs { collect_subquery_table_refs_in_expr(expr, out); } if let Some(having) = &group_by.having { collect_subquery_table_refs_in_expr(having, out); } } } ast::OneSelect::Values(rows) => { for row in rows { for expr in row { collect_subquery_table_refs_in_expr(expr, out); } } } } } fn collect_subquery_table_refs_in_expr(expr: &Expr, out: &mut Vec) { let _ = walk_expr(expr, &mut |node: &Expr| -> Result { match node { Expr::Exists(select) | Expr::Subquery(select) => { collect_from_clause_table_refs(select, out); Ok(WalkControl::SkipChildren) } Expr::InSelect { rhs, .. } => { collect_from_clause_table_refs(rhs, out); Ok(WalkControl::SkipChildren) } _ => Ok(WalkControl::Continue), } }); } /// Valid ways to refer to the rowid of a btree table. pub const ROWID_STRS: [&str; 3] = ["rowid", "_rowid_", "oid"]; /// This function walks the expression tree and identifies aggregate /// and window functions. /// /// # Window functions /// - If `windows` is `Some`, window functions will be resolved against the /// provided set of windows or added to it if not present. /// - If `windows` is `None`, any encountered window function is treated /// as a misuse and results in a parse error. /// /// # Aggregates /// Aggregate functions are always allowed. They are collected in `aggs`. /// /// # Returns /// - `Ok(true)` if at least one aggregate function was found. /// - `Ok(false)` if no aggregates were found. /// - `Err(..)` if an invalid function usage is detected (e.g., window /// function encountered while `windows` is `None`). pub fn resolve_window_and_aggregate_functions( top_level_expr: &Expr, resolver: &Resolver, aggs: &mut Vec, mut windows: Option<&mut Vec>, ) -> Result { let mut contains_aggregates = false; walk_expr(top_level_expr, &mut |expr: &Expr| -> Result { match expr { Expr::FunctionCall { name, args, distinctness, filter_over, order_by, } => { validate_aggregate_function_tail(filter_over, order_by)?; let args_count = args.len(); let distinctness = Distinctness::from_ast(distinctness.as_ref()); match Func::resolve_function(name.as_str(), args_count) { Ok(Func::Agg(f)) => { if let Some(over_clause) = filter_over.over_clause.as_ref() { link_with_window( windows.as_deref_mut(), expr, WindowFunctionKind::Agg(f), over_clause, distinctness, )?; } else { add_aggregate_if_not_exists(aggs, expr, args, distinctness, f)?; contains_aggregates = true; } return Ok(WalkControl::SkipChildren); } Ok(Func::Window(f)) => { if let Some(over_clause) = filter_over.over_clause.as_ref() { link_with_window( windows.as_deref_mut(), expr, WindowFunctionKind::Window(f), over_clause, distinctness, )?; } else { crate::bail_parse_error!("misuse of window function: {}()", f); } return Ok(WalkControl::SkipChildren); } Err(e) => { if let Some(f) = resolver .symbol_table .resolve_function(name.as_str(), args_count) { let func = AggFunc::External(f.func.clone().into()); if let ExtFunc::Aggregate { .. } = f.as_ref().func { if let Some(over_clause) = filter_over.over_clause.as_ref() { link_with_window( windows.as_deref_mut(), expr, WindowFunctionKind::Agg(func), over_clause, distinctness, )?; } else { add_aggregate_if_not_exists( aggs, expr, args, distinctness, func, )?; contains_aggregates = true; } return Ok(WalkControl::SkipChildren); } } else { return Err(e); } } _ => { if filter_over.over_clause.is_some() { crate::bail_parse_error!( "{} may not be used as a window function", name.as_str() ); } } } } Expr::FunctionCallStar { name, filter_over } => { validate_aggregate_function_tail(filter_over, &[])?; match Func::resolve_function(name.as_str(), 0) { Ok(Func::Agg(f)) => { if let Some(over_clause) = filter_over.over_clause.as_ref() { link_with_window( windows.as_deref_mut(), expr, WindowFunctionKind::Agg(f), over_clause, Distinctness::NonDistinct, )?; } else { add_aggregate_if_not_exists( aggs, expr, &[], Distinctness::NonDistinct, f, )?; contains_aggregates = true; } return Ok(WalkControl::SkipChildren); } Ok(Func::Window(f)) => { if let Some(over_clause) = filter_over.over_clause.as_ref() { link_with_window( windows.as_deref_mut(), expr, WindowFunctionKind::Window(f), over_clause, Distinctness::NonDistinct, )?; } else { crate::bail_parse_error!("misuse of window function: {}()", f); } return Ok(WalkControl::SkipChildren); } Ok(_) => { if filter_over.over_clause.is_some() { crate::bail_parse_error!( "{} may not be used as a window function", name.as_str() ); } // Check if the function supports (*) syntax using centralized logic match crate::function::Func::resolve_function(name.as_str(), 0) { Ok(func) => { if func.supports_star_syntax() { return Ok(WalkControl::Continue); } else { crate::bail_parse_error!( "wrong number of arguments to function {}()", name.as_str() ); } } Err(_) => { crate::bail_parse_error!( "wrong number of arguments to function {}()", name.as_str() ); } } } Err(e) => match e { crate::LimboError::ParseError(e) => { crate::bail_parse_error!("{}", e); } _ => { crate::bail_parse_error!( "Invalid aggregate function: {}", name.as_str() ); } }, } } _ => {} } Ok(WalkControl::Continue) })?; Ok(contains_aggregates) } fn link_with_window( windows: Option<&mut Vec>, expr: &Expr, func: WindowFunctionKind, over_clause: &Over, distinctness: Distinctness, ) -> Result<()> { if distinctness.is_distinct() { crate::bail_parse_error!("DISTINCT is not supported for window functions"); } expr_vector_size(expr)?; if let Some(windows) = windows { let window = resolve_window(windows, over_clause)?; window.functions.push(WindowFunction { func, original_expr: expr.clone(), }); } else { let func_name = match &func { WindowFunctionKind::Agg(f) => f.as_str().to_string(), WindowFunctionKind::Window(f) => f.to_string(), }; crate::bail_parse_error!("misuse of window function: {}()", func_name); } Ok(()) } fn resolve_window<'a>(windows: &'a mut Vec, over_clause: &Over) -> Result<&'a mut Window> { match over_clause { Over::Window(window) => { if let Some(idx) = windows.iter().position(|w| w.is_equivalent(window)) { return Ok(&mut windows[idx]); } windows.push(Window::new(None, window)?); Ok(windows.last_mut().expect("just pushed, so must exist")) } Over::Name(name) => { let window_name = normalize_ident(name.as_str()); // When multiple windows share the same name, SQLite uses the most recent // definition. Iterate in reverse so we find the last definition first. for window in windows.iter_mut().rev() { if window.name.as_ref() == Some(&window_name) { return Ok(window); } } crate::bail_parse_error!("no such window: {}", window_name); } } } fn add_aggregate_if_not_exists( aggs: &mut Vec, expr: &Expr, args: &[Box], distinctness: Distinctness, func: AggFunc, ) -> Result<()> { if distinctness.is_distinct() && args.len() != 1 { crate::bail_parse_error!("DISTINCT aggregate functions must have exactly one argument"); } if aggs .iter() .all(|a| !exprs_are_equivalent(&a.original_expr, expr)) { aggs.push(Aggregate::new(func, args, expr, distinctness)); } Ok(()) } /// Plan a CTE when it's referenced in a query. /// Each call produces a fresh plan with unique internal_ids, ensuring that /// multiple references to the same CTE get independent cursor IDs, /// yield registers and so on. /// /// `count_reference`: Controls whether this call increments the CTE reference count. /// /// **Reference counting determines materialization strategy:** /// - ref_count = 1: CTE can use efficient coroutine (no materialization needed) /// - ref_count > 1: CTE must be materialized into ephemeral table for sharing /// /// **When to count (true):** /// - CTE appears in a FROM/JOIN clause (actual usage site) /// - CTE is referenced via outer_query_refs (e.g., in scalar subqueries) /// /// **When NOT to count (false):** /// - Pre-planning CTEs for outer_query_refs visibility (making CTEs available /// for potential use by nested subqueries - not actual usage) /// - Recursively planning CTE dependencies (CTE A references CTE B internally - /// B needs to be visible to A's planning, but this isn't a usage from the /// main query's perspective) #[allow(clippy::too_many_arguments)] fn plan_cte( cte_idx: usize, cte_definitions: &[CteDefinition], base_outer_query_refs: &[OuterQueryReference], resolver: &Resolver, program: &mut ProgramBuilder, connection: &Arc, count_reference: bool, ) -> Result { let cte_def = &cte_definitions[cte_idx]; // Build outer_query_refs including only the CTEs this one directly references. // By tracking direct dependencies instead of all preceding CTEs, we avoid // exponential re-planning when CTEs have transitive dependencies. let mut outer_query_refs = base_outer_query_refs.to_vec(); for &ref_idx in &cte_def.referenced_cte_indices { let ref_cte_name = &cte_definitions[ref_idx].name; // Check if this CTE has already been planned and is in outer_query_refs. // This avoids exponential re-planning when CTEs have transitive dependencies. if outer_query_refs .iter() .any(|r| &r.identifier == ref_cte_name) { continue; } // Recursively plan the referenced CTE so it's visible within this CTE's body. // Example: WITH a AS (...), b AS (SELECT * FROM a) - when planning b, we need // a to be in scope. But this internal dependency doesn't count as a "reference" // for materialization purposes - only actual usage in the main query counts. let referenced_table = plan_cte( ref_idx, cte_definitions, base_outer_query_refs, resolver, program, connection, false, )?; outer_query_refs.push(OuterQueryReference { identifier: referenced_table.identifier.clone(), internal_id: referenced_table.internal_id, table: referenced_table.table.clone(), col_used_mask: ColumnUsedMask::default(), cte_select: None, cte_explicit_columns: vec![], cte_id: Some(cte_definitions[ref_idx].cte_id), cte_definition_only: false, rowid_referenced: false, }); } // Block the CTE's own name from resolving to a schema object during // planning of its body. Without this, a same-named view would be expanded // recursively (stack overflow) and a same-named table would give wrong // results. `parse_table` checks this and produces "circular reference". program.push_cte_being_defined(cte_def.name.clone()); // Plan this CTE with fresh IDs let cte_plan = prepare_select_plan( cte_def.select.clone(), resolver, program, &outer_query_refs, QueryDestination::placeholder_for_subquery(), connection, ); program.pop_cte_being_defined(); let cte_plan = cte_plan?; // CTEs can be either simple SELECT or compound SELECT (UNION/INTERSECT/EXCEPT) let explicit_cols = if cte_def.explicit_columns.is_empty() { None } else { Some(cte_def.explicit_columns.as_slice()) }; // Track CTE reference count globally for materialization decisions during emission. // Multi-ref CTEs should be materialized; single-ref CTEs can use coroutine. // Only count actual references (FROM/JOIN usage), not pre-planning or recursive deps. if count_reference { program.increment_cte_reference(cte_def.cte_id); // Validate explicit column count only on actual references (matching SQLite behavior, // which defers this check until the CTE is used). if let Some(cols) = explicit_cols { let result_col_count = cte_plan .select_result_columns() .expect("should be a select plan") .len(); if cols.len() != result_col_count { crate::bail_parse_error!( "table {} has {} columns but {} column names were provided", cte_def.name, result_col_count, cols.len() ); } } } match cte_plan { Plan::Select(_) | Plan::CompoundSelect { .. } => JoinedTable::new_subquery_from_plan( cte_def.name.clone(), cte_plan, None, program.table_reference_counter.next(), explicit_cols, Some(cte_def.cte_id), // Pass the CTE identity for sharing materialized data cte_def.materialize_hint, ), Plan::Delete(_) | Plan::Update(_) => { crate::bail_parse_error!("DELETE/UPDATE queries are not supported in CTEs") } } } /// Plan CTEs from a WITH clause and add them as outer query references. /// This is used by DML statements (DELETE, UPDATE) to make CTEs available /// for subqueries in WHERE and SET clauses. pub fn plan_ctes_as_outer_refs( with: Option, resolver: &Resolver, program: &mut ProgramBuilder, table_references: &mut TableReferences, connection: &Arc, ) -> Result<()> { let Some(with) = with else { return Ok(()); }; if with.recursive { crate::bail_parse_error!("Recursive CTEs are not yet supported"); } for cte in with.ctes { // Normalize explicit column names let explicit_columns: Vec = cte .columns .iter() .map(|c| normalize_ident(c.col_name.as_str())) .collect(); let cte_name = normalize_ident(cte.tbl_name.as_str()); // Check for duplicate CTE names if table_references .outer_query_refs() .iter() .any(|r| r.identifier == cte_name) { crate::bail_parse_error!("duplicate WITH table name: {}", cte.tbl_name.as_str()); } // Clone the CTE select AST before planning, so we can store it for re-planning let cte_select_ast = cte.select.clone(); // AS MATERIALIZED forces materialization let materialize_hint = cte.materialized == Materialized::Yes; // Block the CTE's own name from resolving to a schema object during // planning of its body (see push_cte_being_defined). program.push_cte_being_defined(cte_name.clone()); // Plan the CTE SELECT let cte_plan = prepare_select_plan( cte.select, resolver, program, table_references.outer_query_refs(), QueryDestination::placeholder_for_subquery(), connection, ); program.pop_cte_being_defined(); let cte_plan = cte_plan?; // Convert plan to JoinedTable to extract column info let explicit_cols = if explicit_columns.is_empty() { None } else { Some(explicit_columns.as_slice()) }; let joined_table = match cte_plan { Plan::Select(_) | Plan::CompoundSelect { .. } => JoinedTable::new_subquery_from_plan( cte_name.clone(), cte_plan, None, program.table_reference_counter.next(), explicit_cols, None, // CTEs in DML don't share materialized data (TODO: implement if needed) materialize_hint, )?, Plan::Delete(_) | Plan::Update(_) => { crate::bail_parse_error!("Only SELECT queries are supported in CTEs") } }; // Add CTE as outer query reference so it's available to subqueries. // cte_definition_only = true: the CTE is only for subquery FROM lookup // (e.g. UPDATE t SET b = (SELECT v FROM c)), not for direct column // resolution (e.g. UPDATE t SET b = c.v which SQLite rejects as // "no such column"). table_references.add_outer_query_reference(OuterQueryReference { identifier: cte_name, internal_id: joined_table.internal_id, table: joined_table.table, col_used_mask: ColumnUsedMask::default(), cte_select: Some(cte_select_ast), cte_explicit_columns: explicit_columns, cte_id: None, // DML CTEs don't track CTE sharing (TODO: implement if needed) cte_definition_only: true, rowid_referenced: false, }); } Ok(()) } fn parse_from_clause_table( table: ast::SelectTable, resolver: &Resolver, program: &mut ProgramBuilder, table_references: &mut TableReferences, vtab_predicates: &mut Vec, cte_definitions: &[CteDefinition], connection: &Arc, ) -> Result<()> { match table { ast::SelectTable::Table(qualified_name, maybe_alias, indexed) => parse_table( table_references, resolver, program, cte_definitions, vtab_predicates, &qualified_name, maybe_alias.as_ref(), &[], indexed, connection, ), ast::SelectTable::Select(subselect, maybe_alias) => { // For inline subqueries, we plan all CTEs once and pass them as outer_query_refs. // This allows the subquery to reference CTEs defined in the parent's WITH clause. let mut outer_query_refs_for_subquery = table_references.outer_query_refs().to_vec(); let base_outer_query_refs_for_subquery = base_outer_refs_for_cte_planning( table_references.outer_query_refs(), cte_definitions, ); for (idx, cte_def) in cte_definitions.iter().enumerate() { // Check if this CTE has already been planned and is in outer_query_refs. // This avoids exponential re-planning when CTEs have transitive dependencies. if outer_query_refs_for_subquery .iter() .any(|r| r.identifier == cte_def.name) { continue; } // Plan each CTE so it's visible to this inline subquery's FROM clause. // Example: WITH cte AS (...) SELECT * FROM (SELECT * FROM cte) sub // The inline subquery "(SELECT * FROM cte)" needs cte in scope, but // planning this visibility isn't a reference - the actual reference // happens when the inline subquery's FROM clause resolves "cte". let cte_table = plan_cte( idx, cte_definitions, &base_outer_query_refs_for_subquery, resolver, program, connection, false, )?; outer_query_refs_for_subquery.push(OuterQueryReference { identifier: cte_def.name.clone(), internal_id: cte_table.internal_id, table: cte_table.table, col_used_mask: ColumnUsedMask::default(), cte_select: Some(cte_def.select.clone()), cte_explicit_columns: cte_def.explicit_columns.clone(), cte_id: Some(cte_def.cte_id), cte_definition_only: false, rowid_referenced: false, }); } let subplan = prepare_select_plan( subselect, resolver, program, &outer_query_refs_for_subquery, QueryDestination::placeholder_for_subquery(), connection, )?; match &subplan { Plan::Select(_) | Plan::CompoundSelect { .. } => {} Plan::Delete(_) | Plan::Update(_) => { crate::bail_parse_error!( "DELETE/UPDATE queries are not supported in FROM clause subqueries" ); } } let cur_table_index = table_references.joined_tables().len(); let identifier = maybe_alias .map(|a| match a { ast::As::As(id) => id, ast::As::Elided(id) => id, }) .map(|id| normalize_ident(id.as_str())) .unwrap_or_else(|| format!("subquery_{cur_table_index}")); table_references.add_joined_table(JoinedTable::new_subquery_from_plan( identifier, subplan, None, program.table_reference_counter.next(), None, // No explicit columns for regular subqueries None, // Regular inline subqueries don't have a CTE identity false, // No materialize hint for inline subqueries )?); Ok(()) } ast::SelectTable::TableCall(qualified_name, args, maybe_alias) => parse_table( table_references, resolver, program, cte_definitions, vtab_predicates, &qualified_name, maybe_alias.as_ref(), &args, None, // table-valued functions don't support INDEXED BY connection, ), ast::SelectTable::Sub(..) => { crate::bail_parse_error!("Parenthesized FROM clause subqueries are not supported") } } } #[allow(clippy::too_many_arguments)] fn parse_table( table_references: &mut TableReferences, resolver: &Resolver, program: &mut ProgramBuilder, cte_definitions: &[CteDefinition], vtab_predicates: &mut Vec, qualified_name: &QualifiedName, maybe_alias: Option<&As>, args: &[Box], indexed: Option, connection: &Arc, ) -> Result<()> { let normalized_qualified_name = normalize_ident(qualified_name.name.as_str()); let database_id = resolver.resolve_database_id(qualified_name)?; let table_name = &qualified_name.name; // Check if the FROM clause table is referring to a CTE in the current scope. // Each reference gets a freshly planned CTE to ensure unique internal_ids and cursor IDs. if let Some(cte_idx) = cte_definitions .iter() .position(|cte| cte.name == normalized_qualified_name) { let planning_outer_query_refs = base_outer_refs_for_cte_planning(table_references.outer_query_refs(), cte_definitions); // This is an actual CTE reference in the FROM/JOIN clause - count it let mut cte_table = plan_cte( cte_idx, cte_definitions, &planning_outer_query_refs, resolver, program, connection, true, // Actual FROM/JOIN reference - count it )?; // If there's an alias provided, update the identifier to use that alias if let Some(a) = maybe_alias { let alias = match a { ast::As::As(id) => id, ast::As::Elided(id) => id, }; cte_table.identifier = normalize_ident(alias.as_str()); } // Mark the pre-planned outer_query_ref as "CTE definition only" so it is // still available for CTE lookup in subquery FROM clauses (e.g. // EXISTS (SELECT 1 FROM ...)), but no longer participates in // column resolution. Column resolution now goes through the joined_table // which has the alias (if any) or the original name. table_references.mark_outer_query_ref_cte_definition_only(&normalized_qualified_name); table_references.add_joined_table(cte_table); return Ok(()); } // A non-recursive CTE's body cannot reference its own name. The CTE name // shadows any same-named schema object, but without RECURSIVE it's circular. if program.is_cte_being_defined(&normalized_qualified_name) { crate::bail_parse_error!("circular reference: {}", table_name.as_str()); } // Check if the table is a CTE from an outer scope (e.g., a CTE referencing another CTE). // This handles cases like: WITH a AS (...), b AS (SELECT ... FROM a) SELECT * FROM b; // When planning b's body, 'a' is in outer_query_refs. if let Some(outer_ref) = table_references.find_outer_query_ref_by_identifier(&normalized_qualified_name) { // If this is a CTE reference (via outer_query_refs), count it for materialization decisions. // This handles scalar subqueries that reference CTEs. if let Some(cte_id) = outer_ref.cte_id { program.increment_cte_reference(cte_id); } let alias = maybe_alias .map(|a| match a { ast::As::As(id) => id, ast::As::Elided(id) => id, }) .map(|a| normalize_ident(a.as_str())); // Clone fields we need before dropping the borrow on table_references. let cte_select = outer_ref.cte_select.clone(); let cte_explicit_columns = outer_ref.cte_explicit_columns.clone(); let cte_id = outer_ref.cte_id; let outer_table = outer_ref.table.clone(); let materialize_hint = match &outer_table { Table::FromClauseSubquery(subquery) => subquery.materialize_hint(), _ => false, }; if let Some(cte_ast) = cte_select { // Re-plan the CTE from its original AST to get fresh internal_ids. // This prevents cursor key collisions when the same CTE is // referenced multiple times in the same scope. let cte_plan = prepare_select_plan( cte_ast, resolver, program, table_references.outer_query_refs(), QueryDestination::placeholder_for_subquery(), connection, )?; let explicit_cols = if cte_explicit_columns.is_empty() { None } else { Some(cte_explicit_columns.as_slice()) }; // Validate explicit column count on actual CTE reference (matching SQLite // behavior, which defers this check until the CTE is used). if let Some(cols) = explicit_cols { let result_col_count = cte_plan .select_result_columns() .expect("should be a select plan") .len(); if cols.len() != result_col_count { crate::bail_parse_error!( "table {} has {} columns but {} column names were provided", normalized_qualified_name, result_col_count, cols.len() ); } } // Use the CTE name for the subquery name so query plans show // "SCAN cte_name AS alias" instead of just "SCAN alias". let mut jt = JoinedTable::new_subquery_from_plan( normalized_qualified_name.clone(), cte_plan, None, program.table_reference_counter.next(), explicit_cols, cte_id, materialize_hint, )?; if let Some(alias) = alias { jt.identifier = alias; } jt.database_id = database_id; table_references.add_joined_table(jt); } else { let internal_id = program.table_reference_counter.next(); table_references.add_joined_table(JoinedTable { op: Operation::default_scan_for(&outer_table), table: outer_table, identifier: alias.unwrap_or(normalized_qualified_name), internal_id, join_info: None, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id, indexed: None, }); } return Ok(()); } // Resolve table using connection's with_schema method let table = resolver.with_schema(database_id, |schema| schema.get_table(table_name.as_str())); if let Some(table) = table { let alias = maybe_alias .map(|a| match a { ast::As::As(id) => id, ast::As::Elided(id) => id, }) .map(|a| normalize_ident(a.as_str())); let internal_id = program.table_reference_counter.next(); let tbl_ref = if let Table::Virtual(tbl) = table.as_ref() { transform_args_into_where_terms(args, internal_id, vtab_predicates, table.as_ref())?; Table::Virtual(tbl.clone()) } else if let Table::BTree(table) = table.as_ref() { Table::BTree(table.clone()) } else { return Err(crate::LimboError::InvalidArgument( "Table type not supported".to_string(), )); }; table_references.add_joined_table(JoinedTable { op: Operation::default_scan_for(&tbl_ref), table: tbl_ref, identifier: alias.unwrap_or(normalized_qualified_name), internal_id, join_info: None, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id, indexed, }); return Ok(()); }; let regular_view = resolver.with_schema(database_id, |schema| schema.get_view(table_name.as_str())); if let Some(view) = regular_view { // Views are essentially query aliases, so just Expand the view as a subquery view.process()?; let mut view_select = view.select_stmt.clone(); if let ast::OneSelect::Select { ref mut columns, .. } = view_select.body.select { for (col, result_col) in view.columns.iter().zip(columns.iter_mut()) { if let (Some(name_str), ast::ResultColumn::Expr(_, ref mut alias)) = (&col.name, result_col) { *alias = Some(ast::As::As(ast::Name::exact(name_str.clone()))); } } } let subselect = Box::new(view_select); // Use the view name as alias if no explicit alias was provided let view_alias = maybe_alias .cloned() .or_else(|| Some(ast::As::As(table_name.clone()))); // Views are pre-defined definitions — their body resolves against the // schema only, not against CTEs from the calling query context. // Pass empty cte_definitions and temporarily clear the ctes_being_defined // stack so that e.g. `WITH t AS (...) SELECT * FROM v` where view v // references table t will correctly use the real table, not the CTE. let saved_ctes = program.take_ctes_being_defined(); let result = parse_from_clause_table( ast::SelectTable::Select(*subselect, view_alias), resolver, program, table_references, vtab_predicates, &[], connection, ); program.restore_ctes_being_defined(saved_ctes); view.done(); return result; } let view = resolver.with_schema(database_id, |schema| { schema.get_materialized_view(table_name.as_str()) }); if let Some(view) = view { // First check if the DBSP state table exists with the correct version let has_compatible_state = resolver.with_schema(database_id, |schema| { schema.has_compatible_dbsp_state_table(table_name.as_str()) }); if !has_compatible_state { use crate::incremental::compiler::DBSP_CIRCUIT_VERSION; return Err(crate::LimboError::InternalError(format!( "Materialized view '{table_name}' has an incompatible version. \n\ The current version is {DBSP_CIRCUIT_VERSION}, but the view was created with a different version. \n\ Please DROP and recreate the view to use it." ))); } // Check if this materialized view has persistent storage let view_guard = view.lock(); let root_page = view_guard.get_root_page(); if root_page == 0 { drop(view_guard); return Err(crate::LimboError::InternalError( "Materialized view has no storage allocated".to_string(), )); } // This is a materialized view with storage - treat it as a regular BTree table // Create a BTreeTable from the view's metadata let btree_table = Arc::new(crate::schema::BTreeTable { name: view_guard.name().to_string(), root_page, columns: view_guard.column_schema.flat_columns(), primary_key_columns: Vec::new(), has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }); drop(view_guard); let alias = maybe_alias .map(|a| match a { ast::As::As(id) => id, ast::As::Elided(id) => id, }) .map(|a| normalize_ident(a.as_str())); table_references.add_joined_table(JoinedTable { op: Operation::Scan(Scan::BTreeTable { iter_dir: IterationDirection::Forwards, index: None, }), table: Table::BTree(btree_table), identifier: alias.unwrap_or(normalized_qualified_name), internal_id: program.table_reference_counter.next(), join_info: None, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id, indexed: None, }); return Ok(()); } // CTEs are transformed into FROM clause subqueries. // If we find a CTE with this name in our outer query references, // we can use it as a joined table, but we must clone it since it's not MATERIALIZED. // // For other types of tables in the outer query references, we do not add them as joined tables, // because the query can simply _reference_ them in e.g. the SELECT columns or the WHERE clause, // but it's not part of the join order. if let Some(outer_ref) = table_references.find_outer_query_ref_by_identifier(&normalized_qualified_name) { if matches!(outer_ref.table, Table::FromClauseSubquery(_)) { table_references.add_joined_table(JoinedTable { op: Operation::default_scan_for(&outer_ref.table), table: outer_ref.table.clone(), identifier: outer_ref.identifier.clone(), internal_id: program.table_reference_counter.next(), join_info: None, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id, indexed: None, }); return Ok(()); } } // Check if this is an incompatible view let is_incompatible = resolver.with_schema(database_id, |schema| { schema .incompatible_views .contains(&normalized_qualified_name) }); if is_incompatible { use crate::incremental::compiler::DBSP_CIRCUIT_VERSION; crate::bail_parse_error!( "Materialized view '{}' has an incompatible version. \n\ The view was created with a different DBSP version than the current version ({}). \n\ Please DROP and recreate the view to use it.", normalized_qualified_name, DBSP_CIRCUIT_VERSION ); } crate::bail_parse_error!("no such table: {}", normalized_qualified_name); } fn transform_args_into_where_terms( args: &[Box], internal_id: TableInternalId, predicates: &mut Vec, table: &Table, ) -> Result<()> { let mut args_iter = args.iter(); let mut hidden_count = 0; for (i, col) in table.columns().iter().enumerate() { if !col.hidden() { continue; } hidden_count += 1; if let Some(arg_expr) = args_iter.next() { let column_expr = Expr::Column { database: None, table: internal_id, column: i, is_rowid_alias: col.is_rowid_alias(), }; let expr = match arg_expr.as_ref() { Expr::Literal(Null) => Expr::IsNull(Box::new(column_expr)), other => Expr::Binary( column_expr.into(), ast::Operator::Equals, other.clone().into(), ), }; predicates.push(expr); } } if args_iter.next().is_some() { return Err(crate::LimboError::ParseError(format!( "Too many arguments for {}: expected at most {}, got {}", table.get_name(), hidden_count, hidden_count + 1 + args_iter.count() ))); } Ok(()) } /// Build a stable outer-scope reference set for CTE planning. /// Current WITH-scope CTE entries are excluded to avoid cloning/replanning cascades. fn base_outer_refs_for_cte_planning( refs: &[OuterQueryReference], cte_definitions: &[CteDefinition], ) -> Vec { refs.iter() .filter(|r| !cte_definitions.iter().any(|cte| cte.name == r.identifier)) .cloned() .map(|mut r| { if matches!(r.table, Table::FromClauseSubquery(_)) { r.cte_select = None; } r }) .collect() } #[allow(clippy::too_many_arguments)] pub fn parse_from( from: Option, resolver: &Resolver, program: &mut ProgramBuilder, with: Option, preplan_ctes_for_non_from_subqueries: bool, out_where_clause: &mut Vec, vtab_predicates: &mut Vec, table_references: &mut TableReferences, connection: &Arc, ) -> Result<()> { // Collect CTE definitions instead of planning them immediately. // Each CTE reference will be planned fresh when encountered, ensuring unique internal_ids. let mut cte_definitions: Vec = vec![]; if let Some(with) = with { if with.recursive { crate::bail_parse_error!("Recursive CTEs are not yet supported"); } for (idx, cte) in with.ctes.into_iter().enumerate() { // Normalize explicit column names let explicit_columns: Vec = cte .columns .iter() .map(|c| normalize_ident(c.col_name.as_str())) .collect(); let cte_name_normalized = normalize_ident(cte.tbl_name.as_str()); if cte_definitions .iter() .any(|d| d.name == cte_name_normalized) { crate::bail_parse_error!("duplicate WITH table name: {}", cte.tbl_name.as_str()); } // Collect table names referenced in this CTE's FROM clause. let mut referenced_tables = Vec::new(); collect_from_clause_table_refs(&cte.select, &mut referenced_tables); // Find which preceding CTEs are directly referenced by this CTE. // This avoids exponential re-planning when CTEs have transitive dependencies. let referenced_cte_indices: SmallVec<[usize; 2]> = (0..idx) .filter(|&i| referenced_tables.contains(&cte_definitions[i].name)) .collect(); // AS MATERIALIZED forces materialization; AS NOT MATERIALIZED prevents it let materialize_hint = cte.materialized == Materialized::Yes; cte_definitions.push(CteDefinition { cte_id: program.alloc_cte_id(), name: cte_name_normalized, select: cte.select, explicit_columns, referenced_cte_indices, materialize_hint, }); } if preplan_ctes_for_non_from_subqueries { // Pre-plan all CTEs and add them to outer_query_refs for visibility. // This is needed when non-FROM expressions contain subqueries that may reference // CTEs from this WITH scope. let base_outer_query_refs = base_outer_refs_for_cte_planning( table_references.outer_query_refs(), &cte_definitions, ); for (idx, cte_def) in cte_definitions.iter().enumerate() { let cte_table = plan_cte( idx, &cte_definitions, &base_outer_query_refs, resolver, program, connection, false, )?; table_references.add_outer_query_reference(OuterQueryReference { identifier: cte_def.name.clone(), internal_id: cte_table.internal_id, table: cte_table.table, col_used_mask: ColumnUsedMask::default(), cte_select: Some(cte_def.select.clone()), cte_explicit_columns: cte_def.explicit_columns.clone(), cte_id: Some(cte_def.cte_id), // Preplanned CTE refs are for subquery FROM lookup only. They are not // visible as column sources unless the CTE is explicitly referenced in // this scope's FROM/JOIN clause. cte_definition_only: true, rowid_referenced: false, }); } } } // Process FROM clause if present if let Some(from_owned) = from { let select_owned = from_owned.select; let joins_owned = from_owned.joins; parse_from_clause_table( *select_owned, resolver, program, table_references, vtab_predicates, &cte_definitions, connection, )?; for join in joins_owned.into_iter() { parse_join( join, resolver, program, &cte_definitions, out_where_clause, vtab_predicates, table_references, connection, )?; } } Ok(()) } pub fn parse_where( where_clause: Option<&Expr>, table_references: &mut TableReferences, result_columns: Option<&[ResultSetColumn]>, out_where_clause: &mut Vec, resolver: &Resolver, ) -> Result<()> { if let Some(where_expr) = where_clause { let start_idx = out_where_clause.len(); break_predicate_at_and_boundaries(where_expr, out_where_clause); for expr in out_where_clause[start_idx..].iter_mut() { bind_and_rewrite_expr( &mut expr.expr, Some(table_references), result_columns, resolver, BindingBehavior::TryCanonicalColumnsFirst, )?; let _ = walk_expr_mut(&mut expr.expr, &mut |e: &mut Expr| -> Result { if let Expr::Between { lhs, not, start, end, } = e { let lhs_expr = std::mem::take(lhs.as_mut()); let start_expr = std::mem::take(start.as_mut()); let end_expr = std::mem::take(end.as_mut()); let (lower, upper, combine_op) = if *not { ( Expr::Binary( Box::new(start_expr), ast::Operator::Greater, Box::new(lhs_expr.clone()), ), Expr::Binary( Box::new(lhs_expr), ast::Operator::Greater, Box::new(end_expr), ), ast::Operator::Or, ) } else { ( Expr::Binary( Box::new(start_expr), ast::Operator::LessEquals, Box::new(lhs_expr.clone()), ), Expr::Binary( Box::new(lhs_expr), ast::Operator::LessEquals, Box::new(end_expr), ), ast::Operator::And, ) }; *e = Expr::Binary(Box::new(lower), combine_op, Box::new(upper)); } Ok(WalkControl::Continue) }); } // BETWEEN in WHERE is rewritten to binary terms here so each side can be // considered independently by constraint extraction and range planning. // Re-break any ANDs that were created so they become separate WhereTerms for // constraint extraction. let mut i = start_idx; while i < out_where_clause.len() { if matches!( &out_where_clause[i].expr, Expr::Binary(_, ast::Operator::And, _) ) { let term = out_where_clause.remove(i); let mut new_terms: Vec = Vec::new(); break_predicate_at_and_boundaries(&term.expr, &mut new_terms); // Preserve from_outer_join from the original term for new_term in new_terms.iter_mut() { new_term.from_outer_join = term.from_outer_join; } let count = new_terms.len(); for (j, new_term) in new_terms.into_iter().enumerate() { out_where_clause.insert(i + j, new_term); } i += count; } else { i += 1; } } Ok(()) } else { Ok(()) } } /** Returns the earliest point at which a WHERE term can be evaluated. For expressions referencing tables, this is the innermost loop that contains a row for each table referenced in the expression. For expressions not referencing any tables (e.g. constants), this is before the main loop is opened, because they do not need any table data. */ pub fn determine_where_to_eval_term( term: &WhereTerm, join_order: &[JoinOrderMember], subqueries: &[NonFromClauseSubquery], table_references: Option<&TableReferences>, ) -> Result { if let Some(table_id) = term.from_outer_join { return Ok(EvalAt::Loop( join_order .iter() .position(|t| t.table_id == table_id) .unwrap_or(usize::MAX), )); } determine_where_to_eval_expr(&term.expr, join_order, subqueries, table_references) } /// A bitmask representing a set of tables in a query plan. /// Tables are numbered by their index in [SelectPlan::joined_tables]. /// In the bitmask, the first bit is unused so that a mask with all zeros /// can represent "no tables". /// /// E.g. table 0 is represented by bit index 1, table 1 by bit index 2, etc. /// /// Usage in Join Optimization /// /// In join optimization, [TableMask] is used to: /// - Generate subsets of tables for dynamic programming in join optimization /// - Ensure tables are joined in valid orders (e.g., respecting LEFT JOIN order) /// /// Usage with constraints (WHERE clause) /// /// [TableMask] helps determine: /// - Which tables are referenced in a constraint /// - When a constraint can be applied as a join condition (all referenced tables must be on the left side of the table being joined) /// /// Note that although [TableReference]s contain an internal ID as well, in join order optimization /// the [TableMask] refers to the index of the table in the original join order, not the internal ID. /// This is simply because we want to represent the tables as a contiguous set of bits, and the internal ID /// might not be contiguous after e.g. subquery unnesting or other transformations. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct TableMask(pub u128); impl std::ops::BitOrAssign for TableMask { fn bitor_assign(&mut self, rhs: Self) { self.0 |= rhs.0; } } impl TableMask { /// Creates a new empty table mask. /// /// The initial mask represents an empty set of tables. pub fn new() -> Self { Self(0) } /// Returns true if the mask represents an empty set of tables. pub fn is_empty(&self) -> bool { self.0 == 0 } /// Creates a new mask that is the same as this one but without the specified table. pub fn without_table(&self, table_no: usize) -> Self { turso_assert_less_than!(table_no, 127, "table_no must be less than 127"); Self(self.0 ^ (1 << (table_no + 1))) } /// Creates a table mask from raw bits. /// /// The bits are shifted left by 1 to maintain the convention that table 0 is at bit 1. pub fn from_bits(bits: u128) -> Self { Self(bits << 1) } /// Creates a table mask from an iterator of table numbers. pub fn from_table_number_iter(iter: impl Iterator) -> Self { iter.fold(Self::new(), |mut mask, table_no| { turso_assert_less_than!(table_no, 127, "table_no must be less than 127"); mask.add_table(table_no); mask }) } /// Adds a table to the mask. pub fn add_table(&mut self, table_no: usize) { turso_assert_less_than!(table_no, 127, "table_no must be less than 127"); self.0 |= 1 << (table_no + 1); } /// Returns true if the mask contains the specified table. pub fn contains_table(&self, table_no: usize) -> bool { turso_assert_less_than!(table_no, 127, "table_no must be less than 127"); self.0 & (1 << (table_no + 1)) != 0 } /// Returns true if this mask contains all tables in the other mask. pub fn contains_all(&self, other: &TableMask) -> bool { self.0 & other.0 == other.0 } /// Returns the number of tables in the mask. pub fn table_count(&self) -> usize { self.0.count_ones() as usize } /// Returns true if this mask shares any tables with the other mask. pub fn intersects(&self, other: &TableMask) -> bool { self.0 & other.0 != 0 } /// Iterate the table indices present in this mask. pub fn tables_iter(&self) -> impl Iterator + '_ { (0..127).filter(move |table_no| self.contains_table(*table_no)) } } /// Returns a [TableMask] representing the tables referenced in the given expression. /// /// This includes outer references from subqueries, even if the subquery plan has /// already been consumed, by relying on the cached outer reference ids. /// Used in the optimizer for constraint analysis. pub fn table_mask_from_expr( top_level_expr: &Expr, table_references: &TableReferences, subqueries: &[NonFromClauseSubquery], ) -> Result { let mut mask = TableMask::new(); walk_expr(top_level_expr, &mut |expr: &Expr| -> Result { match expr { Expr::Column { table, .. } | Expr::RowId { table, .. } => { if let Some(table_idx) = table_references .joined_tables() .iter() .position(|t| t.internal_id == *table) { mask.add_table(table_idx); } else if table_references .find_outer_query_ref_by_internal_id(*table) .is_none() { // Tables from outer query scopes are guaranteed to be 'in scope' for this query, // so they don't need to be added to the table mask. However, if the table is not found // in the outer scope either, then it's an invalid reference. crate::bail_parse_error!("table not found in joined_tables"); } } // Given something like WHERE t.a = (SELECT ...), we can only evaluate that expression // when all both table 't' and all outer scope tables referenced by the subquery OR its nested subqueries are in scope. // Hence, the tables referenced in subqueries must be added to the table mask. Expr::SubqueryResult { subquery_id, .. } => { let Some(subquery) = subqueries.iter().find(|s| s.internal_id == *subquery_id) else { crate::bail_parse_error!("subquery not found"); }; match &subquery.state { SubqueryState::Unevaluated { plan } => { let used_outer_query_refs = plan .as_ref() .unwrap() .table_references .outer_query_refs() .iter() .filter(|t| t.is_used()); for outer_query_ref in used_outer_query_refs { if let Some(table_idx) = table_references .joined_tables() .iter() .position(|t| t.internal_id == outer_query_ref.internal_id) { mask.add_table(table_idx); } } } SubqueryState::Evaluated { outer_ref_ids, .. } => { // Now hash-join plans can now translate some correlated subqueries early, we // still revisit those predicates even though the plan has already been consumed. // Without this cache we'd panic or lose the knowledge that an outer table was required. // // Example: `SELECT t.a FROM t WHERE t.a = (SELECT MAX(x.a) FROM x WHERE x.b = t.b)`. // The outer expression `x.b = t.b` is visited after the subquery is translated, // so we need cached `outer_ref_ids` to realize that `t` must already be in scope. for outer_ref_id in outer_ref_ids { if let Some(table_idx) = table_references .joined_tables() .iter() .position(|t| t.internal_id == *outer_ref_id) { mask.add_table(table_idx); } } } } } _ => {} } Ok(WalkControl::Continue) })?; Ok(mask) } /// Determines the earliest loop where an expression can be safely evaluated. /// /// When a referenced table is not found in `join_order`, we check if it's a hash-join /// build table and map the condition to the probe loop where its rows are produced. /// Subquery references are also respected, even after their plans are consumed. pub fn determine_where_to_eval_expr( top_level_expr: &Expr, join_order: &[JoinOrderMember], subqueries: &[NonFromClauseSubquery], table_references: Option<&TableReferences>, ) -> Result { // If the expression references no tables, it can be evaluated before any table loops are opened. let mut eval_at: EvalAt = EvalAt::BeforeLoop; walk_expr(top_level_expr, &mut |expr: &Expr| -> Result { match expr { Expr::Column { table, .. } | Expr::RowId { table, .. } => { let Some(join_idx) = join_order.iter().position(|t| t.table_id == *table) else { // Table not found in join_order. Check if it's a hash join build table. // If so, we need to evaluate the condition at the probe table's loop position. if let Some(tables) = table_references { for (probe_idx, member) in join_order.iter().enumerate() { let probe_table = &tables.joined_tables()[member.original_idx]; if let Operation::HashJoin(ref hj) = probe_table.op { let build_table = &tables.joined_tables()[hj.build_table_idx]; if build_table.internal_id == *table { // This table is the build side of a hash join. // Evaluate the condition at the probe table's loop position. eval_at = eval_at.max(EvalAt::Loop(probe_idx)); return Ok(WalkControl::Continue); } } } } // Must be an outer query reference; in that case, the table is already in scope. return Ok(WalkControl::Continue); }; eval_at = eval_at.max(EvalAt::Loop(join_idx)); } // Given something like WHERE t.a = (SELECT ...), we can only evaluate that expression // when all both table 't' and all outer scope tables referenced by the subquery OR its nested subqueries are in scope. Expr::SubqueryResult { subquery_id, .. } => { let Some(subquery) = subqueries.iter().find(|s| s.internal_id == *subquery_id) else { crate::bail_parse_error!("subquery not found"); }; match &subquery.state { SubqueryState::Evaluated { evaluated_at, .. } => { eval_at = eval_at.max(*evaluated_at); } SubqueryState::Unevaluated { plan } => { let used_outer_refs = plan .as_ref() .unwrap() .table_references .outer_query_refs() .iter() .filter(|t| t.is_used()); for outer_ref in used_outer_refs { let join_idx = join_order .iter() .position(|t| t.table_id == outer_ref.internal_id) .or_else(|| { let tables = table_references?; for (probe_idx, member) in join_order.iter().enumerate() { let probe_table = &tables.joined_tables()[member.original_idx]; if let Operation::HashJoin(ref hj) = probe_table.op { let build_table = &tables.joined_tables()[hj.build_table_idx]; if build_table.internal_id == outer_ref.internal_id { return Some(probe_idx); } } } None }); if let Some(join_idx) = join_idx { eval_at = eval_at.max(EvalAt::Loop(join_idx)); } } return Ok(WalkControl::Continue); } } } _ => {} } Ok(WalkControl::Continue) })?; Ok(eval_at) } #[allow(clippy::too_many_arguments)] fn parse_join( join: ast::JoinedSelectTable, resolver: &Resolver, program: &mut ProgramBuilder, cte_definitions: &[CteDefinition], out_where_clause: &mut Vec, vtab_predicates: &mut Vec, table_references: &mut TableReferences, connection: &Arc, ) -> Result<()> { let ast::JoinedSelectTable { operator: join_operator, table, constraint, } = join; parse_from_clause_table( table.as_ref().clone(), resolver, program, table_references, vtab_predicates, cte_definitions, connection, )?; let is_cross = matches!(join_operator, ast::JoinOperator::TypedJoin(Some(jt)) if jt.contains(JoinType::CROSS)); let (outer, natural, full_outer) = match join_operator { ast::JoinOperator::TypedJoin(Some(join_type)) => { let is_right = join_type.contains(JoinType::RIGHT); let is_left = join_type.contains(JoinType::LEFT); let is_outer = join_type.contains(JoinType::OUTER); let is_natural = join_type.contains(JoinType::NATURAL); // FULL OUTER: LEFT+RIGHT or bare OUTER let is_full = (is_left && is_right) || (is_outer && !is_left && !is_right); if is_right && !is_left && !is_full { // RIGHT JOIN: swap the last two tables, then treat as LEFT JOIN. let len = table_references.joined_tables().len(); // Only valid for a two-table FROM clause; with prior joins the swap // would break ON clause column references. if len > 2 { crate::bail_parse_error!( "RIGHT JOIN following another join is not yet supported. \ Try rewriting as LEFT JOIN or using a subquery." ); } table_references.joined_tables_mut().swap(len - 2, len - 1); table_references.set_right_join_swapped(); // outer flag goes on the originally-left table (now rightmost after swap). (true, is_natural, false) } else if is_full { (true, is_natural, true) } else { (is_outer || is_left, is_natural, false) } } _ => (false, false, false), }; if natural && constraint.is_some() { crate::bail_parse_error!("NATURAL JOIN cannot be combined with ON or USING clause"); } // this is called once for each join, so we only need to check the rightmost table // against all previous tables for duplicates let rightmost_table = table_references.joined_tables().last().unwrap(); let has_duplicate = table_references .joined_tables() .iter() .take(table_references.joined_tables().len() - 1) .any(|t| t.identifier == rightmost_table.identifier); if has_duplicate && !natural && constraint .as_ref() .is_none_or(|c| !matches!(c, ast::JoinConstraint::Using(_))) { // Duplicate table names are only allowed for NATURAL or USING joins crate::bail_parse_error!( "table name {} specified more than once - use an alias to disambiguate", rightmost_table.identifier ); } let constraint = if natural { turso_assert_greater_than_or_equal!(table_references.joined_tables().len(), 2); // NATURAL JOIN is first transformed into a USING join with the common columns let mut distinct_names: Vec = vec![]; // TODO: O(n^2) maybe not great for large tables or big multiway joins // SQLite doesn't use HIDDEN columns for NATURAL joins: https://www3.sqlite.org/src/info/ab09ef427181130b for right_col in rightmost_table.columns().iter().filter(|col| !col.hidden()) { let mut found_match = false; for left_table in table_references .joined_tables() .iter() .take(table_references.joined_tables().len() - 1) { for left_col in left_table.columns().iter().filter(|col| !col.hidden()) { if left_col.name == right_col.name { distinct_names.push(ast::Name::exact( left_col.name.clone().expect("column name is None"), )); found_match = true; break; } } if found_match { break; } } } if distinct_names.is_empty() { None // No common columns = cross join } else { Some(ast::JoinConstraint::Using(distinct_names)) } } else { constraint }; let mut using = vec![]; if let Some(constraint) = constraint { match constraint { ast::JoinConstraint::On(ref expr) => { let start_idx = out_where_clause.len(); break_predicate_at_and_boundaries(expr, out_where_clause); for predicate in out_where_clause[start_idx..].iter_mut() { predicate.from_outer_join = if outer { Some(table_references.joined_tables().last().unwrap().internal_id) } else { None }; bind_and_rewrite_expr( &mut predicate.expr, Some(table_references), None, resolver, BindingBehavior::TryResultColumnsFirst, )?; } } ast::JoinConstraint::Using(distinct_names) => { // USING join is replaced with a list of equality predicates for distinct_name in distinct_names.iter() { let name_normalized = normalize_ident(distinct_name.as_str()); let cur_table_idx = table_references.joined_tables().len() - 1; let left_tables = &table_references.joined_tables()[..cur_table_idx]; turso_assert!(!left_tables.is_empty()); let right_table = table_references.joined_tables().last().unwrap(); let mut left_col = None; for (left_table_idx, left_table) in left_tables.iter().enumerate() { left_col = left_table .columns() .iter() .enumerate() .filter(|(_, col)| !natural || !col.hidden()) .find(|(_, col)| { col.name .as_ref() .is_some_and(|name| *name == name_normalized) }) .map(|(idx, col)| (left_table_idx, left_table.internal_id, idx, col)); if left_col.is_some() { break; } } if left_col.is_none() { crate::bail_parse_error!( "cannot join using column {} - column not present in all tables", distinct_name.as_str() ); } let right_col = right_table.columns().iter().enumerate().find(|(_, col)| { col.name .as_ref() .is_some_and(|name| *name == name_normalized) }); if right_col.is_none() { crate::bail_parse_error!( "cannot join using column {} - column not present in all tables", distinct_name.as_str() ); } let (left_table_idx, left_table_id, left_col_idx, left_col) = left_col.unwrap(); let (right_col_idx, right_col) = right_col.unwrap(); let expr = Expr::Binary( Box::new(Expr::Column { database: None, table: left_table_id, column: left_col_idx, is_rowid_alias: left_col.is_rowid_alias(), }), ast::Operator::Equals, Box::new(Expr::Column { database: None, table: right_table.internal_id, column: right_col_idx, is_rowid_alias: right_col.is_rowid_alias(), }), ); let left_table: &mut JoinedTable = table_references .joined_tables_mut() .get_mut(left_table_idx) .unwrap(); left_table.mark_column_used(left_col_idx); let right_table: &mut JoinedTable = table_references .joined_tables_mut() .get_mut(cur_table_idx) .unwrap(); right_table.mark_column_used(right_col_idx); out_where_clause.push(WhereTerm { expr, from_outer_join: if outer { Some(right_table.internal_id) } else { None }, consumed: false, }); } using = distinct_names; } } } assert!(table_references.joined_tables().len() >= 2); let last_idx = table_references.joined_tables().len() - 1; let rightmost_table = table_references .joined_tables_mut() .get_mut(last_idx) .unwrap(); let plan_join_type = if full_outer { PlanJoinType::FullOuter } else if outer { PlanJoinType::LeftOuter } else { PlanJoinType::Inner }; rightmost_table.join_info = Some(JoinInfo { join_type: plan_join_type, using, no_reorder: is_cross, }); Ok(()) } pub fn break_predicate_at_and_boundaries>( predicate: &Expr, out_predicates: &mut Vec, ) { // Unwrap single-element parenthesized expressions recursively: ((expr)) -> expr. // This is semantically equivalent since single-element Parenthesized is purely // syntactic grouping. Multi-element Parenthesized (row values like (x, y)) are // left as-is by unwrap_parens. let predicate = unwrap_parens(predicate).unwrap_or(predicate); match predicate { Expr::Binary(left, ast::Operator::And, right) => { break_predicate_at_and_boundaries(left, out_predicates); break_predicate_at_and_boundaries(right, out_predicates); } _ => { out_predicates.push(predicate.clone().into()); } } } pub fn parse_row_id( column_name: &str, table_id: TableInternalId, fn_check: F, ) -> Result> where F: FnOnce() -> bool, { if ROWID_STRS .iter() .any(|s| s.eq_ignore_ascii_case(column_name)) { if fn_check() { crate::bail_parse_error!("ROWID is ambiguous"); } return Ok(Some(Expr::RowId { database: None, // TODO: support different databases table: table_id, })); } Ok(None) } #[allow(clippy::type_complexity)] pub fn parse_limit( mut limit: Limit, resolver: &Resolver, ) -> Result<(Option>, Option>)> { bind_and_rewrite_expr( &mut limit.expr, None, None, resolver, BindingBehavior::TryResultColumnsFirst, )?; if let Some(ref mut off_expr) = limit.offset { bind_and_rewrite_expr( off_expr, None, None, resolver, BindingBehavior::TryResultColumnsFirst, )?; } Ok((Some(limit.expr), limit.offset)) } ================================================ FILE: core/translate/pragma.rs ================================================ //! VDBE bytecode generation for pragma statements. //! More info: https://www.sqlite.org/pragma.html. use crate::sync::Arc; use crate::turso_soft_unreachable; use chrono::Datelike; use turso_macros::match_ignore_ascii_case; use turso_parser::ast::PragmaName; use turso_parser::ast::{self, Expr, Literal}; use super::integrity_check::{ translate_integrity_check, translate_quick_check, MAX_INTEGRITY_CHECK_ERRORS, }; use crate::function::Func; use crate::pragma::pragma_for; use crate::schema::Schema; use crate::storage::encryption::{CipherMode, EncryptionKey}; use crate::storage::pager::AutoVacuumMode; use crate::storage::pager::Pager; use crate::storage::sqlite3_ondisk::CacheSize; use crate::storage::wal::CheckpointMode; use crate::translate::emitter::{Resolver, TransactionMode}; use crate::util::{normalize_ident, parse_signed_number, parse_string, IOExt as _}; use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts}; use crate::vdbe::insn::{Cookie, Insn}; use crate::{bail_parse_error, CaptureDataChangesInfo, LimboError, Numeric, Value}; use std::str::FromStr; use strum::IntoEnumIterator; fn list_pragmas(program: &mut ProgramBuilder) { for x in PragmaName::iter() { let register = program.emit_string8_new_reg(x.to_string()); program.emit_result_row(register, 1); } program.add_pragma_result_column("pragma_list".into()); } /// Parse max_errors from an optional value expression. /// Returns the parsed integer if value is a numeric literal, otherwise returns the default. fn parse_max_errors_from_value(value: &Option) -> usize { match value { Some(Expr::Literal(Literal::Numeric(n))) => { n.parse::().unwrap_or(MAX_INTEGRITY_CHECK_ERRORS) } _ => MAX_INTEGRITY_CHECK_ERRORS, } } pub fn translate_pragma( resolver: &Resolver, name: &ast::QualifiedName, body: Option, pager: Arc, connection: Arc, program: &mut ProgramBuilder, ) -> crate::Result<()> { let opts = ProgramBuilderOpts { num_cursors: 0, approx_num_insns: 20, approx_num_labels: 0, }; program.extend(&opts); if name.name.as_str().eq_ignore_ascii_case("pragma_list") { list_pragmas(program); return Ok(()); } let pragma = match PragmaName::from_str(name.name.as_str()) { Ok(pragma) => pragma, Err(_) => bail_parse_error!("Not a valid pragma name"), }; let database_id = resolver.resolve_database_id(name)?; let mode = match body { None => query_pragma( pragma, resolver, None, pager, connection, database_id, program, )?, Some(ast::PragmaBody::Equals(value) | ast::PragmaBody::Call(value)) => match pragma { // These pragmas take a parameter but are queries, not setters PragmaName::IndexInfo | PragmaName::IndexXinfo | PragmaName::IndexList | PragmaName::TableList | PragmaName::TableInfo | PragmaName::TableXinfo | PragmaName::IntegrityCheck | PragmaName::DatabaseList | PragmaName::QuickCheck => query_pragma( pragma, resolver, Some(*value), pager, connection, database_id, program, )?, _ => update_pragma( pragma, resolver, *value, pager, connection, database_id, program, )?, }, }; match mode { TransactionMode::None => {} TransactionMode::Read => { if crate::is_attached_db(database_id) { let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version); program.begin_read_on_database(database_id, schema_cookie); } program.begin_read_operation(); } TransactionMode::Write => { if crate::is_attached_db(database_id) { let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version); program.begin_write_on_database(database_id, schema_cookie); } program.begin_write_operation(); } TransactionMode::Concurrent => { program.begin_concurrent_operation(); } } Ok(()) } fn update_pragma( pragma: PragmaName, resolver: &Resolver, value: ast::Expr, pager: Arc, connection: Arc, database_id: usize, program: &mut ProgramBuilder, ) -> crate::Result { let parse_pragma_enabled = |expr: &ast::Expr| -> bool { if let Expr::Literal(Literal::Numeric(n)) = expr { return !matches!(n.as_str(), "0"); }; let name_bytes = match expr { Expr::Literal(Literal::Keyword(name)) => name.as_bytes(), Expr::Name(name) | Expr::Id(name) => name.as_str().as_bytes(), _ => "".as_bytes(), }; match_ignore_ascii_case!(match name_bytes { b"ON" | b"TRUE" | b"YES" | b"1" => true, _ => false, }) }; match pragma { PragmaName::ApplicationId => { let data = parse_signed_number(&value)?; let app_id_value = match data { Value::Numeric(Numeric::Integer(i)) => i as i32, Value::Numeric(Numeric::Float(f)) => f64::from(f) as i32, _ => bail_parse_error!("expected integer, got {:?}", data), }; program.emit_insn(Insn::SetCookie { db: database_id, cookie: Cookie::ApplicationId, value: app_id_value, p5: 1, }); Ok(TransactionMode::Write) } PragmaName::BusyTimeout => { let data = parse_signed_number(&value)?; let busy_timeout_ms = match data { Value::Numeric(Numeric::Integer(i)) => i as i32, Value::Numeric(Numeric::Float(f)) => f64::from(f) as i32, _ => bail_parse_error!("expected integer, got {:?}", data), }; let busy_timeout_ms = busy_timeout_ms.max(0); connection.set_busy_timeout(std::time::Duration::from_millis(busy_timeout_ms as u64)); Ok(TransactionMode::Write) } PragmaName::CacheSize => { let cache_size = match parse_signed_number(&value)? { Value::Numeric(Numeric::Integer(size)) => size, Value::Numeric(Numeric::Float(size)) => f64::from(size) as i64, _ => bail_parse_error!("Invalid value for cache size pragma"), }; update_cache_size(cache_size, pager, connection)?; Ok(TransactionMode::None) } PragmaName::CacheSpill => { let enabled = parse_pragma_enabled(&value); connection.get_pager().set_spill_enabled(enabled); connection.bump_prepare_context_generation(); Ok(TransactionMode::None) } PragmaName::Encoding => { let year = chrono::Local::now().year(); bail_parse_error!("It's {year}. UTF-8 won."); } PragmaName::JournalMode => { // For JournalMode, when setting a value, we use the opcode let mode_str = match value { Expr::Name(name) => name.as_str().to_string(), Expr::Literal(Literal::Keyword(ref kw)) => kw.clone(), _ => parse_string(&value)?, }; let result_reg = program.alloc_register(); program.emit_insn(Insn::JournalMode { db: database_id, dest: result_reg, new_mode: Some(mode_str), }); program.emit_result_row(result_reg, 1); program.add_pragma_result_column("journal_mode".into()); Ok(TransactionMode::None) } PragmaName::LegacyFileFormat => Ok(TransactionMode::None), PragmaName::WalCheckpoint => query_pragma( PragmaName::WalCheckpoint, resolver, Some(value), pager, connection, database_id, program, ), PragmaName::ModuleList => Ok(TransactionMode::None), PragmaName::PageCount => query_pragma( PragmaName::PageCount, resolver, None, pager, connection, database_id, program, ), PragmaName::MaxPageCount => { let data = parse_signed_number(&value)?; let max_page_count_value = match data { Value::Numeric(Numeric::Integer(i)) => i as usize, Value::Numeric(Numeric::Float(f)) => f64::from(f) as usize, _ => unreachable!(), }; let result_reg = program.alloc_register(); program.emit_insn(Insn::MaxPgcnt { db: database_id, dest: result_reg, new_max: max_page_count_value, }); program.emit_result_row(result_reg, 1); program.add_pragma_result_column("max_page_count".into()); Ok(TransactionMode::Write) } PragmaName::UserVersion => { let data = parse_signed_number(&value)?; let version_value = match data { Value::Numeric(Numeric::Integer(i)) => i as i32, Value::Numeric(Numeric::Float(f)) => f64::from(f) as i32, _ => unreachable!(), }; program.emit_insn(Insn::SetCookie { db: database_id, cookie: Cookie::UserVersion, value: version_value, p5: 1, }); Ok(TransactionMode::Write) } PragmaName::SchemaVersion => { // SQLite allowing this to be set is an incredibly stupid idea in my view. // In "defensive mode", this is a silent nop. So let's emulate that always. program.emit_insn(Insn::Noop {}); Ok(TransactionMode::None) } PragmaName::TableInfo => { // because we need control over the write parameter for the transaction, // this should be unreachable. We have to force-call query_pragma before // getting here unreachable!(); } PragmaName::TableXinfo => { // because we need control over the write parameter for the transaction, // this should be unreachable. We have to force-call query_pragma before // getting here unreachable!(); } PragmaName::PageSize => { let page_size = match parse_signed_number(&value)? { Value::Numeric(Numeric::Integer(size)) => size, Value::Numeric(Numeric::Float(size)) => f64::from(size) as i64, _ => bail_parse_error!("Invalid value for page size pragma"), }; update_page_size(connection, page_size as u32)?; Ok(TransactionMode::None) } PragmaName::AutoVacuum => { // Check if autovacuum is enabled in database opts if !connection.db.opts.enable_autovacuum { return Err(LimboError::InvalidArgument( "Autovacuum is not enabled. Use --experimental-autovacuum flag to enable it." .to_string(), )); } let is_empty = is_database_empty(resolver.schema(), &pager)?; tracing::debug!( "Checking if database is empty for auto_vacuum pragma: {}", is_empty ); if !is_empty { // SQLite's behavior is to silently ignore this pragma if the database is not empty. tracing::debug!( "Attempted to set auto_vacuum, database is not empty so we are ignoring pragma." ); return Ok(TransactionMode::None); } let auto_vacuum_mode = match value { Expr::Name(name) => { let name = name.as_str().as_bytes(); match_ignore_ascii_case!(match name { b"none" => 0, b"full" => 1, b"incremental" => 2, _ => { return Err(LimboError::InvalidArgument( "invalid auto vacuum mode".to_string(), )); } }) } _ => { return Err(LimboError::InvalidArgument( "invalid auto vacuum mode".to_string(), )); } }; match auto_vacuum_mode { 0 => update_auto_vacuum_mode(AutoVacuumMode::None, 0, pager)?, 1 => update_auto_vacuum_mode(AutoVacuumMode::Full, 1, pager)?, 2 => update_auto_vacuum_mode(AutoVacuumMode::Incremental, 1, pager)?, _ => { return Err(LimboError::InvalidArgument( "invalid auto vacuum mode".to_string(), )); } } let largest_root_page_number_reg = program.alloc_register(); program.emit_insn(Insn::ReadCookie { db: database_id, dest: largest_root_page_number_reg, cookie: Cookie::LargestRootPageNumber, }); let set_cookie_label = program.allocate_label(); program.emit_insn(Insn::If { reg: largest_root_page_number_reg, target_pc: set_cookie_label, jump_if_null: false, }); program.emit_insn(Insn::Halt { err_code: 0, description: "Early halt because auto vacuum mode is not enabled".to_string(), on_error: None, description_reg: None, }); program.resolve_label(set_cookie_label, program.offset()); program.emit_insn(Insn::SetCookie { db: database_id, cookie: Cookie::IncrementalVacuum, value: auto_vacuum_mode - 1, p5: 0, }); Ok(TransactionMode::None) } PragmaName::IntegrityCheck => unreachable!("integrity_check cannot be set"), PragmaName::QuickCheck => unreachable!("quick_check cannot be set"), PragmaName::CaptureDataChangesConn | PragmaName::UnstableCaptureDataChangesConn => { let value = parse_string(&value)?; let opts = CaptureDataChangesInfo::parse(&value, Some(CDC_VERSION_CURRENT))?; if opts.is_some() && connection.mvcc_enabled() { bail_parse_error!("CDC is not supported in MVCC mode"); } // InitCdcVersion handles everything at execution time: // - For enable: creates CDC table + version table, records version, // reads back actual version, defers CDC state to Halt // - For disable ("off"): defers CDC=None to Halt let cdc_table_name = opts .as_ref() .map(|i| i.table.to_string()) .unwrap_or_default(); program.emit_insn(Insn::InitCdcVersion { cdc_table_name, version: CDC_VERSION_CURRENT, cdc_mode: value, }); Ok(TransactionMode::Write) } PragmaName::DatabaseList => unreachable!("database_list cannot be set"), PragmaName::IndexInfo => unreachable!("index_info cannot be set"), PragmaName::IndexXinfo => unreachable!("index_xinfo cannot be set"), PragmaName::IndexList => unreachable!("index_list cannot be set"), PragmaName::TableList => unreachable!("table_list cannot be set"), PragmaName::QueryOnly => query_pragma( PragmaName::QueryOnly, resolver, Some(value), pager, connection, database_id, program, ), PragmaName::FreelistCount => query_pragma( PragmaName::FreelistCount, resolver, Some(value), pager, connection, database_id, program, ), PragmaName::EncryptionKey => { let value = parse_string(&value)?; let key = EncryptionKey::from_hex_string(&value)?; connection.set_encryption_key(key)?; Ok(TransactionMode::None) } PragmaName::EncryptionCipher => { let value = parse_string(&value)?; let cipher = CipherMode::try_from(value.as_str())?; connection.set_encryption_cipher(cipher)?; Ok(TransactionMode::None) } PragmaName::Synchronous => { use crate::SyncMode; let mode = if let Expr::Literal(Literal::Numeric(n)) = &value { match n.as_str() { "0" => SyncMode::Off, "1" => SyncMode::Normal, _ => SyncMode::Full, // SQLite defaults to NORMAL for invalid values, but we want to default to a higher durability level so deviating here. } } else { let name_bytes = match &value { Expr::Literal(Literal::Keyword(name)) => name.as_bytes(), Expr::Name(name) | Expr::Id(name) => name.as_str().as_bytes(), _ => b"", }; match_ignore_ascii_case!(match name_bytes { b"OFF" | b"0" => SyncMode::Off, b"NORMAL" | b"1" => SyncMode::Normal, _ => SyncMode::Full, }) }; connection.set_sync_mode(mode); Ok(TransactionMode::None) } PragmaName::DataSyncRetry => { let retry_enabled = parse_pragma_enabled(&value); connection.set_data_sync_retry(retry_enabled); Ok(TransactionMode::None) } PragmaName::MvccCheckpointThreshold => { let threshold = match parse_signed_number(&value)? { Value::Numeric(Numeric::Integer(size)) if size >= -1 => size, _ => bail_parse_error!( "mvcc_checkpoint_threshold must be -1, 0, or a positive integer" ), }; connection.set_mvcc_checkpoint_threshold(threshold)?; Ok(TransactionMode::None) } PragmaName::ForeignKeys => { let enabled = parse_pragma_enabled(&value); connection.set_foreign_keys_enabled(enabled); Ok(TransactionMode::None) } PragmaName::IAmADummy | PragmaName::RequireWhere => { let enabled = parse_pragma_enabled(&value); connection.set_dml_require_where(enabled); Ok(TransactionMode::None) } PragmaName::IgnoreCheckConstraints => { let enabled = parse_pragma_enabled(&value); connection.set_check_constraints_ignored(enabled); Ok(TransactionMode::None) } #[cfg(target_vendor = "apple")] PragmaName::Fullfsync => { let enabled = parse_pragma_enabled(&value); let sync_type = if enabled { crate::io::FileSyncType::FullFsync } else { crate::io::FileSyncType::Fsync }; connection.set_sync_type(sync_type); Ok(TransactionMode::None) } PragmaName::ListTypes => bail_parse_error!("list_types cannot be set"), PragmaName::TempStore => { use crate::TempStore; // Try to parse as a string first (default, file, memory) let temp_store = if let Expr::Literal(Literal::Numeric(n)) = &value { // Numeric value: 0, 1, or 2 match n.as_str() { "0" => TempStore::Default, "1" => TempStore::File, "2" => TempStore::Memory, _ => bail_parse_error!("temp_store must be 0, 1, 2, DEFAULT, FILE, or MEMORY"), } } else { // Try as keyword/identifier: DEFAULT, FILE, MEMORY let name_bytes = match &value { Expr::Literal(Literal::Keyword(name)) => name.as_bytes(), Expr::Name(name) | Expr::Id(name) => name.as_str().as_bytes(), Expr::Literal(Literal::String(s)) => s.as_bytes(), _ => bail_parse_error!("temp_store must be 0, 1, 2, DEFAULT, FILE, or MEMORY"), }; match_ignore_ascii_case!(match name_bytes { b"DEFAULT" | b"0" => TempStore::Default, b"FILE" | b"1" => TempStore::File, b"MEMORY" | b"2" => TempStore::Memory, _ => bail_parse_error!("temp_store must be 0, 1, 2, DEFAULT, FILE, or MEMORY"), }) }; connection.set_temp_store(temp_store); Ok(TransactionMode::None) } PragmaName::FunctionList => query_pragma( PragmaName::FunctionList, resolver, Some(value), pager, connection, database_id, program, ), } } fn query_pragma( pragma: PragmaName, resolver: &Resolver, value: Option, pager: Arc, connection: Arc, database_id: usize, program: &mut ProgramBuilder, ) -> crate::Result { let schema = resolver.schema(); let register = program.alloc_register(); match pragma { PragmaName::ApplicationId => { program.emit_insn(Insn::ReadCookie { db: database_id, dest: register, cookie: Cookie::ApplicationId, }); program.add_pragma_result_column(pragma.to_string()); program.emit_result_row(register, 1); Ok(TransactionMode::Read) } PragmaName::BusyTimeout => { program.emit_int(connection.get_busy_timeout().as_millis() as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::CacheSize => { program.emit_int(connection.get_cache_size() as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::CacheSpill => { let spill_enabled = connection.get_pager().get_spill_enabled(); program.emit_int(spill_enabled as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::DatabaseList => { let base_reg = register; program.alloc_registers(2); // Get all databases (main + attached) and emit a row for each let all_databases = connection.list_all_databases(); for (seq_number, name, file_path) in all_databases { // seq (sequence number) program.emit_int(seq_number as i64, base_reg); // name (alias) program.emit_string8(name, base_reg + 1); // file path program.emit_string8(file_path, base_reg + 2); program.emit_result_row(base_reg, 3); } let pragma = pragma_for(&pragma); for col_name in pragma.columns.iter() { program.add_pragma_result_column(col_name.to_string()); } Ok(TransactionMode::None) } PragmaName::Encoding => { let encoding = pager .io .block(|| pager.with_header(|header| header.text_encoding)) .unwrap_or_default() .to_string(); program.emit_string8(encoding, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::JournalMode => { // Use the JournalMode opcode to get the current journal mode program.emit_insn(Insn::JournalMode { db: database_id, dest: register, new_mode: None, }); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::LegacyFileFormat => Ok(TransactionMode::None), PragmaName::WalCheckpoint => { // Checkpoint uses 3 registers: P1, P2, P3. Ref Insn::Checkpoint for more info. // Allocate two more here as one was allocated at the top. let mode = match value { Some(ast::Expr::Name(name)) => { let mode_name = normalize_ident(name.as_str()); CheckpointMode::from_str(&mode_name).map_err(|e| { LimboError::ParseError(format!("Unknown Checkpoint Mode: {e}")) })? } _ => CheckpointMode::Passive { upper_bound_inclusive: None, }, }; program.alloc_registers(2); program.emit_insn(Insn::Checkpoint { database: database_id, checkpoint_mode: mode, dest: register, }); program.emit_result_row(register, 3); program.add_pragma_result_column("busy".to_string()); program.add_pragma_result_column("log".to_string()); program.add_pragma_result_column("checkpointed".to_string()); Ok(TransactionMode::None) } PragmaName::ModuleList => { let modules = connection.get_syms_vtab_mods(); for module in modules { program.emit_string8(module.to_string(), register); program.emit_result_row(register, 1); } program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::FunctionList => { // 6 columns: name, builtin, type, enc, narg, flags let base_reg = register; program.alloc_registers(5); const SQLITE_DETERMINISTIC: i64 = 0x800; const SQLITE_INNOCUOUS: i64 = 0x200000; // Built-in functions for entry in Func::builtin_function_list() { let mut flags: i64 = 0; if entry.deterministic { flags |= SQLITE_DETERMINISTIC; } flags |= SQLITE_INNOCUOUS; program.emit_string8(entry.name, base_reg); program.emit_int(1, base_reg + 1); // builtin = 1 program.emit_string8(entry.func_type.to_string(), base_reg + 2); program.emit_string8("utf8".to_string(), base_reg + 3); program.emit_int(entry.narg as i64, base_reg + 4); program.emit_int(flags, base_reg + 5); program.emit_result_row(base_reg, 6); } // External (extension) functions for (name, is_agg, argc) in connection.get_syms_functions() { let func_type = if is_agg { "a" } else { "s" }; program.emit_string8(name, base_reg); program.emit_int(0, base_reg + 1); // builtin = 0 program.emit_string8(func_type.to_string(), base_reg + 2); program.emit_string8("utf8".to_string(), base_reg + 3); program.emit_int(argc as i64, base_reg + 4); program.emit_int(0, base_reg + 5); // flags = 0 for extensions program.emit_result_row(base_reg, 6); } let pragma_meta = pragma_for(&pragma); for col_name in pragma_meta.columns.iter() { program.add_pragma_result_column(col_name.to_string()); } Ok(TransactionMode::None) } PragmaName::PageCount => { program.emit_insn(Insn::PageCount { db: database_id, dest: register, }); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::Read) } PragmaName::MaxPageCount => { program.emit_insn(Insn::MaxPgcnt { db: database_id, dest: register, new_max: 0, // 0 means just return current max }); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::Read) } PragmaName::IndexInfo => { let index_name = match value { Some(ast::Expr::Name(name)) => Some(normalize_ident(name.as_str())), _ => None, }; let base_reg = register; // 3 columns: seqno, cid, name program.alloc_registers(2); if let Some(index_name) = index_name { let index = schema .indexes .values() .flatten() .find(|idx| idx.name.eq_ignore_ascii_case(&index_name)); if let Some(index) = index { for (seqno, col) in index.columns.iter().enumerate() { program.emit_int(seqno as i64, base_reg); program.emit_int(col.pos_in_table as i64, base_reg + 1); program.emit_string8(col.name.clone(), base_reg + 2); program.emit_result_row(base_reg, 3); } } } let pragma_meta = pragma_for(&pragma); for col_name in pragma_meta.columns.iter() { program.add_pragma_result_column(col_name.to_string()); } Ok(TransactionMode::None) } PragmaName::IndexXinfo => { let index_name = match value { Some(ast::Expr::Name(name)) => Some(normalize_ident(name.as_str())), _ => None, }; let base_reg = register; // 6 columns: seqno, cid, name, desc, coll, key program.alloc_registers(5); if let Some(index_name) = index_name { let index = schema .indexes .values() .flatten() .find(|idx| idx.name.eq_ignore_ascii_case(&index_name)); if let Some(index) = index { for (seqno, col) in index.columns.iter().enumerate() { let desc = matches!(col.order, ast::SortOrder::Desc); let coll = col .collation .map(|c| c.to_string().to_uppercase()) .unwrap_or_else(|| "BINARY".to_string()); program.emit_int(seqno as i64, base_reg); program.emit_int(col.pos_in_table as i64, base_reg + 1); program.emit_string8(col.name.clone(), base_reg + 2); program.emit_int(desc as i64, base_reg + 3); program.emit_string8(coll, base_reg + 4); program.emit_int(1, base_reg + 5); // key column program.emit_result_row(base_reg, 6); } // Emit trailing rowid row if the index has one if index.has_rowid { let seqno = index.columns.len(); program.emit_int(seqno as i64, base_reg); program.emit_int(-1, base_reg + 1); program.emit_string8(String::new(), base_reg + 2); program.emit_int(0, base_reg + 3); program.emit_string8("BINARY".to_string(), base_reg + 4); program.emit_int(0, base_reg + 5); // not a key column program.emit_result_row(base_reg, 6); } } } let pragma_meta = pragma_for(&pragma); for col_name in pragma_meta.columns.iter() { program.add_pragma_result_column(col_name.to_string()); } Ok(TransactionMode::None) } PragmaName::IndexList => { let table_name = match value { Some(ast::Expr::Name(name)) => Some(normalize_ident(name.as_str())), _ => None, }; let base_reg = register; // 5 columns: seq, name, unique, origin, partial program.alloc_registers(4); if let Some(table_name) = table_name { if let Some(table) = schema.get_table(&table_name) { let pk_cols: Vec = table .btree() .map(|bt| { bt.primary_key_columns .iter() .map(|(name, _)| name.clone()) .collect() }) .unwrap_or_default(); for (seq, index) in schema.get_indices(&table_name).enumerate() { let origin = if index.name.starts_with("sqlite_autoindex_") { let idx_cols: Vec<&str> = index.columns.iter().map(|c| c.name.as_str()).collect(); if idx_cols.len() == pk_cols.len() && idx_cols .iter() .zip(pk_cols.iter()) .all(|(a, b)| a.eq_ignore_ascii_case(b)) { "pk" } else { "u" } } else { "c" }; program.emit_int(seq as i64, base_reg); program.emit_string8(index.name.clone(), base_reg + 1); program.emit_int(index.unique as i64, base_reg + 2); program.emit_string8(origin.to_string(), base_reg + 3); program.emit_int(index.where_clause.is_some() as i64, base_reg + 4); program.emit_result_row(base_reg, 5); } } } let pragma_meta = pragma_for(&pragma); for col_name in pragma_meta.columns.iter() { program.add_pragma_result_column(col_name.to_string()); } Ok(TransactionMode::None) } PragmaName::TableList => { let name = match value { Some(ast::Expr::Name(name)) => Some(normalize_ident(name.as_str())), _ => None, }; let base_reg = register; // 6 columns: schema, name, type, ncol, wr, strict program.alloc_registers(5); let emit_table_row = |program: &mut ProgramBuilder, name: &str, obj_type: &str, ncol: usize, wr: bool, strict: bool| { program.emit_string8("main".to_string(), base_reg); program.emit_string8(name.to_string(), base_reg + 1); program.emit_string8(obj_type.to_string(), base_reg + 2); program.emit_int(ncol as i64, base_reg + 3); program.emit_int(wr as i64, base_reg + 4); program.emit_int(strict as i64, base_reg + 5); program.emit_result_row(base_reg, 6); }; if let Some(name) = name { // Specific table/view lookup if let Some(table) = schema.get_table(&name) { let (wr, strict) = match table.btree() { Some(bt) => (!bt.has_rowid, bt.is_strict), None => (false, false), }; emit_table_row( program, table.get_name(), "table", table.columns().len(), wr, strict, ); } else if let Some(view) = schema.get_view(&name) { emit_table_row( program, &view.name, "view", view.columns.len(), false, false, ); } } else { // List all tables and views (only BTree tables, not built-in virtual tables) for table in schema.tables.values() { let Some(bt) = table.btree() else { continue; }; emit_table_row( program, &bt.name, "table", bt.columns.len(), !bt.has_rowid, bt.is_strict, ); } for view in schema.views.values() { emit_table_row( program, &view.name, "view", view.columns.len(), false, false, ); } } let pragma_meta = pragma_for(&pragma); for col_name in pragma_meta.columns.iter() { program.add_pragma_result_column(col_name.to_string()); } Ok(TransactionMode::None) } PragmaName::TableInfo => { let name = match value { Some(ast::Expr::Name(name)) => Some(normalize_ident(name.as_str())), _ => None, }; let base_reg = register; // we need 6 registers, but first register was allocated at the beginning of the "query_pragma" function program.alloc_registers(5); if let Some(name) = name { resolver.with_schema(database_id, |db_schema| { if let Some(table) = db_schema.get_table(&name) { emit_columns_for_table_info(program, table.columns(), base_reg, false); } else if let Some(view_mutex) = db_schema.get_materialized_view(&name) { let view = view_mutex.lock(); let flat_columns = view.column_schema.flat_columns(); emit_columns_for_table_info(program, &flat_columns, base_reg, false); } else if let Some(view) = db_schema.get_view(&name) { emit_columns_for_table_info(program, &view.columns, base_reg, false); } }); } let col_names = ["cid", "name", "type", "notnull", "dflt_value", "pk"]; for name in col_names { program.add_pragma_result_column(name.into()); } Ok(TransactionMode::None) } PragmaName::TableXinfo => { let name = match value { Some(ast::Expr::Name(name)) => Some(normalize_ident(name.as_str())), _ => None, }; let base_reg = register; // we need 7 registers, but first register was allocated at the beginning of the "query_pragma" function program.alloc_registers(6); if let Some(name) = name { resolver.with_schema(database_id, |db_schema| { if let Some(table) = db_schema.get_table(&name) { emit_columns_for_table_info(program, table.columns(), base_reg, true); } else if let Some(view_mutex) = db_schema.get_materialized_view(&name) { let view = view_mutex.lock(); let flat_columns = view.column_schema.flat_columns(); emit_columns_for_table_info(program, &flat_columns, base_reg, true); } else if let Some(view) = db_schema.get_view(&name) { emit_columns_for_table_info(program, &view.columns, base_reg, true); } }); } let col_names = [ "cid", "name", "type", "notnull", "dflt_value", "pk", "hidden", ]; for name in col_names { program.add_pragma_result_column(name.into()); } Ok(TransactionMode::None) } PragmaName::UserVersion => { program.emit_insn(Insn::ReadCookie { db: database_id, dest: register, cookie: Cookie::UserVersion, }); program.add_pragma_result_column(pragma.to_string()); program.emit_result_row(register, 1); Ok(TransactionMode::Read) } PragmaName::SchemaVersion => { program.emit_insn(Insn::ReadCookie { db: database_id, dest: register, cookie: Cookie::SchemaVersion, }); program.add_pragma_result_column(pragma.to_string()); program.emit_result_row(register, 1); Ok(TransactionMode::Read) } PragmaName::PageSize => { program.emit_int( pager .io .block(|| pager.with_header(|header| header.page_size.get())) .unwrap_or_else(|_| connection.get_page_size().get()) as i64, register, ); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::AutoVacuum => { let auto_vacuum_mode = pager.get_auto_vacuum_mode(); let auto_vacuum_mode_i64: i64 = match auto_vacuum_mode { AutoVacuumMode::None => 0, AutoVacuumMode::Full => 1, AutoVacuumMode::Incremental => 2, }; let register = program.alloc_register(); program.emit_insn(Insn::Int64 { _p1: 0, out_reg: register, _p3: 0, value: auto_vacuum_mode_i64, }); program.emit_result_row(register, 1); Ok(TransactionMode::None) } PragmaName::IntegrityCheck => { let max_errors = parse_max_errors_from_value(&value); translate_integrity_check(schema, program, resolver, database_id, max_errors)?; Ok(TransactionMode::Read) } PragmaName::QuickCheck => { let max_errors = parse_max_errors_from_value(&value); translate_quick_check(schema, program, resolver, database_id, max_errors)?; Ok(TransactionMode::Read) } PragmaName::CaptureDataChangesConn | PragmaName::UnstableCaptureDataChangesConn => { let pragma = pragma_for(&pragma); let second_column = program.alloc_register(); let third_column = program.alloc_register(); let opts = connection.get_capture_data_changes_info(); match opts.as_ref() { Some(info) => { program.emit_string8(info.mode_name().to_string(), register); program.emit_string8(info.table.clone(), second_column); match &info.version { Some(v) => program.emit_string8(v.to_string(), third_column), None => program.emit_null(third_column, None), } } None => { program.emit_string8("off".to_string(), register); program.emit_null(second_column, None); program.emit_null(third_column, None); } } program.emit_result_row(register, 3); program.add_pragma_result_column(pragma.columns[0].to_string()); program.add_pragma_result_column(pragma.columns[1].to_string()); program.add_pragma_result_column(pragma.columns[2].to_string()); Ok(TransactionMode::Read) } PragmaName::QueryOnly => { if let Some(value_expr) = value { let is_query_only = match value_expr { ast::Expr::Literal(Literal::Numeric(i)) => i .parse::() .map(|v| v != 0) .or_else(|_| i.parse::().map(|v| v != 0.0)) .map_err(|_| { LimboError::ParseError(format!( "Invalid numeric value for PRAGMA query_only: {i}" )) })?, ast::Expr::Literal(Literal::String(..)) | ast::Expr::Name(..) => { let s = match &value_expr { ast::Expr::Literal(Literal::String(s)) => s.as_bytes(), ast::Expr::Name(n) => n.as_str().as_bytes(), _ => unreachable!(), }; match_ignore_ascii_case!(match s { b"1" | b"on" | b"true" => true, _ => false, }) } _ => { return Err(LimboError::ParseError(format!( "Invalid value for PRAGMA query_only: {value_expr:?}" ))); } }; connection.set_query_only(is_query_only); return Ok(TransactionMode::None); }; let register = program.alloc_register(); let is_query_only = connection.get_query_only(); program.emit_int(is_query_only as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::FreelistCount => { let value = pager.freepage_list(); let register = program.alloc_register(); program.emit_int(value as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::EncryptionKey => { let msg = { if connection.encryption_key.read().is_some() { "encryption key is set for this session" } else { "encryption key is not set for this session" } }; let register = program.alloc_register(); program.emit_string8(msg.to_string(), register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::EncryptionCipher => { if let Some(cipher) = connection.get_encryption_cipher_mode() { let register = program.alloc_register(); program.emit_string8(cipher.to_string(), register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); } Ok(TransactionMode::None) } PragmaName::Synchronous => { let mode = connection.get_sync_mode(); let register = program.alloc_register(); program.emit_int(mode as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::DataSyncRetry => { let retry_enabled = connection.get_data_sync_retry(); let register = program.alloc_register(); program.emit_int(retry_enabled as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::MvccCheckpointThreshold => { let threshold = connection.mvcc_checkpoint_threshold()?; let register = program.alloc_register(); program.emit_int(threshold, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::ForeignKeys => { let enabled = connection.foreign_keys_enabled(); let register = program.alloc_register(); program.emit_int(enabled as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::IAmADummy | PragmaName::RequireWhere => { let register = program.alloc_register(); let enabled = connection.get_dml_require_where(); program.emit_int(enabled as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::IgnoreCheckConstraints => { let ignored = connection.check_constraints_ignored(); let register = program.alloc_register(); program.emit_int(ignored as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } #[cfg(target_vendor = "apple")] PragmaName::Fullfsync => { let enabled = connection.get_sync_type() == crate::io::FileSyncType::FullFsync; let register = program.alloc_register(); program.emit_int(enabled as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::TempStore => { let temp_store = connection.get_temp_store(); let register = program.alloc_register(); program.emit_int(temp_store as i64, register); program.emit_result_row(register, 1); program.add_pragma_result_column(pragma.to_string()); Ok(TransactionMode::None) } PragmaName::ListTypes => { let base_reg = register; program.alloc_registers(5); // 6 total (1 already allocated) // Built-in types: NULL parent, encode, decode, default, operators for builtin in &["INTEGER", "REAL", "TEXT", "BLOB", "ANY"] { program.emit_string8(builtin.to_string(), base_reg); program.emit_null(base_reg + 1, None); program.emit_null(base_reg + 2, None); program.emit_null(base_reg + 3, None); program.emit_null(base_reg + 4, None); program.emit_null(base_reg + 5, None); program.emit_result_row(base_reg, 6); } // Custom types from the type registry are only shown when strict mode is enabled // Custom types are always shown since strict mode is always enabled { // Skip aliases where key != canonical name let mut type_names: Vec<_> = schema .type_registry .iter() .filter(|(key, td)| *key == &td.name.to_lowercase()) .map(|(key, _)| key) .collect(); type_names.sort(); for type_name in type_names { let type_def = &schema.type_registry[type_name]; let display_name = if type_def.params.is_empty() { type_def.name.clone() } else { let params: Vec = type_def .params .iter() .map(|p| match &p.ty { Some(ty) => format!("{} {}", p.name, ty), None => p.name.clone(), }) .collect(); format!("{}({})", type_def.name, params.join(", ")) }; program.emit_string8(display_name, base_reg); program.emit_string8(type_def.base.clone(), base_reg + 1); if let Some(ref expr) = type_def.encode { program.emit_string8(expr.to_string(), base_reg + 2); } else { program.emit_null(base_reg + 2, None); } if let Some(ref expr) = type_def.decode { program.emit_string8(expr.to_string(), base_reg + 3); } else { program.emit_null(base_reg + 3, None); } if let Some(ref expr) = type_def.default { program.emit_string8(expr.to_string(), base_reg + 4); } else { program.emit_null(base_reg + 4, None); } if type_def.operators.is_empty() { program.emit_null(base_reg + 5, None); } else { let ops: Vec = type_def .operators .iter() .map(|op| match &op.func_name { Some(f) => format!("'{}' {}", op.op, f), None => format!("'{}'", op.op), }) .collect(); program.emit_string8(ops.join(", "), base_reg + 5); } program.emit_result_row(base_reg, 6); } } let pragma_meta = pragma_for(&pragma); for col_name in pragma_meta.columns.iter() { program.add_pragma_result_column(col_name.to_string()); } Ok(TransactionMode::None) } } } /// Helper function to emit column information for PRAGMA table_info /// Used by both tables and views since they now have the same column emission logic fn emit_columns_for_table_info( program: &mut ProgramBuilder, columns: &[crate::schema::Column], base_reg: usize, extended: bool, ) { // According to the SQLite documentation: "The 'cid' column should not be taken to // mean more than 'rank within the current result set'." // Therefore, we enumerate only after filtering out hidden columns (if extended set to false). let mut cid = 0; for column in columns.iter() { // Determine column type which will be used for filtering in table_info pragma or as "hidden" column for table_xinfo pragma. // // SQLite docs about table_xinfo: // > The output has the same columns as for PRAGMA table_info plus a column, "hidden", // > whose value signifies a normal column (0), a dynamic or stored generated column (2 or 3), // > or a hidden column in a virtual table (1). The rows for which this field is non-zero are those omitted for PRAGMA table_info. // // (see https://sqlite.org/pragma.html#pragma_table_xinfo) let column_type = if column.hidden() { // hidden column 1 } else { // normal column 0 }; if !extended && column_type != 0 { // This pragma (table_info) does not show information about generated columns or hidden columns. continue; } // cid program.emit_int(cid as i64, base_reg); cid += 1; // name program.emit_string8(column.name.clone().unwrap_or_default(), base_reg + 1); // type program.emit_string8(column.ty_str.clone(), base_reg + 2); // notnull program.emit_bool(column.notnull(), base_reg + 3); // dflt_value match &column.default { None => { program.emit_null(base_reg + 4, None); } Some(expr) => { program.emit_string8(expr.to_string(), base_reg + 4); } } // pk program.emit_bool(column.primary_key(), base_reg + 5); if extended { program.emit_int(column_type, base_reg + 6); } program.emit_result_row(base_reg, 6 + if extended { 1 } else { 0 }); } } fn update_auto_vacuum_mode( auto_vacuum_mode: AutoVacuumMode, largest_root_page_number: u32, pager: Arc, ) -> crate::Result<()> { pager.io.block(|| { pager.with_header_mut(|header| { header.vacuum_mode_largest_root_page = largest_root_page_number.into() }) })?; pager.set_auto_vacuum_mode(auto_vacuum_mode); Ok(()) } fn update_cache_size( value: i64, pager: Arc, connection: Arc, ) -> crate::Result<()> { let mut cache_size_unformatted: i64 = value; let mut cache_size = if cache_size_unformatted < 0 { let kb = cache_size_unformatted .checked_abs() .unwrap_or(i64::MAX) .saturating_mul(1024); let page_size = pager .io .block(|| pager.with_header(|header| header.page_size)) .unwrap_or_default() .get() as i64; if page_size == 0 { turso_soft_unreachable!("Page size cannot be zero"); return Err(LimboError::InternalError( "Page size cannot be zero".to_string(), )); } kb / page_size } else { value }; if cache_size > CacheSize::MAX_SAFE { cache_size = 0; cache_size_unformatted = 0; } if cache_size < 0 { cache_size = 0; cache_size_unformatted = 0; } let final_cache_size = if cache_size < CacheSize::MIN { cache_size_unformatted = CacheSize::MIN; CacheSize::MIN } else { cache_size }; connection.set_cache_size(cache_size_unformatted as i32); pager .change_page_cache_size(final_cache_size as usize) .map_err(|e| LimboError::InternalError(format!("Failed to update page cache size: {e}")))?; Ok(()) } pub const TURSO_CDC_DEFAULT_TABLE_NAME: &str = "turso_cdc"; pub const TURSO_CDC_VERSION_TABLE_NAME: &str = "turso_cdc_version"; pub use crate::CDC_VERSION_CURRENT; fn update_page_size(connection: Arc, page_size: u32) -> crate::Result<()> { connection.reset_page_size(page_size)?; Ok(()) } fn is_database_empty(schema: &Schema, pager: &Arc) -> crate::Result { if schema.tables.len() > 1 { return Ok(false); } if let Some(table_arc) = schema.tables.values().next() { let table_name = match table_arc.as_ref() { crate::schema::Table::BTree(tbl) => &tbl.name, crate::schema::Table::Virtual(tbl) => &tbl.name, crate::schema::Table::FromClauseSubquery(tbl) => &tbl.name, }; if table_name != "sqlite_schema" { return Ok(false); } } let db_size_result = pager .io .block(|| pager.with_header(|header| header.database_size.get())); match db_size_result { Err(_) => Ok(true), Ok(0 | 1) => Ok(true), Ok(_) => Ok(false), } } ================================================ FILE: core/translate/result_row.rs ================================================ use crate::turso_assert_eq; use crate::{ vdbe::{ builder::ProgramBuilder, insn::{to_u16, IdxInsertFlags, InsertFlags, Insn}, BranchOffset, }, Result, }; use turso_parser::ast; use super::{ emitter::{LimitCtx, Resolver}, expr::{ emit_array_decode, expr_is_array, translate_expr, translate_expr_no_constant_opt, NoConstantOptReason, }, plan::{Distinctness, QueryDestination, ResultSetColumn, SelectPlan, TableReferences}, }; /// Emits the bytecode for: /// - all result columns /// - result row (or if a subquery, yields to the parent query) /// - limit #[allow(clippy::too_many_arguments)] pub fn emit_select_result( program: &mut ProgramBuilder, resolver: &Resolver, plan: &SelectPlan, label_on_limit_reached: Option, offset_jump_to: Option, reg_nonagg_emit_once_flag: Option, reg_offset: Option, reg_result_cols_start: usize, limit_ctx: Option, ) -> Result<()> { let has_distinct = matches!(plan.distinctness, Distinctness::Distinct { .. }); if !has_distinct { if let (Some(jump_to), Some(_)) = (offset_jump_to, label_on_limit_reached) { emit_offset(program, jump_to, reg_offset); } } let start_reg = reg_result_cols_start; // For EXISTS subqueries, we usually only need to determine whether any row exists, not // the row's column values. The result is simply writing `1` to the result register. // // Important: SELECT DISTINCT deduplication reads the result registers as its key. So for // EXISTS(SELECT DISTINCT ...), we must still evaluate the result expressions; otherwise the // dedup key is uninitialized and EXISTS can incorrectly evaluate to false. let skip_column_eval = matches!( plan.query_destination, QueryDestination::ExistsSubqueryResult { .. } ) && !matches!(plan.distinctness, Distinctness::Distinct { .. }); // For compound selects (UNION, UNION ALL, etc.), multiple subselects may share the same // result column registers. If constants are moved to the init section, they can be // overwritten by subsequent subselects before being used. // // We conservatively disable constant optimization for EphemeralIndex, CoroutineYield, // and EphemeralTable destinations because these are used in compound select contexts // and CTE materialization. This is slightly over-broad (e.g., simple INSERT INTO ... // SELECT with no UNION doesn't need this), but we lack context here to distinguish // compound vs non-compound cases. let disable_constant_opt = matches!( plan.query_destination, QueryDestination::EphemeralIndex { .. } | QueryDestination::CoroutineYield { .. } | QueryDestination::EphemeralTable { .. } ); if !skip_column_eval { for (i, rc) in plan.result_columns.iter().enumerate().filter(|(_, rc)| { // For aggregate queries, we handle columns differently; example: select id, first_name, sum(age) from users limit 1; // 1. Columns with aggregates (e.g., sum(age)) are computed in each iteration of aggregation // 2. Non-aggregate columns (e.g., id, first_name) are only computed once in the first iteration // This filter ensures we only emit expressions for non aggregate columns once, // preserving previously calculated values while updating aggregate results // For all other queries where reg_nonagg_emit_once_flag is none we do nothing. reg_nonagg_emit_once_flag.is_some() && rc.contains_aggregates || reg_nonagg_emit_once_flag.is_none() }) { let reg = start_reg + i; if disable_constant_opt { translate_expr_no_constant_opt( program, Some(&plan.table_references), &rc.expr, reg, resolver, NoConstantOptReason::RegisterReuse, )?; } else { translate_expr( program, Some(&plan.table_references), &rc.expr, reg, resolver, )?; } } } // Emit ArrayDecode for result columns that produce array blobs. // Array values are stored as record-format blobs internally; decode // them to JSON text for user-facing display. if !skip_column_eval { emit_array_decode_for_results( program, &plan.result_columns, &plan.table_references, start_reg, resolver, )?; } // Handle SELECT DISTINCT deduplication if let Distinctness::Distinct { ctx } = &plan.distinctness { let distinct_ctx = ctx.as_ref().expect("distinct context must exist"); let num_regs = plan.result_columns.len(); distinct_ctx.emit_deduplication_insns(program, num_regs, start_reg); } if has_distinct { if let (Some(jump_to), Some(_)) = (offset_jump_to, label_on_limit_reached) { emit_offset(program, jump_to, reg_offset); } } emit_result_row_and_limit(program, plan, start_reg, limit_ctx, label_on_limit_reached)?; Ok(()) } /// Emits bytecode to send column values to a destination. /// This is the core "emit to destination" logic shared by both regular SELECT emission /// and compound SELECT (UNION/INTERSECT/EXCEPT) final output. /// /// Parameters: /// - `start_reg`: First register containing column values /// - `num_columns`: Number of columns to emit /// - `destination`: Where to send the columns (ResultRow, EphemeralIndex, etc.) pub fn emit_columns_to_destination( program: &mut ProgramBuilder, destination: &QueryDestination, start_reg: usize, num_columns: usize, ) -> Result<()> { match destination { QueryDestination::ResultRows => { program.emit_insn(Insn::ResultRow { start_reg, count: num_columns, }); } QueryDestination::EphemeralIndex { cursor_id: index_cursor_id, index: dedupe_index, affinity_str, is_delete, } => { if *is_delete { program.emit_insn(Insn::IdxDelete { start_reg, num_regs: num_columns, cursor_id: *index_cursor_id, raise_error_if_no_matching_entry: false, }); } else { let record_reg = program.alloc_register(); // For ephemeral indexes, we may need to: // 1. Reorder columns if index key order differs from result order (seek indexes) // 2. Append a unique rowid to allow duplicate key values (has_rowid indexes) let (record_start, record_count) = if dedupe_index.ephemeral && dedupe_index.columns.len() == num_columns { // Check if reordering is needed (any pos_in_table != idx_pos) let needs_reorder = dedupe_index .columns .iter() .enumerate() .any(|(idx_pos, col)| col.pos_in_table != idx_pos); if needs_reorder { // Reorder columns to match index key order let extra_for_rowid = if dedupe_index.has_rowid { 1 } else { 0 }; let reordered_start = program.alloc_registers(num_columns + extra_for_rowid); for (idx_pos, idx_col) in dedupe_index.columns.iter().enumerate() { program.emit_insn(Insn::Copy { src_reg: start_reg + idx_col.pos_in_table, dst_reg: reordered_start + idx_pos, extra_amount: 0, }); } if dedupe_index.has_rowid { program.emit_insn(Insn::Sequence { cursor_id: *index_cursor_id, target_reg: reordered_start + num_columns, }); } (reordered_start, num_columns + extra_for_rowid) } else if dedupe_index.has_rowid { // No reordering needed, but need to append rowid let new_start = program.alloc_registers(num_columns + 1); program.emit_insn(Insn::Copy { src_reg: start_reg, dst_reg: new_start, extra_amount: num_columns - 1, }); program.emit_insn(Insn::Sequence { cursor_id: *index_cursor_id, target_reg: new_start + num_columns, }); (new_start, num_columns + 1) } else { // No reordering or rowid needed - use registers directly (start_reg, num_columns) } } else { (start_reg, num_columns) }; program.emit_insn(Insn::MakeRecord { start_reg: to_u16(record_start), count: to_u16(record_count), dest_reg: to_u16(record_reg), index_name: Some(dedupe_index.name.clone()), affinity_str: affinity_str.as_ref().map(|s| (**s).clone()), }); program.emit_insn(Insn::IdxInsert { cursor_id: *index_cursor_id, record_reg, unpacked_start: None, unpacked_count: None, flags: IdxInsertFlags::new().no_op_duplicate(), }); } } QueryDestination::EphemeralTable { cursor_id: table_cursor_id, table, rowid_mode, } => { // Prevent constant hoisting so that each row's constants are evaluated inline. // This is critical for UNION ALL where each arm should insert its own values. program.constant_span_end_all(); let record_reg = program.alloc_register(); match rowid_mode { super::plan::EphemeralRowidMode::FromResultColumns => { // For single-column case (RowidOnly materialization), we still need to // create a record containing the rowid so it can be read back later. if num_columns == 1 { program.emit_insn(Insn::MakeRecord { start_reg: to_u16(start_reg), count: to_u16(1), dest_reg: to_u16(record_reg), index_name: Some(table.name.clone()), affinity_str: None, }); } else if num_columns > 1 { program.emit_insn(Insn::MakeRecord { start_reg: to_u16(start_reg), count: to_u16(num_columns - 1), dest_reg: to_u16(record_reg), index_name: Some(table.name.clone()), affinity_str: None, }); } program.emit_insn(Insn::Insert { cursor: *table_cursor_id, key_reg: start_reg + (num_columns - 1), record_reg, flag: InsertFlags::new() .require_seek() .is_ephemeral_table_insert(), table_name: table.name.clone(), }); } super::plan::EphemeralRowidMode::Auto => { if num_columns > 0 { program.emit_insn(Insn::MakeRecord { start_reg: to_u16(start_reg), count: to_u16(num_columns), dest_reg: to_u16(record_reg), index_name: Some(table.name.clone()), affinity_str: None, }); } let rowid_reg = program.alloc_register(); program.emit_insn(Insn::NewRowid { cursor: *table_cursor_id, rowid_reg, prev_largest_reg: 0, }); program.emit_insn(Insn::Insert { cursor: *table_cursor_id, key_reg: rowid_reg, record_reg, flag: InsertFlags::new().is_ephemeral_table_insert(), table_name: table.name.clone(), }); } } } QueryDestination::CoroutineYield { yield_reg, .. } => { program.emit_insn(Insn::Yield { yield_reg: *yield_reg, end_offset: BranchOffset::Offset(0), subtype_clear_start_reg: start_reg, subtype_clear_count: num_columns, }); } QueryDestination::ExistsSubqueryResult { result_reg } => { program.emit_insn(Insn::Integer { value: 1, dest: *result_reg, }); } QueryDestination::RowValueSubqueryResult { result_reg_start, num_regs, } => { turso_assert_eq!( num_columns, *num_regs, "Row value subqueries should have the same number of result columns as the number of registers" ); program.emit_insn(Insn::Copy { src_reg: start_reg, dst_reg: *result_reg_start, extra_amount: num_regs - 1, }); } QueryDestination::RowSet { rowset_reg } => { turso_assert_eq!( num_columns, 1, "RowSet should only have one result column (rowid)" ); program.emit_insn(Insn::RowSetAdd { rowset_reg: *rowset_reg, value_reg: start_reg, }); } QueryDestination::Unset => unreachable!("Unset query destination should not be reached"), } Ok(()) } /// Emits the bytecode for: /// - result row (or if a subquery, yields to the parent query) /// - limit pub fn emit_result_row_and_limit( program: &mut ProgramBuilder, plan: &SelectPlan, result_columns_start_reg: usize, limit_ctx: Option, label_on_limit_reached: Option, ) -> Result<()> { emit_columns_to_destination( program, &plan.query_destination, result_columns_start_reg, plan.result_columns.len(), )?; if plan.limit.is_some() { if label_on_limit_reached.is_none() { return Ok(()); } let limit_ctx = limit_ctx.expect("limit_ctx must be Some if plan.limit is Some"); program.emit_insn(Insn::DecrJumpZero { reg: limit_ctx.reg_limit, target_pc: label_on_limit_reached.unwrap(), }); } Ok(()) } pub fn emit_offset(program: &mut ProgramBuilder, jump_to: BranchOffset, reg_offset: Option) { let Some(reg_offset) = ®_offset else { return; }; program.emit_insn(Insn::IfPos { reg: *reg_offset, target_pc: jump_to, decrement_by: 1, }); } /// Emit ArrayDecode for result columns that produce array record blobs. /// Array values are stored as record-format blobs internally; this converts /// them to JSON text for user-facing display, just before ResultRow. pub(crate) fn emit_array_decode_for_results( program: &mut ProgramBuilder, result_columns: &[ResultSetColumn], table_references: &TableReferences, start_reg: usize, resolver: &Resolver, ) -> Result<()> { for (i, rc) in result_columns.iter().enumerate() { // Check if this is a column reference to an array column let array_col = if let ast::Expr::Column { table, column, .. } = &rc.expr { table_references .find_table_by_internal_id(*table) .map(|(_, t)| t) .and_then(|t| t.get_column_at(*column)) .filter(|col| col.is_array()) } else { None }; // Check if this expression produces an array (function call, || operator, etc.) let is_array_expr = array_col.is_none() && expr_is_array(&rc.expr, Some(table_references)); if array_col.is_some() || is_array_expr { let reg = start_reg + i; let skip = program.allocate_label(); program.emit_insn(Insn::IsNull { reg, target_pc: skip, }); if let Some(col) = array_col { emit_array_decode(program, reg, col, resolver)?; } else { program.emit_insn(Insn::ArrayDecode { reg }); } program.preassign_label_to_next_insn(skip); } } Ok(()) } ================================================ FILE: core/translate/rollback.rs ================================================ use crate::{ vdbe::{ builder::ProgramBuilder, insn::{Insn, SavepointOp}, }, Result, }; use turso_parser::ast::Name; /// Emits bytecode for `SAVEPOINT `. pub fn translate_savepoint(program: &mut ProgramBuilder, name: Name) -> Result<()> { program.emit_insn(Insn::Savepoint { op: SavepointOp::Begin, name: name.as_str().to_ascii_lowercase(), }); Ok(()) } /// Emits bytecode for `RELEASE [SAVEPOINT] `. pub fn translate_release(program: &mut ProgramBuilder, name: Name) -> Result<()> { program.emit_insn(Insn::Savepoint { op: SavepointOp::Release, name: name.as_str().to_ascii_lowercase(), }); Ok(()) } /// Emits bytecode for either full transaction rollback or `ROLLBACK TO` named savepoint. pub fn translate_rollback( program: &mut ProgramBuilder, _txn_name: Option, savepoint_name: Option, ) -> Result<()> { if let Some(savepoint_name) = savepoint_name { program.emit_insn(Insn::Savepoint { op: SavepointOp::RollbackTo, name: savepoint_name.as_str().to_ascii_lowercase(), }); } else { program.emit_insn(Insn::AutoCommit { auto_commit: true, rollback: true, }); program.rollback(); } Ok(()) } ================================================ FILE: core/translate/schema.rs ================================================ use crate::sync::Arc; use crate::ast; use crate::ext::VTabImpl; use crate::function::{Deterministic, Func, MathFunc, ScalarFunc}; use crate::schema::{ create_table, translate_ident_to_string_literal, BTreeTable, ColDef, Column, SchemaObjectType, Table, Type, RESERVED_TABLE_PREFIXES, SQLITE_SEQUENCE_TABLE_NAME, TURSO_TYPES_TABLE_NAME, }; use crate::stats::STATS_TABLE; use crate::storage::pager::CreateBTreeFlags; use crate::translate::emitter::{ emit_cdc_autocommit_commit, emit_cdc_full_record, emit_cdc_insns, prepare_cdc_if_necessary, OperationMode, Resolver, }; use crate::translate::expr::{walk_expr, WalkControl}; use crate::translate::fkeys::emit_fk_drop_table_check; use crate::translate::planner::ROWID_STRS; use crate::translate::{ProgramBuilder, ProgramBuilderOpts}; use crate::util::{ escape_sql_string_literal, normalize_ident, PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX, }; use crate::vdbe::builder::CursorType; use crate::vdbe::insn::{ to_u16, {CmpInsFlags, Cookie, InsertFlags, Insn, RegisterOrLiteral}, }; use crate::Connection; use crate::{bail_parse_error, CaptureDataChangesExt, Result}; use turso_ext::VTabKind; use turso_parser::ast::ColumnDefinition; /// Validate a CHECK constraint expression at CREATE TABLE / ALTER TABLE ADD COLUMN time. /// Rejects non-existent columns, non-existent functions, aggregates, window functions, /// bind parameters, and subqueries. pub(crate) fn validate_check_expr( expr: &ast::Expr, table_name: &str, column_names: &[&str], resolver: &Resolver, ) -> Result<()> { let normalized_table = normalize_ident(table_name); walk_expr(expr, &mut |e: &ast::Expr| -> Result { match e { ast::Expr::Id(name) | ast::Expr::Name(name) => { let n = normalize_ident(name.as_str()); if !column_names.iter().any(|c| normalize_ident(c) == n) && !ROWID_STRS.iter().any(|r| r.eq_ignore_ascii_case(&n)) { bail_parse_error!("no such column: {}", name.as_str()); } } ast::Expr::Qualified(tbl, col) => { if normalize_ident(tbl.as_str()) != normalized_table { bail_parse_error!("no such column: {}.{}", tbl.as_str(), col.as_str()); } let cn = normalize_ident(col.as_str()); if !column_names.iter().any(|c| normalize_ident(c) == cn) && !ROWID_STRS.iter().any(|r| r.eq_ignore_ascii_case(&cn)) { bail_parse_error!("no such column: {}", col.as_str()); } } ast::Expr::DoublyQualified(db, tbl, col) => { bail_parse_error!( "no such column: {}.{}.{}", db.as_str(), tbl.as_str(), col.as_str() ); } ast::Expr::FunctionCall { name, args, filter_over, .. } => { if filter_over.over_clause.is_some() { bail_parse_error!("misuse of window function {}()", name.as_str()); } if let Some(func) = resolver.resolve_function(name.as_str(), args.len()) { if matches!(func, Func::Agg(..)) { bail_parse_error!("misuse of aggregate function {}()", name.as_str()); } if matches!(func, Func::Window(..)) { bail_parse_error!("misuse of window function {}()", name.as_str()); } } else { bail_parse_error!("no such function: {}", name.as_str()); } } ast::Expr::FunctionCallStar { name, filter_over } => { if filter_over.over_clause.is_some() { bail_parse_error!("misuse of window function {}()", name.as_str()); } if let Some(func) = resolver.resolve_function(name.as_str(), 0) { if matches!(func, Func::Agg(..)) { bail_parse_error!("misuse of aggregate function {}()", name.as_str()); } if matches!(func, Func::Window(..)) { bail_parse_error!("misuse of window function {}()", name.as_str()); } } else { bail_parse_error!("no such function: {}", name.as_str()); } } ast::Expr::Variable(_) => { bail_parse_error!("parameters prohibited in CHECK constraints"); } ast::Expr::Subquery(_) | ast::Expr::Exists(_) | ast::Expr::InSelect { .. } => { bail_parse_error!("subqueries prohibited in CHECK constraints"); } _ => {} } Ok(WalkControl::Continue) })?; Ok(()) } fn validate_default_expr(expr: &ast::Expr, col: &ColumnDefinition) -> Result<()> { walk_expr(expr, &mut |e: &ast::Expr| -> Result { match e { ast::Expr::Column { .. } | ast::Expr::RowId { .. } | ast::Expr::Name(_) | ast::Expr::Qualified(_, _) | ast::Expr::DoublyQualified(_, _, _) | ast::Expr::Variable(_) | ast::Expr::Raise(_, _) | ast::Expr::Exists(_) | ast::Expr::InSelect { .. } | ast::Expr::InTable { .. } | ast::Expr::Subquery(_) | ast::Expr::SubqueryResult { .. } | ast::Expr::Id(_) => { bail_parse_error!( "default value of column [{}] is not constant", col.col_name.as_str() ); } _ => Ok(WalkControl::Continue), } })?; Ok(()) } /// Resolved type of an expression node for strict type checking of CHECK constraints. /// In STRICT tables, every comparison operand must have a determinable, compatible type. /// If a type cannot be determined (e.g. function calls), the user must use an explicit CAST. #[derive(Debug, Clone, PartialEq)] enum CheckExprType { Integer, Real, Text, Blob, Any, Null, CustomType(String), } impl CheckExprType { fn is_numeric(&self) -> bool { matches!(self, Self::Integer | Self::Real) } fn is_compatible_with(&self, other: &Self) -> bool { match (self, other) { (Self::Null, _) | (_, Self::Null) => true, (Self::Any, _) | (_, Self::Any) => true, (a, b) if a == b => true, (a, b) if a.is_numeric() && b.is_numeric() => true, _ => false, } } fn display_name(&self) -> &str { match self { Self::Integer => "INTEGER", Self::Real => "REAL", Self::Text => "TEXT", Self::Blob => "BLOB", Self::Any => "ANY", Self::Null => "NULL", Self::CustomType(name) => name.as_str(), } } } /// Resolve the type of an expression node in a CHECK constraint. /// Returns an error if the type cannot be determined — the user must use CAST. fn resolve_check_expr_type( expr: &ast::Expr, columns: &[&ast::ColumnDefinition], resolver: &Resolver, ) -> Result { use ast::{Literal, Operator, UnaryOperator}; match expr { ast::Expr::Id(name) | ast::Expr::Name(name) => { let n = normalize_ident(name.as_str()); // rowid/oid/_rowid_ are INTEGER if ROWID_STRS.iter().any(|r| r.eq_ignore_ascii_case(&n)) { return Ok(CheckExprType::Integer); } for col in columns { if normalize_ident(col.col_name.as_str()) == n { return resolve_column_type(col, resolver); } } bail_parse_error!("no such column: {}", name.as_str()); } ast::Expr::Qualified(_tbl, col) => { let cn = normalize_ident(col.as_str()); if ROWID_STRS.iter().any(|r| r.eq_ignore_ascii_case(&cn)) { return Ok(CheckExprType::Integer); } for c in columns { if normalize_ident(c.col_name.as_str()) == cn { return resolve_column_type(c, resolver); } } bail_parse_error!("no such column: {}", col.as_str()); } ast::Expr::Literal(lit) => match lit { Literal::Numeric(s) => { if s.contains('.') || s.contains('e') || s.contains('E') { Ok(CheckExprType::Real) } else { Ok(CheckExprType::Integer) } } Literal::String(_) => Ok(CheckExprType::Text), Literal::Blob(_) => Ok(CheckExprType::Blob), Literal::Null => Ok(CheckExprType::Null), Literal::True | Literal::False => Ok(CheckExprType::Integer), Literal::CurrentDate | Literal::CurrentTime | Literal::CurrentTimestamp => { Ok(CheckExprType::Text) } Literal::Keyword(s) => { bail_parse_error!( "cannot determine type of '{}' in CHECK constraint; use CAST", s ); } }, ast::Expr::Parenthesized(exprs) => { if exprs.len() == 1 { resolve_check_expr_type(&exprs[0], columns, resolver) } else { bail_parse_error!( "cannot determine type of expression in CHECK constraint; use CAST" ); } } ast::Expr::Cast { type_name, .. } => { if let Some(ref tn) = type_name { resolve_type_name(&tn.name, resolver) } else { bail_parse_error!( "cannot determine type of CAST in CHECK constraint; use CAST with explicit type" ); } } ast::Expr::Unary(op, inner) => match op { UnaryOperator::Negative | UnaryOperator::Positive => { let inner_ty = resolve_check_expr_type(inner, columns, resolver)?; if !inner_ty.is_numeric() && inner_ty != CheckExprType::Null { bail_parse_error!( "unary minus/plus requires a numeric type, got {}", inner_ty.display_name() ); } Ok(inner_ty) } UnaryOperator::BitwiseNot => Ok(CheckExprType::Integer), UnaryOperator::Not => Ok(CheckExprType::Integer), }, ast::Expr::Binary(lhs, op, rhs) => { match op { // Arithmetic: both must be numeric, result follows promotion rules Operator::Add | Operator::Subtract | Operator::Multiply | Operator::Divide => { let lty = resolve_check_expr_type(lhs, columns, resolver)?; let rty = resolve_check_expr_type(rhs, columns, resolver)?; if lty == CheckExprType::Null || rty == CheckExprType::Null { return Ok(CheckExprType::Null); } if !lty.is_numeric() || !rty.is_numeric() { bail_parse_error!( "arithmetic requires numeric types, got {} and {}", lty.display_name(), rty.display_name() ); } if lty == CheckExprType::Real || rty == CheckExprType::Real { Ok(CheckExprType::Real) } else { Ok(CheckExprType::Integer) } } Operator::Modulus => Ok(CheckExprType::Integer), Operator::Concat => Ok(CheckExprType::Text), Operator::BitwiseAnd | Operator::BitwiseOr | Operator::LeftShift | Operator::RightShift => Ok(CheckExprType::Integer), // Logical: recurse to find nested comparisons Operator::And | Operator::Or => { // The result of AND/OR is boolean (integer), but we need to // recurse to validate any comparisons inside. validate_check_types_in_expr(lhs, columns, resolver)?; validate_check_types_in_expr(rhs, columns, resolver)?; Ok(CheckExprType::Integer) } // Comparison operators: validate type compatibility and return Integer (boolean) Operator::Equals | Operator::NotEquals | Operator::Less | Operator::LessEquals | Operator::Greater | Operator::GreaterEquals => { let lty = resolve_check_expr_type(lhs, columns, resolver)?; let rty = resolve_check_expr_type(rhs, columns, resolver)?; if !lty.is_compatible_with(&rty) { bail_parse_error!( "type mismatch in CHECK constraint: cannot compare {} with {}", lty.display_name(), rty.display_name() ); } Ok(CheckExprType::Integer) } // IS/IS NOT are NULL-checking operators, skip type validation Operator::Is | Operator::IsNot => Ok(CheckExprType::Integer), _ => { bail_parse_error!( "cannot determine type of expression in CHECK constraint; use CAST" ); } } } ast::Expr::NotNull(_) | ast::Expr::IsNull(_) => Ok(CheckExprType::Integer), ast::Expr::FunctionCall { name, args, .. } => { if let Some(func) = resolver.resolve_function(name.as_str(), args.len()) { resolve_func_return_type(&func, name.as_str(), args, columns, resolver) } else { bail_parse_error!( "cannot determine return type of function {}() in CHECK constraint; \ wrap with CAST to specify the type, e.g. CAST({}(...) AS INTEGER)", name.as_str(), name.as_str() ); } } ast::Expr::FunctionCallStar { name, .. } => { if let Some(func) = resolver.resolve_function(name.as_str(), 0) { resolve_func_return_type(&func, name.as_str(), &[], columns, resolver) } else { bail_parse_error!( "cannot determine return type of function {}() in CHECK constraint; \ wrap with CAST to specify the type, e.g. CAST({}(...) AS INTEGER)", name.as_str(), name.as_str() ); } } _ => { bail_parse_error!("cannot determine type of expression in CHECK constraint; use CAST"); } } } /// Resolve the return type of a built-in function for CHECK constraint type checking. fn resolve_func_return_type( func: &Func, name: &str, args: &[Box], columns: &[&ast::ColumnDefinition], resolver: &Resolver, ) -> Result { match func { Func::Scalar(sf) => resolve_scalar_func_return_type(sf, args, columns, resolver), Func::Math(mf) => resolve_math_func_return_type(mf), #[cfg(feature = "json")] Func::Json(jf) => resolve_json_func_return_type(jf), Func::Agg(_) => bail_parse_error!("misuse of aggregate function {}()", name), Func::External(_) => { bail_parse_error!( "cannot determine return type of function {}() in CHECK constraint; \ wrap with CAST to specify the type, e.g. CAST({}(...) AS INTEGER)", name, name ); } _ => Ok(CheckExprType::Any), } } /// Resolve the return type of a scalar function. fn resolve_scalar_func_return_type( func: &ScalarFunc, args: &[Box], columns: &[&ast::ColumnDefinition], resolver: &Resolver, ) -> Result { match func { // Functions that always return INTEGER ScalarFunc::Length | ScalarFunc::OctetLength | ScalarFunc::Instr | ScalarFunc::Unicode | ScalarFunc::Sign | ScalarFunc::Random | ScalarFunc::Changes | ScalarFunc::TotalChanges | ScalarFunc::LastInsertRowid | ScalarFunc::Glob | ScalarFunc::Like | ScalarFunc::Likely | ScalarFunc::Unlikely | ScalarFunc::Likelihood | ScalarFunc::BooleanToInt | ScalarFunc::IntToBoolean | ScalarFunc::IsAutocommit | ScalarFunc::ConnTxnId | ScalarFunc::TestUintLt | ScalarFunc::TestUintEq | ScalarFunc::NumericLt | ScalarFunc::NumericEq | ScalarFunc::ValidateIpAddr | ScalarFunc::UnixEpoch => Ok(CheckExprType::Integer), // Functions that always return TEXT ScalarFunc::Upper | ScalarFunc::Lower | ScalarFunc::Trim | ScalarFunc::LTrim | ScalarFunc::RTrim | ScalarFunc::Hex | ScalarFunc::Soundex | ScalarFunc::Quote | ScalarFunc::Replace | ScalarFunc::Substr | ScalarFunc::Substring | ScalarFunc::Char | ScalarFunc::Concat | ScalarFunc::ConcatWs | ScalarFunc::Typeof | ScalarFunc::SqliteVersion | ScalarFunc::TursoVersion | ScalarFunc::SqliteSourceId | ScalarFunc::Date | ScalarFunc::Time | ScalarFunc::DateTime | ScalarFunc::StrfTime | ScalarFunc::TimeDiff | ScalarFunc::Printf | ScalarFunc::StringReverse => Ok(CheckExprType::Text), // Functions that always return REAL ScalarFunc::Round | ScalarFunc::JulianDay => Ok(CheckExprType::Real), // Functions that always return BLOB ScalarFunc::RandomBlob | ScalarFunc::ZeroBlob | ScalarFunc::Unhex => { Ok(CheckExprType::Blob) } // Functions whose return type depends on arguments ScalarFunc::Abs | ScalarFunc::Nullif => { if let Some(arg) = args.first() { resolve_check_expr_type(arg, columns, resolver) } else { Ok(CheckExprType::Any) } } ScalarFunc::Coalesce | ScalarFunc::IfNull => { for arg in args { let ty = resolve_check_expr_type(arg, columns, resolver)?; if ty != CheckExprType::Null { return Ok(ty); } } Ok(CheckExprType::Null) } ScalarFunc::Min | ScalarFunc::Max => { if let Some(first) = args.first() { resolve_check_expr_type(first, columns, resolver) } else { Ok(CheckExprType::Any) } } ScalarFunc::Iif => { // iif(cond, then_val, else_val) — return type of then_val if args.len() >= 2 { resolve_check_expr_type(&args[1], columns, resolver) } else { Ok(CheckExprType::Any) } } // Internal/custom type functions ScalarFunc::TestUintEncode | ScalarFunc::TestUintDecode | ScalarFunc::TestUintAdd | ScalarFunc::TestUintSub | ScalarFunc::TestUintMul | ScalarFunc::TestUintDiv | ScalarFunc::NumericEncode | ScalarFunc::NumericDecode | ScalarFunc::NumericAdd | ScalarFunc::NumericSub | ScalarFunc::NumericMul | ScalarFunc::NumericDiv => Ok(CheckExprType::Blob), // Remaining functions — treat as ANY _ => Ok(CheckExprType::Any), } } /// Resolve the return type of a math function. fn resolve_math_func_return_type(func: &MathFunc) -> Result { match func { // Floor/ceil/trunc return INTEGER for integer inputs, but always produce numeric results MathFunc::Ceil | MathFunc::Ceiling | MathFunc::Floor | MathFunc::Trunc => { Ok(CheckExprType::Integer) } // All other math functions return REAL _ => Ok(CheckExprType::Real), } } /// Resolve the return type of a JSON function. #[cfg(feature = "json")] fn resolve_json_func_return_type(func: &crate::function::JsonFunc) -> Result { use crate::function::JsonFunc; match func { // Functions that return TEXT (JSON text) JsonFunc::Json | JsonFunc::JsonArray | JsonFunc::JsonObject | JsonFunc::JsonPatch | JsonFunc::JsonRemove | JsonFunc::JsonReplace | JsonFunc::JsonInsert | JsonFunc::JsonSet | JsonFunc::JsonPretty | JsonFunc::JsonQuote | JsonFunc::JsonType => Ok(CheckExprType::Text), // Functions that return BLOB (JSONB binary) JsonFunc::Jsonb | JsonFunc::JsonbArray | JsonFunc::JsonbObject | JsonFunc::JsonbPatch | JsonFunc::JsonbRemove | JsonFunc::JsonbReplace | JsonFunc::JsonbInsert | JsonFunc::JsonbSet => Ok(CheckExprType::Blob), // Functions that return INTEGER JsonFunc::JsonArrayLength | JsonFunc::JsonErrorPosition | JsonFunc::JsonValid => { Ok(CheckExprType::Integer) } // Extract functions can return any type JsonFunc::JsonExtract | JsonFunc::JsonbExtract | JsonFunc::JsonArrowExtract | JsonFunc::JsonArrowShiftExtract => Ok(CheckExprType::Any), } } /// Resolve a column's type from its definition. fn resolve_column_type(col: &ast::ColumnDefinition, resolver: &Resolver) -> Result { if let Some(ref col_type) = col.col_type { resolve_type_name(&col_type.name, resolver) } else { // No type specified — in STRICT tables this would be caught elsewhere, // but treat as ANY for CHECK validation purposes. Ok(CheckExprType::Any) } } /// Resolve a type name string to a CheckExprType. fn resolve_type_name(type_name: &str, resolver: &Resolver) -> Result { let name_bytes = type_name.as_bytes(); let result = turso_macros::match_ignore_ascii_case!(match name_bytes { b"INT" | b"INTEGER" => Some(CheckExprType::Integer), b"REAL" | b"FLOAT" | b"DOUBLE" => Some(CheckExprType::Real), b"TEXT" => Some(CheckExprType::Text), b"BLOB" => Some(CheckExprType::Blob), b"ANY" => Some(CheckExprType::Any), _ => None, }); if let Some(ty) = result { return Ok(ty); } // Check if it's a known custom type if resolver .schema() .get_type_def_unchecked(type_name) .is_some() { return Ok(CheckExprType::CustomType(type_name.to_lowercase())); } bail_parse_error!("unknown type '{}' in CHECK constraint", type_name); } /// Walk a CHECK expression and validate that all comparisons have compatible types. /// Only called for STRICT tables. fn validate_check_types_in_expr( expr: &ast::Expr, columns: &[&ast::ColumnDefinition], resolver: &Resolver, ) -> Result<()> { use ast::Operator; match expr { ast::Expr::Binary(lhs, op, rhs) => { match op { Operator::Equals | Operator::NotEquals | Operator::Less | Operator::LessEquals | Operator::Greater | Operator::GreaterEquals => { let lty = resolve_check_expr_type(lhs, columns, resolver)?; let rty = resolve_check_expr_type(rhs, columns, resolver)?; if !lty.is_compatible_with(&rty) { bail_parse_error!( "type mismatch in CHECK constraint: cannot compare {} with {}", lty.display_name(), rty.display_name() ); } } Operator::And | Operator::Or => { validate_check_types_in_expr(lhs, columns, resolver)?; validate_check_types_in_expr(rhs, columns, resolver)?; } // Arithmetic, concat, bitwise — recurse to find nested comparisons _ => { validate_check_types_in_expr(lhs, columns, resolver)?; validate_check_types_in_expr(rhs, columns, resolver)?; } } } ast::Expr::Between { lhs, start, end, .. } => { let lty = resolve_check_expr_type(lhs, columns, resolver)?; let sty = resolve_check_expr_type(start, columns, resolver)?; let ety = resolve_check_expr_type(end, columns, resolver)?; if !lty.is_compatible_with(&sty) { bail_parse_error!( "type mismatch in CHECK BETWEEN: cannot compare {} with {}", lty.display_name(), sty.display_name() ); } if !lty.is_compatible_with(&ety) { bail_parse_error!( "type mismatch in CHECK BETWEEN: cannot compare {} with {}", lty.display_name(), ety.display_name() ); } } ast::Expr::InList { lhs, rhs, .. } => { let lty = resolve_check_expr_type(lhs, columns, resolver)?; for item in rhs { let ity = resolve_check_expr_type(item, columns, resolver)?; if !lty.is_compatible_with(&ity) { bail_parse_error!( "type mismatch in CHECK IN list: cannot compare {} with {}", lty.display_name(), ity.display_name() ); } } } ast::Expr::Parenthesized(exprs) => { for e in exprs { validate_check_types_in_expr(e, columns, resolver)?; } } ast::Expr::Unary(_, inner) => { validate_check_types_in_expr(inner, columns, resolver)?; } ast::Expr::Case { base, when_then_pairs, else_expr, } => { if let Some(op) = base { validate_check_types_in_expr(op, columns, resolver)?; } for (when_expr, then_expr) in when_then_pairs { validate_check_types_in_expr(when_expr, columns, resolver)?; validate_check_types_in_expr(then_expr, columns, resolver)?; } if let Some(else_e) = else_expr { validate_check_types_in_expr(else_e, columns, resolver)?; } } ast::Expr::FunctionCall { args, .. } => { for arg in args { validate_check_types_in_expr(arg, columns, resolver)?; } } // Leaf nodes and other expressions: no nested comparisons to validate _ => {} } Ok(()) } fn validate(body: &ast::CreateTableBody, table_name: &str, resolver: &Resolver) -> Result<()> { if let ast::CreateTableBody::ColumnsAndConstraints { options, columns, constraints, } = &body { if options.contains_without_rowid() { bail_parse_error!("WITHOUT ROWID tables are not supported"); } let column_names: Vec<&str> = columns.iter().map(|c| c.col_name.as_str()).collect(); for i in 0..columns.len() { let col_i = &columns[i]; for constraint in &col_i.constraints { match &constraint.constraint { ast::ColumnConstraint::Check(expr) => { validate_check_expr(expr, table_name, &column_names, resolver)?; } ast::ColumnConstraint::Generated { .. } => { bail_parse_error!("GENERATED columns are not supported yet"); } ast::ColumnConstraint::Default(expr) => { let expr = translate_ident_to_string_literal(expr).unwrap_or_else(|| expr.clone()); validate_default_expr(&expr, col_i)? } _ => {} } } for j in &columns[(i + 1)..] { if col_i .col_name .as_str() .eq_ignore_ascii_case(j.col_name.as_str()) { bail_parse_error!("duplicate column name: {}", j.col_name.as_str()); } } } for constraint in constraints { if let ast::TableConstraint::Check(ref expr) = constraint.constraint { validate_check_expr(expr, table_name, &column_names, resolver)?; } } let is_strict = options.contains_strict(); for c in columns { if let Some(ref col_type) = c.col_type { let type_name = &col_type.name; let name_bytes = type_name.as_bytes(); let is_builtin = turso_macros::match_ignore_ascii_case!(match name_bytes { b"INT" | b"INTEGER" | b"REAL" | b"TEXT" | b"BLOB" | b"ANY" => true, _ => false, }); // Array columns require STRICT tables because the encode/decode // pipeline is only emitted for STRICT tables. if col_type.is_array() && !is_strict { bail_parse_error!( "array type columns require STRICT tables: {}.{}", table_name, c.col_name ); } if !is_builtin && is_strict { // On non-STRICT tables any type name is allowed and is // treated as a plain affinity hint (no encode/decode). // Custom type validation only applies to STRICT tables. let type_def = resolver.schema().get_type_def_unchecked(type_name); { match type_def { None => { bail_parse_error!( "unknown datatype for {}.{}: \"{}\"", table_name, c.col_name, type_name ); } Some(td) if td.user_params().next().is_some() => { // Parametric type: verify the column provides the right // number of user parameters (excluding `value`). let provided = match &col_type.size { Some(ast::TypeSize::TypeSize(_, _)) => 2, Some(ast::TypeSize::MaxSize(_)) => 1, None => 0, }; let expected = td.user_params().count(); if provided != expected { bail_parse_error!( "type \"{}\" requires {} parameter(s), got {}", type_name, expected, provided ); } } Some(_) => {} } } } } } // In STRICT tables, validate that CHECK constraint comparisons have // compatible types. This catches type mismatches at CREATE TABLE time // rather than producing wrong results at INSERT/UPDATE time. if is_strict { let col_refs: Vec<&ast::ColumnDefinition> = columns.iter().collect(); for col in columns { for constraint in &col.constraints { if let ast::ColumnConstraint::Check(expr) = &constraint.constraint { validate_check_types_in_expr(expr, &col_refs, resolver)?; } } } for constraint in constraints { if let ast::TableConstraint::Check(ref expr) = constraint.constraint { validate_check_types_in_expr(expr, &col_refs, resolver)?; } } } } Ok(()) } pub fn translate_create_table( tbl_name: ast::QualifiedName, resolver: &Resolver, temporary: bool, if_not_exists: bool, body: ast::CreateTableBody, program: &mut ProgramBuilder, connection: &Connection, ) -> Result<()> { let database_id = resolver.resolve_database_id(&tbl_name)?; if crate::is_attached_db(database_id) { let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version); program.begin_write_on_database(database_id, schema_cookie); } let normalized_tbl_name = normalize_ident(tbl_name.name.as_str()); if temporary { bail_parse_error!("TEMPORARY table not supported yet"); } validate(&body, &normalized_tbl_name, resolver)?; // Gate array column types behind the experimental custom types flag. if !connection.experimental_custom_types_enabled() { if let ast::CreateTableBody::ColumnsAndConstraints { columns, .. } = &body { for col in columns { if col.col_type.as_ref().is_some_and(|t| t.is_array()) { bail_parse_error!( "Array column types require --experimental-custom-types flag" ); } } } } let opts = ProgramBuilderOpts { num_cursors: 1, approx_num_insns: 30, approx_num_labels: 1, }; program.extend(&opts); if !connection.is_mvcc_bootstrap_connection() && RESERVED_TABLE_PREFIXES .iter() .any(|prefix| normalized_tbl_name.starts_with(prefix)) && !connection.is_nested_stmt() { bail_parse_error!( "Object name reserved for internal use: {}", tbl_name.name.as_str() ); } // Check for name conflicts with existing schema objects if let Some(object_type) = resolver.with_schema(database_id, |s| s.get_object_type(&normalized_tbl_name)) { match object_type { // IF NOT EXISTS suppresses errors for table/view conflicts SchemaObjectType::Table | SchemaObjectType::View if if_not_exists => { return Ok(()); } _ => { let type_str = match object_type { SchemaObjectType::Table => "table", SchemaObjectType::View => "view", SchemaObjectType::Index => "index", }; bail_parse_error!("{} {} already exists", type_str, normalized_tbl_name); } } } let mut has_autoincrement = false; if let ast::CreateTableBody::ColumnsAndConstraints { columns, constraints, .. } = &body { for col in columns { for constraint in &col.constraints { if let ast::ColumnConstraint::PrimaryKey { auto_increment, .. } = constraint.constraint { if auto_increment { has_autoincrement = true; break; } } } if has_autoincrement { break; } } if !has_autoincrement { for constraint in constraints { if let ast::TableConstraint::PrimaryKey { auto_increment, .. } = constraint.constraint { if auto_increment { has_autoincrement = true; break; } } } } } if has_autoincrement && connection.mvcc_enabled() { bail_parse_error!( "AUTOINCREMENT is not supported in MVCC mode (journal_mode=experimental_mvcc)" ); } let schema_master_table = resolver.schema().get_btree_table(SQLITE_TABLEID).unwrap(); let sqlite_schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(schema_master_table)); program.emit_insn(Insn::OpenWrite { cursor_id: sqlite_schema_cursor_id, root_page: 1i64.into(), db: database_id, }); let cdc_table = prepare_cdc_if_necessary(program, resolver.schema(), SQLITE_TABLEID)?; let created_sequence_table = if has_autoincrement && resolver.with_schema(database_id, |s| { s.get_table(SQLITE_SEQUENCE_TABLE_NAME).is_none() }) { let seq_table_root_reg = program.alloc_register(); program.emit_insn(Insn::CreateBtree { db: database_id, root: seq_table_root_reg, flags: CreateBTreeFlags::new_table(), }); let seq_sql = "CREATE TABLE sqlite_sequence(name,seq)"; emit_schema_entry( program, resolver, sqlite_schema_cursor_id, cdc_table.as_ref().map(|x| x.0), SchemaEntryType::Table, SQLITE_SEQUENCE_TABLE_NAME, SQLITE_SEQUENCE_TABLE_NAME, seq_table_root_reg, Some(seq_sql.to_string()), )?; true } else { false }; let sql = create_table_body_to_str(&tbl_name, &body)?; let parse_schema_label = program.allocate_label(); // TODO: ReadCookie // TODO: If // TODO: SetCookie // TODO: SetCookie let table_root_reg = program.alloc_register(); program.emit_insn(Insn::CreateBtree { db: database_id, root: table_root_reg, flags: CreateBTreeFlags::new_table(), }); // Create an automatic index B-tree if needed // // NOTE: we are deviating from SQLite bytecode here. For some reason, SQLite first creates a placeholder entry // for the table in sqlite_schema, then writes the index to sqlite_schema, then UPDATEs the table placeholder entry // in sqlite_schema with actual data. // // What we do instead is: // 1. Create the table B-tree // 2. Create the index B-tree // 3. Add the table entry to sqlite_schema // 4. Add the index entry to sqlite_schema // // I.e. we skip the weird song and dance with the placeholder entry. Unclear why sqlite does this. // The sqlite code has this comment: // // "This just creates a place-holder record in the sqlite_schema table. // The record created does not contain anything yet. It will be replaced // by the real entry in code generated at sqlite3EndTable()." // // References: // https://github.com/sqlite/sqlite/blob/95f6df5b8d55e67d1e34d2bff217305a2f21b1fb/src/build.c#L1355 // https://github.com/sqlite/sqlite/blob/95f6df5b8d55e67d1e34d2bff217305a2f21b1fb/src/build.c#L2856-L2871 // https://github.com/sqlite/sqlite/blob/95f6df5b8d55e67d1e34d2bff217305a2f21b1fb/src/build.c#L1334C5-L1336C65 let index_regs = collect_autoindexes(&body, program, &normalized_tbl_name)?; if let Some(index_regs) = index_regs.as_ref() { for index_reg in index_regs.iter() { program.emit_insn(Insn::CreateBtree { db: database_id, root: *index_reg, flags: CreateBTreeFlags::new_index(), }); } } let table = resolver.schema().get_btree_table(SQLITE_TABLEID).unwrap(); let sqlite_schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(table)); program.emit_insn(Insn::OpenWrite { cursor_id: sqlite_schema_cursor_id, root_page: 1i64.into(), db: database_id, }); let cdc_table = prepare_cdc_if_necessary(program, resolver.schema(), SQLITE_TABLEID)?; emit_schema_entry( program, resolver, sqlite_schema_cursor_id, cdc_table.as_ref().map(|x| x.0), SchemaEntryType::Table, &normalized_tbl_name, &normalized_tbl_name, table_root_reg, Some(sql), )?; if let Some(index_regs) = index_regs { for (idx, index_reg) in index_regs.into_iter().enumerate() { let index_name = format!( "{PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX}{}_{}", normalized_tbl_name, idx + 1 ); emit_schema_entry( program, resolver, sqlite_schema_cursor_id, None, SchemaEntryType::Index, &index_name, &normalized_tbl_name, index_reg, None, )?; } } program.resolve_label(parse_schema_label, program.offset()); let schema_version = resolver.with_schema(database_id, |s| s.schema_version); program.emit_insn(Insn::SetCookie { db: database_id, cookie: Cookie::SchemaVersion, value: schema_version as i32 + 1, p5: 0, }); // TODO: remove format, it sucks for performance but is convenient let escaped_tbl_name = escape_sql_string_literal(&normalized_tbl_name); let mut parse_schema_where_clause = format!("tbl_name = '{escaped_tbl_name}' AND type != 'trigger'"); if created_sequence_table { parse_schema_where_clause.push_str(" OR tbl_name = 'sqlite_sequence'"); } program.emit_insn(Insn::ParseSchema { db: database_id, where_clause: Some(parse_schema_where_clause), }); // TODO: SqlExec Ok(()) } #[derive(Debug, Clone, Copy)] pub enum SchemaEntryType { Table, Index, View, Trigger, } impl SchemaEntryType { fn as_str(&self) -> &'static str { match self { SchemaEntryType::Table => "table", SchemaEntryType::Index => "index", SchemaEntryType::View => "view", SchemaEntryType::Trigger => "trigger", } } } pub const SQLITE_TABLEID: &str = "sqlite_schema"; #[allow(clippy::too_many_arguments)] pub fn emit_schema_entry( program: &mut ProgramBuilder, resolver: &Resolver, sqlite_schema_cursor_id: usize, cdc_table_cursor_id: Option, entry_type: SchemaEntryType, name: &str, tbl_name: &str, root_page_reg: usize, sql: Option, ) -> Result<()> { let rowid_reg = program.alloc_register(); program.emit_insn(Insn::NewRowid { cursor: sqlite_schema_cursor_id, rowid_reg, prev_largest_reg: 0, }); let type_reg = program.emit_string8_new_reg(entry_type.as_str().to_string()); program.emit_string8_new_reg(name.to_string()); program.emit_string8_new_reg(tbl_name.to_string()); let table_root_reg = program.alloc_register(); if root_page_reg == 0 { program.emit_insn(Insn::Integer { dest: table_root_reg, value: 0, // virtual tables in sqlite always have rootpage=0 }); } else { program.emit_insn(Insn::Copy { src_reg: root_page_reg, dst_reg: table_root_reg, extra_amount: 0, }); } let sql_reg = program.alloc_register(); if let Some(sql) = sql { program.emit_string8(sql, sql_reg); } else { program.emit_null(sql_reg, None); } let record_reg = program.alloc_register(); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(type_reg), count: to_u16(5), dest_reg: to_u16(record_reg), index_name: None, affinity_str: None, }); program.emit_insn(Insn::Insert { cursor: sqlite_schema_cursor_id, key_reg: rowid_reg, record_reg, flag: InsertFlags::new(), table_name: tbl_name.to_string(), }); if let Some(cdc_table_cursor_id) = cdc_table_cursor_id { let after_record_reg = if program.capture_data_changes_info().has_after() { Some(record_reg) } else { None }; emit_cdc_insns( program, resolver, OperationMode::INSERT, cdc_table_cursor_id, rowid_reg, None, after_record_reg, None, SQLITE_TABLEID, )?; emit_cdc_autocommit_commit(program, resolver, cdc_table_cursor_id)?; } Ok(()) } /// Check if an automatic PRIMARY KEY index is required for the table. /// If so, create a register for the index root page and return it. /// /// An automatic PRIMARY KEY index is not required if: /// - The table has no PRIMARY KEY /// - The table has a single-column PRIMARY KEY whose typename is _exactly_ "INTEGER" e.g. not "INT". /// In this case, the PRIMARY KEY column becomes an alias for the rowid. /// /// Otherwise, an automatic PRIMARY KEY index is required. fn collect_autoindexes( body: &ast::CreateTableBody, program: &mut ProgramBuilder, tbl_name: &str, ) -> Result>> { let table = create_table(tbl_name, body, 0)?; let mut regs: Vec = Vec::new(); // include UNIQUE singles, include PK single only if not rowid alias for us in table.unique_sets.iter().filter(|us| us.columns.len() == 1) { let (col_name, _sort) = us.columns.first().unwrap(); let Some((_pos, col)) = table.get_column(col_name) else { bail_parse_error!("Column {col_name} not found in table {}", table.name); }; let needs_index = if us.is_primary_key { !(col.primary_key() && col.is_rowid_alias()) } else { // UNIQUE single needs an index true }; if needs_index { regs.push(program.alloc_register()); } } for _us in table.unique_sets.iter().filter(|us| us.columns.len() > 1) { regs.push(program.alloc_register()); } if regs.is_empty() { Ok(None) } else { Ok(Some(regs)) } } fn create_table_body_to_str( tbl_name: &ast::QualifiedName, body: &ast::CreateTableBody, ) -> crate::Result { let mut sql = String::new(); sql.push_str(format!("CREATE TABLE {} {}", tbl_name.name.as_ident(), body).as_str()); match body { ast::CreateTableBody::ColumnsAndConstraints { columns: _, constraints: _, options: _, } => {} ast::CreateTableBody::AsSelect(_select) => { crate::bail_parse_error!("CREATE TABLE AS SELECT is not supported") } } Ok(sql) } fn create_vtable_body_to_str(vtab: &ast::CreateVirtualTable, module: Arc) -> String { let args = vtab .args .iter() .map(|arg| arg.to_string()) .collect::>() .join(", "); let if_not_exists = if vtab.if_not_exists { "IF NOT EXISTS " } else { "" }; let ext_args = vtab .args .iter() .map(|a| turso_ext::Value::from_text(a.to_string())) .collect::>(); let schema = module .implementation .create_schema(ext_args) .unwrap_or_default(); let vtab_args = if let Some(first_paren) = schema.find('(') { let closing_paren = schema.rfind(')').unwrap_or_default(); &schema[first_paren..=closing_paren] } else { "()" }; format!( "CREATE VIRTUAL TABLE {} {} USING {}{}\n /*{}{}*/", vtab.tbl_name.name.as_ident(), if_not_exists, vtab.module_name.as_ident(), if args.is_empty() { String::new() } else { format!("({args})") }, vtab.tbl_name.name.as_ident(), vtab_args ) } pub fn translate_create_virtual_table( vtab: ast::CreateVirtualTable, resolver: &Resolver, program: &mut ProgramBuilder, connection: &Arc, ) -> Result<()> { if connection.mvcc_enabled() { bail_parse_error!("Virtual tables are not supported in MVCC mode"); } let ast::CreateVirtualTable { if_not_exists, tbl_name, module_name, args, } = &vtab; let table_name = tbl_name.name.as_str().to_string(); let module_name_str = module_name.as_str().to_string(); let args_vec = args.clone(); let Some(vtab_module) = resolver.symbol_table.vtab_modules.get(&module_name_str) else { bail_parse_error!("no such module: {}", module_name_str); }; if !vtab_module.module_kind.eq(&VTabKind::VirtualTable) { bail_parse_error!("module {} is not a virtual table", module_name_str); }; if resolver.schema().get_table(&table_name).is_some() { if *if_not_exists { return Ok(()); } bail_parse_error!("Table {} already exists", tbl_name); } let opts = ProgramBuilderOpts { num_cursors: 2, approx_num_insns: 40, approx_num_labels: 2, }; program.extend(&opts); let module_name_reg = program.emit_string8_new_reg(module_name_str.clone()); let table_name_reg = program.emit_string8_new_reg(table_name.clone()); let args_reg = if !args_vec.is_empty() { let args_start = program.alloc_register(); // Emit string8 instructions for each arg for (i, arg) in args_vec.iter().enumerate() { program.emit_string8(arg.clone(), args_start + i); } let args_record_reg = program.alloc_register(); // VCreate expects an array of args as a record program.emit_insn(Insn::MakeRecord { start_reg: to_u16(args_start), count: to_u16(args_vec.len()), dest_reg: to_u16(args_record_reg), index_name: None, affinity_str: None, }); Some(args_record_reg) } else { None }; program.emit_insn(Insn::VCreate { module_name: module_name_reg, table_name: table_name_reg, args_reg, }); let table = resolver.schema().get_btree_table(SQLITE_TABLEID).unwrap(); let sqlite_schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(table)); program.emit_insn(Insn::OpenWrite { cursor_id: sqlite_schema_cursor_id, root_page: 1i64.into(), db: crate::MAIN_DB_ID, }); let cdc_table = prepare_cdc_if_necessary(program, resolver.schema(), SQLITE_TABLEID)?; let sql = create_vtable_body_to_str(&vtab, vtab_module.clone()); emit_schema_entry( program, resolver, sqlite_schema_cursor_id, cdc_table.map(|x| x.0), SchemaEntryType::Table, tbl_name.name.as_str(), tbl_name.name.as_str(), 0, // virtual tables dont have a root page Some(sql), )?; program.emit_insn(Insn::SetCookie { db: crate::MAIN_DB_ID, cookie: Cookie::SchemaVersion, value: resolver.schema().schema_version as i32 + 1, p5: 0, }); let escaped_table_name = escape_sql_string_literal(&table_name); let parse_schema_where_clause = format!("tbl_name = '{escaped_table_name}' AND type != 'trigger'"); program.emit_insn(Insn::ParseSchema { db: sqlite_schema_cursor_id, where_clause: Some(parse_schema_where_clause), }); Ok(()) } /// Validates whether a DROP TABLE operation is allowed on the given table name. fn validate_drop_table( resolver: &Resolver, tbl_name: &str, connection: &Arc, ) -> Result<()> { if !connection.is_nested_stmt() && crate::schema::is_system_table(tbl_name) // special case, allow dropping `sqlite_stat1` && !tbl_name.eq_ignore_ascii_case(STATS_TABLE) { bail_parse_error!("Cannot drop system table {}", tbl_name); } // Check if this is a materialized view - if so, refuse to drop it with DROP TABLE if resolver.schema().is_materialized_view(tbl_name) { bail_parse_error!( "Cannot DROP TABLE on materialized view {tbl_name}. Use DROP VIEW instead.", ); } Ok(()) } pub fn translate_drop_table( tbl_name: ast::QualifiedName, resolver: &mut Resolver, if_exists: bool, program: &mut ProgramBuilder, connection: &Arc, ) -> Result<()> { let database_id = resolver.resolve_database_id(&tbl_name)?; let name = tbl_name.name.as_str(); let opts = ProgramBuilderOpts { num_cursors: 4, approx_num_insns: 40, approx_num_labels: 4, }; program.extend(&opts); if crate::is_attached_db(database_id) { let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version); program.begin_write_on_database(database_id, schema_cookie); } let Some(table) = resolver.with_schema(database_id, |s| s.get_table(name)) else { if if_exists { return Ok(()); } bail_parse_error!("No such table: {name}"); }; validate_drop_table(resolver, name, connection)?; // Check if foreign keys are enabled and if this table is referenced by foreign keys // Fire FK actions (CASCADE, SET NULL, SET DEFAULT) or check for violations (RESTRICT, NO ACTION) if connection.foreign_keys_enabled() && resolver.with_schema(database_id, |s| s.any_resolved_fks_referencing(name)) { emit_fk_drop_table_check(program, resolver, name, connection, database_id)?; } let cdc_table = prepare_cdc_if_necessary(program, resolver.schema(), SQLITE_TABLEID)?; let null_reg = program.alloc_register(); // r1 program.emit_null(null_reg, None); let table_name_and_root_page_register = program.alloc_register(); // r2, this register is special because it's first used to track table name and then moved root page let table_reg = program.emit_string8_new_reg(normalize_ident(tbl_name.name.as_str())); // r3 program.mark_last_insn_constant(); let _table_type = program.emit_string8_new_reg("trigger".to_string()); // r4 program.mark_last_insn_constant(); let row_id_reg = program.alloc_register(); // r5 let schema_table = resolver.schema().get_btree_table(SQLITE_TABLEID).unwrap(); let sqlite_schema_cursor_id_0 = program.alloc_cursor_id( // cursor 0 CursorType::BTreeTable(schema_table.clone()), ); program.emit_insn(Insn::OpenWrite { cursor_id: sqlite_schema_cursor_id_0, root_page: 1i64.into(), db: database_id, }); // 1. Remove all entries from the schema table related to the table we are dropping (including triggers) // loop to beginning of schema table let end_metadata_label = program.allocate_label(); let metadata_loop = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: sqlite_schema_cursor_id_0, pc_if_empty: end_metadata_label, }); program.preassign_label_to_next_insn(metadata_loop); // start loop on schema table program.emit_column_or_rowid( sqlite_schema_cursor_id_0, 2, table_name_and_root_page_register, ); let next_label = program.allocate_label(); program.emit_insn(Insn::Ne { lhs: table_name_and_root_page_register, rhs: table_reg, target_pc: next_label, flags: CmpInsFlags::default(), collation: program.curr_collation(), }); program.emit_insn(Insn::RowId { cursor_id: sqlite_schema_cursor_id_0, dest: row_id_reg, }); if let Some((cdc_cursor_id, _)) = cdc_table { let table_type = program.emit_string8_new_reg("table".to_string()); // r4 program.mark_last_insn_constant(); let skip_cdc_label = program.allocate_label(); let entry_type_reg = program.alloc_register(); program.emit_column_or_rowid(sqlite_schema_cursor_id_0, 0, entry_type_reg); program.emit_insn(Insn::Ne { lhs: entry_type_reg, rhs: table_type, target_pc: skip_cdc_label, flags: CmpInsFlags::default(), collation: None, }); let before_record_reg = if program.capture_data_changes_info().has_before() { Some(emit_cdc_full_record( program, &schema_table.columns, sqlite_schema_cursor_id_0, row_id_reg, schema_table.is_strict, )) } else { None }; emit_cdc_insns( program, resolver, OperationMode::DELETE, cdc_cursor_id, row_id_reg, before_record_reg, None, None, SQLITE_TABLEID, )?; program.resolve_label(skip_cdc_label, program.offset()); } program.emit_insn(Insn::Delete { cursor_id: sqlite_schema_cursor_id_0, table_name: SQLITE_TABLEID.to_string(), is_part_of_update: false, }); program.resolve_label(next_label, program.offset()); program.emit_insn(Insn::Next { cursor_id: sqlite_schema_cursor_id_0, pc_if_next: metadata_loop, }); program.preassign_label_to_next_insn(end_metadata_label); // end of loop on schema table if let Some((cdc_cursor_id, _)) = cdc_table { emit_cdc_autocommit_commit(program, resolver, cdc_cursor_id)?; } // 2. Destroy the indices within a loop let indices = resolver.schema().get_indices(tbl_name.name.as_str()); for index in indices { if index.index_method.is_some() && !index.is_backing_btree_index() { // Index methods without backing btree need special destroy handling let cursor_id = program.alloc_cursor_index(None, index)?; program.emit_insn(Insn::IndexMethodDestroy { db: database_id, cursor_id, }); } else { program.emit_insn(Insn::Destroy { db: database_id, root: index.root_page, former_root_reg: 0, // no autovacuum (https://www.sqlite.org/opcode.html#Destroy) is_temp: 0, }); } // 3. TODO: Open an ephemeral table, and read over triggers from schema table into ephemeral table // Requires support via https://github.com/tursodatabase/turso/pull/768 // 4. TODO: Open a write cursor to the schema table and re-insert all triggers into the sqlite schema table from the ephemeral table and delete old trigger // Requires support via https://github.com/tursodatabase/turso/pull/768 } // 3. Destroy the table structure match table.as_ref() { Table::BTree(table) => { program.emit_insn(Insn::Destroy { db: database_id, root: table.root_page, former_root_reg: table_name_and_root_page_register, is_temp: 0, }); } Table::Virtual(vtab) => { // From what I see, TableValuedFunction is not stored in the schema as a table. // But this line here below is a safeguard in case this behavior changes in the future // And mirrors what SQLite does. if matches!(vtab.kind, turso_ext::VTabKind::TableValuedFunction) { return Err(crate::LimboError::ParseError(format!( "table {} may not be dropped", vtab.name ))); } program.emit_insn(Insn::VDestroy { table_name: vtab.name.clone(), db: database_id, }); } Table::FromClauseSubquery(..) => panic!("FromClauseSubquery can't be dropped"), }; let schema_data_register = program.alloc_register(); let schema_row_id_register = program.alloc_register(); program.emit_null(schema_data_register, Some(schema_row_id_register)); // All of the following processing needs to be done only if the table is not a virtual table if table.btree().is_some() { // 4. Open an ephemeral table, and read over the entry from the schema table whose root page was moved in the destroy operation // cursor id 1 let sqlite_schema_cursor_id_1 = program.alloc_cursor_id(CursorType::BTreeTable(schema_table.clone())); let simple_table_rc = Arc::new(BTreeTable { root_page: 0, // Not relevant for ephemeral table definition name: "ephemeral_scratch".to_string(), has_rowid: true, has_autoincrement: false, primary_key_columns: vec![], columns: vec![Column::new( Some("rowid".to_string()), "INTEGER".to_string(), None, None, Type::Integer, None, ColDef::default(), )], is_strict: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }); // cursor id 2 let ephemeral_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(simple_table_rc)); program.emit_insn(Insn::OpenEphemeral { cursor_id: ephemeral_cursor_id, is_table: true, }); let if_not_label = program.allocate_label(); program.emit_insn(Insn::IfNot { reg: table_name_and_root_page_register, target_pc: if_not_label, jump_if_null: true, // jump anyway }); program.emit_insn(Insn::OpenRead { cursor_id: sqlite_schema_cursor_id_1, root_page: 1i64, db: database_id, }); let schema_column_0_register = program.alloc_register(); let schema_column_1_register = program.alloc_register(); let schema_column_2_register = program.alloc_register(); let moved_to_root_page_register = program.alloc_register(); // the register that will contain the root page number the last root page is moved to let schema_column_4_register = program.alloc_register(); let prev_root_page_register = program.alloc_register(); // the register that will contain the root page number that the last root page was on before VACUUM let _r14 = program.alloc_register(); // Unsure why this register is allocated but putting it in here to make comparison with SQLite easier let new_record_register = program.alloc_register(); // Loop to copy over row id's from the schema table for rows that have the same root page as the one that was moved let copy_schema_to_temp_table_loop_end_label = program.allocate_label(); let copy_schema_to_temp_table_loop = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: sqlite_schema_cursor_id_1, pc_if_empty: copy_schema_to_temp_table_loop_end_label, }); program.preassign_label_to_next_insn(copy_schema_to_temp_table_loop); // start loop on schema table program.emit_column_or_rowid(sqlite_schema_cursor_id_1, 3, prev_root_page_register); // The label and Insn::Ne are used to skip over any rows in the schema table that don't have the root page that was moved let next_label = program.allocate_label(); program.emit_insn(Insn::Ne { lhs: prev_root_page_register, rhs: table_name_and_root_page_register, target_pc: next_label, flags: CmpInsFlags::default(), collation: program.curr_collation(), }); program.emit_insn(Insn::RowId { cursor_id: sqlite_schema_cursor_id_1, dest: schema_row_id_register, }); program.emit_insn(Insn::Insert { cursor: ephemeral_cursor_id, key_reg: schema_row_id_register, record_reg: schema_data_register, flag: InsertFlags::new(), table_name: "scratch_table".to_string(), }); program.resolve_label(next_label, program.offset()); program.emit_insn(Insn::Next { cursor_id: sqlite_schema_cursor_id_1, pc_if_next: copy_schema_to_temp_table_loop, }); program.preassign_label_to_next_insn(copy_schema_to_temp_table_loop_end_label); // End loop to copy over row id's from the schema table for rows that have the same root page as the one that was moved program.resolve_label(if_not_label, program.offset()); // 5. Open a write cursor to the schema table and re-insert the records placed in the ephemeral table but insert the correct root page now program.emit_insn(Insn::OpenWrite { cursor_id: sqlite_schema_cursor_id_1, root_page: 1i64.into(), db: database_id, }); // Loop to copy over row id's from the ephemeral table and then re-insert into the schema table with the correct root page let copy_temp_table_to_schema_loop_end_label = program.allocate_label(); let copy_temp_table_to_schema_loop = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: ephemeral_cursor_id, pc_if_empty: copy_temp_table_to_schema_loop_end_label, }); program.preassign_label_to_next_insn(copy_temp_table_to_schema_loop); // start loop on schema table program.emit_insn(Insn::RowId { cursor_id: ephemeral_cursor_id, dest: schema_row_id_register, }); // the next_label and Insn::NotExists are used to skip patching any rows in the schema table that don't have the row id that was written to the ephemeral table let next_label = program.allocate_label(); program.emit_insn(Insn::NotExists { cursor: sqlite_schema_cursor_id_1, rowid_reg: schema_row_id_register, target_pc: next_label, }); program.emit_column_or_rowid(sqlite_schema_cursor_id_1, 0, schema_column_0_register); program.emit_column_or_rowid(sqlite_schema_cursor_id_1, 1, schema_column_1_register); program.emit_column_or_rowid(sqlite_schema_cursor_id_1, 2, schema_column_2_register); let root_page = table.get_root_page()?; program.emit_insn(Insn::Integer { value: root_page, dest: moved_to_root_page_register, }); program.emit_column_or_rowid(sqlite_schema_cursor_id_1, 4, schema_column_4_register); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(schema_column_0_register), count: to_u16(5), dest_reg: to_u16(new_record_register), index_name: None, affinity_str: None, }); program.emit_insn(Insn::Delete { cursor_id: sqlite_schema_cursor_id_1, table_name: SQLITE_TABLEID.to_string(), is_part_of_update: false, }); program.emit_insn(Insn::Insert { cursor: sqlite_schema_cursor_id_1, key_reg: schema_row_id_register, record_reg: new_record_register, flag: InsertFlags::new(), table_name: SQLITE_TABLEID.to_string(), }); program.resolve_label(next_label, program.offset()); program.emit_insn(Insn::Next { cursor_id: ephemeral_cursor_id, pc_if_next: copy_temp_table_to_schema_loop, }); program.preassign_label_to_next_insn(copy_temp_table_to_schema_loop_end_label); // End loop to copy over row id's from the ephemeral table and then re-insert into the schema table with the correct root page } // if drops table, sequence table should reset. if let Some(seq_table) = resolver .schema() .get_table(SQLITE_SEQUENCE_TABLE_NAME) .and_then(|t| t.btree()) { let seq_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(seq_table.clone())); let seq_table_name_reg = program.alloc_register(); let dropped_table_name_reg = program.emit_string8_new_reg(tbl_name.name.as_str().to_string()); program.mark_last_insn_constant(); program.emit_insn(Insn::OpenWrite { cursor_id: seq_cursor_id, root_page: seq_table.root_page.into(), db: database_id, }); let end_loop_label = program.allocate_label(); let loop_start_label = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: seq_cursor_id, pc_if_empty: end_loop_label, }); program.preassign_label_to_next_insn(loop_start_label); program.emit_column_or_rowid(seq_cursor_id, 0, seq_table_name_reg); let continue_loop_label = program.allocate_label(); program.emit_insn(Insn::Ne { lhs: seq_table_name_reg, rhs: dropped_table_name_reg, target_pc: continue_loop_label, flags: CmpInsFlags::default(), collation: None, }); program.emit_insn(Insn::Delete { cursor_id: seq_cursor_id, table_name: SQLITE_SEQUENCE_TABLE_NAME.to_string(), is_part_of_update: false, }); program.resolve_label(continue_loop_label, program.offset()); program.emit_insn(Insn::Next { cursor_id: seq_cursor_id, pc_if_next: loop_start_label, }); program.preassign_label_to_next_insn(end_loop_label); } // Clean up turso_cdc_version entry for the dropped table (if version table exists) if let Some(version_table) = resolver .schema() .get_table(crate::translate::pragma::TURSO_CDC_VERSION_TABLE_NAME) .and_then(|t| t.btree()) { let ver_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(version_table.clone())); let ver_table_name_reg = program.alloc_register(); let dropped_name_reg = program.emit_string8_new_reg(tbl_name.name.as_str().to_string()); program.mark_last_insn_constant(); program.emit_insn(Insn::OpenWrite { cursor_id: ver_cursor_id, root_page: version_table.root_page.into(), db: crate::MAIN_DB_ID, }); let end_ver_loop_label = program.allocate_label(); let ver_loop_start_label = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: ver_cursor_id, pc_if_empty: end_ver_loop_label, }); program.preassign_label_to_next_insn(ver_loop_start_label); program.emit_column_or_rowid(ver_cursor_id, 0, ver_table_name_reg); let continue_ver_label = program.allocate_label(); program.emit_insn(Insn::Ne { lhs: ver_table_name_reg, rhs: dropped_name_reg, target_pc: continue_ver_label, flags: CmpInsFlags::default(), collation: None, }); program.emit_insn(Insn::Delete { cursor_id: ver_cursor_id, table_name: crate::translate::pragma::TURSO_CDC_VERSION_TABLE_NAME.to_string(), is_part_of_update: false, }); program.resolve_label(continue_ver_label, program.offset()); program.emit_insn(Insn::Next { cursor_id: ver_cursor_id, pc_if_next: ver_loop_start_label, }); program.preassign_label_to_next_insn(end_ver_loop_label); } // Drop the in-memory structures for the table program.emit_insn(Insn::DropTable { db: database_id, _p2: 0, _p3: 0, table_name: tbl_name.name.as_str().to_string(), }); let current_schema_version = resolver.with_schema(database_id, |s| s.schema_version); program.emit_insn(Insn::SetCookie { db: database_id, cookie: Cookie::SchemaVersion, value: current_schema_version as i32 + 1, p5: 0, }); Ok(()) } /// Validate an encode or decode expression for safety. /// Rejects subqueries, aggregates, and window functions. fn validate_type_expr(expr: &ast::Expr, kind: &str, resolver: &Resolver) -> Result<()> { walk_expr(expr, &mut |e: &ast::Expr| -> Result { match e { ast::Expr::Subquery(_) | ast::Expr::Exists(_) | ast::Expr::InSelect { .. } => { bail_parse_error!("subqueries prohibited in {kind} expressions"); } ast::Expr::FunctionCall { name, args, filter_over, .. } => { if filter_over.over_clause.is_some() { bail_parse_error!("window functions prohibited in {kind} expressions"); } if let Some(func) = resolver.resolve_function(name.as_str(), args.len()) { if matches!(func, Func::Agg(..)) { bail_parse_error!( "aggregate functions prohibited in {kind} expressions: {}", name.as_str() ); } // Reject known non-deterministic built-in functions. // External functions are excluded from this check since // they default to non-deterministic but may actually be // deterministic (e.g. uuid_blob). if !matches!(func, Func::External(_)) && !func.is_deterministic() { bail_parse_error!( "non-deterministic functions prohibited in {kind} expressions: {}", name.as_str() ); } } } ast::Expr::FunctionCallStar { name, .. } => { bail_parse_error!( "aggregate functions prohibited in {kind} expressions: {}", name.as_str() ); } _ => {} } Ok(WalkControl::Continue) })?; Ok(()) } pub fn translate_create_type( type_name: &str, body: &ast::CreateTypeBody, if_not_exists: bool, resolver: &Resolver, program: &mut ProgramBuilder, ) -> Result<()> { let normalized_name = normalize_ident(type_name); // Reject names that shadow SQLite base types — these are not in the // type_registry but are handled by the column type system. Allowing // them would create confusion and undropable types. let is_base_type = turso_macros::match_ignore_ascii_case!(match normalized_name.as_bytes() { b"INT" | b"INTEGER" | b"REAL" | b"TEXT" | b"BLOB" | b"ANY" => true, _ => false, }); if is_base_type { bail_parse_error!("cannot create type \"{normalized_name}\": name is a built-in type"); } // Check if type already exists if resolver .schema() .get_type_def_unchecked(&normalized_name) .is_some() { if if_not_exists { return Ok(()); } bail_parse_error!("type {normalized_name} already exists"); } // Validate encode/decode expressions for safety if let Some(ref encode) = body.encode { validate_type_expr(encode, "ENCODE", resolver)?; } if let Some(ref decode) = body.decode { validate_type_expr(decode, "DECODE", resolver)?; } // Reconstruct the SQL string (without IF NOT EXISTS) using TypeDef::to_sql() let type_def = crate::schema::TypeDef::from_create_type(&normalized_name, body, false); let sql = type_def.to_sql(); // Ensure sqlite_turso_types table exists (lazy creation) let types_table: Arc; let types_root_page: RegisterOrLiteral; if let Some(existing) = resolver.schema().get_btree_table(TURSO_TYPES_TABLE_NAME) { types_table = existing.clone(); types_root_page = RegisterOrLiteral::Literal(existing.root_page); } else { // Create the sqlite_turso_types btree let table_root_reg = program.alloc_register(); program.emit_insn(Insn::CreateBtree { db: 0, root: table_root_reg, flags: CreateBTreeFlags::new_table(), }); let create_sql = format!("CREATE TABLE {TURSO_TYPES_TABLE_NAME}(name TEXT PRIMARY KEY, sql TEXT)"); types_table = Arc::new(BTreeTable::from_sql(&create_sql, 0)?); types_root_page = RegisterOrLiteral::Register(table_root_reg); // Register it in sqlite_schema so it persists let schema_table = resolver.schema().get_btree_table(SQLITE_TABLEID).unwrap(); let schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(schema_table)); program.emit_insn(Insn::OpenWrite { cursor_id: schema_cursor_id, root_page: 1i64.into(), db: 0, }); emit_schema_entry( program, resolver, schema_cursor_id, None, SchemaEntryType::Table, TURSO_TYPES_TABLE_NAME, TURSO_TYPES_TABLE_NAME, table_root_reg, Some(create_sql), )?; // Parse schema to register the new table in-memory program.emit_insn(Insn::ParseSchema { db: schema_cursor_id, where_clause: Some(format!( "tbl_name = '{TURSO_TYPES_TABLE_NAME}' AND type != 'trigger'" )), }); } // Open sqlite_turso_types for writing let types_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(types_table)); program.emit_insn(Insn::OpenWrite { cursor_id: types_cursor_id, root_page: types_root_page, db: 0, }); // Insert (name, sql) record let rowid_reg = program.alloc_register(); program.emit_insn(Insn::NewRowid { cursor: types_cursor_id, rowid_reg, prev_largest_reg: 0, }); let name_reg = program.emit_string8_new_reg(normalized_name); program.emit_string8_new_reg(sql.clone()); let record_reg = program.alloc_register(); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(name_reg), count: to_u16(2), dest_reg: to_u16(record_reg), index_name: None, affinity_str: None, }); program.emit_insn(Insn::Insert { cursor: types_cursor_id, key_reg: rowid_reg, record_reg, flag: InsertFlags::new(), table_name: TURSO_TYPES_TABLE_NAME.to_string(), }); // Add the type to the in-memory registry program.emit_insn(Insn::AddType { db: 0, sql }); program.emit_insn(Insn::SetCookie { db: 0, cookie: Cookie::SchemaVersion, value: (resolver.schema().schema_version + 1) as i32, p5: 0, }); Ok(()) } pub fn translate_drop_type( type_name: &str, if_exists: bool, resolver: &Resolver, program: &mut ProgramBuilder, ) -> Result<()> { let normalized_name = normalize_ident(type_name); // Check if type exists let type_def = resolver.schema().get_type_def_unchecked(&normalized_name); if type_def.is_none() { if if_exists { return Ok(()); } bail_parse_error!("no such type: {normalized_name}"); } // Check if built-in type if type_def.unwrap().is_builtin { bail_parse_error!("cannot drop built-in type: {normalized_name}"); } // Check if any table uses this type for (_, table) in resolver.schema().tables.iter() { for col in table.columns() { if normalize_ident(&col.ty_str) == normalized_name { bail_parse_error!( "cannot drop type {normalized_name}: used by column {} in table {}", col.name.as_deref().unwrap_or("?"), table.get_name() ); } } } // Open cursor to sqlite_turso_types table let types_table = resolver .schema() .get_btree_table(TURSO_TYPES_TABLE_NAME) .ok_or_else(|| crate::LimboError::ParseError(format!("no such type: {normalized_name}")))?; let types_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(types_table.clone())); program.emit_insn(Insn::OpenWrite { cursor_id: types_cursor_id, root_page: types_table.root_page.into(), db: 0, }); // Search for matching row: name=type_name (col 0) let name_reg = program.alloc_register(); program.emit_insn(Insn::String8 { dest: name_reg, value: normalized_name.clone(), }); let end_loop_label = program.allocate_label(); let loop_start_label = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: types_cursor_id, pc_if_empty: end_loop_label, }); program.preassign_label_to_next_insn(loop_start_label); // Read name (col 0) let col0_reg = program.alloc_register(); program.emit_column_or_rowid(types_cursor_id, 0, col0_reg); let skip_delete_label = program.allocate_label(); // Check name=type_name program.emit_insn(Insn::Ne { lhs: col0_reg, rhs: name_reg, target_pc: skip_delete_label, flags: CmpInsFlags::default(), collation: program.curr_collation(), }); // Delete matching row program.emit_insn(Insn::Delete { cursor_id: types_cursor_id, table_name: TURSO_TYPES_TABLE_NAME.to_string(), is_part_of_update: false, }); program.resolve_label(skip_delete_label, program.offset()); program.emit_insn(Insn::Next { cursor_id: types_cursor_id, pc_if_next: loop_start_label, }); program.preassign_label_to_next_insn(end_loop_label); // Remove from in-memory schema program.emit_insn(Insn::DropType { db: 0, type_name: normalized_name, }); program.emit_insn(Insn::SetCookie { db: 0, cookie: Cookie::SchemaVersion, value: (resolver.schema().schema_version + 1) as i32, p5: 0, }); Ok(()) } ================================================ FILE: core/translate/select.rs ================================================ use super::emitter::{emit_program, TranslateCtx}; use super::plan::{ select_star, Distinctness, InSeekSource, JoinOrderMember, Operation, OuterQueryReference, QueryDestination, Search, TableReferences, WhereTerm, Window, }; use crate::schema::Table; use crate::sync::Arc; use crate::translate::emitter::{OperationMode, Resolver}; use crate::translate::expr::{bind_and_rewrite_expr, expr_vector_size, BindingBehavior}; use crate::translate::group_by::compute_group_by_sort_order; use crate::translate::optimizer::optimize_plan; use crate::translate::plan::{GroupBy, Plan, ResultSetColumn, SelectPlan, SubqueryState}; use crate::translate::planner::{ break_predicate_at_and_boundaries, parse_from, parse_limit, parse_where, plan_ctes_as_outer_refs, resolve_window_and_aggregate_functions, }; use crate::translate::result_row::emit_select_result; use crate::translate::subquery::{plan_subqueries_from_select_plan, plan_subqueries_from_values}; use crate::translate::window::plan_windows; use crate::util::{exprs_are_equivalent, normalize_ident}; use crate::vdbe::builder::ProgramBuilderOpts; use crate::vdbe::insn::Insn; use crate::{vdbe::builder::ProgramBuilder, Result}; use std::borrow::Cow; use turso_parser::ast::ResultColumn; use turso_parser::ast::{self, CompoundSelect, Expr}; /// Maximum number of columns in a result set. /// SQLite's default SQLITE_MAX_COLUMN is 2000, with a hard upper limit of 32767. const SQLITE_MAX_COLUMN: usize = 2000; pub fn translate_select( select: ast::Select, resolver: &Resolver, program: &mut ProgramBuilder, query_destination: QueryDestination, connection: &Arc, ) -> Result { let mut select_plan = prepare_select_plan( select, resolver, program, &[], query_destination, connection, )?; if program.trigger.is_some() { if let Some(virtual_table) = plan_first_virtual_table_name(&select_plan) { crate::bail_parse_error!("unsafe use of virtual table \"{}\"", virtual_table); } } optimize_plan(program, &mut select_plan, resolver)?; let num_result_cols; let opts = match &select_plan { Plan::Select(select) => { num_result_cols = select.result_columns.len(); ProgramBuilderOpts { num_cursors: count_required_cursors_for_simple_select(select), approx_num_insns: estimate_num_instructions_for_simple_select(select), approx_num_labels: estimate_num_labels_for_simple_select(select), } } Plan::CompoundSelect { left, right_most, .. } => { // Compound Selects must return the same number of columns num_result_cols = right_most.result_columns.len(); ProgramBuilderOpts { num_cursors: count_required_cursors_for_simple_select(right_most) + left .iter() .map(|(plan, _)| count_required_cursors_for_simple_select(plan)) .sum::(), approx_num_insns: estimate_num_instructions_for_simple_select(right_most) + left .iter() .map(|(plan, _)| estimate_num_instructions_for_simple_select(plan)) .sum::(), approx_num_labels: estimate_num_labels_for_simple_select(right_most) + left .iter() .map(|(plan, _)| estimate_num_labels_for_simple_select(plan)) .sum::(), } } other => panic!("plan is not a SelectPlan: {other:?}"), }; program.extend(&opts); emit_program(connection, resolver, program, select_plan, |_| {})?; Ok(num_result_cols) } fn plan_first_virtual_table_name(plan: &Plan) -> Option { match plan { Plan::Select(select_plan) => select_plan_first_virtual_table_name(select_plan), Plan::CompoundSelect { left, right_most, .. } => select_plan_first_virtual_table_name(right_most).or_else(|| { left.iter() .find_map(|(plan, _)| select_plan_first_virtual_table_name(plan)) }), Plan::Delete(_) | Plan::Update(_) => None, } } fn select_plan_first_virtual_table_name(select_plan: &SelectPlan) -> Option { for joined_table in select_plan.joined_tables() { match &joined_table.table { Table::Virtual(virtual_table) if !virtual_table.innocuous => { return Some(virtual_table.name.clone()) } Table::FromClauseSubquery(from_clause_subquery) => { if let Some(name) = plan_first_virtual_table_name(&from_clause_subquery.plan) { return Some(name); } } _ => {} } } for subquery in &select_plan.non_from_clause_subqueries { if let SubqueryState::Unevaluated { plan: Some(plan) } = &subquery.state { if let Some(name) = select_plan_first_virtual_table_name(plan) { return Some(name); } } } None } pub fn prepare_select_plan( select: ast::Select, resolver: &Resolver, program: &mut ProgramBuilder, outer_query_refs: &[OuterQueryReference], query_destination: QueryDestination, connection: &Arc, ) -> Result { let compounds = select.body.compounds; match compounds.is_empty() { true => Ok(Plan::Select(prepare_one_select_plan( select.body.select, resolver, program, select.limit, select.order_by, select.with, outer_query_refs, query_destination, connection, )?)), false => { // For compound SELECTs, the WITH clause applies to all parts. // We clone the WITH clause for each SELECT in the compound so that // each one can resolve CTE references independently. let with = select.with; let mut last = prepare_one_select_plan( select.body.select, resolver, program, None, vec![], with.clone(), outer_query_refs, query_destination.clone(), connection, )?; let mut left = Vec::with_capacity(compounds.len()); for CompoundSelect { select: compound_select, operator, } in compounds { left.push((last, operator)); last = prepare_one_select_plan( compound_select, resolver, program, None, vec![], with.clone(), outer_query_refs, query_destination.clone(), connection, )?; } // Ensure all subplans have the same number of result columns let right_most_num_result_columns = last.result_columns.len(); for (plan, operator) in left.iter() { if plan.result_columns.len() != right_most_num_result_columns { crate::bail_parse_error!( "SELECTs to the left and right of {} do not have the same number of result columns", operator ); } } let (limit, offset) = select .limit .map_or(Ok((None, None)), |l| parse_limit(l, resolver))?; // FIXME: handle ORDER BY for compound selects if !select.order_by.is_empty() { crate::bail_parse_error!("ORDER BY is not supported for compound SELECTs yet"); } Ok(Plan::CompoundSelect { left, right_most: last, limit, offset, order_by: None, }) } } } #[allow(clippy::too_many_arguments)] fn prepare_one_select_plan( select: ast::OneSelect, resolver: &Resolver, program: &mut ProgramBuilder, limit: Option, order_by: Vec, with: Option, outer_query_refs: &[OuterQueryReference], query_destination: QueryDestination, connection: &Arc, ) -> Result { if order_by .iter() .filter_map(|o| o.nulls) .any(|n| n == ast::NullsOrder::Last) { crate::bail_parse_error!("NULLS LAST is not supported yet in ORDER BY"); } match select { ast::OneSelect::Select { columns, from, where_clause, group_by, distinctness, window_clause, } => { let col_count = columns.len(); if col_count == 0 { crate::bail_parse_error!("SELECT without columns is not allowed"); } let mut where_predicates = vec![]; let mut vtab_predicates = vec![]; let mut table_references = TableReferences::new(vec![], outer_query_refs.to_vec()); if from.is_none() { for column in &columns { if matches!(column, ResultColumn::Star) { crate::bail_parse_error!("no tables specified"); } } } // Parse the FROM clause into a vec of TableReferences. Fold all the join conditions expressions into the WHERE clause. let preplan_ctes_for_non_from_subqueries = with.is_some() && select_has_non_from_subqueries( &columns, where_clause.as_deref(), group_by.as_ref(), &window_clause, &order_by, limit.as_ref(), ); parse_from( from, resolver, program, with, preplan_ctes_for_non_from_subqueries, &mut where_predicates, &mut vtab_predicates, &mut table_references, connection, )?; // Preallocate space for the result columns let result_columns = Vec::with_capacity( columns .iter() .map(|c| match c { // Allocate space for all columns in all tables ResultColumn::Star => table_references .joined_tables() .iter() .map(|t| t.columns().iter().filter(|col| !col.hidden()).count()) .sum(), // Guess 5 columns if we can't find the table using the identifier (maybe it's in [brackets] or `tick_quotes`, or miXeDcAse) ResultColumn::TableStar(n) => table_references .joined_tables() .iter() .find(|t| t.identifier == n.as_str()) .map(|t| t.columns().iter().filter(|col| !col.hidden()).count()) .unwrap_or(5), // Otherwise allocate space for 1 column ResultColumn::Expr(_, _) => 1, }) .sum(), ); let mut plan = SelectPlan { join_order: table_references .joined_tables() .iter() .enumerate() .map(|(i, t)| JoinOrderMember { table_id: t.internal_id, original_idx: i, is_outer: t.join_info.as_ref().is_some_and(|j| j.is_outer()), }) .collect(), table_references, result_columns, where_clause: where_predicates, group_by: None, order_by: vec![], aggregates: vec![], limit: None, offset: None, contains_constant_false_condition: false, query_destination, distinctness: Distinctness::from_ast(distinctness.as_ref()), values: vec![], window: None, non_from_clause_subqueries: vec![], input_cardinality_hint: None, estimated_output_rows: None, simple_aggregate: None, }; let mut windows = Vec::with_capacity(window_clause.len()); for window_def in window_clause.iter() { let name = normalize_ident(window_def.name.as_str()); let mut window = Window::new(Some(name), &window_def.window)?; for expr in window.partition_by.iter_mut() { bind_and_rewrite_expr( expr, Some(&mut plan.table_references), None, resolver, BindingBehavior::ResultColumnsNotAllowed, )?; } for (expr, _) in window.order_by.iter_mut() { bind_and_rewrite_expr( expr, Some(&mut plan.table_references), None, resolver, BindingBehavior::ResultColumnsNotAllowed, )?; } windows.push(window); } let mut aggregate_expressions = Vec::new(); for column in columns.into_iter() { match column { ResultColumn::Star => { select_star( plan.table_references.joined_tables(), &mut plan.result_columns, plan.table_references.right_join_swapped(), ); for table in plan.table_references.joined_tables_mut() { for idx in 0..table.columns().len() { let column = &table.columns()[idx]; if column.hidden() { continue; } table.mark_column_used(idx); } } } ResultColumn::TableStar(name) => { let name_normalized = normalize_ident(name.as_str()); let referenced_table = plan .table_references .joined_tables_mut() .iter_mut() .find(|t| t.identifier == name_normalized); if referenced_table.is_none() { crate::bail_parse_error!("no such table: {}", name.as_str()); } let table = referenced_table.unwrap(); let num_columns = table.columns().len(); for idx in 0..num_columns { let column = &table.columns()[idx]; if column.hidden() { continue; } plan.result_columns.push(ResultSetColumn { expr: ast::Expr::Column { database: None, // TODO: support different databases table: table.internal_id, column: idx, is_rowid_alias: column.is_rowid_alias(), }, alias: None, contains_aggregates: false, }); table.mark_column_used(idx); } } ResultColumn::Expr(mut expr, maybe_alias) => { bind_and_rewrite_expr( &mut expr, Some(&mut plan.table_references), None, resolver, BindingBehavior::ResultColumnsNotAllowed, )?; let contains_aggregates = resolve_window_and_aggregate_functions( &expr, resolver, &mut aggregate_expressions, Some(&mut windows), )?; plan.result_columns.push(ResultSetColumn { alias: maybe_alias.as_ref().map(|alias| match alias { ast::As::Elided(alias) => alias.as_str().to_string(), ast::As::As(alias) => alias.as_str().to_string(), }), expr: *expr, contains_aggregates, }); } } } if plan.result_columns.len() > SQLITE_MAX_COLUMN { crate::bail_parse_error!("too many columns in result set"); } // This step can only be performed at this point, because all table references are now available. // Virtual table predicates may depend on column bindings from tables to the right in the join order, // so we must wait until the full set of references has been collected. add_vtab_predicates_to_where_clause(&mut vtab_predicates, &mut plan, resolver)?; // Parse the actual WHERE clause and add its conditions to the plan WHERE clause that already contains the join conditions. parse_where( where_clause.as_deref(), &mut plan.table_references, Some(&plan.result_columns), &mut plan.where_clause, resolver, )?; if let Some(mut group_by) = group_by { // Process HAVING clause if present let having_predicates = if let Some(having) = group_by.having { Some(process_having_clause( having, &mut plan.table_references, &plan.result_columns, resolver, &mut aggregate_expressions, )?) } else { None }; if !group_by.exprs.is_empty() { // Normal GROUP BY with expressions for expr in group_by.exprs.iter_mut() { replace_column_number_with_copy_of_column_expr(expr, &plan.result_columns)?; bind_and_rewrite_expr( expr, Some(&mut plan.table_references), Some(&plan.result_columns), resolver, BindingBehavior::TryCanonicalColumnsFirst, )?; } plan.group_by = Some(GroupBy { sort_order: Vec::new(), sort_elided: false, exprs: group_by.exprs.iter().map(|expr| *expr.clone()).collect(), having: having_predicates, }); } else { // HAVING without GROUP BY: treat as ungrouped aggregation with filter plan.group_by = Some(GroupBy { sort_order: Vec::new(), sort_elided: false, exprs: vec![], having: having_predicates, }); } } plan.aggregates = aggregate_expressions; // HAVING without GROUP BY requires aggregates in the SELECT if let Some(ref group_by) = plan.group_by { if group_by.exprs.is_empty() && group_by.having.is_some() && plan.aggregates.is_empty() { crate::bail_parse_error!("HAVING clause on a non-aggregate query"); } } // Parse the ORDER BY clause let mut key = Vec::new(); for mut o in order_by { replace_column_number_with_copy_of_column_expr(&mut o.expr, &plan.result_columns)?; bind_and_rewrite_expr( &mut o.expr, Some(&mut plan.table_references), Some(&plan.result_columns), resolver, BindingBehavior::TryResultColumnsFirst, )?; resolve_window_and_aggregate_functions( &o.expr, resolver, &mut plan.aggregates, Some(&mut windows), )?; key.push((o.expr, o.order.unwrap_or(ast::SortOrder::Asc))); } // Remove duplicate ORDER BY expressions, keeping the first occurrence. // Duplicates are semantically redundant. let mut i = 0; while i < key.len() { if key[..i] .iter() .any(|(prev, _)| exprs_are_equivalent(prev, &key[i].0)) { key.remove(i); } else { i += 1; } } plan.order_by = key; // Single-row aggregate queries (aggregates without GROUP BY and without window functions) // produce exactly one row, so ORDER BY is meaningless. Clearing it here also avoids // eagerly validating subqueries in ORDER BY that SQLite would skip due to optimization. // Note: HAVING without GROUP BY sets group_by to Some with empty exprs, still single-row. let is_single_row_aggregate = !plan.aggregates.is_empty() && plan.group_by.as_ref().is_none_or(|gb| gb.exprs.is_empty()) && windows.is_empty(); if is_single_row_aggregate { plan.order_by.clear(); } // SQLite optimizes away ORDER BY clauses after a rowid/INTEGER PRIMARY KEY column // when it's FIRST in the ORDER BY, since the table is stored in rowid order. // This means we truncate the ORDER BY to just the rowid column. // We do this for SQLite compatibility - SQLite truncates before validating, so // even invalid constructions like ORDER BY rowid, a IN (SELECT a, b FROM t) pass. if plan.order_by.len() > 1 && plan.table_references.joined_tables().len() == 1 { let joined = &plan.table_references.joined_tables()[0]; let table_id = joined.internal_id; let rowid_alias_col = joined .btree() .and_then(|t| t.get_rowid_alias_column().map(|(idx, _)| idx)); let first_is_rowid = match plan.order_by[0].0.as_ref() { ast::Expr::Column { table, column, .. } => { *table == table_id && rowid_alias_col == Some(*column) } ast::Expr::RowId { table, .. } => *table == table_id, _ => false, }; if first_is_rowid { plan.order_by.truncate(1); } } if let Some(group_by) = &mut plan.group_by { // now that we have resolved the ORDER BY expressions and aggregates, we can // compute the necessary sort order for the GROUP BY clause group_by.sort_order = compute_group_by_sort_order( &group_by.exprs, &plan.order_by, &plan.aggregates, resolver, ); debug_assert_eq!( group_by.exprs.len(), group_by.sort_order.len(), "GROUP BY exprs and sort_order must have the same length" ); } // Parse the LIMIT/OFFSET clause (plan.limit, plan.offset) = limit.map_or(Ok((None, None)), |l| parse_limit(l, resolver))?; if !windows.is_empty() { plan_windows( &mut plan, resolver, &mut program.table_reference_counter, &mut windows, )?; } plan_subqueries_from_select_plan(program, &mut plan, resolver, connection)?; validate_expr_correct_column_counts(&plan)?; // Return the unoptimized query plan Ok(plan) } ast::OneSelect::Values(mut values) => { if !order_by.is_empty() { crate::bail_parse_error!("ORDER BY clause is not allowed with VALUES clause"); } if limit.is_some() { crate::bail_parse_error!("LIMIT clause is not allowed with VALUES clause"); } let len = values[0].len(); if len > SQLITE_MAX_COLUMN { crate::bail_parse_error!("too many columns in result set"); } let mut result_columns = Vec::with_capacity(len); for i in 0..len { result_columns.push(ResultSetColumn { // these result_columns work as placeholders for the values, so the expr doesn't matter expr: ast::Expr::Literal(ast::Literal::Numeric(i.to_string())), alias: Some(format!("column{}", i + 1)), contains_aggregates: false, }); } let mut table_references = TableReferences::new(vec![], outer_query_refs.to_vec()); // Plan CTEs from WITH clause so they're available for subqueries in VALUES plan_ctes_as_outer_refs(with, resolver, program, &mut table_references, connection)?; for value_row in values.iter_mut() { for value in value_row.iter_mut() { // Before binding, we check for unquoted literals. Sqlite throws an error in this case bind_and_rewrite_expr( value, Some(&mut table_references), None, resolver, // Allow sqlite quirk of inserting "double-quoted" literals (which our AST maps as identifiers) BindingBehavior::TryResultColumnsFirst, )?; } } // Plan subqueries in VALUES expressions let mut non_from_clause_subqueries = vec![]; plan_subqueries_from_values( program, &mut non_from_clause_subqueries, &mut table_references, &mut values, resolver, connection, )?; let plan = SelectPlan { join_order: vec![], table_references, result_columns, where_clause: vec![], group_by: None, order_by: vec![], aggregates: vec![], limit: None, offset: None, contains_constant_false_condition: false, query_destination, distinctness: Distinctness::NonDistinct, values: values .iter() .map(|values| values.iter().map(|value| *value.clone()).collect()) .collect(), window: None, non_from_clause_subqueries, input_cardinality_hint: None, estimated_output_rows: None, simple_aggregate: None, }; validate_expr_correct_column_counts(&plan)?; Ok(plan) } } } /// Validate that all expressions in the plan return the correct number of values; /// generally this only applies to parenthesized lists and subqueries. fn validate_expr_correct_column_counts(plan: &SelectPlan) -> Result<()> { for result_column in plan.result_columns.iter() { let vec_size = expr_vector_size(&result_column.expr)?; if vec_size != 1 { crate::bail_parse_error!("result column must return 1 value, got {}", vec_size); } } for (expr, _) in plan.order_by.iter() { let vec_size = expr_vector_size(expr)?; if vec_size != 1 { crate::bail_parse_error!("order by expression must return 1 value, got {}", vec_size); } } if let Some(group_by) = &plan.group_by { for expr in group_by.exprs.iter() { let vec_size = expr_vector_size(expr)?; if vec_size != 1 { crate::bail_parse_error!( "group by expression must return 1 value, got {}", vec_size ); } } if let Some(having) = &group_by.having { for expr in having.iter() { let vec_size = expr_vector_size(expr)?; if vec_size != 1 { crate::bail_parse_error!( "having expression must return 1 value, got {}", vec_size ); } } } } for aggregate in plan.aggregates.iter() { for arg in aggregate.args.iter() { let vec_size = expr_vector_size(arg)?; if vec_size != 1 { crate::bail_parse_error!( "aggregate argument must return 1 value, got {}", vec_size ); } } } for term in plan.where_clause.iter() { let vec_size = expr_vector_size(&term.expr)?; if vec_size != 1 { crate::bail_parse_error!( "where clause expression must return 1 value, got {}", vec_size ); } } for expr in plan.values.iter() { for value in expr.iter() { let vec_size = expr_vector_size(value)?; if vec_size != 1 { crate::bail_parse_error!("value must return 1 value, got {}", vec_size); } } } if let Some(limit) = &plan.limit { let vec_size = expr_vector_size(limit)?; if vec_size != 1 { crate::bail_parse_error!("limit expression must return 1 value, got {}", vec_size); } } if let Some(offset) = &plan.offset { let vec_size = expr_vector_size(offset)?; if vec_size != 1 { crate::bail_parse_error!("offset expression must return 1 value, got {}", vec_size); } } Ok(()) } fn add_vtab_predicates_to_where_clause( vtab_predicates: &mut Vec, plan: &mut SelectPlan, resolver: &Resolver, ) -> Result<()> { for expr in vtab_predicates.iter_mut() { bind_and_rewrite_expr( expr, Some(&mut plan.table_references), Some(&plan.result_columns), resolver, BindingBehavior::TryCanonicalColumnsFirst, )?; } for expr in vtab_predicates.drain(..) { // Virtual table argument predicates (e.g. the 't2' in pragma_table_info('t2')) // must be associated with the virtual table's outer join context if the table is // the RHS of a LEFT JOIN. Otherwise the optimizer may incorrectly simplify the // LEFT JOIN into an INNER JOIN, breaking NULL row emission for unmatched rows. let from_outer_join = vtab_predicate_table_id(&expr).and_then(|table_id| { plan.table_references .find_joined_table_by_internal_id(table_id) .and_then(|t| { t.join_info .as_ref() .and_then(|ji| ji.is_outer().then_some(table_id)) }) }); plan.where_clause.push(WhereTerm { expr, from_outer_join, consumed: false, }); } Ok(()) } /// Extract the table internal_id from a virtual table argument predicate. /// These are always of the form `Column { table, .. } = literal` or `IsNull(Column { table, .. })`. fn vtab_predicate_table_id(expr: &Expr) -> Option { match expr { Expr::Binary(lhs, _, _) | Expr::IsNull(lhs) => match lhs.as_ref() { Expr::Column { table, .. } => Some(*table), _ => None, }, _ => None, } } /// Replaces a column number in an ORDER BY or GROUP BY expression with a copy of the column expression. /// For example, in SELECT u.first_name, count(1) FROM users u GROUP BY 1 ORDER BY 2, /// the column number 1 is replaced with u.first_name and the column number 2 is replaced with count(1). /// /// Per SQLite documentation, only constant integers are treated as column references. /// Non-integer numeric literals (floats) are treated as constant expressions. fn replace_column_number_with_copy_of_column_expr( order_by_or_group_by_expr: &mut ast::Expr, columns: &[ResultSetColumn], ) -> Result<()> { if let ast::Expr::Literal(ast::Literal::Numeric(num)) = order_by_or_group_by_expr { // Only treat as column reference if it parses as a positive integer. // Float literals like "0.5" or "1.0" are valid constant expressions, not column references. if let Ok(column_number) = num.parse::() { if column_number == 0 { crate::bail_parse_error!("invalid column index: {}", column_number); } let maybe_result_column = columns.get(column_number - 1); match maybe_result_column { Some(ResultSetColumn { expr, .. }) => { *order_by_or_group_by_expr = expr.clone(); } None => { crate::bail_parse_error!("invalid column index: {}", column_number) } }; } // Otherwise, leave the expression as-is (constant expression, case 3 per SQLite docs) } Ok(()) } /// Count required cursors for a Plan (either Select or CompoundSelect) fn count_required_cursors_for_simple_or_compound_select(plan: &Plan) -> usize { match plan { Plan::Select(select_plan) => count_required_cursors_for_simple_select(select_plan), Plan::CompoundSelect { left, right_most, .. } => { count_required_cursors_for_simple_select(right_most) + left .iter() .map(|(p, _)| count_required_cursors_for_simple_select(p)) .sum::() } Plan::Delete(_) | Plan::Update(_) => 0, } } fn count_required_cursors_for_simple_select(plan: &SelectPlan) -> usize { let num_table_cursors: usize = plan .joined_tables() .iter() .map(|t| match &t.op { Operation::Scan { .. } => 1, Operation::Search(search) => match search { Search::RowidEq { .. } => 1, Search::Seek { index, .. } => 1 + index.is_some() as usize, Search::InSeek { index, source } => match source { // table cursor + new ephemeral cursor + optional index cursor InSeekSource::LiteralList { .. } => 2 + index.is_some() as usize, // table cursor + optional index cursor (ephemeral already counted) InSeekSource::Subquery { .. } => 1 + index.is_some() as usize, }, } Operation::IndexMethodQuery(_) => 1, Operation::HashJoin(_) => 2, // One table cursor + one cursor per index branch Operation::MultiIndexScan(multi_idx) => 1 + multi_idx.branches.len(), } + if let Table::FromClauseSubquery(from_clause_subquery) = &t.table { count_required_cursors_for_simple_or_compound_select(&from_clause_subquery.plan) } else { 0 }) .sum(); let has_group_by_with_exprs = plan .group_by .as_ref() .is_some_and(|gb| !gb.exprs.is_empty()); let num_sorter_cursors = has_group_by_with_exprs as usize + !plan.order_by.is_empty() as usize; let num_pseudo_cursors = has_group_by_with_exprs as usize + !plan.order_by.is_empty() as usize; num_table_cursors + num_sorter_cursors + num_pseudo_cursors } /// Estimate number of instructions for a Plan (either Select or CompoundSelect) fn estimate_num_instructions_for_simple_or_compound_select(plan: &Plan) -> usize { match plan { Plan::Select(select_plan) => estimate_num_instructions_for_simple_select(select_plan), Plan::CompoundSelect { left, right_most, .. } => { estimate_num_instructions_for_simple_select(right_most) + left .iter() .map(|(p, _)| estimate_num_instructions_for_simple_select(p)) .sum::() + 20 // overhead for compound select operations } Plan::Delete(_) | Plan::Update(_) => 0, } } fn estimate_num_instructions_for_simple_select(select: &SelectPlan) -> usize { let table_instructions: usize = select .joined_tables() .iter() .map(|t| match &t.op { Operation::Scan { .. } => 10, Operation::Search(_) => 15, Operation::IndexMethodQuery(_) => 15, Operation::HashJoin(_) => 20, // Multi-index scan: scan overhead per branch + deduplication + final rowid fetch Operation::MultiIndexScan(multi_idx) => 15 * multi_idx.branches.len() + 10, } + if let Table::FromClauseSubquery(from_clause_subquery) = &t.table { 10 + estimate_num_instructions_for_simple_or_compound_select(&from_clause_subquery.plan) } else { 0 }) .sum(); let group_by_instructions = select.group_by.is_some() as usize * 10; let order_by_instructions = !select.order_by.is_empty() as usize * 10; let condition_instructions = select.where_clause.len() * 3; 20 + table_instructions + group_by_instructions + order_by_instructions + condition_instructions } fn push_function_tail_exprs<'a>(stack: &mut Vec<&'a Expr>, tail: &'a ast::FunctionTail) { if let Some(filter_expr) = tail.filter_clause.as_deref() { stack.push(filter_expr); } let Some(ast::Over::Window(window)) = tail.over_clause.as_ref() else { return; }; if let Some(frame_clause) = window.frame_clause.as_ref() { if let ast::FrameBound::Following(expr) | ast::FrameBound::Preceding(expr) = &frame_clause.start { stack.push(expr.as_ref()); } if let Some(ast::FrameBound::Following(expr) | ast::FrameBound::Preceding(expr)) = frame_clause.end.as_ref() { stack.push(expr.as_ref()); } } for sorted in window.order_by.iter().rev() { stack.push(sorted.expr.as_ref()); } for part_expr in window.partition_by.iter().rev() { stack.push(part_expr.as_ref()); } } fn expr_contains_subquery(expr: &Expr) -> bool { // Iterative traversal avoids stack overflows on deeply nested expression trees // such as very large left-associative AND chains. let mut stack = vec![expr]; while let Some(node) = stack.pop() { match node { Expr::Subquery(_) | Expr::InSelect { .. } | Expr::Exists(_) => return true, Expr::Between { lhs, start, end, .. } => { stack.push(lhs.as_ref()); stack.push(start.as_ref()); stack.push(end.as_ref()); } Expr::Binary(lhs, _, rhs) => { stack.push(rhs.as_ref()); stack.push(lhs.as_ref()); } Expr::Case { base, when_then_pairs, else_expr, } => { if let Some(expr) = else_expr.as_deref() { stack.push(expr); } for (when_expr, then_expr) in when_then_pairs.iter().rev() { stack.push(then_expr.as_ref()); stack.push(when_expr.as_ref()); } if let Some(base_expr) = base.as_deref() { stack.push(base_expr); } } Expr::Cast { expr, .. } | Expr::Collate(expr, _) | Expr::IsNull(expr) | Expr::NotNull(expr) | Expr::Unary(_, expr) => { stack.push(expr.as_ref()); } Expr::FunctionCall { args, order_by, filter_over, .. } => { push_function_tail_exprs(&mut stack, filter_over); for sorted in order_by.iter().rev() { stack.push(sorted.expr.as_ref()); } for arg in args.iter().rev() { stack.push(arg.as_ref()); } } Expr::FunctionCallStar { filter_over, .. } => { push_function_tail_exprs(&mut stack, filter_over); } Expr::InList { lhs, rhs, .. } => { for item in rhs.iter().rev() { stack.push(item.as_ref()); } stack.push(lhs.as_ref()); } Expr::InTable { lhs, args, .. } => { for arg in args.iter().rev() { stack.push(arg.as_ref()); } stack.push(lhs.as_ref()); } Expr::Like { lhs, rhs, escape, .. } => { if let Some(escape_expr) = escape.as_deref() { stack.push(escape_expr); } stack.push(rhs.as_ref()); stack.push(lhs.as_ref()); } Expr::Parenthesized(exprs) => { for expr in exprs.iter().rev() { stack.push(expr.as_ref()); } } Expr::Raise(_, raise_expr) => { if let Some(expr) = raise_expr.as_deref() { stack.push(expr); } } Expr::SubqueryResult { lhs, .. } => { if let Some(expr) = lhs.as_deref() { stack.push(expr); } } Expr::Array { .. } | Expr::Subscript { .. } => { unreachable!("Array and Subscript are desugared into function calls by the parser") } Expr::Column { .. } | Expr::DoublyQualified(_, _, _) | Expr::Id(_) | Expr::Literal(_) | Expr::Name(_) | Expr::Qualified(_, _) | Expr::Register(_) | Expr::RowId { .. } | Expr::Variable(_) => {} } } false } fn select_has_non_from_subqueries( columns: &[ResultColumn], where_clause: Option<&Expr>, group_by: Option<&ast::GroupBy>, window_clause: &[ast::WindowDef], order_by: &[ast::SortedColumn], limit: Option<&ast::Limit>, ) -> bool { if columns.iter().any(|column| match column { ResultColumn::Expr(expr, _) => expr_contains_subquery(expr), ResultColumn::Star | ResultColumn::TableStar(_) => false, }) { return true; } if where_clause.is_some_and(expr_contains_subquery) { return true; } if let Some(group_by) = group_by { if group_by.exprs.iter().any(|e| expr_contains_subquery(e)) || group_by .having .as_deref() .is_some_and(expr_contains_subquery) { return true; } } if window_clause.iter().any(|w| { w.window .partition_by .iter() .any(|e| expr_contains_subquery(e)) || w.window .order_by .iter() .any(|s| expr_contains_subquery(&s.expr)) }) { return true; } if order_by.iter().any(|s| expr_contains_subquery(&s.expr)) { return true; } if let Some(limit) = limit { if expr_contains_subquery(&limit.expr) || limit.offset.as_deref().is_some_and(expr_contains_subquery) { return true; } } false } /// Estimate number of labels for a Plan (either Select or CompoundSelect) fn estimate_num_labels_for_simple_or_compound_select(plan: &Plan) -> usize { match plan { Plan::Select(select_plan) => estimate_num_labels_for_simple_select(select_plan), Plan::CompoundSelect { left, right_most, .. } => { estimate_num_labels_for_simple_select(right_most) + left .iter() .map(|(p, _)| estimate_num_labels_for_simple_select(p)) .sum::() + 10 // overhead for compound select operations } Plan::Delete(_) | Plan::Update(_) => 0, } } fn estimate_num_labels_for_simple_select(select: &SelectPlan) -> usize { let init_halt_labels = 2; // 3 loop labels for each table in main loop + 1 to signify end of main loop let table_labels = select .joined_tables() .iter() .map(|t| match &t.op { Operation::Scan { .. } => 3, Operation::Search(_) => 3, Operation::IndexMethodQuery(_) => 3, Operation::HashJoin(_) => 3, // Multi-index scan needs extra labels for each branch + rowset loop Operation::MultiIndexScan(multi_idx) => 3 + multi_idx.branches.len() * 2, } + if let Table::FromClauseSubquery(from_clause_subquery) = &t.table { 3 + estimate_num_labels_for_simple_or_compound_select(&from_clause_subquery.plan) } else { 0 }) .sum::() + 1; let group_by_labels = select.group_by.is_some() as usize * 10; let order_by_labels = !select.order_by.is_empty() as usize * 10; let condition_labels = select.where_clause.len() * 2; init_halt_labels + table_labels + group_by_labels + order_by_labels + condition_labels } pub fn emit_simple_count( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, plan: &SelectPlan, ) -> Result { let cursors = plan .joined_tables() .first() .unwrap() .resolve_cursors(program, OperationMode::SELECT)?; let cursor_id = { match cursors { (_, Some(cursor_id)) | (Some(cursor_id), None) => cursor_id, _ => return Ok(false), } }; // Count opcode only works on BTree cursors. Materialized view trigger // queries may have pseudo cursors — fall back to normal aggregation. if !program.cursor_is_btree(cursor_id) { return Ok(false); } let target_reg = program.alloc_register(); program.emit_insn(Insn::Count { cursor_id, target_reg, exact: true, }); program.emit_insn(Insn::Close { cursor_id }); let agg = plan .aggregates .first() .expect("simple count requires exactly one aggregate"); t_ctx.resolver.cache_expr_reg( Cow::Owned(agg.original_expr.clone()), target_reg, false, None, ); t_ctx.resolver.enable_expr_to_reg_cache(); emit_select_result( program, &t_ctx.resolver, plan, None, None, None, None, t_ctx.reg_result_cols_start.unwrap(), t_ctx.limit_ctx, )?; Ok(true) } fn process_having_clause( having: Box, table_references: &mut TableReferences, result_columns: &[ResultSetColumn], resolver: &Resolver, aggregate_expressions: &mut Vec, ) -> Result> { let mut predicates = vec![]; break_predicate_at_and_boundaries(&having, &mut predicates); // Before alias resolution replaces identifiers with their underlying expressions, // check for aliased aggregate misuse. SQLite does this during name resolution by // checking the NC_AllowAgg flag on the NameContext (see resolve.c). When an identifier // inside an aggregate function's arguments resolves to an alias whose original expression // has EP_Agg, SQLite reports "misuse of aliased aggregate X". for expr in predicates.iter() { check_aliased_aggregate_misuse(expr, result_columns)?; } for expr in predicates.iter_mut() { bind_and_rewrite_expr( expr, Some(table_references), Some(result_columns), resolver, BindingBehavior::TryResultColumnsFirst, )?; resolve_window_and_aggregate_functions(expr, resolver, aggregate_expressions, None)?; } Ok(predicates) } /// Walk a HAVING expression looking for aggregate function calls whose arguments /// reference aliases of aggregate result columns (SQLite ticket #2526). fn check_aliased_aggregate_misuse( expr: &ast::Expr, result_columns: &[ResultSetColumn], ) -> Result<()> { use crate::translate::expr::{walk_expr, WalkControl}; walk_expr(expr, &mut |e| { match e { Expr::FunctionCall { name, args, .. } => { let is_agg = matches!( crate::function::Func::resolve_function(name.as_str(), args.len()), Ok(crate::function::Func::Agg(_)) ); if is_agg { for arg in args.iter() { find_aliased_aggregate_ref(arg, result_columns)?; } return Ok(WalkControl::SkipChildren); } } Expr::FunctionCallStar { name, .. } => { if matches!( crate::function::Func::resolve_function(name.as_str(), 0), Ok(crate::function::Func::Agg(_)) ) { return Ok(WalkControl::SkipChildren); } } _ => {} } Ok(WalkControl::Continue) })?; Ok(()) } /// Check if an expression (inside an aggregate's arguments) contains an identifier /// that matches an alias of an aggregate result column. fn find_aliased_aggregate_ref(expr: &ast::Expr, result_columns: &[ResultSetColumn]) -> Result<()> { use crate::translate::expr::{walk_expr, WalkControl}; walk_expr(expr, &mut |e| { if let Expr::Id(id) = e { let normalized = normalize_ident(id.as_str()); for rc in result_columns.iter() { if let Some(alias) = &rc.alias { if alias.eq_ignore_ascii_case(&normalized) && rc.contains_aggregates { crate::bail_parse_error!("misuse of aliased aggregate {}", normalized); } } } } Ok(WalkControl::Continue) })?; Ok(()) } ================================================ FILE: core/translate/stmt_journal.rs ================================================ //! Statement journal flag analysis (`is_multi_write` / `may_abort`). //! //! Inside an explicit transaction (BEGIN...COMMIT), each statement runs within //! the larger transaction. If a statement partially completes and then aborts //! (e.g. a UNIQUE constraint violation on the third row of a multi-row INSERT), //! the partial writes must be rolled back without discarding the entire //! transaction. SQLite solves this with a "statement journal" (subjournal): a //! savepoint taken at the start of each statement, rolled back on abort. //! //! Statement journals are expensive, so SQLite skips them when provably //! unnecessary. The condition is: `usesStmtJournal = isMultiWrite && mayAbort`. //! //! - **isMultiWrite**: the statement may modify more than one row (or more than //! one table, e.g. FK counter + data table). A single-row write is atomic — //! either all writes happen or none do — so no partial state to roll back. //! //! - **mayAbort**: the statement may fail mid-execution with an ABORT (e.g. //! constraint violation, FK violation, RAISE(ABORT) in a trigger). If a //! multi-write statement can never abort, partial rollback is moot. //! //! Both flags default to `true` (conservative). Each DML translate path calls //! into this module to set them to `false` when safe. use crate::translate::emitter::Resolver; use crate::translate::plan::{DeletePlan, DmlSafetyReason, UpdatePlan}; use crate::translate::trigger_exec::has_relevant_triggers_type_only; use crate::vdbe::builder::ProgramBuilder; use crate::{sync::Arc, Connection, HashSet, Result}; use turso_parser::ast::{ResolveType, TriggerEvent}; /// Check whether any DDL-level constraint (IPK or index) uses REPLACE. pub(crate) fn any_index_or_ipk_has_replace( rowid_alias_conflict: Option, mut indexes: impl Iterator>, ) -> bool { rowid_alias_conflict == Some(ResolveType::Replace) || indexes.any(|oc| oc == Some(ResolveType::Replace)) } /// Check whether any constraint's effective resolution is REPLACE. /// /// When a statement-level override exists, only the statement conflict mode matters. /// Otherwise, both the PK's DDL mode and each index's DDL mode are checked. pub(crate) fn any_effective_replace( has_statement_conflict: bool, statement_conflict: ResolveType, rowid_alias_conflict: Option, indexes: impl Iterator>, ) -> bool { if has_statement_conflict { matches!(statement_conflict, ResolveType::Replace) } else { any_index_or_ipk_has_replace(rowid_alias_conflict, indexes) } } /// Check whether a table has any FK relationships (child or parent side). fn table_has_fks( connection: &crate::Connection, resolver: &Resolver, database_id: usize, table_name: &str, ) -> bool { connection.foreign_keys_enabled() && (resolver.with_schema(database_id, |s| s.has_child_fks(table_name)) || resolver.with_schema(database_id, |s| s.any_resolved_fks_referencing(table_name))) } /// Determine whether any constraint's effective resolution can trigger an /// ABORT. Each constraint has an effective resolution mode — either the /// statement-level override (when present) or its DDL-level mode (defaulting /// to ABORT). A constraint can cause an ABORT when: /// /// - Its effective mode is ABORT and it has any checkable constraint /// (NOT NULL, CHECK, UNIQUE). /// - Its effective mode is REPLACE and the table has NOT NULL or CHECK /// constraints, because REPLACE falls back to ABORT for those. /// /// IGNORE and FAIL never trigger statement-level ABORT. /// Each index is represented as `(on_conflict, is_unique)`. pub(crate) fn constraint_may_abort( has_statement_conflict: bool, statement_conflict: ResolveType, rowid_alias_conflict: Option, mut indexes: impl Iterator, bool)>, has_notnull: bool, has_check: bool, has_unique: bool, ) -> bool { if has_statement_conflict { // Statement-level override applies uniformly to all constraints. return match statement_conflict { ResolveType::Abort => has_notnull || has_check || has_unique, ResolveType::Replace => has_notnull || has_check, // UNIQUE conflict gets replaced, not aborted. _ => false, // IGNORE, FAIL, ROLLBACK don't need statement journal }; } // No statement-level override — each constraint uses its DDL-level mode. let pk_mode = rowid_alias_conflict.unwrap_or(ResolveType::Abort); let pk_aborts = match pk_mode { ResolveType::Abort => has_unique, // PK is a unique constraint ResolveType::Replace => false, // PK REPLACE doesn't fall back for unique _ => false, }; let idx_aborts = indexes.any(|(on_conflict, unique)| { let mode = on_conflict.unwrap_or(ResolveType::Abort); match mode { ResolveType::Abort => unique, // only unique indexes can conflict ResolveType::Replace => has_notnull || has_check, _ => false, } }); // Default ABORT applies to NOT NULL and CHECK (they aren't per-index). let default_aborts = has_notnull || has_check; pk_aborts || idx_aborts || default_aborts } /// Set multi_write / may_abort for INSERT statements. /// /// Constraint analysis (any_replace, constraint_may_abort) is computed internally /// from the table schema and resolver. The caller provides INSERT-specific flags /// that come from the emitter's own analysis. #[allow(clippy::too_many_arguments)] pub(crate) fn set_insert_stmt_journal_flags( program: &mut ProgramBuilder, resolver: &Resolver, database_id: usize, table: &crate::schema::BTreeTable, has_statement_conflict: bool, statement_conflict: ResolveType, inserting_multiple_rows: bool, has_triggers: bool, has_fks: bool, has_upsert: bool, has_autoincrement: bool, notnull_col_exists: bool, has_unique: bool, ) { let index_modes: Vec<(Option, bool)> = resolver.with_schema(database_id, |s| { s.get_indices(&table.name) .map(|idx| (idx.on_conflict, idx.unique)) .collect() }); let any_replace = any_effective_replace( has_statement_conflict, statement_conflict, table.rowid_alias_conflict_clause, index_modes.iter().map(|(oc, _)| *oc), ); let has_check = !table.check_constraints.is_empty(); let may_abort = has_triggers || has_fks || constraint_may_abort( has_statement_conflict, statement_conflict, table.rowid_alias_conflict_clause, index_modes.into_iter(), notnull_col_exists, has_check, has_unique, ); // UPSERT is multi-write because DO UPDATE modifies an existing row. // AUTOINCREMENT is multi-write because sqlite_sequence is updated before constraint checks. if !inserting_multiple_rows && !has_triggers && !any_replace && !has_upsert && !has_autoincrement { program.set_multi_write(false); } program.set_may_abort(may_abort); } /// Set multi_write / may_abort for UPDATE statements. pub(crate) fn set_update_stmt_journal_flags( program: &mut ProgramBuilder, plan: &UpdatePlan, resolver: &Resolver, connection: &crate::sync::Arc, ) -> Result<()> { // When an ephemeral table is used (key mutation / Halloween protection), // the actual target table is in the ephemeral_plan's table_references. let table_refs = plan .ephemeral_plan .as_ref() .map(|ep| &ep.table_references) .unwrap_or(&plan.table_references); let Some(target_table) = table_refs.joined_tables().first() else { crate::bail_parse_error!("UPDATE should have one target table"); }; let Some(btree_table) = target_table.btree() else { return Ok(()); // Virtual table — keep conservative defaults. }; let database_id = target_table.database_id; let updated_cols: HashSet = plan.set_clauses.iter().map(|(i, _)| *i).collect(); let has_triggers = resolver.with_schema(database_id, |s| { has_relevant_triggers_type_only(s, TriggerEvent::Update, Some(&updated_cols), &btree_table) }); let has_fks = table_has_fks(connection, resolver, database_id, btree_table.name.as_str()); let or_conflict = plan.or_conflict.unwrap_or(ResolveType::Abort); let has_statement_conflict = plan.or_conflict.is_some(); let any_replace = any_effective_replace( has_statement_conflict, or_conflict, btree_table.rowid_alias_conflict_clause, plan.indexes_to_update.iter().map(|idx| idx.on_conflict), ); // Ephemeral tables (used for key mutation / Halloween protection) always scan all // collected rows, so affects_max_1_row() returns false — multi_write stays true. let is_single_row = plan.limit.is_none() && plan.offset.is_none() && target_table.op.affects_max_1_row(); if is_single_row && !has_triggers && !any_replace && !has_fks { program.set_multi_write(false); } let has_notnull_cols = plan.set_clauses.iter().any(|(col_idx, _)| { if *col_idx == crate::schema::ROWID_SENTINEL { return false; } btree_table .columns .get(*col_idx) .is_some_and(|c| c.notnull() && !c.is_rowid_alias()) }); let has_check = !btree_table.check_constraints.is_empty(); let has_unique = !btree_table.unique_sets.is_empty() || plan.indexes_to_update.iter().any(|idx| idx.unique); let may_abort = has_triggers || has_fks || constraint_may_abort( has_statement_conflict, or_conflict, btree_table.rowid_alias_conflict_clause, plan.indexes_to_update .iter() .map(|idx| (idx.on_conflict, idx.unique)), has_notnull_cols, has_check, has_unique, ); program.set_may_abort(may_abort); Ok(()) } /// Set multi_write / may_abort for DELETE statements. pub(crate) fn set_delete_stmt_journal_flags( program: &mut ProgramBuilder, plan: &DeletePlan, resolver: &Resolver, connection: &Arc, database_id: usize, ) -> Result<()> { let Some(target_table) = plan.table_references.joined_tables().first() else { crate::bail_parse_error!("DELETE should have one target table"); }; let Some(btree_table) = target_table.btree() else { return Ok(()); // Virtual table — keep conservative defaults. }; let has_triggers = plan.safety.reasons.contains(&DmlSafetyReason::Trigger); let has_fks = table_has_fks(connection, resolver, database_id, btree_table.name.as_str()); // After rowset rewriting (for triggers/safety), the target table op is reset to a // Scan, so affects_max_1_row correctly returns false — no false optimization. let is_single_row = plan.limit.is_none() && plan.offset.is_none() && target_table.op.affects_max_1_row(); if is_single_row && !has_triggers && !has_fks { program.set_multi_write(false); } // DELETE has no ON CONFLICT clause, so NOT NULL/CHECK/UNIQUE don't apply — // only triggers (RAISE(ABORT)) or FK violations can abort. if !has_triggers && !has_fks { program.set_may_abort(false); } Ok(()) } ================================================ FILE: core/translate/subquery.rs ================================================ use std::sync::Arc; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use turso_parser::ast::{self, SortOrder, SubqueryType}; use crate::{ emit_explain, schema::{BTreeTable, Column, Index, IndexColumn, Table}, translate::{ collate::get_collseq_from_expr, compound_select::emit_program_for_compound_select, emitter::select::{ emit_program_for_select, emit_program_for_select_with_resolver, emit_query, }, expr::{ compare_affinity, get_expr_affinity_info, unwrap_parens, walk_expr_mut, WalkControl, }, optimizer::optimize_select_plan, plan::{ plan_has_outer_scope_dependency, plan_is_correlated, ColumnUsedMask, EvalAt, JoinOrderMember, NonFromClauseSubquery, OuterQueryReference, Plan, SetOperation, SubqueryEvalPhase, SubqueryOrigin, SubqueryPosition, SubqueryState, TableReferences, WhereTerm, }, select::prepare_select_plan, }, types::Value, util::parse_signed_number, vdbe::{ builder::{CursorKey, CursorType, MaterializedCteInfo, ProgramBuilder}, insn::Insn, CursorID, }, Connection, Numeric, Result, }; use super::{ emitter::{Resolver, TranslateCtx}, main_loop::LoopLabels, plan::{Aggregate, Operation, QueryDestination, Scan, Search, SelectPlan}, planner::resolve_window_and_aggregate_functions, }; struct DirectMaterializedSubquery { index: Arc, affinity_str: Option>, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum MaterializedFromClauseSubqueryStorage { TableBacked, DirectIndex, } enum FromClauseSubqueryExecutionMode { Coroutine, MaterializedTable, DirectMaterializedIndex(DirectMaterializedSubquery), } pub(crate) fn materialized_from_clause_subquery_storage( subquery: &crate::schema::FromClauseSubquery, ) -> Option { match subquery.plan.select_query_destination() { Some(QueryDestination::EphemeralTable { .. }) => { Some(MaterializedFromClauseSubqueryStorage::TableBacked) } Some(QueryDestination::EphemeralIndex { .. }) => { Some(MaterializedFromClauseSubqueryStorage::DirectIndex) } _ => None, } } /// Mark CTE references that must be materialized once and shared across /// multiple reads of the same query tree. /// /// Correlated plans are explicitly excluded: they must re-run for each outer /// row, so sharing a single materialized result would be semantically wrong. fn mark_shared_cte_materialization_requirements(program: &ProgramBuilder, plan: &mut SelectPlan) { fn annotate_plan(program: &ProgramBuilder, plan: &mut Plan) { match plan { Plan::Select(select_plan) => { mark_shared_cte_materialization_requirements(program, select_plan) } Plan::CompoundSelect { left, right_most, .. } => { for (select_plan, _) in left.iter_mut() { mark_shared_cte_materialization_requirements(program, select_plan); } mark_shared_cte_materialization_requirements(program, right_most); } Plan::Delete(_) | Plan::Update(_) => unreachable!("DML plans cannot be subqueries"), } } for table in plan.table_references.joined_tables_mut().iter_mut() { if let Table::FromClauseSubquery(from_clause_subquery) = &mut table.table { let from_clause_subquery = Arc::make_mut(from_clause_subquery); let shared_materialization = from_clause_subquery.cte_id().is_some_and(|cte_id| { program.get_cte_reference_count(cte_id) > 1 && !plan_has_outer_scope_dependency(&from_clause_subquery.plan) }); from_clause_subquery.set_shared_materialization(shared_materialization); if let Some(cte_id) = from_clause_subquery.cte_id() { tracing::trace!( cte_id, reference_count = program.get_cte_reference_count(cte_id), shared_materialization, outer_scope_dependency = plan_has_outer_scope_dependency( &from_clause_subquery.plan, ), contains_nested_correlation = plan_is_correlated(&from_clause_subquery.plan), identifier = %table.identifier, "annotated CTE materialization requirements" ); } annotate_plan(program, from_clause_subquery.plan.as_mut()); } } for subquery in plan.non_from_clause_subqueries.iter_mut() { let SubqueryState::Unevaluated { plan: Some(subquery_plan), } = &mut subquery.state else { continue; }; mark_shared_cte_materialization_requirements(program, subquery_plan); } } // Compute query plans for subqueries occurring in any position other than the FROM clause. // This includes the WHERE clause, HAVING clause, GROUP BY clause, ORDER BY clause, LIMIT clause, and OFFSET clause. /// The AST expression containing the subquery ([ast::Expr::Exists], [ast::Expr::Subquery], [ast::Expr::InSelect]) is replaced with a [ast::Expr::SubqueryResult] expression. /// The [ast::Expr::SubqueryResult] expression contains the subquery ID, the left-hand side expression (only applicable to IN subqueries), the NOT IN flag (only applicable to IN subqueries), and the subquery type. /// The computed plans are stored in the [NonFromClauseSubquery] structs on the [SelectPlan], and evaluated at the appropriate time during the translation of the main query. /// The appropriate time is determined by whether the subquery is correlated or uncorrelated; /// if it is uncorrelated, it can be evaluated as early as possible, but if it is correlated, it must be evaluated after all of its dependencies from the /// outer query are 'in scope', i.e. their cursors are open and rewound. pub fn plan_subqueries_from_select_plan( program: &mut ProgramBuilder, plan: &mut SelectPlan, resolver: &Resolver, connection: &Arc, ) -> Result<()> { // WHERE plan_subqueries_with_outer_query_access( program, &mut plan.non_from_clause_subqueries, &mut plan.table_references, resolver, plan.where_clause.iter_mut().map(|t| &mut t.expr), connection, SubqueryPosition::Where, SubqueryOrigin::SelectWhere, SubqueryPosition::Where.allow_correlated(), )?; // GROUP BY if let Some(group_by) = &mut plan.group_by { plan_subqueries_with_outer_query_access( program, &mut plan.non_from_clause_subqueries, &mut plan.table_references, resolver, group_by.exprs.iter_mut(), connection, SubqueryPosition::GroupBy, SubqueryOrigin::SelectGroupBy, SubqueryPosition::GroupBy.allow_correlated(), )?; if let Some(having) = group_by.having.as_mut() { plan_subqueries_with_outer_query_access( program, &mut plan.non_from_clause_subqueries, &mut plan.table_references, resolver, having.iter_mut(), connection, SubqueryPosition::Having, SubqueryOrigin::SelectHaving, !group_by.exprs.is_empty(), )?; } } // Result columns plan_subqueries_with_outer_query_access( program, &mut plan.non_from_clause_subqueries, &mut plan.table_references, resolver, plan.result_columns.iter_mut().map(|c| &mut c.expr), connection, SubqueryPosition::ResultColumn, SubqueryOrigin::SelectList, SubqueryPosition::ResultColumn.allow_correlated(), )?; // ORDER BY plan_subqueries_with_outer_query_access( program, &mut plan.non_from_clause_subqueries, &mut plan.table_references, resolver, plan.order_by.iter_mut().map(|(expr, _)| &mut **expr), connection, SubqueryPosition::OrderBy, SubqueryOrigin::SelectOrderBy, SubqueryPosition::OrderBy.allow_correlated(), )?; // LIMIT and OFFSET cannot reference columns from the outer query let get_outer_query_refs = |_: &TableReferences| vec![]; { let mut subquery_parser = get_subquery_parser( program, &mut plan.non_from_clause_subqueries, &mut plan.table_references, resolver, connection, get_outer_query_refs, SubqueryPosition::LimitOffset, SubqueryOrigin::SelectLimitOffset, false, ); // Limit if let Some(limit) = &mut plan.limit { walk_expr_mut(limit, &mut subquery_parser)?; } // Offset if let Some(offset) = &mut plan.offset { walk_expr_mut(offset, &mut subquery_parser)?; } } // Recollect aggregates after all subquery planning. // This is necessary because: // 1. Aggregates are collected with cloned expressions before subquery planning modifies them // (e.g., EXISTS -> SubqueryResult), causing stale args in aggregates. // 2. ORDER BY may be cleared for single-row aggregates AFTER aggregates were collected from it, // leaving orphaned aggregates with unprocessed subqueries in their args. // Recollecting from the current state of result_columns, HAVING, and ORDER BY ensures // aggregates have updated expressions and excludes aggregates from cleared ORDER BY. if !plan.aggregates.is_empty() { recollect_aggregates(plan, resolver)?; } assign_select_subquery_eval_phases(plan); mark_shared_cte_materialization_requirements(program, plan); update_column_used_masks( &mut plan.table_references, &mut plan.non_from_clause_subqueries, ); Ok(()) } /// Compute query plans for subqueries in a DML statement's WHERE clause. /// This is used by DELETE and UPDATE statements which only have subqueries in the WHERE clause. /// Similar to [plan_subqueries_from_select_plan] but only handles the WHERE clause /// since these statements don't have GROUP BY, ORDER BY, or result column subqueries. pub fn plan_subqueries_from_where_clause( program: &mut ProgramBuilder, non_from_clause_subqueries: &mut Vec, table_references: &mut TableReferences, where_clause: &mut [WhereTerm], resolver: &Resolver, connection: &Arc, ) -> Result<()> { plan_subqueries_with_outer_query_access( program, non_from_clause_subqueries, table_references, resolver, where_clause.iter_mut().map(|t| &mut t.expr), connection, SubqueryPosition::Where, SubqueryOrigin::DmlWhere, SubqueryPosition::Where.allow_correlated(), )?; update_column_used_masks(table_references, non_from_clause_subqueries); Ok(()) } /// Compute query plans for subqueries in VALUES expressions. /// This is used by INSERT statements with VALUES clauses and SELECT with VALUES. /// The VALUES expressions may contain scalar subqueries that need to be planned. #[allow(clippy::vec_box)] pub fn plan_subqueries_from_values( program: &mut ProgramBuilder, non_from_clause_subqueries: &mut Vec, table_references: &mut TableReferences, values: &mut [Vec>], resolver: &Resolver, connection: &Arc, ) -> Result<()> { plan_subqueries_with_outer_query_access( program, non_from_clause_subqueries, table_references, resolver, values.iter_mut().flatten().map(|e| e.as_mut()), connection, SubqueryPosition::ResultColumn, // VALUES are similar to result columns in terms of subquery handling SubqueryOrigin::SelectList, SubqueryPosition::ResultColumn.allow_correlated(), )?; update_column_used_masks(table_references, non_from_clause_subqueries); Ok(()) } /// Compute query plans for subqueries in UPDATE SET clause expressions. /// This is used by UPDATE statements where SET clause values contain scalar subqueries. /// e.g. `UPDATE t SET col = (SELECT max(id) FROM t2)` pub fn plan_subqueries_from_set_clauses( program: &mut ProgramBuilder, non_from_clause_subqueries: &mut Vec, table_references: &mut TableReferences, set_clauses: &mut [(usize, Box)], resolver: &Resolver, connection: &Arc, ) -> Result<()> { plan_subqueries_with_outer_query_access( program, non_from_clause_subqueries, table_references, resolver, set_clauses.iter_mut().map(|(_, expr)| expr.as_mut()), connection, SubqueryPosition::ResultColumn, // SET clause subqueries are similar to result columns SubqueryOrigin::DmlSet, SubqueryPosition::ResultColumn.allow_correlated(), )?; update_column_used_masks(table_references, non_from_clause_subqueries); Ok(()) } /// Compute query plans for subqueries in RETURNING expressions. /// This is used by INSERT, UPDATE, and DELETE statements with RETURNING clauses. /// RETURNING expressions may contain scalar subqueries that need to be planned. pub fn plan_subqueries_from_returning( program: &mut ProgramBuilder, non_from_clause_subqueries: &mut Vec, table_references: &mut TableReferences, returning: &mut [ast::ResultColumn], resolver: &Resolver, connection: &Arc, ) -> Result<()> { // Extract mutable references to expressions from ResultColumn::Expr variants let exprs = returning.iter_mut().filter_map(|rc| match rc { ast::ResultColumn::Expr(expr, _) => Some(expr.as_mut()), ast::ResultColumn::Star | ast::ResultColumn::TableStar(_) => None, }); plan_subqueries_with_outer_query_access( program, non_from_clause_subqueries, table_references, resolver, exprs, connection, SubqueryPosition::ResultColumn, SubqueryOrigin::DmlReturning, SubqueryPosition::ResultColumn.allow_correlated(), )?; update_column_used_masks(table_references, non_from_clause_subqueries); Ok(()) } /// Plan subqueries in a trigger WHEN clause expression. /// The WHEN clause has no FROM clause, so there are no outer query references. /// NEW/OLD references should already be rewritten to Expr::Register before calling this. pub fn plan_subqueries_from_trigger_when_clause( program: &mut ProgramBuilder, non_from_clause_subqueries: &mut Vec, expr: &mut ast::Expr, resolver: &Resolver, connection: &Arc, ) -> Result<()> { let mut table_references = TableReferences::new(vec![], vec![]); plan_subqueries_with_outer_query_access( program, non_from_clause_subqueries, &mut table_references, resolver, std::iter::once(expr), connection, SubqueryPosition::Where, SubqueryOrigin::TriggerWhen, false, ) } /// Compute query plans for subqueries in the WHERE clause and HAVING clause (both of which have access to the outer query scope) #[allow(clippy::too_many_arguments)] fn plan_subqueries_with_outer_query_access<'a>( program: &mut ProgramBuilder, out_subqueries: &mut Vec, referenced_tables: &mut TableReferences, resolver: &Resolver, exprs: impl Iterator, connection: &Arc, position: SubqueryPosition, origin: SubqueryOrigin, allow_correlated: bool, ) -> Result<()> { // Most subqueries can reference columns from the outer query, // including nested cases where a subquery inside a subquery references columns from its parent's parent // and so on. let get_outer_query_refs = |referenced_tables: &TableReferences| { referenced_tables .joined_tables() .iter() .map(|t| { // Extract cte_id from FromClauseSubquery if this is a CTE reference let cte_id = match &t.table { Table::FromClauseSubquery(subq) => subq.cte_id(), _ => None, }; OuterQueryReference { table: t.table.clone(), identifier: t.identifier.clone(), internal_id: t.internal_id, col_used_mask: ColumnUsedMask::default(), cte_select: None, cte_explicit_columns: vec![], cte_id, cte_definition_only: false, rowid_referenced: false, } }) .chain( referenced_tables .outer_query_refs() .iter() .map(|t| OuterQueryReference { table: t.table.clone(), identifier: t.identifier.clone(), internal_id: t.internal_id, col_used_mask: ColumnUsedMask::default(), cte_select: t.cte_select.clone(), cte_explicit_columns: t.cte_explicit_columns.clone(), cte_id: t.cte_id, // Preserve CTE ID from outer query refs cte_definition_only: t.cte_definition_only, rowid_referenced: false, }), ) .collect::>() }; let mut subquery_parser = get_subquery_parser( program, out_subqueries, referenced_tables, resolver, connection, get_outer_query_refs, position, origin, allow_correlated, ); for expr in exprs { walk_expr_mut(expr, &mut subquery_parser)?; } Ok(()) } /// Create a closure that will walk the AST and replace subqueries with [ast::Expr::SubqueryResult] expressions.] #[allow(clippy::too_many_arguments)] fn get_subquery_parser<'a>( program: &'a mut ProgramBuilder, out_subqueries: &'a mut Vec, referenced_tables: &'a mut TableReferences, resolver: &'a Resolver, connection: &'a Arc, get_outer_query_refs: fn(&TableReferences) -> Vec, position: SubqueryPosition, origin: SubqueryOrigin, allow_correlated: bool, ) -> impl FnMut(&mut ast::Expr) -> Result + 'a { let handle_unsupported_correlation = |correlated: bool, position: SubqueryPosition, allow_correlated: bool| -> Result<()> { if correlated && !allow_correlated { crate::bail_parse_error!( "correlated subqueries in {} clause are not supported yet", position.name() ); } Ok(()) }; move |expr: &mut ast::Expr| -> Result { match expr { ast::Expr::Exists(_) => { let subquery_id = program.table_reference_counter.next(); let outer_query_refs = get_outer_query_refs(referenced_tables); let result_reg = program.alloc_register(); let subquery_type = SubqueryType::Exists { result_reg }; let result_expr = ast::Expr::SubqueryResult { subquery_id, lhs: None, not_in: false, query_type: subquery_type.clone(), }; let ast::Expr::Exists(subselect) = std::mem::replace(expr, result_expr) else { unreachable!(); }; let plan = prepare_select_plan( subselect, resolver, program, &outer_query_refs, QueryDestination::ExistsSubqueryResult { result_reg }, connection, )?; let Plan::Select(mut plan) = plan else { crate::bail_parse_error!( "compound SELECT queries not supported yet in WHERE clause subqueries" ); }; optimize_select_plan(&mut plan, resolver.schema())?; let correlated = plan.is_correlated(); handle_unsupported_correlation(correlated, position, allow_correlated)?; out_subqueries.push(NonFromClauseSubquery { internal_id: subquery_id, query_type: subquery_type, state: SubqueryState::Unevaluated { plan: Some(Box::new(plan)), }, correlated, origin, eval_phase: origin.phase_floor(), }); Ok(WalkControl::Continue) } ast::Expr::Subquery(_) => { let subquery_id = program.table_reference_counter.next(); let outer_query_refs = get_outer_query_refs(referenced_tables); let result_expr = ast::Expr::SubqueryResult { subquery_id, lhs: None, not_in: false, // Placeholder values because the number of columns returned is not known until the plan is prepared. // These are replaced below after planning. query_type: SubqueryType::RowValue { result_reg_start: 0, num_regs: 0, }, }; let ast::Expr::Subquery(subselect) = std::mem::replace(expr, result_expr) else { unreachable!(); }; let plan = prepare_select_plan( subselect, resolver, program, &outer_query_refs, QueryDestination::Unset, connection, )?; let Plan::Select(mut plan) = plan else { crate::bail_parse_error!( "compound SELECT queries not supported yet in WHERE clause subqueries" ); }; optimize_select_plan(&mut plan, resolver.schema())?; let reg_count = plan.result_columns.len(); let reg_start = program.alloc_registers(reg_count); plan.query_destination = QueryDestination::RowValueSubqueryResult { result_reg_start: reg_start, num_regs: reg_count, }; // Only inject LIMIT 1 if there's no existing limit, or the existing limit is > 1, // If LIMIT 0, subquery should return no rows (NULL). let limit = match &plan.limit { Some(expr) => match parse_signed_number(expr) { Ok(Value::Numeric(Numeric::Integer(v))) => !(0..=1).contains(&v), _ => true, }, None => true, }; if limit { // RowValue subqueries are satisfied after at most 1 row has been returned, // as they are used in comparisons with a scalar or a tuple of scalars like (x,y) = (SELECT ...) or x = (SELECT ...). plan.limit = Some(Box::new(ast::Expr::Literal(ast::Literal::Numeric( "1".to_string(), )))); } let ast::Expr::SubqueryResult { subquery_id, lhs: None, not_in: false, query_type: SubqueryType::RowValue { result_reg_start, num_regs, }, } = &mut *expr else { unreachable!(); }; *result_reg_start = reg_start; *num_regs = reg_count; let correlated = plan.is_correlated(); handle_unsupported_correlation(correlated, position, allow_correlated)?; out_subqueries.push(NonFromClauseSubquery { internal_id: *subquery_id, query_type: SubqueryType::RowValue { result_reg_start: reg_start, num_regs: reg_count, }, state: SubqueryState::Unevaluated { plan: Some(Box::new(plan)), }, correlated, origin, eval_phase: origin.phase_floor(), }); Ok(WalkControl::Continue) } ast::Expr::InSelect { .. } => { let subquery_id = program.table_reference_counter.next(); let outer_query_refs = get_outer_query_refs(referenced_tables); let ast::Expr::InSelect { lhs, not, rhs } = std::mem::take(expr) else { unreachable!(); }; let plan = prepare_select_plan( rhs, resolver, program, &outer_query_refs, QueryDestination::Unset, connection, )?; let Plan::Select(mut plan) = plan else { crate::bail_parse_error!( "compound SELECT queries not supported yet in WHERE clause subqueries" ); }; optimize_select_plan(&mut plan, resolver.schema())?; // e.g. (x,y) IN (SELECT ...) // or x IN (SELECT ...) let lhs_columns = match unwrap_parens(lhs.as_ref())? { ast::Expr::Parenthesized(exprs) => { either::Left(exprs.iter().map(|e| e.as_ref())) } expr => either::Right(core::iter::once(expr)), }; let lhs_column_count = lhs_columns.len(); if lhs_column_count != plan.result_columns.len() { crate::bail_parse_error!( "sub-select returns {} columns - expected {lhs_column_count}", plan.result_columns.len() ); } // Collect affinity and LHS collation in a single pass over lhs_columns. // "x IN (SELECT y ...)" uses the collation of x // (https://www.sqlite.org/datatype3.html#collation §7.1), // so the ephemeral index must use the LHS collation for correct // NotFound/Found probe comparisons. let mut affinity_chars = String::with_capacity(lhs_column_count); let mut lhs_collations = Vec::with_capacity(lhs_column_count); for (i, lhs_expr) in lhs_columns.enumerate() { let lhs_affinity = get_expr_affinity_info(lhs_expr, Some(referenced_tables), None); affinity_chars.push( compare_affinity( &plan.result_columns[i].expr, lhs_affinity, Some(&plan.table_references), None, ) .aff_mask(), ); lhs_collations.push(get_collseq_from_expr(lhs_expr, referenced_tables)?); } let in_affinity_str: Arc = Arc::new(affinity_chars); let columns = plan .result_columns .iter() .enumerate() .map(|(i, c)| { let rhs_collation = get_collseq_from_expr(&c.expr, &plan.table_references)?; Ok(IndexColumn { name: c.name(&plan.table_references).unwrap_or("").to_string(), order: SortOrder::Asc, pos_in_table: i, collation: lhs_collations[i].or(rhs_collation), default: None, expr: None, }) }) .collect::>>()?; let ephemeral_index = Arc::new(Index { columns, name: format!("ephemeral_index_where_sub_{subquery_id}"), table_name: String::new(), ephemeral: true, has_rowid: false, root_page: 0, unique: false, where_clause: None, index_method: None, on_conflict: None, }); let cursor_id = program.alloc_cursor_id(CursorType::BTreeIndex(ephemeral_index.clone())); plan.query_destination = QueryDestination::EphemeralIndex { cursor_id, index: ephemeral_index, affinity_str: Some(in_affinity_str.clone()), is_delete: false, }; *expr = ast::Expr::SubqueryResult { subquery_id, lhs: Some(lhs), not_in: not, query_type: SubqueryType::In { cursor_id, affinity_str: in_affinity_str.clone(), }, }; let correlated = plan.is_correlated(); handle_unsupported_correlation(correlated, position, allow_correlated)?; out_subqueries.push(NonFromClauseSubquery { internal_id: subquery_id, query_type: SubqueryType::In { cursor_id, affinity_str: in_affinity_str, }, state: SubqueryState::Unevaluated { plan: Some(Box::new(plan)), }, correlated, origin, eval_phase: origin.phase_floor(), }); Ok(WalkControl::Continue) } _ => Ok(WalkControl::Continue), } } } /// Recollect all aggregates after subquery planning. /// /// Aggregates are collected during parsing with cloned expressions. When subquery planning /// modifies expressions in place (e.g. replacing EXISTS with SubqueryResult), the aggregate's /// cloned original_expr and args become stale. This causes cache misses during translation. /// /// Instead of trying to sync stale clones, this function recollects all aggregates fresh /// from the updated expressions in result_columns, HAVING, and ORDER BY. fn recollect_aggregates(plan: &mut SelectPlan, resolver: &Resolver) -> Result<()> { let mut new_aggregates: Vec = Vec::new(); // Collect from result columns (same order as original collection) for rc in &plan.result_columns { resolve_window_and_aggregate_functions(&rc.expr, resolver, &mut new_aggregates, None)?; } // Collect from HAVING if let Some(group_by) = &plan.group_by { if let Some(having) = &group_by.having { for expr in having { resolve_window_and_aggregate_functions(expr, resolver, &mut new_aggregates, None)?; } } } // Collect from ORDER BY for (expr, _) in &plan.order_by { resolve_window_and_aggregate_functions(expr, resolver, &mut new_aggregates, None)?; } plan.aggregates = new_aggregates; Ok(()) } /// We make decisions about when to evaluate expressions or whether to use covering indexes based on /// which columns of a table have been referenced. /// Since subquery nesting is arbitrarily deep, a reference to a column must propagate recursively /// up to the parent. Example: /// /// SELECT * FROM t WHERE EXISTS (SELECT * FROM u WHERE EXISTS (SELECT * FROM v WHERE v.foo = t.foo)) /// /// In this case, t.foo is referenced in the innermost subquery, so the top level query must be notified /// that t.foo has been used. fn update_column_used_masks( table_refs: &mut TableReferences, subqueries: &mut [NonFromClauseSubquery], ) { fn propagate_outer_refs_from_select_plan(table_refs: &mut TableReferences, plan: &SelectPlan) { for child_outer_query_ref in plan .table_references .outer_query_refs() .iter() .filter(|t| t.is_used()) { if let Some(joined_table) = table_refs.find_joined_table_by_internal_id_mut(child_outer_query_ref.internal_id) { // Propagate column_use_counts so that expression index coverage // checks see the additional references from correlated subqueries. // Without this, apply_expression_index_coverage() may conclude that // all uses of a column are satisfied by an expression index when in // fact the correlated subquery needs the column directly. for col_idx in child_outer_query_ref.col_used_mask.iter() { if col_idx >= joined_table.column_use_counts.len() { joined_table.column_use_counts.resize(col_idx + 1, 0); } joined_table.column_use_counts[col_idx] += 1; } joined_table.col_used_mask |= &child_outer_query_ref.col_used_mask; } if let Some(outer_query_ref) = table_refs .find_outer_query_ref_by_internal_id_mut(child_outer_query_ref.internal_id) { outer_query_ref.col_used_mask |= &child_outer_query_ref.col_used_mask; } } for joined_table in plan.table_references.joined_tables().iter() { if let Table::FromClauseSubquery(from_clause_subquery) = &joined_table.table { propagate_outer_refs_from_plan(table_refs, from_clause_subquery.plan.as_ref()); } } } fn propagate_outer_refs_from_plan(table_refs: &mut TableReferences, plan: &Plan) { match plan { Plan::Select(select_plan) => { propagate_outer_refs_from_select_plan(table_refs, select_plan); } Plan::CompoundSelect { left, right_most, .. } => { for (select_plan, _) in left.iter() { propagate_outer_refs_from_select_plan(table_refs, select_plan); } propagate_outer_refs_from_select_plan(table_refs, right_most); } Plan::Delete(_) | Plan::Update(_) => {} } } for subquery in subqueries.iter_mut() { let SubqueryState::Unevaluated { plan } = &mut subquery.state else { panic!("subquery has already been evaluated"); }; let Some(child_plan) = plan.as_mut() else { panic!("subquery has no plan"); }; propagate_outer_refs_from_select_plan(table_refs, child_plan); } // Collect raw plan pointers to avoid cloning while sidestepping borrow rules. let from_clause_plans = table_refs .joined_tables() .iter() .filter_map(|t| match &t.table { Table::FromClauseSubquery(from_clause_subquery) => { Some(from_clause_subquery.plan.as_ref() as *const Plan) } _ => None, }) .collect::>(); for plan in from_clause_plans { // SAFETY: plans live within table_refs for the duration of this function. let plan = unsafe { &*plan }; propagate_outer_refs_from_plan(table_refs, plan); } } /// Recursively pre-materialize all multi-ref CTEs in a plan tree. /// This must be called BEFORE emitting any coroutines to ensure CTEs referenced /// inside coroutines have their cursors opened at the top level. fn pre_materialize_multi_ref_ctes( program: &mut ProgramBuilder, plan: &mut Plan, t_ctx: &mut TranslateCtx, ) -> Result<()> { match plan { Plan::Select(select_plan) => { pre_materialize_multi_ref_ctes_in_select_plan(program, select_plan, t_ctx)?; } Plan::CompoundSelect { left, right_most, .. } => { for (select_plan, _) in left.iter_mut() { pre_materialize_multi_ref_ctes_in_select_plan(program, select_plan, t_ctx)?; } pre_materialize_multi_ref_ctes_in_select_plan(program, right_most, t_ctx)?; } Plan::Delete(_) | Plan::Update(_) => {} } Ok(()) } fn pre_materialize_multi_ref_ctes_in_select_plan( program: &mut ProgramBuilder, plan: &mut SelectPlan, t_ctx: &mut TranslateCtx, ) -> Result<()> { pre_materialize_multi_ref_ctes_in_tables(program, &mut plan.table_references, t_ctx)?; pre_materialize_multi_ref_ctes_in_non_from_subqueries( program, &mut plan.non_from_clause_subqueries, t_ctx, ) } fn pre_materialize_multi_ref_ctes_in_non_from_subqueries( program: &mut ProgramBuilder, subqueries: &mut [NonFromClauseSubquery], t_ctx: &mut TranslateCtx, ) -> Result<()> { for subquery in subqueries.iter_mut() { let SubqueryState::Unevaluated { plan: Some(subquery_plan), } = &mut subquery.state else { continue; }; pre_materialize_multi_ref_ctes_in_select_plan(program, subquery_plan.as_mut(), t_ctx)?; } Ok(()) } fn pre_materialize_multi_ref_ctes_in_tables( program: &mut ProgramBuilder, tables: &mut TableReferences, t_ctx: &mut TranslateCtx, ) -> Result<()> { for table_reference in tables.joined_tables_mut().iter_mut() { if let Table::FromClauseSubquery(from_clause_subquery) = &mut table_reference.table { let from_clause_subquery = Arc::make_mut(from_clause_subquery); // First, recursively process nested plans pre_materialize_multi_ref_ctes(program, from_clause_subquery.plan.as_mut(), t_ctx)?; // Then check if THIS CTE should be materialized if let Some(cte_id) = from_clause_subquery.cte_id() { if program.get_materialized_cte(cte_id).is_some() { continue; } if from_clause_subquery.requires_table_materialization() { tracing::trace!( cte_id, identifier = %table_reference.identifier, "pre-materializing shared CTE" ); let (result_columns_start, cte_cursor_id, cte_table) = emit_materialized_subquery_table( program, from_clause_subquery.plan.as_mut(), t_ctx, &from_clause_subquery.columns, )?; program.register_materialized_cte( cte_id, MaterializedCteInfo { cursor_id: cte_cursor_id, table: cte_table, num_columns: from_clause_subquery.columns.len(), }, ); from_clause_subquery.materialized_cursor_id = Some(cte_cursor_id); from_clause_subquery.result_columns_start_reg = Some(result_columns_start); program .set_subquery_result_reg(table_reference.internal_id, result_columns_start); } } } } Ok(()) } fn choose_from_clause_subquery_execution_mode( operation: &Operation, from_clause_subquery: &crate::schema::FromClauseSubquery, ) -> FromClauseSubqueryExecutionMode { let needs_materialized_seek = matches!( operation, Operation::Search(Search::Seek { index: Some(index), .. }) if index.ephemeral ); // Compound SELECTs still need their own internal ephemeral indexes for // UNION/INTERSECT/EXCEPT bookkeeping. Reusing the subquery's synthesized // seek index as the storage target would collapse those roles together and // break set-operation semantics, so keep the direct-index fast path limited // to simple SELECT plans. let can_direct_materialize_index = from_clause_subquery.supports_direct_index_materialization(); match operation { Operation::Search(Search::Seek { index: Some(index), seek_def, }) if index.ephemeral && can_direct_materialize_index => { FromClauseSubqueryExecutionMode::DirectMaterializedIndex(DirectMaterializedSubquery { index: index.clone(), affinity_str: super::plan::synthesized_seek_affinity_str(index, seek_def), }) } _ if needs_materialized_seek => FromClauseSubqueryExecutionMode::MaterializedTable, _ if from_clause_subquery.requires_table_materialization() => { FromClauseSubqueryExecutionMode::MaterializedTable } _ => FromClauseSubqueryExecutionMode::Coroutine, } } /// Emit the subqueries contained in the FROM clause. /// This is done first so the results can be read in the main query loop. pub fn emit_from_clause_subqueries( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, tables: &mut TableReferences, join_order: &[JoinOrderMember], ) -> Result<()> { if tables.joined_tables().is_empty() { emit_explain!(program, false, "SCAN CONSTANT ROW".to_owned()); } // FIRST PASS: Pre-materialize all recursively reachable multi-ref / hinted CTEs // before any coroutine bodies are emitted. Otherwise a coroutine could try to // OpenDup a CTE whose backing table has not been created yet. pre_materialize_multi_ref_ctes_in_tables(program, tables, t_ctx)?; // Build the iteration order: join_order first (execution order), then any // hash-join build tables that aren't already in the join order. let mut visit_order: Vec = join_order .iter() .map(|member| member.original_idx) .collect(); let visit_set: HashSet = visit_order.iter().copied().collect(); for table in tables.joined_tables().iter() { if let Operation::HashJoin(hash_join_op) = &table.op { let build_idx = hash_join_op.build_table_idx; if !visit_set.contains(&build_idx) { visit_order.push(build_idx); } } } // Build lookup from table index to is_outer for LEFT-JOIN annotations let outer_table_set: HashSet = join_order .iter() .filter(|m| m.is_outer) .map(|m| m.original_idx) .collect(); for table_index in visit_order { let table_reference = &mut tables.joined_tables_mut()[table_index]; let left_join_suffix = if outer_table_set.contains(&table_index) { " LEFT-JOIN" } else { "" }; emit_explain!( program, true, match &table_reference.op { Operation::Scan(scan) => { let table_name = if table_reference.table.get_name() == table_reference.identifier { table_reference.identifier.clone() } else { format!( "{} AS {}", table_reference.table.get_name(), table_reference.identifier ) }; match scan { Scan::BTreeTable { index, .. } => { if let Some(index) = index { if table_reference.utilizes_covering_index() { format!("SCAN {table_name} USING COVERING INDEX {}", index.name) } else { format!("SCAN {table_name} USING INDEX {}", index.name) } } else { format!("SCAN {table_name}") } } Scan::VirtualTable { .. } | Scan::Subquery { .. } => { format!("SCAN {table_name}") } } } Operation::Search(search) => match search { Search::RowidEq { .. } | Search::Seek { index: None, .. } | Search::InSeek { index: None, .. } => { format!( "SEARCH {} USING INTEGER PRIMARY KEY (rowid=?){left_join_suffix}", table_reference.identifier ) } Search::Seek { index: Some(index), seek_def, } => { let constraints = super::display::seek_constraint_annotation(index, seek_def); format!( "SEARCH {} USING INDEX {}{constraints}{left_join_suffix}", table_reference.identifier, index.name ) } Search::InSeek { index: Some(index), .. } => { let constraint = if let Some(col) = index.columns.first() { format!(" ({}=?)", col.name) } else { String::new() }; format!( "SEARCH {} USING INDEX {}{constraint}{left_join_suffix}", table_reference.identifier, index.name ) } }, Operation::IndexMethodQuery(query) => { let index_method = query.index.index_method.as_ref().unwrap(); format!( "QUERY INDEX METHOD {}", index_method.definition().method_name ) } Operation::HashJoin(_) => { let table_name = if table_reference.table.get_name() == table_reference.identifier { table_reference.identifier.clone() } else { format!( "{} AS {}", table_reference.table.get_name(), table_reference.identifier ) }; format!("HASH JOIN {table_name}") } Operation::MultiIndexScan(multi_idx) => { let index_names: Vec<&str> = multi_idx .branches .iter() .map(|b| { b.index .as_ref() .map(|i| i.name.as_str()) .unwrap_or("PRIMARY KEY") }) .collect(); format!( "MULTI-INDEX {} {} ({})", match multi_idx.set_op { SetOperation::Union => "OR", SetOperation::Intersection { .. } => "AND", }, table_reference.identifier, index_names.join(", ") ) } } ); if let Table::FromClauseSubquery(from_clause_subquery) = &mut table_reference.table { let execution_mode = { let from_clause_subquery = from_clause_subquery.as_ref(); choose_from_clause_subquery_execution_mode( &table_reference.op, from_clause_subquery, ) }; let from_clause_subquery = Arc::make_mut(from_clause_subquery); // Check if this is a CTE that's already materialized if let Some(cte_id) = from_clause_subquery.cte_id() { if let Some(cte_info) = program.get_materialized_cte(cte_id).cloned() { if from_clause_subquery.materialized_cursor_id.is_some() { tracing::trace!( cte_id, identifier = %table_reference.identifier, "reusing pre-materialized CTE on original reference" ); program.pop_current_parent_explain(); continue; } // === SUBSEQUENT CTE REFERENCE: Use OpenDup === // Create a dup cursor pointing to the same ephemeral table let dup_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(cte_info.table.clone())); program.emit_insn(Insn::OpenDup { new_cursor_id: dup_cursor_id, original_cursor_id: cte_info.cursor_id, }); tracing::trace!( cte_id, identifier = %table_reference.identifier, original_cursor_id = cte_info.cursor_id, dup_cursor_id, "opening duplicate cursor for materialized CTE" ); // Update the plan's query destination to EphemeralTable so that // main_loop knows to use Rewind/Next instead of coroutine Yield if let Some(dest) = from_clause_subquery.plan.select_query_destination_mut() { *dest = QueryDestination::EphemeralTable { cursor_id: dup_cursor_id, table: cte_info.table.clone(), rowid_mode: super::plan::EphemeralRowidMode::Auto, }; } // Each CTE reference needs its OWN registers to read column values into. // We cannot share the original's result_columns_start_reg because multiple // iterators of the same CTE (e.g., outer query and subquery) would // overwrite each other's values when reading columns from their cursors. let result_columns_start = program.alloc_registers(cte_info.num_columns); from_clause_subquery.materialized_cursor_id = Some(dup_cursor_id); from_clause_subquery.result_columns_start_reg = Some(result_columns_start); program .set_subquery_result_reg(table_reference.internal_id, result_columns_start); program.pop_current_parent_explain(); continue; // Skip normal emission } } let result_columns_start = match execution_mode { FromClauseSubqueryExecutionMode::Coroutine => { emit_from_clause_subquery(program, from_clause_subquery.plan.as_mut(), t_ctx)? } FromClauseSubqueryExecutionMode::MaterializedTable => { let (result_columns_start, cte_cursor_id, cte_table) = emit_materialized_subquery_table( program, from_clause_subquery.plan.as_mut(), t_ctx, &from_clause_subquery.columns, )?; from_clause_subquery.materialized_cursor_id = Some(cte_cursor_id); if let Some(cte_id) = from_clause_subquery.cte_id() { program.register_materialized_cte( cte_id, MaterializedCteInfo { cursor_id: cte_cursor_id, table: cte_table, num_columns: from_clause_subquery.columns.len(), }, ); } result_columns_start } FromClauseSubqueryExecutionMode::DirectMaterializedIndex(direct_index) => { emit_indexed_materialized_subquery( program, from_clause_subquery.plan.as_mut(), t_ctx, table_reference.internal_id, direct_index.index, direct_index.affinity_str, from_clause_subquery.columns.len(), )? } }; from_clause_subquery.result_columns_start_reg = Some(result_columns_start); program.set_subquery_result_reg(table_reference.internal_id, result_columns_start); } program.pop_current_parent_explain(); } Ok(()) } /// Emit a FROM clause subquery and return the start register of the result columns. /// This is done by emitting a coroutine that stores the result columns in sequential registers. /// Each FROM clause subquery has its own Plan (either SelectPlan or CompoundSelect) which is wrapped in a coroutine. /// /// The resulting bytecode from a subquery is mostly exactly the same as a regular query, except: /// - it ends in an EndCoroutine instead of a Halt. /// - instead of emitting ResultRows, the coroutine yields to the main query loop. /// - the first register of the result columns is returned to the parent query, /// so that translate_expr() can read the result columns of the subquery, /// as if it were reading from a regular table. /// /// Since a subquery has its own Plan, it can contain nested subqueries, /// which can contain even more nested subqueries, etc. pub fn emit_from_clause_subquery( program: &mut ProgramBuilder, plan: &mut Plan, t_ctx: &mut TranslateCtx, ) -> Result { let yield_reg = program.alloc_register(); let coroutine_implementation_start_offset = program.allocate_label(); // Set up the coroutine yield destination for the plan match plan.select_query_destination_mut() { Some(QueryDestination::CoroutineYield { yield_reg: y, coroutine_implementation_start, }) => { // The parent query will use this register to jump to/from the subquery. *y = yield_reg; // The parent query will use this register to reinitialize the coroutine when it needs to run multiple times. *coroutine_implementation_start = coroutine_implementation_start_offset; } _ => unreachable!("emit_from_clause_subquery called on non-subquery"), } let subquery_body_end_label = program.allocate_label(); program.emit_insn(Insn::InitCoroutine { yield_reg, jump_on_definition: subquery_body_end_label, start_offset: coroutine_implementation_start_offset, }); program.preassign_label_to_next_insn(coroutine_implementation_start_offset); let result_column_start_reg = match plan { Plan::Select(select_plan) => { let mut metadata = TranslateCtx { labels_main_loop: (0..select_plan.joined_tables().len()) .map(|_| LoopLabels::new(program)) .collect(), label_main_loop_end: None, meta_group_by: None, meta_left_joins: (0..select_plan.joined_tables().len()) .map(|_| None) .collect(), meta_semi_anti_joins: (0..select_plan.joined_tables().len()) .map(|_| None) .collect(), meta_sort: None, reg_agg_start: None, reg_nonagg_emit_once_flag: None, reg_result_cols_start: None, limit_ctx: None, reg_offset: None, reg_limit_offset_sum: None, resolver: t_ctx.resolver.fork(), non_aggregate_expressions: Vec::new(), agg_leaf_columns: Vec::new(), cdc_cursor_id: None, meta_window: None, meta_in_seeks: (0..select_plan.joined_tables().len()) .map(|_| None) .collect(), materialized_build_inputs: HashMap::default(), hash_table_contexts: HashMap::default(), unsafe_testing: t_ctx.unsafe_testing, }; emit_query(program, select_plan, &mut metadata)? } Plan::CompoundSelect { .. } => { // Clone the plan to pass to emit_program_for_compound_select (it takes ownership) let plan_clone = plan.clone(); let resolver = t_ctx.resolver.fork(); // emit_program_for_compound_select returns the result column start register // for coroutine mode, which is needed by the outer query. emit_program_for_compound_select(program, &resolver, plan_clone)? .expect("compound CTE in coroutine mode must have result register") } Plan::Delete(_) | Plan::Update(_) => { unreachable!("DELETE/UPDATE plans cannot be FROM clause subqueries") } }; program.emit_insn(Insn::EndCoroutine { yield_reg }); program.preassign_label_to_next_insn(subquery_body_end_label); Ok(result_column_start_reg) } /// Materialize a single-reference seekable FROM-subquery directly into an /// ephemeral index. /// /// This skips the intermediate EphemeralTable when we only need seek access and do /// not need table-backed sharing via OpenDup. Result columns for this path are read /// back from the index using `pos_in_table` mapping rather than raw index position. fn emit_indexed_materialized_subquery( program: &mut ProgramBuilder, plan: &mut Plan, t_ctx: &mut TranslateCtx, internal_id: ast::TableInternalId, index: Arc, affinity_str: Option>, num_columns: usize, ) -> Result { let cursor_id = program .alloc_cursor_index_if_not_exists(CursorKey::index(internal_id, index.clone()), &index)?; let result_columns_start_reg = program.alloc_registers(num_columns); if let Some(dest) = plan.select_query_destination_mut() { *dest = QueryDestination::EphemeralIndex { cursor_id, index, affinity_str, is_delete: false, }; } program.emit_insn(Insn::OpenEphemeral { cursor_id, is_table: false, }); match plan { Plan::Select(select_plan) => { let mut metadata = TranslateCtx { labels_main_loop: (0..select_plan.joined_tables().len()) .map(|_| LoopLabels::new(program)) .collect(), label_main_loop_end: None, meta_group_by: None, meta_left_joins: (0..select_plan.joined_tables().len()) .map(|_| None) .collect(), meta_semi_anti_joins: (0..select_plan.joined_tables().len()) .map(|_| None) .collect(), meta_sort: None, reg_agg_start: None, reg_nonagg_emit_once_flag: None, reg_result_cols_start: None, limit_ctx: None, reg_offset: None, reg_limit_offset_sum: None, resolver: t_ctx.resolver.fork(), non_aggregate_expressions: Vec::new(), agg_leaf_columns: Vec::new(), cdc_cursor_id: None, meta_window: None, meta_in_seeks: (0..select_plan.joined_tables().len()) .map(|_| None) .collect(), materialized_build_inputs: HashMap::default(), hash_table_contexts: HashMap::default(), unsafe_testing: t_ctx.unsafe_testing, }; emit_query(program, select_plan, &mut metadata)?; } Plan::CompoundSelect { .. } => { let plan_clone = plan.clone(); let resolver = t_ctx.resolver.fork(); emit_program_for_compound_select(program, &resolver, plan_clone)?; } Plan::Delete(_) | Plan::Update(_) => { unreachable!("DELETE/UPDATE plans cannot be FROM clause subqueries") } } Ok(result_columns_start_reg) } fn emit_materialized_subquery_table( program: &mut ProgramBuilder, plan: &mut Plan, t_ctx: &mut TranslateCtx, columns: &[Column], ) -> Result<(usize, CursorID, Arc)> { use super::plan::EphemeralRowidMode; // EphemeralTable (not EphemeralIndex) is required because it preserves // insertion order, which SQL semantics require for UNION ALL. It also // needs the subquery's column layout so later Column opcodes can read // materialized rows through the normal table-cursor path. let ephemeral_table = Arc::new(BTreeTable { root_page: 0, name: String::new(), columns: columns.to_vec(), primary_key_columns: vec![], has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }); let cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(ephemeral_table.clone())); // Allocate registers for reading result columns let result_columns_start_reg = program.alloc_registers(columns.len()); // Open the ephemeral table program.emit_insn(Insn::OpenEphemeral { cursor_id, is_table: true, }); // Set the query destination to write to the ephemeral table if let Some(dest) = plan.select_query_destination_mut() { *dest = QueryDestination::EphemeralTable { cursor_id, table: ephemeral_table.clone(), rowid_mode: EphemeralRowidMode::Auto, }; } // Emit the subquery - it will insert rows into the ephemeral table match plan { Plan::Select(select_plan) => { let mut metadata = TranslateCtx { labels_main_loop: (0..select_plan.joined_tables().len()) .map(|_| LoopLabels::new(program)) .collect(), label_main_loop_end: None, meta_group_by: None, meta_left_joins: (0..select_plan.joined_tables().len()) .map(|_| None) .collect(), meta_semi_anti_joins: (0..select_plan.joined_tables().len()) .map(|_| None) .collect(), meta_sort: None, reg_agg_start: None, reg_nonagg_emit_once_flag: None, reg_result_cols_start: None, limit_ctx: None, reg_offset: None, reg_limit_offset_sum: None, resolver: t_ctx.resolver.fork(), non_aggregate_expressions: Vec::new(), agg_leaf_columns: Vec::new(), cdc_cursor_id: None, meta_window: None, meta_in_seeks: (0..select_plan.joined_tables().len()) .map(|_| None) .collect(), materialized_build_inputs: HashMap::default(), hash_table_contexts: HashMap::default(), unsafe_testing: t_ctx.unsafe_testing, }; emit_query(program, select_plan, &mut metadata)?; } Plan::CompoundSelect { .. } => { // Clone the plan to pass to emit_program_for_compound_select (it takes ownership) let plan_clone = plan.clone(); let resolver = t_ctx.resolver.fork(); emit_program_for_compound_select(program, &resolver, plan_clone)?; } Plan::Delete(_) | Plan::Update(_) => { unreachable!("DELETE/UPDATE plans cannot be FROM clause subqueries") } } Ok((result_columns_start_reg, cursor_id, ephemeral_table)) } /// Translate a subquery that is not part of the FROM clause. /// If a subquery is uncorrelated (i.e. does not reference columns from the outer query), /// it will be executed only once. /// /// If it is correlated (i.e. references columns from the outer query), /// it will be executed for each row of the outer query. /// /// The result of the subquery is stored in: /// /// - a single register for EXISTS subqueries, /// - a range of registers for RowValue subqueries, /// - an ephemeral index for IN subqueries. pub fn emit_non_from_clause_subquery( program: &mut ProgramBuilder, resolver: &Resolver, plan: SelectPlan, query_type: &SubqueryType, is_correlated: bool, preserve_outer_expr_cache: bool, ) -> Result<()> { program.nested(|program| { let subquery_id = program.next_subquery_eqp_id(); let correlated_prefix = if is_correlated { "CORRELATED " } else { "" }; match query_type { SubqueryType::Exists { .. } => { // EXISTS subqueries don't get a separate EQP annotation in SQLite; // instead the SEARCH/SCAN line gets an "EXISTS" suffix handled elsewhere. } SubqueryType::In { .. } => { emit_explain!( program, true, format!("{correlated_prefix}LIST SUBQUERY {subquery_id}") ); } SubqueryType::RowValue { .. } => { emit_explain!( program, true, format!("{correlated_prefix}SCALAR SUBQUERY {subquery_id}") ); } } let label_skip_after_first_run = if !is_correlated { let label = program.allocate_label(); program.emit_insn(Insn::Once { target_pc_when_reentered: label, }); Some(label) } else { None }; match query_type { SubqueryType::Exists { result_reg, .. } => { let subroutine_reg = program.alloc_register(); program.emit_insn(Insn::BeginSubrtn { dest: subroutine_reg, dest_end: None, }); program.emit_insn(Insn::Integer { value: 0, dest: *result_reg, }); if preserve_outer_expr_cache { emit_program_for_select_with_resolver( program, resolver.fork_with_expr_cache(), plan, )?; } else { emit_program_for_select(program, resolver, plan)?; } program.emit_insn(Insn::Return { return_reg: subroutine_reg, can_fallthrough: true, }); } SubqueryType::In { cursor_id, .. } => { program.emit_insn(Insn::OpenEphemeral { cursor_id: *cursor_id, is_table: false, }); if preserve_outer_expr_cache { emit_program_for_select_with_resolver( program, resolver.fork_with_expr_cache(), plan, )?; } else { emit_program_for_select(program, resolver, plan)?; } } SubqueryType::RowValue { result_reg_start, num_regs, } => { let subroutine_reg = program.alloc_register(); program.emit_insn(Insn::BeginSubrtn { dest: subroutine_reg, dest_end: None, }); for result_reg in *result_reg_start..*result_reg_start + *num_regs { program.emit_insn(Insn::Null { dest: result_reg, dest_end: None, }); } if preserve_outer_expr_cache { emit_program_for_select_with_resolver( program, resolver.fork_with_expr_cache(), plan, )?; } else { emit_program_for_select(program, resolver, plan)?; } program.emit_insn(Insn::Return { return_reg: subroutine_reg, can_fallthrough: true, }); } } // Pop the parent explain for LIST/SCALAR SUBQUERY annotations. if !matches!(query_type, SubqueryType::Exists { .. }) { program.pop_current_parent_explain(); } if let Some(label) = label_skip_after_first_run { program.preassign_label_to_next_insn(label); } Ok(()) }) } pub fn emit_non_from_clause_subqueries_for_phase( program: &mut ProgramBuilder, resolver: &Resolver, subqueries: &mut [NonFromClauseSubquery], join_order: &[JoinOrderMember], table_references: Option<&TableReferences>, phase: SubqueryEvalPhase, mut should_emit: impl FnMut(&NonFromClauseSubquery) -> bool, ) -> Result<()> { for subquery in subqueries.iter_mut() { if subquery.has_been_evaluated() || !should_emit(subquery) { continue; } let evaluated_at = match phase { SubqueryEvalPhase::BeforeLoop | SubqueryEvalPhase::Loop(_) => { if !matches!(subquery.eval_phase, SubqueryEvalPhase::BeforeLoop) { continue; } let expected_eval_at = match phase { SubqueryEvalPhase::BeforeLoop => EvalAt::BeforeLoop, SubqueryEvalPhase::Loop(loop_idx) => EvalAt::Loop(loop_idx), _ => unreachable!(), }; let evaluated_at = subquery.get_eval_at(join_order, table_references)?; if evaluated_at != expected_eval_at { continue; } evaluated_at } _ => { if subquery.eval_phase != phase { continue; } subquery.get_eval_at(join_order, table_references)? } }; let subquery_plan = subquery.consume_plan(evaluated_at); emit_non_from_clause_subquery( program, resolver, *subquery_plan, &subquery.query_type, subquery.correlated, !matches!( phase, SubqueryEvalPhase::BeforeLoop | SubqueryEvalPhase::Loop(_) ), )?; } Ok(()) } pub fn emit_non_from_clause_subqueries_for_eval_at( program: &mut ProgramBuilder, resolver: &Resolver, subqueries: &mut [NonFromClauseSubquery], join_order: &[JoinOrderMember], table_references: Option<&TableReferences>, eval_at: EvalAt, should_emit: impl FnMut(&NonFromClauseSubquery) -> bool, ) -> Result<()> { emit_non_from_clause_subqueries_for_phase( program, resolver, subqueries, join_order, table_references, match eval_at { EvalAt::BeforeLoop => SubqueryEvalPhase::BeforeLoop, EvalAt::Loop(loop_idx) => SubqueryEvalPhase::Loop(loop_idx), }, should_emit, ) } fn assign_select_subquery_eval_phases(plan: &mut SelectPlan) { let has_grouped_output = plan .group_by .as_ref() .is_some_and(|group_by| !group_by.exprs.is_empty()); for subquery in plan.non_from_clause_subqueries.iter_mut() { subquery.eval_phase = match subquery.origin { SubqueryOrigin::SelectHaving | SubqueryOrigin::SelectOrderBy if has_grouped_output => { SubqueryEvalPhase::GroupedOutput } _ => subquery.origin.phase_floor(), }; } } ================================================ FILE: core/translate/transaction.rs ================================================ use crate::schema::Schema; use crate::translate::emitter::{ emit_cdc_commit_insns, prepare_cdc_if_necessary, Resolver, TransactionMode, }; use crate::translate::{ProgramBuilder, ProgramBuilderOpts}; use crate::vdbe::insn::Insn; use crate::Result; use turso_parser::ast::{Name, TransactionType}; pub fn translate_tx_begin( tx_type: Option, _tx_name: Option, schema: &Schema, program: &mut ProgramBuilder, ) -> Result<()> { program.extend(&ProgramBuilderOpts { num_cursors: 0, approx_num_insns: 0, approx_num_labels: 0, }); let tx_type = tx_type.unwrap_or(TransactionType::Deferred); match tx_type { TransactionType::Deferred => { program.emit_insn(Insn::AutoCommit { auto_commit: false, rollback: false, }); } TransactionType::Immediate | TransactionType::Exclusive => { program.emit_insn(Insn::Transaction { db: crate::MAIN_DB_ID, tx_mode: TransactionMode::Write, schema_cookie: schema.schema_version, }); // TODO: Emit transaction instruction on temporary tables when we support them. program.emit_insn(Insn::AutoCommit { auto_commit: false, rollback: false, }); } TransactionType::Concurrent => { program.emit_insn(Insn::Transaction { db: crate::MAIN_DB_ID, tx_mode: TransactionMode::Concurrent, schema_cookie: schema.schema_version, }); // TODO: Emit transaction instruction on temporary tables when we support them. program.emit_insn(Insn::AutoCommit { auto_commit: false, rollback: false, }); } } Ok(()) } pub fn translate_tx_commit( _tx_name: Option, schema: &Schema, resolver: &Resolver, program: &mut ProgramBuilder, ) -> Result<()> { program.extend(&ProgramBuilderOpts { num_cursors: 0, approx_num_insns: 0, approx_num_labels: 0, }); let cdc_info = program.capture_data_changes_info().as_ref(); if cdc_info.is_some_and(|info| info.cdc_version().has_commit_record()) { // Use a dummy table name for prepare_cdc_if_necessary — any name that isn't the // CDC table itself will work. if let Some((cdc_cursor_id, _)) = prepare_cdc_if_necessary(program, schema, "__tx_commit__")? { emit_cdc_commit_insns(program, resolver, cdc_cursor_id)?; } } program.emit_insn(Insn::AutoCommit { auto_commit: true, rollback: false, }); Ok(()) } ================================================ FILE: core/translate/trigger.rs ================================================ use crate::translate::emitter::Resolver; use crate::translate::schema::{emit_schema_entry, SchemaEntryType, SQLITE_TABLEID}; use crate::translate::ProgramBuilder; use crate::translate::ProgramBuilderOpts; use crate::util::{escape_sql_string_literal, normalize_ident}; use crate::vdbe::builder::CursorType; use crate::vdbe::insn::{Cookie, Insn}; use crate::{bail_parse_error, Result}; use turso_parser::ast::{self, QualifiedName}; /// Reconstruct SQL string from CREATE TRIGGER AST #[allow(clippy::too_many_arguments)] pub(crate) fn create_trigger_to_sql( temporary: bool, if_not_exists: bool, trigger_name: &QualifiedName, time: Option, event: &ast::TriggerEvent, tbl_name: &QualifiedName, for_each_row: bool, when_clause: Option<&ast::Expr>, commands: &[ast::TriggerCmd], ) -> String { let mut sql = String::new(); sql.push_str("CREATE"); if temporary { sql.push_str(" TEMP"); } sql.push_str(" TRIGGER"); if if_not_exists { sql.push_str(" IF NOT EXISTS"); } sql.push(' '); sql.push_str(&trigger_name.name.as_ident()); sql.push(' '); if let Some(t) = time { match t { ast::TriggerTime::Before => sql.push_str("BEFORE "), ast::TriggerTime::After => sql.push_str("AFTER "), ast::TriggerTime::InsteadOf => sql.push_str("INSTEAD OF "), } } match event { ast::TriggerEvent::Delete => sql.push_str("DELETE"), ast::TriggerEvent::Insert => sql.push_str("INSERT"), ast::TriggerEvent::Update => sql.push_str("UPDATE"), ast::TriggerEvent::UpdateOf(cols) => { sql.push_str("UPDATE OF "); for (i, col) in cols.iter().enumerate() { if i > 0 { sql.push_str(", "); } sql.push_str(&col.as_ident()); } } } sql.push_str(" ON "); sql.push_str(&tbl_name.name.as_ident()); if for_each_row { sql.push_str(" FOR EACH ROW"); } if let Some(when) = when_clause { sql.push_str(" WHEN "); sql.push_str(&when.to_string()); } sql.push_str(" BEGIN"); for cmd in commands { sql.push(' '); sql.push_str(&cmd.to_string()); sql.push(';'); } sql.push_str(" END"); sql } /// Translate CREATE TRIGGER statement #[allow(clippy::too_many_arguments)] pub fn translate_create_trigger( trigger_name: QualifiedName, resolver: &Resolver, temporary: bool, if_not_exists: bool, time: Option, tbl_name: QualifiedName, program: &mut ProgramBuilder, sql: String, commands: &[ast::TriggerCmd], when_clause: Option<&ast::Expr>, ) -> Result<()> { let database_id = resolver.resolve_database_id(&trigger_name)?; if crate::is_attached_db(database_id) { let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version); program.begin_write_on_database(database_id, schema_cookie); } program.begin_write_operation(); let normalized_trigger_name = normalize_ident(trigger_name.name.as_str()); let normalized_table_name = normalize_ident(tbl_name.name.as_str()); // Validate that trigger body does not reference other databases. validate_trigger_no_cross_db_refs( resolver, database_id, &normalized_trigger_name, commands, when_clause, )?; if crate::schema::is_system_table(&normalized_table_name) { bail_parse_error!("cannot create trigger on system table"); } // Check if trigger already exists if resolver.with_schema(database_id, |s| { s.get_trigger(&normalized_trigger_name).is_some() }) { if if_not_exists { return Ok(()); } bail_parse_error!("Trigger {} already exists", normalized_trigger_name); } // Verify the table exists let table = resolver.with_schema(database_id, |s| s.get_table(&normalized_table_name)); let Some(table) = table else { bail_parse_error!("no such table: {}", normalized_table_name); }; if table.virtual_table().is_some() { bail_parse_error!("cannot create triggers on virtual tables"); } if time .as_ref() .is_some_and(|t| *t == ast::TriggerTime::InsteadOf) { bail_parse_error!("INSTEAD OF triggers are not supported yet"); } if temporary { bail_parse_error!("TEMPORARY triggers are not supported yet"); } let opts = ProgramBuilderOpts { num_cursors: 1, approx_num_insns: 30, approx_num_labels: 1, }; program.extend(&opts); // Open cursor to sqlite_schema table let table = resolver.schema().get_btree_table(SQLITE_TABLEID).unwrap(); let sqlite_schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(table)); program.emit_insn(Insn::OpenWrite { cursor_id: sqlite_schema_cursor_id, root_page: 1i64.into(), db: database_id, }); // Add the trigger entry to sqlite_schema emit_schema_entry( program, resolver, sqlite_schema_cursor_id, None, // cdc_table_cursor_id, no cdc for triggers SchemaEntryType::Trigger, &normalized_trigger_name, &normalized_table_name, 0, // triggers don't have a root page Some(sql), )?; // Update schema version let schema_version = resolver.with_schema(database_id, |s| s.schema_version); program.emit_insn(Insn::SetCookie { db: database_id, cookie: Cookie::SchemaVersion, value: (schema_version + 1) as i32, p5: 0, }); // Parse schema to load the new trigger let escaped_trigger_name = escape_sql_string_literal(&normalized_trigger_name); program.emit_insn(Insn::ParseSchema { db: database_id, where_clause: Some(format!("name = '{escaped_trigger_name}'")), }); Ok(()) } /// Validate that no table or expression reference in a trigger body points to a /// database other than the trigger's own database. SQLite forbids this with: /// "trigger X cannot reference objects in database Y" fn validate_trigger_no_cross_db_refs( resolver: &Resolver, trigger_db_id: usize, trigger_name: &str, commands: &[ast::TriggerCmd], when_clause: Option<&ast::Expr>, ) -> Result<()> { let ctx = CrossDbCheckCtx { resolver, trigger_db_id, trigger_name, }; if let Some(when) = when_clause { ctx.check_expr(when)?; } for cmd in commands { match cmd { ast::TriggerCmd::Insert { select, .. } => { ctx.check_select(select)?; } ast::TriggerCmd::Update { sets, from, where_clause, .. } => { for set in sets { ctx.check_expr(&set.expr)?; } if let Some(from) = from { ctx.check_from_clause(from)?; } if let Some(wc) = where_clause { ctx.check_expr(wc)?; } } ast::TriggerCmd::Delete { where_clause, .. } => { if let Some(wc) = where_clause { ctx.check_expr(wc)?; } } ast::TriggerCmd::Select(select) => { ctx.check_select(select)?; } } } Ok(()) } struct CrossDbCheckCtx<'a> { resolver: &'a Resolver<'a>, trigger_db_id: usize, trigger_name: &'a str, } impl CrossDbCheckCtx<'_> { fn check_qname(&self, qn: &QualifiedName) -> Result<()> { if let Some(ref db_name) = qn.db_name { let resolved = self.resolver.resolve_database_id(qn)?; if resolved != self.trigger_db_id { bail_parse_error!( "trigger {} cannot reference objects in database {}", self.trigger_name, db_name ); } } Ok(()) } /// Check an expression tree for cross-database references. /// Descends into subqueries that walk_expr skips. /// Note: DoublyQualified expressions (e.g. aux.table.column) are NOT checked /// here — they are column references, not table references. SQLite allows them /// at CREATE TRIGGER time and only rejects them at runtime as "no such column". fn check_expr(&self, expr: &ast::Expr) -> Result<()> { use crate::translate::expr::WalkControl; crate::translate::expr::walk_expr(expr, &mut |e| -> Result { match e { // walk_expr doesn't descend into subqueries, so handle them here ast::Expr::Exists(select) | ast::Expr::Subquery(select) => { self.check_select(select)?; } ast::Expr::InSelect { rhs, .. } => { self.check_select(rhs)?; } _ => {} } Ok(WalkControl::Continue) })?; Ok(()) } fn check_select(&self, select: &ast::Select) -> Result<()> { check_select_table_refs(select, &|qn| self.check_qname(qn), &|e| self.check_expr(e)) } fn check_from_clause(&self, from: &ast::FromClause) -> Result<()> { check_from_clause_refs(from, &|qn| self.check_qname(qn), &|e| self.check_expr(e)) } } fn check_select_table_refs( select: &ast::Select, check_qname: &dyn Fn(&QualifiedName) -> Result<()>, check_expr: &dyn Fn(&ast::Expr) -> Result<()>, ) -> Result<()> { // Check CTEs if let Some(with) = &select.with { for cte in &with.ctes { check_select_table_refs(&cte.select, check_qname, check_expr)?; } } check_one_select_refs(&select.body.select, check_qname, check_expr)?; for compound in &select.body.compounds { check_one_select_refs(&compound.select, check_qname, check_expr)?; } // Check ORDER BY / LIMIT / OFFSET expressions for col in &select.order_by { check_expr(&col.expr)?; } if let Some(limit) = &select.limit { check_expr(&limit.expr)?; if let Some(offset) = &limit.offset { check_expr(offset)?; } } Ok(()) } fn check_one_select_refs( one_select: &ast::OneSelect, check_qname: &dyn Fn(&QualifiedName) -> Result<()>, check_expr: &dyn Fn(&ast::Expr) -> Result<()>, ) -> Result<()> { match one_select { ast::OneSelect::Select { columns, from, where_clause, group_by, .. } => { for col in columns { if let ast::ResultColumn::Expr(expr, _) = col { check_expr(expr)?; } } if let Some(from) = from { check_from_clause_refs(from, check_qname, check_expr)?; } if let Some(wc) = where_clause { check_expr(wc)?; } if let Some(gb) = group_by { for expr in &gb.exprs { check_expr(expr)?; } if let Some(having) = &gb.having { check_expr(having)?; } } } ast::OneSelect::Values(rows) => { for row in rows { for expr in row { check_expr(expr)?; } } } } Ok(()) } fn check_from_clause_refs( from: &ast::FromClause, check_qname: &dyn Fn(&QualifiedName) -> Result<()>, check_expr: &dyn Fn(&ast::Expr) -> Result<()>, ) -> Result<()> { check_select_table_ref(&from.select, check_qname, check_expr)?; for join in &from.joins { check_select_table_ref(&join.table, check_qname, check_expr)?; if let Some(ast::JoinConstraint::On(expr)) = &join.constraint { check_expr(expr)?; } } Ok(()) } fn check_select_table_ref( table: &ast::SelectTable, check_qname: &dyn Fn(&QualifiedName) -> Result<()>, check_expr: &dyn Fn(&ast::Expr) -> Result<()>, ) -> Result<()> { match table { ast::SelectTable::Table(qname, ..) => { check_qname(qname)?; } ast::SelectTable::TableCall(qname, args, _) => { check_qname(qname)?; for arg in args { check_expr(arg)?; } } ast::SelectTable::Select(select, _) => { check_select_table_refs(select, check_qname, check_expr)?; } ast::SelectTable::Sub(from, _) => { check_from_clause_refs(from, check_qname, check_expr)?; } } Ok(()) } /// Translate DROP TRIGGER statement pub fn translate_drop_trigger( resolver: &Resolver, trigger_name: &ast::QualifiedName, if_exists: bool, program: &mut ProgramBuilder, ) -> Result<()> { let database_id = resolver.resolve_database_id(trigger_name)?; if crate::is_attached_db(database_id) { let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version); program.begin_write_on_database(database_id, schema_cookie); } program.begin_write_operation(); let normalized_trigger_name = normalize_ident(trigger_name.name.as_str()); // Check if trigger exists if resolver.with_schema(database_id, |s| { s.get_trigger(&normalized_trigger_name).is_none() }) { if if_exists { return Ok(()); } bail_parse_error!("no such trigger: {}", normalized_trigger_name); } let opts = ProgramBuilderOpts { num_cursors: 1, approx_num_insns: 30, approx_num_labels: 1, }; program.extend(&opts); // Open cursor to sqlite_schema table (structure is the same for all databases) let table = resolver.with_schema(0, |s| s.get_btree_table(SQLITE_TABLEID).unwrap()); let sqlite_schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(table)); program.emit_insn(Insn::OpenWrite { cursor_id: sqlite_schema_cursor_id, root_page: 1i64.into(), db: database_id, }); let search_loop_label = program.allocate_label(); let skip_non_trigger_label = program.allocate_label(); let done_label = program.allocate_label(); let rewind_done_label = program.allocate_label(); // Find and delete the trigger from sqlite_schema program.emit_insn(Insn::Rewind { cursor_id: sqlite_schema_cursor_id, pc_if_empty: rewind_done_label, }); program.preassign_label_to_next_insn(search_loop_label); // Check if this is the trigger we're looking for // sqlite_schema columns: type, name, tbl_name, rootpage, sql // Column 0: type (should be "trigger") // Column 1: name (should match trigger_name) let type_reg = program.alloc_register(); let name_reg = program.alloc_register(); program.emit_insn(Insn::Column { cursor_id: sqlite_schema_cursor_id, column: 0, dest: type_reg, default: None, }); program.emit_insn(Insn::Column { cursor_id: sqlite_schema_cursor_id, column: 1, dest: name_reg, default: None, }); // Check if type == "trigger" let type_str_reg = program.emit_string8_new_reg("trigger".to_string()); program.emit_insn(Insn::Ne { lhs: type_reg, rhs: type_str_reg, target_pc: skip_non_trigger_label, flags: crate::vdbe::insn::CmpInsFlags::default(), collation: program.curr_collation(), }); // Check if name matches let trigger_name_str_reg = program.emit_string8_new_reg(normalized_trigger_name.clone()); program.emit_insn(Insn::Ne { lhs: name_reg, rhs: trigger_name_str_reg, target_pc: skip_non_trigger_label, flags: crate::vdbe::insn::CmpInsFlags::default(), collation: program.curr_collation(), }); // Found it! Delete the row program.emit_insn(Insn::Delete { cursor_id: sqlite_schema_cursor_id, table_name: SQLITE_TABLEID.to_string(), is_part_of_update: false, }); program.emit_insn(Insn::Goto { target_pc: done_label, }); program.preassign_label_to_next_insn(skip_non_trigger_label); // Continue to next row program.emit_insn(Insn::Next { cursor_id: sqlite_schema_cursor_id, pc_if_next: search_loop_label, }); program.preassign_label_to_next_insn(done_label); program.preassign_label_to_next_insn(rewind_done_label); // Update schema version let schema_version = resolver.with_schema(database_id, |s| s.schema_version); program.emit_insn(Insn::SetCookie { db: database_id, cookie: Cookie::SchemaVersion, value: (schema_version + 1) as i32, p5: 0, }); program.emit_insn(Insn::DropTrigger { db: database_id, trigger_name: normalized_trigger_name, }); Ok(()) } ================================================ FILE: core/translate/trigger_exec.rs ================================================ use crate::schema::{BTreeTable, Trigger}; use crate::sync::Arc; use crate::translate::expr::WalkControl; use crate::translate::subquery::{ emit_non_from_clause_subquery, plan_subqueries_from_trigger_when_clause, }; use crate::translate::{ emitter::Resolver, expr::{self, translate_expr, walk_expr_mut}, planner::ROWID_STRS, translate_inner, ProgramBuilder, ProgramBuilderOpts, }; use crate::util::normalize_ident; use crate::vdbe::affinity::Affinity; use crate::vdbe::insn::Insn; use crate::vdbe::BranchOffset; use crate::HashSet; use crate::{bail_parse_error, QueryMode, Result}; use std::num::NonZero; use turso_parser::ast::{self, Expr, TriggerEvent, TriggerTime}; /// Context for trigger execution #[derive(Debug)] pub struct TriggerContext { /// Table the trigger is attached to pub table: Arc, /// NEW row registers (for INSERT/UPDATE). The last element is always the rowid. pub new_registers: Option>, /// OLD row registers (for UPDATE/DELETE). The last element is always the rowid. pub old_registers: Option>, /// Override conflict resolution for statements within this trigger. /// When set, all INSERT/UPDATE statements in the trigger will use this /// conflict resolution instead of their specified OR clause. /// This is needed for UPSERT DO UPDATE triggers where SQLite requires /// that nested OR IGNORE/REPLACE clauses do not suppress errors. pub override_conflict: Option, /// Whether NEW registers contain encoded custom type values that need decoding. /// True for AFTER triggers (values have been encoded for storage). /// False for BEFORE triggers (values are still user-facing). pub new_encoded: bool, } impl TriggerContext { pub fn new( table: Arc, new_registers: Option>, old_registers: Option>, ) -> Self { Self { table, new_registers, old_registers, override_conflict: None, new_encoded: false, } } /// Create a trigger context for AFTER triggers where NEW values are encoded. pub fn new_after( table: Arc, new_registers: Option>, old_registers: Option>, ) -> Self { Self { table, new_registers, old_registers, override_conflict: None, new_encoded: true, } } /// Create a trigger context with a conflict resolution override. /// Used for UPSERT DO UPDATE triggers where nested OR IGNORE/REPLACE /// clauses should not suppress errors. pub fn new_with_override_conflict( table: Arc, new_registers: Option>, old_registers: Option>, override_conflict: ast::ResolveType, ) -> Self { Self { table, new_registers, old_registers, override_conflict: Some(override_conflict), new_encoded: false, } } /// Create a trigger context with a conflict resolution override for AFTER triggers. pub fn new_after_with_override_conflict( table: Arc, new_registers: Option>, old_registers: Option>, override_conflict: ast::ResolveType, ) -> Self { Self { table, new_registers, old_registers, override_conflict: Some(override_conflict), new_encoded: true, } } } #[derive(Debug)] struct ParamMap(Vec>); impl ParamMap { pub fn len(&self) -> usize { self.0.len() } } /// Context for compiling trigger subprograms - maps NEW/OLD to parameter indices #[derive(Debug)] struct TriggerSubprogramContext { /// Map from column index to parameter index for NEW values (1-indexed) new_param_map: Option, /// Map from column index to parameter index for OLD values (1-indexed) old_param_map: Option, table: Arc, /// Override conflict resolution for statements within this trigger. override_conflict: Option, /// Database name for the trigger's database (used to qualify unqualified table names in body) db_name: Option, } fn variable_from_parameter_index(index: NonZero) -> Expr { Expr::Variable(ast::Variable::indexed( u32::try_from(index.get()) .ok() .and_then(std::num::NonZeroU32::new) .expect("trigger parameter index must fit into NonZeroU32"), )) } impl TriggerSubprogramContext { pub fn get_new_param(&self, idx: usize) -> Option> { self.new_param_map .as_ref() .and_then(|map| map.0.get(idx).copied()) } pub fn get_new_rowid_param(&self) -> Option> { self.new_param_map .as_ref() .and_then(|map| map.0.last().copied()) } pub fn get_old_param(&self, idx: usize) -> Option> { self.old_param_map .as_ref() .and_then(|map| map.0.get(idx).copied()) } pub fn get_old_rowid_param(&self) -> Option> { self.old_param_map .as_ref() .and_then(|map| map.0.last().copied()) } } /// Rewrite NEW and OLD references in trigger expressions to use Variable instructions (parameters) fn rewrite_trigger_expr_for_subprogram( expr: &mut ast::Expr, ctx: &TriggerSubprogramContext, ) -> Result<()> { walk_expr_mut(expr, &mut |e: &mut ast::Expr| -> Result { rewrite_trigger_expr_single_for_subprogram(e, ctx)?; Ok(WalkControl::Continue) })?; Ok(()) } /// Rewrite NEW/OLD references in all expressions within an Upsert clause for subprogram fn rewrite_upsert_exprs_for_subprogram( upsert: &mut Option>, ctx: &TriggerSubprogramContext, ) -> Result<()> { let mut current = upsert.as_mut(); while let Some(u) = current { if let ast::UpsertDo::Set { ref mut sets, ref mut where_clause, } = u.do_clause { for set in sets.iter_mut() { rewrite_trigger_expr_for_subprogram(&mut set.expr, ctx)?; } if let Some(ref mut wc) = where_clause { rewrite_trigger_expr_for_subprogram(wc, ctx)?; } } if let Some(ref mut idx) = u.index { if let Some(ref mut wc) = idx.where_clause { rewrite_trigger_expr_for_subprogram(wc, ctx)?; } } current = u.next.as_mut(); } Ok(()) } /// Convert TriggerCmd to Stmt, rewriting NEW/OLD to Variable expressions (for subprogram compilation) fn trigger_cmd_to_stmt_for_subprogram( cmd: &ast::TriggerCmd, subprogram_ctx: &TriggerSubprogramContext, ) -> Result { use ast::{InsertBody, QualifiedName}; match cmd { ast::TriggerCmd::Insert { or_conflict, tbl_name, col_names, select, upsert, returning, } => { // Rewrite NEW/OLD references in the SELECT let mut select_clone = select.clone(); rewrite_expressions_in_select_for_subprogram(&mut select_clone, subprogram_ctx)?; // Rewrite NEW/OLD references in the UPSERT clause (if present) let mut upsert_clone = upsert.clone(); rewrite_upsert_exprs_for_subprogram(&mut upsert_clone, subprogram_ctx)?; let body = InsertBody::Select(select_clone, upsert_clone); // If override_conflict is set (e.g., in UPSERT DO UPDATE context), // use it instead of the command's or_conflict to ensure errors propagate. let effective_or_conflict = subprogram_ctx.override_conflict.or(*or_conflict); Ok(ast::Stmt::Insert { with: None, or_conflict: effective_or_conflict, tbl_name: QualifiedName { db_name: subprogram_ctx.db_name.clone(), name: tbl_name.clone(), alias: None, }, columns: col_names.clone(), body, returning: returning.clone(), }) } ast::TriggerCmd::Update { or_conflict, tbl_name, sets, from, where_clause, } => { // Rewrite NEW/OLD references in SET clauses and WHERE clause let mut sets_clone = sets.clone(); for set in &mut sets_clone { rewrite_trigger_expr_for_subprogram(&mut set.expr, subprogram_ctx)?; } let mut where_clause_clone = where_clause.clone(); if let Some(ref mut where_expr) = where_clause_clone { rewrite_trigger_expr_for_subprogram(where_expr, subprogram_ctx)?; } // If override_conflict is set (e.g., in UPSERT DO UPDATE context), // use it instead of the command's or_conflict to ensure errors propagate. let effective_or_conflict = subprogram_ctx.override_conflict.or(*or_conflict); Ok(ast::Stmt::Update(ast::Update { with: None, or_conflict: effective_or_conflict, tbl_name: QualifiedName { db_name: subprogram_ctx.db_name.clone(), name: tbl_name.clone(), alias: None, }, indexed: None, sets: sets_clone, from: from.clone(), where_clause: where_clause_clone, returning: vec![], order_by: vec![], limit: None, })) } ast::TriggerCmd::Delete { tbl_name, where_clause, } => { // Rewrite NEW/OLD references in WHERE clause let mut where_clause_clone = where_clause.clone(); if let Some(ref mut where_expr) = where_clause_clone { rewrite_trigger_expr_for_subprogram(where_expr, subprogram_ctx)?; } Ok(ast::Stmt::Delete { tbl_name: QualifiedName { db_name: subprogram_ctx.db_name.clone(), name: tbl_name.clone(), alias: None, }, where_clause: where_clause_clone, limit: None, returning: vec![], indexed: None, order_by: vec![], with: None, }) } ast::TriggerCmd::Select(select) => { // Rewrite NEW/OLD references in the SELECT let mut select_clone = select.clone(); rewrite_expressions_in_select_for_subprogram(&mut select_clone, subprogram_ctx)?; Ok(ast::Stmt::Select(select_clone)) } } } /// Rewrite NEW/OLD references in all expressions within a SELECT statement for subprogram fn rewrite_expressions_in_select_for_subprogram( select: &mut ast::Select, ctx: &TriggerSubprogramContext, ) -> Result<()> { rewrite_select_expressions(select, &mut |e: &mut ast::Expr| { rewrite_trigger_expr_single_for_subprogram(e, ctx) }) } /// Rewrite a single NEW/OLD reference for subprogram (called from walk_expr_mut) fn rewrite_trigger_expr_single_for_subprogram( e: &mut ast::Expr, ctx: &TriggerSubprogramContext, ) -> Result<()> { match e { Expr::Exists(select) | Expr::Subquery(select) => { rewrite_expressions_in_select_for_subprogram(select, ctx)?; return Ok(()); } Expr::InSelect { rhs, .. } => { rewrite_expressions_in_select_for_subprogram(rhs, ctx)?; return Ok(()); } Expr::Qualified(ns, col) | Expr::DoublyQualified(_, ns, col) => { let ns = normalize_ident(ns.as_str()); let col = normalize_ident(col.as_str()); // Handle NEW.column references if ns.eq_ignore_ascii_case("new") { if let Some(new_params) = &ctx.new_param_map { if let Some((idx, col_def)) = ctx.table.get_column(&col) { if col_def.is_rowid_alias() { *e = variable_from_parameter_index( ctx.get_new_rowid_param() .expect("NEW parameters must be provided"), ); return Ok(()); } if idx < new_params.len() { *e = variable_from_parameter_index( ctx.get_new_param(idx) .expect("NEW parameters must be provided"), ); return Ok(()); } else { crate::bail_parse_error!("no such column in NEW: {}", col); } } // Handle NEW.rowid if ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(&col)) { *e = variable_from_parameter_index( ctx.get_new_rowid_param() .expect("NEW parameters must be provided"), ); return Ok(()); } bail_parse_error!("no such column in NEW: {}", col); } else { bail_parse_error!( "NEW references are only valid in INSERT and UPDATE triggers" ); } } // Handle OLD.column references if ns.eq_ignore_ascii_case("old") { if let Some(old_params) = &ctx.old_param_map { if let Some((idx, col_def)) = ctx.table.get_column(&col) { if col_def.is_rowid_alias() { *e = variable_from_parameter_index( ctx.get_old_rowid_param() .expect("OLD parameters must be provided"), ); return Ok(()); } if idx < old_params.len() { *e = variable_from_parameter_index( ctx.get_old_param(idx) .expect("OLD parameters must be provided"), ); return Ok(()); } else { crate::bail_parse_error!("no such column in OLD: {}", col) } } // Handle OLD.rowid if ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(&col)) { *e = variable_from_parameter_index( ctx.get_old_rowid_param() .expect("OLD parameters must be provided"), ); return Ok(()); } bail_parse_error!("no such column in OLD: {}", col); } else { bail_parse_error!( "OLD references are only valid in UPDATE and DELETE triggers" ); } } // If the namespace is neither NEW nor OLD, this can be a regular // table-qualified reference inside the SELECT statement of the // trigger subprogram. Leave it untouched so the normal SELECT // binding/resolution phase can handle it. return Ok(()); } _ => {} } Ok(()) } /// Execute trigger commands by compiling them as a subprogram and emitting Program instruction /// Returns true if there are triggers that will fire. fn execute_trigger_commands( program: &mut ProgramBuilder, resolver: &mut Resolver, trigger: &Arc, ctx: &TriggerContext, connection: &Arc, database_id: usize, ignore_jump_target: BranchOffset, ) -> Result { if connection.trigger_is_compiling(trigger) { // Do not recursively compile the same trigger return Ok(false); } connection.start_trigger_compilation(trigger.clone()); // Build parameter mapping: parameters are 1-indexed and sequential // Order: [NEW values..., OLD values..., rowid] // So if we have 2 NEW columns, 2 OLD columns: NEW params are 1,2; OLD params are 3,4; rowid is 5 let num_new = ctx.new_registers.as_ref().map(|r| r.len()).unwrap_or(0); let new_param_map = ctx .new_registers .as_ref() .map(|new_regs| { (1..=new_regs.len()) .map(|i| NonZero::new(i).unwrap()) .collect() }) .map(ParamMap); let old_param_map = ctx .old_registers .as_ref() .map(|old_regs| { (1..=old_regs.len()) .map(|i| NonZero::new(i + num_new).unwrap()) .collect() }) .map(ParamMap); // For triggers on attached databases, resolve the database name so unqualified // table references in the trigger body are correctly qualified to the trigger's database. let db_name = if database_id == crate::MAIN_DB_ID { None } else { resolver .get_database_name_by_index(database_id) .map(ast::Name::exact) }; let subprogram_ctx = TriggerSubprogramContext { new_param_map, old_param_map, table: ctx.table.clone(), override_conflict: ctx.override_conflict, db_name, }; let mut subprogram_builder = ProgramBuilder::new_for_trigger( QueryMode::Normal, program.capture_data_changes_info().clone(), ProgramBuilderOpts { num_cursors: 1, approx_num_insns: 32, approx_num_labels: 2, }, trigger.clone(), ); // If we have an override_conflict (e.g. from UPSERT DO UPDATE context), // propagate it to the subprogram so that nested trigger firing will also use it. if let Some(override_conflict) = ctx.override_conflict { subprogram_builder.set_trigger_conflict_override(override_conflict); } // Restrict table resolution to the trigger's database during subprogram compilation. let prev_trigger_context = resolver.trigger_context.clone(); resolver.set_trigger_context(database_id, trigger.name.clone()); let compile_result = (|| -> Result<()> { for command in trigger.commands.iter() { let stmt = trigger_cmd_to_stmt_for_subprogram(command, &subprogram_ctx)?; subprogram_builder.prologue(); translate_inner( stmt, resolver, &mut subprogram_builder, connection, "trigger subprogram", )?; } Ok(()) })(); // Restore previous trigger context (supports nested triggers). resolver.trigger_context = prev_trigger_context; compile_result?; subprogram_builder.epilogue(resolver.schema()); let built_subprogram = subprogram_builder.build(connection.clone(), true, "trigger subprogram")?; let mut params = Vec::with_capacity( ctx.new_registers.as_ref().map(|r| r.len()).unwrap_or(0) + ctx.old_registers.as_ref().map(|r| r.len()).unwrap_or(0), ); if let Some(new_regs) = &ctx.new_registers { params.extend( new_regs .iter() .copied() .map(|reg_idx| crate::types::Value::from_i64(reg_idx as i64)), ); } if let Some(old_regs) = &ctx.old_registers { params.extend( old_regs .iter() .copied() .map(|reg_idx| crate::types::Value::from_i64(reg_idx as i64)), ); } program.emit_insn(Insn::Program { params, program: built_subprogram.prepared().clone(), ignore_jump_target, }); connection.end_trigger_compilation(); Ok(true) } /// Check if there are any triggers for a given event (regardless of time). /// This is used during plan preparation to determine if materialization is needed. pub fn has_relevant_triggers_type_only( schema: &crate::schema::Schema, event: TriggerEvent, updated_column_indices: Option<&HashSet>, table: &BTreeTable, ) -> bool { let mut triggers = schema.get_triggers_for_table(table.name.as_str()); // Filter triggers by event triggers.any(|trigger| { // Check event matches let event_matches = match (&trigger.event, &event) { (TriggerEvent::Delete, TriggerEvent::Delete) => true, (TriggerEvent::Insert, TriggerEvent::Insert) => true, (TriggerEvent::Update, TriggerEvent::Update) => true, (TriggerEvent::UpdateOf(trigger_cols), TriggerEvent::Update) => { // For UPDATE OF, we need to check if any of the specified columns // are in the UPDATE SET clause let updated_cols = updated_column_indices.expect("UPDATE should contain some updated columns"); // Check if any of the trigger's specified columns are being updated trigger_cols.iter().any(|col_name| { let normalized_col = normalize_ident(col_name.as_str()); if let Some((col_idx, _)) = table.get_column(&normalized_col) { updated_cols.contains(&col_idx) } else { // Column doesn't exist - according to SQLite docs, unrecognized // column names in UPDATE OF are silently ignored false } }) } _ => false, }; event_matches }) } /// Check if there are any triggers for a given event (regardless of time). /// This is used during plan preparation to determine if materialization is needed. pub fn get_relevant_triggers_type_and_time<'a>( schema: &'a crate::schema::Schema, event: TriggerEvent, time: TriggerTime, updated_column_indices: Option>, table: &'a BTreeTable, ) -> impl Iterator> + 'a + Clone { let triggers = schema.get_triggers_for_table(table.name.as_str()); // Filter triggers by event triggers .filter(move |trigger| -> bool { // Check event matches let event_matches = match (&trigger.event, &event) { (TriggerEvent::Delete, TriggerEvent::Delete) => true, (TriggerEvent::Insert, TriggerEvent::Insert) => true, (TriggerEvent::Update, TriggerEvent::Update) => true, (TriggerEvent::UpdateOf(trigger_cols), TriggerEvent::Update) => { // For UPDATE OF, we need to check if any of the specified columns // are in the UPDATE SET clause if let Some(ref updated_cols) = updated_column_indices { // Check if any of the trigger's specified columns are being updated trigger_cols.iter().any(|col_name| { let normalized_col = normalize_ident(col_name.as_str()); if let Some((col_idx, _)) = table.get_column(&normalized_col) { updated_cols.contains(&col_idx) } else { // Column doesn't exist - according to SQLite docs, unrecognized // column names in UPDATE OF are silently ignored false } }) } else { false } } _ => false, }; if !event_matches { return false; } trigger.time == time }) .cloned() } pub fn fire_trigger( program: &mut ProgramBuilder, resolver: &mut Resolver, trigger: Arc, ctx: &TriggerContext, connection: &Arc, database_id: usize, ignore_jump_target: BranchOffset, ) -> Result<()> { // Decode custom type registers so trigger bodies see user-facing values, // not raw encoded blobs from disk. // - OLD registers always come from cursor reads → always encoded → always decode // - NEW registers are only encoded for AFTER triggers (post-encode) → decode when new_encoded let decoded_ctx = decode_trigger_registers(program, resolver, ctx)?; // Apply column affinity to copies of NEW values so that both WHEN clauses // and trigger bodies see affinity-applied values (e.g., integer 42 becomes // real 42.0 for REAL columns). SQLite applies affinity before trigger // evaluation via OP_Affinity + OP_Copy before OP_Program. let affinity_ctx = apply_new_column_affinity(program, &decoded_ctx)?; let ctx = &affinity_ctx; let saved_register_affinities = std::mem::take(&mut resolver.register_affinities); populate_trigger_register_affinities(resolver, ctx); let result = (|| -> Result<()> { // Evaluate WHEN clause if present if let Some(mut when_expr) = trigger.when_clause.clone() { // Rewrite NEW/OLD references in WHEN clause to use registers rewrite_trigger_expr_for_when_clause(&mut when_expr, &ctx.table, ctx)?; // Plan and emit any subqueries in the WHEN clause (e.g. IN (SELECT ...), EXISTS, scalar subqueries). // This transforms InSelect/Exists/Subquery nodes into SubqueryResult nodes that translate_expr can handle. let mut subqueries = Vec::new(); plan_subqueries_from_trigger_when_clause( program, &mut subqueries, &mut when_expr, resolver, connection, )?; // Emit the planned subqueries so their results are available when we evaluate the WHEN expression. // Always treat these as correlated (no `Once` caching) because the WHEN clause is evaluated // per-row, and trigger bodies may modify the tables referenced by the subquery between evaluations. for subquery in &mut subqueries { let plan = subquery.consume_plan(crate::translate::plan::EvalAt::BeforeLoop); emit_non_from_clause_subquery( program, resolver, *plan, &subquery.query_type, true, // always re-evaluate: trigger WHEN is checked per-row false, )?; } let when_reg = program.alloc_register(); translate_expr(program, None, &when_expr, when_reg, resolver)?; let skip_label = program.allocate_label(); program.emit_insn(Insn::IfNot { reg: when_reg, jump_if_null: true, target_pc: skip_label, }); // Execute trigger commands if WHEN clause is true execute_trigger_commands( program, resolver, &trigger, ctx, connection, database_id, ignore_jump_target, )?; program.preassign_label_to_next_insn(skip_label); } else { // No WHEN clause - always execute execute_trigger_commands( program, resolver, &trigger, ctx, connection, database_id, ignore_jump_target, )?; } Ok(()) })(); resolver.register_affinities = saved_register_affinities; result } /// Decode encoded custom type registers in a TriggerContext. /// OLD registers are always decoded (they always come from cursor reads on disk). /// NEW registers are decoded only when `ctx.new_encoded` is true (AFTER triggers). fn decode_trigger_registers( program: &mut ProgramBuilder, resolver: &Resolver, ctx: &TriggerContext, ) -> Result { if !ctx.table.is_strict { // Non-STRICT tables never have custom type encoding return Ok(TriggerContext { table: ctx.table.clone(), new_registers: ctx.new_registers.clone(), old_registers: ctx.old_registers.clone(), override_conflict: ctx.override_conflict, new_encoded: false, }); } let columns = &ctx.table.columns; let decoded_new = if ctx.new_encoded { if let Some(new_regs) = &ctx.new_registers { let rowid_reg = *new_regs.last().expect("NEW registers must include rowid"); Some(expr::emit_trigger_decode_registers( program, resolver, columns, &|i| new_regs[i], rowid_reg, true, // is_strict )?) } else { None } } else { ctx.new_registers.clone() }; let decoded_old = if let Some(old_regs) = &ctx.old_registers { let rowid_reg = *old_regs.last().expect("OLD registers must include rowid"); Some(expr::emit_trigger_decode_registers( program, resolver, columns, &|i| old_regs[i], rowid_reg, true, // is_strict )?) } else { None }; Ok(TriggerContext { table: ctx.table.clone(), new_registers: decoded_new, old_registers: decoded_old, override_conflict: ctx.override_conflict, new_encoded: false, // decoded now }) } /// Apply column affinity to copies of NEW registers for non-strict tables. /// This ensures both WHEN clauses and trigger bodies see affinity-applied values /// (e.g., integer 42 becomes real 42.0 for REAL columns), matching SQLite's behavior. fn apply_new_column_affinity( program: &mut ProgramBuilder, ctx: &TriggerContext, ) -> Result { let new_registers = if let Some(new_regs) = &ctx.new_registers { let num_cols = ctx.table.columns.len(); if !ctx.table.is_strict && num_cols > 0 { let affinities: String = ctx .table .columns .iter() .map(|c| c.affinity().aff_mask()) .collect(); if affinities.chars().any(|c| c != Affinity::Blob.aff_mask()) { let temp_start = program.alloc_registers(num_cols); for (i, ®) in new_regs.iter().take(num_cols).enumerate() { program.emit_insn(Insn::Copy { src_reg: reg, dst_reg: temp_start + i, extra_amount: 0, }); } if let Ok(count) = std::num::NonZeroUsize::try_from(num_cols) { program.emit_insn(Insn::Affinity { start_reg: temp_start, count, affinities, }); } let mut regs: Vec = (temp_start..temp_start + num_cols).collect(); // Preserve the rowid register (always last in the NEW registers list) if new_regs.len() > num_cols { regs.push(*new_regs.last().unwrap()); } Some(regs) } else { Some(new_regs.clone()) } } else { Some(new_regs.clone()) } } else { None }; Ok(TriggerContext { table: ctx.table.clone(), new_registers, old_registers: ctx.old_registers.clone(), override_conflict: ctx.override_conflict, new_encoded: ctx.new_encoded, }) } fn populate_trigger_register_affinities(resolver: &mut Resolver, ctx: &TriggerContext) { populate_trigger_row_register_affinities(resolver, &ctx.table, ctx.new_registers.as_deref()); populate_trigger_row_register_affinities(resolver, &ctx.table, ctx.old_registers.as_deref()); } fn populate_trigger_row_register_affinities( resolver: &mut Resolver, table: &BTreeTable, row_registers: Option<&[usize]>, ) { let Some(registers) = row_registers else { return; }; for (idx, column) in table.columns.iter().enumerate() { let affinity = if column.is_rowid_alias() { Affinity::Integer } else { column.affinity_with_strict(table.is_strict) }; if let Some(®ister) = registers.get(idx) { resolver.register_affinities.insert(register, affinity); } } if let Some(&rowid_register) = registers.last() { resolver .register_affinities .insert(rowid_register, Affinity::Integer); } } /// Rewrite NEW/OLD references in WHEN clause expressions (uses Register expressions, not Variable) fn rewrite_trigger_expr_for_when_clause( expr: &mut ast::Expr, table: &BTreeTable, ctx: &TriggerContext, ) -> Result<()> { walk_expr_mut(expr, &mut |e: &mut ast::Expr| -> Result { rewrite_trigger_expr_single_for_when_clause(e, table, ctx, false)?; Ok(WalkControl::Continue) })?; Ok(()) } /// Rewrite NEW/OLD references in all expressions within a SELECT statement for trigger WHEN clauses. fn rewrite_expressions_in_select_for_when_clause( select: &mut ast::Select, table: &BTreeTable, ctx: &TriggerContext, ) -> Result<()> { rewrite_select_expressions(select, &mut |e: &mut ast::Expr| { rewrite_trigger_expr_single_for_when_clause(e, table, ctx, true) }) } /// Rewrite all expressions in a SELECT tree, including CTEs, compounds, ORDER BY, /// LIMIT/OFFSET, FROM/JOIN subqueries, and window clauses. fn rewrite_select_expressions(select: &mut ast::Select, rewrite_expr: &mut F) -> Result<()> where F: FnMut(&mut ast::Expr) -> Result<()>, { // Rewrite WITH clause (CTEs) if let Some(with_clause) = &mut select.with { for cte in &mut with_clause.ctes { rewrite_select_expressions(&mut cte.select, rewrite_expr)?; } } rewrite_one_select_expressions(&mut select.body.select, rewrite_expr)?; // Rewrite compound SELECT arms (UNION/EXCEPT/INTERSECT) for compound in &mut select.body.compounds { rewrite_one_select_expressions(&mut compound.select, rewrite_expr)?; } // Rewrite top-level ORDER BY for sorted_col in &mut select.order_by { rewrite_expression_tree(&mut sorted_col.expr, rewrite_expr)?; } // Rewrite top-level LIMIT/OFFSET if let Some(limit) = &mut select.limit { rewrite_expression_tree(&mut limit.expr, rewrite_expr)?; if let Some(offset) = &mut limit.offset { rewrite_expression_tree(offset, rewrite_expr)?; } } Ok(()) } fn rewrite_one_select_expressions( one_select: &mut ast::OneSelect, rewrite_expr: &mut F, ) -> Result<()> where F: FnMut(&mut ast::Expr) -> Result<()>, { match one_select { ast::OneSelect::Select { columns, from, where_clause, group_by, window_clause, .. } => { for col in columns { if let ast::ResultColumn::Expr(expr, _) = col { rewrite_expression_tree(expr, rewrite_expr)?; } } if let Some(from_clause) = from { rewrite_from_clause_expressions(from_clause, rewrite_expr)?; } if let Some(where_expr) = where_clause { rewrite_expression_tree(where_expr, rewrite_expr)?; } if let Some(group_by) = group_by { for expr in &mut group_by.exprs { rewrite_expression_tree(expr, rewrite_expr)?; } if let Some(having_expr) = &mut group_by.having { rewrite_expression_tree(having_expr, rewrite_expr)?; } } for window_def in window_clause { rewrite_window_expressions(&mut window_def.window, rewrite_expr)?; } } ast::OneSelect::Values(values) => { for row in values { for expr in row { rewrite_expression_tree(expr, rewrite_expr)?; } } } } Ok(()) } fn rewrite_from_clause_expressions( from_clause: &mut ast::FromClause, rewrite_expr: &mut F, ) -> Result<()> where F: FnMut(&mut ast::Expr) -> Result<()>, { rewrite_select_table_expressions(&mut from_clause.select, rewrite_expr)?; for join in &mut from_clause.joins { rewrite_select_table_expressions(&mut join.table, rewrite_expr)?; if let Some(ast::JoinConstraint::On(expr)) = &mut join.constraint { rewrite_expression_tree(expr, rewrite_expr)?; } } Ok(()) } fn rewrite_select_table_expressions( select_table: &mut ast::SelectTable, rewrite_expr: &mut F, ) -> Result<()> where F: FnMut(&mut ast::Expr) -> Result<()>, { match select_table { ast::SelectTable::Table(..) => {} ast::SelectTable::TableCall(_, args, _) => { for arg in args { rewrite_expression_tree(arg, rewrite_expr)?; } } ast::SelectTable::Select(select, _) => { rewrite_select_expressions(select, rewrite_expr)?; } ast::SelectTable::Sub(from_clause, _) => { rewrite_from_clause_expressions(from_clause, rewrite_expr)?; } } Ok(()) } fn rewrite_window_expressions(window: &mut ast::Window, rewrite_expr: &mut F) -> Result<()> where F: FnMut(&mut ast::Expr) -> Result<()>, { for expr in &mut window.partition_by { rewrite_expression_tree(expr, rewrite_expr)?; } for sorted_col in &mut window.order_by { rewrite_expression_tree(&mut sorted_col.expr, rewrite_expr)?; } if let Some(frame_clause) = &mut window.frame_clause { rewrite_frame_bound_expressions(&mut frame_clause.start, rewrite_expr)?; if let Some(end) = &mut frame_clause.end { rewrite_frame_bound_expressions(end, rewrite_expr)?; } } Ok(()) } fn rewrite_frame_bound_expressions( frame_bound: &mut ast::FrameBound, rewrite_expr: &mut F, ) -> Result<()> where F: FnMut(&mut ast::Expr) -> Result<()>, { match frame_bound { ast::FrameBound::Following(expr) | ast::FrameBound::Preceding(expr) => { rewrite_expression_tree(expr, rewrite_expr)?; } ast::FrameBound::CurrentRow | ast::FrameBound::UnboundedFollowing | ast::FrameBound::UnboundedPreceding => {} } Ok(()) } fn rewrite_expression_tree(expr: &mut ast::Expr, rewrite_expr: &mut F) -> Result<()> where F: FnMut(&mut ast::Expr) -> Result<()>, { walk_expr_mut( expr, &mut |e: &mut ast::Expr| -> Result { rewrite_expr(e)?; Ok(WalkControl::Continue) }, )?; Ok(()) } fn rewrite_trigger_expr_single_for_when_clause( expr: &mut ast::Expr, table: &BTreeTable, ctx: &TriggerContext, allow_non_trigger_qualified: bool, ) -> Result<()> { match expr { // Bare column references are not valid in trigger WHEN clauses. // Per SQLite docs, columns must be qualified with NEW or OLD. Expr::Id(name) if !allow_non_trigger_qualified => { let ident = normalize_ident(name.as_str()); if table.get_column(&ident).is_some() || ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(&ident)) { crate::bail_parse_error!("no such column: {}", ident); } return Ok(()); } Expr::Exists(select) | Expr::Subquery(select) => { rewrite_expressions_in_select_for_when_clause(select, table, ctx)?; return Ok(()); } Expr::InSelect { rhs, .. } => { rewrite_expressions_in_select_for_when_clause(rhs, table, ctx)?; return Ok(()); } Expr::Qualified(ns, col) | Expr::DoublyQualified(_, ns, col) => { let ns = normalize_ident(ns.as_str()); let col = normalize_ident(col.as_str()); // Handle NEW.column references if ns.eq_ignore_ascii_case("new") { if let Some(new_regs) = &ctx.new_registers { if let Some((idx, col_def)) = table.get_column(&col) { if col_def.is_rowid_alias() { // Rowid alias columns map to the rowid register (last element) *expr = Expr::Register( *new_regs.last().expect("NEW registers must be provided"), ); return Ok(()); } if idx < new_regs.len() { *expr = Expr::Register(new_regs[idx]); return Ok(()); } } // Handle NEW.rowid if ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(&col)) { *expr = Expr::Register( *ctx.new_registers .as_ref() .expect("NEW registers must be provided") .last() .expect("NEW registers must be provided"), ); return Ok(()); } bail_parse_error!("no such column in NEW: {}", col); } else { bail_parse_error!( "NEW references are only valid in INSERT and UPDATE triggers" ); } } // Handle OLD.column references if ns.eq_ignore_ascii_case("old") { if let Some(old_regs) = &ctx.old_registers { if let Some((idx, _)) = table.get_column(&col) { if idx < old_regs.len() { *expr = Expr::Register(old_regs[idx]); return Ok(()); } } // Handle OLD.rowid if ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(&col)) { *expr = Expr::Register( *ctx.old_registers .as_ref() .expect("OLD registers must be provided") .last() .expect("OLD registers must be provided"), ); return Ok(()); } bail_parse_error!("no such column in OLD: {}", col); } else { bail_parse_error!( "OLD references are only valid in UPDATE and DELETE triggers" ); } } if !allow_non_trigger_qualified { bail_parse_error!("no such column: {ns}.{col}"); } } _ => {} } Ok(()) } ================================================ FILE: core/translate/update.rs ================================================ use crate::sync::Arc; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use crate::schema::ROWID_SENTINEL; use crate::translate::emitter::Resolver; use crate::translate::expr::{bind_and_rewrite_expr, BindingBehavior}; use crate::translate::expression_index::expression_index_column_usage; use crate::translate::plan::{Operation, Scan}; use crate::translate::planner::{parse_limit, ROWID_STRS}; use crate::{ bail_parse_error, schema::{Schema, Table}, util::normalize_ident, vdbe::builder::{ProgramBuilder, ProgramBuilderOpts}, CaptureDataChangesExt, Connection, }; use turso_parser::ast::{self, Expr, SortOrder}; use super::emitter::emit_program; use super::expr::process_returning_clause; use super::optimizer::optimize_plan; use super::plan::{ ColumnUsedMask, DmlSafety, IterationDirection, JoinedTable, Plan, TableReferences, UpdatePlan, }; use super::planner::{parse_where, plan_ctes_as_outer_refs}; use super::subquery::{ plan_subqueries_from_returning, plan_subqueries_from_select_plan, plan_subqueries_from_set_clauses, plan_subqueries_from_where_clause, }; /* * Update is simple. By default we scan the table, and for each row, we check the WHERE * clause. If it evaluates to true, we build the new record with the updated value and insert. * * EXAMPLE: * sqlite> explain update t set a = 100 where b = 5; addr opcode p1 p2 p3 p4 p5 comment ---- ------------- ---- ---- ---- ------------- -- ------------- 0 Init 0 16 0 0 Start at 16 1 Null 0 1 2 0 r[1..2]=NULL 2 Noop 1 0 1 0 3 OpenWrite 0 2 0 3 0 root=2 iDb=0; t 4 Rewind 0 15 0 0 5 Column 0 1 6 0 r[6]= cursor 0 column 1 6 Ne 7 14 6 BINARY-8 81 if r[6]!=r[7] goto 14 7 Rowid 0 2 0 0 r[2]= rowid of 0 8 IsNull 2 15 0 0 if r[2]==NULL goto 15 9 Integer 100 3 0 0 r[3]=100 10 Column 0 1 4 0 r[4]= cursor 0 column 1 11 Column 0 2 5 0 r[5]= cursor 0 column 2 12 MakeRecord 3 3 1 0 r[1]=mkrec(r[3..5]) 13 Insert 0 1 2 t 7 intkey=r[2] data=r[1] 14 Next 0 5 0 1 15 Halt 0 0 0 0 16 Transaction 0 1 1 0 1 usesStmtJournal=0 17 Integer 5 7 0 0 r[7]=5 18 Goto 0 1 0 0 */ pub fn translate_update( body: ast::Update, resolver: &Resolver, program: &mut ProgramBuilder, connection: &Arc, ) -> crate::Result<()> { let mut plan = prepare_update_plan(program, resolver, body, connection, false)?; // Plan subqueries in the WHERE clause and SET clause if let Plan::Update(ref mut update_plan) = plan { if let Some(ref mut ephemeral_plan) = update_plan.ephemeral_plan { // When using ephemeral plan (key columns are being updated), subqueries are in the ephemeral_plan's WHERE plan_subqueries_from_select_plan(program, ephemeral_plan, resolver, connection)?; } else { // Normal path: subqueries are in the UPDATE plan's WHERE plan_subqueries_from_where_clause( program, &mut update_plan.non_from_clause_subqueries, &mut update_plan.table_references, &mut update_plan.where_clause, resolver, connection, )?; } // Plan subqueries in the SET clause (e.g. UPDATE t SET col = (SELECT ...)) plan_subqueries_from_set_clauses( program, &mut update_plan.non_from_clause_subqueries, &mut update_plan.table_references, &mut update_plan.set_clauses, resolver, connection, )?; } optimize_plan(program, &mut plan, resolver)?; if let Plan::Update(ref update_plan) = plan { super::stmt_journal::set_update_stmt_journal_flags( program, update_plan, resolver, connection, )?; } let opts = ProgramBuilderOpts { num_cursors: 1, approx_num_insns: 20, approx_num_labels: 4, }; program.extend(&opts); emit_program(connection, resolver, program, plan, |_| {})?; Ok(()) } pub fn translate_update_for_schema_change( body: ast::Update, resolver: &Resolver, program: &mut ProgramBuilder, connection: &Arc, ddl_query: &str, after: impl FnOnce(&mut ProgramBuilder), ) -> crate::Result<()> { let mut plan = prepare_update_plan(program, resolver, body, connection, true)?; if let Plan::Update(update_plan) = &mut plan { if program.capture_data_changes_info().has_updates() { update_plan.cdc_update_alter_statement = Some(ddl_query.to_string()); } // Plan subqueries in the WHERE clause if let Some(ref mut ephemeral_plan) = update_plan.ephemeral_plan { plan_subqueries_from_select_plan(program, ephemeral_plan, resolver, connection)?; } else { plan_subqueries_from_where_clause( program, &mut update_plan.non_from_clause_subqueries, &mut update_plan.table_references, &mut update_plan.where_clause, resolver, connection, )?; } // Plan subqueries in the SET clause (e.g. UPDATE t SET col = (SELECT ...)) plan_subqueries_from_set_clauses( program, &mut update_plan.non_from_clause_subqueries, &mut update_plan.table_references, &mut update_plan.set_clauses, resolver, connection, )?; } optimize_plan(program, &mut plan, resolver)?; let opts = ProgramBuilderOpts { num_cursors: 1, approx_num_insns: 20, approx_num_labels: 4, }; program.extend(&opts); emit_program(connection, resolver, program, plan, after)?; Ok(()) } fn validate_update( schema: &Schema, body: &ast::Update, table_name: &str, is_internal_schema_change: bool, conn: &Arc, ) -> crate::Result<()> { // Check if this is a system table that should be protected from direct writes if !is_internal_schema_change && !conn.is_nested_stmt() && !conn.is_mvcc_bootstrap_connection() && !crate::schema::can_write_to_table(table_name) { crate::bail_parse_error!("table {} may not be modified", table_name); } if body.from.is_some() { bail_parse_error!("FROM clause is not supported in UPDATE"); } if !body.order_by.is_empty() { bail_parse_error!("ORDER BY is not supported in UPDATE"); } // Check if this is a materialized view if schema.is_materialized_view(table_name) { bail_parse_error!("cannot modify materialized view {}", table_name); } // Check if this table has any incompatible dependent views let incompatible_views = schema.has_incompatible_dependent_views(table_name); if !incompatible_views.is_empty() { use crate::incremental::compiler::DBSP_CIRCUIT_VERSION; bail_parse_error!( "Cannot UPDATE table '{}' because it has incompatible dependent materialized view(s): {}. \n\ These views were created with a different DBSP version than the current version ({}). \n\ Please DROP and recreate the view(s) before modifying this table.", table_name, incompatible_views.join(", "), DBSP_CIRCUIT_VERSION ); } Ok(()) } pub fn prepare_update_plan( program: &mut ProgramBuilder, resolver: &Resolver, mut body: ast::Update, connection: &Arc, is_internal_schema_change: bool, ) -> crate::Result { let database_id = resolver.resolve_database_id(&body.tbl_name)?; let schema = resolver.schema(); let table_name = &body.tbl_name.name; let table = match resolver.with_schema(database_id, |s| s.get_table(table_name.as_str())) { Some(table) => table, None => bail_parse_error!("Parse error: no such table: {}", table_name), }; if program.trigger.is_some() && table.virtual_table().is_some() { bail_parse_error!( "unsafe use of virtual table \"{}\"", body.tbl_name.name.as_str() ); } if crate::is_attached_db(database_id) { let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version); program.begin_write_on_database(database_id, schema_cookie); } validate_update( schema, &body, table_name.as_str(), is_internal_schema_change, connection, )?; // Extract WITH, OR conflict clause, and INDEXED BY before borrowing body mutably let with = body.with.take(); let or_conflict = body.or_conflict.take(); let indexed = body.indexed.take(); let table_name = table.get_name(); let iter_dir = body .order_by .first() .and_then(|ob| { ob.order.map(|o| match o { SortOrder::Asc => IterationDirection::Forwards, SortOrder::Desc => IterationDirection::Backwards, }) }) .unwrap_or(IterationDirection::Forwards); let joined_tables = vec![JoinedTable { table: match table.as_ref() { Table::Virtual(vtab) => Table::Virtual(vtab.clone()), Table::BTree(btree_table) => Table::BTree(btree_table.clone()), _ => unreachable!(), }, identifier: body.tbl_name.alias.as_ref().map_or_else( || table_name.to_string(), |alias| alias.as_str().to_string(), ), internal_id: program.table_reference_counter.next(), op: build_scan_op(&table, iter_dir), join_info: None, col_used_mask: ColumnUsedMask::default(), column_use_counts: Vec::new(), expression_index_usages: Vec::new(), database_id, indexed, }]; let mut table_references = TableReferences::new(joined_tables, vec![]); // Plan CTEs and add them as outer query references for subquery resolution plan_ctes_as_outer_refs(with, resolver, program, &mut table_references, connection)?; let column_lookup: HashMap = table .columns() .iter() .enumerate() .filter_map(|(i, col)| col.name.as_ref().map(|name| (name.to_lowercase(), i))) .collect(); let mut set_clauses: Vec<(usize, Box)> = Vec::with_capacity(body.sets.len()); // Process each SET assignment and map column names to expressions // e.g the statement `SET x = 1, y = 2, z = 3` has 3 set assigments for set in &mut body.sets { bind_and_rewrite_expr( &mut set.expr, Some(&mut table_references), None, resolver, BindingBehavior::ResultColumnsNotAllowed, )?; let values = match set.expr.as_ref() { Expr::Parenthesized(vals) => vals.clone(), expr => vec![expr.clone().into()], }; if set.col_names.len() != values.len() { bail_parse_error!( "{} columns assigned {} values", set.col_names.len(), values.len() ); } for (col_name, expr) in set.col_names.iter().zip(values.iter()) { let ident = normalize_ident(col_name.as_str()); let col_index = match column_lookup.get(&ident) { Some(idx) => *idx, None => { // Check if this is the 'rowid' keyword if ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(&ident)) { // Find the rowid alias column if it exists if let Some((idx, _col)) = table .columns() .iter() .enumerate() .find(|(_i, c)| c.is_rowid_alias()) { // Use the rowid alias column index match set_clauses.iter_mut().find(|(i, _)| i == &idx) { Some((_, existing_expr)) => existing_expr.clone_from(expr), None => set_clauses.push((idx, expr.clone())), } idx } else { // No rowid alias, use sentinel value for actual rowid match set_clauses.iter_mut().find(|(i, _)| *i == ROWID_SENTINEL) { Some((_, existing_expr)) => existing_expr.clone_from(expr), None => set_clauses.push((ROWID_SENTINEL, expr.clone())), } ROWID_SENTINEL } } else { crate::bail_parse_error!("no such column: {}.{}", table_name, col_name); } } }; match set_clauses.iter_mut().find(|(idx, _)| *idx == col_index) { Some((_, existing_expr)) => { // When multiple SET col[n] = val for the same column are desugared, // compose them: replace the column reference in the new expression // with the existing expression, so // col = array_set_element(col, 0, 'X') then col = array_set_element(col, 2, 'Z') // becomes col = array_set_element(array_set_element(col, 0, 'X'), 2, 'Z') if let Expr::FunctionCall { name, args: new_args, .. } = expr.as_ref() { if name.as_str().eq_ignore_ascii_case("array_set_element") && new_args.len() == 3 { let mut composed_args = new_args.clone(); composed_args[0].clone_from(existing_expr); *existing_expr = Box::new(Expr::FunctionCall { name: name.clone(), distinctness: None, args: composed_args, order_by: vec![], filter_over: turso_parser::ast::FunctionTail { filter_clause: None, over_clause: None, }, }); } else { existing_expr.clone_from(expr); } } else { existing_expr.clone_from(expr); } } None => set_clauses.push((col_index, expr.clone())), } } } // Plan subqueries in RETURNING expressions before processing // (so SubqueryResult nodes are cloned into result_columns) let mut non_from_clause_subqueries = vec![]; plan_subqueries_from_returning( program, &mut non_from_clause_subqueries, &mut table_references, &mut body.returning, resolver, connection, )?; let result_columns = process_returning_clause(&mut body.returning, &mut table_references, resolver)?; let order_by = body .order_by .iter_mut() .map(|o| { let _ = bind_and_rewrite_expr( &mut o.expr, Some(&mut table_references), Some(&result_columns), resolver, BindingBehavior::ResultColumnsNotAllowed, ); (o.expr.clone(), o.order.unwrap_or(SortOrder::Asc)) }) .collect(); // Sqlite determines we should create an ephemeral table if we do not have a FROM clause // Difficult to say what items from the plan can be checked for this so currently just checking if a RowId Alias is referenced // https://github.com/sqlite/sqlite/blob/master/src/update.c#L395 // https://github.com/sqlite/sqlite/blob/master/src/update.c#L670 let columns = table.columns(); let mut where_clause = vec![]; // Parse the WHERE clause parse_where( body.where_clause.as_deref(), &mut table_references, Some(&result_columns), &mut where_clause, resolver, )?; // Parse the LIMIT/OFFSET clause let (limit, offset) = body .limit .map_or(Ok((None, None)), |l| parse_limit(l, resolver))?; // Check what indexes will need to be updated by checking set_clauses and see // if a column is contained in an index. let indexes: Vec<_> = resolver.with_schema(database_id, |s| { s.get_indices(table_name).cloned().collect() }); let updated_cols: HashSet = set_clauses.iter().map(|(i, _)| *i).collect(); let rowid_alias_used = set_clauses .iter() .any(|(idx, _)| *idx == ROWID_SENTINEL || columns[*idx].is_rowid_alias()); let target_table_ref = table_references .joined_tables() .first() .expect("UPDATE must have a target table reference"); let indexes_to_update = if rowid_alias_used { // If the rowid alias is used in the SET clause, we need to update all indexes indexes } else { // otherwise we need to update the indexes whose columns are set in the SET clause, // or if the columns used in the partial index WHERE clause are being updated. let mut indexes_to_update = Vec::new(); for idx in indexes { let mut needs = false; for col in idx.columns.iter() { if let Some(expr) = col.expr.as_ref() { let cols_used = expression_index_column_usage(expr.as_ref(), target_table_ref, resolver)?; if cols_used.iter().any(|cidx| updated_cols.contains(&cidx)) { needs = true; break; } } else if updated_cols.contains(&col.pos_in_table) { needs = true; break; } } if !needs { if let Some(where_expr) = &idx.where_clause { let cols_used = expression_index_column_usage( where_expr.as_ref(), target_table_ref, resolver, )?; // If any column used in the partial index WHERE clause is being updated, // this index must be updated as well. needs = cols_used.iter().any(|cidx| updated_cols.contains(&cidx)); } } if needs { indexes_to_update.push(idx); } } indexes_to_update }; Ok(Plan::Update(UpdatePlan { table_references, or_conflict, set_clauses, where_clause, returning: if result_columns.is_empty() { None } else { Some(result_columns) }, order_by, limit, offset, contains_constant_false_condition: false, indexes_to_update, ephemeral_plan: None, cdc_update_alter_statement: None, non_from_clause_subqueries, safety: DmlSafety::default(), })) } fn build_scan_op(table: &Table, iter_dir: IterationDirection) -> Operation { match table { Table::BTree(_) => Operation::Scan(Scan::BTreeTable { iter_dir, index: None, }), Table::Virtual(_) => Operation::default_scan_for(table), _ => unreachable!(), } } ================================================ FILE: core/translate/upsert.rs ================================================ use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::num::NonZeroUsize; use std::sync::Arc; use turso_parser::ast::{self, TriggerEvent, TriggerTime, Upsert}; use crate::error::SQLITE_CONSTRAINT_PRIMARYKEY; use crate::schema::{BTreeTable, IndexColumn, ROWID_SENTINEL}; use crate::translate::emitter::{emit_check_constraints, UpdateRowSource}; use crate::translate::expr::{walk_expr, WalkControl}; use crate::translate::fkeys::{ emit_fk_child_update_counters, emit_parent_key_change_checks, fire_fk_update_actions, }; use crate::translate::insert::{format_unique_violation_desc, InsertEmitCtx}; use crate::translate::planner::ROWID_STRS; use crate::translate::trigger_exec::{ fire_trigger, get_relevant_triggers_type_and_time, TriggerContext, }; use crate::vdbe::insn::{to_u16, CmpInsFlags}; use crate::{ bail_parse_error, error::SQLITE_CONSTRAINT_NOTNULL, schema::{Index, Schema, Table}, translate::{ emitter::{ emit_cdc_full_record, emit_cdc_insns, emit_cdc_patch_record, OperationMode, Resolver, }, expr::{ emit_returning_results, translate_expr, translate_expr_no_constant_opt, walk_expr_mut, NoConstantOptReason, }, insert::Insertion, plan::{ResultSetColumn, TableReferences}, }, util::{exprs_are_equivalent, normalize_ident}, vdbe::{ affinity::Affinity, builder::ProgramBuilder, insn::{IdxInsertFlags, InsertFlags, Insn}, }, }; use crate::{CaptureDataChangesExt, Connection}; // The following comment is copied directly from SQLite source and should be used as a guiding light // whenever we encounter compatibility bugs related to conflict clause handling: /* UNIQUE and PRIMARY KEY constraints should be handled in the following ** order: ** ** (1) OE_Update ** (2) OE_Abort, OE_Fail, OE_Rollback, OE_Ignore ** (3) OE_Replace ** ** OE_Fail and OE_Ignore must happen before any changes are made. ** OE_Update guarantees that only a single row will change, so it ** must happen before OE_Replace. Technically, OE_Abort and OE_Rollback ** could happen in any order, but they are grouped up front for ** convenience. ** ** 2018-08-14: Ticket https://www.sqlite.org/src/info/908f001483982c43 ** The order of constraints used to have OE_Update as (2) and OE_Abort ** and so forth as (1). But apparently PostgreSQL checks the OE_Update ** constraint before any others, so it had to be moved. ** ** Constraint checking code is generated in this order: ** (A) The rowid constraint ** (B) Unique index constraints that do not have OE_Replace as their ** default conflict resolution strategy ** (C) Unique index that do use OE_Replace by default. ** ** The ordering of (2) and (3) is accomplished by making sure the linked ** list of indexes attached to a table puts all OE_Replace indexes last ** in the list. See sqlite3CreateIndex() for where that happens. */ /// A ConflictTarget is extracted from each ON CONFLICT target, // e.g. INSERT INTO x(a) ON CONFLICT *(a COLLATE nocase)* #[derive(Debug, Clone)] pub struct ConflictTarget { /// The normalized column name in question col_name: String, /// Possible collation name, normalized to lowercase collate: Option, } // Extract `(column, optional_collate)` from an ON CONFLICT target Expr. // Accepts: Id, Qualified, DoublyQualified, Parenthesized, Collate fn extract_target_key(e: &ast::Expr) -> Option { match e { ast::Expr::Collate(inner, c) => { let mut tk = extract_target_key(inner.as_ref())?; let cstr = c.as_str(); tk.collate = Some(cstr.to_ascii_lowercase()); Some(tk) } ast::Expr::Parenthesized(v) if v.len() == 1 => extract_target_key(&v[0]), ast::Expr::Id(name) => Some(ConflictTarget { col_name: normalize_ident(name.as_str()), collate: None, }), // t.a or db.t.a: accept ident or quoted in the column position ast::Expr::Qualified(_, col) | ast::Expr::DoublyQualified(_, _, col) => { let cname = col.as_str(); Some(ConflictTarget { col_name: normalize_ident(cname), collate: None, }) } _ => None, } } /// For an ON CONFLICT target that is an expression (not a simple column), /// extract the inner expression and an optional COLLATE annotation. /// E.g. `lower(val) COLLATE nocase` -> (lower(val), Some("nocase")) fn extract_target_expr(e: &ast::Expr) -> (&ast::Expr, Option) { match e { ast::Expr::Collate(inner, c) => { let (expr, _) = extract_target_expr(inner.as_ref()); (expr, Some(c.as_str().to_ascii_lowercase())) } ast::Expr::Parenthesized(v) if v.len() == 1 => extract_target_expr(&v[0]), _ => (e, None), } } // Return the index key’s effective collation. // If `idx_col.collation` is None, fall back to the column default or "BINARY". fn effective_collation_for_index_col(idx_col: &IndexColumn, table: &Table) -> String { if let Some(c) = idx_col.collation.as_ref() { return c.to_string().to_ascii_lowercase(); } // Otherwise use the table default, or default to BINARY table .get_column_by_name(&idx_col.name) .map(|s| s.1.collation().to_string()) .unwrap_or_else(|| "binary".to_string()) } /// Match ON CONFLICT target to the PRIMARY KEY/rowid alias. pub fn upsert_matches_rowid_alias(upsert: &Upsert, table: &Table) -> bool { let Some(t) = upsert.index.as_ref() else { // omitted target matches everything, CatchAll handled elsewhere return false; }; if t.targets.len() != 1 { return false; } // Only treat as PK if the PK is the rowid alias (INTEGER PRIMARY KEY) let pk = table.columns().iter().find(|c| c.is_rowid_alias()); if let Some(pkcol) = pk { extract_target_key(&t.targets[0].expr).is_some_and(|tk| { tk.col_name .eq_ignore_ascii_case(pkcol.name.as_ref().unwrap_or(&String::new())) }) } else { false } } /// Returns array of chaned column indicies and whether rowid was changed. fn collect_changed_cols( table: &Table, set_pairs: &[(usize, Box)], ) -> (HashSet, bool) { let mut cols_changed = HashSet::with_capacity_and_hasher(table.columns().len(), Default::default()); let mut rowid_changed = false; for (col_idx, _) in set_pairs { if let Some(c) = table.columns().get(*col_idx) { if c.is_rowid_alias() { rowid_changed = true; } else { cols_changed.insert(*col_idx); } } } (cols_changed, rowid_changed) } #[inline] fn upsert_index_is_affected( table: &Table, idx: &Index, changed_cols: &HashSet, rowid_changed: bool, ) -> bool { if rowid_changed { return true; } let km: HashSet = idx .columns .iter() .filter_map(|ic| ic.expr.is_none().then_some(ic.pos_in_table)) .collect(); let pm = referenced_index_cols(idx, table); for c in km.iter().chain(pm.iter()) { if changed_cols.contains(c) { return true; } } false } /// Collect HashSet of columns referenced by the partial WHERE (empty if none), or /// by the expression of any IndexColumn on the index. fn referenced_index_cols(idx: &Index, table: &Table) -> HashSet { let mut out = HashSet::default(); if let Some(expr) = &idx.where_clause { index_expression_cols(table, &mut out, expr); } for ic in &idx.columns { if let Some(expr) = &ic.expr { index_expression_cols(table, &mut out, expr); } } out } /// Columns referenced by any expression index columns on the index. fn index_expression_cols(table: &Table, out: &mut HashSet, expr: &ast::Expr) { use ast::Expr; let _ = walk_expr(expr, &mut |e: &ast::Expr| -> crate::Result { match e { Expr::Id(n) => { if let Some((i, _)) = table.get_column_by_name(&normalize_ident(n.as_str())) { out.insert(i); } else if ROWID_STRS .iter() .any(|r| r.eq_ignore_ascii_case(n.as_str())) { if let Some(rowid_pos) = table .btree() .and_then(|t| t.get_rowid_alias_column().map(|(p, _)| p)) { out.insert(rowid_pos); } } } Expr::Qualified(ns, c) | Expr::DoublyQualified(_, ns, c) => { let nsn = normalize_ident(ns.as_str()); let tname = normalize_ident(table.get_name()); if nsn.eq_ignore_ascii_case(&tname) { if let Some((i, _)) = table.get_column_by_name(&normalize_ident(c.as_str())) { out.insert(i); } } } _ => {} } Ok(WalkControl::Continue) }); } /// Match ON CONFLICT target to a UNIQUE index, *ignoring order* but requiring /// exact coverage (same column multiset). If the target specifies a COLLATED /// column, the collation must match the index column's effective collation. /// If the target omits collation, any index collation is accepted. /// Partial (WHERE) indexes never match. pub fn upsert_matches_index(upsert: &Upsert, index: &Index, table: &Table) -> bool { let Some(target) = upsert.index.as_ref() else { return true; }; // must be a non-partial UNIQUE index with identical arity if !index.unique || index.where_clause.is_some() || target.targets.len() != index.columns.len() { return false; } // Track which index columns have been matched (consumed). let mut matched = vec![false; index.columns.len()]; for te in &target.targets { let mut found = None; if let Some(tk) = extract_target_key(&te.expr) { // Simple column reference target: match by name and collation. let tname = &tk.col_name; for (i, ic) in index.columns.iter().enumerate() { if matched[i] || ic.expr.is_some() { continue; } let iname = normalize_ident(&ic.name); let icoll = effective_collation_for_index_col(ic, table); if tname.eq_ignore_ascii_case(&iname) && match tk.collate.as_ref() { Some(c) => c.eq_ignore_ascii_case(&icoll), None => true, // unspecified collation -> accept any } { found = Some(i); break; } } } else { // Expression target (e.g. lower(val)): match against expression index // columns using semantic equivalence. let (target_expr, target_collate) = extract_target_expr(&te.expr); for (i, ic) in index.columns.iter().enumerate() { if matched[i] { continue; } if let Some(idx_expr) = &ic.expr { if exprs_are_equivalent(target_expr, idx_expr) { // If target specifies a collation, it must match the index column's. if let Some(ref tc) = target_collate { let icoll = effective_collation_for_index_col(ic, table); if !tc.eq_ignore_ascii_case(&icoll) { continue; } } found = Some(i); break; } } } } if let Some(i) = found { matched[i] = true; } else { return false; } } // All target columns matched exactly once, and all index columns consumed matched.iter().all(|&m| m) } #[derive(Clone, Debug)] pub enum ResolvedUpsertTarget { // ON CONFLICT DO CatchAll, // ON CONFLICT(pk) DO PrimaryKey, // matched this non-partial UNIQUE index Index(Arc), } pub fn resolve_upsert_target( schema: &Schema, table: &Table, upsert: &Upsert, ) -> crate::Result { // Omitted target, catch-all if upsert.index.is_none() { return Ok(ResolvedUpsertTarget::CatchAll); } // Targeted: must match PK, only if PK is a rowid alias if upsert_matches_rowid_alias(upsert, table) { return Ok(ResolvedUpsertTarget::PrimaryKey); } // Otherwise match a UNIQUE index, also covering non-rowid PRIMARY KEYs for idx in schema.get_indices(table.get_name()) { if idx.unique && upsert_matches_index(upsert, idx, table) { return Ok(ResolvedUpsertTarget::Index(Arc::clone(idx))); } } crate::bail_parse_error!( "ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint" ); } #[allow(clippy::too_many_arguments)] /// Emit the bytecode to implement the `DO UPDATE` arm of an UPSERT. /// /// This routine is entered after the caller has determined that an INSERT /// would violate a UNIQUE/PRIMARY KEY constraint and that the user requested /// `ON CONFLICT ... DO UPDATE`. /// /// High-level flow: /// 1. Seek to the conflicting row by rowid and load the current row snapshot /// into a contiguous set of registers. /// 2. Optionally duplicate CURRENT into BEFORE* (for index rebuild and CDC). /// 3. Copy CURRENT into NEW, then evaluate SET expressions into NEW, /// with all references to the target table columns rewritten to read from /// the CURRENT registers (per SQLite semantics). /// 4. Enforce NOT NULL constraints and (if STRICT) type checks on NEW. /// 5. Rebuild indexes (delete keys using BEFORE, insert keys using NEW). /// 6. Rewrite the table row payload at the same rowid with NEW. /// 7. Emit CDC rows and RETURNING output if requested. /// 8. Jump to `row_done_label`. /// /// Semantics reference: https://sqlite.org/lang_upsert.html /// Column references in the DO UPDATE expressions refer to the original /// (unchanged) row. To refer to would-be inserted values, use `excluded.x`. #[allow(clippy::too_many_arguments)] pub fn emit_upsert( program: &mut ProgramBuilder, table: &Table, ctx: &InsertEmitCtx, insertion: &Insertion, set_pairs: &mut [(usize, Box)], where_clause: &mut Option>, resolver: &mut Resolver, returning: &mut [ResultSetColumn], connection: &Arc, table_references: &mut TableReferences, ) -> crate::Result<()> { // Seek & snapshot CURRENT program.emit_insn(Insn::SeekRowid { cursor_id: ctx.cursor_id, src_reg: ctx.conflict_rowid_reg, target_pc: ctx.loop_labels.row_done, }); let num_cols = ctx.table.columns.len(); let current_start = program.alloc_registers(num_cols); for (i, col) in ctx.table.columns.iter().enumerate() { if col.is_rowid_alias() { program.emit_insn(Insn::RowId { cursor_id: ctx.cursor_id, dest: current_start + i, }); } else { program.emit_insn(Insn::Column { cursor_id: ctx.cursor_id, column: i, dest: current_start + i, default: None, }); } } // BEFORE for index maintenance / CDC let before_start = if ctx.cdc_table.is_some() || !ctx.idx_cursors.is_empty() { let s = program.alloc_registers(num_cols); program.emit_insn(Insn::Copy { src_reg: current_start, dst_reg: s, extra_amount: num_cols - 1, }); Some(s) } else { None }; // NEW = CURRENT, then apply SET let new_start = program.alloc_registers(num_cols); program.emit_insn(Insn::Copy { src_reg: current_start, dst_reg: new_start, extra_amount: num_cols - 1, }); // For STRICT tables with custom types, values loaded from disk (current_start) // are in encoded form. We need decoded copies so that: // - WHERE clause expressions see user-facing values (Bug 13) // - SET expressions referencing t1.column see user-facing values // - excluded.column references also see decoded values (Bug 7) // current_start itself stays encoded for trigger OLD registers and before_start. // After SET evaluation, we encode ALL columns in new_start before writing to disk. let (decoded_current_start, excluded_decoded_start) = if let Some(bt) = table.btree() { if bt.is_strict { // Create decoded copy of current_start for WHERE/SET expressions let decoded_current = program.alloc_registers(num_cols); program.emit_insn(Insn::Copy { src_reg: current_start, dst_reg: decoded_current, extra_amount: num_cols - 1, }); crate::translate::expr::emit_custom_type_decode_columns( program, resolver, &bt.columns, decoded_current, None, )?; // Decode new_start in-place (was copied from encoded current_start; // after SET applies decoded values, we encode ALL columns) crate::translate::expr::emit_custom_type_decode_columns( program, resolver, &bt.columns, new_start, None, )?; // Create decoded copies of excluded (insertion) registers so that // excluded.column references see user-facing values let decoded_excluded = program.alloc_registers(num_cols); program.emit_insn(Insn::Copy { src_reg: insertion.first_col_register(), dst_reg: decoded_excluded, extra_amount: num_cols - 1, }); crate::translate::expr::emit_custom_type_decode_columns( program, resolver, &bt.columns, decoded_excluded, None, )?; (Some(decoded_current), Some(decoded_excluded)) } else { (None, None) } } else { (None, None) }; // For WHERE and SET, use decoded_current_start if available (STRICT with custom types), // otherwise fall back to current_start (already decoded or non-custom-type). let expr_current_start = decoded_current_start.unwrap_or(current_start); // WHERE on target row if let Some(pred) = where_clause.as_mut() { rewrite_expr_to_registers( pred, table, expr_current_start, ctx.conflict_rowid_reg, Some(table.get_name()), Some(insertion), true, excluded_decoded_start, )?; let pr = program.alloc_register(); translate_expr(program, None, pred, pr, resolver)?; program.emit_insn(Insn::IfNot { reg: pr, target_pc: ctx.loop_labels.row_done, jump_if_null: true, }); } // Apply SET; capture rowid change if any let mut new_rowid_reg: Option = None; for (col_idx, expr) in set_pairs.iter_mut() { rewrite_expr_to_registers( expr, table, expr_current_start, ctx.conflict_rowid_reg, Some(table.get_name()), Some(insertion), true, excluded_decoded_start, )?; translate_expr_no_constant_opt( program, None, expr, new_start + *col_idx, resolver, NoConstantOptReason::RegisterReuse, )?; let col = &table.columns()[*col_idx]; if col.notnull() && !col.is_rowid_alias() { program.emit_insn(Insn::HaltIfNull { target_reg: new_start + *col_idx, err_code: SQLITE_CONSTRAINT_NOTNULL, description: String::from(table.get_name()) + "." + col.name.as_ref().unwrap(), }); } if col.is_rowid_alias() { // Must be integer; remember the NEW rowid value let r = program.alloc_register(); program.emit_insn(Insn::Copy { src_reg: new_start + *col_idx, dst_reg: r, extra_amount: 0, }); program.emit_insn(Insn::MustBeInt { reg: r }); new_rowid_reg = Some(r); } } if let Some(bt) = table.btree() { if bt.is_strict { // Pre-encode TypeCheck: all columns are decoded (user-facing) at this point. program.emit_insn(Insn::TypeCheck { start_reg: new_start, count: num_cols, check_generated: true, table_reference: BTreeTable::input_type_check_table_ref( &bt, resolver.schema(), None, ), }); // Encode ALL columns. Both non-SET columns (decoded from disk above) // and SET columns (user-facing values from expressions) need encoding // before being written to disk. crate::translate::expr::emit_custom_type_encode_columns( program, resolver, &bt.columns, new_start, None, &bt.name, )?; // Post-encode TypeCheck: validate encoded values match storage type. program.emit_insn(Insn::TypeCheck { start_reg: new_start, count: num_cols, check_generated: true, table_reference: BTreeTable::type_check_table_ref(&bt, resolver.schema()), }); } else { // For non-STRICT tables, apply column affinity to the values. // This must happen early so that both index records and the table record // use the converted values. let affinity = bt.columns.iter().map(|c| c.affinity()); // Only emit Affinity if there's meaningful affinity to apply if affinity.clone().any(|a| a != Affinity::Blob) { if let Ok(count) = std::num::NonZeroUsize::try_from(num_cols) { program.emit_insn(Insn::Affinity { start_reg: new_start, count, affinities: affinity.map(|a| a.aff_mask()).collect(), }); } } } // Evaluate CHECK constraints on the new values emit_check_constraints( program, &bt.check_constraints, resolver, &bt.name, new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg), bt.columns .iter() .enumerate() .filter_map(|(idx, col)| col.name.as_deref().map(|n| (n, new_start + idx))), connection, ast::ResolveType::Abort, ctx.loop_labels.row_done, Some(table_references), )?; } let (changed_cols, rowid_changed) = collect_changed_cols(table, set_pairs); // Fire BEFORE UPDATE triggers let upsert_database_id = ctx.database_id; let preserved_old_registers: Option> = if let Some(btree_table) = table.btree() { let updated_column_indices: HashSet = set_pairs.iter().map(|(col_idx, _)| *col_idx).collect(); let relevant_before_update_triggers: Vec<_> = resolver.with_schema(upsert_database_id, |s| { get_relevant_triggers_type_and_time( s, TriggerEvent::Update, TriggerTime::Before, Some(updated_column_indices.clone()), &btree_table, ) .collect() }); // OLD row values are in current_start registers let old_registers: Vec = (0..num_cols) .map(|i| current_start + i) .chain(std::iter::once(ctx.conflict_rowid_reg)) .collect(); if !relevant_before_update_triggers.is_empty() { // NEW row values are in new_start registers. At this point they are // encoded (post-encode for STRICT custom types). Mark new_encoded=true // so fire_trigger's decode_trigger_registers will decode them. let new_rowid_for_trigger = new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg); let new_registers: Vec = (0..num_cols) .map(|i| new_start + i) .chain(std::iter::once(new_rowid_for_trigger)) .collect(); // In UPSERT DO UPDATE context, trigger's INSERT/UPDATE OR IGNORE/REPLACE // clauses should not suppress errors. Override conflict resolution to Abort. // Use new_after variant because NEW values are encoded at this point. let trigger_ctx = TriggerContext::new_after_with_override_conflict( btree_table.clone(), Some(new_registers), Some(old_registers.clone()), ast::ResolveType::Abort, ); for trigger in relevant_before_update_triggers { fire_trigger( program, resolver, trigger, &trigger_ctx, connection, upsert_database_id, ctx.loop_labels.row_done, )?; } // BEFORE UPDATE triggers may have altered the btree, need to re-seek program.emit_insn(Insn::NotExists { cursor: ctx.cursor_id, rowid_reg: ctx.conflict_rowid_reg, target_pc: ctx.loop_labels.row_done, }); let has_relevant_after_triggers = resolver.with_schema(upsert_database_id, |s| { get_relevant_triggers_type_and_time( s, TriggerEvent::Update, TriggerTime::After, Some(updated_column_indices), &btree_table, ) .count() > 0 }); if has_relevant_after_triggers { // Preserve OLD registers for AFTER triggers let preserved: Vec = old_registers .iter() .map(|old_reg| { let preserved_reg = program.alloc_register(); program.emit_insn(Insn::Copy { src_reg: *old_reg, dst_reg: preserved_reg, extra_amount: 0, }); preserved_reg }) .collect(); Some(preserved) } else { None } } else { // Check if we need to preserve for AFTER triggers let has_relevant_after_triggers = resolver.with_schema(upsert_database_id, |s| { get_relevant_triggers_type_and_time( s, TriggerEvent::Update, TriggerTime::After, Some(updated_column_indices), &btree_table, ) .count() > 0 }); if has_relevant_after_triggers { Some(old_registers) } else { None } } } else { None }; let rowid_alias_idx = table.columns().iter().position(|c| c.is_rowid_alias()); let has_direct_rowid_update = set_pairs .iter() .any(|(idx, _)| *idx == rowid_alias_idx.unwrap_or(ROWID_SENTINEL)); let has_user_provided_rowid = if let Some(i) = rowid_alias_idx { set_pairs.iter().any(|(idx, _)| *idx == i) || has_direct_rowid_update } else { has_direct_rowid_update }; let rowid_set_clause_reg = if has_user_provided_rowid { Some(new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg)) } else { None }; if let Some(bt) = table.btree() { if connection.foreign_keys_enabled() { let rowid_new_reg = new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg); // Child-side checks if resolver.with_schema(upsert_database_id, |s| s.has_child_fks(bt.name.as_str())) { emit_fk_child_update_counters( program, &bt, table.get_name(), ctx.cursor_id, new_start, rowid_new_reg, &changed_cols, upsert_database_id, resolver, )?; } let upsert_indices: Vec<_> = resolver.with_schema(upsert_database_id, |s| { s.get_indices(table.get_name()).cloned().collect() }); emit_parent_key_change_checks( program, &bt, upsert_indices.iter().filter(|idx| { upsert_index_is_affected(table, idx, &changed_cols, rowid_changed) }), ctx.cursor_id, ctx.conflict_rowid_reg, new_start, new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg), rowid_set_clause_reg, set_pairs, upsert_database_id, resolver, )?; } } // Index rebuild (DELETE old, INSERT new), honoring partial-index WHEREs if let Some(before) = before_start { for (idx_name, _root, idx_cid) in &ctx.idx_cursors { let idx_meta = resolver .with_schema(ctx.database_id, |s| { s.get_index(table.get_name(), idx_name).cloned() }) .expect("index exists"); if !upsert_index_is_affected(table, &idx_meta, &changed_cols, rowid_changed) { continue; // skip untouched index completely } let k = idx_meta.columns.len(); let before_pred_reg = eval_partial_pred_for_row_image( program, table, &idx_meta, before, ctx.conflict_rowid_reg, resolver, ); let new_rowid = new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg); let new_pred_reg = eval_partial_pred_for_row_image( program, table, &idx_meta, new_start, new_rowid, resolver, ); // Skip delete if BEFORE predicate false/NULL let maybe_skip_del = before_pred_reg.map(|r| { let lbl = program.allocate_label(); program.emit_insn(Insn::IfNot { reg: r, target_pc: lbl, jump_if_null: true, }); lbl }); // DELETE old key let del = program.alloc_registers(k + 1); for (i, ic) in idx_meta.columns.iter().enumerate() { if let Some(expr) = &ic.expr { let mut e = expr.as_ref().clone(); rewrite_expr_to_registers( &mut e, table, before, ctx.conflict_rowid_reg, Some(table.get_name()), None, false, None, )?; translate_expr_no_constant_opt( program, None, &e, del + i, resolver, NoConstantOptReason::RegisterReuse, )?; } else { let (ci, _) = table.get_column_by_name(&ic.name).unwrap(); program.emit_insn(Insn::Copy { src_reg: before + ci, dst_reg: del + i, extra_amount: 0, }); } } program.emit_insn(Insn::Copy { src_reg: ctx.conflict_rowid_reg, dst_reg: del + k, extra_amount: 0, }); program.emit_insn(Insn::IdxDelete { start_reg: del, num_regs: k + 1, cursor_id: *idx_cid, raise_error_if_no_matching_entry: false, }); if let Some(label) = maybe_skip_del { program.resolve_label(label, program.offset()); } // Skip insert if NEW predicate false/NULL let maybe_skip_ins = new_pred_reg.map(|r| { let lbl = program.allocate_label(); program.emit_insn(Insn::IfNot { reg: r, target_pc: lbl, jump_if_null: true, }); lbl }); // INSERT new key (use NEW rowid if present) let ins = program.alloc_registers(k + 1); for (i, ic) in idx_meta.columns.iter().enumerate() { if let Some(expr) = &ic.expr { let mut e = expr.as_ref().clone(); rewrite_expr_to_registers( &mut e, table, new_start, new_rowid, Some(table.get_name()), None, false, None, )?; translate_expr_no_constant_opt( program, None, &e, ins + i, resolver, NoConstantOptReason::RegisterReuse, )?; } else { let (ci, _) = table.get_column_by_name(&ic.name).unwrap(); program.emit_insn(Insn::Copy { src_reg: new_start + ci, dst_reg: ins + i, extra_amount: 0, }); } } program.emit_insn(Insn::Copy { src_reg: new_rowid, dst_reg: ins + k, extra_amount: 0, }); let rec = program.alloc_register(); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(ins), count: to_u16(k + 1), dest_reg: to_u16(rec), index_name: Some((*idx_name).clone()), affinity_str: None, }); if idx_meta.unique { // Affinity on the key columns for the NoConflict probe let ok = program.allocate_label(); let aff: String = idx_meta .columns .iter() .map(|c| { c.expr.as_ref().map_or_else( || { table .get_column_by_name(&c.name) .map(|(_, col)| { let is_strict = table.btree().is_some_and(|btree| btree.is_strict); col.affinity_with_strict(is_strict).aff_mask() }) .unwrap_or('B') }, |_| crate::vdbe::affinity::Affinity::Blob.aff_mask(), ) }) .collect(); program.emit_insn(Insn::Affinity { start_reg: ins, count: NonZeroUsize::new(k).unwrap(), affinities: aff, }); program.emit_insn(Insn::NoConflict { cursor_id: *idx_cid, target_pc: ok, record_reg: ins, num_regs: k, }); let hit = program.alloc_register(); program.emit_insn(Insn::IdxRowId { cursor_id: *idx_cid, dest: hit, }); program.emit_insn(Insn::Eq { lhs: new_rowid, rhs: hit, target_pc: ok, flags: CmpInsFlags::default(), collation: program.curr_collation(), }); let description = format_unique_violation_desc(table.get_name(), &idx_meta); program.emit_insn(Insn::Halt { err_code: SQLITE_CONSTRAINT_PRIMARYKEY, description, on_error: None, description_reg: None, }); program.preassign_label_to_next_insn(ok); } program.emit_insn(Insn::IdxInsert { cursor_id: *idx_cid, record_reg: rec, unpacked_start: Some(ins), unpacked_count: Some((k + 1) as u16), flags: IdxInsertFlags::new().nchange(true), }); if let Some(lbl) = maybe_skip_ins { program.resolve_label(lbl, program.offset()); } } } // Build NEW table payload let rec = program.alloc_register(); let is_strict = table.btree().is_some_and(|btree| btree.is_strict); let affinity_str = table .columns() .iter() .map(|c| c.affinity_with_strict(is_strict).aff_mask()) .collect::(); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(new_start), count: to_u16(num_cols), dest_reg: to_u16(rec), index_name: None, affinity_str: Some(affinity_str), }); // If rowid changed, first ensure no other row owns it, then delete+insert if let Some(rnew) = new_rowid_reg { let ok = program.allocate_label(); // If equal to old rowid, skip uniqueness probe program.emit_insn(Insn::Eq { lhs: rnew, rhs: ctx.conflict_rowid_reg, target_pc: ok, flags: CmpInsFlags::default(), collation: program.curr_collation(), }); // If another row already has rnew -> constraint program.emit_insn(Insn::NotExists { cursor: ctx.cursor_id, rowid_reg: rnew, target_pc: ok, }); program.emit_insn(Insn::Halt { err_code: SQLITE_CONSTRAINT_PRIMARYKEY, description: format!( "{}.{}", table.get_name(), table .columns() .iter() .find(|c| c.is_rowid_alias()) .and_then(|c| c.name.as_deref()) .unwrap_or("rowid") ), on_error: None, description_reg: None, }); program.preassign_label_to_next_insn(ok); // important: the cursor was repositioned in the previous conflict check via NotExists, // so if we didn't conflict+halt above, we need to re-seek to the row under update. program.emit_insn(Insn::SeekRowid { cursor_id: ctx.cursor_id, src_reg: ctx.conflict_rowid_reg, target_pc: ctx.loop_labels.row_done, }); // Now replace the row program.emit_insn(Insn::Delete { cursor_id: ctx.cursor_id, table_name: table.get_name().to_string(), is_part_of_update: true, }); program.emit_insn(Insn::Insert { cursor: ctx.cursor_id, key_reg: rnew, record_reg: rec, flag: InsertFlags::new() .require_seek() .update_rowid_change() .skip_last_rowid(), table_name: table.get_name().to_string(), }); } else { program.emit_insn(Insn::Insert { cursor: ctx.cursor_id, key_reg: ctx.conflict_rowid_reg, record_reg: rec, flag: InsertFlags::new().skip_last_rowid(), table_name: table.get_name().to_string(), }); } // Fire FK actions (CASCADE, SET NULL, SET DEFAULT) for parent-side updates. // This must be done after the update is complete but before AFTER triggers. if let Some(bt) = table.btree() { if connection.foreign_keys_enabled() && resolver.with_schema(upsert_database_id, |s| { s.any_resolved_fks_referencing(bt.name.as_str()) }) { fire_fk_update_actions( program, resolver, bt.name.as_str(), ctx.conflict_rowid_reg, // old_rowid_reg current_start, // old_values_start new_start, // new_values_start new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg), // new_rowid_reg connection, upsert_database_id, )?; } } // emit CDC instructions if let Some((cdc_id, _)) = ctx.cdc_table { let new_rowid = new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg); if new_rowid_reg.is_some() { // DELETE (before) let before_rec = if program.capture_data_changes_info().has_before() { Some(emit_cdc_full_record( program, table.columns(), ctx.cursor_id, ctx.conflict_rowid_reg, table.btree().is_some_and(|btree| btree.is_strict), )) } else { None }; emit_cdc_insns( program, resolver, OperationMode::DELETE, cdc_id, ctx.conflict_rowid_reg, before_rec, None, None, table.get_name(), )?; // INSERT (after) let after_rec = if program.capture_data_changes_info().has_after() { Some(emit_cdc_patch_record( program, table, new_start, rec, new_rowid, )) } else { None }; emit_cdc_insns( program, resolver, OperationMode::INSERT, cdc_id, new_rowid, None, after_rec, None, table.get_name(), )?; } else { let after_rec = if program.capture_data_changes_info().has_after() { Some(emit_cdc_patch_record( program, table, new_start, rec, ctx.conflict_rowid_reg, )) } else { None }; let before_rec = if program.capture_data_changes_info().has_before() { Some(emit_cdc_full_record( program, table.columns(), ctx.cursor_id, ctx.conflict_rowid_reg, table.btree().is_some_and(|btree| btree.is_strict), )) } else { None }; emit_cdc_insns( program, resolver, OperationMode::UPDATE(UpdateRowSource::Normal), cdc_id, ctx.conflict_rowid_reg, before_rec, after_rec, None, table.get_name(), )?; } } // Fire AFTER UPDATE triggers if let (Some(btree_table), Some(old_regs)) = (table.btree(), preserved_old_registers) { let updated_column_indices: HashSet = set_pairs.iter().map(|(col_idx, _)| *col_idx).collect(); let relevant_triggers: Vec<_> = resolver.with_schema(upsert_database_id, |s| { get_relevant_triggers_type_and_time( s, TriggerEvent::Update, TriggerTime::After, Some(updated_column_indices), &btree_table, ) .collect() }); if !relevant_triggers.is_empty() { let new_rowid_for_trigger = new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg); let new_registers_after: Vec = (0..num_cols) .map(|i| new_start + i) .chain(std::iter::once(new_rowid_for_trigger)) .collect(); // In UPSERT DO UPDATE context, trigger's INSERT/UPDATE OR IGNORE/REPLACE // clauses should not suppress errors. Override conflict resolution to Abort. // NEW values are encoded at this point; fire_trigger will decode them. let trigger_ctx_after = TriggerContext::new_after_with_override_conflict( btree_table, Some(new_registers_after), Some(old_regs), ast::ResolveType::Abort, ); // RAISE(IGNORE) in an AFTER trigger should only abort the trigger body, // not skip post-row work (RETURNING). let after_trigger_done = program.allocate_label(); for trigger in relevant_triggers { fire_trigger( program, resolver, trigger, &trigger_ctx_after, connection, upsert_database_id, after_trigger_done, )?; } program.preassign_label_to_next_insn(after_trigger_done); } } // RETURNING from NEW image + final rowid if !returning.is_empty() { emit_returning_results( program, table_references, returning, new_start, new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg), resolver, ctx.returning_buffer.as_ref(), )?; } program.emit_insn(Insn::Goto { target_pc: ctx.loop_labels.row_done, }); Ok(()) } /// Normalize the `SET` clause into `(column_index, Expr)` pairs using table layout. /// /// Supports multi-target row-value SETs: `SET (a, b) = (expr1, expr2)`. /// Enforces same number of column names and RHS values. /// If the same column is assigned multiple times, the last assignment wins. pub fn collect_set_clauses_for_upsert( table: &Table, set_items: &mut [ast::Set], ) -> crate::Result)>> { let lookup: HashMap = table .columns() .iter() .enumerate() .filter_map(|(i, c)| c.name.as_ref().map(|n| (n.to_lowercase(), i))) .collect(); let mut out: Vec<(usize, Box)> = vec![]; for set in set_items { let values: Vec> = match set.expr.as_ref() { ast::Expr::Parenthesized(v) => v.clone(), e => vec![e.clone().into()], }; if set.col_names.len() != values.len() { bail_parse_error!( "{} columns assigned {} values", set.col_names.len(), values.len() ); } for (cn, e) in set.col_names.iter().zip(values.into_iter()) { let Some(idx) = lookup.get(&normalize_ident(cn.as_str())) else { bail_parse_error!("no such column: {}", cn); }; if let Some(existing) = out.iter_mut().find(|(i, _)| *i == *idx) { existing.1 = e; } else { out.push((*idx, e)); } } } Ok(out) } fn eval_partial_pred_for_row_image( prg: &mut ProgramBuilder, table: &Table, idx: &Index, row_start: usize, // base of CURRENT or NEW image rowid_reg: usize, // rowid for that image resolver: &Resolver, ) -> Option { let Some(where_expr) = &idx.where_clause else { return None; }; let mut e = where_expr.as_ref().clone(); rewrite_expr_to_registers( &mut e, table, row_start, rowid_reg, None, // table_name None, // insertion false, // dont allow EXCLUDED None, // no decoded excluded ) .ok()?; let r = prg.alloc_register(); translate_expr_no_constant_opt( prg, None, &e, r, resolver, NoConstantOptReason::RegisterReuse, ) .ok()?; Some(r) } /// Generic rewriter that maps column references to registers for a given row image. /// /// - Id/Qualified refs to the *target table* (when `table_name` is provided) resolve /// to the CURRENT/NEW row image starting at `base_start`, with `rowid` (or the /// rowid-alias) mapped to `rowid_reg`. /// - If `allow_excluded` and `insertion` are provided, `EXCLUDED.x` resolves to the /// insertion registers (and `EXCLUDED.rowid` resolves to `insertion.key_register()`). /// When `excluded_decoded_start` is provided, excluded column references resolve to /// decoded registers at `excluded_decoded_start + col_idx` instead of the raw /// (encoded) insertion registers. This prevents double-encoding in UPSERT. /// - If `table_name` is `None`, qualified refs never match /// - Leaves names from other tables/namespaces untouched. #[allow(clippy::too_many_arguments)] fn rewrite_expr_to_registers( e: &mut ast::Expr, table: &Table, base_start: usize, rowid_reg: usize, table_name: Option<&str>, insertion: Option<&Insertion>, allow_excluded: bool, excluded_decoded_start: Option, ) -> crate::Result { use ast::Expr; let table_name_norm = table_name.map(normalize_ident); // Map a column name to a register within the row image at `base_start`. let col_reg_from_row_image = |name: &str| -> Option { if ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(name)) { return Some(rowid_reg); } let (idx, c) = table.get_column_by_name(name)?; if c.is_rowid_alias() { Some(rowid_reg) } else { Some(base_start + idx) } }; walk_expr_mut( e, &mut |expr: &mut ast::Expr| -> crate::Result { match expr { Expr::Qualified(ns, c) | Expr::DoublyQualified(_, ns, c) => { let ns = normalize_ident(ns.as_str()); let c = normalize_ident(c.as_str()); // Handle EXCLUDED.* if enabled if allow_excluded && ns.eq_ignore_ascii_case("excluded") { if let Some(ins) = insertion { if ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(&c)) { *expr = Expr::Register(ins.key_register()); } else if let Some(cm) = ins.get_col_mapping_by_name(&c) { // Use decoded excluded registers when available // to prevent double-encoding of custom type values if let Some(decoded_start) = excluded_decoded_start { let (col_idx, _) = table.get_column_by_name(&c).expect("column exists"); *expr = Expr::Register(decoded_start + col_idx); } else { *expr = Expr::Register(cm.register); } } else { bail_parse_error!("no such column in EXCLUDED: {}", c); } } // If insertion is None, leave EXCLUDED.* untouched. return Ok(WalkControl::Continue); } // Match the target table namespace if provided if let Some(ref tn) = table_name_norm { if ns.eq_ignore_ascii_case(tn) { if let Some(r) = col_reg_from_row_image(&c) { *expr = Expr::Register(r); } else { bail_parse_error!("no such column: {}.{}", ns, c); } return Ok(WalkControl::Continue); } } // In UPSERT DO UPDATE context (allow_excluded=true), a qualified // reference that doesn't match the target table or EXCLUDED is // invalid. Return a graceful error instead of leaving it // unresolved (which would panic later in translate_expr). if allow_excluded { bail_parse_error!("no such column: {}.{}", ns, c); } } // Unqualified id -> row image (CURRENT/NEW depending on caller) Expr::Id(name) => { if let Some(r) = col_reg_from_row_image(&normalize_ident(name.as_str())) { *expr = Expr::Register(r); } } _ => {} } Ok(WalkControl::Continue) }, ) } ================================================ FILE: core/translate/vacuum.rs ================================================ //! Translation of VACUUM statements to VDBE bytecode. use crate::vdbe::builder::ProgramBuilder; use crate::vdbe::insn::Insn; use crate::{bail_parse_error, Result}; use turso_parser::ast::{Expr, Literal, Name}; /// Translate a VACUUM statement into VDBE bytecode. /// /// Currently only VACUUM INTO is supported. Plain VACUUM (which compacts /// the database in place) is not yet implemented. /// /// # Arguments /// * `program` - The program builder to emit instructions to /// * `schema_name` - Optional schema/database name (not yet supported) /// * `into` - Optional destination path for VACUUM INTO /// /// # Returns /// The modified program builder on success pub fn translate_vacuum( program: &mut ProgramBuilder, schema_name: Option<&Name>, into: Option<&Expr>, ) -> Result<()> { // Schema name support (for attached databases) is not yet implemented if schema_name.is_some() { bail_parse_error!("VACUUM with schema name is not supported yet"); } match into { Some(dest_expr) => { // VACUUM INTO 'path' - create compacted copy at destination let dest_path = extract_path_from_expr(dest_expr)?; program.emit_insn(Insn::VacuumInto { dest_path }); Ok(()) } None => { // Plain VACUUM - not yet supported bail_parse_error!( "VACUUM is not supported yet. Use VACUUM INTO 'filename' to create a compacted copy." ); } } } /// Extract a file path string from an expression. /// /// The expression can be either: /// - A string literal: `VACUUM INTO 'path/to/file.db'` /// - An identifier (variable name, though not commonly used) fn extract_path_from_expr(expr: &Expr) -> Result { match expr { Expr::Literal(Literal::String(s)) => { // Remove surrounding quotes if present let path = s.trim_matches('\'').trim_matches('"'); if path.is_empty() { bail_parse_error!("VACUUM INTO path cannot be empty"); } Ok(path.to_string()) } Expr::Id(name) => { // Allow identifier as path (unusual but valid) Ok(name.as_str().to_string()) } _ => { bail_parse_error!("VACUUM INTO requires a string literal path"); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_extract_path_from_string_literal() { let expr = Expr::Literal(Literal::String("'test.db'".to_string())); let path = extract_path_from_expr(&expr).unwrap(); assert_eq!(path, "test.db"); } #[test] fn test_extract_path_from_string_literal_double_quotes() { let expr = Expr::Literal(Literal::String("\"test.db\"".to_string())); let path = extract_path_from_expr(&expr).unwrap(); assert_eq!(path, "test.db"); } #[test] fn test_extract_path_from_identifier() { let expr = Expr::Id(Name::exact("myfile".to_string())); let path = extract_path_from_expr(&expr).unwrap(); assert_eq!(path, "myfile"); } #[test] fn test_extract_path_empty_fails() { let expr = Expr::Literal(Literal::String("''".to_string())); assert!(extract_path_from_expr(&expr).is_err()); } } ================================================ FILE: core/translate/values.rs ================================================ use crate::translate::emitter::TranslateCtx; use crate::translate::expr::{translate_expr_no_constant_opt, NoConstantOptReason}; use crate::translate::plan::{QueryDestination, SelectPlan}; use crate::translate::result_row::emit_offset; use crate::turso_assert_eq; use crate::vdbe::builder::ProgramBuilder; use crate::vdbe::insn::{to_u16, IdxInsertFlags, InsertFlags, Insn}; use crate::vdbe::BranchOffset; use crate::Result; pub fn emit_values( program: &mut ProgramBuilder, plan: &SelectPlan, t_ctx: &mut TranslateCtx, ) -> Result { if plan.values.len() == 1 { let start_reg = emit_values_when_single_row(program, plan, t_ctx)?; return Ok(start_reg); } let reg_result_cols_start = match plan.query_destination { QueryDestination::ResultRows => emit_toplevel_values(program, plan, t_ctx)?, QueryDestination::CoroutineYield { yield_reg, .. } => { emit_values_in_subquery(program, plan, t_ctx, yield_reg)? } QueryDestination::EphemeralIndex { .. } => emit_toplevel_values(program, plan, t_ctx)?, QueryDestination::EphemeralTable { .. } => emit_toplevel_values(program, plan, t_ctx)?, QueryDestination::ExistsSubqueryResult { result_reg } => { program.emit_insn(Insn::Integer { value: 1, dest: result_reg, }); result_reg } QueryDestination::RowValueSubqueryResult { .. } => { emit_toplevel_values(program, plan, t_ctx)? } QueryDestination::RowSet { .. } => { unreachable!("RowSet query destination should not be used in values emission") } QueryDestination::Unset => unreachable!("Unset query destination should not be reached"), }; Ok(reg_result_cols_start) } fn emit_values_when_single_row( program: &mut ProgramBuilder, plan: &SelectPlan, t_ctx: &mut TranslateCtx, ) -> Result { let end_label = program.allocate_label(); emit_offset(program, end_label, t_ctx.reg_offset); let first_row = &plan.values[0]; let row_len = first_row.len(); let start_reg = if let Some(reg) = t_ctx.reg_result_cols_start { program.reg_result_cols_start = Some(reg); reg } else { let reg = program.alloc_registers(row_len); t_ctx.reg_result_cols_start = Some(reg); program.reg_result_cols_start = Some(reg); reg }; for (i, v) in first_row.iter().enumerate() { translate_expr_no_constant_opt( program, Some(&plan.table_references), v, start_reg + i, &t_ctx.resolver, NoConstantOptReason::RegisterReuse, )?; } emit_values_to_destination(program, plan, t_ctx, start_reg, row_len, end_label)?; program.preassign_label_to_next_insn(end_label); Ok(start_reg) } fn emit_toplevel_values( program: &mut ProgramBuilder, plan: &SelectPlan, t_ctx: &mut TranslateCtx, ) -> Result { let yield_reg = program.alloc_register(); let definition_label = program.allocate_label(); let start_offset_label = program.allocate_label(); program.emit_insn(Insn::InitCoroutine { yield_reg, jump_on_definition: definition_label, start_offset: start_offset_label, }); program.preassign_label_to_next_insn(start_offset_label); let start_reg = emit_values_in_subquery(program, plan, t_ctx, yield_reg)?; program.emit_insn(Insn::EndCoroutine { yield_reg }); program.preassign_label_to_next_insn(definition_label); program.emit_insn(Insn::InitCoroutine { yield_reg, jump_on_definition: BranchOffset::Offset(0), start_offset: start_offset_label, }); let end_label = program.allocate_label(); let yield_label = program.allocate_label(); program.preassign_label_to_next_insn(yield_label); program.emit_insn(Insn::Yield { yield_reg, end_offset: end_label, subtype_clear_start_reg: 0, subtype_clear_count: 0, }); let goto_label = program.allocate_label(); emit_offset(program, goto_label, t_ctx.reg_offset); let row_len = plan.values[0].len(); let copy_start_reg = program.alloc_registers(row_len); for i in 0..row_len { program.emit_insn(Insn::Copy { src_reg: start_reg + i, dst_reg: copy_start_reg + i, extra_amount: 0, }); } emit_values_to_destination(program, plan, t_ctx, copy_start_reg, row_len, end_label)?; program.preassign_label_to_next_insn(goto_label); program.emit_insn(Insn::Goto { target_pc: yield_label, }); program.preassign_label_to_next_insn(end_label); Ok(copy_start_reg) } fn emit_values_in_subquery( program: &mut ProgramBuilder, plan: &SelectPlan, t_ctx: &mut TranslateCtx, yield_reg: usize, ) -> Result { let row_len = plan.values[0].len(); let start_reg = if let Some(reg) = t_ctx.reg_result_cols_start { program.reg_result_cols_start = Some(reg); reg } else { let reg = program.alloc_registers(row_len); t_ctx.reg_result_cols_start = Some(reg); program.reg_result_cols_start = Some(reg); reg }; for value in &plan.values { for (i, v) in value.iter().enumerate() { translate_expr_no_constant_opt( program, None, v, start_reg + i, &t_ctx.resolver, NoConstantOptReason::RegisterReuse, )?; } program.emit_insn(Insn::Yield { yield_reg, end_offset: BranchOffset::Offset(0), subtype_clear_start_reg: start_reg, subtype_clear_count: row_len, }); } Ok(start_reg) } fn emit_values_to_destination( program: &mut ProgramBuilder, plan: &SelectPlan, t_ctx: &TranslateCtx, start_reg: usize, row_len: usize, end_label: BranchOffset, ) -> Result<()> { match &plan.query_destination { QueryDestination::ResultRows => { program.emit_insn(Insn::ResultRow { start_reg, count: row_len, }); if let Some(limit_ctx) = t_ctx.limit_ctx { program.emit_insn(Insn::DecrJumpZero { reg: limit_ctx.reg_limit, target_pc: end_label, }); } } QueryDestination::CoroutineYield { yield_reg, .. } => { program.emit_insn(Insn::Yield { yield_reg: *yield_reg, end_offset: BranchOffset::Offset(0), subtype_clear_start_reg: start_reg, subtype_clear_count: row_len, }); } QueryDestination::EphemeralIndex { .. } => { emit_values_to_index(program, plan, start_reg, row_len)?; } QueryDestination::EphemeralTable { .. } => { emit_values_to_table(program, plan, start_reg, row_len); } QueryDestination::ExistsSubqueryResult { result_reg } => { program.emit_insn(Insn::Integer { value: 1, dest: *result_reg, }); } QueryDestination::RowValueSubqueryResult { result_reg_start, num_regs, } => { turso_assert_eq!( row_len, *num_regs, "row value subqueries must have matching result columns and registers" ); program.emit_insn(Insn::Copy { src_reg: start_reg, dst_reg: *result_reg_start, extra_amount: num_regs - 1, }); } QueryDestination::RowSet { .. } => { unreachable!("RowSet query destination should not be used in values emission") } QueryDestination::Unset => unreachable!("Unset query destination should not be reached"), } Ok(()) } fn emit_values_to_index( program: &mut ProgramBuilder, plan: &SelectPlan, start_reg: usize, row_len: usize, ) -> Result<()> { let (cursor_id, index, affinity_str, is_delete) = match &plan.query_destination { QueryDestination::EphemeralIndex { cursor_id, index, affinity_str, is_delete, } => (cursor_id, index, affinity_str, is_delete), _ => unreachable!(), }; if row_len != index.columns.len() { crate::bail_corrupt_error!( "VALUES column count {} does not match index column count {}", row_len, index.columns.len() ); } if *is_delete { program.emit_insn(Insn::IdxDelete { start_reg, num_regs: row_len, cursor_id: *cursor_id, raise_error_if_no_matching_entry: false, }); } else { let record_reg = program.alloc_register(); // Seek indexes may reorder key columns relative to the subquery result // column layout, so materialized VALUES rows must be reordered to match // the index key definition before insertion. let needs_reorder = index .columns .iter() .enumerate() .any(|(idx_pos, col)| col.pos_in_table != idx_pos); let extra_for_rowid = usize::from(index.ephemeral && index.has_rowid); let (record_start, record_count) = if needs_reorder || extra_for_rowid != 0 { let record_count = row_len + extra_for_rowid; let record_start = program.alloc_registers(record_count); if needs_reorder { for (idx_pos, idx_col) in index.columns.iter().enumerate() { program.emit_insn(Insn::Copy { src_reg: start_reg + idx_col.pos_in_table, dst_reg: record_start + idx_pos, extra_amount: 0, }); } } else { for i in 0..row_len { program.emit_insn(Insn::Copy { src_reg: start_reg + i, dst_reg: record_start + i, extra_amount: 0, }); } } if extra_for_rowid != 0 { program.emit_insn(Insn::Sequence { cursor_id: *cursor_id, target_reg: record_start + row_len, }); } (record_start, record_count) } else { (start_reg, row_len) }; program.emit_insn(Insn::MakeRecord { start_reg: to_u16(record_start), count: to_u16(record_count), dest_reg: to_u16(record_reg), index_name: Some(index.name.clone()), affinity_str: affinity_str.as_ref().map(|s| (**s).clone()), }); program.emit_insn(Insn::IdxInsert { cursor_id: *cursor_id, record_reg, unpacked_start: None, unpacked_count: None, flags: IdxInsertFlags::new().no_op_duplicate(), }); } Ok(()) } fn emit_values_to_table( program: &mut ProgramBuilder, plan: &SelectPlan, start_reg: usize, row_len: usize, ) { let (cursor_id, table) = match &plan.query_destination { QueryDestination::EphemeralTable { cursor_id, table, .. } => (cursor_id, table), _ => unreachable!(), }; let record_reg = program.alloc_register(); let rowid_reg = program.alloc_register(); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(start_reg), count: to_u16(row_len), dest_reg: to_u16(record_reg), index_name: Some(table.name.clone()), affinity_str: None, }); program.emit_insn(Insn::NewRowid { cursor: *cursor_id, rowid_reg, prev_largest_reg: 0, }); program.emit_insn(Insn::Insert { cursor: *cursor_id, key_reg: rowid_reg, record_reg, flag: InsertFlags::new().is_ephemeral_table_insert(), table_name: table.name.clone(), }); } ================================================ FILE: core/translate/view.rs ================================================ use crate::schema::{SchemaObjectType, DBSP_TABLE_PREFIX, RESERVED_TABLE_PREFIXES}; use crate::storage::pager::CreateBTreeFlags; use crate::sync::Arc; use crate::translate::emitter::Resolver; use crate::translate::schema::{emit_schema_entry, SchemaEntryType, SQLITE_TABLEID}; use crate::util::{ escape_sql_string_literal, normalize_ident, PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX, }; use crate::vdbe::builder::{CursorType, ProgramBuilder}; use crate::vdbe::insn::{CmpInsFlags, Cookie, Insn, RegisterOrLiteral}; use crate::{bail_parse_error, Connection, Result}; use turso_parser::ast; pub fn translate_create_materialized_view( view_name: &ast::QualifiedName, resolver: &Resolver, select_stmt: &ast::Select, connection: Arc, program: &mut ProgramBuilder, ) -> Result<()> { // Check if experimental views are enabled if !connection.experimental_views_enabled() { return Err(crate::LimboError::ParseError( "CREATE MATERIALIZED VIEW is an experimental feature. Enable with --experimental-views flag" .to_string(), )); } let database_id = resolver.resolve_database_id(view_name)?; // The DBSP incremental maintenance runtime (populate_from_table, etc.) assumes // the main database pager/schema. Block attached databases until that is fixed. if database_id != crate::MAIN_DB_ID { crate::bail_parse_error!("materialized views are not supported on attached databases"); } if crate::is_attached_db(database_id) { let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version); program.begin_write_on_database(database_id, schema_cookie); } let normalized_view_name = normalize_ident(view_name.name.as_str()); if RESERVED_TABLE_PREFIXES .iter() .any(|prefix| normalized_view_name.starts_with(prefix)) { bail_parse_error!( "Object name reserved for internal use: {}", view_name.name.as_str() ); } // Check if view already exists if resolver.with_schema(database_id, |s| { s.get_materialized_view(&normalized_view_name).is_some() }) { return Err(crate::LimboError::ParseError(format!( "View {normalized_view_name} already exists" ))); } // Validate the view can be created and extract its columns // This validation happens before updating sqlite_master to prevent // storing invalid view definitions // Check for cross-database table references first crate::util::validate_select_for_views(select_stmt, view_name.db_name.as_ref())?; use crate::incremental::view::IncrementalView; use crate::schema::BTreeTable; let view_column_schema = resolver.with_schema(database_id, |s| { IncrementalView::validate_and_extract_columns(select_stmt, s) })?; let view_columns = view_column_schema.flat_columns(); // Reconstruct the SQL string for storage let sql = create_materialized_view_to_str(&view_name.name.as_ident(), select_stmt); // Create a btree for storing the materialized view state // This btree will hold the materialized rows (row_id -> values) let view_root_reg = program.alloc_register(); program.emit_insn(Insn::CreateBtree { db: database_id, root: view_root_reg, flags: CreateBTreeFlags::new_table(), }); // Create a second btree for DBSP operator state (e.g., aggregate state) // This is stored as a hidden table: __turso_internal_dbsp_state_ let dbsp_state_root_reg = program.alloc_register(); program.emit_insn(Insn::CreateBtree { db: database_id, root: dbsp_state_root_reg, flags: CreateBTreeFlags::new_table(), }); // Create a proper BTreeTable for the cursor with the actual view columns let view_table = Arc::new(BTreeTable { root_page: 0, // Will be set to actual root page after creation name: normalized_view_name.clone(), columns: view_columns, primary_key_columns: vec![], // Materialized views use implicit rowid has_rowid: true, is_strict: false, has_autoincrement: false, unique_sets: vec![], foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }); // Allocate a cursor for writing to the view's btree during population let view_cursor_id = program.alloc_cursor_id(crate::vdbe::builder::CursorType::BTreeTable(view_table)); // Open the cursor to the view's btree program.emit_insn(Insn::OpenWrite { cursor_id: view_cursor_id, root_page: RegisterOrLiteral::Register(view_root_reg), db: database_id, }); // Clear any existing data in the btree // This is important because if we're reusing a page that previously held // a materialized view, there might be old data still there // We need to start with a clean slate let clear_loop_label = program.allocate_label(); let clear_done_label = program.allocate_label(); // Rewind to the beginning of the btree program.emit_insn(Insn::Rewind { cursor_id: view_cursor_id, pc_if_empty: clear_done_label, }); // Loop to delete all rows program.preassign_label_to_next_insn(clear_loop_label); program.emit_insn(Insn::Delete { cursor_id: view_cursor_id, table_name: normalized_view_name.clone(), is_part_of_update: false, }); program.emit_insn(Insn::Next { cursor_id: view_cursor_id, pc_if_next: clear_loop_label, }); program.preassign_label_to_next_insn(clear_done_label); // Open cursor to sqlite_schema table let table = resolver.with_schema(database_id, |s| s.get_btree_table(SQLITE_TABLEID).unwrap()); let sqlite_schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(table)); program.emit_insn(Insn::OpenWrite { cursor_id: sqlite_schema_cursor_id, root_page: 1i64.into(), db: database_id, }); // Add the materialized view entry to sqlite_schema emit_schema_entry( program, resolver, sqlite_schema_cursor_id, None, // cdc_table_cursor_id, no cdc for views SchemaEntryType::View, &normalized_view_name, &normalized_view_name, view_root_reg, // btree root for materialized view data Some(sql), )?; // Add the DBSP state table to sqlite_master (required for materialized views) // Include the version number in the table name use crate::incremental::compiler::DBSP_CIRCUIT_VERSION; let dbsp_table_name = ast::Name::exact(format!( "{DBSP_TABLE_PREFIX}{DBSP_CIRCUIT_VERSION}_{normalized_view_name}" )); let dbsp_table_ident = dbsp_table_name.as_ident(); // The element_id column uses SQLite's dynamic typing system to store different value types: // - For hash-based operators (joins, filters): stores INTEGER hash values or rowids // - For future MIN/MAX operators: stores the actual values being compared (INTEGER, REAL, TEXT, BLOB) // SQLite's type affinity and sorting rules ensure correct ordering within each operator's data let dbsp_sql = format!( "CREATE TABLE {dbsp_table_ident} (\ operator_id INTEGER NOT NULL, \ zset_id BLOB NOT NULL, \ element_id BLOB NOT NULL, \ value BLOB, \ weight INTEGER NOT NULL, \ PRIMARY KEY (operator_id, zset_id, element_id)\ )" ); emit_schema_entry( program, resolver, sqlite_schema_cursor_id, None, // cdc_table_cursor_id SchemaEntryType::Table, dbsp_table_name.as_str(), dbsp_table_name.as_str(), dbsp_state_root_reg, // Root for DBSP state table Some(dbsp_sql), )?; // Create automatic primary key index for the DBSP table // Since the table has PRIMARY KEY (operator_id, zset_id, element_id), we need an index let dbsp_index_root_reg = program.alloc_register(); program.emit_insn(Insn::CreateBtree { db: database_id, root: dbsp_index_root_reg, flags: CreateBTreeFlags::new_index(), }); // Register the index in sqlite_schema let dbsp_index_name = format!( "{}{}_1", PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX, &dbsp_table_name.as_str() ); emit_schema_entry( program, resolver, sqlite_schema_cursor_id, None, // cdc_table_cursor_id SchemaEntryType::Index, &dbsp_index_name, dbsp_table_name.as_str(), dbsp_index_root_reg, None, // Automatic indexes don't store SQL )?; // Parse schema to load the new view and DBSP state table let escaped_view_name = escape_sql_string_literal(&normalized_view_name); let escaped_dbsp_table_name = escape_sql_string_literal(dbsp_table_name.as_str()); let escaped_dbsp_index_name = escape_sql_string_literal(&dbsp_index_name); program.emit_insn(Insn::ParseSchema { db: database_id, where_clause: Some(format!( "name = '{escaped_view_name}' OR name = '{escaped_dbsp_table_name}' OR name = '{escaped_dbsp_index_name}'" )), }); let schema_version = resolver.with_schema(database_id, |s| s.schema_version); program.emit_insn(Insn::SetCookie { db: database_id, cookie: Cookie::SchemaVersion, value: (schema_version + 1) as i32, p5: 0, }); // Populate the materialized view let cursor_info = vec![(normalized_view_name.clone(), view_cursor_id)]; program.emit_insn(Insn::PopulateMaterializedViews { cursors: cursor_info, }); program.epilogue(resolver.schema()); Ok(()) } fn create_materialized_view_to_str(view_name: &str, select_stmt: &ast::Select) -> String { format!("CREATE MATERIALIZED VIEW {view_name} AS {select_stmt}") } pub fn translate_create_view( view_name: &ast::QualifiedName, resolver: &Resolver, select_stmt: &ast::Select, columns: &[ast::IndexedColumn], program: &mut ProgramBuilder, ) -> Result<()> { let database_id = resolver.resolve_database_id(view_name)?; if crate::is_attached_db(database_id) { let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version); program.begin_write_on_database(database_id, schema_cookie); } let normalized_view_name = normalize_ident(view_name.name.as_str()); if RESERVED_TABLE_PREFIXES .iter() .any(|prefix| normalized_view_name.starts_with(prefix)) { bail_parse_error!( "Object name reserved for internal use: {}", view_name.name.as_str() ); } // Check for name conflicts with existing schema objects if let Some(object_type) = resolver.with_schema(database_id, |s| s.get_object_type(&normalized_view_name)) { let type_str = match object_type { SchemaObjectType::Table => "table", SchemaObjectType::View => "view", SchemaObjectType::Index => "index", }; return Err(crate::LimboError::ParseError(format!( "{type_str} {normalized_view_name} already exists" ))); } // Also check materialized views (not in get_object_type since they're stored differently) if resolver .with_schema(database_id, |s| { s.get_materialized_view(&normalized_view_name) }) .is_some() { return Err(crate::LimboError::ParseError(format!( "view {normalized_view_name} already exists" ))); } crate::util::validate_select_for_views(select_stmt, view_name.db_name.as_ref())?; // Reconstruct the SQL string let sql = create_view_to_str(&view_name.name.as_ident(), columns, select_stmt); // Open cursor to sqlite_schema table let table = resolver.schema().get_btree_table(SQLITE_TABLEID).unwrap(); let sqlite_schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(table)); program.emit_insn(Insn::OpenWrite { cursor_id: sqlite_schema_cursor_id, root_page: 1i64.into(), db: database_id, }); // Add the view entry to sqlite_schema emit_schema_entry( program, resolver, sqlite_schema_cursor_id, None, // cdc_table_cursor_id, no cdc for views SchemaEntryType::View, &normalized_view_name, &normalized_view_name, 0, // Regular views don't have a btree Some(sql), )?; // Parse schema to load the new view let escaped_view_name = escape_sql_string_literal(&normalized_view_name); program.emit_insn(Insn::ParseSchema { db: database_id, where_clause: Some(format!("name = '{escaped_view_name}'")), }); let schema_version = resolver.with_schema(database_id, |s| s.schema_version); program.emit_insn(Insn::SetCookie { db: database_id, cookie: Cookie::SchemaVersion, value: (schema_version + 1) as i32, p5: 0, }); Ok(()) } fn create_view_to_str( view_name: &str, columns: &[ast::IndexedColumn], select_stmt: &ast::Select, ) -> String { let columns_str = columns .iter() .map(|col| col.col_name.as_str()) .collect::>() .join(", "); if !columns_str.is_empty() { return format!("CREATE VIEW {view_name} ({columns_str}) AS {select_stmt}"); } format!("CREATE VIEW {view_name} AS {select_stmt}") } pub fn translate_drop_view( resolver: &Resolver, view_name: &ast::QualifiedName, if_exists: bool, program: &mut ProgramBuilder, ) -> Result<()> { let database_id = resolver.resolve_database_id(view_name)?; if crate::is_attached_db(database_id) { let schema_cookie = resolver.with_schema(database_id, |s| s.schema_version); program.begin_write_on_database(database_id, schema_cookie); } let normalized_view_name = normalize_ident(view_name.name.as_str()); // Check if view exists (either regular or materialized) let (is_regular_view, is_materialized_view) = resolver.with_schema(database_id, |s| { ( s.get_view(&normalized_view_name).is_some(), s.is_materialized_view(&normalized_view_name), ) }); let view_exists = is_regular_view || is_materialized_view; if !view_exists && !if_exists { return Err(crate::LimboError::ParseError(format!( "no such view: {normalized_view_name}" ))); } if !view_exists && if_exists { // View doesn't exist but IF EXISTS was specified, nothing to do return Ok(()); } // If this is a materialized view, we need to destroy its btree as well // and also clean up the associated DBSP state table and index let dbsp_table_name = if is_materialized_view { if let Some(table) = resolver.with_schema(database_id, |s| s.get_table(&normalized_view_name)) { if let Some(btree_table) = table.btree() { // Destroy the btree for the materialized view program.emit_insn(Insn::Destroy { db: database_id, root: btree_table.root_page, former_root_reg: 0, // No autovacuum is_temp: 0, }); } } // Construct the DBSP state table name use crate::incremental::compiler::DBSP_CIRCUIT_VERSION; Some(format!( "{DBSP_TABLE_PREFIX}{DBSP_CIRCUIT_VERSION}_{normalized_view_name}" )) } else { None }; // Destroy DBSP state table and index btrees if this is a materialized view if let Some(ref dbsp_table_name) = dbsp_table_name { // Destroy DBSP indexes first let dbsp_indexes: Vec<_> = resolver.with_schema(database_id, |s| { s.get_indices(dbsp_table_name).cloned().collect() }); for index in &dbsp_indexes { program.emit_insn(Insn::Destroy { db: database_id, root: index.root_page, former_root_reg: 0, // No autovacuum is_temp: 0, }); } // Destroy DBSP state table btree if let Some(dbsp_table) = resolver.with_schema(database_id, |s| s.get_table(dbsp_table_name)) { if let Some(dbsp_btree_table) = dbsp_table.btree() { program.emit_insn(Insn::Destroy { db: database_id, root: dbsp_btree_table.root_page, former_root_reg: 0, // No autovacuum is_temp: 0, }); } } } // Open cursor to sqlite_schema table (structure is the same for all databases) let schema_table = resolver.with_schema(0, |s| s.get_btree_table(SQLITE_TABLEID).unwrap()); let sqlite_schema_cursor_id = program.alloc_cursor_id(CursorType::BTreeTable(schema_table)); program.emit_insn(Insn::OpenWrite { cursor_id: sqlite_schema_cursor_id, root_page: 1i64.into(), db: database_id, }); // Allocate registers for searching let view_name_reg = program.alloc_register(); let type_reg = program.alloc_register(); let rowid_reg = program.alloc_register(); // Set the view name and type we're looking for program.emit_insn(Insn::String8 { dest: view_name_reg, value: normalized_view_name.clone(), }); program.emit_insn(Insn::String8 { dest: type_reg, value: "view".to_string(), }); // Start scanning from the beginning let end_loop_label = program.allocate_label(); let loop_start_label = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: sqlite_schema_cursor_id, pc_if_empty: end_loop_label, }); program.preassign_label_to_next_insn(loop_start_label); // Check if this row should be deleted // Column 0 is type, Column 1 is name, Column 2 is tbl_name let col0_reg = program.alloc_register(); let col1_reg = program.alloc_register(); program.emit_column_or_rowid(sqlite_schema_cursor_id, 0, col0_reg); program.emit_column_or_rowid(sqlite_schema_cursor_id, 1, col1_reg); // Check if this row matches the view, DBSP table, or DBSP index let skip_delete_label = program.allocate_label(); // Check if this is the view entry (type='view' and name=view_name) program.emit_insn(Insn::Ne { lhs: col0_reg, rhs: type_reg, target_pc: skip_delete_label, flags: CmpInsFlags::default(), collation: program.curr_collation(), }); program.emit_insn(Insn::Ne { lhs: col1_reg, rhs: view_name_reg, target_pc: skip_delete_label, flags: CmpInsFlags::default(), collation: program.curr_collation(), }); // Matches view - delete it program.emit_insn(Insn::RowId { cursor_id: sqlite_schema_cursor_id, dest: rowid_reg, }); program.emit_insn(Insn::Delete { cursor_id: sqlite_schema_cursor_id, table_name: "sqlite_schema".to_string(), is_part_of_update: false, }); program.resolve_label(skip_delete_label, program.offset()); // Move to next row program.emit_insn(Insn::Next { cursor_id: sqlite_schema_cursor_id, pc_if_next: loop_start_label, }); program.preassign_label_to_next_insn(end_loop_label); // If this is a materialized view, delete DBSP table and index entries in a second pass // We do this in a separate loop to ensure we catch all entries even if they come // in different orders in sqlite_schema if let Some(ref dbsp_table_name) = dbsp_table_name { // Set up registers for DBSP table name and types (outside the loop for efficiency) let dbsp_table_name_reg_2 = program.alloc_register(); program.emit_insn(Insn::String8 { dest: dbsp_table_name_reg_2, value: dbsp_table_name.clone(), }); let table_type_reg_2 = program.alloc_register(); program.emit_insn(Insn::String8 { dest: table_type_reg_2, value: "table".to_string(), }); let index_type_reg_2 = program.alloc_register(); program.emit_insn(Insn::String8 { dest: index_type_reg_2, value: "index".to_string(), }); let dbsp_index_name_reg_2 = program.alloc_register(); let dbsp_index_name_2 = format!("{PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX}{dbsp_table_name}_1"); program.emit_insn(Insn::String8 { dest: dbsp_index_name_reg_2, value: dbsp_index_name_2, }); // Allocate column registers once (outside the loop) let dbsp_col0_reg = program.alloc_register(); let dbsp_col1_reg = program.alloc_register(); // Second pass: delete DBSP table and index entries let dbsp_end_loop_label = program.allocate_label(); let dbsp_loop_start_label = program.allocate_label(); program.emit_insn(Insn::Rewind { cursor_id: sqlite_schema_cursor_id, pc_if_empty: dbsp_end_loop_label, }); program.preassign_label_to_next_insn(dbsp_loop_start_label); // Read columns for this row (reusing the same registers) program.emit_column_or_rowid(sqlite_schema_cursor_id, 0, dbsp_col0_reg); program.emit_column_or_rowid(sqlite_schema_cursor_id, 1, dbsp_col1_reg); let dbsp_skip_delete_label = program.allocate_label(); // Check if this is the DBSP table entry (type='table' and name=dbsp_table_name) let check_dbsp_index_label = program.allocate_label(); program.emit_insn(Insn::Ne { lhs: dbsp_col0_reg, rhs: table_type_reg_2, target_pc: check_dbsp_index_label, flags: CmpInsFlags::default(), collation: program.curr_collation(), }); program.emit_insn(Insn::Ne { lhs: dbsp_col1_reg, rhs: dbsp_table_name_reg_2, target_pc: check_dbsp_index_label, flags: CmpInsFlags::default(), collation: program.curr_collation(), }); // Matches DBSP table - delete it program.emit_insn(Insn::RowId { cursor_id: sqlite_schema_cursor_id, dest: rowid_reg, }); program.emit_insn(Insn::Delete { cursor_id: sqlite_schema_cursor_id, table_name: "sqlite_schema".to_string(), is_part_of_update: false, }); program.emit_insn(Insn::Goto { target_pc: dbsp_skip_delete_label, }); // Check if this is the DBSP index entry (type='index' and name=dbsp_index_name) program.preassign_label_to_next_insn(check_dbsp_index_label); program.emit_insn(Insn::Ne { lhs: dbsp_col0_reg, rhs: index_type_reg_2, target_pc: dbsp_skip_delete_label, flags: CmpInsFlags::default(), collation: program.curr_collation(), }); program.emit_insn(Insn::Ne { lhs: dbsp_col1_reg, rhs: dbsp_index_name_reg_2, target_pc: dbsp_skip_delete_label, flags: CmpInsFlags::default(), collation: program.curr_collation(), }); // Matches DBSP index - delete it program.emit_insn(Insn::RowId { cursor_id: sqlite_schema_cursor_id, dest: rowid_reg, }); program.emit_insn(Insn::Delete { cursor_id: sqlite_schema_cursor_id, table_name: "sqlite_schema".to_string(), is_part_of_update: false, }); program.resolve_label(dbsp_skip_delete_label, program.offset()); // Move to next row program.emit_insn(Insn::Next { cursor_id: sqlite_schema_cursor_id, pc_if_next: dbsp_loop_start_label, }); program.preassign_label_to_next_insn(dbsp_end_loop_label); } // Remove the view from the in-memory schema program.emit_insn(Insn::DropView { db: database_id, view_name: normalized_view_name, }); // Update schema version (increment schema cookie) let schema_version = resolver.with_schema(database_id, |s| s.schema_version); let schema_version_reg = program.alloc_register(); program.emit_insn(Insn::Integer { dest: schema_version_reg, value: (schema_version + 1) as i64, }); program.emit_insn(Insn::SetCookie { db: database_id, cookie: Cookie::SchemaVersion, value: (schema_version + 1) as i32, p5: 1, // update version }); program.epilogue(resolver.schema()); Ok(()) } ================================================ FILE: core/translate/window.rs ================================================ use crate::function::WindowFunc; use crate::schema::{BTreeTable, Table}; use crate::sync::Arc; use crate::translate::aggregation::{translate_aggregation_step, AggArgumentSource}; use crate::translate::collate::{get_collseq_from_expr, CollationSeq}; use crate::translate::emitter::{Resolver, TranslateCtx}; use crate::translate::expr::{walk_expr, walk_expr_mut, WalkControl}; use crate::translate::order_by::EmitOrderBy; use crate::translate::plan::{ Aggregate, Distinctness, JoinOrderMember, JoinedTable, QueryDestination, ResultSetColumn, SelectPlan, TableReferences, Window, WindowFunctionKind, }; use crate::translate::planner::resolve_window_and_aggregate_functions; use crate::translate::result_row::emit_select_result; use crate::types::KeyInfo; use crate::util::exprs_are_equivalent; use crate::vdbe::builder::{CursorType, ProgramBuilder, TableRefIdCounter}; use crate::vdbe::insn::{ to_u16, {InsertFlags, Insn}, }; use crate::vdbe::{BranchOffset, CursorID}; use crate::Result; use crate::{turso_assert, turso_assert_eq}; use std::mem; use turso_parser::ast::Name; use turso_parser::ast::{Expr, FunctionTail, Literal, Over, SortOrder, TableInternalId}; const SUBQUERY_DATABASE_ID: usize = 0; struct WindowSubqueryContext<'a> { resolver: &'a Resolver<'a>, subquery_order_by: &'a mut Vec<(Box, SortOrder)>, subquery_result_columns: &'a mut Vec, subquery_id: &'a TableInternalId, } /// Rewrite a `SELECT` plan for window function processing. /// /// A `SELECT` may reference multiple window definitions, but internally, each `SELECT` plan /// operates on **exactly one** window. Multiple window functions may reference the same window. /// /// The original plan is rewritten into a series of nested subqueries, each bound to a single /// window definition. Each subquery produces rows in the order determined by its parent window /// definition. The innermost subquery does not have any window assigned to it; instead, /// the FROM, WHERE, GROUP BY, and HAVING clauses from the original query are pushed down to it. /// The outermost query retains ORDER BY, LIMIT, and OFFSET. /// /// # Examples /// ```sql /// -- Example 1: Query with one window /// SELECT /// a+1, /// max(b) OVER (PARTITION BY c ORDER BY d), /// min(c) OVER (PARTITION BY c ORDER BY d) /// FROM t1 /// ORDER BY e; /// /// -- Rewritten form /// SELECT /// a+1, /// max(b) OVER (PARTITION BY c ORDER BY d), /// min(c) OVER (PARTITION BY c ORDER BY d) /// FROM (SELECT a, b, c, d, e FROM t1 ORDER BY c, d) /// ORDER BY e; /// /// -- Example 2: Query with multiple windows /// SELECT /// a, /// max(b) OVER (PARTITION BY c ORDER BY d), /// min(c) OVER (PARTITION BY e ORDER BY f) /// FROM t1; /// /// -- Rewritten form /// SELECT /// a, /// max(b) OVER (PARTITION BY c ORDER BY d) AS w1, /// w2 /// FROM ( /// SELECT /// a, /// b, /// c, /// d, /// min(c) OVER (PARTITION BY e ORDER BY f) AS w2 /// FROM (SELECT a, b, c, d, e, f FROM t1 ORDER BY e, f) /// ORDER BY c, d /// ); /// ``` pub fn plan_windows( plan: &mut SelectPlan, resolver: &Resolver, table_ref_counter: &mut TableRefIdCounter, windows: &mut Vec, ) -> crate::Result<()> { // Remove named windows that are not referenced by any function, as they can be ignored. windows.retain(|w| !w.functions.is_empty()); if !windows.is_empty() { // Sanity check: this should never happen because the syntax disallows combining VALUES with windows turso_assert!( plan.values.is_empty(), "VALUES clause with windows is not supported" ); } prepare_window_subquery(plan, resolver, table_ref_counter, windows, 0) } fn prepare_window_subquery( outer_plan: &mut SelectPlan, resolver: &Resolver, table_ref_counter: &mut TableRefIdCounter, windows: &mut Vec, processed_window_count: usize, ) -> crate::Result<()> { if windows.is_empty() { return Ok(()); } let mut current_window = windows.swap_remove(0); let mut subquery_result_columns = Vec::new(); let mut subquery_order_by = Vec::new(); let subquery_id = table_ref_counter.next(); if current_window.name.is_none() { // This is part of normalizing the window definition. The remaining logic lives in // `rewrite_expr_referencing_current_window`, which replaces inline window definitions // with references by name. // // The goal is to always work with named windows instead of a mix of named and // inline ones. This way, we don’t need to rewrite expressions embedded in inline // definitions (there might be many equivalent definitions per subquery). Instead, // we rewrite the named definition once, and all associated window functions // require no additional processing. // // At this stage, window definitions and window functions are already bound, // so this normalization is purely to keep the plan valid. // // If the generated name is not unique across the entire query, that’s acceptable — // the final plan always associates exactly one window with one subquery. current_window.name = Some(format!("window_{processed_window_count}")); } let mut ctx = WindowSubqueryContext { resolver, subquery_order_by: &mut subquery_order_by, subquery_result_columns: &mut subquery_result_columns, subquery_id: &subquery_id, }; // Build the ORDER BY clause for the subquery by concatenating the window’s PARTITION BY // columns with its ORDER BY columns.This ensures that rows in the subquery are returned // in the correct order for partitioning and window function evaluation. for expr in current_window.partition_by.iter_mut() { append_order_by(outer_plan, expr, &SortOrder::Asc, &mut ctx)?; current_window.deduplicated_partition_by_len = Some(ctx.subquery_result_columns.len()) } for (expr, order) in current_window.order_by.iter_mut() { append_order_by(outer_plan, expr, order, &mut ctx)?; } // Rewrite expressions from the outer query’s result columns and ORDER BY clause so that // they reference the subquery instead. The original expressions are included in the // subquery’s result columns. for col in outer_plan.result_columns.iter_mut() { rewrite_terminal_expr( &mut outer_plan.aggregates, &mut col.expr, &mut current_window, &mut ctx, )?; } for (expr, _) in outer_plan.order_by.iter_mut() { rewrite_terminal_expr( &mut outer_plan.aggregates, expr, &mut current_window, &mut ctx, )?; } // When there is no ORDER BY or PARTITION BY clause, the window function takes zero arguments, // and no other columns are selected (e.g., "SELECT count() OVER () FROM products"), // `subquery_result_columns` may be empty. Add a constant expression to keep the query valid. if subquery_result_columns.is_empty() { subquery_result_columns.push(ResultSetColumn { expr: Expr::Literal(Literal::Numeric("0".to_string())), alias: None, contains_aggregates: false, }); } let new_join_order = vec![JoinOrderMember { table_id: subquery_id, original_idx: 0, is_outer: false, }]; let new_table_references = TableReferences::new( vec![], outer_plan.table_references.outer_query_refs().to_vec(), ); let mut inner_plan = SelectPlan { join_order: mem::replace(&mut outer_plan.join_order, new_join_order), table_references: mem::replace(&mut outer_plan.table_references, new_table_references), result_columns: subquery_result_columns, where_clause: mem::take(&mut outer_plan.where_clause), group_by: mem::take(&mut outer_plan.group_by), order_by: subquery_order_by, aggregates: mem::take(&mut outer_plan.aggregates), limit: None, offset: None, contains_constant_false_condition: false, query_destination: QueryDestination::placeholder_for_subquery(), distinctness: Distinctness::NonDistinct, values: vec![], window: None, non_from_clause_subqueries: vec![], input_cardinality_hint: None, estimated_output_rows: None, simple_aggregate: None, }; prepare_window_subquery( &mut inner_plan, resolver, table_ref_counter, windows, processed_window_count + 1, )?; let subquery = JoinedTable::new_subquery( format!("window_subquery_{processed_window_count}"), inner_plan, None, subquery_id, )?; // Verify that the subquery has the expected database ID. // This is required to ensure that assumptions in `rewrite_terminal_expr` are valid. turso_assert_eq!( subquery.database_id, SUBQUERY_DATABASE_ID, "subquery database id must be SUBQUERY_DATABASE_ID", {"SUBQUERY_DATABASE_ID": SUBQUERY_DATABASE_ID} ); outer_plan.window = Some(current_window); outer_plan.table_references.add_joined_table(subquery); Ok(()) } fn append_order_by( plan: &mut SelectPlan, expr: &mut Expr, sort_order: &SortOrder, ctx: &mut WindowSubqueryContext, ) -> crate::Result<()> { // Deduplicate: if an equivalent expression already exists in the subquery ORDER BY, // skip adding it again. This can happen when the same column appears in both // PARTITION BY and ORDER BY (e.g. OVER (PARTITION BY a ORDER BY a)), and prevents // the optimizer assertion group_by.exprs.len() >= order_by.len() from being violated. let already_exists = ctx .subquery_order_by .iter() .any(|(existing, _)| exprs_are_equivalent(existing, expr)); if !already_exists { ctx.subquery_order_by .push((Box::new(expr.clone()), *sort_order)); } let contains_aggregates = resolve_window_and_aggregate_functions(expr, ctx.resolver, &mut plan.aggregates, None)?; rewrite_expr_as_subquery_column(expr, ctx, contains_aggregates); Ok(()) } fn rewrite_terminal_expr( aggregates: &mut Vec, top_level_expr: &mut Expr, current_window: &mut Window, ctx: &mut WindowSubqueryContext, ) -> crate::Result { walk_expr_mut( top_level_expr, &mut |expr: &mut Expr| -> crate::Result { match expr { Expr::FunctionCall { filter_over, .. } | Expr::FunctionCallStar { filter_over, .. } => { if filter_over.over_clause.is_none() { // If the expression is a standard aggregate (non-window), push it down // to the subquery. if aggregates .iter() .any(|a| exprs_are_equivalent(&a.original_expr, expr)) { rewrite_expr_as_subquery_column(expr, ctx, true); } } else if let Some(window_function) = current_window .functions .iter_mut() .find(|f| exprs_are_equivalent(&f.original_expr, expr)) { // If the expression is a window function tied to the current window, // do not push it to the subquery. Instead, rewrite it so its child // expressions reference the subquery where needed. rewrite_expr_referencing_current_window( aggregates, current_window .name .clone() .expect("current_window must always have a name here"), ctx, expr, )?; window_function.original_expr = expr.clone(); // At this point, the expression and all its children now reference the subquery, // so further traversal is unnecessary. return Ok(WalkControl::SkipChildren); } else { // This is a window function referencing a different window (not the current one). // Push the entire expression to the subquery; it will be rewritten later. rewrite_expr_as_subquery_column(expr, ctx, false); } } Expr::RowId { .. } | Expr::Column { .. } => { rewrite_expr_as_subquery_column(expr, ctx, false); } _ => {} } Ok(WalkControl::Continue) }, ) } fn rewrite_expr_referencing_current_window( aggregates: &mut Vec, window_name: String, ctx: &mut WindowSubqueryContext, expr: &mut Expr, ) -> crate::Result<()> { fn normalize_over_clause(filter_over: &mut FunctionTail, window_name: &str) { // FILTER clause is not supported yet. Proper checks elsewhere return appropriate // error messages, and this ensures that nothing slips through unnoticed. turso_assert!( filter_over.filter_clause.is_none(), "FILTER in window functions is not supported" ); // Replace inline OVER clause with a reference to the named window. // The window name may be user-provided or planner-generated. *filter_over = FunctionTail { filter_clause: None, over_clause: Some(Over::Name(Name::exact(window_name.to_string()))), }; } match expr { Expr::FunctionCall { name: _, distinctness: _, args, order_by, filter_over, } => { for arg in args.iter_mut() { let contains_aggregates = resolve_window_and_aggregate_functions(arg, ctx.resolver, aggregates, None)?; rewrite_expr_as_subquery_column(arg, ctx, contains_aggregates); } turso_assert!( order_by.is_empty(), "ORDER BY in window functions is not supported" ); normalize_over_clause(filter_over, &window_name); } Expr::FunctionCallStar { filter_over, name: _, } => { normalize_over_clause(filter_over, &window_name); } _ => unreachable!("only functions can reference windows"), } Ok(()) } /// Rewrites an expression into a reference to a subquery column. /// If the expression was already pushed down, reuses the existing column index. /// Otherwise, adds it as a new column in the subquery's result set. fn rewrite_expr_as_subquery_column( expr: &mut Expr, ctx: &mut WindowSubqueryContext, contains_aggregates: bool, ) { let (column_idx, existing) = match ctx .subquery_result_columns .iter() .position(|col| exprs_are_equivalent(&col.expr, expr)) { Some(pos) => (pos, true), None => (ctx.subquery_result_columns.len(), false), }; let subquery_ref = Expr::Column { database: Some(SUBQUERY_DATABASE_ID), table: *ctx.subquery_id, column: column_idx, is_rowid_alias: false, }; if existing { *expr = subquery_ref; } else { let subquery_expr = mem::replace(expr, subquery_ref); ctx.subquery_result_columns.push(ResultSetColumn { expr: subquery_expr, alias: None, contains_aggregates, }); } } #[derive(Debug)] pub struct WindowMetadata<'a> { pub labels: WindowLabels, pub registers: WindowRegisters, pub cursors: WindowCursors, /// Number of input columns in the source subquery. pub src_column_count: usize, /// Maps expressions in the current query that reference subquery columns /// to their corresponding column indexes in the subquery’s result. pub expressions_referencing_subquery: Vec<(&'a Expr, usize)>, pub buffer_table_name: String, } #[derive(Debug)] pub struct WindowLabels { /// Address of the subroutine for flushing buffered rows pub flush_buffer: BranchOffset, /// Address of the end of window processing pub window_processing_end: BranchOffset, } #[derive(Debug)] pub struct WindowRegisters { /// Stores the ROWID of the last row inserted into the buffer table. /// If NULL, we are before inserting the first row of a new partition. pub rowid: usize, /// Start of the register array storing partition key values for the current partition. pub partition_start: Option, /// Start of the register array storing per-function state for window functions. /// Aggregates use `AggStep` to populate their state. pub acc_start: usize, /// Start of the register array storing per-function outputs. Aggregate windows /// populate these via `AggValue`; window-only functions like ROW_NUMBER() /// keep their running state here. pub acc_result_start: usize, /// Stores the address to which control returns after all buffered rows are flushed. pub flush_buffer_return_offset: usize, /// Start of consecutive registers containing column values for the current row /// read from the subquery. pub src_columns_start: usize, /// Start of the register array storing column values that need to be propagated /// from the subquery to the parent query. pub result_columns_start: usize, /// Start of the register array holding ORDER BY column values for the current row. /// These registers are used to detect whether the current row is a "peer" /// (i.e., has identical ORDER BY values to the previous row). pub new_order_by_columns_start: Option, /// Start of the register array holding ORDER BY column values from the previous row. /// These are used to compare against the current row to determine peer relationships. pub prev_order_by_columns_start: Option, } #[derive(Debug)] pub struct WindowCursors { /// Cursor used to read from the ephemeral buffer table pub buffer_read: CursorID, /// Cursor used to write to the ephemeral buffer table pub buffer_write: CursorID, } pub struct EmitWindow; impl EmitWindow { pub fn init<'a>( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx<'a>, window: &'a Window, plan: &SelectPlan, result_columns: &'a [ResultSetColumn], order_by: &'a [(Box, SortOrder)], ) -> crate::Result<()> { let joined_tables = &plan.joined_tables(); turso_assert_eq!(joined_tables.len(), 1, "expected only one joined table"); let src_table = &joined_tables[0]; let reg_src_columns_start = if let Table::FromClauseSubquery(from_clause_subquery) = &src_table.table { from_clause_subquery .result_columns_start_reg .expect("Subquery result_columns_start_reg must be set") } else { panic!( "expected source table to be a FromClauseSubquery, but got: {:?}", src_table.table ); }; let src_columns = src_table.columns().to_vec(); let src_column_count = src_columns.len(); let window_name = window.name.clone().expect("window name is missing"); let partition_by_len = window .deduplicated_partition_by_len .unwrap_or(window.partition_by.len()); let order_by_len = window.order_by.len(); let window_function_count = window.functions.len(); // An ephemeral table used to buffer rows for the current frame let buffer_table = Arc::new(BTreeTable { root_page: 0, // TODO: Generating the name this way may cause collisions with real tables in the // attached database. Other ephemeral tables are created similarly, so it’s left // as-is for now. Ideally, there should be a way to mark tables as ephemeral so // they can be handled differently from regular tables. name: format!("buffer_table_{window_name}"), has_rowid: true, primary_key_columns: vec![], columns: src_columns, is_strict: false, unique_sets: vec![], has_autoincrement: false, foreign_keys: vec![], check_constraints: vec![], rowid_alias_conflict_clause: None, }); let cursor_buffer_read = program.alloc_cursor_id(CursorType::BTreeTable(buffer_table.clone())); let cursor_buffer_write = program.alloc_cursor_id(CursorType::BTreeTable(buffer_table.clone())); program.emit_insn(Insn::OpenEphemeral { cursor_id: cursor_buffer_read, is_table: true, }); program.emit_insn(Insn::OpenDup { original_cursor_id: cursor_buffer_read, new_cursor_id: cursor_buffer_write, }); // Window function processing is similar to aggregation processing in how results are mapped // to registers. Each function expression is stored in `expr_to_reg_cache` along with its // result register. Later, when bytecode generation encounters the expression, the value is // copied from the result register instead of generating code to evaluate the expression. let reg_acc_start = program.alloc_registers(window_function_count); let reg_acc_result_start = program.alloc_registers(window_function_count); for (i, func) in window.functions.iter().enumerate() { t_ctx.resolver.cache_expr_reg( std::borrow::Cow::Borrowed(&func.original_expr), reg_acc_result_start + i, false, None, ); } // The same approach applies to expressions referencing the subquery (columns). // Instead of reading directly from the subquery, we redirect them to the corresponding // result registers. This is necessary because rows are buffered in an ephemeral table and // returned according to the rules of the window definition. let expressions_referencing_subquery = collect_expressions_referencing_subquery( result_columns, order_by, &src_table.internal_id, )?; let reg_col_start = program.alloc_registers(expressions_referencing_subquery.len()); for (i, (expr, _)) in expressions_referencing_subquery.iter().enumerate() { t_ctx.resolver.cache_scalar_expr_reg( std::borrow::Cow::Borrowed(expr), reg_col_start + i, false, &plan.table_references, )?; } t_ctx.meta_window = Some(WindowMetadata { labels: WindowLabels { flush_buffer: program.allocate_label(), window_processing_end: program.allocate_label(), }, registers: WindowRegisters { rowid: program.alloc_registers_and_init_w_null(1), partition_start: if partition_by_len > 0 { Some(program.alloc_registers_and_init_w_null(partition_by_len)) } else { None }, acc_start: reg_acc_start, acc_result_start: reg_acc_result_start, flush_buffer_return_offset: program.alloc_register(), src_columns_start: reg_src_columns_start, result_columns_start: reg_col_start, prev_order_by_columns_start: alloc_optional_registers(program, order_by_len), new_order_by_columns_start: alloc_optional_registers(program, order_by_len), }, cursors: WindowCursors { buffer_read: cursor_buffer_read, buffer_write: cursor_buffer_write, }, src_column_count, expressions_referencing_subquery, buffer_table_name: buffer_table.name.clone(), }); Ok(()) } /// Emits bytecode to process a single row of the window’s input (always a subquery). /// /// Note: /// The **buffer table** mentioned below is an ephemeral B-tree that temporarily /// stores rows for the current window frame. /// /// High-level overview: /// - Each row from the subquery is read, and its ORDER BY columns are loaded into /// dedicated registers for comparison and partitioning purposes. /// - If the row starts a new partition (based on PARTITION BY columns), the buffer /// and accumulators are flushed or reset as needed. /// - Rows are compared against the previous row to determine if they are "peers" /// (i.e., have the same ORDER BY values). Non-peer rows may trigger flushing /// of intermediate results. /// - The row is then inserted into the window’s buffer table. /// - Aggregate steps for any window functions are executed. pub fn emit_window_loop_source( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, plan: &SelectPlan, ) -> crate::Result<()> { let WindowMetadata { labels, registers, cursors, src_column_count: input_column_count, buffer_table_name, .. } = t_ctx.meta_window.as_ref().expect("missing window metadata"); let window = plan.window.as_ref().expect("missing window"); emit_load_order_by_columns(program, window, registers); emit_flush_buffer_if_new_partition(program, labels, registers, window, plan)?; emit_reset_state_if_new_partition(program, registers, window); emit_flush_buffer_if_not_peer(program, labels, registers, window, plan)?; emit_insert_row_into_buffer( program, registers, cursors, input_column_count, buffer_table_name, ); emit_aggregation_step(program, window, &t_ctx.resolver, plan, registers)?; Ok(()) } } fn alloc_optional_registers(program: &mut ProgramBuilder, count: usize) -> Option { if count > 0 { Some(program.alloc_registers(count)) } else { None } } fn collect_expressions_referencing_subquery<'a>( result_columns: &'a [ResultSetColumn], order_by: &'a [(Box, SortOrder)], subquery_id: &TableInternalId, ) -> crate::Result> { let mut expressions_referencing_subquery: Vec<(&'a Expr, usize)> = Vec::new(); for root_expr in result_columns .iter() .map(|col| &col.expr) .chain(order_by.iter().map(|(e, _)| e.as_ref())) { walk_expr( root_expr, &mut |expr: &Expr| -> crate::Result { match expr { Expr::FunctionCall { filter_over, .. } | Expr::FunctionCallStar { filter_over, .. } => { if filter_over.over_clause.is_some() { return Ok(WalkControl::SkipChildren); } } Expr::Column { column, table, .. } => { turso_assert_eq!( table, subquery_id, "only subquery columns can be referenced" ); if expressions_referencing_subquery .iter() .all(|(_, existing_column)| column != existing_column) { expressions_referencing_subquery.push((expr, *column)); } } _ => {} }; Ok(WalkControl::Continue) }, )?; } Ok(expressions_referencing_subquery) } fn emit_flush_buffer_if_new_partition( program: &mut ProgramBuilder, labels: &WindowLabels, registers: &WindowRegisters, window: &Window, plan: &SelectPlan, ) -> Result<()> { if let Some(reg_partition_start) = registers.partition_start { let same_partition_label = program.allocate_label(); let new_partition_label = program.allocate_label(); // Compare the first `deduplicated_partition_by_len` source columns with the saved // partition keys. If they differ, this row starts a new partition and we flush the buffer. let partition_by_len = window .deduplicated_partition_by_len .expect("deduplicated_partition_by_len must exist"); program.add_comment( program.offset(), "compare partition keys to detect new partition", ); let mut compare_key_info = (0..partition_by_len) .map(|_| KeyInfo { sort_order: SortOrder::Asc, collation: CollationSeq::default(), }) .collect::>(); for (i, c) in compare_key_info .iter_mut() .enumerate() .take(partition_by_len) { // After rewriting, partition_by entries are Expr::Column references to the // subquery. Duplicates reference the same column index, so we find the entry // that references column i (the i-th unique partition column) to get the // correct collation. let expr = window .partition_by .iter() .find(|e| matches!(e, Expr::Column { column, .. } if *column == i)) .unwrap_or(&window.partition_by[i]); let maybe_collation = get_collseq_from_expr(expr, &plan.table_references)?; c.collation = maybe_collation.unwrap_or_default(); } program.emit_insn(Insn::Compare { start_reg_a: registers.src_columns_start, start_reg_b: reg_partition_start, count: partition_by_len, key_info: compare_key_info, }); program.emit_insn(Insn::Jump { target_pc_lt: new_partition_label, target_pc_eq: same_partition_label, target_pc_gt: new_partition_label, }); program.resolve_label(new_partition_label, program.offset()); program.add_comment(program.offset(), "detected new partition"); program.emit_insn(Insn::Gosub { target_pc: labels.flush_buffer, return_reg: registers.flush_buffer_return_offset, }); // Reset rowid to signal the start of processing a new partition. program.emit_insn(Insn::Null { dest: registers.rowid, dest_end: None, }); program.emit_insn(Insn::Copy { src_reg: registers.src_columns_start, dst_reg: reg_partition_start, extra_amount: partition_by_len - 1, }); program.resolve_label(same_partition_label, program.offset()); } Ok(()) } fn emit_reset_state_if_new_partition( program: &mut ProgramBuilder, registers: &WindowRegisters, window: &Window, ) { let label_skip_reset_state = program.allocate_label(); // If rowid is null, it means we are starting a new partition. It was either set by the code // initializing window processing or by code detecting the start of a new partition. program.emit_insn(Insn::NotNull { reg: registers.rowid, target_pc: label_skip_reset_state, }); if let Some(dst_reg_start) = registers.new_order_by_columns_start { // Initialize previous ORDER BY values for the new partition. The first row of the // partition is compared to itself, not to the row from the previous partition. program.add_comment( program.offset(), "initialize previous peer register for new partition", ); program.emit_insn(Insn::Copy { src_reg: dst_reg_start, dst_reg: registers .prev_order_by_columns_start .expect("prev_order_by_columns_start must exist"), extra_amount: window.order_by.len() - 1, }); } // Since this is a new partition, we must reset accumulator registers. program.add_comment(program.offset(), "reset accumulator registers"); program.emit_insn(Insn::Null { dest: registers.acc_start, dest_end: Some(registers.acc_start + window.functions.len() - 1), }); for (i, func) in window.functions.iter().enumerate() { if matches!(func.func, WindowFunctionKind::Window(WindowFunc::RowNumber)) { program.emit_int(0, registers.acc_result_start + i); } } program.preassign_label_to_next_insn(label_skip_reset_state); } fn emit_flush_buffer_if_not_peer( program: &mut ProgramBuilder, labels: &WindowLabels, registers: &WindowRegisters, window: &Window, plan: &SelectPlan, ) -> Result<()> { if let Some(reg_new_order_by_columns_start) = registers.new_order_by_columns_start { let label_peer = program.allocate_label(); let label_not_peer = program.allocate_label(); let order_by_len = window.order_by.len(); let reg_prev_order_by_columns_start = registers .prev_order_by_columns_start .expect("prev_order_by_columns_start must exist"); program.add_comment(program.offset(), "compare ORDER BY columns to detect peer"); let mut compare_key_info = (0..window.order_by.len()) .map(|_| KeyInfo { sort_order: SortOrder::Asc, collation: CollationSeq::default(), }) .collect::>(); for (i, c) in compare_key_info .iter_mut() .enumerate() .take(window.order_by.len()) { let maybe_collation = get_collseq_from_expr(&window.order_by[i].0, &plan.table_references)?; c.collation = maybe_collation.unwrap_or_default(); } program.emit_insn(Insn::Compare { start_reg_a: reg_prev_order_by_columns_start, start_reg_b: reg_new_order_by_columns_start, count: order_by_len, key_info: compare_key_info, }); program.emit_insn(Insn::Jump { target_pc_lt: label_not_peer, target_pc_eq: label_peer, target_pc_gt: label_not_peer, }); program.resolve_label(label_not_peer, program.offset()); program.add_comment(program.offset(), "detected non-peer row"); program.emit_insn(Insn::Gosub { target_pc: labels.flush_buffer, return_reg: registers.flush_buffer_return_offset, }); program.emit_insn(Insn::Copy { src_reg: reg_new_order_by_columns_start, dst_reg: reg_prev_order_by_columns_start, extra_amount: order_by_len - 1, }); program.resolve_label(label_peer, program.offset()); } Ok(()) } fn emit_load_order_by_columns( program: &mut ProgramBuilder, window: &Window, registers: &WindowRegisters, ) { if let Some(reg_new_order_by_columns_start) = registers.new_order_by_columns_start { // Source columns are deduplicated and may appear in a different order than // the ORDER BY terms. Therefore, we must restore the original ORDER BY layout // here by copying the values into an array of registers. for (i, (expr, _)) in window.order_by.iter().enumerate() { match expr { Expr::Column { column, .. } => { program.emit_insn(Insn::Copy { src_reg: registers.src_columns_start + column, dst_reg: reg_new_order_by_columns_start + i, extra_amount: 0, }); } _ => unreachable!("expected Column, got {:?}", expr), } } } } fn emit_insert_row_into_buffer( program: &mut ProgramBuilder, registers: &WindowRegisters, cursors: &WindowCursors, input_column_count: &usize, table_name: &str, ) { let reg_record = program.alloc_register(); program.emit_insn(Insn::MakeRecord { start_reg: to_u16(registers.src_columns_start), count: to_u16(*input_column_count), dest_reg: to_u16(reg_record), index_name: None, affinity_str: None, }); program.emit_insn(Insn::NewRowid { cursor: cursors.buffer_write, rowid_reg: registers.rowid, prev_largest_reg: 0, }); program.emit_insn(Insn::Insert { cursor: cursors.buffer_write, key_reg: registers.rowid, record_reg: reg_record, flag: InsertFlags::new(), table_name: table_name.to_string(), }); } fn emit_aggregation_step( program: &mut ProgramBuilder, window: &Window, resolver: &Resolver, plan: &SelectPlan, registers: &WindowRegisters, ) -> crate::Result<()> { for (i, func) in window.functions.iter().enumerate() { let WindowFunctionKind::Agg(agg_func) = &func.func else { continue; }; // The aggregation step is performed incrementally as each row from the subquery is // processed. Therefore, we don’t need to access the buffer table and can obtain argument // values directly by evaluating the expressions that reference the subquery result columns. let args = match &func.original_expr { Expr::FunctionCall { args, .. } => args.iter().map(|a| (**a).clone()).collect(), Expr::FunctionCallStar { .. } => vec![], _ => unreachable!( "All window functions should be either FunctionCall or FunctionCallStar expressions" ), }; let reg_acc_start = registers.acc_start + i; translate_aggregation_step( program, &plan.table_references, AggArgumentSource::new_from_expression(agg_func, &args, &Distinctness::NonDistinct), reg_acc_start, resolver, )?; } Ok(()) } /// Emits bytecode to output all buffered rows produced by window processing. /// /// The generated code has two possible entry points: /// * **Fallthrough mode** (normal flow): After all source rows have been processed, /// this code executes inline to flush any remaining buffered rows, then continues execution. /// * **Subroutine mode** (jump into `labels.flush_buffer`): In this case the code /// returns control to the address stored in `registers.flush_buffer_return_offset` /// once all buffered rows are processed. pub fn emit_window_results( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, plan: &SelectPlan, ) -> crate::Result<()> { let WindowMetadata { labels, registers, cursors, .. } = t_ctx.meta_window.as_ref().expect("missing window metadata"); let window = plan.window.as_ref().expect("missing window"); let label_empty = program.allocate_label(); let label_window_processing_end = labels.window_processing_end; let reg_flush_buffer_return_offset = registers.flush_buffer_return_offset; let cursor_buffer_read = cursors.buffer_read; // All source rows have already been processed at this point. // In fallthrough mode, we are not returning to a caller — we just flush // the buffered rows and continue execution. program.add_comment(program.offset(), "return remaining buffered rows"); program.emit_insn(Insn::Null { dest: registers.flush_buffer_return_offset, dest_end: None, }); // If control jumps here (labels.flush_buffer), we are in subroutine mode. // In that case, after flushing the buffer, execution will return to the // address stored in `flush_buffer_return_offset`. program.preassign_label_to_next_insn(labels.flush_buffer); program.emit_insn(Insn::Rewind { cursor_id: cursor_buffer_read, pc_if_empty: label_empty, }); emit_return_buffered_rows(program, window, t_ctx, plan)?; program.resolve_label(label_empty, program.offset()); program.emit_insn(Insn::ResetSorter { cursor_id: cursor_buffer_read, }); program.emit_insn(Insn::Return { return_reg: reg_flush_buffer_return_offset, can_fallthrough: true, }); program.preassign_label_to_next_insn(label_window_processing_end); Ok(()) } fn emit_return_buffered_rows( program: &mut ProgramBuilder, window: &Window, t_ctx: &mut TranslateCtx, plan: &SelectPlan, ) -> crate::Result<()> { let WindowMetadata { labels, registers, cursors, expressions_referencing_subquery, .. } = t_ctx.meta_window.as_ref().expect("missing window metadata"); for (i, func) in window.functions.iter().enumerate() { if let WindowFunctionKind::Agg(agg_func) = &func.func { program.emit_insn(Insn::AggValue { acc_reg: registers.acc_start + i, dest_reg: registers.acc_result_start + i, func: agg_func.clone(), }); } } let label_skip_returning_row = program.allocate_label(); let label_loop_start = program.allocate_label(); let reg_one = window .functions .iter() .any(|func| matches!(func.func, WindowFunctionKind::Window(WindowFunc::RowNumber))) .then(|| { let reg = program.alloc_register(); program.emit_int(1, reg); reg }); program.preassign_label_to_next_insn(label_loop_start); // Propagate subquery result column values to the outer query (if any) or directly to // the final output that will be returned to the user, by copying them from the buffer table // into the dedicated registers. for (i, (_, col_idx)) in expressions_referencing_subquery.iter().enumerate() { let reg_result = registers.result_columns_start + i; program.emit_column_or_rowid(cursors.buffer_read, *col_idx, reg_result); } for (i, func) in window.functions.iter().enumerate() { if let WindowFunctionKind::Window(WindowFunc::RowNumber) = &func.func { let reg_one = reg_one.expect("row_number must allocate reg_one"); let reg_row_number = registers.acc_result_start + i; program.emit_insn(Insn::Add { lhs: reg_row_number, rhs: reg_one, dest: reg_row_number, }); } } t_ctx.resolver.enable_expr_to_reg_cache(); match plan.order_by.is_empty() { true => { emit_select_result( program, &t_ctx.resolver, plan, Some(labels.window_processing_end), Some(label_skip_returning_row), t_ctx.reg_nonagg_emit_once_flag, t_ctx.reg_offset, t_ctx.reg_result_cols_start.unwrap(), t_ctx.limit_ctx, )?; } false => { EmitOrderBy::sorter_insert(program, t_ctx, plan)?; } } program.resolve_label(label_skip_returning_row, program.offset()); if let Distinctness::Distinct { ctx } = &plan.distinctness { let distinct_ctx = ctx.as_ref().expect("distinct context must exist"); program.preassign_label_to_next_insn(distinct_ctx.label_on_conflict); } program.emit_insn(Insn::Next { cursor_id: cursors.buffer_read, pc_if_next: label_loop_start, }); Ok(()) } ================================================ FILE: core/turso_types_vtab.rs ================================================ use crate::sync::Arc; use crate::sync::RwLock; use crate::vtab::{InternalVirtualTable, InternalVirtualTableCursor}; use crate::{Connection, Result, Value}; use turso_ext::{ConstraintInfo, ConstraintUsage, IndexInfo, OrderByInfo, ResultCode}; #[derive(Debug)] pub struct TursoTypesTable; impl Default for TursoTypesTable { fn default() -> Self { Self::new() } } impl TursoTypesTable { pub fn new() -> Self { Self } } impl InternalVirtualTable for TursoTypesTable { fn name(&self) -> String { "sqlite_turso_types".to_string() } fn sql(&self) -> String { "CREATE TABLE sqlite_turso_types(name TEXT, sql TEXT)".to_string() } fn open(&self, conn: Arc) -> Result>> { let cursor = TursoTypesCursor::new(conn); Ok(Arc::new(RwLock::new(cursor))) } fn best_index( &self, constraints: &[ConstraintInfo], _order_by: &[OrderByInfo], ) -> std::result::Result { let constraint_usages = constraints .iter() .map(|_| ConstraintUsage { argv_index: None, omit: false, }) .collect(); Ok(IndexInfo { idx_num: 0, idx_str: None, order_by_consumed: false, estimated_cost: 10.0, estimated_rows: 20, constraint_usages, }) } } pub struct TursoTypesCursor { conn: Arc, /// Snapshot of type entries: (display_name, sql_string) entries: Vec<(String, String)>, index: usize, } impl TursoTypesCursor { fn new(conn: Arc) -> Self { Self { conn, entries: Vec::new(), index: 0, } } fn snapshot_types(&mut self) { self.entries.clear(); self.conn.with_schema(0, |schema| { let mut names: Vec<_> = schema .type_registry .iter() .filter(|(key, td)| *key == &td.name.to_lowercase()) .map(|(key, _)| key.clone()) .collect(); names.sort(); for name in names { let td = &schema.type_registry[&name]; let display_name = if td.params.is_empty() { td.name.clone() } else { let params: Vec = td .params .iter() .map(|p| match &p.ty { Some(ty) => format!("{} {}", p.name, ty), None => p.name.clone(), }) .collect(); format!("{}({})", td.name, params.join(", ")) }; self.entries.push((display_name, td.to_sql())); } }); } } impl InternalVirtualTableCursor for TursoTypesCursor { fn filter(&mut self, _args: &[Value], _idx_str: Option, _idx_num: i32) -> Result { self.snapshot_types(); self.index = 0; Ok(!self.entries.is_empty()) } fn next(&mut self) -> Result { self.index += 1; Ok(self.index < self.entries.len()) } fn column(&self, column: usize) -> Result { if self.index >= self.entries.len() { return Ok(Value::Null); } let (ref name, ref sql) = self.entries[self.index]; match column { 0 => Ok(Value::from_text(name.clone())), 1 => Ok(Value::from_text(sql.clone())), _ => Ok(Value::Null), } } fn rowid(&self) -> i64 { self.index as i64 + 1 } } ================================================ FILE: core/types.rs ================================================ use crate::turso_debug_assert; use branches::{mark_unlikely, unlikely}; use either::Either; use turso_ext::{AggCtx, FinalizeFunction, StepFunction}; use turso_parser::ast::SortOrder; use crate::error::LimboError; use crate::ext::{ExtValue, ExtValueType}; use crate::index_method::IndexMethodCursor; use crate::numeric::format_float; use crate::numeric::nonnan::NonNan; use crate::numeric::Numeric; use crate::pseudo::PseudoCursor; use crate::schema::Index; use crate::storage::btree::CursorTrait; use crate::storage::sqlite3_ondisk::{read_integer, read_value, read_varint, write_varint}; use crate::translate::collate::CollationSeq; use crate::translate::plan::IterationDirection; use crate::vdbe::sorter::Sorter; use crate::vdbe::Register; use crate::vtab::VirtualTableCursor; use crate::{Completion, CompletionError, Result, IO}; use std::borrow::{Borrow, Cow}; use std::cell::Cell; use std::fmt::{Debug, Display}; use std::future::Future; use std::iter::{FusedIterator, Peekable}; use std::ops::Deref; use std::task::{Poll, Waker}; /// SQLite by default uses 2000 as maximum numbers in a row. /// It controlld by the constant called SQLITE_MAX_COLUMN /// But the hard limit of number of columns is 32,767 columns i16::MAX /// const MAX_COLUMN: usize = 2000; #[derive(Debug, Clone, Copy, PartialEq)] pub enum ValueType { Null, Integer, Float, Text, Blob, Error, } impl Display for ValueType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let value = match self { Self::Null => "NULL", Self::Integer => "INT", Self::Float => "REAL", Self::Blob => "BLOB", Self::Text => "TEXT", Self::Error => "ERROR", }; write!(f, "{value}") } } #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TextSubtype { Text, #[cfg(feature = "json")] Json, } #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Text { pub value: Cow<'static, str>, pub subtype: TextSubtype, } impl Display for Text { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } impl Text { pub fn new(value: impl Into>) -> Self { Self { value: value.into(), subtype: TextSubtype::Text, } } #[cfg(feature = "json")] pub fn json(value: String) -> Self { Self { value: value.into(), subtype: TextSubtype::Json, } } pub fn as_str(&self) -> &str { &self.value } } #[derive(Debug, Clone, Copy)] pub struct TextRef<'a> { pub value: &'a str, pub subtype: TextSubtype, } impl<'a> TextRef<'a> { pub fn new(value: &'a str, subtype: TextSubtype) -> Self { Self { value, subtype } } #[inline] pub fn as_str(&self) -> &'a str { self.value } } impl<'a> Borrow for TextRef<'a> { #[inline] fn borrow(&self) -> &str { self.as_str() } } impl<'a> Deref for TextRef<'a> { type Target = str; #[inline] fn deref(&self) -> &Self::Target { self.as_str() } } pub trait Extendable { fn do_extend(&mut self, other: &T); } impl Extendable for Text { #[inline(always)] fn do_extend(&mut self, other: &T) { let other_str = other.as_ref(); match &mut self.value { Cow::Owned(s) => { let needed = other_str.len(); if s.capacity() >= needed { // SAFETY: capacity >= needed, source is valid UTF-8 turso_debug_assert!( s.as_ptr().wrapping_add(s.len()) <= other_str.as_ptr() || other_str.as_ptr().wrapping_add(other_str.len()) <= s.as_ptr(), "source and destination ranges must not overlap" ); unsafe { std::ptr::copy_nonoverlapping(other_str.as_ptr(), s.as_mut_ptr(), needed); s.as_mut_vec().set_len(needed); } } else { other_str.clone_into(s); } } Cow::Borrowed(_) => { self.value = Cow::Owned(other_str.to_owned()); } } self.subtype = other.subtype(); } } impl Extendable for Vec { #[inline(always)] fn do_extend(&mut self, other: &T) { let other_slice = other.as_slice(); let needed = other_slice.len(); if self.capacity() >= needed { // SAFETY: capacity >= needed turso_debug_assert!( self.as_ptr().wrapping_add(self.len()) <= other_slice.as_ptr() || other_slice.as_ptr().wrapping_add(other_slice.len()) <= self.as_ptr(), "source and destination ranges must not overlap" ); unsafe { std::ptr::copy_nonoverlapping(other_slice.as_ptr(), self.as_mut_ptr(), needed); self.set_len(needed); } } else { self.clear(); self.extend_from_slice(other_slice); } } } pub trait AnyText: AsRef { fn subtype(&self) -> TextSubtype; } impl AnyText for Text { fn subtype(&self) -> TextSubtype { self.subtype } } impl AnyText for &str { fn subtype(&self) -> TextSubtype { TextSubtype::Text } } pub trait AnyBlob { fn as_slice(&self) -> &[u8]; } impl AnyBlob for Vec { fn as_slice(&self) -> &[u8] { self.as_slice() } } impl AnyBlob for &[u8] { fn as_slice(&self) -> &[u8] { self } } impl AsRef for Text { fn as_ref(&self) -> &str { self.as_str() } } impl From<&str> for Text { fn from(value: &str) -> Self { Text { value: value.to_owned().into(), subtype: TextSubtype::Text, } } } impl From for Text { fn from(value: String) -> Self { Text { value: Cow::from(value), subtype: TextSubtype::Text, } } } impl From for String { fn from(value: Text) -> Self { value.value.into_owned() } } #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Value { Null, Numeric(Numeric), Text(Text), Blob(Vec), } #[derive(Clone, Copy)] pub enum ValueRef<'a> { Null, Numeric(Numeric), Text(TextRef<'a>), Blob(&'a [u8]), } impl Debug for ValueRef<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ValueRef::Null => write!(f, "Null"), ValueRef::Numeric(Numeric::Integer(i)) => f.debug_tuple("Integer").field(i).finish(), ValueRef::Numeric(Numeric::Float(float)) => { let fval: f64 = (*float).into(); f.debug_tuple("Float").field(&fval).finish() } ValueRef::Text(text_ref) => { // truncate string to at most 256 chars let text = text_ref.as_str(); let max_len = text.len().min(256); f.debug_struct("Text") .field("data", &&text[0..max_len]) // Indicates to the developer debugging that the data is truncated for printing .field("truncated", &(text.len() > max_len)) .finish() } ValueRef::Blob(blob) => { // truncate blob_slice to at most 32 bytes let max_len = blob.len().min(32); f.debug_struct("Blob") .field("data", &&blob[0..max_len]) // Indicates to the developer debugging that the data is truncated for printing .field("truncated", &(blob.len() > max_len)) .finish() } } } } pub trait AsValueRef { fn as_value_ref<'a>(&'a self) -> ValueRef<'a>; } impl<'b> AsValueRef for ValueRef<'b> { #[inline] fn as_value_ref<'a>(&'a self) -> ValueRef<'a> { *self } } impl AsValueRef for Value { #[inline] fn as_value_ref<'a>(&'a self) -> ValueRef<'a> { self.as_ref() } } impl AsValueRef for &mut Value { #[inline] fn as_value_ref<'a>(&'a self) -> ValueRef<'a> { self.as_ref() } } impl AsValueRef for Either where V1: AsValueRef, V2: AsValueRef, { #[inline] fn as_value_ref<'a>(&'a self) -> ValueRef<'a> { match self { Either::Left(left) => left.as_value_ref(), Either::Right(right) => right.as_value_ref(), } } } impl AsValueRef for &V { fn as_value_ref<'a>(&'a self) -> ValueRef<'a> { (*self).as_value_ref() } } impl Value { pub fn from_f64(f: f64) -> Self { match NonNan::new(f) { Some(nn) => Self::Numeric(Numeric::Float(nn)), None => Self::Null, } } pub fn from_i64(i: i64) -> Self { Self::Numeric(Numeric::Integer(i)) } pub fn as_ref<'a>(&'a self) -> ValueRef<'a> { match self { Value::Null => ValueRef::Null, Value::Numeric(n) => ValueRef::Numeric(*n), Value::Text(v) => ValueRef::Text(TextRef { value: &v.value, subtype: v.subtype, }), Value::Blob(v) => ValueRef::Blob(v.as_slice()), } } // A helper function that makes building a text Value easier. pub fn build_text(text: impl Into>) -> Self { Self::Text(Text::new(text)) } pub fn to_blob(&self) -> Option<&[u8]> { match self { Self::Blob(blob) => Some(blob), _ => None, } } pub fn from_blob(data: Vec) -> Self { Value::Blob(data) } pub fn to_text(&self) -> Option<&str> { match self { Value::Text(t) => Some(t.as_str()), _ => None, } } pub fn as_blob(&self) -> &Vec { match self { Value::Blob(b) => b, _ => panic!("as_blob must be called only for Value::Blob"), } } pub fn as_blob_mut(&mut self) -> &mut Vec { match self { Value::Blob(b) => b, _ => panic!("as_blob must be called only for Value::Blob"), } } pub fn as_float(&self) -> f64 { match self { Value::Numeric(Numeric::Float(f)) => f64::from(*f), Value::Numeric(Numeric::Integer(i)) => *i as f64, _ => panic!("as_float must be called only for Value::Numeric"), } } pub fn to_float_or_zero(&self) -> f64 { match self { Value::Numeric(Numeric::Float(f)) => f64::from(*f), Value::Numeric(Numeric::Integer(i)) => *i as f64, _ => 0.0, } } pub fn as_int(&self) -> Option { match self { Value::Numeric(Numeric::Integer(i)) => Some(*i), _ => None, } } pub fn as_uint(&self) -> u64 { match self { Value::Numeric(Numeric::Integer(i)) => (*i).cast_unsigned(), _ => 0, } } pub fn from_text(text: impl Into>) -> Self { Value::Text(Text::new(text)) } pub fn value_type(&self) -> ValueType { match self { Value::Null => ValueType::Null, Value::Numeric(Numeric::Integer(_)) => ValueType::Integer, Value::Numeric(Numeric::Float(_)) => ValueType::Float, Value::Text(_) => ValueType::Text, Value::Blob(_) => ValueType::Blob, } } pub fn serialize_serial(&self, out: &mut Vec) { match self { Value::Null => {} Value::Numeric(Numeric::Integer(i)) => { let serial_type = SerialType::from(self); match serial_type.kind() { SerialTypeKind::I8 => out.extend_from_slice(&(*i as i8).to_be_bytes()), SerialTypeKind::I16 => out.extend_from_slice(&(*i as i16).to_be_bytes()), SerialTypeKind::I24 => out.extend_from_slice(&(*i as i32).to_be_bytes()[1..]), // remove most significant byte SerialTypeKind::I32 => out.extend_from_slice(&(*i as i32).to_be_bytes()), SerialTypeKind::I48 => out.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes SerialTypeKind::I64 => out.extend_from_slice(&i.to_be_bytes()), _ => unreachable!(), } } Value::Numeric(Numeric::Float(f)) => { let fval: f64 = (*f).into(); out.extend_from_slice(&fval.to_be_bytes()); } Value::Text(t) => out.extend_from_slice(t.value.as_bytes()), Value::Blob(b) => out.extend_from_slice(b), }; } /// Cast Value to String, if Value is NULL returns None pub fn cast_text(&self) -> Option { Some(match self { Value::Null => return None, v => v.to_string(), }) } } #[derive(Debug, Clone, PartialEq)] pub struct ExternalAggState { pub state: *mut AggCtx, pub argc: usize, pub step_fn: StepFunction, pub finalize_fn: FinalizeFunction, } /// Please use Display trait for all limbo output so we have single origin of truth /// When you need value as string: /// ---GOOD--- /// format!("{}", value); /// ---BAD--- /// match value { /// Value::Numeric(Numeric::Integer(i)) => i.to_string(), /// Value::Numeric(Numeric::Float(f)) => f64::from(*f).to_string(), /// .... /// } impl Display for Value { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Null => write!(f, ""), Self::Numeric(Numeric::Integer(i)) => write!(f, "{i}"), Self::Numeric(Numeric::Float(fl)) => f.write_str(&format_float(f64::from(*fl))), Self::Text(s) => write!(f, "{}", s.as_str()), Self::Blob(b) => write!(f, "{}", String::from_utf8_lossy(b)), } } } impl Value { pub fn to_ffi(&self) -> ExtValue { match self { Self::Null => ExtValue::null(), Self::Numeric(Numeric::Integer(i)) => ExtValue::from_integer(*i), Self::Numeric(Numeric::Float(fl)) => ExtValue::from_float(f64::from(*fl)), Self::Text(text) => ExtValue::from_text(text.as_str().to_string()), Self::Blob(blob) => ExtValue::from_blob(blob.to_vec()), } } pub fn from_ffi(v: ExtValue) -> Result { let res = match v.value_type() { ExtValueType::Null => Ok(Value::Null), ExtValueType::Integer => { let Some(int) = v.to_integer() else { return Ok(Value::Null); }; Ok(Value::from_i64(int)) } ExtValueType::Float => { let Some(float) = v.to_float() else { return Ok(Value::Null); }; Ok(Value::from_f64(float)) } ExtValueType::Text => { let Some(text) = v.to_text() else { return Ok(Value::Null); }; #[cfg(feature = "json")] if v.is_json() { return Ok(Value::Text(Text::json(text.to_string()))); } Ok(Value::build_text(text.to_string())) } ExtValueType::Blob => { let Some(blob) = v.to_blob() else { return Ok(Value::Null); }; Ok(Value::Blob(blob)) } ExtValueType::Error => { let Some(err) = v.to_error_details() else { return Ok(Value::Null); }; match err { (_, Some(msg)) => Err(LimboError::ExtensionError(msg)), (code, None) => Err(LimboError::ExtensionError(code.to_string())), } } }; unsafe { v.__free_internal_type() }; res } } /// Convert a `Value` into the implementors type. pub trait FromValue: Sealed { fn from_sql(val: Value) -> Result where Self: Sized; } impl FromValue for Value { fn from_sql(val: Value) -> Result { Ok(val) } } impl Sealed for crate::Value {} macro_rules! impl_int_from_value { ($ty:ty, $cast:expr) => { impl FromValue for $ty { fn from_sql(val: Value) -> Result { match val { Value::Null => Err(LimboError::NullValue), Value::Numeric(Numeric::Integer(i)) => Ok($cast(i)), _ => unreachable!("invalid value type"), } } } impl Sealed for $ty {} }; } impl_int_from_value!(i32, |i| i as i32); impl_int_from_value!(u32, |i| i as u32); impl_int_from_value!(i64, |i| i); impl_int_from_value!(u64, |i| i as u64); impl FromValue for f64 { fn from_sql(val: Value) -> Result { match val { Value::Null => Err(LimboError::NullValue), Value::Numeric(Numeric::Float(f)) => Ok(f64::from(f)), _ => unreachable!("invalid value type"), } } } impl Sealed for f64 {} impl FromValue for Vec { fn from_sql(val: Value) -> Result { match val { Value::Null => Err(LimboError::NullValue), Value::Blob(blob) => Ok(blob), _ => unreachable!("invalid value type"), } } } impl Sealed for Vec {} impl FromValue for [u8; N] { fn from_sql(val: Value) -> Result { match val { Value::Null => Err(LimboError::NullValue), Value::Blob(blob) => blob.try_into().map_err(|_| LimboError::InvalidBlobSize(N)), _ => unreachable!("invalid value type"), } } } impl Sealed for [u8; N] {} impl FromValue for String { fn from_sql(val: Value) -> Result { match val { Value::Null => Err(LimboError::NullValue), Value::Text(s) => Ok(s.to_string()), _ => unreachable!("invalid value type"), } } } impl Sealed for String {} impl FromValue for bool { fn from_sql(val: Value) -> Result { match val { Value::Null => Err(LimboError::NullValue), Value::Numeric(Numeric::Integer(i)) => match i { 0 => Ok(false), 1 => Ok(true), _ => Err(LimboError::InvalidColumnType), }, _ => unreachable!("invalid value type"), } } } impl Sealed for bool {} impl FromValue for Option where T: FromValue, { fn from_sql(val: Value) -> Result { match val { Value::Null => Ok(None), _ => T::from_sql(val).map(Some), } } } impl Sealed for Option {} mod sealed { pub trait Sealed {} } use sealed::Sealed; #[derive(Debug, Clone, PartialEq)] pub struct SumAggState { pub r_err: f64, // Error term for Kahan-Babushka-Neumaier summation pub approx: bool, // True if any non-integer value was input to the sum pub ovrfl: bool, // Integer overflow seen } impl Default for SumAggState { fn default() -> Self { Self { r_err: 0.0, approx: false, ovrfl: false, } } } /// Aggregate context for accumulating values during GROUP BY. /// Built-in aggregates use a flat payload representation for efficiency and /// to share code between register-based and hash-based aggregation (future enhancement). #[derive(Debug, Clone, PartialEq)] pub enum AggContext { /// Built-in aggregates store state as a flat Vec payload. /// The layout depends on the aggregate function (see init_agg_payload). Builtin(Vec), /// External (extension) aggregates need FFI state that can't be serialized. External(ExternalAggState), } impl AggContext { pub fn compute_external(&self) -> Result { if let Self::External(ext_state) = self { let final_value = unsafe { (ext_state.finalize_fn)(ext_state.state) }; Value::from_ffi(final_value) } else { panic!("AggContext::compute_external() expected External, found {self:?}"); } } /// Get a mutable reference to the builtin payload as a slice pub fn payload_mut(&mut self) -> &mut [Value] { match self { Self::Builtin(payload) => payload, Self::External(_) => panic!("payload_mut() called on External aggregate"), } } /// Get a mutable reference to the builtin payload Vec (for aggregates that /// grow the payload, e.g. array_agg). pub fn payload_vec_mut(&mut self) -> &mut Vec { match self { Self::Builtin(payload) => payload, Self::External(_) => panic!("payload_vec_mut() called on External aggregate"), } } /// Get an immutable reference to the builtin payload pub fn payload(&self) -> &[Value] { match self { Self::Builtin(payload) => payload, Self::External(_) => panic!("payload() called on External aggregate"), } } } impl PartialEq for Value { fn eq(&self, other: &Value) -> bool { let (left, right) = (self.as_value_ref(), other.as_value_ref()); left.eq(&right) } } impl PartialOrd for Value { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl PartialOrd for AggContext { fn partial_cmp(&self, other: &AggContext) -> Option { match (self, other) { (Self::Builtin(a), Self::Builtin(b)) => { // Compare by first element (the accumulator) if present match (a.first(), b.first()) { (Some(a), Some(b)) => a.partial_cmp(b), _ => None, } } _ => None, } } } impl Eq for Value {} impl Ord for Value { fn cmp(&self, other: &Self) -> std::cmp::Ordering { let (left, right) = (self.as_value_ref(), other.as_value_ref()); left.cmp(&right) } } impl std::ops::Add for Value { type Output = Value; fn add(mut self, rhs: Self) -> Self::Output { self += rhs; self } } impl std::ops::Add for Value { type Output = Value; fn add(mut self, rhs: f64) -> Self::Output { self += rhs; self } } impl std::ops::Add for Value { type Output = Value; fn add(mut self, rhs: i64) -> Self::Output { self += rhs; self } } impl std::ops::AddAssign for Value { fn add_assign(mut self: &mut Self, rhs: Self) { match (&mut self, &rhs) { (Self::Numeric(_), Self::Numeric(_)) => { let sum = (|| { let lhs_num = Numeric::from_value(&self)?; let rhs_num = Numeric::from_value(&rhs)?; lhs_num.checked_add(rhs_num) })(); *self = sum.into(); } (Self::Text(string_left), Self::Text(string_right)) => { string_left.value.to_mut().push_str(&string_right.value); string_left.subtype = TextSubtype::Text; } (Self::Text(string_left), Self::Numeric(Numeric::Integer(int_right))) => { let string_right = int_right.to_string(); string_left.value.to_mut().push_str(&string_right); string_left.subtype = TextSubtype::Text; } (Self::Numeric(Numeric::Integer(int_left)), Self::Text(string_right)) => { let string_left = int_left.to_string(); *self = Self::build_text(string_left + string_right.as_str()); } (Self::Text(string_left), Self::Numeric(Numeric::Float(_))) => { let string_right = rhs.to_string(); string_left.value.to_mut().push_str(&string_right); string_left.subtype = TextSubtype::Text; } (Self::Numeric(Numeric::Float(_)), Self::Text(string_right)) => { let string_left = self.to_string(); *self = Self::build_text(string_left + string_right.as_str()); } (_, Self::Null) => {} (Self::Null, _) => *self = rhs, _ => *self = Self::from_f64(0.0), } } } impl std::ops::AddAssign for Value { fn add_assign(&mut self, rhs: i64) { let sum = (|| { let lhs_num = Numeric::from_value(&self)?; let rhs_num = Numeric::Integer(rhs); lhs_num.checked_add(rhs_num) })(); *self = sum.into(); } } impl std::ops::AddAssign for Value { fn add_assign(&mut self, rhs: f64) { let sum = (|| { let lhs_num = Numeric::from_value(&self)?; let rhs_num = NonNan::new(rhs).map(Numeric::Float)?; lhs_num.checked_add(rhs_num) })(); *self = sum.into(); } } impl std::ops::Div for Value { type Output = Value; fn div(self, rhs: Value) -> Self::Output { let div = (|| { let lhs_num = Numeric::from_value(self)?; let rhs_num = Numeric::from_value(rhs)?; lhs_num.checked_div(rhs_num) })(); div.into() } } impl std::ops::DivAssign for Value { fn div_assign(&mut self, rhs: Value) { *self = self.clone() / rhs; } } impl TryFrom> for i64 { type Error = LimboError; fn try_from(value: ValueRef<'_>) -> Result { match value { ValueRef::Numeric(Numeric::Integer(i)) => Ok(i), _ => Err(LimboError::ConversionError("Expected integer value".into())), } } } impl TryFrom> for String { type Error = LimboError; #[inline] fn try_from(value: ValueRef<'_>) -> Result { Ok(<&str>::try_from(value)?.to_string()) } } impl<'a> TryFrom> for &'a str { type Error = LimboError; #[inline] fn try_from(value: ValueRef<'a>) -> Result { match value { ValueRef::Text(s) => Ok(s.as_str()), _ => Err(LimboError::ConversionError("Expected text value".into())), } } } /// This struct serves the purpose of not allocating multiple vectors of bytes if not needed. /// A value in a record that has already been serialized can stay serialized and what this struct offsers /// is easy acces to each value which point to the payload. /// The name might be contradictory as it is immutable in the sense that you cannot modify the values without modifying the payload. pub struct ImmutableRecord { // We have to be super careful with this buffer since we make values point to the payload we need to take care reallocations // happen in a controlled manner. If we realocate with values that should be correct, they will now point to undefined data. // We don't use pin here because it would make it imposible to reuse the buffer if we need to push a new record in the same struct. // // payload is the Vec but in order to use Register which holds ImmutableRecord as a Value - we store Vec as Value::Blob payload: Value, } // SAFETY: all ImmutableRecord instances are intended to be used in a single thread // by a single connection. unsafe impl Send for ImmutableRecord {} unsafe impl Sync for ImmutableRecord {} impl Clone for ImmutableRecord { fn clone(&self) -> Self { Self { payload: self.payload.clone(), } } } impl PartialEq for ImmutableRecord { fn eq(&self, other: &Self) -> bool { self.payload == other.payload // Only compare payload, ignore cursor state } } impl Eq for ImmutableRecord {} impl PartialOrd for ImmutableRecord { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for ImmutableRecord { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.payload.cmp(&other.payload) // Only compare payload, ignore cursor state } } impl std::fmt::Debug for ImmutableRecord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.payload { Value::Blob(bytes) => { let preview = if bytes.len() > 20 { format!("{:?} ... ({} bytes total)", &bytes[..20], bytes.len()) } else { format!("{bytes:?}") }; write!(f, "ImmutableRecord {{ payload: {preview} }}") } Value::Text(s) => { let string = s.as_str(); let preview = if string.len() > 20 { format!("{:?} ... ({} chars total)", &string[..20], string.len()) } else { format!("{string:?}") }; write!(f, "ImmutableRecord {{ payload: {preview} }}") } other => write!(f, "ImmutableRecord {{ payload: {other:?} }}"), } } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Record { values: Vec, } impl Record { // pub fn get<'a, T: FromValue<'a> + 'a>(&'a self, idx: usize) -> Result { // let value = &self.values[idx]; // T::from_value(value) // } pub fn count(&self) -> usize { self.values.len() } pub fn last_value(&self) -> Option<&Value> { self.values.last() } pub fn get_values(&self) -> &Vec { &self.values } pub fn get_value(&self, idx: usize) -> &Value { &self.values[idx] } pub fn len(&self) -> usize { self.values.len() } pub fn is_empty(&self) -> bool { self.values.is_empty() } } struct AppendWriter<'a> { buf: &'a mut Vec, pos: usize, buf_capacity_start: usize, buf_ptr_start: *const u8, } impl<'a> AppendWriter<'a> { pub fn new(buf: &'a mut Vec, pos: usize) -> Self { let buf_ptr_start = buf.as_ptr(); let buf_capacity_start = buf.capacity(); Self { buf, pos, buf_capacity_start, buf_ptr_start, } } #[inline] pub fn extend_from_slice(&mut self, slice: &[u8]) { self.buf[self.pos..self.pos + slice.len()].copy_from_slice(slice); self.pos += slice.len(); } fn assert_finish_capacity(&self) { // let's make sure we didn't reallocate anywhere else assert_eq!(self.buf_capacity_start, self.buf.capacity()); assert_eq!(self.buf_ptr_start, self.buf.as_ptr()); } } impl ImmutableRecord { pub fn new(payload_capacity: usize) -> Self { Self { payload: Value::Blob(Vec::with_capacity(payload_capacity)), } } pub fn from_bin_record(payload: Vec) -> Self { Self { payload: Value::Blob(payload), } } // Don't use this in performance critical paths, prefer using `iter()` instead pub fn get_values(&self) -> Result>> { let iter = self.iter()?; let mut values = Vec::with_capacity(iter.size_hint().0); for value in iter { values.push(value?); } Ok(values) } // Don't use this in performance critical paths, prefer using `iter()` instead pub fn get_values_range(&self, range: std::ops::Range) -> Result>> { let mut iter = self.iter()?; let mut values = Vec::with_capacity(range.end - range.start); // advance to start if let Some(value) = iter.nth(range.start) { values.push(value?); } else { return Ok(values); } // collect rest for _ in range.start + 1..range.end { if let Some(value) = iter.next() { values.push(value?); } else { break; } } Ok(values) } // Idx values must be sorted ascending pub fn get_two_values(&self, idx1: usize, idx2: usize) -> Result<(ValueRef<'_>, ValueRef<'_>)> { let mut iter = self.iter()?; let val1 = iter.nth(idx1); let val2 = iter.nth(idx2 - idx1 - 1); // idx2 - idx1 - 1 because we already advanced to idx1 match (val1, val2) { (Some(v1), Some(v2)) => Ok((v1?, v2?)), _ => Err(LimboError::InternalError("index out of bound".to_string())), } } // Idx values must be sorted ascending pub fn get_three_values( &self, idx1: usize, idx2: usize, idx3: usize, ) -> Result<(ValueRef<'_>, ValueRef<'_>, ValueRef<'_>)> { let mut iter = self.iter()?; let val1 = iter.nth(idx1); let val2 = iter.nth(idx2 - idx1 - 1); // idx2 - idx1 - 1 because we already advanced to idx1 let val3 = iter.nth(idx3 - idx2 - 1); // idx3 - idx2 - 1 because we already advanced to idx2 match (val1, val2, val3) { (Some(v1), Some(v2), Some(v3)) => Ok((v1?, v2?, v3?)), _ => Err(LimboError::InternalError("index out of bound".to_string())), } } // Idx values must be sorted ascending pub fn get_four_values( &self, idx1: usize, idx2: usize, idx3: usize, idx4: usize, ) -> Result<(ValueRef<'_>, ValueRef<'_>, ValueRef<'_>, ValueRef<'_>)> { let mut iter = self.iter()?; let val1 = iter.nth(idx1); let val2 = iter.nth(idx2 - idx1 - 1); // idx2 - idx1 - 1 because we already advanced to idx1 let val3 = iter.nth(idx3 - idx2 - 1); // idx3 - idx2 - 1 because we already advanced to idx2 let val4 = iter.nth(idx4 - idx3 - 1); // idx4 - idx3 - 1 because we already advanced to idx3 match (val1, val2, val3, val4) { (Some(v1), Some(v2), Some(v3), Some(v4)) => Ok((v1?, v2?, v3?, v4?)), _ => Err(LimboError::InternalError("index out of bound".to_string())), } } // Don't use this in performance critical paths, prefer using `iter()` instead pub fn get_values_owned(&self) -> Result> { let iter = self.iter().expect("Failed to create payload iterator"); let mut values = Vec::with_capacity(iter.size_hint().0); for value in iter { values.push(value?.to_owned()); } Ok(values) } // Don't use this in performance critical paths, prefer using `iter()` instead pub fn get_values_owned_range(&self, range: std::ops::Range) -> Result> { let mut iter = self.iter().expect("Failed to create payload iterator"); let mut values = Vec::with_capacity(range.end - range.start); // advance to start if let Some(value) = iter.nth(range.start) { values.push(value?.to_owned()); } else { return Ok(values); } // collect rest for _ in range.start + 1..range.end { if let Some(value) = iter.next() { values.push(value?.to_owned()); } else { break; } } Ok(values) } pub fn from_registers<'a, I: Iterator + Clone>( // we need to accept both &[Register] and &[&Register] values - that's why non-trivial signature // // std::slice::Iter under the hood just stores pointer and length of slice and also implements a Clone which just copy those meta-values // (without copying the data itself) registers: impl IntoIterator, len: usize, ) -> Self { Self::from_values(registers.into_iter().map(|x| x.get_value()), len) } pub fn from_values<'a>( values: impl IntoIterator + Clone, len: usize, ) -> Self { let mut serials = Vec::with_capacity(len); let mut size_header = 0; let mut size_values = 0; let mut serial_type_buf = [0; 9]; // write serial types for value in values.clone() { let serial_type = SerialType::from(value.as_value_ref()); let n = write_varint(&mut serial_type_buf[0..], serial_type.into()); serials.push((serial_type_buf, n)); let value_size = serial_type.size(); size_header += n; size_values += value_size; } let header_size = Record::calc_header_size(size_header); // 1. write header size let mut buf = Vec::new(); buf.reserve_exact(header_size + size_values); assert_eq!(buf.capacity(), header_size + size_values); let n = write_varint(&mut serial_type_buf, header_size as u64); buf.resize(buf.capacity(), 0); let mut writer = AppendWriter::new(&mut buf, 0); writer.extend_from_slice(&serial_type_buf[..n]); // 2. Write serial for (value, n) in serials { writer.extend_from_slice(&value[..n]); } // write content for value in values { let value = value.as_value_ref(); match value { ValueRef::Null => {} ValueRef::Numeric(Numeric::Integer(i)) => { let serial_type = SerialType::from(value); match serial_type.kind() { SerialTypeKind::ConstInt0 | SerialTypeKind::ConstInt1 => {} SerialTypeKind::I8 => writer.extend_from_slice(&(i as i8).to_be_bytes()), SerialTypeKind::I16 => writer.extend_from_slice(&(i as i16).to_be_bytes()), SerialTypeKind::I24 => { writer.extend_from_slice(&(i as i32).to_be_bytes()[1..]) } // remove most significant byte SerialTypeKind::I32 => writer.extend_from_slice(&(i as i32).to_be_bytes()), SerialTypeKind::I48 => writer.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes SerialTypeKind::I64 => writer.extend_from_slice(&i.to_be_bytes()), other => panic!("Serial type is not an integer: {other:?}"), } } ValueRef::Numeric(Numeric::Float(f)) => { let fval: f64 = f.into(); writer.extend_from_slice(&fval.to_be_bytes()); } ValueRef::Text(t) => { writer.extend_from_slice(t.value.as_bytes()); } ValueRef::Blob(b) => { writer.extend_from_slice(b); } }; } writer.assert_finish_capacity(); Self { payload: Value::Blob(buf), } } #[inline] pub fn into_payload(self) -> Vec { match self.payload { Value::Blob(b) => b, _ => panic!("payload must be a blob"), } } #[inline] pub fn as_blob(&self) -> &Vec { match &self.payload { Value::Blob(b) => b, _ => panic!("payload must be a blob"), } } #[inline] pub fn as_blob_mut(&mut self) -> &mut Vec { match &mut self.payload { Value::Blob(b) => b, _ => panic!("payload must be a blob"), } } #[inline] pub fn as_blob_value(&self) -> &Value { &self.payload } #[inline] pub fn start_serialization(&mut self, payload: &[u8]) { self.as_blob_mut().extend_from_slice(payload); } #[inline] pub fn invalidate(&mut self) { self.as_blob_mut().clear(); } #[inline] pub fn is_invalidated(&self) -> bool { self.as_blob().is_empty() } #[inline] pub fn get_payload(&self) -> &[u8] { self.as_blob() } #[inline(always)] pub fn iter(&self) -> Result, LimboError> { ValueIterator::new(self.get_payload()) } #[inline] /// Returns true if the record contains any NULL values. /// This is an optimization that only examines the header (serial types) /// without deserializing the data section. pub fn contains_null(&self) -> Result { let payload = self.get_payload(); let (header_size, header_varint_len) = read_varint(payload)?; let header_size = header_size as usize; if header_size > payload.len() || header_varint_len > payload.len() { return Err(LimboError::Corrupt( "Payload too small for indicated header size".into(), )); } let mut header = &payload[header_varint_len..header_size]; while !header.is_empty() { let (serial_type, bytes_read) = read_varint(header)?; if serial_type == 0 { return Ok(true); } header = &header[bytes_read..]; } Ok(false) } #[inline] pub fn last_value(&self) -> Option>> { if unlikely(self.is_invalidated()) { return Some(Err(LimboError::InternalError( "Record is invalidated".into(), ))); } let iter = match self.iter() { Ok(it) => it, Err(e) => return Some(Err(e)), }; iter.last() } #[inline] pub fn first_value(&self) -> Result> { if unlikely(self.is_invalidated()) { return Err(LimboError::InternalError("Record is invalidated".into())); } match self.iter()?.next() { Some(v) => v, None => Err(LimboError::InternalError("Record has no columns".into())), } } #[inline] pub fn get_value(&self, idx: usize) -> Result> { if unlikely(self.is_invalidated()) { return Err(LimboError::InternalError("Record is invalidated".into())); } let mut iter = self.iter()?; iter.nth(idx) .transpose()? .ok_or_else(|| LimboError::InternalError("Index out of bounds".into())) } #[inline] pub fn get_value_opt(&self, idx: usize) -> Option> { let mut iter = match self.iter() { Ok(it) => it, Err(_) => { mark_unlikely(); return None; } }; match iter.nth(idx) { Some(Ok(v)) => Some(v), _ => { mark_unlikely(); None } } } pub fn column_count(&self) -> usize { self.iter().map(|it| it.count()).unwrap_or_default() } } /// A zero-allocation iterator over SQLite record payload data. /// /// This iterator provides efficient, lazy parsing of SQLite records without /// any heap allocation. It processes record data on-the-fly, returning `ValueRef` /// instances that borrow directly from the underlying payload. /// /// # Memory Layout /// /// SQLite records follow this binary format: /// ```text /// [header_size: varint][serial_type1: varint][serial_type2: varint]... /// [data1][data2][data3]... /// ``` /// /// - **header_size**: Total bytes in the header section (including this varint) /// - **serial_typeN**: Encodes the type and size of column N's data /// - **dataN**: The actual data for column N (length determined by serial_typeN) pub struct ValueIterator<'a> { /// Reference to header section up to data offset header_section: Cell<&'a [u8]>, /// Reference to data section only data_section: Cell<&'a [u8]>, } impl<'a> ValueIterator<'a> { /// Creates a new payload iterator from a raw payload slice. /// /// # Arguments /// /// * `payload` - The serialized SQLite record payload /// /// # Returns /// /// Returns `Ok(Self)` if the header can be parsed, or an error if the /// payload is malformed. #[inline(always)] pub fn new(payload: &'a [u8]) -> Result { let (header_size, header_varint_len) = read_varint(payload)?; let header_size = header_size as usize; if header_size > payload.len() || header_varint_len > payload.len() || header_varint_len > header_size { return Err(LimboError::Corrupt( "Payload too small for indicated header size".into(), )); } Ok(Self { header_section: Cell::new(&payload[header_varint_len..header_size]), data_section: Cell::new(&payload[header_size..]), }) } /// Returns `true` if the payload is empty or the record has no columns. pub fn is_empty(&self) -> bool { self.header_section.get().is_empty() } /// Returns a reference to the current header section. #[inline(always)] pub fn header_section_ref(&self) -> &'a [u8] { self.header_section.get() } /// Returns a reference to the current data section. #[inline(always)] pub fn data_section_ref(&self) -> &'a [u8] { self.data_section.get() } /// Sets the header section to a new slice. #[inline(always)] pub fn set_header_section(&self, header: &'a [u8]) { self.header_section.set(header); } /// Sets the data section to a new slice. #[inline(always)] pub fn set_data_section(&self, data: &'a [u8]) { self.data_section.set(data); } } impl<'a> Iterator for ValueIterator<'a> { type Item = Result, LimboError>; #[inline(always)] fn count(self) -> usize where Self: Sized, { let mut count = 0; let mut header = self.header_section.get(); while !header.is_empty() { match read_varint(header) { Ok((_, bytes_read)) => { count += 1; header = &header[bytes_read..]; } Err(_) => break, } } count } #[inline(always)] fn size_hint(&self) -> (usize, Option) { let mut count = 0; let mut header = self.header_section.get(); while !header.is_empty() { match read_varint(header) { Ok((_, bytes_read)) => { count += 1; header = &header[bytes_read..]; } Err(_) => break, } } (count, Some(count)) } fn fold(self, init: B, mut f: F) -> B where F: FnMut(B, Self::Item) -> B, { let mut acc = init; for item in self { acc = f(acc, item); } acc } /// Returns the nth element of the iterator. #[inline(always)] fn nth(&mut self, n: usize) -> Option { let mut header = self.header_section.get(); let mut data = self.data_section.get(); let mut data_sum = 0; for _ in 0..n { if unlikely(header.is_empty()) { return None; } let (serial_type, bytes_read) = match read_varint(header) { Ok(v) => v, Err(e) => { mark_unlikely(); return Some(Err(e)); } }; header = &header[bytes_read..]; data_sum += match get_serial_type_size(serial_type) { Ok(size) => size, Err(e) => { mark_unlikely(); return Some(Err(e)); } }; } if unlikely(data_sum > data.len()) { return Some(Err(LimboError::Corrupt( "Data section too small for indicated serial type size".into(), ))); } data = &data[data_sum..]; // Update iterator state self.header_section.set(header); self.data_section.set(data); // Return the nth value self.next() } #[inline(always)] fn next(&mut self) -> Option { let header = self.header_section.get(); if unlikely(header.is_empty()) { return None; } // Read next serial type let (serial_type, bytes_read) = match read_varint(header) { Ok(v) => v, Err(e) => { mark_unlikely(); return Some(Err(e)); } }; // Update header section to remove the consumed serial type self.header_section.set(&header[bytes_read..]); let data_section = self.data_section.get(); match crate::storage::sqlite3_ondisk::read_value_serial_type(data_section, serial_type) { Ok((value, n)) => { self.data_section.set(&data_section[n..]); Some(Ok(value)) } Err(e) => { mark_unlikely(); Some(Err(e)) } } } } // Optimization: indicate that once the iterator is exhausted, it will always return None. impl<'a> FusedIterator for ValueIterator<'a> {} impl<'a> Clone for ValueIterator<'a> { fn clone(&self) -> Self { Self { header_section: Cell::new(self.header_section.get()), data_section: Cell::new(self.data_section.get()), } } } impl<'a> ValueRef<'a> { pub fn from_f64(f: f64) -> Self { match NonNan::new(f) { Some(nn) => Self::Numeric(Numeric::Float(nn)), None => Self::Null, } } pub fn from_i64(i: i64) -> Self { Self::Numeric(Numeric::Integer(i)) } pub fn to_ffi(&self) -> ExtValue { match self { Self::Null => ExtValue::null(), Self::Numeric(Numeric::Integer(i)) => ExtValue::from_integer(*i), Self::Numeric(Numeric::Float(fl)) => ExtValue::from_float(f64::from(*fl)), Self::Text(text) => ExtValue::from_text(text.as_str().to_string()), Self::Blob(blob) => ExtValue::from_blob(blob.to_vec()), } } pub fn to_blob(&self) -> Option<&'a [u8]> { match self { Self::Blob(blob) => Some(*blob), _ => None, } } pub fn to_text(&self) -> Option<&'a str> { match self { Self::Text(t) => Some(t.as_str()), _ => None, } } pub fn as_blob(&self) -> &'a [u8] { match self { Self::Blob(b) => b, _ => panic!("as_blob must be called only for Value::Blob"), } } pub fn as_float(&self) -> f64 { match self { Self::Numeric(Numeric::Float(f)) => f64::from(*f), Self::Numeric(Numeric::Integer(i)) => *i as f64, _ => panic!("as_float must be called only for ValueRef::Numeric"), } } pub fn as_int(&self) -> Option { match self { Self::Numeric(Numeric::Integer(i)) => Some(*i), _ => None, } } pub fn as_uint(&self) -> u64 { match self { Self::Numeric(Numeric::Integer(i)) => (*i).cast_unsigned(), _ => 0, } } #[inline] pub fn to_owned(&self) -> Value { match self { ValueRef::Null => Value::Null, ValueRef::Numeric(n) => Value::from(*n), ValueRef::Text(text) => Value::Text(Text { value: text.value.to_string().into(), subtype: text.subtype, }), ValueRef::Blob(b) => Value::Blob(b.to_vec()), } } pub fn value_type(&self) -> ValueType { match self { Self::Null => ValueType::Null, Self::Numeric(Numeric::Integer(_)) => ValueType::Integer, Self::Numeric(Numeric::Float(_)) => ValueType::Float, Self::Text(_) => ValueType::Text, Self::Blob(_) => ValueType::Blob, } } } impl Display for ValueRef<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Null => write!(f, "NULL"), Self::Numeric(Numeric::Integer(i)) => write!(f, "{i}"), Self::Numeric(Numeric::Float(fl)) => { let fval: f64 = (*fl).into(); write!(f, "{fval:?}") } Self::Text(s) => write!(f, "{}", s.as_str()), Self::Blob(b) => write!(f, "{}", String::from_utf8_lossy(b)), } } } impl<'a> PartialEq> for ValueRef<'a> { fn eq(&self, other: &ValueRef<'a>) -> bool { match (self, other) { (Self::Null, Self::Null) => true, (Self::Numeric(a), Self::Numeric(b)) => a == b, (Self::Text(text_left), Self::Text(text_right)) => { text_left.value.as_bytes() == text_right.value.as_bytes() } (Self::Blob(blob_left), Self::Blob(blob_right)) => blob_left.eq(blob_right), _ => false, } } } impl<'a> PartialEq for ValueRef<'a> { fn eq(&self, other: &Value) -> bool { let other = other.as_value_ref(); self.eq(&other) } } impl<'a> Eq for ValueRef<'a> {} impl<'a> PartialOrd> for ValueRef<'a> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl<'a> Ord for ValueRef<'a> { fn cmp(&self, other: &Self) -> std::cmp::Ordering { match (self, other) { (Self::Null, Self::Null) => std::cmp::Ordering::Equal, (Self::Null, _) => std::cmp::Ordering::Less, (_, Self::Null) => std::cmp::Ordering::Greater, (Self::Numeric(a), Self::Numeric(b)) => a.cmp(b), // Numeric < Text < Blob (Self::Numeric(_), _) => std::cmp::Ordering::Less, (_, Self::Numeric(_)) => std::cmp::Ordering::Greater, (Self::Text(text_left), Self::Text(text_right)) => { text_left.value.as_bytes().cmp(text_right.value.as_bytes()) } (Self::Text(_), Self::Blob(_)) => std::cmp::Ordering::Less, (Self::Blob(_), Self::Text(_)) => std::cmp::Ordering::Greater, (Self::Blob(blob_left), Self::Blob(blob_right)) => blob_left.cmp(blob_right), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct KeyInfo { pub sort_order: SortOrder, pub collation: CollationSeq, } #[derive(Debug, Clone, PartialEq, Eq)] /// Metadata about an index, used for handling and comparing index keys. /// /// This struct provides information about the sorting order of columns, /// whether the index includes a row ID, and the total number of columns /// in the index. pub struct IndexInfo { /// Specifies the sorting order (ascending or descending) for each column in the index. pub key_info: Vec, /// Indicates whether the index includes a row ID column. pub has_rowid: bool, /// The total number of columns in the index, including the row ID column if present. pub num_cols: usize, /// Indicates whether index rows should be unique. pub is_unique: bool, } impl Default for IndexInfo { fn default() -> Self { Self { key_info: vec![], has_rowid: true, num_cols: 1, is_unique: false, } } } impl IndexInfo { pub fn new_from_index(index: &Index) -> Self { Self { key_info: { let mut key_info: Vec = index .columns .iter() .map(|c| KeyInfo { sort_order: c.order, collation: c.collation.unwrap_or_default(), }) .collect(); if index.has_rowid { key_info.push(KeyInfo { sort_order: SortOrder::Asc, collation: CollationSeq::Binary, }); } key_info }, has_rowid: index.has_rowid, num_cols: index.columns.len() + (index.has_rowid as usize), is_unique: index.unique, } } } pub fn compare_immutable( l: I1, r: I2, column_info: &[KeyInfo], ) -> std::cmp::Ordering where V1: AsValueRef, V2: AsValueRef, E1: ExactSizeIterator, E2: ExactSizeIterator, I1: IntoIterator, I2: IntoIterator, { let (l, r): (E1, E2) = (l.into_iter(), r.into_iter()); assert!( l.len() >= column_info.len(), "{} < {}", l.len(), column_info.len() ); assert!( r.len() >= column_info.len(), "{} < {}", r.len(), column_info.len() ); let (l, r) = (l.take(column_info.len()), r.take(column_info.len())); for (i, (l, r)) in l.zip(r).enumerate() { let column_order = column_info[i].sort_order; let collation = column_info[i].collation; let cmp = compare_immutable_single(l, r, collation); if !cmp.is_eq() { return match column_order { SortOrder::Asc => cmp, SortOrder::Desc => cmp.reverse(), }; } } std::cmp::Ordering::Equal } pub fn compare_immutable_iter( mut l: E1, mut r: E2, column_info: &[KeyInfo], ) -> Result where V: AsValueRef, E1: Iterator>, E2: Iterator>, { for col_info in column_info.iter() { let l = match l.next() { Some(v) => v, None => break, }; let r = match r.next() { Some(v) => v, None => break, }; let column_order = col_info.sort_order; let collation = col_info.collation; let cmp = compare_immutable_single(l?, r?, collation); if !cmp.is_eq() { return match column_order { SortOrder::Asc => Ok(cmp), SortOrder::Desc => Ok(cmp.reverse()), }; } } Ok(std::cmp::Ordering::Equal) } pub fn compare_immutable_single(l: V1, r: V2, collation: CollationSeq) -> std::cmp::Ordering where V1: AsValueRef, V2: AsValueRef, { let l = l.as_value_ref(); let r = r.as_value_ref(); match (l, r) { (ValueRef::Text(left), ValueRef::Text(right)) => collation.compare_strings(&left, &right), _ => l.cmp(&r), } } #[derive(Debug, Clone, Copy)] pub enum RecordCompare { Int, String, Generic, } impl RecordCompare { pub fn compare( &self, serialized: &ImmutableRecord, unpacked: I, index_info: &IndexInfo, skip: usize, tie_breaker: std::cmp::Ordering, ) -> Result where V: AsValueRef, E: ExactSizeIterator, I: IntoIterator, { let unpacked = unpacked.into_iter(); match self { RecordCompare::Int => { compare_records_int(serialized, unpacked, index_info, tie_breaker) } RecordCompare::String => { compare_records_string(serialized, unpacked, index_info, tie_breaker) } RecordCompare::Generic => { compare_records_generic(serialized, unpacked, index_info, skip, tie_breaker) } } } } pub fn find_compare(unpacked: I, index_info: &IndexInfo) -> RecordCompare where V: AsValueRef, E: ExactSizeIterator, I: IntoIterator, Item = V>, { let mut unpacked = unpacked.into_iter(); if unpacked.len() != 0 && index_info.num_cols <= 13 { let val = unpacked.peek().unwrap(); match val.as_value_ref() { ValueRef::Numeric(Numeric::Integer(_)) => RecordCompare::Int, ValueRef::Text(_) if index_info.key_info[0].collation == CollationSeq::Binary => { RecordCompare::String } _ => RecordCompare::Generic, } } else { RecordCompare::Generic } } pub fn get_tie_breaker_from_seek_op(seek_op: SeekOp) -> std::cmp::Ordering { match seek_op { // exact‐match “key == X” opcodes SeekOp::GE { eq_only: true } | SeekOp::LE { eq_only: true } => std::cmp::Ordering::Equal, // forward search – want the *first* ≥ / > key SeekOp::GE { eq_only: false } => std::cmp::Ordering::Greater, SeekOp::GT => std::cmp::Ordering::Less, // backward search – want the *last* ≤ / < key SeekOp::LE { eq_only: false } => std::cmp::Ordering::Less, SeekOp::LT => std::cmp::Ordering::Greater, } } /// Optimized integer-first record comparison function. /// /// This function is an optimized version of `compare_records_generic()` for the /// common case where: /// - (a) The first field of the unpacked record is an integer /// - (b) The serialized record's first field is also an integer /// - (c) The header size varint fits in a single byte and is ≤ 63 bytes /// /// The 63-byte header limit prevents buffer overreads and ensures safe direct /// memory access patterns. This optimization avoids generic parsing overhead /// by directly extracting and comparing integer values using known layouts. /// /// # Fast Path Conditions /// /// The function uses the optimized path when ALL of these conditions are met: /// - Payload is at least 2 bytes (header size + first serial type) /// - First serial type indicates integer (`1-6`, `8`, or `9`) /// - First unpacked field is a `ValueRef::Numeric(Numeric::Integer)` /// /// If any condition fails, it falls back to `compare_records_generic()`. /// /// # Arguments /// /// * `serialized` - The left-hand side record in serialized format /// * `unpacked` - The right-hand side record as an array of parsed values /// * `index_info` - Contains sort order information for each field /// * `collations` - Array of collation sequences (unused for integers) /// * `tie_breaker` - Result to return when all compared fields are equal /// /// /// # Comparison Logic /// /// The function follows optimized integer comparison semantics: /// /// 1. **Type validation**: Ensures both sides are integers, otherwise falls back /// 2. **Direct extraction**: Reads integer value using specialized decoder /// 3. **Native comparison**: Uses Rust's built-in `i64::cmp()` for speed /// 4. **Sort order**: Applies ascending/descending order to comparison result /// 5. **Remaining fields**: If first field is equal and more fields exist, /// delegates to `compare_records_generic()` with `skip=1` fn compare_records_int( serialized: &ImmutableRecord, unpacked: I, index_info: &IndexInfo, tie_breaker: std::cmp::Ordering, ) -> Result where V: AsValueRef, I: ExactSizeIterator, { let payload = serialized.get_payload(); if payload.len() < 2 { return compare_records_generic(serialized, unpacked, index_info, 0, tie_breaker); } let (header_size, offset_1st_serialtype) = read_varint(payload)?; let header_size = header_size as usize; if payload.len() < header_size { return Err(LimboError::Corrupt(format!( "Record payload too short: claimed header size {} but payload only {} bytes", header_size, payload.len() ))); } let (first_serial_type, _) = read_varint(&payload[offset_1st_serialtype..])?; let serialtype_is_integer = matches!(first_serial_type, 1..=6 | 8 | 9); if !serialtype_is_integer { return compare_records_generic(serialized, unpacked, index_info, 0, tie_breaker); } let data_start = header_size; let lhs_int = read_integer(&payload[data_start..], first_serial_type as u8)?; let mut unpacked = unpacked.peekable(); // Do not consume iterator here let ValueRef::Numeric(Numeric::Integer(rhs_int)) = unpacked.peek().unwrap().as_value_ref() else { return compare_records_generic(serialized, unpacked, index_info, 0, tie_breaker); }; let comparison = match index_info.key_info[0].sort_order { SortOrder::Asc => lhs_int.cmp(&rhs_int), SortOrder::Desc => lhs_int.cmp(&rhs_int).reverse(), }; match comparison { std::cmp::Ordering::Equal => { // First fields equal, compare remaining fields if any if unpacked.len() > 1 { return compare_records_generic(serialized, unpacked, index_info, 1, tie_breaker); } Ok(tie_breaker) } other => Ok(other), } } /// This function is an optimized version of `compare_records_generic()` for the /// common case where: /// - (a) The first field of the unpacked record is a string /// - (b) The serialized record's first field is also a string /// - (c) The header size varint fits in a single byte (most records) /// /// This optimization avoids the overhead of generic field parsing by directly /// accessing the first string field using known offsets, then falling back to /// the generic comparison for remaining fields if needed. /// /// # Fast Path Conditions /// /// The function uses the optimized path when ALL of these conditions are met: /// - Payload is at least 2 bytes (header size + first serial type) /// - Header size fits in single byte (`payload[0] < 0x80`) /// - First serial type indicates string (`>= 13` and odd number) /// - First unpacked field is a `RefValue::Text` /// /// If any condition fails, it falls back to `compare_records_generic()`. /// /// # Arguments /// /// * `serialized` - The left-hand side record in serialized format /// * `unpacked` - The right-hand side record as an array of parsed values /// * `index_info` - Contains sort order information for each field /// * `collations` - Array of collation sequences for string comparisons /// * `tie_breaker` - Result to return when all compared fields are equal /// /// # Comparison Logic /// /// The function follows SQLite's string comparison semantics: /// /// 1. **Type checking**: Ensures both sides are strings, otherwise falls back /// 2. **String comparison**: Uses collation if provided, binary otherwise /// 3. **Sort order**: Applies ascending/descending order to comparison result /// 4. **Length comparison**: If strings are equal, compares lengths /// 5. **Remaining fields**: If first field is equal and more fields exist, /// delegates to `compare_records_generic()` with `skip=1` fn compare_records_string( serialized: &ImmutableRecord, unpacked: I, index_info: &IndexInfo, tie_breaker: std::cmp::Ordering, ) -> Result where V: AsValueRef, I: ExactSizeIterator, { let payload = serialized.get_payload(); if payload.len() < 2 { return compare_records_generic(serialized, unpacked, index_info, 0, tie_breaker); } let (header_size, offset_1st_serialtype) = read_varint(payload)?; let header_size = header_size as usize; if payload.len() < header_size { return Err(LimboError::Corrupt(format!( "Record payload too short: claimed header size {} but payload only {} bytes", header_size, payload.len() ))); } let (first_serial_type, _) = read_varint(&payload[offset_1st_serialtype..])?; let serialtype_is_string = first_serial_type >= 13 && (first_serial_type & 1) == 1; if !serialtype_is_string { return compare_records_generic(serialized, unpacked, index_info, 0, tie_breaker); } let mut unpacked = unpacked.peekable(); let ValueRef::Text(rhs_text) = unpacked.peek().unwrap().as_value_ref() else { return compare_records_generic(serialized, unpacked, index_info, 0, tie_breaker); }; let string_len = (first_serial_type as usize - 13) / 2; let data_start = header_size; turso_debug_assert!(data_start + string_len <= payload.len()); let serial_type = SerialType::try_from(first_serial_type)?; let (lhs_value, _) = read_value(&payload[data_start..], serial_type)?; let ValueRef::Text(lhs_text) = lhs_value else { return compare_records_generic(serialized, unpacked, index_info, 0, tie_breaker); }; let collation = index_info.key_info[0].collation; let comparison = collation.compare_strings(&lhs_text, &rhs_text); let final_comparison = match index_info.key_info[0].sort_order { SortOrder::Asc => comparison, SortOrder::Desc => comparison.reverse(), }; match final_comparison { std::cmp::Ordering::Equal => { let len_cmp = lhs_text.len().cmp(&rhs_text.len()); if len_cmp != std::cmp::Ordering::Equal { let adjusted = match index_info.key_info[0].sort_order { SortOrder::Asc => len_cmp, SortOrder::Desc => len_cmp.reverse(), }; return Ok(adjusted); } if unpacked.len() > 1 { return compare_records_generic(serialized, unpacked, index_info, 1, tie_breaker); } Ok(tie_breaker) } other => Ok(other), } } /// Compare two table rows or index records. /// /// This function compares a serialized record (`serialized`) with an unpacked /// record (`unpacked`) and returns a comparison result. It returns `Less`, `Equal`, /// or `Greater` if the serialized record is less than, equal to, or greater than /// the unpacked record. /// /// The `serialized` record must be a blob created by the record serialization /// process (equivalent to SQLite's OP_MakeRecord opcode). The `unpacked` record /// must be a parsed key array of `RefValue` objects. /// /// # Arguments /// /// * `serialized` - The left-hand side record in serialized format /// * `unpacked` - The right-hand side record as an array of parsed values /// * `index_info` - Contains sort order information for each field /// * `collations` - Array of collation sequences for string comparisons /// * `skip` - Number of initial fields to skip (assumes caller verified equality) /// * `tie_breaker` - Result to return when all compared fields are equal /// /// # Skipping Fields /// /// If `skip` is non-zero, it is assumed that the caller has already determined /// that the first `skip` fields of the records are equal. This function will /// begin comparing at field index `skip`, skipping over the header and data /// portions of the already-verified fields. /// /// # Field Count Differences /// /// The serialized and unpacked records do not have to contain the same number /// of fields. If all fields that appear in both records are equal, then /// `tie_breaker` is returned. pub fn compare_records_generic( serialized: &ImmutableRecord, unpacked: I, index_info: &IndexInfo, skip: usize, tie_breaker: std::cmp::Ordering, ) -> Result where V: AsValueRef, I: ExactSizeIterator, { let payload = serialized.get_payload(); if payload.is_empty() { return Ok(std::cmp::Ordering::Less); } let (header_size, mut header_pos) = read_varint(payload)?; let header_end = header_size as usize; turso_debug_assert!(header_end <= payload.len()); let mut data_pos = header_size as usize; // Skip over `skip` number of fields for _ in 0..skip { if header_pos >= header_end { break; } let (serial_type_raw, bytes_read) = read_varint(&payload[header_pos..])?; header_pos += bytes_read; let serial_type = SerialType::try_from(serial_type_raw)?; if !matches!( serial_type.kind(), SerialTypeKind::ConstInt0 | SerialTypeKind::ConstInt1 | SerialTypeKind::Null ) { data_pos += serial_type.size(); } } let mut field_idx = skip; let field_limit = unpacked.len().min(index_info.key_info.len()); // assumes that that the `unpacked' iterator was not skipped outside this function call` for rhs_value in unpacked.skip(skip) { let rhs_value = &rhs_value.as_value_ref(); if field_idx >= field_limit || header_pos >= header_end { break; } let (serial_type_raw, bytes_read) = read_varint(&payload[header_pos..])?; header_pos += bytes_read; let serial_type = SerialType::try_from(serial_type_raw)?; let lhs_value = match serial_type.kind() { SerialTypeKind::ConstInt0 => ValueRef::Numeric(Numeric::Integer(0)), SerialTypeKind::ConstInt1 => ValueRef::Numeric(Numeric::Integer(1)), SerialTypeKind::Null => ValueRef::Null, _ => { let (value, field_size) = read_value(&payload[data_pos..], serial_type)?; data_pos += field_size; value } }; let comparison = match (&lhs_value, rhs_value) { (ValueRef::Text(lhs_text), ValueRef::Text(rhs_text)) => index_info.key_info[field_idx] .collation .compare_strings(lhs_text, rhs_text), _ => lhs_value.cmp(rhs_value), }; let final_comparison = match index_info.key_info[field_idx].sort_order { SortOrder::Asc => comparison, SortOrder::Desc => comparison.reverse(), }; if final_comparison != std::cmp::Ordering::Equal { return Ok(final_comparison); } field_idx += 1; } Ok(tie_breaker) } const I8_LOW: i64 = -128; const I8_HIGH: i64 = 127; const I16_LOW: i64 = -32768; const I16_HIGH: i64 = 32767; const I24_LOW: i64 = -8388608; const I24_HIGH: i64 = 8388607; const I32_LOW: i64 = -2147483648; const I32_HIGH: i64 = 2147483647; const I48_LOW: i64 = -140737488355328; const I48_HIGH: i64 = 140737488355327; /// Sqlite Serial Types /// https://www.sqlite.org/fileformat.html#record_format #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] pub struct SerialType(u64); #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum SerialTypeKind { Null, I8, I16, I24, I32, I48, I64, F64, ConstInt0, ConstInt1, Text, Blob, } impl SerialType { #[inline(always)] pub fn u64_is_valid_serial_type(n: u64) -> bool { n != 10 && n != 11 } const NULL: Self = Self(0); const I8: Self = Self(1); const I16: Self = Self(2); const I24: Self = Self(3); const I32: Self = Self(4); const I48: Self = Self(5); const I64: Self = Self(6); const F64: Self = Self(7); const CONST_INT0: Self = Self(8); const CONST_INT1: Self = Self(9); pub const fn null() -> Self { Self::NULL } pub const fn i8() -> Self { Self::I8 } pub const fn i16() -> Self { Self::I16 } pub const fn i24() -> Self { Self::I24 } pub const fn i32() -> Self { Self::I32 } pub const fn i48() -> Self { Self::I48 } pub const fn i64() -> Self { Self::I64 } pub const fn f64() -> Self { Self::F64 } pub const fn const_int0() -> Self { Self::CONST_INT0 } pub const fn const_int1() -> Self { Self::CONST_INT1 } pub const fn blob(size: u64) -> Self { Self(12 + size * 2) } pub const fn text(size: u64) -> Self { Self(13 + size * 2) } #[inline(always)] pub const fn kind(&self) -> SerialTypeKind { match self.0 { 0 => SerialTypeKind::Null, 1 => SerialTypeKind::I8, 2 => SerialTypeKind::I16, 3 => SerialTypeKind::I24, 4 => SerialTypeKind::I32, 5 => SerialTypeKind::I48, 6 => SerialTypeKind::I64, 7 => SerialTypeKind::F64, 8 => SerialTypeKind::ConstInt0, 9 => SerialTypeKind::ConstInt1, n if n >= 12 => match n % 2 { 0 => SerialTypeKind::Blob, 1 => SerialTypeKind::Text, _ => { mark_unlikely(); unreachable!(); } }, _ => { mark_unlikely(); unreachable!(); } } } pub const fn size(&self) -> usize { match self.kind() { SerialTypeKind::Null => 0, SerialTypeKind::I8 => 1, SerialTypeKind::I16 => 2, SerialTypeKind::I24 => 3, SerialTypeKind::I32 => 4, SerialTypeKind::I48 => 6, SerialTypeKind::I64 => 8, SerialTypeKind::F64 => 8, SerialTypeKind::ConstInt0 => 0, SerialTypeKind::ConstInt1 => 0, SerialTypeKind::Text => (self.0 as usize - 13) / 2, SerialTypeKind::Blob => (self.0 as usize - 12) / 2, } } } #[inline(always)] pub fn get_serial_type_size(serial: u64) -> Result { match serial { 0 | 8 | 9 => Ok(0), 1 => Ok(1), 2 => Ok(2), 3 => Ok(3), 4 => Ok(4), 5 => Ok(6), 6 | 7 => Ok(8), n if n >= 12 => match n % 2 { 0 => Ok(((n - 12) / 2) as usize), // Blob 1 => Ok(((n - 13) / 2) as usize), // Text _ => { mark_unlikely(); unreachable!(); } }, _ => { mark_unlikely(); Err(LimboError::Corrupt(format!( "Invalid serial type: {serial}" ))) } } } impl From for SerialType { fn from(value: T) -> Self { let value = value.as_value_ref(); match value { ValueRef::Null => SerialType::null(), ValueRef::Numeric(Numeric::Integer(i)) => match i { 0 => SerialType::const_int0(), 1 => SerialType::const_int1(), i if (I8_LOW..=I8_HIGH).contains(&i) => SerialType::i8(), i if (I16_LOW..=I16_HIGH).contains(&i) => SerialType::i16(), i if (I24_LOW..=I24_HIGH).contains(&i) => SerialType::i24(), i if (I32_LOW..=I32_HIGH).contains(&i) => SerialType::i32(), i if (I48_LOW..=I48_HIGH).contains(&i) => SerialType::i48(), _ => SerialType::i64(), }, ValueRef::Numeric(Numeric::Float(_)) => SerialType::f64(), ValueRef::Text(t) => SerialType::text(t.value.len() as u64), ValueRef::Blob(b) => SerialType::blob(b.len() as u64), } } } impl From for u64 { fn from(serial_type: SerialType) -> Self { serial_type.0 } } impl TryFrom for SerialType { type Error = LimboError; #[inline(always)] fn try_from(uint: u64) -> Result { if unlikely(uint == 10 || uint == 11) { return Err(LimboError::Corrupt(format!("Invalid serial type: {uint}"))); } Ok(SerialType(uint)) } } impl Record { pub fn new(values: Vec) -> Self { Self { values } } /// Calculates the total size needed for a SQLite record header. /// /// The record header consists of: /// 1. A varint encoding the total header size (self-referentially, e.g. a 100 byte header literally has the number '100' in the header suffix) /// 2. A sequence of varints encoding the serial types /// /// For small headers (<=126 bytes), we only need 1 byte to encode the header size, because 127 fits in 7 bits (varint uses 7 bits for the value and 1 continuation bit) /// For larger headers, we need to account for the variable length of the header size varint. pub fn calc_header_size(sizeof_serial_types: usize) -> usize { if sizeof_serial_types < i8::MAX as usize { return sizeof_serial_types + 1; } let mut header_size = sizeof_serial_types; // For larger headers, calculate how many bytes we need for the header size varint let mut temp_buf = [0u8; 9]; let mut prev_header_size; loop { prev_header_size = header_size; let varint_len = write_varint(&mut temp_buf, header_size as u64); header_size = sizeof_serial_types + varint_len; if header_size == prev_header_size { break; } } header_size } pub fn serialize(&self, buf: &mut Vec) { let initial_i = buf.len(); // write serial types for value in &self.values { let serial_type = SerialType::from(value); buf.resize(buf.len() + 9, 0); // Ensure space for varint (1-9 bytes in length) let len = buf.len(); let n = write_varint(&mut buf[len - 9..], serial_type.into()); buf.truncate(buf.len() - 9 + n); // Remove unused bytes } let mut header_size = buf.len() - initial_i; // write content for value in &self.values { match value { Value::Null => {} Value::Numeric(Numeric::Integer(i)) => { let serial_type = SerialType::from(value); match serial_type.kind() { SerialTypeKind::ConstInt0 | SerialTypeKind::ConstInt1 => {} SerialTypeKind::I8 => buf.extend_from_slice(&(*i as i8).to_be_bytes()), SerialTypeKind::I16 => buf.extend_from_slice(&(*i as i16).to_be_bytes()), SerialTypeKind::I24 => { buf.extend_from_slice(&(*i as i32).to_be_bytes()[1..]) } // remove most significant byte SerialTypeKind::I32 => buf.extend_from_slice(&(*i as i32).to_be_bytes()), SerialTypeKind::I48 => buf.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes SerialTypeKind::I64 => buf.extend_from_slice(&i.to_be_bytes()), _ => { mark_unlikely(); unreachable!(); } } } Value::Numeric(Numeric::Float(f)) => { buf.extend_from_slice(&f64::from(*f).to_be_bytes()) } Value::Text(t) => buf.extend_from_slice(t.value.as_bytes()), Value::Blob(b) => buf.extend_from_slice(b), }; } let mut header_bytes_buf: Vec = Vec::new(); header_size = Record::calc_header_size(header_size); header_bytes_buf.extend(std::iter::repeat_n(0, 9)); let n = write_varint(header_bytes_buf.as_mut_slice(), header_size as u64); header_bytes_buf.truncate(n); buf.splice(initial_i..initial_i, header_bytes_buf.iter().cloned()); } } pub enum Cursor { BTree(Box), IndexMethod(Box), Pseudo(Box), Sorter(Box), Virtual(VirtualTableCursor), MaterializedView(Box), } impl Debug for Cursor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::BTree(..) => f.debug_tuple("BTree").finish(), Self::IndexMethod(..) => f.debug_tuple("IndexMethod").finish(), Self::Pseudo(..) => f.debug_tuple("Pseudo").finish(), Self::Sorter(..) => f.debug_tuple("Sorter").finish(), Self::Virtual(..) => f.debug_tuple("Virtual").finish(), Self::MaterializedView(..) => f.debug_tuple("MaterializedView").finish(), } } } impl Cursor { pub fn new_btree(cursor: Box) -> Self { Self::BTree(cursor) } pub fn new_pseudo(cursor: PseudoCursor) -> Self { Self::Pseudo(Box::new(cursor)) } pub fn new_sorter(cursor: Sorter) -> Self { Self::Sorter(Box::new(cursor)) } pub fn new_materialized_view( cursor: crate::incremental::cursor::MaterializedViewCursor, ) -> Self { Self::MaterializedView(Box::new(cursor)) } pub fn as_btree_mut(&mut self) -> &mut dyn CursorTrait { match self { Self::BTree(cursor) => cursor.as_mut(), _ => { mark_unlikely(); panic!("Cursor is not a btree cursor"); } } } pub fn as_pseudo_mut(&mut self) -> &mut PseudoCursor { match self { Self::Pseudo(cursor) => cursor, _ => { mark_unlikely(); panic!("Cursor is not a pseudo cursor"); } } } pub fn as_sorter_mut(&mut self) -> &mut Sorter { match self { Self::Sorter(cursor) => cursor, _ => { mark_unlikely(); panic!("Cursor is not a sorter cursor") } } } pub fn as_virtual_mut(&mut self) -> &mut VirtualTableCursor { match self { Self::Virtual(cursor) => cursor, _ => { mark_unlikely(); panic!("Cursor is not a virtual cursor") } } } pub fn as_materialized_view_mut( &mut self, ) -> &mut crate::incremental::cursor::MaterializedViewCursor { match self { Self::MaterializedView(cursor) => cursor, _ => { mark_unlikely(); panic!("Cursor is not a materialized view cursor"); } } } pub fn as_index_method_mut(&mut self) -> &mut dyn IndexMethodCursor { match self { Self::IndexMethod(cursor) => cursor.as_mut(), _ => { mark_unlikely(); panic!("Cursor is not an IndexMethod cursor"); } } } pub fn set_null_flag(&mut self, flag: bool) { match self { Self::BTree(cursor) => cursor.set_null_flag(flag), Self::Virtual(cursor) => cursor.set_null_flag(flag), _ => { mark_unlikely(); panic!("set_null_flag on unexpected cursor type"); } } } } #[derive(Debug)] #[must_use] pub enum IOCompletions { Single(Completion), } pub struct IOCompletionAsync<'a, I: ?Sized + IO> { io: &'a I, completion: Completion, } impl<'a, I: ?Sized + IO> Future for IOCompletionAsync<'a, I> { type Output = Result<()>; fn poll( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { let completion = std::pin::pin!(&mut self.as_mut().completion); match completion.poll(cx) { Poll::Pending => { self.io.step()?; Poll::Pending } res => res, } } } impl IOCompletions { /// Wais for the Completions to complete pub fn wait(self, io: &I) -> Result<()> { match self { IOCompletions::Single(c) => io.wait_for_completion(c), } } /// Waits for Completion to complete and `steps` IO. Ideally the user should do the stepping, /// but we do not have yet a good api for this pub async fn wait_async(self, io: &I) -> Result<()> { match self { IOCompletions::Single(c) => IOCompletionAsync { io, completion: c }.await, } } pub fn finished(&self) -> bool { match self { IOCompletions::Single(c) => c.finished(), } } /// Returns true if this is an explicit yield — a signal to return control /// to the cooperative scheduler so other fibers can make progress. pub fn is_explicit_yield(&self) -> bool { match self { IOCompletions::Single(c) => c.is_explicit_yield(), } } /// Send abort signal to completions pub fn abort(&self) { match self { IOCompletions::Single(c) => c.abort(), } } pub fn get_error(&self) -> Option { match self { IOCompletions::Single(c) => c.get_error(), } } pub fn set_waker(&self, waker: Option<&Waker>) { if let Some(waker) = waker { match self { IOCompletions::Single(c) => c.set_waker(waker), } } } } #[derive(Debug)] #[must_use] pub enum IOResult { Done(T), IO(IOCompletions), } impl IOResult { #[inline] pub fn is_io(&self) -> bool { matches!(self, IOResult::IO(..)) } #[inline] pub fn io(self) -> Option { match self { IOResult::Done(_) => None, IOResult::IO(io) => Some(io), } } #[inline] pub fn map(self, func: impl FnOnce(T) -> U) -> IOResult { match self { IOResult::Done(t) => IOResult::Done(func(t)), IOResult::IO(io) => IOResult::IO(io), } } } /// Evaluate a Result>, if IO return IO. #[macro_export] macro_rules! return_if_io { ($expr:expr) => { match $expr { Ok(IOResult::Done(v)) => v, Ok(IOResult::IO(io)) => return Ok(IOResult::IO(io)), Err(err) => { branches::mark_unlikely(); return Err(err); } } }; } #[macro_export] macro_rules! return_and_restore_if_io { ($field:expr, $saved_state:expr, $e:expr) => { match $e { Ok(IOResult::Done(v)) => v, Ok(IOResult::IO(io)) => { let _ = std::mem::replace($field, $saved_state); return Ok(IOResult::IO(io)); } Err(e) => { let _ = std::mem::replace($field, $saved_state); return Err(e); } } }; } #[derive(Debug, PartialEq, Clone, Copy)] pub enum SeekResult { /// Record matching the [SeekOp] found in the B-tree and cursor was positioned to point onto that record Found, /// Record matching the [SeekOp] doesn't exists in the B-tree NotFound, /// This result can happen only if eq_only for [SeekOp] is false /// In this case Seek can position cursor to the leaf page boundaries (before the start, after the end) /// (e.g. if leaf page holds rows with keys from range [1..10], key 10 is absent and [SeekOp] is >= 10) /// /// turso-db has this extra [SeekResult] in order to make [BTreeCursor::seek] method to position cursor at /// the leaf of potential insertion, but also communicate to caller the fact that current cursor position /// doesn't hold a matching entry /// (necessary for Seek{XX} VM op-codes, so these op-codes will try to advance cursor in order to move it to matching entry) TryAdvance, } #[derive(Clone, Copy, PartialEq, Eq, Debug)] /// The match condition of a table/index seek. pub enum SeekOp { /// If eq_only is true, this means in practice: /// We are iterating forwards, but we are really looking for an exact match on the seek key. GE { eq_only: bool, }, GT, /// If eq_only is true, this means in practice: /// We are iterating backwards, but we are really looking for an exact match on the seek key. LE { eq_only: bool, }, LT, } impl SeekOp { /// A given seek op implies an iteration direction. /// /// For example, a seek with SeekOp::GT implies: /// Find the first table/index key that compares greater than the seek key /// -> used in forwards iteration. /// /// A seek with SeekOp::LE implies: /// Find the last table/index key that compares less than or equal to the seek key /// -> used in backwards iteration. #[inline(always)] pub fn iteration_direction(&self) -> IterationDirection { match self { SeekOp::GE { .. } | SeekOp::GT => IterationDirection::Forwards, SeekOp::LE { .. } | SeekOp::LT => IterationDirection::Backwards, } } pub fn eq_only(&self) -> bool { match self { SeekOp::GE { eq_only } | SeekOp::LE { eq_only } => *eq_only, _ => false, } } pub fn reverse(&self) -> Self { match self { SeekOp::GE { eq_only } => SeekOp::LE { eq_only: *eq_only }, SeekOp::GT => SeekOp::LT, SeekOp::LE { eq_only } => SeekOp::GE { eq_only: *eq_only }, SeekOp::LT => SeekOp::GT, } } } #[derive(Clone, PartialEq, Debug)] pub enum SeekKey<'a> { TableRowId(i64), IndexKey(&'a ImmutableRecord), } #[derive(Debug)] pub enum DatabaseChangeType { Delete, Update { bin_record: Vec }, Insert { bin_record: Vec }, } #[derive(Debug)] pub struct DatabaseChange { pub change_id: i64, pub change_time: u64, pub change: DatabaseChangeType, pub table_name: String, pub id: i64, } #[derive(Debug)] pub struct WalFrameInfo { pub page_no: u32, pub db_size: u32, } #[derive(Debug, PartialEq)] pub struct WalState { pub checkpoint_seq_no: u32, pub max_frame: u64, } impl WalFrameInfo { pub fn is_commit_frame(&self) -> bool { self.db_size > 0 } pub fn from_frame_header(frame: &[u8]) -> Self { let page_no = u32::from_be_bytes(frame[0..4].try_into().unwrap()); let db_size = u32::from_be_bytes(frame[4..8].try_into().unwrap()); Self { page_no, db_size } } pub fn put_to_frame_header(&self, frame: &mut [u8]) { frame[0..4].copy_from_slice(&self.page_no.to_be_bytes()); frame[4..8].copy_from_slice(&self.db_size.to_be_bytes()); } } #[cfg(test)] mod tests { use super::*; use crate::translate::collate::CollationSeq; #[test] fn test_value_iterator_simple() { let mut buf = Vec::new(); let record = Record::new(vec![Value::from_i64(42), Value::Text(Text::new("hello"))]); record.serialize(&mut buf); let iter = ValueIterator::new(&buf).unwrap(); assert!(!iter.is_empty()); assert_eq!(iter.clone().count(), 2); let mut iter = ValueIterator::new(&buf).unwrap(); let val = iter.next().unwrap().unwrap(); assert_eq!(val, ValueRef::from_i64(42)); let val = iter.next().unwrap().unwrap(); assert_eq!( val, ValueRef::Text(TextRef::new("hello", TextSubtype::Text)) ); assert!(iter.next().is_none()); } #[test] fn test_value_iterator_nulls() { let mut buf = Vec::new(); let record = Record::new(vec![Value::Null, Value::Null, Value::Null]); record.serialize(&mut buf); let iter = ValueIterator::new(&buf).unwrap(); for val in iter { assert_eq!(val.unwrap(), ValueRef::Null); } } #[test] fn test_value_iterator_mixed_types() { let mut buf = Vec::new(); let record = Record::new(vec![ Value::Null, Value::from_i64(100), Value::from_f64(std::f64::consts::PI), Value::Text(Text::new("test")), Value::Blob(vec![1, 2, 3]), Value::from_i64(0), Value::from_i64(1), ]); record.serialize(&mut buf); let iter = ValueIterator::new(&buf).unwrap(); let values: Vec<_> = iter.collect::>>().unwrap(); assert_eq!(values[0], ValueRef::Null); assert_eq!(values[1], ValueRef::from_i64(100)); assert_eq!(values[2], ValueRef::from_f64(std::f64::consts::PI)); assert_eq!( values[3], ValueRef::Text(TextRef::new("test", TextSubtype::Text)) ); assert_eq!(values[4], ValueRef::Blob(&[1, 2, 3])); assert_eq!(values[5], ValueRef::from_i64(0)); assert_eq!(values[6], ValueRef::from_i64(1)); } #[test] fn test_value_iterator_large_record() { let mut buf = Vec::new(); let values: Vec = (0..20).map(|i| Value::from_i64(i as i64)).collect(); let record = Record::new(values); record.serialize(&mut buf); let iter = ValueIterator::new(&buf).unwrap(); assert_eq!(iter.count(), 20); let iter = ValueIterator::new(&buf).unwrap(); for (i, val) in iter.enumerate() { assert_eq!(val.unwrap(), ValueRef::from_i64(i as i64)); } } #[test] fn test_value_iterator_zero_allocation() { let mut buf = Vec::new(); let values: Vec = (0..5).map(|i| Value::from_i64(i as i64)).collect(); let record = Record::new(values); record.serialize(&mut buf); let mut iter = ValueIterator::new(&buf).unwrap(); let _ = iter.next(); let _ = iter.next(); } pub fn compare_immutable_for_testing( l: &[ValueRef], r: &[ValueRef], index_key_info: &[KeyInfo], tie_breaker: std::cmp::Ordering, ) -> std::cmp::Ordering { let min_len = l.len().min(r.len()); for i in 0..min_len { let column_order = index_key_info[i].sort_order; let collation = index_key_info[i].collation; let cmp = match (&l[i], &r[i]) { (ValueRef::Text(left), ValueRef::Text(right)) => { collation.compare_strings(left, right) } _ => l[i].partial_cmp(&r[i]).unwrap_or(std::cmp::Ordering::Equal), }; if cmp != std::cmp::Ordering::Equal { return match column_order { SortOrder::Asc => cmp, SortOrder::Desc => cmp.reverse(), }; } } tie_breaker } fn create_record(values: Vec) -> ImmutableRecord { let registers: Vec = values.into_iter().map(Register::Value).collect(); ImmutableRecord::from_registers(®isters, registers.len()) } fn create_index_info( num_cols: usize, sort_orders: Vec, collations: Vec, ) -> IndexInfo { IndexInfo { key_info: sort_orders .into_iter() .zip(collations) .map(|(sort_order, collation)| KeyInfo { sort_order, collation, }) .collect(), has_rowid: false, num_cols, is_unique: false, } } fn assert_compare_matches_full_comparison( serialized_values: Vec, unpacked_values: Vec, index_info: &IndexInfo, test_name: &str, ) { let serialized = create_record(serialized_values.clone()); let serialized_ref_values: Vec = serialized_values.iter().map(Value::as_ref).collect(); let tie_breaker = std::cmp::Ordering::Equal; let gold_result = compare_immutable_for_testing( &serialized_ref_values, &unpacked_values, &index_info.key_info, tie_breaker, ); let comparer = find_compare(unpacked_values.iter().peekable(), index_info); let optimized_result = comparer .compare(&serialized, &unpacked_values, index_info, 0, tie_breaker) .unwrap(); assert_eq!( gold_result, optimized_result, "Test '{test_name}' failed: Full Comparison: {gold_result:?}, Optimized: {optimized_result:?}, Strategy: {comparer:?}" ); let generic_result = compare_records_generic( &serialized, unpacked_values.iter(), index_info, 0, tie_breaker, ) .unwrap(); assert_eq!( gold_result, generic_result, "Test '{test_name}' failed with generic: Full Comparison: {gold_result:?}, Generic: {generic_result:?}\n LHS: {serialized_values:?}\n RHS: {unpacked_values:?}" ); } #[test] fn test_calc_header_size() { // Test 1-byte header size (serial type sizes 0 to 126) const MIN_SERIALTYPES_SIZE_FOR_1_BYTE_HEADER: usize = 0; assert_eq!( Record::calc_header_size(MIN_SERIALTYPES_SIZE_FOR_1_BYTE_HEADER), MIN_SERIALTYPES_SIZE_FOR_1_BYTE_HEADER + 1 ); const BITS_7_MAX: usize = (1 << 7) - 1; // varints use 7 bits for the value and 1 continuation bit const MAX_SERIALTYPES_SIZE_FOR_1_BYTE_HEADER: usize = BITS_7_MAX - 1; assert_eq!( Record::calc_header_size(MAX_SERIALTYPES_SIZE_FOR_1_BYTE_HEADER), MAX_SERIALTYPES_SIZE_FOR_1_BYTE_HEADER + 1 ); // Test 2-byte header size (serial type sizes 127 to 16381) const MIN_SERIALTYPES_SIZE_FOR_2_BYTE_HEADER: usize = MAX_SERIALTYPES_SIZE_FOR_1_BYTE_HEADER + 1; assert_eq!( Record::calc_header_size(MIN_SERIALTYPES_SIZE_FOR_2_BYTE_HEADER), MIN_SERIALTYPES_SIZE_FOR_2_BYTE_HEADER + 2 ); const BITS_14_MAX: usize = (1 << 14) - 1; const MAX_SERIALTYPES_SIZE_FOR_2_BYTE_HEADER: usize = BITS_14_MAX - 2; assert_eq!( Record::calc_header_size(MAX_SERIALTYPES_SIZE_FOR_2_BYTE_HEADER), MAX_SERIALTYPES_SIZE_FOR_2_BYTE_HEADER + 2 ); // Test 3-byte header size (serial type sizes 16382 to 2097148) const MIN_SERIALTYPES_SIZE_FOR_3_BYTE_HEADER: usize = MAX_SERIALTYPES_SIZE_FOR_2_BYTE_HEADER + 1; assert_eq!( Record::calc_header_size(MIN_SERIALTYPES_SIZE_FOR_3_BYTE_HEADER), MIN_SERIALTYPES_SIZE_FOR_3_BYTE_HEADER + 3 ); const BITS_21_MAX: usize = (1 << 21) - 1; const MAX_SERIALTYPES_SIZE_FOR_3_BYTE_HEADER: usize = BITS_21_MAX - 3; assert_eq!( Record::calc_header_size(MAX_SERIALTYPES_SIZE_FOR_3_BYTE_HEADER), MAX_SERIALTYPES_SIZE_FOR_3_BYTE_HEADER + 3 ); // Test 4-byte header size (serial type sizes 2097149 to 268435451) const MIN_SERIALTYPES_SIZE_FOR_4_BYTE_HEADER: usize = MAX_SERIALTYPES_SIZE_FOR_3_BYTE_HEADER + 1; assert_eq!( Record::calc_header_size(MIN_SERIALTYPES_SIZE_FOR_4_BYTE_HEADER), MIN_SERIALTYPES_SIZE_FOR_4_BYTE_HEADER + 4 ); const BITS_28_MAX: usize = (1 << 28) - 1; const MAX_SERIALTYPES_SIZE_FOR_4_BYTE_HEADER: usize = BITS_28_MAX - 4; assert_eq!( Record::calc_header_size(MAX_SERIALTYPES_SIZE_FOR_4_BYTE_HEADER), MAX_SERIALTYPES_SIZE_FOR_4_BYTE_HEADER + 4 ); } #[test] fn test_integer_fast_path() { let index_info = create_index_info( 2, vec![SortOrder::Asc, SortOrder::Asc], vec![CollationSeq::Binary; 2], ); let test_cases = vec![ ( vec![Value::from_i64(42)], vec![ValueRef::from_i64(42)], "equal_integers", ), ( vec![Value::from_i64(10)], vec![ValueRef::from_i64(20)], "less_than_integers", ), ( vec![Value::from_i64(30)], vec![ValueRef::from_i64(20)], "greater_than_integers", ), ( vec![Value::from_i64(0)], vec![ValueRef::from_i64(0)], "zero_integers", ), ( vec![Value::from_i64(-5)], vec![ValueRef::from_i64(-5)], "negative_integers", ), ( vec![Value::from_i64(i64::MAX)], vec![ValueRef::from_i64(i64::MAX)], "max_integers", ), ( vec![Value::from_i64(i64::MIN)], vec![ValueRef::from_i64(i64::MIN)], "min_integers", ), ( vec![Value::from_i64(42), Value::Text(Text::new("hello"))], vec![ ValueRef::from_i64(42), ValueRef::Text(TextRef::new("hello", TextSubtype::Text)), ], "integer_text_equal", ), ( vec![Value::from_i64(42), Value::Text(Text::new("hello"))], vec![ ValueRef::from_i64(42), ValueRef::Text(TextRef::new("world", TextSubtype::Text)), ], "integer_equal_text_different", ), ]; for (serialized_values, unpacked_values, test_name) in test_cases { println!( "Testing integer fast path `{test_name}`\nLHS: {serialized_values:?}\nRHS: {unpacked_values:?}" ); assert_compare_matches_full_comparison( serialized_values, unpacked_values, &index_info, test_name, ); } } #[test] fn test_string_fast_path() { let index_info = create_index_info( 2, vec![SortOrder::Asc, SortOrder::Asc], vec![CollationSeq::Binary; 2], ); let test_cases = vec![ ( vec![Value::Text(Text::new("hello"))], vec![ValueRef::Text(TextRef::new("hello", TextSubtype::Text))], "equal_strings", ), ( vec![Value::Text(Text::new("abc"))], vec![ValueRef::Text(TextRef::new("def", TextSubtype::Text))], "less_than_strings", ), ( vec![Value::Text(Text::new("xyz"))], vec![ValueRef::Text(TextRef::new("abc", TextSubtype::Text))], "greater_than_strings", ), ( vec![Value::Text(Text::new(""))], vec![ValueRef::Text(TextRef::new("", TextSubtype::Text))], "empty_strings", ), ( vec![Value::Text(Text::new("a"))], vec![ValueRef::Text(TextRef::new("aa", TextSubtype::Text))], "prefix_strings", ), // Multi-field with string first ( vec![Value::Text(Text::new("hello")), Value::from_i64(42)], vec![ ValueRef::Text(TextRef::new("hello", TextSubtype::Text)), ValueRef::from_i64(42), ], "string_integer_equal", ), ( vec![Value::Text(Text::new("hello")), Value::from_i64(42)], vec![ ValueRef::Text(TextRef::new("hello", TextSubtype::Text)), ValueRef::from_i64(99), ], "string_equal_integer_different", ), ]; for (serialized_values, unpacked_values, test_name) in test_cases { assert_compare_matches_full_comparison( serialized_values, unpacked_values, &index_info, test_name, ); } } #[test] fn test_type_precedence() { let index_info = create_index_info(1, vec![SortOrder::Asc], vec![CollationSeq::Binary]); // Test SQLite type precedence: NULL < Numbers < Text < Blob let test_cases = vec![ // NULL vs others ( vec![Value::Null], vec![ValueRef::from_i64(42)], "null_vs_integer", ), ( vec![Value::Null], vec![ValueRef::from_f64(64.4)], "null_vs_float", ), ( vec![Value::Null], vec![ValueRef::Text(TextRef::new("hello", TextSubtype::Text))], "null_vs_text", ), ( vec![Value::Null], vec![ValueRef::Blob(b"blob")], "null_vs_blob", ), // Numbers vs Text/Blob ( vec![Value::from_i64(42)], vec![ValueRef::Text(TextRef::new("hello", TextSubtype::Text))], "integer_vs_text", ), ( vec![Value::from_f64(64.4)], vec![ValueRef::Text(TextRef::new("hello", TextSubtype::Text))], "float_vs_text", ), ( vec![Value::from_i64(42)], vec![ValueRef::Blob(b"blob")], "integer_vs_blob", ), ( vec![Value::from_f64(64.4)], vec![ValueRef::Blob(b"blob")], "float_vs_blob", ), // Text vs Blob ( vec![Value::Text(Text::new("hello"))], vec![ValueRef::Blob(b"blob")], "text_vs_blob", ), // Integer vs Float (affinity conversion) ( vec![Value::from_i64(42)], vec![ValueRef::from_f64(42.0)], "integer_vs_equal_float", ), ( vec![Value::from_i64(42)], vec![ValueRef::from_f64(42.5)], "integer_vs_different_float", ), ( vec![Value::from_f64(42.5)], vec![ValueRef::from_i64(42)], "float_vs_integer", ), ]; for (serialized_values, unpacked_values, test_name) in test_cases { assert_compare_matches_full_comparison( serialized_values, unpacked_values, &index_info, test_name, ); } } #[test] fn test_sort_order_desc() { let index_info = create_index_info( 2, vec![SortOrder::Desc, SortOrder::Asc], vec![CollationSeq::Binary; 2], ); let test_cases = vec![ // DESC order should reverse first field comparison ( vec![Value::from_i64(10)], vec![ValueRef::from_i64(20)], "desc_integer_reversed", ), ( vec![Value::Text(Text::new("abc"))], vec![ValueRef::Text(TextRef::new("def", TextSubtype::Text))], "desc_string_reversed", ), // Mixed sort orders ( vec![Value::from_i64(10), Value::Text(Text::new("hello"))], vec![ ValueRef::from_i64(20), ValueRef::Text(TextRef::new("hello", TextSubtype::Text)), ], "desc_first_asc_second", ), ]; for (serialized_values, unpacked_values, test_name) in test_cases { assert_compare_matches_full_comparison( serialized_values, unpacked_values, &index_info, test_name, ); } } #[test] fn test_edge_cases() { let index_info = create_index_info(15, vec![SortOrder::Asc; 15], vec![CollationSeq::Binary; 15]); let test_cases = vec![ ( vec![Value::from_i64(42)], vec![ ValueRef::from_i64(42), ValueRef::Text(TextRef::new("extra", TextSubtype::Text)), ], "fewer_serialized_fields", ), ( vec![Value::from_i64(42), Value::Text(Text::new("extra"))], vec![ValueRef::from_i64(42)], "fewer_unpacked_fields", ), (vec![], vec![], "both_empty"), (vec![], vec![ValueRef::from_i64(42)], "empty_serialized"), ( (0..15).map(Value::from_i64).collect(), (0..15).map(ValueRef::from_i64).collect(), "large_field_count", ), ( vec![Value::Blob(vec![1, 2, 3])], vec![ValueRef::Blob(&[1, 2, 3])], "blob_first_field", ), ( vec![Value::Text(Text::new("hello")), Value::from_i64(5)], vec![ValueRef::Text(TextRef::new("hello", TextSubtype::Text))], "equal_text_prefix_but_more_serialized_fields", ), ( vec![Value::Text(Text::new("same")), Value::from_i64(5)], vec![ ValueRef::Text(TextRef::new("same", TextSubtype::Text)), ValueRef::from_i64(5), ], "equal_text_then_equal_int", ), ]; for (serialized_values, unpacked_values, test_name) in test_cases { assert_compare_matches_full_comparison( serialized_values, unpacked_values, &index_info, test_name, ); } } #[test] fn test_skip_parameter() { let index_info = create_index_info( 3, vec![SortOrder::Asc, SortOrder::Asc, SortOrder::Asc], vec![CollationSeq::Binary; 3], ); let serialized = create_record(vec![ Value::from_i64(1), Value::from_i64(2), Value::from_i64(3), ]); let unpacked = [ ValueRef::from_i64(1), ValueRef::from_i64(99), ValueRef::from_i64(3), ]; let tie_breaker = std::cmp::Ordering::Equal; let result_skip_0 = compare_records_generic(&serialized, unpacked.iter(), &index_info, 0, tie_breaker) .unwrap(); let result_skip_1 = compare_records_generic(&serialized, unpacked.iter(), &index_info, 1, tie_breaker) .unwrap(); assert_eq!(result_skip_0, std::cmp::Ordering::Less); assert_eq!(result_skip_1, std::cmp::Ordering::Less); } #[test] fn test_strategy_selection() { let collations_small = vec![CollationSeq::Binary; 3]; let collations_large = vec![CollationSeq::Binary; 15]; let index_info_small = create_index_info( 3, vec![SortOrder::Asc, SortOrder::Asc, SortOrder::Asc], collations_small, ); let index_info_large = create_index_info(15, vec![SortOrder::Asc; 15], collations_large); let int_values = [ ValueRef::from_i64(42), ValueRef::Text(TextRef::new("hello", TextSubtype::Text)), ]; assert!(matches!( find_compare(int_values.iter().peekable(), &index_info_small), RecordCompare::Int )); let string_values = [ ValueRef::Text(TextRef::new("hello", TextSubtype::Text)), ValueRef::from_i64(42), ]; assert!(matches!( find_compare(string_values.iter().peekable(), &index_info_small), RecordCompare::String )); let large_values: Vec = (0..15).map(ValueRef::from_i64).collect(); assert!(matches!( find_compare(large_values.iter().peekable(), &index_info_large), RecordCompare::Generic )); let blob_values = [ValueRef::Blob(&[1, 2, 3])]; assert!(matches!( find_compare(blob_values.iter().peekable(), &index_info_small), RecordCompare::Generic )); } #[test] fn test_serialize_null() { let record = Record::new(vec![Value::Null]); let mut buf = Vec::new(); record.serialize(&mut buf); let header_length = record.values.len() + 1; let header = &buf[0..header_length]; // First byte should be header size assert_eq!(header[0], header_length as u8); // Second byte should be serial type for NULL assert_eq!(header[1] as u64, u64::from(SerialType::null())); // Check that the buffer is empty after the header assert_eq!(buf.len(), header_length); } #[test] fn test_serialize_integers() { let record = Record::new(vec![ Value::from_i64(0), // Should use ConstInt0 Value::from_i64(1), // Should use ConstInt1 Value::from_i64(42), // Should use SERIAL_TYPE_I8 Value::from_i64(1000), // Should use SERIAL_TYPE_I16 Value::from_i64(1_000_000), // Should use SERIAL_TYPE_I24 Value::from_i64(1_000_000_000), // Should use SERIAL_TYPE_I32 Value::from_i64(1_000_000_000_000), // Should use SERIAL_TYPE_I48 Value::from_i64(i64::MAX), // Should use SERIAL_TYPE_I64 ]); let mut buf = Vec::new(); record.serialize(&mut buf); let header_length = record.values.len() + 1; let header = &buf[0..header_length]; // First byte should be header size assert_eq!(header[0], header_length as u8); // Header should be larger than number of values // Check that correct serial types were chosen assert_eq!(header[1] as u64, u64::from(SerialType::const_int0())); // 8 assert_eq!(header[2] as u64, u64::from(SerialType::const_int1())); // 9 assert_eq!(header[3] as u64, u64::from(SerialType::i8())); // 1 assert_eq!(header[4] as u64, u64::from(SerialType::i16())); // 2 assert_eq!(header[5] as u64, u64::from(SerialType::i24())); // 3 assert_eq!(header[6] as u64, u64::from(SerialType::i32())); // 4 assert_eq!(header[7] as u64, u64::from(SerialType::i48())); // 5 assert_eq!(header[8] as u64, u64::from(SerialType::i64())); // 6 // test that the bytes after the header can be interpreted as the correct values let mut cur_offset = header_length; // Value::from_i64(0) - ConstInt0: NO PAYLOAD BYTES // Value::from_i64(1) - ConstInt1: NO PAYLOAD BYTES // Value::from_i64(42) - I8: 1 byte let i8_bytes = &buf[cur_offset..cur_offset + size_of::()]; cur_offset += size_of::(); // Value::from_i64(1000) - I16: 2 bytes let i16_bytes = &buf[cur_offset..cur_offset + size_of::()]; cur_offset += size_of::(); // Value::from_i64(1_000_000) - I24: 3 bytes let i24_bytes = &buf[cur_offset..cur_offset + 3]; cur_offset += 3; // Value::from_i64(1_000_000_000) - I32: 4 bytes let i32_bytes = &buf[cur_offset..cur_offset + size_of::()]; cur_offset += size_of::(); // Value::from_i64(1_000_000_000_000) - I48: 6 bytes let i48_bytes = &buf[cur_offset..cur_offset + 6]; cur_offset += 6; // Value::from_i64(i64::MAX) - I64: 8 bytes let i64_bytes = &buf[cur_offset..cur_offset + size_of::()]; // Verify the payload values let val_int8 = i8::from_be_bytes(i8_bytes.try_into().unwrap()); let val_int16 = i16::from_be_bytes(i16_bytes.try_into().unwrap()); let mut i24_with_padding = vec![0]; i24_with_padding.extend(i24_bytes); let val_int24 = i32::from_be_bytes(i24_with_padding.try_into().unwrap()); let val_int32 = i32::from_be_bytes(i32_bytes.try_into().unwrap()); let mut i48_with_padding = vec![0, 0]; i48_with_padding.extend(i48_bytes); let val_int48 = i64::from_be_bytes(i48_with_padding.try_into().unwrap()); let val_int64 = i64::from_be_bytes(i64_bytes.try_into().unwrap()); assert_eq!(val_int8, 42); assert_eq!(val_int16, 1000); assert_eq!(val_int24, 1_000_000); assert_eq!(val_int32, 1_000_000_000); assert_eq!(val_int48, 1_000_000_000_000); assert_eq!(val_int64, i64::MAX); //Size of buffer = header + payload bytes // ConstInt0 and ConstInt1 contribute 0 bytes to payload assert_eq!( buf.len(), header_length // 9 bytes (header size + 8 serial types) + size_of::() // I8: 1 byte + size_of::() // I16: 2 bytes + (size_of::() - 1) // I24: 3 bytes + size_of::() // I32: 4 bytes + (size_of::() - 2) // I48: 6 bytes + size_of::() // I64: 8 bytes ); } #[test] fn test_serialize_const_integers() { let record = Record::new(vec![Value::from_i64(0), Value::from_i64(1)]); let mut buf = Vec::new(); record.serialize(&mut buf); // [header_size, serial_type_0, serial_type_1] + no payload bytes let expected_header_size = 3; // 1 byte for header size + 2 bytes for serial types assert_eq!(buf.len(), expected_header_size); // Check header size assert_eq!(buf[0], expected_header_size as u8); assert_eq!(buf[1] as u64, u64::from(SerialType::const_int0())); // Should be 8 assert_eq!(buf[2] as u64, u64::from(SerialType::const_int1())); // Should be 9 assert_eq!(buf[1], 8); // ConstInt0 serial type assert_eq!(buf[2], 9); // ConstInt1 serial type } #[test] fn test_serialize_single_const_int0() { let record = Record::new(vec![Value::from_i64(0)]); let mut buf = Vec::new(); record.serialize(&mut buf); // Expected: [header_size=2, serial_type=8] assert_eq!(buf.len(), 2); assert_eq!(buf[0], 2); // Header size assert_eq!(buf[1], 8); // ConstInt0 serial type } #[test] fn test_serialize_float() { #[warn(clippy::approx_constant)] let record = Record::new(vec![Value::from_f64(3.15555)]); let mut buf = Vec::new(); record.serialize(&mut buf); let header_length = record.values.len() + 1; let header = &buf[0..header_length]; assert_eq!(header[0], header_length as u8); // Second byte should be serial type for FLOAT assert_eq!(header[1] as u64, u64::from(SerialType::f64())); // Check that the bytes after the header can be interpreted as the float let float_bytes = &buf[header_length..header_length + size_of::()]; let float = f64::from_be_bytes(float_bytes.try_into().unwrap()); assert_eq!(float, 3.15555); // Check that buffer length is correct assert_eq!(buf.len(), header_length + size_of::()); } #[test] fn test_serialize_text() { let text = "hello"; let record = Record::new(vec![Value::Text(Text::new(text))]); let mut buf = Vec::new(); record.serialize(&mut buf); let header_length = record.values.len() + 1; let header = &buf[0..header_length]; // First byte should be header size assert_eq!(header[0], header_length as u8); // Second byte should be serial type for TEXT, which is (len * 2 + 13) assert_eq!(header[1], (5 * 2 + 13) as u8); // Check the actual text bytes assert_eq!(&buf[2..7], b"hello"); // Check that buffer length is correct assert_eq!(buf.len(), header_length + text.len()); } #[test] fn test_serialize_blob() { let blob = vec![1, 2, 3, 4, 5]; let record = Record::new(vec![Value::Blob(blob.clone())]); let mut buf = Vec::new(); record.serialize(&mut buf); let header_length = record.values.len() + 1; let header = &buf[0..header_length]; // First byte should be header size assert_eq!(header[0], header_length as u8); // Second byte should be serial type for BLOB, which is (len * 2 + 12) assert_eq!(header[1], (5 * 2 + 12) as u8); // Check the actual blob bytes assert_eq!(&buf[2..7], &[1, 2, 3, 4, 5]); // Check that buffer length is correct assert_eq!(buf.len(), header_length + blob.len()); } #[test] fn test_serialize_mixed_types() { let text = "test"; let record = Record::new(vec![ Value::Null, Value::from_i64(42), Value::from_f64(3.15), Value::Text(Text::new(text)), ]); let mut buf = Vec::new(); record.serialize(&mut buf); let header_length = record.values.len() + 1; let header = &buf[0..header_length]; // First byte should be header size assert_eq!(header[0], header_length as u8); // Second byte should be serial type for NULL assert_eq!(header[1] as u64, u64::from(SerialType::null())); // Third byte should be serial type for I8 assert_eq!(header[2] as u64, u64::from(SerialType::i8())); // Fourth byte should be serial type for F64 assert_eq!(header[3] as u64, u64::from(SerialType::f64())); // Fifth byte should be serial type for TEXT, which is (len * 2 + 13) assert_eq!(header[4] as u64, (4 * 2 + 13) as u64); // Check that the bytes after the header can be interpreted as the correct values let mut cur_offset = header_length; let i8_bytes = &buf[cur_offset..cur_offset + size_of::()]; cur_offset += size_of::(); let f64_bytes = &buf[cur_offset..cur_offset + size_of::()]; cur_offset += size_of::(); let text_bytes = &buf[cur_offset..cur_offset + text.len()]; let val_int8 = i8::from_be_bytes(i8_bytes.try_into().unwrap()); let val_float = f64::from_be_bytes(f64_bytes.try_into().unwrap()); let val_text = String::from_utf8(text_bytes.to_vec()).unwrap(); assert_eq!(val_int8, 42); assert_eq!(val_float, 3.15); assert_eq!(val_text, "test"); // Check that buffer length is correct assert_eq!( buf.len(), header_length + size_of::() + size_of::() + text.len() ); } /// Before the Numeric refactor, ValueRef had separate Float(f64) and Integer(i64) /// variants. A raw f64::NAN could be stored in Float, and comparing two NaN floats /// via partial_cmp returned None. The .unwrap() in Ord::cmp and /// compare_immutable_single would then panic. /// /// Now Numeric::Float wraps NonNan, which rejects NaN at construction time. /// This makes it impossible to represent NaN in a ValueRef, so partial_cmp /// is total and can never return None for any representable value. #[test] fn test_valueref_partial_cmp_no_panic_on_nan() { use crate::numeric::nonnan::NonNan; // NonNan::new rejects NaN — this is the type-level guarantee that // prevents the old panic. No ValueRef::Float(NAN) can be constructed. assert!(NonNan::new(f64::NAN).is_none()); // from_f64(NAN) falls back to Null instead of storing a NaN float. assert_eq!(ValueRef::from_f64(f64::NAN), ValueRef::Null); // Exercise every representable float edge case through partial_cmp, // Ord::cmp, and compare_immutable_single — none of these can panic now. let values: Vec = vec![ ValueRef::Null, ValueRef::from_i64(0), ValueRef::from_i64(-1), ValueRef::from_i64(i64::MAX), ValueRef::from_i64(i64::MIN), ValueRef::from_f64(0.0), ValueRef::from_f64(-0.0), ValueRef::from_f64(1.5), ValueRef::from_f64(-1.5), ValueRef::from_f64(f64::MAX), ValueRef::from_f64(f64::MIN), ValueRef::from_f64(f64::MIN_POSITIVE), ValueRef::from_f64(f64::INFINITY), ValueRef::from_f64(f64::NEG_INFINITY), ValueRef::from_f64(f64::NAN), // becomes Null ValueRef::Text(TextRef::new("hello", TextSubtype::Text)), ValueRef::Text(TextRef::new("", TextSubtype::Text)), ValueRef::Blob(&[1, 2, 3]), ValueRef::Blob(&[]), ]; // partial_cmp must return Some for every pair — the old code panicked // here when either side was Float(NAN). for (i, a) in values.iter().enumerate() { for (j, b) in values.iter().enumerate() { let result = a.partial_cmp(b); assert!( result.is_some(), "partial_cmp returned None for values[{i}]={a:?} vs values[{j}]={b:?}" ); // Ord::cmp (which previously called partial_cmp().unwrap()) must agree. assert_eq!(result.unwrap(), a.cmp(b)); } } // compare_immutable_single is where the unwrap panic originally surfaced. for a in &values { for b in &values { let _ = compare_immutable_single(*a, *b, CollationSeq::Binary); } } // Antisymmetry holds for all pairs. for a in &values { for b in &values { let ab = a.cmp(b); let ba = b.cmp(a); assert_eq!(ab, ba.reverse(), "antisymmetry failed for {a:?} vs {b:?}"); } } } #[test] fn test_column_count_matches_values_written() { // Test with different numbers of values for num_values in 1..=10 { let values: Vec = (0..num_values).map(|i| Value::from_i64(i as i64)).collect(); let record = ImmutableRecord::from_values(&values, values.len()); let cnt = record.column_count(); assert_eq!( cnt, num_values, "column_count should be {num_values}, not {cnt}" ); } } } ================================================ FILE: core/util.rs ================================================ use crate::incremental::view::IncrementalView; use crate::numeric::StrToF64; use crate::schema::ColDef; use crate::sync::Mutex; use crate::translate::emitter::TransactionMode; use crate::translate::expr::{walk_expr, walk_expr_mut, WalkControl}; use crate::translate::plan::{JoinedTable, TableReferences}; use crate::translate::planner::{parse_row_id, TableMask}; use crate::types::IOResult; use crate::IO; use crate::{ schema::{Column, Schema, Table, Type}, types::{Value, ValueType}, LimboError, OpenFlags, Result, Statement, SymbolTable, }; use either::Either; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::future::Future; use std::sync::Arc; use tracing::{instrument, Level}; use turso_macros::match_ignore_ascii_case; use turso_parser::ast::{self, CreateTableBody, Expr, Literal, UnaryOperator}; use turso_parser::parser::Parser; #[macro_export] macro_rules! io_yield_one { ($c:expr) => { return Ok(IOResult::IO(IOCompletions::Single($c))); }; } #[macro_export] macro_rules! eq_ignore_ascii_case { ( $var:expr, $value:literal ) => {{ match_ignore_ascii_case!(match $var { $value => true, _ => false, }) }}; } #[macro_export] macro_rules! contains_ignore_ascii_case { ( $var:expr, $value:literal ) => {{ let compare_to_idx = $var.len().saturating_sub($value.len()); if $var.len() < $value.len() { false } else { let mut result = false; for i in 0..=compare_to_idx { if eq_ignore_ascii_case!(&$var[i..i + $value.len()], $value) { result = true; break; } } result } }}; } #[macro_export] macro_rules! starts_with_ignore_ascii_case { ( $var:expr, $value:literal ) => {{ if $var.len() < $value.len() { false } else { eq_ignore_ascii_case!(&$var[..$value.len()], $value) } }}; } #[macro_export] macro_rules! ends_with_ignore_ascii_case { ( $var:expr, $value:literal ) => {{ if $var.len() < $value.len() { false } else { eq_ignore_ascii_case!(&$var[$var.len() - $value.len()..], $value) } }}; } pub trait IOExt { fn block(&self, f: impl FnMut() -> Result>) -> Result; fn wait(&self, f: F) -> impl Future> + Send where F: FnMut() -> Result> + Send, T: Send; } impl IOExt for I { fn block(&self, mut f: impl FnMut() -> Result>) -> Result { Ok(loop { match f()? { IOResult::Done(v) => break v, IOResult::IO(io) => io.wait(self)?, } }) } async fn wait(&self, mut f: F) -> Result where F: FnMut() -> Result> + Send, T: Send, { Ok(loop { match f()? { IOResult::Done(v) => break v, IOResult::IO(io) => io.wait_async(self).await?, } }) } } // https://sqlite.org/lang_keywords.html const QUOTE_PAIRS: &[(char, char)] = &[ ('"', '"'), ('[', ']'), ('`', '`'), ('\'', '\''), // string sometimes used as identifier quoting ]; pub fn normalize_ident(identifier: &str) -> String { // quotes normalization already happened in the parser layer (see Name ast node implementation) // so, we only need to convert identifier string to lowercase identifier.to_lowercase() } /// Escape a SQL string literal payload for safe interpolation inside single quotes. pub fn escape_sql_string_literal(literal: &str) -> String { literal.replace('\'', "''") } pub const PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX: &str = "sqlite_autoindex_"; /// Unparsed index that comes from a sql query, i.e not an automatic index /// /// CREATE INDEX idx ON table_name(sql) pub struct UnparsedFromSqlIndex { pub table_name: String, pub root_page: i64, pub sql: String, } #[instrument(skip_all, level = Level::INFO)] pub fn parse_schema_rows( mut rows: Statement, schema: &mut Schema, syms: &SymbolTable, mv_tx: Option<(u64, TransactionMode)>, _existing_views: HashMap>>, ) -> Result<()> { rows.set_mv_tx(mv_tx); let mv_store = rows.mv_store().clone(); // TODO: if we IO, this unparsed indexes is lost. Will probably need some state between // IO runs let mut from_sql_indexes = Vec::with_capacity(10); let mut automatic_indices = HashMap::with_capacity_and_hasher(10, Default::default()); // Store DBSP state table root pages: view_name -> dbsp_state_root_page let mut dbsp_state_roots: HashMap = HashMap::default(); // Store DBSP state table index root pages: view_name -> dbsp_state_index_root_page let mut dbsp_state_index_roots: HashMap = HashMap::default(); // Store materialized view info (SQL and root page) for later creation let mut materialized_view_info: HashMap = HashMap::default(); // TODO: How do we ensure that the I/O we submitted to // read the schema is actually complete? rows.run_with_row_callback(|row| { let ty = row.get::<&str>(0)?; let name = row.get::<&str>(1)?; let table_name = row.get::<&str>(2)?; let root_page = row.get::(3)?; let sql = row.get::<&str>(4).ok(); schema.handle_schema_row( ty, name, table_name, root_page, sql, syms, &mut from_sql_indexes, &mut automatic_indices, &mut dbsp_state_roots, &mut dbsp_state_index_roots, &mut materialized_view_info, ) })?; schema.populate_indices( syms, from_sql_indexes, automatic_indices, mv_store.is_some(), )?; schema.populate_materialized_views( materialized_view_info, dbsp_state_roots, dbsp_state_index_roots, )?; Ok(()) } fn cmp_numeric_strings(num_str: &str, other: &str) -> bool { fn parse(s: &str) -> Option> { if let Ok(i) = s.parse::() { Some(Either::Left(i)) } else if let Ok(f) = s.parse::() { Some(Either::Right(f)) } else { None } } match (parse(num_str), parse(other)) { (Some(Either::Left(i1)), Some(Either::Left(i2))) => i1 == i2, (Some(Either::Right(f1)), Some(Either::Right(f2))) => f1 == f2, // Integer and Float are NOT equivalent even if values match, // because result type of operations depends on operand types (Some(Either::Left(_)), Some(Either::Right(_))) | (Some(Either::Right(_)), Some(Either::Left(_))) => false, _ => num_str == other, } } pub fn check_ident_equivalency(ident1: &str, ident2: &str) -> bool { fn strip_quotes(identifier: &str) -> &str { for &(start, end) in QUOTE_PAIRS { if identifier.starts_with(start) && identifier.ends_with(end) { return &identifier[1..identifier.len() - 1]; } } identifier } strip_quotes(ident1).eq_ignore_ascii_case(strip_quotes(ident2)) } pub fn module_name_from_sql(sql: &str) -> Result<&str> { if let Some(start) = sql.find("USING") { let start = start + 6; // stop at the first space, semicolon, or parenthesis let end = sql[start..] .find(|c: char| c.is_whitespace() || c == ';' || c == '(') .unwrap_or(sql.len() - start) + start; Ok(sql[start..end].trim()) } else { Err(LimboError::InvalidArgument( "Expected 'USING' in module name".to_string(), )) } } // CREATE VIRTUAL TABLE table_name USING module_name(arg1, arg2, ...); // CREATE VIRTUAL TABLE table_name USING module_name; pub fn module_args_from_sql(sql: &str) -> Result> { if !sql.contains('(') { return Ok(vec![]); } let start = sql.find('(').ok_or_else(|| { LimboError::InvalidArgument("Expected '(' in module argument list".to_string()) })? + 1; let end = sql.rfind(')').ok_or_else(|| { LimboError::InvalidArgument("Expected ')' in module argument list".to_string()) })?; let mut args = Vec::new(); let mut current_arg = String::new(); let mut chars = sql[start..end].chars().peekable(); let mut in_quotes = false; while let Some(c) = chars.next() { match c { '\'' => { if in_quotes { if chars.peek() == Some(&'\'') { // Escaped quote current_arg.push('\''); chars.next(); } else { in_quotes = false; args.push(turso_ext::Value::from_text(current_arg.trim().to_string())); current_arg.clear(); // Skip until comma or end while let Some(&nc) = chars.peek() { if nc == ',' { chars.next(); // Consume comma break; } else if nc.is_whitespace() { chars.next(); } else { return Err(LimboError::InvalidArgument( "Unexpected characters after quoted argument".to_string(), )); } } } } else { in_quotes = true; } } ',' => { if !in_quotes { if !current_arg.trim().is_empty() { args.push(turso_ext::Value::from_text(current_arg.trim().to_string())); current_arg.clear(); } } else { current_arg.push(c); } } _ => { current_arg.push(c); } } } if !current_arg.trim().is_empty() && !in_quotes { args.push(turso_ext::Value::from_text(current_arg.trim().to_string())); } if in_quotes { return Err(LimboError::InvalidArgument( "Unterminated string literal in module arguments".to_string(), )); } Ok(args) } pub fn check_literal_equivalency(lhs: &Literal, rhs: &Literal) -> bool { match (lhs, rhs) { (Literal::Numeric(n1), Literal::Numeric(n2)) => cmp_numeric_strings(n1, n2), (Literal::String(s1), Literal::String(s2)) => s1 == s2, (Literal::Blob(b1), Literal::Blob(b2)) => b1 == b2, (Literal::Keyword(k1), Literal::Keyword(k2)) => check_ident_equivalency(k1, k2), (Literal::Null, Literal::Null) => true, (Literal::CurrentDate, Literal::CurrentDate) => true, (Literal::CurrentTime, Literal::CurrentTime) => true, (Literal::CurrentTimestamp, Literal::CurrentTimestamp) => true, _ => false, } } /// Returns true if every Column/RowId table reference in `expr` is contained /// in `allowed`. Constants (no table refs) pass. pub(crate) fn expr_tables_subset_of( expr: &Expr, table_references: &TableReferences, allowed: &TableMask, ) -> bool { let mut ok = true; let _ = walk_expr(expr, &mut |e: &Expr| -> Result { match e { Expr::Column { table, .. } | Expr::RowId { table, .. } => { if let Some(idx) = table_references .joined_tables() .iter() .position(|t| t.internal_id == *table) { if !allowed.contains_table(idx) { ok = false; return Ok(WalkControl::SkipChildren); } } // Outer query references are already in scope — allow them. } _ => {} } Ok(WalkControl::Continue) }); ok } /// bind AST identifiers to either Column or Rowid if possible pub fn simple_bind_expr( joined_table: &JoinedTable, result_columns: &[ast::ResultColumn], expr: &mut ast::Expr, ) -> Result<()> { let internal_id = joined_table.internal_id; walk_expr_mut(expr, &mut |expr: &mut ast::Expr| -> Result { #[allow(clippy::single_match)] match expr { Expr::Id(id) => { let normalized_id = normalize_ident(id.as_str()); for result_column in result_columns.iter() { if let ast::ResultColumn::Expr(result, Some(ast::As::As(alias))) = result_column { if alias.as_str().eq_ignore_ascii_case(&normalized_id) { *expr = *result.clone(); return Ok(WalkControl::Continue); } } } let col_idx = joined_table.columns().iter().position(|c| { c.name .as_ref() .is_some_and(|name| name.eq_ignore_ascii_case(&normalized_id)) }); if let Some(col_idx) = col_idx { let col = joined_table.table.columns().get(col_idx).unwrap(); *expr = ast::Expr::Column { database: None, table: internal_id, column: col_idx, is_rowid_alias: col.is_rowid_alias(), }; } else { // only if we haven't found a match, check for explicit rowid reference let is_btree_table = matches!(joined_table.table, Table::BTree(_)); if is_btree_table { if let Some(rowid) = parse_row_id(&normalized_id, internal_id, || false)? { *expr = rowid; } } } } _ => {} } Ok(WalkControl::Continue) })?; Ok(()) } pub fn try_substitute_parameters( pattern: &Expr, parameters: &HashMap, ) -> Option> { match pattern { Expr::FunctionCall { name, distinctness, args, order_by, filter_over, } => { let mut substituted = Vec::new(); for arg in args { substituted.push(try_substitute_parameters(arg, parameters)?); } Some(Box::new(Expr::FunctionCall { args: substituted, distinctness: *distinctness, name: name.clone(), order_by: order_by.clone(), filter_over: filter_over.clone(), })) } Expr::Variable(var) => { if var.name.is_some() { return None; } let Ok(var) = i32::try_from(var.index.get()) else { return None; }; Some(Box::new(parameters.get(&var)?.clone())) } _ => Some(Box::new(pattern.clone())), } } pub fn try_capture_parameters(pattern: &Expr, query: &Expr) -> Option> { let mut captured = HashMap::default(); match (pattern, query) { ( Expr::FunctionCall { name: name1, distinctness: distinct1, args: args1, order_by: order1, filter_over: filter1, }, Expr::FunctionCall { name: name2, distinctness: distinct2, args: args2, order_by: order2, filter_over: filter2, }, ) => { if !name1.as_str().eq_ignore_ascii_case(name2.as_str()) { return None; } if distinct1.is_some() || distinct2.is_some() { return None; } if !order1.is_empty() || !order2.is_empty() { return None; } if filter1.filter_clause.is_some() || filter1.over_clause.is_some() { return None; } if filter2.filter_clause.is_some() || filter2.over_clause.is_some() { return None; } for (arg1, arg2) in args1.iter().zip(args2.iter()) { let result = try_capture_parameters(arg1, arg2)?; captured.extend(result); } Some(captured) } (Expr::Variable(var), expr) => { if var.name.is_some() { return None; } let Ok(var) = i32::try_from(var.index.get()) else { return None; }; captured.insert(var, expr.clone()); Some(captured) } ( Expr::Id(_) | Expr::Name(_) | Expr::Column { .. }, Expr::Id(_) | Expr::Name(_) | Expr::Column { .. }, ) => { if pattern == query { Some(captured) } else { None } } (_, _) => None, } } /// Returns the number of column arguments for FTS functions. /// FTS functions have column arguments followed by non-column arguments: /// - fts_match(col1, col2, ..., query_string) -> columns = args.len() - 1 /// - fts_score(col1, col2, ..., query_string) -> columns = args.len() - 1 /// - fts_highlight(col1, col2, ..., before_tag, after_tag, query_string) -> columns = args.len() - 3 /// /// Returns 0 for non-FTS functions. /// Specific for FTS but cannot gate behind feature = "fts" so it must /// live in util.rs :/ pub fn count_fts_column_args(expr: &Expr) -> usize { match expr { Expr::FunctionCall { name, args, .. } => { let name_lower = name.as_str().to_lowercase(); match name_lower.as_str() { "fts_match" | "fts_score" => args.len().saturating_sub(1), "fts_highlight" => args.len().saturating_sub(3), _ => 0, } } _ => 0, } } /// Match FTS function calls where column arguments can appear in any order. /// /// FTS functions like `fts_match(col1, col2, 'query')` should match /// `fts_match(col2, col1, 'query')` as long as the same columns are used. /// /// Semi-specific for FTS but cannot gate behind feature = "fts" so it must /// live in util.rs :/ pub fn try_capture_parameters_column_agnostic( pattern: &Expr, // pattern expression from index definition query: &Expr, // the actual query expression num_column_args: usize, // number of leading column arguments ) -> Option> { // If not a function call or no column args, fall back to standard matching if num_column_args == 0 { return try_capture_parameters(pattern, query); } let ( Expr::FunctionCall { name: pattern_name, distinctness: pattern_distinct, args: pattern_args, order_by: pattern_order, filter_over: pattern_filter, }, Expr::FunctionCall { name: query_name, distinctness: query_distinct, args: query_args, order_by: query_order, filter_over: query_filter, }, ) = (pattern, query) else { return try_capture_parameters(pattern, query); }; // Function names must match if !pattern_name .as_str() .eq_ignore_ascii_case(query_name.as_str()) { return None; } // Argument counts must match if pattern_args.len() != query_args.len() { return None; } // Distinctness must match (we don't support it) if pattern_distinct.is_some() || query_distinct.is_some() { return None; } // ORDER BY within function not supported if !pattern_order.is_empty() || !query_order.is_empty() { return None; } // Filter/over clause not supported if pattern_filter.filter_clause.is_some() || pattern_filter.over_clause.is_some() { return None; } if query_filter.filter_clause.is_some() || query_filter.over_clause.is_some() { return None; } let mut captured = HashMap::default(); // Split args into column args (reorderable) and remaining args (positional) let pattern_col_args = &pattern_args[..num_column_args]; let query_col_args = &query_args[..num_column_args]; let pattern_rest = &pattern_args[num_column_args..]; let query_rest = &query_args[num_column_args..]; // For column arguments: check that the same set of columns is used (order-independent) // We use a greedy matching approach: for each query column, find a matching pattern column let mut matched_pattern_indices: HashSet = HashSet::default(); for query_col in query_col_args { let mut found_match = false; for (i, pattern_col) in pattern_col_args.iter().enumerate() { if matched_pattern_indices.contains(&i) { continue; } if exprs_are_equivalent(pattern_col, query_col) { matched_pattern_indices.insert(i); found_match = true; break; } } if !found_match { return None; } } // All pattern columns must be matched if matched_pattern_indices.len() != pattern_col_args.len() { return None; } // Remaining args must match positionally (includes the query string parameter) for (pattern_arg, query_arg) in pattern_rest.iter().zip(query_rest.iter()) { let result = try_capture_parameters(pattern_arg, query_arg)?; captured.extend(result); } Some(captured) } /// This function is used to determine whether two expressions are logically /// equivalent in the context of queries, even if their representations /// differ. e.g.: `SUM(x)` and `sum(x)`, `x + y` and `y + x` pub fn exprs_are_equivalent(expr1: &Expr, expr2: &Expr) -> bool { match (expr1, expr2) { ( Expr::Between { lhs: lhs1, not: not1, start: start1, end: end1, }, Expr::Between { lhs: lhs2, not: not2, start: start2, end: end2, }, ) => { not1 == not2 && exprs_are_equivalent(lhs1, lhs2) && exprs_are_equivalent(start1, start2) && exprs_are_equivalent(end1, end2) } (Expr::Binary(lhs1, op1, rhs1), Expr::Binary(lhs2, op2, rhs2)) => { op1 == op2 && ((exprs_are_equivalent(lhs1, lhs2) && exprs_are_equivalent(rhs1, rhs2)) || (op1.is_commutative() && exprs_are_equivalent(lhs1, rhs2) && exprs_are_equivalent(rhs1, lhs2))) } ( Expr::Case { base: base1, when_then_pairs: pairs1, else_expr: else1, }, Expr::Case { base: base2, when_then_pairs: pairs2, else_expr: else2, }, ) => { base1 == base2 && pairs1.len() == pairs2.len() && pairs1.iter().zip(pairs2).all(|((w1, t1), (w2, t2))| { exprs_are_equivalent(w1, w2) && exprs_are_equivalent(t1, t2) }) && else1 == else2 } ( Expr::Cast { expr: expr1, type_name: type1, }, Expr::Cast { expr: expr2, type_name: type2, }, ) => { exprs_are_equivalent(expr1, expr2) && match (type1, type2) { (Some(t1), Some(t2)) => t1.name.eq_ignore_ascii_case(&t2.name), _ => false, } } (Expr::Collate(expr1, collation1), Expr::Collate(expr2, collation2)) => { // TODO: check correctness of comparing colation as strings exprs_are_equivalent(expr1, expr2) && collation1 .as_str() .eq_ignore_ascii_case(collation2.as_str()) } ( Expr::FunctionCall { name: name1, distinctness: distinct1, args: args1, order_by: order1, filter_over: filter1, }, Expr::FunctionCall { name: name2, distinctness: distinct2, args: args2, order_by: order2, filter_over: filter2, }, ) => { name1.as_str().eq_ignore_ascii_case(name2.as_str()) && distinct1 == distinct2 && args1 == args2 && order1 == order2 && filter1 == filter2 } ( Expr::FunctionCallStar { name: name1, filter_over: filter1, }, Expr::FunctionCallStar { name: name2, filter_over: filter2, }, ) => { name1.as_str().eq_ignore_ascii_case(name2.as_str()) && match (&filter1.filter_clause, &filter2.filter_clause) { (Some(expr1), Some(expr2)) => exprs_are_equivalent(expr1, expr2), (None, None) => true, _ => false, } && filter1.over_clause == filter2.over_clause } (Expr::NotNull(expr1), Expr::NotNull(expr2)) => exprs_are_equivalent(expr1, expr2), (Expr::IsNull(expr1), Expr::IsNull(expr2)) => exprs_are_equivalent(expr1, expr2), (Expr::Literal(lit1), Expr::Literal(lit2)) => check_literal_equivalency(lit1, lit2), (Expr::Id(id1), Expr::Id(id2)) => check_ident_equivalency(id1.as_str(), id2.as_str()), (Expr::Unary(op1, expr1), Expr::Unary(op2, expr2)) => { op1 == op2 && exprs_are_equivalent(expr1, expr2) } (Expr::Variable(val), Expr::Variable(val2)) => val == val2, (Expr::Parenthesized(exprs1), Expr::Parenthesized(exprs2)) => { exprs1.len() == exprs2.len() && exprs1 .iter() .zip(exprs2) .all(|(e1, e2)| exprs_are_equivalent(e1, e2)) } (Expr::Parenthesized(exprs1), exprs2) | (exprs2, Expr::Parenthesized(exprs1)) => { exprs1.len() == 1 && exprs_are_equivalent(&exprs1[0], exprs2) } (Expr::Qualified(tn1, cn1), Expr::Qualified(tn2, cn2)) => { check_ident_equivalency(tn1.as_str(), tn2.as_str()) && check_ident_equivalency(cn1.as_str(), cn2.as_str()) } (Expr::DoublyQualified(sn1, tn1, cn1), Expr::DoublyQualified(sn2, tn2, cn2)) => { check_ident_equivalency(sn1.as_str(), sn2.as_str()) && check_ident_equivalency(tn1.as_str(), tn2.as_str()) && check_ident_equivalency(cn1.as_str(), cn2.as_str()) } ( Expr::InList { lhs: lhs1, not: not1, rhs: rhs1, }, Expr::InList { lhs: lhs2, not: not2, rhs: rhs2, }, ) => { *not1 == *not2 && exprs_are_equivalent(lhs1, lhs2) && rhs1.len() == rhs2.len() && rhs1 .iter() .zip(rhs2.iter()) .all(|(a, b)| exprs_are_equivalent(a, b)) } ( Expr::Column { database: db1, is_rowid_alias: r1, table: tbl_1, column: col_1, }, Expr::Column { database: db2, is_rowid_alias: r2, table: tbl_2, column: col_2, }, ) => tbl_1 == tbl_2 && col_1 == col_2 && db1 == db2 && r1 == r2, // fall back to naive equality check _ => expr1 == expr2, } } /// "evaluate" an expression to determine if it contains a poisonous NULL /// which will propagate through most expressions and result in it's evaluation /// into NULL. This is used to prevent things like the following: /// `ALTER TABLE t ADD COLUMN (a NOT NULL DEFAULT (NULL + 5)` pub(crate) fn expr_contains_null(expr: &ast::Expr) -> bool { let mut contains_null = false; let _ = walk_expr(expr, &mut |expr: &ast::Expr| -> Result { if let ast::Expr::Literal(ast::Literal::Null) = expr { contains_null = true; return Ok(WalkControl::SkipChildren); } Ok(WalkControl::Continue) }); // infallible contains_null } // this function returns the affinity type and whether the type name was exactly "INTEGER" // https://www.sqlite.org/datatype3.html pub(crate) fn type_from_name(type_name: &str) -> (Type, bool) { let type_name = type_name.as_bytes(); if type_name.is_empty() { return (Type::Blob, false); } if eq_ignore_ascii_case!(type_name, b"INTEGER") { return (Type::Integer, true); } if contains_ignore_ascii_case!(type_name, b"INT") { return (Type::Integer, false); } if let Some(ty) = type_name.windows(4).find_map(|s| { match_ignore_ascii_case!(match s { b"CHAR" | b"CLOB" | b"TEXT" => Some(Type::Text), b"BLOB" => Some(Type::Blob), b"REAL" | b"FLOA" | b"DOUB" => Some(Type::Real), _ => None, }) }) { return (ty, false); } (Type::Numeric, false) } pub fn columns_from_create_table_body( body: &turso_parser::ast::CreateTableBody, ) -> crate::Result> { let CreateTableBody::ColumnsAndConstraints { columns, .. } = body else { return Err(crate::LimboError::ParseError( "CREATE TABLE body must contain columns and constraints".to_string(), )); }; columns .iter() .map(Column::try_from) .collect::>>() } #[derive(Debug, Default, PartialEq)] pub struct OpenOptions<'a> { /// The authority component of the URI. may be 'localhost' or empty pub authority: Option<&'a str>, /// The normalized path to the database file pub path: String, /// The vfs query parameter causes the database connection to be opened using the VFS called NAME pub vfs: Option, /// read-only, read-write, read-write and created if it does not exist, or pure in-memory database that never interacts with disk pub mode: OpenMode, /// Attempt to set the permissions of the new database file to match the existing file "filename". pub modeof: Option, /// Specifies Cache mode shared | private pub cache: CacheMode, /// immutable=1|0 specifies that the database is stored on read-only media pub immutable: bool, // The encryption cipher pub cipher: Option, // The encryption key in hex format pub hexkey: Option, } pub const MEMORY_PATH: &str = ":memory:"; #[derive(Clone, Default, Debug, Copy, PartialEq)] pub enum OpenMode { ReadOnly, ReadWrite, Memory, #[default] ReadWriteCreate, } #[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum CacheMode { #[default] Private, Shared, } impl From<&str> for CacheMode { fn from(s: &str) -> Self { match s { "private" => CacheMode::Private, "shared" => CacheMode::Shared, _ => CacheMode::Private, } } } impl OpenMode { pub fn from_str(s: &str) -> Result { let s_bytes = s.trim().as_bytes(); match_ignore_ascii_case!(match s_bytes { b"ro" => Ok(OpenMode::ReadOnly), b"rw" => Ok(OpenMode::ReadWrite), b"memory" => Ok(OpenMode::Memory), b"rwc" => Ok(OpenMode::ReadWriteCreate), _ => Err(LimboError::InvalidArgument(format!( "Invalid mode: '{s}'. Expected one of 'ro', 'rw', 'memory', 'rwc'" ))), }) } } fn is_windows_path(path: &str) -> bool { path.len() >= 3 && path.chars().nth(1) == Some(':') && (path.chars().nth(2) == Some('/') || path.chars().nth(2) == Some('\\')) } /// converts windows-style paths to forward slashes, per SQLite spec. fn normalize_windows_path(path: &str) -> String { let mut normalized = path.replace("\\", "/"); // remove duplicate slashes (`//` → `/`) while normalized.contains("//") { normalized = normalized.replace("//", "/"); } // if absolute windows path (`C:/...`), ensure it starts with `/` if normalized.len() >= 3 && !normalized.starts_with('/') && normalized.chars().nth(1) == Some(':') && normalized.chars().nth(2) == Some('/') { normalized.insert(0, '/'); } normalized } impl<'a> OpenOptions<'a> { /// Parses a SQLite URI, handling Windows and Unix paths separately. pub fn parse(uri: &'a str) -> Result> { if !uri.starts_with("file:") { return Ok(OpenOptions { path: uri.to_string(), ..Default::default() }); } let mut opts = OpenOptions::default(); let without_scheme = &uri[5..]; let (without_fragment, _) = without_scheme .split_once('#') .unwrap_or((without_scheme, "")); let (without_query, query) = without_fragment .split_once('?') .unwrap_or((without_fragment, "")); parse_query_params(query, &mut opts)?; // handle authority + path separately if let Some(after_slashes) = without_query.strip_prefix("//") { let (authority, path) = after_slashes.split_once('/').unwrap_or((after_slashes, "")); // sqlite allows only `localhost` or empty authority. if !(authority.is_empty() || authority == "localhost") { return Err(LimboError::InvalidArgument(format!( "Invalid authority '{authority}'. Only '' or 'localhost' allowed." ))); } opts.authority = if authority.is_empty() { None } else { Some(authority) }; if is_windows_path(path) { opts.path = normalize_windows_path(&decode_percent(path)); } else if !path.is_empty() { opts.path = format!("/{}", decode_percent(path)); } else { opts.path = String::new(); } } else { // no authority, must be a normal absolute or relative path. opts.path = decode_percent(without_query); } Ok(opts) } pub fn get_flags(&self) -> Result { // Only use modeof if we're in a mode that can create files if self.mode != OpenMode::ReadWriteCreate && self.modeof.is_some() { return Err(LimboError::InvalidArgument( "modeof is not applicable without mode=rwc".to_string(), )); } // If modeof is not applicable or file doesn't exist, use default flags Ok(match self.mode { OpenMode::ReadWriteCreate => OpenFlags::Create, OpenMode::ReadOnly => OpenFlags::ReadOnly, _ => OpenFlags::default(), }) } } // parses query parameters and updates OpenOptions fn parse_query_params(query: &str, opts: &mut OpenOptions) -> Result<()> { for param in query.split('&') { if let Some((key, value)) = param.split_once('=') { let decoded_value = decode_percent(value); match key { "mode" => opts.mode = OpenMode::from_str(value)?, "modeof" => opts.modeof = Some(decoded_value), "cache" => opts.cache = decoded_value.as_str().into(), "immutable" => opts.immutable = decoded_value == "1", "vfs" => opts.vfs = Some(decoded_value), "cipher" => opts.cipher = Some(decoded_value), "hexkey" => opts.hexkey = Some(decoded_value), _ => {} } } } Ok(()) } /// Decodes percent-encoded characters /// this function was adapted from the 'urlencoding' crate. MIT pub fn decode_percent(uri: &str) -> String { let from_hex_digit = |digit: u8| -> Option { match digit { b'0'..=b'9' => Some(digit - b'0'), b'A'..=b'F' => Some(digit - b'A' + 10), b'a'..=b'f' => Some(digit - b'a' + 10), _ => None, } }; let offset = uri.chars().take_while(|&c| c != '%').count(); if offset >= uri.len() { return uri.to_string(); } let mut decoded: Vec = Vec::with_capacity(uri.len()); let (ascii, mut data) = uri.as_bytes().split_at(offset); decoded.extend_from_slice(ascii); loop { let mut parts = data.splitn(2, |&c| c == b'%'); let non_escaped_part = parts.next().unwrap(); let rest = parts.next(); if rest.is_none() && decoded.is_empty() { return String::from_utf8_lossy(data).to_string(); } decoded.extend_from_slice(non_escaped_part); match rest { Some(rest) => match rest.get(0..2) { Some([first, second]) => match from_hex_digit(*first) { Some(first_val) => match from_hex_digit(*second) { Some(second_val) => { decoded.push((first_val << 4) | second_val); data = &rest[2..]; } None => { decoded.extend_from_slice(&[b'%', *first]); data = &rest[1..]; } }, None => { decoded.push(b'%'); data = rest; } }, _ => { decoded.push(b'%'); decoded.extend_from_slice(rest); break; } }, None => break, } } String::from_utf8_lossy(&decoded).to_string() } pub fn trim_ascii_whitespace(s: &str) -> &str { let bytes = s.as_bytes(); let start = bytes .iter() .position(|&b| !b.is_ascii_whitespace()) .unwrap_or(bytes.len()); let end = bytes .iter() .rposition(|&b| !b.is_ascii_whitespace()) .map(|i| i + 1) .unwrap_or(0); if start <= end { &s[start..end] } else { "" } } /// NUMERIC Casting a TEXT or BLOB value into NUMERIC yields either an INTEGER or a REAL result. /// If the input text looks like an integer (there is no decimal point nor exponent) and the value /// is small enough to fit in a 64-bit signed integer, then the result will be INTEGER. /// Input text that looks like floating point (there is a decimal point and/or an exponent) /// and the text describes a value that can be losslessly converted back and forth between IEEE 754 /// 64-bit float and a 51-bit signed integer, then the result is INTEGER. (In the previous sentence, /// a 51-bit integer is specified since that is one bit less than the length of the mantissa of an /// IEEE 754 64-bit float and thus provides a 1-bit of margin for the text-to-float conversion operation.) /// Any text input that describes a value outside the range of a 64-bit signed integer yields a REAL result. /// Casting a REAL or INTEGER value to NUMERIC is a no-op, even if a real value could be losslessly converted to an integer. /// /// `lossless`: If `true`, rejects the input if any characters remain after the numeric prefix (strict / exact conversion). pub fn checked_cast_text_to_numeric(text: &str, lossless: bool) -> std::result::Result { // sqlite will parse the first N digits of a string to numeric value, then determine // whether _that_ value is more likely a real or integer value. e.g. // '-100234-2344.23e14' evaluates to -100234 instead of -100234.0 let original_len = text.trim().len(); let (kind, text) = parse_numeric_str(text)?; if original_len != text.len() && lossless { return Err(()); } match kind { ValueType::Integer => match text.parse::() { Ok(i) => Ok(Value::from_i64(i)), Err(e) => { if matches!( e.kind(), std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow ) { // if overflow, we return the representation as a real. // we have to match sqlite exactly here, so we match sqlite3AtoF let value = text.parse::().unwrap_or_default(); let factor = 10f64.powi(15 - value.abs().log10().ceil() as i32); Ok(Value::from_f64((value * factor).round() / factor)) } else { Err(()) } } }, ValueType::Float => Ok(text .parse::() .map_or(Value::from_f64(0.0), Value::from_f64)), _ => unreachable!(), } } fn parse_numeric_str(text: &str) -> Result<(ValueType, &str), ()> { let text = text.trim(); let bytes = text.as_bytes(); if matches!( bytes, [] | [b'e', ..] | [b'E', ..] | [b'.', b'e' | b'E', ..] ) { return Err(()); } let mut end = 0; let mut has_decimal = false; let mut has_exponent = false; if bytes[0] == b'-' || bytes[0] == b'+' { end = 1; } while end < bytes.len() { match bytes[end] { b'0'..=b'9' => end += 1, b'.' if !has_decimal && !has_exponent => { has_decimal = true; end += 1; } b'e' | b'E' if !has_exponent => { has_exponent = true; end += 1; // allow exponent sign if end < bytes.len() && (bytes[end] == b'+' || bytes[end] == b'-') { end += 1; } } _ => break, } } if end == 0 || (end == 1 && (bytes[0] == b'-' || bytes[0] == b'+')) { return Err(()); } // edge case: if it ends with exponent, strip and cast valid digits as float let last = bytes[end - 1]; if last.eq_ignore_ascii_case(&b'e') { return Ok((ValueType::Float, &text[0..end - 1])); // edge case: ends with extponent / sign } else if has_exponent && (last == b'-' || last == b'+') { return Ok((ValueType::Float, &text[0..end - 2])); } Ok(( if !has_decimal && !has_exponent { ValueType::Integer } else { ValueType::Float }, &text[..end], )) } // Check if float can be converted to integer for INTEGER PRIMARY KEY columns. // SQLite uses sqlite3VdbeIntegerAffinity which requires: // 1. The float must round-trip correctly (float -> int -> float gives same value) // 2. The integer must be strictly between i64::MIN and i64::MAX (exclusive) // // This matches SQLite's check: ix > SMALLEST_INT64 && ix < LARGEST_INT64 pub fn cast_real_to_integer(float: f64) -> std::result::Result { // Must be finite and a whole number (no fractional part) if !float.is_finite() || float.trunc() != float { return Err(()); } // Convert to i64, clamping to i64 range if necessary // Note: Rust's f64 as i64 saturates to i64::MIN/MAX for out-of-range values let int_val = float as i64; // SQLite requires the value to be STRICTLY between i64::MIN and i64::MAX // (i.e., ix > SMALLEST_INT64 && ix < LARGEST_INT64) if int_val == i64::MIN || int_val == i64::MAX { return Err(()); } // Verify round-trip: converting back to f64 must give the same value // This matches SQLite's check: pMem->u.r == ix if (int_val as f64) != float { return Err(()); } Ok(int_val) } // we don't need to verify the numeric literal here, as it is already verified by the parser pub fn parse_numeric_literal(text: &str) -> Result { // a single extra underscore ("_") character can exist between any two digits let text = if text.contains('_') { std::borrow::Cow::Owned(text.replace('_', "")) } else { std::borrow::Cow::Borrowed(text) }; if text.starts_with("0x") || text.starts_with("0X") { let value = u64::from_str_radix(&text[2..], 16)? as i64; return Ok(Value::from_i64(value)); } else if text.starts_with("-0x") || text.starts_with("-0X") { let value = u64::from_str_radix(&text[3..], 16)? as i64; if value == i64::MIN { return Err(LimboError::IntegerOverflow); } return Ok(Value::from_i64(-value)); } if let Ok(int_value) = text.parse::() { return Ok(Value::from_i64(int_value)); } let Some(StrToF64::Fractional(float) | StrToF64::Decimal(float)) = crate::numeric::str_to_f64(text) else { unreachable!(); }; Ok(Value::Numeric(crate::numeric::Numeric::Float(float))) } pub fn parse_signed_number(expr: &Expr) -> Result { match expr { Expr::Literal(Literal::Numeric(num)) => parse_numeric_literal(num), Expr::Unary(op, expr) => match (op, expr.as_ref()) { (UnaryOperator::Negative, Expr::Literal(Literal::Numeric(num))) => { let data = "-".to_owned() + &num.to_string(); parse_numeric_literal(&data) } (UnaryOperator::Positive, Expr::Literal(Literal::Numeric(num))) => { parse_numeric_literal(num) } _ => Err(LimboError::InvalidArgument( "signed-number must follow the format: ([+|-] numeric-literal)".to_string(), )), }, _ => Err(LimboError::InvalidArgument( "signed-number must follow the format: ([+|-] numeric-literal)".to_string(), )), } } pub fn parse_string(expr: &Expr) -> Result { match expr { Expr::Name(name) if name.quoted_with('\'') => Ok(name.as_str().to_string()), _ => Err(LimboError::InvalidArgument(format!( "string parameter expected, got {expr:?} instead" ))), } } #[allow(unused)] pub fn parse_pragma_bool(expr: &Expr) -> Result { const TRUE_VALUES: &[&str] = &["yes", "true", "on"]; const FALSE_VALUES: &[&str] = &["no", "false", "off"]; if let Ok(number) = parse_signed_number(expr) { if let Value::Numeric(crate::numeric::Numeric::Integer(x @ (0 | 1))) = number { return Ok(x != 0); } } else if let Expr::Name(name) = expr { let ident = normalize_ident(name.as_str()); if TRUE_VALUES.contains(&ident.as_str()) { return Ok(true); } if FALSE_VALUES.contains(&ident.as_str()) { return Ok(false); } } Err(LimboError::InvalidArgument( "boolean pragma value must be either 0|1 integer or yes|true|on|no|false|off token" .to_string(), )) } /// Extract column name from an expression (e.g., for SELECT clauses) pub fn extract_column_name_from_expr(expr: impl AsRef) -> Option { match expr.as_ref() { ast::Expr::Id(name) => Some(name.as_str().to_string()), ast::Expr::DoublyQualified(_, _, name) | ast::Expr::Qualified(_, name) => { Some(normalize_ident(name.as_str())) } _ => None, } } /// Information about a table referenced in a view #[derive(Debug, Clone)] pub struct ViewTable { /// Unqualified table name, normalized. pub name: String, /// Database qualifier if present, normalized. pub db_name: Option, /// Optional alias (e.g., "c" in "FROM customers c") pub alias: Option, } /// Information about a column in the view's output #[derive(Debug, Clone)] pub struct ViewColumn { /// Index into ViewColumnSchema.tables indicating which table this column comes from /// For computed columns or constants, this will be usize::MAX pub table_index: usize, /// The actual column definition pub column: Column, } /// Schema information for a view, tracking which columns come from which tables #[derive(Debug, Clone)] pub struct ViewColumnSchema { /// All tables referenced by the view (in order of appearance) pub tables: Vec, /// The view's output columns with their table associations pub columns: Vec, } impl ViewColumnSchema { /// Get all columns as a flat vector (without table association info) pub fn flat_columns(&self) -> Vec { self.columns.iter().map(|vc| vc.column.clone()).collect() } /// Get columns that belong to a specific table pub fn table_columns(&self, table_index: usize) -> Vec { self.columns .iter() .filter(|vc| vc.table_index == table_index) .map(|vc| vc.column.clone()) .collect() } } /// Walk all expressions in a SELECT statement, including subqueries. pub fn walk_select_expressions(select: &ast::Select, func: &mut F) -> Result<()> where F: FnMut(&ast::Expr) -> Result, { walk_select_expressions_inner(select, func) } pub fn validate_aggregate_function_tail( filter_over: &ast::FunctionTail, order_by: &[ast::SortedColumn], ) -> Result<()> { if filter_over.filter_clause.is_some() { crate::bail_parse_error!("FILTER clause is not supported yet in aggregate functions"); } if !order_by.is_empty() { crate::bail_parse_error!("ORDER BY clause is not supported yet in aggregate functions"); } Ok(()) } fn walk_select_expressions_inner(select: &ast::Select, func: &mut F) -> Result<()> where F: FnMut(&ast::Expr) -> Result, { if let Some(with_clause) = &select.with { for cte in &with_clause.ctes { walk_select_expressions_inner(&cte.select, func)?; } } walk_one_select_expressions(&select.body.select, func)?; for compound in &select.body.compounds { walk_one_select_expressions(&compound.select, func)?; } for sorted_col in &select.order_by { walk_expr_with_subqueries(&sorted_col.expr, func)?; } if let Some(limit) = &select.limit { walk_expr_with_subqueries(&limit.expr, func)?; if let Some(offset) = &limit.offset { walk_expr_with_subqueries(offset, func)?; } } Ok(()) } fn walk_one_select_expressions(one_select: &ast::OneSelect, func: &mut F) -> Result<()> where F: FnMut(&ast::Expr) -> Result, { match one_select { ast::OneSelect::Select { columns, from, where_clause, group_by, window_clause, .. } => { for col in columns { if let ast::ResultColumn::Expr(expr, _) = col { walk_expr_with_subqueries(expr, func)?; } } if let Some(from_clause) = from { walk_from_clause_expressions(from_clause, func)?; } if let Some(where_expr) = where_clause { walk_expr_with_subqueries(where_expr, func)?; } if let Some(group_by) = group_by { for expr in &group_by.exprs { walk_expr_with_subqueries(expr, func)?; } if let Some(having_expr) = &group_by.having { walk_expr_with_subqueries(having_expr, func)?; } } for window_def in window_clause { walk_window_expressions(&window_def.window, func)?; } } ast::OneSelect::Values(values) => { for row in values { for expr in row { walk_expr_with_subqueries(expr, func)?; } } } } Ok(()) } fn walk_from_clause_expressions(from_clause: &ast::FromClause, func: &mut F) -> Result<()> where F: FnMut(&ast::Expr) -> Result, { walk_select_table_expressions(&from_clause.select, func)?; for join in &from_clause.joins { walk_select_table_expressions(&join.table, func)?; if let Some(ast::JoinConstraint::On(expr)) = &join.constraint { walk_expr_with_subqueries(expr, func)?; } } Ok(()) } fn walk_select_table_expressions(select_table: &ast::SelectTable, func: &mut F) -> Result<()> where F: FnMut(&ast::Expr) -> Result, { match select_table { ast::SelectTable::Select(select, _) => walk_select_expressions_inner(select, func), ast::SelectTable::Sub(from_clause, _) => walk_from_clause_expressions(from_clause, func), ast::SelectTable::TableCall(_, args, _) => { for arg in args { walk_expr_with_subqueries(arg, func)?; } Ok(()) } ast::SelectTable::Table(_, _, _) => Ok(()), } } fn walk_window_expressions(window: &ast::Window, func: &mut F) -> Result<()> where F: FnMut(&ast::Expr) -> Result, { for expr in &window.partition_by { walk_expr_with_subqueries(expr, func)?; } for sorted_col in &window.order_by { walk_expr_with_subqueries(&sorted_col.expr, func)?; } if let Some(frame_clause) = &window.frame_clause { walk_frame_bound_expressions(&frame_clause.start, func)?; if let Some(end_bound) = &frame_clause.end { walk_frame_bound_expressions(end_bound, func)?; } } Ok(()) } fn walk_frame_bound_expressions(bound: &ast::FrameBound, func: &mut F) -> Result<()> where F: FnMut(&ast::Expr) -> Result, { match bound { ast::FrameBound::Following(expr) | ast::FrameBound::Preceding(expr) => { walk_expr_with_subqueries(expr, func) } ast::FrameBound::CurrentRow | ast::FrameBound::UnboundedFollowing | ast::FrameBound::UnboundedPreceding => Ok(()), } } pub fn walk_expr_with_subqueries(expr: &ast::Expr, func: &mut F) -> Result<()> where F: FnMut(&ast::Expr) -> Result, { walk_expr(expr, &mut |e| { let control = func(e)?; if matches!(control, WalkControl::Continue) { match e { ast::Expr::Subquery(select) | ast::Expr::Exists(select) => { walk_select_expressions_inner(select, func)?; } ast::Expr::InSelect { rhs, .. } => { walk_select_expressions_inner(rhs, func)?; } _ => {} } } Ok(control) })?; Ok(()) } fn validate_no_cross_db_references( select_stmt: &ast::Select, view_db_name: Option<&ast::Name>, ) -> Result<()> { if let Some(with_clause) = &select_stmt.with { for cte in &with_clause.ctes { validate_no_cross_db_references(&cte.select, view_db_name)?; } } validate_one_select_no_cross_db(&select_stmt.body.select, view_db_name)?; for compound in &select_stmt.body.compounds { validate_one_select_no_cross_db(&compound.select, view_db_name)?; } Ok(()) } fn validate_one_select_no_cross_db( one_select: &ast::OneSelect, view_db_name: Option<&ast::Name>, ) -> Result<()> { match one_select { ast::OneSelect::Select { from, .. } => { if let Some(from_clause) = from { validate_from_clause_no_cross_db(from_clause, view_db_name)?; } } ast::OneSelect::Values(_) => {} } Ok(()) } fn validate_from_clause_no_cross_db( from_clause: &ast::FromClause, view_db_name: Option<&ast::Name>, ) -> Result<()> { validate_select_table_no_cross_db(&from_clause.select, view_db_name)?; for join in &from_clause.joins { validate_select_table_no_cross_db(&join.table, view_db_name)?; } Ok(()) } fn reject_cross_db_qualified_name( qualified_name: &ast::QualifiedName, view_db_name: Option<&ast::Name>, ) -> Result<()> { if let Some(table_db_name) = &qualified_name.db_name { let is_cross_db = match view_db_name { Some(view_db) => { normalize_ident(view_db.as_str()) != normalize_ident(table_db_name.as_str()) } None => !table_db_name.as_str().eq_ignore_ascii_case("main"), }; if is_cross_db { return Err(crate::LimboError::ParseError(format!( "view cannot reference table in attached database: {qualified_name}" ))); } } Ok(()) } fn validate_select_table_no_cross_db( select_table: &ast::SelectTable, view_db_name: Option<&ast::Name>, ) -> Result<()> { match select_table { ast::SelectTable::Table(name, _, _) | ast::SelectTable::TableCall(name, _, _) => { reject_cross_db_qualified_name(name, view_db_name)?; } ast::SelectTable::Select(select, _) => { validate_no_cross_db_references(select, view_db_name)?; } ast::SelectTable::Sub(from_clause, _) => { validate_from_clause_no_cross_db(from_clause, view_db_name)?; } } Ok(()) } pub fn validate_select_for_unsupported_features(select_stmt: &ast::Select) -> Result<()> { walk_select_expressions(select_stmt, &mut |expr| { match expr { ast::Expr::FunctionCall { filter_over, order_by, .. } => { validate_aggregate_function_tail(filter_over, order_by)?; } ast::Expr::FunctionCallStar { filter_over, .. } => { validate_aggregate_function_tail(filter_over, &[])?; } _ => {} } Ok(WalkControl::Continue) }) } pub fn validate_select_for_views( select_stmt: &ast::Select, view_db_name: Option<&ast::Name>, ) -> Result<()> { validate_select_for_unsupported_features(select_stmt)?; validate_no_cross_db_references(select_stmt, view_db_name)?; walk_select_expressions(select_stmt, &mut |expr| { match expr { ast::Expr::Subquery(subquery_select) | ast::Expr::Exists(subquery_select) => { validate_no_cross_db_references(subquery_select, view_db_name)?; } ast::Expr::InSelect { rhs, .. } => { validate_no_cross_db_references(rhs, view_db_name)?; } _ => {} } Ok(WalkControl::Continue) })?; Ok(()) } /// Extract column information from a SELECT statement for view creation pub fn extract_view_columns( select_stmt: &ast::Select, schema: &Schema, ) -> Result { let mut tables = Vec::new(); let mut columns = Vec::new(); let mut column_name_counts: HashMap = HashMap::default(); // Navigate to the first SELECT in the statement if let ast::OneSelect::Select { ref from, columns: select_columns, .. } = &select_stmt.body.select { // First, extract all tables (from FROM clause and JOINs) if let Some(from) = from { // Add the main table from FROM clause match from.select.as_ref() { ast::SelectTable::Table(qualified_name, alias, _) => { let table_name = normalize_ident(qualified_name.name.as_str()); let db_name = qualified_name .db_name .as_ref() .map(|db| normalize_ident(db.as_str())); tables.push(ViewTable { name: table_name, db_name, alias: alias.as_ref().map(|a| match a { ast::As::As(name) => normalize_ident(name.as_str()), ast::As::Elided(name) => normalize_ident(name.as_str()), }), }); } _ => { // Handle other types like subqueries if needed } } // Add tables from JOINs for join in &from.joins { match join.table.as_ref() { ast::SelectTable::Table(qualified_name, alias, _) => { let table_name = normalize_ident(qualified_name.name.as_str()); let db_name = qualified_name .db_name .as_ref() .map(|db| normalize_ident(db.as_str())); tables.push(ViewTable { name: table_name, db_name, alias: alias.as_ref().map(|a| match a { ast::As::As(name) => normalize_ident(name.as_str()), ast::As::Elided(name) => normalize_ident(name.as_str()), }), }); } _ => { // Handle other types like subqueries if needed } } } } // Helper function to find table index by name or alias let find_table_index = |name: &str| -> Option { let name_norm = normalize_ident(name); tables.iter().position(|t| { t.name == name_norm || t.alias.as_ref().is_some_and(|a| a == &name_norm) }) }; // Process each column in the SELECT list for result_col in select_columns.iter() { match result_col { ast::ResultColumn::Expr(expr, alias) => { // Figure out which table this expression comes from let table_index = match expr.as_ref() { ast::Expr::Qualified(table_ref, _col_name) => { // Column qualified with table name find_table_index(table_ref.as_str()) } ast::Expr::Id(_col_name) => { // Unqualified column - would need to resolve based on schema // For now, assume it's from the first table if there is one if !tables.is_empty() { Some(0) } else { None } } _ => None, // Expression, literal, etc. }; let col_name = alias .as_ref() .map(|a| match a { ast::As::Elided(name) => name.as_str().to_string(), ast::As::As(name) => name.as_str().to_string(), }) .or_else(|| extract_column_name_from_expr(expr)) .unwrap_or_else(|| { // If we can't extract a simple column name, use the expression itself expr.to_string() }); columns.push(ViewColumn { table_index: table_index.unwrap_or(usize::MAX), column: Column::new_default_text(Some(col_name), "TEXT".to_string(), None), }); } ast::ResultColumn::Star => { // For SELECT *, expand to all columns from all tables for (table_idx, table) in tables.iter().enumerate() { if let Some(table_obj) = schema.get_table(&table.name) { for table_column in table_obj.columns() { let col_name = table_column.name.clone().unwrap_or_else(|| "?".to_string()); // Handle duplicate column names by adding suffix let final_name = if let Some(count) = column_name_counts.get_mut(&col_name) { *count += 1; format!("{}:{}", col_name, *count - 1) } else { column_name_counts.insert(col_name.clone(), 1); col_name.clone() }; columns.push(ViewColumn { table_index: table_idx, column: Column::new( Some(final_name), table_column.ty_str.clone(), None, None, table_column.ty(), table_column.collation_opt(), ColDef::default(), ), }); } } } // If no tables, create a placeholder if tables.is_empty() { columns.push(ViewColumn { table_index: usize::MAX, column: Column::new_default_text( Some("*".to_string()), "TEXT".to_string(), None, ), }); } } ast::ResultColumn::TableStar(table_ref) => { // For table.*, expand to all columns from the specified table let table_name_str = normalize_ident(table_ref.as_str()); if let Some(table_idx) = find_table_index(&table_name_str) { if let Some(table) = schema.get_table(&tables[table_idx].name) { for table_column in table.columns() { let col_name = table_column.name.clone().unwrap_or_else(|| "?".to_string()); // Handle duplicate column names by adding suffix let final_name = if let Some(count) = column_name_counts.get_mut(&col_name) { *count += 1; format!("{}:{}", col_name, *count - 1) } else { column_name_counts.insert(col_name.clone(), 1); col_name.clone() }; columns.push(ViewColumn { table_index: table_idx, column: Column::new( Some(final_name), table_column.ty_str.clone(), None, None, table_column.ty(), table_column.collation_opt(), ColDef::default(), ), }); } } else { // Table not found, create placeholder columns.push(ViewColumn { table_index: usize::MAX, column: Column::new_default_text( Some(format!("{table_name_str}.*")), "TEXT".to_string(), None, ), }); } } } } } } Ok(ViewColumnSchema { tables, columns }) } pub fn rewrite_fk_parent_cols_if_self_ref( clause: &mut ast::ForeignKeyClause, table: &str, from: &str, to: &str, ) { if normalize_ident(clause.tbl_name.as_str()) == normalize_ident(table) { for c in &mut clause.columns { if normalize_ident(c.col_name.as_str()) == normalize_ident(from) { c.col_name = ast::Name::exact(to.to_owned()); } } } } /// Returns true if the expression tree references a column whose normalized /// name equals `col_name_normalized`. pub fn check_expr_references_column(expr: &ast::Expr, col_name_normalized: &str) -> bool { let mut found = false; // The closure is infallible, so walk_expr cannot fail. let _ = walk_expr(expr, &mut |e| { if found { return Ok(WalkControl::SkipChildren); } match e { ast::Expr::Id(name) | ast::Expr::Name(name) => { if normalize_ident(name.as_str()) == col_name_normalized { found = true; return Ok(WalkControl::SkipChildren); } } ast::Expr::Qualified(_, col) | ast::Expr::DoublyQualified(_, _, col) => { if normalize_ident(col.as_str()) == col_name_normalized { found = true; return Ok(WalkControl::SkipChildren); } } _ => {} } Ok(WalkControl::Continue) }); found } /// Rewrite column name references; used in e.g. ALTER TABLE RENAME COLUMN /// to rewrite references to the old column name to the new column name. /// Replaces `Id(old)` and `Name(old)` with `Id(new)`, and updates the /// column name in `Qualified(tbl, old)` references. pub fn rename_identifiers(expr: &mut ast::Expr, from: &str, to: &str) { let from_normalized = normalize_ident(from); // The closure is infallible, so walk_expr_mut cannot fail. let _ = walk_expr_mut( expr, &mut |e: &mut ast::Expr| -> crate::Result { match e { ast::Expr::Id(ref name) | ast::Expr::Name(ref name) if normalize_ident(name.as_str()) == from_normalized => { *e = ast::Expr::Id(ast::Name::exact(to.to_owned())); } ast::Expr::Qualified(ref tbl, ref col_name) if normalize_ident(col_name.as_str()) == from_normalized => { let tbl = tbl.clone(); *e = ast::Expr::Qualified(tbl, ast::Name::exact(to.to_owned())); } _ => {} } Ok(WalkControl::Continue) }, ); } /// Like `rename_identifiers` but scope-aware: only renames qualified refs /// (e.g. `t1.b`) when the qualifier matches the target table or is NEW/OLD /// (which always refer to the trigger's owning table). Unqualified refs /// are renamed unconditionally (caller must ensure they're in the right scope). /// Also enters Subquery/Exists/InSelect expressions that walk_expr_mut skips. pub fn rename_identifiers_scoped( expr: &mut ast::Expr, target_table: &str, trigger_table: &str, from: &str, to: &str, ) { rename_identifiers_scoped_inner(expr, target_table, trigger_table, from, to, true); } /// Rename column references in a trigger WHEN clause. /// Only renames qualified NEW.col / OLD.col references — bare column names /// are invalid in WHEN clauses per SQLite semantics and must not be renamed. pub fn rename_identifiers_scoped_when_clause( expr: &mut ast::Expr, target_table: &str, trigger_table: &str, from: &str, to: &str, ) { rename_identifiers_scoped_inner(expr, target_table, trigger_table, from, to, false); } /// Inner implementation with `rename_unqualified` flag controlling whether bare `Expr::Id` /// references should be renamed. When `false`, only qualified refs (table.col, NEW.col, OLD.col) /// are renamed — used when the enclosing SELECT's FROM clause does NOT reference the target table. fn rename_identifiers_scoped_inner( expr: &mut ast::Expr, target_table: &str, trigger_table: &str, from: &str, to: &str, rename_unqualified: bool, ) { let from_normalized = normalize_ident(from); let target_normalized = normalize_ident(target_table); let trigger_normalized = normalize_ident(trigger_table); let is_renaming_trigger_table = target_normalized == trigger_normalized; let _ = walk_expr_mut( expr, &mut |e: &mut ast::Expr| -> crate::Result { match e { ast::Expr::Subquery(select) | ast::Expr::Exists(select) => { rewrite_select_column_refs_scoped( select, target_table, trigger_table, from, to, ); } ast::Expr::InSelect { rhs, .. } => { rewrite_select_column_refs_scoped(rhs, target_table, trigger_table, from, to); // lhs will be walked by walk_expr_mut } ast::Expr::Id(ref name) | ast::Expr::Name(ref name) if rename_unqualified && normalize_ident(name.as_str()) == from_normalized => { *e = ast::Expr::Id(ast::Name::exact(to.to_owned())); } ast::Expr::Qualified(ref tbl, ref col_name) if normalize_ident(col_name.as_str()) == from_normalized => { let tbl_norm = normalize_ident(tbl.as_str()); let should_rename = if tbl_norm == "new" || tbl_norm == "old" { is_renaming_trigger_table } else { tbl_norm == target_normalized }; if should_rename { let tbl = tbl.clone(); *e = ast::Expr::Qualified(tbl, ast::Name::exact(to.to_owned())); } } _ => {} } Ok(WalkControl::Continue) }, ); } mod rename_column_view { use super::*; #[derive(Debug, Clone)] pub struct RewrittenView { pub sql: String, pub select_stmt: ast::Select, pub columns: Vec, } pub fn rewrite_view_sql_for_column_rename( view_sql: &str, schema: &Schema, target_table: &str, target_db_name: &str, old_column: &str, new_column: &str, ) -> Result> { let mut visiting_views = HashSet::default(); rewrite_view_sql_for_column_rename_inner( view_sql, schema, target_table, target_db_name, old_column, new_column, &mut visiting_views, ) } fn rewrite_view_sql_for_column_rename_inner( view_sql: &str, schema: &Schema, target_table: &str, target_db_name: &str, old_column: &str, new_column: &str, visiting_views: &mut HashSet, ) -> Result> { let mut parser = Parser::new(view_sql.as_bytes()); let cmd = parser .next_cmd() .map_err(|e| LimboError::ParseError(format!("failed to parse view SQL: {e}")))?; let Some(ast::Cmd::Stmt(ast::Stmt::CreateView { temporary, if_not_exists, view_name, columns: view_columns, mut select, })) = cmd else { return Ok(None); }; let current_view_name = normalize_ident(view_name.name.as_str()); if !visiting_views.insert(current_view_name.clone()) { return Err(LimboError::ParseError(format!( "view {current_view_name} is circularly defined" ))); } let rewrite_result = (|| -> Result> { let original_select = select.clone(); let original_columns = view_columns_from_select(&original_select, schema, &view_columns)?; let ctx = ViewRewriteCtx::new(schema, target_table, target_db_name, old_column, new_column); let sql_changed = rewrite_view_select_for_column_rename(&mut select, &ctx, &[], visiting_views)?; let view_column_schema = extract_view_columns(&select, schema)?; let mut final_columns = apply_view_column_rename(view_column_schema, &ctx); for (i, indexed_col) in view_columns.iter().enumerate() { if let Some(col) = final_columns.get_mut(i) { col.name = Some(indexed_col.col_name.to_string()); } } let columns_changed = !columns_equivalent(&original_columns, &final_columns); if !sql_changed && !columns_changed { return Ok(None); } let new_sql = if sql_changed { let new_stmt = ast::Stmt::CreateView { temporary, if_not_exists, view_name, columns: view_columns, select: select.clone(), }; new_stmt.to_string() } else { view_sql.to_string() }; Ok(Some(RewrittenView { sql: new_sql, select_stmt: select, columns: final_columns, })) })(); visiting_views.remove(¤t_view_name); rewrite_result } fn apply_view_column_rename( view_columns: ViewColumnSchema, ctx: &ViewRewriteCtx, ) -> Vec { let target_norm = ctx.target_table_norm.as_str(); let mut columns = view_columns.columns; for view_column in &mut columns { if view_column.table_index == usize::MAX { continue; } let table = &view_columns.tables[view_column.table_index]; if table_name_matches_target( &table.name, table.db_name.as_deref(), target_norm, &ctx.target_db_norm, ) { if let Some(ref mut name) = view_column.column.name { if name.as_str().eq_ignore_ascii_case(ctx.old_column) { *name = ctx.new_column.to_string(); } } } } columns.into_iter().map(|vc| vc.column).collect() } fn view_columns_from_select( select: &ast::Select, schema: &Schema, explicit: &[ast::IndexedColumn], ) -> Result> { let view_column_schema = extract_view_columns(select, schema)?; let mut columns = view_column_schema.flat_columns(); for (i, indexed_col) in explicit.iter().enumerate() { if let Some(col) = columns.get_mut(i) { col.name = Some(indexed_col.col_name.to_string()); } } Ok(columns) } fn columns_equivalent(left: &[Column], right: &[Column]) -> bool { if left.len() != right.len() { return false; } left.iter().zip(right.iter()).all(|(l, r)| { let l_name = l.name.as_deref().unwrap_or(""); let r_name = r.name.as_deref().unwrap_or(""); l_name.eq_ignore_ascii_case(r_name) }) } #[derive(Clone)] struct ViewSourceInfo { qualifiers: Vec, columns_before: HashSet, rename_map: HashMap, is_target_table: bool, db_name: Option, } impl ViewSourceInfo { fn matches_qualifier(&self, qualifier: &str) -> bool { self.qualifiers.iter().any(|q| q == qualifier) } } fn alias_name(alias: &ast::As) -> &str { match alias { ast::As::As(name) | ast::As::Elided(name) => name.as_str(), } } #[derive(Clone)] struct CteInfo { columns_before: HashSet, rename_map: HashMap, } struct ViewRewriteCtx<'a> { schema: &'a Schema, target_table: &'a str, target_table_norm: String, target_db_norm: String, old_column: &'a str, old_column_norm: String, new_column: &'a str, } impl<'a> ViewRewriteCtx<'a> { fn new( schema: &'a Schema, target_table: &'a str, target_db_name: &'a str, old_column: &'a str, new_column: &'a str, ) -> Self { Self { schema, target_table, target_table_norm: normalize_ident(target_table), target_db_norm: normalize_ident(target_db_name), old_column, old_column_norm: normalize_ident(old_column), new_column, } } } fn rewrite_view_select_for_column_rename( select: &mut ast::Select, ctx: &ViewRewriteCtx, outer_scopes: &[&[ViewSourceInfo]], visiting_views: &mut HashSet, ) -> Result { let mut changed = false; let mut ctes: HashMap = HashMap::default(); if let Some(ref mut with_clause) = select.with { for cte in &mut with_clause.ctes { let mut before_cols = select_output_columns(&cte.select, ctx, false)?; apply_explicit_column_names(&mut before_cols, &cte.columns); let cte_changed = rewrite_view_select_for_column_rename( &mut cte.select, ctx, &[], visiting_views, )?; changed |= cte_changed; let mut after_cols = select_output_columns(&cte.select, ctx, true)?; apply_explicit_column_names(&mut after_cols, &cte.columns); let rename_map = build_rename_map(&before_cols, &after_cols, &ctx.old_column_norm); ctes.insert( normalize_ident(cte.tbl_name.as_str()), CteInfo { columns_before: before_cols .into_iter() .map(|c| normalize_ident(&c)) .collect(), rename_map, }, ); } } let mut scope_sources = rewrite_one_select_for_column_rename( &mut select.body.select, ctx, &ctes, outer_scopes, &mut changed, visiting_views, )?; for compound in &mut select.body.compounds { let compound_sources = rewrite_one_select_for_column_rename( &mut compound.select, ctx, &ctes, outer_scopes, &mut changed, visiting_views, )?; if scope_sources.is_none() { scope_sources = compound_sources; } } if let Some(ref sources) = scope_sources { for sorted_col in &mut select.order_by { rewrite_expr_in_scope( &mut sorted_col.expr, sources, outer_scopes, ctx, &mut changed, visiting_views, )?; } if let Some(ref mut limit) = select.limit { rewrite_expr_in_scope( &mut limit.expr, sources, outer_scopes, ctx, &mut changed, visiting_views, )?; if let Some(ref mut offset) = limit.offset { rewrite_expr_in_scope( offset, sources, outer_scopes, ctx, &mut changed, visiting_views, )?; } } } Ok(changed) } fn rewrite_one_select_for_column_rename( one_select: &mut ast::OneSelect, ctx: &ViewRewriteCtx, ctes: &HashMap, outer_scopes: &[&[ViewSourceInfo]], changed: &mut bool, visiting_views: &mut HashSet, ) -> Result>> { match one_select { ast::OneSelect::Select { columns, from, where_clause, group_by, window_clause, .. } => { let sources = if let Some(ref mut from_clause) = from { rewrite_from_clause_for_column_rename( from_clause, ctx, ctes, outer_scopes, changed, visiting_views, )? } else { Vec::new() }; for col in columns { if let ast::ResultColumn::Expr(expr, _) = col { rewrite_expr_in_scope( expr, &sources, outer_scopes, ctx, changed, visiting_views, )?; } } if let Some(ref mut where_expr) = where_clause { rewrite_expr_in_scope( where_expr, &sources, outer_scopes, ctx, changed, visiting_views, )?; } if let Some(ref mut group_by) = group_by { for expr in &mut group_by.exprs { rewrite_expr_in_scope( expr, &sources, outer_scopes, ctx, changed, visiting_views, )?; } if let Some(ref mut having_expr) = group_by.having { rewrite_expr_in_scope( having_expr, &sources, outer_scopes, ctx, changed, visiting_views, )?; } } for window_def in window_clause { for expr in &mut window_def.window.partition_by { rewrite_expr_in_scope( expr, &sources, outer_scopes, ctx, changed, visiting_views, )?; } for sorted in &mut window_def.window.order_by { rewrite_expr_in_scope( &mut sorted.expr, &sources, outer_scopes, ctx, changed, visiting_views, )?; } } Ok(Some(sources)) } ast::OneSelect::Values(values) => { for row in values { for expr in row { rewrite_expr_in_scope( expr, &[], outer_scopes, ctx, changed, visiting_views, )?; } } Ok(None) } } } fn rewrite_from_clause_for_column_rename( from_clause: &mut ast::FromClause, ctx: &ViewRewriteCtx, ctes: &HashMap, outer_scopes: &[&[ViewSourceInfo]], changed: &mut bool, visiting_views: &mut HashSet, ) -> Result> { let mut sources = Vec::new(); let first_source = rewrite_select_table_for_column_rename( &mut from_clause.select, &[], ctx, ctes, outer_scopes, changed, visiting_views, )?; sources.push(first_source); for join in &mut from_clause.joins { let right_source = rewrite_select_table_for_column_rename( &mut join.table, &sources, ctx, ctes, outer_scopes, changed, visiting_views, )?; sources.push(right_source); let (right_source, left_sources) = sources .split_last() .expect("sources should include the right-hand side join source"); if let Some(ref mut constraint) = join.constraint { match constraint { ast::JoinConstraint::On(expr) => { rewrite_expr_in_scope( expr, &sources, outer_scopes, ctx, changed, visiting_views, )?; } ast::JoinConstraint::Using(cols) => { *changed |= rewrite_using_columns( cols, left_sources, right_source, &ctx.old_column_norm, ctx.new_column, ); } } } } Ok(sources) } fn rewrite_select_table_for_column_rename( select_table: &mut ast::SelectTable, visible_sources: &[ViewSourceInfo], ctx: &ViewRewriteCtx, ctes: &HashMap, outer_scopes: &[&[ViewSourceInfo]], changed: &mut bool, visiting_views: &mut HashSet, ) -> Result { match select_table { ast::SelectTable::Table(tbl_name, alias, _) => { let table_name_norm = normalize_ident(tbl_name.name.as_str()); let table_db_norm = tbl_name .db_name .as_ref() .map(|db| normalize_ident(db.as_str())); let mut qualifiers = Vec::new(); qualifiers.push(table_name_norm.clone()); if let Some(ref alias) = alias { qualifiers.push(normalize_ident(alias_name(alias))); } if table_db_norm.is_none() { if let Some(cte) = ctes.get(&table_name_norm) { return Ok(ViewSourceInfo { qualifiers, columns_before: cte.columns_before.clone(), rename_map: cte.rename_map.clone(), is_target_table: false, db_name: None, }); } } let is_local = table_db_norm .as_deref() .is_none_or(|db| db == ctx.target_db_norm); if is_local { if let Some(view) = ctx.schema.views.get(&table_name_norm) { let columns_before = view .columns .iter() .filter_map(|col| col.name.clone()) .map(|name| normalize_ident(&name)) .collect(); let mut rename_map = HashMap::default(); if let Some(rewritten) = rewrite_view_sql_for_column_rename_inner( &view.sql, ctx.schema, ctx.target_table, &ctx.target_db_norm, ctx.old_column, ctx.new_column, visiting_views, )? { rename_map = build_rename_map_from_columns( &view.columns, &rewritten.columns, &ctx.old_column_norm, ); } return Ok(ViewSourceInfo { qualifiers, columns_before, rename_map, is_target_table: false, db_name: table_db_norm, }); } } let is_target = table_name_matches_target( &table_name_norm, table_db_norm.as_deref(), &ctx.target_table_norm, &ctx.target_db_norm, ); let columns_before = if is_local { table_source_columns(ctx.schema, &table_name_norm) .unwrap_or_default() .into_iter() .map(|c| normalize_ident(&c)) .collect() } else { HashSet::default() }; Ok(ViewSourceInfo { qualifiers, columns_before, rename_map: HashMap::default(), is_target_table: is_target, db_name: table_db_norm, }) } ast::SelectTable::Select(select, alias) => { let before_cols = select_output_columns(select, ctx, false)?; *changed |= rewrite_view_select_for_column_rename(select, ctx, &[], visiting_views)?; let after_cols = select_output_columns(select, ctx, true)?; let rename_map = build_rename_map(&before_cols, &after_cols, &ctx.old_column_norm); let qualifiers = alias .as_ref() .map(|alias| vec![normalize_ident(alias_name(alias))]) .unwrap_or_default(); Ok(ViewSourceInfo { qualifiers, columns_before: before_cols .into_iter() .map(|c| normalize_ident(&c)) .collect(), rename_map, is_target_table: false, db_name: None, }) } ast::SelectTable::Sub(from_clause, alias) => { let before_cols = from_clause_output_columns(from_clause, ctx, false)?; let _ = rewrite_from_clause_for_column_rename( from_clause, ctx, ctes, outer_scopes, changed, visiting_views, )?; let after_cols = from_clause_output_columns(from_clause, ctx, true)?; let rename_map = build_rename_map(&before_cols, &after_cols, &ctx.old_column_norm); let qualifiers = alias .as_ref() .map(|alias| vec![normalize_ident(alias_name(alias))]) .unwrap_or_default(); Ok(ViewSourceInfo { qualifiers, columns_before: before_cols .into_iter() .map(|c| normalize_ident(&c)) .collect(), rename_map, is_target_table: false, db_name: None, }) } ast::SelectTable::TableCall(_, args, alias) => { for arg in args { rewrite_expr_in_scope( arg, visible_sources, outer_scopes, ctx, changed, visiting_views, )?; } let qualifiers = alias .as_ref() .map(|alias| vec![normalize_ident(alias_name(alias))]) .unwrap_or_default(); Ok(ViewSourceInfo { qualifiers, columns_before: HashSet::default(), rename_map: HashMap::default(), is_target_table: false, db_name: None, }) } } } fn rewrite_expr_in_scope( expr: &mut ast::Expr, sources: &[ViewSourceInfo], outer_scopes: &[&[ViewSourceInfo]], ctx: &ViewRewriteCtx, changed: &mut bool, visiting_views: &mut HashSet, ) -> Result<()> { let mut outer_scopes_for_subqueries: Vec<&[ViewSourceInfo]> = Vec::with_capacity(outer_scopes.len() + 1); if !sources.is_empty() { outer_scopes_for_subqueries.push(sources); } outer_scopes_for_subqueries.extend_from_slice(outer_scopes); walk_expr_mut(expr, &mut |e: &mut ast::Expr| -> Result { if rewrite_expr_column_ref_view( e, sources, outer_scopes, &ctx.target_db_norm, &ctx.old_column_norm, ctx.new_column, ) { *changed = true; } match e { ast::Expr::Subquery(select) | ast::Expr::Exists(select) => { if rewrite_view_select_for_column_rename( select, ctx, outer_scopes_for_subqueries.as_slice(), visiting_views, )? { *changed = true; } } ast::Expr::InSelect { rhs, .. } => { if rewrite_view_select_for_column_rename( rhs, ctx, outer_scopes_for_subqueries.as_slice(), visiting_views, )? { *changed = true; } } _ => {} } Ok(WalkControl::Continue) })?; Ok(()) } fn rewrite_expr_column_ref_view( expr: &mut ast::Expr, sources: &[ViewSourceInfo], outer_scopes: &[&[ViewSourceInfo]], target_db_norm: &str, old_column_norm: &str, new_column: &str, ) -> bool { let apply_rename = |source: &ViewSourceInfo, set_name: &mut dyn FnMut(String)| { if source.is_target_table { set_name(new_column.to_string()); return true; } if let Some(mapped) = source.rename_map.get(old_column_norm) { set_name(mapped.to_string()); return true; } false }; match expr { ast::Expr::Qualified(ns, col) => { let ns_norm = normalize_ident(ns.as_str()); if !col.as_str().eq_ignore_ascii_case(old_column_norm) { return false; } let (source, local_ambiguous) = resolve_qualified(sources, &ns_norm, target_db_norm); if let Some(source) = source { return apply_rename(source, &mut |name| { *col = ast::Name::exact(name); }); } if local_ambiguous { return false; } for scope in outer_scopes { let (source, ambiguous) = resolve_qualified(scope, &ns_norm, target_db_norm); if let Some(source) = source { return apply_rename(source, &mut |name| { *col = ast::Name::exact(name); }); } if ambiguous { return false; } } } ast::Expr::DoublyQualified(schema, ns, col) => { let schema_norm = normalize_ident(schema.as_str()); if schema_norm != target_db_norm { return false; } let ns_norm = normalize_ident(ns.as_str()); if !col.as_str().eq_ignore_ascii_case(old_column_norm) { return false; } let (source, local_ambiguous) = resolve_qualified(sources, &ns_norm, &schema_norm); if let Some(source) = source { return apply_rename(source, &mut |name| { *col = ast::Name::exact(name); }); } if local_ambiguous { return false; } for scope in outer_scopes { let (source, ambiguous) = resolve_qualified(scope, &ns_norm, &schema_norm); if let Some(source) = source { return apply_rename(source, &mut |name| { *col = ast::Name::exact(name); }); } if ambiguous { return false; } } } ast::Expr::Id(col) | ast::Expr::Name(col) => { if !col.as_str().eq_ignore_ascii_case(old_column_norm) { return false; } let col_norm = normalize_ident(col.as_str()); let (source, local_ambiguous) = resolve_unqualified(sources, &col_norm); if let Some(source) = source { return apply_rename(source, &mut |name| { *expr = ast::Expr::Id(ast::Name::exact(name)); }); } if local_ambiguous { return false; } for scope in outer_scopes { let (source, ambiguous) = resolve_unqualified(scope, &col_norm); if let Some(source) = source { return apply_rename(source, &mut |name| { *expr = ast::Expr::Id(ast::Name::exact(name)); }); } if ambiguous { return false; } } } _ => {} } false } fn resolve_unqualified<'a>( candidates: &'a [ViewSourceInfo], old_column_norm: &str, ) -> (Option<&'a ViewSourceInfo>, bool) { let mut matches = candidates .iter() .filter(|s| s.columns_before.contains(old_column_norm)); let Some(first) = matches.next() else { return (None, false); }; if matches.next().is_some() { return (None, true); } (Some(first), false) } fn resolve_qualified<'a>( candidates: &'a [ViewSourceInfo], qualifier: &str, target_db_norm: &str, ) -> (Option<&'a ViewSourceInfo>, bool) { let mut matches = candidates.iter().filter(|s| { s.matches_qualifier(qualifier) && s.db_name.as_deref().is_none_or(|db| db == target_db_norm) }); let Some(first) = matches.next() else { return (None, false); }; if matches.next().is_some() { return (None, true); } (Some(first), false) } fn rewrite_using_columns( cols: &mut [ast::Name], left_sources: &[ViewSourceInfo], right: &ViewSourceInfo, old_column_norm: &str, new_column: &str, ) -> bool { let mut changed = false; let left_map = left_sources .iter() .find_map(|source| source.rename_map.get(old_column_norm)); let left_has_target = left_sources.iter().any(|source| source.is_target_table); let right_map = right.rename_map.get(old_column_norm); let should_rename = left_has_target || right.is_target_table || left_map.is_some() || right_map.is_some(); if !should_rename { return false; } let replacement = left_map .or(right_map) .map(|s| s.as_str()) .unwrap_or(new_column); for col in cols { if col.as_str().eq_ignore_ascii_case(old_column_norm) { *col = ast::Name::exact(replacement.to_string()); changed = true; } } changed } fn select_output_columns( select: &ast::Select, ctx: &ViewRewriteCtx, apply_rename: bool, ) -> Result> { let view_columns = extract_view_columns(select, ctx.schema)?; let mut columns = view_columns.columns; if apply_rename { let target_norm = ctx.target_table_norm.as_str(); for view_column in &mut columns { if view_column.table_index == usize::MAX { continue; } let table = &view_columns.tables[view_column.table_index]; if table_name_matches_target( &table.name, table.db_name.as_deref(), target_norm, &ctx.target_db_norm, ) { if let Some(ref mut name) = view_column.column.name { if name.as_str().eq_ignore_ascii_case(ctx.old_column) { *name = ctx.new_column.to_string(); } } } } } Ok(columns .into_iter() .map(|vc| vc.column.name.unwrap_or_else(|| "?".to_string())) .collect()) } fn apply_explicit_column_names(columns: &mut [String], explicit: &[ast::IndexedColumn]) { for (i, indexed_col) in explicit.iter().enumerate() { if let Some(col) = columns.get_mut(i) { *col = indexed_col.col_name.to_string(); } } } fn from_clause_output_columns( from_clause: &ast::FromClause, ctx: &ViewRewriteCtx, apply_rename: bool, ) -> Result> { let dummy_select = ast::Select { with: None, body: ast::SelectBody { select: ast::OneSelect::Select { distinctness: None, columns: vec![ast::ResultColumn::Star], from: Some(from_clause.clone()), where_clause: None, group_by: None, window_clause: Vec::new(), }, compounds: Vec::new(), }, order_by: Vec::new(), limit: None, }; select_output_columns(&dummy_select, ctx, apply_rename) } fn build_rename_map( before_cols: &[String], after_cols: &[String], old_column_norm: &str, ) -> HashMap { let mut map = HashMap::default(); for (before, after) in before_cols.iter().zip(after_cols.iter()) { if before.as_str().eq_ignore_ascii_case(old_column_norm) && !after.as_str().eq_ignore_ascii_case(before.as_str()) { map.insert(old_column_norm.to_string(), after.to_string()); } } map } fn build_rename_map_from_columns( before_cols: &[Column], after_cols: &[Column], old_column_norm: &str, ) -> HashMap { if before_cols.len() != after_cols.len() { return HashMap::default(); } let mut map = HashMap::default(); for (before, after) in before_cols.iter().zip(after_cols.iter()) { let Some(before_name) = before.name.as_ref() else { continue; }; let Some(after_name) = after.name.as_ref() else { continue; }; if before_name.as_str().eq_ignore_ascii_case(old_column_norm) && !after_name .as_str() .eq_ignore_ascii_case(before_name.as_str()) { map.insert(old_column_norm.to_string(), after_name.to_string()); } } map } fn table_name_matches_target( table_name: &str, table_db: Option<&str>, target_table_norm: &str, target_db_norm: &str, ) -> bool { if !table_name.eq_ignore_ascii_case(target_table_norm) { return false; } match table_db { None => true, Some(db) => db.eq_ignore_ascii_case(target_db_norm), } } fn table_source_columns(schema: &Schema, table_name: &str) -> Option> { if let Some(table) = schema.get_table(table_name) { return Some( table .columns() .iter() .filter_map(|col| col.name.clone()) .collect(), ); } let table_norm = normalize_ident(table_name); if let Some(view) = schema.views.get(&table_norm) { return Some( view.columns .iter() .filter_map(|col| col.name.clone()) .collect(), ); } None } } pub use rename_column_view::{rewrite_view_sql_for_column_rename, RewrittenView}; /// Rewrite table-qualified column references in a CHECK constraint expression, /// replacing the table name from `from` to `to`. For example, `t1.a > 0` becomes /// `t2.a > 0` when renaming t1 to t2. This matches SQLite 3.49.1+ behavior which /// rewrites qualified refs during ALTER TABLE RENAME instead of rejecting them. pub fn rewrite_check_expr_table_refs(expr: &mut ast::Expr, from: &str, to: &str) { let from_normalized = normalize_ident(from); let _ = walk_expr_mut( expr, &mut |e: &mut ast::Expr| -> crate::Result { if let ast::Expr::Qualified(ref tbl, ref col) = *e { if normalize_ident(tbl.as_str()) == from_normalized { let col = col.clone(); *e = ast::Expr::Qualified(ast::Name::exact(to.to_owned()), col); } } Ok(WalkControl::Continue) }, ); } /// Update a column-level REFERENCES (col,...) constraint pub fn rewrite_column_references_if_needed( col: &mut ast::ColumnDefinition, table: &str, from: &str, to: &str, ) { for cc in &mut col.constraints { match &mut cc.constraint { ast::ColumnConstraint::ForeignKey { clause, .. } => { rewrite_fk_parent_cols_if_self_ref(clause, table, from, to); } ast::ColumnConstraint::Check(expr) => { rename_identifiers(expr, from, to); } _ => {} } } } /// If a FK REFERENCES targets `old_tbl`, change it to `new_tbl` pub fn rewrite_fk_parent_table_if_needed( clause: &mut ast::ForeignKeyClause, old_tbl: &str, new_tbl: &str, ) -> bool { if normalize_ident(clause.tbl_name.as_str()) == normalize_ident(old_tbl) { clause.tbl_name = ast::Name::exact(new_tbl.to_owned()); return true; } false } /// For inline REFERENCES tbl in a column definition. pub fn rewrite_inline_col_fk_target_if_needed( col: &mut ast::ColumnDefinition, old_tbl: &str, new_tbl: &str, ) -> bool { let mut changed = false; for cc in &mut col.constraints { if let ast::NamedColumnConstraint { constraint: ast::ColumnConstraint::ForeignKey { clause, .. }, .. } = cc { changed |= rewrite_fk_parent_table_if_needed(clause, old_tbl, new_tbl); } } changed } /// Rewrite table name references inside a trigger's body commands for ALTER TABLE RENAME. /// Updates tbl_name fields in INSERT/UPDATE/DELETE commands and table references /// in FROM clauses and qualified expressions throughout the trigger body. pub fn rewrite_trigger_cmd_table_refs(cmd: &mut ast::TriggerCmd, old_tbl: &str, new_tbl: &str) { let old_normalized = normalize_ident(old_tbl); match cmd { ast::TriggerCmd::Update { tbl_name, sets, from, where_clause, .. } => { if normalize_ident(tbl_name.as_str()) == old_normalized { *tbl_name = ast::Name::exact(new_tbl.to_owned()); } for set in sets { rewrite_check_expr_table_refs(&mut set.expr, old_tbl, new_tbl); } if let Some(ref mut from) = from { rewrite_from_clause_table_refs(from, old_tbl, new_tbl); } if let Some(ref mut wc) = where_clause { rewrite_check_expr_table_refs(wc, old_tbl, new_tbl); } } ast::TriggerCmd::Insert { tbl_name, select, upsert, .. } => { if normalize_ident(tbl_name.as_str()) == old_normalized { *tbl_name = ast::Name::exact(new_tbl.to_owned()); } rewrite_select_table_refs(select, old_tbl, new_tbl); if let Some(ref mut upsert) = upsert { rewrite_upsert_table_refs(upsert, old_tbl, new_tbl); } } ast::TriggerCmd::Delete { tbl_name, where_clause, } => { if normalize_ident(tbl_name.as_str()) == old_normalized { *tbl_name = ast::Name::exact(new_tbl.to_owned()); } if let Some(ref mut wc) = where_clause { rewrite_check_expr_table_refs(wc, old_tbl, new_tbl); } } ast::TriggerCmd::Select(select) => { rewrite_select_table_refs(select, old_tbl, new_tbl); } } } /// Scope-aware version of `rewrite_select_column_refs` that checks table qualifiers. fn rewrite_select_column_refs_scoped( select: &mut ast::Select, target_table: &str, trigger_table: &str, old_col: &str, new_col: &str, ) { rewrite_one_select_column_refs_scoped( &mut select.body.select, target_table, trigger_table, old_col, new_col, ); for compound in &mut select.body.compounds { rewrite_one_select_column_refs_scoped( &mut compound.select, target_table, trigger_table, old_col, new_col, ); } // ORDER BY is in the same scope as the body's FROM let body_from_has_target = match &select.body.select { ast::OneSelect::Select { from, .. } => from_clause_has_target(from, target_table), _ => false, }; let target_normalized = normalize_ident(target_table); let trigger_normalized = normalize_ident(trigger_table); let rename_unqualified = body_from_has_target || target_normalized == trigger_normalized; for col in &mut select.order_by { rename_identifiers_scoped_inner( &mut col.expr, target_table, trigger_table, old_col, new_col, rename_unqualified, ); } } /// Check if a FROM clause contains a reference to the given table name. fn from_clause_has_target(from: &Option, target_table: &str) -> bool { let Some(from_clause) = from else { return false; }; let target_normalized = normalize_ident(target_table); let check_table = |st: &ast::SelectTable| -> bool { matches!( st, ast::SelectTable::Table(name, _, _) if normalize_ident(name.name.as_str()) == target_normalized ) }; if check_table(&from_clause.select) { return true; } from_clause.joins.iter().any(|j| check_table(&j.table)) } fn rewrite_one_select_column_refs_scoped( one: &mut ast::OneSelect, target_table: &str, trigger_table: &str, old_col: &str, new_col: &str, ) { match one { ast::OneSelect::Select { from, where_clause, columns, group_by, .. } => { // Check if FROM clause references the target table to determine // whether unqualified Expr::Id should be renamed in this scope let from_has_target = from_clause_has_target(from, target_table); let target_normalized = normalize_ident(target_table); let trigger_normalized = normalize_ident(trigger_table); let rename_unqualified = from_has_target || target_normalized == trigger_normalized; if let Some(ref mut from) = from { rewrite_from_clause_column_refs_scoped( from, target_table, trigger_table, old_col, new_col, ); } if let Some(ref mut wc) = where_clause { rename_identifiers_scoped_inner( wc, target_table, trigger_table, old_col, new_col, rename_unqualified, ); } for col in columns { if let ast::ResultColumn::Expr(ref mut expr, _) = col { rename_identifiers_scoped_inner( expr, target_table, trigger_table, old_col, new_col, rename_unqualified, ); } } if let Some(ref mut gb) = group_by { for expr in &mut gb.exprs { rename_identifiers_scoped_inner( expr, target_table, trigger_table, old_col, new_col, rename_unqualified, ); } if let Some(ref mut having) = gb.having { rename_identifiers_scoped_inner( having, target_table, trigger_table, old_col, new_col, rename_unqualified, ); } } } ast::OneSelect::Values(rows) => { for row in rows { for expr in row { rename_identifiers_scoped(expr, target_table, trigger_table, old_col, new_col); } } } } } fn rewrite_from_clause_column_refs_scoped( from: &mut ast::FromClause, target_table: &str, trigger_table: &str, old_col: &str, new_col: &str, ) { // Check if this FROM clause references the target table for JOIN ON expressions let from_has_target = { let target_normalized = normalize_ident(target_table); let check_table = |st: &ast::SelectTable| -> bool { matches!( st, ast::SelectTable::Table(name, _, _) if normalize_ident(name.name.as_str()) == target_normalized ) }; check_table(&from.select) || from.joins.iter().any(|j| check_table(&j.table)) }; let target_normalized = normalize_ident(target_table); let trigger_normalized = normalize_ident(trigger_table); let rename_unqualified = from_has_target || target_normalized == trigger_normalized; rewrite_select_table_entry_column_refs_scoped( &mut from.select, target_table, trigger_table, old_col, new_col, ); for join in &mut from.joins { rewrite_select_table_entry_column_refs_scoped( &mut join.table, target_table, trigger_table, old_col, new_col, ); if let Some(ast::JoinConstraint::On(ref mut expr)) = join.constraint { rename_identifiers_scoped_inner( expr, target_table, trigger_table, old_col, new_col, rename_unqualified, ); } } } fn rewrite_select_table_entry_column_refs_scoped( st: &mut ast::SelectTable, target_table: &str, trigger_table: &str, old_col: &str, new_col: &str, ) { match st { ast::SelectTable::TableCall(_, ref mut args, _) => { for arg in args { rename_identifiers_scoped(arg, target_table, trigger_table, old_col, new_col); } } ast::SelectTable::Select(ref mut select, _) => { rewrite_select_column_refs_scoped( select, target_table, trigger_table, old_col, new_col, ); } ast::SelectTable::Sub(ref mut from, _) => { rewrite_from_clause_column_refs_scoped( from, target_table, trigger_table, old_col, new_col, ); } ast::SelectTable::Table(..) => {} } } fn rename_excluded_column_refs(expr: &mut ast::Expr, old_col: &str, new_col: &str) { let old_col_normalized = normalize_ident(old_col); let _ = walk_expr_mut( expr, &mut |e: &mut ast::Expr| -> crate::Result { if let ast::Expr::Qualified(ns, col) | ast::Expr::DoublyQualified(_, ns, col) = e { if normalize_ident(ns.as_str()) == "excluded" && normalize_ident(col.as_str()) == old_col_normalized { *col = ast::Name::exact(new_col.to_owned()); } } Ok(WalkControl::Continue) }, ); } fn rewrite_upsert_column_refs_scoped( upsert: &mut ast::Upsert, table: &str, trigger_table: &str, insert_table: &str, old_col: &str, new_col: &str, ) { let insert_targets_renamed_table = normalize_ident(insert_table) == normalize_ident(table); let rewrite_expr = |expr: &mut ast::Expr| { if insert_targets_renamed_table { rename_identifiers_scoped(expr, table, trigger_table, old_col, new_col); rename_excluded_column_refs(expr, old_col, new_col); } else { rename_identifiers_scoped_when_clause(expr, table, trigger_table, old_col, new_col); } }; if let Some(ref mut index) = upsert.index { for target in &mut index.targets { rewrite_expr(&mut target.expr); } if let Some(ref mut wc) = index.where_clause { rewrite_expr(wc); } } if let ast::UpsertDo::Set { ref mut sets, ref mut where_clause, } = upsert.do_clause { for set in sets { if insert_targets_renamed_table { for col_name in &mut set.col_names { if normalize_ident(col_name.as_str()) == normalize_ident(old_col) { *col_name = ast::Name::exact(new_col.to_owned()); } } } rewrite_expr(&mut set.expr); } if let Some(ref mut wc) = where_clause { rewrite_expr(wc); } } if let Some(ref mut next) = upsert.next { rewrite_upsert_column_refs_scoped( next, table, trigger_table, insert_table, old_col, new_col, ); } } /// Rewrite column references inside a trigger's body commands for ALTER TABLE RENAME COLUMN. /// Uses scope-aware renaming: only renames qualified refs when the qualifier matches /// the target table (or NEW/OLD for the trigger's owning table). pub fn rewrite_trigger_cmd_column_refs( cmd: &mut ast::TriggerCmd, table: &str, trigger_table: &str, old_col: &str, new_col: &str, ) { let table_normalized = normalize_ident(table); match cmd { ast::TriggerCmd::Update { tbl_name, sets, from, where_clause, .. } => { let cmd_tbl_norm = normalize_ident(tbl_name.as_str()); let targets_renamed_table = cmd_tbl_norm == table_normalized; if targets_renamed_table { for set in sets { for col_name in &mut set.col_names { if normalize_ident(col_name.as_str()) == normalize_ident(old_col) { *col_name = ast::Name::exact(new_col.to_owned()); } } rename_identifiers_scoped( &mut set.expr, table, trigger_table, old_col, new_col, ); } if let Some(ref mut wc) = where_clause { rename_identifiers_scoped(wc, table, trigger_table, old_col, new_col); } } else { for set in sets { rename_identifiers_scoped_when_clause( &mut set.expr, table, trigger_table, old_col, new_col, ); } if let Some(ref mut wc) = where_clause { rename_identifiers_scoped_when_clause( wc, table, trigger_table, old_col, new_col, ); } } if let Some(ref mut from) = from { rewrite_from_clause_column_refs_scoped( from, table, trigger_table, old_col, new_col, ); } } ast::TriggerCmd::Insert { tbl_name, col_names, select, upsert, .. } => { let cmd_tbl_norm = normalize_ident(tbl_name.as_str()); let targets_renamed_table = cmd_tbl_norm == table_normalized; if targets_renamed_table { for col_name in col_names { if normalize_ident(col_name.as_str()) == normalize_ident(old_col) { *col_name = ast::Name::exact(new_col.to_owned()); } } } rewrite_select_column_refs_scoped(select, table, trigger_table, old_col, new_col); if let Some(ref mut upsert) = upsert { rewrite_upsert_column_refs_scoped( upsert, table, trigger_table, tbl_name.as_str(), old_col, new_col, ); } } ast::TriggerCmd::Delete { tbl_name, where_clause, } => { let cmd_tbl_norm = normalize_ident(tbl_name.as_str()); let targets_renamed_table = cmd_tbl_norm == table_normalized; if targets_renamed_table { if let Some(ref mut wc) = where_clause { rename_identifiers_scoped(wc, table, trigger_table, old_col, new_col); } } else if let Some(ref mut wc) = where_clause { rename_identifiers_scoped_when_clause(wc, table, trigger_table, old_col, new_col); } } ast::TriggerCmd::Select(select) => { rewrite_select_column_refs_scoped(select, table, trigger_table, old_col, new_col); } } } fn rewrite_select_table_refs(select: &mut ast::Select, old_tbl: &str, new_tbl: &str) { rewrite_one_select_table_refs(&mut select.body.select, old_tbl, new_tbl); for compound in &mut select.body.compounds { rewrite_one_select_table_refs(&mut compound.select, old_tbl, new_tbl); } for col in &mut select.order_by { rewrite_check_expr_table_refs(&mut col.expr, old_tbl, new_tbl); } } fn rewrite_one_select_table_refs(one: &mut ast::OneSelect, old_tbl: &str, new_tbl: &str) { match one { ast::OneSelect::Select { from, where_clause, columns, group_by, .. } => { if let Some(ref mut from) = from { rewrite_from_clause_table_refs(from, old_tbl, new_tbl); } if let Some(ref mut wc) = where_clause { rewrite_check_expr_table_refs(wc, old_tbl, new_tbl); } for col in columns { match col { ast::ResultColumn::Expr(ref mut expr, _) => { rewrite_check_expr_table_refs(expr, old_tbl, new_tbl); } ast::ResultColumn::TableStar(ref mut name) => { if normalize_ident(name.as_str()) == normalize_ident(old_tbl) { *name = ast::Name::exact(new_tbl.to_owned()); } } ast::ResultColumn::Star => {} } } if let Some(ref mut gb) = group_by { for expr in &mut gb.exprs { rewrite_check_expr_table_refs(expr, old_tbl, new_tbl); } if let Some(ref mut having) = gb.having { rewrite_check_expr_table_refs(having, old_tbl, new_tbl); } } } ast::OneSelect::Values(rows) => { for row in rows { for expr in row { rewrite_check_expr_table_refs(expr, old_tbl, new_tbl); } } } } } fn rewrite_from_clause_table_refs(from: &mut ast::FromClause, old_tbl: &str, new_tbl: &str) { rewrite_select_table_entry_table_refs(&mut from.select, old_tbl, new_tbl); for join in &mut from.joins { rewrite_select_table_entry_table_refs(&mut join.table, old_tbl, new_tbl); if let Some(ast::JoinConstraint::On(ref mut expr)) = join.constraint { rewrite_check_expr_table_refs(expr, old_tbl, new_tbl); } } } fn rewrite_select_table_entry_table_refs(st: &mut ast::SelectTable, old_tbl: &str, new_tbl: &str) { let old_normalized = normalize_ident(old_tbl); match st { ast::SelectTable::Table(ref mut name, _, _) => { if normalize_ident(name.name.as_str()) == old_normalized { name.name = ast::Name::exact(new_tbl.to_owned()); } } ast::SelectTable::TableCall(ref mut name, ref mut args, _) => { if normalize_ident(name.name.as_str()) == old_normalized { name.name = ast::Name::exact(new_tbl.to_owned()); } for arg in args { rewrite_check_expr_table_refs(arg, old_tbl, new_tbl); } } ast::SelectTable::Select(ref mut select, _) => { rewrite_select_table_refs(select, old_tbl, new_tbl); } ast::SelectTable::Sub(ref mut from, _) => { rewrite_from_clause_table_refs(from, old_tbl, new_tbl); } } } fn rewrite_upsert_table_refs(upsert: &mut ast::Upsert, old_tbl: &str, new_tbl: &str) { if let Some(ref mut index) = upsert.index { if let Some(ref mut wc) = index.where_clause { rewrite_check_expr_table_refs(wc, old_tbl, new_tbl); } } if let ast::UpsertDo::Set { ref mut sets, ref mut where_clause, } = upsert.do_clause { for set in sets { rewrite_check_expr_table_refs(&mut set.expr, old_tbl, new_tbl); } if let Some(ref mut wc) = where_clause { rewrite_check_expr_table_refs(wc, old_tbl, new_tbl); } } if let Some(ref mut next) = upsert.next { rewrite_upsert_table_refs(next, old_tbl, new_tbl); } } #[cfg(test)] pub mod tests { use super::*; use crate::schema::{BTreeTable, Type as SchemaValueType}; use turso_parser::ast::{self, Expr, FunctionTail, Literal, Name, Operator::*, Type, Variable}; #[test] fn test_normalize_ident() { assert_eq!(normalize_ident("foo"), "foo"); assert_eq!(normalize_ident("FOO"), "foo"); assert_eq!(normalize_ident("ὈΔΥΣΣΕΎΣ"), "ὀδυσσεύς"); } fn schema_with_tables(create_table_sqls: &[&str]) -> Schema { let mut schema = Schema::new(); for (index, create_table_sql) in create_table_sqls.iter().enumerate() { let root_page = i64::try_from(index).expect("test table index should fit in i64") + 2; let table = BTreeTable::from_sql(create_table_sql, root_page) .expect("test CREATE TABLE should parse"); schema .add_btree_table(std::sync::Arc::new(table)) .expect("test table should be added to schema"); } schema } fn schema_with_table(create_table_sql: &str) -> Schema { schema_with_tables(&[create_table_sql]) } #[test] fn test_rewrite_view_sql_select_table_branch() { let schema = schema_with_table("CREATE TABLE t (a, b)"); let view_sql = "CREATE VIEW v AS SELECT s.x FROM (SELECT b AS x FROM t) AS s"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert!(rewritten.sql.contains("SELECT c AS x FROM t")); } #[test] fn test_rewrite_view_sql_sub_branch() { let schema = schema_with_table("CREATE TABLE t (a, b)"); let view_sql = "CREATE VIEW v AS SELECT s.b FROM (t) AS s"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert!(!rewritten.sql.contains("s.b"), "{}", rewritten.sql); } #[test] fn test_rewrite_view_sql_table_call_branch() { let schema = schema_with_table("CREATE TABLE t (a, b)"); let view_sql = "CREATE VIEW v AS SELECT j.value FROM t JOIN json_each(json_array(t.b)) AS j"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert!(!rewritten.sql.contains("t.b"), "{}", rewritten.sql); } #[test] fn test_rewrite_view_sql_compound_branch() { let schema = schema_with_table("CREATE TABLE t (a, b)"); let view_sql = "CREATE VIEW v AS SELECT b FROM t UNION ALL SELECT b FROM t ORDER BY b"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert_eq!(rewritten.sql.matches("SELECT c FROM t").count(), 2); assert!(!rewritten.sql.contains("ORDER BY b"), "{}", rewritten.sql); assert!(rewritten.sql.contains("ORDER BY c"), "{}", rewritten.sql); } #[test] fn test_rewrite_view_sql_cte_branch() { let schema = schema_with_table("CREATE TABLE t (a, b)"); let view_sql = "CREATE VIEW v AS WITH cte AS (SELECT b FROM t) SELECT b FROM cte"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert!( rewritten.sql.contains("WITH cte AS (SELECT c FROM t)"), "{}", rewritten.sql ); assert!( rewritten.sql.contains("SELECT c FROM cte"), "{}", rewritten.sql ); } #[test] fn test_rewrite_view_sql_cte_branch_with_explicit_columns() { let schema = schema_with_table("CREATE TABLE t (a, b)"); let view_sql = "CREATE VIEW v AS WITH cte(x) AS (SELECT b FROM t) SELECT x FROM cte"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert!( rewritten.sql.contains("WITH cte(x)") || rewritten.sql.contains("WITH cte (x)"), "{}", rewritten.sql ); assert!( rewritten.sql.contains("AS (SELECT c FROM t)"), "{}", rewritten.sql ); assert!( rewritten.sql.contains("SELECT x FROM cte"), "{}", rewritten.sql ); } #[test] fn test_rewrite_view_sql_join_on_branch() { let schema = schema_with_tables(&["CREATE TABLE t (a, b)", "CREATE TABLE u (b)"]); let view_sql = "CREATE VIEW v AS SELECT t.a FROM t JOIN u ON t.b = u.b"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert!(!rewritten.sql.contains("t.b"), "{}", rewritten.sql); assert!(rewritten.sql.contains("t.c = u.b"), "{}", rewritten.sql); } #[test] fn test_rewrite_view_sql_join_using_branch() { let schema = schema_with_tables(&["CREATE TABLE t (a, b)", "CREATE TABLE u (b)"]); let view_sql = "CREATE VIEW v AS SELECT t.a FROM t JOIN u USING (b)"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert!( !rewritten.sql.contains("USING (b)") && !rewritten.sql.contains("USING(b)"), "{}", rewritten.sql ); assert!( rewritten.sql.contains("USING (c)") || rewritten.sql.contains("USING(c)"), "{}", rewritten.sql ); } #[test] fn test_rewrite_view_sql_group_by_having_branch() { let schema = schema_with_table("CREATE TABLE t (a, b)"); let view_sql = "CREATE VIEW v AS SELECT b FROM t GROUP BY b HAVING b > 0"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert!( rewritten .sql .contains("SELECT c FROM t GROUP BY c HAVING c > 0"), "{}", rewritten.sql ); } #[test] fn test_rewrite_view_sql_window_clause_branch() { let schema = schema_with_table("CREATE TABLE t (a, b)"); let view_sql = "CREATE VIEW v AS SELECT sum(a) OVER (PARTITION BY b ORDER BY b) FROM t"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert!( !rewritten.sql.contains("PARTITION BY b"), "{}", rewritten.sql ); assert!(!rewritten.sql.contains("ORDER BY b"), "{}", rewritten.sql); assert!( rewritten.sql.contains("PARTITION BY c"), "{}", rewritten.sql ); assert!(rewritten.sql.contains("ORDER BY c"), "{}", rewritten.sql); } #[test] fn test_rewrite_view_sql_limit_offset_branch() { let schema = schema_with_table("CREATE TABLE t (a, b)"); let view_sql = "CREATE VIEW v AS SELECT a FROM t LIMIT b OFFSET b"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert!(!rewritten.sql.contains("LIMIT b"), "{}", rewritten.sql); assert!(!rewritten.sql.contains("OFFSET b"), "{}", rewritten.sql); assert!(rewritten.sql.contains("LIMIT c"), "{}", rewritten.sql); assert!(rewritten.sql.contains("OFFSET c"), "{}", rewritten.sql); } #[test] fn test_rewrite_view_sql_values_branch() { let schema = schema_with_table("CREATE TABLE t (a, b)"); let view_sql = "CREATE VIEW v AS VALUES ((SELECT b FROM t LIMIT 1))"; let rewritten = rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c") .unwrap() .expect("view should be rewritten"); assert!( rewritten.sql.contains("VALUES ((SELECT c FROM t LIMIT 1))"), "{}", rewritten.sql ); } #[test] fn test_indexed_variable_comparison() { let expr1 = Expr::Variable(Variable::indexed(1u32.try_into().unwrap())); let expr2 = Expr::Variable(Variable::indexed(1u32.try_into().unwrap())); assert!(exprs_are_equivalent(&expr1, &expr2)); } #[test] fn test_named_variable_comparison() { let expr1 = Expr::Variable(Variable::named(":a".to_string(), 1u32.try_into().unwrap())); let expr2 = Expr::Variable(Variable::named(":a".to_string(), 1u32.try_into().unwrap())); assert!(exprs_are_equivalent(&expr1, &expr2)); let expr1 = Expr::Variable(Variable::named(":a".to_string(), 1u32.try_into().unwrap())); let expr2 = Expr::Variable(Variable::named(":b".to_string(), 2u32.try_into().unwrap())); assert!(!exprs_are_equivalent(&expr1, &expr2)); } #[test] fn test_basic_addition_exprs_are_equivalent() { let expr1 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("826".to_string()))), Add, Box::new(Expr::Literal(Literal::Numeric("389".to_string()))), ); let expr2 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("389".to_string()))), Add, Box::new(Expr::Literal(Literal::Numeric("826".to_string()))), ); assert!(exprs_are_equivalent(&expr1, &expr2)); } #[test] fn test_addition_expressions_equivalent_normalized() { // Same types: 123.0 + 243.0 == 243.0 + 123.0 (commutative) let expr1 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("123.0".to_string()))), Add, Box::new(Expr::Literal(Literal::Numeric("243.0".to_string()))), ); let expr2 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("243.0".to_string()))), Add, Box::new(Expr::Literal(Literal::Numeric("123.0".to_string()))), ); assert!(exprs_are_equivalent(&expr1, &expr2)); // Mixed types are NOT equivalent (different result types) let expr3 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("123.0".to_string()))), Add, Box::new(Expr::Literal(Literal::Numeric("243".to_string()))), ); let expr4 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("243.0".to_string()))), Add, Box::new(Expr::Literal(Literal::Numeric("123".to_string()))), ); assert!(!exprs_are_equivalent(&expr3, &expr4)); } #[test] fn test_subtraction_expressions_not_equivalent() { let expr3 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("364".to_string()))), Subtract, Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))), ); let expr4 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))), Subtract, Box::new(Expr::Literal(Literal::Numeric("364".to_string()))), ); assert!(!exprs_are_equivalent(&expr3, &expr4)); } #[test] fn test_subtraction_expressions_normalized() { // Same types: 66.0 - 22.0 == 66.0 - 22.0 let expr3 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("66.0".to_string()))), Subtract, Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))), ); let expr4 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("66.0".to_string()))), Subtract, Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))), ); assert!(exprs_are_equivalent(&expr3, &expr4)); // Mixed types are NOT equivalent let expr5 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("66.0".to_string()))), Subtract, Box::new(Expr::Literal(Literal::Numeric("22".to_string()))), ); let expr6 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("66".to_string()))), Subtract, Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))), ); assert!(!exprs_are_equivalent(&expr5, &expr6)); } #[test] fn test_expressions_equivalent_case_insensitive_functioncalls() { let func1 = Expr::FunctionCall { name: Name::exact("SUM".to_string()), distinctness: None, args: vec![Expr::Id(Name::exact("x".to_string())).into()], order_by: vec![], filter_over: FunctionTail { filter_clause: None, over_clause: None, }, }; let func2 = Expr::FunctionCall { name: Name::exact("sum".to_string()), distinctness: None, args: vec![Expr::Id(Name::exact("x".to_string())).into()], order_by: vec![], filter_over: FunctionTail { filter_clause: None, over_clause: None, }, }; assert!(exprs_are_equivalent(&func1, &func2)); let func3 = Expr::FunctionCall { name: Name::exact("SUM".to_string()), distinctness: Some(ast::Distinctness::Distinct), args: vec![Expr::Id(Name::exact("x".to_string())).into()], order_by: vec![], filter_over: FunctionTail { filter_clause: None, over_clause: None, }, }; assert!(!exprs_are_equivalent(&func1, &func3)); } #[test] fn test_expressions_equivalent_identical_fn_with_distinct() { let sum = Expr::FunctionCall { name: Name::exact("SUM".to_string()), distinctness: None, args: vec![Expr::Id(Name::exact("x".to_string())).into()], order_by: vec![], filter_over: FunctionTail { filter_clause: None, over_clause: None, }, }; let sum_distinct = Expr::FunctionCall { name: Name::exact("SUM".to_string()), distinctness: Some(ast::Distinctness::Distinct), args: vec![Expr::Id(Name::exact("x".to_string())).into()], order_by: vec![], filter_over: FunctionTail { filter_clause: None, over_clause: None, }, }; assert!(!exprs_are_equivalent(&sum, &sum_distinct)); } #[test] fn test_expressions_equivalent_multiplication() { // Same types: 42.0 * 38.0 == 38.0 * 42.0 (commutative) let expr1 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("42.0".to_string()))), Multiply, Box::new(Expr::Literal(Literal::Numeric("38.0".to_string()))), ); let expr2 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("38.0".to_string()))), Multiply, Box::new(Expr::Literal(Literal::Numeric("42.0".to_string()))), ); assert!(exprs_are_equivalent(&expr1, &expr2)); } #[test] fn test_expressions_both_parenthesized_equivalent() { // Same types: (683 + 799) == 799 + 683 (commutative, integers only) let expr1 = Expr::Parenthesized(vec![Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("683".to_string()))), Add, Box::new(Expr::Literal(Literal::Numeric("799".to_string()))), ) .into()]); let expr2 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("799".to_string()))), Add, Box::new(Expr::Literal(Literal::Numeric("683".to_string()))), ); assert!(exprs_are_equivalent(&expr1, &expr2)); } #[test] fn test_expressions_parenthesized_equivalent() { let expr7 = Expr::Parenthesized(vec![Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("6".to_string()))), Add, Box::new(Expr::Literal(Literal::Numeric("7".to_string()))), ) .into()]); let expr8 = Expr::Binary( Box::new(Expr::Literal(Literal::Numeric("6".to_string()))), Add, Box::new(Expr::Literal(Literal::Numeric("7".to_string()))), ); assert!(exprs_are_equivalent(&expr7, &expr8)); } #[test] fn test_like_expressions_equivalent() { let expr1 = Expr::Like { lhs: Box::new(Expr::Id(Name::exact("name".to_string()))), not: false, op: ast::LikeOperator::Like, rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))), escape: Some(Box::new(Expr::Literal(Literal::String("\\".to_string())))), }; let expr2 = Expr::Like { lhs: Box::new(Expr::Id(Name::exact("name".to_string()))), not: false, op: ast::LikeOperator::Like, rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))), escape: Some(Box::new(Expr::Literal(Literal::String("\\".to_string())))), }; assert!(exprs_are_equivalent(&expr1, &expr2)); } #[test] fn test_expressions_equivalent_like_escaped() { let expr1 = Expr::Like { lhs: Box::new(Expr::Id(Name::exact("name".to_string()))), not: false, op: ast::LikeOperator::Like, rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))), escape: Some(Box::new(Expr::Literal(Literal::String("\\".to_string())))), }; let expr2 = Expr::Like { lhs: Box::new(Expr::Id(Name::exact("name".to_string()))), not: false, op: ast::LikeOperator::Like, rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))), escape: Some(Box::new(Expr::Literal(Literal::String("#".to_string())))), }; assert!(!exprs_are_equivalent(&expr1, &expr2)); } #[test] fn test_expressions_equivalent_between() { let expr1 = Expr::Between { lhs: Box::new(Expr::Id(Name::exact("age".to_string()))), not: false, start: Box::new(Expr::Literal(Literal::Numeric("18".to_string()))), end: Box::new(Expr::Literal(Literal::Numeric("65".to_string()))), }; let expr2 = Expr::Between { lhs: Box::new(Expr::Id(Name::exact("age".to_string()))), not: false, start: Box::new(Expr::Literal(Literal::Numeric("18".to_string()))), end: Box::new(Expr::Literal(Literal::Numeric("65".to_string()))), }; assert!(exprs_are_equivalent(&expr1, &expr2)); // differing BETWEEN bounds let expr3 = Expr::Between { lhs: Box::new(Expr::Id(Name::exact("age".to_string()))), not: false, start: Box::new(Expr::Literal(Literal::Numeric("20".to_string()))), end: Box::new(Expr::Literal(Literal::Numeric("65".to_string()))), }; assert!(!exprs_are_equivalent(&expr1, &expr3)); } #[test] fn test_cast_exprs_equivalent() { let cast1 = Expr::Cast { expr: Box::new(Expr::Literal(Literal::Numeric("123".to_string()))), type_name: Some(Type { name: "INTEGER".to_string(), size: None, array_dimensions: 0, }), }; let cast2 = Expr::Cast { expr: Box::new(Expr::Literal(Literal::Numeric("123".to_string()))), type_name: Some(Type { name: "integer".to_string(), size: None, array_dimensions: 0, }), }; assert!(exprs_are_equivalent(&cast1, &cast2)); } #[test] fn test_ident_equivalency() { assert!(check_ident_equivalency("\"foo\"", "foo")); assert!(check_ident_equivalency("[foo]", "foo")); assert!(check_ident_equivalency("`FOO`", "foo")); assert!(check_ident_equivalency("\"foo\"", "`FOO`")); assert!(!check_ident_equivalency("\"foo\"", "[bar]")); assert!(!check_ident_equivalency("foo", "\"bar\"")); } #[test] fn test_simple_uri() { let uri = "file:/home/user/db.sqlite"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db.sqlite"); assert_eq!(opts.authority, None); } #[test] fn test_uri_with_authority() { let uri = "file://localhost/home/user/db.sqlite"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db.sqlite"); assert_eq!(opts.authority, Some("localhost")); } #[test] fn test_uri_with_invalid_authority() { let uri = "file://example.com/home/user/db.sqlite"; let result = OpenOptions::parse(uri); assert!(result.is_err()); } #[test] fn test_uri_with_query_params() { let uri = "file:/home/user/db.sqlite?vfs=unix&mode=ro&immutable=1"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db.sqlite"); assert_eq!(opts.vfs, Some("unix".to_string())); assert_eq!(opts.mode, OpenMode::ReadOnly); assert!(opts.immutable); } #[test] fn test_uri_with_fragment() { let uri = "file:/home/user/db.sqlite#section1"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db.sqlite"); } #[test] fn test_uri_with_percent_encoding() { let uri = "file:/home/user/db%20with%20spaces.sqlite?vfs=unix"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db with spaces.sqlite"); assert_eq!(opts.vfs, Some("unix".to_string())); } #[test] fn test_uri_without_scheme() { let uri = "/home/user/db.sqlite"; let result = OpenOptions::parse(uri); assert!(result.is_ok()); assert_eq!(result.unwrap().path, "/home/user/db.sqlite"); } #[test] fn test_uri_with_empty_query() { let uri = "file:/home/user/db.sqlite?"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db.sqlite"); assert_eq!(opts.vfs, None); } #[test] fn test_uri_with_partial_query() { let uri = "file:/home/user/db.sqlite?mode=rw"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db.sqlite"); assert_eq!(opts.mode, OpenMode::ReadWrite); assert_eq!(opts.vfs, None); } #[test] fn test_uri_windows_style_path() { let uri = "file:///C:/Users/test/db.sqlite"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/C:/Users/test/db.sqlite"); } #[test] fn test_uri_with_only_query_params() { let uri = "file:?mode=memory&cache=shared"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, ""); assert_eq!(opts.mode, OpenMode::Memory); assert_eq!(opts.cache, CacheMode::Shared); } #[test] fn test_uri_with_only_fragment() { let uri = "file:#fragment"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, ""); } #[test] fn test_uri_with_invalid_scheme() { let uri = "http:/home/user/db.sqlite"; let result = OpenOptions::parse(uri); assert!(result.is_ok()); assert_eq!(result.unwrap().path, "http:/home/user/db.sqlite"); } #[test] fn test_uri_with_multiple_query_params() { let uri = "file:/home/user/db.sqlite?vfs=unix&mode=rw&cache=private&immutable=0"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db.sqlite"); assert_eq!(opts.vfs, Some("unix".to_string())); assert_eq!(opts.mode, OpenMode::ReadWrite); assert_eq!(opts.cache, CacheMode::Private); assert!(!opts.immutable); } #[test] fn test_uri_with_unknown_query_param() { let uri = "file:/home/user/db.sqlite?unknown=param"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db.sqlite"); assert_eq!(opts.vfs, None); } #[test] fn test_uri_with_multiple_equal_signs() { let uri = "file:/home/user/db.sqlite?vfs=unix=custom"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db.sqlite"); assert_eq!(opts.vfs, Some("unix=custom".to_string())); } #[test] fn test_uri_with_trailing_slash() { let uri = "file:/home/user/db.sqlite/"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db.sqlite/"); } #[test] fn test_uri_with_encoded_characters_in_query() { let uri = "file:/home/user/db.sqlite?vfs=unix%20mode"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/user/db.sqlite"); assert_eq!(opts.vfs, Some("unix mode".to_string())); } #[test] fn test_uri_windows_network_path() { let uri = "file://server/share/db.sqlite"; let result = OpenOptions::parse(uri); assert!(result.is_err()); // non-localhost authority should fail } #[test] fn test_uri_windows_drive_letter_with_slash() { let uri = "file:///C:/database.sqlite"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/C:/database.sqlite"); } #[test] fn test_localhost_with_double_slash_and_no_path() { let uri = "file://localhost"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, ""); assert_eq!(opts.authority, Some("localhost")); } #[test] fn test_uri_windows_drive_letter_without_slash() { let uri = "file:///C:/database.sqlite"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/C:/database.sqlite"); } #[test] fn test_improper_mode() { // any other mode but ro, rwc, rw, memory should fail per sqlite let uri = "file:data.db?mode=readonly"; let res = OpenOptions::parse(uri); assert!(res.is_err()); // including empty let uri = "file:/home/user/db.sqlite?vfs=&mode="; let res = OpenOptions::parse(uri); assert!(res.is_err()); } // Some examples from https://www.sqlite.org/c3ref/open.html#urifilenameexamples #[test] fn test_simple_file_current_dir() { let uri = "file:data.db"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "data.db"); assert_eq!(opts.authority, None); assert_eq!(opts.vfs, None); assert_eq!(opts.mode, OpenMode::ReadWriteCreate); } #[test] fn test_simple_file_three_slash() { let uri = "file:///home/data/data.db"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/data/data.db"); assert_eq!(opts.authority, None); assert_eq!(opts.vfs, None); assert_eq!(opts.mode, OpenMode::ReadWriteCreate); } #[test] fn test_simple_file_two_slash_localhost() { let uri = "file://localhost/home/fred/data.db"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/home/fred/data.db"); assert_eq!(opts.authority, Some("localhost")); assert_eq!(opts.vfs, None); } #[test] fn test_windows_double_invalid() { let uri = "file://C:/home/fred/data.db?mode=ro"; let opts = OpenOptions::parse(uri); assert!(opts.is_err()); } #[test] fn test_simple_file_two_slash() { let uri = "file:///C:/Documents%20and%20Settings/fred/Desktop/data.db"; let opts = OpenOptions::parse(uri).unwrap(); assert_eq!(opts.path, "/C:/Documents and Settings/fred/Desktop/data.db"); assert_eq!(opts.vfs, None); } #[test] fn test_decode_percent_basic() { assert_eq!(decode_percent("hello%20world"), "hello world"); assert_eq!(decode_percent("file%3Adata.db"), "file:data.db"); assert_eq!(decode_percent("path%2Fto%2Ffile"), "path/to/file"); } #[test] fn test_decode_percent_edge_cases() { assert_eq!(decode_percent(""), ""); assert_eq!(decode_percent("plain_text"), "plain_text"); assert_eq!( decode_percent("%2Fhome%2Fuser%2Fdb.sqlite"), "/home/user/db.sqlite" ); // multiple percent-encoded characters in sequence assert_eq!(decode_percent("%41%42%43"), "ABC"); assert_eq!(decode_percent("%61%62%63"), "abc"); } #[test] fn test_decode_percent_invalid_sequences() { // invalid percent encoding (single % without two hex digits) assert_eq!(decode_percent("hello%"), "hello%"); // only one hex digit after % assert_eq!(decode_percent("file%2"), "file%2"); // invalid hex digits (not 0-9, A-F, a-f) assert_eq!(decode_percent("file%2X.db"), "file%2X.db"); // Incomplete sequence at the end, leave untouched assert_eq!(decode_percent("path%2Fto%2"), "path/to%2"); } #[test] fn test_decode_percent_mixed_valid_invalid() { assert_eq!(decode_percent("hello%20world%"), "hello world%"); assert_eq!(decode_percent("%2Fpath%2Xto%2Ffile"), "/path%2Xto/file"); assert_eq!(decode_percent("file%3Adata.db%2"), "file:data.db%2"); } #[test] fn test_decode_percent_special_characters() { assert_eq!( decode_percent("%21%40%23%24%25%5E%26%2A%28%29"), "!@#$%^&*()" ); assert_eq!(decode_percent("%5B%5D%7B%7D%7C%5C%3A"), "[]{}|\\:"); } #[test] fn test_decode_percent_unmodified_valid_text() { // ensure already valid text remains unchanged assert_eq!( decode_percent("C:/Users/Example/Database.sqlite"), "C:/Users/Example/Database.sqlite" ); assert_eq!( decode_percent("/home/user/db.sqlite"), "/home/user/db.sqlite" ); } #[test] fn test_text_to_integer() { assert_eq!( checked_cast_text_to_numeric("1", false).unwrap(), Value::from_i64(1) ); assert_eq!( checked_cast_text_to_numeric("-1", false).unwrap(), Value::from_i64(-1) ); assert_eq!( checked_cast_text_to_numeric("1823400-00000", false).unwrap(), Value::from_i64(1823400) ); assert_eq!( checked_cast_text_to_numeric("-10000000", false).unwrap(), Value::from_i64(-10000000) ); assert_eq!( checked_cast_text_to_numeric("123xxx", false).unwrap(), Value::from_i64(123) ); assert_eq!( checked_cast_text_to_numeric("9223372036854775807", false).unwrap(), Value::from_i64(i64::MAX) ); // Overflow becomes Float (different from cast_text_to_integer which returned 0) assert_eq!( checked_cast_text_to_numeric("9223372036854775808", false).unwrap(), Value::from_f64(9.22337203685478e18) ); assert_eq!( checked_cast_text_to_numeric("-9223372036854775808", false).unwrap(), Value::from_i64(i64::MIN) ); // Overflow becomes Float (different from cast_text_to_integer which returned 0) assert_eq!( checked_cast_text_to_numeric("-9223372036854775809", false).unwrap(), Value::from_f64(-9.22337203685478e18) ); assert!(checked_cast_text_to_numeric("-", false).is_err()); } #[test] fn test_text_to_real() { assert_eq!( checked_cast_text_to_numeric("1", false).unwrap(), Value::from_i64(1) ); assert_eq!( checked_cast_text_to_numeric("-1", false).unwrap(), Value::from_i64(-1) ); assert_eq!( checked_cast_text_to_numeric("1.0", false).unwrap(), Value::from_f64(1.0) ); assert_eq!( checked_cast_text_to_numeric("-1.0", false).unwrap(), Value::from_f64(-1.0) ); assert_eq!( checked_cast_text_to_numeric("1e10", false).unwrap(), Value::from_f64(1e10) ); assert_eq!( checked_cast_text_to_numeric("-1e10", false).unwrap(), Value::from_f64(-1e10) ); assert_eq!( checked_cast_text_to_numeric("1e-10", false).unwrap(), Value::from_f64(1e-10) ); assert_eq!( checked_cast_text_to_numeric("-1e-10", false).unwrap(), Value::from_f64(-1e-10) ); assert_eq!( checked_cast_text_to_numeric("1.123e10", false).unwrap(), Value::from_f64(1.123e10) ); assert_eq!( checked_cast_text_to_numeric("-1.123e10", false).unwrap(), Value::from_f64(-1.123e10) ); assert_eq!( checked_cast_text_to_numeric("1.123e-10", false).unwrap(), Value::from_f64(1.123e-10) ); assert_eq!( checked_cast_text_to_numeric("-1.123-e-10", false).unwrap(), Value::from_f64(-1.123) ); assert_eq!( checked_cast_text_to_numeric("1-282584294928", false).unwrap(), Value::from_i64(1) ); assert_eq!( checked_cast_text_to_numeric("1.7976931348623157e309", false).unwrap(), Value::from_f64(f64::INFINITY), ); assert_eq!( checked_cast_text_to_numeric("-1.7976931348623157e308", false).unwrap(), Value::from_f64(f64::MIN), ); assert_eq!( checked_cast_text_to_numeric("-1.7976931348623157e309", false).unwrap(), Value::from_f64(f64::NEG_INFINITY), ); assert_eq!( checked_cast_text_to_numeric("1E", false).unwrap(), Value::from_f64(1.0) ); assert_eq!( checked_cast_text_to_numeric("1EE", false).unwrap(), Value::from_f64(1.0) ); assert_eq!( checked_cast_text_to_numeric("-1E", false).unwrap(), Value::from_f64(-1.0) ); assert_eq!( checked_cast_text_to_numeric("1.", false).unwrap(), Value::from_f64(1.0) ); assert_eq!( checked_cast_text_to_numeric("-1.", false).unwrap(), Value::from_f64(-1.0) ); assert_eq!( checked_cast_text_to_numeric("1.23E", false).unwrap(), Value::from_f64(1.23) ); assert_eq!( checked_cast_text_to_numeric(".1.23E-", false).unwrap(), Value::from_f64(0.1) ); assert_eq!( checked_cast_text_to_numeric("0", false).unwrap(), Value::from_i64(0) ); assert_eq!( checked_cast_text_to_numeric("-0", false).unwrap(), Value::from_i64(0) ); assert_eq!( checked_cast_text_to_numeric("-0", false).unwrap(), Value::from_i64(0) ); assert_eq!( checked_cast_text_to_numeric("-0.0", false).unwrap(), Value::from_f64(0.0) ); assert_eq!( checked_cast_text_to_numeric("0.0", false).unwrap(), Value::from_f64(0.0) ); assert!(checked_cast_text_to_numeric("-", false).is_err()); } #[test] fn test_text_to_numeric() { assert_eq!( checked_cast_text_to_numeric("1", false).unwrap(), Value::from_i64(1) ); assert_eq!( checked_cast_text_to_numeric("-1", false).unwrap(), Value::from_i64(-1) ); assert_eq!( checked_cast_text_to_numeric("1823400-00000", false).unwrap(), Value::from_i64(1823400) ); assert_eq!( checked_cast_text_to_numeric("-10000000", false).unwrap(), Value::from_i64(-10000000) ); assert_eq!( checked_cast_text_to_numeric("123xxx", false).unwrap(), Value::from_i64(123) ); assert_eq!( checked_cast_text_to_numeric("9223372036854775807", false).unwrap(), Value::from_i64(i64::MAX) ); assert_eq!( checked_cast_text_to_numeric("9223372036854775808", false).unwrap(), Value::from_f64(9.22337203685478e18) ); // Exceeds i64, becomes float assert_eq!( checked_cast_text_to_numeric("-9223372036854775808", false).unwrap(), Value::from_i64(i64::MIN) ); assert_eq!( checked_cast_text_to_numeric("-9223372036854775809", false).unwrap(), Value::from_f64(-9.22337203685478e18) ); // Exceeds i64, becomes float assert_eq!( checked_cast_text_to_numeric("1.0", false).unwrap(), Value::from_f64(1.0) ); assert_eq!( checked_cast_text_to_numeric("-1.0", false).unwrap(), Value::from_f64(-1.0) ); assert_eq!( checked_cast_text_to_numeric("1e10", false).unwrap(), Value::from_f64(1e10) ); assert_eq!( checked_cast_text_to_numeric("-1e10", false).unwrap(), Value::from_f64(-1e10) ); assert_eq!( checked_cast_text_to_numeric("1e-10", false).unwrap(), Value::from_f64(1e-10) ); assert_eq!( checked_cast_text_to_numeric("-1e-10", false).unwrap(), Value::from_f64(-1e-10) ); assert_eq!( checked_cast_text_to_numeric("1.123e10", false).unwrap(), Value::from_f64(1.123e10) ); assert_eq!( checked_cast_text_to_numeric("-1.123e10", false).unwrap(), Value::from_f64(-1.123e10) ); assert_eq!( checked_cast_text_to_numeric("1.123e-10", false).unwrap(), Value::from_f64(1.123e-10) ); assert_eq!( checked_cast_text_to_numeric("-1.123-e-10", false).unwrap(), Value::from_f64(-1.123) ); assert_eq!( checked_cast_text_to_numeric("1-282584294928", false).unwrap(), Value::from_i64(1) ); assert!(checked_cast_text_to_numeric("xxx", false).is_err()); assert_eq!( checked_cast_text_to_numeric("1.7976931348623157e309", false).unwrap(), Value::from_f64(f64::INFINITY) ); assert_eq!( checked_cast_text_to_numeric("-1.7976931348623157e308", false).unwrap(), Value::from_f64(f64::MIN) ); assert_eq!( checked_cast_text_to_numeric("-1.7976931348623157e309", false).unwrap(), Value::from_f64(f64::NEG_INFINITY) ); assert_eq!( checked_cast_text_to_numeric("1E", false).unwrap(), Value::from_f64(1.0) ); assert_eq!( checked_cast_text_to_numeric("1EE", false).unwrap(), Value::from_f64(1.0) ); assert_eq!( checked_cast_text_to_numeric("-1E", false).unwrap(), Value::from_f64(-1.0) ); assert_eq!( checked_cast_text_to_numeric("1.", false).unwrap(), Value::from_f64(1.0) ); assert_eq!( checked_cast_text_to_numeric("-1.", false).unwrap(), Value::from_f64(-1.0) ); assert_eq!( checked_cast_text_to_numeric("1.23E", false).unwrap(), Value::from_f64(1.23) ); assert_eq!( checked_cast_text_to_numeric("1.23E-", false).unwrap(), Value::from_f64(1.23) ); assert_eq!( checked_cast_text_to_numeric("0", false).unwrap(), Value::from_i64(0) ); assert_eq!( checked_cast_text_to_numeric("-0", false).unwrap(), Value::from_i64(0) ); assert_eq!( checked_cast_text_to_numeric("-0.0", false).unwrap(), Value::from_f64(0.0) ); assert_eq!( checked_cast_text_to_numeric("0.0", false).unwrap(), Value::from_f64(0.0) ); assert!(checked_cast_text_to_numeric("-", false).is_err()); assert_eq!( checked_cast_text_to_numeric("-e", false).unwrap(), Value::from_f64(0.0) ); assert_eq!( checked_cast_text_to_numeric("-E", false).unwrap(), Value::from_f64(0.0) ); } #[test] fn test_parse_numeric_str_valid_integer() { assert_eq!(parse_numeric_str("123"), Ok((ValueType::Integer, "123"))); assert_eq!(parse_numeric_str("-456"), Ok((ValueType::Integer, "-456"))); assert_eq!(parse_numeric_str("+789"), Ok((ValueType::Integer, "+789"))); assert_eq!( parse_numeric_str("000789"), Ok((ValueType::Integer, "000789")) ); } #[test] fn test_parse_numeric_str_valid_float() { assert_eq!( parse_numeric_str("123.456"), Ok((ValueType::Float, "123.456")) ); assert_eq!( parse_numeric_str("-0.789"), Ok((ValueType::Float, "-0.789")) ); assert_eq!( parse_numeric_str("+0.789"), Ok((ValueType::Float, "+0.789")) ); assert_eq!(parse_numeric_str("1e10"), Ok((ValueType::Float, "1e10"))); assert_eq!(parse_numeric_str("+1e10"), Ok((ValueType::Float, "+1e10"))); assert_eq!( parse_numeric_str("-1.23e-4"), Ok((ValueType::Float, "-1.23e-4")) ); assert_eq!( parse_numeric_str("1.23E+4"), Ok((ValueType::Float, "1.23E+4")) ); assert_eq!(parse_numeric_str("1.2.3"), Ok((ValueType::Float, "1.2"))) } #[test] fn test_parse_numeric_str_edge_cases() { assert_eq!(parse_numeric_str("1e"), Ok((ValueType::Float, "1"))); assert_eq!(parse_numeric_str("1e-"), Ok((ValueType::Float, "1"))); assert_eq!(parse_numeric_str("1e+"), Ok((ValueType::Float, "1"))); assert_eq!(parse_numeric_str("-1e"), Ok((ValueType::Float, "-1"))); assert_eq!(parse_numeric_str("-1e-"), Ok((ValueType::Float, "-1"))); } #[test] fn test_parse_numeric_str_invalid() { assert_eq!(parse_numeric_str(""), Err(())); assert_eq!(parse_numeric_str("abc"), Err(())); assert_eq!(parse_numeric_str("-"), Err(())); assert_eq!(parse_numeric_str("+"), Err(())); assert_eq!(parse_numeric_str("e10"), Err(())); assert_eq!(parse_numeric_str(".e10"), Err(())); } #[test] fn test_parse_numeric_str_with_whitespace() { assert_eq!(parse_numeric_str(" 123"), Ok((ValueType::Integer, "123"))); assert_eq!( parse_numeric_str(" -456.78 "), Ok((ValueType::Float, "-456.78")) ); assert_eq!( parse_numeric_str(" 1.23e4 "), Ok((ValueType::Float, "1.23e4")) ); } #[test] fn test_parse_numeric_str_leading_zeros() { assert_eq!( parse_numeric_str("000123"), Ok((ValueType::Integer, "000123")) ); assert_eq!( parse_numeric_str("000.456"), Ok((ValueType::Float, "000.456")) ); assert_eq!( parse_numeric_str("0001e3"), Ok((ValueType::Float, "0001e3")) ); } #[test] fn test_parse_numeric_str_trailing_characters() { assert_eq!(parse_numeric_str("123abc"), Ok((ValueType::Integer, "123"))); assert_eq!( parse_numeric_str("456.78xyz"), Ok((ValueType::Float, "456.78")) ); assert_eq!( parse_numeric_str("1.23e4extra"), Ok((ValueType::Float, "1.23e4")) ); } #[test] fn test_module_name_basic() { let sql = "CREATE VIRTUAL TABLE x USING y;"; assert_eq!(module_name_from_sql(sql).unwrap(), "y"); } #[test] fn test_module_name_with_args() { let sql = "CREATE VIRTUAL TABLE x USING modname('a', 'b');"; assert_eq!(module_name_from_sql(sql).unwrap(), "modname"); } #[test] fn test_module_name_missing_using() { let sql = "CREATE VIRTUAL TABLE x (a, b);"; assert!(module_name_from_sql(sql).is_err()); } #[test] fn test_module_name_no_semicolon() { let sql = "CREATE VIRTUAL TABLE x USING limbo(a, b)"; assert_eq!(module_name_from_sql(sql).unwrap(), "limbo"); } #[test] fn test_module_name_no_semicolon_or_args() { let sql = "CREATE VIRTUAL TABLE x USING limbo"; assert_eq!(module_name_from_sql(sql).unwrap(), "limbo"); } #[test] fn test_module_args_none() { let sql = "CREATE VIRTUAL TABLE x USING modname;"; let args = module_args_from_sql(sql).unwrap(); assert_eq!(args.len(), 0); } #[test] fn test_module_args_basic() { let sql = "CREATE VIRTUAL TABLE x USING modname('arg1', 'arg2');"; let args = module_args_from_sql(sql).unwrap(); assert_eq!(args.len(), 2); assert_eq!("arg1", args[0].to_text().unwrap()); assert_eq!("arg2", args[1].to_text().unwrap()); for arg in args { unsafe { arg.__free_internal_type() } } } #[test] fn test_module_args_with_escaped_quote() { let sql = "CREATE VIRTUAL TABLE x USING modname('a''b', 'c');"; let args = module_args_from_sql(sql).unwrap(); assert_eq!(args.len(), 2); assert_eq!(args[0].to_text().unwrap(), "a'b"); assert_eq!(args[1].to_text().unwrap(), "c"); for arg in args { unsafe { arg.__free_internal_type() } } } #[test] fn test_module_args_unterminated_string() { let sql = "CREATE VIRTUAL TABLE x USING modname('arg1, 'arg2');"; assert!(module_args_from_sql(sql).is_err()); } #[test] fn test_module_args_extra_garbage_after_quote() { let sql = "CREATE VIRTUAL TABLE x USING modname('arg1'x);"; assert!(module_args_from_sql(sql).is_err()); } #[test] fn test_module_args_trailing_comma() { let sql = "CREATE VIRTUAL TABLE x USING modname('arg1',);"; let args = module_args_from_sql(sql).unwrap(); assert_eq!(args.len(), 1); assert_eq!("arg1", args[0].to_text().unwrap()); for arg in args { unsafe { arg.__free_internal_type() } } } #[test] fn test_parse_numeric_literal_hex() { assert_eq!( parse_numeric_literal("0x1234").unwrap(), Value::from_i64(4660) ); assert_eq!( parse_numeric_literal("0xFFFFFFFF").unwrap(), Value::from_i64(4294967295) ); assert_eq!( parse_numeric_literal("0x7FFFFFFF").unwrap(), Value::from_i64(2147483647) ); assert_eq!( parse_numeric_literal("0x7FFFFFFFFFFFFFFF").unwrap(), Value::from_i64(9223372036854775807) ); assert_eq!( parse_numeric_literal("0xFFFFFFFFFFFFFFFF").unwrap(), Value::from_i64(-1) ); assert_eq!( parse_numeric_literal("0x8000000000000000").unwrap(), Value::from_i64(-9223372036854775808) ); assert_eq!( parse_numeric_literal("-0x1234").unwrap(), Value::from_i64(-4660) ); // too big hex assert!(parse_numeric_literal("-0x8000000000000000").is_err()); } #[test] fn test_parse_numeric_literal_integer() { assert_eq!(parse_numeric_literal("123").unwrap(), Value::from_i64(123)); assert_eq!( parse_numeric_literal("9_223_372_036_854_775_807").unwrap(), Value::from_i64(9223372036854775807) ); } #[test] fn test_parse_numeric_literal_float() { assert_eq!( parse_numeric_literal("123.456").unwrap(), Value::from_f64(123.456) ); assert_eq!( parse_numeric_literal(".123").unwrap(), Value::from_f64(0.123) ); assert_eq!( parse_numeric_literal("1.23e10").unwrap(), Value::from_f64(1.23e10) ); assert_eq!( parse_numeric_literal("1e-10").unwrap(), Value::from_f64(1e-10) ); assert_eq!( parse_numeric_literal("1.23E+10").unwrap(), Value::from_f64(1.23e10) ); assert_eq!( parse_numeric_literal("1.1_1").unwrap(), Value::from_f64(1.11) ); // > i64::MAX, convert to float assert_eq!( parse_numeric_literal("9223372036854775808").unwrap(), Value::from_f64(9.223_372_036_854_776e18) ); // < i64::MIN, convert to float assert_eq!( parse_numeric_literal("-9223372036854775809").unwrap(), Value::from_f64(-9.223_372_036_854_776e18) ); } #[test] fn test_parse_pragma_bool() { assert!(parse_pragma_bool(&Expr::Literal(Literal::Numeric("1".into()))).unwrap(),); assert!(parse_pragma_bool(&Expr::Name(Name::exact("true".into()))).unwrap(),); assert!(parse_pragma_bool(&Expr::Name(Name::exact("on".into()))).unwrap(),); assert!(parse_pragma_bool(&Expr::Name(Name::exact("yes".into()))).unwrap(),); assert!(!parse_pragma_bool(&Expr::Literal(Literal::Numeric("0".into()))).unwrap(),); assert!(!parse_pragma_bool(&Expr::Name(Name::exact("false".into()))).unwrap(),); assert!(!parse_pragma_bool(&Expr::Name(Name::exact("off".into()))).unwrap(),); assert!(!parse_pragma_bool(&Expr::Name(Name::exact("no".into()))).unwrap(),); assert!(parse_pragma_bool(&Expr::Name(Name::exact("nono".into()))).is_err()); assert!(parse_pragma_bool(&Expr::Name(Name::exact("10".into()))).is_err()); assert!(parse_pragma_bool(&Expr::Name(Name::exact("-1".into()))).is_err()); } #[test] fn test_type_from_name() { let tc = vec![ ("", (SchemaValueType::Blob, false)), ("INTEGER", (SchemaValueType::Integer, true)), ("INT", (SchemaValueType::Integer, false)), ("CHAR", (SchemaValueType::Text, false)), ("CLOB", (SchemaValueType::Text, false)), ("TEXT", (SchemaValueType::Text, false)), ("BLOB", (SchemaValueType::Blob, false)), ("REAL", (SchemaValueType::Real, false)), ("FLOAT", (SchemaValueType::Real, false)), ("DOUBLE", (SchemaValueType::Real, false)), ("U128", (SchemaValueType::Numeric, false)), ]; for (input, expected) in tc { let result = type_from_name(input); assert_eq!(result, expected, "Failed for input: {input}"); } } #[test] fn test_checked_cast_text_to_numeric_lossless_property() { assert_eq!(checked_cast_text_to_numeric("1.xx", true), Err(())); assert_eq!(checked_cast_text_to_numeric("abc", true), Err(())); assert_eq!(checked_cast_text_to_numeric("--5", true), Err(())); assert_eq!(checked_cast_text_to_numeric("12.34.56", true), Err(())); assert_eq!(checked_cast_text_to_numeric("", true), Err(())); assert_eq!(checked_cast_text_to_numeric(" ", true), Err(())); assert_eq!( checked_cast_text_to_numeric("0", true), Ok(Value::from_i64(0)) ); assert_eq!( checked_cast_text_to_numeric("42", true), Ok(Value::from_i64(42)) ); assert_eq!( checked_cast_text_to_numeric("-42", true), Ok(Value::from_i64(-42)) ); assert_eq!( checked_cast_text_to_numeric("999999999999", true), Ok(Value::from_i64(999_999_999_999)) ); assert_eq!( checked_cast_text_to_numeric("1.0", true), Ok(Value::from_f64(1.0)) ); assert_eq!( checked_cast_text_to_numeric("-3.22", true), Ok(Value::from_f64(-3.22)) ); assert_eq!( checked_cast_text_to_numeric("0.001", true), Ok(Value::from_f64(0.001)) ); assert_eq!( checked_cast_text_to_numeric("2e3", true), Ok(Value::from_f64(2000.0)) ); assert_eq!( checked_cast_text_to_numeric("-5.5e-2", true), Ok(Value::from_f64(-0.055)) ); assert_eq!( checked_cast_text_to_numeric(" 123 ", true), Ok(Value::from_i64(123)) ); assert_eq!( checked_cast_text_to_numeric("\t-3.22\n", true), Ok(Value::from_f64(-3.22)) ); } #[test] fn test_trim_ascii_whitespace_helper() { assert_eq!(trim_ascii_whitespace(" hello "), "hello"); assert_eq!(trim_ascii_whitespace("\t\nhello\r\n"), "hello"); assert_eq!(trim_ascii_whitespace("hello"), "hello"); assert_eq!(trim_ascii_whitespace(" "), ""); assert_eq!(trim_ascii_whitespace(""), ""); // non-breaking space should NOT be trimmed assert_eq!( trim_ascii_whitespace("\u{00A0}hello\u{00A0}"), "\u{00A0}hello\u{00A0}" ); assert_eq!( trim_ascii_whitespace(" \u{00A0}hello\u{00A0} "), "\u{00A0}hello\u{00A0}" ); } #[test] fn test_cast_real_to_integer_limits() { // Values that are exactly representable in f64 and strictly within i64 range let max_exact = ((1i64 << 51) - 1) as f64; assert_eq!(cast_real_to_integer(max_exact), Ok((1i64 << 51) - 1)); assert_eq!(cast_real_to_integer(-max_exact), Ok(-((1i64 << 51) - 1))); // Values beyond 2^51 are valid if they round-trip correctly and are strictly within bounds assert_eq!(cast_real_to_integer((1i64 << 51) as f64), Ok(1i64 << 51)); assert_eq!(cast_real_to_integer((1i64 << 52) as f64), Ok(1i64 << 52)); // 2^62 round-trips correctly and is strictly between i64::MIN and i64::MAX assert_eq!(cast_real_to_integer((1i64 << 62) as f64), Ok(1i64 << 62)); // The original bug's value: 426601719749026560 should work assert_eq!( cast_real_to_integer(426601719749026560.0), Ok(426601719749026560) ); // SQLite rejects boundary values: i64::MIN and i64::MAX exactly // (ix > SMALLEST_INT64 && ix < LARGEST_INT64 requires STRICT inequality) assert_eq!(cast_real_to_integer(i64::MIN as f64), Err(())); assert_eq!(cast_real_to_integer(i64::MAX as f64), Err(())); // Values at or beyond i64::MAX + 1 (2^63) should fail assert_eq!(cast_real_to_integer(9223372036854775808.0), Err(())); // Values below i64::MIN should fail assert_eq!(cast_real_to_integer(-9223372036854777856.0), Err(())); // Non-whole numbers should fail assert_eq!(cast_real_to_integer(1.5), Err(())); assert_eq!(cast_real_to_integer(-1.5), Err(())); // Non-finite values should fail assert_eq!(cast_real_to_integer(f64::INFINITY), Err(())); assert_eq!(cast_real_to_integer(f64::NEG_INFINITY), Err(())); assert_eq!(cast_real_to_integer(f64::NAN), Err(())); } } ================================================ FILE: core/uuid.rs ================================================ use crate::ext::register_scalar_function; use turso_ext::{scalar, ExtensionApi, ResultCode, Value, ValueType}; pub fn register_extension(ext_api: &mut ExtensionApi) { // FIXME: Add macro magic to register functions automatically. unsafe { register_scalar_function(ext_api.ctx, c"uuid4_str".as_ptr(), uuid4_str); register_scalar_function(ext_api.ctx, c"gen_random_uuid".as_ptr(), uuid4_str); register_scalar_function(ext_api.ctx, c"uuid4".as_ptr(), uuid4_blob); register_scalar_function(ext_api.ctx, c"uuid7_str".as_ptr(), uuid7_str); register_scalar_function(ext_api.ctx, c"uuid7".as_ptr(), uuid7); register_scalar_function(ext_api.ctx, c"uuid7_timestamp_ms".as_ptr(), uuid7_ts); register_scalar_function(ext_api.ctx, c"uuid_str".as_ptr(), uuid_str); register_scalar_function(ext_api.ctx, c"uuid_blob".as_ptr(), uuid_blob); } } #[scalar(name = "uuid4_str", alias = "gen_random_uuid")] fn uuid4_str(_args: &[Value]) -> Value { let uuid = uuid::Uuid::new_v4().to_string(); Value::from_text(uuid) } #[scalar(name = "uuid4")] fn uuid4_blob(_args: &[Value]) -> Value { let uuid = uuid::Uuid::new_v4(); let bytes = uuid.as_bytes(); Value::from_blob(bytes.to_vec()) } #[scalar(name = "uuid7_str")] fn uuid7_str(args: &[Value]) -> Value { let timestamp = if args.is_empty() { let ctx = uuid::ContextV7::new(); uuid::Timestamp::now(ctx) } else { match args[0].value_type() { ValueType::Integer => { let ctx = uuid::ContextV7::new(); let Some(int) = args[0].to_integer() else { return Value::error(ResultCode::InvalidArgs); }; uuid::Timestamp::from_unix(ctx, int as u64, 0) } ValueType::Text => { let Some(text) = args[0].to_text() else { return Value::error(ResultCode::InvalidArgs); }; match text.parse::() { Ok(unix) => { if unix <= 0 { return Value::error_with_message("Invalid timestamp".to_string()); } uuid::Timestamp::from_unix(uuid::ContextV7::new(), unix as u64, 0) } Err(_) => return Value::error(ResultCode::InvalidArgs), } } _ => return Value::error(ResultCode::InvalidArgs), } }; let uuid = uuid::Uuid::new_v7(timestamp); Value::from_text(uuid.to_string()) } #[scalar(name = "uuid7")] fn uuid7(&self, args: &[Value]) -> Value { let timestamp = if args.is_empty() { let ctx = uuid::ContextV7::new(); uuid::Timestamp::now(ctx) } else { match args[0].value_type() { ValueType::Integer => { let ctx = uuid::ContextV7::new(); let Some(int) = args[0].to_integer() else { return Value::null(); }; uuid::Timestamp::from_unix(ctx, int as u64, 0) } _ => return Value::null(), } }; let uuid = uuid::Uuid::new_v7(timestamp); let bytes = uuid.as_bytes(); Value::from_blob(bytes.to_vec()) } #[scalar(name = "uuid7_timestamp_ms")] fn uuid7_ts(args: &[Value]) -> Value { match args.first().map(|a| a.value_type()) { Some(ValueType::Blob) => { let Some(blob) = &args[0].to_blob() else { return Value::null(); }; let Ok(uuid) = uuid::Uuid::from_slice(blob.as_slice()) else { return Value::null(); }; let unix = uuid_to_unix(uuid.as_bytes()); Value::from_integer(unix as i64) } Some(ValueType::Text) => { let Some(text) = args[0].to_text() else { return Value::null(); }; let Ok(uuid) = uuid::Uuid::parse_str(text) else { return Value::null(); }; let unix = uuid_to_unix(uuid.as_bytes()); Value::from_integer(unix as i64) } None => Value::error_with_message( "wrong number of arguments to function uuid7_timestamp_ms()".into(), ), _ => Value::null(), } } #[scalar(name = "uuid_str")] fn uuid_str(args: &[Value]) -> Value { let Some(blob) = args.first().and_then(|a| a.to_blob()) else { return Value::error_with_message( "wrong number of arguments to function uuid_str()".into(), ); }; let parsed = uuid::Uuid::from_slice(blob.as_slice()) .ok() .map(|u| u.to_string()); match parsed { Some(s) => Value::from_text(s), None => Value::null(), } } #[scalar(name = "uuid_blob")] fn uuid_blob(&self, args: &[Value]) -> Value { let Some(text) = args.first().and_then(|a| a.to_text()) else { return Value::error_with_message( "wrong number of arguments to function uuid_blob()".into(), ); }; match uuid::Uuid::parse_str(text) { Ok(uuid) => Value::from_blob(uuid.as_bytes().to_vec()), Err(_) => Value::null(), } } #[inline(always)] fn uuid_to_unix(uuid: &[u8; 16]) -> u64 { ((uuid[0] as u64) << 40) | ((uuid[1] as u64) << 32) | ((uuid[2] as u64) << 24) | ((uuid[3] as u64) << 16) | ((uuid[4] as u64) << 8) | (uuid[5] as u64) } ================================================ FILE: core/vdbe/affinity.rs ================================================ use either::Either; use turso_parser::ast::{Expr, Literal}; use crate::{ numeric::{format_float, DoubleDouble, Numeric}, types::AsValueRef, Value, ValueRef, }; /// # SQLite Column Type Affinities /// /// Each column in an SQLite 3 database is assigned one of the following type affinities: /// /// - **TEXT** /// - **NUMERIC** /// - **INTEGER** /// - **REAL** /// - **BLOB** /// /// > **Note:** Historically, the "BLOB" type affinity was called "NONE". However, this term was renamed to avoid confusion with "no affinity". /// /// ## Affinity Descriptions /// /// ### **TEXT** /// - Stores data using the NULL, TEXT, or BLOB storage classes. /// - Numerical data inserted into a column with TEXT affinity is converted into text form before being stored. /// - **Example:** /// ```sql /// CREATE TABLE example (col TEXT); /// INSERT INTO example (col) VALUES (123); -- Stored as '123' (text) /// SELECT typeof(col) FROM example; -- Returns 'text' /// ``` /// /// ### **NUMERIC** /// - Can store values using all five storage classes. /// - Text data is converted to INTEGER or REAL (in that order of preference) if it is a well-formed integer or real literal. /// - If the text represents an integer too large for a 64-bit signed integer, it is converted to REAL. /// - If the text is not a well-formed literal, it is stored as TEXT. /// - Hexadecimal integer literals are stored as TEXT for historical compatibility. /// - Floating-point values that can be exactly represented as integers are converted to integers. /// - **Example:** /// ```sql /// CREATE TABLE example (col NUMERIC); /// INSERT INTO example (col) VALUES ('3.0e+5'); -- Stored as 300000 (integer) /// SELECT typeof(col) FROM example; -- Returns 'integer' /// ``` /// /// ### **INTEGER** /// - Behaves like NUMERIC affinity but differs in `CAST` expressions. /// - **Example:** /// ```sql /// CREATE TABLE example (col INTEGER); /// INSERT INTO example (col) VALUES (4.0); -- Stored as 4 (integer) /// SELECT typeof(col) FROM example; -- Returns 'integer' /// ``` /// /// ### **REAL** /// - Similar to NUMERIC affinity but forces integer values into floating-point representation. /// - **Optimization:** Small floating-point values with no fractional component may be stored as integers on disk to save space. This is invisible at the SQL level. /// - **Example:** /// ```sql /// CREATE TABLE example (col REAL); /// INSERT INTO example (col) VALUES (4); -- Stored as 4.0 (real) /// SELECT typeof(col) FROM example; -- Returns 'real' /// ``` /// /// ### **BLOB** /// - Does not prefer any storage class. /// - No coercion is performed between storage classes. /// - **Example:** /// ```sql /// CREATE TABLE example (col BLOB); /// INSERT INTO example (col) VALUES (x'1234'); -- Stored as a binary blob /// SELECT typeof(col) FROM example; -- Returns 'blob' /// ``` #[derive(Debug, Clone, Copy, PartialEq)] pub enum Affinity { Blob = 0, Text = 1, Numeric = 2, Integer = 3, Real = 4, } pub const SQLITE_AFF_NONE: char = 'A'; // Historically called NONE, but it's the same as BLOB pub const SQLITE_AFF_TEXT: char = 'B'; pub const SQLITE_AFF_NUMERIC: char = 'C'; pub const SQLITE_AFF_INTEGER: char = 'D'; pub const SQLITE_AFF_REAL: char = 'E'; impl Affinity { /// This is meant to be used in opcodes like Eq, which state: /// /// "The SQLITE_AFF_MASK portion of P5 must be an affinity character - SQLITE_AFF_TEXT, SQLITE_AFF_INTEGER, and so forth. /// An attempt is made to coerce both inputs according to this affinity before the comparison is made. /// If the SQLITE_AFF_MASK is 0x00, then numeric affinity is used. /// Note that the affinity conversions are stored back into the input registers P1 and P3. /// So this opcode can cause persistent changes to registers P1 and P3."" pub fn aff_mask(&self) -> char { match self { Affinity::Integer => SQLITE_AFF_INTEGER, Affinity::Text => SQLITE_AFF_TEXT, Affinity::Blob => SQLITE_AFF_NONE, Affinity::Real => SQLITE_AFF_REAL, Affinity::Numeric => SQLITE_AFF_NUMERIC, } } pub fn from_char(char: char) -> Self { match char { SQLITE_AFF_INTEGER => Affinity::Integer, SQLITE_AFF_TEXT => Affinity::Text, SQLITE_AFF_NONE => Affinity::Blob, SQLITE_AFF_REAL => Affinity::Real, SQLITE_AFF_NUMERIC => Affinity::Numeric, _ => Affinity::Blob, } } pub fn as_char_code(&self) -> u8 { self.aff_mask() as u8 } pub fn from_char_code(code: u8) -> Self { Self::from_char(code as char) } pub fn is_numeric(&self) -> bool { matches!(self, Affinity::Integer | Affinity::Real | Affinity::Numeric) } pub fn has_affinity(&self) -> bool { !matches!(self, Affinity::Blob) } /// 3.1. Determination Of Column Affinity /// For tables not declared as STRICT, the affinity of a column is determined by the declared type of the column, according to the following rules in the order shown: /// /// If the declared type contains the string "INT" then it is assigned INTEGER affinity. /// /// If the declared type of the column contains any of the strings "CHAR", "CLOB", or "TEXT" then that column has TEXT affinity. Notice that the type VARCHAR contains the string "CHAR" and is thus assigned TEXT affinity. /// /// If the declared type for a column contains the string "BLOB" or if no type is specified then the column has affinity BLOB. /// /// If the declared type for a column contains any of the strings "REAL", "FLOA", or "DOUB" then the column has REAL affinity. /// /// Otherwise, the affinity is NUMERIC. /// /// Note that the order of the rules for determining column affinity is important. A column whose declared type is "CHARINT" will match both rules 1 and 2 but the first rule takes precedence and so the column affinity will be INTEGER. #[expect(clippy::self_named_constructors)] pub fn affinity(datatype: &str) -> Self { let datatype = datatype.to_ascii_uppercase(); // Rule 1: INT -> INTEGER affinity if datatype.contains("INT") { return Affinity::Integer; } // Rule 2: CHAR/CLOB/TEXT -> TEXT affinity if datatype.contains("CHAR") || datatype.contains("CLOB") || datatype.contains("TEXT") { return Affinity::Text; } // Rule 3: BLOB or empty -> BLOB affinity (historically called NONE) if datatype.contains("BLOB") || datatype.is_empty() { return Affinity::Blob; } // Rule 4: REAL/FLOA/DOUB -> REAL affinity if datatype.contains("REAL") || datatype.contains("FLOA") || datatype.contains("DOUB") { return Affinity::Real; } // Rule 5: Otherwise -> NUMERIC affinity Affinity::Numeric } pub fn convert<'a>(&self, val: &'a impl AsValueRef) -> Option, Value>> { let val = val.as_value_ref(); let is_text = matches!(val, ValueRef::Text(_)); // Apply affinity conversions match self { Affinity::Numeric | Affinity::Integer => is_text .then(|| apply_numeric_affinity(val, false)) .flatten() .map(Either::Left), Affinity::Text => { // TEXT affinity: Convert numeric values to their text representation match val { ValueRef::Numeric(Numeric::Integer(i)) => { Some(Either::Right(Value::Text(i.to_string().into()))) } ValueRef::Numeric(Numeric::Float(f)) => Some(Either::Right(Value::Text( format_float(f64::from(f)).into(), ))), ValueRef::Text(_) => { // If it's already text but looks numeric, ensure it's in canonical text form if is_numeric_value(val) { stringify_register(val).map(Either::Right) } else { None // Already text, no conversion needed } } _ => None, // Blob and Null are not converted } } Affinity::Real => { let mut left = is_text .then(|| apply_numeric_affinity(val, false)) .flatten(); if let ValueRef::Numeric(Numeric::Integer(i)) = left.unwrap_or(val) { left = Some(ValueRef::from_f64(i as f64)); } left.map(Either::Left) } Affinity::Blob => None, // Do nothing for blob affinity. } } /// Return TRUE if the given expression is a constant which would be /// unchanged by OP_Affinity with the affinity given in the second /// argument. /// /// This routine is used to determine if the OP_Affinity operation /// can be omitted. When in doubt return FALSE. A false negative /// is harmless. A false positive, however, can result in the wrong /// answer. /// /// reference https://github.com/sqlite/sqlite/blob/master/src/expr.c#L3000 pub fn expr_needs_no_affinity_change(&self, expr: &Expr) -> bool { if !self.has_affinity() { return true; } // TODO: check for unary minus in the expr, as it may be an additional optimization. // This involves mostly likely walking the expression match expr { Expr::Literal(literal) => match literal { Literal::Numeric(_) => self.is_numeric(), Literal::String(_) => matches!(self, Affinity::Text), Literal::Blob(_) => true, _ => false, }, Expr::Column { is_rowid_alias: true, .. } => self.is_numeric(), _ => false, } } } #[derive(Debug, PartialEq)] pub enum NumericParseResult { NotNumeric, // not a valid number PureInteger, // pure integer (entire string) HasDecimalOrExp, // has decimal point or exponent (entire string) ValidPrefixOnly, // valid prefix but not entire string } #[derive(Debug)] pub enum ParsedNumber { None, Integer(i64), Float(f64), } impl ParsedNumber { fn as_integer(&self) -> Option { match self { ParsedNumber::Integer(i) => Some(*i), _ => None, } } fn as_float(&self) -> Option { match self { ParsedNumber::Float(f) => Some(*f), _ => None, } } } pub fn try_for_float(bytes: &[u8]) -> (NumericParseResult, ParsedNumber) { if bytes.is_empty() { return (NumericParseResult::NotNumeric, ParsedNumber::None); } let mut pos = 0; let len = bytes.len(); while pos < len && is_space(bytes[pos]) { pos += 1; } if pos >= len { return (NumericParseResult::NotNumeric, ParsedNumber::None); } let mut sign = 1i64; if bytes[pos] == b'-' { sign = -1; pos += 1; } else if bytes[pos] == b'+' { pos += 1; } if pos >= len { return (NumericParseResult::NotNumeric, ParsedNumber::None); } let mut significand = 0u64; let mut decimal_adjust = 0i32; let mut has_digits = false; // Parse digits before decimal point while pos < len && bytes[pos].is_ascii_digit() { has_digits = true; let digit = (bytes[pos] - b'0') as u64; if significand <= (u64::MAX - 9) / 10 { significand = significand * 10 + digit; } else { // Skip overflow digits but adjust exponent decimal_adjust += 1; } pos += 1; } let mut has_decimal = false; let mut has_exponent = false; // Check for decimal point if pos < len && bytes[pos] == b'.' { has_decimal = true; pos += 1; // Parse fractional digits while pos < len && bytes[pos].is_ascii_digit() { has_digits = true; let digit = (bytes[pos] - b'0') as u64; if significand <= (u64::MAX - 9) / 10 { significand = significand * 10 + digit; decimal_adjust -= 1; } pos += 1; } } if !has_digits { return (NumericParseResult::NotNumeric, ParsedNumber::None); } // Check for exponent let mut exponent = 0i32; if pos < len && (bytes[pos] == b'e' || bytes[pos] == b'E') { has_exponent = true; pos += 1; if pos >= len { // Incomplete exponent, but we have valid digits before return create_result_from_significand( significand, sign, decimal_adjust, has_decimal, has_exponent, NumericParseResult::ValidPrefixOnly, ); } let mut exp_sign = 1i32; if bytes[pos] == b'-' { exp_sign = -1; pos += 1; } else if bytes[pos] == b'+' { pos += 1; } if pos >= len || !bytes[pos].is_ascii_digit() { // Incomplete exponent return create_result_from_significand( significand, sign, decimal_adjust, has_decimal, false, NumericParseResult::ValidPrefixOnly, ); } // Parse exponent digits while pos < len && bytes[pos].is_ascii_digit() { let digit = (bytes[pos] - b'0') as i32; if exponent < 10000 { exponent = exponent * 10 + digit; } else { exponent = 10000; // Cap at large value } pos += 1; } exponent *= exp_sign; } // Skip trailing whitespace while pos < len && is_space(bytes[pos]) { pos += 1; } // Determine if we consumed the entire string let consumed_all = pos >= len; let final_exponent = decimal_adjust + exponent; let parse_result = if !consumed_all { NumericParseResult::ValidPrefixOnly } else if has_decimal || has_exponent { NumericParseResult::HasDecimalOrExp } else { NumericParseResult::PureInteger }; create_result_from_significand( significand, sign, final_exponent, has_decimal, has_exponent, parse_result, ) } fn create_result_from_significand( significand: u64, sign: i64, exponent: i32, has_decimal: bool, has_exponent: bool, parse_result: NumericParseResult, ) -> (NumericParseResult, ParsedNumber) { if significand == 0 { match parse_result { NumericParseResult::PureInteger => { return (parse_result, ParsedNumber::Integer(0)); } _ => { return (parse_result, ParsedNumber::Float(0.0)); } } } // For pure integers without exponent, try to return as integer if !has_decimal && !has_exponent && exponent == 0 && significand <= i64::MAX as u64 { let signed_val = (significand as i64).wrapping_mul(sign); return (parse_result, ParsedNumber::Integer(signed_val)); } // Convert to float using Dekker double-double arithmetic for precision // This matches SQLite's sqlite3AtoF implementation let mut result = DoubleDouble::from(significand); let mut exp = exponent; match exp.cmp(&0) { std::cmp::Ordering::Greater => { while exp >= 100 { result *= DoubleDouble::E100; exp -= 100; } while exp >= 10 { result *= DoubleDouble::E10; exp -= 10; } while exp >= 1 { result *= DoubleDouble::E1; exp -= 1; } } std::cmp::Ordering::Less => { while exp <= -100 { result *= DoubleDouble::NEG_E100; exp += 100; } while exp <= -10 { result *= DoubleDouble::NEG_E10; exp += 10; } while exp <= -1 { result *= DoubleDouble::NEG_E1; exp += 1; } } std::cmp::Ordering::Equal => {} } let mut final_result: f64 = result.into(); if final_result.is_nan() { final_result = f64::INFINITY; } if sign < 0 { final_result = -final_result; } (parse_result, ParsedNumber::Float(final_result)) } pub fn is_space(byte: u8) -> bool { matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | b'\x0c') } pub(crate) fn real_to_i64(r: f64) -> i64 { if r < -9223372036854774784.0 { i64::MIN } else if r > 9223372036854774784.0 { i64::MAX } else { r as i64 } } fn apply_integer_affinity(val: ValueRef) -> Option { let ValueRef::Numeric(Numeric::Float(nn)) = val else { return None; }; let f: f64 = nn.into(); let ix = real_to_i64(f); // Only convert if round-trip is exact and not at extreme values if f == (ix as f64) && ix > i64::MIN && ix < i64::MAX { Some(ValueRef::Numeric(Numeric::Integer(ix))) } else { None } } /// Try to convert a value into a numeric representation if we can /// do so without loss of information. In other words, if the string /// looks like a number, convert it into a number. If it does not /// look like a number, leave it alone. pub fn apply_numeric_affinity(val: ValueRef, try_for_int: bool) -> Option { let ValueRef::Text(text) = val else { return None; // Only apply to text values }; let text_str = text.as_str(); let (parse_result, parsed_value) = try_for_float(text_str.as_bytes()); // Only convert if we have a complete valid number (not just a prefix) match parse_result { NumericParseResult::NotNumeric | NumericParseResult::ValidPrefixOnly => { None // Leave as text } NumericParseResult::PureInteger => { if let Some(int_val) = parsed_value.as_integer() { Some(ValueRef::Numeric(Numeric::Integer(int_val))) } else if let Some(float_val) = parsed_value.as_float() { let res = ValueRef::from_f64(float_val); if try_for_int { apply_integer_affinity(res) } else { Some(res) } } else { None } } NumericParseResult::HasDecimalOrExp => { if let Some(float_val) = parsed_value.as_float() { // Failed parses can occasionally surface as NaN. Treat those as // non-convertible so we keep the original text value instead of // coercing to NULL during comparison affinity conversion. if float_val.is_nan() { return None; } let res = ValueRef::from_f64(float_val); // If try_for_int is true, try to convert float to int if exact if try_for_int { apply_integer_affinity(res) } else { Some(res) } } else { None } } } } fn is_numeric_value(val: ValueRef) -> bool { matches!(val, ValueRef::Numeric(_)) } fn stringify_register(val: ValueRef) -> Option { match val { ValueRef::Numeric(Numeric::Integer(i)) => Some(Value::build_text(i.to_string())), ValueRef::Numeric(Numeric::Float(f)) => Some(Value::build_text(f64::from(f).to_string())), _ => None, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_apply_numeric_affinity_partial_numbers() { let val = Value::Text("123abc".into()); let res = apply_numeric_affinity(val.as_value_ref(), false); assert!(res.is_none()); let val = Value::Text("-53093015420544-15062897".into()); let res = apply_numeric_affinity(val.as_value_ref(), false); assert!(res.is_none()); let val = Value::Text("123.45xyz".into()); let res = apply_numeric_affinity(val.as_value_ref(), false); assert!(res.is_none()); } #[test] fn test_apply_numeric_affinity_complete_numbers() { let val = Value::Text("123".into()); let res = apply_numeric_affinity(val.as_value_ref(), false); assert_eq!(res, Some(ValueRef::Numeric(Numeric::Integer(123)))); let val = Value::Text("123.45".into()); let res = apply_numeric_affinity(val.as_value_ref(), false); assert_eq!(res, Some(ValueRef::from_f64(123.45))); let val = Value::Text(" -456 ".into()); let res = apply_numeric_affinity(val.as_value_ref(), false); assert_eq!(res, Some(ValueRef::Numeric(Numeric::Integer(-456)))); let val = Value::Text("0".into()); let res = apply_numeric_affinity(val.as_value_ref(), false); assert_eq!(res, Some(ValueRef::Numeric(Numeric::Integer(0)))); } #[test] fn test_apply_numeric_affinity_extreme_exponent_gives_infinity() { let val = Value::Text("3139353734372E383932303939343135".into()); let res = apply_numeric_affinity(val.as_value_ref(), false); assert!(res.is_some()); match res.unwrap() { ValueRef::Numeric(Numeric::Float(f)) => assert!(f64::from(f).is_infinite()), other => panic!("expected Float, got {other:?}"), } } #[test] fn test_try_for_float_precision() { // This test verifies that try_for_float uses high-precision arithmetic // to avoid rounding errors when computing significand * 10^exponent. // Naive f64 multiplication accumulates errors; Dekker double-double fixes this. let (_, parsed) = try_for_float(b"12345678901234567e-5"); let expected: f64 = "12345678901234567e-5".parse().unwrap(); assert_eq!( parsed.as_float().unwrap().to_bits(), expected.to_bits(), "try_for_float precision mismatch: got {}, expected {expected}", parsed.as_float().unwrap(), ); } } ================================================ FILE: core/vdbe/array.rs ================================================ use std::collections::BTreeSet; use std::fmt::Write; use crate::numeric::Numeric; use crate::types::{ImmutableRecord, Value, ValueIterator}; use crate::Result; /// Extract values from a record-format array blob. /// Returns Err if the blob is not a valid record. /// Uses zero-copy iteration over the blob bytes — no Vec allocation. pub(crate) fn array_values_from_blob(blob: &[u8]) -> Result> { let iter = ValueIterator::new(blob)?; let mut values = Vec::with_capacity(iter.size_hint().0); for value in iter { values.push(value?.to_owned()); } Ok(values) } /// Extract elements from any Value that represents an array. /// Handles record blobs, JSON text input, and NULL (empty array). /// Returns None if the value cannot be interpreted as an array. pub(crate) fn array_values_from_any(arr: &Value) -> Option> { match arr { Value::Blob(blob) => array_values_from_blob(blob).ok(), Value::Text(text) => parse_text_array(text.as_str()), Value::Null => Some(Vec::new()), _ => None, } } /// Parse a text array literal in PG format `{1, hello, NULL}` into a Vec. /// Handles integers, floats, strings (quoted and unquoted), and NULL. pub(crate) fn parse_text_array(text: &str) -> Option> { let text = text.trim(); if text.starts_with('{') && text.ends_with('}') { return parse_pg_text_array(text); } None } /// Parse a PG-style text array like `{1, hello, NULL, 3.14}` into a Vec. /// Unquoted `NULL` (case-insensitive) → Value::Null. /// Quoted strings use `"..."` with `\"` and `\\` escapes. /// Unquoted tokens are parsed as integer, then float, then text. fn parse_pg_text_array(text: &str) -> Option> { let inner = text[1..text.len() - 1].trim(); if inner.is_empty() { return Some(Vec::new()); } let bytes = inner.as_bytes(); let mut pos = 0; let mut elements = Vec::new(); loop { // Skip whitespace while pos < bytes.len() && bytes[pos].is_ascii_whitespace() { pos += 1; } if pos >= bytes.len() { break; } if bytes[pos] == b'"' { // Quoted string pos += 1; let mut s = String::new(); loop { if pos >= bytes.len() { return None; } match bytes[pos] { b'\\' => { pos += 1; if pos >= bytes.len() { return None; } match bytes[pos] { b'n' => s.push('\n'), b't' => s.push('\t'), b'r' => s.push('\r'), other => s.push(other as char), } } b'"' => { pos += 1; break; } _ => { let remaining = &inner[pos..]; let ch = remaining.chars().next().unwrap_or('\u{FFFD}'); s.push(ch); pos += ch.len_utf8(); continue; } } pos += 1; } elements.push(Value::build_text(s)); } else { // Unquoted token: read until comma, whitespace, or end let start = pos; while pos < bytes.len() && bytes[pos] != b',' && !bytes[pos].is_ascii_whitespace() { pos += 1; } let token = &inner[start..pos]; if token.eq_ignore_ascii_case("null") { elements.push(Value::Null); } else if let Ok(i) = token.parse::() { elements.push(Value::from_i64(i)); } else if let Ok(f) = token.parse::() { if !f.is_finite() { return None; // reject Infinity and NaN } elements.push(Value::from_f64(f)); } else { elements.push(Value::build_text(token.to_string())); } } // Skip whitespace while pos < bytes.len() && bytes[pos].is_ascii_whitespace() { pos += 1; } if pos >= bytes.len() { break; } if bytes[pos] == b',' { pos += 1; // Reject trailing commas: after consuming ',' there must be another element let mut peek = pos; while peek < bytes.len() && bytes[peek].is_ascii_whitespace() { peek += 1; } if peek >= bytes.len() { return None; // trailing comma } } else if pos < bytes.len() { return None; } } Some(elements) } /// Pack values into a record-format array blob. pub(crate) fn values_to_record_blob(values: &[Value]) -> Value { Value::Blob(ImmutableRecord::from_values(values, values.len()).into_payload()) } /// Serialize a record-format array blob to PostgreSQL text representation. /// Uses `{...}` delimiters and PG quoting rules: /// - NULL elements → uppercase `NULL` (unquoted) /// - Text elements → double-quoted if they contain special chars, unquoted otherwise /// - Numeric elements → unquoted pub(crate) fn serialize_array_from_blob(blob: &[u8]) -> Result { let iter = ValueIterator::new(blob)?; let mut result = String::from("{"); let mut first = true; for vref in iter { let vref = vref?; if !first { result.push(','); } first = false; write_value_ref_pg(&mut result, &vref); } result.push('}'); Ok(result) } fn write_value_ref_pg(result: &mut String, val: &crate::ValueRef<'_>) { match val { crate::ValueRef::Null => result.push_str("NULL"), crate::ValueRef::Numeric(Numeric::Integer(n)) => { let _ = write!(result, "{n}"); } crate::ValueRef::Numeric(Numeric::Float(f)) => { let fval: f64 = (*f).into(); // Normalize -0.0 to 0.0 for display let fval = if fval == 0.0 { 0.0 } else { fval }; if fval.fract() == 0.0 && fval.is_finite() { let _ = write!(result, "{fval:.1}"); } else { let _ = write!(result, "{fval}"); } } crate::ValueRef::Text(t) => { write_pg_text_element(result, t.as_str()); } crate::ValueRef::Blob(b) => { result.push_str("\"X'"); for byte in *b { let _ = write!(result, "{byte:02X}"); } result.push_str("'\""); } } } /// Write a text element in PG array format. /// Simple values are unquoted; values with special chars are double-quoted. fn write_pg_text_element(result: &mut String, s: &str) { let needs_quoting = s.is_empty() || s.eq_ignore_ascii_case("null") || s.contains(|c: char| { c == ',' || c == '{' || c == '}' || c == '"' || c == '\\' || c.is_whitespace() || c.is_control() }); if needs_quoting { result.push('"'); for ch in s.chars() { match ch { '"' => result.push_str("\\\""), '\\' => result.push_str("\\\\"), '\n' => result.push_str("\\n"), '\r' => result.push_str("\\r"), '\t' => result.push_str("\\t"), c if c.is_control() => { let _ = write!(result, "\\u{:04x}", c as u32); } c => result.push(c), } } result.push('"'); } else { result.push_str(s); } } /// Compute the number of elements in an array value. Shared by /// op_array_length (instruction) and ScalarFunc::ArrayLength (function). /// Returns None for NULL or non-blob input (maps to SQL NULL). pub(crate) fn compute_array_length(val: &Value) -> Option { match val { Value::Null => None, Value::Blob(b) => match ValueIterator::new(b) { Ok(iter) => Some(iter.count() as i64), Err(_) => None, }, Value::Text(t) => parse_text_array(t.as_str()).map(|v| v.len() as i64), _ => None, } } pub(crate) fn exec_array_append(arr: &Value, elem: &Value) -> Value { let Some(mut elements) = array_values_from_any(arr) else { return Value::Null; }; elements.push(elem.clone()); values_to_record_blob(&elements) } pub(crate) fn exec_array_prepend(arr: &Value, elem: &Value) -> Value { let Some(elements) = array_values_from_any(arr) else { return Value::Null; }; // Build new vec with elem first — avoids O(n) shift from Vec::insert(0, ...) let mut result = Vec::with_capacity(elements.len() + 1); result.push(elem.clone()); result.extend(elements); values_to_record_blob(&result) } pub(crate) fn exec_array_cat(a: &Value, b: &Value) -> Value { if matches!(a, Value::Null) || matches!(b, Value::Null) { return Value::Null; } let Some(mut elems_a) = array_values_from_any(a) else { return Value::Null; }; let Some(elems_b) = array_values_from_any(b) else { return Value::Null; }; elems_a.extend(elems_b); values_to_record_blob(&elems_a) } pub(crate) fn exec_array_remove(arr: &Value, target: &Value) -> Value { if matches!(arr, Value::Null) { return Value::Null; } let Some(elements) = array_values_from_any(arr) else { return Value::Null; }; let result: Vec = elements.into_iter().filter(|e| e != target).collect(); values_to_record_blob(&result) } pub(crate) fn exec_array_contains(arr: &Value, target: &Value) -> Value { if matches!(arr, Value::Null) { return Value::Null; } if let Value::Blob(blob) = arr { return array_find_streaming(blob, |vref| vref == *target) .map(|_| Value::from_i64(1)) .unwrap_or_else(|| Value::from_i64(0)); } let Some(elements) = array_values_from_any(arr) else { return Value::Null; }; let found = elements.iter().any(|e| e == target); Value::from_i64(found as i64) } pub(crate) fn exec_array_position(arr: &Value, target: &Value) -> Value { if matches!(arr, Value::Null) { return Value::Null; } if let Value::Blob(blob) = arr { return array_find_streaming(blob, |vref| vref == *target) .map(|i| Value::from_i64(i as i64 + 1)) // 1-based (PG convention) .unwrap_or(Value::Null); } let Some(elements) = array_values_from_any(arr) else { return Value::Null; }; for (i, elem) in elements.iter().enumerate() { if elem == target { return Value::from_i64(i as i64 + 1); // 1-based (PG convention) } } Value::Null } /// Stream through a record-format blob, calling `predicate` on each element. /// Returns Some(index) for the first element where the predicate returns true, /// or None if no match or on error. fn array_find_streaming( blob: &[u8], predicate: impl Fn(crate::ValueRef<'_>) -> bool, ) -> Option { let iter = ValueIterator::new(blob).ok()?; for (i, vref) in iter.enumerate() { let vref = vref.ok()?; if predicate(vref) { return Some(i); } } None } pub(crate) fn exec_array_slice(arr: &Value, start: &Value, end: &Value) -> Value { if matches!(arr, Value::Null) { return Value::Null; } let Some(elements) = array_values_from_any(arr) else { return Value::Null; }; // PG convention: 1-based inclusive bounds let start_idx = match start { Value::Numeric(Numeric::Integer(i)) if *i >= 1 => (*i - 1) as usize, _ => 0, }; let end_idx = match end { Value::Numeric(Numeric::Integer(i)) if *i >= 1 => *i as usize, // inclusive → exclusive _ => 0, }; let end = end_idx.min(elements.len()); let start = start_idx.min(end); values_to_record_blob(&elements[start..end]) } /// Split a string into an array using a delimiter. /// string_to_array(text, delimiter [, null_string]) /// If text is NULL, returns NULL. /// If delimiter is NULL, splits into individual characters (PostgreSQL behavior). /// If null_string is provided, any element matching it becomes NULL. pub(crate) fn exec_string_to_array( text: &Value, delimiter: &Value, null_str: Option<&Value>, ) -> Value { let text_str = match text { Value::Text(t) => t.as_str().to_string(), Value::Null => return Value::Null, other => other.to_string(), }; let null_match: Option = match null_str { Some(Value::Text(t)) => Some(t.as_str().to_string()), Some(Value::Null) | None => None, Some(other) => Some(other.to_string()), }; // NULL delimiter: split into individual characters (PostgreSQL behavior) if matches!(delimiter, Value::Null) { let values: Vec = text_str .chars() .map(|c| { let s = c.to_string(); if let Some(ref nm) = null_match { if s == *nm { return Value::Null; } } Value::build_text(s) }) .collect(); return values_to_record_blob(&values); } let delim_str = match delimiter { Value::Text(d) => d.as_str().to_string(), other => other.to_string(), }; let parts: Vec<&str> = if delim_str.is_empty() { // Empty delimiter: return single-element array with the whole string vec![&text_str] } else { text_str.split(&delim_str).collect() }; let values: Vec = parts .into_iter() .map(|p| { if let Some(ref nm) = null_match { if p == nm.as_str() { return Value::Null; } } Value::build_text(p.to_string()) }) .collect(); values_to_record_blob(&values) } /// Join array elements into a string with a delimiter. /// array_to_string(array, delimiter [, null_string]) /// NULL elements are omitted unless null_string is provided. pub(crate) fn exec_array_to_string( arr: &Value, delimiter: &Value, null_str: Option<&Value>, ) -> Value { if matches!(arr, Value::Null) { return Value::Null; } let delim = match delimiter { Value::Text(t) => t.as_str().to_string(), Value::Null => return Value::Null, other => other.to_string(), }; let null_replacement: Option = match null_str { Some(Value::Text(t)) => Some(t.as_str().to_string()), Some(Value::Null) | None => None, Some(other) => Some(other.to_string()), }; // Fast path: stream from blob without materializing Vec if let Value::Blob(blob) = arr { if let Ok(iter) = ValueIterator::new(blob) { let mut result = String::new(); let mut first = true; for vref in iter { let Ok(vref) = vref else { return Value::Null; }; let part = match &vref { crate::ValueRef::Null => { if let Some(ref replacement) = null_replacement { replacement.clone() } else { continue; } } crate::ValueRef::Text(t) => t.as_str().to_string(), other => format!("{other}"), }; if !first { result.push_str(&delim); } result.push_str(&part); first = false; } return Value::build_text(result); } } let Some(elements) = array_values_from_any(arr) else { return Value::Null; }; let mut result = String::new(); let mut first = true; for elem in &elements { let part = match elem { Value::Null => { if let Some(ref replacement) = null_replacement { replacement.clone() } else { continue; } } Value::Text(t) => t.as_str().to_string(), other => other.to_string(), }; if !first { result.push_str(&delim); } result.push_str(&part); first = false; } Value::build_text(result) } /// Check if two arrays have any elements in common. /// Returns 1 if they share at least one element, 0 otherwise. /// NULL if either input is not a valid array. pub(crate) fn exec_array_overlap(a: &Value, b: &Value) -> Value { if matches!(a, Value::Null) || matches!(b, Value::Null) { return Value::Null; } let Some(elems_a) = array_values_from_any(a) else { return Value::Null; }; let Some(elems_b) = array_values_from_any(b) else { return Value::Null; }; // O(n log n + m log n) via BTreeSet instead of O(n*m) let set: BTreeSet<&Value> = elems_a.iter().collect(); let found = elems_b.iter().any(|eb| set.contains(eb)); Value::from_i64(found as i64) } /// Check if array `a` contains all elements of array `b` (@> operator). /// Returns 1 if every element in `b` appears in `a`, 0 otherwise. /// NULL if either input is not a valid array. pub(crate) fn exec_array_contains_all(a: &Value, b: &Value) -> Value { if matches!(a, Value::Null) || matches!(b, Value::Null) { return Value::Null; } let Some(elems_a) = array_values_from_any(a) else { return Value::Null; }; let Some(elems_b) = array_values_from_any(b) else { return Value::Null; }; // O(n log n + m log n) via BTreeSet instead of O(n*m) let set: BTreeSet<&Value> = elems_a.iter().collect(); let all_found = elems_b.iter().all(|eb| set.contains(eb)); Value::from_i64(all_found as i64) } /// Collect values from contiguous registers into a record-format array blob. pub(crate) fn make_array_from_registers( registers: &[super::Register], start_reg: usize, count: usize, ) -> Value { let values: Vec = (0..count) .map(|i| registers[start_reg + i].get_value().clone()) .collect(); let record = ImmutableRecord::from_values(&values, count); Value::Blob(record.into_payload()) } /// Element-wise comparison of two record-format array blobs. /// Compares corresponding elements using ValueRef ordering. /// If all common elements are equal, the shorter array is less. /// Returns Err if either blob is not a valid record. pub(crate) fn compare_arrays(a: &[u8], b: &[u8]) -> Result { let iter_a = ValueIterator::new(a)?; let iter_b = ValueIterator::new(b)?; let mut count_a = 0usize; let mut count_b = 0usize; for (va, vb) in iter_a.zip(iter_b) { count_a += 1; count_b += 1; let (va, vb) = (va?, vb?); let ord = va.cmp(&vb); if !ord.is_eq() { return Ok(ord); } } // Count remaining elements in the longer array let len_a = count_a + ValueIterator::new(a)?.skip(count_a).count(); let len_b = count_b + ValueIterator::new(b)?.skip(count_b).count(); Ok(len_a.cmp(&len_b)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_text_array_multibyte_utf8() { let input = r#"{"café","naïve","über"}"#; let result = parse_text_array(input).unwrap(); assert_eq!(result.len(), 3); assert_eq!(result[0], Value::build_text("café")); assert_eq!(result[1], Value::build_text("naïve")); assert_eq!(result[2], Value::build_text("über")); } #[test] fn test_parse_text_array_emoji() { let input = r#"{"hello 🌍","test 🚀"}"#; let result = parse_text_array(input).unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0], Value::build_text("hello 🌍")); assert_eq!(result[1], Value::build_text("test 🚀")); } #[test] fn test_parse_text_array_cjk() { let input = r#"{"你好","世界"}"#; let result = parse_text_array(input).unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0], Value::build_text("你好")); assert_eq!(result[1], Value::build_text("世界")); } #[test] fn test_compute_array_length_null_returns_none() { assert_eq!(compute_array_length(&Value::Null), None); } #[test] fn test_compute_array_length_valid_array() { let blob = values_to_record_blob(&[Value::from_i64(1), Value::from_i64(2)]); assert_eq!(compute_array_length(&blob), Some(2)); } #[test] fn test_compute_array_length_non_blob_returns_none() { assert_eq!(compute_array_length(&Value::from_i64(42)), None,); } #[test] fn test_array_remove_all_occurrences() { let arr = values_to_record_blob(&[ Value::from_i64(1), Value::from_i64(2), Value::from_i64(3), Value::from_i64(2), Value::from_i64(1), ]); let result = exec_array_remove(&arr, &Value::from_i64(2)); let Value::Blob(blob) = &result else { panic!("Expected Blob"); }; let elements = array_values_from_blob(blob).unwrap(); assert_eq!(elements.len(), 3); assert_eq!(elements[0], Value::from_i64(1)); assert_eq!(elements[1], Value::from_i64(3)); assert_eq!(elements[2], Value::from_i64(1)); } #[test] fn test_array_contains_null_array_returns_null() { assert_eq!( exec_array_contains(&Value::Null, &Value::from_i64(1)), Value::Null, ); } #[test] fn test_array_position_null_array_returns_null() { assert_eq!( exec_array_position(&Value::Null, &Value::from_i64(1)), Value::Null, ); } #[test] fn test_compute_array_length_invalid_blob_returns_none() { // A random blob that is not a valid record should return None let invalid = Value::Blob(vec![0xFF, 0xFE, 0xFD]); assert_eq!(compute_array_length(&invalid), None); } #[test] fn test_parse_text_array_rejects_json_format() { // JSON [1,2,3] format is no longer accepted — only PG {1,2,3} assert!(parse_text_array("[1,2,3]").is_none()); assert!(parse_text_array(r#"["hello"]"#).is_none()); } #[test] fn test_parse_text_array_rejects_trailing_comma() { assert!(parse_text_array("{1,2,}").is_none()); assert!(parse_text_array("{1, 2, }").is_none()); } #[test] fn test_parse_text_array_rejects_infinity() { assert!(parse_text_array("{1e309}").is_none()); assert!(parse_text_array("{-1e309}").is_none()); } #[test] fn test_string_to_array_null_delimiter_splits_chars() { let result = exec_string_to_array(&Value::build_text("hello"), &Value::Null, None); let Value::Blob(blob) = &result else { panic!("Expected Blob, got {result:?}"); }; let elements = array_values_from_blob(blob).unwrap(); assert_eq!(elements.len(), 5); assert_eq!(elements[0], Value::build_text("h")); assert_eq!(elements[1], Value::build_text("e")); assert_eq!(elements[4], Value::build_text("o")); } #[test] fn test_exec_array_contains_streaming() { let arr = values_to_record_blob(&[ Value::from_i64(10), Value::from_i64(20), Value::from_i64(30), ]); assert_eq!( exec_array_contains(&arr, &Value::from_i64(20)), Value::from_i64(1) ); assert_eq!( exec_array_contains(&arr, &Value::from_i64(99)), Value::from_i64(0) ); } #[test] fn test_exec_array_position_streaming() { let arr = values_to_record_blob(&[ Value::from_i64(10), Value::from_i64(20), Value::from_i64(30), ]); // 1-based: element 20 is at position 2 assert_eq!( exec_array_position(&arr, &Value::from_i64(20)), Value::from_i64(2) ); assert_eq!(exec_array_position(&arr, &Value::from_i64(99)), Value::Null); } #[test] fn test_dc1_negative_index_preserves_array() { let arr = values_to_record_blob(&[ Value::from_i64(10), Value::from_i64(20), Value::from_i64(30), ]); // array_find_streaming with impossible predicate should return None let Value::Blob(blob) = &arr else { panic!("Expected Blob"); }; assert!(array_find_streaming(blob, |_| false).is_none()); } #[test] fn test_dc4_array_remove_null_returns_null() { assert_eq!( exec_array_remove(&Value::Null, &Value::from_i64(1)), Value::Null ); } #[test] fn test_dc4_array_slice_null_returns_null() { assert_eq!( exec_array_slice(&Value::Null, &Value::from_i64(0), &Value::from_i64(2)), Value::Null, ); } #[test] fn test_dc4_array_cat_null_returns_null() { assert_eq!(exec_array_cat(&Value::Null, &Value::Null), Value::Null); assert_eq!( exec_array_cat(&Value::Null, &Value::from_i64(1)), Value::Null, ); } #[test] fn test_serialize_array_from_blob() { let arr = values_to_record_blob(&[Value::from_i64(1), Value::build_text("hello"), Value::Null]); let Value::Blob(blob) = &arr else { panic!("Expected Blob"); }; let text = serialize_array_from_blob(blob).unwrap(); assert_eq!(text, "{1,hello,NULL}"); } #[test] fn test_make_array_from_registers() { use super::super::Register; let registers = vec![ Register::Value(Value::from_i64(1)), Register::Value(Value::build_text("two")), Register::Value(Value::from_i64(3)), ]; let result = make_array_from_registers(®isters, 0, 3); let Value::Blob(blob) = &result else { panic!("Expected Blob"); }; let elements = array_values_from_blob(blob).unwrap(); assert_eq!(elements.len(), 3); assert_eq!(elements[0], Value::from_i64(1)); assert_eq!(elements[1], Value::build_text("two")); assert_eq!(elements[2], Value::from_i64(3)); } } ================================================ FILE: core/vdbe/bloom_filter.rs ================================================ use fastbloom::BloomFilter as BloomFilterInner; use std::fmt; use std::hash::{Hash, Hasher}; use crate::numeric::Numeric; use crate::types::{Value, ValueRef}; /// Default number of expected items for bloom filter sizing. /// This is used when the expected count is not known ahead of time. const DEFAULT_EXPECTED_ITEMS: u32 = 1024; /// Default false positive rate (1%). const DEFAULT_FALSE_POSITIVE_RATE: f32 = 0.01; /// A bloom filter for fast probabilistic set membership testing. /// /// Each bloom filter is associated with a cursor or operation that builds it /// during ephemeral index/hash table construction. pub struct BloomFilter { inner: BloomFilterInner, /// Number of items inserted into the filter count: usize, } impl fmt::Debug for BloomFilter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("BloomFilter") .field("count", &self.count) .finish_non_exhaustive() } } impl BloomFilter { /// Creates a new bloom filter with default parameters. pub fn new() -> Self { Self::with_capacity(DEFAULT_EXPECTED_ITEMS, DEFAULT_FALSE_POSITIVE_RATE) } /// Creates a new bloom filter with the specified expected item count and false positive rate. pub fn with_capacity(expected_items: u32, false_positive_rate: f32) -> Self { Self { inner: BloomFilterInner::with_false_pos(false_positive_rate as f64) .expected_items(expected_items as usize), count: 0, } } #[inline] /// Inserts an i64 value into the bloom filter. pub fn insert_i64(&mut self, value: i64) { self.inner.insert(&value); self.count += 1; } #[inline] /// Checks if an i64 value might be in the bloom filter. pub fn contains_i64(&self, value: i64) -> bool { self.inner.contains(&value) } #[inline] /// Inserts a byte slice into the bloom filter. pub fn insert_bytes(&mut self, value: &[u8]) { self.inner.insert(&value); self.count += 1; } #[inline] /// Checks if a byte slice might be in the bloom filter. pub fn contains_bytes(&self, value: &[u8]) -> bool { self.inner.contains(&value) } /// Inserts a Value into the bloom filter. /// Safety NOTE: does not accept NULL values. pub fn insert_value(&mut self, value: &Value) { if !matches!(value, Value::Null) { let mut hasher = rapidhash::fast::RapidHasher::default(); hash_value(&mut hasher, &value.as_ref()); let hash = hasher.finish(); self.inner.insert(&hash); } self.count += 1; } /// Checks if a Value might be in the bloom filter. pub fn contains_value(&self, value: &Value) -> bool { if matches!(value, Value::Null) { return false; } let mut hasher = rapidhash::fast::RapidHasher::default(); hash_value(&mut hasher, &value.as_ref()); let hash = hasher.finish(); self.inner.contains(&hash) } /// Inserts multiple owned Values as a composite key into the bloom filter. /// This is because bloom filters only support a single value insertion, so to handle multi /// join-key situations we hash the composite key into a single u64 and then insert that pub fn insert_values(&mut self, values: &[&Value]) { let mut hasher = rapidhash::fast::RapidHasher::default(); for value in values { hash_value(&mut hasher, &value.as_ref()); } let hash = hasher.finish(); self.inner.insert(&hash); self.count += 1; } /// Checks if multiple owned Values as a composite key might be in the bloom filter. pub fn contains_values(&self, values: &[&Value]) -> bool { let mut hasher = rapidhash::fast::RapidHasher::default(); for value in values { if matches!(value, Value::Null) { // if any value is NULL, we can never have a match return false; } hash_value(&mut hasher, &value.as_ref()); } let hash = hasher.finish(); self.inner.contains(&hash) } pub fn count(&self) -> usize { self.count } pub fn is_empty(&self) -> bool { self.count == 0 } pub fn clear(&mut self) { self.inner.clear(); self.count = 0; } } impl Default for BloomFilter { fn default() -> Self { Self::new() } } /// Hashes an owned Value into the provided hasher. fn hash_value(hasher: &mut H, value: &ValueRef) { match value { // do nothing for NULLs as we will always return false for set membership ValueRef::Null => {} ValueRef::Numeric(Numeric::Integer(i)) => { // Hash integers in the same bucket as numerically equivalent REALs so // bloom-filter membership can never return a false-negative for e.g. 10 vs 10.0. let f = *i as f64; if (f as i64) == *i && f.is_finite() { hash_numeric(hasher, f); } else { // Fallback to the integer domain when the float representation would lose precision. 1u8.hash(hasher); i.hash(hasher); } } ValueRef::Numeric(Numeric::Float(f)) => { hash_numeric(hasher, f64::from(*f)); } ValueRef::Text(s) => { 3u8.hash(hasher); s.as_str().hash(hasher); } ValueRef::Blob(b) => { 4u8.hash(hasher); b.hash(hasher); } } } /// Hashes numeric values (both INTEGER and REAL) into the same domain to mirror SQLite's /// numeric comparison semantics (e.g. 10 == 10.0, -0.0 == 0.0). fn hash_numeric(hasher: &mut H, f: f64) { const NUMERIC_TAG: u8 = 2; let bits = normalized_f64_bits(f); NUMERIC_TAG.hash(hasher); bits.hash(hasher); } /// Normalize signed zero so 0.0 and -0.0 hash the same. #[inline] fn normalized_f64_bits(f: f64) -> u64 { if f == 0.0 { 0.0f64.to_bits() } else { f.to_bits() } } #[cfg(test)] mod bloomtests { use super::*; use crate::types::Text; #[test] fn test_bloom_filter_i64() { let mut bf = BloomFilter::new(); bf.insert_i64(1); bf.insert_i64(2); bf.insert_i64(3); // These should definitely be found (no false negatives) assert!(bf.contains_i64(1)); assert!(bf.contains_i64(2)); assert!(bf.contains_i64(3)); } #[test] fn test_bloom_filter_values() { let mut bf = BloomFilter::new(); let int_val = Value::from_i64(42); let text_val = Value::Text(Text::new("hello".to_string())); let null_val = Value::Null; bf.insert_value(&int_val); bf.insert_value(&text_val); bf.insert_value(&null_val); assert!(bf.contains_value(&int_val)); assert!(bf.contains_value(&text_val)); // NULLs are not hashed into the filter, so membership should be false. assert!(!bf.contains_value(&null_val)); } #[test] fn test_bloom_filter_false_positive_rate() { // Test that false positive rate is roughly as expected let mut bf = BloomFilter::with_capacity(1000, 0.01); // Insert 1000 values for i in 0..1000 { bf.insert_i64(i); } // Check false positive rate on non-inserted values let mut false_positives = 0; let test_count = 10000; for i in 1000..(1000 + test_count) { if bf.contains_i64(i) { false_positives += 1; } } // False positive rate should be around 1% (allow some variance) let rate = false_positives as f64 / test_count as f64; assert!(rate < 0.05, "False positive rate {rate} is too high"); } #[test] fn test_bloom_filter_numeric_equivalence() { let mut bf = BloomFilter::new(); // Zero variants should all be found regardless of sign or int/float representation let zero_float = Value::from_f64(0.0); let zero_neg_float = Value::from_f64(-0.0); let zero_int = Value::from_i64(0); bf.insert_value(&zero_float); assert!(bf.contains_value(&zero_float)); assert!(bf.contains_value(&zero_neg_float)); assert!(bf.contains_value(&zero_int)); // Integer/float representations of the same numeric value should match let ten_int = Value::from_i64(10); let ten_float = Value::from_f64(10.0); bf.insert_value(&ten_int); assert!(bf.contains_value(&ten_int)); assert!(bf.contains_value(&ten_float)); let neg_ten_float = Value::from_f64(-10.0); let neg_ten_int = Value::from_i64(-10); bf.insert_value(&neg_ten_float); assert!(bf.contains_value(&neg_ten_float)); assert!(bf.contains_value(&neg_ten_int)); } } ================================================ FILE: core/vdbe/builder.rs ================================================ use crate::{turso_assert, turso_assert_eq, turso_debug_assert}; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use tracing::{instrument, Level}; use turso_parser::ast::{self, ResolveType, SortOrder, TableInternalId}; use crate::{ index_method::IndexMethodAttachment, parameters::Parameters, schema::{BTreeTable, Index, PseudoCursorType, Schema, Table, Trigger}, translate::{ collate::CollationSeq, emitter::{MaterializedColumnRef, TransactionMode}, plan::{ResultSetColumn, TableReferences}, }, vdbe::affinity::Affinity, Arc, CaptureDataChangesInfo, Connection, VirtualTable, }; // Keep distinct hash-table ids far from table internal ids to avoid collisions. const HASH_TABLE_ID_BASE: usize = 1 << 30; #[derive(Default)] pub struct TableRefIdCounter { next_free: ast::TableInternalId, } impl TableRefIdCounter { pub fn new() -> Self { Self { next_free: TableInternalId::default(), } } #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> ast::TableInternalId { let id = self.next_free; self.next_free += 1; id } } use super::{ BranchOffset, CursorID, Insn, InsnReference, JumpTarget, PrepareContext, PreparedProgram, Program, }; /// A key that uniquely identifies a cursor. /// The key is a pair of table reference id and index. /// The index is only provided when the cursor is an index cursor. #[derive(Debug, Clone)] pub struct CursorKey { /// The table reference that the cursor is associated with. /// We cannot use e.g. the table query identifier (e.g. 'users' or 'u') /// because it might be ambiguous, e.g. this silly example: /// `SELECT * FROM t WHERE EXISTS (SELECT * from t)` <-- two different cursors, which 't' should we use as key? /// TableInternalIds are unique within a program, since there is one id per table reference. pub table_reference_id: TableInternalId, /// The index, in case of an index cursor. /// The combination of table internal id and index is enough to disambiguate. pub index: Option>, /// Whether this cursor is an special case build cursor. pub is_build: bool, } impl CursorKey { pub fn table(table_reference_id: TableInternalId) -> Self { Self { table_reference_id, index: None, is_build: false, } } pub fn index(table_reference_id: TableInternalId, index: Arc) -> Self { Self { table_reference_id, index: Some(index), is_build: false, } } /// Create a cursor key for hash join build operations. /// This creates a separate cursor from the regular table cursor. pub fn hash_build(table_reference_id: TableInternalId) -> Self { Self { table_reference_id, index: None, is_build: true, } } pub fn equals(&self, other: &CursorKey) -> bool { if self.table_reference_id != other.table_reference_id { return false; } if self.is_build != other.is_build { return false; } match (self.index.as_ref(), other.index.as_ref()) { (Some(self_index), Some(other_index)) => self_index.name == other_index.name, (None, None) => true, _ => false, } } } #[allow(dead_code)] pub struct ProgramBuilder { pub table_reference_counter: TableRefIdCounter, next_free_register: usize, next_free_cursor_id: usize, next_hash_table_id: usize, /// Instruction, the function to execute it with, and its original index in the vector. pub insns: Vec<(Insn, usize)>, /// A span of instructions from (offset_start_inclusive, offset_end_exclusive), /// that are deemed to be compile-time constant and can be hoisted out of loops /// so that they get evaluated only once at the start of the program. pub constant_spans: Vec<(usize, usize)>, /// Cursors that are referenced by the program. Indexed by [CursorKey]. /// Certain types of cursors do not need a [CursorKey] (e.g. temp tables, sorter), /// because they never need to use [ProgramBuilder::resolve_cursor_id] to find it /// again. Hence, the key is optional. pub cursor_ref: Vec<(Option, CursorType)>, /// A vector where index=label number, value=resolved offset. Resolved in build(). label_to_resolved_offset: Vec>, // Bitmask of cursors that have emitted a SeekRowid instruction. seekrowid_emitted_bitmask: u64, // map of instruction index to manual comment (used in EXPLAIN only) comments: Vec<(InsnReference, &'static str)>, pub parameters: Parameters, pub result_columns: Vec, pub table_references: TableReferences, /// Curr collation sequence. Bool indicates whether it was set by a COLLATE expr collation: Option<(CollationSeq, bool)>, /// Current parsing nesting level nested_level: usize, init_label: BranchOffset, start_offset: BranchOffset, capture_data_changes_info: Option, // TODO: when we support multiple dbs, this should be a write mask to track which DBs need to be written txn_mode: TransactionMode, /// Set of database IDs that need write transactions (for attached databases). write_databases: HashSet, /// Set of attached database IDs that need read transactions. read_databases: HashSet, /// Schema cookies for attached databases at prepare time. write_database_cookies: HashMap, /// Schema cookies for attached databases opened for reading. read_database_cookies: HashMap, rollback: bool, /// The mode in which the query is being executed. query_mode: QueryMode, /// Current parent explain address, if any. current_parent_explain_idx: Option, pub(crate) reg_result_cols_start: Option, /// Mirrors SQLite's isMultiWrite: true if the statement may modify/insert multiple rows. /// If a non-autocommit transaction can modify multiple rows, statement subjournaling is always /// required for proper cleanup on abort. If only one row can be modified, then journaling is not /// necessary because on abort there is nothing to clean up. /// Defaults to true for safety; specific translate paths (e.g., single-row INSERT) set false. is_multi_write: bool, /// Mirrors SQLite's mayAbort: true if the statement may throw an ABORT exception. /// This flag is used in combination with is_multi_write to determine if statement subjournaling is required. /// Defaults to true for safety; specific translate paths (e.g., INSERT with no constraints) set false. may_abort: bool, /// If this ProgramBuilder is building trigger subprogram, a ref to the trigger is stored here. pub trigger: Option>, /// Whether this is a subprogram (trigger or FK action). Subprograms skip Transaction instructions. pub is_subprogram: bool, pub resolve_type: ResolveType, /// Whether the resolve_type was explicitly set from a statement-level OR clause. /// When false, per-constraint ON CONFLICT clauses from CREATE TABLE should be used. pub has_statement_conflict: bool, /// When set, all triggers fired from this program should use this conflict resolution. /// This is used in UPSERT DO UPDATE context to ensure nested trigger's OR IGNORE/REPLACE /// clauses don't suppress errors. pub trigger_conflict_override: Option, /// Temporary cursor overrides maps table internal IDs to cursor IDs that should be used instead of the normal resolution. /// This allows for things like hash build to use a separate cursor for iterating the same table. cursor_overrides: HashMap, /// Maps identifier names to registers for custom type encode/decode expressions. /// When set, `Expr::Id("value")` resolves to the register holding the input value, /// and type parameter names resolve to registers holding their concrete values. pub id_register_overrides: HashMap, /// When set, translate_expr will skip custom type decode for Expr::Column. /// This is used when building ORDER BY sort keys so the sorter compares /// encoded (on-disk) values. Decode is presentation-only. pub suppress_custom_type_decode: bool, /// When true, the next `emit_column` call will not bake the default value /// into the Column instruction. Used for custom type columns where the default /// needs to be encoded before use. pub suppress_column_default: bool, /// Hash join build signatures keyed by hash table id. hash_build_signatures: HashMap, /// Hash tables to keep open across subplans (e.g. materialization). hash_tables_to_keep_open: HashSet, /// Maps table internal_id to result_columns_start_reg for FROM clause subqueries. /// Used when nested subqueries need to reference columns from outer query subqueries. subquery_result_regs: HashMap, /// Counter for CTE identity tracking. Each CTE definition gets a unique ID /// so that multiple references to the same CTE can share materialized data. next_cte_id: usize, /// Registry of materialized CTEs, keyed by cte_id. /// Used to share materialized data across multiple CTE references via OpenDup. materialized_ctes: HashMap, /// Global count of references to each CTE across the entire query. /// Used to determine whether a CTE should be materialized (multi-ref) or use coroutine (single-ref). cte_reference_counts: HashMap, /// Stack of CTE names currently being planned. Used to detect circular /// references in non-recursive CTEs and to prevent fallthrough to schema /// resolution for same-named tables/views. ctes_being_defined: Vec, /// Counter for subquery numbering in EXPLAIN QUERY PLAN output. next_subquery_eqp_id: usize, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum MaterializedBuildInputModeTag { RowidOnly, Payload, } #[derive(Debug, Clone, PartialEq, Eq)] /// Signature of a hash build to allow reuse when inputs are unchanged. /// TODO: this is very heavy... we might consider hashing instead of storing full data. pub struct HashBuildSignature { /// WHERE term indices used as hash join keys. pub join_key_indices: Vec, /// Build-table columns stored as payload. pub payload_refs: Vec, /// Affinity string applied to join keys. pub key_affinities: String, /// Whether a bloom filter is enabled for this build. pub use_bloom_filter: bool, /// Rowid input cursor when the build side is materialized. pub materialized_input_cursor: Option, /// RowidOnly vs KeyPayload pub materialized_mode: Option, } /// Information about a materialized CTE, used for sharing data across multiple references. #[derive(Debug, Clone)] pub struct MaterializedCteInfo { /// The ephemeral table cursor holding materialized CTE data. pub cursor_id: CursorID, /// The table definition, needed for allocating dup cursors with the same CursorType. pub table: Arc, /// Number of result columns. pub num_columns: usize, } #[derive(Debug, Clone)] pub enum CursorType { BTreeTable(Arc), BTreeIndex(Arc), IndexMethod(Arc), Pseudo(PseudoCursorType), Sorter, VirtualTable(Arc), MaterializedView( Arc, Arc>, ), } impl CursorType { pub fn is_index(&self) -> bool { matches!(self, CursorType::BTreeIndex(_)) } pub fn get_explain_description(&self) -> String { let out = match self { CursorType::BTreeTable(btree_table) => { let mut col_count = btree_table.columns.len(); if btree_table.get_rowid_alias_column().is_none() { col_count += 1; } Some(( col_count, btree_table .columns .iter() .map(|col| { if let Some(coll) = col.collation_opt() { format!("{coll}") } else { "B".to_string() } }) .collect::>() .join(","), )) } CursorType::BTreeIndex(index) => { let mut col_count = index.columns.len(); if index.has_rowid { col_count += 1; } Some(( col_count, index .columns .iter() .map(|col| { let sign = match col.order { SortOrder::Asc => "", SortOrder::Desc => "-", }; if let Some(coll) = col.collation { format!("{sign}{coll}") } else { format!("{sign}B") } }) .collect::>() .join(","), )) } _ => None, }; out.map_or(String::new(), |(col_count, collations)| { format!("k({col_count},{collations})") }) } } #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub enum QueryMode { Normal, Explain, ExplainQueryPlan, } impl QueryMode { pub fn new(cmd: &ast::Cmd) -> Self { match cmd { ast::Cmd::ExplainQueryPlan(_) => QueryMode::ExplainQueryPlan, ast::Cmd::Explain(_) => QueryMode::Explain, ast::Cmd::Stmt(_) => QueryMode::Normal, } } } pub struct ProgramBuilderOpts { pub num_cursors: usize, pub approx_num_insns: usize, pub approx_num_labels: usize, } /// Use this macro to emit an OP_Explain instruction. /// Please use this macro instead of calling emit_explain() directly, /// because we want to avoid allocating a String if we are not in explain mode. #[macro_export] macro_rules! emit_explain { ($builder:expr, $push:expr, $detail:expr) => { if let $crate::QueryMode::ExplainQueryPlan = $builder.get_query_mode() { $builder.emit_explain($push, $detail); } }; } impl ProgramBuilder { /// Run a nested emission scope without leaking its result-column register base /// into the surrounding builder state. pub fn with_scoped_result_cols_start( &mut self, f: impl FnOnce(&mut Self) -> crate::Result, ) -> crate::Result { let saved = self.reg_result_cols_start; let result = f(self); self.reg_result_cols_start = saved; result } pub fn new( query_mode: QueryMode, capture_data_changes_info: Option, opts: ProgramBuilderOpts, ) -> Self { ProgramBuilder::_new(query_mode, capture_data_changes_info, opts, None, false) } pub fn new_for_trigger( query_mode: QueryMode, capture_data_changes_info: Option, opts: ProgramBuilderOpts, trigger: Arc, ) -> Self { ProgramBuilder::_new( query_mode, capture_data_changes_info, opts, Some(trigger), true, ) } /// Create a ProgramBuilder for a subprogram (FK actions, etc.) that runs within /// an existing transaction and doesn't emit Transaction instructions. pub fn new_for_subprogram( query_mode: QueryMode, capture_data_changes_info: Option, opts: ProgramBuilderOpts, ) -> Self { ProgramBuilder::_new(query_mode, capture_data_changes_info, opts, None, true) } fn _new( query_mode: QueryMode, capture_data_changes_info: Option, opts: ProgramBuilderOpts, trigger: Option>, is_subprogram: bool, ) -> Self { Self { table_reference_counter: TableRefIdCounter::new(), next_free_register: 1, next_free_cursor_id: 0, next_hash_table_id: HASH_TABLE_ID_BASE, insns: Vec::with_capacity(opts.approx_num_insns), cursor_ref: Vec::with_capacity(opts.num_cursors), constant_spans: Vec::new(), label_to_resolved_offset: Vec::with_capacity(opts.approx_num_labels), seekrowid_emitted_bitmask: 0, comments: Vec::new(), parameters: Parameters::new(), result_columns: Vec::new(), table_references: TableReferences::new(vec![], vec![]), collation: None, nested_level: 0, // These labels will be filled when `prologue()` is called init_label: BranchOffset::Placeholder, start_offset: BranchOffset::Placeholder, capture_data_changes_info, txn_mode: TransactionMode::None, write_databases: HashSet::default(), read_databases: HashSet::default(), write_database_cookies: HashMap::default(), read_database_cookies: HashMap::default(), rollback: false, query_mode, current_parent_explain_idx: None, reg_result_cols_start: None, is_multi_write: true, may_abort: true, trigger, is_subprogram, resolve_type: ResolveType::Abort, has_statement_conflict: false, trigger_conflict_override: None, cursor_overrides: HashMap::default(), id_register_overrides: HashMap::default(), suppress_custom_type_decode: false, suppress_column_default: false, hash_build_signatures: HashMap::default(), hash_tables_to_keep_open: HashSet::default(), subquery_result_regs: HashMap::default(), next_cte_id: 0, materialized_ctes: HashMap::default(), cte_reference_counts: HashMap::default(), ctes_being_defined: Vec::new(), next_subquery_eqp_id: 1, } } pub fn next_subquery_eqp_id(&mut self) -> usize { let id = self.next_subquery_eqp_id; self.next_subquery_eqp_id += 1; id } pub fn alloc_hash_table_id(&mut self) -> usize { let id = self.next_hash_table_id; self.next_hash_table_id = self .next_hash_table_id .checked_add(1) .expect("hash table id overflow"); id } /// Allocate a unique CTE identity. Each CTE definition in a query gets a unique ID /// so that multiple references to the same CTE can share materialized data via OpenDup. pub fn alloc_cte_id(&mut self) -> usize { let id = self.next_cte_id; self.next_cte_id += 1; id } /// Check if a CTE has already been materialized. /// Returns the materialization info if the CTE cursor can be shared via OpenDup. pub fn get_materialized_cte(&self, cte_id: usize) -> Option<&MaterializedCteInfo> { self.materialized_ctes.get(&cte_id) } /// Register a materialized CTE so that subsequent references can share it via OpenDup. pub fn register_materialized_cte(&mut self, cte_id: usize, info: MaterializedCteInfo) { self.materialized_ctes.insert(cte_id, info); } /// Increment the global reference count for a CTE. /// Called during planning when a CTE reference is created. pub fn increment_cte_reference(&mut self, cte_id: usize) { *self.cte_reference_counts.entry(cte_id).or_insert(0) += 1; } /// Get the global reference count for a CTE. /// Used during emission to decide whether to materialize (multi-ref) or use coroutine (single-ref). pub fn get_cte_reference_count(&self, cte_id: usize) -> usize { self.cte_reference_counts.get(&cte_id).copied().unwrap_or(0) } /// Mark a CTE name as currently being planned. While on the stack, /// `parse_table` will reject references to this name with "circular /// reference" instead of falling through to schema resolution. pub fn push_cte_being_defined(&mut self, name: String) { self.ctes_being_defined.push(name); } /// Remove the most recently pushed CTE name after planning completes. pub fn pop_cte_being_defined(&mut self) { self.ctes_being_defined.pop(); } /// Check whether a name refers to a CTE currently being planned. pub fn is_cte_being_defined(&self, name: &str) -> bool { self.ctes_being_defined.iter().any(|n| n == name) } /// Temporarily take the CTE-being-defined stack (e.g. during view /// expansion, which should not see CTE context from the caller). pub fn take_ctes_being_defined(&mut self) -> Vec { std::mem::take(&mut self.ctes_being_defined) } /// Restore the CTE-being-defined stack after a context-isolated expansion. pub fn restore_ctes_being_defined(&mut self, saved: Vec) { self.ctes_being_defined = saved; } pub fn set_resolve_type(&mut self, resolve_type: ResolveType) { self.resolve_type = resolve_type; } /// Set the trigger conflict override. When set, all triggers fired from this program /// should use this conflict resolution instead of their own OR clauses. pub fn set_trigger_conflict_override(&mut self, resolve_type: ResolveType) { self.trigger_conflict_override = Some(resolve_type); } /// Returns true if the given hash table id should be kept open across subplans. pub fn should_keep_hash_table_open(&self, hash_table_id: usize) -> bool { self.hash_tables_to_keep_open.contains(&hash_table_id) } /// Set the set of hash tables to keep open across subplans. pub fn set_hash_tables_to_keep_open(&mut self, tables: &HashSet) { self.hash_tables_to_keep_open.clone_from(tables); } /// Reset the set of hash tables to keep open. pub fn clear_hash_tables_to_keep_open(&mut self) { self.hash_tables_to_keep_open.clear(); } /// Returns true if the given hash build signature matches the recorded one for the given hash table id. pub fn hash_build_signature_matches( &self, hash_table_id: usize, signature: &HashBuildSignature, ) -> bool { self.hash_build_signatures .get(&hash_table_id) .is_some_and(|existing| existing == signature) } /// Returns true if there is a recorded hash build signature for the given hash table id. pub fn has_hash_build_signature(&self, hash_table_id: usize) -> bool { self.hash_build_signatures.contains_key(&hash_table_id) } /// Insert or update the hash build signature for the given hash table id. pub fn record_hash_build_signature( &mut self, hash_table_id: usize, signature: HashBuildSignature, ) { self.hash_build_signatures.insert(hash_table_id, signature); } /// Clear the hash build signature for the given hash table id. pub fn clear_hash_build_signature(&mut self, hash_table_id: usize) { self.hash_build_signatures.remove(&hash_table_id); } /// Store the result_columns_start_reg for a FROM clause subquery by its internal_id. /// Used so nested subqueries can access columns from outer query subqueries. pub fn set_subquery_result_reg(&mut self, internal_id: TableInternalId, result_reg: usize) { self.subquery_result_regs.insert(internal_id, result_reg); } /// Look up the result_columns_start_reg for a FROM clause subquery by its internal_id. /// Returns None if the subquery hasn't been emitted yet. pub fn get_subquery_result_reg(&self, internal_id: TableInternalId) -> Option { self.subquery_result_regs.get(&internal_id).copied() } /// Mark that this statement may modify/insert multiple rows (mirrors SQLite's sqlite3MultiWrite). /// When false, statement journals are skipped since single-write statements are atomic. pub fn set_multi_write(&mut self, is_multi_write: bool) { self.is_multi_write = is_multi_write; } /// Mark that this statement may throw an ABORT exception (mirrors SQLite's sqlite3MayAbort). pub fn set_may_abort(&mut self, may_abort: bool) { self.may_abort = may_abort; } pub fn capture_data_changes_info(&self) -> &Option { &self.capture_data_changes_info } pub fn extend(&mut self, opts: &ProgramBuilderOpts) { self.insns.reserve(opts.approx_num_insns); self.cursor_ref.reserve(opts.num_cursors); self.label_to_resolved_offset .reserve(opts.approx_num_labels); } /// Start a new constant span. The next instruction to be emitted will be the first /// instruction in the span. pub fn constant_span_start(&mut self) -> usize { let span = self.constant_spans.len(); let start = self.insns.len(); self.constant_spans.push((start, usize::MAX)); span } /// End the current constant span. The last instruction that was emitted is the last /// instruction in the span. pub fn constant_span_end(&mut self, span_idx: usize) { let span = &mut self.constant_spans[span_idx]; if span.1 == usize::MAX { span.1 = self.insns.len().saturating_sub(1); } } /// End all constant spans that are currently open. This is used to handle edge cases /// where we think a parent expression is constant, but we decide during the evaluation /// of one of its children that it is not. pub fn constant_span_end_all(&mut self) { for span in self.constant_spans.iter_mut() { if span.1 == usize::MAX { span.1 = self.insns.len().saturating_sub(1); } } } /// Check if there is a constant span that is currently open. pub fn constant_span_is_open(&self) -> bool { self.constant_spans .last() .is_some_and(|(_, end)| *end == usize::MAX) } /// Get the index of the next constant span. /// Used in [crate::translate::expr::translate_expr_no_constant_opt()] to invalidate /// all constant spans after the given index. pub fn constant_spans_next_idx(&self) -> usize { self.constant_spans.len() } /// Invalidate all constant spans after the given index. This is used when we want to /// be sure that constant optimization is never used for translating a given expression. /// See [crate::translate::expr::translate_expr_no_constant_opt()] for more details. pub fn constant_spans_invalidate_after(&mut self, idx: usize) { self.constant_spans.truncate(idx); } pub fn alloc_register(&mut self) -> usize { let reg = self.next_free_register; self.next_free_register += 1; reg } pub fn alloc_registers(&mut self, amount: usize) -> usize { let reg = self.next_free_register; self.next_free_register += amount; reg } /// Returns the next register that will be allocated by alloc_register/alloc_registers. pub fn peek_next_register(&self) -> usize { self.next_free_register } pub fn alloc_registers_and_init_w_null(&mut self, amount: usize) -> usize { let reg = self.alloc_registers(amount); self.emit_insn(Insn::Null { dest: reg, dest_end: if amount == 1 { None } else { Some(reg + amount - 1) }, }); reg } pub fn alloc_cursor_id_keyed(&mut self, key: CursorKey, cursor_type: CursorType) -> usize { turso_assert!( !self .cursor_ref .iter() .any(|(k, _)| k.as_ref().is_some_and(|k| k.equals(&key))), "duplicate cursor key" ); self._alloc_cursor_id(Some(key), cursor_type) } pub fn alloc_cursor_id_keyed_if_not_exists( &mut self, key: CursorKey, cursor_type: CursorType, ) -> usize { if let Some(cursor_id) = self.resolve_cursor_id_safe(&key) { cursor_id } else { self._alloc_cursor_id(Some(key), cursor_type) } } /// allocate proper cursor for the given index (either [CursorType::BTreeIndex] or [CursorType::IndexMethod]) pub fn alloc_cursor_index( &mut self, key: Option, index: &Arc, ) -> crate::Result { tracing::debug!("alloc cursor: {:?} {:?}", key, index.index_method.is_some()); let module = index.index_method.as_ref(); if let Some(m) = module { if !m.definition().backing_btree { return Ok(self._alloc_cursor_id(key, CursorType::IndexMethod(m.clone()))); } } Ok(self._alloc_cursor_id(key, CursorType::BTreeIndex(index.clone()))) } pub fn alloc_cursor_index_if_not_exists( &mut self, key: CursorKey, index: &Arc, ) -> crate::Result { if let Some(cursor_id) = self.resolve_cursor_id_safe(&key) { Ok(cursor_id) } else { self.alloc_cursor_index(Some(key), index) } } pub fn alloc_cursor_id(&mut self, cursor_type: CursorType) -> usize { self._alloc_cursor_id(None, cursor_type) } fn _alloc_cursor_id(&mut self, key: Option, cursor_type: CursorType) -> usize { let cursor = self.next_free_cursor_id; self.next_free_cursor_id += 1; self.cursor_ref.push((key, cursor_type)); turso_assert_eq!(self.cursor_ref.len(), self.next_free_cursor_id); cursor } pub fn add_pragma_result_column(&mut self, col_name: String) { // TODO figure out a better type definition for ResultSetColumn // or invent another way to set pragma result columns let expr = ast::Expr::Id(ast::Name::exact("".to_string())); self.result_columns.push(ResultSetColumn { expr, alias: Some(col_name), contains_aggregates: false, }); } #[instrument(skip(self), level = Level::DEBUG)] pub fn emit_insn(&mut self, insn: Insn) { // This seemingly empty trace here is needed so that a function span is emmited with it tracing::trace!(""); self.insns.push((insn, self.insns.len())); } /// Emit an instruction that should not start or extend a constant span on its own. /// If a parent constant span is already open, the instruction is emitted normally /// within that span (the parent's `is_constant` classification takes precedence). #[instrument(skip(self), level = Level::DEBUG)] pub fn emit_no_constant_insn(&mut self, insn: Insn) { if !self.constant_span_is_open() { self.constant_span_end_all(); } self.emit_insn(insn); } pub fn close_cursors(&mut self, cursors: &[CursorID]) { for cursor in cursors { self.emit_insn(Insn::Close { cursor_id: *cursor }); } } pub fn emit_string8(&mut self, value: String, dest: usize) { self.emit_insn(Insn::String8 { value, dest }); } pub fn emit_string8_new_reg(&mut self, value: String) -> usize { let dest = self.alloc_register(); self.emit_insn(Insn::String8 { value, dest }); dest } pub fn emit_int(&mut self, value: i64, dest: usize) { self.emit_insn(Insn::Integer { value, dest }); } pub fn emit_bool(&mut self, value: bool, dest: usize) { self.emit_insn(Insn::Integer { value: if value { 1 } else { 0 }, dest, }); } pub fn emit_null(&mut self, dest: usize, dest_end: Option) { self.emit_insn(Insn::Null { dest, dest_end }); } pub fn emit_result_row(&mut self, start_reg: usize, count: usize) { self.emit_insn(Insn::ResultRow { start_reg, count }); } fn emit_halt(&mut self, rollback: bool) { self.emit_insn(Insn::Halt { err_code: 0, description: if rollback { "rollback".to_string() } else { String::new() }, on_error: None, description_reg: None, }); } // no users yet, but I want to avoid someone else in the future // just adding parameters to emit_halt! If you use this, remove the // clippy warning please. #[allow(dead_code)] pub fn emit_halt_err(&mut self, err_code: usize, description: String) { self.emit_insn(Insn::Halt { err_code, description, on_error: None, description_reg: None, }); } pub fn add_comment(&mut self, insn_index: BranchOffset, comment: &'static str) { if let QueryMode::Explain | QueryMode::ExplainQueryPlan = self.query_mode { self.comments.push((insn_index.as_offset_int(), comment)); } } pub fn get_query_mode(&self) -> QueryMode { self.query_mode } /// use emit_explain macro instead, because we don't want to allocate /// String if we are not in explain mode pub fn emit_explain(&mut self, push: bool, detail: String) { if let QueryMode::ExplainQueryPlan = self.query_mode { self.emit_insn(Insn::Explain { p1: self.insns.len(), p2: self.current_parent_explain_idx, detail, }); if push { self.current_parent_explain_idx = Some(self.insns.len() - 1); } } } pub fn pop_current_parent_explain(&mut self) { if let QueryMode::ExplainQueryPlan = self.query_mode { if let Some(current) = self.current_parent_explain_idx { let (Insn::Explain { p2, .. }, _) = &self.insns[current] else { unreachable!("current_parent_explain_idx must point to an Explain insn"); }; self.current_parent_explain_idx = *p2; } } else { turso_debug_assert!(self.current_parent_explain_idx.is_none()); } } pub fn mark_last_insn_constant(&mut self) { if self.constant_span_is_open() { // no need to mark this insn as constant as the surrounding parent expression is already constant return; } let prev = self.insns.len().saturating_sub(1); self.constant_spans.push((prev, prev)); } fn emit_constant_insns(&mut self) { // Move compile-time constant instructions to the end of the program, // where they are executed once after Init jumps to it. // Stable partition: non-constant instructions first, then constant. // Since spans are sorted and non-overlapping, we track our position // in the span list and never look back - O(n + m) total, where // n = number of instructions, m = number of constant spans. let mut non_constant = Vec::with_capacity(self.insns.len()); let mut constant = Vec::new(); let mut span_idx = 0; for item in self.insns.drain(..) { let idx = item.1; // Advance past spans we've completely passed while span_idx < self.constant_spans.len() && self.constant_spans[span_idx].1 < idx { span_idx += 1; } // Check if current span contains this index let is_constant = span_idx < self.constant_spans.len() && self.constant_spans[span_idx].0 <= idx; if is_constant { constant.push(item); } else { non_constant.push(item); } } self.insns = non_constant; self.insns.extend(constant); // Build old index -> new position mapping let mut old_to_new = vec![0usize; self.insns.len()]; for (new_pos, (_, old_idx)) in self.insns.iter().enumerate() { old_to_new[*old_idx] = new_pos; } for resolved_offset in self.label_to_resolved_offset.iter_mut() { if let Some((old_offset, target)) = resolved_offset { *resolved_offset = Some((old_to_new[*old_offset as usize] as u32, *target)); } } for (offset, _) in self.comments.iter_mut() { *offset = old_to_new[*offset as usize] as u32; } if let QueryMode::ExplainQueryPlan = self.query_mode { self.current_parent_explain_idx = self.current_parent_explain_idx.map(|old| old_to_new[old]); for i in 0..self.insns.len() { let (Insn::Explain { p2, .. }, _) = &self.insns[i] else { continue; }; let new_p2 = p2.map(|old| old_to_new[old]); let (Insn::Explain { p1, p2, .. }, _) = &mut self.insns[i] else { unreachable!(); }; *p1 = i; *p2 = new_p2; } } } pub fn offset(&self) -> BranchOffset { BranchOffset::Offset(self.insns.len() as InsnReference) } pub fn allocate_label(&mut self) -> BranchOffset { let label_n = self.label_to_resolved_offset.len(); self.label_to_resolved_offset.push(None); BranchOffset::Label(label_n as u32) } /// Resolve a label to whatever instruction follows the one that was /// last emitted. /// /// Use this when your use case is: "the program should jump to whatever instruction /// follows the one that was previously emitted", and you don't care exactly /// which instruction that is. Examples include "the start of a loop", or /// "after the loop ends". /// /// It is important to handle those cases this way, because the precise /// instruction that follows any given instruction might change due to /// reordering the emitted instructions. #[inline] pub fn preassign_label_to_next_insn(&mut self, label: BranchOffset) { turso_assert!(label.is_label(), "BranchOffset should be a label", { "label": label }); self._resolve_label(label, self.offset().sub(1u32), JumpTarget::AfterThisInsn); } /// Resolve a label to exactly the instruction that was last emitted. /// /// Use this when your use case is: "the program should jump to the exact instruction /// that was last emitted", and you don't care WHERE exactly that ends up being /// once the order of the bytecode of the program is finalized. Examples include /// "jump to the Halt instruction", or "jump to the Next instruction of a loop". #[inline] pub fn resolve_label(&mut self, label: BranchOffset, to_offset: BranchOffset) { self._resolve_label(label, to_offset, JumpTarget::ExactlyThisInsn); } fn _resolve_label(&mut self, label: BranchOffset, to_offset: BranchOffset, target: JumpTarget) { turso_assert!(matches!(label, BranchOffset::Label(_))); turso_assert!(matches!(to_offset, BranchOffset::Offset(_))); let BranchOffset::Label(label_number) = label else { unreachable!("Label is not a label"); }; self.label_to_resolved_offset[label_number as usize] = Some((to_offset.as_offset_int(), target)); } /// Resolve unresolved labels to a specific offset in the instruction list. /// /// This function scans all instructions and resolves any labels to their corresponding offsets. /// It ensures that all labels are resolved correctly and updates the target program counter (PC) /// of each instruction that references a label. pub fn resolve_labels(&mut self) -> crate::Result<()> { let resolve = |pc: &mut BranchOffset, insn_name: &str| -> crate::Result<()> { if let BranchOffset::Label(label) = pc { let Some(Some((to_offset, target))) = self.label_to_resolved_offset.get(*label as usize) else { crate::bail_corrupt_error!( "Reference to undefined or unresolved label in {insn_name}: {label}" ); }; *pc = BranchOffset::Offset( to_offset + if *target == JumpTarget::ExactlyThisInsn { 0 } else { 1 }, ); } Ok(()) }; for (insn, _) in self.insns.iter_mut() { match insn { Insn::Init { target_pc } => { resolve(target_pc, "Init")?; } Insn::Eq { lhs: _lhs, rhs: _rhs, target_pc, .. } => { resolve(target_pc, "Eq")?; } Insn::Ne { lhs: _lhs, rhs: _rhs, target_pc, .. } => { resolve(target_pc, "Ne")?; } Insn::Lt { lhs: _lhs, rhs: _rhs, target_pc, .. } => { resolve(target_pc, "Lt")?; } Insn::Le { lhs: _lhs, rhs: _rhs, target_pc, .. } => { resolve(target_pc, "Le")?; } Insn::Gt { lhs: _lhs, rhs: _rhs, target_pc, .. } => { resolve(target_pc, "Gt")?; } Insn::Ge { lhs: _lhs, rhs: _rhs, target_pc, .. } => { resolve(target_pc, "Ge")?; } Insn::If { reg: _reg, target_pc, jump_if_null: _, } => { resolve(target_pc, "If")?; } Insn::IfNot { reg: _reg, target_pc, jump_if_null: _, } => { resolve(target_pc, "IfNot")?; } Insn::Rewind { pc_if_empty, .. } => { resolve(pc_if_empty, "Rewind")?; } Insn::Last { pc_if_empty, .. } => { resolve(pc_if_empty, "Last")?; } Insn::Goto { target_pc } => { resolve(target_pc, "Goto")?; } Insn::DecrJumpZero { reg: _reg, target_pc, } => { resolve(target_pc, "DecrJumpZero")?; } Insn::SorterNext { cursor_id: _cursor_id, pc_if_next, } => { resolve(pc_if_next, "SorterNext")?; } Insn::SorterSort { pc_if_empty, .. } => { resolve(pc_if_empty, "SorterSort")?; } Insn::SorterCompare { pc_when_nonequal: target_pc, .. } => { resolve(target_pc, "SorterCompare")?; } Insn::NotNull { reg: _reg, target_pc, } => { resolve(target_pc, "NotNull")?; } Insn::IfPos { target_pc, .. } => { resolve(target_pc, "IfPos")?; } Insn::Next { pc_if_next, .. } => { resolve(pc_if_next, "Next")?; } Insn::Once { target_pc_when_reentered, .. } => { resolve(target_pc_when_reentered, "Once")?; } Insn::Prev { pc_if_prev, .. } => { resolve(pc_if_prev, "Prev")?; } Insn::InitCoroutine { yield_reg: _, jump_on_definition, start_offset, } => { resolve(jump_on_definition, "InitCoroutine")?; resolve(start_offset, "InitCoroutine")?; } Insn::NotExists { cursor: _, rowid_reg: _, target_pc, } => { resolve(target_pc, "NotExists")?; } Insn::Yield { yield_reg: _, end_offset, subtype_clear_start_reg: _, subtype_clear_count: _, } => { resolve(end_offset, "Yield")?; } Insn::SeekRowid { target_pc, .. } => { resolve(target_pc, "SeekRowid")?; } Insn::Gosub { target_pc, .. } => { resolve(target_pc, "Gosub")?; } Insn::Jump { target_pc_eq, target_pc_lt, target_pc_gt, } => { resolve(target_pc_eq, "Jump")?; resolve(target_pc_lt, "Jump")?; resolve(target_pc_gt, "Jump")?; } Insn::SeekGE { target_pc, .. } => resolve(target_pc, "SeekGE")?, Insn::SeekGT { target_pc, .. } => resolve(target_pc, "SeekGT")?, Insn::SeekLE { target_pc, .. } => resolve(target_pc, "SeekLE")?, Insn::SeekLT { target_pc, .. } => resolve(target_pc, "SeekLT")?, Insn::IdxGE { target_pc, .. } => resolve(target_pc, "IdxGE")?, Insn::IdxLE { target_pc, .. } => resolve(target_pc, "IdxLE")?, Insn::IdxGT { target_pc, .. } => resolve(target_pc, "IdxGT")?, Insn::IdxLT { target_pc, .. } => resolve(target_pc, "IdxLT")?, Insn::IndexMethodQuery { pc_if_empty, .. } => { resolve(pc_if_empty, "IndexMethodQuery")?; } Insn::IsNull { reg: _, target_pc } => resolve(target_pc, "IsNull")?, Insn::VNext { pc_if_next, .. } => resolve(pc_if_next, "VNext")?, Insn::VFilter { pc_if_empty, .. } => resolve(pc_if_empty, "VFilter")?, Insn::RowSetRead { pc_if_empty, .. } => resolve(pc_if_empty, "RowSetRead")?, Insn::RowSetTest { pc_if_found, .. } => resolve(pc_if_found, "RowSetTest")?, Insn::NoConflict { target_pc, .. } => resolve(target_pc, "NoConflict")?, Insn::Found { target_pc, .. } => resolve(target_pc, "Found")?, Insn::NotFound { target_pc, .. } => resolve(target_pc, "NotFound")?, Insn::FkIfZero { target_pc, .. } => resolve(target_pc, "FkIfZero")?, Insn::Filter { target_pc, .. } => resolve(target_pc, "Filter")?, Insn::HashProbe { target_pc, .. } => resolve(target_pc, "HashProbe")?, Insn::HashNext { target_pc, .. } => resolve(target_pc, "HashNext")?, Insn::HashDistinct { data } => resolve(&mut data.target_pc, "HashDistinct")?, Insn::HashScanUnmatched { target_pc, .. } => { resolve(target_pc, "HashScanUnmatched")? } Insn::HashNextUnmatched { target_pc, .. } => { resolve(target_pc, "HashNextUnmatched")? } Insn::HashGraceInit { target_pc, .. } => resolve(target_pc, "HashGraceInit")?, Insn::HashGraceLoadPartition { target_pc, .. } => { resolve(target_pc, "HashGraceLoadPartition")? } Insn::HashGraceNextProbe { target_pc, .. } => { resolve(target_pc, "HashGraceNextProbe")? } Insn::HashGraceAdvancePartition { target_pc, .. } => { resolve(target_pc, "HashGraceAdvancePartition")? } Insn::Program { ignore_jump_target, .. } => resolve(ignore_jump_target, "Program")?, _ => {} } } self.label_to_resolved_offset.clear(); Ok(()) } /// Set a cursor override for a table. When resolving a table cursor for this table, /// the override cursor will be used instead of the normal resolution. pub fn set_cursor_override(&mut self, table_ref_id: TableInternalId, cursor_id: CursorID) { self.cursor_overrides.insert(table_ref_id.into(), cursor_id); } /// Clear the cursor override for a table. pub fn clear_cursor_override(&mut self, table_ref_id: TableInternalId) { self.cursor_overrides.remove(&table_ref_id.into()); } /// Clear all cursor overrides. pub fn clear_all_cursor_overrides(&mut self) { self.cursor_overrides.clear(); } /// Check if a cursor override is active for a given table. pub fn has_cursor_override(&self, table_ref_id: TableInternalId) -> bool { self.cursor_overrides.contains_key(&table_ref_id.into()) } // translate [CursorKey] to cursor id pub fn resolve_cursor_id_safe(&self, key: &CursorKey) -> Option { // Check cursor overrides first, only apply override for table cursors. // Index cursor lookups are not overridden because when a cursor override is active, // the calling code (translate_expr) should skip index logic entirely. if key.index.is_none() && !key.is_build { let table_id: usize = key.table_reference_id.into(); if let Some(&cursor_id) = self.cursor_overrides.get(&table_id) { return Some(cursor_id); } } self.cursor_ref .iter() .position(|(k, _)| k.as_ref().is_some_and(|k| k.equals(key))) } pub fn resolve_cursor_id(&self, key: &CursorKey) -> CursorID { self.resolve_cursor_id_safe(key) .unwrap_or_else(|| panic!("Cursor not found: {key:?}")) } /// Resolve the first allocated index cursor for a given table reference. /// This method exists due to a limitation of our translation system where /// a subquery that references an outer query table cannot know whether a /// table cursor, index cursor, or both were opened for that table reference. /// Hence: currently we first try to resolve a table cursor, and if that fails, /// we resolve an index cursor via this method. pub fn resolve_any_index_cursor_id_for_table(&self, table_ref_id: TableInternalId) -> CursorID { self.resolve_any_index_cursor_id_for_table_safe(table_ref_id) .unwrap_or_else(|| panic!("No index cursor found for table {table_ref_id}")) } pub fn resolve_any_index_cursor_id_for_table_safe( &self, table_ref_id: TableInternalId, ) -> Option { self.cursor_ref.iter().position(|(k, _)| { k.as_ref() .is_some_and(|k| k.table_reference_id == table_ref_id && k.index.is_some()) }) } /// Resolve the [Index] that a given cursor is associated with. pub fn resolve_index_for_cursor_id(&self, cursor_id: CursorID) -> Arc { let cursor_ref = &self .cursor_ref .get(cursor_id) .unwrap_or_else(|| panic!("Cursor not found: {cursor_id}")) .1; let CursorType::BTreeIndex(index) = cursor_ref else { panic!("Cursor is not an index: {cursor_id}"); }; index.clone() } /// Get the [CursorType] of a given cursor. pub fn get_cursor_type(&self, cursor_id: CursorID) -> Option<&CursorType> { self.cursor_ref .get(cursor_id) .map(|(_, cursor_type)| cursor_type) } pub fn set_collation(&mut self, c: Option<(CollationSeq, bool)>) { self.collation = c } pub fn curr_collation_ctx(&self) -> Option<(CollationSeq, bool)> { self.collation } pub fn curr_collation(&self) -> Option { self.collation.map(|c| c.0) } pub fn reset_collation(&mut self) { self.collation = None; } #[inline] pub fn nested(&mut self, body: impl FnOnce(&mut Self) -> T) -> T { self.incr_nesting(); let res = body(self); self.decr_nesting(); res } #[inline] fn incr_nesting(&mut self) { self.nested_level += 1; } #[inline] fn decr_nesting(&mut self) { self.nested_level -= 1; } /// Returns true if we are inside a nested subquery context. #[inline] pub fn is_nested(&self) -> bool { self.nested_level > 0 } /// Initialize the program with basic setup and return initial metadata and labels pub fn prologue(&mut self) { if self.is_subprogram { // Subprograms (triggers, FK actions) don't need Transaction - they run within parent's tx self.init_label = self.allocate_label(); self.emit_insn(Insn::Init { target_pc: self.init_label, }); self.preassign_label_to_next_insn(self.init_label); self.start_offset = self.offset(); return; } if self.nested_level == 0 { self.init_label = self.allocate_label(); self.emit_insn(Insn::Init { target_pc: self.init_label, }); self.start_offset = self.offset(); } } /// Tries to mirror: https://github.com/sqlite/sqlite/blob/e77e589a35862f6ac9c4141cfd1beb2844b84c61/src/build.c#L5379 pub fn begin_write_operation(&mut self) { self.txn_mode = TransactionMode::Write; self.write_databases.insert(0); } /// Begin a write operation on a specific database (for attached databases). pub fn begin_write_on_database(&mut self, database_id: usize, schema_cookie: u32) { self.txn_mode = TransactionMode::Write; self.write_databases.insert(database_id); self.write_database_cookies .insert(database_id, schema_cookie); } pub fn begin_read_operation(&mut self) { // Just override the transaction mode when it is None if matches!(self.txn_mode, TransactionMode::None) { self.txn_mode = TransactionMode::Read; } } /// Begin a read operation on a specific attached database. /// This ensures a Transaction instruction is emitted for the attached pager /// so that a WAL read lock is acquired. pub fn begin_read_on_database(&mut self, database_id: usize, schema_cookie: u32) { self.begin_read_operation(); if crate::is_attached_db(database_id) { self.read_databases.insert(database_id); self.read_database_cookies .insert(database_id, schema_cookie); } } pub fn begin_concurrent_operation(&mut self) { self.txn_mode = TransactionMode::Concurrent; } /// Indicates the rollback behvaiour for the halt instruction in epilogue pub fn rollback(&mut self) { self.rollback = true; } /// Clean up and finalize the program, resolving any remaining labels /// Note that although these are the final instructions, typically an SQLite /// query will jump to the Transaction instruction via init_label. pub fn epilogue(&mut self, schema: &Schema) { if self.is_subprogram { // Subprograms (triggers, FK actions) just emit Halt without Transaction let description = if self.trigger.is_some() { "trigger" } else { "fk action" }; self.emit_insn(Insn::Halt { err_code: 0, description: description.to_string(), on_error: None, description_reg: None, }); return; } if self.nested_level == 0 { // "rollback" flag is used to determine if halt should rollback the transaction. self.emit_halt(self.rollback); self.preassign_label_to_next_insn(self.init_label); if !matches!(self.txn_mode, TransactionMode::None) { // Emit Transaction for main database always self.emit_insn(Insn::Transaction { db: crate::MAIN_DB_ID, tx_mode: self.txn_mode, schema_cookie: schema.schema_version, }); // Emit Transaction for each attached database that needs a write for &db_id in &self.write_databases.clone() { if crate::is_attached_db(db_id) { let cookie = self .write_database_cookies .get(&db_id) .copied() .unwrap_or(0); self.emit_insn(Insn::Transaction { db: db_id, tx_mode: self.txn_mode, schema_cookie: cookie, }); } } // Emit Transaction for each attached database that only needs a read // (skip databases already covered by write_databases) for &db_id in &self.read_databases.clone() { if !self.write_databases.contains(&db_id) { let cookie = self.read_database_cookies.get(&db_id).copied().unwrap_or(0); self.emit_insn(Insn::Transaction { db: db_id, tx_mode: TransactionMode::Read, schema_cookie: cookie, }); } } } if !self.constant_spans.is_empty() { self.emit_constant_insns(); } self.emit_insn(Insn::Goto { target_pc: self.start_offset, }); } } /// Checks whether `table` or any of its indices has been opened in the program pub fn is_table_open(&self, table: &Table) -> bool { self.table_references.contains_table(table) } /// Returns true if the cursor is a BTreeTable cursor. pub fn cursor_is_btree(&self, cursor_id: CursorID) -> bool { matches!(self.cursor_ref[cursor_id].1, CursorType::BTreeTable(_)) } #[inline] pub fn cursor_loop(&mut self, cursor_id: CursorID, f: impl Fn(&mut ProgramBuilder, usize)) { let loop_start = self.allocate_label(); let loop_end = self.allocate_label(); self.emit_insn(Insn::Rewind { cursor_id, pc_if_empty: loop_end, }); self.preassign_label_to_next_insn(loop_start); let rowid = self.alloc_register(); self.emit_insn(Insn::RowId { cursor_id, dest: rowid, }); self.emit_insn(Insn::IsNull { reg: rowid, target_pc: loop_end, }); f(self, rowid); self.emit_insn(Insn::Next { cursor_id, pc_if_next: loop_start, }); self.preassign_label_to_next_insn(loop_end); } pub fn emit_column_or_rowid(&mut self, cursor_id: CursorID, column: usize, out: usize) { let (_, cursor_type) = self.cursor_ref.get(cursor_id).expect("cursor_id is valid"); if let CursorType::BTreeTable(btree) = cursor_type { let column_def = btree .columns .get(column) .expect("column index out of bounds"); if column_def.is_rowid_alias() { // Consume the suppress_column_default flag so it doesn't // leak to the next column (emit_column normally consumes it). self.suppress_column_default = false; self.emit_insn(Insn::RowId { cursor_id, dest: out, }); } else { self.emit_column(cursor_id, column, out); } } else { self.emit_column(cursor_id, column, out); } } fn emit_column(&mut self, cursor_id: CursorID, column: usize, out: usize) { let (_, cursor_type) = self.cursor_ref.get(cursor_id).expect("cursor_id is valid"); let default = 'value: { let default = match cursor_type { CursorType::BTreeTable(btree) => &btree.columns[column].default, CursorType::BTreeIndex(index) => &index.columns[column].default, CursorType::MaterializedView(btree, _) => &btree.columns[column].default, _ => break 'value None, }; let Some(ref default_expr) = default else { break 'value None; }; // Try to constant-fold the default expression into a Value for the // Column instruction. Non-constant defaults (e.g. DEFAULT (ABS(-5))) // can't be folded and yield None here — that's correct: they are // evaluated at INSERT time via translate_expr. The Column default // only matters for pre-existing rows after ALTER TABLE ADD COLUMN, // and ALTER TABLE already validates that the default is constant. let mut value = match crate::translate::alter::eval_constant_default_value(default_expr) { Ok(v) => v, Err(_) => break 'value None, }; // Apply column affinity to the default value, matching SQLite's // sqlite3ColumnDefault which calls sqlite3ValueFromExpr with // pCol->affinity. This ensures e.g. ALTER TABLE ADD COLUMN c TEXT // DEFAULT 0 returns text "0" rather than integer 0 for pre-existing rows. let affinity = match cursor_type { CursorType::BTreeTable(btree) => btree.columns[column].affinity(), CursorType::MaterializedView(btree, _) => btree.columns[column].affinity(), _ => Affinity::Blob, }; if let Some(converted) = affinity.convert(&value) { value = match converted { either::Either::Left(val_ref) => val_ref.to_owned(), either::Either::Right(val) => val, }; } Some(value) }; let default = if self.suppress_column_default { self.suppress_column_default = false; None } else { default }; self.emit_insn(Insn::Column { cursor_id, column, dest: out, default, }); } pub fn build_prepared_program( mut self, prepare_context: PrepareContext, change_cnt_on: bool, sql: &str, ) -> crate::Result { self.resolve_labels()?; self.parameters.list.dedup(); // Mirrors SQLite's: usesStmtJournal = isMultiWrite && mayAbort // Statement journals are only needed when a statement writes multiple rows AND could // abort midway (e.g. constraint violation). Single-row writes are atomic and don't // need statement-level rollback. Both flags default to true; specific translate paths // (e.g., single-row INSERT) set is_multi_write=false to opt out. let needs_stmt_subtransactions = matches!(self.txn_mode, TransactionMode::Write) && self.is_multi_write && self.may_abort; let contains_trigger_subprograms = self .insns .iter() .any(|(insn, _)| matches!(insn, Insn::Program { .. })); let prepared = PreparedProgram { max_registers: self.next_free_register, insns: self.insns, cursor_ref: self.cursor_ref, comments: self.comments, parameters: self.parameters, change_cnt_on, result_columns: self.result_columns, table_references: self.table_references, sql: sql.to_string(), needs_stmt_subtransactions: crate::Arc::new(crate::AtomicBool::new( needs_stmt_subtransactions, )), trigger: self.trigger.take(), is_subprogram: self.is_subprogram, contains_trigger_subprograms, resolve_type: self.resolve_type, prepare_context, write_databases: self.write_databases, read_databases: self.read_databases, }; Ok(prepared) } pub fn build( self, connection: Arc, change_cnt_on: bool, sql: &str, ) -> crate::Result { let prepare_context = PrepareContext::from_connection(&connection); let prepared = self.build_prepared_program(prepare_context, change_cnt_on, sql)?; Ok(Program::from_prepared(Arc::new(prepared), connection)) } } ================================================ FILE: core/vdbe/execute.rs ================================================ use crate::error::SQLITE_CONSTRAINT_UNIQUE; use crate::function::AlterTableFunc; use crate::mvcc::cursor::{MvccCursorType, NextRowidResult}; use crate::mvcc::database::CheckpointStateMachine; use crate::mvcc::MvccClock; use crate::numeric::Numeric; use crate::schema::{Schema, Table, SQLITE_SEQUENCE_TABLE_NAME}; use crate::state_machine::StateMachine; use crate::storage::btree::{ integrity_check, CursorTrait, IntegrityCheckError, IntegrityCheckState, PageCategory, }; use crate::storage::database::DatabaseFile; use crate::storage::journal_mode; use crate::storage::page_cache::PageCache; use crate::storage::pager::{default_page1, CreateBTreeFlags, PageRef, SavepointResult}; use crate::storage::sqlite3_ondisk::{DatabaseHeader, PageSize, RawVersion}; use crate::translate::collate::CollationSeq; use crate::translate::pragma::TURSO_CDC_VERSION_TABLE_NAME; use crate::types::{ compare_immutable, compare_records_generic, AsValueRef, Extendable, IOCompletions, IOResult, ImmutableRecord, IndexInfo, SeekResult, Text, ValueIterator, }; use crate::util::{ escape_sql_string_literal, normalize_ident, rename_identifiers, rename_identifiers_scoped_when_clause, rewrite_check_expr_table_refs, rewrite_column_references_if_needed, rewrite_fk_parent_cols_if_self_ref, rewrite_fk_parent_table_if_needed, rewrite_inline_col_fk_target_if_needed, rewrite_trigger_cmd_column_refs, rewrite_trigger_cmd_table_refs, rewrite_view_sql_for_column_rename, trim_ascii_whitespace, RewrittenView, }; use crate::vdbe::affinity::{ apply_numeric_affinity, try_for_float, Affinity, NumericParseResult, ParsedNumber, }; use crate::vdbe::hash_table::{ HashEntry, HashInsertResult, HashTable, HashTableConfig, PendingHashInsert, DEFAULT_MEM_BUDGET, }; use crate::vdbe::insn::InsertFlags; use crate::vdbe::metrics::HashJoinMetrics; use crate::vdbe::value::ComparisonOp; use crate::vdbe::{ registers_to_ref_values, DeferredSeekState, EndStatement, OpHashBuildState, OpHashProbeState, StepResult, TxnCleanup, }; use crate::vector::{ vector1bit, vector32, vector32_sparse, vector64, vector8, vector_concat, vector_distance_cos, vector_distance_dot, vector_distance_jaccard, vector_distance_l2, vector_extract, vector_slice, }; use crate::{ error::{ LimboError, SQLITE_CONSTRAINT, SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_NOTNULL, SQLITE_CONSTRAINT_PRIMARYKEY, SQLITE_CONSTRAINT_TRIGGER, SQLITE_ERROR, }, ext::ExtValue, function::{AggFunc, ExtFunc, MathFunc, MathFuncArity, ScalarFunc, VectorFunc}, functions::{ datetime::{ exec_date, exec_datetime_full, exec_julianday, exec_strftime, exec_time, exec_unixepoch, }, printf::exec_printf, }, stats::StatAccum, translate::emitter::TransactionMode, }; use crate::{ get_cursor, CaptureDataChangesInfo, CheckpointMode, Completion, Connection, DatabaseStorage, IOExt, MvCursor, NonNan, QueryMode, }; use crate::{CdcVersion, Statement}; use branches::{mark_unlikely, unlikely}; use either::Either; use smallvec::SmallVec; use std::any::Any; use std::env::temp_dir; use std::str::FromStr; use std::{ borrow::BorrowMut, num::NonZero, sync::{atomic::Ordering, Arc}, }; use turso_macros::match_ignore_ascii_case; use crate::pseudo::PseudoCursor; use crate::storage::btree::{BTreeCursor, BTreeKey}; use crate::{ storage::wal::CheckpointResult, types::{AggContext, Cursor, ExternalAggState, SeekKey, SeekOp, SumAggState, Value, ValueType}, util::{cast_real_to_integer, checked_cast_text_to_numeric, parse_schema_rows}, vdbe::{ builder::CursorType, insn::{IdxInsertFlags, Insn, SavepointOp}, }, }; use crate::{connection::Row, info, turso_assert, OpenFlags, TransactionState, ValueRef}; use super::{ array::{ array_values_from_blob, compare_arrays, compute_array_length, exec_array_append, exec_array_cat, exec_array_contains, exec_array_contains_all, exec_array_overlap, exec_array_position, exec_array_prepend, exec_array_remove, exec_array_slice, exec_array_to_string, exec_string_to_array, make_array_from_registers, parse_text_array, serialize_array_from_blob, values_to_record_blob, }, insn::{Cookie, RegisterOrLiteral}, CommitState, }; use crate::sync::{Mutex, RwLock}; use turso_parser::ast::{self, ForeignKeyClause, Name, ResolveType}; use turso_parser::parser::Parser; use super::sorter::Sorter; #[cfg(feature = "json")] use crate::{ function::JsonFunc, json, json::convert_dbtype_to_raw_jsonb, json::get_json, json::is_json_valid, json::json_array, json::json_array_length, json::json_arrow_extract, json::json_arrow_shift_extract, json::json_error_position, json::json_extract, json::json_from_raw_bytes_agg, json::json_insert, json::json_object, json::json_patch, json::json_quote, json::json_remove, json::json_replace, json::json_set, json::json_type, json::jsonb, json::jsonb_array, json::jsonb_extract, json::jsonb_insert, json::jsonb_object, json::jsonb_patch, json::jsonb_remove, json::jsonb_replace, json::jsonb_set, }; use super::{make_record, Program, ProgramState, Register}; #[cfg(feature = "fs")] use crate::connection::resolve_ext_path; use crate::{bail_constraint_error, must_be_btree_cursor, MvStore, Pager, Result}; /// Macro to destructure an Insn enum variant, only to be used when it /// is *impossible* to be another variant. macro_rules! load_insn { ($variant:ident { $($field:tt $(: $binding:pat)?),* $(,)? }, $insn:expr) => { #[cfg(debug_assertions)] let Insn::$variant { $($field $(: $binding)?),* } = $insn else { panic!("Expected Insn::{}, got {:?}", stringify!($variant), $insn); }; #[cfg(not(debug_assertions))] let Insn::$variant { $($field $(: $binding)?),*} = $insn else { // this will optimize away the branch unsafe { std::hint::unreachable_unchecked() }; }; }; } macro_rules! return_if_io { ($expr:expr) => { match $expr { Ok(IOResult::Done(v)) => v, Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), Err(err) => { mark_unlikely(); return Err(err); } } }; } macro_rules! check_arg_count { ($actual:expr, $expected:expr) => { if unlikely($actual != $expected) { return Err(LimboError::InternalError(format!( "expected {} argument(s), got {}", $expected, $actual ))); } }; } pub type InsnFunction = fn(&Program, &mut ProgramState, &Insn, &Arc) -> Result; /// Parse a Value (text, int, float, or blob) into a BigDecimal. fn value_to_bigdecimal(val: &Value) -> Result { use bigdecimal::BigDecimal; use std::str::FromStr; match val { Value::Numeric(Numeric::Integer(i)) => Ok(BigDecimal::from(*i)), Value::Numeric(Numeric::Float(f)) => BigDecimal::from_str(&f.to_string()) .map_err(|_| LimboError::Constraint(format!("invalid numeric value: {f}"))), Value::Text(t) => BigDecimal::from_str(&t.value) .map_err(|_| LimboError::Constraint(format!("invalid numeric value: \"{}\"", t.value))), Value::Blob(b) => crate::numeric::decimal::blob_to_bigdecimal(b), _ => Err(LimboError::Constraint(format!( "cannot convert to numeric: \"{val}\"" ))), } } /// Create a sort comparator closure from a SortComparatorType enum. fn make_sort_comparator( cmp_type: &crate::vdbe::insn::SortComparatorType, ) -> crate::vdbe::sorter::SortComparator { use crate::types::ValueRef; use crate::vdbe::insn::SortComparatorType; use std::cmp::Ordering; match cmp_type { SortComparatorType::NumericLt => { std::sync::Arc::new(|a: &ValueRef, b: &ValueRef| -> Ordering { match (a, b) { (ValueRef::Null, ValueRef::Null) => Ordering::Equal, (ValueRef::Null, _) => Ordering::Less, (_, ValueRef::Null) => Ordering::Greater, _ => { // Decode from ValueRef to Value for value_to_bigdecimal let a_val = a.to_owned(); let b_val = b.to_owned(); match (value_to_bigdecimal(&a_val), value_to_bigdecimal(&b_val)) { (Ok(a_dec), Ok(b_dec)) => a_dec.cmp(&b_dec), _ => a.partial_cmp(b).unwrap_or(Ordering::Equal), } } } }) } SortComparatorType::StringReverse => { std::sync::Arc::new(|a: &ValueRef, b: &ValueRef| -> Ordering { fn reverse_str(v: &ValueRef) -> String { match v { ValueRef::Text(t) => t.to_string().chars().rev().collect(), _ => String::new(), } } match (a, b) { (ValueRef::Null, ValueRef::Null) => Ordering::Equal, (ValueRef::Null, _) => Ordering::Less, (_, ValueRef::Null) => Ordering::Greater, _ => reverse_str(a).cmp(&reverse_str(b)), } }) } SortComparatorType::TestUintLt => { std::sync::Arc::new(|a: &ValueRef, b: &ValueRef| -> Ordering { fn to_u64(v: &ValueRef) -> Option { match v { ValueRef::Null => None, ValueRef::Numeric(Numeric::Integer(i)) => { if *i >= 0 { Some(*i as u64) } else { None } } ValueRef::Text(t) => t.to_string().parse::().ok(), _ => None, } } match (a, b) { (ValueRef::Null, ValueRef::Null) => Ordering::Equal, (ValueRef::Null, _) => Ordering::Less, (_, ValueRef::Null) => Ordering::Greater, _ => match (to_u64(a), to_u64(b)) { (Some(a), Some(b)) => a.cmp(&b), _ => a.partial_cmp(b).unwrap_or(Ordering::Equal), }, } }) } SortComparatorType::ArrayLt => { std::sync::Arc::new(|a: &ValueRef, b: &ValueRef| -> Ordering { match (a, b) { (ValueRef::Null, ValueRef::Null) => Ordering::Equal, (ValueRef::Null, _) => Ordering::Less, (_, ValueRef::Null) => Ordering::Greater, (ValueRef::Blob(a_blob), ValueRef::Blob(b_blob)) => { crate::vdbe::array::compare_arrays(a_blob, b_blob) .unwrap_or(Ordering::Equal) } (ValueRef::Text(a_text), ValueRef::Text(b_text)) => { let a_vals = crate::vdbe::array::parse_text_array(a_text); let b_vals = crate::vdbe::array::parse_text_array(b_text); match (a_vals, b_vals) { (Some(av), Some(bv)) => { let a_blob = crate::vdbe::array::values_to_record_blob(&av); let b_blob = crate::vdbe::array::values_to_record_blob(&bv); if let (Value::Blob(ab), Value::Blob(bb)) = (&a_blob, &b_blob) { crate::vdbe::array::compare_arrays(ab, bb) .unwrap_or(Ordering::Equal) } else { Ordering::Equal } } _ => a.partial_cmp(b).unwrap_or(Ordering::Equal), } } _ => a.partial_cmp(b).unwrap_or(Ordering::Equal), } }) } } } /// Compare two values using the specified collation for text values. /// Non-text values are compared using their natural ordering. fn compare_with_collation( lhs: &Value, rhs: &Value, collation: Option, ) -> std::cmp::Ordering { match (lhs, rhs) { (Value::Text(lhs_text), Value::Text(rhs_text)) => { if let Some(coll) = collation { coll.compare_strings(lhs_text.as_str(), rhs_text.as_str()) } else { lhs.cmp(rhs) } } _ => lhs.cmp(rhs), } } pub enum InsnFunctionStepResult { Done, IO(IOCompletions), Row, Step, } impl From> for InsnFunctionStepResult { fn from(value: IOResult) -> Self { match value { IOResult::Done(_) => InsnFunctionStepResult::Done, IOResult::IO(io) => InsnFunctionStepResult::IO(io), } } } pub fn op_init( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Init { target_pc }, insn); if unlikely(!target_pc.is_offset()) { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } pub fn op_add( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Add { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_add(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_subtract( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Subtract { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_subtract(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_multiply( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Multiply { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_multiply(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_divide( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Divide { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_divide(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_drop_index( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(DropIndex { index, db }, insn); let conn = program.connection.clone(); let is_mvcc = conn.mv_store_for_db(*db).is_some(); conn.with_database_schema_mut(*db, |schema| { // In MVCC mode, track dropped index root pages so integrity_check knows about them. // The btree pages won't be freed until checkpoint, so integrity_check needs to // include them to avoid "page never used" false positives. if is_mvcc && index.root_page > 0 { schema.dropped_root_pages.insert(index.root_page); } schema.remove_index(index); }); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_remainder( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Remainder { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_remainder(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_bit_and( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(BitAnd { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_bit_and(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_bit_or( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(BitOr { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_bit_or(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_bit_not( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(BitNot { reg, dest }, insn); state.registers[*dest].set_value(state.registers[*reg].get_value().exec_bit_not()); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_checkpoint( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Checkpoint { database, checkpoint_mode, dest, }, insn ); if !program.connection.auto_commit.load(Ordering::SeqCst) { // TODO: sqlite returns "Runtime error: database table is locked (6)" when a table is in use // when a checkpoint is attempted. We don't have table locks, so return TableLocked for any // attempt to checkpoint in an interactive transaction. This does not end the transaction, // however. return Err(LimboError::TableLocked); } let pager = program.get_pager_from_database_index(database); // In autocommit mode, this statement can still hold an implicit read tx. // RESTART/TRUNCATE checkpoint needs to restart WAL and may fail with Busy // if we keep our own statement read slot while checkpointing. if matches!( checkpoint_mode, CheckpointMode::Restart | CheckpointMode::Truncate { .. } ) && pager.holds_read_lock() { pager.end_read_tx(); } // Re-fetch mv_store from connection to get the latest value. // This is necessary because the mv_store may have been set by a preceding JournalMode instruction // (e.g., when switching from WAL to MVCC mode via `PRAGMA journal_mode = "mvcc"`). let mv_store = program.connection.mv_store_for_db(*database); if let Some(mv_store) = mv_store.as_ref() { if !matches!(checkpoint_mode, CheckpointMode::Truncate { .. }) { return Err(LimboError::InvalidArgument( "Only TRUNCATE checkpoint mode is supported for MVCC".to_string(), )); } use crate::state_machine::{StateTransition, TransitionResult}; let mut ckpt_sm = CheckpointStateMachine::new( pager.clone(), mv_store.clone(), program.connection.clone(), true, program.connection.get_sync_mode(), ); let CheckpointResult { wal_max_frame, wal_total_backfilled, .. } = loop { match ckpt_sm.step(&()) { Ok(TransitionResult::Continue) => {} Ok(TransitionResult::Done(result)) => break result, Ok(TransitionResult::Io(iocompletions)) => { if let Err(err) = iocompletions.wait(pager.io.as_ref()) { ckpt_sm.cleanup_after_external_io_error(); return Err(err); } } Err(err) => return Err(err), } }; // https://sqlite.org/pragma.html#pragma_wal_checkpoint // 1st col: 1 (checkpoint SQLITE_BUSY) or 0 (not busy). state.registers[*dest].set_int(0); // 2nd col: # modified pages written to wal file state.registers[*dest + 1].set_int(wal_max_frame as i64); // 3rd col: # pages moved to db after checkpoint state.registers[*dest + 2].set_int(wal_total_backfilled as i64); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let step_result = pager.checkpoint(*checkpoint_mode, program.connection.get_sync_mode(), true); match step_result { Ok(IOResult::Done(CheckpointResult { wal_max_frame, wal_total_backfilled, .. })) => { // https://sqlite.org/pragma.html#pragma_wal_checkpoint // 1st col: 1 (checkpoint SQLITE_BUSY) or 0 (not busy). state.registers[*dest].set_int(0); // 2nd col: # modified pages written to wal file state.registers[*dest + 1].set_int(wal_max_frame as i64); // 3rd col: # pages moved to db after checkpoint state.registers[*dest + 2].set_int(wal_total_backfilled as i64); state.pc += 1; Ok(InsnFunctionStepResult::Step) } Ok(IOResult::IO(io)) => Ok(InsnFunctionStepResult::IO(io)), Err(err) => { tracing::debug!("PRAGMA wal_checkpoint failed: {err:?}"); pager.clear_checkpoint_state(); state.registers[*dest].set_int(1); state.pc += 1; Ok(InsnFunctionStepResult::Step) } } } pub fn op_null( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { match insn { Insn::Null { dest, dest_end } | Insn::BeginSubrtn { dest, dest_end } => { if let Some(dest_end) = dest_end { for i in *dest..=*dest_end { state.registers[i].set_null(); // Clear any associated RowSet so it can be reused in a fresh // state. In SQLite the RowSet lives inside the register and // is destroyed by OP_Null; we keep RowSets in a side map, so // we must remove them explicitly. state.rowsets.remove(&i); } } else { state.registers[*dest].set_null(); state.rowsets.remove(dest); } } _ => { mark_unlikely(); unreachable!("unexpected Insn {:?}", insn) } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_null_row( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(NullRow { cursor_id }, insn); state.get_cursor(*cursor_id).set_null_flag(true); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_compare( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Compare { start_reg_a, start_reg_b, count, key_info, }, insn ); let start_reg_a = *start_reg_a; let start_reg_b = *start_reg_b; let count = *count; if unlikely(start_reg_a + count > start_reg_b) { return Err(LimboError::InternalError( "Compare registers overlap".to_string(), )); } // (https://github.com/tursodatabase/turso/issues/2304): reusing logic from compare_immutable(). // TODO: There are tons of cases like this where we could reuse this in a similar vein let a_range = (start_reg_a..start_reg_a + count + 1).map(|idx| state.registers[idx].get_value()); let b_range = (start_reg_b..start_reg_b + count + 1).map(|idx| state.registers[idx].get_value()); let cmp = compare_immutable(a_range, b_range, key_info); state.last_compare = Some(cmp); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_jump( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Jump { target_pc_lt, target_pc_eq, target_pc_gt, }, insn ); assert!(target_pc_lt.is_offset()); assert!(target_pc_eq.is_offset()); assert!(target_pc_gt.is_offset()); let cmp = state.last_compare.take(); if unlikely(cmp.is_none()) { return Err(LimboError::InternalError( "Jump without compare".to_string(), )); } let target_pc = match cmp.expect("comparison should succeed for valid operands") { std::cmp::Ordering::Less => *target_pc_lt, std::cmp::Ordering::Equal => *target_pc_eq, std::cmp::Ordering::Greater => *target_pc_gt, }; state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } pub fn op_move( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Move { source_reg, dest_reg, count, }, insn ); let source_reg = *source_reg; let dest_reg = *dest_reg; let count = *count; for i in 0..count { state.registers[dest_reg + i] = std::mem::replace( &mut state.registers[source_reg + i], Register::Value(Value::Null), ); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_if_pos( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( IfPos { reg, target_pc, decrement_by, }, insn ); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } let reg = *reg; let target_pc = *target_pc; match state.registers[reg].get_value() { Value::Numeric(Numeric::Integer(n)) if *n > 0 => { state.pc = target_pc.as_offset_int(); state.registers[reg].set_int(*n - *decrement_by as i64); } Value::Numeric(Numeric::Integer(_)) => { state.pc += 1; } _ => { mark_unlikely(); return Err(LimboError::InternalError( "IfPos: the value in the register is not an integer".into(), )); } } Ok(InsnFunctionStepResult::Step) } pub fn op_not_null( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(NotNull { reg, target_pc }, insn); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } let reg = *reg; let target_pc = *target_pc; match &state.registers[reg].get_value() { Value::Null => { state.pc += 1; } _ => { state.pc = target_pc.as_offset_int(); } } Ok(InsnFunctionStepResult::Step) } pub fn op_comparison( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { let (lhs, rhs, target_pc, flags, collation, op) = match insn { Insn::Eq { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Eq, ), Insn::Ne { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Ne, ), Insn::Lt { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Lt, ), Insn::Le { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Le, ), Insn::Gt { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Gt, ), Insn::Ge { lhs, rhs, target_pc, flags, collation, } => ( *lhs, *rhs, *target_pc, *flags, collation.unwrap_or_default(), ComparisonOp::Ge, ), _ => unreachable!("unexpected Insn {:?}", insn), }; if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } let null_eq = flags.has_nulleq(); let jump_if_null = flags.has_jump_if_null(); let affinity = flags.get_affinity(); let lhs_value = state.registers[lhs].get_value(); let rhs_value = state.registers[rhs].get_value(); // Fast path for integers if matches!(lhs_value, Value::Numeric(Numeric::Integer(_))) && matches!(rhs_value, Value::Numeric(Numeric::Integer(_))) { if op.compare(lhs_value, rhs_value, collation) { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } return Ok(InsnFunctionStepResult::Step); } // Handle NULL values if matches!(lhs_value, Value::Null) || matches!(rhs_value, Value::Null) { let cmp_res = op.compare_nulls(lhs_value, rhs_value, null_eq); let jump = match op { ComparisonOp::Eq => cmp_res || (!null_eq && jump_if_null), ComparisonOp::Ne => cmp_res || (!null_eq && jump_if_null), ComparisonOp::Lt | ComparisonOp::Le | ComparisonOp::Gt | ComparisonOp::Ge => { jump_if_null } }; if jump { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } return Ok(InsnFunctionStepResult::Step); } // Element-wise array comparison when ARRAY_CMP flag is set if flags.has_array_cmp() { if let (Value::Blob(lb), Value::Blob(rb)) = (lhs_value, rhs_value) { if let Ok(ord) = compare_arrays(lb, rb) { let should_jump = match op { ComparisonOp::Eq => ord.is_eq(), ComparisonOp::Ne => !ord.is_eq(), ComparisonOp::Lt => ord.is_lt(), ComparisonOp::Le => ord.is_le(), ComparisonOp::Gt => ord.is_gt(), ComparisonOp::Ge => ord.is_ge(), }; if should_jump { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } return Ok(InsnFunctionStepResult::Step); } } } let (new_lhs, new_rhs) = (affinity.convert(lhs_value), affinity.convert(rhs_value)); let should_jump = op.compare( new_lhs .as_ref() .map_or(Either::Left(lhs_value), Either::Right), new_rhs .as_ref() .map_or(Either::Left(rhs_value), Either::Right), collation, ); match (new_lhs, new_rhs) { (Some(new_lhs), None) => { state.registers[lhs].set_value(new_lhs.as_value_ref().to_owned()); } (None, Some(new_rhs)) => { state.registers[rhs].set_value(new_rhs.as_value_ref().to_owned()); } (Some(new_lhs), Some(new_rhs)) => { let (new_lhs, new_rhs) = ( new_lhs.as_value_ref().to_owned(), new_rhs.as_value_ref().to_owned(), ); state.registers[lhs].set_value(new_lhs); state.registers[rhs].set_value(new_rhs); } (None, None) => {} } if should_jump { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_if( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( If { reg, target_pc, jump_if_null, }, insn ); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } if state.registers[*reg] .get_value() .exec_if(*jump_if_null, false) { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_if_not( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( IfNot { reg, target_pc, jump_if_null, }, insn ); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } if state.registers[*reg] .get_value() .exec_if(*jump_if_null, true) { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_open_read( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( OpenRead { cursor_id, root_page, db, }, insn ); let pager = program.get_pager_from_database_index(db); let mv_store = program.connection.mv_store_for_db(*db); if let (_, CursorType::IndexMethod(module)) = &program.cursor_ref[*cursor_id] { if state.cursors[*cursor_id].is_none() { let cursor = module.init()?; let cursor_ref = &mut state.cursors[*cursor_id]; *cursor_ref = Some(Cursor::IndexMethod(cursor)); } let cursor = state.cursors[*cursor_id] .as_mut() .expect("cursor should exist after initialization"); let cursor = cursor.as_index_method_mut(); return_if_io!(cursor.open_read(&program.connection)); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let (_, cursor_type) = program .cursor_ref .get(*cursor_id) .expect("cursor_id should exist in cursor_ref"); if program.connection.get_mv_tx_id_for_db(*db).is_none() { assert!( *root_page >= 0, "root page should be non negative when we are not in a MVCC transaction" ); } let cursors = &mut state.cursors; let num_columns = match cursor_type { CursorType::BTreeTable(table_rc) => table_rc.columns.len(), CursorType::BTreeIndex(index_arc) => index_arc.columns.len(), CursorType::MaterializedView(table_rc, _) => table_rc.columns.len(), _ => unreachable!("This should not have happened"), }; let maybe_promote_to_mvcc_cursor = |btree_cursor: Box, mv_cursor_type: MvccCursorType| -> Result> { if let Some(tx_id) = program.connection.get_mv_tx_id_for_db(*db) { let mv_store = mv_store .as_ref() .expect("mv_store should be Some when MVCC transaction is active") .clone(); Ok(Box::new(MvCursor::new( mv_store, tx_id, *root_page, mv_cursor_type, btree_cursor, )?)) } else { Ok(btree_cursor) } }; match cursor_type { CursorType::MaterializedView(_, view_mutex) => { // This is a materialized view with storage // Create btree cursor for reading the persistent data let btree_cursor = Box::new(BTreeCursor::new_table( pager.clone(), maybe_transform_root_page_to_positive(mv_store.as_ref(), *root_page), num_columns, )); let cursor = maybe_promote_to_mvcc_cursor(btree_cursor, MvccCursorType::Table)?; // Get the view name and look up or create its transaction state let view_name = view_mutex.lock().name().to_string(); let tx_state = program .connection .view_transaction_states .get_or_create(&view_name); // Create materialized view cursor with this view's transaction state let mv_cursor = crate::incremental::cursor::MaterializedViewCursor::new( cursor, view_mutex.clone(), pager, tx_state, )?; cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") .replace(Cursor::new_materialized_view(mv_cursor)); } CursorType::BTreeTable(_) => { // Regular table let btree_cursor = Box::new(BTreeCursor::new_table( pager, maybe_transform_root_page_to_positive(mv_store.as_ref(), *root_page), num_columns, )); let cursor = maybe_promote_to_mvcc_cursor(btree_cursor, MvccCursorType::Table)?; cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") .replace(Cursor::new_btree(cursor)); } CursorType::BTreeIndex(index) => { let btree_cursor = Box::new(BTreeCursor::new_index( pager, *root_page, index.as_ref(), num_columns, )); let index_info = Arc::new(IndexInfo::new_from_index(index)); let cursor = maybe_promote_to_mvcc_cursor(btree_cursor, MvccCursorType::Index(index_info))?; cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") .replace(Cursor::new_btree(cursor)); } CursorType::Pseudo(_) => { panic!("OpenRead on pseudo cursor"); } CursorType::Sorter => { panic!("OpenRead on sorter cursor"); } CursorType::IndexMethod(..) => { unreachable!("IndexMethod handled above") } CursorType::VirtualTable(_) => { panic!("OpenRead on virtual table cursor, use Insn:VOpen instead"); } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vopen( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(VOpen { cursor_id }, insn); let (_, cursor_type) = program .cursor_ref .get(*cursor_id) .expect("cursor_id should exist in cursor_ref"); let CursorType::VirtualTable(virtual_table) = cursor_type else { panic!("VOpen on non-virtual table cursor"); }; let cursor = virtual_table.open(program.connection.clone())?; state .cursors .get_mut(*cursor_id) .unwrap_or_else(|| panic!("cursor id {} out of bounds", *cursor_id)) .replace(Cursor::Virtual(cursor)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vcreate( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( VCreate { module_name, table_name, args_reg, }, insn ); let module_name = state.registers[*module_name].get_value().to_string(); let table_name = state.registers[*table_name].get_value().to_string(); let args = if let Some(args_reg) = args_reg { if let Register::Record(rec) = &state.registers[*args_reg] { rec.iter()? .map(|v| v.map(|v| v.to_ffi())) .collect::>()? } else { mark_unlikely(); return Err(LimboError::InternalError( "VCreate: args_reg is not a record".to_string(), )); } } else { vec![] }; let conn = program.connection.clone(); let table = crate::VirtualTable::table(Some(&table_name), &module_name, args, &conn.syms.read())?; { conn.syms.write().vtabs.insert(table_name, table); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vfilter( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( VFilter { cursor_id, pc_if_empty, arg_count, args_reg, idx_str, idx_num, }, insn ); let has_rows = { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_virtual_mut(); let mut args = Vec::with_capacity(*arg_count); for i in 0..*arg_count { args.push(state.registers[args_reg + i].get_value().clone()); } let idx_str = if let Some(idx_str) = idx_str { Some(state.registers[*idx_str].get_value().to_string()) } else { None }; cursor.filter(*idx_num as i32, idx_str, *arg_count, args)? }; // Increment filter_operations metric for virtual table filter state.metrics.filter_operations = state.metrics.filter_operations.saturating_add(1); if !has_rows { state.pc = pc_if_empty.as_offset_int(); } else { // VFilter positions to the first row if any exist, which counts as a read state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_vcolumn( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( VColumn { cursor_id, column, dest, }, insn ); let value = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_virtual_mut(); cursor.column(*column)? }; state.registers[*dest].set_value(value); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vupdate( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { #[cfg(not(feature = "cli_only"))] let _ = pager; load_insn!( VUpdate { cursor_id, arg_count, start_reg, conflict_action, .. }, insn ); let (_, cursor_type) = program .cursor_ref .get(*cursor_id) .expect("cursor_id should exist in cursor_ref"); let CursorType::VirtualTable(virtual_table) = cursor_type else { panic!("VUpdate on non-virtual table cursor"); }; let allow_dbpage_write = { #[cfg(feature = "cli_only")] { virtual_table.name == crate::dbpage::DBPAGE_TABLE_NAME && program.connection.db.opts.unsafe_testing } #[cfg(not(feature = "cli_only"))] { false } }; if virtual_table.readonly() && !allow_dbpage_write { return Err(LimboError::ReadOnly); } if unlikely(*arg_count < 2) { return Err(LimboError::InternalError( "VUpdate: arg_count must be at least 2 (rowid and insert_rowid)".to_string(), )); } let mut argv = Vec::with_capacity(*arg_count); for i in 0..*arg_count { if let Some(value) = state.registers.get(*start_reg + i) { argv.push(value.get_value().clone()); } else { mark_unlikely(); return Err(LimboError::InternalError(format!( "VUpdate: register out of bounds at {}", *start_reg + i ))); } } let result = if allow_dbpage_write { #[cfg(feature = "cli_only")] { crate::dbpage::update_dbpage(pager, &argv) } #[cfg(not(feature = "cli_only"))] { unreachable!("sqlite_dbpage writes require cli_only feature"); } } else { virtual_table.update(&argv) }; match result { Ok(Some(new_rowid)) => { if *conflict_action == 5 { // ResolveType::Replace program.connection.update_last_rowid(new_rowid); } state.pc += 1; } Ok(None) => { // no-op or successful update without rowid return state.pc += 1; } Err(e) => { // virtual table update failed mark_unlikely(); return Err(LimboError::ExtensionError(format!( "Virtual table update failed: {e}" ))); } } Ok(InsnFunctionStepResult::Step) } pub fn op_vnext( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( VNext { cursor_id, pc_if_next, }, insn ); let has_more = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_virtual_mut(); cursor.next()? }; if has_more { // Increment metrics for row read from virtual table (including materialized views) state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.pc = pc_if_next.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_vdestroy( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(VDestroy { db: _, table_name }, insn); let conn = program.connection.clone(); { let Some(vtab) = conn.syms.write().vtabs.remove(table_name) else { mark_unlikely(); return Err(crate::LimboError::InternalError( "Could not find Virtual Table to Destroy".to_string(), )); }; vtab.destroy()?; } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vbegin( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(VBegin { cursor_id }, insn); let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_virtual_mut(); let vtab_id = cursor .vtab_id() .expect("VBegin on non ext-virtual table cursor"); let mut states = program.connection.vtab_txn_states.write(); if states.insert(vtab_id) { // Only begin a new transaction if one is not already active for this virtual table module let vtabs = &program.connection.syms.read().vtabs; let vtab = vtabs .iter() .find(|p| p.1.id().eq(&vtab_id)) .expect("Could not find virtual table for VBegin"); vtab.1.begin()?; } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_vrename( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( VRename { cursor_id, new_name_reg }, insn ); let name = state.registers[*new_name_reg].get_value().to_string(); let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_virtual_mut(); let vtabs = &program.connection.syms.read().vtabs; let vtab = vtabs .iter() .find(|p| { p.1.id().eq(&cursor .vtab_id() .expect("non ext-virtual table used in VRollback")) }) .expect("Could not find virtual table for VRollback"); vtab.1.rename(&name)?; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_open_pseudo( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( OpenPseudo { cursor_id, content_reg: _, num_fields: _, }, insn ); { let cursors = &mut state.cursors; let cursor = PseudoCursor::default(); cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") .replace(Cursor::new_pseudo(cursor)); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_rewind( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Rewind { cursor_id, pc_if_empty, }, insn ); assert!(pc_if_empty.is_offset()); // Clear any bloom filter associated with this cursor so stale filter data // does not incorrectly reject valid matches in subsequent iterations. if let Some(filter) = state.get_bloom_filter_mut(*cursor_id) { filter.clear(); } let is_empty = { let cursor = state.get_cursor(*cursor_id); match cursor { Cursor::BTree(btree_cursor) => { return_if_io!(btree_cursor.rewind()); btree_cursor.is_empty() } Cursor::MaterializedView(mv_cursor) => { return_if_io!(mv_cursor.rewind()); !mv_cursor.is_valid()? } _ => panic!("Rewind on non-btree/materialized-view cursor"), } }; if is_empty { state.pc = pc_if_empty.as_offset_int(); } else { // Rewind positions to the first row, which is effectively a read state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_last( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Last { cursor_id, pc_if_empty, }, insn ); assert!(pc_if_empty.is_offset()); let is_empty = { let cursor = must_be_btree_cursor!(*cursor_id, program.cursor_ref, state, "Last"); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.last()); cursor.is_empty() }; if is_empty { state.pc = pc_if_empty.as_offset_int(); } else { // Last positions to the last row, which is effectively a read state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.pc += 1; } Ok(InsnFunctionStepResult::Step) } #[derive(Debug, Clone, Copy)] pub enum OpColumnState { Start, Rowid { index_cursor_id: usize, table_cursor_id: usize, }, Seek { rowid: i64, table_cursor_id: usize, }, GetColumn, } pub fn op_column( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Column { cursor_id, column, dest, default, }, insn ); 'outer: loop { match state.op_column_state { OpColumnState::Start => { if let Some(deferred) = state.deferred_seeks[*cursor_id].take() { state.op_column_state = OpColumnState::Rowid { index_cursor_id: deferred.index_cursor_id, table_cursor_id: deferred.table_cursor_id, }; } else { state.op_column_state = OpColumnState::GetColumn; } } OpColumnState::Rowid { index_cursor_id, table_cursor_id, } => { let Some(rowid) = ({ let index_cursor = state.get_cursor(index_cursor_id); match index_cursor { Cursor::BTree(cursor) => return_if_io!(cursor.rowid()), Cursor::IndexMethod(cursor) => return_if_io!(cursor.query_rowid()), _ => panic!("unexpected cursor type"), } }) else { state.registers[*dest].set_null(); break 'outer; }; state.op_column_state = OpColumnState::Seek { rowid, table_cursor_id, }; } OpColumnState::Seek { rowid, table_cursor_id, } => { { let table_cursor = state.get_cursor(table_cursor_id); // MaterializedView cursors shouldn't go through deferred seek logic // but if we somehow get here, handle it appropriately match table_cursor { Cursor::MaterializedView(mv_cursor) => { // Seek to the rowid in the materialized view return_if_io!(mv_cursor .seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true })); } _ => { // Regular btree cursor let table_cursor = table_cursor.as_btree_mut(); return_if_io!(table_cursor .seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true })); } } } state.op_column_state = OpColumnState::GetColumn; } OpColumnState::GetColumn => { let (active_cursor_id, active_column) = (*cursor_id, *column); // First check if this is a MaterializedViewCursor { let cursor = state.get_cursor(active_cursor_id); if let Cursor::MaterializedView(mv_cursor) = cursor { // Handle materialized view column access let value = return_if_io!(mv_cursor.column(active_column)); state.registers[*dest].set_value(value); break 'outer; } // Fall back to normal handling } let (_, cursor_type) = program .cursor_ref .get(active_cursor_id) .expect("cursor_id should exist in cursor_ref"); match cursor_type { CursorType::BTreeTable(_) | CursorType::BTreeIndex(_) | CursorType::MaterializedView(_, _) => { { let cursor_ref = must_be_btree_cursor!( active_cursor_id, program.cursor_ref, state, "Column" ); let cursor = cursor_ref.as_btree_mut(); if cursor.get_null_flag() { tracing::trace!("op_column(null_flag)"); state.registers[*dest].set_null(); break 'outer; } let record_result = return_if_io!(cursor.record()); let Some(record) = record_result else { // Cursor is not positioned on a valid row (e.g., empty table). // Return NULL, not the column's default value. // DEFAULT handling below is for when record exists // but has fewer columns than expected. state.registers[*dest].set_null(); break 'outer; }; let mut payload_iterator = record.iter()?; // Parse the header for serial types incrementally until we have the target column // Use nth_into_register to write directly to the register without // creating intermediate ValueRef allocations use crate::vdbe::ValueIteratorExt; match payload_iterator .nth_into_register(*column, &mut state.registers[*dest]) { Some(result) => { result?; break 'outer; } None => { branches::mark_unlikely(); // record has fewer columns than expected } }; //break; }; // DEFAULT handling let Some(ref default) = default else { state.registers[*dest].set_null(); break; }; match (default, &mut state.registers[*dest]) { ( Value::Text(new_text), Register::Value(Value::Text(existing_text)), ) => { existing_text.do_extend(new_text); } ( Value::Blob(new_blob), Register::Value(Value::Blob(existing_blob)), ) => { existing_blob.do_extend(new_blob); } _ => { state.registers[*dest].set_value(default.clone()); } } break; } CursorType::Sorter => { let record = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_sorter_mut(); cursor.record().cloned() }; if let Some(record) = record { state.registers[*dest].set_value(match record.get_value_opt(*column) { Some(val) => val.to_owned(), None => default.clone().unwrap_or(Value::Null), }); } else { state.registers[*dest].set_null(); } } CursorType::Pseudo(_) => { let value = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_pseudo_mut(); cursor.get_value(*column)? }; state.registers[*dest].set_value(value); } CursorType::IndexMethod(..) => { let cursor = state.cursors[*cursor_id] .as_mut() .expect("cursor should exist"); let cursor = cursor.as_index_method_mut(); let value = return_if_io!(cursor.query_column(*column)); state.registers[*dest].set_value(value); } CursorType::VirtualTable(_) => { panic!("Insn:Column on virtual table cursor, use Insn:VColumn instead"); } } break; } } } state.op_column_state = OpColumnState::Start; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_type_check( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( TypeCheck { start_reg, count, check_generated: _, table_reference, }, insn ); assert!(table_reference.is_strict); state.registers[*start_reg..*start_reg + *count] .iter_mut() .zip(table_reference.columns.iter()) .try_for_each(|(reg, col)| { // INT PRIMARY KEY is not row_id_alias so we throw error if this col is NULL if !col.is_rowid_alias() && col.primary_key() && matches!(reg.get_value(), Value::Null) { bail_constraint_error!( "NOT NULL constraint failed: {}.{} ({})", &table_reference.name, col.name.as_deref().unwrap_or(""), SQLITE_CONSTRAINT ) } else if col.is_rowid_alias() && matches!(reg.get_value(), Value::Null) { // Handle INTEGER PRIMARY KEY for null as usual (Rowid will be auto-assigned) return Ok(()); } else if matches!(reg.get_value(), Value::Null) { // STRICT only enforces type affinity on non-NULL values. // NULL is valid in any column without NOT NULL constraint. return Ok(()); } let ty_str = &col.ty_str; let ty_bytes = ty_str.as_bytes(); let is_builtin_type = turso_macros::match_ignore_ascii_case!(match ty_bytes { b"ANY" | b"INTEGER" | b"INT" | b"REAL" | b"BLOB" | b"TEXT" => true, _ => false, }); if is_builtin_type { match_ignore_ascii_case!(match ty_bytes { b"ANY" => {} _ => { let col_affinity = col.affinity(); let _applied = apply_affinity_char(reg, col_affinity); let value_type = reg.get_value().value_type(); match_ignore_ascii_case!(match ty_bytes { b"INTEGER" | b"INT" if value_type == ValueType::Integer => {} b"REAL" if value_type == ValueType::Float => {} b"BLOB" if value_type == ValueType::Blob => {} b"TEXT" if value_type == ValueType::Text => {} _ => bail_constraint_error!( "cannot store {} value in {} column {}.{} ({})", value_type, ty_str, &table_reference.name, col.name.as_deref().unwrap_or(""), SQLITE_CONSTRAINT ), }); } }); } // Custom types: skip type check — encode function validates Ok(()) })?; state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Parse an array input (JSON text or record blob), validate/coerce each element /// against the declared element type using STRICT type-checking logic, then /// serialize to a native record-format BLOB for storage. pub fn op_array_encode( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( ArrayEncode { reg, element_affinity, element_type, table_name, col_name, }, insn ); let val = state.registers[*reg].get_value(); if matches!(val, Value::Null) { state.pc += 1; return Ok(InsnFunctionStepResult::Step); } // Fast path: blob input with ANY type — validate it is a well-formed record // before accepting. This avoids the redundant extract→reserialize when MakeArray // output feeds directly into ArrayEncode with ANY affinity, but rejects raw blobs // (e.g. zeroblob, X'DEADBEEF') that would crash on read. if let Value::Blob(b) = val { if element_type.eq_ignore_ascii_case("ANY") { // Validate blob is a well-formed record using streaming iterator // (no Vec allocation). Rejects empty blobs and invalid records. let valid = !b.is_empty() && ValueIterator::new(b) .map(|iter| iter.into_iter().all(|r| r.is_ok())) .unwrap_or(false); if !valid { bail_constraint_error!( "cannot store non-array value in {} column {}.{} ({})", element_type, table_name, col_name, SQLITE_CONSTRAINT ); } state.pc += 1; return Ok(InsnFunctionStepResult::Step); } } // Extract elements from either blob (MakeArray) or text (JSON literal) let raw_elements = match val { Value::Blob(b) => array_values_from_blob(b).ok(), Value::Text(text) => parse_text_array(text.as_str()), _ => None, }; let Some(raw_elements) = raw_elements else { bail_constraint_error!( "cannot store non-array value in {} column {}.{} ({})", element_type, table_name, col_name, SQLITE_CONSTRAINT ); }; const MAX_ARRAY_ELEMENTS: usize = 100_000; if raw_elements.len() > MAX_ARRAY_ELEMENTS { bail_constraint_error!( "array exceeds maximum element count ({MAX_ARRAY_ELEMENTS}) for column {table_name}.{col_name} ({SQLITE_CONSTRAINT})" ); } let mut coerced_elements: Vec = Vec::with_capacity(raw_elements.len()); for elem in raw_elements { // NULL elements are allowed — same as STRICT allows NULL in columns if matches!(elem, Value::Null) { coerced_elements.push(elem); continue; } // Apply affinity coercion — same as STRICT's TypeCheck let mut reg_tmp = Register::Value(elem); apply_affinity_char(&mut reg_tmp, *element_affinity); let coerced = reg_tmp.get_value().clone(); // Check value type matches the declared element type — same as STRICT's TypeCheck let value_type = coerced.value_type(); let ty_bytes = element_type.as_bytes(); let type_ok = turso_macros::match_ignore_ascii_case!(match ty_bytes { b"ANY" => true, b"INTEGER" | b"INT" => value_type == ValueType::Integer, b"REAL" => value_type == ValueType::Float, b"TEXT" => value_type == ValueType::Text, b"BLOB" => value_type == ValueType::Blob, _ => true, // custom types validated by their own encode }); if !type_ok { bail_constraint_error!( "cannot store {} value in {} ({})", value_type, element_type, SQLITE_CONSTRAINT ); } coerced_elements.push(coerced); } // Serialize coerced elements as a native record-format BLOB let record = ImmutableRecord::from_values(&coerced_elements, coerced_elements.len()); state.registers[*reg].set_blob(record.into_payload()); state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Convert a native record-format array BLOB to PG text representation for display. pub fn op_array_decode( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(ArrayDecode { reg }, insn); let val = state.registers[*reg].get_value(); if matches!(val, Value::Null) { state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let text = match val { Value::Blob(b) if b.is_empty() => "{}".to_string(), Value::Blob(b) => serialize_array_from_blob(b)?, _ => { // Not a blob — leave as-is (might be text from a function result) state.pc += 1; return Ok(InsnFunctionStepResult::Step); } }; state.registers[*reg].set_text(Text::new(text)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Access element at index from a record-format array BLOB. pub fn op_array_element( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( ArrayElement { array_reg, index_reg, dest, }, insn ); let arr_val = state.registers[*array_reg].get_value(); if matches!(arr_val, Value::Null) { state.registers[*dest].set_null(); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let idx = match state.registers[*index_reg].get_value() { Value::Numeric(Numeric::Integer(i)) if *i >= 1 => (*i - 1) as usize, _ => { // Non-positive, non-integer, or NULL index → NULL result (PG convention: 1-based) state.registers[*dest].set_null(); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } }; let result = match arr_val { Value::Blob(blob) => match ValueIterator::new(blob) { Ok(mut iter) => iter .nth(idx) .and_then(|r| r.ok()) .map(|vref| { // The blob may not be a real record — text fields could // contain invalid UTF-8 (from_utf8_unchecked in the // record decoder). Validate and demote to blob if needed. if let ValueRef::Text(t) = &vref { if t.value.as_bytes().iter().any(|&b| b > 0x7F) && std::str::from_utf8(t.value.as_bytes()).is_err() { return Value::Blob(t.value.as_bytes().to_vec()); } } vref.to_owned() }) .unwrap_or(Value::Null), Err(_) => Value::Null, }, _ => Value::Null, }; state.registers[*dest].set_value(result); state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Get the number of elements in a record-format array BLOB. pub fn op_array_length( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(ArrayLength { reg, dest }, insn); let val = state.registers[*reg].get_value(); match compute_array_length(val) { Some(count) => state.registers[*dest].set_int(count), None => state.registers[*dest].set_null(), }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Create an array from contiguous registers as a record-format BLOB. pub fn op_make_array( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( MakeArray { start_reg, count, dest }, insn ); let end = start_reg .checked_add(*count) .ok_or_else(|| LimboError::InternalError("MakeArray: register range overflow".into()))?; if end > state.registers.len() { return Err(LimboError::InternalError(format!( "MakeArray: register range {}..{} exceeds register file size {}", start_reg, end, state.registers.len() ))); } state.registers[*dest].set_value(make_array_from_registers( &state.registers, *start_reg, *count, )); state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Create an array from contiguous registers with dynamic count. pub fn op_make_array_dynamic( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( MakeArrayDynamic { start_reg, count_reg, dest }, insn ); let count = match state.registers[*count_reg].get_value() { Value::Numeric(Numeric::Integer(n)) if *n >= 0 => *n as usize, _ => 0, }; let end = start_reg.checked_add(count).ok_or_else(|| { LimboError::InternalError("MakeArrayDynamic: register range overflow".into()) })?; if end > state.registers.len() { return Err(LimboError::InternalError(format!( "MakeArrayDynamic: register range {}..{} exceeds register file size {}", start_reg, end, state.registers.len() ))); } state.registers[*dest].set_value(make_array_from_registers( &state.registers, *start_reg, count, )); state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Copy a register value to a dynamically-computed destination register. pub fn op_reg_copy_offset( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( RegCopyOffset { src, base, offset_reg }, insn ); let offset = match state.registers[*offset_reg].get_value() { Value::Numeric(Numeric::Integer(n)) if *n >= 0 => *n as usize, _ => 0, }; let dest = *base + offset; if dest >= state.registers.len() { return Err(LimboError::InternalError(format!( "RegCopyOffset: destination register {} out of bounds (max {})", dest, state.registers.len() ))); } state.registers[dest] = state.registers[*src].clone(); state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Concatenate/append/prepend arrays. Runtime dispatch based on operand types. pub fn op_array_concat( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(ArrayConcat { lhs, rhs, dest }, insn); // Check NULL before cloning to avoid unnecessary allocation let lhs_ref = state.registers[*lhs].get_value(); let rhs_ref = state.registers[*rhs].get_value(); // PG-compatible NULL handling for arrays: // array || NULL = array, NULL || array = array, NULL || NULL = NULL if matches!(lhs_ref, Value::Null) && matches!(rhs_ref, Value::Null) { state.registers[*dest].set_null(); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } if matches!(lhs_ref, Value::Null) { state.registers[*dest].set_value(rhs_ref.clone()); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } if matches!(rhs_ref, Value::Null) { state.registers[*dest].set_value(lhs_ref.clone()); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let result = match (lhs_ref, rhs_ref) { (Value::Blob(lb), Value::Blob(rb)) => { let mut elems_a = array_values_from_blob(lb)?; let elems_b = array_values_from_blob(rb)?; elems_a.extend(elems_b); values_to_record_blob(&elems_a) } (Value::Blob(lb), _) => { let mut elems = array_values_from_blob(lb)?; elems.push(rhs_ref.clone()); values_to_record_blob(&elems) } (_, Value::Blob(rb)) => { let mut elems = array_values_from_blob(rb)?; elems.insert(0, lhs_ref.clone()); values_to_record_blob(&elems) } _ => { // Neither is an array blob — fall back to string concat Value::build_text(format!("{lhs_ref}{rhs_ref}")) } }; state.registers[*dest].set_value(result); state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Set element at index in a record-format array BLOB. pub fn op_array_set_element( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( ArraySetElement { array_reg, index_reg, value_reg, dest, }, insn ); let arr_val = state.registers[*array_reg].get_value(); if matches!(arr_val, Value::Null) { state.registers[*dest].set_null(); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let idx = match state.registers[*index_reg].get_value() { Value::Numeric(Numeric::Integer(i)) if *i >= 1 => (*i - 1) as usize, _ => { // Invalid index (non-positive, non-integer): preserve original array (PG: 1-based) state.registers[*dest].set_value(arr_val.clone()); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } }; let new_val = state.registers[*value_reg].get_value().clone(); let Value::Blob(blob) = arr_val else { return Err(LimboError::InternalError( "ArraySetElement: expected blob array".into(), )); }; let mut elements = array_values_from_blob(blob)?; if idx >= elements.len() { // Out-of-bounds: preserve original array unchanged state.registers[*dest].set_blob(blob.clone()); } else { elements[idx] = new_val; state.registers[*dest].set_value(values_to_record_blob(&elements)); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Extract a subslice of elements from a record-format array BLOB. pub fn op_array_slice( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( ArraySlice { array_reg, start_reg, end_reg, dest, }, insn ); let arr_val = state.registers[*array_reg].get_value().clone(); let start_val = state.registers[*start_reg].get_value().clone(); let end_val = state.registers[*end_reg].get_value().clone(); let result = exec_array_slice(&arr_val, &start_val, &end_val); state.registers[*dest].set_value(result); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_make_record( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( MakeRecord { start_reg, count, dest_reg, affinity_str, .. }, insn ); let start_reg = *start_reg as usize; let count = *count as usize; let dest_reg = *dest_reg as usize; if let Some(affinity_str) = affinity_str { if unlikely(affinity_str.len() != count) { return Err(LimboError::InternalError(format!( "MakeRecord: the length of affinity string ({}) does not match the count ({})", affinity_str.len(), count ))); } for (i, affinity_ch) in affinity_str.chars().enumerate().take(count) { let reg_index = start_reg + i; let affinity = Affinity::from_char(affinity_ch); apply_affinity_char(&mut state.registers[reg_index], affinity); } } let record = make_record(&state.registers, &start_reg, &count); state.registers[dest_reg] = Register::Record(record); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_mem_max( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(MemMax { dest_reg, src_reg }, insn); let dest_val = state.registers[*dest_reg].get_value(); let src_val = state.registers[*src_reg].get_value(); let dest_int = extract_int_value(dest_val); let src_int = extract_int_value(src_val); if dest_int < src_int { state.registers[*dest_reg].set_int(src_int); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_result_row( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(ResultRow { start_reg, count }, insn); let row = Row { values: &state.registers[*start_reg] as *const Register, count: *count, }; state.result_row = Some(row); state.pc += 1; Ok(InsnFunctionStepResult::Row) } pub fn op_next( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Next { cursor_id, pc_if_next, }, insn ); assert!(pc_if_next.is_offset()); let is_empty = { let cursor = state.get_cursor(*cursor_id); match cursor { Cursor::BTree(btree_cursor) => { // If cursor is in NullRow state, don't advance - just return empty. // This matches SQLite's OP_Next behavior: btreeNext() returns // SQLITE_DONE when eState==CURSOR_INVALID (NullRow calls // sqlite3BtreeClearCursor which sets CURSOR_INVALID). let is_null_row = btree_cursor.get_null_flag(); btree_cursor.set_null_flag(false); if is_null_row { true // is_empty = true } else { return_if_io!(btree_cursor.next()); btree_cursor.is_empty() } } Cursor::MaterializedView(mv_cursor) => { let has_more = return_if_io!(mv_cursor.next()); !has_more } Cursor::IndexMethod(_) => { let cursor = cursor.as_index_method_mut(); let has_more = return_if_io!(cursor.query_next()); !has_more } _ => panic!("Next on non-btree/materialized-view cursor"), } }; if !is_empty { // Increment metrics for row read state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.metrics.btree_next = state.metrics.btree_next.saturating_add(1); // Track if this is a full table scan or index scan if let Some((_, cursor_type)) = program.cursor_ref.get(*cursor_id) { if cursor_type.is_index() { state.metrics.index_steps = state.metrics.index_steps.saturating_add(1); } else if matches!(cursor_type, CursorType::BTreeTable(_)) { state.metrics.fullscan_steps = state.metrics.fullscan_steps.saturating_add(1); } } state.pc = pc_if_next.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_prev( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Prev { cursor_id, pc_if_prev, }, insn ); assert!(pc_if_prev.is_offset()); let is_empty = { let cursor = must_be_btree_cursor!(*cursor_id, program.cursor_ref, state, "Prev"); let cursor = cursor.as_btree_mut(); // If cursor is in NullRow state, don't advance - just return empty. // This matches SQLite's OP_Prev behavior which checks nullRow first. let is_null_row = cursor.get_null_flag(); cursor.set_null_flag(false); if is_null_row { true // is_empty = true } else { return_if_io!(cursor.prev()); cursor.is_empty() } }; if !is_empty { // Increment metrics for row read state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.metrics.btree_prev = state.metrics.btree_prev.saturating_add(1); // Track if this is a full table scan or index scan if let Some((_, cursor_type)) = program.cursor_ref.get(*cursor_id) { if cursor_type.is_index() { state.metrics.index_steps = state.metrics.index_steps.saturating_add(1); } else if matches!(cursor_type, CursorType::BTreeTable(_)) { state.metrics.fullscan_steps = state.metrics.fullscan_steps.saturating_add(1); } } state.pc = pc_if_prev.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn halt( program: &Program, state: &mut ProgramState, pager: &Arc, err_code: usize, description: &str, on_error: Option, ) -> Result { let mv_store = program.connection.mv_store(); let auto_commit = program.connection.auto_commit.load(Ordering::SeqCst); // Check if we're resuming from a FAIL commit I/O wait. // If pending_fail_error is set, we were in the middle of committing partial changes // for FAIL mode and need to continue the commit, then return the stored error. if let Some(pending_error) = state.pending_fail_error.take() { match program.commit_txn(pager.clone(), state, mv_store.as_ref(), false)? { IOResult::Done(_) => return Err(pending_error), IOResult::IO(io) => { state.pending_fail_error = Some(pending_error); // put it back and wait return Ok(InsnFunctionStepResult::IO(io)); } } } if err_code > 0 { vtab_rollback_all(&program.connection)?; } // Handle RAISE errors - these carry their own resolve type if let Some(resolve_type) = on_error { // RAISE(IGNORE) signals the parent to skip the current row if resolve_type == ResolveType::Ignore { return Err(LimboError::RaiseIgnore); } if err_code > 0 { let error = match resolve_type { ResolveType::Abort | ResolveType::Rollback | ResolveType::Fail => { LimboError::Raise(resolve_type, description.to_string()) } ResolveType::Ignore => unreachable!("handled above"), ResolveType::Replace => unreachable!("Replace not valid for RAISE"), }; // Trigger subprograms must not commit — just propagate the error. // The parent program's abort() handles transaction state. if program.is_trigger_subprogram() { return Err(error); } return Err(error); } } // Determine the constraint error (if any) based on error code let constraint_error = match err_code { 0 => None, SQLITE_CONSTRAINT_PRIMARYKEY => Some(LimboError::Constraint(format!( "UNIQUE constraint failed: {description} (19)" ))), SQLITE_CONSTRAINT_CHECK => Some(LimboError::Constraint(format!( "CHECK constraint failed: {description} (19)" ))), SQLITE_CONSTRAINT_NOTNULL => Some(LimboError::Constraint(format!( "NOT NULL constraint failed: {description} (19)" ))), SQLITE_CONSTRAINT_UNIQUE => Some(LimboError::Constraint(format!( "UNIQUE constraint failed: {description} (19)" ))), SQLITE_CONSTRAINT_FOREIGNKEY => { Some(LimboError::ForeignKeyConstraint(description.to_string())) } SQLITE_CONSTRAINT_TRIGGER => Some(LimboError::Constraint(description.to_string())), // SQLITE_ERROR is a generic error (e.g. ALTER TABLE validation), not a constraint. // Use InternalError so abort() doesn't apply ON CONFLICT resolution to it. SQLITE_ERROR => Some(LimboError::InternalError(description.to_string())), _ => Some(LimboError::Constraint(format!( "undocumented halt error code {description}" ))), }; // Handle constraint errors if let Some(error) = constraint_error { // For FAIL mode with autocommit, commit partial changes before returning error. // This matches SQLite behavior where FAIL keeps changes made before the error. // Note: ON CONFLICT FAIL does NOT apply to FK violations, so we check for those first. if program.resolve_type == ResolveType::Fail && auto_commit { // Check for immediate FK violations - FK errors don't respect ON CONFLICT if program.connection.foreign_keys_enabled() && state.get_fk_immediate_violations_during_stmt() > 0 { return Err(LimboError::ForeignKeyConstraint( "immediate foreign key constraint failed".to_string(), )); } // Release savepoint to preserve partial changes, then commit state.end_statement(&program.connection, pager, EndStatement::ReleaseSavepoint)?; vtab_commit_all(&program.connection)?; index_method_pre_commit_all(state, pager)?; // Commit the transaction with partial changes match program.commit_txn(pager.clone(), state, mv_store.as_ref(), false)? { IOResult::Done(_) => return Err(error), IOResult::IO(io) => { // store the error for reentrancy state.pending_fail_error = Some(error); return Ok(InsnFunctionStepResult::IO(io)); } } } // For non-FAIL modes (or non-autocommit), just return the error. // abort() will handle rollback based on resolve_type. return Err(error); } tracing::trace!("halt(auto_commit={})", auto_commit); // Check for immediate foreign key violations. // Any immediate violation causes the statement subtransaction to roll back. if program.connection.foreign_keys_enabled() && state.get_fk_immediate_violations_during_stmt() > 0 { return Err(LimboError::ForeignKeyConstraint( "immediate foreign key constraint failed".to_string(), )); } if program.is_trigger_subprogram() { return Ok(InsnFunctionStepResult::Done); } if auto_commit { // In autocommit mode, a statement that leaves deferred violations must fail here, // and it also ends the transaction. if program.connection.foreign_keys_enabled() { let deferred_violations = program .connection .fk_deferred_violations .swap(0, Ordering::AcqRel); if deferred_violations > 0 { vtab_rollback_all(&program.connection)?; if let Some(mv_store) = mv_store.as_ref() { if let Some(tx_id) = program.connection.get_mv_tx_id() { mv_store.rollback_tx( tx_id, pager.clone(), &program.connection, crate::MAIN_DB_ID, ); } pager.end_read_tx(); } else { pager.rollback_tx(&program.connection); } program.connection.set_tx_state(TransactionState::None); return Err(LimboError::ForeignKeyConstraint( "deferred foreign key constraint failed".to_string(), )); } } state.end_statement(&program.connection, pager, EndStatement::ReleaseSavepoint)?; vtab_commit_all(&program.connection)?; index_method_pre_commit_all(state, pager)?; let result = program .commit_txn(pager.clone(), state, mv_store.as_ref(), false) .map(Into::into); // Apply deferred CDC state and reset CDC txn ID after successful commit if matches!(result, Ok(InsnFunctionStepResult::Done)) { if let Some(cdc_info) = state.pending_cdc_info.take() { program.connection.set_capture_data_changes_info(cdc_info); } program.connection.set_cdc_transaction_id(-1); } result } else { // Even if deferred violations are present, the statement subtransaction completes successfully when // it is part of an interactive transaction. state.end_statement(&program.connection, pager, EndStatement::ReleaseSavepoint)?; // Apply deferred CDC state after successful statement completion if let Some(cdc_info) = state.pending_cdc_info.take() { program.connection.set_capture_data_changes_info(cdc_info); } if program.change_cnt_on { program .connection .set_changes(state.n_change.load(Ordering::SeqCst)); } Ok(InsnFunctionStepResult::Done) } } /// Call xCommit on all virtual tables that participated in the current transaction. pub(crate) fn vtab_commit_all(conn: &Connection) -> crate::Result<()> { let mut set = conn.vtab_txn_states.write(); if set.is_empty() { return Ok(()); } let reg = &conn.syms.read().vtabs; for id in set.drain() { let vtab = reg .iter() .find(|(_, vtab)| vtab.id() == id) .expect("vtab must exist"); vtab.1.commit()?; } Ok(()) } /// Flush pending writes on all index method cursors before transaction commit. /// This ensures index method writes are persisted as part of the transaction. pub(crate) fn index_method_pre_commit_all( state: &mut ProgramState, pager: &Arc, ) -> crate::Result<()> { for cursor_opt in state.cursors.iter_mut().flatten() { let Cursor::IndexMethod(cursor) = cursor_opt else { continue; }; loop { match cursor.pre_commit()? { IOResult::Done(()) => break, IOResult::IO(io) => { while !io.finished() { pager.io.step()?; } } } } } Ok(()) } /// Rollback all virtual tables that are part of the current transaction. fn vtab_rollback_all(conn: &Connection) -> crate::Result<()> { let mut set = conn.vtab_txn_states.write(); if set.is_empty() { return Ok(()); } let reg = &conn.syms.read().vtabs; for id in set.drain() { let vtab = reg .iter() .find(|(_, vtab)| vtab.id() == id) .expect("vtab must exist"); vtab.1.rollback()?; } Ok(()) } pub fn op_halt( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( Halt { err_code, description, on_error, description_reg, }, insn ); // If description_reg is set, read the error message from that register at runtime // (used by RAISE with expression-based error messages). let desc = if let Some(reg) = description_reg { state.registers[*reg].get_value().to_string() } else { description.to_string() }; halt(program, state, pager, *err_code, &desc, *on_error) } pub fn op_halt_if_null( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( HaltIfNull { target_reg, err_code, description, }, insn ); if state.registers[*target_reg].get_value() == &Value::Null { halt(program, state, pager, *err_code, description, None) } else { state.pc += 1; Ok(InsnFunctionStepResult::Step) } } #[derive(Debug, Clone, Copy)] pub enum OpTransactionState { Start, AttachedBeginWriteTx, CheckSchemaCookie, BeginStatement, } pub fn op_transaction( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { let result = op_transaction_inner(program, state, insn, pager); tracing::debug!( "op_transaction: end: state={:?}, tx_state={:?}", state.op_transaction_state, program.connection.get_tx_state() ); match result { Ok(result) => Ok(result), Err(err) => { state.op_transaction_state = OpTransactionState::Start; Err(err) } } } /// Begin an MVCC transaction on the given MvStore using the specified mode. /// When `existing_tx_id` is `Some`, upgrades an existing transaction to exclusive. fn begin_mvcc_tx( mv_store: &MvStore, pager: &Arc, mode: &TransactionMode, existing_tx_id: Option, ) -> Result { match mode { TransactionMode::None | TransactionMode::Read | TransactionMode::Concurrent => { mv_store.begin_tx(pager.clone()) } TransactionMode::Write => mv_store.begin_exclusive_tx(pager.clone(), existing_tx_id), } } pub fn op_transaction_inner( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Transaction { db, tx_mode, schema_cookie, }, insn ); if program.is_trigger_subprogram() { crate::bail_parse_error!( "Transaction instruction should not be used in trigger subprograms" ); } let pager = program.get_pager_from_database_index(db); // Get the MvStore for the specific database (main or attached). let mv_store = program.connection.mv_store_for_db(*db); loop { match state.op_transaction_state { OpTransactionState::Start => { let conn = program.connection.clone(); let write = matches!(tx_mode, TransactionMode::Write); if write && conn.is_readonly(*db) { return Err(LimboError::ReadOnly); } // 1. We try to upgrade current version let current_state = conn.get_tx_state(); let is_attached = crate::is_attached_db(*db); let (new_transaction_state, updated) = if conn.is_nested_stmt() { (current_state, false) } else if is_attached { // For attached databases, don't modify the connection-level // transaction state — it tracks the main database's state. // Attached pager locks are managed independently below. (current_state, false) } else { match (current_state, write) { // pending state means that we tried beginning a tx and the method returned IO. // instead of ending the read tx, just update the state to pending. (TransactionState::PendingUpgrade { .. }, write) => { turso_assert!( write, "pending upgrade should only be set for write transactions" ); ( TransactionState::Write { schema_did_change: false, }, true, ) } (TransactionState::Write { schema_did_change }, true) => { (TransactionState::Write { schema_did_change }, false) } (TransactionState::Write { schema_did_change }, false) => { (TransactionState::Write { schema_did_change }, false) } (TransactionState::Read, true) => ( TransactionState::Write { schema_did_change: false, }, true, ), (TransactionState::Read, false) => (TransactionState::Read, false), (TransactionState::None, true) => ( TransactionState::Write { schema_did_change: false, }, true, ), (TransactionState::None, false) => (TransactionState::Read, true), } }; // 2. Start transaction if needed if let Some(mv_store) = mv_store.as_ref() { if is_attached { // Attached databases don't participate in the connection-level // transaction state machine above (phase 1), so the pager read // tx that the main DB path starts on None→Read isn't triggered // for them. We need it here to pin a consistent WAL snapshot // for the attached pager's B-tree page reads. if !pager.holds_read_lock() { pager.begin_read_tx()?; } pager.mvcc_refresh_if_db_changed(); let current_mv_tx = conn.get_mv_tx_for_db(*db); if current_mv_tx.is_none() { // Reject CONCURRENT on an attached DB if the main // DB already started with BEGIN DEFERRED. let conn_has_executed_begin_deferred = !conn.auto_commit.load(Ordering::SeqCst) && conn.get_mv_tx().is_none(); if conn_has_executed_begin_deferred && *tx_mode == TransactionMode::Concurrent { mark_unlikely(); return Err(LimboError::TxError( "Cannot start CONCURRENT transaction after BEGIN DEFERRED" .to_string(), )); } // Use the same tx_mode as the main DB's active // transaction when available, so BEGIN CONCURRENT // applies to all databases uniformly. let effective_mode = conn.get_mv_tx().map(|(_, mode)| mode).unwrap_or(*tx_mode); match begin_mvcc_tx(mv_store, &pager, &effective_mode, None) { Ok(tx_id) => { conn.set_mv_tx_for_db(*db, Some((tx_id, effective_mode))); } Err(err) => { pager.end_read_tx(); return Err(err); } } } else if write { // Upgrade: attached DB has a Read/Concurrent tx but the // statement needs write access. Mirror the main DB's // upgrade logic so that exclusive locks are acquired. let (tx_id, current_mode) = current_mv_tx.unwrap(); if matches!(current_mode, TransactionMode::None | TransactionMode::Read) && matches!(tx_mode, TransactionMode::Write) { if let Err(err) = begin_mvcc_tx(mv_store, &pager, tx_mode, Some(tx_id)) { pager.end_read_tx(); return Err(err); } conn.set_mv_tx_for_db(*db, Some((tx_id, *tx_mode))); } } } else { // Main database MVCC path (unchanged logic) let started_read_tx = updated && matches!(current_state, TransactionState::None); if started_read_tx { turso_assert!( !conn.is_nested_stmt(), "nested stmt should not begin a new read transaction" ); pager.begin_read_tx()?; state.auto_txn_cleanup = TxnCleanup::RollbackTxn; } // MVCC reads must refresh WAL change counters to avoid stale page-cache reads. pager.mvcc_refresh_if_db_changed(); // In MVCC we don't have write exclusivity, therefore we just need to start a transaction if needed. // Programs can run Transaction twice, first with read flag and then with write flag. So a single txid is enough // for both. let current_mv_tx = program.connection.get_mv_tx_for_db(*db); let has_existing_mv_tx = current_mv_tx.is_some(); let conn_has_executed_begin_deferred = !has_existing_mv_tx && !program.connection.auto_commit.load(Ordering::SeqCst); if conn_has_executed_begin_deferred && *tx_mode == TransactionMode::Concurrent { mark_unlikely(); return Err(LimboError::TxError( "Cannot start CONCURRENT transaction after BEGIN DEFERRED" .to_string(), )); } if !has_existing_mv_tx { match begin_mvcc_tx(mv_store, &pager, tx_mode, None) { Ok(tx_id) => { program .connection .set_mv_tx_for_db(*db, Some((tx_id, *tx_mode))); } Err(err) => { if started_read_tx { pager.end_read_tx(); conn.set_tx_state(TransactionState::None); state.auto_txn_cleanup = TxnCleanup::None; } return Err(err); } } } else if updated { // TODO: fix tx_mode in Insn::Transaction, now each statement overrides it even if there's already a CONCURRENT Tx in progress, for example let (tx_id, mv_tx_mode) = current_mv_tx .expect("current_mv_tx should be Some when updated is true"); let actual_tx_mode = if mv_tx_mode == TransactionMode::Concurrent { TransactionMode::Concurrent } else { *tx_mode }; if matches!(new_transaction_state, TransactionState::Write { .. }) && matches!(actual_tx_mode, TransactionMode::Write) { if let Err(err) = begin_mvcc_tx(mv_store, &pager, &actual_tx_mode, Some(tx_id)) { if started_read_tx { pager.end_read_tx(); conn.set_tx_state(TransactionState::None); state.auto_txn_cleanup = TxnCleanup::None; } return Err(err); } } } } } else { if matches!(tx_mode, TransactionMode::Concurrent) { mark_unlikely(); return Err(LimboError::TxError( "Concurrent transaction mode is only supported when MVCC is enabled" .to_string(), )); } // For attached databases without MVCC, always start read/write // transactions on the attached pager, since the connection-level // transaction state may already be Read/Write from the main database. let is_attached = crate::is_attached_db(*db); if is_attached { // If the pager already holds a read lock (e.g., after // SchemaUpdated reprepare or prior write tx), skip // locks that are already held. if pager.holds_read_lock() { if matches!(tx_mode, TransactionMode::Write) && !pager.holds_write_lock() { state.op_transaction_state = OpTransactionState::AttachedBeginWriteTx; continue; } state.op_transaction_state = OpTransactionState::CheckSchemaCookie; continue; } pager.begin_read_tx()?; if matches!(tx_mode, TransactionMode::Write) { // Transition to AttachedBeginWriteTx to handle begin_write_tx // separately, so if it returns IO we don't re-call begin_read_tx // on re-entry. state.op_transaction_state = OpTransactionState::AttachedBeginWriteTx; continue; } } else if updated && matches!(current_state, TransactionState::None) { turso_assert!( !conn.is_nested_stmt(), "nested stmt should not begin a new read transaction" ); pager.begin_read_tx()?; state.auto_txn_cleanup = TxnCleanup::RollbackTxn; } if !is_attached && updated && matches!(new_transaction_state, TransactionState::Write { .. }) { turso_assert!( !conn.is_nested_stmt(), "nested stmt should not begin a new write transaction" ); let begin_w_tx_res = pager.begin_write_tx(); if matches!( begin_w_tx_res, Err(LimboError::Busy | LimboError::BusySnapshot) ) { // We failed to upgrade to write transaction so put the transaction into its original state. // That is, if the transaction had not started, end the read transaction so that next time we // start a new one. match current_state { TransactionState::None | TransactionState::PendingUpgrade { has_read_txn: false, } => { pager.end_read_tx(); conn.set_tx_state(TransactionState::None); state.auto_txn_cleanup = TxnCleanup::None; } TransactionState::Read | TransactionState::PendingUpgrade { has_read_txn: true } => { conn.set_tx_state(TransactionState::Read); } TransactionState::Write { .. } => { panic!("impossible state: {current_state:?}") } } return Err(begin_w_tx_res.unwrap_err()); } if let IOResult::IO(io) = begin_w_tx_res? { // set the transaction state to pending so we don't have to // end the read transaction. conn.set_tx_state(TransactionState::PendingUpgrade { has_read_txn: matches!(current_state, TransactionState::Read), }); return Ok(InsnFunctionStepResult::IO(io)); } } } // 3. Transaction state should be updated before checking for Schema cookie so that the tx is ended properly on error if updated { conn.set_tx_state(new_transaction_state); } state.op_transaction_state = OpTransactionState::CheckSchemaCookie; continue; } // 3b. For attached databases, begin the write transaction after // begin_read_tx has already completed in the Start state. OpTransactionState::AttachedBeginWriteTx => { let res = pager.begin_write_tx()?; if let IOResult::IO(io) = res { return Ok(InsnFunctionStepResult::IO(io)); } state.op_transaction_state = OpTransactionState::CheckSchemaCookie; continue; } // 4. Check whether schema has changed if we are actually going to access the database. // Can only read header if page 1 has been allocated already // begin_write_tx that happens, but not begin_read_tx OpTransactionState::CheckSchemaCookie => { let res = get_schema_cookie(&pager, mv_store.as_ref(), program, *db); match res { Ok(IOResult::Done(header_schema_cookie)) => { if header_schema_cookie != *schema_cookie { tracing::debug!( "schema changed, force reprepare: {} != {}", header_schema_cookie, *schema_cookie ); return Err(LimboError::SchemaUpdated); } } Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), // This means we are starting a read_tx and we do not have a page 1 yet, so we just continue execution Err(LimboError::Page1NotAlloc) => {} Err(err) => { return Err(err); } } state.op_transaction_state = OpTransactionState::BeginStatement; } OpTransactionState::BeginStatement => { let needs_stmt_journal = program.needs_stmt_subtransactions.load(Ordering::Relaxed); if *db == crate::MAIN_DB_ID && needs_stmt_journal { let write = matches!(tx_mode, TransactionMode::Write); let res = state.begin_statement(&program.connection, &pager, write)?; if let IOResult::IO(io) = res { return Ok(InsnFunctionStepResult::IO(io)); } } else if crate::is_attached_db(*db) && matches!(tx_mode, TransactionMode::Write) && needs_stmt_journal { if let Some(mv_store) = program.connection.mv_store_for_db(*db) { // Attached MVCC DB: open an MvStore savepoint. if let Some(tx_id) = program.connection.get_mv_tx_id_for_db(*db) { mv_store.begin_savepoint(tx_id); } } else { // Attached WAL DB: open a pager savepoint for statement rollback. let db_size = return_if_io!(pager.with_header(|header| header.database_size.get())); pager.open_subjournal()?; pager.try_use_subjournal()?; let result = pager.open_savepoint(db_size); if result.is_err() { pager.stop_use_subjournal(); } result?; state.attached_savepoint_pagers.push(pager.clone()); } } if *db == crate::MAIN_DB_ID && matches!(tx_mode, TransactionMode::Write) && !program.connection.auto_commit.load(Ordering::SeqCst) { program .connection .n_active_writes .fetch_add(1, Ordering::SeqCst); state.is_active_write = true; } state.pc += 1; state.op_transaction_state = OpTransactionState::Start; return Ok(InsnFunctionStepResult::Step); } } } } pub fn op_auto_commit( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( AutoCommit { auto_commit, rollback }, insn ); // Main DB's MvStore drives the commit/rollback routing. The attach-time // journal-mode compatibility check ensures all attached DBs match, so // checking the main DB is sufficient to choose the MVCC vs WAL path. let mv_store = program.connection.mv_store(); let conn = program.connection.clone(); let fk_on = conn.foreign_keys_enabled(); let had_autocommit = conn.auto_commit.load(Ordering::SeqCst); // true, not in tx // Drive any multi-step commit/rollback that's already in progress. // This handles main DB commits (Committing), attached DB commits // (CommittingAttached), MVCC commits (CommittingMvcc), and attached // MVCC commits (CommittingAttachedMvcc) that yielded on IO and need re-entry. if !matches!(state.commit_state, CommitState::Ready) { let res = program .commit_txn(pager.clone(), state, mv_store.as_ref(), *rollback) .map(Into::into); // Only clear after a final, successful non-rollback COMMIT. if fk_on && !*rollback && matches!( res, Ok(InsnFunctionStepResult::Step | InsnFunctionStepResult::Done) ) { conn.clear_deferred_foreign_key_violations(); } return res; } if program.is_trigger_subprogram() { // Trigger subprograms never commit or rollback. state.pc += 1; return Ok(InsnFunctionStepResult::Step); } // The logic in this opcode can be a bit confusing, so to make things a bit clearer lets be // very explicit about the currently existing and requested state. let requested_autocommit = *auto_commit; let requested_rollback = *rollback; let changed = requested_autocommit != had_autocommit; let is_txn_end_eq = changed && requested_autocommit; // what the requested operation is let is_begin_req = had_autocommit && !requested_autocommit && !requested_rollback; let is_commit_req = !had_autocommit && requested_autocommit && !requested_rollback; let is_rollback_req = !had_autocommit && requested_autocommit && requested_rollback; if is_txn_end_eq && conn.n_active_writes.load(Ordering::SeqCst) > 0 { return Err(LimboError::Busy); } if changed { if requested_rollback { // ROLLBACK transition if let Some(mv_store) = mv_store.as_ref() { if let Some(tx_id) = conn.get_mv_tx_id() { mv_store.rollback_tx(tx_id, pager.clone(), &conn, crate::MAIN_DB_ID); } pager.end_read_tx(); conn.rollback_attached_mvcc_txs(true); } else { pager.rollback_tx(&conn); } conn.rollback_attached_wal_txns(); conn.set_tx_state(TransactionState::None); conn.auto_commit.store(true, Ordering::SeqCst); conn.set_cdc_transaction_id(-1); } else { // BEGIN (true->false) or COMMIT (false->true) if is_commit_req { // Pre-check deferred FKs; leave tx open and do NOT clear violations check_deferred_fk_on_commit(&conn)?; } conn.auto_commit .store(requested_autocommit, Ordering::SeqCst); } } else { // No autocommit flip. let mvcc_tx_active = conn.get_mv_tx().is_some(); if !mvcc_tx_active { if !requested_autocommit { return Err(LimboError::TxError( "cannot start a transaction within a transaction".to_string(), )); } else if requested_rollback { return Err(LimboError::TxError( "cannot rollback - no transaction is active".to_string(), )); } else { return Err(LimboError::TxError( "cannot commit - no transaction is active".to_string(), )); } } else if is_begin_req { return Err(LimboError::TxError( "cannot use BEGIN after BEGIN CONCURRENT".to_string(), )); } } // For explicit COMMIT, flush any pending index method writes first if is_commit_req { index_method_pre_commit_all(state, pager)?; } let res = program .commit_txn(pager.clone(), state, mv_store.as_ref(), requested_rollback) .map(Into::into); if mv_store.is_none() && matches!( res, Ok(InsnFunctionStepResult::Step | InsnFunctionStepResult::Done) ) && (is_rollback_req || is_commit_req) { pager.clear_savepoints()?; } // Clear deferred FK counters only after FINAL success of COMMIT/ROLLBACK. if fk_on && matches!( res, Ok(InsnFunctionStepResult::Step | InsnFunctionStepResult::Done) ) && (is_rollback_req || is_commit_req) { conn.clear_deferred_foreign_key_violations(); } // Reset CDC transaction ID after successful COMMIT or ROLLBACK. if matches!( res, Ok(InsnFunctionStepResult::Step | InsnFunctionStepResult::Done) ) && (is_rollback_req || is_commit_req) { conn.set_cdc_transaction_id(-1); } res } pub fn op_savepoint( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!(Savepoint { op, name }, insn); let conn = program.connection.clone(); let mv_store = conn.mv_store(); match *op { SavepointOp::Begin => { let starts_transaction = conn.auto_commit.load(Ordering::SeqCst); let deferred_fk_violations = conn.get_deferred_foreign_key_violations(); if let Some(mv_store) = mv_store.as_ref() { let tx_id = if let Some(tx_id) = conn.get_mv_tx_id() { tx_id } else { let tx_id = mv_store.begin_tx(pager.clone())?; conn.set_mv_tx(Some((tx_id, TransactionMode::Read))); if matches!(conn.get_tx_state(), TransactionState::None) { conn.set_tx_state(TransactionState::Read); } tx_id }; mv_store.begin_named_savepoint( tx_id, name.clone(), starts_transaction, deferred_fk_violations, ); // Open matching named savepoints on attached MVCC databases. conn.for_each_attached_mv_tx(|db_id, att_tx_id| { if let Some(att_mv) = conn.mv_store_for_db(db_id) { att_mv.begin_named_savepoint( att_tx_id, name.clone(), false, deferred_fk_violations, ); } }); } else { pager.open_subjournal()?; let db_size = return_if_io!(pager.with_header(|header| header.database_size.get())); pager.open_named_savepoint( name.clone(), db_size, starts_transaction, deferred_fk_violations, )?; } if starts_transaction { conn.auto_commit.store(false, Ordering::SeqCst); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } SavepointOp::Release => { let release_result = if let Some(mv_store) = mv_store.as_ref() { match conn.get_mv_tx_id() { Some(tx_id) => mv_store.release_named_savepoint(tx_id, name)?, None => SavepointResult::NotFound, } } else { pager.release_named_savepoint(name)? }; // Release matching named savepoints on attached MVCC databases. if mv_store.is_some() { conn.for_each_attached_mv_tx(|db_id, att_tx_id| { if let Some(att_mv) = conn.mv_store_for_db(db_id) { let _ = att_mv.release_named_savepoint(att_tx_id, name); } }); } match release_result { SavepointResult::NotFound => { mark_unlikely(); return Err(LimboError::TxError(format!("no such savepoint: {name}"))); } SavepointResult::Release => { // Savepoint released successfully, just continue } SavepointResult::Commit => { // This means that releasing the savepoint caused the transaction to commit, so we need to auto-commit here. let auto_commit = Insn::AutoCommit { auto_commit: true, rollback: false, }; return op_auto_commit(program, state, &auto_commit, pager); } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } SavepointOp::RollbackTo => { let deferred_fk_snapshot = if let Some(mv_store) = mv_store.as_ref() { // Rollback named savepoints on attached MVCC databases. conn.for_each_attached_mv_tx(|db_id, att_tx_id| { if let Some(att_mv) = conn.mv_store_for_db(db_id) { let _ = att_mv.rollback_to_named_savepoint(att_tx_id, name); } }); match conn.get_mv_tx_id() { Some(tx_id) => mv_store.rollback_to_named_savepoint(tx_id, name)?, None => None, } } else { pager.rollback_to_named_savepoint(name)? }; let Some(deferred_fk_snapshot) = deferred_fk_snapshot else { mark_unlikely(); return Err(LimboError::TxError(format!("no such savepoint: {name}"))); }; conn.fk_deferred_violations .store(deferred_fk_snapshot, Ordering::SeqCst); // After rolling back pages, the in-memory schema cache may be stale // if DDL was executed within the savepoint. Invalidate the pager's // cached schema cookie and check if a schema reparse is needed. pager.set_schema_cookie(None); let in_memory_version = conn.schema.read().schema_version; let pager_ref = conn.pager.load().clone(); match pager_ref .io .block(|| pager.with_header(|h| h.schema_cookie.get())) { Ok(on_disk_cookie) if in_memory_version != on_disk_cookie => { // Schema was modified during the savepoint. Try to reparse // from the restored database pages. If that fails (e.g. the // database was empty at the savepoint), use an empty schema. if conn.reparse_schema().is_err() { conn.with_schema_mut(|schema| { *schema = Schema::new(); }); } } Err(_) => { // Header page is not readable (database empty after rollback). // Reset to an empty schema. conn.with_schema_mut(|schema| { *schema = Schema::new(); }); } _ => {} // Schema unchanged, nothing to do. } state.pc += 1; Ok(InsnFunctionStepResult::Step) } } } fn check_deferred_fk_on_commit(conn: &Connection) -> Result<()> { if !conn.foreign_keys_enabled() { return Ok(()); } if conn.get_deferred_foreign_key_violations() > 0 { return Err(LimboError::ForeignKeyConstraint( "deferred foreign key constraint failed on commit".into(), )); } Ok(()) } pub fn op_goto( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Goto { target_pc }, insn); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } pub fn op_gosub( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Gosub { target_pc, return_reg, }, insn ); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } state.registers[*return_reg].set_int((state.pc + 1) as i64); state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } pub fn op_return( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Return { return_reg, can_fallthrough, }, insn ); if let Value::Numeric(Numeric::Integer(pc)) = state.registers[*return_reg].get_value() { let pc: u32 = (*pc) .try_into() .unwrap_or_else(|_| panic!("Return register is negative: {pc}")); state.pc = pc; } else { if unlikely(!*can_fallthrough) { return Err(LimboError::InternalError( "Return register is not an integer".to_string(), )); } state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_integer( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Integer { value, dest }, insn); state.registers[*dest].set_int(*value); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub enum OpProgramState { Start, /// Step state tracks whether we're executing a trigger subprogram (vs FK action subprogram) Step { is_trigger: bool, statement: Box, /// Saved last_insert_rowid to restore after trigger subprogram completes. /// Per SQLite docs, trigger-body INSERTs must not overwrite the top-level rowid. saved_last_insert_rowid: Option, }, } /// Execute a subprogram (Program opcode). /// Used for both triggers and FK actions (CASCADE, SET NULL, etc.) pub fn op_program( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( Program { params, program: subprogram, ignore_jump_target, }, insn ); loop { match &mut state.op_program_state { OpProgramState::Start => { let mut statement = Statement::new( Program::from_prepared(subprogram.clone(), program.connection.clone()), pager.clone(), QueryMode::Normal, 0, ); statement.reset()?; // Check if this is a trigger subprogram - if so, track execution // and save last_insert_rowid so it can be restored after the trigger finishes. let (is_trigger, saved_last_insert_rowid) = if let Some(ref trigger) = statement.get_trigger() { program.connection.start_trigger_execution(trigger.clone()); (true, Some(program.connection.last_insert_rowid())) } else { (false, None) }; // Extract register values from params (which contain register indices encoded as negative integers) // and bind them to the subprogram's parameters for (param_idx, param_value) in params.iter().enumerate() { if let Value::Numeric(Numeric::Integer(reg_idx)) = param_value { let reg_idx = *reg_idx as usize; if reg_idx < state.registers.len() { let value = state.registers[reg_idx].get_value().clone(); let param_index = NonZero::::new(param_idx + 1) .expect("param_idx + 1 should be non-zero"); statement.bind_at(param_index, value); } else { crate::bail_corrupt_error!( "Register index {} out of bounds (len={})", reg_idx, state.registers.len() ); } } else { crate::bail_parse_error!( "Subprogram parameters should be integers, got {:?}", param_value ); } } state.op_program_state = OpProgramState::Step { is_trigger, statement: Box::new(statement), saved_last_insert_rowid, }; } OpProgramState::Step { is_trigger, statement, saved_last_insert_rowid, } => { let is_trigger = *is_trigger; let saved_last_insert_rowid = *saved_last_insert_rowid; let mut raise_ignore = false; // Track whether the subprogram aborted with an error. When abort() // runs inside the subprogram, it already calls end_trigger_execution(), // so we must not call it again after the loop. let mut subprogram_aborted = false; loop { let res = statement.step(); match res { Ok(step_result) => match step_result { StepResult::Done => break, StepResult::IO => { let io = statement.take_io_completions().unwrap_or_else(|| { IOCompletions::Single(Completion::new_yield()) }); return Ok(InsnFunctionStepResult::IO(io)); } StepResult::Row => continue, StepResult::Interrupt | StepResult::Busy => { return Err(LimboError::Busy); } }, Err(LimboError::Constraint(constraint_err)) => { if program.resolve_type != ResolveType::Ignore { return Err(LimboError::Constraint(constraint_err)); } subprogram_aborted = true; break; } Err(LimboError::RaiseIgnore) => { raise_ignore = true; subprogram_aborted = true; break; } Err(err) => { return Err(err); } } } // Only end trigger execution for normal completion. Error paths // already called end_trigger_execution() via abort() in the subprogram. if is_trigger && !subprogram_aborted { program.connection.end_trigger_execution(); } // Restore last_insert_rowid after trigger execution, per SQLite semantics: // trigger-body INSERTs must not overwrite the top-level rowid. if let Some(rowid) = saved_last_insert_rowid { program.connection.update_last_rowid(rowid); } state.op_program_state = OpProgramState::Start; if raise_ignore { // RAISE(IGNORE) — skip the current row by jumping to ignore_jump_target state.pc = ignore_jump_target.as_offset_int(); } else { state.pc += 1; } return Ok(InsnFunctionStepResult::Step); } } } } pub fn op_real( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Real { value, dest }, insn); state.registers[*dest] .set_float(NonNan::new(*value).expect("f64 passed to op_real should be a valid NonNan")); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_real_affinity( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(RealAffinity { register }, insn); if let Value::Numeric(Numeric::Integer(i)) = &state.registers[*register].get_value() { state.registers[*register].set_float( NonNan::new(*i as f64) .expect("i64 passed to op_real_affinity should be a valid NonNan"), ); }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_string8( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(String8 { value, dest }, insn); state.registers[*dest].set_text(Text::new(value.clone())); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_blob( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Blob { value, dest }, insn); state.registers[*dest].set_blob(value.clone()); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_row_data( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(RowData { cursor_id, dest }, insn); let record = { let cursor_ref = must_be_btree_cursor!(*cursor_id, program.cursor_ref, state, "RowData"); let cursor = cursor_ref.as_btree_mut(); let record_option = return_if_io!(cursor.record()); let record = record_option.ok_or_else(|| { mark_unlikely(); LimboError::InternalError("RowData: cursor has no record".to_string()) })?; record.clone() }; let reg = &mut state.registers[*dest]; *reg = Register::Record(record); state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[derive(Debug, Clone, Copy)] pub enum OpRowIdState { Start, Record { index_cursor_id: usize, table_cursor_id: usize, }, Seek { rowid: i64, table_cursor_id: usize, }, GetRowid, } pub fn op_row_id( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(RowId { cursor_id, dest }, insn); loop { match state.op_row_id_state { OpRowIdState::Start => { if let Some(deferred) = state.deferred_seeks[*cursor_id].take() { state.op_row_id_state = OpRowIdState::Record { index_cursor_id: deferred.index_cursor_id, table_cursor_id: deferred.table_cursor_id, }; } else { state.op_row_id_state = OpRowIdState::GetRowid; } } OpRowIdState::Record { index_cursor_id, table_cursor_id, } => { let rowid = { let index_cursor = state.get_cursor(index_cursor_id); match index_cursor { Cursor::BTree(index_cursor) => { let record = return_if_io!(index_cursor.record()); let record = record.as_ref().expect("index cursor should have a record"); let rowid = record .last_value() .expect("record should have a last value"); match rowid { Ok(ValueRef::Numeric(Numeric::Integer(rowid))) => rowid, _ => unreachable!(), } } Cursor::IndexMethod(index_cursor) => { return_if_io!(index_cursor.query_rowid()) .expect("index cursor should have a rowid") } _ => panic!("unexpected cursor type"), } }; state.op_row_id_state = OpRowIdState::Seek { rowid, table_cursor_id, } } OpRowIdState::Seek { rowid, table_cursor_id, } => { { let table_cursor = state.get_cursor(table_cursor_id); let table_cursor = table_cursor.as_btree_mut(); return_if_io!( table_cursor.seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true }) ); } state.op_row_id_state = OpRowIdState::GetRowid; } OpRowIdState::GetRowid => { let cursors = &mut state.cursors; if let Some(Cursor::BTree(btree_cursor)) = cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") { if btree_cursor.get_null_flag() { state.registers[*dest].set_null(); break; } if let Some(ref rowid) = return_if_io!(btree_cursor.rowid()) { state.registers[*dest].set_int(*rowid); } else { state.registers[*dest].set_null(); } } else if let Some(Cursor::Virtual(virtual_cursor)) = cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") { let rowid = virtual_cursor.rowid(); if rowid != 0 { state.registers[*dest].set_int(rowid); } else { state.registers[*dest].set_null(); } } else if let Some(Cursor::MaterializedView(mv_cursor)) = cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") { if let Some(rowid) = return_if_io!(mv_cursor.rowid()) { state.registers[*dest].set_int(rowid); } else { state.registers[*dest].set_null(); } } else if let Some(Cursor::IndexMethod(cursor)) = cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") { if let Some(rowid) = return_if_io!(cursor.query_rowid()) { state.registers[*dest].set_int(rowid); } else { state.registers[*dest].set_null(); } } else { mark_unlikely(); return Err(LimboError::InternalError( "RowId: cursor is not a table, virtual, or materialized view cursor" .to_string(), )); } break; } } } state.op_row_id_state = OpRowIdState::Start; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_idx_row_id( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(IdxRowId { cursor_id, dest }, insn); let cursors = &mut state.cursors; let cursor = cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") .as_mut() .expect("cursor should exist"); let rowid = match cursor { Cursor::BTree(cursor) => return_if_io!(cursor.rowid()), Cursor::IndexMethod(cursor) => return_if_io!(cursor.query_rowid()), _ => panic!("unexpected cursor type"), }; match rowid { Some(rowid) => state.registers[*dest].set_int(rowid), None => state.registers[*dest].set_null(), }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_seek_rowid( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( SeekRowid { cursor_id, src_reg, target_pc, }, insn ); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } let (pc, did_seek) = { let cursor = get_cursor!(state, *cursor_id); // Handle MaterializedView cursor let (pc, did_seek) = match cursor { Cursor::MaterializedView(mv_cursor) => { let rowid = match state.registers[*src_reg].get_value() { Value::Numeric(Numeric::Integer(rowid)) => Some(*rowid), Value::Null => None, _ => None, }; match rowid { Some(rowid) => { let seek_result = return_if_io!(mv_cursor .seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true })); let pc = if !matches!(seek_result, SeekResult::Found) { target_pc.as_offset_int() } else { state.pc + 1 }; (pc, true) } None => (target_pc.as_offset_int(), false), } } Cursor::BTree(btree_cursor) => { let rowid = match state.registers[*src_reg].get_value() { Value::Numeric(Numeric::Integer(rowid)) => Some(*rowid), Value::Null => None, // For non-integer values try to apply affinity and convert them to integer. other => { let mut temp_reg = Register::Value(other.clone()); let converted = apply_affinity_char(&mut temp_reg, Affinity::Numeric); if converted { match temp_reg.get_value() { Value::Numeric(Numeric::Integer(i)) => Some(*i), Value::Numeric(Numeric::Float(f)) => Some(f64::from(*f) as i64), _ => None, } } else { None } } }; match rowid { Some(rowid) => { let seek_result = return_if_io!(btree_cursor .seek(SeekKey::TableRowId(rowid), SeekOp::GE { eq_only: true })); let pc = if !matches!(seek_result, SeekResult::Found) { target_pc.as_offset_int() } else { state.pc + 1 }; (pc, true) } None => (target_pc.as_offset_int(), false), } } _ => panic!("SeekRowid on non-btree/materialized-view cursor"), }; (pc, did_seek) }; // Increment btree_seeks metric for SeekRowid operation after cursor is dropped if did_seek { state.metrics.btree_seeks = state.metrics.btree_seeks.saturating_add(1); } state.pc = pc; Ok(InsnFunctionStepResult::Step) } pub fn op_deferred_seek( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( DeferredSeek { index_cursor_id, table_cursor_id, }, insn ); state.deferred_seeks[*table_cursor_id] = Some(DeferredSeekState { index_cursor_id: *index_cursor_id, table_cursor_id: *table_cursor_id, }); state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Separate enum for seek key to avoid lifetime issues /// with using [SeekKey] - OpSeekState always owns the key, /// unless it's [OpSeekKey::IndexKeyFromRegister] in which case the record /// is owned by the program state's registers and we store the register number. #[derive(Debug)] pub enum OpSeekKey { TableRowId(i64), IndexKeyFromRegister(usize), IndexKeyUnpacked { start_reg: usize, num_regs: usize }, } #[derive(Debug)] pub enum OpSeekState { /// Initial state Start, /// Position cursor with seek operation with (rowid, op) search parameters Seek { key: OpSeekKey, op: SeekOp }, /// Advance cursor (with [BTreeCursor::next]/[BTreeCursor::prev] methods) which was /// positioned after [OpSeekState::Seek] state if [BTreeCursor::seek] returned [SeekResult::TryAdvance] Advance { op: SeekOp }, /// Move cursor to the last BTree row if DB knows that comparison result will be fixed (due to type ordering, e.g. NUMBER always <= TEXT) MoveLast, } pub fn op_seek( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { let (cursor_id, is_index, record_source, target_pc) = match insn { Insn::SeekGE { cursor_id, is_index, start_reg, num_regs, target_pc, .. } | Insn::SeekLE { cursor_id, is_index, start_reg, num_regs, target_pc, .. } | Insn::SeekGT { cursor_id, is_index, start_reg, num_regs, target_pc, .. } | Insn::SeekLT { cursor_id, is_index, start_reg, num_regs, target_pc, .. } => ( cursor_id, *is_index, RecordSource::Unpacked { start_reg: *start_reg, num_regs: *num_regs, }, target_pc, ), _ => unreachable!("unexpected Insn {:?}", insn), }; assert!( target_pc.is_offset(), "op_seek: target_pc should be an offset, is: {target_pc:?}" ); let op = match insn { Insn::SeekGE { eq_only, .. } => SeekOp::GE { eq_only: *eq_only }, Insn::SeekGT { .. } => SeekOp::GT, Insn::SeekLE { eq_only, .. } => SeekOp::LE { eq_only: *eq_only }, Insn::SeekLT { .. } => SeekOp::LT, _ => unreachable!("unexpected Insn {:?}", insn), }; match seek_internal( program, state, pager, record_source, *cursor_id, is_index, op, ) { Ok(SeekInternalResult::Found) => { state.pc += 1; Ok(InsnFunctionStepResult::Step) } Ok(SeekInternalResult::NotFound) => { state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } Ok(SeekInternalResult::IO(io)) => Ok(InsnFunctionStepResult::IO(io)), Err(e) => Err(e), } } #[derive(Debug)] pub enum SeekInternalResult { Found, NotFound, IO(IOCompletions), } #[derive(Clone, Copy)] pub enum RecordSource { Unpacked { start_reg: usize, num_regs: usize }, Packed { record_reg: usize }, } /// Internal function used by many VDBE instructions that need to perform a seek operation. /// /// Explanation for some of the arguments: /// - `record_source`: whether the seek key record is already a record (packed) or it will be constructed from registers (unpacked) /// - `cursor_id`: the cursor id /// - `is_index`: true if the cursor is an index, false if it is a table /// - `op`: the [SeekOp] to perform #[allow(clippy::too_many_arguments)] pub fn seek_internal( program: &Program, state: &mut ProgramState, pager: &Arc, record_source: RecordSource, cursor_id: usize, is_index: bool, op: SeekOp, ) -> Result { let mv_store = program.connection.mv_store(); /// wrapper so we can use the ? operator and handle errors correctly in this outer function fn inner( _program: &Program, state: &mut ProgramState, _pager: &Arc, _mv_store: Option<&Arc>, record_source: RecordSource, cursor_id: usize, is_index: bool, op: SeekOp, ) -> Result { loop { match &state.seek_state { OpSeekState::Start => { if is_index { // FIXME: text-to-numeric conversion should also happen here when applicable (when index column is numeric) // See below for the table-btree implementation of this match record_source { RecordSource::Unpacked { start_reg, num_regs, } => { state.seek_state = OpSeekState::Seek { key: OpSeekKey::IndexKeyUnpacked { start_reg, num_regs, }, op, }; } RecordSource::Packed { record_reg } => { state.seek_state = OpSeekState::Seek { key: OpSeekKey::IndexKeyFromRegister(record_reg), op, }; } }; continue; } let RecordSource::Unpacked { start_reg, num_regs, } = record_source else { unreachable!("op_seek: record_source should be Unpacked for table-btree"); }; assert_eq!(num_regs, 1, "op_seek: num_regs should be 1 for table-btree"); let original_value = state.registers[start_reg].get_value(); let mut temp_value = original_value.clone(); let conversion_successful = if matches!(temp_value, Value::Text(_)) { let new_val = apply_numeric_affinity(temp_value.as_value_ref(), false); let converted = new_val.is_some(); if let Some(new_val) = new_val { temp_value = new_val.to_owned(); } converted } else { true // Non-text values don't need conversion }; let int_key = extract_int_value(&temp_value); let lost_precision = !conversion_successful || !matches!(temp_value, Value::Numeric(Numeric::Integer(_))); let actual_op = if lost_precision { match &temp_value { Value::Numeric(Numeric::Float(f)) => { let f_val = f64::from(*f); // When extract_int_value clamped to i64::MAX/MIN, // the cast `int_key as f64` loses the fact that the // float is outside the i64 range. Detect this and // set the comparison result directly. // // For i64::MAX: any float > 9223372036854774784.0 // is >= 9223372036854775808.0 (the next f64), which // exceeds i64::MAX (9223372036854775807). i64::MAX // is not exactly representable as f64, so the float // is always strictly greater. // // For i64::MIN: -2^63 IS exactly representable as // f64, so we must distinguish the exact match from // floats that are strictly less. let c = if int_key == i64::MAX && f_val > 9223372036854774784.0 { // Float exceeds i64::MAX, so int_key < float -1 } else if int_key == i64::MIN && f_val < -9223372036854774784.0 { if f_val == (i64::MIN as f64) { // Float is exactly i64::MIN 0 } else { // Float is below i64::MIN, so int_key > float 1 } } else { let int_key_as_float = int_key as f64; if int_key_as_float > f_val { 1 } else if int_key_as_float < f_val { -1 } else { 0 } }; match c.cmp(&0) { std::cmp::Ordering::Less => match op { SeekOp::LT => SeekOp::LE { eq_only: false }, // (x < 5.1) -> (x <= 5) SeekOp::GE { .. } => SeekOp::GT, // (x >= 5.1) -> (x > 5) other => other, }, std::cmp::Ordering::Greater => match op { SeekOp::GT => SeekOp::GE { eq_only: false }, // (x > 4.9) -> (x >= 5) SeekOp::LE { .. } => SeekOp::LT, // (x <= 4.9) -> (x < 5) other => other, }, std::cmp::Ordering::Equal => op, } } Value::Text(_) | Value::Blob(_) => { match op { SeekOp::GT | SeekOp::GE { .. } => { // No integers are > or >= non-numeric text return Ok(SeekInternalResult::NotFound); } SeekOp::LT | SeekOp::LE { .. } => { // All integers are < or <= non-numeric text // Move to last position and then use the normal seek logic state.seek_state = OpSeekState::MoveLast; continue; } } } _ => op, } } else { op }; let rowid = if matches!(original_value, Value::Null) { match actual_op { SeekOp::GE { .. } | SeekOp::GT => { return Ok(SeekInternalResult::NotFound); } SeekOp::LE { .. } | SeekOp::LT => { // No integers are < NULL return Ok(SeekInternalResult::NotFound); } } } else { int_key }; state.seek_state = OpSeekState::Seek { key: OpSeekKey::TableRowId(rowid), op: actual_op, }; continue; } OpSeekState::Seek { key, op } => { let seek_result = match key { OpSeekKey::TableRowId(rowid) => { let cursor = get_cursor!(state, cursor_id).as_btree_mut(); match cursor.seek(SeekKey::TableRowId(*rowid), *op)? { IOResult::Done(seek_result) => seek_result, IOResult::IO(io) => return Ok(SeekInternalResult::IO(io)), } } OpSeekKey::IndexKeyFromRegister(record_reg) => { let (cursor, record) = { let (cursors, registers) = (&mut state.cursors, &state.registers); let cursor = cursors .get_mut(cursor_id) .and_then(|c| c.as_mut()) .expect("op_seek: cursor should be allocated") .as_btree_mut(); let record = match ®isters[*record_reg] { Register::Record(ref record) => record, _ => unreachable!( "op_seek: record_reg should be a Record register when OpSeekKey::IndexKeyFromRegister is used" ), }; (cursor, record) }; match cursor.seek(SeekKey::IndexKey(record), *op)? { IOResult::Done(seek_result) => seek_result, IOResult::IO(io) => return Ok(SeekInternalResult::IO(io)), } } OpSeekKey::IndexKeyUnpacked { start_reg, num_regs, } => { let start_reg = *start_reg; let num_regs = *num_regs; let cursor = get_cursor!(state, cursor_id).as_btree_mut(); let registers = &state.registers[start_reg..start_reg + num_regs]; match cursor.seek_unpacked(registers, *op)? { IOResult::Done(seek_result) => seek_result, IOResult::IO(io) => return Ok(SeekInternalResult::IO(io)), } } }; // Increment btree_seeks metric after seek operation and cursor is dropped state.metrics.btree_seeks = state.metrics.btree_seeks.saturating_add(1); let found = match seek_result { SeekResult::Found => true, SeekResult::NotFound => false, SeekResult::TryAdvance => { state.seek_state = OpSeekState::Advance { op: *op }; continue; } }; return Ok(if found { SeekInternalResult::Found } else { SeekInternalResult::NotFound }); } OpSeekState::Advance { op } => { let found = { let cursor = get_cursor!(state, cursor_id); let cursor = cursor.as_btree_mut(); // Seek operation has anchor number which equals to the closed boundary of the range // (e.g. for >= x - anchor is x, for > x - anchor is x + 1) // // Before Advance state, cursor was positioned to the leaf page which should hold the anchor. // Sometimes this leaf page can have no matching rows, and in this case // we need to move cursor in the direction of Seek to find record which matches the seek filter // // Consider following scenario: Seek [> 666] // interior page dividers: I1: [ .. 667 .. ] // / \ // leaf pages: P1[661,665] P2[anything here is GT 666] // After the initial Seek, cursor will be positioned after the end of leaf page P1 [661, 665] // because this is potential position for insertion of value 666. // But as P1 has no row matching Seek criteria - we need to move it to the right // (and as we at the page boundary, we will move cursor to the next neighbor leaf, which guaranteed to have // row keys greater than divider, which is greater or equal than anchor) // this same logic applies for indexes, but the next/prev record is expected to be found in the parent page's // divider cell. turso_assert!( !cursor.get_skip_advance(), "skip_advance should not be true in the middle of a seek operation" ); let result = match op { // deliberately call get_next_record() instead of next() to avoid skip_advance triggering unwantedly SeekOp::GT | SeekOp::GE { .. } => cursor.next()?, SeekOp::LT | SeekOp::LE { .. } => cursor.prev()?, }; match result { IOResult::Done(()) => cursor.has_record(), IOResult::IO(io) => return Ok(SeekInternalResult::IO(io)), } }; return Ok(if found { SeekInternalResult::Found } else { SeekInternalResult::NotFound }); } OpSeekState::MoveLast => { let cursor = state.get_cursor(cursor_id); let cursor = cursor.as_btree_mut(); match cursor.last()? { IOResult::Done(()) => {} IOResult::IO(io) => return Ok(SeekInternalResult::IO(io)), } // the MoveLast variant is only used for SeekOp::LT and SeekOp::LE when the seek condition is always true, // so we have always found what we were looking for. return Ok(SeekInternalResult::Found); } } } } let result = inner( program, state, pager, mv_store.as_ref(), record_source, cursor_id, is_index, op, ); if !matches!(result, Ok(SeekInternalResult::IO(..))) { state.seek_state = OpSeekState::Start; } result } /// Returns the tie-breaker ordering for SQLite index comparison opcodes. /// /// When comparing index keys that omit the PRIMARY KEY/ROWID, SQLite uses a /// tie-breaker value (`default_rc` in the C code) to determine the result when /// the non-primary-key portions of the keys are equal. /// /// This function extracts the appropriate tie-breaker based on the comparison opcode: /// /// ## Tie-breaker Logic /// /// - **`IdxLE` and `IdxGT`**: Return `Ordering::Less` (equivalent to `default_rc = -1`) /// - When keys are equal, these operations should favor the "less than" result /// - `IdxLE`: "less than or equal" - equality should be treated as "less" /// - `IdxGT`: "greater than" - equality should be treated as "less" (so condition fails) /// /// - **`IdxGE` and `IdxLT`**: Return `Ordering::Equal` (equivalent to `default_rc = 0`) /// - When keys are equal, these operations should treat it as true equality /// - `IdxGE`: "greater than or equal" - equality should be treated as "equal" /// - `IdxLT`: "less than" - equality should be treated as "equal" (so condition fails) /// /// ## SQLite Implementation Details /// /// In SQLite's C implementation, this corresponds to: /// ```c /// if( pOp->opcodeopcode==OP_IdxLE || pOp->opcode==OP_IdxGT ); /// r.default_rc = -1; // Ordering::Less /// }else{ /// assert( pOp->opcode==OP_IdxGE || pOp->opcode==OP_IdxLT ); /// r.default_rc = 0; // Ordering::Equal /// } /// ``` #[inline(always)] fn get_tie_breaker_from_idx_comp_op(insn: &Insn) -> std::cmp::Ordering { match insn { Insn::IdxLE { .. } | Insn::IdxGT { .. } => std::cmp::Ordering::Less, Insn::IdxGE { .. } | Insn::IdxLT { .. } => std::cmp::Ordering::Equal, _ => panic!("Invalid instruction for index comparison"), } } #[allow(clippy::let_and_return)] pub fn op_idx_ge( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( IdxGE { cursor_id, start_reg, num_regs, target_pc, }, insn ); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } let pc = { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_btree_mut(); let index_info = cursor.get_index_info().clone(); let pc = if let Some(idx_record) = return_if_io!(cursor.record()) { // Create the comparison record from registers let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + *num_regs]); let tie_breaker = get_tie_breaker_from_idx_comp_op(insn); let ord = compare_records_generic( idx_record, // The serialized record from the index values, // The record built from registers &index_info, // Sort order flags 0, tie_breaker, )?; if ord.is_ge() { target_pc.as_offset_int() } else { state.pc + 1 } } else { // No record at cursor position, jump to target target_pc.as_offset_int() }; pc }; state.pc = pc; Ok(InsnFunctionStepResult::Step) } pub fn op_seek_end( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(SeekEnd { cursor_id }, *insn); { let cursor = state.get_cursor(cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.seek_end()); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[allow(clippy::let_and_return)] pub fn op_idx_le( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( IdxLE { cursor_id, start_reg, num_regs, target_pc, }, insn ); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } let pc = { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_btree_mut(); let index_info = cursor.get_index_info().clone(); let pc = if let Some(idx_record) = return_if_io!(cursor.record()) { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + *num_regs]); let tie_breaker = get_tie_breaker_from_idx_comp_op(insn); let ord = compare_records_generic(idx_record, values, &index_info, 0, tie_breaker)?; if ord.is_le() { target_pc.as_offset_int() } else { state.pc + 1 } } else { // No record at cursor position, jump to target target_pc.as_offset_int() }; pc }; state.pc = pc; Ok(InsnFunctionStepResult::Step) } #[allow(clippy::let_and_return)] pub fn op_idx_gt( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( IdxGT { cursor_id, start_reg, num_regs, target_pc, }, insn ); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } let pc = { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_btree_mut(); let index_info = cursor.get_index_info().clone(); let pc = if let Some(idx_record) = return_if_io!(cursor.record()) { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + *num_regs]); let tie_breaker = get_tie_breaker_from_idx_comp_op(insn); let ord = compare_records_generic(idx_record, values, &index_info, 0, tie_breaker)?; if ord.is_gt() { target_pc.as_offset_int() } else { state.pc + 1 } } else { // No record at cursor position, jump to target target_pc.as_offset_int() }; pc }; state.pc = pc; Ok(InsnFunctionStepResult::Step) } #[allow(clippy::let_and_return)] pub fn op_idx_lt( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( IdxLT { cursor_id, start_reg, num_regs, target_pc, }, insn ); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } let pc = { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_btree_mut(); let index_info = cursor.get_index_info().clone(); let pc = if let Some(idx_record) = return_if_io!(cursor.record()) { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + *num_regs]); let tie_breaker = get_tie_breaker_from_idx_comp_op(insn); let ord = compare_records_generic(idx_record, values, &index_info, 0, tie_breaker)?; if ord.is_lt() { target_pc.as_offset_int() } else { state.pc + 1 } } else { // No record at cursor position, jump to target target_pc.as_offset_int() }; pc }; state.pc = pc; Ok(InsnFunctionStepResult::Step) } pub fn op_decr_jump_zero( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(DecrJumpZero { reg, target_pc }, insn); if !target_pc.is_offset() { crate::bail_corrupt_error!("Unresolved label: {target_pc:?}"); } match &mut state.registers[*reg] { Register::Value(Value::Numeric(Numeric::Integer(n))) => { *n = n.saturating_sub(1); if *n == 0 { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } } Register::Value(_) | Register::Record(_) => { bail_constraint_error!("datatype mismatch"); } Register::Aggregate(_) => { mark_unlikely(); return Err(LimboError::InternalError( "DecrJumpZero: unexpected aggregate register".into(), )); } } Ok(InsnFunctionStepResult::Step) } fn apply_kbn_step(acc: &mut Value, r: f64, state: &mut SumAggState) { // NaN from Inf + (-Inf) is sticky: once acc is Null, it stays Null. // See https://sqlite.org/lang_aggfunc.html ("result is NULL"). if matches!(acc, Value::Null) { return; } let s = acc.to_float_or_zero(); let t = s + r; if t.is_nan() { *acc = Value::Null; return; } // When t is infinite, the KBN correction computes inf - inf = NaN, // which is meaningless. Skip compensation in that case. if t.is_finite() { let correction = if s.abs() > r.abs() { (s - t) + r } else { (r - t) + s }; state.r_err += correction; } *acc = Value::from_f64(t); } // Add a (possibly large) integer to the running sum. fn apply_kbn_step_int(acc: &mut Value, i: i64, state: &mut SumAggState) { const THRESHOLD: i64 = 4503599627370496; // 2^52 if i <= -THRESHOLD || i >= THRESHOLD { let i_sm = i % 16384; let i_big = i - i_sm; apply_kbn_step(acc, i_big as f64, state); apply_kbn_step(acc, i_sm as f64, state); } else { apply_kbn_step(acc, i as f64, state); } } /// Initialize aggregate payload with default values. /// Payload layout by aggregate type: /// - Count/Count0: [Integer(0)] /// - Sum: [Null, Float(0.0), Integer(0), Integer(0)] // acc, r_err, approx, ovrfl /// - Total: [Float(0.0), Float(0.0), Integer(0), Integer(0)] // same but starts at 0.0 /// - Avg: [Float(0.0), Float(0.0), Integer(0)] // sum, r_err, count - uses KBN like SUM /// - Min/Max: [Null] /// - GroupConcat/StringAgg: [Null] (becomes Text on first non-null value) /// - JsonGroupObject/JsonbGroupObject: [Blob([])] /// - JsonGroupArray/JsonbGroupArray: [Blob([])] fn init_agg_payload(func: &AggFunc, payload: &mut Vec) -> Result<()> { match func { AggFunc::Count | AggFunc::Count0 => payload.push(Value::from_i64(0)), AggFunc::Sum | AggFunc::Total => { let acc = if matches!(func, AggFunc::Total) { Value::from_f64(0.0) } else { Value::Null }; payload.push(acc); payload.push(Value::from_f64(0.0)); payload.push(Value::from_i64(0)); payload.push(Value::from_i64(0)); } AggFunc::Avg => { payload.push(Value::from_f64(0.0)); payload.push(Value::from_f64(0.0)); payload.push(Value::from_i64(0)); } AggFunc::Min | AggFunc::Max => payload.push(Value::Null), AggFunc::GroupConcat | AggFunc::StringAgg => { // Use Null as sentinel to distinguish "no values yet" from "accumulated empty string" payload.push(Value::Null); } AggFunc::External(_) => { mark_unlikely(); // External aggregates use ExternalAggState, not flat payload return Err(LimboError::InternalError( "External aggregate not supported in init_agg_payload".to_string(), )); } AggFunc::ArrayAgg => { // payload[0] = element count (Integer), remaining slots = accumulated values. // We serialize to a record blob only in finalize, avoiding O(n²) re-serialization. payload.push(Value::from_i64(0)); } #[cfg(feature = "json")] AggFunc::JsonGroupObject | AggFunc::JsonbGroupObject => { payload.push(Value::Blob(vec![])); } #[cfg(feature = "json")] AggFunc::JsonGroupArray | AggFunc::JsonbGroupArray => { payload.push(Value::Blob(vec![])); } }; Ok(()) } /// Process a single input row and update the aggregate state in the payload. /// /// This is the core aggregation logic shared between both aggregation strategies: /// - **Register-based (sort-stream)**: Called from `op_agg_step` (AggStep instruction). /// The payload lives in `AggContext::Builtin` stored in a register. /// - **Hash-based (future enhancement)**: Called from `step_aggregate` during HashAggStep. The payload lives /// in hash table entries keyed by GROUP BY values. /// /// The payload slice contains the intermediate aggregate state (initialized by /// `init_agg_payload`), and this function incorporates the new row's values. /// /// # Payload layouts (see `init_agg_payload` for initial values): /// - **Count**: `[count: Integer]` - increments if arg is not NULL /// - **Count0**: `[count: Integer]` - always increments (COUNT(*)) /// - **Avg**: `[sum: Float, r_err: Float, count: Integer]` - uses KBN compensation like SUM /// - **Sum/Total**: `[acc, r_err: Float, approx: Integer, ovrfl: Integer]` /// - `acc`: running sum (Null/Integer/Float depending on inputs) /// - `r_err`: Kahan-Babuška-Neumaier compensation term for floating-point precision /// - `approx`: 1 if result is approximate (float arithmetic used) /// - `ovrfl`: 1 if integer overflow occurred (Total promotes to float, Sum errors) /// - **Min/Max**: `[current_extreme: Value]` - tracks min/max seen so far /// - **GroupConcat/StringAgg**: `[accumulated: Null|Text]` - Null until first value, then Text /// - **JsonGroup***: `[raw_jsonb: Blob]` - accumulated raw JSONB bytes fn update_agg_payload( func: &AggFunc, arg: Value, // most agg functions take one argument maybe_arg2: Option, // for GroupConcat/StringAgg, JsonGroupObject/JsonbGroupObject, payload: &mut [Value], collation: CollationSeq, comparator: &Option, ) -> Result<()> { match func { AggFunc::Count => { // COUNT(column) increments only when arg is not NULL. Empty args treated as non-NULL // (would indicate a bug in query translation, but matches SQLite behavior of counting). if !matches!(arg, Value::Null) { // invariant as per init_agg_payload: payload[0] is always an integer let Value::Numeric(Numeric::Integer(i)) = &mut payload[0] else { mark_unlikely(); return Err(LimboError::InternalError( "Count: payload is not an integer".to_string(), )); }; *i = i.checked_add(1).ok_or(LimboError::IntegerOverflow)?; } } AggFunc::Count0 => { // invariant as per init_agg_payload: payload[0] is always an integer let Value::Numeric(Numeric::Integer(i)) = &mut payload[0] else { mark_unlikely(); return Err(LimboError::InternalError( "Count0: payload is not an integer".to_string(), )); }; *i = i.checked_add(1).ok_or(LimboError::IntegerOverflow)?; } AggFunc::Avg => { if matches!(arg, Value::Null) { return Ok(()); } // invariant as per init_agg_payload: payload[0] is Float (sum), payload[1] is Float (r_err), payload[2] is Integer (count) let [sum_val, r_err_val, count_val, ..] = payload else { mark_unlikely(); return Err(LimboError::InternalError( "Avg: payload too short".to_string(), )); }; if matches!(*sum_val, Value::Null) { return Ok(()); } let r_err = r_err_val.to_float_or_zero(); let Value::Numeric(Numeric::Integer(count)) = count_val else { mark_unlikely(); return Err(LimboError::InternalError( "Avg: payload[2] is not an integer".to_string(), )); }; let val = match arg { Value::Numeric(Numeric::Integer(i)) => i as f64, Value::Numeric(Numeric::Float(f)) => f64::from(f), Value::Text(t) => match try_for_float(t.as_str().as_bytes()).1 { ParsedNumber::Integer(i) => i as f64, ParsedNumber::Float(f) => f, ParsedNumber::None => 0.0, }, Value::Blob(b) => match try_for_float(&b).1 { ParsedNumber::Integer(i) => i as f64, ParsedNumber::Float(f) => f, ParsedNumber::None => 0.0, }, _ => unreachable!(), }; // Use Kahan-Babuška-Neumaier compensation for better floating-point precision let s = sum_val.to_float_or_zero(); let t = s + val; // When t is infinite, the KBN correction computes inf - inf = NaN, // which is meaningless. Skip compensation in that case. if t.is_finite() { let correction = if s.abs() > val.abs() { (s - t) + val } else { (val - t) + s }; *r_err_val = Value::from_f64(r_err + correction); } *sum_val = Value::from_f64(t); *count = count.checked_add(1).ok_or(LimboError::IntegerOverflow)?; } AggFunc::Sum | AggFunc::Total => { // invariant as per init_agg_payload: payload[0] is acc (Null/Integer/Float), // payload[1] is Float (r_err), payload[2] is Integer (approx), payload[3] is Integer (ovrfl) let [acc, r_err_val, approx_val, ovrfl_val, ..] = payload else { return Err(LimboError::InternalError( "Sum/Total: payload too short".to_string(), )); }; let r_err_f = r_err_val.to_float_or_zero(); let Value::Numeric(Numeric::Integer(approx_i)) = approx_val else { mark_unlikely(); return Err(LimboError::InternalError( "Sum/Total: payload[2] is not an integer".to_string(), )); }; let Value::Numeric(Numeric::Integer(ovrfl_i)) = ovrfl_val else { mark_unlikely(); return Err(LimboError::InternalError( "Sum/Total: payload[3] is not an integer".to_string(), )); }; let mut sum_state = SumAggState { r_err: r_err_f, approx: *approx_i != 0, ovrfl: *ovrfl_i != 0, }; if matches!(*acc, Value::Null) && sum_state.approx { return Ok(()); } match arg { Value::Null => {} Value::Numeric(Numeric::Integer(i)) => match acc { Value::Null => { *acc = Value::from_i64(i); } Value::Numeric(Numeric::Integer(acc_i)) => match acc_i.checked_add(i) { Some(sum) => *acc_i = sum, None => { if matches!(func, AggFunc::Total) { let acc_f = *acc_i as f64; *acc = Value::from_f64(acc_f); sum_state.approx = true; sum_state.ovrfl = true; apply_kbn_step_int(acc, i, &mut sum_state); } else { mark_unlikely(); return Err(LimboError::IntegerOverflow); } } }, Value::Numeric(Numeric::Float(_)) => { apply_kbn_step_int(acc, i, &mut sum_state); } _ => unreachable!("Sum/Total accumulator initialized to Null/Integer/Float"), }, Value::Numeric(Numeric::Float(f)) => match acc { Value::Null => { *acc = Value::Numeric(Numeric::Float(f)); sum_state.approx = true; } Value::Numeric(Numeric::Integer(i)) => { *acc = Value::from_f64(*i as f64); sum_state.approx = true; apply_kbn_step(acc, f64::from(f), &mut sum_state); } Value::Numeric(Numeric::Float(_)) => { sum_state.approx = true; apply_kbn_step(acc, f64::from(f), &mut sum_state); } _ => unreachable!("Sum/Total accumulator initialized to Null/Integer/Float"), }, Value::Text(t) => { let (parse_result, parsed_number) = try_for_float(t.as_str().as_bytes()); handle_text_sum(acc, &mut sum_state, parsed_number, parse_result, false); } Value::Blob(b) => { let (parse_result, parsed_number) = try_for_float(&b); handle_text_sum(acc, &mut sum_state, parsed_number, parse_result, true); } } *r_err_val = Value::from_f64(sum_state.r_err); *approx_i = sum_state.approx as i64; *ovrfl_i = sum_state.ovrfl as i64; } AggFunc::Min | AggFunc::Max => { if matches!(arg, Value::Null) { return Ok(()); } if matches!(payload[0], Value::Null) { payload[0] = arg; return Ok(()); } use std::cmp::Ordering; // Use custom type comparator if available, otherwise fall back to collation let cmp = if let Some(ref cmp_fn) = comparator { let arg_ref = arg.as_ref(); let payload_ref = payload[0].as_ref(); cmp_fn(&arg_ref, &payload_ref) } else { compare_with_collation(&arg, &payload[0], Some(collation)) }; let should_update = match func { AggFunc::Max => cmp == Ordering::Greater, AggFunc::Min => cmp == Ordering::Less, _ => false, }; if should_update { payload[0] = arg; } } AggFunc::GroupConcat | AggFunc::StringAgg => { if matches!(arg, Value::Null) { return Ok(()); } let delimiter = maybe_arg2.unwrap_or_else(|| Value::build_text(",")); let acc = &mut payload[0]; if matches!(acc, Value::Null) { // First non-null value: convert to Text *acc = Value::build_text(arg.to_string()); } else { acc.exec_group_concat(&delimiter); acc.exec_group_concat(&arg); } } AggFunc::External(_) => { mark_unlikely(); return Err(LimboError::InternalError( "External aggregate not supported in update_agg_payload".to_string(), )); } #[cfg(feature = "json")] AggFunc::JsonGroupObject | AggFunc::JsonbGroupObject => { // arg = key, maybe_arg2 = value let Some(value) = maybe_arg2 else { mark_unlikely(); return Err(LimboError::InternalError( "JsonGroupObject/JsonbGroupObject: no value provided".to_string(), )); }; let mut key_vec = convert_dbtype_to_raw_jsonb(&arg)?; let mut val_vec = convert_dbtype_to_raw_jsonb(&value)?; let Value::Blob(vec) = &mut payload[0] else { mark_unlikely(); return Err(LimboError::InternalError( "JsonGroupObject: payload[0] is not a blob".to_string(), )); }; if vec.is_empty() { // bits for obj header vec.push(12); } vec.append(&mut key_vec); vec.append(&mut val_vec); } AggFunc::ArrayAgg => { // ArrayAgg accumulation is handled directly in the AggStep caller // via payload_vec_mut() to grow the Vec (O(1) per row). return Err(LimboError::InternalError( "ArrayAgg should be handled directly in op_agg_step, not update_agg_payload".into(), )); } #[cfg(feature = "json")] AggFunc::JsonGroupArray | AggFunc::JsonbGroupArray => { // arg = value let mut data = convert_dbtype_to_raw_jsonb(&arg)?; let Value::Blob(vec) = &mut payload[0] else { mark_unlikely(); return Err(LimboError::InternalError( "JsonGroupArray: payload[0] is not a blob".to_string(), )); }; if vec.is_empty() { vec.push(11); // bits for array header } vec.append(&mut data); } } Ok(()) } /// Convert the intermediate aggregate state in `payload` into the final result value. /// /// This finalization logic is shared between both aggregation strategies: /// - **Register-based (sort-stream)**: Called from `op_agg_final` (AggFinal/AggValue /// instructions) when a group boundary is crossed. /// - **Hash-based (future enhancement)**: Called during the emit phase of HashAggNext after all rows have /// been processed. The payload may have been merged via `merge_agg_payload` if /// spilling occurred. /// /// # Finalization logic by aggregate type: /// - **Count/Count0**: Returns the count directly /// - **Avg**: Computes `sum / count`, returns NULL if count is 0 /// - **Sum**: Returns the accumulated value, applying Kahan compensation if approximate. /// Returns NULL if no non-NULL values were seen (unless float arithmetic was used). /// - **Total**: Like Sum but always returns Float, defaulting to 0.0 for empty groups /// - **Min/Max**: Returns the tracked extreme value directly /// - **GroupConcat/StringAgg**: Returns the accumulated string /// - **JsonGroup***: Parses accumulated raw JSONB bytes into proper JSON output fn finalize_agg_payload(func: &AggFunc, payload: &[Value]) -> Result { let val = match func { AggFunc::Count | AggFunc::Count0 => payload[0].clone(), AggFunc::Avg => { // Payload: [sum, r_err, count] let count = payload[2].as_int().unwrap_or(0); if count == 0 || matches!(&payload[0], Value::Null) { Value::Null } else { let sum = payload[0].to_float_or_zero(); let r_err = payload[1].to_float_or_zero(); // Apply KBN compensation before dividing Value::from_f64((sum + r_err) / count as f64) } } AggFunc::Sum => { let acc = &payload[0]; let approx = payload[2].as_int().unwrap_or(0) != 0; let ovrfl = payload[3].as_int().unwrap_or(0) != 0; let r_err = payload[1].to_float_or_zero(); match acc { Value::Null => Value::Null, Value::Numeric(Numeric::Integer(i)) if !approx && !ovrfl => Value::from_i64(*i), Value::Numeric(Numeric::Float(f)) => Value::from_f64(f64::from(*f) + r_err), _ => Value::from_f64(acc.to_float_or_zero() + r_err), } } AggFunc::Total => { // Payload: [acc, r_err, approx, ovrfl] let acc = &payload[0]; let approx = payload[2].as_int().unwrap_or(0) != 0; let r_err = payload[1].to_float_or_zero(); match acc { Value::Null if approx => Value::Null, Value::Null => Value::from_f64(0.0), Value::Numeric(Numeric::Integer(i)) => Value::from_f64(*i as f64 + r_err), Value::Numeric(Numeric::Float(f)) => Value::from_f64(f64::from(*f) + r_err), _ => unreachable!("Total accumulator initialized to Null/Integer/Float"), } } AggFunc::Min | AggFunc::Max => payload[0].clone(), AggFunc::GroupConcat | AggFunc::StringAgg => payload[0].clone(), AggFunc::ArrayAgg => { // payload[0] = count, payload[1..] = accumulated values. // Serialize to a record blob only once at finalization. let count = payload[0].as_int().unwrap_or(0) as usize; if count == 0 { Value::Null } else if 1 + count > payload.len() { return Err(LimboError::InternalError(format!( "ArrayAgg: count ({count}) exceeds payload length ({})", payload.len() - 1 ))); } else { let elements = &payload[1..1 + count]; Value::Blob(ImmutableRecord::from_values(elements, count).into_payload()) } } AggFunc::External(_) => { mark_unlikely(); // External aggregates are finalized via AggContext::compute_external() return Err(LimboError::InternalError( "finalize_agg_payload called for External aggregate".to_string(), )); } #[cfg(feature = "json")] AggFunc::JsonGroupObject => { let data = payload[0].to_blob().expect("Should be blob"); json_from_raw_bytes_agg(data, false)? } #[cfg(feature = "json")] AggFunc::JsonbGroupObject => { let data = payload[0].to_blob().expect("Should be blob"); json_from_raw_bytes_agg(data, true)? } #[cfg(feature = "json")] AggFunc::JsonGroupArray => { let data = payload[0].to_blob().expect("Should be blob"); json_from_raw_bytes_agg(data, false)? } #[cfg(feature = "json")] AggFunc::JsonbGroupArray => { let data = payload[0].to_blob().expect("Should be blob"); json_from_raw_bytes_agg(data, true)? } }; Ok(val) } pub fn op_agg_step( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( AggStep { acc_reg, col, delimiter, func, comparator, }, insn ); // Initialize aggregate state if not already done if let Register::Value(Value::Null) = state.registers[*acc_reg] { state.registers[*acc_reg] = match func { AggFunc::External(ext_func) => match ext_func.as_ref() { ExtFunc::Aggregate { init, step, finalize, argc, } => Register::Aggregate(AggContext::External(ExternalAggState { state: unsafe { (init)() }, argc: *argc, step_fn: *step, finalize_fn: *finalize, })), _ => unreachable!("scalar function called in aggregate context"), }, _ => { // Built-in aggregates use flat payload let mut payload = Vec::new(); init_agg_payload(func, &mut payload)?; Register::Aggregate(AggContext::Builtin(payload)) } }; } // Resolve custom type comparator for MIN/MAX if provided let comparator = comparator.as_ref().map(make_sort_comparator); // Step the aggregate match func { AggFunc::External(_) => { // External aggregates use FFI and need special handling let (step_fn, state_ptr, argc) = { let Register::Aggregate(agg) = &state.registers[*acc_reg] else { unreachable!(); }; let AggContext::External(agg_state) = agg else { unreachable!(); }; (agg_state.step_fn, agg_state.state, agg_state.argc) }; if argc == 0 { unsafe { step_fn(state_ptr, 0, std::ptr::null()) }; } else { let register_slice = &state.registers[*col..*col + argc]; let mut ext_values: Vec = Vec::with_capacity(argc); for ov in register_slice.iter() { ext_values.push(ov.get_value().to_ffi()); } let argv_ptr = ext_values.as_ptr(); unsafe { step_fn(state_ptr, argc as i32, argv_ptr) }; for ext_value in ext_values { unsafe { ext_value.__free_internal_type() }; } } } _ => { let arg = state.registers[*col].get_value().clone(); if matches!(func, AggFunc::ArrayAgg) { // ArrayAgg grows the payload Vec directly (O(1) per row). let Register::Aggregate(agg) = &mut state.registers[*acc_reg] else { panic!( "Unexpected value {:?} in AggStep at register {}", state.registers[*acc_reg], *acc_reg ); }; let payload = agg.payload_vec_mut(); let count = payload[0] .as_int() .expect("array_agg count must be an integer") as usize; payload[0] = Value::from_i64((count + 1) as i64); payload.push(arg); } else { // Only a subset of aggregate functions take two arguments let maybe_arg2 = match func { AggFunc::GroupConcat | AggFunc::StringAgg => { Some(state.registers[*delimiter].get_value().clone()) } #[cfg(feature = "json")] AggFunc::JsonGroupObject | AggFunc::JsonbGroupObject => { Some(state.registers[*delimiter].get_value().clone()) } _ => None, }; let collation = state.current_collation.unwrap_or(CollationSeq::Binary); // Now get mutable borrow on payload let Register::Aggregate(agg) = &mut state.registers[*acc_reg] else { panic!( "Unexpected value {:?} in AggStep at register {}", state.registers[*acc_reg], *acc_reg ); }; let payload = agg.payload_mut(); update_agg_payload(func, arg, maybe_arg2, payload, collation, &comparator)?; } } }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_agg_final( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { let (acc_reg, dest_reg, func) = match insn { Insn::AggFinal { register, func } => (*register, *register, func), Insn::AggValue { acc_reg, dest_reg, func, } => (*acc_reg, *dest_reg, func), _ => unreachable!("unexpected Insn {:?}", insn), }; match &state.registers[acc_reg] { Register::Aggregate(agg) => { let value = match agg { AggContext::External(_) => { // External aggregates use FFI finalization agg.compute_external()? } AggContext::Builtin(payload) => { // Built-in aggregates use shared finalization finalize_agg_payload(func, payload)? } }; state.registers[dest_reg].set_value(value); } Register::Value(Value::Null) => { // When the set is empty, return appropriate default match func { AggFunc::Total => { state.registers[dest_reg] .set_float(NonNan::new(0.0).expect("0.0 is a valid NonNan")); } AggFunc::Count | AggFunc::Count0 => { state.registers[dest_reg].set_int(0); } #[cfg(feature = "json")] AggFunc::JsonGroupArray => { state.registers[dest_reg].set_text(Text::json("[]".to_string())); } #[cfg(feature = "json")] AggFunc::JsonbGroupArray => { state.registers[dest_reg] .set_blob(json::jsonb::Jsonb::make_empty_array(1).data()); } #[cfg(feature = "json")] AggFunc::JsonGroupObject => { state.registers[dest_reg].set_text(Text::json("{}".to_string())); } #[cfg(feature = "json")] AggFunc::JsonbGroupObject => { state.registers[dest_reg] .set_blob(json::jsonb::Jsonb::make_empty_obj(1).data()); } _ => {} } } other => { panic!("Unexpected value {other:?} in AggFinal"); } }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_open( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( SorterOpen { cursor_id, columns: _, order_and_collations, comparators, }, insn ); // be careful here - we must not use any async operations after pager.with_header because this op-code has no proper state-machine let page_size = match pager.with_header(|header| header.page_size) { Ok(IOResult::Done(page_size)) => page_size, Err(_) => PageSize::default(), Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), }; let page_size = page_size.get() as usize; let cache_size = program.connection.get_cache_size(); // Set the buffer size threshold to be roughly the same as the limit configured for the page-cache. let max_buffer_size_bytes = if cache_size < 0 { (cache_size.abs() * 1024) as usize } else { (cache_size as usize) * page_size }; let (order, collations): (Vec<_>, Vec<_>) = order_and_collations .iter() .map(|(ord, coll)| (*ord, coll.unwrap_or_default())) .unzip(); let comparators = comparators .iter() .map(|c| c.as_ref().map(make_sort_comparator)) .collect(); let temp_store = program.connection.get_temp_store(); let cursor = Sorter::new( &order, collations, comparators, max_buffer_size_bytes, page_size, pager.io.clone(), temp_store, ); let cursors = &mut state.cursors; cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") .replace(Cursor::new_sorter(cursor)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_data( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( SorterData { cursor_id, dest_reg, pseudo_cursor, }, insn ); let record = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_sorter_mut(); cursor.record().cloned() }; let record = match record { Some(record) => record, None => { state.pc += 1; return Ok(InsnFunctionStepResult::Step); } }; state.registers[*dest_reg] = Register::Record(record.clone()); { let pseudo_cursor = state.get_cursor(*pseudo_cursor); pseudo_cursor.as_pseudo_mut().insert(record); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_insert( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( SorterInsert { cursor_id, record_reg, }, insn ); { let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_sorter_mut(); let record = match &state.registers[*record_reg] { Register::Record(record) => record, _ => unreachable!("SorterInsert on non-record register"), }; return_if_io!(cursor.insert(record)); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_sort( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( SorterSort { cursor_id, pc_if_empty, }, insn ); let (is_empty, did_sort) = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_sorter_mut(); let is_empty = cursor.is_empty(); if !is_empty { return_if_io!(cursor.sort()); } (is_empty, !is_empty) }; // Increment metrics for sort operation after cursor is dropped if did_sort { state.metrics.sort_operations = state.metrics.sort_operations.saturating_add(1); } if is_empty { state.pc = pc_if_empty.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_next( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( SorterNext { cursor_id, pc_if_next, }, insn ); assert!(pc_if_next.is_offset()); let has_more = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_sorter_mut(); return_if_io!(cursor.next()); cursor.has_more() }; if has_more { state.pc = pc_if_next.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_sorter_compare( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( SorterCompare { cursor_id, sorted_record_reg, num_regs, pc_when_nonequal, }, insn ); let previous_sorter_values = { let Register::Record(record) = &state.registers[*sorted_record_reg] else { mark_unlikely(); return Err(LimboError::InternalError( "Sorted record must be a record".to_string(), )); }; &record.get_values_range(0..*num_regs)? }; // Inlined `state.get_cursor` to prevent borrowing conflit with `state.registers` let cursor = state .cursors .get_mut(*cursor_id) .unwrap_or_else(|| panic!("cursor id {cursor_id} out of bounds")) .as_mut() .unwrap_or_else(|| panic!("cursor id {cursor_id} is None")); let cursor = cursor.as_sorter_mut(); let Some(current_sorter_record) = cursor.record() else { mark_unlikely(); return Err(LimboError::InternalError( "Sorter must have a record".to_string(), )); }; let current_sorter_values = ¤t_sorter_record.get_values_range(0..*num_regs)?; // If the current sorter record has a NULL in any of the significant fields, the comparison is not equal. let is_equal = current_sorter_values .iter() .all(|v| !matches!(v, ValueRef::Null)) && compare_immutable( previous_sorter_values, current_sorter_values, &cursor.index_key_info, ) .is_eq(); if is_equal { state.pc += 1; } else { state.pc = pc_when_nonequal.as_offset_int(); } Ok(InsnFunctionStepResult::Step) } /// Insert the integer value held by register P2 into a RowSet object held in register P1. /// /// An assertion fails if P2 is not an integer. pub fn op_rowset_add( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( RowSetAdd { rowset_reg, value_reg, }, insn ); let value = state.registers[*value_reg].get_value(); let rowid = match value { Value::Numeric(Numeric::Integer(i)) => *i, _ => { mark_unlikely(); return Err(LimboError::InternalError( "RowSetAdd: P2 must be an integer".to_string(), )); } }; let rowset = state.rowsets.entry(*rowset_reg).or_default(); rowset.insert(rowid); state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Extract the smallest value from the RowSet object in P1 and put that value into register P3. /// Or, if RowSet object P1 is initially empty, leave P3 unchanged and jump to instruction P2. pub fn op_rowset_read( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( RowSetRead { rowset_reg, pc_if_empty, dest_reg, }, insn ); assert!(pc_if_empty.is_offset()); let rowset = state.rowsets.get_mut(rowset_reg); match rowset { Some(rowset) => { if rowset.is_empty() { state.pc = pc_if_empty.as_offset_int(); } else if let Some(smallest) = rowset.smallest() { state.registers[*dest_reg].set_int(smallest); state.pc += 1; } else { state.pc = pc_if_empty.as_offset_int(); } } None => { state.pc = pc_if_empty.as_offset_int(); } } Ok(InsnFunctionStepResult::Step) } /// Register P3 is assumed to hold a 64-bit integer value. If register P1 contains a RowSet object /// and that RowSet object contains the value held in P3, jump to register P2. Otherwise, insert /// the integer in P3 into the RowSet and continue on to the next opcode. /// /// The RowSet object is optimized for the case where sets of integers are inserted in distinct /// phases, which each set contains no duplicates. Each set is identified by a unique P4 value. /// The first set must have P4==0, the final set must have P4==-1, and for all other sets must /// have P4>0. /// /// This allows optimizations: (a) when P4==0 there is no need to test the RowSet object for P3, /// as it is guaranteed not to contain it, (b) when P4==-1 there is no need to insert the value, /// as it will never be tested for, and (c) when a value that is part of set X is inserted, there /// is no need to search to see if the same value was previously inserted as part of set X (only /// if it was previously inserted as part of some other set). pub fn op_rowset_test( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( RowSetTest { rowset_reg, pc_if_found, value_reg, batch, }, insn ); assert!(pc_if_found.is_offset()); let value = state.registers[*value_reg].get_value(); let rowid = match value { Value::Numeric(Numeric::Integer(i)) => *i, _ => { mark_unlikely(); return Err(LimboError::InternalError( "RowSetTest: P3 must be an integer".to_string(), )); } }; let rowset = state.rowsets.entry(*rowset_reg).or_default(); let found = if *batch == 0 { // SQLite rowsets assume that in each batch, the caller makes sure no // duplicates are inserted. Hence if batch==0, we can return false without // checking. false } else { rowset.test(rowid, *batch) }; if found { state.pc = pc_if_found.as_offset_int(); } else { if *batch != -1 { rowset.insert(rowid); } state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_function( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( Function { constant_mask: _, func, start_reg, dest, }, insn ); let arg_count = func.arg_count; match &func.func { #[cfg(feature = "json")] crate::function::Func::Json(json_func) => match json_func { JsonFunc::Json => { let json_value = &state.registers[*start_reg]; let json_str = get_json(json_value.get_value(), None); match json_str { Ok(json) => state.registers[*dest].set_value(json), Err(e) => return Err(e), } } JsonFunc::Jsonb => { let json_value = &state.registers[*start_reg]; let json_blob = jsonb(json_value.get_value(), &state.json_cache); match json_blob { Ok(json) => state.registers[*dest].set_value(json), Err(e) => return Err(e), } } JsonFunc::JsonArray | JsonFunc::JsonObject | JsonFunc::JsonbArray | JsonFunc::JsonbObject => { let reg_values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]); let json_func = match json_func { JsonFunc::JsonArray => json_array, JsonFunc::JsonObject => json_object, JsonFunc::JsonbArray => jsonb_array, JsonFunc::JsonbObject => jsonb_object, _ => unreachable!(), }; let json_result = json_func(reg_values); match json_result { Ok(json) => state.registers[*dest].set_value(json), Err(e) => return Err(e), } } JsonFunc::JsonExtract => { let result = match arg_count { 0 => Ok(Value::Null), _ => { let val = &state.registers[*start_reg]; let reg_values = registers_to_ref_values( &state.registers[*start_reg + 1..*start_reg + arg_count], ); json_extract(val.get_value(), reg_values, &state.json_cache) } }; match result { Ok(json) => state.registers[*dest].set_value(json), Err(e) => return Err(e), } } JsonFunc::JsonbExtract => { let result = match arg_count { 0 => Ok(Value::Null), _ => { let val = &state.registers[*start_reg]; let reg_values = registers_to_ref_values( &state.registers[*start_reg + 1..*start_reg + arg_count], ); jsonb_extract(val.get_value(), reg_values, &state.json_cache) } }; match result { Ok(json) => state.registers[*dest].set_value(json), Err(e) => return Err(e), } } JsonFunc::JsonArrowExtract | JsonFunc::JsonArrowShiftExtract => { assert_eq!(arg_count, 2); let json = &state.registers[*start_reg]; let path = &state.registers[*start_reg + 1]; let json_func = match json_func { JsonFunc::JsonArrowExtract => json_arrow_extract, JsonFunc::JsonArrowShiftExtract => json_arrow_shift_extract, _ => unreachable!(), }; let json_str = json_func(json.get_value(), path.get_value(), &state.json_cache); match json_str { Ok(json) => state.registers[*dest].set_value(json), Err(e) => return Err(e), } } JsonFunc::JsonArrayLength | JsonFunc::JsonType => { let json_value = &state.registers[*start_reg]; let path_value = if arg_count > 1 { Some(&state.registers[*start_reg + 1]) } else { None }; let func_result = match json_func { JsonFunc::JsonArrayLength => json_array_length( json_value.get_value(), path_value.map(|x| x.get_value()), &state.json_cache, ), JsonFunc::JsonType => { json_type(json_value.get_value(), path_value.map(|x| x.get_value())) } _ => unreachable!(), }; match func_result { Ok(result) => state.registers[*dest].set_value(result), Err(e) => return Err(e), } } JsonFunc::JsonErrorPosition => { let json_value = &state.registers[*start_reg]; match json_error_position(json_value.get_value()) { Ok(pos) => state.registers[*dest].set_value(pos), Err(e) => return Err(e), } } JsonFunc::JsonValid => { let json_value = &state.registers[*start_reg]; state.registers[*dest].set_value(is_json_valid(json_value.get_value())); } JsonFunc::JsonPatch => { assert_eq!(arg_count, 2); assert!(*start_reg + 1 < state.registers.len()); let target = &state.registers[*start_reg]; let patch = &state.registers[*start_reg + 1]; state.registers[*dest].set_value(json_patch( target.get_value(), patch.get_value(), &state.json_cache, )?); } JsonFunc::JsonbPatch => { assert_eq!(arg_count, 2); assert!(*start_reg + 1 < state.registers.len()); let target = &state.registers[*start_reg]; let patch = &state.registers[*start_reg + 1]; state.registers[*dest].set_value(jsonb_patch( target.get_value(), patch.get_value(), &state.json_cache, )?); } JsonFunc::JsonRemove => { if let Ok(json) = json_remove( registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]), &state.json_cache, ) { state.registers[*dest].set_value(json); } else { state.registers[*dest].set_null(); } } JsonFunc::JsonbRemove => { if let Ok(json) = jsonb_remove( registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]), &state.json_cache, ) { state.registers[*dest].set_value(json); } else { state.registers[*dest].set_null(); } } JsonFunc::JsonReplace => { if let Ok(json) = json_replace( registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]), &state.json_cache, ) { state.registers[*dest].set_value(json); } else { state.registers[*dest].set_null(); } } JsonFunc::JsonbReplace => { if let Ok(json) = jsonb_replace( registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]), &state.json_cache, ) { state.registers[*dest].set_value(json); } else { state.registers[*dest].set_null(); } } JsonFunc::JsonInsert => { if let Ok(json) = json_insert( registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]), &state.json_cache, ) { state.registers[*dest].set_value(json); } else { state.registers[*dest].set_null(); } } JsonFunc::JsonbInsert => { if let Ok(json) = jsonb_insert( registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]), &state.json_cache, ) { state.registers[*dest].set_value(json); } else { state.registers[*dest].set_null(); } } JsonFunc::JsonPretty => { let json_value = &state.registers[*start_reg]; let indent = if arg_count > 1 { Some(&state.registers[*start_reg + 1]) } else { None }; // Blob should be converted to Ascii in a lossy way // However, Rust strings uses utf-8 // so the behavior at the moment is slightly different // To the way blobs are parsed here in SQLite. let indent = match indent { Some(value) => match value.get_value() { Value::Text(text) => text.as_str(), Value::Numeric(Numeric::Integer(val)) => &val.to_string(), Value::Numeric(Numeric::Float(val)) => &f64::from(*val).to_string(), Value::Blob(val) => &String::from_utf8_lossy(val), _ => " ", }, // If the second argument is omitted or is NULL, then indentation is four spaces per level None => " ", }; let json_str = get_json(json_value.get_value(), Some(indent))?; state.registers[*dest].set_value(json_str); } JsonFunc::JsonSet => { if arg_count % 2 == 0 { bail_constraint_error!("json_set() needs an odd number of arguments") } let reg_values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]); let json_result = json_set(reg_values, &state.json_cache); match json_result { Ok(json) => state.registers[*dest].set_value(json), Err(e) => return Err(e), } } JsonFunc::JsonbSet => { if arg_count % 2 == 0 { bail_constraint_error!("json_set() needs an odd number of arguments") } let reg_values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]); let json_result = jsonb_set(reg_values, &state.json_cache); match json_result { Ok(json) => state.registers[*dest].set_value(json), Err(e) => return Err(e), } } JsonFunc::JsonQuote => { let json_value = &state.registers[*start_reg]; match json_quote(json_value.get_value()) { Ok(result) => state.registers[*dest].set_value(result), Err(e) => return Err(e), } } }, crate::function::Func::Scalar(scalar_func) => match scalar_func { ScalarFunc::Array | ScalarFunc::ArrayElement | ScalarFunc::ArraySetElement => { unreachable!("desugared to dedicated instructions, not Function") } ScalarFunc::Cast => { assert_eq!(arg_count, 2); assert!(*start_reg + 1 < state.registers.len()); let reg_value_argument = state.registers[*start_reg].clone(); let Value::Text(reg_value_type) = state.registers[*start_reg + 1].get_value().clone() else { unreachable!("Cast with non-text type"); }; let result = reg_value_argument .get_value() .exec_cast(reg_value_type.as_str()); state.registers[*dest].set_value(result); } ScalarFunc::Changes => { let res = &program.connection.last_change; let changes = res.load(Ordering::SeqCst); state.registers[*dest].set_int(changes); } ScalarFunc::Char => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; state.registers[*dest].set_value(Value::exec_char( reg_values.iter().map(|reg| reg.get_value()), )); } ScalarFunc::Coalesce => {} ScalarFunc::Concat => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; let result = Value::exec_concat_strings(reg_values.iter().map(|reg| reg.get_value())); state.registers[*dest].set_value(result); } ScalarFunc::ConcatWs => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; let result = Value::exec_concat_ws(reg_values.iter().map(|reg| reg.get_value())); state.registers[*dest].set_value(result); } ScalarFunc::Glob => { if arg_count != 2 { mark_unlikely(); return Err(LimboError::ParseError( "wrong number of arguments to function GLOB()".to_string(), )); } let pattern_reg = &state.registers[*start_reg]; let match_reg = &state.registers[*start_reg + 1]; let pattern_value = pattern_reg.get_value(); let match_value = match_reg.get_value(); if pattern_value == &Value::Null || match_value == &Value::Null { state.registers[*dest].set_null(); } else { let pattern_cow = match pattern_value { Value::Text(s) => std::borrow::Cow::Borrowed(s.as_str()), v => match v.exec_cast("TEXT") { Value::Text(s) => std::borrow::Cow::Owned(s.to_string()), _ => unreachable!("Cast to TEXT should yield Text"), }, }; let match_cow = match match_value { Value::Text(s) => std::borrow::Cow::Borrowed(s.as_str()), v => match v.exec_cast("TEXT") { Value::Text(s) => std::borrow::Cow::Owned(s.to_string()), _ => unreachable!("Cast to TEXT should yield Text"), }, }; let matches = Value::exec_glob(&pattern_cow, &match_cow)?; state.registers[*dest].set_int(matches as i64); } } ScalarFunc::IfNull => {} ScalarFunc::Iif => {} ScalarFunc::Instr => { let reg_value = &state.registers[*start_reg]; let pattern_value = &state.registers[*start_reg + 1]; match reg_value.get_value().exec_instr(pattern_value.get_value()) { Value::Numeric(Numeric::Integer(i)) => state.registers[*dest].set_int(i), _ => state.registers[*dest].set_null(), }; } ScalarFunc::LastInsertRowid => { state.registers[*dest].set_int(program.connection.last_insert_rowid()); } ScalarFunc::Like => { let pattern_reg = &state.registers[*start_reg]; let match_reg = &state.registers[*start_reg + 1]; let pattern_value = pattern_reg.get_value(); let match_value = match_reg.get_value(); // 1. Check for NULL inputs if pattern_value == &Value::Null || match_value == &Value::Null { state.registers[*dest].set_null(); } else { // 2. Resolve Escape Character (if 3rd arg exists) let mut escape_char = None; let mut is_null_result = false; if arg_count == 3 { let escape_value = state.registers[*start_reg + 2].get_value(); match escape_value { Value::Null => { is_null_result = true; } _ => { let escape_cow = match escape_value { Value::Text(s) => std::borrow::Cow::Borrowed(s.as_str()), v => match v.exec_cast("TEXT") { Value::Text(s) => std::borrow::Cow::Owned(s.to_string()), _ => unreachable!("Cast to TEXT should yield Text"), }, }; let mut chars = escape_cow.chars(); let c = chars.next(); if c.is_none() || chars.next().is_some() { return Err(LimboError::Constraint( "ESCAPE expression must be a single character".to_string(), )); } escape_char = c; } } } if is_null_result { state.registers[*dest].set_null(); } else { // 3. Prepare Pattern and Text let pattern_cow = match pattern_value { Value::Text(s) => std::borrow::Cow::Borrowed(s.as_str()), v => match v.exec_cast("TEXT") { Value::Text(s) => std::borrow::Cow::Owned(s.to_string()), _ => unreachable!("Cast to TEXT should yield Text"), }, }; let match_cow = match match_value { Value::Text(s) => std::borrow::Cow::Borrowed(s.as_str()), v => match v.exec_cast("TEXT") { Value::Text(s) => std::borrow::Cow::Owned(s.to_string()), _ => unreachable!("Cast to TEXT should yield Text"), }, }; // 4. Execute Like let matches = Value::exec_like(&pattern_cow, &match_cow, escape_char)?; state.registers[*dest].set_int(matches as i64); } } } ScalarFunc::Abs | ScalarFunc::Lower | ScalarFunc::Upper | ScalarFunc::Length | ScalarFunc::OctetLength | ScalarFunc::Typeof | ScalarFunc::Unicode | ScalarFunc::Quote | ScalarFunc::RandomBlob | ScalarFunc::Sign | ScalarFunc::Soundex | ScalarFunc::ZeroBlob => { let reg_value = state.registers[*start_reg].borrow_mut().get_value(); let result = match scalar_func { ScalarFunc::Sign => reg_value.exec_sign(), ScalarFunc::Abs => Some(reg_value.exec_abs()?), ScalarFunc::Lower => reg_value.exec_lower(), ScalarFunc::Upper => reg_value.exec_upper(), ScalarFunc::Length => Some(reg_value.exec_length()), ScalarFunc::OctetLength => Some(reg_value.exec_octet_length()), ScalarFunc::Typeof => Some(reg_value.exec_typeof()), ScalarFunc::Unicode => Some(reg_value.exec_unicode()), ScalarFunc::Quote => Some(reg_value.exec_quote()), ScalarFunc::RandomBlob => { Some(reg_value.exec_randomblob(|dest| pager.io.fill_bytes(dest))?) } ScalarFunc::ZeroBlob => Some(reg_value.exec_zeroblob()?), ScalarFunc::Soundex => Some(reg_value.exec_soundex()), _ => unreachable!(), }; state.registers[*dest].set_value(result.unwrap_or(Value::Null)); } ScalarFunc::Hex => { let reg_value = state.registers[*start_reg].borrow_mut(); let result = reg_value.get_value().exec_hex(); state.registers[*dest].set_value(result); } ScalarFunc::Unhex => { let reg_value = &state.registers[*start_reg]; let ignored_chars = if func.arg_count == 2 { state.registers.get(*start_reg + 1) } else { None }; let result = reg_value .get_value() .exec_unhex(ignored_chars.map(|x| x.get_value())); state.registers[*dest].set_value(result); } ScalarFunc::Random => { state.registers[*dest].set_int(pager.io.generate_random_number()); } ScalarFunc::Trim => { let reg_value = &state.registers[*start_reg]; let pattern_value = if func.arg_count == 2 { state.registers.get(*start_reg + 1) } else { None }; let result = reg_value .get_value() .exec_trim(pattern_value.map(|x| x.get_value())); state.registers[*dest].set_value(result); } ScalarFunc::LTrim => { let reg_value = &state.registers[*start_reg]; let pattern_value = if func.arg_count == 2 { state.registers.get(*start_reg + 1) } else { None }; let result = reg_value .get_value() .exec_ltrim(pattern_value.map(|x| x.get_value())); state.registers[*dest].set_value(result); } ScalarFunc::RTrim => { let reg_value = &state.registers[*start_reg]; let pattern_value = if func.arg_count == 2 { state.registers.get(*start_reg + 1) } else { None }; let result = reg_value .get_value() .exec_rtrim(pattern_value.map(|x| x.get_value())); state.registers[*dest].set_value(result); } ScalarFunc::Round => { let reg_value = &state.registers[*start_reg]; assert!(arg_count == 1 || arg_count == 2); let precision_value = if arg_count > 1 { state.registers.get(*start_reg + 1) } else { None }; let result = reg_value .get_value() .exec_round(precision_value.map(|x| x.get_value())); state.registers[*dest].set_value(result); } ScalarFunc::Min => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; state.registers[*dest] .set_value(Value::exec_min(reg_values.iter().map(|v| v.get_value()))); } ScalarFunc::Max => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; state.registers[*dest] .set_value(Value::exec_max(reg_values.iter().map(|v| v.get_value()))); } ScalarFunc::Nullif => { let first_value = &state.registers[*start_reg]; let second_value = &state.registers[*start_reg + 1]; state.registers[*dest].set_value(Value::exec_nullif( first_value.get_value(), second_value.get_value(), )); } ScalarFunc::Substr | ScalarFunc::Substring => { let str_value = &state.registers[*start_reg]; let start_value = &state.registers[*start_reg + 1]; let length_value = if func.arg_count == 3 { Some(&state.registers[*start_reg + 2]) } else { None }; let result = Value::exec_substring( str_value.get_value(), start_value.get_value(), length_value.map(|x| x.get_value()), ); state.registers[*dest].set_value(result); } ScalarFunc::Date => { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]); let result = exec_date(values); state.registers[*dest].set_value(result); } ScalarFunc::Time => { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]); let result = exec_time(values); state.registers[*dest].set_value(result); } ScalarFunc::TimeDiff => { if arg_count != 2 { state.registers[*dest].set_null(); } else { let start = state.registers[*start_reg].get_value(); let end = state.registers[*start_reg + 1].get_value(); let result = crate::functions::datetime::exec_timediff([start, end]); state.registers[*dest].set_value(result); } } ScalarFunc::TotalChanges => { let res = &program.connection.total_changes; let total_changes = res.load(Ordering::SeqCst); state.registers[*dest].set_int(total_changes); } ScalarFunc::DateTime => { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]); let result = exec_datetime_full(values); state.registers[*dest].set_value(result); } ScalarFunc::JulianDay => { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]); let result = exec_julianday(values); state.registers[*dest].set_value(result); } ScalarFunc::UnixEpoch => { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]); let result = exec_unixepoch(values); state.registers[*dest].set_value(result); } ScalarFunc::TursoVersion => { if !program.connection.is_db_initialized() { state.registers[*dest].set_text(Text::new(info::build::PKG_VERSION)); } else { let version_integer = return_if_io!(pager.with_header(|header| header.version_number)).get() as i64; let version = execute_turso_version(version_integer); state.registers[*dest].set_text(Text::new(version)); } } ScalarFunc::SqliteVersion => { let version = execute_sqlite_version(); state.registers[*dest].set_text(Text::new(version)); } ScalarFunc::SqliteSourceId => { let src_id = format!( "{} {}", info::build::BUILT_TIME_SQLITE, info::build::GIT_COMMIT_HASH.unwrap_or("unknown") ); state.registers[*dest].set_text(Text::new(src_id)); } ScalarFunc::Replace => { assert_eq!(arg_count, 3); let source = &state.registers[*start_reg]; let pattern = &state.registers[*start_reg + 1]; let replacement = &state.registers[*start_reg + 2]; state.registers[*dest].set_value(Value::exec_replace( source.get_value(), pattern.get_value(), replacement.get_value(), )); } #[cfg(feature = "fs")] #[cfg(not(target_family = "wasm"))] ScalarFunc::LoadExtension => { if !program.connection.db.can_load_extensions() { crate::bail_parse_error!("runtime extension loading is disabled"); } let extension = &state.registers[*start_reg]; let ext = resolve_ext_path(&extension.get_value().to_string())?; program.connection.load_extension(ext)?; } ScalarFunc::StrfTime => { let values = registers_to_ref_values(&state.registers[*start_reg..*start_reg + arg_count]); let result = exec_strftime(values); state.registers[*dest].set_value(result); } ScalarFunc::Printf => { let result = exec_printf(&state.registers[*start_reg..*start_reg + arg_count])?; state.registers[*dest].set_value(result); } ScalarFunc::TableColumnsJsonArray => { assert_eq!(arg_count, 1); #[cfg(not(feature = "json"))] { return Err(LimboError::InvalidArgument( "table_columns_json_array: turso must be compiled with JSON support" .to_string(), )); } #[cfg(feature = "json")] { use crate::types::TextSubtype; let table = state.registers[*start_reg].get_value(); let Value::Text(table) = table else { return Err(LimboError::InvalidArgument( "table_columns_json_array: function argument must be of type TEXT" .to_string(), )); }; let table = { let schema = program.connection.schema.read(); match schema.get_table(table.as_str()) { Some(table) => table, None => { return Err(LimboError::InvalidArgument(format!( "table_columns_json_array: table {table} doesn't exists" ))); } } }; let mut json = json::jsonb::Jsonb::make_empty_array(table.columns().len() * 10); for column in table.columns() { use crate::types::TextRef; let name = column.name.as_ref().expect("column should have a name"); let name_json = json::convert_ref_dbtype_to_jsonb( ValueRef::Text(TextRef::new(name, TextSubtype::Text)), json::Conv::ToString, )?; json.append_jsonb_to_end(name_json.data()); } json.finalize_unsafe(json::jsonb::ElementType::ARRAY)?; state.registers[*dest].set_value(json::json_string_to_db_type( json, json::jsonb::ElementType::ARRAY, json::OutputVariant::String, )?); } } ScalarFunc::BinRecordJsonObject => { assert_eq!(arg_count, 2); #[cfg(not(feature = "json"))] { return Err(LimboError::InvalidArgument( "bin_record_json_object: turso must be compiled with JSON support" .to_string(), )); } #[cfg(feature = "json")] 'outer: { use crate::types::ValueIterator; use std::str::FromStr; let columns_str = state.registers[*start_reg].get_value(); let bin_record = state.registers[*start_reg + 1].get_value(); let Value::Text(columns_str) = columns_str else { return Err(LimboError::InvalidArgument( "bin_record_json_object: function arguments must be of type TEXT and BLOB correspondingly".to_string() )); }; if let Value::Null = bin_record { state.registers[*dest].set_null(); break 'outer; } let Value::Blob(bin_record) = bin_record else { return Err(LimboError::InvalidArgument( "bin_record_json_object: function arguments must be of type TEXT and BLOB correspondingly".to_string() )); }; let mut columns_json_array = json::jsonb::Jsonb::from_str(columns_str.as_str())?; let columns_len = columns_json_array.array_len()?; let mut payload_iterator = ValueIterator::new(bin_record.as_slice())?; let mut json = json::jsonb::Jsonb::make_empty_obj(columns_len); for i in 0..columns_len { let mut op = json::jsonb::SearchOperation::new(0); let path = json::path::JsonPath { elements: vec![ json::path::PathElement::Root(), json::path::PathElement::ArrayLocator(Some(i as i32)), ], }; columns_json_array.operate_on_path(&path, &mut op)?; let column_name = op.result(); json.append_jsonb_to_end(column_name.data()); let val = match payload_iterator.next() { Some(Ok(v)) => v, Some(Err(e)) => return Err(e), None => { return Err(LimboError::InvalidArgument( "bin_record_json_object: binary record has fewer columns than specified in the columns argument".to_string() )); } }; if let ValueRef::Blob(..) = val { return Err(LimboError::InvalidArgument( "bin_record_json_object: formatting of BLOB values stored in binary record is not supported".to_string() )); } let val_json = json::convert_ref_dbtype_to_jsonb(val, json::Conv::NotStrict)?; json.append_jsonb_to_end(val_json.data()); } json.finalize_unsafe(json::jsonb::ElementType::OBJECT)?; state.registers[*dest].set_value(json::json_string_to_db_type( json, json::jsonb::ElementType::OBJECT, json::OutputVariant::String, )?); } } ScalarFunc::Attach => { assert_eq!(arg_count, 3); let filename = state.registers[*start_reg].get_value(); let dbname = state.registers[*start_reg + 1].get_value(); let _key = state.registers[*start_reg + 2].get_value(); // Not used in read-only implementation let Value::Text(filename_str) = filename else { return Err(LimboError::InvalidArgument( "attach: filename argument must be text".to_string(), )); }; let Value::Text(dbname_str) = dbname else { return Err(LimboError::InvalidArgument( "attach: database name argument must be text".to_string(), )); }; program .connection .attach_database(filename_str.as_str(), dbname_str.as_str())?; state.registers[*dest].set_null(); } ScalarFunc::Detach => { assert_eq!(arg_count, 1); let dbname = state.registers[*start_reg].get_value(); let Value::Text(dbname_str) = dbname else { return Err(LimboError::InvalidArgument( "detach: database name argument must be text".to_string(), )); }; // Call the detach_database method on the connection program.connection.detach_database(dbname_str.as_str())?; // Set result to NULL (detach doesn't return a value) state.registers[*dest].set_null(); } ScalarFunc::Unlikely | ScalarFunc::Likely | ScalarFunc::Likelihood => { panic!( "{scalar_func:?} should be stripped during expression translation and never reach VDBE", ); } ScalarFunc::StatInit => { // stat_init(n_col): Initialize a statistics accumulator // Returns a blob containing the serialized StatAccum assert!(arg_count >= 1); let n_col = match state.registers[*start_reg].get_value() { Value::Numeric(Numeric::Integer(n)) => *n as usize, _ => 0, }; let accum = StatAccum::new(n_col); state.registers[*dest].set_blob(accum.to_bytes()); } ScalarFunc::StatPush => { // stat_push(accum_blob, i_chng): Push a row into the accumulator // i_chng is the index of the leftmost column that changed from the previous row // Returns the updated accumulator blob assert!(arg_count >= 2); let accum_blob = state.registers[*start_reg].get_value(); let i_chng = match state.registers[*start_reg + 1].get_value() { Value::Numeric(Numeric::Integer(n)) => *n as usize, _ => 0, }; let result = match accum_blob { Value::Blob(bytes) => { if let Some(mut accum) = StatAccum::from_bytes(bytes) { accum.push(i_chng); Value::Blob(accum.to_bytes()) } else { Value::Null } } _ => Value::Null, }; state.registers[*dest].set_value(result); } ScalarFunc::StatGet => { // stat_get(accum_blob): Get the stat1 string from the accumulator // Returns the stat string "total avg1 avg2 ..." assert!(arg_count >= 1); let accum_blob = state.registers[*start_reg].get_value(); let result = match accum_blob { Value::Blob(bytes) => { if let Some(accum) = StatAccum::from_bytes(bytes) { let stat_str = accum.get_stat1(); if stat_str.is_empty() { Value::Null } else { Value::build_text(stat_str) } } else { Value::Null } } _ => Value::Null, }; state.registers[*dest].set_value(result); } ScalarFunc::ConnTxnId => { // conn_txn_id(candidate): get-or-set semantics for CDC transaction ID. // If unset (-1), store the candidate and return it. // If already set, return the existing value, ignoring the candidate. assert_eq!(arg_count, 1); let candidate = match state.registers[*start_reg].get_value() { Value::Numeric(Numeric::Integer(n)) => *n, _ => -1, }; let current = program.connection.get_cdc_transaction_id(); if current == -1 { program.connection.set_cdc_transaction_id(candidate); state.registers[*dest].set_int(candidate); } else { state.registers[*dest].set_int(current); } } ScalarFunc::IsAutocommit => { // is_autocommit(): returns 1 if autocommit, 0 otherwise. let auto_commit = program.connection.auto_commit.load(Ordering::SeqCst); state.registers[*dest].set_int(if auto_commit { 1 } else { 0 }); } ScalarFunc::TestUintEncode => { check_arg_count!(arg_count, 1); let val = &state.registers[*start_reg]; let result = match val.get_value() { Value::Null => Value::Null, Value::Numeric(Numeric::Integer(i)) => { if *i < 0 { return Err(LimboError::InternalError( "test_uint_encode: negative value".to_string(), )); } Value::build_text(i.to_string()) } Value::Numeric(Numeric::Float(f)) => { if *f < 0.0 || f.fract() != 0.0 { return Err(LimboError::InternalError( "test_uint_encode: not a non-negative integer".to_string(), )); } Value::build_text((f64::from(*f) as u64).to_string()) } Value::Text(t) => { let s = t.to_string(); s.parse::().map_err(|_| { LimboError::InternalError(format!( "test_uint_encode: invalid uint: {s}" )) })?; Value::build_text(s) } _ => { return Err(LimboError::InternalError( "test_uint_encode: unsupported type".to_string(), )); } }; state.registers[*dest].set_value(result); } ScalarFunc::TestUintDecode => { check_arg_count!(arg_count, 1); let val = &state.registers[*start_reg]; let result = match val.get_value() { Value::Null => Value::Null, other => other.clone(), }; state.registers[*dest].set_value(result); } ScalarFunc::TestUintAdd | ScalarFunc::TestUintSub | ScalarFunc::TestUintMul | ScalarFunc::TestUintDiv => { check_arg_count!(arg_count, 2); let a = parse_test_uint(&state.registers[*start_reg])?; let b = parse_test_uint(&state.registers[*start_reg + 1])?; let result = match (a, b) { (Some(a), Some(b)) => { let r = match scalar_func { ScalarFunc::TestUintAdd => a.checked_add(b), ScalarFunc::TestUintSub => a.checked_sub(b), ScalarFunc::TestUintMul => a.checked_mul(b), ScalarFunc::TestUintDiv => { if b == 0 { None } else { Some(a / b) } } _ => unreachable!(), }; match r { Some(v) => Value::build_text(v.to_string()), None => { return Err(LimboError::InternalError( "test_uint arithmetic overflow/underflow".to_string(), )); } } } _ => Value::Null, }; state.registers[*dest].set_value(result); } ScalarFunc::TestUintLt | ScalarFunc::TestUintEq => { check_arg_count!(arg_count, 2); let a = parse_test_uint(&state.registers[*start_reg])?; let b = parse_test_uint(&state.registers[*start_reg + 1])?; let result = match (a, b) { (Some(a), Some(b)) => { let cmp = match scalar_func { ScalarFunc::TestUintLt => a < b, ScalarFunc::TestUintEq => a == b, _ => unreachable!(), }; Value::from_i64(if cmp { 1 } else { 0 }) } _ => Value::Null, }; state.registers[*dest].set_value(result); } ScalarFunc::StringReverse => { check_arg_count!(arg_count, 1); let val = &state.registers[*start_reg]; let result = match val.get_value() { Value::Null => Value::Null, Value::Text(t) => { let reversed: String = t.to_string().chars().rev().collect(); Value::build_text(reversed) } other => { let s = other.to_string(); let reversed: String = s.chars().rev().collect(); Value::build_text(reversed) } }; state.registers[*dest].set_value(result); } ScalarFunc::BooleanToInt => { check_arg_count!(arg_count, 1); let val = &state.registers[*start_reg]; let result = match val.get_value() { Value::Null => Value::Null, Value::Numeric(Numeric::Integer(i)) => match *i { 0 => Value::from_i64(0), 1 => Value::from_i64(1), _ => { return Err(LimboError::Constraint(format!( "invalid input for type boolean: \"{i}\"" ))); } }, Value::Text(t) => { let v = &t.value; match v.to_ascii_lowercase().as_str() { "true" | "t" | "yes" | "on" | "1" => Value::from_i64(1), "false" | "f" | "no" | "off" | "0" => Value::from_i64(0), _ => { return Err(LimboError::Constraint(format!( "invalid input for type boolean: \"{v}\"" ))); } } } other => { return Err(LimboError::Constraint(format!( "invalid input for type boolean: \"{other}\"" ))); } }; state.registers[*dest].set_value(result); } ScalarFunc::IntToBoolean => { check_arg_count!(arg_count, 1); let val = &state.registers[*start_reg]; let result = match val.get_value() { Value::Null => Value::Null, Value::Numeric(Numeric::Integer(0)) => Value::build_text("false".to_string()), _ => Value::build_text("true".to_string()), }; state.registers[*dest].set_value(result); } ScalarFunc::ValidateIpAddr => { check_arg_count!(arg_count, 1); let val = &state.registers[*start_reg]; let result = match val.get_value() { Value::Null => Value::Null, Value::Text(t) => { let v = &t.value; v.parse::().map_err(|_| { LimboError::Constraint(format!("invalid input for type inet: \"{v}\"")) })?; val.get_value().clone() } other => { return Err(LimboError::Constraint(format!( "invalid input for type inet: \"{other}\"" ))); } }; state.registers[*dest].set_value(result); } ScalarFunc::NumericEncode => { check_arg_count!(arg_count, 3); let val = &state.registers[*start_reg]; let precision_reg = &state.registers[*start_reg + 1]; let scale_reg = &state.registers[*start_reg + 2]; let result = match val.get_value() { Value::Null => Value::Null, other => { use crate::numeric::decimal::{ bigdecimal_to_blob, validate_precision_scale, }; use bigdecimal::BigDecimal; use std::str::FromStr; let precision = match precision_reg.get_value() { Value::Numeric(Numeric::Integer(i)) => *i, _ => { return Err(LimboError::Constraint( "numeric_encode: precision must be an integer".to_string(), )); } }; let scale = match scale_reg.get_value() { Value::Numeric(Numeric::Integer(i)) => *i, _ => { return Err(LimboError::Constraint( "numeric_encode: scale must be an integer".to_string(), )); } }; let text = match other { Value::Numeric(Numeric::Integer(i)) => i.to_string(), Value::Numeric(Numeric::Float(f)) => f.to_string(), Value::Text(t) => t.value.to_string(), _ => { return Err(LimboError::Constraint(format!( "invalid input for type numeric: \"{other}\"" ))); } }; let bd = BigDecimal::from_str(&text).map_err(|_| { LimboError::Constraint(format!( "invalid input for type numeric: \"{text}\"" )) })?; let validated = validate_precision_scale(&bd, precision, scale)?; Value::from_blob(bigdecimal_to_blob(&validated)) } }; state.registers[*dest].set_value(result); } ScalarFunc::NumericDecode => { check_arg_count!(arg_count, 1); let val = &state.registers[*start_reg]; let result = match val.get_value() { Value::Null => Value::Null, Value::Blob(b) => { let bd = crate::numeric::decimal::blob_to_bigdecimal(b)?; Value::build_text(crate::numeric::decimal::format_numeric(&bd)) } other => { return Err(LimboError::Constraint(format!( "numeric_decode: expected blob, got \"{other}\"" ))); } }; state.registers[*dest].set_value(result); } ScalarFunc::NumericAdd | ScalarFunc::NumericSub | ScalarFunc::NumericMul | ScalarFunc::NumericDiv => { check_arg_count!(arg_count, 2); let lhs_val = state.registers[*start_reg].get_value().clone(); let rhs_val = state.registers[*start_reg + 1].get_value().clone(); let result = match (&lhs_val, &rhs_val) { (Value::Null, _) | (_, Value::Null) => Value::Null, _ => { let a = value_to_bigdecimal(&lhs_val)?; let b = value_to_bigdecimal(&rhs_val)?; let res = match scalar_func { ScalarFunc::NumericAdd => a + b, ScalarFunc::NumericSub => a - b, ScalarFunc::NumericMul => a * b, ScalarFunc::NumericDiv => { use bigdecimal::Zero; if b.is_zero() { return Err(LimboError::Constraint( "division by zero".to_string(), )); } a / b } _ => unreachable!(), }; Value::build_text(crate::numeric::decimal::format_numeric(&res)) } }; state.registers[*dest].set_value(result); } ScalarFunc::NumericLt | ScalarFunc::NumericEq => { check_arg_count!(arg_count, 2); let lhs_val = state.registers[*start_reg].get_value().clone(); let rhs_val = state.registers[*start_reg + 1].get_value().clone(); match (&lhs_val, &rhs_val) { (Value::Null, _) | (_, Value::Null) => state.registers[*dest].set_null(), _ => { let a = value_to_bigdecimal(&lhs_val)?; let b = value_to_bigdecimal(&rhs_val)?; let cmp_result = match scalar_func { ScalarFunc::NumericLt => a < b, ScalarFunc::NumericEq => a == b, _ => unreachable!(), }; state.registers[*dest].set_int(cmp_result as i64) } }; } ScalarFunc::ArrayAppend => { check_arg_count!(arg_count, 2); let arr_val = state.registers[*start_reg].get_value().clone(); let elem_val = state.registers[*start_reg + 1].get_value().clone(); state.registers[*dest].set_value(exec_array_append(&arr_val, &elem_val)); } ScalarFunc::ArrayPrepend => { check_arg_count!(arg_count, 2); let elem_val = state.registers[*start_reg].get_value().clone(); let arr_val = state.registers[*start_reg + 1].get_value().clone(); state.registers[*dest].set_value(exec_array_prepend(&arr_val, &elem_val)); } ScalarFunc::ArrayCat => { check_arg_count!(arg_count, 2); let a_val = state.registers[*start_reg].get_value().clone(); let b_val = state.registers[*start_reg + 1].get_value().clone(); state.registers[*dest].set_value(exec_array_cat(&a_val, &b_val)); } ScalarFunc::ArrayRemove => { check_arg_count!(arg_count, 2); let arr_val = state.registers[*start_reg].get_value().clone(); let target = state.registers[*start_reg + 1].get_value().clone(); state.registers[*dest].set_value(exec_array_remove(&arr_val, &target)); } ScalarFunc::ArrayContains => { check_arg_count!(arg_count, 2); let arr_val = state.registers[*start_reg].get_value().clone(); let target = state.registers[*start_reg + 1].get_value().clone(); state.registers[*dest].set_value(exec_array_contains(&arr_val, &target)); } ScalarFunc::ArrayPosition => { check_arg_count!(arg_count, 2); let arr_val = state.registers[*start_reg].get_value().clone(); let target = state.registers[*start_reg + 1].get_value().clone(); state.registers[*dest].set_value(exec_array_position(&arr_val, &target)); } ScalarFunc::ArrayLength => { // Accept 1 or 2 args; dimension arg (PG compat) ignored for 1D arrays let arr_val = state.registers[*start_reg].get_value(); match compute_array_length(arr_val) { Some(count) => state.registers[*dest].set_int(count), None => state.registers[*dest].set_null(), }; } ScalarFunc::ArraySlice => { check_arg_count!(arg_count, 3); let arr_val = state.registers[*start_reg].get_value().clone(); let start_idx = state.registers[*start_reg + 1].get_value().clone(); let end_idx = state.registers[*start_reg + 2].get_value().clone(); let result = exec_array_slice(&arr_val, &start_idx, &end_idx); state.registers[*dest].set_value(result); } ScalarFunc::StringToArray => { let text = state.registers[*start_reg].get_value().clone(); let delimiter = state.registers[*start_reg + 1].get_value().clone(); let null_str = if arg_count >= 3 { Some(state.registers[*start_reg + 2].get_value().clone()) } else { None }; state.registers[*dest].set_value(exec_string_to_array( &text, &delimiter, null_str.as_ref(), )); } ScalarFunc::ArrayToString => { let arr_val = state.registers[*start_reg].get_value().clone(); let delimiter = state.registers[*start_reg + 1].get_value().clone(); let null_str = if arg_count >= 3 { Some(state.registers[*start_reg + 2].get_value().clone()) } else { None }; state.registers[*dest].set_value(exec_array_to_string( &arr_val, &delimiter, null_str.as_ref(), )); } ScalarFunc::ArrayOverlap => { check_arg_count!(arg_count, 2); let a_val = state.registers[*start_reg].get_value().clone(); let b_val = state.registers[*start_reg + 1].get_value().clone(); state.registers[*dest].set_value(exec_array_overlap(&a_val, &b_val)); } ScalarFunc::ArrayContainsAll => { check_arg_count!(arg_count, 2); let a_val = state.registers[*start_reg].get_value().clone(); let b_val = state.registers[*start_reg + 1].get_value().clone(); state.registers[*dest].set_value(exec_array_contains_all(&a_val, &b_val)); } }, crate::function::Func::Vector(vector_func) => { let args = &state.registers[*start_reg..*start_reg + arg_count]; match vector_func { VectorFunc::Vector => { let result = vector32(args)?; state.registers[*dest].set_value(result); } VectorFunc::Vector32 => { let result = vector32(args)?; state.registers[*dest].set_value(result); } VectorFunc::Vector32Sparse => { let result = vector32_sparse(args)?; state.registers[*dest].set_value(result); } VectorFunc::Vector64 => { let result = vector64(args)?; state.registers[*dest].set_value(result); } VectorFunc::Vector8 => { let result = vector8(args)?; state.registers[*dest].set_value(result); } VectorFunc::Vector1Bit => { let result = vector1bit(args)?; state.registers[*dest].set_value(result); } VectorFunc::VectorExtract => { let result = vector_extract(args)?; state.registers[*dest].set_value(result); } VectorFunc::VectorDistanceCos => { let result = vector_distance_cos(args)?; state.registers[*dest].set_value(result); } VectorFunc::VectorDistanceDot => { let result = vector_distance_dot(args)?; state.registers[*dest].set_value(result); } VectorFunc::VectorDistanceL2 => { let result = vector_distance_l2(args)?; state.registers[*dest].set_value(result); } VectorFunc::VectorDistanceJaccard => { let result = vector_distance_jaccard(args)?; state.registers[*dest].set_value(result); } VectorFunc::VectorConcat => { let result = vector_concat(args)?; state.registers[*dest].set_value(result); } VectorFunc::VectorSlice => { let result = vector_slice(args)?; state.registers[*dest].set_value(result) } } } crate::function::Func::External(f) => match f.func { ExtFunc::Scalar(f) => { if arg_count == 0 { let result_c_value: ExtValue = unsafe { (f)(0, std::ptr::null()) }; match Value::from_ffi(result_c_value) { Ok(result_ov) => { state.registers[*dest].set_value(result_ov); } Err(e) => { return Err(e); } } } else { let register_slice = &state.registers[*start_reg..*start_reg + arg_count]; let mut ext_values: Vec = Vec::with_capacity(arg_count); for ov in register_slice.iter() { let val = ov.get_value().to_ffi(); ext_values.push(val); } let argv_ptr = ext_values.as_ptr(); let result_c_value: ExtValue = unsafe { (f)(arg_count as i32, argv_ptr) }; match Value::from_ffi(result_c_value) { Ok(result_ov) => { state.registers[*dest].set_value(result_ov); } Err(e) => { return Err(e); } } } } _ => unreachable!("aggregate called in scalar context"), }, crate::function::Func::Math(math_func) => match math_func.arity() { MathFuncArity::Nullary => match math_func { MathFunc::Pi => { state.registers[*dest].set_float( NonNan::new(std::f64::consts::PI).expect("PI is a valid NonNan"), ); } _ => { unreachable!("Unexpected mathematical Nullary function {:?}", math_func); } }, MathFuncArity::Unary => { let reg_value = &state.registers[*start_reg]; let result = reg_value.get_value().exec_math_unary(math_func); state.registers[*dest].set_value(result); } MathFuncArity::Binary => { let lhs = &state.registers[*start_reg]; let rhs = &state.registers[*start_reg + 1]; let result = lhs.get_value().exec_math_binary(rhs.get_value(), math_func); state.registers[*dest].set_value(result); } MathFuncArity::UnaryOrBinary => match math_func { MathFunc::Log => { let result = match arg_count { 1 => { let arg = &state.registers[*start_reg]; arg.get_value().exec_math_log(None) } 2 => { let base = &state.registers[*start_reg]; let arg = &state.registers[*start_reg + 1]; arg.get_value().exec_math_log(Some(base.get_value())) } _ => unreachable!( "{:?} function with unexpected number of arguments", math_func ), }; state.registers[*dest].set_value(result); } _ => unreachable!( "Unexpected mathematical UnaryOrBinary function {:?}", math_func ), }, }, crate::function::Func::AlterTable(alter_func) => { let r#type = &state.registers[*start_reg].get_value().clone(); let Value::Text(name) = &state.registers[*start_reg + 1].get_value() else { panic!("sqlite_schema.name should be TEXT") }; let name = name.to_string(); let Value::Text(tbl_name) = &state.registers[*start_reg + 2].get_value() else { panic!("sqlite_schema.tbl_name should be TEXT") }; let tbl_name = tbl_name.to_string(); let Value::Numeric(Numeric::Integer(root_page)) = &state.registers[*start_reg + 3].get_value().clone() else { panic!("sqlite_schema.root_page should be INTEGER") }; let sql = &state.registers[*start_reg + 4].get_value().clone(); let (new_name, new_tbl_name, new_sql) = match alter_func { AlterTableFunc::RenameTable => { let rename_from = { match &state.registers[*start_reg + 5].get_value() { Value::Text(rename_from) => normalize_ident(rename_from.as_str()), _ => panic!("rename_from parameter should be TEXT"), } }; let original_rename_to = { match &state.registers[*start_reg + 6].get_value() { Value::Text(rename_to) => rename_to, _ => panic!("rename_to parameter should be TEXT"), } }; let rename_to = normalize_ident(original_rename_to.as_str()); let new_name = if let Some(column) = &name.strip_prefix(&format!("sqlite_autoindex_{rename_from}_")) { format!("sqlite_autoindex_{rename_to}_{column}") } else if name == rename_from { rename_to.clone() } else { name }; let new_tbl_name = if tbl_name == rename_from { rename_to.clone() } else { tbl_name }; let new_sql = 'sql: { let Value::Text(sql) = sql else { break 'sql None; }; let mut parser = Parser::new(sql.as_str().as_bytes()); let ast::Cmd::Stmt(stmt) = parser.next().expect("parser should have next item")? else { return Err(LimboError::InternalError( "Unexpected command during ALTER TABLE RENAME processing" .to_string(), )); }; match stmt { ast::Stmt::CreateIndex { tbl_name, unique, if_not_exists, idx_name, columns, where_clause, using, with_clause, } => { let table_name = normalize_ident(tbl_name.as_str()); if rename_from != table_name { break 'sql None; } Some( ast::Stmt::CreateIndex { tbl_name: ast::Name::exact(original_rename_to.to_string()), unique, if_not_exists, idx_name, columns, where_clause, using, with_clause, } .to_string(), ) } ast::Stmt::CreateTable { tbl_name, temporary, if_not_exists, body, } => { let this_table = normalize_ident(tbl_name.name.as_str()); let ast::CreateTableBody::ColumnsAndConstraints { mut columns, mut constraints, options, } = body else { return Err(LimboError::InternalError( "CREATE TABLE AS SELECT schemas cannot be altered" .to_string(), )); }; let mut any_change = false; // Rewrite FK targets in both paths for c in &mut constraints { if let ast::TableConstraint::ForeignKey { clause, .. } = &mut c.constraint { any_change |= rewrite_fk_parent_table_if_needed( clause, &rename_from, original_rename_to.as_str(), ); } } for col in &mut columns { any_change |= rewrite_inline_col_fk_target_if_needed( col, &rename_from, original_rename_to.as_str(), ); } // Rewrite table-qualified refs in CHECK constraints // (e.g. t1.a > 0 → t2.a > 0) if this_table == rename_from { for c in &mut constraints { if let ast::TableConstraint::Check(ref mut expr) = c.constraint { rewrite_check_expr_table_refs( expr, &rename_from, &rename_to, ); } } for col in &mut columns { for cc in &mut col.constraints { if let ast::ColumnConstraint::Check(ref mut expr) = cc.constraint { rewrite_check_expr_table_refs( expr, &rename_from, &rename_to, ); } } } } if this_table == rename_from { // Rebuild with new table identifier so SQL persists the new name. let new_stmt = ast::Stmt::CreateTable { tbl_name: ast::QualifiedName { db_name: None, name: ast::Name::exact(original_rename_to.to_string()), alias: None, }, temporary, if_not_exists, body: ast::CreateTableBody::ColumnsAndConstraints { columns, constraints, options, }, }; Some(new_stmt.to_string()) } else { // Other tables: only emit if we actually changed their FK targets. if !any_change { break 'sql None; } Some( ast::Stmt::CreateTable { tbl_name, temporary, if_not_exists, body: ast::CreateTableBody::ColumnsAndConstraints { columns, constraints, options, }, } .to_string(), ) } } ast::Stmt::CreateVirtualTable(ast::CreateVirtualTable { tbl_name, if_not_exists, module_name, args, }) => { let this_table = normalize_ident(tbl_name.name.as_str()); if this_table != rename_from { None } else { let new_stmt = ast::Stmt::CreateVirtualTable(ast::CreateVirtualTable { tbl_name: ast::QualifiedName { db_name: tbl_name.db_name, name: ast::Name::exact( original_rename_to.to_string(), ), alias: None, }, if_not_exists, module_name, args, }); Some(new_stmt.to_string()) } } ast::Stmt::CreateTrigger { temporary, if_not_exists, trigger_name, time, event, tbl_name: trigger_tbl_name, for_each_row, mut when_clause, mut commands, } => { let trigger_tbl = normalize_ident(trigger_tbl_name.name.as_str()); // Rewrite ON table name if it matches the renamed table let new_trigger_tbl_name = if trigger_tbl == rename_from { ast::QualifiedName { db_name: trigger_tbl_name.db_name, name: ast::Name::exact(original_rename_to.to_string()), alias: None, } } else { trigger_tbl_name }; // Rewrite WHEN clause qualified refs if let Some(ref mut when) = when_clause { rewrite_check_expr_table_refs( when, &rename_from, original_rename_to.as_str(), ); } // Rewrite table references in trigger body commands for cmd in &mut commands { rewrite_trigger_cmd_table_refs( cmd, &rename_from, original_rename_to.as_str(), ); } Some( ast::Stmt::CreateTrigger { temporary, if_not_exists, trigger_name, time, event, tbl_name: new_trigger_tbl_name, for_each_row, when_clause, commands, } .to_string(), ) } _ => None, } }; (new_name, new_tbl_name, new_sql) } AlterTableFunc::AlterColumn | AlterTableFunc::RenameColumn => { let table = { match &state.registers[*start_reg + 5].get_value() { Value::Text(rename_to) => normalize_ident(rename_to.as_str()), _ => panic!("table parameter should be TEXT"), } }; let original_rename_from = { match &state.registers[*start_reg + 6].get_value() { Value::Text(rename_from) => rename_from, _ => panic!("rename_from parameter should be TEXT"), } }; let rename_from = normalize_ident(original_rename_from.as_str()); let column_def = { match &state.registers[*start_reg + 7].get_value() { Value::Text(column_def) => column_def.as_str(), _ => panic!("rename_to parameter should be TEXT"), } }; let column_def = Parser::new(column_def.as_bytes()).parse_column_definition(true)?; let _rename_to = normalize_ident(column_def.col_name.as_str()); let new_sql = 'sql: { let Value::Text(sql) = sql else { break 'sql None; }; let mut parser = Parser::new(sql.as_str().as_bytes()); let ast::Cmd::Stmt(stmt) = parser.next().expect("parser should have next item")? else { return Err(LimboError::InternalError( "Unexpected command during ALTER TABLE RENAME COLUMN processing" .to_string(), )); }; match stmt { ast::Stmt::CreateIndex { tbl_name, mut columns, unique, if_not_exists, idx_name, mut where_clause, using, with_clause, } => { if table != normalize_ident(tbl_name.as_str()) { break 'sql None; } for column in &mut columns { match column.expr.as_mut() { ast::Expr::Id(id) if normalize_ident(id.as_str()) == rename_from => { *id = Name::exact( column_def.col_name.as_str().to_owned(), ); } _ => {} } } if let Some(ref mut wc) = where_clause { rename_identifiers( wc, &rename_from, column_def.col_name.as_str(), ); } Some( ast::Stmt::CreateIndex { tbl_name, columns, unique, if_not_exists, idx_name, where_clause, using, with_clause, } .to_string(), ) } ast::Stmt::CreateTable { tbl_name, body, temporary, if_not_exists, } => { let ast::CreateTableBody::ColumnsAndConstraints { mut columns, mut constraints, options, } = body else { return Err(LimboError::InternalError( "CREATE TABLE AS SELECT schemas cannot be altered" .to_string(), )); }; let normalized_tbl_name = normalize_ident(tbl_name.name.as_str()); if normalized_tbl_name == table { // This is the table being altered - update its column let column = columns .iter_mut() .find(|column| { normalize_ident(column.col_name.as_str()) == rename_from }) .expect("column being renamed should be present"); match alter_func { AlterTableFunc::AlterColumn => *column = column_def.clone(), AlterTableFunc::RenameColumn => { column.col_name = column_def.col_name.clone() } _ => unreachable!(), } // Update table-level constraints (PRIMARY KEY, UNIQUE, FOREIGN KEY) for constraint in &mut constraints { match &mut constraint.constraint { ast::TableConstraint::PrimaryKey { columns: pk_cols, .. } => { for col in pk_cols { let (ast::Expr::Name(ref name) | ast::Expr::Id(ref name)) = *col.expr else { return Err(LimboError::ParseError("Unexpected expression in PRIMARY KEY constraint".to_string())); }; if normalize_ident(name.as_str()) == rename_from { *col.expr = ast::Expr::Name(Name::exact( column_def.col_name.as_str().to_owned(), )); } } } ast::TableConstraint::Unique { columns: uniq_cols, .. } => { for col in uniq_cols { let (ast::Expr::Name(ref name) | ast::Expr::Id(ref name)) = *col.expr else { return Err(LimboError::ParseError("Unexpected expression in UNIQUE constraint".to_string())); }; if normalize_ident(name.as_str()) == rename_from { *col.expr = ast::Expr::Name(Name::exact( column_def.col_name.as_str().to_owned(), )); } } } ast::TableConstraint::ForeignKey { columns: child_cols, clause, .. } => { // Update child columns in this table's FK definitions for child_col in child_cols { if normalize_ident(child_col.col_name.as_str()) == rename_from { child_col.col_name = Name::exact( column_def.col_name.as_str().to_owned(), ); } } rewrite_fk_parent_cols_if_self_ref( clause, &normalized_tbl_name, &rename_from, column_def.col_name.as_str(), ); } ast::TableConstraint::Check(ref mut expr) => { rename_identifiers( expr, &rename_from, column_def.col_name.as_str(), ); } } } for col in &mut columns { rewrite_column_references_if_needed( col, &normalized_tbl_name, &rename_from, column_def.col_name.as_str(), ); } } else { // This is a different table, check if it has FKs referencing the renamed column let mut fk_updated = false; for constraint in &mut constraints { if let ast::TableConstraint::ForeignKey { columns: _, clause: ForeignKeyClause { tbl_name, columns: parent_cols, .. }, .. } = &mut constraint.constraint { // Check if this FK references the table being altered if normalize_ident(tbl_name.as_str()) == table { // Update parent column references if they match the renamed column for parent_col in parent_cols { if normalize_ident(parent_col.col_name.as_str()) == rename_from { parent_col.col_name = Name::exact( column_def.col_name.as_str().to_owned(), ); fk_updated = true; } } } } } for col in &mut columns { let _before = fk_updated; let mut local_col = col.clone(); rewrite_column_references_if_needed( &mut local_col, &table, &rename_from, column_def.col_name.as_str(), ); if local_col != *col { *col = local_col; fk_updated = true; } } // Only return updated SQL if we actually changed something if !fk_updated { break 'sql None; } } Some( ast::Stmt::CreateTable { tbl_name, body: ast::CreateTableBody::ColumnsAndConstraints { columns, constraints, options, }, temporary, if_not_exists, } .to_string(), ) } // Trigger SQL is rewritten by separate UPDATE statements // generated by alter.rs (via rewrite_trigger_sql_for_column_rename), // so we skip triggers here to avoid redundant work. _ => None, } }; (name, tbl_name, new_sql) } }; state.registers[*dest].set_value(r#type.clone()); state.registers[*dest + 1].set_text(Text::from(new_name)); state.registers[*dest + 2].set_text(Text::from(new_tbl_name)); state.registers[*dest + 3].set_int(*root_page); if let Some(new_sql) = new_sql { state.registers[*dest + 4].set_text(Text::from(new_sql)); } else { state.registers[*dest + 4].set_value(sql.clone()); } } #[cfg(all(feature = "fts", not(target_family = "wasm")))] crate::function::Func::Fts(fts_func) => { // FTS functions are typically handled via index method pattern matching. // If we reach here, just return a fallback since no FTS index matched. use crate::function::FtsFunc; match fts_func { FtsFunc::Score => { // Without an FTS index match, return 0.0 as a default score state.registers[*dest] .set_float(NonNan::new(0.0).expect("0.0 is a valid NonNan")); } FtsFunc::Match => { // fts_match(col1, col2, ..., query): returns 1 if any column matches query // Minimum: fts_match(text, query) = 2 args if arg_count < 2 { return Err(LimboError::InvalidArgument( "fts_match requires at least 2 arguments: text, query".to_string(), )); } // Last arg is the query, first N-1 args are text columns let num_text_cols = arg_count - 1; let query = state.registers[*start_reg + num_text_cols].get_value(); if matches!(query, Value::Null) { state.registers[*dest].set_int(0); } else { let query_str = query.to_string(); // Concatenate all text columns with space separator let est_len = 16; let mut combined_text = String::with_capacity(num_text_cols * est_len); for i in 0..num_text_cols { let text = state.registers[*start_reg + i].get_value(); if !matches!(text, Value::Null) { if !combined_text.is_empty() { combined_text.push(' '); } combined_text.push_str(&text.to_string()); } } let matches = crate::index_method::fts::fts_match(&combined_text, &query_str); state.registers[*dest].set_int(matches.into()); } } FtsFunc::Highlight => { // fts_highlight(col1, col2, ..., before_tag, after_tag, query) // Variable number of text columns, followed by before_tag, after_tag, query // Minimum: fts_highlight(text, before_tag, after_tag, query) = 4 args if arg_count < 4 { return Err(LimboError::InvalidArgument( "fts_highlight requires at least 4 arguments: text, before_tag, after_tag, query" .to_string(), )); } // Last 3 args are: before_tag, after_tag, query // First N-3 args are text columns let num_text_cols = arg_count - 3; let before_tag = state.registers[*start_reg + num_text_cols].get_value(); let after_tag = state.registers[*start_reg + num_text_cols + 1].get_value(); let query = state.registers[*start_reg + num_text_cols + 2].get_value(); // Handle NULL values in tags or query if matches!(query, Value::Null) || matches!(before_tag, Value::Null) || matches!(after_tag, Value::Null) { state.registers[*dest].set_null(); } else { let query_str = query.to_string(); let before_str = before_tag.to_string(); let after_str = after_tag.to_string(); // Concatenate all text columns with space separator let mut combined_text = String::new(); for i in 0..num_text_cols { let text = state.registers[*start_reg + i].get_value(); if !matches!(text, Value::Null) { if !combined_text.is_empty() { combined_text.push(' '); } combined_text.push_str(&text.to_string()); } } let highlighted = crate::index_method::fts::fts_highlight( &combined_text, &query_str, &before_str, &after_str, ); state.registers[*dest].set_text(Text::new(highlighted)); } } } } crate::function::Func::Agg(_) => { unreachable!("Aggregate functions should not be handled here") } crate::function::Func::Window(_) => { unreachable!("Window functions should not be handled here") } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sequence( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Sequence { cursor_id, target_reg }, insn ); let cursor_seq = state .cursor_seqs .get_mut(*cursor_id) .expect("cursor_id should be valid"); let seq_num = *cursor_seq; *cursor_seq += 1; state.registers[*target_reg].set_int(seq_num); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_sequence_test( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( SequenceTest { cursor_id, target_pc, value_reg: _ }, insn ); let cursor_seq = state .cursor_seqs .get_mut(*cursor_id) .expect("cursor_id should be valid"); let was_zero = *cursor_seq == 0; *cursor_seq += 1; state.pc = if was_zero { target_pc.as_offset_int() } else { state.pc + 1 }; Ok(InsnFunctionStepResult::Step) } pub fn op_init_coroutine( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( InitCoroutine { yield_reg, jump_on_definition, start_offset, }, insn ); assert!(jump_on_definition.is_offset()); let start_offset = start_offset.as_offset_int(); state.registers[*yield_reg].set_int(start_offset as i64); state.ended_coroutine.retain(|n| *n != *yield_reg as u32); let jump_on_definition = jump_on_definition.as_offset_int(); state.pc = if jump_on_definition == 0 { state.pc + 1 } else { jump_on_definition }; Ok(InsnFunctionStepResult::Step) } pub fn op_end_coroutine( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(EndCoroutine { yield_reg }, insn); if let Value::Numeric(Numeric::Integer(pc)) = state.registers[*yield_reg].get_value() { state.ended_coroutine.push(*yield_reg as u32); let pc: u32 = (*pc) .try_into() .unwrap_or_else(|_| panic!("EndCoroutine: pc overflow: {pc}")); state.pc = pc - 1; // yield jump is always next to yield. Here we subtract 1 to go back to yield instruction } else { unreachable!(); } Ok(InsnFunctionStepResult::Step) } pub fn op_yield( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Yield { yield_reg, end_offset, subtype_clear_start_reg, subtype_clear_count, }, insn ); if let Value::Numeric(Numeric::Integer(pc)) = state.registers[*yield_reg].get_value() { if state.ended_coroutine.contains(&(*yield_reg as u32)) { state.pc = end_offset.as_offset_int(); } else { let pc: u32 = (*pc) .try_into() .unwrap_or_else(|_| panic!("Yield: pc overflow: {pc}")); // swap the program counter with the value in the yield register // this is the mechanism that allows jumping back and forth between the coroutine and the caller state.registers[*yield_reg].set_int((state.pc + 1) as i64); state.pc = pc; // Strip JSON subtypes from co-routine output columns so they do not // survive the subquery boundary, matching SQLite's OP_Copy P5=0x0002. // subtype_clear_count > 0 only for coroutine body yields. #[cfg(feature = "json")] if *subtype_clear_count > 0 { use crate::types::TextSubtype; for reg in &mut state.registers [*subtype_clear_start_reg..*subtype_clear_start_reg + *subtype_clear_count] { if let Register::Value(Value::Text(text)) = reg { if text.subtype == TextSubtype::Json { text.subtype = TextSubtype::Text; } } } } } } else { unreachable!( "yield_reg {} contains non-integer value: {:?}", *yield_reg, state.registers[*yield_reg] ); } Ok(InsnFunctionStepResult::Step) } pub struct OpInsertState { pub sub_state: OpInsertSubState, pub old_record: Option<(i64, Vec)>, /// Set by the NoopCheck sub-state to indicate the row already has the exact /// same payload, so the physical write can be skipped. pub is_noop_update: bool, } #[derive(Debug, PartialEq)] pub enum OpInsertSubState { /// If this insert overwrites a record, capture the old record for incremental view maintenance. /// If cursor is already positioned (no REQUIRE_SEEK), capture directly. /// If REQUIRE_SEEK is set, transition to Seek first. MaybeCaptureRecord, /// Seek to the correct position if needed. /// In a table insert, if the caller does not pass InsertFlags::REQUIRE_SEEK, they must ensure that a seek has already happened to the correct location. /// This typically happens by invoking either Insn::NewRowid or Insn::NotExists, because: /// 1. op_new_rowid() seeks to the end of the table, which is the correct insertion position. /// 2. op_not_exists() seeks to the position in the table where the target rowid would be inserted. Seek, /// Capture the old record at the current cursor position for IVM. /// The cursor must already be positioned (by a prior seek or by NotExists/NewRowid). CaptureRecord, /// Check whether the update is a no-op (existing record matches new record). /// Must complete before Insert so that cursor.rowid()/record() are never /// interleaved with a partially-completed cursor.insert(). NoopCheck, /// Insert the row into the table. Insert, /// Updating last_insert_rowid may return IO, so we need a separate state for it so that we don't /// start inserting the same row multiple times. UpdateLastRowid, /// If there are dependent incremental views, apply the change. ApplyViewChange, } pub fn op_insert( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( Insert { cursor: cursor_id, key_reg, record_reg, flag, table_name, }, insn ); loop { match &state.op_insert_state.sub_state { OpInsertSubState::MaybeCaptureRecord => { let has_dependent_views = { let schema = program.connection.schema.read(); !schema .get_dependent_materialized_views(table_name) .is_empty() }; // If there are no dependent views, we don't need to capture the old record. // We also don't need to do it if the rowid of the UPDATEd row was changed, because // op_delete already captured the deletion for IVM, and this insert only needs to // record the new row (which ApplyViewChange handles without old_record). let needs_capture = has_dependent_views && !flag.has(InsertFlags::UPDATE_ROWID_CHANGE); if flag.has(InsertFlags::REQUIRE_SEEK) { state.op_insert_state.sub_state = OpInsertSubState::Seek; } else if needs_capture { state.op_insert_state.sub_state = OpInsertSubState::CaptureRecord; } else { state.op_insert_state.sub_state = OpInsertSubState::NoopCheck; } continue; } OpInsertSubState::Seek => { if let SeekInternalResult::IO(io) = seek_internal( program, state, pager, RecordSource::Unpacked { start_reg: *key_reg, num_regs: 1, }, *cursor_id, false, SeekOp::GE { eq_only: true }, )? { return Ok(InsnFunctionStepResult::IO(io)); } let has_dependent_views = { let schema = program.connection.schema.read(); !schema .get_dependent_materialized_views(table_name) .is_empty() }; let needs_capture = has_dependent_views && !flag.has(InsertFlags::UPDATE_ROWID_CHANGE); if needs_capture { state.op_insert_state.sub_state = OpInsertSubState::CaptureRecord; } else { state.op_insert_state.sub_state = OpInsertSubState::NoopCheck; } continue; } OpInsertSubState::CaptureRecord => { let insert_key = match &state.registers[*key_reg].get_value() { Value::Numeric(Numeric::Integer(i)) => *i, _ => unreachable!("expected integer key in insert"), }; let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); let maybe_key = return_if_io!(cursor.rowid()); let old_record = if let Some(key) = maybe_key { if key == insert_key { let maybe_record = return_if_io!(cursor.record()); if let Some(record) = maybe_record { let mut values = record.get_values_owned()?; let schema = program.connection.schema.read(); if let Some(table) = schema.get_table(table_name) { for (i, col) in table.columns().iter().enumerate() { if col.is_rowid_alias() && i < values.len() { values[i] = Value::from_i64(key); } } } Some((key, values)) } else { None } } else { None } } else { None }; state.op_insert_state.old_record = old_record; state.op_insert_state.sub_state = OpInsertSubState::NoopCheck; continue; } // TODO: add some InsertFlags that allows us to skip this check when we know for // certain that the update is not a no-op to avoid the branch. OpInsertSubState::NoopCheck => { // UPDATE fast path: skip the physical write if the target row already // has the exact same record payload. This check is isolated in its own // sub-state so that cursor.rowid()/record() IO yields never interleave // with a partially-completed cursor.insert(). // // In MVCC mode this check must be skipped: after Delete, cursor.record() // cannot reliably return the pre-delete payload (btree-resident rows // still appear physically intact, MVCC-store rows become invisible). // The noop check is fundamentally incompatible with the MVCC // Delete+Insert update pattern. state.op_insert_state.is_noop_update = false; let is_mvcc = { let cursor_ref = get_cursor!(state, *cursor_id); cursor_ref.as_btree_mut().is_mvcc() }; if !is_mvcc && flag.has(InsertFlags::SKIP_LAST_ROWID) && !flag.has(InsertFlags::UPDATE_ROWID_CHANGE) { let key = match &state.registers[*key_reg].get_value() { Value::Numeric(Numeric::Integer(i)) => *i, _ => unreachable!("expected integer key"), }; let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_btree_mut(); let existing_key = return_if_io!(cursor.rowid()); if existing_key == Some(key) { let record = match &state.registers[*record_reg] { Register::Record(r) => std::borrow::Cow::Borrowed(r), Register::Value(value) => { let values = [value]; let record = ImmutableRecord::from_values(values, values.len()); std::borrow::Cow::Owned(record) } Register::Aggregate(..) => { unreachable!("Cannot insert an aggregate value.") } }; let existing_record = return_if_io!(cursor.record()); if existing_record.is_some_and(|r| r == record.as_ref()) { state.op_insert_state.is_noop_update = true; } } } state.op_insert_state.sub_state = OpInsertSubState::Insert; continue; } OpInsertSubState::Insert => { if !state.op_insert_state.is_noop_update { let key = match &state.registers[*key_reg].get_value() { Value::Numeric(Numeric::Integer(i)) => *i, _ => unreachable!("expected integer key"), }; let record = match &state.registers[*record_reg] { Register::Record(r) => std::borrow::Cow::Borrowed(r), Register::Value(value) => { let values = [value]; let record = ImmutableRecord::from_values(values, values.len()); std::borrow::Cow::Owned(record) } Register::Aggregate(..) => { unreachable!("Cannot insert an aggregate value.") } }; let cursor = get_cursor!(state, *cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.insert(&BTreeKey::new_table_rowid(key, Some(&record)))); state.metrics.rows_written = state.metrics.rows_written.saturating_add(1); } // Only update last_insert_rowid for regular table inserts, not schema modifications let root_page = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); cursor.root_page() }; if root_page != 1 && table_name != SQLITE_SEQUENCE_TABLE_NAME && !flag.has(InsertFlags::EPHEMERAL_TABLE_INSERT) { state.op_insert_state.sub_state = OpInsertSubState::UpdateLastRowid; } else { // Schema table writes (sqlite_master, sqlite_sequence, ephemeral) // must not produce view deltas. The p4 table_name on these inserts // refers to the *target* table (for the update hook), not the table // actually being written to. Tracking deltas here would feed the // sqlite_master record into the DBSP circuit as if it were data // from the named table, corrupting the materialized view. state.op_insert_state.old_record = None; break; } } OpInsertSubState::UpdateLastRowid => { let maybe_rowid = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.rowid()) }; if let Some(rowid) = maybe_rowid { if !flag.has(InsertFlags::SKIP_LAST_ROWID) { program.connection.update_last_rowid(rowid); } state .n_change .fetch_add(1, crate::sync::atomic::Ordering::SeqCst); } let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); if !dependent_views.is_empty() { state.op_insert_state.sub_state = OpInsertSubState::ApplyViewChange; continue; } break; } OpInsertSubState::ApplyViewChange => { let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); assert!(!dependent_views.is_empty()); let (key, values) = { let key = match &state.registers[*key_reg].get_value() { Value::Numeric(Numeric::Integer(i)) => *i, _ => unreachable!("expected integer key"), }; let record = match &state.registers[*record_reg] { Register::Record(r) => std::borrow::Cow::Borrowed(r), Register::Value(value) => { let values = [value]; let record = ImmutableRecord::from_values(values, values.len()); std::borrow::Cow::Owned(record) } Register::Aggregate(..) => { unreachable!("Cannot insert an aggregate value.") } }; // Add insertion of new row to view deltas let mut new_values = record.get_values_owned()?; // Fix rowid alias columns: replace Null with actual rowid value let schema = program.connection.schema.read(); if let Some(table) = schema.get_table(table_name) { for (i, col) in table.columns().iter().enumerate() { if col.is_rowid_alias() && i < new_values.len() { new_values[i] = Value::from_i64(key); } } } (key, new_values) }; if let Some((key, values)) = state.op_insert_state.old_record.take() { for view_name in dependent_views.iter() { let tx_state = program .connection .view_transaction_states .get_or_create(view_name); tx_state.delete(table_name, key, values.clone()); } } for view_name in dependent_views.iter() { let tx_state = program .connection .view_transaction_states .get_or_create(view_name); tx_state.insert(table_name, key, values.clone()); } break; } } } state.op_insert_state.sub_state = OpInsertSubState::MaybeCaptureRecord; state.op_insert_state.is_noop_update = false; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_int_64( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Int64 { _p1, out_reg, _p3, value, }, insn ); state.registers[*out_reg].set_int(*value); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub struct OpDeleteState { pub sub_state: OpDeleteSubState, pub deleted_record: Option<(i64, Vec)>, } pub enum OpDeleteSubState { /// Capture the record before deletion, if the are dependent views. MaybeCaptureRecord, /// Delete the record. Delete, /// Apply the change to the dependent views. ApplyViewChange, } pub fn op_delete( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Delete { cursor_id, table_name, is_part_of_update, }, insn ); loop { match &state.op_delete_state.sub_state { OpDeleteSubState::MaybeCaptureRecord => { let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); if dependent_views.is_empty() { state.op_delete_state.sub_state = OpDeleteSubState::Delete; continue; } let deleted_record = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); // Get the current key let maybe_key = return_if_io!(cursor.rowid()); let key = maybe_key.ok_or_else(|| { LimboError::InternalError("Cannot delete: no current row".to_string()) })?; // Get the current record before deletion and extract values let maybe_record = return_if_io!(cursor.record()); if let Some(record) = maybe_record { let mut values = record.get_values_owned()?; // Fix rowid alias columns: replace Null with actual rowid value if let Some(table) = schema.get_table(table_name) { for (i, col) in table.columns().iter().enumerate() { if col.is_rowid_alias() && i < values.len() { values[i] = Value::from_i64(key); } } } Some((key, values)) } else { None } }; state.op_delete_state.deleted_record = deleted_record; state.op_delete_state.sub_state = OpDeleteSubState::Delete; continue; } OpDeleteSubState::Delete => { { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.delete()); } // Increment metrics for row write (DELETE is a write operation) state.metrics.rows_written = state.metrics.rows_written.saturating_add(1); let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); if dependent_views.is_empty() { break; } state.op_delete_state.sub_state = OpDeleteSubState::ApplyViewChange; continue; } OpDeleteSubState::ApplyViewChange => { let schema = program.connection.schema.read(); let dependent_views = schema.get_dependent_materialized_views(table_name); assert!(!dependent_views.is_empty()); let maybe_deleted_record = state.op_delete_state.deleted_record.take(); if let Some((key, values)) = maybe_deleted_record { for view_name in dependent_views { let tx_state = program .connection .view_transaction_states .get_or_create(&view_name); tx_state.delete(table_name, key, values.clone()); } } break; } } } state.op_delete_state.sub_state = OpDeleteSubState::MaybeCaptureRecord; if !is_part_of_update { // DELETEs do not count towards the total changes if they are part of an UPDATE statement, // i.e. the DELETE and subsequent INSERT of a row are the same "change". state .n_change .fetch_add(1, crate::sync::atomic::Ordering::SeqCst); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[derive(Debug)] pub enum OpIdxDeleteState { Seeking, Verifying, Deleting, } pub fn op_idx_delete( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( IdxDelete { cursor_id, start_reg, num_regs, raise_error_if_no_matching_entry, }, insn ); if let Some(Cursor::IndexMethod(cursor)) = &mut state.cursors[*cursor_id] { return_if_io!(cursor.delete(&state.registers[*start_reg..*start_reg + *num_regs])); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } loop { #[cfg(debug_assertions)] tracing::debug!( "op_idx_delete(cursor_id={}, start_reg={}, num_regs={}, rootpage={}, state={:?})", cursor_id, start_reg, num_regs, state.get_cursor(*cursor_id).as_btree_mut().root_page(), state.op_idx_delete_state ); match &state.op_idx_delete_state { Some(OpIdxDeleteState::Seeking) => { let found = match seek_internal( program, state, pager, RecordSource::Unpacked { start_reg: *start_reg, num_regs: *num_regs, }, *cursor_id, true, SeekOp::GE { eq_only: true }, ) { Ok(SeekInternalResult::Found) => true, Ok(SeekInternalResult::NotFound) => false, Ok(SeekInternalResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), Err(e) => return Err(e), }; if !found { // If P5 is not zero, then raise an SQLITE_CORRUPT_INDEX error if no matching index entry is found // Also, do not raise this (self-correcting and non-critical) error if in writable_schema mode. if *raise_error_if_no_matching_entry { let reg_values = (*start_reg..*start_reg + *num_regs) .map(|i| &state.registers[i]) .collect::>(); return Err(LimboError::Corrupt(format!( "IdxDelete: no matching index entry found for key {reg_values:?} while seeking" ))); } state.pc += 1; state.op_idx_delete_state = None; return Ok(InsnFunctionStepResult::Step); } state.op_idx_delete_state = Some(OpIdxDeleteState::Verifying); } Some(OpIdxDeleteState::Verifying) => { let rowid = { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.rowid()) }; if rowid.is_none() && *raise_error_if_no_matching_entry { let reg_values = (*start_reg..*start_reg + *num_regs) .map(|i| &state.registers[i]) .collect::>(); return Err(LimboError::Corrupt(format!( "IdxDelete: no matching index entry found for key while verifying: {reg_values:?}" ))); } state.op_idx_delete_state = Some(OpIdxDeleteState::Deleting); } Some(OpIdxDeleteState::Deleting) => { { let cursor = state.get_cursor(*cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.delete()); } // Increment metrics for index write (delete is a write operation) state.metrics.rows_written = state.metrics.rows_written.saturating_add(1); state.pc += 1; state.op_idx_delete_state = None; return Ok(InsnFunctionStepResult::Step); } None => { state.op_idx_delete_state = Some(OpIdxDeleteState::Seeking); } } } } #[derive(Debug, PartialEq, Copy, Clone)] pub enum OpIdxInsertState { /// Optional seek step done before an unique constraint check or if the caller indicates a seek is required. MaybeSeek, /// Optional unique constraint check done before an insert. UniqueConstraintCheck, /// Main insert step. This is always performed. Insert, } pub fn op_idx_insert( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( IdxInsert { cursor_id, record_reg, flags, unpacked_start, unpacked_count, .. }, *insn ); if let Some(Cursor::IndexMethod(cursor)) = &mut state.cursors[cursor_id] { let Some(start) = unpacked_start else { return Err(LimboError::InternalError( "IndexMethod must receive unpacked values".to_string(), )); }; let Some(count) = unpacked_count else { return Err(LimboError::InternalError( "IndexMethod must receive unpacked values".to_string(), )); }; return_if_io!(cursor.insert(&state.registers[start..start + count as usize])); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let record_to_insert = match &state.registers[record_reg] { Register::Record(ref r) => r, o => { return Err(LimboError::InternalError(format!( "expected record, got {o:?}" ))); } }; match state.op_idx_insert_state { OpIdxInsertState::MaybeSeek => { let (_, cursor_type) = program .cursor_ref .get(cursor_id) .expect("cursor_id should exist in cursor_ref"); let CursorType::BTreeIndex(index_meta) = cursor_type else { panic!("IdxInsert: not a BTreeIndex cursor"); }; // USE_SEEK: cursor was already positioned by a preceding NoConflict operation. // Skip the redundant seek and go directly to insert. // For unique indexes, this also skips UniqueConstraintCheck since NoConflict already verified uniqueness. // // HOWEVER: If the record contains NULLs, NoConflict skips the seek entirely // (since NULLs can't conflict), so we must fall back to seeking here. if flags.has(IdxInsertFlags::USE_SEEK) && !record_to_insert.contains_null()? { state.op_idx_insert_state = OpIdxInsertState::Insert; return Ok(InsnFunctionStepResult::Step); // Fall through to do the seek since NoConflict skipped it due to NULLs } match seek_internal( program, state, pager, RecordSource::Packed { record_reg }, cursor_id, true, SeekOp::GE { eq_only: true }, )? { SeekInternalResult::Found => { state.op_idx_insert_state = if index_meta.unique { OpIdxInsertState::UniqueConstraintCheck } else { OpIdxInsertState::Insert }; Ok(InsnFunctionStepResult::Step) } SeekInternalResult::NotFound => { state.op_idx_insert_state = OpIdxInsertState::Insert; Ok(InsnFunctionStepResult::Step) } SeekInternalResult::IO(io) => Ok(InsnFunctionStepResult::IO(io)), } } OpIdxInsertState::UniqueConstraintCheck => { let ignore_conflict = 'i: { let cursor = get_cursor!(state, cursor_id); let cursor = cursor.as_btree_mut(); let has_rowid = cursor.has_rowid(); let index_info = cursor.get_index_info().clone(); let record_opt = return_if_io!(cursor.record()); let Some(record) = record_opt.as_ref() else { // Cursor not pointing at a record — table is empty or past last break 'i false; }; // Cursor is pointing at a record; if the index has a rowid, exclude it from the comparison since it's a pointer to the table row; // UNIQUE indexes disallow duplicates like (a=1,b=2,rowid=1) and (a=1,b=2,rowid=2). let existing_key = if has_rowid { let count = record.column_count(); &record.get_values_range(0..count.saturating_sub(1))? } else { &record.get_values()?[..] }; let inserted_key_vals = &record_to_insert.get_values()?; if existing_key.len() != inserted_key_vals.len() { break 'i false; } let conflict = compare_immutable(existing_key, inserted_key_vals, &index_info.key_info) == std::cmp::Ordering::Equal; if conflict { if flags.has(IdxInsertFlags::NO_OP_DUPLICATE) { break 'i true; } return Err(LimboError::Constraint( "UNIQUE constraint failed: duplicate key".into(), )); } false }; state.op_idx_insert_state = if ignore_conflict { state.pc += 1; OpIdxInsertState::MaybeSeek } else { OpIdxInsertState::Insert }; Ok(InsnFunctionStepResult::Step) } OpIdxInsertState::Insert => { { let cursor = get_cursor!(state, cursor_id); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.insert(&BTreeKey::new_index_key(record_to_insert))); } // Increment metrics for index write if flags.has(IdxInsertFlags::NCHANGE) { state.metrics.rows_written = state.metrics.rows_written.saturating_add(1); } state.op_idx_insert_state = OpIdxInsertState::MaybeSeek; state.pc += 1; Ok(InsnFunctionStepResult::Step) } } } #[derive(Debug, Clone, Copy)] pub enum OpNewRowidState { Start, SeekingToLast { mvcc_already_initialized: bool, }, ReadingMaxRowid, GeneratingRandom { attempts: u32, }, VerifyingCandidate { attempts: u32, candidate: i64, }, /// In case a rowid was generated and not provided by the user, we need to call next() on the cursor /// after generating the rowid. This is because the rowid was generated by seeking to the last row in the /// table, and we need to insert _after_ that row. GoNext, } pub fn op_new_rowid( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { new_rowid_inner(program, state, insn, pager).inspect_err(|_| { // In case of error we need to unlock rowid lock from mvcc cursor load_insn!( NewRowid { cursor, rowid_reg: _, prev_largest_reg: _, }, insn ); let mv_store = program.connection.mv_store(); if mv_store.is_some() { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut() as &mut dyn Any; if let Some(mvcc_cursor) = cursor.downcast_mut::() { mvcc_cursor.end_new_rowid(); } } }) } fn new_rowid_inner( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( NewRowid { cursor, rowid_reg, prev_largest_reg, }, insn ); const MAX_ROWID: i64 = i64::MAX; const MAX_ATTEMPTS: u32 = 100; let mv_store = program.connection.mv_store(); loop { match state.op_new_rowid_state { OpNewRowidState::Start => { if mv_store.is_some() { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut() as &mut dyn Any; if let Some(mvcc_cursor) = cursor.downcast_mut::() { match return_if_io!(mvcc_cursor.start_new_rowid()) { NextRowidResult::Uninitialized => { state.op_new_rowid_state = OpNewRowidState::SeekingToLast { mvcc_already_initialized: false, }; } NextRowidResult::Next { new_rowid, prev_rowid, } => { // Allocator already initialized — release lock immediately mvcc_cursor.end_new_rowid(); state.registers[*rowid_reg].set_int(new_rowid); if *prev_largest_reg > 0 { state.registers[*prev_largest_reg] .set_int(prev_rowid.unwrap_or(0)); } state.op_new_rowid_state = OpNewRowidState::SeekingToLast { mvcc_already_initialized: true, }; } NextRowidResult::FindRandom => { mvcc_cursor.end_new_rowid(); state.op_new_rowid_state = OpNewRowidState::GeneratingRandom { attempts: 0 }; } } } else { // Not an MvCursor — must be an ephemeral cursor or an attached // DB cursor without MVCC (e.g., :memory: attached DBs skip MVCC). // Keep the downcast check as a safety net against unexpected cursor types. assert!( cursor.downcast_ref::().is_some(), "Expected MvCursor or BTreeCursor in op_new_rowid" ); state.op_new_rowid_state = OpNewRowidState::SeekingToLast { mvcc_already_initialized: false, }; } } else { state.op_new_rowid_state = OpNewRowidState::SeekingToLast { mvcc_already_initialized: false, }; } } OpNewRowidState::SeekingToLast { mvcc_already_initialized, } => { { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut(); // We have an optimization in the btree cursor to not seek if we know the rightmost page and are already on it. // However, this optimization should NOT never performed in cases where we cannot be sure that the btree wasn't modified from under us // e.g. by a trigger subprogram. let always_seek = program.contains_trigger_subprograms; return_if_io!(cursor.seek_to_last(always_seek)); } if mvcc_already_initialized { state.op_new_rowid_state = OpNewRowidState::GoNext; } else { state.op_new_rowid_state = OpNewRowidState::ReadingMaxRowid; } } OpNewRowidState::ReadingMaxRowid => { let current_max = { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.rowid()) }; if mv_store.is_some() { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut() as &mut dyn Any; if let Some(mvcc_cursor) = cursor.downcast_mut::() { // Initialize the monotonic counter from the btree max. // The allocator lock is held, so no other thread can // race between this read and initialize. mvcc_cursor.initialize_max_rowid(current_max)?; // Allocate the first rowid from the freshly initialized counter. match mvcc_cursor.allocate_next_rowid() { Some((new_rowid, prev_rowid)) => { state.registers[*rowid_reg].set_int(new_rowid); if *prev_largest_reg > 0 { state.registers[*prev_largest_reg] .set_int(prev_rowid.unwrap_or(0)); } tracing::trace!("new_rowid={}", new_rowid); state.op_new_rowid_state = OpNewRowidState::GoNext; continue; } None => { // At i64::MAX — fall back to random state.op_new_rowid_state = OpNewRowidState::GeneratingRandom { attempts: 0 }; continue; } } } } // Non-MVCC path (or ephemeral cursor in MVCC mode) if *prev_largest_reg > 0 { state.registers[*prev_largest_reg].set_int(current_max.unwrap_or(0)); } match current_max { Some(rowid) if rowid < MAX_ROWID => { state.registers[*rowid_reg].set_int(rowid + 1); tracing::trace!("new_rowid={}", rowid + 1); state.op_new_rowid_state = OpNewRowidState::GoNext; continue; } Some(_) => { state.op_new_rowid_state = OpNewRowidState::GeneratingRandom { attempts: 0 }; } None => { tracing::trace!("new_rowid=1"); state.registers[*rowid_reg].set_int(1); state.op_new_rowid_state = OpNewRowidState::GoNext; continue; } } } OpNewRowidState::GeneratingRandom { attempts } => { if attempts >= MAX_ATTEMPTS { return Err(LimboError::DatabaseFull("Unable to find an unused rowid after 100 attempts - database is probably full".to_string())); } // Generate a random i64 and constrain it to the lower half of the rowid range. // We use the lower half (1 to MAX_ROWID/2) because we're in random mode only // when sequential allocation reached MAX_ROWID, meaning the upper range is full. let mut random_rowid: i64 = pager.io.generate_random_number(); random_rowid &= MAX_ROWID >> 1; // Mask to keep value in range [0, MAX_ROWID/2] random_rowid += 1; // Ensure positive state.op_new_rowid_state = OpNewRowidState::VerifyingCandidate { attempts, candidate: random_rowid, }; } OpNewRowidState::VerifyingCandidate { attempts, candidate, } => { let exists = { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut(); let seek_result = return_if_io!(cursor .seek(SeekKey::TableRowId(candidate), SeekOp::GE { eq_only: true })); matches!(seek_result, SeekResult::Found) }; if !exists { // Found unused rowid! state.registers[*rowid_reg].set_int(candidate); state.op_new_rowid_state = OpNewRowidState::Start; state.pc += 1; if mv_store.is_some() { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut() as &mut dyn Any; if let Some(mvcc_cursor) = cursor.downcast_mut::() { mvcc_cursor.end_new_rowid(); } } return Ok(InsnFunctionStepResult::Step); } else { // Collision, try again state.op_new_rowid_state = OpNewRowidState::GeneratingRandom { attempts: attempts + 1, }; } } OpNewRowidState::GoNext => { { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.next()); } state.op_new_rowid_state = OpNewRowidState::Start; state.pc += 1; if mv_store.is_some() { let cursor = state.get_cursor(*cursor); let cursor = cursor.as_btree_mut() as &mut dyn Any; if let Some(mvcc_cursor) = cursor.downcast_mut::() { mvcc_cursor.end_new_rowid(); } } return Ok(InsnFunctionStepResult::Step); } } } } pub fn op_must_be_int( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(MustBeInt { reg }, insn); match &state.registers[*reg].get_value() { Value::Numeric(Numeric::Integer(_)) => {} Value::Numeric(Numeric::Float(f)) => match cast_real_to_integer(f64::from(*f)) { Ok(i) => state.registers[*reg].set_int(i), Err(_) => bail_constraint_error!("datatype mismatch"), }, Value::Text(text) => match checked_cast_text_to_numeric(text.as_str(), true) { Ok(Value::Numeric(Numeric::Integer(i))) => state.registers[*reg].set_int(i), Ok(Value::Numeric(Numeric::Float(f))) => match cast_real_to_integer(f64::from(f)) { Ok(i) => state.registers[*reg].set_int(i), Err(_) => bail_constraint_error!("datatype mismatch"), }, _ => bail_constraint_error!("datatype mismatch"), }, _ => { bail_constraint_error!("datatype mismatch"); } }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_soft_null( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(SoftNull { reg }, insn); state.registers[*reg].set_null(); state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[derive(Clone, Copy)] pub enum OpNoConflictState { Start, Seeking(RecordSource), } /// If a matching record is not found in the btree ("no conflict"), jump to the target PC. /// Otherwise, continue execution. pub fn op_no_conflict( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( NoConflict { cursor_id, target_pc, record_reg, num_regs, }, insn ); loop { match state.op_no_conflict_state { OpNoConflictState::Start => { let record_source = if *num_regs == 0 { RecordSource::Packed { record_reg: *record_reg, } } else { RecordSource::Unpacked { start_reg: *record_reg, num_regs: *num_regs, } }; // If there is at least one NULL in the index record, there cannot be a conflict so we can immediately jump. let contains_nulls = match &record_source { RecordSource::Packed { record_reg } => { let Register::Record(record) = &state.registers[*record_reg] else { return Err(LimboError::InternalError( "NoConflict: expected a record in the register".into(), )); }; record.iter()?.any(|val| matches!(val, Ok(ValueRef::Null))) } RecordSource::Unpacked { start_reg, num_regs, } => (0..*num_regs).any(|i| { matches!( &state.registers[start_reg + i], Register::Value(Value::Null) ) }), }; if contains_nulls { state.pc = target_pc.as_offset_int(); state.op_no_conflict_state = OpNoConflictState::Start; return Ok(InsnFunctionStepResult::Step); } else { state.op_no_conflict_state = OpNoConflictState::Seeking(record_source); } } OpNoConflictState::Seeking(record_source) => { return match seek_internal( program, state, pager, record_source, *cursor_id, true, SeekOp::GE { eq_only: true }, )? { SeekInternalResult::Found => { state.pc += 1; state.op_no_conflict_state = OpNoConflictState::Start; Ok(InsnFunctionStepResult::Step) } SeekInternalResult::NotFound => { state.pc = target_pc.as_offset_int(); state.op_no_conflict_state = OpNoConflictState::Start; Ok(InsnFunctionStepResult::Step) } SeekInternalResult::IO(io) => Ok(InsnFunctionStepResult::IO(io)), }; } } } } pub fn op_not_exists( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( NotExists { cursor, rowid_reg, target_pc, }, insn ); let cursor = must_be_btree_cursor!(*cursor, program.cursor_ref, state, "NotExists"); let cursor = cursor.as_btree_mut(); let exists = return_if_io!(cursor.exists(state.registers[*rowid_reg].get_value())); if exists { state.pc += 1; } else { state.pc = target_pc.as_offset_int(); } Ok(InsnFunctionStepResult::Step) } pub fn op_offset_limit( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( OffsetLimit { limit_reg, combined_reg, offset_reg, }, insn ); let limit_val = match state.registers[*limit_reg].get_value() { Value::Numeric(Numeric::Integer(val)) => val, _ => { return Err(LimboError::InternalError( "OffsetLimit: the value in limit_reg is not an integer".into(), )); } }; let offset_val = match state.registers[*offset_reg].get_value() { Value::Numeric(Numeric::Integer(val)) if *val < 0 => 0, Value::Numeric(Numeric::Integer(val)) if *val >= 0 => *val, _ => { return Err(LimboError::InternalError( "OffsetLimit: the value in offset_reg is not an integer".into(), )); } }; let offset_limit_sum = limit_val.overflowing_add(offset_val); if *limit_val <= 0 || offset_limit_sum.1 { state.registers[*combined_reg].set_int(-1); } else { state.registers[*combined_reg].set_int(offset_limit_sum.0); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } // this cursor may be reused for next insert // Update: tablemoveto is used to travers on not exists, on insert depending on flags if nonseek it traverses again. // If not there might be some optimizations obviously. pub fn op_open_write( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( OpenWrite { cursor_id, root_page, db, }, insn ); if program.connection.is_readonly(*db) { return Err(LimboError::ReadOnly); } let pager = program.get_pager_from_database_index(db); let mv_store = program.connection.mv_store_for_db(*db); if let (_, CursorType::IndexMethod(module)) = &program.cursor_ref[*cursor_id] { if state.cursors[*cursor_id].is_none() { let cursor = module.init()?; let cursor_ref = &mut state.cursors[*cursor_id]; *cursor_ref = Some(Cursor::IndexMethod(cursor)); } let cursor = state.cursors[*cursor_id] .as_mut() .expect("cursor should exist"); let cursor = cursor.as_index_method_mut(); return_if_io!(cursor.open_write(&program.connection)); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let root_page = match root_page { RegisterOrLiteral::Literal(lit) => *lit, RegisterOrLiteral::Register(reg) => match &state.registers[*reg].get_value() { Value::Numeric(Numeric::Integer(val)) => *val, _ => { return Err(LimboError::InternalError( "OpenWrite: the value in root_page is not an integer".into(), )); } }, }; const SQLITE_SCHEMA_ROOT_PAGE: i64 = 1; if root_page == SQLITE_SCHEMA_ROOT_PAGE { if let Some(mv_store) = mv_store.as_ref() { let Some(tx_id) = program.connection.get_mv_tx_id_for_db(*db) else { return Err(LimboError::InternalError( "Schema changes in MVCC mode require an exclusive MVCC transaction".to_string(), )); }; if !mv_store.is_exclusive_tx(&tx_id) { return Err(LimboError::TxError( "DDL statements require an exclusive transaction (use BEGIN instead of BEGIN CONCURRENT)".to_string(), )); } } } let (_, cursor_type) = program .cursor_ref .get(*cursor_id) .expect("cursor_id should exist in cursor_ref"); let cursors = &mut state.cursors; let maybe_index = match cursor_type { CursorType::BTreeIndex(index) => Some(index), _ => None, }; // Check if we can reuse the existing cursor let can_reuse_cursor = if let Some(Some(Cursor::BTree(btree_cursor))) = cursors.get(*cursor_id) { // Reuse if the root_page matches (same table/index) btree_cursor.root_page() == root_page } else { false }; if !can_reuse_cursor { let maybe_promote_to_mvcc_cursor = |btree_cursor: Box, mv_cursor_type: MvccCursorType| -> Result> { if let Some(tx_id) = program.connection.get_mv_tx_id_for_db(*db) { let mv_store = mv_store .as_ref() .expect("mv_store should be Some when MVCC transaction is active") .clone(); Ok(Box::new(MvCursor::new( mv_store, tx_id, root_page, mv_cursor_type, btree_cursor, )?)) } else { Ok(btree_cursor) } }; if let Some(index) = maybe_index { let num_columns = index.columns.len(); let btree_cursor = Box::new(BTreeCursor::new_index( pager, maybe_transform_root_page_to_positive(mv_store.as_ref(), root_page), index.as_ref(), num_columns, )); let index_info = Arc::new(IndexInfo::new_from_index(index)); let cursor = maybe_promote_to_mvcc_cursor(btree_cursor, MvccCursorType::Index(index_info))?; cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") .replace(Cursor::new_btree(cursor)); } else { let num_columns = match cursor_type { CursorType::BTreeTable(table_rc) => table_rc.columns.len(), CursorType::MaterializedView(table_rc, _) => table_rc.columns.len(), _ => unreachable!( "Expected BTreeTable or MaterializedView. This should not have happened." ), }; let btree_cursor = Box::new(BTreeCursor::new_table( pager, maybe_transform_root_page_to_positive(mv_store.as_ref(), root_page), num_columns, )); let cursor = maybe_promote_to_mvcc_cursor(btree_cursor, MvccCursorType::Table)?; cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") .replace(Cursor::new_btree(cursor)); } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_copy( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Copy { src_reg, dst_reg, extra_amount, }, insn ); for i in 0..=*extra_amount { state.registers[*dst_reg + i] = state.registers[*src_reg + i].clone(); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_create_btree( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(CreateBtree { db, root, flags }, insn); if program.connection.is_readonly(*db) { return Err(LimboError::ReadOnly); } let mv_store = program.connection.mv_store_for_db(*db); if let Some(mv_store) = mv_store.as_ref() { let root_page = mv_store.get_next_table_id(); state.registers[*root].set_int(root_page); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let pager = program.get_pager_from_database_index(db); // FIXME: handle page cache is full let root_page = return_if_io!(pager.btree_create(flags)); state.registers[*root].set_int(root_page as i64); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_index_method_create( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(IndexMethodCreate { db, cursor_id }, insn); if program.connection.is_readonly(*db) { return Err(LimboError::ReadOnly); } let mv_store = program.connection.mv_store_for_db(*db); if let Some(_mv_store) = mv_store.as_ref() { todo!("MVCC is not supported yet"); } if let (_, CursorType::IndexMethod(module)) = &program.cursor_ref[*cursor_id] { if state.cursors[*cursor_id].is_none() { let cursor = module.init()?; let cursor_ref = &mut state.cursors[*cursor_id]; *cursor_ref = Some(Cursor::IndexMethod(cursor)); } } let cursor = state.cursors[*cursor_id] .as_mut() .expect("cursor should exist"); let cursor = cursor.as_index_method_mut(); return_if_io!(cursor.create(&program.connection)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_index_method_destroy( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(IndexMethodDestroy { db, cursor_id }, insn); if program.connection.is_readonly(*db) { return Err(LimboError::ReadOnly); } let mv_store = program.connection.mv_store_for_db(*db); if let Some(_mv_store) = mv_store.as_ref() { todo!("MVCC is not supported yet"); } if let Some((_, CursorType::IndexMethod(module))) = program.cursor_ref.get(*cursor_id) { if state.cursors[*cursor_id].is_none() { let cursor = module.init()?; let cursor_ref = &mut state.cursors[*cursor_id]; *cursor_ref = Some(Cursor::IndexMethod(cursor)); } } let cursor = state.cursors[*cursor_id] .as_mut() .expect("cursor should exist"); let cursor = cursor.as_index_method_mut(); return_if_io!(cursor.destroy(&program.connection)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_index_method_optimize( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(IndexMethodOptimize { db, cursor_id }, insn); if program.connection.is_readonly(*db) { return Err(LimboError::ReadOnly); } let mv_store = program.connection.mv_store_for_db(*db); if let Some(_mv_store) = mv_store.as_ref() { todo!("MVCC is not supported yet"); } if let Some((_, CursorType::IndexMethod(module))) = program.cursor_ref.get(*cursor_id) { if state.cursors[*cursor_id].is_none() { let cursor = module.init()?; let cursor_ref = &mut state.cursors[*cursor_id]; *cursor_ref = Some(Cursor::IndexMethod(cursor)); } } let cursor = state.cursors[*cursor_id] .as_mut() .expect("cursor should exist"); let cursor = cursor.as_index_method_mut(); return_if_io!(cursor.optimize(&program.connection)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_index_method_query( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( IndexMethodQuery { db: _, cursor_id, start_reg, count_reg, pc_if_empty, }, insn ); let mv_store = program.connection.mv_store(); if let Some(_mv_store) = mv_store.as_ref() { todo!("MVCC is not supported yet"); } let cursor = state.cursors[*cursor_id] .as_mut() .expect("cursor should exist"); let cursor = cursor.as_index_method_mut(); let has_rows = return_if_io!(cursor.query_start(&state.registers[*start_reg..*start_reg + *count_reg])); if !has_rows { state.pc = pc_if_empty.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub enum OpDestroyState { CreateCursor, DestroyBtree(Arc>), } pub fn op_destroy( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( Destroy { db, root, former_root_reg, is_temp, }, insn ); if *is_temp == 1 { todo!("temp databases not implemented yet."); } let mv_store = program.connection.mv_store_for_db(*db); if mv_store.is_some() { // MVCC only does pager operations in checkpoint state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let destroy_pager = if *db != crate::MAIN_DB_ID { program.get_pager_from_database_index(db) } else { pager.clone() }; loop { match state.op_destroy_state { OpDestroyState::CreateCursor => { // Destroy doesn't do anything meaningful with the table/index distinction so we can just use a // table btree cursor for both. let cursor = BTreeCursor::new(destroy_pager.clone(), *root, 0); state.op_destroy_state = OpDestroyState::DestroyBtree(Arc::new(RwLock::new(cursor))); } OpDestroyState::DestroyBtree(ref mut cursor) => { let maybe_former_root_page = return_if_io!(cursor.write().btree_destroy()); state.registers[*former_root_reg] .set_int(maybe_former_root_page.unwrap_or(0) as i64); state.op_destroy_state = OpDestroyState::CreateCursor; state.pc += 1; return Ok(InsnFunctionStepResult::Step); } } } } pub fn op_reset_sorter( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(ResetSorter { cursor_id }, insn); let (_, cursor_type) = program .cursor_ref .get(*cursor_id) .expect("cursor_id should exist in cursor_ref"); let cursor = state.get_cursor(*cursor_id); match cursor_type { CursorType::BTreeTable(_) => { let cursor = cursor.as_btree_mut(); return_if_io!(cursor.clear_btree()); // FIXME: cuurently we don't have a good way to identify cursors that are // iterating in the same underlying BTree // After clearing the btree, invalidate cached navigation state on all // other cursors that share the same underlying btree (e.g. OpenDup cursors). // Without this, dup cursors may use stale cached rightmost-page info and // attempt to insert into freed pages, causing corruption. let cleared_pager = { let cursor = state.get_cursor(*cursor_id); cursor.as_btree_mut().get_pager() }; for (i, other_cursor_opt) in state.cursors.iter_mut().enumerate() { if i == *cursor_id { continue; } if let Some(Cursor::BTree(ref mut btree_cursor)) = other_cursor_opt { if Arc::ptr_eq(&btree_cursor.get_pager(), &cleared_pager) { btree_cursor.invalidate_btree_cache(); } } } } CursorType::Sorter => { return Err(LimboError::InternalError( "ResetSorter is not supported for sorter cursors".to_string(), )); } _ => { return Err(LimboError::InternalError(format!( "ResetSorter is not supported for {cursor_type:?}" ))); } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_drop_table( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(DropTable { db, table_name, .. }, insn); let conn = program.connection.clone(); let is_mvcc = conn.mv_store_for_db(*db).is_some(); { conn.with_database_schema_mut(*db, |schema| { // In MVCC mode, track dropped root pages so integrity_check knows about them. // The btree pages won't be freed until checkpoint, so integrity_check needs // to include them to avoid "page never used" false positives. if is_mvcc { let table = schema .get_table(table_name) .expect("DROP TABLE: table must exist in schema"); if let Some(btree) = table.btree() { // Only track positive root pages (checkpointed tables). // Negative root pages are non-checkpointed and don't exist in btree file. if btree.root_page > 0 { schema.dropped_root_pages.insert(btree.root_page); } } // Capture index root pages (table may not have indexes) if let Some(indexes) = schema.indexes.get(table_name) { for index in indexes.iter() { if index.root_page > 0 { schema.dropped_root_pages.insert(index.root_page); } } } } schema.remove_indices_for_table(table_name); schema.remove_triggers_for_table(table_name); schema.remove_table(table_name); }); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_drop_view( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(DropView { db, view_name }, insn); let conn = program.connection.clone(); conn.with_database_schema_mut(*db, |schema| { schema.remove_view(view_name).ok(); }); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_drop_type( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(DropType { db, type_name }, insn); if *db > 0 { todo!("temp databases not implemented yet"); } let conn = program.connection.clone(); conn.with_schema_mut(|schema| { schema.remove_type(type_name); Ok::<(), crate::LimboError>(()) })?; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_add_type( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(AddType { db, sql }, insn); if *db > 0 { todo!("temp databases not implemented yet"); } let conn = program.connection.clone(); conn.with_schema_mut(|schema| schema.add_type_from_sql(sql))?; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_drop_trigger( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(DropTrigger { db, trigger_name }, insn); let conn = program.connection.clone(); conn.with_database_schema_mut(*db, |schema| { schema.remove_trigger(trigger_name).ok(); }); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_close( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Close { cursor_id }, insn); let cursors = &mut state.cursors; cursors .get_mut(*cursor_id) .expect("cursor_id should be valid") .take(); if let Some(deferred_seek) = state.deferred_seeks.get_mut(*cursor_id) { deferred_seek.take(); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_is_null( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(IsNull { reg, target_pc }, insn); if matches!(state.registers[*reg], Register::Value(Value::Null)) { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_coll_seq( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { let Insn::CollSeq { reg, collation } = insn else { unreachable!("unexpected Insn {:?}", insn) }; // Set the current collation sequence for use by subsequent functions state.current_collation = Some(*collation); // If P1 is not zero, initialize that register to 0 if let Some(reg_idx) = reg { state.registers[*reg_idx].set_int(0); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_page_count( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(PageCount { db, dest }, insn); let pager = program.get_pager_from_database_index(db); let mv_store = program.connection.mv_store_for_db(*db); let count = match with_header(&pager, mv_store.as_ref(), program, *db, |header| { header.database_size.get() }) { Err(_) => 0.into(), Ok(IOResult::Done(v)) => v.into(), Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), }; state.registers[*dest].set_int(count); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_parse_schema( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(ParseSchema { db, where_clause }, insn); let conn = program.connection.clone(); // set auto commit to false in order for parse schema to not commit changes as transaction state is stored in connection, // and we use the same connection for nested query. let previous_auto_commit = conn.auto_commit.load(Ordering::SeqCst); conn.auto_commit.store(false, Ordering::SeqCst); // For attached databases, qualify the sqlite_schema table with the database name let schema_table = if crate::is_attached_db(*db) { let db_name = conn .get_database_name_by_index(*db) .unwrap_or_else(|| "main".to_string()); format!("{db_name}.sqlite_schema") } else { "sqlite_schema".to_string() }; let sql = if let Some(where_clause) = where_clause { format!("SELECT * FROM {schema_table} WHERE {where_clause}") } else { format!("SELECT * FROM {schema_table}") }; let stmt = conn.prepare(sql)?; // Get a mutable schema clone *without* holding the schema lock during // nested statement execution. The nested Statement may call reprepare() // which also acquires the schema / database_schemas write lock, so holding // it here would deadlock on the same thread (parking_lot RwLock is not // re-entrant). let mut schema_arc = if crate::is_attached_db(*db) { // Fetch the fallback schema from attached_databases BEFORE acquiring // database_schemas.write() to avoid a nested lock ordering dependency // (database_schemas.write -> attached_databases.read). let fallback_schema = { let attached_dbs = conn.attached_databases.read(); attached_dbs .index_to_data .get(db) .map(|(db_inst, _pager)| db_inst.schema.lock().clone()) }; let Some(fallback_schema) = fallback_schema else { // The db index refers to a database that was detached after this // program was compiled. The schema cookie check should have caught // this, but defensively return an error instead of panicking. return Err(LimboError::InternalError(format!( "stale reference to detached database (index {db})" ))); }; let mut schemas = conn.database_schemas().write(); schemas .entry(*db) .or_insert_with(|| fallback_schema) .clone() // cheap Arc clone; write lock released at end of block } else { conn.schema.read().clone() }; let schema = Arc::make_mut(&mut schema_arc); // TODO: This function below is synchronous, make it async let existing_views = schema.incremental_views.clone(); conn.start_nested(); let maybe_nested_stmt_err = parse_schema_rows( stmt, schema, &conn.syms.read(), // NOTE: We always pass the main DB's mv_tx here because // Statement::set_mv_tx() writes to connection.mv_tx (main DB field). // Passing an attached DB's tx would corrupt the main DB's transaction // state. The nested statement's opcodes use get_mv_tx_id_for_db(db) // to read the correct per-database tx, so the attached DB case works // correctly without setting mv_tx. program.connection.get_mv_tx(), existing_views, ); // Store the modified schema back if crate::is_attached_db(*db) { conn.database_schemas().write().insert(*db, schema_arc); } else { *conn.schema.write() = schema_arc; } conn.end_nested(); conn.auto_commit .store(previous_auto_commit, Ordering::SeqCst); maybe_nested_stmt_err?; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_init_cdc_version( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( InitCdcVersion { cdc_table_name, version, cdc_mode, }, insn ); let conn = program.connection.clone(); let escaped_cdc_table_name = escape_sql_string_literal(cdc_table_name); // "off" — disable CDC (table and version entry are preserved) if CaptureDataChangesInfo::parse(cdc_mode, None)?.is_none() { state.pending_cdc_info = Some(None); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } // If CDC is already enabled, re-parse with current version and exit early. // This makes the operation idempotent and avoids CDC capturing its own // table creation when the pragma is called multiple times. { let current = conn.get_capture_data_changes_info(); if let Some(info) = current.as_ref() { let opts = CaptureDataChangesInfo::parse(cdc_mode, info.version)?; state.pending_cdc_info = Some(opts); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } } // Step 0: Check if the CDC table already exists but has no version row. // If so, it's a legacy v1 table that pre-dates version tracking. let cdc_table_exists = { conn.start_nested(); let mut stmt = conn.prepare(format!( "SELECT 1 FROM sqlite_schema WHERE type='table' AND name='{escaped_cdc_table_name}'", ))?; stmt.program .prepared .needs_stmt_subtransactions .store(false, Ordering::Relaxed); let rows = stmt.run_collect_rows(); conn.end_nested(); !rows?.is_empty() }; // Step 1: Create CDC table if needed { conn.start_nested(); let create_sql = match version { CdcVersion::V1 => format!( "CREATE TABLE IF NOT EXISTS {cdc_table_name} (change_id INTEGER PRIMARY KEY AUTOINCREMENT, change_time INTEGER, change_type INTEGER, table_name TEXT, id, before BLOB, after BLOB, updates BLOB)", ), CdcVersion::V2 => format!( "CREATE TABLE IF NOT EXISTS {cdc_table_name} (change_id INTEGER PRIMARY KEY AUTOINCREMENT, change_time INTEGER, change_txn_id INTEGER, change_type INTEGER, table_name TEXT, id, before BLOB, after BLOB, updates BLOB)", ), }; let mut stmt = conn.prepare(create_sql)?; stmt.program .prepared .needs_stmt_subtransactions .store(false, Ordering::Relaxed); let res = stmt.run_ignore_rows(); conn.end_nested(); res?; } // Step 2: Create version table if needed { conn.start_nested(); let mut stmt = conn.prepare(format!( "CREATE TABLE IF NOT EXISTS {TURSO_CDC_VERSION_TABLE_NAME} (table_name TEXT PRIMARY KEY, version TEXT NOT NULL)", ))?; stmt.program .prepared .needs_stmt_subtransactions .store(false, Ordering::Relaxed); let res = stmt.run_ignore_rows(); conn.end_nested(); res?; } // Step 3: Insert version row only if one doesn't already exist. // If the CDC table pre-existed without a version row, it's a legacy v1 table. let version_to_insert = if cdc_table_exists { CdcVersion::V1 } else { *version }; { conn.start_nested(); let mut stmt = conn.prepare(format!( "INSERT OR IGNORE INTO {TURSO_CDC_VERSION_TABLE_NAME} (table_name, version) VALUES ('{escaped_cdc_table_name}', '{version_to_insert}')", ))?; stmt.program .prepared .needs_stmt_subtransactions .store(false, Ordering::Relaxed); let res = stmt.run_ignore_rows(); conn.end_nested(); res?; } // Step 4: Read back the actual version from the table (may differ from // `version` if the row already existed with an older version). let actual_version = { conn.start_nested(); let mut stmt = conn.prepare(format!( "SELECT version FROM {TURSO_CDC_VERSION_TABLE_NAME} WHERE table_name = '{escaped_cdc_table_name}'", ))?; stmt.program .prepared .needs_stmt_subtransactions .store(false, Ordering::Relaxed); let rows = stmt.run_collect_rows(); conn.end_nested(); let rows = rows?; match rows.first().and_then(|r| r.first()) { Some(crate::Value::Text(text)) => text.to_string().parse::()?, _ => *version, } }; // Defer enabling CDC until the program completes successfully (Halt). // This ensures that if the transaction rolls back, the connection's // CDC state remains unchanged. let opts = CaptureDataChangesInfo::parse(cdc_mode, Some(actual_version))?; state.pending_cdc_info = Some(opts); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_populate_materialized_views( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!(PopulateMaterializedViews { cursors }, insn); let conn = program.connection.clone(); // For each view, get its cursor and root page let mut view_info = Vec::new(); { let cursors_ref = &state.cursors; for (view_name, cursor_id) in cursors { // Get the cursor to find the root page let cursor = cursors_ref .get(*cursor_id) .and_then(|c| c.as_ref()) .ok_or_else(|| { LimboError::InternalError(format!("Cursor {cursor_id} not found")) })?; let root_page = match cursor { crate::types::Cursor::BTree(btree_cursor) => btree_cursor.root_page(), _ => { return Err(LimboError::InternalError( "Expected BTree cursor for materialized view".into(), )); } }; view_info.push((view_name.clone(), root_page, *cursor_id)); } } // Now populate the views (after releasing the schema borrow) for (view_name, _root_page, cursor_id) in view_info { let schema = conn.schema.read(); if let Some(view) = schema.get_materialized_view(&view_name) { let mut view = view.lock(); // Drop the schema borrow before calling populate_from_table drop(schema); // Get the cursor for writing // Get a mutable reference to the cursor let cursors_ref = &mut state.cursors; let cursor = cursors_ref .get_mut(cursor_id) .and_then(|c| c.as_mut()) .ok_or_else(|| { LimboError::InternalError(format!( "Cursor {cursor_id} not found for population" )) })?; // Extract the BTreeCursor let btree_cursor = match cursor { crate::types::Cursor::BTree(btree_cursor) => btree_cursor, _ => { return Err(LimboError::InternalError( "Expected BTree cursor for materialized view population".into(), )); } }; // Now populate it with the cursor for writing return_if_io!(view.populate_from_table(&conn, pager, btree_cursor.as_mut())); } } // All views populated, advance to next instruction state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_read_cookie( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(ReadCookie { db, dest, cookie }, insn); let pager = program.get_pager_from_database_index(db); let mv_store = program.connection.mv_store_for_db(*db); let cookie_value = match with_header( &pager, mv_store.as_ref(), program, *db, |header| match cookie { Cookie::ApplicationId => header.application_id.get().into(), Cookie::UserVersion => header.user_version.get().into(), Cookie::SchemaVersion => header.schema_cookie.get().into(), Cookie::LargestRootPageNumber => header.vacuum_mode_largest_root_page.get().into(), cookie => todo!("{cookie:?} is not yet implement for ReadCookie"), }, ) { Err(_) => 0.into(), Ok(IOResult::Done(v)) => v, Ok(IOResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), }; state.registers[*dest].set_int(cookie_value); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_set_cookie( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( SetCookie { db, cookie, value, p5: _, }, insn ); let pager = program.get_pager_from_database_index(db); let mv_store = program.connection.mv_store_for_db(*db); if let Some(mv_store) = mv_store.as_ref() { let Some(tx_id) = program.connection.get_mv_tx_id_for_db(*db) else { return Err(LimboError::InternalError( "Header updates in MVCC mode require an active transaction".to_string(), )); }; if !mv_store.is_exclusive_tx(&tx_id) { // Header cookies are global metadata with no row-level conflict keys; require // SQLite-style single-writer semantics (same policy as DDL in MVCC). return Err(LimboError::TxError( "Header updates require an exclusive transaction (use BEGIN instead of BEGIN CONCURRENT)".to_string(), )); } } return_if_io!(with_header_mut( &pager, mv_store.as_ref(), program, *db, |header| { match cookie { Cookie::ApplicationId => header.application_id = (*value).into(), Cookie::UserVersion => header.user_version = (*value).into(), Cookie::LargestRootPageNumber => { header.vacuum_mode_largest_root_page = (*value as u32).into(); } Cookie::IncrementalVacuum => { header.incremental_vacuum_enabled = (*value as u32).into() } Cookie::SchemaVersion => { // Only mark schema_did_change on connection for main database (db 0). // Attached databases track their schema independently. if *db == crate::MAIN_DB_ID { match program.connection.get_tx_state() { TransactionState::Write { .. } => { program.connection.set_tx_state(TransactionState::Write { schema_did_change: true }); }, TransactionState::Read => unreachable!("invalid transaction state for SetCookie: TransactionState::Read, should be write"), TransactionState::None => unreachable!("invalid transaction state for SetCookie: TransactionState::None, should be write"), TransactionState::PendingUpgrade { .. } => unreachable!("invalid transaction state for SetCookie: TransactionState::PendingUpgrade, should be write"), } } program.connection.with_database_schema_mut(*db, |schema| { schema.schema_version = *value as u32 }); header.schema_cookie = (*value as u32).into(); } cookie => todo!("{cookie:?} is not yet implement for SetCookie"), }; } )); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_shift_right( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(ShiftRight { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_shift_right(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_shift_left( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(ShiftLeft { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_shift_left(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_add_imm( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(AddImm { register, value }, insn); let current = &state.registers[*register]; let current_value = match current { Register::Value(val) => val, Register::Aggregate(_) => &Value::Null, Register::Record(_) => &Value::Null, }; let int_val = match current_value { Value::Numeric(Numeric::Integer(i)) => i + value, Value::Numeric(Numeric::Float(f)) => (f64::from(*f) as i64) + value, Value::Text(s) => s.as_str().parse::().unwrap_or(0) + value, Value::Blob(_) => *value, // BLOB becomes the added value Value::Null => *value, // NULL becomes the added value }; state.registers[*register].set_int(int_val); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_variable( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Variable { index, dest }, insn); state.registers[*dest].set_value(state.get_parameter(*index)); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_zero_or_null( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(ZeroOrNull { rg1, rg2, dest }, insn); if state.registers[*rg1].is_null() || state.registers[*rg2].is_null() { state.registers[*dest].set_null() } else { state.registers[*dest].set_int(0); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_not( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Not { reg, dest }, insn); match state.registers[*reg].get_value().exec_boolean_not() { Value::Numeric(Numeric::Integer(i)) => state.registers[*dest].set_int(i), _ => state.registers[*dest].set_null(), }; state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Implements IS TRUE, IS FALSE, IS NOT TRUE, IS NOT FALSE. /// A value is "true" only if it's a non-zero number. /// Text and blobs are parsed as numbers; if not parseable, treated as 0 (falsy). /// NULL is handled specially with the null_value parameter. pub fn op_is_true( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( IsTrue { reg, dest, null_value, invert }, insn ); let value = state.registers[*reg].get_value(); // Use Numeric::try_into_bool which handles the conversion of text/blob to numbers let final_result = match Numeric::from_value(value).map(|val| val.to_bool()) { // For NULL, store null_value directly (no inversion) None => { if *null_value { 1 } else { 0 } } // For non-NULL, optionally invert the boolean result Some(is_truthy) => { let result = if is_truthy { 1 } else { 0 }; if *invert { 1 - result } else { result } } }; state.registers[*dest].set_int(final_result); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_concat( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Concat { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_concat(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_and( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(And { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_and(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_or( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Or { lhs, rhs, dest }, insn); state.registers[*dest].set_value( state.registers[*lhs] .get_value() .exec_or(state.registers[*rhs].get_value()), ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_noop( _program: &Program, state: &mut ProgramState, _insn: &Insn, _pager: &Arc, ) -> Result { // Do nothing // Advance the program counter for the next opcode state.pc += 1; Ok(InsnFunctionStepResult::Step) } #[derive(Default)] pub enum OpOpenEphemeralState { #[default] Start, // Fast path states for reusing existing ephemeral cursor ClearExisting, RewindExisting, // Slow path states for creating new ephemeral cursor StartingTxn { pager: Arc, }, CreateBtree { pager: Arc, }, // clippy complains this variant is too big when compared to the rest of the variants // so it says we need to box it here Rewind { cursor: Box, }, } pub fn op_open_ephemeral( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { let (cursor_id, is_table) = match insn { Insn::OpenEphemeral { cursor_id, is_table, } => (*cursor_id, *is_table), Insn::OpenAutoindex { cursor_id } => (*cursor_id, false), _ => unreachable!("unexpected Insn {:?}", insn), }; let mv_store = program.connection.mv_store(); match &mut state.op_open_ephemeral_state { OpOpenEphemeralState::Start => { tracing::trace!("Start"); // Fast path: if cursor already has an ephemeral btree, just clear it instead of // recreating the entire pager/file/btree. This is important for performance when // OpenEphemeral is called repeatedly during statement execution. if state.cursors[cursor_id].is_some() { state.op_open_ephemeral_state = OpOpenEphemeralState::ClearExisting; return Ok(InsnFunctionStepResult::Step); } // Ephemeral tables always use the main DB's page size (db index 0) // regardless of which database triggered the ephemeral allocation. let page_size = return_if_io!(with_header( pager, mv_store.as_ref(), program, 0, |header| { header.page_size } )); let conn = program.connection.clone(); let io = conn.pager.load().io.clone(); let rand_num = io.generate_random_number(); let db_file: Arc; let db_file_io: Arc; // we support OPFS in WASM - but it require files to be pre-opened in the browser before use // we can fix this if we will make open_file interface async // but for now for simplicity we use MemoryIO for all intermediate calculations #[cfg(target_family = "wasm")] { use crate::MemoryIO; db_file_io = Arc::new(MemoryIO::new()); let file = db_file_io.open_file("temp-file", OpenFlags::Create, false)?; db_file = Arc::new(DatabaseFile::new(file)); } #[cfg(not(target_family = "wasm"))] { let temp_store = conn.get_temp_store(); if matches!(temp_store, crate::TempStore::Memory) { // When temp_store=memory, use in-memory storage for ephemeral tables use crate::MemoryIO; db_file_io = Arc::new(MemoryIO::new()); let file = db_file_io.open_file("temp-file", OpenFlags::Create, false)?; db_file = Arc::new(DatabaseFile::new(file)); } else { let temp_dir = temp_dir(); let rand_path = std::path::Path::new(&temp_dir) .join(format!("tursodb-ephemeral-{rand_num}")); let Some(rand_path_str) = rand_path.to_str() else { return Err(LimboError::InternalError( "Failed to convert path to string".to_string(), )); }; let file = io.open_file(rand_path_str, OpenFlags::Create, false)?; db_file = Arc::new(DatabaseFile::new(file)); db_file_io = io; } } let buffer_pool = program.connection.db.buffer_pool.clone(); // Ephemeral databases always start empty, so create their own init_page_1 let ephemeral_init_page_1 = Arc::new(arc_swap::ArcSwapOption::new(Some(default_page1(None)))); let pager = Arc::new(Pager::new( db_file, None, db_file_io, PageCache::default(), buffer_pool, Arc::new(Mutex::new(())), ephemeral_init_page_1, )?); pager.set_page_size(page_size); state.op_open_ephemeral_state = OpOpenEphemeralState::StartingTxn { pager }; } OpOpenEphemeralState::ClearExisting => { tracing::trace!("ClearExisting"); let cursor = state.cursors[cursor_id] .as_mut() .expect("cursor should exist in ClearExisting state"); let btree_cursor = cursor.as_btree_mut(); btree_cursor.set_null_flag(false); return_if_io!(btree_cursor.clear_btree()); // iterate over existing deferred seeks and clear them as well, // as any deferred seek on this cursor is now invalid. for deferred_seek in &mut state.deferred_seeks { if let Some(ds) = deferred_seek { if ds.index_cursor_id == cursor_id || ds.table_cursor_id == cursor_id { *deferred_seek = None; } } } state.op_open_ephemeral_state = OpOpenEphemeralState::RewindExisting; } OpOpenEphemeralState::RewindExisting => { tracing::trace!("RewindExisting"); let cursor = state.cursors[cursor_id] .as_mut() .expect("cursor should exist in RewindExisting state"); let btree_cursor = cursor.as_btree_mut(); return_if_io!(btree_cursor.rewind()); state.pc += 1; state.op_open_ephemeral_state = OpOpenEphemeralState::Start; } OpOpenEphemeralState::StartingTxn { pager } => { tracing::trace!("StartingTxn"); pager .begin_read_tx() // we have to begin a read tx before beginning a write .expect("Failed to start read transaction"); return_if_io!(pager.begin_write_tx()); state.op_open_ephemeral_state = OpOpenEphemeralState::CreateBtree { pager: pager.clone(), }; } OpOpenEphemeralState::CreateBtree { pager } => { tracing::trace!("CreateBtree"); // FIXME: handle page cache is full let flag = if is_table { &CreateBTreeFlags::new_table() } else { &CreateBTreeFlags::new_index() }; let root_page = return_if_io!(pager.btree_create(flag)) as i64; let (_, cursor_type) = program .cursor_ref .get(cursor_id) .expect("cursor_id should exist in cursor_ref"); let num_columns = match cursor_type { CursorType::BTreeTable(table_rc) => table_rc.columns.len(), CursorType::BTreeIndex(index_arc) => index_arc.columns.len(), _ => unreachable!("This should not have happened"), }; let cursor = if let CursorType::BTreeIndex(index) = cursor_type { BTreeCursor::new_index(pager.clone(), root_page, index, num_columns) } else { BTreeCursor::new_table(pager.clone(), root_page, num_columns) }; state.op_open_ephemeral_state = OpOpenEphemeralState::Rewind { cursor: Box::new(cursor), }; } OpOpenEphemeralState::Rewind { cursor } => { return_if_io!(cursor.rewind()); let cursors = &mut state.cursors; let (_, cursor_type) = program .cursor_ref .get(cursor_id) .expect("cursor_id should exist in cursor_ref"); let OpOpenEphemeralState::Rewind { cursor } = std::mem::take(&mut state.op_open_ephemeral_state) else { unreachable!() }; // Table content is erased if the cursor already exists match cursor_type { CursorType::BTreeTable(_) => { cursors .get_mut(cursor_id) .expect("cursor_id should be valid") .replace(Cursor::new_btree(cursor)); } CursorType::BTreeIndex(_) => { cursors .get_mut(cursor_id) .expect("cursor_id should be valid") .replace(Cursor::new_btree(cursor)); } CursorType::Pseudo(_) => { panic!("OpenEphemeral on pseudo cursor"); } CursorType::Sorter => { panic!("OpenEphemeral on sorter cursor"); } CursorType::VirtualTable(_) => { panic!("OpenEphemeral on virtual table cursor, use Insn::VOpen instead"); } CursorType::IndexMethod(..) => { panic!("OpenEphemeral on index method cursor") } CursorType::MaterializedView(_, _) => { panic!("OpenEphemeral on materialized view cursor"); } } state.pc += 1; state.op_open_ephemeral_state = OpOpenEphemeralState::Start; } } Ok(InsnFunctionStepResult::Step) } pub fn op_open_dup( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( OpenDup { new_cursor_id, original_cursor_id, }, insn ); let mv_store = program.connection.mv_store(); let original_cursor = state.get_cursor(*original_cursor_id); let original_cursor = original_cursor.as_btree_mut(); let root_page = original_cursor.root_page(); // We use the pager from the original cursor instead of the one attached to // the connection because each ephemeral table creates its own pager (and // a separate database file). let pager = original_cursor.get_pager(); // Ephemeral tables have their own pager, so we need to check if this is an // ephemeral cursor by comparing pagers. Ephemeral cursors should NOT be wrapped // in MvCursor because the mv_store doesn't have mappings for ephemeral table root pages. let is_ephemeral = pager.wal.is_none(); let (_, cursor_type) = program .cursor_ref .get(*original_cursor_id) .expect("cursor_id should exist in cursor_ref"); match cursor_type { CursorType::BTreeTable(table) => { let cursor = Box::new(BTreeCursor::new_table( pager, maybe_transform_root_page_to_positive(mv_store.as_ref(), root_page), table.columns.len(), )); let cursor: Box = if !is_ephemeral { if let Some(tx_id) = program.connection.get_mv_tx_id() { let mv_store = mv_store .as_ref() .expect("mv_store should be Some when MVCC transaction is active") .clone(); Box::new(MvCursor::new( mv_store, tx_id, root_page, MvccCursorType::Table, cursor, )?) } else { cursor } } else { cursor }; let cursors = &mut state.cursors; cursors .get_mut(*new_cursor_id) .expect("cursor_id should be valid") .replace(Cursor::new_btree(cursor)); } CursorType::BTreeIndex(_) => { return Err(LimboError::InternalError( "OpenDup is not supported for BTreeIndex".to_string(), )); } _ => { return Err(LimboError::InternalError(format!( "OpenDup is not supported for {cursor_type:?}" ))); } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Execute the [Insn::Once] instruction. /// /// This instruction is used to execute a block of code only once. /// If the instruction is executed again, it will jump to the target program counter. pub fn op_once( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Once { target_pc_when_reentered, }, insn ); assert!(target_pc_when_reentered.is_offset()); let offset = state.pc; if state.once.contains(&offset) { state.pc = target_pc_when_reentered.as_offset_int(); return Ok(InsnFunctionStepResult::Step); } state.once.push(offset); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_found( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { let (cursor_id, target_pc, record_reg, num_regs) = match insn { Insn::NotFound { cursor_id, target_pc, record_reg, num_regs, } => (cursor_id, target_pc, record_reg, num_regs), Insn::Found { cursor_id, target_pc, record_reg, num_regs, } => (cursor_id, target_pc, record_reg, num_regs), _ => unreachable!("unexpected Insn {:?}", insn), }; let not = matches!(insn, Insn::NotFound { .. }); let record_source = if *num_regs == 0 { RecordSource::Packed { record_reg: *record_reg, } } else { RecordSource::Unpacked { start_reg: *record_reg, num_regs: *num_regs, } }; let seek_result = match seek_internal( program, state, pager, record_source, *cursor_id, true, SeekOp::GE { eq_only: true }, ) { Ok(SeekInternalResult::Found) => SeekResult::Found, Ok(SeekInternalResult::NotFound) => SeekResult::NotFound, Ok(SeekInternalResult::IO(io)) => return Ok(InsnFunctionStepResult::IO(io)), Err(e) => return Err(e), }; let found = matches!(seek_result, SeekResult::Found); let do_jump = (!found && not) || (found && !not); if do_jump { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_affinity( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Affinity { start_reg, count, affinities, }, insn ); if affinities.len() != count.get() { return Err(LimboError::InternalError( "Affinity: the length of affinities does not match the count".into(), )); } for (i, affinity_char) in affinities.chars().enumerate().take(count.get()) { let reg_index = *start_reg + i; let affinity = Affinity::from_char(affinity_char); apply_affinity_char(&mut state.registers[reg_index], affinity); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_count( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Count { cursor_id, target_reg, exact, }, insn ); let count = { let cursor = must_be_btree_cursor!(*cursor_id, program.cursor_ref, state, "Count"); let cursor = cursor.as_btree_mut(); return_if_io!(cursor.count()) }; state.registers[*target_reg].set_int(count as i64); // For optimized COUNT(*) queries, the count represents rows that would be read // SQLite tracks this differently (as pages read), but for consistency we track as rows if *exact { state.metrics.rows_read = state.metrics.rows_read.saturating_add(count as u64); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Format integrity check errors into a result string. /// Returns NULL when no errors were found. fn format_integrity_check_result(errors: &[IntegrityCheckError]) -> Option { if errors.is_empty() { None } else { Some( errors .iter() .map(|e| e.to_string()) .collect::>() .join("\n"), ) } } fn has_freelist_error(errors: &[IntegrityCheckError]) -> bool { errors.iter().any(|err| match err { IntegrityCheckError::FreelistTrunkCorrupt { .. } | IntegrityCheckError::FreelistPointerOutOfRange { .. } => true, IntegrityCheckError::PageReferencedMultipleTimes { page_category, .. } => { matches!( page_category, PageCategory::FreeListTrunk | PageCategory::FreePage ) } _ => false, }) } pub enum OpIntegrityCheckState { Start, CheckingBTreeStructure { errors: Vec, current_root_idx: usize, state: IntegrityCheckState, }, } pub fn op_integrity_check( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!( IntegrityCk { db, max_errors, roots, message_register, }, insn ); let mv_store = program.connection.mv_store_for_db(*db); // Use the correct pager for the target database (main or attached) let target_pager = if *db == crate::MAIN_DB_ID { pager.clone() } else { program.get_pager_from_database_index(db) }; match &mut state.op_integrity_check_state { OpIntegrityCheckState::Start => { let (freelist_trunk_page, db_size) = return_if_io!(with_header( &target_pager, mv_store.as_ref(), program, *db, |header| (header.freelist_trunk_page.get(), header.database_size.get()) )); let mut errors = Vec::new(); let mut integrity_check_state = IntegrityCheckState::new(db_size as usize); let mut current_root_idx = 0; if freelist_trunk_page > 0 { let expected_freelist_count = return_if_io!(with_header( &target_pager, mv_store.as_ref(), program, *db, |header| { header.freelist_pages.get() } )); integrity_check_state.set_expected_freelist_count(expected_freelist_count as usize); integrity_check_state.start( freelist_trunk_page as i64, PageCategory::FreeListTrunk, &mut errors, ); } else if !roots.is_empty() { integrity_check_state.start(roots[0], PageCategory::Normal, &mut errors); current_root_idx += 1; } state.op_integrity_check_state = OpIntegrityCheckState::CheckingBTreeStructure { errors, state: integrity_check_state, current_root_idx, }; } OpIntegrityCheckState::CheckingBTreeStructure { errors, current_root_idx, state: integrity_check_state, } => { return_if_io!(integrity_check( integrity_check_state, errors, &target_pager, mv_store.as_ref() )); if errors.len() >= *max_errors { errors.truncate(*max_errors); let message = format_integrity_check_result(errors); match message { Some(msg) => state.registers[*message_register].set_text(Text::new(msg)), None => state.registers[*message_register].set_null(), }; state.op_integrity_check_state = OpIntegrityCheckState::Start; state.pc += 1; return Ok(InsnFunctionStepResult::Step); } if *current_root_idx < roots.len() { integrity_check_state.start(roots[*current_root_idx], PageCategory::Normal, errors); *current_root_idx += 1; return Ok(InsnFunctionStepResult::Step); } if !has_freelist_error(errors) && integrity_check_state.freelist_count.actual_count != integrity_check_state.freelist_count.expected_count { errors.push(IntegrityCheckError::FreelistCountMismatch { actual_count: integrity_check_state.freelist_count.actual_count, expected_count: integrity_check_state.freelist_count.expected_count, }); } #[cfg(not(feature = "omit_autovacuum"))] let skip_page_never_used = !matches!( target_pager.get_auto_vacuum_mode(), crate::storage::pager::AutoVacuumMode::None ); #[cfg(feature = "omit_autovacuum")] let skip_page_never_used = false; if !skip_page_never_used { for page_number in 2..=integrity_check_state.db_size { if !integrity_check_state .page_reference .contains_key(&(page_number as i64)) { if target_pager.pending_byte_page_id() != Some(page_number as u32) { errors.push(IntegrityCheckError::PageNeverUsed { page_id: page_number as i64, }); } } else if target_pager.pending_byte_page_id() == Some(page_number as u32) { errors.push(IntegrityCheckError::PendingBytePageUsed { page_id: page_number as i64, }) } if errors.len() >= *max_errors { break; } } } errors.truncate(*max_errors); let message = format_integrity_check_result(errors); match message { Some(msg) => state.registers[*message_register].set_text(Text::new(msg)), None => state.registers[*message_register].set_null(), }; state.op_integrity_check_state = OpIntegrityCheckState::Start; state.pc += 1; } } Ok(InsnFunctionStepResult::Step) } pub fn op_cast( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(Cast { reg, affinity }, insn); let value = state.registers[*reg].get_value().clone(); let result = match affinity { Affinity::Blob => value.exec_cast("BLOB"), Affinity::Text => value.exec_cast("TEXT"), Affinity::Numeric => value.exec_cast("NUMERIC"), Affinity::Integer => value.exec_cast("INTEGER"), Affinity::Real => value.exec_cast("REAL"), }; state.registers[*reg].set_value(result); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_rename_table( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(RenameTable { db, from, to }, insn); let normalized_from = normalize_ident(from.as_str()); let normalized_to = normalize_ident(to.as_str()); let conn = program.connection.clone(); conn.with_database_schema_mut(*db, |schema| -> crate::Result<()> { if let Some(mut indexes) = schema.indexes.remove(&normalized_from) { let autoindex_prefix = format!("sqlite_autoindex_{normalized_from}_"); indexes.iter_mut().for_each(|index| { let index = Arc::make_mut(index); normalized_to.clone_into(&mut index.table_name); // Rename autoindexes to match the new table name if let Some(suffix) = index.name.strip_prefix(&autoindex_prefix) { index.name = format!("sqlite_autoindex_{normalized_to}_{suffix}"); } }); schema.indexes.insert(normalized_to.to_owned(), indexes); }; let mut table = schema .tables .remove(&normalized_from) .expect("table being renamed should be in schema"); match Arc::make_mut(&mut table) { Table::BTree(btree) => { let btree = Arc::make_mut(btree); // update this table's own foreign keys for fk_arc in &mut btree.foreign_keys { let fk = Arc::make_mut(fk_arc); if normalize_ident(&fk.parent_table) == normalized_from { fk.parent_table.clone_from(&normalized_to); } } // Rewrite table-qualified refs in CHECK constraints for check in &mut btree.check_constraints { rewrite_check_expr_table_refs( &mut check.expr, &normalized_from, &normalized_to, ); } normalized_to.clone_into(&mut btree.name); } Table::Virtual(vtab) => { Arc::make_mut(vtab).name.clone_from(&normalized_to); } _ => panic!("only btree and virtual tables can be renamed"), } schema.tables.insert(normalized_to.to_owned(), table); for (tname, t_arc) in schema.tables.iter_mut() { // skip the table we just renamed if normalize_ident(tname) == normalized_to { continue; } if let Table::BTree(ref mut child_btree_arc) = Arc::make_mut(t_arc) { let child_btree = Arc::make_mut(child_btree_arc); for fk_arc in &mut child_btree.foreign_keys { if normalize_ident(&fk_arc.parent_table) == normalized_from { let fk = Arc::make_mut(fk_arc); fk.parent_table.clone_from(&normalized_to); } } } } // Update triggers: move from old table name key to new, and update // each trigger's table_name field and body commands. if let Some(mut triggers) = schema.triggers.remove(&normalized_from) { for trigger_arc in &mut triggers { let trigger = Arc::make_mut(trigger_arc); normalized_to.clone_into(&mut trigger.table_name); // Rewrite table references in trigger body commands for cmd in &mut trigger.commands { rewrite_trigger_cmd_table_refs(cmd, &normalized_from, &normalized_to); } // Rewrite WHEN clause qualified refs if let Some(ref mut when) = trigger.when_clause { rewrite_check_expr_table_refs(when, &normalized_from, &normalized_to); } } schema.triggers.insert(normalized_to.to_owned(), triggers); } // Also update triggers on OTHER tables that reference the renamed table // in their body commands (e.g., INSERT INTO old_name in a trigger on another table) for (_, triggers) in schema.triggers.iter_mut() { for trigger_arc in triggers.iter_mut() { let trigger = Arc::make_mut(trigger_arc); for cmd in &mut trigger.commands { rewrite_trigger_cmd_table_refs(cmd, &normalized_from, &normalized_to); } if let Some(ref mut when) = trigger.when_clause { rewrite_check_expr_table_refs(when, &normalized_from, &normalized_to); } } } Ok(()) })?; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_drop_column( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( DropColumn { db, table, column_index }, insn ); let conn = program.connection.clone(); let normalized_table_name = normalize_ident(table.as_str()); let column_name = conn.with_schema(*db, |schema| { let table = schema .tables .get(&normalized_table_name) .expect("table being ALTERed should be in schema"); table .get_column_at(*column_index) .expect("column being ALTERed should be in schema") .name .as_ref() .expect("column being ALTERed should be named") .clone() }); conn.with_database_schema_mut(*db, |schema| { let table = schema .tables .get_mut(&normalized_table_name) .expect("table being renamed should be in schema"); let table = Arc::get_mut(table).expect("this should be the only strong reference"); let Table::BTree(btree) = table else { panic!("only btree tables can be renamed"); }; let btree = Arc::make_mut(btree); btree.columns.remove(*column_index); // Remove column-level CHECK constraints for the dropped column let col_name = column_name.clone(); btree.check_constraints.retain(|c| { c.column .as_ref() .is_none_or(|col| normalize_ident(col) != normalize_ident(&col_name)) }); }); conn.with_schema(*db, |schema| -> crate::Result<()> { if let Some(indexes) = schema.indexes.get(&normalized_table_name) { for index in indexes { if index .columns .iter() .any(|column| column.pos_in_table == *column_index) { return Err(LimboError::ParseError(format!( "cannot drop column \"{column_name}\": indexed" ))); } } } Ok(()) })?; // Update index.pos_in_table for all indexes. // For example, if the dropped column had index 2, then anything that was indexed on column 3 or higher should be decremented by 1. conn.with_database_schema_mut(*db, |schema| { if let Some(indexes) = schema.indexes.get_mut(&normalized_table_name) { for index in indexes { let index = Arc::get_mut(index).expect("this should be the only strong reference"); for index_column in index.columns.iter_mut() { if index_column.pos_in_table > *column_index { index_column.pos_in_table -= 1; } } } } }); conn.with_schema(*db, |schema| -> crate::Result<()> { for (view_name, view) in schema.views.iter() { let view_select_sql = format!("SELECT * FROM {view_name}"); let _ = conn.prepare(view_select_sql.as_str()).map_err(|e| { LimboError::ParseError(format!( "cannot drop column \"{}\": referenced in VIEW {view_name}: {}. {e}", column_name, view.sql, )) })?; } Ok(()) })?; state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_add_column( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( AddColumn { db, table, column, check_constraints }, insn ); let conn = program.connection.clone(); let normalized_table_name = normalize_ident(table.as_str()); conn.with_database_schema_mut(*db, |schema| { let table_ref = schema .tables .get_mut(&normalized_table_name) .expect("table being altered should be in schema"); let table_ref = Arc::make_mut(table_ref); let crate::schema::Table::BTree(btree) = table_ref else { panic!("only btree tables can have columns added"); }; let btree = Arc::make_mut(btree); btree.columns.push((**column).clone()); // Update CHECK constraints to include any constraints from the new column btree.check_constraints.clone_from(check_constraints); }); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_alter_column( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( AlterColumn { db, table: table_name, column_index, definition, rename, }, insn ); let conn = program.connection.clone(); let normalized_table_name = normalize_ident(table_name.as_str()); let old_column_name = conn.with_schema(*db, |schema| { let table = schema .tables .get(&normalized_table_name) .expect("table being ALTERed should be in schema"); table .get_column_at(*column_index) .expect("column being ALTERed should be in schema") .name .as_ref() .expect("column being ALTERed should be named") .clone() }); let new_column = crate::schema::Column::try_from(definition.as_ref())?; let new_name = definition.col_name.as_str().to_owned(); let view_rewrites = if *rename { let target_db_name = conn.get_database_name_by_index(*db).ok_or_else(|| { LimboError::InternalError(format!("unknown database id {} during ALTER TABLE", *db)) })?; conn.with_schema( *db, |schema| -> crate::Result> { let mut rewrites = Vec::new(); for (view_name, view) in schema.views.iter() { if let Some(rewritten) = rewrite_view_sql_for_column_rename( &view.sql, schema, table_name.as_str(), &target_db_name, &old_column_name, &new_name, )? { rewrites.push((view_name.clone(), rewritten)); } } Ok(rewrites) }, )? } else { Vec::new() }; conn.with_database_schema_mut(*db, |schema| { let table_arc = schema .tables .get_mut(&normalized_table_name) .expect("table being ALTERed should be in schema"); let table = Arc::make_mut(table_arc); let Table::BTree(ref mut btree_arc) = table else { panic!("only btree tables can be altered"); }; let btree = Arc::make_mut(btree_arc); let col = btree .columns .get_mut(*column_index) .expect("column being ALTERed should be in schema"); // Update indexes on THIS table that name the old column (you already had this) if let Some(idxs) = schema.indexes.get_mut(&normalized_table_name) { for idx in idxs { let idx = Arc::make_mut(idx); for ic in &mut idx.columns { if ic.name.eq_ignore_ascii_case( col.name.as_ref().expect("btree column should be named"), ) { ic.name.clone_from(&new_name); } } // Update partial index WHERE clause column references if let Some(ref mut wc) = idx.where_clause { rename_identifiers(wc, &old_column_name, &new_name); } } } if *rename { col.name = Some(new_name.clone()); } else { *col = new_column.clone(); } // Keep primary_key_columns consistent (names may change on rename) for (pk_name, _ord) in &mut btree.primary_key_columns { if pk_name.eq_ignore_ascii_case(&old_column_name) { pk_name.clone_from(&new_name); } } // Update unique_sets to reflect the renamed column for unique_set in &mut btree.unique_sets { for (col_name, _) in &mut unique_set.columns { if col_name.eq_ignore_ascii_case(&old_column_name) { col_name.clone_from(&new_name); } } } // Update CHECK constraint expressions to reference the new column name let old_col_normalized = normalize_ident(&old_column_name); for check in &mut btree.check_constraints { rename_identifiers(&mut check.expr, &old_col_normalized, &new_name); if let Some(ref mut col) = check.column { if col.eq_ignore_ascii_case(&old_column_name) { col.clone_from(&new_name); } } } // Maintain rowid-alias bit after change/rename (INTEGER PRIMARY KEY) if !*rename { // recompute alias from `new_column` btree.columns[*column_index].set_rowid_alias(new_column.is_rowid_alias()); } // Update this table's OWN foreign keys for fk_arc in &mut btree.foreign_keys { let fk = Arc::make_mut(fk_arc); // child side: rename child column if it matches for cc in &mut fk.child_columns { if cc.eq_ignore_ascii_case(&old_column_name) { cc.clone_from(&new_name); } } // parent side: if self-referencing, rename parent column too if normalize_ident(&fk.parent_table) == normalized_table_name { for pc in &mut fk.parent_columns { if pc.eq_ignore_ascii_case(&old_column_name) { pc.clone_from(&new_name); } } } } // fix OTHER tables that reference this table as parent for (tname, t_arc) in schema.tables.iter_mut() { if normalize_ident(tname) == normalized_table_name { continue; } if let Table::BTree(ref mut child_btree_arc) = Arc::make_mut(t_arc) { let child_btree = Arc::make_mut(child_btree_arc); for fk_arc in &mut child_btree.foreign_keys { if normalize_ident(&fk_arc.parent_table) != normalized_table_name { continue; } let fk = Arc::make_mut(fk_arc); for pc in &mut fk.parent_columns { if pc.eq_ignore_ascii_case(&old_column_name) { pc.clone_from(&new_name); } } } } } }); if *rename { let old_col = old_column_name.clone(); let new_col = new_name; let tbl_name = normalized_table_name.clone(); // Update in-memory trigger objects for the renamed column conn.with_database_schema_mut(*db, move |schema| { for (_, triggers) in schema.triggers.iter_mut() { for trigger_arc in triggers.iter_mut() { let trigger_tbl = normalize_ident(&trigger_arc.table_name); let trigger = Arc::make_mut(trigger_arc); // Rewrite WHEN clause: only rename NEW.col / OLD.col qualified refs. // Bare column names in WHEN clauses are invalid per SQLite semantics, // so we must not rename them (SQLite would error on such triggers). if let Some(ref mut when) = trigger.when_clause { rename_identifiers_scoped_when_clause( when, &tbl_name, &trigger_tbl, &old_col, &new_col, ); } // Rewrite UPDATE OF columns if trigger is on the renamed table if trigger_tbl == tbl_name { if let ast::TriggerEvent::UpdateOf(ref mut cols) = trigger.event { for col in cols { if normalize_ident(col.as_str()) == normalize_ident(&old_col) { *col = ast::Name::exact(new_col.clone()); } } } } // Rewrite trigger body commands for cmd in &mut trigger.commands { rewrite_trigger_cmd_column_refs( cmd, &tbl_name, &trigger_tbl, &old_col, &new_col, ); } } } }); let rewrites = view_rewrites; conn.with_database_schema_mut(*db, move |schema| -> crate::Result<()> { for (view_name, rewritten) in rewrites { if let Some(view_arc) = schema.views.get_mut(&view_name) { let view = Arc::make_mut(view_arc); view.sql = rewritten.sql; view.select_stmt = rewritten.select_stmt; view.columns = rewritten.columns; } } Ok(()) })?; if !crate::is_attached_db(*db) { conn.with_schema(*db, |schema| -> crate::Result<()> { let table = schema .tables .get(&normalized_table_name) .expect("table being ALTERed should be in schema"); let _column = table .get_column_at(*column_index) .expect("column being ALTERed should be in schema"); for (view_name, view) in schema.views.iter() { let view_select_sql = format!("SELECT * FROM {view_name}"); let _ = conn.prepare(view_select_sql.as_str()).map_err(|e| { LimboError::ParseError(format!( "cannot rename column \"{}\": referenced in VIEW {view_name}: {}. {e}", old_column_name, view.sql, )) })?; } Ok(()) })?; } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_if_neg( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(IfNeg { reg, target_pc }, insn); match &state.registers[*reg] { Register::Value(Value::Numeric(Numeric::Integer(i))) if *i < 0 => { state.pc = target_pc.as_offset_int(); } Register::Value(Value::Numeric(Numeric::Float(f))) if f64::from(*f) < 0.0 => { state.pc = target_pc.as_offset_int(); } Register::Value(Value::Null) => { state.pc += 1; } _ => { state.pc += 1; } } Ok(InsnFunctionStepResult::Step) } fn handle_text_sum( acc: &mut Value, sum_state: &mut SumAggState, parsed_number: ParsedNumber, parse_result: NumericParseResult, force_approx: bool, ) { // SQLite treats text that only partially parses as numeric (ValidPrefixOnly) // as approximate, so SUM returns real instead of integer. Non-integer inputs // (e.g. BLOB) should also force approximate results. let is_approx = force_approx || matches!(parse_result, NumericParseResult::ValidPrefixOnly); match parsed_number { ParsedNumber::Integer(i) => { if is_approx { sum_state.approx = true; match acc { Value::Null => { *acc = Value::from_f64(i as f64); } Value::Numeric(Numeric::Integer(acc_i)) => { *acc = Value::from_f64(*acc_i as f64); apply_kbn_step_int(acc, i, sum_state); } Value::Numeric(Numeric::Float(_)) => { apply_kbn_step_int(acc, i, sum_state); } _ => unreachable!(), } } else { match acc { Value::Null => { *acc = Value::from_i64(i); } Value::Numeric(Numeric::Integer(acc_i)) => match acc_i.checked_add(i) { Some(sum) => *acc = Value::from_i64(sum), None => { let acc_f = *acc_i as f64; *acc = Value::from_f64(acc_f); sum_state.approx = true; sum_state.ovrfl = true; apply_kbn_step_int(acc, i, sum_state); } }, Value::Numeric(Numeric::Float(_)) => { apply_kbn_step_int(acc, i, sum_state); } _ => unreachable!(), } } } ParsedNumber::Float(f) => { if !sum_state.approx { if let Value::Numeric(Numeric::Integer(current_sum)) = *acc { *acc = Value::from_f64(current_sum as f64); } else if matches!(*acc, Value::Null) { *acc = Value::from_f64(0.0); } sum_state.approx = true; } apply_kbn_step(acc, f, sum_state); } ParsedNumber::None => { if !sum_state.approx { if let Value::Numeric(Numeric::Integer(current_sum)) = *acc { *acc = Value::from_f64(current_sum as f64); } else if matches!(*acc, Value::Null) { *acc = Value::from_f64(0.0); } sum_state.approx = true; } } } } pub fn op_fk_counter( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( FkCounter { increment_value, deferred, }, insn ); if !*deferred { state.increment_fk_immediate_violations_during_stmt(*increment_value); } else { // Transaction-level counter: add/subtract for deferred FKs. program .connection .increment_deferred_foreign_key_violations(*increment_value); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_fk_if_zero( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( FkIfZero { deferred, target_pc, }, insn ); let fk_enabled = program.connection.foreign_keys_enabled(); // Jump if any: // Foreign keys are disabled globally // p1 is true AND deferred constraint counter is zero // p1 is false AND deferred constraint counter is non-zero if !fk_enabled { state.pc = target_pc.as_offset_int(); return Ok(InsnFunctionStepResult::Step); } let v = if *deferred { program.connection.get_deferred_foreign_key_violations() } else { state.get_fk_immediate_violations_during_stmt() }; state.pc = if v == 0 { target_pc.as_offset_int() } else { state.pc + 1 }; Ok(InsnFunctionStepResult::Step) } pub fn op_fk_check( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(FkCheck { deferred }, insn); if !program.connection.foreign_keys_enabled() { state.pc += 1; return Ok(InsnFunctionStepResult::Step); } let v = if *deferred { program.connection.get_deferred_foreign_key_violations() } else { state.get_fk_immediate_violations_during_stmt() }; if v > 0 { return Err(LimboError::ForeignKeyConstraint( "immediate foreign key constraint failed".to_string(), )); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_hash_build( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!(HashBuild { data }, insn); let mut op_state = state .op_hash_build_state .take() .filter(|s| { s.hash_table_id == data.hash_table_id && s.cursor_id == data.cursor_id && s.key_start_reg == data.key_start_reg && s.num_keys == data.num_keys }) .unwrap_or_else(|| OpHashBuildState { key_values: Vec::with_capacity(data.num_keys), key_idx: 0, payload_values: Vec::with_capacity(data.num_payload), payload_idx: 0, rowid: None, cursor_id: data.cursor_id, hash_table_id: data.hash_table_id, key_start_reg: data.key_start_reg, num_keys: data.num_keys, }); // Create hash table if it doesn't exist yet let temp_store = program.connection.get_temp_store(); // When temp_store=memory, disable the memory limit entirely to avoid spilling. // Spilling to an in-memory file has serialization overhead - simpler to never spill. let mem_budget = if matches!(temp_store, crate::TempStore::Memory) { usize::MAX } else { data.mem_budget }; state .hash_tables .entry(data.hash_table_id) .or_insert_with(|| { let config = HashTableConfig { initial_buckets: 1024, mem_budget, num_keys: data.num_keys, collations: data.collations.clone(), temp_store, track_matched: data.track_matched, partition_count: None, }; HashTable::new(config, pager.io.clone()) }); // Read pre-computed key values directly from registers while op_state.key_idx < data.num_keys { let i = op_state.key_idx; let reg = &state.registers[data.key_start_reg + i]; let value = reg.get_value().clone(); op_state.key_values.push(value); op_state.key_idx += 1; } // Read payload values from registers if provided if let Some(payload_reg) = data.payload_start_reg { while op_state.payload_idx < data.num_payload { let i = op_state.payload_idx; let reg = &state.registers[payload_reg + i]; let value = reg.get_value().clone(); op_state.payload_values.push(value); op_state.payload_idx += 1; } } // Get the rowid from the cursor if op_state.rowid.is_none() { let cursor = state.get_cursor(data.cursor_id); let rowid_val = match cursor { Cursor::BTree(btree_cursor) => { let rowid_opt = match btree_cursor.rowid() { Ok(IOResult::Done(v)) => v, Ok(IOResult::IO(io)) => { state.op_hash_build_state = Some(op_state); return Ok(InsnFunctionStepResult::IO(io)); } Err(e) => { state.op_hash_build_state = Some(op_state); return Err(e); } }; rowid_opt.ok_or_else(|| { LimboError::InternalError("HashBuild: cursor has no rowid".to_string()) })? } _ => { return Err(LimboError::InternalError( "HashBuild: unsupported cursor type".to_string(), )); } }; op_state.rowid = Some(rowid_val); } // Insert the rowid into the hash table if let Some(ht) = state.hash_tables.get_mut(&data.hash_table_id) { let rowid = op_state.rowid.expect("rowid set"); let pending = PendingHashInsert { key_values: std::mem::take(&mut op_state.key_values), rowid, payload_values: std::mem::take(&mut op_state.payload_values), }; match ht.insert_pending(pending, Some(&mut state.metrics.hash_join))? { HashInsertResult::Done => {} HashInsertResult::IO { io, pending } => { op_state.key_values = pending.key_values; op_state.payload_values = pending.payload_values; state.op_hash_build_state = Some(op_state); return Ok(InsnFunctionStepResult::IO(io)); } } } state.op_hash_build_state = None; state.metrics.rows_read = state.metrics.rows_read.saturating_add(1); state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_hash_distinct( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { load_insn!(HashDistinct { data }, insn); let temp_store = program.connection.get_temp_store(); // When temp_store=memory, disable the memory limit entirely to avoid spilling. let mem_budget = if matches!(temp_store, crate::TempStore::Memory) { usize::MAX } else { DEFAULT_MEM_BUDGET }; let hash_table = state .hash_tables .entry(data.hash_table_id) .or_insert_with(|| { let config = HashTableConfig { initial_buckets: 1024, mem_budget, num_keys: data.num_keys, collations: data.collations.clone(), temp_store, track_matched: false, partition_count: None, }; HashTable::new(config, pager.io.clone()) }); let key_values = &mut state.distinct_key_values; key_values.clear(); for i in 0..data.num_keys { let reg = &state.registers[data.key_start_reg + i]; key_values.push(reg.get_value().clone()); } let mut key_refs: SmallVec<[ValueRef; 2]> = SmallVec::with_capacity(data.num_keys); key_refs.extend(key_values.iter().map(|v| v.as_ref())); match hash_table.insert_distinct(key_values, &key_refs, Some(&mut state.metrics.hash_join))? { IOResult::Done(inserted) => { state.pc = if inserted { state.pc + 1 } else { data.target_pc.as_offset_int() }; Ok(InsnFunctionStepResult::Step) } IOResult::IO(io) => Ok(InsnFunctionStepResult::IO(io)), } } pub fn op_hash_build_finalize( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(HashBuildFinalize { hash_table_id }, insn); if let Some(ht) = state.hash_tables.get_mut(hash_table_id) { // Finalize the build phase, may flush remaining partitions to disk if spilled match ht.finalize_build(Some(&mut state.metrics.hash_join))? { crate::types::IOResult::Done(()) => {} crate::types::IOResult::IO(io) => { return Ok(InsnFunctionStepResult::IO(io)); } } } state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// Write payload values from a hash entry to registers. fn write_hash_payload_to_registers( registers: &mut [Register], entry: &HashEntry, payload_dest_reg: Option, num_payload: usize, ) { if let Some(dest_reg) = payload_dest_reg { for (i, value) in entry.payload_values.iter().take(num_payload).enumerate() { registers[dest_reg + i].set_value(value.clone()); } } } pub fn op_hash_probe( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( HashProbe { hash_table_id, key_start_reg, num_keys, dest_reg, target_pc, payload_dest_reg, num_payload, probe_rowid_reg, }, insn ); let hash_table_id = *hash_table_id as usize; let key_start_reg = *key_start_reg as usize; let num_keys = *num_keys as usize; let dest_reg = *dest_reg as usize; let payload_dest_reg = payload_dest_reg.map(|r| r as usize); let num_payload = *num_payload as usize; let probe_rowid_reg = probe_rowid_reg.map(|r| r as usize); let (probe_keys, partition_idx, probe_buffered) = if let Some(op_state) = state.op_hash_probe_state.take() { if op_state.hash_table_id == hash_table_id { ( op_state.probe_keys, Some(op_state.partition_idx), op_state.probe_buffered, ) } else { // Different hash table, read fresh keys let mut keys = Vec::with_capacity(num_keys); for i in 0..num_keys { let reg = &state.registers[key_start_reg + i]; keys.push(reg.get_value().clone()); } (keys, None, false) } } else { // First entry, read probe keys from registers let mut keys = Vec::with_capacity(num_keys); for i in 0..num_keys { let reg = &state.registers[key_start_reg + i]; keys.push(reg.get_value().clone()); } (keys, None, false) }; let Some(hash_table) = state.hash_tables.get_mut(&hash_table_id) else { // Empty build side: treat as no match and jump to target. state.op_hash_probe_state = None; state.pc = target_pc.as_offset_int(); return Ok(InsnFunctionStepResult::Step); }; // For spilled hash tables, either buffer main-loop probe rows for grace // processing or probe a partition that grace logic already loaded. if hash_table.has_spilled() { let partition_idx = partition_idx.unwrap_or_else(|| hash_table.partition_for_keys(&probe_keys)); // Main probe loop: buffer probe rows targeting spilled build partitions. if let Some(rowid_reg) = probe_rowid_reg { if probe_buffered { state.pc = target_pc.as_offset_int(); return Ok(InsnFunctionStepResult::Step); } if !hash_table.is_partition_loaded(partition_idx) { // Partition is on disk: buffer this probe row for grace processing let probe_rowid = match state.registers[rowid_reg].get_value() { Value::Numeric(Numeric::Integer(i)) => *i, _ => 0, }; match hash_table.buffer_probe_row( probe_keys, probe_rowid, Some(&mut state.metrics.hash_join), )? { IOResult::Done(()) => {} IOResult::IO(io) => { state.op_hash_probe_state = Some(OpHashProbeState { probe_keys: Vec::new(), // keys consumed hash_table_id, partition_idx, probe_buffered: true, }); return Ok(InsnFunctionStepResult::IO(io)); } } // Jump to target_pc: this row is deferred to grace processing. state.pc = target_pc.as_offset_int(); return Ok(InsnFunctionStepResult::Step); } // Partition is in memory: probe immediately (fast path) state.metrics.hash_join.grace_probe_rows_streamed = state .metrics .hash_join .grace_probe_rows_streamed .saturating_add(1); } else if unlikely(!hash_table.is_partition_loaded(partition_idx)) { return Err(LimboError::InternalError(format!( "HashProbe reached spilled partition {partition_idx} without a preloaded build partition; probe_rowid_reg=None is grace-only" ))); } // Probe the loaded partition match hash_table.probe_partition( partition_idx, &probe_keys, Some(&mut state.metrics.hash_join), ) { Some(entry) => { state.registers[dest_reg].set_int(entry.rowid); write_hash_payload_to_registers( &mut state.registers, entry, payload_dest_reg, num_payload, ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } None => { state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } } } else { // Non-spilled hash table, use normal probe match hash_table.probe(probe_keys, Some(&mut state.metrics.hash_join)) { Some(entry) => { state.registers[dest_reg].set_int(entry.rowid); write_hash_payload_to_registers( &mut state.registers, entry, payload_dest_reg, num_payload, ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } None => { state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } } } } pub fn op_hash_next( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( HashNext { hash_table_id, dest_reg, target_pc, payload_dest_reg, num_payload, }, insn ); let hash_table = state.hash_tables.get_mut(hash_table_id).ok_or_else(|| { LimboError::InternalError(format!("Hash table not found with ID: {hash_table_id}")) })?; match hash_table.next_match() { Some(entry) => { state.registers[*dest_reg].set_int(entry.rowid); write_hash_payload_to_registers( &mut state.registers, entry, *payload_dest_reg, *num_payload, ); state.pc += 1; Ok(InsnFunctionStepResult::Step) } None => { state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } } } pub fn op_hash_close( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(HashClose { hash_table_id }, insn); if let Some(mut hash_table) = state.hash_tables.remove(hash_table_id) { hash_table.close(); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_hash_clear( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(HashClear { hash_table_id }, insn); if let Some(hash_table) = state.hash_tables.get_mut(hash_table_id) { hash_table.clear(); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_hash_reset_matched( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(HashResetMatched { hash_table_id }, insn); if let Some(hash_table) = state.hash_tables.get_mut(hash_table_id) { hash_table.reset_matched_bits(); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_hash_mark_matched( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(HashMarkMatched { hash_table_id }, insn); if let Some(hash_table) = state.hash_tables.get_mut(hash_table_id) { hash_table.mark_current_matched(); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_hash_scan_unmatched( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( HashScanUnmatched { hash_table_id, dest_reg, target_pc, payload_dest_reg, num_payload, }, insn ); let Some(hash_table) = state.hash_tables.get_mut(hash_table_id) else { state.pc = target_pc.as_offset_int(); return Ok(InsnFunctionStepResult::Step); }; hash_table.begin_unmatched_scan(); advance_unmatched_scan( hash_table, &mut state.registers, &mut state.pc, *dest_reg, target_pc.as_offset_int(), *payload_dest_reg, *num_payload, &mut state.metrics.hash_join, ) } pub fn op_hash_next_unmatched( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( HashNextUnmatched { hash_table_id, dest_reg, target_pc, payload_dest_reg, num_payload, }, insn ); let hash_table = state.hash_tables.get_mut(hash_table_id).ok_or_else(|| { LimboError::InternalError(format!("Hash table not found with ID: {hash_table_id}")) })?; advance_unmatched_scan( hash_table, &mut state.registers, &mut state.pc, *dest_reg, target_pc.as_offset_int(), *payload_dest_reg, *num_payload, &mut state.metrics.hash_join, ) } /// Shared logic for HashScanUnmatched/HashNextUnmatched: find the next unmatched /// entry, loading spilled partitions as needed. #[allow(clippy::too_many_arguments)] fn advance_unmatched_scan( hash_table: &mut HashTable, registers: &mut [Register], pc: &mut u32, dest_reg: usize, target_pc: u32, payload_dest_reg: Option, num_payload: usize, metrics: &mut HashJoinMetrics, ) -> Result { if hash_table.has_spilled() { if let Some(partition_idx) = hash_table.unmatched_scan_current_partition() { if !hash_table.is_partition_loaded(partition_idx) { match hash_table.load_spilled_partition(partition_idx, Some(metrics))? { crate::types::IOResult::Done(()) => {} crate::types::IOResult::IO(io) => { return Ok(InsnFunctionStepResult::IO(io)); } } } } } loop { match hash_table.next_unmatched() { Some(entry) => { registers[dest_reg].set_int(entry.rowid); write_hash_payload_to_registers(registers, entry, payload_dest_reg, num_payload); *pc += 1; return Ok(InsnFunctionStepResult::Step); } None => { if hash_table.has_spilled() { if let Some(partition_idx) = hash_table.unmatched_scan_current_partition() { if !hash_table.is_partition_loaded(partition_idx) { match hash_table.load_spilled_partition(partition_idx, Some(metrics))? { crate::types::IOResult::Done(()) => continue, crate::types::IOResult::IO(io) => { return Ok(InsnFunctionStepResult::IO(io)); } } } } } *pc = target_pc; return Ok(InsnFunctionStepResult::Step); } } } } pub fn op_hash_grace_init( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( HashGraceInit { hash_table_id, target_pc, }, insn ); let hash_table_id = *hash_table_id as usize; let Some(hash_table) = state.hash_tables.get_mut(&hash_table_id) else { state.pc = target_pc.as_offset_int(); return Ok(InsnFunctionStepResult::Step); }; // If no spilling occurred, skip grace processing if !hash_table.has_grace_partitions() { state.pc = target_pc.as_offset_int(); return Ok(InsnFunctionStepResult::Step); } // Finalize probe spill match hash_table.finalize_probe_spill(Some(&mut state.metrics.hash_join))? { IOResult::Done(()) => {} IOResult::IO(io) => { return Ok(InsnFunctionStepResult::IO(io)); } } // Initialize grace processing if !hash_table.grace_begin() { state.pc = target_pc.as_offset_int(); return Ok(InsnFunctionStepResult::Step); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } pub fn op_hash_grace_load_partition( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( HashGraceLoadPartition { hash_table_id, target_pc, }, insn ); let hash_table_id = *hash_table_id as usize; let hash_table = state.hash_tables.get_mut(&hash_table_id).ok_or_else(|| { LimboError::InternalError(format!("Hash table not found with ID: {hash_table_id}")) })?; match hash_table.grace_load_current_partition(Some(&mut state.metrics.hash_join))? { IOResult::Done(true) => { state.pc += 1; Ok(InsnFunctionStepResult::Step) } IOResult::Done(false) => { state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } IOResult::IO(io) => Ok(InsnFunctionStepResult::IO(io)), } } pub fn op_hash_grace_next_probe( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( HashGraceNextProbe { hash_table_id, key_start_reg, num_keys, probe_rowid_dest, target_pc, }, insn ); let hash_table_id = *hash_table_id as usize; let key_start_reg = *key_start_reg as usize; let num_keys = *num_keys as usize; let probe_rowid_dest = *probe_rowid_dest as usize; let hash_table = state.hash_tables.get_mut(&hash_table_id).ok_or_else(|| { mark_unlikely(); LimboError::InternalError(format!("Hash table not found with ID: {hash_table_id}")) })?; match hash_table.grace_next_probe_entry()? { IOResult::Done(Some(entry)) => { // Write probe keys to registers for (i, value) in entry.key_values.into_iter().enumerate() { if i < num_keys { state.registers[key_start_reg + i].set_value(value); } } // Write probe rowid state.registers[probe_rowid_dest].set_int(entry.probe_rowid); state.pc += 1; Ok(InsnFunctionStepResult::Step) } IOResult::Done(None) => { state.pc = target_pc.as_offset_int(); Ok(InsnFunctionStepResult::Step) } IOResult::IO(io) => Ok(InsnFunctionStepResult::IO(io)), } } pub fn op_hash_grace_advance_partition( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( HashGraceAdvancePartition { hash_table_id, target_pc, }, insn ); let hash_table_id = *hash_table_id as usize; let hash_table = state.hash_tables.get_mut(&hash_table_id).ok_or_else(|| { mark_unlikely(); LimboError::InternalError(format!("Hash table not found with ID: {hash_table_id}")) })?; if hash_table.grace_advance_partition() { state.pc += 1; } else { state.pc = target_pc.as_offset_int(); } Ok(InsnFunctionStepResult::Step) } fn apply_affinity_char(target: &mut Register, affinity: Affinity) -> bool { if let Register::Value(value) = target { if matches!(value, Value::Blob(_)) { return true; } match affinity { Affinity::Blob => return true, Affinity::Text => { if matches!(value, Value::Text(_) | Value::Null) { return true; } let text = value.to_string(); *value = Value::Text(text.into()); return true; } Affinity::Integer | Affinity::Numeric => { if matches!(value, Value::Numeric(Numeric::Integer(_))) { return true; } if !matches!(value, Value::Text(_) | Value::Numeric(Numeric::Float(_))) { return true; } if let Value::Numeric(Numeric::Float(fl)) = *value { // For floats, try to convert to integer if it's exact // This is similar to sqlite3VdbeIntegerAffinity return try_float_to_integer_affinity(value, f64::from(fl)); } if let Value::Text(t) = value { let text = trim_ascii_whitespace(t.as_str()); // Handle hex numbers - they shouldn't be converted if text.starts_with("0x") { return false; } let (parse_result, parsed_value) = try_for_float(text.as_bytes()); let num = match parse_result { NumericParseResult::NotNumeric | NumericParseResult::ValidPrefixOnly => { return false; } NumericParseResult::PureInteger | NumericParseResult::HasDecimalOrExp => { match parsed_value { ParsedNumber::Integer(i) => Value::from_i64(i), ParsedNumber::Float(f) => Value::from_f64(f), ParsedNumber::None => return false, } } }; match num { Value::Numeric(Numeric::Integer(i)) => { *value = Value::from_i64(i); return true; } Value::Numeric(Numeric::Float(fl)) => { // For both Numeric and Integer affinity, try to convert // float to int if exact. SQLite treats INTEGER identically // to NUMERIC here: both enter applyNumericAffinity() with // bTryForInt=1 (sqlite/src/vdbe.c:403-408). return try_float_to_integer_affinity(value, f64::from(fl)); } other => { *value = other; return true; } } } return false; } Affinity::Real => match value { Value::Numeric(Numeric::Integer(i)) => { *value = Value::from_f64(*i as f64); return true; } Value::Numeric(Numeric::Float(_)) | Value::Null => { return true; } Value::Text(t) => { let text = trim_ascii_whitespace(t.as_str()); if text.starts_with("0x") { return false; } let (parse_result, parsed_value) = try_for_float(text.as_bytes()); let coerced = match parse_result { NumericParseResult::NotNumeric | NumericParseResult::ValidPrefixOnly => { return false; } NumericParseResult::PureInteger | NumericParseResult::HasDecimalOrExp => { match parsed_value { ParsedNumber::Integer(i) => Value::from_f64(i as f64), ParsedNumber::Float(f) => Value::from_f64(f), ParsedNumber::None => return false, } } }; *value = coerced; return true; } _ => return true, }, } } true } fn try_float_to_integer_affinity(value: &mut Value, fl: f64) -> bool { // Check if the float can be exactly represented as an integer if let Ok(int_val) = cast_real_to_integer(fl) { // Additional check: ensure round-trip conversion is exact // and value is within safe bounds (similar to SQLite's checks) if (int_val as f64) == fl && int_val > i64::MIN + 1 && int_val < i64::MAX - 1 { *value = Value::from_i64(int_val); return true; } } // If we can't convert to exact integer, keep as float for Numeric affinity // but return false to indicate the conversion wasn't "complete" *value = Value::from_f64(fl); false } fn parse_test_uint(reg: &Register) -> Result> { match reg.get_value() { Value::Null => Ok(None), Value::Numeric(Numeric::Integer(i)) => { if *i < 0 { Err(LimboError::InternalError( "test_uint: negative value".to_string(), )) } else { Ok(Some(*i as u64)) } } Value::Text(t) => { let s = t.to_string(); let v = s .parse::() .map_err(|_| LimboError::InternalError(format!("test_uint: invalid uint: {s}")))?; Ok(Some(v)) } _ => Err(LimboError::InternalError( "test_uint: unsupported type".to_string(), )), } } // Compat for applications that test for SQLite. fn execute_sqlite_version() -> String { "3.50.4".to_string() } fn execute_turso_version(version_integer: i64) -> String { let major = version_integer / 1_000_000; let minor = (version_integer % 1_000_000) / 1_000; let release = version_integer % 1_000; format!("{major}.{minor}.{release}") } pub fn extract_int_value(value: V) -> i64 { let value = value.as_value_ref(); match value { ValueRef::Numeric(Numeric::Integer(i)) => i, ValueRef::Numeric(Numeric::Float(f)) => { let f = f64::from(f); // Use sqlite3RealToI64 equivalent if f < -9223372036854774784.0 { i64::MIN } else if f > 9223372036854774784.0 { i64::MAX } else { f as i64 } } ValueRef::Text(t) => { // Try to parse as integer, return 0 if failed t.as_str().parse::().unwrap_or(0) } ValueRef::Blob(b) => { // Try to parse blob as string then as integer if let Ok(s) = std::str::from_utf8(b) { s.parse::().unwrap_or(0) } else { 0 } } ValueRef::Null => 0, } } pub fn op_max_pgcnt( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!(MaxPgcnt { db, dest, new_max }, insn); let pager = program.get_pager_from_database_index(db); let result_value = if *new_max == 0 { // If new_max is 0, just return current maximum without changing it pager.get_max_page_count() } else { // Set new maximum page count (will be clamped to current database size) return_if_io!(pager.set_max_page_count(*new_max as u32)) }; state.registers[*dest].set_int(result_value.into()); state.pc += 1; Ok(InsnFunctionStepResult::Step) } /// State machine for PRAGMA journal_mode changes #[derive(Debug, Clone, Copy, Default)] pub enum OpJournalModeSubState { /// Initial state - read header to get current mode #[default] Start, /// Checkpointing WAL/MVCC before mode change Checkpoint, /// Update the header with new version and get page reference UpdateHeader, /// Write page 1 to disk WritePage, /// Finalize - clear cache and setup new mode Finalize, } /// Holds the state for the journal mode change operation #[derive(Default)] pub struct OpJournalModeState { pub sub_state: OpJournalModeSubState, /// The previous journal mode (before the change) pub prev_mode: Option, /// The new journal mode we're changing to pub new_mode: Option, /// Checkpoint state machine for MVCC mode pub checkpoint_sm: Option>>, /// Page reference for writing header pub page_ref: Option, } pub fn op_journal_mode( program: &Program, state: &mut ProgramState, insn: &Insn, pager: &Arc, ) -> Result { match op_journal_mode_inner(program, state, insn, pager) { Ok(result) => { if !matches!(result, InsnFunctionStepResult::IO(_)) { // Reset state if we are done with this instruction state.op_journal_mode_state = Default::default(); } Ok(result) } Err(err) => { // Reset state on error state.op_journal_mode_state = Default::default(); Err(err) } } } fn op_journal_mode_inner( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { use crate::storage::sqlite3_ondisk::begin_write_btree_page; load_insn!(JournalMode { db, dest, new_mode }, insn); let pager = program.get_pager_from_database_index(db); let pager = &pager; loop { match state.op_journal_mode_state.sub_state { OpJournalModeSubState::Start => { // Read header to get current mode let mv_store = program.connection.mv_store_for_db(*db); let header_result = with_header(pager, mv_store.as_ref(), program, *db, |header| { header.read_version }); let prev_mode_raw = return_if_io!(header_result); let prev_mode_version = prev_mode_raw .to_version() .map_err(|val| LimboError::Corrupt(format!("Invalid read_version: {val}")))?; let prev_mode = journal_mode::JournalMode::from(prev_mode_version); state.op_journal_mode_state.prev_mode = Some(prev_mode); // If no new mode specified, just return current mode let Some(mode_str) = new_mode else { let ret: &'static str = prev_mode.into(); state.registers[*dest].set_text(Text::new(ret)); state.pc += 1; return Ok(InsnFunctionStepResult::Step); }; // Parse the new mode. If unknown or unsupported, silently return // current mode (matches SQLite behavior). let new_mode = match journal_mode::JournalMode::from_str(mode_str.as_str()) { Ok(mode) if mode.supported() => mode, _ => { let ret: &'static str = prev_mode.into(); state.registers[*dest].set_text(Text::new(ret)); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } }; // If same mode, just return if prev_mode == new_mode { let ret: &'static str = new_mode.into(); state.registers[*dest].set_text(Text::new(ret)); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } // Check if database is readonly - cannot change journal mode on readonly databases if program.connection.is_readonly(*db) { return Err(LimboError::ReadOnly); } state.op_journal_mode_state.new_mode = Some(new_mode); state.op_journal_mode_state.sub_state = OpJournalModeSubState::Checkpoint; } OpJournalModeSubState::Checkpoint => { // Checkpoint WAL or MVCC before changing mode let mv_store = program.connection.mv_store_for_db(*db); if let Some(mv_store) = mv_store.as_ref() { // MVCC checkpoint using state machine if state.op_journal_mode_state.checkpoint_sm.is_none() { state.op_journal_mode_state.checkpoint_sm = Some(StateMachine::new(CheckpointStateMachine::new( pager.clone(), mv_store.clone(), program.connection.clone(), true, program.connection.get_sync_mode(), ))); } let ckpt_sm = state.op_journal_mode_state.checkpoint_sm.as_mut().unwrap(); return_if_io!(ckpt_sm.step(&())); state.op_journal_mode_state.checkpoint_sm = None; state.op_journal_mode_state.sub_state = OpJournalModeSubState::UpdateHeader; } else { // WAL checkpoint let checkpoint_result = pager.checkpoint( CheckpointMode::Truncate { upper_bound_inclusive: None, }, program.connection.get_sync_mode(), false, // Don't clear cache yet, we'll do it in Finalize ); return_if_io!(checkpoint_result); state.op_journal_mode_state.sub_state = OpJournalModeSubState::UpdateHeader; } } OpJournalModeSubState::UpdateHeader => { let new_mode = state .op_journal_mode_state .new_mode .expect("new_mode should be set"); let new_version = new_mode .as_version() .expect("Should be a supported Journal Mode"); let raw_version = RawVersion::from(new_version); // Get the header page reference (handles both initialized and uninitialized databases) // This uses the pager's cache and won't fail for empty database files let header_ref = return_if_io!(crate::storage::pager::HeaderRefMut::from_pager(pager)); // Update the header version { let header = header_ref.borrow_mut(); header.read_version = raw_version; header.write_version = raw_version; } // Save the page reference for writing state.op_journal_mode_state.page_ref = Some(header_ref.page().clone()); // Skip ReadPage and go directly to WritePage state.op_journal_mode_state.sub_state = OpJournalModeSubState::WritePage; } OpJournalModeSubState::WritePage => { // Write page 1 to disk to flush the header let page = state .op_journal_mode_state .page_ref .as_ref() .expect("page_ref should be set"); let completion = begin_write_btree_page(pager, page)?; state.op_journal_mode_state.sub_state = OpJournalModeSubState::Finalize; return Ok(InsnFunctionStepResult::IO(IOCompletions::Single( completion, ))); } OpJournalModeSubState::Finalize => { let new_mode = state .op_journal_mode_state .new_mode .expect("new_mode should be set"); // Clear page cache pager.clear_page_cache(true); // Setup new mode if matches!(new_mode, journal_mode::JournalMode::Mvcc) { if program.connection.get_capture_data_changes_info().is_some() { return Err(LimboError::InternalError( "cannot enable MVCC while CDC is active".to_string(), )); } let db_path = program.connection.get_database_canonical_path(); // todo(v): pass required encryption ctx to enable encryption with mvcc let mv_store = journal_mode::open_mv_store( pager.io.clone(), &db_path, program.connection.db.open_flags, program.connection.db.durable_storage.clone(), None, )?; program.connection.db.mv_store.store(Some(mv_store.clone())); program.connection.demote_to_mvcc_connection(); mv_store.bootstrap(program.connection.clone())?; } if matches!(new_mode, journal_mode::JournalMode::Wal) { program.connection.db.mv_store.store(None); } // Return result let ret: &'static str = new_mode.into(); state.registers[*dest].set_text(Text::new(ret)); state.pc += 1; return Ok(InsnFunctionStepResult::Step); } } } } pub fn op_filter( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( Filter { cursor_id, target_pc, key_reg, num_keys }, insn ); let Some(filter) = state.get_bloom_filter(*cursor_id) else { // always safe to fall though, no filter present state.pc += 1; return Ok(InsnFunctionStepResult::Step); }; let contains = if *num_keys == 1 { // Single key optimization, avoid allocating a Vec let value = state.registers[*key_reg].get_value(); if matches!(value, Value::Null) { // its always safe to fall through, so this *should* be `true` but // since it's always an equality predicate and we have a NULL value, // we can just short-circuit to false here. false } else { filter.contains_value(value) } } else { let values: Vec<&Value> = (0..*num_keys) .map(|i| state.registers[*key_reg + i].get_value()) .collect(); if values.iter().any(|v| matches!(*v, Value::Null)) { false } else { filter.contains_values(&values) } }; if !contains { state.pc = target_pc.as_offset_int(); } else { state.pc += 1; } Ok(InsnFunctionStepResult::Step) } pub fn op_filter_add( _program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { load_insn!( FilterAdd { cursor_id, key_reg, num_keys }, insn ); if *num_keys == 1 { let reg: *const Register = &state.registers[*key_reg]; let value = unsafe { &*reg }.get_value(); let filter = state.get_or_create_bloom_filter(*cursor_id); filter.insert_value(value); } else { let values: Vec = (0..*num_keys) .map(|i| state.registers[*key_reg + i].get_value().clone()) .collect(); let filter = state.get_or_create_bloom_filter(*cursor_id); let value_refs: Vec<&Value> = values.iter().collect(); filter.insert_values(&value_refs); } state.pc += 1; Ok(InsnFunctionStepResult::Step) } fn extract_pragma_int(rows: &[Vec], pragma_name: &str) -> Result where T: TryFrom, { rows.first() .and_then(|row| row.first()) .and_then(|v| match v { Value::Numeric(Numeric::Integer(i)) => T::try_from(*i).ok(), _ => None, }) .ok_or_else(|| { LimboError::InternalError(format!("failed to read {pragma_name} from source")) }) } /// Sub-states for the VACUUM INTO operation state machine. #[derive(Default)] pub(crate) enum OpVacuumIntoSubState { /// Initial state - validate preconditions and create destination database #[default] Init, /// Step through schema query to collect rows CollectSchemaRows { dest_conn: Arc, schema_stmt: Box, }, /// Prepare CREATE statement on destination (idx into schema_rows) PrepareDestSchema { dest_conn: Arc, idx: usize, }, /// Step through CREATE statement on destination (async) StepDestSchema { dest_conn: Arc, dest_schema_stmt: Box, idx: usize, }, /// Start copying a table - prepare column info query StartCopyTable { dest_conn: Arc, table_idx: usize, }, /// Collect column info for current table CollectColumnInfo { dest_conn: Arc, column_stmt: Box, table_idx: usize, }, /// Select rows from source table and insert into destination CopyRows { dest_conn: Arc, select_stmt: Box, dest_insert_stmt: Box, table_idx: usize, }, /// Step through INSERT statement on destination (async) StepDestInsert { dest_conn: Arc, select_stmt: Box, dest_insert_stmt: Box, table_idx: usize, }, /// Copy meta values (user_version, application_id) from source to destination CopyMetaValues { dest_conn: Arc }, /// Create triggers and views after data copy (to avoid triggers firing during copy) PrepareTriggersViews { dest_conn: Arc, idx: usize, }, /// Step through CREATE TRIGGER/VIEW statement on destination StepTriggersViews { dest_conn: Arc, dest_schema_stmt: Box, idx: usize, }, /// Operation complete Done { dest_conn: Arc }, } /// Holds the state for the VACUUM INTO operation. #[derive(Default)] pub(crate) struct OpVacuumIntoState { sub_state: OpVacuumIntoSubState, /// Keep dest_db alive while vacuum is in progress. #[allow(dead_code)] dest_db: Option>, /// Schema rows: [(type, name, tbl_name, sql), ...] schema_rows: Vec>, /// Names of tables to copy data for table_names: Vec, /// Column names for the current table being copied current_table_columns: Vec, /// Meta values read from source database header source_user_version: i32, source_application_id: i32, } /// VACUUM INTO - create a compacted copy of the database at the specified path. /// /// This is an async state machine implementation that yields on I/O operations. /// It: /// 1. Creates a new database at the destination path with matching page_size /// 2. Queries sqlite_schema for all schema objects (tables, indexes, triggers, views) /// 3. Creates tables and indexes in destination (skipping sqlite_sequence - it's /// auto-created when AUTOINCREMENT tables are created, see translate/schema.rs) /// 4. Copies data for each table, including sqlite_sequence to preserve AUTOINCREMENT counters /// 5. Copies meta values (user_version, application_id) from source to destination /// 6. Creates triggers and views last (after data copy to avoid triggers firing during copy) pub fn op_vacuum_into( program: &Program, state: &mut ProgramState, insn: &Insn, _pager: &Arc, ) -> Result { match op_vacuum_into_inner(program, state, insn) { Ok(InsnFunctionStepResult::Step) => { // Instruction complete, reset state state.op_vacuum_into_state = None; Ok(InsnFunctionStepResult::Step) } Ok(InsnFunctionStepResult::IO(io)) => { // Waiting for I/O, keep state for resumption Ok(InsnFunctionStepResult::IO(io)) } Ok(InsnFunctionStepResult::Done | InsnFunctionStepResult::Row) => { unreachable!("op_vacuum_into_inner only returns Step or IO") } Err(err) => { // Reset state on error state.op_vacuum_into_state = None; Err(err) } } } fn op_vacuum_into_inner( program: &Program, state: &mut ProgramState, insn: &Insn, ) -> Result { load_insn!(VacuumInto { dest_path }, insn); let vacuum_state = state .op_vacuum_into_state .get_or_insert_with(OpVacuumIntoState::default); loop { let current_sub_state = std::mem::take(&mut vacuum_state.sub_state); match current_sub_state { OpVacuumIntoSubState::Init => { // Check if we're in a transaction // as vacuum cannot be run inside a transaction if !program.connection.auto_commit.load(Ordering::SeqCst) { return Err(LimboError::TxError( "cannot VACUUM INTO from within a transaction".to_string(), )); } // we always vacuum into a new file, so check if it exists if std::path::Path::new(dest_path).exists() { return Err(LimboError::ParseError(format!( "output file already exists: {dest_path}" ))); } // make sure to create destination database with same experimental features as source // Always use PlatformIO for the destination file, even if source is in-memory. // This ensures VACUUM INTO actually writes to disk. let io: Arc = Arc::new(crate::io::PlatformIO::new()?); let source_db = &program.connection.db; let dest_opts = crate::DatabaseOpts::new() .with_views(source_db.experimental_views_enabled()) .with_index_method(source_db.experimental_index_method_enabled()); program.connection.execute("BEGIN")?; // lets set the same meta values as source db let user_version: i32 = extract_pragma_int( &program.connection.pragma_query("user_version")?, "user_version", )?; let application_id: i32 = extract_pragma_int( &program.connection.pragma_query("application_id")?, "application_id", )?; let page_size: u32 = extract_pragma_int( &program.connection.pragma_query("page_size")?, "page_size", )?; let reserved_space = { let pager = program.connection.pager.load(); let reserved_space: u8 = match program.connection.get_reserved_bytes() { Some(val) => val, None => io.block(|| pager.with_header(|header| header.reserved_space))?, }; reserved_space }; let dest_db = crate::Database::open_file_with_flags( io, dest_path, OpenFlags::Create, dest_opts, None, )?; let dest_conn = dest_db.connect()?; dest_conn.reset_page_size(page_size)?; // set reserved_space on destination to match source // this is important for databases using encryption or checksums // must be set before page 1 is allocated (before any schema operations) dest_conn.set_reserved_bytes(reserved_space)?; // Enable MVCC on destination if source has it enabled // Must be done before any schema operations to ensure the log file is created if program.connection.db.mvcc_enabled() { dest_conn.execute("PRAGMA journal_mode = 'mvcc'")?; } // Performance optimizations for destination database: // 1. Disable fsync - destination is a new file, if crash occurs we just delete it // 2. Disable foreign key checks - source data is already consistent // These match SQLite's vacuum.c optimizations (PAGER_SYNCHRONOUS_OFF, ~SQLITE_ForeignKeys) dest_conn.execute("PRAGMA synchronous = OFF")?; dest_conn.execute("PRAGMA foreign_keys = OFF")?; // Wrap all operations in a single transaction for atomicity and performance. // This batches all writes and ensures destination is either empty or complete. dest_conn.execute("BEGIN")?; // Exclude the MVCC metadata table from the vacuum destination — it is an // internal artifact of mvcc mode and must not appear in a // standalone SQLite file produced by VACUUM INTO. let schema_sql = format!( "SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE sql IS NOT NULL AND name <> '{}' ORDER BY CASE type WHEN 'table' THEN 1 WHEN 'index' THEN 2 WHEN 'trigger' THEN 3 WHEN 'view' THEN 4 ELSE 5 END", crate::mvcc::database::MVCC_META_TABLE_NAME ); let schema_stmt = program.connection.prepare(schema_sql.as_str())?; vacuum_state.dest_db = Some(dest_db); vacuum_state.source_user_version = user_version; vacuum_state.source_application_id = application_id; vacuum_state.sub_state = OpVacuumIntoSubState::CollectSchemaRows { dest_conn, schema_stmt: Box::new(schema_stmt), }; continue; } OpVacuumIntoSubState::CollectSchemaRows { dest_conn, mut schema_stmt, } => { // Collect rows from sqlite_schema query: (type, name, tbl_name, sql) // These define all tables, indexes, triggers, and views to recreate in destination match schema_stmt.step()? { crate::StepResult::Row => { let row = schema_stmt .row() .expect("StepResult::Row but row() returned None"); let values: Vec = row.get_values().cloned().collect(); vacuum_state.schema_rows.push(values); vacuum_state.sub_state = OpVacuumIntoSubState::CollectSchemaRows { dest_conn, schema_stmt, }; continue; } crate::StepResult::Done => { // Extract table names for data copy phase // Include sqlite_sequence for AUTOINCREMENT counters, but not other sqlite_ tables vacuum_state.table_names = vacuum_state .schema_rows .iter() .filter_map(|row| { if row.len() >= 2 { if let (Value::Text(type_val), Value::Text(name_val)) = (&row[0], &row[1]) { let name = name_val.as_str(); if type_val.as_str() == "table" && (!name.starts_with("sqlite_") || name == "sqlite_sequence") && name != crate::mvcc::database::MVCC_META_TABLE_NAME { return Some(name.to_string()); } } } None }) .collect(); vacuum_state.sub_state = OpVacuumIntoSubState::PrepareDestSchema { dest_conn, idx: 0 }; continue; } crate::StepResult::IO => { let io = schema_stmt .take_io_completions() .expect("StepResult::IO returned but no completions available"); vacuum_state.sub_state = OpVacuumIntoSubState::CollectSchemaRows { dest_conn, schema_stmt, }; return Ok(InsnFunctionStepResult::IO(io)); } crate::StepResult::Busy | crate::StepResult::Interrupt => { return Err(LimboError::Busy); } } } OpVacuumIntoSubState::PrepareDestSchema { dest_conn, idx } => { let schema_rows_len = vacuum_state.schema_rows.len(); turso_assert!( idx <= schema_rows_len, "idx incremented past end of schema_rows", { "idx": idx, "schema_rows_len": schema_rows_len } ); if idx == schema_rows_len { // Done creating schema, start copying data vacuum_state.sub_state = OpVacuumIntoSubState::StartCopyTable { dest_conn, table_idx: 0, }; continue; } let row = &vacuum_state.schema_rows[idx]; turso_assert!( row.len() == 4, "schema row should have exactly 4 columns (type, name, tbl_name, sql)", { "row_len": row.len() } ); // Skip triggers and views - they'll be created after data copy // to avoid triggers firing during data copy if let Value::Text(type_val) = &row[0] { let type_str = type_val.as_str(); if type_str == "trigger" || type_str == "view" { vacuum_state.sub_state = OpVacuumIntoSubState::PrepareDestSchema { dest_conn, idx: idx + 1, }; continue; } } // Skip sqlite_sequence in schema creation phase. When we create an AUTOINCREMENT // table, Turso automatically creates sqlite_sequence if it doesn't exist (see // translate/schema.rs). Since schema_rows order depends on sqlite_schema rowids, // an AUTOINCREMENT table may appear before sqlite_sequence. If we create that // table first (which auto-creates sqlite_sequence), then later try to run // "CREATE TABLE sqlite_sequence(name,seq)", it fails with "table already exists". // We still copy sqlite_sequence data in StartCopyTable to preserve counters. if let Value::Text(name_val) = &row[1] { if name_val.as_str() == "sqlite_sequence" { vacuum_state.sub_state = OpVacuumIntoSubState::PrepareDestSchema { dest_conn, idx: idx + 1, }; continue; } } // Query filters WHERE sql IS NOT NULL, so sql column must be text let Value::Text(sql) = &row[3] else { unreachable!("sql column should be text (query has WHERE sql IS NOT NULL)"); }; let sql_str = sql.as_str(); // Internal tables (e.g. __turso_internal_types) have a reserved // name prefix that translate_create_table rejects for user SQL. // Temporarily mark the dest connection as nested during prepare() // so the reserved-name check is bypassed at compile time. We must // NOT keep it nested during step() because that would prevent // sub-statements from upgrading to write transactions. let is_internal = matches!(&row[1], Value::Text(n) if n.as_str().starts_with(crate::schema::TURSO_INTERNAL_PREFIX)); if is_internal { dest_conn.start_nested(); } let dest_stmt = dest_conn.prepare(sql_str); if is_internal { dest_conn.end_nested(); } let dest_stmt = dest_stmt?; vacuum_state.sub_state = OpVacuumIntoSubState::StepDestSchema { dest_conn, dest_schema_stmt: Box::new(dest_stmt), idx, }; continue; } OpVacuumIntoSubState::StepDestSchema { dest_conn, mut dest_schema_stmt, idx, } => match dest_schema_stmt.step()? { crate::StepResult::Row => { unreachable!("CREATE statement unexpectedly returned a row"); } crate::StepResult::Done => { // After creating __turso_internal_types in the dest, load // custom type definitions from the source so that subsequent // CREATE TABLE statements for STRICT tables with custom type // columns can resolve those types. let row = &vacuum_state.schema_rows[idx]; if matches!(&row[1], Value::Text(n) if n.as_str() == crate::schema::TURSO_TYPES_TABLE_NAME) { let source_types: Vec<(String, std::sync::Arc)> = { let source_schema = program.connection.schema.read(); source_schema .type_registry .iter() .filter(|(_, td)| !td.is_builtin) .map(|(name, td)| (name.clone(), td.clone())) .collect() }; dest_conn.with_schema_mut(|dest_schema| { for (name, td) in source_types { dest_schema.type_registry.insert(name, td); } }); } vacuum_state.sub_state = OpVacuumIntoSubState::PrepareDestSchema { dest_conn, idx: idx + 1, }; continue; } crate::StepResult::IO => { let io = dest_schema_stmt .take_io_completions() .expect("StepResult::IO returned but no completions available"); vacuum_state.sub_state = OpVacuumIntoSubState::StepDestSchema { dest_conn, dest_schema_stmt, idx, }; return Ok(InsnFunctionStepResult::IO(io)); } crate::StepResult::Busy | crate::StepResult::Interrupt => { return Err(LimboError::Busy); } }, OpVacuumIntoSubState::StartCopyTable { dest_conn, table_idx, } => { let table_names_len = vacuum_state.table_names.len(); turso_assert!( table_idx <= table_names_len, "table_idx incremented past end of table_names", { "table_idx": table_idx, "table_names_len": table_names_len } ); if table_idx == table_names_len { // Done copying all tables, now copy meta values vacuum_state.sub_state = OpVacuumIntoSubState::CopyMetaValues { dest_conn }; continue; } let table_name = &vacuum_state.table_names[table_idx]; // Escape double quotes in table name for safe SQL let escaped_table_name = table_name.replace('"', "\"\""); let pragma_sql = format!("PRAGMA table_info(\"{escaped_table_name}\")"); let column_stmt = program.connection.prepare(&pragma_sql)?; vacuum_state.current_table_columns.clear(); vacuum_state.sub_state = OpVacuumIntoSubState::CollectColumnInfo { dest_conn, column_stmt: Box::new(column_stmt), table_idx, }; continue; } OpVacuumIntoSubState::CollectColumnInfo { dest_conn, mut column_stmt, table_idx, } => { match column_stmt.step()? { crate::StepResult::Row => { let row = column_stmt .row() .expect("StepResult::Row but row() returned None"); // Column name is at index 1 if let Value::Text(name) = row.get_value(1) { // Escape double quotes in column name for safe SQL let escaped_name = name.as_str().replace('"', "\"\""); let col_name = format!("\"{escaped_name}\""); vacuum_state.current_table_columns.push(col_name); } vacuum_state.sub_state = OpVacuumIntoSubState::CollectColumnInfo { dest_conn, column_stmt, table_idx, }; continue; } crate::StepResult::Done => { if vacuum_state.current_table_columns.is_empty() { // if no columns, then db is corrupt return Err(LimboError::Corrupt( "found a table without any columns".to_string(), )); } // Prepare SELECT and INSERT statements for this table let table_name = &vacuum_state.table_names[table_idx]; let escaped_table_name = table_name.replace('"', "\"\""); let source_btree_table = program.connection.schema.read().get_btree_table(table_name); let rowid_alias = source_btree_table .as_ref() .filter(|table| table.has_rowid) .and_then(|table| { ["rowid", "_rowid_", "oid"] .iter() .copied() .find(|alias| table.get_column(alias).is_none()) }); let rowid_alias_column_index = source_btree_table .as_ref() .and_then(|table| table.get_rowid_alias_column().map(|(idx, _)| idx)); let mut data_columns: Vec<&str> = vacuum_state .current_table_columns .iter() .map(String::as_str) .collect(); let mut excluded_rowid_alias_column = false; if rowid_alias.is_some() { if let Some(idx) = rowid_alias_column_index { turso_assert!( idx < data_columns.len(), "rowid alias column index out of bounds for table columns", { "idx": idx, "columns_len": data_columns.len() } ); data_columns.remove(idx); excluded_rowid_alias_column = true; } } let column_names = data_columns.join(", "); let select_sql = match rowid_alias { Some(alias) if excluded_rowid_alias_column && column_names.is_empty() => { format!("SELECT {alias} FROM \"{escaped_table_name}\"") } Some(alias) if excluded_rowid_alias_column => { format!( "SELECT {alias}, {column_names} FROM \"{escaped_table_name}\"" ) } Some(alias) => { format!("SELECT {alias}, * FROM \"{escaped_table_name}\"") } None => format!("SELECT * FROM \"{escaped_table_name}\""), }; let select_stmt = program.connection.prepare(&select_sql)?; // Prepare INSERT statement once per table (reused for all rows) let bind_count = if rowid_alias.is_some() { data_columns.len() + 1 } else { data_columns.len() }; let placeholders: String = (0..bind_count).map(|_| "?").collect::>().join(", "); let insert_columns = if let Some(alias) = rowid_alias { if column_names.is_empty() { alias.to_string() } else { format!("{alias}, {column_names}") } } else { column_names }; let insert_sql = format!( "INSERT INTO \"{escaped_table_name}\" ({insert_columns}) VALUES ({placeholders})" ); // Internal tables need nested mode to bypass "may not // be modified" checks during prepare (compile time). let is_internal = table_name.starts_with(crate::schema::TURSO_INTERNAL_PREFIX); if is_internal { dest_conn.start_nested(); } let dest_insert_stmt = dest_conn.prepare(&insert_sql); if is_internal { dest_conn.end_nested(); } let dest_insert_stmt = dest_insert_stmt?; vacuum_state.sub_state = OpVacuumIntoSubState::CopyRows { dest_conn, select_stmt: Box::new(select_stmt), dest_insert_stmt: Box::new(dest_insert_stmt), table_idx, }; continue; } crate::StepResult::IO => { let io = column_stmt .take_io_completions() .expect("StepResult::IO returned but no completions available"); vacuum_state.sub_state = OpVacuumIntoSubState::CollectColumnInfo { dest_conn, column_stmt, table_idx, }; return Ok(InsnFunctionStepResult::IO(io)); } crate::StepResult::Busy | crate::StepResult::Interrupt => { return Err(LimboError::Busy); } } } OpVacuumIntoSubState::CopyRows { dest_conn, mut select_stmt, mut dest_insert_stmt, table_idx, } => match select_stmt.step()? { crate::StepResult::Row => { let row = select_stmt .row() .expect("StepResult::Row but row() returned None"); let values: Vec = row.get_values().cloned().collect(); dest_insert_stmt.reset()?; dest_insert_stmt.clear_bindings(); for (i, value) in values.iter().enumerate() { let index = std::num::NonZero::new(i + 1).expect("i + 1 is always non-zero"); dest_insert_stmt.bind_at(index, value.clone()); } vacuum_state.sub_state = OpVacuumIntoSubState::StepDestInsert { dest_conn, select_stmt, dest_insert_stmt, table_idx, }; continue; } crate::StepResult::Done => { // Move to next table vacuum_state.sub_state = OpVacuumIntoSubState::StartCopyTable { dest_conn, table_idx: table_idx + 1, }; continue; } crate::StepResult::IO => { let io = select_stmt .take_io_completions() .expect("StepResult::IO returned but no completions available"); vacuum_state.sub_state = OpVacuumIntoSubState::CopyRows { dest_conn, select_stmt, dest_insert_stmt, table_idx, }; return Ok(InsnFunctionStepResult::IO(io)); } crate::StepResult::Busy | crate::StepResult::Interrupt => { return Err(LimboError::Busy); } }, OpVacuumIntoSubState::StepDestInsert { dest_conn, select_stmt, mut dest_insert_stmt, table_idx, } => match dest_insert_stmt.step()? { crate::StepResult::Row => { unreachable!("INSERT statement unexpectedly returned a row"); } crate::StepResult::Done => { // Go back to get next row from source vacuum_state.sub_state = OpVacuumIntoSubState::CopyRows { dest_conn, select_stmt, dest_insert_stmt, table_idx, }; continue; } crate::StepResult::IO => { let io = dest_insert_stmt .take_io_completions() .expect("StepResult::IO returned but no completions available"); vacuum_state.sub_state = OpVacuumIntoSubState::StepDestInsert { dest_conn, select_stmt, dest_insert_stmt, table_idx, }; return Ok(InsnFunctionStepResult::IO(io)); } crate::StepResult::Busy | crate::StepResult::Interrupt => { return Err(LimboError::Busy); } }, OpVacuumIntoSubState::CopyMetaValues { dest_conn } => { // Copy meta values to destination database // Use pragma_update to set user_version and application_id // Note: schema_version is not copied - VACUUM INTO creates a new file so // there's no cache to invalidate. The destination will have its own // schema_version based on the schema operations performed. dest_conn .pragma_update("user_version", vacuum_state.source_user_version.to_string())?; dest_conn.pragma_update( "application_id", vacuum_state.source_application_id.to_string(), )?; // Now create triggers and views (after data copy to avoid triggers firing) vacuum_state.sub_state = OpVacuumIntoSubState::PrepareTriggersViews { dest_conn, idx: 0 }; continue; } OpVacuumIntoSubState::PrepareTriggersViews { dest_conn, idx } => { let schema_rows_len = vacuum_state.schema_rows.len(); turso_assert!( idx <= schema_rows_len, "idx incremented past end of schema_rows", { "idx": idx, "schema_rows_len": schema_rows_len } ); if idx == schema_rows_len { // Done creating triggers and views vacuum_state.sub_state = OpVacuumIntoSubState::Done { dest_conn }; continue; } // We validated row.len() == 4 in PrepareDestSchema let row = &vacuum_state.schema_rows[idx]; // Only process triggers and views in this phase if let Value::Text(type_val) = &row[0] { let type_str = type_val.as_str(); if type_str == "trigger" || type_str == "view" { if let Value::Text(sql) = &row[3] { let sql_str = sql.as_str(); let dest_stmt = dest_conn.prepare(sql_str)?; vacuum_state.sub_state = OpVacuumIntoSubState::StepTriggersViews { dest_conn, dest_schema_stmt: Box::new(dest_stmt), idx, }; continue; } } } // Skip non-trigger/view entries vacuum_state.sub_state = OpVacuumIntoSubState::PrepareTriggersViews { dest_conn, idx: idx + 1, }; } OpVacuumIntoSubState::StepTriggersViews { dest_conn, mut dest_schema_stmt, idx, } => match dest_schema_stmt.step()? { crate::StepResult::Row => { unreachable!("CREATE TRIGGER/VIEW statement unexpectedly returned a row"); } crate::StepResult::Done => { vacuum_state.sub_state = OpVacuumIntoSubState::PrepareTriggersViews { dest_conn, idx: idx + 1, }; continue; } crate::StepResult::IO => { let io = dest_schema_stmt .take_io_completions() .expect("StepResult::IO returned but no completions available"); vacuum_state.sub_state = OpVacuumIntoSubState::StepTriggersViews { dest_conn, dest_schema_stmt, idx, }; return Ok(InsnFunctionStepResult::IO(io)); } crate::StepResult::Busy | crate::StepResult::Interrupt => { return Err(LimboError::Busy); } }, OpVacuumIntoSubState::Done { dest_conn } => { // Commit the transaction that was started in Init state dest_conn.execute("COMMIT")?; program.connection.execute("COMMIT")?; state.pc += 1; return Ok(InsnFunctionStepResult::Step); } } } } fn with_header( pager: &Pager, mv_store: Option<&Arc>, program: &Program, db: usize, f: F, ) -> Result> where F: Fn(&DatabaseHeader) -> T, { if let Some(mv_store) = mv_store { let tx_id = program.connection.get_mv_tx_id_for_db(db); mv_store.with_header(f, tx_id.as_ref()).map(IOResult::Done) } else { pager.with_header(&f) } } pub fn with_header_mut( pager: &Pager, mv_store: Option<&Arc>, program: &Program, db: usize, f: F, ) -> Result> where F: Fn(&mut DatabaseHeader) -> T, { if let Some(mv_store) = mv_store { let tx_id = program.connection.get_mv_tx_id_for_db(db); mv_store .with_header_mut(f, tx_id.as_ref()) .map(IOResult::Done) } else { pager.with_header_mut(&f) } } fn get_schema_cookie( pager: &Arc, mv_store: Option<&Arc>, program: &Program, db: usize, ) -> Result> { if let Some(mv_store) = mv_store { let tx_id = program.connection.get_mv_tx_id_for_db(db); mv_store .with_header(|header| header.schema_cookie.get(), tx_id.as_ref()) .map(IOResult::Done) } else { pager.get_schema_cookie() } } /// A root page in MVCC might still be marked as negative in schema. On restart it is automatically transformed to positive but in other cases /// we need to map it to positive if we can in case checkpoint happened. fn maybe_transform_root_page_to_positive(mvcc_store: Option<&Arc>, root_page: i64) -> i64 { if let Some(mvcc_store) = mvcc_store { if root_page < 0 { mvcc_store.get_real_table_id(root_page) } else { root_page } } else { root_page } } #[cfg(test)] mod tests { use super::*; use crate::translate::collate::CollationSeq; use crate::vdbe::BranchOffset; use crate::{Database, DatabaseOpts, MemoryIO, IO}; fn prepare_test_statement() -> Statement { let io: Arc = Arc::new(MemoryIO::new()); let db = Database::open_file_with_flags( io, ":memory:", OpenFlags::Create, DatabaseOpts::new(), None, ) .unwrap(); let conn = db.connect().unwrap(); conn.prepare("SELECT 1;").unwrap() } fn make_spilled_hash_table() -> (HashTable, Vec, usize) { let io: Arc = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, ..Default::default() }; let mut ht = HashTable::new(config, io); for i in 0..1024 { match ht .insert(vec![Value::from_i64(i)], i, vec![], None) .unwrap() { IOResult::Done(()) => {} IOResult::IO(_) => panic!("memory IO should complete synchronously"), } } match ht.finalize_build(None).unwrap() { IOResult::Done(()) => {} IOResult::IO(_) => panic!("memory IO should complete synchronously"), } assert!(ht.has_spilled(), "test requires spilled hash table"); let probe_key = (0..1024) .map(|i| vec![Value::from_i64(i)]) .find(|key| { let partition_idx = ht.partition_for_keys(key); !ht.is_partition_loaded(partition_idx) }) .expect("expected an unloaded spilled partition"); let partition_idx = ht.partition_for_keys(&probe_key); (ht, probe_key, partition_idx) } #[test] fn test_hash_probe_rejects_unloaded_spilled_partition_without_probe_rowid() { let stmt = prepare_test_statement(); let (ht, probe_key, _) = make_spilled_hash_table(); let mut state = ProgramState::new(2, 0); state.hash_tables.insert(7, ht); state.set_register(0, Register::Value(probe_key[0].clone())); let insn = Insn::HashProbe { hash_table_id: 7, key_start_reg: 0, num_keys: 1, dest_reg: 1, target_pc: BranchOffset::Offset(99), payload_dest_reg: None, num_payload: 0, probe_rowid_reg: None, }; let err = match op_hash_probe(stmt.get_program(), &mut state, &insn, stmt.get_pager()) { Ok(_) => { panic!("HashProbe should reject grace-only probing without a loaded partition") } Err(err) => err, }; assert!( matches!(err, LimboError::InternalError(ref message) if message.contains("probe_rowid_reg=None is grace-only")), "unexpected error: {err:?}" ); assert_eq!(state.pc, 0, "pc should not advance on invariant violation"); assert!( state.op_hash_probe_state.is_none(), "HashProbe should not stash resumable state for the removed fallback path" ); } #[test] fn test_hash_probe_allows_grace_style_probe_after_partition_preload() { let stmt = prepare_test_statement(); let (mut ht, probe_key, partition_idx) = make_spilled_hash_table(); loop { match ht.load_spilled_partition(partition_idx, None).unwrap() { IOResult::Done(()) => break, IOResult::IO(_) => continue, } } assert!( ht.is_partition_loaded(partition_idx), "grace-style probe requires the build partition to be resident" ); let expected_rowid = match &probe_key[0] { Value::Numeric(Numeric::Integer(i)) => *i, ref other => panic!("expected integer probe key, got {other:?}"), }; let mut state = ProgramState::new(2, 0); state.hash_tables.insert(7, ht); state.set_register(0, Register::Value(probe_key[0].clone())); let insn = Insn::HashProbe { hash_table_id: 7, key_start_reg: 0, num_keys: 1, dest_reg: 1, target_pc: BranchOffset::Offset(99), payload_dest_reg: None, num_payload: 0, probe_rowid_reg: None, }; let step = op_hash_probe(stmt.get_program(), &mut state, &insn, stmt.get_pager()) .expect("preloaded grace probe should succeed"); assert!(matches!(step, InsnFunctionStepResult::Step)); assert_eq!(state.pc, 1, "matching probe should fall through"); assert_eq!( state.get_register(1).get_value(), &Value::from_i64(expected_rowid), "HashProbe should return the matching build rowid" ); } #[test] fn test_decr_jump_zero_non_integer_register_returns_error() { let stmt = prepare_test_statement(); let mut state = ProgramState::new(1, 0); state.set_register(0, Register::Value(Value::Text("not-an-int".into()))); let insn = Insn::DecrJumpZero { reg: 0, target_pc: crate::vdbe::BranchOffset::Offset(1), }; let err = match op_decr_jump_zero(stmt.get_program(), &mut state, &insn, stmt.get_pager()) { Ok(_) => panic!("non-integer register must fail"), Err(err) => err, }; assert!(matches!(err, LimboError::Constraint(message) if message == "datatype mismatch")); assert_eq!(state.pc, 0); } #[test] fn test_execute_sqlite_version() { let version_integer = 3046001; let expected = "3.46.1"; assert_eq!(execute_turso_version(version_integer), expected); } #[test] fn test_ascii_whitespace_is_trimmed() { // Regular ASCII whitespace SHOULD be trimmed let ascii_whitespace_cases = vec![ (" 12", 12i64), // space ("12 ", 12i64), // trailing space (" 12 ", 12i64), // both sides ("\t42\t", 42i64), // tab ("\n99\n", 99i64), // newline (" \t\n123\r\n ", 123i64), // mixed ASCII whitespace ]; for (input, expected_int) in ascii_whitespace_cases { let mut register = Register::Value(Value::Text(input.into())); apply_affinity_char(&mut register, Affinity::Integer); match register { Register::Value(Value::Numeric(Numeric::Integer(i))) => { assert_eq!( i, expected_int, "String '{input}' should convert to {expected_int}, got {i}" ); } other => { panic!( "String '{input}' should be converted to integer {expected_int}, got {other:?}" ); } } } } #[test] fn test_non_breaking_space_not_trimmed() { let test_strings = vec![ ("12\u{00A0}", "text", 3), // '12' + non-breaking space (3 chars, 4 bytes) ("\u{00A0}12", "text", 3), // non-breaking space + '12' (3 chars, 4 bytes) ("12\u{00A0}34", "text", 5), // '12' + nbsp + '34' (5 chars, 6 bytes) ]; for (input, _expected_type, expected_len) in test_strings { let mut register = Register::Value(Value::Text(input.into())); apply_affinity_char(&mut register, Affinity::Integer); match register { Register::Value(Value::Text(t)) => { assert_eq!( t.as_str().chars().count(), expected_len, "String '{input}' should have {expected_len} characters", ); } Register::Value(Value::Numeric(Numeric::Integer(_))) => { panic!("String '{input}' should NOT be converted to integer"); } other => panic!("Unexpected value type: {other:?}"), } } } #[test] fn test_affinity_keeps_nan_inf_text() { let cases = ["nan", "inf"]; for input in cases { let mut register = Register::Value(Value::Text(input.into())); apply_affinity_char(&mut register, Affinity::Integer); match register { Register::Value(Value::Text(t)) => { assert_eq!(t.as_str(), input, "Unexpected conversion for '{input}'"); } other => { panic!("'{input}' should remain text, got {other:?}"); } } let mut register = Register::Value(Value::Text(input.into())); apply_affinity_char(&mut register, Affinity::Numeric); match register { Register::Value(Value::Text(t)) => { assert_eq!(t.as_str(), input, "Unexpected conversion for '{input}'"); } other => { panic!("'{input}' should remain text, got {other:?}"); } } } } #[test] fn test_init_agg_payload_count() { let mut payload = Vec::new(); init_agg_payload(&AggFunc::Count, &mut payload).unwrap(); assert_eq!(payload.len(), 1); assert_eq!(payload[0], Value::from_i64(0)); } #[test] fn test_init_agg_payload_sum() { let mut payload = Vec::new(); init_agg_payload(&AggFunc::Sum, &mut payload).unwrap(); assert_eq!(payload.len(), 4); assert_eq!(payload[0], Value::Null); // acc assert_eq!(payload[1], Value::from_f64(0.0)); // r_err assert_eq!(payload[2], Value::from_i64(0)); // approx assert_eq!(payload[3], Value::from_i64(0)); // ovrfl } #[test] fn test_init_agg_payload_avg() { let mut payload = Vec::new(); init_agg_payload(&AggFunc::Avg, &mut payload).unwrap(); assert_eq!(payload.len(), 3); assert_eq!(payload[0], Value::from_f64(0.0)); // sum assert_eq!(payload[1], Value::from_f64(0.0)); // r_err assert_eq!(payload[2], Value::from_i64(0)); // count } #[test] fn test_update_count_skips_null() { let mut payload = vec![Value::from_i64(5)]; update_agg_payload( &AggFunc::Count, Value::Null, None, &mut payload, CollationSeq::Binary, &None, ) .unwrap(); assert_eq!(payload[0], Value::from_i64(5)); // unchanged } #[test] fn test_update_count_increments() { let mut payload = vec![Value::from_i64(5)]; update_agg_payload( &AggFunc::Count, Value::from_i64(42), None, &mut payload, CollationSeq::Binary, &None, ) .unwrap(); assert_eq!(payload[0], Value::from_i64(6)); } #[test] fn test_update_sum_integers() { let mut payload = vec![ Value::Null, Value::from_f64(0.0), Value::from_i64(0), Value::from_i64(0), ]; update_agg_payload( &AggFunc::Sum, Value::from_i64(10), None, &mut payload, CollationSeq::Binary, &None, ) .unwrap(); assert_eq!(payload[0], Value::from_i64(10)); update_agg_payload( &AggFunc::Sum, Value::from_i64(5), None, &mut payload, CollationSeq::Binary, &None, ) .unwrap(); assert_eq!(payload[0], Value::from_i64(15)); } #[test] fn test_update_sum_null_is_skipped() { let mut payload = vec![ Value::from_i64(10), Value::from_f64(0.0), Value::from_i64(0), Value::from_i64(0), ]; update_agg_payload( &AggFunc::Sum, Value::Null, None, &mut payload, CollationSeq::Binary, &None, ) .unwrap(); assert_eq!(payload[0], Value::from_i64(10)); // unchanged } #[test] fn test_update_min_max() { let mut payload = vec![Value::Null]; // First value sets the min/max update_agg_payload( &AggFunc::Min, Value::from_i64(5), None, &mut payload, CollationSeq::Binary, &None, ) .unwrap(); assert_eq!(payload[0], Value::from_i64(5)); // Smaller value updates min update_agg_payload( &AggFunc::Min, Value::from_i64(3), None, &mut payload, CollationSeq::Binary, &None, ) .unwrap(); assert_eq!(payload[0], Value::from_i64(3)); // Larger value doesn't update min update_agg_payload( &AggFunc::Min, Value::from_i64(10), None, &mut payload, CollationSeq::Binary, &None, ) .unwrap(); assert_eq!(payload[0], Value::from_i64(3)); } #[test] fn test_update_avg() { // Payload: [sum, r_err, count] let mut payload = vec![ Value::from_f64(0.0), Value::from_f64(0.0), Value::from_i64(0), ]; update_agg_payload( &AggFunc::Avg, Value::from_i64(10), None, &mut payload, CollationSeq::Binary, &None, ) .unwrap(); assert_eq!(payload[0], Value::from_f64(10.0)); assert_eq!(payload[2], Value::from_i64(1)); update_agg_payload( &AggFunc::Avg, Value::from_i64(20), None, &mut payload, CollationSeq::Binary, &None, ) .unwrap(); assert_eq!(payload[0], Value::from_f64(30.0)); assert_eq!(payload[2], Value::from_i64(2)); } #[test] fn test_finalize_count() { let payload = vec![Value::from_i64(42)]; let result = finalize_agg_payload(&AggFunc::Count, &payload).unwrap(); assert_eq!(result, Value::from_i64(42)); } #[test] fn test_finalize_avg() { // Payload: [sum, r_err, count] let payload = vec![ Value::from_f64(30.0), Value::from_f64(0.0), Value::from_i64(3), ]; let result = finalize_agg_payload(&AggFunc::Avg, &payload).unwrap(); assert_eq!(result, Value::from_f64(10.0)); } #[test] fn test_finalize_avg_empty() { // Payload: [sum, r_err, count] let payload = vec![ Value::from_f64(0.0), Value::from_f64(0.0), Value::from_i64(0), ]; let result = finalize_agg_payload(&AggFunc::Avg, &payload).unwrap(); assert_eq!(result, Value::Null); } #[test] fn test_array_agg_accumulates_correctly() { // Verify that array_agg produces correct results when accumulating // multiple values. Uses the direct payload approach (O(1) per row). let mut payload = Vec::new(); init_agg_payload(&AggFunc::ArrayAgg, &mut payload).unwrap(); // Simulate how AggStep accumulates values directly into the payload Vec. for i in 0..100 { let count = payload[0].as_int().unwrap_or(0) as usize; payload[0] = Value::from_i64((count + 1) as i64); payload.push(Value::from_i64(i)); } let result = finalize_agg_payload(&AggFunc::ArrayAgg, &payload).unwrap(); let blob = match &result { Value::Blob(b) => b, _ => panic!("Expected Blob, got {result:?}"), }; let elements = array_values_from_blob(blob).unwrap(); assert_eq!(elements.len(), 100); for (i, elem) in elements.iter().enumerate() { assert_eq!(*elem, Value::from_i64(i as i64)); } } #[test] fn test_array_agg_zero_rows_produces_valid_result() { // array_agg with zero rows should return NULL, matching PostgreSQL. // The result must not be an invalid empty blob that crashes on decode. let mut payload = Vec::new(); init_agg_payload(&AggFunc::ArrayAgg, &mut payload).unwrap(); // No values accumulated — count stays 0. let result = finalize_agg_payload(&AggFunc::ArrayAgg, &payload).unwrap(); assert_eq!(result, Value::Null); } #[test] fn test_array_agg_finalize_bounds_check() { // If payload[0] count is larger than the actual payload length, // finalize should return an error rather than panicking. let payload = vec![Value::from_i64(999)]; // claims 999 elements but has none let result = finalize_agg_payload(&AggFunc::ArrayAgg, &payload); assert!( result.is_err(), "Should error on count exceeding payload length" ); } #[test] fn test_negate_blob_subscript_invalid_utf8_no_panic() { // Negating a blob subscript that extracts a "text" value containing // invalid UTF-8 bytes must not panic. The record decoder uses // from_utf8_unchecked, so ArrayElement must validate extracted text. // // Reproduces fuzzer bug at seed 27035. let io: Arc = Arc::new(MemoryIO::new()); let db = Database::open_file_with_flags( io, ":memory:", OpenFlags::Create, DatabaseOpts::new(), None, ) .unwrap(); let conn = db.connect().unwrap(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { conn.execute("SELECT -X'18530E218A8D2D8D8F7456733370E68357745AFE13FC1B94751B77FCB00D0CAD971017936278BFF49BB4C8BD47F874ECA5226D3A433B7DFCD18661673598CED1FDB30A795F6F25'[2]") })); assert!( result.is_ok(), "Negating a blob subscript with invalid UTF-8 text should not panic" ); } } ================================================ FILE: core/vdbe/explain.rs ================================================ use crate::vdbe::{builder::CursorType, insn::RegisterOrLiteral}; use crate::HashSet; use turso_parser::ast::{ResolveType, SortOrder}; use super::{Insn, InsnReference, PreparedProgram, Value}; use crate::function::{Func, ScalarFunc}; pub const EXPLAIN_COLUMNS: [&str; 8] = ["addr", "opcode", "p1", "p2", "p3", "p4", "p5", "comment"]; pub const EXPLAIN_COLUMNS_TYPE: [&str; 8] = [ "INTEGER", "TEXT", "INTEGER", "INTEGER", "INTEGER", "TEXT", "INTEGER", "TEXT", ]; pub const EXPLAIN_QUERY_PLAN_COLUMNS: [&str; 4] = ["id", "parent", "notused", "detail"]; pub const EXPLAIN_QUERY_PLAN_COLUMNS_TYPE: [&str; 4] = ["INTEGER", "INTEGER", "INTEGER", "TEXT"]; pub fn insn_to_row( program: &PreparedProgram, insn: &Insn, ) -> (&'static str, i64, i64, i64, Value, i64, String) { let mut ephemeral_cursors = HashSet::default(); for (insn, _) in &program.insns { match insn { Insn::OpenEphemeral { cursor_id, .. } => { ephemeral_cursors.insert(*cursor_id); } Insn::OpenAutoindex { cursor_id } => { ephemeral_cursors.insert(*cursor_id); } Insn::OpenDup { new_cursor_id, .. } => { // Note: relies on invariant that OpenDup is only for ephemeral cursors ephemeral_cursors.insert(*new_cursor_id); } _ => {} } } let get_table_or_index_name = |cursor_id: usize| -> String { let cursor_type = &program.cursor_ref[cursor_id].1; let name = match cursor_type { CursorType::BTreeTable(table) => table.name.as_str(), CursorType::BTreeIndex(index) => index.name.as_str(), CursorType::IndexMethod(descriptor) => descriptor.definition().index_name, CursorType::Pseudo(_) => "pseudo", CursorType::VirtualTable(virtual_table) => virtual_table.name.as_str(), CursorType::MaterializedView(table, _) => table.name.as_str(), CursorType::Sorter => "sorter", }; if ephemeral_cursors.contains(&cursor_id) { format!("ephemeral({name})") } else { name.to_string() } }; match insn { Insn::Init { target_pc } => ( "Init", 0, target_pc.as_debug_int() as i64, 0, Value::build_text(""), 0, format!("Start at {}", target_pc.as_debug_int()), ), Insn::Add { lhs, rhs, dest } => ( "Add", *lhs as i64, *rhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=r[{lhs}]+r[{rhs}]"), ), Insn::Subtract { lhs, rhs, dest } => ( "Subtract", *lhs as i64, *rhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=r[{lhs}]-r[{rhs}]"), ), Insn::Multiply { lhs, rhs, dest } => ( "Multiply", *lhs as i64, *rhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=r[{lhs}]*r[{rhs}]"), ), Insn::Divide { lhs, rhs, dest } => ( "Divide", *lhs as i64, *rhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=r[{lhs}]/r[{rhs}]"), ), Insn::BitAnd { lhs, rhs, dest } => ( "BitAnd", *lhs as i64, *rhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=r[{lhs}]&r[{rhs}]"), ), Insn::BitOr { lhs, rhs, dest } => ( "BitOr", *lhs as i64, *rhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=r[{lhs}]|r[{rhs}]"), ), Insn::BitNot { reg, dest } => ( "BitNot", *reg as i64, *dest as i64, 0, Value::build_text(""), 0, format!("r[{dest}]=~r[{reg}]"), ), Insn::Checkpoint { database, checkpoint_mode: _, dest, } => ( "Checkpoint", *database as i64, *dest as i64, 0, Value::build_text(""), 0, format!("r[{dest}]=~r[{database}]"), ), Insn::Remainder { lhs, rhs, dest } => ( "Remainder", *lhs as i64, *rhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=r[{lhs}]%r[{rhs}]"), ), Insn::Null { dest, dest_end } => ( "Null", 0, *dest as i64, dest_end.map_or(0, |end| end as i64), Value::build_text(""), 0, dest_end.map_or(format!("r[{dest}]=NULL"), |end| { format!("r[{dest}..{end}]=NULL") }), ), Insn::NullRow { cursor_id } => ( "NullRow", *cursor_id as i64, 0, 0, Value::build_text(""), 0, format!("Set cursor {cursor_id} to a (pseudo) NULL row"), ), Insn::NotNull { reg, target_pc } => ( "NotNull", *reg as i64, target_pc.as_debug_int() as i64, 0, Value::build_text(""), 0, format!("r[{}]!=NULL -> goto {}", reg, target_pc.as_debug_int()), ), Insn::Compare { start_reg_a, start_reg_b, count, key_info, } => ( "Compare", *start_reg_a as i64, *start_reg_b as i64, *count as i64, Value::build_text(format!("k({count}, {})", key_info.iter().map(|k| k.collation.to_string()).collect::>().join(", "))), 0, format!( "r[{}..{}]==r[{}..{}]", start_reg_a, start_reg_a + (count - 1), start_reg_b, start_reg_b + (count - 1) ), ), Insn::Jump { target_pc_lt, target_pc_eq, target_pc_gt, } => ( "Jump", target_pc_lt.as_debug_int() as i64, target_pc_eq.as_debug_int() as i64, target_pc_gt.as_debug_int() as i64, Value::build_text(""), 0, "".to_string(), ), Insn::Move { source_reg, dest_reg, count, } => ( "Move", *source_reg as i64, *dest_reg as i64, *count as i64, Value::build_text(""), 0, format!( "r[{}..{}]=r[{}..{}]", dest_reg, dest_reg + (count - 1), source_reg, source_reg + (count - 1) ), ), Insn::IfPos { reg, target_pc, decrement_by, } => ( "IfPos", *reg as i64, target_pc.as_debug_int() as i64, *decrement_by as i64, Value::build_text(""), 0, format!( "r[{}]>0 -> r[{}]-={}, goto {}", reg, reg, decrement_by, target_pc.as_debug_int() ), ), Insn::Eq { lhs, rhs, target_pc, collation, .. } => ( "Eq", *lhs as i64, *rhs as i64, target_pc.as_debug_int() as i64, Value::build_text(collation.map_or("".to_string(), |c| c.to_string())), 0, format!( "if r[{}]==r[{}] goto {}", lhs, rhs, target_pc.as_debug_int() ), ), Insn::Ne { lhs, rhs, target_pc, collation, .. } => ( "Ne", *lhs as i64, *rhs as i64, target_pc.as_debug_int() as i64, Value::build_text(collation.map_or("".to_string(), |c| c.to_string())), 0, format!( "if r[{}]!=r[{}] goto {}", lhs, rhs, target_pc.as_debug_int() ), ), Insn::Lt { lhs, rhs, target_pc, collation, .. } => ( "Lt", *lhs as i64, *rhs as i64, target_pc.as_debug_int() as i64, Value::build_text(collation.map_or("".to_string(), |c| c.to_string())), 0, format!("if r[{}] ( "Le", *lhs as i64, *rhs as i64, target_pc.as_debug_int() as i64, Value::build_text(collation.map_or("".to_string(), |c| c.to_string())), 0, format!( "if r[{}]<=r[{}] goto {}", lhs, rhs, target_pc.as_debug_int() ), ), Insn::Gt { lhs, rhs, target_pc, collation, .. } => ( "Gt", *lhs as i64, *rhs as i64, target_pc.as_debug_int() as i64, Value::build_text(collation.map_or("".to_string(), |c| c.to_string())), 0, format!("if r[{}]>r[{}] goto {}", lhs, rhs, target_pc.as_debug_int()), ), Insn::Ge { lhs, rhs, target_pc, collation, .. } => ( "Ge", *lhs as i64, *rhs as i64, target_pc.as_debug_int() as i64, Value::build_text(collation.map_or("".to_string(), |c| c.to_string())), 0, format!( "if r[{}]>=r[{}] goto {}", lhs, rhs, target_pc.as_debug_int() ), ), Insn::If { reg, target_pc, jump_if_null, } => ( "If", *reg as i64, target_pc.as_debug_int() as i64, *jump_if_null as i64, Value::build_text(""), 0, format!("if r[{}] goto {}", reg, target_pc.as_debug_int()), ), Insn::IfNot { reg, target_pc, jump_if_null, } => ( "IfNot", *reg as i64, target_pc.as_debug_int() as i64, *jump_if_null as i64, Value::build_text(""), 0, format!("if !r[{}] goto {}", reg, target_pc.as_debug_int()), ), Insn::OpenRead { cursor_id, root_page, db, } => ( "OpenRead", *cursor_id as i64, *root_page, *db as i64, Value::build_text(program.cursor_ref[*cursor_id] .1.get_explain_description()), 0, { let cursor_type = program.cursor_ref[*cursor_id] .0 .as_ref() .map_or("", |cursor_key| { if cursor_key.index.is_some() { "index" } else { "table" } }); format!( "{}={}, root={}, iDb={}", cursor_type, get_table_or_index_name(*cursor_id), root_page, db ) }, ), Insn::VOpen { cursor_id } => ( "VOpen", *cursor_id as i64, 0, 0, Value::build_text(""), 0, { let cursor_type = program.cursor_ref[*cursor_id] .0 .as_ref() .map_or("", |cursor_key| { if cursor_key.index.is_some() { "index" } else { "table" } }); format!("{} {}", cursor_type, get_table_or_index_name(*cursor_id),) }, ), Insn::VCreate { table_name, module_name, args_reg, } => ( "VCreate", *table_name as i64, *module_name as i64, args_reg.unwrap_or(0) as i64, Value::build_text(""), 0, format!("table={table_name}, module={module_name}"), ), Insn::VFilter { cursor_id, pc_if_empty, arg_count, .. } => ( "VFilter", *cursor_id as i64, pc_if_empty.as_debug_int() as i64, *arg_count as i64, Value::build_text(""), 0, "".to_string(), ), Insn::VColumn { cursor_id, column, dest, } => ( "VColumn", *cursor_id as i64, *column as i64, *dest as i64, Value::build_text(""), 0, "".to_string(), ), Insn::VUpdate { cursor_id, arg_count, // P2: Number of arguments in argv[] start_reg, // P3: Start register for argv[] conflict_action, // P4: Conflict resolution flags } => ( "VUpdate", *cursor_id as i64, *arg_count as i64, *start_reg as i64, Value::build_text(""), *conflict_action as i64, format!("args=r[{}..{}]", start_reg, start_reg + arg_count - 1), ), Insn::VNext { cursor_id, pc_if_next, } => ( "VNext", *cursor_id as i64, pc_if_next.as_debug_int() as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::VDestroy { db, table_name } => ( "VDestroy", *db as i64, 0, 0, Value::build_text(table_name.clone()), 0, "".to_string(), ), Insn::VBegin{cursor_id} => ( "VBegin", *cursor_id as i64, 0, 0, Value::build_text(""), 0, "".into() ), Insn::VRename{cursor_id, new_name_reg} => ( "VRename", *cursor_id as i64, *new_name_reg as i64, 0, Value::build_text(""), 0, "".into(), ), Insn::OpenPseudo { cursor_id, content_reg, num_fields, } => ( "OpenPseudo", *cursor_id as i64, *content_reg as i64, *num_fields as i64, Value::build_text(""), 0, format!("{num_fields} columns in r[{content_reg}]"), ), Insn::Rewind { cursor_id, pc_if_empty, } => ( "Rewind", *cursor_id as i64, pc_if_empty.as_debug_int() as i64, 0, Value::build_text(""), 0, { let cursor_type = program.cursor_ref[*cursor_id] .0 .as_ref() .map_or("", |cursor_key| { if cursor_key.index.is_some() { "index" } else { "table" } }); format!( "Rewind {} {}", cursor_type, get_table_or_index_name(*cursor_id), ) }, ), Insn::Column { cursor_id, column, dest, default, } => { let cursor_type = &program.cursor_ref[*cursor_id].1; let column_name: Option<&String> = match cursor_type { CursorType::BTreeTable(table) => { let name = table.columns.get(*column).and_then(|v| v.name.as_ref()); name } CursorType::BTreeIndex(index) => { let name = &index.columns.get(*column).expect("column index out of bounds").name; Some(name) } CursorType::MaterializedView(table, _) => { let name = table.columns.get(*column).and_then(|v| v.name.as_ref()); name } CursorType::Pseudo(_) => None, CursorType::Sorter => None, CursorType::IndexMethod(..) => None, CursorType::VirtualTable(v) => v.columns.get(*column).expect("column index out of bounds").name.as_ref(), }; ( "Column", *cursor_id as i64, *column as i64, *dest as i64, default.clone().unwrap_or_else(|| Value::build_text("")), 0, format!( "r[{}]={}.{}", dest, get_table_or_index_name(*cursor_id), &column_name.map_or_else(|| format!("column {}", *column), |name| name.to_string()) ), ) } Insn::TypeCheck { start_reg, count, check_generated, .. } => ( "TypeCheck", *start_reg as i64, *count as i64, *check_generated as i64, Value::build_text(""), 0, String::from(""), ), Insn::ArrayEncode { reg, element_type, table_name, col_name, .. } => ( "ArrayEncode", *reg as i64, 0, 0, Value::build_text(""), 0, format!("{table_name}.{col_name} ({element_type})"), ), Insn::ArrayDecode { reg } => ( "ArrayDecode", *reg as i64, 0, 0, Value::build_text(""), 0, String::new(), ), Insn::ArrayElement { array_reg, index_reg, dest, } => ( "ArrayElement", *array_reg as i64, *index_reg as i64, *dest as i64, Value::build_text(""), 0, String::new(), ), Insn::ArrayLength { reg, dest } => ( "ArrayLength", *reg as i64, *dest as i64, 0, Value::build_text(""), 0, String::new(), ), Insn::MakeArray { start_reg, count, dest, } => ( "MakeArray", *start_reg as i64, *count as i64, *dest as i64, Value::build_text(""), 0, String::new(), ), Insn::MakeArrayDynamic { start_reg, count_reg, dest, } => ( "MakeArrayDynamic", *start_reg as i64, *count_reg as i64, *dest as i64, Value::build_text(""), 0, String::new(), ), Insn::RegCopyOffset { src, base, offset_reg, } => ( "RegCopyOffset", *src as i64, *base as i64, *offset_reg as i64, Value::build_text(""), 0, String::new(), ), Insn::ArrayConcat { lhs, rhs, dest } => ( "ArrayConcat", *lhs as i64, *rhs as i64, *dest as i64, Value::build_text(""), 0, String::new(), ), Insn::ArraySetElement { array_reg, index_reg, value_reg, dest, } => ( "ArraySetElement", *array_reg as i64, *index_reg as i64, *dest as i64, Value::build_text(""), 0, format!("r[{value_reg}]"), ), Insn::ArraySlice { array_reg, start_reg, end_reg, dest, } => ( "ArraySlice", *array_reg as i64, *start_reg as i64, *dest as i64, Value::build_text(""), 0, format!("end_reg=r[{end_reg}]"), ), Insn::MakeRecord { start_reg, count, dest_reg, index_name, affinity_str: _, } => { let for_index = index_name.as_ref().map(|name| format!("; for {name}")); ( "MakeRecord", *start_reg as i64, *count as i64, *dest_reg as i64, Value::build_text(""), 0, format!( "r[{}]=mkrec(r[{}..{}]){}", dest_reg, start_reg, start_reg + count - 1, for_index.unwrap_or_else(|| "".to_string()) ), ) } Insn::ResultRow { start_reg, count } => ( "ResultRow", *start_reg as i64, *count as i64, 0, Value::build_text(""), 0, if *count == 1 { format!("output=r[{start_reg}]") } else { format!("output=r[{}..{}]", start_reg, start_reg + count - 1) }, ), Insn::Next { cursor_id, pc_if_next, } => ( "Next", *cursor_id as i64, pc_if_next.as_debug_int() as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::Halt { err_code, description, on_error, description_reg, } => { let p2 = match on_error { Some(ResolveType::Rollback) => 1, Some(ResolveType::Abort) => 2, Some(ResolveType::Fail) => 3, Some(ResolveType::Ignore) => 4, Some(ResolveType::Replace) => 5, None => 0, }; let p3 = description_reg.unwrap_or(0) as i64; ( "Halt", *err_code as i64, p2, p3, Value::build_text(description.clone()), 0, "".to_string(), ) } Insn::HaltIfNull { err_code, target_reg, description, } => ( "HaltIfNull", *err_code as i64, 0, *target_reg as i64, Value::build_text(description.clone()), 0, "".to_string(), ), Insn::Transaction { db, tx_mode, schema_cookie} => ( "Transaction", *db as i64, *tx_mode as i64, *schema_cookie as i64, Value::build_text(""), 0, format!("iDb={db} tx_mode={tx_mode:?}"), ), Insn::Goto { target_pc } => ( "Goto", 0, target_pc.as_debug_int() as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::Gosub { target_pc, return_reg, } => ( "Gosub", *return_reg as i64, target_pc.as_debug_int() as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::Return { return_reg, can_fallthrough, } => ( "Return", *return_reg as i64, 0, *can_fallthrough as i64, Value::build_text(""), 0, "".to_string(), ), Insn::Integer { value, dest } => ( "Integer", *value, *dest as i64, 0, Value::build_text(""), 0, format!("r[{dest}]={value}"), ), Insn::Program { params, ignore_jump_target, .. } => ( "Program", // P1: first register that contains a param params.first().map(|v| match v { crate::types::Value::Numeric(crate::numeric::Numeric::Integer(i)) if *i < 0 => -i - 1, _ => 0, }).unwrap_or(0), // P2: ignore jump target (for RAISE(IGNORE)) ignore_jump_target.as_debug_int() as i64, // P3: number of registers that contain params params.len() as i64, Value::build_text(program.sql.clone()), 0, format!("subprogram={}", program.sql), ), Insn::Real { value, dest } => ( "Real", 0, *dest as i64, 0, Value::from_f64(*value), 0, format!("r[{dest}]={value}"), ), Insn::RealAffinity { register } => ( "RealAffinity", *register as i64, 0, 0, Value::build_text(""), 0, "".to_string(), ), Insn::String8 { value, dest } => ( "String8", 0, *dest as i64, 0, Value::build_text(value.clone()), 0, format!("r[{dest}]='{value}'"), ), Insn::Blob { value, dest } => ( "Blob", 0, *dest as i64, 0, Value::Blob(value.clone()), 0, format!( "r[{}]={} (len={})", dest, String::from_utf8_lossy(value), value.len() ), ), Insn::RowId { cursor_id, dest } => ( "RowId", *cursor_id as i64, *dest as i64, 0, Value::build_text(""), 0, format!("r[{}]={}.rowid", dest, get_table_or_index_name(*cursor_id)), ), Insn::IdxRowId { cursor_id, dest } => ( "IdxRowId", *cursor_id as i64, *dest as i64, 0, Value::build_text(""), 0, format!( "r[{}]={}.rowid", dest, program.cursor_ref[*cursor_id] .0 .as_ref() .map(|k| format!( "cursor {} for {} {}", cursor_id, if k.index.is_some() { "index" } else { "table" }, get_table_or_index_name(*cursor_id), )) .unwrap_or_else(|| format!("cursor {cursor_id}")) ), ), Insn::SeekRowid { cursor_id, src_reg, target_pc, } => ( "SeekRowid", *cursor_id as i64, *src_reg as i64, target_pc.as_debug_int() as i64, Value::build_text(""), 0, format!( "if (r[{}]!={}.rowid) goto {}", src_reg, &program.cursor_ref[*cursor_id] .0 .as_ref() .map(|k| format!( "cursor {} for {} {}", cursor_id, if k.index.is_some() { "index" } else { "table" }, get_table_or_index_name(*cursor_id), )) .unwrap_or_else(|| format!("cursor {cursor_id}")), target_pc.as_debug_int() ), ), Insn::DeferredSeek { index_cursor_id, table_cursor_id, } => ( "DeferredSeek", *index_cursor_id as i64, *table_cursor_id as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::SeekGT { is_index: _, cursor_id, start_reg, num_regs, target_pc, } | Insn::SeekGE { is_index: _, cursor_id, start_reg, num_regs, target_pc, .. } | Insn::SeekLE { is_index: _, cursor_id, start_reg, num_regs, target_pc, .. } | Insn::SeekLT { is_index: _, cursor_id, start_reg, num_regs, target_pc, } => ( match insn { Insn::SeekGT { .. } => "SeekGT", Insn::SeekGE { .. } => "SeekGE", Insn::SeekLE { .. } => "SeekLE", Insn::SeekLT { .. } => "SeekLT", _ => unreachable!(), }, *cursor_id as i64, target_pc.as_debug_int() as i64, *start_reg as i64, Value::build_text(""), 0, format!("key=[{}..{}]", start_reg, start_reg + num_regs - 1), ), Insn::SeekEnd { cursor_id } => ( "SeekEnd", *cursor_id as i64, 0, 0, Value::build_text(""), 0, "".to_string(), ), Insn::IdxInsert { cursor_id, record_reg, unpacked_start, flags, .. } => ( "IdxInsert", *cursor_id as i64, *record_reg as i64, unpacked_start.unwrap_or(0) as i64, Value::build_text(""), flags.0 as i64, format!("key=r[{record_reg}]"), ), Insn::IdxGT { cursor_id, start_reg, num_regs, target_pc, } | Insn::IdxGE { cursor_id, start_reg, num_regs, target_pc, } | Insn::IdxLE { cursor_id, start_reg, num_regs, target_pc, } | Insn::IdxLT { cursor_id, start_reg, num_regs, target_pc, } => ( match insn { Insn::IdxGT { .. } => "IdxGT", Insn::IdxGE { .. } => "IdxGE", Insn::IdxLE { .. } => "IdxLE", Insn::IdxLT { .. } => "IdxLT", _ => unreachable!(), }, *cursor_id as i64, target_pc.as_debug_int() as i64, *start_reg as i64, Value::build_text(""), 0, format!("key=[{}..{}]", start_reg, start_reg + num_regs - 1), ), Insn::DecrJumpZero { reg, target_pc } => ( "DecrJumpZero", *reg as i64, target_pc.as_debug_int() as i64, 0, Value::build_text(""), 0, format!("if (--r[{}]==0) goto {}", reg, target_pc.as_debug_int()), ), Insn::AggStep { func, acc_reg, delimiter: _, col, comparator: _, } => ( "AggStep", 0, *col as i64, *acc_reg as i64, Value::build_text(func.as_str()), 0, format!("accum=r[{}] step(r[{}])", *acc_reg, *col), ), Insn::AggFinal { register, func } => ( "AggFinal", 0, *register as i64, 0, Value::build_text(func.as_str()), 0, format!("accum=r[{}]", *register), ), Insn::AggValue { acc_reg, dest_reg, func } => ( "AggValue", 0, *acc_reg as i64, *dest_reg as i64, Value::build_text(func.as_str()), 0, format!("accum=r[{}] dest=r[{}]", *acc_reg, *dest_reg), ), Insn::SorterOpen { cursor_id, columns, order_and_collations, .. } => { let to_print: Vec = order_and_collations .iter() .map(|(order, collation)| { let sign = match order { SortOrder::Asc => "", SortOrder::Desc => "-", }; if let Some(coll) = collation { format!("{sign}{coll}") } else { format!("{sign}B") } }) .collect(); ( "SorterOpen", *cursor_id as i64, *columns as i64, 0, Value::build_text(format!("k({},{})", order_and_collations.len(), to_print.join(","))), 0, format!("cursor={cursor_id}"), ) } Insn::SorterData { cursor_id, dest_reg, pseudo_cursor, } => ( "SorterData", *cursor_id as i64, *dest_reg as i64, *pseudo_cursor as i64, Value::build_text(""), 0, format!("r[{dest_reg}]=data"), ), Insn::SorterInsert { cursor_id, record_reg, } => ( "SorterInsert", *cursor_id as i64, *record_reg as i64, 0, Value::from_i64(0), 0, format!("key=r[{record_reg}]"), ), Insn::SorterSort { cursor_id, pc_if_empty, } => ( "SorterSort", *cursor_id as i64, pc_if_empty.as_debug_int() as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::SorterNext { cursor_id, pc_if_next, } => ( "SorterNext", *cursor_id as i64, pc_if_next.as_debug_int() as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::SorterCompare { cursor_id, pc_when_nonequal, sorted_record_reg, num_regs, } => ( "SorterCompare", *cursor_id as i64, pc_when_nonequal.as_debug_int() as i64, *sorted_record_reg as i64, Value::build_text(num_regs.to_string()), 0, "".to_string(), ), Insn::RowSetAdd { rowset_reg, value_reg, } => ( "RowSetAdd", *rowset_reg as i64, *value_reg as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::RowSetRead { rowset_reg, pc_if_empty, dest_reg, } => ( "RowSetRead", *rowset_reg as i64, pc_if_empty.as_debug_int() as i64, *dest_reg as i64, Value::build_text(""), 0, "".to_string(), ), Insn::RowSetTest { rowset_reg, pc_if_found, value_reg, batch, } => ( "RowSetTest", *rowset_reg as i64, pc_if_found.as_debug_int() as i64, *value_reg as i64, Value::build_text(batch.to_string()), 0, "".to_string(), ), Insn::Function { constant_mask, start_reg, dest, func, } => ( "Function", *constant_mask as i64, *start_reg as i64, *dest as i64, { let s = if matches!(&func.func, Func::Scalar(ScalarFunc::Like)) { format!("like({})", func.arg_count) } else { func.func.to_string() }; Value::build_text(s) }, 0, if func.arg_count == 0 { format!("r[{dest}]=func()") } else if *start_reg == *start_reg + func.arg_count - 1 { format!("r[{dest}]=func(r[{start_reg}])") } else { format!( "r[{}]=func(r[{}..{}])", dest, start_reg, start_reg + func.arg_count - 1 ) }, ), Insn::InitCoroutine { yield_reg, jump_on_definition, start_offset, } => ( "InitCoroutine", *yield_reg as i64, jump_on_definition.as_debug_int() as i64, start_offset.as_debug_int() as i64, Value::build_text(""), 0, "".to_string(), ), Insn::EndCoroutine { yield_reg } => ( "EndCoroutine", *yield_reg as i64, 0, 0, Value::build_text(""), 0, "".to_string(), ), Insn::Yield { yield_reg, end_offset, .. } => ( "Yield", *yield_reg as i64, end_offset.as_debug_int() as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::Insert { cursor, key_reg, record_reg, flag, table_name, } => ( "Insert", *cursor as i64, *record_reg as i64, *key_reg as i64, Value::build_text(table_name.clone()), flag.0 as i64, format!("intkey=r[{key_reg}] data=r[{record_reg}]"), ), Insn::Delete { cursor_id, table_name, .. } => ( "Delete", *cursor_id as i64, 0, 0, Value::build_text(table_name.clone()), 0, "".to_string(), ), Insn::IdxDelete { cursor_id, start_reg, num_regs, raise_error_if_no_matching_entry, } => ( "IdxDelete", *cursor_id as i64, *start_reg as i64, *num_regs as i64, Value::build_text(""), *raise_error_if_no_matching_entry as i64, "".to_string(), ), Insn::NewRowid { cursor, rowid_reg, prev_largest_reg, } => ( "NewRowid", *cursor as i64, *rowid_reg as i64, *prev_largest_reg as i64, Value::build_text(""), 0, format!("r[{rowid_reg}]=rowid"), ), Insn::MustBeInt { reg } => ( "MustBeInt", *reg as i64, 0, 0, Value::build_text(""), 0, "".to_string(), ), Insn::SoftNull { reg } => ( "SoftNull", *reg as i64, 0, 0, Value::build_text(""), 0, "".to_string(), ), Insn::NoConflict { cursor_id, target_pc, record_reg, num_regs, } => { let key = if *num_regs > 0 { format!("key=r[{}..{}]", record_reg, record_reg + num_regs - 1) } else { format!("key=r[{record_reg}]") }; ( "NoConflict", *cursor_id as i64, target_pc.as_debug_int() as i64, *record_reg as i64, Value::build_text(format!("{num_regs}")), 0, key, ) } Insn::NotExists { cursor, rowid_reg, target_pc, } => ( "NotExists", *cursor as i64, target_pc.as_debug_int() as i64, *rowid_reg as i64, Value::build_text(""), 0, "".to_string(), ), Insn::OffsetLimit { limit_reg, combined_reg, offset_reg, } => ( "OffsetLimit", *limit_reg as i64, *combined_reg as i64, *offset_reg as i64, Value::build_text(""), 0, format!( "if r[{limit_reg}]>0 then r[{combined_reg}]=r[{limit_reg}]+max(0,r[{offset_reg}]) else r[{combined_reg}]=(-1)" ), ), Insn::OpenWrite { cursor_id, root_page, db, .. } => ( "OpenWrite", *cursor_id as i64, match root_page { RegisterOrLiteral::Literal(i) => *i as _, RegisterOrLiteral::Register(i) => *i as _, }, *db as i64, Value::build_text(""), 0, format!("root={root_page}; iDb={db}"), ), Insn::Copy { src_reg, dst_reg, extra_amount, } => ( "Copy", *src_reg as i64, *dst_reg as i64, *extra_amount as i64, Value::build_text(""), 0, format!("r[{dst_reg}]=r[{src_reg}]"), ), Insn::CreateBtree { db, root, flags } => ( "CreateBtree", *db as i64, *root as i64, flags.get_flags() as i64, Value::build_text(""), 0, format!("r[{}]=root iDb={} flags={}", root, db, flags.get_flags()), ), Insn::IndexMethodCreate { db, cursor_id } => ( "IndexMethodCreate", *db as i64, *cursor_id as i64, 0, Value::build_text(""), 0, "".to_string() ), Insn::IndexMethodDestroy { db, cursor_id } => ( "IndexMethodDestroy", *db as i64, *cursor_id as i64, 0, Value::build_text(""), 0, "".to_string() ), Insn::IndexMethodOptimize { db, cursor_id } => ( "IndexMethodOptimize", *db as i64, *cursor_id as i64, 0, Value::build_text(""), 0, "".to_string() ), Insn::IndexMethodQuery { db, cursor_id, start_reg, .. } => ( "IndexMethodQuery", *db as i64, *cursor_id as i64, *start_reg as i64, Value::build_text(""), 0, "".to_string() ), Insn::Destroy { db, root, former_root_reg, is_temp, } => ( "Destroy", *root, *former_root_reg as i64, *is_temp as i64, Value::build_text(""), 0, format!( "root iDb={db} former_root={former_root_reg} is_temp={is_temp}" ), ), Insn::ResetSorter { cursor_id } => ( "ResetSorter", *cursor_id as i64, 0, 0, Value::build_text(""), 0, format!("cursor={cursor_id}"), ), Insn::DropTable { db, _p2, _p3, table_name, } => ( "DropTable", *db as i64, 0, 0, Value::build_text(table_name.clone()), 0, format!("DROP TABLE {table_name}"), ), Insn::DropTrigger { db, trigger_name } => ( "DropTrigger", *db as i64, 0, 0, Value::build_text(trigger_name.clone()), 0, format!("DROP TRIGGER {trigger_name}"), ), Insn::DropType { db, type_name } => ( "DropType", *db as i64, 0, 0, Value::build_text(type_name.clone()), 0, format!("DROP TYPE {type_name}"), ), Insn::AddType { db, sql } => ( "AddType", *db as i64, 0, 0, Value::build_text(sql.clone()), 0, "ADD TYPE".to_string(), ), Insn::DropView { db, view_name } => ( "DropView", *db as i64, 0, 0, Value::build_text(view_name.clone()), 0, format!("DROP VIEW {view_name}"), ), Insn::DropIndex { db: _, index } => ( "DropIndex", 0, 0, 0, Value::build_text(index.name.clone()), 0, format!("DROP INDEX {}", index.name), ), Insn::Close { cursor_id } => ( "Close", *cursor_id as i64, 0, 0, Value::build_text(""), 0, "".to_string(), ), Insn::Last { cursor_id, pc_if_empty, } => ( "Last", *cursor_id as i64, pc_if_empty.as_debug_int() as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::IsNull { reg, target_pc } => ( "IsNull", *reg as i64, target_pc.as_debug_int() as i64, 0, Value::build_text(""), 0, format!("if (r[{}]==NULL) goto {}", reg, target_pc.as_debug_int()), ), Insn::ParseSchema { db, where_clause } => ( "ParseSchema", *db as i64, 0, 0, Value::build_text(where_clause.clone().unwrap_or_else(|| "NULL".to_string())), 0, where_clause.clone().unwrap_or_else(|| "NULL".to_string()), ), Insn::PopulateMaterializedViews { cursors } => ( "PopulateMaterializedViews", 0, 0, 0, Value::Null, cursors.len() as i64, "".to_string(), ), Insn::Prev { cursor_id, pc_if_prev, } => ( "Prev", *cursor_id as i64, pc_if_prev.as_debug_int() as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::ShiftRight { lhs, rhs, dest } => ( "ShiftRight", *rhs as i64, *lhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=r[{lhs}] >> r[{rhs}]"), ), Insn::ShiftLeft { lhs, rhs, dest } => ( "ShiftLeft", *rhs as i64, *lhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=r[{lhs}] << r[{rhs}]"), ), Insn::AddImm { register, value } => ( "AddImm", *register as i64, *value, 0, Value::build_text(""), 0, format!("r[{register}]=r[{register}]+{value}"), ), Insn::Variable { index, dest } => ( "Variable", usize::from(*index) as i64, *dest as i64, 0, Value::build_text(""), 0, format!("r[{}]=parameter({})", *dest, *index), ), Insn::ZeroOrNull { rg1, rg2, dest } => ( "ZeroOrNull", *rg1 as i64, *dest as i64, *rg2 as i64, Value::build_text(""), 0, format!( "((r[{rg1}]=NULL)|(r[{rg2}]=NULL)) ? r[{dest}]=NULL : r[{dest}]=0" ), ), Insn::Not { reg, dest } => ( "Not", *reg as i64, *dest as i64, 0, Value::build_text(""), 0, format!("r[{dest}]=!r[{reg}]"), ), Insn::IsTrue { reg, dest, null_value, invert } => ( "IsTrue", *reg as i64, *dest as i64, if *null_value { 1 } else { 0 }, Value::build_text(""), if *invert { 1 } else { 0 }, format!("r[{dest}] = IsTrue(r[{reg}], null={}, invert={})", *null_value as i64, *invert as i64), ), Insn::Concat { lhs, rhs, dest } => ( "Concat", *rhs as i64, *lhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=r[{lhs}] + r[{rhs}]"), ), Insn::And { lhs, rhs, dest } => ( "And", *rhs as i64, *lhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=(r[{lhs}] && r[{rhs}])"), ), Insn::Or { lhs, rhs, dest } => ( "Or", *rhs as i64, *lhs as i64, *dest as i64, Value::build_text(""), 0, format!("r[{dest}]=(r[{lhs}] || r[{rhs}])"), ), Insn::Noop => ("Noop", 0, 0, 0, Value::build_text(""), 0, String::new()), Insn::PageCount { db, dest } => ( "Pagecount", *db as i64, *dest as i64, 0, Value::build_text(""), 0, "".to_string(), ), Insn::ReadCookie { db, dest, cookie } => ( "ReadCookie", *db as i64, *dest as i64, *cookie as i64, Value::build_text(""), 0, "".to_string(), ), Insn::Filter{cursor_id, target_pc, key_reg, num_keys} => ( "Filter", *cursor_id as i64, target_pc.as_debug_int() as i64, *key_reg as i64, Value::build_text(""), *num_keys as i64, format!("if !bloom_filter(r[{}..{}]) goto {}", key_reg, key_reg + num_keys, target_pc.as_debug_int()), ), Insn::FilterAdd{cursor_id, key_reg, num_keys} => ( "FilterAdd", *cursor_id as i64, *key_reg as i64, *num_keys as i64, Value::build_text(""), 0, format!("bloom_filter_add(r[{}..{}])", key_reg, key_reg + num_keys), ), Insn::SetCookie { db, cookie, value, p5, } => ( "SetCookie", *db as i64, *cookie as i64, *value as i64, Value::build_text(""), *p5 as i64, "".to_string(), ), Insn::AutoCommit { auto_commit, rollback, } => ( "AutoCommit", *auto_commit as i64, *rollback as i64, 0, Value::build_text(""), 0, format!("auto_commit={auto_commit}, rollback={rollback}"), ), Insn::Savepoint { op, name } => ( "Savepoint", 0, 0, 0, Value::build_text(name.clone()), 0, format!("op={op:?}, name={name}"), ), Insn::OpenEphemeral { cursor_id, is_table, } => ( "OpenEphemeral", *cursor_id as i64, *is_table as i64, 0, Value::build_text(""), 0, format!( "cursor={} is_table={}", cursor_id, if *is_table { "true" } else { "false" } ), ), Insn::OpenAutoindex { cursor_id } => ( "OpenAutoindex", *cursor_id as i64, 0, 0, Value::build_text(""), 0, format!("cursor={cursor_id}"), ), Insn::OpenDup { new_cursor_id, original_cursor_id } => ( "OpenDup", *new_cursor_id as i64, *original_cursor_id as i64, 0, Value::build_text(""), 0, format!("new_cursor={new_cursor_id}, original_cursor={original_cursor_id}"), ), Insn::Once { target_pc_when_reentered, } => ( "Once", target_pc_when_reentered.as_debug_int() as i64, 0, 0, Value::build_text(""), 0, format!("goto {}", target_pc_when_reentered.as_debug_int()), ), Insn::BeginSubrtn { dest, dest_end } => ( "BeginSubrtn", *dest as i64, dest_end.map_or(0, |end| end as i64), 0, Value::build_text(""), 0, dest_end.map_or(format!("r[{dest}]=NULL"), |end| { format!("r[{dest}..{end}]=NULL") }), ), Insn::NotFound { cursor_id, target_pc, record_reg, .. } | Insn::Found { cursor_id, target_pc, record_reg, .. } => ( if matches!(insn, Insn::NotFound { .. }) { "NotFound" } else { "Found" }, *cursor_id as i64, target_pc.as_debug_int() as i64, *record_reg as i64, Value::build_text(""), 0, format!( "if {}found goto {}", if matches!(insn, Insn::NotFound { .. }) { "not " } else { "" }, target_pc.as_debug_int() ), ), Insn::Affinity { start_reg, count, affinities, } => ( "Affinity", *start_reg as i64, count.get() as i64, 0, Value::build_text(""), 0, format!( "r[{}..{}] = {}", start_reg, start_reg + count.get(), affinities .chars() .map(|a| a.to_string()) .collect::>() .join(", ") ), ), Insn::Count { cursor_id, target_reg, exact, } => ( "Count", *cursor_id as i64, *target_reg as i64, if *exact { 0 } else { 1 }, Value::build_text(""), 0, "".to_string(), ), Insn::Int64 { _p1, out_reg, _p3, value, } => ( "Int64", 0, *out_reg as i64, 0, Value::from_i64(*value), 0, format!("r[{}]={}", *out_reg, *value), ), Insn::IntegrityCk { db, max_errors, roots, message_register, } => ( "IntegrityCk", *max_errors as i64, 0, 0, Value::build_text(""), 0, format!("db={db} roots={roots:?} message_register={message_register}"), ), Insn::RowData { cursor_id, dest } => ( "RowData", *cursor_id as i64, *dest as i64, 0, Value::build_text(""), 0, format!("r[{}] = data", *dest), ), Insn::Cast { reg, affinity } => ( "Cast", *reg as i64, 0, 0, Value::build_text(""), 0, format!("affinity(r[{}]={:?})", *reg, affinity), ), Insn::RenameTable { db: _, from, to } => ( "RenameTable", 0, 0, 0, Value::build_text(""), 0, format!("rename_table({from}, {to})"), ), Insn::DropColumn { db: _, table, column_index } => ( "DropColumn", 0, 0, 0, Value::build_text(""), 0, format!("drop_column({table}, {column_index})"), ), Insn::AddColumn { db: _, table, column, .. } => ( "AddColumn", 0, 0, 0, Value::build_text(""), 0, format!("add_column({table}, {column:?})"), ), Insn::AlterColumn { db: _, table, column_index, definition: column, rename } => ( "AlterColumn", 0, 0, 0, Value::build_text(""), 0, format!("alter_column({table}, {column_index}, {column:?}, {rename:?})"), ), Insn::MaxPgcnt { db, dest, new_max } => ( "MaxPgcnt", *db as i64, *dest as i64, *new_max as i64, Value::build_text(""), 0, format!("r[{dest}]=max_page_count(db[{db}],{new_max})"), ), Insn::JournalMode { db, dest, new_mode } => ( "JournalMode", *db as i64, *dest as i64, 0, Value::build_text(new_mode.clone().unwrap_or(String::new())), 0, format!("r[{dest}]=journal_mode(db[{db}]{})", new_mode.as_ref().map_or(String::new(), |m| format!(",'{m}'"))), ), Insn::CollSeq { reg, collation } => ( "CollSeq", reg.unwrap_or(0) as i64, 0, 0, Value::build_text(collation.to_string()), 0, format!("collation={collation}"), ), Insn::IfNeg { reg, target_pc } => ( "IfNeg", *reg as i64, target_pc.as_debug_int() as i64, 0, Value::build_text(""), 0, format!("if (r[{}] < 0) goto {}", reg, target_pc.as_debug_int()), ), Insn::Explain { p1, p2, detail } => ( "Explain", *p1 as i64, p2.as_ref().map(|p| *p).unwrap_or(0) as i64, 0, Value::build_text(detail.clone()), 0, String::new(), ), Insn::MemMax { dest_reg, src_reg } => ( "MemMax", *dest_reg as i64, *src_reg as i64, 0, Value::build_text(""), 0, format!("r[{dest_reg}]=Max(r[{dest_reg}],r[{src_reg}])"), ), Insn::Sequence{ cursor_id, target_reg} => ( "Sequence", *cursor_id as i64, *target_reg as i64, 0, Value::build_text(""), 0, String::new(), ), Insn::SequenceTest{ cursor_id, target_pc, value_reg } => ( "SequenceTest", *cursor_id as i64, target_pc.as_debug_int() as i64, *value_reg as i64, Value::build_text(""), 0, String::new(), ), Insn::FkCounter{increment_value, deferred } => ( "FkCounter", *increment_value as i64, *deferred as i64, 0, Value::build_text(""), 0, String::new(), ), Insn::FkIfZero{target_pc, deferred } => ( "FkIfZero", target_pc.as_debug_int() as i64, *deferred as i64, 0, Value::build_text(""), 0, String::new(), ), Insn::FkCheck{ deferred } => ( "FkCheck", *deferred as i64, 0, 0, Value::build_text(""), 0, String::new(), ), Insn::HashBuild { data } => { let payload_info = if let Some(p_reg) = data.payload_start_reg { format!(" payload=r[{}]..r[{}]", p_reg, p_reg + data.num_payload - 1) } else { String::new() }; ( "HashBuild", data.cursor_id as i64, data.key_start_reg as i64, data.num_keys as i64, Value::build_text(format!("r=[{}] budget={}{payload_info}", data.hash_table_id, data.mem_budget)), 0, String::new(), ) } Insn::HashBuildFinalize{hash_table_id: hash_table_reg} => ( "HashBuildFinalize", *hash_table_reg as i64, 0, 0, Value::build_text(""), 0, String::new(), ), Insn::HashProbe{hash_table_id: hash_table_reg, key_start_reg, num_keys, dest_reg, target_pc, payload_dest_reg, num_payload, probe_rowid_reg: _} => { let payload_info = if let Some(p_reg) = payload_dest_reg { format!(" payload=r[{}]..r[{}]", p_reg, p_reg + num_payload - 1) } else { String::new() }; ( "HashProbe", *hash_table_reg as i64, *key_start_reg as i64, *num_keys as i64, Value::build_text(format!("r[{}]={}{}", dest_reg, target_pc.as_debug_int(), payload_info)), 0, String::new(), ) } Insn::HashNext{hash_table_id: hash_table_reg, dest_reg, target_pc, payload_dest_reg, num_payload} => { let payload_info = if let Some(p_reg) = payload_dest_reg { format!(" payload=r[{}]..r[{}]", p_reg, p_reg + num_payload - 1) } else { String::new() }; ( "HashNext", *hash_table_reg as i64, *dest_reg as i64, target_pc.as_debug_int() as i64, Value::build_text(payload_info), 0, String::new(), ) } Insn::HashDistinct { data } => ( "HashDistinct", data.hash_table_id as i64, data.key_start_reg as i64, data.num_keys as i64, Value::build_text(format!("jmp={}", data.target_pc.as_debug_int())), 0, String::new(), ), Insn::HashClose{hash_table_id: hash_table_reg} => ( "HashClose", *hash_table_reg as i64, 0, 0, Value::build_text(""), 0, String::new(), ), Insn::HashClear { hash_table_id: hash_table_reg } => ( "HashClear", *hash_table_reg as i64, 0, 0, Value::build_text(""), 0, String::new(), ), Insn::HashMarkMatched { hash_table_id } => ( "HashMarkMatched", *hash_table_id as i64, 0, 0, Value::build_text(""), 0, String::new(), ), Insn::HashResetMatched { hash_table_id } => ( "HashResetMatched", *hash_table_id as i64, 0, 0, Value::build_text(""), 0, String::new(), ), Insn::HashScanUnmatched { hash_table_id, dest_reg, target_pc, payload_dest_reg, num_payload } => { let payload_info = if let Some(p_reg) = payload_dest_reg { format!(" payload=r[{}]..r[{}]", p_reg, p_reg + num_payload - 1) } else { String::new() }; ( "HashScanUnmatched", *hash_table_id as i64, *dest_reg as i64, target_pc.as_debug_int() as i64, Value::build_text(""), 0, format!("hash_table_id={hash_table_id}{payload_info}"), ) }, Insn::HashNextUnmatched { hash_table_id, dest_reg, target_pc, payload_dest_reg, num_payload } => { let payload_info = if let Some(p_reg) = payload_dest_reg { format!(" payload=r[{}]..r[{}]", p_reg, p_reg + num_payload - 1) } else { String::new() }; ( "HashNextUnmatched", *hash_table_id as i64, *dest_reg as i64, target_pc.as_debug_int() as i64, Value::build_text(""), 0, format!("hash_table_id={hash_table_id}{payload_info}"), ) }, Insn::HashGraceInit { hash_table_id, target_pc } => { ( "HashGraceInit", *hash_table_id as i64, 0, target_pc.as_debug_int() as i64, Value::build_text(""), 0, format!("hash_table_id={hash_table_id}"), ) }, Insn::HashGraceLoadPartition { hash_table_id, target_pc } => { ( "HashGraceLoadPart", *hash_table_id as i64, 0, target_pc.as_debug_int() as i64, Value::build_text(""), 0, format!("hash_table_id={hash_table_id}"), ) }, Insn::HashGraceNextProbe { hash_table_id, key_start_reg, num_keys, probe_rowid_dest, target_pc } => { ( "HashGraceNextProbe", *hash_table_id as i64, *key_start_reg as i64, target_pc.as_debug_int() as i64, Value::build_text(""), 0, format!("hash_table_id={hash_table_id} keys=r[{}]..r[{}] probe_rowid_dest=r[{probe_rowid_dest}]", key_start_reg, *key_start_reg + *num_keys - 1), ) }, Insn::HashGraceAdvancePartition { hash_table_id, target_pc } => { ( "HashGraceAdvPart", *hash_table_id as i64, 0, target_pc.as_debug_int() as i64, Value::build_text(""), 0, format!("hash_table_id={hash_table_id}"), ) }, Insn::VacuumInto { dest_path } => ( "VacuumInto", 0, 0, 0, Value::build_text(dest_path.to_string()), 0, format!("dest={dest_path}"), ), Insn::InitCdcVersion { cdc_table_name, version, cdc_mode } => ( "InitCdcVersion", 0, 0, 0, Value::build_text(format!("{cdc_table_name}={version}")), 0, format!("ensure turso_cdc_version({cdc_table_name}, {version}); set cdc={cdc_mode}"), ), } } pub fn insn_to_row_with_comment( program: &PreparedProgram, insn: &Insn, manual_comment: Option<&str>, ) -> (&'static str, i64, i64, i64, Value, i64, String) { let (opcode, p1, p2, p3, p4, p5, comment) = insn_to_row(program, insn); ( opcode, p1, p2, p3, p4, p5, manual_comment.map_or(comment.to_string(), |mc| format!("{comment}; {mc}")), ) } pub fn insn_to_str( program: &PreparedProgram, addr: InsnReference, insn: &Insn, indent: String, manual_comment: Option<&str>, ) -> String { let (opcode, p1, p2, p3, p4, p5, comment) = insn_to_row(program, insn); format!( "{:<4} {:<17} {:<4} {:<4} {:<4} {:<13} {:<2} {}", addr, &(indent + opcode), p1, p2, p3, p4.to_string(), p5, manual_comment.map_or(comment.to_string(), |mc| format!("{comment}; {mc}")) ) } ================================================ FILE: core/vdbe/hash_table.rs ================================================ use crate::turso_assert; use crate::{ error::LimboError, io::{Buffer, Completion, TempFile, IO}, io_yield_one, return_if_io, storage::sqlite3_ondisk::{read_varint, read_varint_partial, varint_len, write_varint}, sync::{ atomic::{self, AtomicUsize}, Arc, RwLock, }, translate::collate::CollationSeq, types::{IOCompletions, IOResult, Value, ValueRef}, vdbe::metrics::HashJoinMetrics, CompletionError, Numeric, Result, }; use branches::{mark_unlikely, unlikely}; use rapidhash::fast::RapidHasher; use std::cmp::Ordering; use std::hash::Hasher; use std::{cell::RefCell, collections::VecDeque}; use turso_macros::{turso_assert_eq, AtomicEnum}; const DEFAULT_SEED: u64 = 1337; // set to a *very* small 32KB, intentionally to trigger frequent spilling during tests #[cfg(debug_assertions)] pub const DEFAULT_MEM_BUDGET: usize = 32 * 1024; /// 64MB default memory budget for hash joins. /// TODO: make configurable via PRAGMA #[cfg(not(debug_assertions))] pub const DEFAULT_MEM_BUDGET: usize = 64 * 1024 * 1024; const DEFAULT_BUCKETS: usize = 1024; /// Minimum number of partitions for grace hash join. pub const MIN_PARTITIONS: usize = 16; /// Maximum number of partitions for adaptive partitioning. pub const MAX_PARTITIONS: usize = 128; const NULL_HASH: u8 = 0; const INT_HASH: u8 = 1; const FLOAT_HASH: u8 = 2; const TEXT_HASH: u8 = 3; const BLOB_HASH: u8 = 4; #[inline] /// Hash text case-insensitively without allocation (ASCII-only for SQLite NOCASE). /// SQLite's NOCASE collation only considers ASCII case, so to_ascii_lowercase() is correct. fn hash_text_nocase(hasher: &mut impl Hasher, text: &str) { for byte in text.bytes() { hasher.write_u8(byte.to_ascii_lowercase()); } } /// Hash function for join keys using rapidhash /// Takes collation into account when hashing text values fn hash_join_key(key_values: &[ValueRef], collations: &[CollationSeq]) -> u64 { let mut hasher = RapidHasher::new(DEFAULT_SEED); for (idx, value) in key_values.iter().enumerate() { match value { ValueRef::Null => { hasher.write_u8(NULL_HASH); } ValueRef::Numeric(Numeric::Integer(i)) => { // Hash integers in the same bucket as numerically equivalent REALs so e.g. 10 and 10.0 have the same hash. let f = *i as f64; if (f as i64) == *i && f.is_finite() { hasher.write_u8(FLOAT_HASH); let bits = normalized_f64_bits(f); hasher.write(&bits.to_le_bytes()); } else { // Fallback to the integer domain when the float representation would lose precision. hasher.write_u8(INT_HASH); hasher.write_i64(*i); } } ValueRef::Numeric(Numeric::Float(f)) => { hasher.write_u8(FLOAT_HASH); let bits = normalized_f64_bits(f64::from(*f)); hasher.write(&bits.to_le_bytes()); } ValueRef::Text(text) => { let collation = collations.get(idx).unwrap_or(&CollationSeq::Binary); hasher.write_u8(TEXT_HASH); match collation { CollationSeq::NoCase => { hash_text_nocase(&mut hasher, text.as_str()); } CollationSeq::Rtrim => { let trimmed = text.as_str().trim_end(); hasher.write(trimmed.as_bytes()); } CollationSeq::Binary | CollationSeq::Unset => { hasher.write(text.as_bytes()); } } } ValueRef::Blob(blob) => { hasher.write_u8(BLOB_HASH); hasher.write(blob); } } } hasher.finish() } /// Normalize signed zero so 0.0 and -0.0 hash the same. #[inline] fn normalized_f64_bits(f: f64) -> u64 { if f == 0.0 { 0.0f64.to_bits() } else { f.to_bits() } } /// Check if any of the key values is NULL. /// Rows with NULL join keys should be skipped in hash joins since NULL != NULL in SQL. fn has_null_key(key_values: &[Value]) -> bool { key_values.iter().any(|v| matches!(v, Value::Null)) } /// Check if any of the key value refs is NULL. fn has_null_key_ref(key_values: &[ValueRef]) -> bool { key_values.iter().any(|v| matches!(v, ValueRef::Null)) } /// Check if two key value arrays are equal, taking collation into account. fn keys_equal(key1: &[Value], key2: &[ValueRef], collations: &[CollationSeq]) -> bool { if key1.len() != key2.len() { return false; } for (idx, (v1, v2)) in key1.iter().zip(key2.iter()).enumerate() { let collation = collations.get(idx).copied().unwrap_or(CollationSeq::Binary); if !values_equal(v1.as_ref(), *v2, collation) { return false; } } true } /// Check if two values are equal, using the specified collation for text comparison. /// NOTE: In SQL, NULL = NULL evaluates to NULL (falsy), so this returns false for NULL comparisons. fn values_equal(v1: ValueRef, v2: ValueRef, collation: CollationSeq) -> bool { match (v1, v2) { // NULL = NULL is false in SQL (actually NULL, which is falsy) (ValueRef::Null, _) | (_, ValueRef::Null) => false, (ValueRef::Numeric(n1), ValueRef::Numeric(n2)) => { ValueRef::Numeric(n1) == ValueRef::Numeric(n2) } (ValueRef::Blob(b1), ValueRef::Blob(b2)) => b1 == b2, (ValueRef::Text(t1), ValueRef::Text(t2)) => { // Use collation for text comparison collation.compare_strings(t1.as_str(), t2.as_str()) == Ordering::Equal } _ => false, } } /// DISTINCT equality: NULLs compare equal to NULL. fn values_equal_distinct(v1: ValueRef, v2: ValueRef, collation: CollationSeq) -> bool { match (v1, v2) { (ValueRef::Null, ValueRef::Null) => true, (ValueRef::Null, _) | (_, ValueRef::Null) => false, _ => values_equal(v1, v2, collation), } } fn keys_equal_distinct(key1: &[Value], key2: &[ValueRef], collations: &[CollationSeq]) -> bool { if key1.len() != key2.len() { return false; } for (idx, (v1, v2)) in key1.iter().zip(key2.iter()).enumerate() { let collation = collations.get(idx).copied().unwrap_or(CollationSeq::Binary); if !values_equal_distinct(v1.as_ref(), *v2, collation) { return false; } } true } /// State machine states for hash table operations. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HashTableState { Building, Probing, Spilled, GraceProcessing, Closed, } /// A probe entry returned by `grace_next_probe_entry()`. /// The VDBE writes these to registers for HashProbe to use. #[derive(Debug)] pub struct GraceProbeEntry { pub key_values: Vec, pub probe_rowid: i64, } /// A single entry in a hash table bucket. #[derive(Debug, Clone)] pub struct HashEntry { /// Hash value of the join keys. pub hash: u64, /// The join key values. pub key_values: Vec, /// The rowid of the row in the build table. /// During probe phase, we'll use SeekRowid to fetch the full row /// (unless payload_values contains all needed columns). pub rowid: i64, /// Optional payload values - columns from the build table that are stored /// directly in the hash entry to avoid SeekRowid during probe phase. /// When populated, these are the result columns needed from the build table, /// stored in column index order as specified during hash table construction. pub payload_values: Vec, } #[derive(Debug)] pub(crate) struct PendingHashInsert { pub(crate) key_values: Vec, pub(crate) rowid: i64, pub(crate) payload_values: Vec, } #[derive(Debug)] pub(crate) enum HashInsertResult { Done, IO { io: IOCompletions, pending: PendingHashInsert, }, } impl HashEntry { fn new(hash: u64, key_values: Vec, rowid: i64) -> Self { Self { hash, key_values, rowid, payload_values: Vec::new(), } } fn new_with_payload( hash: u64, key_values: Vec, rowid: i64, payload_values: Vec, ) -> Self { Self { hash, key_values, rowid, payload_values, } } /// Returns true if this entry has payload values stored. pub fn has_payload(&self) -> bool { !self.payload_values.is_empty() } /// Get the size of this entry in bytes (approximate). /// This is a lightweight estimate for memory budgeting, not a precise measurement. fn size_bytes(&self) -> usize { Self::size_from_values(&self.key_values, &self.payload_values) } fn size_from_values(key_values: &[Value], payload_values: &[Value]) -> usize { let value_size = |v: &Value| match v { Value::Null => 1, Value::Numeric(_) => 8, Value::Text(t) => t.as_str().len(), Value::Blob(b) => b.len(), }; let key_size: usize = key_values.iter().map(value_size).sum(); let payload_size: usize = payload_values.iter().map(value_size).sum(); key_size + payload_size + 8 + 8 // +8 for hash, +8 for rowid } /// Calculate the serialized size of a single Value. #[inline] fn value_serialized_size(v: &Value) -> usize { 1 + match v { Value::Null => 0, Value::Numeric(_) => 8, Value::Text(t) => { let len = t.as_str().len(); varint_len(len as u64) + len } Value::Blob(b) => varint_len(b.len() as u64) + b.len(), } } /// Calculate the exact serialized size of this entry. fn serialized_size(&self) -> usize { 8 + 8 // hash + rowid + varint_len(self.key_values.len() as u64) + self.key_values.iter().map(Self::value_serialized_size).sum::() + varint_len(self.payload_values.len() as u64) + self.payload_values.iter().map(Self::value_serialized_size).sum::() } /// Serialize this entry directly to a slice, returns bytes written. /// The caller must ensure the slice is large enough (use serialized_size()). fn serialize_to_slice(&self, buf: &mut [u8]) -> usize { let mut offset = 0; // Write hash and rowid buf[offset..offset + 8].copy_from_slice(&self.hash.to_le_bytes()); offset += 8; buf[offset..offset + 8].copy_from_slice(&self.rowid.to_le_bytes()); offset += 8; // Write number of keys and key values offset += write_varint(&mut buf[offset..], self.key_values.len() as u64); for value in &self.key_values { offset += Self::serialize_value_to_slice(value, &mut buf[offset..]); } // Write number of payload values and payload values offset += write_varint(&mut buf[offset..], self.payload_values.len() as u64); for value in &self.payload_values { offset += Self::serialize_value_to_slice(value, &mut buf[offset..]); } offset } /// Helper to serialize a single Value directly to a slice. Returns bytes written. #[inline] fn serialize_value_to_slice(value: &Value, buf: &mut [u8]) -> usize { let mut offset = 0; match value { Value::Null => { buf[offset] = NULL_HASH; offset += 1; } Value::Numeric(Numeric::Integer(i)) => { buf[offset] = INT_HASH; offset += 1; buf[offset..offset + 8].copy_from_slice(&i.to_le_bytes()); offset += 8; } Value::Numeric(Numeric::Float(f)) => { buf[offset] = FLOAT_HASH; offset += 1; buf[offset..offset + 8].copy_from_slice(&f64::from(*f).to_le_bytes()); offset += 8; } Value::Text(t) => { buf[offset] = TEXT_HASH; offset += 1; let bytes = t.as_str().as_bytes(); offset += write_varint(&mut buf[offset..], bytes.len() as u64); buf[offset..offset + bytes.len()].copy_from_slice(bytes); offset += bytes.len(); } Value::Blob(b) => { buf[offset] = BLOB_HASH; offset += 1; offset += write_varint(&mut buf[offset..], b.len() as u64); buf[offset..offset + b.len()].copy_from_slice(b); offset += b.len(); } } offset } /// Serialize this entry to bytes for disk storage. /// Format: [hash:8][rowid:8][num_keys:varint][keys...][num_payload:varint][payload...] /// Each value is: [type:1][len:varint (for text/blob)][data] fn serialize(&self, buf: &mut Vec) { buf.extend_from_slice(&self.hash.to_le_bytes()); buf.extend_from_slice(&self.rowid.to_le_bytes()); // Write number of keys and key values let varint_buf = &mut [0u8; 9]; let len = write_varint(varint_buf, self.key_values.len() as u64); buf.extend_from_slice(&varint_buf[..len]); for value in &self.key_values { Self::serialize_value(value, buf, varint_buf); } // Write number of payload values and payload values let len = write_varint(varint_buf, self.payload_values.len() as u64); buf.extend_from_slice(&varint_buf[..len]); for value in &self.payload_values { Self::serialize_value(value, buf, varint_buf); } } /// Helper to serialize a single Value to bytes. fn serialize_value(value: &Value, buf: &mut Vec, varint_buf: &mut [u8; 9]) { match value { Value::Null => { buf.push(NULL_HASH); } Value::Numeric(Numeric::Integer(i)) => { buf.push(INT_HASH); buf.extend_from_slice(&i.to_le_bytes()); } Value::Numeric(Numeric::Float(f)) => { buf.push(FLOAT_HASH); buf.extend_from_slice(&f64::from(*f).to_le_bytes()); } Value::Text(t) => { buf.push(TEXT_HASH); let bytes = t.as_str().as_bytes(); let len = write_varint(varint_buf, bytes.len() as u64); buf.extend_from_slice(&varint_buf[..len]); buf.extend_from_slice(bytes); } Value::Blob(b) => { buf.push(BLOB_HASH); let len = write_varint(varint_buf, b.len() as u64); buf.extend_from_slice(&varint_buf[..len]); buf.extend_from_slice(b); } } } /// Deserialize an entry from bytes, returning (entry, bytes_consumed) or error. fn deserialize(buf: &[u8]) -> Result<(Self, usize)> { if unlikely(buf.len() < 16) { return Err(LimboError::Corrupt( "HashEntry: buffer too small for header".to_string(), )); } // buffer len checked above let hash = u64::from_le_bytes(buf[0..8].try_into().expect("expect 8 bytes")); let rowid = i64::from_le_bytes(buf[8..16].try_into().expect("expect 8 bytes")); let mut offset = 16; // Read number of keys and key values let (num_keys, varint_len) = read_varint(&buf[offset..])?; offset += varint_len; let mut key_values = Vec::with_capacity(num_keys as usize); for _ in 0..num_keys { let (value, consumed) = Self::deserialize_value(&buf[offset..])?; key_values.push(value); offset += consumed; } // Read number of payload values and payload values let (num_payload, varint_len) = read_varint(&buf[offset..])?; offset += varint_len; let mut payload_values = Vec::with_capacity(num_payload as usize); for _ in 0..num_payload { let (value, consumed) = Self::deserialize_value(&buf[offset..])?; payload_values.push(value); offset += consumed; } Ok(( Self { hash, key_values, rowid, payload_values, }, offset, )) } /// Helper to deserialize a single Value from bytes. /// Returns (Value, bytes_consumed). fn deserialize_value(buf: &[u8]) -> Result<(Value, usize)> { if unlikely(buf.is_empty()) { return Err(LimboError::Corrupt( "HashEntry: unexpected end of buffer".to_string(), )); } let value_type = buf[0]; let mut offset = 1; let value = match value_type { NULL_HASH => Value::Null, INT_HASH => { if unlikely(offset + 8 > buf.len()) { return Err(LimboError::Corrupt( "HashEntry: buffer too small for integer".to_string(), )); } let i = i64::from_le_bytes(buf[offset..offset + 8].try_into().expect("expect 8 bytes")); offset += 8; Value::from_i64(i) } FLOAT_HASH => { if unlikely(offset + 8 > buf.len()) { return Err(LimboError::Corrupt( "HashEntry: buffer too small for float".to_string(), )); } let f = f64::from_le_bytes(buf[offset..offset + 8].try_into().expect("expect 8 bytes")); offset += 8; Value::from_f64(f) } TEXT_HASH => { let (str_len, varint_len) = read_varint(&buf[offset..])?; offset += varint_len; if unlikely(offset + str_len as usize > buf.len()) { return Err(LimboError::Corrupt( "HashEntry: buffer too small for text".to_string(), )); } // SAFETY: We serialized this data ourselves, so it should be valid UTF-8. // Skipping validation here for performance in the spill/reload path. // Doing checked utf8 construction here is a massive performance hit. let s = unsafe { String::from_utf8_unchecked(buf[offset..offset + str_len as usize].to_vec()) }; offset += str_len as usize; Value::Text(s.into()) } BLOB_HASH => { let (blob_len, varint_len) = read_varint(&buf[offset..])?; offset += varint_len; if unlikely(offset + blob_len as usize > buf.len()) { return Err(LimboError::Corrupt( "HashEntry: buffer too small for blob".to_string(), )); } let b = buf[offset..offset + blob_len as usize].to_vec(); offset += blob_len as usize; Value::Blob(b) } _ => { mark_unlikely(); return Err(LimboError::Corrupt(format!( "HashEntry: unknown value type {value_type}", ))); } }; Ok((value, offset)) } } #[derive(Debug, Clone, Copy)] struct Partitioning { count: usize, mask: usize, shift: u32, } impl Partitioning { fn new(count: usize) -> Self { turso_assert!( count.is_power_of_two(), "partition count must be a power of two" ); let bits = count.trailing_zeros(); Self { count, mask: count - 1, shift: 64 - bits, } } #[inline(always)] fn index(&self, hash: u64) -> usize { ((hash >> self.shift) as usize) & self.mask } } /// A bucket in the hash table. Uses chaining for collision resolution. #[derive(Debug, Clone)] pub struct HashBucket { entries: Vec, } impl HashBucket { fn new() -> Self { Self { entries: Vec::new(), } } fn insert(&mut self, entry: HashEntry) { self.entries.push(entry); } fn find_matches<'a>( &'a self, hash: u64, probe_keys: &[ValueRef], collations: &[CollationSeq], ) -> Vec<&'a HashEntry> { self.entries .iter() .filter(|entry| { entry.hash == hash && keys_equal(&entry.key_values, probe_keys, collations) }) .collect() } fn is_empty(&self) -> bool { self.entries.is_empty() } fn size_bytes(&self) -> usize { self.entries.iter().map(|e| e.size_bytes()).sum() } } /// I/O state for spilled partition operations #[derive(Debug, AtomicEnum, Clone, Copy, PartialEq, Eq)] pub enum SpillIOState { None, WaitingForWrite, WriteComplete, WaitingForRead, ReadComplete, Error, } /// State of a partition in a spilled hash table #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PartitionState { /// data is in partition_buffers InMemory, /// Has been written to disk, not yet loaded OnDisk, /// Is being loaded from disk (I/O in progress) Loading, /// Has been loaded from disk and is ready for probing Loaded, } /// A chunk of partition data spilled to disk. /// A partition may be spilled multiple times, creating multiple chunks. #[derive(Debug)] struct SpillChunk { /// File offset where this chunk's data starts file_offset: u64, /// Size in bytes of this chunk on disk size_bytes: usize, /// Number of entries in this chunk num_entries: usize, } /// Tracks a partition that has been spilled to disk during grace hash join. pub struct SpilledPartition { /// Partition index (0 to partition_count - 1) pub partition_idx: usize, /// Chunks of data belonging to this partition (may have multiple spills) chunks: Vec, /// Current state of the partition state: PartitionState, /// I/O state for async operations io_state: Arc, /// Read buffer for loading partition back read_buffer: Arc>>, /// Length of data in read buffer buffer_len: Arc, /// Hash buckets for this partition (populated after loading) buckets: Vec, /// Current chunk being loaded (for multi-chunk reads) current_chunk_idx: usize, /// Approximate memory used by the resident buckets for this partition resident_mem: usize, /// Parallel to `buckets`: tracks which entries have been matched (for FULL OUTER JOIN). matched_bits: Vec>, /// Partial entry bytes spanning chunk boundaries partial_entry: Vec, /// Parsed entries for validation parsed_entries: usize, } impl SpilledPartition { fn new(partition_idx: usize) -> Self { Self { partition_idx, chunks: Vec::new(), state: PartitionState::OnDisk, io_state: Arc::new(AtomicSpillIOState::new(SpillIOState::None)), read_buffer: Arc::new(RwLock::new(Vec::new())), buffer_len: Arc::new(atomic::AtomicUsize::new(0)), buckets: Vec::new(), current_chunk_idx: 0, resident_mem: 0, matched_bits: Vec::new(), partial_entry: Vec::new(), parsed_entries: 0, } } /// Add a new chunk of data to this partition fn add_chunk(&mut self, file_offset: u64, size_bytes: usize, num_entries: usize) { self.chunks.push(SpillChunk { file_offset, size_bytes, num_entries, }); } /// Get total size in bytes across all chunks fn total_size_bytes(&self) -> usize { self.chunks.iter().map(|c| c.size_bytes).sum() } /// Get total number of entries across all chunks fn total_num_entries(&self) -> usize { self.chunks.iter().map(|c| c.num_entries).sum() } fn buffer_len(&self) -> usize { self.buffer_len.load(atomic::Ordering::Acquire) } /// Check if partition is ready for probing pub fn is_loaded(&self) -> bool { matches!( self.state, PartitionState::Loaded | PartitionState::InMemory ) } /// Check if there are more chunks to load fn has_more_chunks(&self) -> bool { self.current_chunk_idx < self.chunks.len() } /// Get the current chunk to load, if any fn current_chunk(&self) -> Option<&SpillChunk> { self.chunks.get(self.current_chunk_idx) } } /// In-memory partition buffer for grace hash join. /// During build phase, entries are first accumulated here before spilling. struct PartitionBuffer { /// Entries in this partition entries: Vec, /// Total memory used by entries in this partition mem_used: usize, } impl PartitionBuffer { fn new() -> Self { Self { entries: Vec::new(), mem_used: 0, } } fn insert(&mut self, entry: HashEntry) { self.mem_used += entry.size_bytes(); self.entries.push(entry); } fn clear(&mut self) { self.entries.clear(); self.mem_used = 0; } fn is_empty(&self) -> bool { self.entries.is_empty() } } /// Configuration for the hash table. #[derive(Debug, Clone)] pub struct HashTableConfig { /// Initial number of buckets (must be power of 2). pub initial_buckets: usize, /// Maximum memory budget in bytes. pub mem_budget: usize, /// Number of keys in the join condition. pub num_keys: usize, /// Collation sequences for each join key. pub collations: Vec, /// Only spill to a file when != TempStore::Memory pub temp_store: crate::TempStore, /// Whether to track which entries have been matched during probing (for FULL OUTER JOIN). pub track_matched: bool, /// Optional override for the number of partitions (must be power of two). pub partition_count: Option, } impl Default for HashTableConfig { fn default() -> Self { Self { initial_buckets: DEFAULT_BUCKETS, mem_budget: DEFAULT_MEM_BUDGET, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, } } } struct SpillState { /// In-memory partition buffers for grace hash join. /// When spilling is triggered, entries are partitioned by hash before writing. partition_buffers: Vec, /// Spilled partitions metadata, tracks what's on disk partitions: Vec, /// Current file offset for next spill write next_spill_offset: u64, /// Temporary file for spilled data. temp_file: TempFile, /// Partitioning strategy for this spill. partitioning: Partitioning, } impl SpillState { fn new( io: &Arc, temp_store: crate::TempStore, partitioning: Partitioning, ) -> Result { Ok(SpillState { partition_buffers: (0..partitioning.count) .map(|_| PartitionBuffer::new()) .collect(), partitions: Vec::new(), next_spill_offset: 0, temp_file: TempFile::with_temp_store(io, temp_store)?, partitioning, }) } fn find_partition_mut(&mut self, logical_idx: usize) -> Option<&mut SpilledPartition> { self.partitions .iter_mut() .find(|p| p.partition_idx == logical_idx) } fn find_partition(&self, logical_idx: usize) -> Option<&SpilledPartition> { self.partitions .iter() .find(|p| p.partition_idx == logical_idx) } } /// Probe-side buffering/spilling state for grace hash join. /// Reuses the same serialization format and types as build-side spilling. struct ProbeSpillState { /// In-memory partition buffers for probe rows targeting spilled build partitions. partition_buffers: Vec, /// Spilled probe partition metadata. partitions: Vec, /// Current file offset for next probe spill write. next_spill_offset: u64, /// Separate temp file for probe-side spills. temp_file: TempFile, /// Same partitioning as build side, so partition indices correspond. partitioning: Partitioning, /// Current memory used by probe buffers. mem_used: usize, /// Memory budget for probe-side buffers. mem_budget: usize, } impl ProbeSpillState { fn new( io: &Arc, temp_store: crate::TempStore, partitioning: Partitioning, mem_budget: usize, ) -> Result { Ok(Self { partition_buffers: (0..partitioning.count) .map(|_| PartitionBuffer::new()) .collect(), partitions: Vec::new(), next_spill_offset: 0, temp_file: TempFile::with_temp_store(io, temp_store)?, partitioning, mem_used: 0, mem_budget, }) } fn find_partition_mut(&mut self, logical_idx: usize) -> Option<&mut SpilledPartition> { self.partitions .iter_mut() .find(|p| p.partition_idx == logical_idx) } fn find_partition(&self, logical_idx: usize) -> Option<&SpilledPartition> { self.partitions .iter() .find(|p| p.partition_idx == logical_idx) } } /// State for grace hash join partition-by-partition processing. /// The VDBE drives the loop; this tracks partition iteration and probe chunk loading. struct GraceState { /// Current probe entries loaded from disk (one chunk at a time). probe_entries: Vec, /// Cursor into probe_entries. probe_entry_cursor: usize, /// Ordered list of partition indices to process (only spilled ones). partitions_to_process: Vec, /// Index into partitions_to_process. partition_list_idx: usize, /// Current load state for the active grace partition. load_state: GracePartitionLoadState, } impl GraceState { fn current_partition_idx(&self) -> Option { self.partitions_to_process .get(self.partition_list_idx) .copied() } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum GracePartitionLoadState { NeedBuildLoad, NeedProbeLoad, Ready, } /// HashTable is the build-side data structure used for hash joins and DISTINCT. It behaves like a /// standard in-memory hash table until a configurable memory budget is exceeded, at /// which point it transparently switches to a grace-hash-join style layout and spills /// partitions to disk. /// /// # Overview /// /// The table is keyed by an N-column join key. Keys are hashed using a stable /// rapidhash hasher that is aware of SQLite-style collations for text values. /// Each entry stores: /// /// - the precomputed hash value, /// - a owned copy of the join key values, and /// - the rowid of the build-side row, used later to SeekRowid into the build table. /// /// Collisions within a hash bucket are resolved using simple chaining (a `Vec`), /// and equality is determined by comparing the stored key values against probe keys using /// the same collation-aware comparison logic that was used when hashing. pub struct HashTable { /// Initial bucket count used to reinitialize after spills. initial_buckets: usize, /// The hash buckets (used when not spilled). buckets: Vec, /// Number of entries in the table. num_entries: usize, /// Current memory usage in bytes. mem_used: usize, /// Memory budget in bytes. mem_budget: usize, /// Number of join keys. num_keys: usize, /// Collation sequences for each join key. collations: Vec, /// Current state of the hash table. state: HashTableState, /// IO object for disk operations. io: Arc, /// Current probe position bucket index. probe_bucket_idx: usize, /// Current probe entry index within bucket. probe_entry_idx: usize, /// Cached hash of current probe keys (to avoid recomputing) current_probe_hash: Option, /// Current probe key values being searched. current_probe_keys: Option>, spill_state: Option, /// Index of current spilled partition being probed current_spill_partition_idx: usize, /// Track non-empty buckets for fast clear in distinct/group-by usage. non_empty_buckets: Vec, /// LRU of resident spilled partitions to cap memory for DISTINCT, grace, /// and unmatched-scan partition loads. loaded_partitions_lru: RefCell>, /// Memory used by resident (loaded or in-memory) partitions loaded_partitions_mem: usize, /// Temp storage mode (memory vs file) for spilled data temp_store: crate::TempStore, /// Whether to track matched entries (for FULL OUTER JOIN). track_matched: bool, /// Parallel to `buckets`: one Vec per bucket tracking which entries were matched. matched_bits: Vec>, /// Bucket index for iterating unmatched entries. unmatched_scan_bucket: usize, /// Entry index within bucket for iterating unmatched entries. unmatched_scan_entry: usize, /// Partition index for iterating unmatched entries in spilled mode. unmatched_scan_partition: usize, /// Optional override for partition count selection partition_count_override: Option, /// Probe-side spill state for grace hash join. probe_spill_state: Option, /// Grace processing state machine. grace_state: Option, } crate::assert::assert_send!(HashTable); enum SpillAction { AlreadyLoaded, ParseChunk { partition_idx: usize, }, WaitingForIO, NoChunks, LoadChunk { read_size: usize, file_offset: u64, io_state: Arc, buffer_len: Arc, read_buffer_ref: Arc>>, }, Restart, NotFound, } enum GraceProbeChunkAction { WaitingForIO, ParseChunk { partition_idx: usize, }, LoadChunk { read_size: usize, file_offset: u64, io_state: Arc, buffer_len: Arc, read_buffer_ref: Arc>>, }, Restart, NoMoreChunks, } enum ParseChunkResult { MoreChunks, Done { resident_mem: usize }, } impl HashTable { /// Create a new hash table. pub fn new(config: HashTableConfig, io: Arc) -> Self { let num_buckets = config.initial_buckets; let buckets = (0..num_buckets).map(|_| HashBucket::new()).collect(); let matched_bits = if config.track_matched { (0..num_buckets).map(|_| Vec::new()).collect() } else { Vec::new() }; Self { initial_buckets: config.initial_buckets, buckets, num_entries: 0, mem_used: 0, mem_budget: config.mem_budget, num_keys: config.num_keys, collations: config.collations, state: HashTableState::Building, io, probe_bucket_idx: 0, probe_entry_idx: 0, current_probe_keys: None, current_probe_hash: None, spill_state: None, current_spill_partition_idx: 0, loaded_partitions_lru: VecDeque::new().into(), loaded_partitions_mem: 0, non_empty_buckets: Vec::new(), temp_store: config.temp_store, track_matched: config.track_matched, matched_bits, unmatched_scan_bucket: 0, unmatched_scan_entry: 0, unmatched_scan_partition: 0, partition_count_override: config.partition_count, probe_spill_state: None, grace_state: None, } } /// Get the current state of the hash table. pub fn get_state(&self) -> &HashTableState { &self.state } /// Based on average entry size and number of entries, /// determine the number of partitions to use for spilling. fn choose_partition_count(&self, entry_size: usize) -> usize { if let Some(count) = self.partition_count_override { turso_assert!( count.is_power_of_two(), "partition count override must be a power of two" ); return count; } let avg_entry_size = if self.num_entries > 0 { (self.mem_used / self.num_entries).max(entry_size) } else { entry_size.max(1) }; let target_partition_bytes = (self.mem_budget / 2).max(avg_entry_size); let target_entries_per_partition = (target_partition_bytes / avg_entry_size).max(1); let estimated_total_entries = self.num_entries.saturating_add(1); let mut partitions = estimated_total_entries.div_ceil(target_entries_per_partition); partitions = partitions.clamp(MIN_PARTITIONS, MAX_PARTITIONS); partitions.next_power_of_two() } /// For a given hash value, get the partition index. /// SAFETY: only call this when spill_state is Some. fn partition_index(&self, hash: u64) -> usize { let spill_state = self.spill_state.as_ref().expect("spill state must exist"); spill_state.partitioning.index(hash) } fn record_probe_call(&mut self, metrics: Option<&mut HashJoinMetrics>) { if let Some(metrics) = metrics { metrics.probe_calls = metrics.probe_calls.saturating_add(1); } } /// Insert a row into the hash table, returns IOResult because this may spill to disk. /// When memory budget is exceeded, triggers grace hash join by partitioning and spilling. /// Rows with NULL join keys are skipped since NULL != NULL in SQL. /// (This is specific to hash join semantics, not DISTINCT.) pub fn insert( &mut self, key_values: Vec, rowid: i64, payload_values: Vec, metrics: Option<&mut HashJoinMetrics>, ) -> Result> { let pending = PendingHashInsert { key_values, rowid, payload_values, }; match self.insert_pending(pending, metrics)? { HashInsertResult::Done => Ok(IOResult::Done(())), HashInsertResult::IO { io, .. } => Ok(IOResult::IO(io)), } } pub(crate) fn insert_pending( &mut self, pending: PendingHashInsert, metrics: Option<&mut HashJoinMetrics>, ) -> Result { turso_assert!( matches!(self.state, HashTableState::Building | HashTableState::Spilled), "Cannot insert into hash table in unexpected state", { "state": format!("{:?}", self.state) } ); // Skip rows with NULL join keys - they can never match anything since NULL != NULL in SQL. // However, when track_matched is enabled (outer joins), we must keep NULL-key entries // so they appear as unmatched in the unmatched scan. if has_null_key(&pending.key_values) && !self.track_matched { return Ok(HashInsertResult::Done); } // Compute hash of the join keys using collations let key_refs: Vec = pending.key_values.iter().map(|v| v.as_ref()).collect(); let hash = hash_join_key(&key_refs, &self.collations); let entry_size = HashEntry::size_from_values(&pending.key_values, &pending.payload_values); // Check if we would exceed memory budget if self.mem_used + entry_size > self.mem_budget { if self.spill_state.is_none() { tracing::debug!( "Hash table memory budget exceeded (used: {}, budget: {}), spilling to disk", self.mem_used, self.mem_budget ); // First time exceeding budget, trigger spill // Move all existing bucket entries into partition buffers let partition_count = self.choose_partition_count(entry_size); let partitioning = Partitioning::new(partition_count); self.spill_state = Some(SpillState::new(&self.io, self.temp_store, partitioning)?); self.redistribute_to_partitions(); self.state = HashTableState::Spilled; }; // Spill whole partitions until the new entry fits if let Some(c) = self.spill_partitions_for_entry(entry_size, metrics)? { // I/O pending, caller will re-enter after completion and retry the insert. if !c.finished() { return Ok(HashInsertResult::IO { io: IOCompletions::Single(c), pending, }); } } } let PendingHashInsert { key_values, rowid, payload_values, } = pending; let entry = if payload_values.is_empty() { HashEntry::new(hash, key_values, rowid) } else { HashEntry::new_with_payload(hash, key_values, rowid, payload_values) }; if self.spill_state.is_some() { let partition_idx = { let spill_state = self.spill_state.as_ref().expect("spill state must exist"); spill_state.partitioning.index(hash) }; let spill_state = self.spill_state.as_mut().expect("spill state must exist"); // In spilled mode, insert into partition buffer spill_state.partition_buffers[partition_idx].insert(entry); } else { // Normal mode, insert into hash bucket let bucket_idx = (hash as usize) % self.buckets.len(); if self.buckets[bucket_idx].entries.is_empty() { self.non_empty_buckets.push(bucket_idx); } self.buckets[bucket_idx].insert(entry); if self.track_matched { self.matched_bits[bucket_idx].push(false); } } self.num_entries += 1; self.mem_used += entry_size; Ok(HashInsertResult::Done) } /// Insert keys into the hash table if not already present. /// Returns true if inserted, false if duplicate found. /// Unlike hash join inserts, DISTINCT keeps NULLs and treats NULL==NULL. pub fn insert_distinct( &mut self, key_values: &[Value], key_refs: &[ValueRef], mut metrics: Option<&mut HashJoinMetrics>, ) -> Result> { turso_assert!( self.state == HashTableState::Building || self.state == HashTableState::Spilled, "Cannot insert_distinct into hash table in unexpected state", { "state": format!("{:?}", self.state) } ); let hash = hash_join_key(key_refs, &self.collations); if self.spill_state.is_some() { let partition_idx = self.partition_index(hash); // Check partition buffer for duplicates let has_buffer_dup = { let spill_state = self.spill_state.as_ref().expect("spill state exists"); let buffer = &spill_state.partition_buffers[partition_idx]; buffer.entries.iter().any(|entry| { entry.hash == hash && keys_equal_distinct(&entry.key_values, key_refs, &self.collations) }) }; if has_buffer_dup { return Ok(IOResult::Done(false)); } // Ensure spilled partition is loaded before checking let has_partition = { let spill_state = self.spill_state.as_ref().expect("spill state exists"); spill_state.find_partition(partition_idx).is_some() }; if has_partition && !self.is_partition_loaded(partition_idx) { return_if_io!(self.load_spilled_partition(partition_idx, metrics.as_deref_mut())); } // Check loaded partition for duplicates let has_spilled_dup = 'has_spilled_dup: { let spill_state = self.spill_state.as_ref().expect("spill state exists"); let Some(partition) = spill_state.find_partition(partition_idx) else { break 'has_spilled_dup false; }; if partition.buckets.is_empty() { break 'has_spilled_dup false; } let bucket_idx = (hash as usize) % partition.buckets.len(); let bucket = &partition.buckets[bucket_idx]; bucket.entries.iter().any(|entry| { entry.hash == hash && keys_equal_distinct(&entry.key_values, key_refs, &self.collations) }) }; if has_spilled_dup { return Ok(IOResult::Done(false)); } let entry_size = HashEntry::size_from_values(key_values, &[]); if let Some(c) = self.spill_partitions_for_entry(entry_size, metrics.as_deref_mut())? { if !c.succeeded() { return Ok(IOResult::IO(IOCompletions::Single(c))); } } { let spill_state = self.spill_state.as_mut().expect("spill state exists"); spill_state.partition_buffers[partition_idx].insert(HashEntry::new( hash, key_values.to_vec(), 0, )); } self.num_entries += 1; self.mem_used += entry_size; return Ok(IOResult::Done(true)); } // Non-spilled mode: check main buckets let bucket_idx = (hash as usize) % self.buckets.len(); let bucket = &self.buckets[bucket_idx]; for entry in &bucket.entries { if entry.hash == hash && keys_equal_distinct(&entry.key_values, key_refs, &self.collations) { return Ok(IOResult::Done(false)); } } let entry_size = HashEntry::size_from_values(key_values, &[]); if self.mem_used + entry_size > self.mem_budget { if self.spill_state.is_none() { let partition_count = self.choose_partition_count(entry_size); let partitioning = Partitioning::new(partition_count); self.spill_state = Some(SpillState::new(&self.io, self.temp_store, partitioning)?); self.redistribute_to_partitions(); self.state = HashTableState::Spilled; } return self.insert_distinct(key_values, key_refs, metrics); } if self.buckets[bucket_idx].entries.is_empty() { self.non_empty_buckets.push(bucket_idx); } self.buckets[bucket_idx].insert(HashEntry::new(hash, key_values.to_vec(), 0)); self.num_entries += 1; self.mem_used += entry_size; Ok(IOResult::Done(true)) } /// Clear all entries and reset spill state. pub fn clear(&mut self) { if self.num_entries == 0 && self.spill_state.is_none() { self.state = HashTableState::Building; self.current_probe_keys = None; self.current_probe_hash = None; self.probe_bucket_idx = 0; self.probe_entry_idx = 0; self.current_spill_partition_idx = 0; self.loaded_partitions_lru.borrow_mut().clear(); self.loaded_partitions_mem = 0; self.non_empty_buckets.clear(); self.probe_spill_state = None; self.grace_state = None; return; } if self.spill_state.is_some() { // Drop spilled partitions and reset buckets. self.spill_state = None; let bucket_count = self.initial_buckets.max(1); self.buckets = (0..bucket_count).map(|_| HashBucket::new()).collect(); self.non_empty_buckets.clear(); } else { for &idx in &self.non_empty_buckets { self.buckets[idx].entries.clear(); } self.non_empty_buckets.clear(); } self.num_entries = 0; self.mem_used = 0; self.state = HashTableState::Building; self.current_probe_keys = None; self.current_probe_hash = None; self.probe_bucket_idx = 0; self.probe_entry_idx = 0; self.current_spill_partition_idx = 0; self.loaded_partitions_lru.borrow_mut().clear(); self.loaded_partitions_mem = 0; } /// Redistribute existing bucket entries into partition buffers for grace hash join. fn redistribute_to_partitions(&mut self) { let partitioning = { let spill_state = self.spill_state.as_ref().expect("spill state must exist"); spill_state.partitioning }; for bucket in self.buckets.drain(..) { for entry in bucket.entries { let partition_idx = partitioning.index(entry.hash); self.spill_state .as_mut() .expect("spill state must exist") .partition_buffers[partition_idx] .insert(entry); } } // Clear in-memory matched bits; spilled partitions will have their own. self.matched_bits.clear(); } /// Return the next partition which should be spilled to disk, for simplicity, /// we always select the largest non-empty partition buffer. fn next_partition_to_spill(&self, _required_free: usize) -> Option { let spill_state = self.spill_state.as_ref()?; spill_state .partition_buffers .iter() .enumerate() .filter(|(_, p)| !p.is_empty()) .max_by_key(|(_, p)| p.mem_used) .map(|(idx, _)| idx) } /// Spill the given partition buffer to disk and return the pending completion. /// Uses single-pass serialization directly into the I/O buffer to avoid intermediate copies. fn spill_partition( &mut self, partition_idx: usize, metrics: Option<&mut HashJoinMetrics>, ) -> Result> { let mut metrics = metrics; let spill_state = self.spill_state.as_mut().expect("Spill state must exist"); let partition = &spill_state.partition_buffers[partition_idx]; if partition.is_empty() { return Ok(None); } // Phase 1: Calculate sizes and cache them to avoid recomputation let num_entries = partition.entries.len(); let mut entry_sizes = Vec::with_capacity(num_entries); let mut total_size = 0usize; for entry in &partition.entries { let entry_size = entry.serialized_size(); entry_sizes.push(entry_size); total_size += varint_len(entry_size as u64) + entry_size; } // Allocate I/O buffer and serialize using cached sizes let buffer = Buffer::new_temporary(total_size); let buf = buffer.as_mut_slice(); let mut offset = 0; for (entry, &entry_size) in partition.entries.iter().zip(entry_sizes.iter()) { offset += write_varint(&mut buf[offset..], entry_size as u64); offset += entry.serialize_to_slice(&mut buf[offset..]); } turso_assert!(offset == total_size, "serialized size mismatch"); let file_offset = spill_state.next_spill_offset; let data_size = total_size; let num_entries = spill_state.partition_buffers[partition_idx].entries.len(); let mem_freed = spill_state.partition_buffers[partition_idx].mem_used; spill_state.partition_buffers[partition_idx].clear(); // Find existing partition or create new one let io_state = if let Some(existing) = spill_state.find_partition_mut(partition_idx) { existing.add_chunk(file_offset, data_size, num_entries); if let Some(metrics) = metrics.as_deref_mut() { metrics.spill_bytes_written = metrics.spill_bytes_written.saturating_add(data_size as u64); metrics.spill_chunks = metrics.spill_chunks.saturating_add(1); metrics.spill_max_chunks_per_partition = metrics .spill_max_chunks_per_partition .max(existing.chunks.len() as u64); metrics.spill_max_partition_bytes = metrics .spill_max_partition_bytes .max(existing.total_size_bytes() as u64); } existing.io_state.clone() } else { let mut new_partition = SpilledPartition::new(partition_idx); new_partition.add_chunk(file_offset, data_size, num_entries); if let Some(metrics) = metrics { metrics.spill_bytes_written = metrics.spill_bytes_written.saturating_add(data_size as u64); metrics.spill_chunks = metrics.spill_chunks.saturating_add(1); metrics.spill_max_chunks_per_partition = metrics .spill_max_chunks_per_partition .max(new_partition.chunks.len() as u64); metrics.spill_max_partition_bytes = metrics .spill_max_partition_bytes .max(new_partition.total_size_bytes() as u64); } let io_state = new_partition.io_state.clone(); spill_state.partitions.push(new_partition); io_state }; io_state.set(SpillIOState::WaitingForWrite); let buffer_ref = Arc::new(buffer); let write_complete = Box::new(move |res: Result| match res { Ok(_) => { tracing::trace!("Successfully wrote spilled partition to disk"); io_state.set(SpillIOState::WriteComplete); } Err(e) => { tracing::error!("Error writing spilled partition to disk: {e:?}"); io_state.set(SpillIOState::Error); } }); let completion = Completion::new_write(write_complete); let file = spill_state.temp_file.file.clone(); let completion = file.pwrite(file_offset, buffer_ref, completion)?; // Update state self.mem_used -= mem_freed; spill_state.next_spill_offset += data_size as u64; Ok(Some(completion)) } /// Spill multiple partitions in a single I/O operation. /// This batches the work to reduce syscall overhead when freeing large amounts of memory. fn spill_multiple_partitions( &mut self, partition_indices: &[usize], metrics: Option<&mut HashJoinMetrics>, ) -> Result> { let mut metrics = metrics; if partition_indices.is_empty() { return Ok(None); } // If only one partition, use the simpler single-partition path if partition_indices.len() == 1 { return self.spill_partition(partition_indices[0], metrics); } let spill_state = self.spill_state.as_mut().expect("Spill state must exist"); // Phase 1: Calculate total size and per-partition metadata struct PartitionMeta { idx: usize, num_entries: usize, data_size: usize, mem_freed: usize, entry_sizes: Vec, } let mut metas = Vec::with_capacity(partition_indices.len()); let mut total_size = 0usize; for &partition_idx in partition_indices { let partition = &spill_state.partition_buffers[partition_idx]; if partition.is_empty() { continue; } let mut entry_sizes = Vec::with_capacity(partition.entries.len()); let mut partition_size = 0usize; for entry in &partition.entries { let entry_size = entry.serialized_size(); entry_sizes.push(entry_size); partition_size += varint_len(entry_size as u64) + entry_size; } metas.push(PartitionMeta { idx: partition_idx, num_entries: partition.entries.len(), data_size: partition_size, mem_freed: partition.mem_used, entry_sizes, }); total_size += partition_size; } if metas.is_empty() { return Ok(None); } // Allocate single I/O buffer and serialize all partitions let buffer = Buffer::new_temporary(total_size); let buf = buffer.as_mut_slice(); let mut offset = 0; let base_file_offset = spill_state.next_spill_offset; // Track where each partition's data starts in the buffer let mut partition_offsets = Vec::with_capacity(metas.len()); for meta in &metas { partition_offsets.push(offset); let partition = &spill_state.partition_buffers[meta.idx]; for (entry, &entry_size) in partition.entries.iter().zip(meta.entry_sizes.iter()) { offset += write_varint(&mut buf[offset..], entry_size as u64); offset += entry.serialize_to_slice(&mut buf[offset..]); } } turso_assert!(offset == total_size, "serialized size mismatch"); // Update partition metadata and clear buffers let mut total_mem_freed = 0usize; let mut io_states = Vec::with_capacity(metas.len()); for (meta, &partition_offset) in metas.iter().zip(partition_offsets.iter()) { let file_offset = base_file_offset + partition_offset as u64; spill_state.partition_buffers[meta.idx].clear(); total_mem_freed += meta.mem_freed; // Find existing partition or create new one let io_state = if let Some(existing) = spill_state.find_partition_mut(meta.idx) { existing.add_chunk(file_offset, meta.data_size, meta.num_entries); if let Some(metrics) = metrics.as_deref_mut() { metrics.spill_bytes_written = metrics .spill_bytes_written .saturating_add(meta.data_size as u64); metrics.spill_chunks = metrics.spill_chunks.saturating_add(1); metrics.spill_max_chunks_per_partition = metrics .spill_max_chunks_per_partition .max(existing.chunks.len() as u64); metrics.spill_max_partition_bytes = metrics .spill_max_partition_bytes .max(existing.total_size_bytes() as u64); } existing.io_state.clone() } else { let mut new_partition = SpilledPartition::new(meta.idx); new_partition.add_chunk(file_offset, meta.data_size, meta.num_entries); if let Some(metrics) = metrics.as_deref_mut() { metrics.spill_bytes_written = metrics .spill_bytes_written .saturating_add(meta.data_size as u64); metrics.spill_chunks = metrics.spill_chunks.saturating_add(1); metrics.spill_max_chunks_per_partition = metrics .spill_max_chunks_per_partition .max(new_partition.chunks.len() as u64); metrics.spill_max_partition_bytes = metrics .spill_max_partition_bytes .max(new_partition.total_size_bytes() as u64); } let io_state = new_partition.io_state.clone(); spill_state.partitions.push(new_partition); io_state }; io_state.set(SpillIOState::WaitingForWrite); io_states.push(io_state); } // Submit single I/O write let buffer_ref = Arc::new(buffer); let _buffer_ref_clone = buffer_ref.clone(); let write_complete = Box::new(move |res: Result| match res { Ok(_) => { let _buf = _buffer_ref_clone.clone(); tracing::trace!( "Successfully wrote {} batched partitions to disk", io_states.len() ); for io_state in &io_states { io_state.set(SpillIOState::WriteComplete); } } Err(e) => { tracing::error!("Error writing batched partitions to disk: {e:?}"); for io_state in &io_states { io_state.set(SpillIOState::Error); } } }); let completion = Completion::new_write(write_complete); let file = spill_state.temp_file.file.clone(); let completion = file.pwrite(base_file_offset, buffer_ref, completion)?; // Update state self.mem_used -= total_mem_freed; spill_state.next_spill_offset += total_size as u64; Ok(Some(completion)) } /// Spill as many whole partitions as needed to keep the incoming entry within budget. /// Uses batch spilling to combine multiple partitions into a single I/O operation. fn spill_partitions_for_entry( &mut self, entry_size: usize, metrics: Option<&mut HashJoinMetrics>, ) -> Result> { if self.mem_used + entry_size <= self.mem_budget { return Ok(None); } // Collect all partitions that need to be spilled let mut partitions_to_spill = Vec::new(); let mut projected_mem_used = self.mem_used; let spill_state = self.spill_state.as_ref().expect("spill state must exist"); // Sort partitions by size (largest first) to minimize number of spills let mut candidates: Vec<(usize, usize)> = spill_state .partition_buffers .iter() .enumerate() .filter(|(_, p)| !p.is_empty()) .map(|(idx, p)| (idx, p.mem_used)) .collect(); candidates.sort_by(|a, b| b.1.cmp(&a.1)); // Sort descending by mem_used for (partition_idx, mem_used) in candidates { if projected_mem_used + entry_size <= self.mem_budget { break; } partitions_to_spill.push(partition_idx); projected_mem_used -= mem_used; } if partitions_to_spill.is_empty() { return Ok(None); } self.spill_multiple_partitions(&partitions_to_spill, metrics) } /// Convert a never-spilled partition buffer into in-memory buckets for probing. fn materialize_partition_in_memory(&mut self, partition_idx: usize) { let spill_state = self.spill_state.as_mut().expect("spill state must exist"); if spill_state.find_partition(partition_idx).is_some() { return; } let partition_buffer = &mut spill_state.partition_buffers[partition_idx]; if partition_buffer.is_empty() { return; } let entries = std::mem::take(&mut partition_buffer.entries); // we don't change self.mem_used here, as these entries // were always in memory. we’re just changing their layout partition_buffer.mem_used = 0; let bucket_count = entries.len().next_power_of_two().max(64); let mut buckets = (0..bucket_count) .map(|_| HashBucket::new()) .collect::>(); for entry in entries { let bucket_idx = (entry.hash as usize) % bucket_count; buckets[bucket_idx].insert(entry); } let matched_bits = if self.track_matched { buckets .iter() .map(|b| vec![false; b.entries.len()]) .collect() } else { Vec::new() }; let mut partition = SpilledPartition::new(partition_idx); partition.state = PartitionState::InMemory; partition.buckets = buckets; partition.matched_bits = matched_bits; partition.resident_mem = 0; spill_state.partitions.push(partition); } /// Finalize the build phase and prepare for probing. /// If spilled, flushes remaining in-memory partition entries to disk. pub fn finalize_build( &mut self, metrics: Option<&mut HashJoinMetrics>, ) -> Result> { let mut metrics = metrics; turso_assert!( self.state == HashTableState::Building || self.state == HashTableState::Spilled, "Cannot finalize build in unexpected state", { "state": format!("{:?}", self.state) } ); if self.spill_state.is_some() { { // Check for pending writes from previous call let spill_state = self.spill_state.as_ref().expect("spill state must exist"); for spilled in &spill_state.partitions { if matches!(spilled.io_state.get(), SpillIOState::WaitingForWrite) { io_yield_one!(Completion::new_yield()); } } } // Determine which partitions need to spill vs stay in memory without holding // a mutable borrow across the spill/materialize calls. let mut spill_targets = Vec::new(); let mut materialize_targets = Vec::new(); { let spill_state = self.spill_state.as_ref().expect("spill state must exist"); for partition_idx in 0..spill_state.partitioning.count { let partition = &spill_state.partition_buffers[partition_idx]; if partition.is_empty() { continue; } if spill_state.find_partition(partition_idx).is_some() { spill_targets.push(partition_idx); } else { materialize_targets.push(partition_idx); } } } for partition_idx in spill_targets { if let Some(completion) = self.spill_partition(partition_idx, metrics.as_deref_mut())? { // Return I/O completion to caller, they will re-enter after completion if !completion.finished() { io_yield_one!(completion); } } } for partition_idx in materialize_targets { self.materialize_partition_in_memory(partition_idx); } } self.current_spill_partition_idx = 0; self.state = HashTableState::Probing; Ok(IOResult::Done(())) } /// Probe the hash table with the given keys, returns the first matching entry if found. /// NOTE: Calling `probe` on a spilled table requires the relevant partition to be loaded. /// Returns None immediately if any probe key is NULL since NULL != NULL in SQL. pub fn probe( &mut self, probe_keys: Vec, metrics: Option<&mut HashJoinMetrics>, ) -> Option<&HashEntry> { turso_assert!( self.state == HashTableState::Probing, "Cannot probe hash table in unexpected state", { "state": format!("{:?}", self.state) } ); // Skip probing if any key is NULL - NULL can never match anything in SQL if has_null_key(&probe_keys) { self.current_probe_keys = Some(probe_keys); self.current_probe_hash = None; return None; } // Compute hash of probe keys using collations let hash = { let key_refs: Vec = probe_keys.iter().map(|v| v.as_ref()).collect(); hash_join_key(&key_refs, &self.collations) }; self.current_probe_keys = Some(probe_keys); self.current_probe_hash = Some(hash); // Reset probe state self.probe_entry_idx = 0; if self.spill_state.is_some() { // In spilled mode, search through loaded entries from spilled partitions // that match this probe key's partition let partitioning = { let spill_state = self.spill_state.as_ref().expect("spill state must exist"); spill_state.partitioning }; let target_partition = partitioning.index(hash); self.record_probe_call(metrics); self.touch_partition_lru(target_partition); let bucket_idx = { let spill_state = self.spill_state.as_ref().expect("spill state must exist"); let partition = spill_state.find_partition(target_partition)?; if partition.buckets.is_empty() { return None; } (hash as usize) % partition.buckets.len() }; self.probe_bucket_idx = bucket_idx; self.current_spill_partition_idx = target_partition; let match_idx = { let key_refs: Vec = self .current_probe_keys .as_ref() .expect("probe keys were set") .iter() .map(|v| v.as_ref()) .collect(); let spill_state = self.spill_state.as_ref().expect("spill state must exist"); let partition = spill_state.find_partition(target_partition)?; let bucket = &partition.buckets[bucket_idx]; let mut found = None; for (idx, entry) in bucket.entries.iter().enumerate() { if entry.hash == hash && keys_equal(&entry.key_values, &key_refs, &self.collations) { found = Some(idx); break; } } found }; if let Some(idx) = match_idx { self.probe_entry_idx = idx + 1; let spill_state = self.spill_state.as_ref().expect("spill state must exist"); let partition = spill_state.find_partition(target_partition)?; let bucket = &partition.buckets[bucket_idx]; return bucket.entries.get(idx); } None } else { // Normal mode - search in hash buckets let bucket_idx = (hash as usize) % self.buckets.len(); self.probe_bucket_idx = bucket_idx; let match_idx = { let key_refs: Vec = self .current_probe_keys .as_ref() .expect("probe keys were set") .iter() .map(|v| v.as_ref()) .collect(); let bucket = &self.buckets[bucket_idx]; let mut found = None; for (idx, entry) in bucket.entries.iter().enumerate() { if entry.hash == hash && keys_equal(&entry.key_values, &key_refs, &self.collations) { found = Some(idx); break; } } found }; if let Some(idx) = match_idx { self.probe_entry_idx = idx + 1; return self.buckets[bucket_idx].entries.get(idx); } None } } /// Get the next matching entry for the current probe keys. pub fn next_match(&mut self) -> Option<&HashEntry> { turso_assert!( self.state == HashTableState::Probing || self.state == HashTableState::GraceProcessing, "Cannot get next match in unexpected state", { "state": format!("{:?}", self.state) } ); turso_assert!(self.current_probe_keys.is_some(), "probe keys must be set"); let probe_keys = self.current_probe_keys.as_ref()?; let key_refs: Vec = probe_keys.iter().map(|v| v.as_ref()).collect(); let hash = match self.current_probe_hash { Some(h) => h, None => { let h = hash_join_key(&key_refs, &self.collations); self.current_probe_hash = Some(h); h } }; if let Some(spill_state) = self.spill_state.as_ref() { let partition_idx = self.current_spill_partition_idx; turso_assert_eq!(partition_idx, self.partition_index(hash)); let partition = spill_state.find_partition(partition_idx)?; if partition.buckets.is_empty() { return None; } let bucket = &partition.buckets[self.probe_bucket_idx]; // Continue from where we left off for idx in self.probe_entry_idx..bucket.entries.len() { let entry = &bucket.entries[idx]; if entry.hash == hash && keys_equal(&entry.key_values, &key_refs, &self.collations) { self.probe_entry_idx = idx + 1; return Some(entry); } } None } else { // non-spilled case, seach in main buckets let bucket = &self.buckets[self.probe_bucket_idx]; for idx in self.probe_entry_idx..bucket.entries.len() { let entry = &bucket.entries[idx]; if entry.hash == hash && keys_equal(&entry.key_values, &key_refs, &self.collations) { // update probe entry index for next call self.probe_entry_idx = idx + 1; return Some(entry); } } None } } /// Mark the current matched entry as "matched" for outer join tracking. /// Must be called after a successful probe/next_match. pub fn mark_current_matched(&mut self) { if !self.track_matched { return; } let entry_idx = self .probe_entry_idx .checked_sub(1) .expect("mark_current_matched called without prior probe match"); let bucket_idx = self.probe_bucket_idx; if let Some(spill_state) = &mut self.spill_state { let partition_idx = self.current_spill_partition_idx; let partition = spill_state .find_partition_mut(partition_idx) .expect("spilled partition missing during mark_current_matched"); partition.matched_bits[bucket_idx][entry_idx] = true; } else { self.matched_bits[bucket_idx][entry_idx] = true; } } /// Reset all matched_bits to false. Called at the start of each outer-loop /// iteration so that marks from a previous iteration don't suppress NULL-fill /// rows in the current one. pub fn reset_matched_bits(&mut self) { if let Some(spill_state) = self.spill_state.as_mut() { for partition in &mut spill_state.partitions { for bits in &mut partition.matched_bits { bits.fill(false); } } } for bits in &mut self.matched_bits { bits.fill(false); } } /// Reset the unmatched scan state to the beginning. pub fn begin_unmatched_scan(&mut self) { self.unmatched_scan_bucket = 0; self.unmatched_scan_entry = 0; self.unmatched_scan_partition = 0; } /// Advance to the next unmatched entry in the hash table. /// Returns the entry if found, or None when the scan is complete /// (or a spilled partition needs loading). pub fn next_unmatched(&mut self) -> Option<&HashEntry> { let has_spill_state = self.spill_state.is_some(); let in_grace = self.grace_state.is_some(); let has_pending_grace = !in_grace && self.has_grace_partitions(); if has_spill_state { if in_grace { // During grace: scan only the partition currently owned by grace. return self.next_unmatched_current_grace_partition(); } if has_pending_grace { // Before grace starts, emit unmatched rows from never-spilled in-memory // partitions here. Grace will handle only partitions that actually have // disk chunks, so skipping these would lose unmatched build rows. return self.next_unmatched_spilled_in_memory_only(); } return self.next_unmatched_spilled(); } self.next_unmatched_main_buckets() } fn next_unmatched_main_buckets(&mut self) -> Option<&HashEntry> { while self.unmatched_scan_bucket < self.buckets.len() { let bucket = &self.buckets[self.unmatched_scan_bucket]; let matched = &self.matched_bits[self.unmatched_scan_bucket]; while self.unmatched_scan_entry < bucket.entries.len() { let idx = self.unmatched_scan_entry; self.unmatched_scan_entry += 1; if !matched[idx] { return Some(&bucket.entries[idx]); } } self.unmatched_scan_bucket += 1; self.unmatched_scan_entry = 0; } None } /// Advance to the next unmatched entry across spilled partitions. /// Returns None when a partition needs loading (caller must load and retry). fn next_unmatched_spilled(&mut self) -> Option<&HashEntry> { let spill_state = self.spill_state.as_ref()?; while self.unmatched_scan_partition < spill_state.partitions.len() { let partition = &spill_state.partitions[self.unmatched_scan_partition]; if !partition.is_loaded() { // Partition not loaded yet — caller needs to load it. // Return None to signal we need I/O; caller will re-enter. return None; } while self.unmatched_scan_bucket < partition.buckets.len() { let bucket = &partition.buckets[self.unmatched_scan_bucket]; let matched = &partition.matched_bits[self.unmatched_scan_bucket]; while self.unmatched_scan_entry < bucket.entries.len() { let idx = self.unmatched_scan_entry; self.unmatched_scan_entry += 1; if !matched[idx] { return Some(&bucket.entries[idx]); } } self.unmatched_scan_bucket += 1; self.unmatched_scan_entry = 0; } self.unmatched_scan_partition += 1; self.unmatched_scan_bucket = 0; self.unmatched_scan_entry = 0; } None } /// Scan unmatched rows from partitions that stayed resident in memory after spilling. /// These partitions have no disk chunks, so grace never revisits them. fn next_unmatched_spilled_in_memory_only(&mut self) -> Option<&HashEntry> { let spill_state = self.spill_state.as_ref()?; while self.unmatched_scan_partition < spill_state.partitions.len() { let partition = &spill_state.partitions[self.unmatched_scan_partition]; if !partition.chunks.is_empty() { self.unmatched_scan_partition += 1; self.unmatched_scan_bucket = 0; self.unmatched_scan_entry = 0; continue; } turso_assert!( partition.is_loaded(), "in-memory partition unexpectedly unavailable during unmatched scan", { "partition_idx": partition.partition_idx } ); while self.unmatched_scan_bucket < partition.buckets.len() { let bucket = &partition.buckets[self.unmatched_scan_bucket]; let matched = &partition.matched_bits[self.unmatched_scan_bucket]; while self.unmatched_scan_entry < bucket.entries.len() { let idx = self.unmatched_scan_entry; self.unmatched_scan_entry += 1; if !matched[idx] { return Some(&bucket.entries[idx]); } } self.unmatched_scan_bucket += 1; self.unmatched_scan_entry = 0; } self.unmatched_scan_partition += 1; self.unmatched_scan_bucket = 0; self.unmatched_scan_entry = 0; } None } /// Scan unmatched entries only for the active grace partition. /// Grace unmatched emission happens partition-by-partition before eviction, so /// scanning any other loaded partition can duplicate or suppress rows. fn next_unmatched_current_grace_partition(&mut self) -> Option<&HashEntry> { let partition_idx = self.grace_state.as_ref()?.current_partition_idx()?; let spill_state = self.spill_state.as_ref()?; let partition = spill_state .find_partition(partition_idx) .expect("current grace partition missing from spill state"); if !partition.is_loaded() { return None; } while self.unmatched_scan_bucket < partition.buckets.len() { let bucket = &partition.buckets[self.unmatched_scan_bucket]; let matched = &partition.matched_bits[self.unmatched_scan_bucket]; while self.unmatched_scan_entry < bucket.entries.len() { let idx = self.unmatched_scan_entry; self.unmatched_scan_entry += 1; if !matched[idx] { return Some(&bucket.entries[idx]); } } self.unmatched_scan_bucket += 1; self.unmatched_scan_entry = 0; } None } /// Get the current partition index for unmatched scan (for spilled partition loading). pub fn unmatched_scan_current_partition(&self) -> Option { if let Some(grace_state) = self.grace_state.as_ref() { return grace_state.current_partition_idx(); } if self.has_grace_partitions() { return None; } let spill_state = self.spill_state.as_ref()?; if self.unmatched_scan_partition < spill_state.partitions.len() { Some(spill_state.partitions[self.unmatched_scan_partition].partition_idx) } else { None } } /// Get the number of spilled partitions. pub fn num_partitions(&self) -> usize { self.spill_state .as_ref() .map(|s| s.partitions.len()) .unwrap_or(0) } /// Check if a specific partition is loaded and ready for probing. pub fn is_partition_loaded(&self, partition_idx: usize) -> bool { self.spill_state .as_ref() .and_then(|s| s.find_partition(partition_idx)) .is_some_and(|p| p.is_loaded()) } /// Re-entrantly load spilled partitions from disk pub fn load_spilled_partition( &mut self, partition_idx: usize, mut metrics: Option<&mut HashJoinMetrics>, ) -> Result> { loop { // to avoid holding mut borrows, split this into two phases. let action = { let spill_state = match &mut self.spill_state { Some(s) => s, None => return Ok(IOResult::Done(())), }; let spilled = match spill_state.find_partition_mut(partition_idx) { Some(p) => p, None => return Ok(IOResult::Done(())), }; let io_state = spilled.io_state.get(); if unlikely(matches!(io_state, SpillIOState::Error)) { return Err(LimboError::InternalError( "hash join spill I/O failure".into(), )); } // Already fully loaded if spilled.is_loaded() { SpillAction::AlreadyLoaded } else if matches!(io_state, SpillIOState::WaitingForRead) { // We've scheduled a read, caller must wait for completion. SpillAction::WaitingForIO } else if matches!(io_state, SpillIOState::ReadComplete) { SpillAction::ParseChunk { partition_idx } } else { match spilled.current_chunk() { Some(chunk) => { let read_size = chunk.size_bytes; let file_offset = chunk.file_offset; let is_first_load = matches!(spilled.state, PartitionState::OnDisk) && spilled.current_chunk_idx == 0; if is_first_load { let total_entries = spilled.total_num_entries(); let bucket_count = total_entries.next_power_of_two().max(64); spilled.buckets = (0..bucket_count).map(|_| HashBucket::new()).collect(); spilled.parsed_entries = 0; spilled.partial_entry.clear(); } if read_size == 0 { // Empty chunk: skip it and move to the next. spilled.current_chunk_idx += 1; if spilled.has_more_chunks() { SpillAction::Restart } else { // No more chunks, but nothing to read; mark loaded. spilled.state = PartitionState::Loaded; SpillAction::NoChunks } } else { // Non-empty chunk, schedule a read for it. let buffer_len = spilled.buffer_len.clone(); let read_buffer_ref = spilled.read_buffer.clone(); spilled.io_state.set(SpillIOState::WaitingForRead); spilled.state = PartitionState::Loading; SpillAction::LoadChunk { read_size, file_offset, io_state: spilled.io_state.clone(), buffer_len, read_buffer_ref, } } } None => { // No chunks at all: partition is logically empty, mark as loaded. spilled.state = PartitionState::Loaded; SpillAction::NoChunks } } } }; match action { SpillAction::AlreadyLoaded => { self.touch_partition_lru(partition_idx); return Ok(IOResult::Done(())); } SpillAction::NoChunks => { self.evict_partitions_to_fit(0, partition_idx); self.record_partition_resident(partition_idx, 0); return Ok(IOResult::Done(())); } SpillAction::NotFound => { return Ok(IOResult::Done(())); } SpillAction::ParseChunk { partition_idx } => { match self.parse_partition_chunk(partition_idx, metrics.as_deref_mut())? { ParseChunkResult::MoreChunks => continue, ParseChunkResult::Done { resident_mem } => { self.evict_partitions_to_fit(resident_mem, partition_idx); self.record_partition_resident(partition_idx, resident_mem); return Ok(IOResult::Done(())); } } } SpillAction::WaitingForIO => { io_yield_one!(Completion::new_yield()); } SpillAction::Restart => { // We advanced state (e.g., moved past an empty chunk or completed a chunk), // so just loop again and recompute the next action. continue; } SpillAction::LoadChunk { read_size, file_offset, io_state, buffer_len, read_buffer_ref, } => { let read_buffer = Arc::new(Buffer::new_temporary(read_size)); let read_complete = Box::new( move |res: Result<(Arc, i32), CompletionError>| match res { Ok((buf, bytes_read)) => { tracing::trace!( "Completed read of spilled partition chunk: bytes_read={}", bytes_read ); let mut persistent_buf = read_buffer_ref.write(); persistent_buf.clear(); persistent_buf .extend_from_slice(&buf.as_slice()[..bytes_read as usize]); buffer_len.store(bytes_read as usize, atomic::Ordering::Release); io_state.set(SpillIOState::ReadComplete); None } Err(e) => { tracing::error!("Error reading spilled partition chunk: {e:?}"); io_state.set(SpillIOState::Error); None } }, ); let completion = Completion::new_read(read_buffer, read_complete); let spill_state = self.spill_state.as_ref().expect("spill state must exist"); let c = spill_state.temp_file.file.pread(file_offset, completion)?; if !c.finished() { io_yield_one!(c); } } } } } /// Parse entries from the current chunk buffer into buckets for a partition. fn parse_partition_chunk( &mut self, partition_idx: usize, mut metrics: Option<&mut HashJoinMetrics>, ) -> Result { let (has_more_chunks, resident_mem) = { let spill_state = self.spill_state.as_mut().expect("spill state must exist"); let partition = spill_state .find_partition_mut(partition_idx) .expect("partition must exist for parsing"); let data_len = partition.buffer_len(); if let Some(metrics) = metrics.as_mut() { metrics.load_bytes_read = metrics.load_bytes_read.saturating_add(data_len as u64); } let data_guard = partition.read_buffer.read(); let data = &data_guard[..data_len]; let parse_buf = if partition.partial_entry.is_empty() { data.to_vec() } else { let mut combined = Vec::with_capacity(partition.partial_entry.len() + data.len()); combined.extend_from_slice(&partition.partial_entry); combined.extend_from_slice(data); combined }; drop(data_guard); partition.partial_entry.clear(); partition.buffer_len.store(0, atomic::Ordering::Release); partition.read_buffer.write().clear(); partition.io_state.set(SpillIOState::None); let mut offset = 0; while offset < parse_buf.len() { let Some((entry_len, varint_size)) = read_varint_partial(&parse_buf[offset..])? else { partition .partial_entry .extend_from_slice(&parse_buf[offset..]); break; }; let total_needed = varint_size + entry_len as usize; if offset + total_needed > parse_buf.len() { partition .partial_entry .extend_from_slice(&parse_buf[offset..]); break; } let start = offset + varint_size; let end = start + entry_len as usize; let (entry, consumed) = HashEntry::deserialize(&parse_buf[start..end])?; turso_assert!( consumed == entry_len as usize, "expected to consume entire entry" ); let bucket_idx = (entry.hash as usize) % partition.buckets.len(); partition.buckets[bucket_idx].insert(entry); partition.parsed_entries += 1; offset += total_needed; } partition.current_chunk_idx += 1; if partition.has_more_chunks() { (true, 0) } else { if unlikely(!partition.partial_entry.is_empty()) { return Err(LimboError::Corrupt("HashEntry: truncated entry".into())); } let total_num_entries = partition.total_num_entries(); turso_assert!( partition.parsed_entries == total_num_entries, "parsed entry count mismatch" ); if self.track_matched && partition.matched_bits.is_empty() { // Only initialize matched_bits on the first load. On subsequent // reloads (after eviction), the existing bits are preserved so that // probe marks set during earlier passes are not lost. partition.matched_bits = partition .buckets .iter() .map(|b| vec![false; b.entries.len()]) .collect(); } partition.state = PartitionState::Loaded; partition.resident_mem = Self::partition_bucket_mem(&partition.buckets); // Release staging buffer to free memory now that buckets are built. partition.buffer_len.store(0, atomic::Ordering::SeqCst); partition.read_buffer.write().clear(); (false, partition.resident_mem) } }; if has_more_chunks { Ok(ParseChunkResult::MoreChunks) } else { Ok(ParseChunkResult::Done { resident_mem }) } } /// Probe a specific partition with the given keys. The partition must be loaded first via `load_spilled_partition`. /// VDBE *must* call load_spilled_partition(partition_idx) and get IOResult::Done before calling probe. /// Returns None immediately if any probe key is NULL since NULL != NULL in SQL. pub fn probe_partition( &mut self, partition_idx: usize, probe_keys: &[Value], metrics: Option<&mut HashJoinMetrics>, ) -> Option<&HashEntry> { // Skip probing if any key is NULL - NULL can never match anything in SQL if has_null_key(probe_keys) { self.current_probe_keys = Some(probe_keys.to_vec()); self.current_probe_hash = None; return None; } let key_refs: Vec = probe_keys.iter().map(|v| v.as_ref()).collect(); let hash = hash_join_key(&key_refs, &self.collations); // Store probe keys for subsequent next_match calls self.current_probe_keys = Some(probe_keys.to_vec()); self.current_probe_hash = Some(hash); self.record_probe_call(metrics); self.touch_partition_lru(partition_idx); let spill_state = self.spill_state.as_ref()?; let partition = spill_state.find_partition(partition_idx)?; if !partition.is_loaded() || partition.buckets.is_empty() { return None; } let bucket_idx = (hash as usize) % partition.buckets.len(); let bucket = &partition.buckets[bucket_idx]; self.probe_bucket_idx = bucket_idx; self.current_spill_partition_idx = partition_idx; for (idx, entry) in bucket.entries.iter().enumerate() { if entry.hash == hash && keys_equal(&entry.key_values, &key_refs, &self.collations) { self.probe_entry_idx = idx + 1; return Some(entry); } } None } /// Get the partition index for a given probe key hash. pub fn partition_for_keys(&self, probe_keys: &[Value]) -> usize { turso_assert!( self.spill_state.is_some(), "partition_for_keys requires spill state" ); let key_refs: Vec = probe_keys.iter().map(|v| v.as_ref()).collect(); let hash = hash_join_key(&key_refs, &self.collations); self.partition_index(hash) } /// Returns true if the hash table has spilled to disk. pub fn has_spilled(&self) -> bool { self.spill_state.is_some() } /// Approximate memory used by a partition's buckets. fn partition_bucket_mem(buckets: &[HashBucket]) -> usize { buckets.iter().map(|b| b.size_bytes()).sum() } /// Touch a resident spilled partition for LRU ordering without changing its /// accounted memory. fn touch_partition_lru(&self, partition_idx: usize) { let mut lru = self.loaded_partitions_lru.borrow_mut(); if let Some(pos) = lru.iter().position(|p| *p == partition_idx) { lru.remove(pos); } lru.push_back(partition_idx); } /// Record that a spilled partition is resident with the given memory /// footprint and update LRU ordering. fn record_partition_resident(&mut self, partition_idx: usize, mem_used: usize) { if let Some(spill_state) = self.spill_state.as_mut() { if let Some(partition) = spill_state.find_partition_mut(partition_idx) { self.loaded_partitions_mem = self .loaded_partitions_mem .saturating_sub(partition.resident_mem); partition.resident_mem = mem_used; self.loaded_partitions_mem += mem_used; self.touch_partition_lru(partition_idx); } } } fn evict_partitions_to_fit(&mut self, incoming_mem: usize, protect_idx: usize) { while self.mem_used + self.loaded_partitions_mem + incoming_mem > self.mem_budget { let Some(victim_idx) = self.next_evictable(protect_idx) else { break; }; let mut freed = 0; if let Some(spill_state) = self.spill_state.as_mut() { if let Some(victim) = spill_state.find_partition_mut(victim_idx) { if matches!(victim.state, PartitionState::Loaded) { freed = victim.resident_mem; victim.buckets.clear(); victim.state = PartitionState::OnDisk; victim.resident_mem = 0; victim.current_chunk_idx = 0; victim.buffer_len.store(0, atomic::Ordering::Release); victim.read_buffer.write().clear(); victim.partial_entry.clear(); victim.parsed_entries = 0; victim.io_state.set(SpillIOState::None); } } } self.loaded_partitions_mem = self.loaded_partitions_mem.saturating_sub(freed); } } /// Find the next evictable resident spilled partition (LRU) that is not /// protected and has backing spill data. fn next_evictable(&mut self, protect_idx: usize) -> Option { let spill_state = self.spill_state.as_ref()?; let len = self.loaded_partitions_lru.borrow().len(); for i in 0..len { let lru = self.loaded_partitions_lru.borrow(); let candidate = lru[i]; if candidate == protect_idx { continue; } if let Some(p) = spill_state.find_partition(candidate) { let has_disk = !p.chunks.is_empty(); drop(lru); if matches!(p.state, PartitionState::Loaded) && has_disk { self.loaded_partitions_lru.borrow_mut().remove(i); return Some(candidate); } } } None } /// Buffer a probe row whose target build partition is on disk. /// Called from op_hash_probe when `probe_rowid_reg` is Some and the /// partition is OnDisk. pub fn buffer_probe_row( &mut self, key_values: Vec, probe_rowid: i64, metrics: Option<&mut HashJoinMetrics>, ) -> Result> { let spill_state = self .spill_state .as_ref() .expect("buffer_probe_row requires build-side spill state"); let partitioning = spill_state.partitioning; // Lazily initialize probe spill state on first call if self.probe_spill_state.is_none() { self.probe_spill_state = Some(ProbeSpillState::new( &self.io, self.temp_store, partitioning, self.mem_budget / 2, )?); } let key_refs: Vec = key_values.iter().map(|v| v.as_ref()).collect(); let hash = hash_join_key(&key_refs, &self.collations); let partition_idx = partitioning.index(hash); let entry = HashEntry::new(hash, key_values, probe_rowid); let entry_size = entry.size_bytes(); let probe_state = self .probe_spill_state .as_mut() .expect("probe spill state just initialized"); probe_state.partition_buffers[partition_idx].insert(entry); probe_state.mem_used += entry_size; if let Some(metrics) = metrics { metrics.grace_probe_rows_buffered = metrics.grace_probe_rows_buffered.saturating_add(1); } // If probe buffers exceed budget, spill the largest one if probe_state.mem_used > probe_state.mem_budget { if let Some(c) = self.spill_largest_probe_partition(None)? { if !c.finished() { return Ok(IOResult::IO(IOCompletions::Single(c))); } } } Ok(IOResult::Done(())) } /// Spill the largest probe partition buffer to disk. fn spill_largest_probe_partition( &mut self, metrics: Option<&mut HashJoinMetrics>, ) -> Result> { let probe_state = self .probe_spill_state .as_mut() .expect("probe spill state must exist"); let largest_idx = probe_state .partition_buffers .iter() .enumerate() .filter(|(_, p)| !p.is_empty()) .max_by_key(|(_, p)| p.mem_used) .map(|(idx, _)| idx); let Some(partition_idx) = largest_idx else { return Ok(None); }; Self::spill_probe_partition(probe_state, partition_idx, &self.io, metrics) } /// Spill a probe partition buffer to its temp file. fn spill_probe_partition( probe_state: &mut ProbeSpillState, partition_idx: usize, io: &Arc, metrics: Option<&mut HashJoinMetrics>, ) -> Result> { let partition = &probe_state.partition_buffers[partition_idx]; if partition.is_empty() { return Ok(None); } // Calculate total serialized size let mut total_size = 0usize; let mut entry_sizes = Vec::with_capacity(partition.entries.len()); for entry in &partition.entries { let s = entry.serialized_size(); entry_sizes.push(s); total_size += varint_len(s as u64) + s; } // Serialize into I/O buffer let buffer = Buffer::new_temporary(total_size); let buf = buffer.as_mut_slice(); let mut offset = 0; for (entry, &entry_size) in partition.entries.iter().zip(entry_sizes.iter()) { offset += write_varint(&mut buf[offset..], entry_size as u64); offset += entry.serialize_to_slice(&mut buf[offset..]); } turso_assert!(offset == total_size, "serialized size mismatch"); let file_offset = probe_state.next_spill_offset; let num_entries = partition.entries.len(); let mem_freed = partition.mem_used; probe_state.partition_buffers[partition_idx].clear(); // Record chunk let io_state = if let Some(existing) = probe_state.find_partition_mut(partition_idx) { existing.add_chunk(file_offset, total_size, num_entries); existing.io_state.clone() } else { let mut new_partition = SpilledPartition::new(partition_idx); new_partition.add_chunk(file_offset, total_size, num_entries); let io_state = new_partition.io_state.clone(); probe_state.partitions.push(new_partition); io_state }; if let Some(metrics) = metrics { metrics.probe_spill_bytes_written = metrics .probe_spill_bytes_written .saturating_add(total_size as u64); metrics.probe_spill_chunks = metrics.probe_spill_chunks.saturating_add(1); } io_state.set(SpillIOState::WaitingForWrite); let buffer_ref = Arc::new(buffer); let write_complete = Box::new(move |res: Result| match res { Ok(_) => io_state.set(SpillIOState::WriteComplete), Err(e) => { tracing::error!("Error writing probe partition to disk: {e:?}"); io_state.set(SpillIOState::Error); } }); let completion = Completion::new_write(write_complete); let file = probe_state.temp_file.file.clone(); let completion = file.pwrite(file_offset, buffer_ref, completion)?; probe_state.mem_used -= mem_freed; probe_state.next_spill_offset += total_size as u64; let _ = io; Ok(Some(completion)) } /// Finalize probe-side spilling after the probe cursor is exhausted. /// Flushes remaining in-memory probe buffers for partitions that have spill /// chunks, keeps purely in-memory probe buffers as-is. pub fn finalize_probe_spill( &mut self, metrics: Option<&mut HashJoinMetrics>, ) -> Result> { let mut metrics = metrics; let Some(probe_state) = self.probe_spill_state.as_ref() else { return Ok(IOResult::Done(())); }; // Wait for any pending probe writes for spilled in &probe_state.partitions { if matches!(spilled.io_state.get(), SpillIOState::WaitingForWrite) { io_yield_one!(Completion::new_yield()); } } // Collect partition indices that need flushing let partition_count = probe_state.partitioning.count; let mut flush_targets = Vec::new(); for partition_idx in 0..partition_count { if probe_state.partition_buffers[partition_idx].is_empty() { continue; } if probe_state.find_partition(partition_idx).is_some() { flush_targets.push(partition_idx); } } for partition_idx in flush_targets { if let Some(c) = Self::spill_probe_partition( self.probe_spill_state.as_mut().expect("probe state exists"), partition_idx, &self.io, metrics.as_deref_mut(), )? { if !c.finished() { io_yield_one!(c); } } } Ok(IOResult::Done(())) } /// Initialize grace processing. Builds partition list, frees in-memory build partitions. /// Returns true if there are partitions to process. No IO. pub fn grace_begin(&mut self) -> bool { if self.probe_spill_state.is_none() || self.spill_state.is_none() { return false; } // Build list of partitions that were spilled on the build side let partitions_to_process: Vec = { let spill_state = self.spill_state.as_ref().expect("spill state exists"); spill_state .partitions .iter() .filter(|p| !p.chunks.is_empty()) .map(|p| p.partition_idx) .collect() }; if partitions_to_process.is_empty() { return false; } // Free in-memory build partitions -- initial probe is done with them self.free_in_memory_build_partitions(); self.grace_state = Some(GraceState { probe_entries: Vec::new(), probe_entry_cursor: 0, partitions_to_process, partition_list_idx: 0, load_state: GracePartitionLoadState::NeedBuildLoad, }); if let Some(probe_state) = self.probe_spill_state.as_mut() { for partition in &mut probe_state.partitions { partition.current_chunk_idx = 0; partition.buffer_len.store(0, atomic::Ordering::Release); partition.read_buffer.write().clear(); partition.partial_entry.clear(); partition.parsed_entries = 0; partition.io_state.set(SpillIOState::None); } } self.state = HashTableState::GraceProcessing; true } /// Load build partition at current index + first probe chunk. IO-blocking. /// Returns true if loaded, false if partition list exhausted. pub fn grace_load_current_partition( &mut self, mut metrics: Option<&mut HashJoinMetrics>, ) -> Result> { loop { let grace = self.grace_state.as_ref().expect("grace state must exist"); if grace.partition_list_idx >= grace.partitions_to_process.len() { return Ok(IOResult::Done(false)); } let partition_idx = grace.partitions_to_process[grace.partition_list_idx]; match grace.load_state { GracePartitionLoadState::NeedBuildLoad => { self.evict_all_loaded_partitions(); return_if_io!( self.load_spilled_partition(partition_idx, metrics.as_deref_mut()) ); let grace = self.grace_state.as_mut().expect("grace state"); grace.probe_entries.clear(); grace.probe_entry_cursor = 0; grace.load_state = GracePartitionLoadState::NeedProbeLoad; } GracePartitionLoadState::NeedProbeLoad => { return_if_io!(self.grace_load_probe_entries(partition_idx)); let grace = self.grace_state.as_mut().expect("grace state"); grace.load_state = GracePartitionLoadState::Ready; if let Some(m) = metrics.as_mut() { m.grace_partitions_processed = m.grace_partitions_processed.saturating_add(1); } return Ok(IOResult::Done(true)); } GracePartitionLoadState::Ready => return Ok(IOResult::Done(true)), } } } /// Advance to next probe entry. Returns keys+rowid or None when exhausted. pub fn grace_next_probe_entry(&mut self) -> Result>> { loop { let grace = self.grace_state.as_ref().expect("grace state must exist"); if grace.probe_entry_cursor < grace.probe_entries.len() { let entry = &grace.probe_entries[grace.probe_entry_cursor]; let result = GraceProbeEntry { key_values: entry.key_values.clone(), probe_rowid: entry.rowid, }; let grace = self.grace_state.as_mut().expect("grace state"); grace.probe_entry_cursor += 1; return Ok(IOResult::Done(Some(result))); } // Current probe entries exhausted, try loading more let grace = self.grace_state.as_ref().expect("grace state"); let partition_idx = match grace.current_partition_idx() { Some(idx) => idx, None => return Ok(IOResult::Done(None)), }; match self.grace_try_load_next_probe_chunk(partition_idx)? { IOResult::Done(true) => continue, IOResult::Done(false) => return Ok(IOResult::Done(None)), IOResult::IO(io) => return Ok(IOResult::IO(io)), } } } /// Try to load the next probe chunk for the given partition. /// Returns true if more probe entries were loaded, false if exhausted. fn grace_try_load_next_probe_chunk(&mut self, partition_idx: usize) -> Result> { loop { let Some(probe_state) = self.probe_spill_state.as_ref() else { return Ok(IOResult::Done(false)); }; let Some(spilled) = probe_state.find_partition(partition_idx) else { return Ok(IOResult::Done(false)); }; if spilled.current_chunk_idx >= spilled.chunks.len() { return Ok(IOResult::Done(false)); } match self.grace_load_next_probe_chunk(partition_idx)? { IOResult::Done(true) => return Ok(IOResult::Done(true)), IOResult::Done(false) => continue, IOResult::IO(io) => return Ok(IOResult::IO(io)), } } } /// Evict current partition, advance to next. Returns true if more partitions. No IO. pub fn grace_advance_partition(&mut self) -> bool { self.evict_all_loaded_partitions(); let grace = self.grace_state.as_mut().expect("grace state must exist"); grace.partition_list_idx += 1; grace.probe_entries.clear(); grace.probe_entry_cursor = 0; grace.load_state = GracePartitionLoadState::NeedBuildLoad; grace.partition_list_idx < grace.partitions_to_process.len() } /// Free all in-memory build partitions (InMemory state). fn free_in_memory_build_partitions(&mut self) { if let Some(spill_state) = self.spill_state.as_mut() { for partition in &mut spill_state.partitions { if matches!(partition.state, PartitionState::InMemory) { partition.buckets.clear(); partition.state = PartitionState::OnDisk; partition.resident_mem = 0; } } } // Also free the main buckets self.buckets.clear(); self.loaded_partitions_lru.borrow_mut().clear(); self.loaded_partitions_mem = 0; } /// Evict all currently loaded build partitions. fn evict_all_loaded_partitions(&mut self) { if let Some(spill_state) = self.spill_state.as_mut() { for partition in &mut spill_state.partitions { if matches!(partition.state, PartitionState::Loaded) && !partition.chunks.is_empty() { partition.buckets.clear(); partition.state = PartitionState::OnDisk; partition.resident_mem = 0; partition.current_chunk_idx = 0; partition.buffer_len.store(0, atomic::Ordering::Release); partition.read_buffer.write().clear(); partition.partial_entry.clear(); partition.parsed_entries = 0; partition.io_state.set(SpillIOState::None); } } } self.loaded_partitions_lru.borrow_mut().clear(); self.loaded_partitions_mem = 0; } /// Load probe entries for a given partition into grace_state.probe_entries. /// Loads from in-memory buffers or from the first spill chunk. fn grace_load_probe_entries(&mut self, partition_idx: usize) -> Result> { { let grace = self.grace_state.as_mut().expect("grace state"); grace.probe_entries.clear(); grace.probe_entry_cursor = 0; } let Some(probe_state) = self.probe_spill_state.as_ref() else { return Ok(IOResult::Done(())); }; // First: check if there are in-memory entries for this partition let buffer = &probe_state.partition_buffers[partition_idx]; if !buffer.is_empty() { let grace = self.grace_state.as_mut().expect("grace state"); grace.probe_entries.clone_from(&buffer.entries); return Ok(IOResult::Done(())); } // Check if there are spill chunks let Some(spilled) = probe_state.find_partition(partition_idx) else { return Ok(IOResult::Done(())); }; if spilled.chunks.is_empty() { return Ok(IOResult::Done(())); } self.grace_load_next_probe_chunk(partition_idx) .map(|result| result.map(|_| ())) } /// Load the next probe spill chunk into grace_state.probe_entries. fn grace_load_next_probe_chunk(&mut self, partition_idx: usize) -> Result> { loop { let action = { let probe_state = self.probe_spill_state.as_mut().expect("probe spill state"); let spilled = probe_state .find_partition_mut(partition_idx) .expect("probe partition must exist"); let io_state = spilled.io_state.get(); if unlikely(matches!(io_state, SpillIOState::Error)) { return Err(LimboError::InternalError( "grace probe spill I/O failure".into(), )); } if matches!(io_state, SpillIOState::WaitingForRead) { GraceProbeChunkAction::WaitingForIO } else if matches!(io_state, SpillIOState::ReadComplete) { GraceProbeChunkAction::ParseChunk { partition_idx } } else { match spilled.current_chunk() { Some(chunk) if chunk.size_bytes == 0 => { spilled.current_chunk_idx += 1; GraceProbeChunkAction::Restart } Some(chunk) => { spilled.io_state.set(SpillIOState::WaitingForRead); GraceProbeChunkAction::LoadChunk { read_size: chunk.size_bytes, file_offset: chunk.file_offset, io_state: spilled.io_state.clone(), buffer_len: spilled.buffer_len.clone(), read_buffer_ref: spilled.read_buffer.clone(), } } None => GraceProbeChunkAction::NoMoreChunks, } } }; match action { GraceProbeChunkAction::WaitingForIO => { io_yield_one!(Completion::new_yield()); } GraceProbeChunkAction::ParseChunk { partition_idx } => { return Ok(IOResult::Done(self.parse_grace_probe_chunk(partition_idx)?)); } GraceProbeChunkAction::LoadChunk { read_size, file_offset, io_state, buffer_len, read_buffer_ref, } => { let read_buffer = Arc::new(Buffer::new_temporary(read_size)); let read_complete = Box::new( move |res: Result<(Arc, i32), CompletionError>| match res { Ok((buf, bytes_read)) => { let mut persistent_buf = read_buffer_ref.write(); persistent_buf.clear(); persistent_buf .extend_from_slice(&buf.as_slice()[..bytes_read as usize]); buffer_len.store(bytes_read as usize, atomic::Ordering::Release); io_state.set(SpillIOState::ReadComplete); None } Err(e) => { mark_unlikely(); tracing::error!("Error reading probe chunk: {e:?}"); io_state.set(SpillIOState::Error); None } }, ); let completion = Completion::new_read(read_buffer, read_complete); let probe_state = self.probe_spill_state.as_ref().expect("probe spill state"); let c = probe_state.temp_file.file.pread(file_offset, completion)?; if !c.finished() { io_yield_one!(c); } } GraceProbeChunkAction::Restart => continue, GraceProbeChunkAction::NoMoreChunks => return Ok(IOResult::Done(false)), } } } fn parse_grace_probe_chunk(&mut self, partition_idx: usize) -> Result { let entries = { let probe_state = self.probe_spill_state.as_mut().expect("probe spill state"); let partition = probe_state .find_partition_mut(partition_idx) .expect("probe partition must exist for parsing"); let chunk = partition .current_chunk() .expect("probe chunk must exist while parsing"); let expected_entries = chunk.num_entries; let data_len = partition.buffer_len(); let data_guard = partition.read_buffer.read(); let data = &data_guard[..data_len]; let mut entries = Vec::with_capacity(expected_entries); let mut offset = 0; while offset < data_len { let Some((entry_len, varint_size)) = read_varint_partial(&data[offset..])? else { return Err(LimboError::InternalError( "truncated grace probe spill chunk header".into(), )); }; let total_needed = varint_size + entry_len as usize; if offset + total_needed > data_len { return Err(LimboError::InternalError( "truncated grace probe spill chunk payload".into(), )); } let start = offset + varint_size; let end = start + entry_len as usize; let (entry, _consumed) = HashEntry::deserialize(&data[start..end])?; entries.push(entry); offset += total_needed; } drop(data_guard); if unlikely(entries.len() != expected_entries) { return Err(LimboError::InternalError(format!( "grace probe spill chunk entry count mismatch: expected {expected_entries}, got {}", entries.len() ))); } partition.buffer_len.store(0, atomic::Ordering::Release); partition.read_buffer.write().clear(); partition.io_state.set(SpillIOState::None); partition.current_chunk_idx += 1; entries }; let grace = self.grace_state.as_mut().expect("grace state"); grace.probe_entries = entries; grace.probe_entry_cursor = 0; Ok(!grace.probe_entries.is_empty()) } /// Returns true if grace processing has any spilled partitions to process. pub fn has_grace_partitions(&self) -> bool { self.probe_spill_state.is_some() } /// Close the hash table and free resources. pub fn close(&mut self) { self.state = HashTableState::Closed; self.buckets.clear(); self.num_entries = 0; self.mem_used = 0; self.loaded_partitions_lru.borrow_mut().clear(); self.loaded_partitions_mem = 0; let _ = self.spill_state.take(); self.probe_spill_state = None; self.grace_state = None; } } #[cfg(test)] mod hashtests { use super::*; use crate::io::Buffer; use crate::MemoryIO; #[test] fn test_hash_function_consistency() { // Test that the same keys produce the same hash let keys1 = vec![ ValueRef::from_i64(42), ValueRef::Text(crate::types::TextRef::new( "hello", crate::types::TextSubtype::Text, )), ]; let keys2 = vec![ ValueRef::from_i64(42), ValueRef::Text(crate::types::TextRef::new( "hello", crate::types::TextSubtype::Text, )), ]; let keys3 = vec![ ValueRef::from_i64(43), ValueRef::Text(crate::types::TextRef::new( "hello", crate::types::TextSubtype::Text, )), ]; let collations = vec![CollationSeq::Binary, CollationSeq::Binary]; let hash1 = hash_join_key(&keys1, &collations); let hash2 = hash_join_key(&keys2, &collations); let hash3 = hash_join_key(&keys3, &collations); assert_eq!(hash1, hash2); assert_ne!(hash1, hash3); } #[test] fn test_hash_function_numeric_equivalence() { let collations = vec![CollationSeq::Binary]; // Zero variants should hash identically let h_zero = hash_join_key(&[ValueRef::from_f64(0.0)], &collations); let h_neg_zero = hash_join_key(&[ValueRef::from_f64(-0.0)], &collations); let h_int_zero = hash_join_key(&[ValueRef::from_i64(0)], &collations); assert_eq!(h_zero, h_neg_zero); assert_eq!(h_zero, h_int_zero); // Integer/float representations of the same numeric value should match let h_ten_int = hash_join_key(&[ValueRef::from_i64(10)], &collations); let h_ten_float = hash_join_key(&[ValueRef::from_f64(10.0)], &collations); assert_eq!(h_ten_int, h_ten_float); let h_neg_ten_int = hash_join_key(&[ValueRef::from_i64(-10)], &collations); let h_neg_ten_float = hash_join_key(&[ValueRef::from_f64(-10.0)], &collations); assert_eq!(h_neg_ten_int, h_neg_ten_float); // Positive/negative values should still differ assert_ne!(h_ten_int, h_neg_ten_int); } #[test] fn test_keys_equal() { let key1 = vec![Value::from_i64(42), Value::Text("hello".to_string().into())]; let key2 = vec![ ValueRef::from_i64(42), ValueRef::Text(crate::types::TextRef::new( "hello", crate::types::TextSubtype::Text, )), ]; let key3 = vec![ ValueRef::from_i64(43), ValueRef::Text(crate::types::TextRef::new( "hello", crate::types::TextSubtype::Text, )), ]; let collations = vec![CollationSeq::Binary, CollationSeq::Binary]; assert!(keys_equal(&key1, &key2, &collations)); assert!(!keys_equal(&key1, &key3, &collations)); } #[test] fn test_hash_table_basic() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024 * 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); // Insert some entries (late materialization - only store rowids) let key1 = vec![Value::from_i64(1)]; let _ = ht.insert(key1.clone(), 100, vec![], None).unwrap(); let key2 = vec![Value::from_i64(2)]; let _ = ht.insert(key2.clone(), 200, vec![], None).unwrap(); let _ = ht.finalize_build(None); // Probe for key1 let result = ht.probe(key1, None); assert!(result.is_some()); let entry1 = result.unwrap(); assert_eq!(entry1.key_values[0].as_ref(), ValueRef::from_i64(1)); assert_eq!(entry1.rowid, 100); // Probe for key2 let result = ht.probe(key2, None); assert!(result.is_some()); let entry2 = result.unwrap(); assert_eq!(entry2.key_values[0].as_ref(), ValueRef::from_i64(2)); assert_eq!(entry2.rowid, 200); // Probe for non-existent key let result = ht.probe(vec![Value::from_i64(999)], None); assert!(result.is_none()); } #[test] fn test_hash_table_collisions() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 2, // Small number to force collisions mem_budget: 1024 * 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); // Insert multiple entries (late materialization - only store rowids) for i in 0..10 { let key = vec![Value::from_i64(i)]; let _ = ht.insert(key, i * 100, vec![], None).unwrap(); } let _ = ht.finalize_build(None); // Verify all entries can be found for i in 0..10 { let result = ht.probe(vec![Value::from_i64(i)], None); assert!(result.is_some()); let entry = result.unwrap(); assert_eq!(entry.key_values[0].as_ref(), ValueRef::from_i64(i)); assert_eq!(entry.rowid, i * 100); } } #[test] fn test_hash_table_duplicate_keys() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024 * 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); // Insert multiple entries with the same key let key = vec![Value::from_i64(42)]; for i in 0..3 { let _ = ht.insert(key.clone(), 1000 + i, vec![], None).unwrap(); } let _ = ht.finalize_build(None); // Probe should return first match let result = ht.probe(key, None); assert!(result.is_some()); assert_eq!(result.unwrap().rowid, 1000); // next_match should return additional matches let result2 = ht.next_match(); assert!(result2.is_some()); assert_eq!(result2.unwrap().rowid, 1001); let result3 = ht.next_match(); assert!(result3.is_some()); assert_eq!(result3.unwrap().rowid, 1002); // No more matches let result4 = ht.next_match(); assert!(result4.is_none()); } #[test] fn test_hash_entry_serialization() { // Test that entries serialize and deserialize correctly let entry = HashEntry::new( 12345, vec![ Value::from_i64(42), Value::Text("hello".to_string().into()), Value::Null, Value::from_f64(std::f64::consts::PI), ], 100, ); let mut buf = Vec::new(); entry.serialize(&mut buf); let (deserialized, consumed) = HashEntry::deserialize(&buf).unwrap(); assert_eq!(consumed, buf.len()); assert_eq!(deserialized.hash, entry.hash); assert_eq!(deserialized.rowid, entry.rowid); assert_eq!(deserialized.key_values.len(), entry.key_values.len()); for (v1, v2) in deserialized.key_values.iter().zip(entry.key_values.iter()) { match (v1, v2) { (Value::Numeric(Numeric::Integer(i1)), Value::Numeric(Numeric::Integer(i2))) => { assert_eq!(i1, i2) } (Value::Text(t1), Value::Text(t2)) => assert_eq!(t1.as_str(), t2.as_str()), (Value::Numeric(Numeric::Float(f1)), Value::Numeric(Numeric::Float(f2))) => { assert!((f64::from(*f1) - f64::from(*f2)).abs() < 1e-10) } (Value::Null, Value::Null) => {} _ => panic!("Value type mismatch"), } } } #[test] fn test_serialize_to_slice_matches_serialize() { // Test that serialize_to_slice produces identical output to serialize let entry = HashEntry::new_with_payload( 12345, vec![ Value::from_i64(42), Value::Text("hello world".to_string().into()), Value::Null, Value::from_f64(std::f64::consts::PI), ], 100, vec![Value::Blob(vec![1, 2, 3, 4, 5]), Value::from_i64(-999)], ); // Serialize using the Vec-based method let mut vec_buf = Vec::new(); entry.serialize(&mut vec_buf); // Serialize using the slice-based method let size = entry.serialized_size(); assert_eq!( size, vec_buf.len(), "serialized_size must match actual size" ); let mut slice_buf = vec![0u8; size]; let written = entry.serialize_to_slice(&mut slice_buf); assert_eq!(written, size, "bytes written must match serialized_size"); // Both methods must produce identical output assert_eq!( vec_buf, slice_buf, "serialize and serialize_to_slice must produce identical output" ); // Verify the output is valid by deserializing let (deserialized, consumed) = HashEntry::deserialize(&slice_buf).unwrap(); assert_eq!(consumed, size); assert_eq!(deserialized.hash, entry.hash); assert_eq!(deserialized.rowid, entry.rowid); assert_eq!(deserialized.key_values.len(), entry.key_values.len()); assert_eq!( deserialized.payload_values.len(), entry.payload_values.len() ); } #[test] fn test_partition_from_hash() { // Test partition distribution let partitioning = Partitioning::new(16); let mut counts = [0usize; 16]; for i in 0u64..10000 { let hash = i.wrapping_mul(0x9E3779B97F4A7C15); // Simple hash spreading let partition = partitioning.index(hash); assert!(partition < counts.len()); counts[partition] += 1; } // Check reasonable distribution (each partition should have some entries) for count in counts { assert!(count > 0, "Each partition should have some entries"); } } #[test] fn test_spill_chunk_tracking() { // Test that SpilledPartition can track multiple chunks let mut partition = SpilledPartition::new(5); assert_eq!(partition.partition_idx, 5); assert!(partition.chunks.is_empty()); assert_eq!(partition.total_size_bytes(), 0); assert_eq!(partition.total_num_entries(), 0); // Add first chunk partition.add_chunk(0, 1000, 50); assert_eq!(partition.chunks.len(), 1); assert_eq!(partition.total_size_bytes(), 1000); assert_eq!(partition.total_num_entries(), 50); // Add second chunk partition.add_chunk(1000, 500, 25); assert_eq!(partition.chunks.len(), 2); assert_eq!(partition.total_size_bytes(), 1500); assert_eq!(partition.total_num_entries(), 75); // Check individual chunks assert_eq!(partition.chunks[0].file_offset, 0); assert_eq!(partition.chunks[0].size_bytes, 1000); assert_eq!(partition.chunks[1].file_offset, 1000); assert_eq!(partition.chunks[1].size_bytes, 500); } #[test] fn test_partition_count_override() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: Some(64), }; let mut ht = HashTable::new(config, io); insert_many_force_spill(&mut ht, 0, 1024); let _ = ht.finalize_build(None).unwrap(); assert!(ht.has_spilled()); let spill_state = ht.spill_state.as_ref().expect("spill state exists"); assert_eq!(spill_state.partitioning.count, 64); } #[test] fn test_adaptive_partition_count_bounds() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); insert_many_force_spill(&mut ht, 0, 1024); let _ = ht.finalize_build(None).unwrap(); assert!(ht.has_spilled()); let spill_state = ht.spill_state.as_ref().expect("spill state exists"); let count = spill_state.partitioning.count; assert!(count.is_power_of_two()); assert!(count >= MIN_PARTITIONS); assert!(count <= MAX_PARTITIONS); } #[test] fn test_spill_streaming_parse_multiple_chunks() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: Some(16), }; let mut ht = HashTable::new(config, io); let key = vec![Value::from_i64(1)]; for i in 0..2048 { match ht.insert(key.clone(), i, vec![], None).unwrap() { IOResult::Done(()) => {} IOResult::IO(_) => panic!("memory IO"), } } match ht.finalize_build(None).unwrap() { IOResult::Done(()) => {} IOResult::IO(_) => panic!("memory IO"), } assert!(ht.has_spilled()); let partition_idx = ht.partition_for_keys(&key); { let spill_state = ht.spill_state.as_ref().expect("spill state exists"); let partition = spill_state .find_partition(partition_idx) .expect("partition exists"); assert!(partition.chunks.len() > 1, "expected multiple spill chunks"); } while let IOResult::IO(_) = ht.load_spilled_partition(partition_idx, None).unwrap() {} assert!(ht.is_partition_loaded(partition_idx)); let entry = ht.probe_partition(partition_idx, &key, None).unwrap(); assert_eq!(entry.rowid, 0); let mut matches = 1usize; while ht.next_match().is_some() { matches += 1; } assert_eq!(matches, 2048); } #[test] fn test_load_partition_empty_chunk() { let io: Arc = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: Some(16), }; let mut ht = HashTable::new(config, io.clone()); let partitioning = Partitioning::new(16); let temp_file = TempFile::with_temp_store(&io, crate::TempStore::Default).unwrap(); let mut partition = SpilledPartition::new(0); partition.add_chunk(0, 0, 0); let spill_state = SpillState { partition_buffers: (0..partitioning.count) .map(|_| PartitionBuffer::new()) .collect(), partitions: vec![partition], next_spill_offset: 0, temp_file, partitioning, }; ht.spill_state = Some(spill_state); ht.state = HashTableState::Probing; while let IOResult::IO(_) = ht.load_spilled_partition(0, None).unwrap() {} assert!(ht.is_partition_loaded(0)); } #[test] fn test_load_partition_truncated_chunk() { let io: Arc = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: Some(16), }; let mut ht = HashTable::new(config, io.clone()); let entry = HashEntry::new(1, vec![Value::from_i64(1)], 7); let mut buf = Vec::new(); entry.serialize(&mut buf); let truncated = &buf[..buf.len() - 1]; let temp_file = TempFile::with_temp_store(&io, crate::TempStore::Default).unwrap(); let write_buf = Buffer::new_temporary(truncated.len()); write_buf.as_mut_slice().copy_from_slice(truncated); let write_buf = Arc::new(write_buf); let completion = temp_file .file .pwrite(0, write_buf, Completion::new_write(|_| {})) .unwrap(); assert!(completion.finished(), "memory write should complete"); let partitioning = Partitioning::new(16); let mut partition = SpilledPartition::new(0); partition.add_chunk(0, truncated.len(), 1); let spill_state = SpillState { partition_buffers: (0..partitioning.count) .map(|_| PartitionBuffer::new()) .collect(), partitions: vec![partition], next_spill_offset: truncated.len() as u64, temp_file, partitioning, }; ht.spill_state = Some(spill_state); ht.state = HashTableState::Probing; let mut saw_err = false; loop { match ht.load_spilled_partition(0, None) { Ok(IOResult::Done(())) => break, Ok(IOResult::IO(_)) => continue, Err(_) => { saw_err = true; break; } } } assert!(saw_err, "truncated chunk should return an error"); } #[test] fn test_hash_function_respects_collation_nocase() { use crate::types::{TextRef, TextSubtype}; let keys1 = vec![ValueRef::Text(TextRef::new("Hello", TextSubtype::Text))]; let keys2 = vec![ValueRef::Text(TextRef::new("hello", TextSubtype::Text))]; // Under BINARY: hashes must differ let bin_coll = vec![CollationSeq::Binary]; let h1_bin = hash_join_key(&keys1, &bin_coll); let h2_bin = hash_join_key(&keys2, &bin_coll); assert_ne!(h1_bin, h2_bin); // Under NOCASE: hashes should be equal let nocase_coll = vec![CollationSeq::NoCase]; let h1_nc = hash_join_key(&keys1, &nocase_coll); let h2_nc = hash_join_key(&keys2, &nocase_coll); assert_eq!(h1_nc, h2_nc); } #[test] fn test_hash_nocase_preserves_non_ascii() { use crate::types::{TextRef, TextSubtype}; // SQLite NOCASE only affects ASCII a-z/A-Z. // Non-ASCII characters like ü should hash identically regardless of case conversion. let keys1 = vec![ValueRef::Text(TextRef::new("über", TextSubtype::Text))]; let keys2 = vec![ValueRef::Text(TextRef::new("ÜBER", TextSubtype::Text))]; // Under NOCASE: ASCII portion differs (b/B), so hashes should differ // (because SQLite NOCASE doesn't handle Unicode case folding) let nocase_coll = vec![CollationSeq::NoCase]; let h1 = hash_join_key(&keys1, &nocase_coll); let h2 = hash_join_key(&keys2, &nocase_coll); // The 'b' and 'B' will be lowercased to 'b', but the 'ü' and 'Ü' are not // ASCII so they remain as-is. Since ü != Ü at byte level, hashes will differ. // This is correct SQLite NOCASE behavior (ASCII-only case folding). assert_ne!( h1, h2, "non-ASCII chars should not be case-folded by NOCASE" ); } #[test] fn test_values_equal_with_collations() { use crate::types::{TextRef, TextSubtype}; let h1 = ValueRef::Text(TextRef::new("Hello ", TextSubtype::Text)); let h2 = ValueRef::Text(TextRef::new("hello", TextSubtype::Text)); // Binary: case / trailing spaces matter assert!(!values_equal(h1, h2, CollationSeq::Binary)); // NOCASE: case-insensitive but trailing spaces still matter -> likely false assert!(!values_equal(h1, h2, CollationSeq::NoCase)); // RTRIM: ignore trailing spaces, but case is still significant let h3 = ValueRef::Text(TextRef::new("Hello", TextSubtype::Text)); assert!(values_equal(h1, h3, CollationSeq::Rtrim)); } #[test] fn test_keys_equal_with_collations() { use crate::types::{TextRef, TextSubtype}; let key1 = vec![Value::Text("Hello".into())]; let key2 = vec![ValueRef::Text(TextRef::new("hello", TextSubtype::Text))]; // Binary: not equal assert!(!keys_equal(&key1, &key2, &[CollationSeq::Binary])); // NOCASE: equal assert!(keys_equal(&key1, &key2, &[CollationSeq::NoCase])); } #[test] fn test_hash_entry_deserialization_truncated() { let entry = HashEntry::new(123, vec![Value::from_i64(1), Value::Text("abc".into())], 42); let mut buf = Vec::new(); entry.serialize(&mut buf); // Cut off the buffer mid-entry let truncated = &buf[..buf.len() - 2]; let res = HashEntry::deserialize(truncated); assert!( res.is_err(), "truncated buffer should be rejected as corrupt" ); } #[test] fn test_hash_entry_deserialization_garbage_type_tag() { let entry = HashEntry::new(1, vec![Value::from_i64(10)], 7); let mut buf = Vec::new(); entry.serialize(&mut buf); // Compute the exact offset of the *first* type tag. // Layout: [0..8] hash | [8..16] rowid | varint(num_keys) | type | payload... let mut corrupted = buf.clone(); let mut offset = 16; let (_num_keys, varint_len) = read_varint(&corrupted[offset..]).unwrap(); offset += varint_len; corrupted[offset] = 0xFF; let res = HashEntry::deserialize(&corrupted); assert!( res.is_err(), "invalid type tag should be rejected as corrupt" ); } fn insert_many_force_spill(ht: &mut HashTable, start: i64, count: i64) { for i in 0..count { let rowid = start + i; let key = vec![Value::from_i64(rowid)]; let _ = ht.insert(key, rowid, vec![], None); } } #[test] fn test_hash_table_spill_and_load_partition_round_trip() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, // very small budget to force spill mem_budget: 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, ..Default::default() }; let mut ht = HashTable::new(config, io); // Insert enough fat rows to exceed budget and force spills insert_many_force_spill(&mut ht, 0, 1024); let _ = ht.finalize_build(None).unwrap(); assert!(ht.has_spilled(), "hash table should have spilled"); // Pick a key and find its partition let probe_key = vec![Value::from_i64(10)]; let partition_idx = ht.partition_for_keys(&probe_key); // Load that partition into memory match ht.load_spilled_partition(partition_idx, None).unwrap() { IOResult::Done(()) => {} IOResult::IO(_) => panic!("test harness must drive IO completions here"), } assert!( ht.is_partition_loaded(partition_idx), "partition must be resident after load_spilled_partition" ); // Probe via partition API let entry = ht.probe_partition(partition_idx, &probe_key, None); assert!(entry.is_some()); // here assert_eq!(entry.unwrap().rowid, 10); } #[test] fn test_partition_lru_eviction() { let io = Arc::new(MemoryIO::new()); // tiny mem_budget so only ~1 partition can stay resident let config = HashTableConfig { initial_buckets: 4, mem_budget: 8 * 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); // Insert two disjoint key ranges that will hash to different partitions insert_many_force_spill(&mut ht, 0, 256); insert_many_force_spill(&mut ht, 256, 1024); let _ = ht.finalize_build(None).unwrap(); assert!(ht.has_spilled()); let key_a = vec![Value::from_i64(1)]; let key_b = vec![Value::from_i64(10_001)]; let pa = ht.partition_for_keys(&key_a); let pb = ht.partition_for_keys(&key_b); assert_ne!(pa, pb); // Load partition A while let IOResult::IO(_) = ht.load_spilled_partition(pa, None).unwrap() {} assert!(ht.is_partition_loaded(pa)); // Now load partition B, this should (under tight memory) evict A let _ = ht.load_spilled_partition(pb, None).unwrap(); assert!(ht.is_partition_loaded(pb)); // Depending on mem_budget and actual entry sizes, A should now be evicted // We can't *guarantee* that without knowing exact sizes, but in practice // this test will detect regressions in the LRU bookkeeping. assert!( !ht.is_partition_loaded(pa) || ht.loaded_partitions_mem <= ht.mem_budget, "either partition A is evicted, or loaded memory is within budget" ); } #[test] fn test_probe_partition_with_duplicate_keys() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 8 * 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); let key = vec![Value::from_i64(42)]; for i in 0..1024 { match ht.insert(key.clone(), 1000 + i, vec![], None).unwrap() { IOResult::Done(()) => {} IOResult::IO(_) => panic!("memory IO"), } } match ht.finalize_build(None).unwrap() { IOResult::Done(()) => {} IOResult::IO(_) => panic!("memory IO"), } assert!(ht.has_spilled()); let partition_idx = ht.partition_for_keys(&key); match ht.load_spilled_partition(partition_idx, None).unwrap() { IOResult::Done(()) => {} IOResult::IO(_) => panic!("memory IO"), } assert!(ht.is_partition_loaded(partition_idx)); // First probe should give us the first rowid let entry1 = ht.probe_partition(partition_idx, &key, None).unwrap(); assert_eq!(entry1.rowid, 1000); // Then iterate through the rest with next_match for i in 0..1023 { let next = ht.next_match().unwrap(); assert_eq!(next.rowid, 1001 + i); } assert!(ht.next_match().is_none()); } #[test] fn test_hash_table_with_payload() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024 * 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); // Insert entries with payload values (simulating cached result columns) let key1 = vec![Value::from_i64(1)]; let payload1 = vec![ Value::Text("Alice".into()), Value::from_i64(30), Value::from_f64(1000.50), ]; let _ = ht.insert(key1.clone(), 100, payload1, None).unwrap(); let key2 = vec![Value::from_i64(2)]; let payload2 = vec![ Value::Text("Bob".into()), Value::from_i64(25), Value::from_f64(2000.75), ]; let _ = ht.insert(key2.clone(), 200, payload2, None).unwrap(); let _ = ht.finalize_build(None); // Probe and verify payload is returned correctly let result = ht.probe(key1, None); assert!(result.is_some()); let entry1 = result.unwrap(); assert_eq!(entry1.rowid, 100); assert!(entry1.has_payload()); assert_eq!(entry1.payload_values.len(), 3); assert_eq!(entry1.payload_values[0], Value::Text("Alice".into())); assert_eq!(entry1.payload_values[1], Value::from_i64(30)); assert_eq!(entry1.payload_values[2], Value::from_f64(1000.50)); let result = ht.probe(key2, None); assert!(result.is_some()); let entry2 = result.unwrap(); assert_eq!(entry2.rowid, 200); assert!(entry2.has_payload()); assert_eq!(entry2.payload_values[0], Value::Text("Bob".into())); assert_eq!(entry2.payload_values[1], Value::from_i64(25)); assert_eq!(entry2.payload_values[2], Value::from_f64(2000.75)); } #[test] fn test_hash_table_payload_with_nulls() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024 * 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); // Insert entry with NULL values in payload let key = vec![Value::from_i64(1)]; let payload = vec![Value::Null, Value::Text("test".into()), Value::Null]; let _ = ht.insert(key.clone(), 100, payload, None).unwrap(); let _ = ht.finalize_build(None); let result = ht.probe(key, None); assert!(result.is_some()); let entry = result.unwrap(); assert_eq!(entry.payload_values.len(), 3); assert_eq!(entry.payload_values[0], Value::Null); assert_eq!(entry.payload_values[1], Value::Text("test".into())); assert_eq!(entry.payload_values[2], Value::Null); } #[test] fn test_null_keys_are_skipped() { // In SQL, NULL = NULL is false (actually NULL which is falsy). // Hash joins should skip rows with NULL keys during both insert and probe. let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024 * 1024, num_keys: 2, collations: vec![CollationSeq::Binary, CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); // Insert entry with NULL key - should be silently skipped let null_key = vec![Value::Null, Value::from_i64(1)]; let _ = ht.insert(null_key.clone(), 100, vec![], None).unwrap(); // Insert entry with non-NULL keys let valid_key = vec![Value::from_i64(1), Value::from_i64(2)]; let _ = ht.insert(valid_key.clone(), 200, vec![], None).unwrap(); // Insert another entry where second key is NULL let null_key2 = vec![Value::from_i64(1), Value::Null]; let _ = ht.insert(null_key2.clone(), 300, vec![], None).unwrap(); let _ = ht.finalize_build(None); // Only one entry should be in the table (the one with valid keys) assert_eq!(ht.num_entries, 1); // Probing with NULL key should return None let result = ht.probe(null_key, None); assert!(result.is_none()); // Probing with valid key should return the entry let result = ht.probe(valid_key, None); assert!(result.is_some()); assert_eq!(result.unwrap().rowid, 200); // Probing with NULL in second position should also return None let result = ht.probe(null_key2, None); assert!(result.is_none()); } #[test] fn test_hash_table_payload_with_blobs() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024 * 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); // Insert entry with blob payload let key = vec![Value::from_i64(1)]; let blob_data = vec![0xDE, 0xAD, 0xBE, 0xEF]; let payload = vec![Value::Blob(blob_data.clone()), Value::from_i64(42)]; let _ = ht.insert(key.clone(), 100, payload, None).unwrap(); let _ = ht.finalize_build(None); let result = ht.probe(key, None); assert!(result.is_some()); let entry = result.unwrap(); assert_eq!(entry.payload_values.len(), 2); assert_eq!(entry.payload_values[0], Value::Blob(blob_data)); assert_eq!(entry.payload_values[1], Value::from_i64(42)); } #[test] fn test_hash_table_payload_duplicate_keys() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024 * 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, partition_count: None, }; let mut ht = HashTable::new(config, io); // Insert multiple entries with the same key but different payloads let key = vec![Value::from_i64(42)]; let _ = ht .insert( key.clone(), 100, vec![Value::Text("first".into()), Value::from_i64(1)], None, ) .unwrap(); let _ = ht .insert( key.clone(), 200, vec![Value::Text("second".into()), Value::from_i64(2)], None, ) .unwrap(); let _ = ht .insert( key.clone(), 300, vec![Value::Text("third".into()), Value::from_i64(3)], None, ) .unwrap(); let _ = ht.finalize_build(None); // First probe should return first match let result = ht.probe(key, None); assert!(result.is_some()); let entry1 = result.unwrap(); assert_eq!(entry1.rowid, 100); assert_eq!(entry1.payload_values[0], Value::Text("first".into())); assert_eq!(entry1.payload_values[1], Value::from_i64(1)); // next_match should return subsequent matches with their payloads let entry2 = ht.next_match().unwrap(); assert_eq!(entry2.rowid, 200); assert_eq!(entry2.payload_values[0], Value::Text("second".into())); assert_eq!(entry2.payload_values[1], Value::from_i64(2)); let entry3 = ht.next_match().unwrap(); assert_eq!(entry3.rowid, 300); assert_eq!(entry3.payload_values[0], Value::Text("third".into())); assert_eq!(entry3.payload_values[1], Value::from_i64(3)); // No more matches assert!(ht.next_match().is_none()); } #[test] fn test_hash_entry_payload_serialization() { // Test that payload values survive serialization/deserialization let entry = HashEntry::new_with_payload( 12345, vec![Value::from_i64(1), Value::Text("key".into())], 100, vec![ Value::Text("payload_text".into()), Value::from_i64(999), Value::from_f64(std::f64::consts::PI), Value::Null, Value::Blob(vec![1, 2, 3, 4]), ], ); let mut buf = Vec::new(); entry.serialize(&mut buf); let (deserialized, bytes_consumed) = HashEntry::deserialize(&buf).unwrap(); assert_eq!(bytes_consumed, buf.len()); // Verify key values assert_eq!(deserialized.hash, entry.hash); assert_eq!(deserialized.rowid, entry.rowid); assert_eq!(deserialized.key_values.len(), 2); assert_eq!(deserialized.key_values[0], Value::from_i64(1)); assert_eq!(deserialized.key_values[1], Value::Text("key".into())); // Verify payload values assert_eq!(deserialized.payload_values.len(), 5); assert_eq!( deserialized.payload_values[0], Value::Text("payload_text".into()) ); assert_eq!(deserialized.payload_values[1], Value::from_i64(999)); assert_eq!( deserialized.payload_values[2], Value::from_f64(std::f64::consts::PI) ); assert_eq!(deserialized.payload_values[3], Value::Null); assert_eq!( deserialized.payload_values[4], Value::Blob(vec![1, 2, 3, 4]) ); } #[test] fn test_hash_entry_empty_payload() { // Test that entries without payload work correctly let entry = HashEntry::new(12345, vec![Value::from_i64(1)], 100); assert!(!entry.has_payload()); assert!(entry.payload_values.is_empty()); // Serialization should still work let mut buf = Vec::new(); entry.serialize(&mut buf); let (deserialized, _) = HashEntry::deserialize(&buf).unwrap(); assert!(!deserialized.has_payload()); assert!(deserialized.payload_values.is_empty()); assert_eq!(deserialized.rowid, 100); } #[test] fn test_hash_entry_size_includes_payload() { let entry_no_payload = HashEntry::new(12345, vec![Value::from_i64(1)], 100); let entry_with_payload = HashEntry::new_with_payload( 12345, vec![Value::from_i64(1)], 100, vec![ Value::Text("a]long payload string".into()), Value::from_i64(42), ], ); // Entry with payload should have larger size assert!(entry_with_payload.size_bytes() > entry_no_payload.size_bytes()); } // ── Grace hash join tests ────────────────────────────────────── /// Helper: build a spilled hash table with given keys and payloads fn make_spilled_ht_with_payload( io: Arc, build_keys: &[(i64, Vec)], // (rowid, key_values) payload: bool, ) -> HashTable { let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024, // tiny, forces spill num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, ..Default::default() }; let mut ht = HashTable::new(config, io); for (rowid, keys) in build_keys { let payload_values = if payload { vec![Value::Text(format!("payload_{rowid}").into())] } else { vec![] }; let _ = ht.insert(keys.clone(), *rowid, payload_values, None); } let _ = ht.finalize_build(None).unwrap(); ht } /// Helper: run grace processing with the new fine-grained API. /// Returns (build_rowid, probe_rowid) pairs for all matches found. fn run_grace_processing(ht: &mut HashTable) -> Vec<(i64, i64)> { let _ = ht.finalize_probe_spill(None).unwrap(); let mut matches = Vec::new(); if !ht.grace_begin() { return matches; } // Partition loop loop { match ht.grace_load_current_partition(None).unwrap() { IOResult::Done(true) => {} IOResult::Done(false) => break, _ => panic!("unexpected IO"), } // Probe entry loop loop { let entry = match ht.grace_next_probe_entry().unwrap() { IOResult::Done(entry) => entry, IOResult::IO(_) => panic!("unexpected IO"), }; let Some(entry) = entry else { break; }; // Use probe_partition + next_match to find build matches let key_values = entry.key_values; let probe_rowid = entry.probe_rowid; let partition_idx = ht.partition_for_keys(&key_values); if ht .probe_partition(partition_idx, &key_values, None) .is_some() { // First match from probe_partition let build_entry = ht.probe_partition(partition_idx, &key_values, None); // Re-probe to get entry again if let Some(build_entry) = build_entry { matches.push((build_entry.rowid, probe_rowid)); } // Get additional matches via next_match while let Some(build_entry) = ht.next_match() { matches.push((build_entry.rowid, probe_rowid)); } } } if !ht.grace_advance_partition() { break; } } matches } #[test] fn test_grace_basic() { let io = Arc::new(MemoryIO::new()); let build_keys: Vec<(i64, Vec)> = (0..200).map(|i| (i, vec![Value::from_i64(i)])).collect(); let mut ht = make_spilled_ht_with_payload(io, &build_keys, true); assert!(ht.has_spilled(), "should have spilled"); // Buffer probe rows for keys that map to spilled partitions let mut buffered = 0; for i in 0..200 { let key = vec![Value::from_i64(i)]; let partition_idx = ht.partition_for_keys(&key); if !ht.is_partition_loaded(partition_idx) { let _ = ht.buffer_probe_row(key, i + 1000, None).unwrap(); buffered += 1; } } assert!(buffered > 0, "should have buffered some probe rows"); let matches = run_grace_processing(&mut ht); // Every buffered probe row should have found a match assert_eq!( matches.len(), buffered, "each buffered probe row should match exactly one build row" ); // Verify correctness: build_rowid should equal probe_rowid - 1000 for (build_rowid, probe_rowid) in &matches { assert_eq!( *build_rowid, probe_rowid - 1000, "build_rowid should match probe key" ); } } #[test] fn test_grace_no_spill_noop() { let io = Arc::new(MemoryIO::new()); // Use large budget so nothing spills let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024 * 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: false, ..Default::default() }; let mut ht = HashTable::new(config, io); for i in 0..10 { let _ = ht.insert(vec![Value::from_i64(i)], i, vec![], None); } let _ = ht.finalize_build(None).unwrap(); assert!(!ht.has_spilled(), "should NOT have spilled"); // grace_begin should return false since nothing was spilled assert!( !ht.grace_begin(), "grace_begin should return false when nothing spilled" ); } #[test] fn test_grace_duplicate_keys() { let io = Arc::new(MemoryIO::new()); // Insert multiple build rows with same key let mut build_keys: Vec<(i64, Vec)> = Vec::new(); for i in 0..100 { // 3 build rows per key value build_keys.push((i * 3, vec![Value::from_i64(i)])); build_keys.push((i * 3 + 1, vec![Value::from_i64(i)])); build_keys.push((i * 3 + 2, vec![Value::from_i64(i)])); } let mut ht = make_spilled_ht_with_payload(io, &build_keys, false); assert!(ht.has_spilled()); // Buffer probe rows let mut buffered_keys = Vec::new(); for i in 0..100 { let key = vec![Value::from_i64(i)]; let partition_idx = ht.partition_for_keys(&key); if !ht.is_partition_loaded(partition_idx) { let _ = ht.buffer_probe_row(key, i + 500, None).unwrap(); buffered_keys.push(i); } } let matches = run_grace_processing(&mut ht); // Each buffered probe key should match 3 build rows assert_eq!( matches.len(), buffered_keys.len() * 3, "each probe key should find 3 matches" ); } #[test] fn test_grace_empty_partitions() { let io = Arc::new(MemoryIO::new()); // Build with keys 0..100, probe with keys 200..300 (no overlap) let build_keys: Vec<(i64, Vec)> = (0..200).map(|i| (i, vec![Value::from_i64(i)])).collect(); let mut ht = make_spilled_ht_with_payload(io, &build_keys, false); assert!(ht.has_spilled()); // Buffer probe rows with non-matching keys for i in 1000..1050 { let key = vec![Value::from_i64(i)]; let partition_idx = ht.partition_for_keys(&key); if !ht.is_partition_loaded(partition_idx) { let _ = ht.buffer_probe_row(key, i, None).unwrap(); } } let matches = run_grace_processing(&mut ht); assert_eq!( matches.len(), 0, "non-matching keys should produce no matches" ); } #[test] fn test_grace_null_keys() { let io = Arc::new(MemoryIO::new()); let build_keys: Vec<(i64, Vec)> = (0..200).map(|i| (i, vec![Value::from_i64(i)])).collect(); let mut ht = make_spilled_ht_with_payload(io, &build_keys, false); assert!(ht.has_spilled()); // Buffer a probe row with NULL key - should be skipped let null_key = vec![Value::Null]; // NULL keys can't match, so we just verify no crash let partition_idx = ht.partition_for_keys(&[Value::from_i64(0)]); if !ht.is_partition_loaded(partition_idx) { // Buffer with a valid key to ensure grace processing runs let _ = ht .buffer_probe_row(vec![Value::from_i64(0)], 999, None) .unwrap(); } // Buffer a null key row to the same partition let _ = ht.buffer_probe_row(null_key, 888, None).unwrap(); let _ = ht.finalize_probe_spill(None).unwrap(); // Should not crash; NULL key entries should return from grace_next_probe_entry assert!(ht.grace_begin(), "should have partitions to process"); loop { match ht.grace_load_current_partition(None).unwrap() { IOResult::Done(true) => {} IOResult::Done(false) => break, _ => panic!("unexpected IO"), } loop { let entry = match ht.grace_next_probe_entry().unwrap() { IOResult::Done(entry) => entry, IOResult::IO(_) => panic!("unexpected IO"), }; if entry.is_none() { break; } // NULL key probe entries are still returned; the VDBE's HashProbe // handles NULL skip. Just verify no crash. } if !ht.grace_advance_partition() { break; } } } #[test] fn test_grace_with_payload() { let io = Arc::new(MemoryIO::new()); let build_keys: Vec<(i64, Vec)> = (0..200).map(|i| (i, vec![Value::from_i64(i)])).collect(); let mut ht = make_spilled_ht_with_payload(io, &build_keys, true); assert!(ht.has_spilled()); // Buffer some probe rows let mut buffered = 0; for i in 0..200 { let key = vec![Value::from_i64(i)]; let partition_idx = ht.partition_for_keys(&key); if !ht.is_partition_loaded(partition_idx) { let _ = ht.buffer_probe_row(key, i + 1000, None).unwrap(); buffered += 1; } } let _ = ht.finalize_probe_spill(None).unwrap(); assert!(ht.grace_begin(), "should have partitions to process"); let mut match_count = 0; loop { match ht.grace_load_current_partition(None).unwrap() { IOResult::Done(true) => {} IOResult::Done(false) => break, _ => panic!("unexpected IO"), } loop { let entry = match ht.grace_next_probe_entry().unwrap() { IOResult::Done(entry) => entry, IOResult::IO(_) => panic!("unexpected IO"), }; let Some(entry) = entry else { break; }; let key_values = entry.key_values; let partition_idx = ht.partition_for_keys(&key_values); if let Some(build_entry) = ht.probe_partition(partition_idx, &key_values, None) { // Check payload was correctly round-tripped let expected_payload = format!("payload_{}", build_entry.rowid); assert_eq!(build_entry.payload_values.len(), 1); match &build_entry.payload_values[0] { Value::Text(t) => assert_eq!(t.as_str(), expected_payload.as_str()), other => panic!("expected text payload, got {other:?}"), } match_count += 1; } } if !ht.grace_advance_partition() { break; } } assert_eq!(match_count, buffered); } #[test] fn test_grace_unmatched_scan_uses_current_partition() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 4096, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: true, partition_count: Some(4), }; let mut ht = HashTable::new(config, io); for i in 0..400 { let key = vec![Value::from_i64(i)]; let _ = ht.insert(key, i, vec![], None); } let _ = ht.finalize_build(None).unwrap(); assert!(ht.has_spilled(), "should have spilled"); let mut keys_by_partition = std::collections::BTreeMap::>::new(); for i in 0..400 { let partition_idx = ht.partition_for_keys(&[Value::from_i64(i)]); keys_by_partition.entry(partition_idx).or_default().push(i); } let partitions_to_process: Vec = ht .spill_state .as_ref() .expect("spill state") .partitions .iter() .filter(|partition| !partition.chunks.is_empty()) .map(|partition| partition.partition_idx) .collect(); assert!( partitions_to_process.len() >= 2, "test requires at least two spilled partitions" ); let first_partition = partitions_to_process[0]; let second_partition = partitions_to_process[1]; let first_keys = keys_by_partition .get(&first_partition) .cloned() .expect("first partition keys"); let second_keys = keys_by_partition .get(&second_partition) .cloned() .expect("second partition keys"); for key in &first_keys { let _ = ht .buffer_probe_row(vec![Value::from_i64(*key)], key + 10_000, None) .unwrap(); } let _ = ht.finalize_probe_spill(None).unwrap(); assert!(ht.grace_begin(), "should enter grace"); match ht.grace_load_current_partition(None).unwrap() { IOResult::Done(true) => {} other => panic!("unexpected grace load result: {other:?}"), } assert_eq!( ht.grace_state .as_ref() .expect("grace state") .current_partition_idx(), Some(first_partition) ); loop { let entry = match ht.grace_next_probe_entry().unwrap() { IOResult::Done(entry) => entry, IOResult::IO(_) => panic!("unexpected IO"), }; let Some(entry) = entry else { break; }; let partition_idx = ht.partition_for_keys(&entry.key_values); if ht .probe_partition(partition_idx, &entry.key_values, None) .is_some() { ht.mark_current_matched(); while ht.next_match().is_some() { ht.mark_current_matched(); } } } ht.begin_unmatched_scan(); assert!( ht.next_unmatched().is_none(), "all rows in the first partition were matched" ); assert!( ht.grace_advance_partition(), "should have another partition" ); match ht.grace_load_current_partition(None).unwrap() { IOResult::Done(true) => {} other => panic!("unexpected grace load result: {other:?}"), } assert_eq!( ht.grace_state .as_ref() .expect("grace state") .current_partition_idx(), Some(second_partition) ); match ht.load_spilled_partition(first_partition, None).unwrap() { IOResult::Done(()) => {} other => panic!("unexpected spill load result: {other:?}"), } ht.begin_unmatched_scan(); assert_eq!( ht.unmatched_scan_current_partition(), Some(second_partition), "grace unmatched scan must target the active grace partition" ); match ht.load_spilled_partition(second_partition, None).unwrap() { IOResult::Done(()) => {} other => panic!("unexpected spill load result: {other:?}"), } let mut unmatched_rowids = Vec::new(); while let Some(entry) = ht.next_unmatched() { unmatched_rowids.push(entry.rowid); } unmatched_rowids.sort_unstable(); let mut expected = second_keys; expected.sort_unstable(); assert_eq!(unmatched_rowids, expected); } #[test] fn test_unmatched_scan_preserves_in_memory_partitions_before_grace() { let io = Arc::new(MemoryIO::new()); let config = HashTableConfig { initial_buckets: 4, mem_budget: 1024, num_keys: 1, collations: vec![CollationSeq::Binary], temp_store: crate::TempStore::Default, track_matched: true, partition_count: Some(16), }; let mut ht = HashTable::new(config, io); let mut next_rowid = 0i64; while !ht.has_spilled() { let _ = ht .insert(vec![Value::from_i64(next_rowid)], next_rowid, vec![], None) .unwrap(); next_rowid += 1; } let partition_keys: std::collections::BTreeMap> = (0..4096) .map(|i| (ht.partition_for_keys(&[Value::from_i64(i)]), i)) .fold( std::collections::BTreeMap::new(), |mut acc, (partition, key)| { acc.entry(partition).or_default().push(key); acc }, ); let hot_partition = ht .spill_state .as_ref() .expect("spill state") .partitions .first() .map(|partition| partition.partition_idx) .expect("expected at least one spilled partition"); let cold_partition = partition_keys .keys() .copied() .find(|partition_idx| { ht.spill_state .as_ref() .expect("spill state") .find_partition(*partition_idx) .is_none() }) .expect("expected at least one partition without spill chunks yet"); let hot_key = partition_keys .get(&hot_partition) .and_then(|keys| keys.first()) .copied() .expect("hot partition key"); let cold_key = partition_keys .get(&cold_partition) .and_then(|keys| keys.first()) .copied() .expect("cold partition key"); for _ in 0..160 { let _ = ht .insert(vec![Value::from_i64(hot_key)], next_rowid, vec![], None) .unwrap(); next_rowid += 1; } for _ in 0..6 { let _ = ht .insert(vec![Value::from_i64(cold_key)], next_rowid, vec![], None) .unwrap(); next_rowid += 1; } let _ = ht.finalize_build(None).unwrap(); assert!(ht.has_spilled(), "should have spilled"); let (spilled_partition, mut expected_unmatched) = { let spill_state = ht.spill_state.as_ref().expect("spill state"); let spilled_partition = spill_state .partitions .iter() .find(|partition| !partition.chunks.is_empty()) .map(|partition| partition.partition_idx) .expect("expected at least one spilled partition"); let expected_unmatched: Vec = spill_state .partitions .iter() .filter(|partition| partition.chunks.is_empty()) .flat_map(|partition| { partition .buckets .iter() .flat_map(|bucket| bucket.entries.iter().map(|entry| entry.rowid)) }) .collect(); (spilled_partition, expected_unmatched) }; assert!( !expected_unmatched.is_empty(), "expected at least one resident in-memory partition" ); let probe_key = (0..400) .map(|i| vec![Value::from_i64(i)]) .find(|key| ht.partition_for_keys(key) == spilled_partition) .expect("spilled partition should have at least one key"); let _ = ht.buffer_probe_row(probe_key, 10_000, None).unwrap(); assert!( ht.has_grace_partitions(), "probe buffering should enable grace" ); ht.begin_unmatched_scan(); let mut actual_unmatched = Vec::new(); while let Some(entry) = ht.next_unmatched() { actual_unmatched.push(entry.rowid); } expected_unmatched.sort_unstable(); actual_unmatched.sort_unstable(); assert_eq!(actual_unmatched, expected_unmatched); } } ================================================ FILE: core/vdbe/insn.rs ================================================ use std::{ num::{NonZero, NonZeroUsize}, sync::Arc, }; /// Convert a usize to u16 for instruction fields (registers, counts). /// Panics if the value exceeds u16::MAX. #[inline] pub fn to_u16(v: usize) -> u16 { v.try_into().expect("value exceeds u16::MAX") } use super::{execute, AggFunc, BranchOffset, CursorID, FuncCtx, InsnFunction, PageIdx}; use crate::{ schema::{BTreeTable, CheckConstraint, Column, Index}, storage::{pager::CreateBTreeFlags, wal::CheckpointMode}, translate::{collate::CollationSeq, emitter::TransactionMode}, types::KeyInfo, vdbe::affinity::Affinity, PreparedProgram, Value, }; use strum::EnumCount; use strum_macros::{EnumDiscriminants, FromRepr, VariantArray}; use turso_macros::Description; use turso_parser::ast::{ResolveType, SortOrder}; /// Known custom type comparator functions for sorting and MIN/MAX aggregates. /// These replace heap-allocated String names with a compact enum. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SortComparatorType { NumericLt, StringReverse, TestUintLt, ArrayLt, } /// Flags provided to comparison instructions (e.g. Eq, Ne) which determine behavior related to NULL values. #[derive(Clone, Copy, Debug, Default)] pub struct CmpInsFlags(usize); impl CmpInsFlags { const NULL_EQ: usize = 0x80; const JUMP_IF_NULL: usize = 0x10; const AFFINITY_MASK: usize = 0x47; const ARRAY_CMP: usize = 0x100; fn has(&self, flag: usize) -> bool { (self.0 & flag) != 0 } pub fn null_eq(mut self) -> Self { self.0 |= CmpInsFlags::NULL_EQ; self } pub fn jump_if_null(mut self) -> Self { self.0 |= CmpInsFlags::JUMP_IF_NULL; self } pub fn has_jump_if_null(&self) -> bool { self.has(CmpInsFlags::JUMP_IF_NULL) } pub fn has_nulleq(&self) -> bool { self.has(CmpInsFlags::NULL_EQ) } pub fn with_affinity(mut self, affinity: Affinity) -> Self { let aff_code = affinity.as_char_code() as usize; self.0 = (self.0 & !Self::AFFINITY_MASK) | aff_code; self } pub fn get_affinity(&self) -> Affinity { let aff_code = (self.0 & Self::AFFINITY_MASK) as u8; Affinity::from_char_code(aff_code) } pub fn array_cmp(mut self) -> Self { self.0 |= Self::ARRAY_CMP; self } pub fn has_array_cmp(&self) -> bool { self.has(Self::ARRAY_CMP) } } #[derive(Clone, Copy, Debug, Default)] pub struct IdxInsertFlags(pub u8); impl IdxInsertFlags { pub const APPEND: u8 = 0x01; // Hint: insert likely at the end pub const NCHANGE: u8 = 0x02; // Increment the change counter pub const USE_SEEK: u8 = 0x04; // Skip seek if last one was same key pub const NO_OP_DUPLICATE: u8 = 0x08; // Do not error on duplicate key pub fn new() -> Self { IdxInsertFlags(0) } pub fn has(&self, flag: u8) -> bool { (self.0 & flag) != 0 } pub fn append(mut self, append: bool) -> Self { if append { self.0 |= IdxInsertFlags::APPEND; } else { self.0 &= !IdxInsertFlags::APPEND; } self } pub fn use_seek(mut self, seek: bool) -> Self { if seek { self.0 |= IdxInsertFlags::USE_SEEK; } else { self.0 &= !IdxInsertFlags::USE_SEEK; } self } pub fn nchange(mut self, change: bool) -> Self { if change { self.0 |= IdxInsertFlags::NCHANGE; } else { self.0 &= !IdxInsertFlags::NCHANGE; } self } /// If this is set, we will not error on duplicate key. /// This is a bit of a hack we use to make ephemeral indexes for UNION work -- /// instead we should allow overwriting index interior cells, which we currently don't; /// this should (and will) be fixed in a future PR. pub fn no_op_duplicate(mut self) -> Self { self.0 |= IdxInsertFlags::NO_OP_DUPLICATE; self } } #[derive(Clone, Copy, Debug, Default)] pub struct InsertFlags(pub u8); impl InsertFlags { pub const UPDATE_ROWID_CHANGE: u8 = 0x01; // Flag indicating this is part of an UPDATE statement where the row's rowid is changed pub const REQUIRE_SEEK: u8 = 0x02; // Flag indicating that a seek is required to insert the row pub const EPHEMERAL_TABLE_INSERT: u8 = 0x04; // Flag indicating that this is an insert into an ephemeral table pub const SKIP_LAST_ROWID: u8 = 0x08; // Flag indicating that last_insert_rowid() must not be updated pub fn new() -> Self { InsertFlags(0) } pub fn has(&self, flag: u8) -> bool { (self.0 & flag) != 0 } pub fn require_seek(mut self) -> Self { self.0 |= InsertFlags::REQUIRE_SEEK; self } pub fn update_rowid_change(mut self) -> Self { self.0 |= InsertFlags::UPDATE_ROWID_CHANGE; self } pub fn is_ephemeral_table_insert(mut self) -> Self { self.0 |= InsertFlags::EPHEMERAL_TABLE_INSERT; self } pub fn skip_last_rowid(mut self) -> Self { self.0 |= InsertFlags::SKIP_LAST_ROWID; self } } #[derive(Clone, Copy, Debug)] pub enum RegisterOrLiteral { Register(usize), Literal(T), } #[derive(Debug, Clone, Copy)] pub enum SavepointOp { Begin, Release, RollbackTo, } impl From for RegisterOrLiteral { fn from(value: PageIdx) -> Self { RegisterOrLiteral::Literal(value) } } impl std::fmt::Display for RegisterOrLiteral { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Literal(lit) => lit.fmt(f), Self::Register(reg) => reg.fmt(f), } } } /// Data for HashBuild instruction (boxed to keep Insn small). #[derive(Debug, Clone)] pub struct HashBuildData { pub cursor_id: CursorID, pub key_start_reg: usize, pub num_keys: usize, pub hash_table_id: usize, pub mem_budget: usize, pub collations: Vec, /// Starting register for payload columns to store in the hash entry. /// When Some: payload_start_reg..payload_start_reg+num_payload-1 contain values to cache. pub payload_start_reg: Option, /// Number of payload columns to read pub num_payload: usize, /// Whether to track which entries are matched (for FULL OUTER JOIN). pub track_matched: bool, } /// Data for HashDistinct instruction (boxed to keep Insn small). #[derive(Debug, Clone)] pub struct HashDistinctData { pub hash_table_id: usize, pub key_start_reg: usize, pub num_keys: usize, pub collations: Vec, pub target_pc: BranchOffset, } // There are currently 190 opcodes in sqlite #[repr(u8)] #[derive(Description, Debug, Clone, EnumDiscriminants)] #[strum_discriminants(vis(pub(crate)))] #[strum_discriminants(derive(VariantArray, EnumCount, FromRepr))] #[strum_discriminants(name(InsnVariants))] pub enum Insn { /// Initialize the program state and jump to the given PC. Init { target_pc: BranchOffset, }, /// Write a NULL into register dest. If dest_end is Some, then also write NULL into register dest_end and every register in between dest and dest_end. If dest_end is not set, then only register dest is set to NULL. Null { dest: usize, dest_end: Option, }, /// Mark the beginning of a subroutine tha can be entered in-line. This opcode is identical to Null /// it has a different name only to make the byte code easier to read and verify BeginSubrtn { dest: usize, dest_end: Option, }, /// Move the cursor P1 to a null row. Any Column operations that occur while the cursor is on the null row will always write a NULL. NullRow { cursor_id: CursorID, }, /// Add two registers and store the result in a third register. Add { lhs: usize, rhs: usize, dest: usize, }, /// Subtract rhs from lhs and store in dest Subtract { lhs: usize, rhs: usize, dest: usize, }, /// Multiply two registers and store the result in a third register. Multiply { lhs: usize, rhs: usize, dest: usize, }, /// Updates the value of register dest_reg to the maximum of its current /// value and the value in src_reg. /// /// - dest_reg = max(int(dest_reg), int(src_reg)) /// /// Both registers are converted to integers before the comparison. MemMax { dest_reg: usize, // P1 src_reg: usize, // P2 }, /// Divide lhs by rhs and store the result in a third register. Divide { lhs: usize, rhs: usize, dest: usize, }, /// Compare two vectors of registers in reg(P1)..reg(P1+P3-1) (call this vector "A") and in reg(P2)..reg(P2+P3-1) ("B"). Save the result of the comparison for use by the next Jump instruct. Compare { start_reg_a: usize, start_reg_b: usize, count: usize, key_info: Vec, }, /// Place the result of rhs bitwise AND lhs in third register. BitAnd { lhs: usize, rhs: usize, dest: usize, }, /// Place the result of rhs bitwise OR lhs in third register. BitOr { lhs: usize, rhs: usize, dest: usize, }, /// Place the result of bitwise NOT register P1 in dest register. BitNot { reg: usize, dest: usize, }, /// Checkpoint the database (applying wal file content to database file). Checkpoint { database: usize, // checkpoint database P1 checkpoint_mode: CheckpointMode, // P2 checkpoint mode dest: usize, // P3 checkpoint result }, /// Divide lhs by rhs and place the remainder in dest register. Remainder { lhs: usize, rhs: usize, dest: usize, }, /// Jump to the instruction at address P1, P2, or P3 depending on whether in the most recent Compare instruction the P1 vector was less than, equal to, or greater than the P2 vector, respectively. Jump { target_pc_lt: BranchOffset, target_pc_eq: BranchOffset, target_pc_gt: BranchOffset, }, /// Move the P3 values in register P1..P1+P3-1 over into registers P2..P2+P3-1. Registers P1..P1+P3-1 are left holding a NULL. It is an error for register ranges P1..P1+P3-1 and P2..P2+P3-1 to overlap. It is an error for P3 to be less than 1. Move { source_reg: usize, dest_reg: usize, count: usize, }, /// If the given register is a positive integer, decrement it by decrement_by and jump to the given PC. IfPos { reg: usize, target_pc: BranchOffset, decrement_by: usize, }, /// If the given register is not NULL, jump to the given PC. NotNull { reg: usize, target_pc: BranchOffset, }, /// Compare two registers and jump to the given PC if they are equal. Eq { lhs: usize, rhs: usize, target_pc: BranchOffset, /// CmpInsFlags are nulleq (null = null) or jump_if_null. /// /// jump_if_null jumps if either of the operands is null. Used for "jump when false" logic. /// Eg. "SELECT * FROM users WHERE id = NULL" becomes: /// /// Without the jump_if_null flag it would not jump because the logical comparison "id != NULL" is never true. /// This flag indicates that if either is null we should still jump. flags: CmpInsFlags, collation: Option, }, /// Compute a hash on num_keys registers starting with r[key_reg]. Check to see if that hash /// is found in the bloom filter associated with the cursor/hash_table. If it is not present /// then jump to target_pc. Otherwise fall through. /// False negatives are harmless. It is always safe to fall through, even if the value is /// in the bloom filter. A false negative causes more CPU cycles to be used, but it should /// still yield the correct answer. However, an incorrect answer may well arise from a /// false positive - if the jump is taken when it should fall through. Filter { cursor_id: CursorID, /// Jump target if bloom filter says "definitely not present" target_pc: BranchOffset, /// Start register containing the key(s) to check key_reg: usize, /// Number of key registers to hash together num_keys: usize, }, /// Compute a hash on num_keys registers starting with r[key_reg] and add that hash to /// the bloom filter associated with the cursor/hash_table. FilterAdd { cursor_id: CursorID, key_reg: usize, num_keys: usize, }, /// Compare two registers and jump to the given PC if they are not equal. Ne { lhs: usize, rhs: usize, target_pc: BranchOffset, /// CmpInsFlags are nulleq (null = null) or jump_if_null. /// /// jump_if_null jumps if either of the operands is null. Used for "jump when false" logic. flags: CmpInsFlags, collation: Option, }, /// Compare two registers and jump to the given PC if the left-hand side is less than the right-hand side. Lt { lhs: usize, rhs: usize, target_pc: BranchOffset, /// jump_if_null: Jump if either of the operands is null. Used for "jump when false" logic. flags: CmpInsFlags, collation: Option, }, // Compare two registers and jump to the given PC if the left-hand side is less than or equal to the right-hand side. Le { lhs: usize, rhs: usize, target_pc: BranchOffset, /// jump_if_null: Jump if either of the operands is null. Used for "jump when false" logic. flags: CmpInsFlags, collation: Option, }, /// Compare two registers and jump to the given PC if the left-hand side is greater than the right-hand side. Gt { lhs: usize, rhs: usize, target_pc: BranchOffset, /// jump_if_null: Jump if either of the operands is null. Used for "jump when false" logic. flags: CmpInsFlags, collation: Option, }, /// Compare two registers and jump to the given PC if the left-hand side is greater than or equal to the right-hand side. Ge { lhs: usize, rhs: usize, target_pc: BranchOffset, /// jump_if_null: Jump if either of the operands is null. Used for "jump when false" logic. flags: CmpInsFlags, collation: Option, }, /// Jump to target_pc if r\[reg\] != 0 or (r\[reg\] == NULL && r\[jump_if_null\] != 0) If { reg: usize, // P1 target_pc: BranchOffset, // P2 /// P3. If r\[reg\] is null, jump iff r\[jump_if_null\] != 0 jump_if_null: bool, }, /// Jump to target_pc if r\[reg\] != 0 or (r\[reg\] == NULL && r\[jump_if_null\] != 0) IfNot { reg: usize, // P1 target_pc: BranchOffset, // P2 /// P3. If r\[reg\] is null, jump iff r\[jump_if_null\] != 0 jump_if_null: bool, }, /// Open a cursor for reading. OpenRead { cursor_id: CursorID, root_page: PageIdx, db: usize, }, /// Open a cursor for a virtual table. VOpen { cursor_id: CursorID, }, /// Create a new virtual table. VCreate { module_name: usize, // P1: Name of the module that contains the virtual table implementation table_name: usize, // P2: Name of the virtual table args_reg: Option, }, /// Initialize the position of the virtual table cursor. VFilter { cursor_id: CursorID, pc_if_empty: BranchOffset, arg_count: usize, args_reg: usize, idx_str: Option, idx_num: usize, }, /// Read a column from the current row of the virtual table cursor. VColumn { cursor_id: CursorID, column: usize, dest: usize, }, /// `VUpdate`: Virtual Table Insert/Update/Delete Instruction VUpdate { cursor_id: usize, // P1: Virtual table cursor number arg_count: usize, // P2: Number of arguments in argv[] start_reg: usize, // P3: Start register for argv[] conflict_action: u16, // P4: Conflict resolution flags }, /// Advance the virtual table cursor to the next row. /// TODO: async VNext { cursor_id: CursorID, pc_if_next: BranchOffset, }, /// P4 is the name of a virtual table in database P1. Call the xDestroy method of that table. VDestroy { /// Name of a virtual table being destroyed table_name: String, /// The database within which this virtual table needs to be destroyed (P1). db: usize, }, VBegin { /// The database within which this virtual table transaction needs to begin (P1). cursor_id: CursorID, }, VRename { /// The database within which this virtual table needs to be renamed (P1). cursor_id: CursorID, /// New name of the virtual table (P2). new_name_reg: usize, }, /// Open a cursor for a pseudo-table that contains a single row. OpenPseudo { cursor_id: CursorID, content_reg: usize, num_fields: usize, }, /// Rewind the cursor to the beginning of the B-Tree. Rewind { cursor_id: CursorID, pc_if_empty: BranchOffset, }, Last { cursor_id: CursorID, pc_if_empty: BranchOffset, }, /// Read a column from the current row of the cursor. Column { cursor_id: CursorID, column: usize, dest: usize, default: Option, }, TypeCheck { start_reg: usize, // P1 count: usize, // P2 /// GENERATED ALWAYS AS ... STATIC columns are only checked if P3 is zero. /// When P3 is non-zero, no type checking occurs for static generated columns. check_generated: bool, // P3 table_reference: Arc, // P4 }, /// Parse a JSON text array into a native record-format BLOB, validating /// and coercing each element against the declared type using STRICT /// type-checking logic (apply_affinity_char + value_type check). /// Input: reg = JSON text like '[1,2,3]'. Output: reg = record-format BLOB. /// Raises SQLITE_CONSTRAINT on type mismatch. ArrayEncode { reg: usize, element_affinity: Affinity, element_type: Arc, table_name: Arc, col_name: Arc, }, /// Convert a native record-format BLOB back to JSON text for display. /// Input: reg = record-format BLOB. Output: reg = JSON text '[1,2,3]'. ArrayDecode { reg: usize, }, /// Access element at index from a record-format array BLOB. /// If array is NULL or index out of bounds, dest = NULL. ArrayElement { array_reg: usize, index_reg: usize, dest: usize, }, /// Get the number of elements in a record-format array BLOB. /// If input is NULL, dest = 0. ArrayLength { reg: usize, dest: usize, }, /// Create an array from contiguous registers (static count). /// Reads `count` values from start_reg..start_reg+count, /// serializes via ImmutableRecord, stores Value::Blob in dest. MakeArray { start_reg: usize, count: usize, dest: usize, }, /// Create an array from contiguous registers (dynamic count). /// Like MakeArray but count is read from count_reg at runtime. MakeArrayDynamic { start_reg: usize, count_reg: usize, dest: usize, }, /// Copy a register value to a dynamically-computed destination. /// dest = registers[base + registers[offset_reg]] /// registers[base + registers[offset_reg]] = registers[src] RegCopyOffset { src: usize, base: usize, offset_reg: usize, }, /// Concatenate/append/prepend arrays. PostgreSQL-compatible semantics: /// - blob || blob → array_cat /// - blob || scalar → array_append /// - scalar || blob → array_prepend /// /// Falls back to string Concat for non-array operands. ArrayConcat { lhs: usize, rhs: usize, dest: usize, }, /// Set element at index in a record-format array BLOB. /// Extracts all elements, replaces element at index, rebuilds blob. ArraySetElement { array_reg: usize, index_reg: usize, value_reg: usize, dest: usize, }, /// Extract a subslice of elements from a record-format array BLOB. /// Creates a new array blob from elements[start..end]. ArraySlice { array_reg: usize, start_reg: usize, end_reg: usize, dest: usize, }, // Make a record and write it to destination register. MakeRecord { start_reg: u16, // P1 count: u16, // P2 dest_reg: u16, // P3 index_name: Option, affinity_str: Option, }, /// Emit a row of results. ResultRow { start_reg: usize, // P1 count: usize, // P2 }, /// Advance the cursor to the next row. Next { cursor_id: CursorID, pc_if_next: BranchOffset, }, Prev { cursor_id: CursorID, pc_if_prev: BranchOffset, }, /// Halt the program. Halt { err_code: usize, description: String, /// Override the program's resolve_type for error handling (used by RAISE). on_error: Option, /// If set, read the error description from this register instead of /// the static `description` field (used by RAISE with expression messages). description_reg: Option, }, /// Halt the program if P3 is null. HaltIfNull { target_reg: usize, // P3 description: String, // p4 err_code: usize, // p1 }, /// Start a transaction. Transaction { db: usize, // p1 tx_mode: TransactionMode, // p2 schema_cookie: u32, // p3 }, /// Set database auto-commit mode and potentially rollback. AutoCommit { auto_commit: bool, rollback: bool, }, /// Execute a named savepoint operation. Savepoint { op: SavepointOp, name: String, }, /// Branch to the given PC. Goto { target_pc: BranchOffset, }, /// Stores the current program counter into register 'return_reg' then jumps to address target_pc. Gosub { target_pc: BranchOffset, return_reg: usize, }, /// Returns to the program counter stored in register 'return_reg'. /// If can_fallthrough is true, fall through to the next instruction /// if return_reg does not contain an integer value. Otherwise raise an error. Return { return_reg: usize, can_fallthrough: bool, }, /// Invoke a trigger subprogram. /// /// According to SQLite documentation (https://sqlite.org/opcode.html): /// "The Program opcode invokes the trigger subprogram. The Program instruction /// allocates and initializes a fresh register set for each invocation of the /// subprogram, so subprograms can be reentrant and recursive. The Param opcode /// is used by subprograms to access content in registers of the calling bytecode program." Program { params: Vec, program: Arc, /// Jump target when RAISE(IGNORE) fires in the subprogram. /// Points to the "skip this row" address in the parent program. ignore_jump_target: BranchOffset, }, /// Write an integer value into a register. Integer { value: i64, dest: usize, }, /// Write a float value into a register Real { value: f64, dest: usize, }, /// If register holds an integer, transform it to a float RealAffinity { register: usize, }, // Write a string value into a register. String8 { value: String, dest: usize, }, /// Write a blob value into a register. Blob { value: Vec, dest: usize, }, /// Read a complete row of data from the current cursor and write it to the destination register. RowData { cursor_id: CursorID, dest: usize, }, /// Read the rowid of the current row. RowId { cursor_id: CursorID, dest: usize, }, /// Read the rowid of the current row from an index cursor. IdxRowId { cursor_id: CursorID, dest: usize, }, /// Seek to a rowid in the cursor. If not found, jump to the given PC. Otherwise, continue to the next instruction. SeekRowid { cursor_id: CursorID, src_reg: usize, target_pc: BranchOffset, }, SeekEnd { cursor_id: CursorID, }, /// P1 is an open index cursor and P3 is a cursor on the corresponding table. This opcode does a deferred seek of the P3 table cursor to the row that corresponds to the current row of P1. /// This is a deferred seek. Nothing actually happens until the cursor is used to read a record. That way, if no reads occur, no unnecessary I/O happens. DeferredSeek { index_cursor_id: CursorID, table_cursor_id: CursorID, }, /// If cursor_id refers to an SQL table (B-Tree that uses integer keys), use the value in start_reg as the key. /// If cursor_id refers to an SQL index, then start_reg is the first in an array of num_regs registers that are used as an unpacked index key. /// Seek to the first index entry that is greater than or equal to the given key. If not found, jump to the given PC. Otherwise, continue to the next instruction. SeekGE { is_index: bool, cursor_id: CursorID, start_reg: usize, num_regs: usize, target_pc: BranchOffset, eq_only: bool, }, /// If cursor_id refers to an SQL table (B-Tree that uses integer keys), use the value in start_reg as the key. /// If cursor_id refers to an SQL index, then start_reg is the first in an array of num_regs registers that are used as an unpacked index key. /// Seek to the first index entry that is greater than the given key. If not found, jump to the given PC. Otherwise, continue to the next instruction. SeekGT { is_index: bool, cursor_id: CursorID, start_reg: usize, num_regs: usize, target_pc: BranchOffset, }, /// cursor_id is a cursor pointing to a B-Tree index that uses integer keys, this op writes the value obtained from MakeRecord into the index. /// P3 + P4 are for the original column values that make up that key in unpacked (pre-serialized) form. /// If P5 has the OPFLAG_APPEND bit set, that is a hint to the b-tree layer that this insert is likely to be an append. /// OPFLAG_NCHANGE bit set, then the change counter is incremented by this instruction. If the OPFLAG_NCHANGE bit is clear, then the change counter is unchanged IdxInsert { cursor_id: CursorID, record_reg: usize, // P2 the register containing the record to insert unpacked_start: Option, // P3 the index of the first register for the unpacked key unpacked_count: Option, // P4 # of unpacked values in the key in P2 flags: IdxInsertFlags, // TODO: optimization }, /// The P4 register values beginning with P3 form an unpacked index key that omits the PRIMARY KEY. Compare this key value against the index that P1 is currently pointing to, ignoring the PRIMARY KEY or ROWID fields at the end. /// If the P1 index entry is greater or equal than the key value then jump to P2. Otherwise fall through to the next instruction. // If cursor_id refers to an SQL table (B-Tree that uses integer keys), use the value in start_reg as the key. // If cursor_id refers to an SQL index, then start_reg is the first in an array of num_regs registers that are used as an unpacked index key. // Seek to the first index entry that is less than or equal to the given key. If not found, jump to the given PC. Otherwise, continue to the next instruction. SeekLE { is_index: bool, cursor_id: CursorID, start_reg: usize, num_regs: usize, target_pc: BranchOffset, eq_only: bool, }, // If cursor_id refers to an SQL table (B-Tree that uses integer keys), use the value in start_reg as the key. // If cursor_id refers to an SQL index, then start_reg is the first in an array of num_regs registers that are used as an unpacked index key. // Seek to the first index entry that is less than the given key. If not found, jump to the given PC. Otherwise, continue to the next instruction. SeekLT { is_index: bool, cursor_id: CursorID, start_reg: usize, num_regs: usize, target_pc: BranchOffset, }, // The P4 register values beginning with P3 form an unpacked index key that omits the PRIMARY KEY. Compare this key value against the index that P1 is currently pointing to, ignoring the PRIMARY KEY or ROWID fields at the end. // If the P1 index entry is greater or equal than the key value then jump to P2. Otherwise fall through to the next instruction. IdxGE { cursor_id: CursorID, start_reg: usize, num_regs: usize, target_pc: BranchOffset, }, /// The P4 register values beginning with P3 form an unpacked index key that omits the PRIMARY KEY. Compare this key value against the index that P1 is currently pointing to, ignoring the PRIMARY KEY or ROWID fields at the end. /// If the P1 index entry is greater than the key value then jump to P2. Otherwise fall through to the next instruction. IdxGT { cursor_id: CursorID, start_reg: usize, num_regs: usize, target_pc: BranchOffset, }, /// The P4 register values beginning with P3 form an unpacked index key that omits the PRIMARY KEY. Compare this key value against the index that P1 is currently pointing to, ignoring the PRIMARY KEY or ROWID fields at the end. /// If the P1 index entry is lesser or equal than the key value then jump to P2. Otherwise fall through to the next instruction. IdxLE { cursor_id: CursorID, start_reg: usize, num_regs: usize, target_pc: BranchOffset, }, /// The P4 register values beginning with P3 form an unpacked index key that omits the PRIMARY KEY. Compare this key value against the index that P1 is currently pointing to, ignoring the PRIMARY KEY or ROWID fields at the end. /// If the P1 index entry is lesser than the key value then jump to P2. Otherwise fall through to the next instruction. IdxLT { cursor_id: CursorID, start_reg: usize, num_regs: usize, target_pc: BranchOffset, }, /// Decrement the given register and jump to the given PC if the result is zero. DecrJumpZero { reg: usize, target_pc: BranchOffset, }, AggStep { acc_reg: usize, col: usize, delimiter: usize, func: AggFunc, /// Optional custom type comparator for MIN/MAX aggregates. comparator: Option, }, AggFinal { register: usize, func: AggFunc, }, /// Similar to AggFinal, but instead of writing the result back into the /// accumulator register, it stores the result in a separate destination /// register. AggValue { acc_reg: usize, dest_reg: usize, func: AggFunc, }, /// Open a sorter. SorterOpen { cursor_id: CursorID, // P1 columns: usize, // P2 /// Combined order and collation per column (keeps Insn small, and order+collations are always the same length). order_and_collations: Vec<(SortOrder, Option)>, /// Per-column custom type comparators for ORDER BY sorting. /// When present, the comparator is used instead of standard value comparison. comparators: Vec>, }, /// Insert a row into the sorter. SorterInsert { cursor_id: CursorID, record_reg: usize, }, /// `cursor_id` is a sorter cursor. This instruction compares a prefix of the record blob in register `sorted_record_reg` /// against a prefix of the entry that the sorter cursor currently points to. /// Only the first `num_regs` fields of `sorted_record_reg` and the sorter record are compared. /// Fall through to next instruction if the two records compare equal to each other. /// Jump to `pc_when_nonequal` if they are different. SorterCompare { cursor_id: CursorID, pc_when_nonequal: BranchOffset, sorted_record_reg: usize, num_regs: usize, }, /// Sort the rows in the sorter. SorterSort { cursor_id: CursorID, pc_if_empty: BranchOffset, }, /// Retrieve the next row from the sorter. SorterData { cursor_id: CursorID, // P1 dest_reg: usize, // P2 pseudo_cursor: usize, // P3 }, /// Advance to the next row in the sorter. SorterNext { cursor_id: CursorID, pc_if_next: BranchOffset, }, /// Insert the integer value held by register P2 into a RowSet object held in register P1. /// An assertion fails if P2 is not an integer. RowSetAdd { rowset_reg: usize, // P1 - register holding RowSet value_reg: usize, // P2 - register holding integer value to add }, /// Extract the smallest value from the RowSet object in P1 and put that value into register P3. /// Or, if RowSet object P1 is initially empty, leave P3 unchanged and jump to instruction P2. RowSetRead { rowset_reg: usize, // P1 - register holding RowSet pc_if_empty: BranchOffset, // P2 - jump target if empty dest_reg: usize, // P3 - register to store smallest value }, /// Register P3 is assumed to hold a 64-bit integer value. If register P1 contains a RowSet object /// and that RowSet object contains the value held in P3, jump to register P2. Otherwise, insert /// the integer in P3 into the RowSet and continue on to the next opcode. /// P4 is the batch identifier (0 for first set, -1 for final set, >0 for other sets). RowSetTest { rowset_reg: usize, // P1 - register holding RowSet pc_if_found: BranchOffset, // P2 - jump target if value found value_reg: usize, // P3 - register holding integer value to test/insert batch: i32, // P4 - batch identifier }, /// Function Function { constant_mask: i32, // P1 start_reg: usize, // P2, start of argument registers dest: usize, // P3 func: FuncCtx, // P4 }, /// Cast register P1 to affinity P2 and store in register P1 Cast { reg: usize, affinity: Affinity, }, InitCoroutine { yield_reg: usize, jump_on_definition: BranchOffset, start_offset: BranchOffset, }, EndCoroutine { yield_reg: usize, }, Yield { yield_reg: usize, end_offset: BranchOffset, /// For coroutine body yields (end_offset == 0): the start register of the /// output columns and how many there are. op_yield uses these to strip /// the JSON subtype so that it does not survive the subquery boundary, /// mirroring SQLite's OP_Copy P5=0x0002 behaviour. /// Set to 0/0 for parent-side (non-body) yields. subtype_clear_start_reg: usize, subtype_clear_count: usize, }, Insert { cursor: CursorID, key_reg: usize, // Must be int. record_reg: usize, // Blob of record data. flag: InsertFlags, // Flags used by insert, for now not used. table_name: String, }, Int64 { _p1: usize, // unused out_reg: usize, // the output register _p3: usize, // unused value: i64, // the value being written into the output register }, Delete { cursor_id: CursorID, table_name: String, /// Whether the DELETE is part of an UPDATE statement. If so, it doesn't count towards the change counter. is_part_of_update: bool, }, /// If P5 is not zero, then raise an SQLITE_CORRUPT_INDEX error if no matching index entry /// is found. This happens when running an UPDATE or DELETE statement and the index entry to /// be updated or deleted is not found. For some uses of IdxDelete (example: the EXCEPT operator) /// it does not matter that no matching entry is found. For those cases, P5 is zero. IdxDelete { start_reg: usize, num_regs: usize, cursor_id: CursorID, raise_error_if_no_matching_entry: bool, // P5 }, NewRowid { cursor: CursorID, // P1 rowid_reg: usize, // P2 Destination register to store the new rowid prev_largest_reg: usize, // P3 Previous largest rowid in the table (Not used for now) }, MustBeInt { reg: usize, }, SoftNull { reg: usize, }, /// If P4==0 then register P3 holds a blob constructed by [MakeRecord](https://sqlite.org/opcode.html#MakeRecord). /// If P4>0 then register P3 is the first of P4 registers that form an unpacked record. /// /// Cursor P1 is on an index btree. If the record identified by P3 and P4 contains any NULL value, jump immediately /// to P2. If all terms of the record are not-NULL then a check is done to determine if any row in the P1 index /// btree has a matching key prefix. If there are no matches, jump immediately to P2. If there is a match, fall /// through and leave the P1 cursor pointing to the matching row.\ /// /// This opcode is similar to [NotFound](https://sqlite.org/opcode.html#NotFound) with the exceptions that the /// branch is always taken if any part of the search key input is NULL. NoConflict { cursor_id: CursorID, // P1 index cursor target_pc: BranchOffset, // P2 jump target record_reg: usize, num_regs: usize, }, NotExists { cursor: CursorID, rowid_reg: usize, target_pc: BranchOffset, }, OffsetLimit { limit_reg: usize, combined_reg: usize, offset_reg: usize, }, OpenWrite { cursor_id: CursorID, root_page: RegisterOrLiteral, db: usize, }, /// Make a copy of register src..src+extra_amount into dst..dst+extra_amount. Copy { src_reg: usize, dst_reg: usize, /// 0 extra_amount means we include src_reg, dst_reg..=dst_reg+amount = src_reg..=src_reg+amount extra_amount: usize, }, /// Allocate a new b-tree. CreateBtree { /// Allocate b-tree in main database if zero or in temp database if non-zero (P1). db: usize, /// The root page of the new b-tree (P2). root: usize, /// Flags (P3). flags: CreateBTreeFlags, }, /// Create custom index method (calls [crate::index_method::IndexMethodCursor::create] under the hood) IndexMethodCreate { db: usize, cursor_id: CursorID, }, /// Destroy custom index method (calls [crate::index_method::IndexMethodCursor::destroy] under the hood) IndexMethodDestroy { db: usize, cursor_id: CursorID, }, /// Optimize custom index method (calls [crate::index_method::IndexMethodCursor::optimize] under the hood) IndexMethodOptimize { db: usize, cursor_id: CursorID, }, /// Query custom index method (call [crate::index_method::IndexMethodCursor::query_start] under the hood) IndexMethodQuery { db: usize, cursor_id: CursorID, start_reg: usize, count_reg: usize, pc_if_empty: BranchOffset, }, /// Deletes an entire database table or index whose root page in the database file is given by P1. Destroy { /// The database index (0 = main, 1 = temp, 2+ = attached) db: usize, /// The root page of the table/index to destroy root: i64, /// Register to store the former value of any moved root page (for AUTOVACUUM) former_root_reg: usize, /// Whether this is a temporary table (1) or main database table (0) is_temp: usize, }, /// Deletes all contents from the ephemeral table that the cursor points to. /// /// In Turso, we do not currently distinguish strictly between ephemeral /// and standard tables at the type level. Therefore, it is the caller’s /// responsibility to ensure that `ResetSorter` is applied only to ephemeral /// tables. /// /// SQLite also supports sorter cursors, but this is not yet implemented in Turso. ResetSorter { cursor_id: CursorID, }, /// Drop a table DropTable { /// The database within which this b-tree needs to be dropped (P1). db: usize, /// unused register p2 _p2: usize, /// unused register p3 _p3: usize, // The name of the table being dropped table_name: String, }, DropView { /// The database within which this view needs to be dropped db: usize, /// The name of the view being dropped view_name: String, }, DropIndex { /// The database within which this index needs to be dropped (P1). db: usize, // The name of the index being dropped index: Arc, }, /// Drop a trigger DropTrigger { /// The database within which this trigger needs to be dropped (P1). db: usize, /// The name of the trigger being dropped trigger_name: String, }, /// Drop a custom type from the in-memory schema DropType { /// The database within which this type needs to be dropped db: usize, /// The name of the type being dropped type_name: String, }, /// Add a custom type to the in-memory schema by parsing its CREATE TYPE SQL AddType { /// The database within which this type needs to be added db: usize, /// The full CREATE TYPE SQL string sql: String, }, /// Close a cursor. Close { cursor_id: CursorID, }, /// Check if the register is null. IsNull { /// Source register (P1). reg: usize, /// Jump to this PC if the register is null (P2). target_pc: BranchOffset, }, /// Set the collation sequence for the next function call. /// P4 is a pointer to a CollationSeq. If the next call to a user function /// or aggregate calls sqlite3GetFuncCollSeq(), this collation sequence will /// be returned. This is used by the built-in min(), max() and nullif() /// functions. /// /// If P1 is not zero, then it is a register that a subsequent min() or /// max() aggregate will set to 1 if the current row is not the minimum or /// maximum. The P1 register is initialized to 0 by this instruction. CollSeq { /// Optional register to initialize to 0 (P1). reg: Option, /// The collation sequence to set (P4). collation: CollationSeq, }, ParseSchema { db: usize, where_clause: Option, }, /// Populate all materialized views after schema parsing /// The cursors parameter contains a mapping of view names to cursor IDs that have been /// opened to the view's btree for writing the materialized data PopulateMaterializedViews { /// Mapping of view name to cursor_id for writing to the view's btree cursors: Vec<(String, usize)>, }, /// Place the result of lhs >> rhs in dest register. ShiftRight { lhs: usize, rhs: usize, dest: usize, }, /// Place the result of lhs << rhs in dest register. ShiftLeft { lhs: usize, rhs: usize, dest: usize, }, /// Add immediate value to register and force integer conversion. /// Add the constant P2 to the value in register P1. The result is always an integer. /// To force any register to be an integer, just add 0. AddImm { register: usize, // P1: target register value: i64, // P2: immediate value to add }, /// Get parameter variable. Variable { index: NonZero, dest: usize, }, /// If either register is null put null else put 0 ZeroOrNull { /// Source register (P1). rg1: usize, rg2: usize, dest: usize, }, /// Interpret the value in reg as boolean and store its compliment in destination Not { reg: usize, dest: usize, }, /// Interpret the value in register `reg` as a boolean and store in `dest`. /// Used to implement IS TRUE, IS FALSE, IS NOT TRUE, IS NOT FALSE. /// /// A value is considered "true" if it is a non-zero number. /// Strings, blobs, and zero are "false". NULL is handled specially. /// /// - If reg is NULL, store `null_value` in dest /// - Otherwise, store 1 if the value is a non-zero number, 0 otherwise /// - If `invert` is true, invert the result (0↔1) IsTrue { reg: usize, dest: usize, /// Value to store if input is NULL (0 or 1) null_value: bool, /// Whether to invert the result invert: bool, }, /// Concatenates the `rhs` and `lhs` values and stores the result in the third register. Concat { lhs: usize, rhs: usize, dest: usize, }, /// Take the logical AND of the values in registers P1 and P2 and write the result into register P3. And { lhs: usize, rhs: usize, dest: usize, }, /// Take the logical OR of the values in register P1 and P2 and store the answer in register P3. Or { lhs: usize, rhs: usize, dest: usize, }, /// Do nothing. Continue downward to the next opcode. Noop, /// Write the current number of pages in database P1 to memory cell P2. PageCount { db: usize, dest: usize, }, /// Read cookie number P3 from database P1 and write it into register P2 ReadCookie { db: usize, dest: usize, cookie: Cookie, }, /// Write the value in register P3 into cookie number P2 of database P1. /// If P2 is the SCHEMA_VERSION cookie (cookie number 1) then the internal schema version is set to P3-P5 SetCookie { db: usize, cookie: Cookie, value: i32, p5: u16, }, /// Open a new cursor P1 to a transient table. OpenEphemeral { cursor_id: usize, is_table: bool, }, /// Works the same as OpenEphemeral, name just distinguishes its use; used for transient indexes in joins. OpenAutoindex { cursor_id: usize, }, /// Opens a new cursor that points to the same table as the original. /// In SQLite, this is restricted to cursors opened by `OpenEphemeral` /// (i.e., ephemeral tables), and only ephemeral cursors may be duplicated. /// In Turso, we currently do not strictly distinguish between ephemeral /// and standard tables at the type level. Therefore, it is the caller’s /// responsibility to ensure that `OpenDup` is applied only to ephemeral /// cursors. OpenDup { new_cursor_id: CursorID, original_cursor_id: CursorID, }, /// Fall through to the next instruction on the first invocation, otherwise jump to target_pc Once { target_pc_when_reentered: BranchOffset, }, /// Search for a record in the index cursor. /// If any entry for which the key is a prefix exists, jump to target_pc. /// Otherwise, continue to the next instruction. Found { cursor_id: CursorID, target_pc: BranchOffset, record_reg: usize, num_regs: usize, }, /// Search for record in the index cusor, if any entry for which the key is a prefix exists /// is a no-op, otherwise go to target_pc /// Example => /// For a index key (1,2,3): /// NotFound((1,2,3)) => No-op /// NotFound((1,2)) => No-op /// NotFound((2,2, 1)) => Jump NotFound { cursor_id: CursorID, target_pc: BranchOffset, record_reg: usize, num_regs: usize, }, /// Apply affinities to a range of registers. Affinities must have the same size of count Affinity { start_reg: usize, count: NonZeroUsize, affinities: String, }, /// Store the number of entries (an integer value) in the table or index opened by cursor P1 in register P2. /// /// If P3==0, then an exact count is obtained, which involves visiting every btree page of the table. /// But if P3 is non-zero, an estimate is returned based on the current cursor position. Count { cursor_id: CursorID, target_reg: usize, exact: bool, }, /// Perform low-level btree/freelist structural integrity checks. /// Writes NULL to `message_register` when no structural problem is found, /// otherwise writes a textual error summary. /// Higher-level semantic checks (row/index consistency, constraints, etc.) /// are emitted as normal VDBE bytecode in translation. IntegrityCk { db: usize, max_errors: usize, roots: Vec, message_register: usize, }, RenameTable { db: usize, from: String, to: String, }, DropColumn { db: usize, table: String, column_index: usize, }, AddColumn { db: usize, table: String, column: Box, check_constraints: Vec, }, AlterColumn { db: usize, table: String, column_index: usize, definition: Box, rename: bool, }, /// Try to set the maximum page count for database P1 to the value in P3. /// Do not let the maximum page count fall below the current page count and /// do not change the maximum page count value if P3==0. /// Store the maximum page count after the change in register P2. MaxPgcnt { db: usize, // P1: database index dest: usize, // P2: output register new_max: usize, // P3: new maximum page count (0 = just return current) }, /// Get or set the journal mode for database P1. /// If P3 is not null, it contains the new journal mode string. /// Store the resulting journal mode in register P2. JournalMode { db: usize, // P1: database index dest: usize, // P2: output register for result new_mode: Option, // P3: new journal mode (if setting) }, IfNeg { reg: usize, target_pc: BranchOffset, }, /// Find the next available sequence number for cursor P1. Write the sequence number into register P2. /// The sequence number on the cursor is incremented after this instruction. Sequence { cursor_id: CursorID, target_reg: usize, }, /// P1 is a sorter cursor. If the sequence counter is currently zero, jump to P2. Regardless of whether or not the jump is taken, increment the the sequence value. SequenceTest { cursor_id: CursorID, target_pc: BranchOffset, value_reg: usize, }, // OP_Explain Explain { p1: usize, // P1: address of instruction p2: Option, // P2: address of parent explain instruction detail: String, // P4: detail text }, // Increment a "constraint counter" by P2 (P2 may be negative or positive). // If P1 is non-zero, the database constraint counter is incremented (deferred foreign key constraints). // Otherwise, if P1 is zero, the statement counter is incremented (immediate foreign key constraints). FkCounter { increment_value: isize, deferred: bool, }, // This opcode tests if a foreign key constraint-counter is currently zero. If so, jump to instruction P2. Otherwise, fall through to the next instruction. // If P1 is non-zero, then the jump is taken if the database constraint-counter is zero (the one that counts deferred constraint violations). // If P1 is zero, the jump is taken if the statement constraint-counter is zero (immediate foreign key constraint violations). FkIfZero { deferred: bool, target_pc: BranchOffset, }, // Check if there are any unresolved foreign key constraint violations. // If P1 is zero, check the statement constraint-counter (immediate FK violations). // If P1 is non-zero, check the database constraint-counter (deferred FK violations). // If violations exist, throw SQLITE_CONSTRAINT_FOREIGNKEY. FkCheck { deferred: bool, }, /// Build a hash table from a cursor for hash join. HashBuild { data: Box, }, /// Deduplicate using a hash table. Jumps to target_pc if duplicate found. HashDistinct { data: Box, }, /// Finalize the hash table build phase. Transitions the hash table from Building to Probing state. /// Should be called after the HashBuild loop completes. HashBuildFinalize { hash_table_id: usize, }, /// Probe a hash table for matches. /// Extract probe keys from registers key_start_reg..key_start_reg+num_keys-1, /// hash them, and look up matches in the hash table stored in hash_table_reg. /// For each match, load the build-side rowid into dest_reg and continue. /// If payload columns were stored during build, they are written to /// payload_dest_reg..payload_dest_reg+num_payload-1. /// If no matches, jump to target_pc. HashProbe { hash_table_id: u16, key_start_reg: u16, num_keys: u16, dest_reg: u16, target_pc: BranchOffset, /// Starting register to write payload columns from hash entry. payload_dest_reg: Option, /// Number of payload columns expected num_payload: u16, /// Register containing probe-side rowid for grace hash join buffering. /// When Some and target partition is on disk, buffer the probe row /// instead of loading the partition on demand. /// When None, this instruction is running inside grace processing and /// the build partition must already be loaded. probe_rowid_reg: Option, }, /// Advance to next matching row in hash table bucket. /// Used for handling hash collisions and duplicate keys. /// If another match is found, store rowid in dest_reg (and payload in payload_dest_reg if set). /// If no more matches, jump to target_pc. HashNext { hash_table_id: usize, dest_reg: usize, target_pc: BranchOffset, /// Starting register to write payload columns from hash entry, if we are caching payload. payload_dest_reg: Option, /// Number of payload columns expected num_payload: usize, }, /// Free hash table resources. /// Closes the hash table referenced by hash_table_id and releases memory. HashClose { hash_table_id: usize, }, /// Clear hash table entries without releasing the table itself. HashClear { hash_table_id: usize, }, /// Mark the current hash table match entry as "matched" (for FULL OUTER JOIN). HashMarkMatched { hash_table_id: usize, }, /// Reset all matched_bits in a hash table to false. /// Emitted at the start of each outer-loop iteration so that marks from /// a previous probe pass don't suppress NULL-fill rows in the current one. HashResetMatched { hash_table_id: usize, }, /// Begin scanning unmatched entries in the hash table (for FULL OUTER JOIN). /// Writes the first unmatched entry's rowid to dest_reg and payload to payload_dest_reg. /// If no unmatched entries exist, jumps to target_pc. HashScanUnmatched { hash_table_id: usize, dest_reg: usize, target_pc: BranchOffset, payload_dest_reg: Option, num_payload: usize, }, /// Advance to the next unmatched entry in the hash table (for FULL OUTER JOIN). /// If another unmatched entry is found, writes rowid to dest_reg and payload to payload_dest_reg. /// If no more unmatched entries, jumps to target_pc. HashNextUnmatched { hash_table_id: usize, dest_reg: usize, target_pc: BranchOffset, payload_dest_reg: Option, num_payload: usize, }, /// Initialize grace hash join processing after the probe cursor is exhausted. /// Finalizes probe-side spills and calls grace_begin. /// Jumps to target_pc if no spilling occurred or no partitions to process. HashGraceInit { hash_table_id: u16, target_pc: BranchOffset, }, /// Load the current grace partition's build side from disk. /// Also loads the first probe chunk. Jumps to target_pc when all partitions done. HashGraceLoadPartition { hash_table_id: u16, target_pc: BranchOffset, }, /// Advance to next probe entry in the current grace partition. /// Writes probe keys to key_start_reg..key_start_reg+num_keys-1 and probe rowid to probe_rowid_dest. /// Jumps to target_pc when probe entries exhausted. HashGraceNextProbe { hash_table_id: u16, key_start_reg: u16, num_keys: u16, probe_rowid_dest: u16, target_pc: BranchOffset, }, /// Evict current grace partition and advance to the next one. /// Jumps to target_pc when all partitions are processed. HashGraceAdvancePartition { hash_table_id: u16, target_pc: BranchOffset, }, /// VACUUM INTO - create a compacted copy of the database at the specified path. /// This copies all schema and data from the current database to a new file. VacuumInto { /// Destination file path for the vacuumed database dest_path: String, }, /// Ensure turso_cdc_version table exists and insert/replace a version row, /// then enable CDC on the connection. Runs nested SQL at VDBE execution time /// (same pattern as ParseSchema). CDC is enabled after version table operations /// so those operations are not captured. /// /// A dedicated opcode is needed because the PRAGMA SET handler may create the /// CDC table (via translate_create_table) and then needs to insert data into /// turso_cdc_version — which requires a schema change followed by DML against /// the new table. This is hard to express in a single translation plan since /// plans are compiled against a fixed schema, so the version table operations /// are deferred to execution time via this opcode. InitCdcVersion { cdc_table_name: String, version: crate::CdcVersion, cdc_mode: String, }, } const fn get_insn_virtual_table() -> [InsnFunction; InsnVariants::COUNT] { let mut result: [InsnFunction; InsnVariants::COUNT] = [execute::op_init; InsnVariants::COUNT]; let mut insn = 0; while insn < InsnVariants::COUNT { result[insn] = InsnVariants::from_repr(insn as u8) .expect("insn index should be valid within COUNT") .to_function(); insn += 1; } result } const INSN_VTABLE: [InsnFunction; InsnVariants::COUNT] = get_insn_virtual_table(); impl InsnVariants { // This function is used for testing #[allow(dead_code)] #[inline(always)] pub(crate) const fn to_function_fast(self) -> InsnFunction { INSN_VTABLE[self as usize] } // This function is used for generating `INSN_VTABLE`. // We need to keep this function to make sure we implement all opcodes pub(crate) const fn to_function(self) -> InsnFunction { match self { InsnVariants::Init => execute::op_init, InsnVariants::Null => execute::op_null, InsnVariants::BeginSubrtn => execute::op_null, InsnVariants::NullRow => execute::op_null_row, InsnVariants::Add => execute::op_add, InsnVariants::Subtract => execute::op_subtract, InsnVariants::Multiply => execute::op_multiply, InsnVariants::Divide => execute::op_divide, InsnVariants::DropIndex => execute::op_drop_index, InsnVariants::Compare => execute::op_compare, InsnVariants::BitAnd => execute::op_bit_and, InsnVariants::BitOr => execute::op_bit_or, InsnVariants::BitNot => execute::op_bit_not, InsnVariants::Checkpoint => execute::op_checkpoint, InsnVariants::Remainder => execute::op_remainder, InsnVariants::Jump => execute::op_jump, InsnVariants::Move => execute::op_move, InsnVariants::IfPos => execute::op_if_pos, InsnVariants::NotNull => execute::op_not_null, InsnVariants::Eq | InsnVariants::Ne | InsnVariants::Lt | InsnVariants::Le | InsnVariants::Gt | InsnVariants::Ge => execute::op_comparison, InsnVariants::If => execute::op_if, InsnVariants::IfNot => execute::op_if_not, InsnVariants::OpenRead => execute::op_open_read, InsnVariants::VOpen => execute::op_vopen, InsnVariants::VCreate => execute::op_vcreate, InsnVariants::VFilter => execute::op_vfilter, InsnVariants::VColumn => execute::op_vcolumn, InsnVariants::VUpdate => execute::op_vupdate, InsnVariants::VNext => execute::op_vnext, InsnVariants::VDestroy => execute::op_vdestroy, InsnVariants::OpenPseudo => execute::op_open_pseudo, InsnVariants::Rewind => execute::op_rewind, InsnVariants::Last => execute::op_last, InsnVariants::Column => execute::op_column, InsnVariants::TypeCheck => execute::op_type_check, InsnVariants::ArrayEncode => execute::op_array_encode, InsnVariants::ArrayDecode => execute::op_array_decode, InsnVariants::ArrayElement => execute::op_array_element, InsnVariants::ArrayLength => execute::op_array_length, InsnVariants::MakeArray => execute::op_make_array, InsnVariants::MakeArrayDynamic => execute::op_make_array_dynamic, InsnVariants::RegCopyOffset => execute::op_reg_copy_offset, InsnVariants::ArrayConcat => execute::op_array_concat, InsnVariants::ArraySetElement => execute::op_array_set_element, InsnVariants::ArraySlice => execute::op_array_slice, InsnVariants::MakeRecord => execute::op_make_record, InsnVariants::ResultRow => execute::op_result_row, InsnVariants::Next => execute::op_next, InsnVariants::Prev => execute::op_prev, InsnVariants::Halt => execute::op_halt, InsnVariants::HaltIfNull => execute::op_halt_if_null, InsnVariants::Transaction => execute::op_transaction, InsnVariants::AutoCommit => execute::op_auto_commit, InsnVariants::Savepoint => execute::op_savepoint, InsnVariants::Goto => execute::op_goto, InsnVariants::Gosub => execute::op_gosub, InsnVariants::Return => execute::op_return, InsnVariants::Integer => execute::op_integer, InsnVariants::Program => execute::op_program, InsnVariants::Real => execute::op_real, InsnVariants::RealAffinity => execute::op_real_affinity, InsnVariants::String8 => execute::op_string8, InsnVariants::Blob => execute::op_blob, InsnVariants::RowData => execute::op_row_data, InsnVariants::RowId => execute::op_row_id, InsnVariants::IdxRowId => execute::op_idx_row_id, InsnVariants::SeekRowid => execute::op_seek_rowid, InsnVariants::DeferredSeek => execute::op_deferred_seek, InsnVariants::SeekGE | InsnVariants::SeekGT | InsnVariants::SeekLE | InsnVariants::SeekLT => execute::op_seek, InsnVariants::SeekEnd => execute::op_seek_end, InsnVariants::IdxGE => execute::op_idx_ge, InsnVariants::IdxGT => execute::op_idx_gt, InsnVariants::IdxLE => execute::op_idx_le, InsnVariants::IdxLT => execute::op_idx_lt, InsnVariants::DecrJumpZero => execute::op_decr_jump_zero, InsnVariants::AggStep => execute::op_agg_step, InsnVariants::AggFinal | InsnVariants::AggValue => execute::op_agg_final, InsnVariants::SorterOpen => execute::op_sorter_open, InsnVariants::SorterInsert => execute::op_sorter_insert, InsnVariants::SorterSort => execute::op_sorter_sort, InsnVariants::SorterData => execute::op_sorter_data, InsnVariants::SorterNext => execute::op_sorter_next, InsnVariants::SorterCompare => execute::op_sorter_compare, InsnVariants::RowSetAdd => execute::op_rowset_add, InsnVariants::RowSetRead => execute::op_rowset_read, InsnVariants::RowSetTest => execute::op_rowset_test, InsnVariants::Function => execute::op_function, InsnVariants::Cast => execute::op_cast, InsnVariants::InitCoroutine => execute::op_init_coroutine, InsnVariants::EndCoroutine => execute::op_end_coroutine, InsnVariants::Yield => execute::op_yield, InsnVariants::Insert => execute::op_insert, InsnVariants::Int64 => execute::op_int_64, InsnVariants::IdxInsert => execute::op_idx_insert, InsnVariants::Delete => execute::op_delete, InsnVariants::NewRowid => execute::op_new_rowid, InsnVariants::MustBeInt => execute::op_must_be_int, InsnVariants::SoftNull => execute::op_soft_null, InsnVariants::NoConflict => execute::op_no_conflict, InsnVariants::NotExists => execute::op_not_exists, InsnVariants::OffsetLimit => execute::op_offset_limit, InsnVariants::OpenWrite => execute::op_open_write, InsnVariants::Copy => execute::op_copy, InsnVariants::CreateBtree => execute::op_create_btree, InsnVariants::IndexMethodCreate => execute::op_index_method_create, InsnVariants::IndexMethodDestroy => execute::op_index_method_destroy, InsnVariants::IndexMethodOptimize => execute::op_index_method_optimize, InsnVariants::IndexMethodQuery => execute::op_index_method_query, InsnVariants::Destroy => execute::op_destroy, InsnVariants::ResetSorter => execute::op_reset_sorter, InsnVariants::DropTable => execute::op_drop_table, InsnVariants::DropTrigger => execute::op_drop_trigger, InsnVariants::DropType => execute::op_drop_type, InsnVariants::AddType => execute::op_add_type, InsnVariants::DropView => execute::op_drop_view, InsnVariants::Close => execute::op_close, InsnVariants::IsNull => execute::op_is_null, InsnVariants::CollSeq => execute::op_coll_seq, InsnVariants::ParseSchema => execute::op_parse_schema, InsnVariants::PopulateMaterializedViews => execute::op_populate_materialized_views, InsnVariants::ShiftRight => execute::op_shift_right, InsnVariants::ShiftLeft => execute::op_shift_left, InsnVariants::AddImm => execute::op_add_imm, InsnVariants::Variable => execute::op_variable, InsnVariants::ZeroOrNull => execute::op_zero_or_null, InsnVariants::Not => execute::op_not, InsnVariants::IsTrue => execute::op_is_true, InsnVariants::Concat => execute::op_concat, InsnVariants::And => execute::op_and, InsnVariants::Or => execute::op_or, InsnVariants::Noop => execute::op_noop, InsnVariants::PageCount => execute::op_page_count, InsnVariants::ReadCookie => execute::op_read_cookie, InsnVariants::SetCookie => execute::op_set_cookie, InsnVariants::OpenEphemeral | InsnVariants::OpenAutoindex => execute::op_open_ephemeral, InsnVariants::Once => execute::op_once, InsnVariants::Found | InsnVariants::NotFound => execute::op_found, InsnVariants::Affinity => execute::op_affinity, InsnVariants::IdxDelete => execute::op_idx_delete, InsnVariants::Count => execute::op_count, InsnVariants::IntegrityCk => execute::op_integrity_check, InsnVariants::RenameTable => execute::op_rename_table, InsnVariants::DropColumn => execute::op_drop_column, InsnVariants::AddColumn => execute::op_add_column, InsnVariants::AlterColumn => execute::op_alter_column, InsnVariants::MaxPgcnt => execute::op_max_pgcnt, InsnVariants::JournalMode => execute::op_journal_mode, InsnVariants::IfNeg => execute::op_if_neg, InsnVariants::Explain => execute::op_noop, InsnVariants::OpenDup => execute::op_open_dup, InsnVariants::MemMax => execute::op_mem_max, InsnVariants::Sequence => execute::op_sequence, InsnVariants::SequenceTest => execute::op_sequence_test, InsnVariants::FkCounter => execute::op_fk_counter, InsnVariants::FkIfZero => execute::op_fk_if_zero, InsnVariants::FkCheck => execute::op_fk_check, InsnVariants::VBegin => execute::op_vbegin, InsnVariants::VRename => execute::op_vrename, InsnVariants::FilterAdd => execute::op_filter_add, InsnVariants::Filter => execute::op_filter, InsnVariants::HashBuild => execute::op_hash_build, InsnVariants::HashDistinct => execute::op_hash_distinct, InsnVariants::HashBuildFinalize => execute::op_hash_build_finalize, InsnVariants::HashProbe => execute::op_hash_probe, InsnVariants::HashNext => execute::op_hash_next, InsnVariants::HashClose => execute::op_hash_close, InsnVariants::HashClear => execute::op_hash_clear, InsnVariants::HashMarkMatched => execute::op_hash_mark_matched, InsnVariants::HashResetMatched => execute::op_hash_reset_matched, InsnVariants::HashScanUnmatched => execute::op_hash_scan_unmatched, InsnVariants::HashNextUnmatched => execute::op_hash_next_unmatched, InsnVariants::HashGraceInit => execute::op_hash_grace_init, InsnVariants::HashGraceLoadPartition => execute::op_hash_grace_load_partition, InsnVariants::HashGraceNextProbe => execute::op_hash_grace_next_probe, InsnVariants::HashGraceAdvancePartition => execute::op_hash_grace_advance_partition, InsnVariants::VacuumInto => execute::op_vacuum_into, InsnVariants::InitCdcVersion => execute::op_init_cdc_version, } } } impl Insn { // SAFETY: If the enumeration specifies a primitive representation, // then the discriminant may be reliably accessed via unsafe pointer casting #[inline(always)] fn discriminant(&self) -> u8 { unsafe { *(self as *const Self as *const u8) } } #[inline(always)] pub fn to_function(&self) -> InsnFunction { // dont use this because its still using match // InsnVariants::from(self).to_function_fast() INSN_VTABLE[self.discriminant() as usize] } } // TODO: Add remaining cookies. #[derive(Description, Debug, Clone, Copy)] pub enum Cookie { /// The schema cookie. SchemaVersion = 1, /// The schema format number. Supported schema formats are 1, 2, 3, and 4. DatabaseFormat = 2, /// Default page cache size. DefaultPageCacheSize = 3, /// The page number of the largest root b-tree page when in auto-vacuum or incremental-vacuum modes, or zero otherwise. LargestRootPageNumber = 4, /// The database text encoding. A value of 1 means UTF-8. A value of 2 means UTF-16le. A value of 3 means UTF-16be. DatabaseTextEncoding = 5, /// The "user version" as read and set by the user_version pragma. UserVersion = 6, /// The auto-vacuum mode setting. IncrementalVacuum = 7, /// The application ID as set by the application_id pragma. ApplicationId = 8, } #[cfg(test)] mod tests { use strum::VariantArray; #[test] fn test_make_sure_correct_insn_table() { for variant in super::InsnVariants::VARIANTS { let func1 = variant.to_function(); let func2 = variant.to_function_fast(); assert_eq!( func1 as usize, func2 as usize, "Variant {:?} does not match in fast table at index {}", variant, *variant as usize ); } } } ================================================ FILE: core/vdbe/metrics.rs ================================================ use std::fmt; /// Hash join spill/probe metrics. #[derive(Debug, Default, Clone)] pub struct HashJoinMetrics { // Spill metrics pub spill_bytes_written: u64, pub spill_chunks: u64, pub spill_max_chunks_per_partition: u64, pub spill_max_partition_bytes: u64, // Load metrics pub load_bytes_read: u64, // Probe metrics pub probe_calls: u64, // Grace hash join metrics pub probe_spill_bytes_written: u64, pub probe_spill_chunks: u64, pub grace_partitions_processed: u64, pub grace_probe_rows_streamed: u64, pub grace_probe_rows_buffered: u64, pub grace_matches: u64, } impl HashJoinMetrics { pub fn merge(&mut self, other: &HashJoinMetrics) { self.spill_bytes_written = self .spill_bytes_written .saturating_add(other.spill_bytes_written); self.spill_chunks = self.spill_chunks.saturating_add(other.spill_chunks); self.spill_max_chunks_per_partition = self .spill_max_chunks_per_partition .max(other.spill_max_chunks_per_partition); self.spill_max_partition_bytes = self .spill_max_partition_bytes .max(other.spill_max_partition_bytes); self.load_bytes_read = self.load_bytes_read.saturating_add(other.load_bytes_read); self.probe_calls = self.probe_calls.saturating_add(other.probe_calls); self.probe_spill_bytes_written = self .probe_spill_bytes_written .saturating_add(other.probe_spill_bytes_written); self.probe_spill_chunks = self .probe_spill_chunks .saturating_add(other.probe_spill_chunks); self.grace_partitions_processed = self .grace_partitions_processed .saturating_add(other.grace_partitions_processed); self.grace_probe_rows_streamed = self .grace_probe_rows_streamed .saturating_add(other.grace_probe_rows_streamed); self.grace_probe_rows_buffered = self .grace_probe_rows_buffered .saturating_add(other.grace_probe_rows_buffered); self.grace_matches = self.grace_matches.saturating_add(other.grace_matches); } pub fn reset(&mut self) { *self = Self::default(); } } /// Statement-level execution metrics /// /// These metrics are collected unconditionally during statement execution /// with minimal overhead (simple counter increments). The cost of incrementing /// these counters is negligible compared to the actual work being measured. #[derive(Debug, Default, Clone)] pub struct StatementMetrics { // Row operations pub rows_read: u64, pub rows_written: u64, // Execution statistics pub vm_steps: u64, pub insn_executed: u64, // Table scan metrics pub fullscan_steps: u64, pub index_steps: u64, // Sort and filter operations pub sort_operations: u64, pub filter_operations: u64, // B-tree operations pub btree_seeks: u64, pub btree_next: u64, pub btree_prev: u64, // Hash join spill/probe metrics pub hash_join: HashJoinMetrics, } impl StatementMetrics { pub fn new() -> Self { Self::default() } /// Get total row operations pub fn total_row_ops(&self) -> u64 { self.rows_read + self.rows_written } /// Merge another metrics instance into this one (for aggregation) pub fn merge(&mut self, other: &StatementMetrics) { self.rows_read = self.rows_read.saturating_add(other.rows_read); self.rows_written = self.rows_written.saturating_add(other.rows_written); self.vm_steps = self.vm_steps.saturating_add(other.vm_steps); self.insn_executed = self.insn_executed.saturating_add(other.insn_executed); self.fullscan_steps = self.fullscan_steps.saturating_add(other.fullscan_steps); self.index_steps = self.index_steps.saturating_add(other.index_steps); self.sort_operations = self.sort_operations.saturating_add(other.sort_operations); self.filter_operations = self .filter_operations .saturating_add(other.filter_operations); self.btree_seeks = self.btree_seeks.saturating_add(other.btree_seeks); self.btree_next = self.btree_next.saturating_add(other.btree_next); self.btree_prev = self.btree_prev.saturating_add(other.btree_prev); self.hash_join.merge(&other.hash_join); } /// Reset all counters to zero pub fn reset(&mut self) { *self = Self::default(); } } impl fmt::Display for StatementMetrics { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Statement Metrics:")?; writeln!(f, " Row Operations:")?; writeln!(f, " Rows read: {}", self.rows_read)?; writeln!(f, " Rows written: {}", self.rows_written)?; writeln!(f, " Execution:")?; writeln!(f, " VM steps: {}", self.vm_steps)?; writeln!(f, " Instructions: {}", self.insn_executed)?; writeln!(f, " Table Access:")?; writeln!(f, " Full scan steps: {}", self.fullscan_steps)?; writeln!(f, " Index steps: {}", self.index_steps)?; writeln!(f, " Operations:")?; writeln!(f, " Sort operations: {}", self.sort_operations)?; writeln!(f, " Filter operations:{}", self.filter_operations)?; writeln!(f, " B-tree Operations:")?; writeln!(f, " Seeks: {}", self.btree_seeks)?; writeln!(f, " Next: {}", self.btree_next)?; writeln!(f, " Prev: {}", self.btree_prev)?; writeln!(f, " Hash Join:")?; writeln!( f, " Spill bytes: {}", self.hash_join.spill_bytes_written )?; writeln!(f, " Spill chunks: {}", self.hash_join.spill_chunks)?; writeln!( f, " Max chunks/part: {}", self.hash_join.spill_max_chunks_per_partition )?; writeln!( f, " Max part bytes: {}", self.hash_join.spill_max_partition_bytes )?; writeln!( f, " Load bytes: {}", self.hash_join.load_bytes_read )?; writeln!(f, " Probes: {}", self.hash_join.probe_calls)?; writeln!( f, " Probe spill bytes: {}", self.hash_join.probe_spill_bytes_written )?; writeln!( f, " Probe spill chunks: {}", self.hash_join.probe_spill_chunks )?; writeln!( f, " Grace partitions: {}", self.hash_join.grace_partitions_processed )?; writeln!( f, " Grace streamed: {}", self.hash_join.grace_probe_rows_streamed )?; writeln!( f, " Grace buffered: {}", self.hash_join.grace_probe_rows_buffered )?; writeln!(f, " Grace matches: {}", self.hash_join.grace_matches)?; Ok(()) } } /// Connection-level metrics aggregation #[derive(Debug, Default, Clone)] pub struct ConnectionMetrics { /// Total number of statements executed pub total_statements: u64, /// Aggregate metrics from all statements pub aggregate: StatementMetrics, /// High-water marks for monitoring pub max_vm_steps_per_statement: u64, pub max_rows_read_per_statement: u64, } impl ConnectionMetrics { pub fn new() -> Self { Self::default() } /// Record a completed statement's metrics (borrows, no clone). pub fn record_statement(&mut self, metrics: &StatementMetrics) { self.total_statements = self.total_statements.saturating_add(1); // Update high-water marks self.max_vm_steps_per_statement = self.max_vm_steps_per_statement.max(metrics.vm_steps); self.max_rows_read_per_statement = self.max_rows_read_per_statement.max(metrics.rows_read); // Aggregate into total self.aggregate.merge(metrics); } /// Reset connection metrics pub fn reset(&mut self) { *self = Self::default(); } } impl fmt::Display for ConnectionMetrics { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Connection Metrics:")?; writeln!(f, " Total statements: {}", self.total_statements)?; writeln!(f, " High-water marks:")?; writeln!( f, " Max VM steps: {}", self.max_vm_steps_per_statement )?; writeln!( f, " Max rows read: {}", self.max_rows_read_per_statement )?; writeln!(f)?; writeln!(f, "Aggregate Statistics:")?; write!(f, "{}", self.aggregate)?; Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_metrics_merge() { let mut m1 = StatementMetrics::new(); m1.rows_read = 100; m1.vm_steps = 50; m1.hash_join.spill_bytes_written = 42; let mut m2 = StatementMetrics::new(); m2.rows_read = 200; m2.vm_steps = 75; m2.hash_join.spill_bytes_written = 8; m2.hash_join.spill_max_partition_bytes = 1024; m1.merge(&m2); assert_eq!(m1.rows_read, 300); assert_eq!(m1.vm_steps, 125); assert_eq!(m1.hash_join.spill_bytes_written, 50); assert_eq!(m1.hash_join.spill_max_partition_bytes, 1024); } #[test] fn test_connection_metrics_high_water() { let mut conn_metrics = ConnectionMetrics::new(); let mut stmt1 = StatementMetrics::new(); stmt1.vm_steps = 100; stmt1.rows_read = 50; conn_metrics.record_statement(&stmt1); let mut stmt2 = StatementMetrics::new(); stmt2.vm_steps = 75; stmt2.rows_read = 100; conn_metrics.record_statement(&stmt2); assert_eq!(conn_metrics.max_vm_steps_per_statement, 100); assert_eq!(conn_metrics.max_rows_read_per_statement, 100); assert_eq!(conn_metrics.total_statements, 2); assert_eq!(conn_metrics.aggregate.vm_steps, 175); assert_eq!(conn_metrics.aggregate.rows_read, 150); } } ================================================ FILE: core/vdbe/mod.rs ================================================ //! The virtual database engine (VDBE). //! //! The VDBE is a register-based virtual machine that execute bytecode //! instructions that represent SQL statements. When an application prepares //! an SQL statement, the statement is compiled into a sequence of bytecode //! instructions that perform the needed operations, such as reading or //! writing to a b-tree, sorting, or aggregating data. //! //! The instruction set of the VDBE is similar to SQLite's instruction set, //! but with the exception that bytecodes that perform I/O operations are //! return execution back to the caller instead of blocking. This is because //! Turso is designed for applications that need high concurrency such as //! serverless runtimes. In addition, asynchronous I/O makes storage //! disaggregation easier. //! //! You can find a full list of SQLite opcodes at: //! //! https://www.sqlite.org/opcode.html use crate::types::{Extendable, Text}; use crate::{turso_assert, turso_assert_ne, turso_debug_assert, HashSet, NonNan}; pub mod affinity; pub mod array; pub mod bloom_filter; pub mod builder; pub mod execute; pub mod explain; #[allow(dead_code)] pub mod hash_table; pub mod insn; pub mod metrics; pub mod rowset; pub mod sorter; pub mod value; // for benchmarks pub use crate::translate::collate::CollationSeq; use crate::{ error::LimboError, function::{AggFunc, FuncCtx}, mvcc::{database::CommitStateMachine, MvccClock}, numeric::Numeric, return_if_io, schema::Trigger, state_machine::StateMachine, translate::plan::TableReferences, types::{IOCompletions, IOResult}, vdbe::{ execute::{ OpColumnState, OpDeleteState, OpDeleteSubState, OpDestroyState, OpIdxInsertState, OpInsertState, OpInsertSubState, OpJournalModeState, OpNewRowidState, OpNoConflictState, OpProgramState, OpRowIdState, OpSeekState, OpTransactionState, OpVacuumIntoState, }, hash_table::HashTable, metrics::StatementMetrics, }, ValueRef, }; use smallvec::SmallVec; #[cfg(feature = "json")] use crate::json::JsonCacheCell; use crate::sync::RwLock; use crate::{ storage::pager::Pager, translate::plan::ResultSetColumn, types::{AggContext, Cursor, ImmutableRecord, Value}, vdbe::{builder::CursorType, insn::Insn}, }; use crate::{ AtomicBool, CaptureDataChangesInfo, Connection, MvStore, Result, SyncMode, TransactionState, }; use branches::{mark_unlikely, unlikely}; use builder::{CursorKey, QueryMode}; use execute::{ InsnFunction, InsnFunctionStepResult, OpIdxDeleteState, OpIntegrityCheckState, OpOpenEphemeralState, }; use turso_parser::ast::ResolveType; use crate::vdbe::bloom_filter::BloomFilter; use crate::vdbe::rowset::RowSet; use explain::{insn_to_row_with_comment, EXPLAIN_COLUMNS, EXPLAIN_QUERY_PLAN_COLUMNS}; use std::{ collections::HashMap, num::NonZero, ops::Deref, sync::{ atomic::{AtomicI64, AtomicIsize, Ordering}, Arc, }, task::Waker, }; use tracing::{instrument, Level}; /// State machine for committing view deltas with I/O handling #[derive(Debug, Clone)] pub enum ViewDeltaCommitState { NotStarted, Processing { views: Vec, // view names (all materialized views have storage) current_index: usize, }, Done, } /// We use labels to indicate that we want to jump to whatever the instruction offset /// will be at runtime, because the offset cannot always be determined when the jump /// instruction is created. /// /// In some cases, we want to jump to EXACTLY a specific instruction. /// - Example: a condition is not met, so we want to jump to wherever Halt is. /// /// In other cases, we don't care what the exact instruction is, but we know that we /// want to jump to whatever comes AFTER a certain instruction. /// - Example: a Next instruction will want to jump to "whatever the start of the loop is", /// but it doesn't care what instruction that is. /// /// The reason this distinction is important is that we might reorder instructions that are /// constant at compile time, and when we do that, we need to change the offsets of any impacted /// jump instructions, so the instruction that comes immediately after "next Insn" might have changed during the reordering. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum JumpTarget { ExactlyThisInsn, AfterThisInsn, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// Represents a target for a jump instruction. /// Stores 32-bit ints to keep the enum word-sized. pub enum BranchOffset { /// A label is a named location in the program. /// If there are references to it, it must always be resolved to an Offset /// via program.resolve_label(). Label(u32), /// An offset is a direct index into the instruction list. Offset(InsnReference), /// A placeholder is a temporary value to satisfy the compiler. /// It must be set later. Placeholder, } impl BranchOffset { /// Returns true if the branch offset is a label. pub fn is_label(&self) -> bool { matches!(self, BranchOffset::Label(_)) } /// Returns true if the branch offset is an offset. pub fn is_offset(&self) -> bool { matches!(self, BranchOffset::Offset(_)) } /// Returns the offset value. Panics if the branch offset is a label or placeholder. pub fn as_offset_int(&self) -> InsnReference { match self { BranchOffset::Label(v) => unreachable!("Unresolved label: {}", v), BranchOffset::Offset(v) => *v, BranchOffset::Placeholder => unreachable!("Unresolved placeholder"), } } /// Returns the branch offset as a signed integer. /// Used in explain output, where we don't want to panic in case we have an unresolved /// label or placeholder. pub fn as_debug_int(&self) -> i32 { match self { BranchOffset::Label(v) => *v as i32, BranchOffset::Offset(v) => *v as i32, BranchOffset::Placeholder => i32::MAX, } } /// Adds an integer value to the branch offset. /// Returns a new branch offset. /// Panics if the branch offset is a label or placeholder. #[allow(clippy::should_implement_trait)] pub fn add>(self, n: N) -> BranchOffset { BranchOffset::Offset(self.as_offset_int() + n.into()) } #[allow(clippy::should_implement_trait)] pub fn sub>(self, n: N) -> BranchOffset { BranchOffset::Offset(self.as_offset_int() - n.into()) } } pub type CursorID = usize; pub type PageIdx = i64; // Index of insn in list of insns type InsnReference = u32; #[derive(Debug)] pub enum StepResult { Done, IO, Row, Interrupt, Busy, } #[derive(Debug)] #[allow(clippy::large_enum_variant)] /// The commit state of the program. /// There are two states: /// - Ready: The program is ready to run the next instruction, or has shut down after /// the last instruction. /// - Committing: The program is committing a write transaction. It is waiting for the pager to finish flushing the cache to disk, /// primarily to the WAL, but also possibly checkpointing the WAL to the database file. enum CommitState { Ready, Committing, /// Committing attached database pagers after main pager commit is done. CommittingAttached, CommittingMvcc { state_machine: StateMachine>, }, /// Committing MVCC transactions on attached databases after main MVCC commit is done. CommittingAttachedMvcc { state_machine: StateMachine>, db_id: usize, mv_store: Arc, }, } #[derive(Debug, Clone)] pub enum Register { Value(Value), Aggregate(AggContext), Record(ImmutableRecord), } impl Register { #[inline] pub fn is_null(&self) -> bool { matches!(self, Register::Value(Value::Null)) } #[inline(always)] /// Sets the value of the register to an integer, /// reusing the existing Register::Value(Value::Numeric(Numeric::Integer(_))) if possible, /// which is faster than always creating a new one. pub fn set_int(&mut self, val: i64) { match self { Register::Value(Value::Numeric(Numeric::Integer(existing))) => { *existing = val; } Register::Value(Value::Numeric(float)) => { *float = Numeric::Integer(val); } Register::Value(other_value_kind) => { *other_value_kind = Value::from_i64(val); } _ => { *self = Register::Value(Value::from_i64(val)); } } } /// Set the value of the register to a floating point, /// reusing Register::Value(Value::Numeric(Numeric::Float(_))) if possible. #[inline(always)] pub fn set_float(&mut self, val: NonNan) { match self { Register::Value(Value::Numeric(Numeric::Float(existing))) => { *existing = val; } Register::Value(Value::Numeric(integer)) => { *integer = Numeric::Float(val); } Register::Value(other_value_kind) => { *other_value_kind = Value::Numeric(Numeric::Float(val)); } _ => { *self = Register::Value(Value::Numeric(Numeric::Float(val))); } } } /// Set the value of the register to a Text, /// reusing Register::Value(Value::Text(_)) buffer if possible. #[inline] pub fn set_text(&mut self, val: Text) { match self { Register::Value(Value::Text(existing)) => { existing.do_extend(&val); } Register::Value(other_value_kind) => { *other_value_kind = Value::Text(val); } _ => { *self = Register::Value(Value::Text(val)); } } } /// Set the value of the register to a blob, /// reusing Register::Value(Value::Blob(_)) buffer if possible. #[inline] pub fn set_blob(&mut self, val: Vec) { match self { Register::Value(Value::Blob(existing)) => { existing.do_extend(&val); } Register::Value(other_value_kind) => { *other_value_kind = Value::Blob(val); } _ => { *self = Register::Value(Value::Blob(val)); } } } // Set the value of the register to NULL, // reusing the existing Register::Value(Value::Null) if possible. pub fn set_null(&mut self) { match self { Register::Value(Value::Null) => {} Register::Value(other_value_kind) => { *other_value_kind = Value::Null; } _ => { *self = Register::Value(Value::Null); } } } /// Set the register to a generic Value, attempting to reuse backing allocation if compatible. pub fn set_value(&mut self, val: Value) { match self { Register::Value(v) => { *v = val; } _ => { *self = Register::Value(val); } } } } /// A row is a the list of registers that hold the values for a filtered row. This row is a pointer, therefore /// after stepping again, row will be invalidated to be sure it doesn't point to somewhere unexpected. #[derive(Debug)] pub struct Row { values: *const Register, count: usize, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum TxnCleanup { None, RollbackTxn, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum ProgramExecutionState { /// No steps of the program was executed Init, /// Program started execution but didn't reach any terminal state Running, /// Interrupt requested for the program Interrupting, /// Terminal state: program interrupted Interrupted, /// Terminal state: program finished successfully Done, /// Terminal state: program failed with error Failed, } impl ProgramExecutionState { pub fn is_running(&self) -> bool { matches!( self, ProgramExecutionState::Interrupting | ProgramExecutionState::Running ) } pub fn is_terminal(&self) -> bool { matches!( self, ProgramExecutionState::Interrupted | ProgramExecutionState::Failed | ProgramExecutionState::Done ) } } /// Re-entrant state for [Insn::HashBuild]. /// Allows HashBuild to resume cleanly after async I/O without re-reading the row. #[derive(Debug, Default)] pub struct OpHashBuildState { pub key_values: Vec, pub key_idx: usize, pub payload_values: Vec, pub payload_idx: usize, pub rowid: Option, pub cursor_id: CursorID, pub hash_table_id: usize, pub key_start_reg: usize, pub num_keys: usize, } /// Re-entrant state for [Insn::HashProbe]. /// Allows HashProbe to resume cleanly after async probe-row buffering I/O. #[derive(Debug, Default)] pub struct OpHashProbeState { /// Cached probe key values to avoid re-reading from registers pub probe_keys: Vec, /// Hash table register being probed pub hash_table_id: usize, /// Partition index being loaded (if any) pub partition_idx: usize, /// Whether the probe row was already buffered for grace processing. pub probe_buffered: bool, } #[derive(Debug, Clone)] pub(crate) struct DeferredSeekState { pub index_cursor_id: CursorID, pub table_cursor_id: CursorID, } /// The program state describes the environment in which the program executes. pub struct ProgramState { pub io_completions: Option, pub pc: InsnReference, pub(crate) cursors: Vec>, cursor_seqs: Vec, registers: Box<[Register]>, pub(crate) result_row: Option, last_compare: Option, deferred_seeks: Vec>, /// Indicate whether a coroutine has ended for a given yield register. /// If an element is present, it means the coroutine with the given register number has ended. ended_coroutine: Vec, /// Indicate whether an [Insn::Once] instruction at a given program counter position has already been executed, well, once. once: SmallVec<[u32; 4]>, pub execution_state: ProgramExecutionState, pub parameters: Vec, commit_state: CommitState, #[cfg(feature = "json")] json_cache: JsonCacheCell, op_delete_state: OpDeleteState, op_destroy_state: OpDestroyState, op_idx_delete_state: Option, op_integrity_check_state: OpIntegrityCheckState, /// Metrics collected during statement execution pub metrics: StatementMetrics, op_open_ephemeral_state: OpOpenEphemeralState, op_program_state: OpProgramState, op_new_rowid_state: OpNewRowidState, op_idx_insert_state: OpIdxInsertState, op_insert_state: OpInsertState, op_no_conflict_state: OpNoConflictState, seek_state: OpSeekState, /// Current collation sequence set by OP_CollSeq instruction current_collation: Option, op_column_state: OpColumnState, op_row_id_state: OpRowIdState, op_transaction_state: OpTransactionState, op_journal_mode_state: OpJournalModeState, op_vacuum_into_state: Option, /// State machine for committing view deltas with I/O handling view_delta_state: ViewDeltaCommitState, /// Marker which tells about auto transaction cleanup necessary for that connection in case of reset /// This is used when statement in auto-commit mode reseted after previous uncomplete execution - in which case we may need to rollback transaction started on previous attempt pub(crate) auto_txn_cleanup: TxnCleanup, /// Number of deferred foreign key violations when the statement started. /// When a statement subtransaction rolls back, the connection's deferred foreign key violations counter /// is reset to this value. fk_deferred_violations_when_stmt_started: AtomicIsize, /// Number of immediate foreign key violations that occurred during the active statement. If nonzero, /// the statement subtransactionwill roll back. fk_immediate_violations_during_stmt: AtomicIsize, /// RowSet objects stored by register index rowsets: HashMap, /// Bloom filters stored by cursor ID for probabilistic set membership testing /// Used to avoid unnecessary seeks on ephemeral indexes and hash tables pub(crate) bloom_filters: HashMap, op_hash_build_state: Option, op_hash_probe_state: Option, /// Scratch buffer for [Insn::HashDistinct] to avoid per-row allocations. distinct_key_values: Vec, hash_tables: HashMap, uses_subjournal: bool, /// Whether this statement is an active write inside an explicit transaction. pub(crate) is_active_write: bool, /// Whether begin_statement was called (savepoint + FK bookkeeping active). has_stmt_transaction: bool, /// Attached pagers that have open savepoints for statement rollback. attached_savepoint_pagers: Vec>, pub n_change: AtomicI64, pub explain_state: RwLock, /// Pending error to return after FAIL mode commit completes. /// When a constraint error occurs with FAIL resolve type in autocommit mode, /// we need to commit partial changes before returning the error. pub(crate) pending_fail_error: Option, /// Pending CDC info to apply after the program completes successfully. /// Set by InitCdcVersion opcode, applied at Halt/Done so that if the /// transaction rolls back, the connection's CDC state remains unchanged. /// /// capture_data_changes has type Option (off mode is None) /// so, for pending_cdc_info we wrap it in one more Option<...> layer to represent if mode changed during program execution pub(crate) pending_cdc_info: Option>, } impl std::fmt::Debug for Program { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Program").finish() } } // See: https://github.com/tursodatabase/turso/issues/1552 // SAFETY: Rust cannot derive Send + Sync automatically mainly because of `Row` struct // as it contains a `*const Register`. // Program + Program State upholds Rust aliasing rules with `Row` by only giving out immutable references to // the internal `result_row` and by invalidating the result row whenever the program is stepped. unsafe impl Send for ProgramState {} unsafe impl Sync for ProgramState {} crate::assert::assert_send_sync!(ProgramState); impl ProgramState { pub fn new(max_registers: usize, max_cursors: usize) -> Self { let cursors: Vec> = (0..max_cursors).map(|_| None).collect(); let cursor_seqs = vec![0i64; max_cursors]; let registers = vec![Register::Value(Value::Null); max_registers].into_boxed_slice(); Self { io_completions: None, pc: 0, cursors, cursor_seqs, registers, result_row: None, last_compare: None, deferred_seeks: vec![None; max_cursors], ended_coroutine: vec![], once: SmallVec::<[u32; 4]>::new(), execution_state: ProgramExecutionState::Init, parameters: Vec::new(), commit_state: CommitState::Ready, #[cfg(feature = "json")] json_cache: JsonCacheCell::new(), op_delete_state: OpDeleteState { sub_state: OpDeleteSubState::MaybeCaptureRecord, deleted_record: None, }, op_destroy_state: OpDestroyState::CreateCursor, op_idx_delete_state: None, op_integrity_check_state: OpIntegrityCheckState::Start, metrics: StatementMetrics::new(), op_open_ephemeral_state: OpOpenEphemeralState::Start, op_program_state: OpProgramState::Start, op_new_rowid_state: OpNewRowidState::Start, op_idx_insert_state: OpIdxInsertState::MaybeSeek, op_insert_state: OpInsertState { sub_state: OpInsertSubState::MaybeCaptureRecord, old_record: None, is_noop_update: false, }, op_no_conflict_state: OpNoConflictState::Start, op_hash_build_state: None, op_hash_probe_state: None, distinct_key_values: Vec::new(), seek_state: OpSeekState::Start, current_collation: None, op_column_state: OpColumnState::Start, op_row_id_state: OpRowIdState::Start, op_transaction_state: OpTransactionState::Start, op_journal_mode_state: OpJournalModeState::default(), op_vacuum_into_state: None, view_delta_state: ViewDeltaCommitState::NotStarted, auto_txn_cleanup: TxnCleanup::None, fk_deferred_violations_when_stmt_started: AtomicIsize::new(0), fk_immediate_violations_during_stmt: AtomicIsize::new(0), rowsets: HashMap::default(), bloom_filters: HashMap::default(), hash_tables: HashMap::default(), uses_subjournal: false, is_active_write: false, has_stmt_transaction: false, attached_savepoint_pagers: Vec::new(), n_change: AtomicI64::new(0), explain_state: RwLock::new(ExplainState::default()), pending_fail_error: None, pending_cdc_info: None, } } pub fn set_register(&mut self, idx: usize, value: Register) { self.registers[idx] = value; } pub fn get_register(&self, idx: usize) -> &Register { &self.registers[idx] } pub fn column_count(&self) -> usize { self.registers.len() } pub fn column(&self, i: usize) -> Option { Some(format!("{:?}", self.registers[i])) } pub fn interrupt(&mut self) { self.execution_state = ProgramExecutionState::Interrupting; } pub fn bind_at(&mut self, index: NonZero, value: Value) { let i = index.get() - 1; if i >= self.parameters.len() { self.parameters.resize(i + 1, Value::Null); } let slot = &mut self.parameters[i]; match (slot, value) { (Value::Null, Value::Null) => {} (Value::Numeric(Numeric::Integer(existing)), Value::Numeric(Numeric::Integer(new))) => { *existing = new } (Value::Numeric(Numeric::Float(existing)), Value::Numeric(Numeric::Float(new))) => { *existing = new } (Value::Text(existing), Value::Text(new)) => existing.do_extend(&new), (Value::Blob(existing), Value::Blob(new)) => existing.do_extend(&new), (slot, value) => *slot = value, } } pub fn clear_bindings(&mut self) { self.parameters.clear(); } pub fn get_parameter(&self, index: NonZero) -> Value { let i = index.get() - 1; self.parameters.get(i).cloned().unwrap_or(Value::Null) } pub fn reset(&mut self, max_registers: Option, max_cursors: Option) { self.io_completions = None; self.pc = 0; if let Some(max_cursors) = max_cursors { self.cursors.resize_with(max_cursors, || None); self.cursor_seqs.resize(max_cursors, 0); self.deferred_seeks.resize(max_cursors, None); } self.result_row = None; if let Some(max_registers) = max_registers { // into_vec and into_boxed_slice do not allocate let mut registers = std::mem::take(&mut self.registers).into_vec(); // As we are dropping whatever is in the result row, we can be sure that no one is referencing values from `*const Register` inside `Row`. registers.resize_with(max_registers, || Register::Value(Value::Null)); self.registers = registers.into_boxed_slice(); } // reset cursors as they can have cached information which will be no longer relevant on next program execution self.cursors.iter_mut().for_each(|c| { let _ = c.take(); }); for r in self.registers.iter_mut() { match r { Register::Value(v) => *v = Value::Null, _ => r.set_null(), } } self.last_compare = None; self.deferred_seeks.iter_mut().for_each(|s| *s = None); self.ended_coroutine.clear(); self.once.clear(); self.execution_state = ProgramExecutionState::Init; self.current_collation = None; #[cfg(feature = "json")] self.json_cache.clear(); // Reset state machines self.op_delete_state = OpDeleteState { sub_state: OpDeleteSubState::MaybeCaptureRecord, deleted_record: None, }; self.op_idx_delete_state = None; self.op_integrity_check_state = OpIntegrityCheckState::Start; self.metrics = StatementMetrics::new(); self.op_open_ephemeral_state = OpOpenEphemeralState::Start; self.op_new_rowid_state = OpNewRowidState::Start; self.op_idx_insert_state = OpIdxInsertState::MaybeSeek; self.op_insert_state = OpInsertState { sub_state: OpInsertSubState::MaybeCaptureRecord, old_record: None, is_noop_update: false, }; self.op_no_conflict_state = OpNoConflictState::Start; self.seek_state = OpSeekState::Start; self.current_collation = None; self.op_column_state = OpColumnState::Start; self.op_row_id_state = OpRowIdState::Start; self.commit_state = CommitState::Ready; self.op_destroy_state = OpDestroyState::CreateCursor; self.op_program_state = OpProgramState::Start; self.op_transaction_state = OpTransactionState::Start; self.op_journal_mode_state = OpJournalModeState::default(); self.op_vacuum_into_state = None; self.view_delta_state = ViewDeltaCommitState::NotStarted; self.auto_txn_cleanup = TxnCleanup::None; self.fk_immediate_violations_during_stmt .store(0, Ordering::SeqCst); self.fk_deferred_violations_when_stmt_started .store(0, Ordering::SeqCst); self.rowsets.clear(); self.bloom_filters.clear(); self.hash_tables.clear(); self.op_hash_build_state = None; self.op_hash_probe_state = None; self.uses_subjournal = false; self.is_active_write = false; self.has_stmt_transaction = false; self.distinct_key_values.clear(); self.attached_savepoint_pagers.clear(); self.n_change.store(0, Ordering::SeqCst); *self.explain_state.write() = ExplainState::default(); self.pending_fail_error = None; self.pending_cdc_info = None; } pub fn get_cursor(&mut self, cursor_id: CursorID) -> &mut Cursor { self.cursors .get_mut(cursor_id) .unwrap_or_else(|| panic!("cursor id {cursor_id} out of bounds")) .as_mut() .unwrap_or_else(|| panic!("cursor id {cursor_id} is None")) } /// Begin a statement subtransaction. /// /// Creates a savepoint on the main DB's MvStore (or pager for WAL mode), /// and snapshots FK violation counters for potential statement rollback. /// Attached DB savepoints are opened per-DB in `op_transaction_inner` /// when each DB's Transaction opcode is executed. /// /// Pager/MVCC savepoints are only opened for write statements inside an /// explicit transaction. In autocommit mode, a statement abort is a /// transaction abort, so savepoints are unnecessary. pub fn begin_statement( &mut self, connection: &Connection, pager: &Arc, write: bool, ) -> Result> { let in_explicit_txn = !connection.auto_commit.load(Ordering::SeqCst); if write && in_explicit_txn { // Check if MVCC is active - if so, use MVCC savepoints instead of pager savepoints if let Some(mv_store) = connection.mv_store().as_ref() { if let Some(tx_id) = connection.get_mv_tx_id() { mv_store.begin_savepoint(tx_id); } } else { // Non-MVCC mode: use pager savepoints let db_size = return_if_io!(pager.with_header(|header| header.database_size.get())); pager.open_subjournal()?; pager.try_use_subjournal()?; let result = pager.open_savepoint(db_size); if result.is_err() { pager.stop_use_subjournal(); } result?; self.uses_subjournal = true; } } self.has_stmt_transaction = true; // Store the deferred foreign key violations counter at the start of the statement. // This is used to ensure that if an interactive transaction had deferred FK violations and a statement subtransaction rolls back, // the deferred FK violations are not lost. self.fk_deferred_violations_when_stmt_started.store( connection.fk_deferred_violations.load(Ordering::Acquire), Ordering::SeqCst, ); // Reset the immediate foreign key violations counter to 0. If this is nonzero when the statement completes, the statement subtransaction will roll back. self.fk_immediate_violations_during_stmt .store(0, Ordering::Release); Ok(IOResult::Done(())) } /// End a statement subtransaction. /// /// Mirrors SQLite's vdbeCloseStatement (vdbeaux.c:3203-3248). Pager/MVCC /// savepoint management and FK violation counter restoration are independent /// concerns: pager savepoints may be skipped (e.g. autocommit optimization) /// while FK bookkeeping still needs cleanup. pub fn end_statement( &mut self, connection: &Connection, pager: &Arc, end_statement: EndStatement, ) -> Result<()> { if self.is_active_write { connection.n_active_writes.fetch_sub(1, Ordering::SeqCst); self.is_active_write = false; } // If begin_statement was never called, no savepoint/FK cleanup needed. if !self.has_stmt_transaction { return Ok(()); } self.has_stmt_transaction = false; // Drain attached pagers upfront so we can clean them up regardless of path. let attached_pagers: Vec> = self.attached_savepoint_pagers.drain(..).collect(); let result = match end_statement { EndStatement::ReleaseSavepoint => { if let Some(mv_store) = connection.mv_store().as_ref() { if let Some(tx_id) = connection.get_mv_tx_id() { mv_store.release_savepoint(tx_id); } connection.for_each_attached_mv_tx(|db_id, tx_id| { if let Some(attached_mv) = connection.mv_store_for_db(db_id) { attached_mv.release_savepoint(tx_id); } }); Ok(()) } else if self.uses_subjournal { pager.release_savepoint()?; for p in &attached_pagers { p.release_savepoint()?; } Ok(()) } else { Ok(()) } } EndStatement::RollbackSavepoint => { // Rollback pager/MVCC savepoint if one was opened. let pager_err = if let Some(mv_store) = connection.mv_store().as_ref() { let mut err = None; if let Some(tx_id) = connection.get_mv_tx_id() { if let Err(e) = mv_store.rollback_first_savepoint(tx_id) { err = Some(e); } } connection.for_each_attached_mv_tx(|db_id, tx_id| { if let Some(attached_mv) = connection.mv_store_for_db(db_id) { if let Err(e) = attached_mv.rollback_first_savepoint(tx_id) { if err.is_none() { err = Some(e); } } } }); err } else if self.uses_subjournal { match pager.rollback_to_newest_savepoint() { Ok(_) => { let mut err = None; for p in &attached_pagers { if let Err(e) = p.rollback_to_newest_savepoint() { err = Some(e); break; } } err } Err(e) => Some(e), } } else { None }; // Always restore FK violation counters on statement rollback, // regardless of whether a pager savepoint was opened. // Mirrors SQLite's vdbeCloseStatement (vdbeaux.c:3243-3246). connection.fk_deferred_violations.store( self.fk_deferred_violations_when_stmt_started .load(Ordering::Acquire), Ordering::SeqCst, ); match pager_err { Some(e) => Err(e), None => Ok(()), } } }; if self.uses_subjournal { pager.stop_use_subjournal(); self.uses_subjournal = false; } for p in &attached_pagers { p.stop_use_subjournal(); } result } /// Gets or creates a bloom filter for the given cursor ID. pub fn get_or_create_bloom_filter(&mut self, cursor_id: usize) -> &mut BloomFilter { self.bloom_filters.entry(cursor_id).or_default() } /// Gets or creates a bloom filter with a specific capacity for the given cursor ID. pub fn get_or_create_bloom_filter_with_capacity( &mut self, cursor_id: usize, expected_items: u32, false_positive_rate: f32, ) -> &mut BloomFilter { self.bloom_filters .entry(cursor_id) .or_insert_with(|| BloomFilter::with_capacity(expected_items, false_positive_rate)) } /// Gets an existing bloom filter for the given cursor ID. pub fn get_bloom_filter(&self, cursor_id: usize) -> Option<&BloomFilter> { self.bloom_filters.get(&cursor_id) } /// Gets a mutable reference to an existing bloom filter for the given cursor ID. pub fn get_bloom_filter_mut(&mut self, cursor_id: usize) -> Option<&mut BloomFilter> { self.bloom_filters.get_mut(&cursor_id) } /// Removes and drops the bloom filter for the given cursor ID. pub fn remove_bloom_filter(&mut self, cursor_id: usize) { self.bloom_filters.remove(&cursor_id); } /// Checks if a bloom filter exists for the given cursor ID. pub fn has_bloom_filter(&self, cursor_id: usize) -> bool { self.bloom_filters.contains_key(&cursor_id) } pub fn get_fk_immediate_violations_during_stmt(&self) -> isize { self.fk_immediate_violations_during_stmt .load(Ordering::Acquire) } pub fn increment_fk_immediate_violations_during_stmt(&self, v: isize) { self.fk_immediate_violations_during_stmt .fetch_add(v, Ordering::AcqRel); } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Action to take at the end of a statement subtransaction. pub enum EndStatement { /// Release (commit) the savepoint -- effectively removing the savepoint as it is no longer needed for undo purposes. ReleaseSavepoint, /// Rollback (abort) to the newest savepoint: read pages from the subjournal and restore them to the page cache. /// This is used to undo the changes made by the statement. RollbackSavepoint, } impl Register { pub fn get_value(&self) -> &Value { match self { Register::Value(v) => v, Register::Record(r) => { turso_assert!(!r.is_invalidated()); r.as_blob_value() } _ => panic!("register holds unexpected value: {self:?}"), } } } #[macro_export] macro_rules! must_be_btree_cursor { ($cursor_id:expr, $cursor_ref:expr, $state:expr, $insn_name:expr) => {{ let (_, cursor_type) = $cursor_ref.get($cursor_id).unwrap(); if matches!( cursor_type, CursorType::BTreeTable(_) | CursorType::BTreeIndex(_) | CursorType::MaterializedView(_, _) ) { $crate::get_cursor!($state, $cursor_id) } else { panic!("{} on unexpected cursor", $insn_name) } }}; } /// Macro is necessary to help the borrow checker see we are only accessing state.cursor field /// and nothing else #[macro_export] macro_rules! get_cursor { ($state:expr, $cursor_id:expr) => { $state .cursors .get_mut($cursor_id) .unwrap_or_else(|| panic!("cursor id {} out of bounds", $cursor_id)) .as_mut() .unwrap_or_else(|| panic!("cursor id {} is None", $cursor_id)) }; } /// Tracks the state of explain mode execution, including which subprograms need to be processed. #[derive(Default)] pub struct ExplainState { /// Subprograms queued for explain output, processed after the parent program finishes. pending: std::collections::VecDeque>, /// The subprogram currently being explained, if any. current: Option>, } #[derive(Debug, Clone)] pub struct PreparedProgram { pub max_registers: usize, // we store original indices because we don't want to create new vec from // ProgramBuilder pub insns: Vec<(Insn, usize)>, pub cursor_ref: Vec<(Option, CursorType)>, pub comments: Vec<(InsnReference, &'static str)>, pub parameters: crate::parameters::Parameters, pub change_cnt_on: bool, pub result_columns: Vec, pub table_references: TableReferences, pub sql: String, /// Whether the statement needs to be wrapped in a statement subtransaction /// when run as part of an interactive (non-autocommit) transaction. /// See [crate::vdbe::builder::ProgramBuilder::is_multi_write] and [crate::vdbe::builder::ProgramBuilder::may_abort] for more details. pub needs_stmt_subtransactions: Arc, /// If this Program is a trigger subprogram, a ref to the trigger is stored here. pub trigger: Option>, /// Whether this program is a subprogram (trigger or FK action) that runs within a parent statement. pub is_subprogram: bool, /// Whether the program contains any trigger subprograms. pub contains_trigger_subprograms: bool, pub resolve_type: ResolveType, pub prepare_context: PrepareContext, /// Set of attached database indices that need write transactions. pub write_databases: HashSet, /// Set of attached database indices that need read transactions. pub read_databases: HashSet, } #[derive(Clone)] pub struct Program { pub(crate) prepared: Arc, pub connection: Arc, } /// Captures connection settings at statement preparation time for cache invalidation. /// /// This struct is used to detect when a cached prepared statement needs to be recompiled /// because relevant connection settings have changed. When `matches_connection()` returns /// false, the statement will be automatically reprepared before execution. /// /// # Adding New Fields /// /// If you add a new setting to `Connection` that affects statement compilation or execution, /// When adding a new connection setting that affects query compilation, you MUST call /// `bump_prepare_context_generation()` in its setter so that prepared statements know /// they need to be reprepared. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PrepareContext { /// Identity check: the prepared statement must belong to the same database. database_ptr: usize, /// Generation counter snapshot taken at prepare time. Compared against the /// connection's current generation to detect setting changes (pragmas, /// attach/detach, extension registration, etc.) without rebuilding the full /// context on every step. generation: u64, } impl PrepareContext { pub fn from_connection(connection: &Connection) -> Self { Self { database_ptr: connection.database_ptr(), generation: connection.prepare_context_generation(), } } #[inline] pub fn matches_connection(&self, connection: &Connection) -> bool { self.database_ptr == connection.database_ptr() && self.generation == connection.prepare_context_generation() } } impl PreparedProgram { pub fn bind(self: Arc, connection: Arc) -> Program { Program { prepared: self, connection, } } pub fn is_compatible_with(&self, connection: &Connection) -> bool { self.prepare_context.matches_connection(connection) } } impl Program { pub fn prepared(&self) -> &Arc { &self.prepared } pub fn from_prepared(prepared: Arc, connection: Arc) -> Self { Self { prepared, connection, } } } impl Program { fn get_pager_from_database_index(&self, idx: &usize) -> Arc { self.connection.get_pager_from_database_index(idx) } pub fn step( &self, state: &mut ProgramState, pager: &Arc, query_mode: QueryMode, waker: Option<&Waker>, ) -> Result { state.execution_state = ProgramExecutionState::Running; let result = match query_mode { QueryMode::Normal => self.normal_step(state, pager, waker), QueryMode::Explain => self.explain_step(state, pager), QueryMode::ExplainQueryPlan => self.explain_query_plan_step(state, pager), }; match &result { Ok(StepResult::Done) => { state.execution_state = ProgramExecutionState::Done; } Ok(StepResult::Interrupt) => { state.execution_state = ProgramExecutionState::Interrupted; } Err(_) => { state.execution_state = ProgramExecutionState::Failed; } _ => {} } result } fn explain_step(&self, state: &mut ProgramState, pager: &Arc) -> Result { turso_debug_assert!(state.column_count() == EXPLAIN_COLUMNS.len()); if self.connection.is_closed() { let tx_state = self.connection.get_tx_state(); if let TransactionState::Write { .. } = tx_state { pager.rollback_tx(&self.connection); } return Err(LimboError::InternalError("Connection closed".to_string())); } if matches!(state.execution_state, ProgramExecutionState::Interrupting) { return Ok(StepResult::Interrupt); } state.metrics.vm_steps = state.metrics.vm_steps.saturating_add(1); let mut explain_state = state.explain_state.write(); // Advance to the next subprogram if the current one is finished loop { if let Some(ref current) = explain_state.current { if (state.pc as usize) < current.insns.len() { break; } } else if (state.pc as usize) < self.insns.len() { break; } // Current program is done, pop next subprogram from queue if let Some(next) = explain_state.pending.pop_front() { explain_state.current = Some(next); state.pc = 0; } else { explain_state.current = None; return Ok(StepResult::Done); } } let pc = state.pc as usize; // Explain the current instruction from the active program. // We collect subprograms separately to avoid borrow conflicts with explain_state. let (row, subprogram) = if let Some(ref current) = explain_state.current { let (insn, _) = ¤t.insns[pc]; let sub = if let Insn::Program { program: prepared, .. } = insn { Some(prepared.clone()) } else { None }; let comment = current .comments .iter() .find(|(offset, _)| *offset == state.pc) .map(|(_, c)| *c); (insn_to_row_with_comment(current, insn, comment), sub) } else { let (insn, _) = &self.insns[pc]; let sub = if let Insn::Program { program: prepared, .. } = insn { Some(prepared.clone()) } else { None }; let comment = self .comments .iter() .find(|(offset, _)| *offset == state.pc) .map(|(_, c)| *c); (insn_to_row_with_comment(self, insn, comment), sub) }; if let Some(sub) = subprogram { explain_state.pending.push_back(sub); } let (opcode, p1, p2, p3, p4, p5, comment) = row; state.registers[0].set_int(state.pc as i64); state.registers[1].set_value(Value::from_text(opcode)); state.registers[2].set_int(p1); state.registers[3].set_int(p2); state.registers[4].set_int(p3); state.registers[5].set_value(p4); state.registers[6].set_int(p5); state.registers[7].set_value(Value::from_text(comment)); state.result_row = Some(Row { values: &state.registers[0] as *const Register, count: EXPLAIN_COLUMNS.len(), }); state.pc += 1; Ok(StepResult::Row) } fn explain_query_plan_step( &self, state: &mut ProgramState, pager: &Arc, ) -> Result { turso_debug_assert!(state.column_count() == EXPLAIN_QUERY_PLAN_COLUMNS.len()); loop { if self.connection.is_closed() { // Connection is closed for whatever reason, rollback the transaction. let state = self.connection.get_tx_state(); if let TransactionState::Write { .. } = state { pager.rollback_tx(&self.connection); } return Err(LimboError::InternalError("Connection closed".to_string())); } if matches!(state.execution_state, ProgramExecutionState::Interrupting) { return Ok(StepResult::Interrupt); } // FIXME: do we need this? state.metrics.vm_steps = state.metrics.vm_steps.saturating_add(1); if state.pc as usize >= self.insns.len() { return Ok(StepResult::Done); } let Insn::Explain { p1, p2, detail } = &self.insns[state.pc as usize].0 else { state.pc += 1; continue; }; state.registers[0].set_int(*p1 as i64); state.registers[1] = Register::Value(Value::from_i64(p2.as_ref().map(|p| *p).unwrap_or(0) as i64)); state.registers[2].set_int(0); state.registers[3].set_value(Value::from_text(detail.clone())); state.result_row = Some(Row { values: &state.registers[0] as *const Register, count: EXPLAIN_QUERY_PLAN_COLUMNS.len(), }); state.pc += 1; return Ok(StepResult::Row); } } #[instrument(skip_all, level = Level::DEBUG)] fn normal_step( &self, state: &mut ProgramState, pager: &Arc, waker: Option<&Waker>, ) -> Result { let enable_tracing = tracing::enabled!(tracing::Level::TRACE); loop { if self.connection.is_closed() { // Connection is closed for whatever reason, rollback the transaction. let state = self.connection.get_tx_state(); if let TransactionState::Write { .. } = state { pager.rollback_tx(&self.connection); } return Err(LimboError::InternalError("Connection closed".to_string())); } if matches!(state.execution_state, ProgramExecutionState::Interrupting) { self.abort(pager, None, state)?; return Ok(StepResult::Interrupt); } if let Some(io) = &state.io_completions { if !io.finished() { io.set_waker(waker); return Ok(StepResult::IO); } if let Some(err) = io.get_error() { if pager.is_checkpointing() { // Wrap IO errors that occurred during checkpointing in CheckpointFailed error, // so that abort() knows not to try to rollback the transaction, because the transaction // is already durable in the WAL and hence committed. // This also lets the simulator know that it should shadow the results of the query because // the write itself succeeded. let checkpoint_err = LimboError::CheckpointFailed(err.to_string()); tracing::error!("Checkpoint failed: {checkpoint_err}"); if let Err(abort_err) = self.abort(pager, Some(&checkpoint_err), state) { tracing::error!( "Abort also failed during checkpoint error handling: {abort_err}" ); } return Err(checkpoint_err); } let err = err.into(); if let Err(abort_err) = self.abort(pager, Some(&err), state) { tracing::error!("Abort failed during error handling: {abort_err}"); } return Err(err); } state.io_completions = None; } // invalidate row let _ = state.result_row.take(); let (insn, _) = &self.insns[state.pc as usize]; let insn_function = insn.to_function(); if enable_tracing { trace_insn(self, state.pc as InsnReference, insn); } // Always increment VM steps for every loop iteration state.metrics.vm_steps = state.metrics.vm_steps.saturating_add(1); match insn_function(self, state, insn, pager) { Ok(InsnFunctionStepResult::Step) => { // Instruction completed, moving to next state.metrics.insn_executed = state.metrics.insn_executed.saturating_add(1); } Ok(InsnFunctionStepResult::Done) => { // Instruction completed execution state.metrics.insn_executed = state.metrics.insn_executed.saturating_add(1); state.auto_txn_cleanup = TxnCleanup::None; return Ok(StepResult::Done); } Ok(InsnFunctionStepResult::IO(io)) => { // Instruction not complete - waiting for I/O, will resume at same PC io.set_waker(waker); let is_yield = io.is_explicit_yield(); if is_yield { // Yield: return control to the cooperative scheduler so // other connections can make progress (e.g. release a // contended lock). Don't store in io_completions — // yields aren't pending I/O, so the instruction will // simply re-execute on the next step. return Ok(StepResult::IO); } let finished = io.finished(); state.io_completions = Some(io); if !finished { return Ok(StepResult::IO); } // just continue the outer loop if IO is finished so db will continue execution immediately } Ok(InsnFunctionStepResult::Row) => { // Instruction completed (ResultRow already incremented PC) state.metrics.insn_executed = state.metrics.insn_executed.saturating_add(1); return Ok(StepResult::Row); } Err(LimboError::Busy) => { // Instruction blocked - will retry at same PC return Ok(StepResult::Busy); } Err(LimboError::BusySnapshot) if self.connection.transaction_state.get() == TransactionState::None => { // For interactive transactions that are already in a read transaction, retrying BusySnapshot is pointless // because the snapshot will continue to be stale no matter how many times we retry. // However, for auto-commits or BEGIN IMMEDIATE, failing to promote to write transaction means it was rolled // back, so auto-retrying can be useful. return Ok(StepResult::Busy); } Err(err) => { if let Err(abort_err) = self.abort(pager, Some(&err), state) { tracing::error!("Abort failed during error handling: {abort_err}"); } return Err(err); } } } } #[instrument(skip_all, level = Level::DEBUG)] fn apply_view_deltas( &self, state: &mut ProgramState, rollback: bool, pager: &Arc, ) -> Result> { use crate::types::IOResult; loop { match &state.view_delta_state { ViewDeltaCommitState::NotStarted => { if self.connection.view_transaction_states.is_empty() { return Ok(IOResult::Done(())); } if rollback { // On rollback, just clear and done self.connection.view_transaction_states.clear(); return Ok(IOResult::Done(())); } // Not a rollback - proceed with processing let schema = self.connection.schema.read(); // Collect materialized views - they should all have storage let mut views = Vec::new(); for view_name in self.connection.view_transaction_states.get_view_names() { if let Some(view_mutex) = schema.get_materialized_view(&view_name) { let view = view_mutex.lock(); let root_page = view.get_root_page(); // Materialized views should always have storage (root_page != 0) turso_assert_ne!( root_page, 0, "Materialized view should have a root page", { "view_name": view_name } ); views.push(view_name); } } state.view_delta_state = ViewDeltaCommitState::Processing { views, current_index: 0, }; } ViewDeltaCommitState::Processing { views, current_index, } => { // At this point we know it's not a rollback if *current_index >= views.len() { // All done, clear the transaction states self.connection.view_transaction_states.clear(); state.view_delta_state = ViewDeltaCommitState::Done; return Ok(IOResult::Done(())); } let view_name = &views[*current_index]; let table_deltas = self .connection .view_transaction_states .get(view_name) .expect("view should have transaction state") .get_table_deltas(); let schema = self.connection.schema.read(); if let Some(view_mutex) = schema.get_materialized_view(view_name) { let mut view = view_mutex.lock(); // Create a DeltaSet from the per-table deltas let mut delta_set = crate::incremental::compiler::DeltaSet::new(); for (table_name, delta) in table_deltas { delta_set.insert(table_name, delta); } // Handle I/O from merge_delta - pass pager, circuit will create its own cursor match view.merge_delta(delta_set, pager.clone())? { IOResult::Done(_) => { // Move to next view state.view_delta_state = ViewDeltaCommitState::Processing { views: views.clone(), current_index: current_index + 1, }; } IOResult::IO(io) => { // Return I/O, will resume at same index return Ok(IOResult::IO(io)); } } } } ViewDeltaCommitState::Done => { return Ok(IOResult::Done(())); } } } } pub fn commit_txn( &self, pager: Arc, program_state: &mut ProgramState, mv_store: Option<&Arc>, rollback: bool, ) -> Result> { // Apply view deltas with I/O handling match self.apply_view_deltas(program_state, rollback, &pager)? { IOResult::IO(io) => return Ok(IOResult::IO(io)), IOResult::Done(_) => {} } // Reset state for next use program_state.view_delta_state = ViewDeltaCommitState::NotStarted; let tx_state = self.connection.get_tx_state(); if tx_state == TransactionState::None && matches!(program_state.commit_state, CommitState::Ready) { // No active transaction and no in-progress commit — nothing to do. // Attached MVCC transactions are only started after the main DB's // Transaction opcode runs, so tx_state==None implies no attached // MVCC txs either. return Ok(IOResult::Done(())); } if self.connection.is_nested_stmt() { // We don't want to commit on nested statements. Let parent handle it. return Ok(IOResult::Done(())); } let res = if let Some(mv_store) = mv_store { self.commit_txn_mvcc(pager, program_state, mv_store, rollback) } else { self.commit_txn_wal(pager, program_state, rollback) }?; if !res.is_io() && self.change_cnt_on { self.connection .set_changes(program_state.n_change.load(Ordering::SeqCst)); } Ok(res) } fn commit_txn_wal( &self, pager: Arc, program_state: &mut ProgramState, rollback: bool, ) -> Result> { let connection = self.connection.clone(); let auto_commit = connection.auto_commit.load(Ordering::SeqCst); let tx_state = connection.get_tx_state(); tracing::debug!( "Halt auto_commit {}, commit_state={:?}, tx_state={:?}", auto_commit, program_state.commit_state, tx_state, ); if matches!(program_state.commit_state, CommitState::Committing) { let TransactionState::Write { .. } = tx_state else { unreachable!("invalid state for write commit step") }; self.step_end_write_txn(&pager, &connection, program_state, rollback) } else if matches!(program_state.commit_state, CommitState::CommittingAttached) { // Re-entry after IO yield from attached pager commit. match self.end_attached_write_txns(&connection, rollback)? { IOResult::Done(_) => { program_state.commit_state = CommitState::Ready; if pager.holds_read_lock() { pager.end_read_tx(); } self.end_attached_read_txns(&connection); Ok(IOResult::Done(())) } IOResult::IO(io) => Ok(IOResult::IO(io)), } } else if auto_commit { match tx_state { TransactionState::Write { .. } => { self.step_end_write_txn(&pager, &connection, program_state, rollback) } TransactionState::Read => { connection.set_tx_state(TransactionState::None); // Commit any attached write transactions that were opened // independently of the main connection's transaction state. // (e.g., UPDATE aux0.t SET ... only needs Read on main DB // but holds a write lock on the attached pager.) match self.end_attached_write_txns(&connection, rollback)? { IOResult::Done(_) => {} IOResult::IO(io) => { program_state.commit_state = CommitState::CommittingAttached; return Ok(IOResult::IO(io)); } } pager.end_read_tx(); self.end_attached_read_txns(&connection); Ok(IOResult::Done(())) } TransactionState::None => Ok(IOResult::Done(())), TransactionState::PendingUpgrade { .. } => { panic!("Unexpected transaction state: {tx_state:?} during auto-commit",) } } } else { Ok(IOResult::Done(())) } } /// Commit MVCC transactions across all databases in a multi-phase protocol: /// /// 1. **Main DB MVCC** — commit the main database's MvStore transaction. /// 2. **Attached MVCC** — commit each attached database's MvStore transaction. /// 3. **Attached WAL** — flush dirty pages on attached databases that use WAL /// (e.g. :memory: attached while main is MVCC). /// /// **IMPORTANT**: This multi-phase commit is NOT atomic across databases. /// A crash between phases can leave the main and attached databases in /// inconsistent states (main committed, some attached DBs not committed). /// This matches SQLite's WAL mode behavior — cross-file atomicity only /// exists in legacy rollback journal mode, which we do not support. fn commit_txn_mvcc( &self, pager: Arc, program_state: &mut ProgramState, mv_store: &Arc, rollback: bool, ) -> Result> { let conn = self.connection.clone(); let auto_commit = conn.auto_commit.load(Ordering::SeqCst); if !auto_commit { return Ok(IOResult::Done(())); } // Phase 1: Commit main DB MVCC transaction if matches!(program_state.commit_state, CommitState::Ready) { if let Some(tx_id) = conn.get_mv_tx_id() { let state_machine = mv_store.commit_tx(tx_id, &conn)?; program_state.commit_state = CommitState::CommittingMvcc { state_machine }; } // If no main MVCC tx, commit_state stays Ready and we fall // through directly to phase 2 (the CommittingMvcc and // CommittingAttachedMvcc checks will both miss). } if matches!( program_state.commit_state, CommitState::CommittingMvcc { .. } ) { let CommitState::CommittingMvcc { state_machine } = &mut program_state.commit_state else { unreachable!() }; match self.step_end_mvcc_txn(state_machine, mv_store)? { IOResult::Done(_) => { assert!(state_machine.is_finalized()); conn.set_mv_tx(None); conn.set_tx_state(TransactionState::None); pager.end_read_tx(); program_state.commit_state = CommitState::Ready; // Fall through to attached phase } IOResult::IO(io) => return Ok(IOResult::IO(io)), } } // Phase 2: Commit MVCC transactions on attached databases // Resume an in-progress attached MVCC commit if matches!( program_state.commit_state, CommitState::CommittingAttachedMvcc { .. } ) { let (step_result, db_id) = { let CommitState::CommittingAttachedMvcc { state_machine, db_id, mv_store: ref attached_mv, } = &mut program_state.commit_state else { unreachable!() }; (state_machine.step(attached_mv)?, *db_id) }; match step_result { IOResult::Done(_) => { let attached_pager = conn.get_pager_from_database_index(&db_id); conn.publish_attached_schema(db_id); conn.set_mv_tx_for_db(db_id, None); attached_pager.end_read_tx(); // Fall through to look for more } IOResult::IO(io) => return Ok(IOResult::IO(io)), } } // Start/continue committing remaining attached MVCC transactions loop { let Some((db_id, tx_id, _mode)) = conn.next_attached_mv_tx() else { break; }; let Some(attached_mv_store) = conn.mv_store_for_db(db_id) else { conn.set_mv_tx_for_db(db_id, None); continue; }; let mut state_machine = match attached_mv_store.commit_tx(tx_id, &conn) { Ok(sm) => sm, Err(e) => { tracing::error!( db_id, "attached DB commit failed after main DB already committed; \ cross-database state is inconsistent: {e}" ); // Rollback remaining uncommitted attached MVCC transactions // so they don't block checkpointing until connection close. conn.rollback_attached_mvcc_txs(true); return Err(e); } }; match state_machine.step(&attached_mv_store)? { IOResult::Done(_) => { let attached_pager = conn.get_pager_from_database_index(&db_id); conn.publish_attached_schema(db_id); conn.set_mv_tx_for_db(db_id, None); attached_pager.end_read_tx(); continue; } IOResult::IO(io) => { program_state.commit_state = CommitState::CommittingAttachedMvcc { state_machine, db_id, mv_store: attached_mv_store, }; return Ok(IOResult::IO(io)); } } } // Phase 3: Commit WAL transactions on attached databases that don't use MVCC. // When the main DB uses MVCC, we route through commit_txn_mvcc, but attached // DBs may use WAL mode and need their dirty pages committed via the WAL path. if matches!(program_state.commit_state, CommitState::CommittingAttached) { // Re-entry after IO yield from attached WAL pager commit. match self.end_attached_write_txns(&conn, rollback)? { IOResult::Done(_) => { program_state.commit_state = CommitState::Ready; self.end_attached_read_txns(&conn); return Ok(IOResult::Done(())); } IOResult::IO(io) => return Ok(IOResult::IO(io)), } } match self.end_attached_write_txns(&conn, rollback)? { IOResult::Done(_) => {} IOResult::IO(io) => { program_state.commit_state = CommitState::CommittingAttached; return Ok(IOResult::IO(io)); } } self.end_attached_read_txns(&conn); program_state.commit_state = CommitState::Ready; Ok(IOResult::Done(())) } #[instrument(skip(self, pager, connection, program_state), level = Level::DEBUG)] fn step_end_write_txn( &self, pager: &Arc, connection: &Connection, program_state: &mut ProgramState, rollback: bool, ) -> Result> { let commit_state = &mut program_state.commit_state; if matches!(commit_state, CommitState::CommittingAttached) { // Resume committing attached pagers after IO yield. match self.end_attached_write_txns(connection, rollback)? { IOResult::Done(_) => { *commit_state = CommitState::Ready; } IOResult::IO(io) => { return Ok(IOResult::IO(io)); } } // Release read locks on attached pagers that only had read transactions // (end_attached_write_txns only handles pagers with write locks). self.end_attached_read_txns(connection); return Ok(IOResult::Done(())); } let txn_finish_result = if !rollback { pager.commit_tx(connection, true) } else { pager.rollback_tx(connection); Ok(IOResult::Done(())) }; tracing::debug!("txn_finish_result: {:?}", txn_finish_result); match txn_finish_result? { IOResult::Done(_) => { // Main pager commit done, now commit attached database pagers match self.end_attached_write_txns(connection, rollback)? { IOResult::Done(_) => { *commit_state = CommitState::Ready; } IOResult::IO(io) => { *commit_state = CommitState::CommittingAttached; return Ok(IOResult::IO(io)); } } } IOResult::IO(io) => { tracing::trace!("Cacheflush IO"); *commit_state = CommitState::Committing; return Ok(IOResult::IO(io)); } } // Release read locks on attached pagers that only had read transactions // (end_attached_write_txns only handles pagers with write locks). self.end_attached_read_txns(connection); Ok(IOResult::Done(())) } /// End write transactions on all attached databases that hold write locks. /// Iterates ALL attached pagers (not just the current program's write_databases) /// because in explicit transactions, the COMMIT statement's program may differ /// from the statement that acquired the attached write lock. /// On IO yield, already-committed pagers are skipped on re-entry via holds_write_lock(). fn end_attached_write_txns( &self, connection: &Connection, rollback: bool, ) -> Result> { let pagers = connection.get_all_attached_pagers_with_index(); for (db_id, attached_pager) in pagers { // MVCC-enabled attached DBs are committed in commit_txn_mvcc phase 2 if connection.mv_store_for_db(db_id).is_some() { continue; } if !attached_pager.holds_write_lock() { continue; } if !rollback { // Commit dirty pages to WAL, then end write+read transactions. // We disable auto-checkpoint and avoid pager.commit_tx() since // the checkpoint logic can leave read locks held. match attached_pager.commit_dirty_pages(true, SyncMode::Normal, false) { Ok(IOResult::Done(_)) => {} Ok(IOResult::IO(io)) => { // IO pending — return so the caller can yield and re-enter. // commit_dirty_pages tracks its own internal state, so calling // it again on re-entry will resume correctly. return Ok(IOResult::IO(io)); } Err(e) => return Err(e), } // WAL commit succeeded — publish the connection-local schema // changes to the shared Database so other connections can see them. connection.publish_attached_schema(db_id); attached_pager.end_write_tx(); attached_pager.end_read_tx(); attached_pager.commit_dirty_pages_end(); } else { // Discard any local schema changes on rollback connection.database_schemas().write().remove(&db_id); attached_pager.rollback_attached(); } } Ok(IOResult::Done(())) } /// End read transactions on all attached databases that had transactions started. fn end_attached_read_txns(&self, connection: &Connection) { for attached_pager in connection.get_all_attached_pagers() { if attached_pager.holds_read_lock() { attached_pager.end_read_tx(); } } } #[instrument(skip(self, commit_state, mv_store), level = Level::DEBUG)] fn step_end_mvcc_txn( &self, commit_state: &mut StateMachine>, mv_store: &Arc, ) -> Result> { commit_state.step(mv_store) } /// Aborts the program due to various conditions (explicit error, interrupt or reset of unfinished statement) by rolling back the transaction /// This method is no-op if program was already finished (either aborted or executed to completion) /// Returns an error if cleanup operations (savepoint rollback/release) fail. pub fn abort( &self, pager: &Arc, err: Option<&LimboError>, state: &mut ProgramState, ) -> Result<()> { fn capture_abort_error( abort_error: &mut Option, err: LimboError, context: &str, ) { tracing::error!("{context}: {err}"); if abort_error.is_none() { *abort_error = Some(err); } } let mut abort_error: Option = None; if self.is_trigger_subprogram() { self.connection.end_trigger_execution(); } // Errors from nested statements are handled by the parent statement. if !self.connection.is_nested_stmt() && !self.is_trigger_subprogram() { if err.is_some() && !pager.is_checkpointing() { // For ON CONFLICT FAIL, do NOT rollback the statement savepoint — // changes made before the error should persist. // For all other resolve types (ABORT, ROLLBACK, etc.), rollback the statement. let is_fail_constraint = (matches!(err, Some(LimboError::Constraint(_))) && self.resolve_type == ResolveType::Fail) || matches!(err, Some(LimboError::Raise(ResolveType::Fail, _))); if !is_fail_constraint { if let Err(end_stmt_err) = state.end_statement( &self.connection, pager, EndStatement::RollbackSavepoint, ) { capture_abort_error( &mut abort_error, end_stmt_err, "Failed to rollback statement savepoint during abort", ); } } } match err { // Transaction errors, e.g. trying to start a nested transaction, do not cause a rollback. Some(LimboError::TxError(_)) => {} // Table locked errors, e.g. trying to checkpoint in an interactive transaction, do not cause a rollback. Some(LimboError::TableLocked) => {} // Busy errors do not cause a rollback. Some(LimboError::Busy) => {} // BusySnapshot errors do not cause a rollback either - user must rollback explicitly. // BusySnapshot is distinct from Busy in that a busy_timeout or handler should not be // used because it will not help - the snapshot is permanently stale and rollback is // the only way out for this poor transaction. Some(LimboError::BusySnapshot) => {} // Schema updated errors do not cause a rollback; the statement will be reprepared and retried, // and the caller is expected to handle transaction cleanup explicitly if needed. Some(LimboError::SchemaUpdated) => {} // Foreign key constraint errors: ON CONFLICT does NOT apply to FK violations. // FK errors always behave like ABORT: rollback statement, // rollback transaction in autocommit mode. Some(LimboError::ForeignKeyConstraint(_)) => { if self.connection.get_auto_commit() { self.rollback_current_txn(pager); } } // Constraint and RAISE errors: behavior depends on the effective resolve type. // For normal constraints, the resolve type comes from the statement (ON CONFLICT). // For RAISE errors, the resolve type is embedded in the error variant itself. // - ROLLBACK: rollback the entire transaction regardless of autocommit mode // - FAIL: don't rollback anything - changes persist, transaction stays active // - ABORT (default): rollback statement, rollback txn if autocommit Some(LimboError::Constraint(_)) | Some(LimboError::Raise(_, _)) => { let effective_resolve = match err { Some(LimboError::Raise(rt, _)) => *rt, _ => self.resolve_type, }; match effective_resolve { ResolveType::Rollback => { self.rollback_current_txn(pager); // All deferred FK violations are undone by the full rollback. self.connection.clear_deferred_foreign_key_violations(); } ResolveType::Fail => { // FAIL: Don't rollback the transaction. // Changes made before the error persist. if let Err(end_stmt_err) = state.end_statement( &self.connection, pager, EndStatement::ReleaseSavepoint, ) { capture_abort_error( &mut abort_error, end_stmt_err, "Failed to release statement savepoint during abort", ); } if self.connection.get_auto_commit() { // Autocommit FAIL: commit partial changes. // This matches halt()'s FAIL+autocommit path. let mv_store = self.connection.mv_store(); if let Err(e) = execute::vtab_commit_all(&self.connection) { capture_abort_error( &mut abort_error, e, "vtab_commit_all failed during FAIL abort", ); } if let Err(e) = execute::index_method_pre_commit_all(state, pager) { capture_abort_error( &mut abort_error, e, "index_method_pre_commit_all failed during FAIL abort", ); } loop { match self.commit_txn( pager.clone(), state, mv_store.as_ref(), false, ) { Ok(IOResult::Done(_)) => break, Ok(IOResult::IO(io)) => { if let Err(e) = io.wait(pager.io.as_ref()) { capture_abort_error( &mut abort_error, e, "IO error during FAIL commit in abort", ); break; } } Err(e) => { capture_abort_error( &mut abort_error, e, "commit_txn failed during FAIL abort", ); break; } } } } } _ => { if self.connection.get_auto_commit() { self.rollback_current_txn(pager); } } } } Some(LimboError::RaiseIgnore) => { tracing::error!( "BUG: RaiseIgnore reached abort() - should be caught by op_program" ); debug_assert!( false, "RaiseIgnore should be caught by op_program, not reach abort" ); } _ => { if state.auto_txn_cleanup != TxnCleanup::None || err.is_some() { self.rollback_current_txn(pager); } } } } if state.uses_subjournal { pager.stop_use_subjournal(); state.uses_subjournal = false; } state.auto_txn_cleanup = TxnCleanup::None; if let Some(err) = abort_error { return Err(err); } Ok(()) } fn rollback_current_txn(&self, pager: &Arc) { if let Some(mv_store) = self.connection.mv_store().as_ref() { if let Some(tx_id) = self.connection.get_mv_tx_id() { self.connection.auto_commit.store(true, Ordering::SeqCst); mv_store.rollback_tx(tx_id, pager.clone(), &self.connection, crate::MAIN_DB_ID); } pager.end_read_tx(); self.connection.rollback_attached_mvcc_txs(true); } else { pager.rollback_tx(&self.connection); self.connection.auto_commit.store(true, Ordering::SeqCst); } self.connection.rollback_attached_wal_txns(); self.connection.set_tx_state(TransactionState::None); } pub fn is_trigger_subprogram(&self) -> bool { self.trigger.is_some() || self.is_subprogram } } impl Deref for Program { type Target = PreparedProgram; fn deref(&self) -> &PreparedProgram { &self.prepared } } pub(crate) fn make_record( registers: &[Register], start_reg: &usize, count: &usize, ) -> ImmutableRecord { let regs = ®isters[*start_reg..*start_reg + *count]; ImmutableRecord::from_registers(regs, regs.len()) } pub fn registers_to_ref_values<'a>( registers: &'a [Register], ) -> impl ExactSizeIterator> { registers.iter().map(|reg| reg.get_value().as_ref()) } #[instrument(skip(program), level = Level::DEBUG)] fn trace_insn(program: &Program, addr: InsnReference, insn: &Insn) { tracing::trace!( "\n{}", explain::insn_to_str( program, addr, insn, String::new(), program .comments .iter() .find(|(offset, _)| *offset == addr) .map(|(_, comment)| comment) .copied() ) ); } pub trait FromValueRow<'a> { fn from_value(value: &'a Value) -> Result where Self: Sized + 'a; } impl<'a> FromValueRow<'a> for i64 { fn from_value(value: &'a Value) -> Result { match value { Value::Numeric(Numeric::Integer(i)) => Ok(*i), _ => Err(LimboError::ConversionError("Expected integer value".into())), } } } impl<'a> FromValueRow<'a> for f64 { fn from_value(value: &'a Value) -> Result { match value { Value::Numeric(Numeric::Float(f)) => Ok(f64::from(*f)), _ => Err(LimboError::ConversionError("Expected integer value".into())), } } } impl<'a> FromValueRow<'a> for String { fn from_value(value: &'a Value) -> Result { match value { Value::Text(s) => Ok(s.as_str().to_string()), _ => Err(LimboError::ConversionError("Expected text value".into())), } } } impl<'a> FromValueRow<'a> for &'a str { fn from_value(value: &'a Value) -> Result { match value { Value::Text(s) => Ok(s.as_str()), _ => Err(LimboError::ConversionError("Expected text value".into())), } } } impl<'a> FromValueRow<'a> for &'a Value { fn from_value(value: &'a Value) -> Result { Ok(value) } } impl Row { pub fn get<'a, T: FromValueRow<'a> + 'a>(&'a self, idx: usize) -> Result { let value = unsafe { self.values .add(idx) .as_ref() .expect("row value pointer should be valid") }; let value = match value { Register::Value(value) => value, _ => unreachable!("a row should be formed of values only"), }; T::from_value(value) } pub fn get_value(&self, idx: usize) -> &Value { let value = unsafe { self.values .add(idx) .as_ref() .expect("row value pointer should be valid") }; match value { Register::Value(value) => value, _ => unreachable!("a row should be formed of values only"), } } pub fn get_values(&self) -> impl Iterator { let values = unsafe { std::slice::from_raw_parts(self.values, self.count) }; // This should be ownedvalues // TODO: add check for this values.iter().map(|v| v.get_value()) } pub fn len(&self) -> usize { self.count } pub fn is_empty(&self) -> bool { self.count == 0 } } /// Extension trait for `ValueIterator` that allows writing directly to a `Register` /// without allocating intermediate `ValueRef` values. pub trait ValueIteratorExt { /// Skips `n` elements and writes the value directly to the register. /// Returns `Some(Ok(()))` on success, `Some(Err(...))` on parse error, /// or `None` if there are fewer than `n+1` elements. fn nth_into_register(&mut self, n: usize, dest: &mut Register) -> Option>; } impl<'a> ValueIteratorExt for crate::types::ValueIterator<'a> { #[inline(always)] fn nth_into_register(&mut self, n: usize, dest: &mut Register) -> Option> { use crate::storage::sqlite3_ondisk::read_varint; use crate::types::{get_serial_type_size, Extendable, Text}; let mut header = self.header_section_ref(); let mut data = self.data_section_ref(); // Skip n elements let mut data_sum = 0; for _ in 0..n { if header.is_empty() { return None; } let (serial_type, bytes_read) = match read_varint(header) { Ok(v) => v, Err(e) => return Some(Err(e)), }; header = &header[bytes_read..]; data_sum += match get_serial_type_size(serial_type) { Ok(size) => size, Err(e) => return Some(Err(e)), }; } if data_sum > data.len() { return Some(Err(LimboError::Corrupt( "Data section too small for indicated serial type size".into(), ))); } data = &data[data_sum..]; // Read the serial type for the target element if header.is_empty() { return None; } let (serial_type, bytes_read) = match read_varint(header) { Ok(v) => v, Err(e) => return Some(Err(e)), }; // Update iterator state self.set_header_section(&header[bytes_read..]); // Decode directly into register based on serial type match serial_type { // NULL 0 => { self.set_data_section(data); dest.set_null(); } // I8 1 => { if unlikely(data.is_empty()) { return Some(Err(LimboError::Corrupt("Invalid 1-byte int".into()))); } self.set_data_section(&data[1..]); dest.set_int(data[0] as i8 as i64); } // I16 2 => { if unlikely(data.len() < 2) { return Some(Err(LimboError::Corrupt("Invalid 2-byte int".into()))); } self.set_data_section(&data[2..]); dest.set_int(i16::from_be_bytes([data[0], data[1]]) as i64); } // I24 3 => { if unlikely(data.len() < 3) { return Some(Err(LimboError::Corrupt("Invalid 3-byte int".into()))); } self.set_data_section(&data[3..]); let sign_extension = if data[0] <= 0x7F { 0 } else { 0xFF }; dest.set_int( i32::from_be_bytes([sign_extension, data[0], data[1], data[2]]) as i64, ); } // I32 4 => { if unlikely(data.len() < 4) { return Some(Err(LimboError::Corrupt("Invalid 4-byte int".into()))); } self.set_data_section(&data[4..]); dest.set_int(i32::from_be_bytes([data[0], data[1], data[2], data[3]]) as i64); } // I48 5 => { if unlikely(data.len() < 6) { return Some(Err(LimboError::Corrupt("Invalid 6-byte int".into()))); } self.set_data_section(&data[6..]); let sign_extension = if data[0] <= 0x7F { 0 } else { 0xFF }; dest.set_int(i64::from_be_bytes([ sign_extension, sign_extension, data[0], data[1], data[2], data[3], data[4], data[5], ])); } // I64 6 => { if unlikely(data.len() < 8) { return Some(Err(LimboError::Corrupt("Invalid 8-byte int".into()))); } self.set_data_section(&data[8..]); dest.set_int(i64::from_be_bytes([ data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], ])); } // F64 7 => { if unlikely(data.len() < 8) { return Some(Err(LimboError::Corrupt("Invalid 8-byte float".into()))); } self.set_data_section(&data[8..]); let val = f64::from_be_bytes([ data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], ]); if let Some(nn) = NonNan::new(val) { dest.set_float(nn); } else { dest.set_null(); } } // CONST_INT0 8 => { self.set_data_section(data); dest.set_int(0); } // CONST_INT1 9 => { self.set_data_section(data); dest.set_int(1); } // Reserved 10 | 11 => { mark_unlikely(); return Some(Err(LimboError::Corrupt(format!( "Reserved serial type: {serial_type}" )))); } // BLOB (n >= 12 && n & 1 == 0) n if n >= 12 && n & 1 == 0 => { let content_size = ((n - 12) / 2) as usize; if unlikely(data.len() < content_size) { return Some(Err(LimboError::Corrupt("Invalid Blob value".into()))); } self.set_data_section(&data[content_size..]); let blob_data = &data[..content_size]; match dest { Register::Value(Value::Blob(existing_blob)) => { existing_blob.do_extend(&blob_data); } _ => { dest.set_blob(blob_data.to_vec()); } } } // TEXT (n >= 13 && n & 1 == 1) n if n >= 13 && n & 1 == 1 => { let content_size = ((n - 13) / 2) as usize; if unlikely(data.len() < content_size) { return Some(Err(LimboError::Corrupt("Invalid Text value".into()))); } self.set_data_section(&data[content_size..]); let text_data = &data[..content_size]; // SAFETY: TEXT serial type contains valid UTF-8 let text_str = if cfg!(debug_assertions) { match std::str::from_utf8(text_data) { Ok(s) => s, Err(e) => { return Some(Err(LimboError::InternalError(format!( "Invalid UTF-8 in TEXT serial type: {e}" )))); } } } else { unsafe { std::str::from_utf8_unchecked(text_data) } }; match dest { Register::Value(Value::Text(existing_text)) => { existing_text.do_extend(&text_str); } _ => { dest.set_text(Text::new(text_str.to_string())); } } } _ => { mark_unlikely(); return Some(Err(LimboError::Corrupt(format!( "Invalid serial type: {serial_type}" )))); } } Some(Ok(())) } } /// Shuttle tests for validating the `unsafe impl Send + Sync for ProgramState` safety claims. /// /// The safety claims are: /// 1. `Row` contains a `*const Register` pointing into `ProgramState.registers` /// 2. Only immutable references (`&Row`) are given out via `result_row.as_ref()` /// 3. `result_row` is invalidated (via `.take()`) at the start of each step iteration /// /// These tests verify that the implementation correctly upholds these invariants /// under concurrent access patterns. #[cfg(all(shuttle, test))] mod shuttle_tests { use super::*; use crate::sync::Arc; use crate::thread; use crate::types::Value; /// Creates a minimal ProgramState for testing. fn create_test_state(num_registers: usize, num_cursors: usize) -> ProgramState { ProgramState::new(num_registers, num_cursors) } /// Test that ProgramState can be safely sent between threads. /// This validates the `unsafe impl Send for ProgramState` claim. #[test] fn shuttle_program_state_send() { shuttle::check_random( || { let mut state = create_test_state(10, 2); // Write some data to registers state.registers[0].set_int(42); state.registers[1].set_text(Text::new("test".to_string())); // Send state to another thread let handle = thread::spawn(move || { // Verify data is intact after send assert!(matches!( &state.registers[0], Register::Value(Value::Numeric(Numeric::Integer(42))) )); if let Register::Value(Value::Text(t)) = &state.registers[1] { assert_eq!(t.as_str(), "test"); } else { panic!("Expected text value"); } // Modify in new thread state.registers[2].set_int(100); state }); let state = handle.join().unwrap(); assert!(matches!( &state.registers[2], Register::Value(Value::Numeric(Numeric::Integer(100))) )); }, 1000, ); } /// Test that ProgramState with a set result_row can be safely sent. /// The Row contains a raw pointer that must remain valid after the send. #[test] fn shuttle_program_state_send_with_row() { shuttle::check_random( || { let mut state = create_test_state(10, 2); // Set up registers with test data state.registers[0].set_int(1); state.registers[1].set_int(2); state.registers[2].set_int(3); // Create a result_row pointing to registers state.result_row = Some(Row { values: &state.registers[0] as *const Register, count: 3, }); // Send to another thread - the pointer must remain valid // because it points to memory owned by state (the registers Vec) let handle = thread::spawn(move || { // The row pointer should still be valid because registers moved with state if let Some(row) = &state.result_row { assert_eq!(row.len(), 3); // Read through the pointer - this validates the pointer is still valid let val = row.get::(0).unwrap(); assert_eq!(val, 1); let val = row.get::(1).unwrap(); assert_eq!(val, 2); let val = row.get::(2).unwrap(); assert_eq!(val, 3); } else { panic!("Expected result_row to be set"); } state }); let _ = handle.join().unwrap(); }, 1000, ); } /// Test concurrent reads of result_row through shared reference. /// This validates the `unsafe impl Sync for ProgramState` claim for read access. #[test] fn shuttle_program_state_sync_concurrent_reads() { shuttle::check_random( || { let mut state = create_test_state(10, 2); // Set up registers state.registers[0].set_int(42); state.registers[1].set_int(43); // Create result_row state.result_row = Some(Row { values: &state.registers[0] as *const Register, count: 2, }); let state = Arc::new(state); let state2 = Arc::clone(&state); let state3 = Arc::clone(&state); // Multiple threads reading concurrently let h1 = thread::spawn(move || { if let Some(row) = &state.result_row { let val = row.get::(0).unwrap(); assert_eq!(val, 42); } }); let h2 = thread::spawn(move || { if let Some(row) = &state2.result_row { let val = row.get::(1).unwrap(); assert_eq!(val, 43); } }); let h3 = thread::spawn(move || { if let Some(row) = &state3.result_row { assert_eq!(row.len(), 2); } }); h1.join().unwrap(); h2.join().unwrap(); h3.join().unwrap(); }, 1000, ); } /// Test that Row values read through the pointer are consistent. /// Multiple threads reading the same row values should see the same data. #[test] fn shuttle_row_pointer_consistency() { shuttle::check_random( || { let mut state = create_test_state(10, 2); // Set up registers with distinct values for i in 0..5 { state.registers[i].set_int(i as i64 * 10); } state.result_row = Some(Row { values: &state.registers[0] as *const Register, count: 5, }); let state = Arc::new(state); let mut handles = vec![]; for _ in 0..4 { let state_clone = Arc::clone(&state); let h = thread::spawn(move || { if let Some(row) = &state_clone.result_row { // All threads should see the same values for i in 0..5 { let val = row.get::(i).unwrap(); assert_eq!(val, i as i64 * 10); } } }); handles.push(h); } for h in handles { h.join().unwrap(); } }, 1000, ); } /// Test the result_row invalidation pattern. /// When result_row is taken (invalidated), concurrent reads should not see stale data. /// This simulates the pattern used in `normal_step()` where `result_row.take()` is called. #[test] fn shuttle_result_row_invalidation() { shuttle::check_random( || { let mut state = create_test_state(10, 2); state.registers[0].set_int(100); state.result_row = Some(Row { values: &state.registers[0] as *const Register, count: 1, }); // Simulate the invalidation pattern from normal_step // In real code, this requires &mut self, so there's no concurrent access let taken_row = state.result_row.take(); // After take(), result_row should be None assert!(state.result_row.is_none()); // The taken row still holds valid data (until dropped) if let Some(row) = taken_row { let val = row.get::(0).unwrap(); assert_eq!(val, 100); } }, 1000, ); } /// Test register modification after row invalidation. /// This validates that modifying registers after take() is safe. #[test] fn shuttle_register_modification_after_invalidation() { shuttle::check_random( || { let mut state = create_test_state(10, 2); state.registers[0].set_int(1); state.result_row = Some(Row { values: &state.registers[0] as *const Register, count: 1, }); // Invalidate row (simulating what normal_step does) let _ = state.result_row.take(); // Now safe to modify registers state.registers[0].set_int(999); // Create new row pointing to modified registers state.result_row = Some(Row { values: &state.registers[0] as *const Register, count: 1, }); // New row should see new value if let Some(row) = &state.result_row { let val = row.get::(0).unwrap(); assert_eq!(val, 999); } }, 1000, ); } /// Test sequential send-receive pattern (simulating async task scheduling). /// ProgramState is moved between threads in a producer-consumer pattern. #[test] fn shuttle_sequential_thread_transfer() { shuttle::check_random( || { let mut state = create_test_state(10, 2); state.registers[0].set_int(0); // Thread 1: increment let h1 = thread::spawn(move || { if let Register::Value(Value::Numeric(Numeric::Integer(v))) = &state.registers[0] { state.registers[0].set_int(v + 1); } state }); let mut state = h1.join().unwrap(); // Thread 2: increment let h2 = thread::spawn(move || { if let Register::Value(Value::Numeric(Numeric::Integer(v))) = &state.registers[0] { state.registers[0].set_int(v + 1); } state }); let mut state = h2.join().unwrap(); // Thread 3: increment let h3 = thread::spawn(move || { if let Register::Value(Value::Numeric(Numeric::Integer(v))) = &state.registers[0] { state.registers[0].set_int(v + 1); } state }); let state = h3.join().unwrap(); // Final value should be 3 assert!(matches!( &state.registers[0], Register::Value(Value::Numeric(Numeric::Integer(3))) )); }, 1000, ); } /// Test that ProgramState can be wrapped in Arc for shared ownership. /// This is the typical pattern for concurrent database operations. #[test] fn shuttle_arc_wrapped_state() { shuttle::check_random( || { let mut state = create_test_state(10, 2); // Initialize with test data for i in 0..5 { state.registers[i].set_int(i as i64); } let state = Arc::new(state); let mut handles = vec![]; // Multiple threads reading registers through Arc for thread_id in 0u8..4 { let state_clone = Arc::clone(&state); let h = thread::spawn(move || { // Each thread reads all registers for i in 0..5 { if let Register::Value(Value::Numeric(Numeric::Integer(v))) = &state_clone.registers[i] { assert_eq!(*v, i as i64); } } thread_id }); handles.push(h); } for h in handles { h.join().unwrap(); } }, 1000, ); } /// Test Row::get_values iterator under concurrent access. #[test] fn shuttle_row_get_values_concurrent() { shuttle::check_random( || { let mut state = create_test_state(10, 2); state.registers[0].set_int(10); state.registers[1].set_int(20); state.registers[2].set_int(30); state.result_row = Some(Row { values: &state.registers[0] as *const Register, count: 3, }); let state = Arc::new(state); let state2 = Arc::clone(&state); let h1 = thread::spawn(move || { if let Some(row) = &state.result_row { let values: Vec<_> = row.get_values().collect(); assert_eq!(values.len(), 3); } }); let h2 = thread::spawn(move || { if let Some(row) = &state2.result_row { let mut sum = 0i64; for val in row.get_values() { if let Value::Numeric(Numeric::Integer(i)) = val { sum += i; } } assert_eq!(sum, 60); // 10 + 20 + 30 } }); h1.join().unwrap(); h2.join().unwrap(); }, 1000, ); } /// Stress test: Many threads reading from shared ProgramState. #[test] fn shuttle_stress_concurrent_reads() { shuttle::check_random( || { let mut state = create_test_state(20, 2); // Fill registers with identifiable data for i in 0..20 { state.registers[i].set_int(i as i64 * 100); } state.result_row = Some(Row { values: &state.registers[0] as *const Register, count: 20, }); let state = Arc::new(state); let mut handles = vec![]; for thread_id in 0..6u8 { let state_clone = Arc::clone(&state); let h = thread::spawn(move || { // Each thread reads different parts let start = (thread_id as usize * 3) % 20; if let Some(row) = &state_clone.result_row { for i in 0..3 { let idx = (start + i) % row.len(); let val = row.get::(idx).unwrap(); assert_eq!(val, idx as i64 * 100); } } thread_id }); handles.push(h); } for h in handles { h.join().unwrap(); } }, 1000, ); } } ================================================ FILE: core/vdbe/rowset.rs ================================================ //! RowSet data structure for efficient set operations on integer rowids. //! //! RowSet is optimized for batch-oriented insertions where sets of integers are inserted //! in distinct phases, with each phase containing no duplicates. Operations are optimized //! for two distinct use cases: //! //! 1. **TEST mode**: Check membership and insert values with batch-based consolidation. //! Values are inserted into a fresh list and consolidated into a BTreeSet when the batch //! number changes, enabling efficient membership tests. //! //! 2. **SMALLEST mode**: Extract values in sorted order. Once extraction begins, a sorted //! buffer is built and values are extracted one at a time. //! //! **Critical constraint**: TEST and SMALLEST operations are mutually exclusive. Once `test()` //! has been called, `smallest()` cannot be used. Once `smallest()` has been called, `test()` //! cannot be used. This matches SQLite's RowSet semantics. //! //! ## Batch Semantics //! //! Batches identify distinct phases of insertion: //! - `batch == 0`: First set (guaranteed not to contain values, so no test needed) //! - `batch > 0`: Intermediate sets (consolidation happens when batch changes) //! - `batch == -1`: Final set (no insertion needed, only testing) //! //! When `test()` is called with a different batch number than the current `i_batch`, all //! values in the fresh list are consolidated into the consolidated set. use branches::mark_unlikely; use crate::turso_assert; use std::collections::BTreeSet; /// The mode of usage for a RowSet. /// Test: the rowset will be used for set membership tests. /// Smallest: the rowset will be used to extract the smallest value in sorted order. #[derive(Debug)] pub enum RowSetMode { Test { /// Set of distinct rowids. set: BTreeSet, /// Batch number of the last test. batch_number: i32, }, Smallest { sorted_vec: Vec, }, Unset, } /// A set of integer rowids optimized for batch-oriented operations. #[derive(Debug)] pub struct RowSet { /// Fresh inserts since last consolidation fresh: Vec, /// The mode of usage for the RowSet. mode: RowSetMode, } impl Default for RowSet { fn default() -> Self { Self::new() } } impl RowSet { /// Creates a new empty RowSet. pub fn new() -> Self { Self { fresh: Vec::new(), mode: RowSetMode::Unset, } } /// Inserts a rowid into the set. /// /// Values are added to the fresh list and will be consolidated when `test()` is called /// with a different batch number. /// /// # Panics /// /// Panics if `smallest()` extraction has already started. pub fn insert(&mut self, rowid: i64) { turso_assert!( !matches!(self.mode, RowSetMode::Smallest { .. }), "cannot insert after smallest() has been used" ); self.fresh.push(rowid); } /// Tests if the rowid exists in the set, with batch-based consolidation. /// /// If `batch` differs from the current batch, consolidates fresh values into the /// consolidated set. Returns `true` if the rowid is found. /// /// # Panics /// /// Panics if `smallest()` extraction has already started, because rowsets have two /// mutually exclusive uses: set membership tests (test()) and in-order iteration (smallest()). pub fn test(&mut self, rowid: i64, batch: i32) -> bool { turso_assert!( !matches!(self.mode, RowSetMode::Smallest { .. }), "cannot call test() after smallest() has started" ); if matches!(self.mode, RowSetMode::Unset) { self.mode = RowSetMode::Test { set: BTreeSet::new(), batch_number: 0, }; } let RowSetMode::Test { set, batch_number } = &mut self.mode else { mark_unlikely(); unreachable!() }; // If a new batch has started, fold the fresh vector into the set. if batch != *batch_number { for v in self.fresh.drain(..) { set.insert(v); } *batch_number = batch; } // Note: If the batch number has not changed, we only check whether any previous batch inserted this value, // since the rowset implementation expects that any single batch does not insert any duplicates nor // test for duplicates wrt the current batch. set.contains(&rowid) } /// Extracts and returns the smallest rowid from the set. /// /// On the first call, builds a sorted buffer from all values (O(N log N)). /// Subsequent calls are O(1). Returns `None` if the set is empty. /// /// # Panics /// /// Panics if `test()` has been called on this RowSet, because rowsets have two /// mutually exclusive uses: set membership tests (test()) and in-order iteration (smallest()). pub fn smallest(&mut self) -> Option { turso_assert!( !matches!(self.mode, RowSetMode::Test { .. }), "cannot call smallest() after test() has been used" ); if matches!(self.mode, RowSetMode::Unset) { let mut v = Vec::with_capacity(self.fresh.len()); v.append(&mut self.fresh); v.sort_unstable(); v.dedup(); v.reverse(); self.mode = RowSetMode::Smallest { sorted_vec: v }; } let RowSetMode::Smallest { sorted_vec } = &mut self.mode else { mark_unlikely(); unreachable!() }; sorted_vec.pop() } /// Returns `true` if the RowSet contains no values. pub fn is_empty(&self) -> bool { if !self.fresh.is_empty() { return false; } match &self.mode { RowSetMode::Test { set, .. } => set.is_empty(), RowSetMode::Smallest { sorted_vec, .. } => sorted_vec.is_empty(), RowSetMode::Unset => true, } } } #[cfg(test)] mod tests { use super::*; use rand_chacha::{ rand_core::{RngCore, SeedableRng}, ChaCha8Rng, }; fn get_seed() -> u64 { std::env::var("SEED").map_or( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis(), |v| { v.parse() .expect("Failed to parse SEED environment variable as u64") }, ) as u64 } #[test] fn test_empty_rowset() { let rowset = RowSet::new(); assert!(rowset.is_empty()); } #[test] fn test_insert_and_test() { let mut rowset = RowSet::new(); rowset.insert(10); rowset.insert(20); rowset.insert(30); assert!(!rowset.test(10, 0)); assert!(!rowset.test(20, 0)); assert!(!rowset.test(30, 0)); assert!(rowset.test(10, 1)); assert!(rowset.test(20, 1)); assert!(rowset.test(30, 1)); assert!(!rowset.test(40, 1)); } #[test] fn test_batch_consolidation() { let mut rowset = RowSet::new(); // Insert values into batch 0 (first set) rowset.insert(10); rowset.insert(20); // Batch 0 doesn't test for membership (guaranteed not to contain values) assert!(!rowset.test(10, 0)); // Insert more values (still in fresh, not consolidated yet) rowset.insert(30); rowset.insert(40); // Test with batch 1: triggers consolidation of fresh values (10, 20, 30, 40) // All should be found after consolidation assert!(rowset.test(10, 1)); assert!(rowset.test(20, 1)); assert!(rowset.test(30, 1)); assert!(rowset.test(40, 1)); assert!(!rowset.test(50, 1)); // Insert value 50 (goes to fresh, not consolidated yet) rowset.insert(50); // Test with same batch (1): no new consolidation, so 50 not found yet assert!(rowset.test(10, 1)); assert!(!rowset.test(50, 1)); // Test with new batch (2): triggers consolidation, now 50 is found assert!(rowset.test(50, 2)); } #[test] fn test_smallest_extraction() { let mut rowset = RowSet::new(); rowset.insert(30); rowset.insert(10); rowset.insert(50); rowset.insert(20); rowset.insert(40); assert_eq!(rowset.smallest(), Some(10)); assert_eq!(rowset.smallest(), Some(20)); assert_eq!(rowset.smallest(), Some(30)); assert_eq!(rowset.smallest(), Some(40)); assert_eq!(rowset.smallest(), Some(50)); assert_eq!(rowset.smallest(), None); assert!(rowset.is_empty()); } #[test] fn test_smallest_with_duplicates() { let mut rowset = RowSet::new(); rowset.insert(10); rowset.insert(20); rowset.insert(10); rowset.insert(30); rowset.insert(20); assert_eq!(rowset.smallest(), Some(10)); assert_eq!(rowset.smallest(), Some(20)); assert_eq!(rowset.smallest(), Some(30)); assert_eq!(rowset.smallest(), None); } #[test] fn test_insert_after_smallest_panics() { let mut rowset = RowSet::new(); rowset.insert(10); rowset.smallest(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { rowset.insert(20); })); assert!(result.is_err()); } #[test] fn test_test_after_smallest_panics() { let mut rowset = RowSet::new(); rowset.insert(10); rowset.smallest(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { rowset.test(10, 1); })); assert!(result.is_err()); } #[test] fn test_smallest_after_test_panics() { let mut rowset = RowSet::new(); rowset.insert(10); rowset.test(10, 1); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { rowset.smallest(); })); assert!(result.is_err()); } #[test] fn test_batch_zero_allows_smallest() { let mut rowset = RowSet::new(); rowset.insert(10); rowset.insert(20); rowset.insert(30); rowset.insert(5); rowset.insert(15); assert_eq!(rowset.smallest(), Some(5)); assert_eq!(rowset.smallest(), Some(10)); assert_eq!(rowset.smallest(), Some(15)); assert_eq!(rowset.smallest(), Some(20)); assert_eq!(rowset.smallest(), Some(30)); assert_eq!(rowset.smallest(), None); } #[test] fn test_empty_smallest() { let mut rowset = RowSet::new(); assert_eq!(rowset.smallest(), None); assert!(rowset.is_empty()); } #[test] fn test_batch_zero_semantics() { let mut rowset = RowSet::new(); rowset.insert(10); rowset.insert(20); assert!(!rowset.test(10, 0)); assert!(!rowset.test(20, 0)); assert!(rowset.test(10, 1)); assert!(rowset.test(20, 1)); } #[test] fn test_batch_final_semantics() { let mut rowset = RowSet::new(); // Insert and consolidate with batch 1 rowset.insert(10); assert!(rowset.test(10, 1)); // Insert more (goes to fresh) rowset.insert(20); // Test with batch -1 (final set): consolidates fresh values // Both 10 (already consolidated) and 20 (now consolidated) should be found assert!(rowset.test(10, -1)); assert!(rowset.test(20, -1)); // Test non-existent value: should not insert (batch == -1 means no insertion) // Verify it's still not found on second test assert!(!rowset.test(30, -1)); assert!(!rowset.test(30, -1)); } #[test] fn test_negative_values() { let mut rowset = RowSet::new(); rowset.insert(-10); rowset.insert(-5); rowset.insert(0); rowset.insert(5); rowset.insert(10); assert!(rowset.test(-10, 1)); assert!(rowset.test(-5, 1)); assert!(rowset.test(0, 1)); assert!(rowset.test(5, 1)); assert!(rowset.test(10, 1)); assert!(rowset.test(-10, 2)); assert!(rowset.test(-5, 2)); assert!(rowset.test(0, 2)); assert!(rowset.test(5, 2)); assert!(rowset.test(10, 2)); } #[test] fn test_large_values() { let mut rowset = RowSet::new(); let large1 = i64::MAX; let large2 = i64::MAX - 1; let large3 = i64::MIN; let large4 = i64::MIN + 1; rowset.insert(large1); rowset.insert(large2); rowset.insert(large3); rowset.insert(large4); assert!(rowset.test(large1, 1)); assert!(rowset.test(large2, 1)); assert!(rowset.test(large3, 1)); assert!(rowset.test(large4, 1)); assert!(rowset.test(large1, 2)); assert!(rowset.test(large2, 2)); assert!(rowset.test(large3, 2)); assert!(rowset.test(large4, 2)); } #[test] fn fuzz_basic_operations() { // Fuzz test for smallest() extraction: insert random values and verify // they are extracted in sorted order without duplicates. let seed = get_seed(); let mut rng = ChaCha8Rng::seed_from_u64(seed); let attempts = 10; for _ in 0..attempts { let mut rowset = RowSet::new(); let mut inserted = std::collections::BTreeSet::new(); // Insert random values (may include duplicates) let num_inserts = 100 + (rng.next_u64() % 900) as usize; for _ in 0..num_inserts { let value = rng.next_u64() as i64; rowset.insert(value); inserted.insert(value); } // Extract all values using smallest() let mut extracted = Vec::new(); while let Some(value) = rowset.smallest() { extracted.push(value); } // Verify all unique values were extracted exactly once assert_eq!(extracted.len(), inserted.len()); // Verify they're in sorted order let mut sorted_inserted: Vec = inserted.iter().copied().collect(); sorted_inserted.sort_unstable(); assert_eq!(extracted, sorted_inserted); } } #[test] fn fuzz_batch_operations() { // Fuzz test for batch-based consolidation: insert values in distinct batches // and verify they can be found after consolidation. let seed = get_seed(); let mut rng = ChaCha8Rng::seed_from_u64(seed); let attempts = 10; for _ in 0..attempts { let mut rowset = RowSet::new(); let mut batches: Vec<(i32, Vec)> = Vec::new(); // Create multiple batches: batch 0 (first), intermediate batches (>0), and batch -1 (final) let num_batches = 5 + (rng.next_u64() % 10) as usize; for batch_idx in 0..num_batches { let batch = if batch_idx == 0 { 0 } else if batch_idx == num_batches - 1 { -1 } else { batch_idx as i32 }; // Insert values for this batch let mut batch_values = Vec::new(); let num_values = 10 + (rng.next_u64() % 90) as usize; for _ in 0..num_values { let value = rng.next_u64() as i64; rowset.insert(value); batch_values.push(value); } batches.push((batch, batch_values)); } // Verify all values can be found when testing with their batch for (batch, values) in &batches { for &value in values { if *batch == 0 { // Batch 0: guaranteed not to contain values assert!(!rowset.test(value, *batch)); } else { // Other batches: should find values after consolidation assert!( rowset.test(value, *batch), "Value {value} should be found in batch {batch}", ); } } } } } #[test] fn fuzz_mixed_operations() { // Fuzz test mixing insertions and tests: randomly insert values and test them // with incrementing batch numbers to verify consolidation works correctly. let seed = get_seed(); let mut rng = ChaCha8Rng::seed_from_u64(seed); let attempts = 3; for _ in 0..attempts { let mut rowset = RowSet::new(); let mut all_values = std::collections::BTreeSet::new(); let mut next_batch = 1; let num_ops = 20 + (rng.next_u64() % 30) as usize; // Randomly interleave insertions and tests for _ in 0..num_ops { let op = rng.next_u64() % 2; match op { 0 => { // Insert a random value let value = rng.next_u64() as i64; rowset.insert(value); all_values.insert(value); } _ => { // Test a previously inserted value with a new batch number // This triggers consolidation of all fresh values if !all_values.is_empty() { let values_vec: Vec = all_values.iter().copied().collect(); let idx = (rng.next_u64() % values_vec.len() as u64) as usize; let value = values_vec[idx]; let found = rowset.test(value, next_batch); assert!(found, "Value {value} should be found in batch {next_batch}",); next_batch += 1; } } } } // Verify all inserted values can be found after final consolidation if !all_values.is_empty() { let final_batch = next_batch; for &value in &all_values { assert!( rowset.test(value, final_batch), "Value {value} should be found in batch {final_batch}", ); } } } } #[test] fn fuzz_long() { // Long-running fuzz test: insert values in batches and verify batch consolidation. // This tests the core RowSet behavior where values are inserted in distinct phases // and consolidated when the batch number changes. let seed = get_seed(); let mut rng = ChaCha8Rng::seed_from_u64(seed); println!("Fuzz seed: {seed}"); let attempts = 2; for attempt in 0..attempts { let mut rowset = RowSet::new(); let mut reference = std::collections::BTreeSet::new(); let mut batches: Vec<(i32, Vec)> = Vec::new(); // Generate random number of batches and total inserts let num_batches = 10 + (rng.next_u64() % 40) as usize; let total_inserts = 1000 + (rng.next_u64() % 9000) as usize; let inserts_per_batch = (total_inserts / num_batches).max(1); // Insert values in batches for batch_idx in 0..num_batches { // Determine batch number: 0 for first, -1 for last, sequential for others let batch = if batch_idx == 0 { 0 } else if batch_idx == num_batches - 1 { -1 } else { batch_idx as i32 }; // Calculate how many values to insert in this batch // (distribute total_inserts across batches with some randomness) let mut batch_values = Vec::new(); let already_inserted = batches.iter().map(|(_, v)| v.len()).sum::(); let batch_inserts = if batch_idx == num_batches - 1 { // Last batch gets remaining values total_inserts.saturating_sub(already_inserted) } else { // Other batches get inserts_per_batch plus some random variation let remaining = total_inserts.saturating_sub(already_inserted); let max_for_this_batch = remaining.min(inserts_per_batch * 2); inserts_per_batch + (rng.next_u64() % (max_for_this_batch.saturating_sub(inserts_per_batch) + 1) as u64) as usize }; // Insert values for this batch for _ in 0..batch_inserts { let value = rng.next_u64() as i64; rowset.insert(value); reference.insert(value); batch_values.push(value); } // For batches > 0, test some values to verify consolidation works if batch > 0 { let test_count = (batch_values.len() / 10).max(1); for _ in 0..test_count { let idx = (rng.next_u64() % batch_values.len() as u64) as usize; let value = batch_values[idx]; let found = rowset.test(value, batch); assert!( found, "Attempt {attempt}, batch {batch}, value {value} should be found", ); } } batches.push((batch, batch_values)); } // Final verification: ensure all values can be found after consolidation if !reference.is_empty() && !batches.is_empty() { let last_batch = batches.last().unwrap().0; // Use a new batch number to force final consolidation let final_batch = if last_batch == -1 { -1 } else { last_batch + 1 }; for &value in &reference { assert!( rowset.test(value, final_batch), "Attempt {attempt}, value {value} should be found in batch {final_batch}", ); } } } } } ================================================ FILE: core/vdbe/sorter.rs ================================================ use crate::{turso_assert, turso_assert_eq}; use branches::mark_unlikely; use turso_parser::ast::SortOrder; use crate::sync::RwLock; use crate::sync::{atomic, Arc}; use bumpalo::Bump; use std::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd, Reverse}; use std::collections::BinaryHeap; use std::ptr::NonNull; use std::rc::Rc; use crate::io::TempFile; use crate::types::{IOCompletions, ValueIterator}; use crate::{ error::LimboError, io::{Buffer, Completion, CompletionGroup, File, IO}, storage::sqlite3_ondisk::{read_varint, varint_len, write_varint}, translate::collate::CollationSeq, types::{IOResult, ImmutableRecord, KeyInfo, ValueRef}, Result, }; use crate::{io_yield_one, return_if_io, CompletionError}; /// A custom comparison function for sorting custom type columns. /// Takes two value references and returns an Ordering. /// Used when a custom type defines a `<` operator for correct sort behavior. pub type SortComparator = Arc Ordering + Send + Sync>; #[derive(Debug, Clone, Copy)] enum SortState { Start, Flush, InitHeap, Next, } #[derive(Debug, Clone, Copy)] enum InsertState { Start, Insert, } #[derive(Debug, Clone, Copy)] enum InitChunkHeapState { Start, PushChunk, } pub struct Sorter { /// Arena allocator for records - provides fast bump allocation and bulk deallocation. /// All record data (payload bytes, key_values) is stored here for in-memory sorting. arena: Bump, /// Pointers to records allocated in the arena. Sorting moves only 8-byte pointers, /// which prevents high memmove costs during sorting. /// SAFETY: These pointers are valid as long as the arena hasn't been reset. records: Vec>, /// The current record. current: Option, /// The number of values in the key. key_len: usize, /// The key info. pub index_key_info: Rc>, /// Per-column custom comparators for custom type ordering. /// When present, used instead of standard ValueRef comparison for that column. comparators: Rc>>, /// Sorted chunks stored on disk. chunks: Vec, /// The heap of records consumed from the chunks and their corresponding chunk index. chunk_heap: BinaryHeap<(Reverse>, usize)>, /// The maximum size of the in-memory buffer in bytes before the records are flushed to a chunk file. max_buffer_size: usize, /// The current size of the in-memory buffer in bytes. current_buffer_size: usize, /// The minimum size of a chunk read buffer in bytes. The actual buffer size can be larger if the largest /// record in the buffer is larger than this value. min_chunk_read_buffer_size: usize, /// The maximum record payload size in the in-memory buffer. max_payload_size_in_buffer: usize, /// The IO object. io: Arc, /// The temporary file for chunks. temp_file: Option, /// Offset where the next chunk will be placed in the `temp_file` next_chunk_offset: usize, /// State machine for [Sorter::sort] sort_state: SortState, /// State machine for [Sorter::insert] insert_state: InsertState, /// State machine for [Sorter::init_chunk_heap] init_chunk_heap_state: InitChunkHeapState, /// Pending IO completion along with the chunk index that needs to be retried after IO completes. pending_completion: Option<(Completion, usize)>, /// Temp storage mode (memory vs file) for spilled data temp_store: crate::TempStore, } impl Sorter { pub fn new( order: &[SortOrder], collations: Vec, comparators: Vec>, max_buffer_size_bytes: usize, min_chunk_read_buffer_size_bytes: usize, io: Arc, temp_store: crate::TempStore, ) -> Self { turso_assert_eq!(order.len(), collations.len()); Self { arena: Bump::new(), records: Vec::new(), current: None, key_len: order.len(), index_key_info: Rc::new( order .iter() .zip(collations) .map(|(order, collation)| KeyInfo { sort_order: *order, collation, }) .collect(), ), comparators: Rc::new(comparators), chunks: Vec::new(), chunk_heap: BinaryHeap::new(), max_buffer_size: max_buffer_size_bytes, current_buffer_size: 0, min_chunk_read_buffer_size: min_chunk_read_buffer_size_bytes, max_payload_size_in_buffer: 0, io, temp_file: None, next_chunk_offset: 0, sort_state: SortState::Start, insert_state: InsertState::Start, init_chunk_heap_state: InitChunkHeapState::Start, pending_completion: None, temp_store, } } pub fn is_empty(&self) -> bool { self.records.is_empty() && self.chunks.is_empty() } pub fn has_more(&self) -> bool { self.current.is_some() } // We do the sorting here since this is what is called by the SorterSort instruction pub fn sort(&mut self) -> Result> { loop { match self.sort_state { SortState::Start => { if self.chunks.is_empty() { // Sort ascending then reverse - we pop from end so this gives ascending output. // NOTE: We can't just sort descending because stable sort preserves insertion // order for equal elements, and descending sort doesn't reverse equal elements. // SAFETY: All pointers in records are valid (arena hasn't been reset). self.records .sort_by(|a, b| unsafe { a.as_ref().cmp(b.as_ref()) }); self.records.reverse(); self.sort_state = SortState::Next; } else { self.sort_state = SortState::Flush; } } SortState::Flush => { self.sort_state = SortState::InitHeap; if let Some(c) = self.flush()? { io_yield_one!(c); } } SortState::InitHeap => { // Check for write errors before proceeding if self.chunks.iter().any(|chunk| { matches!(*chunk.io_state.read(), SortedChunkIOState::WriteError) }) { return Err(CompletionError::IOError( std::io::ErrorKind::WriteZero, "sorter write", ) .into()); } turso_assert!( !self.chunks.iter().any(|chunk| { matches!(*chunk.io_state.read(), SortedChunkIOState::WaitingForWrite) }), "chunks should been written" ); return_if_io!(self.init_chunk_heap()); self.sort_state = SortState::Next; } SortState::Next => { return_if_io!(self.next()); self.sort_state = SortState::Start; return Ok(IOResult::Done(())); } } } } #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Result> { if self.chunks.is_empty() { match self.records.pop() { Some(ptr) => { // SAFETY: ptr is valid - arena hasn't been reset yet. let arena_record = unsafe { ptr.as_ref() }; let payload = arena_record.payload(); match &mut self.current { Some(record) => { record.invalidate(); record.start_serialization(payload); } None => { self.current = Some(arena_record.to_immutable_record()); } } if self.records.is_empty() { self.arena.reset(); } } None => self.current = None, } } else { // Serve from sorted chunk files match return_if_io!(self.next_from_chunk_heap()) { Some(boxed_record) => { if let Some(ref error) = boxed_record.deserialization_error { return Err(error.clone()); } let payload = boxed_record.record.get_payload(); match &mut self.current { Some(record) => { record.invalidate(); record.start_serialization(payload); } None => { self.current = Some(boxed_record.record); } } } None => self.current = None, } } Ok(IOResult::Done(())) } pub fn record(&self) -> Option<&ImmutableRecord> { self.current.as_ref() } pub fn insert(&mut self, record: &ImmutableRecord) -> Result> { let payload_size = record.get_payload().len(); loop { match self.insert_state { InsertState::Start => { self.insert_state = InsertState::Insert; if self.current_buffer_size + payload_size > self.max_buffer_size { if let Some(c) = self.flush()? { if !c.succeeded() { io_yield_one!(c); } } // Check for write errors immediately after flush completes if self.chunks.iter().any(|chunk| { matches!(*chunk.io_state.read(), SortedChunkIOState::WriteError) }) { return Err(CompletionError::IOError( std::io::ErrorKind::WriteZero, "sorter write", ) .into()); } } } InsertState::Insert => { let sortable_record = ArenaSortableRecord::new( &self.arena, record, self.key_len, &self.index_key_info, &self.comparators, )?; let record_ref = self.arena.alloc(sortable_record); // SAFETY: arena.alloc returns a valid, aligned, non-null pointer self.records.push(NonNull::from(record_ref)); self.current_buffer_size += payload_size; self.max_payload_size_in_buffer = self.max_payload_size_in_buffer.max(payload_size); self.insert_state = InsertState::Start; return Ok(IOResult::Done(())); } } } } fn init_chunk_heap(&mut self) -> Result> { match self.init_chunk_heap_state { InitChunkHeapState::Start => { let mut group = CompletionGroup::new(|_| {}); for chunk in self.chunks.iter_mut() { match chunk.read() { Err(e) => { tracing::error!("Failed to read chunk: {e}"); group.cancel(); self.io.drain()?; return Err(e); } Ok(Some(c)) => group.add(&c), Ok(None) => {} }; } self.init_chunk_heap_state = InitChunkHeapState::PushChunk; let completion = group.build(); io_yield_one!(completion); } InitChunkHeapState::PushChunk => { // Make sure all chunks read at least one record into their buffer. turso_assert!( !self.chunks.iter().any(|chunk| matches!( *chunk.io_state.read(), SortedChunkIOState::WaitingForRead )), "chunks should have been read" ); self.chunk_heap.reserve(self.chunks.len()); // TODO: blocking will be unnecessary here with IO completions let mut group = CompletionGroup::new(|_| {}); for chunk_idx in 0..self.chunks.len() { if let Some(c) = self.push_to_chunk_heap(chunk_idx)? { group.add(&c); }; } self.init_chunk_heap_state = InitChunkHeapState::Start; let completion = group.build(); if completion.finished() { Ok(IOResult::Done(())) } else { io_yield_one!(completion); } } } } /// Returns the next record from the chunk heap in sorted order. /// /// The heap contains at most one record per chunk. When we pop a record, we try to refill /// from that chunk. If IO is needed, we store it in `pending_completion` and wait for it /// on the next call before popping again - this ensures all non-exhausted chunks have /// a record in the heap before we decide which is smallest. fn next_from_chunk_heap(&mut self) -> Result>>> { // If there is a pending IO, we must wait for it before popping from the heap, // otherwise we might return records out of order. while let Some((completion, chunk_idx)) = self.pending_completion.take() { if !completion.succeeded() { // IO not complete - put it back and yield self.pending_completion = Some((completion.clone(), chunk_idx)); return Ok(IOResult::IO(IOCompletions::Single(completion))); } // IO completed - push result to heap and retry if let Some(c) = self.push_to_chunk_heap(chunk_idx)? { self.pending_completion = Some((c, chunk_idx)); } } // No pending IO - safe to pop from heap if let Some((next_record, chunk_idx)) = self.chunk_heap.pop() { if let Some(c) = self.push_to_chunk_heap(chunk_idx)? { self.pending_completion = Some((c, chunk_idx)); } return Ok(IOResult::Done(Some(next_record.0))); } // Heap empty and no pending IO - sorter exhausted Ok(IOResult::Done(None)) } fn push_to_chunk_heap(&mut self, chunk_idx: usize) -> Result> { let chunk = &mut self.chunks[chunk_idx]; match chunk.next()? { ChunkNextResult::Done(Some(record)) => { self.chunk_heap.push(( Reverse(Box::new(BoxedSortableRecord::new( record, self.key_len, self.index_key_info.clone(), self.comparators.clone(), )?)), chunk_idx, )); Ok(None) } ChunkNextResult::Done(None) => Ok(None), ChunkNextResult::IO(io) => Ok(Some(io)), } } fn flush(&mut self) -> Result> { if self.records.is_empty() { // Dummy completion to not complicate logic handling return Ok(None); } // SAFETY: All pointers are valid (arena not reset). self.records .sort_by(|a, b| unsafe { a.as_ref().cmp(b.as_ref()) }); let chunk_file = match &self.temp_file { Some(temp_file) => temp_file.file.clone(), None => { let temp_file = TempFile::with_temp_store(&self.io, self.temp_store)?; let chunk_file = temp_file.file.clone(); self.temp_file = Some(temp_file); chunk_file } }; // Make sure the chunk buffer size can fit the largest record and its size varint. let chunk_buffer_size = self .min_chunk_read_buffer_size .max(self.max_payload_size_in_buffer + 9); let mut chunk_size = 0; // Pre-compute varint lengths for record sizes to determine the total buffer size. // SAFETY: All pointers are valid because they are allocated in the arena, // and the arena hasn't been reset. let mut record_size_lengths = Vec::with_capacity(self.records.len()); for ptr in self.records.iter() { let record_size = unsafe { ptr.as_ref().payload().len() }; let size_len = varint_len(record_size as u64); record_size_lengths.push(size_len); chunk_size += size_len + record_size; } let mut chunk = SortedChunk::new(chunk_file, self.next_chunk_offset, chunk_buffer_size); let c = chunk.write(&self.records, record_size_lengths, chunk_size)?; self.chunks.push(chunk); self.records.clear(); self.arena.reset(); self.current_buffer_size = 0; self.max_payload_size_in_buffer = 0; // increase offset start for next chunk self.next_chunk_offset += chunk_size; Ok(Some(c)) } } #[derive(Debug, Clone, Copy)] enum NextState { Start, Finish, } /// A sorted chunk represents a portion of sorted data that has been written to disk /// during external merge sort. When the in-memory buffer fills up, records are sorted /// and flushed to a chunk file. During the merge phase, chunks are read back and merged /// using a heap to produce the final sorted output. /// /// # Buffer management /// /// The chunk uses a fixed-size read buffer (`buffer`) to read data from disk. The buffer /// has two relevant sizes: /// - `buffer.len()` (capacity): The total allocated size of the buffer (fixed at creation) /// - `buffer_len`: The amount of valid data currently in the buffer (0 to capacity) /// /// The difference `buffer.len() - buffer_len` is the free space available for reading /// more data from disk. /// /// # Reading progress /// /// - `chunk_size`: Total bytes of this chunk on disk (set when chunk is written) /// - `total_bytes_read`: Cumulative bytes read from disk so far (0 to chunk_size) /// /// The difference `chunk_size - total_bytes_read` is the remaining data on disk that /// hasn't been read yet. When `total_bytes_read == chunk_size`, we've read all data. /// /// # Record parsing /// /// Data flows: disk -> buffer -> records -> caller /// /// 1. `read()` fills `buffer` from disk, updates `total_bytes_read` /// 2. `next()` parses records from `buffer` into `records` vec, updates `buffer_len` /// 3. `next()` returns records one at a time from `records` /// /// Incomplete records at the end of the buffer are kept (buffer compacted) until /// more data is read to complete them. struct SortedChunk { /// The file containing the chunk data. file: Arc, /// Byte offset where this chunk starts in the file. start_offset: u64, /// Total size of this chunk in bytes (set during write, used to detect EOF during read). chunk_size: usize, /// Fixed-size buffer for reading data from disk. The capacity (`buffer.len()`) is /// constant; use `buffer_len` for the amount of valid data. buffer: Arc>>, /// Amount of valid (unparsed) data in `buffer`, from index 0 to buffer_len. /// This is separate from buffer.len() because we reuse the same allocation. buffer_len: Arc, /// Records parsed from the buffer, waiting to be returned by `next()`. /// Stored in reverse order so we can efficiently pop from the end. records: Vec, /// Current async IO state (None, WaitingForRead, ReadComplete, ReadEOF, etc). io_state: Arc>, /// Cumulative bytes read from disk. When this equals `chunk_size`, we've read everything. total_bytes_read: Arc, /// State machine for the `next()` method. next_state: NextState, } enum ChunkNextResult { Done(Option), IO(Completion), } impl SortedChunk { fn new(file: Arc, start_offset: usize, buffer_size: usize) -> Self { Self { file, start_offset: start_offset as u64, chunk_size: 0, buffer: Arc::new(RwLock::new(vec![0; buffer_size])), buffer_len: Arc::new(atomic::AtomicUsize::new(0)), records: Vec::new(), io_state: Arc::new(RwLock::new(SortedChunkIOState::None)), total_bytes_read: Arc::new(atomic::AtomicUsize::new(0)), next_state: NextState::Start, } } fn buffer_len(&self) -> usize { self.buffer_len.load(atomic::Ordering::SeqCst) } fn set_buffer_len(&self, len: usize) { self.buffer_len.store(len, atomic::Ordering::SeqCst); } /// Returns the next record from this chunk, or None if exhausted. /// /// May return `ChunkNextResult::IO` if async IO is needed, in which case /// the caller should wait for the completion and call `next()` again. /// /// Internally manages a two-phase state machine: /// - `Start`: Parse records from buffer, issue prefetch read if needed /// - `Finish`: Return the next parsed record fn next(&mut self) -> Result { loop { match self.next_state { NextState::Start => { let mut buffer_len = self.buffer_len(); if self.records.is_empty() && buffer_len == 0 { return Ok(ChunkNextResult::Done(None)); } if self.records.is_empty() { let mut buffer_ref = self.buffer.write(); let buffer = buffer_ref.as_mut_slice(); let mut buffer_offset = 0; while buffer_offset < buffer_len { // Extract records from the buffer until we run out of the buffer or we hit an incomplete record. let (record_size, bytes_read) = match read_varint(&buffer[buffer_offset..buffer_len]) { Ok((record_size, bytes_read)) => { (record_size as usize, bytes_read) } Err(LimboError::Corrupt(_)) if *self.io_state.read() != SortedChunkIOState::ReadEOF => { // Failed to decode a partial varint. break; } Err(e) => { return Err(e); } }; if record_size > buffer_len - (buffer_offset + bytes_read) { if *self.io_state.read() == SortedChunkIOState::ReadEOF { crate::bail_corrupt_error!("Incomplete record"); } break; } buffer_offset += bytes_read; let mut record = ImmutableRecord::new(record_size); record.start_serialization( &buffer[buffer_offset..buffer_offset + record_size], ); buffer_offset += record_size; self.records.push(record); } if buffer_offset < buffer_len { buffer.copy_within(buffer_offset..buffer_len, 0); buffer_len -= buffer_offset; } else { buffer_len = 0; } self.set_buffer_len(buffer_len); self.records.reverse(); } self.next_state = NextState::Finish; // Prefetch: if down to last record, try to read more data into the buffer. if self.records.len() == 1 && *self.io_state.read() != SortedChunkIOState::ReadEOF { if let Some(c) = self.read()? { if !c.succeeded() { return Ok(ChunkNextResult::IO(c)); } } } } NextState::Finish => { self.next_state = NextState::Start; return Ok(ChunkNextResult::Done(self.records.pop())); } } } } /// Issues an async read to fill the buffer with more data from the chunk file. /// /// Reads up to `min(free_buffer_space, remaining_chunk_bytes)` bytes. Returns `None` /// if there's no room in the buffer or no data left to read (no IO issued). /// /// On completion, appends data to `buffer` and updates `buffer_len` and `total_bytes_read`. fn read(&mut self) -> Result> { let free_buffer_space = self.buffer.read().len() - self.buffer_len(); let remaining_chunk_bytes = self.chunk_size - self.total_bytes_read.load(atomic::Ordering::SeqCst); let read_buffer_size = free_buffer_space.min(remaining_chunk_bytes); // If there's no room in the buffer or nothing left to read, skip the read. if read_buffer_size == 0 { if remaining_chunk_bytes == 0 { // No more data in the chunk file. *self.io_state.write() = SortedChunkIOState::ReadEOF; } return Ok(None); } *self.io_state.write() = SortedChunkIOState::WaitingForRead; let read_buffer = Buffer::new_temporary(read_buffer_size); let read_buffer_ref = Arc::new(read_buffer); let chunk_io_state_copy = self.io_state.clone(); let stored_buffer_copy = self.buffer.clone(); let stored_buffer_len_copy = self.buffer_len.clone(); let total_bytes_read_copy = self.total_bytes_read.clone(); let read_complete = Box::new(move |res: Result<(Arc, i32), CompletionError>| { let Ok((buf, bytes_read)) = res else { return None; }; let read_buf = buf.as_slice(); let bytes_read = bytes_read as usize; if bytes_read == 0 { *chunk_io_state_copy.write() = SortedChunkIOState::ReadEOF; return None; } *chunk_io_state_copy.write() = SortedChunkIOState::ReadComplete; let mut stored_buf_ref = stored_buffer_copy.write(); let stored_buf = stored_buf_ref.as_mut_slice(); let mut stored_buf_len = stored_buffer_len_copy.load(atomic::Ordering::SeqCst); stored_buf[stored_buf_len..stored_buf_len + bytes_read] .copy_from_slice(&read_buf[..bytes_read]); stored_buf_len += bytes_read; stored_buffer_len_copy.store(stored_buf_len, atomic::Ordering::SeqCst); total_bytes_read_copy.fetch_add(bytes_read, atomic::Ordering::SeqCst); None }); let c = Completion::new_read(read_buffer_ref, read_complete); let c = self.file.pread( self.start_offset + self.total_bytes_read.load(atomic::Ordering::SeqCst) as u64, c, )?; Ok(Some(c)) } fn write( &mut self, records: &[NonNull], record_size_lengths: Vec, chunk_size: usize, ) -> Result { turso_assert_eq!(*self.io_state.read(), SortedChunkIOState::None); *self.io_state.write() = SortedChunkIOState::WaitingForWrite; self.chunk_size = chunk_size; let buffer = Buffer::new_temporary(self.chunk_size); let mut buf_pos = 0; let buf = buffer.as_mut_slice(); for (ptr, size_len) in records.iter().zip(record_size_lengths) { // SAFETY: All pointers are valid (arena not reset). let payload = unsafe { ptr.as_ref().payload() }; // Write the record size varint. write_varint(&mut buf[buf_pos..buf_pos + size_len], payload.len() as u64); buf_pos += size_len; // Write the record payload. buf[buf_pos..buf_pos + payload.len()].copy_from_slice(payload); buf_pos += payload.len(); } let buffer_ref = Arc::new(buffer); let buffer_ref_copy = buffer_ref.clone(); let chunk_io_state_copy = self.io_state.clone(); let write_complete = Box::new(move |res: Result| { let Ok(bytes_written) = res else { *chunk_io_state_copy.write() = SortedChunkIOState::WriteError; return; }; let buf_len = buffer_ref_copy.len(); if bytes_written < buf_len as i32 { tracing::error!("wrote({bytes_written}) less than expected({buf_len})"); *chunk_io_state_copy.write() = SortedChunkIOState::WriteError; } else { *chunk_io_state_copy.write() = SortedChunkIOState::WriteComplete; } }); let c = Completion::new_write(write_complete); let c = self.file.pwrite(self.start_offset, buffer_ref, c)?; Ok(c) } } /// Record for in-memory sorting. All data lives in the arena, so no Drop is needed. struct ArenaSortableRecord { /// Payload bytes in arena. Using NonNull avoids lifetime issues with /// self-referential struct (key_values points into this payload). payload: NonNull<[u8]>, /// Pre-computed key values in arena. Points into `payload`. key_values: NonNull<[ValueRef<'static>]>, /// Shared KeyInfo owned by Sorter. Avoids Rc refcount overhead that would /// leak when arena.reset() skips Drop. index_key_info: NonNull<[KeyInfo]>, /// Shared comparators owned by Sorter. Same safety model as index_key_info. comparators: NonNull<[Option]>, } impl ArenaSortableRecord { fn new( arena: &Bump, record: &ImmutableRecord, key_len: usize, index_key_info: &[KeyInfo], comparators: &[Option], ) -> Result { let payload = arena.alloc_slice_copy(record.get_payload()); let mut payload_iter = ValueIterator::new(payload)?; let mut key_values = bumpalo::collections::Vec::with_capacity_in(payload_iter.clone().count(), arena); for _ in 0..key_len { let value = match payload_iter.next() { Some(Ok(v)) => v, Some(Err(e)) => return Err(e), None => crate::bail_corrupt_error!("Not enough columns in record"), }; // SAFETY: value borrows from payload which is in the arena and outlives this struct. let value: ValueRef<'static> = unsafe { std::mem::transmute(value) }; key_values.push(value); } Ok(Self { payload: NonNull::from(payload), key_values: NonNull::from(key_values.into_bump_slice()), index_key_info: NonNull::from(index_key_info), comparators: NonNull::from(comparators), }) } #[inline] fn key_values(&self) -> &[ValueRef<'static>] { // SAFETY: valid from construction, arena not reset unsafe { self.key_values.as_ref() } } #[inline] fn payload(&self) -> &[u8] { // SAFETY: valid from construction, arena not reset unsafe { self.payload.as_ref() } } /// Create an ImmutableRecord by copying payload bytes out of the arena. fn to_immutable_record(&self) -> ImmutableRecord { let payload = self.payload(); let mut record = ImmutableRecord::new(payload.len()); record.start_serialization(payload); record } } impl Ord for ArenaSortableRecord { #[inline] fn cmp(&self, other: &Self) -> Ordering { let self_values = self.key_values(); let other_values = other.key_values(); // SAFETY: index_key_info and comparators point to Sorter-owned data that outlives all records. let index_key_info = unsafe { self.index_key_info.as_ref() }; let comparators = unsafe { self.comparators.as_ref() }; for (i, ((&self_val, &other_val), key_info)) in self_values .iter() .zip(other_values.iter()) .zip(index_key_info.iter()) .enumerate() { let cmp = if let Some(Some(comparator)) = comparators.get(i) { comparator(&self_val, &other_val) } else { match (self_val, other_val) { (ValueRef::Text(left), ValueRef::Text(right)) => { key_info.collation.compare_strings(&left, &right) } _ => self_val.partial_cmp(&other_val).unwrap_or(Ordering::Equal), } }; if cmp != Ordering::Equal { return match key_info.sort_order { SortOrder::Asc => cmp, SortOrder::Desc => cmp.reverse(), }; } } Ordering::Equal } } impl PartialOrd for ArenaSortableRecord { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl PartialEq for ArenaSortableRecord { fn eq(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Equal } } impl Eq for ArenaSortableRecord {} /// Heap-allocated record for external merge sort. Used when records are read /// back from chunk files. Normal Drop semantics apply. struct BoxedSortableRecord { record: ImmutableRecord, key_values: Vec>, index_key_info: Rc>, comparators: Rc>>, deserialization_error: Option, } impl BoxedSortableRecord { fn new( record: ImmutableRecord, key_len: usize, index_key_info: Rc>, comparators: Rc>>, ) -> Result { let mut value_iterator = record.iter()?; let mut key_values = Vec::with_capacity(key_len); let mut deserialization_error = None; for _ in 0..key_len { match value_iterator.next() { Some(Ok(value)) => { // SAFETY: value points into record which lives as long as this struct let value: ValueRef<'static> = unsafe { std::mem::transmute(value) }; key_values.push(value); } Some(Err(err)) => { mark_unlikely(); deserialization_error = Some(err); break; } None => { mark_unlikely(); deserialization_error = Some(LimboError::Corrupt( "Not enough columns in record".to_string(), )); break; } } } Ok(Self { record, key_values, index_key_info, comparators, deserialization_error, }) } } impl Ord for BoxedSortableRecord { #[inline] fn cmp(&self, other: &Self) -> Ordering { if self.deserialization_error.is_some() || other.deserialization_error.is_some() { return Ordering::Equal; } for (i, ((&self_val, &other_val), key_info)) in self .key_values .iter() .zip(other.key_values.iter()) .zip(self.index_key_info.iter()) .enumerate() { let cmp = if let Some(Some(comparator)) = self.comparators.get(i) { comparator(&self_val, &other_val) } else { match (self_val, other_val) { (ValueRef::Text(left), ValueRef::Text(right)) => { key_info.collation.compare_strings(&left, &right) } _ => self_val.partial_cmp(&other_val).unwrap_or(Ordering::Equal), } }; if cmp != Ordering::Equal { return match key_info.sort_order { SortOrder::Asc => cmp, SortOrder::Desc => cmp.reverse(), }; } } Ordering::Equal } } impl PartialOrd for BoxedSortableRecord { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl PartialEq for BoxedSortableRecord { fn eq(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Equal } } impl Eq for BoxedSortableRecord {} #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum SortedChunkIOState { WaitingForRead, ReadComplete, WaitingForWrite, WriteComplete, WriteError, ReadEOF, None, } #[cfg(test)] mod tests { use super::*; use crate::translate::collate::CollationSeq; use crate::types::{ImmutableRecord, Value, ValueRef, ValueType}; use crate::util::IOExt; use crate::PlatformIO; use rand_chacha::{ rand_core::{RngCore, SeedableRng}, ChaCha8Rng, }; fn get_seed() -> u64 { std::env::var("SEED").map_or( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis(), |v| { v.parse() .expect("Failed to parse SEED environment variable as u64") }, ) as u64 } #[test] fn fuzz_external_sort() { let seed = get_seed(); let mut rng = ChaCha8Rng::seed_from_u64(seed); let io = Arc::new(PlatformIO::new().unwrap()); let attempts = 8; for _ in 0..attempts { let mut sorter = Sorter::new( &[SortOrder::Asc], vec![CollationSeq::Binary], vec![None], 256, 64, io.clone(), crate::TempStore::Default, ); let num_records = 1000 + rng.next_u64() % 2000; let num_records = num_records as i64; let num_values = 1 + rng.next_u64() % 4; let value_types = generate_value_types(&mut rng, num_values as usize); let mut initial_records = Vec::with_capacity(num_records as usize); for i in (0..num_records).rev() { let mut values = vec![Value::from_i64(i)]; values.append(&mut generate_values(&mut rng, &value_types)); let record = ImmutableRecord::from_values(&values, values.len()); io.block(|| sorter.insert(&record)) .expect("Failed to insert the record"); initial_records.push(record); } io.block(|| sorter.sort()) .expect("Failed to sort the records"); assert!(!sorter.is_empty()); assert!(!sorter.chunks.is_empty()); for i in 0..num_records { assert!(sorter.has_more()); let record = sorter.record().unwrap(); assert_eq!(record.get_values().unwrap()[0], ValueRef::from_i64(i)); // Check that the record remained unchanged after sorting. assert_eq!(record, &initial_records[(num_records - i - 1) as usize]); io.block(|| sorter.next()) .expect("Failed to get the next record"); } assert!(!sorter.has_more()); } } fn generate_value_types(rng: &mut R, num_values: usize) -> Vec { let mut value_types = Vec::with_capacity(num_values); for _ in 0..num_values { let value_type: ValueType = match rng.next_u64() % 4 { 0 => ValueType::Integer, 1 => ValueType::Float, 2 => ValueType::Blob, 3 => ValueType::Null, _ => unreachable!(), }; value_types.push(value_type); } value_types } fn generate_values(rng: &mut R, value_types: &[ValueType]) -> Vec { let mut values = Vec::with_capacity(value_types.len()); for value_type in value_types { let value = match value_type { ValueType::Integer => Value::from_i64(rng.next_u64() as i64), ValueType::Float => { let numerator = rng.next_u64() as f64; let denominator = rng.next_u64() as f64; Value::from_f64(numerator / denominator) } ValueType::Blob => { let mut blob = Vec::with_capacity((rng.next_u64() % 2047 + 1) as usize); rng.fill_bytes(&mut blob); Value::Blob(blob) } ValueType::Null => Value::Null, _ => unreachable!(), }; values.push(value); } values } } ================================================ FILE: core/vdbe/value.rs ================================================ use crate::turso_assert; use crate::{ function::MathFunc, numeric::{format_float, format_float_for_quote, NullableInteger, Numeric}, translate::collate::CollationSeq, types::{compare_immutable_single, AsValueRef, SeekOp}, vdbe::affinity::{real_to_i64, Affinity}, LimboError, Result, Value, ValueRef, }; // we use math functions from Rust stdlib in order to be as portable as possible for the production version of the tursodb #[cfg(not(test))] mod cmath { pub fn exp(x: f64) -> f64 { x.exp() } pub fn log(x: f64) -> f64 { x.ln() } pub fn log10(x: f64) -> f64 { x.log(10.) } pub fn log2(x: f64) -> f64 { x.log(2.) } pub fn pow(x: f64, y: f64) -> f64 { x.powf(y) } pub fn sin(x: f64) -> f64 { x.sin() } pub fn sinh(x: f64) -> f64 { x.sinh() } pub fn asin(x: f64) -> f64 { x.asin() } pub fn asinh(x: f64) -> f64 { x.asinh() } pub fn cos(x: f64) -> f64 { x.cos() } pub fn cosh(x: f64) -> f64 { x.cosh() } pub fn acos(x: f64) -> f64 { x.acos() } pub fn acosh(x: f64) -> f64 { x.acosh() } pub fn tan(x: f64) -> f64 { x.tan() } pub fn tanh(x: f64) -> f64 { x.tanh() } pub fn atan(x: f64) -> f64 { x.atan() } pub fn atanh(x: f64) -> f64 { x.atanh() } pub fn atan2(x: f64, y: f64) -> f64 { x.atan2(y) } pub fn degrees(x: f64) -> f64 { x.to_degrees() } pub fn radians(x: f64) -> f64 { x.to_radians() } } // we use exactly same math function as SQLite in tests in order to avoid mismatch in the differential tests due to floating-point precision issues #[cfg(test)] mod cmath { extern "C" { pub fn exp(x: f64) -> f64; pub fn log(x: f64) -> f64; pub fn log10(x: f64) -> f64; pub fn log2(x: f64) -> f64; pub fn pow(x: f64, y: f64) -> f64; pub fn sin(x: f64) -> f64; pub fn sinh(x: f64) -> f64; pub fn asin(x: f64) -> f64; pub fn asinh(x: f64) -> f64; pub fn cos(x: f64) -> f64; pub fn cosh(x: f64) -> f64; pub fn acos(x: f64) -> f64; pub fn acosh(x: f64) -> f64; pub fn tan(x: f64) -> f64; pub fn tanh(x: f64) -> f64; pub fn atan(x: f64) -> f64; pub fn atanh(x: f64) -> f64; pub fn atan2(x: f64, y: f64) -> f64; } // SQLite's M_PI constant (same value as SQLite's func.c) #[allow(clippy::excessive_precision)] const M_PI: f64 = 3.141592653589793238462643383279502884; pub fn degrees(x: f64) -> f64 { x * 180.0 / M_PI } pub fn radians(x: f64) -> f64 { x * M_PI / 180.0 } } #[derive(Debug, Clone, Copy, PartialEq)] pub(super) enum ComparisonOp { Eq, Ne, Lt, Le, Gt, Ge, } impl ComparisonOp { pub(super) fn compare( &self, lhs: V1, rhs: V2, collation: CollationSeq, ) -> bool { let order = compare_immutable_single(lhs, rhs, collation); match self { ComparisonOp::Eq => order.is_eq(), ComparisonOp::Ne => order.is_ne(), ComparisonOp::Lt => order.is_lt(), ComparisonOp::Le => order.is_le(), ComparisonOp::Gt => order.is_gt(), ComparisonOp::Ge => order.is_ge(), } } pub(super) fn compare_nulls( &self, lhs: V1, rhs: V2, null_eq: bool, ) -> bool { let (lhs, rhs) = (lhs.as_value_ref(), rhs.as_value_ref()); turso_assert!(matches!(lhs, ValueRef::Null) || matches!(rhs, ValueRef::Null)); match self { ComparisonOp::Eq => { let both_null = lhs == rhs; null_eq && both_null } ComparisonOp::Ne => { let at_least_one_null = lhs != rhs; null_eq && at_least_one_null } ComparisonOp::Lt | ComparisonOp::Le | ComparisonOp::Gt | ComparisonOp::Ge => false, } } } impl From for ComparisonOp { fn from(value: SeekOp) -> Self { match value { SeekOp::GE { eq_only: true } | SeekOp::LE { eq_only: true } => ComparisonOp::Eq, SeekOp::GE { eq_only: false } => ComparisonOp::Ge, SeekOp::GT => ComparisonOp::Gt, SeekOp::LE { eq_only: false } => ComparisonOp::Le, SeekOp::LT => ComparisonOp::Lt, } } } enum TrimType { All, Left, Right, } impl Value { pub fn exec_lower(&self) -> Option { self.cast_text() .map(|s| Value::build_text(s.to_ascii_lowercase())) } pub fn exec_length(&self) -> Self { match self { Value::Text(t) => { let s = t.as_str(); let len_before_null = s.find('\0').map_or_else( || s.chars().count(), |null_pos| s[..null_pos].chars().count(), ); Value::from_i64(len_before_null as i64) } Value::Numeric(_) => { // For numbers, SQLite returns the length of the string representation Value::from_i64(self.to_string().chars().count() as i64) } Value::Blob(blob) => Value::from_i64(blob.len() as i64), _ => self.to_owned(), } } pub fn exec_octet_length(&self) -> Self { match self { Value::Text(s) => Value::from_i64(s.as_str().len() as i64), Value::Blob(blob) => Value::from_i64(blob.len() as i64), Value::Numeric(_) => Value::from_i64(self.to_string().len() as i64), _ => self.to_owned(), } } pub fn exec_upper(&self) -> Option { self.cast_text() .map(|s| Value::build_text(s.to_ascii_uppercase())) } pub fn exec_sign(&self) -> Option { let v = Numeric::from_value_strict(self).map(|value| value.to_f64())?; Some(Value::from_i64(if v > 0.0 { 1 } else if v < 0.0 { -1 } else { 0 })) } /// Generates the Soundex code for a given word pub fn exec_soundex(&self) -> Value { let s = match self { Value::Text(s) => s.as_str(), Value::Null => return Value::build_text("?000"), _ => return Value::build_text("?000"), }; if s.bytes().any(|b| !b.is_ascii_alphabetic()) { return Value::build_text("?000"); } let mut bytes = s.bytes(); let Some(first_char) = bytes.next() else { return Value::build_text("?000"); }; let first_upper = first_char.to_ascii_uppercase(); let mut result = String::with_capacity(4); result.push(first_upper as char); let get_code = |b: u8| -> Option { match b.to_ascii_lowercase() { b'b' | b'f' | b'p' | b'v' => Some('1'), b'c' | b'g' | b'j' | b'k' | b'q' | b's' | b'x' | b'z' => Some('2'), b'd' | b't' => Some('3'), b'l' => Some('4'), b'm' | b'n' => Some('5'), b'r' => Some('6'), _ => None, // a, e, i, o, u, y, h, w } }; let mut prev_code = get_code(first_char); for b in bytes { if result.len() >= 4 { break; } // H and W are ignored completely in this step for continuity checks let lower = b.to_ascii_lowercase(); if lower == b'h' || lower == b'w' { continue; } let code = get_code(b); if code.is_some() && code != prev_code { result.push(code.unwrap()); prev_code = code; } else if code.is_none() { // Reset previous code for vowels/separators (a,e,i,o,u,y) prev_code = None; } } while result.len() < 4 { result.push('0'); } Value::build_text(result) } pub fn exec_abs(&self) -> Result { Ok(match self { Value::Null => Value::Null, Value::Numeric(Numeric::Integer(v)) => { Value::from_i64(v.checked_abs().ok_or(LimboError::IntegerOverflow)?) } Value::Numeric(Numeric::Float(non_nan)) => Value::from_f64(f64::from(*non_nan).abs()), _ => { let s = match self { Value::Text(text) => std::borrow::Cow::Borrowed(text.as_str()), Value::Blob(blob) => String::from_utf8_lossy(blob), _ => unreachable!(), }; crate::numeric::str_to_f64(s) .map(|v| Value::from_f64(f64::from(v).abs())) .unwrap_or_else(|| Value::from_f64(0.0)) } }) } pub fn exec_random(generate_random_number: F) -> Self where F: Fn() -> i64, { Value::from_i64(generate_random_number()) } /// SQLite default max blob/string size (1GB) pub const MAX_BLOB_LENGTH: i64 = 1_000_000_000; pub fn exec_randomblob(&self, fill_bytes: F) -> Result where F: Fn(&mut [u8]), { let length = match self { Value::Numeric(Numeric::Integer(i)) => *i, Value::Numeric(Numeric::Float(f)) => f64::from(*f) as i64, Value::Text(t) => t.as_str().parse().unwrap_or(1), _ => 1, } .max(1); if length > Self::MAX_BLOB_LENGTH { return Err(LimboError::TooBig); } let mut blob: Vec = vec![0; length as usize]; fill_bytes(&mut blob); Ok(Value::Blob(blob)) } pub fn exec_quote(&self) -> Self { use std::fmt::Write; match self { Value::Null => Value::build_text("NULL"), Value::Numeric(Numeric::Integer(i)) => Value::build_text(i.to_string()), Value::Numeric(Numeric::Float(f)) => { Value::build_text(format_float_for_quote(f64::from(*f))) } Value::Blob(b) => { // SQLite returns X'hexdigits' for blobs let mut quoted = String::with_capacity(3 + b.len() * 2); quoted.push_str("X'"); for byte in b.iter() { write!(&mut quoted, "{byte:02X}").expect("unable to write hex bytes"); } quoted.push('\''); Value::build_text(quoted) } Value::Text(s) => { let mut quoted = String::with_capacity(s.as_str().len() + 2); quoted.push('\''); for c in s.as_str().chars() { if c == '\0' { break; } else if c == '\'' { quoted.push('\''); quoted.push(c); } else { quoted.push(c); } } quoted.push('\''); Value::build_text(quoted) } } } pub fn exec_nullif(&self, second_value: &Self) -> Self { if self != second_value { self.clone() } else { Value::Null } } pub fn exec_substring( value: &Value, start_value: &Value, length_value: Option<&Value>, ) -> Value { /// Function is stabilized but not released for version 1.88 \ /// https://doc.rust-lang.org/src/core/str/mod.rs.html#453 const fn ceil_char_boundary(s: &str, index: usize) -> usize { const fn is_utf8_char_boundary(c: u8) -> bool { // This is bit magic equivalent to: b < 128 || b >= 192 (c as i8) >= -0x40 } if index >= s.len() { s.len() } else { let mut i = index; while i < s.len() { if is_utf8_char_boundary(s.as_bytes()[i]) { break; } i += 1; } // The character boundary will be within four bytes of the index debug_assert!(i <= index + 3); i } } // Match SQLite's substr algorithm exactly (func.c substrFunc) // Uses wrapping arithmetic to match C overflow behavior fn calculate_postions( mut p1: i64, len: usize, length_value: Option<&Value>, ) -> (usize, usize) { let len = len as i64; let mut p2 = match length_value { Some(Value::Numeric(Numeric::Integer(length))) => *length, // SQLite uses SQLITE_LIMIT_LENGTH (default 1 billion) when no explicit length. // Using len causes wrong results when p1 is large negative number. _ => Value::MAX_BLOB_LENGTH, }; // Track if length was explicitly provided let explicit_length = length_value.is_some(); // Handle negative start position (count from end) if p1 < 0 { p1 = p1.wrapping_add(len); if p1 < 0 { p2 = p2.wrapping_add(p1); p1 = 0; } } else if p1 > 0 { p1 -= 1; // Convert 1-indexed to 0-indexed } else if p2 > 0 && explicit_length { // SQLite quirk: when p1==0, p2>0, and explicit length, decrement p2 // This means substr('x', 0, 3) returns 2 chars, not 3 // But substr('x', 0) with no length returns whole string p2 -= 1; } // Handle negative length (characters preceding position) if p2 < 0 { if p2 < -p1 { p2 = p1; } else { p2 = -p2; } p1 -= p2; } // Clamp to valid range let start = p1.max(0).min(len) as usize; let end = p1.saturating_add(p2).max(0).min(len) as usize; (start, end) } let start_value = start_value.exec_cast("INT"); let length_value = length_value.map(|value| value.exec_cast("INT")); // If length is explicitly NULL, return NULL (SQLite behavior) if matches!(length_value, Some(Value::Null)) { return Value::Null; } match (value, start_value) { (Value::Blob(b), Value::Numeric(Numeric::Integer(start))) => { let (start, end) = calculate_postions(start, b.len(), length_value.as_ref()); Value::from_blob(b[start..end].to_vec()) } (value, Value::Numeric(Numeric::Integer(start))) => { if let Some(text) = value.cast_text() { // Use character count to accurately resolve negative offsets in UTF-8 strings let char_count = text.chars().count(); let (mut start, mut end) = calculate_postions(start, char_count, length_value.as_ref()); // https://github.com/sqlite/sqlite/blob/a248d84f/src/func.c#L417 let s = text.as_str(); let mut start_byte_idx = 0; end -= start; while start > 0 { start_byte_idx = ceil_char_boundary(s, start_byte_idx + 1); start -= 1; } let mut end_byte_idx = start_byte_idx; while end > 0 { end_byte_idx = ceil_char_boundary(s, end_byte_idx + 1); end -= 1; } Value::build_text(s[start_byte_idx..end_byte_idx].to_string()) } else { Value::Null } } _ => Value::Null, } } pub fn exec_instr(&self, pattern: &Value) -> Value { if self == &Value::Null || pattern == &Value::Null { return Value::Null; } if let (Value::Blob(reg), Value::Blob(pattern)) = (self, pattern) { // SQLite returns 1 for empty pattern (found at position 1) if pattern.is_empty() { return Value::from_i64(1); } let result = reg .windows(pattern.len()) .position(|window| window == *pattern) .map_or(0, |i| i + 1); return Value::from_i64(result as i64); } let reg_str; let reg = match self { Value::Text(s) => s.as_str(), _ => { reg_str = self.to_string(); reg_str.as_str() } }; let pattern_str; let pattern = match pattern { Value::Text(s) => s.as_str(), _ => { pattern_str = pattern.to_string(); pattern_str.as_str() } }; match reg.find(pattern) { Some(byte_pos) => { // Convert byte position to character position (1-indexed) let char_pos = reg[..byte_pos].chars().count() + 1; Value::from_i64(char_pos as i64) } None => Value::from_i64(0), } } pub fn exec_typeof(&self) -> Value { match self { Value::Null => Value::build_text("null"), Value::Numeric(Numeric::Integer(_)) => Value::build_text("integer"), Value::Numeric(Numeric::Float(_)) => Value::build_text("real"), Value::Text(_) => Value::build_text("text"), Value::Blob(_) => Value::build_text("blob"), } } pub fn exec_hex(&self) -> Value { match self { Value::Text(_) | Value::Numeric(_) => { let text = self.to_string(); Value::build_text(hex::encode_upper(text)) } Value::Blob(blob_bytes) => Value::build_text(hex::encode_upper(blob_bytes)), Value::Null => Value::build_text(""), } } pub fn exec_unhex(&self, ignored_chars: Option<&Value>) -> Value { match self { Value::Null => Value::Null, _ => match ignored_chars { None => match self .cast_text() .map(|s| hex::decode(&s[0..s.find('\0').unwrap_or(s.len())])) { Some(Ok(bytes)) => Value::Blob(bytes), _ => Value::Null, }, Some(ignore) => match ignore { Value::Text(_) => { let pat = ignore.to_string(); let trimmed = self .to_string() .trim_start_matches(|x| pat.contains(x)) .trim_end_matches(|x| pat.contains(x)) .to_string(); match hex::decode(trimmed) { Ok(bytes) => Value::Blob(bytes), _ => Value::Null, } } _ => Value::Null, }, }, } } pub fn exec_unicode(&self) -> Value { match self { Value::Text(_) | Value::Numeric(_) | Value::Blob(_) => { let text = self.to_string(); if let Some(first_char) = text.chars().next() { if first_char == '\0' { return Value::Null; } Value::from_i64(first_char as u32 as i64) } else { Value::Null } } _ => Value::Null, } } pub fn exec_round(&self, precision: Option<&Value>) -> Value { let Some(f) = Numeric::from_value(self).map(|v| v.to_f64()) else { return Value::Null; }; let precision = match precision.map(|v| Numeric::from_value(v).map(|v| v.to_f64())) { None => 0.0, Some(Some(v)) => v, Some(None) => return Value::Null, }; if !(-4503599627370496.0..=4503599627370496.0).contains(&f) { return Value::from_f64(f); } let precision = if precision < 1.0 { 0.0 } else { precision }; let precision = precision.clamp(0.0, 30.0) as usize; if precision == 0 { return Value::from_f64(((f + if f < 0.0 { -0.5 } else { 0.5 }) as i64) as f64); } let f: f64 = crate::numeric::str_to_f64(format!("{f:.precision$}")) .expect("formatted float should always parse successfully") .into(); Value::from_f64(f) } fn _exec_trim(&self, pattern: Option<&Value>, trim_type: TrimType) -> Value { let text_cow = match self { Value::Text(s) => std::borrow::Cow::Borrowed(s.as_str()), Value::Null => return Value::Null, _ => std::borrow::Cow::Owned(self.to_string()), }; let trimmed = match pattern { Some(p) => { if matches!(p, Value::Null) { return Value::Null; } let pat_cow = match p { Value::Text(s) => std::borrow::Cow::Borrowed(s.as_str()), _ => std::borrow::Cow::Owned(p.to_string()), }; let p_str = pat_cow.as_ref(); match trim_type { TrimType::All => text_cow.trim_matches(|c| p_str.contains(c)), TrimType::Left => text_cow.trim_start_matches(|c| p_str.contains(c)), TrimType::Right => text_cow.trim_end_matches(|c| p_str.contains(c)), } } None => match trim_type { TrimType::All => text_cow.trim_matches(' '), TrimType::Left => text_cow.trim_start_matches(' '), TrimType::Right => text_cow.trim_end_matches(' '), }, }; Value::build_text(trimmed.to_string()) } // Implements TRIM pattern matching. pub fn exec_trim(&self, pattern: Option<&Value>) -> Value { self._exec_trim(pattern, TrimType::All) } // Implements RTRIM pattern matching. pub fn exec_rtrim(&self, pattern: Option<&Value>) -> Value { self._exec_trim(pattern, TrimType::Right) } // Implements LTRIM pattern matching. pub fn exec_ltrim(&self, pattern: Option<&Value>) -> Value { self._exec_trim(pattern, TrimType::Left) } pub fn exec_zeroblob(&self) -> Result { let length: i64 = match self { Value::Numeric(Numeric::Integer(i)) => *i, Value::Numeric(Numeric::Float(f)) => f64::from(*f) as i64, Value::Text(s) => s.as_str().parse().unwrap_or(0), _ => 0, } .max(0); if length > Self::MAX_BLOB_LENGTH { return Err(LimboError::TooBig); } Ok(Value::Blob(vec![0; length as usize])) } // exec_if returns whether you should jump pub fn exec_if(&self, jump_if_null: bool, not: bool) -> bool { Numeric::from_value(self) .map(|v| v.to_bool()) .map(|jump| if not { !jump } else { jump }) .unwrap_or(jump_if_null) } pub fn exec_cast(&self, datatype: &str) -> Value { if matches!(self, Value::Null) { return Value::Null; } match Affinity::affinity(datatype) { // NONE Casting a value to a type-name with no affinity causes the value to be converted into a BLOB. Casting to a BLOB consists of first casting the value to TEXT in the encoding of the database connection, then interpreting the resulting byte sequence as a BLOB instead of as TEXT. // Historically called NONE, but it's the same as BLOB Affinity::Blob => { // Convert to TEXT first, then interpret as BLOB // TODO: handle encoding let text = self.to_string(); Value::Blob(text.into_bytes()) } // TEXT To cast a BLOB value to TEXT, the sequence of bytes that make up the BLOB is interpreted as text encoded using the database encoding. // Casting an INTEGER or REAL value into TEXT renders the value as if via sqlite3_snprintf() except that the resulting TEXT uses the encoding of the database connection. Affinity::Text => { // Convert everything to text representation // TODO: handle encoding and whatever sqlite3_snprintf does Value::build_text(self.to_string()) } Affinity::Real => match self { Value::Blob(b) => { let text = String::from_utf8_lossy(b); Value::from_f64( crate::numeric::str_to_f64(&text) .map(f64::from) .unwrap_or(0.0), ) } Value::Text(t) => { Value::from_f64(crate::numeric::str_to_f64(t).map(f64::from).unwrap_or(0.0)) } Value::Numeric(Numeric::Integer(i)) => Value::from_f64(*i as f64), Value::Numeric(Numeric::Float(f)) => Value::Numeric(Numeric::Float(*f)), _ => Value::from_f64(0.0), }, Affinity::Integer => match self { Value::Blob(b) => { // Convert BLOB to TEXT first let text = String::from_utf8_lossy(b); Value::from_i64(crate::numeric::str_to_i64(&text).unwrap_or(0)) } Value::Text(t) => Value::from_i64(crate::numeric::str_to_i64(t).unwrap_or(0)), Value::Numeric(Numeric::Integer(i)) => Value::from_i64(*i), // A cast of a REAL value into an INTEGER follows SQLite's sqlite3RealToI64: // truncate toward zero and clamp to i64::MIN/MAX if outside the safe range. Value::Numeric(Numeric::Float(f)) => Value::from_i64(real_to_i64(f64::from(*f))), _ => Value::from_i64(0), }, Affinity::Numeric => match self { Value::Null => Value::Null, Value::Numeric(Numeric::Integer(v)) => Value::from_i64(*v), Value::Numeric(Numeric::Float(v)) => Value::Numeric(Numeric::Float(*v)), _ => { let s = match self { Value::Text(text) => text.as_str().into(), Value::Blob(blob) => String::from_utf8_lossy(blob.as_slice()), _ => unreachable!(), }; crate::util::checked_cast_text_to_numeric(&s, false) .ok() .unwrap_or_else(|| Value::from_i64(0)) } }, } } pub fn exec_replace(source: &Value, pattern: &Value, replacement: &Value) -> Value { // The replace(X,Y,Z) function returns a string formed by substituting string Z for every occurrence of // string Y in string X. The BINARY collating sequence is used for comparisons. If Y is an empty string // then return X unchanged. If Z is not initially a string, it is cast to a UTF-8 string prior to processing. // If any of the arguments is NULL, the result is NULL. if matches!(source, Value::Null) || matches!(pattern, Value::Null) || matches!(replacement, Value::Null) { return Value::Null; } let source = source.exec_cast("TEXT"); let pattern = pattern.exec_cast("TEXT"); let replacement = replacement.exec_cast("TEXT"); // If any of the casts failed, panic as text casting is not expected to fail. match (&source, &pattern, &replacement) { (Value::Text(source), Value::Text(pattern), Value::Text(replacement)) => { if pattern.as_str().is_empty() || pattern.as_str().starts_with('\0') { return Value::Text(source.clone()); } let result = source .as_str() .replace(pattern.as_str(), replacement.as_str()); Value::build_text(result) } _ => unreachable!("text cast should never fail"), } } pub fn exec_math_unary(&self, function: &MathFunc) -> Value { let v = Numeric::from_value_strict(self); // In case of some functions and integer input, return the input as is if let Some(Numeric::Integer(i)) = v { if matches! { function, MathFunc::Ceil | MathFunc::Ceiling | MathFunc::Floor | MathFunc::Trunc } { return Value::from_i64(i); } } let Some(f) = v.map(|v| v.to_f64()) else { return Value::Null; }; if matches! { function, MathFunc::Ln | MathFunc::Log10 | MathFunc::Log2 } && f <= 0.0 { return Value::Null; } #[allow(unused_unsafe)] let result = match function { MathFunc::Acos => unsafe { cmath::acos(f) }, MathFunc::Acosh => unsafe { cmath::acosh(f) }, MathFunc::Asin => unsafe { cmath::asin(f) }, MathFunc::Asinh => unsafe { cmath::asinh(f) }, MathFunc::Atan => unsafe { cmath::atan(f) }, MathFunc::Atanh => unsafe { cmath::atanh(f) }, MathFunc::Ceil | MathFunc::Ceiling => libm::ceil(f), MathFunc::Cos => unsafe { cmath::cos(f) }, MathFunc::Cosh => unsafe { cmath::cosh(f) }, MathFunc::Degrees => cmath::degrees(f), MathFunc::Exp => unsafe { cmath::exp(f) }, MathFunc::Floor => libm::floor(f), MathFunc::Ln => unsafe { cmath::log(f) }, MathFunc::Log10 => unsafe { cmath::log10(f) }, MathFunc::Log2 => unsafe { cmath::log2(f) }, MathFunc::Radians => cmath::radians(f), MathFunc::Sin => unsafe { cmath::sin(f) }, MathFunc::Sinh => unsafe { cmath::sinh(f) }, MathFunc::Sqrt => libm::sqrt(f), MathFunc::Tan => unsafe { cmath::tan(f) }, MathFunc::Tanh => unsafe { cmath::tanh(f) }, MathFunc::Trunc => libm::trunc(f), _ => unreachable!("Unexpected mathematical unary function {:?}", function), }; if result.is_nan() { Value::Null } else { Value::from_f64(result) } } pub fn exec_math_binary(&self, rhs: &Value, function: &MathFunc) -> Value { let Some(lhs) = Numeric::from_value_strict(self).map(|v| v.to_f64()) else { return Value::Null; }; let Some(rhs) = Numeric::from_value_strict(rhs).map(|v| v.to_f64()) else { return Value::Null; }; #[allow(unused_unsafe)] let result = match function { MathFunc::Atan2 => unsafe { cmath::atan2(lhs, rhs) }, MathFunc::Mod => libm::fmod(lhs, rhs), MathFunc::Pow | MathFunc::Power => unsafe { cmath::pow(lhs, rhs) }, _ => unreachable!("Unexpected mathematical binary function {:?}", function), }; if result.is_nan() { Value::Null } else { Value::from_f64(result) } } pub fn exec_math_log(&self, base: Option<&Value>) -> Value { let Some(f) = Numeric::from_value_strict(self).map(|v| v.to_f64()) else { return Value::Null; }; let base = match base.map(|value| Numeric::from_value_strict(value).map(|v| v.to_f64())) { Some(Some(f)) => f, Some(None) => return Value::Null, None => 10.0, }; if f <= 0.0 || base <= 0.0 || base == 1.0 { return Value::Null; } if base == 2.0 { return Value::from_f64(libm::log2(f)); } else if base == 10.0 { return Value::from_f64(libm::log10(f)); }; let log_x = libm::log(f); let log_base = libm::log(base); if log_base <= 0.0 { return Value::Null; } let result = log_x / log_base; Value::from_f64(result) } pub fn exec_add(&self, rhs: &Value) -> Value { (|| Numeric::from_value(self)?.checked_add(Numeric::from_value(rhs)?))().into() } pub fn exec_subtract(&self, rhs: &Value) -> Value { (|| Numeric::from_value(self)?.checked_sub(Numeric::from_value(rhs)?))().into() } pub fn exec_multiply(&self, rhs: &Value) -> Value { (|| Numeric::from_value(self)?.checked_mul(Numeric::from_value(rhs)?))().into() } pub fn exec_divide(&self, rhs: &Value) -> Value { (|| Numeric::from_value(self)?.checked_div(Numeric::from_value(rhs)?))().into() } pub fn exec_bit_and(&self, rhs: &Value) -> Value { (NullableInteger::from(self) & NullableInteger::from(rhs)).into() } pub fn exec_bit_or(&self, rhs: &Value) -> Value { (NullableInteger::from(self) | NullableInteger::from(rhs)).into() } pub fn exec_remainder(&self, rhs: &Value) -> Value { let convert_to_float = matches!(Numeric::from_value(self), Some(Numeric::Float(_))) || matches!(Numeric::from_value(rhs), Some(Numeric::Float(_))); match NullableInteger::from(self) % NullableInteger::from(rhs) { NullableInteger::Null => Value::Null, NullableInteger::Integer(v) => { if convert_to_float { Value::from_f64(v as f64) } else { Value::from_i64(v) } } } } pub fn exec_bit_not(&self) -> Value { (!NullableInteger::from(self)).into() } pub fn exec_shift_left(&self, rhs: &Value) -> Value { (NullableInteger::from(self) << NullableInteger::from(rhs)).into() } pub fn exec_shift_right(&self, rhs: &Value) -> Value { (NullableInteger::from(self) >> NullableInteger::from(rhs)).into() } pub fn exec_boolean_not(&self) -> Value { match Numeric::from_value(self).map(|v| v.to_bool()) { None => Value::Null, Some(v) => Value::from_i64(!v as i64), } } pub fn exec_concat(&self, rhs: &Value) -> Value { if let (Value::Blob(lhs), Value::Blob(rhs)) = (self, rhs) { return Value::Blob([lhs.as_slice(), rhs.as_slice()].concat().to_vec()); } let Some(lhs) = self.cast_text() else { return Value::Null; }; let Some(rhs) = rhs.cast_text() else { return Value::Null; }; Value::build_text(lhs + &rhs) } pub fn exec_and(&self, rhs: &Value) -> Value { match ( Numeric::from_value(self).map(|v| v.to_bool()), Numeric::from_value(rhs).map(|v| v.to_bool()), ) { (Some(false), _) | (_, Some(false)) => Value::from_i64(0), (None, _) | (_, None) => Value::Null, _ => Value::from_i64(1), } } pub fn exec_or(&self, rhs: &Value) -> Value { match ( Numeric::from_value(self).map(|v| v.to_bool()), Numeric::from_value(rhs).map(|v| v.to_bool()), ) { (Some(true), _) | (_, Some(true)) => Value::from_i64(1), (None, _) | (_, None) => Value::Null, _ => Value::from_i64(0), } } pub fn exec_like(pattern: &str, text: &str, escape: Option) -> Result { const MAX_LIKE_PATTERN_LENGTH: usize = 50000; if pattern.len() > MAX_LIKE_PATTERN_LENGTH { return Err(LimboError::Constraint( "LIKE or GLOB pattern too complex".to_string(), )); } let has_escape = escape.is_some_and(|e| pattern.contains(e)); // 1. Exact match (no wildcards) if !has_escape && !pattern.contains(['%', '_']) { return Ok(pattern.eq_ignore_ascii_case(text)); } // 2. Fast Path: 'abc%' (Prefix) if !has_escape && pattern.ends_with('%') && !pattern[..pattern.len() - 1].contains(['%', '_']) { let prefix = &pattern[..pattern.len() - 1]; if text.len() >= prefix.len() && text.is_char_boundary(prefix.len()) { return Ok(text[..prefix.len()].eq_ignore_ascii_case(prefix)); } // Fall through to pattern_compare if boundary check fails (multi-byte UTF-8) } // 3. Fast Path: '%abc' (Suffix) if !has_escape && pattern.starts_with('%') && !pattern[1..].contains(['%', '_']) { let suffix = &pattern[1..]; let start = text.len().wrapping_sub(suffix.len()); if text.len() >= suffix.len() && text.is_char_boundary(start) { return Ok(text[start..].eq_ignore_ascii_case(suffix)); } // Fall through to pattern_compare if boundary check fails (multi-byte UTF-8) } Ok(pattern_compare(pattern, text, &LIKE_INFO, escape) == CompareResult::Match) } pub fn exec_glob(pattern: &str, text: &str) -> Result { const MAX_GLOB_PATTERN_LENGTH: usize = 50000; const GLOB_CHARS: [char; 3] = ['*', '?', '[']; if pattern.len() > MAX_GLOB_PATTERN_LENGTH { return Err(LimboError::Constraint( "GLOB pattern too complex".to_string(), )); } // 1. Exact match (no wildcards) if !pattern.contains(GLOB_CHARS) { return Ok(pattern == text); } // 2. Fast Path: 'abc*' (Prefix) if pattern.ends_with('*') && !pattern[..pattern.len() - 1].contains(GLOB_CHARS) { let prefix = &pattern[..pattern.len() - 1]; if text.len() >= prefix.len() && text.is_char_boundary(prefix.len()) { return Ok(&text[..prefix.len()] == prefix); } // Fall through to pattern_compare if boundary check fails (multi-byte UTF-8) } // 3. Fast Path: '*abc' (Suffix) if pattern.starts_with('*') && !pattern[1..].contains(GLOB_CHARS) { let suffix = &pattern[1..]; let start = text.len().wrapping_sub(suffix.len()); if text.len() >= suffix.len() && text.is_char_boundary(start) { return Ok(&text[start..] == suffix); } // Fall through to pattern_compare if boundary check fails (multi-byte UTF-8) } Ok(pattern_compare(pattern, text, &GLOB_INFO, None) == CompareResult::Match) } pub fn exec_min<'a, T: Iterator>(regs: T) -> Value { // SQLite: multi-arg min() returns NULL if ANY argument is NULL let mut result: Option<&Value> = None; for v in regs { if matches!(v, Value::Null) { return Value::Null; } result = Some(match result { None => v, Some(cur) if v < cur => v, Some(cur) => cur, }); } result.map(|v| v.to_owned()).unwrap_or(Value::Null) } pub fn exec_max<'a, T: Iterator>(regs: T) -> Value { // SQLite: multi-arg max() returns NULL if ANY argument is NULL let mut result: Option<&Value> = None; for v in regs { if matches!(v, Value::Null) { return Value::Null; } result = Some(match result { None => v, Some(cur) if v > cur => v, Some(cur) => cur, }); } result.map(|v| v.to_owned()).unwrap_or(Value::Null) } /// Concatenate another value onto this Text value, converting both to strings. /// Used by GROUP_CONCAT/STRING_AGG to properly handle all value types. /// Panics if self is not a Text value. pub fn exec_group_concat(&mut self, other: &Value) { let Value::Text(text) = self else { panic!("concat_to_text must be called only on Value::Text"); }; text.value.to_mut().push_str(&other.to_string()); } pub fn exec_concat_strings<'a, T: Iterator>(registers: T) -> Self { let mut result = String::new(); for val in registers { match val { Value::Null => continue, Value::Text(s) => result.push_str(s.as_str()), Value::Blob(b) => result.push_str(&String::from_utf8_lossy(b)), Value::Numeric(Numeric::Integer(i)) => result.push_str(&i.to_string()), Value::Numeric(Numeric::Float(f)) => result.push_str(&format_float(f64::from(*f))), } } Value::build_text(result) } pub fn exec_concat_ws<'a, T: ExactSizeIterator>(mut registers: T) -> Self { if registers.len() == 0 { return Value::Null; } let separator = match registers .next() .expect("registers should have at least one element after length check") { Value::Null | Value::Blob(_) => return Value::Null, v => format!("{v}"), }; let parts = registers.filter_map(|val| match val { Value::Text(_) | Value::Numeric(_) => Some(format!("{val}")), _ => None, }); let result = parts.collect::>().join(&separator); Value::build_text(result) } pub fn exec_char<'a, T: Iterator>(values: T) -> Self { let result: String = values .filter_map(|x| match x { Value::Numeric(Numeric::Integer(i)) => { // Convert integer to Unicode codepoint. // For invalid codepoints (negative, surrogates, or > U+10FFFF), // output U+FFFD (replacement character) to match SQLite behavior. if *i >= 0 { Some(char::from_u32(*i as u32).unwrap_or('\u{FFFD}')) } else { Some('\u{FFFD}') } } // NULL arguments produce NUL characters to match SQLite behavior. Value::Null => Some('\0'), _ => None, }) .collect(); Value::build_text(result) } } /// Result of LIKE pattern comparison. /// `NoWildcardMatch` signals an early abort when a literal after `%` cannot be found, /// allowing the algorithm to skip unnecessary backtracking. #[derive(PartialEq)] enum CompareResult { Match, NoMatch, NoWildcardMatch, } struct PatternInfo { match_all: char, match_one: char, match_set: Option, no_case: bool, } const LIKE_INFO: PatternInfo = PatternInfo { match_all: '%', match_one: '_', match_set: None, no_case: true, }; const GLOB_INFO: PatternInfo = PatternInfo { match_all: '*', match_one: '?', match_set: Some('['), no_case: false, }; /// LIKE and GLOB pattern matching based on SQLite's patternCompare algorithm (src/func.c). /// Uses recursive descent with early termination via `NoWildcardMatch` to avoid /// exponential backtracking on patterns like `%a%a%a%...%b`. /// Ref: https://github.com/sqlite/sqlite/blob/master/src/func.c#L728 fn pattern_compare( pattern: &str, text: &str, info: &PatternInfo, escape: Option, ) -> CompareResult { let mut p_indices = pattern.char_indices(); let mut t_indices = text.char_indices(); let mut p_curr = p_indices.next(); let mut t_curr = t_indices.next(); // Checkpoints for backtracking let mut wildcard_p_iter: Option = None; let mut wildcard_t_iter: Option = None; loop { match (p_curr, t_curr) { (Some((_, p_char)), Some((_, t_char))) => { if p_char == info.match_all && Some(p_char) != escape { // Consume consecutive match_alls let mut next_p = p_indices.clone(); while let Some((_, c)) = next_p.clone().next() { if c == info.match_all && Some(c) != escape { next_p.next(); } else { break; } } let mut lookahead_p = next_p.clone(); if let Some((_, next_char)) = lookahead_p.next() { let is_wildcard = (next_char == info.match_all && Some(next_char) != escape) || (next_char == info.match_one && Some(next_char) != escape) || (info.match_set == Some(next_char)); let is_escaped_next = Some(next_char) == escape; if !is_wildcard && !is_escaped_next { let mut found = false; // Check current text char if compare_chars(next_char, t_char, info.no_case) { found = true; } else { // Scan remaining text let lookahead_t = t_indices.clone(); for (_, t_c) in lookahead_t { if compare_chars(next_char, t_c, info.no_case) { found = true; break; } } } if !found { return CompareResult::NoWildcardMatch; } } } p_indices = next_p; wildcard_p_iter = Some(p_indices.clone()); p_curr = p_indices.next(); if p_curr.is_none() { return CompareResult::Match; } wildcard_t_iter = Some(t_indices.clone()); continue; } if p_char == info.match_one && Some(p_char) != escape { p_curr = p_indices.next(); t_curr = t_indices.next(); continue; } // Handle Set (GLOB only) if info.match_set == Some(p_char) { let mut seen = false; let mut invert = false; let c = t_char; let mut next_c_opt = p_indices.next(); if let Some((_, c2)) = next_c_opt { if c2 == '^' { invert = true; next_c_opt = p_indices.next(); } } let mut c2_opt = next_c_opt; if let Some((_, c2)) = c2_opt { if c2 == ']' { if c == ']' { seen = true; } c2_opt = p_indices.next(); } } let mut prior_c: Option = None; while let Some((_, c2)) = c2_opt { if c2 == ']' { break; } let mut is_range = false; if c2 == '-' && prior_c.is_some() { let lookahead = p_indices.clone().next(); if let Some((_, c3)) = lookahead { if c3 != ']' { is_range = true; let start = prior_c.unwrap(); let end = c3; if c >= start && c <= end { seen = true; } p_indices.next(); prior_c = None; } } } if !is_range { if c == c2 { seen = true; } prior_c = Some(c2); } c2_opt = p_indices.next(); } if c2_opt.is_none() || !(seen ^ invert) { // Fallthrough to backtracking } else { p_curr = p_indices.next(); t_curr = t_indices.next(); continue; } } else { let (expected_char, next_p_iter) = if Some(p_char) == escape { if let Some((_, literal)) = p_indices.next() { (literal, p_indices.clone()) } else { return CompareResult::NoMatch; } } else { (p_char, p_indices.clone()) }; if compare_chars(expected_char, t_char, info.no_case) { p_indices = next_p_iter; p_curr = p_indices.next(); t_curr = t_indices.next(); continue; } } } (None, None) => return CompareResult::Match, (Some((_, p_char)), None) if p_char == info.match_all && Some(p_char) != escape => { let mut temp = p_indices.clone(); loop { match temp.next() { Some((_, c)) if c == info.match_all && Some(c) != escape => continue, None => return CompareResult::Match, _ => break, } } } _ => {} } // Backtracking if let (Some(wp), Some(wt)) = (wildcard_p_iter.clone(), wildcard_t_iter.clone()) { p_indices = wp; p_curr = p_indices.next(); t_indices = wt.clone(); t_curr = t_indices.next(); if t_curr.is_some() { wildcard_t_iter = Some(t_indices.clone()); continue; } } return CompareResult::NoMatch; } } fn compare_chars(p: char, t: char, no_case: bool) -> bool { if no_case { p.eq_ignore_ascii_case(&t) } else { p == t } } #[cfg(test)] mod tests { use crate::numeric::Numeric; use crate::types::Value; use crate::vdbe::Register; use rand::{Rng, RngCore}; #[test] fn test_exec_add() { let inputs = vec![ (Value::from_i64(3), Value::from_i64(1)), (Value::from_f64(3.0), Value::from_f64(1.0)), (Value::from_f64(3.0), Value::from_i64(1)), (Value::from_i64(3), Value::from_f64(1.0)), (Value::Null, Value::Null), (Value::Null, Value::from_i64(1)), (Value::Null, Value::from_f64(1.0)), (Value::Null, Value::Text("2".into())), (Value::from_i64(1), Value::Null), (Value::from_f64(1.0), Value::Null), (Value::Text("1".into()), Value::Null), (Value::Text("1".into()), Value::Text("3".into())), (Value::Text("1.0".into()), Value::Text("3.0".into())), (Value::Text("1.0".into()), Value::from_f64(3.0)), (Value::Text("1.0".into()), Value::from_i64(3)), (Value::from_f64(1.0), Value::Text("3.0".into())), (Value::from_i64(1), Value::Text("3".into())), ]; let outputs = [ Value::from_i64(4), Value::from_f64(4.0), Value::from_f64(4.0), Value::from_f64(4.0), Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::from_i64(4), Value::from_f64(4.0), Value::from_f64(4.0), Value::from_f64(4.0), Value::from_f64(4.0), Value::from_f64(4.0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_add(rhs), outputs[i], "Wrong ADD for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_subtract() { let inputs = vec![ (Value::from_i64(3), Value::from_i64(1)), (Value::from_f64(3.0), Value::from_f64(1.0)), (Value::from_f64(3.0), Value::from_i64(1)), (Value::from_i64(3), Value::from_f64(1.0)), (Value::Null, Value::Null), (Value::Null, Value::from_i64(1)), (Value::Null, Value::from_f64(1.0)), (Value::Null, Value::Text("1".into())), (Value::from_i64(1), Value::Null), (Value::from_f64(1.0), Value::Null), (Value::Text("4".into()), Value::Null), (Value::Text("1".into()), Value::Text("3".into())), (Value::Text("1.0".into()), Value::Text("3.0".into())), (Value::Text("1.0".into()), Value::from_f64(3.0)), (Value::Text("1.0".into()), Value::from_i64(3)), (Value::from_f64(1.0), Value::Text("3.0".into())), (Value::from_i64(1), Value::Text("3".into())), ]; let outputs = [ Value::from_i64(2), Value::from_f64(2.0), Value::from_f64(2.0), Value::from_f64(2.0), Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::from_i64(-2), Value::from_f64(-2.0), Value::from_f64(-2.0), Value::from_f64(-2.0), Value::from_f64(-2.0), Value::from_f64(-2.0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_subtract(rhs), outputs[i], "Wrong subtract for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_multiply() { let inputs = vec![ (Value::from_i64(3), Value::from_i64(2)), (Value::from_f64(3.0), Value::from_f64(2.0)), (Value::from_f64(3.0), Value::from_i64(2)), (Value::from_i64(3), Value::from_f64(2.0)), (Value::Null, Value::Null), (Value::Null, Value::from_i64(1)), (Value::Null, Value::from_f64(1.0)), (Value::Null, Value::Text("1".into())), (Value::from_i64(1), Value::Null), (Value::from_f64(1.0), Value::Null), (Value::Text("4".into()), Value::Null), (Value::Text("2".into()), Value::Text("3".into())), (Value::Text("2.0".into()), Value::Text("3.0".into())), (Value::Text("2.0".into()), Value::from_f64(3.0)), (Value::Text("2.0".into()), Value::from_i64(3)), (Value::from_f64(2.0), Value::Text("3.0".into())), (Value::from_i64(2), Value::Text("3.0".into())), ]; let outputs = [ Value::from_i64(6), Value::from_f64(6.0), Value::from_f64(6.0), Value::from_f64(6.0), Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::from_i64(6), Value::from_f64(6.0), Value::from_f64(6.0), Value::from_f64(6.0), Value::from_f64(6.0), Value::from_f64(6.0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_multiply(rhs), outputs[i], "Wrong multiply for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_divide() { let inputs = vec![ (Value::from_i64(1), Value::from_i64(0)), (Value::from_f64(1.0), Value::from_f64(0.0)), (Value::from_i64(i64::MIN), Value::from_i64(-1)), (Value::from_f64(6.0), Value::from_f64(2.0)), (Value::from_f64(6.0), Value::from_i64(2)), (Value::from_i64(6), Value::from_i64(2)), (Value::Null, Value::from_i64(2)), (Value::from_i64(2), Value::Null), (Value::Null, Value::Null), (Value::Text("6".into()), Value::Text("2".into())), (Value::Text("6".into()), Value::from_i64(2)), ]; let outputs = [ Value::Null, Value::Null, Value::from_f64(9.223372036854776e18), Value::from_f64(3.0), Value::from_f64(3.0), Value::from_f64(3.0), Value::Null, Value::Null, Value::Null, Value::from_f64(3.0), Value::from_f64(3.0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_divide(rhs), outputs[i], "Wrong divide for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_remainder() { let inputs = vec![ (Value::Null, Value::Null), (Value::Null, Value::from_f64(1.0)), (Value::Null, Value::from_i64(1)), (Value::Null, Value::Text("1".into())), (Value::from_f64(1.0), Value::Null), (Value::from_i64(1), Value::Null), (Value::from_i64(12), Value::from_i64(0)), (Value::from_f64(12.0), Value::from_f64(0.0)), (Value::from_f64(12.0), Value::from_i64(0)), (Value::from_i64(12), Value::from_f64(0.0)), (Value::from_i64(i64::MIN), Value::from_i64(-1)), (Value::from_i64(12), Value::from_i64(3)), (Value::from_f64(12.0), Value::from_f64(3.0)), (Value::from_f64(12.0), Value::from_i64(3)), (Value::from_i64(12), Value::from_f64(3.0)), (Value::from_i64(12), Value::from_i64(-3)), (Value::from_f64(12.0), Value::from_f64(-3.0)), (Value::from_f64(12.0), Value::from_i64(-3)), (Value::from_i64(12), Value::from_f64(-3.0)), (Value::Text("12.0".into()), Value::Text("3.0".into())), (Value::Text("12.0".into()), Value::from_f64(3.0)), (Value::from_f64(12.0), Value::Text("3.0".into())), ]; let outputs = vec![ Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::from_f64(0.0), Value::from_i64(0), Value::from_f64(0.0), Value::from_f64(0.0), Value::from_f64(0.0), Value::from_i64(0), Value::from_f64(0.0), Value::from_f64(0.0), Value::from_f64(0.0), Value::from_f64(0.0), Value::from_f64(0.0), Value::from_f64(0.0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_remainder(rhs), outputs[i], "Wrong remainder for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_and() { let inputs = vec![ (Value::from_i64(0), Value::Null), (Value::Null, Value::from_i64(1)), (Value::Null, Value::Null), (Value::from_f64(0.0), Value::Null), (Value::from_i64(1), Value::from_f64(2.2)), (Value::from_i64(0), Value::Text("string".into())), (Value::from_i64(0), Value::Text("1".into())), (Value::from_i64(1), Value::Text("1".into())), ]; let outputs = [ Value::from_i64(0), Value::Null, Value::Null, Value::from_i64(0), Value::from_i64(1), Value::from_i64(0), Value::from_i64(0), Value::from_i64(1), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_and(rhs), outputs[i], "Wrong AND for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_exec_or() { let inputs = vec![ (Value::from_i64(0), Value::Null), (Value::Null, Value::from_i64(1)), (Value::Null, Value::Null), (Value::from_f64(0.0), Value::Null), (Value::from_i64(1), Value::from_f64(2.2)), (Value::from_f64(0.0), Value::from_i64(0)), (Value::from_i64(0), Value::Text("string".into())), (Value::from_i64(0), Value::Text("1".into())), (Value::from_i64(0), Value::Text("".into())), ]; let outputs = [ Value::Null, Value::from_i64(1), Value::Null, Value::Null, Value::from_i64(1), Value::from_i64(0), Value::from_i64(0), Value::from_i64(1), Value::from_i64(0), ]; assert_eq!( inputs.len(), outputs.len(), "Inputs and Outputs should have same size" ); for (i, (lhs, rhs)) in inputs.iter().enumerate() { assert_eq!( lhs.exec_or(rhs), outputs[i], "Wrong OR for lhs: {lhs}, rhs: {rhs}" ); } } #[test] fn test_length() { let input_str = Value::build_text("bob"); let expected_len = Value::from_i64(3); assert_eq!(input_str.exec_length(), expected_len); let input_integer = Value::from_i64(123); let expected_len = Value::from_i64(3); assert_eq!(input_integer.exec_length(), expected_len); let input_float = Value::from_f64(123.456); let expected_len = Value::from_i64(7); assert_eq!(input_float.exec_length(), expected_len); let expected_blob = Value::Blob("example".as_bytes().to_vec()); let expected_len = Value::from_i64(7); assert_eq!(expected_blob.exec_length(), expected_len); } #[test] fn test_quote() { let input = Value::build_text("abc\0edf"); let expected = Value::build_text("'abc'"); assert_eq!(input.exec_quote(), expected); let input = Value::from_i64(123); let expected = Value::build_text("123"); assert_eq!(input.exec_quote(), expected); let input = Value::from_f64(12.34); let expected = Value::build_text("12.34"); assert_eq!(input.exec_quote(), expected); let input = Value::build_text("hello''world"); let expected = Value::build_text("'hello''''world'"); assert_eq!(input.exec_quote(), expected); let input = Value::from_f64( crate::numeric::str_to_f64("2.042747795102219097e+05") .map(f64::from) .unwrap(), ); let expected = Value::build_text("2.042747795102219097e+05"); assert_eq!(input.exec_quote(), expected); } #[test] fn test_typeof() { let input = Value::Null; let expected: Value = Value::build_text("null"); assert_eq!(input.exec_typeof(), expected); let input = Value::from_i64(123); let expected: Value = Value::build_text("integer"); assert_eq!(input.exec_typeof(), expected); let input = Value::from_f64(123.456); let expected: Value = Value::build_text("real"); assert_eq!(input.exec_typeof(), expected); let input = Value::build_text("hello"); let expected: Value = Value::build_text("text"); assert_eq!(input.exec_typeof(), expected); let input = Value::Blob("limbo".as_bytes().to_vec()); let expected: Value = Value::build_text("blob"); assert_eq!(input.exec_typeof(), expected); } #[test] fn test_unicode() { assert_eq!(Value::build_text("a").exec_unicode(), Value::from_i64(97)); assert_eq!( Value::build_text("😊").exec_unicode(), Value::from_i64(128522) ); assert_eq!(Value::build_text("").exec_unicode(), Value::Null); assert_eq!(Value::build_text("\0").exec_unicode(), Value::Null); assert_eq!(Value::from_i64(23).exec_unicode(), Value::from_i64(50)); assert_eq!(Value::from_i64(0).exec_unicode(), Value::from_i64(48)); assert_eq!(Value::from_f64(0.0).exec_unicode(), Value::from_i64(48)); assert_eq!(Value::from_f64(23.45).exec_unicode(), Value::from_i64(50)); assert_eq!(Value::Null.exec_unicode(), Value::Null); assert_eq!( Value::Blob("example".as_bytes().to_vec()).exec_unicode(), Value::from_i64(101) ); } #[test] fn test_min_max() { let input_int_vec = [ Register::Value(Value::from_i64(-1)), Register::Value(Value::from_i64(10)), ]; assert_eq!( Value::exec_min(input_int_vec.iter().map(|v| v.get_value())), Value::from_i64(-1) ); assert_eq!( Value::exec_max(input_int_vec.iter().map(|v| v.get_value())), Value::from_i64(10) ); let str1 = Register::Value(Value::build_text("A")); let str2 = Register::Value(Value::build_text("z")); let input_str_vec = [str2, str1.clone()]; assert_eq!( Value::exec_min(input_str_vec.iter().map(|v| v.get_value())), Value::build_text("A") ); assert_eq!( Value::exec_max(input_str_vec.iter().map(|v| v.get_value())), Value::build_text("z") ); let input_null_vec = [Register::Value(Value::Null), Register::Value(Value::Null)]; assert_eq!( Value::exec_min(input_null_vec.iter().map(|v| v.get_value())), Value::Null ); assert_eq!( Value::exec_max(input_null_vec.iter().map(|v| v.get_value())), Value::Null ); let input_mixed_vec = [Register::Value(Value::from_i64(10)), str1]; assert_eq!( Value::exec_min(input_mixed_vec.iter().map(|v| v.get_value())), Value::from_i64(10) ); assert_eq!( Value::exec_max(input_mixed_vec.iter().map(|v| v.get_value())), Value::build_text("A") ); // SQLite: multi-arg min/max returns NULL if ANY argument is NULL let input_with_null = [ Register::Value(Value::from_i64(1)), Register::Value(Value::Null), ]; assert_eq!( Value::exec_min(input_with_null.iter().map(|v| v.get_value())), Value::Null ); assert_eq!( Value::exec_max(input_with_null.iter().map(|v| v.get_value())), Value::Null ); } #[test] fn test_trim() { let input_str = Value::build_text(" Bob and Alice "); let expected_str = Value::build_text("Bob and Alice"); assert_eq!(input_str.exec_trim(None), expected_str); let input_str = Value::build_text(" Bob and Alice "); let pattern_str = Value::build_text("Bob and"); let expected_str = Value::build_text("Alice"); assert_eq!(input_str.exec_trim(Some(&pattern_str)), expected_str); let input_str = Value::build_text("\ta"); let expected_str = Value::build_text("\ta"); assert_eq!(input_str.exec_trim(None), expected_str); let input_str = Value::build_text("\na"); let expected_str = Value::build_text("\na"); assert_eq!(input_str.exec_trim(None), expected_str); // TRIM on Integer should return TEXT (SQLite compatibility) let input_int = Value::from_i64(12345); let expected_text = Value::build_text("12345"); assert_eq!(input_int.exec_trim(None), expected_text); // TRIM on Float should return TEXT (SQLite compatibility) let input_float = Value::from_f64(123.5); let expected_text = Value::build_text("123.5"); assert_eq!(input_float.exec_trim(None), expected_text); } #[test] fn test_ltrim() { let input_str = Value::build_text(" Bob and Alice "); let expected_str = Value::build_text("Bob and Alice "); assert_eq!(input_str.exec_ltrim(None), expected_str); let input_str = Value::build_text(" Bob and Alice "); let pattern_str = Value::build_text("Bob and"); let expected_str = Value::build_text("Alice "); assert_eq!(input_str.exec_ltrim(Some(&pattern_str)), expected_str); } #[test] fn test_rtrim() { let input_str = Value::build_text(" Bob and Alice "); let expected_str = Value::build_text(" Bob and Alice"); assert_eq!(input_str.exec_rtrim(None), expected_str); let input_str = Value::build_text(" Bob and Alice "); let pattern_str = Value::build_text("Bob and"); let expected_str = Value::build_text(" Bob and Alice"); assert_eq!(input_str.exec_rtrim(Some(&pattern_str)), expected_str); let input_str = Value::build_text(" Bob and Alice "); let pattern_str = Value::build_text("and Alice"); let expected_str = Value::build_text(" Bob"); assert_eq!(input_str.exec_rtrim(Some(&pattern_str)), expected_str); } #[test] fn test_soundex() { let input_str = Value::build_text("Pfister"); let expected_str = Value::build_text("P236"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("husobee"); let expected_str = Value::build_text("H210"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Tymczak"); let expected_str = Value::build_text("T522"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Ashcraft"); let expected_str = Value::build_text("A261"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Robert"); let expected_str = Value::build_text("R163"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Rupert"); let expected_str = Value::build_text("R163"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Rubin"); let expected_str = Value::build_text("R150"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Kant"); let expected_str = Value::build_text("K530"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("Knuth"); let expected_str = Value::build_text("K530"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("x"); let expected_str = Value::build_text("X000"); assert_eq!(input_str.exec_soundex(), expected_str); let input_str = Value::build_text("闪电五连鞭"); let expected_str = Value::build_text("?000"); assert_eq!(input_str.exec_soundex(), expected_str); } #[test] fn test_upper_case() { let input_str = Value::build_text("Limbo"); let expected_str = Value::build_text("LIMBO"); assert_eq!(input_str.exec_upper().unwrap(), expected_str); let input_int = Value::from_i64(10); assert_eq!(input_int.exec_upper().unwrap(), Value::build_text("10")); assert_eq!(Value::Null.exec_upper(), None) } #[test] fn test_lower_case() { let input_str = Value::build_text("Limbo"); let expected_str = Value::build_text("limbo"); assert_eq!(input_str.exec_lower().unwrap(), expected_str); let input_int = Value::from_i64(10); assert_eq!(input_int.exec_lower().unwrap(), Value::build_text("10")); assert_eq!(Value::Null.exec_lower(), None) } #[test] fn test_hex() { let input_str = Value::build_text("limbo"); let expected_val = Value::build_text("6C696D626F"); assert_eq!(input_str.exec_hex(), expected_val); let input_int = Value::from_i64(100); let expected_val = Value::build_text("313030"); assert_eq!(input_int.exec_hex(), expected_val); let input_float = Value::from_f64(12.34); let expected_val = Value::build_text("31322E3334"); assert_eq!(input_float.exec_hex(), expected_val); let input_blob = Value::Blob(vec![0xff]); let expected_val = Value::build_text("FF"); assert_eq!(input_blob.exec_hex(), expected_val); } #[test] fn test_unhex() { let input = Value::build_text("6f"); let expected = Value::Blob(vec![0x6f]); assert_eq!(input.exec_unhex(None), expected); let input = Value::build_text("6f"); let expected = Value::Blob(vec![0x6f]); assert_eq!(input.exec_unhex(None), expected); let input = Value::build_text("611"); let expected = Value::Null; assert_eq!(input.exec_unhex(None), expected); let input = Value::build_text(""); let expected = Value::Blob(vec![]); assert_eq!(input.exec_unhex(None), expected); let input = Value::build_text("61x"); let expected = Value::Null; assert_eq!(input.exec_unhex(None), expected); let input = Value::Null; let expected = Value::Null; assert_eq!(input.exec_unhex(None), expected); } #[test] fn test_abs() { let int_positive_reg = Value::from_i64(10); let int_negative_reg = Value::from_i64(-10); assert_eq!(int_positive_reg.exec_abs().unwrap(), int_positive_reg); assert_eq!(int_negative_reg.exec_abs().unwrap(), int_positive_reg); let float_positive_reg = Value::from_i64(10); let float_negative_reg = Value::from_i64(-10); assert_eq!(float_positive_reg.exec_abs().unwrap(), float_positive_reg); assert_eq!(float_negative_reg.exec_abs().unwrap(), float_positive_reg); assert_eq!( Value::build_text("a").exec_abs().unwrap(), Value::from_f64(0.0) ); assert_eq!(Value::Null.exec_abs().unwrap(), Value::Null); // ABS(i64::MIN) should return RuntimeError assert!(Value::from_i64(i64::MIN).exec_abs().is_err()); } #[test] fn test_char() { assert_eq!( Value::exec_char( [ Register::Value(Value::from_i64(108)), Register::Value(Value::from_i64(105)) ] .iter() .map(|reg| reg.get_value()) ), Value::build_text("li") ); assert_eq!(Value::exec_char(std::iter::empty()), Value::build_text("")); assert_eq!( Value::exec_char( [Register::Value(Value::Null)] .iter() .map(|reg| reg.get_value()) ), Value::build_text("\0") ); assert_eq!( Value::exec_char( [Register::Value(Value::build_text("a"))] .iter() .map(|reg| reg.get_value()) ), Value::build_text("") ); } #[test] fn test_like_with_escape_or_regexmeta_chars() { assert!(Value::exec_like(r#"\%A"#, r#"\A"#, None).unwrap()); assert!(Value::exec_like("%a%a", "aaaa", None).unwrap()); } #[test] fn test_like_without_escape() { assert!(Value::exec_like("a%", "aaaa", None).unwrap()); assert!(Value::exec_like("%a%a", "aaaa", None).unwrap()); assert!(!Value::exec_like("%a.a", "aaaa", None).unwrap()); assert!(!Value::exec_like("a.a%", "aaaa", None).unwrap()); assert!(!Value::exec_like("%a.ab", "aaaa", None).unwrap()); } #[test] fn test_exec_like_with_escape() { assert!(Value::exec_like("abcX%", "abc%", Some('X')).unwrap()); assert!(!Value::exec_like("abcX%", "abc5", Some('X')).unwrap()); assert!(!Value::exec_like("abcX%", "abc", Some('X')).unwrap()); assert!(!Value::exec_like("abcX%", "abcX%", Some('X')).unwrap()); assert!(!Value::exec_like("abcX%", "abc%%", Some('X')).unwrap()); assert!(Value::exec_like("abcX_", "abc_", Some('X')).unwrap()); assert!(!Value::exec_like("abcX_", "abc5", Some('X')).unwrap()); assert!(!Value::exec_like("abcX_", "abc", Some('X')).unwrap()); assert!(!Value::exec_like("abcX_", "abcX_", Some('X')).unwrap()); assert!(!Value::exec_like("abcX_", "abc__", Some('X')).unwrap()); assert!(Value::exec_like("abcXX", "abcX", Some('X')).unwrap()); assert!(!Value::exec_like("abcXX", "abc5", Some('X')).unwrap()); assert!(!Value::exec_like("abcXX", "abc", Some('X')).unwrap()); assert!(!Value::exec_like("abcXX", "abcXX", Some('X')).unwrap()); } #[test] fn test_glob() { assert!(Value::exec_glob(r#"?*/abc/?*"#, r#"x//a/ab/abc/y"#).unwrap()); assert!(Value::exec_glob(r#"a[1^]"#, r#"a1"#).unwrap()); assert!(Value::exec_glob(r#"a[1^]*"#, r#"a^"#).unwrap()); assert!(!Value::exec_glob(r#"a[a*"#, r#"a["#).unwrap()); assert!(!Value::exec_glob(r#"a[a"#, r#"a[a"#).unwrap()); assert!(Value::exec_glob(r#"a[[]"#, r#"a["#).unwrap()); assert!(Value::exec_glob(r#"abc[^][*?]efg"#, r#"abcdefg"#).unwrap()); assert!(!Value::exec_glob(r#"abc[^][*?]efg"#, r#"abc]efg"#).unwrap()); } #[test] fn test_random() { match Value::exec_random(|| rand::rng().random()) { Value::Numeric(Numeric::Integer(value)) => { // Check that the value is within the range of i64 assert!( (i64::MIN..=i64::MAX).contains(&value), "Random number out of range" ); } _ => panic!("exec_random did not return an Integer variant"), } } #[test] fn test_exec_randomblob() { struct TestCase { input: Value, expected_len: usize, } let test_cases = vec![ TestCase { input: Value::from_i64(5), expected_len: 5, }, TestCase { input: Value::from_i64(0), expected_len: 1, }, TestCase { input: Value::from_i64(-1), expected_len: 1, }, TestCase { input: Value::build_text(""), expected_len: 1, }, TestCase { input: Value::build_text("5"), expected_len: 5, }, TestCase { input: Value::build_text("0"), expected_len: 1, }, TestCase { input: Value::build_text("-1"), expected_len: 1, }, TestCase { input: Value::from_f64(2.9), expected_len: 2, }, TestCase { input: Value::from_f64(-3.15), expected_len: 1, }, TestCase { input: Value::Null, expected_len: 1, }, ]; for test_case in &test_cases { let result = test_case .input .exec_randomblob(|dest| { rand::rng().fill_bytes(dest); }) .unwrap(); match result { Value::Blob(blob) => { assert_eq!(blob.len(), test_case.expected_len); } _ => panic!("exec_randomblob did not return a Blob variant"), } } // Test TooBig error let input = Value::from_i64(Value::MAX_BLOB_LENGTH + 1); assert!(input.exec_randomblob(|_| {}).is_err()); } #[test] fn test_exec_round() { let input_val = Value::from_f64(123.456); let expected_val = Value::from_f64(123.0); assert_eq!(input_val.exec_round(None), expected_val); let input_val = Value::from_f64(123.456); let precision_val = Value::from_i64(2); let expected_val = Value::from_f64(123.46); assert_eq!(input_val.exec_round(Some(&precision_val)), expected_val); let input_val = Value::from_f64(123.456); let precision_val = Value::build_text("1"); let expected_val = Value::from_f64(123.5); assert_eq!(input_val.exec_round(Some(&precision_val)), expected_val); let input_val = Value::build_text("123.456"); let precision_val = Value::from_i64(2); let expected_val = Value::from_f64(123.46); assert_eq!(input_val.exec_round(Some(&precision_val)), expected_val); let input_val = Value::from_i64(123); let precision_val = Value::from_i64(1); let expected_val = Value::from_f64(123.0); assert_eq!(input_val.exec_round(Some(&precision_val)), expected_val); let input_val = Value::from_f64(100.123); let expected_val = Value::from_f64(100.0); assert_eq!(input_val.exec_round(None), expected_val); let input_val = Value::from_f64(100.123); let expected_val = Value::Null; assert_eq!(input_val.exec_round(Some(&Value::Null)), expected_val); } #[test] fn test_exec_if() { let reg = Value::from_i64(0); assert!(!reg.exec_if(false, false)); assert!(reg.exec_if(false, true)); let reg = Value::from_i64(1); assert!(reg.exec_if(false, false)); assert!(!reg.exec_if(false, true)); let reg = Value::Null; assert!(!reg.exec_if(false, false)); assert!(!reg.exec_if(false, true)); let reg = Value::Null; assert!(reg.exec_if(true, false)); assert!(reg.exec_if(true, true)); let reg = Value::Null; assert!(!reg.exec_if(false, false)); assert!(!reg.exec_if(false, true)); } #[test] fn test_nullif() { assert_eq!( Value::from_i64(1).exec_nullif(&Value::from_i64(1)), Value::Null ); assert_eq!( Value::from_f64(1.1).exec_nullif(&Value::from_f64(1.1)), Value::Null ); assert_eq!( Value::build_text("limbo").exec_nullif(&Value::build_text("limbo")), Value::Null ); assert_eq!( Value::from_i64(1).exec_nullif(&Value::from_i64(2)), Value::from_i64(1) ); assert_eq!( Value::from_f64(1.1).exec_nullif(&Value::from_f64(1.2)), Value::from_f64(1.1) ); assert_eq!( Value::build_text("limbo").exec_nullif(&Value::build_text("limb")), Value::build_text("limbo") ); } #[test] fn test_substring() { let str_value = Value::build_text("limbo"); let start_value = Value::from_i64(1); let length_value = Value::from_i64(3); let expected_val = Value::build_text("lim"); assert_eq!( Value::exec_substring(&str_value, &start_value, Some(&length_value)), expected_val ); let str_value = Value::build_text("limbo"); let start_value = Value::from_i64(1); let length_value = Value::from_i64(10); let expected_val = Value::build_text("limbo"); assert_eq!( Value::exec_substring(&str_value, &start_value, Some(&length_value)), expected_val ); let str_value = Value::build_text("limbo"); let start_value = Value::from_i64(10); let length_value = Value::from_i64(3); let expected_val = Value::build_text(""); assert_eq!( Value::exec_substring(&str_value, &start_value, Some(&length_value)), expected_val ); let str_value = Value::build_text("limbo"); let start_value = Value::from_i64(3); let length_value = Value::Null; let expected_val = Value::Null; assert_eq!( Value::exec_substring(&str_value, &start_value, Some(&length_value)), expected_val ); let str_value = Value::build_text("limbo"); let start_value = Value::from_i64(10); let length_value = Value::Null; let expected_val = Value::Null; assert_eq!( Value::exec_substring(&str_value, &start_value, Some(&length_value)), expected_val ); } #[test] fn test_exec_instr() { let input = Value::build_text("limbo"); let pattern = Value::build_text("im"); let expected = Value::from_i64(2); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("limbo"); let pattern = Value::build_text("limbo"); let expected = Value::from_i64(1); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("limbo"); let pattern = Value::build_text("o"); let expected = Value::from_i64(5); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("liiiiimbo"); let pattern = Value::build_text("ii"); let expected = Value::from_i64(2); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("limbo"); let pattern = Value::build_text("limboX"); let expected = Value::from_i64(0); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("limbo"); let pattern = Value::build_text(""); let expected = Value::from_i64(1); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text(""); let pattern = Value::build_text("limbo"); let expected = Value::from_i64(0); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text(""); let pattern = Value::build_text(""); let expected = Value::from_i64(1); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Null; let pattern = Value::Null; let expected = Value::Null; assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("limbo"); let pattern = Value::Null; let expected = Value::Null; assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Null; let pattern = Value::build_text("limbo"); let expected = Value::Null; assert_eq!(input.exec_instr(&pattern), expected); let input = Value::from_i64(123); let pattern = Value::from_i64(2); let expected = Value::from_i64(2); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::from_i64(123); let pattern = Value::from_i64(5); let expected = Value::from_i64(0); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::from_f64(12.34); let pattern = Value::from_f64(2.3); let expected = Value::from_i64(2); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::from_f64(12.34); let pattern = Value::from_f64(5.6); let expected = Value::from_i64(0); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::from_f64(12.34); let pattern = Value::build_text("."); let expected = Value::from_i64(3); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Blob(vec![1, 2, 3, 4, 5]); let pattern = Value::Blob(vec![3, 4]); let expected = Value::from_i64(3); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Blob(vec![1, 2, 3, 4, 5]); let pattern = Value::Blob(vec![3, 2]); let expected = Value::from_i64(0); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::Blob(vec![0x61, 0x62, 0x63, 0x64, 0x65]); let pattern = Value::build_text("cd"); let expected = Value::from_i64(3); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("abcde"); let pattern = Value::Blob(vec![0x63, 0x64]); let expected = Value::from_i64(3); assert_eq!(input.exec_instr(&pattern), expected); let input = Value::build_text("abcde"); let pattern = Value::build_text(""); let expected = Value::from_i64(1); assert_eq!(input.exec_instr(&pattern), expected); } #[test] fn test_exec_sign() { let input = Value::from_i64(42); let expected = Some(Value::from_i64(1)); assert_eq!(input.exec_sign(), expected); let input = Value::from_i64(-42); let expected = Some(Value::from_i64(-1)); assert_eq!(input.exec_sign(), expected); let input = Value::from_i64(0); let expected = Some(Value::from_i64(0)); assert_eq!(input.exec_sign(), expected); let input = Value::from_f64(0.0); let expected = Some(Value::from_i64(0)); assert_eq!(input.exec_sign(), expected); let input = Value::from_f64(0.1); let expected = Some(Value::from_i64(1)); assert_eq!(input.exec_sign(), expected); let input = Value::from_f64(42.0); let expected = Some(Value::from_i64(1)); assert_eq!(input.exec_sign(), expected); let input = Value::from_f64(-42.0); let expected = Some(Value::from_i64(-1)); assert_eq!(input.exec_sign(), expected); let input = Value::build_text("abc"); let expected = None; assert_eq!(input.exec_sign(), expected); let input = Value::build_text("42"); let expected = Some(Value::from_i64(1)); assert_eq!(input.exec_sign(), expected); let input = Value::build_text("-42"); let expected = Some(Value::from_i64(-1)); assert_eq!(input.exec_sign(), expected); let input = Value::build_text("0"); let expected = Some(Value::from_i64(0)); assert_eq!(input.exec_sign(), expected); let input = Value::Blob(b"abc".to_vec()); let expected = None; assert_eq!(input.exec_sign(), expected); let input = Value::Blob(b"42".to_vec()); let expected = None; assert_eq!(input.exec_sign(), expected); let input = Value::Blob(b"-42".to_vec()); let expected = None; assert_eq!(input.exec_sign(), expected); let input = Value::Blob(b"0".to_vec()); let expected = None; assert_eq!(input.exec_sign(), expected); let input = Value::Null; let expected = None; assert_eq!(input.exec_sign(), expected); } #[test] fn test_exec_zeroblob() { let input = Value::from_i64(0); let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob().unwrap(), expected); let input = Value::Null; let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob().unwrap(), expected); let input = Value::from_i64(4); let expected = Value::Blob(vec![0; 4]); assert_eq!(input.exec_zeroblob().unwrap(), expected); let input = Value::from_i64(-1); let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob().unwrap(), expected); let input = Value::build_text("5"); let expected = Value::Blob(vec![0; 5]); assert_eq!(input.exec_zeroblob().unwrap(), expected); let input = Value::build_text("-5"); let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob().unwrap(), expected); let input = Value::build_text("text"); let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob().unwrap(), expected); let input = Value::from_f64(2.6); let expected = Value::Blob(vec![0; 2]); assert_eq!(input.exec_zeroblob().unwrap(), expected); let input = Value::Blob(vec![1]); let expected = Value::Blob(vec![]); assert_eq!(input.exec_zeroblob().unwrap(), expected); // Test TooBig error let input = Value::from_i64(Value::MAX_BLOB_LENGTH + 1); assert!(input.exec_zeroblob().is_err()); } #[test] fn test_replace() { let input_str = Value::build_text("bob"); let pattern_str = Value::build_text("b"); let replace_str = Value::build_text("a"); let expected_str = Value::build_text("aoa"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bob"); let pattern_str = Value::build_text("b"); let replace_str = Value::build_text(""); let expected_str = Value::build_text("o"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bob"); let pattern_str = Value::build_text("b"); let replace_str = Value::build_text("abc"); let expected_str = Value::build_text("abcoabc"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bob"); let pattern_str = Value::build_text("a"); let replace_str = Value::build_text("b"); let expected_str = Value::build_text("bob"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bob"); let pattern_str = Value::build_text(""); let replace_str = Value::build_text("a"); let expected_str = Value::build_text("bob"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bob"); let pattern_str = Value::Null; let replace_str = Value::build_text("a"); let expected_str = Value::Null; assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bo5"); let pattern_str = Value::from_i64(5); let replace_str = Value::build_text("a"); let expected_str = Value::build_text("boa"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bo5.0"); let pattern_str = Value::from_f64(5.0); let replace_str = Value::build_text("a"); let expected_str = Value::build_text("boa"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bo5"); let pattern_str = Value::from_f64(5.0); let replace_str = Value::build_text("a"); let expected_str = Value::build_text("bo5"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); let input_str = Value::build_text("bo5.0"); let pattern_str = Value::from_f64(5.0); let replace_str = Value::from_f64(6.0); let expected_str = Value::build_text("bo6.0"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); // todo: change this test to use (0.1 + 0.2) instead of 0.3 when decimals are implemented. let input_str = Value::build_text("tes3"); let pattern_str = Value::from_i64(3); let replace_str = Value::from_f64(0.3); let expected_str = Value::build_text("tes0.3"); assert_eq!( Value::exec_replace(&input_str, &pattern_str, &replace_str), expected_str ); } } ================================================ FILE: core/vector/mod.rs ================================================ use crate::types::AsValueRef; use crate::types::Value; use crate::types::ValueType; use crate::vdbe::Register; use crate::LimboError; use crate::Result; use crate::ValueRef; pub mod operations; pub mod vector_types; use vector_types::*; pub fn parse_vector<'a>( value: &'a (impl AsValueRef + 'a), type_hint: Option, ) -> Result> { let value = value.as_value_ref(); match value.value_type() { ValueType::Text => operations::text::vector_from_text( type_hint.unwrap_or(VectorType::Float32Dense), value.to_text().expect("value must be text"), ), ValueType::Blob => { let Some(blob) = value.to_blob() else { return Err(LimboError::ConversionError( "Invalid vector value".to_string(), )); }; Vector::from_slice(blob) } _ => Err(LimboError::ConversionError( "Invalid vector type".to_string(), )), } } pub fn vector32(args: &[Register]) -> Result { if args.len() != 1 { return Err(LimboError::ConversionError( "vector32 requires exactly one argument".to_string(), )); } let value = args[0].get_value(); let vector = parse_vector(value, Some(VectorType::Float32Dense))?; let vector = operations::convert::vector_convert(vector, VectorType::Float32Dense)?; Ok(operations::serialize::vector_serialize(vector)) } pub fn vector32_sparse(args: &[Register]) -> Result { if args.len() != 1 { return Err(LimboError::ConversionError( "vector32_sparse requires exactly one argument".to_string(), )); } let value = args[0].get_value(); let vector = parse_vector(value, Some(VectorType::Float32Sparse))?; let vector = operations::convert::vector_convert(vector, VectorType::Float32Sparse)?; Ok(operations::serialize::vector_serialize(vector)) } pub fn vector64(args: &[Register]) -> Result { if args.len() != 1 { return Err(LimboError::ConversionError( "vector64 requires exactly one argument".to_string(), )); } let value = args[0].get_value(); let vector = parse_vector(value, Some(VectorType::Float64Dense))?; let vector = operations::convert::vector_convert(vector, VectorType::Float64Dense)?; Ok(operations::serialize::vector_serialize(vector)) } pub fn vector8(args: &[Register]) -> Result { if args.len() != 1 { return Err(LimboError::ConversionError( "vector8 requires exactly one argument".to_string(), )); } let value = args[0].get_value(); let vector = parse_vector(value, Some(VectorType::Float8))?; let vector = operations::convert::vector_convert(vector, VectorType::Float8)?; Ok(operations::serialize::vector_serialize(vector)) } pub fn vector1bit(args: &[Register]) -> Result { if args.len() != 1 { return Err(LimboError::ConversionError( "vector1bit requires exactly one argument".to_string(), )); } let value = args[0].get_value(); let vector = parse_vector(value, Some(VectorType::Float1Bit))?; let vector = operations::convert::vector_convert(vector, VectorType::Float1Bit)?; Ok(operations::serialize::vector_serialize(vector)) } pub fn vector_extract(args: &[Register]) -> Result { if args.len() != 1 { return Err(LimboError::ConversionError( "vector_extract requires exactly one argument".to_string(), )); } let value = args[0].get_value().as_value_ref(); let blob = match value { ValueRef::Blob(b) => b, _ => { return Err(LimboError::ConversionError( "Expected blob value".to_string(), )) } }; if blob.is_empty() { return Ok(Value::build_text("[]")); } let vector = Vector::from_slice(blob)?; Ok(Value::build_text(operations::text::vector_to_text(&vector))) } pub fn vector_distance_cos(args: &[Register]) -> Result { if args.len() != 2 { return Err(LimboError::ConversionError( "vector_distance_cos requires exactly two arguments".to_string(), )); } let value_0 = args[0].get_value(); let value_1 = args[1].get_value(); let x = parse_vector(value_0, None)?; let y = parse_vector(value_1, None)?; let dist = operations::distance_cos::vector_distance_cos(&x, &y)?; Ok(Value::from_f64(dist)) } pub fn vector_distance_l2(args: &[Register]) -> Result { if args.len() != 2 { return Err(LimboError::ConversionError( "distance_l2 requires exactly two arguments".to_string(), )); } let value_0 = args[0].get_value(); let value_1 = args[1].get_value(); let x = parse_vector(value_0, None)?; let y = parse_vector(value_1, None)?; let dist = operations::distance_l2::vector_distance_l2(&x, &y)?; Ok(Value::from_f64(dist)) } pub fn vector_distance_jaccard(args: &[Register]) -> Result { if args.len() != 2 { return Err(LimboError::ConversionError( "distance_jaccard requires exactly two arguments".to_string(), )); } let value_0 = args[0].get_value(); let value_1 = args[1].get_value(); let x = parse_vector(value_0, None)?; let y = parse_vector(value_1, None)?; let dist = operations::jaccard::vector_distance_jaccard(&x, &y)?; Ok(Value::from_f64(dist)) } pub fn vector_distance_dot(args: &[Register]) -> Result { if args.len() != 2 { return Err(LimboError::ConversionError( "distance_dot requires exactly two arguments".to_string(), )); } let value_0 = args[0].get_value(); let value_1 = args[1].get_value(); let x = parse_vector(value_0, None)?; let y = parse_vector(value_1, None)?; let dist = operations::distance_dot::vector_distance_dot(&x, &y)?; Ok(Value::from_f64(dist)) } pub fn vector_concat(args: &[Register]) -> Result { if args.len() != 2 { return Err(LimboError::InvalidArgument( "concat requires exactly two arguments".into(), )); } let value_0 = args[0].get_value(); let value_1 = args[1].get_value(); let x = parse_vector(value_0, None)?; let y = parse_vector(value_1, None)?; let vector = operations::concat::vector_concat(&x, &y)?; Ok(operations::serialize::vector_serialize(vector)) } pub fn vector_slice(args: &[Register]) -> Result { if args.len() != 3 { return Err(LimboError::InvalidArgument( "vector_slice requires exactly three arguments".into(), )); } let value_0 = args[0].get_value(); let value_1 = args[1].get_value().as_value_ref(); let value_2 = args[2].get_value().as_value_ref(); let vector = parse_vector(value_0, None)?; let start_index = value_1 .as_int() .ok_or_else(|| LimboError::InvalidArgument("start index must be an integer".into()))?; let end_index = value_2 .as_int() .ok_or_else(|| LimboError::InvalidArgument("end_index must be an integer".into()))?; if start_index < 0 || end_index < 0 { return Err(LimboError::InvalidArgument( "start index and end_index must be non-negative".into(), )); } let result = operations::slice::vector_slice(&vector, start_index as usize, end_index as usize)?; Ok(operations::serialize::vector_serialize(result)) } ================================================ FILE: core/vector/operations/concat.rs ================================================ use crate::{ vector::vector_types::{Vector, VectorType}, LimboError, Result, }; pub fn vector_concat(v1: &Vector, v2: &Vector) -> Result> { if v1.vector_type != v2.vector_type { return Err(LimboError::ConversionError( "Mismatched vector types".into(), )); } let data = match v1.vector_type { VectorType::Float32Dense | VectorType::Float64Dense => { let mut data = Vec::with_capacity(v1.bin_len() + v2.bin_len()); data.extend_from_slice(v1.bin_data()); data.extend_from_slice(v2.bin_data()); data } VectorType::Float32Sparse => { let mut data = Vec::with_capacity(v1.bin_len() + v2.bin_len()); data.extend_from_slice(&v1.bin_data()[..v1.bin_len() / 2]); data.extend_from_slice(&v2.bin_data()[..v2.bin_len() / 2]); data.extend_from_slice(&v1.bin_data()[v1.bin_len() / 2..]); data.extend_from_slice(&v2.bin_data()[v2.bin_len() / 2..]); data } VectorType::Float1Bit | VectorType::Float8 => { return Err(LimboError::ConversionError( "vector_concat is not supported for float1bit/float8 vectors".to_string(), )); } }; Ok(Vector { vector_type: v1.vector_type, dims: v1.dims + v2.dims, owned: Some(data), refer: None, }) } #[cfg(test)] mod tests { use crate::vector::{ operations::concat::vector_concat, vector_types::{Vector, VectorType}, }; fn float32_vec_from(slice: &[f32]) -> Vector<'static> { let mut data = Vec::new(); for &v in slice { data.extend_from_slice(&v.to_le_bytes()); } Vector { vector_type: VectorType::Float32Dense, dims: slice.len(), owned: Some(data), refer: None, } } fn f32_slice_from_vector(vector: &Vector) -> Vec { vector.as_f32_slice().to_vec() } #[test] fn test_vector_concat_normal_case() { let v1 = float32_vec_from(&[1.0, 2.0, 3.0]); let v2 = float32_vec_from(&[4.0, 5.0, 6.0]); let result = vector_concat(&v1, &v2).unwrap(); assert_eq!(result.dims, 6); assert_eq!(result.vector_type, VectorType::Float32Dense); assert_eq!( f32_slice_from_vector(&result), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0] ); } #[test] fn test_vector_concat_empty_left() { let v1 = float32_vec_from(&[]); let v2 = float32_vec_from(&[4.0, 5.0]); let result = vector_concat(&v1, &v2).unwrap(); assert_eq!(result.dims, 2); assert_eq!(f32_slice_from_vector(&result), vec![4.0, 5.0]); } #[test] fn test_vector_concat_empty_right() { let v1 = float32_vec_from(&[1.0, 2.0]); let v2 = float32_vec_from(&[]); let result = vector_concat(&v1, &v2).unwrap(); assert_eq!(result.dims, 2); assert_eq!(f32_slice_from_vector(&result), vec![1.0, 2.0]); } #[test] fn test_vector_concat_both_empty() { let v1 = float32_vec_from(&[]); let v2 = float32_vec_from(&[]); let result = vector_concat(&v1, &v2).unwrap(); assert_eq!(result.dims, 0); assert_eq!(f32_slice_from_vector(&result), Vec::::new()); } #[test] fn test_vector_concat_different_lengths() { let v1 = float32_vec_from(&[1.0]); let v2 = float32_vec_from(&[2.0, 3.0, 4.0]); let result = vector_concat(&v1, &v2).unwrap(); assert_eq!(result.dims, 4); assert_eq!(f32_slice_from_vector(&result), vec![1.0, 2.0, 3.0, 4.0]); } } ================================================ FILE: core/vector/operations/convert.rs ================================================ use crate::{ vector::vector_types::{Vector, VectorType}, Result, }; pub fn vector_convert(v: Vector, target_type: VectorType) -> Result { if v.vector_type == target_type { return Ok(v); } match (v.vector_type, target_type) { (VectorType::Float32Dense, VectorType::Float64Dense) => Ok(Vector::from_f64( v.as_f32_slice().iter().map(|&x| x as f64).collect(), )), (VectorType::Float64Dense, VectorType::Float32Dense) => Ok(Vector::from_f32( v.as_f64_slice().iter().map(|&x| x as f32).collect(), )), (VectorType::Float32Dense, VectorType::Float32Sparse) => { let (mut idx, mut values) = (Vec::new(), Vec::new()); for (i, &value) in v.as_f32_slice().iter().enumerate() { if value == 0.0 { continue; } idx.push(i as u32); values.push(value); } Ok(Vector::from_f32_sparse(v.dims, values, idx)) } (VectorType::Float64Dense, VectorType::Float32Sparse) => { let (mut idx, mut values) = (Vec::new(), Vec::new()); for (i, &value) in v.as_f64_slice().iter().enumerate() { if value == 0.0 { continue; } idx.push(i as u32); values.push(value as f32); } Ok(Vector::from_f32_sparse(v.dims, values, idx)) } (VectorType::Float32Sparse, VectorType::Float32Dense) => { let sparse = v.as_f32_sparse(); let mut data = vec![0f32; v.dims]; for (&i, &value) in sparse.idx.iter().zip(sparse.values.iter()) { data[i as usize] = value; } Ok(Vector::from_f32(data)) } (VectorType::Float32Sparse, VectorType::Float64Dense) => { let sparse = v.as_f32_sparse(); let mut data = vec![0f64; v.dims]; for (&i, &value) in sparse.idx.iter().zip(sparse.values.iter()) { data[i as usize] = value as f64; } Ok(Vector::from_f64(data)) } // Float1Bit conversions (VectorType::Float32Dense, VectorType::Float1Bit) => { let dims = v.dims; let byte_count = dims.div_ceil(8); let mut bits = vec![0u8; byte_count]; for (i, &val) in v.as_f32_slice().iter().enumerate() { if val > 0.0 { bits[i / 8] |= 1 << (i & 7); } } Ok(Vector::from_1bit(dims, bits)) } (VectorType::Float64Dense, VectorType::Float1Bit) => { let dims = v.dims; let byte_count = dims.div_ceil(8); let mut bits = vec![0u8; byte_count]; for (i, &val) in v.as_f64_slice().iter().enumerate() { if val > 0.0 { bits[i / 8] |= 1 << (i & 7); } } Ok(Vector::from_1bit(dims, bits)) } (VectorType::Float1Bit, VectorType::Float32Dense) => { let data = v.as_1bit_data(); let floats: Vec = (0..v.dims) .map(|i| { if (data[i / 8] >> (i & 7)) & 1 == 1 { 1.0 } else { -1.0 } }) .collect(); Ok(Vector::from_f32(floats)) } (VectorType::Float1Bit, VectorType::Float64Dense) => { let data = v.as_1bit_data(); let floats: Vec = (0..v.dims) .map(|i| { if (data[i / 8] >> (i & 7)) & 1 == 1 { 1.0 } else { -1.0 } }) .collect(); Ok(Vector::from_f64(floats)) } // Float8 conversions (VectorType::Float32Dense, VectorType::Float8) => { convert_floats_to_f8(v.as_f32_slice().iter().copied(), v.dims) } (VectorType::Float64Dense, VectorType::Float8) => { convert_floats_to_f8(v.as_f64_slice().iter().map(|&x| x as f32), v.dims) } (VectorType::Float8, VectorType::Float32Dense) => { let (quantized, alpha, shift) = v.as_f8_data(); let floats: Vec = quantized .iter() .map(|&q| alpha * q as f32 + shift) .collect(); Ok(Vector::from_f32(floats)) } (VectorType::Float8, VectorType::Float64Dense) => { let (quantized, alpha, shift) = v.as_f8_data(); let floats: Vec = quantized .iter() .map(|&q| alpha as f64 * q as f64 + shift as f64) .collect(); Ok(Vector::from_f64(floats)) } // Cross-conversions via intermediate (VectorType::Float1Bit, VectorType::Float8) => { let f32_vec = vector_convert(v, VectorType::Float32Dense)?; vector_convert(f32_vec, VectorType::Float8) } (VectorType::Float8, VectorType::Float1Bit) => { let f32_vec = vector_convert(v, VectorType::Float32Dense)?; vector_convert(f32_vec, VectorType::Float1Bit) } (VectorType::Float1Bit, VectorType::Float32Sparse) | (VectorType::Float8, VectorType::Float32Sparse) | (VectorType::Float32Sparse, VectorType::Float1Bit) | (VectorType::Float32Sparse, VectorType::Float8) => { let f32_vec = vector_convert(v, VectorType::Float32Dense)?; vector_convert(f32_vec, target_type) } _ => unreachable!( "unexpected conversion: {:?} -> {:?}", v.vector_type, target_type ), } } fn convert_floats_to_f8( values: impl Iterator + Clone, dims: usize, ) -> Result> { if dims == 0 { return Ok(Vector::from_f8(0, Vec::new(), 0.0, 0.0)); } let mut min_val = f32::INFINITY; let mut max_val = f32::NEG_INFINITY; for val in values.clone() { if val < min_val { min_val = val; } if val > max_val { max_val = val; } } let alpha = (max_val - min_val) / 255.0; let shift = min_val; let mut quantized = Vec::with_capacity(dims); for val in values { let q = if alpha == 0.0 { 0u8 } else { let v = (val - shift) / alpha + 0.5; (v as i32).clamp(0, 255) as u8 }; quantized.push(q); } Ok(Vector::from_f8(dims, quantized, alpha, shift)) } #[cfg(test)] mod tests { use crate::vector::{ operations::convert::vector_convert, vector_types::{tests::ArbitraryVector, Vector, VectorType}, }; use quickcheck_macros::quickcheck; fn concat(data: &[[u8; N]]) -> Vec { data.iter().flatten().cloned().collect::>() } fn assert_vectors(v1: &Vector, v2: &Vector) { assert_eq!(v1.vector_type, v2.vector_type); assert_eq!(v1.dims, v2.dims); assert_eq!(v1.bin_data(), v2.bin_data()); } fn clone_vector(v: &Vector) -> Vector<'static> { Vector { vector_type: v.vector_type, dims: v.dims, owned: Some(v.bin_data().to_vec()), refer: None, } } #[test] pub fn test_vector_convert() { let vf32 = Vector { vector_type: VectorType::Float32Dense, dims: 3, owned: Some(concat(&[ 1.0f32.to_le_bytes(), 0.0f32.to_le_bytes(), 2.0f32.to_le_bytes(), ])), refer: None, }; let vf64 = Vector { vector_type: VectorType::Float64Dense, dims: 3, owned: Some(concat(&[ 1.0f64.to_le_bytes(), 0.0f64.to_le_bytes(), 2.0f64.to_le_bytes(), ])), refer: None, }; let vf32_sparse = Vector { vector_type: VectorType::Float32Sparse, dims: 3, owned: Some(concat(&[ 1.0f32.to_le_bytes(), 2.0f32.to_le_bytes(), 0u32.to_le_bytes(), 2u32.to_le_bytes(), ])), refer: None, }; let vectors = [vf32, vf64, vf32_sparse]; for v1 in &vectors { for v2 in &vectors { println!("{:?} -> {:?}", v1.vector_type, v2.vector_type); let v_copy = Vector { vector_type: v1.vector_type, dims: v1.dims, owned: v1.owned.clone(), refer: None, }; assert_vectors(&vector_convert(v_copy, v2.vector_type).unwrap(), v2); } } } /// Test that all 5x5 type conversions succeed and produce correct type/dims. #[test] pub fn test_vector_convert_all_types() { let source = Vector::from_f32(vec![1.0, 0.5, 2.0]); let all_types = [ VectorType::Float32Dense, VectorType::Float64Dense, VectorType::Float32Sparse, VectorType::Float1Bit, VectorType::Float8, ]; for &src_type in &all_types { let src = vector_convert(clone_vector(&source), src_type).unwrap(); for &dst_type in &all_types { let result = vector_convert(clone_vector(&src), dst_type); assert!( result.is_ok(), "conversion {:?} -> {:?} failed: {:?}", src_type, dst_type, result.err() ); let converted = result.unwrap(); assert_eq!(converted.vector_type, dst_type); assert_eq!(converted.dims, 3); } } } /// Lossless conversions roundtrip exactly. #[test] pub fn test_vector_convert_lossless_roundtrip() { let vf32 = Vector::from_f32(vec![1.0, 0.0, 2.0]); // f32 -> f64 -> f32 is exact let via_f64 = vector_convert( vector_convert(clone_vector(&vf32), VectorType::Float64Dense).unwrap(), VectorType::Float32Dense, ) .unwrap(); assert_eq!(vf32.bin_data(), via_f64.bin_data()); // f32 -> sparse -> f32 is exact let via_sparse = vector_convert( vector_convert(clone_vector(&vf32), VectorType::Float32Sparse).unwrap(), VectorType::Float32Dense, ) .unwrap(); assert_eq!(vf32.bin_data(), via_sparse.bin_data()); } /// Float1Bit roundtrip preserves sign information: positive → 1, non-positive → -1. #[quickcheck] fn prop_vector_convert_1bit_roundtrip(v: ArbitraryVector<100>) -> bool { let v_f32 = vector_convert(v.into(), VectorType::Float32Dense).unwrap(); let orig_slice = v_f32.as_f32_slice().to_vec(); let v_1bit = vector_convert(v_f32, VectorType::Float1Bit).unwrap(); let v_back = vector_convert(v_1bit, VectorType::Float32Dense).unwrap(); let back_slice = v_back.as_f32_slice(); for i in 0..100 { let expected = if orig_slice[i] > 0.0 { 1.0f32 } else { -1.0f32 }; if back_slice[i] != expected { return false; } } true } /// Float8 roundtrip approximately preserves values within one quantization step. #[quickcheck] fn prop_vector_convert_f8_roundtrip(v: ArbitraryVector<100>) -> bool { let v_f32 = vector_convert(v.into(), VectorType::Float32Dense).unwrap(); let orig_slice = v_f32.as_f32_slice().to_vec(); let v_f8 = vector_convert(v_f32, VectorType::Float8).unwrap(); let v_back = vector_convert(v_f8, VectorType::Float32Dense).unwrap(); let back_slice = v_back.as_f32_slice(); let min_val = orig_slice.iter().cloned().fold(f32::INFINITY, f32::min); let max_val = orig_slice.iter().cloned().fold(f32::NEG_INFINITY, f32::max); let alpha = (max_val - min_val) / 255.0; let tolerance = alpha + 1e-6; for i in 0..100 { if (orig_slice[i] - back_slice[i]).abs() > tolerance { return false; } } true } /// All type-pair conversions succeed for arbitrary vectors. #[quickcheck] fn prop_vector_convert_all_pairs(v: ArbitraryVector<16>) -> bool { let v: Vector = v.into(); let all_types = [ VectorType::Float32Dense, VectorType::Float64Dense, VectorType::Float32Sparse, VectorType::Float1Bit, VectorType::Float8, ]; for &target_type in &all_types { if vector_convert(clone_vector(&v), target_type).is_err() { return false; } } true } #[test] fn test_vector_convert_empty_to_f8() { let empty_f32 = Vector::from_f32(vec![]); let f8 = vector_convert(empty_f32, VectorType::Float8).unwrap(); assert_eq!(f8.dims, 0); assert_eq!(f8.vector_type, VectorType::Float8); let (quantized, alpha, shift) = f8.as_f8_data(); assert!(quantized.is_empty()); assert_eq!(alpha, 0.0); assert_eq!(shift, 0.0); } #[test] fn test_vector_convert_empty_f8_to_f32() { let empty_f8 = Vector::from_f8(0, Vec::new(), 0.0, 0.0); let f32_vec = vector_convert(empty_f8, VectorType::Float32Dense).unwrap(); assert_eq!(f32_vec.dims, 0); assert!(f32_vec.as_f32_slice().is_empty()); } } ================================================ FILE: core/vector/operations/distance_cos.rs ================================================ use crate::{ vector::vector_types::{Vector, VectorSparse, VectorType}, LimboError, Result, }; #[cfg(not(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") )))] use simsimd::SpatialSimilarity; pub fn vector_distance_cos(v1: &Vector, v2: &Vector) -> Result { if v1.dims != v2.dims { return Err(LimboError::ConversionError( "Vectors must have the same dimensions".to_string(), )); } if v1.vector_type != v2.vector_type { return Err(LimboError::ConversionError( "Vectors must be of the same type".to_string(), )); } match v1.vector_type { VectorType::Float32Dense => Ok(vector_f32_distance_cos_simsimd( v1.as_f32_slice(), v2.as_f32_slice(), )), VectorType::Float64Dense => Ok(vector_f64_distance_cos_simsimd( v1.as_f64_slice(), v2.as_f64_slice(), )), VectorType::Float32Sparse => Ok(vector_f32_sparse_distance_cos( v1.as_f32_sparse(), v2.as_f32_sparse(), )), VectorType::Float1Bit => Ok(vector_1bit_distance_cos(v1, v2)), VectorType::Float8 => Ok(vector_f8_distance_cos(v1, v2)), } } fn vector_1bit_distance_cos(v1: &Vector, v2: &Vector) -> f64 { // For 1-bit vectors, cosine distance returns hamming distance (matching libsql) let d1 = v1.as_1bit_data(); let d2 = v2.as_1bit_data(); let mut hamming = 0u32; for (&a, &b) in d1.iter().zip(d2.iter()) { hamming += (a ^ b).count_ones(); } hamming as f64 } fn vector_f8_distance_cos(v1: &Vector, v2: &Vector) -> f64 { let (data1, alpha1, shift1) = v1.as_f8_data(); let (data2, alpha2, shift2) = v2.as_f8_data(); let dims = v1.dims; let (mut sum1, mut sum2, mut sumsq1, mut sumsq2, mut doti) = (0u64, 0u64, 0u64, 0u64, 0u64); for i in 0..dims { let q1 = data1[i] as u64; let q2 = data2[i] as u64; sum1 += q1; sum2 += q2; sumsq1 += q1 * q1; sumsq2 += q2 * q2; doti += q1 * q2; } let a1 = alpha1 as f64; let a2 = alpha2 as f64; let s1 = shift1 as f64; let s2 = shift2 as f64; let d = dims as f64; let dot = a1 * a2 * doti as f64 + a1 * s2 * sum1 as f64 + a2 * s1 * sum2 as f64 + s1 * s2 * d; let norm1 = a1 * a1 * sumsq1 as f64 + 2.0 * a1 * s1 * sum1 as f64 + s1 * s1 * d; let norm2 = a2 * a2 * sumsq2 as f64 + 2.0 * a2 * s2 * sum2 as f64 + s2 * s2 * d; 1.0 - dot / (norm1 * norm2).sqrt() } #[allow(dead_code)] #[cfg(not(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") )))] fn vector_f32_distance_cos_simsimd(v1: &[f32], v2: &[f32]) -> f64 { f32::cosine(v1, v2).unwrap_or(f64::NAN) } // SimSIMD does not support WASM, and Windows AArch64 has linker issues with simsimd.lib. #[allow(dead_code)] fn vector_f32_distance_cos_rust(v1: &[f32], v2: &[f32]) -> f64 { let (mut dot, mut norm1, mut norm2) = (0.0, 0.0, 0.0); for (a, b) in v1.iter().zip(v2.iter()) { dot += a * b; norm1 += a * a; norm2 += b * b; } if norm1 == 0.0 || norm2 == 0.0 { return 0.0; } (1.0 - dot / (norm1 * norm2).sqrt()) as f64 } #[allow(dead_code)] #[cfg(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") ))] fn vector_f32_distance_cos_simsimd(v1: &[f32], v2: &[f32]) -> f64 { vector_f32_distance_cos_rust(v1, v2) } #[allow(dead_code)] #[cfg(not(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") )))] fn vector_f64_distance_cos_simsimd(v1: &[f64], v2: &[f64]) -> f64 { f64::cosine(v1, v2).unwrap_or(f64::NAN) } // SimSIMD does not support WASM, and Windows AArch64 has linker issues with simsimd.lib. #[allow(dead_code)] fn vector_f64_distance_cos_rust(v1: &[f64], v2: &[f64]) -> f64 { let (mut dot, mut norm1, mut norm2) = (0.0, 0.0, 0.0); for (a, b) in v1.iter().zip(v2.iter()) { dot += a * b; norm1 += a * a; norm2 += b * b; } if norm1 == 0.0 || norm2 == 0.0 { return 0.0; } 1.0 - dot / (norm1 * norm2).sqrt() } #[allow(dead_code)] #[cfg(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") ))] fn vector_f64_distance_cos_simsimd(v1: &[f64], v2: &[f64]) -> f64 { vector_f64_distance_cos_rust(v1, v2) } fn vector_f32_sparse_distance_cos(v1: VectorSparse, v2: VectorSparse) -> f64 { let mut v1_pos = 0; let mut v2_pos = 0; let (mut dot, mut norm1, mut norm2) = (0.0, 0.0, 0.0); while v1_pos < v1.idx.len() && v2_pos < v2.idx.len() { let e1 = v1.values[v1_pos]; let e2 = v2.values[v2_pos]; if v1.idx[v1_pos] == v2.idx[v2_pos] { dot += e1 * e2; norm1 += e1 * e1; norm2 += e2 * e2; v1_pos += 1; v2_pos += 1; } else if v1.idx[v1_pos] < v2.idx[v2_pos] { norm1 += e1 * e1; v1_pos += 1; } else { norm2 += e2 * e2; v2_pos += 1; } } while v1_pos < v1.idx.len() { norm1 += v1.values[v1_pos] * v1.values[v1_pos]; v1_pos += 1; } while v2_pos < v2.idx.len() { norm2 += v2.values[v2_pos] * v2.values[v2_pos]; v2_pos += 1; } // Check for zero norms if norm1 == 0.0f32 || norm2 == 0.0f32 { return f64::NAN; } (1.0f32 - (dot / (norm1 * norm2).sqrt())) as f64 } #[cfg(test)] mod tests { use crate::vector::{ operations::convert::vector_convert, vector_types::tests::ArbitraryVector, }; use super::*; use quickcheck_macros::quickcheck; #[test] fn test_vector_distance_cos_f32() { assert_eq!(vector_f32_distance_cos_simsimd(&[], &[]), 0.0); assert_eq!( vector_f32_distance_cos_simsimd(&[1.0, 2.0], &[0.0, 0.0]), 1.0 ); assert!(vector_f32_distance_cos_simsimd(&[1.0, 2.0], &[1.0, 2.0]).abs() < 1e-6); assert!((vector_f32_distance_cos_simsimd(&[1.0, 2.0], &[-1.0, -2.0]) - 2.0).abs() < 1e-6); assert!((vector_f32_distance_cos_simsimd(&[1.0, 2.0], &[-2.0, 1.0]) - 1.0).abs() < 1e-6); } #[test] fn test_vector_distance_cos_f64() { assert_eq!(vector_f64_distance_cos_simsimd(&[], &[]), 0.0); assert_eq!( vector_f64_distance_cos_simsimd(&[1.0, 2.0], &[0.0, 0.0]), 1.0 ); assert!(vector_f64_distance_cos_simsimd(&[1.0, 2.0], &[1.0, 2.0]).abs() < 1e-6); assert!((vector_f64_distance_cos_simsimd(&[1.0, 2.0], &[-1.0, -2.0]) - 2.0).abs() < 1e-6); assert!((vector_f64_distance_cos_simsimd(&[1.0, 2.0], &[-2.0, 1.0]) - 1.0).abs() < 1e-6); } #[test] fn test_vector_distance_cos_f32_sparse() { assert!( (vector_f32_sparse_distance_cos( VectorSparse { idx: &[0, 1], values: &[1.0, 2.0] }, VectorSparse { idx: &[1, 2], values: &[1.0, 3.0] }, ) - vector_f32_distance_cos_simsimd(&[1.0, 2.0, 0.0], &[0.0, 1.0, 3.0])) .abs() < 1e-7 ); } #[quickcheck] fn prop_vector_distance_cos_dense_vs_sparse( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float32Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float32Dense).unwrap(); let d1 = vector_distance_cos(&v1, &v2).unwrap(); let sparse1 = vector_convert(v1, VectorType::Float32Sparse).unwrap(); let sparse2 = vector_convert(v2, VectorType::Float32Sparse).unwrap(); let d2 = vector_f32_sparse_distance_cos(sparse1.as_f32_sparse(), sparse2.as_f32_sparse()); (d1.is_nan() && d2.is_nan()) || (d1 - d2).abs() < 1e-6 } #[quickcheck] fn prop_vector_distance_cos_rust_vs_simsimd_f32( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float32Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float32Dense).unwrap(); let d1 = vector_f32_distance_cos_rust(v1.as_f32_slice(), v2.as_f32_slice()); let d2 = vector_f32_distance_cos_simsimd(v1.as_f32_slice(), v2.as_f32_slice()); println!("d1 vs d2: {d1} vs {d2}"); (d1.is_nan() && d2.is_nan()) || (d1 - d2).abs() < 1e-4 } #[quickcheck] fn prop_vector_distance_cos_rust_vs_simsimd_f64( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float64Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float64Dense).unwrap(); let d1 = vector_f64_distance_cos_rust(v1.as_f64_slice(), v2.as_f64_slice()); let d2 = vector_f64_distance_cos_simsimd(v1.as_f64_slice(), v2.as_f64_slice()); println!("d1 vs d2: {d1} vs {d2}"); (d1.is_nan() && d2.is_nan()) || (d1 - d2).abs() < 1e-6 } /// Float8 optimized cosine distance matches dequantized Float32 cosine distance. #[quickcheck] fn prop_vector_distance_cos_f8_vs_dequantized( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float32Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float32Dense).unwrap(); let v1_f8 = vector_convert(v1, VectorType::Float8).unwrap(); let v2_f8 = vector_convert(v2, VectorType::Float8).unwrap(); let d_f8 = vector_distance_cos(&v1_f8, &v2_f8).unwrap(); let v1_deq = vector_convert(v1_f8, VectorType::Float32Dense).unwrap(); let v2_deq = vector_convert(v2_f8, VectorType::Float32Dense).unwrap(); let d_deq = vector_distance_cos(&v1_deq, &v2_deq).unwrap(); (d_f8.is_nan() && d_deq.is_nan()) || (d_f8 - d_deq).abs() < 1e-4 } /// Float1Bit cosine distance (hamming) matches dot-product relationship: /// hamming = (dims + dot_distance) / 2 #[quickcheck] fn prop_vector_distance_cos_1bit_dot_relationship( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { use crate::vector::operations::distance_dot::vector_distance_dot; let v1 = vector_convert(v1.into(), VectorType::Float1Bit).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float1Bit).unwrap(); let cos = vector_distance_cos(&v1, &v2).unwrap(); let dot = vector_distance_dot(&v1, &v2).unwrap(); // hamming = cos, dot = -(dims - 2*hamming), so cos = (dims + dot) / 2 (cos - (100.0 + dot) / 2.0).abs() < 1e-10 } } ================================================ FILE: core/vector/operations/distance_dot.rs ================================================ use crate::{ vector::vector_types::{Vector, VectorSparse, VectorType}, LimboError, Result, }; #[cfg(not(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") )))] use simsimd::SpatialSimilarity; pub fn vector_distance_dot(v1: &Vector, v2: &Vector) -> Result { if v1.dims != v2.dims { return Err(LimboError::ConversionError( "Vectors must have the same dimensions".to_string(), )); } if v1.vector_type != v2.vector_type { return Err(LimboError::ConversionError( "Vectors must be of the same type".to_string(), )); } match v1.vector_type { VectorType::Float32Dense => Ok(vector_f32_distance_dot_simsimd( v1.as_f32_slice(), v2.as_f32_slice(), )), VectorType::Float64Dense => Ok(vector_f64_distance_dot_simsimd( v1.as_f64_slice(), v2.as_f64_slice(), )), VectorType::Float32Sparse => Ok(vector_f32_sparse_distance_dot( v1.as_f32_sparse(), v2.as_f32_sparse(), )), VectorType::Float1Bit => Ok(vector_1bit_distance_dot(v1, v2)), VectorType::Float8 => Ok(vector_f8_distance_dot(v1, v2)), } } fn vector_1bit_distance_dot(v1: &Vector, v2: &Vector) -> f64 { // 1-bit values represent +1/-1. // Dot product = dims - 2 * hamming_distance // Return negated (consistent with existing dot distance convention). let d1 = v1.as_1bit_data(); let d2 = v2.as_1bit_data(); let mut hamming = 0u32; for (&a, &b) in d1.iter().zip(d2.iter()) { hamming += (a ^ b).count_ones(); } let dot = v1.dims as f64 - 2.0 * hamming as f64; -dot } fn vector_f8_distance_dot(v1: &Vector, v2: &Vector) -> f64 { let (data1, alpha1, shift1) = v1.as_f8_data(); let (data2, alpha2, shift2) = v2.as_f8_data(); let dims = v1.dims; let (mut sum1, mut sum2, mut doti) = (0u64, 0u64, 0u64); for i in 0..dims { let q1 = data1[i] as u64; let q2 = data2[i] as u64; sum1 += q1; sum2 += q2; doti += q1 * q2; } let a1 = alpha1 as f64; let a2 = alpha2 as f64; let s1 = shift1 as f64; let s2 = shift2 as f64; let d = dims as f64; let dot = a1 * a2 * doti as f64 + a1 * s2 * sum1 as f64 + a2 * s1 * sum2 as f64 + s1 * s2 * d; -dot } #[allow(dead_code)] #[cfg(not(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") )))] fn vector_f32_distance_dot_simsimd(v1: &[f32], v2: &[f32]) -> f64 { -f32::dot(v1, v2).unwrap_or(f64::NAN) } // SimSIMD does not support WASM, and Windows AArch64 has linker issues with simsimd.lib. #[allow(dead_code)] fn vector_f32_distance_dot_rust(v1: &[f32], v2: &[f32]) -> f64 { let mut dot = 0.0; for (a, b) in v1.iter().zip(v2.iter()) { dot += (*a as f64) * (*b as f64); } -dot } #[allow(dead_code)] #[cfg(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") ))] fn vector_f32_distance_dot_simsimd(v1: &[f32], v2: &[f32]) -> f64 { vector_f32_distance_dot_rust(v1, v2) } #[allow(dead_code)] #[cfg(not(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") )))] fn vector_f64_distance_dot_simsimd(v1: &[f64], v2: &[f64]) -> f64 { -f64::dot(v1, v2).unwrap_or(f64::NAN) } // SimSIMD does not support WASM, and Windows AArch64 has linker issues with simsimd.lib. #[allow(dead_code)] fn vector_f64_distance_dot_rust(v1: &[f64], v2: &[f64]) -> f64 { let mut dot = 0.0; for (a, b) in v1.iter().zip(v2.iter()) { dot += *a * *b; } -dot } #[allow(dead_code)] #[cfg(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") ))] fn vector_f64_distance_dot_simsimd(v1: &[f64], v2: &[f64]) -> f64 { vector_f64_distance_dot_rust(v1, v2) } fn vector_f32_sparse_distance_dot(v1: VectorSparse, v2: VectorSparse) -> f64 { let mut v1_pos = 0; let mut v2_pos = 0; let mut dot = 0.0; while v1_pos < v1.idx.len() && v2_pos < v2.idx.len() { let idx1 = v1.idx[v1_pos]; let idx2 = v2.idx[v2_pos]; if idx1 == idx2 { let e1 = v1.values[v1_pos]; let e2 = v2.values[v2_pos]; dot += (e1 as f64) * (e2 as f64); v1_pos += 1; v2_pos += 1; } else if idx1 < idx2 { v1_pos += 1; } else { v2_pos += 1; } } -dot } #[cfg(test)] mod tests { use crate::vector::{ operations::convert::vector_convert, vector_types::tests::ArbitraryVector, }; use super::*; use quickcheck_macros::quickcheck; #[test] fn test_vector_distance_dot_f32() { assert_eq!(vector_f32_distance_dot_simsimd(&[], &[]), 0.0); assert_eq!( vector_f32_distance_dot_simsimd(&[1.0, 0.0], &[0.0, 1.0]), 0.0 ); assert!((vector_f32_distance_dot_simsimd(&[1.0, 2.0], &[1.0, 2.0]) - (-5.0)).abs() < 1e-6); assert!((vector_f32_distance_dot_simsimd(&[1.0, 2.0], &[2.0, 4.0]) - (-10.0)).abs() < 1e-6); assert!((vector_f32_distance_dot_simsimd(&[1.0, 2.0], &[-1.0, -2.0]) - 5.0).abs() < 1e-6); } #[test] fn test_vector_distance_dot_f64() { assert_eq!(vector_f64_distance_dot_simsimd(&[], &[]), 0.0); assert_eq!( vector_f64_distance_dot_simsimd(&[1.0, 0.0], &[0.0, 1.0]), 0.0 ); assert!((vector_f64_distance_dot_simsimd(&[1.0, 2.0], &[1.0, 2.0]) - (-5.0)).abs() < 1e-6); assert!((vector_f64_distance_dot_simsimd(&[1.0, 2.0], &[-1.0, -2.0]) - 5.0).abs() < 1e-6); } #[test] fn test_vector_distance_dot_f32_sparse() { let v1_sparse = VectorSparse { idx: &[1, 2], values: &[1.0, 2.0], }; let v2_sparse = VectorSparse { idx: &[1, 3], values: &[2.0, 3.0], }; let v1_dense = &[0.0, 1.0, 2.0, 0.0]; let v2_dense = &[0.0, 2.0, 0.0, 3.0]; let sparse_dist = vector_f32_sparse_distance_dot(v1_sparse, v2_sparse); let dense_dist = vector_f32_distance_dot_simsimd(v1_dense, v2_dense); assert!((sparse_dist - dense_dist).abs() < 1e-7); assert!((sparse_dist - (-2.0)).abs() < 1e-7); } #[quickcheck] fn prop_vector_distance_dot_dense_vs_sparse( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1_dense = vector_convert(v1.into(), VectorType::Float32Dense).unwrap(); let v2_dense = vector_convert(v2.into(), VectorType::Float32Dense).unwrap(); let d_dense = vector_f32_distance_dot_rust(v1_dense.as_f32_slice(), v2_dense.as_f32_slice()); let v1_sparse = vector_convert(v1_dense, VectorType::Float32Sparse).unwrap(); let v2_sparse = vector_convert(v2_dense, VectorType::Float32Sparse).unwrap(); let d_sparse = vector_f32_sparse_distance_dot(v1_sparse.as_f32_sparse(), v2_sparse.as_f32_sparse()); (d_dense.is_nan() && d_sparse.is_nan()) || (d_dense - d_sparse).abs() < 1e-5 } #[quickcheck] fn prop_vector_distance_dot_rust_vs_simsimd_f32( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float32Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float32Dense).unwrap(); let d_rust = vector_f32_distance_dot_rust(v1.as_f32_slice(), v2.as_f32_slice()); let d_simd = vector_f32_distance_dot_simsimd(v1.as_f32_slice(), v2.as_f32_slice()); (d_rust.is_nan() && d_simd.is_nan()) || (d_rust - d_simd).abs() < 1e-4 } #[quickcheck] fn prop_vector_distance_dot_rust_vs_simsimd_f64( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float64Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float64Dense).unwrap(); let d_rust = vector_f64_distance_dot_rust(v1.as_f64_slice(), v2.as_f64_slice()); let d_simd = vector_f64_distance_dot_simsimd(v1.as_f64_slice(), v2.as_f64_slice()); (d_rust.is_nan() && d_simd.is_nan()) || (d_rust - d_simd).abs() < 1e-6 } /// Float8 optimized dot distance matches dequantized Float32 dot distance. #[quickcheck] fn prop_vector_distance_dot_f8_vs_dequantized( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float32Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float32Dense).unwrap(); let v1_f8 = vector_convert(v1, VectorType::Float8).unwrap(); let v2_f8 = vector_convert(v2, VectorType::Float8).unwrap(); let d_f8 = vector_distance_dot(&v1_f8, &v2_f8).unwrap(); let v1_deq = vector_convert(v1_f8, VectorType::Float32Dense).unwrap(); let v2_deq = vector_convert(v2_f8, VectorType::Float32Dense).unwrap(); let d_deq = vector_distance_dot(&v1_deq, &v2_deq).unwrap(); (d_f8.is_nan() && d_deq.is_nan()) || (d_f8 - d_deq).abs() < 1e-3 } /// Float1Bit dot distance matches dequantized ±1 Float32 dot distance. #[quickcheck] fn prop_vector_distance_dot_1bit_vs_dequantized( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float1Bit).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float1Bit).unwrap(); let d_1bit = vector_distance_dot(&v1, &v2).unwrap(); let v1_f32 = vector_convert(v1, VectorType::Float32Dense).unwrap(); let v2_f32 = vector_convert(v2, VectorType::Float32Dense).unwrap(); let d_f32 = vector_distance_dot(&v1_f32, &v2_f32).unwrap(); (d_1bit - d_f32).abs() < 1e-6 } } ================================================ FILE: core/vector/operations/distance_l2.rs ================================================ use crate::{ vector::vector_types::{Vector, VectorSparse, VectorType}, LimboError, Result, }; #[cfg(not(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") )))] use simsimd::SpatialSimilarity; pub fn vector_distance_l2(v1: &Vector, v2: &Vector) -> Result { if v1.dims != v2.dims { return Err(LimboError::ConversionError( "Vectors must have the same dimensions".to_string(), )); } if v1.vector_type != v2.vector_type { return Err(LimboError::ConversionError( "Vectors must be of the same type".to_string(), )); } match v1.vector_type { VectorType::Float32Dense => Ok(vector_f32_distance_l2_simsimd( v1.as_f32_slice(), v2.as_f32_slice(), )), VectorType::Float64Dense => Ok(vector_f64_distance_l2_simsimd( v1.as_f64_slice(), v2.as_f64_slice(), )), VectorType::Float32Sparse => Ok(vector_f32_sparse_distance_l2( v1.as_f32_sparse(), v2.as_f32_sparse(), )), VectorType::Float1Bit => Err(LimboError::ConversionError( "L2 distance is not supported for float1bit vectors".to_string(), )), VectorType::Float8 => Ok(vector_f8_distance_l2(v1, v2)), } } fn vector_f8_distance_l2(v1: &Vector, v2: &Vector) -> f64 { let (data1, alpha1, shift1) = v1.as_f8_data(); let (data2, alpha2, shift2) = v2.as_f8_data(); let mut sum = 0.0f64; for i in 0..v1.dims { let f1 = alpha1 as f64 * data1[i] as f64 + shift1 as f64; let f2 = alpha2 as f64 * data2[i] as f64 + shift2 as f64; let d = f1 - f2; sum += d * d; } sum.sqrt() } #[allow(dead_code)] #[cfg(not(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") )))] fn vector_f32_distance_l2_simsimd(v1: &[f32], v2: &[f32]) -> f64 { f32::euclidean(v1, v2).unwrap_or(f64::NAN) } // SimSIMD does not support WASM, and Windows AArch64 has linker issues with simsimd.lib. #[allow(dead_code)] fn vector_f32_distance_l2_rust(v1: &[f32], v2: &[f32]) -> f64 { let sum = v1 .iter() .zip(v2.iter()) .map(|(a, b)| (a - b).powi(2)) .sum::() as f64; sum.sqrt() } #[allow(dead_code)] #[cfg(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") ))] fn vector_f32_distance_l2_simsimd(v1: &[f32], v2: &[f32]) -> f64 { vector_f32_distance_l2_rust(v1, v2) } #[allow(dead_code)] #[cfg(not(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") )))] fn vector_f64_distance_l2_simsimd(v1: &[f64], v2: &[f64]) -> f64 { f64::euclidean(v1, v2).unwrap_or(f64::NAN) } // SimSIMD does not support WASM, and Windows AArch64 has linker issues with simsimd.lib. #[allow(dead_code)] fn vector_f64_distance_l2_rust(v1: &[f64], v2: &[f64]) -> f64 { let sum = v1 .iter() .zip(v2.iter()) .map(|(a, b)| (a - b).powi(2)) .sum::(); sum.sqrt() } #[allow(dead_code)] #[cfg(any( target_family = "wasm", all(target_os = "windows", target_arch = "aarch64") ))] fn vector_f64_distance_l2_simsimd(v1: &[f64], v2: &[f64]) -> f64 { vector_f64_distance_l2_rust(v1, v2) } fn vector_f32_sparse_distance_l2(v1: VectorSparse, v2: VectorSparse) -> f64 { let mut v1_pos = 0; let mut v2_pos = 0; let mut sum = 0.0; while v1_pos < v1.idx.len() && v2_pos < v2.idx.len() { if v1.idx[v1_pos] == v2.idx[v2_pos] { sum += (v1.values[v1_pos] - v2.values[v2_pos]).powi(2); v1_pos += 1; v2_pos += 1; } else if v1.idx[v1_pos] < v2.idx[v2_pos] { sum += v1.values[v1_pos].powi(2); v1_pos += 1; } else { sum += v2.values[v2_pos].powi(2); v2_pos += 1; } } while v1_pos < v1.idx.len() { sum += v1.values[v1_pos].powi(2); v1_pos += 1; } while v2_pos < v2.idx.len() { sum += v2.values[v2_pos].powi(2); v2_pos += 1; } (sum as f64).sqrt() } #[cfg(test)] mod tests { use quickcheck_macros::quickcheck; use crate::vector::{ operations::convert::vector_convert, vector_types::tests::ArbitraryVector, }; use super::*; #[test] fn test_vector_distance_l2_f32_another() { let vectors = [ (0..8).map(|x| x as f32).collect::>(), (1..9).map(|x| x as f32).collect::>(), (2..10).map(|x| x as f32).collect::>(), (3..11).map(|x| x as f32).collect::>(), ]; let query = (2..10).map(|x| x as f32).collect::>(); let expected: Vec = vec![ 32.0_f64.sqrt(), 8.0_f64.sqrt(), 0.0_f64.sqrt(), 8.0_f64.sqrt(), ]; let results = vectors .iter() .map(|v| vector_f32_distance_l2_rust(&query, v)) .collect::>(); assert_eq!(results, expected); } #[test] fn test_vector_distance_l2_odd_len() { let v = (0..5).map(|x| x as f32).collect::>(); let query = (2..7).map(|x| x as f32).collect::>(); assert_eq!(vector_f32_distance_l2_rust(&v, &query), 20.0_f64.sqrt()); } #[test] fn test_vector_distance_l2_f32() { assert_eq!(vector_f32_distance_l2_rust(&[], &[]), 0.0); assert_eq!( vector_f32_distance_l2_rust(&[1.0, 2.0], &[0.0, 0.0]), (1f64 + 2f64 * 2f64).sqrt() ); assert_eq!(vector_f32_distance_l2_rust(&[1.0, 2.0], &[1.0, 2.0]), 0.0); assert_eq!( vector_f32_distance_l2_rust(&[1.0, 2.0], &[-1.0, -2.0]), (2f64 * 2f64 + 4f64 * 4f64).sqrt() ); assert_eq!( vector_f32_distance_l2_rust(&[1.0, 2.0], &[-2.0, 1.0]), (3f64 * 3f64 + 1f64 * 1f64).sqrt() ); } #[test] fn test_vector_distance_l2_f64() { assert_eq!(vector_f64_distance_l2_rust(&[], &[]), 0.0); assert_eq!( vector_f64_distance_l2_rust(&[1.0, 2.0], &[0.0, 0.0]), (1f64 + 2f64 * 2f64).sqrt() ); assert_eq!(vector_f64_distance_l2_rust(&[1.0, 2.0], &[1.0, 2.0]), 0.0); assert_eq!( vector_f64_distance_l2_rust(&[1.0, 2.0], &[-1.0, -2.0]), (2f64 * 2f64 + 4f64 * 4f64).sqrt() ); assert_eq!( vector_f64_distance_l2_rust(&[1.0, 2.0], &[-2.0, 1.0]), (3f64 * 3f64 + 1f64 * 1f64).sqrt() ); } #[test] fn test_vector_distance_l2_f32_sparse() { assert!( (vector_f32_sparse_distance_l2( VectorSparse { idx: &[0, 1], values: &[1.0, 2.0] }, VectorSparse { idx: &[1, 2], values: &[1.0, 3.0] }, ) - vector_f32_distance_l2_rust(&[1.0, 2.0, 0.0], &[0.0, 1.0, 3.0])) .abs() < 1e-7 ); } #[quickcheck] fn prop_vector_distance_l2_dense_vs_sparse( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { // Dense uses simsimd, sparse uses rust impl. These can differ by up to 1e-4 // (as demonstrated by prop_vector_distance_l2_rust_vs_simsimd_f32). let tolerance = 1e-4; let v1 = vector_convert(v1.into(), VectorType::Float32Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float32Dense).unwrap(); let d1 = vector_distance_l2(&v1, &v2).unwrap(); let sparse1 = vector_convert(v1, VectorType::Float32Sparse).unwrap(); let sparse2 = vector_convert(v2, VectorType::Float32Sparse).unwrap(); let d2 = vector_f32_sparse_distance_l2(sparse1.as_f32_sparse(), sparse2.as_f32_sparse()); (d1.is_nan() && d2.is_nan()) || (d1 - d2).abs() < tolerance } #[quickcheck] fn prop_vector_distance_l2_rust_vs_simsimd_f32( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float32Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float32Dense).unwrap(); let d1 = vector_f32_distance_l2_rust(v1.as_f32_slice(), v2.as_f32_slice()); let d2 = vector_f32_distance_l2_simsimd(v1.as_f32_slice(), v2.as_f32_slice()); (d1.is_nan() && d2.is_nan()) || (d1 - d2).abs() < 1e-4 } #[quickcheck] fn prop_vector_distance_l2_rust_vs_simsimd_f64( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float64Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float64Dense).unwrap(); let d1 = vector_f64_distance_l2_rust(v1.as_f64_slice(), v2.as_f64_slice()); let d2 = vector_f64_distance_l2_simsimd(v1.as_f64_slice(), v2.as_f64_slice()); (d1.is_nan() && d2.is_nan()) || (d1 - d2).abs() < 1e-6 } /// Float8 L2 distance matches dequantized Float32 L2 distance. #[quickcheck] fn prop_vector_distance_l2_f8_vs_dequantized( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float32Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float32Dense).unwrap(); let v1_f8 = vector_convert(v1, VectorType::Float8).unwrap(); let v2_f8 = vector_convert(v2, VectorType::Float8).unwrap(); let d_f8 = vector_distance_l2(&v1_f8, &v2_f8).unwrap(); let v1_deq = vector_convert(v1_f8, VectorType::Float32Dense).unwrap(); let v2_deq = vector_convert(v2_f8, VectorType::Float32Dense).unwrap(); let d_deq = vector_distance_l2(&v1_deq, &v2_deq).unwrap(); (d_f8.is_nan() && d_deq.is_nan()) || (d_f8 - d_deq).abs() < 1e-4 } /// Float1Bit L2 distance returns an error. #[test] fn test_vector_distance_l2_1bit_error() { let v1 = Vector::from_1bit(4, vec![0b1010]); let v2 = Vector::from_1bit(4, vec![0b0101]); assert!(vector_distance_l2(&v1, &v2).is_err()); } } ================================================ FILE: core/vector/operations/jaccard.rs ================================================ use crate::{ vector::vector_types::{Vector, VectorSparse, VectorType}, LimboError, Result, }; pub fn vector_distance_jaccard(v1: &Vector, v2: &Vector) -> Result { if v1.dims != v2.dims { return Err(LimboError::ConversionError( "Vectors must have the same dimensions".to_string(), )); } if v1.vector_type != v2.vector_type { return Err(LimboError::ConversionError( "Vectors must be of the same type".to_string(), )); } match v1.vector_type { VectorType::Float32Dense => Ok(vector_f32_distance_jaccard( v1.as_f32_slice(), v2.as_f32_slice(), )), VectorType::Float64Dense => Ok(vector_f64_distance_jaccard( v1.as_f64_slice(), v2.as_f64_slice(), )), VectorType::Float32Sparse => Ok(vector_f32_sparse_distance_jaccard( v1.as_f32_sparse(), v2.as_f32_sparse(), )), VectorType::Float1Bit => Ok(vector_1bit_distance_jaccard(v1, v2)), VectorType::Float8 => Ok(vector_f8_distance_jaccard(v1, v2)), } } fn vector_1bit_distance_jaccard(v1: &Vector, v2: &Vector) -> f64 { // Binary Jaccard: 1.0 - popcount(a & b) / popcount(a | b) let d1 = v1.as_1bit_data(); let d2 = v2.as_1bit_data(); let mut intersection = 0u32; let mut union = 0u32; for (&a, &b) in d1.iter().zip(d2.iter()) { intersection += (a & b).count_ones(); union += (a | b).count_ones(); } if union == 0 { return f64::NAN; } 1.0 - intersection as f64 / union as f64 } fn vector_f8_distance_jaccard(v1: &Vector, v2: &Vector) -> f64 { let (data1, alpha1, shift1) = v1.as_f8_data(); let (data2, alpha2, shift2) = v2.as_f8_data(); let (mut min_sum, mut max_sum) = (0.0f64, 0.0f64); for i in 0..v1.dims { let f1 = alpha1 as f64 * data1[i] as f64 + shift1 as f64; let f2 = alpha2 as f64 * data2[i] as f64 + shift2 as f64; min_sum += f1.min(f2); max_sum += f1.max(f2); } if max_sum == 0.0 { return f64::NAN; } 1.0 - min_sum / max_sum } fn vector_f32_distance_jaccard(v1: &[f32], v2: &[f32]) -> f64 { let (mut min_sum, mut max_sum) = (0.0, 0.0); for (&a, &b) in v1.iter().zip(v2.iter()) { min_sum += a.min(b); max_sum += a.max(b); } if max_sum == 0.0 { return f64::NAN; } 1. - (min_sum / max_sum) as f64 } fn vector_f64_distance_jaccard(v1: &[f64], v2: &[f64]) -> f64 { let (mut min_sum, mut max_sum) = (0.0, 0.0); for (&a, &b) in v1.iter().zip(v2.iter()) { min_sum += a.min(b); max_sum += a.max(b); } if max_sum == 0.0 { return f64::NAN; } 1. - min_sum / max_sum } fn vector_f32_sparse_distance_jaccard(v1: VectorSparse, v2: VectorSparse) -> f64 { let mut v1_pos = 0; let mut v2_pos = 0; let (mut min_sum, mut max_sum) = (0.0, 0.0); while v1_pos < v1.idx.len() && v2_pos < v2.idx.len() { if v1.idx[v1_pos] == v2.idx[v2_pos] { min_sum += v1.values[v1_pos].min(v2.values[v2_pos]); max_sum += v1.values[v1_pos].max(v2.values[v2_pos]); v1_pos += 1; v2_pos += 1; } else if v1.idx[v1_pos] < v2.idx[v2_pos] { min_sum += v1.values[v1_pos].min(0.); max_sum += v1.values[v1_pos].max(0.); v1_pos += 1; } else { min_sum += v2.values[v2_pos].min(0.); max_sum += v2.values[v2_pos].max(0.); v2_pos += 1; } } while v1_pos < v1.idx.len() { min_sum += v1.values[v1_pos].min(0.); max_sum += v1.values[v1_pos].max(0.); v1_pos += 1; } while v2_pos < v2.idx.len() { min_sum += v2.values[v2_pos].min(0.); max_sum += v2.values[v2_pos].max(0.); v2_pos += 1; } if max_sum == 0.0 { return f64::NAN; } 1. - (min_sum / max_sum) as f64 } #[cfg(test)] mod tests { use quickcheck_macros::quickcheck; use crate::vector::{ operations::convert::vector_convert, vector_types::tests::ArbitraryVector, }; use super::*; #[test] fn test_vector_distance_jaccard_f32() { assert!(vector_f32_distance_jaccard(&[0.0, 0.0, 0.0], &[0.0, 0.0, 0.0]).is_nan()); assert_eq!(vector_f32_distance_jaccard(&[1.0, 2.0], &[0.0, 0.0]), 1.0); assert_eq!(vector_f32_distance_jaccard(&[1.0, 2.0], &[1.0, 2.0]), 0.0); assert_eq!( vector_f32_distance_jaccard(&[1.0, 2.0], &[2.0, 1.0]), 1. - (1.0 + 1.0) / (2.0 + 2.0) ); } #[test] fn test_vector_distance_jaccard_f64() { assert!(vector_f64_distance_jaccard(&[], &[]).is_nan()); assert!(vector_f64_distance_jaccard(&[0.0, 0.0, 0.0], &[0.0, 0.0, 0.0]).is_nan()); assert_eq!(vector_f64_distance_jaccard(&[1.0, 2.0], &[0.0, 0.0]), 1.0); assert_eq!(vector_f64_distance_jaccard(&[1.0, 2.0], &[1.0, 2.0]), 0.0); assert_eq!( vector_f64_distance_jaccard(&[1.0, 2.0], &[2.0, 1.0]), 1. - (1.0 + 1.0) / (2.0 + 2.0) ); } #[test] fn test_vector_distance_jaccard_f32_sparse() { assert!( (vector_f32_sparse_distance_jaccard( VectorSparse { idx: &[0, 1], values: &[1.0, 2.0] }, VectorSparse { idx: &[1, 2], values: &[1.0, 3.0] }, ) - vector_f32_distance_jaccard(&[1.0, 2.0, 0.0], &[0.0, 1.0, 3.0])) .abs() < 1e-7 ); } #[quickcheck] fn prop_vector_distance_jaccard_dense_vs_sparse( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float32Dense).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float32Dense).unwrap(); let d1 = vector_distance_jaccard(&v1, &v2).unwrap(); println!("v1: {:?}, v2: {:?}", v1.as_f32_slice(), v2.as_f32_slice()); let sparse1 = vector_convert(v1, VectorType::Float32Sparse).unwrap(); let sparse2 = vector_convert(v2, VectorType::Float32Sparse).unwrap(); let d2 = vector_f32_sparse_distance_jaccard(sparse1.as_f32_sparse(), sparse2.as_f32_sparse()); println!("d1: {}, d2: {}, delta: {}", d1, d2, (d1 - d2).abs()); (d1.is_nan() && d2.is_nan()) || (d1 - d2).abs() < 1e-6 } // FIXME: flaky // Float8 optimized Jaccard distance matches dequantized Float32 Jaccard distance. // Tolerance is looser here because the Float8 path computes in f64 precision // while the dequantized path accumulates in f32, causing precision differences // that are amplified by Jaccard's min/max ratio when values are close to zero. // #[quickcheck] // fn prop_vector_distance_jaccard_f8_vs_dequantized( // v1: ArbitraryVector<100>, // v2: ArbitraryVector<100>, // ) -> bool { // let v1 = vector_convert(v1.into(), VectorType::Float32Dense).unwrap(); // let v2 = vector_convert(v2.into(), VectorType::Float32Dense).unwrap(); // let v1_f8 = vector_convert(v1, VectorType::Float8).unwrap(); // let v2_f8 = vector_convert(v2, VectorType::Float8).unwrap(); // let d_f8 = vector_distance_jaccard(&v1_f8, &v2_f8).unwrap(); // let v1_deq = vector_convert(v1_f8, VectorType::Float32Dense).unwrap(); // let v2_deq = vector_convert(v2_f8, VectorType::Float32Dense).unwrap(); // let d_deq = vector_distance_jaccard(&v1_deq, &v2_deq).unwrap(); // (d_f8.is_nan() && d_deq.is_nan()) || (d_f8 - d_deq).abs() < 0.01 // } /// Float1Bit binary Jaccard matches manual computation from dequantized ±1 set bits. #[quickcheck] fn prop_vector_distance_jaccard_1bit_vs_manual( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { let v1 = vector_convert(v1.into(), VectorType::Float1Bit).unwrap(); let v2 = vector_convert(v2.into(), VectorType::Float1Bit).unwrap(); let d = vector_distance_jaccard(&v1, &v2).unwrap(); // Manual: binary Jaccard = 1 - |intersection| / |union| over set bits let d1 = v1.as_1bit_data(); let d2 = v2.as_1bit_data(); let mut intersection = 0u32; let mut union = 0u32; for (&a, &b) in d1.iter().zip(d2.iter()) { intersection += (a & b).count_ones(); union += (a | b).count_ones(); } if union == 0 { return d.is_nan(); } let expected = 1.0 - intersection as f64 / union as f64; (d - expected).abs() < 1e-10 } } ================================================ FILE: core/vector/operations/mod.rs ================================================ pub mod concat; pub mod convert; pub mod distance_cos; pub mod distance_dot; pub mod distance_l2; pub mod jaccard; pub mod serialize; pub mod slice; pub mod text; ================================================ FILE: core/vector/operations/serialize.rs ================================================ use crate::{ vector::vector_types::{Vector, VectorType}, Value, }; pub fn vector_serialize(x: Vector) -> Value { match x.vector_type { VectorType::Float32Dense => Value::from_blob(x.bin_eject()), VectorType::Float64Dense => { let mut data = x.bin_eject(); data.push(2); Value::from_blob(data) } VectorType::Float32Sparse => { let dims = x.dims; let mut data = x.bin_eject(); data.extend_from_slice(&(dims as u32).to_le_bytes()); data.push(9); Value::from_blob(data) } VectorType::Float1Bit => { // Format: [data bytes][optional padding][trailing_bits][0x03] let dims = x.dims; let data_size = dims.div_ceil(8); let needs_padding = data_size % 2 == 0; let meta_size = if needs_padding { 3 } else { 2 }; let blob_size = data_size + meta_size; let mut blob = Vec::with_capacity(blob_size); let raw = x.bin_eject(); blob.extend_from_slice(&raw[..data_size]); if needs_padding { blob.push(0); // padding } let trailing_bits = (blob_size - 1) * 8 - dims; blob.push(trailing_bits as u8); blob.push(3); // type byte Value::from_blob(blob) } VectorType::Float8 => { // Format: [quantized bytes][alignment padding][alpha f32][shift f32][padding 0x00][trailing_bytes][0x04] let dims = x.dims; let data = x.bin_eject(); // ALIGN(dims, 4) + 8 bytes let trailing_bytes = dims.div_ceil(4) * 4 - dims; let mut blob = Vec::with_capacity(data.len() + 3); blob.extend_from_slice(&data); blob.push(0); // padding blob.push(trailing_bytes as u8); blob.push(4); // type byte Value::from_blob(blob) } } } ================================================ FILE: core/vector/operations/slice.rs ================================================ use crate::{ vector::vector_types::{Vector, VectorType}, LimboError, Result, }; pub fn vector_slice(vector: &Vector, start: usize, end: usize) -> Result> { if start > end { return Err(LimboError::InvalidArgument( "start index must not be greater than end index".into(), )); } if end > vector.dims || end < start { return Err(LimboError::ConversionError( "vector_slice range out of bounds".into(), )); } match vector.vector_type { VectorType::Float32Dense => Ok(Vector { vector_type: vector.vector_type, dims: end - start, owned: Some(vector.bin_data()[start * 4..end * 4].to_vec()), refer: None, }), VectorType::Float64Dense => Ok(Vector { vector_type: vector.vector_type, dims: end - start, owned: Some(vector.bin_data()[start * 8..end * 8].to_vec()), refer: None, }), VectorType::Float32Sparse => { let mut values = Vec::new(); let mut idx = Vec::new(); let sparse = vector.as_f32_sparse(); for (&i, &value) in sparse.idx.iter().zip(sparse.values.iter()) { let i = i as usize; if i < start || i >= end { continue; } values.extend_from_slice(&value.to_le_bytes()); idx.extend_from_slice(&((i - start) as u32).to_le_bytes()); } values.extend_from_slice(&idx); Ok(Vector { vector_type: vector.vector_type, dims: end - start, owned: Some(values), refer: None, }) } VectorType::Float1Bit | VectorType::Float8 => Err(LimboError::ConversionError( "vector_slice is not supported for float1bit/float8 vectors".to_string(), )), } } #[cfg(test)] mod tests { use crate::vector::{ operations::slice::vector_slice, vector_types::{Vector, VectorType}, }; fn float32_vec_from(slice: &[f32]) -> Vector { let mut data = Vec::new(); for &v in slice { data.extend_from_slice(&v.to_le_bytes()); } Vector { vector_type: VectorType::Float32Dense, dims: slice.len(), owned: Some(data), refer: None, } } fn f32_slice_from_vector(vector: &Vector) -> Vec { vector.as_f32_slice().to_vec() } #[test] fn test_vector_slice_normal_case() { let input_vec = float32_vec_from(&[1.0, 2.0, 3.0, 4.0, 5.0]); let result = vector_slice(&input_vec, 1, 4).unwrap(); assert_eq!(result.dims, 3); assert_eq!(f32_slice_from_vector(&result), vec![2.0, 3.0, 4.0]); } #[test] fn test_vector_slice_full_range() { let input_vec = float32_vec_from(&[10.0, 20.0, 30.0]); let result = vector_slice(&input_vec, 0, 3).unwrap(); assert_eq!(result.dims, 3); assert_eq!(f32_slice_from_vector(&result), vec![10.0, 20.0, 30.0]); } #[test] fn test_vector_slice_single_element() { let input_vec = float32_vec_from(&[4.40, 2.71]); let result = vector_slice(&input_vec, 1, 2).unwrap(); assert_eq!(result.dims, 1); assert_eq!(f32_slice_from_vector(&result), vec![2.71]); } #[test] fn test_vector_slice_empty_list() { let input_vec = float32_vec_from(&[1.0, 2.0]); let result = vector_slice(&input_vec, 2, 2).unwrap(); assert_eq!(result.dims, 0); } #[test] fn test_vector_slice_zero_length() { let input_vec = float32_vec_from(&[1.0, 2.0, 3.0]); let err = vector_slice(&input_vec, 2, 1); assert!(err.is_err(), "Expected error on zero-length range"); } #[test] fn test_vector_slice_out_of_bounds() { let input_vec = float32_vec_from(&[1.0, 2.0]); let err = vector_slice(&input_vec, 0, 5); assert!(err.is_err()); } #[test] fn test_vector_slice_start_out_of_bounds() { let input_vec = float32_vec_from(&[1.0, 2.0]); let err = vector_slice(&input_vec, 5, 5); assert!(err.is_err()); } #[test] fn test_vector_slice_end_out_of_bounds() { let input_vec = float32_vec_from(&[1.0, 2.0]); let err = vector_slice(&input_vec, 1, 3); assert!(err.is_err()); } } ================================================ FILE: core/vector/operations/text.rs ================================================ use crate::{ vector::vector_types::{Vector, VectorType}, LimboError, Result, }; pub fn vector_to_text(vector: &Vector) -> String { match vector.vector_type { VectorType::Float32Dense => format_text(vector.as_f32_slice().iter()), VectorType::Float64Dense => format_text(vector.as_f64_slice().iter()), VectorType::Float32Sparse => { let mut dense = vec![0.0f32; vector.dims]; let sparse = vector.as_f32_sparse(); tracing::info!("{:?}", sparse); for (&idx, &value) in sparse.idx.iter().zip(sparse.values.iter()) { dense[idx as usize] = value; } format_text(dense.iter()) } VectorType::Float1Bit => { // Each bit → +1 or -1 let data = vector.as_1bit_data(); let values: Vec = (0..vector.dims) .map(|i| { if (data[i / 8] >> (i & 7)) & 1 == 1 { 1 } else { -1 } }) .collect(); format_text(values.iter()) } VectorType::Float8 => { // Dequantize: f_i = alpha * q_i + shift let (quantized, alpha, shift) = vector.as_f8_data(); let values: Vec = quantized .iter() .map(|&q| alpha * (q as f32) + shift) .collect(); format_text(values.iter()) } } } fn format_text(values: impl Iterator) -> String { let mut text = String::new(); text.push('['); let mut first = true; for value in values { if !first { text.push(','); } first = false; text.push_str(&value.to_string()); } text.push(']'); text } /// Parse a vector in text representation into a Vector. /// /// The format of a vector in text representation looks as follows: /// /// ```console /// [1.0, 2.0, 3.0] /// ``` pub fn vector_from_text(vector_type: VectorType, text: &str) -> Result { let text = text.trim(); let mut chars = text.chars(); if chars.next() != Some('[') || chars.last() != Some(']') { return Err(LimboError::ConversionError( "Invalid vector value".to_string(), )); } let text = &text[1..text.len() - 1]; if text.trim().is_empty() { return Ok(match vector_type { VectorType::Float1Bit => { return Err(LimboError::ConversionError( "empty vector not supported for this type".to_string(), )); } VectorType::Float8 => { return Ok(Vector::from_f8(0, Vec::new(), 0.0, 0.0)); } _ => Vector { vector_type, dims: 0, owned: Some(Vec::new()), refer: None, }, }); } let tokens = text.split(',').map(|x| x.trim()); match vector_type { VectorType::Float32Dense => vector32_from_text(tokens), VectorType::Float64Dense => vector64_from_text(tokens), VectorType::Float32Sparse => vector32_sparse_from_text(tokens), VectorType::Float1Bit => vector_1bit_from_text(tokens), VectorType::Float8 => vector_f8_from_text(tokens), } } fn vector32_from_text<'a>(tokens: impl Iterator) -> Result> { let mut data = Vec::new(); for token in tokens { let value = token .parse::() .map_err(|_| LimboError::ConversionError("Invalid vector value".to_string()))?; if !value.is_finite() { return Err(LimboError::ConversionError( "Invalid vector value".to_string(), )); } data.extend_from_slice(&value.to_le_bytes()); } Ok(Vector { vector_type: VectorType::Float32Dense, dims: data.len() / 4, owned: Some(data), refer: None, }) } fn vector64_from_text<'a>(tokens: impl Iterator) -> Result> { let mut data = Vec::new(); for token in tokens { let value = token .parse::() .map_err(|_| LimboError::ConversionError("Invalid vector value".to_string()))?; if !value.is_finite() { return Err(LimboError::ConversionError( "Invalid vector value".to_string(), )); } data.extend_from_slice(&value.to_le_bytes()); } Ok(Vector { vector_type: VectorType::Float64Dense, dims: data.len() / 8, owned: Some(data), refer: None, }) } fn vector32_sparse_from_text<'a>(tokens: impl Iterator) -> Result> { let mut idx = Vec::new(); let mut values = Vec::new(); let mut dims = 0u32; for token in tokens { let value = token .parse::() .map_err(|_| LimboError::ConversionError("Invalid vector value".to_string()))?; if !value.is_finite() { return Err(LimboError::ConversionError( "Invalid vector value".to_string(), )); } dims += 1; if value == 0.0 { continue; } idx.extend_from_slice(&(dims - 1).to_le_bytes()); values.extend_from_slice(&value.to_le_bytes()); } values.extend_from_slice(&idx); Ok(Vector { vector_type: VectorType::Float32Sparse, dims: dims as usize, owned: Some(values), refer: None, }) } fn vector_1bit_from_text<'a>(tokens: impl Iterator) -> Result> { let mut floats = Vec::new(); for token in tokens { let value = token .parse::() .map_err(|_| LimboError::ConversionError("Invalid vector value".to_string()))?; if !value.is_finite() { return Err(LimboError::ConversionError( "Invalid vector value".to_string(), )); } floats.push(value); } let dims = floats.len(); let byte_count = dims.div_ceil(8); let mut bits = vec![0u8; byte_count]; for (i, &f) in floats.iter().enumerate() { if f > 0.0 { bits[i / 8] |= 1 << (i & 7); } } Ok(Vector::from_1bit(dims, bits)) } fn vector_f8_from_text<'a>(tokens: impl Iterator) -> Result> { let mut floats = Vec::new(); for token in tokens { let value = token .parse::() .map_err(|_| LimboError::ConversionError("Invalid vector value".to_string()))?; if !value.is_finite() { return Err(LimboError::ConversionError( "Invalid vector value".to_string(), )); } floats.push(value); } let dims = floats.len(); let min_val = floats.iter().cloned().fold(f32::INFINITY, f32::min); let max_val = floats.iter().cloned().fold(f32::NEG_INFINITY, f32::max); let alpha = (max_val - min_val) / 255.0; let shift = min_val; let mut quantized = Vec::with_capacity(dims); for &f in &floats { let q = if alpha == 0.0 { 0u8 } else { let v = (f - shift) / alpha + 0.5; (v as i32).clamp(0, 255) as u8 }; quantized.push(q); } Ok(Vector::from_f8(dims, quantized, alpha, shift)) } ================================================ FILE: core/vector/vector_types.rs ================================================ use crate::turso_debug_assert; use crate::{LimboError, Result}; #[derive(Debug, Clone, PartialEq, Copy)] pub enum VectorType { Float32Dense, Float64Dense, Float32Sparse, Float1Bit, Float8, } #[derive(Debug)] pub struct Vector<'a> { pub vector_type: VectorType, pub dims: usize, pub owned: Option>, pub refer: Option<&'a [u8]>, } #[derive(Debug)] pub struct VectorSparse<'a, T: std::fmt::Debug> { pub idx: &'a [u32], pub values: &'a [T], } impl<'a> Vector<'a> { /// Returns (VectorType, data_length, dims) from a serialized blob. /// `data_length` is the number of bytes of actual vector data (before meta/type bytes). /// `dims` is the number of vector dimensions (only meaningful for Float1Bit/Float8 where /// it can't be inferred from data length alone; for other types it's set to 0 and /// computed later in from_data). pub fn vector_type(blob: &[u8]) -> Result<(VectorType, usize, usize)> { // Even-sized blobs are always float32. if blob.len() % 2 == 0 { return Ok((VectorType::Float32Dense, blob.len(), 0)); } // Odd-sized blobs have type byte at the end let vector_type = blob[blob.len() - 1]; /* vector types used by LibSQL: (see https://github.com/tursodatabase/libsql/blob/a55bf61192bdb89e97568de593c4af5b70d24bde/libsql-sqlite3/src/vectorInt.h#L52) #define VECTOR_TYPE_FLOAT32 1 #define VECTOR_TYPE_FLOAT64 2 #define VECTOR_TYPE_FLOAT1BIT 3 #define VECTOR_TYPE_FLOAT8 4 #define VECTOR_TYPE_FLOAT16 5 #define VECTOR_TYPE_FLOATB16 6 */ match vector_type { 1 => Ok((VectorType::Float32Dense, blob.len() - 1, 0)), 2 => Ok((VectorType::Float64Dense, blob.len() - 1, 0)), 3 => { // Float1Bit: [data bytes][optional padding][trailing_bits][0x03] let n_blob_size = blob.len() - 1; // without type byte if n_blob_size == 0 || n_blob_size % 2 != 0 { return Err(LimboError::ConversionError( "float1bit vector blob length must be even and non-empty".to_string(), )); } let trailing_bits = blob[n_blob_size - 1] as usize; let dims = n_blob_size * 8 - trailing_bits; let data_size = dims.div_ceil(8); Ok((VectorType::Float1Bit, data_size, dims)) } 4 => { // Float8: [quantized bytes][alignment padding][alpha f32][shift f32][padding 0x00][trailing_bytes][0x04] let n_blob_size = blob.len() - 1; // without type byte if n_blob_size < 2 || n_blob_size % 2 != 0 { return Err(LimboError::ConversionError( "float8 vector blob must have even length >= 2 (excluding type byte)" .to_string(), )); } let trailing_bytes = blob[n_blob_size - 1] as usize; let dims = (n_blob_size - 2) - 8 - trailing_bytes; // data_size = ALIGN(dims, 4) + 8 let data_size = n_blob_size - 2; Ok((VectorType::Float8, data_size, dims)) } 5..=6 => Err(LimboError::ConversionError( "unsupported vector type from LibSQL".to_string(), )), 9 => Ok((VectorType::Float32Sparse, blob.len() - 1, 0)), _ => Err(LimboError::ConversionError(format!( "unknown vector type: {vector_type}" ))), } } pub fn from_f32(mut values_f32: Vec) -> Self { let dims = values_f32.len(); let values = unsafe { Vec::from_raw_parts( values_f32.as_mut_ptr() as *mut u8, values_f32.len() * 4, values_f32.capacity() * 4, ) }; std::mem::forget(values_f32); Self { vector_type: VectorType::Float32Dense, dims, owned: Some(values), refer: None, } } pub fn from_f64(mut values_f64: Vec) -> Self { let dims = values_f64.len(); let values = unsafe { Vec::from_raw_parts( values_f64.as_mut_ptr() as *mut u8, values_f64.len() * 8, values_f64.capacity() * 8, ) }; std::mem::forget(values_f64); Self { vector_type: VectorType::Float64Dense, dims, owned: Some(values), refer: None, } } pub fn from_f32_sparse(dims: usize, mut values_f32: Vec, mut idx_u32: Vec) -> Self { let mut values = unsafe { Vec::from_raw_parts( values_f32.as_mut_ptr() as *mut u8, values_f32.len() * 4, values_f32.capacity() * 4, ) }; std::mem::forget(values_f32); let idx = unsafe { Vec::from_raw_parts( idx_u32.as_mut_ptr() as *mut u8, idx_u32.len() * 4, idx_u32.capacity() * 4, ) }; std::mem::forget(idx_u32); values.extend_from_slice(&idx); Self { vector_type: VectorType::Float32Sparse, dims, owned: Some(values), refer: None, } } fn align4(n: usize) -> usize { n.div_ceil(4) * 4 } pub fn from_1bit(dims: usize, bits: Vec) -> Self { debug_assert!(bits.len() == dims.div_ceil(8)); Self { vector_type: VectorType::Float1Bit, dims, owned: Some(bits), refer: None, } } pub fn from_f8(dims: usize, quantized: Vec, alpha: f32, shift: f32) -> Self { let aligned = Self::align4(dims); let mut data = Vec::with_capacity(aligned + 8); data.extend_from_slice(&quantized); data.resize(aligned, 0); // alignment padding data.extend_from_slice(&alpha.to_le_bytes()); data.extend_from_slice(&shift.to_le_bytes()); debug_assert!(data.len() == aligned + 8); Self { vector_type: VectorType::Float8, dims, owned: Some(data), refer: None, } } pub fn from_vec(mut blob: Vec) -> Result { let (vector_type, len, explicit_dims) = Self::vector_type(&blob)?; blob.truncate(len); Self::from_data_with_dims(vector_type, Some(blob), None, explicit_dims) } pub fn from_slice(blob: &'a [u8]) -> Result { let (vector_type, len, explicit_dims) = Self::vector_type(blob)?; Self::from_data_with_dims(vector_type, None, Some(&blob[..len]), explicit_dims) } pub fn from_data( vector_type: VectorType, owned: Option>, refer: Option<&'a [u8]>, ) -> Result { Self::from_data_with_dims(vector_type, owned, refer, 0) } fn from_data_with_dims( vector_type: VectorType, owned: Option>, refer: Option<&'a [u8]>, explicit_dims: usize, ) -> Result { let owned_slice = owned.as_deref(); let refer_slice = refer.as_ref().map(|&x| x); let data = owned_slice.or(refer_slice).ok_or_else(|| { LimboError::InternalError("Vector must have either owned or refer data".to_string()) })?; match vector_type { VectorType::Float32Dense => { if data.len() % 4 != 0 { return Err(LimboError::InvalidArgument(format!( "f32 dense vector unexpected data length: {}", data.len(), ))); } Ok(Vector { vector_type, dims: data.len() / 4, owned, refer, }) } VectorType::Float64Dense => { if data.len() % 8 != 0 { return Err(LimboError::InvalidArgument(format!( "f64 dense vector unexpected data length: {}", data.len(), ))); } Ok(Vector { vector_type, dims: data.len() / 8, owned, refer, }) } VectorType::Float32Sparse => { if data.is_empty() || data.len() % 4 != 0 || (data.len() - 4) % 8 != 0 { return Err(LimboError::InvalidArgument(format!( "f32 sparse vector unexpected data length: {}", data.len(), ))); } let original_len = data.len(); let dims_bytes = &data[original_len - 4..]; let dims = u32::from_le_bytes([ dims_bytes[0], dims_bytes[1], dims_bytes[2], dims_bytes[3], ]) as usize; let owned = owned.map(|mut x| { x.truncate(original_len - 4); x }); let refer = refer.map(|x| &x[0..original_len - 4]); Ok(Vector { vector_type, dims, owned, refer, }) } VectorType::Float1Bit => { let expected_len = explicit_dims.div_ceil(8); if explicit_dims == 0 || data.len() != expected_len { return Err(LimboError::InvalidArgument(format!( "f1bit vector data length mismatch: got {} expected {} for {} dims", data.len(), expected_len, explicit_dims, ))); } Ok(Vector { vector_type, dims: explicit_dims, owned, refer, }) } VectorType::Float8 => { if data.len() < 8 { return Err(LimboError::InvalidArgument(format!( "f8 vector data too short: {}", data.len(), ))); } let expected_len = Self::align4(explicit_dims) + 8; if explicit_dims == 0 || data.len() != expected_len { return Err(LimboError::InvalidArgument(format!( "f8 vector data length mismatch: got {} expected {} for {} dims", data.len(), expected_len, explicit_dims, ))); } Ok(Vector { vector_type, dims: explicit_dims, owned, refer, }) } } } pub fn bin_len(&self) -> usize { let owned = self.owned.as_ref().map(|x| x.len()); let refer = self.refer.as_ref().map(|x| x.len()); owned .or(refer) .expect("Vector invariant: exactly one of owned or refer must be Some") } pub fn bin_data(&'a self) -> &'a [u8] { let owned = self.owned.as_deref(); let refer = self.refer.as_ref().map(|&x| x); owned .or(refer) .expect("Vector invariant: exactly one of owned or refer must be Some") } pub fn bin_eject(self) -> Vec { self.owned.unwrap_or_else(|| { self.refer .expect("Vector invariant: exactly one of owned or refer must be Some") .to_vec() }) } /// # Safety /// /// This method is used to reinterpret the underlying `Vec` data /// as a `&[f32]` slice. This is only valid if: /// - The buffer is correctly aligned for `f32` /// - The length of the buffer is exactly `dims * size_of::()` pub fn as_f32_slice(&self) -> &[f32] { turso_debug_assert!(self.vector_type == VectorType::Float32Dense); if self.dims == 0 { return &[]; } assert_eq!( self.bin_len(), self.dims * std::mem::size_of::(), "data length must equal dims * size_of::()" ); let ptr = self.bin_data().as_ptr(); let align = std::mem::align_of::(); assert_eq!( ptr.align_offset(align), 0, "data pointer must be aligned to {align} bytes for f32 access" ); unsafe { std::slice::from_raw_parts(ptr as *const f32, self.dims) } } /// # Safety /// /// This method is used to reinterpret the underlying `Vec` data /// as a `&[f64]` slice. This is only valid if: /// - The buffer is correctly aligned for `f64` /// - The length of the buffer is exactly `dims * size_of::()` pub fn as_f64_slice(&self) -> &[f64] { turso_debug_assert!(self.vector_type == VectorType::Float64Dense); if self.dims == 0 { return &[]; } assert_eq!( self.bin_len(), self.dims * std::mem::size_of::(), "data length must equal dims * size_of::()" ); let ptr = self.bin_data().as_ptr(); let align = std::mem::align_of::(); assert_eq!( ptr.align_offset(align), 0, "data pointer must be aligned to {align} bytes for f64 access" ); unsafe { std::slice::from_raw_parts(ptr as *const f64, self.dims) } } pub fn as_f32_sparse(&self) -> VectorSparse<'_, f32> { turso_debug_assert!(self.vector_type == VectorType::Float32Sparse); let ptr = self.bin_data().as_ptr(); let align = std::mem::align_of::(); assert_eq!( ptr.align_offset(align), 0, "data pointer must be aligned to {align} bytes for f32 access" ); let length = self.bin_data().len() / 4 / 2; let values = unsafe { std::slice::from_raw_parts(ptr as *const f32, length) }; let idx = unsafe { std::slice::from_raw_parts((ptr as *const u32).add(length), length) }; turso_debug_assert!(idx.is_sorted()); VectorSparse { idx, values } } /// Returns the raw bit-packed bytes for a Float1Bit vector. /// Bit `i` is at byte `i/8`, position `i & 7`. pub fn as_1bit_data(&self) -> &[u8] { debug_assert!(self.vector_type == VectorType::Float1Bit); let data = self.bin_data(); &data[..self.dims.div_ceil(8)] } /// Returns (quantized_bytes, alpha, shift) for a Float8 vector. /// Dequantization: `f_i = alpha * q_i + shift` pub fn as_f8_data(&self) -> (&[u8], f32, f32) { debug_assert!(self.vector_type == VectorType::Float8); let data = self.bin_data(); let aligned = Self::align4(self.dims); let alpha = f32::from_le_bytes([ data[aligned], data[aligned + 1], data[aligned + 2], data[aligned + 3], ]); let shift = f32::from_le_bytes([ data[aligned + 4], data[aligned + 5], data[aligned + 6], data[aligned + 7], ]); (&data[..self.dims], alpha, shift) } } #[cfg(test)] pub(crate) mod tests { use crate::vector::operations; use super::*; use quickcheck::{Arbitrary, Gen}; use quickcheck_macros::quickcheck; // Helper to generate arbitrary vectors of specific type and dimensions #[derive(Debug, Clone)] pub struct ArbitraryVector { vector_type: VectorType, data: Vec, } /// How to create an arbitrary vector of DIMS dims. impl ArbitraryVector { fn generate_f32_vector(g: &mut Gen) -> Vec { (0..DIMS) .map(|_| { loop { // generate zeroes with some probability since we have support for sparse vectors if bool::arbitrary(g) { return 0.0; } let f = f32::arbitrary(g); // f32::arbitrary() can generate "problem values" like NaN, infinity, and very small values // Skip these values if f.is_finite() && f.abs() >= 1e-6 { // Scale to [-1, 1] range return f % 2.0 - 1.0; } } }) .collect() } fn generate_f64_vector(g: &mut Gen) -> Vec { (0..DIMS) .map(|_| { loop { // generate zeroes with some probability since we have support for sparse vectors if bool::arbitrary(g) { return 0.0; } let f = f64::arbitrary(g); // f64::arbitrary() can generate "problem values" like NaN, infinity, and very small values // Skip these values if f.is_finite() && f.abs() >= 1e-6 { // Scale to [-1, 1] range return f % 2.0 - 1.0; } } }) .collect() } } /// Convert an ArbitraryVector to a Vector. impl From> for Vector<'static> { fn from(v: ArbitraryVector) -> Self { Vector { vector_type: v.vector_type, dims: DIMS, owned: Some(v.data), refer: None, } } } /// Implement the quickcheck Arbitrary trait for ArbitraryVector. impl Arbitrary for ArbitraryVector { fn arbitrary(g: &mut Gen) -> Self { let choice = u8::arbitrary(g) % 4; let vector_type = match choice { 0 => VectorType::Float32Dense, 1 => VectorType::Float64Dense, 2 => VectorType::Float1Bit, _ => VectorType::Float8, }; let data = match vector_type { VectorType::Float32Dense => { let floats = Self::generate_f32_vector(g); floats.iter().flat_map(|f| f.to_le_bytes()).collect() } VectorType::Float64Dense => { let floats = Self::generate_f64_vector(g); floats.iter().flat_map(|f| f.to_le_bytes()).collect() } VectorType::Float1Bit => { // Generate random bits let byte_count = DIMS.div_ceil(8); let mut bits = vec![0u8; byte_count]; for b in bits.iter_mut() { *b = u8::arbitrary(g); } // Mask off unused bits in the last byte if DIMS % 8 != 0 { let mask = (1u8 << (DIMS % 8)) - 1; if let Some(last) = bits.last_mut() { *last &= mask; } } bits } VectorType::Float8 => { // Generate random quantized values + alpha/shift let floats = Self::generate_f32_vector(g); let min_val = floats.iter().cloned().fold(f32::INFINITY, f32::min); let max_val = floats.iter().cloned().fold(f32::NEG_INFINITY, f32::max); let alpha = (max_val - min_val) / 255.0; let shift = min_val; let aligned = DIMS.div_ceil(4) * 4; let mut data = Vec::with_capacity(aligned + 8); for &f in &floats { let q = if alpha == 0.0 { 0u8 } else { ((f - shift) / alpha + 0.5) as u8 }; data.push(q); } data.resize(aligned, 0); // alignment padding data.extend_from_slice(&alpha.to_le_bytes()); data.extend_from_slice(&shift.to_le_bytes()); data } _ => unreachable!(), }; ArbitraryVector { vector_type, data } } } #[quickcheck] fn prop_vector_type_identification_2d(v: ArbitraryVector<2>) -> bool { test_vector_type::<2>(v.into()) } #[quickcheck] fn prop_vector_type_identification_3d(v: ArbitraryVector<3>) -> bool { test_vector_type::<3>(v.into()) } #[quickcheck] fn prop_vector_type_identification_4d(v: ArbitraryVector<4>) -> bool { test_vector_type::<4>(v.into()) } #[quickcheck] fn prop_vector_type_identification_100d(v: ArbitraryVector<100>) -> bool { test_vector_type::<100>(v.into()) } #[quickcheck] fn prop_vector_type_identification_1536d(v: ArbitraryVector<1536>) -> bool { test_vector_type::<1536>(v.into()) } /// Test if the vector type identification is correct for a given vector. fn test_vector_type(v: Vector) -> bool { let vtype = v.vector_type; let value = operations::serialize::vector_serialize(v); let blob = value.to_blob().unwrap().to_vec(); match Vector::vector_type(&blob) { Ok((detected_type, _, _)) => detected_type == vtype, Err(_) => false, } } #[quickcheck] fn prop_slice_conversion_safety_2d(v: ArbitraryVector<2>) -> bool { test_slice_conversion::<2>(v.into()) } #[quickcheck] fn prop_slice_conversion_safety_3d(v: ArbitraryVector<3>) -> bool { test_slice_conversion::<3>(v.into()) } #[quickcheck] fn prop_slice_conversion_safety_4d(v: ArbitraryVector<4>) -> bool { test_slice_conversion::<4>(v.into()) } #[quickcheck] fn prop_slice_conversion_safety_100d(v: ArbitraryVector<100>) -> bool { test_slice_conversion::<100>(v.into()) } #[quickcheck] fn prop_slice_conversion_safety_1536d(v: ArbitraryVector<1536>) -> bool { test_slice_conversion::<1536>(v.into()) } /// Test if the slice conversion is safe for a given vector: /// - The slice length matches the dimensions /// - The data length is correct (4 bytes per float for f32, 8 bytes per float for f64) fn test_slice_conversion(v: Vector) -> bool { match v.vector_type { VectorType::Float32Dense => { let slice = v.as_f32_slice(); slice.len() == DIMS && (slice.len() * 4 == v.bin_len()) } VectorType::Float64Dense => { let slice = v.as_f64_slice(); slice.len() == DIMS && (slice.len() * 8 == v.bin_len()) } VectorType::Float1Bit => { let data = v.as_1bit_data(); data.len() == DIMS.div_ceil(8) && v.dims == DIMS } VectorType::Float8 => { let (quantized, _alpha, _shift) = v.as_f8_data(); quantized.len() == DIMS && v.dims == DIMS } _ => true, } } #[quickcheck] fn prop_vector_distance_safety_2d(v1: ArbitraryVector<2>, v2: ArbitraryVector<2>) -> bool { test_vector_distance::<2>(&v1.into(), &v2.into()) } #[quickcheck] fn prop_vector_distance_safety_3d(v1: ArbitraryVector<3>, v2: ArbitraryVector<3>) -> bool { test_vector_distance::<3>(&v1.into(), &v2.into()) } #[quickcheck] fn prop_vector_distance_safety_4d(v1: ArbitraryVector<4>, v2: ArbitraryVector<4>) -> bool { test_vector_distance::<4>(&v1.into(), &v2.into()) } #[quickcheck] fn prop_vector_distance_safety_100d( v1: ArbitraryVector<100>, v2: ArbitraryVector<100>, ) -> bool { test_vector_distance::<100>(&v1.into(), &v2.into()) } #[quickcheck] fn prop_vector_distance_safety_1536d( v1: ArbitraryVector<1536>, v2: ArbitraryVector<1536>, ) -> bool { test_vector_distance::<1536>(&v1.into(), &v2.into()) } /// Test if the vector distance calculation is correct for a given pair of vectors: /// - Skips cases with invalid input vectors. /// - Assumes vectors are well-formed (same type and dimension) fn test_vector_distance(v1: &Vector, v2: &Vector) -> bool { match operations::distance_cos::vector_distance_cos(v1, v2) { Ok(distance) => { if distance.is_nan() { return true; } match v1.vector_type { // Float1Bit cosine distance returns hamming distance [0, dims] VectorType::Float1Bit => (0.0 - 1e-6..=DIMS as f64 + 1e-6).contains(&distance), // Normal cosine distance [0, 2] _ => (0.0 - 1e-6..=2.0 + 1e-6).contains(&distance), } } Err(_) => true, } } #[test] fn test_vector_some_cosine_dist() { let a = Vector { vector_type: VectorType::Float32Dense, dims: 2, owned: Some(vec![0, 0, 0, 0, 52, 208, 106, 63]), refer: None, }; let b = Vector { vector_type: VectorType::Float32Dense, dims: 2, owned: Some(vec![0, 0, 0, 0, 58, 100, 45, 192]), refer: None, }; assert!( (operations::distance_cos::vector_distance_cos(&a, &b).unwrap() - 2.0).abs() <= 1e-6 ); } #[test] fn parse_string_vector_zero_length() { let vector = operations::text::vector_from_text(VectorType::Float32Dense, "[]").unwrap(); assert_eq!(vector.dims, 0); assert_eq!(vector.vector_type, VectorType::Float32Dense); } #[test] fn parse_string_vector_f8_zero_length() { let vector = operations::text::vector_from_text(VectorType::Float8, "[]").unwrap(); assert_eq!(vector.dims, 0); assert_eq!(vector.vector_type, VectorType::Float8); let (quantized, alpha, shift) = vector.as_f8_data(); assert!(quantized.is_empty()); assert_eq!(alpha, 0.0); assert_eq!(shift, 0.0); } #[test] fn test_parse_string_vector_valid_whitespace() { let vector = operations::text::vector_from_text( VectorType::Float32Dense, " [ 1.0 , 2.0 , 3.0 ] ", ) .unwrap(); assert_eq!(vector.dims, 3); assert_eq!(vector.vector_type, VectorType::Float32Dense); } #[test] fn test_parse_string_vector_valid() { let vector = operations::text::vector_from_text(VectorType::Float32Dense, "[1.0, 2.0, 3.0]") .unwrap(); assert_eq!(vector.dims, 3); assert_eq!(vector.vector_type, VectorType::Float32Dense); } #[quickcheck] fn prop_vector_text_roundtrip_2d(v: ArbitraryVector<2>) -> bool { test_vector_text_roundtrip(v.into()) } #[quickcheck] fn prop_vector_text_roundtrip_3d(v: ArbitraryVector<3>) -> bool { test_vector_text_roundtrip(v.into()) } #[quickcheck] fn prop_vector_text_roundtrip_4d(v: ArbitraryVector<4>) -> bool { test_vector_text_roundtrip(v.into()) } #[quickcheck] fn prop_vector_text_roundtrip_100d(v: ArbitraryVector<100>) -> bool { test_vector_text_roundtrip(v.into()) } #[quickcheck] fn prop_vector_text_roundtrip_1536d(v: ArbitraryVector<1536>) -> bool { test_vector_text_roundtrip(v.into()) } /// Test that a vector can be converted to text and back without loss of precision fn test_vector_text_roundtrip(v: Vector) -> bool { // Convert to text let text = operations::text::vector_to_text(&v); // Parse back from text let parsed = operations::text::vector_from_text(v.vector_type, &text); match parsed { Ok(parsed_vector) => { // Check dimensions match if v.dims != parsed_vector.dims { return false; } match v.vector_type { VectorType::Float32Dense => { let original = v.as_f32_slice(); let parsed = parsed_vector.as_f32_slice(); original.iter().zip(parsed.iter()).all(|(a, b)| a == b) } VectorType::Float64Dense => { let original = v.as_f64_slice(); let parsed = parsed_vector.as_f64_slice(); original.iter().zip(parsed.iter()).all(|(a, b)| a == b) } VectorType::Float1Bit => { // 1bit text roundtrip: bits → +1/-1 text → threshold → bits let original = v.as_1bit_data(); let parsed_data = parsed_vector.as_1bit_data(); original == parsed_data } VectorType::Float8 => { // Float8 text roundtrip: lossy due to quantization // Just check dims match (re-quantization changes alpha/shift) true } _ => true, } } Err(_) => false, } } } ================================================ FILE: core/vtab.rs ================================================ use crate::pragma::{PragmaVirtualTable, PragmaVirtualTableCursor}; use crate::schema::Column; use crate::sync::atomic::{AtomicPtr, AtomicU64, Ordering}; use crate::sync::{Arc, RwLock, Weak}; use crate::util::columns_from_create_table_body; use crate::{Connection, LimboError, SymbolTable, Value}; use std::ffi::c_void; use std::ptr::NonNull; use turso_ext::{ConstraintInfo, IndexInfo, OrderByInfo, ResultCode, VTabKind, VTabModuleImpl}; use turso_parser::{ast, parser::Parser}; #[derive(Debug, Clone)] pub(crate) enum VirtualTableType { Pragma(PragmaVirtualTable), External(ExtVirtualTable), Internal(Arc>), } #[derive(Clone, Debug)] pub struct VirtualTable { pub(crate) name: String, pub(crate) columns: Vec, pub(crate) kind: VTabKind, vtab_type: VirtualTableType, // identifier to tie a cursor to a specific instantiated virtual table instance vtab_id: u64, // Whether this virtual table is safe to use from within triggers and views. // Corresponds to SQLite's SQLITE_VTAB_INNOCUOUS flag. pub(crate) innocuous: bool, } impl VirtualTable { pub(crate) fn id(&self) -> u64 { self.vtab_id } pub(crate) fn readonly(&self) -> bool { match &self.vtab_type { VirtualTableType::Pragma(_) => true, VirtualTableType::External(table) => table.readonly(), VirtualTableType::Internal(_) => true, } } #[cfg(feature = "cli_only")] fn dbpage_virtual_table() -> Arc { let dbpage_table = crate::dbpage::DbPageTable::new(); let dbpage_vtab = VirtualTable { name: dbpage_table.name(), columns: Self::resolve_columns(dbpage_table.sql()) .expect("sqlite_dbpage schema resolution should not fail"), kind: VTabKind::TableValuedFunction, vtab_type: VirtualTableType::Internal(Arc::new(RwLock::new(dbpage_table))), vtab_id: 0, innocuous: true, }; Arc::new(dbpage_vtab) } #[cfg(feature = "cli_only")] fn btree_dump_virtual_table() -> Arc { let btree_dump_table = crate::btree_dump::BtreeDumpTable::new(); let vtab = VirtualTable { name: btree_dump_table.name(), columns: Self::resolve_columns(btree_dump_table.sql()) .expect("btree_dump schema resolution should not fail"), kind: VTabKind::TableValuedFunction, vtab_type: VirtualTableType::Internal(Arc::new(RwLock::new(btree_dump_table))), vtab_id: 0, innocuous: true, }; Arc::new(vtab) } fn turso_types_virtual_table() -> Arc { let table = crate::turso_types_vtab::TursoTypesTable::new(); let vtab = VirtualTable { name: table.name(), columns: Self::resolve_columns(table.sql()) .expect("sqlite_turso_types schema resolution should not fail"), kind: VTabKind::TableValuedFunction, vtab_type: VirtualTableType::Internal(Arc::new(RwLock::new(table))), vtab_id: 0, innocuous: true, }; Arc::new(vtab) } pub(crate) fn builtin_functions(enable_custom_types: bool) -> Vec> { let mut vtables: Vec> = PragmaVirtualTable::functions() .into_iter() .map(|(tab, schema)| { let vtab = VirtualTable { name: format!("pragma_{}", tab.pragma_name), columns: Self::resolve_columns(schema) .expect("pragma table-valued function schema resolution should not fail"), kind: VTabKind::TableValuedFunction, vtab_type: VirtualTableType::Pragma(tab), vtab_id: 0, innocuous: true, }; Arc::new(vtab) }) .collect(); #[cfg(feature = "json")] vtables.extend(Self::json_virtual_tables()); #[cfg(feature = "cli_only")] vtables.push(Self::dbpage_virtual_table()); #[cfg(feature = "cli_only")] vtables.push(Self::btree_dump_virtual_table()); if enable_custom_types { vtables.push(Self::turso_types_virtual_table()); } vtables } #[cfg(feature = "json")] fn json_virtual_tables() -> Vec> { use crate::json::vtab::JsonVirtualTable; let json_each = JsonVirtualTable::json_each(); let json_each_virtual_table = VirtualTable { name: json_each.name(), columns: Self::resolve_columns(json_each.sql()) .expect("internal table-valued function schema resolution should not fail"), kind: VTabKind::TableValuedFunction, vtab_type: VirtualTableType::Internal(Arc::new(RwLock::new(json_each))), vtab_id: 0, innocuous: true, }; let json_tree = JsonVirtualTable::json_tree(); let json_tree_virtual_table = VirtualTable { name: json_tree.name(), columns: Self::resolve_columns(json_tree.sql()) .expect("internal table-valued function schema resolution should not fail"), kind: VTabKind::TableValuedFunction, vtab_type: VirtualTableType::Internal(Arc::new(RwLock::new(json_tree))), vtab_id: 0, innocuous: true, }; vec![ Arc::new(json_each_virtual_table), Arc::new(json_tree_virtual_table), ] } pub(crate) fn function(name: &str, syms: &SymbolTable) -> crate::Result> { let module = syms.vtab_modules.get(name); let (vtab_type, schema) = if module.is_some() { ExtVirtualTable::create(name, module, Vec::new(), VTabKind::TableValuedFunction) .map(|(vtab, columns)| (VirtualTableType::External(vtab), columns))? } else { return Err(LimboError::ParseError(format!( "No such table-valued function: {name}" ))); }; let vtab = VirtualTable { name: name.to_owned(), columns: Self::resolve_columns(schema)?, kind: VTabKind::TableValuedFunction, vtab_type, vtab_id: 0, innocuous: false, }; Ok(Arc::new(vtab)) } pub fn table( tbl_name: Option<&str>, module_name: &str, args: Vec, syms: &SymbolTable, ) -> crate::Result> { let module = syms.vtab_modules.get(module_name); let (table, schema) = ExtVirtualTable::create(module_name, module, args, VTabKind::VirtualTable)?; let vtab = VirtualTable { name: tbl_name.unwrap_or(module_name).to_owned(), columns: Self::resolve_columns(schema)?, kind: VTabKind::VirtualTable, vtab_type: VirtualTableType::External(table), vtab_id: VTAB_ID_COUNTER.fetch_add(1, Ordering::Acquire), innocuous: false, }; Ok(Arc::new(vtab)) } fn resolve_columns(schema: String) -> crate::Result> { let mut parser = Parser::new(schema.as_bytes()); if let ast::Cmd::Stmt(ast::Stmt::CreateTable { body, .. }) = parser.next_cmd()?.ok_or_else(|| { LimboError::ParseError( "Failed to parse schema from virtual table module".to_string(), ) })? { columns_from_create_table_body(&body) } else { Err(LimboError::ParseError( "Failed to parse schema from virtual table module".to_string(), )) } } pub(crate) fn open(&self, conn: Arc) -> crate::Result { match &self.vtab_type { VirtualTableType::Pragma(table) => { Ok(VirtualTableCursor::new_pragma(table.open(conn)?)) } VirtualTableType::External(table) => Ok(VirtualTableCursor::new_external( table.open(conn, self.vtab_id)?, )), VirtualTableType::Internal(table) => { Ok(VirtualTableCursor::new_internal(table.read().open(conn)?)) } } } pub(crate) fn update(&self, args: &[Value]) -> crate::Result> { match &self.vtab_type { VirtualTableType::Pragma(_) => Err(LimboError::ReadOnly), VirtualTableType::External(table) => table.update(args), VirtualTableType::Internal(_) => Err(LimboError::ReadOnly), } } pub(crate) fn destroy(&self) -> crate::Result<()> { match &self.vtab_type { VirtualTableType::Pragma(_) => Ok(()), VirtualTableType::External(table) => table.destroy(), VirtualTableType::Internal(_) => Ok(()), } } pub(crate) fn best_index( &self, constraints: &[ConstraintInfo], order_by: &[OrderByInfo], ) -> Result { match &self.vtab_type { VirtualTableType::Pragma(table) => table.best_index(constraints), VirtualTableType::External(table) => table.best_index(constraints, order_by), VirtualTableType::Internal(table) => table.read().best_index(constraints, order_by), } } pub(crate) fn begin(&self) -> crate::Result<()> { match &self.vtab_type { VirtualTableType::Pragma(_) => Err(LimboError::ExtensionError( "Pragma virtual tables do not support transactions".to_string(), )), VirtualTableType::External(table) => table.begin(), VirtualTableType::Internal(_) => Err(LimboError::ExtensionError( "Internal virtual tables currently do not support transactions".to_string(), )), } } pub(crate) fn commit(&self) -> crate::Result<()> { match &self.vtab_type { VirtualTableType::Pragma(_) => Err(LimboError::ExtensionError( "Pragma virtual tables do not support transactions".to_string(), )), VirtualTableType::External(table) => table.commit(), VirtualTableType::Internal(_) => Err(LimboError::ExtensionError( "Internal virtual tables currently do not support transactions".to_string(), )), } } pub(crate) fn rollback(&self) -> crate::Result<()> { match &self.vtab_type { VirtualTableType::Pragma(_) => Err(LimboError::ExtensionError( "Pragma virtual tables do not support transactions".to_string(), )), VirtualTableType::External(table) => table.rollback(), VirtualTableType::Internal(_) => Err(LimboError::ExtensionError( "Internal virtual tables currently do not support transactions".to_string(), )), } } pub(crate) fn rename(&self, new_name: &str) -> crate::Result<()> { match &self.vtab_type { VirtualTableType::Pragma(_) => Err(LimboError::ExtensionError( "Pragma virtual tables do not support renaming".to_string(), )), VirtualTableType::External(table) => table.rename(new_name), VirtualTableType::Internal(_) => Err(LimboError::ExtensionError( "Internal virtual tables currently do not support renaming".to_string(), )), } } } enum VirtualTableCursorInner { Pragma(Box), External(ExtVirtualTableCursor), Internal(Arc>), } pub struct VirtualTableCursor { inner: VirtualTableCursorInner, null_flag: bool, } crate::assert::assert_send_sync!(VirtualTableCursor); impl VirtualTableCursor { pub(crate) fn new_pragma(cursor: PragmaVirtualTableCursor) -> Self { Self { inner: VirtualTableCursorInner::Pragma(Box::new(cursor)), null_flag: false, } } pub(crate) fn new_external(cursor: ExtVirtualTableCursor) -> Self { Self { inner: VirtualTableCursorInner::External(cursor), null_flag: false, } } pub(crate) fn new_internal(cursor: Arc>) -> Self { Self { inner: VirtualTableCursorInner::Internal(cursor), null_flag: false, } } pub(crate) fn set_null_flag(&mut self, flag: bool) { self.null_flag = flag; } pub(crate) fn next(&mut self) -> crate::Result { self.null_flag = false; match &mut self.inner { VirtualTableCursorInner::Pragma(cursor) => cursor.next(), VirtualTableCursorInner::External(cursor) => cursor.next(), VirtualTableCursorInner::Internal(cursor) => cursor.write().next(), } } pub(crate) fn rowid(&self) -> i64 { match &self.inner { VirtualTableCursorInner::Pragma(cursor) => cursor.rowid(), VirtualTableCursorInner::External(cursor) => cursor.rowid(), VirtualTableCursorInner::Internal(cursor) => cursor.read().rowid(), } } pub(crate) fn column(&self, column: usize) -> crate::Result { if self.null_flag { return Ok(Value::Null); } match &self.inner { VirtualTableCursorInner::Pragma(cursor) => cursor.column(column), VirtualTableCursorInner::External(cursor) => cursor.column(column), VirtualTableCursorInner::Internal(cursor) => cursor.read().column(column), } } pub(crate) fn filter( &mut self, idx_num: i32, idx_str: Option, arg_count: usize, args: Vec, ) -> crate::Result { self.null_flag = false; match &mut self.inner { VirtualTableCursorInner::Pragma(cursor) => cursor.filter(args), VirtualTableCursorInner::External(cursor) => { cursor.filter(idx_num, idx_str, arg_count, args) } VirtualTableCursorInner::Internal(cursor) => { cursor.write().filter(&args, idx_str, idx_num) } } } pub(crate) fn vtab_id(&self) -> Option { match &self.inner { VirtualTableCursorInner::Pragma(_) => None, VirtualTableCursorInner::External(cursor) => cursor.vtab_id.into(), VirtualTableCursorInner::Internal(_) => None, } } } #[derive(Debug)] pub(crate) struct ExtVirtualTable { implementation: Arc, table_ptr: AtomicPtr, } static VTAB_ID_COUNTER: AtomicU64 = AtomicU64::new(1); impl Clone for ExtVirtualTable { fn clone(&self) -> Self { Self { implementation: self.implementation.clone(), table_ptr: AtomicPtr::new(self.table_ptr.load(Ordering::SeqCst)), } } } impl ExtVirtualTable { pub(crate) fn readonly(&self) -> bool { self.implementation.readonly } fn best_index( &self, constraints: &[ConstraintInfo], order_by: &[OrderByInfo], ) -> Result { unsafe { IndexInfo::from_ffi((self.implementation.best_idx)( constraints.as_ptr(), constraints.len() as i32, order_by.as_ptr(), order_by.len() as i32, )) } } /// takes ownership of the provided Args fn create( module_name: &str, module: Option<&Arc>, args: Vec, kind: VTabKind, ) -> crate::Result<(Self, String)> { let module = module.ok_or_else(|| { LimboError::ExtensionError(format!("Virtual table module not found: {module_name}")) })?; if kind != module.module_kind { let expected = match kind { VTabKind::VirtualTable => "virtual table", VTabKind::TableValuedFunction => "table-valued function", }; return Err(LimboError::ExtensionError(format!( "{module_name} is not a {expected} module" ))); } let (schema, table_ptr) = module.implementation.create(args)?; let vtab = ExtVirtualTable { implementation: module.implementation.clone(), table_ptr: AtomicPtr::new(table_ptr as *mut c_void), }; Ok((vtab, schema)) } /// Accepts a pointer connection that owns the VTable, that the module /// can optionally use to query the other tables. fn open(&self, conn: Arc, id: u64) -> crate::Result { // we need a Weak to upgrade and call from the extension. let weak = Arc::downgrade(&conn); let weak_box = Box::into_raw(Box::new(weak)); let conn = turso_ext::Conn::new( weak_box as *mut c_void, crate::ext::prepare_stmt, crate::ext::execute, ); let ext_conn_ptr = NonNull::new(Box::into_raw(Box::new(conn))).expect("null pointer"); // store the leaked connection pointer on the table so it can be freed on drop let Some(cursor) = NonNull::new(unsafe { (self.implementation.open)( self.table_ptr.load(Ordering::SeqCst) as *const c_void, ext_conn_ptr.as_ptr(), ) as *mut c_void }) else { return Err(LimboError::ExtensionError("Open returned null".to_string())); }; ExtVirtualTableCursor::new(cursor, ext_conn_ptr, self.implementation.clone(), id) } fn update(&self, args: &[Value]) -> crate::Result> { let arg_count = args.len(); let ext_args = args.iter().map(|arg| arg.to_ffi()).collect::>(); let newrowid = 0i64; let rc = unsafe { (self.implementation.update)( self.table_ptr.load(Ordering::SeqCst) as *const c_void, arg_count as i32, ext_args.as_ptr(), &newrowid as *const _ as *mut i64, ) }; for arg in ext_args { unsafe { arg.__free_internal_type(); } } match rc { ResultCode::OK => Ok(None), ResultCode::RowID => Ok(Some(newrowid)), _ => Err(LimboError::ExtensionError(rc.to_string())), } } fn destroy(&self) -> crate::Result<()> { let rc = unsafe { (self.implementation.destroy)(self.table_ptr.load(Ordering::SeqCst) as *const c_void) }; match rc { ResultCode::OK => Ok(()), _ => Err(LimboError::ExtensionError(rc.to_string())), } } fn commit(&self) -> crate::Result<()> { let rc = unsafe { (self.implementation.commit)(self.table_ptr.load(Ordering::SeqCst)) }; match rc { ResultCode::OK => Ok(()), _ => Err(LimboError::ExtensionError("Commit failed".to_string())), } } fn begin(&self) -> crate::Result<()> { let rc = unsafe { (self.implementation.begin)(self.table_ptr.load(Ordering::SeqCst)) }; match rc { ResultCode::OK => Ok(()), _ => Err(LimboError::ExtensionError("Begin failed".to_string())), } } fn rollback(&self) -> crate::Result<()> { let rc = unsafe { (self.implementation.rollback)(self.table_ptr.load(Ordering::SeqCst)) }; match rc { ResultCode::OK => Ok(()), _ => Err(LimboError::ExtensionError("Rollback failed".to_string())), } } fn rename(&self, new_name: &str) -> crate::Result<()> { let c_new_name = std::ffi::CString::new(new_name).unwrap(); let rc = unsafe { (self.implementation.rename)(self.table_ptr.load(Ordering::SeqCst), c_new_name.as_ptr()) }; match rc { ResultCode::OK => Ok(()), _ => Err(LimboError::ExtensionError("Rename failed".to_string())), } } } pub struct ExtVirtualTableCursor { cursor: NonNull, // the core `[Connection]` pointer the vtab module needs to // query other internal tables. conn_ptr: Option>, implementation: Arc, vtab_id: u64, } // SAFETY: Extension provider must guarantee Send + Sync on their side // we cannot properly infer Send + Sync for dynamic libraries unsafe impl Send for ExtVirtualTableCursor {} unsafe impl Sync for ExtVirtualTableCursor {} crate::assert::assert_send_sync!(ExtVirtualTableCursor); impl ExtVirtualTableCursor { fn new( cursor: NonNull, conn_ptr: NonNull, implementation: Arc, id: u64, ) -> crate::Result { Ok(Self { cursor, conn_ptr: Some(conn_ptr), implementation, vtab_id: id, }) } fn rowid(&self) -> i64 { unsafe { (self.implementation.rowid)(self.cursor.as_ptr()) } } #[tracing::instrument(skip(self))] fn filter( &self, idx_num: i32, idx_str: Option, arg_count: usize, args: Vec, ) -> crate::Result { tracing::trace!("xFilter"); let ext_args = args.iter().map(|arg| arg.to_ffi()).collect::>(); let idx_str = match idx_str { Some(idx_str) => Some(std::ffi::CString::new(idx_str).map_err(|e| { crate::LimboError::InternalError(format!("failed to convert idx_str string: {e}")) })?), None => None, }; let c_idx_str_ptr = idx_str .as_ref() .map(|s| s.as_ptr()) .unwrap_or(std::ptr::null_mut()); let rc = unsafe { (self.implementation.filter)( self.cursor.as_ptr(), arg_count as i32, ext_args.as_ptr(), c_idx_str_ptr, idx_num, ) }; for arg in ext_args { unsafe { arg.__free_internal_type(); } } match rc { ResultCode::OK => Ok(true), ResultCode::EOF => Ok(false), _ => Err(LimboError::ExtensionError(rc.to_string())), } } fn column(&self, column: usize) -> crate::Result { let val = unsafe { (self.implementation.column)(self.cursor.as_ptr(), column as u32) }; Value::from_ffi(val) } fn next(&self) -> crate::Result { let rc = unsafe { (self.implementation.next)(self.cursor.as_ptr()) }; match rc { ResultCode::OK => Ok(true), ResultCode::EOF => Ok(false), _ => Err(LimboError::ExtensionError("Next failed".to_string())), } } } impl Drop for ExtVirtualTableCursor { fn drop(&mut self) { if let Some(ptr) = self.conn_ptr.take() { // first free the boxed turso_ext::Conn pointer itself let conn = unsafe { Box::from_raw(ptr.as_ptr()) }; if !conn._ctx.is_null() { // we also leaked the Weak 'ctx' pointer, so free this as well let _ = unsafe { Box::from_raw(conn._ctx as *mut Weak) }; } } let result = unsafe { (self.implementation.close)(self.cursor.as_ptr()) }; if !result.is_ok() { tracing::error!("Failed to close virtual table cursor"); } } } pub trait InternalVirtualTable: std::fmt::Debug + Send + Sync { fn name(&self) -> String; fn open( &self, conn: Arc, ) -> crate::Result>>; /// best_index is used by the optimizer. See the comment on `Table::best_index`. fn best_index( &self, constraints: &[turso_ext::ConstraintInfo], order_by: &[turso_ext::OrderByInfo], ) -> Result; fn sql(&self) -> String; } pub trait InternalVirtualTableCursor: Send + Sync { /// next returns `Ok(true)` if there are more rows, and `Ok(false)` otherwise. fn next(&mut self) -> Result; fn rowid(&self) -> i64; fn column(&self, column: usize) -> Result; fn filter( &mut self, args: &[Value], idx_str: Option, idx_num: i32, ) -> Result; } ================================================ FILE: deny.toml ================================================ # cargo-deny configuration # See: https://embarkstudios.github.io/cargo-deny/ [graph] # Include all targets for comprehensive checking all-features = true # Exclude internal perf/testing crates from license checks exclude = [ "encryption-throughput", "write-throughput", "write-throughput-sqlite", ] [licenses] # Use version 2 for modern cargo-deny behavior version = 2 # Confidence threshold for license detection confidence-threshold = 0.95 # List of explicitly allowed licenses # All other licenses are denied by default (including all GPL variants) allow = [ "MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", "BSD-3-Clause", "BSL-1.0", "ISC", "Zlib", "Unicode-3.0", "CC0-1.0", "Unlicense", "MPL-2.0", ] # Clarification for ring crate which has a complex license [[licenses.clarify]] name = "ring" expression = "MIT AND ISC AND OpenSSL" license-files = [ { path = "LICENSE", hash = 0xbd0eed23 }, ] # Private crates (workspace members) are ignored [licenses.private] ignore = true registries = [] ================================================ FILE: dist-workspace.toml ================================================ [workspace] members = ["cargo:."] # Config for 'dist' [dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) cargo-dist-version = "0.30.2" # CI backends to support ci = "github" # The installers to generate for each app installers = ["shell", "powershell"] # Target platforms to build apps for (Rust target-triple syntax) targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] # Which actions to run on pull requests pr-run-mode = "plan" # Path that installers should place binaries in install-path = "~/.turso" # Whether to install an updater program install-updater = true # Whether to consider the binaries in a package for distribution (defaults true) dist = false # Whether to enable GitHub Attestations github-attestations = true ================================================ FILE: docs/CODEOWNERS ================================================ * @penberg /core/ @pereman2 @jussisaurio @penberg /core/storage @pereman2 /core/translate @jussisaurio /extensions/ @PThorpe92 /core/ext/ @PThorpe92 /bindings/go @PThorpe92 # These are commented out until they get access from the overlords # /bindings/java @seonWKim # /simulator/ @alpaylan # /core/storage @krishvishal ================================================ FILE: docs/agent-guides/async-io-model.md ================================================ --- name: async-io-model description: IOResult, state machines, re-entrancy pitfalls, CompletionGroup --- # Async I/O Model Guide Turso uses cooperative yielding with explicit state machines instead of Rust async/await. ## Core Types ```rust pub enum IOCompletions { Single(Completion), } #[must_use] pub enum IOResult { Done(T), // Operation complete, here's the result IO(IOCompletions), // Need I/O, call me again after completions finish } ``` Functions returning `IOResult` must be called repeatedly until `Done`. ## Completion and CompletionGroup A `Completion` tracks a single I/O operation: ```rust pub struct Completion { /* ... */ } impl Completion { pub fn finished(&self) -> bool; pub fn succeeded(&self) -> bool; pub fn get_error(&self) -> Option; } ``` To wait for multiple I/O operations, use `CompletionGroup`: ```rust let mut group = CompletionGroup::new(|_| {}); // Add individual completions group.add(&completion1); group.add(&completion2); // Build into single completion that finishes when all complete let combined = group.build(); io_yield_one!(combined); ``` `CompletionGroup` features: - Aggregates multiple completions into one - Calls callback when all complete (or any errors) - Can nest groups (add a group's completion to another group) - Cancellable via `group.cancel()` ## Helper Macros ### `return_if_io!` Unwraps `IOResult`, propagates IO variant up the call stack: ```rust let result = return_if_io!(some_io_operation()); // Only reaches here if operation returned Done ``` ### `io_yield_one!` Yields a single completion: ```rust io_yield_one!(completion); // Returns Ok(IOResult::IO(Single(completion))) ``` ## State Machine Pattern Operations that may yield use explicit state enums: ```rust enum MyOperationState { Start, WaitingForRead { page: PageRef }, Processing { data: Vec }, Done, } ``` The function loops, matching on state and transitioning: ```rust fn my_operation(&mut self) -> Result> { loop { match &mut self.state { MyOperationState::Start => { let (page, completion) = start_read(); self.state = MyOperationState::WaitingForRead { page }; io_yield_one!(completion); } MyOperationState::WaitingForRead { page } => { let data = page.get_contents(); self.state = MyOperationState::Processing { data: data.to_vec() }; // No yield, continue loop } MyOperationState::Processing { data } => { let result = process(data); self.state = MyOperationState::Done; return Ok(IOResult::Done(result)); } MyOperationState::Done => unreachable!(), } } } ``` ## Re-Entrancy: The Critical Pitfall **State mutations before yield points cause bugs on re-entry.** ### Wrong ```rust fn bad_example(&mut self) -> Result> { self.counter += 1; // Mutates state return_if_io!(something_that_might_yield()); // If yields, re-entry will increment again! Ok(IOResult::Done(())) } ``` If `something_that_might_yield()` returns `IO`, caller waits for completion, then calls `bad_example()` again. `counter` gets incremented twice (or more). ### Correct: Mutate After Yield ```rust fn good_example(&mut self) -> Result> { return_if_io!(something_that_might_yield()); self.counter += 1; // Only reached once, after IO completes Ok(IOResult::Done(())) } ``` ### Correct: Use State Machine ```rust enum State { Start, AfterIO } fn good_example(&mut self) -> Result> { loop { match self.state { State::Start => { // Don't mutate shared state here self.state = State::AfterIO; return_if_io!(something_that_might_yield()); } State::AfterIO => { self.counter += 1; // Safe: only entered once return Ok(IOResult::Done(())); } } } } ``` ## Common Re-Entrancy Bugs | Pattern | Problem | |---------|---------| | `vec.push(x); return_if_io!(...)` | Vec grows on each re-entry | | `idx += 1; return_if_io!(...)` | Index advances multiple times | | `map.insert(k,v); return_if_io!(...)` | Duplicate inserts or overwrites | | `flag = true; return_if_io!(...)` | Usually ok, but check logic | ## State Enum Design Encode progress in state variants: ```rust // Good: index is part of state, preserved across yields enum ProcessState { Start, ProcessingItem { idx: usize, items: Vec }, Done, } // Loop advances idx only when transitioning states ProcessingItem { idx, items } => { return_if_io!(process_item(&items[idx])); if idx + 1 < items.len() { self.state = ProcessingItem { idx: idx + 1, items }; } else { self.state = Done; } } ``` ## Turso Implementation Key files: - `core/types.rs` - `IOResult`, `IOCompletions`, `return_if_io!`, `return_and_restore_if_io!` - `core/io/completions.rs` - `Completion`, `CompletionGroup` - `core/util.rs` - `io_yield_one!` macro - `core/state_machine.rs` - Generic `StateMachine` wrapper - `core/storage/btree.rs` - Many state machine examples - `core/storage/pager.rs` - `CompletionGroup` usage examples ## Testing Async Code Re-entrancy bugs often only manifest under specific IO timing. Use: - Deterministic simulation (`testing/simulator/`) - Whopper concurrent DST (`testing/concurrent-simulator/`) - Fault injection to force yields at different points ## References - `docs/manual.md` section on I/O ================================================ FILE: docs/agent-guides/code-quality.md ================================================ --- name: code-quality description: Correctness rules, Rust patterns, comments, avoiding over-engineering --- # Code Quality Guide ## Core Principle Production database. Correctness paramount. Crash > corrupt. ## Correctness Rules 1. **No workarounds or quick hacks.** Handle all errors, check invariants 2. **Assert often.** Never silently fail or swallow edge cases 3. **Crash on invalid state** if it risks data integrity. Don't continue in undefined state 4. **Consider edge cases.** On long enough timeline, all possible bugs will happen ## Rust Patterns - Make illegal states unrepresentable - Exhaustive pattern matching - Prefer enums over strings/sentinels - Minimize heap allocations - Write CPU-friendly code (microsecond = long time) ### Avoid `unwrap()` Never use bare `unwrap()`. Instead: **For truly unreachable states** (invariants that should never be violated): ```rust // Good: documents the invariant let value = option.expect("value must be set in Init phase"); ``` **For recoverable errors** (states that could happen at runtime): ```rust // Good: proper error handling let Some(value) = option else { return Err(LimboError::InvalidArgument("value not provided".into())); }; ``` The choice depends on whether the None/Err case represents: - A bug in the code (use `expect` with descriptive message) - A valid runtime condition (use `let ... else` or `match`) ## If-Statements Wrong: ```rust if condition { // happy path } else { // "shouldn't happen" - silently ignored } ``` Right: ```rust // If only one branch should ever be hit: assert!(condition, "invariant violated: ..."); // OR return Err(LimboError::InternalError("unexpected state".into())); // OR unreachable!("impossible state: ..."); ``` Use if-statements only when both branches are expected paths. ## Comments **Do:** - Document WHY, not what - Document functions, structs, enums, variants - Focus on why something is necessary - Preserve explanatory comments when refactoring **Don't:** - Comments that repeat code - References to AI conversations ("This test should trigger the bug") - Temporal markers ("added", "existing code", "Phase 1") ## Avoid Over-Engineering - Only changes directly requested or clearly necessary - Don't add features beyond what's asked - Don't add docstrings/comments to unchanged code - Don't add error handling for impossible scenarios - Don't create abstractions for one-time operations - Three similar lines > premature abstraction ## Ensure understanding of IO model - [Async IO model](./async-io-model.md) ## Cleanup - Delete unused code completely - No backwards-compat hacks (renamed `_vars`, re-exports, `// removed` comments) ================================================ FILE: docs/agent-guides/debugging.md ================================================ --- name: debugging description: Bytecode comparison, logging, ThreadSanitizer, deterministic simulation --- # Debugging Guide ## Bytecode Comparison Flow Turso aims for SQLite compatibility. When behavior differs: ``` 1. EXPLAIN query in sqlite3 2. EXPLAIN query in tursodb 3. Compare bytecode ├─ Different → bug in code generation └─ Same but results differ → bug in VM or storage layer ``` ### Example ```bash # SQLite sqlite3 :memory: "EXPLAIN SELECT 1 + 1;" # Turso cargo run --bin tursodb :memory: "EXPLAIN SELECT 1 + 1;" ``` ## Manual Query Inspection ```bash cargo run --bin tursodb :memory: 'SELECT * FROM foo;' cargo run --bin tursodb :memory: 'EXPLAIN SELECT * FROM foo;' ``` ## Logging ```bash # Trace core during tests RUST_LOG=none,turso_core=trace make test # Output goes to testing/test.log # Warning: can be megabytes per test run ``` ## Threading Issues Use stress tests with ThreadSanitizer: ```bash rustup toolchain install nightly rustup override set nightly cargo run -Zbuild-std --target x86_64-unknown-linux-gnu \ -p turso_stress -- --vfs syscall --nr-threads 4 --nr-iterations 1000 ``` ## Deterministic Simulation Reproduce bugs with seed. Note: simulator uses legacy "limbo" naming. ```bash # Simulator RUST_LOG=limbo_sim=debug cargo run --bin limbo_sim -- -s # Whopper (concurrent DST) SEED=1234 ./testing/concurrent-simulator/bin/run ``` ## Architecture Reference - **Parser** → AST from SQL strings - **Code generator** → bytecode from AST - **Virtual machine** → executes SQLite-compatible bytecode - **Storage layer** → B-tree operations, paging ================================================ FILE: docs/agent-guides/mvcc.md ================================================ --- name: mvcc description: MVCC feature - snapshot isolation, versioning, limitations --- # MVCC Guide (Experimental) Multi-Version Concurrency Control. **Work in progress, not production-ready.** **CRITICAL**: Ignore MVCC when debugging unless the bug is MVCC-specific. ## Enabling MVCC ```sql PRAGMA journal_mode = 'mvcc'; ``` Runtime configuration, not a compile-time feature flag. Per-database setting. ## How It Works Standard WAL: single version per page, readers see snapshot at read mark time. MVCC: multiple row versions, snapshot isolation. Each transaction sees consistent snapshot at begin time. ### Key Differences from WAL | Aspect | WAL | MVCC | |--------|-----|------| | Write granularity | Every commit writes full pages | Affected rows only | Readers/Writers | Don't block each other | Don't block each other | | Persistence | `.db-wal` | `.db-log` (logical log) | | Isolation | Snapshot (page-level) | Snapshot (row-level) | ### Versioning Each row version tracks: - `begin` - timestamp when visible - `end` - timestamp when deleted/replaced - `btree_resident` - existed before MVCC enabled ## Architecture ``` Database └─ mv_store: MvStore ├─ rows: SkipMap> ├─ txs: SkipMap ├─ Storage (.db-log file) └─ CheckpointStateMachine ``` **Per-connection**: `mv_tx` tracks current MVCC transaction. **Shared**: `MvStore` with lock-free `crossbeam_skiplist` structures. ## Key Files - `core/mvcc/mod.rs` - Module overview - `core/mvcc/database/mod.rs` - Main implementation (~3000 lines) - `core/mvcc/cursor.rs` - Merged MVCC + B-tree cursor - `core/mvcc/persistent_storage/logical_log.rs` - Disk format - `core/mvcc/database/checkpoint_state_machine.rs` - Checkpoint logic ## Checkpointing Flushes row versions to B-tree periodically. ```sql PRAGMA mvcc_checkpoint_threshold = ; ``` Process: acquire lock → begin pager txn → write rows → commit → truncate log → fsync → release. ## Current Limitations **Not implemented:** - Garbage collection (old versions accumulate) - Recovery from logical log on restart **Known issues:** - Checkpoint blocks other transactions, even reads! - No spilling to disk; memory use concerns ## Testing ```bash # Run MVCC-specific tests cargo test mvcc # TCL tests with MVCC make test-mvcc ``` Use `#[turso_macros::test(mvcc)]` attribute for MVCC-enabled tests. ```rust #[turso_macros::test(mvcc)] fn test_something() { // runs with MVCC enabled } ``` ## References - `core/mvcc/mod.rs` documents data anomalies (dirty reads, lost updates, etc.) - Snapshot isolation vs serializability: MVCC provides the former, not the latter ================================================ FILE: docs/agent-guides/pr-workflow.md ================================================ --- name: pr-workflow description: Commits, formatting, CI, dependencies, security --- # PR Workflow Guide ## Commit Practices - **Atomic commits.** Small, focused, single purpose - **Don't mix:** logic + formatting, logic + refactoring - **Good message** = easy to write short description of intent Learn `git rebase -i` for clean history. ## PR Guidelines - Keep PRs focused and small - Run relevant tests before submitting - Each commit tells part of the story ## CI Environment Notes If running as GitHub Action: - Max-turns limit in `.github/workflows/claude.yml` - OK to commit WIP state and push - OK to open WIP PR and continue in another action - Don't spiral into rabbit holes. Stay focused on key task ## Security Never commit: - `.env` files - Credentials - Secrets ## Third-Party Dependencies When adding: 1. Add license file under `licenses/` 2. Update `NOTICE.md` with dependency info ## External APIs/Tools - Never guess API params or CLI args - Search official docs first - Ask for clarification if ambiguous ================================================ FILE: docs/agent-guides/storage-format.md ================================================ --- name: storage-format description: SQLite file format, B-trees, pages, cells, overflow, freelist --- # Storage Format Guide ## Database File Structure ``` ┌─────────────────────────────┐ │ Page 1: Header + Schema │ ← First 100 bytes = DB header ├─────────────────────────────┤ │ Page 2..N: B-tree pages │ ← Tables and indexes │ Overflow pages │ │ Freelist pages │ └─────────────────────────────┘ ``` Page size: power of 2, 512-65536 bytes. Default 4096. ## Database Header (First 100 Bytes) | Offset | Size | Field | |--------|------|-------| | 0 | 16 | Magic: `"SQLite format 3\0"` | | 16 | 2 | Page size (big-endian) | | 18 | 1 | Write format version (1=rollback, 2=WAL) | | 19 | 1 | Read format version | | 24 | 4 | Change counter | | 28 | 4 | Database size in pages | | 32 | 4 | First freelist trunk page | | 36 | 4 | Total freelist pages | | 40 | 4 | Schema cookie | | 56 | 4 | Text encoding (1=UTF8, 2=UTF16LE, 3=UTF16BE) | All multi-byte integers: **big-endian**. ## Page Types | Flag | Type | Purpose | |------|------|---------| | 0x02 | Interior index | Index B-tree internal node | | 0x05 | Interior table | Table B-tree internal node | | 0x0a | Leaf index | Index B-tree leaf | | 0x0d | Leaf table | Table B-tree leaf | | - | Overflow | Payload exceeding cell capacity | | - | Freelist | Unused pages (trunk or leaf) | ## B-tree Structure Two B-tree types: - **Table B-tree**: 64-bit rowid keys, stores row data - **Index B-tree**: Arbitrary keys (index columns + rowid) ``` Interior page: [ptr0] key1 [ptr1] key2 [ptr2] ... │ │ │ ▼ ▼ ▼ child child child pages pages pages Leaf page: key1:data key2:data key3:data ... ``` Page 1 always root of `sqlite_schema` table. ## Cell Format ### Table Leaf Cell ``` [payload_size: varint] [rowid: varint] [payload] [overflow_ptr: u32?] ``` ### Table Interior Cell ``` [left_child_page: u32] [rowid: varint] ``` ### Index Cells Similar but key is arbitrary (columns + rowid), not just rowid. ## Record Format (Payload) ``` [header_size: varint] [type1: varint] [type2: varint] ... [data1] [data2] ... ``` Serial types: | Type | Meaning | |------|---------| | 0 | NULL | | 1-4 | 1/2/3/4 byte signed int | | 5 | 6 byte signed int | | 6 | 8 byte signed int | | 7 | IEEE 754 float | | 8 | Integer 0 | | 9 | Integer 1 | | ≥12 even | BLOB, length=(N-12)/2 | | ≥13 odd | Text, length=(N-13)/2 | ## Overflow Pages When payload exceeds threshold, excess stored in overflow chain: ``` [next_page: u32] [data...] ``` Last page has next_page=0. ## Freelist Linked list of trunk pages, each containing leaf page numbers: ``` Trunk: [next_trunk: u32] [leaf_count: u32] [leaf_pages: u32...] ``` ## Turso Implementation Key files: - `core/storage/sqlite3_ondisk.rs` - On-disk format, `PageType` enum - `core/storage/btree.rs` - B-tree operations (large file) - `core/storage/pager.rs` - Page management - `core/storage/buffer_pool.rs` - Page caching ## Debugging Storage ```bash # Integrity check cargo run --bin tursodb test.db "PRAGMA integrity_check;" # Page count cargo run --bin tursodb test.db "PRAGMA page_count;" # Freelist info cargo run --bin tursodb test.db "PRAGMA freelist_count;" ``` ## References - [SQLite File Format](https://sqlite.org/fileformat.html) - [SQLite B-Tree Module](https://sqlite.org/btreemodule.html) - [SQLite Internals: Pages & B-trees](https://fly.io/blog/sqlite-internals-btree/) ================================================ FILE: docs/agent-guides/testing.md ================================================ --- name: testing description: Test types, when to use each, how to write and run tests --- # Testing Guide ## Test Types & When to Use | Type | Location | Use Case | |------|----------|----------| | `.sqltest` | `testing/sqltests/tests/` | SQL compatibility. **Preferred for new tests** | | TCL `.test` | `testing/` | Legacy SQL compat (being phased out) | | Rust integration | `tests/integration/` | Regression tests, complex scenarios | | Fuzz | `tests/fuzz/` | Complex features, edge case discovery | **Note:** TCL tests are being phased out in favor of testing/sqltests. The `.sqltest` format allows the same test cases to run against multiple backends (CLI, Rust bindings, etc.). ## Running Tests ```bash # Main test suite (TCL compat, sqlite3 compat, Python wrappers) make test # Single TCL test make test-single TEST=select.test # SQL test runner make -C testing/sqltests run-cli # Rust unit/integration tests (full workspace) cargo test ``` ## Writing Tests ### .sqltest (Preferred) ```sql @database :memory: @query SELECT 1 + 1; @expected 2 ``` Location: `testing/sqltests/tests/*.sqltest` ### TCL ```tcl do_execsql_test_on_specific_db {:memory:} test-name { SELECT 1 + 1; } {2} ``` Location: `testing/*.test` ### Rust Integration ```rust // tests/integration/test_foo.rs #[test] fn test_something() { let conn = Connection::open_in_memory().unwrap(); // ... } ``` ## Key Rules - Every functional change needs a test - Test must fail without change, pass with it - Prefer in-memory DBs: `:memory:` (sqltest) or `{:memory:}` (TCL) - Don't invent new test formats. Follow existing patterns - Write tests first when possible ## Test Database Schema `testing/testing.db` has `users` and `products` tables. See [docs/testing.md](../testing.md) for schema. ## Logging During Tests ```bash RUST_LOG=none,turso_core=trace make test ``` Output: `testing/test.log`. Warning: very verbose. ================================================ FILE: docs/agent-guides/transaction-correctness.md ================================================ --- name: transaction-correctness description: WAL mechanics, checkpointing, concurrency rules, recovery --- # Transaction Correctness Guide Turso uses WAL (Write-Ahead Logging) mode exclusively. Files: `.db`, `.db-wal` (no `.db-shm` - Turso uses in-memory WAL index) ## WAL Mechanics ### Write Path 1. Writer appends frames (page data) to WAL file (sequential I/O) 2. COMMIT = frame with non-zero db_size in header (marks transaction end) 3. Original DB unchanged until checkpoint ### Read Path 1. Reader acquires read mark (mxFrame = last valid commit frame) 2. For each page: check WAL up to mxFrame, fall back to main DB 3. Reader sees consistent snapshot at its read mark ### Checkpointing Transfers WAL content back to main DB. ``` WAL grows → checkpoint triggered (default: 1000 pages) → pages copied to DB → WAL reused ``` Checkpoint types: - **PASSIVE**: Non-blocking, stops at pages needed by active readers - **FULL**: Waits for readers, checkpoints everything - **RESTART**: Like FULL, also resets WAL to beginning - **TRUNCATE**: Like RESTART, also truncates WAL file to zero length ### WAL-Index SQLite uses a shared memory file (`-shm`) for WAL index. **Turso does not** - it uses in-memory data structures (`frame_cache` hashmap, atomic read marks) since multi-process access is not supported. ## Concurrency Rules - One writer at a time - Readers don't block writer, writer doesn't block readers - Checkpoint must stop at pages needed by active readers ## Recovery On crash: 1. First connection acquires exclusive lock 2. Replays valid commits from WAL 3. Releases lock, normal operation resumes ## Turso Implementation Key files: - `core/storage/wal.rs` - WAL implementation - `core/storage/pager.rs` - Page management, transactions ### Connection-Private vs Shared **Per-Connection (private):** - `Pager` - page cache, dirty pages, savepoints, commit state - `WalFile` - connection's snapshot view: - `max_frame` / `min_frame` - frame range for this connection's snapshot - `max_frame_read_lock_index` - which read lock slot this connection holds - `last_checksum` - rolling checksum state **Shared across connections:** - `WalFileShared` - global WAL state: - `frame_cache` - page-to-frame index (replaces `.shm` file) - `max_frame` / `nbackfills` - global WAL progress - `read_locks[5]` - read mark slots (TursoRwLock with embedded frame values) - `write_lock` - exclusive writer lock - `checkpoint_lock` - checkpoint serialization - `file` - WAL file handle - `DatabaseStorage` - main `.db` file - `BufferPool` - shared memory allocation ## Correctness Invariants 1. **Durability**: COMMIT record must be fsynced before returning success 2. **Atomicity**: Partial transactions never visible to readers 3. **Isolation**: Each reader sees consistent snapshot 4. **No lost updates**: Checkpoint can't overwrite uncommitted changes ## References - [SQLite WAL](https://sqlite.org/wal.html) - [WAL File Format](https://sqlite.org/walformat.html) ================================================ FILE: docs/contributing/contributing_functions.md ================================================ # How to contribute a SQL function implementation? Steps 1. Pick a `SQL functions` in [COMPAT.md](../../COMPAT.md) file with a No (not implemented yet) status. 2. Create an issue for that function. 3. Implement the function in a feature branch. 4. Push it as a Merge Request, get it review. Sample Pull Requests of function contributing - [partial support for datetime() and julianday()](https://github.com/tursodatabase/turso/pull/600) - [support for changes() and total_changes()](https://github.com/tursodatabase/turso/pull/589) - [support for unhex(X)](https://github.com/tursodatabase/turso/pull/353) ## An example with function `date(..)` > Note that the files, code location, steps might be not exactly the same because of refactor but the idea of the changes needed in each layer stays. [Issue #158](https://github.com/tursodatabase/turso/issues/158) was created for it. Refer to commit [4ff7058](https://github.com/tursodatabase/turso/commit/4ff705868a054643f6113cbe009655c32bc5f235). ![limbo_architecture.png](limbo_architecture.png) To add a function we generally need to touch at least the following modules - SQL Command Processor - The `SQL Command Processor` module is responsible for turning sql function string into a sequence of instructions to be executed by the `Virtual Machine` module. - we need the following things: function definition, how the `bytecode generator` in `core/translate` generates bytecode program for this function to be executed. - Virtual Machine `core/vdbe` - we need to add logic of how the `vdbe` should execute the logic of this function in Rust and write result to destination register of the vm. - [more info](https://www.sqlite.org/opcode.html) - Tests ``` SQL function string --Tokenizer and Parser--> AST (enum Func) --Bytecode Generator (core/translate)--> Bytecode Instructions --Virtual Machine--> Result ``` TODO for implementing the function: - analysis - read and try out how the function works in SQLite. - compare `explain` output of SQLite and Limbo. - add/ update the function definition in `functions.rs`. - add/ update how to function is translated from `definition` to `instruction` in virtual machine layer VDBE. - add/ update the function Rust execution code and tests in vdbe layer. - add/ update how the bytecode `Program` executes when steps into the function. - add/ update TCL tests for this function in limbo/testing. - update doc for function compatibility. ### Analysis How `date` works in SQLite? ```bash > sqlite3 sqlite> explain select date('now'); addr opcode p1 p2 p3 p4 p5 comment ---- ------------- ---- ---- ---- ------------- -- ------------- 0 Init 0 6 0 0 Start at 6 1 Once 0 3 0 0 2 Function 0 0 2 date(-1) 0 r[2]=func() 3 Copy 2 1 0 0 r[1]=r[2] 4 ResultRow 1 1 0 0 output=r[1] 5 Halt 0 0 0 0 6 Goto 0 1 0 0 ``` Comparing that with `Limbo`: ```bash # created a sqlite database file database.db # or cargo run to use the memory mode if it is already available. > cargo run database.db Enter ".help" for usage hints. limbo> explain select date('now'); Parse error: unknown function date ``` We can see that the function is not implemented yet so the Parser did not understand it and throw an error `Parse error: unknown function date`. - we only need to pay attention to opcode `Function` at addr 2. The rest is already set up in limbo. - we have up to 5 registers p1 to p5 for each opcode. ### Function definition For limbo to understand the meaning of `date`, we need to define it as a Function somewhere. That place can be found currently in `core/functions.rs`. We need to edit 3 places 1. add to ScalarFunc as `date` is a scalar function. ```diff // file core/functions.rs pub enum ScalarFunc { // other funcs... Soundex, + Date, Time, // other funcs... } ``` 2. add to Display to show the function as string in our program. ```diff // file core/functions.rs impl Display for ScalarFunc { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let str = match self { // ... ScalarFunc::Soundex => "soundex".to_string(), + ScalarFunc::Date => "date".to_string(), ScalarFunc::Time => "time".to_string(), // ... } ``` 3. add to `fn resolve_function(..)` of `impl Func` to enable parsing from str to this function. ```diff // file core/functions.rs impl Func { pub fn resolve_function(name: &str, arg_count: usize) -> Result { match name { // ... + "date" => Ok(Func::Scalar(ScalarFunc::Date)), // ... } ``` ### Function translation How to translate the function into bytecode `Instruction`? - `date` function can have zero to many arguments. - in case there are arguments, we loop through the args and allocate a register `let target_reg = program.alloc_register();` for each argument expression. - then we emit the bytecode instruction for Function `program.emit_insn(Insn::Function {...})` https://github.com/tursodatabase/turso/blob/69e3dd28f77e59927da4313e517b2b428ede480d/core/translate/expr.rs#L1235C1-L1256C26 ```diff // file core/translate/expr.rs pub fn translate_expr(...) -> Result { // ... match expr { // .. ast::Expr::FunctionCall { // ... match &func_ctx.func { // ... Func::Scalar(srf) => { // ... + ScalarFunc::Date => { + if let Some(args) = args { + for arg in args.iter() { + // register containing result of each argument expression + let target_reg = program.alloc_register(); + _ = translate_expr( + program, + referenced_tables, + arg, + target_reg, + precomputed_exprs_to_registers, + )?; + } + } + program.emit_insn(Insn::Function { + constant_mask: 0, + start_reg: target_register + 1, + dest: target_register, + func: func_ctx, + }); + Ok(target_register) + } // ... ``` ### Function execution The function execution code is implemented in `vdbe/datetime.rs` file [here](https://github.com/tursodatabase/turso/commit/9cc965186fecf4ba4dd81c783a841c71575123bf#diff-839435241d4ffb648ad2d162bc6ba6a94f052309865251dc2aff36eaa14fa3c5R11-R30) as we already implemented the datetime features in this file. Note that for other functions it might be implemented in other location in vdbe module. ```diff // file vdbe/datetime.rs // ... + pub fn exec_date(values: &[Value]) -> Value { + // ... implementation + } // ... ``` ### Program bytecode execution Next step is to implement how the virtual machine (VDBE layer) executes the bytecode `Program` when the program step into the function instruction `Insn::Function` date `ScalarFunc::Date`. Per [SQLite spec](https://www.sqlite.org/lang_datefunc.html#time_values) if there is no `time value` (no start register) , we want to execute the function with default param `'now'`. > In all functions other than timediff(), the time-value (and all modifiers) may be omitted, in which case a time value of 'now' is assumed. ```diff // file vdbe/mod.rs impl Program { pub fn step<'a>(...) { loop { // ... match isin { // ... Insn::Function { // ... + ScalarFunc::Date => { + let result = + exec_date(&state.registers[*start_reg..*start_reg + arg_count]); + state.registers[*dest] = result; + } // ... ``` ### Adding tests There are 2 kind of tests we need to add 1. tests for Rust code 2. TCL tests for executing the sql function One test for the Rust code is shown as example below https://github.com/tursodatabase/turso/blob/69e3dd28f77e59927da4313e517b2b428ede480d/core/vdbe/datetime.rs#L620C1-L661C1 TCL tests for `date` functions can be referenced from SQLite source code which is already very comprehensive. - https://github.com/sqlite/sqlite/blob/f2b21a5f57e1a1db1a286c42af40563077635c3d/test/date3.test#L36 - https://github.com/sqlite/sqlite/blob/f2b21a5f57e1a1db1a286c42af40563077635c3d/test/date.test#L611C1-L652C73 ### Updating doc Update the [COMPAT.md](../../COMPAT.md) file to mark this function as implemented. Change Status to - `Yes` if it is fully supported, - `Partial` if supported but not fully yet compared to SQLite. An example: ```diff // file COMPAT.md | Function | Status | Comment | |------------------------------|---------|------------------------------| - | date() | No | | + | date() | Yes | partially supports modifiers | ... ``` ================================================ FILE: docs/fts.md ================================================ # Full-Text Search in tursodb This proposal is to use the https://github.com/quickwit-oss/tantivy library to provide full-text search capabilities in tursodb. # Tantivy overview *introduction and terminology*: **Term:** a normalized token extracted from text during indexing. **Posting list:** (also referred to as an *inverted list*) is the list of all documents that contain a given term, along with metadata needed for scoring the results. **Document:** Tantivy’s unit of indexing and retrieval, analogous to a single row in a db table. **Field:** Analogous to a column in a table **Position:** The sequential offset/token index of a term within a field. **Segment:** self-contained and immutable chunk of the index. Each commit writes one or more new segments to disk, searches query all active segments in parallel. Inside each segment: - A term dictionary lists every term in that segment. - For each term, the posting list maps to documents. - Stored fields (original text) are compressed for retrieval. - Deletes are stored as a separate bitmap until the next merge. Merges rewrite new segments without the deleted docs. More details on everything here: https://fulmicoton.gitbooks.io/tantivy-doc/content/step-by-step.html ### Note: this blog post here is a bit old but is a very interesting read. https://fulmicoton.com/posts/behold-tantivy-part2/ At a high level, a Tantivy index is a self-contained directory containing immutable “segments” of inverted index data file. *“Your index is in fact divided into smaller independent indexes called **segments**. The reason for this division is explained in [Incremental Indexing](https://fulmicoton.gitbooks.io/tantivy-doc/content/incremental-indexing.html). The UUID part stands as the segment name, while the extension express which data-structure is stored in the file.* - ***.info** contains some meta information about the segment* - ***.term** contains the term dictionary* - ***.idx** contains the inverted lists* - ***.fieldnorm** contains the field norms* - ***.pos** contains the positions information* - ***.store** contains the stored documents* - ***.fas**t contains the fast terms* *The last file, meta.json, contains in a JSON format :* - *The schema of your index* - *The name of the segments that were committed and are available for search.”* source: https://fulmicoton.gitbooks.io/tantivy-doc/content/index-files.html ### Writing / indexing - Applications obtain a single `IndexWriter`, which owns the memory arena and background worker threads. - Each call to `add_document()` tokenizes and indexes the text fields into in-memory postings. - When the writer’s memory budget is full or `commit()` is called, Tantivy serializes a new segment (posting lists, term dictionary, stored field data) and updates the `meta.json` (all these segments are Write-once, Read-many, with the exception of the per-index meta.json). - `commit()` is atomic: either the new segment is visible or not at all. - `delete_term()` records tombstones that are applied at merge time (all updates done are just “delete + insert”). ### Storage abstraction Tantivy’s I/O is implemented through the `Directory` trait (https://github.com/quickwit-oss/tantivy/blob/dabcaa58093a3f7f10e98a5a3b06cfe2370482f9/src/directory/directory.rs#L107), which is kinda similar to our own `IO` / `File` traits. Tantivy includes `MmapDirectory` (mmap’d file based segments, the default option), and `RamDirectory` (in-memory, usually for testing). Obviously the default will not work for our use case, as we cannot have a whole directory created for each index with a bunch of files. ### Threading and concurrency - One write per index at a time, but internally multi-threaded for indexing throughput. - Multiple readers can coexist and search concurrently on immutable segments. - Writer commits are visible to new readers only after `reader.reload()` or automatic reload. ### Typical workflow 1. Build a `Schema` describing indexed fields. 2. Create or open an `Index` using a `Directory`. 3. Acquire an `IndexWriter`, call `add_document()` or `delete_term()`. 4. Call `commit()` to persist and publish new data. 5. Use `IndexReader`/`Searcher` + `QueryParser` + `TopDocs` to execute searches. 6. Fetch stored fields for displayed results. ### =========================================================== ## Usage: From the user perspective, the syntax for using full text search features in tursodb will look like the following: ## DDL: ```sql CREATE INDEX idx_posts ON posts USING fts (title, body); ``` ### Tokenizer Configuration You can configure the tokenizer used for text processing via the `WITH` clause: ```sql -- Use raw tokenizer for exact-match fields (IDs, tags) CREATE INDEX idx_tags ON articles USING fts (tag) WITH (tokenizer = 'raw'); -- Use ngram tokenizer for autocomplete/substring matching CREATE INDEX idx_products ON products USING fts (name) WITH (tokenizer = 'ngram'); ``` #### Available Tokenizers | Tokenizer | Description | Use Case | |-----------|-------------|----------| | `default` | Lowercase, punctuation split, 40 char limit | General English text | | `raw` | No tokenization - exact match only | IDs, UUIDs, tags, categories | | `simple` | Basic whitespace/punctuation split | Simple text without lowercase | | `whitespace` | Split on whitespace only | Space-separated tokens | | `ngram` | 2-3 character n-grams | Autocomplete, substring matching | ### Field Weights You can configure relative importance of indexed columns using the `weights` parameter: ```sql -- Title matches are 2x more important than body matches CREATE INDEX idx_articles ON articles USING fts (title, body) WITH (weights = 'title=2.0,body=1.0'); -- Combined with tokenizer CREATE INDEX idx_docs ON docs USING fts (name, description) WITH (tokenizer = 'simple', weights = 'name=3.0,description=1.0'); ``` **Weight behavior:** - Default weight is `1.0` for all fields - Weights affect the BM25 relevance score from `fts_score()` - Higher weights increase the score contribution from that field - Weights must be positive numbers #### Tokenizer Examples ```sql -- Default tokenizer: "Hello World" → ["hello", "world"] -- Searches for "hello" or "HELLO" will match -- Raw tokenizer: "user-123" → ["user-123"] -- Only exact match "user-123" will work, "user" won't match -- Ngram tokenizer: "iPhone" → ["iP", "iPh", "Ph", "Pho", "ho", "hon", "on", "one", "ne"] -- Search for "Pho" will match documents containing "iPhone" ``` ## DQL: Three FTS functions are provided: - **`fts_score(col1, col2, ..., 'query')`** - Returns the BM25 relevance score for each matching row - **`fts_match(col1, col2, ..., 'query')`** - Returns a boolean indicating if the row matches (used in WHERE clause) - **`fts_highlight(col1, col2, ..., before_tag, after_tag, 'query')`** - Returns text with matching terms wrapped in tags `WHERE col MATCH 'query'` is available for use instead of `fts_match(col, 'query')` ### Basic Query Examples ```sql -- Get scores for matching documents, ordered by relevance SELECT fts_score(title, body, 'database') as score, id, title FROM articles ORDER BY score DESC LIMIT 10; -- Simple match filter SELECT id, title FROM articles WHERE fts_match(body, 'science') LIMIT 10; ``` ### Highlighting Search Results The `fts_highlight` function wraps matching query terms with custom tags for display: ```sql -- Basic highlighting (single column) SELECT fts_highlight('Learn about database optimization', '', '', 'database'); -- Returns: "Learn about database optimization" -- Multiple columns - text is concatenated with spaces SELECT fts_highlight(title, body, '', '', 'database') as highlighted FROM articles WHERE fts_match(title, body, 'database'); -- If title='Database Design' and body='Learn about optimization', -- Returns: "Database Design Learn about optimization" -- Use with FTS queries to highlight matched content SELECT id, title, fts_highlight(body, '', '', 'database') as highlighted_body FROM articles WHERE fts_match(title, body, 'database') ORDER BY fts_score(title, body, 'database') DESC; -- Multiple terms are highlighted SELECT fts_highlight('The quick brown fox', '', '', 'quick fox'); -- Returns: "The quick brown fox" ``` **Features:** - Supports multiple text columns (concatenated with spaces) - Case-insensitive matching (uses the default tokenizer) - Highlights all occurrences of matching terms - Works as a standalone function (doesn't require an FTS index) - Returns original text if no matches found - Returns NULL if query, before_tag, or after_tag is NULL - NULL text columns are skipped (not returned as NULL) ### Supported Query Patterns The FTS index recognizes and optimizes these query patterns: | Pattern | Description | |---------|-------------| | `SELECT fts_score(...) as score FROM t ORDER BY score DESC LIMIT ?` | Score with ORDER BY and LIMIT | | `SELECT fts_score(...) as score FROM t WHERE fts_match(...) ORDER BY score DESC LIMIT ?` | Combined score+match with ORDER BY and LIMIT | | `SELECT fts_score(...) as score FROM t WHERE fts_match(...) ORDER BY score DESC` | Combined without LIMIT | | `SELECT fts_score(...) as score FROM t WHERE fts_match(...) LIMIT ?` | Combined with LIMIT only | | `SELECT fts_score(...) as score FROM t WHERE fts_match(...)` | Combined without ORDER BY or LIMIT | | `SELECT * FROM t WHERE fts_match(...) LIMIT ?` | Match filter with LIMIT | | `SELECT * FROM t WHERE fts_match(...)` | Match filter only | ### Function Recognition Mode Queries that don't exactly match the predefined patterns still work via **function recognition**. When `fts_match()` or `fts_score()` functions are detected in a query, the FTS index is used even with: - Additional SELECT columns - Extra WHERE conditions (e.g., `AND category = 'tech'`) - ORDER BY non-score columns - Computed expressions ```sql -- Complex query with extra columns and WHERE conditions SELECT id, author, title, category, views, fts_score(title, body, 'Rust') as score FROM articles WHERE fts_match(title, body, 'Rust') AND category = 'tech' AND views > 100 ORDER BY score DESC; -- ORDER BY non-score column SELECT id, title FROM docs WHERE fts_match(title, body, 'Rust') ORDER BY created_at DESC; ``` ### Query Syntax The query string passed to `fts_match`/`fts_score` supports Tantivy's QueryParser syntax: | Syntax | Example | Description | |--------|---------|-------------| | Single term | `database` | Match documents containing "database" | | Multiple terms (OR) | `database sql` | Match documents with "database" OR "sql" | | AND operator | `database AND sql` | Match documents with both terms | | NOT operator | `database NOT nosql` | Match "database" but exclude "nosql" | | Phrase search | `"full text search"` | Match exact phrase | | Prefix search | `data*` | Match terms starting with "data" | | Column filter | `title:database` | Match "database" only in title field | | Boosting | `title:database^2 body:database` | Boost title matches | This syntax can be improved on in the future, and maybe eventually we can support some fancy elasticsearch/paradeDB syntax. ### DML Operations DML statements (INSERT, UPDATE, DELETE) work automatically with FTS indexes: - **INSERT**: Documents are indexed immediately but batched for efficiency. Tantivy commits after every 1000 documents (`BATCH_COMMIT_SIZE`). - **UPDATE**: Implemented as DELETE + INSERT internally. - **DELETE**: Uses term-based tombstones that are cleaned up at merge time. ```sql -- These trigger automatic FTS index updates INSERT INTO articles VALUES (1, 'Title', 'Body text'); UPDATE articles SET body = 'New body' WHERE id = 1; DELETE FROM articles WHERE id = 1; ``` # Planning For the above SELECT query, we would look up which FTS index handles `t.name` from the in-memory schema representation, construct a Tantivy query for this index with `QueryParser::parse_query("tursodb")`. Tantivy then returns a list of `(score, doc_address)` pairs, which we translate each result into `(rowid, rating)` because we stored the table’s PK or rowid as a field during the indexing. Then an index lookup/SeekRowID for each of those result rows from the FtsQuery internal function fetches the `t.*` columns and no extra sort is required. If there are additional filters on `t` (`WHERE t.created_at > ...`) we should probably prefer ‘fts-first’, then filter out results from that. Run the Tantivy query, materialize results into an ephemeral table of some sort and then discard rows failing the post-filter. TODO: expand on this a bit more # Metadata CREATE INDEX statements are already stored in the schema table, when we parse the sqlite_schema table which contains the DDL statement that created the index, we build the in-memory schema representation that allows us to send these queries to Tantivy. ## Storage We implement Tantivy’s `Directory` over our pager/B-tree but just as a regular table. We do not reinterpret Tantivy’s files, we store them exactly as Tantivy names and writes them. We should probably store the files as chunks of 256-512 kb blobs. One table, and one index: per each FTS index - **Data table:** ```sql CREATE TABLE fts_dir_{idx_id} ( path TEXT NOT NULL, chunk_no INTEGER NOT NULL, bytes BLOB NOT NULL ); ``` - **Index:** ```sql CREATE INDEX IF NOT EXISTS idx_name ON table_name USING backing_btree (path, chunk_no, bytes) ``` Use `backing_btree` to create a BTree that stores all columns without rowid indirection This allows direct cursor access with the exact key structure. This way we can use an index cursor to `SeekGE` (path, chunk_no) where chunk_no is just computed from the offset requested by `read_bytes` on the file handle. # Current Architecture: HybridBTreeDirectory The architecture uses a hybrid approach that balances memory usage and performance: ``` ┌─────────────────────────────────────────────────────────────┐ │ HybridBTreeDirectory │ ├─────────────────────────────────────────────────────────────┤ │ File Catalog (always in memory) │ │ ├── path -> FileMetadata { size, num_chunks, category } │ │ │ │ Hot Cache (metadata + term dictionaries) │ │ ├── meta.json, .managed.json (always loaded) │ │ ├── .term files (loaded on first access) │ │ ├── .fast, .fieldnorm (small, frequently accessed) │ │ │ │ Chunk LRU Cache (lazy-loaded segment data) │ │ ├── .idx, .pos, .store chunks │ │ └── Eviction when over memory budget │ └─────────────────────────────────────────────────────────────┘ ``` ### File Categories Files are classified based on their role in Tantivy operations: | Category | Files | Behavior | |----------|-------|----------| | Metadata | `meta.json`, `.managed.json`, `.lock` | Always in hot cache | | TermDictionary | `*.term` | Hot, loaded on first access | | FastFields | `*.fast`, `*.fieldnorm` | Hot, small and frequent | | SegmentData | `*.idx`, `*.pos`, `*.store` | Lazy-loaded on demand | ### Loading Flow (Catalog-First) The FTS cursor uses an async state machine to load the index: 1. **LoadingCatalog**: Scan BTree records, building file metadata (path, max_chunk, size) 2. **PreloadingEssentials**: Load hot files (metadata, term dicts) into memory 3. **CreatingIndex**: Open/create Tantivy index using HybridBTreeDirectory 4. **Ready**: Index is usable; segment data lazy-loaded on query Additional states for write operations: - **FlushingWrites**: Persisting pending writes to BTree - **Querying**: Executing a Tantivy search The state machine is driven by `FtsCursor` which handles the async IO integration with our pager. ### Lazy Loading with Blocking Reads Since blocking reads are acceptable in the query path: ```rust impl FileHandle for LazyFileHandle { fn read_bytes(&self, range: Range) -> io::Result { // 1. Check hot cache // 2. Calculate required chunks from byte range // 3. For each chunk: check LRU cache, or blocking BTree fetch // 4. Assemble and return result } } ``` The `get_chunk_blocking` method creates a temporary BTree cursor and loops on `pager.io.step()` until the chunk is fetched. ### Memory Management ```rust pub const DEFAULT_CHUNK_CACHE_BYTES: usize = 128 * 1024 * 1024; // 128MB pub const DEFAULT_HOT_CACHE_BYTES: usize = 64 * 1024 * 1024; // 64MB ``` The `ChunkLruCache` evicts least-recently-used chunks when over capacity. Each FTS index has its own cache for isolation. ### Key Components | Component | Purpose | |-----------|---------| | `FileCategory` | Classifies files for caching decisions | | `FileMetadata` | Stores file size, chunk count, category | | `ChunkLruCache` | Memory-bounded LRU cache for segment chunks | | `HybridBTreeDirectory` | Implements Tantivy's Directory trait | | `LazyFileHandle` | Fetches chunks on demand | ### Memory Usage Comparison | Index Size | Old (CachedBTreeDirectory) | New (HybridBTreeDirectory) | |------------|---------------------------|---------------------------| | 100MB | ~100MB | ~25MB | | 500MB | ~500MB | ~80MB | | 1GB | ~1GB | ~150MB | ### Configuration Constants ```rust DEFAULT_MEMORY_BUDGET_BYTES = 64 MB // Tantivy IndexWriter memory budget DEFAULT_CHUNK_SIZE = 1 MB // BTree blob chunk size DEFAULT_HOT_CACHE_BYTES = 64 MB // Bounded LRU cache for metadata/term dicts DEFAULT_CHUNK_CACHE_BYTES = 128 MB // Bounded LRU cache for segment chunks BATCH_COMMIT_SIZE = 1000 // Documents per Tantivy commit ``` Both the hot cache and chunk cache use approximate LRU eviction to stay within their memory budgets, preventing unbounded memory growth with many FTS indexes. --- # Index Maintenance ## OPTIMIZE INDEX The `OPTIMIZE INDEX` command merges all Tantivy segments into a single segment, which can improve query performance and reduce storage overhead. This is especially useful after bulk inserts that create many small segments. ```sql -- Optimize a specific FTS index OPTIMIZE INDEX fts_articles; -- Optimize all FTS indexes in the database OPTIMIZE INDEX; ``` **When to use:** - After bulk data imports (many INSERTs) - When you notice query performance degradation - During maintenance windows **What it does:** - Merges all segments into one optimized segment - Flushes any pending documents first - Invalidates caches to ensure fresh reads - Works within normal transaction semantics **Note:** Optimization can take time on large indexes. For very large indexes with millions of documents, consider running this during off-peak hours. --- # Current Limitations The FTS implementation has some known limitations that may be addressed in future versions: | Limitation | Description | |------------|-------------| | No `snippet()` function | Cannot return context snippets around matches (highlight is available) | | No automatic segment merging | Uses `NoMergePolicy` - use `OPTIMIZE INDEX` for manual segment merging | | No read-your-writes in transaction | FTS changes within a transaction aren't visible to queries until COMMIT | | No MATCH operator syntax | Must use `fts_match()` function instead of `WHERE table MATCH 'query'` | **Note on transactions:** ROLLBACK works correctly - FTS data is stored in BTrees that participate in the same WAL transaction as table data. When a transaction is rolled back, both table changes and FTS index changes are discarded together. # Comparison with SQLite FTS5 | Feature | SQLite FTS5 | tursodb FTS | |---------|-------------|-------------| | MATCH operator | `WHERE t MATCH 'query'` | `WHERE fts_match(col, 'query')` | | Ranking | `bm25(t)`, `rank` column | `fts_score(col, 'query')` | | Highlighting | `highlight(t, ...)` | `fts_highlight(text, query, before, after)` ✓ | | Snippets | `snippet(t, ...)` | Not implemented | | Boolean operators | AND, OR, NOT | AND, OR, NOT ✓ | | Phrase search | `"exact phrase"` | `"exact phrase"` ✓ | | Prefix search | `word*` | `word*` ✓ | | Column filter | `col:term` | `col:term` ✓ | | Tokenizers | unicode61, ascii, porter | default, raw, simple, whitespace, ngram ✓ | | External content | contentless tables | Not supported | ================================================ FILE: docs/internals/mvcc/DESIGN.md ================================================ # Design ## Persistent storage Persistent storage must implement the `Storage` trait that the MVCC module uses for transaction logging. Figure 1 shows an example of write-ahead log across three transactions. The first transaction T0 executes a `INSERT (id) VALUES (1)` statement, which results in a log record with `id` set to `1`, begin timestamp to 0 (which is the transaction ID) and end timestamp as infinity (meaning the row version is still visible). The second transaction T1 executes another `INSERT` statement, which adds another log record to the transaction log with `id` set to `2`, begin timesstamp to 1 and end timestamp as infinity, similar to what T0 did. Finally, a third transaction T2 executes two statements: `DELETE WHERE id = 1` and `INSERT (id) VALUES (3)`. The first one results in a log record with `id` set to `1` and begin timestamp set to 0 (which is the transaction that created the entry). However, the end timestamp is now set to 2 (the current transaction), which means the entry is now deleted. The second statement results in an entry in the transaction log similar to the `INSERT` statements in T0 and T1. ![Transactions](figures/transactions.png)

Figure 1. Transaction log of three transactions.

When MVCC bootstraps or recovers, it simply redos the transaction log. If the transaction log grows big, we can checkpoint it it by dropping all entries that are no longer visible after the the latest transaction and create a snapshot. ================================================ FILE: docs/internals/mvcc/GC.md ================================================ # MVCC Garbage Collection ## Overview The MVCC store keeps every row version in memory: inserts, updates, deletes, and rolled-back garbage. Without GC, memory grows monotonically with write volume. GC reclaims versions that no active reader can see and that are redundant with the B-tree. GC is driven by two parameters computed at GC time: - **LWM (low-water mark)**: `min(tx.begin_ts)` across Active/Preparing transactions, or `u64::MAX` if none. Tells GC which versions are still visible to some reader. - **ckpt_max** (`durable_txid_max`): the highest committed timestamp whose data has been written to the B-tree. Tells GC when B-tree fallthrough is safe. All GC logic lives in a single function, `gc_version_chain`, shared by both checkpoint-time and background GC. The four rules are applied in order: 1. **Aborted garbage** (`begin=None, end=None`) — remove unconditionally. 2. **Superseded versions** (`end=Timestamp(e), e ≤ lwm`) — remove, unless doing so would let the dual cursor surface a stale B-tree row (tombstone guard). 3. **Sole-survivor current version** (`end=None, b ≤ ckpt_max, b < lwm`, chain length = 1) — remove, because the B-tree has the same data. 4. **TxID references** (`begin=TxID` or `end=TxID`) — keep, the owning transaction hasn't resolved yet. The same code works under both blocking checkpoint (`lwm = u64::MAX`, all versions reclaimable) and a future non-blocking checkpoint (`lwm` finite, pinned by the oldest reader). ## When GC Runs GC is triggered automatically in the `Finalize` stage of checkpoint (`checkpoint_state_machine.rs`), in two phases: 1. `gc_checkpointed_versions()` — iterates only the checkpoint write set (rows just written to B-tree). O(checkpointed rows). 2. `drop_unused_row_versions()` — sweeps all table and index rows. Computes LWM once, then applies `gc_version_chain` to every chain. O(total rows). Both run while the checkpoint lock is still held, before it is released. ## The Dual Cursor Invariant Readers merge B-tree rows with MVCC SkipMap versions via a dual cursor. For each B-tree row, the cursor checks `is_btree_invalidating_version` against every version in the SkipMap entry. If any version invalidates, the B-tree row is hidden and the visible MVCC version (if any) is returned instead. If the SkipMap has **no entry** for the RowID, the B-tree row is returned as-is. This means GC must maintain: > If a row exists in the B-tree, either the SkipMap correctly represents the > row's current state for all active readers, **or** the SkipMap has no entry > (B-tree fallthrough, only safe when B-tree data is up to date). Two hazards follow from this: - **Removing a tombstone before its deletion is checkpointed** resurrects a deleted row — the dual cursor falls through to the stale B-tree row. - **Removing the current version while leaving superseded versions** causes data loss — the superseded version's `end` timestamp still invalidates the B-tree row, but there's no MVCC version to serve reads. These are guarded by Rule 2's tombstone guard and Rule 3's sole-survivor condition respectively. ## Rule Details ### Rule 2: Tombstone Guard When removing a superseded version (`e ≤ lwm`), we check whether the chain has a **committed current version** (`end=None, begin=Timestamp(_)`). If it does, the current version takes over B-tree invalidation and removal is safe. If no committed current version exists, the superseded version may be the only thing hiding a stale B-tree row. Removal is only safe when: - `e ≤ ckpt_max` — the deletion has been checkpointed, B-tree no longer has the row. - But NOT when `e == 0 && ckpt_max == 0` — recovery tombstones before the first real checkpoint (see Recovery below). Pending inserts (`begin=TxID`) do not count as committed current — they might roll back. ### Rule 3: Sole Survivor A current version is redundant with the B-tree when `b ≤ ckpt_max` and `b < lwm`. But we only remove it when it's the **sole** remaining version in the chain. If superseded versions remain, removing the current version would leave orphaned invalidators that hide the B-tree row without providing data. Rule 3 also guards recovery versions: `b=0` versions are protected by requiring `ckpt_max > 0` (see Recovery below). ## Recovery Versions Log recovery stamps versions with `LOGICAL_LOG_RECOVERY_COMMIT_TIMESTAMP = 0`. Since `durable_txid_max` is advanced via `NonZeroU64`, it stays at 0 until the first real transaction is checkpointed. This means `ckpt_max == 0` acts as a natural "recovery data not yet checkpointed" flag: - **Rule 2**: `e == 0 && ckpt_max == 0` → retain (recovery tombstone, B-tree may still have the row). - **Rule 3**: `b == 0 && ckpt_max == 0` → `(b > 0 || ckpt_max > 0)` is false → retain (recovery insert, B-tree may not have the row). Once `ckpt_max > 0`, the first real checkpoint has processed recovery data alongside it, so recovery versions become collectible by the normal rules. The recovery transaction itself is removed from `txs` at the end of `commit_load_tx` to prevent pinning LWM to 0 (which would disable Rules 2-3). ## SkipMap Entry Removal After GC empties a version chain, the SkipMap entry is handled differently depending on the GC path: - **Checkpoint-time GC** (`gc_checkpointed_versions`): removes empty entries using a re-check-under-lock pattern. This is a TOCTOU gap (a writer could insert between the lock release and `remove()`), but safe under the current **blocking** checkpoint — no concurrent writers exist. - **Background GC** (`gc_table_row_versions`, `gc_index_row_versions`): leaves empty entries in place (lazy removal). This avoids the TOCTOU race entirely. Empty entries are reused by `get_or_insert_with` on subsequent inserts, and cleaned up by the next checkpoint-time GC pass. ## Non-blocking Checkpoint Readiness The GC rules are designed to work with both blocking and non-blocking checkpoints — the LWM parameter naturally constrains what can be collected when readers coexist with the checkpoint. **What works today**: all four GC rules, LWM computation, recovery version protection, tombstone guard, lazy removal in background GC. **What needs work for non-blocking checkpoint**: - **Checkpoint-time entry removal**: the re-check-under-lock pattern in `gc_checkpointed_versions` has a TOCTOU gap. Under non-blocking checkpoint, concurrent writers could lose inserted versions. Fix: either hold the write lock across the emptiness check and `remove()`, or switch to lazy removal (same as background GC). ## Key Files | File | Contents | |------|----------| | `core/mvcc/database/mod.rs` | `gc_version_chain`, `compute_lwm`, `drop_unused_row_versions`, `gc_table_row_versions`, `gc_index_row_versions`, recovery tx cleanup in `commit_load_tx` | | `core/mvcc/database/checkpoint_state_machine.rs` | `gc_checkpointed_versions`, auto-trigger wiring in `Finalize` | | `core/mvcc/database/tests.rs` | 39 GC tests (unit, quickcheck, integration, e2e) | ================================================ FILE: docs/internals/mvcc/RECOVERY_SEMANTICS.md ================================================ # MVCC Recovery and Checkpoint Semantics This document describes the MVCC recovery and checkpoint behavior as implemented in `core/mvcc/database/mod.rs` and `core/mvcc/database/checkpoint_state_machine.rs`. The checkpoint model is stop-the-world (blocking checkpoint lock). ## Durable Artifacts Startup decisions use four durable artifacts: - Main database file (`.db`) - WAL file (`.db-wal`) - MVCC logical log (`.db-log`) - MVCC metadata table row: `__turso_internal_mvcc_meta(k='persistent_tx_ts_max')` The logical-log header (56 bytes) contains format metadata and a CRC chain seed: magic, version, flags, hdr_len, salt (u64), reserved, hdr_crc32c. The salt is regenerated on each log truncation; frame CRCs are chained (`crc32c_append(prev_frame_crc, data)`) with the initial seed derived from the salt. `persistent_tx_ts_max` in `__turso_internal_mvcc_meta`is the durable replay boundary, stored inside the main database file/WAL. Recovery replays logical-log frames only when `commit_ts > persistent_tx_ts_max`. ## Bootstrap Order `MvStore::bootstrap()` runs in this order: 1. `maybe_complete_interrupted_checkpoint()` 2. `reparse_schema()` 3. Ensure metadata table exists (or fail closed in invalid states) 4. Build in-memory table-id/root-page mapping from schema 5. `maybe_recover_logical_log()` 6. Promote bootstrap connection to regular MVCC connection Any committed WAL state is reconciled before logical-log replay. The replay boundary comes from the metadata row. ## Startup Case Classification Recovery classifies startup state using two checks: - Does the WAL have committed frames? (`wal.get_max_frame_in_wal() > 0`) - What does `try_read_header()` return? (`Valid`, `Invalid`, or `NoLog`) | Case | Startup artifacts | Recovery behavior | |---|---|---| | 1 | WAL has committed frames + log header valid | Complete interrupted checkpoint: backfill WAL into DB, sync DB, truncate WAL. Then run logical-log recovery with metadata cutoff. | | 2 | WAL has committed frames + log header missing (`NoLog`) | Fail closed with `Corrupt`. | | 3 | WAL has committed frames + log header invalid/torn | Fail closed with `Corrupt`. | | 4 | WAL has no committed frames | Truncate/discard WAL tail bytes and continue logical-log recovery. | | 5 | No WAL + log header invalid/torn | Fail closed with `Corrupt`. | | 6 | No WAL + valid header, no frames (size <= `LOG_HDR_SIZE`) | No replay needed; timestamp state comes from metadata row. | | 7 | No WAL + empty log (0 bytes / `NoLog`) | Timestamp state loaded from metadata row if present; no replay. | Notes: - After checkpoint, the log is truncated to 0 bytes. On restart this is case 7. - Torn tail in log body is treated as EOF (prefix frames remain valid). - First invalid frame during forward scan terminates the scan (prefix preserved), matching SQLite WAL availability semantics. - Missing or corrupt metadata row is treated as corruption when the metadata table is expected to exist. ## Checkpoint Sequence (Blocking Model) 1. Acquire blocking checkpoint lock. 2. Begin pager transaction. 3. Write committed MVCC table/index versions into pager. 4. Upsert metadata row `persistent_tx_ts_max` in the same pager transaction. 5. Commit pager transaction. WAL now contains committed frames for both data and the metadata row. In-memory `durable_txid_max` advances on this transition. 6. Checkpoint WAL (backfill WAL frames into DB file). 7. Fsync DB file (unless `SyncMode::Off`). 8. Truncate logical log to 0 (salt regenerated in memory; header written with next frame). 9. Fsync logical log (unless `SyncMode::Off`). 10. Truncate WAL. 11. Finalize: GC checkpointed versions, release lock. WAL truncation is last. Until the DB file and logical-log cleanup are durable, the WAL remains the authoritative recovery source. ## Correctness Invariants 1. Startup reaches one consistent state or fails closed; no best-effort ambiguity. 2. Committed WAL state is never ignored. 3. Invalid logical-log tail frames are never replayed. 4. Torn or invalid-tail bytes are never interpreted as committed operations. 5. Replay applies only frames with `commit_ts > persistent_tx_ts_max`. 6. `persistent_tx_ts_max` is advanced atomically with pager commit during checkpoint. 7. Same-process checkpoint retries resume from the pager-committed boundary even if later checkpoint phases fail. 8. Logical clock is reseeded to `max(persistent_tx_ts_max, max_replayed_log_commit_ts) + 1`. 9. After interrupted-checkpoint reconciliation, WAL is truncated. ## SyncMode::Off `SyncMode::Off` skips fsync calls. This weakens durability but does not change logical ordering or fail-closed validation behavior. ## Test Coverage Key tests in `core/mvcc/database/tests.rs`: - `test_bootstrap_completes_interrupted_checkpoint_with_committed_wal` - `test_bootstrap_rejects_committed_wal_without_log_file` - `test_bootstrap_rejects_torn_log_header_with_committed_wal` - `test_bootstrap_handles_committed_wal_when_log_truncated` - `test_bootstrap_ignores_wal_frames_without_commit_marker` - `test_bootstrap_rejects_corrupt_log_header_without_wal` - `test_empty_log_recovery_loads_checkpoint_watermark` - `test_meta_checkpoint_case_10_metadata_upsert_is_atomic_with_pager_commit` - `test_meta_checkpoint_case_11_auto_checkpoint_failure_after_commit_remains_recoverable` Logical-log corruption and torn-tail tests are in `core/mvcc/persistent_storage/logical_log.rs`. ================================================ FILE: docs/internals/mvcc/figures/transactions.excalidraw ================================================ { "type": "excalidraw", "version": 2, "source": "https://excalidraw.com", "elements": [ { "id": "tFvpBUMWe3qPFUTQVV14X", "type": "text", "x": 233.14035848761839, "y": 205.73272444200816, "width": 278.57781982421875, "height": 25, "angle": 0, "strokeColor": "#087f5b", "backgroundColor": "#82c91e", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "roundness": null, "seed": 94988319, "version": 510, "versionNonce": 1210831775, "isDeleted": false, "boundElements": null, "updated": 1683370319070, "link": null, "locked": false, "text": "", "fontSize": 20, "fontFamily": 1, "textAlign": "left", "verticalAlign": "top", "baseline": 18, "containerId": null, "originalText": "", "lineHeight": 1.25 }, { "type": "text", "version": 515, "versionNonce": 1881893969, "isDeleted": false, "id": "7i88n1PIb89NxUbVQmTTi", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 938.4614491858606, "y": 311.23272444200813, "strokeColor": "#0b7285", "backgroundColor": "#82c91e", "width": 279.0400085449219, "height": 25, "seed": 1123646321, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370316909, "link": null, "locked": false, "fontSize": 20, "fontFamily": 1, "text": "", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "", "lineHeight": 1.25, "baseline": 18 }, { "type": "text", "version": 556, "versionNonce": 153125934, "isDeleted": false, "id": "Yh8XLtKqXUUYmcmG4SEXn", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 581.1603475012903, "y": 256.23272444200813, "strokeColor": "#e67700", "backgroundColor": "#82c91e", "width": 270.71783447265625, "height": 25, "seed": 1685524017, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683371076075, "link": null, "locked": false, "fontSize": 20, "fontFamily": 1, "text": "", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "", "lineHeight": 1.25, "baseline": 18 }, { "id": "8l0CCJzCAtOLt_2GRcNpa", "type": "text", "x": 256.1403584876185, "y": 409.73272444200813, "width": 234.41998291015625, "height": 75, "angle": 0, "strokeColor": "#087f5b", "backgroundColor": "#82c91e", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "roundness": null, "seed": 583129809, "version": 570, "versionNonce": 561756721, "isDeleted": false, "boundElements": null, "updated": 1683370316909, "link": null, "locked": false, "text": "BEGIN\nINSERT (id) VALUEs (1)\nCOMMIT", "fontSize": 20, "fontFamily": 1, "textAlign": "left", "verticalAlign": "top", "baseline": 68, "containerId": null, "originalText": "BEGIN\nINSERT (id) VALUEs (1)\nCOMMIT", "lineHeight": 1.25 }, { "type": "text", "version": 628, "versionNonce": 282656095, "isDeleted": false, "id": "3m7VluAP5tair6-60b_sp", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 962.0903554358606, "y": 416.23272444200813, "strokeColor": "#0b7285", "backgroundColor": "#82c91e", "width": 243.91998291015625, "height": 100, "seed": 479705617, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370316909, "link": null, "locked": false, "fontSize": 20, "fontFamily": 1, "text": "BEGIN\nDELETE WHERE id =1\nINSERT (id) VALUES (3)\nCOMMIT", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "BEGIN\nDELETE WHERE id =1\nINSERT (id) VALUES (3)\nCOMMIT", "lineHeight": 1.25, "baseline": 93 }, { "type": "text", "version": 574, "versionNonce": 1128746001, "isDeleted": false, "id": "Z-Mh1kti2oC6sIMnuGluo", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 613.0903554358607, "y": 417.23272444200813, "strokeColor": "#e67700", "backgroundColor": "#82c91e", "width": 243.239990234375, "height": 75, "seed": 580440625, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370316909, "link": null, "locked": false, "fontSize": 20, "fontFamily": 1, "text": "BEGIN\nINSERT (id) VALUEs (2)\nCOMMIT", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "BEGIN\nINSERT (id) VALUEs (2)\nCOMMIT", "lineHeight": 1.25, "baseline": 68 }, { "type": "line", "version": 1502, "versionNonce": 1835608607, "isDeleted": false, "id": "VuJNZCgz1Y0WEWwug7pGk", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "x": 226.3083636621349, "y": 173.11701218356845, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 20.336010349032712, "height": 203.23377930246647, "seed": 1879839231, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370316909, "link": null, "locked": false, "startBinding": null, "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": null, "points": [ [ 0, 0 ], [ -20.264781987976257, -0.0011773927935071482 ], [ -20.336010349032712, 203.23260190967298 ], [ -0.07239358683375485, 203.135377672515 ] ] }, { "type": "line", "version": 1755, "versionNonce": 1487752017, "isDeleted": false, "id": "GpZg3Rw4Hszxzxf38Q4Hn", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 3.141592653589793, "x": 539.3083636621348, "y": 178.11701218356845, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 20.336010349032712, "height": 203.23377930246647, "seed": 470135121, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370316909, "link": null, "locked": false, "startBinding": null, "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": null, "points": [ [ 0, 0 ], [ -20.264781987976257, -0.0011773927935071482 ], [ -20.336010349032712, 203.23260190967298 ], [ -0.07239358683375485, 203.135377672515 ] ] }, { "type": "text", "version": 528, "versionNonce": 1276939839, "isDeleted": false, "id": "AGEyNvBxBm2cwm1WRW8n8", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 576.6403584876185, "y": 210.23272444200816, "strokeColor": "#087f5b", "backgroundColor": "#82c91e", "width": 278.57781982421875, "height": 25, "seed": 877528401, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370316909, "link": null, "locked": false, "fontSize": 20, "fontFamily": 1, "text": "", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "", "lineHeight": 1.25, "baseline": 18 }, { "type": "line", "version": 1557, "versionNonce": 773679889, "isDeleted": false, "id": "Q8E0gAcLvq6VXqMDZhLdA", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "x": 581.8083636621351, "y": 177.61701218356845, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 20.336010349032712, "height": 203.23377930246647, "seed": 153279217, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370316909, "link": null, "locked": false, "startBinding": null, "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": null, "points": [ [ 0, 0 ], [ -20.264781987976257, -0.0011773927935071482 ], [ -20.336010349032712, 203.23260190967298 ], [ -0.07239358683375485, 203.135377672515 ] ] }, { "type": "line", "version": 1810, "versionNonce": 1561283199, "isDeleted": false, "id": "uhh3ZkPO6bwwf0-AI8syI", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 3.141592653589793, "x": 894.8083636621349, "y": 182.61701218356845, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 20.336010349032712, "height": 203.23377930246647, "seed": 315380945, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370316909, "link": null, "locked": false, "startBinding": null, "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": null, "points": [ [ 0, 0 ], [ -20.264781987976257, -0.0011773927935071482 ], [ -20.336010349032712, 203.23260190967298 ], [ -0.07239358683375485, 203.135377672515 ] ] }, { "type": "text", "version": 575, "versionNonce": 910156017, "isDeleted": false, "id": "jI5YKyaOdGYYKiBWZmCMs", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 929.6403584876182, "y": 215.23272444200813, "strokeColor": "#087f5b", "backgroundColor": "#82c91e", "width": 278.57781982421875, "height": 25, "seed": 121503167, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370316909, "link": null, "locked": false, "fontSize": 20, "fontFamily": 1, "text": "", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "", "lineHeight": 1.25, "baseline": 18 }, { "type": "line", "version": 1604, "versionNonce": 19920575, "isDeleted": false, "id": "QqIk7VTnRWYq499wkttvv", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "x": 934.8083636621348, "y": 182.61701218356842, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 20.336010349032712, "height": 203.23377930246647, "seed": 2012037663, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370316909, "link": null, "locked": false, "startBinding": null, "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": null, "points": [ [ 0, 0 ], [ -20.264781987976257, -0.0011773927935071482 ], [ -20.336010349032712, 203.23260190967298 ], [ -0.07239358683375485, 203.135377672515 ] ] }, { "type": "line", "version": 1857, "versionNonce": 1660885169, "isDeleted": false, "id": "gk89VsYpnf9Jby9KEUBd3", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 3.141592653589793, "x": 1247.808363662135, "y": 187.61701218356842, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 20.336010349032712, "height": 203.23377930246647, "seed": 509453887, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370316909, "link": null, "locked": false, "startBinding": null, "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": null, "points": [ [ 0, 0 ], [ -20.264781987976257, -0.0011773927935071482 ], [ -20.336010349032712, 203.23260190967298 ], [ -0.07239358683375485, 203.135377672515 ] ] }, { "type": "text", "version": 620, "versionNonce": 1588681010, "isDeleted": false, "id": "a1c-iZI0SafCiy0u4xieZ", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 934.3714375891809, "y": 261.23272444200813, "strokeColor": "#e67700", "backgroundColor": "#82c91e", "width": 270.71783447265625, "height": 25, "seed": 1742829553, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683371080181, "link": null, "locked": false, "fontSize": 20, "fontFamily": 1, "text": "", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "", "lineHeight": 1.25, "baseline": 18 }, { "type": "text", "version": 564, "versionNonce": 1968863633, "isDeleted": false, "id": "hdhhgp5nA06o5EcSgHQE8", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 937.6203542151575, "y": 354.23272444200813, "strokeColor": "#0b7285", "backgroundColor": "#82c91e", "width": 287.73785400390625, "height": 25, "seed": 309558367, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1683370363648, "link": null, "locked": false, "fontSize": 20, "fontFamily": 1, "text": "", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "", "lineHeight": 1.25, "baseline": 18 } ], "appState": { "gridSize": null, "viewBackgroundColor": "#ffffff" }, "files": {} } ================================================ FILE: docs/javascript-api-reference.md ================================================ # JavaScript API reference This document describes the JavaScript API for Turso. The API is implemented in two different packages: - [@tursodatabase/database](https://www.npmjs.com/package/@tursodatabase/database) (`bindings/javascript`) - Native bindings for the Turso database. - [@tursodatabase/serverless](https://www.npmjs.com/package/@tursodatabase/serverless) (`packages/turso-serverless`) - Serverless driver for Turso Cloud databases. The API is compatible with the libSQL promise API, which is an asynchronous variant of the `better-sqlite3` API. ## Functions #### connect(path, [options]) ⇒ Database Opens a new database connection. | Param | Type | Description | | ------- | ------------------- | ------------------------- | | path | string | Path to the database file | The `path` parameter points to the SQLite database file to open. If the file pointed to by `path` does not exists, it will be created. To open an in-memory database, please pass `:memory:` as the `path` parameter. The function returns a `Database` object. ## class Database The `Database` class represents a connection that can prepare and execute SQL statements. ### Methods #### prepare(sql) ⇒ Statement Prepares a SQL statement for execution. | Param | Type | Description | | ------ | ------------------- | ------------------------------------ | | sql | string | The SQL statement string to prepare. | The function returns a `Statement` object. #### transaction(function) ⇒ function This function is currently not supported. #### pragma(string, [options]) ⇒ results This function is currently not supported. #### backup(destination, [options]) ⇒ promise This function is currently not supported. #### serialize([options]) ⇒ Buffer This function is currently not supported. #### function(name, [options], function) ⇒ this This function is currently not supported. #### aggregate(name, options) ⇒ this This function is currently not supported. #### table(name, definition) ⇒ this This function is currently not supported. #### authorizer(rules) ⇒ this This function is currently not supported. #### loadExtension(path, [entryPoint]) ⇒ this This function is currently not supported. #### exec(sql) ⇒ this Executes a SQL statement. | Param | Type | Description | | ------ | ------------------- | ------------------------------------ | | sql | string | The SQL statement string to execute. | #### interrupt() ⇒ this This function is currently not supported. #### close() ⇒ this Closes the database connection. ## class Statement ### Methods #### run([...bindParameters]) ⇒ object Executes the SQL statement and returns an info object. | Param | Type | Description | | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | The returned info object contains two properties: `changes` that describes the number of modified rows and `info.lastInsertRowid` that represents the `rowid` of the last inserted row. #### get([...bindParameters]) ⇒ row Executes the SQL statement and returns the first row. | Param | Type | Description | | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | ### all([...bindParameters]) ⇒ array of rows Executes the SQL statement and returns an array of the resulting rows. | Param | Type | Description | | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | ### iterate([...bindParameters]) ⇒ iterator Executes the SQL statement and returns an iterator to the resulting rows. | Param | Type | Description | | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | #### pluck([toggleState]) ⇒ this This function is currently not supported. #### expand([toggleState]) ⇒ this This function is currently not supported. #### raw([rawMode]) ⇒ this This function is currently not supported. #### timed([toggle]) ⇒ this This function is currently not supported. #### columns() ⇒ array of objects This function is currently not supported. #### bind([...bindParameters]) ⇒ this This function is currently not supported. ================================================ FILE: docs/language-reference/book/print.html ================================================ Turso SQL Language Reference

Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Turso SQL Language Reference

Turso is a SQLite-compatible database. This reference documents the SQL language as supported by Turso.

If you are familiar with SQLite, Turso supports most of the same SQL syntax. This reference covers only what Turso supports — features not listed here are not yet available. For Turso-specific extensions beyond SQLite (custom types, vector search, CDC, materialized views, encryption), see the Turso Extensions section.

How to Read Syntax Definitions

Every statement page begins with a Syntax section showing the grammar using the following notation:

NotationMeaning
KEYWORDA literal SQL keyword. Keywords are case-insensitive; uppercase is used by convention.
nameA placeholder for a user-supplied identifier (table name, column name, etc.).
exprA placeholder for any SQL expression.
[X]X is optional.
{A | B}Choose exactly one of A or B.
[A | B]Optionally choose one of A or B.
[, ...]The preceding element may be repeated, separated by commas.

Example

INSERT [OR {ROLLBACK | ABORT | FAIL | IGNORE | REPLACE}]
  INTO table-name [(column-name [, ...])]
  VALUES (expr [, ...]) [, ...]

This means:

  • INSERT is required.
  • OR ROLLBACK, OR ABORT, etc. are optional — pick one if used.
  • The column list is optional.
  • At least one VALUES row is required, and you may provide more separated by commas.
  • Each row contains one or more expressions separated by commas.

Identifiers and Quoting

Identifiers (table names, column names) follow these rules:

  • Unquoted identifiers may contain letters, digits, and underscores, and must not start with a digit.
  • Identifiers can be quoted with double quotes ("name"), square brackets ([name]), or backticks (`name`).
  • Quoted identifiers may contain any character, including spaces and reserved words.
  • String literals use single quotes ('text'). Double quotes are for identifiers, not strings.

Type Affinity

Turso uses SQLite’s dynamic type system. Every value has one of five storage classes:

Storage ClassDescription
NULLA null value.
INTEGERA signed integer, stored in 1, 2, 3, 4, 6, or 8 bytes.
REALA floating-point number, stored as an 8-byte IEEE 754 float.
TEXTA UTF-8 string.
BLOBRaw binary data, stored exactly as provided.

Column type names in CREATE TABLE determine the column’s type affinity, which influences how values are coerced on insertion. See Type Conversions for the full rules.

SELECT

Syntax

SELECT [DISTINCT | ALL] result-column [, ...]
  [FROM table-or-subquery [, ...]]
  [WHERE expr]
  [GROUP BY expr [, ...] [HAVING expr]]
  [ORDER BY ordering-term [, ...]]
  [LIMIT expr [OFFSET expr]]

Where result-column is one of:

*
table-name.*
expr [[AS] column-alias]

And table-or-subquery is one of:

table-name [[AS] table-alias]
(select-statement) [[AS] table-alias]
table-or-subquery join-operator table-or-subquery join-constraint

This page covers the basic SELECT statement: the SELECT clause, FROM clause, WHERE clause, and DISTINCT/ALL keywords. For related topics, see JOINs, GROUP BY and HAVING, ORDER BY, LIMIT, OFFSET, Set Operations, Subqueries, and Common Table Expressions.

Description

The SELECT statement queries the database and returns zero or more rows of data. Each row has a fixed number of columns determined by the result expression list. A SELECT statement does not modify the database.

Processing a SELECT statement follows four steps:

  1. FROM clause – the input dataset is determined. If there is no FROM clause, the input is implicitly a single row with zero columns.
  2. WHERE clause – the input rows are filtered by evaluating the WHERE expression as a boolean for each row; only rows where the expression is true are kept.
  3. Result expressions – the result columns are computed by evaluating each expression in the SELECT list against the filtered rows.
  4. DISTINCT/ALL – if DISTINCT is specified, duplicate result rows are removed.

Clauses

SELECT (Result Expressions)

The list of expressions between SELECT and FROM is the result expression list. Each expression becomes a column in the output. Expressions can be constants, column references, computed values, or function calls.

-- Selecting literal values (no FROM clause needed)
SELECT 1 + 2;
-- Selecting with string concatenation
SELECT 10 * 2 AS doubled, 'hello' || ' ' || 'world' AS combined;

Column Aliases

Any result expression can be given a name using the AS keyword. This alias becomes the column header in the output and can be referenced in ORDER BY and GROUP BY clauses.

SELECT name AS user_name, email AS contact
  FROM users
  WHERE active = 1;

The AS keyword is optional – SELECT name user_name FROM users is also valid – but including it is recommended for clarity.

The Asterisk Wildcard

The special expression * expands to all columns from all tables in the FROM clause.

-- Return all columns from the users table
SELECT * FROM users;

To expand all columns from a specific table (useful with multiple tables), use table.* or alias.*:

SELECT u.* FROM users u WHERE u.active = 1;

The * and table.* forms can only be used in the result expression list of a SELECT that has a FROM clause.

FROM

The FROM clause specifies the input data for the query. If omitted, the input is implicitly a single row with zero columns, which is useful for evaluating expressions.

-- No FROM clause: evaluate an expression directly
SELECT typeof(42), typeof(3.14), typeof('text'), typeof(NULL);

Single Table

The simplest FROM clause names a single table. The query operates on all rows of that table.

SELECT name, email FROM users;

Table Aliases

A table can be given an alias with the AS keyword (or simply by placing the alias after the table name). The alias can then be used to qualify column names.

SELECT u.name, u.email
  FROM users AS u
  WHERE u.active = 1;

Aliases are required when the same table appears more than once in a query, and they are convenient for shortening long table names.

Multiple Tables

When multiple tables are listed in the FROM clause separated by commas, Turso computes the Cartesian product of all rows from each table. This means every combination of rows is produced. A WHERE clause is typically used to filter the result to only the meaningful combinations.

-- Comma-separated tables with a WHERE condition (implicit join)
SELECT u.name, o.amount
  FROM users AS u, orders AS o
  WHERE u.id = o.user_id;

A comma between tables is equivalent to INNER JOIN or JOIN with no ON clause. For explicit join syntax with ON or USING clauses, see JOINs.

Subqueries in FROM

A parenthesized SELECT statement can appear in the FROM clause. The subquery is treated as a virtual table containing the data it returns. A subquery in FROM should be given an alias.

SELECT *
  FROM (
    SELECT user_id, sum(amount) AS total
      FROM orders
      GROUP BY user_id
  ) AS user_totals
  WHERE total > 500;

Each column of the subquery inherits the type affinity and collation of the corresponding expression in the subquery’s result list.

WHERE

The WHERE clause filters the input rows by evaluating its expression as a boolean for each row. Only rows where the expression evaluates to true are included in the result. Rows for which the expression evaluates to false or NULL are excluded.

-- Simple equality condition
SELECT name, email FROM users WHERE active = 1;
-- Multiple conditions with AND
SELECT name, email FROM users WHERE active = 1 AND name <> 'Alice';
-- Using OR to match alternative conditions
SELECT name, price
  FROM products
  WHERE category = 'Electronics' OR price < 250;

NULL Handling

Comparisons with NULL using = or <> always evaluate to NULL (not true or false), so rows with NULL values in the compared column are excluded by such conditions. Use IS NULL and IS NOT NULL to test for null values explicitly.

-- Find rows where email is missing
SELECT name FROM users WHERE email IS NULL;
-- Find rows where email is present
SELECT name, email FROM users WHERE email IS NOT NULL;

Pattern Matching with LIKE

The LIKE operator performs case-insensitive pattern matching on text values. The % wildcard matches any sequence of characters, and _ matches any single character.

-- Names starting with 'A'
SELECT name, email FROM users WHERE name LIKE 'A%';
-- Names containing 'a' anywhere (case-insensitive)
SELECT name FROM products WHERE name LIKE '%a%';

For more details, see Pattern Matching.

BETWEEN

The BETWEEN operator tests whether a value falls within an inclusive range.

SELECT name, price FROM products WHERE price BETWEEN 200 AND 700;

For more details, see IN and BETWEEN.

IN

The IN operator tests whether a value matches any value in a list or subquery result.

SELECT name, category
  FROM products
  WHERE category IN ('Electronics', 'Furniture') AND price > 500;

For more details, see IN and BETWEEN.

DISTINCT and ALL

By default (or when ALL is specified explicitly), all result rows are returned, including duplicates. When DISTINCT is specified, duplicate rows are removed from the result set before it is returned.

-- Without DISTINCT: may contain duplicate categories
SELECT ALL category FROM products;

-- With DISTINCT: each category appears only once
SELECT DISTINCT category FROM products;

For the purposes of detecting duplicates, two NULL values are considered equal. An integer is equal to a floating-point number if they represent the same quantity. Text values are compared using the appropriate collation sequence.

Examples

The examples below use the following tables:

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  name TEXT,
  email TEXT,
  active INTEGER
);
INSERT INTO users VALUES
  (1, 'Alice', 'alice@example.com', 1),
  (2, 'Bob', 'bob@example.com', 0),
  (3, 'Charlie', 'charlie@example.com', 1);

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  category TEXT,
  price REAL
);
INSERT INTO products VALUES
  (1, 'Laptop', 'Electronics', 999.99),
  (2, 'Phone', 'Electronics', 699.99),
  (3, 'Desk', 'Furniture', 299.99),
  (4, 'Chair', 'Furniture', 199.99),
  (5, 'Tablet', 'Electronics', 499.99);

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  user_id INTEGER,
  product TEXT,
  amount REAL
);
INSERT INTO orders VALUES
  (1, 1, 'Laptop', 999.99),
  (2, 1, 'Phone', 699.99),
  (3, 2, 'Desk', 299.99),
  (4, 3, 'Chair', 199.99),
  (5, 1, 'Tablet', 499.99);

Selecting All Columns

SELECT * FROM users;
-- id | name    | email               | active
-- 1  | Alice   | alice@example.com   | 1
-- 2  | Bob     | bob@example.com     | 0
-- 3  | Charlie | charlie@example.com | 1

Selecting Specific Columns

SELECT name, email FROM users;
-- name    | email
-- Alice   | alice@example.com
-- Bob     | bob@example.com
-- Charlie | charlie@example.com

Filtering with WHERE

SELECT name, email FROM users WHERE active = 1;
-- name    | email
-- Alice   | alice@example.com
-- Charlie | charlie@example.com

Column Aliases and Computed Expressions

SELECT
  name,
  price,
  price * 0.9 AS discounted_price
FROM products
WHERE price > 300;
-- name   | price  | discounted_price
-- Laptop | 999.99 | 899.991
-- Phone  | 699.99 | 629.991
-- Tablet | 499.99 | 449.991

CASE Expressions in SELECT

SELECT
  name,
  price,
  CASE
    WHEN price > 500 THEN 'expensive'
    WHEN price > 200 THEN 'moderate'
    ELSE 'affordable'
  END AS price_tier
FROM products;
-- name   | price  | price_tier
-- Laptop | 999.99 | expensive
-- Phone  | 699.99 | expensive
-- Desk   | 299.99 | moderate
-- Chair  | 199.99 | affordable
-- Tablet | 499.99 | moderate

Multiple Tables with Implicit Join

SELECT u.name, o.amount
  FROM users AS u, orders AS o
  WHERE u.id = o.user_id;
-- name  | amount
-- Alice | 999.99
-- Alice | 699.99
-- Alice | 499.99
-- Bob   | 299.99
-- Charlie | 199.99

Subquery in FROM

SELECT *
  FROM (
    SELECT user_id, sum(amount) AS total
      FROM orders
      GROUP BY user_id
  ) AS user_totals
  WHERE total > 500;
-- user_id | total
-- 1       | 2199.97

DISTINCT

SELECT DISTINCT category FROM products;
-- category
-- Electronics
-- Furniture

Compatibility

Turso supports the standard SELECT statement with the following note:

FeatureStatus
schema.table.column (three-part names)Not supported. Turso does not support attached databases or schema-qualified table names. Use table.column (two-part names) instead.

JOIN

Syntax

table-or-subquery {[INNER] JOIN | LEFT [OUTER] JOIN | NATURAL [LEFT [OUTER]] JOIN} table-or-subquery [join-constraint]

Where join-constraint is one of:

ON expr
USING (column-name [, ...])

Tables may also be joined implicitly using a comma:

SELECT ... FROM table1, table2 WHERE expr

Description

A JOIN combines rows from two or more tables based on a related column between them. The result of a join is a new set of rows, where each row contains columns from both tables.

Conceptually, a join starts with the cartesian product of the left and right datasets – every row from the left table paired with every row from the right table. A join constraint (ON or USING) then filters this cartesian product to only the rows where the constraint is satisfied. Different join types control what happens with rows that have no match.

Turso supports INNER JOIN, LEFT OUTER JOIN, and NATURAL JOIN. Multiple joins can be chained in a single query and are evaluated left to right.

Join Types

INNER JOIN

An INNER JOIN returns only the rows where the join constraint is satisfied in both tables. Rows from either table that have no matching row in the other table are excluded from the result.

The keyword INNER is optional – JOIN by itself is equivalent to INNER JOIN.

-- These are equivalent
SELECT * FROM users INNER JOIN departments ON users.department_id = departments.id;
SELECT * FROM users JOIN departments ON users.department_id = departments.id;

LEFT OUTER JOIN

A LEFT JOIN returns all rows from the left table, even if there is no matching row in the right table. When a left-table row has no match, the columns from the right table are filled with NULL.

The keyword OUTER is optional – LEFT JOIN and LEFT OUTER JOIN are equivalent.

-- These are equivalent
SELECT * FROM users LEFT JOIN departments ON users.department_id = departments.id;
SELECT * FROM users LEFT OUTER JOIN departments ON users.department_id = departments.id;

NATURAL JOIN

A NATURAL JOIN automatically joins two tables on all columns that share the same name in both tables. It is equivalent to a join with a USING clause that lists every common column name.

If the two tables share no column names, a NATURAL JOIN behaves like a cartesian product (every row paired with every row).

NATURAL can be combined with LEFT to form a NATURAL LEFT JOIN. A NATURAL JOIN cannot have an explicit ON or USING clause.

-- If both tables have a column named "id", this is equivalent to:
-- SELECT * FROM users JOIN profiles USING(id)
SELECT * FROM users NATURAL JOIN profiles;

Comma-Separated Tables (Implicit Join)

Listing tables separated by commas in the FROM clause produces the cartesian product of those tables. A WHERE clause is typically used to filter the result, which is functionally equivalent to an INNER JOIN with an ON clause.

-- These produce the same result
SELECT * FROM users, departments WHERE users.department_id = departments.id;
SELECT * FROM users JOIN departments ON users.department_id = departments.id;

Join Constraints

ON Clause

The ON clause specifies a boolean expression that is evaluated for each row of the cartesian product. Only rows where the expression evaluates to true are included in the result. The expression can reference columns from both tables.

SELECT users.name, departments.name
FROM users JOIN departments ON users.department_id = departments.id;

The ON clause can contain compound conditions using AND, OR, and other operators:

SELECT u.name, o.amount
FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.amount > 75.00;

USING Clause

The USING clause specifies one or more column names that must exist in both tables. For each named column, the join matches rows where the values are equal. This is equivalent to writing ON left.col = right.col for each column, but more concise.

An important difference from ON: the USING clause eliminates the duplicate column from the result. Only one copy of each named column appears in the output.

SELECT users.name, departments.dept_name
FROM users JOIN departments USING(department_id);

Multiple columns can be specified in a single USING clause:

SELECT * FROM t1 JOIN t2 USING(a, b);

ON vs WHERE in Outer Joins

For INNER JOINs, placing a condition in the ON clause or the WHERE clause produces the same result. For LEFT JOINs, however, the distinction matters:

  • ON clause: The condition is applied during the join. Rows from the left table that do not match still appear in the result with NULLs for the right-table columns.
  • WHERE clause: The condition is applied after the join, including after NULL rows have been added. This can filter out the unmatched rows.

Consider this example where Charlie has no department (department_id is NULL):

-- ON clause: Charlie appears with NULL department
SELECT users.name, departments.name
FROM users LEFT JOIN departments ON users.department_id = departments.id;
┌─────────┬─────────────┐
│ name    │ name        │
├─────────┼─────────────┤
│ Alice   │ Engineering │
│ Bob     │ Marketing   │
│ Charlie │             │
└─────────┴─────────────┘
-- WHERE clause: Charlie is excluded because departments.name IS NOT NULL fails
SELECT users.name, departments.name
FROM users LEFT JOIN departments ON users.department_id = departments.id
WHERE departments.name IS NOT NULL;
┌───────┬─────────────┐
│ name  │ name        │
├───────┼─────────────┤
│ Alice │ Engineering │
│ Bob   │ Marketing   │
└───────┴─────────────┘

Examples

Basic Inner Join

CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, department_id INTEGER);
CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT);

INSERT INTO users VALUES (1, 'Alice', 1);
INSERT INTO users VALUES (2, 'Bob', 2);
INSERT INTO users VALUES (3, 'Charlie', NULL);

INSERT INTO departments VALUES (1, 'Engineering');
INSERT INTO departments VALUES (2, 'Marketing');
INSERT INTO departments VALUES (3, 'Sales');

-- Return only users that have a matching department
SELECT users.name, departments.name
FROM users INNER JOIN departments ON users.department_id = departments.id;
┌───────┬─────────────┐
│ name  │ name        │
├───────┼─────────────┤
│ Alice │ Engineering │
│ Bob   │ Marketing   │
└───────┴─────────────┘

Left Join to Find Unmatched Rows

CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER, amount REAL);

INSERT INTO users VALUES (1, 'Alice');
INSERT INTO users VALUES (2, 'Bob');
INSERT INTO users VALUES (3, 'Charlie');

INSERT INTO orders VALUES (1, 1, 99.99);
INSERT INTO orders VALUES (2, 1, 49.50);
INSERT INTO orders VALUES (3, 2, 150.00);

-- Find users who have never placed an order
SELECT u.name
FROM users u LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
┌─────────┐
│ name    │
├─────────┤
│ Charlie │
└─────────┘

Self-Join

A table can be joined to itself using aliases. This is useful for hierarchical data such as employee-manager relationships.

CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, manager_id INTEGER);

INSERT INTO employees VALUES (1, 'Alice', NULL);
INSERT INTO employees VALUES (2, 'Bob', 1);
INSERT INTO employees VALUES (3, 'Charlie', 1);
INSERT INTO employees VALUES (4, 'Diana', 2);

-- Show each employee alongside their manager's name
SELECT e.name AS employee, m.name AS manager
FROM employees e LEFT JOIN employees m ON e.manager_id = m.id;
┌──────────┬─────────┐
│ employee │ manager │
├──────────┼─────────┤
│ Alice    │         │
│ Bob      │ Alice   │
│ Charlie  │ Alice   │
│ Diana    │ Bob     │
└──────────┴─────────┘

Multi-Table Join with Aggregation

CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, department_id INTEGER);
CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER, amount REAL);

INSERT INTO users VALUES (1, 'Alice', 1);
INSERT INTO users VALUES (2, 'Bob', 2);
INSERT INTO users VALUES (3, 'Charlie', NULL);

INSERT INTO departments VALUES (1, 'Engineering');
INSERT INTO departments VALUES (2, 'Marketing');
INSERT INTO departments VALUES (3, 'Sales');

INSERT INTO orders VALUES (1, 1, 99.99);
INSERT INTO orders VALUES (2, 1, 49.50);
INSERT INTO orders VALUES (3, 2, 150.00);

-- Show each user's department and total spending
SELECT u.name, d.name AS department, SUM(o.amount) AS total_spent
FROM users u
JOIN departments d ON u.department_id = d.id
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id;
┌───────┬─────────────┬─────────────┐
│ name  │ department  │ total_spent │
├───────┼─────────────┼─────────────┤
│ Alice │ Engineering │      149.49 │
│ Bob   │ Marketing   │       150.0 │
└───────┴─────────────┴─────────────┘

Natural Left Join

CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE profiles (id INTEGER PRIMARY KEY, bio TEXT);

INSERT INTO users VALUES (1, 'Alice');
INSERT INTO users VALUES (2, 'Bob');
INSERT INTO users VALUES (3, 'Charlie');

INSERT INTO profiles VALUES (1, 'Engineer');
INSERT INTO profiles VALUES (2, 'Designer');

-- NATURAL LEFT JOIN matches on the shared "id" column
-- and preserves users with no profile
SELECT name, bio FROM users NATURAL LEFT JOIN profiles;
┌─────────┬──────────┐
│ name    │ bio      │
├─────────┼──────────┤
│ Alice   │ Engineer │
│ Bob     │ Designer │
│ Charlie │          │
└─────────┴──────────┘

Join with a Subquery

CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER, amount REAL);

INSERT INTO users VALUES (1, 'Alice');
INSERT INTO users VALUES (2, 'Bob');
INSERT INTO users VALUES (3, 'Charlie');

INSERT INTO orders VALUES (1, 1, 99.99);
INSERT INTO orders VALUES (2, 1, 49.50);
INSERT INTO orders VALUES (3, 2, 150.00);

-- Join against an aggregated subquery
SELECT u.name, totals.total_amount
FROM users u
JOIN (SELECT user_id, SUM(amount) AS total_amount FROM orders GROUP BY user_id) AS totals
  ON u.id = totals.user_id;
┌───────┬──────────────┐
│ name  │ total_amount │
├───────┼──────────────┤
│ Alice │       149.49 │
│ Bob   │        150.0 │
└───────┴──────────────┘

Cartesian Product with Comma Syntax

CREATE TABLE colors (name TEXT);
CREATE TABLE sizes (name TEXT);

INSERT INTO colors VALUES ('red');
INSERT INTO colors VALUES ('blue');

INSERT INTO sizes VALUES ('small');
INSERT INTO sizes VALUES ('large');

-- Every combination of color and size
SELECT colors.name, sizes.name FROM colors, sizes;
┌──────┬───────┐
│ name │ name  │
├──────┼───────┤
│ red  │ small │
│ red  │ large │
│ blue │ small │
│ blue │ large │
└──────┴───────┘

Compatibility

Turso does not support the following join features available in SQLite:

FeatureStatus
RIGHT JOIN / RIGHT OUTER JOINNot supported
FULL JOIN / FULL OUTER JOINNot supported
CROSS JOINNot supported. In SQLite, CROSS JOIN is semantically identical to INNER JOIN but hints the optimizer not to reorder the join. Turso does not parse this syntax. Use INNER JOIN or comma syntax instead.

GROUP BY and HAVING

Syntax

SELECT result-column [, ...]
FROM table-or-subquery
[WHERE where-expr]
[GROUP BY expr [, ...]]
[HAVING having-expr]

Description

The GROUP BY clause organizes rows into groups based on one or more expressions. When GROUP BY is present, each unique combination of values in the grouping expressions forms a single group, and the query returns one row per group. Aggregate functions in the result set (such as COUNT(), SUM(), AVG(), MIN(), MAX(), and GROUP_CONCAT()) are evaluated once per group rather than once for the entire result set.

The HAVING clause filters groups after they have been formed. It works like WHERE, but operates on the grouped results rather than on individual rows. HAVING is evaluated once per group and may reference aggregate functions.

Together, GROUP BY and HAVING enable summary queries – computing totals, averages, counts, and other statistics across categories of data.

Clauses

GROUP BY

The GROUP BY clause accepts one or more expressions, separated by commas. Each expression is evaluated for every row in the input, and rows that produce equal values for all grouping expressions are combined into a single group.

Key behaviors:

  • NULL values are considered equal for grouping purposes. All rows with NULL in a grouping column belong to the same group.
  • Expressions, not just column names, may be used. You can group by computed values, CASE expressions, or function calls.
  • Column position numbers may be used. GROUP BY 1 refers to the first column in the result set.
  • Grouping expressions need not appear in the result set. You can group by a column without selecting it.
  • Grouping expressions must not be aggregate expressions. GROUP BY SUM(x) is an error.
  • Collation sequences apply when comparing TEXT values. The default collation is BINARY.

When no GROUP BY clause is present but the result set contains aggregate functions, the entire input is treated as a single group and the query returns exactly one row.

HAVING

The HAVING clause contains a boolean expression that is evaluated once per group. Groups for which the expression evaluates to false (or NULL) are excluded from the result set.

  • HAVING may reference aggregate functions. This is the primary distinction from WHERE, which cannot.
  • HAVING may reference values that are not in the result set.
  • If HAVING contains a non-aggregate expression, it is evaluated against an arbitrarily selected row from the group.
  • HAVING can be used without GROUP BY. In that case the entire result set is treated as one group, and HAVING determines whether that single group is returned or discarded.

Evaluation Order

When WHERE, GROUP BY, and HAVING all appear in the same query, they are processed in this order:

StepClausePurpose
1WHEREFilters individual rows before grouping
2GROUP BYOrganizes remaining rows into groups
3Aggregate functionsComputed once per group
4HAVINGFilters groups after aggregation

This means WHERE reduces the input before any grouping occurs, while HAVING operates on the already-grouped results. Use WHERE to exclude rows you do not want aggregated. Use HAVING to exclude groups based on aggregate values.

Bare Columns in Aggregate Queries

A “bare” column is a non-aggregate column that does not appear in the GROUP BY clause. For example:

SELECT customer, product, SUM(quantity) FROM orders GROUP BY customer;

Here customer is in GROUP BY, SUM(quantity) is an aggregate, but product is a bare column. Since each group may contain multiple distinct values for product, the value of product in the result is selected from an arbitrary row within the group.

Special behavior with MIN() and MAX(): When there is exactly one MIN() or MAX() aggregate in the query, bare columns take their values from the row that contains the minimum or maximum value. For example:

SELECT customer, product, MAX(quantity) AS max_quantity
FROM orders
GROUP BY customer;

The product value in each row comes from the input row that has the largest quantity for that customer.

This behavior is an extension beyond the SQL standard. Most other database engines require every non-aggregate column to appear in the GROUP BY clause.

Examples

Basic GROUP BY with COUNT

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer TEXT,
  product TEXT,
  quantity INTEGER,
  price REAL,
  region TEXT
);
INSERT INTO orders VALUES (1, 'Alice', 'Widget', 5, 9.99, 'East');
INSERT INTO orders VALUES (2, 'Bob', 'Gadget', 2, 24.99, 'West');
INSERT INTO orders VALUES (3, 'Alice', 'Gadget', 1, 24.99, 'East');
INSERT INTO orders VALUES (4, 'Carol', 'Widget', 10, 9.99, 'East');
INSERT INTO orders VALUES (5, 'Bob', 'Widget', 3, 9.99, 'West');
INSERT INTO orders VALUES (6, 'Alice', 'Gizmo', 2, 49.99, 'East');
INSERT INTO orders VALUES (7, 'Carol', 'Gadget', 4, 24.99, 'West');
INSERT INTO orders VALUES (8, 'Dave', 'Widget', 7, 9.99, 'South');

-- Count orders per customer
SELECT customer, COUNT(*) AS order_count
FROM orders
GROUP BY customer;
-- Alice|3
-- Bob|2
-- Carol|2
-- Dave|1

GROUP BY with SUM

-- Total quantity and revenue per product
SELECT product,
       SUM(quantity) AS total_quantity,
       SUM(quantity * price) AS total_revenue
FROM orders
GROUP BY product;
-- Gadget|7|174.93
-- Gizmo|2|99.98
-- Widget|25|249.75

GROUP BY with HAVING

-- Customers who spent more than $100
SELECT customer, SUM(quantity * price) AS total_spent
FROM orders
GROUP BY customer
HAVING SUM(quantity * price) > 100;
-- Alice|174.92
-- Carol|199.86

Multiple GROUP BY Columns

-- Quantity sold per region and product
SELECT region, product, SUM(quantity) AS total_quantity
FROM orders
GROUP BY region, product;
-- East|Gadget|1
-- East|Gizmo|2
-- East|Widget|15
-- South|Widget|7
-- West|Gadget|6
-- West|Widget|3

GROUP BY with Expression

-- Classify products into price tiers and count orders per tier
SELECT CASE WHEN price < 20 THEN 'Budget' ELSE 'Premium' END AS tier,
       COUNT(*) AS order_count
FROM orders
GROUP BY CASE WHEN price < 20 THEN 'Budget' ELSE 'Premium' END;
-- Budget|4
-- Premium|4

Combining WHERE, GROUP BY, and HAVING

-- Products in the East region with total quantity above 3
SELECT product, SUM(quantity) AS total_quantity
FROM orders
WHERE region = 'East'
GROUP BY product
HAVING SUM(quantity) > 3;
-- Widget|15

Multiple Aggregate Functions

-- Summary statistics per product
SELECT product,
       AVG(quantity) AS avg_quantity,
       MIN(quantity) AS min_quantity,
       MAX(quantity) AS max_quantity
FROM orders
GROUP BY product;
-- Gadget|2.33333333333333|1|4
-- Gizmo|2.0|2|2
-- Widget|6.25|3|10

COUNT(DISTINCT) with GROUP BY

-- Count unique customers and products per region
SELECT region,
       COUNT(DISTINCT customer) AS unique_customers,
       COUNT(DISTINCT product) AS unique_products
FROM orders
GROUP BY region;
-- East|2|3
-- South|1|1
-- West|2|2

GROUP_CONCAT with GROUP BY

-- List distinct products purchased by each customer
SELECT customer, GROUP_CONCAT(DISTINCT product) AS products
FROM orders
GROUP BY customer;
-- Alice|Widget,Gadget,Gizmo
-- Bob|Gadget,Widget
-- Carol|Widget,Gadget
-- Dave|Widget

Aggregate Without GROUP BY

When aggregate functions appear without a GROUP BY clause, the entire table is treated as one group:

-- Overall totals across all orders
SELECT COUNT(*) AS total_orders, SUM(quantity) AS total_items
FROM orders;
-- 8|34

HAVING Without GROUP BY

-- Return total revenue only if it exceeds $500
SELECT SUM(quantity * price) AS revenue
FROM orders
HAVING SUM(quantity * price) > 500;
-- 524.66

NULL Grouping

CREATE TABLE survey (
  id INTEGER PRIMARY KEY,
  respondent TEXT,
  rating INTEGER
);
INSERT INTO survey VALUES (1, 'Alice', 5);
INSERT INTO survey VALUES (2, NULL, 3);
INSERT INTO survey VALUES (3, 'Bob', 4);
INSERT INTO survey VALUES (4, NULL, 2);
INSERT INTO survey VALUES (5, 'Alice', 3);

-- NULL respondents are grouped together
SELECT respondent, COUNT(*) AS responses, AVG(rating) AS avg_rating
FROM survey
GROUP BY respondent;
-- (NULL)|2|2.5
-- Alice|2|4.0
-- Bob|1|4.0

GROUP BY with Column Position

-- Group by the first column in the result set
SELECT customer, COUNT(*) AS order_count
FROM orders
GROUP BY 1;
-- Alice|3
-- Bob|2
-- Carol|2
-- Dave|1

ORDER BY, LIMIT, OFFSET

Syntax

SELECT result-column [, ...]
FROM table-or-subquery
[WHERE expr]
[GROUP BY expr [, ...]]
[ORDER BY ordering-term [, ...]]
[LIMIT expr [{OFFSET expr | , expr}]]

Where each ordering-term is:

expr [{ASC | DESC}] [COLLATE collation-name]

Description

The ORDER BY clause determines the order in which rows are returned by a SELECT statement. Without an ORDER BY clause, the order of rows in the result set is undefined – the database may return them in any order it chooses, and that order may differ between executions.

The LIMIT clause places an upper bound on the number of rows returned. The optional OFFSET clause (or the comma syntax) skips a number of rows before returning results. Together, ORDER BY, LIMIT, and OFFSET are the building blocks for sorted output, top-N queries, and pagination.

These clauses appear at the end of a SELECT statement, after any WHERE, GROUP BY, and HAVING clauses. In a compound SELECT (using UNION, INTERSECT, or EXCEPT), only the final SELECT may include ORDER BY and LIMIT, and they apply to the entire compound result.

Clauses

ORDER BY

The ORDER BY clause accepts one or more ordering terms, separated by commas. Each term is an expression that defines a sort key. Rows are sorted by the first term; ties are broken by the second term, and so on.

Each ordering term is evaluated against every row. The resulting values are compared to determine output order. By default, rows are sorted in ascending order (ASC), where smaller values come first.

Sort direction:

KeywordBehavior
ASCAscending order (default). Smaller values first.
DESCDescending order. Larger values first.

NULL handling:

Turso considers NULL values to be smaller than any other value for sorting purposes. This means:

  • In ascending order (ASC), NULLs appear at the beginning of the result set.
  • In descending order (DESC), NULLs appear at the end of the result set.

Expression resolution:

Each ORDER BY expression is resolved in the following order of precedence:

  1. Integer constant K: Treated as a reference to the K-th column of the result set, numbered from left to right starting at 1.
  2. Identifier matching a column alias: If the expression is a simple identifier that matches the alias of an output column, it refers to that column.
  3. Arbitrary expression: Otherwise, the expression is evaluated per row and the resulting value determines sort order. Any valid expression may be used, including function calls, arithmetic, and CASE expressions.

Collation:

Text values are compared using a collation sequence. The collation used for each ordering term is determined by this precedence:

  1. If the ordering term includes COLLATE collation-name, that collation is used.
  2. If the ordering term refers to a column (directly or via alias) that has a default collation, that collation is used.
  3. Otherwise, the BINARY collation is used.

The built-in collation sequences are:

CollationBehavior
BINARYByte-by-byte comparison. Uppercase letters sort before lowercase. Default.
NOCASECase-insensitive comparison for ASCII characters.
RTRIMLike BINARY, but trailing spaces are ignored.

LIMIT

The LIMIT clause restricts the number of rows returned. It takes a single expression that must evaluate to an integer (or a value that can be losslessly converted to an integer).

  • If the LIMIT value is non-negative, at most that many rows are returned.
  • If the LIMIT value is negative, there is no upper bound – all rows are returned.
  • If the LIMIT expression evaluates to NULL or to a value that cannot be losslessly converted to an integer, an error is returned.

LIMIT is most useful when combined with ORDER BY. Without ORDER BY, the set of rows returned by LIMIT is arbitrary and unpredictable.

OFFSET

The OFFSET clause skips a specified number of rows from the beginning of the result set before returning rows. The expression must evaluate to an integer (or a value that can be losslessly converted to an integer).

  • If the OFFSET value is zero or negative, no rows are skipped.
  • If the OFFSET value is NULL or cannot be losslessly converted to an integer, an error is returned.

When both LIMIT and OFFSET are specified, the first M rows are skipped (where M is the OFFSET value), and then the next N rows are returned (where N is the LIMIT value). If the result set contains fewer than M + N rows, all rows after the first M are returned.

Two syntax forms:

Turso supports two equivalent ways to write LIMIT with OFFSET:

FormSyntaxMeaning
Keyword formLIMIT N OFFSET MSkip M rows, return at most N rows
Comma formLIMIT M, NSkip M rows, return at most N rows

Note that the comma form reverses the order of the values: the first value is the offset and the second is the limit. This is a common source of confusion. The keyword form (LIMIT N OFFSET M) is recommended for clarity.

Examples

Sorting by a Single Column

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  price REAL,
  category TEXT
);
INSERT INTO products VALUES (1, 'Keyboard', 49.99, 'Electronics');
INSERT INTO products VALUES (2, 'Notebook', 5.99, 'Stationery');
INSERT INTO products VALUES (3, 'Monitor', 299.99, 'Electronics');
INSERT INTO products VALUES (4, 'Pen', 1.99, 'Stationery');
INSERT INTO products VALUES (5, 'Mouse', 29.99, 'Electronics');

-- Sort by price, cheapest first (ascending is the default)
SELECT name, price FROM products ORDER BY price;
-- Pen|1.99
-- Notebook|5.99
-- Mouse|29.99
-- Keyboard|49.99
-- Monitor|299.99

Descending Order

-- Sort by price, most expensive first
SELECT name, price FROM products ORDER BY price DESC;
-- Monitor|299.99
-- Keyboard|49.99
-- Mouse|29.99
-- Notebook|5.99
-- Pen|1.99

Sorting by Multiple Columns

-- Sort by category ascending, then by price descending within each category
SELECT name, category, price FROM products ORDER BY category ASC, price DESC;
-- Monitor|Electronics|299.99
-- Keyboard|Electronics|49.99
-- Mouse|Electronics|29.99
-- Notebook|Stationery|5.99
-- Pen|Stationery|1.99

Ordering by Column Number

-- Order by the second column in the result set (price)
SELECT name, price FROM products ORDER BY 2;
-- Pen|1.99
-- Notebook|5.99
-- Mouse|29.99
-- Keyboard|49.99
-- Monitor|299.99

Ordering by Alias

-- Order by a computed column alias
SELECT name, price * 1.1 AS price_with_tax
FROM products ORDER BY price_with_tax LIMIT 3;
-- Pen|2.189
-- Notebook|6.589
-- Mouse|32.989

Ordering by Expression

-- Sort by name length (descending), then alphabetically for ties
SELECT name, length(name) AS name_len
FROM products ORDER BY length(name) DESC, name ASC LIMIT 3;
-- Keyboard|8
-- Notebook|8
-- Monitor|7

Collation in ORDER BY

CREATE TABLE words (word TEXT);
INSERT INTO words VALUES ('banana');
INSERT INTO words VALUES ('Apple');
INSERT INTO words VALUES ('cherry');
INSERT INTO words VALUES ('Blueberry');

-- Default BINARY collation: uppercase letters sort before lowercase
SELECT word FROM words ORDER BY word;
-- Apple
-- Blueberry
-- banana
-- cherry

-- NOCASE collation: case-insensitive sorting
SELECT word FROM words ORDER BY word COLLATE NOCASE;
-- Apple
-- banana
-- Blueberry
-- cherry

NULL Ordering

CREATE TABLE scores (student TEXT, score INTEGER);
INSERT INTO scores VALUES ('Alice', 90);
INSERT INTO scores VALUES ('Bob', NULL);
INSERT INTO scores VALUES ('Carol', 85);
INSERT INTO scores VALUES ('Dave', NULL);

-- Ascending: NULLs appear first (NULLs are considered smaller than all other values)
SELECT student, score FROM scores ORDER BY score ASC;
-- Bob|
-- Dave|
-- Carol|85
-- Alice|90

-- Descending: NULLs appear last
SELECT student, score FROM scores ORDER BY score DESC;
-- Alice|90
-- Carol|85
-- Bob|
-- Dave|

Basic LIMIT

-- Return the 3 cheapest products
SELECT name, price FROM products ORDER BY price LIMIT 3;
-- Pen|1.99
-- Notebook|5.99
-- Mouse|29.99

LIMIT with OFFSET (Keyword Form)

-- Skip the 2 cheapest products, return the next 2
SELECT name, price FROM products ORDER BY price LIMIT 2 OFFSET 2;
-- Mouse|29.99
-- Keyboard|49.99

LIMIT with OFFSET (Comma Form)

-- Same result as above: LIMIT offset, count
SELECT name, price FROM products ORDER BY price LIMIT 2, 2;
-- Mouse|29.99
-- Keyboard|49.99

Negative LIMIT

-- A negative LIMIT returns all rows (no upper bound)
SELECT name FROM products ORDER BY name LIMIT -1;
-- Keyboard
-- Monitor
-- Mouse
-- Notebook
-- Pen

Pagination Pattern

CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT,
  department TEXT,
  salary REAL
);
INSERT INTO employees VALUES (1, 'Alice', 'Engineering', 95000);
INSERT INTO employees VALUES (2, 'Bob', 'Marketing', 72000);
INSERT INTO employees VALUES (3, 'Carol', 'Engineering', 98000);
INSERT INTO employees VALUES (4, 'Dave', 'Sales', 68000);
INSERT INTO employees VALUES (5, 'Eve', 'Marketing', 75000);
INSERT INTO employees VALUES (6, 'Frank', 'Engineering', 102000);
INSERT INTO employees VALUES (7, 'Grace', 'Sales', 71000);
INSERT INTO employees VALUES (8, 'Heidi', 'Engineering', 91000);

-- Page 1 (first 3 employees by salary descending)
SELECT name, salary FROM employees ORDER BY salary DESC LIMIT 3 OFFSET 0;
-- Frank|102000.0
-- Carol|98000.0
-- Alice|95000.0

-- Page 2 (next 3)
SELECT name, salary FROM employees ORDER BY salary DESC LIMIT 3 OFFSET 3;
-- Heidi|91000.0
-- Eve|75000.0
-- Bob|72000.0

-- Page 3 (remaining)
SELECT name, salary FROM employees ORDER BY salary DESC LIMIT 3 OFFSET 6;
-- Grace|71000.0
-- Dave|68000.0

Compatibility

  • NULLS FIRST and NULLS LAST are not yet fully supported. NULLS LAST returns a parse error. NULLS FIRST is accepted by the parser but does not change the sort behavior – NULLs always sort as the smallest values regardless. This means there is currently no way to override the default NULL placement in sort results.

Set Operations

Syntax

select-statement {UNION | UNION ALL | INTERSECT | EXCEPT} select-statement

A compound SELECT chains two or more simple SELECT statements with a set operator. Multiple operators can be chained:

select-statement op select-statement [op select-statement ...]

An optional LIMIT clause may appear after the last SELECT:

select-statement op select-statement [LIMIT expr [OFFSET expr]]

Description

Set operations combine the results of two or more SELECT statements into a single result set. Each constituent SELECT must return the same number of columns. Columns are matched by position (left to right), not by name. The column names in the final result are taken from the leftmost SELECT statement.

Turso supports four set operators:

OperatorDuplicatesDescription
UNION ALLKeeps allReturns every row from both queries, including duplicates.
UNIONRemovedReturns all rows from both queries, removing duplicate rows.
INTERSECTRemovedReturns only rows that appear in both queries.
EXCEPTRemovedReturns rows from the left query that do not appear in the right query.

UNION, INTERSECT, and EXCEPT all remove duplicate rows from the final result. UNION ALL is the only operator that preserves duplicates.

Clauses

UNION ALL

UNION ALL returns all rows from the left SELECT followed by all rows from the right SELECT. No duplicate detection or removal is performed, making UNION ALL the most efficient set operator.

-- Combine all employees and contractors, keeping duplicates
SELECT name, department FROM employees
UNION ALL
SELECT name, department FROM contractors;

If a person appears in both tables with the same name and department, that combination appears twice in the result.

UNION

UNION works the same way as UNION ALL but removes duplicate rows from the combined result. Two rows are considered duplicates when every corresponding column value is equal.

-- Combine employees and contractors, removing duplicates
SELECT name, department FROM employees
UNION
SELECT name, department FROM contractors;

INTERSECT

INTERSECT returns only rows that appear in both the left and right result sets. The output contains no duplicates.

-- Find people who appear in both tables with the same department
SELECT name, department FROM employees
INTERSECT
SELECT name, department FROM contractors;

EXCEPT

EXCEPT returns rows from the left query that are not present in the right query. The output contains no duplicates. The order of the two queries matters: A EXCEPT B is different from B EXCEPT A.

-- Find employees who are not also contractors (by name and department)
SELECT name, department FROM employees
EXCEPT
SELECT name, department FROM contractors;

LIMIT

A LIMIT clause may appear after the final SELECT in a compound statement. The limit applies to the entire combined result, not just the last SELECT.

-- Get the first 4 rows from the combined result
SELECT name, department FROM employees
UNION ALL
SELECT name, department FROM contractors
LIMIT 4;

OFFSET is also supported:

-- Skip the first row and return the next 3
SELECT name, department FROM employees
UNION ALL
SELECT name, department FROM contractors
LIMIT 3 OFFSET 1;

Column Matching Rules

All SELECT statements in a compound query must produce the same number of result columns. If they do not, Turso returns an error:

-- Error: different number of columns
SELECT name, department FROM employees
UNION ALL
SELECT name FROM contractors;
-- SELECTs to the left and right of UNION ALL do not have the same number of result columns

Columns are matched by position. The first column of the left SELECT pairs with the first column of the right SELECT, and so on. Column names and types do not need to match – only the count must be equal.

The result column names are always determined by the leftmost SELECT:

SELECT 1 AS first_col, 2 AS second_col
UNION ALL
SELECT 3, 4;
-- Column headers are "first_col" and "second_col"

Duplicate Detection

For the purpose of identifying duplicate rows in UNION, INTERSECT, and EXCEPT:

  • NULL values are considered equal to each other. Two rows that both contain NULL in the same column position are treated as matching in that column.
  • No type affinity transformations are applied when comparing rows. Values are compared as-is.
  • Text comparisons use the collation sequence that would apply if the columns from the left and right SELECTs were operands of the = operator.
-- NULL is treated as equal to NULL for dedup purposes
SELECT NULL UNION SELECT NULL;
-- Returns one row (a single NULL)

Chaining Multiple Operators

Three or more SELECT statements can be connected with set operators. When chained, they group from left to right. That is, A op1 B op2 C is evaluated as (A op1 B) op2 C.

-- Three-way UNION ALL: employees, contractors, and interns
SELECT name, department FROM employees
UNION ALL
SELECT name, department FROM contractors
UNION ALL
SELECT name, department FROM interns;

Different operators can be mixed in the same compound statement:

-- First combine and deduplicate, then append without dedup
SELECT name, department FROM employees
UNION
SELECT name, department FROM contractors
UNION ALL
SELECT name, department FROM interns;

Using Set Operations in Subqueries

A compound SELECT can be used as a subquery in the FROM clause. This is useful for aggregating or filtering the combined result:

-- Count headcount per department across all worker types
SELECT department, COUNT(*) AS headcount
FROM (
    SELECT name, department FROM employees
    UNION ALL
    SELECT name, department FROM contractors
)
GROUP BY department;
-- Filter the combined result
SELECT *
FROM (
    SELECT name, department FROM employees
    UNION ALL
    SELECT name, department FROM contractors
)
WHERE department = 'Engineering';

Examples

The examples below use the following tables:

CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT,
  department TEXT,
  salary REAL
);
INSERT INTO employees VALUES
  (1, 'Alice', 'Engineering', 95000),
  (2, 'Bob', 'Engineering', 88000),
  (3, 'Carol', 'Marketing', 72000),
  (4, 'Dave', 'Marketing', 68000),
  (5, 'Eve', 'Sales', 75000);

CREATE TABLE contractors (
  id INTEGER PRIMARY KEY,
  name TEXT,
  department TEXT,
  rate REAL
);
INSERT INTO contractors VALUES
  (1, 'Frank', 'Engineering', 110000),
  (2, 'Grace', 'Marketing', 65000),
  (3, 'Alice', 'Engineering', 95000);

UNION ALL – All People Including Duplicates

SELECT name, department FROM employees
UNION ALL
SELECT name, department FROM contractors;
-- Alice|Engineering
-- Bob|Engineering
-- Carol|Marketing
-- Dave|Marketing
-- Eve|Sales
-- Frank|Engineering
-- Grace|Marketing
-- Alice|Engineering

Note that “Alice | Engineering” appears twice because UNION ALL does not remove duplicates.

UNION – All People Without Duplicates

SELECT name, department FROM employees
UNION
SELECT name, department FROM contractors;
-- Alice|Engineering
-- Bob|Engineering
-- Carol|Marketing
-- Dave|Marketing
-- Eve|Sales
-- Frank|Engineering
-- Grace|Marketing

The duplicate “Alice | Engineering” row has been removed. The result has 7 rows instead of 8.

INTERSECT – People in Both Tables

SELECT name, department FROM employees
INTERSECT
SELECT name, department FROM contractors;
-- Alice|Engineering

Only “Alice | Engineering” appears in both tables.

EXCEPT – Employees Who Are Not Contractors

SELECT name, department FROM employees
EXCEPT
SELECT name, department FROM contractors;
-- Bob|Engineering
-- Carol|Marketing
-- Dave|Marketing
-- Eve|Sales

Alice is excluded because she appears in both tables with the same name and department.

Finding Common Departments

SELECT department FROM employees
INTERSECT
SELECT department FROM contractors;
-- Engineering
-- Marketing

Finding Departments Unique to Employees

SELECT department FROM employees
EXCEPT
SELECT department FROM contractors;
-- Sales

Sales exists only in the employees table.

Aggregating a Combined Result

SELECT department, COUNT(*) AS headcount
FROM (
    SELECT name, department FROM employees
    UNION ALL
    SELECT name, department FROM contractors
)
GROUP BY department;
-- Engineering|4
-- Marketing|3
-- Sales|1

Note that UNION ALL is used here so that each person is counted, even if they appear in both tables.

Limiting the Combined Result

SELECT name, department FROM employees
UNION ALL
SELECT name, department FROM contractors
LIMIT 4;
-- Alice|Engineering
-- Bob|Engineering
-- Carol|Marketing
-- Dave|Marketing

Using VALUES with Set Operations

VALUES (1, 'Alice'), (2, 'Bob')
UNION ALL
VALUES (3, 'Carol');
-- 1|Alice
-- 2|Bob
-- 3|Carol

Compatibility

FeatureStatus
ORDER BY on compound SELECTNot yet supported. Turso does not currently allow ORDER BY on the final result of a compound SELECT. As a workaround, wrap the compound SELECT in a subquery and apply ORDER BY to the outer query.

Subqueries

Syntax

A subquery is a SELECT statement enclosed in parentheses, used as an expression or table source within another SQL statement.

-- Scalar subquery (returns a single value)
(SELECT expr FROM table-name [WHERE ...])

-- EXISTS / NOT EXISTS
[NOT] EXISTS (SELECT ... FROM table-name [WHERE ...])

-- IN / NOT IN with subquery
expr [NOT] IN (SELECT expr FROM table-name [WHERE ...])

-- Derived table (subquery in FROM clause)
SELECT ... FROM (SELECT ... FROM table-name) AS alias

Description

Subqueries allow you to nest one query inside another. They appear in several forms depending on context: as a single value in an expression (scalar subquery), as a set membership test (IN), as an existence check (EXISTS), or as a virtual table in the FROM clause (derived table).

A subquery can reference columns from its enclosing query. When it does, it is called a correlated subquery and is re-evaluated for each row of the outer query. When a subquery does not reference any outer columns, it is an uncorrelated subquery and may be evaluated once and its result reused.

Scalar Subqueries

A scalar subquery is a SELECT enclosed in parentheses that returns a single column. It can appear anywhere an expression is allowed: in the SELECT list, WHERE clause, HAVING clause, or even LIMIT and OFFSET.

The value of a scalar subquery is the value from the first row returned. If the subquery returns no rows, the result is NULL. If the subquery returns more than one column, Turso raises an error.

-- Scalar subquery in SELECT list
SELECT expr, (SELECT agg_func(...) FROM table-name) AS alias FROM table-name

-- Scalar subquery in WHERE clause
SELECT ... FROM table-name WHERE column > (SELECT agg_func(...) FROM table-name)

EXISTS and NOT EXISTS

The EXISTS operator takes a subquery and evaluates to 1 (true) if the subquery returns at least one row, or 0 (false) if the subquery returns no rows. NOT EXISTS returns the opposite.

The number of columns returned by the subquery and their values do not matter. Only the presence or absence of rows is significant. By convention, SELECT 1 is often used inside EXISTS subqueries.

-- Returns rows from outer query where the subquery matches at least one row
SELECT ... FROM table-name t
WHERE EXISTS (SELECT 1 FROM other-table o WHERE o.ref_id = t.id)

-- Returns rows where no matching row exists
SELECT ... FROM table-name t
WHERE NOT EXISTS (SELECT 1 FROM other-table o WHERE o.ref_id = t.id)

EXISTS is commonly used with correlated subqueries to test whether related rows exist in another table.

IN and NOT IN with Subqueries

The IN operator tests whether a value is a member of the set of values returned by a subquery. The subquery must return a single column. NOT IN tests the inverse.

expr [NOT] IN (SELECT column FROM table-name [WHERE ...])

NULL Handling

The behavior of IN and NOT IN with NULL values follows specific rules:

Left ValueSubquery Contains NULLValue FoundIN ResultNOT IN Result
non-NULLnono0 (false)1 (true)
non-NULLnoyes1 (true)0 (false)
non-NULLyesnoNULLNULL
non-NULLyesyes1 (true)0 (false)
NULLany(empty set)0 (false)1 (true)
NULLany(non-empty)NULLNULL

Key takeaways:

  • When the subquery returns an empty set, IN always returns 0 and NOT IN always returns 1, regardless of NULL values.
  • When the left value is found in the set, IN returns 1 even if the set also contains NULL.
  • When the left value is not found and the set contains NULL, the result is NULL (unknown), not 0. This is because the value might match the unknown (NULL) element.

Correlated Subqueries

A correlated subquery references one or more columns from the outer query. Turso re-evaluates a correlated subquery for each row processed by the outer query.

-- The inner query references e.department_id from the outer query
SELECT ... FROM employees e
WHERE e.salary > (
    SELECT AVG(e2.salary) FROM employees e2
    WHERE e2.department_id = e.department_id
)

Correlated subqueries are supported in the SELECT list, WHERE clause, HAVING clause, and GROUP BY clause. Correlated subqueries can also be used with EXISTS and IN.

Note: Correlated subqueries in the ORDER BY clause are not currently supported.

Derived Tables

A subquery in the FROM clause creates a derived table (also called an inline view). The subquery result is treated as a temporary table for the duration of the outer query.

A derived table must be given an alias using the AS keyword.

SELECT alias.column [, ...]
FROM (SELECT ... FROM table-name [WHERE ...] [GROUP BY ...]) AS alias
[JOIN other-table ON ...]
[WHERE ...]

Each column of the derived table inherits the type affinity and collation sequence of the corresponding expression in the subquery.

Examples

Scalar Subquery in SELECT List

CREATE TABLE employees (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    department_id INTEGER,
    salary REAL
);
INSERT INTO employees VALUES
    (1, 'Alice', 1, 95000), (2, 'Bob', 1, 88000),
    (3, 'Carol', 2, 72000), (4, 'Dave', 3, 68000),
    (5, 'Eve', 1, 105000), (6, 'Frank', 2, 71000);

-- Show each employee's salary alongside the company average
SELECT name, salary,
       (SELECT AVG(salary) FROM employees) AS avg_salary
FROM employees
ORDER BY salary DESC;
-- Eve   | 105000.0 | 83166.6666666667
-- Alice |  95000.0 | 83166.6666666667
-- Bob   |  88000.0 | 83166.6666666667
-- Carol |  72000.0 | 83166.6666666667
-- Frank |  71000.0 | 83166.6666666667
-- Dave  |  68000.0 | 83166.6666666667

Scalar Subquery in WHERE Clause

-- Find employees earning above the company average
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
-- Alice |  95000.0
-- Bob   |  88000.0
-- Eve   | 105000.0

Scalar Subquery Returning NULL

When a scalar subquery matches no rows, it returns NULL:

SELECT (SELECT name FROM employees WHERE id = 999) AS result;
-- (NULL)

EXISTS: Find Departments with Employees

CREATE TABLE departments (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL
);
INSERT INTO departments VALUES
    (1, 'Engineering'), (2, 'Marketing'),
    (3, 'Sales'), (4, 'HR');

-- Departments that have at least one employee
SELECT d.name
FROM departments d
WHERE EXISTS (
    SELECT 1 FROM employees e WHERE e.department_id = d.id
);
-- Engineering
-- Marketing
-- Sales

NOT EXISTS: Find Departments without Employees

-- Departments with no employees assigned
SELECT d.name
FROM departments d
WHERE NOT EXISTS (
    SELECT 1 FROM employees e WHERE e.department_id = d.id
);
-- HR

IN with Subquery

-- Employees in the Engineering department
SELECT name
FROM employees
WHERE department_id IN (
    SELECT id FROM departments WHERE name = 'Engineering'
);
-- Alice
-- Bob
-- Eve

NOT IN with Subquery

-- Employees outside the Engineering department
SELECT name
FROM employees
WHERE department_id NOT IN (
    SELECT id FROM departments WHERE name = 'Engineering'
);
-- Carol
-- Dave
-- Frank
-- Show each employee with their department name
SELECT e.name, e.salary,
       (SELECT d.name FROM departments d WHERE d.id = e.department_id) AS dept_name
FROM employees e
ORDER BY e.salary DESC;
-- Eve   | 105000.0 | Engineering
-- Alice |  95000.0 | Engineering
-- Bob   |  88000.0 | Engineering
-- Carol |  72000.0 | Marketing
-- Frank |  71000.0 | Marketing
-- Dave  |  68000.0 | Sales

Correlated Subquery: Compare Against Group Average

-- Employees earning above their department's average salary
SELECT e.name, e.salary, e.department_id
FROM employees e
WHERE e.salary > (
    SELECT AVG(e2.salary)
    FROM employees e2
    WHERE e2.department_id = e.department_id
);
-- Carol | 72000.0 | 2
-- Eve   | 105000.0 | 1

Correlated Subquery with COUNT

-- Count employees per department
SELECT d.name,
       (SELECT COUNT(*) FROM employees e WHERE e.department_id = d.id) AS emp_count
FROM departments d;
-- Engineering | 3
-- Marketing   | 2
-- Sales       | 1
-- HR          | 0

Derived Table

-- Average salary per department using a derived table
SELECT dept_name, avg_salary
FROM (
    SELECT d.name AS dept_name, AVG(e.salary) AS avg_salary
    FROM departments d
    JOIN employees e ON d.id = e.department_id
    GROUP BY d.name
) AS dept_stats
ORDER BY avg_salary DESC;
-- Engineering | 96000.0
-- Marketing   | 71500.0
-- Sales       | 68000.0

Derived Table Joined with Another Table

-- Join a derived table of aggregate stats back to the departments table
SELECT c.name AS department, stats.emp_count, stats.avg_salary
FROM departments c
JOIN (
    SELECT department_id,
           COUNT(*) AS emp_count,
           AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department_id
) AS stats ON stats.department_id = c.id;
-- Engineering | 3 | 96000.0
-- Marketing   | 2 | 71500.0
-- Sales       | 1 | 68000.0

Combining EXISTS and IN

CREATE TABLE products (
    id INTEGER PRIMARY KEY, name TEXT,
    category_id INTEGER, price REAL
);
CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE reviews (id INTEGER PRIMARY KEY, product_id INTEGER, rating INTEGER);

INSERT INTO categories VALUES (1, 'Electronics'), (2, 'Clothing');
INSERT INTO products VALUES
    (1, 'Laptop', 1, 999.99),
    (2, 'T-Shirt', 2, 29.99),
    (3, 'Headphones', 1, 79.99);
INSERT INTO reviews VALUES (1, 1, 5), (2, 1, 4), (3, 3, 3);

-- Electronics with at least one review rated 4 or higher
SELECT p.name, p.price
FROM products p
WHERE EXISTS (
    SELECT 1 FROM reviews r
    WHERE r.product_id = p.id AND r.rating >= 4
)
AND p.category_id IN (
    SELECT id FROM categories WHERE name = 'Electronics'
);
-- Laptop | 999.99

Compatibility

Turso supports scalar subqueries used with comparison operators (e.g. WHERE x > (SELECT ...)), but does not support row value subqueries such as (x, y) = (SELECT a, b FROM ...). Only single-column subqueries are valid in comparison contexts.

Correlated subqueries in the ORDER BY clause are not yet supported. Uncorrelated subqueries in ORDER BY work as expected.

Common Table Expressions

Syntax

WITH cte-name [(column-name [, ...])] AS (select-stmt)
  [, cte-name [(column-name [, ...])] AS (select-stmt) [, ...]]
{SELECT | INSERT | UPDATE | DELETE} ...

Description

A Common Table Expression (CTE) is a named temporary result set defined within a WITH clause. CTEs exist only for the duration of the statement they are attached to. They behave like temporary views: you define them once and can reference them by name in the main statement that follows.

CTEs make complex queries easier to read by breaking them into named, reusable pieces. Instead of deeply nested subqueries, you can define each logical step as a separate CTE and compose them together.

A single WITH clause can define multiple CTEs, separated by commas. Later CTEs in the list can reference earlier ones, allowing you to build up results incrementally.

Clauses

WITH

The WITH keyword introduces one or more CTE definitions. It must appear at the beginning of a top-level SELECT, INSERT, UPDATE, or DELETE statement.

WITH cte-name AS (select-stmt)

Each CTE definition consists of:

  • cte-name – the name used to reference the CTE in the main statement. CTE names must be unique within a single WITH clause; duplicate names produce an error.
  • select-stmt – a SELECT statement (including compound SELECT with UNION, UNION ALL, INTERSECT, or EXCEPT) that defines the CTE’s contents.

Column Names

You can optionally specify explicit column names for the CTE by listing them in parentheses after the CTE name:

WITH cte-name (column-name [, ...]) AS (select-stmt)

When column names are provided, they replace whatever column names the select-stmt would otherwise produce. This is useful when the CTE body contains expressions without natural names, or when you want to rename columns for clarity.

Multiple CTEs

Multiple CTEs are separated by commas within a single WITH clause. Each subsequent CTE can reference any CTE defined before it:

WITH
  first AS (select-stmt),
  second AS (select-stmt),   -- can reference 'first'
  third AS (select-stmt)     -- can reference 'first' and 'second'
SELECT ... FROM third;

CTE Body with Compound SELECT

The select-stmt inside a CTE definition can be a compound SELECT using set operators:

OperatorBehavior
UNIONCombines results, removing duplicate rows
UNION ALLCombines results, keeping all rows including duplicates
INTERSECTReturns only rows present in both result sets
EXCEPTReturns rows from the first result set that are not in the second

The compound SELECT can also include LIMIT and OFFSET to restrict the CTE’s result set.

Using CTEs with Different Statements

WITH … SELECT

The most common use of CTEs. The main SELECT can reference any defined CTE by name in its FROM clause, in subqueries, or in WHERE clause conditions. A CTE can be referenced multiple times within the same statement.

WITH cte AS (select-stmt)
SELECT ... FROM cte;

WITH … INSERT

A WITH clause can precede an INSERT statement. The CTEs are visible in the INSERT ... SELECT source query, in scalar subqueries within a VALUES clause, and in RETURNING clause subqueries.

WITH cte AS (select-stmt)
INSERT INTO table-name SELECT ... FROM cte;

WITH … UPDATE

A WITH clause can precede an UPDATE statement. The CTEs are visible in the WHERE clause, SET expressions, and RETURNING clause of the UPDATE.

WITH cte AS (select-stmt)
UPDATE table-name SET ... WHERE ... IN (SELECT ... FROM cte);

WITH … DELETE

A WITH clause can precede a DELETE statement. The CTEs are visible in the WHERE clause and RETURNING clause of the DELETE.

WITH cte AS (select-stmt)
DELETE FROM table-name WHERE ... IN (SELECT ... FROM cte);

Examples

Basic CTE

-- Define a simple CTE and select from it
WITH recent_cutoff AS (SELECT 30 AS days)
SELECT days FROM recent_cutoff;
-- 30

CTE with Explicit Column Names

-- Rename the CTE columns to 'sum' and 'product'
WITH calculations(sum, product) AS (SELECT 3 + 4, 3 * 4)
SELECT sum, product FROM calculations;
-- 7|12

Multiple CTEs

-- Chain CTEs: the second references the first
WITH
  base AS (SELECT 10 AS val),
  doubled AS (SELECT val * 2 AS val FROM base)
SELECT * FROM doubled;
-- 20

Long CTE Chain

-- Build a pipeline of CTEs, each referencing the previous one
WITH
  step1 AS (SELECT 1 AS x),
  step2 AS (SELECT x FROM step1),
  step3 AS (SELECT x FROM step2),
  step4 AS (SELECT x FROM step3)
SELECT * FROM step4;
-- 1

CTE with UNION

-- Combine two result sets, removing duplicates
WITH statuses AS (
  SELECT 'active' AS status
  UNION
  SELECT 'inactive'
  UNION
  SELECT 'pending'
)
SELECT * FROM statuses ORDER BY 1;
-- active
-- inactive
-- pending

CTE with UNION ALL and Aggregation

-- UNION ALL preserves all rows, enabling accurate counts
WITH all_scores AS (
  SELECT 1 AS score
  UNION ALL
  SELECT 2
  UNION ALL
  SELECT 3
)
SELECT COUNT(*) FROM all_scores;
-- 3

CTE with LIMIT and OFFSET

-- Restrict the CTE result set using LIMIT and OFFSET
WITH numbers AS (
  SELECT 1 UNION SELECT 2 UNION SELECT 3
  LIMIT 2 OFFSET 1
)
SELECT * FROM numbers ORDER BY 1;
-- 2
-- 3

CTE Referenced Multiple Times

-- Reference the same CTE twice to form a cross join
WITH codes AS (SELECT 1 AS x UNION SELECT 2)
SELECT * FROM codes AS a, codes AS b ORDER BY 1, 2;
-- 1|1
-- 1|2
-- 2|1
-- 2|2

CTE Visible in Scalar Subqueries

-- A CTE can be referenced inside scalar subqueries in the SELECT list
WITH constants AS (SELECT 10 AS x, 20 AS y)
SELECT (SELECT x FROM constants), (SELECT y FROM constants);
-- 10|20

CTE with INSERT

-- Use a CTE to supply rows for an INSERT ... SELECT
CREATE TABLE orders(amount);
WITH new_orders AS (
  SELECT 100 UNION SELECT 250 UNION SELECT 75
)
INSERT INTO orders SELECT * FROM new_orders;

SELECT * FROM orders ORDER BY 1;
-- 75
-- 100
-- 250

CTE with INSERT and RETURNING

-- CTEs work with the RETURNING clause
CREATE TABLE items(name TEXT);
WITH new_items AS (SELECT 'widget' AS name)
INSERT INTO items SELECT * FROM new_items RETURNING name;
-- widget

CTE with INSERT VALUES Subquery

-- Reference a CTE inside a VALUES clause via scalar subquery
CREATE TABLE settings(key TEXT, value INT);
WITH defaults AS (SELECT 99 AS x)
INSERT INTO settings VALUES ('threshold', (SELECT x FROM defaults));

SELECT * FROM settings;
-- threshold|99

CTE with UPDATE

-- Use a CTE to identify rows to update
CREATE TABLE products(id INTEGER, price REAL);
INSERT INTO products VALUES (1, 10.0), (2, 20.0), (3, 30.0);

WITH expensive AS (SELECT id FROM products WHERE price > 15.0)
UPDATE products SET price = price * 0.9
WHERE id IN (SELECT id FROM expensive);

SELECT * FROM products ORDER BY id;
-- 1|10.0
-- 2|18.0
-- 3|27.0

CTE with DELETE

-- Use a CTE to identify rows to delete
CREATE TABLE tasks(id INTEGER, status TEXT);
INSERT INTO tasks VALUES (1, 'done'), (2, 'pending'), (3, 'done');

WITH completed AS (SELECT id FROM tasks WHERE status = 'done')
DELETE FROM tasks WHERE id IN (SELECT id FROM completed);

SELECT * FROM tasks;
-- 2|pending

CTE with Multiple CTEs in DELETE

-- Combine multiple CTEs to build complex conditions
CREATE TABLE inventory(id INTEGER);
INSERT INTO inventory VALUES (1),(2),(3),(4),(5);

WITH
  low AS (SELECT 1 UNION SELECT 2),
  high AS (SELECT 4 UNION SELECT 5)
DELETE FROM inventory
WHERE id IN (SELECT * FROM low) OR id IN (SELECT * FROM high);

SELECT * FROM inventory;
-- 3

CTE Feeding Another CTE for INSERT

-- Chain CTEs: the first provides base data, the second transforms it
CREATE TABLE results(value INTEGER);
WITH
  base AS (SELECT 1 AS x),
  transformed AS (SELECT x + 10 FROM base)
INSERT INTO results SELECT * FROM transformed;

SELECT * FROM results;
-- 11

CTE Visible in RETURNING Subqueries

-- Reference a CTE inside the RETURNING clause
CREATE TABLE logs(entry TEXT);
INSERT INTO logs VALUES ('old_entry');

WITH marker AS (SELECT 99 AS code)
DELETE FROM logs WHERE entry = 'old_entry'
RETURNING (SELECT code FROM marker);
-- 99

Compatibility

Turso supports the WITH clause with the following limitations compared to SQLite:

FeatureStatus
Ordinary CTEsSupported
Multiple CTEs in one WITH clauseSupported
CTE with SELECT, INSERT, UPDATE, DELETESupported
Explicit CTE column namesSupported
Compound SELECT in CTE body (UNION, UNION ALL, INTERSECT, EXCEPT)Supported
RECURSIVE CTEsNot supported
MATERIALIZED / NOT MATERIALIZED hintsNot supported
Non-SELECT statements in CTE body (INSERT, UPDATE, DELETE inside the CTE definition)Not supported

Only SELECT statements (including compound SELECT) are allowed in the CTE body. The main statement that follows the WITH clause can be SELECT, INSERT, UPDATE, or DELETE.

INSERT

Syntax

{INSERT | REPLACE} [OR {ROLLBACK | ABORT | FAIL | IGNORE | REPLACE}]
  INTO table-name [AS alias] [(column-name [, ...])]
  {VALUES (expr [, ...]) [, ...] | SELECT ... | DEFAULT VALUES}
  [RETURNING expr [AS alias] [, ...]]

Description

The INSERT statement creates new rows in an existing table. There are three forms: INSERT … VALUES inserts one or more explicitly specified rows, INSERT … SELECT inserts rows produced by a query, and INSERT … DEFAULT VALUES inserts a single row where every column takes its default value.

The keyword REPLACE is shorthand for INSERT OR REPLACE. When a constraint violation occurs, REPLACE deletes the conflicting row and inserts the new one.

Clauses

Column List

An optional parenthesized list of column names may appear after the table name. When provided, the number of values in each VALUES row (or columns in the SELECT result) must match the number of listed columns. Columns not named in the list receive their default value, or NULL if no default is defined.

When the column list is omitted, the number of values must match the total number of columns in the table, and values are assigned left-to-right.

-- With column list: unlisted columns get defaults
INSERT INTO products (name, price) VALUES ('Widget', 9.99);

-- Without column list: must supply every column
INSERT INTO products VALUES (1, 'Widget', 9.99, 1);

VALUES

The VALUES clause provides one or more rows of literal expressions. Multiple rows are separated by commas.

-- Single row
INSERT INTO products (name, price) VALUES ('Widget', 9.99);

-- Multiple rows
INSERT INTO products (name, price)
  VALUES ('Widget', 9.99), ('Gadget', 24.95), ('Gizmo', 4.50);

SELECT

A SELECT statement may be used instead of VALUES to insert rows produced by a query. Any valid SELECT is allowed, including compound SELECTs (UNION, UNION ALL, INTERSECT, EXCEPT) and SELECTs with ORDER BY or LIMIT.

If a column list is specified, the number of columns in the SELECT result must match the number of listed columns. Otherwise it must match the total number of columns in the target table.

CREATE TABLE products_archive (id INTEGER PRIMARY KEY, name TEXT, price REAL);
INSERT INTO products_archive SELECT id, name, price FROM products WHERE in_stock = 0;

DEFAULT VALUES

The DEFAULT VALUES form inserts exactly one row. Every column receives its default value as specified in the CREATE TABLE statement, or NULL if no default was defined.

CREATE TABLE logs (id INTEGER PRIMARY KEY, created_at TEXT DEFAULT 'now', msg TEXT DEFAULT 'empty');
INSERT INTO logs DEFAULT VALUES;
-- Result: id=1, created_at='now', msg='empty'

OR Conflict Algorithm

By prefixing the INSERT with OR algorithm, you can control what happens when the insertion would violate a constraint (UNIQUE, NOT NULL, CHECK, or PRIMARY KEY). The keyword appears between INSERT and INTO.

AlgorithmBehavior
ABORTAbort the current statement and roll back any changes it made. This is the default behavior when no algorithm is specified.
ROLLBACKAbort the current statement and roll back the entire enclosing transaction.
FAILAbort the current statement but keep changes made by earlier rows within the same statement.
IGNORESkip the row that caused the violation and continue processing remaining rows.
REPLACEDelete the existing row that caused the conflict, then insert the new row. If the conflicting column has a NOT NULL constraint with a DEFAULT value, the default is used when NULL is supplied. If there is no default, the statement fails.

Foreign key constraint violations are not affected by the conflict algorithm. They always behave like ABORT regardless of which algorithm is specified.

-- IGNORE: silently skip rows that violate constraints
CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);
INSERT INTO users VALUES (1, 'alice@example.com');
INSERT OR IGNORE INTO users VALUES (2, 'alice@example.com');
-- The second row is silently skipped; the table still contains only row 1.

-- REPLACE: delete the conflicting row and insert the new one
INSERT OR REPLACE INTO users VALUES (2, 'alice@example.com');
-- Row 1 is deleted, row 2 with the same email is inserted.

The REPLACE keyword (without INSERT OR) is equivalent to INSERT OR REPLACE:

REPLACE INTO users VALUES (3, 'alice@example.com');

RETURNING

The RETURNING clause causes the INSERT statement to return values from each inserted row, much like a SELECT. It accepts a list of expressions that may reference columns of the inserted row, use functions, or contain arbitrary expressions. Use * to return all columns.

CREATE TABLE orders (id INTEGER PRIMARY KEY, product TEXT, qty INTEGER);
INSERT INTO orders (product, qty) VALUES ('Widget', 5) RETURNING *;
-- Returns: 1|Widget|5

INSERT INTO orders (product, qty)
  VALUES ('Gadget', 3), ('Gizmo', 12)
  RETURNING id, product;
-- Returns:
-- 2|Gadget
-- 3|Gizmo

RETURNING expressions can include functions and computed values:

CREATE TABLE line_items (id INTEGER PRIMARY KEY, product TEXT, qty INTEGER, unit_price REAL);
INSERT INTO line_items (product, qty, unit_price)
  VALUES ('Widget', 5, 9.99)
  RETURNING id, product, qty * unit_price AS total;
-- Returns: 1|Widget|49.95

For full details on the RETURNING clause, see RETURNING.

Table Alias

The optional AS alias after the table name provides an alternative name for the table. This alias is primarily useful with the UPSERT clause (ON CONFLICT … DO UPDATE), which is documented separately.

Examples

-- Create a sample table
CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  department TEXT,
  salary REAL DEFAULT 50000.00
);
-- Insert a single row, specifying all columns
INSERT INTO employees VALUES (1, 'Alice Johnson', 'Engineering', 95000.00);
-- Insert with a column list; salary gets its default value
INSERT INTO employees (id, name, department)
  VALUES (2, 'Bob Smith', 'Marketing');
-- Insert multiple rows at once
INSERT INTO employees (name, department, salary) VALUES
  ('Carol White', 'Engineering', 105000.00),
  ('David Brown', 'Sales', 72000.00),
  ('Eve Davis', 'Marketing', 68000.00);
-- Insert from a SELECT: copy all engineers into a new table
CREATE TABLE engineers (id INTEGER PRIMARY KEY, name TEXT, salary REAL);
INSERT INTO engineers
  SELECT id, name, salary FROM employees WHERE department = 'Engineering';
-- Insert from a compound SELECT
CREATE TABLE all_names (name TEXT);
INSERT INTO all_names
  SELECT name FROM employees
  UNION ALL
  SELECT name FROM engineers;
-- Insert a single row with all defaults
CREATE TABLE events (id INTEGER PRIMARY KEY, description TEXT DEFAULT 'unknown');
INSERT INTO events DEFAULT VALUES;
-- Result: id=1, description='unknown'
-- INSERT OR IGNORE: skip rows that would violate a UNIQUE constraint
CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);
INSERT INTO tags (label) VALUES ('urgent'), ('review'), ('done');
INSERT OR IGNORE INTO tags (label) VALUES ('urgent'), ('new'), ('done');
-- Only 'new' is inserted; 'urgent' and 'done' are skipped.
SELECT label FROM tags ORDER BY label;
-- done
-- new
-- review
-- urgent
-- INSERT OR REPLACE: replace the conflicting row
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT);
INSERT INTO settings VALUES ('theme', 'light');
INSERT OR REPLACE INTO settings VALUES ('theme', 'dark');
SELECT * FROM settings;
-- theme|dark
-- REPLACE shorthand (equivalent to INSERT OR REPLACE)
REPLACE INTO settings VALUES ('theme', 'solarized');
-- INSERT with RETURNING to get generated IDs
CREATE TABLE tickets (id INTEGER PRIMARY KEY, title TEXT, priority INTEGER);
INSERT INTO tickets (title, priority)
  VALUES ('Fix login bug', 1), ('Update docs', 3)
  RETURNING id, title;
-- 1|Fix login bug
-- 2|Update docs
-- INSERT ... SELECT with RETURNING
CREATE TABLE source (name TEXT, amount INTEGER);
INSERT INTO source VALUES ('Alice', 10), ('Bob', 20);
CREATE TABLE totals (name TEXT, amount INTEGER);
INSERT INTO totals SELECT * FROM source RETURNING *;
-- Alice|10
-- Bob|20
-- INSERT OR IGNORE with RETURNING: only inserted rows are returned
CREATE TABLE codes (id INTEGER PRIMARY KEY, code TEXT UNIQUE);
INSERT INTO codes (code) VALUES ('A');
INSERT OR IGNORE INTO codes (code) VALUES ('A'), ('B') RETURNING id, code;
-- 2|B
-- (The duplicate 'A' is skipped and does not appear in the output.)

UPSERT (ON CONFLICT)

Syntax

INSERT [OR {ROLLBACK | ABORT | FAIL | IGNORE | REPLACE}]
  INTO table-name [(column-name [, ...])]
  {VALUES (expr [, ...]) [, ...] | select-stmt}
  [ON CONFLICT [(column-name [, ...]) [WHERE expr]]
    {DO NOTHING | DO UPDATE SET assignment [, ...] [WHERE expr]}
  ] [, ...]

Where each assignment is:

column-name = expr

or:

(column-name [, ...]) = (expr [, ...])

Description

UPSERT is not a standalone statement. It is an optional clause that can be appended to an INSERT statement to control what happens when the insertion would violate a uniqueness constraint (a UNIQUE column, a UNIQUE index, or a PRIMARY KEY). Without an UPSERT clause, a uniqueness violation causes the statement to fail with an error.

An UPSERT clause begins with ON CONFLICT and specifies one of two actions: DO NOTHING, which silently skips the conflicting row, or DO UPDATE SET, which converts the insert into an update of the existing row. This gives you an atomic “insert or update” operation in a single statement, eliminating the need for separate existence checks.

Turso follows the PostgreSQL-style UPSERT syntax. UPSERT only applies to uniqueness constraints. It does not intercept NOT NULL, CHECK, or foreign key violations – those always produce an error regardless of any ON CONFLICT clause.

Clauses

Conflict Target

The conflict target appears between ON CONFLICT and DO. It specifies which uniqueness constraint should trigger the upsert behavior.

ON CONFLICT (column-name [, ...]) [WHERE expr] DO ...

The column list must exactly match the columns of a UNIQUE index or PRIMARY KEY. For a composite unique index, all columns must be listed, though the order does not matter.

-- Matches a UNIQUE index on (a, b), regardless of column order
ON CONFLICT (b, a) DO UPDATE SET ...

The conflict target is optional on the last (or only) ON CONFLICT clause. When omitted, the clause matches any uniqueness constraint violation that has not already been handled by a preceding ON CONFLICT clause.

-- Omitted target: matches any uniqueness violation
ON CONFLICT DO NOTHING

If a conflict target includes a WHERE clause, it becomes a partial conflict target. The WHERE expression restricts which rows of a partial unique index are considered when matching. This is relevant when the unique index itself was created with a WHERE clause.

DO NOTHING

ON CONFLICT [(conflict-target)] DO NOTHING

When a uniqueness constraint is violated, the conflicting row is silently skipped. No insert or update occurs for that row. If the INSERT statement includes a RETURNING clause, skipped rows produce no output.

DO UPDATE SET

ON CONFLICT [(conflict-target)] DO UPDATE SET assignment [, ...] [WHERE expr]

When a uniqueness constraint is violated, Turso updates the existing row instead of inserting a new one. The SET clause works the same as in a regular UPDATE statement. You can set individual columns or use the tuple form to set multiple columns at once:

-- Individual assignments
DO UPDATE SET price = excluded.price, quantity = excluded.quantity

-- Tuple assignment
DO UPDATE SET (price, quantity) = (excluded.price, excluded.quantity)

Within the SET clause and its optional WHERE clause, column references that are unqualified or qualified with the target table name refer to the existing row (before the update). To reference the values that were proposed for insertion, use the special excluded table qualifier.

ReferenceMeaning
column-nameValue in the existing row
table-name.column-nameValue in the existing row (explicit)
excluded.column-nameValue from the attempted INSERT

The DO UPDATE clause always uses ABORT conflict resolution internally. If the update itself causes a constraint violation (for example, setting a column to a value that duplicates another row’s unique key), the entire INSERT statement is rolled back.

WHERE on DO UPDATE

An optional WHERE clause after DO UPDATE SET controls whether the update actually takes effect. If the condition evaluates to false or NULL, the update is skipped for that row, effectively making the clause behave like DO NOTHING for that particular conflict.

ON CONFLICT (name) DO UPDATE SET
  phonenumber = excluded.phonenumber,
  valid_date = excluded.valid_date
WHERE excluded.valid_date > table-name.valid_date

This is useful for “only update if the new data is newer” patterns, or for conditional merges.

Multiple ON CONFLICT Clauses

An INSERT statement may include more than one ON CONFLICT clause. Turso evaluates them in order. When a uniqueness violation occurs, the first clause whose conflict target matches the violated constraint is used. Only one clause executes per conflicting row.

Every ON CONFLICT clause except the last one must include a conflict target. The last clause may omit the conflict target to serve as a catch-all for any remaining uniqueness violations.

INSERT INTO table-name (...)
  VALUES (...)
  ON CONFLICT (x) DO UPDATE SET ...   -- handles conflicts on x
  ON CONFLICT (y) DO UPDATE SET ...   -- handles conflicts on y
  ON CONFLICT DO NOTHING;             -- catch-all for any other uniqueness violation

Multi-Row Inserts

When an INSERT provides multiple rows (either through multiple VALUES rows or a SELECT subquery), the upsert decision is made independently for each row. Some rows may be inserted normally, while others trigger DO UPDATE or DO NOTHING.

Examples

-- Create a table with a unique constraint
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT UNIQUE,
  price REAL,
  quantity INTEGER
);

INSERT INTO products VALUES (1, 'Widget', 9.99, 100);
-- DO NOTHING: silently skip if the name already exists
INSERT INTO products VALUES (2, 'Widget', 12.99, 200)
  ON CONFLICT DO NOTHING;

SELECT * FROM products;
-- 1|Widget|9.99|100
-- DO UPDATE: update price and quantity when name conflicts
INSERT INTO products VALUES (1, 'Widget', 12.99, 200)
  ON CONFLICT(name) DO UPDATE SET
    price = excluded.price,
    quantity = excluded.quantity;

SELECT * FROM products;
-- 1|Widget|12.99|200
-- Mix existing row values with excluded values
CREATE TABLE counters (key TEXT UNIQUE, hits INTEGER, last_seen TEXT);
INSERT INTO counters VALUES ('page_home', 1, '2024-01-01');

-- Increment hits while updating last_seen from the new row
INSERT INTO counters VALUES ('page_home', 1, '2024-06-15')
  ON CONFLICT(key) DO UPDATE SET
    hits = hits + 1,
    last_seen = excluded.last_seen;

SELECT * FROM counters;
-- page_home|2|2024-06-15
-- Conditional update: only apply if the new value is greater
CREATE TABLE high_scores (player TEXT UNIQUE, score INTEGER);
INSERT INTO high_scores VALUES ('Alice', 5);

INSERT INTO high_scores VALUES ('Alice', 3)
  ON CONFLICT(player) DO UPDATE SET score = excluded.score
  WHERE excluded.score > score;

SELECT * FROM high_scores;
-- Alice|5  (unchanged because 3 is not greater than 5)

INSERT INTO high_scores VALUES ('Alice', 10)
  ON CONFLICT(player) DO UPDATE SET score = excluded.score
  WHERE excluded.score > score;

SELECT * FROM high_scores;
-- Alice|10  (updated because 10 > 5)
-- Multi-row insert with upsert: each row handled independently
CREATE TABLE inventory (sku TEXT UNIQUE, name TEXT);
INSERT INTO inventory VALUES ('A001', 'Original');

INSERT INTO inventory VALUES ('A001', 'Updated'), ('B002', 'New Item')
  ON CONFLICT(sku) DO UPDATE SET name = excluded.name;

SELECT * FROM inventory ORDER BY sku;
-- A001|Updated
-- B002|New Item
-- Multiple ON CONFLICT clauses with different targets
CREATE TABLE records (
  id INTEGER PRIMARY KEY,
  code TEXT UNIQUE,
  email TEXT UNIQUE,
  note TEXT DEFAULT NULL
);

INSERT INTO records VALUES (1, 'x', 'a@test.com', 'original');
INSERT INTO records VALUES (2, 'y', 'b@test.com', 'original');

INSERT INTO records VALUES (3, 'x', 'c@test.com', 'new')
  ON CONFLICT(code) DO UPDATE SET note = 'code-conflict'
  ON CONFLICT(email) DO UPDATE SET note = 'email-conflict'
  ON CONFLICT DO UPDATE SET note = 'other-conflict';

SELECT * FROM records ORDER BY id;
-- 1|x|a@test.com|code-conflict
-- 2|y|b@test.com|original
-- Upsert with RETURNING clause
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT);
INSERT INTO settings VALUES ('theme', 'light');

INSERT INTO settings VALUES ('theme', 'dark')
  ON CONFLICT DO UPDATE SET value = excluded.value
  RETURNING key, value;
-- theme|dark
-- DO NOTHING with RETURNING produces no output for skipped rows
CREATE TABLE tags (name TEXT PRIMARY KEY);
INSERT INTO tags VALUES ('important');

INSERT INTO tags VALUES ('important')
  ON CONFLICT DO NOTHING
  RETURNING name;
-- (no output)
-- Composite unique index: target must list all columns
CREATE TABLE assignments (project TEXT, employee TEXT, role TEXT);
CREATE UNIQUE INDEX assignments_pk ON assignments(project, employee);

INSERT INTO assignments VALUES ('Atlas', 'Alice', 'Lead');

-- Column order in the target does not need to match the index
INSERT INTO assignments VALUES ('Atlas', 'Alice', 'Manager')
  ON CONFLICT(employee, project) DO UPDATE SET role = excluded.role;

SELECT * FROM assignments;
-- Atlas|Alice|Manager
-- Using the table-qualified name in the conflict target
CREATE TABLE metrics (sensor_id INTEGER UNIQUE, reading REAL);
INSERT INTO metrics VALUES (1, 23.5);

INSERT INTO metrics VALUES (1, 25.0)
  ON CONFLICT(metrics.sensor_id) DO UPDATE SET
    reading = metrics.reading + excluded.reading;

SELECT * FROM metrics;
-- 1|48.5

Compatibility

UPSERT is fully supported in Turso. The syntax and behavior match SQLite, including support for multiple ON CONFLICT clauses, the excluded table, conditional WHERE clauses on DO UPDATE, composite conflict targets, and tuple-form SET assignments.

UPDATE

Syntax

UPDATE [OR {ROLLBACK | ABORT | FAIL | IGNORE | REPLACE}]
  table-name [AS alias]
  SET {column-name = expr | (column-name [, ...]) = (expr [, ...])} [, ...]
  [WHERE expr]
  [RETURNING expr [AS alias] [, ...]]
  [LIMIT expr [OFFSET expr]]

Description

The UPDATE statement modifies the values of columns in zero or more rows of an existing table. Each SET clause assigns a new value to a column. If no WHERE clause is provided, every row in the table is updated. When a WHERE clause is present, only rows for which the expression evaluates to true are modified.

It is not an error if the WHERE clause matches zero rows. The statement completes successfully and modifies nothing.

All expressions on the right-hand side of SET assignments are evaluated before any assignments are made. This means SET expressions can safely reference the current (pre-update) values of any column in the same row, including columns that appear on the left-hand side of another assignment in the same statement.

Clauses

SET

The SET clause specifies one or more column assignments. Each assignment is either a single column name paired with an expression, or a parenthesized list of column names paired with a matching parenthesized list of expressions (row value syntax).

-- Single column assignment
UPDATE products SET price = 19.99 WHERE id = 1;

-- Multiple column assignments
UPDATE products SET price = 19.99, in_stock = 1 WHERE id = 1;

-- Row value syntax (equivalent to individual assignments)
UPDATE products SET (price, in_stock) = (19.99, 1) WHERE id = 1;

Columns not mentioned in the SET clause retain their existing values. If a column name appears more than once in the SET clause, all but the rightmost occurrence are ignored.

WHERE

The WHERE clause limits which rows are updated. Only rows for which the expression evaluates to true are affected. The expression can be any valid SQL expression, including subqueries.

-- Update rows matching a condition
UPDATE orders SET status = 'shipped' WHERE status = 'pending';

-- Update using a subquery in WHERE
UPDATE orders SET status = 'priority'
  WHERE customer_id IN (SELECT id FROM customers WHERE tier = 'gold');

OR Conflict Algorithm

By prefixing the UPDATE with OR algorithm, you can control what happens when an updated value would violate a constraint (UNIQUE, NOT NULL, CHECK, or PRIMARY KEY). The keyword appears between UPDATE and the table name.

AlgorithmBehavior
ABORTAbort the current statement and roll back any changes it made. This is the default behavior when no algorithm is specified.
ROLLBACKAbort the current statement and roll back the entire enclosing transaction.
FAILAbort the current statement but keep changes already made to earlier rows within the same statement.
IGNORESkip the row that caused the violation and continue processing remaining rows.
REPLACEDelete the existing row that conflicts with the updated value, then apply the update. If the conflicting column has a NOT NULL constraint with a DEFAULT value, the default is used when NULL is supplied. If there is no default, the statement fails.

Foreign key constraint violations are not affected by the conflict algorithm. They always behave like ABORT regardless of which algorithm is specified.

-- IGNORE: skip updates that would violate a UNIQUE constraint
CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);
INSERT INTO users VALUES (1, 'alice@example.com'), (2, 'bob@example.com');
UPDATE OR IGNORE users SET email = 'alice@example.com' WHERE id = 2;
-- Row 2 is unchanged because the update would violate the UNIQUE constraint.

-- REPLACE: delete the conflicting row, then apply the update
UPDATE OR REPLACE users SET email = 'alice@example.com' WHERE id = 2;
-- Row 1 is deleted, row 2 now has email 'alice@example.com'.

RETURNING

The RETURNING clause causes the UPDATE statement to return values from each modified row, much like a SELECT. It accepts a list of expressions that may reference columns of the updated row (with their new, post-update values), use functions, or contain arbitrary expressions. Use * to return all columns.

UPDATE employees SET salary = salary * 1.10 WHERE department = 'Engineering'
  RETURNING id, name, salary;

For full details on the RETURNING clause, see RETURNING.

LIMIT and OFFSET

The LIMIT clause restricts the maximum number of rows that the UPDATE modifies. A negative value for LIMIT means no limit. When OFFSET is specified, the first N rows that would otherwise be updated are skipped.

Note that without ORDER BY (which Turso does not currently support for UPDATE), the order in which rows are considered is arbitrary. Therefore, LIMIT and OFFSET choose from an unpredictable set of qualifying rows.

-- Update at most 1 row
UPDATE products SET featured = 1 WHERE category = 'electronics' LIMIT 1;

-- Update 2 rows, skipping the first 3
UPDATE logs SET archived = 1 LIMIT 2 OFFSET 3;

Examples

-- Update a single row
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, department TEXT, salary REAL);
INSERT INTO employees VALUES
  (1, 'Alice', 'Engineering', 85000.0),
  (2, 'Bob', 'Marketing', 72000.0),
  (3, 'Charlie', 'Engineering', 92000.0);
UPDATE employees SET salary = 90000.0 WHERE id = 1;
SELECT * FROM employees WHERE id = 1;
-- 1|Alice|Engineering|90000.0
-- Update multiple columns at once
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, department TEXT, salary REAL);
INSERT INTO employees VALUES (2, 'Bob', 'Marketing', 72000.0);
UPDATE employees SET department = 'Sales', salary = 78000.0 WHERE id = 2;
SELECT * FROM employees WHERE id = 2;
-- 2|Bob|Sales|78000.0
-- Update all rows using an expression that references the current value
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, salary REAL);
INSERT INTO employees VALUES (1, 'Alice', 100.0), (2, 'Bob', 200.0), (3, 'Charlie', 300.0);
UPDATE employees SET salary = salary * 2;
SELECT * FROM employees ORDER BY id;
-- 1|Alice|200.0
-- 2|Bob|400.0
-- 3|Charlie|600.0
-- Self-referencing expression: columns on the right side use pre-update values
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, salary REAL);
INSERT INTO employees VALUES (1, 'Alice', 10.0), (2, 'Bob', 20.0);
UPDATE employees SET salary = salary + 5.0 WHERE salary < 15.0;
SELECT * FROM employees ORDER BY id;
-- 1|Alice|15.0
-- 2|Bob|20.0
-- Update with a subquery in the WHERE clause
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, department TEXT, salary REAL);
INSERT INTO employees VALUES (1, 'Alice', 'Engineering', 85000.0), (2, 'Bob', 'Sales', 72000.0);
CREATE TABLE priority_departments (name TEXT);
INSERT INTO priority_departments VALUES ('Engineering');
UPDATE employees SET salary = salary + 10000.0
  WHERE department IN (SELECT name FROM priority_departments);
SELECT name, salary FROM employees ORDER BY id;
-- Alice|95000.0
-- Bob|72000.0
-- Scalar subquery in SET to assign a computed value
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, salary REAL);
INSERT INTO employees VALUES (1, 'Alice', 80000.0), (2, 'Bob', 60000.0);
UPDATE employees SET salary = (SELECT AVG(salary) FROM employees) WHERE id = 2;
SELECT * FROM employees ORDER BY id;
-- 1|Alice|80000.0
-- 2|Bob|70000.0
-- Update with EXISTS subquery
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, status TEXT);
INSERT INTO orders VALUES (1, 100, 'pending'), (2, 101, 'pending'), (3, 102, 'pending');
CREATE TABLE order_items (order_id INTEGER, product TEXT);
INSERT INTO order_items VALUES (1, 'widget'), (3, 'gadget');
UPDATE orders SET status = 'has_items'
  WHERE EXISTS (SELECT 1 FROM order_items WHERE order_items.order_id = orders.id);
SELECT id, status FROM orders ORDER BY id;
-- 1|has_items
-- 2|pending
-- 3|has_items
-- UPDATE with RETURNING to see new values
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, salary REAL);
INSERT INTO employees VALUES (1, 'Alice', 85000.0), (2, 'Bob', 72000.0);
UPDATE employees SET salary = salary + 10000.0 WHERE id = 1 RETURNING id, name, salary;
-- 1|Alice|95000.0
-- RETURNING with expressions and functions
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, salary REAL);
INSERT INTO employees VALUES (1, 'Alice', 85000.0);
UPDATE employees SET name = 'alice johnson' WHERE id = 1
  RETURNING id, upper(name), salary;
-- 1|ALICE JOHNSON|85000.0
-- RETURNING all columns with *
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, salary REAL);
INSERT INTO employees VALUES (1, 'Alice', 85000.0);
UPDATE employees SET salary = 90000.0 WHERE id = 1 RETURNING *;
-- 1|Alice|90000.0
-- Row value syntax for multiple assignments
CREATE TABLE contacts (id INTEGER PRIMARY KEY, first_name TEXT, last_name TEXT);
INSERT INTO contacts VALUES (1, 'Jane', 'Doe');
UPDATE contacts SET (first_name, last_name) = ('John', 'Smith') WHERE id = 1
  RETURNING *;
-- 1|John|Smith
-- UPDATE OR IGNORE: skip rows that would violate a UNIQUE constraint
CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);
INSERT INTO tags VALUES (1, 'urgent'), (2, 'review');
UPDATE OR IGNORE tags SET label = 'urgent' WHERE id = 2;
SELECT * FROM tags ORDER BY id;
-- 1|urgent
-- 2|review
-- UPDATE OR REPLACE: delete the conflicting row and apply the update
CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);
INSERT INTO tags VALUES (1, 'urgent'), (2, 'review');
UPDATE OR REPLACE tags SET label = 'urgent' WHERE id = 2;
SELECT * FROM tags ORDER BY id;
-- 2|urgent
-- UPDATE with LIMIT
CREATE TABLE tasks (id INTEGER PRIMARY KEY, done INTEGER DEFAULT 0);
INSERT INTO tasks (id) VALUES (1), (2), (3), (4), (5);
UPDATE tasks SET done = 1 LIMIT 2;
SELECT COUNT(*) FROM tasks WHERE done = 1;
-- 2
-- Update the rowid directly
CREATE TABLE notes (content TEXT);
INSERT INTO notes (content) VALUES ('hello');
UPDATE notes SET rowid = 42;
SELECT rowid, content FROM notes;
-- 42|hello
-- Update using a table alias in the WHERE clause
CREATE TABLE scores (player TEXT, points INTEGER);
INSERT INTO scores VALUES ('Alice', 10), ('Bob', 20);
UPDATE scores AS s SET points = 99 WHERE s.player = 'Alice';
SELECT * FROM scores ORDER BY player;
-- Alice|99
-- Bob|20

Compatibility

Turso supports the core UPDATE statement with full compatibility. The following features are not yet available:

FeatureStatus
UPDATE … FROMNot supported. Use subqueries in SET or WHERE instead.
ORDER BY clauseNot supported. LIMIT and OFFSET select from an arbitrary set of qualifying rows.
INDEXED BY / NOT INDEXEDNot supported. The query planner chooses indexes automatically.

DELETE

Syntax

[WITH cte-name AS (SELECT ...) [, ...]]
DELETE FROM table-name
  [WHERE expr]
  [RETURNING expr [AS alias] [, ...]]
  [LIMIT expr]

Description

The DELETE statement removes rows from a table. If a WHERE clause is provided, only the rows for which the WHERE expression evaluates to true are removed. Rows where the expression evaluates to false or NULL are retained.

If the WHERE clause is omitted, all rows in the table are deleted. The table itself is not dropped – it remains in the schema with zero rows.

The optional RETURNING clause causes the DELETE statement to return values from each deleted row, behaving much like a SELECT over the rows being removed. The optional LIMIT clause restricts the maximum number of rows deleted.

Clauses

WHERE

The WHERE clause specifies which rows to delete. It accepts any SQL expression that evaluates to a boolean result. Only rows where the expression is true are deleted. The expression may reference columns of the target table, use subqueries, and include any supported operators or functions.

When the WHERE clause is omitted, every row in the table is deleted.

-- Delete a single row by primary key
DELETE FROM employees WHERE id = 42;

-- Delete rows matching a compound condition
DELETE FROM employees WHERE salary < 90000 AND department = 'Marketing';

-- Delete rows using an IN list
DELETE FROM orders WHERE status IN ('cancelled', 'expired');

RETURNING

The RETURNING clause causes the DELETE statement to return values from each deleted row. It accepts a comma-separated list of expressions that may reference columns of the deleted row, use functions, or contain computed values. Use * to return all columns.

DELETE FROM products WHERE quantity = 0 RETURNING id, name;

DELETE FROM employees WHERE department = 'Sales' RETURNING *;

For full details on the RETURNING clause, see RETURNING.

LIMIT

The LIMIT clause restricts the maximum number of rows deleted. When present, at most expr rows are removed. A negative LIMIT value means no limit.

-- Delete at most 2 rows from the table
DELETE FROM employees LIMIT 2;

WITH (Common Table Expressions)

A DELETE statement may be preceded by a WITH clause that defines one or more common table expressions. CTEs defined this way can be referenced in the WHERE clause or in subqueries within the statement.

WITH low_earners AS (
  SELECT id FROM employees WHERE salary < 90000
)
DELETE FROM employees WHERE id IN (SELECT id FROM low_earners);

For more on CTEs, see Common Table Expressions.

Examples

-- Create and populate a sample table
CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  department TEXT,
  salary REAL
);

INSERT INTO employees VALUES
  (1, 'Alice', 'Engineering', 120000.00),
  (2, 'Bob', 'Marketing', 85000.00),
  (3, 'Carol', 'Engineering', 110000.00),
  (4, 'Dave', 'Sales', 90000.00),
  (5, 'Eve', 'Marketing', 78000.00);
-- Delete a single row by primary key
DELETE FROM employees WHERE id = 4;
SELECT * FROM employees;
-- 1|Alice|Engineering|120000.0
-- 2|Bob|Marketing|85000.0
-- 3|Carol|Engineering|110000.0
-- 5|Eve|Marketing|78000.0
-- Delete all rows matching a condition
DELETE FROM employees WHERE department = 'Marketing';
SELECT * FROM employees;
-- 1|Alice|Engineering|120000.0
-- 3|Carol|Engineering|110000.0
-- 4|Dave|Sales|90000.0
-- Delete all rows from a table (table itself remains)
DELETE FROM employees;
SELECT * FROM employees;
-- (no rows returned)
-- Delete with RETURNING to see which rows were removed
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  price REAL,
  quantity INTEGER
);

INSERT INTO products VALUES
  (1, 'Widget', 9.99, 100),
  (2, 'Gadget', 24.99, 0),
  (3, 'Doohickey', 4.99, 0),
  (4, 'Thingamajig', 14.99, 50);

DELETE FROM products WHERE quantity = 0 RETURNING id, name;
-- 2|Gadget
-- 3|Doohickey
-- RETURNING with a computed expression
DELETE FROM products WHERE quantity = 0
  RETURNING id, name, price * quantity AS lost_value;
-- 2|Gadget|0.0
-- 3|Doohickey|0.0
-- RETURNING * returns all columns of each deleted row
DELETE FROM products WHERE price > 100.00 RETURNING *;
-- Delete using a subquery in WHERE
CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  department TEXT,
  salary REAL
);

INSERT INTO employees VALUES
  (1, 'Alice', 'Engineering', 120000.00),
  (2, 'Bob', 'Marketing', 85000.00),
  (3, 'Carol', 'Engineering', 110000.00);

DELETE FROM employees
  WHERE id IN (SELECT id FROM employees WHERE department = 'Engineering');
SELECT * FROM employees;
-- 2|Bob|Marketing|85000.0
-- Delete with a CTE
CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  department TEXT,
  salary REAL
);

INSERT INTO employees VALUES
  (1, 'Alice', 'Engineering', 120000.00),
  (2, 'Bob', 'Marketing', 85000.00),
  (3, 'Carol', 'Engineering', 110000.00),
  (4, 'Dave', 'Sales', 90000.00),
  (5, 'Eve', 'Marketing', 78000.00);

WITH low_earners AS (
  SELECT id FROM employees WHERE salary < 90000
)
DELETE FROM employees WHERE id IN (SELECT id FROM low_earners);
SELECT * FROM employees;
-- 1|Alice|Engineering|120000.0
-- 3|Carol|Engineering|110000.0
-- 4|Dave|Sales|90000.0
-- Delete with LIMIT: remove at most 2 rows
CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  department TEXT,
  salary REAL
);

INSERT INTO employees VALUES
  (1, 'Alice', 'Engineering', 120000.00),
  (2, 'Bob', 'Marketing', 85000.00),
  (3, 'Carol', 'Engineering', 110000.00),
  (4, 'Dave', 'Sales', 90000.00),
  (5, 'Eve', 'Marketing', 78000.00);

DELETE FROM employees LIMIT 2;
SELECT * FROM employees;
-- 3|Carol|Engineering|110000.0
-- 4|Dave|Sales|90000.0
-- 5|Eve|Marketing|78000.0
-- Delete with WHERE and LIMIT: remove one matching row
CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  department TEXT,
  salary REAL
);

INSERT INTO employees VALUES
  (1, 'Alice', 'Engineering', 120000.00),
  (2, 'Bob', 'Marketing', 85000.00),
  (3, 'Carol', 'Engineering', 110000.00);

DELETE FROM employees WHERE department = 'Engineering' LIMIT 1;
SELECT * FROM employees;
-- 2|Bob|Marketing|85000.0
-- 3|Carol|Engineering|110000.0
-- Use changes() to check how many rows were deleted
CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  department TEXT,
  salary REAL
);

INSERT INTO employees VALUES
  (1, 'Alice', 'Engineering', 120000.00),
  (2, 'Bob', 'Marketing', 85000.00),
  (3, 'Carol', 'Engineering', 110000.00),
  (4, 'Dave', 'Sales', 90000.00),
  (5, 'Eve', 'Marketing', 78000.00);

DELETE FROM employees WHERE department = 'Marketing';
SELECT changes();
-- 2

Compatibility

Turso supports the DELETE statement with broad compatibility to SQLite, including the WHERE clause, RETURNING clause, LIMIT, and common table expressions.

The following differences from SQLite apply:

  • ORDER BY on DELETE is not supported. In SQLite, ORDER BY is available (when compiled with SQLITE_ENABLE_UPDATE_DELETE_LIMIT) and is used together with LIMIT to control which specific rows are deleted. Turso supports LIMIT on DELETE but does not support ORDER BY in this context.

  • OFFSET on DELETE is parsed but not currently effective. While the syntax is accepted, the OFFSET value is ignored and all deletions start from the first matching row. Use a subquery with LIMIT and OFFSET in a SELECT if you need to skip rows before deleting.

  • INDEXED BY / NOT INDEXED hints are not supported on DELETE statements.

RETURNING

Syntax

{INSERT | UPDATE | DELETE} ...
  RETURNING {expr [[AS] column-alias] | *} [, ...]

Description

The RETURNING clause is an optional clause that can be appended to INSERT, UPDATE, and DELETE statements. It causes the statement to return one result row for each database row that is inserted, updated, or deleted. This eliminates the need for a separate SELECT query to retrieve values that were generated or modified by the statement.

A common use case is retrieving auto-generated primary keys, computed default values, or confirming which rows were affected by an UPDATE or DELETE.

The RETURNING clause is not part of the SQL standard. It follows the syntax established by PostgreSQL.

Expressions

The RETURNING keyword is followed by a comma-separated list of expressions, similar to the expressions that follow SELECT in a query. Each expression may reference columns of the table being modified, use literal values, call scalar functions, or combine these with operators.

Column References

Expressions in RETURNING can reference any column of the modified table, either unqualified or qualified with the table name:

RETURNING id, name
RETURNING orders.id, orders.name

For INSERT and UPDATE, column references reflect the values after the change has been applied. For DELETE, column references reflect the values of the row before it is removed.

The * Operator

The * expands into all columns of the table being modified:

INSERT INTO orders (...) VALUES (...) RETURNING *;

Column Aliases

Each expression may optionally be followed by AS column-alias (or just column-alias without AS) to set the name of the result column:

RETURNING quantity * unit_price AS total

Allowed Expressions

The following kinds of expressions are allowed in a RETURNING clause:

  • Literal values (42, 'hello', NULL)
  • Column references (id, table_name.column_name)
  • Arithmetic and comparison operators (price * 1.1, value > 0)
  • String concatenation (first || ' ' || last)
  • Scalar function calls (upper(name), round(price, 2), coalesce(a, b))
  • CASE expressions
  • CAST expressions
  • IN, BETWEEN, LIKE, GLOB, IS NULL, IS NOT NULL operators
  • The rowid pseudo-column

Disallowed Expressions

Top-level aggregate functions (SUM, COUNT, AVG, etc.) and window functions are not permitted in RETURNING. Using them produces an error:

-- Error: aggregate functions not allowed in RETURNING
INSERT INTO t VALUES (1, 42) RETURNING SUM(value);

Behavior

INSERT RETURNING

Returns one row for each inserted row. When inserting multiple rows, one result row is produced per input row. The returned values reflect the state after insertion, including auto-generated primary keys and evaluated DEFAULT expressions.

UPDATE RETURNING

Returns one row for each row that was actually modified by the UPDATE. Column values in the result reflect the new values after the update. If the WHERE clause matches no rows, no result rows are produced.

DELETE RETURNING

Returns one row for each deleted row. Column values in the result reflect the values the row had before deletion. If the WHERE clause matches no rows, no result rows are produced.

UPSERT (ON CONFLICT) RETURNING

When RETURNING is used with an INSERT ... ON CONFLICT statement, it returns rows for both the insert and update code paths. If the conflict resolution is DO NOTHING and a conflict occurs, no row is returned for that conflicting input row.

Output Order

The order of rows returned by RETURNING is not guaranteed. It typically matches the order in which rows were processed, but applications should not rely on any particular ordering.

Triggers

The RETURNING clause reports the direct changes made by the statement. It does not report additional changes caused by triggers or foreign key constraint actions.

Examples

-- Retrieve the auto-generated id after inserting a row
CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer TEXT NOT NULL,
  product TEXT NOT NULL,
  quantity INTEGER NOT NULL,
  unit_price REAL NOT NULL
);

INSERT INTO orders (customer, product, quantity, unit_price)
  VALUES ('Alice', 'Widget', 5, 9.99)
  RETURNING id;
-- 1
-- Return all columns of newly inserted rows
INSERT INTO orders (customer, product, quantity, unit_price)
  VALUES ('Alice', 'Widget', 5, 9.99)
  RETURNING *;
-- 1|Alice|Widget|5|9.99
-- Return a computed expression with an alias
INSERT INTO orders (customer, product, quantity, unit_price)
  VALUES ('Alice', 'Widget', 5, 9.99)
  RETURNING id, customer, quantity * unit_price AS total;
-- 1|Alice|49.95
-- Return multiple rows from a multi-row INSERT
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT NOT NULL,
  active INTEGER DEFAULT 1
);

INSERT INTO users (name, email)
  VALUES ('Alice', 'alice@example.com'),
         ('Bob', 'bob@example.com'),
         ('Charlie', 'charlie@example.com')
  RETURNING id, name;
-- 1|Alice
-- 2|Bob
-- 3|Charlie
-- Retrieve auto-filled DEFAULT values
CREATE TABLE events (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  created_at TEXT DEFAULT (datetime('now'))
);

INSERT INTO events (name) VALUES ('signup')
  RETURNING id, name, created_at;
-- 1|signup|2026-02-11 18:52:48
-- See which rows were updated and their new values
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  price REAL NOT NULL
);

INSERT INTO products (name, price)
  VALUES ('Laptop', 999.99), ('Mouse', 29.99), ('Keyboard', 79.99);

UPDATE products SET price = price * 0.9
  RETURNING id, name, round(price, 2) AS discounted_price;
-- 1|Laptop|899.99
-- 2|Mouse|26.99
-- 3|Keyboard|71.99
-- Use a CASE expression in RETURNING
CREATE TABLE tasks (
  id INTEGER PRIMARY KEY,
  title TEXT NOT NULL,
  done INTEGER DEFAULT 0
);

INSERT INTO tasks (title)
  VALUES ('Write report'), ('Fix bug'), ('Review PR');

UPDATE tasks SET done = 1 WHERE id = 2
  RETURNING id, title,
    CASE WHEN done THEN 'completed' ELSE 'pending' END AS status;
-- 2|Fix bug|completed
-- Confirm which rows were deleted
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT NOT NULL,
  active INTEGER DEFAULT 1
);

INSERT INTO users (name, email)
  VALUES ('Alice', 'alice@example.com'),
         ('Bob', 'bob@example.com'),
         ('Charlie', 'charlie@example.com');

DELETE FROM users WHERE active = 1
  RETURNING id, name, email;
-- 1|Alice|alice@example.com
-- 2|Bob|bob@example.com
-- 3|Charlie|charlie@example.com
-- Use function expressions in DELETE RETURNING
CREATE TABLE logs (
  id INTEGER PRIMARY KEY,
  message TEXT NOT NULL,
  level TEXT NOT NULL
);

INSERT INTO logs (message, level)
  VALUES ('User login', 'info'), ('Disk full', 'error'), ('Timeout', 'warn');

DELETE FROM logs WHERE level = 'error'
  RETURNING id, message, upper(level) AS level;
-- 2|Disk full|ERROR
-- RETURNING with UPSERT: insert path
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);

INSERT INTO settings (key, value) VALUES ('theme', 'dark')
  ON CONFLICT(key) DO UPDATE SET value = excluded.value
  RETURNING key, value;
-- theme|dark
-- RETURNING with UPSERT: update path (conflict triggers DO UPDATE)
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
INSERT INTO settings (key, value) VALUES ('theme', 'dark');

INSERT INTO settings (key, value) VALUES ('theme', 'light')
  ON CONFLICT(key) DO UPDATE SET value = excluded.value
  RETURNING key, value;
-- theme|light
-- RETURNING with UPSERT: DO NOTHING returns no rows on conflict
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
INSERT INTO settings (key, value) VALUES ('theme', 'dark');

INSERT INTO settings (key, value) VALUES ('theme', 'light')
  ON CONFLICT DO NOTHING
  RETURNING key, value;
-- (no rows returned)
-- RETURNING with INSERT ... SELECT
CREATE TABLE source (id INTEGER, name TEXT, score INTEGER);
CREATE TABLE archive (name TEXT, score INTEGER);

INSERT INTO source VALUES (1, 'Alice', 95), (2, 'Bob', 82), (3, 'Charlie', 78);

INSERT INTO archive SELECT name, score FROM source WHERE score >= 80
  RETURNING *;
-- Alice|95
-- Bob|82

CREATE TABLE

Syntax

CREATE TABLE [IF NOT EXISTS] table-name (
  column-def [, ...]
  [, table-constraint [, ...]]
) [STRICT]

Where column-def is:

column-name [type-name] [column-constraint ...]

And column-constraint is one of:

PRIMARY KEY [ASC | DESC] [AUTOINCREMENT] [conflict-clause]
NOT NULL [conflict-clause]
UNIQUE [conflict-clause]
CHECK (expr)
DEFAULT {value | (expr)}
COLLATE {BINARY | NOCASE | RTRIM}
REFERENCES foreign-table (foreign-column [, ...]) [foreign-key-action ...]
[CONSTRAINT constraint-name] column-constraint

And table-constraint is one of:

PRIMARY KEY (column-name [, ...]) [conflict-clause]
UNIQUE (column-name [, ...]) [conflict-clause]
CHECK (expr)
FOREIGN KEY (column-name [, ...]) REFERENCES foreign-table (foreign-column [, ...]) [foreign-key-action ...]

And conflict-clause is:

ON CONFLICT {ROLLBACK | ABORT | FAIL | IGNORE | REPLACE}

Description

The CREATE TABLE statement creates a new table in the database. Each table has a name, a list of column definitions, and optional table-level constraints. Table names beginning with sqlite_ are reserved for internal use and cannot be created by user SQL.

Every ordinary table in Turso has an implicit 64-bit signed integer key called the rowid. The rowid uniquely identifies each row within the table and provides fast lookup. You can access it using the names rowid, _rowid_, or oid, unless one of those names is used as an explicit column name.

Clauses

IF NOT EXISTS

When IF NOT EXISTS is specified, the statement is a no-op if a table with the same name already exists. Without this clause, attempting to create a table with an existing name produces an error.

CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);

Column Definitions

Each column definition specifies a column name and an optional declared type. The declared type determines the column’s type affinity, which influences how values are coerced on insertion. See Type Conversions for the full affinity rules.

Turso uses dynamic typing. A column’s declared type does not restrict what values can be stored in it (unless the table uses STRICT mode). Any column can hold any storage class: NULL, INTEGER, REAL, TEXT, or BLOB.

DEFAULT

The DEFAULT clause specifies a value to use when an INSERT statement omits a column. If no DEFAULT clause is specified, the default value is NULL.

The default value can be:

FormDescription
NULLNull value.
Signed numberA literal integer or real number, optionally with a + or - prefix.
String literalA single-quoted string.
TRUE / FALSEBoolean literals (stored as 1 and 0).
CURRENT_TIMECurrent time as HH:MM:SS.
CURRENT_DATECurrent date as YYYY-MM-DD.
CURRENT_TIMESTAMPCurrent date and time as YYYY-MM-DD HH:MM:SS.
(expr)An expression in parentheses, evaluated at insert time.
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL DEFAULT 'Unknown',
  price REAL DEFAULT 0.0,
  in_stock INTEGER DEFAULT TRUE,
  created_at TEXT DEFAULT CURRENT_TIMESTAMP
);

PRIMARY KEY

Each table may have at most one primary key. A primary key can be declared as a column constraint (single-column) or as a table constraint (composite).

Single-column primary key:

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL
);

Composite primary key (table constraint):

CREATE TABLE enrollment (
  student_id INTEGER,
  course_id INTEGER,
  grade TEXT,
  PRIMARY KEY (student_id, course_id)
);

Attempting to insert a duplicate primary key value produces a constraint violation error.

INTEGER PRIMARY KEY and the Rowid

When a single column is declared with the exact type name INTEGER and is the PRIMARY KEY, that column becomes an alias for the rowid. This is a special case:

  • The type name must be exactly INTEGER – not INT, BIGINT, SMALLINT, or any other variation.
  • The column can only contain integer values (or NULL, which is auto-assigned a rowid).
  • Inserting NULL into an INTEGER PRIMARY KEY column automatically assigns the next available rowid.
CREATE TABLE events (
  id INTEGER PRIMARY KEY,
  description TEXT
);
INSERT INTO events (description) VALUES ('Server started');
-- id is automatically assigned (e.g., 1)
INSERT INTO events (id, description) VALUES (NULL, 'User login');
-- id is automatically assigned (e.g., 2)

AUTOINCREMENT

The AUTOINCREMENT keyword can only be used with INTEGER PRIMARY KEY. It modifies the automatic rowid assignment to guarantee that automatically-assigned rowids are never reused, even after rows are deleted.

Without AUTOINCREMENT, Turso reuses deleted rowid values. With AUTOINCREMENT, Turso tracks the largest rowid ever inserted in the internal sqlite_sequence table, and new auto-assigned rowids are always greater than any previously used value.

CREATE TABLE audit_log (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  action TEXT NOT NULL
);
INSERT INTO audit_log (action) VALUES ('create');
INSERT INTO audit_log (action) VALUES ('update');
DELETE FROM audit_log WHERE id = 2;
INSERT INTO audit_log (action) VALUES ('delete');
-- The new row gets id=3, not id=2

AUTOINCREMENT has a small performance cost because it requires reading and writing the sqlite_sequence table on each insert. Use it only when strictly monotonically increasing rowids are required.

If the maximum rowid value (9223372036854775807) has been used, inserting a new row with AUTOINCREMENT produces an error rather than attempting to find an unused rowid.

NOT NULL

The NOT NULL constraint prevents a column from containing NULL values. Attempting to insert or update a NULL into a NOT NULL column produces a constraint violation error.

CREATE TABLE contacts (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT NOT NULL,
  phone TEXT
);

UNIQUE

The UNIQUE constraint ensures that all values in a column (or combination of columns) are distinct. Multiple UNIQUE constraints can appear on the same table.

CREATE TABLE accounts (
  id INTEGER PRIMARY KEY,
  username TEXT UNIQUE,
  email TEXT UNIQUE
);

A UNIQUE table constraint can span multiple columns:

CREATE TABLE assignments (
  employee_id INTEGER,
  project_id INTEGER,
  role TEXT,
  UNIQUE (employee_id, project_id)
);

Note: NULL values are considered distinct from each other for UNIQUE constraint purposes. Multiple rows may have NULL in a UNIQUE column without violating the constraint.

CHECK

The CHECK constraint specifies an expression that must evaluate to a non-zero (true) value for every row. If the expression evaluates to zero, the insert or update is rejected. NULL values pass CHECK constraints (NULL is neither true nor false).

CHECK can be used as a column constraint or a table constraint:

CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  age INTEGER CHECK (age >= 18),
  salary INTEGER CHECK (salary > 0),
  CHECK (age <= 120)
);

CHECK expressions may reference multiple columns, use functions, and include operators like IN, BETWEEN, LIKE, and CASE:

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  status TEXT CHECK (status IN ('pending', 'active', 'shipped', 'delivered')),
  quantity INTEGER,
  unit_price INTEGER,
  total INTEGER,
  CHECK (total = quantity * unit_price)
);

CHECK expressions cannot contain subqueries, aggregate functions, or bind parameters. Referencing a non-existent column in a CHECK expression produces an error at table creation time.

COLLATE

The COLLATE clause on a column definition sets the default collation sequence for that column. This affects comparisons, sorting, and UNIQUE/PRIMARY KEY constraints.

CollationBehavior
BINARYCompares bytes directly (default).
NOCASECase-insensitive comparison for ASCII characters.
RTRIMLike BINARY, but trailing spaces are ignored.
CREATE TABLE tags (
  id INTEGER PRIMARY KEY,
  name TEXT COLLATE NOCASE UNIQUE
);

FOREIGN KEY

Foreign key constraints enforce referential integrity between tables. A foreign key in a child table references a column (or columns) in a parent table, ensuring that every value in the child column exists in the parent column.

Foreign key enforcement must be enabled with PRAGMA foreign_keys = ON (it is off by default).

Column-level syntax:

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER REFERENCES customers (id)
);

Table-level syntax:

CREATE TABLE order_items (
  id INTEGER PRIMARY KEY,
  order_id INTEGER,
  product_id INTEGER,
  FOREIGN KEY (order_id) REFERENCES orders (id),
  FOREIGN KEY (product_id) REFERENCES products (id)
);

Foreign key actions specify what happens when the referenced row in the parent table is deleted or updated:

FOREIGN KEY (column) REFERENCES parent (column)
  [ON DELETE {SET NULL | CASCADE | RESTRICT | NO ACTION}]
  [ON UPDATE {SET NULL | CASCADE | RESTRICT | NO ACTION}]
ActionBehavior
NO ACTIONReject the change if child rows exist (default).
RESTRICTSame as NO ACTION, but checked immediately rather than deferred.
CASCADEDelete or update the child rows to match the parent change.
SET NULLSet the child foreign key column(s) to NULL.
PRAGMA foreign_keys = ON;
CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE staff (
  id INTEGER PRIMARY KEY,
  dept_id INTEGER REFERENCES departments (id) ON DELETE CASCADE
);

Conflict Clauses

The PRIMARY KEY, NOT NULL, and UNIQUE constraints accept an optional ON CONFLICT clause that specifies how constraint violations are handled:

AlgorithmBehavior
ABORTAbort the current statement and roll back its changes (default).
ROLLBACKAbort the current statement and roll back the entire transaction.
FAILAbort the current statement but keep changes from earlier rows.
IGNORESkip the row that caused the violation and continue.
REPLACEFor UNIQUE/PRIMARY KEY: delete the conflicting row, then insert. For NOT NULL: replace the NULL with the column’s default value.
CREATE TABLE settings (
  key TEXT PRIMARY KEY ON CONFLICT REPLACE,
  value TEXT NOT NULL
);

Note: CHECK constraints do not accept an ON CONFLICT clause in the column definition. The conflict resolution for CHECK violations is always ABORT by default, but can be overridden per-statement using INSERT OR IGNORE, INSERT OR REPLACE, etc.

STRICT

The STRICT keyword at the end of the column list enables strict type checking. In a STRICT table, every column must have a declared type, and the type must be one of: INTEGER, REAL, TEXT, BLOB, or ANY.

When inserting or updating values, Turso rejects values that do not match the declared type (with some coercion: numeric strings are accepted for INTEGER and REAL columns). Columns without a NOT NULL constraint still accept NULL values.

CREATE TABLE measurements (
  id INTEGER PRIMARY KEY,
  sensor_name TEXT,
  value REAL,
  recorded_at TEXT
) STRICT;

Examples

Basic Table with Constraints

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  username TEXT NOT NULL UNIQUE,
  email TEXT NOT NULL UNIQUE,
  display_name TEXT DEFAULT 'Anonymous',
  age INTEGER CHECK (age >= 13)
);

INSERT INTO users (id, username, email, age)
VALUES (1, 'alice', 'alice@example.com', 30);
SELECT * FROM users;
-- 1|alice|alice@example.com|Anonymous|30

Composite Primary Key

CREATE TABLE enrollment (
  student_id INTEGER,
  course_id INTEGER,
  enrolled_at TEXT DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (student_id, course_id)
);

INSERT INTO enrollment (student_id, course_id) VALUES (1, 101);
INSERT INTO enrollment (student_id, course_id) VALUES (1, 102);
INSERT INTO enrollment (student_id, course_id) VALUES (2, 101);

SELECT student_id, course_id FROM enrollment ORDER BY student_id, course_id;
-- 1|101
-- 1|102
-- 2|101

AUTOINCREMENT

CREATE TABLE audit_log (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  action TEXT NOT NULL
);

INSERT INTO audit_log (action) VALUES ('user.login');
INSERT INTO audit_log (action) VALUES ('user.logout');
SELECT * FROM audit_log;
-- 1|user.login
-- 2|user.logout

CHECK Constraints with Functions

CREATE TABLE accounts (
  id INTEGER PRIMARY KEY,
  username TEXT CHECK (length(username) >= 3 AND length(username) <= 20),
  email TEXT CHECK (email LIKE '%@%.%')
);

INSERT INTO accounts VALUES (1, 'alice', 'alice@example.com');
SELECT * FROM accounts;
-- 1|alice|alice@example.com

Foreign Keys with CASCADE

PRAGMA foreign_keys = ON;

CREATE TABLE departments (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL
);

CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  dept_id INTEGER REFERENCES departments (id) ON DELETE CASCADE
);

INSERT INTO departments VALUES (1, 'Engineering'), (2, 'Sales');
INSERT INTO employees VALUES (1, 'Alice', 1), (2, 'Bob', 1), (3, 'Carol', 2);

DELETE FROM departments WHERE id = 1;
SELECT * FROM employees;
-- 3|Carol|2

STRICT Table

CREATE TABLE inventory (
  id INTEGER PRIMARY KEY,
  item_name TEXT,
  quantity INTEGER
) STRICT;

INSERT INTO inventory VALUES (1, 'Bolts', 500);
SELECT * FROM inventory;
-- 1|Bolts|500

Default Values with Expressions

CREATE TABLE tasks (
  id INTEGER PRIMARY KEY,
  title TEXT NOT NULL,
  status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'done')),
  priority INTEGER DEFAULT (1 + 1)
);

INSERT INTO tasks (id, title) VALUES (1, 'Review pull request');
SELECT * FROM tasks;
-- 1|Review pull request|pending|2

IF NOT EXISTS

CREATE TABLE config (key TEXT PRIMARY KEY, value TEXT);
CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT);
-- No error on the second statement

Conflict Resolution

CREATE TABLE kv_store (
  key TEXT PRIMARY KEY ON CONFLICT REPLACE,
  value TEXT NOT NULL
);

INSERT INTO kv_store VALUES ('theme', 'light');
INSERT INTO kv_store VALUES ('theme', 'dark');

SELECT * FROM kv_store;
-- dark

Compatibility

  • CREATE TABLE AS SELECT is not supported. Use CREATE TABLE followed by INSERT ... SELECT instead.
  • TEMPORARY tables (CREATE TEMP TABLE) are not supported.
  • WITHOUT ROWID tables are not supported.
  • STRICT tables are experimental and partially supported.
  • SET DEFAULT as a foreign key action is not supported; use SET NULL or CASCADE instead.

CREATE INDEX

Syntax

CREATE [UNIQUE] INDEX [IF NOT EXISTS] index-name
  ON table-name (indexed-column [, ...])
  [WHERE expr]

Where indexed-column is:

{column-name | (expr)} [COLLATE collation-name] [ASC | DESC]

To remove an index:

DROP INDEX [IF EXISTS] index-name

Description

The CREATE INDEX statement creates a new index on one or more columns of an existing table. Indexes speed up queries that filter, sort, or join on the indexed columns, at the cost of additional storage and slightly slower writes.

An index does not change the logical content of a table. Queries produce the same results whether or not an index exists. Turso automatically decides whether to use an available index when executing a query. You can use EXPLAIN to see whether a query plan uses a particular index.

The DROP INDEX statement removes an index from the database. Dropping an index has no effect on the table data.

Clauses

UNIQUE

When the UNIQUE keyword appears between CREATE and INDEX, the index enforces a uniqueness constraint on the indexed columns. Any attempt to insert or update a row that would create a duplicate entry in the indexed columns produces a UNIQUE constraint failed error.

NULL values are considered distinct from each other for uniqueness purposes. Multiple rows may contain NULL in a UNIQUE index column without violating the constraint.

CREATE UNIQUE INDEX idx_users_email ON users(email);
-- Two rows with email = 'alice@example.com' would be rejected.
-- Two rows with email = NULL are allowed.

IF NOT EXISTS

When IF NOT EXISTS is specified, the statement is a no-op if an index with the same name already exists. Without this clause, attempting to create an index whose name is already in use produces an error.

CREATE INDEX IF NOT EXISTS idx_products_name ON products(name);
-- Safe to run repeatedly without error.

IF EXISTS (DROP INDEX)

When IF EXISTS is included in a DROP INDEX statement, no error is raised if the named index does not exist. Without this clause, dropping a nonexistent index produces an error.

DROP INDEX IF EXISTS idx_old_report;

Indexed Columns

Each indexed column entry in the column list is either a column name or a parenthesized expression. Multiple columns or expressions can be listed, separated by commas, to create a composite (multi-column) index.

The order of columns in a composite index matters. An index on (state, city) is most useful for queries that filter on state, or on both state and city, but provides little benefit for queries that filter only on city.

COLLATE

Each indexed column or expression may include a COLLATE clause specifying the collation sequence used for text comparisons within the index. If omitted, the collation defaults to the one defined on the column in CREATE TABLE, or BINARY if none was specified.

CollationBehavior
BINARYCompares bytes directly (default).
NOCASECase-insensitive comparison for ASCII characters.
RTRIMLike BINARY, but trailing spaces are ignored.

For a query to use an index, the collation in the query’s comparison must match the collation of the index. If a column is defined as COLLATE NOCASE but the index uses COLLATE BINARY, queries that rely on case-insensitive matching will not use that index.

CREATE INDEX idx_contacts_name ON contacts(last_name COLLATE NOCASE);

ASC / DESC

Each indexed column or expression may be followed by ASC (ascending, the default) or DESC (descending) to specify the sort order stored in the index. Descending indexes are useful for queries that sort in descending order, allowing the index to be scanned in its natural order rather than reversed.

CREATE INDEX idx_transactions_recent ON transactions(account_id, created_at DESC);

WHERE (Partial Indexes)

When a WHERE clause is appended to CREATE INDEX, the result is a partial index. Only rows that satisfy the WHERE expression are included in the index. This reduces index size and write overhead for tables where queries consistently filter on a known condition.

The WHERE expression in a partial index has the following restrictions:

  • It may not contain subqueries.
  • It may not use functions whose results can change (such as random()).
  • It may only reference columns of the indexed table.

For Turso to use a partial index when executing a query, the query’s WHERE clause must imply the index’s WHERE clause. In the simplest case, the query includes the same condition as the index.

CREATE INDEX idx_orders_pending ON orders(customer_id)
  WHERE status = 'pending';
-- Only rows with status = 'pending' are indexed.

Expression Indexes

Instead of a plain column name, an indexed column entry may be a parenthesized expression. This is useful for indexing computed values, such as the lowercase version of a text column.

Expression indexes have the following restrictions:

  • The expression may not reference other tables.
  • The expression may not contain subqueries.
  • The expression may only use deterministic functions (not random(), last_insert_rowid(), etc.).
CREATE INDEX idx_users_email_lower ON users(lower(email));
-- Speeds up queries like: SELECT * FROM users WHERE lower(email) = 'alice@example.com';

Examples

Basic single-column index

CREATE TABLE employees (
  id INTEGER PRIMARY KEY,
  name TEXT,
  department TEXT,
  salary REAL
);

CREATE INDEX idx_employees_name ON employees(name);

Unique index

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  email TEXT NOT NULL
);

CREATE UNIQUE INDEX idx_users_email ON users(email);

INSERT INTO users VALUES (1, 'alice@example.com');
INSERT INTO users VALUES (2, 'bob@example.com');
-- INSERT INTO users VALUES (3, 'alice@example.com');
-- Error: UNIQUE constraint failed: users.email

Multi-column index

CREATE TABLE shipments (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER,
  city TEXT,
  state TEXT,
  zip TEXT
);

CREATE INDEX idx_shipments_location ON shipments(state, city, zip);
-- Useful for queries that filter by state, or by state + city, or by all three.

Index with ASC and DESC

CREATE TABLE transactions (
  id INTEGER PRIMARY KEY,
  account_id INTEGER,
  created_at TEXT,
  amount REAL
);

CREATE INDEX idx_transactions_recent ON transactions(account_id, created_at DESC);

INSERT INTO transactions VALUES (1, 100, '2025-01-15', 250.00);
INSERT INTO transactions VALUES (2, 100, '2025-03-20', 75.00);
INSERT INTO transactions VALUES (3, 100, '2025-06-01', 300.00);

-- The index stores rows per account with the most recent date first.
SELECT created_at, amount FROM transactions
  WHERE account_id = 100
  ORDER BY created_at DESC;
-- 2025-06-01|300.0
-- 2025-03-20|75.0
-- 2025-01-15|250.0

Partial index

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER,
  status TEXT,
  amount REAL
);

CREATE INDEX idx_orders_pending ON orders(customer_id, amount)
  WHERE status = 'pending';

INSERT INTO orders VALUES (1, 10, 'pending', 99.99);
INSERT INTO orders VALUES (2, 10, 'shipped', 149.99);
INSERT INTO orders VALUES (3, 20, 'pending', 49.99);

-- This query can use the partial index because its WHERE clause
-- includes the index's condition.
SELECT id, customer_id, amount FROM orders
  WHERE status = 'pending'
  ORDER BY amount DESC;
-- 1|10|99.99
-- 3|20|49.99

Expression index

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  first_name TEXT,
  last_name TEXT,
  email TEXT
);

CREATE INDEX idx_users_email_lower ON users(lower(email));

INSERT INTO users VALUES (1, 'Alice', 'Smith', 'Alice@Example.COM');
INSERT INTO users VALUES (2, 'Bob', 'Jones', 'bob@example.com');

-- The expression index speeds up case-insensitive email lookups.
SELECT first_name, email FROM users
  WHERE lower(email) = 'alice@example.com';
-- Alice|Alice@Example.COM

Collation in an index

CREATE TABLE contacts (
  id INTEGER PRIMARY KEY,
  first_name TEXT,
  last_name TEXT
);

CREATE INDEX idx_contacts_fullname
  ON contacts(first_name COLLATE NOCASE, last_name COLLATE NOCASE);
-- Queries using case-insensitive comparisons on these columns can use this index.

IF NOT EXISTS

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  category TEXT,
  price REAL
);

CREATE INDEX IF NOT EXISTS idx_products_name ON products(name);
-- Running the same statement again does not produce an error.
CREATE INDEX IF NOT EXISTS idx_products_name ON products(name);

Dropping an index

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  price REAL
);

CREATE INDEX idx_products_name ON products(name);

-- Remove the index
DROP INDEX idx_products_name;

-- Safe to run even if the index does not exist
DROP INDEX IF EXISTS idx_products_name;

NULL values in a unique index

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  email TEXT
);

CREATE UNIQUE INDEX idx_users_email ON users(email);

-- Multiple NULLs are allowed because NULL values are considered distinct.
INSERT INTO users VALUES (1, NULL);
INSERT INTO users VALUES (2, NULL);

SELECT * FROM users;
-- 1|
-- 2|

CREATE VIEW

Syntax

CREATE [TEMP | TEMPORARY] VIEW [IF NOT EXISTS] view-name
  AS select-stmt
DROP VIEW [IF EXISTS] view-name

Description

The CREATE VIEW statement assigns a name to a pre-packaged SELECT statement. Once created, a view can be used anywhere a table name is accepted in the FROM clause of a SELECT: in simple queries, JOINs, subqueries, and even in the definitions of other views.

Views are read-only. You cannot use INSERT, UPDATE, or DELETE on a view. A view does not store data; every time it is referenced in a query, Turso expands it into the underlying SELECT and executes it against the current table data. This means the results of a view always reflect the latest state of the base tables.

The DROP VIEW statement removes a view definition from the database. It has no effect on the underlying tables or their data.

Clauses

AS select-stmt

The AS keyword is followed by any valid SELECT statement. This SELECT defines what the view returns when queried. The column names of the view are derived from the result columns of the SELECT. Use the AS alias syntax in the SELECT to give view columns well-defined names, particularly when the result includes expressions or function calls.

-- Without aliases, computed columns get auto-generated names
CREATE VIEW order_stats AS
  SELECT customer_id, COUNT(*), SUM(amount)
  FROM orders
  GROUP BY customer_id;

-- With aliases, column names are explicit and predictable
CREATE VIEW order_stats AS
  SELECT customer_id,
         COUNT(*) AS order_count,
         SUM(amount) AS total_spent
  FROM orders
  GROUP BY customer_id;

TEMP / TEMPORARY

If the TEMP or TEMPORARY keyword appears between CREATE and VIEW, the view is only visible to the current database connection and is automatically deleted when the connection is closed. Temporary views are useful for intermediate results within a session.

CREATE TEMP VIEW recent_orders AS
  SELECT * FROM orders WHERE created_at > '2024-01-01';

IF NOT EXISTS

When IF NOT EXISTS is included, the statement does not produce a fatal error if a view with the same name already exists. Without this clause, attempting to create a view that already exists raises an error.

IF EXISTS (DROP VIEW)

When IF EXISTS is included in a DROP VIEW statement, no error is raised if the named view does not exist. Without this clause, dropping a nonexistent view produces an error.

-- Safe to run even if the view does not exist
DROP VIEW IF EXISTS old_report;

Examples

Basic view

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  price REAL,
  category TEXT
);

INSERT INTO products VALUES (1, 'Laptop', 999.99, 'Electronics');
INSERT INTO products VALUES (2, 'Headphones', 49.99, 'Electronics');
INSERT INTO products VALUES (3, 'Notebook', 5.99, 'Office');
INSERT INTO products VALUES (4, 'Pen', 1.99, 'Office');

-- Create a view that filters to one category
CREATE VIEW electronics AS
  SELECT id, name, price
  FROM products
  WHERE category = 'Electronics';

SELECT * FROM electronics;
-- 1|Laptop|999.99
-- 2|Headphones|49.99

View with computed columns

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  price REAL,
  quantity INTEGER
);

INSERT INTO products VALUES (1, 'Laptop', 999.99, 5);
INSERT INTO products VALUES (2, 'Mouse', 29.99, 50);

CREATE VIEW inventory_value AS
  SELECT name, price, quantity, price * quantity AS total_value
  FROM products;

SELECT * FROM inventory_value;
-- Laptop|999.99|5|4999.95
-- Mouse|29.99|50|1499.5

View with aggregation

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER,
  product TEXT,
  amount REAL
);

INSERT INTO orders VALUES (1, 10, 'Laptop', 999.99);
INSERT INTO orders VALUES (2, 10, 'Mouse', 29.99);
INSERT INTO orders VALUES (3, 20, 'Keyboard', 79.99);
INSERT INTO orders VALUES (4, 20, 'Monitor', 399.99);
INSERT INTO orders VALUES (5, 20, 'Cable', 9.99);

CREATE VIEW customer_totals AS
  SELECT customer_id,
         COUNT(*) AS order_count,
         SUM(amount) AS total_spent
  FROM orders
  GROUP BY customer_id;

SELECT * FROM customer_totals;
-- 10|2|1029.98
-- 20|3|489.97

View with a JOIN

CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount REAL);

INSERT INTO customers VALUES (1, 'Alice');
INSERT INTO customers VALUES (2, 'Bob');
INSERT INTO orders VALUES (1, 1, 100.00);
INSERT INTO orders VALUES (2, 1, 200.00);
INSERT INTO orders VALUES (3, 2, 150.00);

CREATE VIEW order_details AS
  SELECT c.name, o.amount
  FROM customers c
  JOIN orders o ON c.id = o.customer_id;

SELECT * FROM order_details;
-- Alice|100.0
-- Alice|200.0
-- Bob|150.0

Querying a view with additional clauses

A view can be used just like a table in a SELECT. You can apply WHERE, ORDER BY, LIMIT, and any other clause on top of it.

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  price REAL,
  category TEXT
);

INSERT INTO products VALUES (1, 'Laptop', 999.99, 'Electronics');
INSERT INTO products VALUES (2, 'Mouse', 29.99, 'Electronics');
INSERT INTO products VALUES (3, 'Notebook', 5.99, 'Office');

CREATE VIEW electronics AS
  SELECT id, name, price
  FROM products
  WHERE category = 'Electronics';

-- Filter the view further
SELECT * FROM electronics WHERE price > 50;
-- 1|Laptop|999.99

-- Sort the view results
SELECT * FROM electronics ORDER BY price DESC;
-- 1|Laptop|999.99
-- 2|Mouse|29.99

Joining a table with a view

CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, amount REAL);

INSERT INTO customers VALUES (1, 'Alice');
INSERT INTO customers VALUES (2, 'Bob');
INSERT INTO orders VALUES (1, 1, 100.00);
INSERT INTO orders VALUES (2, 1, 200.00);
INSERT INTO orders VALUES (3, 2, 150.00);

CREATE VIEW customer_totals AS
  SELECT customer_id, SUM(amount) AS total
  FROM orders
  GROUP BY customer_id;

SELECT c.name, ct.total
  FROM customers c
  JOIN customer_totals ct ON c.id = ct.customer_id
  ORDER BY ct.total DESC;
-- Alice|300.0
-- Bob|150.0

View referencing another view

Views can be built on top of other views.

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  price REAL,
  category TEXT
);

INSERT INTO products VALUES (1, 'Laptop', 999.99, 'Electronics');
INSERT INTO products VALUES (2, 'Mouse', 29.99, 'Electronics');
INSERT INTO products VALUES (3, 'Notebook', 5.99, 'Office');

CREATE VIEW electronics AS
  SELECT id, name, price
  FROM products
  WHERE category = 'Electronics';

CREATE VIEW expensive_electronics AS
  SELECT * FROM electronics WHERE price > 100;

SELECT * FROM expensive_electronics;
-- 1|Laptop|999.99

Using a view in a subquery

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  price REAL,
  category TEXT
);

INSERT INTO products VALUES (1, 'Laptop', 999.99, 'Electronics');
INSERT INTO products VALUES (2, 'Mouse', 29.99, 'Electronics');
INSERT INTO products VALUES (3, 'Notebook', 5.99, 'Office');

CREATE VIEW electronics AS
  SELECT id, name, price
  FROM products
  WHERE category = 'Electronics';

SELECT name FROM products WHERE id IN (SELECT id FROM electronics);
-- Laptop
-- Mouse

Temporary view

CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price REAL);

INSERT INTO products VALUES (1, 'Laptop', 999.99);
INSERT INTO products VALUES (2, 'Pen', 1.99);

CREATE TEMP VIEW cheap_products AS
  SELECT id, name, price
  FROM products
  WHERE price < 100;

SELECT * FROM cheap_products;
-- 2|Pen|1.99

Dropping a view

CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT);
CREATE VIEW all_products AS SELECT * FROM products;

-- Drop the view
DROP VIEW all_products;

-- Safe to run even if the view does not exist
DROP VIEW IF EXISTS all_products;

Views are read-only

Attempting to INSERT, UPDATE, or DELETE against a view produces an error.

CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price REAL);
INSERT INTO products VALUES (1, 'Laptop', 999.99);

CREATE VIEW all_products AS SELECT * FROM products;

-- All of the following produce errors:
-- INSERT INTO all_products VALUES (2, 'Mouse', 29.99);
-- UPDATE all_products SET price = 500 WHERE id = 1;
-- DELETE FROM all_products WHERE id = 1;

Compatibility

Turso supports CREATE VIEW with the same syntax as SQLite, with the following notes:

  • Column name list: The parenthesized column name list after the view name (e.g., CREATE VIEW v(a, b) AS SELECT ...) is parsed but the specified column names are not applied to the view output. Use AS aliases in the SELECT statement instead to control column names.

  • IF NOT EXISTS: The IF NOT EXISTS clause is accepted by the parser. When a view with the same name already exists, a diagnostic message is emitted but execution continues.

CREATE TRIGGER

Syntax

CREATE TRIGGER [IF NOT EXISTS] trigger-name
  [BEFORE | AFTER] {DELETE | INSERT | UPDATE [OF column-name [, ...]]}
  ON table-name
  [FOR EACH ROW]
  [WHEN expr]
BEGIN
  statement; [statement; ...]
END;

Description

The CREATE TRIGGER statement defines a trigger – a set of SQL statements that automatically execute in response to INSERT, UPDATE, or DELETE operations on a specified table. Triggers are useful for enforcing business rules, maintaining audit logs, synchronizing related tables, and computing derived values.

Turso supports BEFORE and AFTER triggers on tables. Each trigger fires once per affected row (FOR EACH ROW semantics). The trigger body can contain one or more INSERT, UPDATE, DELETE, or SELECT statements, which execute as part of the same transaction as the triggering statement.

Triggers require the --experimental-triggers flag when starting the Turso CLI.

Clauses

Trigger Timing

The optional timing keyword controls when the trigger body executes relative to the triggering operation.

TimingBehavior
BEFOREExecutes before the row is modified. Default if no timing is specified.
AFTERExecutes after the row has been modified.

When no timing keyword is provided, BEFORE is used by default.

Trigger Event

The event determines which data modification operation causes the trigger to fire.

EventDescription
INSERTFires when a new row is inserted into the table.
UPDATEFires when any column of a row is updated.
UPDATE OF column-name [, ...]Fires only when one of the specified columns appears in the SET clause of an UPDATE statement.
DELETEFires when a row is deleted from the table.

For UPDATE OF, the trigger fires based on which columns appear in the SET clause, not on whether the column value actually changes. Column names that do not exist in the table are silently ignored.

FOR EACH ROW

Turso supports only row-level triggers. The FOR EACH ROW clause is optional and has no effect – row-level semantics are always used. The trigger body executes once for each row affected by the triggering statement.

WHEN

The optional WHEN clause provides a condition that is evaluated for each row. If the condition evaluates to false or NULL, the trigger body is skipped for that row. The WHEN expression can reference NEW and OLD row values (see below).

NEW and OLD References

Inside a trigger body and WHEN clause, the special table references NEW and OLD provide access to the row values being modified.

EventNEWOLD
INSERTThe row being inserted.Not available.
UPDATEThe row after the update.The row before the update.
DELETENot available.The row being deleted.

Use dot notation to reference individual columns: NEW.column_name or OLD.column_name. Both NEW.rowid and named rowid aliases (e.g., NEW.id for an INTEGER PRIMARY KEY column) are supported.

IF NOT EXISTS

When IF NOT EXISTS is included, Turso silently does nothing if a trigger with the same name already exists. Without this clause, attempting to create a trigger whose name is already in use results in an error.

Trigger Body

The trigger body is enclosed between BEGIN and END and contains one or more SQL statements, each terminated by a semicolon. The supported statement types are:

  • INSERT
  • UPDATE
  • DELETE
  • SELECT

All statements in the trigger body execute within the same transaction as the triggering statement. If any statement in the trigger body fails, the entire triggering operation is rolled back.

Trigger Execution Order

When multiple triggers are defined on the same table for the same event and timing, they fire in reverse order of creation (last created fires first).

Nested and Recursive Triggers

A trigger’s body can cause other triggers to fire. For example, an AFTER INSERT trigger on table A that inserts into table B will fire any insert triggers on table B.

Triggers can also be recursive – a trigger can modify the same table that caused it to fire. However, recursive triggers do not fire recursively by default; a trigger that fires once will not fire itself again in the same chain. This means a recursive AFTER INSERT trigger that inserts into the same table will execute at most one additional level.

Trigger Lifecycle

Triggers are automatically dropped when the table they are attached to is dropped. To manually remove a trigger, use DROP TRIGGER:

DROP TRIGGER [IF EXISTS] trigger-name;

Examples

Audit Log with AFTER INSERT

CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price REAL);
CREATE TABLE audit_log (id INTEGER PRIMARY KEY, action TEXT, product_name TEXT);

-- Log every new product insertion
CREATE TRIGGER log_product_insert AFTER INSERT ON products
BEGIN
  INSERT INTO audit_log (action, product_name) VALUES ('INSERT', NEW.name);
END;

INSERT INTO products (name, price) VALUES ('Laptop', 999.99);
INSERT INTO products (name, price) VALUES ('Mouse', 29.99);

SELECT * FROM audit_log;
-- 1|INSERT|Laptop
-- 2|INSERT|Mouse

Tracking Changes with OLD and NEW

CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, salary REAL);
CREATE TABLE salary_changes (id INTEGER PRIMARY KEY, old_salary REAL, new_salary REAL);

CREATE TRIGGER log_salary_update AFTER UPDATE ON employees
BEGIN
  INSERT INTO salary_changes (old_salary, new_salary)
    VALUES (OLD.salary, NEW.salary);
END;

INSERT INTO employees VALUES (1, 'Alice', 75000.0);
UPDATE employees SET salary = 80000.0 WHERE id = 1;

SELECT * FROM salary_changes;
-- 1|75000.0|80000.0

Archiving Deleted Rows with BEFORE DELETE

CREATE TABLE orders (id INTEGER PRIMARY KEY, customer TEXT, total REAL);
CREATE TABLE deleted_orders (id INTEGER PRIMARY KEY, customer TEXT, total REAL);

CREATE TRIGGER archive_deleted_order BEFORE DELETE ON orders
BEGIN
  INSERT INTO deleted_orders (id, customer, total) VALUES (OLD.id, OLD.customer, OLD.total);
END;

INSERT INTO orders VALUES (1, 'Bob', 150.00);
INSERT INTO orders VALUES (2, 'Carol', 250.00);
DELETE FROM orders WHERE customer = 'Bob';

SELECT * FROM deleted_orders;
-- 1|Bob|150.0

Conditional Trigger with WHEN Clause

CREATE TABLE sensor_readings (id INTEGER PRIMARY KEY, value INTEGER);
CREATE TABLE alerts (id INTEGER PRIMARY KEY, reading_id INTEGER);

-- Only log an alert when the reading exceeds a threshold
CREATE TRIGGER high_value_alert AFTER INSERT ON sensor_readings
WHEN NEW.value > 100
BEGIN
  INSERT INTO alerts (reading_id) VALUES (NEW.id);
END;

INSERT INTO sensor_readings VALUES (1, 50);
INSERT INTO sensor_readings VALUES (2, 150);
INSERT INTO sensor_readings VALUES (3, 75);

SELECT * FROM alerts;
-- 1|2

UPDATE OF Specific Columns

CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT, password_hash TEXT);
CREATE TABLE security_log (id INTEGER PRIMARY KEY, msg TEXT);

-- Only fire when the password column is updated
CREATE TRIGGER log_password_change AFTER UPDATE OF password_hash ON accounts
BEGIN
  INSERT INTO security_log (msg) VALUES ('password changed for account ' || NEW.id);
END;

INSERT INTO accounts VALUES (1, 'alice@example.com', 'hash1');
UPDATE accounts SET email = 'alice-new@example.com' WHERE id = 1;
UPDATE accounts SET password_hash = 'hash2' WHERE id = 1;

SELECT * FROM security_log;
-- 1|password changed for account 1

Multiple Statements in Trigger Body

CREATE TABLE inventory (id INTEGER PRIMARY KEY, product TEXT, quantity INTEGER);
CREATE TABLE restock_log (id INTEGER PRIMARY KEY, product TEXT);
CREATE TABLE quantity_log (id INTEGER PRIMARY KEY, product TEXT, qty INTEGER);

CREATE TRIGGER track_inventory AFTER INSERT ON inventory
BEGIN
  INSERT INTO restock_log (product) VALUES (NEW.product);
  INSERT INTO quantity_log (product, qty) VALUES (NEW.product, NEW.quantity);
END;

INSERT INTO inventory VALUES (1, 'Widget', 100);

SELECT * FROM restock_log;
-- 1|Widget
SELECT * FROM quantity_log;
-- 1|Widget|100

Cascading Triggers Across Tables

CREATE TABLE departments (id INTEGER PRIMARY KEY, budget INTEGER);
CREATE TABLE projects (id INTEGER PRIMARY KEY, dept_id INTEGER, cost INTEGER);
CREATE TABLE notifications (id INTEGER PRIMARY KEY, msg TEXT);

INSERT INTO departments VALUES (1, 50000);

-- When a project is added, update department budget
CREATE TRIGGER deduct_budget AFTER INSERT ON projects
BEGIN
  UPDATE departments SET budget = budget - NEW.cost WHERE id = NEW.dept_id;
END;

-- When budget changes, log a notification
CREATE TRIGGER budget_notification AFTER UPDATE ON departments
BEGIN
  INSERT INTO notifications (msg) VALUES ('budget updated: ' || NEW.budget);
END;

INSERT INTO projects VALUES (1, 1, 10000);

SELECT * FROM departments;
-- 1|40000
SELECT * FROM notifications;
-- 1|budget updated: 40000

Complex WHEN Clause

CREATE TABLE transactions (id INTEGER PRIMARY KEY, amount INTEGER, status TEXT DEFAULT 'pending');

-- Automatically mark large transactions as requiring review
CREATE TRIGGER flag_large_transactions AFTER INSERT ON transactions
WHEN NEW.amount > 500 AND NEW.amount < 10000
BEGIN
  UPDATE transactions SET status = 'review' WHERE id = NEW.id;
END;

INSERT INTO transactions (id, amount) VALUES (1, 100);
INSERT INTO transactions (id, amount) VALUES (2, 750);
INSERT INTO transactions (id, amount) VALUES (3, 50000);

SELECT * FROM transactions ORDER BY id;
-- 1|100|pending
-- 2|750|review
-- 3|50000|pending

IF NOT EXISTS

CREATE TABLE events (id INTEGER PRIMARY KEY, name TEXT);

CREATE TRIGGER IF NOT EXISTS log_event BEFORE INSERT ON events
BEGIN
  SELECT 1;
END;

-- This does not produce an error because of IF NOT EXISTS
CREATE TRIGGER IF NOT EXISTS log_event BEFORE INSERT ON events
BEGIN
  SELECT 1;
END;

SELECT name FROM sqlite_schema WHERE type = 'trigger' AND name = 'log_event';
-- log_event

Compatibility

  • Triggers are an experimental feature and require the --experimental-triggers flag.
  • INSTEAD OF triggers (used with views) are not yet supported.
  • TEMPORARY triggers are not yet supported.
  • The RAISE() function is not supported within trigger bodies.
  • Recursive triggers are limited to a single additional level of recursion by default.

ALTER TABLE

Syntax

ALTER TABLE table-name RENAME TO new-table-name
ALTER TABLE table-name RENAME [COLUMN] column-name TO new-column-name
ALTER TABLE table-name ADD [COLUMN] column-def
ALTER TABLE table-name DROP [COLUMN] column-name
ALTER TABLE table-name ALTER COLUMN column-name TO column-def

Description

The ALTER TABLE statement modifies the schema of an existing table. Turso supports five operations: renaming a table, renaming a column, adding a column, dropping a column, and altering a column definition. Unlike CREATE TABLE, ALTER TABLE works on tables that already exist and may already contain data.

Each operation modifies the table’s schema entry in sqlite_schema. Renaming and adding columns are fast operations whose execution time is independent of the number of rows. Dropping a column rewrites the table data and is proportional to table size.

System tables (those with names beginning with sqlite_) cannot be altered.

Operations

RENAME TO

Renames the table from table-name to new-table-name. The new name must not collide with any existing table, view, or index name. The table cannot be moved between attached databases – it is only renamed within its current database.

References to the table in triggers, views, and foreign key constraints are automatically updated to use the new name, including self-referencing foreign keys.

CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT);
ALTER TABLE employees RENAME TO staff;
SELECT name FROM sqlite_schema WHERE type = 'table';
-- staff

RENAME COLUMN

Renames an existing column from column-name to new-column-name. The COLUMN keyword is optional. All references to the column in indexes, triggers, views, and foreign key constraints (both child and parent sides) are automatically updated.

If the rename would introduce a semantic ambiguity in a trigger or view, the operation fails and no changes are applied.

CREATE TABLE products (id INTEGER PRIMARY KEY, product_name TEXT NOT NULL, price REAL);
ALTER TABLE products RENAME COLUMN product_name TO name;
SELECT sql FROM sqlite_schema WHERE name = 'products';
-- CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL)

Renaming a column that participates in a foreign key constraint updates both sides of the relationship:

PRAGMA foreign_keys = ON;
CREATE TABLE orders (order_id INTEGER PRIMARY KEY, date TEXT);
CREATE TABLE items (item_id INTEGER PRIMARY KEY, oid INTEGER,
  FOREIGN KEY (oid) REFERENCES orders(order_id));

ALTER TABLE orders RENAME COLUMN order_id TO ord_id;

SELECT sql FROM sqlite_schema WHERE name = 'items';
-- CREATE TABLE items (item_id INTEGER PRIMARY KEY, oid INTEGER, FOREIGN KEY (oid) REFERENCES orders (ord_id))

ADD COLUMN

Appends a new column to the end of the table’s column list. The COLUMN keyword is optional. The column-def follows the same syntax as a column definition in CREATE TABLE, including an optional type name, DEFAULT, NOT NULL, CHECK, COLLATE, and REFERENCES clauses.

CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER);
ALTER TABLE orders ADD COLUMN total_amount REAL DEFAULT 0.0;
ALTER TABLE orders ADD COLUMN status TEXT DEFAULT 'pending';
SELECT sql FROM sqlite_schema WHERE name = 'orders';
-- CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, total_amount REAL DEFAULT 0.0, status TEXT DEFAULT 'pending')

Restrictions

The new column may not have:

RestrictionReason
PRIMARY KEY or UNIQUE constraintWould require rewriting existing data and indexes.
A non-constant default expressionThe default must be a literal, a signed literal, or a parenthesized constant. Expressions like (NULL + 5) are rejected.
NOT NULL without a non-null default (when the table has rows)Existing rows would have NULL for the new column, violating the constraint.
CURRENT_TIME, CURRENT_DATE, or CURRENT_TIMESTAMP as default (when the table has rows)These are non-deterministic and cannot be used to backfill existing rows.
GENERATED ALWAYS ... STORED or AS (expr)Adding generated columns via ALTER TABLE is not supported.

A NOT NULL column without a default value is permitted if the table is empty:

CREATE TABLE contacts (id INTEGER PRIMARY KEY);
ALTER TABLE contacts ADD name TEXT NOT NULL;
SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = 'contacts';
-- CREATE TABLE contacts (id INTEGER PRIMARY KEY, name TEXT NOT NULL)

A NOT NULL column with a non-null default succeeds even on tables with existing rows:

CREATE TABLE tasks (id INTEGER PRIMARY KEY);
INSERT INTO tasks VALUES (1);
ALTER TABLE tasks ADD priority INTEGER NOT NULL DEFAULT 5;
INSERT INTO tasks (id) VALUES (2);
SELECT * FROM tasks ORDER BY id;
-- 1|5
-- 2|5

Adding a column with a foreign key reference is supported and updates the schema to include a table-level FOREIGN KEY clause:

CREATE TABLE departments (id INTEGER PRIMARY KEY);
CREATE TABLE staff (id INTEGER PRIMARY KEY);
ALTER TABLE staff ADD COLUMN dept_id REFERENCES departments(id);
SELECT sql FROM sqlite_schema WHERE name = 'staff';
-- CREATE TABLE staff (id INTEGER PRIMARY KEY, dept_id, FOREIGN KEY (dept_id) REFERENCES departments(id))

Duplicate column names are rejected (case-insensitive):

CREATE TABLE items (name TEXT);
ALTER TABLE items ADD COLUMN name TEXT;
-- Error: duplicate column name

DROP COLUMN

Removes a column from the table and rewrites the table data to exclude the dropped column’s values. The COLUMN keyword is optional.

CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, email TEXT, phone TEXT);
INSERT INTO customers VALUES (1, 'Alice', 'alice@example.com', '555-0100');
ALTER TABLE customers DROP COLUMN phone;
SELECT * FROM customers;
-- 1|Alice|alice@example.com

Restrictions

A column cannot be dropped if it:

RestrictionReason
Is a PRIMARY KEY or part of oneThe primary key is essential to the table’s identity.
Has a UNIQUE constraintA unique index depends on the column.
Is referenced by an indexThe index would become invalid.
Is referenced by a table-level CHECK constraintThe CHECK expression would reference a missing column.
Is used in a generated column expressionThe generated column would become invalid.

A column-level CHECK constraint attached to the dropped column is removed along with the column:

CREATE TABLE metrics (
  id INTEGER PRIMARY KEY,
  value REAL CHECK (value BETWEEN 0.0 AND 1000.0),
  label TEXT NOT NULL
);
INSERT INTO metrics VALUES (1, 500.0, 'temperature');
ALTER TABLE metrics DROP COLUMN value;
INSERT INTO metrics VALUES (2, 'humidity');
SELECT * FROM metrics;
-- 1|temperature
-- 2|humidity

ALTER COLUMN

This is a Turso extension not present in SQLite.

The ALTER COLUMN operation changes a column’s name and definition in a single statement. The column-def after TO is a full column definition (name, type, and constraints), replacing the old column definition entirely. Data in existing rows is preserved.

CREATE TABLE sensors (id INTEGER PRIMARY KEY, reading INTEGER);
CREATE INDEX idx_reading ON sensors (reading);
ALTER TABLE sensors ALTER COLUMN reading TO measurement BLOB;
SELECT sql FROM sqlite_schema;
-- CREATE TABLE sensors (id INTEGER PRIMARY KEY, measurement BLOB)
-- CREATE INDEX idx_reading ON sensors (measurement)

The new column definition may not include PRIMARY KEY or UNIQUE constraints:

CREATE TABLE config (key TEXT, value TEXT);
ALTER TABLE config ALTER COLUMN value TO value PRIMARY KEY;
-- Error: cannot add PRIMARY KEY via ALTER COLUMN

The new definition may specify a generated column expression, as long as at least one non-generated column remains in the table:

CREATE TABLE items (name TEXT, price REAL);
ALTER TABLE items ALTER COLUMN price TO computed_price AS (123);
SELECT sql FROM sqlite_schema WHERE name = 'items';
-- CREATE TABLE items (name TEXT, computed_price AS (123))

Examples

Evolving a Schema

A common workflow is creating a table and then adding columns as requirements change:

CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT NOT NULL);
INSERT INTO users VALUES (1, 'alice'), (2, 'bob');

ALTER TABLE users ADD email TEXT DEFAULT 'unknown';
ALTER TABLE users ADD created_at TEXT DEFAULT CURRENT_TIMESTAMP;

SELECT id, username, email FROM users ORDER BY id;
-- 1|alice|unknown
-- 2|bob|unknown

Renaming for Clarity

CREATE TABLE t (a INTEGER, b TEXT, c REAL);
CREATE INDEX idx_t_a ON t (a);

ALTER TABLE t RENAME COLUMN a TO user_id;
ALTER TABLE t RENAME COLUMN b TO user_name;
ALTER TABLE t RENAME COLUMN c TO balance;
ALTER TABLE t RENAME TO accounts;

SELECT sql FROM sqlite_schema ORDER BY type;
-- CREATE INDEX idx_t_a ON accounts (user_id)
-- CREATE TABLE accounts (user_id INTEGER, user_name TEXT, balance REAL)

Dropping Unused Columns

CREATE TABLE logs (id INTEGER PRIMARY KEY, message TEXT, debug_info TEXT);
INSERT INTO logs VALUES (1, 'Server started', 'verbose debug data');
INSERT INTO logs VALUES (2, 'User login', 'session details');

ALTER TABLE logs DROP COLUMN debug_info;
SELECT * FROM logs;
-- 1|Server started
-- 2|User login

Foreign Key Updates After Rename

When a parent table is renamed, foreign key references in child tables are updated automatically:

PRAGMA foreign_keys = ON;
CREATE TABLE categories (id INTEGER PRIMARY KEY);
CREATE TABLE products (id INTEGER PRIMARY KEY, cat_id INTEGER,
  FOREIGN KEY (cat_id) REFERENCES categories(id));

ALTER TABLE categories RENAME TO product_categories;

SELECT sql FROM sqlite_schema WHERE name = 'products';
-- CREATE TABLE products (id INTEGER PRIMARY KEY, cat_id INTEGER, FOREIGN KEY (cat_id) REFERENCES product_categories (id))

Compatibility

  • ALTER COLUMN is a Turso extension. This operation is not available in SQLite. It allows changing a column’s name, type, and constraints in a single statement.
  • Adding generated columns via ALTER TABLE ADD COLUMN is not supported. Use ALTER COLUMN to convert an existing column to a generated column, or recreate the table.

DROP

Syntax

DROP TABLE [IF EXISTS] table-name
DROP INDEX [IF EXISTS] index-name
DROP VIEW [IF EXISTS] view-name
DROP TRIGGER [IF EXISTS] trigger-name

Description

The DROP statement removes a database object (table, index, view, or trigger) from the database. The object and all its associated data are permanently deleted. This action cannot be undone.

If the IF EXISTS clause is included, the statement is a no-op when the named object does not exist. Without IF EXISTS, attempting to drop a nonexistent object raises an error.

DROP TABLE

Removes a table and all of its data, indexes, and triggers from the database.

-- Remove a table
DROP TABLE users;

-- Remove a table only if it exists (no error if missing)
DROP TABLE IF EXISTS users;

When a table is dropped:

  • All rows in the table are deleted.
  • All indexes associated with the table are removed.
  • All triggers associated with the table are removed.
  • Foreign key references to the table are not automatically updated. If other tables have foreign key constraints pointing to the dropped table, those constraints become orphaned.

DROP INDEX

Removes an index from the database. The underlying table data is not affected.

-- Remove an index
DROP INDEX idx_users_email;

-- Remove an index only if it exists
DROP INDEX IF EXISTS idx_users_email;

Dropping an index does not affect the data in the table — it only removes the index structure. Queries that previously used the index will still work, but may run slower without it.

DROP VIEW

Removes a view definition from the database. Since views do not store data, no data is deleted.

-- Remove a view
DROP VIEW active_users;

-- Remove a view only if it exists
DROP VIEW IF EXISTS active_users;

DROP TRIGGER

Removes a trigger from the database.

-- Remove a trigger
DROP TRIGGER audit_insert;

-- Remove a trigger only if it exists
DROP TRIGGER IF EXISTS audit_insert;

Examples

-- Create a table with an index, then drop both
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  price REAL
);
CREATE INDEX idx_products_name ON products(name);

-- Drop the index first, then the table
DROP INDEX idx_products_name;
DROP TABLE products;
-- Safely clean up objects that may or may not exist
DROP VIEW IF EXISTS sales_summary;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS customers;
-- Drop and recreate a table (reset)
DROP TABLE IF EXISTS temp_results;
CREATE TABLE temp_results (id INTEGER PRIMARY KEY, value TEXT);

Compatibility

FeatureStatus
DROP TABLESupported
DROP INDEXSupported
DROP VIEWSupported
DROP TRIGGERRequires --experimental-triggers flag
IF EXISTSSupported

Transactions

Syntax

BEGIN [DEFERRED | IMMEDIATE | EXCLUSIVE] [TRANSACTION]

COMMIT [TRANSACTION]

END [TRANSACTION]

ROLLBACK [TRANSACTION]

Description

A transaction is a sequence of SQL statements that are executed as a single atomic unit. Either all statements in the transaction complete successfully and their changes are made permanent, or none of them take effect. Transactions guarantee that the database moves from one consistent state to another, even in the presence of errors or unexpected termination.

Turso supports three commands for explicit transaction control: BEGIN starts a new transaction, COMMIT (or its alias END) makes all changes within the transaction permanent, and ROLLBACK discards all changes made since the transaction began.

Transactions in Turso do not nest. Issuing BEGIN while a transaction is already active will produce an error. For the same reason, issuing COMMIT or ROLLBACK outside of a transaction will also produce an error.

Autocommit Mode

When no explicit transaction is active, Turso operates in autocommit mode. In this mode every individual SQL statement that reads from or writes to the database is automatically wrapped in its own implicit transaction. The implicit transaction is committed as soon as the statement finishes executing.

This means a single INSERT, UPDATE, or DELETE statement executed outside of an explicit transaction is atomic by itself – it either fully succeeds or has no effect. However, if you need multiple statements to succeed or fail together, you must wrap them in an explicit BEGINCOMMIT block.

-- Autocommit mode: each statement is its own transaction
CREATE TABLE orders (id INTEGER PRIMARY KEY, product TEXT, qty INTEGER);
INSERT INTO orders VALUES (1, 'Widget', 10);
INSERT INTO orders VALUES (2, 'Gadget', 5);
-- Both rows are committed independently

Transaction Types

The BEGIN statement accepts an optional keyword that controls when the transaction acquires its lock on the database.

TypeBehavior
DEFERREDThe default. The transaction does not acquire any lock until the database is first accessed. A read statement starts a read transaction; a write statement starts a write transaction.
IMMEDIATEA write lock is acquired immediately when BEGIN IMMEDIATE is executed, without waiting for the first write statement. This guarantees that no other connection can write to the database while this transaction is open.
EXCLUSIVEBehaves the same as IMMEDIATE under WAL mode, which is the journaling mode used by Turso. A write lock is acquired immediately.

When the transaction type is omitted, DEFERRED is assumed.

DEFERRED

A deferred transaction does not acquire any database lock when BEGIN is executed. The lock is acquired lazily the first time the database is actually read from or written to within the transaction. If the first operation is a SELECT, a read lock is acquired. If the first operation is an INSERT, UPDATE, DELETE, or other write statement, a write lock is acquired.

-- DEFERRED is the default; these two are equivalent
BEGIN TRANSACTION;
-- ...
COMMIT;

BEGIN DEFERRED TRANSACTION;
-- ...
COMMIT;

IMMEDIATE

An immediate transaction acquires a write lock as soon as BEGIN IMMEDIATE is executed. This is useful when you know the transaction will perform writes, because it avoids a potential conflict that could occur if a deferred transaction tries to upgrade from a read lock to a write lock after another connection has already started writing.

BEGIN IMMEDIATE TRANSACTION;
-- Write lock is held from this point
INSERT INTO orders (product, qty) VALUES ('Keyboard', 75);
COMMIT;

EXCLUSIVE

Under WAL mode – the mode Turso uses – EXCLUSIVE behaves identically to IMMEDIATE. Both acquire a write lock immediately. In other journaling modes (not used by Turso), EXCLUSIVE would additionally prevent other connections from reading the database, but this distinction does not apply here.

BEGIN EXCLUSIVE;
INSERT INTO orders (product, qty) VALUES ('Mouse', 200);
END;

COMMIT and END

COMMIT makes all changes performed within the current transaction permanent. END is an alias for COMMIT; they are interchangeable.

The optional TRANSACTION keyword after COMMIT or END is purely decorative and has no effect on behavior.

-- These are all equivalent
COMMIT;
COMMIT TRANSACTION;
END;
END TRANSACTION;

ROLLBACK

ROLLBACK discards all changes made within the current transaction. The database is restored to the state it was in before BEGIN was executed.

The optional TRANSACTION keyword after ROLLBACK is purely decorative and has no effect on behavior.

CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT, balance REAL);
INSERT INTO accounts VALUES (1, 'Alice', 1000.00);
INSERT INTO accounts VALUES (2, 'Bob', 500.00);

BEGIN;
UPDATE accounts SET balance = balance - 200.00 WHERE name = 'Alice';
UPDATE accounts SET balance = balance + 200.00 WHERE name = 'Bob';
-- At this point, Alice has 800 and Bob has 700 within the transaction
ROLLBACK;

SELECT * FROM accounts;
-- Alice still has 1000.00 and Bob still has 500.00

Examples

Atomic money transfer

Wrapping related updates in a transaction ensures that a transfer between accounts either fully completes or has no effect.

CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT, balance REAL);
INSERT INTO accounts VALUES (1, 'Alice', 1000.00);
INSERT INTO accounts VALUES (2, 'Bob', 500.00);

BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 200.00 WHERE name = 'Alice';
UPDATE accounts SET balance = balance + 200.00 WHERE name = 'Bob';
COMMIT;

SELECT * FROM accounts;
-- 1|Alice|800.0
-- 2|Bob|700.0

Batch inserts within a transaction

Grouping multiple inserts into a single transaction is significantly faster than executing each insert in autocommit mode, because the database only needs to sync to disk once at COMMIT rather than after every individual statement.

CREATE TABLE inventory (id INTEGER PRIMARY KEY, product TEXT NOT NULL, qty INTEGER DEFAULT 0);

BEGIN;
INSERT INTO inventory (product, qty) VALUES ('Laptop', 50);
INSERT INTO inventory (product, qty) VALUES ('Mouse', 200);
INSERT INTO inventory (product, qty) VALUES ('Keyboard', 75);
COMMIT;

SELECT * FROM inventory;
-- 1|Laptop|50
-- 2|Mouse|200
-- 3|Keyboard|75

Rolling back on error

If something goes wrong during a sequence of operations, ROLLBACK ensures that partially applied changes do not corrupt the database.

CREATE TABLE orders (id INTEGER PRIMARY KEY, product TEXT, qty INTEGER);

BEGIN;
INSERT INTO orders VALUES (1, 'Widget', 10);
INSERT INTO orders VALUES (2, 'Gadget', 5);
-- Decide to discard these changes
ROLLBACK;

SELECT count(*) FROM orders;
-- 0

Using IMMEDIATE for write-heavy work

When you know a transaction will write to the database, starting with BEGIN IMMEDIATE avoids the overhead and potential failure of upgrading a read lock to a write lock mid-transaction.

CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price REAL);

BEGIN IMMEDIATE;
INSERT INTO products VALUES (1, 'Widget', 9.99);
INSERT INTO products VALUES (2, 'Gadget', 24.95);
END;

SELECT * FROM products;
-- 1|Widget|9.99
-- 2|Gadget|24.95

END as an alias for COMMIT

END and COMMIT are interchangeable. Use whichever reads more naturally in your application.

CREATE TABLE events (id INTEGER PRIMARY KEY, description TEXT);

BEGIN DEFERRED TRANSACTION;
INSERT INTO events (description) VALUES ('user_login');
INSERT INTO events (description) VALUES ('page_view');
END TRANSACTION;

SELECT * FROM events;
-- 1|user_login
-- 2|page_view

Compatibility

Turso supports BEGIN, COMMIT / END, and ROLLBACK with the same syntax and semantics as SQLite. The TRANSACTION keyword is optional in all three commands, matching SQLite behavior.

SAVEPOINT and RELEASE SAVEPOINT are not supported. Nested transactions using savepoints are not available.

Turso operates exclusively in WAL (Write-Ahead Logging) mode. As a result, BEGIN EXCLUSIVE and BEGIN IMMEDIATE behave identically – both acquire a write lock immediately. The distinction between these two modes that exists in other SQLite journaling modes does not apply.

EXPLAIN

Syntax

EXPLAIN sql-statement

EXPLAIN QUERY PLAN sql-statement

Description

The EXPLAIN statement is a diagnostic tool for understanding how Turso executes a SQL statement. It does not run the statement itself. Instead, it returns metadata about the execution strategy.

There are two forms. EXPLAIN returns the full sequence of virtual machine (VDBE) bytecode instructions that Turso would execute for the given statement. EXPLAIN QUERY PLAN returns a high-level summary of the query plan, showing which tables and indexes are accessed and in what order. Both forms are intended for interactive analysis, debugging, and performance tuning.

The EXPLAIN prefix can be applied to any SQL statement, including SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, and others. The prefixed statement is compiled but never executed, so it has no side effects.

Output Columns

EXPLAIN

EXPLAIN returns one row per bytecode instruction. Each row has eight columns:

ColumnTypeDescription
addrINTEGERThe instruction address (sequential, starting at 0).
opcodeTEXTThe name of the VDBE opcode (e.g., Init, OpenRead, Column, ResultRow).
p1INTEGERFirst operand. Meaning varies by opcode.
p2INTEGERSecond operand. Often a jump target or register number.
p3INTEGERThird operand.
p4TEXTFourth operand. May contain a string constant, function name, key format, or table/index name.
p5INTEGERFifth operand. Typically contains flags.
commentTEXTA human-readable comment describing what the instruction does.

The bytecode format is an internal implementation detail and may change between Turso releases. Do not write application logic that depends on specific opcodes or instruction sequences.

EXPLAIN QUERY PLAN

EXPLAIN QUERY PLAN returns one row per step in the query plan. Each row has four columns:

ColumnTypeDescription
idINTEGERA unique identifier for this node in the plan tree.
parentINTEGERThe id of the parent node (0 for root nodes).
notusedINTEGERReserved. Always 0.
detailTEXTA human-readable description of the operation at this step.

The detail column contains the most useful information. Common values include:

Detail prefixMeaning
SCAN table-nameA full table scan with no index.
SEARCH table-name USING INDEX index-nameAn indexed lookup.
SEARCH table-name USING INTEGER PRIMARY KEY (rowid=?)A direct rowid lookup via the primary key.
USE TEMP B-TREE FOR ORDER BYA temporary structure is used to sort results.
SCAN CONSTANT ROWA row is produced from constant values (no table access).

When a query involves multiple tables (joins, subqueries), EXPLAIN QUERY PLAN returns multiple rows showing the access order and method for each table.

Interpreting Query Plans

The output of EXPLAIN QUERY PLAN is the primary tool for diagnosing slow queries. The key things to look for:

  • SCAN vs. SEARCH: A SCAN reads every row in the table. A SEARCH uses an index to jump directly to matching rows. If a query is slow, look for unexpected SCAN operations on large tables and consider adding an index.
  • Index usage: When an index is used, the detail line names it. Verify that the expected index is being chosen.
  • Temporary B-trees: Lines mentioning USE TEMP B-TREE indicate that Turso must build a temporary data structure (for sorting, grouping, or deduplication). This is normal for ORDER BY on non-indexed columns but can be a performance concern for large result sets.
  • Join order: In multi-table queries, the rows appear in the order that tables are accessed. The first table listed is the outer loop; subsequent tables are inner loops. The optimizer chooses the join order it estimates to be fastest.

Examples

Bytecode for a Simple Query

EXPLAIN SELECT 1;
-- addr  opcode             p1    p2    p3    p4             p5  comment
-- ----  -----------------  ----  ----  ----  -------------  --  -------
-- 0     Init               0     3     0                    0   Start at 3
-- 1     ResultRow          1     1     0                    0   output=r[1]
-- 2     Halt               0     0     0                    0
-- 3     Integer            1     1     0                    0   r[1]=1
-- 4     Goto               0     1     0                    0

The Init instruction jumps to address 3, where the integer 1 is loaded into register 1. Control then jumps to address 1, which outputs register 1 as a result row. Finally, Halt terminates execution.

Query Plan for a Full Table Scan

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER,
  product TEXT,
  amount REAL
);

EXPLAIN QUERY PLAN SELECT * FROM orders;
-- QUERY PLAN
-- `--SCAN orders

With no WHERE clause and no index needed, Turso performs a full scan of the orders table.

Query Plan with Index Usage

CREATE INDEX idx_orders_customer ON orders(customer_id);

EXPLAIN QUERY PLAN SELECT id, product, amount
FROM orders WHERE customer_id = 42;
-- QUERY PLAN
-- `--SEARCH orders USING INDEX idx_orders_customer

The SEARCH line shows that Turso uses the idx_orders_customer index to find rows where customer_id = 42, avoiding a full table scan.

Query Plan with Primary Key Lookup

EXPLAIN QUERY PLAN SELECT * FROM orders WHERE id = 10;
-- QUERY PLAN
-- `--SEARCH orders USING INTEGER PRIMARY KEY (rowid=?)

When filtering by the INTEGER PRIMARY KEY, Turso performs a direct rowid lookup, which is the fastest possible access method.

Query Plan for a Join

CREATE TABLE customers (
  id INTEGER PRIMARY KEY,
  name TEXT,
  email TEXT
);

EXPLAIN QUERY PLAN SELECT c.name, o.product, o.amount
FROM customers c
JOIN orders o ON c.id = o.customer_id
WHERE c.name = 'Alice';
-- QUERY PLAN
-- |--SCAN customers AS c
-- `--SEARCH o USING INDEX idx_orders_customer

Turso scans the customers table (the outer loop), and for each matching customer, uses the index on orders(customer_id) to find their orders (the inner loop).

Query Plan with GROUP BY and ORDER BY

EXPLAIN QUERY PLAN SELECT product, SUM(amount)
FROM orders
GROUP BY product
ORDER BY SUM(amount) DESC;
-- QUERY PLAN
-- |--SCAN orders
-- `--USE TEMP B-TREE FOR ORDER BY

The plan shows a full table scan to read and group the data, followed by a temporary B-tree to sort the grouped results by the aggregate value.

Bytecode for an INSERT

EXPLAIN INSERT INTO orders VALUES (1, 42, 'Widget', 9.99);
-- addr  opcode             p1    p2    p3    p4             p5  comment
-- ----  -----------------  ----  ----  ----  -------------  --  -------
-- 0     Init               0     20    0                    0   Start at 20
-- 1     OpenWrite          0     2     0                    0   root=2; iDb=0
-- 2     Integer            1     2     0                    0   r[2]=1
-- 3     SoftNull           3     0     0                    0
-- 4     Integer            42    4     0                    0   r[4]=42
-- 5     String8            0     5     0     Widget         0   r[5]='Widget'
-- 6     Real               0     6     0     9.99           0   r[6]=9.99
-- 7     NotNull            2     9     0                    0   r[2]!=NULL -> goto 9
-- 8     Goto               0     11    0                    0
-- 9     MustBeInt          2     0     0                    0
-- 10    Goto               0     12    0                    0
-- 11    NewRowid           0     2     0                    0   r[2]=rowid
-- 12    Affinity           3     4     0                    0   r[3..7] = D, D, B, E
-- 13    NotExists          0     15    2                    0
-- 14    Halt               1555  0     0     orders.id      0
-- 15    MakeRecord         3     4     7                    0   r[7]=mkrec(r[3..6])
-- 16    Insert             0     7     2     orders         0   intkey=r[2] data=r[7]
-- 17    Goto               0     18    0                    0
-- 18    Goto               0     19    0                    0
-- 19    Halt               0     0     0                    0
-- 20    Transaction        0     2     1                    0   iDb=0 tx_mode=Write
-- 21    Goto               0     1     0                    0

The bytecode shows the full instruction sequence: opening the table for writing, loading the values into registers, checking for primary key conflicts, assembling the record, and inserting it.

Operators

Operators perform arithmetic, comparison, logical, bitwise, and string operations on values in SQL expressions. Turso supports the full set of SQLite operators, including the standard SQL IS DISTINCT FROM and IS NOT DISTINCT FROM forms.

Operator Precedence

When an expression contains multiple operators, precedence determines the order of evaluation. Operators with higher precedence bind more tightly. Operators at the same precedence level are left-associative (evaluated left to right). Parentheses override precedence.

PrecedenceOperatorsCategory
1 (highest)~ + -Unary bitwise NOT, unary plus, unary minus
2||String concatenation
3* / %Multiplication, division, modulo
4+ -Addition, subtraction
5& | << >>Bitwise AND, OR, left shift, right shift
6< > <= >=Relational comparison
7= == <> IS IS NOT IS DISTINCT FROM IS NOT DISTINCT FROMEquality and identity
8NOTLogical NOT (unary)
9ANDLogical AND
10 (lowest)ORLogical OR
-- Multiplication binds tighter than addition
SELECT 2 + 3 * 4;    -- 14  (not 20)
SELECT (2 + 3) * 4;  -- 20  (parentheses override)

-- AND binds tighter than OR
SELECT 1 OR 0 AND 0;    -- 1  (equivalent to: 1 OR (0 AND 0))

-- NOT binds tighter than AND
SELECT NOT 1 AND 0;     -- 0  (equivalent to: (NOT 1) AND 0)

-- Left associativity
SELECT 10 - 2 - 3;      -- 5  (equivalent to: (10 - 2) - 3)

Unary Operators

Unary operators take a single operand. They have the highest precedence of all operators.

OperatorDescription
-exprNegates the numeric value of expr.
+exprNo-op. Returns the value of expr unchanged.
~exprBitwise NOT. Inverts every bit of the integer value of expr.
NOT exprLogical NOT. Returns 1 if expr is 0, returns 0 if expr is non-zero, returns NULL if expr is NULL.
SELECT -5;       -- -5
SELECT +5;       -- 5
SELECT ~5;       -- -6  (inverts all bits of integer 5)
SELECT NOT 1;    -- 0
SELECT NOT 0;    -- 1
SELECT NOT NULL;  -- NULL

Arithmetic Operators

Arithmetic operators perform numeric calculations. When both operands are integers, the result is an integer. When either operand is a real (floating-point) value, the result is a real.

OperatorDescription
expr1 + expr2Addition.
expr1 - expr2Subtraction.
expr1 * expr2Multiplication.
expr1 / expr2Division. Integer division truncates toward zero.
expr1 % expr2Modulo. Returns the remainder after integer division. Both operands are cast to integers.
SELECT 5 + 3;      -- 8
SELECT 10 - 4;     -- 6
SELECT 6 * 7;      -- 42
SELECT 15 / 4;     -- 3    (integer division truncates)
SELECT 7.0 / 2;    -- 3.5  (real division when either operand is real)
SELECT 2.5 + 1.5;  -- 4.0
SELECT 17 % 5;     -- 2
SELECT -7 % 3;     -- -1

Arithmetic with NULL

Any arithmetic operation involving NULL produces NULL:

SELECT 5 + NULL;  -- NULL

Realistic Example

CREATE TABLE products (name TEXT, price REAL, quantity INTEGER);
INSERT INTO products VALUES ('Widget', 9.99, 100),
                            ('Gadget', 24.50, 50),
                            ('Gizmo', 4.75, 200);

SELECT name, price * quantity AS total_value FROM products;
-- Widget|999.0
-- Gadget|1225.0
-- Gizmo|950.0

String Concatenation

The || operator joins two values into a single text string. Non-text operands are converted to text before concatenation. If either operand is NULL, the result is NULL.

SELECT 'Hello' || ' ' || 'World';  -- Hello World
SELECT 'Order #' || 42;            -- Order #42
SELECT 'text' || NULL;             -- NULL

Realistic Example

CREATE TABLE users (first_name TEXT, last_name TEXT);
INSERT INTO users VALUES ('Alice', 'Smith'), ('Bob', 'Jones');

SELECT first_name || ' ' || last_name AS full_name FROM users;
-- Alice Smith
-- Bob Jones

Comparison Operators

Comparison operators compare two values and return an integer: 1 for true, 0 for false. When either operand is NULL, standard comparison operators return NULL (with exceptions noted below).

Equality

OperatorDescription
expr1 = expr2True if operands are equal.
expr1 == expr2Synonym for =.
expr1 <> expr2True if operands are not equal.
SELECT 1 = 1;    -- 1
SELECT 1 == 1;   -- 1
SELECT 1 <> 2;   -- 1

Relational

OperatorDescription
expr1 < expr2True if expr1 is less than expr2.
expr1 > expr2True if expr1 is greater than expr2.
expr1 <= expr2True if expr1 is less than or equal to expr2.
expr1 >= expr2True if expr1 is greater than or equal to expr2.
SELECT 3 < 5;    -- 1
SELECT 5 > 3;    -- 1
SELECT 3 <= 3;   -- 1
SELECT 3 >= 3;   -- 1

NULL and Equality

Standard comparison operators return NULL when either operand is NULL, because NULL represents an unknown value:

SELECT NULL = NULL;    -- NULL  (not 1!)
SELECT NULL <> NULL;   -- NULL  (not 1!)

This is why WHERE column = NULL never matches any rows. Use IS NULL instead.

IS and IS NOT

The IS and IS NOT operators work like = and <> but handle NULL deterministically: they never return NULL.

LeftRightISIS NOT
NULLNULL10
NULLnon-NULL01
non-NULLNULL01
valuevaluesame as =same as <>
SELECT NULL IS NULL;       -- 1
SELECT 5 IS NOT NULL;      -- 1
SELECT 5 IS NULL;          -- 0
SELECT NULL IS NOT NULL;   -- 0

IS DISTINCT FROM and IS NOT DISTINCT FROM

These are standard SQL aliases for IS NOT and IS, respectively. They are useful for writing portable SQL that avoids the compact SQLite-specific IS/IS NOT syntax.

ExpressionEquivalent To
a IS DISTINCT FROM ba IS NOT b
a IS NOT DISTINCT FROM ba IS b
SELECT NULL IS NOT DISTINCT FROM NULL;  -- 1  (equivalent to NULL IS NULL)
SELECT 5 IS DISTINCT FROM NULL;         -- 1  (equivalent to 5 IS NOT NULL)
SELECT 5 IS DISTINCT FROM 5;            -- 0  (equivalent to 5 IS NOT 5)
SELECT NULL IS NOT DISTINCT FROM 5;     -- 0  (equivalent to NULL IS 5)

Logical Operators

Logical operators evaluate boolean expressions. Turso uses three-valued logic: true (1), false (0), and unknown (NULL).

AND

Returns 1 if both operands are true, 0 if either operand is false, and NULL otherwise.

LeftRightResult
111
100
000
NULL1NULL
NULL00

The key behavior: NULL AND 0 is 0 (not NULL), because regardless of the unknown value, the result must be false when the other operand is false.

SELECT 1 AND 1;      -- 1
SELECT 1 AND 0;      -- 0
SELECT NULL AND 0;   -- 0
SELECT NULL AND 1;   -- NULL

OR

Returns 1 if either operand is true, 0 if both operands are false, and NULL otherwise.

LeftRightResult
101
000
111
NULL11
NULL0NULL

The key behavior: NULL OR 1 is 1 (not NULL), because regardless of the unknown value, the result must be true when the other operand is true.

SELECT 1 OR 0;      -- 1
SELECT 0 OR 0;      -- 0
SELECT NULL OR 1;   -- 1
SELECT NULL OR 0;   -- NULL

NOT

Returns 0 if the operand is true, 1 if the operand is false, and NULL if the operand is NULL.

SELECT NOT 1;      -- 0
SELECT NOT 0;      -- 1
SELECT NOT NULL;   -- NULL

Realistic Example

CREATE TABLE orders (id INTEGER, amount REAL, status TEXT);
INSERT INTO orders VALUES (1, 150.00, 'shipped'),
                          (2, 75.50, 'pending'),
                          (3, 200.00, 'shipped'),
                          (4, 30.00, 'cancelled');

SELECT * FROM orders WHERE amount > 100 AND status = 'shipped';
-- 1|150.0|shipped
-- 3|200.0|shipped

Bitwise Operators

Bitwise operators work on the integer representation of their operands. Non-integer operands are cast to integers before the operation.

OperatorDescription
expr1 & expr2Bitwise AND. Sets each bit to 1 only if both corresponding bits are 1.
expr1 | expr2Bitwise OR. Sets each bit to 1 if either corresponding bit is 1.
~exprBitwise NOT (unary). Inverts every bit.
expr1 << expr2Left shift. Shifts bits of expr1 left by expr2 positions, filling with zeros.
expr1 >> expr2Right shift. Shifts bits of expr1 right by expr2 positions.
SELECT 5 & 3;       -- 1    (0101 & 0011 = 0001)
SELECT 5 | 3;       -- 7    (0101 | 0011 = 0111)
SELECT ~5;          -- -6   (inverts all 64 bits)
SELECT 1 << 4;      -- 16   (shift 1 left by 4 positions)
SELECT 16 >> 2;     -- 4    (shift 16 right by 2 positions)
SELECT 0xFF & 0x0F; -- 15   (mask lower nibble)
SELECT 0xF0 | 0x0F; -- 255  (combine nibbles)

NULL Handling Summary

Most operators propagate NULL: if any operand is NULL, the result is NULL. The exceptions are:

OperatorNULL Behavior
IS / IS NOTNever returns NULL. Treats NULL as a comparable value.
IS DISTINCT FROM / IS NOT DISTINCT FROMNever returns NULL. Same as IS NOT / IS.
ANDReturns 0 if the other operand is 0, even when one operand is NULL.
ORReturns 1 if the other operand is 1, even when one operand is NULL.
||Returns NULL if either operand is NULL.

Compatibility

The !< (not less than) and !> (not greater than) operators recognized by some databases are not supported. Use >= and <= instead.

Literals

A literal (also called a constant) is a fixed value written directly in a SQL statement. Turso supports six kinds of literal values: integers, reals, strings, blobs, NULL, and booleans.

Integer Literals

An integer literal is a sequence of decimal digits with no decimal point and no exponent. Integer values are stored as 64-bit signed integers, supporting the range -9223372036854775808 to 9223372036854775807.

SELECT 42;       -- 42
SELECT -100;     -- -100
SELECT 0;        -- 0

If a numeric literal without a decimal point or exponent exceeds the 64-bit signed integer range, it is automatically treated as a real (floating-point) value:

SELECT typeof(9223372036854775807);  -- integer (fits in 64-bit)
SELECT typeof(9223372036854775808);  -- real (exceeds 64-bit range)

Hexadecimal Integers

Integer literals may also be written in hexadecimal using the 0x or 0X prefix followed by hexadecimal digits (0-9, a-f, A-F). Hexadecimal literals are interpreted as 64-bit two’s-complement integers.

SELECT 0x1F;     -- 31
SELECT 0xFF;     -- 255
SELECT 0x0;      -- 0
SELECT 0X1F;     -- 31 (uppercase prefix also works)

Hexadecimal notation is recognized only in SQL literal syntax. Runtime string-to-integer conversions (such as CAST('0xFF' AS INTEGER)) do not interpret hex prefixes.

Real Literals

A numeric literal is treated as a real (floating-point) value if it contains a decimal point, an exponent clause, or both. Real values are stored as 8-byte IEEE 754 floating-point numbers.

Decimal Point

SELECT 3.14;     -- 3.14
SELECT .5;       -- 0.5
SELECT 100.0;    -- 100.0

Scientific Notation

An exponent clause consists of the letter E or e, an optional sign (+ or -), and one or more digits:

SELECT 2.5e3;    -- 2500.0
SELECT 1.5E-2;   -- 0.015
SELECT 1e10;     -- 10000000000.0

Underscore Separators

For readability, a single underscore (_) may be placed between any two digits in a numeric literal. Underscores are ignored during parsing and do not affect the value. This works for both integer and real literals.

SELECT 1_000_000;       -- 1000000
SELECT 1_000.000_001;   -- 1000.000001

String Literals

A string literal is a sequence of characters enclosed in single quotes ('). The value has TEXT storage class.

SELECT 'Hello, world!';   -- Hello, world!
SELECT '';                 -- (empty string)

Escaping Single Quotes

To include a literal single-quote character within a string, write two single quotes in a row (''). This is the standard SQL escaping mechanism. C-style backslash escapes (\') are not supported.

SELECT 'It''s a test';    -- It's a test
SELECT 'She said ''hi'''; -- She said 'hi'

Blob Literals

A blob literal is a string of hexadecimal digits preceded by X or x and enclosed in single quotes. Each pair of hex digits represents one byte. The number of hex digits must be even. The value has BLOB storage class.

SELECT X'48656C6C6F';           -- blob containing bytes for "Hello"
SELECT typeof(X'48656C6C6F');   -- blob
SELECT length(X'48656C6C6F');   -- 5
SELECT x'48656C6C6F';           -- lowercase x prefix also works
SELECT X'FF';                   -- single byte: 0xFF

A blob literal with no hex digits (X'') produces a zero-length blob.

Invalid blob literals are rejected at parse time. The hex digits must be valid (0-9, a-f, A-F) and the total count must be even.

NULL

The keyword NULL represents a missing or unknown value. NULL is its own storage class and is distinct from any other value, including zero, an empty string, or a zero-length blob.

SELECT NULL;            -- (empty result)
SELECT typeof(NULL);    -- null

NULL propagates through most operations. An expression involving NULL generally produces NULL:

SELECT NULL + 5;        -- NULL

Use IS NULL or IS NOT NULL to test for null values. The = operator does not match NULL because NULL is not equal to anything, including itself.

Boolean Literals

Turso recognizes TRUE and FALSE as boolean literals. They are aliases for the integer values 1 and 0, respectively.

SELECT TRUE;            -- 1
SELECT FALSE;           -- 0
SELECT typeof(TRUE);    -- integer
SELECT typeof(FALSE);   -- integer

Because TRUE and FALSE are integers, they participate in arithmetic like any other integer:

SELECT TRUE + TRUE;     -- 2

IS TRUE / IS FALSE

When TRUE or FALSE appear on the right-hand side of the IS operator, the expression performs a boolean evaluation of the left operand. Any non-zero, non-NULL value IS TRUE, and zero IS FALSE. NULL is neither true nor false.

SELECT 5 IS TRUE;       -- 1 (non-zero is true)
SELECT 0 IS FALSE;      -- 1 (zero is false)
SELECT NULL IS TRUE;    -- 0 (NULL is not true)
SELECT NULL IS FALSE;   -- 0 (NULL is not false)

Determining Literal Type

Use the typeof() function to inspect the storage class of any literal:

SELECT typeof(42);              -- integer
SELECT typeof(3.14);            -- real
SELECT typeof('hello');         -- text
SELECT typeof(X'FF');           -- blob
SELECT typeof(NULL);            -- null
SELECT typeof(TRUE);            -- integer

Summary

Literal KindSyntaxStorage Class
Integer42, -7, 0xFFINTEGER
Real3.14, 2.5e3, .5REAL
String'text'TEXT
BlobX'hex'BLOB
NULLNULLNULL
BooleanTRUE, FALSEINTEGER

Conditional Expressions

Turso provides several ways to express conditional logic within SQL queries: the CASE expression for general-purpose branching, and the built-in functions IIF, COALESCE, NULLIF, and IFNULL for common patterns.

CASE

Syntax

There are two forms of the CASE expression.

Searched CASE (without a base expression):

CASE
  WHEN condition THEN result
  [WHEN condition THEN result ...]
  [ELSE default]
END

Simple CASE (with a base expression):

CASE expr
  WHEN value THEN result
  [WHEN value THEN result ...]
  [ELSE default]
END

Description

The CASE expression evaluates a series of conditions and returns the result associated with the first condition that is true.

In the searched form, each WHEN clause contains an arbitrary boolean expression. Turso evaluates the WHEN expressions from left to right and returns the THEN result corresponding to the first expression that is true. A WHEN expression is considered true if its result is non-zero and non-NULL.

In the simple form, Turso evaluates the base expression once, then compares it against each WHEN value from left to right using the = operator. The result of the first matching WHEN value is returned. Because NULL = NULL evaluates to NULL (not true), a NULL base expression will never match any WHEN value, and the ELSE branch (or NULL) is returned.

Both forms use short-circuit evaluation: once a matching WHEN is found, the remaining WHEN clauses are not evaluated. If no WHEN clause matches and there is no ELSE, the result is NULL.

Examples

-- Searched CASE: classify a number
SELECT CASE
  WHEN 1 > 0 THEN 'positive'
  ELSE 'non-positive'
END;
-- positive
-- Simple CASE: map a value to a label
SELECT CASE 2
  WHEN 1 THEN 'one'
  WHEN 2 THEN 'two'
  WHEN 3 THEN 'three'
  ELSE 'other'
END;
-- two
-- Multiple WHEN clauses; first match wins
SELECT CASE
  WHEN 1 = 2 THEN 'first'
  WHEN 2 = 3 THEN 'second'
  WHEN 3 = 3 THEN 'third'
  ELSE 'none'
END;
-- third
-- NULL and 0 are not true in WHEN conditions
SELECT CASE WHEN NULL THEN 'yes' ELSE 'no' END;
-- no

SELECT CASE WHEN 0 THEN 'matched' END;
-- NULL (no ELSE clause, so NULL is returned)
-- NULL base expression never matches
SELECT CASE NULL
  WHEN 1    THEN 'one'
  WHEN NULL THEN 'null'
  ELSE 'else'
END;
-- else
-- Categorize product availability
CREATE TABLE products (name TEXT, price REAL, stock INTEGER);
INSERT INTO products VALUES ('Widget', 25.99, 100);
INSERT INTO products VALUES ('Gadget', 0, 50);
INSERT INTO products VALUES ('Doohickey', 15.50, 0);

SELECT name,
  CASE
    WHEN stock = 0  THEN 'out of stock'
    WHEN stock < 20 THEN 'low stock'
    ELSE 'in stock'
  END AS availability
FROM products;
-- Widget    | in stock
-- Gadget    | in stock
-- Doohickey | out of stock

iif

iif(condition, true_value, false_value) -> value

Returns true_value if condition is true (non-zero and non-NULL), otherwise returns false_value. This is a shorthand for CASE WHEN condition THEN true_value ELSE false_value END.

SELECT iif(1, 'true', 'false');
-- true

SELECT iif(10 > 5, 'big', 'small');
-- big
-- Label orders based on status
CREATE TABLE orders (id INTEGER, total REAL, status TEXT);
INSERT INTO orders VALUES (1, 150.00, 'shipped');
INSERT INTO orders VALUES (2, 0, 'pending');
INSERT INTO orders VALUES (3, 75.50, 'delivered');

SELECT id, iif(status = 'shipped', 'in transit', 'other') AS label
FROM orders;
-- 1 | in transit
-- 2 | other
-- 3 | other

coalesce

coalesce(x, y, …) -> value

Returns the first argument that is not NULL. If all arguments are NULL, returns NULL. Requires at least two arguments.

COALESCE uses short-circuit evaluation: arguments to the right of the first non-NULL value are not evaluated.

SELECT coalesce(NULL, NULL, 'third');
-- third

SELECT coalesce(NULL, NULL);
-- NULL
-- Pick the best available contact method
CREATE TABLE contacts (name TEXT, phone TEXT, email TEXT);
INSERT INTO contacts VALUES ('Alice', NULL, 'alice@example.com');
INSERT INTO contacts VALUES ('Bob', '555-1234', NULL);
INSERT INTO contacts VALUES ('Charlie', NULL, NULL);

SELECT name, coalesce(phone, email, 'no contact info') AS contact
FROM contacts;
-- Alice   | alice@example.com
-- Bob     | 555-1234
-- Charlie | no contact info

nullif

nullif(x, y) -> value

Returns x if x and y are different. Returns NULL if x and y are equal. The comparison uses the same rules as the = operator.

A common use of NULLIF is to convert a sentinel value (such as zero or an empty string) into NULL, which can then be handled by COALESCE, IFNULL, or aggregate functions that skip NULL values.

SELECT nullif(5, 5);
-- NULL

SELECT nullif(5, 8);
-- 5
-- Prevent division by zero (dividing by NULL yields NULL instead of an error)
SELECT 100.0 / nullif(0, 0);
-- NULL

SELECT 100.0 / nullif(5, 0);
-- 20.0
-- Convert zero prices to NULL
CREATE TABLE products (name TEXT, price REAL, stock INTEGER);
INSERT INTO products VALUES ('Widget', 25.99, 100);
INSERT INTO products VALUES ('Gadget', 0, 50);
INSERT INTO products VALUES ('Doohickey', 15.50, 0);

SELECT name, nullif(price, 0) AS nonzero_price
FROM products;
-- Widget    | 25.99
-- Gadget    | NULL
-- Doohickey | 15.5

ifnull

ifnull(x, y) -> value

Returns x if x is not NULL, otherwise returns y. This is equivalent to coalesce(x, y) with exactly two arguments.

SELECT ifnull(NULL, 'fallback');
-- fallback

SELECT ifnull('present', 'fallback');
-- present
-- Provide a default for missing phone numbers
CREATE TABLE contacts (name TEXT, phone TEXT, email TEXT);
INSERT INTO contacts VALUES ('Alice', NULL, 'alice@example.com');
INSERT INTO contacts VALUES ('Bob', '555-1234', NULL);
INSERT INTO contacts VALUES ('Charlie', NULL, NULL);

SELECT name, ifnull(phone, 'N/A') AS phone
FROM contacts;
-- Alice   | N/A
-- Bob     | 555-1234
-- Charlie | N/A

Choosing the Right Construct

NeedUseExample
Multi-way branchingCASE WHEN ... THEN ... ENDClassify rows into categories
Match a value against a listCASE expr WHEN ... THEN ... ENDMap status codes to labels
Simple if/else in one lineiif(cond, a, b)Toggle between two values
First non-NULL from a listcoalesce(a, b, c)Pick best available contact
Convert a value to NULLnullif(x, sentinel)Turn 0 into NULL before division
Default for a single NULLifnull(x, default)Replace NULL with a placeholder

Pattern Matching

Turso provides three pattern-matching operators for comparing strings against patterns: LIKE, GLOB, and REGEXP. Each uses different wildcard conventions and case-sensitivity rules. All three can be negated with the NOT keyword.

Syntax

expr [NOT] LIKE pattern [ESCAPE escape-char]
expr [NOT] GLOB pattern
expr [NOT] REGEXP pattern

Each operator returns 1 (true) if the string matches the pattern, 0 (false) if it does not, or NULL if either operand is NULL.

LIKE

The LIKE operator performs pattern matching using two wildcard characters:

WildcardMeaning
%Matches any sequence of zero or more characters.
_Matches exactly one character.

Any other character in the pattern matches itself. LIKE is case-insensitive for ASCII characters by default – 'a' LIKE 'A' evaluates to true. For Unicode characters outside the ASCII range, LIKE is case-sensitive: 'ä' LIKE 'Ä' evaluates to false.

-- % matches any sequence of characters
SELECT 'sweater' LIKE 'sweat%';   -- 1
SELECT 'sweatshirt' LIKE 'sweat%'; -- 1
SELECT 'hat' LIKE 'sweat%';       -- 0

-- _ matches exactly one character
SELECT 'sweater' LIKE 'sweat_r';  -- 1
SELECT 'sweatshirt' LIKE 'sweat_r'; -- 0

-- Case-insensitive for ASCII
SELECT 'sweater' LIKE 'SWEAT%';   -- 1
SELECT 'sweater' LIKE 'SwEaT_R';  -- 1

The % and _ wildcards can be combined to build expressive patterns. Use % at the beginning and end of a pattern to search for a substring anywhere in a string:

SELECT 'hello world' LIKE '%world%'; -- 1
SELECT 'hello world' LIKE '%xyz%';   -- 0

ESCAPE Clause

To match a literal % or _ character in a pattern, use the ESCAPE clause. The escape character causes the next %, _, or the escape character itself to be treated as a literal rather than a wildcard.

-- Match a literal % character using \ as the escape character
SELECT '10%' LIKE '10\%' ESCAPE '\';  -- 1
SELECT '10x' LIKE '10\%' ESCAPE '\';  -- 0

The escape character must be a single character. If the ESCAPE value is NULL, the entire LIKE expression evaluates to NULL.

-- Any single character can serve as the escape character
SELECT 'a%bc' LIKE 'a5%%' ESCAPE '5'; -- 1

NOT LIKE

Prefixing with NOT inverts the result:

SELECT 'sweater' NOT LIKE 'sweat%'; -- 0
SELECT 'hat' NOT LIKE 'sweat%';     -- 1

Function Form

The LIKE operator can also be invoked as a function. The infix expression X LIKE Y is equivalent to like(Y, X), and X LIKE Y ESCAPE Z is equivalent to like(Y, X, Z). Note that the pattern is the first argument in function form.

SELECT like('sweat%', 'sweater');           -- 1
SELECT like('abcX%', 'abc%', 'X');          -- 1

GLOB

The GLOB operator is similar to LIKE but uses Unix file-globbing syntax for wildcards and is case-sensitive.

WildcardMeaning
*Matches any sequence of zero or more characters (like % in LIKE).
?Matches exactly one character (like _ in LIKE).
[chars]Matches one character from the set or range inside the brackets.
[^chars]Matches one character not in the set or range inside the brackets.

Because GLOB is case-sensitive, 'hello' GLOB 'H*' evaluates to false.

-- * matches any sequence of characters
SELECT 'hello' GLOB 'h*';    -- 1
SELECT 'hello' GLOB 'H*';    -- 0 (case-sensitive)

-- ? matches exactly one character
SELECT 'hello' GLOB '?ello'; -- 1
SELECT 'hello' GLOB '??llo'; -- 1

Character Classes

Square brackets define a set of characters to match against a single position. A range can be specified with a hyphen. Use ^ after the opening bracket to negate the set.

-- [cde] matches one character that is c, d, or e
SELECT 'abcdefg' GLOB 'abc[cde]efg'; -- 1 (d matches [cde])
SELECT 'abcbefg' GLOB 'abc[cde]efg'; -- 0 (b not in [cde])

-- [c-e] matches one character in the range c through e
SELECT 'abcdefg' GLOB 'abc[c-e]efg'; -- 1
SELECT 'abcfefg' GLOB 'abc[c-e]efg'; -- 0

-- [^cde] matches one character NOT in the set
SELECT 'abcbefg' GLOB 'abc[^cde]efg'; -- 1
SELECT 'abccefg' GLOB 'abc[^cde]efg'; -- 0

A literal - can be included in a character class by placing it first or last: [-c] matches either - or c.

SELECT '-' GLOB '[-c]'; -- 1
SELECT 'c' GLOB '[-c]'; -- 1
SELECT 'x' GLOB '[-c]'; -- 0

NOT GLOB

Prefixing with NOT inverts the result:

SELECT 'hello' NOT GLOB 'h*'; -- 0
SELECT 'hello' NOT GLOB 'H*'; -- 1

Function Form

The infix expression X GLOB Y is equivalent to glob(Y, X). The pattern is the first argument.

SELECT glob('h*', 'hello');       -- 1
SELECT glob('[a-k]*', 'hello');   -- 1

REGEXP

The REGEXP operator tests whether a string matches a regular expression pattern. Turso provides a built-in regexp() function, so REGEXP works without loading an extension.

The infix expression X REGEXP Y is equivalent to regexp(Y, X). The pattern is the first argument in function form.

SELECT 'hello' REGEXP 'h.*o';      -- 1
SELECT 'hello' REGEXP '^world$';   -- 0

REGEXP supports standard regular expression syntax, including character classes, anchors, quantifiers, and escape sequences:

-- Anchors: ^ (start of string) and $ (end of string)
SELECT 'hello' REGEXP '^hello$'; -- 1
SELECT 'hello' REGEXP '^ello';   -- 0

-- Character classes and quantifiers
SELECT 'test@example.com' REGEXP '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; -- 1

-- \d for digits, \b for word boundaries
SELECT '2024-01-15' REGEXP '^\d{4}-\d{2}-\d{2}$'; -- 1
SELECT 'hello world' REGEXP '\bworld\b';           -- 1

NOT REGEXP

Prefixing with NOT inverts the result:

SELECT 'hello' NOT REGEXP 'h.*o';     -- 0
SELECT 'hello' NOT REGEXP '^world$';  -- 1

Using REGEXP in Queries

REGEXP can be used in WHERE clauses, CASE expressions, and subqueries just like any other boolean expression:

CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, sku TEXT);
INSERT INTO products VALUES (1, 'Widget A', 'WGT-001'),
                            (2, 'Gadget B', 'GDT-002'),
                            (3, 'Widget C', 'WGT-003'),
                            (4, 'Doohickey', 'DHK-004');

-- Filter rows with REGEXP in WHERE
SELECT name FROM products WHERE sku REGEXP '^WGT';
-- Widget A
-- Widget C

-- Classify rows with REGEXP in CASE
SELECT name,
       CASE WHEN name REGEXP '^Widget' THEN 'widget'
            ELSE 'other'
       END AS category
FROM products;
-- Widget A|widget
-- Gadget B|other
-- Widget C|widget
-- Doohickey|other

NULL Handling

For all three operators, if either operand is NULL, the result is NULL:

SELECT NULL LIKE 'hello';  -- NULL (empty result)
SELECT 'hello' LIKE NULL;  -- NULL (empty result)
SELECT NULL GLOB 'h*';     -- NULL (empty result)
SELECT NULL REGEXP 'abc';  -- NULL (empty result)

Comparison of Operators

FeatureLIKEGLOBREGEXP
Case sensitivityCase-insensitive (ASCII)Case-sensitiveDepends on pattern
Zero-or-more wildcard%*.*
Single-char wildcard_?.
Character classesNo[abc], [a-z], [^abc][abc], [a-z], [^abc], \d, \w, etc.
Escape clauseESCAPE keywordNo\ (backslash)
NegationNOT LIKENOT GLOBNOT REGEXP

Type Conversions

Turso uses a dynamic type system inherited from SQLite. Every value belongs to one of five storage classes – NULL, INTEGER, REAL, TEXT, or BLOB – but columns do not enforce a single type. Instead, each column has a type affinity that recommends how values should be stored. This page covers how type affinities are determined, how values are coerced on insertion, how the CAST expression performs explicit conversions, and how types interact during comparisons.

Storage Classes

Every value in Turso has exactly one storage class at any given time. Use the typeof() function to inspect it:

SELECT typeof(42);        -- integer
SELECT typeof(3.14);      -- real
SELECT typeof('hello');   -- text
SELECT typeof(x'ABCD');  -- blob
SELECT typeof(NULL);      -- null

Arithmetic and string operations produce values whose storage class follows from the operation:

SELECT typeof(1 + 1);      -- integer
SELECT typeof(1 + 1.0);    -- real (integer promoted to real)
SELECT typeof('a' || 'b'); -- text

Type Affinity

A column’s type affinity is determined by the declared type name in the CREATE TABLE statement. Affinity is a recommendation, not a constraint – any column can store a value of any storage class.

The Five Affinities

AffinityBehavior on INSERT
TEXTNumeric values are converted to their text representation before storage.
NUMERICText that looks like an integer or real number is converted to INTEGER or REAL.
INTEGERBehaves identically to NUMERIC on insertion. Differs from NUMERIC only in CAST expressions.
REALLike NUMERIC, but integer values are stored as floating-point.
BLOBNo conversion. Values are stored exactly as provided.

Affinity Determination Rules

The affinity of a column is determined by applying the following rules to the declared type name, in order. The first matching rule wins:

RuleConditionResulting Affinity
1Type name contains "INT"INTEGER
2Type name contains "CHAR", "CLOB", or "TEXT"TEXT
3Type name contains "BLOB", or no type is specifiedBLOB
4Type name contains "REAL", "FLOA", or "DOUB"REAL
5OtherwiseNUMERIC

The matching is case-insensitive and checks for substrings anywhere in the declared type name. Because rules are applied in order, some type names produce counterintuitive results:

Declared TypeMatching RuleAffinity
INT, INTEGER, BIGINT, SMALLINT1 (contains “INT”)INTEGER
TEXT, VARCHAR(255), CLOB2 (contains “TEXT”, “CHAR”, or “CLOB”)TEXT
BLOB, (no type)3 (contains “BLOB” or empty)BLOB
REAL, DOUBLE, FLOAT4 (contains “REAL”, “DOUB”, or “FLOA”)REAL
NUMERIC, DECIMAL(10,5), BOOLEAN, DATE5 (no match above)NUMERIC
CHARINT1 (contains “INT”, rule 1 before rule 2)INTEGER
FLOATING POINT1 (contains “INT” in “POINT”)INTEGER
STRING5 (no match for rules 1-4)NUMERIC

Affinity in Action

When a value is inserted into a column, the column’s affinity determines whether a type conversion is attempted. Conversions only happen if they are lossless and reversible.

CREATE TABLE demo (
  t TEXT,
  n NUMERIC,
  i INTEGER,
  r REAL,
  b BLOB
);

-- Insert the string '500' into every column
INSERT INTO demo VALUES ('500', '500', '500', '500', '500');
SELECT typeof(t), typeof(n), typeof(i), typeof(r), typeof(b) FROM demo;
-- text|integer|integer|real|text

Here is what happened to the string '500' in each column:

  • t (TEXT): Stored as text. No conversion needed.
  • n (NUMERIC): Converted to integer 500 because '500' is a well-formed integer literal.
  • i (INTEGER): Converted to integer 500, same as NUMERIC.
  • r (REAL): Converted to real 500.0 because REAL affinity forces floating-point storage.
  • b (BLOB): Stored as text. BLOB affinity performs no conversion.
-- Insert the integer 500 into every column
DELETE FROM demo;
INSERT INTO demo VALUES (500, 500, 500, 500, 500);
SELECT typeof(t), typeof(n), typeof(i), typeof(r), typeof(b) FROM demo;
-- text|integer|integer|real|integer
  • t (TEXT): Converted to text '500' because TEXT affinity converts numerics to text.
  • n (NUMERIC): Stored as integer. Already numeric.
  • i (INTEGER): Stored as integer. Already numeric.
  • r (REAL): Stored as real 500.0. REAL affinity promotes integers to floating-point.
  • b (BLOB): Stored as integer. BLOB affinity performs no conversion; the value keeps its original type.

NULL and BLOB Bypass Affinity

NULL values and BLOB values are never converted by affinity, regardless of the column’s declared type:

DELETE FROM demo;
INSERT INTO demo VALUES (NULL, NULL, NULL, NULL, NULL);
SELECT typeof(t), typeof(n), typeof(i), typeof(r), typeof(b) FROM demo;
-- null|null|null|null|null
DELETE FROM demo;
INSERT INTO demo VALUES (x'01', x'01', x'01', x'01', x'01');
SELECT typeof(t), typeof(n), typeof(i), typeof(r), typeof(b) FROM demo;
-- blob|blob|blob|blob|blob

CAST Expressions

The CAST expression explicitly converts a value to a different storage class.

Syntax

CAST(expr AS type-name)

The type-name follows the same affinity determination rules as column type names. If expr is NULL, the result is always NULL.

Conversion Rules

CAST to INTEGER

Converts the value to a 64-bit signed integer.

  • From REAL: Truncates toward zero. Values outside the 64-bit integer range are clamped to the minimum or maximum value.
  • From TEXT: Extracts the longest leading substring that is a valid integer. Leading whitespace is ignored. Returns 0 if no valid prefix exists.
  • From BLOB: The blob is first interpreted as text, then the text-to-integer rules apply.
SELECT CAST(3.14 AS INTEGER);      -- 3
SELECT CAST(9.99 AS INTEGER);      -- 9
SELECT CAST(-7.8 AS INTEGER);      -- -7
SELECT CAST('42' AS INTEGER);      -- 42
SELECT CAST('123abc' AS INTEGER);  -- 123
SELECT CAST('abc' AS INTEGER);     -- 0
SELECT CAST('  42  ' AS INTEGER);  -- 42
SELECT CAST('' AS INTEGER);        -- 0
SELECT CAST('99.9' AS INTEGER);    -- 99

CAST to REAL

Converts the value to an 8-byte IEEE 754 floating-point number.

  • From TEXT: Extracts the longest leading substring that is a valid real number. Returns 0.0 if no valid prefix exists.
  • From INTEGER: Converts to the nearest representable floating-point value.
  • From BLOB: The blob is first interpreted as text, then the text-to-real rules apply.
SELECT CAST(42 AS REAL);              -- 42.0
SELECT CAST('3.14' AS REAL);          -- 3.14
SELECT CAST('123.45abc' AS REAL);     -- 123.45

CAST to TEXT

Converts the value to a text string.

  • From INTEGER or REAL: Renders the number as its string representation.
  • From BLOB: Interprets the blob bytes as a UTF-8 text string.
SELECT CAST(42 AS TEXT);                    -- 42
SELECT CAST(x'68656C6C6F' AS TEXT);         -- hello
SELECT typeof(CAST(42 AS TEXT));            -- text

CAST to BLOB

Converts the value to a blob.

  • From TEXT: The text is first encoded as UTF-8, then the resulting bytes are treated as a blob.
  • From INTEGER or REAL: The value is first converted to text, then to blob.
SELECT typeof(CAST('hello' AS BLOB));  -- blob

CAST to NUMERIC

CAST to NUMERIC returns either an INTEGER or a REAL, depending on the input:

  • If the value looks like a well-formed integer and fits in 64 bits, the result is INTEGER.
  • If the value looks like a floating-point number, the result is REAL.
  • From REAL or INTEGER, the value is returned as-is.

This is where INTEGER and NUMERIC affinity differ in CAST expressions:

SELECT CAST(4.0 AS INTEGER), typeof(CAST(4.0 AS INTEGER));
-- 4|integer

SELECT CAST(4.0 AS NUMERIC), typeof(CAST(4.0 AS NUMERIC));
-- 4.0|real

CAST(4.0 AS INTEGER) truncates the real value to an integer, while CAST(4.0 AS NUMERIC) preserves the real type because the input is already a REAL.

Comparison and Type Affinity

When Turso compares two values, it may apply affinity conversions to the operands before performing the comparison. The rules depend on the affinities of the expressions being compared.

Affinity Application During Comparisons

Before a comparison is performed, the following rules are applied in order:

  1. If one operand has INTEGER, REAL, or NUMERIC affinity and the other has TEXT, BLOB, or no affinity: NUMERIC affinity is applied to the non-numeric operand. This means text that looks like a number is converted to a number before comparing.

  2. If one operand has TEXT affinity and the other has no affinity: TEXT affinity is applied to the operand with no affinity.

  3. Otherwise: No conversion is applied. Values are compared using their existing storage classes.

Comparison Sort Order

When comparing values of different storage classes, Turso follows this ordering:

  1. NULL is less than any other value. (Note: NULL < x evaluates to NULL, not TRUE. Use IS NULL to test for nulls. This ordering applies to ORDER BY and similar contexts.)
  2. INTEGER and REAL values are compared numerically with each other, and are less than any TEXT or BLOB value.
  3. TEXT values are less than BLOB values. Text comparisons use the applicable collating sequence (default: BINARY).
  4. BLOB values are compared byte-by-byte using memcmp ordering.

Affinity Effects on Comparisons

Column affinity can cause the same literal to compare differently depending on the column’s declared type:

CREATE TABLE inventory (
  label TEXT,
  count NUMERIC
);
INSERT INTO inventory VALUES ('500', '500');

-- TEXT column: integer 40 is converted to text '40' for comparison
-- String comparison: '500' > '40' (compares character by character)
SELECT label < 40, label < 60, label < 600 FROM inventory;
-- 0|1|1

-- NUMERIC column: stored as integer 500, numeric comparison
SELECT count < 40, count < 60, count < 600 FROM inventory;
-- 0|0|1

In the TEXT column example, the integer literal 40 is converted to the string '40' before comparing. In string comparison, '500' < '60' is true because '5' < '6' lexicographically. In the NUMERIC column, the value is already stored as integer 500, so the comparison is purely numeric.

Expression Affinity

Expressions in SQL do not always carry an affinity. The rules for expression affinity are:

ExpressionAffinity
Column referenceSame affinity as the column
CAST(expr AS type)Affinity determined by the type name
Unary + applied to a column (e.g., +column)No affinity
Any operator or function resultNo affinity

The unary + operator is a common technique to strip affinity from a column reference, forcing the value to be compared without automatic type coercion.

The typeof() Function

The typeof() function returns a string indicating the storage class of its argument. It is the primary tool for inspecting how Turso stores a value.

SELECT typeof(42);                          -- integer
SELECT typeof(3.14);                        -- real
SELECT typeof('hello');                     -- text
SELECT typeof(x'FF');                       -- blob
SELECT typeof(NULL);                        -- null
SELECT typeof(CAST('42' AS INTEGER));       -- integer

The return value is always one of: 'null', 'integer', 'real', 'text', or 'blob'.

IN and BETWEEN

Turso supports the IN, NOT IN, BETWEEN, and NOT BETWEEN operators for testing whether a value belongs to a set or falls within a range. These operators are commonly used in WHERE clauses to filter rows, but they can appear anywhere an expression is allowed.

IN

Syntax

expr [NOT] IN (value [, ...])
expr [NOT] IN (select-stmt)

Description

The IN operator tests whether the left-hand expression is equal to any value in the right-hand set. The set can be a parenthesized list of scalar values or the result of a subquery. IN returns 1 (true) if a match is found and 0 (false) if no match is found, subject to the NULL handling rules described below.

NOT IN is the logical negation of IN. It returns 1 (true) when the left-hand expression does not match any value in the set, and 0 (false) when a match is found.

When the right-hand side is a subquery, it must return a single column. Each row returned by the subquery is treated as one element in the set.

Turso also allows an empty parenthesized list (). When the right-hand side is an empty set, IN always returns 0 (false) and NOT IN always returns 1 (true), regardless of the left-hand operand – even if it is NULL.

NULL Handling

The interaction between IN/NOT IN and NULL values follows three-valued logic. The result depends on whether a match is found, whether the set contains NULL, and whether the left operand is NULL.

Left operandSet contains NULLMatch foundEmpty setIN resultNOT IN result
non-NULLnonono01
non-NULLnoyes10
non-NULLyesnonoNULLNULL
non-NULLyesyes10
NULLanyanynoNULLNULL
anyanyyes01

Key rules to remember:

  • Match found: When the left operand is found in the set, IN returns 1 and NOT IN returns 0, regardless of any NULLs in the set.
  • No match, set contains NULL: When no match is found but the set contains NULL, the result is NULL for both IN and NOT IN. The NULL represents “unknown” – the value might match the unknown element.
  • Left operand is NULL: When the left operand is NULL and the set is non-empty, the result is always NULL (unknown).
  • Empty set: When the right-hand set is empty, IN returns 0 and NOT IN returns 1 – even if the left operand is NULL. An empty set cannot contain any value.

Examples

-- Basic IN with a value list
SELECT 1 IN (1, 2, 3);
-- 1

SELECT 4 IN (1, 2, 3);
-- 0
-- IN with string values
SELECT 'apple' IN ('apple', 'banana', 'cherry');
-- 1
-- NOT IN excludes matching values
SELECT 'grape' NOT IN ('apple', 'banana', 'cherry');
-- 1
-- IN with an empty list always returns 0
SELECT 1 IN ();
-- 0

-- NOT IN with an empty list always returns 1
SELECT NULL NOT IN ();
-- 1
-- NULL handling: left operand is NULL
SELECT NULL IN (1, 2, 3);
-- NULL

-- Match found despite NULLs in the set
SELECT 1 IN (1, 2, NULL);
-- 1

-- No match and set contains NULL: result is NULL
SELECT 4 IN (1, 2, NULL);
-- NULL
-- Filter rows using IN with a value list
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  category TEXT,
  price REAL,
  stock INTEGER
);
INSERT INTO products VALUES (1, 'Widget', 'Hardware', 9.99, 100);
INSERT INTO products VALUES (2, 'Gadget', 'Electronics', 24.99, 50);
INSERT INTO products VALUES (3, 'Gizmo', 'Electronics', 49.99, 25);
INSERT INTO products VALUES (4, 'Bolt', 'Hardware', 1.99, 500);
INSERT INTO products VALUES (5, 'Sensor', 'Electronics', 14.99, 75);

SELECT name, category FROM products
WHERE category NOT IN ('Electronics');
-- Widget|Hardware
-- Bolt|Hardware
-- IN with a subquery
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  category TEXT,
  price REAL,
  stock INTEGER
);
INSERT INTO products VALUES (1, 'Widget', 'Hardware', 9.99, 100);
INSERT INTO products VALUES (2, 'Gadget', 'Electronics', 24.99, 50);
INSERT INTO products VALUES (3, 'Gizmo', 'Electronics', 49.99, 25);
INSERT INTO products VALUES (4, 'Bolt', 'Hardware', 1.99, 500);
INSERT INTO products VALUES (5, 'Sensor', 'Electronics', 14.99, 75);

CREATE TABLE featured_ids (id INTEGER);
INSERT INTO featured_ids VALUES (2);
INSERT INTO featured_ids VALUES (4);

SELECT name, price FROM products
WHERE id IN (SELECT id FROM featured_ids);
-- Gadget|24.99
-- Bolt|1.99
-- NOT IN with a subquery to find customers with no pending orders
CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer TEXT,
  amount REAL,
  status TEXT
);
INSERT INTO orders VALUES (1, 'Alice', 150.00, 'shipped');
INSERT INTO orders VALUES (2, 'Bob', 75.50, 'pending');
INSERT INTO orders VALUES (3, 'Alice', 200.00, 'delivered');
INSERT INTO orders VALUES (4, 'Carol', 50.00, 'shipped');
INSERT INTO orders VALUES (5, 'Bob', 300.00, 'delivered');

SELECT customer, amount FROM orders
WHERE customer NOT IN (
  SELECT customer FROM orders WHERE status = 'pending'
);
-- Alice|150.0
-- Alice|200.0
-- Carol|50.0

BETWEEN

Syntax

expr [NOT] BETWEEN expr AND expr

Description

The BETWEEN operator tests whether a value falls within an inclusive range. The expression:

x BETWEEN y AND z

is equivalent to:

x >= y AND x <= z

except that with BETWEEN, the x expression is evaluated only once. This makes no difference in results but can matter when x is a complex or expensive expression.

NOT BETWEEN inverts the test. The expression x NOT BETWEEN y AND z is equivalent to x < y OR x > z.

BETWEEN works with any data type that supports comparison: integers, reals, text (compared according to the active collation), and blobs.

Examples

-- Numeric range check
SELECT 5 BETWEEN 1 AND 10;
-- 1

SELECT 15 BETWEEN 1 AND 10;
-- 0
-- NOT BETWEEN
SELECT 5 NOT BETWEEN 1 AND 10;
-- 0

SELECT 15 NOT BETWEEN 1 AND 10;
-- 1
-- BETWEEN with text values (compared lexicographically)
SELECT 'M' BETWEEN 'A' AND 'Z';
-- 1

SELECT 'banana' BETWEEN 'apple' AND 'cherry';
-- 1
-- Filter products by price range
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  category TEXT,
  price REAL,
  stock INTEGER
);
INSERT INTO products VALUES (1, 'Widget', 'Hardware', 9.99, 100);
INSERT INTO products VALUES (2, 'Gadget', 'Electronics', 24.99, 50);
INSERT INTO products VALUES (3, 'Gizmo', 'Electronics', 49.99, 25);
INSERT INTO products VALUES (4, 'Bolt', 'Hardware', 1.99, 500);
INSERT INTO products VALUES (5, 'Sensor', 'Electronics', 14.99, 75);

SELECT name, price FROM products
WHERE price BETWEEN 10.00 AND 30.00;
-- Gadget|24.99
-- Sensor|14.99
-- NOT BETWEEN to find outlier prices
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  category TEXT,
  price REAL,
  stock INTEGER
);
INSERT INTO products VALUES (1, 'Widget', 'Hardware', 9.99, 100);
INSERT INTO products VALUES (2, 'Gadget', 'Electronics', 24.99, 50);
INSERT INTO products VALUES (3, 'Gizmo', 'Electronics', 49.99, 25);
INSERT INTO products VALUES (4, 'Bolt', 'Hardware', 1.99, 500);
INSERT INTO products VALUES (5, 'Sensor', 'Electronics', 14.99, 75);

SELECT name, price FROM products
WHERE price NOT BETWEEN 10.00 AND 30.00;
-- Widget|9.99
-- Gizmo|49.99
-- Bolt|1.99
-- BETWEEN with date strings (ISO 8601 format sorts correctly)
CREATE TABLE events (
  id INTEGER PRIMARY KEY,
  name TEXT,
  event_date TEXT
);
INSERT INTO events VALUES (1, 'Launch', '2024-01-15');
INSERT INTO events VALUES (2, 'Review', '2024-03-20');
INSERT INTO events VALUES (3, 'Release', '2024-06-01');
INSERT INTO events VALUES (4, 'Summit', '2024-09-10');

SELECT name, event_date FROM events
WHERE event_date BETWEEN '2024-01-01' AND '2024-06-30';
-- Launch|2024-01-15
-- Review|2024-03-20
-- Release|2024-06-01

Combining IN and BETWEEN

IN and BETWEEN can be used together in the same WHERE clause, combined with AND and OR:

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer TEXT,
  amount REAL,
  status TEXT
);
INSERT INTO orders VALUES (1, 'Alice', 150.00, 'shipped');
INSERT INTO orders VALUES (2, 'Bob', 75.50, 'pending');
INSERT INTO orders VALUES (3, 'Alice', 200.00, 'delivered');
INSERT INTO orders VALUES (4, 'Carol', 50.00, 'shipped');
INSERT INTO orders VALUES (5, 'Bob', 300.00, 'delivered');

-- Orders that are shipped or delivered with amount in a range
SELECT customer, amount FROM orders
WHERE status IN ('shipped', 'delivered')
  AND amount BETWEEN 100.00 AND 250.00;
-- Alice|150.0
-- Alice|200.0

Operator Precedence

Both IN and BETWEEN have defined positions in the operator precedence hierarchy. From highest to lowest among the comparison operators:

PrecedenceOperators
Higher<, >, <=, >=
=, ==, <>, IS, IS NOT
BETWEEN ... AND ...
IN, LIKE, GLOB, MATCH, REGEXP
LowerISNULL, NOTNULL, NOT NULL

The NOT keyword that precedes IN or BETWEEN is part of the operator itself (not a separate prefix operator) and does not change the precedence.

Collation

Syntax

expr COLLATE {BINARY | NOCASE | RTRIM}

In column definitions:

column-name type-name COLLATE {BINARY | NOCASE | RTRIM}

In ORDER BY clauses:

ORDER BY expr COLLATE {BINARY | NOCASE | RTRIM} [{ASC | DESC}]

In index definitions:

CREATE INDEX index-name ON table-name (column-name COLLATE {BINARY | NOCASE | RTRIM})

Description

A collation sequence determines how text values are compared and sorted. The COLLATE operator is a unary postfix operator that assigns a collation sequence to an expression, overriding whatever collation would otherwise apply.

Collation sequences affect only text comparisons. Numeric values are always compared numerically, and BLOB values are always compared byte-by-byte regardless of any collation setting.

Built-in Collation Sequences

Turso provides three built-in collation sequences:

CollationBehavior
BINARYCompares text byte-by-byte using raw byte values. This is the default collation for all columns. Uppercase letters sort before lowercase ('A' < 'a').
NOCASESame as BINARY, except the 26 uppercase ASCII letters (A-Z) are folded to their lowercase equivalents before comparison. Only ASCII characters are folded; accented or non-Latin characters are not affected.
RTRIMSame as BINARY, except trailing space characters are ignored. 'abc' and 'abc ' are considered equal.

Every column has an associated collation sequence. If no COLLATE clause is specified in the column definition, the default is BINARY.

COLLATE in Column Definitions

A COLLATE clause in a column definition sets the default collation for that column. This collation is used whenever the column appears in a comparison or ORDER BY without an explicit COLLATE operator.

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  email TEXT COLLATE NOCASE,
  username TEXT
);
INSERT INTO users VALUES (1, 'Alice@Example.com', 'alice');
INSERT INTO users VALUES (2, 'bob@example.com', 'bob');
INSERT INTO users VALUES (3, 'CAROL@EXAMPLE.COM', 'carol');

-- NOCASE column: matches regardless of case
SELECT id, email FROM users WHERE email = 'alice@example.com';
-- 1|Alice@Example.com

Because email is declared with COLLATE NOCASE, the comparison email = 'alice@example.com' matches the stored value 'Alice@Example.com'.

COLLATE Operator in Expressions

The COLLATE operator can be applied to any expression to control how a comparison is performed. It has very high precedence – higher than any binary operator – so it binds tightly to its operand.

-- Default BINARY comparison is case-sensitive
SELECT 'Hello' = 'hello';
-- 0

-- COLLATE NOCASE makes the comparison case-insensitive
SELECT 'Hello' = 'hello' COLLATE NOCASE;
-- 1

-- COLLATE RTRIM ignores trailing spaces
SELECT 'hello   ' = 'hello' COLLATE RTRIM;
-- 1

When used in a WHERE clause, the COLLATE operator overrides the column’s default collation:

CREATE TABLE items (
  id INTEGER PRIMARY KEY,
  label TEXT
);
INSERT INTO items VALUES (1, 'abc  ');
INSERT INTO items VALUES (2, 'abc');
INSERT INTO items VALUES (3, 'ABC');
INSERT INTO items VALUES (4, 'xyz');

-- Without COLLATE, default BINARY collation is exact
SELECT id FROM items WHERE label = 'abc';
-- 2

-- With COLLATE RTRIM, trailing spaces are ignored
SELECT id FROM items WHERE label = 'abc' COLLATE RTRIM;
-- 1
-- 2

Rules for Choosing a Collation in Comparisons

When two values are compared using a binary operator (=, <, >, <=, >=, <>, IS, IS NOT), the collation sequence is determined by these rules, applied in order:

  1. If either operand has an explicit COLLATE operator, that collation is used. If both operands have explicit COLLATE operators, the leftmost one wins.
  2. If either operand is a column with a defined collation (from CREATE TABLE), that column’s collation is used. If both operands are columns, the left operand’s collation takes precedence.
  3. Otherwise, the BINARY collation is used.
CREATE TABLE contacts (
  id INTEGER PRIMARY KEY,
  name TEXT COLLATE NOCASE,
  tag TEXT
);
INSERT INTO contacts VALUES (1, 'alice', 'alpha');
INSERT INTO contacts VALUES (2, 'Alice', 'Alpha');
INSERT INTO contacts VALUES (3, 'BOB', 'beta');

-- Column collation applies: name is NOCASE, so 'alice' matches 'Alice'
SELECT id FROM contacts WHERE name = 'alice';
-- 1
-- 2

-- Default BINARY: tag has no COLLATE, so comparison is case-sensitive
SELECT id FROM contacts WHERE tag = 'alpha';
-- 1

-- Explicit COLLATE overrides defaults
SELECT id FROM contacts WHERE tag = 'alpha' COLLATE NOCASE;
-- 1
-- 2

COLLATE in ORDER BY

The ORDER BY clause uses collation to determine sort order for text values. The collation is chosen as follows:

  • If the ORDER BY expression has an explicit COLLATE clause, that collation is used.
  • If the ORDER BY expression is a column, the column’s defined collation is used.
  • Otherwise, BINARY is used.
CREATE TABLE contacts (
  id INTEGER PRIMARY KEY,
  name TEXT COLLATE NOCASE,
  tag TEXT
);
INSERT INTO contacts VALUES (1, 'alice', 'alpha');
INSERT INTO contacts VALUES (2, 'Alice', 'Alpha');
INSERT INTO contacts VALUES (3, 'BOB', 'beta');
INSERT INTO contacts VALUES (4, 'bob', 'Beta');
INSERT INTO contacts VALUES (5, 'Carol', 'gamma');

-- ORDER BY tag with default BINARY: uppercase sorts before lowercase
SELECT id, tag FROM contacts ORDER BY tag COLLATE BINARY;
-- 2|Alpha
-- 4|Beta
-- 1|alpha
-- 3|beta
-- 5|gamma

-- ORDER BY tag with NOCASE: case-insensitive sort
SELECT id, tag FROM contacts ORDER BY tag COLLATE NOCASE;
-- 1|alpha
-- 2|Alpha
-- 3|beta
-- 4|Beta
-- 5|gamma

COLLATE in CREATE INDEX

A COLLATE clause can be specified on indexed columns. This is useful when you want an index to support case-insensitive lookups on a column that does not itself have a NOCASE collation.

CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  email TEXT,
  username TEXT
);
INSERT INTO users VALUES (1, 'Alice@Example.com', 'alice');
INSERT INTO users VALUES (2, 'bob@example.com', 'bob');
INSERT INTO users VALUES (3, 'CAROL@EXAMPLE.COM', 'carol');
CREATE INDEX idx_email ON users(email COLLATE NOCASE);

SELECT id, email FROM users WHERE email = 'alice@example.com' COLLATE NOCASE;
-- 1|Alice@Example.com

COLLATE with GROUP BY and DISTINCT

Collation sequences also affect grouping. When COLLATE NOCASE is applied to a grouping expression, values that differ only in case are placed in the same group.

CREATE TABLE words (id INTEGER PRIMARY KEY, word TEXT);
INSERT INTO words VALUES (1, 'apple');
INSERT INTO words VALUES (2, 'Apple');
INSERT INTO words VALUES (3, 'APPLE');
INSERT INTO words VALUES (4, 'banana');

-- GROUP BY with NOCASE: all case variants of 'apple' form one group
SELECT word COLLATE NOCASE, COUNT(*) AS cnt
FROM words
GROUP BY word COLLATE NOCASE;
-- apple|3
-- banana|1

-- DISTINCT with NOCASE: collapses case variants
SELECT DISTINCT word COLLATE NOCASE FROM words;
-- apple
-- banana

COLLATE with IN

The COLLATE operator can be combined with the IN operator. Attach COLLATE to the left-hand expression to control how membership is tested.

SELECT 'hello' COLLATE NOCASE IN ('Hello', 'World');
-- 1

Compatibility

Turso supports the three built-in collation sequences (BINARY, NOCASE, RTRIM) and the COLLATE operator in expressions, column definitions, ORDER BY, and CREATE INDEX. Custom collation sequences registered via the C API (sqlite3_create_collation) are not supported. The PRAGMA collation_list command is not available.

Scalar Functions

Scalar functions accept zero or more arguments and return a single value. They can be used anywhere an expression is allowed: in SELECT lists, WHERE clauses, ORDER BY, and so on.


abs

abs(X) -> numeric

Returns the absolute value of X. Returns NULL if X is NULL. Returns 0.0 if X is a string that cannot be converted to a number.

SELECT abs(-42);    -- 42
SELECT abs(3.14);   -- 3.14
SELECT abs(NULL);   -- NULL

char

char(X1, X2, …, XN) -> text

Returns a string composed of characters having the Unicode code points given by the integer arguments.

SELECT char(72, 101, 108, 108, 111);  -- 'Hello'
SELECT char(9731);                     -- snowman character

coalesce

coalesce(X, Y, …) -> value

Returns the first non-NULL argument. Requires at least two arguments. Returns NULL only if every argument is NULL.

SELECT coalesce(NULL, NULL, 'hello');  -- 'hello'
SELECT coalesce(1, 2, 3);             -- 1

concat

concat(X, …) -> text

Returns a string formed by concatenating the text representations of all non-NULL arguments. NULL arguments are silently skipped. Returns an empty string if all arguments are NULL.

SELECT concat('Hello', ' ', 'World');  -- 'Hello World'
SELECT concat(NULL, 'hello');          -- 'hello'

concat_ws

concat_ws(separator, X, …) -> text

Returns a string formed by concatenating the non-NULL arguments after the first, using the first argument as a separator. Returns NULL if the separator is NULL.

SELECT concat_ws('-', '2024', '01', '15');  -- '2024-01-15'
SELECT concat_ws(', ', 'Alice', 'Bob');     -- 'Alice, Bob'
SELECT concat_ws(NULL, 'a', 'b');           -- NULL

format

format(FORMAT, …) -> text

Returns a string formed by substituting arguments into the format string, following printf conventions. Supports %s (string), %d (integer), %f (floating-point), and other standard format specifiers. Returns NULL if FORMAT is NULL.

SELECT format('%s has %d items', 'cart', 5);  -- 'cart has 5 items'
SELECT format('%.2f', 3.14159);               -- '3.14'

glob

glob(pattern, string) -> integer

Tests whether the string matches the glob pattern. Equivalent to the expression string GLOB pattern. Returns 1 for a match, 0 otherwise. Glob patterns use * to match any sequence of characters and ? to match any single character. Matching is case-sensitive.

SELECT glob('*ello', 'Hello');  -- 1
SELECT glob('H?llo', 'Hello');  -- 1

hex

hex(X) -> text

Returns an uppercase hexadecimal string rendering of the content of X. If X is a text string, each character is converted to its UTF-8 byte representation. If X is a blob, the raw bytes are converted.

SELECT hex('Hello');       -- '48656C6C6F'
SELECT hex(X'CAFE');       -- 'CAFE'
SELECT hex(zeroblob(4));   -- '00000000'

ifnull

ifnull(X, Y) -> value

Returns X if X is not NULL, otherwise returns Y. Equivalent to coalesce(X, Y) but restricted to exactly two arguments.

SELECT ifnull(NULL, 'backup');  -- 'backup'
SELECT ifnull('value', 42);    -- 'value'

iif

iif(condition, true_value, false_value) -> value

Returns true_value if condition is true, or false_value if condition is false or NULL. The if keyword is an alias for iif.

SELECT iif(1, 'yes', 'no');   -- 'yes'
SELECT iif(0, 'yes', 'no');   -- 'no'
SELECT if(1 > 0, 'positive', 'non-positive');  -- 'positive'

instr

instr(X, Y) -> integer

Returns the 1-based position of the first occurrence of Y within X, or 0 if Y is not found. Returns NULL if either argument is NULL. Both arguments must be the same type (both text or both blob).

SELECT instr('Hello World', 'World');  -- 7
SELECT instr('Hello World', 'xyz');    -- 0
SELECT instr('hello', NULL);           -- NULL

last_insert_rowid

last_insert_rowid() -> integer

Returns the ROWID of the most recent successful INSERT into a rowid table from the same database connection. Returns 0 if no INSERT has occurred.

SELECT last_insert_rowid();  -- 0 (before any inserts)

length

length(X) -> integer

Returns the number of characters in X if X is a text string, or the number of bytes if X is a blob. Returns NULL if X is NULL.

SELECT length('Hello');           -- 5
SELECT length(X'0102030405');     -- 5
SELECT length(NULL);              -- NULL

like

like(pattern, string) -> integer

Tests whether the string matches the LIKE pattern. Equivalent to string LIKE pattern. The % wildcard matches any sequence of characters and _ matches any single character. Matching is case-insensitive for ASCII characters.

SELECT like('H%', 'Hello');     -- 1
SELECT like('H_llo', 'Hello');  -- 1
SELECT like('%world%', 'Hello World');  -- 1

likelihood

likelihood(X, P) -> value

Returns X unchanged. The second argument P is a probability hint (a floating-point number between 0.0 and 1.0) for the query planner, indicating the likelihood that X is true. This function has no effect on the result, only on query optimization.

SELECT likelihood(1, 0.5);  -- 1

likely

likely(X) -> value

Returns X unchanged. Provides a hint to the query planner that X is usually true (equivalent to likelihood(X, 0.9375)). This function has no effect on the result, only on query optimization.

SELECT likely(1);  -- 1

lower

lower(X) -> text

Returns a copy of X with all ASCII uppercase characters converted to lowercase. Non-ASCII characters are unchanged.

SELECT lower('HELLO');       -- 'hello'
SELECT lower('Hello World'); -- 'hello world'

ltrim, rtrim, trim

ltrim(X) -> text ltrim(X, Y) -> text rtrim(X) -> text rtrim(X, Y) -> text trim(X) -> text trim(X, Y) -> text

ltrim removes characters from the left side of X. rtrim removes from the right side. trim removes from both sides. With one argument, spaces are removed. With two arguments, all characters found in Y are removed from the respective side(s).

SELECT ltrim('   Hello');          -- 'Hello'
SELECT ltrim('xxxHello', 'x');     -- 'Hello'
SELECT rtrim('Hello   ');          -- 'Hello'
SELECT rtrim('Helloxxxx', 'x');    -- 'Hello'
SELECT trim('  Hello  ');          -- 'Hello'
SELECT trim('xxHelloxx', 'x');     -- 'Hello'

max (scalar)

max(X, Y, …) -> value

Returns the argument with the maximum value when given two or more arguments. Uses the standard comparison rules to determine ordering. Returns NULL if any argument is NULL. (With a single argument, max acts as an aggregate function instead.)

SELECT max(1, 5, 3);           -- 5
SELECT max('alpha', 'beta');   -- 'beta'

min (scalar)

min(X, Y, …) -> value

Returns the argument with the minimum value when given two or more arguments. Uses the standard comparison rules to determine ordering. Returns NULL if any argument is NULL. (With a single argument, min acts as an aggregate function instead.)

SELECT min(1, 5, 3);           -- 1
SELECT min('alpha', 'beta');   -- 'alpha'

nullif

nullif(X, Y) -> value

Returns X if X and Y are different, or NULL if they are equal. Useful for converting sentinel values into NULLs.

SELECT nullif(5, 5);    -- NULL
SELECT nullif(5, 3);    -- 5
SELECT nullif('', '');   -- NULL

octet_length

octet_length(X) -> integer

Returns the number of bytes in X. Unlike length, which counts characters for text values, octet_length always counts bytes. Returns NULL if X is NULL.

SELECT octet_length('Hello');           -- 5
SELECT octet_length(X'0102030405');     -- 5

printf

printf(FORMAT, …) -> text

Alias for format. Returns a string formed by substituting arguments into the format string using printf conventions. Returns NULL if FORMAT is NULL.

SELECT printf('%d items', 5);    -- '5 items'
SELECT printf('%.2f', 3.14159);  -- '3.14'

quote

quote(X) -> text

Returns a string that is the SQL literal representation of X. Text strings are surrounded by single quotes with internal quotes doubled. Blobs are returned as hex literals. NULL returns the string NULL. Integers and reals are returned as-is.

SELECT quote('Hello');          -- 'Hello'  (with enclosing quotes)
SELECT quote(3.14);             -- 3.14
SELECT quote(NULL);             -- NULL
SELECT quote(X'48656C6C6F');    -- X'48656C6C6F'

random

random() -> integer

Returns a pseudo-random integer between -9223372036854775808 and +9223372036854775807. A different value is returned each time the function is called.

SELECT typeof(random());  -- 'integer'

randomblob

randomblob(N) -> blob

Returns an N-byte blob containing pseudo-random bytes. Useful for generating unique identifiers or random data.

SELECT length(randomblob(16));  -- 16
SELECT hex(randomblob(4));      -- (random 8-character hex string)

replace

replace(X, Y, Z) -> text

Returns a copy of X with every occurrence of Y replaced by Z. If Y is an empty string, X is returned unchanged.

SELECT replace('Hello World', 'World', 'Turso');  -- 'Hello Turso'
SELECT replace('aabbcc', 'bb', 'XX');              -- 'aaXXcc'

round

round(X) -> real round(X, Y) -> real

Rounds X to Y decimal places. If Y is omitted, it defaults to 0. The result is always a floating-point value.

SELECT round(3.14159);     -- 3.0
SELECT round(3.14159, 2);  -- 3.14
SELECT round(123.5);       -- 124.0

sign

sign(X) -> integer

Returns -1 for negative values, 0 for zero, or +1 for positive values. Returns NULL if X is NULL or is a string or blob that cannot be converted to a number.

SELECT sign(-42);   -- -1
SELECT sign(0);     -- 0
SELECT sign(42);    -- 1
SELECT sign(NULL);  -- NULL

soundex

soundex(X) -> text

Returns the Soundex encoding of X. The Soundex encoding is a four-character string that represents the phonetic sound of the input. Returns ?000 if X is NULL or contains no ASCII letters.

SELECT soundex('Robert');   -- 'R163'
SELECT soundex('Rupert');   -- 'R163'

sqlite_source_id

sqlite_source_id() -> text

Returns a string that identifies the specific version and build of the library. The format includes a date, time, and a SHA hash.

SELECT sqlite_source_id();
-- e.g. '2026-02-11 18:26:30 fd3ab2fd48b711aa9bec80562dd1175ce10f4d9a'

sqlite_version

sqlite_version() -> text

Returns the version string for the SQLite-compatible library.

SELECT sqlite_version();  -- e.g. '3.50.4'

substr / substring

substr(X, Y) -> text substr(X, Y, Z) -> text substring(X, Y) -> text substring(X, Y, Z) -> text

Returns a substring of X starting at position Y (1-based). If Z is given, it specifies the length of the substring; otherwise the substring extends to the end of the string. substring is an alias for substr.

If Y is negative, the substring starts that many characters from the end. If Z is negative, the substring consists of the Z characters preceding (and including) position Y.

SELECT substr('Hello World', 7);      -- 'World'
SELECT substr('Hello World', 1, 5);   -- 'Hello'
SELECT substring('Hello World', 7);   -- 'World'

typeof

typeof(X) -> text

Returns the type of X as a string. The possible return values are 'null', 'integer', 'real', 'text', and 'blob'.

SELECT typeof(42);        -- 'integer'
SELECT typeof(3.14);      -- 'real'
SELECT typeof('Hello');   -- 'text'
SELECT typeof(NULL);      -- 'null'
SELECT typeof(X'CAFE');   -- 'blob'

unhex

unhex(X) -> blob unhex(X, Y) -> blob

Converts the hexadecimal string X into a blob. Returns NULL if X contains characters that are not hexadecimal digits, unless those characters appear in Y (the set of characters to ignore). Returns NULL if X or Y is NULL.

SELECT unhex('48656C6C6F');      -- Hello (as blob)
SELECT hex(unhex('48656C6C6F')); -- '48656C6C6F'
SELECT unhex('48GG', 'G');       -- H (as blob, 'G' chars are ignored)

unicode

unicode(X) -> integer

Returns the numeric Unicode code point of the first character of X. Returns NULL if X is an empty string.

SELECT unicode('A');     -- 65
SELECT unicode('Hello'); -- 72

unlikely

unlikely(X) -> value

Returns X unchanged. Provides a hint to the query planner that X is usually false (equivalent to likelihood(X, 0.0625)). This function has no effect on the result, only on query optimization.

SELECT unlikely(1);  -- 1

upper

upper(X) -> text

Returns a copy of X with all ASCII lowercase characters converted to uppercase. Non-ASCII characters are unchanged.

SELECT upper('hello');        -- 'HELLO'
SELECT upper('Hello World');  -- 'HELLO WORLD'

zeroblob

zeroblob(N) -> blob

Returns a blob consisting of N zero-valued bytes (0x00). Useful as a placeholder for blob values to be filled in later with incremental I/O.

SELECT length(zeroblob(10));  -- 10
SELECT hex(zeroblob(4));      -- '00000000'

Aggregate Functions

Aggregate functions compute a single result from a set of input values. They are most commonly used with a GROUP BY clause to produce one result per group of rows, but when used without GROUP BY, they treat the entire result set as a single group and return one row.

All built-in aggregate functions ignore NULL inputs (except count(*)). If every input to an aggregate function is NULL, the result is NULL – with the exceptions of count() (which returns 0) and total() (which returns 0.0). This NULL-skipping behavior is consistent with the SQL standard.

Aggregate functions can be combined with GROUP BY and HAVING to filter groups after aggregation. See GROUP BY and HAVING for details on grouping.

The DISTINCT keyword can precede the argument of any single-argument aggregate to eliminate duplicate values before processing:

SELECT count(DISTINCT category) FROM products;

The examples below use the following tables:

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  category TEXT,
  price REAL
);
INSERT INTO products VALUES
  (1, 'Laptop', 'Electronics', 999.99),
  (2, 'Phone', 'Electronics', 699.99),
  (3, 'Desk', 'Furniture', 299.99),
  (4, 'Chair', 'Furniture', 199.99),
  (5, 'Tablet', 'Electronics', 499.99);

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  user_id INTEGER,
  product TEXT,
  amount REAL
);
INSERT INTO orders VALUES
  (1, 1, 'Laptop', 999.99),
  (2, 1, 'Phone', 699.99),
  (3, 2, 'Desk', 299.99),
  (4, 3, 'Chair', 199.99),
  (5, 1, 'Tablet', 499.99);

avg

avg(X) -> real

Returns the average of all non-NULL values of X. The result is always a floating-point value when there is at least one non-NULL input. String and BLOB values that do not look like numbers are treated as 0. Returns NULL if all inputs are NULL or if the input set is empty. Internally, avg(X) is equivalent to total(X) / count(X).

SELECT avg(amount) FROM orders;
-- 539.99
SELECT user_id, avg(amount) AS avg_order
  FROM orders
  GROUP BY user_id;
-- 1 | 733.323333333333
-- 2 | 299.99
-- 3 | 199.99

The DISTINCT keyword causes duplicate values to be removed before computing the average:

SELECT avg(DISTINCT x) FROM (SELECT 1 AS x UNION ALL SELECT 2 UNION ALL SELECT 2 UNION ALL SELECT 3);
-- 2.0

count

count(X) -> integer count(*) -> integer

The count(X) form returns the number of times X is not NULL. The count(*) form returns the total number of rows in the group, regardless of NULL values.

count() always returns an integer and never returns NULL. For an empty input set, both forms return 0.

SELECT count(*) FROM orders;
-- 5
SELECT count(amount) FROM orders;
-- 5
SELECT count(DISTINCT user_id) FROM orders;
-- 3
SELECT user_id, count(*) AS order_count
  FROM orders
  GROUP BY user_id;
-- 1 | 3
-- 2 | 1
-- 3 | 1

group_concat

group_concat(X) -> text group_concat(X, Y) -> text

Returns a string formed by concatenating all non-NULL values of X, separated by Y. If Y is omitted, a comma (",") is used as the default separator. Returns NULL if all inputs are NULL.

The order of concatenated elements is determined by the order in which rows are processed. When used with GROUP BY, the order within each group is not guaranteed unless the query uses a subquery or other mechanism to control row ordering.

SELECT group_concat(product) FROM orders;
-- Laptop,Phone,Desk,Chair,Tablet
SELECT group_concat(product, ' | ') FROM orders;
-- Laptop | Phone | Desk | Chair | Tablet
SELECT user_id, group_concat(product, ', ') AS products
  FROM orders
  GROUP BY user_id;
-- 1 | Laptop, Phone, Tablet
-- 2 | Desk
-- 3 | Chair

string_agg

string_agg(X, Y) -> text

An alias for group_concat(X, Y). Returns a string formed by concatenating all non-NULL values of X, separated by Y. Unlike group_concat, the separator argument Y is required. This form provides compatibility with PostgreSQL and SQL Server, which use string_agg rather than group_concat.

SELECT string_agg(product, ', ') FROM orders;
-- Laptop, Phone, Desk, Chair, Tablet

max

max(X) -> value

Returns the maximum value of all non-NULL values of X. The maximum is determined by the sort order that would be used by ORDER BY on the same column. Returns NULL if all inputs are NULL or if the input set is empty.

max() works on any type: integers, reals, text, and BLOBs are compared using the normal comparison rules.

SELECT max(amount) FROM orders;
-- 999.99
SELECT max(name) FROM products;
-- Tablet
SELECT category, max(price) AS most_expensive
  FROM products
  GROUP BY category;
-- Electronics | 999.99
-- Furniture   | 299.99

min

min(X) -> value

Returns the minimum non-NULL value of X. The minimum is the value that would appear first in an ORDER BY on the same column. Returns NULL if all inputs are NULL or if the input set is empty.

min() works on any type: integers, reals, text, and BLOBs are compared using the normal comparison rules.

SELECT min(amount) FROM orders;
-- 199.99
SELECT min(name) FROM products;
-- Chair
SELECT category, min(price) AS cheapest
  FROM products
  GROUP BY category;
-- Electronics | 499.99
-- Furniture   | 199.99

sum

sum(X) -> integer or real

Returns the sum of all non-NULL values of X. If all inputs are integers, the result is an integer. If any input is a real number, the result is a real. Returns NULL if all inputs are NULL or if the input set is empty.

An integer overflow error is raised if all inputs are integers and the sum exceeds the integer range.

SELECT sum(amount) FROM orders;
-- 2699.95
-- sum returns integer type when all inputs are integers
SELECT typeof(sum(x)) FROM (SELECT 1 AS x UNION ALL SELECT 2 UNION ALL SELECT 3);
-- integer

-- sum returns real type when any input is real
SELECT typeof(sum(x)) FROM (SELECT 1.0 AS x UNION ALL SELECT 2.0 UNION ALL SELECT 3.0);
-- real
SELECT user_id, sum(amount) AS total_spent
  FROM orders
  GROUP BY user_id
  HAVING total_spent > 500;
-- 1 | 2199.97

The DISTINCT keyword causes duplicate values to be removed before summing:

SELECT sum(DISTINCT x) FROM (SELECT 1 AS x UNION ALL SELECT 2 UNION ALL SELECT 2 UNION ALL SELECT 3);
-- 6

total

total(X) -> real

Returns the sum of all non-NULL values of X, similar to sum(), but with two key differences:

  1. total() always returns a floating-point value (0.0 for an empty set), whereas sum() returns NULL for an empty set.
  2. total() never raises an integer overflow error.

Use total() when you need a guaranteed numeric result and want to avoid NULL checks.

Behaviorsum(X)total(X)
Empty set resultNULL0.0
Return type (integer inputs)integerreal
Return type (real inputs)realreal
Integer overflowraises errorno error
SELECT total(amount) FROM orders;
-- 2699.95
-- total returns 0.0 on an empty table; sum returns NULL
CREATE TABLE empty (x INTEGER);
SELECT sum(x), total(x) FROM empty;
-- (NULL) | 0.0
-- total always returns real type, even with integer inputs
SELECT typeof(total(x)) FROM (SELECT 1 AS x UNION ALL SELECT 2 UNION ALL SELECT 3);
-- real

Turso Extension Aggregates

The following aggregate functions are provided by the percentile extension. They are not built-in and must be loaded explicitly before use. Once loaded, they behave like any other aggregate function and can be used with GROUP BY, HAVING, and DISTINCT.

.load liblimbo_percentile

median

median(X) -> real

Returns the median value of all non-NULL values of X. For an odd number of values, this is the middle value. For an even number of values, this is the average of the two middle values. Returns NULL if all inputs are NULL.

.load liblimbo_percentile

CREATE TABLE scores (value REAL);
INSERT INTO scores VALUES (1.0), (2.0), (3.0), (4.0), (5.0);

SELECT median(value) FROM scores;
-- 3.0

percentile

percentile(Y, P) -> real

Returns the value at the P-th percentile among all non-NULL values of Y, using linear interpolation. P is expressed on a 0-to-100 scale (e.g., 50 for the median) and must be the same value for every row in the group. An error is raised if P is outside the range 0 to 100 or if different rows supply different values of P. Returns NULL if all inputs are NULL.

SELECT percentile(value, 50) FROM scores;
-- 3.0

percentile_cont

percentile_cont(Y, P) -> real

Computes a percentile using continuous distribution, following the SQL standard PERCENTILE_CONT semantics. P is expressed on a 0.0-to-1.0 scale (e.g., 0.5 for the median) and must be the same value for every row in the group. Uses linear interpolation between adjacent values when the percentile falls between two data points. Returns NULL if all inputs are NULL.

SELECT percentile_cont(value, 0.5) FROM scores;
-- 3.0

percentile_disc

percentile_disc(Y, P) -> real

Computes a percentile using discrete distribution, following the SQL standard PERCENTILE_DISC semantics. P is expressed on a 0.0-to-1.0 scale and must be the same value for every row in the group. Unlike percentile_cont, this function always returns an actual value from the input set rather than interpolating between values. Returns NULL if all inputs are NULL.

SELECT percentile_disc(value, 0.5) FROM scores;
-- 3.0

stddev

stddev(X) -> real

Returns the population standard deviation of all non-NULL values of X. The standard deviation measures how spread out values are from their mean. Returns NULL if all inputs are NULL or if the input set is empty.

SELECT stddev(value) FROM scores;
-- 1.58113883008419

Date and Time Functions

Turso provides seven date and time functions for computing dates, times, and timestamps. All functions operate on UTC internally and accept an optional list of modifiers that transform the result.

date

date(time-value, modifier, …) -> TEXT

Returns the date as text in the format YYYY-MM-DD.

SELECT date('2024-06-15 14:30:00');  -- 2024-06-15
SELECT date('2024-06-15', '+10 days');  -- 2024-06-25
SELECT date('2024-06-15', '-30 days');  -- 2024-05-16

time

time(time-value, modifier, …) -> TEXT

Returns the time as text in the format HH:MM:SS.

SELECT time('2024-06-15 14:30:45');  -- 14:30:45
SELECT time('14:30:00');  -- 14:30:00

datetime

datetime(time-value, modifier, …) -> TEXT

Returns the date and time as text in the format YYYY-MM-DD HH:MM:SS. When the subsec modifier is present, the output includes fractional seconds: YYYY-MM-DD HH:MM:SS.SSS.

SELECT datetime('2024-06-15 14:30:00');  -- 2024-06-15 14:30:00
SELECT datetime('2024-06-15 14:30:00', '+3 hours');  -- 2024-06-15 17:30:00
SELECT datetime('2024-06-15 14:30:00', 'subsec');  -- 2024-06-15 14:30:00.000

Converting a Unix timestamp to a human-readable datetime:

SELECT datetime(1718461800, 'unixepoch');  -- 2024-06-15 14:30:00

julianday

julianday(time-value, modifier, …) -> REAL

Returns the Julian day number – the fractional number of days since noon in Greenwich on November 24, 4714 B.C. (Proleptic Gregorian calendar).

SELECT julianday('2024-06-15');  -- 2460476.5

Compute the number of days between two dates:

SELECT julianday('2024-06-15') - julianday('2024-01-01');  -- 166.0

unixepoch

unixepoch(time-value, modifier, …) -> INTEGER

Returns the number of seconds since 1970-01-01 00:00:00 UTC. Returns an integer by default; use the subsec modifier for a floating-point result with fractional seconds.

SELECT unixepoch('2024-06-15 14:30:00');  -- 1718461800

Compute the number of seconds between two timestamps:

SELECT unixepoch('2024-06-15 14:30:00') - unixepoch('2024-06-15 12:00:00');  -- 9000

strftime

strftime(format, time-value, modifier, …) -> TEXT

Returns the date formatted according to the format string specified as the first argument. The format string supports the substitutions listed in the Format Specifiers table below.

SELECT strftime('%Y/%m/%d', '2024-06-15');  -- 2024/06/15
SELECT strftime('%H:%M', '2024-06-15 14:30:00');  -- 14:30
SELECT strftime('%j', '2024-06-15');  -- 167
SELECT strftime('%s', '2024-06-15 14:30:00');  -- 1718461800
SELECT strftime('%J', '2024-06-15');  -- 2460476.5

The other date/time functions can be expressed as strftime equivalents:

Functionstrftime Equivalent
date(...)strftime('%F', ...)
time(...)strftime('%T', ...)
datetime(...)strftime('%F %T', ...)
julianday(...)CAST(strftime('%J', ...) AS REAL)
unixepoch(...)CAST(strftime('%s', ...) AS INT)

timediff

timediff(time-value-A, time-value-B) -> TEXT

Returns a text string describing the amount of time that must be added to the second argument to reach the first. The result has the format (+|-)YYYY-MM-DD HH:MM:SS.SSS.

The timediff function does not accept modifiers. It satisfies the invariant that datetime(A) equals datetime(B, timediff(A, B)).

SELECT timediff('2024-06-15', '2024-01-01');  -- +0000-05-14 00:00:00.000
SELECT timediff('2024-06-15 14:30:00', '2024-06-15 12:00:00');  -- +0000-00-00 02:30:00.000

Because months and years vary in length, timediff may return the same text for intervals that span different numbers of days. For precise day-level differences, subtract Julian day numbers instead:

SELECT julianday('2024-06-15') - julianday('2024-01-01');  -- 166.0

Time Value Formats

All date/time functions accept time values in the following formats:

FormatExample
YYYY-MM-DD'2024-06-15'
YYYY-MM-DD HH:MM'2024-06-15 14:30'
YYYY-MM-DD HH:MM:SS'2024-06-15 14:30:00'
YYYY-MM-DD HH:MM:SS.SSS'2024-06-15 14:30:00.123'
YYYY-MM-DDTHH:MM:SS'2024-06-15T14:30:00'
HH:MM:SS'14:30:00' (assumes date 2000-01-01)
'now'Current date and time in UTC
DDDDDDDDDDJulian day number as a numeric value

The ISO 8601 T separator between date and time is accepted interchangeably with a space:

SELECT date('2024-06-15T14:30:00');  -- 2024-06-15

An optional timezone suffix [+-]HH:MM or Z may follow any format that includes a time component. The suffix Z denotes UTC (a no-op). A non-zero offset is subtracted to convert the value to UTC.

Modifiers

All date/time functions except timediff accept zero or more modifiers after the time value. Modifiers are applied from left to right; order matters.

Arithmetic Modifiers

Add or subtract a duration from the time value. The NNN value can be an integer or floating-point number, with an optional + or - prefix. The trailing s is optional (e.g., '+1 day' and '+1 days' are equivalent).

ModifierEffect
NNN daysAdd NNN days
NNN hoursAdd NNN hours
NNN minutesAdd NNN minutes
NNN secondsAdd NNN seconds
NNN monthsAdd NNN months
NNN yearsAdd NNN years
SELECT date('2024-06-15', '+1 month');  -- 2024-07-15
SELECT date('2024-06-15', '+1 year');  -- 2025-06-15
SELECT datetime('2024-06-15 14:30:00', '+90 minutes');  -- 2024-06-15 16:00:00
SELECT datetime('2024-06-15 14:30:00', '+30 seconds');  -- 2024-06-15 14:30:30

Multiple modifiers chain together from left to right:

SELECT datetime('2024-06-15 14:30:00', '+1 year', '-2 months');  -- 2025-04-15 14:30:00

Time/Date/DateTime Offset Modifiers

Shift a time value by a compound offset specified in time, date, or full datetime format. A leading + or - is required for date offsets.

Modifier FormatExample
[+-]HH:MM'-05:00'
[+-]HH:MM:SS'+01:30:00'
[+-]YYYY-MM-DD'+0001-06-00'
[+-]YYYY-MM-DD HH:MM:SS'+0000-00-01 02:00:00'
SELECT datetime('2024-06-15 14:30:00', '-05:00');  -- 2024-06-15 09:30:00

Start-of Modifiers

Truncate the time value backward to the beginning of a period.

ModifierEffect
start of daySets time to 00:00:00
start of monthSets to first day of the month at 00:00:00
start of yearSets to January 1 at 00:00:00
SELECT datetime('2024-06-15 14:30:00', 'start of day');  -- 2024-06-15 00:00:00
SELECT date('2024-06-15', 'start of month');  -- 2024-06-01
SELECT date('2024-06-15', 'start of year');  -- 2024-01-01

Chaining start-of modifiers with arithmetic is useful for computing boundaries:

-- Last day of the current month
SELECT date('2024-06-15', 'start of month', '+1 month', '-1 day');  -- 2024-06-30

weekday N

Advance the date forward to the next occurrence of weekday N, where Sunday is 0, Monday is 1, through Saturday which is 6. If the date already falls on the requested weekday, it is left unchanged.

-- 2024-06-15 is a Saturday (6); the next Sunday (0) is 2024-06-16
SELECT date('2024-06-15', 'weekday 0');  -- 2024-06-16

-- First Tuesday in October 2024
SELECT date('2024-06-15', 'start of year', '+9 months', 'weekday 2');  -- 2024-10-01

ceiling and floor

When adding months or years produces an ambiguous date (for example, one month after January 31), the ceiling and floor modifiers control how the ambiguity is resolved.

  • ceiling (the default) rounds forward to the next valid date.
  • floor rounds backward to the last day of the previous month.
SELECT date('2024-01-31', '+1 month', 'ceiling');  -- 2024-03-02
SELECT date('2024-01-31', '+1 month', 'floor');  -- 2024-02-29

Format Interpretation Modifiers

These modifiers control how a bare numeric time value (DDDDDDDDDD) is interpreted. They must appear immediately after the time value.

ModifierEffect
unixepochInterpret the number as seconds since 1970-01-01 00:00:00 UTC
juliandayInterpret the number as a Julian day number (the default)
autoAutomatically choose based on magnitude: small values are Julian day numbers, large values are Unix timestamps
SELECT datetime(1718461800, 'unixepoch');  -- 2024-06-15 14:30:00
SELECT datetime(2460476.5, 'julianday');  -- 2024-06-15 00:00:00
SELECT datetime(1718461800, 'auto');  -- 2024-06-15 14:30:00

Timezone Modifiers

ModifierEffect
localtimeConvert from UTC to local time
utcConvert from local time to UTC

subsec

The subsec (or subsecond) modifier increases output resolution from seconds to milliseconds.

  • With datetime or time: appends .SSS to the seconds field.
  • With unixepoch: returns a floating-point value instead of an integer.
SELECT datetime('2024-06-15 14:30:00', 'subsec');  -- 2024-06-15 14:30:00.000

Format Specifiers

The strftime function accepts the following format specifiers:

SpecifierDescriptionExample
%dDay of month: 01-3115
%fFractional seconds: SS.SSS00.123
%HHour: 00-2414
%jDay of year: 001-366167
%JJulian day number (fractional)2460476.5
%mMonth: 01-1206
%MMinute: 00-5930
%sSeconds since 1970-01-011718461800
%SSeconds: 00-5945
%wDay of week: 0-6 (Sunday=0)6
%WWeek of year: 00-5324
%YYear: 0000-99992024
%%Literal % character%
SELECT strftime('%f', '2024-06-15 14:30:00.123');  -- 00.123
SELECT strftime('%w', '2024-06-15');  -- 6
SELECT strftime('%W', '2024-06-15');  -- 24
SELECT strftime('%S', '2024-06-15 14:30:45');  -- 45
SELECT strftime('%m', '2024-06-15');  -- 06

Unrecognized format specifiers return NULL.

NULL Handling

All date/time functions return NULL when given an invalid input, an out-of-range date, or an unrecognized modifier. The valid date range is 0000-01-01 00:00:00 through 9999-12-31 23:59:59.

Calendar Notes

All computations use the Proleptic Gregorian calendar. Each day is treated as exactly 86,400 seconds; leap seconds are not incorporated.

Math Functions

Turso provides a full set of mathematical functions for trigonometry, logarithms, exponentiation, and rounding. All math functions accept integers, floating-point numbers, or strings that look like numbers. They return an IEEE 754 double-precision floating-point result, except when the input is an integer and the result can be represented exactly as an integer.

All math functions return NULL when any argument is NULL, when an argument is a blob or non-numeric string, or when the result would be mathematically undefined (a domain error).

Constants

pi

pi() -> real

Returns an approximation of the mathematical constant pi.

SELECT pi();  -- 3.14159265358979

Trigonometric Functions

All trigonometric functions work in radians. Use radians() and degrees() to convert between degrees and radians.

acos

acos(X) -> real

Returns the arccosine of X, in radians. X must be between -1.0 and 1.0; returns NULL for values outside that range.

SELECT acos(0.5);  -- 1.0471975511966

asin

asin(X) -> real

Returns the arcsine of X, in radians. X must be between -1.0 and 1.0; returns NULL for values outside that range.

SELECT asin(0.5);  -- 0.523598775598299

atan

atan(X) -> real

Returns the arctangent of X, in radians.

SELECT atan(1.0);  -- 0.785398163397448

atan2

atan2(Y, X) -> real

Returns the arctangent of Y/X, in radians, using the signs of both arguments to determine the quadrant of the result. Unlike atan(Y/X), atan2 correctly handles cases where X is zero.

SELECT atan2(1.0, 1.0);  -- 0.785398163397448

cos

cos(X) -> real

Returns the cosine of X, where X is in radians.

SELECT cos(0.0);  -- 1.0

sin

sin(X) -> real

Returns the sine of X, where X is in radians.

SELECT sin(pi() / 6);  -- 0.5

tan

tan(X) -> real

Returns the tangent of X, where X is in radians.

SELECT tan(pi() / 4);  -- 1.0

Hyperbolic Functions

acosh

acosh(X) -> real

Returns the inverse hyperbolic cosine of X. X must be greater than or equal to 1.0; returns NULL for values less than 1.0.

SELECT acosh(2.0);  -- 1.31695789692482

asinh

asinh(X) -> real

Returns the inverse hyperbolic sine of X.

SELECT asinh(1.0);  -- 0.881373587019543

atanh

atanh(X) -> real

Returns the inverse hyperbolic tangent of X. X must be between -1.0 and 1.0 (exclusive); returns NULL for values outside that range.

SELECT atanh(0.5);  -- 0.549306144334055

cosh

cosh(X) -> real

Returns the hyperbolic cosine of X.

SELECT cosh(1.0);  -- 1.54308063481524

sinh

sinh(X) -> real

Returns the hyperbolic sine of X.

SELECT sinh(1.0);  -- 1.1752011936438

tanh

tanh(X) -> real

Returns the hyperbolic tangent of X.

SELECT tanh(1.0);  -- 0.761594155955765

Angle Conversion

degrees

degrees(X) -> real

Converts X from radians to degrees.

SELECT degrees(pi());  -- 180.0

radians

radians(X) -> real

Converts X from degrees to radians.

SELECT radians(180.0);  -- 3.14159265358979

Exponential and Logarithmic Functions

exp

exp(X) -> real

Returns the value of e (Euler’s number, approximately 2.71828) raised to the power X.

SELECT exp(1.0);  -- 2.71828182845905

ln

ln(X) -> real

Returns the natural logarithm (base e) of X. Returns NULL if X is less than or equal to zero.

SELECT ln(exp(1.0));  -- 1.0

log

log(X) -> real

With a single argument, returns the base-10 logarithm of X. Returns NULL if X is less than or equal to zero.

SELECT log(100.0);  -- 2.0

log(B, X) -> real

With two arguments, returns the base-B logarithm of X. Returns NULL if either argument is less than or equal to zero.

SELECT log(2, 8);  -- 3.0

Note: the single-argument form of log() computes base-10, not the natural logarithm. Use ln() for the natural logarithm.

log10

log10(X) -> real

Returns the base-10 logarithm of X. Returns NULL if X is less than or equal to zero. Equivalent to log(X) with a single argument.

SELECT log10(1000.0);  -- 3.0

log2

log2(X) -> real

Returns the base-2 logarithm of X. Returns NULL if X is less than or equal to zero.

SELECT log2(64);  -- 6.0

Power and Root Functions

pow / power

pow(X, Y) -> real power(X, Y) -> real

Returns X raised to the power Y. pow and power are aliases for the same function.

SELECT pow(2, 10);   -- 1024.0
SELECT power(3, 4);  -- 81.0

sqrt

sqrt(X) -> real

Returns the square root of X. Returns NULL if X is negative.

SELECT sqrt(144);  -- 12.0

Rounding Functions

ceil / ceiling

ceil(X) -> integer/real ceiling(X) -> integer/real

Returns the smallest integer not less than X (rounds toward positive infinity). ceil and ceiling are aliases. When the input is an integer, the result is that same integer. When the input is a float, the result is a float with an integer value.

SELECT ceil(3.2);     -- 4.0
SELECT ceiling(-2.8); -- -2.0
SELECT ceil(5);       -- 5

floor

floor(X) -> integer/real

Returns the largest integer not greater than X (rounds toward negative infinity). When the input is an integer, the result is that same integer. When the input is a float, the result is a float with an integer value.

SELECT floor(3.8);   -- 3.0
SELECT floor(-2.3);  -- -3.0
SELECT floor(5);     -- 5

trunc

trunc(X) -> integer/real

Returns the integer part of X by removing any fractional digits (rounds toward zero). When the input is an integer, the result is that same integer. When the input is a float, the result is a float with an integer value.

SELECT trunc(3.7);   -- 3.0
SELECT trunc(-3.7);  -- -3.0
SELECT trunc(5);     -- 5

The difference between these three rounding functions is visible with negative values:

Inputceilfloortrunc
3.74.03.03.0
-3.7-3.0-4.0-3.0

ceil rounds toward positive infinity, floor rounds toward negative infinity, and trunc rounds toward zero.

Remainder Function

mod

mod(X, Y) -> real

Returns the remainder after dividing X by Y. Unlike the % operator, mod() works correctly with floating-point arguments. Returns NULL if Y is zero.

SELECT mod(10, 3);      -- 1.0
SELECT mod(10.5, 3.0);  -- 1.5

NULL and Error Handling

All math functions follow these rules:

  • NULL input: any NULL argument produces a NULL result.
  • Non-numeric input: blob values and strings that cannot be converted to a number produce a NULL result.
  • Domain errors: operations with no real-valued result return NULL rather than raising an error. Examples include sqrt(-1), acos(2.0), ln(-1), and log(-5).
SELECT sqrt(-1);    -- NULL
SELECT acos(2.0);   -- NULL
SELECT acos(NULL);  -- NULL
SELECT ln(-1);      -- NULL

JSON Functions

Turso includes a comprehensive set of JSON functions for creating, querying, modifying, and aggregating JSON data. These functions accept both JSON text and JSONB (binary JSON) as input.

JSON Path Syntax

Many JSON functions take a path argument that identifies a specific element within a JSON structure. A well-formed path begins with $ (the root element) followed by zero or more accessors:

AccessorMeaning
$.keyObject member by name
$[N]Array element at index N (0-based)
$[#-N]Array element N from the end
$[#]Position after last array element (for appending)

Examples: $ (root), $.name (object field), $[0] (first array element), $.items[2].price (nested access), $[#-1] (last array element).

Value Argument Conventions

When a function parameter is labeled “value,” plain text arguments become quoted JSON strings in the result. To embed actual JSON (not a string), wrap the value with json() or another JSON function:

SELECT json_object('data', '[1,2,3]');           -- {"data":"[1,2,3]"}
SELECT json_object('data', json('[1,2,3]'));      -- {"data":[1,2,3]}

Creation Functions

json

json(json) -> text

Validates and minifies a JSON string. Accepts JSON text, JSONB blobs, and JSON5 input. Returns canonical RFC-8259 JSON with whitespace removed. Raises an error if the input is malformed.

SELECT json(' { "name" : "Alice", "scores": [ 90, 85 ] } ');
-- {"name":"Alice","scores":[90,85]}

JSON5 extensions such as unquoted keys, trailing commas, and comments are accepted on input but normalized to standard JSON on output:

SELECT json('{name: "Alice", age: 25}');
-- {"name":"Alice","age":25}

jsonb

jsonb(json) -> blob

Returns the JSONB (binary) representation of the input. JSONB is an opaque binary format that can be faster to process than text JSON. All JSON functions accept JSONB as input.

SELECT json(jsonb('{"name":"John","age":30}'));
-- {"name":"John","age":30}

json_array

json_array(value1, value2, …) -> text

Creates a JSON array from the given arguments. Text arguments become quoted JSON strings unless they come from another JSON function.

SELECT json_array(1, 2, '3', 4);              -- [1,2,"3",4]
SELECT json_array(json_array(1, 2), 'text');   -- [[1,2],"text"]
SELECT json_array(1, null, json('[4,5]'));      -- [1,null,[4,5]]

jsonb_array(...) returns the same result in JSONB format.

json_object

json_object(label1, value1, …) -> text

Creates a JSON object from label/value pairs. Labels must be strings. Text values become quoted JSON strings unless produced by another JSON function.

SELECT json_object('name', 'Alice', 'age', 25);
-- {"name":"Alice","age":25}

SELECT json_object('user', json_object('id', 1, 'role', 'admin'));
-- {"user":{"id":1,"role":"admin"}}

SELECT json_object();
-- {}

jsonb_object(...) returns the same result in JSONB format.

json_quote

json_quote(value) -> text

Converts an SQL value into its JSON representation. Strings are quoted and interior quotes are escaped. NULL becomes the JSON literal null. If the value is already JSON (from another JSON function), it is returned unchanged.

SELECT json_quote('hello');     -- "hello"
SELECT json_quote(3.14159);     -- 3.14159
SELECT json_quote(12345);       -- 12345
SELECT json_quote(null);        -- null

Extraction Functions

json_extract

json_extract(json, path, …) -> value

Extracts one or more values from a JSON document at the given path(s).

With a single path, the return type depends on the JSON element: SQL NULL for JSON null, INTEGER for integers and booleans, REAL for floating-point numbers, TEXT for strings, and a text JSON representation for objects and arrays.

With multiple paths, returns a JSON array containing all extracted values.

SELECT json_extract('{"a":2,"c":[4,5,{"f":7}]}', '$.c[2].f');
-- 7

SELECT json_extract('{"a":[1,2,3]}', '$.a');
-- [1,2,3]

SELECT json_extract('{"a":[1,2,3]}', '$.a', '$.a[0]', '$.a[1]', '$.a[3]');
-- [[1,2,3],1,2,null]

Returns NULL if the input JSON is NULL. Raises an error if the JSON is malformed.

jsonb_extract(...) returns JSONB for objects and arrays instead of text JSON.

-> (extract as JSON)

json -> path -> text

Extracts a value and always returns it as a JSON text representation. For strings, the result includes the surrounding quotes.

SELECT '{"a":2,"c":[4,5]}' -> '$.a';    -- 2
SELECT '{"a":"xyz"}' -> '$.a';           -- "xyz"
SELECT '[1,2,3]' -> 1;                   -- 2

The right operand can be a full path ('$.field'), a plain text label (interpreted as '$.label'), or an integer array index. Negative integers count from the end.

SELECT '{"a":1}' -> 'a';     -- 1
SELECT '[1,2,3]' -> -1;      -- 3

The -> operator can be chained:

SELECT '{"a":2,"c":[4,5,{"f":7}]}' -> 'c' -> 2 ->> 'f';
-- 7

->> (extract as SQL value)

json ->> path -> value

Extracts a value and returns it as an SQL type: TEXT for strings (without quotes), INTEGER for integers and booleans, REAL for floats, NULL for JSON null.

SELECT '{"a":"xyz"}' ->> '$.a';     -- xyz   (text, no quotes)
SELECT '{"a":2}' ->> '$.a';         -- 2     (integer)
SELECT 'true' ->> '$';              -- 1     (integer)
SELECT 'false' ->> '$';             -- 0     (integer)

Inspection Functions

json_array_length

json_array_length(json [, path]) -> integer

Returns the number of elements in the JSON array. If the value at the specified path is not an array, returns 0. If the path does not exist, returns NULL.

SELECT json_array_length('[1,2,3,4]');                       -- 4
SELECT json_array_length('[]');                               -- 0
SELECT json_array_length('{"one":[1,2,3]}', '$.one');        -- 3
SELECT json_array_length('{"one":[1,2,3]}');                  -- 0
SELECT json_array_length('{"one":[1,2,3]}', '$.two');        -- (NULL)

json_type

json_type(json [, path]) -> text

Returns a string indicating the type of the JSON element. Possible return values: 'object', 'array', 'integer', 'real', 'text', 'true', 'false', 'null'. Returns NULL if the path does not exist.

SELECT json_type('{"a":[2,3.5,true,false,null,"x"]}');           -- object
SELECT json_type('{"a":[2,3.5,true,false,null,"x"]}', '$.a');    -- array
SELECT json_type('{"a":[2,3.5,true,false,null,"x"]}', '$.a[0]'); -- integer
SELECT json_type('{"a":[2,3.5,true,false,null,"x"]}', '$.a[1]'); -- real
SELECT json_type('{"a":[2,3.5,true,false,null,"x"]}', '$.a[2]'); -- true
SELECT json_type('{"a":[2,3.5,true,false,null,"x"]}', '$.a[3]'); -- false
SELECT json_type('{"a":[2,3.5,true,false,null,"x"]}', '$.a[4]'); -- null
SELECT json_type('{"a":[2,3.5,true,false,null,"x"]}', '$.a[5]'); -- text

json_valid

json_valid(json [, flags]) -> integer

Returns 1 if the input is well-formed JSON, 0 if it is malformed, or NULL if the input is NULL.

Without flags (or with flags = 1), validates against RFC-8259 canonical JSON. The optional flags parameter is a bitmask that controls which formats are considered valid:

FlagMeaning
1RFC-8259 canonical JSON text
2JSON5 text
4JSONB (fast superficial check)
8JSONB (strict thorough check)

Flags can be combined with bitwise OR. Common combinations:

ValueAccepts
1Standard JSON text only (default)
2JSON5 text only
5Standard JSON text or JSONB
6JSON5 text or JSONB
SELECT json_valid('{"a":55,"b":72}');       -- 1
SELECT json_valid('not a valid json');       -- 0
SELECT json_valid(NULL);                     -- (NULL)
SELECT json_valid(123);                      -- 1

json_error_position

json_error_position(json) -> integer

Returns 0 if the input is well-formed JSON or JSON5. If malformed, returns the 1-based character position of the first syntax error. Returns NULL if the input is NULL.

SELECT json_error_position('{"a":55,"b":72}');      -- 0
SELECT json_error_position('{"a":55,"b":72,,}');     -- 16
SELECT json_error_position(NULL);                     -- (NULL)

json_pretty

json_pretty(json [, indent]) -> text

Returns the input JSON formatted with whitespace for readability. The optional indent argument specifies the indentation string (default: four spaces). If indent is NULL, the default four spaces are used.

SELECT json_pretty('{"name":"Alice","age":25}');
-- {
--     "name": "Alice",
--     "age": 25
-- }

Modification Functions

json_set

json_set(json, path, value, …) -> text

Creates or replaces values at the given paths. If the path exists, the value is replaced. If it does not exist, it is created (including intermediate objects and arrays as needed). Accepts multiple path/value pairs.

SELECT json_set('{"a":2,"c":4}', '$.a', 99);
-- {"a":99,"c":4}

SELECT json_set('{"a":2,"c":4}', '$.e', 99);
-- {"a":2,"c":4,"e":99}

SELECT json_set('{}', '$.field', 'value');
-- {"field":"value"}

SELECT json_set('[123]', '$[0]', 456, '$[1]', 789);
-- [456,789]

To set a JSON value (not a string), wrap it with json():

SELECT json_set('{"a":2,"c":4}', '$.c', json('[97,96]'));
-- {"a":2,"c":[97,96]}

json_set can create deeply nested structures:

SELECT json_set('{}', '$.object.doesnt.exist', 'value');
-- {"object":{"doesnt":{"exist":"value"}}}

jsonb_set(...) returns the same result in JSONB format.

json_insert

json_insert(json, path, value, …) -> text

Inserts values only where the path does not already exist. Existing values are not modified.

SELECT json_insert('{"a":1}', '$.a', 2);         -- {"a":1}  (no change)
SELECT json_insert('{"a":1}', '$.b', 2);          -- {"a":1,"b":2}
SELECT json_insert('[1,2,3]', '$[3]', 4);          -- [1,2,3,4]
SELECT json_insert('{"a":1}', '$.b', 2, '$.c', 3);
-- {"a":1,"b":2,"c":3}

jsonb_insert(...) returns the same result in JSONB format.

json_replace

json_replace(json, path, value, …) -> text

Replaces values only where the path already exists. Does not create new paths.

SELECT json_replace('{"a":1,"b":2}', '$.a', 42);     -- {"a":42,"b":2}
SELECT json_replace('{"a":1,"b":2}', '$.c', 3);       -- {"a":1,"b":2}  (no change)
SELECT json_replace('[1,2,3,4]', '$[1]', 99);          -- [1,99,3,4]

jsonb_replace(...) returns the same result in JSONB format.

json_remove

json_remove(json, path, …) -> text

Returns a copy of the JSON with elements at the specified paths removed. Paths that do not exist are silently ignored. Removing the root element ($) returns NULL.

SELECT json_remove('{"a":1,"b":2,"c":3}', '$.b');          -- {"a":1,"c":3}
SELECT json_remove('[1,2,3,4]', '$[1]');                     -- [1,3,4]
SELECT json_remove('{"a":1,"b":2}', '$.a', '$.b');          -- {}
SELECT json_remove('{"a":1}', '$');                          -- (NULL)

When removing multiple array elements, removals are applied sequentially, and each removal shifts subsequent indices:

SELECT json_remove('[0,1,2,3,4]', '$[2]', '$[0]');
-- [1,3,4]

jsonb_remove(...) returns the same result in JSONB format.

json_patch

json_patch(json1, json2) -> text

Applies the RFC-7396 MergePatch algorithm. Object members in json2 are merged into json1: new keys are added, existing keys are replaced, and keys with a null value are deleted. Arrays are treated as atomic values and replaced entirely.

SELECT json_patch('{"a":1,"b":2}', '{"c":3,"d":4}');
-- {"a":1,"b":2,"c":3,"d":4}

SELECT json_patch('{"a":1,"b":2}', '{"b":3}');
-- {"a":1,"b":3}

SELECT json_patch('{"a":1,"b":2}', '{"a":null}');
-- {"b":2}

SELECT json_patch('{"user":{"name":"john"}}', '{"user":{"age":30}}');
-- {"user":{"name":"john","age":30}}

SELECT json_patch('{"arr":[1,2,3]}', '{"arr":[4,5,6]}');
-- {"arr":[4,5,6]}

jsonb_patch(...) returns the same result in JSONB format.


Aggregate Functions

json_group_array

json_group_array(value) -> text

An aggregate function that collects all values from the group into a JSON array.

CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price REAL);
INSERT INTO products VALUES (1, 'hat', 9.99), (2, 'shirt', 24.99), (3, 'coat', 79.99);

SELECT json_group_array(name) FROM products;
-- ["hat","shirt","coat"]

Can be combined with other JSON functions to build complex aggregated structures:

SELECT json_group_array(json_object('name', name, 'price', price)) FROM products;
-- [{"name":"hat","price":9.99},{"name":"shirt","price":24.99},{"name":"coat","price":79.99}]

jsonb_group_array(value) returns the same result in JSONB format.

json_group_object

json_group_object(label, value) -> text

An aggregate function that collects label/value pairs from the group into a JSON object.

SELECT json_group_object(name, price) FROM products;
-- {"hat":9.99,"shirt":24.99,"coat":79.99}

jsonb_group_object(label, value) returns the same result in JSONB format.


Table-Valued Functions

json_each

json_each(json [, path])

A table-valued function that returns one row for each immediate child of a JSON array or object. It does not recurse into nested structures. With an optional path argument, iteration starts at the element identified by that path.

The returned columns are:

ColumnTypeDescription
keyanyArray index (integer) or object key (text)
valueanySQL value for primitives; JSON text for objects/arrays
typetextOne of: object, array, integer, real, text, true, false, null
atomanySQL value for primitives; NULL for objects/arrays
idintegerUnique identifier for this element
parentintegerAlways NULL (only populated by json_tree)
fullkeytextFull JSON path to this element
pathtextJSON path to the containing element
SELECT key, value, type FROM json_each('[1, 2.5, "x", true, false, null]');
-- 0|1|integer
-- 1|2.5|real
-- 2|x|text
-- 3|1|true
-- 4|0|false
-- 5||null
SELECT key, value, type FROM json_each('{"name":"Alice","age":30}');
-- name|Alice|text
-- age|30|integer

With a path argument:

SELECT key, value FROM json_each('{"a":[10,20,30]}', '$.a');
-- 0|10
-- 1|20
-- 2|30

A common use case is joining json_each against a table column to search within JSON arrays:

CREATE TABLE contacts (id INTEGER PRIMARY KEY, name TEXT, phones TEXT);
INSERT INTO contacts VALUES (1, 'Alice', '["555-0100","555-0101"]');
INSERT INTO contacts VALUES (2, 'Bob', '["555-0200"]');

SELECT DISTINCT contacts.name
FROM contacts, json_each(contacts.phones)
WHERE json_each.value LIKE '555-01%';
-- Alice

json_tree

json_tree(json [, path])

A table-valued function that recursively walks the entire JSON structure, returning one row for every element at every level of nesting. It returns the same columns as json_each, but the parent column is populated with the id of each element’s parent.

SELECT key, type, fullkey FROM json_tree('{"a":1,"b":{"c":2},"d":[3,4]}') ORDER BY id;
-- |object|$
-- a|integer|$.a
-- b|object|$.b
-- c|integer|$.b.c
-- d|array|$.d
-- 0|integer|$.d[0]
-- 1|integer|$.d[1]

Compatibility

The json_tree function is partially supported. Basic recursive traversal works, but some edge cases involving negative array indices in the path argument have known limitations. See the SQLite documentation for full behavioral details.

All other JSON functions listed on this page are fully compatible with SQLite.

PRAGMAs

PRAGMAs are special SQL statements that configure the database engine or query internal state. Unlike regular SQL statements, PRAGMAs are specific to the database implementation and do not follow the SQL standard.

Syntax

PRAGMA pragma_name;
PRAGMA pragma_name = value;
PRAGMA pragma_name(value);

The first form queries the current value. The second and third forms set a new value. Not all PRAGMAs support both forms.


Database Information

application_id

PRAGMA application_id; PRAGMA application_id = integer;

Queries or sets the 32-bit signed integer “Application ID” stored at offset 68 in the database header. Applications that use Turso as a file format can set a unique application ID so that external tools can identify the file type. The default value is 0.

PRAGMA application_id;
-- 0

PRAGMA application_id = 12345;
PRAGMA application_id;
-- 12345

database_list

PRAGMA database_list;

Returns one row per attached database, with columns seq (sequence number), name (database name), and file (file path). The main database always has sequence number 0 and the name main.

PRAGMA database_list;
-- seq|name|file
-- 0|main|

encoding

PRAGMA encoding;

Returns the text encoding used by the database. Turso uses UTF-8.

PRAGMA encoding;
-- UTF-8

freelist_count

PRAGMA freelist_count;

Returns the number of unused pages in the database file. A high freelist count may indicate the database would benefit from VACUUM.

PRAGMA freelist_count;
-- 0

page_count

PRAGMA page_count;

Returns the total number of pages in the database file. Multiply by the page size to get the approximate database size in bytes.

PRAGMA page_count;
-- 0

page_size

PRAGMA page_size; PRAGMA page_size = bytes;

Queries or sets the database page size in bytes. The value must be a power of two between 512 and 65536. The default is 4096.

Setting a new page size takes effect when the database is first created or after a VACUUM operation.

PRAGMA page_size;
-- 4096

schema_version

PRAGMA schema_version;

Returns the schema version number, an integer that Turso increments automatically whenever the database schema changes (via CREATE TABLE, DROP TABLE, etc.). The default for a new database is 0.

Writing to schema_version is accepted syntactically but treated as a no-op for safety. Modifying the schema version externally can cause prepared statements to use stale schemas and corrupt data.

PRAGMA schema_version;
-- 0

user_version

PRAGMA user_version; PRAGMA user_version = integer;

Queries or sets a user-defined 32-bit integer stored in the database header at offset 60. Turso does not use this value internally – it is available for applications to track their own schema migration version or any other purpose. The default is 0.

PRAGMA user_version;
-- 0

PRAGMA user_version = 100;
PRAGMA user_version;
-- 100

pragma_list

PRAGMA pragma_list;

Returns a list of all PRAGMA names recognized by the current database connection.

PRAGMA pragma_list;
-- application_id
-- busy_timeout
-- cache_size
-- ...

Schema Information

table_info

PRAGMA table_info(table-name);

Returns one row per regular column in the named table. Each row contains:

ColumnDescription
cidColumn index (zero-based)
nameColumn name
typeDeclared type (empty string if none)
notnull1 if the column has a NOT NULL constraint, 0 otherwise
dflt_valueDefault value expression, or empty if none
pk0 if the column is not part of the primary key; otherwise the 1-based index within the primary key

Generated columns and hidden columns are omitted. Use table_xinfo for a complete listing.

CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  price REAL DEFAULT 0.0,
  in_stock INTEGER DEFAULT 1
);

PRAGMA table_info(products);
-- cid|name|type|notnull|dflt_value|pk
-- 0|id|INTEGER|0||1
-- 1|name|TEXT|1||0
-- 2|price|REAL|0|0.0|0
-- 3|in_stock|INTEGER|0|1|0

table_xinfo

PRAGMA table_xinfo(table-name);

Returns the same columns as table_info plus an additional hidden column. This PRAGMA includes all columns – regular, generated, and hidden.

hidden ValueMeaning
0Normal column
1Hidden column in a virtual table
2Virtual (dynamic) generated column
3Stored generated column
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  price REAL DEFAULT 0.0,
  in_stock INTEGER DEFAULT 1
);

PRAGMA table_xinfo(products);
-- cid|name|type|notnull|dflt_value|pk|hidden
-- 0|id|INTEGER|0||1|0
-- 1|name|TEXT|1||0|0
-- 2|price|REAL|0|0.0|0|0
-- 3|in_stock|INTEGER|0|1|0|0

Performance and Caching

busy_timeout

PRAGMA busy_timeout; PRAGMA busy_timeout = milliseconds;

Queries or sets the busy timeout in milliseconds. When another connection holds a lock, Turso will retry for up to this many milliseconds before returning an error. The default is 0 (return immediately on lock contention).

PRAGMA busy_timeout;
-- 0

PRAGMA busy_timeout = 5000;
PRAGMA busy_timeout;
-- 5000

cache_size

PRAGMA cache_size; PRAGMA cache_size = pages;

Queries or sets the suggested maximum number of database pages held in memory. A positive value specifies pages directly. A negative value specifies the cache size in kibibytes (KiB). The default is -2000 (approximately 2 MB).

This setting applies only to the current session and reverts to the default when the connection closes.

PRAGMA cache_size;
-- -2000

-- Set cache to 4000 pages
PRAGMA cache_size = 4000;
PRAGMA cache_size;
-- 4000

-- Set cache to approximately 4 MB
PRAGMA cache_size = -4000;
PRAGMA cache_size;
-- -4000

cache_spill

PRAGMA cache_spill; PRAGMA cache_spill = boolean;

Queries or sets whether the pager is allowed to spill dirty pages to the database file in the middle of a transaction. When enabled (the default, 1), the pager may write dirty pages to disk before a transaction commits. When disabled (0), dirty pages are held in memory until commit.

Disabling cache spill avoids acquiring an exclusive lock on the database file until the transaction commits, which can be useful for long-running transactions.

PRAGMA cache_spill;
-- 1

PRAGMA cache_spill = 0;
PRAGMA cache_spill;
-- 0

max_page_count

PRAGMA max_page_count; PRAGMA max_page_count = N;

Queries or sets the maximum number of pages allowed in the database file. Both the query and set forms return the current maximum. The maximum cannot be reduced below the current database size. The default is 4294967294.

PRAGMA max_page_count;
-- 4294967294

PRAGMA max_page_count = 1000;
PRAGMA max_page_count;
-- 1000

query_only

PRAGMA query_only; PRAGMA query_only = boolean;

When set to 1, all write operations (CREATE, INSERT, UPDATE, DELETE, DROP) are rejected with an error. The default is 0 (writes allowed).

PRAGMA query_only = 1;
CREATE TABLE test (id INTEGER);
-- Error: Cannot execute write statement in query_only mode

PRAGMA query_only = 0;

temp_store

PRAGMA temp_store; PRAGMA temp_store = {0 | 1 | 2};

Queries or sets the storage location for temporary tables and indices.

ValueNameBehavior
0DEFAULTUse the compile-time default
1FILEStore temporary data in a file
2MEMORYStore temporary data in memory

Changing this setting deletes all existing temporary tables, indices, triggers, and views. The default is 0.

PRAGMA temp_store;
-- 0

PRAGMA temp_store = 2;
PRAGMA temp_store;
-- 2

Journaling and Durability

journal_mode

PRAGMA journal_mode; PRAGMA journal_mode = mode;

Queries or sets the journal mode for the database. The journal mode controls how transactions are logged for crash recovery.

ModeDescription
walWrite-ahead logging. Allows concurrent readers and a single writer. This is the default in Turso.

Turso defaults to WAL mode. Setting the journal mode returns the active mode.

PRAGMA journal_mode;
-- wal

PRAGMA journal_mode = 'wal';
-- wal

synchronous

PRAGMA synchronous; PRAGMA synchronous = {0 | 2};

Queries or sets the synchronous flag, which controls how aggressively Turso forces writes to disk. The value is returned as an integer.

ValueNameBehavior
0OFFNo synchronous writes. Fastest, but the database may corrupt if the operating system crashes or power is lost.
2FULLSynchronous writes at every critical moment. Safe against both application crashes and OS/power failures. This is the default.
PRAGMA synchronous;
-- 2

PRAGMA synchronous = OFF;
PRAGMA synchronous;
-- 0

PRAGMA synchronous = FULL;
PRAGMA synchronous;
-- 2

wal_checkpoint

PRAGMA wal_checkpoint;

Runs a checkpoint operation on the write-ahead log (WAL). A checkpoint transfers data from the WAL file back into the main database file. Returns three columns:

ColumnDescription
busy0 if the checkpoint completed, 1 if it was blocked by a concurrent reader or writer
logTotal number of frames in the WAL
checkpointedNumber of frames successfully checkpointed
PRAGMA wal_checkpoint;
-- busy|log|checkpointed
-- 0|0|0

Constraint Enforcement

foreign_keys

PRAGMA foreign_keys; PRAGMA foreign_keys = boolean;

Queries or sets whether foreign key constraints are enforced. When enabled (1), INSERT, UPDATE, and DELETE operations that violate a foreign key constraint will fail with an error. The default is 0 (foreign keys not enforced).

This setting cannot be changed while a transaction is active.

PRAGMA foreign_keys;
-- 0

PRAGMA foreign_keys = 1;

CREATE TABLE orders (id INTEGER PRIMARY KEY, total REAL);
CREATE TABLE items (
  id INTEGER PRIMARY KEY,
  order_id INTEGER REFERENCES orders(id)
);

-- This fails because order 999 does not exist
INSERT INTO items VALUES (1, 999);
-- Error: foreign key constraint failed

ignore_check_constraints

PRAGMA ignore_check_constraints; PRAGMA ignore_check_constraints = boolean;

Queries or sets whether CHECK constraints are enforced. When set to 1, CHECK constraint violations are silently ignored during INSERT and UPDATE operations. The default is 0 (CHECK constraints enforced).

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer TEXT NOT NULL,
  total REAL CHECK(total >= 0)
);

-- With CHECK constraints enforced (default)
INSERT INTO orders VALUES (1, 'Alice', -5.0);
-- Error: CHECK constraint failed: total >= 0

-- Disable CHECK constraints
PRAGMA ignore_check_constraints = 1;
INSERT INTO orders VALUES (1, 'Alice', -5.0);
-- Succeeds despite negative total

Integrity Checking

integrity_check

PRAGMA integrity_check;

Performs a thorough consistency check of the entire database. Examines every page, verifies B-tree structure, checks for missing or duplicate index entries, validates UNIQUE and NOT NULL constraints, and confirms freelist integrity. Returns ok if no problems are found, or one or more rows describing any issues detected.

PRAGMA integrity_check;
-- ok

quick_check

PRAGMA quick_check;

Performs a faster but less comprehensive consistency check than integrity_check. It verifies B-tree structure and record formatting but does not validate UNIQUE constraints or cross-check index content against table content. Returns ok if no problems are found.

PRAGMA quick_check;
-- ok

Deprecated

legacy_file_format

PRAGMA legacy_file_format;

This PRAGMA is a no-op. It exists for compatibility but does not return a value or perform any action.


Compatibility

Turso supports the PRAGMAs listed on this page. The following differences from SQLite are worth noting:

  • journal_mode: Turso defaults to wal mode. Other journal modes (delete, truncate, persist, memory, off) are not supported.
  • synchronous: Only OFF (0) and FULL (2) are supported. NORMAL (1) and EXTRA (3) are not available.
  • cache_spill: Only the boolean enable/disable form is supported. The numeric threshold form (PRAGMA schema.cache_spill = N) is not available.
  • schema_version: Reads work normally. Writes are accepted but silently ignored to prevent accidental database corruption.
  • wal_checkpoint: Only the no-argument form is supported. Checkpoint modes (PASSIVE, FULL, RESTART, TRUNCATE) are not available.
  • legacy_file_format: Accepted for compatibility but is a no-op, matching modern SQLite behavior.

Custom Types

Turso Extension. This feature is not available in SQLite. Custom types require STRICT tables for encode/decode behavior.

Overview

Turso supports user-defined custom types that extend the type system of STRICT tables. A custom type defines how values are transformed when written to and read from storage using ENCODE and DECODE expressions, allowing you to enforce domain constraints, normalize data, or change the storage representation without modifying application queries.

Custom types are built on top of one of the four base storage types (TEXT, INTEGER, REAL, BLOB). When a value is inserted into a column with a custom type, the ENCODE expression transforms it before writing. When the value is read back, the DECODE expression reverses the transformation. This is transparent to queries: SELECT statements return decoded values, and WHERE clauses operate on decoded values.

Type definitions are stored in the sqlite_turso_types system table and are loaded into an in-memory registry when the database is opened. Types persist across sessions.

Syntax

CREATE TYPE

CREATE TYPE [IF NOT EXISTS] type_name [(param [, ...])]
    BASE {TEXT | INTEGER | REAL | BLOB}
    [ENCODE expr]
    [DECODE expr]
    [DEFAULT expr]
    [OPERATOR 'op' (right_type) -> func_name]*;

DROP TYPE

DROP TYPE [IF EXISTS] type_name;

Description

CREATE TYPE registers a new custom type in the database. The type definition specifies:

  • BASE: The underlying storage type. All values for this type are stored on disk using this SQLite storage class. Must be one of TEXT, INTEGER, REAL, or BLOB.
  • ENCODE: An expression applied to the input value before writing to storage. The keyword value is a placeholder that refers to the value being inserted. If omitted, values are stored as-is.
  • DECODE: An expression applied to the stored value when reading from the table. The keyword value is a placeholder that refers to the raw stored value. If omitted, values are returned as-is.
  • DEFAULT: A fallback default expression used when a column of this type has no column-level DEFAULT and no explicit value is provided in an INSERT.
  • OPERATOR: Maps an operator symbol to a named function for use with values of this type.

DROP TYPE removes a custom type definition from the database. If the type does not exist and IF EXISTS is not specified, an error is raised.

Clauses

BASE

The BASE clause is required and specifies the underlying storage type.

Base TypeDescription
TEXTStored as a UTF-8 text string
INTEGERStored as a signed integer (up to 8 bytes)
REALStored as an 8-byte IEEE 754 floating-point number
BLOBStored as raw binary data

The base type determines how the encoded value is stored on disk and what type affinity rules apply during storage.

ENCODE

The ENCODE clause defines an expression that transforms values on write. The special identifier value represents the input being inserted. The expression can be any valid SQL expression, including function calls, arithmetic, CASE expressions, and nested function calls.

If the ENCODE expression raises an error (for example, json(value) on invalid JSON), the INSERT statement fails. This makes ENCODE a natural place to add validation logic.

DECODE

The DECODE clause defines an expression that transforms values on read. The special identifier value represents the raw stored value. The DECODE expression is applied whenever a column with the custom type is selected.

DEFAULT

The DEFAULT clause specifies a fallback value for columns of this type when no value is provided during INSERT. If a column also has its own DEFAULT clause in the CREATE TABLE statement, the column-level default takes priority.

OPERATOR

The OPERATOR clause maps an operator symbol to a named function. The syntax is:

OPERATOR 'op' (right_type) -> func_name

Where op is the operator symbol (such as +, -, <, =), right_type is the type of the right-hand operand, and func_name is the function to call when this operator is used between values of the custom type.

IF NOT EXISTS / IF EXISTS

CREATE TYPE IF NOT EXISTS silently succeeds if a type with the same name already exists. DROP TYPE IF EXISTS silently succeeds if the type does not exist.

Parametric Types

Custom types can accept parameters that are substituted into the ENCODE and DECODE expressions. Parameters are declared in parentheses after the type name and referenced by name in the expressions.

CREATE TYPE type_name(param1, param2)
    BASE base_type
    ENCODE expr_using_param1_and_param2
    DECODE expr_using_param1_and_param2;

When the type is used in a CREATE TABLE statement, the actual parameter values are provided:

CREATE TABLE t (col type_name(100, 2)) STRICT;

The parameter values are substituted into the ENCODE and DECODE expressions at compile time.

Using Custom Types in Tables

Custom types are used as column type names in CREATE TABLE ... STRICT statements. The type name replaces a standard type like TEXT or INTEGER:

CREATE TYPE cents BASE integer ENCODE value * 100 DECODE value / 100;
CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT,
    price cents
) STRICT;

Custom types require STRICT tables. On non-STRICT tables, the ENCODE transformation is not applied during insertion, which leads to incorrect values when DECODE runs on read.

CAST with Custom Types

The CAST expression can target a custom type. When you write CAST(expr AS type_name), the ENCODE and DECODE expressions are both applied (a round-trip), producing the normalized user-facing form of the value:

CREATE TYPE normalized BASE text ENCODE lower(value) DECODE value;
SELECT CAST('Hello World' AS normalized);
-- Returns 'hello world'

For parametric types, the parameters must be provided:

SELECT CAST(42 AS numeric(10,2));
-- Returns '42.00'

This is useful for normalizing values or validating that a value conforms to a custom type's constraints outside of an INSERT context.

Inspecting Custom Types

Custom type definitions are stored in the sqlite_turso_types system table. You can query them directly:

SELECT name, sql FROM sqlite_turso_types;

NULL Handling

NULL values pass through ENCODE and DECODE unchanged. If you insert NULL into a column with a custom type, NULL is stored and NULL is returned on read, regardless of the ENCODE and DECODE expressions.

Examples

Basic Custom Type

-- Define a type that stores monetary values as cents
CREATE TYPE cents BASE integer
    ENCODE value * 100
    DECODE value / 100;

-- Use it in a STRICT table
CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT,
    price cents
) STRICT;

INSERT INTO products VALUES (1, 'Coffee', 4);
INSERT INTO products VALUES (2, 'Tea', 2);
INSERT INTO products VALUES (3, 'Juice', 3);

-- Values are decoded on read
SELECT id, name, price FROM products;
-- 1|Coffee|4
-- 2|Tea|2
-- 3|Juice|3

JSON Validation Type

-- Define a type that validates JSON on insert
CREATE TYPE validated_json BASE text
    ENCODE json(value)
    DECODE value;

CREATE TABLE config (
    id INTEGER PRIMARY KEY,
    data validated_json
) STRICT;

-- Valid JSON is accepted and normalized
INSERT INTO config VALUES (1, '{"key": "val"}');
SELECT id, data FROM config;
-- 1|{"key":"val"}

-- Invalid JSON is rejected at insert time
INSERT INTO config VALUES (2, 'not valid json');
-- Error: malformed JSON

Text Normalization Type

-- Define a type that lowercases text on insert
CREATE TYPE normalized BASE text
    ENCODE lower(value)
    DECODE value;

CREATE TABLE tags (label normalized) STRICT;

INSERT INTO tags VALUES ('JavaScript');
INSERT INTO tags VALUES ('PYTHON');
INSERT INTO tags VALUES ('Rust');

SELECT label FROM tags ORDER BY label;
-- javascript
-- python
-- rust

Type-Level DEFAULT

-- Define a type with a default value
CREATE TYPE cents BASE integer
    ENCODE value * 100
    DECODE value / 100
    DEFAULT 0;

CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    price cents
) STRICT;

-- Omitting price uses the type-level default of 0
INSERT INTO products(id) VALUES (1);
INSERT INTO products VALUES (2, 5);

SELECT id, price FROM products;
-- 1|0
-- 2|5

Column DEFAULT Overrides Type DEFAULT

CREATE TYPE cents BASE integer
    ENCODE value * 100
    DECODE value / 100
    DEFAULT 0;

-- Column-level DEFAULT takes priority over type-level DEFAULT
CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    price cents DEFAULT 10
) STRICT;

INSERT INTO products(id) VALUES (1);
SELECT id, price FROM products;
-- 1|10

Parametric Type

-- Define a clamping type with configurable bounds
CREATE TYPE clamp(lo, hi) BASE integer
    ENCODE CASE
        WHEN value < lo THEN lo
        WHEN value > hi THEN hi
        ELSE value
    END
    DECODE value;

CREATE TABLE readings (
    id INTEGER PRIMARY KEY,
    temperature clamp(0, 100)
) STRICT;

INSERT INTO readings VALUES (1, 50);
INSERT INTO readings VALUES (2, 150);
INSERT INTO readings VALUES (3, -20);

SELECT id, temperature FROM readings;
-- 1|50
-- 2|100
-- 3|0

Multiple Custom Types in One Table

CREATE TYPE cents BASE integer
    ENCODE value * 100
    DECODE value / 100;

CREATE TYPE normalized BASE text
    ENCODE lower(value)
    DECODE value;

CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name normalized,
    price cents
) STRICT;

INSERT INTO products VALUES (1, 'Coffee', 4);
INSERT INTO products VALUES (2, 'TEA', 2);

SELECT id, name, price FROM products;
-- 1|coffee|4
-- 2|tea|2

NULL Passes Through

CREATE TYPE cents BASE integer
    ENCODE value * 100
    DECODE value / 100;

CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    price cents
) STRICT;

INSERT INTO products VALUES (1, NULL);
SELECT id, COALESCE(price, 'IS_NULL') FROM products;
-- 1|IS_NULL

CREATE TYPE IF NOT EXISTS

CREATE TYPE cents BASE integer
    ENCODE value * 100
    DECODE value / 100;

-- Does not raise an error if the type already exists
CREATE TYPE IF NOT EXISTS cents BASE integer
    ENCODE value * 100
    DECODE value / 100;

SELECT count(*) FROM sqlite_turso_types WHERE name = 'cents';
-- 1

DROP TYPE

CREATE TYPE cents BASE integer
    ENCODE value * 100
    DECODE value / 100;

DROP TYPE cents;

SELECT count(*) FROM sqlite_turso_types WHERE name = 'cents';
-- 0

-- DROP TYPE IF EXISTS does not raise an error for missing types
DROP TYPE IF EXISTS nonexistent;

Inspecting Types via sqlite_turso_types

CREATE TYPE cents BASE integer
    ENCODE value * 100
    DECODE value / 100
    DEFAULT 0;

SELECT name, sql FROM sqlite_turso_types WHERE name = 'cents';
-- cents|CREATE TYPE cents BASE integer ENCODE value * 100 DECODE value / 100 DEFAULT 0

Compatibility

  • Custom types are a Turso extension and are not available in SQLite.
  • Custom types require STRICT tables for correct encode/decode behavior. On non-STRICT tables, custom type names are treated as standard type affinity hints and encode/decode expressions are not applied.
  • Type definitions are stored in the sqlite_turso_types system table. SQLite-based tools that open a Turso database may not understand this table.
  • The OPERATOR clause maps operators to named functions. These functions must be available in the Turso function registry (either built-in or loaded via extensions).

Vector Search

Turso extension. Vector search is a Turso-specific feature and is not available in standard SQLite.

Overview

Turso supports vector operations for building similarity search and semantic search applications. Vectors are fixed-length arrays of floating-point numbers that represent data points in a high-dimensional space. They are commonly produced by machine learning embedding models that convert text, images, or other data into numerical representations where similar items have nearby vectors.

Vectors are stored as compact binary BLOBs and can be compared using built-in distance functions to find the most similar items.

A typical workflow is:

  1. Create a table with a BLOB column (or the F32_BLOB/F64_BLOB type hint) to hold embeddings.
  2. Generate embeddings externally using a model (such as OpenAI, Cohere, or a local model) and insert them using the vector32() or vector64() creation functions. These functions convert a JSON text array into a compact binary representation.
  3. At query time, convert the search query into an embedding using the same model, then use a distance function (vector_distance_cos, vector_distance_l2, etc.) with ORDER BY and LIMIT to find the nearest neighbors.

Vector indexes are not yet supported. All vector searches currently use brute-force scanning, which means search time scales linearly with the number of rows. For small to medium datasets (up to hundreds of thousands of rows), brute-force search is often fast enough. For larger datasets, consider partitioning or pre-filtering with WHERE clauses on non-vector columns.

Vector Types

Turso supports three vector formats:

FormatFunctionBytes per dimensionDescription
Float32 densevector32() or vector()432-bit floating-point. Default format, good balance of precision and size.
Float64 densevector64()864-bit floating-point. Higher precision, double the storage.
Float32 sparsevector32_sparse()4 (non-zero only)32-bit floating-point sparse representation. Only stores non-zero dimensions.

The vector() function is an alias for vector32().

Column Types

Vectors are stored on disk as BLOBs. You can use a plain BLOB column or the optional type hints F32_BLOB(n) and F64_BLOB(n), where n is the number of dimensions. The type hint is purely documentary and does not enforce a dimension constraint at the storage layer.

CREATE TABLE documents (
    id INTEGER PRIMARY KEY,
    content TEXT,
    embedding F32_BLOB(4)
);

Vector Creation Functions

vector(text), vector32(text)

vector(text) -> blob vector32(text) -> blob

Parse a JSON array of numbers into a 32-bit floating-point vector BLOB. The input must be a single-quoted string containing a JSON array of numeric values (integers or floats). vector() is an alias for vector32(). Each number in the array becomes one dimension of the vector. The values must be finite (no NaN or infinity).

SELECT vector_extract(vector32('[1.0, 2.0, 3.0]'));  -- [1,2,3]
SELECT vector_extract(vector('[4.5, 5.5, 6.5]'));  -- [4.5,5.5,6.5]

vector64(text)

vector64(text) -> blob

Parse a JSON array of numbers into a 64-bit floating-point vector BLOB. Use this when you need higher numerical precision than 32-bit floats provide. The trade-off is double the storage per dimension (8 bytes instead of 4 bytes).

SELECT vector_extract(vector64('[1.0, 2.0, 3.0]'));  -- [1,2,3]

vector32_sparse(text)

vector32_sparse(text) -> blob

Parse a JSON array into a sparse 32-bit floating-point vector. Zero-valued dimensions are omitted from the binary representation, reducing storage for vectors with many zeros. This is particularly useful for bag-of-words models, TF-IDF representations, or any embedding scheme where most dimensions are zero. The full dimensionality of the vector is preserved – vector_extract will reconstruct the zeros – but only non-zero values are stored on disk.

SELECT vector_extract(vector32_sparse('[1.0, 0.0, 3.0]'));  -- [1,0,3]

Distance Functions

All distance functions take two vector BLOBs and return a floating-point number. Both vectors must have the same number of dimensions. Passing vectors with different dimension counts will result in an error.

When using distance functions for nearest-neighbor search, sort results in ascending order (ORDER BY distance ASC or simply ORDER BY distance) so that the most similar items appear first. All distance functions in Turso follow this convention: smaller values mean more similar vectors.

vector_distance_cos(v1, v2)

vector_distance_cos(v1, v2) -> real

Compute the cosine distance between two vectors, defined as 1 - cosine_similarity. The result ranges from 0 (identical direction) to 2 (opposite direction). A value of 1 means the vectors are orthogonal.

Cosine distance is typically preferred for text and document embeddings because it measures the angle between vectors rather than their magnitude.

-- Identical vectors: distance near 0
SELECT vector_distance_cos(
    vector32('[1.0, 0.0]'),
    vector32('[1.0, 0.0]')
);  -- ~0.0

-- Orthogonal vectors: distance = 1
SELECT vector_distance_cos(
    vector32('[1.0, 0.0, 0.0]'),
    vector32('[0.0, 0.0, 1.0]')
);  -- 1.0

-- Opposite vectors: distance near 2
SELECT vector_distance_cos(
    vector32('[1.0, 0.0]'),
    vector32('[-1.0, 0.0]')
);  -- ~2.0

vector_distance_l2(v1, v2)

vector_distance_l2(v1, v2) -> real

Compute the Euclidean (L2) distance between two vectors. This is the straight-line distance between two points in the vector space. The result is always non-negative, with 0 meaning the vectors are identical. L2 distance is sensitive to vector magnitude, so it works best when vectors are on a similar scale.

SELECT vector_distance_l2(
    vector32('[0.0, 0.0]'),
    vector32('[3.0, 4.0]')
);  -- 5.0

SELECT vector_distance_l2(
    vector32('[1.0, 2.0, 3.0]'),
    vector32('[4.0, 5.0, 6.0]')
);  -- 5.19615242270663

vector_distance_dot(v1, v2)

vector_distance_dot(v1, v2) -> real

Compute the negative dot product between two vectors. A more negative result means the vectors are more similar (larger dot product). The result is negated so that sorting in ascending order returns the most similar vectors first.

SELECT vector_distance_dot(
    vector32('[1.0, 2.0]'),
    vector32('[3.0, 4.0]')
);  -- -11.0

SELECT vector_distance_dot(
    vector32('[1.0, 0.0]'),
    vector32('[0.0, 1.0]')
);  -- 0.0

vector_distance_jaccard(v1, v2)

vector_distance_jaccard(v1, v2) -> real

Compute the Jaccard distance between two vectors treated as sets. The Jaccard distance is defined as 1 - (intersection / union) where non-zero elements are treated as set members. This is useful when vector dimensions represent binary or categorical features, such as presence/absence of tags or keywords.

SELECT vector_distance_jaccard(
    vector32('[1.0, 0.0, 1.0]'),
    vector32('[0.0, 1.0, 1.0]')
);  -- 0.666666656732559

Utility Functions

vector_extract(blob)

vector_extract(blob) -> text

Convert a vector BLOB back into a human-readable JSON array. Useful for inspecting stored vectors.

SELECT vector_extract(vector32('[1.0, 2.0, 3.0]'));  -- [1,2,3]

vector_concat(v1, v2)

vector_concat(v1, v2) -> blob

Concatenate two vectors into a single vector. Both vectors must be the same type.

SELECT vector_extract(
    vector_concat(
        vector32('[1.0, 2.0]'),
        vector32('[3.0, 4.0]')
    )
);  -- [1,2,3,4]

vector_slice(v, start, end)

vector_slice(v, start, end) -> blob

Extract a contiguous sub-vector from dimension index start (inclusive) to end (exclusive). Indices are zero-based.

SELECT vector_extract(
    vector_slice(vector32('[10.0, 20.0, 30.0, 40.0, 50.0]'), 0, 3)
);  -- [10,20,30]

SELECT vector_extract(
    vector_slice(vector32('[10.0, 20.0, 30.0, 40.0, 50.0]'), 2, 5)
);  -- [30,40,50]

Examples

Creating a Table and Inserting Vectors

CREATE TABLE documents (
    id INTEGER PRIMARY KEY,
    content TEXT,
    embedding BLOB
);

INSERT INTO documents VALUES
    (1, 'Introduction to databases', vector32('[0.1, 0.2, 0.3, 0.4]'));
INSERT INTO documents VALUES
    (2, 'SQL query optimization', vector32('[0.2, 0.1, 0.4, 0.3]'));
INSERT INTO documents VALUES
    (3, 'Vector similarity search', vector32('[0.4, 0.3, 0.2, 0.1]'));

Finding Similar Documents with Cosine Distance

SELECT
    id,
    content,
    vector_distance_cos(embedding, vector32('[0.15, 0.25, 0.35, 0.45]')) AS distance
FROM documents
ORDER BY distance
LIMIT 3;
-- 1|Introduction to databases|0.00203462258422431
-- 2|SQL query optimization|0.0590611621657705
-- 3|Vector similarity search|0.287167575420696

Finding Similar Documents with L2 Distance

SELECT
    id,
    content,
    vector_distance_l2(embedding, vector32('[0.15, 0.25, 0.35, 0.45]')) AS distance
FROM documents
ORDER BY distance
LIMIT 3;
-- 1|Introduction to databases|0.0999999802559595
-- 2|SQL query optimization|0.223606791085977
-- 3|Vector similarity search|0.458257562341844

Find all vectors within a certain distance rather than a fixed number of results:

SELECT
    id,
    content,
    vector_distance_cos(embedding, vector32('[0.1, 0.2, 0.3, 0.4]')) AS distance
FROM documents
WHERE vector_distance_cos(embedding, vector32('[0.1, 0.2, 0.3, 0.4]')) < 0.01
ORDER BY distance;

Inspecting Stored Vectors

SELECT id, vector_extract(embedding) FROM documents;
-- 1|[0.1,0.2,0.3,0.4]
-- 2|[0.2,0.1,0.4,0.3]
-- 3|[0.4,0.3,0.2,0.1]

Combining Vector Search with Relational Filters

Vector search can be combined with standard SQL WHERE clauses. Non-vector predicates are applied before or alongside the distance calculation, reducing the number of vectors that need to be compared:

-- Assuming a 'category' column exists alongside the embedding
CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT,
    category TEXT,
    embedding BLOB
);

INSERT INTO products VALUES (1, 'Running Shoes', 'footwear', vector32('[0.8, 0.1, 0.3]'));
INSERT INTO products VALUES (2, 'Hiking Boots', 'footwear', vector32('[0.7, 0.2, 0.5]'));
INSERT INTO products VALUES (3, 'Cotton T-Shirt', 'clothing', vector32('[0.1, 0.9, 0.2]'));
INSERT INTO products VALUES (4, 'Trail Sneakers', 'footwear', vector32('[0.75, 0.15, 0.4]'));

-- Search only within the 'footwear' category
SELECT
    name,
    vector_distance_cos(embedding, vector32('[0.8, 0.1, 0.35]')) AS distance
FROM products
WHERE category = 'footwear'
ORDER BY distance
LIMIT 2;

Semantic Search Application

A complete example modeling an article recommendation system:

-- Create schema
CREATE TABLE articles (
    id INTEGER PRIMARY KEY,
    title TEXT,
    embedding BLOB
);

-- Insert pre-computed embeddings from an external model
INSERT INTO articles VALUES
    (1, 'Database Fundamentals', vector32('[0.12, -0.34, 0.56, 0.78]'));
INSERT INTO articles VALUES
    (2, 'Machine Learning Basics', vector32('[0.23, 0.45, -0.67, 0.89]'));
INSERT INTO articles VALUES
    (3, 'Web Development Guide', vector32('[0.34, -0.12, 0.78, -0.56]'));
INSERT INTO articles VALUES
    (4, 'Data Structures', vector32('[0.11, -0.33, 0.55, 0.77]'));

-- Search: given a query embedding, find the 3 most similar articles
SELECT
    a.id,
    a.title,
    vector_distance_cos(a.embedding, vector32('[0.10, -0.30, 0.50, 0.70]')) AS distance
FROM articles a
ORDER BY distance
LIMIT 3;
-- 4|Data Structures|0.0
-- 1|Database Fundamentals|4.52537882702912e-05
-- 2|Machine Learning Basics|0.843018284332676

Choosing a Distance Function

Different distance functions suit different use cases:

FunctionBest forRangeNotes
vector_distance_cosText/document embeddings0 to 2Ignores vector magnitude; focuses on direction. Most common for NLP embeddings.
vector_distance_l2Spatial data, image features0 to infinitySensitive to magnitude. Good when absolute position matters.
vector_distance_dotNormalized embeddings, rankingnegative infinity to positive infinityReturns negated dot product so ascending sort gives best matches.
vector_distance_jaccardBinary/categorical features0 to 1Treats vectors as sets. Best for presence/absence features.

When in doubt, start with vector_distance_cos. It is the most widely used metric for text embeddings produced by models like OpenAI, Cohere, and Sentence Transformers.

Performance Considerations

Since vector indexes are not yet implemented, keep the following in mind:

  • Linear scan: every search examines all rows in the table. For large datasets, queries will be slower.
  • Use vector32 over vector64 unless double precision is required. It uses half the storage (4 bytes vs. 8 bytes per dimension).
  • Pre-filter with WHERE: apply non-vector predicates first to reduce the number of distance calculations. For example, filtering by a category column before computing distances avoids unnecessary work.
  • Use vector32_sparse when vectors have many zero-valued dimensions to reduce storage.
  • Dimension count matters: storage per row is dimensions * bytes_per_dimension. A 1536-dimensional vector32 column uses about 6 KB per row, while a 384-dimensional column uses about 1.5 KB per row. Choose the smallest embedding model that meets your accuracy requirements.

Compatibility

Vector search is a Turso extension and is not available in standard SQLite. The vector(), vector32(), vector64(), vector32_sparse(), vector_extract(), vector_distance_cos(), vector_distance_l2(), vector_distance_dot(), vector_distance_jaccard(), vector_concat(), and vector_slice() functions are all Turso-specific.

Change Data Capture

Turso extension. Change Data Capture (CDC) is a Turso-specific feature and is not available in standard SQLite.

Overview

Change Data Capture (CDC) records every data modification (insert, update, delete) into a dedicated tracking table. This is useful for building reactive applications, replicating data between systems, maintaining audit logs, and implementing event-driven architectures.

CDC is enabled per connection using a PRAGMA. Once enabled, every INSERT, UPDATE, and DELETE on user tables automatically generates a corresponding row in the CDC table. The level of detail captured depends on the chosen mode.

Note: This feature is currently marked as unstable, meaning the PRAGMA name may change in future versions. The functionality itself is reliable for use.

Syntax

PRAGMA unstable_capture_data_changes_conn('mode[,table_name]');

Parameters

  • mode – the capture mode (see Capture Modes below).
  • table_name – an optional custom name for the CDC table, separated from the mode by a comma. Defaults to turso_cdc when omitted.

Capture Modes

ModeDescription
offDisable CDC for this connection. No further changes are recorded.
idRecord only the primary key (or rowid) of every changed row. The before, after, and updates columns are NULL.
beforeRecord the full row state before each change. Populated for updates and deletes. Inserts still record only the id.
afterRecord the full row state after each change. Populated for inserts and updates. Deletes still record only the id.
fullRecord before state, after state, and an updates blob describing which columns changed. The most detailed mode.

CDC Table Structure

When CDC is first enabled and a DML statement is executed, Turso automatically creates the tracking table (default name turso_cdc) if it does not already exist. The table has the following schema:

(change_id INTEGER PRIMARY KEY AUTOINCREMENT,
 change_time INTEGER,
 change_type INTEGER,
 table_name TEXT,
 id,
 before BLOB,
 after BLOB,
 updates BLOB)
ColumnTypeDescription
change_idINTEGERAuto-incrementing unique identifier for each change entry.
change_timeINTEGERUnix epoch timestamp when the change was recorded.
change_typeINTEGER1 for INSERT, 0 for UPDATE, -1 for DELETE.
table_nameTEXTName of the table that was modified. Schema changes appear as changes to sqlite_schema.
id(any)The primary key or rowid of the affected row.
beforeBLOBA binary record containing the row state before the change. Populated only in before and full modes, and only for updates and deletes. NULL otherwise.
afterBLOBA binary record containing the row state after the change. Populated only in after and full modes, and only for inserts and updates. NULL otherwise.
updatesBLOBA binary record describing per-column update details. Populated only in full mode for updates. NULL otherwise.

Change Type Values

ValueMeaning
1INSERT
0UPDATE
-1DELETE

Enabling CDC

Enable CDC with the desired mode:

-- Capture only primary keys of changed rows
PRAGMA unstable_capture_data_changes_conn('id');
-- Capture full before and after state
PRAGMA unstable_capture_data_changes_conn('full');
-- Store changes in a custom table instead of the default turso_cdc
PRAGMA unstable_capture_data_changes_conn('full,audit_log');

Disabling CDC

Turn off CDC for the current connection. Changes made after this point are not recorded.

PRAGMA unstable_capture_data_changes_conn('off');

Querying Changes

The CDC table is a regular table that can be queried with standard SQL.

-- View all captured changes
SELECT * FROM turso_cdc;

-- View only inserts
SELECT * FROM turso_cdc WHERE change_type = 1;

-- View only updates
SELECT * FROM turso_cdc WHERE change_type = 0;

-- View only deletes
SELECT * FROM turso_cdc WHERE change_type = -1;

-- View changes for a specific table
SELECT * FROM turso_cdc WHERE table_name = 'users';

-- View recent changes (last hour)
SELECT * FROM turso_cdc
WHERE change_time > unixepoch() - 3600;

You can also delete old entries to keep the table from growing indefinitely. Modifications to the CDC table itself are not captured, so deleting rows from turso_cdc does not generate additional CDC entries.

-- Purge entries older than 24 hours
DELETE FROM turso_cdc
WHERE change_time < unixepoch() - 86400;

Helper Functions

Turso provides two scalar functions to decode the binary records stored in the before, after, and updates columns.

table_columns_json_array(table_name)

table_columns_json_array(table_name) -> text

Return a JSON array of column names for the given table. This can be used as the first argument to bin_record_json_object to decode a CDC binary record.

CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price REAL);
SELECT table_columns_json_array('products');
-- ["id","name","price"]

bin_record_json_object(columns_json, blob)

bin_record_json_object(columns_json, blob) -> text

Decode a binary record blob into a JSON object, using the column names from the first argument to label each field. The first argument should be a JSON array of column name strings (typically from table_columns_json_array).

-- Decode the "after" column of a CDC row
SELECT bin_record_json_object(
    table_columns_json_array('products'),
    "after"
) FROM turso_cdc
WHERE table_name = 'products' AND change_type = 1;

Examples

Basic Change Tracking

CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    name TEXT,
    email TEXT
);

-- Enable CDC in id mode
PRAGMA unstable_capture_data_changes_conn('id');

-- Make some changes
INSERT INTO users VALUES (1, 'Alice', 'alice@example.com');
INSERT INTO users VALUES (2, 'Bob', 'bob@example.com');
UPDATE users SET email = 'alice@newdomain.com' WHERE id = 1;
DELETE FROM users WHERE id = 2;

-- View the captured changes
SELECT change_id, change_type, table_name, id
FROM turso_cdc;
-- 1|1|users|1
-- 2|1|users|2
-- 3|0|users|1
-- 4|-1|users|2

Full Mode with Before/After State

CREATE TABLE inventory (
    id INTEGER PRIMARY KEY,
    qty INTEGER
);

PRAGMA unstable_capture_data_changes_conn('full');

INSERT INTO inventory VALUES (1, 100);
UPDATE inventory SET qty = 80 WHERE id = 1;

-- The INSERT has an "after" record but no "before"
-- The UPDATE has both "before" and "after" records, plus "updates"
SELECT change_id, change_type, "before" IS NOT NULL AS has_before,
       "after" IS NOT NULL AS has_after, updates IS NOT NULL AS has_updates
FROM turso_cdc;
-- 1|1|0|1|0
-- 2|0|1|1|1

Custom CDC Table Name

CREATE TABLE orders (id INTEGER PRIMARY KEY, total REAL);

-- Store changes in a table named "order_audit"
PRAGMA unstable_capture_data_changes_conn('id,order_audit');

INSERT INTO orders VALUES (1, 99.95);

SELECT change_id, change_type, table_name, id FROM order_audit;
-- 1|1|orders|1

Per-Connection Isolation

CDC is configured per connection. Each connection can use a different mode and a different CDC table. Changes made by a connection that does not have CDC enabled are not recorded.

-- Connection 1 captures to "audit_log"
PRAGMA unstable_capture_data_changes_conn('full,audit_log');

-- Connection 2 captures to "sync_queue"
PRAGMA unstable_capture_data_changes_conn('id,sync_queue');

-- Changes from Connection 1 go to "audit_log"
-- Changes from Connection 2 go to "sync_queue"

Only changes executed by the connection that enabled CDC are recorded. If another connection modifies the same table without CDC enabled, those changes do not appear in any CDC table.

Schema Changes

In full mode, DDL statements (CREATE TABLE, DROP TABLE, ALTER TABLE, CREATE INDEX, DROP INDEX) are also tracked as changes to sqlite_schema.

PRAGMA unstable_capture_data_changes_conn('full');

CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT);
-- Recorded as an INSERT into sqlite_schema

DROP TABLE products;
-- Recorded as a DELETE from sqlite_schema

Transactions

CDC respects transaction boundaries. Changes within a transaction are recorded when the transaction commits. If a transaction is rolled back, no CDC entries are created for those changes.

CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance REAL);
PRAGMA unstable_capture_data_changes_conn('id');

BEGIN;
INSERT INTO accounts VALUES (1, 1000.00);
INSERT INTO accounts VALUES (2, 2000.00);
COMMIT;

-- Both inserts are recorded after COMMIT
SELECT change_id, change_type, id FROM turso_cdc;
-- 1|1|1
-- 2|1|2

Failed Statements

When a statement fails (for example, due to a constraint violation), neither the data change nor the CDC entry is recorded. Only successful operations appear in the CDC table.

CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);
PRAGMA unstable_capture_data_changes_conn('id');

INSERT INTO tags (label) VALUES ('urgent'), ('review');
-- This fails because 'urgent' already exists:
-- INSERT INTO tags (label) VALUES ('new'), ('other'), ('urgent');

INSERT INTO tags (label) VALUES ('done');

-- Only the successful inserts are recorded
SELECT change_id, change_type, table_name FROM turso_cdc;
-- 1|1|tags
-- 2|1|tags
-- 3|1|tags

Compatibility

Change Data Capture is a Turso extension. It is not available in standard SQLite. The PRAGMA unstable_capture_data_changes_conn, the default turso_cdc table, and the helper functions table_columns_json_array() and bin_record_json_object() are all Turso-specific.

Materialized Views

Turso Extension. This feature is not available in SQLite. Materialized views must be explicitly enabled with the --experimental-views flag.

Syntax

CREATE MATERIALIZED VIEW [IF NOT EXISTS] view-name [(column-name [, ...])]
  AS select-statement
DROP VIEW [IF EXISTS] view-name

Description

Materialized views in Turso are automatically updating database objects that store the results of a query and keep them current in real-time. Unlike traditional materialized views found in other databases that require manual refresh commands, Turso uses Incremental View Maintenance (IVM) to update materialized views as the underlying data changes.

When you insert, update, or delete rows in a base table, any dependent materialized views are updated within the same transaction. Only the incremental changes are processed – not the entire query – making updates efficient even for complex aggregations over large datasets. Because the view is updated inside the same transaction as the base table modification, materialized views are always consistent and never show stale data.

Enabling Materialized Views

Materialized views are an experimental feature. You must pass the --experimental-views flag when starting the Turso CLI:

tursodb --experimental-views database.db

Without this flag, CREATE MATERIALIZED VIEW statements will fail with an error.

How Incremental View Maintenance Works

Traditional materialized views store a snapshot of query results that becomes stale as underlying data changes. Re-executing the entire query to refresh the view is costly for large datasets.

Turso takes a different approach. Instead of re-computing the entire view, IVM tracks what has changed and updates only the affected portions:

  • INSERT – Adds the new row’s contribution to the view.
  • DELETE – Removes the deleted row’s contribution from the view.
  • UPDATE – Treated as a delete of the old value followed by an insert of the new value.

This is particularly powerful for aggregations. If a view computes SUM over millions of rows, inserting one new row only requires adding that single value to the existing sum – not re-summing all rows.

Creating Materialized Views

A materialized view is created with CREATE MATERIALIZED VIEW. The AS clause contains a SELECT statement that defines the view’s contents. When the view is created, the query is executed once to populate the initial data, and then incremental maintenance keeps it up to date.

CREATE TABLE sales (product_id INTEGER, quantity INTEGER, day INTEGER);
INSERT INTO sales VALUES
  (1, 2, 1), (2, 5, 1), (1, 1, 2),
  (3, 1, 2), (2, 3, 3), (1, 1, 3);

CREATE MATERIALIZED VIEW daily_totals AS
  SELECT day, SUM(quantity) as total, COUNT(*) as transactions
  FROM sales
  GROUP BY day;

SELECT * FROM daily_totals ORDER BY day;
-- 1|7.0|2
-- 2|2.0|2
-- 3|4.0|2

Once created, a materialized view is queried like any regular table.

Supported Query Features

Materialized views support a wide range of SQL constructs in their defining query:

  • WHERE filters
  • GROUP BY with positional references, aliases, or expressions
  • Aggregate functions: SUM, COUNT, AVG, MIN, MAX (including DISTINCT variants like COUNT(DISTINCT ...), SUM(DISTINCT ...))
  • JOINs (two-way and three-way)
  • UNION and UNION ALL
  • DISTINCT
  • Scalar expressions and functions in the select list (e.g., b + a, min(a, b))
  • BETWEEN, IN, and CAST in WHERE clauses

Automatic Incremental Updates

Materialized views stay current automatically. Every INSERT, UPDATE, and DELETE on a base table incrementally updates all dependent materialized views.

Insert Maintenance

CREATE TABLE orders (
  order_id INTEGER PRIMARY KEY,
  customer_id INTEGER,
  amount INTEGER
);
INSERT INTO orders VALUES (1, 100, 50), (2, 200, 75);

CREATE MATERIALIZED VIEW customer_totals AS
  SELECT customer_id, SUM(amount) as total, COUNT(*) as order_count
  FROM orders
  GROUP BY customer_id;

SELECT * FROM customer_totals ORDER BY customer_id;
-- 100|50.0|1
-- 200|75.0|1

-- Insert a new order for customer 100
INSERT INTO orders VALUES (3, 100, 25);
SELECT * FROM customer_totals ORDER BY customer_id;
-- 100|75.0|2
-- 200|75.0|1

Update Maintenance

-- Continuing from above: update the amount of order 2
UPDATE orders SET amount = 100 WHERE order_id = 2;
SELECT * FROM customer_totals ORDER BY customer_id;
-- 100|75.0|2
-- 200|100.0|1

Delete Maintenance

-- Continuing from above: delete an order
DELETE FROM orders WHERE order_id = 1;
SELECT * FROM customer_totals ORDER BY customer_id;
-- 100|25.0|1
-- 200|100.0|1

Transactional Consistency

Materialized views are updated inside the same transaction as the base table modification. This guarantees:

  • Atomicity – View changes are committed or rolled back together with base table changes.
  • Consistency – Views never show partial or inconsistent state.
  • Rollback safety – If a transaction rolls back, all view changes are rolled back too.
CREATE TABLE sales (product_id INTEGER, amount INTEGER);
INSERT INTO sales VALUES (1, 100), (1, 200), (2, 150), (2, 250);

CREATE MATERIALIZED VIEW product_totals AS
  SELECT product_id, SUM(amount) as total, COUNT(*) as cnt
  FROM sales
  GROUP BY product_id;

SELECT * FROM product_totals ORDER BY product_id;
-- 1|300.0|2
-- 2|400.0|2

BEGIN;
INSERT INTO sales VALUES (1, 50), (3, 300);
SELECT * FROM product_totals ORDER BY product_id;
-- 1|350.0|3
-- 2|400.0|2
-- 3|300.0|1

ROLLBACK;

-- After rollback, the view returns to its previous state
SELECT * FROM product_totals ORDER BY product_id;
-- 1|300.0|2
-- 2|400.0|2

Materialized Views with JOINs

Materialized views can be defined over joins between two or more tables. Incremental maintenance applies to changes on any of the joined tables.

CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, city TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, product_id INTEGER, quantity INTEGER);
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER);

INSERT INTO customers VALUES (1, 'Alice', 'NYC'), (2, 'Bob', 'LA');
INSERT INTO products VALUES (1, 'Widget', 10), (2, 'Gadget', 20);
INSERT INTO orders VALUES (1, 1, 1, 5), (2, 1, 2, 3), (3, 2, 1, 2);

CREATE MATERIALIZED VIEW sales_summary AS
  SELECT c.name AS customer_name, p.name AS product_name, o.quantity
  FROM customers c
  JOIN orders o ON c.id = o.customer_id
  JOIN products p ON o.product_id = p.id;

SELECT * FROM sales_summary ORDER BY customer_name, product_name;
-- Alice|Gadget|3
-- Alice|Widget|5
-- Bob|Widget|2

Inserting into any of the three tables will incrementally update the view.

Materialized Views with DISTINCT

The DISTINCT keyword is supported both at the query level and inside aggregate functions.

Query-Level DISTINCT

CREATE TABLE events (id INTEGER PRIMARY KEY, category TEXT, status TEXT);
INSERT INTO events VALUES (1, 'A', 'open'), (2, 'B', 'open'),
  (3, 'A', 'open'), (4, 'B', 'closed');

CREATE MATERIALIZED VIEW unique_categories AS
  SELECT DISTINCT category FROM events;

SELECT * FROM unique_categories ORDER BY category;
-- A
-- B

DISTINCT Aggregates

CREATE TABLE orders (id INTEGER PRIMARY KEY, customer TEXT, product TEXT, amount INTEGER);
INSERT INTO orders VALUES (1, 'Alice', 'Widget', 10), (2, 'Alice', 'Gadget', 20),
  (3, 'Alice', 'Widget', 15), (4, 'Bob', 'Widget', 30), (5, 'Bob', 'Widget', 25);

CREATE MATERIALIZED VIEW customer_stats AS
  SELECT customer, COUNT(DISTINCT product) AS unique_products,
    SUM(amount) AS total_amount
  FROM orders
  GROUP BY customer;

SELECT * FROM customer_stats ORDER BY customer;
-- Alice|2|45.0
-- Bob|1|55.0

Materialized Views with UNION

Materialized views can use UNION and UNION ALL to combine results from multiple queries:

CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, department TEXT);
CREATE TABLE contractors (id INTEGER PRIMARY KEY, name TEXT, agency TEXT);
INSERT INTO employees VALUES (1, 'Alice', 'Engineering'), (2, 'Bob', 'Marketing');
INSERT INTO contractors VALUES (1, 'Charlie', 'TechCorp'), (2, 'Diana', 'DesignCo');

CREATE MATERIALIZED VIEW all_workers AS
  SELECT name, department AS affiliation FROM employees
  UNION ALL
  SELECT name, agency AS affiliation FROM contractors;

SELECT * FROM all_workers ORDER BY name;
-- Alice|Engineering
-- Bob|Marketing
-- Charlie|TechCorp
-- Diana|DesignCo

Dropping Materialized Views

Materialized views are dropped using DROP VIEW, the same syntax used for regular views. This removes the view definition and all associated internal state tables.

DROP VIEW sales_summary;

After dropping, the view can be recreated with the same or a different definition:

CREATE MATERIALIZED VIEW sales_summary AS
  SELECT product_id, SUM(amount) AS revenue
  FROM sales
  GROUP BY product_id;

Performance Considerations

Materialized views trade write-time overhead for read-time performance. Each INSERT, UPDATE, or DELETE on a base table must also update any dependent materialized views. Consider these trade-offs when designing your schema:

  • Use materialized views for data that is read frequently and written infrequently.
  • Avoid creating many materialized views on tables with very high write rates.
  • Aggregation views benefit the most: a query that would scan millions of rows becomes a simple table lookup.
  • Join-based views avoid re-executing expensive joins on every read.

Current Limitations

As an experimental feature, materialized views have some limitations:

  • Not all SQL functions are supported in view definitions.
  • Views cannot reference other views.
  • The --experimental-views flag must be provided at startup.

Compatibility

  • This feature is a Turso extension and is not available in SQLite.
  • Materialized views are dropped with DROP VIEW, not a separate DROP MATERIALIZED VIEW statement.
  • The --experimental-views flag is required. The feature is experimental and behavior may change in future releases.

Encryption at Rest

Turso Extension. This feature is not available in SQLite. Encryption must be explicitly enabled with the --experimental-encryption flag.

Overview

Turso supports transparent at-rest encryption to protect database files from unauthorized access. When enabled, all data written to disk is automatically encrypted and all data read from disk is automatically decrypted, with no changes required to SQL queries or application logic.

Encryption operates at the page level: each database page is independently encrypted and authenticated. A random nonce is generated for every page write, and an authentication tag is stored alongside the ciphertext. If a page is corrupted or tampered with, decryption will fail with an error rather than returning garbage data.

Encrypted databases use a modified file header. The first 16 bytes of a standard SQLite database contain the magic string SQLite format 3\0. In an encrypted Turso database, these bytes are replaced with a Turso-specific header that identifies the file as encrypted and records the cipher algorithm. The rest of the database header (bytes 16 through 99) remains unencrypted but is protected by authenticated encryption, so any tampering with the header is detected on read.

Enabling Encryption

Encryption is an experimental feature. You must pass the --experimental-encryption flag when starting the Turso CLI:

tursodb --experimental-encryption database.db

Without this flag, the PRAGMA cipher and PRAGMA hexkey statements are not available.

Supported Ciphers

Turso supports eight authenticated encryption algorithms across two families. All ciphers provide both confidentiality and integrity verification.

AES-GCM Family

Cipher NameKey SizeDescription
aes128gcm16 bytes (128-bit)AES-128 in Galois/Counter Mode
aes256gcm32 bytes (256-bit)AES-256 in Galois/Counter Mode

AES-GCM is a widely deployed AEAD cipher. It is a solid choice when hardware AES-NI acceleration is available.

AEGIS Family

Cipher NameKey SizeDescription
aegis25632 bytes (256-bit)AEGIS-256 (recommended)
aegis128l16 bytes (128-bit)AEGIS-128L
aegis128x216 bytes (128-bit)AEGIS-128 with 2x parallelization
aegis128x416 bytes (128-bit)AEGIS-128 with 4x parallelization
aegis256x232 bytes (256-bit)AEGIS-256 with 2x parallelization
aegis256x432 bytes (256-bit)AEGIS-256 with 4x parallelization

AEGIS ciphers generally offer better performance than AES-GCM while maintaining strong security properties. aegis256 is the recommended default for most use cases. The x2 and x4 variants exploit instruction-level parallelism and may perform better on CPUs with wide execution pipelines.

Cipher names are case-insensitive and accept multiple separator styles. For example, aegis256, aegis-256, and aegis_256 all refer to the same algorithm. Similarly, aes128gcm, aes-128-gcm, and aes_128_gcm are equivalent.

Generating Encryption Keys

Keys are provided as hexadecimal strings. Use OpenSSL or any cryptographically secure random number generator:

# Generate a 256-bit key (32 bytes) -- for aes256gcm, aegis256, aegis256x2, aegis256x4
openssl rand -hex 32

# Generate a 128-bit key (16 bytes) -- for aes128gcm, aegis128l, aegis128x2, aegis128x4
openssl rand -hex 16

A 256-bit key produces a 64-character hex string. A 128-bit key produces a 32-character hex string.

Store your encryption key securely. If the key is lost, the encrypted database cannot be recovered. There is no key recovery mechanism.

Creating an Encrypted Database

There are two ways to configure encryption: PRAGMAs in the SQL shell, or URI parameters on the command line.

Method 1: PRAGMAs

Start Turso with the encryption flag, then set the cipher and key before creating any tables:

tursodb --experimental-encryption database.db
PRAGMA cipher = 'aegis256';
PRAGMA hexkey = '2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d';

-- The database is now encrypted. Use it normally.
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO users VALUES (1, 'Alice');
SELECT * FROM users;

Both PRAGMA cipher and PRAGMA hexkey must be set before any other database operations. The encryption context is established when both values are present. Once set, the cipher and key cannot be changed within the same session.

Method 2: URI Parameters

Specify the cipher and key directly in the database URI:

tursodb --experimental-encryption \
  "file:database.db?cipher=aegis256&hexkey=2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d"

This is equivalent to setting the PRAGMAs and is the preferred method for scripting and automation.

Opening an Encrypted Database

To open an existing encrypted database, you must provide the correct cipher and key. The recommended approach is URI parameters:

tursodb --experimental-encryption \
  "file:database.db?cipher=aegis256&hexkey=2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d"

Alternatively, open the file and set PRAGMAs before any queries:

tursodb --experimental-encryption database.db
PRAGMA cipher = 'aegis256';
PRAGMA hexkey = '2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d';

-- Database is now accessible
SELECT * FROM users;

Opening an encrypted database without providing the correct cipher and key will fail. Providing the wrong cipher or the wrong key will also fail. Turso does not silently return corrupted data.

Reading Encryption Settings

You can query the current cipher with:

PRAGMA cipher;
-- Returns the cipher name, e.g. 'aegis256'

This returns the cipher algorithm configured for the current session. If no cipher has been set, nothing is returned.

PRAGMAs Reference

PRAGMATypeDescription
PRAGMA cipher = 'name'WriteSet the encryption cipher for the session. Must be set before any I/O.
PRAGMA cipherReadReturn the current cipher name.
PRAGMA hexkey = 'hex_string'WriteSet the encryption key as a hex-encoded string. Must match the cipher’s key size.

The key size must match the cipher:

Key SizeHex String LengthCiphers
16 bytes32 charactersaes128gcm, aegis128l, aegis128x2, aegis128x4
32 bytes64 charactersaes256gcm, aegis256, aegis256x2, aegis256x4

Examples

Create and Query an Encrypted Database

# Generate a key
KEY=$(openssl rand -hex 32)

# Create an encrypted database
tursodb --experimental-encryption \
  "file:secret.db?cipher=aegis256&hexkey=$KEY" <<'SQL'
CREATE TABLE secrets (id INTEGER PRIMARY KEY, label TEXT, value TEXT);
INSERT INTO secrets VALUES (1, 'api_key', 'sk-abc123');
INSERT INTO secrets VALUES (2, 'db_password', 'hunter2');
SELECT * FROM secrets;
SQL

Verify Encryption Prevents Unauthorized Access

# Try opening without credentials -- this will fail
tursodb --experimental-encryption secret.db <<'SQL'
SELECT * FROM secrets;
SQL
# Error: database is encrypted or is not a database

Using a 128-bit Cipher

KEY128=$(openssl rand -hex 16)

tursodb --experimental-encryption \
  "file:fast.db?cipher=aes128gcm&hexkey=$KEY128" <<'SQL'
CREATE TABLE logs (id INTEGER PRIMARY KEY, message TEXT);
INSERT INTO logs VALUES (1, 'System started');
SELECT * FROM logs;
SQL

Troubleshooting

“Database is encrypted or is not a database”

This error appears when:

  • Opening an encrypted database without providing the cipher and key.
  • Providing the wrong cipher algorithm for an existing encrypted database.
  • Providing the wrong key.
  • The database file is corrupted.

“Invalid hex string”

This means the hexkey value is not valid hexadecimal. Ensure the string contains only characters 0-9 and a-f (case-insensitive) and that its length matches the cipher’s key size (32 hex characters for 16-byte keys, 64 hex characters for 32-byte keys).

“Cannot reset encryption attributes if already set in the session”

The cipher and key can only be set once per session. If you need to change encryption parameters, close the connection and open a new one.

Compatibility

  • This feature is a Turso extension and is not available in SQLite.
  • Encrypted database files are not compatible with SQLite or other SQLite-based tools. They can only be opened by Turso with the correct cipher and key.
  • The --experimental-encryption flag is required. The feature is experimental and the on-disk format may change in future releases.
  • There is currently no built-in mechanism to change the encryption key of an existing database or to convert between encrypted and unencrypted formats. To re-key a database, create a new encrypted database and copy the data.
================================================ FILE: docs/language-reference/book/turso/custom-types.html ================================================ [File too large to display: 38.6 KB] ================================================ FILE: docs/manual.md ================================================ # Turso Database Manual Welcome to Turso database manual! ## Table of contents - [Turso Database Manual](#turso-database-manual) - [Table of contents](#table-of-contents) - [Introduction](#introduction) - [Getting Started](#getting-started) - [Limitations](#limitations) - [Transactions](#transactions) - [Deferred transaction lifecycle](#deferred-transaction-lifecycle) - [The SQL shell](#the-sql-shell) - [Shell commands](#shell-commands) - [Command line options](#command-line-options) - [The SQL language](#the-sql-language) - [`ALTER TABLE` — change table definition](#alter-table--change-table-definition) - [`BEGIN TRANSACTION` — start a transaction](#begin-transaction--start-a-transaction) - [`COMMIT TRANSACTION` — commit the current transaction](#commit-transaction--commit-the-current-transaction) - [`CREATE INDEX` — define a new index](#create-index--define-a-new-index) - [`CREATE TABLE` — define a new table](#create-table--define-a-new-table) - [`DELETE` - delete rows from a table](#delete---delete-rows-from-a-table) - [`DROP INDEX` - remove an index](#drop-index---remove-an-index) - [`DROP TABLE` — remove a table](#drop-table--remove-a-table) - [`END TRANSACTION` — commit the current transaction](#end-transaction--commit-the-current-transaction) - [`INSERT` — create new rows in a table](#insert--create-new-rows-in-a-table) - [`ROLLBACK TRANSACTION` — abort the current transaction](#rollback-transaction--abort-the-current-transaction) - [`SELECT` — retrieve rows from a table](#select--retrieve-rows-from-a-table) - [`UPDATE` — update rows of a table](#update--update-rows-of-a-table) - [JavaScript API](#javascript-api) - [Installation](#installation) - [Getting Started](#getting-started-1) - [SQLite C API](#sqlite-c-api) - [Basic operations](#basic-operations) - [`sqlite3_open`](#sqlite3_open) - [`sqlite3_prepare`](#sqlite3_prepare) - [`sqlite3_step`](#sqlite3_step) - [`sqlite3_column`](#sqlite3_column) - [WAL manipulation](#wal-manipulation) - [`libsql_wal_frame_count`](#libsql_wal_frame_count) - [Journal Mode](#journal-mode) - [Encryption](#encryption) - [Vector search](#vector-search) - [Full-Text Search](#full-text-search-experimental) - [CDC](#cdc-early-preview) - [Index Method](#index-method-experimental) - [Appendix A: Turso Internals](#appendix-a-turso-internals) - [Frontend](#frontend) - [Parser](#parser) - [Code generator](#code-generator) - [Query optimizer](#query-optimizer) - [Virtual Machine](#virtual-machine) - [MVCC](#mvcc) - [Pager](#pager) - [I/O](#io) - [Encryption](#encryption-1) - [References](#references) ## Introduction Turso is an in-process relational database engine, aiming towards full compatibility with SQLite. Unlike client-server database systems such as PostgreSQL or MySQL, which require applications to communicate over network protocols for SQL execution, an in-process database is in your application memory space. This embedded architecture eliminates network communication overhead, allowing for the best case of low read and write latencies in the order of sub-microseconds. ### Getting Started You can install Turso on your computer as follows: ``` curl --proto '=https' --tlsv1.2 -LsSf \ https://github.com/tursodatabase/turso/releases/latest/download/turso_cli-installer.sh | sh ``` ``` brew install turso ``` When you have the software installed, you can start a SQL shell as follows: ```console $ tursodb Turso Enter ".help" for usage hints. Connected to a transient in-memory database. Use ".open FILENAME" to reopen on a persistent database turso> SELECT 'hello, world'; hello, world ``` ### Limitations Turso aims towards full SQLite compatibility but has the following limitations: * Query result ordering is not guaranteed to be the same (see [#2964](https://github.com/tursodatabase/turso/issues/2964) for more discussion) * No multi-process access * No multi-threading * No savepoints * No triggers * No views * No vacuum * UTF-8 is the only supported character encoding For more detailed list of SQLite compatibility, please refer to [COMPAT.md](../COMPAT.md). #### MVCC limitations The MVCC implementation is experimental and has the following limitations: * Indexes cannot be created and databases with indexes cannot be used. * All the data is eagerly loaded from disk to memory on first access so using big databases may take a long time to start, and will consume a lot of memory * Only `PRAGMA wal_checkpoint(TRUNCATE)` is supported and it blocks both readers and writers * `AUTOINCREMENT` is not supported. Tables with `AUTOINCREMENT` cannot be created or inserted into while MVCC is enabled. * Many features may not work, work incorrectly, and/or cause a panic. * Queries may return incorrect results * If a database is written to using MVCC and then opened again without MVCC, the changes are not visible unless first checkpointed ## The SQL shell The `tursodb` command provides an interactive SQL shell, similar to `sqlite3`. You can start it in in-memory mode as follows: ```console $ tursodb Turso Enter ".help" for usage hints. Connected to a transient in-memory database. Use ".open FILENAME" to reopen on a persistent database turso> SELECT 'hello, world'; hello, world ``` ### Shell commands The shell supports commands in addition to SQL statements. The commands start with a dot (".") followed by the command. The supported commands are: | Command | Description | |---------|-------------| | `.schema` | Display the database schema | | `.dump` | Dump database contents as SQL statements | ### Command line options The SQL shell supports the following command line options: | Option | Description | |--------|-------------| | `-m`, `--output-mode` `` | Configure output mode. Supported values for ``:
  • `pretty` for pretty output (default)
  • `list` for minimal SQLite compatible format
| `-q`, `--quiet` | Don't display program information at startup | | `-e`, `--echo` | Print commands before execution | | `--readonly` | Open database in read-only mode | | `-h`, `--help` | Print help | | `-V`, `--version` | Print version | | `--mcp` | Start a MCP server instead of the interactive shell | | `--experimental-encryption` | Enable experimental encryption at rest feature. **Note:** the feature is not production ready so do not use it for critical data right now. | | `--experimental-views` | Enable experimental views feature. **Note**: the feature is not production ready so do not use it for critical data right now. | ## Transactions A transaction is a sequence of one or more SQL statements that execute as a single, atomic unit of work. A transaction ensures **atomicity** and **isolation**, meaning that either all SQL statements are executed or none of them are, and that concurrent transactions don't interfere with other transactions. Transactions maintain database integrity in the presence of errors, crashes, and concurrent access. Each connection can have exactly one active transaction at a time. All statements prepared on a connection belong to the same transaction context. You cannot interleave statement execution across different transactions on a single connection. When you need concurrency (including `BEGIN CONCURRENT`), you need to use *different connections*, not parallel statements within the same connection. Turso supports three types of transactions: **deferred**, **immediate**, and **concurrent** transactions: * **Deferred (default)**: The transaction begins in a suspended state and does not acquire locks immediately. It starts a read transaction when the first read SQL statement (e.g., `SELECT`) runs, and upgrades to a write transaction only when the first write SQL statement (e.g., `INSERT`, `UPDATE`, `DELETE`) executes. This mode allows concurrency for reads and delays write locks, which can reduce contention. * **Immediate**: The transaction starts immediately with a reserved write lock, preventing other write transactions from starting concurrently but allowing reads. It attempts to acquire the write lock right away on the `BEGIN` statement, which can fail if another write transaction exists. The `EXCLUSIVE` mode is always an alias for `IMMEDIATE` in Turso, just like it is in SQLite in WAL mode. * **Concurrent (MVCC only)**: The transaction begins immediately and allows multiple concurrent read and write transactions. When a concurrent transaction commits, the database performs row-level conflict detection and returns a `SQLITE_BUSY` (write-write conflict) error if the transaction attempted to modify a row that was concurrently modified by another transaction. This mode provides the highest level of concurrency at the cost of potential transaction conflicts that must be retried by the application. The transaction isolation level provided by concurrent transactions is snapshot isolation. ### Deferred transaction lifecycle When the `BEGIN DEFERRED TRANSACTION` statement executes, the database acquires no snapshot or locks. Instead, the transaction is in a suspended state until the first read or write SQL statement executes. When the first read statement executes, a read transaction begins. The database allows multiple read transactions to exist concurrently. When the first write statement executes, a read transaction is either upgraded to a write transaction or a write transaction begins. The database allows a single write transaction at a time. Concurrent write transactions fail with `SQLITE_BUSY` error. If a deferred transaction remains unused (no reads or writes), it is automatically restarted by the database if another write transaction commits before the transaction is used. However, if the deferred transaction has already performed reads and another concurrent write transaction commits, it cannot automatically restart due to potential snapshot inconsistency. In this case, the deferred transaction must be manually rolled back and restarted by the application. ### Concurrent transaction lifecycle Concurrent transactions are only available when MVCC (Multi-Version Concurrency Control) is enabled in the database. They use optimistic concurrency control to allow multiple transactions to modify the database simultaneously. When the `BEGIN CONCURRENT TRANSACTION` statement executes, the database: 1. Assigns a unique transaction ID to the transaction 2. Records a begin timestamp from the logical clock 3. Creates an empty read set and write set to track accessed rows 4. Does **not** acquire any locks Unlike deferred transactions which delay locking, concurrent transactions never acquire locks. Instead, they rely on MVCC's snapshot isolation and conflict detection at commit time. #### Read snapshot isolation Each concurrent transaction reads from a consistent snapshot of the database as of its begin timestamp. This means: - Reads see all data committed before the transaction's begin timestamp - Reads do **not** see writes from other transactions that commit after this transaction starts - Reads from the same transaction are consistent (repeatable reads within the transaction) - Multiple concurrent transactions can read and write simultaneously without blocking each other All rows read by the transaction are tracked in the transaction's read set, and all rows written are tracked in the write set. #### Commit and conflict detection When a concurrent transaction commits, the database performs these steps: 1. **Exclusive transaction check**: If there is an active exclusive transaction (started with `BEGIN IMMEDIATE` or a `BEGIN DEFERRED` that upgraded to a write transaction), the concurrent transaction **cannot commit** and receives a `SQLITE_BUSY` error. Concurrent transactions can read and write concurrently with exclusive transactions, but cannot commit until the exclusive transaction completes. 2. **Write-write conflict detection**: For each row in the transaction's write set, the database checks if the row was modified by another transaction. A write-write conflict occurs when: - The row is currently being modified by another active transaction, or - The row was modified by a transaction that committed after this transaction's begin timestamp 3. **Commit or abort**: If no conflicts are detected, the transaction commits successfully. All row versions in the write set have their begin timestamps updated to the transaction's commit timestamp, making them visible to future transactions. If a conflict is detected, the transaction fails with a `SQLITE_BUSY` error and must be rolled back and retried by the application. #### Interaction with exclusive transactions Concurrent transactions can coexist with exclusive transactions (deferred and immediate), but with important restrictions: - **Concurrent transactions can read and write** while an exclusive transaction is active - **Concurrent transactions cannot commit** while an exclusive transaction holds the exclusive lock - **Exclusive transactions block concurrent transaction commits**, not their reads or writes This design allows concurrent transactions to make progress during an exclusive transaction, but ensures that exclusive transactions truly have exclusive write access when needed (for example, schema changes). **Best practice**: For maximum concurrency in MVCC mode, use `BEGIN CONCURRENT` for all write transactions. Only use `BEGIN IMMEDIATE` or `BEGIN DEFERRED` when you need exclusive write access that prevents any concurrent commits. ## The SQL language ### `ALTER TABLE` — change table definition **Synopsis:** ```sql ALTER TABLE old_name RENAME TO new_name ALTER TABLE table_name ADD COLUMN column_name [ column_type ] ALTER TABLE table_name DROP COLUMN column_name ``` **Example:** ```console turso> CREATE TABLE t(x); turso> .schema t; CREATE TABLE t (x); turso> ALTER TABLE t ADD COLUMN y TEXT; turso> .schema t CREATE TABLE t ( x , y TEXT ); turso> ALTER TABLE t DROP COLUMN y; turso> .schema t CREATE TABLE t ( x ); ``` ### `BEGIN TRANSACTION` — start a transaction **Synopsis:** ```sql BEGIN [ transaction_mode ] [ TRANSACTION ] ``` where `transaction_mode` is one of the following: * A `DEFERRED` transaction in a suspended state and does not acquire locks immediately. It starts a read transaction when the first read SQL statement (e.g., `SELECT`) runs, and upgrades to a write transaction only when the first write SQL statement (e.g., `INSERT`, `UPDATE`, `DELETE`) executes. * An `IMMEDIATE` transaction starts immediately with a reserved write lock, preventing other write transactions from starting concurrently but allowing reads. It attempts to acquire the write lock right away on the `BEGIN` statement, which can fail if another write transaction exists. * An `EXCLUSIVE` transaction is always an alias for `IMMEDIATE`. * A `CONCURRENT` transaction begins immediately, but allows other concurrent transactions. **See also:** * [Transactions](#transactions) * [END TRANSACTION](#end-transaction--commit-the-current-transaction) ### `COMMIT TRANSACTION` — commit the current transaction **Synopsis:** ```sql COMMIT [ TRANSACTION ] ``` **See also:** * [END TRANSACTION](#end-transaction--commit-the-current-transaction) ### `CREATE INDEX` — define a new index > [!NOTE] > Indexes are currently experimental in Turso and not enabled by default. **Synopsis:** ```sql CREATE INDEX [ index_name ] ON table_name ( column_name ) ``` **Example:** ``` turso> CREATE TABLE t(x); turso> CREATE INDEX t_idx ON t(x); ``` ### `CREATE TABLE` — define a new table **Synopsis:** ```sql CREATE TABLE table_name ( column_name [ column_type ], ... ) ``` **Example:** ```console turso> DROP TABLE t; turso> CREATE TABLE t(x); turso> .schema t CREATE TABLE t (x); ``` ### `DELETE` - delete rows from a table **Synopsis:** ```sql DELETE FROM table_name [ WHERE expression ] ``` **Example:** ```console turso> DELETE FROM t WHERE x > 1; ``` ### `DROP INDEX` - remove an index > [!NOTE] > Indexes are currently experimental in Turso and not enabled by default. **Example:** ```console turso> DROP INDEX idx; ``` ### `DROP TABLE` — remove a table **Example:** ```console turso> DROP TABLE t; ``` ### `END TRANSACTION` — commit the current transaction ```sql END [ TRANSACTION ] ``` **See also:** * `COMMIT TRANSACTION` ### `INSERT` — create new rows in a table **Synopsis:** ```sql INSERT INTO table_name [ ( column_name, ... ) ] VALUES ( value, ... ) [, ( value, ... ) ...] ``` **Example:** ``` turso> INSERT INTO t VALUES (1), (2), (3); turso> SELECT * FROM t; ┌───┐ │ x │ ├───┤ │ 1 │ ├───┤ │ 2 │ ├───┤ │ 3 │ └───┘ ``` ### `ROLLBACK TRANSACTION` — abort the current transaction ```sql ROLLBACK [ TRANSACTION ] ``` ### `SELECT` — retrieve rows from a table **Synopsis:** ```sql SELECT expression [ FROM table-or-subquery ] [ WHERE condition ] [ GROUP BY expression ] ``` **Example:** ```console turso> SELECT 1; ┌───┐ │ 1 │ ├───┤ │ 1 │ └───┘ turso> CREATE TABLE t(x); turso> INSERT INTO t VALUES (1), (2), (3); turso> SELECT * FROM t WHERE x >= 2; ┌───┐ │ x │ ├───┤ │ 2 │ ├───┤ │ 3 │ └───┘ ``` ### `UPDATE` — update rows of a table **Synopsis:** ```sql UPDATE table_name SET column_name = value [WHERE expression] ``` **Example:** ```console turso> CREATE TABLE t(x); turso> INSERT INTO t VALUES (1), (2), (3); turso> SELECT * FROM t; ┌───┐ │ x │ ├───┤ │ 1 │ ├───┤ │ 2 │ ├───┤ │ 3 │ └───┘ turso> UPDATE t SET x = 4 WHERE x >= 2; turso> SELECT * FROM t; ┌───┐ │ x │ ├───┤ │ 1 │ ├───┤ │ 4 │ ├───┤ │ 4 │ └───┘ ``` ## JavaScript API Turso supports a JavaScript API, both with native and WebAssembly package options. Please read the [JavaScript API reference](docs/javascript-api-reference.md) for more information. ### Installation Installing the native package: ```console npm i @tursodatabase/database ``` Installing the WebAssembly package: ```console npm i @tursodatabase/database --cpu wasm32 ``` ### Getting Started To use Turso from JavaScript application, you need to import `Database` type from the `@tursodatabase/database` package. You can the prepare a statement with `Database.prepare` method and execute the SQL statement with `Statement.get()` method. ``` import { connect } from '@tursodatabase/database'; const db = await connect('turso.db'); const row = db.prepare('SELECT 1').get(); console.log(row); ``` ## SQLite C API Turso supports a subset of the SQLite C API, including libSQL extensions. ### Basic operations #### `sqlite3_open` Open a connection to a database. **Synopsis:** ```c int sqlite3_open(const char *filename, sqlite3 **db_out); int sqlite3_open_v2(const char *filename, sqlite3 **db_out, int _flags, const char *_z_vfs); ``` #### `sqlite3_prepare` Prepare a SQL statement for execution. **Synopsis:** ```c int sqlite3_prepare_v2(sqlite3 *db, const char *sql, int _len, sqlite3_stmt **out_stmt, const char **_tail); ``` #### `sqlite3_step` Evaluate a prepared statement until it yields the next row or completes. **Synopsis:** ```c int sqlite3_step(sqlite3_stmt *stmt); ``` #### `sqlite3_column` Return the value of a column for the current row of a statement. **Synopsis:** ```c int sqlite3_column_type(sqlite3_stmt *_stmt, int _idx); int sqlite3_column_count(sqlite3_stmt *_stmt); const char *sqlite3_column_decltype(sqlite3_stmt *_stmt, int _idx); const char *sqlite3_column_name(sqlite3_stmt *_stmt, int _idx); int64_t sqlite3_column_int64(sqlite3_stmt *_stmt, int _idx); double sqlite3_column_double(sqlite3_stmt *_stmt, int _idx); const void *sqlite3_column_blob(sqlite3_stmt *_stmt, int _idx); int sqlite3_column_bytes(sqlite3_stmt *_stmt, int _idx); const unsigned char *sqlite3_column_text(sqlite3_stmt *stmt, int idx); ``` ### WAL manipulation #### `libsql_wal_frame_count` Get the number of frames in the WAL. **Synopsis:** ```c int libsql_wal_frame_count(sqlite3 *db, uint32_t *p_frame_count); ``` **Description:** The `libsql_wal_frame_count` function returns the number of frames in the WAL in the `p_frame_count` parameter. **Return Values:** * `SQLITE_OK` if the number of frames in the WAL file is successfully returned. * `SQLITE_MISUSE` if the `db` is NULL. * SQLITE_ERROR if an error occurs while getting the number of frames in the WAL file. **Safety Requirements:** * The `db` parameter must be a valid pointer to a `sqlite3` database connection. * The `p_frame_count` must be a valid pointer to a `u32` that will store the * number of frames in the WAL file. ## Journal Mode Turso supports switching between different journal modes at runtime using the `PRAGMA journal_mode` statement. Journal modes control how the database handles transaction logging and durability. ### Supported Modes | Mode | Description | |------|-------------| | `wal` | Write-Ahead Logging mode. The default mode for new databases. Provides good concurrency for readers and writers. | | `mvcc` | Multi-Version Concurrency Control mode. Enables concurrent transactions with snapshot isolation. **Note:** the feature is not production ready so do not use it for critical data right now. | > **Note:** Legacy SQLite journal modes (`delete`, `truncate`, `persist`, `memory`, `off`) are recognized but not currently supported. Attempting to switch to these modes will return an error. ### Usage **Query the current journal mode:** ```sql PRAGMA journal_mode; ``` **Switch to WAL mode:** ```sql PRAGMA journal_mode = wal; ``` **Switch to MVCC mode:** ```sql PRAGMA journal_mode = mvcc; ``` ### Example ```console turso> PRAGMA journal_mode; ┌──────────────┐ │ journal_mode │ ├──────────────┤ │ wal │ └──────────────┘ turso> PRAGMA journal_mode = mvcc; ┌───────────────────┐ │ journal_mode │ ├───────────────────┤ │ mvcc │ └───────────────────┘ ``` ### Important Notes - Switching journal modes triggers a checkpoint to ensure all pending changes are persisted before the mode change. - When switching from MVCC to WAL mode, the MVCC log file is cleared after checkpointing. - Legacy SQLite databases are automatically converted to WAL mode when opened. ## Encryption The work-in-progress RFC is [here](https://github.com/tursodatabase/turso/issues/2447). To use encryption, you need to enable it via flag `experimental-encryption`. To get started, generate a secure 32 byte key in hex: ```shell $ openssl rand -hex 32 2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d ``` Specify the key and cipher at the time of db creation to use encryption. Here is [sample test](https://github.com/tursodatabase/turso/blob/main/tests/integration/query_processing/encryption.rs): ```shell $ cargo run -- --experimental-encryption database.db PRAGMA cipher = 'aegis256'; -- or 'aes256gcm' PRAGMA hexkey = '2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d'; ``` Alternatively you can provide the encryption parameters directly in a **URI**. For example: ```shell $ cargo run -- --experimental-encryption \ "file:database.db?cipher=aegis256&hexkey=2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d" ``` > **Note:** To reopen an already *encrypted database*, the file **must** be opened in URI format with the `cipher` and `hexkey` passed as URI parameters. Now, to reopen `database.db` the command below must be run: ```shell $ cargo run -- --experimental-encryption \ "file:database.db?cipher=aegis256hexkey=2d7a30108d3eb3e45c90a732041fe54778bdcf707c76749fab7da335d1b39c1d" ``` ## Vector search Turso supports vector search for building workloads such as semantic search, recommendation systems, and similarity matching. Vector embeddings can be stored and queried using specialized functions for distance calculations. ### Vector types Turso supports **dense**, **sparse**, **quantized**, and **binary** vector representations: #### Dense vectors Dense vectors store a value for every dimension. Turso provides two precision levels: * **Float32 dense vectors** (`vector32`): 32-bit floating-point values, suitable for most machine learning embeddings (e.g., OpenAI embeddings, sentence transformers). Uses 4 bytes per dimension. * **Float64 dense vectors** (`vector64`): 64-bit floating-point values for applications requiring higher precision. Uses 8 bytes per dimension. Dense vectors are ideal for embeddings from neural networks where most dimensions contain non-zero values. #### Sparse vectors Sparse vectors only store non-zero values and their indices, making them memory-efficient for high-dimensional data with many zero values: * **Float32 sparse vectors** (`vector32_sparse`): Stores only non-zero 32-bit float values along with their dimension indices. Sparse vectors are ideal for TF-IDF representations, bag-of-words models, and other scenarios where most dimensions are zero. #### Quantized vectors * **8-bit quantized vectors** (`vector8`): Linearly quantizes each float value to an 8-bit integer using min/max scaling. Uses 1 byte per dimension plus 8 bytes for quantization parameters (alpha and shift). Dequantization formula: `f_i = alpha * q_i + shift`. Quantized vectors reduce memory usage by ~4x compared to Float32 with minimal precision loss, ideal for large-scale similarity search where storage is a concern. #### Binary vectors * **1-bit binary vectors** (`vector1bit`): Packs each dimension into a single bit (positive values → 1, non-positive → 0). Uses 1 bit per dimension. Extracted values are displayed as +1/-1. Binary vectors provide extreme compression (~32x vs Float32) and fast distance computation via bitwise operations. Ideal for binary hashing techniques and approximate nearest neighbor search. ### Vector functions #### Creating and converting vectors **`vector32(value)`** Converts a text or blob value into a 32-bit dense vector. ```sql SELECT vector32('[1.0, 2.0, 3.0]'); ``` **`vector32_sparse(value)`** Converts a text or blob value into a 32-bit sparse vector. ```sql SELECT vector32_sparse('[0.0, 1.5, 0.0, 2.3, 0.0]'); ``` **`vector64(value)`** Converts a text or blob value into a 64-bit dense vector. ```sql SELECT vector64('[1.0, 2.0, 3.0]'); ``` **`vector8(value)`** Converts a text or blob value into an 8-bit quantized vector. Float values are linearly quantized to the 0–255 range using the min and max of the input. ```sql SELECT vector8('[1.0, 2.0, 3.0, 4.0]'); ``` **`vector1bit(value)`** Converts a text or blob value into a 1-bit binary vector. Positive values become 1, non-positive values become 0 (displayed as +1/-1 when extracted). ```sql SELECT vector_extract(vector1bit('[1, -1, 1, 1, -1, 0, 0.5]')); -- Returns: [1,-1,1,1,-1,-1,1] ``` **`vector_extract(blob)`** Extracts and displays a vector blob as human-readable text. ```sql SELECT vector_extract(embedding) FROM documents; ``` #### Distance functions Turso provides three distance metrics for measuring vector similarity. Both vectors must be of the same type and dimension. All distance functions support Float32, Float64, Float32Sparse, Float8, and Float1Bit vectors unless noted otherwise. **`vector_distance_cos(v1, v2)`** Computes the cosine distance between two vectors. Returns a value between 0 (identical direction) and 2 (opposite direction). Cosine distance is computed as `1 - cosine_similarity`. For `vector1bit` vectors, returns the Hamming distance (number of differing bits). Cosine distance is ideal for: - Text embeddings where magnitude is less important than direction - Comparing document similarity ```sql SELECT name, vector_distance_cos(embedding, vector32('[0.1, 0.5, 0.3]')) AS distance FROM documents ORDER BY distance LIMIT 10; ``` **`vector_distance_l2(v1, v2)`** Computes the Euclidean (L2) distance between two vectors. Returns the straight-line distance in n-dimensional space. Not supported for `vector1bit` vectors (returns an error). L2 distance is ideal for: - Image embeddings where absolute differences matter - Spatial data and geometric problems - When embeddings are not normalized ```sql SELECT name, vector_distance_l2(embedding, vector32('[0.1, 0.5, 0.3]')) AS distance FROM documents ORDER BY distance LIMIT 10; ``` **`vector_distance_dot(v1, v2)`** Computes the negative dot product between two vectors. Returns `-sum(v1[i] * v2[i])`. Lower values indicate more similar vectors. Dot product distance is ideal for: - Normalized embeddings (equivalent to cosine distance when vectors are unit-length) - Maximum inner product search (MIPS) ```sql SELECT name, vector_distance_dot(embedding, vector32('[0.1, 0.5, 0.3]')) AS distance FROM documents ORDER BY distance LIMIT 10; ``` **`vector_distance_jaccard(v1, v2)`** Computes the weighted Jaccard distance between two vectors, measuring dissimilarity based on the ratio of minimum to maximum values across dimensions. For `vector1bit` vectors, computes binary Jaccard distance: `1 - |intersection| / |union|` over set bits. Jaccard distance is ideal for: - Sparse vectors with many zero values - Set-like comparisons - TF-IDF and bag-of-words representations - Binary similarity with `vector1bit` ```sql SELECT name, vector_distance_jaccard(sparse_embedding, vector32_sparse('[0.0, 1.0, 0.0, 2.0]')) AS distance FROM documents ORDER BY distance LIMIT 10; ``` #### Utility functions **`vector_concat(v1, v2)`** Concatenates two vectors into a single vector. The resulting vector has dimensions equal to the sum of both input vectors. ```sql SELECT vector_concat(vector32('[1.0, 2.0]'), vector32('[3.0, 4.0]')); -- Results in a 4-dimensional vector: [1.0, 2.0, 3.0, 4.0] ``` **`vector_slice(vector, start_index, end_index)`** Extracts a slice of a vector from `start_index` to `end_index` (exclusive). ```sql SELECT vector_slice(vector32('[1.0, 2.0, 3.0, 4.0, 5.0]'), 1, 4); -- Results in: [2.0, 3.0, 4.0] ``` ### Example: Semantic search Here's a complete example of building a semantic search system: ```sql -- Create a table for documents with embeddings CREATE TABLE documents ( id INTEGER PRIMARY KEY, name TEXT, content TEXT, embedding BLOB ); -- Insert documents with precomputed embeddings INSERT INTO documents (name, content, embedding) VALUES ('Doc 1', 'Machine learning basics', vector32('[0.2, 0.5, 0.1, 0.8]')), ('Doc 2', 'Database fundamentals', vector32('[0.1, 0.3, 0.9, 0.2]')), ('Doc 3', 'Neural networks guide', vector32('[0.3, 0.6, 0.2, 0.7]')); -- Find documents similar to a query embedding SELECT name, content, vector_distance_cos(embedding, vector32('[0.25, 0.55, 0.15, 0.75]')) AS similarity FROM documents ORDER BY similarity LIMIT 5; ``` ## Full-Text Search (Experimental) Turso provides full-text search (FTS) capabilities powered by the [Tantivy](https://github.com/quickwit-oss/tantivy) search engine library. FTS enables efficient text search with relevance ranking, boolean queries, phrase matching, and more. > **Note:** Full-text search is an experimental feature and requires the `fts` feature to be enabled at compile time. ### Creating an FTS Index Create an FTS index on text columns using the `USING fts` syntax: ```sql CREATE INDEX idx_articles ON articles USING fts (title, body); ``` You can index multiple columns in a single FTS index. The index automatically tracks inserts, updates, and deletes to the underlying table. ### Tokenizer Configuration Configure how text is tokenized using the `WITH` clause: ```sql -- Use ngram tokenizer for autocomplete/substring matching CREATE INDEX idx_products ON products USING fts (name) WITH (tokenizer = 'ngram'); -- Use raw tokenizer for exact-match fields CREATE INDEX idx_tags ON articles USING fts (tag) WITH (tokenizer = 'raw'); ``` **Available tokenizers:** | Tokenizer | Description | Use Case | |-----------|-------------|----------| | `default` | Lowercase, punctuation split, 40 char limit | General English text | | `raw` | No tokenization - exact match only | IDs, UUIDs, tags | | `simple` | Basic whitespace/punctuation split | Simple text without lowercase | | `whitespace` | Split on whitespace only | Space-separated tokens | | `ngram` | 2-3 character n-grams | Autocomplete, substring matching | ### Field Weights Configure relative importance of indexed columns for relevance scoring: ```sql -- Title matches are 2x more important than body matches CREATE INDEX idx_articles ON articles USING fts (title, body) WITH (weights = 'title=2.0,body=1.0'); -- Combined with tokenizer CREATE INDEX idx_docs ON docs USING fts (name, description) WITH (tokenizer = 'simple', weights = 'name=3.0,description=1.0'); ``` ### Query Functions Turso provides three FTS functions: #### `fts_match(col1, col2, ..., 'query')` #### or `WHERE col1, col2 MATCH 'query'` Returns a boolean indicating if the row matches the query. Used in `WHERE` clauses: ```sql SELECT id, title FROM articles WHERE fts_match(title, body, 'database'); ``` #### `fts_score(col1, col2, ..., 'query')` Returns the BM25 relevance score for ranking results: ```sql SELECT fts_score(title, body, 'database') as score, id, title FROM articles WHERE fts_match(title, body, 'database') ORDER BY score DESC LIMIT 10; ``` #### `fts_highlight(col1, col2, ..., before_tag, after_tag, 'query')` Returns text with matching terms wrapped in tags for display: ```sql SELECT fts_highlight(body, '', '', 'database') as highlighted FROM articles WHERE fts_match(title, body, 'database'); -- Returns: "Learn about database optimization" ``` ### Query Syntax The query string supports Tantivy's query syntax: [docs](https://docs.rs/tantivy/latest/tantivy/query/struct.QueryParser.html) | Syntax | Example | Description | |--------|---------|-------------| | Single term | `database` | Match documents containing "database" | | Multiple terms (OR) | `database sql` | Match documents with "database" OR "sql" | | AND operator | `database AND sql` | Match documents with both terms | | NOT operator | `database NOT nosql` | Match "database" but exclude "nosql" | | Phrase search | `"full text search"` | Match exact phrase | | Prefix search | `data*` | Match terms starting with "data" | | Column filter | `title:database` | Match "database" only in title field | | Boosting | `title:database^2` | Boost matches in title field | ### Complex Queries FTS functions work with additional WHERE conditions: ```sql SELECT id, title, fts_score(title, body, 'Rust') as score FROM articles WHERE fts_match(title, body, 'Rust') AND category = 'tech' AND published = 1 ORDER BY score DESC; ``` ### Index Maintenance Use `OPTIMIZE INDEX` to merge Tantivy segments for better query performance: ```sql -- Optimize a specific FTS index OPTIMIZE INDEX idx_articles; -- Optimize all FTS indexes OPTIMIZE INDEX; ``` Run optimization after bulk inserts or when query performance degrades. ### Example: Building a Search Feature ```sql -- Create a documents table CREATE TABLE documents ( id INTEGER PRIMARY KEY, title TEXT, content TEXT, category TEXT ); -- Create FTS index with weighted fields CREATE INDEX fts_docs ON documents USING fts (title, content) WITH (weights = 'title=2.0,content=1.0'); -- Insert documents INSERT INTO documents VALUES (1, 'Introduction to SQL', 'Learn SQL basics and queries', 'tutorial'), (2, 'Advanced SQL Techniques', 'Complex joins and optimization', 'tutorial'), (3, 'Database Design', 'Schema design best practices', 'architecture'); -- Search with relevance ranking SELECT id, title, fts_score(title, content, 'SQL') as score, fts_highlight(content, '', '', 'SQL') as snippet FROM documents WHERE fts_match(title, content, 'SQL') ORDER BY score DESC; ``` ### Limitations | Limitation | Description | |------------|-------------| | No MATCH operator | Use `fts_match()` function instead of `WHERE table MATCH 'query'` | | No read-your-writes in transaction | FTS changes visible only after COMMIT | ## CDC (Early Preview) Turso supports [Change Data Capture](https://en.wikipedia.org/wiki/Change_data_capture), a powerful pattern for tracking and recording changes to your database in real-time. Instead of periodically scanning tables to find what changed, CDC automatically logs every insert, update, and delete as it happens per connection. ### Enabling CDC ```sql PRAGMA capture_data_changes_conn('[,custom_cdc_table]'); ``` ### Parameters - `` can be: - `off`: Turn off CDC for the connection - `id`: Logs only the `rowid` (most compact) - `before`: Captures row state before updates and deletes - `after`: Captures row state after inserts and updates - `full`: Captures both before and after states (recommended for complete audit trail) - `custom_cdc` is optional, It lets you specify a custom table to capture changes. If no table is provided, Turso uses a default `turso_cdc` table. When **Change Data Capture (CDC)** is enabled for a connection, Turso automatically logs all modifications from that connection into a dedicated table (default: `turso_cdc`). This table records each change with details about the operation, the affected row or schema object, and its state **before** and **after** the modification. > **Note:** Currently, the CDC table is a regular table stored explicitly on disk. If you use full CDC mode and update rows frequently, each update of size N bytes will be written three times to disk (once for the before state, once for the after state, and once for the actual value in the WAL). Frequent updates in full mode can therefore significantly increase disk I/O. - **`change_id` (INTEGER)** A monotonically increasing integer uniquely identifying each change record.(guaranteed by turso-db) - Always strictly increasing. - Serves as the primary key. - **`change_time` (INTEGER)** > turso-db guarantee nothing about properties of the change_time sequence Local timestamp (Unix epoch, seconds) when the change was recorded. - Not guaranteed to be strictly increasing (can drift or repeat). - **`change_type` (INTEGER)** Indicates the type of operation: - `1` → INSERT - `0` → UPDATE (also used for ALTER TABLE) - `-1` → DELETE (also covers DROP TABLE, DROP INDEX) - **`table_name` (TEXT)** Name of the affected table. - For schema changes (DDL), this is always `"sqlite_schema"`. - **`id` (INTEGER)** Rowid of the affected row in the source table. - For DDL operations: rowid of the `sqlite_schema` entry. - **Note:** `WITHOUT ROWID` tables are not supported in the tursodb and CDC - **`before` (BLOB)** Full state of the row/schema **before** an UPDATE or DELETE - NULL for INSERT. - For DDL changes, may contain the definition of the object before modification. - **`after` (BLOB)** Full state of the row/schema **after** an INSERT or UPDATE - NULL for DELETE. - For DDL changes, may contain the definition of the object after modification. - **`updates` (BLOB)** Granular details about the change. - For UPDATE: shows specific column modifications. > CDC records are visible even before a transaction commits. > Operations that fail (e.g., constraint violations) are not recorded in CDC. > Changes to the CDC table itself are also logged to CDC table. if CDC is enabled for that connection. ```zsh Example: turso> PRAGMA capture_data_changes_conn('full'); turso> .tables turso_cdc turso> CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT ); turso> INSERT INTO users VALUES (1, 'John'), (2, 'Jane'); UPDATE users SET name='John Doe' WHERE id=1; DELETE FROM users WHERE id=2; SELECT * FROM turso_cdc; ┌───────────┬─────────────┬─────────────┬───────────────┬────┬──────────┬──────────────────────────────────────────────────────────────────────────────┬───────────────┐ │ change_id │ change_time │ change_type │ table_name │ id │ before │ after │ updates │ ├───────────┼─────────────┼─────────────┼───────────────┼────┼──────────┼──────────────────────────────────────────────────────────────────────────────┼───────────────┤ │ 1 │ 1756713161 │ 1 │ sqlite_schema │ 2 │ │ ytableusersusersCREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT) │ │ ├───────────┼─────────────┼─────────────┼───────────────┼────┼──────────┼──────────────────────────────────────────────────────────────────────────────┼───────────────┤ │ 2 │ 1756713176 │ 1 │ users │ 1 │ │ John │ │ ├───────────┼─────────────┼─────────────┼───────────────┼────┼──────────┼──────────────────────────────────────────────────────────────────────────────┼───────────────┤ │ 3 │ 1756713176 │ 1 │ users │ 2 │ │ Jane │ │ ├───────────┼─────────────┼─────────────┼───────────────┼────┼──────────┼──────────────────────────────────────────────────────────────────────────────┼───────────────┤ │ 4 │ 1756713176 │ 0 │ users │ 1 │ John │ John Doe │ John Doe │ ├───────────┼─────────────┼─────────────┼───────────────┼────┼──────────┼──────────────────────────────────────────────────────────────────────────────┼───────────────┤ │ 5 │ 1756713176 │ -1 │ users │ 2 │ Jane │ │ │ └───────────┴─────────────┴─────────────┴───────────────┴────┴──────────┴──────────────────────────────────────────────────────────────────────────────┴───────────────┘ turso> ``` If you modify your table schema (adding/dropping columns), the `table_columns_json_array()` function returns the current schema, not the historical one. This can lead to incorrect results when decoding older CDC records. Manually track schema versions by storing the output of `table_columns_json_array()` before making schema changes. ## Index Method (Experimental) `tursodb` allows developers to implement custom data access methods and integrate them seamlessly with the query planner. This feature is conceptually similar to [VTable](https://www.sqlite.org/vtab.html) but provides greater flexibility and automatic query planner integration. The feature is experimental and currently gated behind the `--experimental-index-method` flag. ### DDL Index Methods can be created using standard `CREATE INDEX` statements by specifying a custom module name: ```sql CREATE INDEX t_idx ON t USING index_method_name (column1, column2); ``` Index Methods can also include optional parameters whose values may be numeric, floating-point, string, or blob literals: ```sql CREATE INDEX t_idx ON t USING index_method_name (c) WITH (a = 1, b = 1.2, c = 'text', d = x'deadbeef'); ``` To remove an index, use the standard `DROP INDEX t_idx` statement. ### DML Data modification operations for Index Methods are executed implicitly for every modification of the base table (similarly to native B-tree indices): 1. Each `INSERT` operation on the table executes an `IdxInsert` opcode for the Index Method, passing the relevant column values and the `rowid` of the inserted row. 2. Each `DELETE` operation executes an `IdxDelete` opcode with the corresponding column values and the deleted row's `rowid`. 3. Each `UPDATE` operation is internally translated into a pair of `DELETE` + `INSERT` operations. ### DQL At present, Index Methods can only be used implicitly if the query planner decides to apply them. This decision depends on whether the query matches one of the suitable patterns provided by the Index Method implementation. If parts of a query align with a registered pattern, the planner may substitute default table access method with the Index Method. For example, an Index Method can define the following query pattern: ```sql SELECT vector_distance_jaccard(embedding, ?) AS distance FROM documents ORDER BY distance LIMIT ?; ``` This pattern describes the shape of the output (a single `distance` column), the parameter placeholders (query embedding and limit), and the type of query it can optimize (an ordered retrieval by distance). The planner can match this pattern against a user query like: ```sql SELECT id, content, created_at FROM documents ORDER BY vector_distance_jaccard(embedding, ?) LIMIT 10; ``` Because the query is a *superset* of the pattern, the planner can safely apply the Index Method, enriching its output (`distance`) with data from the main table (`id`, `content`, `created_at`), using the `rowid` provided by each row from the Index Method. The query planner is conservative and will avoid using an Index Method if doing so would alter the query's semantics. Consider: ```sql SELECT id, content, created_at FROM documents WHERE user = ? ORDER BY vector_distance_jaccard(embedding, ?) LIMIT 10; ``` The additional filter `WHERE user = ?` does not fit the Index Method's query pattern, so the planner correctly falls back to the default plan. ### Internals Each Index Method consists of three traits that work together (for details, see the index method module [root](../core/index_method/mod.rs)): * **`IndexMethod`** — the root trait for all Index Methods, responsible for creating `IndexMethodAttachment` instances for a given table. * **`IndexMethodAttachment`** — represents an Index Method instance bound to a specific table. It can create cursors for query execution and defines the metadata needed for integration with the query planner. * **`IndexMethodCursor`** — provides methods for accessing and updating data, as well as for managing the underlying storage during `CREATE INDEX` and `DROP INDEX` operations. While Index Methods can implement arbitrary logic internally, it's generally recommended to use a B-tree as the underlying storage mechanism. To support this, `tursodb` provides a special `backing_btree` Index Method that other Index Methods can use to create auxiliary tables for storing supporting data. For more details, see [`toy_vector_sparse_ivf`](../core/index_method/toy_vector_sparse_ivf.rs) implementation. ## Appendix A: Turso Internals Turso's architecture resembles SQLite's but differs primarily in its asynchronous I/O model. This asynchronous design enables applications to leverage modern I/O interfaces like `io_uring,` maximizing storage device performance. While an in-process database offers significant performance advantages, integration with cloud services remains crucial for operations like backups. Turso's asynchronous I/O model facilitates this by supporting networked storage capabilities. The high-level interface to Turso is the same as in SQLite: * SQLite query language * The `sqlite3_prepare()` function for translating SQL statements to programs ("prepared statements") * The `sqlite3_step()` function for executing programs If we start with the SQLite query language, you can use the `turso` command, for example, to evaluate SQL statements in the shell: ``` turso> SELECT 'hello, world'; hello, world ``` To execute this SQL statement, the shell uses the `sqlite3_prepare()` interface to parse the statement and generate a bytecode program, a step called preparing a statement. When a statement is prepared, it can be executed using the `sqlite3_step()` function. To illustrate the different components of Turso, we can look at the sequence diagram of a query from the CLI to the bytecode virtual machine (VDBE): ```mermaid sequenceDiagram participant main as cli/main participant Database as core/lib/Database participant Connection as core/lib/Connection participant Parser as sql/mod/Parser participant translate as translate/mod participant Statement as core/lib/Statement participant Program as vdbe/mod/Program main->>Database: open_file Database->>main: Connection main->>Connection: query(sql) Note left of Parser: Parser uses vendored sqlite3-parser Connection->>Parser: next() Note left of Parser: Passes the SQL query to Parser Parser->>Connection: Cmd::Stmt (ast/mod.rs) Note right of translate: Translates SQL statement into bytecode Connection->>translate:translate(stmt) translate->>Connection: Program Connection->>main: Ok(Some(Rows { Statement })) note right of main: a Statement with
a reference to Program is returned main->>Statement: step() Statement->>Program: step() Note left of Program: Program executes bytecode instructions
See https://www.sqlite.org/opcode.html Program->>Statement: StepResult Statement->>main: StepResult ``` To drill down into more specifics, we inspect the bytecode program for a SQL statement using the `EXPLAIN` command in the shell. For our example SQL statement, the bytecode looks as follows: ``` turso> EXPLAIN SELECT 'hello, world'; addr opcode p1 p2 p3 p4 p5 comment ---- ----------------- ---- ---- ---- ------------- -- ------- 0 Init 0 4 0 0 Start at 4 1 String8 0 1 0 hello, world 0 r[1]='hello, world' 2 ResultRow 1 1 0 0 output=r[1] 3 Halt 0 0 0 0 4 Transaction 0 0 0 0 5 Goto 0 1 0 0 ``` The instruction set of the virtual machine consists of domain specific instructions for a database system. Every instruction consists of an opcode that describes the operation and up to 5 operands. In the example above, execution starts at offset zero with the `Init` instruction. The instruction sets up the program and branches to a instruction at address specified in operand `p2`. In our example, address 4 has the `Transaction` instruction, which begins a transaction. After that, the `Goto` instruction then branches to address 1 where we load a string constant `'hello, world'` to register `r[1]`. The `ResultRow` instruction produces a SQL query result using contents of `r[1]`. Finally, the program terminates with the `Halt` instruction. ### Frontend #### Parser The parser is the module in the front end that processes SQLite query language input data, transforming it into an abstract syntax tree (AST) for further processing. The parser is an in-tree fork of [lemon-rs](https://github.com/gwenn/lemon-rs), which in turn is a port of SQLite parser into Rust. The emitted AST is handed over to the code generation steps to turn the AST into virtual machine programs. #### Code generator The code generator module takes AST as input and produces virtual machine programs representing executable SQL statements. At high-level, code generation works as follows: 1. `JOIN` clauses are transformed into equivalent `WHERE` clauses, which simplifies code generation. 2. `WHERE` clauses are mapped into bytecode loops 3. `ORDER BY` causes the bytecode program to pass result rows to a sorter before returned to the application. 4. `GROUP BY` also causes the bytecode programs to pass result rows to an aggregation function before results are returned to the application. #### Query optimizer TODO ### Virtual Machine TODO ### MVCC The database implements a multi-version concurrency control (MVCC) using a hybrid architecture that combines an in-memory index with persistent storage through WAL (Write-Ahead Logging) and SQLite database files. The implementation draws from the Hekaton approach documented in Larson et al. (2011), with key modifications for durability handling. The database maintains a centralized in-memory MVCC index that serves as the primary coordination point for all database connections. This index provides shared access across all active connections and stores the most recent versions of modified data. It implements version visibility rules for concurrent transactions following the Hekaton MVCC design. The architecture employs a three-tier storage hierarchy consisting of the MVCC index in memory as the primary read/write target for active transactions, a page cache in memory serving as an intermediate buffer for data retrieved from persistent storage, and persistent storage comprising WAL files and SQLite database files on disk. _Read operations_ follow a lazy loading strategy with a specific precedence order. The database first queries the in-memory MVCC index to check if the requested row exists and is visible to the current transaction. If the row is not found in the MVCC index, the system performs a lazy read from the page cache. When necessary, the page cache retrieves data from both the WAL and the underlying SQLite database file. _Write operations_ are handled entirely within the in-memory MVCC index during transaction execution. This design provides high-performance writes with minimal latency, immediate visibility of changes within the transaction scope, and isolation from other concurrent transactions until the transaction is committed. _Commit operation_ ensures durability through a two-phase approach: first, the system writes the complete transaction write set from the MVCC index to the page cache, then the page cache contents are flushed to the WAL, ensuring durable storage of the committed transaction. This commit protocol guarantees that once a transaction commits successfully, all changes are persisted to durable storage and will survive system failures. While the implementation follows Hekaton's core MVCC principles, it differs in one significant aspect regarding logical change tracking. Unlike Hekaton, this system does not maintain a record of logical changes after flushing data to the WAL. This design choice simplifies compatibility with the SQLite database file format. ### Pager TODO ### I/O Every I/O operation shall be tracked by a corresponding `Completion`. A `Completion` is just an object that tracks a particular I/O operation. The database `IO` will call it's complete callback to signal that the operation was complete, thus ensuring that every tracker can be poll to see if the operation succeeded. To advance the Program State Machines, you must first wait for the tracked completions to complete. This can be done either by busy polling (`io.wait_for_completion`) or polling once and then yielding - e.g ```rust if !completion.is_completed { return StepResult::IO; } ``` This allows us to be flexible in places where we do not have the state machines in place to correctly return the Completion. Thus, we can block in certain places to avoid bigger refactorings, which opens up the opportunity for such refactorings in separate PRs. To know if a function does any sort of I/O we just have to look at the function signature. If it returns `Completion`, `Vec` or `IOResult`, then it does I/O. The `IOResult` struct looks as follows: ```rust pub enum IOCompletions { Single(Completion), } #[must_use] pub enum IOResult { Done(T), IO(IOCompletions), } ``` To combine multiple completions, use `CompletionGroup`: ```rust let mut group = CompletionGroup::new(|_| {}); group.add(&completion1); group.add(&completion2); let combined = group.build(); // Single completion that waits for all ``` This implies that when a function returns an `IOResult`, it must be called again until it returns an `IOResult::Done` variant. This works similarly to how `Future`s are polled in rust. When you receive a `Poll::Ready(None)`, it means that the future stopped it's execution. In a similar vein, if we receive `IOResult::Done`, the function/state machine has reached the end of it's execution. `IOCompletions` is here to signal that, if we are executing any I/O operation, that we need to propagate the completions that are generated from it. This design forces us to handle the fact that a function is asynchronous in nature. This is essentially [function coloring](https://www.tedinski.com/2018/11/13/function-coloring.html), but done at the application level instead of the compiler level. ### Encryption #### Goals - Per-page encryption as an opt-in feature, so users don't have to compile/load the encryption extension - All pages in db and WAL file to be encrypted on disk - Least performance overhead as possible #### Design 1. We do encryption at the page level, i.e., each page is encrypted and decrypted individually. 2. At db creation, we take key and cipher scheme information. We store the scheme information (also version) in the db file itself. 3. The key is not stored anywhere. So each connection should carry an encryption key. Trying to open a db with an invalid or empty key should return an error. 4. We generate a new randomized, cryptographically safe nonce every time we write a page. 5. We store the authentication tag and the nonce in the page itself. 6. We can support different cipher algorithms: AES, ChachaPoly, AEGIS, etc. 7. We can support key rotation. But rekeying would require writing the entire database. 8. We should also add import/export functionality to the CLI for better DX and compatibility with SQLite. #### Metadata management We store the nonce and tag (or the verification bits) in the page itself. During decryption, we will load these to decrypt and verify the data. Example: Assume the page size is 4096 bytes and we use AEGIS 256. So we reserve the last 48 bytes for the nonce (32 bytes) and tag (16 bytes). ```ignore Unencrypted Page Encrypted Page ┌───────────────┐ ┌───────────────┐ │ │ │ │ │ Page Content │ │ Encrypted │ │ (4048 bytes) │ ────────► │ Content │ │ │ │ (4048 bytes) │ ├───────────────┤ ├───────────────┤ │ Reserved │ │ Tag (16) │ │ (48 bytes) │ ├───────────────┤ │ [empty] │ │ Nonce (32) │ └───────────────┘ └───────────────┘ 4096 bytes 4096 bytes ``` The above applies to all the pages except Page 1. Page 1 contains the SQLite header (the first 100 bytes). Specifically, bytes 16 to 24 contain metadata which is required to initialize the connection, which happens before we can set up the encryption context. So, we don't encrypt the header but instead use the header data as additional data (AD) for the encryption of the rest of the page. This provides us protection against tampering and corruption for the unencrypted portion. On disk, the encrypted page 1 contains special bytes replacing SQLite's magic bytes (the first 16 bytes): ```ignore Turso Header (16 bytes) ┌─────────┬───────┬────────┬──────────────────┐ │ │ │ │ │ │ "Turso" │Version│ Cipher │ Unused │ │ (5) │ (1) │ (1) │ (9 bytes) │ │ │ │ │ │ └─────────┴───────┴────────┴──────────────────┘ 0-4 5 6 7-15 Standard SQLite Header: "SQLite format 3\0" (16 bytes) ↓ Turso Encrypted Header: "Turso" + Version + Cipher ID + Unused ``` The current version is 0x00. The cipher IDs are: | Algorithm | Cipher ID | |-----------|-----------| | AES-GCM (128-bit) | 1 | | AES-GCM (256-bit) | 2 | | AEGIS-256 | 3 | | AEGIS-256-X2 | 4 | | AEGIS-256-X4 | 5 | | AEGIS-128L | 6 | | AEGIS-128-X2 | 7 | | AEGIS-128-X4 | 8 | #### Future work 1. I have omitted the key derivation aspect. We can later add passphrase and key derivation support. 2. Pages in memory are unencrypted. We can explore memory enclaves and other mechanisms. #### Other Considerations You may check the [RFC discussion](https://github.com/tursodatabase/turso/issues/2447) and also [Checksum RFC discussion](https://github.com/tursodatabase/turso/issues/2178) for the design decisions. - SQLite has some unused bytes in the header left for future expansion. We considered using this portion to store the cipher information metadata but decided not to because these may get used in the future. - Another alternative was to truncate tag bytes of page 1, then store the meta information. Ultimately, it seemed much better to store the metadata in the magic bytes. - For per-page metadata, we decided to store it in the reserved space. The reserved space is for extensions; however, I could not find any usage of it other than the official Checksum shim and other encryption extensions. ### References Per-Åke Larson et al. "High-Performance Concurrency Control Mechanisms for Main-Memory Databases." In _VLDB '11_ [SQLite]: https://www.sqlite.org/ ================================================ FILE: docs/sql-reference/.gitignore ================================================ src/ book/ ================================================ FILE: docs/sql-reference/README.md ================================================ # Turso Reference This directory contains the Turso reference documentation — both the SQL language reference and the CLI reference. The files are `.mdx` (Markdown + JSX components) and render as readable markdown on GitHub. ## Structure ``` sql-reference/ ├── data-types.mdx # Storage classes, type affinity, STRICT tables ├── expressions.mdx # Literals, operators, CAST, CASE, pattern matching ├── experimental-features.mdx # How to enable experimental features (CLI + all SDKs) ├── statements/ # DDL and DML statements │ ├── select.mdx │ ├── insert.mdx │ ├── update.mdx │ ├── ... │ ├── create-type.mdx # Turso-specific custom types │ └── transactions.mdx # Includes BEGIN CONCURRENT (Turso MVCC) ├── functions/ # Built-in functions │ ├── scalar.mdx │ ├── aggregate.mdx │ ├── json.mdx │ ├── vector.mdx # Turso-specific vector search │ ├── fts.mdx # Turso-specific full-text search │ └── ... ├── pragmas.mdx # All supported PRAGMAs (includes CDC, encryption) ├── extensions.mdx # UUID, regexp, vector, time, CSV, percentile ├── compatibility.mdx # SQLite compatibility notes and known differences └── cli/ # CLI reference ├── getting-started.mdx # Installation, usage patterns, output modes ├── command-line-options.mdx # CLI flags and arguments └── shell-commands.mdx # Dot commands (.tables, .schema, etc.) ``` ## Editing guidelines - Turso-specific features are marked with `` callouts. Keep this convention. - Experimental features should link to `experimental-features.mdx` for enablement instructions. - Unsupported SQLite features are documented only in `compatibility.mdx`, not on individual pages. - Every function should have a parameter table with types, a return type, and at least one example. - Use `sql` language tags on all code blocks. - Keep pages self-contained — use explicit cross-references instead of "as mentioned above". ## Local preview (optional) These docs are integrated into [turso-docs](https://github.com/tursodatabase/turso-docs) for production rendering with Mintlify. To preview locally, use mdBook (OSS, no Node required): ```bash # Install mdbook if you don't have it cargo install mdbook # Preview with live reload at http://localhost:3000 cd docs/sql-reference ./preview.sh ``` The script converts `.mdx` files to markdown, transforms Mintlify callouts to blockquotes, and serves via mdBook. The ``/`` callouts render as blockquotes — not as styled boxes, but all content is readable. If you add a new page, update `preview.sh` to include it in the SUMMARY. ================================================ FILE: docs/sql-reference/book.toml ================================================ [book] title = "Turso Reference" authors = ["Turso"] src = "src" [output.html] no-section-label = true git-repository-url = "https://github.com/tursodatabase/turso" ================================================ FILE: docs/sql-reference/cli/command-line-options.mdx ================================================ --- title: Command-Line Options description: CLI flags, arguments, and server modes for the tursodb command sidebarTitle: Command-Line Options --- # Command-Line Options ```bash tursodb [OPTIONS] [DATABASE] [SQL] ``` ## Arguments | Argument | Default | Description | |----------|---------|-------------| | `DATABASE` | `:memory:` | Path to a SQLite database file. If omitted, an in-memory database is created | | `SQL` | — | SQL statement to execute. If provided, the shell runs the query and exits | ### Examples ```bash # In-memory database tursodb # Open an existing file tursodb customers.db # Run a query and exit tursodb customers.db "SELECT count(*) FROM orders;" # Pipe SQL from stdin echo "SELECT 1 + 1;" | tursodb -q ``` ## Options ### `-m, --output-mode ` Set the output display format. | Mode | Description | |------|-------------| | `pretty` | Table with borders (default) | | `list` | Pipe-delimited values | | `line` | One column per line with column names | ```bash tursodb -m list mydata.db "SELECT * FROM users;" ``` ### `-o, --output ` Redirect query output to a file instead of stdout. ```bash tursodb -o results.txt mydata.db "SELECT * FROM users;" ``` ### `-q, --quiet` Suppress the startup banner and version information. ```bash tursodb -q mydata.db ``` ### `-e, --echo` Print each SQL statement before executing it. Useful for debugging scripts. ```bash echo "SELECT 42;" | tursodb -q -e ``` ``` SELECT 42; 42 ``` ### `-v, --vfs ` Select the Virtual File System backend. | VFS | Description | |-----|-------------| | `syscall` | Standard OS file I/O (default) | | `memory` | In-memory storage | | `io_uring` | Linux io_uring (requires feature flag) | ```bash tursodb -v memory ``` ### `-t, --tracing-output ` Write internal log traces to a file. Useful for debugging and performance analysis. ```bash tursodb -t trace.log mydata.db ``` ### `--readonly` Open the database in read-only mode. Write operations will return an error. ```bash tursodb --readonly production.db "SELECT count(*) FROM users;" ``` Attempting to write in read-only mode: ```bash tursodb --readonly production.db "INSERT INTO users VALUES (1, 'test');" # Error: Resource is read-only ``` ### `--mcp` Start a Model Context Protocol server instead of the interactive shell. This allows AI assistants and other MCP clients to interact with the database via JSON-RPC. ```bash tursodb --mcp mydata.db ``` ### `--sync-server
` Start a sync server for database replication, listening at the given address. ```bash tursodb --sync-server 0.0.0.0:8080 mydata.db ``` ## Experimental Feature Flags These flags enable features that are still under development. They may change or be removed in future releases. Experimental features may have incomplete implementations or known limitations. Use them for testing and development only. | Flag | Description | |------|-------------| | `--experimental-views` | Enable views (`CREATE VIEW` / `DROP VIEW`) | | `--experimental-custom-types` | Enable custom types (`CREATE TYPE` / `DROP TYPE`) | | `--experimental-encryption` | Enable at-rest database encryption | | `--experimental-index-method` | Enable custom index methods. Necessary for FTS and Sparse Vector indexes | | `--experimental-autovacuum` | Enable automatic database vacuuming | | `--experimental-triggers` | Enable triggers (`CREATE TRIGGER` / `DROP TRIGGER`) | | `--experimental-attach` | Enable `ATTACH DATABASE` / `DETACH DATABASE` | ```bash # Enable views and triggers tursodb --experimental-views --experimental-triggers mydata.db ``` ## Other Flags | Flag | Description | |------|-------------| | `--unsafe-testing` | Enable unsafe testing features (e.g., `sqlite_dbpage` writes) | | `-h, --help` | Print usage help | | `-V, --version` | Print version information | ================================================ FILE: docs/sql-reference/cli/getting-started.mdx ================================================ --- title: Getting Started description: Install and use the Turso interactive SQL shell sidebarTitle: Getting Started --- # Getting Started The Turso CLI (`tursodb`) is an interactive SQL shell for working with Turso databases. It supports in-memory and file-based databases, multiple output modes, CSV import, database cloning, and built-in documentation. ## Quick Start ### In-Memory Database Launch the shell with a transient in-memory database: ```bash tursodb ``` ### Open a Database File ```bash tursodb mydata.db ``` ### Run a Query Directly Pass SQL as a command-line argument to run it and exit: ```bash tursodb mydata.db "SELECT * FROM users;" ``` ### Pipe SQL from stdin ```bash echo "SELECT sqlite_version();" | tursodb -q ``` ## Interactive Shell When launched without a SQL argument, the shell provides an interactive REPL with command history and syntax highlighting. ### Multi-line Input Unfinished statements (missing semicolons, unbalanced parentheses) automatically continue on the next line. The prompt indicates nesting depth: ``` tursodb> SELECT * ...> FROM employees ...> WHERE department = 'Engineering'; ``` ### Command History Command history is saved automatically to `~/.limbo_history` and accessible with up/down arrow keys. ### Exiting Use `.quit` or `.exit` to leave the shell. Pressing Ctrl+C twice also exits. ## Output Modes Turso supports three output modes, selectable with the `-m` flag or the `.mode` dot command. ### Pretty (Default) Human-readable table with borders: ```bash tursodb -q -m pretty ``` ``` ┌────┬─────────┬─────────────┬─────────┐ │ id │ name │ department │ salary │ ├────┼─────────┼─────────────┼─────────┤ │ 1 │ Alice │ Engineering │ 95000.0 │ ├────┼─────────┼─────────────┼─────────┤ │ 2 │ Bob │ Marketing │ 72000.0 │ └────┴─────────┴─────────────┴─────────┘ ``` ### List Pipe-delimited values suitable for scripting: ```bash tursodb -q -m list ``` ``` 1|Alice|Engineering|95000.0 2|Bob|Marketing|72000.0 ``` ### Line One column per line, with column names: ```bash tursodb -q -m line ``` ``` id = 1 name = Alice department = Engineering salary = 95000.0 ``` ## Non-Interactive Mode When input is piped (not a terminal), the shell runs in non-interactive mode: - Output defaults to `list` mode (override with `-m`) - No startup banner, history, or syntax highlighting - Exits with code 1 on query errors ```bash echo "SELECT 1 + 2;" | tursodb -q ``` ## Server Modes The CLI can also run as a server instead of an interactive shell. ### MCP Server Start a [Model Context Protocol](https://modelcontextprotocol.io/) server, allowing AI assistants to interact with the database: ```bash tursodb --mcp mydata.db ``` ### Sync Server Start a sync server for database replication: ```bash tursodb --sync-server 0.0.0.0:8080 mydata.db ``` ================================================ FILE: docs/sql-reference/cli/shell-commands.mdx ================================================ --- title: Shell Commands description: Dot commands available in the Turso interactive SQL shell sidebarTitle: Shell Commands --- # Shell Commands Dot commands are special commands available in the interactive shell. They start with a period (`.`) and do not require a trailing semicolon. ``` tursodb> .help ``` ## Database & Navigation ### .open Open a database file, optionally specifying a VFS backend. ``` .open [VFS] ``` ``` tursodb> .open mydata.db tursodb> .open mydata.db memory ``` ### .quit Exit the shell. Aliases: `.q`, `.qu`, `.qui`. ``` tursodb> .quit ``` ### .exit Exit the shell with an optional return code. Aliases: `.ex`, `.exi`. ``` .exit [CODE] ``` ``` tursodb> .exit tursodb> .exit 1 ``` ### .cd Change the current working directory. ``` .cd ``` ``` tursodb> .cd /tmp ``` ## Schema Inspection ### .tables List all tables in the database, optionally filtered by a pattern. ``` .tables [PATTERN] ``` ```sql CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT); CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT); ``` ``` tursodb> .tables employees departments tursodb> .tables emp% employees ``` ### .schema Display the CREATE statement for a table, or all tables if no argument is given. ``` .schema [TABLE] ``` ``` tursodb> .schema employees CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT); tursodb> .schema CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT); CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT); ``` ### .indexes Show index names, optionally filtered by table. ``` .indexes [TABLE] ``` ```sql CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, department TEXT); CREATE INDEX idx_dept ON employees(department); CREATE INDEX idx_name ON employees(name); ``` ``` tursodb> .indexes idx_dept idx_name tursodb> .indexes employees idx_dept idx_name ``` ### .databases List all attached databases. ``` tursodb> .databases main: /path/to/mydata.db r/w ``` ## Output Control ### .mode Set the output display mode. ``` .mode ``` | Mode | Description | |------|-------------| | `pretty` | Table with borders (default) | | `list` | Pipe-delimited values | | `line` | One column per line with column names | ``` tursodb> .mode list tursodb> SELECT 1 AS a, 2 AS b; 1|2 tursodb> .mode line tursodb> SELECT 1 AS a, 2 AS b; a = 1 b = 2 tursodb> .mode pretty tursodb> SELECT 1 AS a, 2 AS b; ┌───┬───┐ │ a │ b │ ├───┼───┤ │ 1 │ 2 │ └───┴───┘ ``` ### .headers Toggle column headers on or off in `list` mode. ``` .headers ``` ``` tursodb> .mode list tursodb> .headers on tursodb> SELECT 1 AS x, 2 AS y; x|y 1|2 tursodb> .headers off tursodb> SELECT 1 AS x, 2 AS y; 1|2 ``` ### .nullvalue Set the string displayed for NULL values in `list` mode. ``` .nullvalue ``` ``` tursodb> .mode list tursodb> .nullvalue [NULL] tursodb> SELECT 1 AS a, NULL AS b; 1|[NULL] ``` ### .output Redirect query output to a file. Call with no argument or `stdout` to restore output to the terminal. ``` .output [PATH] ``` ``` tursodb> .output results.txt tursodb> SELECT * FROM employees; tursodb> .output tursodb> -- Output is back to the terminal ``` ### .echo Toggle echo mode. When on, each SQL statement is printed before execution. ``` .echo ``` ``` tursodb> .echo on tursodb> SELECT 42; SELECT 42; ┌────┐ │ 42 │ ├────┤ │ 42 │ └────┘ ``` ### .show Display current shell settings. ``` tursodb> .show Settings: Output mode: pretty DB: mydata.db Output: STDOUT Null value: CWD: /home/user Echo: off Headers: off ``` ## Performance & Diagnostics ### .timer Toggle query timing. When on, shows execution time and I/O statistics after each query. ``` .timer ``` ``` tursodb> .timer on tursodb> SELECT 42; 42 Command stats: ---------------------------- total: 442 us (this includes parsing/coloring of cli app) query execution stats: ---------------------------- Execution: avg=4 us, total=8 us I/O: No samples available ``` ### .stats Display database statistics. Use `on`/`off` to toggle automatic display after every query, or `--reset` to clear counters. ``` .stats [on|off] [--reset] ``` ``` tursodb> .stats Connection Metrics: Total statements: 3 High-water marks: Max VM steps: 19 Max rows read: 1 Aggregate Statistics: Statement Metrics: Row Operations: Rows read: 1 Rows written: 2 Execution: VM steps: 48 Instructions: 47 ... ``` ### .opcodes Show VDBE (Virtual Database Engine) opcodes with descriptions. Optionally filter by opcode name. ``` .opcodes [OPCODE] ``` ``` tursodb> .opcodes Integer Integer ------- The 64-bit integer value P1 is written into register P2. This is different from SQLite, where this opcode is used for 32-bit integers ``` ### .vfslist List available Virtual File System modules. ``` tursodb> .vfslist Available VFS modules: memory syscall ``` ## Data Import & Export ### .dump Output the entire database as SQL statements that can recreate it. ``` tursodb> .dump PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT); INSERT INTO "employees" VALUES(1,'Alice'); INSERT INTO "employees" VALUES(2,'Bob'); COMMIT; ``` The output of `.dump` can be piped into another Turso instance to recreate the database: ```bash tursodb original.db ".dump" | tursodb -q clone.db ``` ### .import Import data from a file into a table. ``` .import [--csv] [--skip N] [--verbose] ``` | Option | Default | Description | |--------|---------|-------------| | `--csv` | on | Use comma as field separator and newline as record separator | | `--skip N` | 0 | Skip the first N rows (useful for skipping a header row) | | `--verbose` | off | Print progress information during import | ``` tursodb> CREATE TABLE people (name TEXT, age INTEGER); tursodb> .import --csv --skip 1 data.csv people tursodb> SELECT * FROM people; ┌─────────┬─────┐ │ name │ age │ ├─────────┼─────┤ │ Alice │ 30 │ ├─────────┼─────┤ │ Bob │ 25 │ ├─────────┼─────┤ │ Charlie │ 35 │ └─────────┴─────┘ ``` Given a CSV file `data.csv`: ```csv name,age Alice,30 Bob,25 Charlie,35 ``` ### .clone Clone the current database to a new file. ``` .clone ``` ``` tursodb> .clone backup.db employees... done ``` ### .read Execute SQL statements from a file. ``` .read ``` ``` tursodb> .read setup.sql ``` ### .load Load an extension library. ``` .load ``` ``` tursodb> .load ./liblimbo_regexp ``` Only Turso-native extensions can be loaded. SQLite `.so`/`.dll` loadable extensions are not supported. See the [Extensions](/docs/sql-reference/extensions) documentation for available extensions. ## Debugging ### .dbtotxt Display raw database page contents in hex format. Useful for debugging storage-level issues. ``` .dbtotxt [--page PAGE_NO] ``` ``` tursodb> .dbtotxt --page 1 | size 8192 pagesize 4096 filename :memory: | page 1 offset 0 | 0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 SQLite format 3. | 16: 10 00 02 02 00 40 20 20 00 00 00 01 00 00 00 02 .....@ ........ ... ``` ### .dbconfig Print or set database configuration flags. Currently a no-op in Turso. ``` .dbconfig [CONFIG] [on|off] ``` ## Documentation ### .manual Display built-in manual pages for Turso features. Call with no argument to list all available manuals. ``` .manual [PAGE] ``` Aliases: `.man`. ``` tursodb> .manual # Turso Manual Pages Available manuals: custom-types Custom Types for STRICT Tables cdc Change Data Capture encryption At-Rest Database Encryption vector Vector Search materialized-views Live Materialized Views mcp Model Context Protocol server tursodb> .manual vector ``` ================================================ FILE: docs/sql-reference/compatibility.mdx ================================================ --- title: SQLite Compatibility description: Turso compatibility with SQLite - supported features, known differences, and limitations sidebarTitle: Compatibility --- # SQLite Compatibility The authoritative compatibility reference is maintained at [`COMPAT.md`](https://github.com/tursodatabase/turso/blob/main/COMPAT.md) in the repository root. It tracks the support status for every SQL statement, expression, function, PRAGMA, C API function, VDBE opcode, and extension. Refer to that document for the current state of compatibility. It is updated alongside code changes to stay accurate. ## Turso-Specific Extensions These features extend Turso beyond SQLite compatibility: | Feature | Description | |---------|-------------| | [CREATE TYPE](/docs/sql-reference/statements/create-type) | User-defined types for STRICT tables | | [CREATE MATERIALIZED VIEW](/docs/sql-reference/statements/create-materialized-view) | Live materialized views with incremental maintenance | | [BEGIN CONCURRENT](/docs/sql-reference/statements/transactions#begin-concurrent) | Optimistic concurrent write transactions (MVCC) | | [Vector functions](/docs/sql-reference/functions/vector) | Vector storage, distance calculations, similarity search | | [Full-text search](/docs/sql-reference/functions/fts) | Tantivy-powered FTS with BM25 scoring | | [CDC](/docs/sql-reference/pragmas#change-data-capture) | Change Data Capture via PRAGMA | | [Encryption](/docs/sql-reference/pragmas#encryption) | At-rest database encryption | | [Custom index methods](/docs/sql-reference/statements/create-index#using-method) | CREATE INDEX ... USING for FTS and custom access methods | | stddev() | Standard deviation aggregate function | ## See Also - [Experimental Features](/docs/sql-reference/experimental-features) for how to enable experimental features - [COMPAT.md](https://github.com/tursodatabase/turso/blob/main/COMPAT.md) for the full compatibility matrix ================================================ FILE: docs/sql-reference/data-types.mdx ================================================ --- title: Data Types description: Storage classes, type affinity, STRICT tables, and custom types in Turso sidebarTitle: Data Types --- # Data Types Turso uses the same dynamic type system as SQLite, where values have types but columns do not enforce a single type (unless using STRICT tables). Every value stored in Turso belongs to one of five storage classes. ## Storage Classes | Storage Class | Description | |---------------|-------------| | NULL | The NULL value | | INTEGER | A signed integer, stored in 1, 2, 3, 4, 6, or 8 bytes depending on magnitude | | REAL | An 8-byte IEEE 754 floating-point number | | TEXT | A UTF-8 encoded string | | BLOB | Raw binary data, stored exactly as input | ```sql SELECT typeof(NULL); -- 'null' SELECT typeof(42); -- 'integer' SELECT typeof(3.14); -- 'real' SELECT typeof('hello'); -- 'text' SELECT typeof(x'CAFE'); -- 'blob' ``` ## Type Affinity When a column is declared with a type name, Turso assigns a **type affinity** to that column. Type affinity is a recommendation for how to store values, not a strict constraint (unless using STRICT tables). Turso uses the same affinity rules as SQLite. ### Affinity Determination Rules The type affinity of a column is determined by the declared type name, using these rules applied in order: | Rule | Condition | Affinity | Examples | |------|-----------|----------|----------| | 1 | Type name contains "INT" | INTEGER | `INT`, `INTEGER`, `BIGINT`, `SMALLINT`, `TINYINT` | | 2 | Type name contains "CHAR", "CLOB", or "TEXT" | TEXT | `TEXT`, `VARCHAR(255)`, `CLOB`, `CHARACTER(20)` | | 3 | Type name contains "BLOB" or no type specified | BLOB | `BLOB`, (no type) | | 4 | Type name contains "REAL", "FLOA", or "DOUB" | REAL | `REAL`, `FLOAT`, `DOUBLE`, `DOUBLE PRECISION` | | 5 | Otherwise | NUMERIC | `NUMERIC`, `DECIMAL`, `BOOLEAN`, `DATE` | ```sql -- Type affinity is a suggestion, not a constraint CREATE TABLE flexible ( id INTEGER, name TEXT, data BLOB ); -- This works - TEXT value in an INTEGER column INSERT INTO flexible VALUES ('not a number', 42, 'text in blob'); SELECT typeof(id), typeof(name), typeof(data) FROM flexible; -- 'text', 'integer', 'text' ``` ### Type Conversions When a value is inserted into a column, Turso attempts to convert the value to the column's affinity: - **INTEGER affinity**: If the value is TEXT or REAL that looks like an integer, Turso converts the value to INTEGER - **REAL affinity**: If the value is TEXT that looks like a number, Turso converts to REAL. If the value is an integer, Turso converts to REAL - **NUMERIC affinity**: Turso tries INTEGER first, then REAL, then keeps as TEXT - **TEXT affinity**: Integer and REAL values are converted to their text representation - **BLOB affinity**: No conversion is attempted ## STRICT Tables STRICT tables enforce type checking at the storage layer. Every value inserted into a STRICT table must match the declared column type or be convertible to the column type. ```sql CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER, score REAL ) STRICT; -- This works - values match declared types INSERT INTO users VALUES (1, 'Alice', 30, 95.5); -- This fails - 'thirty' cannot be converted to INTEGER INSERT INTO users VALUES (2, 'Bob', 'thirty', 80.0); -- Error: cannot store TEXT value in INTEGER column ``` ### Allowed Types in STRICT Tables STRICT tables only allow these base type names: | Type | Description | |------|-------------| | INTEGER | Signed integer | | REAL | Floating-point number | | TEXT | UTF-8 string | | BLOB | Raw binary data | | ANY | Any storage class (disables type checking for this column) | **Turso Extension**: STRICT tables also accept [custom type](/docs/sql-reference/statements/create-type) names. Custom types extend the type system with user-defined encoding, decoding, validation, and operator overloading. ## Custom Types **Turso Extension**: Custom types are a Turso-specific feature not available in SQLite. Custom types work only with STRICT tables. This feature is experimental and must be [enabled before use](/docs/sql-reference/experimental-features). Custom types let you define how values are encoded before storage and decoded when read, enforce domain constraints at the storage layer, attach operators, and provide defaults. Custom types are declared with the `CREATE TYPE` statement. ```sql -- A type that stores monetary values as cents CREATE TYPE cents BASE integer ENCODE value * 100 DECODE value / 100; CREATE TABLE prices ( id INTEGER PRIMARY KEY, amount cents ) STRICT; INSERT INTO prices VALUES (1, 42); SELECT amount FROM prices; -- 42 (stored on disk as 4200) ``` ### Built-in Custom Types Turso provides several built-in custom types available in STRICT tables: | Type | Base | Description | |------|------|-------------| | `date` | TEXT | ISO 8601 date (YYYY-MM-DD) | | `time` | TEXT | ISO 8601 time (HH:MM:SS) | | `timestamp` | TEXT | ISO 8601 datetime | | `varchar(N)` | TEXT | Text with maximum length constraint | | `numeric(P,S)` | BLOB | Fixed-point decimal with precision and scale | | `smallint` | INTEGER | Integer constrained to -32768..32767 | | `boolean` | INTEGER | Integer constrained to 0 or 1 | | `uuid` | BLOB | UUID stored as 16-byte blob, displayed as string | | `bytea` | BLOB | Binary data (PostgreSQL-compatible alias) | | `inet` | TEXT | IP address | | `json` | TEXT | Validated JSON text | | `jsonb` | BLOB | JSON in binary format | ```sql CREATE TABLE events ( id uuid PRIMARY KEY, name varchar(100), event_date date, is_active boolean DEFAULT 1, metadata json ) STRICT; INSERT INTO events VALUES ( uuid4(), 'Product Launch', '2025-03-15', 1, '{"venue": "online"}' ); ``` For the full custom types reference including ENCODE/DECODE, operators, parametric types, and validation, see [CREATE TYPE](/docs/sql-reference/statements/create-type). ## Array Types **Turso Extension**: Array types are a Turso-specific feature not available in SQLite. Array columns work only with STRICT tables. Array columns store ordered collections of values. Declare them by appending `[]` to any base type name: ```sql CREATE TABLE sensors ( id INTEGER PRIMARY KEY, readings REAL[], labels TEXT[], flags INTEGER[] ) STRICT; ``` Multi-dimensional arrays are supported with multiple bracket pairs: ```sql CREATE TABLE matrices ( id INTEGER PRIMARY KEY, data INTEGER[][] ) STRICT; ``` Arrays can be inserted using the `ARRAY[...]` constructor or as JSON text: ```sql INSERT INTO sensors VALUES (1, ARRAY[1.5, 2.5, 3.5], '["a","b"]', '[0, 1, 1]'); ``` Arrays are displayed as JSON arrays on output. Element types are validated against the declared base type — for example, an `INTEGER[]` column rejects non-numeric text elements. For the full set of array functions, operators, and subscript syntax, see [Array Functions](/docs/sql-reference/functions/array). ### Inspecting Types List all available types (built-in and custom): ```sql PRAGMA list_types; ``` All types are also available through the `sqlite_turso_types` virtual table: ```sql SELECT name, sql FROM sqlite_turso_types; ``` ## Comparison and Sorting Turso uses the same comparison rules as SQLite. Values of different storage classes are ordered as: ``` NULL < INTEGER/REAL < TEXT < BLOB ``` - NULL values are considered less than any other value - INTEGER and REAL values are compared numerically - TEXT values are compared using the column's collation sequence (default: BINARY) - BLOB values are compared using `memcmp()` ```sql SELECT 1 < 2; -- 1 (true) SELECT 'abc' < 'abd'; -- 1 (true) SELECT 1 < '2'; -- 1 (true, numeric < text) SELECT NULL < 1; -- NULL (any comparison with NULL yields NULL) SELECT NULL IS NULL; -- 1 (use IS to test for NULL) ``` ## See Also - [CREATE TABLE](/docs/sql-reference/statements/create-table) for column definitions and constraints - [CREATE TYPE](/docs/sql-reference/statements/create-type) for custom type definitions - [Array Functions](/docs/sql-reference/functions/array) for array construction, manipulation, and operators - [Expressions](/docs/sql-reference/expressions) for CAST expressions and type conversions - [Compatibility](/docs/sql-reference/compatibility) for differences from SQLite ================================================ FILE: docs/sql-reference/experimental-features.mdx ================================================ [File too large to display: 3.5 KB] ================================================ FILE: docs/sql-reference/expressions.mdx ================================================ --- title: Expressions description: SQL expression syntax including literals, operators, CAST, CASE, and subqueries sidebarTitle: Expressions --- # Expressions Expressions are combinations of values, operators, and functions that Turso evaluates to produce a result. Expressions appear in many SQL clauses including SELECT columns, WHERE conditions, ORDER BY, GROUP BY, HAVING, CHECK constraints, and DEFAULT values. ## Literals ### Numeric Literals ```sql SELECT 42; -- integer literal SELECT -17; -- negative integer SELECT 3.14; -- real (floating-point) literal SELECT 2.5e10; -- scientific notation SELECT 0xFF; -- hexadecimal integer (255) ``` ### String Literals String literals are enclosed in single quotes. To include a single quote within a string, use two consecutive single quotes: ```sql SELECT 'hello world'; -- text literal SELECT 'it''s a test'; -- embedded single quote SELECT ''; -- empty string ``` ### Blob Literals Blob literals are hexadecimal strings preceded by `x` or `X`: ```sql SELECT x'CAFEBABE'; -- blob literal SELECT X'48454C4C4F'; -- blob literal (case insensitive prefix) ``` ### NULL Literal ```sql SELECT NULL; -- null value ``` ### Boolean Literals Turso does not have a separate boolean type. Use integers `0` (false) and `1` (true): ```sql SELECT 1; -- true SELECT 0; -- false ``` ## Operators ### Arithmetic Operators | Operator | Description | Example | Result | |----------|-------------|---------|--------| | `+` | Addition | `3 + 4` | `7` | | `-` | Subtraction | `10 - 3` | `7` | | `*` | Multiplication | `3 * 4` | `12` | | `/` | Division | `10 / 3` | `3` (integer division) | | `-` (unary) | Negation | `-5` | `-5` | | `+` (unary) | No-op | `+5` | `5` | Integer division truncates toward zero. Use `CAST` or multiply by `1.0` for floating-point division: ```sql SELECT 10 / 3; -- 3 (integer division) SELECT 10 / 3.0; -- 3.333... (real division) SELECT CAST(10 AS REAL) / 3; -- 3.333... ``` ### Comparison Operators | Operator | Description | Example | |----------|-------------|---------| | `=` or `==` | Equal | `x = 5` | | `!=` or `<>` | Not equal | `x != 5` | | `<` | Less than | `x < 5` | | `>` | Greater than | `x > 5` | | `<=` | Less than or equal | `x <= 5` | | `>=` | Greater than or equal | `x >= 5` | All comparison operators return `1` (true), `0` (false), or `NULL` (if either operand is NULL). ### Logical Operators | Operator | Description | Example | |----------|-------------|---------| | `AND` | Logical AND | `x > 0 AND x < 10` | | `OR` | Logical OR | `x = 1 OR x = 2` | | `NOT` | Logical NOT | `NOT x = 5` | ### Bitwise Operators | Operator | Description | Example | Result | |----------|-------------|---------|--------| | `&` | Bitwise AND | `5 & 3` | `1` | | `\|` | Bitwise OR | `5 \| 3` | `7` | | `~` | Bitwise NOT | `~5` | `-6` | | `<<` | Left shift | `1 << 4` | `16` | | `>>` | Right shift | `16 >> 2` | `4` | ### String Concatenation | Operator | Description | Example | Result | |----------|-------------|---------|--------| | `\|\|` | Concatenation | `'hello' \|\| ' ' \|\| 'world'` | `'hello world'` | When either operand is an array, `||` performs array concatenation instead. See [Array Operators](#array-operators). ### Array Operators **Turso Extension**: Array operators are a Turso-specific feature not available in SQLite. | Operator | Description | Example | Result | |----------|-------------|---------|--------| | `@>` | Contains all | `ARRAY[1,2,3] @> ARRAY[1,2]` | `1` | | `&&` | Overlaps | `ARRAY[1,2,3] && ARRAY[3,4,5]` | `1` | | `\|\|` | Array concat | `ARRAY[1,2] \|\| ARRAY[3,4]` | `[1,2,3,4]` | | `=`, `!=`, `<`, `>`, `<=`, `>=` | Comparison | `ARRAY[1,2] < ARRAY[1,3]` | `1` | The `@>` operator tests if the left array contains all elements of the right array. The `&&` operator tests if two arrays share any common elements. Both return `1` (true) or `0` (false). Array comparison is element-wise from left to right. If all compared elements are equal, the shorter array is considered less than the longer one. ```sql SELECT ARRAY[1,2,3] @> ARRAY[1,3]; -- 1 (contains both 1 and 3) SELECT ARRAY[1,2] && ARRAY[3,4]; -- 0 (no common elements) SELECT ARRAY[1,2] < ARRAY[1,2,3]; -- 1 (shorter is less) ``` For the full reference, see [Array Functions](/docs/sql-reference/functions/array). ## CAST Expression The CAST expression converts a value to a specified type. ```sql CAST(expression AS type-name) ``` | Parameter | Type | Description | |-----------|------|-------------| | expression | any | The value to convert | | type-name | type | The target type name | ```sql SELECT CAST(3.7 AS INTEGER); -- 3 (truncates toward zero) SELECT CAST(42 AS TEXT); -- '42' SELECT CAST('123' AS INTEGER); -- 123 SELECT CAST('abc' AS INTEGER); -- 0 SELECT CAST(NULL AS INTEGER); -- NULL ``` **Turso Extension**: In STRICT tables with custom types, `CAST(value AS custom_type)` applies the custom type's ENCODE function. See [CREATE TYPE](/docs/sql-reference/statements/create-type) for details. ## COLLATE Expression The COLLATE expression specifies a collation sequence for string comparison. ```sql expression COLLATE collation-name ``` Built-in collation sequences: | Collation | Description | |-----------|-------------| | `BINARY` | Byte-by-byte comparison (default) | | `NOCASE` | Case-insensitive comparison for ASCII characters | | `RTRIM` | Like BINARY but ignores trailing spaces | ```sql SELECT 'ABC' = 'abc'; -- 0 (BINARY comparison) SELECT 'ABC' = 'abc' COLLATE NOCASE; -- 1 (case-insensitive) SELECT 'abc ' = 'abc' COLLATE RTRIM; -- 1 (trailing space ignored) SELECT name FROM users ORDER BY name COLLATE NOCASE; ``` ## Pattern Matching ### LIKE Operator The LIKE operator performs case-insensitive pattern matching (for ASCII characters). The `%` wildcard matches any sequence of characters, and `_` matches any single character. ```sql expression [NOT] LIKE pattern [ESCAPE escape-char] ``` ```sql SELECT 'Hello World' LIKE 'hello%'; -- 1 (case-insensitive) SELECT 'Hello World' LIKE 'H_llo%'; -- 1 (_ matches 'e') SELECT 'Hello World' LIKE '%World'; -- 1 SELECT '10%' LIKE '10\%' ESCAPE '\'; -- 1 (escaped % literal) ``` ### GLOB Operator The GLOB operator performs case-sensitive pattern matching using Unix-style wildcards. `*` matches any sequence of characters, and `?` matches any single character. ```sql expression [NOT] GLOB pattern ``` ```sql SELECT 'Hello' GLOB 'H*'; -- 1 SELECT 'Hello' GLOB 'h*'; -- 0 (case-sensitive) SELECT 'Hello' GLOB 'H?llo'; -- 1 SELECT 'Hello' GLOB 'H[a-z]*'; -- 1 (character class) ``` ### REGEXP Operator The REGEXP operator performs regular expression matching. Requires the regexp extension (loaded by default). ```sql expression [NOT] REGEXP pattern ``` ```sql SELECT 'Hello123' REGEXP '[A-Za-z]+[0-9]+'; -- 1 SELECT 'test@email.com' REGEXP '^[^@]+@[^@]+\.[^@]+$'; -- 1 ``` ## BETWEEN Expression The BETWEEN expression tests whether a value falls within an inclusive range. ```sql expression [NOT] BETWEEN low AND high ``` The BETWEEN expression is equivalent to `expression >= low AND expression <= high`: ```sql SELECT 5 BETWEEN 1 AND 10; -- 1 SELECT 5 NOT BETWEEN 1 AND 3; -- 1 SELECT 'b' BETWEEN 'a' AND 'c'; -- 1 ``` ## IN Expression The IN expression tests whether a value matches any value in a list or subquery result. ```sql expression [NOT] IN (value1, value2, ...) expression [NOT] IN (select-statement) ``` ```sql SELECT 3 IN (1, 2, 3, 4, 5); -- 1 SELECT 'red' NOT IN ('blue', 'green'); -- 1 SELECT name FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 100); ``` ## EXISTS Expression The EXISTS expression returns `1` if the subquery returns at least one row, and `0` otherwise. ```sql [NOT] EXISTS (select-statement) ``` ```sql SELECT EXISTS (SELECT 1 FROM users WHERE name = 'Alice'); -- 1 if Alice exists SELECT name FROM departments d WHERE EXISTS ( SELECT 1 FROM employees e WHERE e.department_id = d.id ); ``` ## IS NULL / IS NOT NULL The IS NULL expression tests whether a value is NULL. Unlike `= NULL`, which always returns NULL, `IS NULL` returns `1` or `0`. ```sql expression IS NULL expression IS NOT NULL ``` ```sql SELECT NULL IS NULL; -- 1 SELECT NULL = NULL; -- NULL (not 1!) SELECT 42 IS NOT NULL; -- 1 SELECT NULL IS NOT NULL; -- 0 ``` ## IS DISTINCT FROM The IS DISTINCT FROM expression compares two values, treating NULL as a comparable value. ```sql expression IS [NOT] DISTINCT FROM expression ``` ```sql SELECT 1 IS DISTINCT FROM 2; -- 1 (different values) SELECT 1 IS DISTINCT FROM 1; -- 0 (same value) SELECT NULL IS DISTINCT FROM NULL; -- 0 (both NULL) SELECT NULL IS DISTINCT FROM 1; -- 1 (NULL vs non-NULL) SELECT 1 IS NOT DISTINCT FROM 1; -- 1 ``` ## CASE Expression The CASE expression provides conditional logic within SQL expressions. ### Simple CASE ```sql CASE expression WHEN value1 THEN result1 WHEN value2 THEN result2 ... [ELSE default_result] END ``` ### Searched CASE ```sql CASE WHEN condition1 THEN result1 WHEN condition2 THEN result2 ... [ELSE default_result] END ``` If no WHEN clause matches and there is no ELSE clause, the CASE expression returns NULL. ```sql -- Simple CASE SELECT name, CASE status WHEN 'A' THEN 'Active' WHEN 'I' THEN 'Inactive' ELSE 'Unknown' END AS status_text FROM users; -- Searched CASE SELECT name, score, CASE WHEN score >= 90 THEN 'A' WHEN score >= 80 THEN 'B' WHEN score >= 70 THEN 'C' ELSE 'F' END AS grade FROM students; ``` ## Scalar Subqueries A subquery enclosed in parentheses that returns a single value can be used as an expression. ```sql (select-statement) ``` The subquery must return exactly one column and at most one row. If the subquery returns no rows, the expression evaluates to NULL. ```sql SELECT name, (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) AS order_count FROM users; SELECT * FROM products WHERE price > (SELECT AVG(price) FROM products); ``` ## RAISE Function The RAISE function raises an error condition. In standard SQLite, RAISE can only be used inside triggers. In Turso, `RAISE(ABORT, msg)` can also be used outside triggers — in CHECK constraints, custom type ENCODE expressions, and standalone queries. The other forms (`RAISE(IGNORE)`, `RAISE(ROLLBACK, msg)`, `RAISE(FAIL, msg)`) are only valid inside triggers. ```sql RAISE(IGNORE) RAISE(ROLLBACK, error-message) RAISE(ABORT, error-message) RAISE(FAIL, error-message) ``` | Form | Description | |------|-------------| | `RAISE(IGNORE)` | Abandon the current trigger action but continue the statement | | `RAISE(ROLLBACK, msg)` | Abort the statement and roll back the current transaction | | `RAISE(ABORT, msg)` | Abort the current statement; prior changes in the transaction are preserved | | `RAISE(FAIL, msg)` | Abort the current statement at the current point; prior row changes are preserved | ```sql -- In a trigger CREATE TRIGGER validate_age BEFORE INSERT ON users BEGIN SELECT CASE WHEN NEW.age < 0 THEN RAISE(ABORT, 'age must be non-negative') END; END; -- In a custom type ENCODE expression CREATE TYPE positive_int BASE integer ENCODE CASE WHEN value > 0 THEN value ELSE RAISE(ABORT, 'value must be positive') END DECODE value; ``` ## Operator Precedence Operators are evaluated in the following order (highest precedence first): | Precedence | Operators | |------------|-----------| | 1 (highest) | `~` (unary NOT), `+` (unary), `-` (unary) | | 2 | `\|\|` (concatenation) | | 3 | `*`, `/` | | 4 | `+`, `-` | | 5 | `<<`, `>>`, `&`, `\|` | | 6 | `<`, `<=`, `>`, `>=`, `@>`, `&&` | | 7 | `=`, `==`, `!=`, `<>`, `IS`, `IS NOT`, `IS DISTINCT FROM`, `IN`, `LIKE`, `GLOB`, `REGEXP`, `BETWEEN` | | 8 | `NOT` | | 9 | `AND` | | 10 (lowest) | `OR` | Use parentheses to override precedence when needed: ```sql SELECT 2 + 3 * 4; -- 14 (multiplication first) SELECT (2 + 3) * 4; -- 20 (parentheses override) ``` ## See Also - [Data Types](/docs/sql-reference/data-types) for storage classes and type affinity - [Scalar Functions](/docs/sql-reference/functions/scalar) for built-in functions usable in expressions - [SELECT](/docs/sql-reference/statements/select) for using expressions in queries ================================================ FILE: docs/sql-reference/extensions.mdx ================================================ [File too large to display: 10.3 KB] ================================================ FILE: docs/sql-reference/functions/aggregate.mdx ================================================ [File too large to display: 13.9 KB] ================================================ FILE: docs/sql-reference/functions/array.mdx ================================================ [File too large to display: 14.5 KB] ================================================ FILE: docs/sql-reference/functions/date-time.mdx ================================================ --- title: Date and Time Functions description: Functions for creating, formatting, and manipulating dates, times, and timestamps sidebarTitle: Date & Time --- # Date and Time Functions Turso provides a complete set of date and time functions compatible with SQLite. These functions accept time values in various formats, apply optional modifiers, and return results as TEXT, REAL, or INTEGER depending on the function. All date and time functions use the [proleptic Gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar) and assume UTC unless the `localtime` modifier is applied. ## Functions ### date() Returns the date as TEXT in `YYYY-MM-DD` format. ```sql date(time-value, modifier, modifier, ...) ``` | Parameter | Type | Description | |-----------|------|-------------| | `time-value` | TEXT, REAL, or INTEGER | A time value in any [supported format](#time-value-formats) | | `modifier` | TEXT | Zero or more [modifiers](#modifiers) applied left to right | **Returns:** TEXT in `YYYY-MM-DD` format, or NULL if any argument is invalid. ```sql SELECT date('now'); -- '2025-06-15' SELECT date('2025-06-15 14:30:00'); -- '2025-06-15' SELECT date('2025-06-15', '+1 month'); -- '2025-07-15' SELECT date('2025-06-15', 'start of year'); -- '2025-01-01' ``` --- ### time() Returns the time as TEXT in `HH:MM:SS` format. ```sql time(time-value, modifier, modifier, ...) ``` | Parameter | Type | Description | |-----------|------|-------------| | `time-value` | TEXT, REAL, or INTEGER | A time value in any [supported format](#time-value-formats) | | `modifier` | TEXT | Zero or more [modifiers](#modifiers) applied left to right | **Returns:** TEXT in `HH:MM:SS` format, or NULL if any argument is invalid. ```sql SELECT time('now'); -- '14:30:00' SELECT time('2025-06-15 14:30:00'); -- '14:30:00' SELECT time('14:30:00', '+2 hours'); -- '16:30:00' SELECT time('14:30:00', '+90 minutes'); -- '16:00:00' ``` --- ### datetime() Returns the date and time as TEXT in `YYYY-MM-DD HH:MM:SS` format. ```sql datetime(time-value, modifier, modifier, ...) ``` | Parameter | Type | Description | |-----------|------|-------------| | `time-value` | TEXT, REAL, or INTEGER | A time value in any [supported format](#time-value-formats) | | `modifier` | TEXT | Zero or more [modifiers](#modifiers) applied left to right | **Returns:** TEXT in `YYYY-MM-DD HH:MM:SS` format, or NULL if any argument is invalid. ```sql SELECT datetime('now'); -- '2025-06-15 14:30:00' SELECT datetime('2025-06-15', '+1 day', '+6 hours'); -- '2025-06-16 06:00:00' SELECT datetime(1718458200, 'unixepoch'); -- '2024-06-15 14:30:00' ``` --- ### julianday() Returns the [Julian day number](https://en.wikipedia.org/wiki/Julian_day) as a REAL. The Julian day number is the number of days since noon on November 24, 4714 B.C. (proleptic Gregorian calendar). ```sql julianday(time-value, modifier, modifier, ...) ``` | Parameter | Type | Description | |-----------|------|-------------| | `time-value` | TEXT, REAL, or INTEGER | A time value in any [supported format](#time-value-formats) | | `modifier` | TEXT | Zero or more [modifiers](#modifiers) applied left to right | **Returns:** REAL representing the Julian day number, or NULL if any argument is invalid. ```sql SELECT julianday('2025-06-15'); -- 2460811.5 SELECT julianday('now'); -- 2460811.104166... -- Compute the number of days between two dates SELECT julianday('2025-12-25') - julianday('2025-06-15'); -- 193.0 ``` --- ### unixepoch() Returns the Unix timestamp as an INTEGER. The Unix timestamp is the number of seconds since `1970-01-01 00:00:00 UTC`. ```sql unixepoch(time-value, modifier, modifier, ...) ``` | Parameter | Type | Description | |-----------|------|-------------| | `time-value` | TEXT, REAL, or INTEGER | A time value in any [supported format](#time-value-formats) | | `modifier` | TEXT | Zero or more [modifiers](#modifiers) applied left to right | **Returns:** INTEGER representing seconds since the Unix epoch, or NULL if any argument is invalid. If the `subsec` modifier is used, returns REAL with fractional seconds. ```sql SELECT unixepoch('now'); -- 1718458200 SELECT unixepoch('2025-06-15 14:30:00'); -- 1750001400 SELECT unixepoch('2025-06-15 14:30:00.123', 'subsec'); -- 1750001400.123 ``` --- ### strftime() Returns a formatted date/time string according to the specified format string. ```sql strftime(format, time-value, modifier, modifier, ...) ``` | Parameter | Type | Description | |-----------|------|-------------| | `format` | TEXT | A format string containing [substitution codes](#strftime-format-codes) | | `time-value` | TEXT, REAL, or INTEGER | A time value in any [supported format](#time-value-formats) | | `modifier` | TEXT | Zero or more [modifiers](#modifiers) applied left to right | **Returns:** TEXT with format codes replaced by date/time components, or NULL if any argument is invalid. The other date/time functions can be expressed as `strftime` calls: | Function | Equivalent strftime | |----------|---------------------| | `date(...)` | `strftime('%Y-%m-%d', ...)` | | `time(...)` | `strftime('%H:%M:%S', ...)` | | `datetime(...)` | `strftime('%Y-%m-%d %H:%M:%S', ...)` | | `julianday(...)` | `strftime('%J', ...)` (as TEXT) | | `unixepoch(...)` | `strftime('%s', ...)` (as TEXT) | ```sql SELECT strftime('%Y', 'now'); -- '2025' SELECT strftime('%m/%d/%Y', '2025-06-15'); -- '06/15/2025' SELECT strftime('%H:%M', '2025-06-15 14:30:00'); -- '14:30' SELECT strftime('%Y-%m-%d %H:%M:%f', '2025-06-15 14:30:00.123'); -- '2025-06-15 14:30:00.123' SELECT strftime('%s', '2025-06-15 14:30:00'); -- '1750001400' SELECT strftime('%W', '2025-06-15'); -- '23' (week of year) ``` #### strftime Format Codes | Code | Description | Example | |------|-------------|---------| | `%d` | Day of month (01-31) | `15` | | `%e` | Day of month with leading space ( 1-31) | `15` | | `%f` | Fractional seconds (SS.SSS) | `00.123` | | `%F` | ISO 8601 date (YYYY-MM-DD) | `2025-06-15` | | `%H` | Hour (00-23) | `14` | | `%I` | Hour (01-12) | `02` | | `%j` | Day of year (001-366) | `166` | | `%J` | Julian day number | `2460811.5` | | `%k` | Hour with leading space ( 0-23) | `14` | | `%l` | Hour with leading space ( 1-12) | ` 2` | | `%m` | Month (01-12) | `06` | | `%M` | Minute (00-59) | `30` | | `%p` | AM or PM | `PM` | | `%P` | am or pm | `pm` | | `%R` | Time as HH:MM | `14:30` | | `%s` | Seconds since Unix epoch | `1750001400` | | `%S` | Seconds (00-59) | `00` | | `%T` | Time as HH:MM:SS | `14:30:00` | | `%u` | Day of week (1=Monday, 7=Sunday) | `7` | | `%w` | Day of week (0=Sunday, 6=Saturday) | `0` | | `%W` | Week of year (00-53) | `23` | | `%Y` | Year (0000-9999) | `2025` | | `%%` | Literal `%` character | `%` | --- ### timediff() Returns the difference between two time values as a TEXT string in the format `(+|-)YYYY-MM-DD HH:MM:SS.SSS`. The result represents the time that must be added to `time2` to produce `time1`. ```sql timediff(time1, time2) ``` | Parameter | Type | Description | |-----------|------|-------------| | `time1` | TEXT, REAL, or INTEGER | The later time value | | `time2` | TEXT, REAL, or INTEGER | The earlier time value | **Returns:** TEXT in `(+|-)YYYY-MM-DD HH:MM:SS.SSS` format, or NULL if either argument is invalid. ```sql SELECT timediff('2025-06-15', '2025-06-10'); -- '+0000-00-05 00:00:00.000' SELECT timediff('2025-01-01', '2024-01-01'); -- '+0001-00-00 00:00:00.000' SELECT timediff('2025-06-15 14:30:00', '2025-06-15 10:00:00'); -- '+0000-00-00 04:30:00.000' SELECT timediff('2024-01-01', '2025-06-15'); -- '-0001-05-14 00:00:00.000' ``` ## Time Value Formats All date and time functions accept time values in the following formats. | Format | Example | Description | |--------|---------|-------------| | `YYYY-MM-DD` | `'2025-06-15'` | Date only (time defaults to 00:00:00) | | `YYYY-MM-DD HH:MM` | `'2025-06-15 14:30'` | Date and time (seconds default to 00) | | `YYYY-MM-DD HH:MM:SS` | `'2025-06-15 14:30:00'` | Date and time with seconds | | `YYYY-MM-DD HH:MM:SS.SSS` | `'2025-06-15 14:30:00.123'` | Date and time with fractional seconds | | `YYYY-MM-DDTHH:MM` | `'2025-06-15T14:30'` | ISO 8601 with `T` separator | | `YYYY-MM-DDTHH:MM:SS` | `'2025-06-15T14:30:00'` | ISO 8601 with `T` separator | | `YYYY-MM-DDTHH:MM:SS.SSS` | `'2025-06-15T14:30:00.123'` | ISO 8601 with `T` separator | | `HH:MM` | `'14:30'` | Time only (date defaults to 2000-01-01) | | `HH:MM:SS` | `'14:30:00'` | Time only with seconds | | `HH:MM:SS.SSS` | `'14:30:00.123'` | Time only with fractional seconds | | `now` | `'now'` | Current date and time in UTC | | Julian day number | `2460811.5` | REAL number representing a Julian day | | Unix timestamp | `1718458200` | INTEGER, requires `'unixepoch'` modifier | When passing a Unix timestamp as the time value, you must include the `'unixepoch'` modifier so the function knows to interpret the number as seconds since 1970-01-01, not as a Julian day number. ```sql -- Unix timestamp requires the 'unixepoch' modifier SELECT datetime(1718458200, 'unixepoch'); -- '2024-06-15 14:30:00' -- Without 'unixepoch', the number is treated as a Julian day SELECT datetime(1718458200); -- NULL (or a very distant date) -- 'now' returns the current UTC time SELECT datetime('now'); -- '2025-06-15 14:30:00' ``` ## Modifiers Modifiers transform the time value. Multiple modifiers are applied left to right. If any modifier is invalid, the function returns NULL. ### Offset Modifiers Offset modifiers add or subtract a specified amount from the time value. | Modifier | Description | Example | |----------|-------------|---------| | `NNN days` | Add NNN days | `'+7 days'`, `'-1 days'` | | `NNN hours` | Add NNN hours | `'+6 hours'` | | `NNN minutes` | Add NNN minutes | `'+30 minutes'` | | `NNN seconds` | Add NNN seconds | `'+90 seconds'` | | `NNN.NNNN seconds` | Add fractional seconds | `'+0.5 seconds'` | | `NNN months` | Add NNN months | `'+1 months'` | | `NNN years` | Add NNN years | `'+1 years'` | NNN can be a positive or negative integer (or real number for seconds). The `+` sign is optional for positive values. ```sql SELECT date('2025-06-15', '+7 days'); -- '2025-06-22' SELECT datetime('2025-06-15 14:30:00', '-6 hours'); -- '2025-06-15 08:30:00' SELECT date('2025-01-31', '+1 months'); -- '2025-03-03' (Jan 31 + 1 month = Mar 3) ``` ### Time Offset Modifier A time offset in the format `+HH:MM` or `-HH:MM` adds or subtracts the specified hours and minutes. ```sql SELECT time('14:30:00', '+05:30'); -- '20:00:00' SELECT datetime('2025-06-15 14:30:00', '-08:00'); -- '2025-06-15 06:30:00' ``` ### Start-of Modifiers These modifiers reset the time value to the start of a period. | Modifier | Description | |----------|-------------| | `start of day` | Sets time to 00:00:00, keeps date | | `start of month` | Sets to first day of the month, time to 00:00:00 | | `start of year` | Sets to January 1 of the year, time to 00:00:00 | ```sql SELECT datetime('2025-06-15 14:30:00', 'start of day'); -- '2025-06-15 00:00:00' SELECT date('2025-06-15', 'start of month'); -- '2025-06-01' SELECT date('2025-06-15', 'start of year'); -- '2025-01-01' ``` ### Weekday Modifier The `weekday N` modifier advances the date to the next occurrence of the specified weekday, where 0 = Sunday, 1 = Monday, ..., 6 = Saturday. If the current date already falls on that weekday, it is unchanged. ```sql -- Next Sunday (weekday 0) SELECT date('2025-06-15', 'weekday 0'); -- '2025-06-15' (June 15, 2025 is a Sunday) -- Next Monday SELECT date('2025-06-15', 'weekday 1'); -- '2025-06-16' -- Next Friday SELECT date('2025-06-15', 'weekday 5'); -- '2025-06-20' ``` ### Interpretation Modifiers | Modifier | Description | |----------|-------------| | `unixepoch` | Interpret the time value as a Unix timestamp (seconds since 1970-01-01) | | `julianday` | Interpret the time value as a Julian day number | | `auto` | Automatically detect whether the value is a Unix timestamp or Julian day | ```sql SELECT datetime(1718458200, 'unixepoch'); -- '2024-06-15 14:30:00' SELECT datetime(2460811.5, 'julianday'); -- '2025-06-15 00:00:00' SELECT datetime(0, 'unixepoch'); -- '1970-01-01 00:00:00' ``` ### Timezone Modifiers | Modifier | Description | |----------|-------------| | `localtime` | Convert from UTC to local time | | `utc` | Convert from local time to UTC | ```sql SELECT datetime('2025-06-15 14:30:00', 'localtime'); -- '2025-06-15 07:30:00' (example: UTC-7) SELECT time('now', 'localtime'); -- Local current time ``` ### Rounding Modifiers | Modifier | Description | |----------|-------------| | `ceiling` | When adding months, if the day overflows, advance to the first day of the next month | | `floor` | When adding months, if the day overflows, use the last day of the target month | These modifiers affect how month arithmetic handles months with different numbers of days. ```sql -- Without floor/ceiling, month overflow rolls forward SELECT date('2025-01-31', '+1 months'); -- '2025-03-03' -- With floor, clamp to last day of target month SELECT date('2025-01-31', 'floor', '+1 months'); -- '2025-02-28' -- With ceiling, advance to first of next month SELECT date('2025-01-31', 'ceiling', '+1 months'); -- '2025-03-01' ``` ### Subsec Modifier The `subsec` modifier causes `unixepoch()` to return a REAL with fractional seconds instead of truncating to INTEGER. ```sql SELECT unixepoch('2025-06-15 14:30:00.456', 'subsec'); -- 1750001400.456 ``` ## Practical Examples ### Get the Current Date and Time ```sql SELECT date('now'); -- Current UTC date SELECT time('now'); -- Current UTC time SELECT datetime('now'); -- Current UTC date and time SELECT datetime('now', 'localtime'); -- Current local date and time ``` ### Date Arithmetic ```sql -- Tomorrow SELECT date('now', '+1 day'); -- 90 days from now SELECT date('now', '+90 days'); -- Last day of the current month SELECT date('now', 'start of month', '+1 month', '-1 day'); -- First Monday of the current month SELECT date('now', 'start of month', 'weekday 1'); ``` ### Age Calculation ```sql SELECT name, birth_date, timediff('now', birth_date) AS age_diff FROM users; ``` ### Convert Between Formats ```sql -- Unix timestamp to human-readable SELECT datetime(1718458200, 'unixepoch'); -- Human-readable to Unix timestamp SELECT unixepoch('2025-06-15 14:30:00'); -- Date to Julian day SELECT julianday('2025-06-15'); -- Julian day to date SELECT date(2460811.5); ``` ### Group Records by Time Period ```sql -- Count orders per month SELECT strftime('%Y-%m', order_date) AS month, COUNT(*) AS order_count FROM orders GROUP BY strftime('%Y-%m', order_date) ORDER BY month; -- Count events per day of week SELECT CASE strftime('%w', event_date) WHEN '0' THEN 'Sunday' WHEN '1' THEN 'Monday' WHEN '2' THEN 'Tuesday' WHEN '3' THEN 'Wednesday' WHEN '4' THEN 'Thursday' WHEN '5' THEN 'Friday' WHEN '6' THEN 'Saturday' END AS day_name, COUNT(*) AS event_count FROM events GROUP BY strftime('%w', event_date) ORDER BY strftime('%w', event_date); ``` ### Filter by Date Range ```sql -- Records from the last 30 days SELECT * FROM logs WHERE timestamp >= datetime('now', '-30 days'); -- Records from a specific month SELECT * FROM orders WHERE order_date >= '2025-06-01' AND order_date < '2025-07-01'; -- Records from this year SELECT * FROM sales WHERE date(sale_date) >= date('now', 'start of year'); ``` ### Compute Elapsed Time ```sql -- Days between two dates SELECT julianday('2025-12-25') - julianday('now') AS days_until_christmas; -- Seconds between two timestamps SELECT unixepoch('2025-06-15 18:00:00') - unixepoch('2025-06-15 14:30:00') AS seconds_elapsed; -- 12600 ``` ## See Also - [Expressions](/docs/sql-reference/expressions) for using functions in expressions - [Data Types](/docs/sql-reference/data-types) for TEXT, REAL, and INTEGER storage classes - [Scalar Functions](/docs/sql-reference/functions/scalar) for other built-in functions ================================================ FILE: docs/sql-reference/functions/fts.mdx ================================================ [File too large to display: 12.0 KB] ================================================ FILE: docs/sql-reference/functions/json.mdx ================================================ --- title: JSON Functions description: Create, extract, modify, and aggregate JSON data sidebarTitle: JSON --- # JSON Functions Turso provides a full set of JSON functions compatible with SQLite's JSON1 extension. These functions operate on JSON stored as TEXT or in Turso's internal binary JSON (JSONB) format. Most functions come in pairs: a `json_*` variant that returns TEXT and a `jsonb_*` variant that returns BLOB in the internal binary format. The JSONB variants are more efficient when the result will be stored or passed to another JSON function rather than returned to the application. ## JSON Path Syntax Many JSON functions accept a **path** argument that identifies a specific element within a JSON document. | Syntax | Meaning | |--------|---------| | `$` | The root element | | `$.key` | Object member named `key` | | `$[N]` | Array element at index `N` (zero-based) | | `$.key1.key2` | Nested object member | | `$.key[0]` | First element of an array inside an object member | | `$[0].key` | Object member inside the first array element | Path arguments must begin with `$`. If a path does not match any element, functions generally return `NULL`. ```sql SELECT json_extract('{"a": {"b": [10, 20, 30]}}', '$.a.b[1]'); -- 20 ``` --- ## JSON Creation and Validation ### json Validates a JSON string and returns it in minified form. If the input is not valid JSON, an error is raised. ```sql json(json_text) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT | A JSON string to validate and minify | **Returns:** TEXT -- the minified JSON string. ```sql SELECT json(' { "name": "Alice" , "age": 30 } '); -- {"name":"Alice","age":30} ``` ### jsonb Converts a JSON string to the internal binary JSON format. ```sql jsonb(json_text) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT | A JSON string to convert | **Returns:** BLOB -- the value in binary JSON format. ```sql SELECT typeof(jsonb('{"a":1}')); -- blob ``` ### json_array / jsonb_array Creates a JSON array from the arguments. ```sql json_array(value1, value2, ...) jsonb_array(value1, value2, ...) ``` | Parameter | Type | Description | |-----------|------|-------------| | `value1, value2, ...` | any | Values to include in the array. SQL NULL becomes JSON `null`. | **Returns:** TEXT (json_array) or BLOB (jsonb_array) -- a JSON array. ```sql SELECT json_array(1, 'hello', NULL, 3.14); -- [1,"hello",null,3.14] SELECT json_array(); -- [] ``` ### json_object / jsonb_object Creates a JSON object from alternating label/value pairs. When called with `*`, expands all columns of the row into label/value pairs using column names as keys. ```sql json_object(label1, value1, label2, value2, ...) jsonb_object(label1, value1, label2, value2, ...) json_object(*) ``` | Parameter | Type | Description | |-----------|------|-------------| | `label1, label2, ...` | TEXT | Keys for the JSON object. Must be strings. | | `value1, value2, ...` | any | Corresponding values. SQL NULL becomes JSON `null`. | **Returns:** TEXT (json_object) or BLOB (jsonb_object) -- a JSON object. ```sql SELECT json_object('name', 'Alice', 'age', 30); -- {"name":"Alice","age":30} SELECT json_object('items', json_array(1, 2, 3)); -- {"items":[1,2,3]} ``` ### json_quote Converts a SQL value to its JSON representation. ```sql json_quote(value) ``` | Parameter | Type | Description | |-----------|------|-------------| | `value` | any | A SQL value to quote as JSON | **Returns:** TEXT -- the JSON representation of the value. ```sql SELECT json_quote('hello'); -- "hello" SELECT json_quote(42); -- 42 SELECT json_quote(NULL); -- null ``` ### json_valid Returns 1 if the argument is well-formed JSON, or 0 otherwise. ```sql json_valid(json_text) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT | A string to check for valid JSON | **Returns:** INTEGER -- 1 if valid JSON, 0 otherwise. ```sql SELECT json_valid('{"name":"Alice"}'); -- 1 SELECT json_valid('not json'); -- 0 SELECT json_valid(NULL); -- 0 ``` ### json_error_position Returns the character position of the first syntax error in a JSON string, or 0 if the string is valid JSON. ```sql json_error_position(json_text) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT | A string to check for JSON errors | **Returns:** INTEGER -- character position of the first error (1-based), or 0 if valid. ```sql SELECT json_error_position('{"a":1}'); -- 0 SELECT json_error_position('{"a":}'); -- 6 ``` --- ## JSON Extraction ### json_extract / jsonb_extract Extracts one or more values from a JSON document using path arguments. ```sql json_extract(json_text, path) json_extract(json_text, path1, path2, ...) jsonb_extract(json_text, path) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | | `path` | TEXT | One or more JSON path expressions | **Returns:** With a single path, returns the extracted value using its natural SQL type (INTEGER, REAL, TEXT, or NULL). JSON objects and arrays are returned as TEXT. With multiple paths, returns a JSON array of the extracted values. `jsonb_extract` returns BLOB. ```sql SELECT json_extract('{"name":"Alice","age":30}', '$.name'); -- Alice SELECT json_extract('{"name":"Alice","age":30}', '$.age'); -- 30 -- Multiple paths return a JSON array SELECT json_extract('{"a":1,"b":2,"c":3}', '$.a', '$.c'); -- [1,3] ``` ### -> operator Extracts a value from JSON and returns it as JSON. This is a shorthand for `json_extract` that always returns JSON text (objects and arrays remain as JSON, strings are JSON-quoted). ```sql json_text -> path ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | | `path` | TEXT | A JSON path expression | **Returns:** TEXT -- the extracted value as JSON. ```sql SELECT '{"name":"Alice"}' -> '$.name'; -- "Alice" SELECT '{"items":[1,2,3]}' -> '$.items'; -- [1,2,3] ``` ### ->> operator Extracts a value from JSON and returns it as a SQL value. Strings are unquoted, numbers are returned as INTEGER or REAL, and booleans are returned as integers (0 or 1). ```sql json_text ->> path ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | | `path` | TEXT | A JSON path expression | **Returns:** The extracted value as its natural SQL type (TEXT, INTEGER, REAL, or NULL). ```sql SELECT '{"name":"Alice"}' ->> '$.name'; -- Alice SELECT '{"count":42}' ->> '$.count'; -- 42 ``` ### json_type Returns the type of a JSON value as a string: `"null"`, `"true"`, `"false"`, `"integer"`, `"real"`, `"text"`, `"array"`, or `"object"`. ```sql json_type(json_text) json_type(json_text, path) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | | `path` | TEXT | Optional. A JSON path to inspect. If omitted, inspects the root. | **Returns:** TEXT -- the JSON type name. ```sql SELECT json_type('{"a":1}'); -- object SELECT json_type('[1, 2, 3]'); -- array SELECT json_type('{"a": 1}', '$.a'); -- integer SELECT json_type('{"a": "hello"}', '$.a'); -- text ``` --- ## JSON Modification ### json_insert / jsonb_insert Inserts new values into a JSON document. Existing values are **not** overwritten. If the path already exists, the value is left unchanged. ```sql json_insert(json_text, path1, value1, path2, value2, ...) jsonb_insert(json_text, path1, value1, path2, value2, ...) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | | `path` | TEXT | A JSON path where the value should be inserted | | `value` | any | The value to insert | **Returns:** TEXT (json_insert) or BLOB (jsonb_insert) -- the modified JSON. ```sql SELECT json_insert('{"a":1}', '$.b', 2); -- {"a":1,"b":2} -- Existing values are NOT overwritten SELECT json_insert('{"a":1}', '$.a', 99); -- {"a":1} ``` ### json_replace / jsonb_replace Replaces existing values in a JSON document. If the path does not exist, no insertion is made. ```sql json_replace(json_text, path1, value1, path2, value2, ...) jsonb_replace(json_text, path1, value1, path2, value2, ...) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | | `path` | TEXT | A JSON path identifying the value to replace | | `value` | any | The replacement value | **Returns:** TEXT (json_replace) or BLOB (jsonb_replace) -- the modified JSON. ```sql SELECT json_replace('{"a":1,"b":2}', '$.a', 99); -- {"a":99,"b":2} -- Non-existent paths are ignored SELECT json_replace('{"a":1}', '$.b', 2); -- {"a":1} ``` ### json_set / jsonb_set Inserts or replaces values in a JSON document. Combines the behavior of `json_insert` and `json_replace`: if the path exists, the value is replaced; if it does not exist, the value is inserted. ```sql json_set(json_text, path1, value1, path2, value2, ...) jsonb_set(json_text, path1, value1, path2, value2, ...) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | | `path` | TEXT | A JSON path for the value | | `value` | any | The value to set | **Returns:** TEXT (json_set) or BLOB (jsonb_set) -- the modified JSON. ```sql -- Replace existing SELECT json_set('{"a":1}', '$.a', 99); -- {"a":99} -- Insert new SELECT json_set('{"a":1}', '$.b', 2); -- {"a":1,"b":2} ``` ### json_remove / jsonb_remove Removes one or more elements from a JSON document. ```sql json_remove(json_text, path1, path2, ...) jsonb_remove(json_text, path1, path2, ...) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | | `path` | TEXT | One or more JSON paths to remove | **Returns:** TEXT (json_remove) or BLOB (jsonb_remove) -- the modified JSON. ```sql SELECT json_remove('{"a":1,"b":2,"c":3}', '$.b'); -- {"a":1,"c":3} SELECT json_remove('[1,2,3,4]', '$[1]'); -- [1,3,4] ``` ### json_patch / jsonb_patch Applies an RFC 7396 merge patch to a JSON document. Object members in the patch overwrite members in the target. A `null` value in the patch removes the corresponding member. ```sql json_patch(json_text, patch) jsonb_patch(json_text, patch) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | The target JSON document | | `patch` | TEXT or BLOB | The merge patch to apply | **Returns:** TEXT (json_patch) or BLOB (jsonb_patch) -- the patched JSON. ```sql SELECT json_patch('{"a":1,"b":2}', '{"b":3,"c":4}'); -- {"a":1,"b":3,"c":4} -- null in the patch removes a key SELECT json_patch('{"a":1,"b":2}', '{"b":null}'); -- {"a":1} ``` ### json_pretty Returns a pretty-printed (indented) representation of a JSON document. ```sql json_pretty(json_text) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | **Returns:** TEXT -- the formatted JSON string with indentation. ```sql SELECT json_pretty('{"name":"Alice","scores":[90,85,92]}'); /* { "name": "Alice", "scores": [ 90, 85, 92 ] } */ ``` --- ## JSON Array Functions ### json_array_length Returns the number of elements in a JSON array. Returns 0 for an empty array and NULL for non-array JSON values. ```sql json_array_length(json_text) json_array_length(json_text, path) ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | | `path` | TEXT | Optional. A JSON path to an array within the document. | **Returns:** INTEGER -- the number of elements, or NULL if the value at the path is not an array. ```sql SELECT json_array_length('[1, 2, 3, 4]'); -- 4 SELECT json_array_length('{"items": [10, 20]}', '$.items'); -- 2 SELECT json_array_length('{"a": 1}'); -- NULL ``` --- ## JSON Aggregate Functions ### json_group_array / jsonb_group_array Aggregate function that collects values from a group into a JSON array. ```sql json_group_array(value) jsonb_group_array(value) ``` | Parameter | Type | Description | |-----------|------|-------------| | `value` | any | The value to aggregate from each row | **Returns:** TEXT (json_group_array) or BLOB (jsonb_group_array) -- a JSON array of all values in the group. ```sql CREATE TABLE items (category TEXT, name TEXT); INSERT INTO items VALUES ('fruit', 'apple'), ('fruit', 'banana'), ('veggie', 'carrot'); SELECT category, json_group_array(name) FROM items GROUP BY category; -- fruit | ["apple","banana"] -- veggie | ["carrot"] ``` ### json_group_object / jsonb_group_object Aggregate function that collects label/value pairs from a group into a JSON object. ```sql json_group_object(label, value) jsonb_group_object(label, value) ``` | Parameter | Type | Description | |-----------|------|-------------| | `label` | TEXT | The key for each entry | | `value` | any | The value for each entry | **Returns:** TEXT (json_group_object) or BLOB (jsonb_group_object) -- a JSON object. ```sql CREATE TABLE settings (key TEXT, value TEXT); INSERT INTO settings VALUES ('theme', 'dark'), ('lang', 'en'); SELECT json_group_object(key, value) FROM settings; -- {"theme":"dark","lang":"en"} ``` --- ## JSON Table-Valued Functions ### json_each A table-valued function that walks the top-level elements of a JSON array or object, returning one row per element. ```sql SELECT * FROM json_each(json_text); SELECT * FROM json_each(json_text, path); ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | | `path` | TEXT | Optional. A JSON path to the array or object to iterate. Defaults to `$`. | **Output columns:** | Column | Type | Description | |--------|------|-------------| | `key` | TEXT or INTEGER | Object key (TEXT) or array index (INTEGER) | | `value` | any | The value of the element, as JSON text for objects/arrays or as a SQL value for primitives | | `type` | TEXT | JSON type: `null`, `true`, `false`, `integer`, `real`, `text`, `array`, or `object` | | `atom` | any | The SQL value for primitives (NULL for arrays and objects) | | `id` | INTEGER | A sequential identifier for the element | | `parent` | INTEGER | The id of the parent element (NULL for top-level) | | `fullkey` | TEXT | The full JSON path to this element | | `path` | TEXT | The JSON path to the parent of this element | ```sql SELECT key, value, type FROM json_each('[10, "hello", null]'); -- 0 | 10 | integer -- 1 | hello | text -- 2 | null | null SELECT key, value FROM json_each('{"a":1, "b":2}'); -- a | 1 -- b | 2 -- With a path SELECT key, value FROM json_each('{"data": [1, 2, 3]}', '$.data'); -- 0 | 1 -- 1 | 2 -- 2 | 3 ``` ### json_tree `json_tree` has partial support. Some advanced traversal features may not work as expected. A table-valued function that recursively walks a JSON document, returning one row for every element at every level of nesting. ```sql SELECT * FROM json_tree(json_text); SELECT * FROM json_tree(json_text, path); ``` | Parameter | Type | Description | |-----------|------|-------------| | `json_text` | TEXT or BLOB | A JSON document | | `path` | TEXT | Optional. A JSON path to the subtree to walk. Defaults to `$`. | **Output columns:** Same as `json_each`. ```sql SELECT key, value, type, path FROM json_tree('{"a": [1, 2]}'); -- NULL | {"a":[1,2]} | object | $ -- a | [1,2] | array | $ -- 0 | 1 | integer | $.a -- 1 | 2 | integer | $.a ``` --- ## Practical Examples ### Storing and querying JSON data ```sql CREATE TABLE events (id INTEGER PRIMARY KEY, data TEXT); INSERT INTO events VALUES (1, '{"type":"click","x":100,"y":200}'); INSERT INTO events VALUES (2, '{"type":"scroll","offset":500}'); -- Extract specific fields SELECT id, data ->> '$.type' AS event_type FROM events; -- 1 | click -- 2 | scroll -- Filter by JSON value SELECT * FROM events WHERE data ->> '$.type' = 'click'; ``` ### Modifying JSON in place ```sql UPDATE events SET data = json_set(data, '$.timestamp', '2025-01-15T10:30:00Z') WHERE id = 1; SELECT data FROM events WHERE id = 1; -- {"type":"click","x":100,"y":200,"timestamp":"2025-01-15T10:30:00Z"} ``` ### Building JSON from relational data ```sql CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT); INSERT INTO users VALUES (1, 'Alice', 'alice@example.com'); INSERT INTO users VALUES (2, 'Bob', 'bob@example.com'); SELECT json_object('users', json_group_array( json_object('id', id, 'name', name, 'email', email) )) FROM users; -- {"users":[{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}]} ``` ### Flattening JSON arrays with json_each ```sql CREATE TABLE orders (id INTEGER PRIMARY KEY, items TEXT); INSERT INTO orders VALUES (1, '["widget","gadget","gizmo"]'); INSERT INTO orders VALUES (2, '["sprocket"]'); -- Expand each order's items into individual rows SELECT orders.id, each.value AS item FROM orders, json_each(orders.items) AS each; -- 1 | widget -- 1 | gadget -- 1 | gizmo -- 2 | sprocket ``` ## See Also - [Data Types](/docs/sql-reference/data-types) for how JSON values map to SQL types - [Expressions](/docs/sql-reference/expressions) for using JSON operators in expressions ================================================ FILE: docs/sql-reference/functions/math.mdx ================================================ [File too large to display: 12.3 KB] ================================================ FILE: docs/sql-reference/functions/scalar.mdx ================================================ --- title: Scalar Functions description: Built-in scalar functions for string manipulation, math, type inspection, and more sidebarTitle: Scalar Functions --- # Scalar Functions Scalar functions accept one or more arguments and return a single value. They can be used anywhere an expression is valid: SELECT columns, WHERE conditions, ORDER BY, GROUP BY, HAVING, CHECK constraints, and DEFAULT values. ## Function Reference ### Math Functions | Function | Description | |----------|-------------| | `abs(X)` | Absolute value of X. Returns INTEGER if X is INTEGER, REAL if X is REAL, NULL if X is NULL | | `max(X, Y, ...)` | Returns the argument with the maximum value | | `min(X, Y, ...)` | Returns the argument with the minimum value | | `random()` | Returns a random 64-bit signed integer | | `round(X)` | Rounds X to the nearest integer | | `round(X, Y)` | Rounds X to Y decimal places | | `sign(X)` | Returns -1, 0, or 1 for negative, zero, or positive X | ### String Functions | Function | Description | |----------|-------------| | `char(X1, X2, ..., XN)` | Returns a string composed of characters with Unicode code points X1 through XN | | `concat(X, ...)` | Concatenates all arguments as strings. NULL arguments are skipped | | `concat_ws(SEP, X, ...)` | Concatenates arguments with separator SEP. NULL arguments are skipped | | `format(FORMAT, ...)` | Returns a formatted string using printf-style format specifiers | | `hex(X)` | Returns the uppercase hexadecimal representation of X | | `instr(X, Y)` | Returns the 1-based position of the first occurrence of Y in X, or 0 if not found | | `length(X)` | Returns the string length in characters, or blob length in bytes | | `lower(X)` | Returns a lowercase copy of string X | | `ltrim(X)` | Removes leading whitespace from X | | `ltrim(X, Y)` | Removes leading characters found in Y from X | | `octet_length(X)` | Returns the length of X in bytes | | `printf(FORMAT, ...)` | Alias for `format()`. Returns a formatted string | | `quote(X)` | Returns the SQL literal representation of X | | `replace(X, Y, Z)` | Returns X with every occurrence of Y replaced by Z | | `rtrim(X)` | Removes trailing whitespace from X | | `rtrim(X, Y)` | Removes trailing characters found in Y from X | | `soundex(X)` | Returns the Soundex encoding of string X | | `substr(X, Y)` | Returns the substring of X starting at position Y (1-based) | | `substr(X, Y, Z)` | Returns Z characters from X starting at position Y | | `substring(X, Y)` | Alias for `substr(X, Y)` | | `substring(X, Y, Z)` | Alias for `substr(X, Y, Z)` | | `trim(X)` | Removes leading and trailing whitespace from X | | `trim(X, Y)` | Removes leading and trailing characters found in Y from X | | `unhex(X)` | Converts hexadecimal string X to a blob. Returns NULL if X contains non-hex characters | | `unhex(X, Y)` | Like `unhex(X)`, but characters in Y are silently ignored in X | | `unicode(X)` | Returns the Unicode code point of the first character of string X | | `upper(X)` | Returns an uppercase copy of string X | | `zeroblob(N)` | Returns a blob consisting of N zero bytes | ### Conditional Functions | Function | Description | |----------|-------------| | `coalesce(X, Y, ...)` | Returns the first non-NULL argument. Returns NULL if all arguments are NULL | | `ifnull(X, Y)` | Returns X if X is not NULL, otherwise returns Y. Equivalent to `coalesce(X, Y)` | | `iif(X, Y, Z)` | Returns Y if X is true (non-zero), otherwise returns Z | | `if(X, Y, Z)` | Alias for `iif(X, Y, Z)` | | `nullif(X, Y)` | Returns NULL if X equals Y, otherwise returns X | ### Type Functions | Function | Description | |----------|-------------| | `typeof(X)` | Returns the type of X as a string: `"null"`, `"integer"`, `"real"`, `"text"`, or `"blob"` | ### Pattern Matching Functions | Function | Description | |----------|-------------| | `glob(X, Y)` | Returns 1 if string Y matches glob pattern X, 0 otherwise. Case-sensitive | | `like(X, Y)` | Returns 1 if string Y matches LIKE pattern X, 0 otherwise. Case-insensitive for ASCII | | `like(X, Y, Z)` | Like `like(X, Y)` but uses Z as the escape character | ### Optimizer Hints | Function | Description | |----------|-------------| | `likelihood(X, Y)` | Returns X unchanged. Hints to the query planner that X is true with probability Y (0.0 to 1.0) | | `likely(X)` | Returns X unchanged. Equivalent to `likelihood(X, 0.9375)` | | `unlikely(X)` | Returns X unchanged. Equivalent to `likelihood(X, 0.0625)` | ### Blob Functions | Function | Description | |----------|-------------| | `hex(X)` | Returns the uppercase hexadecimal representation of blob or integer X | | `randomblob(N)` | Returns a blob of N bytes filled with pseudo-random data | | `unhex(X)` | Converts hexadecimal string X to a blob | | `zeroblob(N)` | Returns a blob of N zero bytes | ### System Functions | Function | Description | |----------|-------------| | `last_insert_rowid()` | Returns the rowid of the most recent successful INSERT on the same database connection | | `changes()` | Returns the number of rows modified by the most recent INSERT, UPDATE, or DELETE | | `total_changes()` | Returns the total number of rows modified since the database connection was opened | | `sqlite_version()` | Returns the version string `"3.42.0"` | | `sqlite_source_id()` | Returns the source identifier string for the SQLite-compatible engine | | `load_extension(X)` | Loads a Turso-native extension from the shared library at path X | ## Detailed Descriptions and Examples ### abs(X) Returns the absolute value of X. The return type matches the input: INTEGER for integer inputs, REAL for floating-point inputs. Returns NULL if X is NULL. Returns INTEGER for a text value that looks like an integer. ```sql SELECT abs(-42); -- 42 SELECT abs(3.14); -- 3.14 SELECT abs(0); -- 0 SELECT abs(NULL); -- NULL ``` ### char(X1, X2, ..., XN) Returns a string composed of characters having the Unicode code points X1 through XN. Arguments that are not valid code points are replaced with the Unicode replacement character (U+FFFD). ```sql SELECT char(72, 101, 108, 108, 111); -- 'Hello' SELECT char(9731); -- snowman character SELECT hex(char(0)); -- '00' ``` ### coalesce(X, Y, ...) Returns the first argument that is not NULL. If all arguments are NULL, returns NULL. Requires at least two arguments. ```sql SELECT coalesce(NULL, NULL, 'fallback'); -- 'fallback' SELECT coalesce(1, 2, 3); -- 1 SELECT coalesce(NULL, 42); -- 42 ``` ### concat(X, ...) and concat_ws(SEP, X, ...) `concat` joins all arguments as strings, skipping NULLs. `concat_ws` inserts the separator between non-NULL arguments. ```sql SELECT concat('Hello', ' ', 'World'); -- 'Hello World' SELECT concat('a', NULL, 'b'); -- 'ab' SELECT concat_ws(', ', 'Alice', 'Bob', NULL, 'Carol'); -- 'Alice, Bob, Carol' SELECT concat_ws('-', 2024, 1, 15); -- '2024-1-15' ``` ### format(FORMAT, ...) and printf(FORMAT, ...) Returns a formatted string using printf-style format specifiers. `printf` is an alias for `format`. | Specifier | Description | |-----------|-------------| | `%d` | Signed integer | | `%f` | Floating-point | | `%s` | String | | `%x` | Lowercase hexadecimal integer | | `%X` | Uppercase hexadecimal integer | | `%o` | Octal integer | | `%e` | Scientific notation | | `%g` | General floating-point (shortest representation) | | `%%` | Literal percent sign | ```sql SELECT format('Hello, %s! You are #%d.', 'Alice', 1); -- 'Hello, Alice! You are #1.' SELECT printf('%.2f%%', 99.5); -- '99.50%' SELECT format('0x%08X', 255); -- '0x000000FF' ``` ### glob(X, Y) Returns 1 if string Y matches the glob pattern X, and 0 otherwise. Glob matching is case-sensitive and uses `*` for any sequence of characters, `?` for any single character, and `[...]` for character classes. ```sql SELECT glob('*.txt', 'readme.txt'); -- 1 SELECT glob('*.TXT', 'readme.txt'); -- 0 (case-sensitive) SELECT glob('H?llo', 'Hello'); -- 1 ``` The `glob(X, Y)` function is the functional form of the `Y GLOB X` operator. Note the reversed argument order compared to the operator syntax. ### hex(X) and unhex(X) `hex` returns the uppercase hexadecimal representation of its argument. For text, it returns the hex encoding of the UTF-8 bytes. For blobs, it encodes each byte. For integers, it returns the hex of the value. `unhex` converts a hexadecimal string back to a blob. Returns NULL if the input contains non-hex characters, unless a second argument specifies characters to ignore. ```sql SELECT hex('ABC'); -- '414243' SELECT hex(255); -- 'FF' SELECT hex(x'CAFE'); -- 'CAFE' SELECT unhex('48454C4C4F'); -- x'48454C4C4F' (blob for 'HELLO') SELECT unhex('48-45-4C', '-'); -- x'48454C' (ignoring dashes) SELECT unhex('ZZZZ'); -- NULL (invalid hex) ``` ### iif(X, Y, Z) and if(X, Y, Z) Returns Y if X is true (non-zero and non-NULL), otherwise returns Z. `if` is an alias for `iif`. ```sql SELECT iif(1 > 0, 'yes', 'no'); -- 'yes' SELECT iif(NULL, 'yes', 'no'); -- 'no' SELECT if(10 > 5, 'big', 'small'); -- 'big' ``` ### instr(X, Y) Returns the 1-based position of the first occurrence of string Y in string X. Returns 0 if Y is not found in X. If either argument is NULL, returns NULL. ```sql SELECT instr('Hello World', 'World'); -- 7 SELECT instr('Hello World', 'xyz'); -- 0 SELECT instr('abcabc', 'bc'); -- 2 ``` ### length(X) and octet_length(X) `length` returns the number of characters in a text value, or the number of bytes in a blob value. For NULL, returns NULL. For numeric values, returns the length of the text representation. `octet_length` always returns the length in bytes, regardless of type. ```sql SELECT length('Hello'); -- 5 SELECT length(x'AABBCC'); -- 3 (bytes for blob) SELECT length(12345); -- 5 (text representation) SELECT octet_length('Hello'); -- 5 (ASCII, 1 byte per char) ``` ### like(X, Y) and like(X, Y, Z) Returns 1 if string Y matches LIKE pattern X, and 0 otherwise. `%` matches any sequence of characters, `_` matches any single character. Matching is case-insensitive for ASCII characters. The optional third argument Z specifies an escape character. ```sql SELECT like('%ello%', 'Hello World'); -- 1 SELECT like('H_llo', 'Hello'); -- 1 SELECT like('10\%%', '10% discount', '\'); -- 1 (escaped %) ``` The `like(X, Y)` function is the functional form of the `Y LIKE X` operator. Note the reversed argument order compared to the operator syntax. ### lower(X), upper(X) `lower` returns a copy of string X with all ASCII characters converted to lowercase. `upper` converts to uppercase. ```sql SELECT lower('Hello World'); -- 'hello world' SELECT upper('Hello World'); -- 'HELLO WORLD' ``` ### ltrim(X), rtrim(X), trim(X) These functions remove characters from the ends of a string. Without a second argument, they remove whitespace. With a second argument Y, they remove any characters present in the string Y. ```sql -- Whitespace trimming SELECT ltrim(' Hello'); -- 'Hello' SELECT rtrim('Hello '); -- 'Hello' SELECT trim(' Hello '); -- 'Hello' -- Character trimming SELECT ltrim('xxxHello', 'x'); -- 'Hello' SELECT rtrim('Helloyyy', 'y'); -- 'Hello' SELECT trim('***Hello***', '*'); -- 'Hello' SELECT trim('abcHelloabc', 'abc'); -- 'Hello' (removes any of a, b, or c) ``` ### max(X, Y, ...) and min(X, Y, ...) The multi-argument forms of `max` and `min` return the largest or smallest argument, respectively. Arguments are compared using the standard SQLite comparison rules. If any argument is NULL, the result is NULL. ```sql SELECT max(1, 5, 3); -- 5 SELECT min(1, 5, 3); -- 1 SELECT max('apple', 'banana'); -- 'banana' SELECT min(10, NULL, 3); -- NULL ``` The multi-argument `max()` and `min()` are scalar functions. When called with a single argument inside an aggregate query (e.g., `SELECT max(salary) FROM employees`), they act as [aggregate functions](/docs/sql-reference/functions/aggregate). ### nullif(X, Y) Returns NULL if X equals Y, otherwise returns X. This is useful for converting sentinel values to NULL. ```sql SELECT nullif(0, 0); -- NULL SELECT nullif(5, 0); -- 5 SELECT nullif('N/A', 'N/A'); -- NULL SELECT nullif('hello', ''); -- 'hello' ``` ### quote(X) Returns the text of an SQL literal that represents the value X. Strings are enclosed in single quotes with escaping. BLOBs are encoded as hex literals. NULL returns the string `'NULL'`. Numbers are returned as-is. ```sql SELECT quote('it''s'); -- '''it''s''' SELECT quote(42); -- '42' SELECT quote(NULL); -- 'NULL' SELECT quote(x'CAFE'); -- 'X''CAFE''' ``` ### random() and randomblob(N) `random` returns a pseudo-random 64-bit signed integer. `randomblob` returns a blob of N pseudo-random bytes. ```sql SELECT random(); -- e.g., -4520312828827489743 SELECT hex(randomblob(4)); -- e.g., 'A1B2C3D4' (4 random bytes) SELECT abs(random()) % 100; -- random number between 0 and 99 ``` ### replace(X, Y, Z) Returns a copy of string X with every occurrence of string Y replaced by string Z. If Y is empty, X is returned unchanged. ```sql SELECT replace('Hello World', 'World', 'Turso'); -- 'Hello Turso' SELECT replace('aabbcc', 'bb', 'XX'); -- 'aaXXcc' SELECT replace('2024-01-15', '-', '/'); -- '2024/01/15' ``` ### round(X) and round(X, Y) Rounds X to Y decimal places. If Y is omitted, it defaults to 0. The return type is always REAL. ```sql SELECT round(3.7); -- 4.0 SELECT round(3.14159, 2); -- 3.14 SELECT round(2.5); -- 3.0 SELECT round(-2.5); -- -3.0 ``` ### sign(X) Returns -1 for negative values, 0 for zero, and 1 for positive values. Returns NULL if X is NULL. ```sql SELECT sign(-42); -- -1 SELECT sign(0); -- 0 SELECT sign(3.14); -- 1 SELECT sign(NULL); -- NULL ``` ### substr(X, Y) and substr(X, Y, Z) Returns a substring of X starting at the Y-th character (1-based). If Z is provided, the substring is at most Z characters long. Negative Y counts from the end of the string. `substring` is an alias. ```sql SELECT substr('Hello World', 7); -- 'World' SELECT substr('Hello World', 1, 5); -- 'Hello' SELECT substr('Hello World', -5); -- 'World' SELECT substring('Hello', 2, 3); -- 'ell' ``` ### typeof(X) Returns the storage class of X as a lowercase string: `"null"`, `"integer"`, `"real"`, `"text"`, or `"blob"`. ```sql SELECT typeof(42); -- 'integer' SELECT typeof(3.14); -- 'real' SELECT typeof('hello'); -- 'text' SELECT typeof(NULL); -- 'null' SELECT typeof(x'CAFE'); -- 'blob' SELECT typeof(1 + 1.0); -- 'real' ``` ### unicode(X) Returns the Unicode code point of the first character of string X. Returns NULL if X is NULL or an empty string. ```sql SELECT unicode('A'); -- 65 SELECT unicode('Hello'); -- 72 (code point of 'H') ``` ### soundex(X) Returns the Soundex encoding of string X as a four-character code. Soundex encodes a string based on how it sounds in English, which is useful for fuzzy name matching. ```sql SELECT soundex('Robert'); -- 'R163' SELECT soundex('Rupert'); -- 'R163' SELECT soundex('Smith'); -- 'S530' SELECT soundex('Smythe'); -- 'S530' ``` ### zeroblob(N) Returns a blob consisting of N zero bytes (0x00). Useful for pre-allocating blob storage. ```sql SELECT length(zeroblob(10)); -- 10 SELECT hex(zeroblob(4)); -- '00000000' ``` ### last_insert_rowid() Returns the rowid of the most recent successful INSERT on the current database connection. Returns 0 if no INSERT has been performed. ```sql CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); INSERT INTO users (name) VALUES ('Alice'); SELECT last_insert_rowid(); -- 1 INSERT INTO users (name) VALUES ('Bob'); SELECT last_insert_rowid(); -- 2 ``` ### changes() and total_changes() `changes` returns the number of rows modified by the most recent INSERT, UPDATE, or DELETE statement. `total_changes` returns the total number of rows modified since the database connection was opened. ```sql CREATE TABLE t (x INTEGER); INSERT INTO t VALUES (1), (2), (3); SELECT changes(); -- 3 DELETE FROM t WHERE x > 1; SELECT changes(); -- 2 SELECT total_changes(); -- 5 (3 inserted + 2 deleted) ``` ### sqlite_version() Returns the SQLite-compatible version string. ```sql SELECT sqlite_version(); -- '3.42.0' ``` ### load_extension(X) Loads a Turso-native extension from the shared library at path X. ```sql SELECT load_extension('./my_extension'); ``` Extension loading must be enabled on the database connection. See the [Extensions documentation](/docs/extensions) for details on building and loading extensions. ## See Also - [Aggregate Functions](/docs/sql-reference/functions/aggregate) for functions that operate across groups of rows - [Expressions](/docs/sql-reference/expressions) for operator syntax, CAST, CASE, and subqueries - [Data Types](/docs/sql-reference/data-types) for storage classes and type affinity - [SELECT](/docs/sql-reference/statements/select) for using functions in queries ================================================ FILE: docs/sql-reference/functions/vector.mdx ================================================ [File too large to display: 9.8 KB] ================================================ FILE: docs/sql-reference/functions/window.mdx ================================================ --- title: Window Functions description: Perform calculations across sets of rows related to the current row without collapsing them sidebarTitle: Window --- # Window Functions Window functions perform calculations across a set of rows that are related to the current row. Unlike aggregate functions with GROUP BY, window functions do not collapse rows into a single output row. Every input row produces a corresponding output row, with the window function result appended. Turso supports aggregate functions used as window functions with the default frame definition. Custom frame specifications (ROWS, RANGE, or GROUPS with explicit bounds) and dedicated window functions (row_number, rank, dense_rank, ntile, lag, lead, first_value, last_value, nth_value) are not yet supported. ## Syntax ```sql aggregate_function(expression) OVER ( [PARTITION BY expression [, ...]] [ORDER BY expression [ASC | DESC] [, ...]] ) ``` | Clause | Description | |--------|-------------| | `aggregate_function` | Any supported aggregate function: `count`, `sum`, `avg`, `min`, `max`, `total`, `group_concat` | | `OVER (...)` | Defines the window over which the function operates | | `PARTITION BY` | Divides the result set into partitions. The function is applied independently within each partition. If omitted, the entire result set is one partition | | `ORDER BY` | Defines the order of rows within each partition. This determines which rows are included in the frame for each calculation | ### Default Frame When ORDER BY is specified, the default frame is: ``` RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ``` This means the function considers all rows from the start of the partition up to and including the current row (and any rows with equal ORDER BY values, since the frame mode is RANGE). When ORDER BY is omitted, the default frame covers the entire partition. ## Supported Aggregate Functions as Window Functions Any aggregate function can be used as a window function by adding an OVER clause. | Function | Description | |----------|-------------| | `count(*)` | Number of rows in the frame | | `count(expression)` | Number of non-NULL values in the frame | | `sum(expression)` | Sum of non-NULL values in the frame | | `avg(expression)` | Average of non-NULL values in the frame | | `min(expression)` | Minimum value in the frame | | `max(expression)` | Maximum value in the frame | | `total(expression)` | Sum as REAL (returns 0.0 for empty frames instead of NULL) | | `group_concat(expression, separator)` | Concatenation of values in the frame | ## PARTITION BY PARTITION BY divides the rows into groups. The window function resets and recalculates independently for each partition. ```sql SELECT department, name, salary, SUM(salary) OVER (PARTITION BY department) AS dept_total FROM employees; ``` | department | name | salary | dept_total | |------------|------|--------|------------| | Engineering | Alice | 90000 | 250000 | | Engineering | Bob | 85000 | 250000 | | Engineering | Carol | 75000 | 250000 | | Marketing | Dave | 70000 | 130000 | | Marketing | Eve | 60000 | 130000 | Without PARTITION BY, the function treats the entire result set as one partition: ```sql SELECT name, salary, SUM(salary) OVER () AS company_total FROM employees; ``` ## ORDER BY ORDER BY within the OVER clause determines row ordering within each partition. Combined with the default frame (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), this produces running calculations. ```sql SELECT name, salary, SUM(salary) OVER (ORDER BY salary) AS running_total FROM employees; ``` | name | salary | running_total | |------|--------|---------------| | Eve | 60000 | 60000 | | Dave | 70000 | 130000 | | Carol | 75000 | 205000 | | Bob | 85000 | 290000 | | Alice | 90000 | 380000 | ### PARTITION BY with ORDER BY Use both clauses together for running calculations within groups: ```sql SELECT department, name, salary, SUM(salary) OVER ( PARTITION BY department ORDER BY salary ) AS dept_running_total FROM employees; ``` | department | name | salary | dept_running_total | |------------|------|--------|--------------------| | Engineering | Carol | 75000 | 75000 | | Engineering | Bob | 85000 | 160000 | | Engineering | Alice | 90000 | 250000 | | Marketing | Eve | 60000 | 60000 | | Marketing | Dave | 70000 | 130000 | ## Named Windows The WINDOW clause defines a reusable window specification that can be referenced by multiple window functions in the same query. This avoids repeating the same OVER definition. ```sql SELECT department, name, salary, SUM(salary) OVER w AS running_total, AVG(salary) OVER w AS running_avg, COUNT(*) OVER w AS running_count FROM employees WINDOW w AS (PARTITION BY department ORDER BY salary) ORDER BY department, salary; ``` Multiple named windows can be defined: ```sql SELECT department, name, salary, SUM(salary) OVER dept AS dept_total, SUM(salary) OVER company AS company_total FROM employees WINDOW dept AS (PARTITION BY department), company AS () ORDER BY department, name; ``` ## Examples ### Running Total ```sql SELECT order_date, amount, SUM(amount) OVER (ORDER BY order_date) AS running_total FROM orders; ``` ### Count per Group ```sql -- Show each employee alongside their department headcount SELECT name, department, COUNT(*) OVER (PARTITION BY department) AS dept_size FROM employees ORDER BY department, name; ``` ### Running Average ```sql SELECT date, temperature, AVG(temperature) OVER (ORDER BY date) AS running_avg_temp FROM weather_readings ORDER BY date; ``` ### Percentage of Total ```sql SELECT product_name, revenue, ROUND(100.0 * revenue / SUM(revenue) OVER (), 2) AS pct_of_total FROM products ORDER BY revenue DESC; ``` ### Multiple Window Functions in One Query ```sql SELECT department, name, salary, MIN(salary) OVER (PARTITION BY department) AS dept_min, MAX(salary) OVER (PARTITION BY department) AS dept_max, AVG(salary) OVER (PARTITION BY department) AS dept_avg, salary - AVG(salary) OVER (PARTITION BY department) AS diff_from_avg FROM employees ORDER BY department, salary DESC; ``` ### Group Concatenation over a Window ```sql SELECT department, name, GROUP_CONCAT(name, ', ') OVER (PARTITION BY department ORDER BY name) AS names_so_far FROM employees; ``` ## Limitations The following window function features are not yet supported in Turso: | Feature | Status | |---------|--------| | `row_number()` | Not supported | | `rank()` | Not supported | | `dense_rank()` | Not supported | | `ntile(N)` | Not supported | | `lag(expr, offset, default)` | Not supported | | `lead(expr, offset, default)` | Not supported | | `first_value(expr)` | Not supported | | `last_value(expr)` | Not supported | | `nth_value(expr, N)` | Not supported | | `cume_dist()` | Not supported | | `percent_rank()` | Not supported | | Custom frame: `ROWS BETWEEN ...` | Not supported | | Custom frame: `RANGE BETWEEN ... AND ...` | Not supported | | Custom frame: `GROUPS BETWEEN ...` | Not supported | | `EXCLUDE` clause | Not supported | | `FILTER (WHERE ...)` on window functions | Not supported | ## See Also - [SELECT](/docs/sql-reference/statements/select) for the full SELECT syntax including the WINDOW clause - [Aggregate Functions](/docs/sql-reference/functions/aggregate) for aggregate function reference - [Expressions](/docs/sql-reference/expressions) for using window function results in expressions ================================================ FILE: docs/sql-reference/pragmas.mdx ================================================ [File too large to display: 12.0 KB] ================================================ FILE: docs/sql-reference/preview.sh ================================================ #!/usr/bin/env bash # Builds an mdBook preview from the .mdx source files. # Usage: ./preview.sh (builds and serves at localhost:3000) # ./preview.sh build (builds only, output in ./book/) set -euo pipefail cd "$(dirname "$0")" if ! command -v mdbook &>/dev/null; then echo "mdbook not found. Install with: cargo install mdbook" exit 1 fi # Clean and create src directory for mdbook rm -rf src mkdir -p src/statements src/functions src/cli # Convert .mdx files to .md: # - Strip YAML frontmatter # - Transform Mintlify callouts to blockquotes # - Rewrite /docs/sql-reference/ links to relative .md paths convert() { local in="$1" out="$2" # Determine depth: files in subdirs need ../ prefix for top-level pages local depth="" case "$in" in statements/*|functions/*|cli/*) depth="../" ;; esac sed -E \ -e 's/^$/> **Note**/g' \ -e 's/^$/> **Warning**/g' \ -e 's/^$/> **Note**/g' \ -e 's/^<\/(Info|Warning|Note)>$//g' \ -e '/^---$/,/^---$/d' \ -e "s|\(/docs/sql-reference/([^)#]+)(#[^)]+)?\)|(${depth}\1.md\2)|g" \ "$in" > "$out" } for f in *.mdx; do convert "$f" "src/${f%.mdx}.md" done for f in statements/*.mdx; do convert "$f" "src/${f%.mdx}.md" done for f in functions/*.mdx; do convert "$f" "src/${f%.mdx}.md" done for f in cli/*.mdx; do convert "$f" "src/${f%.mdx}.md" done # Generate SUMMARY.md cat > src/SUMMARY.md << 'EOF' # Summary # CLI - [Getting Started](cli/getting-started.md) - [Command-Line Options](cli/command-line-options.md) - [Shell Commands](cli/shell-commands.md) # SQL Language - [Data Types](data-types.md) - [Expressions](expressions.md) # Statements - [SELECT](statements/select.md) - [INSERT](statements/insert.md) - [UPDATE](statements/update.md) - [DELETE](statements/delete.md) - [REPLACE](statements/replace.md) - [UPSERT](statements/upsert.md) - [CREATE TABLE](statements/create-table.md) - [ALTER TABLE](statements/alter-table.md) - [DROP TABLE](statements/drop-table.md) - [CREATE INDEX](statements/create-index.md) - [DROP INDEX](statements/drop-index.md) - [CREATE VIEW](statements/create-view.md) - [CREATE MATERIALIZED VIEW](statements/create-materialized-view.md) - [DROP VIEW](statements/drop-view.md) - [CREATE TRIGGER](statements/create-trigger.md) - [DROP TRIGGER](statements/drop-trigger.md) - [CREATE VIRTUAL TABLE](statements/create-virtual-table.md) - [CREATE TYPE](statements/create-type.md) - [DROP TYPE](statements/drop-type.md) - [Transactions](statements/transactions.md) - [ATTACH DATABASE](statements/attach-database.md) - [DETACH DATABASE](statements/detach-database.md) - [ANALYZE](statements/analyze.md) - [EXPLAIN](statements/explain.md) # Functions - [Scalar](functions/scalar.md) - [Aggregate](functions/aggregate.md) - [Date & Time](functions/date-time.md) - [Math](functions/math.md) - [JSON](functions/json.md) - [Window](functions/window.md) - [Array](functions/array.md) - [Vector](functions/vector.md) - [Full-Text Search](functions/fts.md) # Reference - [PRAGMAs](pragmas.md) - [Extensions](extensions.md) - [Experimental Features](experimental-features.md) - [Compatibility](compatibility.md) EOF if [ "${1:-}" = "build" ]; then mdbook build echo "Built to ./book/" else echo "Starting preview at http://localhost:3000" echo "Press Ctrl+C to stop." mdbook serve --open fi ================================================ FILE: docs/sql-reference/statements/alter-table.mdx ================================================ [File too large to display: 7.8 KB] ================================================ FILE: docs/sql-reference/statements/analyze.mdx ================================================ [File too large to display: 2.2 KB] ================================================ FILE: docs/sql-reference/statements/attach-database.mdx ================================================ [File too large to display: 2.3 KB] ================================================ FILE: docs/sql-reference/statements/create-index.mdx ================================================ [File too large to display: 6.1 KB] ================================================ FILE: docs/sql-reference/statements/create-materialized-view.mdx ================================================ [File too large to display: 3.2 KB] ================================================ FILE: docs/sql-reference/statements/create-table.mdx ================================================ [File too large to display: 12.7 KB] ================================================ FILE: docs/sql-reference/statements/create-trigger.mdx ================================================ [File too large to display: 8.2 KB] ================================================ FILE: docs/sql-reference/statements/create-type.mdx ================================================ [File too large to display: 12.0 KB] ================================================ FILE: docs/sql-reference/statements/create-view.mdx ================================================ [File too large to display: 2.7 KB] ================================================ FILE: docs/sql-reference/statements/create-virtual-table.mdx ================================================ [File too large to display: 3.4 KB] ================================================ FILE: docs/sql-reference/statements/delete.mdx ================================================ [File too large to display: 3.3 KB] ================================================ FILE: docs/sql-reference/statements/detach-database.mdx ================================================ [File too large to display: 1.3 KB] ================================================ FILE: docs/sql-reference/statements/drop-index.mdx ================================================ [File too large to display: 1.5 KB] ================================================ FILE: docs/sql-reference/statements/drop-table.mdx ================================================ [File too large to display: 1.8 KB] ================================================ FILE: docs/sql-reference/statements/drop-trigger.mdx ================================================ [File too large to display: 1.6 KB] ================================================ FILE: docs/sql-reference/statements/drop-type.mdx ================================================ [File too large to display: 1.9 KB] ================================================ FILE: docs/sql-reference/statements/drop-view.mdx ================================================ [File too large to display: 1.3 KB] ================================================ FILE: docs/sql-reference/statements/explain.mdx ================================================ [File too large to display: 3.9 KB] ================================================ FILE: docs/sql-reference/statements/insert.mdx ================================================ [File too large to display: 7.2 KB] ================================================ FILE: docs/sql-reference/statements/replace.mdx ================================================ [File too large to display: 3.6 KB] ================================================ FILE: docs/sql-reference/statements/select.mdx ================================================ [File too large to display: 15.0 KB] ================================================ FILE: docs/sql-reference/statements/transactions.mdx ================================================ [File too large to display: 5.5 KB] ================================================ FILE: docs/sql-reference/statements/update.mdx ================================================ [File too large to display: 5.3 KB] ================================================ FILE: docs/sql-reference/statements/upsert.mdx ================================================ [File too large to display: 6.3 KB] ================================================ FILE: docs/testing.md ================================================ [File too large to display: 5.2 KB] ================================================ FILE: examples/.gitignore ================================================ [File too large to display: 78 B] ================================================ FILE: examples/README.md ================================================ [File too large to display: 594 B] ================================================ FILE: examples/dotnet/Encryption.cs ================================================ [File too large to display: 4.8 KB] ================================================ FILE: examples/dotnet/EncryptionExample.csproj ================================================ [File too large to display: 1.0 KB] ================================================ FILE: examples/dotnet/README.md ================================================ [File too large to display: 306 B] ================================================ FILE: examples/go/README.md ================================================ [File too large to display: 673 B] ================================================ FILE: examples/go/encryption.go ================================================ [File too large to display: 4.2 KB] ================================================ FILE: examples/go/go.mod ================================================ [File too large to display: 356 B] ================================================ FILE: examples/go/go.sum ================================================ [File too large to display: 1.2 KB] ================================================ FILE: examples/java/README.md ================================================ [File too large to display: 274 B] ================================================ FILE: examples/javascript/concurrent-writes/index.mjs ================================================ [File too large to display: 2.1 KB] ================================================ FILE: examples/javascript/concurrent-writes/package.json ================================================ [File too large to display: 311 B] ================================================ FILE: examples/javascript/database-node/README.md ================================================ [File too large to display: 214 B] ================================================ FILE: examples/javascript/database-node/index.mjs ================================================ [File too large to display: 885 B] ================================================ FILE: examples/javascript/database-node/package.json ================================================ [File too large to display: 357 B] ================================================ FILE: examples/javascript/database-wasm-vite/.gitignore ================================================ [File too large to display: 8 B] ================================================ FILE: examples/javascript/database-wasm-vite/README.md ================================================ [File too large to display: 1.4 KB] ================================================ FILE: examples/javascript/database-wasm-vite/index.html ================================================ [File too large to display: 2.4 KB] ================================================ FILE: examples/javascript/database-wasm-vite/package.json ================================================ [File too large to display: 459 B] ================================================ FILE: examples/javascript/database-wasm-vite/server.mjs ================================================ [File too large to display: 1.2 KB] ================================================ FILE: examples/javascript/database-wasm-vite/vercel.json ================================================ [File too large to display: 235 B] ================================================ FILE: examples/javascript/database-wasm-vite/vite.config.ts ================================================ [File too large to display: 212 B] ================================================ FILE: examples/javascript/encryption/encryption.mjs ================================================ [File too large to display: 2.9 KB] ================================================ FILE: examples/javascript/encryption/package.json ================================================ [File too large to display: 311 B] ================================================ FILE: examples/javascript/sync-encryption/README.md ================================================ [File too large to display: 508 B] ================================================ FILE: examples/javascript/sync-encryption/package.json ================================================ [File too large to display: 237 B] ================================================ FILE: examples/javascript/sync-encryption/sync_example.mjs ================================================ [File too large to display: 2.8 KB] ================================================ FILE: examples/javascript/sync-node/README.md ================================================ [File too large to display: 1.2 KB] ================================================ FILE: examples/javascript/sync-node/index.mjs ================================================ [File too large to display: 1.4 KB] ================================================ FILE: examples/javascript/sync-node/package.json ================================================ [File too large to display: 354 B] ================================================ FILE: examples/javascript/sync-wasm-vite/.gitignore ================================================ [File too large to display: 8 B] ================================================ FILE: examples/javascript/sync-wasm-vite/README.md ================================================ [File too large to display: 2.7 KB] ================================================ FILE: examples/javascript/sync-wasm-vite/index.html ================================================ [File too large to display: 3.5 KB] ================================================ FILE: examples/javascript/sync-wasm-vite/package.json ================================================ [File too large to display: 456 B] ================================================ FILE: examples/javascript/sync-wasm-vite/server.mjs ================================================ [File too large to display: 1.2 KB] ================================================ FILE: examples/javascript/sync-wasm-vite/vercel.json ================================================ [File too large to display: 235 B] ================================================ FILE: examples/javascript/sync-wasm-vite/vite.config.ts ================================================ [File too large to display: 212 B] ================================================ FILE: examples/python/README.md ================================================ [File too large to display: 747 B] ================================================ FILE: examples/python/basic.py ================================================ [File too large to display: 1.1 KB] ================================================ FILE: examples/python/concurrent_writes.py ================================================ [File too large to display: 2.3 KB] ================================================ FILE: examples/python/encryption.py ================================================ [File too large to display: 3.1 KB] ================================================ FILE: examples/python/sync_example.py ================================================ [File too large to display: 3.3 KB] ================================================ FILE: examples/react-native/.bundle/config ================================================ [File too large to display: 59 B] ================================================ FILE: examples/react-native/.eslintrc.js ================================================ [File too large to display: 64 B] ================================================ FILE: examples/react-native/.gitignore ================================================ [File too large to display: 1.1 KB] ================================================ FILE: examples/react-native/.prettierrc.js ================================================ [File too large to display: 91 B] ================================================ FILE: examples/react-native/.watchmanconfig ================================================ [File too large to display: 3 B] ================================================ FILE: examples/react-native/Gemfile ================================================ [File too large to display: 520 B] ================================================ FILE: examples/react-native/README.md ================================================ [File too large to display: 4.1 KB] ================================================ FILE: examples/react-native/android/app/build.gradle ================================================ [File too large to display: 4.6 KB] ================================================ FILE: examples/react-native/android/app/proguard-rules.pro ================================================ [File too large to display: 435 B] ================================================ FILE: examples/react-native/android/app/src/main/AndroidManifest.xml ================================================ [File too large to display: 1.0 KB] ================================================ FILE: examples/react-native/android/app/src/main/java/com/tursoexample/MainActivity.kt ================================================ [File too large to display: 855 B] ================================================ FILE: examples/react-native/android/app/src/main/java/com/tursoexample/MainApplication.kt ================================================ [File too large to display: 788 B] ================================================ FILE: examples/react-native/android/app/src/main/res/drawable/rn_edit_text_material.xml ================================================ [File too large to display: 1.9 KB] ================================================ FILE: examples/react-native/android/app/src/main/res/values/strings.xml ================================================ [File too large to display: 75 B] ================================================ FILE: examples/react-native/android/app/src/main/res/values/styles.xml ================================================ [File too large to display: 282 B] ================================================ FILE: examples/react-native/android/build.gradle ================================================ [File too large to display: 547 B] ================================================ FILE: examples/react-native/android/gradle/wrapper/gradle-wrapper.properties ================================================ [File too large to display: 252 B] ================================================ FILE: examples/react-native/android/gradle.properties ================================================ [File too large to display: 1.9 KB] ================================================ FILE: examples/react-native/android/gradlew ================================================ [File too large to display: 8.5 KB] ================================================ FILE: examples/react-native/android/gradlew.bat ================================================ [File too large to display: 3.1 KB] ================================================ FILE: examples/react-native/android/settings.gradle ================================================ [File too large to display: 343 B] ================================================ FILE: examples/react-native/app.json ================================================ [File too large to display: 62 B] ================================================ FILE: examples/react-native/babel.config.js ================================================ [File too large to display: 298 B] ================================================ FILE: examples/react-native/index.js ================================================ [File too large to display: 191 B] ================================================ FILE: examples/react-native/ios/.xcode.env ================================================ [File too large to display: 482 B] ================================================ FILE: examples/react-native/ios/Podfile ================================================ [File too large to display: 1.1 KB] ================================================ FILE: examples/react-native/ios/TursoExample/AppDelegate.swift ================================================ [File too large to display: 1.2 KB] ================================================ FILE: examples/react-native/ios/TursoExample/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ [File too large to display: 849 B] ================================================ FILE: examples/react-native/ios/TursoExample/Images.xcassets/Contents.json ================================================ [File too large to display: 63 B] ================================================ FILE: examples/react-native/ios/TursoExample/Info.plist ================================================ [File too large to display: 1.6 KB] ================================================ FILE: examples/react-native/ios/TursoExample/LaunchScreen.storyboard ================================================ [File too large to display: 4.1 KB] ================================================ FILE: examples/react-native/ios/TursoExample/PrivacyInfo.xcprivacy ================================================ [File too large to display: 986 B] ================================================ FILE: examples/react-native/ios/TursoExample.xcodeproj/project.pbxproj ================================================ [File too large to display: 18.4 KB] ================================================ FILE: examples/react-native/ios/TursoExample.xcodeproj/xcshareddata/xcschemes/TursoExample.xcscheme ================================================ [File too large to display: 3.3 KB] ================================================ FILE: examples/react-native/ios/TursoExample.xcworkspace/contents.xcworkspacedata ================================================ [File too large to display: 230 B] ================================================ FILE: examples/react-native/jest.config.js ================================================ [File too large to display: 48 B] ================================================ FILE: examples/react-native/metro.config.js ================================================ [File too large to display: 416 B] ================================================ FILE: examples/react-native/package.json ================================================ [File too large to display: 1.3 KB] ================================================ FILE: examples/react-native/react-native.config.js ================================================ [File too large to display: 498 B] ================================================ FILE: examples/react-native/src/App.tsx ================================================ [File too large to display: 22.9 KB] ================================================ FILE: examples/react-native/tsconfig.json ================================================ [File too large to display: 214 B] ================================================ FILE: extensions/completion/Cargo.toml ================================================ [File too large to display: 474 B] ================================================ FILE: extensions/completion/build.rs ================================================ [File too large to display: 108 B] ================================================ FILE: extensions/completion/src/keywords.rs ================================================ [File too large to display: 2.0 KB] ================================================ FILE: extensions/completion/src/lib.rs ================================================ [File too large to display: 11.0 KB] ================================================ FILE: extensions/core/Cargo.toml ================================================ [File too large to display: 371 B] ================================================ FILE: extensions/core/README.md ================================================ [File too large to display: 14.7 KB] ================================================ FILE: extensions/core/build.rs ================================================ [File too large to display: 108 B] ================================================ FILE: extensions/core/src/functions.rs ================================================ [File too large to display: 1.0 KB] ================================================ FILE: extensions/core/src/lib.rs ================================================ [File too large to display: 1.5 KB] ================================================ FILE: extensions/core/src/types.rs ================================================ [File too large to display: 15.4 KB] ================================================ FILE: extensions/core/src/vfs_modules.rs ================================================ [File too large to display: 7.1 KB] ================================================ FILE: extensions/core/src/vtabs.rs ================================================ [File too large to display: 24.7 KB] ================================================ FILE: extensions/crypto/Cargo.toml ================================================ [File too large to display: 560 B] ================================================ FILE: extensions/crypto/build.rs ================================================ [File too large to display: 108 B] ================================================ FILE: extensions/crypto/src/crypto.rs ================================================ [File too large to display: 7.3 KB] ================================================ FILE: extensions/crypto/src/lib.rs ================================================ [File too large to display: 2.8 KB] ================================================ FILE: extensions/csv/Cargo.toml ================================================ [File too large to display: 526 B] ================================================ FILE: extensions/csv/build.rs ================================================ [File too large to display: 108 B] ================================================ FILE: extensions/csv/src/lib.rs ================================================ [File too large to display: 25.9 KB] ================================================ FILE: extensions/fuzzy/Cargo.toml ================================================ [File too large to display: 471 B] ================================================ FILE: extensions/fuzzy/build.rs ================================================ [File too large to display: 108 B] ================================================ FILE: extensions/fuzzy/src/caver.rs ================================================ [File too large to display: 5.5 KB] ================================================ FILE: extensions/fuzzy/src/common.rs ================================================ [File too large to display: 2.1 KB] ================================================ FILE: extensions/fuzzy/src/editdist.rs ================================================ [File too large to display: 7.2 KB] ================================================ FILE: extensions/fuzzy/src/lib.rs ================================================ [File too large to display: 17.4 KB] ================================================ FILE: extensions/fuzzy/src/phonetic.rs ================================================ [File too large to display: 2.6 KB] ================================================ FILE: extensions/fuzzy/src/rsoundex.rs ================================================ [File too large to display: 1.3 KB] ================================================ FILE: extensions/fuzzy/src/soundex.rs ================================================ [File too large to display: 1.8 KB] ================================================ FILE: extensions/fuzzy/src/translit.rs ================================================ [File too large to display: 32.7 KB] ================================================ FILE: extensions/ipaddr/Cargo.toml ================================================ [File too large to display: 488 B] ================================================ FILE: extensions/ipaddr/build.rs ================================================ [File too large to display: 108 B] ================================================ FILE: extensions/ipaddr/src/lib.rs ================================================ [File too large to display: 2.2 KB] ================================================ FILE: extensions/percentile/Cargo.toml ================================================ [File too large to display: 474 B] ================================================ FILE: extensions/percentile/build.rs ================================================ [File too large to display: 108 B] ================================================ FILE: extensions/percentile/src/lib.rs ================================================ [File too large to display: 7.0 KB] ================================================ FILE: extensions/regexp/Cargo.toml ================================================ [File too large to display: 510 B] ================================================ FILE: extensions/regexp/build.rs ================================================ [File too large to display: 108 B] ================================================ FILE: extensions/regexp/src/lib.rs ================================================ [File too large to display: 3.2 KB] ================================================ FILE: extensions/tests/Cargo.toml ================================================ [File too large to display: 547 B] ================================================ FILE: extensions/tests/src/lib.rs ================================================ [File too large to display: 17.3 KB] ================================================ FILE: flake.nix ================================================ [File too large to display: 2.6 KB] ================================================ FILE: fuzz/.gitignore ================================================ [File too large to display: 24 B] ================================================ FILE: fuzz/Cargo.toml ================================================ [File too large to display: 697 B] ================================================ FILE: fuzz/README.md ================================================ [File too large to display: 691 B] ================================================ FILE: fuzz/fuzz_targets/cast_real.rs ================================================ [File too large to display: 751 B] ================================================ FILE: fuzz/fuzz_targets/expression.rs ================================================ [File too large to display: 8.2 KB] ================================================ FILE: fuzz/fuzz_targets/scalar_func.rs ================================================ [File too large to display: 17.0 KB] ================================================ FILE: fuzz/fuzz_targets/schema.rs ================================================ [File too large to display: 9.2 KB] ================================================ FILE: licenses/bindings/java/assertj-license.md ================================================ [File too large to display: 11.1 KB] ================================================ FILE: licenses/bindings/java/errorprone-license.md ================================================ [File too large to display: 11.1 KB] ================================================ FILE: licenses/bindings/java/logback-license.md ================================================ [File too large to display: 466 B] ================================================ FILE: licenses/bindings/java/spotless-license.md ================================================ [File too large to display: 10.0 KB] ================================================ FILE: licenses/bindings/python/sqlalchemy-mit-license.md ================================================ [File too large to display: 1.1 KB] ================================================ FILE: licenses/core/libm-mit-license.md ================================================ [File too large to display: 1023 B] ================================================ FILE: licenses/core/pastey-apache-license.md ================================================ [File too large to display: 9.5 KB] ================================================ FILE: licenses/core/pastey-mit-license.md ================================================ [File too large to display: 1023 B] ================================================ FILE: licenses/core/serde-apache-license.md ================================================ [File too large to display: 9.4 KB] ================================================ FILE: licenses/core/serde-mit-license.md ================================================ [File too large to display: 1023 B] ================================================ FILE: licenses/core/serde_json5-license.md ================================================ [File too large to display: 11.1 KB] ================================================ FILE: licenses/core/windows-apache.license.md ================================================ [File too large to display: 11.1 KB] ================================================ FILE: licenses/core/windows-mit-license.md ================================================ [File too large to display: 1.1 KB] ================================================ FILE: licenses/extensions/ipnetwork-apache-license.md ================================================ [File too large to display: 11.1 KB] ================================================ FILE: licenses/extensions/ipnetwork-mit-license.md ================================================ [File too large to display: 1.0 KB] ================================================ FILE: macros/Cargo.toml ================================================ [File too large to display: 432 B] ================================================ FILE: macros/src/assert.rs ================================================ [File too large to display: 10.1 KB] ================================================ FILE: macros/src/atomic_enum.rs ================================================ [File too large to display: 10.5 KB] ================================================ FILE: macros/src/ext/agg_derive.rs ================================================ [File too large to display: 3.3 KB] ================================================ FILE: macros/src/ext/match_ignore_ascii_case.rs ================================================ [File too large to display: 4.8 KB] ================================================ FILE: macros/src/ext/mod.rs ================================================ [File too large to display: 6.7 KB] ================================================ FILE: macros/src/ext/scalars.rs ================================================ [File too large to display: 2.4 KB] ================================================ FILE: macros/src/ext/vfs_derive.rs ================================================ [File too large to display: 13.0 KB] ================================================ FILE: macros/src/ext/vtab_derive.rs ================================================ [File too large to display: 15.8 KB] ================================================ FILE: macros/src/lib.rs ================================================ [File too large to display: 56.1 KB] ================================================ FILE: macros/src/test.rs ================================================ [File too large to display: 9.7 KB] ================================================ FILE: packages/turso-serverless/AGENT.md ================================================ [File too large to display: 7.4 KB] ================================================ FILE: packages/turso-serverless/README.md ================================================ [File too large to display: 3.4 KB] ================================================ FILE: packages/turso-serverless/examples/cloud-encryption/README.md ================================================ [File too large to display: 473 B] ================================================ FILE: packages/turso-serverless/examples/cloud-encryption/index.mjs ================================================ [File too large to display: 1.8 KB] ================================================ FILE: packages/turso-serverless/examples/cloud-encryption/package.json ================================================ [File too large to display: 164 B] ================================================ FILE: packages/turso-serverless/examples/remote/README.md ================================================ [File too large to display: 308 B] ================================================ FILE: packages/turso-serverless/examples/remote/index.mjs ================================================ [File too large to display: 1.0 KB] ================================================ FILE: packages/turso-serverless/examples/remote/package.json ================================================ [File too large to display: 186 B] ================================================ FILE: packages/turso-serverless/examples/remote-compat/README.md ================================================ [File too large to display: 308 B] ================================================ FILE: packages/turso-serverless/examples/remote-compat/index.mjs ================================================ [File too large to display: 566 B] ================================================ FILE: packages/turso-serverless/examples/remote-compat/package.json ================================================ [File too large to display: 185 B] ================================================ FILE: packages/turso-serverless/integration-tests/compat.test.mjs ================================================ [File too large to display: 2.0 KB] ================================================ FILE: packages/turso-serverless/integration-tests/serverless.test.mjs ================================================ [File too large to display: 3.9 KB] ================================================ FILE: packages/turso-serverless/package.json ================================================ [File too large to display: 698 B] ================================================ FILE: packages/turso-serverless/src/async-lock.ts ================================================ [File too large to display: 408 B] ================================================ FILE: packages/turso-serverless/src/compat/index.ts ================================================ [File too large to display: 29 B] ================================================ FILE: packages/turso-serverless/src/compat.ts ================================================ [File too large to display: 13.9 KB] ================================================ FILE: packages/turso-serverless/src/connection.ts ================================================ [File too large to display: 10.0 KB] ================================================ FILE: packages/turso-serverless/src/error.ts ================================================ [File too large to display: 521 B] ================================================ FILE: packages/turso-serverless/src/index.ts ================================================ [File too large to display: 322 B] ================================================ FILE: packages/turso-serverless/src/protocol.ts ================================================ [File too large to display: 7.5 KB] ================================================ FILE: packages/turso-serverless/src/session.ts ================================================ [File too large to display: 10.8 KB] ================================================ FILE: packages/turso-serverless/src/statement.ts ================================================ [File too large to display: 9.3 KB] ================================================ FILE: packages/turso-serverless/tsconfig.json ================================================ [File too large to display: 472 B] ================================================ FILE: parser/Cargo.toml ================================================ [File too large to display: 991 B] ================================================ FILE: parser/README.md ================================================ [File too large to display: 5 B] ================================================ FILE: parser/benches/parser_benchmark.rs ================================================ [File too large to display: 2.7 KB] ================================================ FILE: parser/src/ast/check.rs ================================================ [File too large to display: 4.7 KB] ================================================ FILE: parser/src/ast/fmt.rs ================================================ [File too large to display: 80.4 KB] ================================================ FILE: parser/src/ast.rs ================================================ [File too large to display: 55.2 KB] ================================================ FILE: parser/src/error.rs ================================================ [File too large to display: 3.3 KB] ================================================ FILE: parser/src/lexer.rs ================================================ [File too large to display: 50.4 KB] ================================================ FILE: parser/src/lib.rs ================================================ [File too large to display: 137 B] ================================================ FILE: parser/src/parser.rs ================================================ [File too large to display: 514.7 KB] ================================================ FILE: parser/src/token.rs ================================================ [File too large to display: 22.1 KB] ================================================ FILE: perf/clickbench/.gitignore ================================================ [File too large to display: 29 B] ================================================ FILE: perf/clickbench/benchmark.sh ================================================ [File too large to display: 1.7 KB] ================================================ FILE: perf/clickbench/create.sql ================================================ [File too large to display: 3.6 KB] ================================================ FILE: perf/clickbench/queries.sql ================================================ [File too large to display: 8.1 KB] ================================================ FILE: perf/clickbench/run.sh ================================================ [File too large to display: 1.7 KB] ================================================ FILE: perf/connection/README.md ================================================ [File too large to display: 267 B] ================================================ FILE: perf/connection/gen-database.py ================================================ [File too large to display: 1.2 KB] ================================================ FILE: perf/connection/gen-databases ================================================ [File too large to display: 357 B] ================================================ FILE: perf/connection/limbo/Cargo.toml ================================================ [File too large to display: 276 B] ================================================ FILE: perf/connection/limbo/run-benchmark.sh ================================================ [File too large to display: 481 B] ================================================ FILE: perf/connection/limbo/src/main.rs ================================================ [File too large to display: 1.7 KB] ================================================ FILE: perf/connection/plot.py ================================================ [File too large to display: 1.4 KB] ================================================ FILE: perf/connection/rusqlite/Cargo.toml ================================================ [File too large to display: 211 B] ================================================ FILE: perf/connection/rusqlite/run-benchmark.sh ================================================ [File too large to display: 484 B] ================================================ FILE: perf/connection/rusqlite/src/main.rs ================================================ [File too large to display: 1.5 KB] ================================================ FILE: perf/encryption/Cargo.toml ================================================ [File too large to display: 461 B] ================================================ FILE: perf/encryption/README.md ================================================ [File too large to display: 1.1 KB] ================================================ FILE: perf/encryption/src/main.rs ================================================ [File too large to display: 16.8 KB] ================================================ FILE: perf/graph-queries/.gitignore ================================================ [File too large to display: 45 B] ================================================ FILE: perf/graph-queries/generate_seed.py ================================================ [File too large to display: 10.4 KB] ================================================ FILE: perf/graph-queries/queries/3_aggregate_or_in.sql ================================================ [File too large to display: 570 B] ================================================ FILE: perf/graph-queries/queries/a_cooccurrence.sql ================================================ [File too large to display: 846 B] ================================================ FILE: perf/graph-queries/queries/b_or_join.sql ================================================ [File too large to display: 277 B] ================================================ FILE: perf/graph-queries/queries/c_edge_counts.sql ================================================ [File too large to display: 742 B] ================================================ FILE: perf/graph-queries/queries/d_inlist_union.sql ================================================ [File too large to display: 1.4 KB] ================================================ FILE: perf/graph-queries/queries/e_activity_agg.sql ================================================ [File too large to display: 1.6 KB] ================================================ FILE: perf/graph-queries/queries/f1_streak_current.sql ================================================ [File too large to display: 1.5 KB] ================================================ FILE: perf/graph-queries/queries/f2_streak_longest.sql ================================================ [File too large to display: 1.4 KB] ================================================ FILE: perf/latency/README.md ================================================ [File too large to display: 123 B] ================================================ FILE: perf/latency/limbo/.gitignore ================================================ [File too large to display: 12 B] ================================================ FILE: perf/latency/limbo/Cargo.toml ================================================ [File too large to display: 269 B] ================================================ FILE: perf/latency/limbo/gen-database.py ================================================ [File too large to display: 1.2 KB] ================================================ FILE: perf/latency/limbo/gen-databases ================================================ [File too large to display: 88 B] ================================================ FILE: perf/latency/limbo/plot.py ================================================ [File too large to display: 1.6 KB] ================================================ FILE: perf/latency/limbo/run-benchmark.sh ================================================ [File too large to display: 103 B] ================================================ FILE: perf/latency/limbo/rust-toolchain ================================================ [File too large to display: 19 B] ================================================ FILE: perf/latency/limbo/src/main.rs ================================================ [File too large to display: 2.9 KB] ================================================ FILE: perf/latency/rusqlite/.gitignore ================================================ [File too large to display: 12 B] ================================================ FILE: perf/latency/rusqlite/Cargo.toml ================================================ [File too large to display: 204 B] ================================================ FILE: perf/latency/rusqlite/gen-database.py ================================================ [File too large to display: 1.2 KB] ================================================ FILE: perf/latency/rusqlite/gen-databases ================================================ [File too large to display: 88 B] ================================================ FILE: perf/latency/rusqlite/plot.py ================================================ [File too large to display: 1.2 KB] ================================================ FILE: perf/latency/rusqlite/run-benchmark.sh ================================================ [File too large to display: 106 B] ================================================ FILE: perf/latency/rusqlite/src/main.rs ================================================ [File too large to display: 1.6 KB] ================================================ FILE: perf/mobibench/README.md ================================================ [File too large to display: 1.6 KB] ================================================ FILE: perf/mobibench/plot/plot.py ================================================ [File too large to display: 1.6 KB] ================================================ FILE: perf/mobibench/plot/pyproject.toml ================================================ [File too large to display: 233 B] ================================================ FILE: perf/mobibench/run-eval.sh ================================================ [File too large to display: 1.8 KB] ================================================ FILE: perf/throughput/README.md ================================================ [File too large to display: 551 B] ================================================ FILE: perf/throughput/plot/plot-compute-impact.py ================================================ [File too large to display: 2.5 KB] ================================================ FILE: perf/throughput/plot/plot-thread-scaling.py ================================================ [File too large to display: 2.4 KB] ================================================ FILE: perf/throughput/rusqlite/Cargo.toml ================================================ [File too large to display: 239 B] ================================================ FILE: perf/throughput/rusqlite/scripts/bench.sh ================================================ [File too large to display: 321 B] ================================================ FILE: perf/throughput/rusqlite/src/main.rs ================================================ [File too large to display: 4.2 KB] ================================================ FILE: perf/throughput/turso/Cargo.toml ================================================ [File too large to display: 523 B] ================================================ FILE: perf/throughput/turso/bench.sh ================================================ [File too large to display: 1.6 KB] ================================================ FILE: perf/throughput/turso/scripts/bench.sh ================================================ [File too large to display: 332 B] ================================================ FILE: perf/throughput/turso/src/main.rs ================================================ [File too large to display: 7.8 KB] ================================================ FILE: perf/tpc-h/README.md ================================================ [File too large to display: 1.3 KB] ================================================ FILE: perf/tpc-h/benchmark.sh ================================================ [File too large to display: 1.1 KB] ================================================ FILE: perf/tpc-h/compare.sh ================================================ [File too large to display: 2.2 KB] ================================================ FILE: perf/tpc-h/plot/.python-version ================================================ [File too large to display: 5 B] ================================================ FILE: perf/tpc-h/plot/plot.py ================================================ [File too large to display: 1.9 KB] ================================================ FILE: perf/tpc-h/plot/pyproject.toml ================================================ [File too large to display: 189 B] ================================================ FILE: perf/tpc-h/plot/results2csv.sh ================================================ [File too large to display: 1003 B] ================================================ FILE: perf/tpc-h/queries/1.sql ================================================ [File too large to display: 540 B] ================================================ FILE: perf/tpc-h/queries/10.sql ================================================ [File too large to display: 532 B] ================================================ FILE: perf/tpc-h/queries/11.sql ================================================ [File too large to display: 542 B] ================================================ FILE: perf/tpc-h/queries/12.sql ================================================ [File too large to display: 593 B] ================================================ FILE: perf/tpc-h/queries/13.sql ================================================ [File too large to display: 318 B] ================================================ FILE: perf/tpc-h/queries/14.sql ================================================ [File too large to display: 360 B] ================================================ FILE: perf/tpc-h/queries/15.sql ================================================ [File too large to display: 560 B] ================================================ FILE: perf/tpc-h/queries/16.sql ================================================ [File too large to display: 457 B] ================================================ FILE: perf/tpc-h/queries/17.sql ================================================ [File too large to display: 331 B] ================================================ FILE: perf/tpc-h/queries/18.sql ================================================ [File too large to display: 431 B] ================================================ FILE: perf/tpc-h/queries/19.sql ================================================ [File too large to display: 941 B] ================================================ FILE: perf/tpc-h/queries/2.sql ================================================ [File too large to display: 671 B] ================================================ FILE: perf/tpc-h/queries/20.sql ================================================ [File too large to display: 657 B] ================================================ FILE: perf/tpc-h/queries/21.sql ================================================ [File too large to display: 637 B] ================================================ FILE: perf/tpc-h/queries/22.sql ================================================ [File too large to display: 695 B] ================================================ FILE: perf/tpc-h/queries/3.sql ================================================ [File too large to display: 390 B] ================================================ FILE: perf/tpc-h/queries/4.sql ================================================ [File too large to display: 365 B] ================================================ FILE: perf/tpc-h/queries/5.sql ================================================ [File too large to display: 501 B] ================================================ FILE: perf/tpc-h/queries/6.sql ================================================ [File too large to display: 264 B] ================================================ FILE: perf/tpc-h/queries/7.sql ================================================ [File too large to display: 823 B] ================================================ FILE: perf/tpc-h/queries/8.sql ================================================ [File too large to display: 817 B] ================================================ FILE: perf/tpc-h/queries/9.sql ================================================ [File too large to display: 628 B] ================================================ FILE: perf/tpc-h/run.sh ================================================ [File too large to display: 9.3 KB] ================================================ FILE: pyproject.toml ================================================ [File too large to display: 505 B] ================================================ FILE: rust-toolchain.toml ================================================ [File too large to display: 83 B] ================================================ FILE: scripts/antithesis/launch.sh ================================================ [File too large to display: 715 B] ================================================ FILE: scripts/antithesis/publish-config.sh ================================================ [File too large to display: 432 B] ================================================ FILE: scripts/antithesis/publish-docker.sh ================================================ [File too large to display: 483 B] ================================================ FILE: scripts/antithesis/publish-workload.sh ================================================ [File too large to display: 399 B] ================================================ FILE: scripts/clean_interactions.sh ================================================ [File too large to display: 1.4 KB] ================================================ FILE: scripts/clone_test_db.sh ================================================ [File too large to display: 350 B] ================================================ FILE: scripts/corruption-debug-tools/README.md ================================================ [File too large to display: 8.7 KB] ================================================ FILE: scripts/corruption-debug-tools/find_corrupt_frame.py ================================================ [File too large to display: 4.8 KB] ================================================ FILE: scripts/corruption-debug-tools/lib/__init__.py ================================================ [File too large to display: 1.3 KB] ================================================ FILE: scripts/corruption-debug-tools/lib/diff.py ================================================ [File too large to display: 4.2 KB] ================================================ FILE: scripts/corruption-debug-tools/lib/page.py ================================================ [File too large to display: 5.5 KB] ================================================ FILE: scripts/corruption-debug-tools/lib/record.py ================================================ [File too large to display: 7.9 KB] ================================================ FILE: scripts/corruption-debug-tools/lib/wal.py ================================================ [File too large to display: 4.6 KB] ================================================ FILE: scripts/corruption-debug-tools/page_diff.py ================================================ [File too large to display: 5.9 KB] ================================================ FILE: scripts/corruption-debug-tools/page_history.py ================================================ [File too large to display: 4.8 KB] ================================================ FILE: scripts/corruption-debug-tools/page_info.py ================================================ [File too large to display: 5.0 KB] ================================================ FILE: scripts/corruption-debug-tools/track_rowid.py ================================================ [File too large to display: 5.8 KB] ================================================ FILE: scripts/corruption-debug-tools/verify_stale.py ================================================ [File too large to display: 6.8 KB] ================================================ FILE: scripts/corruption-debug-tools/wal_commits.py ================================================ [File too large to display: 4.0 KB] ================================================ FILE: scripts/corruption-debug-tools/wal_info.py ================================================ [File too large to display: 2.7 KB] ================================================ FILE: scripts/corruption_bisecter.py ================================================ [File too large to display: 6.7 KB] ================================================ FILE: scripts/diff.sh ================================================ [File too large to display: 1.5 KB] ================================================ FILE: scripts/gen-changelog.py ================================================ [File too large to display: 3.3 KB] ================================================ FILE: scripts/install-sqlite3.sh ================================================ [File too large to display: 3.7 KB] ================================================ FILE: scripts/limbo-sqlite3 ================================================ [File too large to display: 609 B] ================================================ FILE: scripts/merge-pr.py ================================================ [File too large to display: 11.4 KB] ================================================ FILE: scripts/publish-crates.sh ================================================ [File too large to display: 289 B] ================================================ FILE: scripts/pyproject.toml ================================================ [File too large to display: 138 B] ================================================ FILE: scripts/release-status.py ================================================ [File too large to display: 8.6 KB] ================================================ FILE: scripts/run-sim ================================================ [File too large to display: 195 B] ================================================ FILE: scripts/run-sim.ps1 ================================================ [File too large to display: 216 B] ================================================ FILE: scripts/run-sqlancer.sh ================================================ [File too large to display: 8.9 KB] ================================================ FILE: scripts/run-until-fail.sh ================================================ [File too large to display: 258 B] ================================================ FILE: scripts/turso-mvcc-sqlite3 ================================================ [File too large to display: 659 B] ================================================ FILE: scripts/update-version.py ================================================ [File too large to display: 8.8 KB] ================================================ FILE: sdk-kit/Cargo.toml ================================================ [File too large to display: 937 B] ================================================ FILE: sdk-kit/README.md ================================================ [File too large to display: 6.5 KB] ================================================ FILE: sdk-kit/bindgen.sh ================================================ [File too large to display: 508 B] ================================================ FILE: sdk-kit/readme-sdk-kit.mdx ================================================ [File too large to display: 977 B] ================================================ FILE: sdk-kit/src/bindings.rs ================================================ [File too large to display: 14.8 KB] ================================================ FILE: sdk-kit/src/capi.rs ================================================ [File too large to display: 47.2 KB] ================================================ FILE: sdk-kit/src/lib.rs ================================================ [File too large to display: 1.4 KB] ================================================ FILE: sdk-kit/src/rsapi.rs ================================================ [File too large to display: 62.8 KB] ================================================ FILE: sdk-kit/turso.h ================================================ [File too large to display: 12.5 KB] ================================================ FILE: sdk-kit-macros/Cargo.toml ================================================ [File too large to display: 327 B] ================================================ FILE: sdk-kit-macros/src/lib.rs ================================================ [File too large to display: 746 B] ================================================ FILE: sql_generation/Cargo.toml ================================================ [File too large to display: 790 B] ================================================ FILE: sql_generation/generation/expr.rs ================================================ [File too large to display: 12.1 KB] ================================================ FILE: sql_generation/generation/mod.rs ================================================ [File too large to display: 7.6 KB] ================================================ FILE: sql_generation/generation/opts.rs ================================================ [File too large to display: 6.9 KB] ================================================ FILE: sql_generation/generation/predicate/binary.rs ================================================ [File too large to display: 21.4 KB] ================================================ FILE: sql_generation/generation/predicate/mod.rs ================================================ [File too large to display: 11.9 KB] ================================================ FILE: sql_generation/generation/predicate/unary.rs ================================================ [File too large to display: 11.5 KB] ================================================ FILE: sql_generation/generation/query.rs ================================================ [File too large to display: 32.0 KB] ================================================ FILE: sql_generation/generation/table.rs ================================================ [File too large to display: 4.1 KB] ================================================ FILE: sql_generation/generation/value/cmp.rs ================================================ [File too large to display: 7.4 KB] ================================================ FILE: sql_generation/generation/value/mod.rs ================================================ [File too large to display: 1.8 KB] ================================================ FILE: sql_generation/generation/value/pattern.rs ================================================ [File too large to display: 1.5 KB] ================================================ FILE: sql_generation/lib.rs ================================================ [File too large to display: 35 B] ================================================ FILE: sql_generation/model/mod.rs ================================================ [File too large to display: 30 B] ================================================ FILE: sql_generation/model/query/alter_table.rs ================================================ [File too large to display: 1.7 KB] ================================================ FILE: sql_generation/model/query/create.rs ================================================ [File too large to display: 565 B] ================================================ FILE: sql_generation/model/query/create_index.rs ================================================ [File too large to display: 1.5 KB] ================================================ FILE: sql_generation/model/query/delete.rs ================================================ [File too large to display: 411 B] ================================================ FILE: sql_generation/model/query/drop.rs ================================================ [File too large to display: 317 B] ================================================ FILE: sql_generation/model/query/drop_index.rs ================================================ [File too large to display: 663 B] ================================================ FILE: sql_generation/model/query/insert.rs ================================================ [File too large to display: 2.7 KB] ================================================ FILE: sql_generation/model/query/mod.rs ================================================ [File too large to display: 425 B] ================================================ FILE: sql_generation/model/query/pragma.rs ================================================ [File too large to display: 783 B] ================================================ FILE: sql_generation/model/query/predicate.rs ================================================ [File too large to display: 5.0 KB] ================================================ FILE: sql_generation/model/query/select.rs ================================================ [File too large to display: 14.5 KB] ================================================ FILE: sql_generation/model/query/transaction.rs ================================================ [File too large to display: 905 B] ================================================ FILE: sql_generation/model/query/update.rs ================================================ [File too large to display: 1.8 KB] ================================================ FILE: sql_generation/model/table.rs ================================================ [File too large to display: 17.1 KB] ================================================ FILE: sqlite3/Cargo.toml ================================================ [File too large to display: 898 B] ================================================ FILE: sqlite3/README.md ================================================ [File too large to display: 3.3 KB] ================================================ FILE: sqlite3/build.rs ================================================ [File too large to display: 75 B] ================================================ FILE: sqlite3/cbindgen.toml ================================================ [File too large to display: 147 B] ================================================ FILE: sqlite3/examples/example.c ================================================ [File too large to display: 603 B] ================================================ FILE: sqlite3/include/sqlite3.h ================================================ [File too large to display: 10.1 KB] ================================================ FILE: sqlite3/src/lib.rs ================================================ [File too large to display: 83.2 KB] ================================================ FILE: sqlite3/tests/.gitignore ================================================ [File too large to display: 22 B] ================================================ FILE: sqlite3/tests/Makefile ================================================ [File too large to display: 162 B] ================================================ FILE: sqlite3/tests/compat/mod.rs ================================================ [File too large to display: 111.5 KB] ================================================ FILE: sqlite3/tests/sqlite3_tests.c ================================================ [File too large to display: 22.2 KB] ================================================ FILE: sync/engine/.gitignore ================================================ [File too large to display: 15 B] ================================================ FILE: sync/engine/Cargo.toml ================================================ [File too large to display: 1.0 KB] ================================================ FILE: sync/engine/src/database_replay_generator.rs ================================================ [File too large to display: 21.0 KB] ================================================ FILE: sync/engine/src/database_sync_engine.rs ================================================ [File too large to display: 44.6 KB] ================================================ FILE: sync/engine/src/database_sync_engine_io.rs ================================================ [File too large to display: 1.2 KB] ================================================ FILE: sync/engine/src/database_sync_lazy_storage.rs ================================================ [File too large to display: 26.4 KB] ================================================ FILE: sync/engine/src/database_sync_operations.rs ================================================ [File too large to display: 62.0 KB] ================================================ FILE: sync/engine/src/database_tape.rs ================================================ [File too large to display: 88.4 KB] ================================================ FILE: sync/engine/src/errors.rs ================================================ [File too large to display: 793 B] ================================================ FILE: sync/engine/src/io_operations.rs ================================================ [File too large to display: 2.3 KB] ================================================ FILE: sync/engine/src/lib.rs ================================================ [File too large to display: 1.5 KB] ================================================ FILE: sync/engine/src/server_proto.rs ================================================ [File too large to display: 13.3 KB] ================================================ FILE: sync/engine/src/sparse_io.rs ================================================ [File too large to display: 7.2 KB] ================================================ FILE: sync/engine/src/types.rs ================================================ [File too large to display: 20.7 KB] ================================================ FILE: sync/engine/src/wal_session.rs ================================================ [File too large to display: 1.5 KB] ================================================ FILE: sync/sdk-kit/Cargo.toml ================================================ [File too large to display: 813 B] ================================================ FILE: sync/sdk-kit/bindgen.sh ================================================ [File too large to display: 1.1 KB] ================================================ FILE: sync/sdk-kit/src/bindings.rs ================================================ [File too large to display: 13.5 KB] ================================================ FILE: sync/sdk-kit/src/capi.rs ================================================ [File too large to display: 55.4 KB] ================================================ FILE: sync/sdk-kit/src/lib.rs ================================================ [File too large to display: 84 B] ================================================ FILE: sync/sdk-kit/src/rsapi.rs ================================================ [File too large to display: 21.3 KB] ================================================ FILE: sync/sdk-kit/src/sync_engine_io.rs ================================================ [File too large to display: 12.3 KB] ================================================ FILE: sync/sdk-kit/src/turso_async_operation.rs ================================================ [File too large to display: 8.3 KB] ================================================ FILE: sync/sdk-kit/turso_sync.h ================================================ [File too large to display: 13.7 KB] ================================================ FILE: testing/README.md ================================================ [File too large to display: 519 B] ================================================ FILE: testing/antithesis/README.md ================================================ [File too large to display: 3.2 KB] ================================================ FILE: testing/antithesis/bank-test/anytime_validate.py ================================================ [File too large to display: 584 B] ================================================ FILE: testing/antithesis/bank-test/eventually_validate.py ================================================ [File too large to display: 587 B] ================================================ FILE: testing/antithesis/bank-test/finally_validate.py ================================================ [File too large to display: 584 B] ================================================ FILE: testing/antithesis/bank-test/first_setup.py ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/antithesis/bank-test/parallel_driver_generate_transaction.py ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/antithesis/pyproject.toml ================================================ [File too large to display: 232 B] ================================================ FILE: testing/antithesis/stress/singleton_driver_stress.sh ================================================ [File too large to display: 76 B] ================================================ FILE: testing/antithesis/stress-composer/first_setup.py ================================================ [File too large to display: 4.7 KB] ================================================ FILE: testing/antithesis/stress-composer/helper_utils.py ================================================ [File too large to display: 723 B] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_alter_table.py ================================================ [File too large to display: 4.4 KB] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_create_index.py ================================================ [File too large to display: 5.6 KB] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_create_table.py ================================================ [File too large to display: 4.3 KB] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_delete.py ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_drop_index.py ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_drop_table.py ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_insert.py ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_integritycheck.py ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_rollback.py ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_schema_rollback.py ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_update.py ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/antithesis/stress-composer/parallel_driver_wal_checkpoint.py ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/antithesis/stress-composer/shuffle-run.sh ================================================ [File too large to display: 319 B] ================================================ FILE: testing/antithesis/stress-io_uring/singleton_driver_stress.sh ================================================ [File too large to display: 91 B] ================================================ FILE: testing/antithesis/stress-io_uring-mvcc/singleton_driver_stress.sh ================================================ [File too large to display: 112 B] ================================================ FILE: testing/antithesis/stress-mvcc/singleton_driver_stress.sh ================================================ [File too large to display: 97 B] ================================================ FILE: testing/antithesis/stress-unreliable/singleton_driver_stress.sh ================================================ [File too large to display: 100 B] ================================================ FILE: testing/cli_tests/cli_test_cases.py ================================================ [File too large to display: 18.2 KB] ================================================ FILE: testing/cli_tests/collate.py ================================================ [File too large to display: 5.0 KB] ================================================ FILE: testing/cli_tests/console.py ================================================ [File too large to display: 2.8 KB] ================================================ FILE: testing/cli_tests/constraint.py ================================================ [File too large to display: 9.4 KB] ================================================ FILE: testing/cli_tests/extensions.py ================================================ [File too large to display: 31.6 KB] ================================================ FILE: testing/cli_tests/memory.py ================================================ [File too large to display: 11.0 KB] ================================================ FILE: testing/cli_tests/mvcc.py ================================================ [File too large to display: 887 B] ================================================ FILE: testing/cli_tests/sqlite_bench.py ================================================ [File too large to display: 6.7 KB] ================================================ FILE: testing/cli_tests/test_files/test.csv ================================================ [File too large to display: 32 B] ================================================ FILE: testing/cli_tests/test_files/test_w_header.csv ================================================ [File too large to display: 79 B] ================================================ FILE: testing/cli_tests/test_turso_cli.py ================================================ [File too large to display: 7.4 KB] ================================================ FILE: testing/cli_tests/update.py ================================================ [File too large to display: 4.0 KB] ================================================ FILE: testing/cli_tests/vfs_bench.py ================================================ [File too large to display: 3.5 KB] ================================================ FILE: testing/cli_tests/write.py ================================================ [File too large to display: 4.9 KB] ================================================ FILE: testing/concurrent-simulator/Cargo.toml ================================================ [File too large to display: 914 B] ================================================ FILE: testing/concurrent-simulator/README.md ================================================ [File too large to display: 938 B] ================================================ FILE: testing/concurrent-simulator/bin/explore ================================================ [File too large to display: 974 B] ================================================ FILE: testing/concurrent-simulator/bin/run ================================================ [File too large to display: 872 B] ================================================ FILE: testing/concurrent-simulator/bin/run-elle ================================================ [File too large to display: 4.5 KB] ================================================ FILE: testing/concurrent-simulator/chaotic_elle.rs ================================================ [File too large to display: 12.4 KB] ================================================ FILE: testing/concurrent-simulator/elle.rs ================================================ [File too large to display: 9.4 KB] ================================================ FILE: testing/concurrent-simulator/io.rs ================================================ [File too large to display: 12.5 KB] ================================================ FILE: testing/concurrent-simulator/lib.rs ================================================ [File too large to display: 40.8 KB] ================================================ FILE: testing/concurrent-simulator/main.rs ================================================ [File too large to display: 8.9 KB] ================================================ FILE: testing/concurrent-simulator/operations.rs ================================================ [File too large to display: 8.8 KB] ================================================ FILE: testing/concurrent-simulator/properties.rs ================================================ [File too large to display: 25.8 KB] ================================================ FILE: testing/concurrent-simulator/regression_tests.rs ================================================ [File too large to display: 4.5 KB] ================================================ FILE: testing/concurrent-simulator/workloads.rs ================================================ [File too large to display: 13.2 KB] ================================================ FILE: testing/differential-oracle/fuzzer/Cargo.toml ================================================ [File too large to display: 989 B] ================================================ FILE: testing/differential-oracle/fuzzer/custom_types_fuzzer.rs ================================================ [File too large to display: 20.5 KB] ================================================ FILE: testing/differential-oracle/fuzzer/docker-runner/Dockerfile ================================================ [File too large to display: 2.6 KB] ================================================ FILE: testing/differential-oracle/fuzzer/docker-runner/docker-entrypoint.fuzzer.ts ================================================ [File too large to display: 7.0 KB] ================================================ FILE: testing/differential-oracle/fuzzer/docker-runner/github.ts ================================================ [File too large to display: 6.4 KB] ================================================ FILE: testing/differential-oracle/fuzzer/docker-runner/levenshtein.ts ================================================ [File too large to display: 1.0 KB] ================================================ FILE: testing/differential-oracle/fuzzer/docker-runner/logParse.ts ================================================ [File too large to display: 1.9 KB] ================================================ FILE: testing/differential-oracle/fuzzer/docker-runner/package.json ================================================ [File too large to display: 395 B] ================================================ FILE: testing/differential-oracle/fuzzer/docker-runner/random.ts ================================================ [File too large to display: 219 B] ================================================ FILE: testing/differential-oracle/fuzzer/docker-runner/slack.ts ================================================ [File too large to display: 4.9 KB] ================================================ FILE: testing/differential-oracle/fuzzer/docker-runner/tsconfig.json ================================================ [File too large to display: 222 B] ================================================ FILE: testing/differential-oracle/fuzzer/generate.rs ================================================ [File too large to display: 7.2 KB] ================================================ FILE: testing/differential-oracle/fuzzer/lib.rs ================================================ [File too large to display: 620 B] ================================================ FILE: testing/differential-oracle/fuzzer/main.rs ================================================ [File too large to display: 8.5 KB] ================================================ FILE: testing/differential-oracle/fuzzer/memory/file.rs ================================================ [File too large to display: 3.2 KB] ================================================ FILE: testing/differential-oracle/fuzzer/memory/io.rs ================================================ [File too large to display: 3.6 KB] ================================================ FILE: testing/differential-oracle/fuzzer/memory/mod.rs ================================================ [File too large to display: 214 B] ================================================ FILE: testing/differential-oracle/fuzzer/oracle.rs ================================================ [File too large to display: 12.9 KB] ================================================ FILE: testing/differential-oracle/fuzzer/printf_fuzzer.rs ================================================ [File too large to display: 16.3 KB] ================================================ FILE: testing/differential-oracle/fuzzer/printf_gen.rs ================================================ [File too large to display: 24.5 KB] ================================================ FILE: testing/differential-oracle/fuzzer/runner.rs ================================================ [File too large to display: 20.5 KB] ================================================ FILE: testing/differential-oracle/fuzzer/schema.rs ================================================ [File too large to display: 17.3 KB] ================================================ FILE: testing/differential-oracle/sql_gen/Cargo.toml ================================================ [File too large to display: 624 B] ================================================ FILE: testing/differential-oracle/sql_gen/src/ast.rs ================================================ [File too large to display: 79.8 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/builder.rs ================================================ [File too large to display: 3.3 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/capabilities.rs ================================================ [File too large to display: 8.5 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/context.rs ================================================ [File too large to display: 18.0 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/error.rs ================================================ [File too large to display: 4.2 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/functions.rs ================================================ [File too large to display: 17.1 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/generate/expr.rs ================================================ [File too large to display: 35.9 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/generate/literal.rs ================================================ [File too large to display: 9.7 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/generate/mod.rs ================================================ [File too large to display: 904 B] ================================================ FILE: testing/differential-oracle/sql_gen/src/generate/select.rs ================================================ [File too large to display: 66.7 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/generate/stmt.rs ================================================ [File too large to display: 56.2 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/lib.rs ================================================ [File too large to display: 7.7 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/policy.rs ================================================ [File too large to display: 65.2 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/schema.rs ================================================ [File too large to display: 10.3 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/strategy.rs ================================================ [File too large to display: 5.3 KB] ================================================ FILE: testing/differential-oracle/sql_gen/src/trace.rs ================================================ [File too large to display: 21.2 KB] ================================================ FILE: testing/differential-oracle/sql_gen_macros/Cargo.toml ================================================ [File too large to display: 255 B] ================================================ FILE: testing/differential-oracle/sql_gen_macros/src/lib.rs ================================================ [File too large to display: 2.9 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/Cargo.toml ================================================ [File too large to display: 363 B] ================================================ FILE: testing/differential-oracle/sql_gen_prop/alter_table.rs ================================================ [File too large to display: 14.0 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/create_index.rs ================================================ [File too large to display: 7.7 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/create_table.rs ================================================ [File too large to display: 31.0 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/create_trigger.rs ================================================ [File too large to display: 15.8 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/cte.rs ================================================ [File too large to display: 19.6 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/delete.rs ================================================ [File too large to display: 2.2 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/drop_index.rs ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/drop_table.rs ================================================ [File too large to display: 2.3 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/drop_trigger.rs ================================================ [File too large to display: 2.1 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/expression.rs ================================================ [File too large to display: 61.3 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/function.rs ================================================ [File too large to display: 25.6 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/generator.rs ================================================ [File too large to display: 5.3 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/insert.rs ================================================ [File too large to display: 6.7 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/lib.rs ================================================ [File too large to display: 10.9 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/profile.rs ================================================ [File too large to display: 25.6 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/result.rs ================================================ [File too large to display: 5.0 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/schema.rs ================================================ [File too large to display: 9.1 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/select.rs ================================================ [File too large to display: 26.8 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/statement.rs ================================================ [File too large to display: 16.7 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/transaction.rs ================================================ [File too large to display: 4.5 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/update.rs ================================================ [File too large to display: 8.1 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/utility.rs ================================================ [File too large to display: 3.4 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/value.rs ================================================ [File too large to display: 6.6 KB] ================================================ FILE: testing/differential-oracle/sql_gen_prop/view.rs ================================================ [File too large to display: 4.5 KB] ================================================ FILE: testing/javascript/__test__/async.test.js ================================================ [File too large to display: 19.1 KB] ================================================ FILE: testing/javascript/__test__/sync.test.js ================================================ [File too large to display: 17.4 KB] ================================================ FILE: testing/javascript/artifacts/basic-test.sql ================================================ [File too large to display: 147 B] ================================================ FILE: testing/javascript/package.json ================================================ [File too large to display: 743 B] ================================================ FILE: testing/pyproject.toml ================================================ [File too large to display: 855 B] ================================================ FILE: testing/simulator/.gitignore ================================================ [File too large to display: 14 B] ================================================ FILE: testing/simulator/COVERAGE.md ================================================ [File too large to display: 31.1 KB] ================================================ FILE: testing/simulator/Cargo.toml ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/simulator/README.md ================================================ [File too large to display: 7.0 KB] ================================================ FILE: testing/simulator/ROADMAP.md ================================================ [File too large to display: 11.1 KB] ================================================ FILE: testing/simulator/common/mod.rs ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/simulator/generation/mod.rs ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/simulator/generation/plan.rs ================================================ [File too large to display: 15.1 KB] ================================================ FILE: testing/simulator/generation/property.rs ================================================ [File too large to display: 80.7 KB] ================================================ FILE: testing/simulator/generation/query.rs ================================================ [File too large to display: 7.5 KB] ================================================ FILE: testing/simulator/main.rs ================================================ [File too large to display: 21.0 KB] ================================================ FILE: testing/simulator/model/interactions.rs ================================================ [File too large to display: 29.4 KB] ================================================ FILE: testing/simulator/model/metrics.rs ================================================ [File too large to display: 5.9 KB] ================================================ FILE: testing/simulator/model/mod.rs ================================================ [File too large to display: 43.9 KB] ================================================ FILE: testing/simulator/model/property.rs ================================================ [File too large to display: 9.7 KB] ================================================ FILE: testing/simulator/plan_to_test.rs ================================================ [File too large to display: 6.4 KB] ================================================ FILE: testing/simulator/profiles/io.rs ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/simulator/profiles/mod.rs ================================================ [File too large to display: 7.6 KB] ================================================ FILE: testing/simulator/profiles/query.rs ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/simulator/run-miri.sh ================================================ [File too large to display: 699 B] ================================================ FILE: testing/simulator/runner/bugbase.rs ================================================ [File too large to display: 11.8 KB] ================================================ FILE: testing/simulator/runner/cli.rs ================================================ [File too large to display: 9.5 KB] ================================================ FILE: testing/simulator/runner/clock.rs ================================================ [File too large to display: 834 B] ================================================ FILE: testing/simulator/runner/differential.rs ================================================ [File too large to display: 11.6 KB] ================================================ FILE: testing/simulator/runner/doublecheck.rs ================================================ [File too large to display: 10.5 KB] ================================================ FILE: testing/simulator/runner/env.rs ================================================ [File too large to display: 47.6 KB] ================================================ FILE: testing/simulator/runner/execution.rs ================================================ [File too large to display: 18.4 KB] ================================================ FILE: testing/simulator/runner/file.rs ================================================ [File too large to display: 8.9 KB] ================================================ FILE: testing/simulator/runner/io.rs ================================================ [File too large to display: 5.1 KB] ================================================ FILE: testing/simulator/runner/memory/file.rs ================================================ [File too large to display: 7.3 KB] ================================================ FILE: testing/simulator/runner/memory/io.rs ================================================ [File too large to display: 9.2 KB] ================================================ FILE: testing/simulator/runner/memory/mod.rs ================================================ [File too large to display: 95 B] ================================================ FILE: testing/simulator/runner/memory/mvcc_recovery.rs ================================================ [File too large to display: 20.3 KB] ================================================ FILE: testing/simulator/runner/memory/statement_abandon.rs ================================================ [File too large to display: 8.7 KB] ================================================ FILE: testing/simulator/runner/mod.rs ================================================ [File too large to display: 705 B] ================================================ FILE: testing/simulator/shrink/mod.rs ================================================ [File too large to display: 14 B] ================================================ FILE: testing/simulator/shrink/plan.rs ================================================ [File too large to display: 12.6 KB] ================================================ FILE: testing/simulator/simulator-docker-runner/Dockerfile.simulator ================================================ [File too large to display: 3.1 KB] ================================================ FILE: testing/simulator/simulator-docker-runner/README.MD ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/simulator/simulator-docker-runner/docker-entrypoint.simulator.ts ================================================ [File too large to display: 7.8 KB] ================================================ FILE: testing/simulator/simulator-docker-runner/github.ts ================================================ [File too large to display: 6.3 KB] ================================================ FILE: testing/simulator/simulator-docker-runner/levenshtein.test.ts ================================================ [File too large to display: 1011 B] ================================================ FILE: testing/simulator/simulator-docker-runner/levenshtein.ts ================================================ [File too large to display: 1.0 KB] ================================================ FILE: testing/simulator/simulator-docker-runner/logParse.ts ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/simulator/simulator-docker-runner/package.json ================================================ [File too large to display: 387 B] ================================================ FILE: testing/simulator/simulator-docker-runner/random.ts ================================================ [File too large to display: 218 B] ================================================ FILE: testing/simulator/simulator-docker-runner/slack.ts ================================================ [File too large to display: 4.8 KB] ================================================ FILE: testing/simulator/simulator-docker-runner/tsconfig.json ================================================ [File too large to display: 522 B] ================================================ FILE: testing/sqlancer/README.md ================================================ [File too large to display: 597 B] ================================================ FILE: testing/sqlancer/patches/LimboProvider.java ================================================ [File too large to display: 12.5 KB] ================================================ FILE: testing/sqlancer/patches/LimboSchema.java ================================================ [File too large to display: 5.2 KB] ================================================ FILE: testing/sqlancer/patches/SQLite3Schema.patch ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqlancer/sqlancer-runner/Dockerfile.sqlancer ================================================ [File too large to display: 5.6 KB] ================================================ FILE: testing/sqlancer/sqlancer-runner/README.md ================================================ [File too large to display: 2.5 KB] ================================================ FILE: testing/sqlancer/sqlancer-runner/corruptionAnalysis.ts ================================================ [File too large to display: 7.5 KB] ================================================ FILE: testing/sqlancer/sqlancer-runner/docker-entrypoint.sqlancer.ts ================================================ [File too large to display: 18.6 KB] ================================================ FILE: testing/sqlancer/sqlancer-runner/github.ts ================================================ [File too large to display: 9.7 KB] ================================================ FILE: testing/sqlancer/sqlancer-runner/levenshtein.ts ================================================ [File too large to display: 1.0 KB] ================================================ FILE: testing/sqlancer/sqlancer-runner/logParse.ts ================================================ [File too large to display: 9.1 KB] ================================================ FILE: testing/sqlancer/sqlancer-runner/package.json ================================================ [File too large to display: 370 B] ================================================ FILE: testing/sqlancer/sqlancer-runner/slack.ts ================================================ [File too large to display: 5.1 KB] ================================================ FILE: testing/sqlancer/sqlancer-runner/tsconfig.json ================================================ [File too large to display: 522 B] ================================================ FILE: testing/sqlite3/README.md ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqlite3/all.test ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqlite3/alter.test ================================================ [File too large to display: 26.5 KB] ================================================ FILE: testing/sqlite3/alter2.test ================================================ [File too large to display: 12.7 KB] ================================================ FILE: testing/sqlite3/alter3.test ================================================ [File too large to display: 10.5 KB] ================================================ FILE: testing/sqlite3/alter4.test ================================================ [File too large to display: 10.1 KB] ================================================ FILE: testing/sqlite3/func.test ================================================ [File too large to display: 44.9 KB] ================================================ FILE: testing/sqlite3/func2.test ================================================ [File too large to display: 15.3 KB] ================================================ FILE: testing/sqlite3/func3.test ================================================ [File too large to display: 5.8 KB] ================================================ FILE: testing/sqlite3/func4.test ================================================ [File too large to display: 21.1 KB] ================================================ FILE: testing/sqlite3/func5.test ================================================ [File too large to display: 2.3 KB] ================================================ FILE: testing/sqlite3/func6.test ================================================ [File too large to display: 5.2 KB] ================================================ FILE: testing/sqlite3/func7.test ================================================ [File too large to display: 6.5 KB] ================================================ FILE: testing/sqlite3/func8.test ================================================ [File too large to display: 2.4 KB] ================================================ FILE: testing/sqlite3/func9.test ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqlite3/insert.test ================================================ [File too large to display: 16.6 KB] ================================================ FILE: testing/sqlite3/insert2.test ================================================ [File too large to display: 7.3 KB] ================================================ FILE: testing/sqlite3/insert3.test ================================================ [File too large to display: 5.2 KB] ================================================ FILE: testing/sqlite3/insert4.test ================================================ [File too large to display: 16.1 KB] ================================================ FILE: testing/sqlite3/insert5.test ================================================ [File too large to display: 3.1 KB] ================================================ FILE: testing/sqlite3/join.test ================================================ [File too large to display: 38.6 KB] ================================================ FILE: testing/sqlite3/join2.test ================================================ [File too large to display: 12.6 KB] ================================================ FILE: testing/sqlite3/join3.test ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqlite3/join4.test ================================================ [File too large to display: 2.9 KB] ================================================ FILE: testing/sqlite3/join5.test ================================================ [File too large to display: 12.7 KB] ================================================ FILE: testing/sqlite3/join6.test ================================================ [File too large to display: 3.9 KB] ================================================ FILE: testing/sqlite3/join7.test ================================================ [File too large to display: 9.4 KB] ================================================ FILE: testing/sqlite3/join8.test ================================================ [File too large to display: 25.0 KB] ================================================ FILE: testing/sqlite3/join9.test ================================================ [File too large to display: 18.4 KB] ================================================ FILE: testing/sqlite3/joinA.test ================================================ [File too large to display: 8.2 KB] ================================================ FILE: testing/sqlite3/joinB.test ================================================ [File too large to display: 148.7 KB] ================================================ FILE: testing/sqlite3/joinC.test ================================================ [File too large to display: 81.9 KB] ================================================ FILE: testing/sqlite3/joinD.test ================================================ [File too large to display: 1.2 MB] ================================================ FILE: testing/sqlite3/joinE.test ================================================ [File too large to display: 8.0 KB] ================================================ FILE: testing/sqlite3/joinF.test ================================================ [File too large to display: 14.1 KB] ================================================ FILE: testing/sqlite3/joinH.test ================================================ [File too large to display: 10.9 KB] ================================================ FILE: testing/sqlite3/select1.test ================================================ [File too large to display: 31.3 KB] ================================================ FILE: testing/sqlite3/select2.test ================================================ [File too large to display: 4.5 KB] ================================================ FILE: testing/sqlite3/select3.test ================================================ [File too large to display: 10.7 KB] ================================================ FILE: testing/sqlite3/select4.test ================================================ [File too large to display: 26.3 KB] ================================================ FILE: testing/sqlite3/select5.test ================================================ [File too large to display: 6.5 KB] ================================================ FILE: testing/sqlite3/select6.test ================================================ [File too large to display: 17.3 KB] ================================================ FILE: testing/sqlite3/select7.test ================================================ [File too large to display: 7.3 KB] ================================================ FILE: testing/sqlite3/select8.test ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqlite3/select9.test ================================================ [File too large to display: 16.1 KB] ================================================ FILE: testing/sqlite3/selectA.test ================================================ [File too large to display: 45.0 KB] ================================================ FILE: testing/sqlite3/selectB.test ================================================ [File too large to display: 10.6 KB] ================================================ FILE: testing/sqlite3/selectC.test ================================================ [File too large to display: 6.8 KB] ================================================ FILE: testing/sqlite3/selectD.test ================================================ [File too large to display: 5.1 KB] ================================================ FILE: testing/sqlite3/selectE.test ================================================ [File too large to display: 3.0 KB] ================================================ FILE: testing/sqlite3/selectF.test ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqlite3/selectG.test ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/sqlite3/selectH.test ================================================ [File too large to display: 3.8 KB] ================================================ FILE: testing/sqlite3/tester.tcl ================================================ [File too large to display: 10.9 KB] ================================================ FILE: testing/sqlite_test_ext/Cargo.toml ================================================ [File too large to display: 291 B] ================================================ FILE: testing/sqlite_test_ext/build.rs ================================================ [File too large to display: 275 B] ================================================ FILE: testing/sqlite_test_ext/include/sqlite3.h ================================================ [File too large to display: 646.4 KB] ================================================ FILE: testing/sqlite_test_ext/include/sqlite3ext.h ================================================ [File too large to display: 37.4 KB] ================================================ FILE: testing/sqlite_test_ext/src/kvstore.c ================================================ [File too large to display: 8.8 KB] ================================================ FILE: testing/sqlite_test_ext/src/lib.rs ================================================ [File too large to display: 669 B] ================================================ FILE: testing/sqlright/.gitignore ================================================ [File too large to display: 126 B] ================================================ FILE: testing/sqlright/Dockerfile ================================================ [File too large to display: 3.5 KB] ================================================ FILE: testing/sqlright/README.md ================================================ [File too large to display: 3.9 KB] ================================================ FILE: testing/sqlright/collect_coverage.sh ================================================ [File too large to display: 2.9 KB] ================================================ FILE: testing/sqlright/crash_reports/.gitignore ================================================ [File too large to display: 106 B] ================================================ FILE: testing/sqlright/crash_reports/README.md ================================================ [File too large to display: 2.4 KB] ================================================ FILE: testing/sqlright/crash_reports/collect_crashes.py ================================================ [File too large to display: 7.3 KB] ================================================ FILE: testing/sqlright/crash_reports/lib/__init__.py ================================================ [File too large to display: 71 B] ================================================ FILE: testing/sqlright/crash_reports/lib/database.py ================================================ [File too large to display: 14.2 KB] ================================================ FILE: testing/sqlright/crash_reports/lib/executor.py ================================================ [File too large to display: 9.4 KB] ================================================ FILE: testing/sqlright/crash_reports/lib/parser.py ================================================ [File too large to display: 2.8 KB] ================================================ FILE: testing/sqlright/crash_reports/lib/scanner.py ================================================ [File too large to display: 8.1 KB] ================================================ FILE: testing/sqlright/crash_reports/query_crashes.py ================================================ [File too large to display: 8.6 KB] ================================================ FILE: testing/sqlright/crash_reports/schema.sql ================================================ [File too large to display: 4.5 KB] ================================================ FILE: testing/sqlright/patches/0001-turso-increase-map-size-and-fix-gcc13.patch ================================================ [File too large to display: 3.0 KB] ================================================ FILE: testing/sqlright/patches/0002-turso-fix-macos-bison-compatibility.patch ================================================ [File too large to display: 853 B] ================================================ FILE: testing/sqlright/patches/0003-turso-fix-macos-compilation-errors.patch ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqlright/run.sh ================================================ [File too large to display: 5.3 KB] ================================================ FILE: testing/sqlright/setup.sh ================================================ [File too large to display: 2.9 KB] ================================================ FILE: testing/sqltests/Cargo.toml ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/Makefile ================================================ [File too large to display: 3.8 KB] ================================================ FILE: testing/sqltests/database/.gitignore ================================================ [File too large to display: 17 B] ================================================ FILE: testing/sqltests/docs/README.md ================================================ [File too large to display: 3.7 KB] ================================================ FILE: testing/sqltests/docs/adding-backends.md ================================================ [File too large to display: 10.5 KB] ================================================ FILE: testing/sqltests/docs/architecture.md ================================================ [File too large to display: 9.5 KB] ================================================ FILE: testing/sqltests/docs/backends/cli.md ================================================ [File too large to display: 6.6 KB] ================================================ FILE: testing/sqltests/docs/cli-usage.md ================================================ [File too large to display: 6.7 KB] ================================================ FILE: testing/sqltests/docs/dsl-spec.md ================================================ [File too large to display: 15.6 KB] ================================================ FILE: testing/sqltests/docs/parallelism.md ================================================ [File too large to display: 13.6 KB] ================================================ FILE: testing/sqltests/docs/snapshot-testing.md ================================================ [File too large to display: 10.1 KB] ================================================ FILE: testing/sqltests/examples/basic.sqltest ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/examples/joins.sqltest ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqltests/examples/snapshot_example.sqltest ================================================ [File too large to display: 816 B] ================================================ FILE: testing/sqltests/examples/snapshots/snapshot_example__query-plan-by-id.snap ================================================ [File too large to display: 1016 B] ================================================ FILE: testing/sqltests/examples/snapshots/snapshot_example__query-plan-by-name.snap ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/sqltests/src/backends/cli.rs ================================================ [File too large to display: 11.0 KB] ================================================ FILE: testing/sqltests/src/backends/js.rs ================================================ [File too large to display: 9.4 KB] ================================================ FILE: testing/sqltests/src/backends/mod.rs ================================================ [File too large to display: 7.0 KB] ================================================ FILE: testing/sqltests/src/backends/rust.rs ================================================ [File too large to display: 15.1 KB] ================================================ FILE: testing/sqltests/src/comparison/exact.rs ================================================ [File too large to display: 3.6 KB] ================================================ FILE: testing/sqltests/src/comparison/mod.rs ================================================ [File too large to display: 5.5 KB] ================================================ FILE: testing/sqltests/src/comparison/pattern.rs ================================================ [File too large to display: 2.5 KB] ================================================ FILE: testing/sqltests/src/comparison/unordered.rs ================================================ [File too large to display: 3.7 KB] ================================================ FILE: testing/sqltests/src/generator/mod.rs ================================================ [File too large to display: 49.8 KB] ================================================ FILE: testing/sqltests/src/lib.rs ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqltests/src/main.rs ================================================ [File too large to display: 27.6 KB] ================================================ FILE: testing/sqltests/src/output/json.rs ================================================ [File too large to display: 3.5 KB] ================================================ FILE: testing/sqltests/src/output/mod.rs ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqltests/src/output/pretty.rs ================================================ [File too large to display: 7.5 KB] ================================================ FILE: testing/sqltests/src/parser/ast.rs ================================================ [File too large to display: 9.0 KB] ================================================ FILE: testing/sqltests/src/parser/lexer.rs ================================================ [File too large to display: 15.6 KB] ================================================ FILE: testing/sqltests/src/parser/mod.rs ================================================ [File too large to display: 48.1 KB] ================================================ FILE: testing/sqltests/src/parser/sql_complete.rs ================================================ [File too large to display: 10.9 KB] ================================================ FILE: testing/sqltests/src/runner/mod.rs ================================================ [File too large to display: 40.1 KB] ================================================ FILE: testing/sqltests/src/snapshot/mod.rs ================================================ [File too large to display: 54.9 KB] ================================================ FILE: testing/sqltests/src/tcl_converter/mod.rs ================================================ [File too large to display: 6.4 KB] ================================================ FILE: testing/sqltests/src/tcl_converter/parser.rs ================================================ [File too large to display: 41.0 KB] ================================================ FILE: testing/sqltests/src/tcl_converter/utils.rs ================================================ [File too large to display: 15.9 KB] ================================================ FILE: testing/sqltests/syntax-highlighter/vscode/README.md ================================================ [File too large to display: 2.1 KB] ================================================ FILE: testing/sqltests/syntax-highlighter/vscode/language-configuration.json ================================================ [File too large to display: 367 B] ================================================ FILE: testing/sqltests/syntax-highlighter/vscode/package.json ================================================ [File too large to display: 918 B] ================================================ FILE: testing/sqltests/syntax-highlighter/vscode/syntaxes/sqltest.tmLanguage.json ================================================ [File too large to display: 8.5 KB] ================================================ FILE: testing/sqltests/tests/affinity.sqltest ================================================ [File too large to display: 12.6 KB] ================================================ FILE: testing/sqltests/tests/agg-functions/agg-extreme-exponent.sqltest ================================================ [File too large to display: 783 B] ================================================ FILE: testing/sqltests/tests/agg-functions/default.sqltest ================================================ [File too large to display: 7.5 KB] ================================================ FILE: testing/sqltests/tests/agg-functions/group-concat-types.sqltest ================================================ [File too large to display: 8.7 KB] ================================================ FILE: testing/sqltests/tests/agg-functions/is-true.sqltest ================================================ [File too large to display: 356 B] ================================================ FILE: testing/sqltests/tests/agg-functions/memory.sqltest ================================================ [File too large to display: 2.7 KB] ================================================ FILE: testing/sqltests/tests/agg-functions/sum-blob-types.sqltest ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/sqltests/tests/agg-functions/sum-inf-cancel.sqltest ================================================ [File too large to display: 865 B] ================================================ FILE: testing/sqltests/tests/agg-functions/sum-large-float.sqltest ================================================ [File too large to display: 434 B] ================================================ FILE: testing/sqltests/tests/agg-functions/sum-text-types.sqltest ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/alter_rename_column_partial_idx.sqltest ================================================ [File too large to display: 2.1 KB] ================================================ FILE: testing/sqltests/tests/alter_table.sqltest ================================================ [File too large to display: 60.2 KB] ================================================ FILE: testing/sqltests/tests/autoincr.sqltest ================================================ [File too large to display: 8.9 KB] ================================================ FILE: testing/sqltests/tests/before-update-trigger-correlated-subquery.sqltest ================================================ [File too large to display: 3.5 KB] ================================================ FILE: testing/sqltests/tests/big.ignored_for_now ================================================ [File too large to display: 463 B] ================================================ FILE: testing/sqltests/tests/boolean.sqltest ================================================ [File too large to display: 3.6 KB] ================================================ FILE: testing/sqltests/tests/btree-backward-scan.sqltest ================================================ [File too large to display: 701 B] ================================================ FILE: testing/sqltests/tests/btree-large-page-overflow.sqltest ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqltests/tests/btree_dump.sqltest ================================================ [File too large to display: 6.1 KB] ================================================ FILE: testing/sqltests/tests/changes.sqltest ================================================ [File too large to display: 5.8 KB] ================================================ FILE: testing/sqltests/tests/char.sqltest ================================================ [File too large to display: 1.4 KB] ================================================ FILE: testing/sqltests/tests/check_constraint.sqltest ================================================ [File too large to display: 47.8 KB] ================================================ FILE: testing/sqltests/tests/cmdlineshell.sqltest ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/sqltests/tests/coalesce/default.sqltest ================================================ [File too large to display: 425 B] ================================================ FILE: testing/sqltests/tests/coalesce/memory.sqltest ================================================ [File too large to display: 1.0 KB] ================================================ FILE: testing/sqltests/tests/collate.sqltest ================================================ [File too large to display: 37.0 KB] ================================================ FILE: testing/sqltests/tests/column_name_case.sqltest ================================================ [File too large to display: 2.5 KB] ================================================ FILE: testing/sqltests/tests/compare.sqltest ================================================ [File too large to display: 7.6 KB] ================================================ FILE: testing/sqltests/tests/composite-index-sort-elim.sqltest ================================================ [File too large to display: 18.5 KB] ================================================ FILE: testing/sqltests/tests/concat.sqltest ================================================ [File too large to display: 552 B] ================================================ FILE: testing/sqltests/tests/correlated-subquery-hash-join.sqltest ================================================ [File too large to display: 2.7 KB] ================================================ FILE: testing/sqltests/tests/correlated-subquery-in-clause.sqltest ================================================ [File too large to display: 696 B] ================================================ FILE: testing/sqltests/tests/correlated-subquery-window.sqltest ================================================ [File too large to display: 998 B] ================================================ FILE: testing/sqltests/tests/create_index.sqltest ================================================ [File too large to display: 7.6 KB] ================================================ FILE: testing/sqltests/tests/create_table.sqltest ================================================ [File too large to display: 5.5 KB] ================================================ FILE: testing/sqltests/tests/cross_join.sqltest ================================================ [File too large to display: 10.4 KB] ================================================ FILE: testing/sqltests/tests/cte-real-affinity-join.sqltest ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/cte-union-all-aggregate-literals.sqltest ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/cte.sqltest ================================================ [File too large to display: 35.9 KB] ================================================ FILE: testing/sqltests/tests/cte_cardinality.sqltest ================================================ [File too large to display: 6.4 KB] ================================================ FILE: testing/sqltests/tests/cte_expressions.sqltest ================================================ [File too large to display: 12.4 KB] ================================================ FILE: testing/sqltests/tests/default_value.sqltest ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/sqltests/tests/delete-correlated-subquery-rowid.sqltest ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/sqltests/tests/delete-correlated-subquery.sqltest ================================================ [File too large to display: 2.5 KB] ================================================ FILE: testing/sqltests/tests/delete-limit-offset.sqltest ================================================ [File too large to display: 2.3 KB] ================================================ FILE: testing/sqltests/tests/delete.sqltest ================================================ [File too large to display: 8.7 KB] ================================================ FILE: testing/sqltests/tests/distinct.sqltest ================================================ [File too large to display: 5.8 KB] ================================================ FILE: testing/sqltests/tests/drop_index.sqltest ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/drop_table.sqltest ================================================ [File too large to display: 3.1 KB] ================================================ FILE: testing/sqltests/tests/duplicate-trigger-names.sqltest ================================================ [File too large to display: 1.4 KB] ================================================ FILE: testing/sqltests/tests/expr-index-correlated-subquery.sqltest ================================================ [File too large to display: 4.3 KB] ================================================ FILE: testing/sqltests/tests/foreign_keys.sqltest ================================================ [File too large to display: 85.8 KB] ================================================ FILE: testing/sqltests/tests/generate_series.sqltest ================================================ [File too large to display: 667 B] ================================================ FILE: testing/sqltests/tests/glob/default.sqltest ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/glob/memory.sqltest ================================================ [File too large to display: 4.7 KB] ================================================ FILE: testing/sqltests/tests/graph_traversal_text_pk.sqltest ================================================ [File too large to display: 6.7 KB] ================================================ FILE: testing/sqltests/tests/group-by-expression-index.sqltest ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/groupby/constant-expr.sqltest ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqltests/tests/groupby/default.sqltest ================================================ [File too large to display: 10.1 KB] ================================================ FILE: testing/sqltests/tests/groupby/duplicate-order-by.sqltest ================================================ [File too large to display: 662 B] ================================================ FILE: testing/sqltests/tests/groupby/memory.sqltest ================================================ [File too large to display: 9.8 KB] ================================================ FILE: testing/sqltests/tests/hex-real-compare.sqltest ================================================ [File too large to display: 823 B] ================================================ FILE: testing/sqltests/tests/in-index-seek.sqltest ================================================ [File too large to display: 20.2 KB] ================================================ FILE: testing/sqltests/tests/in-null-or.sqltest ================================================ [File too large to display: 2.5 KB] ================================================ FILE: testing/sqltests/tests/in-subquery-ungrouped-aggregate.sqltest ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/indexed_by.sqltest ================================================ [File too large to display: 3.2 KB] ================================================ FILE: testing/sqltests/tests/insert-cte-compound.sqltest ================================================ [File too large to display: 7.3 KB] ================================================ FILE: testing/sqltests/tests/insert.sqltest ================================================ [File too large to display: 50.7 KB] ================================================ FILE: testing/sqltests/tests/insert_autorowid_index.sqltest ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqltests/tests/insert_not_null_default_index.sqltest ================================================ [File too large to display: 2.3 KB] ================================================ FILE: testing/sqltests/tests/insert_or_ignore_autoincrement.sqltest ================================================ [File too large to display: 1.0 KB] ================================================ FILE: testing/sqltests/tests/int64-overflow-seek.sqltest ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/integrity_check/expression_index.sqltest ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/integrity_check/memory.sqltest ================================================ [File too large to display: 4.8 KB] ================================================ FILE: testing/sqltests/tests/integrity_check/parity_check_constraint.sqltest ================================================ [File too large to display: 233 B] ================================================ FILE: testing/sqltests/tests/integrity_check/parity_corrupt_expression_index.sqltest ================================================ [File too large to display: 403 B] ================================================ FILE: testing/sqltests/tests/integrity_check/parity_corrupt_index.sqltest ================================================ [File too large to display: 409 B] ================================================ FILE: testing/sqltests/tests/integrity_check/parity_corrupt_partial_index.sqltest ================================================ [File too large to display: 360 B] ================================================ FILE: testing/sqltests/tests/integrity_check/parity_freelist_count_mismatch.sqltest ================================================ [File too large to display: 464 B] ================================================ FILE: testing/sqltests/tests/integrity_check/parity_freelist_trunk_corrupt.sqltest ================================================ [File too large to display: 535 B] ================================================ FILE: testing/sqltests/tests/integrity_check/parity_missing_unique_index.sqltest ================================================ [File too large to display: 343 B] ================================================ FILE: testing/sqltests/tests/integrity_check/parity_non_unique_index.sqltest ================================================ [File too large to display: 369 B] ================================================ FILE: testing/sqltests/tests/integrity_check/parity_not_null_violation.sqltest ================================================ [File too large to display: 313 B] ================================================ FILE: testing/sqltests/tests/integrity_check/parity_overflow_list_length_mismatch.sqltest ================================================ [File too large to display: 614 B] ================================================ FILE: testing/sqltests/tests/integrity_check/parity_quick_check_constraint.sqltest ================================================ [File too large to display: 231 B] ================================================ FILE: testing/sqltests/tests/integrity_check/snapshot_plans.sqltest ================================================ [File too large to display: 1.4 KB] ================================================ FILE: testing/sqltests/tests/integrity_check/snapshots/snapshot_plans__integrity-check-multi-table-vdbe.snap ================================================ [File too large to display: 15.0 KB] ================================================ FILE: testing/sqltests/tests/integrity_check/snapshots/snapshot_plans__integrity-check-vdbe.snap ================================================ [File too large to display: 10.8 KB] ================================================ FILE: testing/sqltests/tests/integrity_check/snapshots/snapshot_plans__quick-check-multi-table-vdbe.snap ================================================ [File too large to display: 10.1 KB] ================================================ FILE: testing/sqltests/tests/integrity_check/snapshots/snapshot_plans__quick-check-vdbe.snap ================================================ [File too large to display: 6.9 KB] ================================================ FILE: testing/sqltests/tests/issue_5116.sqltest ================================================ [File too large to display: 448 B] ================================================ FILE: testing/sqltests/tests/issue_5212.sqltest ================================================ [File too large to display: 588 B] ================================================ FILE: testing/sqltests/tests/join/default.sqltest ================================================ [File too large to display: 12.3 KB] ================================================ FILE: testing/sqltests/tests/join/hash.sqltest ================================================ [File too large to display: 30.1 KB] ================================================ FILE: testing/sqltests/tests/join/memory.sqltest ================================================ [File too large to display: 22.0 KB] ================================================ FILE: testing/sqltests/tests/join/natural_join_no_common.sqltest ================================================ [File too large to display: 1.0 KB] ================================================ FILE: testing/sqltests/tests/join/outer_hash_join.sqltest ================================================ [File too large to display: 43.2 KB] ================================================ FILE: testing/sqltests/tests/json/default.sqltest ================================================ [File too large to display: 59.1 KB] ================================================ FILE: testing/sqltests/tests/json/json_subtype_strip.sqltest ================================================ [File too large to display: 8.4 KB] ================================================ FILE: testing/sqltests/tests/json/json_tree.sqltest ================================================ [File too large to display: 1.4 KB] ================================================ FILE: testing/sqltests/tests/json/memory.sqltest ================================================ [File too large to display: 2.4 KB] ================================================ FILE: testing/sqltests/tests/last_insert_rowid.sqltest ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/left-join-case-iif-null-masking.sqltest ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/left-join-ifnull-optimization.sqltest ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/like.sqltest ================================================ [File too large to display: 5.6 KB] ================================================ FILE: testing/sqltests/tests/limit.sqltest ================================================ [File too large to display: 689 B] ================================================ FILE: testing/sqltests/tests/literal.sqltest ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/math/default.sqltest ================================================ [File too large to display: 4.5 KB] ================================================ FILE: testing/sqltests/tests/math/degrees-radians-precision.sqltest ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/math/memory.sqltest ================================================ [File too large to display: 32.0 KB] ================================================ FILE: testing/sqltests/tests/matview-create-index.sqltest ================================================ [File too large to display: 655 B] ================================================ FILE: testing/sqltests/tests/multi_index_dml.sqltest ================================================ [File too large to display: 12.0 KB] ================================================ FILE: testing/sqltests/tests/multi_index_intersection.sqltest ================================================ [File too large to display: 3.8 KB] ================================================ FILE: testing/sqltests/tests/multi_index_or_adversarial.sqltest ================================================ [File too large to display: 5.0 KB] ================================================ FILE: testing/sqltests/tests/multi_index_or_adversarial_extra.sqltest ================================================ [File too large to display: 14.8 KB] ================================================ FILE: testing/sqltests/tests/multi_index_or_compound.sqltest ================================================ [File too large to display: 9.6 KB] ================================================ FILE: testing/sqltests/tests/multi_index_or_join.sqltest ================================================ [File too large to display: 3.0 KB] ================================================ FILE: testing/sqltests/tests/mvcc-update-noop.sqltest ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/sqltests/tests/negative_zero.sqltest ================================================ [File too large to display: 2.2 KB] ================================================ FILE: testing/sqltests/tests/not_between.sqltest ================================================ [File too large to display: 2.6 KB] ================================================ FILE: testing/sqltests/tests/null/default.sqltest ================================================ [File too large to display: 432 B] ================================================ FILE: testing/sqltests/tests/null/memory.sqltest ================================================ [File too large to display: 5.3 KB] ================================================ FILE: testing/sqltests/tests/offset/default.sqltest ================================================ [File too large to display: 965 B] ================================================ FILE: testing/sqltests/tests/offset/memory.sqltest ================================================ [File too large to display: 2.2 KB] ================================================ FILE: testing/sqltests/tests/on_conflict.sqltest ================================================ [File too large to display: 25.5 KB] ================================================ FILE: testing/sqltests/tests/on_conflict_constraint_def.sqltest ================================================ [File too large to display: 18.5 KB] ================================================ FILE: testing/sqltests/tests/orderby/default.sqltest ================================================ [File too large to display: 6.2 KB] ================================================ FILE: testing/sqltests/tests/orderby/memory.sqltest ================================================ [File too large to display: 3.5 KB] ================================================ FILE: testing/sqltests/tests/orderby/orderby_plan.sqltest ================================================ [File too large to display: 533 B] ================================================ FILE: testing/sqltests/tests/orderby/snapshots/orderby_plan__orderby_a_desc_uses_sorter.snap ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/sqltests/tests/orderby/snapshots/orderby_plan__orderby_a_uses_sorter.snap ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/sqltests/tests/orderby/snapshots/orderby_plan__orderby_rowid_desc_no_sorter.snap ================================================ [File too large to display: 728 B] ================================================ FILE: testing/sqltests/tests/orderby/snapshots/orderby_plan__orderby_rowid_no_sorter.snap ================================================ [File too large to display: 739 B] ================================================ FILE: testing/sqltests/tests/partial_idx.sqltest ================================================ [File too large to display: 33.9 KB] ================================================ FILE: testing/sqltests/tests/placeholder.sqltest ================================================ [File too large to display: 77 B] ================================================ FILE: testing/sqltests/tests/pragma/default.sqltest ================================================ [File too large to display: 5.4 KB] ================================================ FILE: testing/sqltests/tests/pragma/index_info.sqltest ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/sqltests/tests/pragma/index_list.sqltest ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/pragma/index_xinfo.sqltest ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/pragma/memory.sqltest ================================================ [File too large to display: 5.5 KB] ================================================ FILE: testing/sqltests/tests/pragma/require_where.sqltest ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/sqltests/tests/pragma/table_list.sqltest ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/pragma/user_version_10.sqltest ================================================ [File too large to display: 137 B] ================================================ FILE: testing/sqltests/tests/pragma_query_only.sqltest ================================================ [File too large to display: 435 B] ================================================ FILE: testing/sqltests/tests/returning-fk-constraint.sqltest ================================================ [File too large to display: 5.3 KB] ================================================ FILE: testing/sqltests/tests/returning.sqltest ================================================ [File too large to display: 61.2 KB] ================================================ FILE: testing/sqltests/tests/rollback.sqltest ================================================ [File too large to display: 3.5 KB] ================================================ FILE: testing/sqltests/tests/row-value-in.sqltest ================================================ [File too large to display: 3.1 KB] ================================================ FILE: testing/sqltests/tests/savepoint.sqltest ================================================ [File too large to display: 4.8 KB] ================================================ FILE: testing/sqltests/tests/scalar-functions-datetime.sqltest ================================================ [File too large to display: 39.5 KB] ================================================ FILE: testing/sqltests/tests/scalar-functions-format.sqltest ================================================ [File too large to display: 6.3 KB] ================================================ FILE: testing/sqltests/tests/scalar-functions-printf.sqltest ================================================ [File too large to display: 20.6 KB] ================================================ FILE: testing/sqltests/tests/scalar-functions.sqltest ================================================ [File too large to display: 27.2 KB] ================================================ FILE: testing/sqltests/tests/select/default.sqltest ================================================ [File too large to display: 10.1 KB] ================================================ FILE: testing/sqltests/tests/select/memory.sqltest ================================================ [File too large to display: 44.9 KB] ================================================ FILE: testing/sqltests/tests/select/simple_min_max.sqltest ================================================ [File too large to display: 14.5 KB] ================================================ FILE: testing/sqltests/tests/simple-count-optimization.sqltest ================================================ [File too large to display: 600 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/aggregation.sqltest ================================================ [File too large to display: 6.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__aggregation-with-join.snap ================================================ [File too large to display: 7.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__count-column.snap ================================================ [File too large to display: 962 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__count-distinct.snap ================================================ [File too large to display: 6.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__count-star-plus-expr.snap ================================================ [File too large to display: 1.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__distinct-count-with-join.snap ================================================ [File too large to display: 7.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__group-by-with-having.snap ================================================ [File too large to display: 5.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__min-max-per-group.snap ================================================ [File too large to display: 4.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__min-max-with-index.snap ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__multi-column-group-by.snap ================================================ [File too large to display: 4.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__multiple-aggregates.snap ================================================ [File too large to display: 6.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__nested-aggregation.snap ================================================ [File too large to display: 5.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__simple-count-mixed-case.snap ================================================ [File too large to display: 772 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__simple-count-no-args.snap ================================================ [File too large to display: 771 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__simple-count-star.snap ================================================ [File too large to display: 772 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__simple-group-by-single-column.snap ================================================ [File too large to display: 4.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/aggregation/snapshots/aggregation__whole-table-aggregates.snap ================================================ [File too large to display: 2.9 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/analyze.sqltest ================================================ [File too large to display: 14.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__aggregate-after-analyze.snap ================================================ [File too large to display: 4.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__analyze-all-stat1-exists.snap ================================================ [File too large to display: 4.9 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__analyze-all.snap ================================================ [File too large to display: 22.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__analyze-database.snap ================================================ [File too large to display: 22.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__analyze-empty-table.snap ================================================ [File too large to display: 10.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__analyze-index-stat1-exists.snap ================================================ [File too large to display: 3.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__analyze-specific-index.snap ================================================ [File too large to display: 6.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__analyze-stat1-exists.snap ================================================ [File too large to display: 4.9 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__join-after-analyze.snap ================================================ [File too large to display: 2.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__join-aggregate-after-analyze.snap ================================================ [File too large to display: 5.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__max-after-analyze.snap ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__min-after-analyze.snap ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__order-by-index-after-analyze.snap ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-after-analyze-category.snap ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-after-analyze-equality.snap ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-after-analyze-range.snap ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-composite-full-after-analyze.snap ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-composite-partial-after-analyze.snap ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-composite-range-after-analyze.snap ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-for-nulls-after-analyze.snap ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-identical-composite-after-analyze.snap ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-identical-values-after-analyze.snap ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-three-column-first-only-after-analyze.snap ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-three-column-full-after-analyze.snap ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-three-column-partial-after-analyze.snap ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/analyze/snapshots/analyze__query-with-nulls-after-analyze.snap ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/indexes.sqltest ================================================ [File too large to display: 22.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__aggregate-over-in-subquery-or-in-subquery.snap ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__composite-index-first-column-only.snap ================================================ [File too large to display: 317 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__composite-index-full-usage.snap ================================================ [File too large to display: 360 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__composite-index-order-by-match.snap ================================================ [File too large to display: 343 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__composite-index-range-on-second.snap ================================================ [File too large to display: 413 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__composite-index-second-column-only.snap ================================================ [File too large to display: 271 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__covering-index-all-columns.snap ================================================ [File too large to display: 368 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__covering-index-subset.snap ================================================ [File too large to display: 318 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__covering-index-with-aggregation.snap ================================================ [File too large to display: 364 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__full-scan-function-on-indexed.snap ================================================ [File too large to display: 240 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__full-scan-leading-wildcard.snap ================================================ [File too large to display: 234 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__full-scan-no-index.snap ================================================ [File too large to display: 227 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__full-scan-non-indexed-column.snap ================================================ [File too large to display: 239 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__full-scan-with-aggregation.snap ================================================ [File too large to display: 278 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-clause-multiple-values.snap ================================================ [File too large to display: 332 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-composite-collation-mismatch-larger-table.snap ================================================ [File too large to display: 406 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-composite-collation-mismatch.snap ================================================ [File too large to display: 296 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-list-collation-match-explicit-nocase.snap ================================================ [File too large to display: 277 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-list-collation-mismatch.snap ================================================ [File too large to display: 262 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-list-composite-prefix.snap ================================================ [File too large to display: 329 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-list-index-seek.snap ================================================ [File too large to display: 320 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-list-no-index-scan.snap ================================================ [File too large to display: 241 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-list-rowid-seek.snap ================================================ [File too large to display: 272 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-subquery-collation-mismatch.snap ================================================ [File too large to display: 365 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-subquery-or-in-list-left-join.snap ================================================ [File too large to display: 508 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__in-subquery-or-in-subquery-indexed.snap ================================================ [File too large to display: 488 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-for-correlated-max-materialized-cte-bytecode.snap ================================================ [File too large to display: 3.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-for-correlated-max-materialized-cte.snap ================================================ [File too large to display: 559 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-for-max-bytecode.snap ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-for-max-materialized-cte-bytecode.snap ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-for-max-materialized-cte-no-where-bytecode.snap ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-for-max-materialized-cte-no-where.snap ================================================ [File too large to display: 370 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-for-max-materialized-cte.snap ================================================ [File too large to display: 388 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-for-max.snap ================================================ [File too large to display: 249 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-for-min-bytecode.snap ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-for-min.snap ================================================ [File too large to display: 249 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-for-order-by.snap ================================================ [File too large to display: 268 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__index-not-equal.snap ================================================ [File too large to display: 245 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__indexed-by-delete-category.snap ================================================ [File too large to display: 312 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__indexed-by-delete-sku.snap ================================================ [File too large to display: 283 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__indexed-by-join-both.snap ================================================ [File too large to display: 510 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__indexed-by-join-one-side.snap ================================================ [File too large to display: 459 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__indexed-by-no-where-clause.snap ================================================ [File too large to display: 271 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__indexed-by-select-category.snap ================================================ [File too large to display: 327 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__indexed-by-select-price-range.snap ================================================ [File too large to display: 305 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__indexed-by-select-sku.snap ================================================ [File too large to display: 302 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__indexed-by-three-way-join.snap ================================================ [File too large to display: 664 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__indexed-by-update-price.snap ================================================ [File too large to display: 300 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__indexed-by-update-sku.snap ================================================ [File too large to display: 298 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__join-with-index.snap ================================================ [File too large to display: 437 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__max-with-equality-prefix-bytecode.snap ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__multiple-indexes-choice.snap ================================================ [File too large to display: 332 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__non-covering-needs-table-lookup.snap ================================================ [File too large to display: 382 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__not-indexed-delete.snap ================================================ [File too large to display: 228 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__not-indexed-select.snap ================================================ [File too large to display: 247 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__not-indexed-update.snap ================================================ [File too large to display: 243 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__or-different-columns.snap ================================================ [File too large to display: 326 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__or-indexed-and-non-indexed.snap ================================================ [File too large to display: 286 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__or-same-column.snap ================================================ [File too large to display: 333 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__or-with-and-combinations.snap ================================================ [File too large to display: 387 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__primary-key-lookup.snap ================================================ [File too large to display: 264 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__range-scan-between.snap ================================================ [File too large to display: 312 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__range-scan-greater-equal.snap ================================================ [File too large to display: 285 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__range-scan-greater-than.snap ================================================ [File too large to display: 283 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__range-scan-less-equal.snap ================================================ [File too large to display: 285 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__range-scan-less-than.snap ================================================ [File too large to display: 283 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__range-scan-timestamp.snap ================================================ [File too large to display: 362 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__range-scan-with-equality.snap ================================================ [File too large to display: 341 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__rowid-range-no-alias.snap ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__rowid-seek-no-alias.snap ================================================ [File too large to display: 270 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__simple-index-equality-lookup.snap ================================================ [File too large to display: 274 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__simple-index-with-filter.snap ================================================ [File too large to display: 292 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__subquery-with-index.snap ================================================ [File too large to display: 445 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__unordered-correlated-max-materialized-cte-bytecode.snap ================================================ [File too large to display: 4.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/indexes/snapshots/indexes__unordered-materialized-cte-max-bytecode.snap ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/joins.sqltest ================================================ [File too large to display: 26.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__bigass-three-table-join-rowid-seek.snap ================================================ [File too large to display: 720 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__cross-join-explicit.snap ================================================ [File too large to display: 1.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__cross-join-implicit.snap ================================================ [File too large to display: 358 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__cross-join-limited.snap ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__cross-join-with-filter.snap ================================================ [File too large to display: 2.9 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__four-table-join-with-aggregation.snap ================================================ [File too large to display: 1.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__hash-join-full-outer.snap ================================================ [File too large to display: 7.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__hash-join-inner-basic.snap ================================================ [File too large to display: 4.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__hash-join-left-outer-agg.snap ================================================ [File too large to display: 11.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__hash-join-left-outer.snap ================================================ [File too large to display: 6.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__hash-join-three-table.snap ================================================ [File too large to display: 10.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__hash-join-with-aggregation.snap ================================================ [File too large to display: 9.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__hash-join-with-order-by.snap ================================================ [File too large to display: 5.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__index-assisted-join-order-items.snap ================================================ [File too large to display: 702 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__index-assisted-join-product-lookup.snap ================================================ [File too large to display: 863 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__index-assisted-join-range-scan.snap ================================================ [File too large to display: 592 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__index-assisted-join-single-customer.snap ================================================ [File too large to display: 533 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__join-multiple-conditions.snap ================================================ [File too large to display: 560 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__join-with-exists.snap ================================================ [File too large to display: 545 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__join-with-subquery.snap ================================================ [File too large to display: 905 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__left-outer-join-basic.snap ================================================ [File too large to display: 2.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__left-outer-join-chained.snap ================================================ [File too large to display: 4.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__left-outer-join-find-nulls.snap ================================================ [File too large to display: 2.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__left-outer-join-with-aggregation.snap ================================================ [File too large to display: 7.9 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__mixed-join-types.snap ================================================ [File too large to display: 762 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__multi-table-join-complex-filter.snap ================================================ [File too large to display: 887 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__natural-join.snap ================================================ [File too large to display: 302 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__prefer-constant-bound-index-over-join-dependent.snap ================================================ [File too large to display: 321 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__self-join-category-hierarchy.snap ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__self-join-employee-manager.snap ================================================ [File too large to display: 1.9 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__self-join-find-subordinates.snap ================================================ [File too large to display: 640 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__self-join-prefer-join-dependent-index.snap ================================================ [File too large to display: 1.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__self-join-same-department.snap ================================================ [File too large to display: 488 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__three-table-join-explicit.snap ================================================ [File too large to display: 806 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__three-table-join-rowid-seek.snap ================================================ [File too large to display: 706 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__three-table-join.snap ================================================ [File too large to display: 738 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__two-table-inner-join-basic.snap ================================================ [File too large to display: 458 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__two-table-inner-join-explicit.snap ================================================ [File too large to display: 474 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__two-table-inner-join-with-filter.snap ================================================ [File too large to display: 530 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/joins/snapshots/joins__using-clause-join.snap ================================================ [File too large to display: 392 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/modifications.sqltest ================================================ [File too large to display: 5.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__delete-with-subquery.snap ================================================ [File too large to display: 3.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__delete-with-where.snap ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-multiple-rows.snap ================================================ [File too large to display: 3.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-or-abort-multi-unique.snap ================================================ [File too large to display: 3.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-or-fail-multi-unique.snap ================================================ [File too large to display: 3.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-or-ignore-multi-unique.snap ================================================ [File too large to display: 3.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-or-ignore.snap ================================================ [File too large to display: 2.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-or-replace-multi-notnull-default-partial.snap ================================================ [File too large to display: 2.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-or-replace-multi-notnull-default.snap ================================================ [File too large to display: 2.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-or-replace-multi-unique.snap ================================================ [File too large to display: 4.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-or-replace.snap ================================================ [File too large to display: 3.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-or-rollback-multi-unique.snap ================================================ [File too large to display: 3.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-select.snap ================================================ [File too large to display: 3.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__insert-single-row.snap ================================================ [File too large to display: 2.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/modifications/snapshots/modifications__update-simple.snap ================================================ [File too large to display: 2.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/returning/returning.sqltest ================================================ [File too large to display: 1.9 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/returning/snapshots/returning__delete-returning-expr.snap ================================================ [File too large to display: 2.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/returning/snapshots/returning__delete-returning-star.snap ================================================ [File too large to display: 2.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/returning/snapshots/returning__insert-returning-expr.snap ================================================ [File too large to display: 3.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/returning/snapshots/returning__insert-returning-star.snap ================================================ [File too large to display: 3.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/returning/snapshots/returning__update-returning-expr.snap ================================================ [File too large to display: 2.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/returning/snapshots/returning__update-returning-star.snap ================================================ [File too large to display: 2.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/returning/snapshots/returning__upsert-returning.snap ================================================ [File too large to display: 3.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/setops.sqltest ================================================ [File too large to display: 5.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__compound-mixed-union.snap ================================================ [File too large to display: 2.9 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__compound-union-all-three-way.snap ================================================ [File too large to display: 2.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__compound-union-except.snap ================================================ [File too large to display: 2.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__compound-union-intersect.snap ================================================ [File too large to display: 2.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__except-basic.snap ================================================ [File too large to display: 1.9 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__except-multiple-columns.snap ================================================ [File too large to display: 2.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__intersect-basic.snap ================================================ [File too large to display: 2.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__intersect-multiple-columns.snap ================================================ [File too large to display: 2.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__subquery-with-union.snap ================================================ [File too large to display: 3.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__union-all-basic.snap ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__union-all-filtered.snap ================================================ [File too large to display: 2.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__union-distinct.snap ================================================ [File too large to display: 2.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__union-explicit-distinct.snap ================================================ [File too large to display: 2.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__union-subquery-aggregation.snap ================================================ [File too large to display: 6.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/setops/snapshots/setops__union-with-limit.snap ================================================ [File too large to display: 2.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__large-offset-small-limit.snap ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__limit-with-offset.snap ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__limit-without-order.snap ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-desc.snap ================================================ [File too large to display: 287 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-expression.snap ================================================ [File too large to display: 2.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-index-with-range.snap ================================================ [File too large to display: 340 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-limit-one.snap ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-mixed-direction.snap ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-multiple-columns.snap ================================================ [File too large to display: 306 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-nullable-desc.snap ================================================ [File too large to display: 301 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-single-column.snap ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-with-index.snap ================================================ [File too large to display: 277 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-with-limit.snap ================================================ [File too large to display: 297 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-with-nulls.snap ================================================ [File too large to display: 284 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__order-by-with-where.snap ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__sort-elim-all-eq-order-non-index.snap ================================================ [File too large to display: 430 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__sort-elim-eq-col-desc-direction.snap ================================================ [File too large to display: 350 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__sort-elim-eq-prefix-order-suffix.snap ================================================ [File too large to display: 337 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__sort-elim-expr-index-eq-prefix-order-suffix.snap ================================================ [File too large to display: 369 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__sort-elim-expr-index-group-by-eq-prefix.snap ================================================ [File too large to display: 396 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__sort-elim-left-join-outer-rowid-suffix.snap ================================================ [File too large to display: 520 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__sort-elim-no-eq-order-non-leading.snap ================================================ [File too large to display: 268 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__sort-elim-range-prefix-order-suffix.snap ================================================ [File too large to display: 341 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/snapshots/sorting__sort-elim-two-eq-order-last.snap ================================================ [File too large to display: 364 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/sorting/sorting.sqltest ================================================ [File too large to display: 7.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__correlated-subquery-count.snap ================================================ [File too large to display: 2.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__correlated-subquery-salary.snap ================================================ [File too large to display: 3.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__correlated-subquery-where.snap ================================================ [File too large to display: 3.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-aggregation.snap ================================================ [File too large to display: 761 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-correlated-range-probe.snap ================================================ [File too large to display: 944 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-in-subquery-and-main.snap ================================================ [File too large to display: 7.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-materialized-order-aggregate-opposite-dir.snap ================================================ [File too large to display: 400 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-materialized-order-aggregate-same-dir.snap ================================================ [File too large to display: 399 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-materialized-order-multi-col-all-flipped.snap ================================================ [File too large to display: 379 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-materialized-order-multi-col-mixed-clean-flip.snap ================================================ [File too large to display: 379 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-materialized-order-multi-col-partial-mismatch.snap ================================================ [File too large to display: 405 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-materialized-order-multi-col-same-dir.snap ================================================ [File too large to display: 377 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-materialized-order-outer-beyond-cte.snap ================================================ [File too large to display: 397 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-materialized-order-prefix-match.snap ================================================ [File too large to display: 370 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-materialized-order-reused-opposite-dir.snap ================================================ [File too large to display: 389 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-materialized-order-reused-same-dir.snap ================================================ [File too large to display: 390 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-materialized-scalar-probe.snap ================================================ [File too large to display: 2.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-multiple-chained.snap ================================================ [File too large to display: 882 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-multiple-independent.snap ================================================ [File too large to display: 6.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-referenced-multiple-times.snap ================================================ [File too large to display: 8.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-referencing-previous.snap ================================================ [File too large to display: 8.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-self-join.snap ================================================ [File too large to display: 786 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-simple.snap ================================================ [File too large to display: 460 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-with-correlated-subquery.snap ================================================ [File too large to display: 7.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__cte-with-exists.snap ================================================ [File too large to display: 593 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__delete-returning-target-selfread.snap ================================================ [File too large to display: 3.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__derived-table-join.snap ================================================ [File too large to display: 5.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__derived-table-nested.snap ================================================ [File too large to display: 6.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__derived-table-simple.snap ================================================ [File too large to display: 445 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__exists-correlated.snap ================================================ [File too large to display: 2.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__exists-multiple-conditions.snap ================================================ [File too large to display: 3.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__grouped-correlated-subquery-having.snap ================================================ [File too large to display: 5.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__grouped-correlated-subquery-order-by.snap ================================================ [File too large to display: 6.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__in-subquery-aggregation.snap ================================================ [File too large to display: 5.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__in-subquery-simple.snap ================================================ [File too large to display: 461 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__not-exists-never-ordered.snap ================================================ [File too large to display: 440 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__not-exists-no-orders.snap ================================================ [File too large to display: 428 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__not-in-subquery.snap ================================================ [File too large to display: 486 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__run-length-current-window.snap ================================================ [File too large to display: 3.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__run-length-longest-window.snap ================================================ [File too large to display: 3.0 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__scalar-subquery-cross-table.snap ================================================ [File too large to display: 2.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__scalar-subquery-max-price.snap ================================================ [File too large to display: 2.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__scalar-subquery-simple-count.snap ================================================ [File too large to display: 1.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__subquery-within-cte.snap ================================================ [File too large to display: 4.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subqueries__update-returning-target-selfread.snap ================================================ [File too large to display: 3.9 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subquery-sort-elision__eqp-multi-hop-outer-orderby-elided.snap ================================================ [File too large to display: 447 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subquery-sort-elision__eqp-outer-orderby-elided-by-cte.snap ================================================ [File too large to display: 363 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subquery-sort-elision__eqp-outer-orderby-elided-by-group.snap ================================================ [File too large to display: 377 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subquery-sort-elision__eqp-outer-orderby-not-elided-no-cte-order.snap ================================================ [File too large to display: 357 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subquery-sort-elision__eqp-outer-orderby-not-elided-partial.snap ================================================ [File too large to display: 393 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subquery-sort-elision__eqp-window-sort-elided-by-cte-order.snap ================================================ [File too large to display: 430 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subquery-sort-elision__eqp-window-sort-elided-by-group.snap ================================================ [File too large to display: 446 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subquery-sort-elision__eqp-window-sort-elided-multi-hop.snap ================================================ [File too large to display: 517 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subquery-sort-elision__eqp-window-sort-not-elided-no-cte-order.snap ================================================ [File too large to display: 424 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/snapshots/subquery-sort-elision__eqp-window-sort-not-elided-partial.snap ================================================ [File too large to display: 466 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/subqueries.sqltest ================================================ [File too large to display: 25.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/subqueries/subquery-sort-elision.sqltest ================================================ [File too large to display: 12.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q1-pricing-summary.snap ================================================ [File too large to display: 10.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q10-returned-item.snap ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q11-important-stock.snap ================================================ [File too large to display: 9.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q12-shipping-modes.snap ================================================ [File too large to display: 11.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q13-customer-distribution.snap ================================================ [File too large to display: 19.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q14-promotion-effect.snap ================================================ [File too large to display: 4.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q15-top-supplier.snap ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q16-parts-supplier.snap ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q17-small-quantity-order.snap ================================================ [File too large to display: 5.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q18-large-volume-customer.snap ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q19-discounted-revenue.snap ================================================ [File too large to display: 10.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q2-minimum-cost-supplier.snap ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q20-potential-part-promotion.snap ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q21-suppliers-kept-waiting.snap ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q22-global-sales-opportunity.snap ================================================ [File too large to display: 1.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q3-shipping-priority.snap ================================================ [File too large to display: 932 B] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q4-order-priority.snap ================================================ [File too large to display: 7.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q5-local-supplier-volume.snap ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q6-forecasting-revenue.snap ================================================ [File too large to display: 3.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q7-volume-shipping.snap ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q8-national-market-share.snap ================================================ [File too large to display: 1.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/snapshots/tpch__q9-product-type-profit.snap ================================================ [File too large to display: 1.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/tpch/tpch.sqltest ================================================ [File too large to display: 21.9 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/windows/snapshots/windows__aggregate-windows.snap ================================================ [File too large to display: 9.5 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/windows/snapshots/windows__multiple-windows.snap ================================================ [File too large to display: 24.8 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/windows/snapshots/windows__percent-of-total.snap ================================================ [File too large to display: 8.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/windows/snapshots/windows__row-number-basic.snap ================================================ [File too large to display: 4.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/windows/snapshots/windows__running-total.snap ================================================ [File too large to display: 10.3 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/windows/snapshots/windows__window-order-by.snap ================================================ [File too large to display: 5.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/windows/snapshots/windows__window-over-grouped.snap ================================================ [File too large to display: 8.4 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/windows/snapshots/windows__window-with-subquery.snap ================================================ [File too large to display: 10.6 KB] ================================================ FILE: testing/sqltests/tests/snapshot_tests/windows/windows.sqltest ================================================ [File too large to display: 9.0 KB] ================================================ FILE: testing/sqltests/tests/snapshots/cte_cardinality__cte-graph-traversal-multi-index-or.snap ================================================ [File too large to display: 725 B] ================================================ FILE: testing/sqltests/tests/snapshots/cte_cardinality__small-cte-materialized-big-cte-outer.snap ================================================ [File too large to display: 510 B] ================================================ FILE: testing/sqltests/tests/snapshots/cte_cardinality__small-cte-reordered-to-outer.snap ================================================ [File too large to display: 419 B] ================================================ FILE: testing/sqltests/tests/snapshots/cte_cardinality__small-subquery-reordered-to-outer.snap ================================================ [File too large to display: 360 B] ================================================ FILE: testing/sqltests/tests/snapshots/cte_cardinality__three-ctes-big-outer-others-materialized.snap ================================================ [File too large to display: 704 B] ================================================ FILE: testing/sqltests/tests/snapshots/graph_traversal_text_pk__cte-neighbor-traversal-text-pk.snap ================================================ [File too large to display: 907 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_dml__delete-multi-index-and-access-shape.snap ================================================ [File too large to display: 251 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_dml__delete-multi-index-compound-or-access-shape.snap ================================================ [File too large to display: 298 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_dml__delete-multi-index-or-access-shape.snap ================================================ [File too large to display: 247 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_dml__delete-multi-index-or-one-row-subset-access-shape.snap ================================================ [File too large to display: 262 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_dml__delete-multi-index-or-two-row-subset-access-shape.snap ================================================ [File too large to display: 263 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_dml__multi-index-or-access-shape.snap ================================================ [File too large to display: 250 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_dml__update-multi-index-and-access-shape.snap ================================================ [File too large to display: 266 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_dml__update-multi-index-or-access-shape.snap ================================================ [File too large to display: 260 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_intersection__composite-index-preferred-over-intersection-eqp.snap ================================================ [File too large to display: 278 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_intersection__multi-index-and-2-way-eqp.snap ================================================ [File too large to display: 260 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_or_compound__compound-or-benchmark-shape-eqp.snap ================================================ [File too large to display: 612 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_or_compound__compound-or-compound-branch-seek-eqp.snap ================================================ [File too large to display: 514 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_or_compound__compound-or-correlated-subquery-eqp.snap ================================================ [File too large to display: 374 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_or_compound__compound-or-cross-table-eqp.snap ================================================ [File too large to display: 560 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_or_compound__compound-or-eqp.snap ================================================ [File too large to display: 594 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_or_compound__compound-or-mixed-residual-eqp.snap ================================================ [File too large to display: 615 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_or_compound__compound-or-rowid-branch-eqp.snap ================================================ [File too large to display: 322 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_or_join__cte-join-with-or-plan.snap ================================================ [File too large to display: 575 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_or_join__multi-index-or-in-join-parenthesized-plan.snap ================================================ [File too large to display: 432 B] ================================================ FILE: testing/sqltests/tests/snapshots/multi_index_or_join__multi-index-or-in-join-plan.snap ================================================ [File too large to display: 446 B] ================================================ FILE: testing/sqltests/tests/strict.sqltest ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/subquery/cte_chain_regression.sqltest ================================================ [File too large to display: 2.4 KB] ================================================ FILE: testing/sqltests/tests/subquery/cte_materialization_adversarial.sqltest ================================================ [File too large to display: 47.0 KB] ================================================ FILE: testing/sqltests/tests/subquery/default.sqltest ================================================ [File too large to display: 10.7 KB] ================================================ FILE: testing/sqltests/tests/subquery/expressions.sqltest ================================================ [File too large to display: 14.5 KB] ================================================ FILE: testing/sqltests/tests/subquery/materialized_cte_seek.sqltest ================================================ [File too large to display: 13.8 KB] ================================================ FILE: testing/sqltests/tests/subquery/memory.sqltest ================================================ [File too large to display: 42.5 KB] ================================================ FILE: testing/sqltests/tests/subquery/subquery_cte_equivalence_tests.sqltest ================================================ [File too large to display: 52.7 KB] ================================================ FILE: testing/sqltests/tests/total-changes.sqltest ================================================ [File too large to display: 2.2 KB] ================================================ FILE: testing/sqltests/tests/transactions.sqltest ================================================ [File too large to display: 211 B] ================================================ FILE: testing/sqltests/tests/trigger-before-insert-affinity.sqltest ================================================ [File too large to display: 2.9 KB] ================================================ FILE: testing/sqltests/tests/trigger-last-insert-rowid.sqltest ================================================ [File too large to display: 2.6 KB] ================================================ FILE: testing/sqltests/tests/trigger-quoted-identifiers.sqltest ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/trigger-virtual-table-innocuous.sqltest ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/trigger.sqltest ================================================ [File too large to display: 73.8 KB] ================================================ FILE: testing/sqltests/tests/trigger_attach_regression.sqltest ================================================ [File too large to display: 21.2 KB] ================================================ FILE: testing/sqltests/tests/trigger_fk_cascade.sqltest ================================================ [File too large to display: 4.4 KB] ================================================ FILE: testing/sqltests/tests/trigger_on_conflict.sqltest ================================================ [File too large to display: 18.6 KB] ================================================ FILE: testing/sqltests/tests/unnest-exists.sqltest ================================================ [File too large to display: 20.6 KB] ================================================ FILE: testing/sqltests/tests/update-returning-correlated-subquery.sqltest ================================================ [File too large to display: 1.9 KB] ================================================ FILE: testing/sqltests/tests/update.sqltest ================================================ [File too large to display: 26.1 KB] ================================================ FILE: testing/sqltests/tests/update_attached_db_index.sqltest ================================================ [File too large to display: 2.6 KB] ================================================ FILE: testing/sqltests/tests/update_expression_index.sqltest ================================================ [File too large to display: 10.1 KB] ================================================ FILE: testing/sqltests/tests/update_expression_index_affinity.sqltest ================================================ [File too large to display: 3.6 KB] ================================================ FILE: testing/sqltests/tests/update_index_affinity.sqltest ================================================ [File too large to display: 2.5 KB] ================================================ FILE: testing/sqltests/tests/update_or_replace_rowid_secondary_index.sqltest ================================================ [File too large to display: 1.3 KB] ================================================ FILE: testing/sqltests/tests/upsert-expr-index.sqltest ================================================ [File too large to display: 3.0 KB] ================================================ FILE: testing/sqltests/tests/upsert.sqltest ================================================ [File too large to display: 21.5 KB] ================================================ FILE: testing/sqltests/tests/vacuum_into.sqltest ================================================ [File too large to display: 370 B] ================================================ FILE: testing/sqltests/tests/values.sqltest ================================================ [File too large to display: 2.0 KB] ================================================ FILE: testing/sqltests/tests/view-rowid.sqltest ================================================ [File too large to display: 300 B] ================================================ FILE: testing/sqltests/tests/views.sqltest ================================================ [File too large to display: 7.2 KB] ================================================ FILE: testing/sqltests/tests/virtual-table-left-join.sqltest ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/tests/where/default.sqltest ================================================ [File too large to display: 16.1 KB] ================================================ FILE: testing/sqltests/tests/where/memory.sqltest ================================================ [File too large to display: 763 B] ================================================ FILE: testing/sqltests/tests/where/small.sqltest ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqltests/tests/window/default.sqltest ================================================ [File too large to display: 14.6 KB] ================================================ FILE: testing/sqltests/tests/window/group-by-order-by-partition.sqltest ================================================ [File too large to display: 1.9 KB] ================================================ FILE: testing/sqltests/tests/window/memory.sqltest ================================================ [File too large to display: 8.3 KB] ================================================ FILE: testing/sqltests/tests/window-agg-row-value.sqltest ================================================ [File too large to display: 410 B] ================================================ FILE: testing/sqltests/tests/window-selfjoin-reset-sorter.sqltest ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/turso-tests/alter_column.sqltest ================================================ [File too large to display: 15.8 KB] ================================================ FILE: testing/sqltests/turso-tests/analyze.sqltest ================================================ [File too large to display: 2.8 KB] ================================================ FILE: testing/sqltests/turso-tests/array-bugs.sqltest ================================================ [File too large to display: 4.3 KB] ================================================ FILE: testing/sqltests/turso-tests/array-edge-cases.sqltest ================================================ [File too large to display: 43.8 KB] ================================================ FILE: testing/sqltests/turso-tests/array.sqltest ================================================ [File too large to display: 37.7 KB] ================================================ FILE: testing/sqltests/turso-tests/attach/cross_db_views_rejection.sqltest ================================================ [File too large to display: 4.7 KB] ================================================ FILE: testing/sqltests/turso-tests/attach/default.sqltest ================================================ [File too large to display: 934 B] ================================================ FILE: testing/sqltests/turso-tests/attach/memory.sqltest ================================================ [File too large to display: 1.7 KB] ================================================ FILE: testing/sqltests/turso-tests/attach/small.sqltest ================================================ [File too large to display: 355 B] ================================================ FILE: testing/sqltests/turso-tests/attach/writes.sqltest ================================================ [File too large to display: 41.9 KB] ================================================ FILE: testing/sqltests/turso-tests/builtin_pg_types.sqltest ================================================ [File too large to display: 8.2 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_type_alter_default.sqltest ================================================ [File too large to display: 2.4 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_type_datetime_validation.sqltest ================================================ [File too large to display: 1.9 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_type_default_values.sqltest ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_type_deterministic.sqltest ================================================ [File too large to display: 1.5 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_type_expr_index.sqltest ================================================ [File too large to display: 5.8 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_type_notnull_decode.sqltest ================================================ [File too large to display: 2.9 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_type_operators.sqltest ================================================ [File too large to display: 22.2 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_type_ordering.sqltest ================================================ [File too large to display: 12.5 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_type_upsert_trigger.sqltest ================================================ [File too large to display: 2.3 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_types.sqltest ================================================ [File too large to display: 45.3 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_types_fk_cascade.sqltest ================================================ [File too large to display: 5.9 KB] ================================================ FILE: testing/sqltests/turso-tests/custom_types_non_strict.sqltest ================================================ [File too large to display: 3.5 KB] ================================================ FILE: testing/sqltests/turso-tests/fts.sqltest ================================================ [File too large to display: 28.8 KB] ================================================ FILE: testing/sqltests/turso-tests/json_object_star.sqltest ================================================ [File too large to display: 6.4 KB] ================================================ FILE: testing/sqltests/turso-tests/materialized_view_text_arithmetic.sqltest ================================================ [File too large to display: 1.4 KB] ================================================ FILE: testing/sqltests/turso-tests/materialized_views.sqltest ================================================ [File too large to display: 72.5 KB] ================================================ FILE: testing/sqltests/turso-tests/matview_create_same_txn.sqltest ================================================ [File too large to display: 662 B] ================================================ FILE: testing/sqltests/turso-tests/matview_insert_or_replace.sqltest ================================================ [File too large to display: 805 B] ================================================ FILE: testing/sqltests/turso-tests/matview_integrity_check.sqltest ================================================ [File too large to display: 618 B] ================================================ FILE: testing/sqltests/turso-tests/multi-column-subquery-comparison.sqltest ================================================ [File too large to display: 1.1 KB] ================================================ FILE: testing/sqltests/turso-tests/mvcc_feature_compat.sqltest ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/turso-tests/mvcc_left_join_null_row.sqltest ================================================ [File too large to display: 514 B] ================================================ FILE: testing/sqltests/turso-tests/on_conflict_constraint.sqltest ================================================ [File too large to display: 1.4 KB] ================================================ FILE: testing/sqltests/turso-tests/placeholder.sqltest ================================================ [File too large to display: 77 B] ================================================ FILE: testing/sqltests/turso-tests/raise.sqltest ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/sqltests/turso-tests/regexp.sqltest ================================================ [File too large to display: 3.0 KB] ================================================ FILE: testing/sqltests/turso-tests/row-value-adversarial.sqltest ================================================ [File too large to display: 8.0 KB] ================================================ FILE: testing/sqltests/turso-tests/strict_check_types.sqltest ================================================ [File too large to display: 12.8 KB] ================================================ FILE: testing/sqltests/turso-tests/strict_custom_type_input_validation.sqltest ================================================ [File too large to display: 6.2 KB] ================================================ FILE: testing/sqltests/turso-tests/time.sqltest ================================================ [File too large to display: 13.2 KB] ================================================ FILE: testing/sqltests/turso-tests/vector.sqltest ================================================ [File too large to display: 4.4 KB] ================================================ FILE: testing/stress/Cargo.toml ================================================ [File too large to display: 1.0 KB] ================================================ FILE: testing/stress/Dockerfile.antithesis-config ================================================ [File too large to display: 58 B] ================================================ FILE: testing/stress/docker-compose.yaml ================================================ [File too large to display: 205 B] ================================================ FILE: testing/stress/docker-entrypoint.sh ================================================ [File too large to display: 150 B] ================================================ FILE: testing/stress/lib.rs ================================================ [File too large to display: 1.2 KB] ================================================ FILE: testing/stress/main.rs ================================================ [File too large to display: 33.8 KB] ================================================ FILE: testing/stress/opts.rs ================================================ [File too large to display: 2.7 KB] ================================================ FILE: testing/stress/run-miri.sh ================================================ [File too large to display: 130 B] ================================================ FILE: testing/stress/tests/shuttle_mvcc.rs ================================================ [File too large to display: 23.5 KB] ================================================ FILE: testing/stress-go/README.md ================================================ [File too large to display: 2.3 KB] ================================================ FILE: testing/stress-go/go.mod ================================================ [File too large to display: 570 B] ================================================ FILE: testing/stress-go/go.sum ================================================ [File too large to display: 2.2 KB] ================================================ FILE: testing/stress-go/main.go ================================================ [File too large to display: 15.6 KB] ================================================ FILE: testing/system/all.test ================================================ [File too large to display: 84 B] ================================================ FILE: testing/system/gen-bigass-database.py ================================================ [File too large to display: 17.4 KB] ================================================ FILE: testing/system/gen-database.py ================================================ [File too large to display: 1.6 KB] ================================================ FILE: testing/system/tester.tcl ================================================ [File too large to display: 11.7 KB] ================================================ FILE: testing/system/vtab.test ================================================ [File too large to display: 14.3 KB] ================================================ FILE: testing/unreliable-libc/Makefile ================================================ [File too large to display: 383 B] ================================================ FILE: testing/unreliable-libc/file.c ================================================ [File too large to display: 4.9 KB] ================================================ FILE: tests/Cargo.toml ================================================ [File too large to display: 1.4 KB] ================================================ FILE: tests/README.md ================================================ [File too large to display: 153 B] ================================================ FILE: tests/fuzz/cte.rs ================================================ [File too large to display: 72.2 KB] ================================================ FILE: tests/fuzz/custom_types.rs ================================================ [File too large to display: 65.2 KB] ================================================ FILE: tests/fuzz/expression_index.rs ================================================ [File too large to display: 9.4 KB] ================================================ FILE: tests/fuzz/grammar_generator.rs ================================================ [File too large to display: 14.0 KB] ================================================ FILE: tests/fuzz/helpers.rs ================================================ [File too large to display: 8.1 KB] ================================================ FILE: tests/fuzz/join.rs ================================================ [File too large to display: 14.7 KB] ================================================ FILE: tests/fuzz/journal_mode.rs ================================================ [File too large to display: 15.5 KB] ================================================ FILE: tests/fuzz/mod.rs ================================================ [File too large to display: 339.4 KB] ================================================ FILE: tests/fuzz/orderby_collation.rs ================================================ [File too large to display: 23.3 KB] ================================================ FILE: tests/fuzz/raise.rs ================================================ [File too large to display: 15.1 KB] ================================================ FILE: tests/fuzz/rowid_alias.rs ================================================ [File too large to display: 7.3 KB] ================================================ FILE: tests/fuzz/savepoint.rs ================================================ [File too large to display: 18.4 KB] ================================================ FILE: tests/fuzz/subjournal.rs ================================================ [File too large to display: 48.8 KB] ================================================ FILE: tests/fuzz/subquery.rs ================================================ [File too large to display: 21.3 KB] ================================================ FILE: tests/fuzz/test_join_optimizer.rs ================================================ [File too large to display: 17.8 KB] ================================================ FILE: tests/integration/assert_details.rs ================================================ [File too large to display: 5.8 KB] ================================================ FILE: tests/integration/common.rs ================================================ [File too large to display: 40.8 KB] ================================================ FILE: tests/integration/conflict_resolution.rs ================================================ [File too large to display: 169.3 KB] ================================================ FILE: tests/integration/custom_types.rs ================================================ [File too large to display: 17.9 KB] ================================================ FILE: tests/integration/functions/mod.rs ================================================ [File too large to display: 86 B] ================================================ FILE: tests/integration/functions/test_cdc.rs ================================================ [File too large to display: 59.5 KB] ================================================ FILE: tests/integration/functions/test_function_rowid.rs ================================================ [File too large to display: 2.3 KB] ================================================ FILE: tests/integration/functions/test_sum.rs ================================================ [File too large to display: 2.3 KB] ================================================ FILE: tests/integration/functions/test_uuid.rs ================================================ [File too large to display: 1.9 KB] ================================================ FILE: tests/integration/functions/test_wal_api.rs ================================================ [File too large to display: 31.8 KB] ================================================ FILE: tests/integration/fuzz_transaction/mod.rs ================================================ [File too large to display: 52.0 KB] ================================================ FILE: tests/integration/index_method/mod.rs ================================================ [File too large to display: 80.5 KB] ================================================ FILE: tests/integration/integrity_check.rs ================================================ [File too large to display: 65.3 KB] ================================================ FILE: tests/integration/mod.rs ================================================ [File too large to display: 522 B] ================================================ FILE: tests/integration/mvcc.rs ================================================ [File too large to display: 30.9 KB] ================================================ FILE: tests/integration/pragma.rs ================================================ [File too large to display: 15.6 KB] ================================================ FILE: tests/integration/query_processing/encryption.rs ================================================ [File too large to display: 35.9 KB] ================================================ FILE: tests/integration/query_processing/mod.rs ================================================ [File too large to display: 334 B] ================================================ FILE: tests/integration/query_processing/test_alter_table_reopen.rs ================================================ [File too large to display: 4.1 KB] ================================================ FILE: tests/integration/query_processing/test_btree.rs ================================================ [File too large to display: 16.3 KB] ================================================ FILE: tests/integration/query_processing/test_ddl.rs ================================================ [File too large to display: 6.4 KB] ================================================ FILE: tests/integration/query_processing/test_expr_index.rs ================================================ [File too large to display: 2.9 KB] ================================================ FILE: tests/integration/query_processing/test_hash_join_materialization.rs ================================================ [File too large to display: 10.0 KB] ================================================ FILE: tests/integration/query_processing/test_materialized_subquery.rs ================================================ [File too large to display: 1.4 KB] ================================================ FILE: tests/integration/query_processing/test_multi_thread.rs ================================================ [File too large to display: 11.8 KB] ================================================ FILE: tests/integration/query_processing/test_page1.rs ================================================ [File too large to display: 349 B] ================================================ FILE: tests/integration/query_processing/test_read_path.rs ================================================ [File too large to display: 32.8 KB] ================================================ FILE: tests/integration/query_processing/test_schema_updated.rs ================================================ [File too large to display: 5.8 KB] ================================================ FILE: tests/integration/query_processing/test_transactions.rs ================================================ [File too large to display: 63.5 KB] ================================================ FILE: tests/integration/query_processing/test_type_affinity.rs ================================================ [File too large to display: 5.6 KB] ================================================ FILE: tests/integration/query_processing/test_vacuum.rs ================================================ [File too large to display: 75.9 KB] ================================================ FILE: tests/integration/query_processing/test_write_path.rs ================================================ [File too large to display: 78.9 KB] ================================================ FILE: tests/integration/statement_reset.rs ================================================ [File too large to display: 5.2 KB] ================================================ FILE: tests/integration/stmt_journal.rs ================================================ [File too large to display: 25.1 KB] ================================================ FILE: tests/integration/storage/autovacuum.rs ================================================ [File too large to display: 1.5 KB] ================================================ FILE: tests/integration/storage/checksum.rs ================================================ [File too large to display: 3.6 KB] ================================================ FILE: tests/integration/storage/header_version.rs ================================================ [File too large to display: 34.1 KB] ================================================ FILE: tests/integration/storage/mod.rs ================================================ [File too large to display: 129 B] ================================================ FILE: tests/integration/storage/short_read.rs ================================================ [File too large to display: 7.5 KB] ================================================ FILE: tests/integration/trigger.rs ================================================ [File too large to display: 69.3 KB] ================================================ FILE: tests/integration/wal/mod.rs ================================================ [File too large to display: 14 B] ================================================ FILE: tests/integration/wal/test_wal.rs ================================================ [File too large to display: 7.4 KB] ================================================ FILE: tests/lib.rs ================================================ [File too large to display: 75 B] ================================================ FILE: tlaplus/sqlite-tx/.gitignore ================================================ [File too large to display: 27 B] ================================================ FILE: tlaplus/sqlite-tx/Makefile ================================================ [File too large to display: 651 B] ================================================ FILE: tlaplus/sqlite-tx/README.md ================================================ [File too large to display: 320 B] ================================================ FILE: tlaplus/sqlite-tx/SqliteTx.cfg ================================================ [File too large to display: 413 B] ================================================ FILE: tlaplus/sqlite-tx/SqliteTx.tla ================================================ [File too large to display: 8.5 KB] ================================================ FILE: tools/dbhash/Cargo.toml ================================================ [File too large to display: 425 B] ================================================ FILE: tools/dbhash/README.md ================================================ [File too large to display: 718 B] ================================================ FILE: tools/dbhash/src/encoder.rs ================================================ [File too large to display: 8.6 KB] ================================================ FILE: tools/dbhash/src/lib.rs ================================================ [File too large to display: 5.5 KB] ================================================ FILE: tools/dbhash/src/main.rs ================================================ [File too large to display: 1.5 KB] ================================================ FILE: tools/dbhash/tests/README.md ================================================ [File too large to display: 746 B] ================================================ FILE: tools/dbhash/tests/sqlite_compat.rs ================================================ [File too large to display: 15.3 KB]